Skip to content

Commit

Permalink
Price taker model for DISPATCHES, Rehashed (#1358)
Browse files Browse the repository at this point in the history
* Added basic price-taker framework

* run black

* add in get_optimal_n_clusters as separate method. hand off to Marcus

* Add methods for computing the optimal # of clusters and making an elbow plot

* add fom to typos.toml in github/workflows to pass spellcheck

* fix typo

* Add tests for new functions (excel import failing)

* Update dependencies

* Add pytest marks

* Fix typo where TimeStep data was being used instead of BaseCaseTax

* Update test file

* Add warning for when kmax is not set

* Address Radha's comments and start adding testing (WIP)

* Add more tests

* Add cluster_lmp_data function

* linearizing su_sd cosntraints and fixing the price traker model

* Removing files that were mistakenly saved on this branch

* modified startup and shutdown constraint function

* add set_period to startup and shutdown func

* developed a function to add startup/shutdown constraints

* Constructed a function that adds startup and shutdown constraints

* Added deepgetattr function and had the ramp-up/down and start-up/down functions to add constraints through pyomo blocks

* linearized ramping constraints

* Added auxiliary variables

* added a third mccor constraint for each aux var and created a capacity argument for ramping func

* changed startup_shutdown constraints to have the shutdown binary var

* changed rule for min_start_up constraint

* fixed startup_shutdown function constraints, changed default values for ramping function

* added new method for adding aux variables

* removed aux variable delcaration and added a new config option to opt blk

* added comments

* small addition

* added design_blk

* added deepgetattr and constraint for the add capacity aux var func

* finished the new method and ramping function

* Allowed  _add_capacity_aux_vars to be cosntructed when the build function gets called

* added a for loop that calls on the _add_capacity_aux_vars function t times

* Updating PT model for SOFC case study

* Updated price_taker_model.py doc

Removed some unused variables. Beautified some of the code. Made all documentation for functions consistent and more useful. Added a few ToDo's for new functionality to be added to the class.

* Updated init of PriceTakerModel

Updated the formatting in the initialization of the PriceTakerModel. Added a check to the seed setter for integer inputs. Also added a check to the horizon_length setter for integer inputs.

* Added missing documentation

Added function description and Args/Returns statements for the 'generate_daily_data' function.

* Corrected constraint expressions

Corrected the mathematical expressions for the constraints for ramping rates and startup/shutdown expressions. Namely, the expressions now appropriately constrain power ramp rates under all scenarios. Also, design decisions to build and it's impact on whether an equipment can turn on or not has been fixed. Minimum downtime constraint was also fixed. Also, I added a check to make sure that the up_time and down_time inputs are valid under the assumptions.

* Added tests for input checking

Added tests to check the seed, horizon_length, and minimum up_time/down_time inputs. All tests pass. As a note, the tests for clustering the LMP data need to change as they are failing. Parsed data is passed instead of raw data. I am looking into this.

* Updated checks for get_optimal_clusters

Added validity checks to make sure kmin and kmax >= 0 and are integer values. Also, kmin < kmax. Also, added tests to ensure that each edge case is appropriately handled.

* Fixed typos in test_price_taker.py

Fixed a few typos and made language more clear.

* Added input checks on add_ramping_constraints

Added checks that the ramping rates as inputs are between 0 and 1 (fractions of the maximum capacity). Also added tests for each of these scenarios.

* Fixed broken test

Code used 1-based indexing but test used 0-based. Fixed the mistmatch.

* Added automated LMP population

Added an attribute to the CONFIG of OperationModelData that will be True by default. The attribute, append_lmp_data, will work with building the multiperiod_model so that LMP data will automatically be populated. Old way required a dummy value for m.LMP at the operational level and the user updated LMP manually for each time period.

Also removed some hard-coded values in the append_lmp_data() function.

* Fixed getitem issues with Sets

Updated code to be more modern and follow new Pyomo paradigms. Removed warnings for constructing both the ramping rate and startup/shutdown/min uptime/min downtime constraints. Also edited a few lines to beautify.

* Removed redundant set definition

The set 'range_time_steps' was defined in both the ramping rate and startup/shutdown function. Also, wrote the try/except statement with the hasattr() function instead since it is one line and much neater.

* Added n_clusters check in append_lmp_data

Added a check to make sure that n_clusters is feasible. Also added tests to capture edge cases.

* Reorganizing unit logging messages tests

Disaggregated the unit tests for logger messages. Should make it easier to debug later.

* Moved deepgetattr to import from design_and_operation_models.py

Removed duplicate deepgetattr function and added it as an import to the __init__.py file in the idaes.apps.grid_integration tree. Also, added a test for deepgetattr's error message.

* Added checks and tests

Added a check and test for n_clusters being a valid value for cluster_lmp_data. Also added a check to see if the horizon length exceed the raw_data length, meaning an empty data frame would be returned.

* Updated data processing workflow

Removed reconfigure data as it was poor coding practice (i.e., all functionality was done by generate_daily_data). Updated all tests to accommodate. Updated all functions in price_taker_model to accommodate. Also added warning messages for when sheet and column are not defined along with test for these warning messages.

* Small update to append_lmp_data

Updated lmp_data to allow for non-integer columns to be used. Ultimately, the user should be responsible for column naming and correlating LMP data. Code is now more general.

* Updated get_optimal_n_clusters

Found a case where the warning message should play (and added comments that a few test could be deleted as functions work properly but output may depend on external packages of kneed and scikit-learn).

Also fixed up get_optimal_n_clusters and cluster_lmp_data as there were some unnecessary loops, lines of code, and if statements to test.

* Updated cash flow construction

Updated the ability to make hourly cashflows. Now the user can provide all cashflows associated with all operation block models and the total cost and total revenue for each time point is generated to be used in the build cashflow function for the objective function. Still need to add tests for each of the logger messages (but the function appears to be working correctly).

* Added tests for cashflows

Added tests for some of the logger warning messages from the build_hourly_cashflows and build_cashflows functions.

Through these tests, most of the building code has also been tested. Remaining are two cases that will cover the rest of the build cases, options for the operational block, and the nonlinear case studies.

Also, I am considering adding a function to add the capacity limitation (both linear and nonlinear w/ and w/o linearization).

* Fixed typos and ran black

Ran black to adhere to code formatting guidelines.

* Run black

Finished running black on a missed file from last commit.

* Fixed some broken tests

Broken file tests fixed for other systems. Also fixed a typo in the startup/shutdown assignment constraint.

* Removed a test that is failing

This test may produce different results based on what machine it is run from. Removed, but this will cause a couple lines of code to be uncovered.

* Reverted pytests.ini and corrected tests

Incorporated changes recommended by lbianchi-lbl to avoid conflicts and to correct an uncommitted change to the price_taker_model file.

* Updated plotting test

Updated the plot test to now plt.show() the figure. Instead, the user will use plt.show() if desired.

* Ran Black again

Ran black to format recent changes. Trying to make sure current tests succeed on the builds.

* Updating tests

Made a new test and fixed another. Wanted to cover some stray lines of code that were not run from the tests originally.

* Add capacity limits function

Added a function to add capacity limits for a given commodity. This was originally meant to be a user activity with linearization called at the operation_and_design_models level, however it appears it would be better in the price-taker-class itself. Discussions with Krishna have led us to use a NotImplementedError for a couple other things as well. All code now tested (except multi-year time horizons). Final testing offline for model format and finishing docstrings.

* Added final docstring

Finished adding docstrings to all complex functions.

* Ran black

Ran black to reformat the changed files.

* Bugfix capacity constraints

Fixed a big where I accidentally called the lower bound twice instead of the lower bound AND the upper bound. Code has been tested and lines up with solutions done in a more "by-hand" fashion.

* Removed multiyear LMP support

Decided to let multiyear LMP analysis to rest on the user instead of generalizing that example within the price-taker-class.

* Delete idaes/apps/grid_integration/multiperiod/ERCOT_WEST_2022_shutdown_formulation_NEW.csv

Removed extraneous file added by accident.

* Update docstring for n_clusters

Should resolve sphinx warnings.

Co-authored-by: Ludovico Bianchi <lbianchi@lbl.gov>

* Fixed typos.

* Removed design_blk as an input for Operation_Model

Removed design_blk and assume the user will take care of this as an additional argument in the 'model_args' keyword.

* Fixed typo in error message in tests

* Replace deepgetattr with find_component()

As discussed, replacing the borrowed deepgetattr function with the more "Pyomothonic" find_component method.

* Changed dependence of tests on .xlsx to .csv

Updated the test dependence to be on .csv files, per request.

* Changed deepgetattr functionality to find_component

Updated the borrowed deepgetattr() function to be more "Pyomothonic" with the native find_component() function.

Also updated .xlsx dependency of the test functions to be dependent on .csv file types as requested.

* Updated test to remove .xlsx dependencies

Removed .xlsx dependency for testing and removed deprecated .xlsx test files. Added a shortened .csv file to replace the shortened .xlsx file.

* Update on build_hourly_cashflows docstring

Updating to fix Sphinx build issues.

* Update for Sphinx again

* Ran black

* Updated SkeletonUnitModelData to ProcessBlockData

Using lighter weight parent class for design and operation model blocks.

* Made sklearn and kneed optional, added log msgs

Made sklearn and kneed optional imports to perform clustering. Logger messages and import errors added for when functions are called that use these packages. Updated setup.py to reflect that these are no longer new dependencies.

* Updated tests for price taker class

Updated tests for optional imports. Moved plotting test to within a separate function.

Ran black on all code.

* Fixed optional import tests

Split tests so that optional import tests are now all separated from tests that do not use optional imports.

* Ran black

* Bugfix kmeans for LMP

* merged idaes main

* Revert "merged idaes main"

This reverts commit 46a3331.

* Add initial documentation for pricetaker

* Add autoclass to documentation

* Fix typos in documentation

* Reformat how math equations are handled

* add subtle additions to price-taker model used in workhop

* Test for seed instance and seed ValueError

* Check for expected string outputs rather than using f-strings

* Separate tests that check for multiple error messages

* Separate a majority of the code outside of the pytest.raises

* Update imports

* Add version check for sklearn

* Add separate function & test for generating elbow plots

* Refines version testing and # of optimal clusters testing

* Remove unused imports

* Correct acronym in pricetaker documentation

* Remove commented code in design_and_operation_models

* Add dependencies for scikit-learn and kneed

* Update imports and seed.setter method

* Resolve test failures

* Update pricetaker to not create new dependencies

* Update pricetaker testing based on previous commit

* Refine docstring for compute_sse method

* Make compute_sse a prviate method & move outside of class

* Update compute_sse docstring

* Added tests for design and operation model classes

* repair failing check by updating config option name; also update exception message

* black

* add more exception handling

* Update docs/reference_guides/apps/grid_integration/multiperiod/Price_Taker.rst

Co-authored-by: Adam Atia <aatia@keylogic.com>

* blk

* try to fix docstrings to mitigate sphinx error

* Remove unnecessary f-strings

* Remove/re-add commented code in price taker testing

* Add logger messages for when model_func is not defined

* Refactor: Added clustering and unit commitment

* Empty commit to test permission

* Run Black

* Update clustering.py and the associated test file

* Refine clustering testing

* Updated clustering methods

* Added UnitCommitmentData class for data processing

* Updated tests for the refactored code

* Added separate tests for rep days which can be skipped

* Created a new folder called pricetaker

* Updated import paths

* Fixed sphinx error

* Fix sphinx doc string failure

* Added a method for adding linking constraints

* Fixed a typo in the function name

---------

Co-authored-by: Radhakrishna <radhakrishnatg@gmail.com>
Co-authored-by: adam-a-a <aatia@keylogic.com>
Co-authored-by: MarcusHolly <marcus.holly@keylogic.com>
Co-authored-by: Ludovico Bianchi <lbianchi@lbl.gov>
Co-authored-by: Tyler Jaffe <tyler.jaffe@netl.doe.gov>
Co-authored-by: Daniel Laky <daniellaky@Daniels-MacBook-Pro.local>
Co-authored-by: Daniel Laky <29078718+dlakes94@users.noreply.github.com>
Co-authored-by: Daniel Laky <daniellaky@Daniels-MBP.salemcourthouse.com>
Co-authored-by: MarcusHolly <96305519+MarcusHolly@users.noreply.github.com>
Co-authored-by: Keith Beattie <ksbeattie@lbl.gov>
Co-authored-by: Radhakrishna Tumbalam Gooty <42144353+radhakrishnatg@users.noreply.github.com>
  • Loading branch information
12 people authored Feb 14, 2025
1 parent ce941ce commit 90908a3
Show file tree
Hide file tree
Showing 19 changed files with 3,550 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ equil = "equil"
astroid = "astroid"
# delt == delta
delt = "delt"
# FOM for fixed operating and maintenance for DISPATCHES
FOM = "FOM"
fom = "fom"
# Minimal Infeasible System
mis = "mis"
MIS = "MIS"
Expand Down
1 change: 1 addition & 0 deletions docs/reference_guides/apps/grid_integration/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ wholesale electricity markets. For more information, please look at the introduc
Bidder
Tracker
Coordinator
multiperiod/index

Cite us
^^^^^^^
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
Price Taker
===========

Price takers are entities that must accept market prices since they lack the market share
to directly influence the market price. Likewise, it is assumed that a price taker's resource or energy
system is small enough such that it does not significantly impact the market. When coupled with multi-period modeling,
the price-taker model is able to synthesize grid-centric modeling with steady-state process-centric modeling, as
depicted in figure below.

.. |pricetaker| image:: images/pricetaker.png
:width: 1200
:alt: Alternative text
:align: middle

|pricetaker|

The following equations represent the multi-period price taker model, where :math:`d` are design decisions,
:math:`u` are operating decisions, :math:`δ` are power decisions, :math:`s` are scenarios (timeseries/representative days),
:math:`w` is weight/frequency, :math:`R` is revenue, :math:`π` is price data,
:math:`C` is capital and operating costs, :math:`g` is the process model, and :math:`h` is the temporal constraint.

.. math::
max_{d,u, δ} = \sum_{s ∈ S} \sum_{t ∈ T} w_{s}[R(d,u_{s,t},δ_{s,t},π_{s,t}) - C(d,u_{s,t},δ_{s,t})]
.. math::
g(d,u_{s,t},δ_{s,t}) = 0; ∀_{s} ∈ S, t ∈ T
.. math::
h(d,u_{s,t},δ_{s,t},u_{s,t+1},δ_{s,t+1}) = 0; ∀_{s} ∈ S, t ∈ T
The price taker multi-period modeling workflow involves the integration of multiple software platforms into the IDAES optimization model
and can be broken down into two distinct functions, as shown in the figure below. In part 1, simulated or historical
ISO (Independent System Operator) data is used to generate locational marginal price (LMP)
signals, and production cost models (PCMs) are used to compute and optimize the time-varying dispatch schedules for each
resource based on their respective bid curves. Advanced data analytics (RAVEN) reinterpret the LMP signals and PCM
as stochastic realizations of the LMPs in the form of representative days (or simply the full-year price signals).
In part 2, PRESCIENT uses a variety of input parameters (design capacity, minimum power output, ramp rate, minimum up/down time, marginal cost, no load cost, and startup profile)
to generate data for the market surrogates. Meanwhile, IDAES uses the double loop simulation to integrate detailed
process models (b, ii) into the daily (a, c) and hourly (i, iii) grid operations workflow.

.. |hybrid_energy_system| image:: images/hybrid_energy_system.png
:width: 1200
:alt: Alternative text
:align: middle

|hybrid_energy_system|

.. module:: idaes.apps.grid_integration.pricetaker.price_taker_model

.. autoclass:: PriceTakerModel
:members:
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions docs/reference_guides/apps/grid_integration/multiperiod/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Multi-Period Modeling
=====================

Multi-period modeling can be used to simplify dynamic optimization problems
by linking steady-state models over a time horizon with rate-limiting constraints.
Therefore, sets of constraints at one temporal index will affect decisions taken
at a different moment in time. These interactions can be used to model the relationship
between the energy systems and wholesale electricity markets.

.. toctree::
:maxdepth: 2

Price_Taker
5 changes: 5 additions & 0 deletions idaes/apps/grid_integration/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,8 @@
from .coordinator import DoubleLoopCoordinator
from .forecaster import PlaceHolderForecaster
from .multiperiod.multiperiod import MultiPeriodModel
from .pricetaker.price_taker_model import PriceTakerModel
from .pricetaker.design_and_operation_models import (
DesignModel,
OperationModel,
)
Empty file.
252 changes: 252 additions & 0 deletions idaes/apps/grid_integration/pricetaker/clustering.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
#################################################################################
# The Institute for the Design of Advanced Energy Systems Integrated Platform
# Framework (IDAES IP) was produced under the DOE Institute for the
# Design of Advanced Energy Systems (IDAES).
#
# Copyright (c) 2018-2023 by the software owners: The Regents of the
# University of California, through Lawrence Berkeley National Laboratory,
# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon
# University, West Virginia University Research Corporation, et al.
# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md
# for full copyright and license information.
#################################################################################

"""
Contains functions for clustering the price signal into representative days/periods.
"""

from typing import Union
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from pyomo.common.dependencies import attempt_import
import idaes.logger as idaeslog

sklearn, sklearn_avail = attempt_import("sklearn")

if sklearn_avail:
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score

_logger = idaeslog.getLogger(__name__)


def generate_daily_data(raw_data: list, horizon_length: int):
"""
Function used to generate the daily data in a usable format
from the raw data provided.
Args:
raw_data : list,
Columnar data for a given LMP signal
horizon_length : int,
Length of each representative day/period
Returns:
daily_data: pd.DataFrame,
Correctly formatted daily LMP data for later use
"""
# Converting the data to a list (Needed if the input is not a list, e.g., pd.DataFrame)
raw_data = list(raw_data)

if horizon_length > len(raw_data):
raise ValueError(
f"Horizon length {horizon_length} exceeds the price signal length of {len(raw_data)}"
)

elements_ignored = len(raw_data) % horizon_length
if elements_ignored:
_logger.warning(
f"Length of the price signal is not an integer multiple of horizon_length.\n"
f"\tIgnoring the last {elements_ignored} elements in the price signal."
)

daily_data = {
j: raw_data[((j - 1) * horizon_length) : (j * horizon_length)]
for j in range(1, (len(raw_data) // horizon_length) + 1)
}

# DataFrame arranges the data for each day as a column. Since most clustering techniques
# require different samples as rows, we take the transpose before returning the data.
return pd.DataFrame(daily_data).transpose()


def cluster_lmp_data(
raw_data: list, horizon_length: int, n_clusters: int, seed: int = 42
):
"""
Clusters the given price signal into n_clusters using the k-means clustering
technique.
Args:
raw_data: list,
Columnar data for a given LMP signal.
horizon_length: int,
Length of each cluster (representative day/period)
n_clusters: int,
Number of clusters desired for the data (representative days).
seed: int,
Seed value for initializing random number generator within Kmeans
Returns:
lmp_data_clusters: dict
A dictionary of representative day LMP data, indices are indexed
by integers starting at 1. Example: ::
{1: {1: 4, 2: 3, 3: 5},
2: {1: 1, 2: 7, 3: 3}}
weights: dict
A dictionary of weights for each representative day, indexed the
same way as lmp_data. Example: ::
{1: 45, 2: 56}
"""
# reconfiguring raw data
daily_data = generate_daily_data(raw_data, horizon_length)

# KMeans clustering with the optimal number of clusters
kmeans = KMeans(n_clusters=n_clusters, random_state=seed).fit(daily_data)
centroids = kmeans.cluster_centers_
labels = kmeans.labels_

# Set any centroid values that are < 1e-4 to 0 to avoid noise
centroids = centroids * (abs(centroids) >= 1e-4)

# Create dicts for lmp data and the weight of each cluster
# By default, the data is of type numpy.int or numpy.float.
# Converting the data to python int/float. Otherwise, Pyomo complains!
num_days, num_time_periods = centroids.shape
lmp_data_clusters = {
d + 1: {t + 1: float(centroids[d, t]) for t in range(num_time_periods)}
for d in range(num_days)
}
weights = {d + 1: int(sum(labels == d)) for d in range(num_days)}

return lmp_data_clusters, weights


def get_optimal_num_clusters(
samples: Union[pd.DataFrame, np.array],
kmin: int = 2,
kmax: int = 30,
method: str = "silhouette",
generate_elbow_plot: bool = False,
seed: int = 42,
):
"""
Determines the appropriate number of clusters needed for a
given price signal.
Args:
samples: pd.DataFrame | np.array,
Set of points with rows containing samples and columns
containing features
kmin : int,
Minimum number of clusters
kmax : int,
Maximum number of clusters
method : str,
Method for obtaining the optimal number of clusters.
Supported methods are elbow and silhouette
generate_elbow_plot : bool,
If True, generates an elbow plot for inertia as a function of
number of clusters
seed : int,
Seed value for random number generator
Returns:
n_clusters: int,
The optimal number of clusters for the given data
"""
if kmin >= kmax:
raise ValueError(f"kmin must be less than kmax, but {kmin} >= {kmax}")

# For silhouette method, kmin must be 2. So, we require kmin >= 2 in general
if kmin <= 1:
raise ValueError(f"kmin must be > 1. Received kmin = {kmin}")

if kmax >= len(samples):
raise ValueError(f"kmax must be < len(samples). Received kmax = {kmax}")

k_values = list(range(kmin, kmax + 1))
inertia_values = []
mean_silhouette = []

for k in k_values:
kmeans = KMeans(n_clusters=k, random_state=seed).fit(samples)
inertia_values.append(kmeans.inertia_)

if method == "silhouette":
# Calculate the average silhouette score, if the chosen method
# is silhouette
mean_silhouette.append(silhouette_score(samples, kmeans.labels_))

# Identify the optimal number of clusters
if method == "elbow":
raise NotImplementedError(
"elbow method is not supported currently for finding the optimal "
"number of clusters."
)

elif method == "silhouette":
max_index = mean_silhouette.index(max(mean_silhouette))
n_clusters = k_values[max_index]

else:
raise ValueError(
f"Unrecognized method {method} for optimal number of clusters."
f"\tSupported methods include elbow and silhouette."
)

_logger.info(f"Optimal number of clusters is: {n_clusters}")

if n_clusters + 2 >= kmax:
_logger.warning(
f"Optimal number of clusters is close to kmax: {kmax}. "
f"Consider increasing kmax."
)

if generate_elbow_plot:
plt.plot(k_values, inertia_values)
plt.axvline(x=n_clusters, color="red", linestyle="--", label="Elbow")
plt.xlabel("Number of clusters")
plt.ylabel("Inertia")
plt.title("Elbow Method")
plt.xlim(kmin, kmax)
plt.show()

return n_clusters


def locate_elbow(x: list, y: list):
"""
Identifies the elbow/knee for the input/output data
Args:
x : list
List of independent variables
y : list
List of dependent variables
Returns:
opt_x : float
Optimal x at which curvature changes significantly
"""
# The implementation is based on
# Ville Satopaa, Jeannie Albrecht, David Irwin, Barath Raghavan
# Finding a “Kneedle” in a Haystack:
# Detecting Knee Points in System Behavior
# https://raghavan.usc.edu/papers/kneedle-simplex11.pdf

return len(np.array(x)) + len(np.array(y))
Loading

0 comments on commit 90908a3

Please sign in to comment.