bsm2_cl ¶
This represents the base model in an closed loop simulation.
- BSM2 base: Primary clarifier, 5 asm1 reactors, a second clarifier, sludge thickener, adm1 fermenter, sludge dewatering and wastewater storage in dynamic simulation with controllers.
BSM2CL ¶
BSM2CL(data_in=None, timestep=1 / 60 / 24, endtime=None, evaltime=None, use_noise=1, noise_file=None, noise_seed=1, data_out=None, *, tempmodel=False, activate=False)
Creates a BSM2CL object.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data_in | ndarray(n, 22) | str(optional) | Influent data. Has to be a 2D array. [SI, SS, XI, XS, XBH, XBA, XP, SO, SNO, SNH, SND, XND, SALK, TSS, Q, TEMP, SD1, SD2, SD3, XD4, XD5] | None |
timestep | float(optional) | Timestep for the simulation [d]. | 1 / 60 / 24 |
endtime | float(optional) | Endtime for the simulation [d]. | None |
evaltime | int | ndarray(2)(optional) | Evaluation time for the simulation [d]. [starttime, self.simtime[-1]] | None |
use_noise | int(optional) |
| 1 |
noise_file | str(optional) | Noise data. Needs to be provided if use_noise is 1. If not provided, the default noise file is used. | None |
noise_seed | int(optional) | Seed for the random number generator. Default is 1. | 1 |
data_out | str(optional) | Path to the output data file. | None |
tempmodel | bool(optional) | If | False |
activate | bool(optional) | If | False |
Source code in src/bsm2_python/bsm2_cl.py
def __init__(
self,
data_in: np.ndarray | str | None = None,
timestep: float | None = 1 / 60 / 24,
endtime: float | None = None,
evaltime: int | np.ndarray | None = None,
use_noise: int = 1,
noise_file: str | None = None,
noise_seed: int = 1,
data_out: str | None = None,
*,
tempmodel: bool = False,
activate: bool = False,
):
if timestep is not None and timestep > 1 / 60 / 24:
logger.warning(
'Timestep should not be larger than 1 minute due to sensor sensitivity. \
Will continue with provided timestep, but most probably fail.'
)
super().__init__(
data_in=data_in,
timestep=timestep,
endtime=endtime,
evaltime=evaltime,
tempmodel=tempmodel,
activate=activate,
data_out=data_out,
)
num_sen = 1
den_sen4 = [aerationcontrolinit.T_SO4**2, 2 * aerationcontrolinit.T_SO4, 1]
self.so4_sensor = Sensor(
num_sen, den_sen4, aerationcontrolinit.MIN_SO4, aerationcontrolinit.MAX_SO4, aerationcontrolinit.STD_SO4
)
self.pid4 = PID(**aerationcontrolinit.PID4_PARAMS)
num_act = 1
den_act4 = [aerationcontrolinit.T_KLA4**2, 2 * aerationcontrolinit.T_KLA4, 1]
self.kla4_actuator = Actuator(num_act, den_act4)
if use_noise == 0:
self.noise_so4 = np.zeros(2)
self.noise_timestep = np.array((0, self.endtime)).flatten()
elif use_noise == 1:
if noise_file is None:
noise_file = path_name + '/data/sensornoise.csv'
with open(noise_file, encoding='utf-8-sig') as f:
noise_data = np.array(list(csv.reader(f, delimiter=','))).astype(np.float64)
if noise_data[-1, 0] < self.endtime:
err = 'Noise file does not cover the whole simulation time.\n \
Please provide a valid noise file.'
raise ValueError(err)
noise_shape_criteria = 2
if noise_data.shape[1] < noise_shape_criteria:
err = 'Noise file needs to have at least 2 columns: time and noise data'
raise ValueError(err)
self.noise_so4 = noise_data[:, 1].flatten()
self.noise_timestep = noise_data[:, 0].flatten()
del noise_data
if timestep is None:
# calculate difference between each time step in data_in
self.simtime = self.noise_timestep
self.timesteps = np.diff(
self.noise_timestep, append=(2 * self.noise_timestep[-1,] - self.noise_timestep[-2,])
)
else:
self.simtime = np.arange(0, self.data_in[-1, 0], timestep)
self.timesteps = timestep * np.ones(len(self.simtime))
self.simtime = self.simtime[self.simtime <= self.endtime]
rng_mode = 2 # random number generator mode
if use_noise in {0, 1}:
pass
elif use_noise == rng_mode:
np.random.seed(noise_seed)
# create random noise array with mean 0 and variance 1
self.noise_so4 = np.random.normal(0, 1, len(self.simtime)).flatten()
self.noise_timestep = self.simtime
else:
err = 'use_noise has to be 0, 1 or 2'
raise ValueError(err)
self.data_time = self.data_in[:, 0]
# self.simtime = np.arange(0, self.endtime, self.timestep)
self.kla4_a = aerationcontrolinit.KLA4_INIT
self.kla3_a = aerationcontrolinit.KLA3GAIN * self.kla4_a
self.kla5_a = aerationcontrolinit.KLA5GAIN * self.kla4_a
self.y_in_all = np.zeros((len(self.simtime), 21))
self.y_eff_all = np.zeros((len(self.simtime), 21))
self.y_in_bp_all = np.zeros((len(self.simtime), 21))
self.to_primary_all = np.zeros((len(self.simtime), 21))
self.prim_in_all = np.zeros((len(self.simtime), 21))
self.qpass_plant_all = np.zeros((len(self.simtime), 21))
self.qpassplant_to_as_all = np.zeros((len(self.simtime), 21))
self.qpassAS_all = np.zeros((len(self.simtime), 21))
self.to_as_all = np.zeros((len(self.simtime), 21))
self.feed_settler_all = np.zeros((len(self.simtime), 21))
self.qthick2AS_all = np.zeros((len(self.simtime), 21))
self.qthick2prim_all = np.zeros((len(self.simtime), 21))
self.qstorage2AS_all = np.zeros((len(self.simtime), 21))
self.qstorage2prim_all = np.zeros((len(self.simtime), 21))
self.sludge_all = np.zeros((len(self.simtime), 21))
self.iqi_all = np.zeros(len(self.simtime))
self.eqi_all = np.zeros(len(self.simtime))
self.oci_all = np.zeros(len(self.simtime))
self.oci_factors_all = np.zeros((len(self.simtime), 8))
self.violation_all = np.zeros(len(self.simtime))
step ¶
step(i, so4ref=None)
Simulates one time step of the BSM2 model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
i | int | Index of the current time step [-]. | required |
so4ref | float(optional) | Setpoint for the dissolved oxygen concentration [g(O₂) ⋅ m⁻³] If not provided, the setpoint is set to aerationcontrolinit.SO4REF. | None |
Source code in src/bsm2_python/bsm2_cl.py
def step(self, i: int, so4ref: float | None = None):
"""Simulates one time step of the BSM2 model.
Parameters
----------
i : int
Index of the current time step [-].
so4ref : float (optional)
Setpoint for the dissolved oxygen concentration [g(O₂) ⋅ m⁻³] \n
If not provided, the setpoint is set to aerationcontrolinit.SO4REF.
"""
if so4ref is None:
self.pid4.setpoint = aerationcontrolinit.SO4REF
else:
self.pid4.setpoint = so4ref
step: float = self.simtime[i]
stepsize: float = self.timesteps[i]
# get index of noise that is smaller than and closest to current time step within a small tolerance
idx_noise = int(np.where(self.noise_timestep - 1e-7 <= step)[0][-1])
sensor_signal = self.so4_sensor.output(self.y_out4[SO], stepsize, self.noise_so4[idx_noise])
control_signal = self.pid4.output(sensor_signal, stepsize)
actuator_signal = self.kla4_actuator.output(control_signal, stepsize)
self.kla4_a = actuator_signal
self.kla3_a = aerationcontrolinit.KLA3GAIN * self.kla4_a
self.kla5_a = aerationcontrolinit.KLA5GAIN * self.kla4_a
self.klas = np.array([0, 0, self.kla3_a, self.kla4_a, self.kla5_a])
super().step(i)