Skip to content

Conversation

@caleb-sitton-inl
Copy link
Collaborator

@caleb-sitton-inl caleb-sitton-inl commented Jul 15, 2025

BREAKING CHANGE: changes API for max/min capacities

Addresses #36

Major changes included in this request include:

  • Adds a required float "installed_capacity" attribute and an optional time-dependent "capacity_factor" attribute containing floats from 0 to 1 to the Component class. These correspond to the previous max_capacity_profile attribute.

  • Adds a time-dependent "min_profile" attribute to replace the previous min_capacity_profile.

  • Adds methods to retrieve the capacity and minimum of a Component given a specific time index. Advantages of this design are that:

    1. The Component now interprets its own input instead of placing this responsibility on the System, which seems more appropriate.
    2. "Normalization" of the time series data is no longer necessary. The methods for retrieving Component information will return the correct values as long as there is not both (1) a time series specified for the quantity of interest and (2) insufficient data in that time series to supply data for the requested time index. The System will handle this edge case.

    The primary disadvantage of this design is that a conditional check must now be executed whenever the component's capacity at a specific timestep is requested by Pyomo. Since this is likely a frequent event, this change may increase computation time, though I personally have no estimate of the magnitude of the increase.

  • Adds an attribute for Sink components called demand_profile, which is a time-dependent version of installed_capacity. Exactly one of installed_capacity and demand_profile must be specified. Only if installed_capacity is provided can capacity_factor also be provided. When the Component super class is instantiated in Sink.__init__, if demand_profile was provided, the required installed_capacity is set to the maximum value in the demand profile (installed_capacity is not used or needed for Sinks that have a demand_profile). The method for retrieving the capacity of a Component is overwritten in Sink to return the demand profile at the specified time index if provided, or if not, to call Component.capacity_at_timestep.

  • Reorganizes System validation to be contained in a single method. This method is automatically called immediately before solving the System, which is significant because this is the only window prior to the System solution at which the entire System is known to be final. The validation method does not require an explicit call from the user. This strategy, combined with the new methods added to the Component and CashFlow classes, allows users to modify specific attributes of Components in the System prior to solving the System while preventing them from circumventing the System's validation checks. This also improves computation time by only validating everything once, instead of potentially validating the same data multiple times as was possible previously. The debugging experience is slightly changed; when a System validation check fails, the error will appear on the line containing System.solve(), not the line containing the first faulty input. Well-written error messages should be able to cover the clarity lost by this.

  • The time_index attribute of the System has been renamed to dispatch_window. In addition, validation for time series data has been relaxed to allow for a dispatch_window that is not from 0 to the length of every time series. As long as the time series data contains values for every time index specified by the dispatch_window, the System will not error and will solve the dispatch problem for the dispatch_window.

  • Creates a method belonging to the CashFlow class that calculates the value of the cashflow, given a time index and a dispatch value. This was previously implemented in the objective rule in rulelib.py, but it seems to belong better in the CashFlow class since a method of calculating CashFlow values is inherently part of the CashFlow's definition. The objective rule has been updated to use this new method. This method mirrors the new Component methods by removing the need for time series normalization of CashFlow price_profile attributes.

  • Adds a column to the dataframe returned by extract_results that shows the net cashflow of the system at each timestep.

  • Updates and adds Component validation as necessary.

  • Updates examples to use new API.

  • Updates and adds tests.

BREAKING CHANGE: changes API for max/min capacities
if len(self.price_profile) > 0:
self.price_profile = np.multiply(self.alpha, self.price_profile)

def evaluate(self, t: int, dispatch: float) -> float:
Copy link
Collaborator

Choose a reason for hiding this comment

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

very cool. perhaps we can add some logic to the extract_results() method that puts the time-dependent cashflow in addition to the total "objective"

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

To clarify, you're thinking of printing the sum of the cashflows on all components in the system for each timestep?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes.

@caleb-sitton-inl caleb-sitton-inl marked this pull request as ready for review July 21, 2025 20:34
@caleb-sitton-inl
Copy link
Collaborator Author

I've attempted to approximate the effect of using functions for capacity and minimums on running time. I've done this by timing a DOVE script that contains 10 components and dispatches over a time series of length 8760. I timed this script when using the DOVE setup in this PR , then with a simple design that does not use functions. In this alternate design, I created attributes of components called actual_cap_profile and actual_min_profile that were populated by the System in the _validate method. This comparison strategy was relatively imprecise, but I was able to estimate a difference of about 0.1-0.2 seconds (out of a total time of about 8 seconds) between the two DOVE setups, running on my local machine. This means that added time, per timestep, per component, per run of DOVE, is approximately 2e-6 seconds.

Copy link
Collaborator

@dylanjm dylanjm left a comment

Choose a reason for hiding this comment

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

Changes look good to me.

]
)

# This is a real capacity time series, which must be normalized
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is fine, but I think we should maybe turn off formatting for example files? @GabrielSoto-INL what do you think?

@dylanjm dylanjm merged commit 3a6b40a into idaholab:main Aug 5, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants