-
Notifications
You must be signed in to change notification settings - Fork 38
System level control framework #751
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0abe06f
f65d00f
255b7c3
ade5a20
27ca0a9
85eda16
696ffe6
aacc334
a2fe782
3d0e272
d13d25a
c51d33d
0e9dd0b
0cedd82
6a51fe1
0aff134
9ff6fae
08f8790
d84566f
733c636
d91e8ae
fc6bd5f
efd3093
59f9b16
0fdfde2
2e428b3
237df21
cb9d053
1ef9743
748be32
f8294c6
3bcbc2f
6dff236
c2123f8
9df4fd7
9567318
8910d36
553ed20
5ca4d15
2a741ba
8b6f2c1
23a251a
b271026
c4d95bb
8de0ff1
7a4752a
5881e07
3cf8cd5
d084f69
6010b5c
d520335
a11e37c
db9c9c0
c101048
887873f
93a69c0
1085a68
e4a94bd
9a5cfdc
afb0af3
a6a841d
f06736e
9984c70
18732aa
fb219d5
4ee228b
9108d5d
770e6e3
f2e023b
a56a57f
0f5e5e7
916f369
494475f
e8b0319
8fd6a76
2432793
e565bc3
4bbab17
909af6d
ae3b8d1
add6a4e
0f7d6b9
be846a0
1f2e5cc
8f77755
aac36d4
883c0b9
66c655e
b6b1415
f46e7c8
b34bb91
0caae7a
4fb8990
31711d4
db182b5
9bae204
a50ba5a
b04ece2
66e1153
df47e4f
ebb9abb
b427be8
59b1676
3a20fcf
503c606
4deb91d
2b7fed5
da11a52
92336b1
6eeac71
9beec63
f763302
12f0b11
6af2397
dee1925
37be44e
dc7bbf8
416dc5d
7deabec
c8ecead
b6003fe
2b4f835
e8cf31f
654a008
0263a58
85c8679
26b5e74
ecf4273
4fbd901
4deb083
ecec21f
8c968a4
f08382c
1d51984
784e937
83577e1
8dbd7bd
b2f13c0
67c43b9
dfe4cf4
270b4d8
4f5b911
f4b8401
3286f5e
585c87c
181445a
fc89753
28a0374
010800d
5c7f447
d1da302
2705b18
fa9ef61
74cf077
60a9355
0022143
d38dc79
9ecca8d
8767b4a
f592e92
b88db1d
520a376
e209414
38abfc2
68bc172
c4be629
d3133d9
de3d2f3
fbe1086
754fca6
74ebb58
5a11de0
34c4903
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| # System Level Control Technology Performance Classifiers | ||
|
|
||
| To enable a generic system level control framework we need to classify each technology based on how the model that is included in H2I can operate within the system. | ||
|
|
||
| ```{note} | ||
| While in real life there are a lot of controllable parameters allowing for ramping production up or down for a particular technology (e.g., wind or solar curtailment), the model of that technology in H2I might not be capable of the same response behavior to input signals. | ||
| These classifications are for specific H2I dispatch formulations and are based on how the models in H2I are implemented, **not always** on how the actual physical subsystem might operate. | ||
| This is a useful and necessary distinction that delineates different model capabilities clearly. | ||
| ``` | ||
|
|
||
| We have identified five key classifiers that are able to represent the different behaviors that we can expect from the models. Each performance model includes a parameter setting the classifier `_control_classifier`. | ||
|
|
||
| Classifier | Meaning | Example Technology Models | ||
| -- | -- | -- | ||
| fixed | Always produces commodity and cannot be controlled or reduced; does not receive a set-point | classical nuclear | ||
| flexible | Resource-driven; can only be *reduced* (curtailed) below the resource-determined maximum via a set-point | wind, solar | ||
| dispatchable | Can modulate production within bounds in response to a set-point | grid, electrolyzer, NG turbine | ||
| storage | Can modulate consumption/production within bounds while tracking SOC | battery, h2 storage, any storage | ||
| feedstock | Are not directly controlled, but useful for SLC to make dispatch decisions | feedstocks | ||
|
|
||
| To add a classifier for a particular model it would look something like this in the class: | ||
| ```{python} | ||
| _control_classifier = "flexible" | ||
| ``` | ||
|
|
||
| ```{note} | ||
| **Flexible vs. dispatchable.** Both classifiers receive a `{commodity}_set_point` from the system-level controller, so the distinction is about *what the set-point can do*. A flexible model is a strictly more restricted case of a dispatchable one: the set-point can only *cap* the output below whatever the underlying resource (sun, wind, etc.) makes available. A dispatchable model, by contrast, can be ramped up or down anywhere within its operating bounds in direct response to the set-point. | ||
| ``` | ||
|
|
||
| ## Fixed | ||
| A fixed performance model represents anything that always produces at its rated capacity and cannot be controlled or reduced by the system level controller. The SLC reads the output from a fixed technology and subtracts it from the demand, but does not send a set-point back to the technology. A good example of this is a classical nuclear plant model: it produces a constant output that the rest of the system must accommodate. | ||
|
|
||
| ## Flexible | ||
| A flexible performance model represents anything whose production is determined by an external resource (e.g., wind speed, solar irradiance) and that can only be *reduced* below that resource-determined maximum and never increased above it. The system-level controller sends a `{commodity}_set_point` that acts as an upper bound: when the resource-driven output exceeds the set-point, output is curtailed down to the set-point; otherwise, output is left at the resource-driven value. A good example is the PVWatts PySAM solar plant in H2I; its performance is a function of the input solar resource, and we cannot tell the sun to shine more, but we can curtail the panel output below the available solar production. | ||
|
|
||
| In other words, flexible is a strictly more restricted case of [dispatchable](#dispatchable): a dispatchable model can be ramped both up and down in response to a set-point, while a flexible model can only be ramped down. | ||
|
|
||
| To simplify the implementation of applying this curtailment we added a method, `apply_curtailment()`, to the `PerformanceBaseClass`. | ||
|
|
||
| ```{figure} figures/curtailable.png | ||
| :width: 70% | ||
| :align: center | ||
| ``` | ||
|
|
||
| ### Apply curtailment based on set_point | ||
| Within the `compute()` method in the performance model you can apply the curtailment using the `apply_curtailment()` method. | ||
| ``` | ||
| self.apply_curtailment(outputs) | ||
| ``` | ||
| which applies curtailment to `{commodity}_out` based on `{commodity}_set_point`. This adds `uncurtailed_{commodity}_out` and `{commodity}_out` as outputs from the performance model. | ||
|
|
||
| (dispatchable)= | ||
| ## Dispatchable | ||
| A dispatchable performance model represents anything that can be ramped both *up and down* within its operating bounds in response to a `{commodity}_set_point` from the system-level controller. Unlike a [flexible](#flexible) model, the set-point for a dispatchable model can request any production level within the model's rated capacity (and minimum load, if applicable), and the model will produce at that level. Examples include a grid connection, an electrolyzer, or a natural-gas turbine. | ||
|
|
||
| There aren't additional special methods to handle this because the set-point response is internal to each performance model. | ||
|
|
||
| ```{figure} figures/dispatchable.png | ||
| :width: 70% | ||
| :align: center | ||
| ``` | ||
|
|
||
| ## Storage | ||
| Storage is a unique control classifier because it assumes that within the model that energy isn't created or destroyed (minus some efficiency losses). While it's technically "dispatchable" in that it can receive and change its performance based on a set point, its handling within H2I is unique because it's attached to storage performance models, which is handled differently than converter performance models. A converter model only has positive (or zero) `{commodity}_out`, whereas a storage model can have positive or negative `{commodity}_out`. | ||
|
|
||
| There are two types of cases for the storage control classifier: | ||
| 1. **with a storage controller** | ||
| When the storage performance model is controlled with a storage-level controller (open-loop or feedback controlled), the system-level controller outputs combined demand, that is always positive to the storage-level controller. The demand is `{commodity}_in` from the technologies upstream of the storage that output the same commodity to the storage performance model and the `remaining_demand`. | ||
|
jaredthomas68 marked this conversation as resolved.
|
||
|
|
||
| 2. **without a storage controller** | ||
| The system-level controller outputs set points to the storage performance model which can be considered charge (negative) and discharge (positive) commands (storage-level set points) to the storage performance model, directly. | ||
|
|
||
|
|
||
| ```{figure} figures/storage.png | ||
| :width: 85% | ||
| :align: center | ||
| ``` | ||
|
|
||
| ## Feedstock | ||
| Feedstocks represent commodity *inputs* to the controllable system: they are consumed by other technologies but their availability is not itself something the controller can adjust. Although feedstocks themselves cannot be dispatched, knowing how much of each feedstock is available is valuable information for more advanced controllers, since feedstock supply can constrain what the controllable technologies are actually able to produce. | ||
|
|
||
| For example, consider an ammonia plant that consumes both hydrogen and nitrogen. If the nitrogen feedstock supply is insufficient to meet the ammonia demand, the ammonia output is capped by the nitrogen availability regardless of how much hydrogen and electricity are produced. A controller that is aware of the nitrogen feedstock can recognize that the ammonia demand cannot be met, and can adjust the set-points for the hydrogen and electricity technologies accordingly (e.g., avoiding over-production of hydrogen that would otherwise go unused). This is why feedstocks are classified separately rather than being ignored by the controller: they are uncontrollable, but they are not irrelevant. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| # Control Strategies | ||
| There are several simple control strategies already implemented in the SLC paradigm. While fairly simplistic, they are meant to illustrate how information can be passed from different blocks/components (converters, storage, feedstocks, demand, etc.) and models (performance, cost, finance) to use within the SLC. | ||
|
|
||
| The current control strategies are: | ||
| 1. [Demand Following](#slc-demand-following) | ||
| 2. [Profit Maximization](#slc-profit-max) | ||
| 3. [Cost Minimization](#slc-cost-min) | ||
|
|
||
| ```{note} | ||
| The strategies currently implemented are experimental and will likely require further development for specific analyses. | ||
| ``` | ||
|
|
||
| All control strategies inherit `SystemLevelControlBase`, which is a base class that has common setup logic shared by all system-level control strategies. | ||
|
|
||
| See additional information, which is more developer focused, about the [`SystemLevelControlBase`](#slc-base). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| --- | ||
| jupytext: | ||
| text_representation: | ||
| extension: .md | ||
| format_name: myst | ||
| format_version: 0.13 | ||
| jupytext_version: 1.18.1 | ||
| kernelspec: | ||
| display_name: Python 3.11.13 ('h2i_env') | ||
| language: python | ||
| name: python3 | ||
| --- | ||
|
|
||
| (slc-cost-min)= | ||
| # Cost Minimization System Level Controller | ||
|
|
||
| The cost minimization controller, `CostMinimizationControl`, meets demand at minimum variable cost using merit-order dispatch. | ||
| Unlike the {ref}`demand following controller <slc-demand-following>`, which splits demand evenly across dispatchable technologies, this controller dispatches the cheapest technologies first. | ||
|
vijay092 marked this conversation as resolved.
|
||
|
|
||
| ## Dispatch Logic | ||
|
|
||
| The controller follows a three-step dispatch process: | ||
|
|
||
| 1. **Flexible technologies** run at their available capacity (assumed zero marginal cost). Their output is subtracted from the demand. | ||
| 2. **Storage technologies** absorb any surplus (charging) or provide the deficit (discharging). Residual demand is split evenly across storage technologies producing the demanded commodity. | ||
| 3. **Dispatchable technologies** are dispatched by cheapest marginal cost first, each up to its rated capacity, until the remaining demand is met. | ||
|
|
||
| ## Marginal Cost Configuration | ||
|
|
||
| Marginal costs are specified per dispatchable technology in the `cost_per_tech` dictionary under `system_level_control.control_parameters` in the plant config. Each entry can be: | ||
|
|
||
| | Value | Description | | ||
| | --- | --- | | ||
| | Numeric (e.g. `0.05`) | Constant marginal cost in `$/(commodity_amount_units)` | | ||
| | `"buy_price"` | Uses the technology's configured purchase price | | ||
| | `"VarOpEx"` | Derives marginal cost from the technology's variable operating expenditure divided by total production | | ||
| | `"feedstock"` | Sums upstream feedstock `VarOpEx` values and divides by the technology's total production | | ||
|
|
||
| ```{note} | ||
| The dispatch order is determined by sorting dispatchable technologies by their **mean** marginal cost across all timesteps (cheapest first). | ||
| ``` | ||
|
|
||
| ### Example Configuration | ||
|
|
||
| ```yaml | ||
| system_level_control: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wondering if the system level control should be called "dispatch" and the local control called "control", where actual control decisions are made. I'm not sure we won't have true control at the system level or dispatch at the tech level, so this is just a thought to gather comments from others. It would simplify naming conventions: dispatch making set point decisions, control making path decisions to achieve those set points.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I could see that delineation being useful, primarily to be more precise when discussing system-level control vs. tech-level control. That being said, I do like how broad and inclusive "control" is as a term. @elenya-grant and @kbrunik and others, what do you think of Jared's suggested delineation here?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like the use of
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree that it would clarify things to go with |
||
| control_strategy: CostMinimizationControl | ||
| control_parameters: | ||
| cost_per_tech: | ||
| natural_gas_plant: feedstock | ||
| ``` | ||
|
|
||
| ## Inputs and Outputs | ||
|
|
||
| In addition to the standard inputs inherited from `SystemLevelControlBase`, the cost minimization controller adds marginal cost inputs based on the `cost_per_tech` configuration (see above). | ||
|
|
||
| The base inputs for technologies classified as `flexible`, `dispatchable`, and `storage` are: | ||
|
|
||
| - `f"{tech_name}_{tech_output_commodity}_out"` | ||
| - `f"{tech_name}_rated_{tech_output_commodity}_production"` | ||
| - `f"{tech_name}_{tech_output_commodity}_demand"` | ||
|
|
||
| ## Limitations | ||
|
|
||
| - Greedy dispatch: The merit-order approach is greedy - it does not look ahead across timesteps to optimize total cost over the simulation horizon. | ||
| - Even splitting across storage: Residual demand is split evenly across storage technologies regardless of capacity or state of charge. | ||
| - Demand is always met: Unlike the {ref}`profit maximization controller <slc-profit-max>`, this controller always attempts to meet demand regardless of cost. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| --- | ||
| jupytext: | ||
| text_representation: | ||
| extension: .md | ||
| format_name: myst | ||
| format_version: 0.13 | ||
| jupytext_version: 1.18.1 | ||
| kernelspec: | ||
| display_name: Python 3.11.13 ('h2i_env') | ||
| language: python | ||
| name: python3 | ||
| --- | ||
|
|
||
| (slc-demand-following)= | ||
| # Demand Following System Level Controller | ||
|
|
||
| The demand following controller, `DemandFollowingControl`, aims to fully meet the demand and does not have any inputs related to cost. | ||
|
|
||
| The N2 diagram below shows an example system using the demand following controller with wind, natural gas, and battery storage technologies. | ||
|
|
||
| ```{code-cell} ipython3 | ||
| :tags: [remove-input] | ||
|
|
||
| from h2integrate.core.h2integrate_model import H2IntegrateModel | ||
| import openmdao.api as om | ||
| import os | ||
|
|
||
| import html | ||
| from pathlib import Path | ||
| from h2integrate import EXAMPLE_DIR | ||
| from IPython.display import HTML, display | ||
|
|
||
| os.chdir(EXAMPLE_DIR / "35_system_level_control/battery_with_controller/") | ||
|
|
||
| h2i_model = H2IntegrateModel("wind_ng_demand.yaml") | ||
| h2i_model.setup() | ||
|
|
||
| om.n2( | ||
| h2i_model.prob, | ||
| outfile="h2i_n2.html", | ||
| display_in_notebook=False, | ||
| show_browser=False, | ||
| ) | ||
|
|
||
| n2_html = "h2i_n2.html" | ||
| n2_srcdoc = html.escape(Path(n2_html).read_text(encoding="utf-8")) | ||
| display( | ||
| HTML( | ||
| f'<div style="width:100%; height:600px; overflow:auto; margin:0; padding:0; border:0;">' | ||
| f'<iframe srcdoc="{n2_srcdoc}" ' | ||
| 'style="display:block; width:200%; height:600px; border:0; margin:0; padding:0; background:transparent;" ' | ||
| 'loading="lazy"></iframe>' | ||
| '</div>' | ||
| ) | ||
| ) | ||
| ``` | ||
| ## Dispatch Logic | ||
|
|
||
| The demand is satisfied in a fixed three-step priority order, and each step's shortfall or surplus is passed to the next: | ||
|
|
||
| 1. **Curtailable techs** run at their available capacity. Their total output is subtracted from the demand, which may drive the residual demand negative (surplus). | ||
|
|
||
| 2. **Storage techs** receive the residual demand (which may be positive or negative). When residual demand is positive the storage is commanded to discharge; when negative it is commanded to charge. If multiple storage techs produce the demanded commodity, the residual demand is | ||
| split **evenly** across them (each receives ``demand / n_storage``). | ||
|
|
||
| 3. **Dispatchable techs** cover any remaining positive demand after storage. The remaining demand (floored at zero) is split **evenly** across all dispatchable techs that produce the demanded commodity (each receives ``remaining_demand / n_dispatchable``). | ||
|
|
||
| ### Example Configuration | ||
|
|
||
| ```yaml | ||
| system_level_control: | ||
| control_strategy: DemandFollowingControl | ||
| solver_options: # solver options for resolving feedback | ||
| solver_name: gauss_seidel | ||
| max_iter: 20 | ||
| convergence_tolerance: 1.0e-6 | ||
| ``` | ||
|
|
||
| ## Inputs and Outputs | ||
|
|
||
| The inputs for technologies classified as `curtailable`, `dispatchable`, and `storage` are: | ||
|
|
||
| - `f"{tech_name}_{tech_output_commodity}_out"` | ||
| - `f"{tech_name}_rated_{tech_output_commodity}_production"` | ||
| - `f"{tech_name}_{tech_output_commodity}_demand"` | ||
|
|
||
| The inputs for technologies classified as `feedstock` are: | ||
| - `f"{tech_name}_{commodity}_out"` | ||
|
|
||
| ## Systems with Heterogeneous Commodities | ||
|
|
||
| The `DemandFollowingControl` controller can be used in hybrid systems where technologies produce different commodities. | ||
| For example, in a system where an electrolyzer produces hydrogen and the demand commodity is hydrogen, the controller can set the electricity-generating *curtailable* technologies' set-points to meet the hydrogen demand. | ||
|
|
||
| This framework provides a starting point for hybrid energy system control but is intended to be extended with more sophisticated strategies for complex multi-commodity systems. | ||
|
|
||
| ## Limitations | ||
|
|
||
| - No cost awareness: The controller dispatches technologies purely to meet demand without considering operational costs, commodity prices, or economic optimization. | ||
| - Even splitting across storage: When multiple storage technologies produce the demanded commodity, the residual demand is divided evenly among them (`demand / n_storage`), regardless of differences in capacity, state of charge, or efficiency. | ||
| - Even splitting across dispatchable technologies: Similarly, any remaining demand after storage dispatch is split evenly across all dispatchable technologies (`remaining_demand / n_dispatchable`), without accounting for marginal costs or capacity constraints. | ||
| - Fixed priority order: The dispatch order (curtailable → storage → dispatchable) is fixed in the current implementation. | ||
|
jaredthomas68 marked this conversation as resolved.
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is in case someone is adding a new model, correct? All of the control classifiers have been set for current models?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is correct, yep! I'll add a note in the
adding_a_new_technology.mdpage to make this more clear