Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
245 changes: 72 additions & 173 deletions HydroGenerate/flow_preprocessing.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,3 @@
'''
Copyright 2023, Battelle Energy Alliance, LLC
'''

"""
07-2024
@author: Soumyadeep Nag, Camilo J. Bastidas Pacheco, J. Gallego-Calderon

This module applies the flow limit constraints and incorporates the impact
of annual and major maintenance activities on turbine flow.

It provides utilities to:
1) Cap turbine flow at design limits.
2) Enforce minimum turbine flow thresholds (absolute or % of design flow).
3) Set turbine flow to zero during annual and major maintenance windows.

Expected `flow_obj` interface (used by functions here)
-----------------------------------------------------
Attributes
----------
flow : numpy.ndarray
Raw inflow time series (m³/s), same length as `datetime_index`.
design_flow : float or numpy.ndarray
Turbine design flow (m³/s). If array, must align with `flow`.
turbine_flow : numpy.ndarray
Turbine flow (m³/s); created/updated by the functions in this module.
datetime_index : pandas.DatetimeIndex
Timestamps for `flow`. Weekly grouping assumes this index (or a level)
is named "dateTime" when using pandas.Grouper(level="dateTime", ...).

Notes
-----
- Annual maintenance zeros 7 consecutive days once per year, selecting the week
with the lowest weekly mean flow.
- Major maintenance zeros 14 consecutive days every 5 years (skipping the first),
selecting the two-week period with the lowest bi-weekly mean flow.
"""


import numpy as np
import pandas as pd
from datetime import datetime
Expand All @@ -45,37 +6,35 @@
# Hydraulic design parameters
class FlowPreProcessingParameters:

"""
Container for flow pre-processing options used by flow limit and
maintenance logic.
"""Configuration container for flow preprocessing.

Parameters
----------
minimum_turbineflow : float or None
Absolute minimum turbine flow (m^3/s). If None, a percentage of
`design_flow` is used (see `minimum_turbineflow_percent`).
minimum_turbineflow_percent : float or None
Minimum turbine flow as a percentage of `design_flow` (1–100). Used
only when `minimum_turbineflow` is None. If both are None, a default
of 10% is applied.
annual_maintenance_flag : bool
If True, enforce a 7-day annual maintenance outage (flow set to 0).
major_maintenance_flag : bool
If True, enforce a 14-day major maintenance outage every 5 years
(flow set to 0).
This class holds user-configurable parameters that control minimum turbine
flow enforcement and maintenance scheduling. It does not perform any
computations by itself; the values are consumed by
:class:`~FlowPreProcessing`.

Attributes
Parameters
----------
minimum_turbineflow : float or None
minimum_turbineflow_percent : float or None
Absolute minimum turbine flow (m³/s). If provided, this overrides the
percentage-based minimum flow. If ``None``, the minimum is computed from
``minimum_turbineflow_percent`` (or a default of 10%).
minimum_turbineflow_percent : float or int or None
Minimum turbine flow as a percentage of ``design_flow`` (1–100). Used
only when ``minimum_turbineflow`` is ``None``.
annual_maintenance_flag : bool
If ``True``, apply annual maintenance by setting turbine flow to zero
for the lowest-flow calendar week each year (based on mean weekly flow).
major_maintenance_flag : bool
If ``True``, apply major maintenance by setting turbine flow to zero
for the lowest-flow two-week period every five years.

Notes
-----
This class only stores configuration; calculations are performed by
methods in `FlowPreProcessing`.
"""
These parameters are typically attached to a higher-level *flow object*
(e.g., a project-specific container) and then read by methods in
:class:`~FlowPreProcessing`.
"""

def __init__(self, minimum_turbineflow, minimum_turbineflow_percent, # Flow parameters
annual_maintenance_flag, major_maintenance_flag):
Expand All @@ -89,22 +48,21 @@ def __init__(self, minimum_turbineflow, minimum_turbineflow_percent, # Flo


class FlowPreProcessing():
"""Flow preprocessing routines operating on a provided flow object.

"""
Apply design flow limits and scheduled maintenance to a flow object.

The methods in this class expect a `flow_obj` with, at minimum, the
attributes described in the module docstring. Methods update
`flow_obj.turbine_flow` in place and do not return values.

See Also
--------
max_turbineflow_checker : Cap turbine flow at design flow.
min_turbineflow_checker : Enforce minimum turbine flow threshold.
annual_maintenance_implementer : Zero 7 days/year at lowest-flow week.
major_maintenance_implementer : Zero 14 days/5 years at lowest bi-weekly mean.
"""
Methods in this class update values on ``flow_obj`` in-place (most
importantly ``flow_obj.turbine_flow``).

Notes
-----
The caller is responsible for ensuring that:

- ``flow_obj.turbine_flow`` is a 1D numpy array aligned with
``flow_obj.datetime_index``.
- ``flow_obj.datetime_index`` is a pandas.DatetimeIndex whose name (or level
name) is ``"dateTime"`` so that ``pd.Grouper(level="dateTime", ...)``
works as written in the implementation.
"""
# Function that sets max turbine flow to the design flow
def max_turbineflow_checker(self, flow_obj):
"""
Expand All @@ -113,21 +71,17 @@ def max_turbineflow_checker(self, flow_obj):
Parameters
----------
flow_obj : object
Object with attributes:
- flow : numpy.ndarray
Raw inflow time series (m^3/s).
- design_flow : float or numpy.ndarray
Turbine design flow (m^3/s). If array, must align with `flow`.
A flow object with attributes:

Side Effects
- ``turbine_flow`` (numpy.ndarray)
- ``design_flow`` (float)
- ``minimum_turbineflow`` (float or None)
- ``minimum_turbineflow_percent`` (float or None)

Notes
------------
Updates `flow_obj.turbine_flow` (numpy.ndarray), where each element is
`min(flow, design_flow)`.

Examples
--------
>>> # flow_obj.flow = [12, 8], design_flow = 10
>>> # turbine_flow becomes [10, 8]
"""

flow = flow_obj.flow # this is a numpy array
Expand All @@ -136,42 +90,26 @@ def max_turbineflow_checker(self, flow_obj):

# Function that sets min turbine flow to the design flow
def min_turbineflow_checker(self, flow_obj):
"""
Enforce a minimum turbine flow threshold; set values below it to zero.
"""Enforce a minimum turbine flow threshold.

Logic
-----
- If `flow_obj.minimum_turbineflow` is provided, use it (m^3/s).
- Else, compute min flow as
(`minimum_turbineflow_percent` or 10 by default) × `design_flow` / 100.
The computed value is stored back into `flow_obj.minimum_turbineflow`.
Any values in ``flow_obj.turbine_flow`` that fall below the computed
minimum are replaced with zero.

Parameters
----------
flow_obj : object
Object with attributes:
- turbine_flow : numpy.ndarray
Turbine flow series (m^3/s), typically after max-capping.
- design_flow : float or numpy.ndarray
Turbine design flow (m^3/s).
- minimum_turbineflow : float or None
- minimum_turbineflow_percent : float or None

Side Effects
------------
Updates `flow_obj.turbine_flow`, replacing values `< min_flow` with 0.

Notes
-----
- If `design_flow` is an array, elementwise percentages are applied.
- Default minimum percent is 10 if neither absolute nor percentage is given.
This method modifies ``flow_obj.turbine_flow`` in place.

If ``flow_obj.minimum_turbineflow`` is ``None``, the minimum flow is
computed as::

Examples
--------
>>> # design_flow = 20 m^3/s, minimum_turbineflow_percent = 10
>>> # min_flow = 2.0; turbine_flow < 2.0 becomes 0
min_flow = (minimum_turbineflow_percent / 100) * design_flow

where ``minimum_turbineflow_percent`` defaults to 10 if not provided.
"""

flow = flow_obj.turbine_flow # this is a numpy array
design_flow = flow_obj.design_flow

Expand All @@ -190,40 +128,23 @@ def min_turbineflow_checker(self, flow_obj):
flow_obj.turbine_flow = np.where(flow < min_flow, 0, flow) # replace flows less than min flow with 0

def annual_maintenance_implementer(self, flow_obj):
"""
Zero turbine flow for 7 consecutive days once per calendar year.
"""Apply annual maintenance by zeroing the lowest-flow week each year.

Procedure
---------
1) Compute weekly mean flow and identify the week with the lowest mean
(excluding first/last partial weeks).
2) For each year in the time span, zero out the 7-day window starting on
the Monday of that lowest-flow week.
The method identifies the calendar week with the lowest mean weekly
flow (aggregated across all years) and sets turbine flow to zero for
that week in every year of the dataset.

Parameters
----------
flow_obj : object
Object with attributes:
- turbine_flow : numpy.ndarray
Turbine flow series (m^3/s).
- datetime_index : pandas.DatetimeIndex
Timestamps aligned with `turbine_flow`.

Side Effects
------------
Updates `flow_obj.turbine_flow`, setting the 7 chosen days per year to 0.

Assumptions
-----------
- `datetime_index` covers multiple years or at least one full year.
- Weekly grouping uses `pd.Grouper(level="dateTime", freq='W')`. Ensure your
index (or a MultiIndex level) is named "dateTime" or adjust the grouper
accordingly.

Examples
--------
>>> # After applying, each year has a 7-day zero-flow outage at the
>>> # statistically lowest weekly mean period.
Notes
-----
This method modifies ``flow_obj.turbine_flow`` in place.

Weekly means are computed using ``freq='W'`` and grouped by calendar
week number (``%W``). The maintenance window spans 7 days starting on
the Monday of the selected week.
"""

# Function to set annual maintennace - i.e., make the flow 0 for a week a year
Expand Down Expand Up @@ -262,44 +183,24 @@ def annual_maintenance_implementer(self, flow_obj):

# Function that schedules the major maintennace
def major_maintenance_implementer(self, flow_obj):
"""Apply major maintenance by zeroing a low-flow two-week period every 5 years.

"""
Zero turbine flow for 14 consecutive days every 5 years (skip first year).

Procedure
---------
1) Compute bi-weekly (2W) mean flow and identify the bi-weekly window with
the lowest mean (excluding first/last partial windows).
2) For maintenance years spaced every 5 years (starting after the first),
zero out the 14-day window starting on the Monday of the selected week.
The method identifies the calendar week associated with the lowest
mean two-week flow and applies a 14-day zero-flow window starting
from that week in each major-maintenance year.

Parameters
----------
flow_obj : object
Object with attributes:
- turbine_flow : numpy.ndarray
Turbine flow series (m^3/s).
- datetime_index : pandas.DatetimeIndex
Timestamps aligned with `turbine_flow`.

Side Effects
------------
Updates `flow_obj.turbine_flow`, setting the selected 14 days to 0 in each
maintenance year (every 5 years; first year excluded).

Assumptions
-----------
- Sufficient span to include one or more 5-year intervals.
- Bi-weekly grouping uses `pd.Grouper(level="dateTime", freq='2W')`. Ensure
your index (or a MultiIndex level) is named "dateTime" or adjust the
grouper accordingly.

Examples
--------
>>> # In a 10-year record, days 1–14 of the lowest 2-week flow window
>>> # are zeroed in years 5 and 10 (year 0 excluded).
Notes
-----
This method modifies ``flow_obj.turbine_flow`` in place.

Major maintenance years occur every five years starting after the
first year of the dataset. Two-week means are computed using
``freq='2W'``.
"""

# Major maintenance will happend on the month with lowest flow a year - during the first 15 days of this month.

turbine_flow = flow_obj.turbine_flow # turbine flow series with max and min (if on) limits implemented
Expand Down Expand Up @@ -334,5 +235,3 @@ def major_maintenance_implementer(self, flow_obj):
flow.loc[flow.date.isin(dates_maint), 'flow_cms'] = 0 # replace flow in minimum weeks with 0

flow_obj.turbine_flow = flow['flow_cms'].to_numpy() # update


Loading