Skip to content

Peak load management heuristic control#641

Open
jaredthomas68 wants to merge 65 commits intoNatLabRockies:developfrom
jaredthomas68:peakload
Open

Peak load management heuristic control#641
jaredthomas68 wants to merge 65 commits intoNatLabRockies:developfrom
jaredthomas68:peakload

Conversation

@jaredthomas68
Copy link
Copy Markdown
Collaborator

@jaredthomas68 jaredthomas68 commented Mar 31, 2026

Peak load management heuristic control

This PR adds Peak load management heuristic control to H2I. This does not do demand dispatch, but rather dispatches based on peaks in the provided load and rules defined by the user.

It also adds a time series generation utility that can be called directly with time step, simulation length, etc or with a plant_config dict.

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:

  • Step 1
  • Step 2

Type of Reviewer Feedback Requested (on Draft PR)

This PR is ready for full detailed review.

Structural feedback:

Implementation feedback:

Other feedback:

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
    • At least one complete sentence has been provided to describe the changes made in this PR
    • After the above, a hyperlink has been provided to the PR using the following format:
      "A complete thought. [PR XYZ]((https://github.com/NatLabRockies/H2Integrate/pull/XYZ)", where
      XYZ should be replaced with the actual number.

Section 3: Related Issues

Section 4: Impacted Areas of the Software

Section 4.1: New Files

  • examples/33_peak_load_management/: example to demonstrate peak load controller
  • h2integrate/control/control_strategies/storage/plm_openloop_storage_controller.py: new control for peak load management
  • h2integrate/control/control_strategies/storage/test/data/lmp_month_1.csv testing input file
  • h2integrate/control/control_strategies/storage/test/data/lmp_peaks_month_1.csv testing input file
  • h2integrate/control/control_strategies/storage/test/test_plm_openloop_storage_controller.py: tests for peak load management control

Section 4.2: Modified Files

  • docs/control/control_overview.md: include mention of peak load control
  • docs/control/open-loop_controllers.md: add detailed description for peak load control
  • h2integrate/control/control_strategies/storage/demand_openloop_storage_controller.py: move some shared code to base class
  • h2integrate/control/control_strategies/storage/openloop_storage_control_base.py: include optional parameters and post-attr actions for some child classes
  • h2integrate/core/inputs/plant_schema.yaml: add default time zone`:
  • h2integrate/core/test/test_utilities.py: tests for time series utility function
  • h2integrate/core/utilities.py: add time series generation utility function

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

@elenya-grant elenya-grant self-requested a review March 31, 2026 19:43
Copy link
Copy Markdown
Collaborator

@elenya-grant elenya-grant left a comment

Choose a reason for hiding this comment

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

just left some initial comments/questions - haven't done a deep dive yet (so some of my questions/comments may be silly or I'll be able to answer during a deep-dive) but plan to do a deeper review by Thursday morning. I only looked at the changes and additions to the control classes but will review the tests in the second review I do.

Overall looks like a great start - most of my comments were small or were questions!

Comment thread h2integrate/control/control_strategies/storage/plm_openloop_storage_controller.py Outdated
# determine demand_profile peaks using defaults of daily peaks inside peak_range
# for the full simulation but respecting the peak range specified in the config
self.secondary_peaks_df = self.get_peaks(
demand_profile=self.condig.demand_profile,
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.

should this be inputs[f"{self.config.commodity}_demand"] instead of the demand from the config?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Some of the reasoning for this is in my comment here: #641 (comment). I guess I can split up demand and time stamp as separate inputs so we can use the input like the other controllers.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I have split up demand and date_time

Comment thread h2integrate/control/control_strategies/storage/plm_openloop_storage_controller.py Outdated
Comment thread h2integrate/control/control_strategies/storage/plm_openloop_storage_controller.py Outdated
Comment thread h2integrate/control/control_strategies/storage/plm_openloop_storage_controller.py Outdated
@jaredthomas68 jaredthomas68 requested a review from vijay092 April 2, 2026 16:11
Copy link
Copy Markdown
Collaborator

@elenya-grant elenya-grant left a comment

Choose a reason for hiding this comment

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

Howdy! I gave this more of a deeper look! I think I'm a little confused on how this method works (I didn't try to understand it super hard yet) - so most of my comments were nitpicks or questions. My only blocking comment is about the error being removed from load_plant_yaml - I don't think that error message should be removed at this time.

I think a visual (or two) may be nice to explain some of the inputs to the controller - I think if a doc page with some visuals and explanation on the inputs would be super helpful in making it easier for users to understand how to change the control input parameters based on their use-case.

Comment thread h2integrate/core/utilities.py Outdated
dt_seconds = int(simulation_cfg["dt"])

# Optional start_time in config; default to a fixed reference timestamp.
start_time = simulation_cfg.get("start_time", "2000-01-01 00:00:00")
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.

the start-time format in the plant config is defined as being: mm/dd/yyyy HH:MM:SS or mm/dd HH:MM:SS and defaults to 01/01 00:30:00 (doesn't include a year because it was initially going to be used with resource data and the year may change based on the resource year). The format here does not match - do you think we could make sure that the format is consistent mm/dd/yyyy instead of yyyy-mm-dd?

I made a similar function when I was starting on the resource models (it never made it in) but it handles whether a year was added or not:

from datetime import datetime, timezone, timedelta

def make_time_profile(
    start_time: str,
    dt: float | int,
    n_timesteps: int,
    time_zone: int | float,
    start_year: int | None = None,
):
    """Generate a time-series profile for a given start time, time step interval, and
    number of timesteps, with a timezone signature.

    Args:
        start_time (str): simulation start time formatted as 'mm/dd/yyyy HH:MM:SS' or
            'mm/dd HH:MM:SS'
        dt (float | int): time step interval in seconds.
        n_timesteps (int): number of timesteps in a simulation.
        time_zone (int | float): timezone offset from UTC in hours.
        start_year (int | None, optional): year to use for start-time. if start-time
            is formatted as 'mm/dd/yyyy HH:MM:SS' then will overwrite original year.
            If None, the year will default to 1900 if start-time is formatted as 'mm/dd HH:MM:SS'.
            Defaults to None.

    Returns:
        list[datetime]: list of datetime objects that represents the time profile
    """

    tz_utc_offset = timedelta(hours=time_zone)
    tz = timezone(offset=tz_utc_offset)
    tz_str = str(tz).replace("UTC", "").replace(":", "")
    if tz_str == "":
        tz_str = "+0000"
    # timezone formatted as ±HHMM[SS[.ffffff]]
    start_time_w_tz = f"{start_time} ({tz_str})"
    if len(start_time.split("/")) == 3:
        if start_year is not None:
            start_time_month_day_year, start_time_time = start_time.split(" ")
            start_time_month_day = "/".join(i for i in start_time_month_day_year.split("/")[:-1])
            start_time_w_tz = f"{start_time_month_day}/{start_year} {start_time_time} ({tz_str})"

        t = datetime.strptime(start_time_w_tz, "%m/%d/%Y %H:%M:%S (%z)")
    elif len(start_time.split("/")) == 2:
        if start_year is not None:
            start_time_month_day, start_time_time = start_time.split(" ")
            start_time_w_tz = f"{start_time_month_day}/{start_year} {start_time_time} ({tz_str})"
            t = datetime.strptime(start_time_w_tz, "%m/%d/%Y %H:%M:%S (%z)")
        else:
            # NOTE: year will default to 1900
            t = datetime.strptime(start_time_w_tz, "%m/%d %H:%M:%S (%z)")
    time_profile = [None] * n_timesteps
    time_step = timedelta(seconds=dt)
    for i in range(n_timesteps):
        time_profile[i] = t
        t += time_step
    return time_profile

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for the extended function. I personally much prefer the international format "yyyy-mm-dd", but I understand their being an existing approach. I went ahead and changed the function to handle timezone and added a function to make the time series with direct inputs rather than a config. I do not have much use for the second value of a timestamp, but I did adjust to return python datetime format. We can continue to discuss exact desired format, but I would prefer to make the timeseries in a standard date-time format and allow users and developers to adjust to lists of integers or whatever other format they need from there.

Comment thread h2integrate/core/utilities.py Outdated
Comment thread examples/32_multivariable_streams/driver_config.yaml
Comment thread examples/33_peak_load_management/plant_config.yaml
Comment thread examples/33_peak_load_management/tech_config.yaml Outdated

self.get_allowed_discharge()

@staticmethod
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.

why is this a staticemethod rather than just a normal method? (same with _normalize_peak_range?)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

These are static methods because they need to be called with different attributes of the class rather than the exact same attributes in order each time. This also means the output does not have a consistent target.

# Dispatch strategy outline:
# - Discharge: Starting when time_to_peak <= advance_discharge_period
# * Discharge at max rate (or less to reach targets)
# * Stop discharging only when SOC reaches min_soc
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.

could these inline comments get moved closer to where that logic is represented in the code?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I moved them to the docstring as I think that makes more sense.

@vijay092
Copy link
Copy Markdown
Collaborator

vijay092 commented Apr 2, 2026

Very exciting work @jaredthomas68! Thanks for putting this together in such a short time! I did a full pass through h2integrate/control/control_strategies/storage/plm_openloop_storage_controller.py and left comments wherever I spotted small issues. Feel free to address them as you see fit.

dispatch_priority_demand_profile: str = field(
validator=contains(["demand_profile", "demand_profile_supervisor"]),
)
max_supervisor_events: int | None = (field(default=None),)
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.

Is this supposed to be a tuple?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Nope, thanks for the catch. Fixed

charge_efficiency: float | None = field(default=None, validator=range_val_or_none(0, 1))
discharge_efficiency: float | None = field(default=None, validator=range_val_or_none(0, 1))
round_trip_efficiency: float | None = field(default=None, validator=range_val_or_none(0, 1))
demand_profile_supervisor: int | float | list | None = field()
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.

default = None

Copy link
Copy Markdown
Collaborator Author

@jaredthomas68 jaredthomas68 Apr 8, 2026

Choose a reason for hiding this comment

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

I intentionally left off the default because I want the user to be very aware of how they are using this controller and if they are excluding a supervisory demand profile or not.


self.max_discharge_rate = self.max_charge_rate

# make sure peak_range is in correct format because yaml
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.

Same problem for advance_discharge_period, right?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

No, advance_discharge_period uses a unit, val paradigm instead of time stamps.

)

# Store simulation parameters for later use
self.dt = self.options["plant_config"]["plant"]["simulation"]["dt"]
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.

Never used.

Copy link
Copy Markdown
Collaborator Author

@jaredthomas68 jaredthomas68 Apr 8, 2026

Choose a reason for hiding this comment

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

Thanks. Removed


# Store simulation parameters for later use
self.dt = self.options["plant_config"]["plant"]["simulation"]["dt"]
self.time_index = build_time_series_from_plant_config(self.options["plant_config"])
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 think it is worth adding a length check against self.n_timesteps somewhere.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I don't think so. The time series builds on the same config that self.n_timesteps comes from, so they should be the same by default.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I went ahead and added a check just in case. Won't hurt.

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.

Sounds good. There are a few time-tracking variables in the code. Just wanted to make sure they all correspond to the same index and length. Still getting used to the framework, so apologies if that caused additional work.

day_df = supervisory_peaks_df[
supervisory_peaks_df["date_time"].dt.floor("D") == day
]
# If supervisor has peaks on the day, use supervisor's flags for all rows that day
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.

Good to add check for when supervisor is None.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Supervisor should never be none in this function because the first argument is always treated as the most important peaks. I changed the naming and doc string to make this more clear.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I also added the check you suggested

next_peak_time - self.peaks_df.loc[idx, "date_time"]
)

def get_allowed_discharge(self):
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.

Method name is misleading. It actually computes "allow_charge"?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Great point. I've changed the name.

soc_array[i] = deepcopy(soc)

# stay in discharge mode until the battery is fully discharged
if soc <= soc_min:
Copy link
Copy Markdown
Collaborator

@vijay092 vijay092 Apr 2, 2026

Choose a reason for hiding this comment

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

Note for future:
discharging is only set to False when soc <= soc_min. If the battery doesn't fully drain during the event duration, discharging will continue to stay True

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

That is by design. The intended operation is to fully charge and then fully discharge, not try to meet a demand, so the battery should fully discharge. If you have suggestions for catching corner cases on this I'm all ears.

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 think this should be documented in the docpage on the peak load management, I would have expected the battery to stop discharging once the event is over.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The event period is pretty loose. We find the exact peak, but discharge leading up to and after that peak. I will include more in a docs page.

# start discharging when we approach a peak and have some charge
if time_to_peak <= advance_discharge_period and soc > soc_min:
discharging = True

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.

Suggest adding charging = False here in case charging hasn't been set to False in the previous timestep.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done

# Note: discharge_needed is internal (storage view), max_discharge_rate is external
discharge_needed = max_discharge_rate / discharge_eff
discharge = min(
discharge_needed, available_discharge, max_discharge_rate / discharge_eff
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.

The first and third terms are the same.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes, I was trying to lean into code reuse and hoping I could find a good way, but I went ahead and removed the duplicate.

@jaredthomas68 jaredthomas68 marked this pull request as ready for review April 10, 2026 19:26
Copy link
Copy Markdown
Collaborator

@elenya-grant elenya-grant left a comment

Choose a reason for hiding this comment

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

Mostly just small questions and comments! Really awesome tests! Love to see the thorough testing!

Comment thread docs/control/open-loop_controllers.md Outdated
- `examples/19_simple_dispatch/`

(peak-load-management-open-loop-storage-controller)=
### Peak Load Management Open-Loop Storage Controller
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 very detailed documentation! Since this section is so detailed - should this be moved to a separate file that's linked in this file? Non-blocking, just a thought!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think it is getting close, but not enough to warrant a separate file. I'm open for discussion, but am leaving as is unless I hear more desire for the additional file.

start_time: 2025/07/01 00:00:00
technology_interconnections:
# - [battery, grid_buy, [storage_electricity_charge, electricity_out]]
- [battery, electrical_load_demand, [electricity_out, electricity_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.

could you add comments here? like "subtract battery power from the load demand" and "buy power from the grid to fulfill remaining demand"?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done

charge_equals_discharge: true # enforce symmetric charge and discharge power limits
max_discharge_rate: 300.0 # kW/time step; defaults to battery power limit when matched to max_charge_rate
demand_profile: !include demand_profiles/demand_profile.yaml
control_parameters:
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.

thanks for the comments here! they're helpful!

"model": "SimpleStorageOpenLoopController",
},
"performance_model": {
"model": "StorageAutoSizingModel",
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.

replace StorageAutoSizingModel with StoragePerformanceModel?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done

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.

lots of awesome unit tests! Thanks for the comprehensive testing here!

raise ValueError(
"max_discharge_rate must be provided when charge_equals_discharge is False."
)

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 think these changes to the baseclass make sense given that another component is using them - but why not just have the PLM controller inherit the demand open-loop controller and make modifications to the demand open-loop controller? I guess I'm curious about the reasoning for all the changes to the base-class that are mostly relevant to the demand open-loop controller (not asking for changes - just wanting to understand)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

After a discussion with John, Gen, and others (in dev meeting I think) we decided to take this approach to avoid duplicate code while also limiting inheritance depth. Also see comments here..

timezone:
type: number
description: timezone as UTC offset corresponding to start_time
default: 0
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.

love this change - thank you!

Comment thread h2integrate/core/utilities.py Outdated
Comment thread h2integrate/core/utilities.py
@johnjasa johnjasa added the ready for review This PR is ready for input from folks label Apr 17, 2026
Copy link
Copy Markdown
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.

This is super cool! Thank you for putting so much effort into this dispatch strategy!! I'm really excited to get this into H2I. Just a few small things I'd like to work through before bringing it in

Comment thread docs/control/control_overview.md Outdated
Comment thread docs/control/open-loop_controllers.md Outdated
1. Peak load management dispatch open loop control with two demand profiles of interest
2. Battery charging without an input stream, assuming purchase from the grid

In this example, two load profiles are provided. It is assumed that demand_profile is a
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 appreciate all of the text included here! It really helps to have good explanation about what's happening in the examples

Comment thread docs/control/open-loop_controllers.md

An example output for the first week of a one-year simulation is shown below. Orange shading marks the 12:00–19:00 daily peak window. The top panel shows both demand profiles; the second panel shows battery state of charge; the third shows battery charge/discharge power; the fourth shows the resulting net demand.

![](./figures/example_peak_load_dispatch.png)
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 isn't rendering in the docs.

Copy link
Copy Markdown
Collaborator Author

@jaredthomas68 jaredthomas68 Apr 20, 2026

Choose a reason for hiding this comment

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

I checked in the CI tests and I see the figure render just fine. Let me know if you still can't see it.

Comment thread docs/control/open-loop_controllers.md
Comment thread docs/control/open-loop_controllers.md Outdated
Comment thread h2integrate/control/control_strategies/storage/plm_openloop_storage_controller.py Outdated

The `dispatch_priority_demand_profile` parameter selects which profile acts as the override schedule. On days where the priority profile flags a peak, the controller follows that schedule; on all other days it falls back to the other profile.

**Dispatch logic (state machine)**
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.

After reading all of the docstrings, I think it could be helpful to expand this a bit more based on all of the methods getting employed to calculate this. I appreciate the simplicity of Discharge, Charge and Idle but maybe after this logic you could include a bit more?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I'm happy to add more, but after reviewing it I'm not sure what else would be helpful at this level. Can you be more specific about what details you would like to see at the high-level docs rather than left in the doc strings?

Comment thread docs/control/open-loop_controllers.md Outdated
Copy link
Copy Markdown
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 the speedy changes! I think this looks great to come into H2I :)

Copy link
Copy Markdown
Collaborator

@aditiegarg9 aditiegarg9 left a comment

Choose a reason for hiding this comment

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

The definitions of the classes look good.


Attributes:
demand_profile_2 (int | float | list | None, optional): Demand values for
additional connected system for each timestep, in the same units as
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.

Is this a local load demand storage will be dispatched if the system load (the priority load) is not peaking? I'm raising this to understand what do we mean by additional connected load.

Copy link
Copy Markdown
Collaborator

@elenya-grant elenya-grant 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 iterating on this PR so much!

(Honestly, I'm having trouble viewing all the recent changes since this PR has so many commits and comments - but I trust that all my concerns were addressed (or there was a good reason they weren't) (and if not, I'll make an issue).)

Thanks for adding in this functionality and iterating on the PR! Very cool to have a heuristic method for peak shaving! Nicely done!

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

Labels

ready for review This PR is ready for input from folks

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants