Source code for soft4pes.sim.simulation
"""
Simulation environment for power electronic systems.
"""
import os
import numpy as np
from scipy.io import savemat
from types import SimpleNamespace
[docs]class ProgressPrinter:
"""
A class used to print the progress of the simulation process.
Parameters
----------
total_steps : int
The total number of steps in the process.
Attributes
----------
total_steps : int
The total number of steps in the process.
last_printed_percent : int
The last printed percentage of progress.
"""
def __init__(self, total_steps):
[docs] self.total_steps = total_steps
[docs] self.last_printed_percent = -5
[docs] def __call__(self, current_step):
"""
Prints the current progress with steps of 5 percent.
Parameters
----------
current_step : int
The number of the current step in the process.
"""
current_percent = int(np.round(current_step / self.total_steps * 100))
if current_percent > self.last_printed_percent + 4:
print(f"{current_percent}%")
self.last_printed_percent = current_percent
[docs]class Simulation:
"""
Simulation environment.
Parameters
----------
sys : system object
System model.
ctr : controller object
Control system.
Ts_sim : float
Simulation sampling interval [s].
Attributes
----------
sys : system object
System model.
ctr : controller object.
Control system.
Ts_sim : float
Simulation sampling interval [s].
t_stop : float
Simulation stop time [s].
matrices : SimpleNamespace
Discrete state-space matrices of the simulated system.
simulation_data : dict
Data from the simulation.
"""
def __init__(self, sys, ctr, Ts_sim, disc_method='forward_euler'):
[docs] self.matrices = self.sys.get_discrete_state_space(
self.Ts_sim, disc_method)
[docs] self.simulation_data = None
# Check if self.ctr.Ts/Ts_sim is an integer. Use tolerance to prevent
# floating point errors
Ts_rat = self.ctr.Ts / self.Ts_sim
if abs(Ts_rat - round(Ts_rat)) > 1e-10:
raise ValueError(
"The ratio of control system sampling interval to "
"simulation sampling interval must be an integer.")
[docs] def simulate(self, t_stop):
"""
Simulate the system.
Parameters
----------
t_stop : float
Simulation length [s]. Simulation start time is always 0 s, i.e. kTs = 0.
"""
progress_printer = ProgressPrinter(int(t_stop / self.ctr.Ts))
self.t_stop = t_stop
for k in range(int(self.t_stop / self.ctr.Ts)):
# Execute the controller
kTs = k * self.ctr.Ts
u_abc = self.ctr(self.sys, kTs)
for k_sim in range(int(self.ctr.Ts / self.Ts_sim)):
kTs_sim = kTs + k_sim * self.Ts_sim
self.sys.update(self.matrices, u_abc, kTs_sim)
progress_printer(k)
# Get the simulation data
self.ctr.get_control_system_data()
self.simulation_data = {'ctr': self.ctr.data, 'sys': self.sys.data}
return self.process_simulation_data(
SimpleNamespace(sys=self.sys.data, ctr=self.ctr.data))
[docs] def save_data(self, filename='sim_data.mat', path=''):
"""
Save the simulation data to a .mat file.
Parameters
----------
filename : str, optional
Name of the file to save the data to. The default filename is 'sim_data.mat'.
path : str, optional
Directory path to save the file to. The path can be absolute or relative to the current
directory. The default saving directory is the current directory.
"""
# Ensure the filename ends with .mat
if not filename.endswith('.mat'):
filename += '.mat'
# Ensure the directory exists
if path and not os.path.exists(path):
os.makedirs(path)
full_path = os.path.join(path, filename)
savemat(full_path, self.simulation_data)
[docs] def process_simulation_data(self, data):
"""
Recursively convert lists of arrays in a SimpleNamespace to NumPy arrays.
Parameters
----------
data : SimpleNamespace or list of ndarray
The data to be converted. Can be a SimpleNamespace or a list of arrays.
Returns
-------
SimpleNamespace or ndarray
A SimpleNamespace with lists of arrays converted to NumPy arrays, or a NumPy array if the input is a list of arrays.
"""
if isinstance(data, list):
# If data is a list of arrays, convert it to a single NumPy array
stacked_array = np.array(data)
return stacked_array
elif isinstance(data, SimpleNamespace):
# If data is a SimpleNamespace, recursively process its attributes
for attr in data.__dict__:
setattr(data, attr,
self.process_simulation_data(getattr(data, attr)))
return data
else:
# If data is neither a list nor a SimpleNamespace, return it as is
return data