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 10 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
```
4 changes: 4 additions & 0 deletions src/infrasys/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@ class ISNotStored(ISBaseException):

class ISOperationNotAllowed(ISBaseException):
"""Raised if the requested operation is not allowed."""


class ISMethodError(ISBaseException):
pesap marked this conversation as resolved.
Show resolved Hide resolved
"""Rased if the requested method is not allowed."""
146 changes: 112 additions & 34 deletions src/infrasys/function_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,43 +18,47 @@ class XYCoords(NamedTuple):
class LinearFunctionData(Component):
"""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.
"""Data representation for quadratic cost function.

Used to represent quadratic of cost functions of the form

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`.
.. math:: f(x) = ax^2 + bx + c,

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]).
Expand All @@ -65,28 +69,27 @@ def validate_piecewise_linear_x(points: List[XYCoords]) -> 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")
raise ValueError("Must specify at least two x-coordinates.")
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}")
raise ValueError(f"Piecewise x-coordinates must be ascending, got {x_coords}.")
pesap marked this conversation as resolved.
Show resolved Hide resolved

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]).
Expand All @@ -97,69 +100,72 @@ def validate_piecewise_step_x(x_coords: List[float]) -> 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")
raise ValueError("Must specify at least two x-coordinates.")
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}")
raise ValueError(f"Piecewise x-coordinates must be ascending, got {x_coords}.")

return x_coords


class PiecewiseLinearData(Component):
"""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):
"""Data representation for piecewise step cost function.

Class 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.
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
: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, dy/dx), such as (MW, USD/MWh).
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.
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.
Expand All @@ -170,3 +176,75 @@ def validate_piecewise_xy(self):
raise ValueError("Must specify one fewer y-coordinates than x-coordinates")

return self


def get_slopes(vc: List[XYCoords]) -> List[float]:
"""Calculate slopes from XYCoord data

Slopes are calculated between each section of the piecewise curve.
Returns a list of slopes that can be used to define Value Curves.

Parameters
----------
vc : List[XYCoords]
List of named tuples of (x, y) coordinates.

Returns
----------
slopes : List[float]
List of slopes for each section of given piecewise linear data.
"""
slopes = []
(prev_x, prev_y) = vc[0]
for comp_x, comp_y in vc[1:]:
slopes.append((comp_y - prev_y) / (comp_x - prev_x))
(prev_x, prev_y) = (comp_x, comp_y)
return slopes


def get_x_lengths(x_coords: List[float]) -> List[float]:
"""Calculates the length of each segment of piecewise function

Parameters
----------
x_coords : List[float]
List of x-coordinates

Returns
----------
List[float]
List of values that represent the length of each piecewise segment.
"""
return np.subtract(x_coords[1:], x_coords[:-1]).tolist()


def running_sum(data: PiecewiseStepData) -> List[XYCoords]:
"""Calculates y-values from slope data in PiecewiseStepData

Uses the x coordinates and slope data in PiecewiseStepData to calculate the corresponding y-values, such that:

.. math:: y(i) = y(i-1) + \\text{slope}(i-1) \\times \\left( x(i) - x(i-1) \\right)


Parameters
----------
data : PiecewiseStepData
Piecewise function data used to calculate y-coordinates

Returns
----------
point : List[XYCoords]
List of (x,y) coordinates as NamedTuples.
"""
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
Loading
Loading