Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,51 @@ def is_valid_polynomial_surrogate_data(data: dict):
return new_data


def is_valid_startup_types(data):
"""Validate if the startup_types received is valid"""

if not isinstance(data, dict):
raise TypeError("Data must be a dictionary.")

if len(data) == 0:
Copy link
Contributor

Choose a reason for hiding this comment

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

Based on your tests, I'm wondering if we should change this to if len(data) <= 1:. This method must be used only when there is more than one startup type. If you have only startup type, then this feels unnecessary (Also, we will have to check the minimum uptime and startup time values to make sure they are consistent). Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I think the suggestion makes sense to me. Eq. 54 in Ben's paper once confused me until I asked for clarification from him. If {"hot": 4, "warm": 8, "cold": 12}, when the downtime is between 4 to 8, it is a hot start; 8 to 12, warm start and 12 to infinity, a cold start. This is important to understand eq54, if only one startup_transition_time is given, eq54 will break down.
I will add this example to the comments.

# set to None instead of an empty dict.
raise ConfigurationError("Received an empty dictionary for startup types")

if len(data) == 1:
raise ConfigurationError(
"At least two startup types must be defined for the unit/process. \n"
'if startup_types = {"hot": 4, "warm": 8, "cold": 12}, \n'
"then when downtime is between 4 to 8 -> hot startup; \n"
"when downtime is between 8 to 12 -> warm startup; \n"
"when downtime is greater than 12 -> cold startup. \n"
)

# check the key names and make sure space is handled correctly.
# use a method to check if a string is a valid variable name. string.isidentifier() works for now.

for key, value in data.items():
if not isinstance(key, str):
raise TypeError("key must be a valid string.")

if not key.isidentifier():
raise ConfigurationError(
f"Key '{key}' is not a valid Python variable name. "
"Keys must be valid identifiers."
)

if not isinstance(value, int):
raise TypeError("value must be an int")

# Values must be unique, as they correspond to different startup types
if len(data.values()) > len(set(data.values())):
raise ConfigurationError(
"Startup time for two or more startup types is the same."
)

# Return a dictionary after sorting based on values
return dict(sorted(data.items(), key=lambda item: item[1]))


# pylint: disable = attribute-defined-outside-init, too-many-ancestors
# pylint: disable = invalid-name, logging-fstring-interpolation
@declare_process_block_class("DesignModel")
Expand Down Expand Up @@ -395,6 +440,14 @@ def my_operation_model(m, design_blk):
),
)

CONFIG.declare(
"startup_types",
ConfigValue(
domain=is_valid_startup_types,
doc="Dictionary of startup types and transition times for the unit/process",
),
)

# noinspection PyAttributeOutsideInit
def build(self):
super().build()
Expand All @@ -416,6 +469,13 @@ def build(self):
doc="Binary: 1 if the shutdown is initiated, 0 otherwise",
)

if self.config.startup_types is not None:
self.startup_type_vars = Var(
list(self.config.startup_types.keys()),
within=Binary,
doc="Binary: 1 if the startup type is active, 0 otherwise",
)

if self.config.declare_lmp_param:
self.LMP = Param(
initialize=1,
Expand Down
13 changes: 13 additions & 0 deletions idaes/apps/grid_integration/pricetaker/price_taker_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
DesignModelData,
OperationModelData,
StorageModelData,
is_valid_startup_types,
)
from idaes.apps.grid_integration.pricetaker.clustering import (
generate_daily_data,
Expand Down Expand Up @@ -156,6 +157,14 @@
),
)

CONFIG.declare(
"startup_types",
ConfigValue(
domain=is_valid_startup_types,
doc="Dictionary of startup types and their transition times for the unit/process",
),
)


# pylint: disable = too-many-ancestors, too-many-instance-attributes
class PriceTakerModel(ConcreteModel):
Expand Down Expand Up @@ -816,6 +825,7 @@ def add_startup_shutdown(
des_block_name: Optional[str] = None,
minimum_up_time: int = 1,
minimum_down_time: int = 1,
startup_transition_time: Optional[dict] = None,
):
"""
Adds minimum uptime/downtime constraints for a given unit/process
Expand All @@ -842,6 +852,8 @@ def add_startup_shutdown(
# Check minimum_up_time and minimum_down_time for validity
self.config.minimum_up_time = minimum_up_time
self.config.minimum_down_time = minimum_down_time
# Set startup transition times
self.config.startup_types = startup_transition_time

op_blocks = self._get_operation_blocks(
blk_name=op_block_name,
Expand Down Expand Up @@ -880,6 +892,7 @@ def add_startup_shutdown(
minimum_up_time=minimum_up_time,
minimum_down_time=minimum_down_time,
set_time=self.set_time,
startup_transition_time=startup_transition_time,
)

# Save the uptime and downtime data for reference
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,55 @@
_is_valid_data_type_for_storage_model,
is_valid_variable_design_data,
is_valid_polynomial_surrogate_data,
is_valid_startup_types,
)
from idaes.core.util.config import ConfigurationError


@pytest.mark.unit
def test_is_valid_startup_types_empty_dict():
"""Tests that is_valid_startup_types raises error for empty dictionary"""
with pytest.raises(
ConfigurationError, match="Received an empty dictionary for startup types"
):
is_valid_startup_types({})


@pytest.mark.unit
def test_is_valid_startup_types_invalid_inputs():
"""Tests that is_valid_startup_types rejects invalid inputs"""

with pytest.raises(TypeError, match="Data must be a dictionary."):
is_valid_startup_types([("hot", 4), ("warm", 8)])

with pytest.raises(TypeError, match="Data must be a dictionary."):
is_valid_startup_types(3.14)

with pytest.raises(
ConfigurationError,
match="At least two startup types must be defined for the unit/process.",
):
is_valid_startup_types({"hot": 4})

with pytest.raises(TypeError, match="key must be a valid string."):
is_valid_startup_types({1: 4, 2: 8})

with pytest.raises(
ConfigurationError,
match="Key 'hot-start' is not a valid Python variable name. Keys must be valid identifiers.",
):
is_valid_startup_types({"hot-start": 4, "warm-start": 8})

with pytest.raises(TypeError, match="value must be an int"):
is_valid_startup_types({"hot": 4.2, "warm": 8.1})

with pytest.raises(
ConfigurationError,
match="Startup time for two or more startup types is the same.",
):
is_valid_startup_types({"hot": 4, "warm": 4})


@pytest.mark.unit
def test_format_data():
"""Tests the _format_data function"""
Expand Down Expand Up @@ -270,6 +315,22 @@ def op_model(m, des_blk):
for attr in ["op_mode", "startup", "shutdown", "power", "LMP"]:
assert not hasattr(blk.unit_2_op, attr)

# test the multiple startup types
blk.unit_3_op = OperationModel(
model_func=op_model,
model_args={"des_blk": blk.unit_1_design},
startup_types={"hot": 4, "warm": 8, "cold": 12},
)
assert hasattr(blk.unit_3_op, "startup_type_vars")

# test the startup types = None
blk.unit_3_op = OperationModel(
model_func=op_model,
model_args={"des_blk": blk.unit_1_design},
startup_types=None,
)
assert not hasattr(blk.unit_3_op, "startup_type_vars")


@pytest.mark.unit
def test_operation_model_class_logger_message1(caplog):
Expand Down Expand Up @@ -397,6 +458,15 @@ def test_is_valid_data_type_for_storage_model():
)


@pytest.mark.unit
def test_is_valid_startup_types():
"""Tests the is_valid_startup_types function"""
# Test the correct data structure is returned
data = is_valid_startup_types({"warm": 8, "hot": 4, "cold": 12})
assert list(data.keys()) == ["hot", "warm", "cold"]
assert data == {"hot": 4, "warm": 8, "cold": 12}


@pytest.mark.unit
def test_storage_model_fixed_design():
"""Tests the StorageModel class with int/float-type arguments"""
Expand Down
Loading
Loading