From d2d8fbb9029d78939770328328779d9892f6748e Mon Sep 17 00:00:00 2001 From: Jerry Potts Date: Tue, 2 Jul 2024 12:38:47 -0600 Subject: [PATCH 01/37] initial class definitions --- src/infrasys/value_curves.py | 96 ++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 src/infrasys/value_curves.py diff --git a/src/infrasys/value_curves.py b/src/infrasys/value_curves.py new file mode 100644 index 0000000..9edef12 --- /dev/null +++ b/src/infrasys/value_curves.py @@ -0,0 +1,96 @@ +"""Defines classes for value curves using cost functions""" + +from infrasys import Component +from typing import Union +from typing_extensions import Annotated +from infrasys.function_data import ( + LinearFunctionData, + QuadraticFunctionData, + PiecewiseLinearData, + PiecewiseStepData, +) +from pydantic import Field + + +class InputOutputCurve(Component): + """Input-output curve relating production quality to cost. + + An input-output curve, directly relating the production quantity to the cost: `y = f(x)`. + Can be used, for instance, in the representation of a [`CostCurve`](@ref) where `x` is MW + and `y` is currency/hr, or in the representation of a [`FuelCurve`](@ref) where `x` is MW + and `y` is fuel/hr. + """ + + name: Annotated[str, Field(frozen=True)] = "" + function_data: Annotated[ + Union[QuadraticFunctionData, LinearFunctionData, PiecewiseLinearData], + Field(description="The underlying `FunctionData` representation of this `ValueCurve`"), + ] + input_at_zero: Annotated[ + Union[None, float], + Field( + description="Optional, an explicit representation of the input value at zero output." + ), + ] = None + + +class IncrementalCurve(Component): + """Incremental/marginal curve to relate production quantity to cost derivative. + + An incremental (or 'marginal') curve, relating the production quantity to the derivative of + cost: `y = f'(x)`. Can be used, for instance, in the representation of a [`CostCurve`](@ref) + where `x` is MW and `y` is currency/MWh, or in the representation of a [`FuelCurve`](@ref) + where `x` is MW and `y` is fuel/MWh. + """ + + name: Annotated[str, Field(frozen=True)] = "" + function_data: Annotated[ + Union[LinearFunctionData, PiecewiseStepData], + Field(description="The underlying `FunctionData` representation of this `ValueCurve`"), + ] + initial_input: Annotated[ + Union[float, None], + Field( + description="The value of f(x) at the least x for which the function is defined, or \ + the origin for functions with no left endpoint, used for conversion to `InputOutputCurve`" + ), + ] + input_at_zero: Annotated[ + Union[None, float], + Field( + description="Optional, an explicit representation of the input value at zero output." + ), + ] = None + + +class AverageRateCurve(Component): + """Average rate curve relating production quality to average cost rate. + + An average rate curve, relating the production quantity to the average cost rate from the + origin: `y = f(x)/x`. Can be used, for instance, in the representation of a + [`CostCurve`](@ref) where `x` is MW and `y` is currency/MWh, or in the representation of a + [`FuelCurve`](@ref) where `x` is MW and `y` is fuel/MWh. Typically calculated by dividing + absolute values of cost rate or fuel input rate by absolute values of electric power. + """ + + name: Annotated[str, Field(frozen=True)] = "" + function_data: Annotated[ + Union[LinearFunctionData, PiecewiseStepData], + Field( + description="The underlying `FunctionData` representation of this `ValueCurve`, or \ + only the oblique asymptote when using `LinearFunctionData`" + ), + ] + initial_input: Annotated[ + Union[float, None], + Field( + description="The value of f(x) at the least x for which the function is defined, or \ + the origin for functions with no left endpoint, used for conversion to `InputOutputCurve`" + ), + ] + input_at_zero: Annotated[ + Union[None, float], + Field( + description="Optional, an explicit representation of the input value at zero output." + ), + ] = None From bcd493bf3f6acfe9eeb942f9e4010e4e5acac0eb Mon Sep 17 00:00:00 2001 From: Jerry Potts Date: Mon, 8 Jul 2024 11:29:30 -0600 Subject: [PATCH 02/37] add get_slopes --- src/infrasys/function_data.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/infrasys/function_data.py b/src/infrasys/function_data.py index 551f8e7..8753761 100644 --- a/src/infrasys/function_data.py +++ b/src/infrasys/function_data.py @@ -170,3 +170,28 @@ 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 + + Function used to calculate the slopes from a list of XYCoords. + 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 From b24bdd03a8250ca2686feff2608d500154895fcc Mon Sep 17 00:00:00 2001 From: Jerry Potts Date: Mon, 8 Jul 2024 12:09:24 -0600 Subject: [PATCH 03/37] add InputOutput Conversions --- src/infrasys/value_curves.py | 133 +++++++++++++++++++++++++++++++++-- 1 file changed, 127 insertions(+), 6 deletions(-) diff --git a/src/infrasys/value_curves.py b/src/infrasys/value_curves.py index 9edef12..254e186 100644 --- a/src/infrasys/value_curves.py +++ b/src/infrasys/value_curves.py @@ -8,6 +8,7 @@ QuadraticFunctionData, PiecewiseLinearData, PiecewiseStepData, + get_slopes, ) from pydantic import Field @@ -16,8 +17,8 @@ class InputOutputCurve(Component): """Input-output curve relating production quality to cost. An input-output curve, directly relating the production quantity to the cost: `y = f(x)`. - Can be used, for instance, in the representation of a [`CostCurve`](@ref) where `x` is MW - and `y` is currency/hr, or in the representation of a [`FuelCurve`](@ref) where `x` is MW + Can be used, for instance, in the representation of a Cost Curve where `x` is MW + and `y` is currency/hr, or in the representation of a Fuel Curve where `x` is MW and `y` is fuel/hr. """ @@ -38,8 +39,8 @@ class IncrementalCurve(Component): """Incremental/marginal curve to relate production quantity to cost derivative. An incremental (or 'marginal') curve, relating the production quantity to the derivative of - cost: `y = f'(x)`. Can be used, for instance, in the representation of a [`CostCurve`](@ref) - where `x` is MW and `y` is currency/MWh, or in the representation of a [`FuelCurve`](@ref) + cost: `y = f'(x)`. Can be used, for instance, in the representation of a Cost Curve + where `x` is MW and `y` is currency/MWh, or in the representation of a Fuel Curve where `x` is MW and `y` is fuel/MWh. """ @@ -68,8 +69,8 @@ class AverageRateCurve(Component): An average rate curve, relating the production quantity to the average cost rate from the origin: `y = f(x)/x`. Can be used, for instance, in the representation of a - [`CostCurve`](@ref) where `x` is MW and `y` is currency/MWh, or in the representation of a - [`FuelCurve`](@ref) where `x` is MW and `y` is fuel/MWh. Typically calculated by dividing + Cost Curve where `x` is MW and `y` is currency/MWh, or in the representation of a + Fuel Curve where `x` is MW and `y` is fuel/MWh. Typically calculated by dividing absolute values of cost rate or fuel input rate by absolute values of electric power. """ @@ -94,3 +95,123 @@ class AverageRateCurve(Component): description="Optional, an explicit representation of the input value at zero output." ), ] = None + + +# Converting IO curves to X +def InputOutputLinearToQuadratic(data: InputOutputCurve) -> InputOutputCurve: + """Function to convert linear InputOutput Curve to quadratic + + Parameters + ---------- + data : InputOutputCurve + `InputOutputCurve` using `LinearFunctionData` for its function data. + + Returns + ---------- + InputOutputCurve + `InputOutputCurve` using `QuadraticFunctionData` for its function data. + """ + q = 0.0 + p = data.function_data.proportional_term + c = data.function_data.constant_term + + return InputOutputCurve( + function_data=QuadraticFunctionData( + quadratic_term=q, proportional_term=p, constant_term=c + ), + input_at_zero=data.input_at_zero, + ) + + +def InputOutputToIncremental(data: InputOutputCurve) -> IncrementalCurve: + """Function to convert InputOutputCurve to IncrementalCurve + + TEXT HERE + + Parameters + ---------- + data : InputOutputCurve + `InputOutputCurve` using `LinearFunctionData` for its function data. + + Returns + ---------- + IncrementalCurve + `IncrementalCurve` using either `LinearFunctionData` or `PiecewiseStepData`. + """ + + if isinstance(data.function_data, LinearFunctionData): + q = 0.0 + p = data.function_data.proportional_term + + return IncrementalCurve( + function_data=LinearFunctionData(proportional_term=q, constant_term=p), + initial_input=data.function_data.constant_term, + input_at_zero=data.input_at_zero, + ) + elif isinstance(data.function_data, QuadraticFunctionData): + q = data.function_data.quadratic_term + p = data.function_data.proportional_term + + return IncrementalCurve( + function_data=LinearFunctionData(proportional_term=2 * q, constant_term=p), + initial_input=data.function_data.constant_term, + input_at_zero=data.input_at_zero, + ) + elif isinstance(data.function_data, PiecewiseLinearData): + x = [fd.x for fd in data.function_data.points] + slopes = get_slopes(data.function_data.points) + + return IncrementalCurve( + function_data=PiecewiseStepData(x_coords=x, y_coords=slopes), + initial_input=data.function_data.points[0].y, + input_at_zero=data.input_at_zero, + ) + + return + + +def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: + """Function to convert InputOutputCurve to AverageRateCurve + + TEXT HERE + + Parameters + ---------- + data : InputOutputCurve + `InputOutputCurve` using `LinearFunctionData` for its function data. + + Returns + ---------- + AverageRateCurve + `AverageRateCurve` using either `LinearFunctionData` or `PiecewiseStepData`. + """ + if isinstance(data.function_data, LinearFunctionData): + q = 0.0 + p = data.function_data.proportional_term + + return AverageRateCurve( + function_data=LinearFunctionData(q, p), + initial_input=data.function_data.constant_term, + input_at_zero=data.input_at_zero, + ) + elif isinstance(data.function_data, QuadraticFunctionData): + q = data.function_data.quadratic_term + p = data.function_data.proportional_term + + return AverageRateCurve( + function_data=LinearFunctionData(q, p), + initial_input=data.function_data.constant_term, + input_at_zero=data.input_at_zero, + ) + elif isinstance(data.function_data, PiecewiseLinearData): + # I think I need to add in the getters and stuff from function_data to make this easier + x = [fd.x for fd in data.function_data.points] + slopes_from_origin = [fd.y / fd.x for fd in data.function_data.points[1:]] + + return AverageRateCurve( + function_data=PiecewiseStepData(x_coords=x, y_coords=slopes_from_origin), + initial_input=data.function_data.points[0].y, + input_at_zero=data.input_at_zero, + ) + + return From 28139dc6a023fcc760356f2105e50bf6089e70c4 Mon Sep 17 00:00:00 2001 From: Jerry Potts Date: Mon, 8 Jul 2024 13:55:07 -0600 Subject: [PATCH 04/37] Added IncrementalCurve conversions --- src/infrasys/function_data.py | 19 +++++++ src/infrasys/value_curves.py | 100 ++++++++++++++++++++++++++++++++-- 2 files changed, 113 insertions(+), 6 deletions(-) diff --git a/src/infrasys/function_data.py b/src/infrasys/function_data.py index 8753761..68f36eb 100644 --- a/src/infrasys/function_data.py +++ b/src/infrasys/function_data.py @@ -195,3 +195,22 @@ def get_slopes(vc: List[XYCoords]) -> List[float]: 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]: + 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 diff --git a/src/infrasys/value_curves.py b/src/infrasys/value_curves.py index 254e186..6645012 100644 --- a/src/infrasys/value_curves.py +++ b/src/infrasys/value_curves.py @@ -9,6 +9,7 @@ PiecewiseLinearData, PiecewiseStepData, get_slopes, + running_sum, ) from pydantic import Field @@ -126,17 +127,22 @@ def InputOutputLinearToQuadratic(data: InputOutputCurve) -> InputOutputCurve: def InputOutputToIncremental(data: InputOutputCurve) -> IncrementalCurve: """Function to convert InputOutputCurve to IncrementalCurve - TEXT HERE + Function takes and InputOutputCurve and converts it to a corresponding + incremental curve depending on the type of function_data. If the InputOutputCurve + uses LinearFunctionData or QuadraticFunctionData, the corresponding + IncrementalCurve uses the corresponding derivative for its `function_data`. If + the input uses PiecewiseLinearData, the slopes of each segment are calculated and + converted to PiecewiseStepData for the IncrementalCurve. Parameters ---------- data : InputOutputCurve - `InputOutputCurve` using `LinearFunctionData` for its function data. + InputOutputCurve using LinearFunctionData for its function data. Returns ---------- IncrementalCurve - `IncrementalCurve` using either `LinearFunctionData` or `PiecewiseStepData`. + IncrementalCurve using either LinearFunctionData or PiecewiseStepData. """ if isinstance(data.function_data, LinearFunctionData): @@ -173,17 +179,23 @@ def InputOutputToIncremental(data: InputOutputCurve) -> IncrementalCurve: def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: """Function to convert InputOutputCurve to AverageRateCurve - TEXT HERE + Function takes and InputOutputCurve and converts it to a corresponding + AverageRateCurve depending on the type of function_data. If the InputOutputCurve + uses LinearFunctionData or QuadraticFunctionData, the corresponding + IncrementalCurve uses the LinearFunctionData, with a slope equal to the `quadratic_term` + (0.0 if originally linear), and a intercept equal to the `proportional_term`. If + the input uses PiecewiseLinearData, the slopes of each segment are calculated and + converted to PiecewiseStepData for the AverageRateCurve. Parameters ---------- data : InputOutputCurve - `InputOutputCurve` using `LinearFunctionData` for its function data. + InputOutputCurve using LinearFunctionData for its function data. Returns ---------- AverageRateCurve - `AverageRateCurve` using either `LinearFunctionData` or `PiecewiseStepData`. + AverageRateCurve using either LinearFunctionData or PiecewiseStepData. """ if isinstance(data.function_data, LinearFunctionData): q = 0.0 @@ -215,3 +227,79 @@ def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: ) return + + +# Converting Incremental Curves to X +def IncrementalToInputOutput(data: IncrementalCurve) -> InputOutputCurve: + if isinstance(data.function_data, LinearFunctionData): + p = data.function_data.proportional_term + m = data.function_data.constant_term + + if data.initial_input is None: + ValueError("Cannot convert `IncrementalCurve` with undefined `initial_input`") + else: + c = data.initial_input + + if p == 0: + return InputOutputCurve( + function_data=LinearFunctionData(proportional_term=m, constant_term=c) + ) + else: + return InputOutputCurve( + function_data=QuadraticFunctionData( + quadratic_term=p / 2, proportional_term=m, constant_term=c + ), + input_at_zero=data.input_at_zero, + ) + + elif isinstance(data.function_data, PiecewiseStepData): + if data.initial_input is None: + ValueError("Cannot convert `IncrementalCurve` with undefined `initial_input`") + else: + c = data.initial_input + + points = running_sum(data.function_data) + + return InputOutputCurve( + function_data=PiecewiseLinearData(points=[(p.x, p.y + c) for p in points]), + input_at_zero=data.input_at_zero, + ) + return + + +# Converting Incremental Curves to X +def IncrementalToAverageRate(data: IncrementalCurve) -> AverageRateCurve: + if isinstance(data.function_data, LinearFunctionData): + p = data.function_data.proportional_term + m = data.function_data.constant_term + + if data.initial_input is None: + ValueError("Cannot convert `IncrementalCurve` with undefined `initial_input`") + else: + c = data.initial_input + + if p == 0: + return AverageRateCurve( + function_data=LinearFunctionData(proportional_term=m, constant_term=c) + ) + else: + return AverageRateCurve( + function_data=QuadraticFunctionData( + quadratic_term=p / 2, proportional_term=m, constant_term=c + ), + input_at_zero=data.input_at_zero, + ) + + elif isinstance(data.function_data, PiecewiseStepData): + if data.initial_input is None: + ValueError("Cannot convert `IncrementalCurve` with undefined `initial_input`") + else: + c = data.initial_input + + points = running_sum(data.function_data) + + return AverageRateCurve( + function_data=PiecewiseLinearData(points=[(p.x, p.y + c) for p in points]), + input_at_zero=data.input_at_zero, + ) + return From d7cf0bc5918d50ea1b0ba3a4f81609725e3950e1 Mon Sep 17 00:00:00 2001 From: Jerry Potts Date: Mon, 8 Jul 2024 14:36:51 -0600 Subject: [PATCH 05/37] Add AverageRate conversion functions --- src/infrasys/function_data.py | 2 +- src/infrasys/value_curves.py | 35 +++++++++++++++++++++++++---------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/infrasys/function_data.py b/src/infrasys/function_data.py index 68f36eb..6537153 100644 --- a/src/infrasys/function_data.py +++ b/src/infrasys/function_data.py @@ -198,7 +198,7 @@ def get_slopes(vc: List[XYCoords]) -> List[float]: def get_x_lengths(x_coords: List[float]) -> List[float]: - return np.subtract(x_coords[1:], x_coords[:-1]) + return np.subtract(x_coords[1:], x_coords[:-1]).tolist() def running_sum(data: PiecewiseStepData) -> List[XYCoords]: diff --git a/src/infrasys/value_curves.py b/src/infrasys/value_curves.py index 6645012..cccefb3 100644 --- a/src/infrasys/value_curves.py +++ b/src/infrasys/value_curves.py @@ -12,6 +12,7 @@ running_sum, ) from pydantic import Field +import numpy as np class InputOutputCurve(Component): @@ -267,39 +268,53 @@ def IncrementalToInputOutput(data: IncrementalCurve) -> InputOutputCurve: return -# Converting Incremental Curves to X def IncrementalToAverageRate(data: IncrementalCurve) -> AverageRateCurve: + io_curve = IncrementalToInputOutput(data) + + return InputOutputToAverageRate(io_curve) + + +# Converting Average Rate Curves to X + + +def AverageRateToInputOutput(data: AverageRateCurve) -> InputOutputCurve: if isinstance(data.function_data, LinearFunctionData): p = data.function_data.proportional_term m = data.function_data.constant_term if data.initial_input is None: - ValueError("Cannot convert `IncrementalCurve` with undefined `initial_input`") + ValueError("Cannot convert `AverageRateCurve` with undefined `initial_input`") else: c = data.initial_input if p == 0: - return AverageRateCurve( + return InputOutputCurve( function_data=LinearFunctionData(proportional_term=m, constant_term=c) ) else: - return AverageRateCurve( + return InputOutputCurve( function_data=QuadraticFunctionData( - quadratic_term=p / 2, proportional_term=m, constant_term=c + quadratic_term=p, proportional_term=m, constant_term=c ), input_at_zero=data.input_at_zero, ) elif isinstance(data.function_data, PiecewiseStepData): if data.initial_input is None: - ValueError("Cannot convert `IncrementalCurve` with undefined `initial_input`") + ValueError("Cannot convert `AverageCurve` with undefined `initial_input`") else: c = data.initial_input - points = running_sum(data.function_data) - - return AverageRateCurve( - function_data=PiecewiseLinearData(points=[(p.x, p.y + c) for p in points]), + xs = data.function_data.x_coords + ys = np.multiply(xs[1:], data.function_data.y_coords).tolist() + return InputOutputCurve( + function_data=PiecewiseLinearData(list(zip(xs, ys))), input_at_zero=data.input_at_zero, ) return + + +def AverageRatetoIncremental(data: AverageRateCurve) -> IncrementalCurve: + io_curve = AverageRateToInputOutput(data) + + return InputOutputToIncremental(io_curve) From aef67bc0a8a6098b8ad4ad548ec6e67e8af5ea54 Mon Sep 17 00:00:00 2001 From: Jerry Potts Date: Mon, 8 Jul 2024 15:24:57 -0600 Subject: [PATCH 06/37] docstrings --- src/infrasys/function_data.py | 28 ++++++++++++ src/infrasys/value_curves.py | 81 ++++++++++++++++++++++++++++++++--- 2 files changed, 104 insertions(+), 5 deletions(-) diff --git a/src/infrasys/function_data.py b/src/infrasys/function_data.py index 6537153..c319270 100644 --- a/src/infrasys/function_data.py +++ b/src/infrasys/function_data.py @@ -198,10 +198,38 @@ def get_slopes(vc: List[XYCoords]) -> List[float]: 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: + `y[i] = y[i-1] + slope[i-1]*(x[i] - x[i-1])` + + 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 diff --git a/src/infrasys/value_curves.py b/src/infrasys/value_curves.py index cccefb3..55db4b6 100644 --- a/src/infrasys/value_curves.py +++ b/src/infrasys/value_curves.py @@ -138,12 +138,12 @@ def InputOutputToIncremental(data: InputOutputCurve) -> IncrementalCurve: Parameters ---------- data : InputOutputCurve - InputOutputCurve using LinearFunctionData for its function data. + Original InputOutputCurve for conversion. Returns ---------- IncrementalCurve - IncrementalCurve using either LinearFunctionData or PiecewiseStepData. + IncrementalCurve using either LinearFunctionData or PiecewiseStepData after conversion. """ if isinstance(data.function_data, LinearFunctionData): @@ -191,12 +191,12 @@ def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: Parameters ---------- data : InputOutputCurve - InputOutputCurve using LinearFunctionData for its function data. + Original InputOutputCurve for conversion. Returns ---------- AverageRateCurve - AverageRateCurve using either LinearFunctionData or PiecewiseStepData. + AverageRateCurve using either LinearFunctionData or PiecewiseStepData after conversion. """ if isinstance(data.function_data, LinearFunctionData): q = 0.0 @@ -232,6 +232,25 @@ def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: # Converting Incremental Curves to X def IncrementalToInputOutput(data: IncrementalCurve) -> InputOutputCurve: + """Function to convert IncrementalCurve to InputOutputCurve + + Function takes an IncrementalCurve and converts it to a corresponding + InputOutputCurve depending on the type of function_data. If the IncrementalCurve + uses LinearFunctionData, the new InputOutputCurve is created linear or quadratic data + that correspond to the integral of the original linear function. If the input uses + PiecewiseStepData, the slopes of each segment are used to calculate the corresponding + y values for each x value and used to construct PiecewiseLinearData for the InputOutputCurve. + + Parameters + ---------- + data : IncrementalCurve + Original IncrementalCurve for conversion. + + Returns + ---------- + InputOutputCurve + InputOutputCurve using either QuadraticFunctionData or PiecewiseStepData. + """ if isinstance(data.function_data, LinearFunctionData): p = data.function_data.proportional_term m = data.function_data.constant_term @@ -269,15 +288,49 @@ def IncrementalToInputOutput(data: IncrementalCurve) -> InputOutputCurve: def IncrementalToAverageRate(data: IncrementalCurve) -> AverageRateCurve: + """Function to convert IncrementalCurve to AverageRateCurve + + Function takes an IncrementalCurve and first converts it to an + InputOutputCurve, which is then converted into an AverageRateCurve. + + Parameters + ---------- + data : IncrementalCurve + Original InputOutputCurve for conversion. + + Returns + ---------- + AverageRateCurve + AverageRateCurve using either QuadraticFunctionData or PiecewiseStepData. + """ + io_curve = IncrementalToInputOutput(data) return InputOutputToAverageRate(io_curve) # Converting Average Rate Curves to X +def AverageRateToInputOutput(data: AverageRateCurve) -> InputOutputCurve: + """Function to convert IncrementalCurve to InputOutputCurve + Function takes an AverageRateCurve and converts it to a corresponding + InputOutputCurve depending on the type of function_data. If the AverageRateCurve + uses LinearFunctionData, the new InputOutputCurve is created with either linear or quadratic + function data, depending on if the original function data is constant or linear. If the + input uses PiecewiseStepData, new y-values are calculated for each x value such that `f(x) = x*y` + and used to construct PiecewiseLinearData for the InputOutputCurve. + + Parameters + ---------- + data : AverageRateCurve + Original AverageRateCurve for conversion. + + Returns + ---------- + InputOutputCurve + InputOutputCurve using either QuadraticFunctionData or PiecewiseStepData. + """ -def AverageRateToInputOutput(data: AverageRateCurve) -> InputOutputCurve: if isinstance(data.function_data, LinearFunctionData): p = data.function_data.proportional_term m = data.function_data.constant_term @@ -307,6 +360,8 @@ def AverageRateToInputOutput(data: AverageRateCurve) -> InputOutputCurve: xs = data.function_data.x_coords ys = np.multiply(xs[1:], data.function_data.y_coords).tolist() + ys.insert(0, c) + return InputOutputCurve( function_data=PiecewiseLinearData(list(zip(xs, ys))), input_at_zero=data.input_at_zero, @@ -315,6 +370,22 @@ def AverageRateToInputOutput(data: AverageRateCurve) -> InputOutputCurve: def AverageRatetoIncremental(data: AverageRateCurve) -> IncrementalCurve: + """Function to convert AverageRateCurve to IncrementalCurve + + Function takes an AverageRateCurve and first converts it to an + InputOutputCurve, which is then converted into an IncrementalCurve. + + Parameters + ---------- + data : AverageRateCurve + Original AverageRateCurve for conversion. + + Returns + ---------- + IncrementalCurve + IncrementalCurve using either QuadraticFunctionData or PiecewiseStepData. + """ + io_curve = AverageRateToInputOutput(data) return InputOutputToIncremental(io_curve) From 858db26b6af9aef1b96c83c4cf4422c72380216f Mon Sep 17 00:00:00 2001 From: Jerry Potts Date: Mon, 8 Jul 2024 16:51:43 -0600 Subject: [PATCH 07/37] add function_data tests --- tests/test_function_data.py | 78 ++++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/tests/test_function_data.py b/tests/test_function_data.py index 3888373..9cce084 100644 --- a/tests/test_function_data.py +++ b/tests/test_function_data.py @@ -1,7 +1,20 @@ -from infrasys.function_data import PiecewiseStepData, PiecewiseLinearData, XYCoords +from infrasys.function_data import ( + LinearFunctionData, + PiecewiseStepData, + PiecewiseLinearData, + XYCoords, + get_slopes, + running_sum, +) +from infrasys import Component +from .models.simple_system import SimpleSystem import pytest +class FunctionDataComponent(Component): + function_data: LinearFunctionData + + def test_xycoords(): test_xy = XYCoords(x=1.0, y=2.0) @@ -48,3 +61,66 @@ def test_piecewise_step(): with pytest.raises(ValueError): PiecewiseStepData(x_coords=test_x, y_coords=test_y) + + +def test_function_data_custom_serialization(): + component = FunctionDataComponent( + name="test", function_data=LinearFunctionData(proportional_term=1.0, constant_term=2.0) + ) + + model_dump = component.model_dump(mode="json") + assert model_dump["function_data"]["proportional_term"] == 1.0 + + model_dump = component.model_dump(context={"magnitude_only": True}) + assert model_dump["function_data"]["proportional_term"] == 1.0 + + model_dump = component.model_dump(mode="json", context={"magnitude_only": True}) + assert model_dump["function_data"]["proportional_term"] == 1.0 + + +def test_function_data_serialization(tmp_path): + system = SimpleSystem(auto_add_composed_components=True) + + f1 = FunctionDataComponent( + name="test", function_data=LinearFunctionData(proportional_term=1.0, constant_term=2.0) + ) + system.add_component(f1) + filename = tmp_path / "function_data.json" + + system.to_json(filename, overwrite=True) + system2 = SimpleSystem.from_json(filename) + + assert system2 is not None + + f2 = system2.get_component(FunctionDataComponent, "test") + + assert f2 is not None + assert f1.function_data.proportional_term == f2.function_data.proportional_term + assert f1.function_data.constant_term == f2.function_data.constant_term + + +def test_slopes_calculation(): + test_xy = [XYCoords(1.0, 2.0), XYCoords(2.0, 4.0), XYCoords(4.0, 10.0)] + + slopes = get_slopes(test_xy) + + correct_slopes = [2.0, 3.0] + + assert slopes == correct_slopes + + +def test_running_sum(): + test_x = [1.0, 3.0, 6.0] + test_y = [2.0, 4.0] + + pws = PiecewiseStepData(x_coords=test_x, y_coords=test_y) + + points = running_sum(pws) + + x_values = [p.x for p in points] + y_values = [p.y for p in points] + + correct_y_values = [0.0, 4.0, 16.0] + + assert x_values == test_x + assert y_values == correct_y_values From 8e801cd677f331adc27284acb0391e837444a9f9 Mon Sep 17 00:00:00 2001 From: Jerry Potts Date: Mon, 8 Jul 2024 17:55:06 -0600 Subject: [PATCH 08/37] add initial test functions --- src/infrasys/value_curves.py | 18 ++-- tests/test_values_curves.py | 159 +++++++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+), 10 deletions(-) create mode 100644 tests/test_values_curves.py diff --git a/src/infrasys/value_curves.py b/src/infrasys/value_curves.py index 55db4b6..61a76ad 100644 --- a/src/infrasys/value_curves.py +++ b/src/infrasys/value_curves.py @@ -203,7 +203,7 @@ def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: p = data.function_data.proportional_term return AverageRateCurve( - function_data=LinearFunctionData(q, p), + function_data=LinearFunctionData(proportional_term=q, constant_term=p), initial_input=data.function_data.constant_term, input_at_zero=data.input_at_zero, ) @@ -212,7 +212,7 @@ def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: p = data.function_data.proportional_term return AverageRateCurve( - function_data=LinearFunctionData(q, p), + function_data=LinearFunctionData(proportional_term=q, constant_term=p), initial_input=data.function_data.constant_term, input_at_zero=data.input_at_zero, ) @@ -255,10 +255,9 @@ def IncrementalToInputOutput(data: IncrementalCurve) -> InputOutputCurve: p = data.function_data.proportional_term m = data.function_data.constant_term - if data.initial_input is None: - ValueError("Cannot convert `IncrementalCurve` with undefined `initial_input`") - else: - c = data.initial_input + c = data.initial_input + if c is None: + raise ValueError("Cannot convert `IncrementalCurve` with undefined `initial_input`") if p == 0: return InputOutputCurve( @@ -273,10 +272,9 @@ def IncrementalToInputOutput(data: IncrementalCurve) -> InputOutputCurve: ) elif isinstance(data.function_data, PiecewiseStepData): - if data.initial_input is None: - ValueError("Cannot convert `IncrementalCurve` with undefined `initial_input`") - else: - c = data.initial_input + c = data.initial_input + if c is None: + raise ValueError("Cannot convert `IncrementalCurve` with undefined `initial_input`") points = running_sum(data.function_data) diff --git a/tests/test_values_curves.py b/tests/test_values_curves.py new file mode 100644 index 0000000..10041e2 --- /dev/null +++ b/tests/test_values_curves.py @@ -0,0 +1,159 @@ +from infrasys.function_data import ( + LinearFunctionData, + QuadraticFunctionData, + PiecewiseStepData, + PiecewiseLinearData, + XYCoords, +) +from infrasys.value_curves import ( + InputOutputCurve, + IncrementalCurve, + AverageRateCurve, + InputOutputToAverageRate, + InputOutputToIncremental, + IncrementalToInputOutput, +) +from infrasys import Component +from .models.simple_system import SimpleSystem +import pytest + + +class ValueCurveComponent(Component): + value_curve: InputOutputCurve + + +def test_input_output_curve(): + curve = InputOutputCurve( + function_data=LinearFunctionData(proportional_term=1.0, constant_term=1.0) + ) + + assert isinstance(curve, InputOutputCurve) + assert isinstance(curve.function_data, LinearFunctionData) + + +def test_incremental_curve(): + curve = IncrementalCurve( + function_data=LinearFunctionData(proportional_term=1.0, constant_term=1.0), + initial_input=1.0, + ) + + assert isinstance(curve, IncrementalCurve) + assert isinstance(curve.function_data, LinearFunctionData) + + +def test_average_rate_curve(): + curve = AverageRateCurve( + function_data=LinearFunctionData(proportional_term=1.0, constant_term=1.0), + initial_input=1.0, + ) + + assert isinstance(curve, AverageRateCurve) + assert isinstance(curve.function_data, LinearFunctionData) + + +def test_input_output_conversion(): + # Linear function data + curve = InputOutputCurve( + function_data=LinearFunctionData(proportional_term=1.0, constant_term=1.0) + ) + new_curve = InputOutputToAverageRate(curve) + assert isinstance(new_curve, AverageRateCurve) + + new_curve = InputOutputToIncremental(curve) + assert isinstance(new_curve, IncrementalCurve) + + # Quadratic function data + q = 3.0 + p = 2.0 + c = 1.0 + + curve = InputOutputCurve( + function_data=QuadraticFunctionData(quadratic_term=q, proportional_term=p, constant_term=c) + ) + new_curve = InputOutputToAverageRate(curve) + assert isinstance(new_curve, AverageRateCurve) + assert new_curve.function_data.proportional_term == q + + new_curve = InputOutputToIncremental(curve) + assert isinstance(new_curve, IncrementalCurve) + assert new_curve.function_data.proportional_term == 2 * q + + # Piecewise linear data + xy = [XYCoords(1.0, 2.0), XYCoords(2.0, 4.0), XYCoords(4.0, 10.0)] + + curve = InputOutputCurve(function_data=PiecewiseLinearData(points=xy)) + new_curve = InputOutputToAverageRate(curve) + assert isinstance(new_curve, AverageRateCurve) + assert new_curve.function_data.y_coords == [2.0, 2.5] + + new_curve = InputOutputToIncremental(curve) + assert isinstance(new_curve, IncrementalCurve) + + +def test_incremental_conversion(): + # Linear function data + curve = IncrementalCurve( + function_data=LinearFunctionData(proportional_term=1.0, constant_term=1.0), + initial_input=None, + ) + with pytest.raises(ValueError): + IncrementalToInputOutput(curve) + + curve.initial_input = 0.0 + new_curve = IncrementalToInputOutput(curve) + assert isinstance(new_curve, InputOutputCurve) + + # Piecewise step data + data = PiecewiseStepData(x_coords=[1.0, 3.0, 5.0], y_coords=[2.0, 6.0]) + curve = IncrementalCurve(function_data=data, initial_input=None) + with pytest.raises(ValueError): + IncrementalToInputOutput(curve) + + curve.initial_input = 0.0 + new_curve = IncrementalToInputOutput(curve) + assert isinstance(new_curve, InputOutputCurve) + + +def test_value_curve_custom_serialization(): + component = ValueCurveComponent( + name="test", + value_curve=InputOutputCurve( + function_data=LinearFunctionData(proportional_term=1.0, constant_term=2.0) + ), + ) + + model_dump = component.model_dump(mode="json") + assert model_dump["value_curve"]["function_data"]["proportional_term"] == 1.0 + + model_dump = component.model_dump(context={"magnitude_only": True}) + assert model_dump["value_curve"]["function_data"]["proportional_term"] == 1.0 + + model_dump = component.model_dump(mode="json", context={"magnitude_only": True}) + assert model_dump["value_curve"]["function_data"]["proportional_term"] == 1.0 + + +def test_value_curve_serialization(tmp_path): + system = SimpleSystem(auto_add_composed_components=True) + + v1 = ValueCurveComponent( + name="test", + value_curve=InputOutputCurve( + function_data=LinearFunctionData(proportional_term=1.0, constant_term=2.0) + ), + ) + system.add_component(v1) + filename = tmp_path / "value_curve.json" + + system.to_json(filename, overwrite=True) + system2 = SimpleSystem.from_json(filename) + + assert system2 is not None + + v2 = system2.get_component(ValueCurveComponent, "test") + + assert v2 is not None + assert ( + v1.value_curve.function_data.proportional_term + == v2.value_curve.function_data.proportional_term + ) + assert v1.value_curve.function_data.constant_term == v2.value_curve.function_data.constant_term From 4f6f50f01aaf9de214c4e1eabda6d7e5c941c336 Mon Sep 17 00:00:00 2001 From: Jerry Potts Date: Mon, 8 Jul 2024 18:14:55 -0600 Subject: [PATCH 09/37] additional conversion tests --- src/infrasys/value_curves.py | 24 +++++---------- tests/test_values_curves.py | 59 ++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 16 deletions(-) diff --git a/src/infrasys/value_curves.py b/src/infrasys/value_curves.py index 61a76ad..02c665b 100644 --- a/src/infrasys/value_curves.py +++ b/src/infrasys/value_curves.py @@ -174,8 +174,6 @@ def InputOutputToIncremental(data: InputOutputCurve) -> IncrementalCurve: input_at_zero=data.input_at_zero, ) - return - def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: """Function to convert InputOutputCurve to AverageRateCurve @@ -227,8 +225,6 @@ def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: input_at_zero=data.input_at_zero, ) - return - # Converting Incremental Curves to X def IncrementalToInputOutput(data: IncrementalCurve) -> InputOutputCurve: @@ -282,7 +278,6 @@ def IncrementalToInputOutput(data: IncrementalCurve) -> InputOutputCurve: function_data=PiecewiseLinearData(points=[(p.x, p.y + c) for p in points]), input_at_zero=data.input_at_zero, ) - return def IncrementalToAverageRate(data: IncrementalCurve) -> AverageRateCurve: @@ -333,10 +328,9 @@ def AverageRateToInputOutput(data: AverageRateCurve) -> InputOutputCurve: p = data.function_data.proportional_term m = data.function_data.constant_term - if data.initial_input is None: - ValueError("Cannot convert `AverageRateCurve` with undefined `initial_input`") - else: - c = data.initial_input + c = data.initial_input + if c is None: + raise ValueError("Cannot convert `AverageRateCurve` with undefined `initial_input`") if p == 0: return InputOutputCurve( @@ -351,23 +345,21 @@ def AverageRateToInputOutput(data: AverageRateCurve) -> InputOutputCurve: ) elif isinstance(data.function_data, PiecewiseStepData): - if data.initial_input is None: - ValueError("Cannot convert `AverageCurve` with undefined `initial_input`") - else: - c = data.initial_input + c = data.initial_input + if c is None: + raise ValueError("Cannot convert `AverageRateCurve` with undefined `initial_input`") xs = data.function_data.x_coords ys = np.multiply(xs[1:], data.function_data.y_coords).tolist() ys.insert(0, c) return InputOutputCurve( - function_data=PiecewiseLinearData(list(zip(xs, ys))), + function_data=PiecewiseLinearData(points=list(zip(xs, ys))), input_at_zero=data.input_at_zero, ) - return -def AverageRatetoIncremental(data: AverageRateCurve) -> IncrementalCurve: +def AverageRateToIncremental(data: AverageRateCurve) -> IncrementalCurve: """Function to convert AverageRateCurve to IncrementalCurve Function takes an AverageRateCurve and first converts it to an diff --git a/tests/test_values_curves.py b/tests/test_values_curves.py index 10041e2..86d4292 100644 --- a/tests/test_values_curves.py +++ b/tests/test_values_curves.py @@ -9,9 +9,13 @@ InputOutputCurve, IncrementalCurve, AverageRateCurve, + InputOutputLinearToQuadratic, InputOutputToAverageRate, InputOutputToIncremental, IncrementalToInputOutput, + IncrementalToAverageRate, + AverageRateToInputOutput, + AverageRateToIncremental, ) from infrasys import Component from .models.simple_system import SimpleSystem @@ -62,6 +66,9 @@ def test_input_output_conversion(): new_curve = InputOutputToIncremental(curve) assert isinstance(new_curve, IncrementalCurve) + new_curve = InputOutputLinearToQuadratic(curve) + assert isinstance(new_curve.function_data, QuadraticFunctionData) + # Quadratic function data q = 3.0 p = 2.0 @@ -102,6 +109,17 @@ def test_incremental_conversion(): curve.initial_input = 0.0 new_curve = IncrementalToInputOutput(curve) assert isinstance(new_curve, InputOutputCurve) + assert isinstance(new_curve.function_data, QuadraticFunctionData) + assert new_curve.function_data.quadratic_term == 0.5 + + new_curve = IncrementalToAverageRate(curve) + assert isinstance(new_curve, AverageRateCurve) + + curve.function_data.proportional_term = 0.0 + new_curve = IncrementalToInputOutput(curve) + assert isinstance(new_curve, InputOutputCurve) + assert isinstance(new_curve.function_data, LinearFunctionData) + assert new_curve.function_data.proportional_term == 1.0 # Piecewise step data data = PiecewiseStepData(x_coords=[1.0, 3.0, 5.0], y_coords=[2.0, 6.0]) @@ -113,6 +131,47 @@ def test_incremental_conversion(): new_curve = IncrementalToInputOutput(curve) assert isinstance(new_curve, InputOutputCurve) + new_curve = IncrementalToAverageRate(curve) + assert isinstance(new_curve, AverageRateCurve) + + +def test_average_rate_conversion(): + # Linear function data + curve = AverageRateCurve( + function_data=LinearFunctionData(proportional_term=1.0, constant_term=2.0), + initial_input=None, + ) + with pytest.raises(ValueError): + AverageRateToInputOutput(curve) + + curve.initial_input = 0.0 + new_curve = AverageRateToInputOutput(curve) + assert isinstance(new_curve, InputOutputCurve) + assert isinstance(new_curve.function_data, QuadraticFunctionData) + assert new_curve.function_data.quadratic_term == 1.0 + + new_curve = AverageRateToIncremental(curve) + assert isinstance(new_curve, IncrementalCurve) + + curve.function_data.proportional_term = 0.0 + new_curve = AverageRateToInputOutput(curve) + assert isinstance(new_curve, InputOutputCurve) + assert isinstance(new_curve.function_data, LinearFunctionData) + assert new_curve.function_data.proportional_term == 2.0 + + # Piecewise step data + data = PiecewiseStepData(x_coords=[1.0, 3.0, 5.0], y_coords=[2.0, 6.0]) + curve = AverageRateCurve(function_data=data, initial_input=None) + with pytest.raises(ValueError): + AverageRateToInputOutput(curve) + + curve.initial_input = 0.0 + new_curve = AverageRateToInputOutput(curve) + assert isinstance(new_curve, InputOutputCurve) + + new_curve = AverageRateToIncremental(curve) + assert isinstance(new_curve, IncrementalCurve) + def test_value_curve_custom_serialization(): component = ValueCurveComponent( From e019bc417cea02ed0e38f35e124e1972e7fbd230 Mon Sep 17 00:00:00 2001 From: pesap Date: Fri, 2 Aug 2024 17:44:09 -0600 Subject: [PATCH 10/37] Modification of the docstings and mypy compliant --- docs/reference/api/function_data.md | 9 + docs/reference/api/index.md | 1 + src/infrasys/exceptions.py | 4 + src/infrasys/function_data.py | 84 +++++---- src/infrasys/value_curves.py | 267 ++++++++++++++++------------ 5 files changed, 210 insertions(+), 155 deletions(-) create mode 100644 docs/reference/api/function_data.md diff --git a/docs/reference/api/function_data.md b/docs/reference/api/function_data.md new file mode 100644 index 0000000..bb138e5 --- /dev/null +++ b/docs/reference/api/function_data.md @@ -0,0 +1,9 @@ +```{eval-rst} +.. _function-data-api: +``` +# Function data + +```{eval-rst} +.. automodule:: infrasys.function_data + :members: +``` diff --git a/docs/reference/api/index.md b/docs/reference/api/index.md index 760627b..e6027c6 100644 --- a/docs/reference/api/index.md +++ b/docs/reference/api/index.md @@ -13,4 +13,5 @@ time_series location quantities + function_data ``` diff --git a/src/infrasys/exceptions.py b/src/infrasys/exceptions.py index 8254ac2..b81d00e 100644 --- a/src/infrasys/exceptions.py +++ b/src/infrasys/exceptions.py @@ -31,3 +31,7 @@ class ISNotStored(ISBaseException): class ISOperationNotAllowed(ISBaseException): """Raised if the requested operation is not allowed.""" + + +class ISMethodError(ISBaseException): + """Rased if the requested method is not allowed.""" diff --git a/src/infrasys/function_data.py b/src/infrasys/function_data.py index c319270..7fe1ea6 100644 --- a/src/infrasys/function_data.py +++ b/src/infrasys/function_data.py @@ -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]). @@ -65,7 +69,7 @@ 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. """ @@ -73,12 +77,12 @@ def validate_piecewise_linear_x(points: List[XYCoords]) -> List[XYCoords]: 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}.") return points @@ -86,7 +90,6 @@ def validate_piecewise_linear_x(points: List[XYCoords]) -> List[XYCoords]: 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]). @@ -97,18 +100,18 @@ 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 @@ -116,42 +119,45 @@ def validate_piecewise_step_x(x_coords: List[float]) -> List[float]: 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`." + ) ), ] @@ -159,7 +165,7 @@ class PiecewiseStepData(Component): 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. @@ -175,14 +181,13 @@ def validate_piecewise_xy(self): def get_slopes(vc: List[XYCoords]) -> List[float]: """Calculate slopes from XYCoord data - Function used to calculate the slopes from a list of XYCoords. 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. + List of named tuples of (x, y) coordinates. Returns ---------- @@ -216,9 +221,10 @@ def get_x_lengths(x_coords: List[float]) -> List[float]: 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: - `y[i] = y[i-1] + slope[i-1]*(x[i] - x[i-1])` + 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 ---------- diff --git a/src/infrasys/value_curves.py b/src/infrasys/value_curves.py index 02c665b..bb8e714 100644 --- a/src/infrasys/value_curves.py +++ b/src/infrasys/value_curves.py @@ -3,6 +3,7 @@ from infrasys import Component from typing import Union from typing_extensions import Annotated +from infrasys.exceptions import ISMethodError from infrasys.function_data import ( LinearFunctionData, QuadraticFunctionData, @@ -18,15 +19,18 @@ class InputOutputCurve(Component): """Input-output curve relating production quality to cost. - An input-output curve, directly relating the production quantity to the cost: `y = f(x)`. - Can be used, for instance, in the representation of a Cost Curve where `x` is MW - and `y` is currency/hr, or in the representation of a Fuel Curve where `x` is MW - and `y` is fuel/hr. + An input-output curve, directly relating the production quantity to the cost: + + .. math:: y = f(x). + + Can be used, for instance, in the representation of a Cost Curve where :math:`x` is MW + and :math:`y` is currency/hr, or in the representation of a Fuel Curve where :math:`x` is MW + and :math:`y` is fuel/hr. """ name: Annotated[str, Field(frozen=True)] = "" function_data: Annotated[ - Union[QuadraticFunctionData, LinearFunctionData, PiecewiseLinearData], + QuadraticFunctionData | LinearFunctionData | PiecewiseLinearData, Field(description="The underlying `FunctionData` representation of this `ValueCurve`"), ] input_at_zero: Annotated[ @@ -41,25 +45,29 @@ class IncrementalCurve(Component): """Incremental/marginal curve to relate production quantity to cost derivative. An incremental (or 'marginal') curve, relating the production quantity to the derivative of - cost: `y = f'(x)`. Can be used, for instance, in the representation of a Cost Curve - where `x` is MW and `y` is currency/MWh, or in the representation of a Fuel Curve - where `x` is MW and `y` is fuel/MWh. + cost: + + ..math:: y = f'(x). + + Can be used, for instance, in the representation of a Cost Curve + where :math:`x` is MW and :math:`y` is currency/MWh, or in the representation of a Fuel Curve + where :math:`x` is MW and :math:`y` is fuel/MWh. """ name: Annotated[str, Field(frozen=True)] = "" function_data: Annotated[ - Union[LinearFunctionData, PiecewiseStepData], + LinearFunctionData | PiecewiseStepData, Field(description="The underlying `FunctionData` representation of this `ValueCurve`"), ] initial_input: Annotated[ - Union[float, None], + float | None, Field( description="The value of f(x) at the least x for which the function is defined, or \ the origin for functions with no left endpoint, used for conversion to `InputOutputCurve`" ), ] input_at_zero: Annotated[ - Union[None, float], + float | None, Field( description="Optional, an explicit representation of the input value at zero output." ), @@ -70,50 +78,59 @@ class AverageRateCurve(Component): """Average rate curve relating production quality to average cost rate. An average rate curve, relating the production quantity to the average cost rate from the - origin: `y = f(x)/x`. Can be used, for instance, in the representation of a - Cost Curve where `x` is MW and `y` is currency/MWh, or in the representation of a - Fuel Curve where `x` is MW and `y` is fuel/MWh. Typically calculated by dividing + origin: + + .. math:: y = f(x)/x. + + Can be used, for instance, in the representation of a + Cost Curve where :math:`x` is MW and :math:`y` is currency/MWh, or in the representation of a + Fuel Curve where :math:`x` is MW and :math:`y` is fuel/MWh. Typically calculated by dividing absolute values of cost rate or fuel input rate by absolute values of electric power. """ name: Annotated[str, Field(frozen=True)] = "" function_data: Annotated[ - Union[LinearFunctionData, PiecewiseStepData], + LinearFunctionData | PiecewiseStepData, Field( description="The underlying `FunctionData` representation of this `ValueCurve`, or \ only the oblique asymptote when using `LinearFunctionData`" ), ] initial_input: Annotated[ - Union[float, None], + float | None, Field( description="The value of f(x) at the least x for which the function is defined, or \ the origin for functions with no left endpoint, used for conversion to `InputOutputCurve`" ), ] input_at_zero: Annotated[ - Union[None, float], + float | None, Field( description="Optional, an explicit representation of the input value at zero output." ), ] = None -# Converting IO curves to X def InputOutputLinearToQuadratic(data: InputOutputCurve) -> InputOutputCurve: """Function to convert linear InputOutput Curve to quadratic + Converting IO curves to X + Parameters ---------- data : InputOutputCurve `InputOutputCurve` using `LinearFunctionData` for its function data. Returns - ---------- + ------- InputOutputCurve `InputOutputCurve` using `QuadraticFunctionData` for its function data. """ q = 0.0 + + if isinstance(data.function_data, PiecewiseStepData | PiecewiseLinearData): + raise ISMethodError("Can not convert Piecewise data to Quadratic.") + p = data.function_data.proportional_term c = data.function_data.constant_term @@ -129,10 +146,10 @@ def InputOutputToIncremental(data: InputOutputCurve) -> IncrementalCurve: """Function to convert InputOutputCurve to IncrementalCurve Function takes and InputOutputCurve and converts it to a corresponding - incremental curve depending on the type of function_data. If the InputOutputCurve - uses LinearFunctionData or QuadraticFunctionData, the corresponding - IncrementalCurve uses the corresponding derivative for its `function_data`. If - the input uses PiecewiseLinearData, the slopes of each segment are calculated and + incremental curve depending on the type of function_data. If the :class:`InputOutputCurve` + uses :class:`LinearFunctionData` or :class:`QuadraticFunctionData`, the corresponding + :class:`IncrementalCurve` uses the corresponding derivative for its `function_data`. If + the input uses :class:`PiecewiseLinearData`, the slopes of each segment are calculated and converted to PiecewiseStepData for the IncrementalCurve. Parameters @@ -141,50 +158,57 @@ def InputOutputToIncremental(data: InputOutputCurve) -> IncrementalCurve: Original InputOutputCurve for conversion. Returns - ---------- + ------- IncrementalCurve IncrementalCurve using either LinearFunctionData or PiecewiseStepData after conversion. - """ - if isinstance(data.function_data, LinearFunctionData): - q = 0.0 - p = data.function_data.proportional_term - - return IncrementalCurve( - function_data=LinearFunctionData(proportional_term=q, constant_term=p), - initial_input=data.function_data.constant_term, - input_at_zero=data.input_at_zero, - ) - elif isinstance(data.function_data, QuadraticFunctionData): - q = data.function_data.quadratic_term - p = data.function_data.proportional_term + Raises + ------ + ISMethodError + Function is not valid for the type of data provided. + """ + match data.function_data: + case LinearFunctionData(): + q = 0.0 + p = data.function_data.proportional_term + + return IncrementalCurve( + function_data=LinearFunctionData(proportional_term=q, constant_term=p), + initial_input=data.function_data.constant_term, + input_at_zero=data.input_at_zero, + ) + case QuadraticFunctionData(): + q = data.function_data.quadratic_term + p = data.function_data.proportional_term - return IncrementalCurve( - function_data=LinearFunctionData(proportional_term=2 * q, constant_term=p), - initial_input=data.function_data.constant_term, - input_at_zero=data.input_at_zero, - ) - elif isinstance(data.function_data, PiecewiseLinearData): - x = [fd.x for fd in data.function_data.points] - slopes = get_slopes(data.function_data.points) + return IncrementalCurve( + function_data=LinearFunctionData(proportional_term=2 * q, constant_term=p), + initial_input=data.function_data.constant_term, + input_at_zero=data.input_at_zero, + ) + case PiecewiseLinearData(): + x = [fd.x for fd in data.function_data.points] + slopes = get_slopes(data.function_data.points) - return IncrementalCurve( - function_data=PiecewiseStepData(x_coords=x, y_coords=slopes), - initial_input=data.function_data.points[0].y, - input_at_zero=data.input_at_zero, - ) + return IncrementalCurve( + function_data=PiecewiseStepData(x_coords=x, y_coords=slopes), + initial_input=data.function_data.points[0].y, + input_at_zero=data.input_at_zero, + ) + case _: + raise ISMethodError("Function is not valid for the type of data provided.") def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: """Function to convert InputOutputCurve to AverageRateCurve - Function takes and InputOutputCurve and converts it to a corresponding - AverageRateCurve depending on the type of function_data. If the InputOutputCurve - uses LinearFunctionData or QuadraticFunctionData, the corresponding - IncrementalCurve uses the LinearFunctionData, with a slope equal to the `quadratic_term` - (0.0 if originally linear), and a intercept equal to the `proportional_term`. If - the input uses PiecewiseLinearData, the slopes of each segment are calculated and - converted to PiecewiseStepData for the AverageRateCurve. + If the :class:`InputOutputCurve` uses :class:`LinearFunctionData` or + :class:`QuadraticFunctionData`, the corresponding :class:`IncrementalCurve` + uses the :class`LinearFunctionData`, with a slope equal to the + `quadratic_term` (0.0 if originally linear), and a intercept equal to the + `proportional_term`. If the input uses :class:`PiecewiseLinearData`, the + slopes of each segment are calculated and converted to PiecewiseStepData + for the AverageRateCurve. Parameters ---------- @@ -195,38 +219,45 @@ def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: ---------- AverageRateCurve AverageRateCurve using either LinearFunctionData or PiecewiseStepData after conversion. - """ - if isinstance(data.function_data, LinearFunctionData): - q = 0.0 - p = data.function_data.proportional_term - return AverageRateCurve( - function_data=LinearFunctionData(proportional_term=q, constant_term=p), - initial_input=data.function_data.constant_term, - input_at_zero=data.input_at_zero, - ) - elif isinstance(data.function_data, QuadraticFunctionData): - q = data.function_data.quadratic_term - p = data.function_data.proportional_term + Raises + ------ + ISMethodError + Function is not valid for the type of data provided. + """ + match data.function_data: + case LinearFunctionData(): + q = 0.0 + p = data.function_data.proportional_term + + return AverageRateCurve( + function_data=LinearFunctionData(proportional_term=q, constant_term=p), + initial_input=data.function_data.constant_term, + input_at_zero=data.input_at_zero, + ) + case QuadraticFunctionData(): + q = data.function_data.quadratic_term + p = data.function_data.proportional_term - return AverageRateCurve( - function_data=LinearFunctionData(proportional_term=q, constant_term=p), - initial_input=data.function_data.constant_term, - input_at_zero=data.input_at_zero, - ) - elif isinstance(data.function_data, PiecewiseLinearData): - # I think I need to add in the getters and stuff from function_data to make this easier - x = [fd.x for fd in data.function_data.points] - slopes_from_origin = [fd.y / fd.x for fd in data.function_data.points[1:]] - - return AverageRateCurve( - function_data=PiecewiseStepData(x_coords=x, y_coords=slopes_from_origin), - initial_input=data.function_data.points[0].y, - input_at_zero=data.input_at_zero, - ) + return AverageRateCurve( + function_data=LinearFunctionData(proportional_term=q, constant_term=p), + initial_input=data.function_data.constant_term, + input_at_zero=data.input_at_zero, + ) + case PiecewiseLinearData(): + # I think I need to add in the getters and stuff from function_data to make this easier + x = [fd.x for fd in data.function_data.points] + slopes_from_origin = [fd.y / fd.x for fd in data.function_data.points[1:]] + + return AverageRateCurve( + function_data=PiecewiseStepData(x_coords=x, y_coords=slopes_from_origin), + initial_input=data.function_data.points[0].y, + input_at_zero=data.input_at_zero, + ) + case _: + raise ISMethodError("Function is not valid for the type of data provided.") -# Converting Incremental Curves to X def IncrementalToInputOutput(data: IncrementalCurve) -> InputOutputCurve: """Function to convert IncrementalCurve to InputOutputCurve @@ -302,7 +333,6 @@ def IncrementalToAverageRate(data: IncrementalCurve) -> AverageRateCurve: return InputOutputToAverageRate(io_curve) -# Converting Average Rate Curves to X def AverageRateToInputOutput(data: AverageRateCurve) -> InputOutputCurve: """Function to convert IncrementalCurve to InputOutputCurve @@ -323,40 +353,45 @@ def AverageRateToInputOutput(data: AverageRateCurve) -> InputOutputCurve: InputOutputCurve InputOutputCurve using either QuadraticFunctionData or PiecewiseStepData. """ + match data.function_data: + case LinearFunctionData(): + p = data.function_data.proportional_term + m = data.function_data.constant_term + + c = data.initial_input + if c is None: + raise ValueError( + "Cannot convert `AverageRateCurve` with undefined `initial_input`" + ) + + if p == 0: + return InputOutputCurve( + function_data=LinearFunctionData(proportional_term=m, constant_term=c) + ) + else: + return InputOutputCurve( + function_data=QuadraticFunctionData( + quadratic_term=p, proportional_term=m, constant_term=c + ), + input_at_zero=data.input_at_zero, + ) + case PiecewiseLinearData(): + c = data.initial_input + if c is None: + raise ValueError( + "Cannot convert `AverageRateCurve` with undefined `initial_input`" + ) + + xs = data.function_data.x_coords + ys = np.multiply(xs[1:], data.function_data.y_coords).tolist() + ys.insert(0, c) - if isinstance(data.function_data, LinearFunctionData): - p = data.function_data.proportional_term - m = data.function_data.constant_term - - c = data.initial_input - if c is None: - raise ValueError("Cannot convert `AverageRateCurve` with undefined `initial_input`") - - if p == 0: return InputOutputCurve( - function_data=LinearFunctionData(proportional_term=m, constant_term=c) - ) - else: - return InputOutputCurve( - function_data=QuadraticFunctionData( - quadratic_term=p, proportional_term=m, constant_term=c - ), + function_data=PiecewiseLinearData(points=list(zip(xs, ys))), input_at_zero=data.input_at_zero, ) - - elif isinstance(data.function_data, PiecewiseStepData): - c = data.initial_input - if c is None: - raise ValueError("Cannot convert `AverageRateCurve` with undefined `initial_input`") - - xs = data.function_data.x_coords - ys = np.multiply(xs[1:], data.function_data.y_coords).tolist() - ys.insert(0, c) - - return InputOutputCurve( - function_data=PiecewiseLinearData(points=list(zip(xs, ys))), - input_at_zero=data.input_at_zero, - ) + case _: + raise ISMethodError("Function is not valid for the type of data provided.") def AverageRateToIncremental(data: AverageRateCurve) -> IncrementalCurve: From 27fa407f1609ba39118c466bd9f82736bd6664aa Mon Sep 17 00:00:00 2001 From: Jerry Potts Date: Tue, 2 Jul 2024 12:38:47 -0600 Subject: [PATCH 11/37] initial class definitions --- src/infrasys/value_curves.py | 96 ++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 src/infrasys/value_curves.py diff --git a/src/infrasys/value_curves.py b/src/infrasys/value_curves.py new file mode 100644 index 0000000..9edef12 --- /dev/null +++ b/src/infrasys/value_curves.py @@ -0,0 +1,96 @@ +"""Defines classes for value curves using cost functions""" + +from infrasys import Component +from typing import Union +from typing_extensions import Annotated +from infrasys.function_data import ( + LinearFunctionData, + QuadraticFunctionData, + PiecewiseLinearData, + PiecewiseStepData, +) +from pydantic import Field + + +class InputOutputCurve(Component): + """Input-output curve relating production quality to cost. + + An input-output curve, directly relating the production quantity to the cost: `y = f(x)`. + Can be used, for instance, in the representation of a [`CostCurve`](@ref) where `x` is MW + and `y` is currency/hr, or in the representation of a [`FuelCurve`](@ref) where `x` is MW + and `y` is fuel/hr. + """ + + name: Annotated[str, Field(frozen=True)] = "" + function_data: Annotated[ + Union[QuadraticFunctionData, LinearFunctionData, PiecewiseLinearData], + Field(description="The underlying `FunctionData` representation of this `ValueCurve`"), + ] + input_at_zero: Annotated[ + Union[None, float], + Field( + description="Optional, an explicit representation of the input value at zero output." + ), + ] = None + + +class IncrementalCurve(Component): + """Incremental/marginal curve to relate production quantity to cost derivative. + + An incremental (or 'marginal') curve, relating the production quantity to the derivative of + cost: `y = f'(x)`. Can be used, for instance, in the representation of a [`CostCurve`](@ref) + where `x` is MW and `y` is currency/MWh, or in the representation of a [`FuelCurve`](@ref) + where `x` is MW and `y` is fuel/MWh. + """ + + name: Annotated[str, Field(frozen=True)] = "" + function_data: Annotated[ + Union[LinearFunctionData, PiecewiseStepData], + Field(description="The underlying `FunctionData` representation of this `ValueCurve`"), + ] + initial_input: Annotated[ + Union[float, None], + Field( + description="The value of f(x) at the least x for which the function is defined, or \ + the origin for functions with no left endpoint, used for conversion to `InputOutputCurve`" + ), + ] + input_at_zero: Annotated[ + Union[None, float], + Field( + description="Optional, an explicit representation of the input value at zero output." + ), + ] = None + + +class AverageRateCurve(Component): + """Average rate curve relating production quality to average cost rate. + + An average rate curve, relating the production quantity to the average cost rate from the + origin: `y = f(x)/x`. Can be used, for instance, in the representation of a + [`CostCurve`](@ref) where `x` is MW and `y` is currency/MWh, or in the representation of a + [`FuelCurve`](@ref) where `x` is MW and `y` is fuel/MWh. Typically calculated by dividing + absolute values of cost rate or fuel input rate by absolute values of electric power. + """ + + name: Annotated[str, Field(frozen=True)] = "" + function_data: Annotated[ + Union[LinearFunctionData, PiecewiseStepData], + Field( + description="The underlying `FunctionData` representation of this `ValueCurve`, or \ + only the oblique asymptote when using `LinearFunctionData`" + ), + ] + initial_input: Annotated[ + Union[float, None], + Field( + description="The value of f(x) at the least x for which the function is defined, or \ + the origin for functions with no left endpoint, used for conversion to `InputOutputCurve`" + ), + ] + input_at_zero: Annotated[ + Union[None, float], + Field( + description="Optional, an explicit representation of the input value at zero output." + ), + ] = None From ed074a0ab14a08991fd797e5bd768854eba118e3 Mon Sep 17 00:00:00 2001 From: Jerry Potts Date: Mon, 8 Jul 2024 11:29:30 -0600 Subject: [PATCH 12/37] add get_slopes --- src/infrasys/function_data.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/infrasys/function_data.py b/src/infrasys/function_data.py index 551f8e7..8753761 100644 --- a/src/infrasys/function_data.py +++ b/src/infrasys/function_data.py @@ -170,3 +170,28 @@ 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 + + Function used to calculate the slopes from a list of XYCoords. + 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 From 85b7cc055712c245665b8a703a5f4be31130e1d7 Mon Sep 17 00:00:00 2001 From: Jerry Potts Date: Mon, 8 Jul 2024 12:09:24 -0600 Subject: [PATCH 13/37] add InputOutput Conversions --- src/infrasys/value_curves.py | 133 +++++++++++++++++++++++++++++++++-- 1 file changed, 127 insertions(+), 6 deletions(-) diff --git a/src/infrasys/value_curves.py b/src/infrasys/value_curves.py index 9edef12..254e186 100644 --- a/src/infrasys/value_curves.py +++ b/src/infrasys/value_curves.py @@ -8,6 +8,7 @@ QuadraticFunctionData, PiecewiseLinearData, PiecewiseStepData, + get_slopes, ) from pydantic import Field @@ -16,8 +17,8 @@ class InputOutputCurve(Component): """Input-output curve relating production quality to cost. An input-output curve, directly relating the production quantity to the cost: `y = f(x)`. - Can be used, for instance, in the representation of a [`CostCurve`](@ref) where `x` is MW - and `y` is currency/hr, or in the representation of a [`FuelCurve`](@ref) where `x` is MW + Can be used, for instance, in the representation of a Cost Curve where `x` is MW + and `y` is currency/hr, or in the representation of a Fuel Curve where `x` is MW and `y` is fuel/hr. """ @@ -38,8 +39,8 @@ class IncrementalCurve(Component): """Incremental/marginal curve to relate production quantity to cost derivative. An incremental (or 'marginal') curve, relating the production quantity to the derivative of - cost: `y = f'(x)`. Can be used, for instance, in the representation of a [`CostCurve`](@ref) - where `x` is MW and `y` is currency/MWh, or in the representation of a [`FuelCurve`](@ref) + cost: `y = f'(x)`. Can be used, for instance, in the representation of a Cost Curve + where `x` is MW and `y` is currency/MWh, or in the representation of a Fuel Curve where `x` is MW and `y` is fuel/MWh. """ @@ -68,8 +69,8 @@ class AverageRateCurve(Component): An average rate curve, relating the production quantity to the average cost rate from the origin: `y = f(x)/x`. Can be used, for instance, in the representation of a - [`CostCurve`](@ref) where `x` is MW and `y` is currency/MWh, or in the representation of a - [`FuelCurve`](@ref) where `x` is MW and `y` is fuel/MWh. Typically calculated by dividing + Cost Curve where `x` is MW and `y` is currency/MWh, or in the representation of a + Fuel Curve where `x` is MW and `y` is fuel/MWh. Typically calculated by dividing absolute values of cost rate or fuel input rate by absolute values of electric power. """ @@ -94,3 +95,123 @@ class AverageRateCurve(Component): description="Optional, an explicit representation of the input value at zero output." ), ] = None + + +# Converting IO curves to X +def InputOutputLinearToQuadratic(data: InputOutputCurve) -> InputOutputCurve: + """Function to convert linear InputOutput Curve to quadratic + + Parameters + ---------- + data : InputOutputCurve + `InputOutputCurve` using `LinearFunctionData` for its function data. + + Returns + ---------- + InputOutputCurve + `InputOutputCurve` using `QuadraticFunctionData` for its function data. + """ + q = 0.0 + p = data.function_data.proportional_term + c = data.function_data.constant_term + + return InputOutputCurve( + function_data=QuadraticFunctionData( + quadratic_term=q, proportional_term=p, constant_term=c + ), + input_at_zero=data.input_at_zero, + ) + + +def InputOutputToIncremental(data: InputOutputCurve) -> IncrementalCurve: + """Function to convert InputOutputCurve to IncrementalCurve + + TEXT HERE + + Parameters + ---------- + data : InputOutputCurve + `InputOutputCurve` using `LinearFunctionData` for its function data. + + Returns + ---------- + IncrementalCurve + `IncrementalCurve` using either `LinearFunctionData` or `PiecewiseStepData`. + """ + + if isinstance(data.function_data, LinearFunctionData): + q = 0.0 + p = data.function_data.proportional_term + + return IncrementalCurve( + function_data=LinearFunctionData(proportional_term=q, constant_term=p), + initial_input=data.function_data.constant_term, + input_at_zero=data.input_at_zero, + ) + elif isinstance(data.function_data, QuadraticFunctionData): + q = data.function_data.quadratic_term + p = data.function_data.proportional_term + + return IncrementalCurve( + function_data=LinearFunctionData(proportional_term=2 * q, constant_term=p), + initial_input=data.function_data.constant_term, + input_at_zero=data.input_at_zero, + ) + elif isinstance(data.function_data, PiecewiseLinearData): + x = [fd.x for fd in data.function_data.points] + slopes = get_slopes(data.function_data.points) + + return IncrementalCurve( + function_data=PiecewiseStepData(x_coords=x, y_coords=slopes), + initial_input=data.function_data.points[0].y, + input_at_zero=data.input_at_zero, + ) + + return + + +def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: + """Function to convert InputOutputCurve to AverageRateCurve + + TEXT HERE + + Parameters + ---------- + data : InputOutputCurve + `InputOutputCurve` using `LinearFunctionData` for its function data. + + Returns + ---------- + AverageRateCurve + `AverageRateCurve` using either `LinearFunctionData` or `PiecewiseStepData`. + """ + if isinstance(data.function_data, LinearFunctionData): + q = 0.0 + p = data.function_data.proportional_term + + return AverageRateCurve( + function_data=LinearFunctionData(q, p), + initial_input=data.function_data.constant_term, + input_at_zero=data.input_at_zero, + ) + elif isinstance(data.function_data, QuadraticFunctionData): + q = data.function_data.quadratic_term + p = data.function_data.proportional_term + + return AverageRateCurve( + function_data=LinearFunctionData(q, p), + initial_input=data.function_data.constant_term, + input_at_zero=data.input_at_zero, + ) + elif isinstance(data.function_data, PiecewiseLinearData): + # I think I need to add in the getters and stuff from function_data to make this easier + x = [fd.x for fd in data.function_data.points] + slopes_from_origin = [fd.y / fd.x for fd in data.function_data.points[1:]] + + return AverageRateCurve( + function_data=PiecewiseStepData(x_coords=x, y_coords=slopes_from_origin), + initial_input=data.function_data.points[0].y, + input_at_zero=data.input_at_zero, + ) + + return From 392944bf2b6a06408f4121346efa0179936608db Mon Sep 17 00:00:00 2001 From: Jerry Potts Date: Mon, 8 Jul 2024 13:55:07 -0600 Subject: [PATCH 14/37] Added IncrementalCurve conversions --- src/infrasys/function_data.py | 19 +++++++ src/infrasys/value_curves.py | 100 ++++++++++++++++++++++++++++++++-- 2 files changed, 113 insertions(+), 6 deletions(-) diff --git a/src/infrasys/function_data.py b/src/infrasys/function_data.py index 8753761..68f36eb 100644 --- a/src/infrasys/function_data.py +++ b/src/infrasys/function_data.py @@ -195,3 +195,22 @@ def get_slopes(vc: List[XYCoords]) -> List[float]: 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]: + 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 diff --git a/src/infrasys/value_curves.py b/src/infrasys/value_curves.py index 254e186..6645012 100644 --- a/src/infrasys/value_curves.py +++ b/src/infrasys/value_curves.py @@ -9,6 +9,7 @@ PiecewiseLinearData, PiecewiseStepData, get_slopes, + running_sum, ) from pydantic import Field @@ -126,17 +127,22 @@ def InputOutputLinearToQuadratic(data: InputOutputCurve) -> InputOutputCurve: def InputOutputToIncremental(data: InputOutputCurve) -> IncrementalCurve: """Function to convert InputOutputCurve to IncrementalCurve - TEXT HERE + Function takes and InputOutputCurve and converts it to a corresponding + incremental curve depending on the type of function_data. If the InputOutputCurve + uses LinearFunctionData or QuadraticFunctionData, the corresponding + IncrementalCurve uses the corresponding derivative for its `function_data`. If + the input uses PiecewiseLinearData, the slopes of each segment are calculated and + converted to PiecewiseStepData for the IncrementalCurve. Parameters ---------- data : InputOutputCurve - `InputOutputCurve` using `LinearFunctionData` for its function data. + InputOutputCurve using LinearFunctionData for its function data. Returns ---------- IncrementalCurve - `IncrementalCurve` using either `LinearFunctionData` or `PiecewiseStepData`. + IncrementalCurve using either LinearFunctionData or PiecewiseStepData. """ if isinstance(data.function_data, LinearFunctionData): @@ -173,17 +179,23 @@ def InputOutputToIncremental(data: InputOutputCurve) -> IncrementalCurve: def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: """Function to convert InputOutputCurve to AverageRateCurve - TEXT HERE + Function takes and InputOutputCurve and converts it to a corresponding + AverageRateCurve depending on the type of function_data. If the InputOutputCurve + uses LinearFunctionData or QuadraticFunctionData, the corresponding + IncrementalCurve uses the LinearFunctionData, with a slope equal to the `quadratic_term` + (0.0 if originally linear), and a intercept equal to the `proportional_term`. If + the input uses PiecewiseLinearData, the slopes of each segment are calculated and + converted to PiecewiseStepData for the AverageRateCurve. Parameters ---------- data : InputOutputCurve - `InputOutputCurve` using `LinearFunctionData` for its function data. + InputOutputCurve using LinearFunctionData for its function data. Returns ---------- AverageRateCurve - `AverageRateCurve` using either `LinearFunctionData` or `PiecewiseStepData`. + AverageRateCurve using either LinearFunctionData or PiecewiseStepData. """ if isinstance(data.function_data, LinearFunctionData): q = 0.0 @@ -215,3 +227,79 @@ def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: ) return + + +# Converting Incremental Curves to X +def IncrementalToInputOutput(data: IncrementalCurve) -> InputOutputCurve: + if isinstance(data.function_data, LinearFunctionData): + p = data.function_data.proportional_term + m = data.function_data.constant_term + + if data.initial_input is None: + ValueError("Cannot convert `IncrementalCurve` with undefined `initial_input`") + else: + c = data.initial_input + + if p == 0: + return InputOutputCurve( + function_data=LinearFunctionData(proportional_term=m, constant_term=c) + ) + else: + return InputOutputCurve( + function_data=QuadraticFunctionData( + quadratic_term=p / 2, proportional_term=m, constant_term=c + ), + input_at_zero=data.input_at_zero, + ) + + elif isinstance(data.function_data, PiecewiseStepData): + if data.initial_input is None: + ValueError("Cannot convert `IncrementalCurve` with undefined `initial_input`") + else: + c = data.initial_input + + points = running_sum(data.function_data) + + return InputOutputCurve( + function_data=PiecewiseLinearData(points=[(p.x, p.y + c) for p in points]), + input_at_zero=data.input_at_zero, + ) + return + + +# Converting Incremental Curves to X +def IncrementalToAverageRate(data: IncrementalCurve) -> AverageRateCurve: + if isinstance(data.function_data, LinearFunctionData): + p = data.function_data.proportional_term + m = data.function_data.constant_term + + if data.initial_input is None: + ValueError("Cannot convert `IncrementalCurve` with undefined `initial_input`") + else: + c = data.initial_input + + if p == 0: + return AverageRateCurve( + function_data=LinearFunctionData(proportional_term=m, constant_term=c) + ) + else: + return AverageRateCurve( + function_data=QuadraticFunctionData( + quadratic_term=p / 2, proportional_term=m, constant_term=c + ), + input_at_zero=data.input_at_zero, + ) + + elif isinstance(data.function_data, PiecewiseStepData): + if data.initial_input is None: + ValueError("Cannot convert `IncrementalCurve` with undefined `initial_input`") + else: + c = data.initial_input + + points = running_sum(data.function_data) + + return AverageRateCurve( + function_data=PiecewiseLinearData(points=[(p.x, p.y + c) for p in points]), + input_at_zero=data.input_at_zero, + ) + return From b4ff63915ada074c1cf0e9ef786b0e213e3ab1ab Mon Sep 17 00:00:00 2001 From: Jerry Potts Date: Mon, 8 Jul 2024 14:36:51 -0600 Subject: [PATCH 15/37] Add AverageRate conversion functions --- src/infrasys/function_data.py | 2 +- src/infrasys/value_curves.py | 35 +++++++++++++++++++++++++---------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/infrasys/function_data.py b/src/infrasys/function_data.py index 68f36eb..6537153 100644 --- a/src/infrasys/function_data.py +++ b/src/infrasys/function_data.py @@ -198,7 +198,7 @@ def get_slopes(vc: List[XYCoords]) -> List[float]: def get_x_lengths(x_coords: List[float]) -> List[float]: - return np.subtract(x_coords[1:], x_coords[:-1]) + return np.subtract(x_coords[1:], x_coords[:-1]).tolist() def running_sum(data: PiecewiseStepData) -> List[XYCoords]: diff --git a/src/infrasys/value_curves.py b/src/infrasys/value_curves.py index 6645012..cccefb3 100644 --- a/src/infrasys/value_curves.py +++ b/src/infrasys/value_curves.py @@ -12,6 +12,7 @@ running_sum, ) from pydantic import Field +import numpy as np class InputOutputCurve(Component): @@ -267,39 +268,53 @@ def IncrementalToInputOutput(data: IncrementalCurve) -> InputOutputCurve: return -# Converting Incremental Curves to X def IncrementalToAverageRate(data: IncrementalCurve) -> AverageRateCurve: + io_curve = IncrementalToInputOutput(data) + + return InputOutputToAverageRate(io_curve) + + +# Converting Average Rate Curves to X + + +def AverageRateToInputOutput(data: AverageRateCurve) -> InputOutputCurve: if isinstance(data.function_data, LinearFunctionData): p = data.function_data.proportional_term m = data.function_data.constant_term if data.initial_input is None: - ValueError("Cannot convert `IncrementalCurve` with undefined `initial_input`") + ValueError("Cannot convert `AverageRateCurve` with undefined `initial_input`") else: c = data.initial_input if p == 0: - return AverageRateCurve( + return InputOutputCurve( function_data=LinearFunctionData(proportional_term=m, constant_term=c) ) else: - return AverageRateCurve( + return InputOutputCurve( function_data=QuadraticFunctionData( - quadratic_term=p / 2, proportional_term=m, constant_term=c + quadratic_term=p, proportional_term=m, constant_term=c ), input_at_zero=data.input_at_zero, ) elif isinstance(data.function_data, PiecewiseStepData): if data.initial_input is None: - ValueError("Cannot convert `IncrementalCurve` with undefined `initial_input`") + ValueError("Cannot convert `AverageCurve` with undefined `initial_input`") else: c = data.initial_input - points = running_sum(data.function_data) - - return AverageRateCurve( - function_data=PiecewiseLinearData(points=[(p.x, p.y + c) for p in points]), + xs = data.function_data.x_coords + ys = np.multiply(xs[1:], data.function_data.y_coords).tolist() + return InputOutputCurve( + function_data=PiecewiseLinearData(list(zip(xs, ys))), input_at_zero=data.input_at_zero, ) return + + +def AverageRatetoIncremental(data: AverageRateCurve) -> IncrementalCurve: + io_curve = AverageRateToInputOutput(data) + + return InputOutputToIncremental(io_curve) From 3fe0ddf0fd7b4599069500d593151b349603f63d Mon Sep 17 00:00:00 2001 From: Jerry Potts Date: Mon, 8 Jul 2024 15:24:57 -0600 Subject: [PATCH 16/37] docstrings --- src/infrasys/function_data.py | 28 ++++++++++++ src/infrasys/value_curves.py | 81 ++++++++++++++++++++++++++++++++--- 2 files changed, 104 insertions(+), 5 deletions(-) diff --git a/src/infrasys/function_data.py b/src/infrasys/function_data.py index 6537153..c319270 100644 --- a/src/infrasys/function_data.py +++ b/src/infrasys/function_data.py @@ -198,10 +198,38 @@ def get_slopes(vc: List[XYCoords]) -> List[float]: 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: + `y[i] = y[i-1] + slope[i-1]*(x[i] - x[i-1])` + + 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 diff --git a/src/infrasys/value_curves.py b/src/infrasys/value_curves.py index cccefb3..55db4b6 100644 --- a/src/infrasys/value_curves.py +++ b/src/infrasys/value_curves.py @@ -138,12 +138,12 @@ def InputOutputToIncremental(data: InputOutputCurve) -> IncrementalCurve: Parameters ---------- data : InputOutputCurve - InputOutputCurve using LinearFunctionData for its function data. + Original InputOutputCurve for conversion. Returns ---------- IncrementalCurve - IncrementalCurve using either LinearFunctionData or PiecewiseStepData. + IncrementalCurve using either LinearFunctionData or PiecewiseStepData after conversion. """ if isinstance(data.function_data, LinearFunctionData): @@ -191,12 +191,12 @@ def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: Parameters ---------- data : InputOutputCurve - InputOutputCurve using LinearFunctionData for its function data. + Original InputOutputCurve for conversion. Returns ---------- AverageRateCurve - AverageRateCurve using either LinearFunctionData or PiecewiseStepData. + AverageRateCurve using either LinearFunctionData or PiecewiseStepData after conversion. """ if isinstance(data.function_data, LinearFunctionData): q = 0.0 @@ -232,6 +232,25 @@ def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: # Converting Incremental Curves to X def IncrementalToInputOutput(data: IncrementalCurve) -> InputOutputCurve: + """Function to convert IncrementalCurve to InputOutputCurve + + Function takes an IncrementalCurve and converts it to a corresponding + InputOutputCurve depending on the type of function_data. If the IncrementalCurve + uses LinearFunctionData, the new InputOutputCurve is created linear or quadratic data + that correspond to the integral of the original linear function. If the input uses + PiecewiseStepData, the slopes of each segment are used to calculate the corresponding + y values for each x value and used to construct PiecewiseLinearData for the InputOutputCurve. + + Parameters + ---------- + data : IncrementalCurve + Original IncrementalCurve for conversion. + + Returns + ---------- + InputOutputCurve + InputOutputCurve using either QuadraticFunctionData or PiecewiseStepData. + """ if isinstance(data.function_data, LinearFunctionData): p = data.function_data.proportional_term m = data.function_data.constant_term @@ -269,15 +288,49 @@ def IncrementalToInputOutput(data: IncrementalCurve) -> InputOutputCurve: def IncrementalToAverageRate(data: IncrementalCurve) -> AverageRateCurve: + """Function to convert IncrementalCurve to AverageRateCurve + + Function takes an IncrementalCurve and first converts it to an + InputOutputCurve, which is then converted into an AverageRateCurve. + + Parameters + ---------- + data : IncrementalCurve + Original InputOutputCurve for conversion. + + Returns + ---------- + AverageRateCurve + AverageRateCurve using either QuadraticFunctionData or PiecewiseStepData. + """ + io_curve = IncrementalToInputOutput(data) return InputOutputToAverageRate(io_curve) # Converting Average Rate Curves to X +def AverageRateToInputOutput(data: AverageRateCurve) -> InputOutputCurve: + """Function to convert IncrementalCurve to InputOutputCurve + Function takes an AverageRateCurve and converts it to a corresponding + InputOutputCurve depending on the type of function_data. If the AverageRateCurve + uses LinearFunctionData, the new InputOutputCurve is created with either linear or quadratic + function data, depending on if the original function data is constant or linear. If the + input uses PiecewiseStepData, new y-values are calculated for each x value such that `f(x) = x*y` + and used to construct PiecewiseLinearData for the InputOutputCurve. + + Parameters + ---------- + data : AverageRateCurve + Original AverageRateCurve for conversion. + + Returns + ---------- + InputOutputCurve + InputOutputCurve using either QuadraticFunctionData or PiecewiseStepData. + """ -def AverageRateToInputOutput(data: AverageRateCurve) -> InputOutputCurve: if isinstance(data.function_data, LinearFunctionData): p = data.function_data.proportional_term m = data.function_data.constant_term @@ -307,6 +360,8 @@ def AverageRateToInputOutput(data: AverageRateCurve) -> InputOutputCurve: xs = data.function_data.x_coords ys = np.multiply(xs[1:], data.function_data.y_coords).tolist() + ys.insert(0, c) + return InputOutputCurve( function_data=PiecewiseLinearData(list(zip(xs, ys))), input_at_zero=data.input_at_zero, @@ -315,6 +370,22 @@ def AverageRateToInputOutput(data: AverageRateCurve) -> InputOutputCurve: def AverageRatetoIncremental(data: AverageRateCurve) -> IncrementalCurve: + """Function to convert AverageRateCurve to IncrementalCurve + + Function takes an AverageRateCurve and first converts it to an + InputOutputCurve, which is then converted into an IncrementalCurve. + + Parameters + ---------- + data : AverageRateCurve + Original AverageRateCurve for conversion. + + Returns + ---------- + IncrementalCurve + IncrementalCurve using either QuadraticFunctionData or PiecewiseStepData. + """ + io_curve = AverageRateToInputOutput(data) return InputOutputToIncremental(io_curve) From 4355de0b54e07bb6a0b71afb8295330a8fdd1db1 Mon Sep 17 00:00:00 2001 From: Jerry Potts Date: Mon, 8 Jul 2024 16:51:43 -0600 Subject: [PATCH 17/37] add function_data tests --- tests/test_function_data.py | 78 ++++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/tests/test_function_data.py b/tests/test_function_data.py index 3888373..9cce084 100644 --- a/tests/test_function_data.py +++ b/tests/test_function_data.py @@ -1,7 +1,20 @@ -from infrasys.function_data import PiecewiseStepData, PiecewiseLinearData, XYCoords +from infrasys.function_data import ( + LinearFunctionData, + PiecewiseStepData, + PiecewiseLinearData, + XYCoords, + get_slopes, + running_sum, +) +from infrasys import Component +from .models.simple_system import SimpleSystem import pytest +class FunctionDataComponent(Component): + function_data: LinearFunctionData + + def test_xycoords(): test_xy = XYCoords(x=1.0, y=2.0) @@ -48,3 +61,66 @@ def test_piecewise_step(): with pytest.raises(ValueError): PiecewiseStepData(x_coords=test_x, y_coords=test_y) + + +def test_function_data_custom_serialization(): + component = FunctionDataComponent( + name="test", function_data=LinearFunctionData(proportional_term=1.0, constant_term=2.0) + ) + + model_dump = component.model_dump(mode="json") + assert model_dump["function_data"]["proportional_term"] == 1.0 + + model_dump = component.model_dump(context={"magnitude_only": True}) + assert model_dump["function_data"]["proportional_term"] == 1.0 + + model_dump = component.model_dump(mode="json", context={"magnitude_only": True}) + assert model_dump["function_data"]["proportional_term"] == 1.0 + + +def test_function_data_serialization(tmp_path): + system = SimpleSystem(auto_add_composed_components=True) + + f1 = FunctionDataComponent( + name="test", function_data=LinearFunctionData(proportional_term=1.0, constant_term=2.0) + ) + system.add_component(f1) + filename = tmp_path / "function_data.json" + + system.to_json(filename, overwrite=True) + system2 = SimpleSystem.from_json(filename) + + assert system2 is not None + + f2 = system2.get_component(FunctionDataComponent, "test") + + assert f2 is not None + assert f1.function_data.proportional_term == f2.function_data.proportional_term + assert f1.function_data.constant_term == f2.function_data.constant_term + + +def test_slopes_calculation(): + test_xy = [XYCoords(1.0, 2.0), XYCoords(2.0, 4.0), XYCoords(4.0, 10.0)] + + slopes = get_slopes(test_xy) + + correct_slopes = [2.0, 3.0] + + assert slopes == correct_slopes + + +def test_running_sum(): + test_x = [1.0, 3.0, 6.0] + test_y = [2.0, 4.0] + + pws = PiecewiseStepData(x_coords=test_x, y_coords=test_y) + + points = running_sum(pws) + + x_values = [p.x for p in points] + y_values = [p.y for p in points] + + correct_y_values = [0.0, 4.0, 16.0] + + assert x_values == test_x + assert y_values == correct_y_values From c1030d6f7d1febe1acf3eb18c80f3ae749704994 Mon Sep 17 00:00:00 2001 From: Jerry Potts Date: Mon, 8 Jul 2024 17:55:06 -0600 Subject: [PATCH 18/37] add initial test functions --- src/infrasys/value_curves.py | 18 ++-- tests/test_values_curves.py | 159 +++++++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+), 10 deletions(-) create mode 100644 tests/test_values_curves.py diff --git a/src/infrasys/value_curves.py b/src/infrasys/value_curves.py index 55db4b6..61a76ad 100644 --- a/src/infrasys/value_curves.py +++ b/src/infrasys/value_curves.py @@ -203,7 +203,7 @@ def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: p = data.function_data.proportional_term return AverageRateCurve( - function_data=LinearFunctionData(q, p), + function_data=LinearFunctionData(proportional_term=q, constant_term=p), initial_input=data.function_data.constant_term, input_at_zero=data.input_at_zero, ) @@ -212,7 +212,7 @@ def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: p = data.function_data.proportional_term return AverageRateCurve( - function_data=LinearFunctionData(q, p), + function_data=LinearFunctionData(proportional_term=q, constant_term=p), initial_input=data.function_data.constant_term, input_at_zero=data.input_at_zero, ) @@ -255,10 +255,9 @@ def IncrementalToInputOutput(data: IncrementalCurve) -> InputOutputCurve: p = data.function_data.proportional_term m = data.function_data.constant_term - if data.initial_input is None: - ValueError("Cannot convert `IncrementalCurve` with undefined `initial_input`") - else: - c = data.initial_input + c = data.initial_input + if c is None: + raise ValueError("Cannot convert `IncrementalCurve` with undefined `initial_input`") if p == 0: return InputOutputCurve( @@ -273,10 +272,9 @@ def IncrementalToInputOutput(data: IncrementalCurve) -> InputOutputCurve: ) elif isinstance(data.function_data, PiecewiseStepData): - if data.initial_input is None: - ValueError("Cannot convert `IncrementalCurve` with undefined `initial_input`") - else: - c = data.initial_input + c = data.initial_input + if c is None: + raise ValueError("Cannot convert `IncrementalCurve` with undefined `initial_input`") points = running_sum(data.function_data) diff --git a/tests/test_values_curves.py b/tests/test_values_curves.py new file mode 100644 index 0000000..10041e2 --- /dev/null +++ b/tests/test_values_curves.py @@ -0,0 +1,159 @@ +from infrasys.function_data import ( + LinearFunctionData, + QuadraticFunctionData, + PiecewiseStepData, + PiecewiseLinearData, + XYCoords, +) +from infrasys.value_curves import ( + InputOutputCurve, + IncrementalCurve, + AverageRateCurve, + InputOutputToAverageRate, + InputOutputToIncremental, + IncrementalToInputOutput, +) +from infrasys import Component +from .models.simple_system import SimpleSystem +import pytest + + +class ValueCurveComponent(Component): + value_curve: InputOutputCurve + + +def test_input_output_curve(): + curve = InputOutputCurve( + function_data=LinearFunctionData(proportional_term=1.0, constant_term=1.0) + ) + + assert isinstance(curve, InputOutputCurve) + assert isinstance(curve.function_data, LinearFunctionData) + + +def test_incremental_curve(): + curve = IncrementalCurve( + function_data=LinearFunctionData(proportional_term=1.0, constant_term=1.0), + initial_input=1.0, + ) + + assert isinstance(curve, IncrementalCurve) + assert isinstance(curve.function_data, LinearFunctionData) + + +def test_average_rate_curve(): + curve = AverageRateCurve( + function_data=LinearFunctionData(proportional_term=1.0, constant_term=1.0), + initial_input=1.0, + ) + + assert isinstance(curve, AverageRateCurve) + assert isinstance(curve.function_data, LinearFunctionData) + + +def test_input_output_conversion(): + # Linear function data + curve = InputOutputCurve( + function_data=LinearFunctionData(proportional_term=1.0, constant_term=1.0) + ) + new_curve = InputOutputToAverageRate(curve) + assert isinstance(new_curve, AverageRateCurve) + + new_curve = InputOutputToIncremental(curve) + assert isinstance(new_curve, IncrementalCurve) + + # Quadratic function data + q = 3.0 + p = 2.0 + c = 1.0 + + curve = InputOutputCurve( + function_data=QuadraticFunctionData(quadratic_term=q, proportional_term=p, constant_term=c) + ) + new_curve = InputOutputToAverageRate(curve) + assert isinstance(new_curve, AverageRateCurve) + assert new_curve.function_data.proportional_term == q + + new_curve = InputOutputToIncremental(curve) + assert isinstance(new_curve, IncrementalCurve) + assert new_curve.function_data.proportional_term == 2 * q + + # Piecewise linear data + xy = [XYCoords(1.0, 2.0), XYCoords(2.0, 4.0), XYCoords(4.0, 10.0)] + + curve = InputOutputCurve(function_data=PiecewiseLinearData(points=xy)) + new_curve = InputOutputToAverageRate(curve) + assert isinstance(new_curve, AverageRateCurve) + assert new_curve.function_data.y_coords == [2.0, 2.5] + + new_curve = InputOutputToIncremental(curve) + assert isinstance(new_curve, IncrementalCurve) + + +def test_incremental_conversion(): + # Linear function data + curve = IncrementalCurve( + function_data=LinearFunctionData(proportional_term=1.0, constant_term=1.0), + initial_input=None, + ) + with pytest.raises(ValueError): + IncrementalToInputOutput(curve) + + curve.initial_input = 0.0 + new_curve = IncrementalToInputOutput(curve) + assert isinstance(new_curve, InputOutputCurve) + + # Piecewise step data + data = PiecewiseStepData(x_coords=[1.0, 3.0, 5.0], y_coords=[2.0, 6.0]) + curve = IncrementalCurve(function_data=data, initial_input=None) + with pytest.raises(ValueError): + IncrementalToInputOutput(curve) + + curve.initial_input = 0.0 + new_curve = IncrementalToInputOutput(curve) + assert isinstance(new_curve, InputOutputCurve) + + +def test_value_curve_custom_serialization(): + component = ValueCurveComponent( + name="test", + value_curve=InputOutputCurve( + function_data=LinearFunctionData(proportional_term=1.0, constant_term=2.0) + ), + ) + + model_dump = component.model_dump(mode="json") + assert model_dump["value_curve"]["function_data"]["proportional_term"] == 1.0 + + model_dump = component.model_dump(context={"magnitude_only": True}) + assert model_dump["value_curve"]["function_data"]["proportional_term"] == 1.0 + + model_dump = component.model_dump(mode="json", context={"magnitude_only": True}) + assert model_dump["value_curve"]["function_data"]["proportional_term"] == 1.0 + + +def test_value_curve_serialization(tmp_path): + system = SimpleSystem(auto_add_composed_components=True) + + v1 = ValueCurveComponent( + name="test", + value_curve=InputOutputCurve( + function_data=LinearFunctionData(proportional_term=1.0, constant_term=2.0) + ), + ) + system.add_component(v1) + filename = tmp_path / "value_curve.json" + + system.to_json(filename, overwrite=True) + system2 = SimpleSystem.from_json(filename) + + assert system2 is not None + + v2 = system2.get_component(ValueCurveComponent, "test") + + assert v2 is not None + assert ( + v1.value_curve.function_data.proportional_term + == v2.value_curve.function_data.proportional_term + ) + assert v1.value_curve.function_data.constant_term == v2.value_curve.function_data.constant_term From 336f9619db18534a547532dba2ba930146daca65 Mon Sep 17 00:00:00 2001 From: Jerry Potts Date: Mon, 8 Jul 2024 18:14:55 -0600 Subject: [PATCH 19/37] additional conversion tests --- src/infrasys/value_curves.py | 24 +++++---------- tests/test_values_curves.py | 59 ++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 16 deletions(-) diff --git a/src/infrasys/value_curves.py b/src/infrasys/value_curves.py index 61a76ad..02c665b 100644 --- a/src/infrasys/value_curves.py +++ b/src/infrasys/value_curves.py @@ -174,8 +174,6 @@ def InputOutputToIncremental(data: InputOutputCurve) -> IncrementalCurve: input_at_zero=data.input_at_zero, ) - return - def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: """Function to convert InputOutputCurve to AverageRateCurve @@ -227,8 +225,6 @@ def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: input_at_zero=data.input_at_zero, ) - return - # Converting Incremental Curves to X def IncrementalToInputOutput(data: IncrementalCurve) -> InputOutputCurve: @@ -282,7 +278,6 @@ def IncrementalToInputOutput(data: IncrementalCurve) -> InputOutputCurve: function_data=PiecewiseLinearData(points=[(p.x, p.y + c) for p in points]), input_at_zero=data.input_at_zero, ) - return def IncrementalToAverageRate(data: IncrementalCurve) -> AverageRateCurve: @@ -333,10 +328,9 @@ def AverageRateToInputOutput(data: AverageRateCurve) -> InputOutputCurve: p = data.function_data.proportional_term m = data.function_data.constant_term - if data.initial_input is None: - ValueError("Cannot convert `AverageRateCurve` with undefined `initial_input`") - else: - c = data.initial_input + c = data.initial_input + if c is None: + raise ValueError("Cannot convert `AverageRateCurve` with undefined `initial_input`") if p == 0: return InputOutputCurve( @@ -351,23 +345,21 @@ def AverageRateToInputOutput(data: AverageRateCurve) -> InputOutputCurve: ) elif isinstance(data.function_data, PiecewiseStepData): - if data.initial_input is None: - ValueError("Cannot convert `AverageCurve` with undefined `initial_input`") - else: - c = data.initial_input + c = data.initial_input + if c is None: + raise ValueError("Cannot convert `AverageRateCurve` with undefined `initial_input`") xs = data.function_data.x_coords ys = np.multiply(xs[1:], data.function_data.y_coords).tolist() ys.insert(0, c) return InputOutputCurve( - function_data=PiecewiseLinearData(list(zip(xs, ys))), + function_data=PiecewiseLinearData(points=list(zip(xs, ys))), input_at_zero=data.input_at_zero, ) - return -def AverageRatetoIncremental(data: AverageRateCurve) -> IncrementalCurve: +def AverageRateToIncremental(data: AverageRateCurve) -> IncrementalCurve: """Function to convert AverageRateCurve to IncrementalCurve Function takes an AverageRateCurve and first converts it to an diff --git a/tests/test_values_curves.py b/tests/test_values_curves.py index 10041e2..86d4292 100644 --- a/tests/test_values_curves.py +++ b/tests/test_values_curves.py @@ -9,9 +9,13 @@ InputOutputCurve, IncrementalCurve, AverageRateCurve, + InputOutputLinearToQuadratic, InputOutputToAverageRate, InputOutputToIncremental, IncrementalToInputOutput, + IncrementalToAverageRate, + AverageRateToInputOutput, + AverageRateToIncremental, ) from infrasys import Component from .models.simple_system import SimpleSystem @@ -62,6 +66,9 @@ def test_input_output_conversion(): new_curve = InputOutputToIncremental(curve) assert isinstance(new_curve, IncrementalCurve) + new_curve = InputOutputLinearToQuadratic(curve) + assert isinstance(new_curve.function_data, QuadraticFunctionData) + # Quadratic function data q = 3.0 p = 2.0 @@ -102,6 +109,17 @@ def test_incremental_conversion(): curve.initial_input = 0.0 new_curve = IncrementalToInputOutput(curve) assert isinstance(new_curve, InputOutputCurve) + assert isinstance(new_curve.function_data, QuadraticFunctionData) + assert new_curve.function_data.quadratic_term == 0.5 + + new_curve = IncrementalToAverageRate(curve) + assert isinstance(new_curve, AverageRateCurve) + + curve.function_data.proportional_term = 0.0 + new_curve = IncrementalToInputOutput(curve) + assert isinstance(new_curve, InputOutputCurve) + assert isinstance(new_curve.function_data, LinearFunctionData) + assert new_curve.function_data.proportional_term == 1.0 # Piecewise step data data = PiecewiseStepData(x_coords=[1.0, 3.0, 5.0], y_coords=[2.0, 6.0]) @@ -113,6 +131,47 @@ def test_incremental_conversion(): new_curve = IncrementalToInputOutput(curve) assert isinstance(new_curve, InputOutputCurve) + new_curve = IncrementalToAverageRate(curve) + assert isinstance(new_curve, AverageRateCurve) + + +def test_average_rate_conversion(): + # Linear function data + curve = AverageRateCurve( + function_data=LinearFunctionData(proportional_term=1.0, constant_term=2.0), + initial_input=None, + ) + with pytest.raises(ValueError): + AverageRateToInputOutput(curve) + + curve.initial_input = 0.0 + new_curve = AverageRateToInputOutput(curve) + assert isinstance(new_curve, InputOutputCurve) + assert isinstance(new_curve.function_data, QuadraticFunctionData) + assert new_curve.function_data.quadratic_term == 1.0 + + new_curve = AverageRateToIncremental(curve) + assert isinstance(new_curve, IncrementalCurve) + + curve.function_data.proportional_term = 0.0 + new_curve = AverageRateToInputOutput(curve) + assert isinstance(new_curve, InputOutputCurve) + assert isinstance(new_curve.function_data, LinearFunctionData) + assert new_curve.function_data.proportional_term == 2.0 + + # Piecewise step data + data = PiecewiseStepData(x_coords=[1.0, 3.0, 5.0], y_coords=[2.0, 6.0]) + curve = AverageRateCurve(function_data=data, initial_input=None) + with pytest.raises(ValueError): + AverageRateToInputOutput(curve) + + curve.initial_input = 0.0 + new_curve = AverageRateToInputOutput(curve) + assert isinstance(new_curve, InputOutputCurve) + + new_curve = AverageRateToIncremental(curve) + assert isinstance(new_curve, IncrementalCurve) + def test_value_curve_custom_serialization(): component = ValueCurveComponent( From a6109a4dfdc3cec60747dfeef1ae70dd5bca6884 Mon Sep 17 00:00:00 2001 From: pesap Date: Fri, 2 Aug 2024 17:44:09 -0600 Subject: [PATCH 20/37] Modification of the docstings and mypy compliant --- docs/reference/api/function_data.md | 9 + docs/reference/api/index.md | 1 + src/infrasys/exceptions.py | 4 + src/infrasys/function_data.py | 84 +++++---- src/infrasys/value_curves.py | 267 ++++++++++++++++------------ 5 files changed, 210 insertions(+), 155 deletions(-) create mode 100644 docs/reference/api/function_data.md diff --git a/docs/reference/api/function_data.md b/docs/reference/api/function_data.md new file mode 100644 index 0000000..bb138e5 --- /dev/null +++ b/docs/reference/api/function_data.md @@ -0,0 +1,9 @@ +```{eval-rst} +.. _function-data-api: +``` +# Function data + +```{eval-rst} +.. automodule:: infrasys.function_data + :members: +``` diff --git a/docs/reference/api/index.md b/docs/reference/api/index.md index 760627b..e6027c6 100644 --- a/docs/reference/api/index.md +++ b/docs/reference/api/index.md @@ -13,4 +13,5 @@ time_series location quantities + function_data ``` diff --git a/src/infrasys/exceptions.py b/src/infrasys/exceptions.py index 8254ac2..b81d00e 100644 --- a/src/infrasys/exceptions.py +++ b/src/infrasys/exceptions.py @@ -31,3 +31,7 @@ class ISNotStored(ISBaseException): class ISOperationNotAllowed(ISBaseException): """Raised if the requested operation is not allowed.""" + + +class ISMethodError(ISBaseException): + """Rased if the requested method is not allowed.""" diff --git a/src/infrasys/function_data.py b/src/infrasys/function_data.py index c319270..7fe1ea6 100644 --- a/src/infrasys/function_data.py +++ b/src/infrasys/function_data.py @@ -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]). @@ -65,7 +69,7 @@ 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. """ @@ -73,12 +77,12 @@ def validate_piecewise_linear_x(points: List[XYCoords]) -> List[XYCoords]: 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}.") return points @@ -86,7 +90,6 @@ def validate_piecewise_linear_x(points: List[XYCoords]) -> List[XYCoords]: 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]). @@ -97,18 +100,18 @@ 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 @@ -116,42 +119,45 @@ def validate_piecewise_step_x(x_coords: List[float]) -> List[float]: 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`." + ) ), ] @@ -159,7 +165,7 @@ class PiecewiseStepData(Component): 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. @@ -175,14 +181,13 @@ def validate_piecewise_xy(self): def get_slopes(vc: List[XYCoords]) -> List[float]: """Calculate slopes from XYCoord data - Function used to calculate the slopes from a list of XYCoords. 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. + List of named tuples of (x, y) coordinates. Returns ---------- @@ -216,9 +221,10 @@ def get_x_lengths(x_coords: List[float]) -> List[float]: 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: - `y[i] = y[i-1] + slope[i-1]*(x[i] - x[i-1])` + 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 ---------- diff --git a/src/infrasys/value_curves.py b/src/infrasys/value_curves.py index 02c665b..bb8e714 100644 --- a/src/infrasys/value_curves.py +++ b/src/infrasys/value_curves.py @@ -3,6 +3,7 @@ from infrasys import Component from typing import Union from typing_extensions import Annotated +from infrasys.exceptions import ISMethodError from infrasys.function_data import ( LinearFunctionData, QuadraticFunctionData, @@ -18,15 +19,18 @@ class InputOutputCurve(Component): """Input-output curve relating production quality to cost. - An input-output curve, directly relating the production quantity to the cost: `y = f(x)`. - Can be used, for instance, in the representation of a Cost Curve where `x` is MW - and `y` is currency/hr, or in the representation of a Fuel Curve where `x` is MW - and `y` is fuel/hr. + An input-output curve, directly relating the production quantity to the cost: + + .. math:: y = f(x). + + Can be used, for instance, in the representation of a Cost Curve where :math:`x` is MW + and :math:`y` is currency/hr, or in the representation of a Fuel Curve where :math:`x` is MW + and :math:`y` is fuel/hr. """ name: Annotated[str, Field(frozen=True)] = "" function_data: Annotated[ - Union[QuadraticFunctionData, LinearFunctionData, PiecewiseLinearData], + QuadraticFunctionData | LinearFunctionData | PiecewiseLinearData, Field(description="The underlying `FunctionData` representation of this `ValueCurve`"), ] input_at_zero: Annotated[ @@ -41,25 +45,29 @@ class IncrementalCurve(Component): """Incremental/marginal curve to relate production quantity to cost derivative. An incremental (or 'marginal') curve, relating the production quantity to the derivative of - cost: `y = f'(x)`. Can be used, for instance, in the representation of a Cost Curve - where `x` is MW and `y` is currency/MWh, or in the representation of a Fuel Curve - where `x` is MW and `y` is fuel/MWh. + cost: + + ..math:: y = f'(x). + + Can be used, for instance, in the representation of a Cost Curve + where :math:`x` is MW and :math:`y` is currency/MWh, or in the representation of a Fuel Curve + where :math:`x` is MW and :math:`y` is fuel/MWh. """ name: Annotated[str, Field(frozen=True)] = "" function_data: Annotated[ - Union[LinearFunctionData, PiecewiseStepData], + LinearFunctionData | PiecewiseStepData, Field(description="The underlying `FunctionData` representation of this `ValueCurve`"), ] initial_input: Annotated[ - Union[float, None], + float | None, Field( description="The value of f(x) at the least x for which the function is defined, or \ the origin for functions with no left endpoint, used for conversion to `InputOutputCurve`" ), ] input_at_zero: Annotated[ - Union[None, float], + float | None, Field( description="Optional, an explicit representation of the input value at zero output." ), @@ -70,50 +78,59 @@ class AverageRateCurve(Component): """Average rate curve relating production quality to average cost rate. An average rate curve, relating the production quantity to the average cost rate from the - origin: `y = f(x)/x`. Can be used, for instance, in the representation of a - Cost Curve where `x` is MW and `y` is currency/MWh, or in the representation of a - Fuel Curve where `x` is MW and `y` is fuel/MWh. Typically calculated by dividing + origin: + + .. math:: y = f(x)/x. + + Can be used, for instance, in the representation of a + Cost Curve where :math:`x` is MW and :math:`y` is currency/MWh, or in the representation of a + Fuel Curve where :math:`x` is MW and :math:`y` is fuel/MWh. Typically calculated by dividing absolute values of cost rate or fuel input rate by absolute values of electric power. """ name: Annotated[str, Field(frozen=True)] = "" function_data: Annotated[ - Union[LinearFunctionData, PiecewiseStepData], + LinearFunctionData | PiecewiseStepData, Field( description="The underlying `FunctionData` representation of this `ValueCurve`, or \ only the oblique asymptote when using `LinearFunctionData`" ), ] initial_input: Annotated[ - Union[float, None], + float | None, Field( description="The value of f(x) at the least x for which the function is defined, or \ the origin for functions with no left endpoint, used for conversion to `InputOutputCurve`" ), ] input_at_zero: Annotated[ - Union[None, float], + float | None, Field( description="Optional, an explicit representation of the input value at zero output." ), ] = None -# Converting IO curves to X def InputOutputLinearToQuadratic(data: InputOutputCurve) -> InputOutputCurve: """Function to convert linear InputOutput Curve to quadratic + Converting IO curves to X + Parameters ---------- data : InputOutputCurve `InputOutputCurve` using `LinearFunctionData` for its function data. Returns - ---------- + ------- InputOutputCurve `InputOutputCurve` using `QuadraticFunctionData` for its function data. """ q = 0.0 + + if isinstance(data.function_data, PiecewiseStepData | PiecewiseLinearData): + raise ISMethodError("Can not convert Piecewise data to Quadratic.") + p = data.function_data.proportional_term c = data.function_data.constant_term @@ -129,10 +146,10 @@ def InputOutputToIncremental(data: InputOutputCurve) -> IncrementalCurve: """Function to convert InputOutputCurve to IncrementalCurve Function takes and InputOutputCurve and converts it to a corresponding - incremental curve depending on the type of function_data. If the InputOutputCurve - uses LinearFunctionData or QuadraticFunctionData, the corresponding - IncrementalCurve uses the corresponding derivative for its `function_data`. If - the input uses PiecewiseLinearData, the slopes of each segment are calculated and + incremental curve depending on the type of function_data. If the :class:`InputOutputCurve` + uses :class:`LinearFunctionData` or :class:`QuadraticFunctionData`, the corresponding + :class:`IncrementalCurve` uses the corresponding derivative for its `function_data`. If + the input uses :class:`PiecewiseLinearData`, the slopes of each segment are calculated and converted to PiecewiseStepData for the IncrementalCurve. Parameters @@ -141,50 +158,57 @@ def InputOutputToIncremental(data: InputOutputCurve) -> IncrementalCurve: Original InputOutputCurve for conversion. Returns - ---------- + ------- IncrementalCurve IncrementalCurve using either LinearFunctionData or PiecewiseStepData after conversion. - """ - if isinstance(data.function_data, LinearFunctionData): - q = 0.0 - p = data.function_data.proportional_term - - return IncrementalCurve( - function_data=LinearFunctionData(proportional_term=q, constant_term=p), - initial_input=data.function_data.constant_term, - input_at_zero=data.input_at_zero, - ) - elif isinstance(data.function_data, QuadraticFunctionData): - q = data.function_data.quadratic_term - p = data.function_data.proportional_term + Raises + ------ + ISMethodError + Function is not valid for the type of data provided. + """ + match data.function_data: + case LinearFunctionData(): + q = 0.0 + p = data.function_data.proportional_term + + return IncrementalCurve( + function_data=LinearFunctionData(proportional_term=q, constant_term=p), + initial_input=data.function_data.constant_term, + input_at_zero=data.input_at_zero, + ) + case QuadraticFunctionData(): + q = data.function_data.quadratic_term + p = data.function_data.proportional_term - return IncrementalCurve( - function_data=LinearFunctionData(proportional_term=2 * q, constant_term=p), - initial_input=data.function_data.constant_term, - input_at_zero=data.input_at_zero, - ) - elif isinstance(data.function_data, PiecewiseLinearData): - x = [fd.x for fd in data.function_data.points] - slopes = get_slopes(data.function_data.points) + return IncrementalCurve( + function_data=LinearFunctionData(proportional_term=2 * q, constant_term=p), + initial_input=data.function_data.constant_term, + input_at_zero=data.input_at_zero, + ) + case PiecewiseLinearData(): + x = [fd.x for fd in data.function_data.points] + slopes = get_slopes(data.function_data.points) - return IncrementalCurve( - function_data=PiecewiseStepData(x_coords=x, y_coords=slopes), - initial_input=data.function_data.points[0].y, - input_at_zero=data.input_at_zero, - ) + return IncrementalCurve( + function_data=PiecewiseStepData(x_coords=x, y_coords=slopes), + initial_input=data.function_data.points[0].y, + input_at_zero=data.input_at_zero, + ) + case _: + raise ISMethodError("Function is not valid for the type of data provided.") def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: """Function to convert InputOutputCurve to AverageRateCurve - Function takes and InputOutputCurve and converts it to a corresponding - AverageRateCurve depending on the type of function_data. If the InputOutputCurve - uses LinearFunctionData or QuadraticFunctionData, the corresponding - IncrementalCurve uses the LinearFunctionData, with a slope equal to the `quadratic_term` - (0.0 if originally linear), and a intercept equal to the `proportional_term`. If - the input uses PiecewiseLinearData, the slopes of each segment are calculated and - converted to PiecewiseStepData for the AverageRateCurve. + If the :class:`InputOutputCurve` uses :class:`LinearFunctionData` or + :class:`QuadraticFunctionData`, the corresponding :class:`IncrementalCurve` + uses the :class`LinearFunctionData`, with a slope equal to the + `quadratic_term` (0.0 if originally linear), and a intercept equal to the + `proportional_term`. If the input uses :class:`PiecewiseLinearData`, the + slopes of each segment are calculated and converted to PiecewiseStepData + for the AverageRateCurve. Parameters ---------- @@ -195,38 +219,45 @@ def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: ---------- AverageRateCurve AverageRateCurve using either LinearFunctionData or PiecewiseStepData after conversion. - """ - if isinstance(data.function_data, LinearFunctionData): - q = 0.0 - p = data.function_data.proportional_term - return AverageRateCurve( - function_data=LinearFunctionData(proportional_term=q, constant_term=p), - initial_input=data.function_data.constant_term, - input_at_zero=data.input_at_zero, - ) - elif isinstance(data.function_data, QuadraticFunctionData): - q = data.function_data.quadratic_term - p = data.function_data.proportional_term + Raises + ------ + ISMethodError + Function is not valid for the type of data provided. + """ + match data.function_data: + case LinearFunctionData(): + q = 0.0 + p = data.function_data.proportional_term + + return AverageRateCurve( + function_data=LinearFunctionData(proportional_term=q, constant_term=p), + initial_input=data.function_data.constant_term, + input_at_zero=data.input_at_zero, + ) + case QuadraticFunctionData(): + q = data.function_data.quadratic_term + p = data.function_data.proportional_term - return AverageRateCurve( - function_data=LinearFunctionData(proportional_term=q, constant_term=p), - initial_input=data.function_data.constant_term, - input_at_zero=data.input_at_zero, - ) - elif isinstance(data.function_data, PiecewiseLinearData): - # I think I need to add in the getters and stuff from function_data to make this easier - x = [fd.x for fd in data.function_data.points] - slopes_from_origin = [fd.y / fd.x for fd in data.function_data.points[1:]] - - return AverageRateCurve( - function_data=PiecewiseStepData(x_coords=x, y_coords=slopes_from_origin), - initial_input=data.function_data.points[0].y, - input_at_zero=data.input_at_zero, - ) + return AverageRateCurve( + function_data=LinearFunctionData(proportional_term=q, constant_term=p), + initial_input=data.function_data.constant_term, + input_at_zero=data.input_at_zero, + ) + case PiecewiseLinearData(): + # I think I need to add in the getters and stuff from function_data to make this easier + x = [fd.x for fd in data.function_data.points] + slopes_from_origin = [fd.y / fd.x for fd in data.function_data.points[1:]] + + return AverageRateCurve( + function_data=PiecewiseStepData(x_coords=x, y_coords=slopes_from_origin), + initial_input=data.function_data.points[0].y, + input_at_zero=data.input_at_zero, + ) + case _: + raise ISMethodError("Function is not valid for the type of data provided.") -# Converting Incremental Curves to X def IncrementalToInputOutput(data: IncrementalCurve) -> InputOutputCurve: """Function to convert IncrementalCurve to InputOutputCurve @@ -302,7 +333,6 @@ def IncrementalToAverageRate(data: IncrementalCurve) -> AverageRateCurve: return InputOutputToAverageRate(io_curve) -# Converting Average Rate Curves to X def AverageRateToInputOutput(data: AverageRateCurve) -> InputOutputCurve: """Function to convert IncrementalCurve to InputOutputCurve @@ -323,40 +353,45 @@ def AverageRateToInputOutput(data: AverageRateCurve) -> InputOutputCurve: InputOutputCurve InputOutputCurve using either QuadraticFunctionData or PiecewiseStepData. """ + match data.function_data: + case LinearFunctionData(): + p = data.function_data.proportional_term + m = data.function_data.constant_term + + c = data.initial_input + if c is None: + raise ValueError( + "Cannot convert `AverageRateCurve` with undefined `initial_input`" + ) + + if p == 0: + return InputOutputCurve( + function_data=LinearFunctionData(proportional_term=m, constant_term=c) + ) + else: + return InputOutputCurve( + function_data=QuadraticFunctionData( + quadratic_term=p, proportional_term=m, constant_term=c + ), + input_at_zero=data.input_at_zero, + ) + case PiecewiseLinearData(): + c = data.initial_input + if c is None: + raise ValueError( + "Cannot convert `AverageRateCurve` with undefined `initial_input`" + ) + + xs = data.function_data.x_coords + ys = np.multiply(xs[1:], data.function_data.y_coords).tolist() + ys.insert(0, c) - if isinstance(data.function_data, LinearFunctionData): - p = data.function_data.proportional_term - m = data.function_data.constant_term - - c = data.initial_input - if c is None: - raise ValueError("Cannot convert `AverageRateCurve` with undefined `initial_input`") - - if p == 0: return InputOutputCurve( - function_data=LinearFunctionData(proportional_term=m, constant_term=c) - ) - else: - return InputOutputCurve( - function_data=QuadraticFunctionData( - quadratic_term=p, proportional_term=m, constant_term=c - ), + function_data=PiecewiseLinearData(points=list(zip(xs, ys))), input_at_zero=data.input_at_zero, ) - - elif isinstance(data.function_data, PiecewiseStepData): - c = data.initial_input - if c is None: - raise ValueError("Cannot convert `AverageRateCurve` with undefined `initial_input`") - - xs = data.function_data.x_coords - ys = np.multiply(xs[1:], data.function_data.y_coords).tolist() - ys.insert(0, c) - - return InputOutputCurve( - function_data=PiecewiseLinearData(points=list(zip(xs, ys))), - input_at_zero=data.input_at_zero, - ) + case _: + raise ISMethodError("Function is not valid for the type of data provided.") def AverageRateToIncremental(data: AverageRateCurve) -> IncrementalCurve: From 5552034a7ff20f6b0a20ab4ee34948bb648b5fbd Mon Sep 17 00:00:00 2001 From: pesap Date: Mon, 5 Aug 2024 12:45:16 -0600 Subject: [PATCH 21/37] Fixing pytest --- src/infrasys/value_curves.py | 4 ++-- tests/test_values_curves.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/infrasys/value_curves.py b/src/infrasys/value_curves.py index bb8e714..b8eaf9c 100644 --- a/src/infrasys/value_curves.py +++ b/src/infrasys/value_curves.py @@ -375,10 +375,10 @@ def AverageRateToInputOutput(data: AverageRateCurve) -> InputOutputCurve: ), input_at_zero=data.input_at_zero, ) - case PiecewiseLinearData(): + case PiecewiseStepData(): c = data.initial_input if c is None: - raise ValueError( + raise ISMethodError( "Cannot convert `AverageRateCurve` with undefined `initial_input`" ) diff --git a/tests/test_values_curves.py b/tests/test_values_curves.py index 86d4292..ddb2717 100644 --- a/tests/test_values_curves.py +++ b/tests/test_values_curves.py @@ -18,6 +18,7 @@ AverageRateToIncremental, ) from infrasys import Component +from infrasys.exceptions import ISMethodError from .models.simple_system import SimpleSystem import pytest @@ -153,6 +154,7 @@ def test_average_rate_conversion(): new_curve = AverageRateToIncremental(curve) assert isinstance(new_curve, IncrementalCurve) + assert isinstance(curve.function_data, LinearFunctionData) curve.function_data.proportional_term = 0.0 new_curve = AverageRateToInputOutput(curve) assert isinstance(new_curve, InputOutputCurve) @@ -162,7 +164,7 @@ def test_average_rate_conversion(): # Piecewise step data data = PiecewiseStepData(x_coords=[1.0, 3.0, 5.0], y_coords=[2.0, 6.0]) curve = AverageRateCurve(function_data=data, initial_input=None) - with pytest.raises(ValueError): + with pytest.raises(ISMethodError): AverageRateToInputOutput(curve) curve.initial_input = 0.0 From db5a69210967fd0da2a88bb71a411ea32617b979 Mon Sep 17 00:00:00 2001 From: pesap Date: Mon, 5 Aug 2024 12:55:34 -0600 Subject: [PATCH 22/37] Adding ruff rule EM and making the code compliant. --- pyproject.toml | 9 +++++---- src/infrasys/exceptions.py | 4 ---- src/infrasys/value_curves.py | 34 +++++++++++++++++++--------------- tests/test_values_curves.py | 4 ++-- 4 files changed, 26 insertions(+), 25 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0223958..90bc50e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = [] diff --git a/src/infrasys/exceptions.py b/src/infrasys/exceptions.py index b81d00e..8254ac2 100644 --- a/src/infrasys/exceptions.py +++ b/src/infrasys/exceptions.py @@ -31,7 +31,3 @@ class ISNotStored(ISBaseException): class ISOperationNotAllowed(ISBaseException): """Raised if the requested operation is not allowed.""" - - -class ISMethodError(ISBaseException): - """Rased if the requested method is not allowed.""" diff --git a/src/infrasys/value_curves.py b/src/infrasys/value_curves.py index b8eaf9c..bc89b9d 100644 --- a/src/infrasys/value_curves.py +++ b/src/infrasys/value_curves.py @@ -3,7 +3,7 @@ from infrasys import Component from typing import Union from typing_extensions import Annotated -from infrasys.exceptions import ISMethodError +from infrasys.exceptions import ISOperationNotAllowed from infrasys.function_data import ( LinearFunctionData, QuadraticFunctionData, @@ -129,7 +129,8 @@ def InputOutputLinearToQuadratic(data: InputOutputCurve) -> InputOutputCurve: q = 0.0 if isinstance(data.function_data, PiecewiseStepData | PiecewiseLinearData): - raise ISMethodError("Can not convert Piecewise data to Quadratic.") + msg = "Can not convert Piecewise data to Quadratic." + raise ISOperationNotAllowed(msg) p = data.function_data.proportional_term c = data.function_data.constant_term @@ -164,7 +165,7 @@ def InputOutputToIncremental(data: InputOutputCurve) -> IncrementalCurve: Raises ------ - ISMethodError + ISOperationNotAllowed Function is not valid for the type of data provided. """ match data.function_data: @@ -196,7 +197,8 @@ def InputOutputToIncremental(data: InputOutputCurve) -> IncrementalCurve: input_at_zero=data.input_at_zero, ) case _: - raise ISMethodError("Function is not valid for the type of data provided.") + msg = "Function is not valid for the type of data provided." + raise ISOperationNotAllowed(msg) def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: @@ -222,7 +224,7 @@ def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: Raises ------ - ISMethodError + ISOperationNotAllowed Function is not valid for the type of data provided. """ match data.function_data: @@ -255,7 +257,8 @@ def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: input_at_zero=data.input_at_zero, ) case _: - raise ISMethodError("Function is not valid for the type of data provided.") + msg = "Function is not valid for the type of data provided." + raise ISOperationNotAllowed(msg) def IncrementalToInputOutput(data: IncrementalCurve) -> InputOutputCurve: @@ -284,7 +287,8 @@ def IncrementalToInputOutput(data: IncrementalCurve) -> InputOutputCurve: c = data.initial_input if c is None: - raise ValueError("Cannot convert `IncrementalCurve` with undefined `initial_input`") + msg = "Cannot convert `IncrementalCurve` with undefined `initial_input`" + raise ISOperationNotAllowed(msg) if p == 0: return InputOutputCurve( @@ -301,7 +305,8 @@ def IncrementalToInputOutput(data: IncrementalCurve) -> InputOutputCurve: elif isinstance(data.function_data, PiecewiseStepData): c = data.initial_input if c is None: - raise ValueError("Cannot convert `IncrementalCurve` with undefined `initial_input`") + msg = "Cannot convert `IncrementalCurve` with undefined `initial_input`" + raise ISOperationNotAllowed(msg) points = running_sum(data.function_data) @@ -360,9 +365,8 @@ def AverageRateToInputOutput(data: AverageRateCurve) -> InputOutputCurve: c = data.initial_input if c is None: - raise ValueError( - "Cannot convert `AverageRateCurve` with undefined `initial_input`" - ) + msg = "Cannot convert `AverageRateCurve` with undefined `initial_input`" + raise ISOperationNotAllowed(msg) if p == 0: return InputOutputCurve( @@ -378,9 +382,8 @@ def AverageRateToInputOutput(data: AverageRateCurve) -> InputOutputCurve: case PiecewiseStepData(): c = data.initial_input if c is None: - raise ISMethodError( - "Cannot convert `AverageRateCurve` with undefined `initial_input`" - ) + msg = "Cannot convert `AverageRateCurve` with undefined `initial_input`" + raise ISOperationNotAllowed(msg) xs = data.function_data.x_coords ys = np.multiply(xs[1:], data.function_data.y_coords).tolist() @@ -391,7 +394,8 @@ def AverageRateToInputOutput(data: AverageRateCurve) -> InputOutputCurve: input_at_zero=data.input_at_zero, ) case _: - raise ISMethodError("Function is not valid for the type of data provided.") + msg = "Function is not valid for the type of data provided." + raise ISOperationNotAllowed(msg) def AverageRateToIncremental(data: AverageRateCurve) -> IncrementalCurve: diff --git a/tests/test_values_curves.py b/tests/test_values_curves.py index ddb2717..5456f01 100644 --- a/tests/test_values_curves.py +++ b/tests/test_values_curves.py @@ -18,7 +18,7 @@ AverageRateToIncremental, ) from infrasys import Component -from infrasys.exceptions import ISMethodError +from infrasys.exceptions import ISOperationNotAllowed from .models.simple_system import SimpleSystem import pytest @@ -164,7 +164,7 @@ def test_average_rate_conversion(): # Piecewise step data data = PiecewiseStepData(x_coords=[1.0, 3.0, 5.0], y_coords=[2.0, 6.0]) curve = AverageRateCurve(function_data=data, initial_input=None) - with pytest.raises(ISMethodError): + with pytest.raises(ISOperationNotAllowed): AverageRateToInputOutput(curve) curve.initial_input = 0.0 From b6e03a54683d5dfe9cb82e2253be1fcb87fc2dde Mon Sep 17 00:00:00 2001 From: pesap Date: Mon, 5 Aug 2024 12:58:22 -0600 Subject: [PATCH 23/37] Propagating rule EM --- src/infrasys/function_data.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/infrasys/function_data.py b/src/infrasys/function_data.py index 7fe1ea6..a0c387a 100644 --- a/src/infrasys/function_data.py +++ b/src/infrasys/function_data.py @@ -77,12 +77,14 @@ def validate_piecewise_linear_x(points: List[XYCoords]) -> List[XYCoords]: 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 @@ -106,12 +108,14 @@ def validate_piecewise_step_x(x_coords: List[float]) -> List[float]: """ 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 @@ -173,7 +177,8 @@ def validate_piecewise_xy(self): 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 From 5916fd55d42d7b26472ef1661ee311ee09f41de4 Mon Sep 17 00:00:00 2001 From: pesap Date: Mon, 5 Aug 2024 13:02:12 -0600 Subject: [PATCH 24/37] More code compliant with EM --- src/infrasys/base_quantity.py | 3 ++- src/infrasys/component_manager.py | 3 ++- src/infrasys/parquet_time_series_storage.py | 3 ++- src/infrasys/system.py | 11 ++++++----- src/infrasys/time_series_manager.py | 6 ++++-- src/infrasys/time_series_metadata_store.py | 11 ++++++----- 6 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/infrasys/base_quantity.py b/src/infrasys/base_quantity.py index edb33db..6908c1e 100644 --- a/src/infrasys/base_quantity.py +++ b/src/infrasys/base_quantity.py @@ -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 diff --git a/src/infrasys/component_manager.py b/src/infrasys/component_manager.py index df9a9d0..88e7c2a 100644 --- a/src/infrasys/component_manager.py +++ b/src/infrasys/component_manager.py @@ -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, diff --git a/src/infrasys/parquet_time_series_storage.py b/src/infrasys/parquet_time_series_storage.py index cbb43c6..8ad2515 100644 --- a/src/infrasys/parquet_time_series_storage.py +++ b/src/infrasys/parquet_time_series_storage.py @@ -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: ... diff --git a/src/infrasys/system.py b/src/infrasys/system.py index 611d743..deaa055 100644 --- a/src/infrasys/system.py +++ b/src/infrasys/system.py @@ -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) @@ -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) @@ -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? diff --git a/src/infrasys/time_series_manager.py b/src/infrasys/time_series_manager.py index 2bd428a..7013387 100644 --- a/src/infrasys/time_series_manager.py +++ b/src/infrasys/time_series_manager.py @@ -81,7 +81,8 @@ def add( """ self._handle_read_only() if not components: - raise ISOperationNotAllowed("add_time_series requires at least one component") + msg = "add_time_series requires at least one component" + raise ISOperationNotAllowed(msg) ts_type = type(time_series) if not issubclass(ts_type, TimeSeriesData): @@ -274,4 +275,5 @@ def deserialize( def _handle_read_only(self) -> None: if self._read_only: - raise ISOperationNotAllowed("Cannot modify time series in read-only mode.") + msg = "Cannot modify time series in read-only mode." + raise ISOperationNotAllowed(msg) diff --git a/src/infrasys/time_series_metadata_store.py b/src/infrasys/time_series_metadata_store.py index cb0b418..e1bb195 100644 --- a/src/infrasys/time_series_metadata_store.py +++ b/src/infrasys/time_series_metadata_store.py @@ -376,9 +376,8 @@ def remove( self._execute(cur, query) count_deleted = self._execute(cur, "SELECT changes()").fetchall()[0][0] if count_deleted != len(ids): - raise Exception( - f"Bug: Unexpected length mismatch {len(ts_uuids)=} {count_deleted=}" - ) + msg = f"Bug: Unexpected length mismatch {len(ts_uuids)=} {count_deleted=}" + raise Exception(msg) self._con.commit() return list(ts_uuids) @@ -393,7 +392,8 @@ def remove( self._con.commit() count_deleted = self._execute(cur, "SELECT changes()").fetchall()[0][0] if len(uuids) != count_deleted: - raise Exception(f"Bug: Unexpected length mismatch: {len(uuids)=} {count_deleted=}") + msg = f"Bug: Unexpected length mismatch: {len(uuids)=} {count_deleted=}" + raise Exception(msg) return uuids def sql(self, query: str) -> list[tuple]: @@ -416,7 +416,8 @@ def _insert_rows(self, rows: list[tuple]) -> None: def _make_components_str(self, *components: Component) -> str: if not components: - raise ISOperationNotAllowed("At least one component must be passed.") + msg = "At least one component must be passed." + raise ISOperationNotAllowed(msg) or_clause = "OR ".join([f"component_uuid = '{x.uuid}'" for x in components]) return f"({or_clause})" From c6f1053f933ef2f8b69a8a1b2aabedddf3049970 Mon Sep 17 00:00:00 2001 From: pesap Date: Mon, 5 Aug 2024 13:16:34 -0600 Subject: [PATCH 25/37] Fixing test --- tests/test_values_curves.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/test_values_curves.py b/tests/test_values_curves.py index 5456f01..38e0b85 100644 --- a/tests/test_values_curves.py +++ b/tests/test_values_curves.py @@ -80,10 +80,12 @@ def test_input_output_conversion(): ) new_curve = InputOutputToAverageRate(curve) assert isinstance(new_curve, AverageRateCurve) + assert isinstance(new_curve.function_data, LinearFunctionData) assert new_curve.function_data.proportional_term == q new_curve = InputOutputToIncremental(curve) assert isinstance(new_curve, IncrementalCurve) + assert isinstance(new_curve.function_data, LinearFunctionData) assert new_curve.function_data.proportional_term == 2 * q # Piecewise linear data @@ -92,6 +94,7 @@ def test_input_output_conversion(): curve = InputOutputCurve(function_data=PiecewiseLinearData(points=xy)) new_curve = InputOutputToAverageRate(curve) assert isinstance(new_curve, AverageRateCurve) + assert isinstance(new_curve.function_data, PiecewiseStepData) assert new_curve.function_data.y_coords == [2.0, 2.5] new_curve = InputOutputToIncremental(curve) @@ -104,7 +107,8 @@ def test_incremental_conversion(): function_data=LinearFunctionData(proportional_term=1.0, constant_term=1.0), initial_input=None, ) - with pytest.raises(ValueError): + assert isinstance(curve.function_data, LinearFunctionData) + with pytest.raises(ISOperationNotAllowed): IncrementalToInputOutput(curve) curve.initial_input = 0.0 @@ -125,7 +129,7 @@ def test_incremental_conversion(): # Piecewise step data data = PiecewiseStepData(x_coords=[1.0, 3.0, 5.0], y_coords=[2.0, 6.0]) curve = IncrementalCurve(function_data=data, initial_input=None) - with pytest.raises(ValueError): + with pytest.raises(ISOperationNotAllowed): IncrementalToInputOutput(curve) curve.initial_input = 0.0 @@ -142,7 +146,7 @@ def test_average_rate_conversion(): function_data=LinearFunctionData(proportional_term=1.0, constant_term=2.0), initial_input=None, ) - with pytest.raises(ValueError): + with pytest.raises(ISOperationNotAllowed): AverageRateToInputOutput(curve) curve.initial_input = 0.0 @@ -213,6 +217,7 @@ def test_value_curve_serialization(tmp_path): v2 = system2.get_component(ValueCurveComponent, "test") assert v2 is not None + assert isinstance(v1.value_curve.function_data, LinearFunctionData) assert ( v1.value_curve.function_data.proportional_term == v2.value_curve.function_data.proportional_term From d841464f4b576a3d0f0465cc6576553357087c0b Mon Sep 17 00:00:00 2001 From: pesap Date: Mon, 12 Aug 2024 10:23:45 -0600 Subject: [PATCH 26/37] Adding abstract classes --- src/infrasys/function_data.py | 137 +++---------- src/infrasys/value_curves.py | 364 ++-------------------------------- tests/test_function_data.py | 66 +++--- tests/test_values_curves.py | 262 ++++++++++++------------ 4 files changed, 213 insertions(+), 616 deletions(-) diff --git a/src/infrasys/function_data.py b/src/infrasys/function_data.py index a0c387a..66fdd2c 100644 --- a/src/infrasys/function_data.py +++ b/src/infrasys/function_data.py @@ -15,7 +15,13 @@ 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. Used to represent linear cost functions of the form @@ -25,7 +31,6 @@ class LinearFunctionData(Component): 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.") ] @@ -34,17 +39,17 @@ class LinearFunctionData(Component): ] -class QuadraticFunctionData(Component): +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, - where :math:`a` is the quadratic term, :math:`b` is the proportional term and :math:`c` is the 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.") ] @@ -59,9 +64,8 @@ class QuadraticFunctionData(Component): def validate_piecewise_linear_x(points: List[XYCoords]) -> List[XYCoords]: """Validates the x data for 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 ---------- @@ -92,9 +96,8 @@ def validate_piecewise_linear_x(points: List[XYCoords]) -> List[XYCoords]: def validate_piecewise_step_x(x_coords: List[float]) -> List[float]: """Validates the x data for 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 ---------- @@ -120,15 +123,15 @@ def validate_piecewise_step_x(x_coords: List[float]) -> List[float]: return x_coords -class PiecewiseLinearData(Component): +class PiecewiseLinearData(FunctionData): """Data representation for piecewise linear cost function. - 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). + 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), @@ -136,21 +139,19 @@ class PiecewiseLinearData(Component): ] -class PiecewiseStepData(Component): +class PiecewiseStepData(FunctionData): """Data representation for piecewise step cost 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, :math:`dy/dx`), such as (MW, USD/MWh). + 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, + :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."), @@ -159,8 +160,8 @@ class PiecewiseStepData(Component): 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`." + "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`." ) ), ] @@ -169,10 +170,10 @@ class PiecewiseStepData(Component): 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 :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. + 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) @@ -181,75 +182,3 @@ def validate_piecewise_xy(self): raise ValueError(msg) 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 diff --git a/src/infrasys/value_curves.py b/src/infrasys/value_curves.py index bc89b9d..7bd0b52 100644 --- a/src/infrasys/value_curves.py +++ b/src/infrasys/value_curves.py @@ -1,47 +1,44 @@ """Defines classes for value curves using cost functions""" -from infrasys import Component -from typing import Union from typing_extensions import Annotated -from infrasys.exceptions import ISOperationNotAllowed +from infrasys.component import Component from infrasys.function_data import ( + FunctionData, LinearFunctionData, - QuadraticFunctionData, - PiecewiseLinearData, PiecewiseStepData, - get_slopes, - running_sum, ) from pydantic import Field -import numpy as np -class InputOutputCurve(Component): +class ValueCurve(Component): + name: Annotated[str, Field(frozen=True)] = "" + input_at_zero: Annotated[ + float | None, + Field( + description="Optional, an explicit representation of the input value at zero output." + ), + ] = None + + +class InputOutputCurve(ValueCurve): """Input-output curve relating production quality to cost. An input-output curve, directly relating the production quantity to the cost: .. math:: y = f(x). - Can be used, for instance, in the representation of a Cost Curve where :math:`x` is MW - and :math:`y` is currency/hr, or in the representation of a Fuel Curve where :math:`x` is MW - and :math:`y` is fuel/hr. + Can be used, for instance, in the representation of a Cost Curve where :math:`x` is MW and + :math:`y` is currency/hr, or in the representation of a Fuel Curve where :math:`x` is MW and + :math:`y` is fuel/hr. """ - name: Annotated[str, Field(frozen=True)] = "" function_data: Annotated[ - QuadraticFunctionData | LinearFunctionData | PiecewiseLinearData, + FunctionData, Field(description="The underlying `FunctionData` representation of this `ValueCurve`"), ] - input_at_zero: Annotated[ - Union[None, float], - Field( - description="Optional, an explicit representation of the input value at zero output." - ), - ] = None -class IncrementalCurve(Component): +class IncrementalCurve(ValueCurve): """Incremental/marginal curve to relate production quantity to cost derivative. An incremental (or 'marginal') curve, relating the production quantity to the derivative of @@ -54,7 +51,6 @@ class IncrementalCurve(Component): where :math:`x` is MW and :math:`y` is fuel/MWh. """ - name: Annotated[str, Field(frozen=True)] = "" function_data: Annotated[ LinearFunctionData | PiecewiseStepData, Field(description="The underlying `FunctionData` representation of this `ValueCurve`"), @@ -66,15 +62,9 @@ class IncrementalCurve(Component): the origin for functions with no left endpoint, used for conversion to `InputOutputCurve`" ), ] - input_at_zero: Annotated[ - float | None, - Field( - description="Optional, an explicit representation of the input value at zero output." - ), - ] = None -class AverageRateCurve(Component): +class AverageRateCurve(ValueCurve): """Average rate curve relating production quality to average cost rate. An average rate curve, relating the production quantity to the average cost rate from the @@ -88,7 +78,6 @@ class AverageRateCurve(Component): absolute values of cost rate or fuel input rate by absolute values of electric power. """ - name: Annotated[str, Field(frozen=True)] = "" function_data: Annotated[ LinearFunctionData | PiecewiseStepData, Field( @@ -103,318 +92,3 @@ class AverageRateCurve(Component): the origin for functions with no left endpoint, used for conversion to `InputOutputCurve`" ), ] - input_at_zero: Annotated[ - float | None, - Field( - description="Optional, an explicit representation of the input value at zero output." - ), - ] = None - - -def InputOutputLinearToQuadratic(data: InputOutputCurve) -> InputOutputCurve: - """Function to convert linear InputOutput Curve to quadratic - - Converting IO curves to X - - Parameters - ---------- - data : InputOutputCurve - `InputOutputCurve` using `LinearFunctionData` for its function data. - - Returns - ------- - InputOutputCurve - `InputOutputCurve` using `QuadraticFunctionData` for its function data. - """ - q = 0.0 - - if isinstance(data.function_data, PiecewiseStepData | PiecewiseLinearData): - msg = "Can not convert Piecewise data to Quadratic." - raise ISOperationNotAllowed(msg) - - p = data.function_data.proportional_term - c = data.function_data.constant_term - - return InputOutputCurve( - function_data=QuadraticFunctionData( - quadratic_term=q, proportional_term=p, constant_term=c - ), - input_at_zero=data.input_at_zero, - ) - - -def InputOutputToIncremental(data: InputOutputCurve) -> IncrementalCurve: - """Function to convert InputOutputCurve to IncrementalCurve - - Function takes and InputOutputCurve and converts it to a corresponding - incremental curve depending on the type of function_data. If the :class:`InputOutputCurve` - uses :class:`LinearFunctionData` or :class:`QuadraticFunctionData`, the corresponding - :class:`IncrementalCurve` uses the corresponding derivative for its `function_data`. If - the input uses :class:`PiecewiseLinearData`, the slopes of each segment are calculated and - converted to PiecewiseStepData for the IncrementalCurve. - - Parameters - ---------- - data : InputOutputCurve - Original InputOutputCurve for conversion. - - Returns - ------- - IncrementalCurve - IncrementalCurve using either LinearFunctionData or PiecewiseStepData after conversion. - - Raises - ------ - ISOperationNotAllowed - Function is not valid for the type of data provided. - """ - match data.function_data: - case LinearFunctionData(): - q = 0.0 - p = data.function_data.proportional_term - - return IncrementalCurve( - function_data=LinearFunctionData(proportional_term=q, constant_term=p), - initial_input=data.function_data.constant_term, - input_at_zero=data.input_at_zero, - ) - case QuadraticFunctionData(): - q = data.function_data.quadratic_term - p = data.function_data.proportional_term - - return IncrementalCurve( - function_data=LinearFunctionData(proportional_term=2 * q, constant_term=p), - initial_input=data.function_data.constant_term, - input_at_zero=data.input_at_zero, - ) - case PiecewiseLinearData(): - x = [fd.x for fd in data.function_data.points] - slopes = get_slopes(data.function_data.points) - - return IncrementalCurve( - function_data=PiecewiseStepData(x_coords=x, y_coords=slopes), - initial_input=data.function_data.points[0].y, - input_at_zero=data.input_at_zero, - ) - case _: - msg = "Function is not valid for the type of data provided." - raise ISOperationNotAllowed(msg) - - -def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: - """Function to convert InputOutputCurve to AverageRateCurve - - If the :class:`InputOutputCurve` uses :class:`LinearFunctionData` or - :class:`QuadraticFunctionData`, the corresponding :class:`IncrementalCurve` - uses the :class`LinearFunctionData`, with a slope equal to the - `quadratic_term` (0.0 if originally linear), and a intercept equal to the - `proportional_term`. If the input uses :class:`PiecewiseLinearData`, the - slopes of each segment are calculated and converted to PiecewiseStepData - for the AverageRateCurve. - - Parameters - ---------- - data : InputOutputCurve - Original InputOutputCurve for conversion. - - Returns - ---------- - AverageRateCurve - AverageRateCurve using either LinearFunctionData or PiecewiseStepData after conversion. - - Raises - ------ - ISOperationNotAllowed - Function is not valid for the type of data provided. - """ - match data.function_data: - case LinearFunctionData(): - q = 0.0 - p = data.function_data.proportional_term - - return AverageRateCurve( - function_data=LinearFunctionData(proportional_term=q, constant_term=p), - initial_input=data.function_data.constant_term, - input_at_zero=data.input_at_zero, - ) - case QuadraticFunctionData(): - q = data.function_data.quadratic_term - p = data.function_data.proportional_term - - return AverageRateCurve( - function_data=LinearFunctionData(proportional_term=q, constant_term=p), - initial_input=data.function_data.constant_term, - input_at_zero=data.input_at_zero, - ) - case PiecewiseLinearData(): - # I think I need to add in the getters and stuff from function_data to make this easier - x = [fd.x for fd in data.function_data.points] - slopes_from_origin = [fd.y / fd.x for fd in data.function_data.points[1:]] - - return AverageRateCurve( - function_data=PiecewiseStepData(x_coords=x, y_coords=slopes_from_origin), - initial_input=data.function_data.points[0].y, - input_at_zero=data.input_at_zero, - ) - case _: - msg = "Function is not valid for the type of data provided." - raise ISOperationNotAllowed(msg) - - -def IncrementalToInputOutput(data: IncrementalCurve) -> InputOutputCurve: - """Function to convert IncrementalCurve to InputOutputCurve - - Function takes an IncrementalCurve and converts it to a corresponding - InputOutputCurve depending on the type of function_data. If the IncrementalCurve - uses LinearFunctionData, the new InputOutputCurve is created linear or quadratic data - that correspond to the integral of the original linear function. If the input uses - PiecewiseStepData, the slopes of each segment are used to calculate the corresponding - y values for each x value and used to construct PiecewiseLinearData for the InputOutputCurve. - - Parameters - ---------- - data : IncrementalCurve - Original IncrementalCurve for conversion. - - Returns - ---------- - InputOutputCurve - InputOutputCurve using either QuadraticFunctionData or PiecewiseStepData. - """ - if isinstance(data.function_data, LinearFunctionData): - p = data.function_data.proportional_term - m = data.function_data.constant_term - - c = data.initial_input - if c is None: - msg = "Cannot convert `IncrementalCurve` with undefined `initial_input`" - raise ISOperationNotAllowed(msg) - - if p == 0: - return InputOutputCurve( - function_data=LinearFunctionData(proportional_term=m, constant_term=c) - ) - else: - return InputOutputCurve( - function_data=QuadraticFunctionData( - quadratic_term=p / 2, proportional_term=m, constant_term=c - ), - input_at_zero=data.input_at_zero, - ) - - elif isinstance(data.function_data, PiecewiseStepData): - c = data.initial_input - if c is None: - msg = "Cannot convert `IncrementalCurve` with undefined `initial_input`" - raise ISOperationNotAllowed(msg) - - points = running_sum(data.function_data) - - return InputOutputCurve( - function_data=PiecewiseLinearData(points=[(p.x, p.y + c) for p in points]), - input_at_zero=data.input_at_zero, - ) - - -def IncrementalToAverageRate(data: IncrementalCurve) -> AverageRateCurve: - """Function to convert IncrementalCurve to AverageRateCurve - - Function takes an IncrementalCurve and first converts it to an - InputOutputCurve, which is then converted into an AverageRateCurve. - - Parameters - ---------- - data : IncrementalCurve - Original InputOutputCurve for conversion. - - Returns - ---------- - AverageRateCurve - AverageRateCurve using either QuadraticFunctionData or PiecewiseStepData. - """ - - io_curve = IncrementalToInputOutput(data) - - return InputOutputToAverageRate(io_curve) - - -def AverageRateToInputOutput(data: AverageRateCurve) -> InputOutputCurve: - """Function to convert IncrementalCurve to InputOutputCurve - - Function takes an AverageRateCurve and converts it to a corresponding - InputOutputCurve depending on the type of function_data. If the AverageRateCurve - uses LinearFunctionData, the new InputOutputCurve is created with either linear or quadratic - function data, depending on if the original function data is constant or linear. If the - input uses PiecewiseStepData, new y-values are calculated for each x value such that `f(x) = x*y` - and used to construct PiecewiseLinearData for the InputOutputCurve. - - Parameters - ---------- - data : AverageRateCurve - Original AverageRateCurve for conversion. - - Returns - ---------- - InputOutputCurve - InputOutputCurve using either QuadraticFunctionData or PiecewiseStepData. - """ - match data.function_data: - case LinearFunctionData(): - p = data.function_data.proportional_term - m = data.function_data.constant_term - - c = data.initial_input - if c is None: - msg = "Cannot convert `AverageRateCurve` with undefined `initial_input`" - raise ISOperationNotAllowed(msg) - - if p == 0: - return InputOutputCurve( - function_data=LinearFunctionData(proportional_term=m, constant_term=c) - ) - else: - return InputOutputCurve( - function_data=QuadraticFunctionData( - quadratic_term=p, proportional_term=m, constant_term=c - ), - input_at_zero=data.input_at_zero, - ) - case PiecewiseStepData(): - c = data.initial_input - if c is None: - msg = "Cannot convert `AverageRateCurve` with undefined `initial_input`" - raise ISOperationNotAllowed(msg) - - xs = data.function_data.x_coords - ys = np.multiply(xs[1:], data.function_data.y_coords).tolist() - ys.insert(0, c) - - return InputOutputCurve( - function_data=PiecewiseLinearData(points=list(zip(xs, ys))), - input_at_zero=data.input_at_zero, - ) - case _: - msg = "Function is not valid for the type of data provided." - raise ISOperationNotAllowed(msg) - - -def AverageRateToIncremental(data: AverageRateCurve) -> IncrementalCurve: - """Function to convert AverageRateCurve to IncrementalCurve - - Function takes an AverageRateCurve and first converts it to an - InputOutputCurve, which is then converted into an IncrementalCurve. - - Parameters - ---------- - data : AverageRateCurve - Original AverageRateCurve for conversion. - - Returns - ---------- - IncrementalCurve - IncrementalCurve using either QuadraticFunctionData or PiecewiseStepData. - """ - - io_curve = AverageRateToInputOutput(data) - - return InputOutputToIncremental(io_curve) diff --git a/tests/test_function_data.py b/tests/test_function_data.py index 9cce084..49ca219 100644 --- a/tests/test_function_data.py +++ b/tests/test_function_data.py @@ -3,8 +3,8 @@ PiecewiseStepData, PiecewiseLinearData, XYCoords, - get_slopes, - running_sum, + # get_slopes, + # running_sum, ) from infrasys import Component from .models.simple_system import SimpleSystem @@ -42,22 +42,22 @@ def test_piecewise_linear(): def test_piecewise_step(): # Check minimum x values - test_x = [2] - test_y = [1] + test_x = [2.0] + test_y = [1.0] with pytest.raises(ValueError): PiecewiseStepData(x_coords=test_x, y_coords=test_y) # Check ascending x values - test_x = [1, 4, 3] - test_y = [2, 4] + test_x = [1.0, 4.0, 3.0] + test_y = [2.0, 4.0] with pytest.raises(ValueError): PiecewiseStepData(x_coords=test_x, y_coords=test_y) # Check length of x and y lists - test_x = [1, 2, 3] - test_y = [2, 4, 3] + test_x = [1.0, 2.0, 3.0] + test_y = [2.0, 4.0, 3.0] with pytest.raises(ValueError): PiecewiseStepData(x_coords=test_x, y_coords=test_y) @@ -99,28 +99,28 @@ def test_function_data_serialization(tmp_path): assert f1.function_data.constant_term == f2.function_data.constant_term -def test_slopes_calculation(): - test_xy = [XYCoords(1.0, 2.0), XYCoords(2.0, 4.0), XYCoords(4.0, 10.0)] - - slopes = get_slopes(test_xy) - - correct_slopes = [2.0, 3.0] - - assert slopes == correct_slopes - - -def test_running_sum(): - test_x = [1.0, 3.0, 6.0] - test_y = [2.0, 4.0] - - pws = PiecewiseStepData(x_coords=test_x, y_coords=test_y) - - points = running_sum(pws) - - x_values = [p.x for p in points] - y_values = [p.y for p in points] - - correct_y_values = [0.0, 4.0, 16.0] - - assert x_values == test_x - assert y_values == correct_y_values +# def test_slopes_calculation(): +# test_xy = [XYCoords(1.0, 2.0), XYCoords(2.0, 4.0), XYCoords(4.0, 10.0)] +# +# slopes = get_slopes(test_xy) +# +# correct_slopes = [2.0, 3.0] +# +# assert slopes == correct_slopes + + +# def test_running_sum(): +# test_x = [1.0, 3.0, 6.0] +# test_y = [2.0, 4.0] +# +# pws = PiecewiseStepData(x_coords=test_x, y_coords=test_y) +# +# points = running_sum(pws) +# +# x_values = [p.x for p in points] +# y_values = [p.y for p in points] +# +# correct_y_values = [0.0, 4.0, 16.0] +# +# assert x_values == test_x +# assert y_values == correct_y_values diff --git a/tests/test_values_curves.py b/tests/test_values_curves.py index 38e0b85..5a59cfd 100644 --- a/tests/test_values_curves.py +++ b/tests/test_values_curves.py @@ -1,26 +1,20 @@ from infrasys.function_data import ( LinearFunctionData, - QuadraticFunctionData, - PiecewiseStepData, - PiecewiseLinearData, - XYCoords, ) from infrasys.value_curves import ( InputOutputCurve, IncrementalCurve, AverageRateCurve, - InputOutputLinearToQuadratic, - InputOutputToAverageRate, - InputOutputToIncremental, - IncrementalToInputOutput, - IncrementalToAverageRate, - AverageRateToInputOutput, - AverageRateToIncremental, + # InputOutputLinearToQuadratic, + # InputOutputToAverageRate, + # InputOutputToIncremental, + # IncrementalToInputOutput, + # IncrementalToAverageRate, + # AverageRateToInputOutput, + # AverageRateToIncremental, ) from infrasys import Component -from infrasys.exceptions import ISOperationNotAllowed from .models.simple_system import SimpleSystem -import pytest class ValueCurveComponent(Component): @@ -56,127 +50,127 @@ def test_average_rate_curve(): assert isinstance(curve.function_data, LinearFunctionData) -def test_input_output_conversion(): - # Linear function data - curve = InputOutputCurve( - function_data=LinearFunctionData(proportional_term=1.0, constant_term=1.0) - ) - new_curve = InputOutputToAverageRate(curve) - assert isinstance(new_curve, AverageRateCurve) - - new_curve = InputOutputToIncremental(curve) - assert isinstance(new_curve, IncrementalCurve) - - new_curve = InputOutputLinearToQuadratic(curve) - assert isinstance(new_curve.function_data, QuadraticFunctionData) - - # Quadratic function data - q = 3.0 - p = 2.0 - c = 1.0 - - curve = InputOutputCurve( - function_data=QuadraticFunctionData(quadratic_term=q, proportional_term=p, constant_term=c) - ) - new_curve = InputOutputToAverageRate(curve) - assert isinstance(new_curve, AverageRateCurve) - assert isinstance(new_curve.function_data, LinearFunctionData) - assert new_curve.function_data.proportional_term == q - - new_curve = InputOutputToIncremental(curve) - assert isinstance(new_curve, IncrementalCurve) - assert isinstance(new_curve.function_data, LinearFunctionData) - assert new_curve.function_data.proportional_term == 2 * q - - # Piecewise linear data - xy = [XYCoords(1.0, 2.0), XYCoords(2.0, 4.0), XYCoords(4.0, 10.0)] - - curve = InputOutputCurve(function_data=PiecewiseLinearData(points=xy)) - new_curve = InputOutputToAverageRate(curve) - assert isinstance(new_curve, AverageRateCurve) - assert isinstance(new_curve.function_data, PiecewiseStepData) - assert new_curve.function_data.y_coords == [2.0, 2.5] - - new_curve = InputOutputToIncremental(curve) - assert isinstance(new_curve, IncrementalCurve) - - -def test_incremental_conversion(): - # Linear function data - curve = IncrementalCurve( - function_data=LinearFunctionData(proportional_term=1.0, constant_term=1.0), - initial_input=None, - ) - assert isinstance(curve.function_data, LinearFunctionData) - with pytest.raises(ISOperationNotAllowed): - IncrementalToInputOutput(curve) - - curve.initial_input = 0.0 - new_curve = IncrementalToInputOutput(curve) - assert isinstance(new_curve, InputOutputCurve) - assert isinstance(new_curve.function_data, QuadraticFunctionData) - assert new_curve.function_data.quadratic_term == 0.5 - - new_curve = IncrementalToAverageRate(curve) - assert isinstance(new_curve, AverageRateCurve) - - curve.function_data.proportional_term = 0.0 - new_curve = IncrementalToInputOutput(curve) - assert isinstance(new_curve, InputOutputCurve) - assert isinstance(new_curve.function_data, LinearFunctionData) - assert new_curve.function_data.proportional_term == 1.0 - - # Piecewise step data - data = PiecewiseStepData(x_coords=[1.0, 3.0, 5.0], y_coords=[2.0, 6.0]) - curve = IncrementalCurve(function_data=data, initial_input=None) - with pytest.raises(ISOperationNotAllowed): - IncrementalToInputOutput(curve) - - curve.initial_input = 0.0 - new_curve = IncrementalToInputOutput(curve) - assert isinstance(new_curve, InputOutputCurve) - - new_curve = IncrementalToAverageRate(curve) - assert isinstance(new_curve, AverageRateCurve) - - -def test_average_rate_conversion(): - # Linear function data - curve = AverageRateCurve( - function_data=LinearFunctionData(proportional_term=1.0, constant_term=2.0), - initial_input=None, - ) - with pytest.raises(ISOperationNotAllowed): - AverageRateToInputOutput(curve) - - curve.initial_input = 0.0 - new_curve = AverageRateToInputOutput(curve) - assert isinstance(new_curve, InputOutputCurve) - assert isinstance(new_curve.function_data, QuadraticFunctionData) - assert new_curve.function_data.quadratic_term == 1.0 - - new_curve = AverageRateToIncremental(curve) - assert isinstance(new_curve, IncrementalCurve) - - assert isinstance(curve.function_data, LinearFunctionData) - curve.function_data.proportional_term = 0.0 - new_curve = AverageRateToInputOutput(curve) - assert isinstance(new_curve, InputOutputCurve) - assert isinstance(new_curve.function_data, LinearFunctionData) - assert new_curve.function_data.proportional_term == 2.0 - - # Piecewise step data - data = PiecewiseStepData(x_coords=[1.0, 3.0, 5.0], y_coords=[2.0, 6.0]) - curve = AverageRateCurve(function_data=data, initial_input=None) - with pytest.raises(ISOperationNotAllowed): - AverageRateToInputOutput(curve) - - curve.initial_input = 0.0 - new_curve = AverageRateToInputOutput(curve) - assert isinstance(new_curve, InputOutputCurve) - - new_curve = AverageRateToIncremental(curve) - assert isinstance(new_curve, IncrementalCurve) +# def test_input_output_conversion(): +# # LinearFunctionData function data +# curve = InputOutputCurve( +# function_data=LinearFunctionData(proportional_term=1.0, constant_term=1.0) +# ) +# new_curve = InputOutputToAverageRate(curve) +# assert isinstance(new_curve, AverageRateCurve) +# +# new_curve = InputOutputToIncremental(curve) +# assert isinstance(new_curve, IncrementalCurve) +# +# new_curve = InputOutputLinearToQuadratic(curve) +# assert isinstance(new_curve.function_data, Quadratic) +# +# # Quadratic function data +# q = 3.0 +# p = 2.0 +# c = 1.0 +# +# curve = InputOutputCurve( +# function_data=Quadratic(quadratic_term=q, proportional_term=p, constant_term=c) +# ) +# new_curve = InputOutputToAverageRate(curve) +# assert isinstance(new_curve, AverageRateCurve) +# assert isinstance(new_curve.function_data, LinearFunctionData) +# assert new_curve.function_data.proportional_term == q +# +# new_curve = InputOutputToIncremental(curve) +# assert isinstance(new_curve, IncrementalCurve) +# assert isinstance(new_curve.function_data, LinearFunctionData) +# assert new_curve.function_data.proportional_term == 2 * q +# +# # Piecewise linear data +# xy = [XYCoords(1.0, 2.0), XYCoords(2.0, 4.0), XYCoords(4.0, 10.0)] +# +# curve = InputOutputCurve(function_data=PiecewiseLinearData(points=xy)) +# new_curve = InputOutputToAverageRate(curve) +# assert isinstance(new_curve, AverageRateCurve) +# assert isinstance(new_curve.function_data, PiecewiseStepData) +# assert new_curve.function_data.y_coords == [2.0, 2.5] +# +# new_curve = InputOutputToIncremental(curve) +# assert isinstance(new_curve, IncrementalCurve) + + +# def test_incremental_conversion(): +# # LinearFunctionData function data +# curve = IncrementalCurve( +# function_data=LinearFunctionData(proportional_term=1.0, constant_term=1.0), +# initial_input=None, +# ) +# assert isinstance(curve.function_data, LinearFunctionData) +# with pytest.raises(ISOperationNotAllowed): +# IncrementalToInputOutput(curve) +# +# curve.initial_input = 0.0 +# new_curve = IncrementalToInputOutput(curve) +# assert isinstance(new_curve, InputOutputCurve) +# assert isinstance(new_curve.function_data, Quadratic) +# assert new_curve.function_data.quadratic_term == 0.5 +# +# new_curve = IncrementalToAverageRate(curve) +# assert isinstance(new_curve, AverageRateCurve) +# +# curve.function_data.proportional_term = 0.0 +# new_curve = IncrementalToInputOutput(curve) +# assert isinstance(new_curve, InputOutputCurve) +# assert isinstance(new_curve.function_data, LinearFunctionData) +# assert new_curve.function_data.proportional_term == 1.0 +# +# # Piecewise step data +# data = PiecewiseStepData(x_coords=[1.0, 3.0, 5.0], y_coords=[2.0, 6.0]) +# curve = IncrementalCurve(function_data=data, initial_input=None) +# with pytest.raises(ISOperationNotAllowed): +# IncrementalToInputOutput(curve) +# +# curve.initial_input = 0.0 +# new_curve = IncrementalToInputOutput(curve) +# assert isinstance(new_curve, InputOutputCurve) +# +# new_curve = IncrementalToAverageRate(curve) +# assert isinstance(new_curve, AverageRateCurve) + + +# def test_average_rate_conversion(): +# # LinearFunctionData function data +# curve = AverageRateCurve( +# function_data=LinearFunctionData(proportional_term=1.0, constant_term=2.0), +# initial_input=None, +# ) +# with pytest.raises(ISOperationNotAllowed): +# AverageRateToInputOutput(curve) +# +# curve.initial_input = 0.0 +# new_curve = AverageRateToInputOutput(curve) +# assert isinstance(new_curve, InputOutputCurve) +# assert isinstance(new_curve.function_data, Quadratic) +# assert new_curve.function_data.quadratic_term == 1.0 +# +# new_curve = AverageRateToIncremental(curve) +# assert isinstance(new_curve, IncrementalCurve) +# +# assert isinstance(curve.function_data, LinearFunctionData) +# curve.function_data.proportional_term = 0.0 +# new_curve = AverageRateToInputOutput(curve) +# assert isinstance(new_curve, InputOutputCurve) +# assert isinstance(new_curve.function_data, LinearFunctionData) +# assert new_curve.function_data.proportional_term == 2.0 +# +# # Piecewise step data +# data = PiecewiseStepData(x_coords=[1.0, 3.0, 5.0], y_coords=[2.0, 6.0]) +# curve = AverageRateCurve(function_data=data, initial_input=None) +# with pytest.raises(ISOperationNotAllowed): +# AverageRateToInputOutput(curve) +# +# curve.initial_input = 0.0 +# new_curve = AverageRateToInputOutput(curve) +# assert isinstance(new_curve, InputOutputCurve) +# +# new_curve = AverageRateToIncremental(curve) +# assert isinstance(new_curve, IncrementalCurve) def test_value_curve_custom_serialization(): From 93d27111cdfb20507e0e0db09a102c54402b4cbb Mon Sep 17 00:00:00 2001 From: Jerry Potts Date: Mon, 12 Aug 2024 10:36:11 -0600 Subject: [PATCH 27/37] add conversions to incremental and average rate --- src/infrasys/function_data.py | 21 ++++++ src/infrasys/value_curves.py | 122 ++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) diff --git a/src/infrasys/function_data.py b/src/infrasys/function_data.py index 66fdd2c..723c46f 100644 --- a/src/infrasys/function_data.py +++ b/src/infrasys/function_data.py @@ -182,3 +182,24 @@ def validate_piecewise_xy(self): raise ValueError(msg) 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 diff --git a/src/infrasys/value_curves.py b/src/infrasys/value_curves.py index 7bd0b52..726919d 100644 --- a/src/infrasys/value_curves.py +++ b/src/infrasys/value_curves.py @@ -2,10 +2,14 @@ from typing_extensions import Annotated from infrasys.component import Component +from infrasys.exceptions import ISOperationNotAllowed from infrasys.function_data import ( FunctionData, LinearFunctionData, + QuadraticFunctionData, + PiecewiseLinearData, PiecewiseStepData, + get_slopes, ) from pydantic import Field @@ -92,3 +96,121 @@ class AverageRateCurve(ValueCurve): the origin for functions with no left endpoint, used for conversion to `InputOutputCurve`" ), ] + + +def InputOutputToIncremental(data: InputOutputCurve) -> IncrementalCurve: + """Function to convert InputOutputCurve to IncrementalCurve + + Function takes and InputOutputCurve and converts it to a corresponding + incremental curve depending on the type of function_data. If the :class:`InputOutputCurve` + uses :class:`LinearFunctionData` or :class:`QuadraticFunctionData`, the corresponding + :class:`IncrementalCurve` uses the corresponding derivative for its `function_data`. If + the input uses :class:`PiecewiseLinearData`, the slopes of each segment are calculated and + converted to PiecewiseStepData for the IncrementalCurve. + + Parameters + ---------- + data : InputOutputCurve + Original InputOutputCurve for conversion. + + Returns + ------- + IncrementalCurve + IncrementalCurve using either LinearFunctionData or PiecewiseStepData after conversion. + + Raises + ------ + ISOperationNotAllowed + Function is not valid for the type of data provided. + """ + match data.function_data: + case LinearFunctionData(): + q = 0.0 + p = data.function_data.proportional_term + + return IncrementalCurve( + function_data=LinearFunctionData(proportional_term=q, constant_term=p), + initial_input=data.function_data.constant_term, + input_at_zero=data.input_at_zero, + ) + case QuadraticFunctionData(): + q = data.function_data.quadratic_term + p = data.function_data.proportional_term + + return IncrementalCurve( + function_data=LinearFunctionData(proportional_term=2 * q, constant_term=p), + initial_input=data.function_data.constant_term, + input_at_zero=data.input_at_zero, + ) + case PiecewiseLinearData(): + x = [fd.x for fd in data.function_data.points] + slopes = get_slopes(data.function_data.points) + + return IncrementalCurve( + function_data=PiecewiseStepData(x_coords=x, y_coords=slopes), + initial_input=data.function_data.points[0].y, + input_at_zero=data.input_at_zero, + ) + case _: + msg = "Function is not valid for the type of data provided." + raise ISOperationNotAllowed(msg) + + +def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: + """Function to convert InputOutputCurve to AverageRateCurve + + If the :class:`InputOutputCurve` uses :class:`LinearFunctionData` or + :class:`QuadraticFunctionData`, the corresponding :class:`IncrementalCurve` + uses the :class`LinearFunctionData`, with a slope equal to the + `quadratic_term` (0.0 if originally linear), and a intercept equal to the + `proportional_term`. If the input uses :class:`PiecewiseLinearData`, the + slopes of each segment are calculated and converted to PiecewiseStepData + for the AverageRateCurve. + + Parameters + ---------- + data : InputOutputCurve + Original InputOutputCurve for conversion. + + Returns + ---------- + AverageRateCurve + AverageRateCurve using either LinearFunctionData or PiecewiseStepData after conversion. + + Raises + ------ + ISOperationNotAllowed + Function is not valid for the type of data provided. + """ + match data.function_data: + case LinearFunctionData(): + q = 0.0 + p = data.function_data.proportional_term + + return AverageRateCurve( + function_data=LinearFunctionData(proportional_term=q, constant_term=p), + initial_input=data.function_data.constant_term, + input_at_zero=data.input_at_zero, + ) + case QuadraticFunctionData(): + q = data.function_data.quadratic_term + p = data.function_data.proportional_term + + return AverageRateCurve( + function_data=LinearFunctionData(proportional_term=q, constant_term=p), + initial_input=data.function_data.constant_term, + input_at_zero=data.input_at_zero, + ) + case PiecewiseLinearData(): + # I think I need to add in the getters and stuff from function_data to make this easier + x = [fd.x for fd in data.function_data.points] + slopes_from_origin = [fd.y / fd.x for fd in data.function_data.points[1:]] + + return AverageRateCurve( + function_data=PiecewiseStepData(x_coords=x, y_coords=slopes_from_origin), + initial_input=data.function_data.points[0].y, + input_at_zero=data.input_at_zero, + ) + case _: + msg = "Function is not valid for the type of data provided." + raise ISOperationNotAllowed(msg) From 45658c8086221b73c29b07f9a7b925612f88e0c0 Mon Sep 17 00:00:00 2001 From: Jerry Potts Date: Mon, 12 Aug 2024 10:39:44 -0600 Subject: [PATCH 28/37] re-add function data tests --- tests/test_function_data.py | 36 +++++++++--------------------------- 1 file changed, 9 insertions(+), 27 deletions(-) diff --git a/tests/test_function_data.py b/tests/test_function_data.py index 49ca219..61ab624 100644 --- a/tests/test_function_data.py +++ b/tests/test_function_data.py @@ -3,8 +3,7 @@ PiecewiseStepData, PiecewiseLinearData, XYCoords, - # get_slopes, - # running_sum, + get_slopes, ) from infrasys import Component from .models.simple_system import SimpleSystem @@ -99,28 +98,11 @@ def test_function_data_serialization(tmp_path): assert f1.function_data.constant_term == f2.function_data.constant_term -# def test_slopes_calculation(): -# test_xy = [XYCoords(1.0, 2.0), XYCoords(2.0, 4.0), XYCoords(4.0, 10.0)] -# -# slopes = get_slopes(test_xy) -# -# correct_slopes = [2.0, 3.0] -# -# assert slopes == correct_slopes - - -# def test_running_sum(): -# test_x = [1.0, 3.0, 6.0] -# test_y = [2.0, 4.0] -# -# pws = PiecewiseStepData(x_coords=test_x, y_coords=test_y) -# -# points = running_sum(pws) -# -# x_values = [p.x for p in points] -# y_values = [p.y for p in points] -# -# correct_y_values = [0.0, 4.0, 16.0] -# -# assert x_values == test_x -# assert y_values == correct_y_values +def test_slopes_calculation(): + test_xy = [XYCoords(1.0, 2.0), XYCoords(2.0, 4.0), XYCoords(4.0, 10.0)] + + slopes = get_slopes(test_xy) + + correct_slopes = [2.0, 3.0] + + assert slopes == correct_slopes From 6f5af6796136e8e357701ec4f53c60132083ff0c Mon Sep 17 00:00:00 2001 From: Jerry Potts Date: Mon, 12 Aug 2024 11:40:01 -0600 Subject: [PATCH 29/37] update test functions --- src/infrasys/value_curves.py | 10 +- tests/test_function_data.py | 1 + tests/test_values_curves.py | 177 ++++++++++------------------------- 3 files changed, 51 insertions(+), 137 deletions(-) diff --git a/src/infrasys/value_curves.py b/src/infrasys/value_curves.py index 726919d..63a1a00 100644 --- a/src/infrasys/value_curves.py +++ b/src/infrasys/value_curves.py @@ -2,9 +2,7 @@ from typing_extensions import Annotated from infrasys.component import Component -from infrasys.exceptions import ISOperationNotAllowed from infrasys.function_data import ( - FunctionData, LinearFunctionData, QuadraticFunctionData, PiecewiseLinearData, @@ -37,7 +35,7 @@ class InputOutputCurve(ValueCurve): """ function_data: Annotated[ - FunctionData, + LinearFunctionData | QuadraticFunctionData | PiecewiseLinearData, Field(description="The underlying `FunctionData` representation of this `ValueCurve`"), ] @@ -151,9 +149,6 @@ def InputOutputToIncremental(data: InputOutputCurve) -> IncrementalCurve: initial_input=data.function_data.points[0].y, input_at_zero=data.input_at_zero, ) - case _: - msg = "Function is not valid for the type of data provided." - raise ISOperationNotAllowed(msg) def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: @@ -211,6 +206,3 @@ def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: initial_input=data.function_data.points[0].y, input_at_zero=data.input_at_zero, ) - case _: - msg = "Function is not valid for the type of data provided." - raise ISOperationNotAllowed(msg) diff --git a/tests/test_function_data.py b/tests/test_function_data.py index 61ab624..e566e90 100644 --- a/tests/test_function_data.py +++ b/tests/test_function_data.py @@ -68,6 +68,7 @@ def test_function_data_custom_serialization(): ) model_dump = component.model_dump(mode="json") + print(model_dump) assert model_dump["function_data"]["proportional_term"] == 1.0 model_dump = component.model_dump(context={"magnitude_only": True}) diff --git a/tests/test_values_curves.py b/tests/test_values_curves.py index 5a59cfd..86b4a6f 100644 --- a/tests/test_values_curves.py +++ b/tests/test_values_curves.py @@ -1,17 +1,16 @@ from infrasys.function_data import ( LinearFunctionData, + QuadraticFunctionData, + PiecewiseLinearData, + PiecewiseStepData, + XYCoords, ) from infrasys.value_curves import ( InputOutputCurve, IncrementalCurve, AverageRateCurve, - # InputOutputLinearToQuadratic, - # InputOutputToAverageRate, - # InputOutputToIncremental, - # IncrementalToInputOutput, - # IncrementalToAverageRate, - # AverageRateToInputOutput, - # AverageRateToIncremental, + InputOutputToAverageRate, + InputOutputToIncremental, ) from infrasys import Component from .models.simple_system import SimpleSystem @@ -50,127 +49,46 @@ def test_average_rate_curve(): assert isinstance(curve.function_data, LinearFunctionData) -# def test_input_output_conversion(): -# # LinearFunctionData function data -# curve = InputOutputCurve( -# function_data=LinearFunctionData(proportional_term=1.0, constant_term=1.0) -# ) -# new_curve = InputOutputToAverageRate(curve) -# assert isinstance(new_curve, AverageRateCurve) -# -# new_curve = InputOutputToIncremental(curve) -# assert isinstance(new_curve, IncrementalCurve) -# -# new_curve = InputOutputLinearToQuadratic(curve) -# assert isinstance(new_curve.function_data, Quadratic) -# -# # Quadratic function data -# q = 3.0 -# p = 2.0 -# c = 1.0 -# -# curve = InputOutputCurve( -# function_data=Quadratic(quadratic_term=q, proportional_term=p, constant_term=c) -# ) -# new_curve = InputOutputToAverageRate(curve) -# assert isinstance(new_curve, AverageRateCurve) -# assert isinstance(new_curve.function_data, LinearFunctionData) -# assert new_curve.function_data.proportional_term == q -# -# new_curve = InputOutputToIncremental(curve) -# assert isinstance(new_curve, IncrementalCurve) -# assert isinstance(new_curve.function_data, LinearFunctionData) -# assert new_curve.function_data.proportional_term == 2 * q -# -# # Piecewise linear data -# xy = [XYCoords(1.0, 2.0), XYCoords(2.0, 4.0), XYCoords(4.0, 10.0)] -# -# curve = InputOutputCurve(function_data=PiecewiseLinearData(points=xy)) -# new_curve = InputOutputToAverageRate(curve) -# assert isinstance(new_curve, AverageRateCurve) -# assert isinstance(new_curve.function_data, PiecewiseStepData) -# assert new_curve.function_data.y_coords == [2.0, 2.5] -# -# new_curve = InputOutputToIncremental(curve) -# assert isinstance(new_curve, IncrementalCurve) - - -# def test_incremental_conversion(): -# # LinearFunctionData function data -# curve = IncrementalCurve( -# function_data=LinearFunctionData(proportional_term=1.0, constant_term=1.0), -# initial_input=None, -# ) -# assert isinstance(curve.function_data, LinearFunctionData) -# with pytest.raises(ISOperationNotAllowed): -# IncrementalToInputOutput(curve) -# -# curve.initial_input = 0.0 -# new_curve = IncrementalToInputOutput(curve) -# assert isinstance(new_curve, InputOutputCurve) -# assert isinstance(new_curve.function_data, Quadratic) -# assert new_curve.function_data.quadratic_term == 0.5 -# -# new_curve = IncrementalToAverageRate(curve) -# assert isinstance(new_curve, AverageRateCurve) -# -# curve.function_data.proportional_term = 0.0 -# new_curve = IncrementalToInputOutput(curve) -# assert isinstance(new_curve, InputOutputCurve) -# assert isinstance(new_curve.function_data, LinearFunctionData) -# assert new_curve.function_data.proportional_term == 1.0 -# -# # Piecewise step data -# data = PiecewiseStepData(x_coords=[1.0, 3.0, 5.0], y_coords=[2.0, 6.0]) -# curve = IncrementalCurve(function_data=data, initial_input=None) -# with pytest.raises(ISOperationNotAllowed): -# IncrementalToInputOutput(curve) -# -# curve.initial_input = 0.0 -# new_curve = IncrementalToInputOutput(curve) -# assert isinstance(new_curve, InputOutputCurve) -# -# new_curve = IncrementalToAverageRate(curve) -# assert isinstance(new_curve, AverageRateCurve) - - -# def test_average_rate_conversion(): -# # LinearFunctionData function data -# curve = AverageRateCurve( -# function_data=LinearFunctionData(proportional_term=1.0, constant_term=2.0), -# initial_input=None, -# ) -# with pytest.raises(ISOperationNotAllowed): -# AverageRateToInputOutput(curve) -# -# curve.initial_input = 0.0 -# new_curve = AverageRateToInputOutput(curve) -# assert isinstance(new_curve, InputOutputCurve) -# assert isinstance(new_curve.function_data, Quadratic) -# assert new_curve.function_data.quadratic_term == 1.0 -# -# new_curve = AverageRateToIncremental(curve) -# assert isinstance(new_curve, IncrementalCurve) -# -# assert isinstance(curve.function_data, LinearFunctionData) -# curve.function_data.proportional_term = 0.0 -# new_curve = AverageRateToInputOutput(curve) -# assert isinstance(new_curve, InputOutputCurve) -# assert isinstance(new_curve.function_data, LinearFunctionData) -# assert new_curve.function_data.proportional_term == 2.0 -# -# # Piecewise step data -# data = PiecewiseStepData(x_coords=[1.0, 3.0, 5.0], y_coords=[2.0, 6.0]) -# curve = AverageRateCurve(function_data=data, initial_input=None) -# with pytest.raises(ISOperationNotAllowed): -# AverageRateToInputOutput(curve) -# -# curve.initial_input = 0.0 -# new_curve = AverageRateToInputOutput(curve) -# assert isinstance(new_curve, InputOutputCurve) -# -# new_curve = AverageRateToIncremental(curve) -# assert isinstance(new_curve, IncrementalCurve) +def test_input_output_conversion(): + # LinearFunctionData function data + curve = InputOutputCurve( + function_data=LinearFunctionData(proportional_term=1.0, constant_term=1.0) + ) + new_curve = InputOutputToAverageRate(curve) + assert isinstance(new_curve, AverageRateCurve) + + new_curve = InputOutputToIncremental(curve) + assert isinstance(new_curve, IncrementalCurve) + + # Quadratic function data + q = 3.0 + p = 2.0 + c = 1.0 + + curve = InputOutputCurve( + function_data=QuadraticFunctionData(quadratic_term=q, proportional_term=p, constant_term=c) + ) + new_curve = InputOutputToAverageRate(curve) + assert isinstance(new_curve, AverageRateCurve) + assert isinstance(new_curve.function_data, LinearFunctionData) + assert new_curve.function_data.proportional_term == q + + new_curve = InputOutputToIncremental(curve) + assert isinstance(new_curve, IncrementalCurve) + assert isinstance(new_curve.function_data, LinearFunctionData) + assert new_curve.function_data.proportional_term == 2 * q + + # Piecewise linear data + xy = [XYCoords(1.0, 2.0), XYCoords(2.0, 4.0), XYCoords(4.0, 10.0)] + + curve = InputOutputCurve(function_data=PiecewiseLinearData(points=xy)) + new_curve = InputOutputToAverageRate(curve) + assert isinstance(new_curve, AverageRateCurve) + assert isinstance(new_curve.function_data, PiecewiseStepData) + assert new_curve.function_data.y_coords == [2.0, 2.5] + + new_curve = InputOutputToIncremental(curve) + assert isinstance(new_curve, IncrementalCurve) def test_value_curve_custom_serialization(): @@ -182,12 +100,15 @@ def test_value_curve_custom_serialization(): ) model_dump = component.model_dump(mode="json") + print(model_dump) assert model_dump["value_curve"]["function_data"]["proportional_term"] == 1.0 model_dump = component.model_dump(context={"magnitude_only": True}) + # print(model_dump) assert model_dump["value_curve"]["function_data"]["proportional_term"] == 1.0 model_dump = component.model_dump(mode="json", context={"magnitude_only": True}) + # print(model_dump) assert model_dump["value_curve"]["function_data"]["proportional_term"] == 1.0 From c8a10ee2e36b2f02423695a175e912f7efd4f66f Mon Sep 17 00:00:00 2001 From: Jerry Potts Date: Mon, 12 Aug 2024 14:47:56 -0600 Subject: [PATCH 30/37] initial class definitions for cost and fuel curves --- .../production_variable_cost_curve.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/infrasys/production_variable_cost_curve.py diff --git a/src/infrasys/production_variable_cost_curve.py b/src/infrasys/production_variable_cost_curve.py new file mode 100644 index 0000000..70efdd3 --- /dev/null +++ b/src/infrasys/production_variable_cost_curve.py @@ -0,0 +1,37 @@ +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)] = "" + value_curve: Annotated[ + InputOutputCurve | IncrementalCurve | AverageRateCurve, + Field( + description="The underlying `ValueCurve` representation of this `ProductionVariableCostCurve`" + ), + ] + # not float change this later, should be UnitSystem but not sure if thats in infrasys? + power_units: Annotated[ + float, + Field(description="(default: natural units (MW)) The units for the x-axis of the curve"), + ] + vom_units: Annotated[ + InputOutputCurve, + Field(description="(default: natural units (MW)) The units for the x-axis of the curve"), + ] = InputOutputCurve(LinearFunctionData(0.0)) + + +class CostCurve(ProductionVariableCostCurve): + x = 1 + + +class FuelCurve(ProductionVariableCostCurve): + fuel_cost: Annotated[ + float, + Field( + description="Either a fixed value for fuel cost or the key to a fuel cost time series" + ), + ] From a01c0a27070466cc2da6d54550841ac73ec63396 Mon Sep 17 00:00:00 2001 From: Jerry Potts Date: Tue, 13 Aug 2024 09:25:03 -0600 Subject: [PATCH 31/37] change conversions to convert everything to InputOutputCurve --- src/infrasys/function_data.py | 36 +++--- src/infrasys/value_curves.py | 227 +++++++++++++++++----------------- 2 files changed, 132 insertions(+), 131 deletions(-) diff --git a/src/infrasys/function_data.py b/src/infrasys/function_data.py index 723c46f..182f19b 100644 --- a/src/infrasys/function_data.py +++ b/src/infrasys/function_data.py @@ -184,22 +184,20 @@ def validate_piecewise_xy(self): 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]: + 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 diff --git a/src/infrasys/value_curves.py b/src/infrasys/value_curves.py index 63a1a00..183a1cc 100644 --- a/src/infrasys/value_curves.py +++ b/src/infrasys/value_curves.py @@ -2,14 +2,16 @@ from typing_extensions import Annotated from infrasys.component import Component +from infrasys.exceptions import ISOperationNotAllowed from infrasys.function_data import ( LinearFunctionData, QuadraticFunctionData, PiecewiseLinearData, PiecewiseStepData, - get_slopes, + running_sum, ) from pydantic import Field +import numpy as np class ValueCurve(Component): @@ -65,6 +67,60 @@ class IncrementalCurve(ValueCurve): ), ] + def to_input_output(self) -> InputOutputCurve: + """Function to convert IncrementalCurve to InputOutputCurve + + Function takes an IncrementalCurve and converts it to a corresponding + InputOutputCurve depending on the type of function_data. If the IncrementalCurve + uses LinearFunctionData, the new InputOutputCurve is created linear or quadratic data + that correspond to the integral of the original linear function. If the input uses + PiecewiseStepData, the slopes of each segment are used to calculate the corresponding + y values for each x value and used to construct PiecewiseLinearData for the InputOutputCurve. + + Parameters + ---------- + data : IncrementalCurve + Original IncrementalCurve for conversion. + + Returns + ---------- + InputOutputCurve + InputOutputCurve using either QuadraticFunctionData or PiecewiseStepData. + """ + if isinstance(self.function_data, LinearFunctionData): + p = self.function_data.proportional_term + m = self.function_data.constant_term + + c = self.initial_input + if c is None: + msg = "Cannot convert `IncrementalCurve` with undefined `initial_input`" + raise ISOperationNotAllowed(msg) + + if p == 0: + return InputOutputCurve( + function_data=LinearFunctionData(proportional_term=m, constant_term=c) + ) + else: + return InputOutputCurve( + function_data=QuadraticFunctionData( + quadratic_term=p / 2, proportional_term=m, constant_term=c + ), + input_at_zero=self.input_at_zero, + ) + + elif isinstance(self.function_data, PiecewiseStepData): + c = self.initial_input + if c is None: + msg = "Cannot convert `IncrementalCurve` with undefined `initial_input`" + raise ISOperationNotAllowed(msg) + + points = running_sum(self.function_data) + + return InputOutputCurve( + function_data=PiecewiseLinearData(points=[(p.x, p.y + c) for p in points]), + input_at_zero=self.input_at_zero, + ) + class AverageRateCurve(ValueCurve): """Average rate curve relating production quality to average cost rate. @@ -95,114 +151,61 @@ class AverageRateCurve(ValueCurve): ), ] - -def InputOutputToIncremental(data: InputOutputCurve) -> IncrementalCurve: - """Function to convert InputOutputCurve to IncrementalCurve - - Function takes and InputOutputCurve and converts it to a corresponding - incremental curve depending on the type of function_data. If the :class:`InputOutputCurve` - uses :class:`LinearFunctionData` or :class:`QuadraticFunctionData`, the corresponding - :class:`IncrementalCurve` uses the corresponding derivative for its `function_data`. If - the input uses :class:`PiecewiseLinearData`, the slopes of each segment are calculated and - converted to PiecewiseStepData for the IncrementalCurve. - - Parameters - ---------- - data : InputOutputCurve - Original InputOutputCurve for conversion. - - Returns - ------- - IncrementalCurve - IncrementalCurve using either LinearFunctionData or PiecewiseStepData after conversion. - - Raises - ------ - ISOperationNotAllowed - Function is not valid for the type of data provided. - """ - match data.function_data: - case LinearFunctionData(): - q = 0.0 - p = data.function_data.proportional_term - - return IncrementalCurve( - function_data=LinearFunctionData(proportional_term=q, constant_term=p), - initial_input=data.function_data.constant_term, - input_at_zero=data.input_at_zero, - ) - case QuadraticFunctionData(): - q = data.function_data.quadratic_term - p = data.function_data.proportional_term - - return IncrementalCurve( - function_data=LinearFunctionData(proportional_term=2 * q, constant_term=p), - initial_input=data.function_data.constant_term, - input_at_zero=data.input_at_zero, - ) - case PiecewiseLinearData(): - x = [fd.x for fd in data.function_data.points] - slopes = get_slopes(data.function_data.points) - - return IncrementalCurve( - function_data=PiecewiseStepData(x_coords=x, y_coords=slopes), - initial_input=data.function_data.points[0].y, - input_at_zero=data.input_at_zero, - ) - - -def InputOutputToAverageRate(data: InputOutputCurve) -> AverageRateCurve: - """Function to convert InputOutputCurve to AverageRateCurve - - If the :class:`InputOutputCurve` uses :class:`LinearFunctionData` or - :class:`QuadraticFunctionData`, the corresponding :class:`IncrementalCurve` - uses the :class`LinearFunctionData`, with a slope equal to the - `quadratic_term` (0.0 if originally linear), and a intercept equal to the - `proportional_term`. If the input uses :class:`PiecewiseLinearData`, the - slopes of each segment are calculated and converted to PiecewiseStepData - for the AverageRateCurve. - - Parameters - ---------- - data : InputOutputCurve - Original InputOutputCurve for conversion. - - Returns - ---------- - AverageRateCurve - AverageRateCurve using either LinearFunctionData or PiecewiseStepData after conversion. - - Raises - ------ - ISOperationNotAllowed - Function is not valid for the type of data provided. - """ - match data.function_data: - case LinearFunctionData(): - q = 0.0 - p = data.function_data.proportional_term - - return AverageRateCurve( - function_data=LinearFunctionData(proportional_term=q, constant_term=p), - initial_input=data.function_data.constant_term, - input_at_zero=data.input_at_zero, - ) - case QuadraticFunctionData(): - q = data.function_data.quadratic_term - p = data.function_data.proportional_term - - return AverageRateCurve( - function_data=LinearFunctionData(proportional_term=q, constant_term=p), - initial_input=data.function_data.constant_term, - input_at_zero=data.input_at_zero, - ) - case PiecewiseLinearData(): - # I think I need to add in the getters and stuff from function_data to make this easier - x = [fd.x for fd in data.function_data.points] - slopes_from_origin = [fd.y / fd.x for fd in data.function_data.points[1:]] - - return AverageRateCurve( - function_data=PiecewiseStepData(x_coords=x, y_coords=slopes_from_origin), - initial_input=data.function_data.points[0].y, - input_at_zero=data.input_at_zero, - ) + def to_input_output(self) -> InputOutputCurve: + """Function to convert IncrementalCurve to InputOutputCurve + + Function takes an AverageRateCurve and converts it to a corresponding + InputOutputCurve depending on the type of function_data. If the AverageRateCurve + uses LinearFunctionData, the new InputOutputCurve is created with either linear or quadratic + function data, depending on if the original function data is constant or linear. If the + input uses PiecewiseStepData, new y-values are calculated for each x value such that `f(x) = x*y` + and used to construct PiecewiseLinearData for the InputOutputCurve. + + Parameters + ---------- + data : AverageRateCurve + Original AverageRateCurve for conversion. + + Returns + ---------- + InputOutputCurve + InputOutputCurve using either QuadraticFunctionData or PiecewiseStepData. + """ + match self.function_data: + case LinearFunctionData(): + p = self.function_data.proportional_term + m = self.function_data.constant_term + + c = self.initial_input + if c is None: + msg = "Cannot convert `AverageRateCurve` with undefined `initial_input`" + raise ISOperationNotAllowed(msg) + + if p == 0: + return InputOutputCurve( + function_data=LinearFunctionData(proportional_term=m, constant_term=c) + ) + else: + return InputOutputCurve( + function_data=QuadraticFunctionData( + quadratic_term=p, proportional_term=m, constant_term=c + ), + input_at_zero=self.input_at_zero, + ) + case PiecewiseStepData(): + c = self.initial_input + if c is None: + msg = "Cannot convert `AverageRateCurve` with undefined `initial_input`" + raise ISOperationNotAllowed(msg) + + xs = self.function_data.x_coords + ys = np.multiply(xs[1:], self.function_data.y_coords).tolist() + ys.insert(0, c) + + return InputOutputCurve( + function_data=PiecewiseLinearData(points=list(zip(xs, ys))), + input_at_zero=self.input_at_zero, + ) + case _: + msg = "Function is not valid for the type of data provided." + raise ISOperationNotAllowed(msg) From 78fc1b1e1dab2e77a195a6810696b61f7ec31f2e Mon Sep 17 00:00:00 2001 From: Jerry Potts Date: Tue, 13 Aug 2024 09:35:32 -0600 Subject: [PATCH 32/37] update test functions for correct ValueCurve conversions --- tests/test_function_data.py | 19 +++++--- tests/test_values_curves.py | 94 ++++++++++++++++++++++--------------- 2 files changed, 69 insertions(+), 44 deletions(-) diff --git a/tests/test_function_data.py b/tests/test_function_data.py index e566e90..57e683c 100644 --- a/tests/test_function_data.py +++ b/tests/test_function_data.py @@ -3,7 +3,7 @@ PiecewiseStepData, PiecewiseLinearData, XYCoords, - get_slopes, + running_sum, ) from infrasys import Component from .models.simple_system import SimpleSystem @@ -99,11 +99,18 @@ def test_function_data_serialization(tmp_path): assert f1.function_data.constant_term == f2.function_data.constant_term -def test_slopes_calculation(): - test_xy = [XYCoords(1.0, 2.0), XYCoords(2.0, 4.0), XYCoords(4.0, 10.0)] +def test_running_sum(): + test_x = [1.0, 3.0, 6.0] + test_y = [2.0, 4.0] + + pws = PiecewiseStepData(x_coords=test_x, y_coords=test_y) + + points = running_sum(pws) - slopes = get_slopes(test_xy) + x_values = [p.x for p in points] + y_values = [p.y for p in points] - correct_slopes = [2.0, 3.0] + correct_y_values = [0.0, 4.0, 16.0] - assert slopes == correct_slopes + assert x_values == test_x + assert y_values == correct_y_values diff --git a/tests/test_values_curves.py b/tests/test_values_curves.py index 86b4a6f..3a57b0f 100644 --- a/tests/test_values_curves.py +++ b/tests/test_values_curves.py @@ -1,19 +1,17 @@ from infrasys.function_data import ( LinearFunctionData, QuadraticFunctionData, - PiecewiseLinearData, PiecewiseStepData, - XYCoords, ) from infrasys.value_curves import ( InputOutputCurve, IncrementalCurve, AverageRateCurve, - InputOutputToAverageRate, - InputOutputToIncremental, ) from infrasys import Component +from infrasys.exceptions import ISOperationNotAllowed from .models.simple_system import SimpleSystem +import pytest class ValueCurveComponent(Component): @@ -49,46 +47,66 @@ def test_average_rate_curve(): assert isinstance(curve.function_data, LinearFunctionData) -def test_input_output_conversion(): - # LinearFunctionData function data - curve = InputOutputCurve( - function_data=LinearFunctionData(proportional_term=1.0, constant_term=1.0) - ) - new_curve = InputOutputToAverageRate(curve) - assert isinstance(new_curve, AverageRateCurve) - - new_curve = InputOutputToIncremental(curve) - assert isinstance(new_curve, IncrementalCurve) - - # Quadratic function data - q = 3.0 - p = 2.0 - c = 1.0 - - curve = InputOutputCurve( - function_data=QuadraticFunctionData(quadratic_term=q, proportional_term=p, constant_term=c) +def test_average_rate_conversion(): + # Linear function data + curve = AverageRateCurve( + function_data=LinearFunctionData(proportional_term=1.0, constant_term=2.0), + initial_input=None, ) - new_curve = InputOutputToAverageRate(curve) - assert isinstance(new_curve, AverageRateCurve) + with pytest.raises(ISOperationNotAllowed): + curve.to_input_output() + + curve.initial_input = 0.0 + new_curve = curve.to_input_output() + assert isinstance(new_curve, InputOutputCurve) + assert isinstance(new_curve.function_data, QuadraticFunctionData) + assert new_curve.function_data.quadratic_term == 1.0 + + curve.function_data.proportional_term = 0.0 + new_curve = curve.to_input_output() + assert isinstance(new_curve, InputOutputCurve) assert isinstance(new_curve.function_data, LinearFunctionData) - assert new_curve.function_data.proportional_term == q + assert new_curve.function_data.proportional_term == 2.0 - new_curve = InputOutputToIncremental(curve) - assert isinstance(new_curve, IncrementalCurve) - assert isinstance(new_curve.function_data, LinearFunctionData) - assert new_curve.function_data.proportional_term == 2 * q + # Piecewise step data + data = PiecewiseStepData(x_coords=[1.0, 3.0, 5.0], y_coords=[2.0, 6.0]) + curve = AverageRateCurve(function_data=data, initial_input=None) + with pytest.raises(ISOperationNotAllowed): + curve.to_input_output() - # Piecewise linear data - xy = [XYCoords(1.0, 2.0), XYCoords(2.0, 4.0), XYCoords(4.0, 10.0)] + curve.initial_input = 0.0 + new_curve = curve.to_input_output() + assert isinstance(new_curve, InputOutputCurve) - curve = InputOutputCurve(function_data=PiecewiseLinearData(points=xy)) - new_curve = InputOutputToAverageRate(curve) - assert isinstance(new_curve, AverageRateCurve) - assert isinstance(new_curve.function_data, PiecewiseStepData) - assert new_curve.function_data.y_coords == [2.0, 2.5] - new_curve = InputOutputToIncremental(curve) - assert isinstance(new_curve, IncrementalCurve) +def test_incremental_conversion(): + # Linear function data + curve = IncrementalCurve( + function_data=LinearFunctionData(proportional_term=1.0, constant_term=1.0), + initial_input=None, + ) + with pytest.raises(ISOperationNotAllowed): + curve.to_input_output() + curve.initial_input = 0.0 + new_curve = curve.to_input_output() + assert isinstance(new_curve, InputOutputCurve) + assert isinstance(new_curve.function_data, QuadraticFunctionData) + assert new_curve.function_data.quadratic_term == 0.5 + + curve.function_data.proportional_term = 0.0 + new_curve = curve.to_input_output() + assert isinstance(new_curve, InputOutputCurve) + assert isinstance(new_curve.function_data, LinearFunctionData) + assert new_curve.function_data.proportional_term == 1.0 + + # Piecewise step data + data = PiecewiseStepData(x_coords=[1.0, 3.0, 5.0], y_coords=[2.0, 6.0]) + curve = IncrementalCurve(function_data=data, initial_input=None) + with pytest.raises(ISOperationNotAllowed): + curve.to_input_output() + curve.initial_input = 0.0 + new_curve = curve.to_input_output() + assert isinstance(new_curve, InputOutputCurve) def test_value_curve_custom_serialization(): From 6a184d9fdb98deb85dc83d481ce374bedec0779e Mon Sep 17 00:00:00 2001 From: Jerry Potts Date: Tue, 13 Aug 2024 09:52:41 -0600 Subject: [PATCH 33/37] add docstrings to CostCurve and FuelCurve --- .../production_variable_cost_curve.py | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/infrasys/production_variable_cost_curve.py b/src/infrasys/production_variable_cost_curve.py index 70efdd3..43d589d 100644 --- a/src/infrasys/production_variable_cost_curve.py +++ b/src/infrasys/production_variable_cost_curve.py @@ -7,28 +7,44 @@ 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`" ), ] - # not float change this later, should be UnitSystem but not sure if thats in infrasys? - power_units: Annotated[ - float, - Field(description="(default: natural units (MW)) The units for the x-axis of the curve"), - ] vom_units: Annotated[ InputOutputCurve, Field(description="(default: natural units (MW)) The units for the x-axis of the curve"), ] = InputOutputCurve(LinearFunctionData(0.0)) -class CostCurve(ProductionVariableCostCurve): - x = 1 - - class FuelCurve(ProductionVariableCostCurve): + """Representation of the variable operation cost of a power plant in terms of fuel (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(LinearFunctionData(0.0)) fuel_cost: Annotated[ float, Field( From 8d4f0814152b99de99d2776038f4883e7113dc1a Mon Sep 17 00:00:00 2001 From: Jerry Potts Date: Tue, 13 Aug 2024 10:15:32 -0600 Subject: [PATCH 34/37] add test functions for CostCurve and FuelCurve --- .../production_variable_cost_curve.py | 8 +- tests/test_production_variable_cost_curve.py | 100 ++++++++++++++++++ tests/test_values_curves.py | 3 - 3 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 tests/test_production_variable_cost_curve.py diff --git a/src/infrasys/production_variable_cost_curve.py b/src/infrasys/production_variable_cost_curve.py index 43d589d..ffb6438 100644 --- a/src/infrasys/production_variable_cost_curve.py +++ b/src/infrasys/production_variable_cost_curve.py @@ -25,7 +25,9 @@ class CostCurve(ProductionVariableCostCurve): vom_units: Annotated[ InputOutputCurve, Field(description="(default: natural units (MW)) The units for the x-axis of the curve"), - ] = InputOutputCurve(LinearFunctionData(0.0)) + ] = InputOutputCurve( + function_data=LinearFunctionData(proportional_term=0.0, constant_term=0.0) + ) class FuelCurve(ProductionVariableCostCurve): @@ -44,7 +46,9 @@ class FuelCurve(ProductionVariableCostCurve): vom_units: Annotated[ InputOutputCurve, Field(description="(default: natural units (MW)) The units for the x-axis of the curve"), - ] = InputOutputCurve(LinearFunctionData(0.0)) + ] = InputOutputCurve( + function_data=LinearFunctionData(proportional_term=0.0, constant_term=0.0) + ) fuel_cost: Annotated[ float, Field( diff --git a/tests/test_production_variable_cost_curve.py b/tests/test_production_variable_cost_curve.py new file mode 100644 index 0000000..abbbb55 --- /dev/null +++ b/tests/test_production_variable_cost_curve.py @@ -0,0 +1,100 @@ +from infrasys.production_variable_cost_curve import CostCurve, FuelCurve +from infrasys.function_data import LinearFunctionData +from infrasys.value_curves import InputOutputCurve +from infrasys import Component +from .models.simple_system import SimpleSystem + + +class CurveComponent(Component): + cost_curve: CostCurve + + +def test_cost_curve(): + # Cost curve + cost_curve = CostCurve( + value_curve=InputOutputCurve( + function_data=LinearFunctionData(proportional_term=1.0, constant_term=2.0) + ), + vom_units=InputOutputCurve( + function_data=LinearFunctionData(proportional_term=2.0, constant_term=1.0) + ), + ) + + assert cost_curve.value_curve.function_data.proportional_term == 1.0 + assert cost_curve.vom_units.function_data.proportional_term == 2.0 + + +def test_fuel_curve(): + # Fuel curve + fuel_curve = FuelCurve( + value_curve=InputOutputCurve( + function_data=LinearFunctionData(proportional_term=1.0, constant_term=2.0) + ), + vom_units=InputOutputCurve( + function_data=LinearFunctionData(proportional_term=2.0, constant_term=1.0) + ), + fuel_cost=2.5, + ) + + assert fuel_curve.value_curve.function_data.proportional_term == 1.0 + assert fuel_curve.vom_units.function_data.proportional_term == 2.0 + assert fuel_curve.fuel_cost == 2.5 + + +def test_value_curve_custom_serialization(): + component = CurveComponent( + name="test", + cost_curve=CostCurve( + value_curve=InputOutputCurve( + function_data=LinearFunctionData(proportional_term=1.0, constant_term=2.0) + ), + vom_units=InputOutputCurve( + function_data=LinearFunctionData(proportional_term=2.0, constant_term=1.0) + ), + ), + ) + + model_dump = component.model_dump(mode="json") + assert model_dump["cost_curve"]["value_curve"]["function_data"]["proportional_term"] == 1.0 + + model_dump = component.model_dump(context={"magnitude_only": True}) + assert model_dump["cost_curve"]["value_curve"]["function_data"]["proportional_term"] == 1.0 + + model_dump = component.model_dump(mode="json", context={"magnitude_only": True}) + assert model_dump["cost_curve"]["value_curve"]["function_data"]["proportional_term"] == 1.0 + + +def test_value_curve_serialization(tmp_path): + system = SimpleSystem(auto_add_composed_components=True) + + v1 = CurveComponent( + name="test", + cost_curve=CostCurve( + value_curve=InputOutputCurve( + function_data=LinearFunctionData(proportional_term=1.0, constant_term=2.0) + ), + vom_units=InputOutputCurve( + function_data=LinearFunctionData(proportional_term=2.0, constant_term=1.0) + ), + ), + ) + system.add_component(v1) + filename = tmp_path / "value_curve.json" + + system.to_json(filename, overwrite=True) + system2 = SimpleSystem.from_json(filename) + + assert system2 is not None + + v2 = system2.get_component(CurveComponent, "test") + + assert v2 is not None + assert isinstance(v1.cost_curve.value_curve.function_data, LinearFunctionData) + assert ( + v1.cost_curve.value_curve.function_data.proportional_term + == v2.cost_curve.value_curve.function_data.proportional_term + ) + assert ( + v1.cost_curve.value_curve.function_data.constant_term + == v2.cost_curve.value_curve.function_data.constant_term + ) diff --git a/tests/test_values_curves.py b/tests/test_values_curves.py index 3a57b0f..ca668c1 100644 --- a/tests/test_values_curves.py +++ b/tests/test_values_curves.py @@ -118,15 +118,12 @@ def test_value_curve_custom_serialization(): ) model_dump = component.model_dump(mode="json") - print(model_dump) assert model_dump["value_curve"]["function_data"]["proportional_term"] == 1.0 model_dump = component.model_dump(context={"magnitude_only": True}) - # print(model_dump) assert model_dump["value_curve"]["function_data"]["proportional_term"] == 1.0 model_dump = component.model_dump(mode="json", context={"magnitude_only": True}) - # print(model_dump) assert model_dump["value_curve"]["function_data"]["proportional_term"] == 1.0 From b8d88d5a41a74e4105a968a51064f5136780b1c5 Mon Sep 17 00:00:00 2001 From: Jerry Potts Date: Wed, 14 Aug 2024 15:30:08 -0600 Subject: [PATCH 35/37] update file names --- .../{production_variable_cost_curve.py => cost_curves.py} | 0 ...st_production_variable_cost_curve.py => test_cost_curves.py} | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/infrasys/{production_variable_cost_curve.py => cost_curves.py} (100%) rename tests/{test_production_variable_cost_curve.py => test_cost_curves.py} (97%) diff --git a/src/infrasys/production_variable_cost_curve.py b/src/infrasys/cost_curves.py similarity index 100% rename from src/infrasys/production_variable_cost_curve.py rename to src/infrasys/cost_curves.py diff --git a/tests/test_production_variable_cost_curve.py b/tests/test_cost_curves.py similarity index 97% rename from tests/test_production_variable_cost_curve.py rename to tests/test_cost_curves.py index abbbb55..dae6feb 100644 --- a/tests/test_production_variable_cost_curve.py +++ b/tests/test_cost_curves.py @@ -1,4 +1,4 @@ -from infrasys.production_variable_cost_curve import CostCurve, FuelCurve +from infrasys.cost_curves import CostCurve, FuelCurve from infrasys.function_data import LinearFunctionData from infrasys.value_curves import InputOutputCurve from infrasys import Component From 8f9d4e1a5bd4ce281b3d1510c1c5ddfac0e7c29c Mon Sep 17 00:00:00 2001 From: Jerry Potts Date: Wed, 14 Aug 2024 15:35:41 -0600 Subject: [PATCH 36/37] numpy convention for class docstrings --- src/infrasys/cost_curves.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/infrasys/cost_curves.py b/src/infrasys/cost_curves.py index ffb6438..4f4b7f9 100644 --- a/src/infrasys/cost_curves.py +++ b/src/infrasys/cost_curves.py @@ -10,8 +10,9 @@ class ProductionVariableCostCurve(Component): 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 + """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`. """ @@ -31,9 +32,10 @@ class CostCurve(ProductionVariableCostCurve): class FuelCurve(ProductionVariableCostCurve): - """Representation of the variable operation cost of a power plant in terms of fuel (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. + """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`. """ From fe6431833acd0c29481d1170978b166e4cc5ae7f Mon Sep 17 00:00:00 2001 From: Jerry Potts Date: Wed, 14 Aug 2024 15:46:28 -0600 Subject: [PATCH 37/37] change if statement to match case --- src/infrasys/value_curves.py | 56 ++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/infrasys/value_curves.py b/src/infrasys/value_curves.py index 183a1cc..96d1912 100644 --- a/src/infrasys/value_curves.py +++ b/src/infrasys/value_curves.py @@ -87,40 +87,40 @@ def to_input_output(self) -> InputOutputCurve: InputOutputCurve InputOutputCurve using either QuadraticFunctionData or PiecewiseStepData. """ - if isinstance(self.function_data, LinearFunctionData): - p = self.function_data.proportional_term - m = self.function_data.constant_term + match self.function_data: + case LinearFunctionData(): + p = self.function_data.proportional_term + m = self.function_data.constant_term - c = self.initial_input - if c is None: - msg = "Cannot convert `IncrementalCurve` with undefined `initial_input`" - raise ISOperationNotAllowed(msg) + c = self.initial_input + if c is None: + msg = "Cannot convert `IncrementalCurve` with undefined `initial_input`" + raise ISOperationNotAllowed(msg) + + if p == 0: + return InputOutputCurve( + function_data=LinearFunctionData(proportional_term=m, constant_term=c) + ) + else: + return InputOutputCurve( + function_data=QuadraticFunctionData( + quadratic_term=p / 2, proportional_term=m, constant_term=c + ), + input_at_zero=self.input_at_zero, + ) + case PiecewiseStepData(): + c = self.initial_input + if c is None: + msg = "Cannot convert `IncrementalCurve` with undefined `initial_input`" + raise ISOperationNotAllowed(msg) + + points = running_sum(self.function_data) - if p == 0: - return InputOutputCurve( - function_data=LinearFunctionData(proportional_term=m, constant_term=c) - ) - else: return InputOutputCurve( - function_data=QuadraticFunctionData( - quadratic_term=p / 2, proportional_term=m, constant_term=c - ), + function_data=PiecewiseLinearData(points=[(p.x, p.y + c) for p in points]), input_at_zero=self.input_at_zero, ) - elif isinstance(self.function_data, PiecewiseStepData): - c = self.initial_input - if c is None: - msg = "Cannot convert `IncrementalCurve` with undefined `initial_input`" - raise ISOperationNotAllowed(msg) - - points = running_sum(self.function_data) - - return InputOutputCurve( - function_data=PiecewiseLinearData(points=[(p.x, p.y + c) for p in points]), - input_at_zero=self.input_at_zero, - ) - class AverageRateCurve(ValueCurve): """Average rate curve relating production quality to average cost rate.