Source code for gym_electric_motor.physical_systems.physical_systems

import warnings

import numpy as np
from gymnasium.spaces import Box

import gym_electric_motor as gem

from ..core import PhysicalSystem
from ..random_component import RandomComponent
from ..utils import set_state_array


[docs]class SCMLSystem(PhysicalSystem, RandomComponent): """ The SCML(Supply-Converter-Motor-Load)-System is used for the simulation of a technical setting consisting of these components and a solver for the electrical ODE of the motor and mechanical ODE of the load. """ OMEGA_IDX = 0 TORQUE_IDX = 1 CURRENTS_IDX = [] VOLTAGES_IDX = [] U_SUP_IDX = -1 @property def limits(self): return self._limits @property def nominal_state(self): return self._nominal_state @property def supply(self): """The voltage supply instance in the physical system""" return self._supply @property def converter(self): """The power electronic converter instance in the system""" return self._converter @property def electrical_motor(self): """The electrical motor instance of the system""" return self._electrical_motor @property def mechanical_load(self): """The mechanical load instance in the system""" return self._mechanical_load def __init__(self, converter, motor, load, supply, ode_solver, tau=1e-4, calc_jacobian=None): """ Args: converter(PowerElectronicConverter): Converter for the physical system motor(ElectricMotor): Motor of the system load(MechanicalLoad): Mechanical Load of the System supply(VoltageSupply): Voltage Supply ode_solver(OdeSolver): Ode Solver to use in this setting tau(float): discrete time step of the system calc_jacobian(bool): If True, the jacobian matrices will be taken into account for the ode-solvers. Default: The jacobians are used, if available """ RandomComponent.__init__(self) self._converter = converter self._electrical_motor = motor self._mechanical_load = load self._supply = supply state_names = self._build_state_names() self._ode_solver = ode_solver if calc_jacobian is None: calc_jacobian = self._electrical_motor.HAS_JACOBIAN and self._mechanical_load.HAS_JACOBIAN if calc_jacobian and self._electrical_motor.HAS_JACOBIAN and self._mechanical_load.HAS_JACOBIAN: jac = self._system_jacobian else: jac = None if calc_jacobian and jac is None: warnings.warn("Jacobian Matrix is not provided for either the motor or the load Model") self._ode_solver.set_system_equation(self._system_equation, jac) self._mechanical_load.set_j_rotor(self._electrical_motor.motor_parameter["j_rotor"]) self._t = 0 self._set_indices() state_space = self._build_state_space(state_names) super().__init__(self._converter.action_space, state_space, state_names, tau) self._limits = np.zeros_like(state_names, dtype=float) self._nominal_state = np.zeros_like(state_names, dtype=float) self._set_limits() self._set_nominal_state() self.system_state = np.zeros_like(state_names, dtype=float) self._system_eq_placeholder = None self._motor_deriv_size = None self._load_deriv_size = None self._components = [ self._supply, self._converter, self._electrical_motor, self._mechanical_load, self._ode_solver, ] self._converter.tau = self.tau def _set_limits(self): """ Method to set the physical limits from the modules. """ for ind, state in enumerate(self._state_names): motor_lim = self._electrical_motor.limits.get(state, np.inf) mechanical_lim = self._mechanical_load.limits.get(state, np.inf) self._limits[ind] = min(motor_lim, mechanical_lim) self._limits[self._state_positions["u_sup"]] = self.supply.u_nominal def _set_nominal_state(self): """ Method to set the nominal values from the modules. """ for ind, state in enumerate(self._state_names): motor_nom = self._electrical_motor.nominal_values.get(state, np.inf) mechanical_nom = self._mechanical_load.nominal_values.get(state, np.inf) self._nominal_state[ind] = min(motor_nom, mechanical_nom) self._nominal_state[self._state_positions["u_sup"]] = self.supply.u_nominal def _build_state_space(self, state_names): """ Method to build the normalized state space (i.e. the maximum and minimum possible values for each state variable normalized by the limits). Args: state_names(list(str)): list of the names of each state. """ raise NotImplementedError def _build_state_names(self): """ Setting of the state names in the physical system. """ raise NotImplementedError def _set_indices(self): """ Setting of indices to faster access the arrays during integration. """ self._omega_ode_idx = self._mechanical_load.OMEGA_IDX self._load_ode_idx = list(range(len(self._mechanical_load.state_names))) self._ode_currents_idx = list( range( self._load_ode_idx[-1] + 1, self._load_ode_idx[-1] + 1 + len(self._electrical_motor.CURRENTS), ) ) self._motor_ode_idx = self._ode_currents_idx self.OMEGA_IDX = self.mechanical_load.OMEGA_IDX self.TORQUE_IDX = len(self.mechanical_load.state_names) currents_lower = self.TORQUE_IDX + 1 currents_upper = currents_lower + len(self._electrical_motor.CURRENTS) self.CURRENTS_IDX = list(range(currents_lower, currents_upper)) voltages_lower = currents_upper voltages_upper = voltages_lower + len(self._electrical_motor.VOLTAGES) self.VOLTAGES_IDX = list(range(voltages_lower, voltages_upper)) self.U_SUP_IDX = list(range(voltages_upper, voltages_upper + self._supply.voltage_len))
[docs] def seed(self, seed=None): RandomComponent.seed(self, seed) sub_seeds = self.seed_sequence.spawn(len(self._components)) for component, sub_seed in zip(self._components, sub_seeds): if isinstance(component, gem.RandomComponent): component.seed(sub_seed)
[docs] def simulate(self, action, *_, **__): # Docstring of superclass ode_state = self._ode_solver.y i_in = self._electrical_motor.i_in(ode_state[self._ode_currents_idx]) switching_times = self._converter.set_action(action, self._t) for t in switching_times[:-1]: i_sup = self._converter.i_sup(i_in) u_sup = self._supply.get_voltage(self._t, i_sup) u_in = self._converter.convert(i_in, self._ode_solver.t) u_in = [u * u_s for u in u_in for u_s in u_sup] self._ode_solver.set_f_params(u_in) ode_state = self._ode_solver.integrate(t) i_in = self._electrical_motor.i_in(ode_state[self._ode_currents_idx]) i_sup = self._converter.i_sup(i_in) u_sup = self._supply.get_voltage(self._t, i_sup) u_in = self._converter.convert(i_in, self._ode_solver.t) u_in = [u * u_s for u in u_in for u_s in u_sup] self._ode_solver.set_f_params(u_in) ode_state = self._ode_solver.integrate(self._t + self.tau) self._t = self._ode_solver.t self._k += 1 torque = self._electrical_motor.torque(ode_state[self._motor_ode_idx]) n_mech_states = len(self.mechanical_load.state_names) motor_state = ode_state[n_mech_states:] self.system_state[:n_mech_states] = ode_state[:n_mech_states] self.system_state[self.TORQUE_IDX] = torque self.system_state[self.CURRENTS_IDX] = motor_state[self._electrical_motor.CURRENTS_IDX] self.system_state[self.VOLTAGES_IDX] = u_in self.system_state[self.U_SUP_IDX] = u_sup return self.system_state / self._limits
def _system_equation(self, t, state, u_in, **__): """ Systems differential equation system. It is a concatenation of the motors electrical ode system and the mechanical ode system. Args: t(float): Current systems time state(ndarray(float)): Current systems ODE-State u_in(list(float)): Input voltages from the converter Returns: ndarray(float): The derivatives of the ODE-State. Based on this, the Ode Solver calculates the next state. """ if self._system_eq_placeholder is None: motor_state = state[self._motor_ode_idx] motor_derivative = self._electrical_motor.electrical_ode(motor_state, u_in, state[self._omega_ode_idx]) torque = self._electrical_motor.torque(motor_state) load_derivative = self._mechanical_load.mechanical_ode(t, state[self._load_ode_idx], torque) self._system_eq_placeholder = np.concatenate((load_derivative, motor_derivative)) self._motor_deriv_size = motor_derivative.size self._load_deriv_size = load_derivative.size else: motor_state = state[self._motor_ode_idx] self._system_eq_placeholder[: self._load_deriv_size] = self._mechanical_load.mechanical_ode( t, state[self._load_ode_idx], self._electrical_motor.torque(motor_state) ).ravel() self._system_eq_placeholder[self._load_deriv_size :] = self._electrical_motor.electrical_ode( motor_state, u_in, state[self._omega_ode_idx] ).ravel() return self._system_eq_placeholder def _system_jacobian(self, t, state, u_in, **__): motor_state = state[self._motor_ode_idx] ( motor_jac, el_state_over_omega, torque_over_el_state, ) = self._electrical_motor.electrical_jacobian(motor_state, u_in, state[self._omega_ode_idx]) torque = self._electrical_motor.torque(motor_state) load_jac, load_over_torque = self._mechanical_load.mechanical_jacobian(t, state[self._load_ode_idx], torque) system_jac = np.zeros((state.shape[0], state.shape[0])) system_jac[: load_jac.shape[0], : load_jac.shape[1]] = load_jac system_jac[-motor_jac.shape[0] :, -motor_jac.shape[1] :] = motor_jac system_jac[-motor_jac.shape[0] :, [self._omega_ode_idx]] = el_state_over_omega.reshape((-1, 1)) system_jac[: load_jac.shape[0], load_jac.shape[1] :] = np.matmul( load_over_torque.reshape(-1, 1), torque_over_el_state.reshape(1, -1) ) return system_jac
[docs] def reset(self, *_): """ Reset all the systems modules to an initial state. Returns: The new state of the system. """ self.next_generator() motor_state = self._electrical_motor.reset(state_space=self.state_space, state_positions=self.state_positions) mechanical_state = self._mechanical_load.reset( state_space=self.state_space, state_positions=self.state_positions, nominal_state=self.nominal_state, ) ode_state = np.concatenate((mechanical_state, motor_state)) u_sup = self.supply.reset() u_in = self.converter.reset() u_in = [u * u_s for u in u_in for u_s in u_sup] torque = self.electrical_motor.torque(motor_state) self._t = 0 self._k = 0 self._ode_solver.set_initial_value(ode_state, self._t) system_state = np.concatenate( ( ode_state[: len(self._mechanical_load.state_names)], [torque], motor_state[self._electrical_motor.CURRENTS_IDX], u_in, u_sup, ) ) return system_state / self._limits
[docs]class DcMotorSystem(SCMLSystem): """ SCML-System that can be used for all DC Motors. """ def _build_state_names(self): # Docstring of superclass return ( self._mechanical_load.state_names + ["torque"] + self._electrical_motor.CURRENTS + self._electrical_motor.VOLTAGES + ["u_sup"] ) def _build_state_space(self, state_names): # Docstring of superclass low, high = self._electrical_motor.get_state_space(self._converter.currents, self._converter.voltages) low_mechanical, high_mechanical = self._mechanical_load.get_state_space((low["omega"], high["omega"])) low.update(low_mechanical) high.update(high_mechanical) high["u_sup"] = self._supply.supply_range[1] / self._supply.u_nominal if self._supply.supply_range[0] != self._supply.supply_range[1]: low["u_sup"] = self._supply.supply_range[0] / self._supply.u_nominal else: low["u_sup"] = 0 low = set_state_array(low, state_names) high = set_state_array(high, state_names) return Box(low, high, dtype=np.float64)
class ThreePhaseMotorSystem(SCMLSystem): """ SCML-System that implements the basic transformations needed for three phase drives. """ def abc_to_alphabeta_space(self, abc_quantities): """ Transformation from abc to alphabeta space Args: abc_quantities: Three quantities in abc-space (e.g. (u_a, u_b, u_c) or (i_a, i_b, i_c)) Returns: (quantity_alpha, quantity_beta): The quantities in the alphabeta-space """ alphabeta_quantity = self._electrical_motor.t_23(abc_quantities) return alphabeta_quantity def alphabeta_to_abc_space(self, alphabeta_quantities): """ Transformation from dq to abc space Args: alphabeta_quantities: Two quantities in alphabeta-space (e.g. (u_alpha, u_beta) or (i_alpha, i_beta)) Returns: (quantity_a, quantity_b, quantity_c): The quantities in the abc-space """ return self._electrical_motor.t_32(alphabeta_quantities) def abc_to_dq_space(self, abc_quantities, epsilon_el, normed_epsilon=False): """ Transformation from abc to dq space Args: abc_quantities: Three quantities in abc-space (e.g. (u_a, u_b, u_c) or (i_a, i_b, i_c)) epsilon_el: Electrical angle of the motor normed_epsilon(bool): True, if epsilon is normed to [-1,1] else in [-pi, pi] (default) Returns: (quantity_d, quantity_q): The quantities in the dq-space """ if normed_epsilon: epsilon_el *= np.pi dq_quantity = self._electrical_motor.q_inv(self._electrical_motor.t_23(abc_quantities), epsilon_el) return dq_quantity def dq_to_abc_space(self, dq_quantities, epsilon_el, normed_epsilon=False): """ Transformation from dq to abc space Args: dq_quantities: Three quantities in dq-space (e.g. (u_d, u_q) or (i_d, i_q)) epsilon_el: Electrical angle of the motor normed_epsilon(bool): True, if epsilon is normed to [-1,1] else in [-pi, pi] (default) Returns: (quantity_a, quantity_b, quantity_c): The quantities in the abc-space """ if normed_epsilon: epsilon_el *= np.pi return self._electrical_motor.t_32(self._electrical_motor.q(dq_quantities, epsilon_el)) def alphabeta_to_dq_space(self, alphabeta_quantities, epsilon_el, normed_epsilon=False): """ Transformation from alphabeta to dq space Args: alphabeta_quantities: Two quantities in alphabeta-space (e.g. (u_alpha, u_beta) or (i_alpha, i_beta)) epsilon_el: Electrical angle of the motor normed_epsilon(bool): True, if epsilon is normed to [-1,1] else in [-pi, pi] (default) Returns: (quantity_d, quantity_q): The quantities in the dq-space """ if normed_epsilon: epsilon_el *= np.pi dq_quantity = self._electrical_motor.q_inv(alphabeta_quantities, epsilon_el) return dq_quantity def dq_to_alphabeta_space(self, dq_quantities, epsilon_el, normed_epsilon=False): """ Transformation from dq to alphabeta space Args: dq_quantities: Two quantities in dq-space (e.g. (u_d, u_q) or (i_d, i_q)) epsilon_el: Electrical angle of the motor normed_epsilon(bool): True, if epsilon is normed to [-1,1] else in [-pi, pi] (default) Returns: (quantity_alpha, quantity_beta): The quantities in the alphabeta-space """ if normed_epsilon: epsilon_el *= np.pi return self._electrical_motor.q(dq_quantities, epsilon_el)
[docs]class SynchronousMotorSystem(ThreePhaseMotorSystem): """ SCML-System that can be used with all Synchronous Motors """ def __init__(self, control_space="abc", **kwargs): """ Args: control_space(str):('abc' or 'dq') Choose, if actions the actions space is in dq or abc space kwargs: Further arguments to pass tp SCMLSystem """ super().__init__(**kwargs) self.control_space = control_space if control_space == "dq": assert ( type(self._converter.action_space) == Box ), "dq-control space is only available for Continuous Controlled Converters" self._action_space = Box(-1, 1, shape=(2,), dtype=np.float64) def _build_state_space(self, state_names): # Docstring of superclass low = -1 * np.ones_like(state_names, dtype=float) low[self.U_SUP_IDX] = 0.0 high = np.ones_like(state_names, dtype=float) return Box(low, high, dtype=np.float64) def _build_state_names(self): # Docstring of superclass return self._mechanical_load.state_names + [ "torque", "i_a", "i_b", "i_c", "i_sd", "i_sq", "u_a", "u_b", "u_c", "u_sd", "u_sq", "epsilon", "u_sup", ] def _set_indices(self): # Docstring of superclass self._omega_ode_idx = self._mechanical_load.OMEGA_IDX self._load_ode_idx = list(range(len(self._mechanical_load.state_names))) self._ode_currents_idx = list( range( self._load_ode_idx[-1] + 1, self._load_ode_idx[-1] + 1 + len(self._electrical_motor.CURRENTS), ) ) self._motor_ode_idx = self._ode_currents_idx self._motor_ode_idx += [self._motor_ode_idx[-1] + 1] self._ode_currents_idx = self._motor_ode_idx[:-1] self.OMEGA_IDX = self.mechanical_load.OMEGA_IDX self.TORQUE_IDX = len(self.mechanical_load.state_names) currents_lower = self.TORQUE_IDX + 1 currents_upper = currents_lower + 5 self.CURRENTS_IDX = list(range(currents_lower, currents_upper)) voltages_lower = currents_upper voltages_upper = voltages_lower + 5 self.VOLTAGES_IDX = list(range(voltages_lower, voltages_upper)) self.EPSILON_IDX = voltages_upper self.U_SUP_IDX = list(range(self.EPSILON_IDX + 1, self.EPSILON_IDX + 1 + self._supply.voltage_len)) self._ode_epsilon_idx = self._motor_ode_idx[-1]
[docs] def simulate(self, action, *_, **__): # Docstring of superclass ode_state = self._ode_solver.y eps = ode_state[self._ode_epsilon_idx] if self.control_space == "dq": action = self.dq_to_abc_space(action, eps) i_in = self.dq_to_abc_space(self._electrical_motor.i_in(ode_state[self._ode_currents_idx]), eps) switching_times = self._converter.set_action(action, self._t) for t in switching_times[:-1]: i_sup = self._converter.i_sup(i_in) u_sup = self._supply.get_voltage(self._t, i_sup) u_in = self._converter.convert(i_in, self._ode_solver.t) u_in = [u * u_s for u in u_in for u_s in u_sup] u_dq = self.abc_to_dq_space(u_in, eps) self._ode_solver.set_f_params(u_dq) ode_state = self._ode_solver.integrate(t) eps = ode_state[self._ode_epsilon_idx] i_in = self.dq_to_abc_space(self._electrical_motor.i_in(ode_state[self._ode_currents_idx]), eps) i_sup = self._converter.i_sup(i_in) u_sup = self._supply.get_voltage(self._t, i_sup) u_in = self._converter.convert(i_in, self._ode_solver.t) u_in = [u * u_s for u in u_in for u_s in u_sup] u_dq = self.abc_to_dq_space(u_in, eps) self._ode_solver.set_f_params(u_dq) ode_state = self._ode_solver.integrate(self._t + self._tau) self._t = self._ode_solver.t self._k += 1 torque = self._electrical_motor.torque(ode_state[self._motor_ode_idx]) mechanical_state = ode_state[self._load_ode_idx] i_dq = ode_state[self._ode_currents_idx] i_abc = list(self.dq_to_abc_space(i_dq, eps)) eps = ode_state[self._ode_epsilon_idx] % (2 * np.pi) if eps > np.pi: eps -= 2 * np.pi system_state = np.concatenate((mechanical_state, [torque], i_abc, i_dq, u_in, u_dq, [eps], u_sup)) return system_state / self._limits
[docs] def reset(self, *_): # Docstring of superclass motor_state = self._electrical_motor.reset(state_space=self.state_space, state_positions=self.state_positions) mechanical_state = self._mechanical_load.reset( state_positions=self.state_positions, state_space=self.state_space, nominal_state=self.nominal_state, ) ode_state = np.concatenate((mechanical_state, motor_state)) u_sup = self.supply.reset() eps = ode_state[self._ode_epsilon_idx] if eps > np.pi: eps -= 2 * np.pi u_abc = self.converter.reset() u_abc = [u * u_s for u in u_abc for u_s in u_sup] u_dq = self.abc_to_dq_space(u_abc, eps) i_dq = ode_state[self._ode_currents_idx] i_abc = self.dq_to_abc_space(i_dq, eps) torque = self.electrical_motor.torque(motor_state) self._t = 0 self._k = 0 self._ode_solver.set_initial_value(ode_state, self._t) system_state = np.concatenate( ( mechanical_state, [torque], i_abc, i_dq, u_abc, u_dq, [eps], u_sup, ) ) return system_state / self._limits
class ExternallyExcitedSynchronousMotorSystem(SynchronousMotorSystem): """SCML-System that can be used with the externally excited synchronous motor (EESM)""" def _build_state_space(self, state_names): # Docstring of superclass low = -1 * np.ones_like(state_names, dtype=float) low[self.U_SUP_IDX] = 0.0 high = np.ones_like(state_names, dtype=float) return Box(low, high, dtype=np.float64) def _build_state_names(self): # Docstring of superclass return self._mechanical_load.state_names + [ "torque", "i_a", "i_b", "i_c", "i_sd", "i_sq", "i_e", "u_a", "u_b", "u_c", "u_sd", "u_sq", "u_e", "epsilon", "u_sup", ] def _set_indices(self): # Docstring of superclass self._omega_ode_idx = self._mechanical_load.OMEGA_IDX self._load_ode_idx = list(range(len(self._mechanical_load.state_names))) self._ode_currents_idx = list( range( self._load_ode_idx[-1] + 1, self._load_ode_idx[-1] + 1 + len(self._electrical_motor.CURRENTS), ) ) self._motor_ode_idx = self._ode_currents_idx self._motor_ode_idx += [self._motor_ode_idx[-1] + 1] self._ode_currents_idx = self._motor_ode_idx[:-1] self.OMEGA_IDX = self.mechanical_load.OMEGA_IDX self.TORQUE_IDX = len(self.mechanical_load.state_names) currents_lower = self.TORQUE_IDX + 1 currents_upper = currents_lower + 6 self.CURRENTS_IDX = list(range(currents_lower, currents_upper)) voltages_lower = currents_upper voltages_upper = voltages_lower + 6 self.VOLTAGES_IDX = list(range(voltages_lower, voltages_upper)) self.EPSILON_IDX = voltages_upper self.U_SUP_IDX = list(range(self.EPSILON_IDX + 1, self.EPSILON_IDX + 1 + self._supply.voltage_len)) self._ode_epsilon_idx = self._motor_ode_idx[-1] def simulate(self, action, *_, **__): # Docstring of superclass ode_state = self._ode_solver.y eps = ode_state[self._ode_epsilon_idx] i_in_dq_e = self._electrical_motor.i_in(ode_state[self._ode_currents_idx]) i_in_abc_e = list(self.dq_to_abc_space(i_in_dq_e[:2], eps)) + list(i_in_dq_e[2:]) switching_times = self._converter.set_action(action, self._t) for t in switching_times[:-1]: i_sup = self._converter.i_sup(i_in_abc_e) u_sup = self._supply.get_voltage(self._t, i_sup) u_in = self._converter.convert(i_in_abc_e, self._ode_solver.t) u_in = [u * u_s for u in u_in for u_s in u_sup] u_dq_e = list(self.abc_to_dq_space(u_in[:2], eps)) + u_in[2:] self._ode_solver.set_f_params(u_dq_e) ode_state = self._ode_solver.integrate(t) eps = ode_state[self._ode_epsilon_idx] i_in_dq_e = self._electrical_motor.i_in(ode_state[self._ode_currents_idx]) i_in_abc_e = list(self.dq_to_abc_space(i_in_dq_e[:2], eps)) + i_in_dq_e[2:] i_sup = self._converter.i_sup(i_in_abc_e) u_sup = self._supply.get_voltage(self._t, i_sup) u_in = self._converter.convert(i_in_abc_e, self._ode_solver.t) u_in = [u * u_s for u in u_in for u_s in u_sup] u_dq_e = np.concatenate((self.abc_to_dq_space(u_in[:3], eps), u_in[3:])) self._ode_solver.set_f_params(u_dq_e) ode_state = self._ode_solver.integrate(self._t + self._tau) self._t = self._ode_solver.t self._k += 1 torque = self._electrical_motor.torque(ode_state[self._motor_ode_idx]) mechanical_state = ode_state[self._load_ode_idx] i_dq_e = ode_state[self._ode_currents_idx] i_abc = list(self.dq_to_abc_space(i_dq_e[:2], eps)) eps = ode_state[self._ode_epsilon_idx] % (2 * np.pi) if eps > np.pi: eps -= 2 * np.pi system_state = np.concatenate((mechanical_state, [torque], i_abc, i_dq_e, u_in[:3], u_dq_e, [eps], u_sup)) return system_state / self._limits def reset(self, *_): # Docstring of superclass motor_state = self._electrical_motor.reset(state_space=self.state_space, state_positions=self.state_positions) mechanical_state = self._mechanical_load.reset( state_positions=self.state_positions, state_space=self.state_space, nominal_state=self.nominal_state, ) ode_state = np.concatenate((mechanical_state, motor_state)) u_sup = self.supply.reset() eps = ode_state[self._ode_epsilon_idx] if eps > np.pi: eps -= 2 * np.pi u_abc = self.converter.reset() u_abc = [u * u_s for u in u_abc for u_s in u_sup] u_dq = self.abc_to_dq_space(u_abc[:3], eps) i_dq = ode_state[self._ode_currents_idx] i_abc = self.dq_to_abc_space(i_dq[:2], eps) torque = self.electrical_motor.torque(motor_state) self._t = 0 self._k = 0 self._ode_solver.set_initial_value(ode_state, self._t) system_state = np.concatenate( ( mechanical_state, [torque], i_abc, i_dq, u_abc, u_dq, [eps], u_sup, ) ) return system_state / self._limits
[docs]class SquirrelCageInductionMotorSystem(ThreePhaseMotorSystem): """ SCML-System for the Squirrel Cage Induction Motor """ def __init__(self, control_space="abc", ode_solver="scipy.ode", **kwargs): """ Args: control_space(str):('abc' or 'dq') Choose, if actions the actions space is in dq or abc space kwargs: Further arguments to pass tp SCMLSystem """ super().__init__(ode_solver=ode_solver, **kwargs) self.control_space = control_space if control_space == "dq": self._action_space = Box(-1, 1, shape=(2,), dtype=np.float64) def _build_state_space(self, state_names): # Docstring of superclass low = -1 * np.ones_like(state_names, dtype=float) low[self.U_SUP_IDX] = 0.0 high = np.ones_like(state_names, dtype=float) return Box(low, high, dtype=np.float64) def _build_state_names(self): # Docstring of superclass return self._mechanical_load.state_names + [ "torque", "i_sa", "i_sb", "i_sc", "i_sd", "i_sq", "u_sa", "u_sb", "u_sc", "u_sd", "u_sq", "epsilon", "u_sup", ] def _set_indices(self): # Docstring of superclass super()._set_indices() self._motor_ode_idx += range( self._motor_ode_idx[-1] + 1, self._motor_ode_idx[-1] + 1 + len(self._electrical_motor.FLUXES), ) self._motor_ode_idx += [self._motor_ode_idx[-1] + 1] self._ode_currents_idx = self._motor_ode_idx[ self._electrical_motor.I_SALPHA_IDX : self._electrical_motor.I_SBETA_IDX + 1 ] self._ode_flux_idx = self._motor_ode_idx[ self._electrical_motor.PSI_RALPHA_IDX : self._electrical_motor.PSI_RBETA_IDX + 1 ] self.OMEGA_IDX = self.mechanical_load.OMEGA_IDX self.TORQUE_IDX = len(self.mechanical_load.state_names) currents_lower = self.TORQUE_IDX + 1 currents_upper = currents_lower + 5 self.CURRENTS_IDX = list(range(currents_lower, currents_upper)) voltages_lower = currents_upper voltages_upper = voltages_lower + 5 self.VOLTAGES_IDX = list(range(voltages_lower, voltages_upper)) self.EPSILON_IDX = voltages_upper self.U_SUP_IDX = list(range(self.EPSILON_IDX + 1, self.EPSILON_IDX + 1 + self._supply.voltage_len)) self._ode_epsilon_idx = self._motor_ode_idx[-1] def calculate_field_angle(self, state): psi_ralpha = state[self._ode_flux_idx[0]] psi_rbeta = state[self._ode_flux_idx[1]] eps_fs = np.arctan2(psi_rbeta, psi_ralpha) return eps_fs
[docs] def simulate(self, action, *_, **__): # Docstring of superclass ode_state = self._ode_solver.y eps_fs = self.calculate_field_angle(ode_state) if self.control_space == "dq": action = self.dq_to_abc_space(action, eps_fs) i_in = self.alphabeta_to_abc_space(self._electrical_motor.i_in(ode_state[self._ode_currents_idx])) switching_times = self._converter.set_action(action, self._t) for t in switching_times[:-1]: i_sup = self._converter.i_sup(i_in) u_sup = self._supply.get_voltage(self._t, i_sup) u_in = self._converter.convert(i_in, self._ode_solver.t) u_in = [u * u_s for u in u_in for u_s in u_sup] u_alphabeta = self.abc_to_alphabeta_space(u_in) self._ode_solver.set_f_params(u_alphabeta) ode_state = self._ode_solver.integrate(t) eps_fs = self.calculate_field_angle(ode_state) i_in = self.alphabeta_to_abc_space(self._electrical_motor.i_in(ode_state[self._ode_currents_idx])) i_sup = self._converter.i_sup(i_in) u_sup = self._supply.get_voltage(self._t, i_sup) u_in = self._converter.convert(i_in, self._ode_solver.t) u_in = [u * u_s for u in u_in for u_s in u_sup] u_dq = self.abc_to_dq_space(u_in, eps_fs) u_alphabeta = self.abc_to_alphabeta_space(u_in) self._ode_solver.set_f_params(u_alphabeta) ode_state = self._ode_solver.integrate(self._t + self._tau) self._t = self._ode_solver.t self._k += 1 torque = self._electrical_motor.torque(ode_state[self._motor_ode_idx]) mechanical_state = ode_state[self._load_ode_idx] i_dq = self.alphabeta_to_dq_space(ode_state[self._ode_currents_idx], eps_fs) i_abc = list(self.dq_to_abc_space(i_dq, eps_fs)) eps = ode_state[self._ode_epsilon_idx] % (2 * np.pi) if eps > np.pi: eps -= 2 * np.pi system_state = np.concatenate((mechanical_state, [torque], i_abc, i_dq, u_in, u_dq, [eps], u_sup)) return system_state / self._limits
[docs] def reset(self, *_): # Docstring of superclass mechanical_state = self._mechanical_load.reset( state_positions=self.state_positions, state_space=self.state_space, nominal_state=self.nominal_state, ) motor_state = self._electrical_motor.reset( state_space=self.state_space, state_positions=self.state_positions, omega=mechanical_state, ) ode_state = np.concatenate((mechanical_state, motor_state)) u_sup = self.supply.reset() eps = ode_state[self._ode_epsilon_idx] eps_fs = self.calculate_field_angle(ode_state) if eps > np.pi: eps -= 2 * np.pi u_abc = self.converter.reset() u_abc = [u * u_s for u in u_abc for u_s in u_sup] u_dq = self.abc_to_dq_space(u_abc, eps_fs) i_dq = self.alphabeta_to_dq_space(ode_state[self._ode_currents_idx], eps_fs) i_abc = self.dq_to_abc_space(i_dq, eps_fs) torque = self.electrical_motor.torque(motor_state) self._t = 0 self._k = 0 self._ode_solver.set_initial_value(ode_state, self._t) system_state = np.concatenate([mechanical_state, [torque], i_abc, i_dq, u_abc, u_dq, [eps], u_sup]) return system_state / self._limits
[docs]class DoublyFedInductionMotorSystem(ThreePhaseMotorSystem): """ SCML-System for the Doubly Fed Induction Motor """ def __init__(self, ode_solver="scipy.ode", **kwargs): """ Args: kwargs: Further arguments to pass tp SCMLSystem """ super().__init__(ode_solver=ode_solver, **kwargs) self.stator_voltage_space_idx = 0 self.stator_voltage_low_idx = 0 self.stator_voltage_high_idx = ( self.stator_voltage_low_idx + self._converter.subsignal_voltage_space_dims[self.stator_voltage_space_idx] ) self.rotor_voltage_space_idx = 1 self.rotor_voltage_low_idx = self.stator_voltage_high_idx self.rotor_voltage_high_idx = ( self.rotor_voltage_low_idx + self._converter.subsignal_voltage_space_dims[self.rotor_voltage_space_idx] ) def _set_limits(self): """Method to set the physical limits from the modules.""" for ind, state in enumerate(self._state_names): motor_lim = self._electrical_motor.limits.get(state, np.inf) mechanical_lim = self._mechanical_load.limits.get(state, np.inf) self._limits[ind] = min(motor_lim, mechanical_lim) self._limits[self._state_positions["u_sup"]] = self.supply.u_nominal def _build_state_space(self, state_names): # Docstring of superclass low = -1 * np.ones_like(state_names, dtype=float) low[self.U_SUP_IDX] = 0.0 high = np.ones_like(state_names, dtype=float) return Box(low, high, dtype=np.float64) def _build_state_names(self): # Docstring of superclass names_l = self._mechanical_load.state_names + [ "torque", "i_sa", "i_sb", "i_sc", "i_sd", "i_sq", "i_ra", "i_rb", "i_rc", "i_rd", "i_rq", "u_sa", "u_sb", "u_sc", "u_sd", "u_sq", "u_ra", "u_rb", "u_rc", "u_rd", "u_rq", "epsilon", "u_sup", ] return names_l def _set_indices(self): # Docstring of superclass super()._set_indices() self._motor_ode_idx += range( self._motor_ode_idx[-1] + 1, self._motor_ode_idx[-1] + 1 + len(self._electrical_motor.FLUXES), ) self._motor_ode_idx += [self._motor_ode_idx[-1] + 1] self._ode_currents_idx = self._motor_ode_idx[ self._electrical_motor.I_SALPHA_IDX : self._electrical_motor.I_SBETA_IDX + 1 ] self._ode_flux_idx = self._motor_ode_idx[ self._electrical_motor.PSI_RALPHA_IDX : self._electrical_motor.PSI_RBETA_IDX + 1 ] self.OMEGA_IDX = self.mechanical_load.OMEGA_IDX self.TORQUE_IDX = len(self.mechanical_load.state_names) currents_lower = self.TORQUE_IDX + 1 currents_upper = currents_lower + 10 self.CURRENTS_IDX = list(range(currents_lower, currents_upper)) voltages_lower = currents_upper voltages_upper = voltages_lower + 10 self.VOLTAGES_IDX = list(range(voltages_lower, voltages_upper)) self.EPSILON_IDX = voltages_upper self.U_SUP_IDX = list(range(self.EPSILON_IDX + 1, self.EPSILON_IDX + 1 + self._supply.voltage_len)) self._ode_epsilon_idx = self._motor_ode_idx[-1] def calculate_field_angle(self, state): # field angle is calculated from states psi_ralpha = state[self._motor_ode_idx[self._electrical_motor.PSI_RALPHA_IDX]] psi_rbeta = state[self._motor_ode_idx[self._electrical_motor.PSI_RBETA_IDX]] eps_fs = np.arctan2(psi_rbeta, psi_ralpha) return eps_fs def calculate_rotor_current(self, state): # rotor current is calculated from states mp = self._electrical_motor.motor_parameter l_r = mp["l_m"] + mp["l_sigr"] i_salpha = state[self._motor_ode_idx[self._electrical_motor.I_SALPHA_IDX]] i_sbeta = state[self._motor_ode_idx[self._electrical_motor.I_SBETA_IDX]] psi_ralpha = state[self._motor_ode_idx[self._electrical_motor.PSI_RALPHA_IDX]] psi_rbeta = state[self._motor_ode_idx[self._electrical_motor.PSI_RBETA_IDX]] i_ralpha = 1 / l_r * psi_ralpha - mp["l_m"] / l_r * i_salpha i_rbeta = 1 / l_r * psi_rbeta - mp["l_m"] / l_r * i_sbeta return [i_ralpha, i_rbeta]
[docs] def simulate(self, action, *_, **__): # Docstring of superclass # Coordinate Systems used here: # alphabeta refers to the stator-fixed two-phase reference frame # gammadelta refers to the rotor-fixed two-phase reference frame # abc refers to the stator-fixed three-phase reference frame # def refers to the rotor-fixed three-phase reference frame # dq refers to the field-oriented (two-phase) reference frame # e.g. u_rdef is the rotor voltage representation in the rotor-fixed three-phase reference frame # u_rabc ist the rotor voltage representation in the stator-fixed three-phase reference frame ode_state = self._ode_solver.y eps_field = self.calculate_field_angle(ode_state) eps_el = ode_state[self._ode_epsilon_idx] i_sabc = self.alphabeta_to_abc_space(self._electrical_motor.i_in(ode_state[self._ode_currents_idx])) i_rdef = self.alphabeta_to_abc_space(self.calculate_rotor_current(ode_state)) switching_times = self._converter.set_action(action, self._t) for t in switching_times[:-1]: i_sup = self._converter.i_sup(np.concatenate((i_sabc, i_rdef))) u_sup = self._supply.get_voltage(self._t, i_sup) u_in = self._converter.convert(np.concatenate([i_sabc, i_rdef]).tolist(), self._ode_solver.t) u_in = [u * u_s for u in u_in for u_s in u_sup] u_sabc = u_in[self.stator_voltage_low_idx : self.stator_voltage_high_idx] u_rdef = u_in[self.rotor_voltage_low_idx : self.rotor_voltage_high_idx] u_rdq = self.abc_to_dq_space(u_rdef, eps_field - eps_el) u_salphabeta = self.abc_to_alphabeta_space(u_sabc) u_ralphabeta = self.dq_to_alphabeta_space(u_rdq, eps_field) u_sr_alphabeta = np.array([u_salphabeta, u_ralphabeta]) self._ode_solver.set_f_params(u_sr_alphabeta) ode_state = self._ode_solver.integrate(t) eps_field = self.calculate_field_angle(ode_state) eps_el = ode_state[self._ode_epsilon_idx] i_sabc = self.alphabeta_to_abc_space(self._electrical_motor.i_in(ode_state[self._ode_currents_idx])) i_rdef = self.alphabeta_to_abc_space(self.calculate_rotor_current(ode_state)) i_sup = self._converter.i_sup(np.concatenate((i_sabc, i_rdef))) u_sup = self._supply.get_voltage(self._t, i_sup) u_in = self._converter.convert(np.concatenate([i_sabc, i_rdef]).tolist(), self._ode_solver.t) u_in = [u * u_s for u in u_in for u_s in u_sup] u_sabc = u_in[self.stator_voltage_low_idx : self.stator_voltage_high_idx] u_rdef = u_in[self.rotor_voltage_low_idx : self.rotor_voltage_high_idx] u_sdq = self.abc_to_dq_space(u_sabc, eps_field) u_rdq = self.abc_to_dq_space(u_rdef, eps_field - eps_el) u_salphabeta = self.abc_to_alphabeta_space(u_sabc) u_ralphabeta = self.dq_to_alphabeta_space(u_rdq, eps_field) u_sr_alphabeta = np.array([u_salphabeta, u_ralphabeta]) self._ode_solver.set_f_params(u_sr_alphabeta) ode_state = self._ode_solver.integrate(self._t + self._tau) self._t = self._ode_solver.t self._k += 1 torque = self._electrical_motor.torque(ode_state[self._motor_ode_idx]) mechanical_state = ode_state[self._load_ode_idx] i_sdq = self.alphabeta_to_dq_space(ode_state[self._ode_currents_idx], eps_field) i_sabc = list(self.dq_to_abc_space(i_sdq, eps_field)) i_rdq = self.alphabeta_to_dq_space(self.calculate_rotor_current(ode_state), eps_field) i_rdef = list(self.dq_to_abc_space(i_rdq, eps_field - eps_el)) eps_el = ode_state[self._ode_epsilon_idx] % (2 * np.pi) if eps_el > np.pi: eps_el -= 2 * np.pi system_state = np.concatenate( ( mechanical_state, [torque], i_sabc, i_sdq, i_rdef, i_rdq, u_sabc, u_sdq, u_rdef, u_rdq, [eps_el], u_sup, ) ) return system_state / self._limits
[docs] def reset(self, *_): # Docstring of superclass mechanical_state = self._mechanical_load.reset( state_positions=self.state_positions, state_space=self.state_space, nominal_state=self.nominal_state, ) motor_state = self._electrical_motor.reset( state_space=self.state_space, state_positions=self.state_positions, omega=mechanical_state, ) ode_state = np.concatenate((mechanical_state, motor_state)) u_sup = self.supply.reset() eps_el = ode_state[self._ode_epsilon_idx] eps_field = self.calculate_field_angle(ode_state) if eps_el > np.pi: eps_el -= 2 * np.pi if eps_field > np.pi: eps_field -= 2 * np.pi u_sr_abcdef = self.converter.reset() u_sr_abcdef = [u * u_s for u in u_sr_abcdef for u_s in u_sup] u_sabc = u_sr_abcdef[self.stator_voltage_low_idx : self.stator_voltage_high_idx] u_rdef = u_sr_abcdef[self.rotor_voltage_low_idx : self.rotor_voltage_high_idx] u_sdq = self.abc_to_dq_space(u_sabc, eps_field) u_rdq = self.abc_to_dq_space(u_rdef, eps_field - eps_el) i_sdq = self.alphabeta_to_dq_space(ode_state[self._ode_currents_idx], eps_field) i_sabc = self.dq_to_abc_space(i_sdq, eps_field) i_rdq = self.alphabeta_to_dq_space(self.calculate_rotor_current(ode_state), eps_field - eps_el) i_rdef = self.dq_to_abc_space(i_rdq, eps_field - eps_el) torque = self.electrical_motor.torque(motor_state) self._t = 0 self._k = 0 self._ode_solver.set_initial_value(ode_state, self._t) system_state = np.concatenate( [ mechanical_state, [torque], i_sabc, i_sdq, i_rdef, i_rdq, u_sabc, u_sdq, u_rdef, u_rdq, [eps_el], u_sup, ] ) return system_state / self._limits