Skip to content

Add coal plant controller to Hycon#102

Draft
genevievestarke wants to merge 8 commits into
NatLabRockies:developfrom
genevievestarke:feature/coal_controller
Draft

Add coal plant controller to Hycon#102
genevievestarke wants to merge 8 commits into
NatLabRockies:developfrom
genevievestarke:feature/coal_controller

Conversation

@genevievestarke
Copy link
Copy Markdown
Collaborator

This PR adds a coal controller to Hycon. The controller takes in the day ahead LMP price, a plant status signal (0 if the plant is off, 1 if the plant is on), and a plant power bid curve.
If the plant is on, then the power set point is set according to the bid curve. If the plant is off, the power set point is zero.

To do:

  • Code first draft of controller
  • Code controller interface
  • Add simple example from use case

@misi9170
Copy link
Copy Markdown
Collaborator

misi9170 commented May 1, 2026

Thanks for working on this @genevievestarke ! Heads up that #100 makes some fairly significant changes to the Hercules interface, so we may need to revamp this somewhat once that is merged. We've been doing some testing with it and I think it's close, so perhaps I'll try to wrap up some last tasks on that today and try to get it on by early next week so that you can (hopefully) move forward unimpeded. Still, the logic for the coal plant controller shouldn't change.

I'm happy to talk you through #100 so that you know what's coming.

My other thought: is the logic you're implementing specific to coal, or is it likely going to hold for other types of thermal units, too?

@misi9170
Copy link
Copy Markdown
Collaborator

misi9170 commented May 6, 2026

I've now merged #100, and it looks like there is a merge conflict on the Hercules interface, which isn't surprising. I can try to resolve that if you'd like @genevievestarke ?

@genevievestarke
Copy link
Copy Markdown
Collaborator Author

Ok, I've merged in develop! It would be great to get your thoughts on the external signal handling, @misi9170!

Copy link
Copy Markdown
Collaborator

@misi9170 misi9170 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @genevievestarke , just going through and adding various comments/ideas but I'll work on that externally provided forced outage (I think it's what you already have, maybe we just workshop the name "plant_status" a bit!)

# self.low_soc = low_soc
self.bid_curve = bid_curve
prices, powers = zip(*bid_curve)
self.bid_interpolator = interp1d(prices, powers, kind="quadratic", fill_value="extrapolate")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that a bid curve is not a smooth function but something more step-like, for example:

  • Willing to produce 200--400 MW for $20/MWh
  • Willing to produce 400--500 MW for $25/MWh

So:

  • if the plant clears both, it will produce 500MW at whatever the clearing price is
  • if it's the marginal generator at the $25/MWh price, it will produce, say, 430 MW at $25/MWh (depending on how much the market needs to meet demand)
  • if it's the marginal generator at the $20/MWh price, it will produce, say, 270 MW at $20/MWh (again, depending on demand)
  • If the clearing price is below $20, it will produce zero power

At least, in an idealized case, ignoring any "make-whole" bids or hour-to-hour constraints, etc.

But it seems like fitting a quadratic will give it a smooth bid curve?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One issue here, though, is that is the plant is the marginal generator, we're going to need a SCED signal to know at what level the plant should be operating.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presumably we'll just revert this file prior to merger? Fine to leave the print statements in for the time being if you're using them for debugging

Comment on lines +88 to +95
# # Coal plant parameters
# if self._has_coal_component:
# self.plant_parameters["coal_plant"] = {
# "capacity": h_dict["coal_plant"]["rated_capacity"],
# "min_stable_load": h_dict["coal_plant"]["min_stable_load_fraction"] *
# h_dict["coal_plant"]["rated_capacity"]
# }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reminder to remove prior to merger

Comment on lines +170 to +176
# TODO: @Misha, is there a better way to do this with the new interface?
if "coal_power_reference" in h_dict["external_signals"]:
for c in h_dict["component_names"]:
if self.component_types[c] in hercules_thermal_types:
measurements[c]["power_reference"] = h_dict["external_signals"][
"coal_power_reference"
]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, there isn't a better way, but I guess we need to determine whether this is necessary or not. Usually, we shouldn't need to provide an "external" power reference---the power setpoint is either determined by the controller itself (say, based on the bid curve) OR the power setpoint is passed down from a higher-level hybrid plant controller. In either case, the power setpoint for the coal component isn't set ahead of time and passed in.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm now realizing we may need something like this to mimic a SCED signal if the plant is the marginal generator

power_bids = self.bid_interpolator(day_ahead_lmp)
plant_status = measurements_dict[self.cname]["status_reference"]

external_power_reference = measurements_dict[self.cname]["power_reference"] / 1e3
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine here, but this should be for cases where the power_reference is set by a higher-level hybrid plant controller, not by an external signal (I think). Happy to discuss more on that though

Comment on lines +75 to +80
# # print("Capacity:", self.plant_parameters[self.cname]["capacity"])
# print("Min stable load:", self.plant_parameters[self.cname]["min_stable_load"])
# print(f"Day-ahead LMP: {day_ahead_lmp}, Power bid from curve: {power_bids},
# Plant status: {plant_status}")
# print(f"Minimum power value based on min stable load: {min_power_value}")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reminder to remove prior to merger

# NOTE: Current power calculation is in MW!!
day_ahead_lmp = measurements_dict["DA_LMP"]
power_bids = self.bid_interpolator(day_ahead_lmp)
plant_status = measurements_dict[self.cname]["status_reference"]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll work on options for this---I think we want this to be something that can be set either in external signals as a forced outage, or by a hybrid plant controller that is committing different types of generators

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants