Skip to content

Enable load following optimization dispatch with Pyomo#407

Open
genevievestarke wants to merge 82 commits intoNatLabRockies:developfrom
genevievestarke:feature/pyomo_opt
Open

Enable load following optimization dispatch with Pyomo#407
genevievestarke wants to merge 82 commits intoNatLabRockies:developfrom
genevievestarke:feature/pyomo_opt

Conversation

@genevievestarke
Copy link
Collaborator

@genevievestarke genevievestarke commented Dec 9, 2025

Bring Pyomo load following dispatch capabilities into H2I

This feature would enable dispatch optimization using one storage technology and multiple generation technologies (using the combiner) to determine optimal storage operation to follow a load while minimizing costs. This feature adds the optimization problem formulation, framework, and solver for the optimization in Pyomo. To do this, a hybrid_rule.py file was added to aggregate the storage and production variables to create one problem for the optimizer. The added files are

  • OptimizedDispatchController class in pyomo_controllers.py
  • The hybrid_rule.py file: Aggregates optimization pieces from the various components and formulates the model for the optimization. This file collects the objective function, initializes model parameters, and updates time series parameters.
  • The generic_converter_opt.py file: Houses the variables, parameters, constraints, and objective function for the generator technology in the optimization model.
  • The pyomo_storage_rule_min_operating_cost.py file: houses the storage and system variables, parameters, constraints, and objective function. Ideally the system parameters would be in a separate file, perhaps, but have been combined for efficiency in the first implementation.
  • The controller_opt_problem_state.py file: Formulates the optimization model for the solver

The below is an example of how and where the dispatch is run using a battery for electricity storage:
image

What's changed?

For the optimized dispatch, the control and dispatch functions are no longer separate (see figure above). This also means that the controller needs access to the dispatch parameters. Now all dispatch and controller classes have access to the shared_parameters section in the input yaml, meaning that parameters like commodity_name that were defined in multiple places before should now be defined once in the shared_parameters section. This affects the pyomo heuristic dispatch, as well. Most control parameters are then defined in the shared_parameters, as well, because there is a large overlap with the performance variables. Control/dispatch specific variables can be defined in the control_parameters section. This include:

  • cost_per_charge: in $/kW, cost to charge the storage (note that charging is incentivized)
  • cost_per_discharge: in $/kW, cost to discharge the storage
  • commodity_met_value: in $/kW, penalty for not meeting the desired load demand
  • cost_per_production: in $/kW, cost to use the incoming produced commodity (i.e. electricity from wind)
  • time_weighting_factor: This parameter discounts each subsequent time step incrementally in the future in the horizon window by the defined amount (defaults to 0.995)
  • tech_name: Name that the technology will be defined as in the dispatch

Note that the tech_name variable must match with the names given in the tech_to_dispatch_connections in the plant_config file. TODO: should we eliminate the tech_name variable and make it the same as the defined technology name? Why is it different?

We have exposed the cost values to the user in this implementation, which was not the case for HOPP. This is good for visibility, but the weights are fussy. If your dispatch is taking an abnormally long time (or even longer than a minute), you should check to make sure your weights make sense, or change them to see if that makes a difference. Some rules of thumb and thoughts:

  • You always want your commodity_met_value to be the largest because this is what drives meeting the load for load following
  • You should not set cost_per_charge equal to cost_per_discharge. This will confuse the optimizer. In the objective function, charging the battery is incentivized and discharging is disincentivized, meaning that if they are the same value, they can cause the optimizer to oscillate the battery because they "cancel each other out" in value in the objective function. I usually set cost_per_charge a little bit less than cost_per_discharge to not discourage charging the battery instead of meeting the load by discharging.
  • I view cost_per_production as not that important if the energy is already produced (i.e. from wind for example). I tend to set this to 0 to not discourage charging the battery
  • I have been setting cost_per_charge and cost_per_discharge two orders of magnitude lower than commodity_met_value so that the charging the discharging of the battery don't interfere with the load very much. You can put them closer together, but it will increase the time that the optimization takes.
  • These weights are defined as "$/kW", which is a weird unit. I defined them this way because pyomo is based on dollars and inputs to H2I are in kilowatts. I think it may be better to choose either $/MW or cents/kW and then convert internally, as these are more established cost measures and people have better intuition about what these number mean.

This pull request does not enable the optimal dispatch of a storage technology that can charge from the grid. This implementation currently only involves incoming electricity from upstream (that could be bought using grid component). It also only allows one incoming electricity stream and does not apply optimal dispatch of that stream back through the upstream technologies (no feedback). The dispatch can handle more than one generation technology, but the incoming electricity must be combined using an H2I combiner before going to the storage component, and the cost_per_production, which is defined in the storage technology section, needs to include the cost of production for all production technologies. This could be done using the following:

technology_interconnections: [
  ["wind", "combiner", "electricity", "cable"],
  ["solar", "combiner", "electricity", "cable"],
  ["combiner", "battery", "electricity", "cable"],
]

tech_to_dispatch_connections: [
  ["combiner", "battery"],
  ["battery", "battery"],

The demand is also still set externally after the setup step. Ideally, the dispatch will be integrated with the load demand framework that already exists in H2I, and the demand can be defined that way.

# Define demand signal
demand_profile = np.ones(8760) * 100.0

# Model setup
model.setup()
# Set demand signal directly on the battery technology
model.prob.set_val("battery.electricity_demand", demand_profile, units="MW")

# Run the model
model.run()

Section 1: Type of Contribution

  • Feature Enhancement
    • Framework
    • New Model
    • Updated Model
    • Tools/Utilities
    • Other (please describe):
  • Bug Fix
  • Documentation Update
  • CI Changes
  • Other (please describe):

Section 2: Draft PR Checklist

  • Open draft PR
  • Describe the feature that will be added
  • Fill out TODO list steps
  • Describe requested feedback from reviewers on draft PR
  • Complete Section 7: New Model Checklist (if applicable)

TODO:

  • Get general feedback on framework of added pyomo pieces
  • Apply any framework feedback and update code so that heuristic dispatch works as expected
  • Update example to accurately describe run
  • Update doc strings in methods
  • Write tests for new pyomo optimization
  • Make docs page?
  • Maybe: address where dispatch inputs should live for generator technologies
  • Maybe: make an example with multiple generation technologies, using the combiner to connect to dispatch

Type of Reviewer Feedback Requested (on Draft PR)

Structural feedback:
This dispatch model differs significantly from the heuristic block formulation in H2I. The dispatch for the generation + storage system is aggregated in hybrid_rule.py, which pulls from generic_converter_opt.py and pyomo_storage_rule_min_operating_cost.py, all three of which are classes, and thus outside of the OpenMDAO input/output framework. This was implemented because the previous dispatch_rule framework was difficult to aggregate in the way that was needed to formulate the optimization problem. I would like feedback on this structure, and whether we could make it more OpenMDAO-friendly.

Implementation feedback:

Other feedback:
The inputs to the dispatch are all defined in the storage technology config, including the cost_per_production term, which describes the cost of the production technologies. I think this should be defined under the production technologies in the config, so any feedback on how to pull this into the battery dispatch would be appreciated!

Section 3: General PR Checklist

  • PR description thoroughly describes the new feature, bug fix, etc.
  • Added tests for new functionality or bug fixes
  • Tests pass (If not, and this is expected, please elaborate in the Section 6: Test Results)
  • Documentation
    • Docstrings are up-to-date
    • Related docs/ files are up-to-date, or added when necessary
    • Documentation has been rebuilt successfully
    • Examples have been updated (if applicable)
  • CHANGELOG.md has been updated to describe the changes made in this PR

Section 3: Related Issues

This resolves #386

Section 4: Impacted Areas of the Software

Section 4.1: New Files

  • path/to/file.extension
    • method1: What and why something was changed in one sentence or less.

Section 4.2: Modified Files

  • path/to/file.extension
    • method1: What and why something was changed in one sentence or less.

Section 5: Additional Supporting Information

Section 6: Test Results, if applicable

Section 7 (Optional): New Model Checklist

  • Model Structure:
    • Follows established naming conventions outlined in docs/developer_guide/coding_guidelines.md
    • Used attrs class to define the Config to load in attributes for the model
      • If applicable: inherit from BaseConfig or CostModelBaseConfig
    • Added: initialize() method, setup() method, compute() method
      • If applicable: inherit from CostModelBaseClass
  • Integration: Model has been properly integrated into H2Integrate
    • Added to supported_models.py
    • If a new commodity_type is added, update create_financial_model in h2integrate_model.py
  • Tests: Unit tests have been added for the new model
    • Pytest-style unit tests
    • Unit tests are in a "test" folder within the folder a new model was added to
    • If applicable add integration tests
  • Example: If applicable, a working example demonstrating the new model has been created
    • Input file comments
    • Run file comments
    • Example has been tested and runs successfully in test_all_examples.py
  • Documentation:
    • Write docstrings using the Google style
    • Model added to the main models list in docs/user_guide/model_overview.md
      • Model documentation page added to the appropriate docs/ section
      • <model_name>.md is added to the _toc.yml

jaredthomas68 and others added 15 commits October 2, 2025 09:16
             {build-extensions,convert,download-extensions,help,install-extras,model-viewer,run,solve,test-solvers}
             ...

This is the main driver for the Pyomo optimization software.

options:
  -h, --help            show this help message and exit
  --version             show program's version number and exit

subcommands:
  {build-extensions,convert,download-extensions,help,install-extras,model-viewer,run,solve,test-solvers}
    build-extensions    Build compiled extension modules
    convert             Convert a Pyomo model to another format
    download-extensions
                        Download compiled extension modules
    help                Print help information.
    install-extras      Install "extra" packages that Pyomo can leverage.
    model-viewer        Run the Pyomo model viewer
    run                 Execute a command from the Pyomo bin (or Scripts)
                        directory.
    solve               Optimize a model
    test-solvers        Test Pyomo solvers

-------------------------------------------------------------------------
Pyomo supports a variety of modeling and optimization capabilities,
which are executed either as subcommands of 'pyomo' or as separate
commands.  Use the 'help' subcommand to get information about the
capabilities installed with Pyomo.  Additionally, each subcommand
supports independent command-line options.  Use the -h option to
print details for a subcommand.  For example, type

   pyomo solve -h

to print information about the `solve` subcommand. branch that needs to be saved for later
Merging in current pyomo opt branch
@genevievestarke genevievestarke added the ready for review This PR is ready for input from folks label Dec 31, 2025
@genevievestarke
Copy link
Collaborator Author

This pull request is ready for an overall framework review!

@jaredthomas68 jaredthomas68 self-requested a review January 6, 2026 22:38
@elenya-grant elenya-grant self-requested a review January 6, 2026 22:55
Copy link
Collaborator

@kbrunik kbrunik left a comment

Choose a reason for hiding this comment

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

Thanks for continuing to iterate on this! I think there's still a bit more clean up and adding documentation that would be helpful before bringing it in. If you need help with some of it let me know and we can tackle it together.

In regards to the diagram in the PR body -- how does the hybrid rule class and it's outputs get incorporated into the overall run/dispatch?

demand_profile = np.ones(8760) * 100.0


# TODO: Update with demand module once it is developed
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do you want to address this TODO? I thought you could set the demand in the tech config?

Copy link
Collaborator Author

@genevievestarke genevievestarke Feb 3, 2026

Choose a reason for hiding this comment

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

Currently, you have to set up the problem, then assign the battery demand signal to the battery model, then run the model. #385 is an issue to address this



class PyomoDispatchGenericConverterMinOperatingCosts:
def __init__(
Copy link
Collaborator

Choose a reason for hiding this comment

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

I agree I think it would be helpful to explain what the parameters in those lines are



@define
class OptimizedDispatchControllerConfig(PyomoControllerBaseConfig):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you add a docstring with all of these parameters defined?

- `examples/18_pyomo_heuristic_wind_battery_dispatch`

(optimized-load-following-controller)=
## Optimized Load Following Controller
Copy link
Collaborator

Choose a reason for hiding this comment

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

After further reviewing the PR, I think there's some additions to the doc page that would really help with overall understanding.

  1. Adding something similar to the diagram in the PR body to help explain the mechanics of the interactions.
  2. A bit more about "Arcs", "Ports", "parameters" and "constraints" that are used within the context of the optimized load following controller
  3. It's a little confusing what "system-level" is within the context of the work because everything seems to be housed within the storage model. Explaining that might help users understand what's going on.
  4. I know we talked about making a note about users setting weights and how monkeying around with those can change how the Pyomo solver runs. I still think that would be a helpful addition

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Some responses to the comments above:

  1. We can add some figures to the docs, happy to get some feedback on which parts of the diagram need to be clearer!
  2. "parameters" are values that the model needs to run the optimization (e.g. available production of the commodity to be used for storage), but that are not changed in the optimization like the "variables" are. "Constraints" define constraints on the variables, sometimes involving the parameters, that are enforced in the pyomo optimization. "Ports" define names of the variables that can be used to create external links between pyomo modules. The pyomo dispatch is implemented in a modular structure, where the dispatch for each plant technology is defined individually, and then all of the dispatch models are collected into a single optimization problem for the pyomo solver by hybrid_rule. The ports define what variables are passed from the individual technology dispatch models to the hybrid_rule problem. "Arcs" are what actually form the connection between the ports. This means that the storage dispatch pyomo model has a port for system_production, hybrid_rule has a port for system_production, and an arc connects these two endpoints to connect this variable across the pyomo models.
  3. In this PR, I've used "system-level" to define things that apply to the whole system (i.e. commodity load demand, etc.), and "storage" to define things that are specific to the storage technology (i.e. maximum charge rate, SOC, etc.). These are both included in the storage pyomo model at the moment. In HOPP, these were separated into 'storage' and 'grid' pyomo models, but the grid paradigm doesn't work with H2I at the moment, so they were combined into the storage pyomo model. We could separate these out into two different models (maybe "storage" and "system"). They are not defined in hybrid_rule because hybrid_rule currently only works as an aggregator of models and only includes things that you would need access to all the individual pyomo models to know.
  4. There are some thoughts about the weight settings in the body of the PR. I can distill those down to what would be helpful to have as a note in the docs.

genevievestarke and others added 4 commits February 3, 2026 12:29
…state.py


Apply comment suggestions

Co-authored-by: John Jasa <john.jasa@nrel.gov>
…state.py


Apply comment suggestions

Co-authored-by: John Jasa <john.jasa@nrel.gov>
Apply comment suggestions

Co-authored-by: John Jasa <john.jasa@nrel.gov>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

in progress needs modifications This PR has been reviewed, at least partially, and is ready for PR author response ready for review This PR is ready for input from folks

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants