-
Notifications
You must be signed in to change notification settings - Fork 1
Description
SimulationFunctions that are defined as functions of time currently operate on the simulation day index. That is, in
For example, defining a parameter as a sine wave whose value fluctuates seasonally over the calendar year is more difficult than we would like. A naive implementation might look like this:
from typing_extensions import override
from epymorph.kit import *
import numpy as np
import matplotlib.pyplot as plt
class SeasonalWaveA(ParamFunctionTime[np.float64]):
@override
def evaluate1(self, day: int) -> float:
return np.sin(2 * np.pi * day / 365)
def f_a(time_frame, start_date_index=0):
"""Eval function for time_frame and return (x_values, y_values) for plotting."""
t = np.arange(start_date_index, start_date_index + time_frame.days)
ft = SeasonalWaveA().with_context(time_frame=time_frame).evaluate()
return t, ft
tf1 = TimeFrame.rangex("2020-01-01", "2020-04-01")
tf2 = TimeFrame.rangex("2020-04-01", "2020-07-01")
# Plot the function over the first time frame (blue)
plt.plot(*f_a(tf1))
# Plot the function over the second time frame (orange)
plt.plot(*f_a(tf2, tf1.days))
plt.title("Problematic Example A")
plt.show()
Evaluating the functions over different time frames does not produce a consistent seasonal pattern as we might hope because, during evaluation, the day index always starts from 0.
Fixing this requires encoding explicit calendar date handling into the function logic:
from datetime import date
class SeasonalWaveB(ParamFunctionTime[np.float64]):
def __init__(self, time_zero: date):
self.time_zero = time_zero
@override
def evaluate1(self, day: int) -> float:
offset = (self.time_frame.start_date - self.time_zero).days
return np.sin(2 * np.pi * (offset + day) / 365)
def f_b(time_frame, time_zero: date, start_date_index=0):
"""Eval function for time_frame and return (x_values, y_values) for plotting."""
t = np.arange(start_date_index, start_date_index + time_frame.days)
ft = SeasonalWaveB(time_zero).with_context(time_frame=time_frame).evaluate()
return t, ft
tf1 = TimeFrame.rangex("2020-01-01", "2020-04-01")
tf2 = TimeFrame.rangex("2020-04-01", "2020-07-01")
# Plot the function over the first time frame (blue)
plt.plot(*f_b(tf1, tf1.start_date))
# Plot the function over the second time frame (orange)
plt.plot(*f_b(tf2, tf1.start_date, tf1.days))
plt.title("Nicely Continuous Example B")
plt.show()
We now get seasonal behavior regardless of the time frame evaluated, however this implementation is verbose and requires us to provide a construction-time parameter which might be prone to user error.
This task is to investigate improvements to the way dates are handled for these use-cases.