Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ValueCurves from PowerSystems to infrasys #38

Merged
merged 38 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
d2d8fbb
initial class definitions
jerrypotts Jul 2, 2024
bcd493b
add get_slopes
jerrypotts Jul 8, 2024
b24bdd0
add InputOutput Conversions
jerrypotts Jul 8, 2024
28139dc
Added IncrementalCurve conversions
jerrypotts Jul 8, 2024
d7cf0bc
Add AverageRate conversion functions
jerrypotts Jul 8, 2024
aef67bc
docstrings
jerrypotts Jul 8, 2024
858db26
add function_data tests
jerrypotts Jul 8, 2024
8e801cd
add initial test functions
jerrypotts Jul 8, 2024
4f6f50f
additional conversion tests
jerrypotts Jul 9, 2024
e019bc4
Modification of the docstings and mypy compliant
pesap Aug 2, 2024
27fa407
initial class definitions
jerrypotts Jul 2, 2024
ed074a0
add get_slopes
jerrypotts Jul 8, 2024
85b7cc0
add InputOutput Conversions
jerrypotts Jul 8, 2024
392944b
Added IncrementalCurve conversions
jerrypotts Jul 8, 2024
b4ff639
Add AverageRate conversion functions
jerrypotts Jul 8, 2024
3fe0ddf
docstrings
jerrypotts Jul 8, 2024
4355de0
add function_data tests
jerrypotts Jul 8, 2024
c1030d6
add initial test functions
jerrypotts Jul 8, 2024
336f961
additional conversion tests
jerrypotts Jul 9, 2024
a6109a4
Modification of the docstings and mypy compliant
pesap Aug 2, 2024
0dba29f
Merge branch 'jp/cost_curves' of github.com:jerrypotts/infrasys into …
pesap Aug 2, 2024
5552034
Fixing pytest
pesap Aug 5, 2024
db5a692
Adding ruff rule EM and making the code compliant.
pesap Aug 5, 2024
b6e03a5
Propagating rule EM
pesap Aug 5, 2024
5916fd5
More code compliant with EM
pesap Aug 5, 2024
c6f1053
Fixing test
pesap Aug 5, 2024
d841464
Adding abstract classes
pesap Aug 12, 2024
93d2711
add conversions to incremental and average rate
jerrypotts Aug 12, 2024
45658c8
re-add function data tests
jerrypotts Aug 12, 2024
6f5af67
update test functions
jerrypotts Aug 12, 2024
c8a10ee
initial class definitions for cost and fuel curves
jerrypotts Aug 12, 2024
a01c0a2
change conversions to convert everything to InputOutputCurve
jerrypotts Aug 13, 2024
78fc1b1
update test functions for correct ValueCurve conversions
jerrypotts Aug 13, 2024
6a184d9
add docstrings to CostCurve and FuelCurve
jerrypotts Aug 13, 2024
8d4f081
add test functions for CostCurve and FuelCurve
jerrypotts Aug 13, 2024
b8d88d5
update file names
jerrypotts Aug 14, 2024
8f9d4e1
numpy convention for class docstrings
jerrypotts Aug 14, 2024
fe64318
change if statement to match case
jerrypotts Aug 14, 2024
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
9 changes: 9 additions & 0 deletions docs/reference/api/function_data.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
```{eval-rst}
.. _function-data-api:
```
# Function data

```{eval-rst}
.. automodule:: infrasys.function_data
:members:
```
1 change: 1 addition & 0 deletions docs/reference/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@
time_series
location
quantities
function_data
```
9 changes: 5 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,13 @@ target-version = "py311"
[tool.ruff.lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
select = [
"C901", # McCabe complexity
"E4", # Subset of pycodestyle (E)
"C901", # McCabe complexity
"E4", # Subset of pycodestyle (E)
"E7",
"E9",
"F", # Pyflakes
"W", # pycodestyle warnings
"EM", # string formatting in an exception message
"F", # Pyflakes
"W", # pycodestyle warnings
]
ignore = []

Expand Down
3 changes: 2 additions & 1 deletion src/infrasys/base_quantity.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ def __new__(cls: Type["BaseQuantity"], value, units=None):

def __init_subclass__(cls, **kwargs):
if not cls.__base_unit__:
raise TypeError("__base_unit__ should be defined")
msg = "__base_unit__ should be defined"
raise TypeError(msg)
super().__init_subclass__(**kwargs)

# Required for pydantic validation
Expand Down
3 changes: 2 additions & 1 deletion src/infrasys/component_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,8 @@ def deepcopy(self, component: Component) -> Component:

def change_uuid(self, component: Component) -> None:
"""Change the component UUID."""
raise NotImplementedError("change_component_uuid")
msg = "change_component_uuid"
raise NotImplementedError(msg)

def update(
self,
Expand Down
59 changes: 59 additions & 0 deletions src/infrasys/cost_curves.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from typing_extensions import Annotated
from infrasys.component import Component
from pydantic import Field
from infrasys.value_curves import InputOutputCurve, IncrementalCurve, AverageRateCurve
from infrasys.function_data import LinearFunctionData


class ProductionVariableCostCurve(Component):
name: Annotated[str, Field(frozen=True)] = ""


class CostCurve(ProductionVariableCostCurve):
"""Direct representation of the variable operation cost of a power plant in currency.

Composed of a Value Curve that may represent input-output, incremental, or average rate
data. The default units for the x-axis are MW and can be specified with
`power_units`.
"""

value_curve: Annotated[
InputOutputCurve | IncrementalCurve | AverageRateCurve,
Field(
description="The underlying `ValueCurve` representation of this `ProductionVariableCostCurve`"
),
]
vom_units: Annotated[
InputOutputCurve,
Field(description="(default: natural units (MW)) The units for the x-axis of the curve"),
] = InputOutputCurve(
function_data=LinearFunctionData(proportional_term=0.0, constant_term=0.0)
)


class FuelCurve(ProductionVariableCostCurve):
"""Representation of the variable operation cost of a power plant in terms of fuel.

Fuel units (MBTU, liters, m^3, etc.) coupled with a conversion factor between fuel and currency.
Composed of a Value Curve that may represent input-output, incremental, or average rate data.
The default units for the x-axis are MW and can be specified with `power_units`.
"""

value_curve: Annotated[
InputOutputCurve | IncrementalCurve | AverageRateCurve,
Field(
description="The underlying `ValueCurve` representation of this `ProductionVariableCostCurve`"
),
]
vom_units: Annotated[
InputOutputCurve,
Field(description="(default: natural units (MW)) The units for the x-axis of the curve"),
] = InputOutputCurve(
function_data=LinearFunctionData(proportional_term=0.0, constant_term=0.0)
)
fuel_cost: Annotated[
float,
Field(
description="Either a fixed value for fuel cost or the key to a fuel cost time series"
),
]
135 changes: 83 additions & 52 deletions src/infrasys/function_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,158 +15,189 @@ class XYCoords(NamedTuple):
y: float


class LinearFunctionData(Component):
class FunctionData(Component):
"""BaseClass of FunctionData"""

name: Annotated[str, Field(frozen=True)] = ""


class LinearFunctionData(FunctionData):
"""Data representation for linear cost function.

Class to represent the underlying data of linear functions. Principally used for
the representation of cost functions `f(x) = proportional_term*x + constant_term`.
Used to represent linear cost functions of the form

.. math:: f(x) = mx + c,

where :math:`m` is the proportional term and :math:`c` is the constant term.
"""

name: Annotated[str, Field(frozen=True)] = ""
proportional_term: Annotated[
float, Field(description="the proportional term in the represented function")
float, Field(description="the proportional term in the represented function.")
]
constant_term: Annotated[
float, Field(description="the constant term in the represented function")
float, Field(description="the constant term in the represented function.")
]


class QuadraticFunctionData(Component):
"""Data representation for quadratic cost functions.
class QuadraticFunctionData(FunctionData):
"""Data representation for quadratic cost function.

Used to represent quadratic of cost functions of the form

.. math:: f(x) = ax^2 + bx + c,

Class to represent the underlying data of quadratic functions. Principally used for the
representation of cost functions
`f(x) = quadratic_term*x^2 + proportional_term*x + constant_term`.
where :math:`a` is the quadratic term, :math:`b` is the proportional term and :math:`c` is the
constant term.
"""

name: Annotated[str, Field(frozen=True)] = ""
quadratic_term: Annotated[
float, Field(description="the quadratic term in the represented function")
float, Field(description="the quadratic term in the represented function.")
]
proportional_term: Annotated[
float, Field(description="the proportional term in the represented function")
float, Field(description="the proportional term in the represented function.")
]
constant_term: Annotated[
float, Field(description="the constant term in the represented function")
float, Field(description="the constant term in the represented function.")
]


def validate_piecewise_linear_x(points: List[XYCoords]) -> List[XYCoords]:
"""Validates the x data for PiecewiseLinearData class

Function used to validate given x data for the PiecewiseLinearData class.
X data is checked to ensure there is at least two values of x,
which is the minimum required to generate a cost curve, and is
given in ascending order (e.g. [1, 2, 3], not [1, 3, 2]).
X data is checked to ensure there is at least two values of x, which is the minimum required to
generate a cost curve, and is given in ascending order (e.g. [1, 2, 3], not [1, 3, 2]).

Parameters
----------
points : List[XYCoords]
List of named tuples of (x,y) coordinates for cost function

Returns
----------
-------
points : List[XYCoords]
List of (x,y) data for cost function after successful validation.
"""

x_coords = [p.x for p in points]

if len(x_coords) < 2:
raise ValueError("Must specify at least two x-coordinates")
msg = "Must specify at least two x-coordinates."
raise ValueError(msg)
if not (
x_coords == sorted(x_coords)
or (np.isnan(x_coords[0]) and x_coords[1:] == sorted(x_coords[1:]))
):
raise ValueError(f"Piecewise x-coordinates must be ascending, got {x_coords}")
msg = f"Piecewise x-coordinates must be ascending, got {x_coords}."
raise ValueError(msg)

return points


def validate_piecewise_step_x(x_coords: List[float]) -> List[float]:
"""Validates the x data for PiecewiseStepData class

Function used to validate given x data for the PiecewiseStepData class.
X data is checked to ensure there is at least two values of x,
which is the minimum required to generate a cost curve, and is
given in ascending order (e.g. [1, 2, 3], not [1, 3, 2]).
X data is checked to ensure there is at least two values of x, which is the minimum required to
generate a cost curve, and is given in ascending order (e.g. [1, 2, 3], not [1, 3, 2]).

Parameters
----------
x_coords : List[float]
List of x data for cost function.

Returns
----------
-------
x_coords : List[float]
List of x data for cost function after successful validation.
"""

if len(x_coords) < 2:
raise ValueError("Must specify at least two x-coordinates")
msg = "Must specify at least two x-coordinates."
raise ValueError(msg)
if not (
x_coords == sorted(x_coords)
or (np.isnan(x_coords[0]) and x_coords[1:] == sorted(x_coords[1:]))
):
raise ValueError(f"Piecewise x-coordinates must be ascending, got {x_coords}")
msg = f"Piecewise x-coordinates must be ascending, got {x_coords}."
raise ValueError(msg)

return x_coords


class PiecewiseLinearData(Component):
class PiecewiseLinearData(FunctionData):
"""Data representation for piecewise linear cost function.

Class to represent piecewise linear data as a series of points: two points define one
segment, three points define two segments, etc. The curve starts at the first point given,
not the origin. Principally used for the representation of cost functions where the points
store quantities (x, y), such as (MW, USD/h).
Used to represent linear data as a series of points: two points define one segment, three
points define two segments, etc. The curve starts at the first point given, not the origin.
Principally used for the representation of cost functions where the points store quantities (x,
y), such as (MW, USD/h).
"""

name: Annotated[str, Field(frozen=True)] = ""
points: Annotated[
List[XYCoords],
AfterValidator(validate_piecewise_linear_x),
Field(description="list of (x,y) points that define the function"),
Field(description="list of (x,y) points that define the function."),
]


class PiecewiseStepData(Component):
class PiecewiseStepData(FunctionData):
"""Data representation for piecewise step cost function.

Class to represent a step function as a series of endpoint x-coordinates and segment
Used to represent a step function as a series of endpoint x-coordinates and segment
y-coordinates: two x-coordinates and one y-coordinate defines a single segment, three
x-coordinates and two y-coordinates define two segments, etc. This can be useful to
represent the derivative of a `PiecewiseLinearData`, where the y-coordinates of this
step function represent the slopes of that piecewise linear function.
Principally used for the representation of cost functions where the points store
quantities (x, dy/dx), such as (MW, USD/MWh).
x-coordinates and two y-coordinates define two segments, etc.

This can be useful to represent the derivative of a :class:`PiecewiseLinearData`, where the
y-coordinates of this step function represent the slopes of that piecewise linear function.
Principally used for the representation of cost functions where the points store quantities (x,
:math:`dy/dx`), such as (MW, USD/MWh).
"""

name: Annotated[str, Field(frozen=True)] = ""
x_coords: Annotated[
List[float],
Field(description="the x-coordinates of the endpoints of the segments"),
Field(description="the x-coordinates of the endpoints of the segments."),
]
y_coords: Annotated[
List[float],
Field(
description="the y-coordinates of the segments: `y_coords[1]` is the y-value \
between `x_coords[0]` and `x_coords[1]`, etc. Must have one fewer elements than `x_coords`."
description=(
"The y-coordinates of the segments: `y_coords[1]` is the y-value between "
"`x_coords[0]` and `x_coords[1]`, etc. Must have one fewer elements than `x_coords`."
)
),
]

@model_validator(mode="after")
def validate_piecewise_xy(self):
"""Method to validate the x and y data for PiecewiseStepData class

Model validator used to validate given data for the PiecewiseStepData class.
Calls `validate_piecewise_step_x` to check if `x_coords` is valid, then checks if
the length of `y_coords` is exactly one less than `x_coords`, which is necessary
to define the cost functions correctly.
Model validator used to validate given data for the :class:`PiecewiseStepData`. Calls
`validate_piecewise_step_x` to check if `x_coords` is valid, then checks if the length of
`y_coords` is exactly one less than `x_coords`, which is necessary to define the cost
functions correctly.
"""
validate_piecewise_step_x(self.x_coords)

if len(self.y_coords) != len(self.x_coords) - 1:
raise ValueError("Must specify one fewer y-coordinates than x-coordinates")
msg = "Must specify one fewer y-coordinates than x-coordinates"
raise ValueError(msg)

return self


def get_x_lengths(x_coords: List[float]) -> List[float]:
return np.subtract(x_coords[1:], x_coords[:-1])


def running_sum(data: PiecewiseStepData) -> List[XYCoords]:
points = []
slopes = data.y_coords
x_coords = data.x_coords
x_lengths = get_x_lengths(x_coords)
running_y = 0.0

points.append(XYCoords(x=x_coords[0], y=running_y))
for prev_slope, this_x, dx in zip(slopes, x_coords[1:], x_lengths):
running_y += prev_slope * dx
points.append(XYCoords(x=this_x, y=running_y))

return points
3 changes: 2 additions & 1 deletion src/infrasys/parquet_time_series_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ def get_time_series(
start_time: datetime | None = None,
length: int | None = None,
) -> TimeSeriesData:
raise NotImplementedError("ParquetTimeSeriesStorage.get_time_series")
msg = "ParquetTimeSeriesStorage.get_time_series"
raise NotImplementedError(msg)

def remove_time_series(self, uuid: UUID) -> None:
...
11 changes: 6 additions & 5 deletions src/infrasys/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ def to_json(self, filename: Path | str, overwrite=False, indent=None, data=None)
data = system_data
else:
if "system" in data:
raise ISConflictingArguments("data contains the key 'system'")
msg = "data contains the key 'system'"
raise ISConflictingArguments(msg)
data["system"] = system_data
with open(filename, "w", encoding="utf-8") as f_out:
json.dump(data, f_out, indent=indent)
Expand Down Expand Up @@ -337,9 +338,8 @@ def save(
fpath = Path(fpath)

if fpath.exists() and not overwrite:
raise FileExistsError(
f"{fpath} exists already. To overwrite the folder pass `overwrite=True`"
)
msg = f"{fpath} exists already. To overwrite the folder pass `overwrite=True`"
raise FileExistsError(msg)

fpath.mkdir(parents=True, exist_ok=True)
self.to_json(fpath / filename, overwrite=overwrite)
Expand Down Expand Up @@ -991,7 +991,8 @@ def handle_data_format_upgrade(

def merge_system(self, other: "System") -> None:
"""Merge the contents of another system into this one."""
raise NotImplementedError("merge_system")
msg = "merge_system"
raise NotImplementedError(msg)

# TODO: add delete methods that (1) don't raise if not found and (2) don't return anything?

Expand Down
Loading
Loading