Skip to content

Commit 395ab33

Browse files
elenya-grantjohnjasakbrunikgenevievestarke
authored
System level control framework (#751)
* added test to highlight error * fixed bug of charging with unavailable commodity * added subtests for charging less than available * added test for technology naming and updated logic for using_feedback_control in storage baseclass setup() * other fixes from merge * Add system_level_control to the CI * added control classifiers to key technology models * added curtailable component * added logic in h2imodel to add curtailment component to curtailment control_classifiers models * Working through start of system level controller * Added initial system level control example * Fixed SLC example * Reordering SLC settings * Updating the SLC example * Added curtailment * WIP: adding battery example for SLC * Improving plotting for battery SLC example * Moved combiner and changed battery model * Changed example so wind power is sometimes curtailed after charging battery * updated plotting script * added marker to curtailment component test * added super basic SLC tests to make sure examples dont break * fixed example tests * Minor name clarifications within SLC * Adding a notion of price-considering SLCs * WIP: working on marginal costs for SLC * Updating SLC example to be simpler * Removing prior SLC file * Consolidating methods for SLC * Moving SLC strategy definitions * moved solver options to a config class * minor fixes * Multi-commodity systems for SLC (#717) * minor notes added to h2i model * added test for profit maximization example * updated slc base and h2i to handle multiple commodity streams * added test for multiple commodity streams * added test for multicommodity example, unsure if works * fixed example and updated non-demand commodity units handling in slc baseclass * some fixes in baseclass and tried to add logic for controlling multi-commodities * tried to update control logic to handle multiple commods * working on debugging whatever issue I caused * minor fixes for debugging * fixed bug fully * made electrolyzer capacity smaller * updated electrolyzer to not outputs nans * commented out finances for example 35 with hydrogen * aded output check for nans or infs * cleanups to slc baseclass * added docstrings and inline comments * added tiny comment * minor update to use self.technology_graph attribute * added use of _get_commodity_for_tech * Minor changes for clarity * Combining the three setup methods in SLC base * removed old keys in slc_config --------- Co-authored-by: John Jasa <johnjasa11@gmail.com> * added framework to interface with storage controllers and example * fix so curtailment only applied if using SLC * made slc_config an input option * actually make it so slc get option of slc conig and fixed bug in apply_curtailment * added check for slc in apply_curtailment() method * updated so slc_config is an input option and added control config class for profit max * Adding marginal_cost calcs * replaced lists of tech control classifiers with the dictionary * Directly connected demand profile * Shared commodity sell price as input from finance subgroups * Fixed bad set-point behavior * Uploading complex profit maximization * tried to add helper methods to slc baseclass * added marginal cost to cost models that dont inherit costmodelbaseconfig * updated cost model baseclass handling of marginal cost * updated so that demand is properly output from slc for storage controllers * fixed example 33 * Minor typographical changes * Added control classifiers to all technologies * updated baseclass methods and demand following controller * updated cost control strategies and cleaned up baseclass * updated find_converter_tech method * started adding logic for multi-commods * connected feedstocks * moved logic from compute to method in demand following * updated find_converter_techs and get_upstream_techs_for_commodity * bugfix * Ard control classifier * Updating for changes to slc_config * ard. * test * updated _find_feedstock_techs to use self.feedstock_comps * updated some doc strings in slc baseclass * docs wip * Minor refactor to demand following control * docs * docs * Additional refactoring of demand following control * Refactoring some SLC base methods * Added complex profit max example to test * updated location of cost_per_tech * update SLC add method * update SLC add method * Refactoring some SLC base methods * Expanding SLC docstrings * refactored test_slc_controllers * docs update * more docs * updated no_battery example * cleaned up other slc examples * removed SLC baseconfig since its unused * updated docstring in baseclass * docs added * minor update to demand following and updated profast to handle zero cf * added start of demand following docs * Adding to SLC docs * small doc mods * minor update to doc page * minor clarification in doc * Reconfiguring the connection for varopex * Shifting to five control classifiers * small doc change * connected storage duration * added more subtests to test_slc_examples for the two single-commodity demand following examples * Fixing example dir * fixed failing test * Checkpointing progress on all controllers for techs * Adding passthrough controller file * Updating tests * Added comments to passthrough controller * Fixing import statements * Fixing tests and examples * Partially addressing Jared's PR feedback * Renaming per PR feedback * Added notes in the docs that every tech group has a controller * Updating docs based on PR feedback * Apply suggestions from code review Co-authored-by: genevievestarke <103534902+genevievestarke@users.noreply.github.com> * Updating adding a new tech doc page based on PR review * Merging * Updated control classifier docs * Removing now-defunct storage_techs_to_control parameter * Updating SLC plots and docstrings * Adding curtailment to more techs * Updated SLC battery example regression value * Adding notes that just one demand component is allowed now * Dramatically refactoring, renaming controller based things throughout * Added command value to electrolyzer * Docs build correctly locally * Updating tests and components for new command_value paradigm * Updating tests for set-point changes; simplifying sql timeseries test * Promoting controller parameters up a level * Reverting naming changes * Reverting final changes from naming * doc figures * Dramatic renaming for control nomenclature * doc figures update * PR feedback * Reverting errant storage control logic * inconsequential change to trigger CI --------- Co-authored-by: John Jasa <johnjasa11@gmail.com> Co-authored-by: kbrunik <102193481+kbrunik@users.noreply.github.com> Co-authored-by: kbrunik <kbrunik@gmail.com> Co-authored-by: genevievestarke <103534902+genevievestarke@users.noreply.github.com>
1 parent 344347e commit 395ab33

148 files changed

Lines changed: 5748 additions & 262 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44

5+
- **Controller signal naming overhaul**: Standardized controller input/output names across the codebase. SLC outputs are now `{tech}_{commodity}_set_point` (was `{tech}_{commodity}_demand`); technology-level controllers take `{commodity}_set_point` as input and emit `{commodity}_command_value` as output (was `{commodity}_demand`/`{commodity}_set_point`). Storage performance models in feedback mode receive `{commodity}_set_point`; in open-loop mode they receive `{commodity}_command_value`. The SLC's own input (`{commodity}_demand`) and demand component I/O remain unchanged.
56
- Change commodity in DRI and EAF model from pig iron to sponge iron based on likely carbon content [PR 670](https://github.com/NatLabRockies/H2Integrate/pull/670)
67
- Bugfix for round-trip efficiency handling when calling `check_inputs` around `StoragePerformanceModel` [PR 684](https://github.com/NatLabRockies/H2Integrate/pull/684)
78
- Bugfix. Include nuclear in electricity producing tech list and improve error message for zero-length electricity producing techs in model when electricity is specified as the commodity. [PR 685](https://github.com/NatLabRockies/H2Integrate/pull/685)

docs/_toc.yml

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,20 @@ parts:
7070
- file: resource/tidal_resource
7171
- caption: Control
7272
chapters:
73-
- file: control/control_overview
74-
- file: control/open-loop_controllers
75-
- file: control/pyomo_controllers
76-
- file: control/controller_demonstrations
73+
- file: control/system_level_control/system_level_control
74+
sections:
75+
- file: control/system_level_control/system_level_control_base
76+
- file: control/system_level_control/control_classifier
77+
- file: control/system_level_control/controllers
78+
sections:
79+
- file: control/system_level_control/slc_demand_following
80+
- file: control/system_level_control/slc_profit_max
81+
- file: control/system_level_control/slc_cost_min
82+
- file: control/technology_level_control/technology_control_overview
83+
sections:
84+
- file: control/technology_level_control/open-loop_controllers
85+
- file: control/technology_level_control/pyomo_controllers
86+
- file: control/technology_level_control/controller_demonstrations
7787
- caption: Demand
7888
chapters:
7989
- file: demand/demand_components
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# System Level Control Technology Performance Classifiers
2+
3+
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.
4+
5+
```{note}
6+
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.
7+
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.
8+
This is a useful and necessary distinction that delineates different model capabilities clearly.
9+
```
10+
11+
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`.
12+
13+
Classifier | Meaning | Example Technology Models
14+
-- | -- | --
15+
fixed | Always produces commodity and cannot be controlled or reduced; does not receive a set-point | classical nuclear
16+
flexible | Resource-driven; can only be *reduced* (curtailed) below the resource-determined maximum via a set-point | wind, solar
17+
dispatchable | Can modulate production within bounds in response to a set-point | grid, electrolyzer, NG turbine
18+
storage | Can modulate consumption/production within bounds while tracking SOC | battery, h2 storage, any storage
19+
feedstock | Are not directly controlled, but useful for SLC to make dispatch decisions | feedstocks
20+
21+
To add a classifier for a particular model it would look something like this in the class:
22+
```{python}
23+
_control_classifier = "flexible"
24+
```
25+
26+
```{note}
27+
**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.
28+
```
29+
30+
## Fixed
31+
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.
32+
33+
## Flexible
34+
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.
35+
36+
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.
37+
38+
To simplify the implementation of applying this curtailment we added a method, `apply_curtailment()`, to the `PerformanceBaseClass`.
39+
40+
```{figure} figures/curtailable.png
41+
:width: 70%
42+
:align: center
43+
```
44+
45+
### Apply curtailment based on set_point
46+
Within the `compute()` method in the performance model you can apply the curtailment using the `apply_curtailment()` method.
47+
```
48+
self.apply_curtailment(outputs)
49+
```
50+
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.
51+
52+
(dispatchable)=
53+
## Dispatchable
54+
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.
55+
56+
There aren't additional special methods to handle this because the set-point response is internal to each performance model.
57+
58+
```{figure} figures/dispatchable.png
59+
:width: 70%
60+
:align: center
61+
```
62+
63+
## Storage
64+
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`.
65+
66+
There are two types of cases for the storage control classifier:
67+
1. **with a storage controller**
68+
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`.
69+
70+
2. **without a storage controller**
71+
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.
72+
73+
74+
```{figure} figures/storage.png
75+
:width: 85%
76+
:align: center
77+
```
78+
79+
## Feedstock
80+
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.
81+
82+
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.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Control Strategies
2+
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.
3+
4+
The current control strategies are:
5+
1. [Demand Following](#slc-demand-following)
6+
2. [Profit Maximization](#slc-profit-max)
7+
3. [Cost Minimization](#slc-cost-min)
8+
9+
```{note}
10+
The strategies currently implemented are experimental and will likely require further development for specific analyses.
11+
```
12+
13+
All control strategies inherit `SystemLevelControlBase`, which is a base class that has common setup logic shared by all system-level control strategies.
14+
15+
See additional information, which is more developer focused, about the [`SystemLevelControlBase`](#slc-base).
153 KB
Loading
176 KB
Loading
162 KB
Loading
167 KB
Loading
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
---
2+
jupytext:
3+
text_representation:
4+
extension: .md
5+
format_name: myst
6+
format_version: 0.13
7+
jupytext_version: 1.18.1
8+
kernelspec:
9+
display_name: Python 3.11.13 ('h2i_env')
10+
language: python
11+
name: python3
12+
---
13+
14+
(slc-cost-min)=
15+
# Cost Minimization System Level Controller
16+
17+
The cost minimization controller, `CostMinimizationControl`, meets demand at minimum variable cost using merit-order dispatch.
18+
Unlike the {ref}`demand following controller <slc-demand-following>`, which splits demand evenly across dispatchable technologies, this controller dispatches the cheapest technologies first.
19+
20+
## Dispatch Logic
21+
22+
The controller follows a three-step dispatch process:
23+
24+
1. **Flexible technologies** run at their available capacity (assumed zero marginal cost). Their output is subtracted from the demand.
25+
2. **Storage technologies** absorb any surplus (charging) or provide the deficit (discharging). Residual demand is split evenly across storage technologies producing the demanded commodity.
26+
3. **Dispatchable technologies** are dispatched by cheapest marginal cost first, each up to its rated capacity, until the remaining demand is met.
27+
28+
## Marginal Cost Configuration
29+
30+
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:
31+
32+
| Value | Description |
33+
| --- | --- |
34+
| Numeric (e.g. `0.05`) | Constant marginal cost in `$/(commodity_amount_units)` |
35+
| `"buy_price"` | Uses the technology's configured purchase price |
36+
| `"VarOpEx"` | Derives marginal cost from the technology's variable operating expenditure divided by total production |
37+
| `"feedstock"` | Sums upstream feedstock `VarOpEx` values and divides by the technology's total production |
38+
39+
```{note}
40+
The dispatch order is determined by sorting dispatchable technologies by their **mean** marginal cost across all timesteps (cheapest first).
41+
```
42+
43+
### Example Configuration
44+
45+
```yaml
46+
system_level_control:
47+
control_strategy: CostMinimizationControl
48+
control_parameters:
49+
cost_per_tech:
50+
natural_gas_plant: feedstock
51+
```
52+
53+
## Inputs and Outputs
54+
55+
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).
56+
57+
The base inputs for technologies classified as `flexible`, `dispatchable`, and `storage` are:
58+
59+
- `f"{tech_name}_{tech_output_commodity}_out"`
60+
- `f"{tech_name}_rated_{tech_output_commodity}_production"`
61+
- `f"{tech_name}_{tech_output_commodity}_demand"`
62+
63+
## Limitations
64+
65+
- Greedy dispatch: The merit-order approach is greedy - it does not look ahead across timesteps to optimize total cost over the simulation horizon.
66+
- Even splitting across storage: Residual demand is split evenly across storage technologies regardless of capacity or state of charge.
67+
- Demand is always met: Unlike the {ref}`profit maximization controller <slc-profit-max>`, this controller always attempts to meet demand regardless of cost.
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
---
2+
jupytext:
3+
text_representation:
4+
extension: .md
5+
format_name: myst
6+
format_version: 0.13
7+
jupytext_version: 1.18.1
8+
kernelspec:
9+
display_name: Python 3.11.13 ('h2i_env')
10+
language: python
11+
name: python3
12+
---
13+
14+
(slc-demand-following)=
15+
# Demand Following System Level Controller
16+
17+
The demand following controller, `DemandFollowingControl`, aims to fully meet the demand and does not have any inputs related to cost.
18+
19+
The N2 diagram below shows an example system using the demand following controller with wind, natural gas, and battery storage technologies.
20+
21+
```{code-cell} ipython3
22+
:tags: [remove-input]
23+
24+
from h2integrate.core.h2integrate_model import H2IntegrateModel
25+
import openmdao.api as om
26+
import os
27+
28+
import html
29+
from pathlib import Path
30+
from h2integrate import EXAMPLE_DIR
31+
from IPython.display import HTML, display
32+
33+
os.chdir(EXAMPLE_DIR / "35_system_level_control/battery_with_controller/")
34+
35+
h2i_model = H2IntegrateModel("wind_ng_demand.yaml")
36+
h2i_model.setup()
37+
38+
om.n2(
39+
h2i_model.prob,
40+
outfile="h2i_n2.html",
41+
display_in_notebook=False,
42+
show_browser=False,
43+
)
44+
45+
n2_html = "h2i_n2.html"
46+
n2_srcdoc = html.escape(Path(n2_html).read_text(encoding="utf-8"))
47+
display(
48+
HTML(
49+
f'<div style="width:100%; height:600px; overflow:auto; margin:0; padding:0; border:0;">'
50+
f'<iframe srcdoc="{n2_srcdoc}" '
51+
'style="display:block; width:200%; height:600px; border:0; margin:0; padding:0; background:transparent;" '
52+
'loading="lazy"></iframe>'
53+
'</div>'
54+
)
55+
)
56+
```
57+
## Dispatch Logic
58+
59+
The demand is satisfied in a fixed three-step priority order, and each step's shortfall or surplus is passed to the next:
60+
61+
1. **Curtailable techs** run at their available capacity. Their total output is subtracted from the demand, which may drive the residual demand negative (surplus).
62+
63+
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
64+
split **evenly** across them (each receives ``demand / n_storage``).
65+
66+
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``).
67+
68+
### Example Configuration
69+
70+
```yaml
71+
system_level_control:
72+
control_strategy: DemandFollowingControl
73+
solver_options: # solver options for resolving feedback
74+
solver_name: gauss_seidel
75+
max_iter: 20
76+
convergence_tolerance: 1.0e-6
77+
```
78+
79+
## Inputs and Outputs
80+
81+
The inputs for technologies classified as `curtailable`, `dispatchable`, and `storage` are:
82+
83+
- `f"{tech_name}_{tech_output_commodity}_out"`
84+
- `f"{tech_name}_rated_{tech_output_commodity}_production"`
85+
- `f"{tech_name}_{tech_output_commodity}_demand"`
86+
87+
The inputs for technologies classified as `feedstock` are:
88+
- `f"{tech_name}_{commodity}_out"`
89+
90+
## Systems with Heterogeneous Commodities
91+
92+
The `DemandFollowingControl` controller can be used in hybrid systems where technologies produce different commodities.
93+
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.
94+
95+
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.
96+
97+
## Limitations
98+
99+
- No cost awareness: The controller dispatches technologies purely to meet demand without considering operational costs, commodity prices, or economic optimization.
100+
- 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.
101+
- 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.
102+
- Fixed priority order: The dispatch order (curtailable → storage → dispatchable) is fixed in the current implementation.

0 commit comments

Comments
 (0)