2. Step functions#

In many experimental setups, dynamic loads are applied to simulate realistic operating conditions. Experimental platforms often handle these dynamic loads effectively, interpolating between data points if necessary to create smooth profiles. However, while interpolation is a convenient tool, there are situations where it might not be the best approach for simulating system behavior. Therefore, we also supply helper functions to construct step-based load profiles.

2.1. Why not interpolate?#

Interpolating data can introduce a level of artificial smoothness that doesn’t always reflect the abrupt changes seen in real-world systems. For example, interpolated loads are often used to ease solver convergence, but they may not capture the behavior of systems that respond rapidly to changes. This is particularly important for systems that exhibit stepwise or discrete changes in load, where instantaneous shifts between levels are more appropriate than a continuous curve.

While writing an interpolation function is typically straightforward—requiring little more than a call to a standard library, the complexity increases when building a function that implements stepwise behavior. A step function requires more careful attention to correctly represent when and where the system load changes instantaneously. Consequently, we provide this functionality within the loadfns modeule to reduce the users’ burden to have to develop their own.

2.2. Overview#

When dealing with numerical simulations, introducing ramps between load changes can significantly improve the stability of the solver, reducing the risk of failure during abrupt transitions. Sudden, instantaneous changes in load can sometimes cause solvers to struggle, especially with stiff systems, leading to crashes or errors. That’s why in Thevenin, we offer two classes for defining stepped load profiles: StepFunction and RampedSteps.

The StepFunction class is designed for scenarios where immediate, instantaneous changes in load are appropriate, while the RampedSteps class helps transition between steps by applying an interpolation ramps over a specified time interval at the start of each new step. These two approaches cover a wide range of scenarios, from systems that can handle rapid shifts to those that require more stable transitions.

Below, we will cover:

  1. Building load profiles using interpolated data.

  2. Setting up multi-step experiments using for loops.

  3. Using the StepFunctio class to create instantaneous stepped loads.

  4. Using the RampedSteps class to create stable transitions between load steps.

2.3. Dynamic experiments#

To create dynamic load profiles, especially for more complex experiments, there are many approaches you can take. The Experiment class allows users to pass in any Python Callable like f(t: float) -> float to control each step. Therefore, if you have data, you can easily interpolate the data to create a load profile, or you can automate the construction of load steps using a for loop. Below we demonstrate both approaches.

 1import thevenin
 2import numpy as np
 3
 4model = thevenin.Model()
 5
 6# Fake hour-by-hour load data
 7time_s = 3600.*np.array([0., 1., 2., 3., 4., 5.])
 8current_A = model.capacity*np.array([0.6, 0.3, -0.5, 0.2, 0.3, -0.1])
 9
10# Interpolating the data
11interp = lambda t: np.interp(t, time_s, current_A)
12
13demand = thevenin.Experiment()
14demand.add_step('current_A', interp, (3600*6, 60.))
15
16soln = model.run(demand)
17soln.plot('time_h', 'current_A')
18soln.plot('time_h', 'voltage_V')
[thevenin UserWarning]: Using the default parameter file 'params.yaml'.
../_images/0e1ac83fa495aaf6166e3c42ffe52b67a80feb137b63b30cf10a104b4ea00dc3.png ../_images/6e8bb01372ee244a66128a702d9178f02f560b0f69ae348160eedafc71e231f3.png

In the script above, the data represents hour-by-hour constant-current loads, which might represent some stationary storage system. Since the current is constant across each hour, interpolating between points poorly approximates the actual system behavior. However, interpolation might be more relevant for other dynamic systems like electric vehicles, where data is resolved on shorter timescales, such as seconds.

A better approach for modeling constant-step experiments, rather than using interpolation, is to manually construct the steps using a for loop. In the code block below, we demonstrate how to create a new experiment with multiple steps, where each step lasts one hour, and the current is set by the values in the current_A array.

1# Looping over constant steps
2demand = thevenin.Experiment()
3for amps in current_A:
4    demand.add_step('current_A', amps, (3600, 60.))
5
6soln = model.run(demand)
7soln.plot('time_h', 'current_A')
8soln.plot('time_h', 'voltage_V')
../_images/5e3f2af20ecd1cc65e59efa847be883aae3634cacf3ba80649702c11ba235731.png ../_images/6f6d61c2e2a9e61bde545b17612082e336dbf12657fed44464f81392ef08bcd3.png

This loop-based method significantly improves the accuracy of the results in this case. You can see how different the two voltage profiles are when the load profile is applied correctly, instead of using interpolation. This loop-based approach offers the most flexibility and is recommended when users need precise control over each step. For example, using the add_step method allows you to add different limits to each step, which can be incorporated into the loop. This level of control is not always possible with other methods.

2.4. Instantaneous load steps#

For scenarios involving abrupt changes in load, the StepFunction class can also be useful. It allows users to define a series of instantaneous steps at specific time points, simulating conditions where the load jumps suddenly between levels. However, depending on your model’s parameters, using this class can sometimes lead to instabilities that are not present with the loop-based approach described above.

In the example below, we use the same time and current data to create a StepFunction instance. You’ll see that the model crashes after the second step, which is due to the stiff nature of the instantaneous load changes. The loop-based approach avoids this by recalculating and correcting initial conditions at the start of each step. In contrast, the StepFunction method applies the entire load profile as a single experimental load, meaning the model can only correct the initial conditions once, at the beginning of the simulation.

 1# Automatted instantaneous steps
 2dynamic_load = thevenin.loadfns.StepFunction(time_s, current_A)
 3
 4demand = thevenin.Experiment()
 5demand.add_step('current_A', dynamic_load, (3600*6, 60.))
 6
 7soln = model.run(demand)
 8print(f"{soln.success=}")
 9soln.plot('time_h', 'current_A')
10soln.plot('time_h', 'voltage_V')
soln.success=[False]
../_images/92a4ae3ce66c5787a093e68727ab74ca41566ea2b8988989bc9e1587d73270c4.png ../_images/a1c914a09baf612351952bef7dab8607f206e4878de799a68b08abcc88b54752.png

Aside from causing crashes, this approach is also less flexible in terms of setting limits or adjusting solver settings, as the entire profile is handled as a single experimental step rather than multiple, individual ones. Due to occasional crashes and reduced flexibility, we don’t generally recommend the StepFunction class, though it’s included in the module for completeness. Instead, for instantaneous steps, we suggest using the loop-based approach, even if it requires writing a few more lines of code.

Alternatively, when a true instantaneous step can be avoided and approximated with a rapid ramp between steps, we offer the RampedSteps class. This option is simple, quick to use, and provides improved stability compared to StepFunction.

2.5. Ramped load transitions#

Unlike StepFunction, the RampedSteps class introduces “smooth” transitions between load steps by ramping up or down over a specified time period. This method is especially useful when dealing with stiff systems, where abrupt changes might otherwise cause solver instability. Below we demonstrate this using the same hour-by-hour profile from above. We set the ramp between steps to be just one millisecond so that the transitions are still quick and approximate an instantaneous change. In this case, the added ramps improve the stability and the full simulation is run, as shown in the figure. Overall, the results are nearly identical to the loop-based approach since the ramps are set to occur over such a small time scale. In particular, the main difference is shown in the current profile, where you can briefly see the first ramp (starting from zero current at t = 0).

1# Stabilize the solver with ramped steps
2dynamic_load = thevenin.loadfns.RampedSteps(time_s, current_A, 1e-3)
3
4demand = thevenin.Experiment()
5demand.add_step('current_A', dynamic_load, (3600*6, 60.))
6
7soln = model.run(demand)
8soln.plot('time_h', 'current_A')
9soln.plot('time_h', 'voltage_V')
../_images/7c8f57f652261b4bf928ccf71909dbf3c0315193407a671f723e581b531ebbfe.png ../_images/de5e972375fe7cd173b51f678e6e3618ea3e5dccef27d7d271e594eac2d6adec.png

While the RampedSteps class improves solver stability, it still lacks flexibility for setting limits on each individual step. For instance, you could apply limits to stop the simulation if the voltage goes outside a specific window (e.g., [3, 4.2]), but this would simply end the entire simulation prematurely. In most cases, you wouldn’t want to stop the simulation completely but instead transition to the next step early. To achieve this behavior, you would need to use loops, or alternatively, set up multiple instances of the RampedSteps class if you want to transition between groups of steps based on specific limits rather than between individual steps.

In general, if maximum flexibility is needed, more manual setup is required for multi-step experiments. However, if you can work within the limitations of RampedSteps, it is a powerful tool for quickly constructing step-like profiles while maintaining some degree of stability.

2.6. Conclusion#

In this tutorial, we explored various methods for constructing dynamic load profiles using Thevenin’s StepFunction and RampedSteps classes. We’ve shown how both instantaneous steps and ramps between steps can be modeled and discussed the trade-offs between flexibility, stability, and ease of use. While loop-based approaches offer the greatest control, RampedSteps provides a simple and effective way to ensure stability in simulations, making it a valuable option for many users. Ultimately, the best method depends on the complexity of the load profile you need and the requirements of your specific experiment or model.