From f9bffcd0aae60ae9a9805afd4426e98fdbfadf30 Mon Sep 17 00:00:00 2001 From: Stefan Arndt Date: Fri, 10 Nov 2023 14:30:32 +0100 Subject: [PATCH 01/51] formated with python ruff --- .../classic_controllers.py | 749 ++++++---- .../classic_controllers_dc_motor_example.py | 33 +- .../classic_controllers_ind_motor_example.py | 25 +- ...classic_controllers_synch_motor_example.py | 25 +- .../controllers/cascaded_controller.py | 220 ++- .../controllers/cascaded_foc_controller.py | 266 ++-- .../continuous_action_controller.py | 137 +- .../controllers/continuous_controller.py | 4 +- .../controllers/dicrete_action_controller.py | 84 +- .../controllers/discrete_controller.py | 12 +- .../controllers/flux_observer.py | 44 +- .../controllers/foc_controller.py | 169 ++- .../induction_motor_cascaded_foc.py | 231 ++-- .../controllers/induction_motor_foc.py | 135 +- ...tion_motor_torque_to_current_conversion.py | 223 +-- .../controllers/on_off_controller.py | 14 +- .../controllers/pi_controller.py | 18 +- .../controllers/pid_controller.py | 24 +- .../controllers/plot_external_data.py | 19 +- .../controllers/three_point_controller.py | 38 +- .../torque_to_current_conversion.py | 501 ++++--- ...om_classic_controllers_dc_motor_example.py | 53 +- ...m_classic_controllers_ind_motor_example.py | 31 +- ...classic_controllers_synch_motor_example.py | 64 +- examples/classic_controllers/external_plot.py | 132 +- .../externally_referenced_state_plot.py | 36 +- ...ation_test_classic_controllers_dc_motor.py | 60 +- .../external_speed_profile.py | 58 +- .../scim_ideal_grid_simulation.py | 37 +- .../userdefined_initialization.py | 72 +- .../ddpg_pmsm_dq_current_control.py | 135 +- .../ddpg_series_omega_control.py | 55 +- .../dqn_series_current_control.py | 71 +- gym_electric_motor/__init__.py | 334 ++--- gym_electric_motor/callbacks.py | 90 +- gym_electric_motor/constraints.py | 14 +- gym_electric_motor/core.py | 151 +- gym_electric_motor/envs/__init__.py | 68 +- .../cont_cc_extex_dc_env.py | 90 +- .../cont_sc_extex_dc_env.py | 85 +- .../cont_tc_extex_dc_env.py | 81 +- .../finite_cc_extex_dc_env.py | 95 +- .../finite_sc_extex_dc_env.py | 85 +- .../finite_tc_extex_dc_env.py | 80 +- .../cont_cc_permex_dc_env.py | 81 +- .../cont_sc_permex_dc_env.py | 89 +- .../cont_tc_permex_dc_env.py | 80 +- .../finite_cc_permex_dc_env.py | 82 +- .../finite_sc_permex_dc_env.py | 87 +- .../finite_tc_permex_dc_env.py | 83 +- .../cont_cc_series_dc_env.py | 74 +- .../cont_sc_series_dc_env.py | 225 +-- .../cont_tc_series_dc_env.py | 218 +-- .../finite_cc_series_dc_env.py | 75 +- .../finite_sc_series_dc_env.py | 82 +- .../finite_tc_series_dc_env.py | 74 +- .../cont_cc_shunt_dc_env.py | 218 +-- .../cont_sc_shunt_dc_env.py | 225 +-- .../cont_tc_shunt_dc_env.py | 218 +-- .../finite_cc_shunt_dc_env.py | 226 +-- .../finite_sc_shunt_dc_env.py | 225 +-- .../finite_tc_shunt_dc_env.py | 219 +-- .../envs/gym_eesm/cont_cc_eesm_env.py | 85 +- .../envs/gym_eesm/cont_sc_eesm_env.py | 85 +- .../envs/gym_eesm/cont_tc_eesm_env.py | 80 +- .../envs/gym_eesm/finite_cc_eesm_env.py | 94 +- .../envs/gym_eesm/finite_sc_eesm_env.py | 75 +- .../envs/gym_eesm/finite_tc_eesm_env.py | 81 +- gym_electric_motor/envs/gym_im/__init__.py | 40 +- .../cont_cc_dfim_env.py | 79 +- .../cont_sc_dfim_env.py | 85 +- .../cont_tc_dfim_env.py | 77 +- .../finite_cc_dfim_env.py | 83 +- .../finite_sc_dfim_env.py | 87 +- .../finite_tc_dfim_env.py | 80 +- .../cont_cc_scim_env.py | 233 ++-- .../cont_sc_scim_env.py | 237 ++-- .../cont_tc_scim_env.py | 229 +-- .../finite_cc_scim_env.py | 238 ++-- .../finite_sc_scim_env.py | 242 ++-- .../finite_tc_scim_env.py | 235 ++-- .../envs/gym_pmsm/cont_cc_pmsm_env.py | 77 +- .../envs/gym_pmsm/cont_sc_pmsm_env.py | 80 +- .../envs/gym_pmsm/cont_tc_pmsm_env.py | 73 +- .../envs/gym_pmsm/finite_cc_pmsm_env.py | 82 +- .../envs/gym_pmsm/finite_sc_pmsm_env.py | 85 +- .../envs/gym_pmsm/finite_tc_pmsm_env.py | 79 +- .../gym_srm/srm_continuous_control_env.py | 4 +- .../envs/gym_srm/srm_finite_control_env.py | 4 +- .../envs/gym_synrm/cont_cc_synrm_env.py | 77 +- .../envs/gym_synrm/cont_sc_synrm_env.py | 81 +- .../envs/gym_synrm/cont_tc_synrm_env.py | 73 +- .../envs/gym_synrm/finite_cc_synrm_env.py | 82 +- .../envs/gym_synrm/finite_sc_synrm_env.py | 86 +- .../envs/gym_synrm/finite_tc_synrm_env.py | 79 +- .../cos_sin_processor.py | 36 +- .../current_sum_processor.py | 26 +- .../dead_time_processor.py | 16 +- .../dq_to_abc_action_processor.py | 51 +- .../physical_system_wrappers/flux_observer.py | 56 +- .../physical_system_wrapper.py | 4 +- .../state_noise_processor.py | 28 +- .../physical_systems/__init__.py | 172 ++- .../physical_systems/converters.py | 154 ++- .../electric_motors/__init__.py | 1 - .../dc_externally_excited_motor.py | 37 +- .../electric_motors/dc_motor.py | 203 +-- .../dc_permanently_excited_motor.py | 60 +- .../electric_motors/dc_series_motor.py | 147 +- .../electric_motors/dc_shunt_motor.py | 76 +- .../doubly_fed_induction_motor.py | 252 ++-- .../electric_motors/electric_motor.py | 171 +-- .../externally_excited_synchronous_motor.py | 233 +++- .../electric_motors/induction_motor.py | 514 ++++--- .../permanent_magnet_synchronous_motor.py | 124 +- .../squirrel_cage_induction_motor.py | 224 +-- .../electric_motors/synchronous_motor.py | 58 +- .../synchronous_reluctance_motor.py | 271 ++-- .../electric_motors/three_phase_motor.py | 24 +- .../mechanical_loads/constant_speed_load.py | 20 +- .../mechanical_loads/external_speed_load.py | 31 +- .../mechanical_loads/mechanical_load.py | 66 +- .../ornstein_uhlenbeck_load.py | 12 +- .../polynomial_static_load.py | 44 +- .../physical_systems/physical_systems.py | 600 +++++--- .../physical_systems/solvers.py | 38 +- .../physical_systems/voltage_supplies.py | 108 +- .../reference_generators/__init__.py | 22 +- .../const_reference_generator.py | 6 +- .../laplace_process_reference_generator.py | 5 +- .../multiple_reference_generator.py | 53 +- .../sawtooth_reference_generator.py | 37 +- .../sinusoidal_reference_generator.py | 36 +- .../step_reference_generator.py | 29 +- .../subepisoded_reference_generator.py | 30 +- .../switched_reference_generator.py | 42 +- .../triangle_reference_generator.py | 41 +- .../wiener_process_reference_generator.py | 9 +- .../zero_reference_generator.py | 4 +- .../reward_functions/__init__.py | 3 +- .../weighted_sum_of_errors.py | 32 +- gym_electric_motor/utils.py | 18 +- gym_electric_motor/visualization/__init__.py | 4 +- .../visualization/console_printer.py | 28 +- .../visualization/motor_dashboard.py | 114 +- .../motor_dashboard_plots/action_plot.py | 20 +- .../motor_dashboard_plots/base_plots.py | 36 +- .../cumulative_constraint_violation_plot.py | 2 +- .../episode_length_plot.py | 2 +- .../mean_episode_reward_plot.py | 14 +- .../motor_dashboard_plots/reward_plot.py | 8 +- .../motor_dashboard_plots/state_plot.py | 102 +- tests/conf.py | 474 +++++-- .../test_environment_execution.py | 55 +- .../test_environment_seeding.py | 70 +- tests/integration_tests/test_integration.py | 71 +- tests/test_callbacks.py | 64 +- .../test_constraints/test_limit_constraint.py | 67 +- .../test_squared_constraint.py | 100 +- tests/test_core.py | 331 +++-- tests/test_environments/test_environments.py | 35 +- .../test_cos_sin_processor.py | 28 +- .../test_dead_time_processor.py | 80 +- .../test_dq_to_abc_action_processor.py | 38 +- .../test_flux_observer.py | 36 +- .../test_physical_system_wrapper.py | 11 +- .../test_physical_systems/test_converters.py | 1226 ++++++++++++----- tests/test_physical_systems/test_load.py | 55 +- .../test_mechanical_loads.py | 198 +-- .../test_physical_systems.py | 138 +- tests/test_physical_systems/test_solvers.py | 217 ++- .../test_voltage_supplies.py | 178 +-- tests/test_random_component.py | 11 +- .../test_reference_generators.py | 699 +++++++--- .../test_reward_functions.py | 66 +- .../test_weighted_sum_of_errors.py | 246 +++- tests/test_utils.py | 103 +- tests/testing_utils.py | 309 +++-- tests/utils/physical_system_test_wrapper.py | 1 - 179 files changed, 13413 insertions(+), 6984 deletions(-) diff --git a/examples/classic_controllers/classic_controllers.py b/examples/classic_controllers/classic_controllers.py index e18b3f4b..e24ce6b4 100644 --- a/examples/classic_controllers/classic_controllers.py +++ b/examples/classic_controllers/classic_controllers.py @@ -1,7 +1,16 @@ from gymnasium.spaces import Discrete, Box, MultiDiscrete -from gym_electric_motor.physical_systems import SynchronousMotorSystem, DcMotorSystem, DcSeriesMotor, \ - DcExternallyExcitedMotor, DoublyFedInductionMotorSystem, SquirrelCageInductionMotorSystem -from gym_electric_motor.reference_generators import MultipleReferenceGenerator, SwitchedReferenceGenerator +from gym_electric_motor.physical_systems import ( + SynchronousMotorSystem, + DcMotorSystem, + DcSeriesMotor, + DcExternallyExcitedMotor, + DoublyFedInductionMotorSystem, + SquirrelCageInductionMotorSystem, +) +from gym_electric_motor.reference_generators import ( + MultipleReferenceGenerator, + SwitchedReferenceGenerator, +) from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor import envs @@ -19,13 +28,16 @@ from controllers.foc_controller import FieldOrientedController from controllers.cascaded_foc_controller import CascadedFieldOrientedController from controllers.induction_motor_foc import InductionMotorFieldOrientedController -from controllers.induction_motor_cascaded_foc import InductionMotorCascadedFieldOrientedController +from controllers.induction_motor_cascaded_foc import ( + InductionMotorCascadedFieldOrientedController, +) from externally_referenced_state_plot import ExternallyReferencedStatePlot from external_plot import ExternalPlot import numpy as np + class Controller: """This is the base class for every controller along with the motor environments.""" @@ -47,51 +59,79 @@ def make(cls, environment, stages=None, **controller_kwargs): """ _controllers = { - 'pi_controller': [ContinuousActionController, ContinuousController, PIController], - 'pid_controller': [ContinuousActionController, ContinuousController, PIDController], - 'on_off': [DiscreteActionController, DiscreteController, OnOffController], - 'three_point': [DiscreteActionController, DiscreteController, ThreePointController], - 'cascaded_controller': [CascadedController], - 'foc_controller': [FieldOrientedController], - 'cascaded_foc_controller': [CascadedFieldOrientedController], - 'foc_rotor_flux_observer': [InductionMotorFieldOrientedController], - 'cascaded_foc_rotor_flux_observer': [InductionMotorCascadedFieldOrientedController], - } + "pi_controller": [ + ContinuousActionController, + ContinuousController, + PIController, + ], + "pid_controller": [ + ContinuousActionController, + ContinuousController, + PIDController, + ], + "on_off": [DiscreteActionController, DiscreteController, OnOffController], + "three_point": [ + DiscreteActionController, + DiscreteController, + ThreePointController, + ], + "cascaded_controller": [CascadedController], + "foc_controller": [FieldOrientedController], + "cascaded_foc_controller": [CascadedFieldOrientedController], + "foc_rotor_flux_observer": [InductionMotorFieldOrientedController], + "cascaded_foc_rotor_flux_observer": [ + InductionMotorCascadedFieldOrientedController + ], + } controller_kwargs = cls.reference_states(environment, **controller_kwargs) controller_kwargs = cls.get_visualization(environment, **controller_kwargs) if stages is not None: - controller_type, stages = cls.find_controller_type(environment, stages, **controller_kwargs) - assert controller_type in _controllers.keys(), f'Controller {controller_type} unknown' - stages = cls.automated_gain(environment, stages, controller_type, _controllers, **controller_kwargs) - controller = _controllers[controller_type][0](environment, stages, _controllers, **controller_kwargs) + controller_type, stages = cls.find_controller_type( + environment, stages, **controller_kwargs + ) + assert ( + controller_type in _controllers.keys() + ), f"Controller {controller_type} unknown" + stages = cls.automated_gain( + environment, stages, controller_type, _controllers, **controller_kwargs + ) + controller = _controllers[controller_type][0]( + environment, stages, _controllers, **controller_kwargs + ) else: - controller_type, stages = cls.automated_controller_design(environment, **controller_kwargs) - stages = cls.automated_gain(environment, stages, controller_type, _controllers, **controller_kwargs) - controller = _controllers[controller_type][0](environment, stages, _controllers, **controller_kwargs) + controller_type, stages = cls.automated_controller_design( + environment, **controller_kwargs + ) + stages = cls.automated_gain( + environment, stages, controller_type, _controllers, **controller_kwargs + ) + controller = _controllers[controller_type][0]( + environment, stages, _controllers, **controller_kwargs + ) return controller @staticmethod def get_visualization(environment, **controller_kwargs): """This method separates external_plots and external_ref_plots. It also checks if a MotorDashboard is used.""" - if 'external_plot' in controller_kwargs.keys(): + if "external_plot" in controller_kwargs.keys(): ext_plot = [] ref_plot = [] - for external_plots in controller_kwargs['external_plot']: + for external_plots in controller_kwargs["external_plot"]: if isinstance(external_plots, ExternalPlot): ext_plot.append(external_plots) elif isinstance(external_plots, ExternallyReferencedStatePlot): ref_plot.append(external_plots) - controller_kwargs['external_plot'] = ext_plot - controller_kwargs['external_ref_plots'] = ref_plot + controller_kwargs["external_plot"] = ext_plot + controller_kwargs["external_ref_plots"] = ref_plot for visualization in environment.visualizations: if isinstance(visualization, MotorDashboard): - controller_kwargs['update_interval'] = visualization.update_interval - controller_kwargs['visualization'] = True + controller_kwargs["update_interval"] = visualization.update_interval + controller_kwargs["visualization"] = True return controller_kwargs - controller_kwargs['visualization'] = False + controller_kwargs["visualization"] = False return controller_kwargs @staticmethod @@ -99,7 +139,6 @@ def reference_states(environment, **controller_kwargs): """This method searches the environment for all referenced states and writes them to an array.""" ref_states = [] if isinstance(environment.reference_generator, MultipleReferenceGenerator): - for rg in environment.reference_generator._sub_generators: if isinstance(rg, SwitchedReferenceGenerator): ref_states.append(rg._sub_generators[0]._reference_state) @@ -107,10 +146,12 @@ def reference_states(environment, **controller_kwargs): ref_states.append(rg._reference_state) elif isinstance(environment.reference_generator, SwitchedReferenceGenerator): - ref_states.append(environment.reference_generator._sub_generators[0]._reference_state) + ref_states.append( + environment.reference_generator._sub_generators[0]._reference_state + ) else: ref_states.append(environment.reference_generator._reference_state) - controller_kwargs['ref_states'] = np.array(ref_states) + controller_kwargs["ref_states"] = np.array(ref_states) return controller_kwargs @staticmethod @@ -123,44 +164,48 @@ def find_controller_type(environment, stages, **controller_kwargs): if type(stages[0]) is list: stages = stages[0] if len(stages) > 1: - controller_type = 'cascaded_controller' + controller_type = "cascaded_controller" else: - controller_type = stages[0]['controller_type'] + controller_type = stages[0]["controller_type"] else: - controller_type = stages[0]['controller_type'] + controller_type = stages[0]["controller_type"] else: if type(stages) is dict: - controller_type = stages['controller_type'] + controller_type = stages["controller_type"] _stages = [stages] else: controller_type = stages - _stages = [{'controller_type': stages}] + _stages = [{"controller_type": stages}] elif isinstance(environment.physical_system.unwrapped, SynchronousMotorSystem): if len(stages) == 2: - if len(stages[1]) == 1 and 'i_sq' in controller_kwargs['ref_states']: - controller_type = 'foc_controller' + if len(stages[1]) == 1 and "i_sq" in controller_kwargs["ref_states"]: + controller_type = "foc_controller" else: - controller_type = 'cascaded_foc_controller' + controller_type = "cascaded_foc_controller" else: - controller_type = 'cascaded_foc_controller' + controller_type = "cascaded_foc_controller" - elif isinstance(environment.physical_system.unwrapped, SquirrelCageInductionMotorSystem): + elif isinstance( + environment.physical_system.unwrapped, SquirrelCageInductionMotorSystem + ): if len(stages) == 2: - if len(stages[1]) == 1 and 'i_sq' in controller_kwargs['ref_states']: - controller_type = 'foc_rotor_flux_observer' + if len(stages[1]) == 1 and "i_sq" in controller_kwargs["ref_states"]: + controller_type = "foc_rotor_flux_observer" else: - controller_type = 'cascaded_foc_rotor_flux_observer' + controller_type = "cascaded_foc_rotor_flux_observer" else: - controller_type = 'cascaded_foc_rotor_flux_observer' + controller_type = "cascaded_foc_rotor_flux_observer" - elif isinstance(environment.physical_system.unwrapped, DoublyFedInductionMotorSystem): + elif isinstance( + environment.physical_system.unwrapped, DoublyFedInductionMotorSystem + ): if len(stages) == 2: - if len(stages[1]) == 1 and 'i_sq' in controller_kwargs['ref_states']: - controller_type = 'foc_rotor_flux_observer' + if len(stages[1]) == 1 and "i_sq" in controller_kwargs["ref_states"]: + controller_type = "foc_rotor_flux_observer" else: - controller_type = 'cascaded_foc_rotor_flux_observer' + controller_type = "cascaded_foc_rotor_flux_observer" else: - controller_type = 'cascaded_foc_rotor_flux_observer' + controller_type = "cascaded_foc_rotor_flux_observer" return controller_type, _stages @@ -169,97 +214,151 @@ def automated_controller_design(environment, **controller_kwargs): """This method automatically designs the controller based on the given motor environment and control task.""" action_space_type = type(environment.action_space) - ref_states = controller_kwargs['ref_states'] + ref_states = controller_kwargs["ref_states"] stages = [] - if isinstance(environment.physical_system.unwrapped, DcMotorSystem): # Checking type of motor - - if 'omega' in ref_states or 'torque' in ref_states: # Checking control task - controller_type = 'cascaded_controller' + if isinstance( + environment.physical_system.unwrapped, DcMotorSystem + ): # Checking type of motor + if "omega" in ref_states or "torque" in ref_states: # Checking control task + controller_type = "cascaded_controller" for i in range(len(stages), 2): if i == 0: - if action_space_type is Box: # Checking type of output stage (finite / cont) - stages.append({'controller_type': 'pi_controller'}) + if ( + action_space_type is Box + ): # Checking type of output stage (finite / cont) + stages.append({"controller_type": "pi_controller"}) else: - stages.append({'controller_type': 'three_point'}) + stages.append({"controller_type": "three_point"}) else: - stages.append({'controller_type': 'pi_controller'}) # Adding PI-Controller for overlaid stages + stages.append( + {"controller_type": "pi_controller"} + ) # Adding PI-Controller for overlaid stages - elif 'i' in ref_states or 'i_a' in ref_states: + elif "i" in ref_states or "i_a" in ref_states: # Checking type of output stage (finite / cont) if action_space_type is Discrete or action_space_type is MultiDiscrete: - stages.append({'controller_type': 'three_point'}) + stages.append({"controller_type": "three_point"}) elif action_space_type is Box: - stages.append({'controller_type': 'pi_controller'}) - controller_type = stages[0]['controller_type'] + stages.append({"controller_type": "pi_controller"}) + controller_type = stages[0]["controller_type"] # Add stage for i_e current of the ExtExDC - if isinstance(environment.physical_system.electrical_motor, DcExternallyExcitedMotor): + if isinstance( + environment.physical_system.electrical_motor, DcExternallyExcitedMotor + ): if action_space_type is Box: - stages = [stages, [{'controller_type': 'pi_controller'}]] + stages = [stages, [{"controller_type": "pi_controller"}]] else: - stages = [stages, [{'controller_type': 'three_point'}]] + stages = [stages, [{"controller_type": "three_point"}]] elif isinstance(environment.physical_system.unwrapped, SynchronousMotorSystem): - if 'i_sq' in ref_states or 'torque' in ref_states: # Checking control task - controller_type = 'foc_controller' if 'i_sq' in ref_states else 'cascaded_foc_controller' + if "i_sq" in ref_states or "torque" in ref_states: # Checking control task + controller_type = ( + "foc_controller" + if "i_sq" in ref_states + else "cascaded_foc_controller" + ) if action_space_type is Discrete: - stages = [[{'controller_type': 'on_off'}], [{'controller_type': 'on_off'}], - [{'controller_type': 'on_off'}]] + stages = [ + [{"controller_type": "on_off"}], + [{"controller_type": "on_off"}], + [{"controller_type": "on_off"}], + ] else: - stages = [[{'controller_type': 'pi_controller'}, {'controller_type': 'pi_controller'}]] - elif 'omega' in ref_states: - controller_type = 'cascaded_foc_controller' + stages = [ + [ + {"controller_type": "pi_controller"}, + {"controller_type": "pi_controller"}, + ] + ] + elif "omega" in ref_states: + controller_type = "cascaded_foc_controller" if action_space_type is Discrete: - stages = [[{'controller_type': 'on_off'}], [{'controller_type': 'on_off'}], - [{'controller_type': 'on_off'}], [{'controller_type': 'pi_controller'}]] + stages = [ + [{"controller_type": "on_off"}], + [{"controller_type": "on_off"}], + [{"controller_type": "on_off"}], + [{"controller_type": "pi_controller"}], + ] else: - stages = [[{'controller_type': 'pi_controller'}, - {'controller_type': 'pi_controller'}], [{'controller_type': 'pi_controller'}]] - - elif isinstance(environment.physical_system.unwrapped, (SquirrelCageInductionMotorSystem, DoublyFedInductionMotorSystem)): - if 'i_sq' in ref_states or 'torque' in ref_states: - controller_type = 'foc_rotor_flux_observer' if 'i_sq' in ref_states else 'cascaded_foc_rotor_flux_observer' + stages = [ + [ + {"controller_type": "pi_controller"}, + {"controller_type": "pi_controller"}, + ], + [{"controller_type": "pi_controller"}], + ] + + elif isinstance( + environment.physical_system.unwrapped, + (SquirrelCageInductionMotorSystem, DoublyFedInductionMotorSystem), + ): + if "i_sq" in ref_states or "torque" in ref_states: + controller_type = ( + "foc_rotor_flux_observer" + if "i_sq" in ref_states + else "cascaded_foc_rotor_flux_observer" + ) if action_space_type is Discrete: - stages = [[{'controller_type': 'on_off'}], [{'controller_type': 'on_off'}], - [{'controller_type': 'on_off'}]] + stages = [ + [{"controller_type": "on_off"}], + [{"controller_type": "on_off"}], + [{"controller_type": "on_off"}], + ] else: - stages = [[{'controller_type': 'pi_controller'}, {'controller_type': 'pi_controller'}]] - elif 'omega' in ref_states: - controller_type = 'cascaded_foc_rotor_flux_observer' + stages = [ + [ + {"controller_type": "pi_controller"}, + {"controller_type": "pi_controller"}, + ] + ] + elif "omega" in ref_states: + controller_type = "cascaded_foc_rotor_flux_observer" if action_space_type is Discrete: - stages = [[{'controller_type': 'on_off'}], [{'controller_type': 'on_off'}], - [{'controller_type': 'on_off'}], [{'controller_type': 'pi_controller'}]] + stages = [ + [{"controller_type": "on_off"}], + [{"controller_type": "on_off"}], + [{"controller_type": "on_off"}], + [{"controller_type": "pi_controller"}], + ] else: - stages = [[{'controller_type': 'pi_controller'}, - {'controller_type': 'pi_controller'}], [{'controller_type': 'pi_controller'}]] + stages = [ + [ + {"controller_type": "pi_controller"}, + {"controller_type": "pi_controller"}, + ], + [{"controller_type": "pi_controller"}], + ] else: - controller_type = 'foc_controller' + controller_type = "foc_controller" return controller_type, stages @staticmethod - def automated_gain(environment, stages, controller_type, _controllers, **controller_kwargs): + def automated_gain( + environment, stages, controller_type, _controllers, **controller_kwargs + ): """ - This method automatically parameterizes a given controller design if the parameter automated_gain is True - (default True), based on the design according to the symmetric optimum (SO). Further information about the - design according to the SO can be found in the following paper (https://ieeexplore.ieee.org/document/55967). - - Args: - environment: gym-electric-motor environment - stages: list of the stages of the controller - controller_type: string of the used controller type from the dictionary _controllers - _controllers: dictionary of all possible controllers and controller stages - controller_kwargs: further arguments of the controller - - Returns: - list of stages, which are completely parameterized + This method automatically parameterizes a given controller design if the parameter automated_gain is True + (default True), based on the design according to the symmetric optimum (SO). Further information about the + design according to the SO can be found in the following paper (https://ieeexplore.ieee.org/document/55967). + + Args: + environment: gym-electric-motor environment + stages: list of the stages of the controller + controller_type: string of the used controller type from the dictionary _controllers + _controllers: dictionary of all possible controllers and controller stages + controller_kwargs: further arguments of the controller + + Returns: + list of stages, which are completely parameterized """ - ref_states = controller_kwargs['ref_states'] + ref_states = controller_kwargs["ref_states"] mp = environment.physical_system.electrical_motor.motor_parameter limits = environment.physical_system.limits - omega_lim = limits[environment.state_names.index('omega')] + omega_lim = limits[environment.state_names.index("omega")] if isinstance(environment.physical_system.unwrapped, DcMotorSystem): i_a_lim = limits[environment.physical_system.CURRENTS_IDX[0]] i_e_lim = limits[environment.physical_system.CURRENTS_IDX[-1]] @@ -267,52 +366,57 @@ def automated_gain(environment, stages, controller_type, _controllers, **control u_e_lim = limits[environment.physical_system.VOLTAGES_IDX[-1]] elif isinstance(environment.physical_system.unwrapped, SynchronousMotorSystem): - i_sd_lim = limits[environment.state_names.index('i_sd')] - i_sq_lim = limits[environment.state_names.index('i_sq')] - u_sd_lim = limits[environment.state_names.index('u_sd')] - u_sq_lim = limits[environment.state_names.index('u_sq')] - torque_lim = limits[environment.state_names.index('torque')] + i_sd_lim = limits[environment.state_names.index("i_sd")] + i_sq_lim = limits[environment.state_names.index("i_sq")] + u_sd_lim = limits[environment.state_names.index("u_sd")] + u_sq_lim = limits[environment.state_names.index("u_sq")] + torque_lim = limits[environment.state_names.index("torque")] else: - i_sd_lim = limits[environment.state_names.index('i_sd')] - i_sq_lim = limits[environment.state_names.index('i_sq')] - u_sd_lim = limits[environment.state_names.index('u_sd')] - u_sq_lim = limits[environment.state_names.index('u_sq')] - torque_lim = limits[environment.state_names.index('torque')] + i_sd_lim = limits[environment.state_names.index("i_sd")] + i_sq_lim = limits[environment.state_names.index("i_sq")] + u_sd_lim = limits[environment.state_names.index("u_sd")] + u_sq_lim = limits[environment.state_names.index("u_sq")] + torque_lim = limits[environment.state_names.index("torque")] # The parameter a is a design parameter when designing a controller according to the SO - a = controller_kwargs.get('a', 4) - automated_gain = controller_kwargs.get('automated_gain', True) + a = controller_kwargs.get("a", 4) + automated_gain = controller_kwargs.get("automated_gain", True) if isinstance(environment.physical_system.electrical_motor, DcSeriesMotor): - mp['l'] = mp['l_a'] + mp['l_e'] + mp["l"] = mp["l_a"] + mp["l_e"] elif isinstance(environment.physical_system.unwrapped, DcMotorSystem): - mp['l'] = mp['l_a'] + mp["l"] = mp["l_a"] - if 'automated_gain' not in controller_kwargs.keys() or automated_gain: + if "automated_gain" not in controller_kwargs.keys() or automated_gain: cont_extex_envs = ( envs.ContSpeedControlDcExternallyExcitedMotorEnv, envs.ContCurrentControlDcExternallyExcitedMotorEnv, - envs.ContTorqueControlDcExternallyExcitedMotorEnv + envs.ContTorqueControlDcExternallyExcitedMotorEnv, ) finite_extex_envs = ( envs.FiniteTorqueControlDcExternallyExcitedMotorEnv, envs.FiniteSpeedControlDcExternallyExcitedMotorEnv, - envs.FiniteCurrentControlDcExternallyExcitedMotorEnv + envs.FiniteCurrentControlDcExternallyExcitedMotorEnv, ) if type(environment) in cont_extex_envs: stages_a = stages[0] stages_e = stages[1] - p_gain = mp['l_e'] / (environment.physical_system.tau * a) / u_e_lim * i_e_lim - i_gain = p_gain / (environment.physical_system.tau * a ** 2) + p_gain = ( + mp["l_e"] + / (environment.physical_system.tau * a) + / u_e_lim + * i_e_lim + ) + i_gain = p_gain / (environment.physical_system.tau * a**2) - stages_e[0]['p_gain'] = stages_e[0].get('p_gain', p_gain) - stages_e[0]['i_gain'] = stages_e[0].get('i_gain', i_gain) + stages_e[0]["p_gain"] = stages_e[0].get("p_gain", p_gain) + stages_e[0]["i_gain"] = stages_e[0].get("i_gain", i_gain) - if stages_e[0]['controller_type'] == PIDController: + if stages_e[0]["controller_type"] == PIDController: d_gain = p_gain * environment.physical_system.tau - stages_e[0]['d_gain'] = stages_e[0].get('d_gain', d_gain) + stages_e[0]["d_gain"] = stages_e[0].get("d_gain", d_gain) elif type(environment) in finite_extex_envs: stages_a = stages[0] stages_e = stages[1] @@ -321,78 +425,140 @@ def automated_gain(environment, stages, controller_type, _controllers, **control stages_e = False if _controllers[controller_type][0] == ContinuousActionController: - if 'i' in ref_states or 'i_a' in ref_states or 'torque' in ref_states: - p_gain = mp['l'] / (environment.physical_system.tau * a) / u_a_lim * i_a_lim - i_gain = p_gain / (environment.physical_system.tau * a ** 2) - - stages_a[0]['p_gain'] = stages_a[0].get('p_gain', p_gain) - stages_a[0]['i_gain'] = stages_a[0].get('i_gain', i_gain) + if "i" in ref_states or "i_a" in ref_states or "torque" in ref_states: + p_gain = ( + mp["l"] + / (environment.physical_system.tau * a) + / u_a_lim + * i_a_lim + ) + i_gain = p_gain / (environment.physical_system.tau * a**2) + + stages_a[0]["p_gain"] = stages_a[0].get("p_gain", p_gain) + stages_a[0]["i_gain"] = stages_a[0].get("i_gain", i_gain) if _controllers[controller_type][2] == PIDController: d_gain = p_gain * environment.physical_system.tau - stages_a[0]['d_gain'] = stages_a[0].get('d_gain', d_gain) + stages_a[0]["d_gain"] = stages_a[0].get("d_gain", d_gain) - elif 'omega' in ref_states: - p_gain = environment.physical_system.mechanical_load.j_total * mp['r_a'] ** 2 / ( - a * mp['l']) / u_a_lim * omega_lim - i_gain = p_gain / (a * mp['l']) + elif "omega" in ref_states: + p_gain = ( + environment.physical_system.mechanical_load.j_total + * mp["r_a"] ** 2 + / (a * mp["l"]) + / u_a_lim + * omega_lim + ) + i_gain = p_gain / (a * mp["l"]) - stages_a[0]['p_gain'] = stages_a[0].get('p_gain', p_gain) - stages_a[0]['i_gain'] = stages_a[0].get('i_gain', i_gain) + stages_a[0]["p_gain"] = stages_a[0].get("p_gain", p_gain) + stages_a[0]["i_gain"] = stages_a[0].get("i_gain", i_gain) if _controllers[controller_type][2] == PIDController: d_gain = p_gain * environment.physical_system.tau - stages_a[0]['d_gain'] = stages_a[0].get('d_gain', d_gain) + stages_a[0]["d_gain"] = stages_a[0].get("d_gain", d_gain) elif _controllers[controller_type][0] == CascadedController: - for i in range(len(stages)): if type(stages_a[i]) is list: - if _controllers[stages_a[i][0]['controller_type']][1] == ContinuousController: #had to add [0] to make dict in list acessable - + if ( + _controllers[stages_a[i][0]["controller_type"]][1] + == ContinuousController + ): # had to add [0] to make dict in list acessable if i == 0: - p_gain = mp['l'] / (environment.physical_system.tau * a) / u_a_lim * i_a_lim - i_gain = p_gain / (environment.physical_system.tau * a ** 2) - - if _controllers[stages_a[i][0]['controller_type']][2] == PIDController: + p_gain = ( + mp["l"] + / (environment.physical_system.tau * a) + / u_a_lim + * i_a_lim + ) + i_gain = p_gain / ( + environment.physical_system.tau * a**2 + ) + + if ( + _controllers[stages_a[i][0]["controller_type"]][2] + == PIDController + ): d_gain = p_gain * environment.physical_system.tau - stages_a[i][0]['d_gain'] = stages_a[i][0].get('d_gain', d_gain) + stages_a[i][0]["d_gain"] = stages_a[i][0].get( + "d_gain", d_gain + ) elif i == 1: - t_n = environment.physical_system.tau * a ** 2 - p_gain = environment.physical_system.mechanical_load.j_total / ( - a * t_n) / i_a_lim * omega_lim + t_n = environment.physical_system.tau * a**2 + p_gain = ( + environment.physical_system.mechanical_load.j_total + / (a * t_n) + / i_a_lim + * omega_lim + ) i_gain = p_gain / (a * t_n) - if _controllers[stages_a[i][0]['controller_type']][2] == PIDController: + if ( + _controllers[stages_a[i][0]["controller_type"]][2] + == PIDController + ): d_gain = p_gain * environment.physical_system.tau - stages_a[i][0]['d_gain'] = stages_a[i][0].get('d_gain', d_gain) + stages_a[i][0]["d_gain"] = stages_a[i][0].get( + "d_gain", d_gain + ) - stages_a[i][0]['p_gain'] = stages_a[i][0].get('p_gain', p_gain)#? - stages_a[i][0]['i_gain'] = stages_a[i][0].get('i_gain', i_gain)#? + stages_a[i][0]["p_gain"] = stages_a[i][0].get( + "p_gain", p_gain + ) # ? + stages_a[i][0]["i_gain"] = stages_a[i][0].get( + "i_gain", i_gain + ) # ? elif type(stages_a[i]) is dict: - if _controllers[stages_a[i]['controller_type']][1] == ContinuousController: #had to add [0] to make dict in list acessable - + if ( + _controllers[stages_a[i]["controller_type"]][1] + == ContinuousController + ): # had to add [0] to make dict in list acessable if i == 0: - p_gain = mp['l'] / (environment.physical_system.tau * a) / u_a_lim * i_a_lim - i_gain = p_gain / (environment.physical_system.tau * a ** 2) - - if _controllers[stages_a[i]['controller_type']][2] == PIDController: + p_gain = ( + mp["l"] + / (environment.physical_system.tau * a) + / u_a_lim + * i_a_lim + ) + i_gain = p_gain / ( + environment.physical_system.tau * a**2 + ) + + if ( + _controllers[stages_a[i]["controller_type"]][2] + == PIDController + ): d_gain = p_gain * environment.physical_system.tau - stages_a[i]['d_gain'] = stages_a[i].get('d_gain', d_gain) + stages_a[i]["d_gain"] = stages_a[i].get( + "d_gain", d_gain + ) elif i == 1: - t_n = environment.physical_system.tau * a ** 2 - p_gain = environment.physical_system.mechanical_load.j_total / ( - a * t_n) / i_a_lim * omega_lim + t_n = environment.physical_system.tau * a**2 + p_gain = ( + environment.physical_system.mechanical_load.j_total + / (a * t_n) + / i_a_lim + * omega_lim + ) i_gain = p_gain / (a * t_n) - if _controllers[stages_a[i]['controller_type']][2] == PIDController: + if ( + _controllers[stages_a[i]["controller_type"]][2] + == PIDController + ): d_gain = p_gain * environment.physical_system.tau - stages_a[i]['d_gain'] = stages_a[i].get('d_gain', d_gain) - - stages_a[i]['p_gain'] = stages_a[i].get('p_gain', p_gain)#? - stages_a[i]['i_gain'] = stages_a[i].get('i_gain', i_gain)#? + stages_a[i]["d_gain"] = stages_a[i].get( + "d_gain", d_gain + ) + stages_a[i]["p_gain"] = stages_a[i].get( + "p_gain", p_gain + ) # ? + stages_a[i]["i_gain"] = stages_a[i].get( + "i_gain", i_gain + ) # ? stages = stages_a if not stages_e else [stages_a, stages_e] @@ -400,68 +566,102 @@ def automated_gain(environment, stages, controller_type, _controllers, **control if type(environment.action_space) == Box: stage_d = stages[0][0] stage_q = stages[0][1] - if 'i_sq' in ref_states and _controllers[stage_q['controller_type']][1] == ContinuousController: - p_gain_d = mp['l_d'] / (1.5 * environment.physical_system.tau * a) / u_sd_lim * i_sd_lim - i_gain_d = p_gain_d / (1.5 * environment.physical_system.tau * a ** 2) - - p_gain_q = mp['l_q'] / (1.5 * environment.physical_system.tau * a) / u_sq_lim * i_sq_lim - i_gain_q = p_gain_q / (1.5 * environment.physical_system.tau * a ** 2) - - stage_d['p_gain'] = stage_d.get('p_gain', p_gain_d) - stage_d['i_gain'] = stage_d.get('i_gain', i_gain_d) - - stage_q['p_gain'] = stage_q.get('p_gain', p_gain_q) - stage_q['i_gain'] = stage_q.get('i_gain', i_gain_q) - - if _controllers[stage_d['controller_type']][2] == PIDController: + if ( + "i_sq" in ref_states + and _controllers[stage_q["controller_type"]][1] + == ContinuousController + ): + p_gain_d = ( + mp["l_d"] + / (1.5 * environment.physical_system.tau * a) + / u_sd_lim + * i_sd_lim + ) + i_gain_d = p_gain_d / ( + 1.5 * environment.physical_system.tau * a**2 + ) + + p_gain_q = ( + mp["l_q"] + / (1.5 * environment.physical_system.tau * a) + / u_sq_lim + * i_sq_lim + ) + i_gain_q = p_gain_q / ( + 1.5 * environment.physical_system.tau * a**2 + ) + + stage_d["p_gain"] = stage_d.get("p_gain", p_gain_d) + stage_d["i_gain"] = stage_d.get("i_gain", i_gain_d) + + stage_q["p_gain"] = stage_q.get("p_gain", p_gain_q) + stage_q["i_gain"] = stage_q.get("i_gain", i_gain_q) + + if _controllers[stage_d["controller_type"]][2] == PIDController: d_gain_d = p_gain_d * environment.physical_system.tau - stage_d['d_gain'] = stage_d.get('d_gain', d_gain_d) + stage_d["d_gain"] = stage_d.get("d_gain", d_gain_d) - if _controllers[stage_q['controller_type']][2] == PIDController: + if _controllers[stage_q["controller_type"]][2] == PIDController: d_gain_q = p_gain_q * environment.physical_system.tau - stage_q['d_gain'] = stage_q.get('d_gain', d_gain_q) + stage_q["d_gain"] = stage_q.get("d_gain", d_gain_q) stages = [[stage_d, stage_q]] elif _controllers[controller_type][0] == CascadedFieldOrientedController: if type(environment.action_space) is Box: stage_d = stages[0][0] stage_q = stages[0][1] - if 'torque' not in controller_kwargs['ref_states']: + if "torque" not in controller_kwargs["ref_states"]: overlaid = stages[1] - p_gain_d = mp['l_d'] / (1.5 * environment.physical_system.tau * a) / u_sd_lim * i_sd_lim - i_gain_d = p_gain_d / (1.5 * environment.physical_system.tau * a ** 2) - - p_gain_q = mp['l_q'] / (1.5 * environment.physical_system.tau * a) / u_sq_lim * i_sq_lim - i_gain_q = p_gain_q / (1.5 * environment.physical_system.tau * a ** 2) - - stage_d['p_gain'] = stage_d.get('p_gain', p_gain_d) - stage_d['i_gain'] = stage_d.get('i_gain', i_gain_d) - - stage_q['p_gain'] = stage_q.get('p_gain', p_gain_q) - stage_q['i_gain'] = stage_q.get('i_gain', i_gain_q) - - if _controllers[stage_d['controller_type']][2] == PIDController: + p_gain_d = ( + mp["l_d"] + / (1.5 * environment.physical_system.tau * a) + / u_sd_lim + * i_sd_lim + ) + i_gain_d = p_gain_d / (1.5 * environment.physical_system.tau * a**2) + + p_gain_q = ( + mp["l_q"] + / (1.5 * environment.physical_system.tau * a) + / u_sq_lim + * i_sq_lim + ) + i_gain_q = p_gain_q / (1.5 * environment.physical_system.tau * a**2) + + stage_d["p_gain"] = stage_d.get("p_gain", p_gain_d) + stage_d["i_gain"] = stage_d.get("i_gain", i_gain_d) + + stage_q["p_gain"] = stage_q.get("p_gain", p_gain_q) + stage_q["i_gain"] = stage_q.get("i_gain", i_gain_q) + + if _controllers[stage_d["controller_type"]][2] == PIDController: d_gain_d = p_gain_d * environment.physical_system.tau - stage_d['d_gain'] = stage_d.get('d_gain', d_gain_d) + stage_d["d_gain"] = stage_d.get("d_gain", d_gain_d) - if _controllers[stage_q['controller_type']][2] == PIDController: + if _controllers[stage_q["controller_type"]][2] == PIDController: d_gain_q = p_gain_q * environment.physical_system.tau - stage_q['d_gain'] = stage_q.get('d_gain', d_gain_q) + stage_q["d_gain"] = stage_q.get("d_gain", d_gain_q) - if 'torque' not in controller_kwargs['ref_states'] and \ - _controllers[overlaid[0]['controller_type']][1] == ContinuousController: + if ( + "torque" not in controller_kwargs["ref_states"] + and _controllers[overlaid[0]["controller_type"]][1] + == ContinuousController + ): t_n = p_gain_d / i_gain_d j_total = environment.physical_system.mechanical_load.j_total - p_gain = j_total / (a ** 2 * t_n) / torque_lim * omega_lim + p_gain = j_total / (a**2 * t_n) / torque_lim * omega_lim i_gain = p_gain / (a * t_n) - overlaid[0]['p_gain'] = overlaid[0].get('p_gain', p_gain) - overlaid[0]['i_gain'] = overlaid[0].get('i_gain', i_gain) + overlaid[0]["p_gain"] = overlaid[0].get("p_gain", p_gain) + overlaid[0]["i_gain"] = overlaid[0].get("i_gain", i_gain) - if _controllers[overlaid[0]['controller_type']][2] == PIDController: + if ( + _controllers[overlaid[0]["controller_type"]][2] + == PIDController + ): d_gain = p_gain * environment.physical_system.tau - overlaid[0]['d_gain'] = overlaid[0].get('d_gain', d_gain) + overlaid[0]["d_gain"] = overlaid[0].get("d_gain", d_gain) stages = [[stage_d, stage_q], overlaid] @@ -469,81 +669,106 @@ def automated_gain(environment, stages, controller_type, _controllers, **control stages = [[stage_d, stage_q]] else: - if 'omega' in ref_states and _controllers[stages[3][0]['controller_type']][1] == ContinuousController: - - p_gain = environment.physical_system.mechanical_load.j_total / ( - 1.5 * a ** 2 * mp['p'] * np.abs(mp['l_d'] - mp['l_q'])) / i_sq_lim * omega_lim + if ( + "omega" in ref_states + and _controllers[stages[3][0]["controller_type"]][1] + == ContinuousController + ): + p_gain = ( + environment.physical_system.mechanical_load.j_total + / (1.5 * a**2 * mp["p"] * np.abs(mp["l_d"] - mp["l_q"])) + / i_sq_lim + * omega_lim + ) i_gain = p_gain / (1.5 * environment.physical_system.tau * a) - stages[3][0]['p_gain'] = stages[3][0].get('p_gain', p_gain) - stages[3][0]['i_gain'] = stages[3][0].get('i_gain', i_gain) + stages[3][0]["p_gain"] = stages[3][0].get("p_gain", p_gain) + stages[3][0]["i_gain"] = stages[3][0].get("i_gain", i_gain) - if _controllers[stages[3][0]['controller_type']][2] == PIDController: + if ( + _controllers[stages[3][0]["controller_type"]][2] + == PIDController + ): d_gain = p_gain * environment.physical_system.tau - stages[3][0]['d_gain'] = stages[3][0].get('d_gain', d_gain) - - elif _controllers[controller_type][0] == InductionMotorFieldOrientedController: - - mp['l_s'] = mp['l_m'] + mp['l_sigs'] - mp['l_r'] = mp['l_m'] + mp['l_sigr'] - sigma = (mp['l_s'] * mp['l_r'] - mp['l_m'] ** 2) / (mp['l_s'] * mp['l_r']) - tau_sigma = (sigma * mp['l_s']) / (mp['r_s'] + mp['r_r'] * mp['l_m'] ** 2 / mp['l_r'] ** 2) - tau_r = mp['l_r'] / mp['r_r'] + stages[3][0]["d_gain"] = stages[3][0].get("d_gain", d_gain) + + elif ( + _controllers[controller_type][0] + == InductionMotorFieldOrientedController + ): + mp["l_s"] = mp["l_m"] + mp["l_sigs"] + mp["l_r"] = mp["l_m"] + mp["l_sigr"] + sigma = (mp["l_s"] * mp["l_r"] - mp["l_m"] ** 2) / ( + mp["l_s"] * mp["l_r"] + ) + tau_sigma = (sigma * mp["l_s"]) / ( + mp["r_s"] + mp["r_r"] * mp["l_m"] ** 2 / mp["l_r"] ** 2 + ) + tau_r = mp["l_r"] / mp["r_r"] p_gain = tau_r / tau_sigma i_gain = p_gain / tau_sigma - stages[0][0]['p_gain'] = stages[0][0].get('p_gain', p_gain) - stages[0][0]['i_gain'] = stages[0][0].get('i_gain', i_gain) - stages[0][1]['p_gain'] = stages[0][1].get('p_gain', p_gain) - stages[0][1]['i_gain'] = stages[0][1].get('i_gain', i_gain) + stages[0][0]["p_gain"] = stages[0][0].get("p_gain", p_gain) + stages[0][0]["i_gain"] = stages[0][0].get("i_gain", i_gain) + stages[0][1]["p_gain"] = stages[0][1].get("p_gain", p_gain) + stages[0][1]["i_gain"] = stages[0][1].get("i_gain", i_gain) - if _controllers[stages[0][0]['controller_type']][2] == PIDController: + if _controllers[stages[0][0]["controller_type"]][2] == PIDController: d_gain = p_gain * tau_sigma - stages[0][0]['d_gain'] = stages[0][0].get('d_gain', d_gain) + stages[0][0]["d_gain"] = stages[0][0].get("d_gain", d_gain) - if _controllers[stages[0][1]['controller_type']][2] == PIDController: + if _controllers[stages[0][1]["controller_type"]][2] == PIDController: d_gain = p_gain * tau_sigma - stages[0][1]['d_gain'] = stages[0][1].get('d_gain', d_gain) - - elif _controllers[controller_type][0] == InductionMotorCascadedFieldOrientedController: + stages[0][1]["d_gain"] = stages[0][1].get("d_gain", d_gain) - if 'torque' not in controller_kwargs['ref_states']: + elif ( + _controllers[controller_type][0] + == InductionMotorCascadedFieldOrientedController + ): + if "torque" not in controller_kwargs["ref_states"]: overlaid = stages[1] - mp['l_s'] = mp['l_m'] + mp['l_sigs'] - mp['l_r'] = mp['l_m'] + mp['l_sigr'] - sigma = (mp['l_s'] * mp['l_r'] - mp['l_m'] ** 2) / (mp['l_s'] * mp['l_r']) - tau_sigma = (sigma * mp['l_s']) / (mp['r_s'] + mp['r_r'] * mp['l_m'] ** 2 / mp['l_r'] ** 2) - tau_r = mp['l_r'] / mp['r_r'] + mp["l_s"] = mp["l_m"] + mp["l_sigs"] + mp["l_r"] = mp["l_m"] + mp["l_sigr"] + sigma = (mp["l_s"] * mp["l_r"] - mp["l_m"] ** 2) / ( + mp["l_s"] * mp["l_r"] + ) + tau_sigma = (sigma * mp["l_s"]) / ( + mp["r_s"] + mp["r_r"] * mp["l_m"] ** 2 / mp["l_r"] ** 2 + ) + tau_r = mp["l_r"] / mp["r_r"] p_gain = tau_r / tau_sigma i_gain = p_gain / tau_sigma - stages[0][0]['p_gain'] = stages[0][0].get('p_gain', p_gain) - stages[0][0]['i_gain'] = stages[0][0].get('i_gain', i_gain) - stages[0][1]['p_gain'] = stages[0][1].get('p_gain', p_gain) - stages[0][1]['i_gain'] = stages[0][1].get('i_gain', i_gain) + stages[0][0]["p_gain"] = stages[0][0].get("p_gain", p_gain) + stages[0][0]["i_gain"] = stages[0][0].get("i_gain", i_gain) + stages[0][1]["p_gain"] = stages[0][1].get("p_gain", p_gain) + stages[0][1]["i_gain"] = stages[0][1].get("i_gain", i_gain) - if _controllers[stages[0][0]['controller_type']][2] == PIDController: + if _controllers[stages[0][0]["controller_type"]][2] == PIDController: d_gain = p_gain * tau_sigma - stages[0][0]['d_gain'] = stages[0][0].get('d_gain', d_gain) + stages[0][0]["d_gain"] = stages[0][0].get("d_gain", d_gain) - if _controllers[stages[0][1]['controller_type']][2] == PIDController: + if _controllers[stages[0][1]["controller_type"]][2] == PIDController: d_gain = p_gain * tau_sigma - stages[0][1]['d_gain'] = stages[0][1].get('d_gain', d_gain) + stages[0][1]["d_gain"] = stages[0][1].get("d_gain", d_gain) - if 'torque' not in controller_kwargs['ref_states'] and \ - _controllers[overlaid[0]['controller_type']][1] == ContinuousController: + if ( + "torque" not in controller_kwargs["ref_states"] + and _controllers[overlaid[0]["controller_type"]][1] + == ContinuousController + ): t_n = p_gain / i_gain j_total = environment.physical_system.mechanical_load.j_total - p_gain = j_total / (a ** 2 * t_n) / torque_lim * omega_lim + p_gain = j_total / (a**2 * t_n) / torque_lim * omega_lim i_gain = p_gain / (a * t_n) - overlaid[0]['p_gain'] = overlaid[0].get('p_gain', p_gain) - overlaid[0]['i_gain'] = overlaid[0].get('i_gain', i_gain) + overlaid[0]["p_gain"] = overlaid[0].get("p_gain", p_gain) + overlaid[0]["i_gain"] = overlaid[0].get("i_gain", i_gain) - if _controllers[overlaid[0]['controller_type']][2] == PIDController: + if _controllers[overlaid[0]["controller_type"]][2] == PIDController: d_gain = p_gain * environment.physical_system.tau - overlaid[0]['d_gain'] = overlaid[0].get('d_gain', d_gain) + overlaid[0]["d_gain"] = overlaid[0].get("d_gain", d_gain) stages = [stages[0], overlaid] diff --git a/examples/classic_controllers/classic_controllers_dc_motor_example.py b/examples/classic_controllers/classic_controllers_dc_motor_example.py index 0ac6ef4d..f647c36d 100644 --- a/examples/classic_controllers/classic_controllers_dc_motor_example.py +++ b/examples/classic_controllers/classic_controllers_dc_motor_example.py @@ -3,8 +3,7 @@ import gym_electric_motor as gem from gym_electric_motor.visualization import MotorDashboard -if __name__ == '__main__': - +if __name__ == "__main__": """ motor type: 'PermExDc' Permanently Excited DC Motor 'ExtExDc' Externally Excited MC Motor @@ -19,26 +18,30 @@ 'Finite' Discrete Action Space """ - motor_type = 'PermExDc' - control_type = 'TC' - action_type = 'Cont' + motor_type = "PermExDc" + control_type = "TC" + action_type = "Cont" - motor = action_type + '-' + control_type + '-' + motor_type + '-v0' + motor = action_type + "-" + control_type + "-" + motor_type + "-v0" - if motor_type in ['PermExDc', 'SeriesDc']: - states = ['omega', 'torque', 'i', 'u'] - elif motor_type == 'ShuntDc': - states = ['omega', 'torque', 'i_a', 'i_e', 'u'] - elif motor_type == 'ExtExDc': - states = ['omega', 'torque', 'i_a', 'i_e', 'u_a', 'u_e'] + if motor_type in ["PermExDc", "SeriesDc"]: + states = ["omega", "torque", "i", "u"] + elif motor_type == "ShuntDc": + states = ["omega", "torque", "i_a", "i_e", "u"] + elif motor_type == "ExtExDc": + states = ["omega", "torque", "i_a", "i_e", "u_a", "u_e"] else: - raise KeyError(motor_type + ' is not available') + raise KeyError(motor_type + " is not available") # definition of the plotted variables external_ref_plots = [ExternallyReferencedStatePlot(state) for state in states] # initialize the gym-electric-motor environment - env = gem.make(motor, visualization=MotorDashboard(additional_plots=external_ref_plots), render_mode="figure_once") + env = gem.make( + motor, + visualization=MotorDashboard(additional_plots=external_ref_plots), + render_mode="figure_once", + ) """ initialize the controller @@ -53,7 +56,7 @@ visualization = MotorDashboard(additional_plots=external_ref_plots) controller = Controller.make(env, external_ref_plots=external_ref_plots) - (state, reference), _ = env.reset(seed = None) + (state, reference), _ = env.reset(seed=None) # simulate the environment for i in range(10001): action = controller.control(state, reference) diff --git a/examples/classic_controllers/classic_controllers_ind_motor_example.py b/examples/classic_controllers/classic_controllers_ind_motor_example.py index 4370dc5b..25f36d42 100644 --- a/examples/classic_controllers/classic_controllers_ind_motor_example.py +++ b/examples/classic_controllers/classic_controllers_ind_motor_example.py @@ -6,8 +6,7 @@ from gym_electric_motor.physical_system_wrappers import FluxObserver import numpy as np -if __name__ == '__main__': - +if __name__ == "__main__": """ motor type: 'SCIM' Squirrel Cage Induction Motor @@ -18,21 +17,27 @@ action_type: 'AbcCont' Continuous Action Space """ - motor_type = 'SCIM' - control_type = 'TC' - action_type = 'Cont' + motor_type = "SCIM" + control_type = "TC" + action_type = "Cont" - env_id = action_type + '-' + control_type + '-' + motor_type + '-v0' + env_id = action_type + "-" + control_type + "-" + motor_type + "-v0" # definition of the plotted variables - states = ['omega', 'torque', 'i_sd', 'i_sq', 'u_sd', 'u_sq'] + states = ["omega", "torque", "i_sd", "i_sq", "u_sd", "u_sq"] external_ref_plots = [ExternallyReferencedStatePlot(state) for state in states] - external_plot = [ExternalPlot(referenced=control_type != 'CC'), ExternalPlot(min=-np.pi, max=np.pi)] + external_plot = [ + ExternalPlot(referenced=control_type != "CC"), + ExternalPlot(min=-np.pi, max=np.pi), + ] external_ref_plots += external_plot # initialize the gym-electric-motor environment - env = gem.make(env_id, physical_system_wrappers=(FluxObserver(),), - visualization=MotorDashboard(state_plots=('omega', 'psi_abs', 'psi_angle'))) + env = gem.make( + env_id, + physical_system_wrappers=(FluxObserver(),), + visualization=MotorDashboard(state_plots=("omega", "psi_abs", "psi_angle")), + ) """ initialize the controller diff --git a/examples/classic_controllers/classic_controllers_synch_motor_example.py b/examples/classic_controllers/classic_controllers_synch_motor_example.py index 7fb5953b..ff8ad73f 100644 --- a/examples/classic_controllers/classic_controllers_synch_motor_example.py +++ b/examples/classic_controllers/classic_controllers_synch_motor_example.py @@ -4,8 +4,7 @@ from gym_electric_motor.visualization import MotorDashboard -if __name__ == '__main__': - +if __name__ == "__main__": """ motor type: 'PMSM' Permanent Magnet Synchronous Motor 'SynRM' Synchronous Reluctance Motor @@ -18,18 +17,22 @@ 'Finite' Discrete Action Space """ - motor_type = 'PMSM' - control_type = 'TC' - action_type = 'Cont' - - env_id = action_type + '-' + control_type + '-' + motor_type + '-v0' + motor_type = "PMSM" + control_type = "TC" + action_type = "Cont" + env_id = action_type + "-" + control_type + "-" + motor_type + "-v0" # definition of the plotted variables - external_ref_plots = [ExternallyReferencedStatePlot(state) for state in ['omega', 'torque', 'i_sd', 'i_sq', 'u_sd', 'u_sq']] + external_ref_plots = [ + ExternallyReferencedStatePlot(state) + for state in ["omega", "torque", "i_sd", "i_sq", "u_sd", "u_sq"] + ] # initialize the gym-electric-motor environment - env = gem.make(env_id, visualization=MotorDashboard(additional_plots=external_ref_plots)) + env = gem.make( + env_id, visualization=MotorDashboard(additional_plots=external_ref_plots) + ) """ initialize the controller @@ -48,7 +51,9 @@ """ - controller = Controller.make(env, external_ref_plots=external_ref_plots, torque_control='analytical') + controller = Controller.make( + env, external_ref_plots=external_ref_plots, torque_control="analytical" + ) (state, reference), _ = env.reset() diff --git a/examples/classic_controllers/controllers/cascaded_controller.py b/examples/classic_controllers/controllers/cascaded_controller.py index e526060e..612976bf 100644 --- a/examples/classic_controllers/controllers/cascaded_controller.py +++ b/examples/classic_controllers/controllers/cascaded_controller.py @@ -8,14 +8,22 @@ class CascadedController: """ - This class is used for cascaded torque and speed control of all dc motor environments. Each stage can contain - continuous or discrete controllers. For the externally excited dc motor an additional controller is used for - the excitation current. The calculated reference values of the intermediate stages can be inserted into the - plots. + This class is used for cascaded torque and speed control of all dc motor environments. Each stage can contain + continuous or discrete controllers. For the externally excited dc motor an additional controller is used for + the excitation current. The calculated reference values of the intermediate stages can be inserted into the + plots. """ - def __init__(self, environment, stages, _controllers, visualization, ref_states, external_ref_plots=(), **controller_kwargs): - + def __init__( + self, + environment, + stages, + _controllers, + visualization, + ref_states, + external_ref_plots=(), + **controller_kwargs, + ): self.env = environment self.visualization = visualization self.action_space = environment.action_space @@ -25,27 +33,42 @@ def __init__(self, environment, stages, _controllers, visualization, ref_states, self.i_e_idx = environment.physical_system.CURRENTS_IDX[-1] self.i_a_idx = environment.physical_system.CURRENTS_IDX[0] self.u_idx = environment.physical_system.VOLTAGES_IDX[-1] - self.omega_idx = environment.state_names.index('omega') - self.torque_idx = environment.state_names.index('torque') - self.ref_idx = np.where(ref_states != 'i_e')[0][0] - self.ref_state_idx = [self.i_a_idx, environment.state_names.index(ref_states[self.ref_idx])] + self.omega_idx = environment.state_names.index("omega") + self.torque_idx = environment.state_names.index("torque") + self.ref_idx = np.where(ref_states != "i_e")[0][0] + self.ref_state_idx = [ + self.i_a_idx, + environment.state_names.index(ref_states[self.ref_idx]), + ] self.limit = environment.physical_system.limits[environment.state_filter] - self.nominal_values = environment.physical_system.nominal_state[environment.state_filter] + self.nominal_values = environment.physical_system.nominal_state[ + environment.state_filter + ] - self.control_e = isinstance(environment.physical_system.electrical_motor, DcExternallyExcitedMotor) + self.control_e = isinstance( + environment.physical_system.electrical_motor, DcExternallyExcitedMotor + ) self.control_omega = 0 mp = environment.physical_system.electrical_motor.motor_parameter - self.psi_e = mp.get('psie_e', False) - self.l_e = mp.get('l_e_prime', False) - self.r_e = mp.get('r_e', None) - self.r_a = mp.get('r_a', None) + self.psi_e = mp.get("psie_e", False) + self.l_e = mp.get("l_e_prime", False) + self.r_e = mp.get("r_e", None) + self.r_a = mp.get("r_a", None) # Set the action limits if type(self.action_space) is Box: - self.action_limit_low = self.action_space.low[0] * self.nominal_values[self.u_idx] / self.limit[self.u_idx] - self.action_limit_high = self.action_space.high[0] * self.nominal_values[self.u_idx] / self.limit[self.u_idx] + self.action_limit_low = ( + self.action_space.low[0] + * self.nominal_values[self.u_idx] + / self.limit[self.u_idx] + ) + self.action_limit_high = ( + self.action_space.high[0] + * self.nominal_values[self.u_idx] + / self.limit[self.u_idx] + ) # Set the state limits self.state_limit_low = self.state_space.low * self.nominal_values / self.limit @@ -53,8 +76,12 @@ def __init__(self, environment, stages, _controllers, visualization, ref_states, # Initialize i_e Controller if needed if self.control_e: - assert len(stages) == 2, 'Controller design is incomplete' - self.ref_e_idx = False if 'i_e' not in ref_states else np.where(ref_states=='i_e')[0][0] + assert len(stages) == 2, "Controller design is incomplete" + self.ref_e_idx = ( + False + if "i_e" not in ref_states + else np.where(ref_states == "i_e")[0][0] + ) self.control_e_idx = 1 if self.omega_idx in self.ref_state_idx: @@ -62,100 +89,153 @@ def __init__(self, environment, stages, _controllers, visualization, ref_states, self.control_omega = 1 self.ref_state_idx.append(self.i_e_idx) - self.controller_e = _controllers[stages[1][0]['controller_type']][1].make(environment, stages[1][0], - _controllers, control_e=True, - **controller_kwargs) + self.controller_e = _controllers[stages[1][0]["controller_type"]][1].make( + environment, + stages[1][0], + _controllers, + control_e=True, + **controller_kwargs, + ) stages = stages[0] - u_e_idx = self.state_names.index('u_e') + u_e_idx = self.state_names.index("u_e") # Set action limit for u_e if type(self.action_space) is Box: - self.action_e_limit_low = self.action_space.low[1] * self.nominal_values[u_e_idx] / self.limit[u_e_idx] - self.action_e_limit_high = self.action_space.high[1] * self.nominal_values[u_e_idx] / self.limit[u_e_idx] + self.action_e_limit_low = ( + self.action_space.low[1] + * self.nominal_values[u_e_idx] + / self.limit[u_e_idx] + ) + self.action_e_limit_high = ( + self.action_space.high[1] + * self.nominal_values[u_e_idx] + / self.limit[u_e_idx] + ) else: self.control_e_idx = 0 - assert len(ref_states) <= 1, 'Too many referenced states' + assert len(ref_states) <= 1, "Too many referenced states" # Check of the stages are using continuous or discrete controller - self.stage_type = [_controllers[stage['controller_type']][1] == ContinuousController for stage in stages] + self.stage_type = [ + _controllers[stage["controller_type"]][1] == ContinuousController + for stage in stages + ] # Initialize Controller stages self.controller_stages = [ - _controllers[stage['controller_type']][1].make(environment, stage, _controllers, cascaded=stages.index(stage) != 0) for - stage in stages] + _controllers[stage["controller_type"]][1].make( + environment, stage, _controllers, cascaded=stages.index(stage) != 0 + ) + for stage in stages + ] # Set up the plots self.external_ref_plots = external_ref_plots - internal_refs = np.array([environment.state_names[i] for i in self.ref_state_idx]) + internal_refs = np.array( + [environment.state_names[i] for i in self.ref_state_idx] + ) ref_states_plotted = np.unique(np.append(ref_states, internal_refs)) for external_plots in self.external_ref_plots: external_plots.set_reference(ref_states_plotted) - assert type(self.action_space) is Box or not self.stage_type[0], 'No suitable inner controller' - assert type(self.action_space) in [Discrete, MultiDiscrete] or self.stage_type[ - 0], 'No suitable inner controller' + assert ( + type(self.action_space) is Box or not self.stage_type[0] + ), "No suitable inner controller" + assert ( + type(self.action_space) in [Discrete, MultiDiscrete] or self.stage_type[0] + ), "No suitable inner controller" - self.ref = np.zeros(len(self.controller_stages) + self.control_e_idx + self.control_omega) + self.ref = np.zeros( + len(self.controller_stages) + self.control_e_idx + self.control_omega + ) def control(self, state, reference): """ - Main method that is called by the user to calculate the manipulated variable. + Main method that is called by the user to calculate the manipulated variable. - Args: - state: state of the gem environment - reference: reference for the controlled states + Args: + state: state of the gem environment + reference: reference for the controlled states - Returns: - action: action for the gem environment + Returns: + action: action for the gem environment """ # Set the reference - self.ref[-1-self.control_e_idx] = reference[self.ref_idx] + self.ref[-1 - self.control_e_idx] = reference[self.ref_idx] # Iterate through the high-level controller stages - for i in range(len(self.controller_stages) - 1, 0 + self.control_e_idx - self.control_omega, -1): + for i in range( + len(self.controller_stages) - 1, + 0 + self.control_e_idx - self.control_omega, + -1, + ): # Set the indices ref_idx = i - 1 + self.control_omega state_idx = self.ref_state_idx[ref_idx] # Calculate reference for lower stage self.ref[ref_idx] = self.controller_stages[i].control( - state[state_idx], self.ref[ref_idx + 1]) + state[state_idx], self.ref[ref_idx + 1] + ) # Check limits and integrate - if (self.state_limit_low[state_idx] <= self.ref[ref_idx] <= self.state_limit_high[state_idx]) and self.stage_type[i]: - self.controller_stages[i].integrate(state[self.ref_state_idx[i + self.control_omega]], reference[0]) + if ( + self.state_limit_low[state_idx] + <= self.ref[ref_idx] + <= self.state_limit_high[state_idx] + ) and self.stage_type[i]: + self.controller_stages[i].integrate( + state[self.ref_state_idx[i + self.control_omega]], reference[0] + ) elif self.stage_type[i]: - self.ref[ref_idx] = np.clip(self.ref[ref_idx], self.state_limit_low[state_idx], - self.state_limit_high[state_idx]) + self.ref[ref_idx] = np.clip( + self.ref[ref_idx], + self.state_limit_low[state_idx], + self.state_limit_high[state_idx], + ) # Calculate optimal i_a and i_e for externally excited dc motor if self.control_e: i_e = np.clip( - np.power(self.r_a * (self.ref[1] * self.limit[self.torque_idx]) ** 2 / (self.r_e * self.l_e ** 2), - 1 / 4), self.state_space.low[self.i_e_idx] * self.limit[self.i_e_idx], - self.state_space.high[self.i_e_idx] * self.limit[self.i_e_idx]) - i_a = np.clip(self.ref[1] * self.limit[self.torque_idx] / (self.l_e * i_e), - self.state_space.low[self.i_a_idx] * self.limit[self.i_a_idx], - self.state_space.high[self.i_a_idx] * self.limit[self.i_a_idx]) + np.power( + self.r_a + * (self.ref[1] * self.limit[self.torque_idx]) ** 2 + / (self.r_e * self.l_e**2), + 1 / 4, + ), + self.state_space.low[self.i_e_idx] * self.limit[self.i_e_idx], + self.state_space.high[self.i_e_idx] * self.limit[self.i_e_idx], + ) + i_a = np.clip( + self.ref[1] * self.limit[self.torque_idx] / (self.l_e * i_e), + self.state_space.low[self.i_a_idx] * self.limit[self.i_a_idx], + self.state_space.high[self.i_a_idx] * self.limit[self.i_a_idx], + ) self.ref[-1] = i_e / self.limit[self.i_e_idx] self.ref[0] = i_a / self.limit[self.i_a_idx] # Calculate action for u_a - action = self.controller_stages[0].control(state[self.ref_state_idx[0]], self.ref[0]) + action = self.controller_stages[0].control( + state[self.ref_state_idx[0]], self.ref[0] + ) # Check if stage is continuous if self.stage_type[0]: - action += self.feedforward(state) # EMF compensation + action += self.feedforward(state) # EMF compensation # Check limits and integrate if self.action_limit_low <= action <= self.action_limit_high: - self.controller_stages[0].integrate(state[self.ref_state_idx[0]], self.ref[0]) + self.controller_stages[0].integrate( + state[self.ref_state_idx[0]], self.ref[0] + ) action = [action] else: - action = np.clip([action], self.action_limit_low, self.action_limit_high) + action = np.clip( + [action], self.action_limit_low, self.action_limit_high + ) # Calculate action for u_e if needed if self.control_e: @@ -168,21 +248,33 @@ def control(self, state, reference): action = np.append(action, action_u_e) if self.action_e_limit_low <= action[1] <= self.action_e_limit_high: self.controller_e.integrate(state[self.i_e_idx], self.ref[-1]) - action = np.clip(action, self.action_e_limit_low, self.action_e_limit_high) + action = np.clip( + action, self.action_e_limit_low, self.action_e_limit_high + ) else: - action = np.array([action, action_u_e], dtype='object') + action = np.array([action, action_u_e], dtype="object") if self.env.render_mode != None: # Plot the external references - plot(external_reference_plots=self.external_ref_plots, state_names=self.state_names, - visualization=self.visualization, external_data=self.get_plot_data()) + plot( + external_reference_plots=self.external_ref_plots, + state_names=self.state_names, + visualization=self.visualization, + external_data=self.get_plot_data(), + ) return action def feedforward(self, state): # EMF compensation - psi_e = max(self.psi_e or self.l_e * state[self.i_e_idx] * self.nominal_values[self.i_e_idx], 1e-6) - return (state[self.omega_idx] * self.nominal_values[self.omega_idx] * psi_e) / self.nominal_values[self.u_idx] + psi_e = max( + self.psi_e + or self.l_e * state[self.i_e_idx] * self.nominal_values[self.i_e_idx], + 1e-6, + ) + return ( + state[self.omega_idx] * self.nominal_values[self.omega_idx] * psi_e + ) / self.nominal_values[self.u_idx] def get_plot_data(self): # Getting the external data that should be plotted diff --git a/examples/classic_controllers/controllers/cascaded_foc_controller.py b/examples/classic_controllers/controllers/cascaded_foc_controller.py index 80c78013..6b63fb49 100644 --- a/examples/classic_controllers/controllers/cascaded_foc_controller.py +++ b/examples/classic_controllers/controllers/cascaded_foc_controller.py @@ -7,62 +7,78 @@ class CascadedFieldOrientedController: """ - This controller is used for torque or speed control of synchronous motors. The controller consists of a field - oriented controller for current control, an efficiency-optimized torque controller and an optional speed - controller. The current control is equivalent to the current control of the FieldOrientedController. The torque - controller is based on the maximum torque per current (MTPC) control strategy in the voltage control range and - the maximum torque per flux (MTPF) control strategy with an additional modulation controller in the flux - weakening range. The speed controller is designed as a PI-controller by default. + This controller is used for torque or speed control of synchronous motors. The controller consists of a field + oriented controller for current control, an efficiency-optimized torque controller and an optional speed + controller. The current control is equivalent to the current control of the FieldOrientedController. The torque + controller is based on the maximum torque per current (MTPC) control strategy in the voltage control range and + the maximum torque per flux (MTPF) control strategy with an additional modulation controller in the flux + weakening range. The speed controller is designed as a PI-controller by default. """ - def __init__(self, environment, stages, _controllers, ref_states, external_ref_plots=(), plot_torque=True, - plot_modulation=False, update_interval=1000, torque_control='interpolate', **controller_kwargs): + def __init__( + self, + environment, + stages, + _controllers, + ref_states, + external_ref_plots=(), + plot_torque=True, + plot_modulation=False, + update_interval=1000, + torque_control="interpolate", + **controller_kwargs, + ): t32 = environment.physical_system.electrical_motor.t_32 q = environment.physical_system.electrical_motor.q - self.backward_transformation = (lambda quantities, eps: t32(q(quantities, eps))) + self.backward_transformation = lambda quantities, eps: t32(q(quantities, eps)) self.tau = environment.physical_system.tau self.action_space = environment.action_space self.state_space = environment.physical_system.state_space self.state_names = environment.state_names - self.i_sd_idx = environment.state_names.index('i_sd') - self.i_sq_idx = environment.state_names.index('i_sq') - self.u_sd_idx = environment.state_names.index('u_sd') - self.u_sq_idx = environment.state_names.index('u_sq') - self.u_a_idx = environment.state_names.index('u_a') - self.u_b_idx = environment.state_names.index('u_b') - self.u_c_idx = environment.state_names.index('u_c') - self.omega_idx = environment.state_names.index('omega') - self.eps_idx = environment.state_names.index('epsilon') - self.torque_idx = environment.state_names.index('torque') + self.i_sd_idx = environment.state_names.index("i_sd") + self.i_sq_idx = environment.state_names.index("i_sq") + self.u_sd_idx = environment.state_names.index("u_sd") + self.u_sq_idx = environment.state_names.index("u_sq") + self.u_a_idx = environment.state_names.index("u_a") + self.u_b_idx = environment.state_names.index("u_b") + self.u_c_idx = environment.state_names.index("u_c") + self.omega_idx = environment.state_names.index("omega") + self.eps_idx = environment.state_names.index("epsilon") + self.torque_idx = environment.state_names.index("torque") self.external_ref_plots = external_ref_plots - self.torque_control = 'torque' in ref_states or 'omega' in ref_states - self.current_control = 'i_sd' in ref_states - self.omega_control = 'omega' in ref_states + self.torque_control = "torque" in ref_states or "omega" in ref_states + self.current_control = "i_sd" in ref_states + self.omega_control = "omega" in ref_states if self.current_control: - self.ref_d_idx = np.where(ref_states == 'i_sd')[0][0] - self.ref_idx = np.where(ref_states != 'i_sd')[0][0] + self.ref_d_idx = np.where(ref_states == "i_sd")[0][0] + self.ref_idx = np.where(ref_states != "i_sd")[0][0] - self.omega_control = 'omega' in ref_states and type(environment) + self.omega_control = "omega" in ref_states and type(environment) self.has_cont_action_space = type(self.action_space) is Box self.limit = environment.physical_system.limits self.nominal_values = environment.physical_system.nominal_state self.mp = environment.physical_system.electrical_motor.motor_parameter - self.psi_p = self.mp.get('psi_p', 0) + self.psi_p = self.mp.get("psi_p", 0) self.dead_time = 0.5 - self.decoupling = controller_kwargs.get('decoupling', True) + self.decoupling = controller_kwargs.get("decoupling", True) self.ref_state_idx = [self.i_sq_idx, self.i_sd_idx] # Initialize torque controller if self.torque_control: self.ref_state_idx.append(self.torque_idx) - self.torque_controller = TorqueToCurrentConversion(environment, plot_torque, plot_modulation, - update_interval, torque_control) + self.torque_controller = TorqueToCurrentConversion( + environment, + plot_torque, + plot_modulation, + update_interval, + torque_control, + ) if self.omega_control: self.ref_state_idx.append(self.omega_idx) @@ -70,73 +86,121 @@ def __init__(self, environment, stages, _controllers, ref_states, external_ref_ # Initialize continuous controller stages if self.has_cont_action_space: - assert len(stages[0]) == 2, 'Number of stages not correct' - self.d_controller = _controllers[stages[0][0]['controller_type']][1].make( - environment, stages[0][0], _controllers, **controller_kwargs) - self.q_controller = _controllers[stages[0][1]['controller_type']][1].make( - environment, stages[0][1], _controllers, **controller_kwargs) + assert len(stages[0]) == 2, "Number of stages not correct" + self.d_controller = _controllers[stages[0][0]["controller_type"]][1].make( + environment, stages[0][0], _controllers, **controller_kwargs + ) + self.q_controller = _controllers[stages[0][1]["controller_type"]][1].make( + environment, stages[0][1], _controllers, **controller_kwargs + ) [self.u_sq_0, self.u_sd_0] = [0, 0] if self.omega_control: - self.overlaid_controller = [_controllers[stages[1][i]['controller_type']][1].make( - environment, stages[1][i], _controllers, cascaded=True, **controller_kwargs) for i in range(0, len(stages[1]))] - self.overlaid_type = [_controllers[stages[1][i]['controller_type']][1] == ContinuousController for i in - range(0, len(stages[1]))] + self.overlaid_controller = [ + _controllers[stages[1][i]["controller_type"]][1].make( + environment, + stages[1][i], + _controllers, + cascaded=True, + **controller_kwargs, + ) + for i in range(0, len(stages[1])) + ] + self.overlaid_type = [ + _controllers[stages[1][i]["controller_type"]][1] + == ContinuousController + for i in range(0, len(stages[1])) + ] # Initialize discrete controller stages else: - if self.omega_control: - assert len(stages) == 4, 'Number of stages not correct' - self.overlaid_controller = [_controllers[stages[3][i]['controller_type']][1].make( - environment, stages[3][i], cascaded=True, **controller_kwargs) for i in range(len(stages[3]))] - self.overlaid_type = [_controllers[stages[3][i]['controller_type']][1] == ContinuousController for i in - range(len(stages[3]))] + assert len(stages) == 4, "Number of stages not correct" + self.overlaid_controller = [ + _controllers[stages[3][i]["controller_type"]][1].make( + environment, stages[3][i], cascaded=True, **controller_kwargs + ) + for i in range(len(stages[3])) + ] + self.overlaid_type = [ + _controllers[stages[3][i]["controller_type"]][1] + == ContinuousController + for i in range(len(stages[3])) + ] else: - assert len(stages) == 3, 'Number of stages not correct' - self.abc_controller = [_controllers[stages[0][0]['controller_type']][1].make( - environment, stages[i][0], _controllers, **controller_kwargs) for i in range(3)] - self.i_abc_idx = [environment.state_names.index(state) for state in ['i_a', 'i_b', 'i_c']] - - self.ref = np.zeros(len(self.ref_state_idx)) # Define array for reference values + assert len(stages) == 3, "Number of stages not correct" + self.abc_controller = [ + _controllers[stages[0][0]["controller_type"]][1].make( + environment, stages[i][0], _controllers, **controller_kwargs + ) + for i in range(3) + ] + self.i_abc_idx = [ + environment.state_names.index(state) for state in ["i_a", "i_b", "i_c"] + ] + + self.ref = np.zeros( + len(self.ref_state_idx) + ) # Define array for reference values # Set up the plots - plot_ref = np.append(np.array([environment.state_names[i] for i in self.ref_state_idx]), ref_states) + plot_ref = np.append( + np.array([environment.state_names[i] for i in self.ref_state_idx]), + ref_states, + ) for ext_ref_plot in self.external_ref_plots: ext_ref_plot.set_reference(plot_ref) def control(self, state, reference): """ - Main method that is called by the user to calculate the manipulated variable. + Main method that is called by the user to calculate the manipulated variable. - Args: - state: state of the gem environment - reference: reference for the controlled states + Args: + state: state of the gem environment + reference: reference for the controlled states - Returns: - action: action for the gem environment + Returns: + action: action for the gem environment """ self.ref[-1] = reference[self.ref_idx] # Set the reference - epsilon_d = state[self.eps_idx] * self.limit[self.eps_idx] + self.dead_time * self.tau * state[self.omega_idx] * \ - self.limit[self.omega_idx] * self.mp['p'] # Calculate delta epsilon + epsilon_d = ( + state[self.eps_idx] * self.limit[self.eps_idx] + + self.dead_time + * self.tau + * state[self.omega_idx] + * self.limit[self.omega_idx] + * self.mp["p"] + ) # Calculate delta epsilon # Iterate through high-level controller if self.omega_control: for i in range(len(self.overlaid_controller) + 1, 1, -1): # Calculate reference - self.ref[i] = self.overlaid_controller[i-2].control(state[self.ref_state_idx[i + 1]], self.ref[i + 1]) + self.ref[i] = self.overlaid_controller[i - 2].control( + state[self.ref_state_idx[i + 1]], self.ref[i + 1] + ) # Check limits and integrate - if (0.85 * self.state_space.low[self.ref_state_idx[i]] <= self.ref[i] <= 0.85 * - self.state_space.high[self.ref_state_idx[i]]) and self.overlaid_type[i - 2]: - self.overlaid_controller[i - 2].integrate(state[self.ref_state_idx[i + 1]], self.ref[i + 1]) + if ( + 0.85 * self.state_space.low[self.ref_state_idx[i]] + <= self.ref[i] + <= 0.85 * self.state_space.high[self.ref_state_idx[i]] + ) and self.overlaid_type[i - 2]: + self.overlaid_controller[i - 2].integrate( + state[self.ref_state_idx[i + 1]], self.ref[i + 1] + ) else: - self.ref[i] = np.clip(self.ref[i], self.nominal_values[self.ref_state_idx[i]] / self.limit[ - self.ref_state_idx[i]] * self.state_space.low[self.ref_state_idx[i]], - self.nominal_values[self.ref_state_idx[i]] / self.limit[ - self.ref_state_idx[i]] * self.state_space.high[self.ref_state_idx[i]]) + self.ref[i] = np.clip( + self.ref[i], + self.nominal_values[self.ref_state_idx[i]] + / self.limit[self.ref_state_idx[i]] + * self.state_space.low[self.ref_state_idx[i]], + self.nominal_values[self.ref_state_idx[i]] + / self.limit[self.ref_state_idx[i]] + * self.state_space.high[self.ref_state_idx[i]], + ) # Calculate reference values for i_d and i_q if self.torque_control: @@ -145,35 +209,65 @@ def control(self, state, reference): # Calculate action for continuous action space if self.has_cont_action_space: - # Decouple the two current components if self.decoupling: - self.u_sd_0 = -state[self.omega_idx] * self.mp['p'] * self.mp['l_q'] * state[self.i_sq_idx]\ - * self.limit[self.i_sq_idx] / self.limit[self.u_sd_idx] * self.limit[self.omega_idx] - self.u_sq_0 = state[self.omega_idx] * self.mp['p'] * ( - state[self.i_sd_idx] * self.mp['l_d'] * self.limit[self.u_sd_idx] + self.psi_p) / self.limit[ - self.u_sq_idx] * self.limit[self.omega_idx] + self.u_sd_0 = ( + -state[self.omega_idx] + * self.mp["p"] + * self.mp["l_q"] + * state[self.i_sq_idx] + * self.limit[self.i_sq_idx] + / self.limit[self.u_sd_idx] + * self.limit[self.omega_idx] + ) + self.u_sq_0 = ( + state[self.omega_idx] + * self.mp["p"] + * ( + state[self.i_sd_idx] + * self.mp["l_d"] + * self.limit[self.u_sd_idx] + + self.psi_p + ) + / self.limit[self.u_sq_idx] + * self.limit[self.omega_idx] + ) # Calculate action for u_sd if self.torque_control: - u_sd = self.d_controller.control(state[self.i_sd_idx], self.ref[1]) + self.u_sd_0 + u_sd = ( + self.d_controller.control(state[self.i_sd_idx], self.ref[1]) + + self.u_sd_0 + ) else: - u_sd = self.d_controller.control(state[self.i_sd_idx], reference[self.ref_d_idx]) + self.u_sd_0 + u_sd = ( + self.d_controller.control( + state[self.i_sd_idx], reference[self.ref_d_idx] + ) + + self.u_sd_0 + ) # Calculate action for u_sq - u_sq = self.q_controller.control(state[self.i_sq_idx], self.ref[0]) + self.u_sq_0 + u_sq = ( + self.q_controller.control(state[self.i_sq_idx], self.ref[0]) + + self.u_sq_0 + ) # Shifting the reference potential action_temp = self.backward_transformation((u_sd, u_sq), epsilon_d) action_temp = action_temp - 0.5 * (max(action_temp) + min(action_temp)) # Check limit and integrate - action = np.clip(action_temp, self.action_space.low[0], self.action_space.high[0]) + action = np.clip( + action_temp, self.action_space.low[0], self.action_space.high[0] + ) if (action == action_temp).all(): if self.torque_control: self.d_controller.integrate(state[self.i_sd_idx], self.ref[1]) else: - self.d_controller.integrate(state[self.i_sd_idx], reference[self.ref_d_idx]) + self.d_controller.integrate( + state[self.i_sd_idx], reference[self.ref_d_idx] + ) self.q_controller.integrate(state[self.i_sq_idx], self.ref[0]) # Calculate action for discrete action space @@ -182,17 +276,25 @@ def control(self, state, reference): ref_abc = self.backward_transformation((ref, self.ref[0]), epsilon_d) action = 0 for i in range(3): - action += (2 ** (2 - i)) * self.abc_controller[i].control(state[self.i_abc_idx[i]], ref_abc[i]) + action += (2 ** (2 - i)) * self.abc_controller[i].control( + state[self.i_abc_idx[i]], ref_abc[i] + ) # Plot overlaid reference values - plot(external_reference_plots=self.external_ref_plots, state_names=self.state_names, external_data=self.get_plot_data(), - visualization=True) + plot( + external_reference_plots=self.external_ref_plots, + state_names=self.state_names, + external_data=self.get_plot_data(), + visualization=True, + ) return action def get_plot_data(self): # Getting the external data that should be plotted - return dict(ref_state=self.ref_state_idx[:-1], ref_value=self.ref[:-1], external=[]) + return dict( + ref_state=self.ref_state_idx[:-1], ref_value=self.ref[:-1], external=[] + ) def reset(self): # Reset the Controllers diff --git a/examples/classic_controllers/controllers/continuous_action_controller.py b/examples/classic_controllers/controllers/continuous_action_controller.py index 25be55ac..e58f9c7f 100644 --- a/examples/classic_controllers/controllers/continuous_action_controller.py +++ b/examples/classic_controllers/controllers/continuous_action_controller.py @@ -6,34 +6,55 @@ class ContinuousActionController: """ - This class performs a current-control for all continuous DC motor systems. By default, a PI controller is used - for current control. An EMF compensation is applied. For the externally excited dc motor, the excitation current - is also controlled. + This class performs a current-control for all continuous DC motor systems. By default, a PI controller is used + for current control. An EMF compensation is applied. For the externally excited dc motor, the excitation current + is also controlled. """ - def __init__(self, environment, stages, _controllers, ref_states, external_ref_plots=(), **controller_kwargs): - assert type(environment.action_space) is Box and isinstance(environment.physical_system, - DcMotorSystem), 'No suitable action space for Continuous Action Controller' + def __init__( + self, + environment, + stages, + _controllers, + ref_states, + external_ref_plots=(), + **controller_kwargs, + ): + assert type(environment.action_space) is Box and isinstance( + environment.physical_system, DcMotorSystem + ), "No suitable action space for Continuous Action Controller" self.action_space = environment.action_space self.state_names = environment.state_names - self.ref_idx = np.where(ref_states != 'i_e')[0][0] + self.ref_idx = np.where(ref_states != "i_e")[0][0] self.ref_state_idx = environment.state_names.index(ref_states[self.ref_idx]) self.i_idx = environment.physical_system.CURRENTS_IDX[-1] self.u_idx = environment.physical_system.VOLTAGES_IDX[-1] self.limit = environment.physical_system.limits[environment.state_filter] - self.nominal_values = environment.physical_system.nominal_state[environment.state_filter] - self.omega_idx = self.state_names.index('omega') + self.nominal_values = environment.physical_system.nominal_state[ + environment.state_filter + ] + self.omega_idx = self.state_names.index("omega") self.action = np.zeros(self.action_space.shape[0]) - self.control_e = isinstance(environment.physical_system.electrical_motor, DcExternallyExcitedMotor) + self.control_e = isinstance( + environment.physical_system.electrical_motor, DcExternallyExcitedMotor + ) mp = environment.physical_system.electrical_motor.motor_parameter - self.psi_e = mp.get('psi_e', None) - self.l_e = mp.get('l_e_prime', None) - - self.action_limit_low = self.action_space.low[0] * self.nominal_values[self.u_idx] / self.limit[self.u_idx] - self.action_limit_high = self.action_space.high[0] * self.nominal_values[self.u_idx] / self.limit[self.u_idx] + self.psi_e = mp.get("psi_e", None) + self.l_e = mp.get("l_e_prime", None) + + self.action_limit_low = ( + self.action_space.low[0] + * self.nominal_values[self.u_idx] + / self.limit[self.u_idx] + ) + self.action_limit_high = ( + self.action_space.high[0] + * self.nominal_values[self.u_idx] + / self.limit[self.u_idx] + ) self.external_ref_plots = external_ref_plots for ext_ref_plot in self.external_ref_plots: @@ -41,53 +62,75 @@ def __init__(self, environment, stages, _controllers, ref_states, external_ref_p # Initialize Controller if self.control_e: # Check, if a controller for i_e is needed - assert len(stages) == 2, 'Controller design is incomplete' - assert 'i_e' in ref_states, 'No reference for i_e' - self.ref_e_idx = np.where(ref_states == 'i_e')[0][0] - self.controller_e = _controllers[stages[1][0]['controller_type']][1].make(environment, stages[1][0], - _controllers, **controller_kwargs) - self.controller = _controllers[stages[0][0]['controller_type']][1].make(environment, stages[0][0], - _controllers, **controller_kwargs) - u_e_idx = self.state_names.index('u_e') - self.action_e_limit_low = self.action_space.low[1] * self.nominal_values[u_e_idx] / self.limit[u_e_idx] - self.action_e_limit_high = self.action_space.high[1] * self.nominal_values[u_e_idx] / self.limit[u_e_idx] + assert len(stages) == 2, "Controller design is incomplete" + assert "i_e" in ref_states, "No reference for i_e" + self.ref_e_idx = np.where(ref_states == "i_e")[0][0] + self.controller_e = _controllers[stages[1][0]["controller_type"]][1].make( + environment, stages[1][0], _controllers, **controller_kwargs + ) + self.controller = _controllers[stages[0][0]["controller_type"]][1].make( + environment, stages[0][0], _controllers, **controller_kwargs + ) + u_e_idx = self.state_names.index("u_e") + self.action_e_limit_low = ( + self.action_space.low[1] + * self.nominal_values[u_e_idx] + / self.limit[u_e_idx] + ) + self.action_e_limit_high = ( + self.action_space.high[1] + * self.nominal_values[u_e_idx] + / self.limit[u_e_idx] + ) else: - if 'i_e' not in ref_states: - assert len(ref_states) <= 1, 'Too many referenced states' - self.controller = _controllers[stages[0]['controller_type']][1].make(environment, stages[0], _controllers, - **controller_kwargs) + if "i_e" not in ref_states: + assert len(ref_states) <= 1, "Too many referenced states" + self.controller = _controllers[stages[0]["controller_type"]][1].make( + environment, stages[0], _controllers, **controller_kwargs + ) def control(self, state, reference): """ - Main method that is called by the user to calculate the manipulated variable. + Main method that is called by the user to calculate the manipulated variable. - Args: - state: state of the gem environment - reference: reference for the controlled states + Args: + state: state of the gem environment + reference: reference for the controlled states - Returns: - action: action for the gem environment + Returns: + action: action for the gem environment """ - self.action[0] = self.controller.control(state[self.ref_state_idx], reference[self.ref_idx]) + self.feedforward( - state) # Calculate action + self.action[0] = self.controller.control( + state[self.ref_state_idx], reference[self.ref_idx] + ) + self.feedforward(state) # Calculate action # Limit the action and integrate the I-Controller if self.action_limit_low <= self.action[0] <= self.action_limit_high: - self.controller.integrate(state[self.ref_state_idx], reference[self.ref_idx]) + self.controller.integrate( + state[self.ref_state_idx], reference[self.ref_idx] + ) else: - self.action[0] = np.clip(self.action[0], self.action_limit_low, self.action_limit_high) + self.action[0] = np.clip( + self.action[0], self.action_limit_low, self.action_limit_high + ) # Check, if an i_e Controller is used if self.control_e: # Calculate action - self.action[1] = self.controller_e.control(state[self.i_idx], reference[self.ref_e_idx]) + self.action[1] = self.controller_e.control( + state[self.i_idx], reference[self.ref_e_idx] + ) # Limit the action and integrate the I-Controller if self.action_e_limit_low <= self.action[1] <= self.action_e_limit_high: - self.controller_e.integrate(state[self.i_idx], reference[self.ref_e_idx]) + self.controller_e.integrate( + state[self.i_idx], reference[self.ref_e_idx] + ) else: - self.action[1] = np.clip(self.action[1], self.action_e_limit_low, self.action_e_limit_high) + self.action[1] = np.clip( + self.action[1], self.action_e_limit_low, self.action_e_limit_high + ) - plot(self.external_ref_plots, self.state_names) # Plot the external data + plot(self.external_ref_plots, self.state_names) # Plot the external data return self.action @@ -104,5 +147,9 @@ def reset(self): def feedforward(self, state): # EMF compensation - psi_e = self.psi_e or self.l_e * state[self.i_idx] * self.nominal_values[self.i_idx] - return (state[self.omega_idx] * self.nominal_values[self.omega_idx] * psi_e) / self.nominal_values[self.u_idx] + psi_e = ( + self.psi_e or self.l_e * state[self.i_idx] * self.nominal_values[self.i_idx] + ) + return ( + state[self.omega_idx] * self.nominal_values[self.omega_idx] * psi_e + ) / self.nominal_values[self.u_idx] diff --git a/examples/classic_controllers/controllers/continuous_controller.py b/examples/classic_controllers/controllers/continuous_controller.py index 8c3c1c76..6445e44f 100644 --- a/examples/classic_controllers/controllers/continuous_controller.py +++ b/examples/classic_controllers/controllers/continuous_controller.py @@ -3,7 +3,9 @@ class ContinuousController: @classmethod def make(cls, environment, stage, _controllers, **controller_kwargs): - controller = _controllers[stage['controller_type']][2](environment, param_dict=stage, **controller_kwargs) + controller = _controllers[stage["controller_type"]][2]( + environment, param_dict=stage, **controller_kwargs + ) return controller def control(self, state, reference): diff --git a/examples/classic_controllers/controllers/dicrete_action_controller.py b/examples/classic_controllers/controllers/dicrete_action_controller.py index 4785a192..b0e04e26 100644 --- a/examples/classic_controllers/controllers/dicrete_action_controller.py +++ b/examples/classic_controllers/controllers/dicrete_action_controller.py @@ -6,18 +6,32 @@ class DiscreteActionController: """ - This class is used for current control of all DC motor systems with discrete actions. By default, a three-point - controller is used. For the externally excited dc motor, the excitation current is also controlled. + This class is used for current control of all DC motor systems with discrete actions. By default, a three-point + controller is used. For the externally excited dc motor, the excitation current is also controlled. """ - def __init__(self, environment, stages, _controllers, ref_states, external_ref_plots=(), **controller_kwargs): - assert type(environment.action_space) in [Discrete, MultiDiscrete] and isinstance(environment.physical_system, - DcMotorSystem), 'No suitable action space for Discrete Action Controller' + def __init__( + self, + environment, + stages, + _controllers, + ref_states, + external_ref_plots=(), + **controller_kwargs, + ): + assert type(environment.action_space) in [ + Discrete, + MultiDiscrete, + ] and isinstance( + environment.physical_system, DcMotorSystem + ), "No suitable action space for Discrete Action Controller" - self.ref_idx = np.where(ref_states != 'i_e')[0][0] + self.ref_idx = np.where(ref_states != "i_e")[0][0] self.ref_state_idx = environment.state_names.index(ref_states[self.ref_idx]) self.i_idx = environment.physical_system.CURRENTS_IDX[-1] - self.control_e = isinstance(environment.physical_system.electrical_motor, DcExternallyExcitedMotor) + self.control_e = isinstance( + environment.physical_system.electrical_motor, DcExternallyExcitedMotor + ) self.state_names = environment.state_names self.external_ref_plots = external_ref_plots @@ -26,40 +40,50 @@ def __init__(self, environment, stages, _controllers, ref_states, external_ref_p # Initialize Controller if self.control_e: # Check, if a controller for i_e is needed - assert len(stages) == 2, 'Controller design is incomplete' - assert 'i_e' in ref_states, 'No reference for i_e' - self.ref_e_idx = np.where(ref_states == 'i_e')[0][0] - self.controller_e = _controllers[stages[1][0]['controller_type']][1].make(environment, stages[1][0], - _controllers, - control_e=True, - **controller_kwargs) - self.controller = _controllers[stages[0][0]['controller_type']][1].make(environment, stages[0][0], - **controller_kwargs) + assert len(stages) == 2, "Controller design is incomplete" + assert "i_e" in ref_states, "No reference for i_e" + self.ref_e_idx = np.where(ref_states == "i_e")[0][0] + self.controller_e = _controllers[stages[1][0]["controller_type"]][1].make( + environment, + stages[1][0], + _controllers, + control_e=True, + **controller_kwargs, + ) + self.controller = _controllers[stages[0][0]["controller_type"]][1].make( + environment, stages[0][0], **controller_kwargs + ) else: - assert len(ref_states) <= 1, 'Too many referenced states' - self.controller = _controllers[stages[0]['controller_type']][1].make(environment, stages[0], - _controllers, - **controller_kwargs) + assert len(ref_states) <= 1, "Too many referenced states" + self.controller = _controllers[stages[0]["controller_type"]][1].make( + environment, stages[0], _controllers, **controller_kwargs + ) def control(self, state, reference): """ - Main method that is called by the user to calculate the manipulated variable. + Main method that is called by the user to calculate the manipulated variable. - Args: - state: state of the gem environment - reference: reference for the controlled states + Args: + state: state of the gem environment + reference: reference for the controlled states - Returns: - action: action for the gem environment + Returns: + action: action for the gem environment """ - plot(self.external_ref_plots, self.state_names) # Plot external data + plot(self.external_ref_plots, self.state_names) # Plot external data # Check if i_e controller is used if self.control_e: - return [self.controller.control(state[self.ref_state_idx], reference[self.ref_idx]), - self.controller_e.control(state[self.i_idx], reference[self.ref_e_idx])] + return [ + self.controller.control( + state[self.ref_state_idx], reference[self.ref_idx] + ), + self.controller_e.control(state[self.i_idx], reference[self.ref_e_idx]), + ] else: - return self.controller.control(state[self.ref_state_idx], reference[self.ref_idx]) + return self.controller.control( + state[self.ref_state_idx], reference[self.ref_idx] + ) @staticmethod def get_plot_data(): diff --git a/examples/classic_controllers/controllers/discrete_controller.py b/examples/classic_controllers/controllers/discrete_controller.py index d17fbfdb..b0934374 100644 --- a/examples/classic_controllers/controllers/discrete_controller.py +++ b/examples/classic_controllers/controllers/discrete_controller.py @@ -3,8 +3,8 @@ class DiscreteController: """ - The DiscreteController is the base class for the base discrete controllers (OnOff controller and three-point - controller). + The DiscreteController is the base class for the base discrete controllers (OnOff controller and three-point + controller). """ @classmethod @@ -16,8 +16,12 @@ def make(cls, environment, stage, _controllers, **controller_kwargs): else: action_space_n = 3 - controller = _controllers[stage['controller_type']][2](environment, action_space=action_space_n, - param_dict=stage, **controller_kwargs) + controller = _controllers[stage["controller_type"]][2]( + environment, + action_space=action_space_n, + param_dict=stage, + **controller_kwargs, + ) return controller def control(self, state, reference): diff --git a/examples/classic_controllers/controllers/flux_observer.py b/examples/classic_controllers/controllers/flux_observer.py index 1c21d2c3..f785d5d7 100644 --- a/examples/classic_controllers/controllers/flux_observer.py +++ b/examples/classic_controllers/controllers/flux_observer.py @@ -3,34 +3,41 @@ class FluxObserver: """ - This class represents a rotor flux observer for an induction motor base on a current model. Further information - can be found at https://ieeexplore.ieee.org/document/4270863. + This class represents a rotor flux observer for an induction motor base on a current model. Further information + can be found at https://ieeexplore.ieee.org/document/4270863. """ + def __init__(self, env): mp = env.physical_system.electrical_motor.motor_parameter - self.l_m = mp['l_m'] # Main induction - self.l_r = mp['l_m'] + mp['l_sigr'] # Induction of the rotor - self.r_r = mp['r_r'] # Rotor resistance - self.p = mp['p'] # Pole pair number - self.tau = env.physical_system.tau # Sampling time + self.l_m = mp["l_m"] # Main induction + self.l_r = mp["l_m"] + mp["l_sigr"] # Induction of the rotor + self.r_r = mp["r_r"] # Rotor resistance + self.p = mp["p"] # Pole pair number + self.tau = env.physical_system.tau # Sampling time # function to transform the currents from abc to alpha/beta coordinates - self.abc_to_alphabeta_transformation = env.physical_system.abc_to_alphabeta_space + self.abc_to_alphabeta_transformation = ( + env.physical_system.abc_to_alphabeta_space + ) # Integrated values of the flux for the two directions (Re: alpha, Im: beta) self.integrated = np.complex(0, 0) - self.i_s_idx = [env.state_names.index('i_sa'), env.state_names.index('i_sb'), env.state_names.index('i_sc')] - self.omega_idx = env.state_names.index('omega') + self.i_s_idx = [ + env.state_names.index("i_sa"), + env.state_names.index("i_sb"), + env.state_names.index("i_sc"), + ] + self.omega_idx = env.state_names.index("omega") def estimate(self, state): """ - Method to estimate the flux of an induction motor + Method to estimate the flux of an induction motor - Args: - state: state of the gym-electric-motor environment + Args: + state: state of the gym-electric-motor environment - Returns: - Amount and angle of the estimated flux + Returns: + Amount and angle of the estimated flux """ i_s = state[self.i_s_idx] @@ -40,8 +47,11 @@ def estimate(self, state): [i_s_alpha, i_s_beta] = self.abc_to_alphabeta_transformation(i_s) # Calculate delta flux - delta = np.complex(i_s_alpha, i_s_beta) * self.r_r * self.l_m / self.l_r - self.integrated * np.complex( - self.r_r / self.l_r, -omega) + delta = np.complex( + i_s_alpha, i_s_beta + ) * self.r_r * self.l_m / self.l_r - self.integrated * np.complex( + self.r_r / self.l_r, -omega + ) # Integrate the flux self.integrated += delta * self.tau diff --git a/examples/classic_controllers/controllers/foc_controller.py b/examples/classic_controllers/controllers/foc_controller.py index f0918c85..7fce5e32 100644 --- a/examples/classic_controllers/controllers/foc_controller.py +++ b/examples/classic_controllers/controllers/foc_controller.py @@ -6,25 +6,35 @@ class FieldOrientedController: """ - This class controls the currents of synchronous motors. In the case of continuous manipulated variables, the - control is performed in the rotating dq-coordinates. For this purpose, the two current components are optionally - decoupled and two independent current controllers are used. - In the case of discrete manipulated variables, control takes place in stator-fixed coordinates. The reference - values are converted into these coordinates so that a on-off controller calculates the corresponding - manipulated variable for each current component. + This class controls the currents of synchronous motors. In the case of continuous manipulated variables, the + control is performed in the rotating dq-coordinates. For this purpose, the two current components are optionally + decoupled and two independent current controllers are used. + In the case of discrete manipulated variables, control takes place in stator-fixed coordinates. The reference + values are converted into these coordinates so that a on-off controller calculates the corresponding + manipulated variable for each current component. """ - def __init__(self, environment, stages, _controllers, ref_states, external_ref_plots=(), **controller_kwargs): - assert isinstance(environment.physical_system, SynchronousMotorSystem), 'No suitable Environment for FOC Controller' + def __init__( + self, + environment, + stages, + _controllers, + ref_states, + external_ref_plots=(), + **controller_kwargs, + ): + assert isinstance( + environment.physical_system, SynchronousMotorSystem + ), "No suitable Environment for FOC Controller" t32 = environment.physical_system.electrical_motor.t_32 q = environment.physical_system.electrical_motor.q - self.backward_transformation = (lambda quantities, eps: t32(q(quantities, eps))) + self.backward_transformation = lambda quantities, eps: t32(q(quantities, eps)) self.tau = environment.physical_system.tau - self.ref_d_idx = np.where(ref_states == 'i_sd')[0][0] - self.ref_q_idx = np.where(ref_states == 'i_sq')[0][0] + self.ref_d_idx = np.where(ref_states == "i_sd")[0][0] + self.ref_q_idx = np.where(ref_states == "i_sq")[0][0] self.d_idx = environment.state_names.index(ref_states[self.ref_d_idx]) self.q_idx = environment.state_names.index(ref_states[self.ref_q_idx]) @@ -33,20 +43,22 @@ def __init__(self, environment, stages, _controllers, ref_states, external_ref_p self.state_space = environment.physical_system.state_space self.state_names = environment.state_names - self.i_sd_idx = environment.state_names.index('i_sd') - self.i_sq_idx = environment.state_names.index('i_sq') - self.u_sd_idx = environment.state_names.index('u_sd') - self.u_sq_idx = environment.state_names.index('u_sq') - self.u_a_idx = environment.state_names.index('u_a') - self.u_b_idx = environment.state_names.index('u_b') - self.u_c_idx = environment.state_names.index('u_c') - self.omega_idx = environment.state_names.index('omega') - self.eps_idx = environment.state_names.index('epsilon') + self.i_sd_idx = environment.state_names.index("i_sd") + self.i_sq_idx = environment.state_names.index("i_sq") + self.u_sd_idx = environment.state_names.index("u_sd") + self.u_sq_idx = environment.state_names.index("u_sq") + self.u_a_idx = environment.state_names.index("u_a") + self.u_b_idx = environment.state_names.index("u_b") + self.u_c_idx = environment.state_names.index("u_c") + self.omega_idx = environment.state_names.index("omega") + self.eps_idx = environment.state_names.index("epsilon") self.limit = environment.physical_system.limits self.mp = environment.physical_system.electrical_motor.motor_parameter - self.psi_p = self.mp.get('psi_p', 0) - self.dead_time = 1.5 if environment.physical_system.converter._dead_time else 0.5 + self.psi_p = self.mp.get("psi_p", 0) + self.dead_time = ( + 1.5 if environment.physical_system.converter._dead_time else 0.5 + ) self.has_cont_action_space = type(self.action_space) is Box @@ -56,74 +68,123 @@ def __init__(self, environment, stages, _controllers, ref_states, external_ref_p # Initialize continuous controllers if self.has_cont_action_space: - assert len(stages[0]) == 2, 'Number of stages not correct' - self.decoupling = controller_kwargs.get('decoupling', True) + assert len(stages[0]) == 2, "Number of stages not correct" + self.decoupling = controller_kwargs.get("decoupling", True) [self.u_sq_0, self.u_sd_0] = [0, 0] - self.d_controller = _controllers[stages[0][0]['controller_type']][1].make( - environment, stages[0][0], _controllers, **controller_kwargs) - self.q_controller = _controllers[stages[0][1]['controller_type']][1].make( - environment, stages[0][1], _controllers, **controller_kwargs) + self.d_controller = _controllers[stages[0][0]["controller_type"]][1].make( + environment, stages[0][0], _controllers, **controller_kwargs + ) + self.q_controller = _controllers[stages[0][1]["controller_type"]][1].make( + environment, stages[0][1], _controllers, **controller_kwargs + ) # Initialize discrete controllers else: - assert len(stages) == 3, 'Number of stages not correct' - self.abc_controller = [_controllers[stages[0][0]['controller_type']][1].make( - environment, stages[i][0], _controllers, **controller_kwargs) for i in range(3)] - self.i_abc_idx = [environment.state_names.index(state) for state in ['i_a', 'i_b', 'i_c']] + assert len(stages) == 3, "Number of stages not correct" + self.abc_controller = [ + _controllers[stages[0][0]["controller_type"]][1].make( + environment, stages[i][0], _controllers, **controller_kwargs + ) + for i in range(3) + ] + self.i_abc_idx = [ + environment.state_names.index(state) for state in ["i_a", "i_b", "i_c"] + ] def control(self, state, reference): """ - Main method that is called by the user to calculate the manipulated variable. + Main method that is called by the user to calculate the manipulated variable. - Args: - state: state of the gem environment - reference: reference for the controlled states + Args: + state: state of the gem environment + reference: reference for the controlled states - Returns: - action: action for the gem environment + Returns: + action: action for the gem environment """ # Calculate delta epsilon - epsilon_d = state[self.eps_idx] * self.limit[self.eps_idx] + self.dead_time * self.tau * \ - state[self.omega_idx] * self.limit[self.omega_idx] * self.mp['p'] + epsilon_d = ( + state[self.eps_idx] * self.limit[self.eps_idx] + + self.dead_time + * self.tau + * state[self.omega_idx] + * self.limit[self.omega_idx] + * self.mp["p"] + ) # Check if action space is continuous if self.has_cont_action_space: - # Decoupling of the d- and q-component if self.decoupling: - self.u_sd_0 = -state[self.omega_idx] * self.mp['p'] * self.mp['l_q'] * state[self.i_sq_idx] * self.limit[ - self.i_sq_idx] / self.limit[self.u_sd_idx] * self.limit[self.omega_idx] - self.u_sq_0 = state[self.omega_idx] * self.mp['p'] * ( - state[self.i_sd_idx] * self.mp['l_d'] * self.limit[self.i_sd_idx] + self.psi_p) / self.limit[ - self.u_sq_idx] * self.limit[self.omega_idx] + self.u_sd_0 = ( + -state[self.omega_idx] + * self.mp["p"] + * self.mp["l_q"] + * state[self.i_sq_idx] + * self.limit[self.i_sq_idx] + / self.limit[self.u_sd_idx] + * self.limit[self.omega_idx] + ) + self.u_sq_0 = ( + state[self.omega_idx] + * self.mp["p"] + * ( + state[self.i_sd_idx] + * self.mp["l_d"] + * self.limit[self.i_sd_idx] + + self.psi_p + ) + / self.limit[self.u_sq_idx] + * self.limit[self.omega_idx] + ) # Calculate the two actions - u_sd = self.d_controller.control(state[self.d_idx], reference[self.ref_d_idx]) + self.u_sd_0 - u_sq = self.q_controller.control(state[self.q_idx], reference[self.ref_q_idx]) + self.u_sq_0 + u_sd = ( + self.d_controller.control(state[self.d_idx], reference[self.ref_d_idx]) + + self.u_sd_0 + ) + u_sq = ( + self.q_controller.control(state[self.q_idx], reference[self.ref_q_idx]) + + self.u_sq_0 + ) # Shifting the reference potential action_temp = self.backward_transformation((u_sd, u_sq), epsilon_d) action_temp = action_temp - 0.5 * (max(action_temp) + min(action_temp)) # Check limit and integrate - action = np.clip(action_temp, self.action_space.low[0], self.action_space.high[0]) + action = np.clip( + action_temp, self.action_space.low[0], self.action_space.high[0] + ) if (action == action_temp).all(): - self.d_controller.integrate(state[self.d_idx], reference[self.ref_d_idx]) - self.q_controller.integrate(state[self.q_idx], reference[self.ref_q_idx]) + self.d_controller.integrate( + state[self.d_idx], reference[self.ref_d_idx] + ) + self.q_controller.integrate( + state[self.q_idx], reference[self.ref_q_idx] + ) else: # Transform reference in abc coordinates - ref_abc = self.backward_transformation((reference[self.ref_d_idx], reference[self.ref_q_idx]), epsilon_d) + ref_abc = self.backward_transformation( + (reference[self.ref_d_idx], reference[self.ref_q_idx]), epsilon_d + ) action = 0 # Calculate discrete action for i in range(3): - action += (2 ** (2 - i)) * self.abc_controller[i].control(state[self.i_abc_idx[i]], ref_abc[i]) + action += (2 ** (2 - i)) * self.abc_controller[i].control( + state[self.i_abc_idx[i]], ref_abc[i] + ) # Plot external data - plot(self.external_ref_plots, self.state_names, external_data=self.get_plot_data()) + plot( + self.external_ref_plots, + self.state_names, + external_data=self.get_plot_data(), + ) return action @staticmethod diff --git a/examples/classic_controllers/controllers/induction_motor_cascaded_foc.py b/examples/classic_controllers/controllers/induction_motor_cascaded_foc.py index b6ee8229..5a175f62 100644 --- a/examples/classic_controllers/controllers/induction_motor_cascaded_foc.py +++ b/examples/classic_controllers/controllers/induction_motor_cascaded_foc.py @@ -1,5 +1,7 @@ from .continuous_controller import ContinuousController -from .induction_motor_torque_to_current_conversion import InductionMotorTorqueToCurrentConversion +from .induction_motor_torque_to_current_conversion import ( + InductionMotorTorqueToCurrentConversion, +) from .flux_observer import FluxObserver from .plot_external_data import plot from gymnasium.spaces import Box @@ -8,16 +10,23 @@ class InductionMotorCascadedFieldOrientedController: """ - This controller is used for torque or speed control of induction motors. The controller consists of a field - oriented controller for current control, an efficiency-optimized torque controller and an optional speed - controller. The current control is equivalent to the current control of the FieldOrientedControllerRotorFluxObserver. - The TorqueToCurrentConversionRotorFluxObserver is used for torque control and a PI-Controller by default is used - for speed control. + This controller is used for torque or speed control of induction motors. The controller consists of a field + oriented controller for current control, an efficiency-optimized torque controller and an optional speed + controller. The current control is equivalent to the current control of the FieldOrientedControllerRotorFluxObserver. + The TorqueToCurrentConversionRotorFluxObserver is used for torque control and a PI-Controller by default is used + for speed control. """ - def __init__(self, environment, stages, _controllers, ref_states, external_ref_plots=(), external_plot=(), - **controller_kwargs): - + def __init__( + self, + environment, + stages, + _controllers, + ref_states, + external_ref_plots=(), + external_plot=(), + **controller_kwargs, + ): self.env = environment self.action_space = environment.action_space self.has_cont_action_space = type(self.action_space) is Box @@ -26,42 +35,48 @@ def __init__(self, environment, stages, _controllers, ref_states, external_ref_p self.stages = stages self.flux_observer = FluxObserver(self.env) - self.i_sd_idx = self.env.state_names.index('i_sd') - self.i_sq_idx = self.env.state_names.index('i_sq') - self.u_s_abc_idx = [self.env.state_names.index(state) for state in ['u_sa', 'u_sb', 'u_sc']] - self.omega_idx = self.env.state_names.index('omega') - self.torque_idx = self.env.state_names.index('torque') + self.i_sd_idx = self.env.state_names.index("i_sd") + self.i_sq_idx = self.env.state_names.index("i_sq") + self.u_s_abc_idx = [ + self.env.state_names.index(state) for state in ["u_sa", "u_sb", "u_sc"] + ] + self.omega_idx = self.env.state_names.index("omega") + self.torque_idx = self.env.state_names.index("torque") mp = self.env.physical_system.electrical_motor.motor_parameter - self.p = mp['p'] - self.l_m = mp['l_m'] - self.l_sigma_s = mp['l_sigs'] - self.l_r = self.l_m + mp['l_sigr'] - self.l_s = self.l_m + mp['l_sigs'] - self.r_r = mp['r_r'] - self.r_s = mp['r_s'] + self.p = mp["p"] + self.l_m = mp["l_m"] + self.l_sigma_s = mp["l_sigs"] + self.l_r = self.l_m + mp["l_sigr"] + self.l_s = self.l_m + mp["l_sigs"] + self.r_r = mp["r_r"] + self.r_s = mp["r_s"] self.tau_r = self.l_r / self.r_r - self.sigma = (self.l_s * self.l_r - self.l_m ** 2) / (self.l_s * self.l_r) + self.sigma = (self.l_s * self.l_r - self.l_m**2) / (self.l_s * self.l_r) self.limits = self.env.physical_system.limits self.nominal_values = self.env.physical_system.nominal_state - self.tau_sigma = self.sigma * self.l_s / (self.r_s + self.r_r * self.l_m ** 2 / self.l_r ** 2) + self.tau_sigma = ( + self.sigma * self.l_s / (self.r_s + self.r_r * self.l_m**2 / self.l_r**2) + ) self.tau = self.env.physical_system.tau self.dq_to_abc_transformation = environment.physical_system.dq_to_abc_space - self.torque_control = 'torque' in ref_states or 'omega' in ref_states - self.current_control = 'i_sd' in ref_states - self.omega_control = 'omega' in ref_states + self.torque_control = "torque" in ref_states or "omega" in ref_states + self.current_control = "i_sd" in ref_states + self.omega_control = "omega" in ref_states self.ref_state_idx = [self.i_sq_idx, self.i_sd_idx] if self.current_control: - self.ref_d_idx = np.where(ref_states == 'i_sd')[0][0] - self.ref_idx = np.where(ref_states != 'i_sd')[0][0] + self.ref_d_idx = np.where(ref_states == "i_sd")[0][0] + self.ref_idx = np.where(ref_states != "i_sd")[0][0] # Initialize torque controller if self.torque_control: self.ref_state_idx.append(self.torque_idx) - self.torque_controller = InductionMotorTorqueToCurrentConversion(environment, stages) + self.torque_controller = InductionMotorTorqueToCurrentConversion( + environment, stages + ) if self.omega_control: self.ref_state_idx.append(self.omega_idx) @@ -73,89 +88,141 @@ def __init__(self, environment, stages, _controllers, ref_states, external_ref_p self.external_plot = external_plot self.external_ref_plots = external_ref_plots self.external_ref_plots = external_ref_plots - plot_ref = np.append(np.array([environment.state_names[i] for i in self.ref_state_idx]), ref_states) + plot_ref = np.append( + np.array([environment.state_names[i] for i in self.ref_state_idx]), + ref_states, + ) for ext_ref_plot in self.external_ref_plots: ext_ref_plot.set_reference(plot_ref) labels = [ - {'y_label': r"|$\Psi_{r}$|/Vs", 'state_label': r"|$\hat{\Psi}_{r}$|/Vs", 'ref_label': r"|$\Psi_{r}$|$^*$/Vs"}, - {'y_label': r"$\measuredangle\Psi_r$/rad", 'state_label': r"$\measuredangle\hat{\Psi}_r$/rad"}] + { + "y_label": r"|$\Psi_{r}$|/Vs", + "state_label": r"|$\hat{\Psi}_{r}$|/Vs", + "ref_label": r"|$\Psi_{r}$|$^*$/Vs", + }, + { + "y_label": r"$\measuredangle\Psi_r$/rad", + "state_label": r"$\measuredangle\hat{\Psi}_r$/rad", + }, + ] for ext_plot, label in zip(self.external_plot, labels): ext_plot.set_label(label) # Initialize continuous controllers if self.has_cont_action_space: - assert len(stages[0]) == 2, 'Number of stages not correct' - self.decoupling = controller_kwargs.get('decoupling', True) + assert len(stages[0]) == 2, "Number of stages not correct" + self.decoupling = controller_kwargs.get("decoupling", True) self.u_sd_0 = self.u_sq_0 = 0 - self.d_controller = _controllers[stages[0][0]['controller_type']][1].make( - environment, stages[0][0], _controllers, **controller_kwargs) - self.q_controller = _controllers[stages[0][1]['controller_type']][1].make( - environment, stages[0][1], _controllers, **controller_kwargs) + self.d_controller = _controllers[stages[0][0]["controller_type"]][1].make( + environment, stages[0][0], _controllers, **controller_kwargs + ) + self.q_controller = _controllers[stages[0][1]["controller_type"]][1].make( + environment, stages[0][1], _controllers, **controller_kwargs + ) if self.omega_control: - self.overlaid_controller = [_controllers[stages[1][i]['controller_type']][1].make(environment, - stages[1][i], _controllers, cascaded=True, **controller_kwargs) for i in range(0, len(stages[1]))] - self.overlaid_type = [_controllers[stages[1][i]['controller_type']][1] == ContinuousController for i in - range(0, len(stages[1]))] - - self.ref = np.zeros(len(self.ref_state_idx)) # Define array for reference values + self.overlaid_controller = [ + _controllers[stages[1][i]["controller_type"]][1].make( + environment, + stages[1][i], + _controllers, + cascaded=True, + **controller_kwargs, + ) + for i in range(0, len(stages[1])) + ] + self.overlaid_type = [ + _controllers[stages[1][i]["controller_type"]][1] + == ContinuousController + for i in range(0, len(stages[1])) + ] + + self.ref = np.zeros( + len(self.ref_state_idx) + ) # Define array for reference values def control(self, state, reference): """ - This main method of the InductionMotorCascadedFieldOrientedController is called by the user. It calculates - the input voltages u_a,b,c. + This main method of the InductionMotorCascadedFieldOrientedController is called by the user. It calculates + the input voltages u_a,b,c. - Args: - state: state of the gem environment - reference: reference for the controlled states + Args: + state: state of the gem environment + reference: reference for the controlled states - Returns: - action: action for the gem environment + Returns: + action: action for the gem environment """ self.ref[-1] = reference[self.ref_idx] # Set the reference - self.psi_abs, self.psi_angle = self.flux_observer.estimate(state * self.limits) # Estimate the flux + self.psi_abs, self.psi_angle = self.flux_observer.estimate( + state * self.limits + ) # Estimate the flux # Iterate through the overlaid controller stages if self.omega_control: for i in range(len(self.overlaid_controller) + 1, 1, -1): # Calculate reference - self.ref[i] = self.overlaid_controller[i-2].control(state[self.ref_state_idx[i + 1]], self.ref[i + 1]) + self.ref[i] = self.overlaid_controller[i - 2].control( + state[self.ref_state_idx[i + 1]], self.ref[i + 1] + ) # Check limit and integrate - if (0.85 * self.state_space.low[self.ref_state_idx[i]] <= self.ref[i] <= 0.85 * - self.state_space.high[self.ref_state_idx[i]]) and self.overlaid_type[i - 2]: - self.overlaid_controller[i - 2].integrate(state[self.ref_state_idx[i + 1]], self.ref[i + 1]) + if ( + 0.85 * self.state_space.low[self.ref_state_idx[i]] + <= self.ref[i] + <= 0.85 * self.state_space.high[self.ref_state_idx[i]] + ) and self.overlaid_type[i - 2]: + self.overlaid_controller[i - 2].integrate( + state[self.ref_state_idx[i + 1]], self.ref[i + 1] + ) else: - self.ref[i] = np.clip(self.ref[i], self.nominal_values[self.ref_state_idx[i]] / self.limits[ - self.ref_state_idx[i]] * self.state_space.low[self.ref_state_idx[i]], - self.nominal_values[self.ref_state_idx[i]] / self.limits[ - self.ref_state_idx[i]] * self.state_space.high[self.ref_state_idx[i]]) + self.ref[i] = np.clip( + self.ref[i], + self.nominal_values[self.ref_state_idx[i]] + / self.limits[self.ref_state_idx[i]] + * self.state_space.low[self.ref_state_idx[i]], + self.nominal_values[self.ref_state_idx[i]] + / self.limits[self.ref_state_idx[i]] + * self.state_space.high[self.ref_state_idx[i]], + ) # Calculate reference values for i_d and i_q if self.torque_control: torque = self.ref[2] * self.limits[self.torque_idx] - self.ref[0], self.ref[1], self.psi_opt = self.torque_controller.control(state, torque, self.psi_abs) + self.ref[0], self.ref[1], self.psi_opt = self.torque_controller.control( + state, torque, self.psi_abs + ) if self.has_cont_action_space: - state = state * self.limits # Denormalize the state + state = state * self.limits # Denormalize the state omega_me = state[self.omega_idx] i_sd = state[self.i_sd_idx] i_sq = state[self.i_sq_idx] - omega_s = omega_me + self.r_r * self.l_m / self.l_r * i_sq / max(np.abs(self.psi_abs), 1e-4) * np.sign(self.psi_abs) + omega_s = omega_me + self.r_r * self.l_m / self.l_r * i_sq / max( + np.abs(self.psi_abs), 1e-4 + ) * np.sign(self.psi_abs) # Calculate delate u_sd, u_sq - u_sd_delta = self.d_controller.control(state[self.i_sd_idx], - self.ref[1] * self.limits[self.i_sd_idx]) - u_sq_delta = self.q_controller.control(state[self.i_sq_idx], - self.ref[0] * self.limits[self.i_sq_idx]) + u_sd_delta = self.d_controller.control( + state[self.i_sd_idx], self.ref[1] * self.limits[self.i_sd_idx] + ) + u_sq_delta = self.q_controller.control( + state[self.i_sq_idx], self.ref[0] * self.limits[self.i_sq_idx] + ) # Decouple the two current components if self.decoupling: - self.u_sd_0 = -omega_s * self.sigma * self.l_s * i_sq - self.l_m * self.r_r / (self.l_r ** 2) * self.psi_abs - self.u_sq_0 = omega_s * self.sigma * self.l_s * i_sd + omega_me * self.l_m / self.l_r * self.psi_abs + self.u_sd_0 = ( + -omega_s * self.sigma * self.l_s * i_sq + - self.l_m * self.r_r / (self.l_r**2) * self.psi_abs + ) + self.u_sq_0 = ( + omega_s * self.sigma * self.l_s * i_sd + + omega_me * self.l_m / self.l_r * self.psi_abs + ) u_sd = self.u_sd_0 + u_sd_delta u_sq = self.u_sq_0 + u_sq_delta @@ -167,22 +234,30 @@ def control(self, state, reference): # Limit the action and integrate action = np.clip(u_s_abc, self.action_space.low, self.action_space.high) if (action == u_s_abc).all(): - self.d_controller.integrate(state[self.i_sd_idx], - self.ref[1] * self.limits[self.i_sd_idx]) - self.q_controller.integrate(state[self.i_sq_idx], - self.ref[0] * self.limits[self.i_sq_idx]) + self.d_controller.integrate( + state[self.i_sd_idx], self.ref[1] * self.limits[self.i_sd_idx] + ) + self.q_controller.integrate( + state[self.i_sq_idx], self.ref[0] * self.limits[self.i_sq_idx] + ) # Plot the external data - plot(external_reference_plots=self.external_ref_plots, state_names=self.state_names, - external_plot=self.external_plot, external_data=self.get_plot_data()) + plot( + external_reference_plots=self.external_ref_plots, + state_names=self.state_names, + external_plot=self.external_plot, + external_data=self.get_plot_data(), + ) return action def get_plot_data(self): # Getting the external data that should be plotted - return dict(ref_state=self.ref_state_idx[:-1], ref_value=self.ref[:-1], - external=[[self.psi_abs, self.psi_opt], - [self.psi_angle]]) + return dict( + ref_state=self.ref_state_idx[:-1], + ref_value=self.ref[:-1], + external=[[self.psi_abs, self.psi_opt], [self.psi_angle]], + ) def reset(self): # Reset the Controllers and the observer diff --git a/examples/classic_controllers/controllers/induction_motor_foc.py b/examples/classic_controllers/controllers/induction_motor_foc.py index 699cd06e..dc5c6f3b 100644 --- a/examples/classic_controllers/controllers/induction_motor_foc.py +++ b/examples/classic_controllers/controllers/induction_motor_foc.py @@ -6,14 +6,22 @@ class InductionMotorFieldOrientedController: """ - This class controls the currents of induction motors using a field oriented controller. The control is performed - in the rotating dq-stator-coordinates. For this purpose, the two current components are optionally decoupled and - two independent current controllers are used. The rotor flux required for this is estimated based on a current - model. + This class controls the currents of induction motors using a field oriented controller. The control is performed + in the rotating dq-stator-coordinates. For this purpose, the two current components are optionally decoupled and + two independent current controllers are used. The rotor flux required for this is estimated based on a current + model. """ - def __init__(self, environment, stages, _controllers, ref_states, external_ref_plots=(), external_plot=(), - **controller_kwargs): + def __init__( + self, + environment, + stages, + _controllers, + ref_states, + external_ref_plots=(), + external_plot=(), + **controller_kwargs, + ): self.env = environment self.action_space = environment.action_space self.has_cont_action_space = type(self.action_space) is Box @@ -22,25 +30,29 @@ def __init__(self, environment, stages, _controllers, ref_states, external_ref_p self.stages = stages self.flux_observer = FluxObserver(self.env) - self.i_sd_idx = self.env.state_names.index('i_sd') - self.i_sq_idx = self.env.state_names.index('i_sq') - self.u_s_abc_idx = [self.env.state_names.index(state) for state in ['u_sa', 'u_sb', 'u_sc']] - self.i_sd_ref_idx = np.where(ref_states == 'i_sd')[0][0] - self.i_sq_ref_idx = np.where(ref_states == 'i_sq')[0][0] - self.omega_idx = self.env.state_names.index('omega') + self.i_sd_idx = self.env.state_names.index("i_sd") + self.i_sq_idx = self.env.state_names.index("i_sq") + self.u_s_abc_idx = [ + self.env.state_names.index(state) for state in ["u_sa", "u_sb", "u_sc"] + ] + self.i_sd_ref_idx = np.where(ref_states == "i_sd")[0][0] + self.i_sq_ref_idx = np.where(ref_states == "i_sq")[0][0] + self.omega_idx = self.env.state_names.index("omega") mp = self.env.physical_system.electrical_motor.motor_parameter - self.p = mp['p'] - self.l_m = mp['l_m'] - self.l_sigma_s = mp['l_sigs'] - self.l_r = self.l_m + mp['l_sigr'] - self.l_s = self.l_m + mp['l_sigs'] - self.r_r = mp['r_r'] - self.r_s = mp['r_s'] + self.p = mp["p"] + self.l_m = mp["l_m"] + self.l_sigma_s = mp["l_sigs"] + self.l_r = self.l_m + mp["l_sigr"] + self.l_s = self.l_m + mp["l_sigs"] + self.r_r = mp["r_r"] + self.r_s = mp["r_s"] self.tau_r = self.l_r / self.r_r - self.sigma = (self.l_s * self.l_r - self.l_m ** 2) / (self.l_s * self.l_r) + self.sigma = (self.l_s * self.l_r - self.l_m**2) / (self.l_s * self.l_r) self.limits = self.env.physical_system.limits - self.tau_sigma = self.sigma * self.l_s / (self.r_s + self.r_r * self.l_m**2 / self.l_r**2) + self.tau_sigma = ( + self.sigma * self.l_s / (self.r_s + self.r_r * self.l_m**2 / self.l_r**2) + ) self.tau = self.env.physical_system.tau self.dq_to_abc_transformation = environment.physical_system.dq_to_abc_space @@ -53,52 +65,75 @@ def __init__(self, environment, stages, _controllers, ref_states, external_ref_p for ext_ref_plot in self.external_ref_plots: ext_ref_plot.set_reference(ref_states) - labels = [{'y_label': r"|$\Psi_{r}$|/Vs", 'state_label': r"|$\hat{\Psi}_{r}$|/Vs"}, - {'y_label': r"$\measuredangle\Psi_r$/rad", 'state_label': r"$\measuredangle\hat{\Psi}_r$/rad"}] + labels = [ + {"y_label": r"|$\Psi_{r}$|/Vs", "state_label": r"|$\hat{\Psi}_{r}$|/Vs"}, + { + "y_label": r"$\measuredangle\Psi_r$/rad", + "state_label": r"$\measuredangle\hat{\Psi}_r$/rad", + }, + ] for ext_plot, label in zip(self.external_plot, labels): ext_plot.set_label(label) # Initialize continuous controllers if self.has_cont_action_space: - assert len(stages[0]) == 2, 'Number of stages not correct' - self.decoupling = controller_kwargs.get('decoupling', True) + assert len(stages[0]) == 2, "Number of stages not correct" + self.decoupling = controller_kwargs.get("decoupling", True) self.u_sd_0 = self.u_sq_0 = 0 - self.d_controller = _controllers[stages[0][0]['controller_type']][1].make( - environment, stages[0][0], _controllers, **controller_kwargs) - self.q_controller = _controllers[stages[0][1]['controller_type']][1].make( - environment, stages[0][1], _controllers, **controller_kwargs) + self.d_controller = _controllers[stages[0][0]["controller_type"]][1].make( + environment, stages[0][0], _controllers, **controller_kwargs + ) + self.q_controller = _controllers[stages[0][1]["controller_type"]][1].make( + environment, stages[0][1], _controllers, **controller_kwargs + ) def control(self, state, reference): """ - This main method of the InductionMotorFieldOrientedController is called by the user. It calculates the input - voltages u_a,b,c. + This main method of the InductionMotorFieldOrientedController is called by the user. It calculates the input + voltages u_a,b,c. - Args: - state: state of the gem environment - reference: reference for the controlled states + Args: + state: state of the gem environment + reference: reference for the controlled states - Returns: - action: action for the gem environment + Returns: + action: action for the gem environment """ - state = state * self.limits # Denormalize the state - self.psi_abs, self.psi_angle = self.flux_observer.estimate(state) # Estimate the flux + state = state * self.limits # Denormalize the state + self.psi_abs, self.psi_angle = self.flux_observer.estimate( + state + ) # Estimate the flux omega_me = state[self.omega_idx] i_sd = state[self.i_sd_idx] i_sq = state[self.i_sq_idx] - omega_s = omega_me + self.r_r * self.l_m / self.l_r * i_sq / max(np.abs(self.psi_abs), 1e-4) * np.sign(self.psi_abs) + omega_s = omega_me + self.r_r * self.l_m / self.l_r * i_sq / max( + np.abs(self.psi_abs), 1e-4 + ) * np.sign(self.psi_abs) # Calculate delate u_sd, u_sq - u_sd_delta = self.d_controller.control(state[self.i_sd_idx], reference[self.i_sd_ref_idx] * self.limits[self.i_sd_idx]) - u_sq_delta = self.q_controller.control(state[self.i_sq_idx], reference[self.i_sq_ref_idx] * self.limits[self.i_sq_idx]) + u_sd_delta = self.d_controller.control( + state[self.i_sd_idx], + reference[self.i_sd_ref_idx] * self.limits[self.i_sd_idx], + ) + u_sq_delta = self.q_controller.control( + state[self.i_sq_idx], + reference[self.i_sq_ref_idx] * self.limits[self.i_sq_idx], + ) # Decouple the two current components if self.decoupling: - self.u_sd_0 = -omega_s * self.sigma * self.l_s * i_sq - self.l_m * self.r_r / (self.l_r ** 2) * self.psi_abs - self.u_sq_0 = omega_s * self.sigma * self.l_s * i_sd + omega_me * self.l_m / self.l_r * self.psi_abs + self.u_sd_0 = ( + -omega_s * self.sigma * self.l_s * i_sq + - self.l_m * self.r_r / (self.l_r**2) * self.psi_abs + ) + self.u_sq_0 = ( + omega_s * self.sigma * self.l_s * i_sd + + omega_me * self.l_m / self.l_r * self.psi_abs + ) u_sd = self.u_sd_0 + u_sd_delta u_sq = self.u_sq_0 + u_sq_delta @@ -110,8 +145,14 @@ def control(self, state, reference): # Limit action and integrate action = np.clip(u_s_abc, self.action_space.low, self.action_space.high) if (action == u_s_abc).all(): - self.d_controller.integrate(state[self.i_sd_idx], reference[self.i_sd_ref_idx] * self.limits[self.i_sd_idx]) - self.q_controller.integrate(state[self.i_sq_idx], reference[self.i_sq_ref_idx] * self.limits[self.i_sq_idx]) + self.d_controller.integrate( + state[self.i_sd_idx], + reference[self.i_sd_ref_idx] * self.limits[self.i_sd_idx], + ) + self.q_controller.integrate( + state[self.i_sq_idx], + reference[self.i_sq_ref_idx] * self.limits[self.i_sq_idx], + ) # Plot the external data plot(external_plot=self.external_plot, external_data=self.get_plot_data()) @@ -120,7 +161,9 @@ def control(self, state, reference): def get_plot_data(self): # Getting the external data that should be plotted - return dict(ref_state=[], ref_value=[], external=[[self.psi_abs], [self.psi_angle]]) + return dict( + ref_state=[], ref_value=[], external=[[self.psi_abs], [self.psi_angle]] + ) def reset(self): # Reset the Controllers and the observer diff --git a/examples/classic_controllers/controllers/induction_motor_torque_to_current_conversion.py b/examples/classic_controllers/controllers/induction_motor_torque_to_current_conversion.py index 5ccf1021..beecc1ac 100644 --- a/examples/classic_controllers/controllers/induction_motor_torque_to_current_conversion.py +++ b/examples/classic_controllers/controllers/induction_motor_torque_to_current_conversion.py @@ -5,44 +5,53 @@ class InductionMotorTorqueToCurrentConversion: """ - This class represents the torque controller for the cascaded control of induction motors. The torque controller - uses LUT to find an appropriate operating point for the flux and torque. The flux is limited by a modulation - controller. A reference value for the i_sd current is then determined using the operating point of the flux and - a PI controller. In addition, a reference for the i_sq current is calculated based on the current flux and the - operating point of the torque. - Predefined plots are available for visualization of the operating points (plot_torque: default True). Also the - operation of the modulation controller can be plotted (plot_modulation: default False). - Further information can be found at https://ieeexplore.ieee.org/document/7203404. + This class represents the torque controller for the cascaded control of induction motors. The torque controller + uses LUT to find an appropriate operating point for the flux and torque. The flux is limited by a modulation + controller. A reference value for the i_sd current is then determined using the operating point of the flux and + a PI controller. In addition, a reference for the i_sq current is calculated based on the current flux and the + operating point of the torque. + Predefined plots are available for visualization of the operating points (plot_torque: default True). Also the + operation of the modulation controller can be plotted (plot_modulation: default False). + Further information can be found at https://ieeexplore.ieee.org/document/7203404. """ - def __init__(self, environment, stages, plot_torque=True, plot_modulation=False, update_interval=1000): + def __init__( + self, + environment, + stages, + plot_torque=True, + plot_modulation=False, + update_interval=1000, + ): self.env = environment self.nominal_values = self.env.physical_system.nominal_state self.state_space = self.env.physical_system.state_space # Calculate parameters of the motor mp = self.env.physical_system.electrical_motor.motor_parameter - self.l_m = mp['l_m'] - self.l_r = self.l_m + mp['l_sigr'] - self.l_s = self.l_m + mp['l_sigs'] - self.r_r = mp['r_r'] - self.r_s = mp['r_s'] - self.p = mp['p'] + self.l_m = mp["l_m"] + self.l_r = self.l_m + mp["l_sigr"] + self.l_s = self.l_m + mp["l_sigs"] + self.r_r = mp["r_r"] + self.r_s = mp["r_s"] + self.p = mp["p"] self.tau = self.env.physical_system.tau tau_s = self.l_s / self.r_s - self.i_sd_idx = self.env.state_names.index('i_sd') - self.i_sq_idx = self.env.state_names.index('i_sq') - self.torque_idx = self.env.state_names.index('torque') - self.u_sa_idx = environment.state_names.index('u_sa') - self.u_sd_idx = environment.state_names.index('u_sd') - self.u_sq_idx = environment.state_names.index('u_sq') - self.omega_idx = environment.state_names.index('omega') + self.i_sd_idx = self.env.state_names.index("i_sd") + self.i_sq_idx = self.env.state_names.index("i_sq") + self.torque_idx = self.env.state_names.index("torque") + self.u_sa_idx = environment.state_names.index("u_sa") + self.u_sd_idx = environment.state_names.index("u_sd") + self.u_sq_idx = environment.state_names.index("u_sq") + self.omega_idx = environment.state_names.index("omega") self.limits = self.env.physical_system.limits - p_gain = stages[0][1]['p_gain'] * 2 * tau_s ** 2 # flux controller p gain + p_gain = stages[0][1]["p_gain"] * 2 * tau_s**2 # flux controller p gain i_gain = p_gain / self.tau # flux controller i gain - self.psi_controller = PIController(self.env, p_gain=p_gain, i_gain=i_gain) # flux controller + self.psi_controller = PIController( + self.env, p_gain=p_gain, i_gain=i_gain + ) # flux controller self.torque_count = 1001 @@ -64,15 +73,17 @@ def __init__(self, environment, stages, plot_torque=True, plot_modulation=False, self.a_max = 1 # maximum modulation level self.k_ = 0.8 d = 2 # damping of the modulation controller - alpha = d / (d - np.sqrt(d ** 2 - 1)) - self.i_gain = 1 / (self.l_s / (1.25 * self.r_s)) * (alpha - 1) / alpha ** 2 + alpha = d / (d - np.sqrt(d**2 - 1)) + self.i_gain = 1 / (self.l_s / (1.25 * self.r_s)) * (alpha - 1) / alpha**2 self.u_dc = np.sqrt(3) * self.limits[self.u_sa_idx] self.limited = False self.integrated = 0 self.psi_high = 0.1 * self.psi_max self.psi_low = -self.psi_max - self.integrated_reset = 0.5 * self.psi_low # Reset value of the modulation controller + self.integrated_reset = ( + 0.5 * self.psi_low + ) # Reset value of the modulation controller self.plot_torque = plot_torque self.plot_modulation = plot_modulation @@ -82,19 +93,23 @@ def __init__(self, environment, stages, plot_torque=True, plot_modulation=False, def intitialize_torque_plot(self): plt.ion() - self.fig_torque = plt.figure('Torque Controller') + self.fig_torque = plt.figure("Torque Controller") self.psi_opt_plot = plt.subplot2grid((1, 2), (0, 0)) self.t_max_plot = plt.subplot2grid((1, 2), (0, 1)) - self.psi_opt_plot.plot(self.psi_opt_t[0], self.psi_opt_t[1], label='$\Psi^*_{r, opt}(T^*)$') + self.psi_opt_plot.plot( + self.psi_opt_t[0], self.psi_opt_t[1], label="$\Psi^*_{r, opt}(T^*)$" + ) self.psi_opt_plot.grid() - self.psi_opt_plot.set_xlabel('T / Nm') - self.psi_opt_plot.set_ylabel('$\Psi$ / Vs') + self.psi_opt_plot.set_xlabel("T / Nm") + self.psi_opt_plot.set_ylabel("$\Psi$ / Vs") self.psi_opt_plot.legend() - self.t_max_plot.plot(self.t_max_psi[1], self.t_max_psi[0], label='$T_{max}(\Psi_{max})$') + self.t_max_plot.plot( + self.t_max_psi[1], self.t_max_psi[0], label="$T_{max}(\Psi_{max})$" + ) self.t_max_plot.grid() - self.t_max_plot.set_xlabel('$\Psi$ / Vs') - self.t_max_plot.set_ylabel('T / Nm') + self.t_max_plot.set_xlabel("$\Psi$ / Vs") + self.t_max_plot.set_ylabel("T / Nm") self.t_max_plot.legend() def psi_opt(self): @@ -103,11 +118,18 @@ def psi_opt(self): i_sd = np.linspace(0, self.limits[self.i_sd_idx], self.i_sd_count) for t in np.linspace(self.t_minimum, self.t_maximum, self.torque_count): if t != 0: - i_sq = t / (3/2 * self.p * self.l_m ** 2 / self.l_r * i_sd[1:]) - pv = 3 / 2 * (self.r_s * np.power(i_sd[1:], 2) + ( - self.r_s + self.r_r * self.l_m ** 2 / self.l_r ** 2) * np.power(i_sq, 2)) # Calculate losses - - i_idx = np.argmin(pv) # Minimize losses + i_sq = t / (3 / 2 * self.p * self.l_m**2 / self.l_r * i_sd[1:]) + pv = ( + 3 + / 2 + * ( + self.r_s * np.power(i_sd[1:], 2) + + (self.r_s + self.r_r * self.l_m**2 / self.l_r**2) + * np.power(i_sq, 2) + ) + ) # Calculate losses + + i_idx = np.argmin(pv) # Minimize losses i_sd_opt = i_sd[i_idx] i_sq_opt = i_sq[i_idx] else: @@ -128,7 +150,11 @@ def t_max(self): for psi_ in psi: i_sd = psi_ / self.l_m - i_sq = np.sqrt(self.nominal_values[self.u_sd_idx] ** 2 / (self.nominal_values[self.omega_idx] ** 2 * self.l_s ** 2) - i_sd ** 2) + i_sq = np.sqrt( + self.nominal_values[self.u_sd_idx] ** 2 + / (self.nominal_values[self.omega_idx] ** 2 * self.l_s**2) + - i_sd**2 + ) t = 3 / 2 * self.p * self.l_m / self.l_r * psi_ * i_sq t_val.append(t) @@ -147,7 +173,13 @@ def t_max(self): def get_psi_opt(self, torque): torque = np.clip(torque, self.t_minimum, self.t_maximum) - return int(round((torque - self.t_minimum) / (self.t_maximum - self.t_minimum) * (self.torque_count - 1))) + return int( + round( + (torque - self.t_minimum) + / (self.t_maximum - self.t_minimum) + * (self.torque_count - 1) + ) + ) def get_t_max(self, psi): psi = np.clip(psi, 0, self.psi_max) @@ -155,16 +187,16 @@ def get_t_max(self, psi): def control(self, state, torque, psi_abs): """ - This main method is called by the CascadedFieldOrientedControllerRotorFluxObserver to calculate reference - values for the i_sd and i_sq currents from a given torque reference. + This main method is called by the CascadedFieldOrientedControllerRotorFluxObserver to calculate reference + values for the i_sd and i_sq currents from a given torque reference. - Args: - state: state of the gym-electric-motor environment - torque: reference value for the torque - psi_abs: amount of the estimated flux + Args: + state: state of the gym-electric-motor environment + torque: reference value for the torque + psi_abs: amount of the estimated flux - Returns: - Reference values for the currents i_sq and i_sd, optimal flux + Returns: + Reference values for the currents i_sq and i_sd, optimal flux """ # Calculate the optimal flux @@ -178,14 +210,24 @@ def control(self, state, torque, psi_abs): # Calculate the reference for i_sd i_sd_ = self.psi_controller.control(psi_abs, psi_opt) - i_sd = np.clip(i_sd_, -0.9 * self.nominal_values[self.i_sd_idx], 0.9 * self.nominal_values[self.i_sd_idx]) + i_sd = np.clip( + i_sd_, + -0.9 * self.nominal_values[self.i_sd_idx], + 0.9 * self.nominal_values[self.i_sd_idx], + ) if i_sd_ == i_sd: self.psi_controller.integrate(psi_abs, psi_opt) # Calculate the reference for i_sq - i_sq = np.clip(torque / max(psi_abs, 0.001) * 2 / 3 / self.p * self.l_r / self.l_m, -self.nominal_values[self.i_sq_idx], self.nominal_values[self.i_sq_idx]) - if self.nominal_values[self.i_sq_idx] < np.sqrt(i_sq ** 2 + i_sd ** 2): - i_sq = np.sign(i_sq) * np.sqrt(self.nominal_values[self.i_sq_idx] ** 2 - i_sd ** 2) + i_sq = np.clip( + torque / max(psi_abs, 0.001) * 2 / 3 / self.p * self.l_r / self.l_m, + -self.nominal_values[self.i_sq_idx], + self.nominal_values[self.i_sq_idx], + ) + if self.nominal_values[self.i_sq_idx] < np.sqrt(i_sq**2 + i_sd**2): + i_sq = np.sign(i_sq) * np.sqrt( + self.nominal_values[self.i_sq_idx] ** 2 - i_sd**2 + ) # Update plots if self.plot_torque: @@ -200,8 +242,12 @@ def control(self, state, torque, psi_abs): self.psi_list.append(psi_opt) if self.k % self.update_interval == 0: - self.psi_opt_plot.scatter(self.torque_list, self.psi_list, c='tab:blue', s=3) - self.t_max_plot.scatter(self.psi_list, self.torque_list, c='tab:blue', s=3) + self.psi_opt_plot.scatter( + self.torque_list, self.psi_list, c="tab:blue", s=3 + ) + self.t_max_plot.scatter( + self.psi_list, self.torque_list, c="tab:blue", s=3 + ) self.fig_torque.canvas.draw() self.fig_torque.canvas.flush_events() @@ -210,13 +256,20 @@ def control(self, state, torque, psi_abs): self.psi_list = [] self.k += 1 - return i_sq / self.limits[self.i_sq_idx], i_sd / self.limits[self.i_sd_idx], psi_opt + return i_sq / self.limits[self.i_sq_idx], i_sd / self.limits[ + self.i_sd_idx + ], psi_opt def modulation_control(self, state): - # Calculate modulation - a = 2 * np.sqrt((state[self.u_sd_idx] * self.limits[self.u_sd_idx]) ** 2 + ( - state[self.u_sq_idx] * self.limits[self.u_sq_idx]) ** 2) / self.u_dc + a = ( + 2 + * np.sqrt( + (state[self.u_sd_idx] * self.limits[self.u_sd_idx]) ** 2 + + (state[self.u_sq_idx] * self.limits[self.u_sq_idx]) ** 2 + ) + / self.u_dc + ) # if a > 1.01 * self.a_max: @@ -230,7 +283,9 @@ def modulation_control(self, state): k_i = 2 * np.abs(omega) * self.p / self.u_dc i_gain = self.i_gain * k_i - psi_delta = i_gain * (a_delta * self.tau + self.integrated) # Calculate Flux delta + psi_delta = i_gain * ( + a_delta * self.tau + self.integrated + ) # Calculate Flux delta # Check, if limits are violated if self.psi_low <= psi_delta <= self.psi_high: @@ -251,40 +306,50 @@ def modulation_control(self, state): self.psi_delta_list.append(psi_delta) if self.k % self.update_interval == 0: - self.a_plot.scatter(self.k_list_a, self.a_list, c='tab:blue', s=3) - self.psi_delta_plot.scatter(self.k_list_a, self.psi_delta_list, c='tab:blue', s=3) - self.a_plot.set_xlim(max(self.k * self.tau, 1) - 1, max(self.k * self.tau, 1)) - self.psi_delta_plot.set_xlim(max(self.k * self.tau, 1) - 1, max(self.k * self.tau, 1)) - self.k_list_a = [] - self.a_list = [] - self.psi_delta_list = [] + self.a_plot.scatter(self.k_list_a, self.a_list, c="tab:blue", s=3) + self.psi_delta_plot.scatter( + self.k_list_a, self.psi_delta_list, c="tab:blue", s=3 + ) + self.a_plot.set_xlim( + max(self.k * self.tau, 1) - 1, max(self.k * self.tau, 1) + ) + self.psi_delta_plot.set_xlim( + max(self.k * self.tau, 1) - 1, max(self.k * self.tau, 1) + ) + self.k_list_a = [] + self.a_list = [] + self.psi_delta_list = [] return psi def initialize_modulation_plot(self): if self.plot_modulation: plt.ion() - self.fig_modulation = plt.figure('Modulation Controller') + self.fig_modulation = plt.figure("Modulation Controller") self.a_plot = plt.subplot2grid((1, 2), (0, 0)) self.psi_delta_plot = plt.subplot2grid((1, 2), (0, 1)) # Define modulation plot - self.a_plot.set_title('Modulation') - self.a_plot.axhline(self.k_ * self.a_max, c='tab:orange', label=r'$a^*$') - self.a_plot.plot([], [], c='tab:blue', label='a') - self.a_plot.set_xlabel('t / s') - self.a_plot.set_ylabel('a') + self.a_plot.set_title("Modulation") + self.a_plot.axhline(self.k_ * self.a_max, c="tab:orange", label=r"$a^*$") + self.a_plot.plot([], [], c="tab:blue", label="a") + self.a_plot.set_xlabel("t / s") + self.a_plot.set_ylabel("a") self.a_plot.grid(True) self.a_plot.set_xlim(0, 1) self.a_plot.legend(loc=2) # Define the delta flux plot - self.psi_delta_plot.set_title(r'$\Psi_\mathrm{\Delta}$') - self.psi_delta_plot.axhline(self.psi_low, c='tab:red', linestyle='dashed', label='Limit') - self.psi_delta_plot.axhline(self.psi_high, c='tab:red', linestyle='dashed') - self.psi_delta_plot.plot([], [], c='tab:blue', label=r'$\Psi_\mathrm{\Delta}$') - self.psi_delta_plot.set_xlabel('t / s') - self.psi_delta_plot.set_ylabel(r'$\Psi_\mathrm{\Delta} / Vs$') + self.psi_delta_plot.set_title(r"$\Psi_\mathrm{\Delta}$") + self.psi_delta_plot.axhline( + self.psi_low, c="tab:red", linestyle="dashed", label="Limit" + ) + self.psi_delta_plot.axhline(self.psi_high, c="tab:red", linestyle="dashed") + self.psi_delta_plot.plot( + [], [], c="tab:blue", label=r"$\Psi_\mathrm{\Delta}$" + ) + self.psi_delta_plot.set_xlabel("t / s") + self.psi_delta_plot.set_ylabel(r"$\Psi_\mathrm{\Delta} / Vs$") self.psi_delta_plot.grid(True) self.psi_delta_plot.set_xlim(0, 1) self.psi_delta_plot.legend(loc=2) diff --git a/examples/classic_controllers/controllers/on_off_controller.py b/examples/classic_controllers/controllers/on_off_controller.py index 010a2358..300cd46d 100644 --- a/examples/classic_controllers/controllers/on_off_controller.py +++ b/examples/classic_controllers/controllers/on_off_controller.py @@ -4,9 +4,17 @@ class OnOffController(DiscreteController): """This is a hysteresis controller with two possible output states.""" - def __init__(self, environment, action_space, hysteresis=0.02, param_dict={}, cascaded=False, control_e=False, - **controller_kwargs): - self.hysteresis = param_dict.get('hysteresis', hysteresis) + def __init__( + self, + environment, + action_space, + hysteresis=0.02, + param_dict={}, + cascaded=False, + control_e=False, + **controller_kwargs, + ): + self.hysteresis = param_dict.get("hysteresis", hysteresis) self.switch_on_level = 1 self.switch_off_level = 2 if action_space in [3, 4] and not control_e else 0 diff --git a/examples/classic_controllers/controllers/pi_controller.py b/examples/classic_controllers/controllers/pi_controller.py index 37ac66ce..246f41d4 100644 --- a/examples/classic_controllers/controllers/pi_controller.py +++ b/examples/classic_controllers/controllers/pi_controller.py @@ -3,21 +3,25 @@ class PIController(PController, IController): """ - The PI-Controller is a combination of the base P-Controller and the base I-Controller. The integrate function is - executed after checking compliance with the limitations in the higher-level controller stage in order to adjust - the I-component of the controller accordingly. + The PI-Controller is a combination of the base P-Controller and the base I-Controller. The integrate function is + executed after checking compliance with the limitations in the higher-level controller stage in order to adjust + the I-component of the controller accordingly. """ - def __init__(self, environment, p_gain=5, i_gain=5, param_dict={}, **controller_kwargs): + def __init__( + self, environment, p_gain=5, i_gain=5, param_dict={}, **controller_kwargs + ): self.tau = environment.physical_system.tau - p_gain = param_dict.get('p_gain', p_gain) - i_gain = param_dict.get('i_gain', i_gain) + p_gain = param_dict.get("p_gain", p_gain) + i_gain = param_dict.get("i_gain", i_gain) PController.__init__(self, p_gain) IController.__init__(self, i_gain) def control(self, state, reference): - return self.p_gain * (reference - state) + self.i_gain * (self.integrated + (reference - state) * self.tau) + return self.p_gain * (reference - state) + self.i_gain * ( + self.integrated + (reference - state) * self.tau + ) def reset(self): self.integrated = 0 diff --git a/examples/classic_controllers/controllers/pid_controller.py b/examples/classic_controllers/controllers/pid_controller.py index 42ded6ca..99d085f9 100644 --- a/examples/classic_controllers/controllers/pid_controller.py +++ b/examples/classic_controllers/controllers/pid_controller.py @@ -5,20 +5,30 @@ class PIDController(PIController, DController): """The PID-Controller is a combination of the PI-Controller and the base P-Controller.""" - def __init__(self, environment, p_gain=5, i_gain=5, d_gain=0.005, param_dict={}, **controller_kwargs): - p_gain = param_dict.get('p_gain', p_gain) - i_gain = param_dict.get('i_gain', i_gain) - d_gain = param_dict.get('d_gain', d_gain) + def __init__( + self, + environment, + p_gain=5, + i_gain=5, + d_gain=0.005, + param_dict={}, + **controller_kwargs, + ): + p_gain = param_dict.get("p_gain", p_gain) + i_gain = param_dict.get("i_gain", i_gain) + d_gain = param_dict.get("d_gain", d_gain) PIController.__init__(self, environment, p_gain, i_gain) DController.__init__(self, d_gain) def control(self, state, reference): - action = PIController.control(self, state, reference) + self.d_gain * ( - reference - state - self.e_old) / self.tau + action = ( + PIController.control(self, state, reference) + + self.d_gain * (reference - state - self.e_old) / self.tau + ) self.e_old = reference - state return action def reset(self): PIController.reset(self) - self.e_old = 0 \ No newline at end of file + self.e_old = 0 diff --git a/examples/classic_controllers/controllers/plot_external_data.py b/examples/classic_controllers/controllers/plot_external_data.py index 87fc223f..89e00345 100644 --- a/examples/classic_controllers/controllers/plot_external_data.py +++ b/examples/classic_controllers/controllers/plot_external_data.py @@ -1,4 +1,10 @@ -def plot(external_reference_plots=(), state_names=(), external_plot=(), visualization=True, external_data=()): +def plot( + external_reference_plots=(), + state_names=(), + external_plot=(), + visualization=True, + external_data=(), +): """ This method passes the latest internally generated references of the controller the ExternalReferencePlots. The GEM-Environment uses this data to plot these references with the according states within its MotorDashboard. @@ -22,26 +28,27 @@ def plot(external_reference_plots=(), state_names=(), external_plot=(), visualiz # Check, if the are external ref plots if len(external_ref_plots) != 0: # Read in the indices of the states and refrences - ref_state_idxs = external_data['ref_state'] + ref_state_idxs = external_data["ref_state"] plot_state_idxs = [ - list(state_names).index(external_ref_plot.state_plot) for external_ref_plot in external_reference_plots + list(state_names).index(external_ref_plot.state_plot) + for external_ref_plot in external_reference_plots ] # Read in the data of the reference - ref_values = external_data['ref_value'] + ref_values = external_data["ref_value"] # Pass the values to the ExternallyReferencedStatePlot object for ref_state_idx, ref_value in zip(ref_state_idxs, ref_values): try: plot_idx = plot_state_idxs.index(ref_state_idx) except ValueError: - pass # Ignore reference input, if there is no reference in this plot + pass # Ignore reference input, if there is no reference in this plot else: external_ref_plots[plot_idx].external_reference(ref_value) # Check if the are external plots and pass the data to the ExternalPlot object if len(external_plots) != 0: - ext_state = external_data['external'] + ext_state = external_data["external"] for ext_plot, ext_data in zip(external_plots, ext_state): ext_plot.add_data(ext_data) diff --git a/examples/classic_controllers/controllers/three_point_controller.py b/examples/classic_controllers/controllers/three_point_controller.py index 597bd07a..c6cd825f 100644 --- a/examples/classic_controllers/controllers/three_point_controller.py +++ b/examples/classic_controllers/controllers/three_point_controller.py @@ -4,14 +4,27 @@ class ThreePointController(DiscreteController): """This is a hysteresis controller with three possible output states.""" - def __init__(self, environment, action_space, switch_to_positive_level=0.02, switch_to_negative_level=0.02, - switch_to_neutral_from_positive=0.01, switch_to_neutral_from_negative=0.01, param_dict={}, - cascaded=False, control_e=False, **controller_kwargs): - - self.pos = param_dict.get('switch_to_positive_level', switch_to_positive_level) - self.neg = param_dict.get('switch_to_negative_level', switch_to_negative_level) - self.neutral_from_pos = param_dict.get('switch_to_neutral_from_positive', switch_to_neutral_from_positive) - self.neutral_from_neg = param_dict.get('switch_to_neutral_from_negative', switch_to_neutral_from_negative) + def __init__( + self, + environment, + action_space, + switch_to_positive_level=0.02, + switch_to_negative_level=0.02, + switch_to_neutral_from_positive=0.01, + switch_to_neutral_from_negative=0.01, + param_dict={}, + cascaded=False, + control_e=False, + **controller_kwargs, + ): + self.pos = param_dict.get("switch_to_positive_level", switch_to_positive_level) + self.neg = param_dict.get("switch_to_negative_level", switch_to_negative_level) + self.neutral_from_pos = param_dict.get( + "switch_to_neutral_from_positive", switch_to_neutral_from_positive + ) + self.neutral_from_neg = param_dict.get( + "switch_to_neutral_from_negative", switch_to_neutral_from_negative + ) self.negative = 2 if action_space in [3, 4, 8] and not control_e else 0 if cascaded: @@ -23,11 +36,14 @@ def __init__(self, environment, action_space, switch_to_positive_level=0.02, swi self.recent_action = self.neutral def control(self, state, reference): - if reference - state > self.pos or ((self.neutral_from_pos < reference - state) and self.recent_action == 1): + if reference - state > self.pos or ( + (self.neutral_from_pos < reference - state) and self.recent_action == 1 + ): self.action = self.positive self.recent_action = 1 elif reference - state < -self.neg or ( - (-self.neutral_from_neg > reference - state) and self.recent_action == 2): + (-self.neutral_from_neg > reference - state) and self.recent_action == 2 + ): self.action = self.negative self.recent_action = 2 else: @@ -38,4 +54,4 @@ def control(self, state, reference): def reset(self): self.action = self.neutral - self.recent_action = self.neutral \ No newline at end of file + self.recent_action = self.neutral diff --git a/examples/classic_controllers/controllers/torque_to_current_conversion.py b/examples/classic_controllers/controllers/torque_to_current_conversion.py index 54362614..a60a5a49 100644 --- a/examples/classic_controllers/controllers/torque_to_current_conversion.py +++ b/examples/classic_controllers/controllers/torque_to_current_conversion.py @@ -7,57 +7,68 @@ class TorqueToCurrentConversion: """ - This class represents the torque controller for cascaded control of synchronous motors. For low speeds only the - current limitation of the motor is important. The current vector to set a desired torque is selected so that the - amount of the current vector is minimum (Maximum Torque per Current). For higher speeds, the voltage limitation - of the synchronous motor or the actuator must also be taken into account. This is terminated by converting the - available voltage to a speed-dependent maximum flux. An additional modulation controller is used for the flux - control. By limiting the flux and the maximum torque per flux (MTPF), an operating point for the flux and the - torque is obtained. This is then converted into a current operating point. The conversion can be terminated by - different methods (parameter torque_control). On the one hand, maps can be determined in advance by - interpolation or analytically, or the analytical determination can be terminated online. - For the visualization of the operating points, both for the current operating points as well as the flux and - torque operating points, predefined plots are available (plot_torque: default True). Also the values of the - modulation controller can be visualized (plot_modulation: default False). + This class represents the torque controller for cascaded control of synchronous motors. For low speeds only the + current limitation of the motor is important. The current vector to set a desired torque is selected so that the + amount of the current vector is minimum (Maximum Torque per Current). For higher speeds, the voltage limitation + of the synchronous motor or the actuator must also be taken into account. This is terminated by converting the + available voltage to a speed-dependent maximum flux. An additional modulation controller is used for the flux + control. By limiting the flux and the maximum torque per flux (MTPF), an operating point for the flux and the + torque is obtained. This is then converted into a current operating point. The conversion can be terminated by + different methods (parameter torque_control). On the one hand, maps can be determined in advance by + interpolation or analytically, or the analytical determination can be terminated online. + For the visualization of the operating points, both for the current operating points as well as the flux and + torque operating points, predefined plots are available (plot_torque: default True). Also the values of the + modulation controller can be visualized (plot_modulation: default False). """ - def __init__(self, environment, plot_torque=True, plot_modulation=False, update_interval=1000, - torque_control='interpolate'): - + def __init__( + self, + environment, + plot_torque=True, + plot_modulation=False, + update_interval=1000, + torque_control="interpolate", + ): self.mp = environment.physical_system.electrical_motor.motor_parameter self.limit = environment.physical_system.limits self.nominal_values = environment.physical_system.nominal_state self.torque_control = torque_control - self.l_d = self.mp['l_d'] - self.l_q = self.mp['l_q'] - self.p = self.mp['p'] - self.psi_p = self.mp.get('psi_p', 0) + self.l_d = self.mp["l_d"] + self.l_q = self.mp["l_q"] + self.p = self.mp["p"] + self.psi_p = self.mp.get("psi_p", 0) self.invert = -1 if (self.psi_p == 0 and self.l_q < self.l_d) else 1 self.tau = environment.physical_system.tau - self.omega_idx = environment.state_names.index('omega') - self.i_sd_idx = environment.state_names.index('i_sd') - self.i_sq_idx = environment.state_names.index('i_sq') - self.u_sd_idx = environment.state_names.index('u_sd') - self.u_sq_idx = environment.state_names.index('u_sq') - self.torque_idx = environment.state_names.index('torque') - self.epsilon_idx = environment.state_names.index('epsilon') + self.omega_idx = environment.state_names.index("omega") + self.i_sd_idx = environment.state_names.index("i_sd") + self.i_sq_idx = environment.state_names.index("i_sq") + self.u_sd_idx = environment.state_names.index("u_sd") + self.u_sq_idx = environment.state_names.index("u_sq") + self.torque_idx = environment.state_names.index("torque") + self.epsilon_idx = environment.state_names.index("epsilon") - self.a_max = 2 / np.sqrt(3) # maximum modulation level + self.a_max = 2 / np.sqrt(3) # maximum modulation level self.k_ = 0.95 - d = 1.2 # damping of the modulation controller - alpha = d / (d - np.sqrt(d ** 2 - 1)) - self.i_gain = 1 / (self.mp['l_q'] / (1.25 * self.mp['r_s'])) * (alpha - 1) / alpha ** 2 + d = 1.2 # damping of the modulation controller + alpha = d / (d - np.sqrt(d**2 - 1)) + self.i_gain = ( + 1 / (self.mp["l_q"] / (1.25 * self.mp["r_s"])) * (alpha - 1) / alpha**2 + ) - self.u_a_idx = environment.state_names.index('u_a') + self.u_a_idx = environment.state_names.index("u_a") self.u_dc = np.sqrt(3) * self.limit[self.u_a_idx] self.limited = False self.integrated = 0 - self.psi_high = 0.2 * np.sqrt((self.psi_p + self.l_d * self.nominal_values[self.i_sd_idx]) ** 2 + ( - self.l_q * self.nominal_values[self.i_sq_idx]) ** 2) + self.psi_high = 0.2 * np.sqrt( + (self.psi_p + self.l_d * self.nominal_values[self.i_sd_idx]) ** 2 + + (self.l_q * self.nominal_values[self.i_sq_idx]) ** 2 + ) self.psi_low = -self.psi_high - self.integrated_reset = 0.01 * self.psi_low # Reset value of the modulation controller + self.integrated_reset = ( + 0.01 * self.psi_low + ) # Reset value of the modulation controller self.t_count = 250 self.psi_count = 250 @@ -71,15 +82,21 @@ def __init__(self, environment, plot_torque=True, plot_modulation=False, update_ def mtpc(): def i_q_(i_d, torque): - return torque / (i_d * (self.l_d - self.l_q) + self.psi_p) / (1.5 * self.p) + return ( + torque / (i_d * (self.l_d - self.l_q) + self.psi_p) / (1.5 * self.p) + ) def i_d_(i_q, torque): return -np.abs(torque / (1.5 * self.p * (self.l_d - self.l_q) * i_q)) # calculate the maximum torque self.max_torque = max( - 1.5 * self.p * (self.psi_p + (self.l_d - self.l_q) * (-self.limit[self.i_sd_idx])) * self.limit[ - self.i_sq_idx], self.limit[self.torque_idx]) + 1.5 + * self.p + * (self.psi_p + (self.l_d - self.l_q) * (-self.limit[self.i_sd_idx])) + * self.limit[self.i_sq_idx], + self.limit[self.torque_idx], + ) torque = np.linspace(-self.max_torque, self.max_torque, self.t_count) characteristic = [] @@ -88,10 +105,16 @@ def i_d_(i_q, torque): if self.l_d == self.l_q: i_d = 0 else: - i_d = np.linspace(-2.5*self.limit[self.i_sd_idx], 0, self.i_count) + i_d = np.linspace( + -2.5 * self.limit[self.i_sd_idx], 0, self.i_count + ) i_q = i_q_(i_d, t) else: - i_q = np.linspace(-2.5*self.limit[self.i_sq_idx], 2.5*self.limit[self.i_sq_idx], self.i_count) + i_q = np.linspace( + -2.5 * self.limit[self.i_sq_idx], + 2.5 * self.limit[self.i_sq_idx], + self.i_count, + ) if self.l_d == self.l_q: i_d = 0 else: @@ -108,14 +131,18 @@ def i_d_(i_q, torque): i_d_ret = i_d[min_idx] # The flow is finally calculated from the currents - psi = np.sqrt((self.psi_p + self.l_d * i_d_ret) ** 2 + (self.l_q * i_q_ret) ** 2) + psi = np.sqrt( + (self.psi_p + self.l_d * i_d_ret) ** 2 + (self.l_q * i_q_ret) ** 2 + ) characteristic.append([t, i_d_ret, i_q_ret, psi]) return np.array(characteristic) def mtpf(): # maximum flux is calculated - self.psi_max_mtpf = np.sqrt((self.psi_p + self.l_d * self.nominal_values[self.i_sd_idx]) ** 2 + ( - self.l_q * self.nominal_values[self.i_sq_idx]) ** 2) + self.psi_max_mtpf = np.sqrt( + (self.psi_p + self.l_d * self.nominal_values[self.i_sd_idx]) ** 2 + + (self.l_q * self.nominal_values[self.i_sq_idx]) ** 2 + ) psi = np.linspace(0, self.psi_max_mtpf, self.psi_count) i_d = np.linspace(-self.nominal_values[self.i_sd_idx], 0, self.i_count) i_d_best = 0 @@ -132,20 +159,40 @@ def mtpf(): else: if self.psi_p == 0: - i_q_best = psi_ / np.sqrt(self.l_d ** 2 + self.l_q ** 2) + i_q_best = psi_ / np.sqrt(self.l_d**2 + self.l_q**2) i_d_best = -i_q_best - t = 1.5 * self.p * (self.psi_p + (self.l_d - self.l_q) * i_d_best) * i_q_best + t = ( + 1.5 + * self.p + * (self.psi_p + (self.l_d - self.l_q) * i_d_best) + * i_q_best + ) else: - i_d_idx = np.where(psi_ ** 2 - np.power(self.psi_p + self.l_d * i_d, 2) >= 0) + i_d_idx = np.where( + psi_**2 - np.power(self.psi_p + self.l_d * i_d, 2) >= 0 + ) i_d_ = i_d[i_d_idx] # calculate all possible i_q currents for i_d currents - i_q = np.sqrt(psi_ ** 2 - np.power(self.psi_p + self.l_d * i_d_, 2)) / self.l_q - i_idx = np.where(np.sqrt(np.power(i_q / self.nominal_values[self.i_sq_idx], 2) + np.power( - i_d_ / self.nominal_values[self.i_sd_idx], 2)) <= 1) + i_q = ( + np.sqrt(psi_**2 - np.power(self.psi_p + self.l_d * i_d_, 2)) + / self.l_q + ) + i_idx = np.where( + np.sqrt( + np.power(i_q / self.nominal_values[self.i_sq_idx], 2) + + np.power(i_d_ / self.nominal_values[self.i_sd_idx], 2) + ) + <= 1 + ) i_d_ = i_d_[i_idx] i_q = i_q[i_idx] - torque = 1.5 * self.p * (self.psi_p + (self.l_d - self.l_q) * i_d_) * i_q + torque = ( + 1.5 + * self.p + * (self.psi_p + (self.l_d - self.l_q) * i_d_) + * i_q + ) # choose the maximum torque if np.size(torque) > 0: @@ -153,12 +200,24 @@ def mtpf(): i_idx = np.where(torque == t)[0][0] i_d_best = i_d_[i_idx] i_q_best = i_q[i_idx] - if np.sqrt(i_d_best**2 + i_q_best**2) <= self.nominal_values[self.i_sq_idx]: + if ( + np.sqrt(i_d_best**2 + i_q_best**2) + <= self.nominal_values[self.i_sq_idx] + ): psi_i_d_q.append([psi_, t, i_d_best, i_q_best]) psi_i_d_q = np.array(psi_i_d_q) self.psi_max_mtpf = np.max(psi_i_d_q[:, 0]) - psi_i_d_q_neg = np.rot90(np.array([psi_i_d_q[:, 0], -psi_i_d_q[:, 1], psi_i_d_q[:, 2], -psi_i_d_q[:, 3]])) + psi_i_d_q_neg = np.rot90( + np.array( + [ + psi_i_d_q[:, 0], + -psi_i_d_q[:, 1], + psi_i_d_q[:, 2], + -psi_i_d_q[:, 3], + ] + ) + ) psi_i_d_q = np.append(psi_i_d_q_neg, psi_i_d_q, axis=0) return np.array(psi_i_d_q) @@ -168,22 +227,36 @@ def mtpf(): # Calculate a list with the flux and the corresponding torque of the mtpc characteristic self.psi_t = np.sqrt( - np.power(self.psi_p + self.l_d * self.mtpc[:, 1], 2) + np.power(self.l_q * self.mtpc[:, 2], 2)) + np.power(self.psi_p + self.l_d * self.mtpc[:, 1], 2) + + np.power(self.l_q * self.mtpc[:, 2], 2) + ) self.psi_t = np.array([self.mtpc[:, 0], self.psi_t]) # define a grid for the two current components - self.i_q_max = np.linspace(-self.nominal_values[self.i_sq_idx], self.nominal_values[self.i_sq_idx], self.i_count) - self.i_d_max = -np.sqrt(self.nominal_values[self.i_sq_idx] ** 2 - np.power(self.i_q_max, 2)) + self.i_q_max = np.linspace( + -self.nominal_values[self.i_sq_idx], + self.nominal_values[self.i_sq_idx], + self.i_count, + ) + self.i_d_max = -np.sqrt( + self.nominal_values[self.i_sq_idx] ** 2 - np.power(self.i_q_max, 2) + ) i_count_mgrid = self.i_count * 1j - i_d, i_q = np.mgrid[-self.limit[self.i_sd_idx]:0:i_count_mgrid, - -self.limit[self.i_sq_idx]:self.limit[self.i_sq_idx]:i_count_mgrid / 2] + i_d, i_q = np.mgrid[ + -self.limit[self.i_sd_idx] : 0 : i_count_mgrid, + -self.limit[self.i_sq_idx] : self.limit[self.i_sq_idx] : i_count_mgrid / 2, + ] i_d = i_d.flatten() i_q = i_q.flatten() # Decide between SPMSM and IPMSM if self.l_d != self.l_q: - idx = np.where(np.sign(self.psi_p + i_d * self.l_d) * np.power(self.psi_p + i_d * self.l_d, 2) + np.power( - i_q * self.l_q, 2) > 0) + idx = np.where( + np.sign(self.psi_p + i_d * self.l_d) + * np.power(self.psi_p + i_d * self.l_d, 2) + + np.power(i_q * self.l_q, 2) + > 0 + ) else: idx = np.where(self.psi_p + i_d * self.l_d > 0) @@ -192,7 +265,9 @@ def mtpf(): # Calculate torque and flux for the grid of the currents t = self.p * 1.5 * (self.psi_p + (self.l_d - self.l_q) * i_d) * i_q - psi = np.sqrt(np.power(self.l_d * i_d + self.psi_p, 2) + np.power(self.l_q * i_q, 2)) + psi = np.sqrt( + np.power(self.l_d * i_d + self.psi_p, 2) + np.power(self.l_q * i_q, 2) + ) self.t_min = np.amin(t) self.t_max = np.amax(t) @@ -200,7 +275,7 @@ def mtpf(): self.psi_min = np.amin(psi) self.psi_max = np.amax(psi) - if torque_control == 'analytical': + if torque_control == "analytical": res = [] for psi in np.linspace(self.psi_min, self.psi_max, self.psi_count): ret = [] @@ -216,16 +291,22 @@ def mtpf(): self.i_d_inter_plot = self.i_d_inter.T self.i_q_inter_plot = self.i_q_inter.T - elif torque_control == 'interpolate': + elif torque_control == "interpolate": # Interpolate the torque and flux to get lists for the optimal currents - self.t_grid, self.psi_grid = np.mgrid[np.amin(t):np.amax(t):np.complex(0, self.t_count), - self.psi_min:self.psi_max:np.complex(self.psi_count)] - self.i_q_inter = griddata((t, psi), i_q, (self.t_grid, self.psi_grid), method='linear') - self.i_d_inter = griddata((t, psi), i_d, (self.t_grid, self.psi_grid), method='linear') + self.t_grid, self.psi_grid = np.mgrid[ + np.amin(t) : np.amax(t) : np.complex(0, self.t_count), + self.psi_min : self.psi_max : np.complex(self.psi_count), + ] + self.i_q_inter = griddata( + (t, psi), i_q, (self.t_grid, self.psi_grid), method="linear" + ) + self.i_d_inter = griddata( + (t, psi), i_d, (self.t_grid, self.psi_grid), method="linear" + ) self.i_d_inter_plot = self.i_d_inter self.i_q_inter_plot = self.i_q_inter - elif torque_control != 'online': + elif torque_control != "online": raise NotImplementedError self.k = 0 @@ -236,95 +317,134 @@ def mtpf(): def intitialize_torque_plot(self): if self.plot_torque: plt.ion() - self.fig_torque = plt.figure('Torque Controller') + self.fig_torque = plt.figure("Torque Controller") # Check if current, torque, flux characteristics could be plotted - if self.torque_control in ['interpolate', 'analytical']: + if self.torque_control in ["interpolate", "analytical"]: self.i_d_q_characteristic_ = plt.subplot2grid((2, 3), (0, 0), rowspan=2) self.psi_plot = plt.subplot2grid((2, 3), (0, 1)) - self.i_d_plot = plt.subplot2grid((2, 3), (0, 2), projection='3d') + self.i_d_plot = plt.subplot2grid((2, 3), (0, 2), projection="3d") self.torque_plot = plt.subplot2grid((2, 3), (1, 1)) - self.i_q_plot = plt.subplot2grid((2, 3), (1, 2), projection='3d') + self.i_q_plot = plt.subplot2grid((2, 3), (1, 2), projection="3d") - elif self.torque_control == 'online': + elif self.torque_control == "online": self.i_d_q_characteristic_ = plt.subplot2grid((2, 2), (0, 0), rowspan=2) self.psi_plot = plt.subplot2grid((2, 2), (0, 1)) self.torque_plot = plt.subplot2grid((2, 2), (1, 1)) mtpc_i_idx = np.where( - np.sqrt(np.power(self.mtpc[:, 1], 2) + np.power(self.mtpc[:, 2], 2)) <= self.nominal_values[ - self.i_sd_idx]) + np.sqrt(np.power(self.mtpc[:, 1], 2) + np.power(self.mtpc[:, 2], 2)) + <= self.nominal_values[self.i_sd_idx] + ) # Define the plot for the current characteristics - self.i_d_q_characteristic_.set_title('$i_\mathrm{d,q_{ref}}$') - self.i_d_q_characteristic_.plot(self.mtpc[mtpc_i_idx, 1][0], self.mtpc[mtpc_i_idx, 2][0], label='MTPC', c='tab:orange') - self.i_d_q_characteristic_.plot(self.mtpf[:, 2], self.mtpf[:, 3], label=r'MTPF', c='tab:green') - self.i_d_q_characteristic_.plot(self.i_d_max, self.i_q_max, label=r'$i_\mathrm{max}$', c='tab:red') - self.i_d_q_characteristic_.plot([], [], label=r'$i_\mathrm{d,q}$', c='tab:blue') + self.i_d_q_characteristic_.set_title("$i_\mathrm{d,q_{ref}}$") + self.i_d_q_characteristic_.plot( + self.mtpc[mtpc_i_idx, 1][0], + self.mtpc[mtpc_i_idx, 2][0], + label="MTPC", + c="tab:orange", + ) + self.i_d_q_characteristic_.plot( + self.mtpf[:, 2], self.mtpf[:, 3], label=r"MTPF", c="tab:green" + ) + self.i_d_q_characteristic_.plot( + self.i_d_max, self.i_q_max, label=r"$i_\mathrm{max}$", c="tab:red" + ) + self.i_d_q_characteristic_.plot( + [], [], label=r"$i_\mathrm{d,q}$", c="tab:blue" + ) self.i_d_q_characteristic_.grid(True) self.i_d_q_characteristic_.legend(loc=2) - self.i_d_q_characteristic_.axis('equal') - self.i_d_q_characteristic_.set_xlabel(r'$i_\mathrm{d}$ / A') - self.i_d_q_characteristic_.set_ylabel(r'$i_\mathrm{q}$ / A') + self.i_d_q_characteristic_.axis("equal") + self.i_d_q_characteristic_.set_xlabel(r"$i_\mathrm{d}$ / A") + self.i_d_q_characteristic_.set_ylabel(r"$i_\mathrm{q}$ / A") # Define the plot for the flux characteristic - self.psi_plot.set_title(r'$\Psi^*_\mathrm{max}(T^*)$') - self.psi_plot.plot(self.psi_t[0], self.psi_t[1], label=r'$\Psi^*_\mathrm{max}(T^*)$', c='tab:orange') - self.psi_plot.plot([], [], label=r'$\Psi(T)$', c='tab:blue') + self.psi_plot.set_title(r"$\Psi^*_\mathrm{max}(T^*)$") + self.psi_plot.plot( + self.psi_t[0], + self.psi_t[1], + label=r"$\Psi^*_\mathrm{max}(T^*)$", + c="tab:orange", + ) + self.psi_plot.plot([], [], label=r"$\Psi(T)$", c="tab:blue") self.psi_plot.grid(True) - self.psi_plot.set_xlabel(r'T / Nm') - self.psi_plot.set_ylabel(r'$\Psi$ / Vs') + self.psi_plot.set_xlabel(r"T / Nm") + self.psi_plot.set_ylabel(r"$\Psi$ / Vs") self.psi_plot.set_ylim(bottom=0) self.psi_plot.legend(loc=2) # Define the plot for the torque characteristic torque = self.mtpf[:, 1] - torque[0:np.where(torque == np.min(torque))[0][0]] = np.min(torque) - torque[np.where(torque == np.max(torque))[0][0]:] = np.max(torque) - self.torque_plot.set_title(r'$T_\mathrm{max}(\Psi_\mathrm{max})$') - self.torque_plot.plot(self.mtpf[:, 0], torque, label=r'$T_\mathrm{max}(\Psi)$', c='tab:orange') - self.torque_plot.plot([], [], label=r'$T(\Psi)$', c='tab:blue') - self.torque_plot.set_xlabel(r'$\Psi$ / Vs') - self.torque_plot.set_ylabel(r'$T_\mathrm{max}$ / Nm') + torque[0 : np.where(torque == np.min(torque))[0][0]] = np.min(torque) + torque[np.where(torque == np.max(torque))[0][0] :] = np.max(torque) + self.torque_plot.set_title(r"$T_\mathrm{max}(\Psi_\mathrm{max})$") + self.torque_plot.plot( + self.mtpf[:, 0], torque, label=r"$T_\mathrm{max}(\Psi)$", c="tab:orange" + ) + self.torque_plot.plot([], [], label=r"$T(\Psi)$", c="tab:blue") + self.torque_plot.set_xlabel(r"$\Psi$ / Vs") + self.torque_plot.set_ylabel(r"$T_\mathrm{max}$ / Nm") self.torque_plot.grid(True) self.torque_plot.legend(loc=2) # Define the plot of currents - if self.torque_control in ['interpolate', 'analytical']: - self.i_q_plot.plot_surface(self.t_grid, self.psi_grid, self.i_q_inter_plot, cmap=cm.jet, linewidth=0, - vmin=np.nanmin(self.i_q_inter_plot), vmax=np.nanmax(self.i_q_inter_plot)) - self.i_q_plot.set_ylabel(r'$\Psi / Vs$') - self.i_q_plot.set_xlabel(r'$T / Nm$') - self.i_q_plot.set_title(r'$i_\mathrm{q}(T, \Psi)$') - - self.i_d_plot.plot_surface(self.t_grid, self.psi_grid, self.i_d_inter_plot, cmap=cm.jet, linewidth=0, - vmin=np.nanmin(self.i_d_inter_plot), vmax=np.nanmax(self.i_d_inter_plot)) - self.i_d_plot.set_ylabel(r'$\Psi / Vs$') - self.i_d_plot.set_xlabel(r'$T / Nm$') - self.i_d_plot.set_title(r'$i_\mathrm{d}(T, \Psi)$') + if self.torque_control in ["interpolate", "analytical"]: + self.i_q_plot.plot_surface( + self.t_grid, + self.psi_grid, + self.i_q_inter_plot, + cmap=cm.jet, + linewidth=0, + vmin=np.nanmin(self.i_q_inter_plot), + vmax=np.nanmax(self.i_q_inter_plot), + ) + self.i_q_plot.set_ylabel(r"$\Psi / Vs$") + self.i_q_plot.set_xlabel(r"$T / Nm$") + self.i_q_plot.set_title(r"$i_\mathrm{q}(T, \Psi)$") + + self.i_d_plot.plot_surface( + self.t_grid, + self.psi_grid, + self.i_d_inter_plot, + cmap=cm.jet, + linewidth=0, + vmin=np.nanmin(self.i_d_inter_plot), + vmax=np.nanmax(self.i_d_inter_plot), + ) + self.i_d_plot.set_ylabel(r"$\Psi / Vs$") + self.i_d_plot.set_xlabel(r"$T / Nm$") + self.i_d_plot.set_title(r"$i_\mathrm{d}(T, \Psi)$") def solve_analytical(self, torque, psi): """ - Assuming linear magnetization characteristics, the optimal currents for given torque and flux can be obtained - by solving the torque and flux equations. These lead to a fourth degree polynomial which can be solved - analytically. There are two ways to use this analytical solution for control. On the one hand, the currents - can be determined in advance as in the case of interpolation for different torques and fluxes and stored in a - LUT (torque_control='analytical'). On the other hand, the solution can be calculated at runtime with the - given torque and flux (torque_control='online'). + Assuming linear magnetization characteristics, the optimal currents for given torque and flux can be obtained + by solving the torque and flux equations. These lead to a fourth degree polynomial which can be solved + analytically. There are two ways to use this analytical solution for control. On the one hand, the currents + can be determined in advance as in the case of interpolation for different torques and fluxes and stored in a + LUT (torque_control='analytical'). On the other hand, the solution can be calculated at runtime with the + given torque and flux (torque_control='online'). """ - poly = [self.l_d ** 2 * (self.l_d - self.l_q) ** 2, - 2 * self.l_d ** 2 * (self.l_d - self.l_q) * self.psi_p + 2 * self.l_d * self.psi_p * ( - self.l_d - self.l_q) ** 2, - self.l_d ** 2 * self.psi_p ** 2 + 4 * self.l_d * self.psi_p ** 2 * (self.l_d - self.l_q) + ( - self.psi_p ** 2 - psi ** 2) * ( - self.l_d - self.l_q) ** 2, - 2 * self.l_q * self.psi_p ** 3 + 2 * (self.psi_p ** 2 - psi ** 2) * self.psi_p * (self.l_d - self.l_q), - (self.psi_p ** 2 - psi ** 2) * self.psi_p ** 2 + (self.l_q * 2 * torque / (3 * self.p)) ** 2] - - sol = np.roots(poly) # Solve polynomial + poly = [ + self.l_d**2 * (self.l_d - self.l_q) ** 2, + 2 * self.l_d**2 * (self.l_d - self.l_q) * self.psi_p + + 2 * self.l_d * self.psi_p * (self.l_d - self.l_q) ** 2, + self.l_d**2 * self.psi_p**2 + + 4 * self.l_d * self.psi_p**2 * (self.l_d - self.l_q) + + (self.psi_p**2 - psi**2) * (self.l_d - self.l_q) ** 2, + 2 * self.l_q * self.psi_p**3 + + 2 * (self.psi_p**2 - psi**2) * self.psi_p * (self.l_d - self.l_q), + (self.psi_p**2 - psi**2) * self.psi_p**2 + + (self.l_q * 2 * torque / (3 * self.p)) ** 2, + ] + + sol = np.roots(poly) # Solve polynomial i_d = np.real(sol[-1]) # Select the appropriate solution for i_d - i_q = 2 * torque / (3 * self.p * (self.psi_p + (self.l_d - self.l_q) * i_d)) # Calculate the corresponding i_q + i_q = ( + 2 * torque / (3 * self.p * (self.psi_p + (self.l_d - self.l_q) * i_d)) + ) # Calculate the corresponding i_q return i_d, i_q def get_i_d_q(self, torque, psi, psi_idx): @@ -339,24 +459,49 @@ def get_i_d_q(self, torque, psi, psi_idx): def get_t_idx(self, torque): torque = np.clip(torque, self.t_min, self.t_max) - return int(round((torque - self.t_min) / (self.t_max - self.t_min) * (self.t_count - 1))) + return int( + round( + (torque - self.t_min) / (self.t_max - self.t_min) * (self.t_count - 1) + ) + ) def get_psi_idx(self, psi): psi = np.clip(psi, self.psi_min, self.psi_max) - return int(round((psi - self.psi_min) / (self.psi_max - self.psi_min) * (self.psi_count - 1))) + return int( + round( + (psi - self.psi_min) + / (self.psi_max - self.psi_min) + * (self.psi_count - 1) + ) + ) def get_psi_idx_mtpf(self, psi): - return np.clip(int((self.psi_count - 1) - round(psi / self.psi_max_mtpf * (self.psi_count - 1))), 0, - self.psi_count) + return np.clip( + int( + (self.psi_count - 1) + - round(psi / self.psi_max_mtpf * (self.psi_count - 1)) + ), + 0, + self.psi_count, + ) def get_t_idx_mtpc(self, torque): - return np.clip(int(round((torque + self.max_torque) / (2 * self.max_torque) * (self.t_count - 1))), 0, - self.t_count) + return np.clip( + int( + round( + (torque + self.max_torque) + / (2 * self.max_torque) + * (self.t_count - 1) + ) + ), + 0, + self.t_count, + ) def control(self, state, torque): """ - This main method is called by the CascadedFieldOrientedController to calculate reference values for the i_d - and i_q currents from a given torque reference. + This main method is called by the CascadedFieldOrientedController to calculate reference values for the i_d + and i_q currents from a given torque reference. """ # get the optimal psi for a given torque from the mtpc characteristic @@ -374,7 +519,7 @@ def control(self, state, torque): torque = np.sign(torque) * t_max # calculate the currents online - if self.torque_control == 'online': + if self.torque_control == "online": i_d, i_q = self.get_i_d_q(torque, psi_max, psi_idx_) # get the currents from a LUT @@ -413,9 +558,15 @@ def control(self, state, torque): self.psi_list.append(psi_max) if self.k % self.update_interval == 0: - self.psi_plot.scatter(self.torque_list, self.psi_list, c='tab:blue', s=3) - self.torque_plot.scatter(self.psi_list, self.torque_list, c='tab:blue', s=3) - self.i_d_q_characteristic_.scatter(self.i_d_list, self.i_q_list, c='tab:blue', s=3) + self.psi_plot.scatter( + self.torque_list, self.psi_list, c="tab:blue", s=3 + ) + self.torque_plot.scatter( + self.psi_list, self.torque_list, c="tab:blue", s=3 + ) + self.i_d_q_characteristic_.scatter( + self.i_d_list, self.i_q_list, c="tab:blue", s=3 + ) self.fig_torque.canvas.draw() self.fig_torque.canvas.flush_events() @@ -426,8 +577,22 @@ def control(self, state, torque): self.psi_list = [] # clipping and normalizing the currents - i_q = np.clip(i_q, -self.nominal_values[self.i_sq_idx], self.nominal_values[self.i_sq_idx]) / self.limit[self.i_sq_idx] - i_d = np.clip(i_d, -self.nominal_values[self.i_sd_idx], self.nominal_values[self.i_sd_idx]) / self.limit[self.i_sd_idx] + i_q = ( + np.clip( + i_q, + -self.nominal_values[self.i_sq_idx], + self.nominal_values[self.i_sq_idx], + ) + / self.limit[self.i_sq_idx] + ) + i_d = ( + np.clip( + i_d, + -self.nominal_values[self.i_sd_idx], + self.nominal_values[self.i_sd_idx], + ) + / self.limit[self.i_sd_idx] + ) self.k += 1 @@ -435,14 +600,20 @@ def control(self, state, torque): def modulation_control(self, state): """ - To ensure the functionality of the current control, a small dynamic manipulated variable reserve to the - voltage limitation must be kept available. This control is performed by this modulation controller. Further - information can be found at https://ieeexplore.ieee.org/document/7409195. + To ensure the functionality of the current control, a small dynamic manipulated variable reserve to the + voltage limitation must be kept available. This control is performed by this modulation controller. Further + information can be found at https://ieeexplore.ieee.org/document/7409195. """ # Calculate modulation - a = 2 * np.sqrt((state[self.u_sd_idx] * self.limit[self.u_sd_idx]) ** 2 + ( - state[self.u_sq_idx] * self.limit[self.u_sq_idx]) ** 2) / self.u_dc + a = ( + 2 + * np.sqrt( + (state[self.u_sd_idx] * self.limit[self.u_sd_idx]) ** 2 + + (state[self.u_sq_idx] * self.limit[self.u_sq_idx]) ** 2 + ) + / self.u_dc + ) # Check, if integral part should be reset if a > 1.1 * self.a_max: @@ -483,40 +654,50 @@ def modulation_control(self, state): self.psi_delta_list.append(psi_delta) if self.k % self.update_interval == 0: - self.a_plot.scatter(self.k_list_a, self.a_list, c='tab:blue', s=3) - self.psi_delta_plot.scatter(self.k_list_a, self.psi_delta_list, c='tab:blue', s=3) - self.a_plot.set_xlim(max(self.k * self.tau, 1) - 1, max(self.k * self.tau, 1)) - self.psi_delta_plot.set_xlim(max(self.k * self.tau, 1) - 1, max(self.k * self.tau, 1)) - self.k_list_a = [] - self.a_list = [] - self.psi_delta_list = [] + self.a_plot.scatter(self.k_list_a, self.a_list, c="tab:blue", s=3) + self.psi_delta_plot.scatter( + self.k_list_a, self.psi_delta_list, c="tab:blue", s=3 + ) + self.a_plot.set_xlim( + max(self.k * self.tau, 1) - 1, max(self.k * self.tau, 1) + ) + self.psi_delta_plot.set_xlim( + max(self.k * self.tau, 1) - 1, max(self.k * self.tau, 1) + ) + self.k_list_a = [] + self.a_list = [] + self.psi_delta_list = [] return psi def initialize_modulation_plot(self): if self.plot_modulation: plt.ion() - self.fig_modulation = plt.figure('Modulation Controller') + self.fig_modulation = plt.figure("Modulation Controller") self.a_plot = plt.subplot2grid((1, 2), (0, 0)) self.psi_delta_plot = plt.subplot2grid((1, 2), (0, 1)) # Define the modulation plot - self.a_plot.set_title('Modulation') - self.a_plot.axhline(self.k_ * self.a_max, c='tab:orange', label=r'$a^*$') - self.a_plot.plot([], [], c='tab:blue', label='a') - self.a_plot.set_xlabel('t / s') - self.a_plot.set_ylabel('a') + self.a_plot.set_title("Modulation") + self.a_plot.axhline(self.k_ * self.a_max, c="tab:orange", label=r"$a^*$") + self.a_plot.plot([], [], c="tab:blue", label="a") + self.a_plot.set_xlabel("t / s") + self.a_plot.set_ylabel("a") self.a_plot.grid(True) self.a_plot.set_xlim(0, 1) self.a_plot.legend(loc=2) # Define the delta flux plot - self.psi_delta_plot.set_title(r'$\Psi_\mathrm{\Delta}$') - self.psi_delta_plot.axhline(self.psi_low, c='tab:red', linestyle='dashed', label='Limit') - self.psi_delta_plot.axhline(self.psi_high, c='tab:red', linestyle='dashed') - self.psi_delta_plot.plot([], [], c='tab:blue', label=r'$\Psi_\mathrm{\Delta}$') - self.psi_delta_plot.set_xlabel('t / s') - self.psi_delta_plot.set_ylabel(r'$\Psi_\mathrm{\Delta} / Vs$') + self.psi_delta_plot.set_title(r"$\Psi_\mathrm{\Delta}$") + self.psi_delta_plot.axhline( + self.psi_low, c="tab:red", linestyle="dashed", label="Limit" + ) + self.psi_delta_plot.axhline(self.psi_high, c="tab:red", linestyle="dashed") + self.psi_delta_plot.plot( + [], [], c="tab:blue", label=r"$\Psi_\mathrm{\Delta}$" + ) + self.psi_delta_plot.set_xlabel("t / s") + self.psi_delta_plot.set_ylabel(r"$\Psi_\mathrm{\Delta} / Vs$") self.psi_delta_plot.grid(True) self.psi_delta_plot.set_xlim(0, 1) self.psi_delta_plot.legend(loc=2) diff --git a/examples/classic_controllers/custom_classic_controllers_dc_motor_example.py b/examples/classic_controllers/custom_classic_controllers_dc_motor_example.py index 5ea26e4c..d22a6688 100644 --- a/examples/classic_controllers/custom_classic_controllers_dc_motor_example.py +++ b/examples/classic_controllers/custom_classic_controllers_dc_motor_example.py @@ -3,8 +3,7 @@ import gym_electric_motor as gem from gym_electric_motor.visualization import MotorDashboard -if __name__ == '__main__': - +if __name__ == "__main__": """ motor type: 'PermExDc' Permanently Excited DC Motor 'ExtExDc' Externally Excited MC Motor @@ -20,26 +19,30 @@ """ # following manual controller design addresses an ExtExDc. Other motor types require different controller stages - motor_type = 'ExtExDc' - control_type = 'CC' - action_type = 'Cont' - - motor = action_type + '-' + control_type + '-' + motor_type + '-v0' - - if motor_type in ['PermExDc', 'SeriesDc']: - states = ['omega', 'torque', 'i', 'u'] - elif motor_type == 'ShuntDc': - states = ['omega', 'torque', 'i_a', 'i_e', 'u'] - elif motor_type == 'ExtExDc': - states = ['omega', 'torque', 'i_a', 'i_e', 'u_a', 'u_e'] + motor_type = "ExtExDc" + control_type = "CC" + action_type = "Cont" + + motor = action_type + "-" + control_type + "-" + motor_type + "-v0" + + if motor_type in ["PermExDc", "SeriesDc"]: + states = ["omega", "torque", "i", "u"] + elif motor_type == "ShuntDc": + states = ["omega", "torque", "i_a", "i_e", "u"] + elif motor_type == "ExtExDc": + states = ["omega", "torque", "i_a", "i_e", "u_a", "u_e"] else: - raise KeyError(motor_type + ' is not available') + raise KeyError(motor_type + " is not available") # definition of the plotted variables external_ref_plots = [ExternallyReferencedStatePlot(state) for state in states] # initialize the gym-electric-motor environment - env = gem.make(motor, visualization=MotorDashboard(additional_plots=external_ref_plots), render_mode = 'figure') + env = gem.make( + motor, + visualization=MotorDashboard(additional_plots=external_ref_plots), + render_mode="figure", + ) """ initialize the controller @@ -58,16 +61,26 @@ added in a separate array. """ - current_a_controller = {'controller_type': 'pi_controller', 'p_gain': 0.3, 'i_gain': 50} - speed_controller = {'controller_type': 'pi_controller', 'p_gain': 1, 'i_gain': 40} - current_e_controller = {'controller_type': 'pi_controller', 'p_gain': 5, 'i_gain': 300} + current_a_controller = { + "controller_type": "pi_controller", + "p_gain": 0.3, + "i_gain": 50, + } + speed_controller = {"controller_type": "pi_controller", "p_gain": 1, "i_gain": 40} + current_e_controller = { + "controller_type": "pi_controller", + "p_gain": 5, + "i_gain": 300, + } stages_a = [current_a_controller, speed_controller] stages_e = [current_e_controller] stages = [stages_a, stages_e] - controller = Controller.make(env, external_ref_plots=external_ref_plots, stages=stages) + controller = Controller.make( + env, external_ref_plots=external_ref_plots, stages=stages + ) (state, reference), _ = env.reset() diff --git a/examples/classic_controllers/custom_classic_controllers_ind_motor_example.py b/examples/classic_controllers/custom_classic_controllers_ind_motor_example.py index 4718c37a..50a4c3a3 100644 --- a/examples/classic_controllers/custom_classic_controllers_ind_motor_example.py +++ b/examples/classic_controllers/custom_classic_controllers_ind_motor_example.py @@ -5,7 +5,7 @@ from gym_electric_motor.visualization import MotorDashboard import numpy as np -if __name__ == '__main__': +if __name__ == "__main__": """ motor type: 'SCIM' Squirrel Cage Induction Motor @@ -16,20 +16,25 @@ action_type: 'Cont' Continuous Action Space """ - motor_type = 'SCIM' - control_type = 'SC' - action_type = 'Cont' + motor_type = "SCIM" + control_type = "SC" + action_type = "Cont" - env_id = action_type + '-' + control_type + '-' + motor_type + '-v0' + env_id = action_type + "-" + control_type + "-" + motor_type + "-v0" # definition of the plotted variables - states = ['omega', 'torque', 'i_sd', 'i_sq', 'u_sd', 'u_sq'] + states = ["omega", "torque", "i_sd", "i_sq", "u_sd", "u_sq"] external_ref_plots = [ExternallyReferencedStatePlot(state) for state in states] - external_plot = [ExternalPlot(referenced=control_type != 'CC'), ExternalPlot(min=-np.pi, max=np.pi)] + external_plot = [ + ExternalPlot(referenced=control_type != "CC"), + ExternalPlot(min=-np.pi, max=np.pi), + ] external_ref_plots += external_plot # initialize the gym-electric-motor environment - env = gem.make(env_id, visualization=MotorDashboard(additional_plots=external_ref_plots)) + env = gem.make( + env_id, visualization=MotorDashboard(additional_plots=external_ref_plots) + ) """ initialize the controller @@ -42,9 +47,13 @@ """ - current_controller = [{'controller_type': 'pi_controller', 'p_gain': 40, 'i_gain': 15000}, - {'controller_type': 'pi_controller', 'p_gain': 25, 'i_gain': 10000}] - speed_controller = [{'controller_type': 'pi_controller', 'p_gain': 1, 'i_gain': 100}] + current_controller = [ + {"controller_type": "pi_controller", "p_gain": 40, "i_gain": 15000}, + {"controller_type": "pi_controller", "p_gain": 25, "i_gain": 10000}, + ] + speed_controller = [ + {"controller_type": "pi_controller", "p_gain": 1, "i_gain": 100} + ] stages = [current_controller, speed_controller] controller = Controller.make(env, stages=stages, external_plot=external_ref_plots) diff --git a/examples/classic_controllers/custom_classic_controllers_synch_motor_example.py b/examples/classic_controllers/custom_classic_controllers_synch_motor_example.py index d6cf2f08..293d0655 100644 --- a/examples/classic_controllers/custom_classic_controllers_synch_motor_example.py +++ b/examples/classic_controllers/custom_classic_controllers_synch_motor_example.py @@ -4,8 +4,7 @@ from gym_electric_motor.visualization import MotorDashboard import numpy as np -if __name__ == '__main__': - +if __name__ == "__main__": """ motor type: 'PMSM' Permanent Magnet Synchronous Motor 'SynRM' Synchronous Reluctance Motor @@ -18,25 +17,39 @@ 'Finite' Discrete Action Space """ - motor_type = 'PMSM' - control_type = 'SC' - action_type = 'Cont' + motor_type = "PMSM" + control_type = "SC" + action_type = "Cont" - env_id = action_type + '-' + control_type + '-' + motor_type + '-v0' + env_id = action_type + "-" + control_type + "-" + motor_type + "-v0" # definition of the motor parameters - psi_p = 0 if motor_type == 'SynRM' else 45e-3 + psi_p = 0 if motor_type == "SynRM" else 45e-3 limit_values = dict(omega=12e3 * np.pi / 30, torque=100, i=280, u=320) - nominal_values = dict(omega=10e3 * np.pi / 30, torque=95.0, i=240, epsilon=np.pi, u=300) - motor_parameter = dict(p=3, l_d=0.37e-3, l_q=1.2e-3, j_rotor=0.03883, r_s=18e-3, psi_p=psi_p) + nominal_values = dict( + omega=10e3 * np.pi / 30, torque=95.0, i=240, epsilon=np.pi, u=300 + ) + motor_parameter = dict( + p=3, l_d=0.37e-3, l_q=1.2e-3, j_rotor=0.03883, r_s=18e-3, psi_p=psi_p + ) # definition of the plotted variables - external_ref_plots = [ExternallyReferencedStatePlot(state) for state in ['omega', 'torque', 'i_sd', 'i_sq', 'u_sd', 'u_sq']] + external_ref_plots = [ + ExternallyReferencedStatePlot(state) + for state in ["omega", "torque", "i_sd", "i_sq", "u_sd", "u_sq"] + ] # initialize the gym-electric-motor environment - env = gem.make(env_id, visualization=MotorDashboard(additional_plots=external_ref_plots), - motor=dict(limit_values=limit_values, nominal_values=nominal_values, motor_parameter=motor_parameter), - render_mode = 'figure') + env = gem.make( + env_id, + visualization=MotorDashboard(additional_plots=external_ref_plots), + motor=dict( + limit_values=limit_values, + nominal_values=nominal_values, + motor_parameter=motor_parameter, + ), + render_mode="figure", + ) """ initialize the controller @@ -90,16 +103,33 @@ """ - current_d_controller = {'controller_type': 'pi_controller', 'p_gain': 1, 'i_gain': 500} - current_q_controller = {'controller_type': 'pi_controller', 'p_gain': 3, 'i_gain': 1400} - speed_controller = {'controller_type': 'pi_controller', 'p_gain': 12, 'i_gain': 1300} + current_d_controller = { + "controller_type": "pi_controller", + "p_gain": 1, + "i_gain": 500, + } + current_q_controller = { + "controller_type": "pi_controller", + "p_gain": 3, + "i_gain": 1400, + } + speed_controller = { + "controller_type": "pi_controller", + "p_gain": 12, + "i_gain": 1300, + } current_controller = [current_d_controller, current_q_controller] overlaid_controller = [speed_controller] stages = [current_controller, overlaid_controller] - controller = Controller.make(env, stages=stages, external_ref_plots=external_ref_plots, torque_control='analytical') + controller = Controller.make( + env, + stages=stages, + external_ref_plots=external_ref_plots, + torque_control="analytical", + ) (state, reference), _ = env.reset() diff --git a/examples/classic_controllers/external_plot.py b/examples/classic_controllers/external_plot.py index 679736c0..872b35f9 100644 --- a/examples/classic_controllers/external_plot.py +++ b/examples/classic_controllers/external_plot.py @@ -4,48 +4,48 @@ class ExternalPlot(TimePlot): """ - Class to plot lines that do not belong to the state of the environment. A reference and any number of additional - lines can be plotted. - Usage Example - ------------- - >>> from classic_controllers import Controller - >>> from external_plot import ExternalPlot - >>> import gym_electric_motor as gem - >>> from gym_electric_motor.visualization import MotorDashboard - >>> import numpy as np - - >>> if __name__ == '__main__': - >>> #define ExternalPlot Object with reference and two additional lines - >>> external_plot = ExternalPlot(min=-1, max=1, referenced=True, additional_lines=2) - - >>> # define gem environment and pass the ExternalPlot object as an additional plot - >>> env = gem.make('DqCont-CC-PMSM-v0', visualization=MotorDashboard(state_plots=['i_sd', 'i_sq'], - ... additional_plots=(external_plot,))) - - >>> # setting the labels of the plots - >>> external_plot.set_label({'y_label': 'y', 'state_label': '$state$', 'ref_label': '$reference$', - ... 'add_label': ['$add_1$', '$add_2$']}) - >>> terminated = True - >>> for t in range(100000): - >>> if terminated: - >>> state, reference = env.reset() - >>> data = [np.sin(t / 500), np.sin(t / 1000), np.sin(t / 1500), np.sin(t / 2000)] - >>> external_plot.add_data(data) # passing the data to the external plot - >>> (state, reference), reward, terminated, truncated, _ = env.step([0, 0]) + Class to plot lines that do not belong to the state of the environment. A reference and any number of additional + lines can be plotted. + Usage Example + ------------- + >>> from classic_controllers import Controller + >>> from external_plot import ExternalPlot + >>> import gym_electric_motor as gem + >>> from gym_electric_motor.visualization import MotorDashboard + >>> import numpy as np + + >>> if __name__ == '__main__': + >>> #define ExternalPlot Object with reference and two additional lines + >>> external_plot = ExternalPlot(min=-1, max=1, referenced=True, additional_lines=2) + + >>> # define gem environment and pass the ExternalPlot object as an additional plot + >>> env = gem.make('DqCont-CC-PMSM-v0', visualization=MotorDashboard(state_plots=['i_sd', 'i_sq'], + ... additional_plots=(external_plot,))) + + >>> # setting the labels of the plots + >>> external_plot.set_label({'y_label': 'y', 'state_label': '$state$', 'ref_label': '$reference$', + ... 'add_label': ['$add_1$', '$add_2$']}) + >>> terminated = True + >>> for t in range(100000): + >>> if terminated: + >>> state, reference = env.reset() + >>> data = [np.sin(t / 500), np.sin(t / 1000), np.sin(t / 1500), np.sin(t / 2000)] + >>> external_plot.add_data(data) # passing the data to the external plot + >>> (state, reference), reward, terminated, truncated, _ = env.step([0, 0]) """ def __init__(self, referenced=False, additional_lines=0, min=0, max=1): """ - This function creates an object for external plots in a GEM MotorDashboard. + This function creates an object for external plots in a GEM MotorDashboard. - Args: - referenced: a reference is to be displayed - additional_lines: number of additional lines in plot - min: minimum y-value of the plot - max: maximum y-value of the plot + Args: + referenced: a reference is to be displayed + additional_lines: number of additional lines in plot + min: minimum y-value of the plot + max: maximum y-value of the plot - Returns: - Object that can be passed to a GEM environment to plot additional data. + Returns: + Object that can be passed to a GEM environment to plot additional data. """ super().__init__() @@ -61,8 +61,8 @@ def __init__(self, referenced=False, additional_lines=0, min=0, max=1): self._state_line = None self._reference_line = None - self.state_label = '' - self.ref_label = '' + self.state_label = "" + self.ref_label = "" # Data containers self._state_data = [] @@ -79,7 +79,7 @@ def __init__(self, referenced=False, additional_lines=0, min=0, max=1): for i in range(additional_lines): self._additional_lines.append([]) self._additional_data.append(None) - self.add_labels.append('') + self.add_labels.append("") def set_env(self, env): # Docstring of superclass @@ -97,20 +97,32 @@ def reset_data(self): if self.added: for i in range(self.add_lines): - self._additional_data[i] = np.full(shape=self._x_data.shape, fill_value=np.nan) + self._additional_data[i] = np.full( + shape=self._x_data.shape, fill_value=np.nan + ) def initialize(self, axis): # Docstring of superclass super().initialize(axis) # Line to plot the state data - self._state_line, = self._axis.plot(self._x_data, self._state_data, **self._state_line_config, zorder=self.add_lines+2) + (self._state_line,) = self._axis.plot( + self._x_data, + self._state_data, + **self._state_line_config, + zorder=self.add_lines + 2, + ) self._lines = [self._state_line] # If the state is referenced plot also the reference line if self._referenced: - self._reference_line, = self._axis.plot(self._x_data, self._reference_data, **self._ref_line_config, zorder=self.add_lines+1) - #axis.lines = axis.lines[::-1] + (self._reference_line,) = self._axis.plot( + self._x_data, + self._reference_data, + **self._ref_line_config, + zorder=self.add_lines + 1, + ) + # axis.lines = axis.lines[::-1] self._lines.append(self._reference_line) self._y_data = [self._state_data, self._reference_data] @@ -118,7 +130,12 @@ def initialize(self, axis): # If there are added lines plot also these lines if self.added: for i in range(self.add_lines): - self._additional_lines[i], = self._axis.plot(self._x_data, self._additional_data[i], **self._add_line_config, zorder=self.add_lines-i) + (self._additional_lines[i],) = self._axis.plot( + self._x_data, + self._additional_data[i], + **self._add_line_config, + zorder=self.add_lines - i, + ) self._lines.append(self._additional_lines[i]) self._y_data.append(self._additional_data[i]) @@ -129,24 +146,31 @@ def initialize(self, axis): lines.extend(self._additional_lines) labels = [self.state_label, self.ref_label] labels.extend(self.add_labels) - self._axis.legend((lines), (labels), loc='upper left', numpoints=20) + self._axis.legend((lines), (labels), loc="upper left", numpoints=20) else: - self._axis.legend(([self._state_line, self._reference_line]), ([self.state_label, self.ref_label]), loc='upper left', numpoints=20) + self._axis.legend( + ([self._state_line, self._reference_line]), + ([self.state_label, self.ref_label]), + loc="upper left", + numpoints=20, + ) else: - self._axis.legend((self._state_line, ), (self.state_label, ), loc='upper left', numpoints=20) + self._axis.legend( + (self._state_line,), (self.state_label,), loc="upper left", numpoints=20 + ) def set_label(self, labels): """ - Method to set the labels, A dict must be passed. The keys are: y_label, state_label, ref_label, add_label. - For the key add_label a list with the length of the number of additional lines is passed. + Method to set the labels, A dict must be passed. The keys are: y_label, state_label, ref_label, add_label. + For the key add_label a list with the length of the number of additional lines is passed. """ - self._label = labels.get('y_label', '') - self.state_label = labels['state_label'] + self._label = labels.get("y_label", "") + self.state_label = labels["state_label"] if self._referenced: - self.ref_label = labels.get('ref_label', '') - if 'add_label' in labels.keys(): - self.add_labels = labels['add_label'] + self.ref_label = labels.get("ref_label", "") + if "add_label" in labels.keys(): + self.add_labels = labels["add_label"] def on_step_end(self, k, state, reference, reward, terminated): super().on_step_end(k, state, reference, reward, terminated) diff --git a/examples/classic_controllers/externally_referenced_state_plot.py b/examples/classic_controllers/externally_referenced_state_plot.py index b0daf0ae..e6df2590 100644 --- a/examples/classic_controllers/externally_referenced_state_plot.py +++ b/examples/classic_controllers/externally_referenced_state_plot.py @@ -3,24 +3,24 @@ class ExternallyReferencedStatePlot(StatePlot): """Plot that displays environments states together with externally generated references. - These could be for example references that are generated intermediately within a cascaded controller. - Usage Example - ------------- - .. code-block:: python - :emphasize-lines: 1,12 - my_externally_referenced_plot = ExternallyReferencedStatePlot(state='i_sd') - env = gem.make( - 'DqCont-SC-PMSM-v0', - visualization=dict(additional_plots=(my_externally_referenced_plot,), - ) - terminated = True - for _ in range(10000): - if terminated: - state, reference = env.reset() - external_reference_value = my_external_isd_reference_generator.get_reference() - my_externally_referenced_plot.external_reference(external_reference_value) - action = env.action_space.sample() - (state, reference), reward, terminated, truncated, _ = env.step(action) + These could be for example references that are generated intermediately within a cascaded controller. + Usage Example + ------------- + .. code-block:: python + :emphasize-lines: 1,12 + my_externally_referenced_plot = ExternallyReferencedStatePlot(state='i_sd') + env = gem.make( + 'DqCont-SC-PMSM-v0', + visualization=dict(additional_plots=(my_externally_referenced_plot,), + ) + terminated = True + for _ in range(10000): + if terminated: + state, reference = env.reset() + external_reference_value = my_external_isd_reference_generator.get_reference() + my_externally_referenced_plot.external_reference(external_reference_value) + action = env.action_space.sample() + (state, reference), reward, terminated, truncated, _ = env.step(action) """ def __init__(self, state): diff --git a/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py b/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py index cbec7db2..640e12a1 100644 --- a/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py +++ b/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py @@ -5,8 +5,7 @@ from gym_electric_motor.reference_generators import SinusoidalReferenceGenerator import time -if __name__ == '__main__': - +if __name__ == "__main__": """ motor type: 'PermExDc' Permanently Excited DC Motor 'ExtExDc' Externally Excited MC Motor @@ -21,38 +20,42 @@ 'Finite' Discrete Action Space """ - motor_type = 'PermExDc' - control_type = 'SC' - action_type = 'Cont' + motor_type = "PermExDc" + control_type = "SC" + action_type = "Cont" - motor = action_type + '-' + control_type + '-' + motor_type + '-v0' + motor = action_type + "-" + control_type + "-" + motor_type + "-v0" - if motor_type in ['PermExDc', 'SeriesDc']: - states = ['omega', 'torque', 'i', 'u'] - elif motor_type == 'ShuntDc': - states = ['omega', 'torque', 'i_a', 'i_e', 'u'] - elif motor_type == 'ExtExDc': - states = ['omega', 'torque', 'i_a', 'i_e', 'u_a', 'u_e'] + if motor_type in ["PermExDc", "SeriesDc"]: + states = ["omega", "torque", "i", "u"] + elif motor_type == "ShuntDc": + states = ["omega", "torque", "i_a", "i_e", "u"] + elif motor_type == "ExtExDc": + states = ["omega", "torque", "i_a", "i_e", "u_a", "u_e"] else: - raise KeyError(motor_type + ' is not available') + raise KeyError(motor_type + " is not available") # definition of the plotted variables external_ref_plots = [ExternallyReferencedStatePlot(state) for state in states] # definition of the reference generator - ref_generator = SinusoidalReferenceGenerator(amplitude_range= (1,1), - frequency_range= (5,5), - offset_range = (0,0), - episode_lengths = (10001, 10001)) + ref_generator = SinusoidalReferenceGenerator( + amplitude_range=(1, 1), + frequency_range=(5, 5), + offset_range=(0, 0), + episode_lengths=(10001, 10001), + ) # initialize the gym-electric-motor environment - env = gem.make(motor, - visualization=MotorDashboard(additional_plots=external_ref_plots), - scale_plots=True, - render_mode="figure", - reference_generator = ref_generator) - + env = gem.make( + motor, + visualization=MotorDashboard(additional_plots=external_ref_plots), + scale_plots=True, + render_mode="figure", + reference_generator=ref_generator, + ) + env.metadata["filename_prefix"] = "integration-test" env.metadata["filename_suffix"] = "" env.metadata["save_figure_on_close"] = False @@ -74,14 +77,13 @@ # simulate the environment for i in range(10001): action = controller.control(state, reference) - #if i % 100 == 0: - # (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) - #else: + # if i % 100 == 0: + # (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) + # else: (state, reference), reward, terminated, truncated, _ = env.step(action) - if terminated: env.reset() controller.reset() - - env.close() \ No newline at end of file + + env.close() diff --git a/examples/environment_features/external_speed_profile.py b/examples/environment_features/external_speed_profile.py index 89f38236..a5f0dc72 100644 --- a/examples/environment_features/external_speed_profile.py +++ b/examples/environment_features/external_speed_profile.py @@ -5,10 +5,12 @@ import time from scipy import signal -from gym_electric_motor.physical_systems.mechanical_loads \ - import ExternalSpeedLoad, ConstantSpeedLoad +from gym_electric_motor.physical_systems.mechanical_loads import ( + ExternalSpeedLoad, + ConstantSpeedLoad, +) -''' +""" This code example presents how the speed load classes can be used to define a speed profile which the drive will then follow. This is often useful when prototyping drive controllers that either @@ -16,17 +18,17 @@ drive torque with fixed speed requirements (e.g. traction applications, generator operation). For a more general introduction to GEM, we recommend to have a look at the "_control.py" examples first. -''' +""" # We will have a look at a current control scenario here const_sub_gen = [ # operation at 10 % of the current limit - rg.ConstReferenceGenerator(reference_state='i', reference_value=0.1), + rg.ConstReferenceGenerator(reference_state="i", reference_value=0.1), # operation at 90 % of the current limit - rg.ConstReferenceGenerator(reference_state='i', reference_value=0.9), + rg.ConstReferenceGenerator(reference_state="i", reference_value=0.9), # operation at 110 % of the current limit, - rg.ConstReferenceGenerator(reference_state='i', reference_value=1.1) + rg.ConstReferenceGenerator(reference_state="i", reference_value=1.1), ] # since the case of 110 % current may lead to limit violation, we only assign a small probability of 2 % to it @@ -37,39 +39,49 @@ # The ExternalSpeedLoad class allows to pass an arbitrary function of time which will then dictate the speed profile. # As shown here it can also contain more parameters. Some examples: # Parameterizable sine oscillation -sinus_lambda = (lambda t, frequency, amplitude, bias: amplitude * - np.sin(2 * np.pi * frequency * t) + bias) +sinus_lambda = ( + lambda t, frequency, amplitude, bias: amplitude * np.sin(2 * np.pi * frequency * t) + + bias +) # Constant speed -constant_lambda = (lambda t, value: value) +constant_lambda = lambda t, value: value # Parameterizable triangle oscillation -triangle_lambda = (lambda t, amplitude, frequency, bias: amplitude * signal.sawtooth(2 * np.pi * frequency * t, - width=0.5) + bias) +triangle_lambda = ( + lambda t, amplitude, frequency, bias: amplitude + * signal.sawtooth(2 * np.pi * frequency * t, width=0.5) + + bias +) # Parameterizable sawtooth oscillation -saw_lambda = (lambda t, amplitude, frequency, bias: amplitude * signal.sawtooth(2 * np.pi * frequency * t, - width=0.9) + bias) +saw_lambda = ( + lambda t, amplitude, frequency, bias: amplitude + * signal.sawtooth(2 * np.pi * frequency * t, width=0.9) + + bias +) # usage of a random load initializer is only recommended for the # ConstantSpeedLoad, due to the already given profile by an ExternalSpeedLoad -load_init = {'random_init': 'uniform'}, +load_init = ({"random_init": "uniform"},) # External speed profiles can be given by an ExternalSpeedLoad, # inital value is given by bias of the profile sampling_time = 1e-4 -if __name__ == '__main__': +if __name__ == "__main__": # Create the environment env = gem.make( - 'Cont-CC-SeriesDc-v0', - ode_solver='scipy.solve_ivp', + "Cont-CC-SeriesDc-v0", + ode_solver="scipy.solve_ivp", tau=sampling_time, reference_generator=const_switch_gen, - visualization=MotorDashboard( - state_plots=['omega', 'i'], reward_plot=True), + visualization=MotorDashboard(state_plots=["omega", "i"], reward_plot=True), constraints=(), # using ExternalSpeedLoad: - load=ExternalSpeedLoad(speed_profile=saw_lambda, tau=sampling_time, - speed_profile_kwargs=dict(amplitude=40, frequency=5, bias=40)) + load=ExternalSpeedLoad( + speed_profile=saw_lambda, + tau=sampling_time, + speed_profile_kwargs=dict(amplitude=40, frequency=5, bias=40), + ), ) episode_duration = 0.2 # episode duration in seconds @@ -89,4 +101,4 @@ state, _ = env.reset() cum_rew += reward - print(f'Ep {eps} - cum_rew: {cum_rew:10.3f}') + print(f"Ep {eps} - cum_rew: {cum_rew:10.3f}") diff --git a/examples/environment_features/scim_ideal_grid_simulation.py b/examples/environment_features/scim_ideal_grid_simulation.py index 7d47db5f..a0688ab9 100644 --- a/examples/environment_features/scim_ideal_grid_simulation.py +++ b/examples/environment_features/scim_ideal_grid_simulation.py @@ -25,9 +25,10 @@ def grid_voltage(t): u_abc = [ amplitude * np.sin(omega * t + phi_initial), amplitude * np.sin(omega * t + phi_initial - phi), - amplitude * np.sin(omega * t + phi_initial + phi) - ] + amplitude * np.sin(omega * t + phi_initial + phi), + ] return u_abc + return grid_voltage @@ -35,21 +36,17 @@ def grid_voltage(t): env = gem.make( # Choose the squirrel cage induction motor (SCIM) with continuous-control-set "Cont-CC-SCIM-v0", - # load=gem.physical_systems.PolynomialStaticLoad( dict(a=0.0, b=0.0, c=0.0, j_load=1e-6) ), - # Define the numerical solver for the simulation ode_solver="scipy.ode", - # Define which state variables are to be monitored concerning limit violations # "()" means, that limit violation will not necessitate an env.reset() constraints=(), - # Set the sampling time - tau=1e-5 + tau=1e-5, ) tau = env.physical_system.tau @@ -97,16 +94,20 @@ def grid_voltage(t): # STATE[13]: u_sup (DC-link supply voltage) plt.subplots(2, 2, figsize=(7.45, 2.5)) -plt.subplots_adjust(left=None, bottom=None, right=None, top=None, wspace=0.08, hspace=0.05) -plt.rcParams.update({'font.size': 8}) +plt.subplots_adjust( + left=None, bottom=None, right=None, top=None, wspace=0.08, hspace=0.05 +) +plt.rcParams.update({"font.size": 8}) plt.subplot(2, 2, 1) plt.plot(TIME, STATE[0]) plt.ylabel(r"$\omega_\mathrm{me} \, / \, \frac{1}{\mathrm{s}}$") plt.xlim([TIME[0], TIME[-1]]) plt.yticks([0, 50, 100, 150]) -plt.tick_params(axis='x', which='both', labelbottom=False) -plt.tick_params(axis='both', direction="in", left=True, right=False, bottom=True, top=True) +plt.tick_params(axis="x", which="both", labelbottom=False) +plt.tick_params( + axis="both", direction="in", left=True, right=False, bottom=True, top=True +) plt.grid() ax = plt.subplot(2, 2, 2) @@ -118,8 +119,10 @@ def grid_voltage(t): plt.yticks([-200, 0, 200]) ax.yaxis.set_label_position("right") ax.yaxis.tick_right() -plt.tick_params(axis='x', which='both', labelbottom=False) -plt.tick_params(axis='both', direction="in", left=False, right=True, bottom=True, top=True) +plt.tick_params(axis="x", which="both", labelbottom=False) +plt.tick_params( + axis="both", direction="in", left=False, right=True, bottom=True, top=True +) plt.grid() plt.legend(loc="lower right", ncol=3) @@ -129,7 +132,9 @@ def grid_voltage(t): plt.ylabel(r"$T \, / \, \mathrm{Nm}$") plt.xlim([TIME[0], TIME[-1]]) plt.yticks([0, 20]) -plt.tick_params(axis='both', direction="in", left=True, right=False, bottom=True, top=True) +plt.tick_params( + axis="both", direction="in", left=True, right=False, bottom=True, top=True +) plt.grid() ax = plt.subplot(2, 2, 4) @@ -140,7 +145,9 @@ def grid_voltage(t): plt.xlim([TIME[0], TIME[-1]]) ax.yaxis.set_label_position("right") ax.yaxis.tick_right() -plt.tick_params(axis='both', direction="in", left=False, right=True, bottom=True, top=True) +plt.tick_params( + axis="both", direction="in", left=False, right=True, bottom=True, top=True +) plt.yticks([0, 10, 20, 30]) plt.grid() diff --git a/examples/environment_features/userdefined_initialization.py b/examples/environment_features/userdefined_initialization.py index 7ae529c3..07ddf9ee 100644 --- a/examples/environment_features/userdefined_initialization.py +++ b/examples/environment_features/userdefined_initialization.py @@ -3,7 +3,7 @@ from gym_electric_motor.visualization import MotorDashboard import time -''' +""" This code example presents how the initializer interface can be used to sample random initial states for the drive. This is important, e.g., when using reinforcement learning, because random initialization allows for a better exploration of the state space (so called "exploring starts"). @@ -11,58 +11,54 @@ mechanical load (which sets initial drive speed). For a more general introduction to GEM, we recommend to have a look at the "_control.py" examples first. -''' +""" # Initializers use the state names that are present for the used motor: # initializer for a specific current; e.g. DC series motor ('DcSeriesCont-v1' / 'DcSeriesDisc-v1') -dc_series_init = {'states': {'i': 12}} +dc_series_init = {"states": {"i": 12}} # initializer for a specific current and position; e.g. permanent magnet synchronous motor -pmsm_init = { - 'states': { - 'i_sd': -36.0, - 'i_sq': 55.0, - 'epsilon': 3.0 - } -} +pmsm_init = {"states": {"i_sd": -36.0, "i_sq": 55.0, "epsilon": 3.0}} # initializer for a random initial current with gaussian distribution, parameterized with mu=25 and sigma=10 gaussian_init = { - 'random_init': 'gaussian', - 'random_params': (25, 0.1), - 'states': {'i': 0} + "random_init": "gaussian", + "random_params": (25, 0.1), + "states": {"i": 0}, } # initializer for a ranom initial speed with uniform distribution within the interval omega=60 to omega=80 uniform_init = { - 'random_init': 'uniform', - 'interval': [[60, 80]], - 'states': {'omega': 0} + "random_init": "uniform", + "interval": [[60, 80]], + "states": {"omega": 0}, } # initializer for a specific speed -load_init = {'states': {'omega': 20}} +load_init = {"states": {"omega": 20}} -if __name__ == '__main__': +if __name__ == "__main__": env = gem.make( - 'Cont-CC-SeriesDc-v0', - visualization=MotorDashboard(state_plots=['omega', 'i']), - motor=dict(motor_parameter=dict(j_rotor=0.001), motor_initializer=gaussian_init), - load=dict(j_load=0.001, load_initializer=uniform_init), - ode_solver='scipy.solve_ivp', - reference_generator=rg.SwitchedReferenceGenerator( - sub_generators=[ - rg.SinusoidalReferenceGenerator(reference_state='omega'), - rg.WienerProcessReferenceGenerator(reference_state='omega'), - rg.StepReferenceGenerator(reference_state='omega') - ], - p=[0.2, 0.6, 0.2], - super_episode_length=(1000, 10000) - ), - constraints=(), - ) + "Cont-CC-SeriesDc-v0", + visualization=MotorDashboard(state_plots=["omega", "i"]), + motor=dict( + motor_parameter=dict(j_rotor=0.001), motor_initializer=gaussian_init + ), + load=dict(j_load=0.001, load_initializer=uniform_init), + ode_solver="scipy.solve_ivp", + reference_generator=rg.SwitchedReferenceGenerator( + sub_generators=[ + rg.SinusoidalReferenceGenerator(reference_state="omega"), + rg.WienerProcessReferenceGenerator(reference_state="omega"), + rg.StepReferenceGenerator(reference_state="omega"), + ], + p=[0.2, 0.6, 0.2], + super_episode_length=(1000, 10000), + ), + constraints=(), + ) start = time.time() cum_rew = 0 @@ -72,7 +68,9 @@ # Print the initial states: denorm_state = state * env.limits - print(f"Ep. {j}: Initial speed: {denorm_state[0]:5.2f} 1/s, Initial current: {denorm_state[2]:3.2f} A") + print( + f"Ep. {j}: Initial speed: {denorm_state[0]:5.2f} 1/s, Initial current: {denorm_state[2]:3.2f} A" + ) # We should be able to see that the initial state fits the used initializers # Here we should have omega in the interval [60 1/s, 80 1/s] and current closely around 25 A @@ -82,7 +80,3 @@ if terminated: break - - - - diff --git a/examples/reinforcement_learning_controllers/ddpg_pmsm_dq_current_control.py b/examples/reinforcement_learning_controllers/ddpg_pmsm_dq_current_control.py index d72110e0..bdf36a30 100644 --- a/examples/reinforcement_learning_controllers/ddpg_pmsm_dq_current_control.py +++ b/examples/reinforcement_learning_controllers/ddpg_pmsm_dq_current_control.py @@ -1,6 +1,5 @@ from tensorflow.keras.models import Sequential, Model -from tensorflow.keras.layers import Dense, Flatten, Input, \ - Concatenate +from tensorflow.keras.layers import Dense, Flatten, Input, Concatenate from tensorflow.keras import initializers, regularizers from tensorflow.keras.optimizers import Adam from rl.agents import DDPGAgent @@ -10,19 +9,26 @@ import numpy as np import sys import os -sys.path.append(os.path.abspath(os.path.join('..'))) + +sys.path.append(os.path.abspath(os.path.join(".."))) import gym_electric_motor as gem -from gym_electric_motor.reference_generators import MultipleReferenceGenerator, ConstReferenceGenerator, \ - WienerProcessReferenceGenerator +from gym_electric_motor.reference_generators import ( + MultipleReferenceGenerator, + ConstReferenceGenerator, + WienerProcessReferenceGenerator, +) from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.visualization.motor_dashboard_plots import MeanEpisodeRewardPlot from gym_electric_motor.physical_systems.mechanical_loads import ConstantSpeedLoad from gymnasium.core import Wrapper from gymnasium.spaces import Box, Tuple from gym_electric_motor.constraints import SquaredConstraint -from gym_electric_motor.physical_system_wrappers import DqToAbcActionProcessor, DeadTimeProcessor +from gym_electric_motor.physical_system_wrappers import ( + DqToAbcActionProcessor, + DeadTimeProcessor, +) -''' +""" This example shows how we can use GEM to train a reinforcement learning agent to control the current within a permanent magnet synchronous motor three-phase drive. It is assumed that we have direct access to signals within the flux-oriented dq coordinate system. @@ -30,7 +36,7 @@ The state and action space is continuous. We use a deep-deterministic-policy-gradient (DDPG) agent to determine which action must be taken on a continuous-control-set -''' +""" class AppendLastActionWrapper(Wrapper): @@ -44,16 +50,31 @@ class AppendLastActionWrapper(Wrapper): As a measure of feature engineering we append the last selected action to the observation of each time step, because this action will be the one that is active while the agent has to make the next decision. """ + def __init__(self, environment): super().__init__(environment) # append the action space dimensions to the observation space dimensions - self.observation_space = Tuple((Box( - np.concatenate((environment.observation_space[0].low, environment.action_space.low)), - np.concatenate((environment.observation_space[0].high, environment.action_space.high)) - ), environment.observation_space[1])) + self.observation_space = Tuple( + ( + Box( + np.concatenate( + ( + environment.observation_space[0].low, + environment.action_space.low, + ) + ), + np.concatenate( + ( + environment.observation_space[0].high, + environment.action_space.high, + ) + ), + ), + environment.observation_space[1], + ) + ) def step(self, action): - (state, ref), rew, term, info = self.env.step(action) # extend the output state by the selected action @@ -62,7 +83,6 @@ def step(self, action): return (state, ref), rew, term, info def reset(self, **kwargs): - state, ref = self.env.reset() # extend the output state by zeros after reset @@ -72,13 +92,12 @@ def reset(self, **kwargs): return state, ref -if __name__ == '__main__': - +if __name__ == "__main__": # Define reference generators for both currents of the flux oriented dq frame # d current reference is chosen to be constantly at zero to simplify this showcase scenario - d_generator = ConstReferenceGenerator('i_sd', 0) + d_generator = ConstReferenceGenerator("i_sd", 0) # q current changes dynamically - q_generator = WienerProcessReferenceGenerator(reference_state='i_sq') + q_generator = WienerProcessReferenceGenerator(reference_state="i_sq") # The MultipleReferenceGenerator allows to apply these references simultaneously rg = MultipleReferenceGenerator([d_generator, q_generator]) @@ -89,71 +108,57 @@ def reset(self, **kwargs): ) # Change the motor operational limits (important when limit violations can terminate and reset the environment) - limit_values = dict( - i=160*1.41, - omega=12000 * np.pi / 30, - u=450 - ) + limit_values = dict(i=160 * 1.41, omega=12000 * np.pi / 30, u=450) # Change the motor nominal values nominal_values = {key: 0.7 * limit for key, limit in limit_values.items()} physical_system_wrappers = ( DeadTimeProcessor(), - DqToAbcActionProcessor.make('PMSM'), + DqToAbcActionProcessor.make("PMSM"), ) - + # Create the environment env = gem.make( # Choose the permanent magnet synchronous motor with continuous-control-set - 'Cont-CC-PMSM-v0', + "Cont-CC-PMSM-v0", # Pass a class with extra parameters physical_system_wrappers=physical_system_wrappers, visualization=MotorDashboard( - state_plots=['i_sq', 'i_sd'], - action_plots='all', + state_plots=["i_sq", "i_sd"], + action_plots="all", reward_plot=True, - additional_plots=[MeanEpisodeRewardPlot()] + additional_plots=[MeanEpisodeRewardPlot()], ), # Set the mechanical load to have constant speed load=ConstantSpeedLoad(omega_fixed=1000 * np.pi / 30), - # Define which numerical solver is to be used for the simulation - ode_solver='scipy.ode', - + ode_solver="scipy.ode", # Pass the previously defined reference generator reference_generator=rg, - reward_function=dict( # Set weighting of different addends of the reward function - reward_weights={'i_sq': 1000, 'i_sd': 1000}, + reward_weights={"i_sq": 1000, "i_sd": 1000}, # Exponent of the reward function # Here we use a square root function reward_power=0.5, ), - # Define which state variables are to be monitored concerning limit violations # Here, only overcurrent will lead to termination - constraints=(SquaredConstraint(('i_sq', 'i_sd')),), - + constraints=(SquaredConstraint(("i_sq", "i_sd")),), # Consider converter dead time within the simulation # This means that a given action will show effect only with one step delay # This is realistic behavior of drive applications - # Set the DC-link supply voltage - supply=dict( - u_nominal=400 - ), - + supply=dict(u_nominal=400), motor=dict( # Pass the previously defined motor parameters motor_parameter=motor_parameter, - # Pass the updated motor limits and nominal values limit_values=limit_values, nominal_values=nominal_values, ), # Define which states will be shown in the state observation (what we can "measure") - state_filter=['i_sd', 'i_sq', 'epsilon'], + state_filter=["i_sd", "i_sq", "epsilon"], ) # Now we apply the wrapper defined at the beginning of this script @@ -186,36 +191,37 @@ def reset(self, **kwargs): actor = Sequential() # The network's input fits the observation space of the env actor.add(Flatten(input_shape=(window_length,) + env.observation_space.shape)) - actor.add(Dense(16, activation='relu')) - actor.add(Dense(17, activation='relu')) + actor.add(Dense(16, activation="relu")) + actor.add(Dense(17, activation="relu")) # The network output fits the action space of the env - actor.add(Dense( - nb_actions, - kernel_initializer=initializers.RandomNormal(stddev=1e-5), - activation='tanh', - kernel_regularizer=regularizers.l2(1e-2)) + actor.add( + Dense( + nb_actions, + kernel_initializer=initializers.RandomNormal(stddev=1e-5), + activation="tanh", + kernel_regularizer=regularizers.l2(1e-2), + ) ) print(actor.summary()) # Define another artificial neural network to be used within the agent as critic # note that this network has two inputs - action_input = Input(shape=(nb_actions,), name='action_input') - observation_input = Input(shape=(window_length,) + env.observation_space.shape, name='observation_input') + action_input = Input(shape=(nb_actions,), name="action_input") + observation_input = Input( + shape=(window_length,) + env.observation_space.shape, name="observation_input" + ) # (using keras functional API) flattened_observation = Flatten()(observation_input) x = Concatenate()([action_input, flattened_observation]) - x = Dense(32, activation='relu')(x) - x = Dense(32, activation='relu')(x) - x = Dense(32, activation='relu')(x) - x = Dense(1, activation='linear')(x) + x = Dense(32, activation="relu")(x) + x = Dense(32, activation="relu")(x) + x = Dense(32, activation="relu")(x) + x = Dense(1, activation="linear")(x) critic = Model(inputs=(action_input, observation_input), outputs=x) print(critic.summary()) # Define a memory buffer for the agent, allows to learn from past experiences - memory = SequentialMemory( - limit=5000, - window_length=window_length - ) + memory = SequentialMemory(limit=5000, window_length=window_length) # Create a random process for exploration during training # this is essential for the DDPG algorithm @@ -226,7 +232,7 @@ def reset(self, **kwargs): dt=env.physical_system.tau, sigma_min=0.05, n_steps_annealing=85000, - size=2 + size=2, ) # Create the agent for DDPG learning @@ -238,14 +244,13 @@ def reset(self, **kwargs): critic_action_input=action_input, memory=memory, random_process=random_process, - # Define the overall training parameters nb_steps_warmup_actor=2048, nb_steps_warmup_critic=1024, target_model_update=1000, gamma=0.9, batch_size=128, - memory_interval=2 + memory_interval=2, ) # Compile the function approximators within the agent (making them ready for training) @@ -270,5 +275,5 @@ def reset(self, **kwargs): nb_episodes=5, action_repetition=1, visualize=True, - nb_max_episode_steps=10000 + nb_max_episode_steps=10000, ) diff --git a/examples/reinforcement_learning_controllers/ddpg_series_omega_control.py b/examples/reinforcement_learning_controllers/ddpg_series_omega_control.py index 0faa4738..0b9373f0 100644 --- a/examples/reinforcement_learning_controllers/ddpg_series_omega_control.py +++ b/examples/reinforcement_learning_controllers/ddpg_series_omega_control.py @@ -3,8 +3,7 @@ >> python ddpg_series_omega_control.py """ from tensorflow.keras.models import Sequential, Model -from tensorflow.keras.layers import Dense, Flatten, Input, \ - Concatenate +from tensorflow.keras.layers import Dense, Flatten, Input, Concatenate import tensorflow as tf from tensorflow.keras.optimizers import Adam from rl.agents import DDPGAgent @@ -13,24 +12,24 @@ from gymnasium.wrappers import FlattenObservation import sys import os -sys.path.append(os.path.abspath(os.path.join('..'))) + +sys.path.append(os.path.abspath(os.path.join(".."))) import gym_electric_motor as gem from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator from gym_electric_motor.visualization import MotorDashboard -''' +""" This example shows how we can use GEM to train a reinforcement learning agent to control the motor speed of a DC series motor. The state and action space is continuous. We use a deep-deterministic-policy-gradient (DDPG) agent to determine which action must be taken on a continuous-control-set -''' - -if __name__ == '__main__': +""" +if __name__ == "__main__": # Define the drive environment env = gem.make( # Define the speed control series DC motor environment with continuous-control-set - 'Cont-SC-SeriesDc-v0', + "Cont-SC-SeriesDc-v0", ) # For data processing we want to flatten the env output, @@ -60,39 +59,34 @@ actor = Sequential() # The network's input fits the observation space of the env actor.add(Flatten(input_shape=(window_length,) + env.observation_space.shape)) - actor.add(Dense(16, activation='relu')) - actor.add(Dense(16, activation='relu')) + actor.add(Dense(16, activation="relu")) + actor.add(Dense(16, activation="relu")) # The network output fits the action space of the env - actor.add(Dense(nb_actions, activation='sigmoid')) + actor.add(Dense(nb_actions, activation="sigmoid")) print(actor.summary()) # Define another artificial neural network to be used within the agent as critic # note that this network has two inputs - action_input = Input(shape=(nb_actions,), name='action_input') - observation_input = Input(shape=(window_length,) + env.observation_space.shape, name='observation_input') + action_input = Input(shape=(nb_actions,), name="action_input") + observation_input = Input( + shape=(window_length,) + env.observation_space.shape, name="observation_input" + ) # (using keras functional API) flattened_observation = Flatten()(observation_input) x = Concatenate()([action_input, flattened_observation]) - x = Dense(32, activation='relu')(x) - x = Dense(32, activation='relu')(x) - x = Dense(32, activation='relu')(x) - x = Dense(1, activation='linear')(x) + x = Dense(32, activation="relu")(x) + x = Dense(32, activation="relu")(x) + x = Dense(32, activation="relu")(x) + x = Dense(1, activation="linear")(x) critic = Model(inputs=(action_input, observation_input), outputs=x) print(critic.summary()) # Define a memory buffer for the agent, allows to learn from past experiences - memory = SequentialMemory( - limit=10000, - window_length=window_length - ) + memory = SequentialMemory(limit=10000, window_length=window_length) # Create a random process for exploration during training # this is essential for the DDPG algorithm - random_process = OrnsteinUhlenbeckProcess( - theta=0.5, - mu=0.0, - sigma=0.2 - ) + random_process = OrnsteinUhlenbeckProcess(theta=0.5, mu=0.0, sigma=0.2) # Create the agent for DDPG learning agent = DDPGAgent( @@ -103,19 +97,18 @@ critic_action_input=action_input, memory=memory, random_process=random_process, - # Define the overall training parameters nb_steps_warmup_actor=2048, nb_steps_warmup_critic=1024, target_model_update=1000, gamma=0.95, batch_size=128, - memory_interval=1 + memory_interval=1, ) # Compile the function approximators within the agent (making them ready for training) # Note that the DDPG agent uses two function approximators, hence we define two optimizers here - agent.compile((Adam(lr=1e-6), Adam(lr=1e-4)), metrics=['mae']) + agent.compile((Adam(lr=1e-6), Adam(lr=1e-4)), metrics=["mae"]) # Start training for 1.5 million simulation steps agent.fit( @@ -127,7 +120,7 @@ nb_max_start_steps=0, nb_max_episode_steps=10000, log_interval=10000, - callbacks=[] + callbacks=[], ) # Test the agent @@ -136,5 +129,5 @@ nb_episodes=10, action_repetition=1, nb_max_episode_steps=5000, - visualize=True + visualize=True, ) diff --git a/examples/reinforcement_learning_controllers/dqn_series_current_control.py b/examples/reinforcement_learning_controllers/dqn_series_current_control.py index ac3bae2a..e48ce011 100644 --- a/examples/reinforcement_learning_controllers/dqn_series_current_control.py +++ b/examples/reinforcement_learning_controllers/dqn_series_current_control.py @@ -11,33 +11,31 @@ from gymnasium.wrappers import FlattenObservation import sys import os -sys.path.append(os.path.abspath(os.path.join('..'))) + +sys.path.append(os.path.abspath(os.path.join(".."))) import gym_electric_motor as gem from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -''' +""" This example shows how we can use GEM to train a reinforcement learning agent to control the motor current of a DC series motor. In this scenario, the state space is continuous while the action space is discrete. We use a deep Q learning agent to determine which action must be taken on a finite-control-set -''' - -if __name__ == '__main__': +""" +if __name__ == "__main__": # Define the drive environment # Default DcSeries Motor Parameters are changed to have more dynamic system and to see faster learning result env = gem.make( # Define the series DC motor with finite-control-set - 'Finite-CC-SeriesDc-v0', - + "Finite-CC-SeriesDc-v0", # Defines the utilized power converter, which determines the action space # 'Disc-1QC' is our notation for a discontinuous one-quadrant converter, # which is a one-phase buck converter with available actions 'switch on' and 'switch off' - converter='Finite-1QC', - + converter="Finite-1QC", # Define which states will be shown in the state observation (what we can "measure") - state_filter=['omega', 'i'], + state_filter=["omega", "i"], ) # Now, the environment will output states and references separately @@ -58,25 +56,26 @@ model = Sequential() # The network's input fits the observation space of the env model.add(Flatten(input_shape=(window_length,) + env.observation_space.shape)) - model.add(Dense(16, activation='relu')) - model.add(Dense(16, activation='relu')) - model.add(Dense(4, activation='relu')) + model.add(Dense(16, activation="relu")) + model.add(Dense(16, activation="relu")) + model.add(Dense(4, activation="relu")) # The network output fits the action space of the env - model.add(Dense(nb_actions, activation='linear')) + model.add(Dense(nb_actions, activation="linear")) # Define a memory buffer for the agent, allows to learn from past experiences memory = SequentialMemory(limit=15000, window_length=window_length) # Define the policy which the agent will use for training and testing # in this case, we use an epsilon greedy policy with decreasing epsilon for training - policy = LinearAnnealedPolicy(EpsGreedyQPolicy(eps=0.2), - attr='eps', - value_max=0.2, # initial value of epsilon (during training) - value_min=0.01, # final value of epsilon (during training) - value_test=0, # epsilon during testing, epsilon=0 => deterministic behavior - nb_steps=20000 # annealing interval - # (duration for the transition from value_max to value_min) - ) + policy = LinearAnnealedPolicy( + EpsGreedyQPolicy(eps=0.2), + attr="eps", + value_max=0.2, # initial value of epsilon (during training) + value_min=0.01, # final value of epsilon (during training) + value_test=0, # epsilon during testing, epsilon=0 => deterministic behavior + nb_steps=20000, # annealing interval + # (duration for the transition from value_max to value_min) + ) # Create the agent for deep Q learning dqn = DQNAgent( @@ -85,30 +84,28 @@ policy=policy, nb_actions=nb_actions, memory=memory, - # Define the overall training parameters gamma=0.9, batch_size=128, train_interval=1, - memory_interval=1 + memory_interval=1, ) # Compile the model within the agent (making it ready for training) # using ADAM optimizer - dqn.compile(Adam(lr=1e-4), metrics=['mse']) + dqn.compile(Adam(lr=1e-4), metrics=["mse"]) # Start training the agent - dqn.fit(env, - nb_steps=200000, # number of training steps - action_repetition=1, - verbose=2, - visualize=True, # use the environment's visualization (the dashboard) - nb_max_episode_steps=50000, # maximum length of one episode - # (episodes end prematurely when drive limits are violated) - log_interval=10000) + dqn.fit( + env, + nb_steps=200000, # number of training steps + action_repetition=1, + verbose=2, + visualize=True, # use the environment's visualization (the dashboard) + nb_max_episode_steps=50000, # maximum length of one episode + # (episodes end prematurely when drive limits are violated) + log_interval=10000, + ) # Test the agent (without exploration noise, as we set value_test=0 within our policy) - dqn.test(env, - nb_episodes=3, - nb_max_episode_steps=50000, - visualize=True) + dqn.test(env, nb_episodes=3, nb_max_episode_steps=50000, visualize=True) diff --git a/gym_electric_motor/__init__.py b/gym_electric_motor/__init__.py index 2a73df67..91e4ce2b 100644 --- a/gym_electric_motor/__init__.py +++ b/gym_electric_motor/__init__.py @@ -28,294 +28,298 @@ # Add all superclasses of the modules to the registry. # Deactivate the order enforce wrapper that is put around a created env per default from gymnasium-version 0.21.0 onwards -registration_kwargs = dict(order_enforce=False) if version.parse(gymnasium.__version__) >= version.parse('0.21.0') else dict() -registration_kwargs['disable_env_checker'] = True +registration_kwargs = ( + dict(order_enforce=False) + if version.parse(gymnasium.__version__) >= version.parse("0.21.0") + else dict() +) +registration_kwargs["disable_env_checker"] = True -envs_path = 'gym_electric_motor.envs:' +envs_path = "gym_electric_motor.envs:" # Permanently Excited DC Motor Environments register( - id='Finite-SC-PermExDc-v0', - entry_point=envs_path+'FiniteSpeedControlDcPermanentlyExcitedMotorEnv', - **registration_kwargs + id="Finite-SC-PermExDc-v0", + entry_point=envs_path + "FiniteSpeedControlDcPermanentlyExcitedMotorEnv", + **registration_kwargs, ) register( - id='Cont-SC-PermExDc-v0', - entry_point=envs_path+'ContSpeedControlDcPermanentlyExcitedMotorEnv', - **registration_kwargs + id="Cont-SC-PermExDc-v0", + entry_point=envs_path + "ContSpeedControlDcPermanentlyExcitedMotorEnv", + **registration_kwargs, ) register( - id='Finite-TC-PermExDc-v0', - entry_point=envs_path+'FiniteTorqueControlDcPermanentlyExcitedMotorEnv', - **registration_kwargs + id="Finite-TC-PermExDc-v0", + entry_point=envs_path + "FiniteTorqueControlDcPermanentlyExcitedMotorEnv", + **registration_kwargs, ) register( - id='Cont-TC-PermExDc-v0', - entry_point=envs_path+'ContTorqueControlDcPermanentlyExcitedMotorEnv', - **registration_kwargs + id="Cont-TC-PermExDc-v0", + entry_point=envs_path + "ContTorqueControlDcPermanentlyExcitedMotorEnv", + **registration_kwargs, ) register( - id='Finite-CC-PermExDc-v0', - entry_point=envs_path+'FiniteCurrentControlDcPermanentlyExcitedMotorEnv', - **registration_kwargs + id="Finite-CC-PermExDc-v0", + entry_point=envs_path + "FiniteCurrentControlDcPermanentlyExcitedMotorEnv", + **registration_kwargs, ) register( - id='Cont-CC-PermExDc-v0', - entry_point=envs_path+'ContCurrentControlDcPermanentlyExcitedMotorEnv', - **registration_kwargs + id="Cont-CC-PermExDc-v0", + entry_point=envs_path + "ContCurrentControlDcPermanentlyExcitedMotorEnv", + **registration_kwargs, ) # Externally Excited DC Motor Environments register( - id='Finite-SC-ExtExDc-v0', - entry_point=envs_path+'FiniteSpeedControlDcExternallyExcitedMotorEnv', - **registration_kwargs + id="Finite-SC-ExtExDc-v0", + entry_point=envs_path + "FiniteSpeedControlDcExternallyExcitedMotorEnv", + **registration_kwargs, ) register( - id='Cont-SC-ExtExDc-v0', - entry_point=envs_path+'ContSpeedControlDcExternallyExcitedMotorEnv', - **registration_kwargs + id="Cont-SC-ExtExDc-v0", + entry_point=envs_path + "ContSpeedControlDcExternallyExcitedMotorEnv", + **registration_kwargs, ) register( - id='Finite-TC-ExtExDc-v0', - entry_point=envs_path+'FiniteTorqueControlDcExternallyExcitedMotorEnv', - **registration_kwargs + id="Finite-TC-ExtExDc-v0", + entry_point=envs_path + "FiniteTorqueControlDcExternallyExcitedMotorEnv", + **registration_kwargs, ) register( - id='Cont-TC-ExtExDc-v0', - entry_point=envs_path+'ContTorqueControlDcExternallyExcitedMotorEnv', - **registration_kwargs + id="Cont-TC-ExtExDc-v0", + entry_point=envs_path + "ContTorqueControlDcExternallyExcitedMotorEnv", + **registration_kwargs, ) register( - id='Finite-CC-ExtExDc-v0', - entry_point=envs_path+'FiniteCurrentControlDcExternallyExcitedMotorEnv', - **registration_kwargs + id="Finite-CC-ExtExDc-v0", + entry_point=envs_path + "FiniteCurrentControlDcExternallyExcitedMotorEnv", + **registration_kwargs, ) register( - id='Cont-CC-ExtExDc-v0', - entry_point=envs_path+'ContCurrentControlDcExternallyExcitedMotorEnv', - **registration_kwargs + id="Cont-CC-ExtExDc-v0", + entry_point=envs_path + "ContCurrentControlDcExternallyExcitedMotorEnv", + **registration_kwargs, ) # Series DC Motor Environments register( - id='Finite-SC-SeriesDc-v0', - entry_point=envs_path+'FiniteSpeedControlDcSeriesMotorEnv', - **registration_kwargs + id="Finite-SC-SeriesDc-v0", + entry_point=envs_path + "FiniteSpeedControlDcSeriesMotorEnv", + **registration_kwargs, ) register( - id='Cont-SC-SeriesDc-v0', - entry_point=envs_path+'ContSpeedControlDcSeriesMotorEnv', - **registration_kwargs + id="Cont-SC-SeriesDc-v0", + entry_point=envs_path + "ContSpeedControlDcSeriesMotorEnv", + **registration_kwargs, ) register( - id='Finite-TC-SeriesDc-v0', - entry_point=envs_path+'FiniteTorqueControlDcSeriesMotorEnv', - **registration_kwargs + id="Finite-TC-SeriesDc-v0", + entry_point=envs_path + "FiniteTorqueControlDcSeriesMotorEnv", + **registration_kwargs, ) register( - id='Cont-TC-SeriesDc-v0', - entry_point=envs_path+'ContTorqueControlDcSeriesMotorEnv', - **registration_kwargs + id="Cont-TC-SeriesDc-v0", + entry_point=envs_path + "ContTorqueControlDcSeriesMotorEnv", + **registration_kwargs, ) register( - id='Finite-CC-SeriesDc-v0', - entry_point=envs_path+'FiniteCurrentControlDcSeriesMotorEnv', - **registration_kwargs + id="Finite-CC-SeriesDc-v0", + entry_point=envs_path + "FiniteCurrentControlDcSeriesMotorEnv", + **registration_kwargs, ) register( - id='Cont-CC-SeriesDc-v0', - entry_point=envs_path+'ContCurrentControlDcSeriesMotorEnv', - **registration_kwargs + id="Cont-CC-SeriesDc-v0", + entry_point=envs_path + "ContCurrentControlDcSeriesMotorEnv", + **registration_kwargs, ) # Shunt DC Motor Environments register( - id='Finite-SC-ShuntDc-v0', - entry_point=envs_path+'FiniteSpeedControlDcShuntMotorEnv', - **registration_kwargs + id="Finite-SC-ShuntDc-v0", + entry_point=envs_path + "FiniteSpeedControlDcShuntMotorEnv", + **registration_kwargs, ) register( - id='Cont-SC-ShuntDc-v0', - entry_point=envs_path+'ContSpeedControlDcShuntMotorEnv', - **registration_kwargs + id="Cont-SC-ShuntDc-v0", + entry_point=envs_path + "ContSpeedControlDcShuntMotorEnv", + **registration_kwargs, ) register( - id='Finite-TC-ShuntDc-v0', - entry_point=envs_path+'FiniteTorqueControlDcShuntMotorEnv', - **registration_kwargs + id="Finite-TC-ShuntDc-v0", + entry_point=envs_path + "FiniteTorqueControlDcShuntMotorEnv", + **registration_kwargs, ) register( - id='Cont-TC-ShuntDc-v0', - entry_point=envs_path+'ContTorqueControlDcShuntMotorEnv', - **registration_kwargs + id="Cont-TC-ShuntDc-v0", + entry_point=envs_path + "ContTorqueControlDcShuntMotorEnv", + **registration_kwargs, ) register( - id='Finite-CC-ShuntDc-v0', - entry_point=envs_path+'FiniteCurrentControlDcShuntMotorEnv', - **registration_kwargs + id="Finite-CC-ShuntDc-v0", + entry_point=envs_path + "FiniteCurrentControlDcShuntMotorEnv", + **registration_kwargs, ) register( - id='Cont-CC-ShuntDc-v0', - entry_point=envs_path+'ContCurrentControlDcShuntMotorEnv', - **registration_kwargs + id="Cont-CC-ShuntDc-v0", + entry_point=envs_path + "ContCurrentControlDcShuntMotorEnv", + **registration_kwargs, ) # Permanent Magnet Synchronous Motor Environments register( - id='Finite-SC-PMSM-v0', - entry_point=envs_path+'FiniteSpeedControlPermanentMagnetSynchronousMotorEnv', - **registration_kwargs + id="Finite-SC-PMSM-v0", + entry_point=envs_path + "FiniteSpeedControlPermanentMagnetSynchronousMotorEnv", + **registration_kwargs, ) register( - id='Finite-TC-PMSM-v0', - entry_point=envs_path+'FiniteTorqueControlPermanentMagnetSynchronousMotorEnv', - **registration_kwargs + id="Finite-TC-PMSM-v0", + entry_point=envs_path + "FiniteTorqueControlPermanentMagnetSynchronousMotorEnv", + **registration_kwargs, ) register( - id='Finite-CC-PMSM-v0', - entry_point=envs_path+'FiniteCurrentControlPermanentMagnetSynchronousMotorEnv', - **registration_kwargs + id="Finite-CC-PMSM-v0", + entry_point=envs_path + "FiniteCurrentControlPermanentMagnetSynchronousMotorEnv", + **registration_kwargs, ) register( - id='Cont-CC-PMSM-v0', - entry_point=envs_path+'ContCurrentControlPermanentMagnetSynchronousMotorEnv', - **registration_kwargs + id="Cont-CC-PMSM-v0", + entry_point=envs_path + "ContCurrentControlPermanentMagnetSynchronousMotorEnv", + **registration_kwargs, ) register( - id='Cont-TC-PMSM-v0', - entry_point=envs_path+'ContTorqueControlPermanentMagnetSynchronousMotorEnv', - **registration_kwargs + id="Cont-TC-PMSM-v0", + entry_point=envs_path + "ContTorqueControlPermanentMagnetSynchronousMotorEnv", + **registration_kwargs, ) register( - id='Cont-SC-PMSM-v0', - entry_point=envs_path+'ContSpeedControlPermanentMagnetSynchronousMotorEnv', - **registration_kwargs + id="Cont-SC-PMSM-v0", + entry_point=envs_path + "ContSpeedControlPermanentMagnetSynchronousMotorEnv", + **registration_kwargs, ) # Externally Excited Synchronous Motor Environments register( - id='Finite-SC-EESM-v0', - entry_point=envs_path+'FiniteSpeedControlExternallyExcitedSynchronousMotorEnv', - **registration_kwargs + id="Finite-SC-EESM-v0", + entry_point=envs_path + "FiniteSpeedControlExternallyExcitedSynchronousMotorEnv", + **registration_kwargs, ) register( - id='Finite-TC-EESM-v0', - entry_point=envs_path+'FiniteTorqueControlExternallyExcitedSynchronousMotorEnv', - **registration_kwargs + id="Finite-TC-EESM-v0", + entry_point=envs_path + "FiniteTorqueControlExternallyExcitedSynchronousMotorEnv", + **registration_kwargs, ) register( - id='Finite-CC-EESM-v0', - entry_point=envs_path+'FiniteCurrentControlExternallyExcitedSynchronousMotorEnv', - **registration_kwargs + id="Finite-CC-EESM-v0", + entry_point=envs_path + "FiniteCurrentControlExternallyExcitedSynchronousMotorEnv", + **registration_kwargs, ) register( - id='Cont-CC-EESM-v0', - entry_point=envs_path+'ContCurrentControlExternallyExcitedSynchronousMotorEnv', - **registration_kwargs + id="Cont-CC-EESM-v0", + entry_point=envs_path + "ContCurrentControlExternallyExcitedSynchronousMotorEnv", + **registration_kwargs, ) register( - id='Cont-TC-EESM-v0', - entry_point=envs_path+'ContTorqueControlExternallyExcitedSynchronousMotorEnv', - **registration_kwargs + id="Cont-TC-EESM-v0", + entry_point=envs_path + "ContTorqueControlExternallyExcitedSynchronousMotorEnv", + **registration_kwargs, ) register( - id='Cont-SC-EESM-v0', - entry_point=envs_path+'ContSpeedControlExternallyExcitedSynchronousMotorEnv', - **registration_kwargs + id="Cont-SC-EESM-v0", + entry_point=envs_path + "ContSpeedControlExternallyExcitedSynchronousMotorEnv", + **registration_kwargs, ) # Synchronous Reluctance Motor Environments register( - id='Finite-SC-SynRM-v0', - entry_point=envs_path+'FiniteSpeedControlSynchronousReluctanceMotorEnv', - **registration_kwargs + id="Finite-SC-SynRM-v0", + entry_point=envs_path + "FiniteSpeedControlSynchronousReluctanceMotorEnv", + **registration_kwargs, ) register( - id='Finite-TC-SynRM-v0', - entry_point=envs_path+'FiniteTorqueControlSynchronousReluctanceMotorEnv', - **registration_kwargs + id="Finite-TC-SynRM-v0", + entry_point=envs_path + "FiniteTorqueControlSynchronousReluctanceMotorEnv", + **registration_kwargs, ) register( - id='Finite-CC-SynRM-v0', - entry_point=envs_path+'FiniteCurrentControlSynchronousReluctanceMotorEnv', - **registration_kwargs + id="Finite-CC-SynRM-v0", + entry_point=envs_path + "FiniteCurrentControlSynchronousReluctanceMotorEnv", + **registration_kwargs, ) register( - id='Cont-CC-SynRM-v0', - entry_point=envs_path+'ContCurrentControlSynchronousReluctanceMotorEnv', - **registration_kwargs + id="Cont-CC-SynRM-v0", + entry_point=envs_path + "ContCurrentControlSynchronousReluctanceMotorEnv", + **registration_kwargs, ) register( - id='Cont-TC-SynRM-v0', - entry_point=envs_path+'ContTorqueControlSynchronousReluctanceMotorEnv', - **registration_kwargs + id="Cont-TC-SynRM-v0", + entry_point=envs_path + "ContTorqueControlSynchronousReluctanceMotorEnv", + **registration_kwargs, ) register( - id='Cont-SC-SynRM-v0', - entry_point=envs_path+'ContSpeedControlSynchronousReluctanceMotorEnv', - **registration_kwargs + id="Cont-SC-SynRM-v0", + entry_point=envs_path + "ContSpeedControlSynchronousReluctanceMotorEnv", + **registration_kwargs, ) # Squirrel Cage Induction Motor Environments register( - id='Finite-SC-SCIM-v0', - entry_point=envs_path+'FiniteSpeedControlSquirrelCageInductionMotorEnv', - **registration_kwargs + id="Finite-SC-SCIM-v0", + entry_point=envs_path + "FiniteSpeedControlSquirrelCageInductionMotorEnv", + **registration_kwargs, ) register( - id='Finite-TC-SCIM-v0', - entry_point=envs_path+'FiniteTorqueControlSquirrelCageInductionMotorEnv', - **registration_kwargs + id="Finite-TC-SCIM-v0", + entry_point=envs_path + "FiniteTorqueControlSquirrelCageInductionMotorEnv", + **registration_kwargs, ) register( - id='Finite-CC-SCIM-v0', - entry_point=envs_path+'FiniteCurrentControlSquirrelCageInductionMotorEnv', - **registration_kwargs + id="Finite-CC-SCIM-v0", + entry_point=envs_path + "FiniteCurrentControlSquirrelCageInductionMotorEnv", + **registration_kwargs, ) register( - id='Cont-CC-SCIM-v0', - entry_point=envs_path+'ContCurrentControlSquirrelCageInductionMotorEnv', - **registration_kwargs + id="Cont-CC-SCIM-v0", + entry_point=envs_path + "ContCurrentControlSquirrelCageInductionMotorEnv", + **registration_kwargs, ) register( - id='Cont-TC-SCIM-v0', - entry_point=envs_path+'ContTorqueControlSquirrelCageInductionMotorEnv', - **registration_kwargs + id="Cont-TC-SCIM-v0", + entry_point=envs_path + "ContTorqueControlSquirrelCageInductionMotorEnv", + **registration_kwargs, ) register( - id='Cont-SC-SCIM-v0', - entry_point=envs_path+'ContSpeedControlSquirrelCageInductionMotorEnv', - **registration_kwargs + id="Cont-SC-SCIM-v0", + entry_point=envs_path + "ContSpeedControlSquirrelCageInductionMotorEnv", + **registration_kwargs, ) # Doubly Fed Induction Motor Environments register( - id='Finite-SC-DFIM-v0', - entry_point=envs_path+'FiniteSpeedControlDoublyFedInductionMotorEnv', - **registration_kwargs + id="Finite-SC-DFIM-v0", + entry_point=envs_path + "FiniteSpeedControlDoublyFedInductionMotorEnv", + **registration_kwargs, ) register( - id='Finite-TC-DFIM-v0', - entry_point=envs_path+'FiniteTorqueControlDoublyFedInductionMotorEnv', - **registration_kwargs + id="Finite-TC-DFIM-v0", + entry_point=envs_path + "FiniteTorqueControlDoublyFedInductionMotorEnv", + **registration_kwargs, ) register( - id='Finite-CC-DFIM-v0', - entry_point=envs_path+'FiniteCurrentControlDoublyFedInductionMotorEnv', - **registration_kwargs + id="Finite-CC-DFIM-v0", + entry_point=envs_path + "FiniteCurrentControlDoublyFedInductionMotorEnv", + **registration_kwargs, ) register( - id='Cont-CC-DFIM-v0', - entry_point=envs_path+'ContCurrentControlDoublyFedInductionMotorEnv', - **registration_kwargs + id="Cont-CC-DFIM-v0", + entry_point=envs_path + "ContCurrentControlDoublyFedInductionMotorEnv", + **registration_kwargs, ) register( - id='Cont-TC-DFIM-v0', - entry_point=envs_path+'ContTorqueControlDoublyFedInductionMotorEnv', - **registration_kwargs + id="Cont-TC-DFIM-v0", + entry_point=envs_path + "ContTorqueControlDoublyFedInductionMotorEnv", + **registration_kwargs, ) register( - id='Cont-SC-DFIM-v0', - entry_point=envs_path+'ContSpeedControlDoublyFedInductionMotorEnv', - **registration_kwargs + id="Cont-SC-DFIM-v0", + entry_point=envs_path + "ContSpeedControlDoublyFedInductionMotorEnv", + **registration_kwargs, ) diff --git a/gym_electric_motor/callbacks.py b/gym_electric_motor/callbacks.py index 5cab3ad9..d530345c 100644 --- a/gym_electric_motor/callbacks.py +++ b/gym_electric_motor/callbacks.py @@ -1,7 +1,10 @@ """This module introduces predefined callbacks for the GEM environment.""" from .core import Callback -from gym_electric_motor.reference_generators import SubepisodedReferenceGenerator, SwitchedReferenceGenerator +from gym_electric_motor.reference_generators import ( + SubepisodedReferenceGenerator, + SwitchedReferenceGenerator, +) class RampingLimitMargin(Callback): @@ -14,14 +17,20 @@ class RampingLimitMargin(Callback): :mod:`~gym_electric_motor.reference_generators.subepisoded_reference_generator.SubepisodedReferenceGenerator` as sub generators. """ - + __CLASS_ERROR__ = ( "The RampingLimitMargin does only support the SubepisodedReferenceGenerator as reference generator or " "SwitchedReferenceGenerator with SubepisodedReferenceGenerator as all sub reference generators" ) - def __init__(self, initial_limit_margin=(-0.1, 0.1), maximum_limit_margin=(-1, 1), step_size=0.1, - update_time='episode', update_freq=10): + def __init__( + self, + initial_limit_margin=(-0.1, 0.1), + maximum_limit_margin=(-1, 1), + step_size=0.1, + update_time="episode", + update_freq=10, + ): """ Args: initial_limit_margin(tuple(floats)): The initial limit margin which gets updated by AdaptiveLimitMargin @@ -32,41 +41,56 @@ def __init__(self, initial_limit_margin=(-0.1, 0.1), maximum_limit_margin=(-1, 1 update_time(string): When the update happens. "step" for the end of a step, "episode" for the end of an episode update_freq(int): After how many cumulative units of update_time an update occurs - + Additional Notes: All limit_margins should be between -1 and 1 """ super().__init__() - assert update_time in ['step', 'episode'], \ - "Chose an option of either 'step' or 'episode' for updating on cumulative steps or episodes" - assert initial_limit_margin[1] > initial_limit_margin[0], \ - "First element of limit margin has to be smaller than second" - assert maximum_limit_margin[1] > maximum_limit_margin[0], \ - "First element of limit margin has to be smaller than second" - assert initial_limit_margin[0] >= -1, "Lower limit margin has to be bigger than or equal to -1" - assert maximum_limit_margin[0] >= -1, "Lower limit margin has to be bigger than or equal to -1" - assert initial_limit_margin[1] <= 1, "Upper limit margin has to be smaller than or equal to 1" - assert maximum_limit_margin[1] <= 1, "Upper limit margin has to be smaller than or equal to 1" - + assert ( + update_time in ["step", "episode"] + ), "Chose an option of either 'step' or 'episode' for updating on cumulative steps or episodes" + assert ( + initial_limit_margin[1] > initial_limit_margin[0] + ), "First element of limit margin has to be smaller than second" + assert ( + maximum_limit_margin[1] > maximum_limit_margin[0] + ), "First element of limit margin has to be smaller than second" + assert ( + initial_limit_margin[0] >= -1 + ), "Lower limit margin has to be bigger than or equal to -1" + assert ( + maximum_limit_margin[0] >= -1 + ), "Lower limit margin has to be bigger than or equal to -1" + assert ( + initial_limit_margin[1] <= 1 + ), "Upper limit margin has to be smaller than or equal to 1" + assert ( + maximum_limit_margin[1] <= 1 + ), "Upper limit margin has to be smaller than or equal to 1" + self._limit_margin = initial_limit_margin self._maximum_limit_margin = maximum_limit_margin self._step_size = step_size self._update_time = update_time - if self._update_time == 'step': + if self._update_time == "step": self._step = 0 else: self._episode = 0 self._update_freq = update_freq - + def set_env(self, env): # See docstring of superclass # Assertions added to check for the right reference generator if isinstance(env.reference_generator, SwitchedReferenceGenerator): for sub_generator in env.reference_generator._sub_generators: - assert issubclass(type(sub_generator), SubepisodedReferenceGenerator), self.__CLASS_ERROR__ + assert issubclass( + type(sub_generator), SubepisodedReferenceGenerator + ), self.__CLASS_ERROR__ else: - assert issubclass(type(env.reference_generator), SubepisodedReferenceGenerator), self.__CLASS_ERROR__ + assert issubclass( + type(env.reference_generator), SubepisodedReferenceGenerator + ), self.__CLASS_ERROR__ self._env = env # Initial image margin added to the reference generator @@ -75,32 +99,38 @@ def set_env(self, env): sub_generator._limit_margin = self._limit_margin else: self._env.reference_generator._limit_margin = self._limit_margin - + def on_step_end(self, k, state, reference, reward, terminated): # See docstring of superclass - if self._update_time == 'step': + if self._update_time == "step": self._step += 1 if self._step % self._update_freq == 0: self._step = 0 - self._update_limit_margin() - + self._update_limit_margin() + def on_reset_end(self, state, reference): # See docstring of superclass - if self._update_time == 'episode': + if self._update_time == "episode": self._episode += 1 if self._episode % self._update_freq == 0: self._episode = 0 self._update_limit_margin() - + def _update_limit_margin(self): """Updates the limit margin of the environments according to the step size and maximum limit margin""" if self._limit_margin != self._maximum_limit_margin: new_lower_limit = self._limit_margin[0] - self._step_size - new_lower_limit = new_lower_limit \ - if new_lower_limit > self._maximum_limit_margin[0] else self._maximum_limit_margin[0] + new_lower_limit = ( + new_lower_limit + if new_lower_limit > self._maximum_limit_margin[0] + else self._maximum_limit_margin[0] + ) new_upper_limit = self._limit_margin[1] + self._step_size - new_upper_limit = new_upper_limit \ - if new_upper_limit < self._maximum_limit_margin[1] else self._maximum_limit_margin[1] + new_upper_limit = ( + new_upper_limit + if new_upper_limit < self._maximum_limit_margin[1] + else self._maximum_limit_margin[1] + ) self._limit_margin = (new_lower_limit, new_upper_limit) if isinstance(self._env.reference_generator, SwitchedReferenceGenerator): for sub_generator in self._env.reference_generator._sub_generators: diff --git a/gym_electric_motor/constraints.py b/gym_electric_motor/constraints.py index 1683e447..3a81ef41 100644 --- a/gym_electric_motor/constraints.py +++ b/gym_electric_motor/constraints.py @@ -41,7 +41,7 @@ class LimitConstraint(Constraint): """ - def __init__(self, observed_state_names='all_states'): + def __init__(self, observed_state_names="all_states"): """ Args: observed_state_names(['all_states']/iterable(str)): The states to observe. \n @@ -59,7 +59,7 @@ def __call__(self, state): def set_modules(self, ps): self._limits = ps.limits - if 'all_states' in self._observed_state_names: + if "all_states" in self._observed_state_names: self._observed_state_names = ps.state_names if self._observed_state_names is None: self._observed_state_names = [] @@ -91,8 +91,14 @@ def __init__(self, states=()): def set_modules(self, ps): self._state_indices = [ps.state_positions[state] for state in self._states] self._limits = ps.limits[self._state_indices] - self._normalized = not np.all(ps.state_space.high[self._state_indices] == self._limits) + self._normalized = not np.all( + ps.state_space.high[self._state_indices] == self._limits + ) def __call__(self, state): - state_ = state[self._state_indices] if self._normalized else state[self._state_indices] / self._limits + state_ = ( + state[self._state_indices] + if self._normalized + else state[self._state_indices] / self._limits + ) return float(np.sum(state_**2) > 1.0) diff --git a/gym_electric_motor/core.py b/gym_electric_motor/core.py index 36888069..faeb861d 100644 --- a/gym_electric_motor/core.py +++ b/gym_electric_motor/core.py @@ -91,9 +91,11 @@ class ElectricMotorEnvironment(gymnasium.core.Env): """ env_id = None - metadata = {"render_modes": [None, "figure", "figure_once", "figure_academic"], - "save_figure_on_close": False, - "hold_figure_on_close": True} + metadata = { + "render_modes": [None, "figure", "figure_once", "figure_academic"], + "save_figure_on_close": False, + "hold_figure_on_close": True, + } @property def physical_system(self): @@ -175,8 +177,20 @@ def visualizations(self): """Returns a list of all active motor visualizations.""" return self._visualizations - def __init__(self, physical_system, reference_generator, reward_function, visualization=(), state_filter=None, - callbacks=(), constraints=(), physical_system_wrappers=(), render_mode=None, scale_plots = None, **kwargs): + def __init__( + self, + physical_system, + reference_generator, + reward_function, + visualization=(), + state_filter=None, + callbacks=(), + constraints=(), + physical_system_wrappers=(), + render_mode=None, + scale_plots=None, + **kwargs, + ): """ Setting and initialization of all environments' modules. @@ -202,39 +216,57 @@ def __init__(self, physical_system, reference_generator, reward_function, visual **kwargs: Arguments to be passed to the modules. """ self._physical_system = instantiate(PhysicalSystem, physical_system, **kwargs) - self._reference_generator = instantiate(ReferenceGenerator, reference_generator, **kwargs) + self._reference_generator = instantiate( + ReferenceGenerator, reference_generator, **kwargs + ) self._reward_function = instantiate(RewardFunction, reward_function, **kwargs) - if type(visualization) is str or isinstance(visualization, ElectricMotorVisualization): + if type(visualization) is str or isinstance( + visualization, ElectricMotorVisualization + ): visualization = [visualization] if visualization is None: visualization = [] visualizations = list(visualization) - self._visualizations = [instantiate(ElectricMotorVisualization, visu, **kwargs) for visu in visualizations] + self._visualizations = [ + instantiate(ElectricMotorVisualization, visu, **kwargs) + for visu in visualizations + ] if isinstance(constraints, ConstraintMonitor): cm = constraints else: - limit_constraints = [constraint for constraint in constraints if type(constraint) is str] - additional_constraints = [constraint for constraint in constraints if isinstance(constraint, Constraint)] + limit_constraints = [ + constraint for constraint in constraints if type(constraint) is str + ] + additional_constraints = [ + constraint + for constraint in constraints + if isinstance(constraint, Constraint) + ] cm = ConstraintMonitor(limit_constraints, additional_constraints) self._constraint_monitor = cm # Announcement of the modules among each other for physical_system_wrapper in physical_system_wrappers: - self._physical_system = physical_system_wrapper.set_physical_system(self._physical_system) + self._physical_system = physical_system_wrapper.set_physical_system( + self._physical_system + ) self._reference_generator.set_modules(self.physical_system) self._constraint_monitor.set_modules(self.physical_system) - self._reward_function.set_modules(self.physical_system, self._reference_generator, self._constraint_monitor) + self._reward_function.set_modules( + self.physical_system, self._reference_generator, self._constraint_monitor + ) # Initialization of the state filter and the spaces state_filter = state_filter or self._physical_system.state_names - self.state_filter = [self._physical_system.state_names.index(s) for s in state_filter] + self.state_filter = [ + self._physical_system.state_names.index(s) for s in state_filter + ] states_low = self._physical_system.state_space.low[self.state_filter] states_high = self._physical_system.state_space.high[self.state_filter] state_space = Box(states_low, states_high, dtype=np.float64) - self.observation_space = gymnasium.spaces.Tuple(( - state_space, - self._reference_generator.reference_space - )) + self.observation_space = gymnasium.spaces.Tuple( + (state_space, self._reference_generator.reference_space) + ) self.action_space = self.physical_system.action_space self.reward_range = self._reward_function.reward_range # new API splits done into two attributes @@ -244,12 +276,12 @@ def __init__(self, physical_system, reference_generator, reward_function, visual # Set render mode and metadata assert render_mode in self.metadata["render_modes"] self.render_mode = render_mode - + self.scale_plots = scale_plots self._callbacks = list(callbacks) self._callbacks += list(self._visualizations) - self._call_callbacks('set_env', self) + self._call_callbacks("set_env", self) def make(env_id, *args, **kwargs): env = gymnasium.make(env_id, *args, **kwargs) @@ -265,23 +297,23 @@ def _call_callbacks(self, func_name, *args): for callback in self._callbacks: func = getattr(callback, func_name) func(*args) - - def reset(self, seed = None,*_, **__): + + def reset(self, seed=None, *_, **__): """ Reset of the environment and all its modules to an initial state. Returns: The initial observation consisting of the initial state and initial reference. - info(dict): Auxiliary information (optional) + info(dict): Auxiliary information (optional) """ self._seed(seed) - self._call_callbacks('on_reset_begin') + self._call_callbacks("on_reset_begin") self._terminated = False state = self._physical_system.reset() reference, next_ref, _ = self.reference_generator.reset(state) self._reward_function.reset(state, reference) - self._call_callbacks('on_reset_end', state, reference) + self._call_callbacks("on_reset_end", state, reference) observation = (state[self.state_filter], next_ref) info = {} @@ -304,11 +336,13 @@ def step(self, action): observation(Tuple(ndarray(float),ndarray(float)): Tuple of the new state and the next reference. reward(float): Amount of reward received for the last step. terminated(bool): Flag, indicating if a reset is required before new steps can be taken. - info(dict): Auxiliary information (optional) + info(dict): Auxiliary information (optional) """ - assert not self._terminated, 'A reset is required before the environment can perform further steps' - self._call_callbacks('on_step_begin', self.physical_system.k, action) + assert ( + not self._terminated + ), "A reset is required before the environment can perform further steps" + self._call_callbacks("on_step_begin", self.physical_system.k, action) state = self._physical_system.simulate(action) reference = self.reference_generator.get_reference(state) violation_degree = self._constraint_monitor.check_constraints(state) @@ -318,32 +352,40 @@ def step(self, action): self._terminated = violation_degree >= 1.0 ref_next = self.reference_generator.get_reference_observation(state) self._call_callbacks( - 'on_step_end', self.physical_system.k, state, reference, reward, self._terminated + "on_step_end", + self.physical_system.k, + state, + reference, + reward, + self._terminated, ) # Call render code if self.render_mode == "figure": self.render() - + info = {} - return (state[self.state_filter], ref_next), reward, self._terminated, self._truncated, info - + return ( + state[self.state_filter], + ref_next, + ), reward, self._terminated, self._truncated, info + def _seed(self, seed=None): sg = np.random.SeedSequence(seed) components = [ self._physical_system, self._reference_generator, self._reward_function, - self._constraint_monitor + self._constraint_monitor, ] + list(self._callbacks) sub_sg = sg.spawn(len(components)) for sub, rc in zip(sub_sg, components): if isinstance(rc, gem.RandomComponent): rc.seed(sub) return [sg.entropy] - + def save_fig(self, figure, filetype="png"): - """ Save figure with timestamped as filename """ + """Save figure with timestamped as filename""" # create output folder if it not exists output_folder_name = "plots" if not os.path.exists(output_folder_name): @@ -356,16 +398,13 @@ def save_fig(self, figure, filetype="png"): figure.savefig(filename, dpi=300) def rendering_on_close(self): - # Figure Mode if self.render_mode and self.render_mode.startswith("figure"): - # Academic Mode (latex font) if self.render_mode == "figure_academic": - matplotlib.rcParams.update({ - "text.usetex": True, - "font.family": "Helvetica" - }) + matplotlib.rcParams.update( + {"text.usetex": True, "font.family": "Helvetica"} + ) self.render() @@ -379,26 +418,24 @@ def rendering_on_close(self): # Blocking plot call to still interactive with it if self.metadata["hold_figure_on_close"]: matplotlib.pyplot.show(block=True) - - def close(self): """Called when the environment is deleted. Closes all its modules.""" - self._call_callbacks('on_close') + self._call_callbacks("on_close") self._reward_function.close() self._physical_system.close() self._reference_generator.close() self.rendering_on_close() - def figure(self) -> Figure: - """ Get main figure (MotorDashboard) """ + """Get main figure (MotorDashboard)""" assert len(self._visualizations) == 1 motor_dashboard = self._visualizations[0] assert len(motor_dashboard._figures) == 1 return motor_dashboard._figures[0] + class ReferenceGenerator: """The abstract base class for reference generators in gym electric motor environments. @@ -419,7 +456,7 @@ class ReferenceGenerator: Call of get_reference_observation(): Returns the reference observation, which is shown to the agent. - Any shape and content is generally valid, however, values must be within the declared reference space. + Any shape and content is generally valid, however, values must be within the declared reference space. For example, the reference observation may contain future reference values of the next ``n`` steps. Example: @@ -505,7 +542,9 @@ def reset(self, initial_state=None, initial_reference=None): trajectories(dict(list(float)): If available, \ generated trajectories for the Visualization can be passed here. Otherwise return None. \ """ - return self.get_reference(initial_state), self.get_reference_observation(initial_state), None + return self.get_reference(initial_state), self.get_reference_observation( + initial_state + ), None def close(self): """Called by the environment, when the environment is deleted to close files, store logs, etc.""" @@ -596,7 +635,7 @@ class PhysicalSystem: @property def unwrapped(self): """Returns this instance of the physical system. - + If the system is wrapped into multiple PhysicalSystemWrappers this property returns directly the innermost system.""" return self @@ -676,7 +715,9 @@ def __init__(self, action_space, state_space, state_names, tau): self._action_space = action_space self._state_space = state_space self._state_names = state_names - self._state_positions = {key: index for index, key in enumerate(self._state_names)} + self._state_positions = { + key: index for index, key in enumerate(self._state_names) + } self._tau = tau self._k = 0 @@ -785,7 +826,9 @@ def constraints(self): """Returns the list of all constraints the ConstraintMonitor observes.""" return self._constraints - def __init__(self, limit_constraints=(), additional_constraints=(), merge_violations='max'): + def __init__( + self, limit_constraints=(), additional_constraints=(), merge_violations="max" + ): """ Args: limit_constraints(list(str)/'all_states'): @@ -810,16 +853,18 @@ def __init__(self, limit_constraints=(), additional_constraints=(), merge_viola self._constraints.append(LimitConstraint(limit_constraints)) assert all(callable(constraint) for constraint in self._constraints) - assert merge_violations in ['max', 'product'] or callable(merge_violations) + assert merge_violations in ["max", "product"] or callable(merge_violations) if len(self._constraints) == 0: # Without any constraint, always return 0.0 as violation self._merge_violations = lambda *violation_degrees: 0.0 - elif merge_violations == 'max': + elif merge_violations == "max": self._merge_violations = max - elif merge_violations == 'product': + elif merge_violations == "product": + def product_merge(*violation_degrees): return 1 - np.prod([(1 - violation) for violation in violation_degrees]) + self._merge_violations = product_merge elif callable(merge_violations): self._merge_violations = merge_violations diff --git a/gym_electric_motor/envs/__init__.py b/gym_electric_motor/envs/__init__.py index a7b5f921..e4dbb3dc 100644 --- a/gym_electric_motor/envs/__init__.py +++ b/gym_electric_motor/envs/__init__.py @@ -4,7 +4,9 @@ from .gym_dcm.permex_dc_motor_env import ContTorqueControlDcPermanentlyExcitedMotorEnv from .gym_dcm.permex_dc_motor_env import FiniteTorqueControlDcPermanentlyExcitedMotorEnv from .gym_dcm.permex_dc_motor_env import ContCurrentControlDcPermanentlyExcitedMotorEnv -from .gym_dcm.permex_dc_motor_env import FiniteCurrentControlDcPermanentlyExcitedMotorEnv +from .gym_dcm.permex_dc_motor_env import ( + FiniteCurrentControlDcPermanentlyExcitedMotorEnv, +) from .gym_dcm.extex_dc_motor_env import ContSpeedControlDcExternallyExcitedMotorEnv from .gym_dcm.extex_dc_motor_env import FiniteSpeedControlDcExternallyExcitedMotorEnv @@ -27,23 +29,53 @@ from .gym_dcm.shunt_dc_motor_env import ContCurrentControlDcShuntMotorEnv from .gym_dcm.shunt_dc_motor_env import FiniteCurrentControlDcShuntMotorEnv -from .gym_pmsm.finite_sc_pmsm_env import FiniteSpeedControlPermanentMagnetSynchronousMotorEnv -from .gym_pmsm.finite_cc_pmsm_env import FiniteCurrentControlPermanentMagnetSynchronousMotorEnv -from .gym_pmsm.finite_tc_pmsm_env import FiniteTorqueControlPermanentMagnetSynchronousMotorEnv -from .gym_pmsm.cont_cc_pmsm_env import ContCurrentControlPermanentMagnetSynchronousMotorEnv -from .gym_pmsm.cont_sc_pmsm_env import ContSpeedControlPermanentMagnetSynchronousMotorEnv -from .gym_pmsm.cont_tc_pmsm_env import ContTorqueControlPermanentMagnetSynchronousMotorEnv - -from .gym_eesm.finite_sc_eesm_env import FiniteSpeedControlExternallyExcitedSynchronousMotorEnv -from .gym_eesm.finite_cc_eesm_env import FiniteCurrentControlExternallyExcitedSynchronousMotorEnv -from .gym_eesm.finite_tc_eesm_env import FiniteTorqueControlExternallyExcitedSynchronousMotorEnv -from .gym_eesm.cont_cc_eesm_env import ContCurrentControlExternallyExcitedSynchronousMotorEnv -from .gym_eesm.cont_sc_eesm_env import ContSpeedControlExternallyExcitedSynchronousMotorEnv -from .gym_eesm.cont_tc_eesm_env import ContTorqueControlExternallyExcitedSynchronousMotorEnv - -from .gym_synrm.finite_sc_synrm_env import FiniteSpeedControlSynchronousReluctanceMotorEnv -from .gym_synrm.finite_cc_synrm_env import FiniteCurrentControlSynchronousReluctanceMotorEnv -from .gym_synrm.finite_tc_synrm_env import FiniteTorqueControlSynchronousReluctanceMotorEnv +from .gym_pmsm.finite_sc_pmsm_env import ( + FiniteSpeedControlPermanentMagnetSynchronousMotorEnv, +) +from .gym_pmsm.finite_cc_pmsm_env import ( + FiniteCurrentControlPermanentMagnetSynchronousMotorEnv, +) +from .gym_pmsm.finite_tc_pmsm_env import ( + FiniteTorqueControlPermanentMagnetSynchronousMotorEnv, +) +from .gym_pmsm.cont_cc_pmsm_env import ( + ContCurrentControlPermanentMagnetSynchronousMotorEnv, +) +from .gym_pmsm.cont_sc_pmsm_env import ( + ContSpeedControlPermanentMagnetSynchronousMotorEnv, +) +from .gym_pmsm.cont_tc_pmsm_env import ( + ContTorqueControlPermanentMagnetSynchronousMotorEnv, +) + +from .gym_eesm.finite_sc_eesm_env import ( + FiniteSpeedControlExternallyExcitedSynchronousMotorEnv, +) +from .gym_eesm.finite_cc_eesm_env import ( + FiniteCurrentControlExternallyExcitedSynchronousMotorEnv, +) +from .gym_eesm.finite_tc_eesm_env import ( + FiniteTorqueControlExternallyExcitedSynchronousMotorEnv, +) +from .gym_eesm.cont_cc_eesm_env import ( + ContCurrentControlExternallyExcitedSynchronousMotorEnv, +) +from .gym_eesm.cont_sc_eesm_env import ( + ContSpeedControlExternallyExcitedSynchronousMotorEnv, +) +from .gym_eesm.cont_tc_eesm_env import ( + ContTorqueControlExternallyExcitedSynchronousMotorEnv, +) + +from .gym_synrm.finite_sc_synrm_env import ( + FiniteSpeedControlSynchronousReluctanceMotorEnv, +) +from .gym_synrm.finite_cc_synrm_env import ( + FiniteCurrentControlSynchronousReluctanceMotorEnv, +) +from .gym_synrm.finite_tc_synrm_env import ( + FiniteTorqueControlSynchronousReluctanceMotorEnv, +) from .gym_synrm.cont_tc_synrm_env import ContTorqueControlSynchronousReluctanceMotorEnv from .gym_synrm.cont_cc_synrm_env import ContCurrentControlSynchronousReluctanceMotorEnv from .gym_synrm.cont_sc_synrm_env import ContSpeedControlSynchronousReluctanceMotorEnv diff --git a/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_cc_extex_dc_env.py b/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_cc_extex_dc_env.py index d7604d9c..aff8ef97 100644 --- a/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_cc_extex_dc_env.py +++ b/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_cc_extex_dc_env.py @@ -1,8 +1,15 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.visualization import MotorDashboard -from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator, MultipleReferenceGenerator +from gym_electric_motor.reference_generators import ( + WienerProcessReferenceGenerator, + MultipleReferenceGenerator, +) from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize @@ -82,9 +89,25 @@ class ContCurrentControlDcExternallyExcitedMotorEnv(ElectricMotorEnvironment): >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=('i_a', 'i_e'), calc_jacobian=True, tau=1e-4, physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=("i_a", "i_e"), + calc_jacobian=True, + tau=1e-4, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -123,37 +146,64 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve """ default_subconverters = ( ps.ContFourQuadrantConverter(), - ps.ContFourQuadrantConverter() + ps.ContFourQuadrantConverter(), ) physical_system = DcMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) + ), converter=initialize( ps.PowerElectronicConverter, converter, ps.ContMultiConverter, - dict(subconverters=default_subconverters) + dict(subconverters=default_subconverters), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.DcExternallyExcitedMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100) ), - motor=initialize(ps.ElectricMotor, motor, ps.DcExternallyExcitedMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) sub_generators = ( - WienerProcessReferenceGenerator(reference_state='i_a'), - WienerProcessReferenceGenerator(reference_state='i_e') + WienerProcessReferenceGenerator(reference_state="i_a"), + WienerProcessReferenceGenerator(reference_state="i_e"), ) reference_generator = initialize( - ReferenceGenerator, reference_generator, MultipleReferenceGenerator, dict(sub_generators=sub_generators) + ReferenceGenerator, + reference_generator, + MultipleReferenceGenerator, + dict(sub_generators=sub_generators), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(i_a=0.5, i_e=0.5)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(i_a=0.5, i_e=0.5)), ) visualization = initialize( (ElectricMotorVisualization, list, tuple), - visualization, MotorDashboard, dict(state_plots=('i_a', 'i_e',), action_plots='all')) + visualization, + MotorDashboard, + dict( + state_plots=( + "i_a", + "i_e", + ), + action_plots="all", + ), + ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_sc_extex_dc_env.py b/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_sc_extex_dc_env.py index c1a22090..86db9baf 100644 --- a/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_sc_extex_dc_env.py +++ b/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_sc_extex_dc_env.py @@ -1,5 +1,9 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator @@ -82,9 +86,25 @@ class ContSpeedControlDcExternallyExcitedMotorEnv(ElectricMotorEnvironment): >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=('i_a', 'i_e'), calc_jacobian=True, tau=1e-4, physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=("i_a", "i_e"), + calc_jacobian=True, + tau=1e-4, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -121,32 +141,59 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve This class is then initialized with its default parameters. The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ - default_subconverters = (ps.ContFourQuadrantConverter(), ps.ContFourQuadrantConverter()) + default_subconverters = ( + ps.ContFourQuadrantConverter(), + ps.ContFourQuadrantConverter(), + ) physical_system = DcMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) + ), converter=initialize( ps.PowerElectronicConverter, converter, ps.ContMultiConverter, - dict(subconverters=default_subconverters)), - motor=initialize(ps.ElectricMotor, motor, ps.DcExternallyExcitedMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.PolynomialStaticLoad, dict( - load_parameter=dict(a=0.0, b=0.0, c=0.0, j_load=1e-4) - )), + dict(subconverters=default_subconverters), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.DcExternallyExcitedMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, + load, + ps.PolynomialStaticLoad, + dict(load_parameter=dict(a=0.0, b=0.0, c=0.0, j_load=1e-4)), + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, dict(reference_state='omega') + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="omega"), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(omega=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(omega=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, dict(state_plots=('omega',), action_plots='all')) + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("omega",), action_plots="all"), + ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_tc_extex_dc_env.py b/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_tc_extex_dc_env.py index d3f6c194..97b8e31d 100644 --- a/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_tc_extex_dc_env.py +++ b/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_tc_extex_dc_env.py @@ -1,5 +1,9 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator @@ -82,9 +86,28 @@ class ContTorqueControlDcExternallyExcitedMotorEnv(ElectricMotorEnvironment): >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=('i_a', 'i_e',), calc_jacobian=True, tau=1e-4, physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=( + "i_a", + "i_e", + ), + calc_jacobian=True, + tau=1e-4, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -121,32 +144,56 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve This class is then initialized with its default parameters. The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ - default_subconverters = (ps.ContFourQuadrantConverter(), ps.ContFourQuadrantConverter()) + default_subconverters = ( + ps.ContFourQuadrantConverter(), + ps.ContFourQuadrantConverter(), + ) physical_system = DcMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) + ), converter=initialize( ps.PowerElectronicConverter, converter, ps.ContMultiConverter, - dict(subconverters=default_subconverters) + dict(subconverters=default_subconverters), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.DcExternallyExcitedMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) ), - motor=initialize(ps.ElectricMotor, motor, ps.DcExternallyExcitedMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, dict(reference_state='torque') + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="torque"), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(torque=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(torque=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, dict(state_plots=('torque',), action_plots='all') + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("torque",), action_plots="all"), ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_cc_extex_dc_env.py b/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_cc_extex_dc_env.py index 232de8a5..8b7db04b 100644 --- a/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_cc_extex_dc_env.py +++ b/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_cc_extex_dc_env.py @@ -1,8 +1,15 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.visualization import MotorDashboard -from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator, MultipleReferenceGenerator +from gym_electric_motor.reference_generators import ( + WienerProcessReferenceGenerator, + MultipleReferenceGenerator, +) from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize @@ -82,9 +89,25 @@ class FiniteCurrentControlDcExternallyExcitedMotorEnv(ElectricMotorEnvironment): >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=('i_a', 'i_e'), calc_jacobian=True, tau=1e-5, physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=("i_a", "i_e"), + calc_jacobian=True, + tau=1e-5, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -121,36 +144,66 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve This class is then initialized with its default parameters. The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ - default_subconverters = (ps.FiniteFourQuadrantConverter(), ps.FiniteFourQuadrantConverter()) + default_subconverters = ( + ps.FiniteFourQuadrantConverter(), + ps.FiniteFourQuadrantConverter(), + ) physical_system = DcMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) + ), converter=initialize( ps.PowerElectronicConverter, converter, ps.FiniteMultiConverter, - dict(subconverters=default_subconverters) + dict(subconverters=default_subconverters), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.DcExternallyExcitedMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) ), - motor=initialize(ps.ElectricMotor, motor, ps.DcExternallyExcitedMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) sub_generators = ( - WienerProcessReferenceGenerator(reference_state='i_a'), - WienerProcessReferenceGenerator(reference_state='i_e') + WienerProcessReferenceGenerator(reference_state="i_a"), + WienerProcessReferenceGenerator(reference_state="i_e"), ) reference_generator = initialize( - ReferenceGenerator, reference_generator, MultipleReferenceGenerator, dict(sub_generators=sub_generators) + ReferenceGenerator, + reference_generator, + MultipleReferenceGenerator, + dict(sub_generators=sub_generators), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(i_a=0.5, i_e=0.5)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(i_a=0.5, i_e=0.5)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, - dict(state_plots=('i_a', 'i_e',), action_plots='all')) + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict( + state_plots=( + "i_a", + "i_e", + ), + action_plots="all", + ), + ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_sc_extex_dc_env.py b/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_sc_extex_dc_env.py index de2836a6..0c2ab760 100644 --- a/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_sc_extex_dc_env.py +++ b/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_sc_extex_dc_env.py @@ -1,5 +1,9 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator @@ -82,9 +86,25 @@ class FiniteSpeedControlDcExternallyExcitedMotorEnv(ElectricMotorEnvironment): >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=('i_a', 'i_e'), calc_jacobian=True, tau=1e-5, physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=("i_a", "i_e"), + calc_jacobian=True, + tau=1e-5, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -121,33 +141,60 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve This class is then initialized with its default parameters. The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ - default_subconverters = (ps.FiniteFourQuadrantConverter(), ps.FiniteFourQuadrantConverter()) + default_subconverters = ( + ps.FiniteFourQuadrantConverter(), + ps.FiniteFourQuadrantConverter(), + ) physical_system = DcMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) + ), converter=initialize( ps.PowerElectronicConverter, converter, ps.FiniteMultiConverter, - dict(subconverters=default_subconverters)), - motor=initialize(ps.ElectricMotor, motor, ps.DcExternallyExcitedMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.PolynomialStaticLoad, dict( - load_parameter=dict(a=0.0, b=0.0, c=0.0, j_load=1e-4) - )), + dict(subconverters=default_subconverters), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.DcExternallyExcitedMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, + load, + ps.PolynomialStaticLoad, + dict(load_parameter=dict(a=0.0, b=0.0, c=0.0, j_load=1e-4)), + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, dict(reference_state='omega') + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="omega"), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(omega=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(omega=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, dict(state_plots=('omega',), action_plots='all')) + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("omega",), action_plots="all"), + ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_tc_extex_dc_env.py b/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_tc_extex_dc_env.py index 5a6986e1..d6be4412 100644 --- a/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_tc_extex_dc_env.py +++ b/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_tc_extex_dc_env.py @@ -1,5 +1,9 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator @@ -82,9 +86,25 @@ class FiniteTorqueControlDcExternallyExcitedMotorEnv(ElectricMotorEnvironment): >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=('i_a', 'i_e'), calc_jacobian=True, tau=1e-5,physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=("i_a", "i_e"), + calc_jacobian=True, + tau=1e-5, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -121,31 +141,57 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve This class is then initialized with its default parameters. The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ - default_subconverters = (ps.FiniteFourQuadrantConverter(), ps.FiniteFourQuadrantConverter()) + default_subconverters = ( + ps.FiniteFourQuadrantConverter(), + ps.FiniteFourQuadrantConverter(), + ) physical_system = DcMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) + ), converter=initialize( ps.PowerElectronicConverter, converter, ps.FiniteMultiConverter, - dict(subconverters=default_subconverters)), - motor=initialize(ps.ElectricMotor, motor, ps.DcExternallyExcitedMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), + dict(subconverters=default_subconverters), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.DcExternallyExcitedMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, dict(reference_state='torque') + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="torque"), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(torque=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(torque=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, dict(state_plots=('torque',), action_plots='all')) + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("torque",), action_plots="all"), + ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_cc_permex_dc_env.py b/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_cc_permex_dc_env.py index c6fe0e88..ce9186fa 100644 --- a/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_cc_permex_dc_env.py +++ b/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_cc_permex_dc_env.py @@ -1,8 +1,14 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.visualization import MotorDashboard -from gym_electric_motor.reference_generators.wiener_process_reference_generator import WienerProcessReferenceGenerator +from gym_electric_motor.reference_generators.wiener_process_reference_generator import ( + WienerProcessReferenceGenerator, +) from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize @@ -82,10 +88,24 @@ class ContCurrentControlDcPermanentlyExcitedMotorEnv(ElectricMotorEnvironment): >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ + def __init__( - self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=('i',), calc_jacobian=True, tau=1e-4, physical_system_wrappers=(), **kwargs + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=("i",), + calc_jacobian=True, + tau=1e-4, + physical_system_wrappers=(), + **kwargs, ): """ Args: @@ -124,26 +144,51 @@ def __init__( The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = DcMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.ContFourQuadrantConverter, dict()), - motor=initialize(ps.ElectricMotor, motor, ps.DcPermanentlyExcitedMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.ContFourQuadrantConverter, + dict(), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.DcPermanentlyExcitedMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, - dict(reference_state='i', sigma_range=(1e-2, 1e-1)) + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="i", sigma_range=(1e-2, 1e-1)), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(i=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(i=1.0)), ) visualization = initialize( (ElectricMotorVisualization, list, tuple), - visualization, MotorDashboard, dict(state_plots=('i',), action_plots='all')) + visualization, + MotorDashboard, + dict(state_plots=("i",), action_plots="all"), + ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_sc_permex_dc_env.py b/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_sc_permex_dc_env.py index 217297f0..7eb0b12b 100644 --- a/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_sc_permex_dc_env.py +++ b/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_sc_permex_dc_env.py @@ -1,8 +1,14 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.visualization import MotorDashboard -from gym_electric_motor.reference_generators.wiener_process_reference_generator import WienerProcessReferenceGenerator +from gym_electric_motor.reference_generators.wiener_process_reference_generator import ( + WienerProcessReferenceGenerator, +) from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize @@ -82,9 +88,25 @@ class ContSpeedControlDcPermanentlyExcitedMotorEnv(ElectricMotorEnvironment): >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=('i',), calc_jacobian=True, tau=1e-4, physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=("i",), + calc_jacobian=True, + tau=1e-4, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -122,27 +144,54 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = DcMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.ContFourQuadrantConverter, dict()), - motor=initialize(ps.ElectricMotor, motor, ps.DcPermanentlyExcitedMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.PolynomialStaticLoad, dict( - load_parameter=dict(a=0.0, b=0.0, c=0.0, j_load=1e-4) - )), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.ContFourQuadrantConverter, + dict(), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.DcPermanentlyExcitedMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, + load, + ps.PolynomialStaticLoad, + dict(load_parameter=dict(a=0.0, b=0.0, c=0.0, j_load=1e-4)), + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, - dict(reference_state='omega', sigma_range=(1e-3, 5e-2)) + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="omega", sigma_range=(1e-3, 5e-2)), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(omega=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(omega=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, dict(state_plots=('omega',), action_plots='all')) + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("omega",), action_plots="all"), + ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_tc_permex_dc_env.py b/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_tc_permex_dc_env.py index 7cba7ab8..b6681ddf 100644 --- a/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_tc_permex_dc_env.py +++ b/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_tc_permex_dc_env.py @@ -1,8 +1,14 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.visualization import MotorDashboard -from gym_electric_motor.reference_generators.wiener_process_reference_generator import WienerProcessReferenceGenerator +from gym_electric_motor.reference_generators.wiener_process_reference_generator import ( + WienerProcessReferenceGenerator, +) from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize @@ -83,9 +89,25 @@ class ContTorqueControlDcPermanentlyExcitedMotorEnv(ElectricMotorEnvironment): >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=('i',), calc_jacobian=True, tau=1e-4, physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=("i",), + calc_jacobian=True, + tau=1e-4, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -123,29 +145,51 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = DcMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.ContFourQuadrantConverter, dict()), - motor=initialize(ps.ElectricMotor, motor, ps.DcPermanentlyExcitedMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.ContFourQuadrantConverter, + dict(), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.DcPermanentlyExcitedMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, - dict(reference_state='torque', sigma_range=(1e-2, 1e-1)) + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="torque", sigma_range=(1e-2, 1e-1)), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(torque=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(torque=1.0)), ) visualization = initialize( ElectricMotorVisualization, visualization, MotorDashboard, - dict(state_plots=('torque',), action_plots='all') + dict(state_plots=("torque",), action_plots="all"), ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_cc_permex_dc_env.py b/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_cc_permex_dc_env.py index 07ee24e6..f5574764 100644 --- a/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_cc_permex_dc_env.py +++ b/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_cc_permex_dc_env.py @@ -1,8 +1,14 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.visualization import MotorDashboard -from gym_electric_motor.reference_generators.wiener_process_reference_generator import WienerProcessReferenceGenerator +from gym_electric_motor.reference_generators.wiener_process_reference_generator import ( + WienerProcessReferenceGenerator, +) from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize @@ -82,10 +88,24 @@ class FiniteCurrentControlDcPermanentlyExcitedMotorEnv(ElectricMotorEnvironment) >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ + def __init__( - self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=('i',), calc_jacobian=True, tau=1e-5, physical_system_wrappers=(), **kwargs + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=("i",), + calc_jacobian=True, + tau=1e-5, + physical_system_wrappers=(), + **kwargs, ): """ Args: @@ -124,25 +144,51 @@ def __init__( The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = DcMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.FiniteFourQuadrantConverter, dict()), - motor=initialize(ps.ElectricMotor, motor, ps.DcPermanentlyExcitedMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.FiniteFourQuadrantConverter, + dict(), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.DcPermanentlyExcitedMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, - dict(reference_state='i', sigma_range=(1e-2, 1e-1)) + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="i", sigma_range=(1e-2, 1e-1)), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(i=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(i=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, dict(state_plots=('i',), action_plots='all')) + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("i",), action_plots="all"), + ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_sc_permex_dc_env.py b/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_sc_permex_dc_env.py index 35e3dbd7..0dd43b03 100644 --- a/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_sc_permex_dc_env.py +++ b/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_sc_permex_dc_env.py @@ -1,8 +1,14 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.visualization import MotorDashboard -from gym_electric_motor.reference_generators.wiener_process_reference_generator import WienerProcessReferenceGenerator +from gym_electric_motor.reference_generators.wiener_process_reference_generator import ( + WienerProcessReferenceGenerator, +) from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize @@ -82,10 +88,24 @@ class FiniteSpeedControlDcPermanentlyExcitedMotorEnv(ElectricMotorEnvironment): >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ + def __init__( - self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=('i',), calc_jacobian=True, tau=1e-5, physical_system_wrappers=(), **kwargs + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=("i",), + calc_jacobian=True, + tau=1e-5, + physical_system_wrappers=(), + **kwargs, ): """ Args: @@ -125,27 +145,54 @@ def __init__( """ physical_system = DcMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.FiniteFourQuadrantConverter, dict()), - motor=initialize(ps.ElectricMotor, motor, ps.DcPermanentlyExcitedMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.PolynomialStaticLoad, dict( - load_parameter=dict(a=0.0, b=0.0, c=0.0, j_load=1e-3) - )), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.FiniteFourQuadrantConverter, + dict(), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.DcPermanentlyExcitedMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, + load, + ps.PolynomialStaticLoad, + dict(load_parameter=dict(a=0.0, b=0.0, c=0.0, j_load=1e-3)), + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, - dict(reference_state='omega', sigma_range=(1e-3, 5e-3)) + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="omega", sigma_range=(1e-3, 5e-3)), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(omega=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(omega=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, dict(state_plots=('omega',), action_plots='all')) + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("omega",), action_plots="all"), + ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_tc_permex_dc_env.py b/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_tc_permex_dc_env.py index 00a81039..0741d593 100644 --- a/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_tc_permex_dc_env.py +++ b/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_tc_permex_dc_env.py @@ -1,8 +1,14 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.visualization import MotorDashboard -from gym_electric_motor.reference_generators.wiener_process_reference_generator import WienerProcessReferenceGenerator +from gym_electric_motor.reference_generators.wiener_process_reference_generator import ( + WienerProcessReferenceGenerator, +) from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize @@ -82,10 +88,24 @@ class FiniteTorqueControlDcPermanentlyExcitedMotorEnv(ElectricMotorEnvironment): >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ + def __init__( - self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=('i',), calc_jacobian=True, tau=1e-5, physical_system_wrappers=(), **kwargs + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=("i",), + calc_jacobian=True, + tau=1e-5, + physical_system_wrappers=(), + **kwargs, ): """ Args: @@ -124,26 +144,51 @@ def __init__( The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = DcMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.FiniteFourQuadrantConverter, dict()), - motor=initialize(ps.ElectricMotor, motor, ps.DcPermanentlyExcitedMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.FiniteFourQuadrantConverter, + dict(), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.DcPermanentlyExcitedMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, - dict(reference_state='torque', sigma_range=(1e-2, 1e-1)) + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="torque", sigma_range=(1e-2, 1e-1)), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(torque=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(torque=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, dict(state_plots=('torque',), action_plots='all')) + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("torque",), action_plots="all"), + ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) - diff --git a/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_cc_series_dc_env.py b/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_cc_series_dc_env.py index 0d18b2ca..7d0341a6 100644 --- a/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_cc_series_dc_env.py +++ b/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_cc_series_dc_env.py @@ -1,5 +1,9 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator @@ -82,9 +86,25 @@ class ContCurrentControlDcSeriesMotorEnv(ElectricMotorEnvironment): >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=('i',), calc_jacobian=True, tau=1e-4, physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=("i",), + calc_jacobian=True, + tau=1e-4, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -123,25 +143,49 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = DcMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.ContFourQuadrantConverter, dict()), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.ContFourQuadrantConverter, + dict(), + ), motor=initialize(ps.ElectricMotor, motor, ps.DcSeriesMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100)), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100) + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, dict(reference_state='i') + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="i"), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(i=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(i=1.0)), ) visualization = initialize( (ElectricMotorVisualization, list, tuple), - visualization, MotorDashboard, dict(state_plots=('i',), action_plots='all')) + visualization, + MotorDashboard, + dict(state_plots=("i",), action_plots="all"), + ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_sc_series_dc_env.py b/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_sc_series_dc_env.py index 018757c0..6d272c3c 100644 --- a/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_sc_series_dc_env.py +++ b/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_sc_series_dc_env.py @@ -1,5 +1,9 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator @@ -10,82 +14,97 @@ class ContSpeedControlDcSeriesMotorEnv(ElectricMotorEnvironment): """ - Description: - Environment to simulate a continuous control set speed controlled series DC Motor - - Key: - ``'Cont-SC-SeriesDc-v0'`` - - Default Components: - - Supply: :py:class:`.IdealVoltageSupply` - - Converter: :py:class:`.ContinuousFourQuadrantConverter` - - Motor: :py:class:`.DcSeriesMotor` - - Load: :py:class:`.PolynomialStaticLoad` - - Ode-Solver: :py:class:`.EulerSolver` - - - Reference Generator: :py:class:`.WienerProcessReferenceGenerator` *Reference Quantity:* ``'omega'`` - - - Reward Function: :py:class:`.WeightedSumOfErrors` reward_weights: ``'omega' = 1`` - - - Visualization: :py:class:`.MotorDashboard` omega and action plots - - - Constraints: :py:class:`.LimitConstraint` on the current ``'i'`` - - State Variables: - ``['omega' , 'torque', 'i', 'u', 'u_sup']`` - - Reference Variables: - ``['omega']`` - - Control Cycle Time: - tau = 1e-4 seconds - - Observation Space: - Type: Tuple(State_Space, Reference_Space) - - State Space: - Box(low=[-1, -1, -1, -1, 0], high=[1, 1, 1, 1, 1]) - - Reference Space: - Box(low=[-1], high=[1]) - - Action Space: - Box(low=[-1],high=[1]) - - Initial State: - Zeros on all state variables. - - Example: - >>> import gym_electric_motor as gem - >>> from gym_electric_motor.reference_generators import LaplaceProcessReferenceGenerator - >>> - >>> # Select a different converter with default parameters by passing a keystring - >>> my_overridden_converter = 'Finite-2QC' - >>> - >>> # Update the default arguments to the voltage supply by passing a parameter dict - >>> my_changed_voltage_supply_args = {'u_nominal': 400.0} - >>> - >>> # Replace the reference generator by passing a new instance - >>> my_new_ref_gen_instance = LaplaceProcessReferenceGenerator( - ... reference_state='omega', - ... sigma_range=(1e-3, 1e-2) - ... ) - >>> env = gem.make( - ... 'Cont-SC-SeriesDc-v0', - ... voltage_supply=my_changed_voltage_supply_args, - ... converter=my_overridden_converter, - ... reference_generator=my_new_ref_gen_instance - ... ) - >>> terminated = True - >>> for _ in range(1000): - >>> if terminated: - >>> state, reference = env.reset() - >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) - """ + Description: + Environment to simulate a continuous control set speed controlled series DC Motor + + Key: + ``'Cont-SC-SeriesDc-v0'`` + + Default Components: + - Supply: :py:class:`.IdealVoltageSupply` + - Converter: :py:class:`.ContinuousFourQuadrantConverter` + - Motor: :py:class:`.DcSeriesMotor` + - Load: :py:class:`.PolynomialStaticLoad` + - Ode-Solver: :py:class:`.EulerSolver` + + - Reference Generator: :py:class:`.WienerProcessReferenceGenerator` *Reference Quantity:* ``'omega'`` + + - Reward Function: :py:class:`.WeightedSumOfErrors` reward_weights: ``'omega' = 1`` + + - Visualization: :py:class:`.MotorDashboard` omega and action plots + + - Constraints: :py:class:`.LimitConstraint` on the current ``'i'`` + + State Variables: + ``['omega' , 'torque', 'i', 'u', 'u_sup']`` + + Reference Variables: + ``['omega']`` + + Control Cycle Time: + tau = 1e-4 seconds + + Observation Space: + Type: Tuple(State_Space, Reference_Space) + + State Space: + Box(low=[-1, -1, -1, -1, 0], high=[1, 1, 1, 1, 1]) + + Reference Space: + Box(low=[-1], high=[1]) + + Action Space: + Box(low=[-1],high=[1]) + + Initial State: + Zeros on all state variables. + + Example: + >>> import gym_electric_motor as gem + >>> from gym_electric_motor.reference_generators import LaplaceProcessReferenceGenerator + >>> + >>> # Select a different converter with default parameters by passing a keystring + >>> my_overridden_converter = 'Finite-2QC' + >>> + >>> # Update the default arguments to the voltage supply by passing a parameter dict + >>> my_changed_voltage_supply_args = {'u_nominal': 400.0} + >>> + >>> # Replace the reference generator by passing a new instance + >>> my_new_ref_gen_instance = LaplaceProcessReferenceGenerator( + ... reference_state='omega', + ... sigma_range=(1e-3, 1e-2) + ... ) + >>> env = gem.make( + ... 'Cont-SC-SeriesDc-v0', + ... voltage_supply=my_changed_voltage_supply_args, + ... converter=my_overridden_converter, + ... reference_generator=my_new_ref_gen_instance + ... ) + >>> terminated = True + >>> for _ in range(1000): + >>> if terminated: + >>> state, reference = env.reset() + >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) + """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=('i',), calc_jacobian=True, tau=1e-4, physical_system_wrappers=(), **kwargs): + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=("i",), + calc_jacobian=True, + tau=1e-4, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -123,28 +142,52 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = DcMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.ContFourQuadrantConverter, dict()), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.ContFourQuadrantConverter, + dict(), + ), motor=initialize(ps.ElectricMotor, motor, ps.DcSeriesMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.PolynomialStaticLoad, dict( - load_parameter=dict(a=0.01, b=0.05, c=0.0, j_load=1e-4) - )), + load=initialize( + ps.MechanicalLoad, + load, + ps.PolynomialStaticLoad, + dict(load_parameter=dict(a=0.01, b=0.05, c=0.0, j_load=1e-4)), + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, - dict(reference_state='omega', sigma_range=(1e-3, 2e-2)) + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="omega", sigma_range=(1e-3, 2e-2)), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(omega=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(omega=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, dict(state_plots=('omega',), action_plots='all')) + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("omega",), action_plots="all"), + ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) - diff --git a/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_tc_series_dc_env.py b/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_tc_series_dc_env.py index 0fbb1a58..d89173a8 100644 --- a/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_tc_series_dc_env.py +++ b/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_tc_series_dc_env.py @@ -1,5 +1,9 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator @@ -10,82 +14,97 @@ class ContTorqueControlDcSeriesMotorEnv(ElectricMotorEnvironment): """ - Description: - Environment to simulate a continuous control set torque controlled series DC Motor - - Key: - ``'Cont-TC-SeriesDc-v0'`` - - Default Components: - - Supply: :py:class:`.IdealVoltageSupply` - - Converter: :py:class:`.ContinuousFourQuadrantConverter` - - Motor: :py:class:`.DcSeriesMotor` - - Load: :py:class:`.ConstantSpeedLoad` - - Ode-Solver: :py:class:`.EulerSolver` - - - Reference Generator: :py:class:`.WienerProcessReferenceGenerator` *Reference Quantity:* ``'torque'`` - - - Reward Function: :py:class:`.WeightedSumOfErrors` reward_weights: ``'torque' = 1`` - - - Visualization: :py:class:`.MotorDashboard` torque and action plots - - - Constraints: :py:class:`.LimitConstraint` on the current ``'i'`` - - State Variables: - ``['omega' , 'torque', 'i', 'u', 'u_sup']`` - - Reference Variables: - ``['torque']`` - - Control Cycle Time: - tau = 1e-4 seconds - - Observation Space: - Type: Tuple(State_Space, Reference_Space) - - State Space: - Box(low=[-1, -1, -1, -1, 0], high=[1, 1, 1, 1, 1]) - - Reference Space: - Box(low=[-1], high=[1]) - - Action Space: - Box(low=[-1],high=[1]) - - Initial State: - Zeros on all state variables. - - Example: - >>> import gym_electric_motor as gem - >>> from gym_electric_motor.reference_generators import LaplaceProcessReferenceGenerator - >>> - >>> # Select a different converter with default parameters by passing a keystring - >>> my_overridden_converter = 'Finite-2QC' - >>> - >>> # Update the default arguments to the voltage supply by passing a parameter dict - >>> my_changed_voltage_supply_args = {'u_nominal': 400.0} - >>> - >>> # Replace the reference generator by passing a new instance - >>> my_new_ref_gen_instance = LaplaceProcessReferenceGenerator( - ... reference_state='torque', - ... sigma_range=(1e-3, 1e-2) - ... ) - >>> env = gem.make( - ... 'Cont-TC-SeriesDc-v0', - ... voltage_supply=my_changed_voltage_supply_args, - ... converter=my_overridden_converter, - ... reference_generator=my_new_ref_gen_instance - ... ) - >>> terminated = True - >>> for _ in range(1000): - >>> if terminated: - >>> state, reference = env.reset() - >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) - """ + Description: + Environment to simulate a continuous control set torque controlled series DC Motor + + Key: + ``'Cont-TC-SeriesDc-v0'`` + + Default Components: + - Supply: :py:class:`.IdealVoltageSupply` + - Converter: :py:class:`.ContinuousFourQuadrantConverter` + - Motor: :py:class:`.DcSeriesMotor` + - Load: :py:class:`.ConstantSpeedLoad` + - Ode-Solver: :py:class:`.EulerSolver` + + - Reference Generator: :py:class:`.WienerProcessReferenceGenerator` *Reference Quantity:* ``'torque'`` + + - Reward Function: :py:class:`.WeightedSumOfErrors` reward_weights: ``'torque' = 1`` + + - Visualization: :py:class:`.MotorDashboard` torque and action plots + + - Constraints: :py:class:`.LimitConstraint` on the current ``'i'`` + + State Variables: + ``['omega' , 'torque', 'i', 'u', 'u_sup']`` + + Reference Variables: + ``['torque']`` + + Control Cycle Time: + tau = 1e-4 seconds + + Observation Space: + Type: Tuple(State_Space, Reference_Space) + + State Space: + Box(low=[-1, -1, -1, -1, 0], high=[1, 1, 1, 1, 1]) + + Reference Space: + Box(low=[-1], high=[1]) + + Action Space: + Box(low=[-1],high=[1]) + + Initial State: + Zeros on all state variables. + + Example: + >>> import gym_electric_motor as gem + >>> from gym_electric_motor.reference_generators import LaplaceProcessReferenceGenerator + >>> + >>> # Select a different converter with default parameters by passing a keystring + >>> my_overridden_converter = 'Finite-2QC' + >>> + >>> # Update the default arguments to the voltage supply by passing a parameter dict + >>> my_changed_voltage_supply_args = {'u_nominal': 400.0} + >>> + >>> # Replace the reference generator by passing a new instance + >>> my_new_ref_gen_instance = LaplaceProcessReferenceGenerator( + ... reference_state='torque', + ... sigma_range=(1e-3, 1e-2) + ... ) + >>> env = gem.make( + ... 'Cont-TC-SeriesDc-v0', + ... voltage_supply=my_changed_voltage_supply_args, + ... converter=my_overridden_converter, + ... reference_generator=my_new_ref_gen_instance + ... ) + >>> terminated = True + >>> for _ in range(1000): + >>> if terminated: + >>> state, reference = env.reset() + >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) + """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=('i',), calc_jacobian=True, tau=1e-4, physical_system_wrappers=(), **kwargs): + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=("i",), + calc_jacobian=True, + tau=1e-4, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -123,24 +142,49 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = DcMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.ContFourQuadrantConverter, dict()), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.ContFourQuadrantConverter, + dict(), + ), motor=initialize(ps.ElectricMotor, motor, ps.DcSeriesMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, dict(reference_state='torque') + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="torque"), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(torque=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(torque=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, dict(state_plots=('torque',), action_plots='all')) + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("torque",), action_plots="all"), + ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_cc_series_dc_env.py b/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_cc_series_dc_env.py index 57ef5a8c..4b926a01 100644 --- a/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_cc_series_dc_env.py +++ b/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_cc_series_dc_env.py @@ -1,5 +1,9 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator @@ -82,9 +86,25 @@ class FiniteCurrentControlDcSeriesMotorEnv(ElectricMotorEnvironment): >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=('i',), calc_jacobian=True, tau=1e-5, physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=("i",), + calc_jacobian=True, + tau=1e-5, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -122,24 +142,49 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = DcMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.FiniteFourQuadrantConverter, dict()), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.FiniteFourQuadrantConverter, + dict(), + ), motor=initialize(ps.ElectricMotor, motor, ps.DcSeriesMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, dict(reference_state='i') + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="i"), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(i=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(i=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, dict(state_plots=('i',), action_plots='all')) + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("i",), action_plots="all"), + ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_sc_series_dc_env.py b/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_sc_series_dc_env.py index e6cf017b..f100534c 100644 --- a/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_sc_series_dc_env.py +++ b/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_sc_series_dc_env.py @@ -1,5 +1,9 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator @@ -82,9 +86,25 @@ class FiniteSpeedControlDcSeriesMotorEnv(ElectricMotorEnvironment): >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=('i',), calc_jacobian=True, tau=1e-5, physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=("i",), + calc_jacobian=True, + tau=1e-5, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -122,28 +142,52 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = DcMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.FiniteFourQuadrantConverter, dict()), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.FiniteFourQuadrantConverter, + dict(), + ), motor=initialize(ps.ElectricMotor, motor, ps.DcSeriesMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.PolynomialStaticLoad, dict( - load_parameter=dict(a=0.15, b=0.05, c=0.0, j_load=1e-4) - )), + load=initialize( + ps.MechanicalLoad, + load, + ps.PolynomialStaticLoad, + dict(load_parameter=dict(a=0.15, b=0.05, c=0.0, j_load=1e-4)), + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, - dict(reference_state='omega', sigma_range=(1e-3, 5e-3)) + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="omega", sigma_range=(1e-3, 5e-3)), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(omega=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(omega=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, dict(state_plots=('omega',), action_plots='all')) + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("omega",), action_plots="all"), + ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) - diff --git a/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_tc_series_dc_env.py b/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_tc_series_dc_env.py index 930cb898..b7beac0f 100644 --- a/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_tc_series_dc_env.py +++ b/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_tc_series_dc_env.py @@ -1,5 +1,9 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator @@ -82,9 +86,25 @@ class FiniteTorqueControlDcSeriesMotorEnv(ElectricMotorEnvironment): >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=('i',), calc_jacobian=True, tau=1e-5, physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=("i",), + calc_jacobian=True, + tau=1e-5, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -123,25 +143,49 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve """ physical_system = DcMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.FiniteFourQuadrantConverter, dict()), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.FiniteFourQuadrantConverter, + dict(), + ), motor=initialize(ps.ElectricMotor, motor, ps.DcSeriesMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, dict(reference_state='torque') + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="torque"), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(torque=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(torque=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, dict(state_plots=('torque',), action_plots='all') + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("torque",), action_plots="all"), ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_cc_shunt_dc_env.py b/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_cc_shunt_dc_env.py index f868e0c4..592c450e 100644 --- a/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_cc_shunt_dc_env.py +++ b/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_cc_shunt_dc_env.py @@ -1,5 +1,9 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator @@ -11,82 +15,97 @@ class ContCurrentControlDcShuntMotorEnv(ElectricMotorEnvironment): """ - Description: - Environment to simulate a continuous control set current controlled shunt DC Motor - - Key: - ``'Cont-CC-ShuntDc-v0'`` - - Default Components: - - Supply: :py:class:`.IdealVoltageSupply` - - Converter: :py:class:`.ContinuousFourQuadrantConverter` - - Motor: :py:class:`.DcShuntMotor` - - Load: :py:class:`.ConstantSpeedLoad` - - Ode-Solver: :py:class:`.EulerSolver` - - - Reference Generator: :py:class:`.WienerProcessReferenceGenerator` *Reference Quantity:* ``'i_a'`` - - - Reward Function: :py:class:`.WeightedSumOfErrors` reward_weights: ``'i_a' = 1`` - - - Visualization: :py:class:`.MotorDashboard` torque and action plots - - - Constraints: :py:class:`.LimitConstraint` on the currents ``'i_a', 'i_e'`` - - State Variables: - ``['omega' , 'torque', 'i_a', 'i_e', 'u', 'u_sup']`` - - Reference Variables: - ``['torque']`` - - Control Cycle Time: - tau = 1e-4 seconds - - Observation Space: - Type: Tuple(State_Space, Reference_Space) - - State Space: - Box(low=[-1, -1, -1, -1, 0], high=[1, 1, 1, 1, 1]) - - Reference Space: - Box(low=[-1], high=[1]) - - Action Space: - Box(low=[-1],high=[1]) - - Initial State: - Zeros on all state variables. - - Example: - >>> import gym_electric_motor as gem - >>> from gym_electric_motor.reference_generators import LaplaceProcessReferenceGenerator - >>> - >>> # Select a different converter with default parameters by passing a keystring - >>> my_overridden_converter = 'Finite-2QC' - >>> - >>> # Update the default arguments to the voltage supply by passing a parameter dict - >>> my_changed_voltage_supply_args = {'u_nominal': 400.0} - >>> - >>> # Replace the reference generator by passing a new instance - >>> my_new_ref_gen_instance = LaplaceProcessReferenceGenerator( - ... reference_state='i_a', - ... sigma_range=(1e-3, 1e-2) - ... ) - >>> env = gem.make( - ... 'Cont-CC-ShuntDc-v0', - ... voltage_supply=my_changed_voltage_supply_args, - ... converter=my_overridden_converter, - ... reference_generator=my_new_ref_gen_instance - ... ) - >>> terminated = True - >>> for _ in range(1000): - >>> if terminated: - >>> state, reference = env.reset() - >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) - """ + Description: + Environment to simulate a continuous control set current controlled shunt DC Motor + + Key: + ``'Cont-CC-ShuntDc-v0'`` + + Default Components: + - Supply: :py:class:`.IdealVoltageSupply` + - Converter: :py:class:`.ContinuousFourQuadrantConverter` + - Motor: :py:class:`.DcShuntMotor` + - Load: :py:class:`.ConstantSpeedLoad` + - Ode-Solver: :py:class:`.EulerSolver` + + - Reference Generator: :py:class:`.WienerProcessReferenceGenerator` *Reference Quantity:* ``'i_a'`` + + - Reward Function: :py:class:`.WeightedSumOfErrors` reward_weights: ``'i_a' = 1`` + + - Visualization: :py:class:`.MotorDashboard` torque and action plots + + - Constraints: :py:class:`.LimitConstraint` on the currents ``'i_a', 'i_e'`` + + State Variables: + ``['omega' , 'torque', 'i_a', 'i_e', 'u', 'u_sup']`` + + Reference Variables: + ``['torque']`` + + Control Cycle Time: + tau = 1e-4 seconds + + Observation Space: + Type: Tuple(State_Space, Reference_Space) + + State Space: + Box(low=[-1, -1, -1, -1, 0], high=[1, 1, 1, 1, 1]) + + Reference Space: + Box(low=[-1], high=[1]) + + Action Space: + Box(low=[-1],high=[1]) + + Initial State: + Zeros on all state variables. + + Example: + >>> import gym_electric_motor as gem + >>> from gym_electric_motor.reference_generators import LaplaceProcessReferenceGenerator + >>> + >>> # Select a different converter with default parameters by passing a keystring + >>> my_overridden_converter = 'Finite-2QC' + >>> + >>> # Update the default arguments to the voltage supply by passing a parameter dict + >>> my_changed_voltage_supply_args = {'u_nominal': 400.0} + >>> + >>> # Replace the reference generator by passing a new instance + >>> my_new_ref_gen_instance = LaplaceProcessReferenceGenerator( + ... reference_state='i_a', + ... sigma_range=(1e-3, 1e-2) + ... ) + >>> env = gem.make( + ... 'Cont-CC-ShuntDc-v0', + ... voltage_supply=my_changed_voltage_supply_args, + ... converter=my_overridden_converter, + ... reference_generator=my_new_ref_gen_instance + ... ) + >>> terminated = True + >>> for _ in range(1000): + >>> if terminated: + >>> state, reference = env.reset() + >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) + """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=('i_a', 'i_e'), calc_jacobian=True, tau=1e-4, physical_system_wrappers=(), **kwargs): + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=("i_a", "i_e"), + calc_jacobian=True, + tau=1e-4, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -125,26 +144,51 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve """ physical_system = DcMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.ContFourQuadrantConverter, dict()), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.ContFourQuadrantConverter, + dict(), + ), motor=initialize(ps.ElectricMotor, motor, ps.DcShuntMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100)), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100) + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, dict(reference_state='i_a') + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="i_a"), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(i_a=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(i_a=1.0)), ) visualization = initialize( (ElectricMotorVisualization, list, tuple), - visualization, MotorDashboard, dict(state_plots=('i_a',), action_plots='all')) + visualization, + MotorDashboard, + dict(state_plots=("i_a",), action_plots="all"), + ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=tuple(physical_system_wrappers) + (CurrentSumProcessor(('i_a', 'i_e')),), **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=tuple(physical_system_wrappers) + + (CurrentSumProcessor(("i_a", "i_e")),), + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_sc_shunt_dc_env.py b/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_sc_shunt_dc_env.py index 826b67a4..5b639d2e 100644 --- a/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_sc_shunt_dc_env.py +++ b/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_sc_shunt_dc_env.py @@ -1,5 +1,9 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.physical_system_wrappers import CurrentSumProcessor from gym_electric_motor.visualization import MotorDashboard @@ -11,82 +15,97 @@ class ContSpeedControlDcShuntMotorEnv(ElectricMotorEnvironment): """ - Description: - Environment to simulate a continuous control set speed controlled shunt DC Motor - - Key: - ``'Cont-SC-ShuntDc-v0'`` - - Default Components: - - Supply: :py:class:`.IdealVoltageSupply` - - Converter: :py:class:`.ContinuousFourQuadrantConverter` - - Motor: :py:class:`.DcShuntMotor` - - Load: :py:class:`.PolynomialStaticLoad` - - Ode-Solver: :py:class:`.EulerSolver` - - - Reference Generator: :py:class:`.WienerProcessReferenceGenerator` *Reference Quantity:* ``'omega'`` - - - Reward Function: :py:class:`.WeightedSumOfErrors` reward_weights: ``'omega' = 1`` - - - Visualization: :py:class:`.MotorDashboard` omega and action plots - - - Constraints: :py:class:`.LimitConstraint` on the currents ``'i_a', 'i_e'`` - - State Variables: - ``['omega' , 'torque', 'i_a', 'i_e', 'u', 'u_sup']`` - - Reference Variables: - ``['omega']`` - - Control Cycle Time: - tau = 1e-4 seconds - - Observation Space: - Type: Tuple(State_Space, Reference_Space) - - State Space: - Box(low=[-1, -1, -1, -1, -1, 0], high=[1, 1, 1, 1, 1, 1]) - - Reference Space: - Box(low=[-1], high=[1]) - - Action Space: - Box(low=[-1],high=[1]) - - Initial State: - Zeros on all state variables. - - Example: - >>> import gym_electric_motor as gem - >>> from gym_electric_motor.reference_generators import LaplaceProcessReferenceGenerator - >>> - >>> # Select a different converter with default parameters by passing a keystring - >>> my_overridden_converter = 'Cont-2QC' - >>> - >>> # Update the default arguments to the voltage supply by passing a parameter dict - >>> my_changed_voltage_supply_args = {'u_nominal': 400.0} - >>> - >>> # Replace the reference generator by passing a new instance - >>> my_new_ref_gen_instance = LaplaceProcessReferenceGenerator( - ... reference_state='i_a', - ... sigma_range=(1e-3, 1e-2) - ... ) - >>> env = gem.make( - ... 'Cont-SC-ShuntDc-v0', - ... voltage_supply=my_changed_voltage_supply_args, - ... converter=my_overridden_converter, - ... reference_generator=my_new_ref_gen_instance - ... ) - >>> terminated = True - >>> for _ in range(1000): - >>> if terminated: - >>> state, reference = env.reset() - >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) - """ + Description: + Environment to simulate a continuous control set speed controlled shunt DC Motor + + Key: + ``'Cont-SC-ShuntDc-v0'`` + + Default Components: + - Supply: :py:class:`.IdealVoltageSupply` + - Converter: :py:class:`.ContinuousFourQuadrantConverter` + - Motor: :py:class:`.DcShuntMotor` + - Load: :py:class:`.PolynomialStaticLoad` + - Ode-Solver: :py:class:`.EulerSolver` + + - Reference Generator: :py:class:`.WienerProcessReferenceGenerator` *Reference Quantity:* ``'omega'`` + + - Reward Function: :py:class:`.WeightedSumOfErrors` reward_weights: ``'omega' = 1`` + + - Visualization: :py:class:`.MotorDashboard` omega and action plots + + - Constraints: :py:class:`.LimitConstraint` on the currents ``'i_a', 'i_e'`` + + State Variables: + ``['omega' , 'torque', 'i_a', 'i_e', 'u', 'u_sup']`` + + Reference Variables: + ``['omega']`` + + Control Cycle Time: + tau = 1e-4 seconds + + Observation Space: + Type: Tuple(State_Space, Reference_Space) + + State Space: + Box(low=[-1, -1, -1, -1, -1, 0], high=[1, 1, 1, 1, 1, 1]) + + Reference Space: + Box(low=[-1], high=[1]) + + Action Space: + Box(low=[-1],high=[1]) + + Initial State: + Zeros on all state variables. + + Example: + >>> import gym_electric_motor as gem + >>> from gym_electric_motor.reference_generators import LaplaceProcessReferenceGenerator + >>> + >>> # Select a different converter with default parameters by passing a keystring + >>> my_overridden_converter = 'Cont-2QC' + >>> + >>> # Update the default arguments to the voltage supply by passing a parameter dict + >>> my_changed_voltage_supply_args = {'u_nominal': 400.0} + >>> + >>> # Replace the reference generator by passing a new instance + >>> my_new_ref_gen_instance = LaplaceProcessReferenceGenerator( + ... reference_state='i_a', + ... sigma_range=(1e-3, 1e-2) + ... ) + >>> env = gem.make( + ... 'Cont-SC-ShuntDc-v0', + ... voltage_supply=my_changed_voltage_supply_args, + ... converter=my_overridden_converter, + ... reference_generator=my_new_ref_gen_instance + ... ) + >>> terminated = True + >>> for _ in range(1000): + >>> if terminated: + >>> state, reference = env.reset() + >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) + """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=('i_a', 'i_e'), calc_jacobian=True, tau=1e-4, physical_system_wrappers=(), **kwargs): + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=("i_a", "i_e"), + calc_jacobian=True, + tau=1e-4, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -125,27 +144,53 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve """ physical_system = DcMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.ContFourQuadrantConverter, dict()), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.ContFourQuadrantConverter, + dict(), + ), motor=initialize(ps.ElectricMotor, motor, ps.DcShuntMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.PolynomialStaticLoad, dict( - load_parameter=dict(a=0.05, b=0.01, c=0.0, j_load=1e-4) - )), + load=initialize( + ps.MechanicalLoad, + load, + ps.PolynomialStaticLoad, + dict(load_parameter=dict(a=0.05, b=0.01, c=0.0, j_load=1e-4)), + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, - dict(reference_state='omega', sigma_range=(1e-3, 3e-2)) + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="omega", sigma_range=(1e-3, 3e-2)), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(omega=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(omega=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, dict(state_plots=('omega',), action_plots='all')) + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("omega",), action_plots="all"), + ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=tuple(physical_system_wrappers) + (CurrentSumProcessor(('i_a', 'i_e')),), **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=tuple(physical_system_wrappers) + + (CurrentSumProcessor(("i_a", "i_e")),), + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_tc_shunt_dc_env.py b/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_tc_shunt_dc_env.py index 698607de..4a7bb088 100644 --- a/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_tc_shunt_dc_env.py +++ b/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_tc_shunt_dc_env.py @@ -1,5 +1,9 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator @@ -11,82 +15,97 @@ class ContTorqueControlDcShuntMotorEnv(ElectricMotorEnvironment): """ - Description: - Environment to simulate a continuous control set torque controlled shunt DC Motor - - Key: - ``'Cont-TC-ShuntDc-v0'`` - - Default Components: - - Supply: :py:class:`.IdealVoltageSupply` - - Converter: :py:class:`.ContinuousFourQuadrantConverter` - - Motor: :py:class:`.DcShuntMotor` - - Load: :py:class:`.ConstantSpeedLoad` - - Ode-Solver: :py:class:`.EulerSolver` - - - Reference Generator: :py:class:`.WienerProcessReferenceGenerator` *Reference Quantity:* ``'torque'`` - - - Reward Function: :py:class:`.WeightedSumOfErrors` reward_weights: ``'torque' = 1`` - - - Visualization: :py:class:`.MotorDashboard` torque and action plots - - - Constraints: :py:class:`.LimitConstraint` on the currents ``'i_a', 'i_e'`` - - State Variables: - ``['omega' , 'torque', 'i_a', 'i_e', 'u', 'u_sup']`` - - Reference Variables: - ``['torque']`` - - Control Cycle Time: - tau = 1e-4 seconds - - Observation Space: - Type: Tuple(State_Space, Reference_Space) - - State Space: - Box(low=[-1, -1, -1, -1, 0], high=[1, 1, 1, 1, 1]) - - Reference Space: - Box(low=[-1], high=[1]) - - Action Space: - Box(low=[-1],high=[1]) - - Initial State: - Zeros on all state variables. - - Example: - >>> import gym_electric_motor as gem - >>> from gym_electric_motor.reference_generators import LaplaceProcessReferenceGenerator - >>> - >>> # Select a different converter with default parameters by passing a keystring - >>> my_overridden_converter = 'Finite-2QC' - >>> - >>> # Update the default arguments to the voltage supply by passing a parameter dict - >>> my_changed_voltage_supply_args = {'u_nominal': 400.0} - >>> - >>> # Replace the reference generator by passing a new instance - >>> my_new_ref_gen_instance = LaplaceProcessReferenceGenerator( - ... reference_state='torque', - ... sigma_range=(1e-3, 1e-2) - ... ) - >>> env = gem.make( - ... 'Cont-TC-ShuntDc-v0', - ... voltage_supply=my_changed_voltage_supply_args, - ... converter=my_overridden_converter, - ... reference_generator=my_new_ref_gen_instance - ... ) - >>> terminated = True - >>> for _ in range(1000): - >>> if terminated: - >>> state, reference = env.reset() - >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) - """ + Description: + Environment to simulate a continuous control set torque controlled shunt DC Motor + + Key: + ``'Cont-TC-ShuntDc-v0'`` + + Default Components: + - Supply: :py:class:`.IdealVoltageSupply` + - Converter: :py:class:`.ContinuousFourQuadrantConverter` + - Motor: :py:class:`.DcShuntMotor` + - Load: :py:class:`.ConstantSpeedLoad` + - Ode-Solver: :py:class:`.EulerSolver` + + - Reference Generator: :py:class:`.WienerProcessReferenceGenerator` *Reference Quantity:* ``'torque'`` + + - Reward Function: :py:class:`.WeightedSumOfErrors` reward_weights: ``'torque' = 1`` + + - Visualization: :py:class:`.MotorDashboard` torque and action plots + + - Constraints: :py:class:`.LimitConstraint` on the currents ``'i_a', 'i_e'`` + + State Variables: + ``['omega' , 'torque', 'i_a', 'i_e', 'u', 'u_sup']`` + + Reference Variables: + ``['torque']`` + + Control Cycle Time: + tau = 1e-4 seconds + + Observation Space: + Type: Tuple(State_Space, Reference_Space) + + State Space: + Box(low=[-1, -1, -1, -1, 0], high=[1, 1, 1, 1, 1]) + + Reference Space: + Box(low=[-1], high=[1]) + + Action Space: + Box(low=[-1],high=[1]) + + Initial State: + Zeros on all state variables. + + Example: + >>> import gym_electric_motor as gem + >>> from gym_electric_motor.reference_generators import LaplaceProcessReferenceGenerator + >>> + >>> # Select a different converter with default parameters by passing a keystring + >>> my_overridden_converter = 'Finite-2QC' + >>> + >>> # Update the default arguments to the voltage supply by passing a parameter dict + >>> my_changed_voltage_supply_args = {'u_nominal': 400.0} + >>> + >>> # Replace the reference generator by passing a new instance + >>> my_new_ref_gen_instance = LaplaceProcessReferenceGenerator( + ... reference_state='torque', + ... sigma_range=(1e-3, 1e-2) + ... ) + >>> env = gem.make( + ... 'Cont-TC-ShuntDc-v0', + ... voltage_supply=my_changed_voltage_supply_args, + ... converter=my_overridden_converter, + ... reference_generator=my_new_ref_gen_instance + ... ) + >>> terminated = True + >>> for _ in range(1000): + >>> if terminated: + >>> state, reference = env.reset() + >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) + """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=('i_a', 'i_e'), calc_jacobian=True, tau=1e-4, physical_system_wrappers=(), **kwargs): + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=("i_a", "i_e"), + calc_jacobian=True, + tau=1e-4, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -125,31 +144,50 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve """ physical_system = DcMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.ContFourQuadrantConverter, dict()), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.ContFourQuadrantConverter, + dict(), + ), motor=initialize(ps.ElectricMotor, motor, ps.DcShuntMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=230.0)), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=230.0) + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, dict( - reference_state='torque', limit_margin=(0, 0.8) - ), - + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="torque", limit_margin=(0, 0.8)), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(torque=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(torque=1.0)), ) visualization = initialize( ElectricMotorVisualization, visualization, MotorDashboard, - dict(state_plots=('torque',), action_plots='all') + dict(state_plots=("torque",), action_plots="all"), ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=tuple(physical_system_wrappers) + (CurrentSumProcessor(('i_a', 'i_e')),), **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=tuple(physical_system_wrappers) + + (CurrentSumProcessor(("i_a", "i_e")),), + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_cc_shunt_dc_env.py b/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_cc_shunt_dc_env.py index e49ac86b..61485e29 100644 --- a/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_cc_shunt_dc_env.py +++ b/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_cc_shunt_dc_env.py @@ -1,5 +1,9 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.physical_system_wrappers import CurrentSumProcessor from gym_electric_motor.visualization import MotorDashboard @@ -11,82 +15,97 @@ class FiniteCurrentControlDcShuntMotorEnv(ElectricMotorEnvironment): """ - Description: - Environment to simulate a finite control set current controlled shunt DC Motor - - Key: - ``'Finite-CC-ShuntDc-v0'`` - - Default Components: - - Supply: :py:class:`.IdealVoltageSupply` - - Converter: :py:class:`.FiniteFourQuadrantConverter` - - Motor: :py:class:`.DcShuntMotor` - - Load: :py:class:`.ConstantSpeedLoad` - - Ode-Solver: :py:class:`.EulerSolver` - - - Reference Generator: :py:class:`.WienerProcessReferenceGenerator` *Reference Quantity:* ``'i_a'`` - - - Reward Function: :py:class:`.WeightedSumOfErrors` reward_weights: ``'i_a' = 1`` - - - Visualization: :py:class:`.MotorDashboard` current and action plots - - - Constraints: :py:class:`.LimitConstraint` on the currents ``'i_a', 'i_e'`` - - State Variables: - ``['omega' , 'torque', 'i_a', 'i_e', 'u', 'u_sup']`` - - Reference Variables: - ``['i_a']`` - - Control Cycle Time: - tau = 1e-5 seconds - - Observation Space: - Type: Tuple(State_Space, Reference_Space) - - State Space: - Box(low=[-1, -1, -1, -1, 0], high=[1, 1, 1, 1, 1]) - - Reference Space: - Box(low=[-1], high=[1]) - - Action Space: - Discrete(4) - - Initial State: - Zeros on all state variables. - - Example: - >>> import gym_electric_motor as gem - >>> from gym_electric_motor.reference_generators import LaplaceProcessReferenceGenerator - >>> - >>> # Select a different converter with default parameters by passing a keystring - >>> my_overridden_converter = 'Finite-2QC' - >>> - >>> # Update the default arguments to the voltage supply by passing a parameter dict - >>> my_changed_voltage_supply_args = {'u_nominal': 400.0} - >>> - >>> # Replace the reference generator by passing a new instance - >>> my_new_ref_gen_instance = LaplaceProcessReferenceGenerator( - ... reference_state='i_a', - ... sigma_range=(1e-3, 1e-2) - ... ) - >>> env = gem.make( - ... 'Finite-CC-ShuntDc-v0', - ... voltage_supply=my_changed_voltage_supply_args, - ... converter=my_overridden_converter, - ... reference_generator=my_new_ref_gen_instance - ... ) - >>> terminated = True - >>> for _ in range(1000): - >>> if terminated: - >>> state, reference = env.reset() - >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) - """ + Description: + Environment to simulate a finite control set current controlled shunt DC Motor + + Key: + ``'Finite-CC-ShuntDc-v0'`` + + Default Components: + - Supply: :py:class:`.IdealVoltageSupply` + - Converter: :py:class:`.FiniteFourQuadrantConverter` + - Motor: :py:class:`.DcShuntMotor` + - Load: :py:class:`.ConstantSpeedLoad` + - Ode-Solver: :py:class:`.EulerSolver` + + - Reference Generator: :py:class:`.WienerProcessReferenceGenerator` *Reference Quantity:* ``'i_a'`` + + - Reward Function: :py:class:`.WeightedSumOfErrors` reward_weights: ``'i_a' = 1`` + + - Visualization: :py:class:`.MotorDashboard` current and action plots + + - Constraints: :py:class:`.LimitConstraint` on the currents ``'i_a', 'i_e'`` + + State Variables: + ``['omega' , 'torque', 'i_a', 'i_e', 'u', 'u_sup']`` + + Reference Variables: + ``['i_a']`` + + Control Cycle Time: + tau = 1e-5 seconds + + Observation Space: + Type: Tuple(State_Space, Reference_Space) + + State Space: + Box(low=[-1, -1, -1, -1, 0], high=[1, 1, 1, 1, 1]) + + Reference Space: + Box(low=[-1], high=[1]) + + Action Space: + Discrete(4) + + Initial State: + Zeros on all state variables. + + Example: + >>> import gym_electric_motor as gem + >>> from gym_electric_motor.reference_generators import LaplaceProcessReferenceGenerator + >>> + >>> # Select a different converter with default parameters by passing a keystring + >>> my_overridden_converter = 'Finite-2QC' + >>> + >>> # Update the default arguments to the voltage supply by passing a parameter dict + >>> my_changed_voltage_supply_args = {'u_nominal': 400.0} + >>> + >>> # Replace the reference generator by passing a new instance + >>> my_new_ref_gen_instance = LaplaceProcessReferenceGenerator( + ... reference_state='i_a', + ... sigma_range=(1e-3, 1e-2) + ... ) + >>> env = gem.make( + ... 'Finite-CC-ShuntDc-v0', + ... voltage_supply=my_changed_voltage_supply_args, + ... converter=my_overridden_converter, + ... reference_generator=my_new_ref_gen_instance + ... ) + >>> terminated = True + >>> for _ in range(1000): + >>> if terminated: + >>> state, reference = env.reset() + >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) + """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=('i_a', 'i_e'), calc_jacobian=True, tau=1e-5, physical_system_wrappers=(), **kwargs): + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=("i_a", "i_e"), + calc_jacobian=True, + tau=1e-5, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -125,25 +144,56 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve """ physical_system = DcMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.FiniteFourQuadrantConverter, dict()), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.FiniteFourQuadrantConverter, + dict(), + ), motor=initialize(ps.ElectricMotor, motor, ps.DcShuntMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, dict(reference_state='i_a') + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="i_a"), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(i_a=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(i_a=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, - dict(state_plots=('i_a', 'i_e',), action_plots='all')) + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict( + state_plots=( + "i_a", + "i_e", + ), + action_plots="all", + ), + ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=tuple(physical_system_wrappers) + (CurrentSumProcessor(('i_a', 'i_e')),), **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=tuple(physical_system_wrappers) + + (CurrentSumProcessor(("i_a", "i_e")),), + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_sc_shunt_dc_env.py b/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_sc_shunt_dc_env.py index f1b376b6..1239833c 100644 --- a/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_sc_shunt_dc_env.py +++ b/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_sc_shunt_dc_env.py @@ -1,5 +1,9 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.physical_system_wrappers import CurrentSumProcessor from gym_electric_motor.visualization import MotorDashboard @@ -11,82 +15,97 @@ class FiniteSpeedControlDcShuntMotorEnv(ElectricMotorEnvironment): """ - Description: - Environment to simulate a finite control set speed controlled shunt DC Motor - - Key: - ``'Finite-SC-ShuntDc-v0'`` - - Default Components: - - Supply: :py:class:`.IdealVoltageSupply` - - Converter: :py:class:`.FiniteFourQuadrantConverter` - - Motor: :py:class:`.DcShuntMotor` - - Load: :py:class:`.PolynomialStaticLoad` - - Ode-Solver: :py:class:`.EulerSolver` - - - Reference Generator: :py:class:`.WienerProcessReferenceGenerator` *Reference Quantity:* ``'omega'`` - - - Reward Function: :py:class:`.WeightedSumOfErrors` reward_weights: ``'omega' = 1`` - - - Visualization: :py:class:`.MotorDashboard` omega and action plots - - - Constraints: :py:class:`.LimitConstraint` on the currents ``'i_a', 'i_e'`` - - State Variables: - ``['omega' , 'torque', 'i_a', 'i_e', 'u', 'u_sup']`` - - Reference Variables: - ``['omega']`` - - Control Cycle Time: - tau = 1e-5 seconds - - Observation Space: - Type: Tuple(State_Space, Reference_Space) - - State Space: - Box(low=[-1, -1, -1, -1, 0], high=[1, 1, 1, 1, 1]) - - Reference Space: - Box(low=[-1], high=[1]) - - Action Space: - Discrete(4) - - Initial State: - Zeros on all state variables. - - Example: - >>> import gym_electric_motor as gem - >>> from gym_electric_motor.reference_generators import LaplaceProcessReferenceGenerator - >>> - >>> # Select a different converter with default parameters by passing a keystring - >>> my_overridden_converter = 'Finite-2QC' - >>> - >>> # Update the default arguments to the voltage supply by passing a parameter dict - >>> my_changed_voltage_supply_args = {'u_nominal': 400.0} - >>> - >>> # Replace the reference generator by passing a new instance - >>> my_new_ref_gen_instance = LaplaceProcessReferenceGenerator( - ... reference_state='omega', - ... sigma_range=(1e-3, 1e-2) - ... ) - >>> env = gem.make( - ... 'Finite-SC-ShuntDc-v0', - ... voltage_supply=my_changed_voltage_supply_args, - ... converter=my_overridden_converter, - ... reference_generator=my_new_ref_gen_instance - ... ) - >>> terminated = True - >>> for _ in range(1000): - >>> if terminated: - >>> state, reference = env.reset() - >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) - """ + Description: + Environment to simulate a finite control set speed controlled shunt DC Motor + + Key: + ``'Finite-SC-ShuntDc-v0'`` + + Default Components: + - Supply: :py:class:`.IdealVoltageSupply` + - Converter: :py:class:`.FiniteFourQuadrantConverter` + - Motor: :py:class:`.DcShuntMotor` + - Load: :py:class:`.PolynomialStaticLoad` + - Ode-Solver: :py:class:`.EulerSolver` + + - Reference Generator: :py:class:`.WienerProcessReferenceGenerator` *Reference Quantity:* ``'omega'`` + + - Reward Function: :py:class:`.WeightedSumOfErrors` reward_weights: ``'omega' = 1`` + + - Visualization: :py:class:`.MotorDashboard` omega and action plots + + - Constraints: :py:class:`.LimitConstraint` on the currents ``'i_a', 'i_e'`` + + State Variables: + ``['omega' , 'torque', 'i_a', 'i_e', 'u', 'u_sup']`` + + Reference Variables: + ``['omega']`` + + Control Cycle Time: + tau = 1e-5 seconds + + Observation Space: + Type: Tuple(State_Space, Reference_Space) + + State Space: + Box(low=[-1, -1, -1, -1, 0], high=[1, 1, 1, 1, 1]) + + Reference Space: + Box(low=[-1], high=[1]) + + Action Space: + Discrete(4) + + Initial State: + Zeros on all state variables. + + Example: + >>> import gym_electric_motor as gem + >>> from gym_electric_motor.reference_generators import LaplaceProcessReferenceGenerator + >>> + >>> # Select a different converter with default parameters by passing a keystring + >>> my_overridden_converter = 'Finite-2QC' + >>> + >>> # Update the default arguments to the voltage supply by passing a parameter dict + >>> my_changed_voltage_supply_args = {'u_nominal': 400.0} + >>> + >>> # Replace the reference generator by passing a new instance + >>> my_new_ref_gen_instance = LaplaceProcessReferenceGenerator( + ... reference_state='omega', + ... sigma_range=(1e-3, 1e-2) + ... ) + >>> env = gem.make( + ... 'Finite-SC-ShuntDc-v0', + ... voltage_supply=my_changed_voltage_supply_args, + ... converter=my_overridden_converter, + ... reference_generator=my_new_ref_gen_instance + ... ) + >>> terminated = True + >>> for _ in range(1000): + >>> if terminated: + >>> state, reference = env.reset() + >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) + """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=('i_a', 'i_e'), calc_jacobian=True, tau=1e-5, physical_system_wrappers=(), **kwargs): + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=("i_a", "i_e"), + calc_jacobian=True, + tau=1e-5, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -125,27 +144,53 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve """ physical_system = DcMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.FiniteFourQuadrantConverter, dict()), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.FiniteFourQuadrantConverter, + dict(), + ), motor=initialize(ps.ElectricMotor, motor, ps.DcShuntMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.PolynomialStaticLoad, dict( - load_parameter=dict(a=0.05, b=0.01, c=0.0, j_load=1e-4) - )), + load=initialize( + ps.MechanicalLoad, + load, + ps.PolynomialStaticLoad, + dict(load_parameter=dict(a=0.05, b=0.01, c=0.0, j_load=1e-4)), + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, - dict(reference_state='omega', sigma_range=(1e-3, 5e-3)) + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="omega", sigma_range=(1e-3, 5e-3)), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(omega=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(omega=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, dict(state_plots=('omega',), action_plots='all')) + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("omega",), action_plots="all"), + ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=tuple(physical_system_wrappers) + (CurrentSumProcessor(('i_a', 'i_e')),), **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=tuple(physical_system_wrappers) + + (CurrentSumProcessor(("i_a", "i_e")),), + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_tc_shunt_dc_env.py b/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_tc_shunt_dc_env.py index 03e1cee4..e9a5436b 100644 --- a/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_tc_shunt_dc_env.py +++ b/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_tc_shunt_dc_env.py @@ -1,5 +1,9 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.physical_system_wrappers import CurrentSumProcessor from gym_electric_motor.visualization import MotorDashboard @@ -11,82 +15,97 @@ class FiniteTorqueControlDcShuntMotorEnv(ElectricMotorEnvironment): """ - Description: - Environment to simulate a finite control set torque controlled shunt DC Motor - - Key: - ``'Finite-TC-ShuntDc-v0'`` - - Default Components: - - Supply: :py:class:`.IdealVoltageSupply` - - Converter: :py:class:`.FiniteFourQuadrantConverter` - - Motor: :py:class:`.DcShuntMotor` - - Load: :py:class:`.ConstantSpeedLoad` - - Ode-Solver: :py:class:`.EulerSolver` - - - Reference Generator: :py:class:`.WienerProcessReferenceGenerator` *Reference Quantity:* ``'torque'`` - - - Reward Function: :py:class:`.WeightedSumOfErrors` reward_weights: ``'torque' = 1`` - - - Visualization: :py:class:`.MotorDashboard` torque and action plots - - - Constraints: :py:class:`.LimitConstraint` on the currents ``'i_a', 'i_e'`` - - State Variables: - ``['omega' , 'torque', 'i_a', 'i_e', 'u', 'u_sup']`` - - Reference Variables: - ``['torque']`` - - Control Cycle Time: - tau = 1e-5 seconds - - Observation Space: - Type: Tuple(State_Space, Reference_Space) - - State Space: - Box(low=[-1, -1, -1, -1, 0], high=[1, 1, 1, 1, 1]) - - Reference Space: - Box(low=[-1], high=[1]) - - Action Space: - Discrete(4) - - Initial State: - Zeros on all state variables. - - Example: - >>> import gym_electric_motor as gem - >>> from gym_electric_motor.reference_generators import LaplaceProcessReferenceGenerator - >>> - >>> # Select a different converter with default parameters by passing a keystring - >>> my_overridden_converter = 'Finite-2QC' - >>> - >>> # Update the default arguments to the voltage supply by passing a parameter dict - >>> my_changed_voltage_supply_args = {'u_nominal': 400.0} - >>> - >>> # Replace the reference generator by passing a new instance - >>> my_new_ref_gen_instance = LaplaceProcessReferenceGenerator( - ... reference_state='torque', - ... sigma_range=(1e-3, 1e-2) - ... ) - >>> env = gem.make( - ... 'Finite-TC-ShuntDc-v0', - ... voltage_supply=my_changed_voltage_supply_args, - ... converter=my_overridden_converter, - ... reference_generator=my_new_ref_gen_instance - ... ) - >>> terminated = True - >>> for _ in range(1000): - >>> if terminated: - >>> state, reference = env.reset() - >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) - """ + Description: + Environment to simulate a finite control set torque controlled shunt DC Motor + + Key: + ``'Finite-TC-ShuntDc-v0'`` + + Default Components: + - Supply: :py:class:`.IdealVoltageSupply` + - Converter: :py:class:`.FiniteFourQuadrantConverter` + - Motor: :py:class:`.DcShuntMotor` + - Load: :py:class:`.ConstantSpeedLoad` + - Ode-Solver: :py:class:`.EulerSolver` + + - Reference Generator: :py:class:`.WienerProcessReferenceGenerator` *Reference Quantity:* ``'torque'`` + + - Reward Function: :py:class:`.WeightedSumOfErrors` reward_weights: ``'torque' = 1`` + + - Visualization: :py:class:`.MotorDashboard` torque and action plots + + - Constraints: :py:class:`.LimitConstraint` on the currents ``'i_a', 'i_e'`` + + State Variables: + ``['omega' , 'torque', 'i_a', 'i_e', 'u', 'u_sup']`` + + Reference Variables: + ``['torque']`` + + Control Cycle Time: + tau = 1e-5 seconds + + Observation Space: + Type: Tuple(State_Space, Reference_Space) + + State Space: + Box(low=[-1, -1, -1, -1, 0], high=[1, 1, 1, 1, 1]) + + Reference Space: + Box(low=[-1], high=[1]) + + Action Space: + Discrete(4) + + Initial State: + Zeros on all state variables. + + Example: + >>> import gym_electric_motor as gem + >>> from gym_electric_motor.reference_generators import LaplaceProcessReferenceGenerator + >>> + >>> # Select a different converter with default parameters by passing a keystring + >>> my_overridden_converter = 'Finite-2QC' + >>> + >>> # Update the default arguments to the voltage supply by passing a parameter dict + >>> my_changed_voltage_supply_args = {'u_nominal': 400.0} + >>> + >>> # Replace the reference generator by passing a new instance + >>> my_new_ref_gen_instance = LaplaceProcessReferenceGenerator( + ... reference_state='torque', + ... sigma_range=(1e-3, 1e-2) + ... ) + >>> env = gem.make( + ... 'Finite-TC-ShuntDc-v0', + ... voltage_supply=my_changed_voltage_supply_args, + ... converter=my_overridden_converter, + ... reference_generator=my_new_ref_gen_instance + ... ) + >>> terminated = True + >>> for _ in range(1000): + >>> if terminated: + >>> state, reference = env.reset() + >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) + """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=('i_a', 'i_e'), calc_jacobian=True, tau=1e-5, physical_system_wrappers=(), **kwargs): + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=("i_a", "i_e"), + calc_jacobian=True, + tau=1e-5, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -124,24 +143,50 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = DcMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.FiniteFourQuadrantConverter, dict()), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.FiniteFourQuadrantConverter, + dict(), + ), motor=initialize(ps.ElectricMotor, motor, ps.DcShuntMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, dict(reference_state='torque') + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="torque"), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(torque=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(torque=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, dict(state_plots=('torque',), action_plots='all')) + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("torque",), action_plots="all"), + ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=tuple(physical_system_wrappers) + (CurrentSumProcessor(('i_a', 'i_e')),), **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=tuple(physical_system_wrappers) + + (CurrentSumProcessor(("i_a", "i_e")),), + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_eesm/cont_cc_eesm_env.py b/gym_electric_motor/envs/gym_eesm/cont_cc_eesm_env.py index 7f07e548..4b35f7df 100644 --- a/gym_electric_motor/envs/gym_eesm/cont_cc_eesm_env.py +++ b/gym_electric_motor/envs/gym_eesm/cont_cc_eesm_env.py @@ -1,8 +1,17 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization -from gym_electric_motor.physical_systems.physical_systems import ExternallyExcitedSynchronousMotorSystem +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) +from gym_electric_motor.physical_systems.physical_systems import ( + ExternallyExcitedSynchronousMotorSystem, +) from gym_electric_motor.visualization import MotorDashboard -from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator, MultipleReferenceGenerator +from gym_electric_motor.reference_generators import ( + WienerProcessReferenceGenerator, + MultipleReferenceGenerator, +) from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize @@ -83,10 +92,25 @@ class ContCurrentControlExternallyExcitedSynchronousMotorEnv(ElectricMotorEnviro >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=(SquaredConstraint(('i_sq', 'i_sd')), LimitConstraint(('i_e',))), calc_jacobian=True, - tau=1e-4, physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")), LimitConstraint(("i_e",))), + calc_jacobian=True, + tau=1e-4, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -124,45 +148,60 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve The available strings can be looked up in the documentation. (e.g., ``converter='Finite-2QC'``) """ default_subgenerators = ( - WienerProcessReferenceGenerator(reference_state='i_sd'), - WienerProcessReferenceGenerator(reference_state='i_sq'), - WienerProcessReferenceGenerator(reference_state='i_e', limit_margin=(0, 1)), + WienerProcessReferenceGenerator(reference_state="i_sd"), + WienerProcessReferenceGenerator(reference_state="i_sq"), + WienerProcessReferenceGenerator(reference_state="i_e", limit_margin=(0, 1)), ) default_subconverters = ( ps.ContB6BridgeConverter(), - ps.ContFourQuadrantConverter() + ps.ContFourQuadrantConverter(), ) physical_system = ExternallyExcitedSynchronousMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=300.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=300.0) + ), converter=initialize( ps.PowerElectronicConverter, converter, ps.ContMultiConverter, - dict(subconverters=default_subconverters) + dict(subconverters=default_subconverters), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.ExternallyExcitedSynchronousMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) ), - motor=initialize(ps.ElectricMotor, motor, ps.ExternallyExcitedSynchronousMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( ReferenceGenerator, reference_generator, MultipleReferenceGenerator, - dict(sub_generators=default_subgenerators) + dict(sub_generators=default_subgenerators), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(i_sd=1/3, i_sq=1/3, i_e=1/3)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(i_sd=1 / 3, i_sq=1 / 3, i_e=1 / 3)), ) visualization = initialize( ElectricMotorVisualization, visualization, MotorDashboard, - dict(state_plots=('i_sd', 'i_sq', 'i_e'), action_plots='all') + dict(state_plots=("i_sd", "i_sq", "i_e"), action_plots="all"), ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_eesm/cont_sc_eesm_env.py b/gym_electric_motor/envs/gym_eesm/cont_sc_eesm_env.py index 7ecb11b7..748f1187 100644 --- a/gym_electric_motor/envs/gym_eesm/cont_sc_eesm_env.py +++ b/gym_electric_motor/envs/gym_eesm/cont_sc_eesm_env.py @@ -1,6 +1,13 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization -from gym_electric_motor.physical_systems.physical_systems import ExternallyExcitedSynchronousMotorSystem, SynchronousMotorSystem +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) +from gym_electric_motor.physical_systems.physical_systems import ( + ExternallyExcitedSynchronousMotorSystem, + SynchronousMotorSystem, +) from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator from gym_electric_motor import physical_systems as ps @@ -83,10 +90,25 @@ class ContSpeedControlExternallyExcitedSynchronousMotorEnv(ElectricMotorEnvironm >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=(SquaredConstraint(('i_sq', 'i_sd')), LimitConstraint(('i_e',))), calc_jacobian=True, - tau=1e-4, physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")), LimitConstraint(("i_e",))), + calc_jacobian=True, + tau=1e-4, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -125,34 +147,57 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve """ default_subconverters = ( ps.ContB6BridgeConverter(), - ps.ContFourQuadrantConverter() + ps.ContFourQuadrantConverter(), ) physical_system = ExternallyExcitedSynchronousMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), converter=initialize( ps.PowerElectronicConverter, converter, ps.ContMultiConverter, - dict(subconverters=default_subconverters) + dict(subconverters=default_subconverters), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.ExternallyExcitedSynchronousMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, + load, + ps.PolynomialStaticLoad, + dict(load_parameter=dict(a=0.01, b=0.01, c=0.0)), ), - motor=initialize(ps.ElectricMotor, motor, ps.ExternallyExcitedSynchronousMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.PolynomialStaticLoad, dict( - load_parameter=dict(a=0.01, b=0.01, c=0.0) - )), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, dict(reference_state='omega') + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="omega"), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(omega=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(omega=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, dict(state_plots=('omega',), action_plots='all')) + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("omega",), action_plots="all"), + ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_eesm/cont_tc_eesm_env.py b/gym_electric_motor/envs/gym_eesm/cont_tc_eesm_env.py index b07248a1..ed7bdedd 100644 --- a/gym_electric_motor/envs/gym_eesm/cont_tc_eesm_env.py +++ b/gym_electric_motor/envs/gym_eesm/cont_tc_eesm_env.py @@ -1,8 +1,18 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization -from gym_electric_motor.physical_systems.physical_systems import ExternallyExcitedSynchronousMotorSystem, SynchronousMotorSystem +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) +from gym_electric_motor.physical_systems.physical_systems import ( + ExternallyExcitedSynchronousMotorSystem, + SynchronousMotorSystem, +) from gym_electric_motor.visualization import MotorDashboard -from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator, MultipleReferenceGenerator +from gym_electric_motor.reference_generators import ( + WienerProcessReferenceGenerator, + MultipleReferenceGenerator, +) from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize @@ -23,7 +33,7 @@ class ContTorqueControlExternallyExcitedSynchronousMotorEnv(ElectricMotorEnviron - Motor: :py:class:`.ExternallyExcitedSynchronousMotor` - Load: :py:class:`.ConstantSpeedLoad` - Ode-Solver: :py:class:`.EulerSolver` - + - Reference Generator: :py:class:`.WienerProcessReferenceGenerator` *Reference Quantity:* ``'torque'`` @@ -84,10 +94,25 @@ class ContTorqueControlExternallyExcitedSynchronousMotorEnv(ElectricMotorEnviron >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=(SquaredConstraint(('i_sq', 'i_sd')), LimitConstraint(('i_e',))), calc_jacobian=True, - tau=1e-4, physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")), LimitConstraint(("i_e",))), + calc_jacobian=True, + tau=1e-4, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -126,18 +151,24 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve """ default_subconverters = ( ps.ContB6BridgeConverter(), - ps.ContFourQuadrantConverter() + ps.ContFourQuadrantConverter(), ) physical_system = ExternallyExcitedSynchronousMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), converter=initialize( ps.PowerElectronicConverter, converter, ps.ContMultiConverter, - dict(subconverters=default_subconverters) + dict(subconverters=default_subconverters), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.ExternallyExcitedSynchronousMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) ), - motor=initialize(ps.ElectricMotor, motor, ps.ExternallyExcitedSynchronousMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, @@ -146,19 +177,28 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, - dict(reference_state='torque') + dict(reference_state="torque"), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(torque=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(torque=1.0)), ) visualization = initialize( ElectricMotorVisualization, visualization, MotorDashboard, - dict(state_plots=('torque',), action_plots='all') + dict(state_plots=("torque",), action_plots="all"), ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_eesm/finite_cc_eesm_env.py b/gym_electric_motor/envs/gym_eesm/finite_cc_eesm_env.py index 058b7b8d..d46319e8 100644 --- a/gym_electric_motor/envs/gym_eesm/finite_cc_eesm_env.py +++ b/gym_electric_motor/envs/gym_eesm/finite_cc_eesm_env.py @@ -1,15 +1,26 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization -from gym_electric_motor.physical_systems.physical_systems import ExternallyExcitedSynchronousMotorSystem +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) +from gym_electric_motor.physical_systems.physical_systems import ( + ExternallyExcitedSynchronousMotorSystem, +) from gym_electric_motor.visualization import MotorDashboard -from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator, MultipleReferenceGenerator +from gym_electric_motor.reference_generators import ( + WienerProcessReferenceGenerator, + MultipleReferenceGenerator, +) from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize from gym_electric_motor.constraints import LimitConstraint, SquaredConstraint -class FiniteCurrentControlExternallyExcitedSynchronousMotorEnv(ElectricMotorEnvironment): +class FiniteCurrentControlExternallyExcitedSynchronousMotorEnv( + ElectricMotorEnvironment +): """ Description: Environment to simulate a finite control set current controlled externally excited synchronous motor. @@ -83,10 +94,25 @@ class FiniteCurrentControlExternallyExcitedSynchronousMotorEnv(ElectricMotorEnvi >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=(SquaredConstraint(('i_sq', 'i_sd')), LimitConstraint(('i_e',))), calc_jacobian=True, - tau=1e-5, physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")), LimitConstraint(("i_e",))), + calc_jacobian=True, + tau=1e-5, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -124,40 +150,60 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve The available strings can be looked up in the documentation. (e.g., ``converter='Finite-2QC'``) """ default_subgenerators = ( - WienerProcessReferenceGenerator(reference_state='i_sd'), - WienerProcessReferenceGenerator(reference_state='i_sq'), - WienerProcessReferenceGenerator(reference_state='i_e') + WienerProcessReferenceGenerator(reference_state="i_sd"), + WienerProcessReferenceGenerator(reference_state="i_sq"), + WienerProcessReferenceGenerator(reference_state="i_e"), ) default_subconverters = ( ps.FiniteB6BridgeConverter(), - ps.FiniteFourQuadrantConverter() + ps.FiniteFourQuadrantConverter(), ) physical_system = ExternallyExcitedSynchronousMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.FiniteMultiConverter, dict(subconverters=default_subconverters)), - motor=initialize(ps.ElectricMotor, motor, ps.ExternallyExcitedSynchronousMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.FiniteMultiConverter, + dict(subconverters=default_subconverters), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.ExternallyExcitedSynchronousMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( ReferenceGenerator, reference_generator, MultipleReferenceGenerator, - dict(sub_generators=default_subgenerators) + dict(sub_generators=default_subgenerators), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(i_sd=1/3, i_sq=1/3, i_e=1/3)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(i_sd=1 / 3, i_sq=1 / 3, i_e=1 / 3)), ) visualization = initialize( ElectricMotorVisualization, visualization, MotorDashboard, - dict(state_plots=('i_sd', 'i_sq', 'i_e'), action_plots='all') + dict(state_plots=("i_sd", "i_sq", "i_e"), action_plots="all"), ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_eesm/finite_sc_eesm_env.py b/gym_electric_motor/envs/gym_eesm/finite_sc_eesm_env.py index b0d4f275..ef8e39b8 100644 --- a/gym_electric_motor/envs/gym_eesm/finite_sc_eesm_env.py +++ b/gym_electric_motor/envs/gym_eesm/finite_sc_eesm_env.py @@ -1,6 +1,12 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization -from gym_electric_motor.physical_systems.physical_systems import ExternallyExcitedSynchronousMotorSystem +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) +from gym_electric_motor.physical_systems.physical_systems import ( + ExternallyExcitedSynchronousMotorSystem, +) from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator from gym_electric_motor import physical_systems as ps @@ -83,10 +89,25 @@ class FiniteSpeedControlExternallyExcitedSynchronousMotorEnv(ElectricMotorEnviro >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=(SquaredConstraint(('i_sq', 'i_sd')), LimitConstraint(('i_e',))), calc_jacobian=True, - tau=1e-5, physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")), LimitConstraint(("i_e",))), + calc_jacobian=True, + tau=1e-5, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -125,34 +146,52 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve """ default_subconverters = ( ps.FiniteB6BridgeConverter(), - ps.FiniteFourQuadrantConverter() + ps.FiniteFourQuadrantConverter(), ) physical_system = ExternallyExcitedSynchronousMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.FiniteMultiConverter, dict(subconverters=default_subconverters)), - motor=initialize(ps.ElectricMotor, motor, ps.ExternallyExcitedSynchronousMotor, dict()), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.FiniteMultiConverter, + dict(subconverters=default_subconverters), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.ExternallyExcitedSynchronousMotor, dict() + ), load=initialize(ps.MechanicalLoad, load, ps.PolynomialStaticLoad, dict()), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, - dict(reference_state='omega') + dict(reference_state="omega"), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(omega=1)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(omega=1)), ) visualization = initialize( ElectricMotorVisualization, visualization, MotorDashboard, - dict(state_plots=('omega',), action_plots='all') + dict(state_plots=("omega",), action_plots="all"), ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_eesm/finite_tc_eesm_env.py b/gym_electric_motor/envs/gym_eesm/finite_tc_eesm_env.py index 7e09ab53..f2129ad1 100644 --- a/gym_electric_motor/envs/gym_eesm/finite_tc_eesm_env.py +++ b/gym_electric_motor/envs/gym_eesm/finite_tc_eesm_env.py @@ -1,6 +1,12 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization -from gym_electric_motor.physical_systems.physical_systems import ExternallyExcitedSynchronousMotorSystem +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) +from gym_electric_motor.physical_systems.physical_systems import ( + ExternallyExcitedSynchronousMotorSystem, +) from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator from gym_electric_motor import physical_systems as ps @@ -83,10 +89,25 @@ class FiniteTorqueControlExternallyExcitedSynchronousMotorEnv(ElectricMotorEnvir >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=(SquaredConstraint(('i_sq', 'i_sd')), LimitConstraint(('i_e',))), calc_jacobian=True, - tau=1e-5, physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")), LimitConstraint(("i_e",))), + calc_jacobian=True, + tau=1e-5, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -122,37 +143,57 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve **str:** Pass a string out of the registered classes to select a different class for the component. This class is then initialized with its default parameters. The available strings can be looked up in the documentation. (e.g., ``converter='Finite-2QC'``) - """ + """ default_subconverters = ( ps.FiniteB6BridgeConverter(), - ps.FiniteFourQuadrantConverter() + ps.FiniteFourQuadrantConverter(), ) physical_system = ExternallyExcitedSynchronousMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.FiniteMultiConverter, dict(subconverters=default_subconverters)), - motor=initialize(ps.ElectricMotor, motor, ps.ExternallyExcitedSynchronousMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.FiniteMultiConverter, + dict(subconverters=default_subconverters), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.ExternallyExcitedSynchronousMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, - dict(reference_state='torque') + dict(reference_state="torque"), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(torque=1)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(torque=1)), ) visualization = initialize( ElectricMotorVisualization, visualization, MotorDashboard, - dict(state_plots=('torque',), action_plots='all') + dict(state_plots=("torque",), action_plots="all"), ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_im/__init__.py b/gym_electric_motor/envs/gym_im/__init__.py index 7a3b29ba..c693998e 100644 --- a/gym_electric_motor/envs/gym_im/__init__.py +++ b/gym_electric_motor/envs/gym_im/__init__.py @@ -1,13 +1,33 @@ -from .squirrel_cage_induction_motor_envs import ContCurrentControlSquirrelCageInductionMotorEnv -from .squirrel_cage_induction_motor_envs import ContSpeedControlSquirrelCageInductionMotorEnv -from .squirrel_cage_induction_motor_envs import ContTorqueControlSquirrelCageInductionMotorEnv -from .squirrel_cage_induction_motor_envs import FiniteCurrentControlSquirrelCageInductionMotorEnv -from .squirrel_cage_induction_motor_envs import FiniteSpeedControlSquirrelCageInductionMotorEnv -from .squirrel_cage_induction_motor_envs import FiniteTorqueControlSquirrelCageInductionMotorEnv +from .squirrel_cage_induction_motor_envs import ( + ContCurrentControlSquirrelCageInductionMotorEnv, +) +from .squirrel_cage_induction_motor_envs import ( + ContSpeedControlSquirrelCageInductionMotorEnv, +) +from .squirrel_cage_induction_motor_envs import ( + ContTorqueControlSquirrelCageInductionMotorEnv, +) +from .squirrel_cage_induction_motor_envs import ( + FiniteCurrentControlSquirrelCageInductionMotorEnv, +) +from .squirrel_cage_induction_motor_envs import ( + FiniteSpeedControlSquirrelCageInductionMotorEnv, +) +from .squirrel_cage_induction_motor_envs import ( + FiniteTorqueControlSquirrelCageInductionMotorEnv, +) -from .doubly_fed_induction_motor_envs import ContCurrentControlDoublyFedInductionMotorEnv +from .doubly_fed_induction_motor_envs import ( + ContCurrentControlDoublyFedInductionMotorEnv, +) from .doubly_fed_induction_motor_envs import ContSpeedControlDoublyFedInductionMotorEnv from .doubly_fed_induction_motor_envs import ContTorqueControlDoublyFedInductionMotorEnv -from .doubly_fed_induction_motor_envs import FiniteCurrentControlDoublyFedInductionMotorEnv -from .doubly_fed_induction_motor_envs import FiniteSpeedControlDoublyFedInductionMotorEnv -from .doubly_fed_induction_motor_envs import FiniteTorqueControlDoublyFedInductionMotorEnv +from .doubly_fed_induction_motor_envs import ( + FiniteCurrentControlDoublyFedInductionMotorEnv, +) +from .doubly_fed_induction_motor_envs import ( + FiniteSpeedControlDoublyFedInductionMotorEnv, +) +from .doubly_fed_induction_motor_envs import ( + FiniteTorqueControlDoublyFedInductionMotorEnv, +) diff --git a/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_cc_dfim_env.py b/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_cc_dfim_env.py index aef2bdad..70742aed 100644 --- a/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_cc_dfim_env.py +++ b/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_cc_dfim_env.py @@ -1,8 +1,17 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization -from gym_electric_motor.physical_systems.physical_systems import DoublyFedInductionMotorSystem +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) +from gym_electric_motor.physical_systems.physical_systems import ( + DoublyFedInductionMotorSystem, +) from gym_electric_motor.visualization import MotorDashboard -from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator, MultipleReferenceGenerator +from gym_electric_motor.reference_generators import ( + WienerProcessReferenceGenerator, + MultipleReferenceGenerator, +) from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize @@ -90,11 +99,24 @@ class ContCurrentControlDoublyFedInductionMotorEnv(ElectricMotorEnvironment): >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ + def __init__( - self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=(SquaredConstraint(('i_sq', 'i_sd')),), calc_jacobian=True, tau=1e-4, - physical_system_wrappers=(), **kwargs + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")),), + calc_jacobian=True, + tau=1e-4, + physical_system_wrappers=(), + **kwargs, ): """ Args: @@ -133,24 +155,30 @@ def __init__( The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ default_sub_generators = ( - WienerProcessReferenceGenerator(reference_state='i_sd'), - WienerProcessReferenceGenerator(reference_state='i_sq') + WienerProcessReferenceGenerator(reference_state="i_sd"), + WienerProcessReferenceGenerator(reference_state="i_sq"), ) default_sub_converters = ( ps.ContB6BridgeConverter(), - ps.ContB6BridgeConverter() + ps.ContB6BridgeConverter(), ) physical_system = DoublyFedInductionMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), converter=initialize( ps.PowerElectronicConverter, converter, ps.ContMultiConverter, - dict(subconverters=default_sub_converters) + dict(subconverters=default_sub_converters), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.DoublyFedInductionMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) ), - motor=initialize(ps.ElectricMotor, motor, ps.DoublyFedInductionMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, @@ -159,19 +187,28 @@ def __init__( ReferenceGenerator, reference_generator, MultipleReferenceGenerator, - dict(sub_generators=default_sub_generators) + dict(sub_generators=default_sub_generators), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(i_sd=0.5, i_sq=0.5)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(i_sd=0.5, i_sq=0.5)), ) visualization = initialize( ElectricMotorVisualization, visualization, MotorDashboard, - dict(state_plots=('i_sd', 'i_sq'), action_plots='all') + dict(state_plots=("i_sd", "i_sq"), action_plots="all"), ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_sc_dfim_env.py b/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_sc_dfim_env.py index 5b655c9f..d15f1c73 100644 --- a/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_sc_dfim_env.py +++ b/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_sc_dfim_env.py @@ -1,6 +1,12 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization -from gym_electric_motor.physical_systems.physical_systems import DoublyFedInductionMotorSystem +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) +from gym_electric_motor.physical_systems.physical_systems import ( + DoublyFedInductionMotorSystem, +) from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator from gym_electric_motor import physical_systems as ps @@ -90,10 +96,25 @@ class ContSpeedControlDoublyFedInductionMotorEnv(ElectricMotorEnvironment): >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=(SquaredConstraint(('i_sq', 'i_sd')),), calc_jacobian=True, tau=1e-4, - physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")),), + calc_jacobian=True, + tau=1e-4, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -132,35 +153,57 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve """ default_sub_converters = ( ps.ContB6BridgeConverter(), - ps.ContB6BridgeConverter() + ps.ContB6BridgeConverter(), ) physical_system = DoublyFedInductionMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), converter=initialize( ps.PowerElectronicConverter, converter, ps.ContMultiConverter, - dict(subconverters=default_sub_converters) + dict(subconverters=default_sub_converters), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.DoublyFedInductionMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, + load, + ps.PolynomialStaticLoad, + dict(load_parameter=dict(a=0.01, b=0.01, c=0.0)), ), - motor=initialize(ps.ElectricMotor, motor, ps.DoublyFedInductionMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.PolynomialStaticLoad, dict( - load_parameter=dict(a=0.01, b=0.01, c=0.0) - )), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, - dict(reference_state='omega', sigma_range=(1e-3, 1e-2)), + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="omega", sigma_range=(1e-3, 1e-2)), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(omega=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(omega=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, dict(state_plots=('omega',), action_plots='all')) + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("omega",), action_plots="all"), + ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_tc_dfim_env.py b/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_tc_dfim_env.py index dcd6b4be..10f36343 100644 --- a/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_tc_dfim_env.py +++ b/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_tc_dfim_env.py @@ -1,8 +1,17 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization -from gym_electric_motor.physical_systems.physical_systems import DoublyFedInductionMotorSystem +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) +from gym_electric_motor.physical_systems.physical_systems import ( + DoublyFedInductionMotorSystem, +) from gym_electric_motor.visualization import MotorDashboard -from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator, MultipleReferenceGenerator +from gym_electric_motor.reference_generators import ( + WienerProcessReferenceGenerator, + MultipleReferenceGenerator, +) from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize @@ -90,10 +99,25 @@ class ContTorqueControlDoublyFedInductionMotorEnv(ElectricMotorEnvironment): >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=(SquaredConstraint(('i_sq', 'i_sd')),), calc_jacobian=True, tau=1e-4, - physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")),), + calc_jacobian=True, + tau=1e-4, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -132,18 +156,24 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve """ default_sub_converters = ( ps.ContB6BridgeConverter(), - ps.ContB6BridgeConverter() + ps.ContB6BridgeConverter(), ) physical_system = DoublyFedInductionMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), converter=initialize( ps.PowerElectronicConverter, converter, ps.ContMultiConverter, - dict(subconverters=default_sub_converters) + dict(subconverters=default_sub_converters), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.DoublyFedInductionMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) ), - motor=initialize(ps.ElectricMotor, motor, ps.DoublyFedInductionMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, @@ -152,19 +182,28 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, - dict(reference_state='torque') + dict(reference_state="torque"), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(torque=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(torque=1.0)), ) visualization = initialize( ElectricMotorVisualization, visualization, MotorDashboard, - dict(state_plots=('torque',), action_plots='all') + dict(state_plots=("torque",), action_plots="all"), ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_cc_dfim_env.py b/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_cc_dfim_env.py index 3f5c6e4c..833aab36 100644 --- a/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_cc_dfim_env.py +++ b/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_cc_dfim_env.py @@ -1,8 +1,17 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization -from gym_electric_motor.physical_systems.physical_systems import DoublyFedInductionMotorSystem +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) +from gym_electric_motor.physical_systems.physical_systems import ( + DoublyFedInductionMotorSystem, +) from gym_electric_motor.visualization import MotorDashboard -from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator, MultipleReferenceGenerator +from gym_electric_motor.reference_generators import ( + WienerProcessReferenceGenerator, + MultipleReferenceGenerator, +) from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize @@ -90,10 +99,25 @@ class FiniteCurrentControlDoublyFedInductionMotorEnv(ElectricMotorEnvironment): >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=(SquaredConstraint(('i_sq', 'i_sd')),), calc_jacobian=True, tau=1e-5, - physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")),), + calc_jacobian=True, + tau=1e-5, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -131,45 +155,60 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ default_sub_generators = ( - WienerProcessReferenceGenerator(reference_state='i_sd'), - WienerProcessReferenceGenerator(reference_state='i_sq') + WienerProcessReferenceGenerator(reference_state="i_sd"), + WienerProcessReferenceGenerator(reference_state="i_sq"), ) default_sub_converters = ( ps.FiniteB6BridgeConverter(), - ps.FiniteB6BridgeConverter() + ps.FiniteB6BridgeConverter(), ) physical_system = DoublyFedInductionMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), converter=initialize( ps.PowerElectronicConverter, converter, ps.FiniteMultiConverter, - dict(subconverters=default_sub_converters) + dict(subconverters=default_sub_converters), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.DoublyFedInductionMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) ), - motor=initialize(ps.ElectricMotor, motor, ps.DoublyFedInductionMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( ReferenceGenerator, reference_generator, MultipleReferenceGenerator, - dict(sub_generators=default_sub_generators) + dict(sub_generators=default_sub_generators), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(i_sd=0.5, i_sq=0.5)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(i_sd=0.5, i_sq=0.5)), ) visualization = initialize( ElectricMotorVisualization, visualization, MotorDashboard, - dict(state_plots=('i_sd', 'i_sq'), action_plots='all') + dict(state_plots=("i_sd", "i_sq"), action_plots="all"), ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_sc_dfim_env.py b/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_sc_dfim_env.py index 2154ba2c..b91890dc 100644 --- a/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_sc_dfim_env.py +++ b/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_sc_dfim_env.py @@ -1,6 +1,12 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization -from gym_electric_motor.physical_systems.physical_systems import DoublyFedInductionMotorSystem +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) +from gym_electric_motor.physical_systems.physical_systems import ( + DoublyFedInductionMotorSystem, +) from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator from gym_electric_motor import physical_systems as ps @@ -90,10 +96,25 @@ class FiniteSpeedControlDoublyFedInductionMotorEnv(ElectricMotorEnvironment): >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=(SquaredConstraint(('i_sq', 'i_sd')),), calc_jacobian=True, tau=1e-5, - physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")),), + calc_jacobian=True, + tau=1e-5, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -132,35 +153,57 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve """ default_subconverters = ( ps.FiniteB6BridgeConverter(), - ps.FiniteB6BridgeConverter() + ps.FiniteB6BridgeConverter(), ) physical_system = DoublyFedInductionMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), converter=initialize( ps.PowerElectronicConverter, converter, ps.FiniteMultiConverter, - dict(subconverters=default_subconverters) + dict(subconverters=default_subconverters), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.DoublyFedInductionMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, + load, + ps.PolynomialStaticLoad, + dict(load_parameter=dict(a=0.01, b=0.01, c=0.0)), ), - motor=initialize(ps.ElectricMotor, motor, ps.DoublyFedInductionMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.PolynomialStaticLoad, dict( - load_parameter=dict(a=0.01, b=0.01, c=0.0) - )), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, - dict(reference_state='omega', sigma_range=(1e-3, 1e-2)), + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="omega", sigma_range=(1e-3, 1e-2)), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(omega=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(omega=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, dict(state_plots=('omega',), action_plots='all')) + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("omega",), action_plots="all"), + ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_tc_dfim_env.py b/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_tc_dfim_env.py index 70830114..9b58c465 100644 --- a/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_tc_dfim_env.py +++ b/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_tc_dfim_env.py @@ -1,6 +1,12 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization -from gym_electric_motor.physical_systems.physical_systems import DoublyFedInductionMotorSystem +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) +from gym_electric_motor.physical_systems.physical_systems import ( + DoublyFedInductionMotorSystem, +) from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator from gym_electric_motor import physical_systems as ps @@ -90,10 +96,25 @@ class FiniteTorqueControlDoublyFedInductionMotorEnv(ElectricMotorEnvironment): >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=(SquaredConstraint(('i_sq', 'i_sd')),), calc_jacobian=True, tau=1e-5, - physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")),), + calc_jacobian=True, + tau=1e-5, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -132,33 +153,54 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve """ default_sub_converters = ( ps.FiniteB6BridgeConverter(), - ps.FiniteB6BridgeConverter() + ps.FiniteB6BridgeConverter(), ) physical_system = DoublyFedInductionMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), converter=initialize( ps.PowerElectronicConverter, converter, ps.FiniteMultiConverter, - dict(subconverters=default_sub_converters) + dict(subconverters=default_sub_converters), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.DoublyFedInductionMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) ), - motor=initialize(ps.ElectricMotor, motor, ps.DoublyFedInductionMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, dict(reference_state='torque') + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="torque"), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(torque=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(torque=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, dict(state_plots=('torque',), action_plots='all') + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("torque",), action_plots="all"), ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_cc_scim_env.py b/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_cc_scim_env.py index b74aa172..e82d846c 100644 --- a/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_cc_scim_env.py +++ b/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_cc_scim_env.py @@ -1,8 +1,17 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization -from gym_electric_motor.physical_systems.physical_systems import SquirrelCageInductionMotorSystem +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) +from gym_electric_motor.physical_systems.physical_systems import ( + SquirrelCageInductionMotorSystem, +) from gym_electric_motor.visualization import MotorDashboard -from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator, MultipleReferenceGenerator +from gym_electric_motor.reference_generators import ( + WienerProcessReferenceGenerator, + MultipleReferenceGenerator, +) from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize @@ -11,87 +20,102 @@ class ContCurrentControlSquirrelCageInductionMotorEnv(ElectricMotorEnvironment): """ - Description: - Environment to simulate an abc-domain cont. control set current controlled squirrel cage induction motor. - - Key: - ``'Cont-CC-SCIM-v0'`` - - Default Components: - - Supply: :py:class:`.IdealVoltageSupply` - - Converter: :py:class:`.ContB6BridgeConverter` - - Motor: :py:class:`.SquirrelCageInductionMotor` - - Load: :py:class:`.ConstantSpeedLoad` - - Ode-Solver: :py:class:`.EulerSolver` - - - Reference Generator: :py:class:`.WienerProcessReferenceGenerator` *Reference Quantity:* ``'i_sd', 'i_sq`` - - - Reward Function: :py:class:`.WeightedSumOfErrors` reward_weights: ``'i_sd' = 0.5, 'i_sq' = 0.5`` - - - Visualization: :py:class:`.MotorDashboard` current and action plots - - - Constraints: :py:class:`.SquaredConstraint` on the currents ``'i_sd', 'i_sq'`` - - State Variables: - ``[ - 'omega' , 'torque', - 'i_sa', 'i_sb', 'i_sc', 'i_sd', 'i_sq', - 'u_sa', 'u_sb', 'u_sc', 'u_sd', 'u_sq', - 'epsilon', 'u_sup' - ]`` - - Reference Variables: - ``['i_sd', 'i_sq']`` - - Control Cycle Time: - tau = 1e-4 seconds - - Observation Space: - Type: Tuple(State_Space, Reference_Space) - - State Space: - Box(low=14 * [-1], high=14 * [1]) - - Reference Space: - Box(low=[-1, -1], high=[1, 1]) - - Action Space: - Box(low=[-1, -1, -1], high=[1, 1, 1]) - - Initial State: - Zeros on all state variables. - - Example: - >>> import gym_electric_motor as gem - >>> from gym_electric_motor.reference_generators import LaplaceProcessReferenceGenerator - >>> - >>> # Select a different ode_solver with default parameters by passing a keystring - >>> my_overridden_solver = 'scipy.solve_ivp' - >>> - >>> # Update the default arguments to the voltage supply by passing a parameter dict - >>> my_changed_voltage_supply_args = {'u_nominal': 400.0} - >>> - >>> # Replace the reference generator by passing a new instance - >>> my_new_ref_gen_instance = LaplaceProcessReferenceGenerator( - ... reference_state='i_sq', - ... sigma_range=(1e-3, 1e-2) - ... ) - >>> env = gem.make( - ... 'Cont-CC-SCIM-v0', - ... voltage_supply=my_changed_voltage_supply_args, - ... ode_solver=my_overridden_solver, - ... reference_generator=my_new_ref_gen_instance - ... ) - >>> terminated = True - >>> for _ in range(1000): - >>> if terminated: - >>> state, reference = env.reset() - >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) + Description: + Environment to simulate an abc-domain cont. control set current controlled squirrel cage induction motor. + + Key: + ``'Cont-CC-SCIM-v0'`` + + Default Components: + - Supply: :py:class:`.IdealVoltageSupply` + - Converter: :py:class:`.ContB6BridgeConverter` + - Motor: :py:class:`.SquirrelCageInductionMotor` + - Load: :py:class:`.ConstantSpeedLoad` + - Ode-Solver: :py:class:`.EulerSolver` + + - Reference Generator: :py:class:`.WienerProcessReferenceGenerator` *Reference Quantity:* ``'i_sd', 'i_sq`` + + - Reward Function: :py:class:`.WeightedSumOfErrors` reward_weights: ``'i_sd' = 0.5, 'i_sq' = 0.5`` + + - Visualization: :py:class:`.MotorDashboard` current and action plots + + - Constraints: :py:class:`.SquaredConstraint` on the currents ``'i_sd', 'i_sq'`` + + State Variables: + ``[ + 'omega' , 'torque', + 'i_sa', 'i_sb', 'i_sc', 'i_sd', 'i_sq', + 'u_sa', 'u_sb', 'u_sc', 'u_sd', 'u_sq', + 'epsilon', 'u_sup' + ]`` + + Reference Variables: + ``['i_sd', 'i_sq']`` + + Control Cycle Time: + tau = 1e-4 seconds + + Observation Space: + Type: Tuple(State_Space, Reference_Space) + + State Space: + Box(low=14 * [-1], high=14 * [1]) + + Reference Space: + Box(low=[-1, -1], high=[1, 1]) + + Action Space: + Box(low=[-1, -1, -1], high=[1, 1, 1]) + + Initial State: + Zeros on all state variables. + + Example: + >>> import gym_electric_motor as gem + >>> from gym_electric_motor.reference_generators import LaplaceProcessReferenceGenerator + >>> + >>> # Select a different ode_solver with default parameters by passing a keystring + >>> my_overridden_solver = 'scipy.solve_ivp' + >>> + >>> # Update the default arguments to the voltage supply by passing a parameter dict + >>> my_changed_voltage_supply_args = {'u_nominal': 400.0} + >>> + >>> # Replace the reference generator by passing a new instance + >>> my_new_ref_gen_instance = LaplaceProcessReferenceGenerator( + ... reference_state='i_sq', + ... sigma_range=(1e-3, 1e-2) + ... ) + >>> env = gem.make( + ... 'Cont-CC-SCIM-v0', + ... voltage_supply=my_changed_voltage_supply_args, + ... ode_solver=my_overridden_solver, + ... reference_generator=my_new_ref_gen_instance + ... ) + >>> terminated = True + >>> for _ in range(1000): + >>> if terminated: + >>> state, reference = env.reset() + >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=(SquaredConstraint(('i_sq', 'i_sd')),), calc_jacobian=True, tau=1e-4, - physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")),), + calc_jacobian=True, + tau=1e-4, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -129,15 +153,23 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ default_subgenerators = ( - WienerProcessReferenceGenerator(reference_state='i_sd'), - WienerProcessReferenceGenerator(reference_state='i_sq') + WienerProcessReferenceGenerator(reference_state="i_sd"), + WienerProcessReferenceGenerator(reference_state="i_sq"), ) physical_system = SquirrelCageInductionMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict()), - motor=initialize(ps.ElectricMotor, motor, ps.SquirrelCageInductionMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), + converter=initialize( + ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict() + ), + motor=initialize( + ps.ElectricMotor, motor, ps.SquirrelCageInductionMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, @@ -146,19 +178,28 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve ReferenceGenerator, reference_generator, MultipleReferenceGenerator, - dict(sub_generators=default_subgenerators) + dict(sub_generators=default_subgenerators), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(i_sd=0.5, i_sq=0.5)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(i_sd=0.5, i_sq=0.5)), ) visualization = initialize( ElectricMotorVisualization, visualization, MotorDashboard, - dict(state_plots=('i_sd', 'i_sq'), action_plots='all') + dict(state_plots=("i_sd", "i_sq"), action_plots="all"), ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_sc_scim_env.py b/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_sc_scim_env.py index 0f0740e7..02a51b18 100644 --- a/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_sc_scim_env.py +++ b/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_sc_scim_env.py @@ -1,6 +1,12 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization -from gym_electric_motor.physical_systems.physical_systems import SquirrelCageInductionMotorSystem +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) +from gym_electric_motor.physical_systems.physical_systems import ( + SquirrelCageInductionMotorSystem, +) from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator from gym_electric_motor import physical_systems as ps @@ -11,87 +17,102 @@ class ContSpeedControlSquirrelCageInductionMotorEnv(ElectricMotorEnvironment): """ - Description: - Environment to simulate an abc-domain cont. control set speed controlled squirrel cage induction motor. - - Key: - ``'Cont-SC-SCIM-v0'`` - - Default Components: - - Supply: :py:class:`.IdealVoltageSupply` - - Converter: :py:class:`.ContB6BridgeConverter` - - Motor: :py:class:`.SquirrelCageInductionMotor` - - Load: :py:class:`.PolynomialStaticLoad` - - Ode-Solver: :py:class:`.EulerSolver` - - - Reference Generator: :py:class:`.WienerProcessReferenceGenerator` *Reference Quantity:* ``'omega'`` - - - Reward Function: :py:class:`.WeightedSumOfErrors` reward_weights: ``'omega' = 1.0`` - - - Visualization: :py:class:`.MotorDashboard` speed and action plots - - - Constraints: :py:class:`.SquaredConstraint` on the currents ``'i_sd', 'i_sq'`` - - State Variables: - ``[ - 'omega' , 'torque', - 'i_sa', 'i_sb', 'i_sc', 'i_sd', 'i_sq', - 'u_sa', 'u_sb', 'u_sc', 'u_sd', 'u_sq', - 'epsilon', 'u_sup' - ]`` - - Reference Variables: - ``['omega']`` - - Control Cycle Time: - tau = 1e-4 seconds - - Observation Space: - Type: Tuple(State_Space, Reference_Space) - - State Space: - Box(low=14 * [-1], high=14 * [1]) - - Reference Space: - Box(low=[-1, -1], high=[1, 1]) - - Action Space: - Box(low=[-1, -1, -1], high=[1, 1, 1]) - - Initial State: - Zeros on all state variables. - - Example: - >>> import gym_electric_motor as gem - >>> from gym_electric_motor.reference_generators import LaplaceProcessReferenceGenerator - >>> - >>> # Select a different ode_solver with default parameters by passing a keystring - >>> my_overridden_solver = 'scipy.solve_ivp' - >>> - >>> # Update the default arguments to the voltage supply by passing a parameter dict - >>> my_changed_voltage_supply_args = {'u_nominal': 400.0} - >>> - >>> # Replace the reference generator by passing a new instance - >>> my_new_ref_gen_instance = LaplaceProcessReferenceGenerator( - ... reference_state='i_sq', - ... sigma_range=(1e-3, 1e-2) - ... ) - >>> env = gem.make( - ... 'Cont-SC-SCIM-v0', - ... voltage_supply=my_changed_voltage_supply_args, - ... ode_solver=my_overridden_solver, - ... reference_generator=my_new_ref_gen_instance - ... ) - >>> terminated = True - >>> for _ in range(1000): - >>> if terminated: - >>> state, reference = env.reset() - >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) + Description: + Environment to simulate an abc-domain cont. control set speed controlled squirrel cage induction motor. + + Key: + ``'Cont-SC-SCIM-v0'`` + + Default Components: + - Supply: :py:class:`.IdealVoltageSupply` + - Converter: :py:class:`.ContB6BridgeConverter` + - Motor: :py:class:`.SquirrelCageInductionMotor` + - Load: :py:class:`.PolynomialStaticLoad` + - Ode-Solver: :py:class:`.EulerSolver` + + - Reference Generator: :py:class:`.WienerProcessReferenceGenerator` *Reference Quantity:* ``'omega'`` + + - Reward Function: :py:class:`.WeightedSumOfErrors` reward_weights: ``'omega' = 1.0`` + + - Visualization: :py:class:`.MotorDashboard` speed and action plots + + - Constraints: :py:class:`.SquaredConstraint` on the currents ``'i_sd', 'i_sq'`` + + State Variables: + ``[ + 'omega' , 'torque', + 'i_sa', 'i_sb', 'i_sc', 'i_sd', 'i_sq', + 'u_sa', 'u_sb', 'u_sc', 'u_sd', 'u_sq', + 'epsilon', 'u_sup' + ]`` + + Reference Variables: + ``['omega']`` + + Control Cycle Time: + tau = 1e-4 seconds + + Observation Space: + Type: Tuple(State_Space, Reference_Space) + + State Space: + Box(low=14 * [-1], high=14 * [1]) + + Reference Space: + Box(low=[-1, -1], high=[1, 1]) + + Action Space: + Box(low=[-1, -1, -1], high=[1, 1, 1]) + + Initial State: + Zeros on all state variables. + + Example: + >>> import gym_electric_motor as gem + >>> from gym_electric_motor.reference_generators import LaplaceProcessReferenceGenerator + >>> + >>> # Select a different ode_solver with default parameters by passing a keystring + >>> my_overridden_solver = 'scipy.solve_ivp' + >>> + >>> # Update the default arguments to the voltage supply by passing a parameter dict + >>> my_changed_voltage_supply_args = {'u_nominal': 400.0} + >>> + >>> # Replace the reference generator by passing a new instance + >>> my_new_ref_gen_instance = LaplaceProcessReferenceGenerator( + ... reference_state='i_sq', + ... sigma_range=(1e-3, 1e-2) + ... ) + >>> env = gem.make( + ... 'Cont-SC-SCIM-v0', + ... voltage_supply=my_changed_voltage_supply_args, + ... ode_solver=my_overridden_solver, + ... reference_generator=my_new_ref_gen_instance + ... ) + >>> terminated = True + >>> for _ in range(1000): + >>> if terminated: + >>> state, reference = env.reset() + >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - physical_system_wrappers=(), constraints=(SquaredConstraint(('i_sq', 'i_sd')),), - calc_jacobian=True, tau=1e-4, **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + physical_system_wrappers=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")),), + calc_jacobian=True, + tau=1e-4, + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -130,27 +151,51 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve """ physical_system = SquirrelCageInductionMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict()), - motor=initialize(ps.ElectricMotor, motor, ps.SquirrelCageInductionMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.PolynomialStaticLoad, dict( - load_parameter=dict(a=0.01, b=0.01, c=0.0) - )), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), + converter=initialize( + ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict() + ), + motor=initialize( + ps.ElectricMotor, motor, ps.SquirrelCageInductionMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, + load, + ps.PolynomialStaticLoad, + dict(load_parameter=dict(a=0.01, b=0.01, c=0.0)), + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, - dict(reference_state='omega', sigma_range=(1e-3, 1e-2)), + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="omega", sigma_range=(1e-3, 1e-2)), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(omega=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(omega=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, dict(state_plots=('omega',), action_plots='all')) + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("omega",), action_plots="all"), + ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_tc_scim_env.py b/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_tc_scim_env.py index bdd32746..65e90033 100644 --- a/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_tc_scim_env.py +++ b/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_tc_scim_env.py @@ -1,8 +1,17 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization -from gym_electric_motor.physical_systems.physical_systems import SquirrelCageInductionMotorSystem +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) +from gym_electric_motor.physical_systems.physical_systems import ( + SquirrelCageInductionMotorSystem, +) from gym_electric_motor.visualization import MotorDashboard -from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator, MultipleReferenceGenerator +from gym_electric_motor.reference_generators import ( + WienerProcessReferenceGenerator, + MultipleReferenceGenerator, +) from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize @@ -11,87 +20,102 @@ class ContTorqueControlSquirrelCageInductionMotorEnv(ElectricMotorEnvironment): """ - Description: - Environment to simulate an abc-domain cont. control set torque controlled squirrel cage induction motor. - - Key: - ``'Cont-TC-SCIM-v0'`` - - Default Components: - - Supply: :py:class:`.IdealVoltageSupply` - - Converter: :py:class:`.ContB6BridgeConverter` - - Motor: :py:class:`.SquirrelCageInductionMotor` - - Load: :py:class:`.ConstantSpeedLoad` - - Ode-Solver: :py:class:`.EulerSolver` - - - Reference Generator: :py:class:`.WienerProcessReferenceGenerator` *Reference Quantity:* ``'torque'`` - - - Reward Function: :py:class:`.WeightedSumOfErrors` reward_weights: ``'torque' = 1.0`` - - - Visualization: :py:class:`.MotorDashboard` torque and action plots - - - Constraints: :py:class:`.SquaredConstraint` on the currents ``'i_sd', 'i_sq'`` - - State Variables: - ``[ - 'omega' , 'torque', - 'i_sa', 'i_sb', 'i_sc', 'i_sd', 'i_sq', - 'u_sa', 'u_sb', 'u_sc', 'u_sd', 'u_sq', - 'epsilon', 'u_sup' - ]`` - - Reference Variables: - ``['torque']`` - - Control Cycle Time: - tau = 1e-4 seconds - - Observation Space: - Type: Tuple(State_Space, Reference_Space) - - State Space: - Box(low=14 * [-1], high=14 * [1]) - - Reference Space: - Box(low=[-1, -1], high=[1, 1]) - - Action Space: - Box(low=[-1, -1, -1], high=[1, 1, 1]) - - Initial State: - Zeros on all state variables. - - Example: - >>> import gym_electric_motor as gem - >>> from gym_electric_motor.reference_generators import LaplaceProcessReferenceGenerator - >>> - >>> # Select a different ode_solver with default parameters by passing a keystring - >>> my_overridden_solver = 'scipy.solve_ivp' - >>> - >>> # Update the default arguments to the voltage supply by passing a parameter dict - >>> my_changed_voltage_supply_args = {'u_nominal': 400.0} - >>> - >>> # Replace the reference generator by passing a new instance - >>> my_new_ref_gen_instance = LaplaceProcessReferenceGenerator( - ... reference_state='i_sq', - ... sigma_range=(1e-3, 1e-2) - ... ) - >>> env = gem.make( - ... 'Cont-TC-SCIM-v0', - ... voltage_supply=my_changed_voltage_supply_args, - ... ode_solver=my_overridden_solver, - ... reference_generator=my_new_ref_gen_instance - ... ) - >>> terminated = True - >>> for _ in range(1000): - >>> if terminated: - >>> state, reference = env.reset() - >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) + Description: + Environment to simulate an abc-domain cont. control set torque controlled squirrel cage induction motor. + + Key: + ``'Cont-TC-SCIM-v0'`` + + Default Components: + - Supply: :py:class:`.IdealVoltageSupply` + - Converter: :py:class:`.ContB6BridgeConverter` + - Motor: :py:class:`.SquirrelCageInductionMotor` + - Load: :py:class:`.ConstantSpeedLoad` + - Ode-Solver: :py:class:`.EulerSolver` + + - Reference Generator: :py:class:`.WienerProcessReferenceGenerator` *Reference Quantity:* ``'torque'`` + + - Reward Function: :py:class:`.WeightedSumOfErrors` reward_weights: ``'torque' = 1.0`` + + - Visualization: :py:class:`.MotorDashboard` torque and action plots + + - Constraints: :py:class:`.SquaredConstraint` on the currents ``'i_sd', 'i_sq'`` + + State Variables: + ``[ + 'omega' , 'torque', + 'i_sa', 'i_sb', 'i_sc', 'i_sd', 'i_sq', + 'u_sa', 'u_sb', 'u_sc', 'u_sd', 'u_sq', + 'epsilon', 'u_sup' + ]`` + + Reference Variables: + ``['torque']`` + + Control Cycle Time: + tau = 1e-4 seconds + + Observation Space: + Type: Tuple(State_Space, Reference_Space) + + State Space: + Box(low=14 * [-1], high=14 * [1]) + + Reference Space: + Box(low=[-1, -1], high=[1, 1]) + + Action Space: + Box(low=[-1, -1, -1], high=[1, 1, 1]) + + Initial State: + Zeros on all state variables. + + Example: + >>> import gym_electric_motor as gem + >>> from gym_electric_motor.reference_generators import LaplaceProcessReferenceGenerator + >>> + >>> # Select a different ode_solver with default parameters by passing a keystring + >>> my_overridden_solver = 'scipy.solve_ivp' + >>> + >>> # Update the default arguments to the voltage supply by passing a parameter dict + >>> my_changed_voltage_supply_args = {'u_nominal': 400.0} + >>> + >>> # Replace the reference generator by passing a new instance + >>> my_new_ref_gen_instance = LaplaceProcessReferenceGenerator( + ... reference_state='i_sq', + ... sigma_range=(1e-3, 1e-2) + ... ) + >>> env = gem.make( + ... 'Cont-TC-SCIM-v0', + ... voltage_supply=my_changed_voltage_supply_args, + ... ode_solver=my_overridden_solver, + ... reference_generator=my_new_ref_gen_instance + ... ) + >>> terminated = True + >>> for _ in range(1000): + >>> if terminated: + >>> state, reference = env.reset() + >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=(SquaredConstraint(('i_sq', 'i_sd')),), calc_jacobian=True, tau=1e-4, - physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")),), + calc_jacobian=True, + tau=1e-4, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -129,10 +153,18 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = SquirrelCageInductionMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict()), - motor=initialize(ps.ElectricMotor, motor, ps.SquirrelCageInductionMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), + converter=initialize( + ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict() + ), + motor=initialize( + ps.ElectricMotor, motor, ps.SquirrelCageInductionMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, @@ -141,19 +173,28 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, - dict(reference_state='torque') + dict(reference_state="torque"), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(torque=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(torque=1.0)), ) visualization = initialize( ElectricMotorVisualization, visualization, MotorDashboard, - dict(state_plots=('torque',), action_plots='all') + dict(state_plots=("torque",), action_plots="all"), ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_cc_scim_env.py b/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_cc_scim_env.py index f98f0ab7..f3fb5d73 100644 --- a/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_cc_scim_env.py +++ b/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_cc_scim_env.py @@ -1,8 +1,17 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization -from gym_electric_motor.physical_systems.physical_systems import SquirrelCageInductionMotorSystem +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) +from gym_electric_motor.physical_systems.physical_systems import ( + SquirrelCageInductionMotorSystem, +) from gym_electric_motor.visualization import MotorDashboard -from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator, MultipleReferenceGenerator +from gym_electric_motor.reference_generators import ( + WienerProcessReferenceGenerator, + MultipleReferenceGenerator, +) from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize @@ -11,87 +20,102 @@ class FiniteCurrentControlSquirrelCageInductionMotorEnv(ElectricMotorEnvironment): """ - Description: - Environment to simulate a finite control set current controlled squirrel cage induction motor. - - Key: - ``'Finite-CC-SCIM-v0'`` - - Default Components: - - Supply: :py:class:`.IdealVoltageSupply` - - Converter: :py:class:`.ContB6BridgeConverter` - - Motor: :py:class:`.SquirrelCageInductionMotor` - - Load: :py:class:`.ConstantSpeedLoad` - - Ode-Solver: :py:class:`.EulerSolver` - - - Reference Generator: :py:class:`.WienerProcessReferenceGenerator` *Reference Quantity:* ``'i_sd', 'i_sq`` - - - Reward Function: :py:class:`.WeightedSumOfErrors` reward_weights: ``'i_sd' = 0.5, 'i_sq' = 0.5`` - - - Visualization: :py:class:`.MotorDashboard` current and action plots - - - Constraints: :py:class:`.SquaredConstraint` on the currents ``'i_sd', 'i_sq'`` - - State Variables: - ``[ - 'omega' , 'torque', - 'i_sa', 'i_sb', 'i_sc', 'i_sd', 'i_sq', - 'u_sa', 'u_sb', 'u_sc', 'u_sd', 'u_sq', - 'epsilon', 'u_sup' - ]`` - - Reference Variables: - ``['i_sd', 'i_sq']`` - - Control Cycle Time: - tau = 1e-5 seconds - - Observation Space: - Type: Tuple(State_Space, Reference_Space) - - State Space: - Box(low=14 * [-1], high=14 * [1]) - - Reference Space: - Box(low=[-1, -1], high=[1, 1]) - - Action Space: - Discrete(8) - - Initial State: - Zeros on all state variables. - - Example: - >>> import gym_electric_motor as gem - >>> from gym_electric_motor.reference_generators import LaplaceProcessReferenceGenerator - >>> - >>> # Select a different ode_solver with default parameters by passing a keystring - >>> my_overridden_solver = 'scipy.solve_ivp' - >>> - >>> # Update the default arguments to the voltage supply by passing a parameter dict - >>> my_changed_voltage_supply_args = {'u_nominal': 400.0} - >>> - >>> # Replace the reference generator by passing a new instance - >>> my_new_ref_gen_instance = LaplaceProcessReferenceGenerator( - ... reference_state='i_sq', - ... sigma_range=(1e-3, 1e-2) - ... ) - >>> env = gem.make( - ... 'Finite-CC-SCIM-v0', - ... voltage_supply=my_changed_voltage_supply_args, - ... ode_solver=my_overridden_solver, - ... reference_generator=my_new_ref_gen_instance - ... ) - >>> terminated = True - >>> for _ in range(1000): - >>> if terminated: - >>> state, reference = env.reset() - >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) + Description: + Environment to simulate a finite control set current controlled squirrel cage induction motor. + + Key: + ``'Finite-CC-SCIM-v0'`` + + Default Components: + - Supply: :py:class:`.IdealVoltageSupply` + - Converter: :py:class:`.ContB6BridgeConverter` + - Motor: :py:class:`.SquirrelCageInductionMotor` + - Load: :py:class:`.ConstantSpeedLoad` + - Ode-Solver: :py:class:`.EulerSolver` + + - Reference Generator: :py:class:`.WienerProcessReferenceGenerator` *Reference Quantity:* ``'i_sd', 'i_sq`` + + - Reward Function: :py:class:`.WeightedSumOfErrors` reward_weights: ``'i_sd' = 0.5, 'i_sq' = 0.5`` + + - Visualization: :py:class:`.MotorDashboard` current and action plots + + - Constraints: :py:class:`.SquaredConstraint` on the currents ``'i_sd', 'i_sq'`` + + State Variables: + ``[ + 'omega' , 'torque', + 'i_sa', 'i_sb', 'i_sc', 'i_sd', 'i_sq', + 'u_sa', 'u_sb', 'u_sc', 'u_sd', 'u_sq', + 'epsilon', 'u_sup' + ]`` + + Reference Variables: + ``['i_sd', 'i_sq']`` + + Control Cycle Time: + tau = 1e-5 seconds + + Observation Space: + Type: Tuple(State_Space, Reference_Space) + + State Space: + Box(low=14 * [-1], high=14 * [1]) + + Reference Space: + Box(low=[-1, -1], high=[1, 1]) + + Action Space: + Discrete(8) + + Initial State: + Zeros on all state variables. + + Example: + >>> import gym_electric_motor as gem + >>> from gym_electric_motor.reference_generators import LaplaceProcessReferenceGenerator + >>> + >>> # Select a different ode_solver with default parameters by passing a keystring + >>> my_overridden_solver = 'scipy.solve_ivp' + >>> + >>> # Update the default arguments to the voltage supply by passing a parameter dict + >>> my_changed_voltage_supply_args = {'u_nominal': 400.0} + >>> + >>> # Replace the reference generator by passing a new instance + >>> my_new_ref_gen_instance = LaplaceProcessReferenceGenerator( + ... reference_state='i_sq', + ... sigma_range=(1e-3, 1e-2) + ... ) + >>> env = gem.make( + ... 'Finite-CC-SCIM-v0', + ... voltage_supply=my_changed_voltage_supply_args, + ... ode_solver=my_overridden_solver, + ... reference_generator=my_new_ref_gen_instance + ... ) + >>> terminated = True + >>> for _ in range(1000): + >>> if terminated: + >>> state, reference = env.reset() + >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=(SquaredConstraint(('i_sq', 'i_sd')),), calc_jacobian=True, tau=1e-5, - physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")),), + calc_jacobian=True, + tau=1e-5, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -129,36 +153,56 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ default_sub_generators = ( - WienerProcessReferenceGenerator(reference_state='i_sd'), - WienerProcessReferenceGenerator(reference_state='i_sq') + WienerProcessReferenceGenerator(reference_state="i_sd"), + WienerProcessReferenceGenerator(reference_state="i_sq"), ) physical_system = SquirrelCageInductionMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.FiniteB6BridgeConverter, dict()), - motor=initialize(ps.ElectricMotor, motor, ps.SquirrelCageInductionMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.FiniteB6BridgeConverter, + dict(), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.SquirrelCageInductionMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( ReferenceGenerator, reference_generator, MultipleReferenceGenerator, - dict(sub_generators=default_sub_generators) + dict(sub_generators=default_sub_generators), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(i_sd=0.5, i_sq=0.5)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(i_sd=0.5, i_sq=0.5)), ) visualization = initialize( ElectricMotorVisualization, visualization, MotorDashboard, - dict(state_plots=('i_sd', 'i_sq'), action_plots='all') + dict(state_plots=("i_sd", "i_sq"), action_plots="all"), ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_sc_scim_env.py b/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_sc_scim_env.py index 8232ce9b..be1987c7 100644 --- a/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_sc_scim_env.py +++ b/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_sc_scim_env.py @@ -1,6 +1,12 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization -from gym_electric_motor.physical_systems.physical_systems import SquirrelCageInductionMotorSystem +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) +from gym_electric_motor.physical_systems.physical_systems import ( + SquirrelCageInductionMotorSystem, +) from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator from gym_electric_motor import physical_systems as ps @@ -11,87 +17,102 @@ class FiniteSpeedControlSquirrelCageInductionMotorEnv(ElectricMotorEnvironment): """ - Description: - Environment to simulate a finite control set speed controlled squirrel cage induction motor. - - Key: - ``'Finite-SC-SCIM-v0'`` - - Default Components: - - Supply: :py:class:`.IdealVoltageSupply` - - Converter: :py:class:`.ContB6BridgeConverter` - - Motor: :py:class:`.SquirrelCageInductionMotor` - - Load: :py:class:`.PolynomialStaticLoad` - - Ode-Solver: :py:class:`.EulerSolver` - - - Reference Generator: :py:class:`.WienerProcessReferenceGenerator` *Reference Quantity:* ``'omega'`` - - - Reward Function: :py:class:`.WeightedSumOfErrors` reward_weights: ``'omega' = 1.0`` - - - Visualization: :py:class:`.MotorDashboard` speed and action plots - - - Constraints: :py:class:`.SquaredConstraint` on the currents ``'i_sd', 'i_sq'`` - - State Variables: - ``[ - 'omega' , 'torque', - 'i_sa', 'i_sb', 'i_sc', 'i_sd', 'i_sq', - 'u_sa', 'u_sb', 'u_sc', 'u_sd', 'u_sq', - 'epsilon', 'u_sup' - ]`` - - Reference Variables: - ``['omega']`` - - Control Cycle Time: - tau = 1e-5 seconds - - Observation Space: - Type: Tuple(State_Space, Reference_Space) - - State Space: - Box(low=14 * [-1], high=14 * [1]) - - Reference Space: - Box(low=[-1], high=[1]) - - Action Space: - Discrete(8) - - Initial State: - Zeros on all state variables. - - Example: - >>> import gym_electric_motor as gem - >>> from gym_electric_motor.reference_generators import LaplaceProcessReferenceGenerator - >>> - >>> # Select a different ode_solver with default parameters by passing a keystring - >>> my_overridden_solver = 'scipy.solve_ivp' - >>> - >>> # Update the default arguments to the voltage supply by passing a parameter dict - >>> my_changed_voltage_supply_args = {'u_nominal': 400.0} - >>> - >>> # Replace the reference generator by passing a new instance - >>> my_new_ref_gen_instance = LaplaceProcessReferenceGenerator( - ... reference_state='i_sq', - ... sigma_range=(1e-3, 1e-2) - ... ) - >>> env = gem.make( - ... 'Finite-CC-SCIM-v0', - ... voltage_supply=my_changed_voltage_supply_args, - ... ode_solver=my_overridden_solver, - ... reference_generator=my_new_ref_gen_instance - ... ) - >>> terminated = True - >>> for _ in range(1000): - >>> if terminated: - >>> state, reference = env.reset() - >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) + Description: + Environment to simulate a finite control set speed controlled squirrel cage induction motor. + + Key: + ``'Finite-SC-SCIM-v0'`` + + Default Components: + - Supply: :py:class:`.IdealVoltageSupply` + - Converter: :py:class:`.ContB6BridgeConverter` + - Motor: :py:class:`.SquirrelCageInductionMotor` + - Load: :py:class:`.PolynomialStaticLoad` + - Ode-Solver: :py:class:`.EulerSolver` + + - Reference Generator: :py:class:`.WienerProcessReferenceGenerator` *Reference Quantity:* ``'omega'`` + + - Reward Function: :py:class:`.WeightedSumOfErrors` reward_weights: ``'omega' = 1.0`` + + - Visualization: :py:class:`.MotorDashboard` speed and action plots + + - Constraints: :py:class:`.SquaredConstraint` on the currents ``'i_sd', 'i_sq'`` + + State Variables: + ``[ + 'omega' , 'torque', + 'i_sa', 'i_sb', 'i_sc', 'i_sd', 'i_sq', + 'u_sa', 'u_sb', 'u_sc', 'u_sd', 'u_sq', + 'epsilon', 'u_sup' + ]`` + + Reference Variables: + ``['omega']`` + + Control Cycle Time: + tau = 1e-5 seconds + + Observation Space: + Type: Tuple(State_Space, Reference_Space) + + State Space: + Box(low=14 * [-1], high=14 * [1]) + + Reference Space: + Box(low=[-1], high=[1]) + + Action Space: + Discrete(8) + + Initial State: + Zeros on all state variables. + + Example: + >>> import gym_electric_motor as gem + >>> from gym_electric_motor.reference_generators import LaplaceProcessReferenceGenerator + >>> + >>> # Select a different ode_solver with default parameters by passing a keystring + >>> my_overridden_solver = 'scipy.solve_ivp' + >>> + >>> # Update the default arguments to the voltage supply by passing a parameter dict + >>> my_changed_voltage_supply_args = {'u_nominal': 400.0} + >>> + >>> # Replace the reference generator by passing a new instance + >>> my_new_ref_gen_instance = LaplaceProcessReferenceGenerator( + ... reference_state='i_sq', + ... sigma_range=(1e-3, 1e-2) + ... ) + >>> env = gem.make( + ... 'Finite-CC-SCIM-v0', + ... voltage_supply=my_changed_voltage_supply_args, + ... ode_solver=my_overridden_solver, + ... reference_generator=my_new_ref_gen_instance + ... ) + >>> terminated = True + >>> for _ in range(1000): + >>> if terminated: + >>> state, reference = env.reset() + >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=(SquaredConstraint(('i_sq', 'i_sd')),), calc_jacobian=True, tau=1e-5, - physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")),), + calc_jacobian=True, + tau=1e-5, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -130,27 +151,54 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve """ physical_system = SquirrelCageInductionMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.FiniteB6BridgeConverter, dict()), - motor=initialize(ps.ElectricMotor, motor, ps.SquirrelCageInductionMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.PolynomialStaticLoad, dict( - load_parameter=dict(a=0.01, b=0.01, c=0.0) - )), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.FiniteB6BridgeConverter, + dict(), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.SquirrelCageInductionMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, + load, + ps.PolynomialStaticLoad, + dict(load_parameter=dict(a=0.01, b=0.01, c=0.0)), + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, - dict(reference_state='omega', sigma_range=(1e-3, 1e-2)), + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="omega", sigma_range=(1e-3, 1e-2)), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(omega=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(omega=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, dict(state_plots=('omega',), action_plots='all')) + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("omega",), action_plots="all"), + ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_tc_scim_env.py b/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_tc_scim_env.py index 7fa8846d..cf1fa749 100644 --- a/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_tc_scim_env.py +++ b/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_tc_scim_env.py @@ -1,6 +1,12 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization -from gym_electric_motor.physical_systems.physical_systems import SquirrelCageInductionMotorSystem +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) +from gym_electric_motor.physical_systems.physical_systems import ( + SquirrelCageInductionMotorSystem, +) from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator from gym_electric_motor import physical_systems as ps @@ -11,87 +17,102 @@ class FiniteTorqueControlSquirrelCageInductionMotorEnv(ElectricMotorEnvironment): """ - Description: - Environment to simulate a finite control set torque controlled squirrel cage induction motor. - - Key: - ``'Finite-TC-SCIM-v0'`` - - Default Components: - - Supply: :py:class:`.IdealVoltageSupply` - - Converter: :py:class:`.ContB6BridgeConverter` - - Motor: :py:class:`.SquirrelCageInductionMotor` - - Load: :py:class:`.ConstantSpeedLoad` - - Ode-Solver: :py:class:`.EulerSolver` - - - Reference Generator: :py:class:`.WienerProcessReferenceGenerator` *Reference Quantity:* ``'torque'`` - - - Reward Function: :py:class:`.WeightedSumOfErrors` reward_weights: ``'torque' = 1.0`` - - - Visualization: :py:class:`.MotorDashboard` torque and action plots - - - Constraints: :py:class:`.SquaredConstraint` on the currents ``'i_sd', 'i_sq'`` - - State Variables: - ``[ - 'omega' , 'torque', - 'i_sa', 'i_sb', 'i_sc', 'i_sd', 'i_sq', - 'u_sa', 'u_sb', 'u_sc', 'u_sd', 'u_sq', - 'epsilon', 'u_sup' - ]`` - - Reference Variables: - ``['torque']`` - - Control Cycle Time: - tau = 1e-5 seconds - - Observation Space: - Type: Tuple(State_Space, Reference_Space) - - State Space: - Box(low=14 * [-1], high=14 * [1]) - - Reference Space: - Box(low=[-1], high=[1]) - - Action Space: - Discrete(8) - - Initial State: - Zeros on all state variables. - - Example: - >>> import gym_electric_motor as gem - >>> from gym_electric_motor.reference_generators import LaplaceProcessReferenceGenerator - >>> - >>> # Select a different ode_solver with default parameters by passing a keystring - >>> my_overridden_solver = 'scipy.solve_ivp' - >>> - >>> # Update the default arguments to the voltage supply by passing a parameter dict - >>> my_changed_voltage_supply_args = {'u_nominal': 400.0} - >>> - >>> # Replace the reference generator by passing a new instance - >>> my_new_ref_gen_instance = LaplaceProcessReferenceGenerator( - ... reference_state='i_sq', - ... sigma_range=(1e-3, 1e-2) - ... ) - >>> env = gem.make( - ... 'Finite-TC-SCIM-v0', - ... voltage_supply=my_changed_voltage_supply_args, - ... ode_solver=my_overridden_solver, - ... reference_generator=my_new_ref_gen_instance - ... ) - >>> terminated = True - >>> for _ in range(1000): - >>> if terminated: - >>> state, reference = env.reset() - >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) + Description: + Environment to simulate a finite control set torque controlled squirrel cage induction motor. + + Key: + ``'Finite-TC-SCIM-v0'`` + + Default Components: + - Supply: :py:class:`.IdealVoltageSupply` + - Converter: :py:class:`.ContB6BridgeConverter` + - Motor: :py:class:`.SquirrelCageInductionMotor` + - Load: :py:class:`.ConstantSpeedLoad` + - Ode-Solver: :py:class:`.EulerSolver` + + - Reference Generator: :py:class:`.WienerProcessReferenceGenerator` *Reference Quantity:* ``'torque'`` + + - Reward Function: :py:class:`.WeightedSumOfErrors` reward_weights: ``'torque' = 1.0`` + + - Visualization: :py:class:`.MotorDashboard` torque and action plots + + - Constraints: :py:class:`.SquaredConstraint` on the currents ``'i_sd', 'i_sq'`` + + State Variables: + ``[ + 'omega' , 'torque', + 'i_sa', 'i_sb', 'i_sc', 'i_sd', 'i_sq', + 'u_sa', 'u_sb', 'u_sc', 'u_sd', 'u_sq', + 'epsilon', 'u_sup' + ]`` + + Reference Variables: + ``['torque']`` + + Control Cycle Time: + tau = 1e-5 seconds + + Observation Space: + Type: Tuple(State_Space, Reference_Space) + + State Space: + Box(low=14 * [-1], high=14 * [1]) + + Reference Space: + Box(low=[-1], high=[1]) + + Action Space: + Discrete(8) + + Initial State: + Zeros on all state variables. + + Example: + >>> import gym_electric_motor as gem + >>> from gym_electric_motor.reference_generators import LaplaceProcessReferenceGenerator + >>> + >>> # Select a different ode_solver with default parameters by passing a keystring + >>> my_overridden_solver = 'scipy.solve_ivp' + >>> + >>> # Update the default arguments to the voltage supply by passing a parameter dict + >>> my_changed_voltage_supply_args = {'u_nominal': 400.0} + >>> + >>> # Replace the reference generator by passing a new instance + >>> my_new_ref_gen_instance = LaplaceProcessReferenceGenerator( + ... reference_state='i_sq', + ... sigma_range=(1e-3, 1e-2) + ... ) + >>> env = gem.make( + ... 'Finite-TC-SCIM-v0', + ... voltage_supply=my_changed_voltage_supply_args, + ... ode_solver=my_overridden_solver, + ... reference_generator=my_new_ref_gen_instance + ... ) + >>> terminated = True + >>> for _ in range(1000): + >>> if terminated: + >>> state, reference = env.reset() + >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=(SquaredConstraint(('i_sq', 'i_sd')),), calc_jacobian=True, tau=1e-5, - physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")),), + calc_jacobian=True, + tau=1e-5, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -129,25 +150,51 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = SquirrelCageInductionMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.FiniteB6BridgeConverter, dict()), - motor=initialize(ps.ElectricMotor, motor, ps.SquirrelCageInductionMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.FiniteB6BridgeConverter, + dict(), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.SquirrelCageInductionMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, dict(reference_state='torque') + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="torque"), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(torque=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(torque=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, dict(state_plots=('torque',), action_plots='all') + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("torque",), action_plots="all"), ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_pmsm/cont_cc_pmsm_env.py b/gym_electric_motor/envs/gym_pmsm/cont_cc_pmsm_env.py index ec0f07cc..c8c2e898 100644 --- a/gym_electric_motor/envs/gym_pmsm/cont_cc_pmsm_env.py +++ b/gym_electric_motor/envs/gym_pmsm/cont_cc_pmsm_env.py @@ -1,8 +1,15 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import SynchronousMotorSystem from gym_electric_motor.visualization import MotorDashboard -from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator, MultipleReferenceGenerator +from gym_electric_motor.reference_generators import ( + WienerProcessReferenceGenerator, + MultipleReferenceGenerator, +) from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize @@ -83,10 +90,25 @@ class ContCurrentControlPermanentMagnetSynchronousMotorEnv(ElectricMotorEnvironm >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=(SquaredConstraint(('i_sq', 'i_sd')),), calc_jacobian=True, tau=1e-4, - physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")),), + calc_jacobian=True, + tau=1e-4, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -124,15 +146,23 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ default_subgenerators = ( - WienerProcessReferenceGenerator(reference_state='i_sd'), - WienerProcessReferenceGenerator(reference_state='i_sq') + WienerProcessReferenceGenerator(reference_state="i_sd"), + WienerProcessReferenceGenerator(reference_state="i_sq"), ) physical_system = SynchronousMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=300.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict()), - motor=initialize(ps.ElectricMotor, motor, ps.PermanentMagnetSynchronousMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=300.0) + ), + converter=initialize( + ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict() + ), + motor=initialize( + ps.ElectricMotor, motor, ps.PermanentMagnetSynchronousMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, @@ -141,19 +171,28 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve ReferenceGenerator, reference_generator, MultipleReferenceGenerator, - dict(sub_generators=default_subgenerators) + dict(sub_generators=default_subgenerators), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(i_sd=0.5, i_sq=0.5)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(i_sd=0.5, i_sq=0.5)), ) visualization = initialize( ElectricMotorVisualization, visualization, MotorDashboard, - dict(state_plots=('i_sd', 'i_sq'), action_plots='all') + dict(state_plots=("i_sd", "i_sq"), action_plots="all"), ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_pmsm/cont_sc_pmsm_env.py b/gym_electric_motor/envs/gym_pmsm/cont_sc_pmsm_env.py index 83be801c..408fd8b7 100644 --- a/gym_electric_motor/envs/gym_pmsm/cont_sc_pmsm_env.py +++ b/gym_electric_motor/envs/gym_pmsm/cont_sc_pmsm_env.py @@ -1,5 +1,9 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import SynchronousMotorSystem from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator @@ -83,10 +87,25 @@ class ContSpeedControlPermanentMagnetSynchronousMotorEnv(ElectricMotorEnvironmen >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=(SquaredConstraint(('i_sq', 'i_sd')),), calc_jacobian=True, tau=1e-4, - physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")),), + calc_jacobian=True, + tau=1e-4, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -124,26 +143,51 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = SynchronousMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict()), - motor=initialize(ps.ElectricMotor, motor, ps.PermanentMagnetSynchronousMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.PolynomialStaticLoad, dict( - load_parameter=dict(a=0.01, b=0.01, c=0.0) - )), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), + converter=initialize( + ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict() + ), + motor=initialize( + ps.ElectricMotor, motor, ps.PermanentMagnetSynchronousMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, + load, + ps.PolynomialStaticLoad, + dict(load_parameter=dict(a=0.01, b=0.01, c=0.0)), + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, dict(reference_state='omega') + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="omega"), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(omega=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(omega=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, dict(state_plots=('omega',), action_plots='all')) + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("omega",), action_plots="all"), + ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_pmsm/cont_tc_pmsm_env.py b/gym_electric_motor/envs/gym_pmsm/cont_tc_pmsm_env.py index e9a6d3e3..89e17db6 100644 --- a/gym_electric_motor/envs/gym_pmsm/cont_tc_pmsm_env.py +++ b/gym_electric_motor/envs/gym_pmsm/cont_tc_pmsm_env.py @@ -1,8 +1,15 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import SynchronousMotorSystem from gym_electric_motor.visualization import MotorDashboard -from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator, MultipleReferenceGenerator +from gym_electric_motor.reference_generators import ( + WienerProcessReferenceGenerator, + MultipleReferenceGenerator, +) from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize @@ -83,10 +90,25 @@ class ContTorqueControlPermanentMagnetSynchronousMotorEnv(ElectricMotorEnvironme >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=(SquaredConstraint(('i_sq', 'i_sd')),), calc_jacobian=True, tau=1e-4, - physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")),), + calc_jacobian=True, + tau=1e-4, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -124,10 +146,18 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = SynchronousMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict()), - motor=initialize(ps.ElectricMotor, motor, ps.PermanentMagnetSynchronousMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), + converter=initialize( + ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict() + ), + motor=initialize( + ps.ElectricMotor, motor, ps.PermanentMagnetSynchronousMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, @@ -136,19 +166,28 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, - dict(reference_state='torque') + dict(reference_state="torque"), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(torque=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(torque=1.0)), ) visualization = initialize( ElectricMotorVisualization, visualization, MotorDashboard, - dict(state_plots=('torque',), action_plots='all') + dict(state_plots=("torque",), action_plots="all"), ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_pmsm/finite_cc_pmsm_env.py b/gym_electric_motor/envs/gym_pmsm/finite_cc_pmsm_env.py index a4b78982..46f55259 100644 --- a/gym_electric_motor/envs/gym_pmsm/finite_cc_pmsm_env.py +++ b/gym_electric_motor/envs/gym_pmsm/finite_cc_pmsm_env.py @@ -1,8 +1,15 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import SynchronousMotorSystem from gym_electric_motor.visualization import MotorDashboard -from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator, MultipleReferenceGenerator +from gym_electric_motor.reference_generators import ( + WienerProcessReferenceGenerator, + MultipleReferenceGenerator, +) from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize @@ -83,10 +90,25 @@ class FiniteCurrentControlPermanentMagnetSynchronousMotorEnv(ElectricMotorEnviro >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=(SquaredConstraint(('i_sq', 'i_sd')),), calc_jacobian=True, tau=1e-5, - physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")),), + calc_jacobian=True, + tau=1e-5, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -124,36 +146,56 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ default_sub_generators = ( - WienerProcessReferenceGenerator(reference_state='i_sd'), - WienerProcessReferenceGenerator(reference_state='i_sq') + WienerProcessReferenceGenerator(reference_state="i_sd"), + WienerProcessReferenceGenerator(reference_state="i_sq"), ) physical_system = SynchronousMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.FiniteB6BridgeConverter, dict()), - motor=initialize(ps.ElectricMotor, motor, ps.PermanentMagnetSynchronousMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.FiniteB6BridgeConverter, + dict(), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.PermanentMagnetSynchronousMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( ReferenceGenerator, reference_generator, MultipleReferenceGenerator, - dict(sub_generators=default_sub_generators) + dict(sub_generators=default_sub_generators), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(i_sd=0.5, i_sq=0.5)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(i_sd=0.5, i_sq=0.5)), ) visualization = initialize( ElectricMotorVisualization, visualization, MotorDashboard, - dict(state_plots=('i_sd', 'i_sq'), action_plots='all') + dict(state_plots=("i_sd", "i_sq"), action_plots="all"), ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_pmsm/finite_sc_pmsm_env.py b/gym_electric_motor/envs/gym_pmsm/finite_sc_pmsm_env.py index 041f9078..1dc0e60e 100644 --- a/gym_electric_motor/envs/gym_pmsm/finite_sc_pmsm_env.py +++ b/gym_electric_motor/envs/gym_pmsm/finite_sc_pmsm_env.py @@ -1,5 +1,9 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import SynchronousMotorSystem from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator @@ -83,10 +87,25 @@ class FiniteSpeedControlPermanentMagnetSynchronousMotorEnv(ElectricMotorEnvironm >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=(SquaredConstraint(('i_sq', 'i_sd')),), calc_jacobian=True, tau=1e-5, - physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")),), + calc_jacobian=True, + tau=1e-5, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -124,26 +143,54 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = SynchronousMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.FiniteB6BridgeConverter, dict()), - motor=initialize(ps.ElectricMotor, motor, ps.PermanentMagnetSynchronousMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.PolynomialStaticLoad, dict( - load_parameter=dict(a=0.01, b=0.01, c=0.0) - )), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.FiniteB6BridgeConverter, + dict(), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.PermanentMagnetSynchronousMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, + load, + ps.PolynomialStaticLoad, + dict(load_parameter=dict(a=0.01, b=0.01, c=0.0)), + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, dict(reference_state='omega') + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="omega"), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(omega=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(omega=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, dict(state_plots=('omega',), action_plots='all')) + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("omega",), action_plots="all"), + ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_pmsm/finite_tc_pmsm_env.py b/gym_electric_motor/envs/gym_pmsm/finite_tc_pmsm_env.py index 6228d54a..64ff98cc 100644 --- a/gym_electric_motor/envs/gym_pmsm/finite_tc_pmsm_env.py +++ b/gym_electric_motor/envs/gym_pmsm/finite_tc_pmsm_env.py @@ -1,5 +1,9 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import SynchronousMotorSystem from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator @@ -83,10 +87,25 @@ class FiniteTorqueControlPermanentMagnetSynchronousMotorEnv(ElectricMotorEnviron >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=(SquaredConstraint(('i_sq', 'i_sd')),), calc_jacobian=True, tau=1e-5, - physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")),), + calc_jacobian=True, + tau=1e-5, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -124,25 +143,51 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = SynchronousMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.FiniteB6BridgeConverter, dict()), - motor=initialize(ps.ElectricMotor, motor, ps.PermanentMagnetSynchronousMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.FiniteB6BridgeConverter, + dict(), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.PermanentMagnetSynchronousMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, dict(reference_state='torque') + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="torque"), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(torque=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(torque=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, dict(state_plots=('torque',), action_plots='all') + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("torque",), action_plots="all"), ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_srm/srm_continuous_control_env.py b/gym_electric_motor/envs/gym_srm/srm_continuous_control_env.py index 5a012949..f7b9938c 100644 --- a/gym_electric_motor/envs/gym_srm/srm_continuous_control_env.py +++ b/gym_electric_motor/envs/gym_srm/srm_continuous_control_env.py @@ -2,7 +2,7 @@ class SRMContinuousControlEnv(gymnasium.Env): - metadata = {'render.modes': ['human']} + metadata = {"render.modes": ["human"]} def __init__(self): pass @@ -13,7 +13,7 @@ def step(self, action): def reset(self): raise NotImplementedError() - def render(self, mode='human'): + def render(self, mode="human"): raise NotImplementedError() def close(self): diff --git a/gym_electric_motor/envs/gym_srm/srm_finite_control_env.py b/gym_electric_motor/envs/gym_srm/srm_finite_control_env.py index cdbacd52..d2dbb1bc 100644 --- a/gym_electric_motor/envs/gym_srm/srm_finite_control_env.py +++ b/gym_electric_motor/envs/gym_srm/srm_finite_control_env.py @@ -2,7 +2,7 @@ class SRMFiniteControlEnv(gymnasium.Env): - metadata = {'render.modes': ['human']} + metadata = {"render.modes": ["human"]} def __init__(self): pass @@ -13,7 +13,7 @@ def step(self, action): def reset(self): raise NotImplementedError() - def render(self, mode='human'): + def render(self, mode="human"): raise NotImplementedError() def close(self): diff --git a/gym_electric_motor/envs/gym_synrm/cont_cc_synrm_env.py b/gym_electric_motor/envs/gym_synrm/cont_cc_synrm_env.py index 3b2b42a8..2dbd3e5e 100644 --- a/gym_electric_motor/envs/gym_synrm/cont_cc_synrm_env.py +++ b/gym_electric_motor/envs/gym_synrm/cont_cc_synrm_env.py @@ -1,8 +1,15 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import SynchronousMotorSystem from gym_electric_motor.visualization import MotorDashboard -from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator, MultipleReferenceGenerator +from gym_electric_motor.reference_generators import ( + WienerProcessReferenceGenerator, + MultipleReferenceGenerator, +) from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize @@ -83,10 +90,25 @@ class ContCurrentControlSynchronousReluctanceMotorEnv(ElectricMotorEnvironment): >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=(SquaredConstraint(('i_sq', 'i_sd')),), calc_jacobian=True, tau=1e-4, - physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")),), + calc_jacobian=True, + tau=1e-4, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -124,15 +146,23 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ default_sub_generators = ( - WienerProcessReferenceGenerator(reference_state='i_sd'), - WienerProcessReferenceGenerator(reference_state='i_sq') + WienerProcessReferenceGenerator(reference_state="i_sd"), + WienerProcessReferenceGenerator(reference_state="i_sq"), ) physical_system = SynchronousMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict()), - motor=initialize(ps.ElectricMotor, motor, ps.SynchronousReluctanceMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), + converter=initialize( + ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict() + ), + motor=initialize( + ps.ElectricMotor, motor, ps.SynchronousReluctanceMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, @@ -141,19 +171,28 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve ReferenceGenerator, reference_generator, MultipleReferenceGenerator, - dict(sub_generators=default_sub_generators) + dict(sub_generators=default_sub_generators), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(i_sd=0.5, i_sq=0.5)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(i_sd=0.5, i_sq=0.5)), ) visualization = initialize( ElectricMotorVisualization, visualization, MotorDashboard, - dict(state_plots=('i_sd', 'i_sq'), action_plots='all') + dict(state_plots=("i_sd", "i_sq"), action_plots="all"), ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_synrm/cont_sc_synrm_env.py b/gym_electric_motor/envs/gym_synrm/cont_sc_synrm_env.py index 32ac8682..e53641ce 100644 --- a/gym_electric_motor/envs/gym_synrm/cont_sc_synrm_env.py +++ b/gym_electric_motor/envs/gym_synrm/cont_sc_synrm_env.py @@ -1,5 +1,9 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import SynchronousMotorSystem from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator @@ -83,10 +87,25 @@ class ContSpeedControlSynchronousReluctanceMotorEnv(ElectricMotorEnvironment): >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=(SquaredConstraint(('i_sq', 'i_sd')),), calc_jacobian=True, tau=1e-4, - physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")),), + calc_jacobian=True, + tau=1e-4, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -124,27 +143,51 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = SynchronousMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict()), - motor=initialize(ps.ElectricMotor, motor, ps.SynchronousReluctanceMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.PolynomialStaticLoad, dict( - load_parameter=dict(a=0.01, b=0.01, c=0.0) - )), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), + converter=initialize( + ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict() + ), + motor=initialize( + ps.ElectricMotor, motor, ps.SynchronousReluctanceMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, + load, + ps.PolynomialStaticLoad, + dict(load_parameter=dict(a=0.01, b=0.01, c=0.0)), + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, - dict(reference_state='omega', sigma_range=(1e-3, 1e-2)), + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="omega", sigma_range=(1e-3, 1e-2)), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(omega=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(omega=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, dict(state_plots=('omega',), action_plots='all')) + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("omega",), action_plots="all"), + ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_synrm/cont_tc_synrm_env.py b/gym_electric_motor/envs/gym_synrm/cont_tc_synrm_env.py index e7365ae0..5fe42660 100644 --- a/gym_electric_motor/envs/gym_synrm/cont_tc_synrm_env.py +++ b/gym_electric_motor/envs/gym_synrm/cont_tc_synrm_env.py @@ -1,8 +1,15 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import SynchronousMotorSystem from gym_electric_motor.visualization import MotorDashboard -from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator, MultipleReferenceGenerator +from gym_electric_motor.reference_generators import ( + WienerProcessReferenceGenerator, + MultipleReferenceGenerator, +) from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize @@ -83,10 +90,25 @@ class ContTorqueControlSynchronousReluctanceMotorEnv(ElectricMotorEnvironment): >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=(SquaredConstraint(('i_sq', 'i_sd')),), calc_jacobian=True, tau=1e-4, - physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")),), + calc_jacobian=True, + tau=1e-4, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -124,10 +146,18 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = SynchronousMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict()), - motor=initialize(ps.ElectricMotor, motor, ps.SynchronousReluctanceMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), + converter=initialize( + ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict() + ), + motor=initialize( + ps.ElectricMotor, motor, ps.SynchronousReluctanceMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, @@ -136,19 +166,28 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, - dict(reference_state='torque') + dict(reference_state="torque"), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(torque=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(torque=1.0)), ) visualization = initialize( ElectricMotorVisualization, visualization, MotorDashboard, - dict(state_plots=('torque',), action_plots='all') + dict(state_plots=("torque",), action_plots="all"), ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_synrm/finite_cc_synrm_env.py b/gym_electric_motor/envs/gym_synrm/finite_cc_synrm_env.py index e79608dd..c041d8b4 100644 --- a/gym_electric_motor/envs/gym_synrm/finite_cc_synrm_env.py +++ b/gym_electric_motor/envs/gym_synrm/finite_cc_synrm_env.py @@ -1,8 +1,15 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import SynchronousMotorSystem from gym_electric_motor.visualization import MotorDashboard -from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator, MultipleReferenceGenerator +from gym_electric_motor.reference_generators import ( + WienerProcessReferenceGenerator, + MultipleReferenceGenerator, +) from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize @@ -83,10 +90,25 @@ class FiniteCurrentControlSynchronousReluctanceMotorEnv(ElectricMotorEnvironment >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=(SquaredConstraint(('i_sq', 'i_sd')),), calc_jacobian=True, tau=1e-5, - physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")),), + calc_jacobian=True, + tau=1e-5, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -124,36 +146,56 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ default_sub_generators = ( - WienerProcessReferenceGenerator(reference_state='i_sd'), - WienerProcessReferenceGenerator(reference_state='i_sq') + WienerProcessReferenceGenerator(reference_state="i_sd"), + WienerProcessReferenceGenerator(reference_state="i_sq"), ) physical_system = SynchronousMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.FiniteB6BridgeConverter, dict()), - motor=initialize(ps.ElectricMotor, motor, ps.SynchronousReluctanceMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.FiniteB6BridgeConverter, + dict(), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.SynchronousReluctanceMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( ReferenceGenerator, reference_generator, MultipleReferenceGenerator, - dict(sub_generators=default_sub_generators) + dict(sub_generators=default_sub_generators), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(i_sd=0.5, i_sq=0.5)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(i_sd=0.5, i_sq=0.5)), ) visualization = initialize( ElectricMotorVisualization, visualization, MotorDashboard, - dict(state_plots=('i_sd', 'i_sq'), action_plots='all') + dict(state_plots=("i_sd", "i_sq"), action_plots="all"), ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_synrm/finite_sc_synrm_env.py b/gym_electric_motor/envs/gym_synrm/finite_sc_synrm_env.py index 7ade4aec..3017e757 100644 --- a/gym_electric_motor/envs/gym_synrm/finite_sc_synrm_env.py +++ b/gym_electric_motor/envs/gym_synrm/finite_sc_synrm_env.py @@ -1,5 +1,9 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import SynchronousMotorSystem from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator @@ -83,10 +87,25 @@ class FiniteSpeedControlSynchronousReluctanceMotorEnv(ElectricMotorEnvironment): >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=(SquaredConstraint(('i_sq', 'i_sd')),), calc_jacobian=True, tau=1e-5, - physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")),), + calc_jacobian=True, + tau=1e-5, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -125,27 +144,54 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = SynchronousMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.FiniteB6BridgeConverter, dict()), - motor=initialize(ps.ElectricMotor, motor, ps.SynchronousReluctanceMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.PolynomialStaticLoad, dict( - load_parameter=dict(a=0.01, b=0.01, c=0.0) - )), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.FiniteB6BridgeConverter, + dict(), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.SynchronousReluctanceMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, + load, + ps.PolynomialStaticLoad, + dict(load_parameter=dict(a=0.01, b=0.01, c=0.0)), + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, - dict(reference_state='omega', sigma_range=(1e-3, 1e-2)), + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="omega", sigma_range=(1e-3, 1e-2)), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(omega=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(omega=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, dict(state_plots=('omega',), action_plots='all')) + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("omega",), action_plots="all"), + ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/envs/gym_synrm/finite_tc_synrm_env.py b/gym_electric_motor/envs/gym_synrm/finite_tc_synrm_env.py index 4336ad23..62174a6b 100644 --- a/gym_electric_motor/envs/gym_synrm/finite_tc_synrm_env.py +++ b/gym_electric_motor/envs/gym_synrm/finite_tc_synrm_env.py @@ -1,5 +1,9 @@ -from gym_electric_motor.core import ElectricMotorEnvironment, ReferenceGenerator, RewardFunction, \ - ElectricMotorVisualization +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + ReferenceGenerator, + RewardFunction, + ElectricMotorVisualization, +) from gym_electric_motor.physical_systems.physical_systems import SynchronousMotorSystem from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator @@ -83,10 +87,25 @@ class FiniteTorqueControlSynchronousReluctanceMotorEnv(ElectricMotorEnvironment) >>> state, reference = env.reset() >>> (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) """ - def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solver=None, - reward_function=None, reference_generator=None, visualization=None, state_filter=None, callbacks=(), - constraints=(SquaredConstraint(('i_sq', 'i_sd')),), calc_jacobian=True, tau=1e-5, - physical_system_wrappers=(), **kwargs): + + def __init__( + self, + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, + state_filter=None, + callbacks=(), + constraints=(SquaredConstraint(("i_sq", "i_sd")),), + calc_jacobian=True, + tau=1e-5, + physical_system_wrappers=(), + **kwargs, + ): """ Args: supply(env-arg): Specification of the :py:class:`.VoltageSupply` for the environment @@ -124,25 +143,51 @@ def __init__(self, supply=None, converter=None, motor=None, load=None, ode_solve The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = SynchronousMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), - converter=initialize(ps.PowerElectronicConverter, converter, ps.FiniteB6BridgeConverter, dict()), - motor=initialize(ps.ElectricMotor, motor, ps.SynchronousReluctanceMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), + supply=initialize( + ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) + ), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.FiniteB6BridgeConverter, + dict(), + ), + motor=initialize( + ps.ElectricMotor, motor, ps.SynchronousReluctanceMotor, dict() + ), + load=initialize( + ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) + ), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, - tau=tau + tau=tau, ) reference_generator = initialize( - ReferenceGenerator, reference_generator, WienerProcessReferenceGenerator, dict(reference_state='torque') + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="torque"), ) reward_function = initialize( - RewardFunction, reward_function, WeightedSumOfErrors, dict(reward_weights=dict(torque=1.0)) + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(torque=1.0)), ) visualization = initialize( - ElectricMotorVisualization, visualization, MotorDashboard, dict(state_plots=('torque',), action_plots='all') + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("torque",), action_plots="all"), ) super().__init__( - physical_system=physical_system, reference_generator=reference_generator, reward_function=reward_function, - constraints=constraints, visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=physical_system_wrappers, **kwargs + physical_system=physical_system, + reference_generator=reference_generator, + reward_function=reward_function, + constraints=constraints, + visualization=visualization, + state_filter=state_filter, + callbacks=callbacks, + physical_system_wrappers=physical_system_wrappers, + **kwargs, ) diff --git a/gym_electric_motor/physical_system_wrappers/cos_sin_processor.py b/gym_electric_motor/physical_system_wrappers/cos_sin_processor.py index bf2415cf..c332f81f 100644 --- a/gym_electric_motor/physical_system_wrappers/cos_sin_processor.py +++ b/gym_electric_motor/physical_system_wrappers/cos_sin_processor.py @@ -16,7 +16,7 @@ def angle(self): """Returns the name of the state whose cosine and sine are appended to the state vector.""" return self._angle - def __init__(self, angle='epsilon', physical_system=None, remove_angle=False): + def __init__(self, angle="epsilon", physical_system=None, remove_angle=False): """ Args: angle(string): Name of the state whose cosine and sine will be added to the systems state vector. @@ -36,15 +36,26 @@ def set_physical_system(self, physical_system): self._angle_index = physical_system.state_positions[self._angle] self._remove_idx = self._angle_index if self._remove_angle else [] - low = np.concatenate((np.delete(physical_system.state_space.low, self._remove_idx), [-1., -1.])) - high = np.concatenate((np.delete(physical_system.state_space.high, self._remove_idx), [1., 1.])) + low = np.concatenate( + (np.delete(physical_system.state_space.low, self._remove_idx), [-1.0, -1.0]) + ) + high = np.concatenate( + (np.delete(physical_system.state_space.high, self._remove_idx), [1.0, 1.0]) + ) self.state_space = gymnasium.spaces.Box(low, high, dtype=np.float64) - self._limits = np.concatenate((np.delete(physical_system.limits, self._remove_idx), [1., 1.])) - self._nominal_state = np.concatenate((np.delete(physical_system.nominal_state, self._remove_idx), [1., 1.])) - self._state_names = list(np.delete(physical_system.state_names, self._remove_idx)) \ - + [f'cos({self._angle})', f'sin({self._angle})'] - self._state_positions = {key: index for index, key in enumerate(self._state_names)} + self._limits = np.concatenate( + (np.delete(physical_system.limits, self._remove_idx), [1.0, 1.0]) + ) + self._nominal_state = np.concatenate( + (np.delete(physical_system.nominal_state, self._remove_idx), [1.0, 1.0]) + ) + self._state_names = list( + np.delete(physical_system.state_names, self._remove_idx) + ) + [f"cos({self._angle})", f"sin({self._angle})"] + self._state_positions = { + key: index for index, key in enumerate(self._state_names) + } return self def reset(self): @@ -67,7 +78,7 @@ def _delete_angle(self, state): Returns: numpy.ndarray[float]: The state vector removed by the angle. - """ + """ return np.delete(state, self._remove_idx) def _get_cos_sin(self, state): @@ -79,4 +90,9 @@ def _get_cos_sin(self, state): Returns: numpy.ndarray[float]: The state vector extended by cosine and sine. """ - return np.concatenate((state, [np.cos(state[self._angle_index]), np.sin(state[self._angle_index])])) + return np.concatenate( + ( + state, + [np.cos(state[self._angle_index]), np.sin(state[self._angle_index])], + ) + ) diff --git a/gym_electric_motor/physical_system_wrappers/current_sum_processor.py b/gym_electric_motor/physical_system_wrappers/current_sum_processor.py index 08816ac4..c7e3e20b 100644 --- a/gym_electric_motor/physical_system_wrappers/current_sum_processor.py +++ b/gym_electric_motor/physical_system_wrappers/current_sum_processor.py @@ -7,7 +7,7 @@ class CurrentSumProcessor(PhysicalSystemWrapper): """Adds an ``i_sum`` state to the systems state vector that adds up currents.""" - def __init__(self, currents, limit='max', physical_system=None): + def __init__(self, currents, limit="max", physical_system=None): """ Args: currents(Iterable[string]): Iterable of the names of the currents to be summed up. @@ -16,31 +16,37 @@ def __init__(self, currents, limit='max', physical_system=None): physical_system(PhysicalSystem(optional)): The inner PhysicalSystem of this processor. """ self._currents = currents - assert limit in ['max', 'sum'] - self._limit = max if limit == 'max' else np.sum + assert limit in ["max", "sum"] + self._limit = max if limit == "max" else np.sum self._current_indices = [] super().__init__(physical_system) def set_physical_system(self, physical_system): # Docstring of superclass super().set_physical_system(physical_system) - self._current_indices = [physical_system.state_positions[current] for current in self._currents] + self._current_indices = [ + physical_system.state_positions[current] for current in self._currents + ] # Define the new state space as concatenation of the old state space and [-1,1] for i_sum - low = np.concatenate((physical_system.state_space.low, [-1.])) - high = np.concatenate((physical_system.state_space.high, [1.])) + low = np.concatenate((physical_system.state_space.low, [-1.0])) + high = np.concatenate((physical_system.state_space.high, [1.0])) self.state_space = gymnasium.spaces.Box(low, high, dtype=np.float64) # Set the new limits / nominal values of the state vector current_limit = self._limit(physical_system.limits[self._current_indices]) - current_nominal_value = self._limit(physical_system.nominal_state[self._current_indices]) + current_nominal_value = self._limit( + physical_system.nominal_state[self._current_indices] + ) self._limits = np.concatenate((physical_system.limits, [current_limit])) - self._nominal_state = np.concatenate((physical_system.nominal_state, [current_nominal_value])) + self._nominal_state = np.concatenate( + (physical_system.nominal_state, [current_nominal_value]) + ) # Append the new state to the state name vector and the state positions dictionary - self._state_names = physical_system.state_names + ['i_sum'] + self._state_names = physical_system.state_names + ["i_sum"] self._state_positions = physical_system.state_positions.copy() - self._state_positions['i_sum'] = self._state_names.index('i_sum') + self._state_positions["i_sum"] = self._state_names.index("i_sum") return self def reset(self): diff --git a/gym_electric_motor/physical_system_wrappers/dead_time_processor.py b/gym_electric_motor/physical_system_wrappers/dead_time_processor.py index f447cc48..cdb7c5c4 100644 --- a/gym_electric_motor/physical_system_wrappers/dead_time_processor.py +++ b/gym_electric_motor/physical_system_wrappers/dead_time_processor.py @@ -25,14 +25,16 @@ def dead_time(self): def __init__(self, steps=1, reset_action=None, physical_system=None): """Args: - steps(int): Number of steps to delay the actions. - reset_action(callable): A callable that returns a list of length steps to initialize the dead-actions - after a reset. Default: See above in the class description - physical_system(PhysicalSystem (optional)): The inner physical system of this PhysicalSystemWrapper. + steps(int): Number of steps to delay the actions. + reset_action(callable): A callable that returns a list of length steps to initialize the dead-actions + after a reset. Default: See above in the class description + physical_system(PhysicalSystem (optional)): The inner physical system of this PhysicalSystemWrapper. """ self._reset_actions = reset_action self._steps = int(steps) - assert self._steps > 0, f'The number of steps has to be greater than 0. A "{steps}" has been passed.' + assert ( + self._steps > 0 + ), f'The number of steps has to be greater than 0. A "{steps}" has been passed.' self._action_deque = deque(maxlen=steps) super().__init__(physical_system) @@ -53,8 +55,8 @@ def set_physical_system(self, physical_system): reset_action = np.zeros(action_space.shape, dtype=np.float64) else: raise AssertionError( - f'Action Space {action_space} of type {type(action_space)} unsupported.' - 'Only Discrete / MultiDiscrete and Box allowed for the dead time processor.' + f"Action Space {action_space} of type {type(action_space)} unsupported." + "Only Discrete / MultiDiscrete and Box allowed for the dead time processor." ) self._reset_actions = lambda: [reset_action] * self._action_deque.maxlen return self diff --git a/gym_electric_motor/physical_system_wrappers/dq_to_abc_action_processor.py b/gym_electric_motor/physical_system_wrappers/dq_to_abc_action_processor.py index 042af41a..2482cdf4 100644 --- a/gym_electric_motor/physical_system_wrappers/dq_to_abc_action_processor.py +++ b/gym_electric_motor/physical_system_wrappers/dq_to_abc_action_processor.py @@ -30,11 +30,14 @@ def register_transformation(cls, motor_types): def wrapper(callable_): for motor_type in motor_types: cls._registry[motor_type] = callable_ + return wrapper @classmethod def make(cls, motor_type, *args, **kwargs): - assert motor_type in cls._registry.keys(), f'Not supported motor_type {motor_type}.' + assert ( + motor_type in cls._registry.keys() + ), f"Not supported motor_type {motor_type}." class_ = cls._registry[motor_type] inst = class_(*args, **kwargs) return inst @@ -56,19 +59,21 @@ def __init__(self, angle_name, physical_system=None): def set_physical_system(self, physical_system): # Docstring of super class - assert isinstance(physical_system.electrical_motor, ps.ThreePhaseMotor), \ - 'The motor in the system has to derive from the ThreePhaseMotor to define transformations.' + assert isinstance( + physical_system.electrical_motor, ps.ThreePhaseMotor + ), "The motor in the system has to derive from the ThreePhaseMotor to define transformations." super().set_physical_system(physical_system) - self._omega_index = physical_system.state_names.index('omega') + self._omega_index = physical_system.state_names.index("omega") self._angle_index = physical_system.state_names.index(self._angle_name) - assert self._angle_name in physical_system.state_names, \ - f'Angle {self._angle_name} not in the states of the physical system. ' \ - f'Probably a flux observer is required.' + assert self._angle_name in physical_system.state_names, ( + f"Angle {self._angle_name} not in the states of the physical system. " + f"Probably a flux observer is required." + ) self._angle_advance = 0.5 # If dead time has been added to the system increase the angle advance by the amount of dead time steps - if hasattr(physical_system, 'dead_time'): + if hasattr(physical_system, "dead_time"): self._angle_advance += physical_system.dead_time return self @@ -83,12 +88,13 @@ def reset(self, **kwargs): return normalized_state def _advance_angle(self, state): - return state[self._angle_index] \ + return ( + state[self._angle_index] + self._angle_advance * self._physical_system.tau * state[self._omega_index] + ) class _ClassicDqToAbcActionProcessor(DqToAbcActionProcessor): - @property def action_space(self): return gymnasium.spaces.Box(-1, 1, shape=(2,), dtype=np.float64) @@ -102,25 +108,28 @@ def simulate(self, action): return normalized_state -DqToAbcActionProcessor.register_transformation(['PMSM'])( - lambda angle_name='epsilon', *args, **kwargs: _ClassicDqToAbcActionProcessor(angle_name, *args, **kwargs) +DqToAbcActionProcessor.register_transformation(["PMSM"])( + lambda angle_name="epsilon", *args, **kwargs: _ClassicDqToAbcActionProcessor( + angle_name, *args, **kwargs + ) ) -DqToAbcActionProcessor.register_transformation(['SCIM'])( - lambda angle_name='psi_angle', *args, **kwargs: _ClassicDqToAbcActionProcessor(angle_name, *args, **kwargs) +DqToAbcActionProcessor.register_transformation(["SCIM"])( + lambda angle_name="psi_angle", *args, **kwargs: _ClassicDqToAbcActionProcessor( + angle_name, *args, **kwargs + ) ) -@DqToAbcActionProcessor.register_transformation(['DFIM']) +@DqToAbcActionProcessor.register_transformation(["DFIM"]) class _DFIMDqToAbcActionProcessor(DqToAbcActionProcessor): - @property def action_space(self): return gymnasium.spaces.Box(-1, 1, shape=(4,)) def __init__(self, physical_system=None): - super().__init__('epsilon', physical_system=physical_system) - self._flux_angle_name = 'psi_abs' + super().__init__("epsilon", physical_system=physical_system) + self._flux_angle_name = "psi_abs" self._flux_angle_index = None def simulate(self, action): @@ -135,7 +144,9 @@ def simulate(self, action): dq_action_stator = action[:2] dq_action_rotor = action[2:] abc_action_stator = self._transformation(dq_action_stator, advanced_angle) - abc_action_rotor = self._transformation(dq_action_rotor, self._state[self._flux_angle_index] - advanced_angle) + abc_action_rotor = self._transformation( + dq_action_rotor, self._state[self._flux_angle_index] - advanced_angle + ) abc_action = np.concatenate((abc_action_stator, abc_action_rotor)) normalized_state = self._physical_system.simulate(abc_action) self._state = normalized_state * self._physical_system.limits @@ -143,5 +154,5 @@ def simulate(self, action): def set_physical_system(self, physical_system): super().set_physical_system(physical_system) - self._flux_angle_index = physical_system.state_names.index('psi_angle') + self._flux_angle_index = physical_system.state_names.index("psi_angle") return self diff --git a/gym_electric_motor/physical_system_wrappers/flux_observer.py b/gym_electric_motor/physical_system_wrappers/flux_observer.py index c3f176b0..06eff3a8 100644 --- a/gym_electric_motor/physical_system_wrappers/flux_observer.py +++ b/gym_electric_motor/physical_system_wrappers/flux_observer.py @@ -25,7 +25,7 @@ class FluxObserver(PhysicalSystemWrapper): \Psi_k = \sum_{i=0}^k (\Psi_{k-1} + \Delta\Psi_k) \\tau """ - def __init__(self, current_names=('i_sa', 'i_sb', 'i_sc'), physical_system=None): + def __init__(self, current_names=("i_sa", "i_sb", "i_sc"), physical_system=None): """ Args: current_names(Iterable[string]): Names of the currents to be observed to estimate the flux. @@ -52,25 +52,39 @@ def _abc_to_alphabeta_transformation(i_s): def set_physical_system(self, physical_system): # Docstring of super class - assert isinstance(physical_system.electrical_motor, gem.physical_systems.electric_motors.InductionMotor) + assert isinstance( + physical_system.electrical_motor, + gem.physical_systems.electric_motors.InductionMotor, + ) super().set_physical_system(physical_system) mp = physical_system.electrical_motor.motor_parameter - self._l_m = mp['l_m'] # Main induction - self._l_r = mp['l_m'] + mp['l_sigr'] # Induction of the rotor - self._r_r = mp['r_r'] # Rotor resistance - self._p = mp['p'] # Pole pair number - psi_limit = self._l_m * physical_system.limits[physical_system.state_names.index('i_sd')] + self._l_m = mp["l_m"] # Main induction + self._l_r = mp["l_m"] + mp["l_sigr"] # Induction of the rotor + self._r_r = mp["r_r"] # Rotor resistance + self._p = mp["p"] # Pole pair number + psi_limit = ( + self._l_m + * physical_system.limits[physical_system.state_names.index("i_sd")] + ) low = np.concatenate((physical_system.state_space.low, [-psi_limit, -np.pi])) high = np.concatenate((physical_system.state_space.high, [psi_limit, np.pi])) self.state_space = gymnasium.spaces.Box(low, high, dtype=np.float64) - self._current_indices = [physical_system.state_positions[name] for name in self._current_names] + self._current_indices = [ + physical_system.state_positions[name] for name in self._current_names + ] self._limits = np.concatenate((physical_system.limits, [psi_limit, np.pi])) - self._nominal_state = np.concatenate((physical_system.nominal_state, [psi_limit, np.pi])) - self._state_names = physical_system.state_names + ['psi_abs', 'psi_angle'] - self._state_positions = {key: index for index, key in enumerate(self._state_names)} - - self._i_s_idx = [physical_system.state_positions[name] for name in self._current_names] - self._omega_idx = physical_system.state_positions['omega'] + self._nominal_state = np.concatenate( + (physical_system.nominal_state, [psi_limit, np.pi]) + ) + self._state_names = physical_system.state_names + ["psi_abs", "psi_angle"] + self._state_positions = { + key: index for index, key in enumerate(self._state_names) + } + + self._i_s_idx = [ + physical_system.state_positions[name] for name in self._current_names + ] + self._omega_idx = physical_system.state_positions["omega"] return self def reset(self): @@ -89,9 +103,17 @@ def simulate(self, action): [i_s_alpha, i_s_beta] = self._abc_to_alphabeta_transformation(i_s) # Calculate delta flux - delta_psi = complex(i_s_alpha, i_s_beta) * self._r_r * self._l_m / self._l_r \ - - self._integrated * complex(self._r_r / self._l_r, -omega) + delta_psi = complex( + i_s_alpha, i_s_beta + ) * self._r_r * self._l_m / self._l_r - self._integrated * complex( + self._r_r / self._l_r, -omega + ) # Integrate the flux self._integrated += delta_psi * self._physical_system.tau - return np.concatenate((state, [np.abs(self._integrated), np.angle(self._integrated)])) / self._limits + return ( + np.concatenate( + (state, [np.abs(self._integrated), np.angle(self._integrated)]) + ) + / self._limits + ) diff --git a/gym_electric_motor/physical_system_wrappers/physical_system_wrapper.py b/gym_electric_motor/physical_system_wrappers/physical_system_wrapper.py index 92396fee..8b8ce183 100644 --- a/gym_electric_motor/physical_system_wrappers/physical_system_wrapper.py +++ b/gym_electric_motor/physical_system_wrappers/physical_system_wrapper.py @@ -99,7 +99,9 @@ def set_physical_system(self, physical_system): self._physical_system = physical_system self._action_space = physical_system.action_space self._state_names = physical_system.state_names - self._state_positions = {key: index for index, key in enumerate(self._state_names)} + self._state_positions = { + key: index for index, key in enumerate(self._state_names) + } self._tau = physical_system.tau return self diff --git a/gym_electric_motor/physical_system_wrappers/state_noise_processor.py b/gym_electric_motor/physical_system_wrappers/state_noise_processor.py index 4cff64ac..88710703 100644 --- a/gym_electric_motor/physical_system_wrappers/state_noise_processor.py +++ b/gym_electric_motor/physical_system_wrappers/state_noise_processor.py @@ -29,7 +29,14 @@ def random_kwargs(self): def random_kwargs(self, value): self._random_kwargs = dict(value) - def __init__(self, states, random_dist='normal', random_kwargs=(), random_length=1000, physical_system=None): + def __init__( + self, + states, + random_dist="normal", + random_kwargs=(), + random_length=1000, + physical_system=None, + ): """ Args: states(Iterable[string] / 'all'): Names of the states onto which the noise shall be added. @@ -50,14 +57,17 @@ def __init__(self, states, random_dist='normal', random_kwargs=(), random_length self._random_dist = random_dist self._state_indices = [] super().__init__(physical_system) - assert hasattr(self._random_generator, random_dist), \ - f'The numpy random number generator has no distribution {random_dist}.'\ - 'Check https://numpy.org/doc/stable/reference/random/generator.html#distributions for distributions.' + assert hasattr(self._random_generator, random_dist), ( + f"The numpy random number generator has no distribution {random_dist}." + "Check https://numpy.org/doc/stable/reference/random/generator.html#distributions for distributions." + ) def set_physical_system(self, physical_system): # Docstring from super class super().set_physical_system(physical_system) - self._state_indices = [physical_system.state_positions[state_name] for state_name in self._states] + self._state_indices = [ + physical_system.state_positions[state_name] for state_name in self._states + ] return self def reset(self): @@ -79,7 +89,9 @@ def _add_noise(self, state): Returns: numpy.ndarray[float]): The state with additional noise. """ - state[self._state_indices] = state[self._state_indices] + self._noise[self._random_pointer] + state[self._state_indices] = ( + state[self._state_indices] + self._noise[self._random_pointer] + ) self._random_pointer += 1 return state @@ -87,4 +99,6 @@ def _new_noise(self): """Samples new noise from the random distribution for the next steps.""" self._random_pointer = 0 fct = getattr(self._random_generator, self._random_dist) - self._noise = fct(size=(self._random_length, len(self._state_indices)), **self._random_kwargs) + self._noise = fct( + size=(self._random_length, len(self._state_indices)), **self._random_kwargs + ) diff --git a/gym_electric_motor/physical_systems/__init__.py b/gym_electric_motor/physical_systems/__init__.py index b91b743a..32cad4bd 100644 --- a/gym_electric_motor/physical_systems/__init__.py +++ b/gym_electric_motor/physical_systems/__init__.py @@ -1,22 +1,66 @@ - -from .physical_systems import DcMotorSystem, SynchronousMotorSystem, SquirrelCageInductionMotorSystem, DoublyFedInductionMotorSystem, \ - ExternallyExcitedSynchronousMotorSystem, ThreePhaseMotorSystem, SCMLSystem - -from .converters import PowerElectronicConverter, FiniteOneQuadrantConverter, FiniteTwoQuadrantConverter, \ - FiniteFourQuadrantConverter, FiniteMultiConverter, FiniteB6BridgeConverter, ContOneQuadrantConverter, \ - ContTwoQuadrantConverter, ContFourQuadrantConverter, ContMultiConverter, ContB6BridgeConverter, NoConverter - -from .electric_motors import DcExternallyExcitedMotor, DcSeriesMotor, DcPermanentlyExcitedMotor, DcShuntMotor, \ - PermanentMagnetSynchronousMotor, ElectricMotor, SynchronousReluctanceMotor, SquirrelCageInductionMotor, \ - DoublyFedInductionMotor, ExternallyExcitedSynchronousMotor, ThreePhaseMotor - - -from .mechanical_loads import MechanicalLoad, PolynomialStaticLoad, ExternalSpeedLoad, ConstantSpeedLoad, \ - OrnsteinUhlenbeckLoad - -from .solvers import OdeSolver, EulerSolver, ScipyOdeIntSolver, ScipySolveIvpSolver, ScipyOdeSolver - -from .voltage_supplies import VoltageSupply, IdealVoltageSupply, RCVoltageSupply, AC1PhaseSupply, AC3PhaseSupply +from .physical_systems import ( + DcMotorSystem, + SynchronousMotorSystem, + SquirrelCageInductionMotorSystem, + DoublyFedInductionMotorSystem, + ExternallyExcitedSynchronousMotorSystem, + ThreePhaseMotorSystem, + SCMLSystem, +) + +from .converters import ( + PowerElectronicConverter, + FiniteOneQuadrantConverter, + FiniteTwoQuadrantConverter, + FiniteFourQuadrantConverter, + FiniteMultiConverter, + FiniteB6BridgeConverter, + ContOneQuadrantConverter, + ContTwoQuadrantConverter, + ContFourQuadrantConverter, + ContMultiConverter, + ContB6BridgeConverter, + NoConverter, +) + +from .electric_motors import ( + DcExternallyExcitedMotor, + DcSeriesMotor, + DcPermanentlyExcitedMotor, + DcShuntMotor, + PermanentMagnetSynchronousMotor, + ElectricMotor, + SynchronousReluctanceMotor, + SquirrelCageInductionMotor, + DoublyFedInductionMotor, + ExternallyExcitedSynchronousMotor, + ThreePhaseMotor, +) + + +from .mechanical_loads import ( + MechanicalLoad, + PolynomialStaticLoad, + ExternalSpeedLoad, + ConstantSpeedLoad, + OrnsteinUhlenbeckLoad, +) + +from .solvers import ( + OdeSolver, + EulerSolver, + ScipyOdeIntSolver, + ScipySolveIvpSolver, + ScipyOdeSolver, +) + +from .voltage_supplies import ( + VoltageSupply, + IdealVoltageSupply, + RCVoltageSupply, + AC1PhaseSupply, + AC3PhaseSupply, +) from ..utils import register_class, register_superclass @@ -29,48 +73,48 @@ register_superclass(VoltageSupply) -register_class(DcMotorSystem, PhysicalSystem, 'DcMotorSystem') -register_class(SynchronousMotorSystem, PhysicalSystem, 'SyncMotorSystem') -register_class(SquirrelCageInductionMotorSystem, PhysicalSystem, 'SquirrelCageInductionMotorSystem') -register_class(DoublyFedInductionMotorSystem, PhysicalSystem, 'DoublyFedInductionMotorSystem') - -register_class(FiniteOneQuadrantConverter, PowerElectronicConverter, 'Finite-1QC') -register_class(ContOneQuadrantConverter, PowerElectronicConverter, 'Cont-1QC') -register_class(FiniteTwoQuadrantConverter, PowerElectronicConverter, 'Finite-2QC') -register_class(ContTwoQuadrantConverter, PowerElectronicConverter, 'Cont-2QC') -register_class(FiniteFourQuadrantConverter, PowerElectronicConverter, 'Finite-4QC') -register_class(ContFourQuadrantConverter, PowerElectronicConverter, 'Cont-4QC') -register_class(FiniteMultiConverter, PowerElectronicConverter, 'Finite-Multi') -register_class(ContMultiConverter, PowerElectronicConverter, 'Cont-Multi') -register_class(FiniteB6BridgeConverter, PowerElectronicConverter, 'Finite-B6C') -register_class(ContB6BridgeConverter, PowerElectronicConverter, 'Cont-B6C') -register_class(NoConverter, PowerElectronicConverter, 'NoConverter') - -register_class(PolynomialStaticLoad, MechanicalLoad, 'PolyStaticLoad') -register_class(ConstantSpeedLoad, MechanicalLoad, 'ConstSpeedLoad') -register_class(ExternalSpeedLoad, MechanicalLoad, 'ExtSpeedLoad') - -register_class(EulerSolver, OdeSolver, 'euler') -register_class(ScipyOdeSolver, OdeSolver, 'scipy.ode') -register_class(ScipySolveIvpSolver, OdeSolver, 'scipy.solve_ivp') -register_class(ScipyOdeIntSolver, OdeSolver, 'scipy.odeint') - -register_class(DcSeriesMotor, ElectricMotor, 'DcSeries') -register_class(DcPermanentlyExcitedMotor, ElectricMotor, 'DcPermEx') -register_class(DcExternallyExcitedMotor, ElectricMotor, 'DcExtEx') -register_class(DcShuntMotor, ElectricMotor, 'DcShunt') -register_class(PermanentMagnetSynchronousMotor, ElectricMotor, 'PMSM') -register_class(ExternallyExcitedSynchronousMotor, ElectricMotor, 'EESM') -register_class(SynchronousReluctanceMotor, ElectricMotor, 'SynRM') -register_class(SquirrelCageInductionMotor, ElectricMotor, 'SCIM') -register_class(DoublyFedInductionMotor, ElectricMotor, 'DFIM') - - -register_class(IdealVoltageSupply, VoltageSupply, 'IdealVoltageSupply') -register_class(RCVoltageSupply, VoltageSupply, 'RCVoltageSupply') -register_class(AC1PhaseSupply, VoltageSupply, 'AC1PhaseSupply') -register_class(AC3PhaseSupply, VoltageSupply, 'AC3PhaseSupply') - - - - +register_class(DcMotorSystem, PhysicalSystem, "DcMotorSystem") +register_class(SynchronousMotorSystem, PhysicalSystem, "SyncMotorSystem") +register_class( + SquirrelCageInductionMotorSystem, PhysicalSystem, "SquirrelCageInductionMotorSystem" +) +register_class( + DoublyFedInductionMotorSystem, PhysicalSystem, "DoublyFedInductionMotorSystem" +) + +register_class(FiniteOneQuadrantConverter, PowerElectronicConverter, "Finite-1QC") +register_class(ContOneQuadrantConverter, PowerElectronicConverter, "Cont-1QC") +register_class(FiniteTwoQuadrantConverter, PowerElectronicConverter, "Finite-2QC") +register_class(ContTwoQuadrantConverter, PowerElectronicConverter, "Cont-2QC") +register_class(FiniteFourQuadrantConverter, PowerElectronicConverter, "Finite-4QC") +register_class(ContFourQuadrantConverter, PowerElectronicConverter, "Cont-4QC") +register_class(FiniteMultiConverter, PowerElectronicConverter, "Finite-Multi") +register_class(ContMultiConverter, PowerElectronicConverter, "Cont-Multi") +register_class(FiniteB6BridgeConverter, PowerElectronicConverter, "Finite-B6C") +register_class(ContB6BridgeConverter, PowerElectronicConverter, "Cont-B6C") +register_class(NoConverter, PowerElectronicConverter, "NoConverter") + +register_class(PolynomialStaticLoad, MechanicalLoad, "PolyStaticLoad") +register_class(ConstantSpeedLoad, MechanicalLoad, "ConstSpeedLoad") +register_class(ExternalSpeedLoad, MechanicalLoad, "ExtSpeedLoad") + +register_class(EulerSolver, OdeSolver, "euler") +register_class(ScipyOdeSolver, OdeSolver, "scipy.ode") +register_class(ScipySolveIvpSolver, OdeSolver, "scipy.solve_ivp") +register_class(ScipyOdeIntSolver, OdeSolver, "scipy.odeint") + +register_class(DcSeriesMotor, ElectricMotor, "DcSeries") +register_class(DcPermanentlyExcitedMotor, ElectricMotor, "DcPermEx") +register_class(DcExternallyExcitedMotor, ElectricMotor, "DcExtEx") +register_class(DcShuntMotor, ElectricMotor, "DcShunt") +register_class(PermanentMagnetSynchronousMotor, ElectricMotor, "PMSM") +register_class(ExternallyExcitedSynchronousMotor, ElectricMotor, "EESM") +register_class(SynchronousReluctanceMotor, ElectricMotor, "SynRM") +register_class(SquirrelCageInductionMotor, ElectricMotor, "SCIM") +register_class(DoublyFedInductionMotor, ElectricMotor, "DFIM") + + +register_class(IdealVoltageSupply, VoltageSupply, "IdealVoltageSupply") +register_class(RCVoltageSupply, VoltageSupply, "RCVoltageSupply") +register_class(AC1PhaseSupply, VoltageSupply, "AC1PhaseSupply") +register_class(AC3PhaseSupply, VoltageSupply, "AC3PhaseSupply") diff --git a/gym_electric_motor/physical_systems/converters.py b/gym_electric_motor/physical_systems/converters.py index 3ccd4e0e..6144916f 100644 --- a/gym_electric_motor/physical_systems/converters.py +++ b/gym_electric_motor/physical_systems/converters.py @@ -7,7 +7,7 @@ class PowerElectronicConverter: """ Base class for all converters in a SCMLSystem. - + Properties: | *voltages(tuple(float, float))*: Determines which output voltage polarities the converter can generate. | E.g. (0, 1) - Only positive voltages / (-1, 1) Positive and negative voltages @@ -36,8 +36,8 @@ def tau(self, value): def __init__(self, tau, interlocking_time=0.0): """ - :param tau: Discrete time step of the system in seconds - :param interlocking_time: Interlocking time of the transistors in seconds + :param tau: Discrete time step of the system in seconds + :param interlocking_time: Interlocking time of the transistors in seconds """ self._tau = tau self._interlocking_time = interlocking_time @@ -103,9 +103,9 @@ def _set_switching_pattern(self, action): Method to calculate the switching pattern and corresponding switching times for the next time step. At least, the next time step [t + tau] is returned. - Args: + Args: action(instance of action_space): The action for the next time step. - + Returns: list(float): Switching times. """ @@ -115,6 +115,7 @@ def _set_switching_pattern(self, action): class NoConverter(PowerElectronicConverter): """Dummy Converter class used to directly transfer the supply voltage to the motor""" + # Dummy default values for voltages and currents. # No real use other than to fit the current physical system architecture voltages = Box(0, 1, shape=(3,), dtype=np.float64) @@ -144,11 +145,21 @@ def __init__(self, tau=1e-4, **kwargs): def set_action(self, action, t): # Docstring in base class - return super().set_action(min(max(action, self.action_space.low), self.action_space.high), t) + return super().set_action( + min(max(action, self.action_space.low), self.action_space.high), t + ) def convert(self, i_out, t): # Docstring in base class - return [min(max(self._convert(i_out, t) - self._interlock(i_out, t), self.voltages.low[0]), self.voltages.high[0])] + return [ + min( + max( + self._convert(i_out, t) - self._interlock(i_out, t), + self.voltages.low[0], + ), + self.voltages.high[0], + ) + ] def _convert(self, i_in, t): """ @@ -194,8 +205,9 @@ def __init__(self, tau=1e-5, **kwargs): super().__init__(tau=tau, **kwargs) def set_action(self, action, t): - assert self.action_space.contains(action), \ - f"The selected action {action} is not a valid element of the action space {self.action_space}." + assert self.action_space.contains( + action + ), f"The selected action {action} is not a valid element of the action space {self.action_space}." return super().set_action(action, t) def convert(self, i_out, t): @@ -276,7 +288,7 @@ def convert(self, i_out, t): elif self._switching_state == 2: return [0.0] else: - raise Exception('Invalid switching state of the converter') + raise Exception("Invalid switching state of the converter") def i_sup(self, i_out): # Docstring in base class @@ -287,21 +299,24 @@ def i_sup(self, i_out): elif self._switching_state == 2: return 0 else: - raise Exception('Invalid switching state of the converter') + raise Exception("Invalid switching state of the converter") def _set_switching_pattern(self, action): # Docstring in base class if ( - action == 0 - or self._switching_state == 0 - or action == self._switching_state - or self._interlocking_time == 0 + action == 0 + or self._switching_state == 0 + or action == self._switching_state + or self._interlocking_time == 0 ): self._switching_pattern = [action] return [self._action_start_time + self._tau] else: self._switching_pattern = [0, action] - return [self._action_start_time + self._interlocking_time, self._action_start_time + self._tau] + return [ + self._action_start_time + self._interlocking_time, + self._action_start_time + self._tau, + ] class FiniteFourQuadrantConverter(FiniteConverter): @@ -322,6 +337,7 @@ class FiniteFourQuadrantConverter(FiniteConverter): | Box(-1, 1, shape=(1,)) | Box(-1, 1, shape=(1,)) """ + voltages = Box(-1, 1, shape=(1,), dtype=np.float64) currents = Box(-1, 1, shape=(1,), dtype=np.float64) action_space = Discrete(4) @@ -329,7 +345,10 @@ class FiniteFourQuadrantConverter(FiniteConverter): def __init__(self, **kwargs): # Docstring in base class super().__init__(**kwargs) - self._subconverters = [FiniteTwoQuadrantConverter(**kwargs), FiniteTwoQuadrantConverter(**kwargs)] + self._subconverters = [ + FiniteTwoQuadrantConverter(**kwargs), + FiniteTwoQuadrantConverter(**kwargs), + ] def reset(self): # Docstring in base class @@ -339,12 +358,16 @@ def reset(self): def convert(self, i_out, t): # Docstring in base class - return [self._subconverters[0].convert(i_out, t)[0] - self._subconverters[1].convert([-i_out[0]], t)[0]] + return [ + self._subconverters[0].convert(i_out, t)[0] + - self._subconverters[1].convert([-i_out[0]], t)[0] + ] def set_action(self, action, t): # Docstring in base class - assert self.action_space.contains(action), \ - f"The selected action {action} is not a valid element of the action space {self.action_space}." + assert self.action_space.contains( + action + ), f"The selected action {action} is not a valid element of the action space {self.action_space}." times = [] action0 = [1, 1, 2, 2][action] action1 = [1, 2, 1, 2][action] @@ -354,7 +377,9 @@ def set_action(self, action, t): def i_sup(self, i_out): # Docstring in base class - return self._subconverters[0].i_sup(i_out) + self._subconverters[1].i_sup([-i_out[0]]) + return self._subconverters[0].i_sup(i_out) + self._subconverters[1].i_sup( + [-i_out[0]] + ) class ContOneQuadrantConverter(ContDynamicallyAveragedConverter): @@ -372,6 +397,7 @@ class ContOneQuadrantConverter(ContDynamicallyAveragedConverter): | voltages: Box(0, 1, shape=(1,)) | currents: Box(0, 1, shape=(1,)) """ + voltages = Box(0, 1, shape=(1,), dtype=np.float64) currents = Box(0, 1, shape=(1,), dtype=np.float64) action_space = Box(0, 1, shape=(1,), dtype=np.float64) @@ -405,6 +431,7 @@ class ContTwoQuadrantConverter(ContDynamicallyAveragedConverter): | voltages: Box(0, 1, shape=(1,)) | currents: Box(-1, 1, shape=(1,)) """ + voltages = Box(0, 1, shape=(1,), dtype=np.float64) currents = Box(-1, 1, shape=(1,), dtype=np.float64) action_space = Box(0, 1, shape=(1,), dtype=np.float64) @@ -418,7 +445,9 @@ def i_sup(self, i_out): interlocking_current = 1 if i_out[0] < 0 else 0 return ( self._current_action[0] - + self._interlocking_time / self._tau * (interlocking_current - self._current_action[0]) + + self._interlocking_time + / self._tau + * (interlocking_current - self._current_action[0]) ) * i_out[0] @@ -442,6 +471,7 @@ class ContFourQuadrantConverter(ContDynamicallyAveragedConverter): | voltages: Box(-1, 1, shape=(1,)) | currents: Box(-1, 1, shape=(1,)) """ + voltages = Box(-1, 1, shape=(1,), dtype=np.float64) currents = Box(-1, 1, shape=(1,), dtype=np.float64) action_space = Box(-1, 1, shape=(1,), dtype=np.float64) @@ -449,7 +479,10 @@ class ContFourQuadrantConverter(ContDynamicallyAveragedConverter): def __init__(self, **kwargs): # Docstring in base class super().__init__(**kwargs) - self._subconverters = [ContTwoQuadrantConverter(**kwargs), ContTwoQuadrantConverter(**kwargs)] + self._subconverters = [ + ContTwoQuadrantConverter(**kwargs), + ContTwoQuadrantConverter(**kwargs), + ] def _convert(self, *_): # Not used here @@ -463,7 +496,10 @@ def reset(self): def convert(self, i_out, t): # Docstring in base class - return [self._subconverters[0].convert(i_out, t)[0] - self._subconverters[1].convert(i_out, t)[0]] + return [ + self._subconverters[0].convert(i_out, t)[0] + - self._subconverters[1].convert(i_out, t)[0] + ] def set_action(self, action, t): # Docstring in base class @@ -475,7 +511,9 @@ def set_action(self, action, t): def i_sup(self, i_out): # Docstring in base class - return self._subconverters[0].i_sup(i_out) + self._subconverters[1].i_sup([-i_out[0]]) + return self._subconverters[0].i_sup(i_out) + self._subconverters[1].i_sup( + [-i_out[0]] + ) class FiniteMultiConverter(FiniteConverter): @@ -510,7 +548,6 @@ def tau(self, value): def sub_converters(self): return self._sub_converters - def __init__(self, subconverters, **kwargs): """ Args: @@ -519,7 +556,8 @@ def __init__(self, subconverters, **kwargs): """ super().__init__(**kwargs) self._sub_converters = [ - instantiate(PowerElectronicConverter, subconverter, **kwargs) for subconverter in subconverters + instantiate(PowerElectronicConverter, subconverter, **kwargs) + for subconverter in subconverters ] self.subsignal_current_space_dims = [] self.subsignal_voltage_space_dims = [] @@ -531,8 +569,12 @@ def __init__(self, subconverters, **kwargs): # get the limits and space dims from each subconverter for subconverter in self._sub_converters: - self.subsignal_current_space_dims.append(np.squeeze(subconverter.currents.shape) or 1) - self.subsignal_voltage_space_dims.append(np.squeeze(subconverter.voltages.shape) or 1) + self.subsignal_current_space_dims.append( + np.squeeze(subconverter.currents.shape) or 1 + ) + self.subsignal_voltage_space_dims.append( + np.squeeze(subconverter.voltages.shape) or 1 + ) self.action_space.append(subconverter.action_space.n) @@ -561,7 +603,9 @@ def convert(self, i_out, t): # Docstring in base class u_in = [] subsignal_idx_low = 0 - for subconverter, subsignal_space_size in zip(self._sub_converters, self.subsignal_voltage_space_dims): + for subconverter, subsignal_space_size in zip( + self._sub_converters, self.subsignal_voltage_space_dims + ): subsignal_idx_high = subsignal_idx_low + subsignal_space_size u_in += subconverter.convert(i_out[subsignal_idx_low:subsignal_idx_high], t) subsignal_idx_low = subsignal_idx_high @@ -585,7 +629,9 @@ def i_sup(self, i_out): # Docstring in base class i_sup = 0 subsignal_idx_low = 0 - for subconverter, subsignal_space_size in zip(self._sub_converters, self.subsignal_current_space_dims): + for subconverter, subsignal_space_size in zip( + self._sub_converters, self.subsignal_current_space_dims + ): subsignal_idx_high = subsignal_idx_low + subsignal_space_size i_sup += subconverter.i_sup(i_out[subsignal_idx_low:subsignal_idx_high]) subsignal_idx_low = subsignal_idx_high @@ -630,7 +676,8 @@ def __init__(self, subconverters, **kwargs): """ super().__init__(**kwargs) self._sub_converters = [ - instantiate(PowerElectronicConverter, subconverter, **kwargs) for subconverter in subconverters + instantiate(PowerElectronicConverter, subconverter, **kwargs) + for subconverter in subconverters ] self.subsignal_current_space_dims = [] @@ -644,8 +691,12 @@ def __init__(self, subconverters, **kwargs): # get the limits and space dims from each subconverter for subconverter in self._sub_converters: - self.subsignal_current_space_dims.append(np.squeeze(subconverter.currents.shape) or 1) - self.subsignal_voltage_space_dims.append(np.squeeze(subconverter.voltages.shape) or 1) + self.subsignal_current_space_dims.append( + np.squeeze(subconverter.currents.shape) or 1 + ) + self.subsignal_voltage_space_dims.append( + np.squeeze(subconverter.voltages.shape) or 1 + ) action_space_low.append(subconverter.action_space.low) action_space_high.append(subconverter.action_space.high) @@ -679,7 +730,7 @@ def set_action(self, action, t): times = [] ind = 0 for subconverter in self._sub_converters: - sub_action = action[ind:ind + subconverter.action_space.shape[0]] + sub_action = action[ind : ind + subconverter.action_space.shape[0]] ind += subconverter.action_space.shape[0] times += subconverter.set_action(sub_action, t) return sorted(list(set(times))) @@ -695,7 +746,9 @@ def convert(self, i_out, t): # Docstring in base class u_in = [] subsignal_idx_low = 0 - for subconverter, subsignal_space_size in zip(self._sub_converters, self.subsignal_voltage_space_dims): + for subconverter, subsignal_space_size in zip( + self._sub_converters, self.subsignal_voltage_space_dims + ): subsignal_idx_high = subsignal_idx_low + subsignal_space_size u_in += subconverter.convert(i_out[subsignal_idx_low:subsignal_idx_high], t) subsignal_idx_low = subsignal_idx_high @@ -709,7 +762,9 @@ def i_sup(self, i_out): # Docstring in base class i_sup = 0 subsignal_idx_low = 0 - for subconverter, subsignal_space_size in zip(self._sub_converters, self.subsignal_current_space_dims): + for subconverter, subsignal_space_size in zip( + self._sub_converters, self.subsignal_current_space_dims + ): subsignal_idx_high = subsignal_idx_low + subsignal_space_size i_sup += subconverter.i_sup(i_out[subsignal_idx_low:subsignal_idx_high]) subsignal_idx_low = subsignal_idx_high @@ -770,7 +825,7 @@ class FiniteB6BridgeConverter(FiniteConverter): [1, 2, 2], [1, 2, 1], [1, 1, 2], - [1, 1, 1] + [1, 1, 1], ] def __init__(self, tau=1e-5, **kwargs): @@ -795,14 +850,15 @@ def convert(self, i_out, t): u_out = [ self._sub_converters[0].convert([i_out[0]], t)[0] - 0.5, self._sub_converters[1].convert([i_out[1]], t)[0] - 0.5, - self._sub_converters[2].convert([i_out[2]], t)[0] - 0.5 + self._sub_converters[2].convert([i_out[2]], t)[0] - 0.5, ] return u_out def set_action(self, action, t): # Docstring in base class - assert self.action_space.contains(action), \ - f"The selected action {action} is not a valid element of the action space {self.action_space}." + assert self.action_space.contains( + action + ), f"The selected action {action} is not a valid element of the action space {self.action_space}." subactions = self._subactions[action] times = [] times += self._sub_converters[0].set_action(subactions[0], t) @@ -812,7 +868,12 @@ def set_action(self, action, t): def i_sup(self, i_out): # Docstring in base class - return sum([subconverter.i_sup([i_out_]) for subconverter, i_out_ in zip(self._sub_converters, i_out)]) + return sum( + [ + subconverter.i_sup([i_out_]) + for subconverter, i_out_ in zip(self._sub_converters, i_out) + ] + ) class ContB6BridgeConverter(ContDynamicallyAveragedConverter): @@ -866,7 +927,7 @@ def convert(self, i_out, t): u_out = [ self._subconverters[0].convert([i_out[0]], t)[0] - 0.5, self._subconverters[1].convert([i_out[1]], t)[0] - 0.5, - self._subconverters[2].convert([i_out[2]], t)[0] - 0.5 + self._subconverters[2].convert([i_out[2]], t)[0] - 0.5, ] return u_out @@ -884,4 +945,9 @@ def _convert(self, i_in, t): def i_sup(self, i_out): # Docstring in base class - return sum([subconverter.i_sup([i_out_]) for subconverter, i_out_ in zip(self._subconverters, i_out)]) + return sum( + [ + subconverter.i_sup([i_out_]) + for subconverter, i_out_ in zip(self._subconverters, i_out) + ] + ) diff --git a/gym_electric_motor/physical_systems/electric_motors/__init__.py b/gym_electric_motor/physical_systems/electric_motors/__init__.py index 81a09a24..afa6813c 100644 --- a/gym_electric_motor/physical_systems/electric_motors/__init__.py +++ b/gym_electric_motor/physical_systems/electric_motors/__init__.py @@ -21,4 +21,3 @@ from .induction_motor import InductionMotor from .squirrel_cage_induction_motor import SquirrelCageInductionMotor from .doubly_fed_induction_motor import DoublyFedInductionMotor - diff --git a/gym_electric_motor/physical_systems/electric_motors/dc_externally_excited_motor.py b/gym_electric_motor/physical_systems/electric_motors/dc_externally_excited_motor.py index d3c56e44..ca3f85dd 100644 --- a/gym_electric_motor/physical_systems/electric_motors/dc_externally_excited_motor.py +++ b/gym_electric_motor/physical_systems/electric_motors/dc_externally_excited_motor.py @@ -10,27 +10,32 @@ class DcExternallyExcitedMotor(DcMotor): def electrical_jacobian(self, state, u_in, omega, *_): mp = self._motor_parameter return ( - np.array([ - [-mp['r_a'] / mp['l_a'], -mp['l_e_prime'] / mp['l_a'] * omega], - [0, -mp['r_e'] / mp['l_e']] - ]), - np.array([-mp['l_e_prime'] * state[self.I_E_IDX] / mp['l_a'], 0]), - np.array([mp['l_e_prime'] * state[self.I_E_IDX], - mp['l_e_prime'] * state[self.I_A_IDX]]) + np.array( + [ + [-mp["r_a"] / mp["l_a"], -mp["l_e_prime"] / mp["l_a"] * omega], + [0, -mp["r_e"] / mp["l_e"]], + ] + ), + np.array([-mp["l_e_prime"] * state[self.I_E_IDX] / mp["l_a"], 0]), + np.array( + [ + mp["l_e_prime"] * state[self.I_E_IDX], + mp["l_e_prime"] * state[self.I_A_IDX], + ] + ), ) def _update_limits(self): # Docstring of superclass # R_a might be 0, protect against that - r_a = 1 if self._motor_parameter['r_a'] == 0 else self._motor_parameter['r_a'] + r_a = 1 if self._motor_parameter["r_a"] == 0 else self._motor_parameter["r_a"] - limit_agenda = \ - {'u_a': self._default_limits['u'], - 'u_e': self._default_limits['u'], - 'i_a': self._limits.get('i', None) or - self._limits['u'] / r_a, - 'i_e': self._limits.get('i', None) or - self._limits['u'] / self.motor_parameter['r_e'], - } + limit_agenda = { + "u_a": self._default_limits["u"], + "u_e": self._default_limits["u"], + "i_a": self._limits.get("i", None) or self._limits["u"] / r_a, + "i_e": self._limits.get("i", None) + or self._limits["u"] / self.motor_parameter["r_e"], + } super()._update_limits(limit_agenda) diff --git a/gym_electric_motor/physical_systems/electric_motors/dc_motor.py b/gym_electric_motor/physical_systems/electric_motors/dc_motor.py index e0b8ed72..7d83cf9d 100644 --- a/gym_electric_motor/physical_systems/electric_motors/dc_motor.py +++ b/gym_electric_motor/physical_systems/electric_motors/dc_motor.py @@ -5,76 +5,95 @@ class DcMotor(ElectricMotor): """ - The DcMotor and its subclasses implement the technical system of a dc motor. - - This includes the system equations, the motor parameters of the equivalent circuit diagram, - as well as limits. - - - ===================== ========== ============= =========================================== - Motor Parameter Unit Default Value Description - ===================== ========== ============= =========================================== - r_a Ohm 16e-3 Armature circuit resistance - r_e Ohm 16e-2 Exciting circuit resistance - l_a H 19e-6 Armature circuit inductance - l_e H 5.4e-3 Exciting circuit inductance - l_e_prime H 1.7e-3 Effective excitation inductance - j_rotor kg/m^2 0.025 Moment of inertia of the rotor - ===================== ========== ============= =========================================== - - ..note :: - The motor parameter are based on the following DC Motor (slightly adapted): - https://www.heinzmann-electric-motors.com/en/products/dc-motors/pmg-132-dc-motor - - =============== ====== ============================================= - Motor Currents Unit Description - =============== ====== ============================================= - i_a A Armature circuit current - i_e A Exciting circuit current - =============== ====== ============================================= - =============== ====== ============================================= - Motor Voltages Unit Description - =============== ====== ============================================= - u_a V Armature circuit voltage - u_e v Exciting circuit voltage - =============== ====== ============================================= - - ======== =========================================================== - Limits / Nominal Value Dictionary Entries: - -------- ----------------------------------------------------------- - Entry Description - ======== =========================================================== - i_a Armature current - i_e Exciting current - omega Angular Velocity - torque Motor generated torque - u_a Armature Voltage - u_e Exciting Voltage - ======== =========================================================== + The DcMotor and its subclasses implement the technical system of a dc motor. + + This includes the system equations, the motor parameters of the equivalent circuit diagram, + as well as limits. + + + ===================== ========== ============= =========================================== + Motor Parameter Unit Default Value Description + ===================== ========== ============= =========================================== + r_a Ohm 16e-3 Armature circuit resistance + r_e Ohm 16e-2 Exciting circuit resistance + l_a H 19e-6 Armature circuit inductance + l_e H 5.4e-3 Exciting circuit inductance + l_e_prime H 1.7e-3 Effective excitation inductance + j_rotor kg/m^2 0.025 Moment of inertia of the rotor + ===================== ========== ============= =========================================== + + ..note :: + The motor parameter are based on the following DC Motor (slightly adapted): + https://www.heinzmann-electric-motors.com/en/products/dc-motors/pmg-132-dc-motor + + =============== ====== ============================================= + Motor Currents Unit Description + =============== ====== ============================================= + i_a A Armature circuit current + i_e A Exciting circuit current + =============== ====== ============================================= + =============== ====== ============================================= + Motor Voltages Unit Description + =============== ====== ============================================= + u_a V Armature circuit voltage + u_e v Exciting circuit voltage + =============== ====== ============================================= + + ======== =========================================================== + Limits / Nominal Value Dictionary Entries: + -------- ----------------------------------------------------------- + Entry Description + ======== =========================================================== + i_a Armature current + i_e Exciting current + omega Angular Velocity + torque Motor generated torque + u_a Armature Voltage + u_e Exciting Voltage + ======== =========================================================== """ # Indices for array accesses I_A_IDX = 0 I_E_IDX = 1 CURRENTS_IDX = [0, 1] - CURRENTS = ['i_a', 'i_e'] - VOLTAGES = ['u_a', 'u_e'] + CURRENTS = ["i_a", "i_e"] + VOLTAGES = ["u_a", "u_e"] # Motor parameter, nominal values and limits are based on the following DC Motor: # https://www.heinzmann-electric-motors.com/en/products/dc-motors/pmg-132-dc-motor _default_motor_parameter = { - 'r_a': 16e-3, 'r_e': 16e-2, 'l_a': 19e-6, 'l_e_prime': 1.7e-3, 'l_e': 5.4e-3, 'j_rotor': 0.0025 + "r_a": 16e-3, + "r_e": 16e-2, + "l_a": 19e-6, + "l_e_prime": 1.7e-3, + "l_e": 5.4e-3, + "j_rotor": 0.0025, } - _default_nominal_values = dict(omega=300, torque=16.0, i=97, i_a=97, i_e=97, u=60, u_a=60, u_e=60) - _default_limits = dict(omega=400, torque=38.0, i=210, i_a=210, i_e=210, u=60, u_a=60, u_e=60) - _default_initializer = {'states': {'i_a': 0.0, 'i_e': 0.0}, - 'interval': None, - 'random_init': None, - 'random_params': (None, None)} - - def __init__(self, motor_parameter=None, nominal_values=None, limit_values=None, motor_initializer=None): + _default_nominal_values = dict( + omega=300, torque=16.0, i=97, i_a=97, i_e=97, u=60, u_a=60, u_e=60 + ) + _default_limits = dict( + omega=400, torque=38.0, i=210, i_a=210, i_e=210, u=60, u_a=60, u_e=60 + ) + _default_initializer = { + "states": {"i_a": 0.0, "i_e": 0.0}, + "interval": None, + "random_init": None, + "random_params": (None, None), + } + + def __init__( + self, + motor_parameter=None, + nominal_values=None, + limit_values=None, + motor_initializer=None, + ): # Docstring of superclass - super().__init__(motor_parameter, nominal_values, limit_values, motor_initializer) + super().__init__( + motor_parameter, nominal_values, limit_values, motor_initializer + ) #: Matrix that contains the constant parameters of the systems equation for faster computation self._model_constants = None self._update_model() @@ -87,17 +106,22 @@ def _update_model(self): """ mp = self._motor_parameter self._model_constants = np.array( - [ - [-mp['r_a'], 0, -mp['l_e_prime'], 1, 0], - [0, -mp['r_e'], 0, 0, 1] - ] + [[-mp["r_a"], 0, -mp["l_e_prime"], 1, 0], [0, -mp["r_e"], 0, 0, 1]] + ) + self._model_constants[self.I_A_IDX] = ( + self._model_constants[self.I_A_IDX] / mp["l_a"] + ) + self._model_constants[self.I_E_IDX] = ( + self._model_constants[self.I_E_IDX] / mp["l_e"] ) - self._model_constants[self.I_A_IDX] = self._model_constants[self.I_A_IDX] / mp['l_a'] - self._model_constants[self.I_E_IDX] = self._model_constants[self.I_E_IDX] / mp['l_e'] def torque(self, currents): # Docstring of superclass - return self._motor_parameter['l_e_prime'] * currents[self.I_A_IDX] * currents[self.I_E_IDX] + return ( + self._motor_parameter["l_e_prime"] + * currents[self.I_A_IDX] + * currents[self.I_E_IDX] + ) def i_in(self, currents): # Docstring of superclass @@ -107,13 +131,15 @@ def electrical_ode(self, state, u_in, omega, *_): # Docstring of superclass return np.matmul( self._model_constants, - np.array([ - state[self.I_A_IDX], - state[self.I_E_IDX], - omega * state[self.I_E_IDX], - u_in[0], - u_in[1], - ]) + np.array( + [ + state[self.I_A_IDX], + state[self.I_E_IDX], + omega * state[self.I_E_IDX], + u_in[0], + u_in[1], + ] + ), ) def get_state_space(self, input_currents, input_voltages): @@ -130,23 +156,20 @@ def get_state_space(self, input_currents, input_voltages): a_converter = 0 e_converter = 1 low = { - 'omega': -1 if input_voltages.low[a_converter] == -1 - or input_voltages.low[e_converter] == -1 else 0, - 'torque': -1 if input_currents.low[a_converter] == -1 - or input_currents.low[e_converter] == -1 else 0, - 'i_a': -1 if input_currents.low[a_converter] == -1 else 0, - 'i_e': -1 if input_currents.low[e_converter] == -1 else 0, - 'u_a': -1 if input_voltages.low[a_converter] == -1 else 0, - 'u_e': -1 if input_voltages.low[e_converter] == -1 else 0, - } - high = { - 'omega': 1, - 'torque': 1, - 'i_a': 1, - 'i_e': 1, - 'u_a': 1, - 'u_e': 1 + "omega": -1 + if input_voltages.low[a_converter] == -1 + or input_voltages.low[e_converter] == -1 + else 0, + "torque": -1 + if input_currents.low[a_converter] == -1 + or input_currents.low[e_converter] == -1 + else 0, + "i_a": -1 if input_currents.low[a_converter] == -1 else 0, + "i_e": -1 if input_currents.low[e_converter] == -1 else 0, + "u_a": -1 if input_voltages.low[a_converter] == -1 else 0, + "u_e": -1 if input_voltages.low[e_converter] == -1 else 0, } + high = {"omega": 1, "torque": 1, "i_a": 1, "i_e": 1, "u_a": 1, "u_e": 1} return low, high def _update_limits(self, limits_d=None, nominal_d=None): @@ -155,5 +178,7 @@ def _update_limits(self, limits_d=None, nominal_d=None): limits_d = dict() # torque is replaced the same way for all DC motors - limits_d.update(dict(torque=self.torque([self._limits[state] for state in self.CURRENTS]))) + limits_d.update( + dict(torque=self.torque([self._limits[state] for state in self.CURRENTS])) + ) super()._update_limits(limits_d) diff --git a/gym_electric_motor/physical_systems/electric_motors/dc_permanently_excited_motor.py b/gym_electric_motor/physical_systems/electric_motors/dc_permanently_excited_motor.py index 614c66aa..4c1f2a94 100644 --- a/gym_electric_motor/physical_systems/electric_motors/dc_permanently_excited_motor.py +++ b/gym_electric_motor/physical_systems/electric_motors/dc_permanently_excited_motor.py @@ -14,7 +14,7 @@ class DcPermanentlyExcitedMotor(DcMotor): psi_e Wb 0.165 Magnetic Flux of the permanent magnet j_rotor kg/m^2 0.025 Moment of inertia of the rotor ===================== ========== ============= =========================================== - + =============== ====== ============================================= Motor Currents Unit Description =============== ====== ============================================= @@ -37,24 +37,28 @@ class DcPermanentlyExcitedMotor(DcMotor): u Circuit Voltage ======== =========================================================== """ + I_IDX = 0 CURRENTS_IDX = [0] - CURRENTS = ['i'] - VOLTAGES = ['u'] + CURRENTS = ["i"] + VOLTAGES = ["u"] HAS_JACOBIAN = True # Motor parameter, nominal values and limits are based on the following DC Motor: # https://www.heinzmann-electric-motors.com/en/products/dc-motors/pmg-132-dc-motor _default_motor_parameter = { - 'r_a': 16e-3, 'l_a': 19e-6, 'psi_e': 0.165, 'j_rotor': 0.025 + "r_a": 16e-3, + "l_a": 19e-6, + "psi_e": 0.165, + "j_rotor": 0.025, } _default_nominal_values = dict(omega=300, torque=16.0, i=97, u=60) _default_limits = dict(omega=400, torque=38.0, i=210, u=60) _default_initializer = { - 'states': {'i': 0.0}, - 'interval': None, - 'random_init': None, - 'random_params': (None, None) + "states": {"i": 0.0}, + "interval": None, + "random_init": None, + "random_params": (None, None), } # placeholder for omega, currents and u_in @@ -62,15 +66,13 @@ class DcPermanentlyExcitedMotor(DcMotor): def torque(self, state): # Docstring of superclass - return self._motor_parameter['psi_e'] * state[self.I_IDX] + return self._motor_parameter["psi_e"] * state[self.I_IDX] def _update_model(self): # Docstring of superclass mp = self._motor_parameter - self._model_constants = np.array([ - [-mp['psi_e'], -mp['r_a'], 1.0] - ]) - self._model_constants[self.I_IDX] /= mp['l_a'] + self._model_constants = np.array([[-mp["psi_e"], -mp["r_a"], 1.0]]) + self._model_constants[self.I_IDX] /= mp["l_a"] def i_in(self, state): # Docstring of superclass @@ -78,26 +80,28 @@ def i_in(self, state): def electrical_ode(self, state, u_in, omega, *_): # Docstring of superclass - self._ode_placeholder[:] = [omega] + np.atleast_1d(state[self.I_IDX]).tolist() + [u_in[0]] + self._ode_placeholder[:] = ( + [omega] + np.atleast_1d(state[self.I_IDX]).tolist() + [u_in[0]] + ) return np.matmul(self._model_constants, self._ode_placeholder) def electrical_jacobian(self, state, u_in, omega, *_): mp = self._motor_parameter return ( - np.array([[-mp['r_a'] / mp['l_a']]]), - np.array([-mp['psi_e'] / mp['l_a']]), - np.array([mp['psi_e']]) + np.array([[-mp["r_a"] / mp["l_a"]]]), + np.array([-mp["psi_e"] / mp["l_a"]]), + np.array([mp["psi_e"]]), ) def _update_limits(self): # Docstring of superclass # R_a might be 0, protect against that - r_a = 1 if self._motor_parameter['r_a'] == 0 else self._motor_parameter['r_a'] + r_a = 1 if self._motor_parameter["r_a"] == 0 else self._motor_parameter["r_a"] limits_agenda = { - 'u': self._default_limits['u'], - 'i': self._limits['u'] / r_a, + "u": self._default_limits["u"], + "i": self._limits["u"] / r_a, } super()._update_limits(limits_agenda) @@ -105,15 +109,15 @@ def get_state_space(self, input_currents, input_voltages): # Docstring of superclass lower_limit = 0 low = { - 'omega': -1 if input_voltages.low[0] == -1 else 0, - 'torque': -1 if input_currents.low[0] == -1 else 0, - 'i': -1 if input_currents.low[0] == -1 else 0, - 'u': -1 if input_voltages.low[0] == -1 else 0, + "omega": -1 if input_voltages.low[0] == -1 else 0, + "torque": -1 if input_currents.low[0] == -1 else 0, + "i": -1 if input_currents.low[0] == -1 else 0, + "u": -1 if input_voltages.low[0] == -1 else 0, } high = { - 'omega': 1, - 'torque': 1, - 'i': 1, - 'u': 1, + "omega": 1, + "torque": 1, + "i": 1, + "u": 1, } return low, high diff --git a/gym_electric_motor/physical_systems/electric_motors/dc_series_motor.py b/gym_electric_motor/physical_systems/electric_motors/dc_series_motor.py index a6cc3e82..72f300d0 100644 --- a/gym_electric_motor/physical_systems/electric_motors/dc_series_motor.py +++ b/gym_electric_motor/physical_systems/electric_motors/dc_series_motor.py @@ -6,64 +6,78 @@ class DcSeriesMotor(DcMotor): """The DcSeriesMotor is a DcMotor with an armature and exciting circuit connected in series to one input voltage. - ===================== ========== ============= =========================================== - Motor Parameter Unit Default Value Description - ===================== ========== ============= =========================================== - r_a Ohm 16e-3 Armature circuit resistance - r_e Ohm 48e-3 Exciting circuit resistance - l_a H 19e-6 Armature circuit inductance - l_e H 5.4e-3 Exciting circuit inductance - l_e_prime H 1.7e-3 Effective excitation inductance - j_rotor kg/m^2 0.025 Moment of inertia of the rotor - ===================== ========== ============= =========================================== - - =============== ====== ============================================= - Motor Currents Unit Description - =============== ====== ============================================= - i A Circuit current - =============== ====== ============================================= - =============== ====== ============================================= - Motor Voltages Unit Description - =============== ====== ============================================= - u V Circuit voltage - =============== ====== ============================================= - - ======== =========================================================== - Limits / Nominal Value Dictionary Entries: - -------- ----------------------------------------------------------- - Entry Description - ======== =========================================================== - i Circuit Current - omega Angular Velocity - torque Motor generated torque - u Circuit Voltage - ======== =========================================================== + ===================== ========== ============= =========================================== + Motor Parameter Unit Default Value Description + ===================== ========== ============= =========================================== + r_a Ohm 16e-3 Armature circuit resistance + r_e Ohm 48e-3 Exciting circuit resistance + l_a H 19e-6 Armature circuit inductance + l_e H 5.4e-3 Exciting circuit inductance + l_e_prime H 1.7e-3 Effective excitation inductance + j_rotor kg/m^2 0.025 Moment of inertia of the rotor + ===================== ========== ============= =========================================== + + =============== ====== ============================================= + Motor Currents Unit Description + =============== ====== ============================================= + i A Circuit current + =============== ====== ============================================= + =============== ====== ============================================= + Motor Voltages Unit Description + =============== ====== ============================================= + u V Circuit voltage + =============== ====== ============================================= + + ======== =========================================================== + Limits / Nominal Value Dictionary Entries: + -------- ----------------------------------------------------------- + Entry Description + ======== =========================================================== + i Circuit Current + omega Angular Velocity + torque Motor generated torque + u Circuit Voltage + ======== =========================================================== """ + HAS_JACOBIAN = True I_IDX = 0 CURRENTS_IDX = [0] - CURRENTS = ['i'] - VOLTAGES = ['u'] + CURRENTS = ["i"] + VOLTAGES = ["u"] # Motor parameter, nominal values and limits are based on the following DC Motor: # https://www.heinzmann-electric-motors.com/en/products/dc-motors/pmg-132-dc-motor _default_motor_parameter = { - 'r_a': 16e-3, 'r_e': 48e-3, 'l_a': 19e-6, 'l_e_prime': 1.7e-3, 'l_e': 5.4e-3, 'j_rotor': 0.0025 + "r_a": 16e-3, + "r_e": 48e-3, + "l_a": 19e-6, + "l_e_prime": 1.7e-3, + "l_e": 5.4e-3, + "j_rotor": 0.0025, + } + _default_nominal_values = dict( + omega=300, torque=16.0, i=97, i_a=97, i_e=97, u=60, u_a=60, u_e=60 + ) + _default_limits = dict( + omega=400, torque=38.0, i=210, i_a=210, i_e=210, u=60, u_a=60, u_e=60 + ) + _default_initializer = { + "states": {"i": 0.0}, + "interval": None, + "random_init": None, + "random_params": (None, None), } - _default_nominal_values = dict(omega=300, torque=16.0, i=97, i_a=97, i_e=97, u=60, u_a=60, u_e=60) - _default_limits = dict(omega=400, torque=38.0, i=210, i_a=210, i_e=210, u=60, u_a=60, u_e=60) - _default_initializer = {'states': {'i': 0.0}, - 'interval': None, - 'random_init': None, - 'random_params': (None, None)} def _update_model(self): # Docstring of superclass mp = self._motor_parameter - self._model_constants = np.array([ - [-mp['r_a'] - mp['r_e'], -mp['l_e_prime'], 1] - ]) - self._model_constants[self.I_IDX] = self._model_constants[self.I_IDX] / (mp['l_a'] + mp['l_e']) + self._model_constants = np.array( + [[-mp["r_a"] - mp["r_e"], -mp["l_e_prime"], 1]] + ) + self._model_constants[self.I_IDX] = self._model_constants[self.I_IDX] / ( + mp["l_a"] + mp["l_e"] + ) def torque(self, currents): # Docstring of superclass @@ -73,11 +87,7 @@ def electrical_ode(self, state, u_in, omega, *_): # Docstring of superclass return np.matmul( self._model_constants, - np.array([ - state[self.I_IDX], - omega * state[self.I_IDX], - u_in[0] - ]) + np.array([state[self.I_IDX], omega * state[self.I_IDX], u_in[0]]), ) def i_in(self, state): @@ -88,10 +98,10 @@ def _update_limits(self): # Docstring of superclass # R_a might be 0, protect against that - r_a = 1 if self._motor_parameter['r_a'] == 0 else self._motor_parameter['r_a'] + r_a = 1 if self._motor_parameter["r_a"] == 0 else self._motor_parameter["r_a"] limits_agenda = { - 'u': self._default_limits['u'], - 'i': self._limits['u'] / (r_a + self._motor_parameter['r_e']), + "u": self._default_limits["u"], + "i": self._limits["u"] / (r_a + self._motor_parameter["r_e"]), } super()._update_limits(limits_agenda) @@ -99,25 +109,30 @@ def get_state_space(self, input_currents, input_voltages): # Docstring of superclass lower_limit = 0 low = { - 'omega': 0, - 'torque': 0, - 'i': -1 if input_currents.low[0] == -1 else 0, - 'u': -1 if input_voltages.low[0] == -1 else 0, + "omega": 0, + "torque": 0, + "i": -1 if input_currents.low[0] == -1 else 0, + "u": -1 if input_voltages.low[0] == -1 else 0, } high = { - 'omega': 1, - 'torque': 1, - 'i': 1, - 'u': 1, + "omega": 1, + "torque": 1, + "i": 1, + "u": 1, } return low, high def electrical_jacobian(self, state, u_in, omega, *_): mp = self._motor_parameter return ( - np.array([[-(mp['r_a'] + mp['r_e'] + mp['l_e_prime'] * omega) / ( - mp['l_a'] + mp['l_e'])]]), - np.array([-mp['l_e_prime'] * state[self.I_IDX] / ( - mp['l_a'] + mp['l_e'])]), - np.array([2 * mp['l_e_prime'] * state[self.I_IDX]]) + np.array( + [ + [ + -(mp["r_a"] + mp["r_e"] + mp["l_e_prime"] * omega) + / (mp["l_a"] + mp["l_e"]) + ] + ] + ), + np.array([-mp["l_e_prime"] * state[self.I_IDX] / (mp["l_a"] + mp["l_e"])]), + np.array([2 * mp["l_e_prime"] * state[self.I_IDX]]), ) diff --git a/gym_electric_motor/physical_systems/electric_motors/dc_shunt_motor.py b/gym_electric_motor/physical_systems/electric_motors/dc_shunt_motor.py index 15c4c7d6..12276050 100644 --- a/gym_electric_motor/physical_systems/electric_motors/dc_shunt_motor.py +++ b/gym_electric_motor/physical_systems/electric_motors/dc_shunt_motor.py @@ -23,7 +23,7 @@ class DcShuntMotor(DcMotor): i_a A Armature circuit current i_e A Exciting circuit current =============== ====== ============================================= - + =============== ====== ============================================= Motor Voltages Unit Description =============== ====== ============================================= @@ -42,21 +42,31 @@ class DcShuntMotor(DcMotor): u Voltage ======== =========================================================== """ + HAS_JACOBIAN = True - VOLTAGES = ['u'] + VOLTAGES = ["u"] # Motor parameter, nominal values and limits are based on the following DC Motor: # https://www.heinzmann-electric-motors.com/en/products/dc-motors/pmg-132-dc-motor _default_motor_parameter = { - 'r_a': 16e-3, 'r_e': 4e-1, 'l_a': 19e-6, 'l_e_prime': 1.7e-3, 'l_e': 5.4e-3, 'j_rotor': 0.0025 + "r_a": 16e-3, + "r_e": 4e-1, + "l_a": 19e-6, + "l_e_prime": 1.7e-3, + "l_e": 5.4e-3, + "j_rotor": 0.0025, } - _default_nominal_values = dict(omega=300, torque=16.0, i=97, i_a=97, i_e=97, u=60, u_a=60, u_e=60) - _default_limits = dict(omega=400, torque=38.0, i=210, i_a=210, i_e=210, u=60, u_a=60, u_e=60) + _default_nominal_values = dict( + omega=300, torque=16.0, i=97, i_a=97, i_e=97, u=60, u_a=60, u_e=60 + ) + _default_limits = dict( + omega=400, torque=38.0, i=210, i_a=210, i_e=210, u=60, u_a=60, u_e=60 + ) _default_initializer = { - 'states': {'i_a': 0.0, 'i_e': 0.0}, - 'interval': None, - 'random_init': None, - 'random_params': (None, None) + "states": {"i_a": 0.0, "i_e": 0.0}, + "interval": None, + "random_init": None, + "random_params": (None, None), } def i_in(self, state): @@ -70,12 +80,19 @@ def electrical_ode(self, state, u_in, omega, *_): def electrical_jacobian(self, state, u_in, omega, *_): mp = self._motor_parameter return ( - np.array([ - [-mp['r_a'] / mp['l_a'], -mp['l_e_prime'] / mp['l_a'] * omega], - [0, -mp['r_e'] / mp['l_e']] - ]), - np.array([-mp['l_e_prime'] * state[self.I_E_IDX] / mp['l_a'], 0]), - np.array([mp['l_e_prime'] * state[self.I_E_IDX], mp['l_e_prime'] * state[self.I_A_IDX]]) + np.array( + [ + [-mp["r_a"] / mp["l_a"], -mp["l_e_prime"] / mp["l_a"] * omega], + [0, -mp["r_e"] / mp["l_e"]], + ] + ), + np.array([-mp["l_e_prime"] * state[self.I_E_IDX] / mp["l_a"], 0]), + np.array( + [ + mp["l_e_prime"] * state[self.I_E_IDX], + mp["l_e_prime"] * state[self.I_A_IDX], + ] + ), ) def get_state_space(self, input_currents, input_voltages): @@ -92,18 +109,18 @@ def get_state_space(self, input_currents, input_voltages): lower_limit = 0 low = { - 'omega': 0, - 'torque': -1 if input_currents.low[0] == -1 else 0, - 'i_a': -1 if input_currents.low[0] == -1 else 0, - 'i_e': -1 if input_currents.low[0] == -1 else 0, - 'u': -1 if input_voltages.low[0] == -1 else 0, + "omega": 0, + "torque": -1 if input_currents.low[0] == -1 else 0, + "i_a": -1 if input_currents.low[0] == -1 else 0, + "i_e": -1 if input_currents.low[0] == -1 else 0, + "u": -1 if input_voltages.low[0] == -1 else 0, } high = { - 'omega': 1, - 'torque': 1, - 'i_a': 1, - 'i_e': 1, - 'u': 1, + "omega": 1, + "torque": 1, + "i_a": 1, + "i_e": 1, + "u": 1, } return low, high @@ -111,12 +128,13 @@ def _update_limits(self, limits_d=None, nominal_d=None): # Docstring of superclass # R_a might be 0, protect against that - r_a = 1 if self._motor_parameter['r_a'] == 0 else self._motor_parameter['r_a'] + r_a = 1 if self._motor_parameter["r_a"] == 0 else self._motor_parameter["r_a"] limit_agenda = { - 'u': self._default_limits['u'], - 'i_a': self._limits.get('i', None) or self._limits['u'] / r_a, - 'i_e': self._limits.get('i', None) or self._limits['u'] / self.motor_parameter['r_e'], + "u": self._default_limits["u"], + "i_a": self._limits.get("i", None) or self._limits["u"] / r_a, + "i_e": self._limits.get("i", None) + or self._limits["u"] / self.motor_parameter["r_e"], } super()._update_limits(limit_agenda) diff --git a/gym_electric_motor/physical_systems/electric_motors/doubly_fed_induction_motor.py b/gym_electric_motor/physical_systems/electric_motors/doubly_fed_induction_motor.py index a0742e6c..e4a5c6f4 100644 --- a/gym_electric_motor/physical_systems/electric_motors/doubly_fed_induction_motor.py +++ b/gym_electric_motor/physical_systems/electric_motors/doubly_fed_induction_motor.py @@ -6,119 +6,129 @@ class DoublyFedInductionMotor(InductionMotor): """ - ===================== ========== ============= =========================================== - Motor Parameter Unit Default Value Description - ===================== ========== ============= =========================================== - r_s Ohm 4.42 Stator resistance - r_r Ohm 3.51 Rotor resistance - l_m H 297.5e-3 Main inductance - l_sigs H 25.71e-3 Stator-side stray inductance - l_sigr H 25.71e-3 Rotor-side stray inductance - p 1 2 Pole pair number - j_rotor kg/m^2 13.695e-3 Moment of inertia of the rotor - ===================== ========== ============= =========================================== - - =============== ====== ============================================= - Motor Currents Unit Description - =============== ====== ============================================= - i_sd A Direct axis current - i_sq A Quadrature axis current - i_sa A Current through branch a - i_sb A Current through branch b - i_sc A Current through branch c - i_salpha A Current in alpha axis - i_sbeta A Current in beta axis - =============== ====== ============================================= - =============== ====== ============================================= - Rotor flux Unit Description - =============== ====== ============================================= - psi_rd Vs Direct axis of the rotor oriented flux - psi_rq Vs Quadrature axis of the rotor oriented flux - psi_ra Vs Rotor oriented flux in branch a - psi_rb Vs Rotor oriented flux in branch b - psi_rc Vs Rotor oriented flux in branch c - psi_ralpha Vs Rotor oriented flux in alpha direction - psi_rbeta Vs Rotor oriented flux in beta direction - =============== ====== ============================================= - =============== ====== ============================================= - Motor Voltages Unit Description - =============== ====== ============================================= - u_sd V Direct axis voltage - u_sq V Quadrature axis voltage - u_sa V Stator voltage through branch a - u_sb V Stator voltage through branch b - u_sc V Stator voltage through branch c - u_salpha V Stator voltage in alpha axis - u_sbeta V Stator voltage in beta axis - u_ralpha V Rotor voltage in alpha axis - u_rbeta V Rotor voltage in beta axis - =============== ====== ============================================= - ======== =========================================================== - Limits / Nominal Value Dictionary Entries: - -------- ----------------------------------------------------------- - Entry Description - ======== =========================================================== - i General current limit / nominal value - i_sa Current in phase a - i_sb Current in phase b - i_sc Current in phase c - i_salpha Current in alpha axis - i_sbeta Current in beta axis - i_sd Current in direct axis - i_sq Current in quadrature axis - omega Mechanical angular Velocity - torque Motor generated torque - u_sa Voltage in phase a - u_sb Voltage in phase b - u_sc Voltage in phase c - u_salpha Voltage in alpha axis - u_sbeta Voltage in beta axis - u_sd Voltage in direct axis - u_sq Voltage in quadrature axis - u_ralpha Rotor voltage in alpha axis - u_rbeta Rotor voltage in beta axis - ======== =========================================================== + ===================== ========== ============= =========================================== + Motor Parameter Unit Default Value Description + ===================== ========== ============= =========================================== + r_s Ohm 4.42 Stator resistance + r_r Ohm 3.51 Rotor resistance + l_m H 297.5e-3 Main inductance + l_sigs H 25.71e-3 Stator-side stray inductance + l_sigr H 25.71e-3 Rotor-side stray inductance + p 1 2 Pole pair number + j_rotor kg/m^2 13.695e-3 Moment of inertia of the rotor + ===================== ========== ============= =========================================== - Note: - The voltage limits should be the peak-to-peak value of the phase voltage (:math:`\hat{u}_S`). - A phase voltage denotes the potential difference from a line to the neutral point in contrast to the line voltage between two lines. - Typically the RMS value for the line voltage (:math:`U_L`) is given as - :math:`\hat{u}_S=\sqrt{2/3}~U_L` + =============== ====== ============================================= + Motor Currents Unit Description + =============== ====== ============================================= + i_sd A Direct axis current + i_sq A Quadrature axis current + i_sa A Current through branch a + i_sb A Current through branch b + i_sc A Current through branch c + i_salpha A Current in alpha axis + i_sbeta A Current in beta axis + =============== ====== ============================================= + =============== ====== ============================================= + Rotor flux Unit Description + =============== ====== ============================================= + psi_rd Vs Direct axis of the rotor oriented flux + psi_rq Vs Quadrature axis of the rotor oriented flux + psi_ra Vs Rotor oriented flux in branch a + psi_rb Vs Rotor oriented flux in branch b + psi_rc Vs Rotor oriented flux in branch c + psi_ralpha Vs Rotor oriented flux in alpha direction + psi_rbeta Vs Rotor oriented flux in beta direction + =============== ====== ============================================= + =============== ====== ============================================= + Motor Voltages Unit Description + =============== ====== ============================================= + u_sd V Direct axis voltage + u_sq V Quadrature axis voltage + u_sa V Stator voltage through branch a + u_sb V Stator voltage through branch b + u_sc V Stator voltage through branch c + u_salpha V Stator voltage in alpha axis + u_sbeta V Stator voltage in beta axis + u_ralpha V Rotor voltage in alpha axis + u_rbeta V Rotor voltage in beta axis + =============== ====== ============================================= + ======== =========================================================== + Limits / Nominal Value Dictionary Entries: + -------- ----------------------------------------------------------- + Entry Description + ======== =========================================================== + i General current limit / nominal value + i_sa Current in phase a + i_sb Current in phase b + i_sc Current in phase c + i_salpha Current in alpha axis + i_sbeta Current in beta axis + i_sd Current in direct axis + i_sq Current in quadrature axis + omega Mechanical angular Velocity + torque Motor generated torque + u_sa Voltage in phase a + u_sb Voltage in phase b + u_sc Voltage in phase c + u_salpha Voltage in alpha axis + u_sbeta Voltage in beta axis + u_sd Voltage in direct axis + u_sq Voltage in quadrature axis + u_ralpha Rotor voltage in alpha axis + u_rbeta Rotor voltage in beta axis + ======== =========================================================== - The current limits should be the peak-to-peak value of the phase current (:math:`\hat{i}_S`). - Typically the RMS value for the phase current (:math:`I_S`) is given as - :math:`\hat{i}_S = \sqrt{2}~I_S` + Note: + The voltage limits should be the peak-to-peak value of the phase voltage (:math:`\hat{u}_S`). + A phase voltage denotes the potential difference from a line to the neutral point in contrast to the line voltage between two lines. + Typically the RMS value for the line voltage (:math:`U_L`) is given as + :math:`\hat{u}_S=\sqrt{2/3}~U_L` - If not specified, nominal values are equal to their corresponding limit values. - Furthermore, if specific limits/nominal values (e.g. i_a) are not specified they are inferred from - the general limits/nominal values (e.g. i) + The current limits should be the peak-to-peak value of the phase current (:math:`\hat{i}_S`). + Typically the RMS value for the phase current (:math:`I_S`) is given as + :math:`\hat{i}_S = \sqrt{2}~I_S` + + If not specified, nominal values are equal to their corresponding limit values. + Furthermore, if specific limits/nominal values (e.g. i_a) are not specified they are inferred from + the general limits/nominal values (e.g. i) """ - ROTOR_VOLTAGES = ['u_ralpha', 'u_rbeta'] - ROTOR_CURRENTS = ['i_ralpha', 'i_rbeta'] + ROTOR_VOLTAGES = ["u_ralpha", "u_rbeta"] + ROTOR_CURRENTS = ["i_ralpha", "i_rbeta"] - IO_ROTOR_VOLTAGES = ['u_ra', 'u_rb', 'u_rc', 'u_rd', 'u_rq'] - IO_ROTOR_CURRENTS = ['i_ra', 'i_rb', 'i_rc', 'i_rd', 'i_rq'] + IO_ROTOR_VOLTAGES = ["u_ra", "u_rb", "u_rc", "u_rd", "u_rq"] + IO_ROTOR_CURRENTS = ["i_ra", "i_rb", "i_rc", "i_rd", "i_rq"] #### Parameters taken from DOI: 10.1016/j.jestch.2016.01.015 (N. Kumar, T. R. Chelliah, S. P. Srivastava) _default_motor_parameter = { - 'p': 2, - 'l_m': 297.5e-3, - 'l_sigs': 25.71e-3, - 'l_sigr': 25.71e-3, - 'j_rotor': 13.695e-3, - 'r_s': 4.42, - 'r_r': 3.51, + "p": 2, + "l_m": 297.5e-3, + "l_sigs": 25.71e-3, + "l_sigr": 25.71e-3, + "j_rotor": 13.695e-3, + "r_s": 4.42, + "r_r": 3.51, } - _default_limits = dict(omega=1800 * np.pi / 30, torque=0.0, i=9, epsilon=math.pi, u=720) - _default_nominal_values = dict(omega=1650 * np.pi / 30, torque=0.0, i=7.5, epsilon=math.pi, u=720) - _default_initializer = {'states': {'i_salpha': 0.0, 'i_sbeta': 0.0, - 'psi_ralpha': 0.0, 'psi_rbeta': 0.0, - 'epsilon': 0.0}, - 'interval': None, - 'random_init': None, - 'random_params': (None, None)} + _default_limits = dict( + omega=1800 * np.pi / 30, torque=0.0, i=9, epsilon=math.pi, u=720 + ) + _default_nominal_values = dict( + omega=1650 * np.pi / 30, torque=0.0, i=7.5, epsilon=math.pi, u=720 + ) + _default_initializer = { + "states": { + "i_salpha": 0.0, + "i_sbeta": 0.0, + "psi_ralpha": 0.0, + "psi_rbeta": 0.0, + "epsilon": 0.0, + }, + "interval": None, + "random_init": None, + "random_params": (None, None), + } def __init__(self, **kwargs): self.IO_VOLTAGES += self.IO_ROTOR_VOLTAGES @@ -128,29 +138,37 @@ def __init__(self, **kwargs): def _update_limits(self, limit_values={}, nominal_values={}): # Docstring of superclass - voltage_limit = 0.5 * self._limits['u'] - voltage_nominal = 0.5 * self._nominal_values['u'] + voltage_limit = 0.5 * self._limits["u"] + voltage_nominal = 0.5 * self._nominal_values["u"] limits_agenda = {} nominal_agenda = {} - for u, i in zip(self.IO_VOLTAGES+self.ROTOR_VOLTAGES, - self.IO_CURRENTS+self.ROTOR_CURRENTS): + for u, i in zip( + self.IO_VOLTAGES + self.ROTOR_VOLTAGES, + self.IO_CURRENTS + self.ROTOR_CURRENTS, + ): limits_agenda[u] = voltage_limit nominal_agenda[u] = voltage_nominal - limits_agenda[i] = self._limits.get('i', None) or \ - self._limits[u] / self._motor_parameter['r_r'] - nominal_agenda[i] = self._nominal_values.get('i', None) or \ - self._nominal_values[u] / \ - self._motor_parameter['r_r'] + limits_agenda[i] = ( + self._limits.get("i", None) + or self._limits[u] / self._motor_parameter["r_r"] + ) + nominal_agenda[i] = ( + self._nominal_values.get("i", None) + or self._nominal_values[u] / self._motor_parameter["r_r"] + ) super()._update_limits(limits_agenda, nominal_agenda) def _update_initial_limits(self, nominal_new={}, omega=None): # Docstring of superclass # draw a sample magnetic field angle from [-pi,pi] eps_mag = 2 * np.pi * np.random.random_sample() - np.pi - flux_alphabeta_limits = self._flux_limit(omega=omega, - eps_mag=eps_mag, - u_q_max=self._nominal_values['u_sq'], - u_rq_max=self._nominal_values['u_rq']) - flux_nominal_limits = {state: value for state, value in - zip(self.FLUXES, flux_alphabeta_limits)} + flux_alphabeta_limits = self._flux_limit( + omega=omega, + eps_mag=eps_mag, + u_q_max=self._nominal_values["u_sq"], + u_rq_max=self._nominal_values["u_rq"], + ) + flux_nominal_limits = { + state: value for state, value in zip(self.FLUXES, flux_alphabeta_limits) + } super()._update_initial_limits(flux_nominal_limits) diff --git a/gym_electric_motor/physical_systems/electric_motors/electric_motor.py b/gym_electric_motor/physical_systems/electric_motors/electric_motor.py index 91cba614..6173c57d 100644 --- a/gym_electric_motor/physical_systems/electric_motors/electric_motor.py +++ b/gym_electric_motor/physical_systems/electric_motors/electric_motor.py @@ -8,27 +8,27 @@ class ElectricMotor(RandomComponent): """Base class for all technical electrical motor models. - A motor consists of the ode-state. These are the dynamic quantities of its ODE. - For example: - ODE-State of a DC-shunt motor: `` [i_a, i_e ] `` - * i_a: Anchor circuit current - * i_e: Exciting circuit current - - Each electric motor can be parametrized by a dictionary of motor parameters, - the nominal state dictionary and the limit dictionary. - - Initialization is given by initializer(dict). It can be constant state value - or random value in given interval. - dict should be like: - { 'states'(dict): with state names and initital values - 'interval'(array like): boundaries for each state - (only for random init), shape(num states, 2) - 'random_init'(str): 'uniform' or 'normal' - 'random_params(tuple): mue(float), sigma(int) - Example initializer(dict) for constant initialization: - { 'states': {'omega': 16.0}} - Example initializer(dict) for random initialization: - { 'random_init': 'normal'} + A motor consists of the ode-state. These are the dynamic quantities of its ODE. + For example: + ODE-State of a DC-shunt motor: `` [i_a, i_e ] `` + * i_a: Anchor circuit current + * i_e: Exciting circuit current + + Each electric motor can be parametrized by a dictionary of motor parameters, + the nominal state dictionary and the limit dictionary. + + Initialization is given by initializer(dict). It can be constant state value + or random value in given interval. + dict should be like: + { 'states'(dict): with state names and initital values + 'interval'(array like): boundaries for each state + (only for random init), shape(num states, 2) + 'random_init'(str): 'uniform' or 'normal' + 'random_params(tuple): mue(float), sigma(int) + Example initializer(dict) for constant initialization: + { 'states': {'omega': 16.0}} + Example initializer(dict) for random initialization: + { 'random_init': 'normal'} """ #: Parameter indicating if the class is implementing the optional jacobian function @@ -50,10 +50,10 @@ class ElectricMotor(RandomComponent): _default_limits = {} #: _default_initial_state(dict): Default initial motor-state values _default_initializer = { - 'states': {}, - 'interval': None, - 'random_init': None, - 'random_params': None + "states": {}, + "interval": None, + "random_init": None, + "random_params": None, } #: _default_initial_limits(dict): Default limit for initialization _default_initial_limits = {} @@ -104,8 +104,12 @@ def initial_limits(self): return self._initial_limits def __init__( - self, motor_parameter=None, nominal_values=None, limit_values=None, motor_initializer=None, - initial_limits=None + self, + motor_parameter=None, + nominal_values=None, + limit_values=None, + motor_initializer=None, + initial_limits=None, ): """ :param motor_parameter: Motor parameter dictionary. Contents specified @@ -121,26 +125,28 @@ def __init__( motor_parameter = motor_parameter or {} self._motor_parameter = self._default_motor_parameter.copy() self._motor_parameter = update_parameter_dict( - self._default_motor_parameter, motor_parameter) + self._default_motor_parameter, motor_parameter + ) limit_values = limit_values or {} - self._limits = update_parameter_dict( - self._default_limits, limit_values) + self._limits = update_parameter_dict(self._default_limits, limit_values) nominal_values = nominal_values or {} self._nominal_values = update_parameter_dict( - self._default_nominal_values, nominal_values) + self._default_nominal_values, nominal_values + ) motor_initializer = motor_initializer or {} self._initializer = update_parameter_dict( - self._default_initializer, motor_initializer) + self._default_initializer, motor_initializer + ) self._initial_states = {} - if self._initializer['states'] is not None: - self._initial_states.update(self._initializer['states']) + if self._initializer["states"] is not None: + self._initial_states.update(self._initializer["states"]) # intialize limits, in general they're not needed to be changed # during training or episodes initial_limits = initial_limits or {} self._initial_limits = self._nominal_values.copy() self._initial_limits.update(initial_limits) # preventing wrong user input for the basic case - assert isinstance(self._initializer, dict), 'wrong initializer' + assert isinstance(self._initializer, dict), "wrong initializer" def electrical_ode(self, state, u_in, omega, *_): """Calculation of the derivatives of each motor state variable for the given inputs / The motors ODE-System. @@ -187,20 +193,23 @@ def initialize(self, state_space, state_positions, **__): state_positions(dict): indices of system states (given by physical system) """ # for organization purposes - interval = self._initializer['interval'] - random_dist = self._initializer['random_init'] - random_params = self._initializer['random_params'] - self._initial_states.update(self._default_initializer['states']) - if self._initializer['states'] is not None: - self._initial_states.update(self._initializer['states']) + interval = self._initializer["interval"] + random_dist = self._initializer["random_init"] + random_params = self._initializer["random_params"] + self._initial_states.update(self._default_initializer["states"]) + if self._initializer["states"] is not None: + self._initial_states.update(self._initializer["states"]) # different limits for InductionMotor - if any(state in self._initial_states for state in ['psi_ralpha', 'psi_rbeta']): + if any(state in self._initial_states for state in ["psi_ralpha", "psi_rbeta"]): # caution: _initial_limits sometimes contains singleton ndarrays, they must be # extracted with .item() - nominal_values_ =\ - [self._initial_limits[state].item() if isinstance(self._initial_limits[state], np.ndarray) - else self._initial_limits[state] for state in self._initial_states] + nominal_values_ = [ + self._initial_limits[state].item() + if isinstance(self._initial_limits[state], np.ndarray) + else self._initial_limits[state] + for state in self._initial_states + ] upper_bound = np.asarray(np.abs(nominal_values_), dtype=float) # state space for Induction Envs based on documentation # ['i_salpha', 'i_sbeta', 'psi_ralpha', 'psi_rbeta', 'epsilon'] @@ -209,8 +218,9 @@ def initialize(self, state_space, state_positions, **__): lower_bound = upper_bound * state_space_low else: if isinstance(self._nominal_values, dict): - nominal_values_ = [self._nominal_values[state] - for state in self._initial_states.keys()] + nominal_values_ = [ + self._nominal_values[state] for state in self._initial_states.keys() + ] nominal_values_ = np.asarray(nominal_values_) else: nominal_values_ = np.asarray(self._nominal_values) @@ -220,46 +230,49 @@ def initialize(self, state_space, state_positions, **__): ] upper_bound = np.asarray(nominal_values_, dtype=float) - lower_bound = upper_bound * \ - np.asarray(state_space.low, dtype=float)[state_space_idx] + lower_bound = ( + upper_bound * np.asarray(state_space.low, dtype=float)[state_space_idx] + ) # clip nominal boundaries to user defined if interval is not None: lower_bound = np.clip( - lower_bound, - a_min=np.asarray(interval, dtype=float).T[0], - a_max=None + lower_bound, a_min=np.asarray(interval, dtype=float).T[0], a_max=None ) upper_bound = np.clip( - upper_bound, - a_min=None, - a_max=np.asarray(interval, dtype=float).T[1] + upper_bound, a_min=None, a_max=np.asarray(interval, dtype=float).T[1] ) # random initialization for each motor state (current, epsilon) if random_dist is not None: - if random_dist == 'uniform': - initial_value = (upper_bound - lower_bound) \ - * self._random_generator.uniform(size=len(self._initial_states.keys())) \ - + lower_bound + if random_dist == "uniform": + initial_value = ( + upper_bound - lower_bound + ) * self._random_generator.uniform( + size=len(self._initial_states.keys()) + ) + lower_bound # writing initial values in initial_states dict random_states = { - state: initial_value[idx] for idx, state in enumerate(self._initial_states.keys()) + state: initial_value[idx] + for idx, state in enumerate(self._initial_states.keys()) } self._initial_states.update(random_states) - elif random_dist in ['normal', 'gaussian']: + elif random_dist in ["normal", "gaussian"]: # specific input or middle of interval - mue = random_params[0] or ( - upper_bound - lower_bound) / 2 + lower_bound + mue = random_params[0] or (upper_bound - lower_bound) / 2 + lower_bound sigma = random_params[1] or 1 a, b = (lower_bound - mue) / sigma, (upper_bound - mue) / sigma initial_value = truncnorm.rvs( - a, b, loc=mue, scale=sigma, size=( - len(self._initial_states.keys())), - random_state=self.seed_sequence.pool[0] + a, + b, + loc=mue, + scale=sigma, + size=(len(self._initial_states.keys())), + random_state=self.seed_sequence.pool[0], ) # writing initial values in initial_states dict random_states = { - state: initial_value[idx] for idx, state in enumerate(self._initial_states.keys()) + state: initial_value[idx] + for idx, state in enumerate(self._initial_states.keys()) } self._initial_states.update(random_states) @@ -269,17 +282,20 @@ def initialize(self, state_space, state_positions, **__): elif self._initial_states is not None: initial_value = np.atleast_1d(list(self._initial_states.values())) # check init_value meets interval boundaries - if ((lower_bound <= initial_value).all() - and (initial_value <= upper_bound).all()): - initial_states_ = \ - {state: initial_value[idx] - for idx, state in enumerate(self._initial_states.keys())} + if (lower_bound <= initial_value).all() and ( + initial_value <= upper_bound + ).all(): + initial_states_ = { + state: initial_value[idx] + for idx, state in enumerate(self._initial_states.keys()) + } self._initial_states.update(initial_states_) else: raise Exception( - 'Initialization value has to be within nominal boundaries') + "Initialization value has to be within nominal boundaries" + ) else: - raise Exception('No matching Initialization Case') + raise Exception("No matching Initialization Case") def reset(self, state_space, state_positions, **__): """Reset the motors state to a new initial state. (Default 0) @@ -292,7 +308,7 @@ def reset(self, state_space, state_positions, **__): """ # check for valid initializer self.next_generator() - if self._initializer and self._initializer['states']: + if self._initializer and self._initializer["states"]: self.initialize(state_space, state_positions) return np.asarray(list(self._initial_states.values())) else: @@ -319,7 +335,7 @@ def _update_limits(self, limits_d=None, nominal_d=None): if nominal_d is None: nominal_d = dict() # omega is replaced the same way for all motor types - limits_d.update(dict(omega=self._default_limits['omega'])) + limits_d.update(dict(omega=self._default_limits["omega"])) for qty, lim in limits_d.items(): if self._limits.get(qty, 0) == 0: @@ -327,8 +343,7 @@ def _update_limits(self, limits_d=None, nominal_d=None): for entry in self._limits.keys(): if self._nominal_values.get(entry, 0) == 0: - self._nominal_values[entry] = nominal_d.get( - entry, self._limits[entry]) + self._nominal_values[entry] = nominal_d.get(entry, self._limits[entry]) def _update_initial_limits(self, nominal_new=None): """Complete initial states with further state limits diff --git a/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py b/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py index 077da978..ad8d4bf3 100644 --- a/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py +++ b/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py @@ -92,48 +92,93 @@ class ExternallyExcitedSynchronousMotor(SynchronousMotor): I_E_IDX = 2 EPSILON_IDX = 3 CURRENTS_IDX = [0, 1, 2] - CURRENTS = ['i_sd', 'i_sq', 'i_e'] - VOLTAGES = ['u_sd', 'u_sq', 'u_e'] + CURRENTS = ["i_sd", "i_sq", "i_e"] + VOLTAGES = ["u_sd", "u_sq", "u_e"] #### Parameters taken from DOI: 10.1109/ICELMACH.2014.6960287 (C. D. Nguyen; W. Hofmann) _default_motor_parameter = { - 'p': 3, - 'l_d': 1.66e-3, - 'l_q': 0.35e-3, - 'l_m': 1.589e-3, - 'l_e': 1.74e-3, - 'j_rotor': 0.3883, - 'r_s': 15.55e-3, - 'r_e': 7.2e-3, + "p": 3, + "l_d": 1.66e-3, + "l_q": 0.35e-3, + "l_m": 1.589e-3, + "l_e": 1.74e-3, + "j_rotor": 0.3883, + "r_s": 15.55e-3, + "r_e": 7.2e-3, } HAS_JACOBIAN = True - _default_limits = dict(omega=12e3 * np.pi / 30, torque=0.0, i=150, i_e=150, epsilon=math.pi, u=320) - _default_nominal_values = dict(omega=4.3e3 * np.pi / 30, torque=0.0, i=120, i_e=150, epsilon=math.pi, u=320) + _default_limits = dict( + omega=12e3 * np.pi / 30, torque=0.0, i=150, i_e=150, epsilon=math.pi, u=320 + ) + _default_nominal_values = dict( + omega=4.3e3 * np.pi / 30, torque=0.0, i=120, i_e=150, epsilon=math.pi, u=320 + ) _default_initializer = { - 'states': {'i_sq': 0.0, 'i_sd': 0.0, 'i_e': 0.0, 'epsilon': 0.0}, - 'interval': None, - 'random_init': None, - 'random_params': (None, None) + "states": {"i_sq": 0.0, "i_sd": 0.0, "i_e": 0.0, "epsilon": 0.0}, + "interval": None, + "random_init": None, + "random_params": (None, None), } - IO_VOLTAGES = ['u_a', 'u_b', 'u_c', 'u_sd', 'u_sq', 'u_e'] - IO_CURRENTS = ['i_a', 'i_b', 'i_c', 'i_sd', 'i_sq', 'i_e'] + IO_VOLTAGES = ["u_a", "u_b", "u_c", "u_sd", "u_sq", "u_e"] + IO_CURRENTS = ["i_a", "i_b", "i_c", "i_sd", "i_sq", "i_e"] def _update_model(self): # Docstring of superclass mp = self._motor_parameter - sigma = 1 - mp['l_m'] ** 2 / (mp['l_d'] * mp['l_e']) - self._model_constants = np.array([ - # omega, i_d, i_q, i_e, u_d, u_q, u_e, omega * i_d, omega * i_q, omega * i_e - [ 0, -mp['r_s'] / sigma, 0, mp['l_m'] * mp['r_e'] / (sigma * mp['l_e']), 1 / sigma, 0, -mp['l_m'] / (sigma * mp['l_e']), 0, mp['l_q'] * mp['p'] / sigma, 0], - [ 0, 0, -mp['r_s'], 0, 0, 1, 0, -mp['l_d'] * mp['p'], 0, -mp['p'] * mp['l_m']], - [ 0, mp['l_m'] * mp['r_s'] / (sigma * mp['l_d']), 0, -mp['r_e'] / sigma, -mp['l_m'] / (sigma * mp['l_d']), 0, 1 / sigma, 0, -mp['p'] * mp['l_m'] * mp['l_q'] / (sigma * mp['l_d']), 0], - [ mp['p'], 0, 0, 0, 0, 0, 0, 0, 0, 0], - ]) - - self._model_constants[self.I_SD_IDX] = self._model_constants[self.I_SD_IDX] / mp['l_d'] - self._model_constants[self.I_SQ_IDX] = self._model_constants[self.I_SQ_IDX] / mp['l_q'] - self._model_constants[self.I_E_IDX] = self._model_constants[self.I_E_IDX] / mp['l_e'] + sigma = 1 - mp["l_m"] ** 2 / (mp["l_d"] * mp["l_e"]) + self._model_constants = np.array( + [ + # omega, i_d, i_q, i_e, u_d, u_q, u_e, omega * i_d, omega * i_q, omega * i_e + [ + 0, + -mp["r_s"] / sigma, + 0, + mp["l_m"] * mp["r_e"] / (sigma * mp["l_e"]), + 1 / sigma, + 0, + -mp["l_m"] / (sigma * mp["l_e"]), + 0, + mp["l_q"] * mp["p"] / sigma, + 0, + ], + [ + 0, + 0, + -mp["r_s"], + 0, + 0, + 1, + 0, + -mp["l_d"] * mp["p"], + 0, + -mp["p"] * mp["l_m"], + ], + [ + 0, + mp["l_m"] * mp["r_s"] / (sigma * mp["l_d"]), + 0, + -mp["r_e"] / sigma, + -mp["l_m"] / (sigma * mp["l_d"]), + 0, + 1 / sigma, + 0, + -mp["p"] * mp["l_m"] * mp["l_q"] / (sigma * mp["l_d"]), + 0, + ], + [mp["p"], 0, 0, 0, 0, 0, 0, 0, 0, 0], + ] + ) + + self._model_constants[self.I_SD_IDX] = ( + self._model_constants[self.I_SD_IDX] / mp["l_d"] + ) + self._model_constants[self.I_SQ_IDX] = ( + self._model_constants[self.I_SQ_IDX] / mp["l_q"] + ) + self._model_constants[self.I_E_IDX] = ( + self._model_constants[self.I_E_IDX] / mp["l_e"] + ) def electrical_ode(self, state, u_dq, omega, *_): """ @@ -147,60 +192,104 @@ def electrical_ode(self, state, u_dq, omega, *_): Returns: The derivatives of the state vector d/dt([i_sd, i_sq, epsilon]) """ - return np.matmul(self._model_constants, np.array([ - omega, - state[self.I_SD_IDX], - state[self.I_SQ_IDX], - state[self.I_E_IDX], - u_dq[0], - u_dq[1], - u_dq[2], - omega * state[self.I_SD_IDX], - omega * state[self.I_SQ_IDX], - omega * state[self.I_E_IDX] - ])) + return np.matmul( + self._model_constants, + np.array( + [ + omega, + state[self.I_SD_IDX], + state[self.I_SQ_IDX], + state[self.I_E_IDX], + u_dq[0], + u_dq[1], + u_dq[2], + omega * state[self.I_SD_IDX], + omega * state[self.I_SQ_IDX], + omega * state[self.I_E_IDX], + ] + ), + ) def _torque_limit(self): # Docstring of superclass mp = self._motor_parameter - if mp['l_d'] == mp['l_q']: - return self.torque([0, self._limits['i_sq'], self._limits['i_e'], 0]) + if mp["l_d"] == mp["l_q"]: + return self.torque([0, self._limits["i_sq"], self._limits["i_e"], 0]) else: - i_n = self.nominal_values['i'] - _p = mp['l_m'] * i_n / (2 * (mp['l_d'] - mp['l_q'])) - _q = - i_n ** 2 / 2 - if mp['l_d'] < mp['l_q']: - i_d_opt = - _p / 2 - np.sqrt( (_p / 2) ** 2 - _q) + i_n = self.nominal_values["i"] + _p = mp["l_m"] * i_n / (2 * (mp["l_d"] - mp["l_q"])) + _q = -i_n**2 / 2 + if mp["l_d"] < mp["l_q"]: + i_d_opt = -_p / 2 - np.sqrt((_p / 2) ** 2 - _q) else: - i_d_opt = - _p / 2 + np.sqrt( (_p / 2) ** 2 - _q) - i_q_opt = np.sqrt(i_n ** 2 - i_d_opt ** 2) - return self.torque([i_d_opt, i_q_opt, self._limits['i_e'], 0]) + i_d_opt = -_p / 2 + np.sqrt((_p / 2) ** 2 - _q) + i_q_opt = np.sqrt(i_n**2 - i_d_opt**2) + return self.torque([i_d_opt, i_q_opt, self._limits["i_e"], 0]) def torque(self, currents): # Docstring of superclass mp = self._motor_parameter - return 1.5 * mp['p'] * (mp['l_m'] * currents[self.I_E_IDX] + (mp['l_d'] - mp['l_q']) * currents[self.I_SD_IDX]) * currents[self.I_SQ_IDX] + return ( + 1.5 + * mp["p"] + * ( + mp["l_m"] * currents[self.I_E_IDX] + + (mp["l_d"] - mp["l_q"]) * currents[self.I_SD_IDX] + ) + * currents[self.I_SQ_IDX] + ) def electrical_jacobian(self, state, u_in, omega, *args): mp = self._motor_parameter - sigma = 1 - mp['l_m'] ** 2 / (mp['l_d'] * mp['l_e']) + sigma = 1 - mp["l_m"] ** 2 / (mp["l_d"] * mp["l_e"]) return ( - np.array([ # dx'/dx - [ -mp['r_s'] / mp['l_d'], mp['l_q'] / (sigma * mp['l_d']) * omega * mp['p'], mp['l_m'] * mp['r_e'] / (sigma * mp['l_d'] * mp['l_e']), 0], - [ -mp['l_d'] / mp['l_q'] * omega * mp['p'], -mp['r_s'] / mp['l_q'], -omega * mp['p'] * mp['l_e'] / mp['l_q'], 0], - [mp['l_m'] * mp['r_s'] / (sigma * mp['l_d'] * mp['l_e']), -omega * mp['p'] * mp['l_m'] * mp['l_q'] / (sigma * mp['l_d'] * mp['l_e']), -mp['r_e'] / mp['l_e'], 0], - [ 0, 0, 0, 0], - ]), - np.array([ # dx'/dw - mp['p'] * mp['l_q'] / mp['l_d'] * state[self.I_SQ_IDX], - -mp['p'] * mp['l_d'] / mp['l_q'] * state[self.I_SD_IDX] - mp['p'] * mp['l_m'] / mp['l_q'] * state[self.I_E_IDX], - -mp['p'], - mp['p'], - ]), - np.array([ # dT/dx - 1.5 * mp['p'] * (mp['l_d'] - mp['l_q']) * state[self.I_SQ_IDX], - 1.5 * mp['p'] * (mp['l_e'] * state[self.I_E_IDX] + (mp['l_d'] - mp['l_q']) * state[self.I_SD_IDX]), - 1.5 * mp['p'] * mp['l_e'] * state[self.I_SQ_IDX], - 0, - ]) + np.array( + [ # dx'/dx + [ + -mp["r_s"] / mp["l_d"], + mp["l_q"] / (sigma * mp["l_d"]) * omega * mp["p"], + mp["l_m"] * mp["r_e"] / (sigma * mp["l_d"] * mp["l_e"]), + 0, + ], + [ + -mp["l_d"] / mp["l_q"] * omega * mp["p"], + -mp["r_s"] / mp["l_q"], + -omega * mp["p"] * mp["l_e"] / mp["l_q"], + 0, + ], + [ + mp["l_m"] * mp["r_s"] / (sigma * mp["l_d"] * mp["l_e"]), + -omega + * mp["p"] + * mp["l_m"] + * mp["l_q"] + / (sigma * mp["l_d"] * mp["l_e"]), + -mp["r_e"] / mp["l_e"], + 0, + ], + [0, 0, 0, 0], + ] + ), + np.array( + [ # dx'/dw + mp["p"] * mp["l_q"] / mp["l_d"] * state[self.I_SQ_IDX], + -mp["p"] * mp["l_d"] / mp["l_q"] * state[self.I_SD_IDX] + - mp["p"] * mp["l_m"] / mp["l_q"] * state[self.I_E_IDX], + -mp["p"], + mp["p"], + ] + ), + np.array( + [ # dT/dx + 1.5 * mp["p"] * (mp["l_d"] - mp["l_q"]) * state[self.I_SQ_IDX], + 1.5 + * mp["p"] + * ( + mp["l_e"] * state[self.I_E_IDX] + + (mp["l_d"] - mp["l_q"]) * state[self.I_SD_IDX] + ), + 1.5 * mp["p"] * mp["l_e"] * state[self.I_SQ_IDX], + 0, + ] + ), ) diff --git a/gym_electric_motor/physical_systems/electric_motors/induction_motor.py b/gym_electric_motor/physical_systems/electric_motors/induction_motor.py index 6f20382a..69d5985d 100644 --- a/gym_electric_motor/physical_systems/electric_motors/induction_motor.py +++ b/gym_electric_motor/physical_systems/electric_motors/induction_motor.py @@ -7,84 +7,85 @@ class InductionMotor(ThreePhaseMotor): """The InductionMotor and its subclasses implement the technical system of a three phase induction motor. - This includes the system equations, the motor parameters of the equivalent circuit diagram, - as well as limits and bandwidth. - - ===================== ========== ============= =========================================== - Motor Parameter Unit Default Value Description - ===================== ========== ============= =========================================== - r_s Ohm 2.9338 Stator resistance - r_r Ohm 1.355 Rotor resistance - l_m H 143.75e-3 Main inductance - l_sigs H 5.87e-3 Stator-side stray inductance - l_sigr H 5.87e-3 Rotor-side stray inductance - p 1 2 Pole pair number - j_rotor kg/m^2 0.0011 Moment of inertia of the rotor - ===================== ========== ============= =========================================== - - =============== ====== ============================================= - Motor Currents Unit Description - =============== ====== ============================================= - i_sd A Direct axis current - i_sq A Quadrature axis current - i_sa A Current through line a - i_sb A Current through line b - i_sc A Current through line c - i_salpha A Current in alpha axis - i_sbeta A Current in beta axis - =============== ====== ============================================= - =============== ====== ============================================= - Motor Voltages Unit Description - =============== ====== ============================================= - u_sd V Direct axis voltage - u_sq V Quadrature axis voltage - u_sa V Phase voltage for line a - u_sb V Phase voltage for line b - u_sc V Phase voltage for line c - u_salpha V Phase voltage in alpha axis - u_sbeta V Phase voltage in beta axis - =============== ====== ============================================= - - ======== =========================================================== - Limits / Nominal Value Dictionary Entries: - -------- ----------------------------------------------------------- - Entry Description - ======== =========================================================== - i General current limit / nominal value - i_a Current in phase a - i_b Current in phase b - i_c Current in phase c - i_alpha Current in alpha axis - i_beta Current in beta axis - i_sd Current in direct axis - i_sq Current in quadrature axis - omega Mechanical angular Velocity - torque Motor generated torque - epsilon Electrical rotational angle - u_sa Phase voltage in phase a - u_sb Phase voltage in phase b - u_sc Phase voltage in phase c - u_salpha Phase voltage in alpha axis - u_sbeta Phase voltage in beta axis - u_sd Phase voltage in direct axis - u_sq Phase voltage in quadrature axis - ======== =========================================================== - - - Note: - The voltage limits should be the peak-to-peak value of the phase voltage (:math:`\hat{u}_S`). - A phase voltage denotes the potential difference from a line to the neutral point in contrast to the line voltage between two lines. - Typically the root mean square (RMS) value for the line voltage (:math:`U_L`) is given as - :math:`\hat{u}_S=\sqrt{2/3}~U_L` - - The current limits should be the peak-to-peak value of the phase current (:math:`\hat{i}_S`). - Typically the RMS value for the phase current (:math:`I_S`) is given as - :math:`\hat{i}_S = \sqrt{2}~I_S` - - If not specified, nominal values are equal to their corresponding limit values. - Furthermore, if specific limits/nominal values (e.g. i_a) are not specified they are inferred from - the general limits/nominal values (e.g. i) - """ + This includes the system equations, the motor parameters of the equivalent circuit diagram, + as well as limits and bandwidth. + + ===================== ========== ============= =========================================== + Motor Parameter Unit Default Value Description + ===================== ========== ============= =========================================== + r_s Ohm 2.9338 Stator resistance + r_r Ohm 1.355 Rotor resistance + l_m H 143.75e-3 Main inductance + l_sigs H 5.87e-3 Stator-side stray inductance + l_sigr H 5.87e-3 Rotor-side stray inductance + p 1 2 Pole pair number + j_rotor kg/m^2 0.0011 Moment of inertia of the rotor + ===================== ========== ============= =========================================== + + =============== ====== ============================================= + Motor Currents Unit Description + =============== ====== ============================================= + i_sd A Direct axis current + i_sq A Quadrature axis current + i_sa A Current through line a + i_sb A Current through line b + i_sc A Current through line c + i_salpha A Current in alpha axis + i_sbeta A Current in beta axis + =============== ====== ============================================= + =============== ====== ============================================= + Motor Voltages Unit Description + =============== ====== ============================================= + u_sd V Direct axis voltage + u_sq V Quadrature axis voltage + u_sa V Phase voltage for line a + u_sb V Phase voltage for line b + u_sc V Phase voltage for line c + u_salpha V Phase voltage in alpha axis + u_sbeta V Phase voltage in beta axis + =============== ====== ============================================= + + ======== =========================================================== + Limits / Nominal Value Dictionary Entries: + -------- ----------------------------------------------------------- + Entry Description + ======== =========================================================== + i General current limit / nominal value + i_a Current in phase a + i_b Current in phase b + i_c Current in phase c + i_alpha Current in alpha axis + i_beta Current in beta axis + i_sd Current in direct axis + i_sq Current in quadrature axis + omega Mechanical angular Velocity + torque Motor generated torque + epsilon Electrical rotational angle + u_sa Phase voltage in phase a + u_sb Phase voltage in phase b + u_sc Phase voltage in phase c + u_salpha Phase voltage in alpha axis + u_sbeta Phase voltage in beta axis + u_sd Phase voltage in direct axis + u_sq Phase voltage in quadrature axis + ======== =========================================================== + + + Note: + The voltage limits should be the peak-to-peak value of the phase voltage (:math:`\hat{u}_S`). + A phase voltage denotes the potential difference from a line to the neutral point in contrast to the line voltage between two lines. + Typically the root mean square (RMS) value for the line voltage (:math:`U_L`) is given as + :math:`\hat{u}_S=\sqrt{2/3}~U_L` + + The current limits should be the peak-to-peak value of the phase current (:math:`\hat{i}_S`). + Typically the RMS value for the phase current (:math:`I_S`) is given as + :math:`\hat{i}_S = \sqrt{2}~I_S` + + If not specified, nominal values are equal to their corresponding limit values. + Furthermore, if specific limits/nominal values (e.g. i_a) are not specified they are inferred from + the general limits/nominal values (e.g. i) + """ + I_SALPHA_IDX = 0 I_SBETA_IDX = 1 PSI_RALPHA_IDX = 2 @@ -93,37 +94,45 @@ class InductionMotor(ThreePhaseMotor): CURRENTS_IDX = [0, 1] FLUX_IDX = [2, 3] - CURRENTS = ['i_salpha', 'i_sbeta'] - FLUXES = ['psi_ralpha', 'psi_rbeta'] - STATOR_VOLTAGES = ['u_salpha', 'u_sbeta'] + CURRENTS = ["i_salpha", "i_sbeta"] + FLUXES = ["psi_ralpha", "psi_rbeta"] + STATOR_VOLTAGES = ["u_salpha", "u_sbeta"] - IO_VOLTAGES = ['u_sa', 'u_sb', 'u_sc', 'u_salpha', 'u_sbeta', 'u_sd', - 'u_sq'] - IO_CURRENTS = ['i_sa', 'i_sb', 'i_sc', 'i_salpha', 'i_sbeta', 'i_sd', - 'i_sq'] + IO_VOLTAGES = ["u_sa", "u_sb", "u_sc", "u_salpha", "u_sbeta", "u_sd", "u_sq"] + IO_CURRENTS = ["i_sa", "i_sb", "i_sc", "i_salpha", "i_sbeta", "i_sd", "i_sq"] HAS_JACOBIAN = True # Parameters taken from DOI: 10.1109/EPEPEMC.2018.8522008 (O. Wallscheid, M. Schenke, J. Boecker) _default_motor_parameter = { - 'p': 2, - 'l_m': 143.75e-3, - 'l_sigs': 5.87e-3, - 'l_sigr': 5.87e-3, - 'j_rotor': 1.1e-3, - 'r_s': 2.9338, - 'r_r': 1.355, + "p": 2, + "l_m": 143.75e-3, + "l_sigs": 5.87e-3, + "l_sigr": 5.87e-3, + "j_rotor": 1.1e-3, + "r_s": 2.9338, + "r_r": 1.355, } - _default_limits = dict(omega=4e3 * np.pi / 30, torque=0.0, i=5.5, epsilon=math.pi, u=560) - _default_nominal_values = dict(omega=3e3 * np.pi / 30, torque=0.0, i=3.9, epsilon=math.pi, u=560) + _default_limits = dict( + omega=4e3 * np.pi / 30, torque=0.0, i=5.5, epsilon=math.pi, u=560 + ) + _default_nominal_values = dict( + omega=3e3 * np.pi / 30, torque=0.0, i=3.9, epsilon=math.pi, u=560 + ) _model_constants = None - _default_initializer = {'states': {'i_salpha': 0.0, 'i_sbeta': 0.0, - 'psi_ralpha': 0.0, 'psi_rbeta': 0.0, - 'epsilon': 0.0}, - 'interval': None, - 'random_init': None, - 'random_params': (None, None)} + _default_initializer = { + "states": { + "i_salpha": 0.0, + "i_sbeta": 0.0, + "psi_ralpha": 0.0, + "psi_rbeta": 0.0, + "epsilon": 0.0, + }, + "interval": None, + "random_init": None, + "random_params": (None, None), + } _initializer = None @@ -138,33 +147,40 @@ def initializer(self): return self._initializer def __init__( - self, motor_parameter=None, nominal_values=None, limit_values=None, motor_initializer=None,initial_limits=None, + self, + motor_parameter=None, + nominal_values=None, + limit_values=None, + motor_initializer=None, + initial_limits=None, ): # Docstring of superclass # convert placeholder i and u to actual IO quantities _nominal_values = self._default_nominal_values.copy() - _nominal_values.update({u: _nominal_values['u'] for u in self.IO_VOLTAGES}) - _nominal_values.update({i: _nominal_values['i'] for i in self.IO_CURRENTS}) - del _nominal_values['u'], _nominal_values['i'] + _nominal_values.update({u: _nominal_values["u"] for u in self.IO_VOLTAGES}) + _nominal_values.update({i: _nominal_values["i"] for i in self.IO_CURRENTS}) + del _nominal_values["u"], _nominal_values["i"] _nominal_values.update(nominal_values or {}) # same for limits _limit_values = self._default_limits.copy() - _limit_values.update({u: _limit_values['u'] for u in self.IO_VOLTAGES}) - _limit_values.update({i: _limit_values['i'] for i in self.IO_CURRENTS}) - del _limit_values['u'], _limit_values['i'] + _limit_values.update({u: _limit_values["u"] for u in self.IO_VOLTAGES}) + _limit_values.update({i: _limit_values["i"] for i in self.IO_CURRENTS}) + del _limit_values["u"], _limit_values["i"] _limit_values.update(limit_values or {}) - super().__init__(motor_parameter, nominal_values, - limit_values, motor_initializer, initial_limits) + super().__init__( + motor_parameter, + nominal_values, + limit_values, + motor_initializer, + initial_limits, + ) self._update_model() self._update_limits(_limit_values, _nominal_values) - def reset(self, - state_space, - state_positions, - omega=None): + def reset(self, state_space, state_positions, omega=None): # Docstring of superclass - if self._initializer and self._initializer['states']: + if self._initializer and self._initializer["states"]: self._update_initial_limits(omega=omega) self.initialize(state_space, state_positions) return np.asarray(list(self._initial_states.values())) @@ -183,20 +199,25 @@ def electrical_ode(self, state, u_sr_alphabeta, omega, *args): Returns: The derivatives of the state vector d/dt( [i_salpha, i_sbeta, psi_ralpha, psi_rbeta, epsilon]) """ - return np.matmul(self._model_constants, np.array([ - # omega, i_alpha, i_beta, psi_ralpha, psi_rbeta, omega * psi_ralpha, omega * psi_rbeta, u_salpha, u_sbeta, u_ralpha, u_rbeta, - omega, - state[self.I_SALPHA_IDX], - state[self.I_SBETA_IDX], - state[self.PSI_RALPHA_IDX], - state[self.PSI_RBETA_IDX], - omega * state[self.PSI_RALPHA_IDX], - omega * state[self.PSI_RBETA_IDX], - u_sr_alphabeta[0, 0], - u_sr_alphabeta[0, 1], - u_sr_alphabeta[1, 0], - u_sr_alphabeta[1, 1], - ])) + return np.matmul( + self._model_constants, + np.array( + [ + # omega, i_alpha, i_beta, psi_ralpha, psi_rbeta, omega * psi_ralpha, omega * psi_rbeta, u_salpha, u_sbeta, u_ralpha, u_rbeta, + omega, + state[self.I_SALPHA_IDX], + state[self.I_SBETA_IDX], + state[self.PSI_RALPHA_IDX], + state[self.PSI_RBETA_IDX], + omega * state[self.PSI_RALPHA_IDX], + omega * state[self.PSI_RBETA_IDX], + u_sr_alphabeta[0, 0], + u_sr_alphabeta[0, 1], + u_sr_alphabeta[1, 0], + u_sr_alphabeta[1, 1], + ] + ), + ) def i_in(self, state): # Docstring of superclass @@ -205,18 +226,29 @@ def i_in(self, state): def _torque_limit(self): # Docstring of superclass mp = self._motor_parameter - return 1.5 * mp['p'] * mp['l_m'] ** 2/(mp['l_m']+mp['l_sigr']) * self._limits['i_sd'] * self._limits['i_sq'] / 2 + return ( + 1.5 + * mp["p"] + * mp["l_m"] ** 2 + / (mp["l_m"] + mp["l_sigr"]) + * self._limits["i_sd"] + * self._limits["i_sq"] + / 2 + ) def torque(self, states): # Docstring of superclass mp = self._motor_parameter - return \ - 1.5 * mp['p'] * mp['l_m'] \ - / (mp['l_m'] + mp['l_sigr']) \ + return ( + 1.5 + * mp["p"] + * mp["l_m"] + / (mp["l_m"] + mp["l_sigr"]) * ( states[self.PSI_RALPHA_IDX] * states[self.I_SBETA_IDX] - states[self.PSI_RBETA_IDX] * states[self.I_SALPHA_IDX] ) + ) def _flux_limit(self, omega=0, eps_mag=0, u_q_max=0.0, u_rq_max=0.0): """Calculate Flux limits for given current and magnetic-field angle @@ -231,91 +263,165 @@ def _flux_limit(self, omega=0, eps_mag=0, u_q_max=0.0, u_rq_max=0.0): maximal flux values(list) in alpha-beta-system """ mp = self.motor_parameter - l_s = mp['l_m'] + mp['l_sigs'] - l_r = mp['l_m'] + mp['l_sigr'] - l_mr = mp['l_m'] / l_r - sigma = (l_s * l_r - mp['l_m'] ** 2) / (l_s * l_r) + l_s = mp["l_m"] + mp["l_sigs"] + l_r = mp["l_m"] + mp["l_sigr"] + l_mr = mp["l_m"] / l_r + sigma = (l_s * l_r - mp["l_m"] ** 2) / (l_s * l_r) # limiting flux for a low omega if omega == 0: - psi_d_max = mp['l_m'] * self._nominal_values['i_sd'] + psi_d_max = mp["l_m"] * self._nominal_values["i_sd"] else: - i_d, i_q = self.q_inv([self._initial_states['i_salpha'], - self._initial_states['i_sbeta']], - eps_mag) - psi_d_max = mp['p'] * omega * sigma * l_s * i_d + \ - (mp['r_s'] + mp['r_r'] * l_mr**2) * i_q + \ - u_q_max + \ - l_mr * u_rq_max - psi_d_max /= - mp['p'] * omega * l_mr + i_d, i_q = self.q_inv( + [self._initial_states["i_salpha"], self._initial_states["i_sbeta"]], + eps_mag, + ) + psi_d_max = ( + mp["p"] * omega * sigma * l_s * i_d + + (mp["r_s"] + mp["r_r"] * l_mr**2) * i_q + + u_q_max + + l_mr * u_rq_max + ) + psi_d_max /= -mp["p"] * omega * l_mr # clipping flux and setting nominal limit - psi_d_max = 0.9 * np.clip(psi_d_max, a_min=0, a_max=np.abs(mp['l_m'] * i_d)) + psi_d_max = 0.9 * np.clip(psi_d_max, a_min=0, a_max=np.abs(mp["l_m"] * i_d)) # returning flux in alpha, beta system return self.q([psi_d_max, 0], eps_mag) def _update_model(self): # Docstring of superclass mp = self._motor_parameter - l_s = mp['l_m']+mp['l_sigs'] - l_r = mp['l_m']+mp['l_sigr'] - sigma = (l_s*l_r-mp['l_m']**2) /(l_s*l_r) - tau_r = l_r / mp['r_r'] - tau_sig = sigma * l_s / ( - mp['r_s'] + mp['r_r'] * (mp['l_m'] ** 2) / (l_r ** 2)) - - self._model_constants = np.array([ - # omega, i_alpha, i_beta, psi_ralpha, psi_rbeta, omega * psi_ralpha, omega * psi_rbeta, u_salpha, u_sbeta, u_ralpha, u_rbeta, - [0, -1 / tau_sig, 0,mp['l_m'] * mp['r_r'] / (sigma * l_s * l_r ** 2), 0, 0, - +mp['l_m'] * mp['p'] / (sigma * l_r * l_s), 1 / (sigma * l_s), 0, - -mp['l_m'] / (sigma * l_r * l_s), 0, ], # i_ralpha_dot - [0, 0, -1 / tau_sig, 0, - mp['l_m'] * mp['r_r'] / (sigma * l_s * l_r ** 2), - -mp['l_m'] * mp['p'] / (sigma * l_r * l_s), 0, 0, - 1 / (sigma * l_s), 0, -mp['l_m'] / (sigma * l_r * l_s), ], - # i_rbeta_dot - [0, mp['l_m'] / tau_r, 0, -1 / tau_r, 0, 0, -mp['p'], 0, 0, 1, - 0, ], # psi_ralpha_dot - [0, 0, mp['l_m'] / tau_r, 0, -1 / tau_r, mp['p'], 0, 0, 0, 0, 1, ], - # psi_rbeta_dot - [mp['p'], 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], # epsilon_dot - ]) + l_s = mp["l_m"] + mp["l_sigs"] + l_r = mp["l_m"] + mp["l_sigr"] + sigma = (l_s * l_r - mp["l_m"] ** 2) / (l_s * l_r) + tau_r = l_r / mp["r_r"] + tau_sig = sigma * l_s / (mp["r_s"] + mp["r_r"] * (mp["l_m"] ** 2) / (l_r**2)) + + self._model_constants = np.array( + [ + # omega, i_alpha, i_beta, psi_ralpha, psi_rbeta, omega * psi_ralpha, omega * psi_rbeta, u_salpha, u_sbeta, u_ralpha, u_rbeta, + [ + 0, + -1 / tau_sig, + 0, + mp["l_m"] * mp["r_r"] / (sigma * l_s * l_r**2), + 0, + 0, + +mp["l_m"] * mp["p"] / (sigma * l_r * l_s), + 1 / (sigma * l_s), + 0, + -mp["l_m"] / (sigma * l_r * l_s), + 0, + ], # i_ralpha_dot + [ + 0, + 0, + -1 / tau_sig, + 0, + mp["l_m"] * mp["r_r"] / (sigma * l_s * l_r**2), + -mp["l_m"] * mp["p"] / (sigma * l_r * l_s), + 0, + 0, + 1 / (sigma * l_s), + 0, + -mp["l_m"] / (sigma * l_r * l_s), + ], + # i_rbeta_dot + [ + 0, + mp["l_m"] / tau_r, + 0, + -1 / tau_r, + 0, + 0, + -mp["p"], + 0, + 0, + 1, + 0, + ], # psi_ralpha_dot + [ + 0, + 0, + mp["l_m"] / tau_r, + 0, + -1 / tau_r, + mp["p"], + 0, + 0, + 0, + 0, + 1, + ], + # psi_rbeta_dot + [ + mp["p"], + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], # epsilon_dot + ] + ) def electrical_jacobian(self, state, u_in, omega, *args): mp = self._motor_parameter - l_s = mp['l_m'] + mp['l_sigs'] - l_r = mp['l_m'] + mp['l_sigr'] - sigma = (l_s * l_r - mp['l_m'] ** 2) / (l_s * l_r) - tau_r = l_r / mp['r_r'] - tau_sig = sigma * l_s / ( - mp['r_s'] + mp['r_r'] * (mp['l_m'] ** 2) / (l_r ** 2)) + l_s = mp["l_m"] + mp["l_sigs"] + l_r = mp["l_m"] + mp["l_sigr"] + sigma = (l_s * l_r - mp["l_m"] ** 2) / (l_s * l_r) + tau_r = l_r / mp["r_r"] + tau_sig = sigma * l_s / (mp["r_s"] + mp["r_r"] * (mp["l_m"] ** 2) / (l_r**2)) return ( - np.array([ # dx'/dx - # i_alpha i_beta psi_alpha psi_beta epsilon - [-1 / tau_sig, 0, - mp['l_m'] * mp['r_r'] / (sigma * l_s * l_r ** 2), - omega * mp['l_m'] * mp['p'] / (sigma * l_r * l_s), 0], - [0, - 1 / tau_sig, - - omega * mp['l_m'] * mp['p'] / (sigma * l_r * l_s), - mp['l_m'] * mp['r_r'] / (sigma * l_s * l_r ** 2), 0], - [mp['l_m'] / tau_r, 0, - 1 / tau_r, - omega * mp['p'], 0], - [0, mp['l_m'] / tau_r, omega * mp['p'], - 1 / tau_r, 0], - [0, 0, 0, 0, 0] - ]), - np.array([ # dx'/dw - mp['l_m'] * mp['p'] / (sigma * l_r * l_s) * state[ - self.PSI_RBETA_IDX], - - mp['l_m'] * mp['p'] / (sigma * l_r * l_s) * state[ - self.PSI_RALPHA_IDX], - - mp['p'] * state[self.PSI_RBETA_IDX], - mp['p'] * state[self.PSI_RALPHA_IDX], - mp['p'] - ]), - np.array([ # dT/dx - - state[self.PSI_RBETA_IDX] * 3 / 2 * mp['p'] * mp[ - 'l_m'] / l_r, - state[self.PSI_RALPHA_IDX] * 3 / 2 * mp['p'] * mp['l_m'] / l_r, - state[self.I_SBETA_IDX] * 3 / 2 * mp['p'] * mp['l_m'] / l_r, - - state[self.I_SALPHA_IDX] * 3 / 2 * mp['p'] * mp['l_m'] / l_r, - 0 - ]) + np.array( + [ # dx'/dx + # i_alpha i_beta psi_alpha psi_beta epsilon + [ + -1 / tau_sig, + 0, + mp["l_m"] * mp["r_r"] / (sigma * l_s * l_r**2), + omega * mp["l_m"] * mp["p"] / (sigma * l_r * l_s), + 0, + ], + [ + 0, + -1 / tau_sig, + -omega * mp["l_m"] * mp["p"] / (sigma * l_r * l_s), + mp["l_m"] * mp["r_r"] / (sigma * l_s * l_r**2), + 0, + ], + [mp["l_m"] / tau_r, 0, -1 / tau_r, -omega * mp["p"], 0], + [0, mp["l_m"] / tau_r, omega * mp["p"], -1 / tau_r, 0], + [0, 0, 0, 0, 0], + ] + ), + np.array( + [ # dx'/dw + mp["l_m"] + * mp["p"] + / (sigma * l_r * l_s) + * state[self.PSI_RBETA_IDX], + -mp["l_m"] + * mp["p"] + / (sigma * l_r * l_s) + * state[self.PSI_RALPHA_IDX], + -mp["p"] * state[self.PSI_RBETA_IDX], + mp["p"] * state[self.PSI_RALPHA_IDX], + mp["p"], + ] + ), + np.array( + [ # dT/dx + -state[self.PSI_RBETA_IDX] * 3 / 2 * mp["p"] * mp["l_m"] / l_r, + state[self.PSI_RALPHA_IDX] * 3 / 2 * mp["p"] * mp["l_m"] / l_r, + state[self.I_SBETA_IDX] * 3 / 2 * mp["p"] * mp["l_m"] / l_r, + -state[self.I_SALPHA_IDX] * 3 / 2 * mp["p"] * mp["l_m"] / l_r, + 0, + ] + ), ) diff --git a/gym_electric_motor/physical_systems/electric_motors/permanent_magnet_synchronous_motor.py b/gym_electric_motor/physical_systems/electric_motors/permanent_magnet_synchronous_motor.py index 093ceb87..3cf8641b 100644 --- a/gym_electric_motor/physical_systems/electric_motors/permanent_magnet_synchronous_motor.py +++ b/gym_electric_motor/physical_systems/electric_motors/permanent_magnet_synchronous_motor.py @@ -15,7 +15,7 @@ class PermanentMagnetSynchronousMotor(SynchronousMotor): p 1 3 Pole pair number j_rotor kg/m^2 0.03883 Moment of inertia of the rotor ===================== ========== ============= =========================================== - + =============== ====== ============================================= Motor Currents Unit Description =============== ====== ============================================= @@ -83,73 +83,105 @@ class PermanentMagnetSynchronousMotor(SynchronousMotor): #### Parameters taken from DOI: 10.1109/TPEL.2020.3006779 (A. Brosch, S. Hanke, O. Wallscheid, J. Boecker) #### and DOI: 10.1109/IEMDC.2019.8785122 (S. Hanke, O. Wallscheid, J. Boecker) _default_motor_parameter = { - 'p': 3, - 'l_d': 0.37e-3, - 'l_q': 1.2e-3, - 'j_rotor': 0.03883, - 'r_s': 18e-3, - 'psi_p': 66e-3, + "p": 3, + "l_d": 0.37e-3, + "l_q": 1.2e-3, + "j_rotor": 0.03883, + "r_s": 18e-3, + "psi_p": 66e-3, } HAS_JACOBIAN = True - _default_limits = dict(omega=4e3 * np.pi / 30, torque=0.0, i=400, epsilon=math.pi, u=300) - _default_nominal_values = dict(omega=3e3 * np.pi / 30, torque=0.0, i=240, epsilon=math.pi, u=300) + _default_limits = dict( + omega=4e3 * np.pi / 30, torque=0.0, i=400, epsilon=math.pi, u=300 + ) + _default_nominal_values = dict( + omega=3e3 * np.pi / 30, torque=0.0, i=240, epsilon=math.pi, u=300 + ) _default_initializer = { - 'states': {'i_sq': 0.0, 'i_sd': 0.0, 'epsilon': 0.0}, - 'interval': None, - 'random_init': None, - 'random_params': (None, None) + "states": {"i_sq": 0.0, "i_sd": 0.0, "epsilon": 0.0}, + "interval": None, + "random_init": None, + "random_params": (None, None), } - IO_VOLTAGES = ['u_a', 'u_b', 'u_c', 'u_sd', 'u_sq'] - IO_CURRENTS = ['i_a', 'i_b', 'i_c', 'i_sd', 'i_sq'] + IO_VOLTAGES = ["u_a", "u_b", "u_c", "u_sd", "u_sq"] + IO_CURRENTS = ["i_a", "i_b", "i_c", "i_sd", "i_sq"] def _update_model(self): # Docstring of superclass mp = self._motor_parameter - self._model_constants = np.array([ - # omega, i_d, i_q, u_d, u_q, omega * i_d, omega * i_q - [ 0, -mp['r_s'], 0, 1, 0, 0, mp['l_q'] * mp['p']], - [-mp['psi_p'] * mp['p'], 0, -mp['r_s'], 0, 1, -mp['l_d'] * mp['p'], 0], - [ mp['p'], 0, 0, 0, 0, 0, 0], - ]) + self._model_constants = np.array( + [ + # omega, i_d, i_q, u_d, u_q, omega * i_d, omega * i_q + [0, -mp["r_s"], 0, 1, 0, 0, mp["l_q"] * mp["p"]], + [-mp["psi_p"] * mp["p"], 0, -mp["r_s"], 0, 1, -mp["l_d"] * mp["p"], 0], + [mp["p"], 0, 0, 0, 0, 0, 0], + ] + ) - self._model_constants[self.I_SD_IDX] = self._model_constants[self.I_SD_IDX] / mp['l_d'] - self._model_constants[self.I_SQ_IDX] = self._model_constants[self.I_SQ_IDX] / mp['l_q'] + self._model_constants[self.I_SD_IDX] = ( + self._model_constants[self.I_SD_IDX] / mp["l_d"] + ) + self._model_constants[self.I_SQ_IDX] = ( + self._model_constants[self.I_SQ_IDX] / mp["l_q"] + ) def _torque_limit(self): # Docstring of superclass mp = self._motor_parameter - if mp['l_d'] == mp['l_q']: - return self.torque([0, self._limits['i_sq'], 0]) + if mp["l_d"] == mp["l_q"]: + return self.torque([0, self._limits["i_sq"], 0]) else: - i_n = self.nominal_values['i'] - _p = mp['psi_p'] / (2 * (mp['l_d'] - mp['l_q'])) - _q = - i_n ** 2 / 2 - i_d_opt = - _p / 2 - np.sqrt( (_p / 2) ** 2 - _q) - i_q_opt = np.sqrt(i_n ** 2 - i_d_opt ** 2) + i_n = self.nominal_values["i"] + _p = mp["psi_p"] / (2 * (mp["l_d"] - mp["l_q"])) + _q = -i_n**2 / 2 + i_d_opt = -_p / 2 - np.sqrt((_p / 2) ** 2 - _q) + i_q_opt = np.sqrt(i_n**2 - i_d_opt**2) return self.torque([i_d_opt, i_q_opt, 0]) def torque(self, currents): # Docstring of superclass mp = self._motor_parameter - return 1.5 * mp['p'] * (mp['psi_p'] + (mp['l_d'] - mp['l_q']) * currents[self.I_SD_IDX]) * currents[self.I_SQ_IDX] + return ( + 1.5 + * mp["p"] + * (mp["psi_p"] + (mp["l_d"] - mp["l_q"]) * currents[self.I_SD_IDX]) + * currents[self.I_SQ_IDX] + ) def electrical_jacobian(self, state, u_in, omega, *args): mp = self._motor_parameter return ( - np.array([ # dx'/dx - [-mp['r_s'] / mp['l_d'], mp['l_q']/mp['l_d'] * omega * mp['p'], 0], - [-mp['l_d'] / mp['l_q'] * omega * mp['p'], - mp['r_s'] / mp['l_q'], 0], - [0, 0, 0] - ]), - np.array([ # dx'/dw - mp['p'] * mp['l_q'] / mp['l_d'] * state[self.I_SQ_IDX], - - mp['p'] * mp['l_d'] / mp['l_q'] * state[self.I_SD_IDX] - mp['p'] * mp['psi_p'] / mp['l_q'], - mp['p'] - ]), - np.array([ # dT/dx - 1.5 * mp['p'] * (mp['l_d'] - mp['l_q']) * state[self.I_SQ_IDX], - 1.5 * mp['p'] * (mp['psi_p'] + (mp['l_d'] - mp['l_q']) * state[self.I_SD_IDX]), - 0 - ]) + np.array( + [ # dx'/dx + [ + -mp["r_s"] / mp["l_d"], + mp["l_q"] / mp["l_d"] * omega * mp["p"], + 0, + ], + [ + -mp["l_d"] / mp["l_q"] * omega * mp["p"], + -mp["r_s"] / mp["l_q"], + 0, + ], + [0, 0, 0], + ] + ), + np.array( + [ # dx'/dw + mp["p"] * mp["l_q"] / mp["l_d"] * state[self.I_SQ_IDX], + -mp["p"] * mp["l_d"] / mp["l_q"] * state[self.I_SD_IDX] + - mp["p"] * mp["psi_p"] / mp["l_q"], + mp["p"], + ] + ), + np.array( + [ # dT/dx + 1.5 * mp["p"] * (mp["l_d"] - mp["l_q"]) * state[self.I_SQ_IDX], + 1.5 + * mp["p"] + * (mp["psi_p"] + (mp["l_d"] - mp["l_q"]) * state[self.I_SD_IDX]), + 0, + ] + ), ) diff --git a/gym_electric_motor/physical_systems/electric_motors/squirrel_cage_induction_motor.py b/gym_electric_motor/physical_systems/electric_motors/squirrel_cage_induction_motor.py index ec646b2f..cc3e7ea1 100644 --- a/gym_electric_motor/physical_systems/electric_motors/squirrel_cage_induction_motor.py +++ b/gym_electric_motor/physical_systems/electric_motors/squirrel_cage_induction_motor.py @@ -6,109 +6,120 @@ class SquirrelCageInductionMotor(InductionMotor): """ - ===================== ========== ============= =========================================== - Motor Parameter Unit Default Value Description - ===================== ========== ============= =========================================== - r_s Ohm 2.9338 Stator resistance - r_r Ohm 1.355 Rotor resistance - l_m H 143.75e-3 Main inductance - l_sigs H 5.87e-3 Stator-side stray inductance - l_sigr H 5.87e-3 Rotor-side stray inductance - p 1 2 Pole pair number - j_rotor kg/m^2 0.0011 Moment of inertia of the rotor - ===================== ========== ============= =========================================== + ===================== ========== ============= =========================================== + Motor Parameter Unit Default Value Description + ===================== ========== ============= =========================================== + r_s Ohm 2.9338 Stator resistance + r_r Ohm 1.355 Rotor resistance + l_m H 143.75e-3 Main inductance + l_sigs H 5.87e-3 Stator-side stray inductance + l_sigr H 5.87e-3 Rotor-side stray inductance + p 1 2 Pole pair number + j_rotor kg/m^2 0.0011 Moment of inertia of the rotor + ===================== ========== ============= =========================================== - =============== ====== ============================================= - Motor Currents Unit Description - =============== ====== ============================================= - i_sd A Direct axis current - i_sq A Quadrature axis current - i_sa A Stator current through branch a - i_sb A Stator current through branch b - i_sc A Stator current through branch c - i_salpha A Stator current in alpha direction - i_sbeta A Stator current in beta direction - =============== ====== ============================================= - =============== ====== ============================================= - Rotor flux Unit Description - =============== ====== ============================================= - psi_rd Vs Direct axis of the rotor oriented flux - psi_rq Vs Quadrature axis of the rotor oriented flux - psi_ra Vs Rotor oriented flux in branch a - psi_rb Vs Rotor oriented flux in branch b - psi_rc Vs Rotor oriented flux in branch c - psi_ralpha Vs Rotor oriented flux in alpha direction - psi_rbeta Vs Rotor oriented flux in beta direction - =============== ====== ============================================= - =============== ====== ============================================= - Motor Voltages Unit Description - =============== ====== ============================================= - u_sd V Direct axis voltage - u_sq V Quadrature axis voltage - u_sa V Stator voltage through branch a - u_sb V Stator voltage through branch b - u_sc V Stator voltage through branch c - u_salpha V Stator voltage in alpha axis - u_sbeta V Stator voltage in beta axis - =============== ====== ============================================= + =============== ====== ============================================= + Motor Currents Unit Description + =============== ====== ============================================= + i_sd A Direct axis current + i_sq A Quadrature axis current + i_sa A Stator current through branch a + i_sb A Stator current through branch b + i_sc A Stator current through branch c + i_salpha A Stator current in alpha direction + i_sbeta A Stator current in beta direction + =============== ====== ============================================= + =============== ====== ============================================= + Rotor flux Unit Description + =============== ====== ============================================= + psi_rd Vs Direct axis of the rotor oriented flux + psi_rq Vs Quadrature axis of the rotor oriented flux + psi_ra Vs Rotor oriented flux in branch a + psi_rb Vs Rotor oriented flux in branch b + psi_rc Vs Rotor oriented flux in branch c + psi_ralpha Vs Rotor oriented flux in alpha direction + psi_rbeta Vs Rotor oriented flux in beta direction + =============== ====== ============================================= + =============== ====== ============================================= + Motor Voltages Unit Description + =============== ====== ============================================= + u_sd V Direct axis voltage + u_sq V Quadrature axis voltage + u_sa V Stator voltage through branch a + u_sb V Stator voltage through branch b + u_sc V Stator voltage through branch c + u_salpha V Stator voltage in alpha axis + u_sbeta V Stator voltage in beta axis + =============== ====== ============================================= - ======== =========================================================== - Limits / Nominal Value Dictionary Entries: - -------- ----------------------------------------------------------- - Entry Description - ======== =========================================================== - i General current limit / nominal value - i_sa Current in phase a - i_sb Current in phase b - i_sc Current in phase c - i_salpha Current in alpha axis - i_sbeta Current in beta axis - i_sd Current in direct axis - i_sq Current in quadrature axis - omega Mechanical angular Velocity - torque Motor generated torque - u_sa Voltage in phase a - u_sb Voltage in phase b - u_sc Voltage in phase c - u_salpha Voltage in alpha axis - u_sbeta Voltage in beta axis - u_sd Voltage in direct axis - u_sq Voltage in quadrature axis - ======== =========================================================== + ======== =========================================================== + Limits / Nominal Value Dictionary Entries: + -------- ----------------------------------------------------------- + Entry Description + ======== =========================================================== + i General current limit / nominal value + i_sa Current in phase a + i_sb Current in phase b + i_sc Current in phase c + i_salpha Current in alpha axis + i_sbeta Current in beta axis + i_sd Current in direct axis + i_sq Current in quadrature axis + omega Mechanical angular Velocity + torque Motor generated torque + u_sa Voltage in phase a + u_sb Voltage in phase b + u_sc Voltage in phase c + u_salpha Voltage in alpha axis + u_sbeta Voltage in beta axis + u_sd Voltage in direct axis + u_sq Voltage in quadrature axis + ======== =========================================================== - Note: - The voltage limits should be the peak-to-peak value of the phase voltage (:math:`\hat{u}_S`). - Typically the rms value for the line voltage (:math:`U_L`) is given as - :math:`\hat{u}_S=\sqrt{2/3}~U_L` + Note: + The voltage limits should be the peak-to-peak value of the phase voltage (:math:`\hat{u}_S`). + Typically the rms value for the line voltage (:math:`U_L`) is given as + :math:`\hat{u}_S=\sqrt{2/3}~U_L` - The current limits should be the peak-to-peak value of the phase current (:math:`\hat{i}_S`). - Typically the rms value for the phase current (:math:`I_S`) is given as - :math:`\hat{i}_S = \sqrt{2}~I_S` + The current limits should be the peak-to-peak value of the phase current (:math:`\hat{i}_S`). + Typically the rms value for the phase current (:math:`I_S`) is given as + :math:`\hat{i}_S = \sqrt{2}~I_S` + + If not specified, nominal values are equal to their corresponding limit values. + Furthermore, if specific limits/nominal values (e.g. i_a) are not specified they are inferred from + the general limits/nominal values (e.g. i) + """ - If not specified, nominal values are equal to their corresponding limit values. - Furthermore, if specific limits/nominal values (e.g. i_a) are not specified they are inferred from - the general limits/nominal values (e.g. i) - """ #### Parameters taken from DOI: 10.1109/EPEPEMC.2018.8522008 (O. Wallscheid, M. Schenke, J. Boecker) _default_motor_parameter = { - 'p': 2, - 'l_m': 143.75e-3, - 'l_sigs': 5.87e-3, - 'l_sigr': 5.87e-3, - 'j_rotor': 1.1e-3, - 'r_s': 2.9338, - 'r_r': 1.355, + "p": 2, + "l_m": 143.75e-3, + "l_sigs": 5.87e-3, + "l_sigr": 5.87e-3, + "j_rotor": 1.1e-3, + "r_s": 2.9338, + "r_r": 1.355, } - _default_limits = dict(omega=4e3 * np.pi / 30, torque=0.0, i=5.5, epsilon=math.pi, u=560) - _default_nominal_values = dict(omega=3e3 * np.pi / 30, torque=0.0, i=3.9, epsilon=math.pi, u=560) - _default_initializer = {'states': {'i_salpha': 0.0, 'i_sbeta': 0.0, - 'psi_ralpha': 0.0, 'psi_rbeta': 0.0, - 'epsilon': 0.0}, - 'interval': None, - 'random_init': None, - 'random_params': (None, None)} + _default_limits = dict( + omega=4e3 * np.pi / 30, torque=0.0, i=5.5, epsilon=math.pi, u=560 + ) + _default_nominal_values = dict( + omega=3e3 * np.pi / 30, torque=0.0, i=3.9, epsilon=math.pi, u=560 + ) + _default_initializer = { + "states": { + "i_salpha": 0.0, + "i_sbeta": 0.0, + "psi_ralpha": 0.0, + "psi_rbeta": 0.0, + "epsilon": 0.0, + }, + "interval": None, + "random_init": None, + "random_params": (None, None), + } def electrical_ode(self, state, u_salphabeta, omega, *args): """ @@ -122,31 +133,36 @@ def electrical_ode(self, state, u_salphabeta, omega, *args): def _update_limits(self, limit_values={}, nominal_values={}): # Docstring of superclass - voltage_limit = 0.5 * self._limits['u'] - voltage_nominal = 0.5 * self._nominal_values['u'] + voltage_limit = 0.5 * self._limits["u"] + voltage_nominal = 0.5 * self._nominal_values["u"] limits_agenda = {} nominal_agenda = {} for u, i in zip(self.IO_VOLTAGES, self.IO_CURRENTS): limits_agenda[u] = voltage_limit nominal_agenda[u] = voltage_nominal - limits_agenda[i] = self._limits.get('i', None) or \ - self._limits[u] / self._motor_parameter['r_s'] - nominal_agenda[i] = self._nominal_values.get('i', None) or \ - self._nominal_values[u] / self._motor_parameter['r_s'] + limits_agenda[i] = ( + self._limits.get("i", None) + or self._limits[u] / self._motor_parameter["r_s"] + ) + nominal_agenda[i] = ( + self._nominal_values.get("i", None) + or self._nominal_values[u] / self._motor_parameter["r_s"] + ) super()._update_limits(limits_agenda, nominal_agenda) def _update_initial_limits(self, nominal_new={}, omega=None): # Docstring of superclass # draw a sample magnetic field angle from [-pi,pi] eps_mag = 2 * np.pi * np.random.random_sample() - np.pi - flux_alphabeta_limits = self._flux_limit(omega=omega, - eps_mag=eps_mag, - u_q_max=self._nominal_values['u_sq']) + flux_alphabeta_limits = self._flux_limit( + omega=omega, eps_mag=eps_mag, u_q_max=self._nominal_values["u_sq"] + ) # using absolute value, because limits should describe upper limit # after abs-operator, norm of alphabeta flux still equal to # d-component of flux flux_alphabeta_limits = np.abs(flux_alphabeta_limits) - flux_nominal_limits = {state: value for state, value in - zip(self.FLUXES, flux_alphabeta_limits)} + flux_nominal_limits = { + state: value for state, value in zip(self.FLUXES, flux_alphabeta_limits) + } flux_nominal_limits.update(nominal_new) super()._update_initial_limits(flux_nominal_limits) diff --git a/gym_electric_motor/physical_systems/electric_motors/synchronous_motor.py b/gym_electric_motor/physical_systems/electric_motors/synchronous_motor.py index 2c11ca1d..81709764 100644 --- a/gym_electric_motor/physical_systems/electric_motors/synchronous_motor.py +++ b/gym_electric_motor/physical_systems/electric_motors/synchronous_motor.py @@ -88,19 +88,26 @@ class SynchronousMotor(ThreePhaseMotor): I_SQ_IDX = 1 EPSILON_IDX = 2 CURRENTS_IDX = [0, 1] - CURRENTS = ['i_sd', 'i_sq'] - VOLTAGES = ['u_sd', 'u_sq'] + CURRENTS = ["i_sd", "i_sq"] + VOLTAGES = ["u_sd", "u_sq"] _model_constants = None _initializer = None - def __init__(self, motor_parameter=None, nominal_values=None, limit_values=None, motor_initializer=None): + def __init__( + self, + motor_parameter=None, + nominal_values=None, + limit_values=None, + motor_initializer=None, + ): # Docstring of superclass nominal_values = nominal_values or {} limit_values = limit_values or {} - super().__init__(motor_parameter, nominal_values, - limit_values, motor_initializer) + super().__init__( + motor_parameter, nominal_values, limit_values, motor_initializer + ) self._update_model() self._update_limits() @@ -119,7 +126,7 @@ def _torque_limit(self): def reset(self, state_space, state_positions, **__): # Docstring of superclass - if self._initializer and self._initializer['states']: + if self._initializer and self._initializer["states"]: self.initialize(state_space, state_positions) return np.asarray(list(self._initial_states.values())) else: @@ -147,15 +154,20 @@ def electrical_ode(self, state, u_dq, omega, *_): Returns: The derivatives of the state vector d/dt([i_sd, i_sq, epsilon]) """ - return np.matmul(self._model_constants, np.array([ - omega, - state[self.I_SD_IDX], - state[self.I_SQ_IDX], - u_dq[0], - u_dq[1], - omega * state[self.I_SD_IDX], - omega * state[self.I_SQ_IDX], - ])) + return np.matmul( + self._model_constants, + np.array( + [ + omega, + state[self.I_SD_IDX], + state[self.I_SQ_IDX], + u_dq[0], + u_dq[1], + omega * state[self.I_SD_IDX], + omega * state[self.I_SQ_IDX], + ] + ), + ) def i_in(self, state): # Docstring of superclass @@ -164,16 +176,20 @@ def i_in(self, state): def _update_limits(self): # Docstring of superclass - voltage_limit = 0.5 * self._limits['u'] - voltage_nominal = 0.5 * self._nominal_values['u'] + voltage_limit = 0.5 * self._limits["u"] + voltage_nominal = 0.5 * self._nominal_values["u"] limits_agenda = {} nominal_agenda = {} for u, i in zip(self.IO_VOLTAGES, self.IO_CURRENTS): limits_agenda[u] = voltage_limit nominal_agenda[u] = voltage_nominal - limits_agenda[i] = self._limits.get('i', None) \ - or self._limits[u] / self._motor_parameter['r_s'] - nominal_agenda[i] = self._nominal_values.get('i', None) \ - or self._nominal_values[u] / self._motor_parameter['r_s'] + limits_agenda[i] = ( + self._limits.get("i", None) + or self._limits[u] / self._motor_parameter["r_s"] + ) + nominal_agenda[i] = ( + self._nominal_values.get("i", None) + or self._nominal_values[u] / self._motor_parameter["r_s"] + ) super()._update_limits(limits_agenda, nominal_agenda) diff --git a/gym_electric_motor/physical_systems/electric_motors/synchronous_reluctance_motor.py b/gym_electric_motor/physical_systems/electric_motors/synchronous_reluctance_motor.py index 42d76e81..b6fec909 100644 --- a/gym_electric_motor/physical_systems/electric_motors/synchronous_reluctance_motor.py +++ b/gym_electric_motor/physical_systems/electric_motors/synchronous_reluctance_motor.py @@ -5,140 +5,181 @@ class SynchronousReluctanceMotor(SynchronousMotor): """ - ===================== ========== ============= =========================================== - Motor Parameter Unit Default Value Description - ===================== ========== ============= =========================================== - r_s Ohm 0.57 Stator resistance - l_d H 10.1e-3 Direct axis inductance - l_q H 4.1e-3 Quadrature axis inductance - p 1 4 Pole pair number - j_rotor kg/m^2 0.8e-3 Moment of inertia of the rotor - ===================== ========== ============= =========================================== - - =============== ====== ============================================= - Motor Currents Unit Description - =============== ====== ============================================= - i_sd A Direct axis current - i_sq A Quadrature axis current - i_a A Current through branch a - i_b A Current through branch b - i_c A Current through branch c - i_alpha A Current in alpha axis - i_beta A Current in beta axis - =============== ====== ============================================= - =============== ====== ============================================= - Motor Voltages Unit Description - =============== ====== ============================================= - u_sd V Direct axis voltage - u_sq V Quadrature axis voltage - u_a V Voltage through branch a - u_b V Voltage through branch b - u_c V Voltage through branch c - u_alpha V Voltage in alpha axis - u_beta V Voltage in beta axis - =============== ====== ============================================= - - ======== =========================================================== - Limits / Nominal Value Dictionary Entries: - -------- ----------------------------------------------------------- - Entry Description - ======== =========================================================== - i General current limit / nominal value - i_a Current in phase a - i_b Current in phase b - i_c Current in phase c - i_alpha Current in alpha axis - i_beta Current in beta axis - i_sd Current in direct axis - i_sq Current in quadrature axis - omega Mechanical angular Velocity - epsilon Electrical rotational angle - torque Motor generated torque - u_a Voltage in phase a - u_b Voltage in phase b - u_c Voltage in phase c - u_alpha Voltage in alpha axis - u_beta Voltage in beta axis - u_sd Voltage in direct axis - u_sq Voltage in quadrature axis - ======== =========================================================== - - - Note: - The voltage limits should be the peak-to-peak value of the phase voltage (:math:`\hat{u}_S`). - A phase voltage denotes the potential difference from a line to the neutral point in contrast to the line voltage between two lines. - Typically the root mean square (RMS) value for the line voltage (:math:`U_L`) is given as - :math:`\hat{u}_S=\sqrt{2/3}~U_L` - - The current limits should be the peak-to-peak value of the phase current (:math:`\hat{i}_S`). - Typically the RMS value for the phase current (:math:`I_S`) is given as - :math:`\hat{i}_S = \sqrt{2}~I_S` - - If not specified, nominal values are equal to their corresponding limit values. - Furthermore, if specific limits/nominal values (e.g. i_a) are not specified they are inferred from - the general limits/nominal values (e.g. i) + ===================== ========== ============= =========================================== + Motor Parameter Unit Default Value Description + ===================== ========== ============= =========================================== + r_s Ohm 0.57 Stator resistance + l_d H 10.1e-3 Direct axis inductance + l_q H 4.1e-3 Quadrature axis inductance + p 1 4 Pole pair number + j_rotor kg/m^2 0.8e-3 Moment of inertia of the rotor + ===================== ========== ============= =========================================== + + =============== ====== ============================================= + Motor Currents Unit Description + =============== ====== ============================================= + i_sd A Direct axis current + i_sq A Quadrature axis current + i_a A Current through branch a + i_b A Current through branch b + i_c A Current through branch c + i_alpha A Current in alpha axis + i_beta A Current in beta axis + =============== ====== ============================================= + =============== ====== ============================================= + Motor Voltages Unit Description + =============== ====== ============================================= + u_sd V Direct axis voltage + u_sq V Quadrature axis voltage + u_a V Voltage through branch a + u_b V Voltage through branch b + u_c V Voltage through branch c + u_alpha V Voltage in alpha axis + u_beta V Voltage in beta axis + =============== ====== ============================================= + + ======== =========================================================== + Limits / Nominal Value Dictionary Entries: + -------- ----------------------------------------------------------- + Entry Description + ======== =========================================================== + i General current limit / nominal value + i_a Current in phase a + i_b Current in phase b + i_c Current in phase c + i_alpha Current in alpha axis + i_beta Current in beta axis + i_sd Current in direct axis + i_sq Current in quadrature axis + omega Mechanical angular Velocity + epsilon Electrical rotational angle + torque Motor generated torque + u_a Voltage in phase a + u_b Voltage in phase b + u_c Voltage in phase c + u_alpha Voltage in alpha axis + u_beta Voltage in beta axis + u_sd Voltage in direct axis + u_sq Voltage in quadrature axis + ======== =========================================================== + + + Note: + The voltage limits should be the peak-to-peak value of the phase voltage (:math:`\hat{u}_S`). + A phase voltage denotes the potential difference from a line to the neutral point in contrast to the line voltage between two lines. + Typically the root mean square (RMS) value for the line voltage (:math:`U_L`) is given as + :math:`\hat{u}_S=\sqrt{2/3}~U_L` + + The current limits should be the peak-to-peak value of the phase current (:math:`\hat{i}_S`). + Typically the RMS value for the phase current (:math:`I_S`) is given as + :math:`\hat{i}_S = \sqrt{2}~I_S` + + If not specified, nominal values are equal to their corresponding limit values. + Furthermore, if specific limits/nominal values (e.g. i_a) are not specified they are inferred from + the general limits/nominal values (e.g. i) """ + HAS_JACOBIAN = True #### Parameters taken from DOI: 10.1109/AMC.2008.4516099 (K. Malekian, M. R. Sharif, J. Milimonfared) - _default_motor_parameter = {'p': 4, - 'l_d': 10.1e-3, - 'l_q': 4.1e-3, - 'j_rotor': 0.8e-3, - 'r_s': 0.57 - } - - _default_nominal_values = {'i': 10, 'torque': 0, 'omega': 3e3 * np.pi / 30, 'epsilon': np.pi, 'u': 80} - _default_limits = {'i': 18, 'torque': 0, 'omega': 4.3e3 * np.pi / 30, 'epsilon': np.pi, 'u': 80} - _default_initializer = {'states': {'i_sq': 0.0, 'i_sd': 0.0, 'epsilon': 0.0}, - 'interval': None, - 'random_init': None, - 'random_params': (None, None)} - - IO_VOLTAGES = ['u_a', 'u_b', 'u_c', 'u_sd', 'u_sq'] - IO_CURRENTS = ['i_a', 'i_b', 'i_c', 'i_sd', 'i_sq'] + _default_motor_parameter = { + "p": 4, + "l_d": 10.1e-3, + "l_q": 4.1e-3, + "j_rotor": 0.8e-3, + "r_s": 0.57, + } + + _default_nominal_values = { + "i": 10, + "torque": 0, + "omega": 3e3 * np.pi / 30, + "epsilon": np.pi, + "u": 80, + } + _default_limits = { + "i": 18, + "torque": 0, + "omega": 4.3e3 * np.pi / 30, + "epsilon": np.pi, + "u": 80, + } + _default_initializer = { + "states": {"i_sq": 0.0, "i_sd": 0.0, "epsilon": 0.0}, + "interval": None, + "random_init": None, + "random_params": (None, None), + } + + IO_VOLTAGES = ["u_a", "u_b", "u_c", "u_sd", "u_sq"] + IO_CURRENTS = ["i_a", "i_b", "i_c", "i_sd", "i_sq"] def _update_model(self): # Docstring of superclass mp = self._motor_parameter - self._model_constants = np.array([ - # omega, i_sd, i_sq, u_sd, u_sq, omega * i_sd, omega * i_sq - [ 0, -mp['r_s'], 0, 1, 0, 0, mp['l_q'] * mp['p']], - [ 0, 0, -mp['r_s'], 0, 1, -mp['l_d'] * mp['p'], 0], - [mp['p'], 0, 0, 0, 0, 0, 0] - ]) + self._model_constants = np.array( + [ + # omega, i_sd, i_sq, u_sd, u_sq, omega * i_sd, omega * i_sq + [0, -mp["r_s"], 0, 1, 0, 0, mp["l_q"] * mp["p"]], + [0, 0, -mp["r_s"], 0, 1, -mp["l_d"] * mp["p"], 0], + [mp["p"], 0, 0, 0, 0, 0, 0], + ] + ) - self._model_constants[self.I_SD_IDX] = self._model_constants[self.I_SD_IDX] / mp['l_d'] - self._model_constants[self.I_SQ_IDX] = self._model_constants[self.I_SQ_IDX] / mp['l_q'] + self._model_constants[self.I_SD_IDX] = ( + self._model_constants[self.I_SD_IDX] / mp["l_d"] + ) + self._model_constants[self.I_SQ_IDX] = ( + self._model_constants[self.I_SQ_IDX] / mp["l_q"] + ) def _torque_limit(self): # Docstring of superclass - return self.torque([self._limits['i_sd'] / np.sqrt(2), self._limits['i_sq'] / np.sqrt(2), 0]) + return self.torque( + [self._limits["i_sd"] / np.sqrt(2), self._limits["i_sq"] / np.sqrt(2), 0] + ) def torque(self, currents): # Docstring of superclass mp = self._motor_parameter - return 1.5 * mp['p'] * ( - (mp['l_d'] - mp['l_q']) * currents[self.I_SD_IDX]) * \ - currents[self.I_SQ_IDX] + return ( + 1.5 + * mp["p"] + * ((mp["l_d"] - mp["l_q"]) * currents[self.I_SD_IDX]) + * currents[self.I_SQ_IDX] + ) def electrical_jacobian(self, state, u_in, omega, *_): mp = self._motor_parameter return ( - np.array([ - [-mp['r_s'] / mp['l_d'], mp['l_q'] / mp['l_d'] * mp['p'] * omega, 0], - [-mp['l_d'] / mp['l_q'] * mp['p'] * omega, -mp['r_s'] / mp['l_q'], 0], - [0, 0, 0] - ]), - np.array([ - mp['p'] * mp['l_q'] / mp['l_d'] * state[self.I_SQ_IDX], - - mp['p'] * mp['l_d'] / mp['l_q'] * state[self.I_SD_IDX], - mp['p'] - ]), - np.array([ - 1.5 * mp['p'] * (mp['l_d'] - mp['l_q']) * state[self.I_SQ_IDX], - 1.5 * mp['p'] * (mp['l_d'] - mp['l_q']) * state[self.I_SD_IDX], - 0 - ]) + np.array( + [ + [ + -mp["r_s"] / mp["l_d"], + mp["l_q"] / mp["l_d"] * mp["p"] * omega, + 0, + ], + [ + -mp["l_d"] / mp["l_q"] * mp["p"] * omega, + -mp["r_s"] / mp["l_q"], + 0, + ], + [0, 0, 0], + ] + ), + np.array( + [ + mp["p"] * mp["l_q"] / mp["l_d"] * state[self.I_SQ_IDX], + -mp["p"] * mp["l_d"] / mp["l_q"] * state[self.I_SD_IDX], + mp["p"], + ] + ), + np.array( + [ + 1.5 * mp["p"] * (mp["l_d"] - mp["l_q"]) * state[self.I_SQ_IDX], + 1.5 * mp["p"] * (mp["l_d"] - mp["l_q"]) * state[self.I_SD_IDX], + 0, + ] + ), ) diff --git a/gym_electric_motor/physical_systems/electric_motors/three_phase_motor.py b/gym_electric_motor/physical_systems/electric_motors/three_phase_motor.py index b558097a..ad1ea275 100644 --- a/gym_electric_motor/physical_systems/electric_motors/three_phase_motor.py +++ b/gym_electric_motor/physical_systems/electric_motors/three_phase_motor.py @@ -6,23 +6,17 @@ class ThreePhaseMotor(ElectricMotor): """ - The ThreePhaseMotor and its subclasses implement the technical system of Three Phase Motors. + The ThreePhaseMotor and its subclasses implement the technical system of Three Phase Motors. - This includes the system equations, the motor parameters of the equivalent circuit diagram, - as well as limits and bandwidth. + This includes the system equations, the motor parameters of the equivalent circuit diagram, + as well as limits and bandwidth. """ + # transformation matrix from abc to alpha-beta representation - _t23 = 2 / 3 * np.array([ - [1, -0.5, -0.5], - [0, 0.5 * np.sqrt(3), -0.5 * np.sqrt(3)] - ]) + _t23 = 2 / 3 * np.array([[1, -0.5, -0.5], [0, 0.5 * np.sqrt(3), -0.5 * np.sqrt(3)]]) # transformation matrix from alpha-beta to abc representation - _t32 = np.array([ - [1, 0], - [-0.5, 0.5 * np.sqrt(3)], - [-0.5, -0.5 * np.sqrt(3)] - ]) + _t32 = np.array([[1, 0], [-0.5, 0.5 * np.sqrt(3)], [-0.5, -0.5 * np.sqrt(3)]]) @staticmethod def t_23(quantities): @@ -64,7 +58,9 @@ def q(quantities, epsilon): """ cos = math.cos(epsilon) sin = math.sin(epsilon) - return cos * quantities[0] - sin * quantities[1], sin * quantities[0] + cos * quantities[1] + return cos * quantities[0] - sin * quantities[1], sin * quantities[ + 0 + ] + cos * quantities[1] @staticmethod def q_inv(quantities, epsilon): @@ -94,7 +90,7 @@ def q_me(self, quantities, epsilon): Returns: Array of the two quantities converted to alpha-beta-representation. Example [u_alpha, u_beta] """ - return self.q(quantities, epsilon * self._motor_parameter['p']) + return self.q(quantities, epsilon * self._motor_parameter["p"]) def q_inv_me(self, quantities, epsilon): """ diff --git a/gym_electric_motor/physical_systems/mechanical_loads/constant_speed_load.py b/gym_electric_motor/physical_systems/mechanical_loads/constant_speed_load.py index 965ff10f..ab85c0ba 100644 --- a/gym_electric_motor/physical_systems/mechanical_loads/constant_speed_load.py +++ b/gym_electric_motor/physical_systems/mechanical_loads/constant_speed_load.py @@ -5,16 +5,16 @@ class ConstantSpeedLoad(MechanicalLoad): """ - Constant speed mechanical load system which will always set the speed - to a predefined value. + Constant speed mechanical load system which will always set the speed + to a predefined value. """ HAS_JACOBIAN = True _default_initializer = { - 'states': {'omega': 0.0}, - 'interval': None, - 'random_init': None, - 'random_params': (None, None) + "states": {"omega": 0.0}, + "interval": None, + "random_init": None, + "random_params": (None, None), } @property @@ -31,11 +31,11 @@ def __init__(self, omega_fixed=0, load_initializer=None, **kwargs): omega_fixed(float)): Fix value for the speed in rad/s. """ super().__init__(load_initializer=load_initializer, **kwargs) - self._omega = omega_fixed or self._initializer['states']['omega'] + self._omega = omega_fixed or self._initializer["states"]["omega"] if omega_fixed != 0: - self._initializer['states']['omega'] = omega_fixed - self._ode_result = np.array([0.]) - self._jacobian_result = (np.array([[0.]]), np.array([0.])) + self._initializer["states"]["omega"] = omega_fixed + self._ode_result = np.array([0.0]) + self._jacobian_result = (np.array([[0.0]]), np.array([0.0])) def mechanical_ode(self, *_, **__): # Docstring of superclass diff --git a/gym_electric_motor/physical_systems/mechanical_loads/external_speed_load.py b/gym_electric_motor/physical_systems/mechanical_loads/external_speed_load.py index de3ca3ea..37f1baa1 100644 --- a/gym_electric_motor/physical_systems/mechanical_loads/external_speed_load.py +++ b/gym_electric_motor/physical_systems/mechanical_loads/external_speed_load.py @@ -6,8 +6,8 @@ class ExternalSpeedLoad(MechanicalLoad): """ - External speed mechanical load system which will set the speed to a - predefined speed-function/ speed-profile. + External speed mechanical load system which will set the speed to a + predefined speed-function/ speed-profile. """ HAS_JACOBIAN = False @@ -20,7 +20,14 @@ def omega(self): """ return self._omega_initial - def __init__(self, speed_profile, load_initializer=None, tau=1e-4, speed_profile_kwargs=None, **kwargs): + def __init__( + self, + speed_profile, + load_initializer=None, + tau=1e-4, + speed_profile_kwargs=None, + **kwargs, + ): """ Args: speed_profile(float -> float): A callable(t, **speed_profile_args) -> float @@ -38,11 +45,12 @@ def __init__(self, speed_profile, load_initializer=None, tau=1e-4, speed_profile speed_profile_kwargs = speed_profile_kwargs or {} if load_initializer is not None: warnings.warn( - 'Given initializer will be overwritten with starting value ' - 'from speed-profile, to avoid complications at the load reset.' - ' It is recommended to choose starting value of' - ' load by the defined speed-profile.', - UserWarning) + "Given initializer will be overwritten with starting value " + "from speed-profile, to avoid complications at the load reset." + " It is recommended to choose starting value of" + " load by the defined speed-profile.", + UserWarning, + ) self.speed_profile_kwargs = speed_profile_kwargs self._speed_profile = speed_profile @@ -53,11 +61,12 @@ def __init__(self, speed_profile, load_initializer=None, tau=1e-4, speed_profile def mechanical_ode(self, t, mechanical_state, torque=None): # Docstring of superclass # calc next omega with given profile und tau - omega_next = self._speed_profile(t=t+self._tau, **self.speed_profile_kwargs) + omega_next = self._speed_profile(t=t + self._tau, **self.speed_profile_kwargs) # calculated T out of euler-forward, given omega_next and # actual omega give from system - return np.array([(1 / self._tau) * - (omega_next - mechanical_state[self.OMEGA_IDX])]) + return np.array( + [(1 / self._tau) * (omega_next - mechanical_state[self.OMEGA_IDX])] + ) def mechanical_jacobian(self, t, mechanical_state, torque): # Docstring of superclass diff --git a/gym_electric_motor/physical_systems/mechanical_loads/mechanical_load.py b/gym_electric_motor/physical_systems/mechanical_loads/mechanical_load.py index 841110c1..f6ebbbd9 100644 --- a/gym_electric_motor/physical_systems/mechanical_loads/mechanical_load.py +++ b/gym_electric_motor/physical_systems/mechanical_loads/mechanical_load.py @@ -88,13 +88,15 @@ def __init__(self, state_names=None, j_load=0.0, load_initializer=None): """ RandomComponent.__init__(self) self._j_total = self._j_load = j_load - self._state_names = list(state_names or ['omega']) + self._state_names = list(state_names or ["omega"]) self._limits = {} self._nominal_values = {} load_initializer = load_initializer or {} self._initializer = self._default_initializer.copy() self._initializer.update(load_initializer) - self._initial_states = self._initializer.get('states', {state: 0.0 for state in self._state_names}) + self._initial_states = self._initializer.get( + "states", {state: 0.0 for state in self._state_names} + ) def initialize(self, state_space, state_positions, nominal_state, **__): """Initializes the state of the load on an episode start. @@ -108,13 +110,15 @@ def initialize(self, state_space, state_positions, nominal_state, **__): state_positions(dict): indexes of system states """ # for order and organization purposes - interval = self._initializer['interval'] - random_dist = self._initializer['random_init'] - random_params = self._initializer['random_params'] + interval = self._initializer["interval"] + random_dist = self._initializer["random_init"] + random_params = self._initializer["random_params"] if isinstance(nominal_state, (list, np.ndarray)): nominal_state = np.asarray(nominal_state, dtype=float) elif isinstance(self._nominal_values, dict): - nominal_state = [nominal_state[state] for state in self._initial_states.keys()] + nominal_state = [ + nominal_state[state] for state in self._initial_states.keys() + ] nominal_state = np.asarray(nominal_state) # setting nominal values as interval limits state_idx = [state_positions[state] for state in self._initial_states.keys()] @@ -122,34 +126,45 @@ def initialize(self, state_space, state_positions, nominal_state, **__): lower_bound = upper_bound * np.asarray(state_space.low, dtype=float)[state_idx] # clip nominal boundaries to user defined if interval is not None: - lower_bound = np.clip(lower_bound, a_min=np.asarray(interval, dtype=float).T[0], a_max=None) - upper_bound = np.clip(upper_bound, a_min=None, a_max=np.asarray(interval, dtype=float).T[1]) + lower_bound = np.clip( + lower_bound, a_min=np.asarray(interval, dtype=float).T[0], a_max=None + ) + upper_bound = np.clip( + upper_bound, a_min=None, a_max=np.asarray(interval, dtype=float).T[1] + ) else: pass # random initialization for each load state (omega) if random_dist is not None: - if random_dist == 'uniform': - initial_value = (upper_bound - lower_bound) \ - * self.random_generator.uniform(size=len(self._initial_states.keys())) \ - + lower_bound + if random_dist == "uniform": + initial_value = ( + upper_bound - lower_bound + ) * self.random_generator.uniform( + size=len(self._initial_states.keys()) + ) + lower_bound random_states = { - state: initial_value[idx] for idx, state in enumerate(self._initial_states.keys()) + state: initial_value[idx] + for idx, state in enumerate(self._initial_states.keys()) } self._initial_states.update(random_states) - elif random_dist in ['normal', 'gaussian']: + elif random_dist in ["normal", "gaussian"]: # specific input or middle of interval - mue = random_params[0] \ - or (upper_bound - lower_bound) / 2 + lower_bound + mue = random_params[0] or (upper_bound - lower_bound) / 2 + lower_bound sigma = random_params[1] or 1 a = (lower_bound - mue) / sigma b = (upper_bound - mue) / sigma initial_value = truncnorm.rvs( - a, b, loc=mue, scale=sigma, size=(len(self._initial_states.keys())), - random_state=self.seed_sequence.pool[0] + a, + b, + loc=mue, + scale=sigma, + size=(len(self._initial_states.keys())), + random_state=self.seed_sequence.pool[0], ) random_states = { - state: initial_value[idx] for idx, state in enumerate(self._initial_states.keys()) + state: initial_value[idx] + for idx, state in enumerate(self._initial_states.keys()) } self._initial_states.update(random_states) else: @@ -158,15 +173,18 @@ def initialize(self, state_space, state_positions, nominal_state, **__): elif self._initial_states is not None: initial_value = np.atleast_1d(list(self._initial_states.values())) # check init_value meets interval boundaries - if (lower_bound <= initial_value).all() and (initial_value <= upper_bound).all(): + if (lower_bound <= initial_value).all() and ( + initial_value <= upper_bound + ).all(): initial_states_ = { - state: initial_value[idx] for idx, state in enumerate(self._initial_states.keys()) + state: initial_value[idx] + for idx, state in enumerate(self._initial_states.keys()) } self._initial_states.update(initial_states_) else: - raise Exception('Initialization Value have to be in nominal boundaries') + raise Exception("Initialization Value have to be in nominal boundaries") else: - raise Exception('No matching Initialization Case') + raise Exception("No matching Initialization Case") def reset(self, state_space, state_positions, nominal_state, **__): """ @@ -235,4 +253,4 @@ def get_state_space(self, omega_range): Returns: Tuple(dict,dict): Lowest and highest possible values for all states normalized to (-1, 1) """ - return {'omega': omega_range[0]}, {'omega': omega_range[1]} + return {"omega": omega_range[0]}, {"omega": omega_range[1]} diff --git a/gym_electric_motor/physical_systems/mechanical_loads/ornstein_uhlenbeck_load.py b/gym_electric_motor/physical_systems/mechanical_loads/ornstein_uhlenbeck_load.py index 8915ac75..0ce70b23 100644 --- a/gym_electric_motor/physical_systems/mechanical_loads/ornstein_uhlenbeck_load.py +++ b/gym_electric_motor/physical_systems/mechanical_loads/ornstein_uhlenbeck_load.py @@ -4,12 +4,13 @@ class OrnsteinUhlenbeckLoad(MechanicalLoad): - """The Ornstein-Uhlenbeck Load sets the speed to a torque-independent signal specified by the underlying OU-Process. - """ + """The Ornstein-Uhlenbeck Load sets the speed to a torque-independent signal specified by the underlying OU-Process.""" HAS_JACOBIAN = False - def __init__(self, mu=0, sigma=1e-4, theta=1, tau=1e-4, omega_range=(-200.0, 200.0), **kwargs): + def __init__( + self, mu=0, sigma=1e-4, theta=1, tau=1e-4, omega_range=(-200.0, 200.0), **kwargs + ): """ Args: mu(float): Mean value of the underlying gaussian distribution of the OU-Process. @@ -31,8 +32,9 @@ def mechanical_ode(self, t, mechanical_state, torque): omega = mechanical_state max_diff = (self._omega_range[1] - omega) / self.tau min_diff = (self._omega_range[0] - omega) / self.tau - diff = self.theta * (self.mu - omega) * self.tau \ - + self.sigma * np.sqrt(self.tau) * np.random.normal(size=1) + diff = self.theta * (self.mu - omega) * self.tau + self.sigma * np.sqrt( + self.tau + ) * np.random.normal(size=1) np.clip(diff, min_diff, max_diff, out=diff) return diff diff --git a/gym_electric_motor/physical_systems/mechanical_loads/polynomial_static_load.py b/gym_electric_motor/physical_systems/mechanical_loads/polynomial_static_load.py index 9106b816..3d8807eb 100644 --- a/gym_electric_motor/physical_systems/mechanical_loads/polynomial_static_load.py +++ b/gym_electric_motor/physical_systems/mechanical_loads/polynomial_static_load.py @@ -5,7 +5,7 @@ class PolynomialStaticLoad(MechanicalLoad): - """ Mechanical system that models the Mechanical-ODE based on a static polynomial load torque. + """Mechanical system that models the Mechanical-ODE based on a static polynomial load torque. Parameter dictionary entries: - :math:`a / Nm`: Constant Load Torque coefficient (for modeling static friction) @@ -35,12 +35,12 @@ class PolynomialStaticLoad(MechanicalLoad): """ - _load_parameter = dict(a=0.0, b=0.0, c=0., j_load=1e-5) + _load_parameter = dict(a=0.0, b=0.0, c=0.0, j_load=1e-5) _default_initializer = { - 'states': {'omega': 0.0}, - 'interval': None, - 'random_init': None, - 'random_params': (None, None) + "states": {"omega": 0.0}, + "interval": None, + "random_init": None, + "random_params": (None, None), } #: Time constant to smoothen the static load functions constant term "a" around 0 velocity @@ -64,7 +64,6 @@ def set_j_rotor(self, j_rotor): self._omega_linear_factor = self._j_total / self.tau_decay self._omega_lim = self._a / self._j_total * self.tau_decay - def __init__(self, load_parameter=None, limits=None, load_initializer=None): """ Args: @@ -73,12 +72,16 @@ def __init__(self, load_parameter=None, limits=None, load_initializer=None): load_initializer(dict): Dictionary to parameterize the initializer. """ load_parameter = load_parameter if load_parameter is not None else dict() - self._load_parameter = update_parameter_dict(self._load_parameter, load_parameter) - super().__init__(j_load=self._load_parameter['j_load'], load_initializer=load_initializer) + self._load_parameter = update_parameter_dict( + self._load_parameter, load_parameter + ) + super().__init__( + j_load=self._load_parameter["j_load"], load_initializer=load_initializer + ) self._limits.update(limits or {}) - self._a = self._load_parameter['a'] - self._b = self._load_parameter['b'] - self._c = self._load_parameter['c'] + self._a = self._load_parameter["a"] + self._b = self._load_parameter["b"] + self._c = self._load_parameter["c"] # Speed value at which the linear behavior switches to constant self._omega_lim = self._a / self._j_total * self.tau_decay # Slope for the linear growth of the constant load part around zero speed @@ -88,9 +91,11 @@ def _static_load(self, omega): """Calculation of the load torque for a given speed omega.""" sign = 1 if omega > 0 else -1 if omega < -0 else 0 # Limit the constant load term 'a' for velocities around zero for a more stable integration - a = sign * self._a \ - if abs(omega) > self._omega_lim \ + a = ( + sign * self._a + if abs(omega) > self._omega_lim else self._omega_linear_factor * omega + ) return sign * self._c * omega**2 + self._b * omega + a def mechanical_ode(self, t, mechanical_state, torque): @@ -105,6 +110,11 @@ def mechanical_jacobian(self, t, mechanical_state, torque): omega = mechanical_state[self.OMEGA_IDX] sign = 1 if omega > 0 else -1 if omega < 0 else 0 # Linear region of the constant load term 'a' ? - a = 0 if abs(omega) > self._a * self.tau_decay / self._j_total else self._j_total / self.tau_decay - return np.array([[(-self._b - 2 * sign * self._c * omega - a) / self._j_total]]), \ - np.array([1 / self._j_total]) + a = ( + 0 + if abs(omega) > self._a * self.tau_decay / self._j_total + else self._j_total / self.tau_decay + ) + return np.array( + [[(-self._b - 2 * sign * self._c * omega - a) / self._j_total]] + ), np.array([1 / self._j_total]) diff --git a/gym_electric_motor/physical_systems/physical_systems.py b/gym_electric_motor/physical_systems/physical_systems.py index 5ce61805..881325b7 100644 --- a/gym_electric_motor/physical_systems/physical_systems.py +++ b/gym_electric_motor/physical_systems/physical_systems.py @@ -14,6 +14,7 @@ class SCMLSystem(PhysicalSystem, RandomComponent): 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 = [] @@ -48,7 +49,9 @@ 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): + def __init__( + self, converter, motor, load, supply, ode_solver, tau=1e-4, calc_jacobian=None + ): """ Args: converter(PowerElectronicConverter): Converter for the physical system @@ -68,16 +71,27 @@ def __init__(self, converter, motor, load, supply, ode_solver, tau=1e-4, calc_ja 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: + 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') + 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._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) @@ -91,7 +105,11 @@ def __init__(self, converter, motor, load, supply, ode_solver, tau=1e-4, calc_ja 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._supply, + self._converter, + self._electrical_motor, + self._mechanical_load, + self._ode_solver, ] self._converter.tau = self.tau @@ -103,7 +121,7 @@ def _set_limits(self): 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 + self._limits[self._state_positions["u_sup"]] = self.supply.u_nominal def _set_nominal_state(self): """ @@ -113,7 +131,7 @@ def _set_nominal_state(self): 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 + self._nominal_state[self._state_positions["u_sup"]] = self.supply.u_nominal def _build_state_space(self, state_names): """ @@ -137,9 +155,12 @@ def _set_indices(self): """ 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._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) @@ -149,7 +170,9 @@ def _set_indices(self): 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)) + self.U_SUP_IDX = list( + range(voltages_upper, voltages_upper + self._supply.voltage_len) + ) def seed(self, seed=None): RandomComponent.seed(self, seed) @@ -172,7 +195,7 @@ def simulate(self, action, *_, **__): 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) @@ -187,8 +210,9 @@ def simulate(self, action, *_, **__): 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.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 @@ -216,27 +240,33 @@ def _system_equation(self, t, state, u_in, **__): 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._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() + 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_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) @@ -244,10 +274,12 @@ def _system_jacobian(self, t, state, u_in, **__): 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( + 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 @@ -261,12 +293,13 @@ def reset(self, *_): """ self.next_generator() motor_state = self._electrical_motor.reset( - state_space=self.state_space, - state_positions=self.state_positions) + 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) + nominal_state=self.nominal_state, + ) ode_state = np.concatenate((mechanical_state, motor_state)) u_sup = self.supply.reset() u_in = self.converter.reset() @@ -275,13 +308,15 @@ def reset(self, *_): 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 - )) + 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 @@ -294,23 +329,27 @@ def _build_state_names(self): # Docstring of superclass return ( self._mechanical_load.state_names - + ['torque'] + + ["torque"] + self._electrical_motor.CURRENTS + self._electrical_motor.VOLTAGES - + ['u_sup'] + + ["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, 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 + 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 + low["u_sup"] = self._supply.supply_range[0] / self._supply.u_nominal else: - low['u_sup'] = 0 + 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) @@ -320,6 +359,7 @@ 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 @@ -359,7 +399,9 @@ def abc_to_dq_space(self, abc_quantities, epsilon_el, normed_epsilon=False): """ if normed_epsilon: epsilon_el *= np.pi - dq_quantity = self._electrical_motor.q_inv(self._electrical_motor.t_23(abc_quantities), epsilon_el) + 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): @@ -376,9 +418,13 @@ def dq_to_abc_space(self, dq_quantities, epsilon_el, normed_epsilon=False): """ if normed_epsilon: epsilon_el *= np.pi - return self._electrical_motor.t_32(self._electrical_motor.q(dq_quantities, epsilon_el)) + 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): + def alphabeta_to_dq_space( + self, alphabeta_quantities, epsilon_el, normed_epsilon=False + ): """ Transformation from alphabeta to dq space @@ -417,7 +463,7 @@ class SynchronousMotorSystem(ThreePhaseMotorSystem): SCML-System that can be used with all Synchronous Motors """ - def __init__(self, control_space='abc', **kwargs): + 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 @@ -425,9 +471,10 @@ def __init__(self, control_space='abc', **kwargs): """ 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' + 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): @@ -439,21 +486,32 @@ def _build_state_space(self, state_names): 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', - ] - ) + 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._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] @@ -466,16 +524,20 @@ def _set_indices(self): 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.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] - if self.control_space == 'dq': + 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) + 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]: @@ -487,7 +549,9 @@ def simulate(self, action, *_, **__): 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_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) @@ -501,32 +565,26 @@ def simulate(self, action, *_, **__): 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) - ) + 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 - )) + system_state = np.concatenate( + (mechanical_state, [torque], i_abc, i_dq, u_in, u_dq, [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) + 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) + 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] @@ -541,20 +599,24 @@ def reset(self, *_): 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 + 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) @@ -564,21 +626,34 @@ def _build_state_space(self, state_names): 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' + 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._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] @@ -591,7 +666,9 @@ def _set_indices(self): 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.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, *_, **__): @@ -599,7 +676,9 @@ def simulate(self, action, *_, **__): 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:]) + 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]: @@ -613,7 +692,7 @@ def simulate(self, action, *_, **__): 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) @@ -626,32 +705,26 @@ def simulate(self, action, *_, **__): 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) - ) + 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 - )) + 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) + 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) + 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] @@ -666,21 +739,27 @@ def reset(self, *_): 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, - )) + system_state = np.concatenate( + ( + mechanical_state, + [torque], + i_abc, + i_dq, + u_abc, + u_dq, + [eps], + u_sup, + ) + ) return system_state / self._limits + class SquirrelCageInductionMotorSystem(ThreePhaseMotorSystem): """ SCML-System for the Squirrel Cage Induction Motor """ - def __init__(self, control_space='abc', ode_solver='scipy.ode', **kwargs): + + 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 @@ -688,7 +767,7 @@ def __init__(self, control_space='abc', ode_solver='scipy.ode', **kwargs): """ super().__init__(ode_solver=ode_solver, **kwargs) self.control_space = control_space - if control_space == 'dq': + if control_space == "dq": self._action_space = Box(-1, 1, shape=(2,), dtype=np.float64) def _build_state_space(self, state_names): @@ -700,22 +779,38 @@ def _build_state_space(self, state_names): 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', - ] - ) + 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 += 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._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) @@ -726,7 +821,9 @@ def _set_indices(self): 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.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): @@ -741,10 +838,12 @@ def simulate(self, action, *_, **__): eps_fs = self.calculate_field_angle(ode_state) - if self.control_space == 'dq': + 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])) + 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]: @@ -756,7 +855,9 @@ def simulate(self, action, *_, **__): 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_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) @@ -777,13 +878,9 @@ def simulate(self, action, *_, **__): 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 - )) + system_state = np.concatenate( + (mechanical_state, [torque], i_abc, i_dq, u_in, u_dq, [eps], u_sup) + ) return system_state / self._limits def reset(self, *_): @@ -791,11 +888,13 @@ def reset(self, *_): mechanical_state = self._mechanical_load.reset( state_positions=self.state_positions, state_space=self.state_space, - nominal_state=self.nominal_state) + nominal_state=self.nominal_state, + ) motor_state = self._electrical_motor.reset( state_space=self.state_space, state_positions=self.state_positions, - omega=mechanical_state) + omega=mechanical_state, + ) ode_state = np.concatenate((mechanical_state, motor_state)) u_sup = self.supply.reset() @@ -814,13 +913,9 @@ def reset(self, *_): 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 - ]) + system_state = np.concatenate( + [mechanical_state, [torque], i_abc, i_dq, u_abc, u_dq, [eps], u_sup] + ) return system_state / self._limits @@ -828,7 +923,8 @@ class DoublyFedInductionMotorSystem(ThreePhaseMotorSystem): """ SCML-System for the Doubly Fed Induction Motor """ - def __init__(self, ode_solver='scipy.ode', **kwargs): + + def __init__(self, ode_solver="scipy.ode", **kwargs): """ Args: kwargs: Further arguments to pass tp SCMLSystem @@ -837,15 +933,19 @@ def __init__(self, ode_solver='scipy.ode', **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.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.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.""" @@ -853,7 +953,7 @@ def _set_limits(self): 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 + self._limits[self._state_positions["u_sup"]] = self.supply.u_nominal def _build_state_space(self, state_names): # Docstring of superclass @@ -864,26 +964,49 @@ def _build_state_space(self, state_names): 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', - ] + 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 += 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._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) @@ -894,7 +1017,9 @@ def _set_indices(self): 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.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] @@ -908,15 +1033,15 @@ def calculate_field_angle(self, state): 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'] + 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 + 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] def simulate(self, action, *_, **__): @@ -936,18 +1061,22 @@ def simulate(self, action, *_, **__): 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_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 = 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_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) @@ -957,17 +1086,23 @@ def simulate(self, action, *_, **__): 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_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 = 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_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_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) @@ -982,23 +1117,31 @@ def simulate(self, action, *_, **__): 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)) + 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, - )) + 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 def reset(self, *_): @@ -1006,11 +1149,13 @@ def reset(self, *_): mechanical_state = self._mechanical_load.reset( state_positions=self.state_positions, state_space=self.state_space, - nominal_state=self.nominal_state) + nominal_state=self.nominal_state, + ) motor_state = self._electrical_motor.reset( state_space=self.state_space, state_positions=self.state_positions, - omega=mechanical_state) + omega=mechanical_state, + ) ode_state = np.concatenate((mechanical_state, motor_state)) u_sup = self.supply.reset() @@ -1025,28 +1170,37 @@ def reset(self, *_): 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_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) + 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) + 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 - ]) + 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 diff --git a/gym_electric_motor/physical_systems/solvers.py b/gym_electric_motor/physical_systems/solvers.py index 98a4333d..a9dd85fb 100644 --- a/gym_electric_motor/physical_systems/solvers.py +++ b/gym_electric_motor/physical_systems/solvers.py @@ -94,7 +94,9 @@ def __init__(self, nsteps=1): but take also longer to compute. """ self._nsteps = nsteps - self._integrate = self._integrate_one_step if nsteps == 1 else self._integrate_nsteps + self._integrate = ( + self._integrate_one_step if nsteps == 1 else self._integrate_nsteps + ) def integrate(self, t): # Docstring of superclass @@ -131,7 +133,9 @@ def _integrate_one_step(self, t): Returns: ndarray(float):The new state of the system. """ - self._y = self._y + self._system_equation(self._t, self._y, *self._f_params) * (t - self._t) + self._y = self._y + self._system_equation(self._t, self._y, *self._f_params) * ( + t - self._t + ) self._t = t return self._y @@ -154,7 +158,7 @@ def t(self): def y(self): return self._ode.y - def __init__(self, integrator='dopri5', **kwargs): + def __init__(self, integrator="dopri5", **kwargs): """ Args: integrator(str): String to choose the integrator from the scipy.integrate.ode @@ -167,7 +171,9 @@ def __init__(self, integrator='dopri5', **kwargs): def set_system_equation(self, system_equation, jac=None): # Docstring of superclass super().set_system_equation(system_equation, jac) - self._ode = ode(system_equation, jac).set_integrator(self._integrator, **self._solver_args) + self._ode = ode(system_equation, jac).set_integrator( + self._integrator, **self._solver_args + ) def set_initial_value(self, initial_value, t=0): # Docstring of superclass @@ -197,17 +203,22 @@ def __init__(self, **kwargs): def set_system_equation(self, system_equation, jac=None): # Docstring of superclass - method = self._solver_kwargs.get('method', None) + method = self._solver_kwargs.get("method", None) super().set_system_equation(system_equation, jac) # Only Radau BDF and LSODA support the jacobian. - if method in ['Radau', 'BDF', 'LSODA']: - self._solver_kwargs['jac'] = self._system_jacobian + if method in ["Radau", "BDF", "LSODA"]: + self._solver_kwargs["jac"] = self._system_jacobian def integrate(self, t): # Docstring of superclass result = solve_ivp( - self._system_equation, [self._t, t], self._y, t_eval=[t], args=self._f_params, **self._solver_kwargs + self._system_equation, + [self._t, t], + self._y, + t_eval=[t], + args=self._f_params, + **self._solver_kwargs, ) self._t = t self._y = result.y.T[-1] @@ -230,8 +241,15 @@ def __init__(self, **kwargs): def integrate(self, t): # Docstring of superclass - result = odeint(self._system_equation, self._y, [self._t, t], args=self._f_params, Dfun=self._system_jacobian, - tfirst=True, **self._solver_args) + result = odeint( + self._system_equation, + self._y, + [self._t, t], + args=self._f_params, + Dfun=self._system_jacobian, + tfirst=True, + **self._solver_args, + ) self._t = t self._y = result[-1] return self._y diff --git a/gym_electric_motor/physical_systems/voltage_supplies.py b/gym_electric_motor/physical_systems/voltage_supplies.py index 7dc42944..d431a74e 100644 --- a/gym_electric_motor/physical_systems/voltage_supplies.py +++ b/gym_electric_motor/physical_systems/voltage_supplies.py @@ -72,28 +72,32 @@ def get_voltage(self, *_, **__): class RCVoltageSupply(VoltageSupply): """DC voltage supply modeled as RC element""" - + def __init__(self, u_nominal=600.0, supply_parameter=None): """This Voltage Supply is a model of a non ideal voltage supply. The ideal voltage source U_0 is part of an RC element. - - Args: + + Args: supply_parameter(dict): Consists or Resistance R in Ohm and Capacitance C in Farad - + Additional notes: If the product of R and C get too small the numerical stability of the ODE is not given anymore typical time differences tau are only in the range of 10e-3. One might want to consider R*C as a time constant. The resistance R can be considered as a simplified inner resistance model. """ super().__init__(u_nominal) - supply_parameter = supply_parameter or {'R': 1, 'C': 4e-3} + supply_parameter = supply_parameter or {"R": 1, "C": 4e-3} # Supply range is between 0 - capacitor completely unloaded - and u_nominal - capacitor is completely loaded - assert 'R' in supply_parameter.keys(), "Pass key 'R' for Resistance in your dict" - assert 'C' in supply_parameter.keys(), "Pass key 'C' for Capacitance in your dict" - self.supply_range = (0,u_nominal) - self._r = supply_parameter['R'] - self._c = supply_parameter['C'] - if self._r*self._c < 1e-4: + assert ( + "R" in supply_parameter.keys() + ), "Pass key 'R' for Resistance in your dict" + assert ( + "C" in supply_parameter.keys() + ), "Pass key 'C' for Capacitance in your dict" + self.supply_range = (0, u_nominal) + self._r = supply_parameter["R"] + self._c = supply_parameter["C"] + if self._r * self._c < 1e-4: warnings.warn( "The product of R and C might be too small for the correct calculation of the supply voltage. " "You might want to consider R*C as a time constant." @@ -102,10 +106,10 @@ def __init__(self, u_nominal=600.0, supply_parameter=None): self._u_0 = u_nominal self._solver = EulerSolver() self._solver.set_system_equation(self.system_equation) - + def system_equation(self, t, u_sup, u_0, i_sup, r, c): # ODE for derivate of u_sup - return np.array([(u_0 - u_sup[0] - r*i_sup)/(r*c)]) + return np.array([(u_0 - u_sup[0] - r * i_sup) / (r * c)]) def reset(self): # Docstring of superclass @@ -113,7 +117,7 @@ def reset(self): self._solver.set_initial_value(np.array([self._u_0])) self._u_sup = [self._u_0] return self._u_sup - + def get_voltage(self, t, i_sup): # Docstring of superclass self._solver.set_f_params(self._u_0, i_sup, self._r, self._c) @@ -132,38 +136,45 @@ def __init__(self, u_nominal=230, supply_parameter=None): """ super().__init__(u_nominal) - + self._fixed_phi = False if supply_parameter is not None: - assert isinstance(supply_parameter, dict), "supply_parameter should be a dict" - assert 'frequency' in supply_parameter.keys(), "Pass key 'frequency' for frequency f in Hz in your dict" - if 'phase' in supply_parameter.keys(): - assert 0<= supply_parameter['phase'] < 2*np.pi, "The phase angle has to be given in rad in range [0,2*pi)" + assert isinstance( + supply_parameter, dict + ), "supply_parameter should be a dict" + assert ( + "frequency" in supply_parameter.keys() + ), "Pass key 'frequency' for frequency f in Hz in your dict" + if "phase" in supply_parameter.keys(): + assert ( + 0 <= supply_parameter["phase"] < 2 * np.pi + ), "The phase angle has to be given in rad in range [0,2*pi)" self._fixed_phi = True supply_parameter = supply_parameter else: - supply_parameter['phase'] = np.random.rand()*2*np.pi + supply_parameter["phase"] = np.random.rand() * 2 * np.pi else: - supply_parameter = {'frequency': 50, 'phase': np.random.rand()*2*np.pi} + supply_parameter = {"frequency": 50, "phase": np.random.rand() * 2 * np.pi} + + self._f = supply_parameter["frequency"] + self._phi = supply_parameter["phase"] + self._max_amp = self._u_nominal * np.sqrt(2) + self.supply_range = [-1 * self._max_amp, self._max_amp] - self._f = supply_parameter['frequency'] - self._phi = supply_parameter['phase'] - self._max_amp = self._u_nominal*np.sqrt(2) - self.supply_range = [-1*self._max_amp, self._max_amp] - def reset(self): if not self._fixed_phi: - self._phi = np.random.rand()*2*np.pi + self._phi = np.random.rand() * 2 * np.pi return self.get_voltage(0) - + def get_voltage(self, t, *_, **__): # Docstring of superclass - self._u_sup = [self._max_amp*np.sin(2*np.pi*self._f*t + self._phi)] + self._u_sup = [self._max_amp * np.sin(2 * np.pi * self._f * t + self._phi)] return self._u_sup class AC3PhaseSupply(VoltageSupply): """AC three phase voltage supply""" + voltage_len = 3 def __init__(self, u_nominal=400, supply_parameter=None): @@ -176,30 +187,39 @@ def __init__(self, u_nominal=400, supply_parameter=None): super().__init__(u_nominal) self._fixed_phi = False if supply_parameter is not None: - assert isinstance(supply_parameter, dict), "supply_parameter should be a dict" - assert 'frequency' in supply_parameter.keys(), "Pass key 'frequency' for frequency f in Hz in your dict" - if 'phase' in supply_parameter.keys(): - assert 0 <= supply_parameter['phase'] < 2*np.pi,\ - "The phase angle has to be given in rad in range [0,2*pi)" + assert isinstance( + supply_parameter, dict + ), "supply_parameter should be a dict" + assert ( + "frequency" in supply_parameter.keys() + ), "Pass key 'frequency' for frequency f in Hz in your dict" + if "phase" in supply_parameter.keys(): + assert ( + 0 <= supply_parameter["phase"] < 2 * np.pi + ), "The phase angle has to be given in rad in range [0,2*pi)" self._fixed_phi = True supply_parameter = supply_parameter else: - supply_parameter['phase'] = np.random.rand()*2*np.pi + supply_parameter["phase"] = np.random.rand() * 2 * np.pi else: - supply_parameter = {'frequency': 50, 'phase': np.random.rand()*2*np.pi} + supply_parameter = {"frequency": 50, "phase": np.random.rand() * 2 * np.pi} + + self._f = supply_parameter["frequency"] + self._phi = supply_parameter["phase"] + self._max_amp = self._u_nominal / np.sqrt(3) * np.sqrt(2) + self.supply_range = [-1 * self._max_amp, self._max_amp] - self._f = supply_parameter['frequency'] - self._phi = supply_parameter['phase'] - self._max_amp = self._u_nominal/np.sqrt(3)*np.sqrt(2) - self.supply_range = [-1*self._max_amp, self._max_amp] - def reset(self): # Docstring of superclass if not self._fixed_phi: - self._phi = np.random.rand()*2*np.pi + self._phi = np.random.rand() * 2 * np.pi return self.get_voltage(0) - + def get_voltage(self, t, *_, **__): # Docstring of superclass - self._u_sup = [self._max_amp*np.sin(2*np.pi*self._f*t + self._phi + 2/3*np.pi*i) for i in range(3)] + self._u_sup = [ + self._max_amp + * np.sin(2 * np.pi * self._f * t + self._phi + 2 / 3 * np.pi * i) + for i in range(3) + ] return self._u_sup diff --git a/gym_electric_motor/reference_generators/__init__.py b/gym_electric_motor/reference_generators/__init__.py index 99508f4e..a0643277 100644 --- a/gym_electric_motor/reference_generators/__init__.py +++ b/gym_electric_motor/reference_generators/__init__.py @@ -12,12 +12,16 @@ from ..utils import register_class from ..core import ReferenceGenerator -register_class(WienerProcessReferenceGenerator, ReferenceGenerator, 'WienerProcessReference') -register_class(SwitchedReferenceGenerator, ReferenceGenerator, 'SwitchedReference') -register_class(StepReferenceGenerator, ReferenceGenerator, 'StepReference') -register_class(SinusoidalReferenceGenerator, ReferenceGenerator, 'SinusReference') -register_class(TriangularReferenceGenerator, ReferenceGenerator, 'TriangleReference') -register_class(SawtoothReferenceGenerator, ReferenceGenerator, 'SawtoothReference') -register_class(ConstReferenceGenerator, ReferenceGenerator, 'ConstReference') -register_class(MultipleReferenceGenerator, ReferenceGenerator, 'MultipleReference') -register_class(SubepisodedReferenceGenerator, ReferenceGenerator, 'SubepisodedReference') +register_class( + WienerProcessReferenceGenerator, ReferenceGenerator, "WienerProcessReference" +) +register_class(SwitchedReferenceGenerator, ReferenceGenerator, "SwitchedReference") +register_class(StepReferenceGenerator, ReferenceGenerator, "StepReference") +register_class(SinusoidalReferenceGenerator, ReferenceGenerator, "SinusReference") +register_class(TriangularReferenceGenerator, ReferenceGenerator, "TriangleReference") +register_class(SawtoothReferenceGenerator, ReferenceGenerator, "SawtoothReference") +register_class(ConstReferenceGenerator, ReferenceGenerator, "ConstReference") +register_class(MultipleReferenceGenerator, ReferenceGenerator, "MultipleReference") +register_class( + SubepisodedReferenceGenerator, ReferenceGenerator, "SubepisodedReference" +) diff --git a/gym_electric_motor/reference_generators/const_reference_generator.py b/gym_electric_motor/reference_generators/const_reference_generator.py index f8001ee4..8bab6c35 100644 --- a/gym_electric_motor/reference_generators/const_reference_generator.py +++ b/gym_electric_motor/reference_generators/const_reference_generator.py @@ -9,7 +9,7 @@ class ConstReferenceGenerator(ReferenceGenerator): Reference Generator that generates a constant reference for a single state variable. """ - def __init__(self, reference_state='omega', reference_value=0.5, **kwargs): + def __init__(self, reference_state="omega", reference_value=0.5, **kwargs): """ Args: reference_value(float): Normalized Value for the const reference. @@ -19,7 +19,9 @@ def __init__(self, reference_state='omega', reference_value=0.5, **kwargs): super().__init__(**kwargs) self._reference_value = reference_value self._reference_state = reference_state.lower() - self.reference_space = Box(np.array([reference_value]), np.array([reference_value]), dtype=np.float64) + self.reference_space = Box( + np.array([reference_value]), np.array([reference_value]), dtype=np.float64 + ) self._reference_names = self._reference_state def set_modules(self, physical_system): diff --git a/gym_electric_motor/reference_generators/laplace_process_reference_generator.py b/gym_electric_motor/reference_generators/laplace_process_reference_generator.py index 80aa1a81..018044cf 100644 --- a/gym_electric_motor/reference_generators/laplace_process_reference_generator.py +++ b/gym_electric_motor/reference_generators/laplace_process_reference_generator.py @@ -23,7 +23,9 @@ def __init__(self, sigma_range=(1e-3, 1e-1), **kwargs): def _reset_reference(self): self._current_sigma = 10 ** self._get_current_value(np.log10(self._sigma_range)) - random_values = self.random_generator.laplace(0, self._current_sigma, self._current_episode_length) + random_values = self.random_generator.laplace( + 0, self._current_sigma, self._current_episode_length + ) self._reference = np.zeros_like(random_values) reference_value = self._reference_value for i in range(self._current_episode_length): @@ -33,4 +35,3 @@ def _reset_reference(self): if reference_value < self._limit_margin[0]: reference_value = self._limit_margin[0] self._reference[i] = reference_value - diff --git a/gym_electric_motor/reference_generators/multiple_reference_generator.py b/gym_electric_motor/reference_generators/multiple_reference_generator.py index e6342ff3..74e67552 100644 --- a/gym_electric_motor/reference_generators/multiple_reference_generator.py +++ b/gym_electric_motor/reference_generators/multiple_reference_generator.py @@ -24,13 +24,15 @@ def __init__(self, sub_generators, sub_args=None, **kwargs): self.reference_space = Box(-1, 1, shape=(1,), dtype=np.float64) if type(sub_args) is dict: sub_arguments = [sub_args] * len(sub_generators) - elif hasattr(sub_args, '__iter__'): + elif hasattr(sub_args, "__iter__"): assert len(sub_args) == len(sub_generators) sub_arguments = sub_args else: sub_arguments = [kwargs] * len(sub_generators) - self._sub_generators = [instantiate(ReferenceGenerator, sub_generator, **sub_arg) - for sub_generator, sub_arg in zip(sub_generators, sub_arguments)] + self._sub_generators = [ + instantiate(ReferenceGenerator, sub_generator, **sub_arg) + for sub_generator, sub_arg in zip(sub_generators, sub_arguments) + ] self._reference_names = [] for sub_gen in self._sub_generators: self._reference_names += sub_gen.reference_names @@ -45,14 +47,33 @@ def set_modules(self, physical_system): sub_generator.set_modules(physical_system) # Ensure that all referenced states are different - assert all(sum([sub_generator.referenced_states.astype(int) for sub_generator in self._sub_generators]) < 2), \ - 'Some of the passed reference generators share the same reference variable' + assert all( + sum( + [ + sub_generator.referenced_states.astype(int) + for sub_generator in self._sub_generators + ] + ) + < 2 + ), "Some of the passed reference generators share the same reference variable" - ref_space_low = np.concatenate([sub_generator.reference_space.low for sub_generator in self._sub_generators]) - ref_space_high = np.concatenate([sub_generator.reference_space.high for sub_generator in self._sub_generators]) + ref_space_low = np.concatenate( + [ + sub_generator.reference_space.low + for sub_generator in self._sub_generators + ] + ) + ref_space_high = np.concatenate( + [ + sub_generator.reference_space.high + for sub_generator in self._sub_generators + ] + ) self.reference_space = Box(ref_space_low, ref_space_high, dtype=np.float64) self._referenced_states = np.sum( - [sub_generator.referenced_states for sub_generator in self._sub_generators], dtype=bool, axis=0 + [sub_generator.referenced_states for sub_generator in self._sub_generators], + dtype=bool, + axis=0, ) def reset(self, initial_state=None, initial_reference=None): @@ -60,19 +81,29 @@ def reset(self, initial_state=None, initial_reference=None): refs = np.zeros_like(self._physical_system.state_names, dtype=float) ref_obs = np.array([]) for sub_generator in self._sub_generators: - ref, ref_observation, _ = sub_generator.reset(initial_state, initial_reference) + ref, ref_observation, _ = sub_generator.reset( + initial_state, initial_reference + ) refs += ref ref_obs = np.concatenate((ref_obs, ref_observation)) return refs, ref_obs, None def get_reference(self, state, **kwargs): # docstring from superclass - return sum([sub_generator.get_reference(state, **kwargs) for sub_generator in self._sub_generators]) + return sum( + [ + sub_generator.get_reference(state, **kwargs) + for sub_generator in self._sub_generators + ] + ) def get_reference_observation(self, state, *_, **kwargs): # docstring from superclass return np.concatenate( - [sub_generator.get_reference_observation(state, **kwargs) for sub_generator in self._sub_generators] + [ + sub_generator.get_reference_observation(state, **kwargs) + for sub_generator in self._sub_generators + ] ) def seed(self, seed=None): diff --git a/gym_electric_motor/reference_generators/sawtooth_reference_generator.py b/gym_electric_motor/reference_generators/sawtooth_reference_generator.py index a732bb57..c9a9af57 100644 --- a/gym_electric_motor/reference_generators/sawtooth_reference_generator.py +++ b/gym_electric_motor/reference_generators/sawtooth_reference_generator.py @@ -8,11 +8,14 @@ class SawtoothReferenceGenerator(SubepisodedReferenceGenerator): Reference Generator that generates a Sawtooth wave with a random amplitude, frequency, phase and offset. The reference is generated for a certain length and then new parameters are drawn uniformly from a selectable range. """ + _amplitude = 0 _frequency = 0 _offset = 0 - def __init__(self, amplitude_range=None, frequency_range=(1, 10), offset_range=None, **kwargs): + def __init__( + self, amplitude_range=None, frequency_range=(1, 10), offset_range=None, **kwargs + ): """ Args: amplitude_range(tuple(float,float)): Lower and upper limit for the amplitude. @@ -28,20 +31,38 @@ def __init__(self, amplitude_range=None, frequency_range=(1, 10), offset_range=N def set_modules(self, physical_system): super().set_modules(physical_system) - self._amplitude_range = np.clip(self._amplitude_range, 0, (self._limit_margin[1] - self._limit_margin[0]) / 2) + self._amplitude_range = np.clip( + self._amplitude_range, + 0, + (self._limit_margin[1] - self._limit_margin[0]) / 2, + ) # limit_margin will range from(-1, 1) but amplitude cannot exceed 1 - self._offset_range = np.clip(self._offset_range, self._limit_margin[0], self._limit_margin[1]) + self._offset_range = np.clip( + self._offset_range, self._limit_margin[0], self._limit_margin[1] + ) def _reset_reference(self): # get absolute values of amplitude, frequency and offset self._amplitude = self._get_current_value(self._amplitude_range) self._frequency = self._get_current_value(self._frequency_range) - offset_range = np.clip(self._offset_range, -self._limit_margin[1] + self._amplitude, - self._limit_margin[1] - self._amplitude) + offset_range = np.clip( + self._offset_range, + -self._limit_margin[1] + self._amplitude, + self._limit_margin[1] - self._amplitude, + ) self._offset = self._get_current_value(offset_range) - t = np.linspace(0, (self._current_episode_length - 1) * self._physical_system.tau, self._current_episode_length) + t = np.linspace( + 0, + (self._current_episode_length - 1) * self._physical_system.tau, + self._current_episode_length, + ) phase = self.random_generator.uniform() * 2 * np.pi # note: in the scipy implementation of sawtooth() 1 time-period corresponds to a phase of 2pi - self._reference = self._amplitude * sg.sawtooth(2 * np.pi * self._frequency * t + phase) + self._offset - self._reference = np.clip(self._reference, self._limit_margin[0], self._limit_margin[1]) + self._reference = ( + self._amplitude * sg.sawtooth(2 * np.pi * self._frequency * t + phase) + + self._offset + ) + self._reference = np.clip( + self._reference, self._limit_margin[0], self._limit_margin[1] + ) diff --git a/gym_electric_motor/reference_generators/sinusoidal_reference_generator.py b/gym_electric_motor/reference_generators/sinusoidal_reference_generator.py index 581a361a..1458bb4f 100644 --- a/gym_electric_motor/reference_generators/sinusoidal_reference_generator.py +++ b/gym_electric_motor/reference_generators/sinusoidal_reference_generator.py @@ -13,7 +13,14 @@ class SinusoidalReferenceGenerator(SubepisodedReferenceGenerator): _frequency = 0 _offset = 0 - def __init__(self, amplitude_range=None, frequency_range=(1, 10), offset_range=None, *_, **kwargs): + def __init__( + self, + amplitude_range=None, + frequency_range=(1, 10), + offset_range=None, + *_, + **kwargs, + ): """ Args: amplitude_range(tuple(float,float)): Lower and upper limit for the amplitude. @@ -28,8 +35,14 @@ def __init__(self, amplitude_range=None, frequency_range=(1, 10), offset_range=N def set_modules(self, physical_system): super().set_modules(physical_system) - self._amplitude_range = np.clip(self._amplitude_range, 0, (self._limit_margin[1] - self._limit_margin[0]) / 2) - self._offset_range = np.clip(self._offset_range, self._limit_margin[0], self._limit_margin[1]) + self._amplitude_range = np.clip( + self._amplitude_range, + 0, + (self._limit_margin[1] - self._limit_margin[0]) / 2, + ) + self._offset_range = np.clip( + self._offset_range, self._limit_margin[0], self._limit_margin[1] + ) def _reset_reference(self): self._amplitude = self._get_current_value(self._amplitude_range) @@ -37,10 +50,19 @@ def _reset_reference(self): offset_range = np.clip( self._offset_range, -self._limit_margin[1] + self._amplitude, - self._limit_margin[1] - self._amplitude + self._limit_margin[1] - self._amplitude, ) self._offset = self._get_current_value(offset_range) - t = np.linspace(0, (self._current_episode_length - 1) * self._physical_system.tau, self._current_episode_length) + t = np.linspace( + 0, + (self._current_episode_length - 1) * self._physical_system.tau, + self._current_episode_length, + ) phase = self.random_generator.uniform() * 2 * np.pi - self._reference = self._amplitude * np.sin(2 * np.pi * self._frequency * t + phase) + self._offset - self._reference = np.clip(self._reference, self._limit_margin[0], self._limit_margin[1]) + self._reference = ( + self._amplitude * np.sin(2 * np.pi * self._frequency * t + phase) + + self._offset + ) + self._reference = np.clip( + self._reference, self._limit_margin[0], self._limit_margin[1] + ) diff --git a/gym_electric_motor/reference_generators/step_reference_generator.py b/gym_electric_motor/reference_generators/step_reference_generator.py index d5d40b5b..d4fd4701 100644 --- a/gym_electric_motor/reference_generators/step_reference_generator.py +++ b/gym_electric_motor/reference_generators/step_reference_generator.py @@ -8,11 +8,14 @@ class StepReferenceGenerator(SubepisodedReferenceGenerator): Reference Generator that generates a step function with a random amplitude, frequency, phase and offset. The reference is generated for a certain length and then new parameters are drawn uniformly from a selectable range. """ + _amplitude = 0 _frequency = 0 _offset = 0 - def __init__(self, amplitude_range=None, frequency_range=(1, 10), offset_range=None, **kwargs): + def __init__( + self, amplitude_range=None, frequency_range=(1, 10), offset_range=None, **kwargs + ): """ Args: amplitude_range(tuple(float,float)): Lower and upper limit for the amplitude. @@ -27,8 +30,14 @@ def __init__(self, amplitude_range=None, frequency_range=(1, 10), offset_range=N def set_modules(self, physical_system): super().set_modules(physical_system) - self._amplitude_range = np.clip(self._amplitude_range, 0, (self._limit_margin[1] - self._limit_margin[0]) / 2) - self._offset_range = np.clip(self._offset_range, self._limit_margin[0], self._limit_margin[1]) + self._amplitude_range = np.clip( + self._amplitude_range, + 0, + (self._limit_margin[1] - self._limit_margin[0]) / 2, + ) + self._offset_range = np.clip( + self._offset_range, self._limit_margin[0], self._limit_margin[1] + ) def _reset_reference(self): self._amplitude = self._get_current_value(self._amplitude_range) @@ -36,16 +45,22 @@ def _reset_reference(self): offset_range = np.clip( self._offset_range, self._limit_margin[0] + self._amplitude, - self._limit_margin[1] - self._amplitude + self._limit_margin[1] - self._amplitude, ) self._offset = self._get_current_value(offset_range) high_low_ratio = self.random_generator.triangular(0, 0.5, 1) - t = np.linspace(0, (self._current_episode_length - 1) * self._physical_system.tau, self._current_episode_length) - x = self._frequency * (t % (1/self._frequency)) + t = np.linspace( + 0, + (self._current_episode_length - 1) * self._physical_system.tau, + self._current_episode_length, + ) + x = self._frequency * (t % (1 / self._frequency)) x -= high_low_ratio x = np.sign(x) phase = self.random_generator.uniform() steps_per_period = 1 / self._frequency / self._physical_system.tau x = np.roll(x, int(steps_per_period * phase)) self._reference = self._amplitude * x + self._offset - self._reference = np.clip(self._reference, self._limit_margin[0], self._limit_margin[1]) + self._reference = np.clip( + self._reference, self._limit_margin[0], self._limit_margin[1] + ) diff --git a/gym_electric_motor/reference_generators/subepisoded_reference_generator.py b/gym_electric_motor/reference_generators/subepisoded_reference_generator.py index c8742e5f..74143045 100644 --- a/gym_electric_motor/reference_generators/subepisoded_reference_generator.py +++ b/gym_electric_motor/reference_generators/subepisoded_reference_generator.py @@ -11,7 +11,13 @@ class SubepisodedReferenceGenerator(ReferenceGenerator, RandomComponent): time steps and can pre-calculate their references in these "sub episodes". """ - def __init__(self, reference_state='omega', episode_lengths=(500, 2000), limit_margin=None, **kwargs): + def __init__( + self, + reference_state="omega", + episode_lengths=(500, 2000), + limit_margin=None, + **kwargs, + ): """ Args: reference_state(str): Name of the state that this reference generator is referencing. @@ -44,8 +50,12 @@ def set_modules(self, physical_system): rs = self._referenced_states ps = physical_system if self._limit_margin is None: - upper_margin = (ps.nominal_state[rs] / ps.limits[rs])[0] * ps.state_space.high[rs] - lower_margin = (ps.nominal_state[rs] / ps.limits[rs])[0] * ps.state_space.low[rs] + upper_margin = (ps.nominal_state[rs] / ps.limits[rs])[ + 0 + ] * ps.state_space.high[rs] + lower_margin = (ps.nominal_state[rs] / ps.limits[rs])[ + 0 + ] * ps.state_space.low[rs] self._limit_margin = lower_margin[0], upper_margin[0] elif type(self._limit_margin) in [float, int]: upper_margin = self._limit_margin * ps.state_space.high[rs] @@ -56,8 +66,10 @@ def set_modules(self, physical_system): upper_margin = self._limit_margin[1] * ps.state_space.high[rs] self._limit_margin = lower_margin[0], upper_margin[0] else: - raise Exception('Unknown type for the limit margin.') - self.reference_space = Box(lower_margin[0], upper_margin[0], shape=(1,), dtype=np.float64) + raise Exception("Unknown type for the limit margin.") + self.reference_space = Box( + lower_margin[0], upper_margin[0], shape=(1,), dtype=np.float64 + ) def reset(self, initial_state=None, initial_reference=None): """ @@ -89,7 +101,9 @@ def get_reference(self, *_, **__): def get_reference_observation(self, *_, **__): if self._k >= self._current_episode_length: self._k = 0 - self._current_episode_length = int(self._get_current_value(self._episode_len_range)) + self._current_episode_length = int( + self._get_current_value(self._episode_len_range) + ) self._reset_reference() self._reference_value = self._reference[self._k] self._k += 1 @@ -112,4 +126,6 @@ def _get_current_value(self, value_range): if type(value_range) in [int, float]: return value_range elif type(value_range) in [list, tuple, np.ndarray]: - return (value_range[1] - value_range[0]) * self._random_generator.uniform() + value_range[0] + return ( + value_range[1] - value_range[0] + ) * self._random_generator.uniform() + value_range[0] diff --git a/gym_electric_motor/reference_generators/switched_reference_generator.py b/gym_electric_motor/reference_generators/switched_reference_generator.py index 48f3e9e1..2772ed6c 100644 --- a/gym_electric_motor/reference_generators/switched_reference_generator.py +++ b/gym_electric_motor/reference_generators/switched_reference_generator.py @@ -7,8 +7,7 @@ class SwitchedReferenceGenerator(ReferenceGenerator, RandomComponent): - """Reference Generator that switches randomly between multiple sub generators with a certain probability p for each. - """ + """Reference Generator that switches randomly between multiple sub generators with a certain probability p for each.""" def __init__(self, sub_generators, p=None, super_episode_length=(100, 10000)): """ @@ -25,12 +24,13 @@ def __init__(self, sub_generators, p=None, super_episode_length=(100, 10000)): self._k = 0 self._sub_generators = list(sub_generators) - assert len(self._sub_generators) > 0, 'No sub generator was passed.' + assert len(self._sub_generators) > 0, "No sub generator was passed." ref_names = self._sub_generators[0].reference_names - assert all(sub_gen.reference_names == ref_names for sub_gen in self._sub_generators),\ - 'The passed sub generators have different referenced states.' + assert all( + sub_gen.reference_names == ref_names for sub_gen in self._sub_generators + ), "The passed sub generators have different referenced states." self._reference_names = ref_names - self._probabilities = p or [1/len(sub_generators)] * len(sub_generators) + self._probabilities = p or [1 / len(sub_generators)] * len(sub_generators) self._current_episode_length = 0 if type(super_episode_length) in [float, int]: super_episode_length = super_episode_length, super_episode_length + 1 @@ -45,15 +45,29 @@ def set_modules(self, physical_system): super().set_modules(physical_system) for sub_generator in self._sub_generators: sub_generator.set_modules(physical_system) - ref_space_low = np.min([sub_generator.reference_space.low for sub_generator in self._sub_generators], axis=0) - ref_space_high = np.max([sub_generator.reference_space.high for sub_generator in self._sub_generators], axis=0) + ref_space_low = np.min( + [ + sub_generator.reference_space.low + for sub_generator in self._sub_generators + ], + axis=0, + ) + ref_space_high = np.max( + [ + sub_generator.reference_space.high + for sub_generator in self._sub_generators + ], + axis=0, + ) self.reference_space = Box(ref_space_low, ref_space_high, dtype=float) self._referenced_states = self._sub_generators[0].referenced_states for sub_generator in self._sub_generators: - assert np.all(sub_generator.referenced_states == self._referenced_states), \ - 'Reference Generators reference different state variables' - assert sub_generator.reference_space.shape == self.reference_space.shape, \ - 'Reference Generators have differently shaped reference spaces' + assert np.all( + sub_generator.referenced_states == self._referenced_states + ), "Reference Generators reference different state variables" + assert ( + sub_generator.reference_space.shape == self.reference_space.shape + ), "Reference Generators have differently shaped reference spaces" def reset(self, initial_state=None, initial_reference=None): self.next_generator() @@ -78,7 +92,9 @@ def _reset_reference(self): self._super_episode_length[0], self._super_episode_length[1] ) self._k = 0 - self._current_ref_generator = self.random_generator.choice(self._sub_generators, p=self._probabilities) + self._current_ref_generator = self.random_generator.choice( + self._sub_generators, p=self._probabilities + ) def seed(self, seed=None): super().seed(seed) diff --git a/gym_electric_motor/reference_generators/triangle_reference_generator.py b/gym_electric_motor/reference_generators/triangle_reference_generator.py index 2b940742..0aa0fa42 100644 --- a/gym_electric_motor/reference_generators/triangle_reference_generator.py +++ b/gym_electric_motor/reference_generators/triangle_reference_generator.py @@ -10,7 +10,14 @@ class TriangularReferenceGenerator(SubepisodedReferenceGenerator): The reference is generated for a certain length and then new parameters are drawn uniformly from a selectable range. """ - def __init__(self, amplitude_range=None, frequency_range=(1, 10), offset_range=None, *_, **kwargs): + def __init__( + self, + amplitude_range=None, + frequency_range=(1, 10), + offset_range=None, + *_, + **kwargs, + ): """ Args: amplitude_range(tuple(float,float)): Lower and upper limit for the amplitude. @@ -30,23 +37,41 @@ def set_modules(self, physical_system): super().set_modules(physical_system) # but amplitude and offset cannot exceed limit margin self._amplitude_range = np.clip( - self._amplitude_range, 0, (self._limit_margin[1] - self._limit_margin[0]) / 2 + self._amplitude_range, + 0, + (self._limit_margin[1] - self._limit_margin[0]) / 2, + ) + self._offset_range = np.clip( + self._offset_range, self._limit_margin[0], self._limit_margin[1] ) - self._offset_range = np.clip(self._offset_range, self._limit_margin[0], self._limit_margin[1]) def _reset_reference(self): # get absolute values of amplitude, frequency and offset self._amplitude = self._get_current_value(self._amplitude_range) self._frequency = self._get_current_value(self._frequency_range) offset_range = np.clip( - self._offset_range, -self._limit_margin[1] + self._amplitude, self._limit_margin[1] - self._amplitude + self._offset_range, + -self._limit_margin[1] + self._amplitude, + self._limit_margin[1] - self._amplitude, ) self._offset = self._get_current_value(offset_range) - t = np.linspace(0, (self._current_episode_length - 1) * self._physical_system.tau, self._current_episode_length) - phase = self._random_generator.uniform() * 2 * np.pi # note: in the scipy implementation of sawtooth() 1 time-period + t = np.linspace( + 0, + (self._current_episode_length - 1) * self._physical_system.tau, + self._current_episode_length, + ) + phase = ( + self._random_generator.uniform() * 2 * np.pi + ) # note: in the scipy implementation of sawtooth() 1 time-period # corresponds to a phase of 2pi ref_width = self._random_generator.uniform() # a random value between 0,1 that creates asymmetry in the triangular reference # wave ref_width=1 creates a sawtooth waveform - self._reference = self._amplitude * sg.sawtooth(2*np.pi * self._frequency * t + phase, ref_width) + self._offset - self._reference = np.clip(self._reference, self._limit_margin[0], self._limit_margin[1]) + self._reference = ( + self._amplitude + * sg.sawtooth(2 * np.pi * self._frequency * t + phase, ref_width) + + self._offset + ) + self._reference = np.clip( + self._reference, self._limit_margin[0], self._limit_margin[1] + ) diff --git a/gym_electric_motor/reference_generators/wiener_process_reference_generator.py b/gym_electric_motor/reference_generators/wiener_process_reference_generator.py index dc04f227..749afeba 100644 --- a/gym_electric_motor/reference_generators/wiener_process_reference_generator.py +++ b/gym_electric_motor/reference_generators/wiener_process_reference_generator.py @@ -29,7 +29,9 @@ def set_modules(self, physical_system): def _reset_reference(self): self._current_sigma = 10 ** self._get_current_value(np.log10(self._sigma_range)) - random_values = self._random_generator.normal(0, self._current_sigma, self._current_episode_length) + random_values = self._random_generator.normal( + 0, self._current_sigma, self._current_episode_length + ) self._reference = np.zeros_like(random_values) reference_value = self._reference_value for i in range(self._current_episode_length): @@ -43,6 +45,7 @@ def _reset_reference(self): def reset(self, initial_state=None, initial_reference=None): if initial_reference is None: initial_reference = np.zeros_like(self._referenced_states, dtype=float) - initial_reference[self._referenced_states] =\ - self.random_generator.uniform(self._initial_range[0], self._initial_range[1], 1) + initial_reference[self._referenced_states] = self.random_generator.uniform( + self._initial_range[0], self._initial_range[1], 1 + ) return super().reset(initial_state, initial_reference) diff --git a/gym_electric_motor/reference_generators/zero_reference_generator.py b/gym_electric_motor/reference_generators/zero_reference_generator.py index fe10190b..150f311f 100644 --- a/gym_electric_motor/reference_generators/zero_reference_generator.py +++ b/gym_electric_motor/reference_generators/zero_reference_generator.py @@ -14,7 +14,9 @@ def __init__(self): def set_modules(self, physical_system): super().set_modules(physical_system) - self._referenced_states = np.zeros_like(self._physical_system.state_names, dtype=bool) + self._referenced_states = np.zeros_like( + self._physical_system.state_names, dtype=bool + ) def get_reference(self, state=None, *_, **__): return np.zeros_like(self._physical_system.state_names, dtype=float) diff --git a/gym_electric_motor/reward_functions/__init__.py b/gym_electric_motor/reward_functions/__init__.py index c4a749bf..a520040b 100644 --- a/gym_electric_motor/reward_functions/__init__.py +++ b/gym_electric_motor/reward_functions/__init__.py @@ -2,5 +2,4 @@ from ..utils import register_class from .. import RewardFunction -register_class(WeightedSumOfErrors, RewardFunction, 'WSE') - +register_class(WeightedSumOfErrors, RewardFunction, "WSE") diff --git a/gym_electric_motor/reward_functions/weighted_sum_of_errors.py b/gym_electric_motor/reward_functions/weighted_sum_of_errors.py index 24bb0518..810a4395 100644 --- a/gym_electric_motor/reward_functions/weighted_sum_of_errors.py +++ b/gym_electric_motor/reward_functions/weighted_sum_of_errors.py @@ -44,8 +44,15 @@ class WeightedSumOfErrors(RewardFunction): :math:`r_{wse,min}` is the minimal :math:`r_{wse}` (=reward_range[0]) and :math:`\gamma` the agents discount factor. """ - def __init__(self, reward_weights=None, normed_reward_weights=False, violation_reward=None, - gamma=0.9, reward_power=1, bias=0.0): + def __init__( + self, + reward_weights=None, + normed_reward_weights=False, + violation_reward=None, + gamma=0.9, + reward_power=1, + bias=0.0, + ): """ Args: reward_weights(dict/list/ndarray(float)): Dict mapping state names to reward_weights, 0 otherwise. @@ -88,13 +95,13 @@ def set_modules(self, physical_system, reference_generator, constraint_monitor): if np.any(referenced_states): reward_weights = dict.fromkeys( np.array(physical_system.state_names)[referenced_states], - 1 / len(np.array(physical_system.state_names)[referenced_states]) + 1 / len(np.array(physical_system.state_names)[referenced_states]), ) # If no referenced states and no reward weights passed, uniform reward over all states else: reward_weights = dict.fromkeys( np.array(physical_system.state_names), - 1 / len(np.array(physical_system.state_names)) + 1 / len(np.array(physical_system.state_names)), ) else: reward_weights = self._reward_weights @@ -103,20 +110,27 @@ def set_modules(self, physical_system, reference_generator, constraint_monitor): warnings.warn("All reward weights sum up to zero", Warning, stacklevel=2) rw_sum = sum(self._reward_weights) if self._normed: - if self._bias == 'positive': + if self._bias == "positive": self._bias = 1 self._reward_weights = self._reward_weights / rw_sum self.reward_range = (-1 + self._bias, self._bias) else: - if self._bias == 'positive': + if self._bias == "positive": self._bias = rw_sum self.reward_range = (-rw_sum + self._bias, self._bias) if self._violation_reward is None: self._violation_reward = min(self.reward_range[0] / (1.0 - self._gamma), 0) def reward(self, state, reference, k=None, action=None, violation_degree=0.0): - return (1.0 - violation_degree) * self._wse_reward(state, reference) \ - + violation_degree * self._violation_reward + return (1.0 - violation_degree) * self._wse_reward( + state, reference + ) + violation_degree * self._violation_reward def _wse_reward(self, state, reference): - return -np.sum(self._reward_weights * (abs(state - reference) / self._state_length) ** self._n) + self._bias + return ( + -np.sum( + self._reward_weights + * (abs(state - reference) / self._state_length) ** self._n + ) + + self._bias + ) diff --git a/gym_electric_motor/utils.py b/gym_electric_motor/utils.py index 7c907f00..c9dfb8ba 100644 --- a/gym_electric_motor/utils.py +++ b/gym_electric_motor/utils.py @@ -15,11 +15,13 @@ def state_dict_to_state_array(state_dict, state_array, state_names): state_names(list/ndarray(str)): List of the state names. """ state_dict = dict((key.lower(), v) for key, v in state_dict.items()) - assert all(key in state_names for key in state_dict.keys()), f'A state name in {state_dict.keys()} is invalid.' + assert all( + key in state_names for key in state_dict.keys() + ), f"A state name in {state_dict.keys()} is invalid." for ind, key in enumerate(state_names): try: state_array[ind] = state_dict[key] - except KeyError: # TODO + except KeyError: # TODO pass @@ -52,7 +54,7 @@ def set_state_array(input_values, state_names): elif type(input_values) is float or type(input_values) is int: state_array = input_values * np.ones_like(state_names, dtype=float) else: - raise Exception('Incorrect type for the input values.') + raise Exception("Incorrect type for the input values.") return state_array @@ -92,7 +94,7 @@ def instantiate(superclass, instance, **kwargs): elif type(instance) is str: return make_module(superclass, instance, **kwargs) else: - raise Exception('Instantiation Error.') + raise Exception("Instantiation Error.") # Registry dictionary that stores the keys to instantiate the components with the keystrings @@ -114,7 +116,9 @@ def make_module(superclass, keystring, **kwargs): try: return _registry[superclass][keystring](**kwargs) except KeyError: - raise Exception(f'Key {keystring} or baseclass {superclass.__name__} not found in the registry.') + raise Exception( + f"Key {keystring} or baseclass {superclass.__name__} not found in the registry." + ) def register_superclass(superclass): @@ -152,7 +156,9 @@ def update_parameter_dict(source_dict, update_dict, copy=True): source_keys = source_dict.keys() for key in update_dict.keys(): if key not in source_keys: - raise KeyError(f'Cannot update_dict the source_dict. The key "{key}" is not available.') + raise KeyError( + f'Cannot update_dict the source_dict. The key "{key}" is not available.' + ) new_dict = source_dict.copy() if copy else source_dict new_dict.update(update_dict) return new_dict diff --git a/gym_electric_motor/visualization/__init__.py b/gym_electric_motor/visualization/__init__.py index cde95f64..2268475b 100644 --- a/gym_electric_motor/visualization/__init__.py +++ b/gym_electric_motor/visualization/__init__.py @@ -4,5 +4,5 @@ from ..utils import register_class from .. import ElectricMotorVisualization -register_class(ConsolePrinter, ElectricMotorVisualization, 'ConsolePrinter') -register_class(MotorDashboard, ElectricMotorVisualization, 'MotorDashboard') \ No newline at end of file +register_class(ConsolePrinter, ElectricMotorVisualization, "ConsolePrinter") +register_class(MotorDashboard, ElectricMotorVisualization, "MotorDashboard") diff --git a/gym_electric_motor/visualization/console_printer.py b/gym_electric_motor/visualization/console_printer.py index 6226c96b..135baae8 100644 --- a/gym_electric_motor/visualization/console_printer.py +++ b/gym_electric_motor/visualization/console_printer.py @@ -35,7 +35,7 @@ def __init__(self, verbose=0, update_freq=1): self._cum_reward = 0 self._done = False self._episode = 0 - np.set_printoptions(formatter={'float': '{:9.3f}'.format}) + np.set_printoptions(formatter={"float": "{:9.3f}".format}) self._reset = False def on_reset_begin(self): @@ -68,20 +68,22 @@ def render(self): if self._print_freq > 0: if self._reset: print( - f'\nEpisode {self._episode} ', - f'Constraint Violation! ' if self._done else 'External Reset. ', - f'Number of steps: {self._k: 8d} ', - f'Cumulative Reward: {self._cum_reward:7.3f}\n') + f"\nEpisode {self._episode} ", + f"Constraint Violation! " if self._done else "External Reset. ", + f"Number of steps: {self._k: 8d} ", + f"Cumulative Reward: {self._cum_reward:7.3f}\n", + ) self._cum_reward = 0 self._reset = False self._episode += 1 if self._print_freq == 2 and (self._k % self._update_freq) == 0: - print(f'Episode {self._episode} ' - f'Step {self._k: 8d} ' - f'State {self._state * self._limits} ' - f'Reference {self._reference * self._limits} ' - f'Reward {self._reward:7.3f} ' - f'Cumulative Reward {self._cum_reward:7.3f}', - end='\r' - ) + print( + f"Episode {self._episode} " + f"Step {self._k: 8d} " + f"State {self._state * self._limits} " + f"Reference {self._reference * self._limits} " + f"Reward {self._reward:7.3f} " + f"Cumulative Reward {self._cum_reward:7.3f}", + end="\r", + ) diff --git a/gym_electric_motor/visualization/motor_dashboard.py b/gym_electric_motor/visualization/motor_dashboard.py index 8bd65e7f..4012e696 100644 --- a/gym_electric_motor/visualization/motor_dashboard.py +++ b/gym_electric_motor/visualization/motor_dashboard.py @@ -1,5 +1,12 @@ from gym_electric_motor.core import ElectricMotorVisualization -from .motor_dashboard_plots import StatePlot, ActionPlot, RewardPlot, TimePlot, EpisodePlot, StepPlot +from .motor_dashboard_plots import ( + StatePlot, + ActionPlot, + RewardPlot, + TimePlot, + EpisodePlot, + StepPlot, +) import matplotlib.pyplot as plt import gymnasium @@ -32,8 +39,15 @@ def update_interval(self): return self._update_interval def __init__( - self, state_plots=(), action_plots=(), reward_plot=False, additional_plots=(), - update_interval=1000, time_plot_width=10000, style=None, scale_plots = None + self, + state_plots=(), + action_plots=(), + reward_plot=False, + additional_plots=(), + update_interval=1000, + time_plot_width=10000, + style=None, + scale_plots=None, ): """ Args: @@ -54,7 +68,9 @@ def __init__( """ # Basic assertions assert type(reward_plot) is bool - assert all(isinstance(ap, (TimePlot, EpisodePlot, StepPlot)) for ap in additional_plots) + assert all( + isinstance(ap, (TimePlot, EpisodePlot, StepPlot)) for ap in additional_plots + ) assert type(update_interval) in [int, float] assert update_interval > 0 assert type(time_plot_width) in [int, float] @@ -79,8 +95,12 @@ def __init__( self._reward_plot = reward_plot # Separate the additional plots into StepPlots, EpisodicPlots and StepPlots - self._custom_time_plots = [p for p in additional_plots if isinstance(p, TimePlot)] - self._episodic_plots = [p for p in additional_plots if isinstance(p, EpisodePlot)] + self._custom_time_plots = [ + p for p in additional_plots if isinstance(p, TimePlot) + ] + self._episodic_plots = [ + p for p in additional_plots if isinstance(p, EpisodePlot) + ] self._step_plots = [p for p in additional_plots if isinstance(p, StepPlot)] self._time_plots = [] @@ -90,8 +110,7 @@ def __init__( self._k = 0 self._update_render = False - #self._scale_plots = scale_plots - + # self._scale_plots = scale_plots def on_reset_begin(self): """Called before the environment is reset. All subplots are reset.""" @@ -137,8 +156,11 @@ def on_step_end(self, k, state, reference, reward, terminated): def render(self): """Updates the plots every *update cycle* calls of this method.""" - if not (self._time_plot_figure or self._episodic_plot_figure or self._step_plot_figure) \ - and len(self._plots) > 0: + if not ( + self._time_plot_figure + or self._episodic_plot_figure + or self._step_plot_figure + ) and len(self._plots) > 0: self.initialize() if self._update_render: self._update() @@ -152,12 +174,15 @@ def set_env(self, env): env(ElectricMotorEnvironment): The environment. """ state_names = env.physical_system.state_names - if self._state_plots == 'all': + if self._state_plots == "all": self._state_plots = state_names - if self._action_plots == 'all': + if self._action_plots == "all": if type(env.action_space) is gymnasium.spaces.Discrete: self._action_plots = [0] - elif type(env.action_space) in (gymnasium.spaces.Box, gymnasium.spaces.MultiDiscrete): + elif type(env.action_space) in ( + gymnasium.spaces.Box, + gymnasium.spaces.MultiDiscrete, + ): self._action_plots = list(range(env.action_space.shape[0])) self._time_plots = [] @@ -168,8 +193,14 @@ def set_env(self, env): self._time_plots.append(StatePlot(state)) if len(self._action_plots) > 0: - assert type(env.action_space) in (gymnasium.spaces.Box, gymnasium.spaces.Discrete, gymnasium.spaces.MultiDiscrete), \ - f'Action space of type {type(env.action_space)} not supported for plotting.' + assert ( + type(env.action_space) + in ( + gymnasium.spaces.Box, + gymnasium.spaces.Discrete, + gymnasium.spaces.MultiDiscrete, + ) + ), f"Action space of type {type(env.action_space)} not supported for plotting." for action in self._action_plots: ap = ActionPlot(action) self._time_plots.append(ap) @@ -196,7 +227,9 @@ def reset_figures(self): a jupyter notebook and the figures shall be plotted below a new cell.""" for plot in self._plots: plot.reset_data() - self._episodic_plot_figure = self._time_plot_figure = self._step_plot_figure = None + self._episodic_plot_figure = ( + self._time_plot_figure + ) = self._step_plot_figure = None self._figures = [] def initialize(self): @@ -204,7 +237,7 @@ def initialize(self): plt.close() self._figures = [] - if plt.get_backend() in ['nbAgg', 'module://ipympl.backend_nbagg']: + if plt.get_backend() in ["nbAgg", "module://ipympl.backend_nbagg"]: self._initialize_figures_notebook() else: self._initialize_figures_window() @@ -213,30 +246,32 @@ def initialize(self): def _initialize_figures_notebook(self): # Create all plots below each other: First Time then Episode then Step Plots - no_of_plots = len(self._episodic_plots) + len(self._step_plots) + len(self._time_plots) + no_of_plots = ( + len(self._episodic_plots) + len(self._step_plots) + len(self._time_plots) + ) if no_of_plots == 0: return - fig, axes = plt.subplots(no_of_plots, figsize=(8, 2*no_of_plots)) + fig, axes = plt.subplots(no_of_plots, figsize=(8, 2 * no_of_plots)) self._figures = [fig] axes = [axes] if no_of_plots == 1 else axes - time_axes = axes[:len(self._time_plots)] - axes = axes[len(self._time_plots):] + time_axes = axes[: len(self._time_plots)] + axes = axes[len(self._time_plots) :] if len(self._time_plots) > 0: - time_axes[-1].set_xlabel('t/s') + time_axes[-1].set_xlabel("t/s") self._time_plot_figure = fig for plot, axis in zip(self._time_plots, time_axes): plot.initialize(axis) - episode_axes = axes[:len(self._episodic_plots)] - axes = axes[len(self._episodic_plots):] + episode_axes = axes[: len(self._episodic_plots)] + axes = axes[len(self._episodic_plots) :] if len(self._episodic_plots) > 0: - episode_axes[-1].set_xlabel('Episode No') + episode_axes[-1].set_xlabel("Episode No") self._episodic_plot_figure = fig for plot, axis in zip(self._episodic_plots, episode_axes): plot.initialize(axis) step_axes = axes if len(self._step_plots) > 0: - step_axes[-1].set_xlabel('Cumulative Steps') + step_axes[-1].set_xlabel("Cumulative Steps") self._step_plot_figure = fig for plot, axis in zip(self._step_plots, step_axes): plot.initialize(axis) @@ -244,31 +279,37 @@ def _initialize_figures_notebook(self): def _initialize_figures_window(self): # create separate figures for time based, step and episode based plots if len(self._episodic_plots) > 0: - self._episodic_plot_figure, axes_ep = plt.subplots(len(self._episodic_plots), sharex=True) + self._episodic_plot_figure, axes_ep = plt.subplots( + len(self._episodic_plots), sharex=True + ) axes_ep = [axes_ep] if len(self._episodic_plots) == 1 else axes_ep self._episodic_plot_figure.subplots_adjust(wspace=0.0, hspace=0.02) - self._episodic_plot_figure.canvas.manager.set_window_title('Episodic Plots') - axes_ep[-1].set_xlabel('Episode No') + self._episodic_plot_figure.canvas.manager.set_window_title("Episodic Plots") + axes_ep[-1].set_xlabel("Episode No") self._figures.append(self._episodic_plot_figure) for plot, axis in zip(self._episodic_plots, axes_ep): plot.initialize(axis) if len(self._step_plots) > 0: - self._step_plot_figure, axes_int = plt.subplots(len(self._step_plots), sharex=True) + self._step_plot_figure, axes_int = plt.subplots( + len(self._step_plots), sharex=True + ) axes_int = [axes_int] if len(self._step_plots) == 1 else axes_int - self._step_plot_figure.canvas.manager.set_window_title('Step Plots') + self._step_plot_figure.canvas.manager.set_window_title("Step Plots") self._step_plot_figure.subplots_adjust(wspace=0.0, hspace=0.02) - axes_int[-1].set_xlabel('Cumulative Steps') + axes_int[-1].set_xlabel("Cumulative Steps") self._figures.append(self._step_plot_figure) for plot, axis in zip(self._step_plots, axes_int): plot.initialize(axis) if len(self._time_plots) > 0: - self._time_plot_figure, axes_step = plt.subplots(len(self._time_plots), sharex=True) - self._time_plot_figure.canvas.manager.set_window_title('Time Plots') + self._time_plot_figure, axes_step = plt.subplots( + len(self._time_plots), sharex=True + ) + self._time_plot_figure.canvas.manager.set_window_title("Time Plots") axes_step = [axes_step] if len(self._time_plots) == 1 else axes_step self._time_plot_figure.subplots_adjust(wspace=0.0, hspace=0.2) - axes_step[-1].set_xlabel('$t$/s') + axes_step[-1].set_xlabel("$t$/s") self._figures.append(self._time_plot_figure) for plot, axis in zip(self._time_plots, axes_step): plot.initialize(axis) @@ -280,7 +321,6 @@ def _update(self): for plot in self._plots: plot.render() for fig in self._figures: - #fig.align_ylabels() + # fig.align_ylabels() fig.canvas.draw() fig.canvas.flush_events() - diff --git a/gym_electric_motor/visualization/motor_dashboard_plots/action_plot.py b/gym_electric_motor/visualization/motor_dashboard_plots/action_plot.py index c47a9a19..7eab305c 100644 --- a/gym_electric_motor/visualization/motor_dashboard_plots/action_plot.py +++ b/gym_electric_motor/visualization/motor_dashboard_plots/action_plot.py @@ -5,7 +5,7 @@ class ActionPlot(TimePlot): - """ Class to plot the instantaneous actions applied on the environment""" + """Class to plot the instantaneous actions applied on the environment""" def __init__(self, action=0): """ @@ -30,13 +30,15 @@ def __init__(self, action=0): self._action_type = None self._action_line_config = self._default_time_line_cfg.copy() - self._action_line_config['color'] = self._colors[-2] + self._action_line_config["color"] = self._colors[-2] def initialize(self, axis): # Docstring of superclass super().initialize(axis) - self._action_line, = self._axis.plot( - self._x_data, self._action_data, **self._action_line_config, + (self._action_line,) = self._axis.plot( + self._x_data, + self._action_data, + **self._action_line_config, ) self._lines.append(self._action_line) @@ -55,19 +57,19 @@ def set_env(self, env): # check for the type of action space: Discrete or Continuous if type(self._action_space) is Box: # for continuous action space - self._action_type = 'Continuous' + self._action_type = "Continuous" # fetch the action range of continuous type actions self._action_range_min = self._action_space.low[self._action] self._action_range_max = self._action_space.high[self._action] elif type(self._action_space) is Discrete: - self._action_type = 'Discrete' + self._action_type = "Discrete" # lower bound of discrete action = 0 self._action_range_min = 0 # fetch the action range of discrete type actions self._action_range_max = self._action_space.n elif type(self._action_space) is MultiDiscrete: - self._action_type = 'MultiDiscrete' + self._action_type = "MultiDiscrete" # lower bound of discrete action = 0 self._action_range_min = 0 # fetch the action range of discrete type actions @@ -75,7 +77,7 @@ def set_env(self, env): spacing = 0.1 * (self._action_range_max - self._action_range_min) self._y_lim = self._action_range_min - spacing, self._action_range_max + spacing - self._label = f'Action {self._action}' + self._label = f"Action {self._action}" def on_step_begin(self, k, action): # Docstring of superclass @@ -84,7 +86,7 @@ def on_step_begin(self, k, action): self._x_data[idx] = self._t if action is not None: - if self._action_type == 'Discrete': + if self._action_type == "Discrete": self._action_data[idx] = action else: self._action_data[idx] = action[self._action] diff --git a/gym_electric_motor/visualization/motor_dashboard_plots/base_plots.py b/gym_electric_motor/visualization/motor_dashboard_plots/base_plots.py index 3dd20f4d..0254e54b 100644 --- a/gym_electric_motor/visualization/motor_dashboard_plots/base_plots.py +++ b/gym_electric_motor/visualization/motor_dashboard_plots/base_plots.py @@ -28,7 +28,7 @@ def __init__(self): # A list of all lines in the plot self._lines = [] # The y-axis Label - self._label = '' + self._label = "" # The x-axis data (common to all lines) self._x_data = [] # List of all y-axis data of the lines @@ -39,7 +39,7 @@ def __init__(self): self._y_lim = None # All colors of the current matplotlib style. It is recommended to select one of these for plotting the lines. - self._colors = [cycle['color'] for cycle in plt.rcParams['axes.prop_cycle']] + self._colors = [cycle["color"] for cycle in plt.rcParams["axes.prop_cycle"]] def initialize(self, axis): """Initialization of the plot. @@ -103,23 +103,11 @@ class TimePlot(MotorDashboardPlot): """ - _default_time_line_cfg = { - 'linestyle': '', - 'marker': 'o', - 'markersize': 0.75 - } + _default_time_line_cfg = {"linestyle": "", "marker": "o", "markersize": 0.75} - _default_violation_line_cfg = { - 'color': 'red', - 'linewidth': 1, - 'linestyle': '-' - } + _default_violation_line_cfg = {"color": "red", "linewidth": 1, "linestyle": "-"} - _default_reset_line_cfg = { - 'color': 'blue', - 'linewidth': 1, - 'linestyle': '-' - } + _default_reset_line_cfg = {"color": "blue", "linewidth": 1, "linestyle": "-"} @property def data_idx(self): @@ -160,7 +148,9 @@ def reset_data(self): self._t = 0 self._reset_memory = [] self._violation_memory = [] - self._x_data = np.linspace(0, self._x_width * self._tau, self._x_width, endpoint=False) + self._x_data = np.linspace( + 0, self._x_width * self._tau, self._x_width, endpoint=False + ) self._x_lim = (0, self._x_data[-1]) def on_reset_begin(self): @@ -197,13 +187,14 @@ def _scale_x_axis(self): def _scale_y_axis(self): if self._scale_plots_to_data: - y_min = min(np.nanmin(self._y_data[0]), np.nanmin(self._y_data[1])) y_max = max(np.nanmax(self._y_data[0]), np.nanmax(self._y_data[1])) - self._axis.set_ylim(y_min-np.sign(y_min)*0.1*y_min, - y_max+np.sign(y_max)*0.1*y_max) - + self._axis.set_ylim( + y_min - np.sign(y_min) * 0.1 * y_min, + y_max + np.sign(y_max) * 0.1 * y_max, + ) + class EpisodePlot(MotorDashboardPlot): """Base Plot class that all episode based plots .""" @@ -232,7 +223,6 @@ def _scale_x_axis(self): class StepPlot(MotorDashboardPlot): - def __init__(self): super().__init__() self._k = 0 diff --git a/gym_electric_motor/visualization/motor_dashboard_plots/cumulative_constraint_violation_plot.py b/gym_electric_motor/visualization/motor_dashboard_plots/cumulative_constraint_violation_plot.py index 43b51214..4a4a9304 100644 --- a/gym_electric_motor/visualization/motor_dashboard_plots/cumulative_constraint_violation_plot.py +++ b/gym_electric_motor/visualization/motor_dashboard_plots/cumulative_constraint_violation_plot.py @@ -8,7 +8,7 @@ def __init__(self): super().__init__() self._no_of_violations = 0 self._violations = [0] - self._label = 'Cum. No. of Constraint Violations' + self._label = "Cum. No. of Constraint Violations" self._x_data.append(0) def initialize(self, axis): diff --git a/gym_electric_motor/visualization/motor_dashboard_plots/episode_length_plot.py b/gym_electric_motor/visualization/motor_dashboard_plots/episode_length_plot.py index f4068434..a2ed1364 100644 --- a/gym_electric_motor/visualization/motor_dashboard_plots/episode_length_plot.py +++ b/gym_electric_motor/visualization/motor_dashboard_plots/episode_length_plot.py @@ -9,7 +9,7 @@ def __init__(self): # data container for episode lengths self._episode_lengths = [] self._episode_length = 0 - self._label = 'Episode Length' + self._label = "Episode Length" self._axis = None # Flag, that is true, if an episode has ended before the rendering. self._reset = False diff --git a/gym_electric_motor/visualization/motor_dashboard_plots/mean_episode_reward_plot.py b/gym_electric_motor/visualization/motor_dashboard_plots/mean_episode_reward_plot.py index 418258a8..199d2cf7 100644 --- a/gym_electric_motor/visualization/motor_dashboard_plots/mean_episode_reward_plot.py +++ b/gym_electric_motor/visualization/motor_dashboard_plots/mean_episode_reward_plot.py @@ -13,14 +13,16 @@ def __init__(self): self._reward_data = [] self._reward_sum = 0 self._episode_length = 0 - self._label = 'Mean Reward Per Step' + self._label = "Mean Reward Per Step" self._reward_range = [np.inf, -np.inf] def initialize(self, axis): super().initialize(axis) self._reward_data = [] self._y_data.append(self._reward_data) - self._lines.append(self._axis.plot([], self._reward_data, color=self._colors[0])[0]) + self._lines.append( + self._axis.plot([], self._reward_data, color=self._colors[0])[0] + ) def on_step_end(self, k, state, reference, reward, terminated): super().on_step_end(k, state, reference, reward, terminated) @@ -44,6 +46,10 @@ def _set_y_data(self): self._reset = True def _scale_y_axis(self): - if len(self._reward_data) > 1 and self._axis.get_ylim() != tuple(self._reward_range): + if len(self._reward_data) > 1 and self._axis.get_ylim() != tuple( + self._reward_range + ): spacing = 0.1 * (self._reward_range[1] - self._reward_range[0]) - self._axis.set_ylim(self._reward_range[0]-spacing, self._reward_range[1]+spacing) + self._axis.set_ylim( + self._reward_range[0] - spacing, self._reward_range[1] + spacing + ) diff --git a/gym_electric_motor/visualization/motor_dashboard_plots/reward_plot.py b/gym_electric_motor/visualization/motor_dashboard_plots/reward_plot.py index 8a2b1d63..bdd8f857 100644 --- a/gym_electric_motor/visualization/motor_dashboard_plots/reward_plot.py +++ b/gym_electric_motor/visualization/motor_dashboard_plots/reward_plot.py @@ -12,11 +12,13 @@ def __init__(self): self._reward_line = None self._reward_data = None self._reward_line_cfg = self._default_time_line_cfg.copy() - self._reward_line_cfg['color'] = self._colors[-1] + self._reward_line_cfg["color"] = self._colors[-1] def initialize(self, axis): super().initialize(axis) - self._reward_line, = self._axis.plot(self._x_data, self._reward_data, **self._reward_line_cfg) + (self._reward_line,) = self._axis.plot( + self._x_data, self._reward_data, **self._reward_line_cfg + ) self._lines.append(self._reward_line) def set_env(self, env): @@ -28,7 +30,7 @@ def set_env(self, env): max_limit = self._reward_range[1] spacing = 0.1 * (max_limit - min_limit) self._y_lim = (min_limit - spacing, max_limit + spacing) - self._label = 'reward' + self._label = "reward" def reset_data(self): super().reset_data() diff --git a/gym_electric_motor/visualization/motor_dashboard_plots/state_plot.py b/gym_electric_motor/visualization/motor_dashboard_plots/state_plot.py index 83930b04..29debfae 100644 --- a/gym_electric_motor/visualization/motor_dashboard_plots/state_plot.py +++ b/gym_electric_motor/visualization/motor_dashboard_plots/state_plot.py @@ -6,32 +6,28 @@ class StatePlot(TimePlot): """Plot to display the environments states and their references.""" - _default_limit_line_cfg = { - 'color': 'red', - 'linestyle': '--', - 'linewidth': .75 - } + _default_limit_line_cfg = {"color": "red", "linestyle": "--", "linewidth": 0.75} # Labels for each state variable. state_labels = { - 'omega': r'$\omega$/(1/s)', - 'torque': '$T$/Nm', - 'i': '$i$/A', - 'i_a': r'$i_{\mathrm{a}}$/A', - 'i_e': r'$i_{\mathrm{e}}$/A', - 'i_b': r'$i_{\mathrm{b}}$/A', - 'i_c': r'$i_{\mathrm{c}}$/A', - 'i_sq': r'$i_{\mathrm{sq}}$/A', - 'i_sd': r'$i_{\mathrm{sd}}$/A', - 'u': '$u$/V', - 'u_a': r'$u_{\mathrm{a}}$/V', - 'u_b': r'$u_{\mathrm{b}}$/V', - 'u_c': r'$u_{\mathrm{c}}$/V', - 'u_sq': r'$u_{\mathrm{sq}}$/V', - 'u_sd': r'$u_{\mathrm{sd}}$/V', - 'u_e': r'$u_{\mathrm{e}}$/V', - 'u_sup': r'$u_{\mathrm{sup}}$/V', - 'epsilon': r'$\epsilon$/rad' + "omega": r"$\omega$/(1/s)", + "torque": "$T$/Nm", + "i": "$i$/A", + "i_a": r"$i_{\mathrm{a}}$/A", + "i_e": r"$i_{\mathrm{e}}$/A", + "i_b": r"$i_{\mathrm{b}}$/A", + "i_c": r"$i_{\mathrm{c}}$/A", + "i_sq": r"$i_{\mathrm{sq}}$/A", + "i_sd": r"$i_{\mathrm{sd}}$/A", + "u": "$u$/V", + "u_a": r"$u_{\mathrm{a}}$/V", + "u_b": r"$u_{\mathrm{b}}$/V", + "u_c": r"$u_{\mathrm{c}}$/V", + "u_sq": r"$u_{\mathrm{sq}}$/V", + "u_sd": r"$u_{\mathrm{sd}}$/V", + "u_e": r"$u_{\mathrm{e}}$/V", + "u_sup": r"$u_{\mathrm{sup}}$/V", + "epsilon": r"$\epsilon$/rad", } @property @@ -73,8 +69,6 @@ def __init__(self, state): self._scale_plots_to_data = False - - def set_env(self, env): # Docstring of superclass super().set_env(env) @@ -84,15 +78,26 @@ def set_env(self, env): self._state_idx = ps.state_positions[self._state] # The maximal values of the state. self._limits = ps.limits[self._state_idx] - self._state_space = ps.state_space.low[self._state_idx], ps.state_space.high[self._state_idx] + self._state_space = ( + ps.state_space.low[self._state_idx], + ps.state_space.high[self._state_idx], + ) # Bool: if the state is referenced. self._referenced = rg.referenced_states[self._state_idx] # Bool: if the data is already normalized to an interval of [-1, 1] self._normalized = self._limits != self._state_space[1] self.reset_data() - min_limit = self._limits * self._state_space[0] if self._normalized else self._state_space[0] - max_limit = self._limits * self._state_space[1] if self._normalized else self._state_space[1] + min_limit = ( + self._limits * self._state_space[0] + if self._normalized + else self._state_space[0] + ) + max_limit = ( + self._limits * self._state_space[1] + if self._normalized + else self._state_space[1] + ) spacing = 0.1 * (max_limit - min_limit) # Set the y-axis limits to fixed initital values @@ -103,8 +108,6 @@ def set_env(self, env): self._scale_plots_to_data = env.scale_plots - - def reset_data(self): super().reset_data() # Initialize the data containers @@ -116,33 +119,52 @@ def initialize(self, axis): super().initialize(axis) # Line to plot the state data - self._state_line, = self._axis.plot(self._x_data, self._state_data, **self._state_line_config, zorder=2) + (self._state_line,) = self._axis.plot( + self._x_data, self._state_data, **self._state_line_config, zorder=2 + ) self._lines = [self._state_line] # If the state is referenced plot also the reference line if self._referenced: - self._reference_line, = self._axis.plot(self._x_data, self._ref_data, **self._ref_line_config, zorder=1) + (self._reference_line,) = self._axis.plot( + self._x_data, self._ref_data, **self._ref_line_config, zorder=1 + ) self._lines.append(self._reference_line) - min_limit = self._limits * self._state_space[0] if self._normalized else self._state_space[0] - max_limit = self._limits * self._state_space[1] if self._normalized else self._state_space[1] + min_limit = ( + self._limits * self._state_space[0] + if self._normalized + else self._state_space[0] + ) + max_limit = ( + self._limits * self._state_space[1] + if self._normalized + else self._state_space[1] + ) if self._state_space[0] < 0: self._axis.axhline(min_limit, **self._limit_line_config) lim = self._axis.axhline(max_limit, **self._limit_line_config) y_label = self._label - unit_split = y_label.find('/') + unit_split = y_label.find("/") if unit_split == -1: unit_split = len(y_label) - limit_label = y_label[:unit_split] + r'$_{\mathrm{max}}$' + y_label[unit_split:] + limit_label = y_label[:unit_split] + r"$_{\mathrm{max}}$" + y_label[unit_split:] if self._referenced: - ref_label = y_label[:unit_split] + r'$^*$' + y_label[unit_split:] + ref_label = y_label[:unit_split] + r"$^*$" + y_label[unit_split:] self._axis.legend( - (self._state_line, self._reference_line, lim), (y_label, ref_label, limit_label), loc='upper left', - numpoints=20 + (self._state_line, self._reference_line, lim), + (y_label, ref_label, limit_label), + loc="upper left", + numpoints=20, ) else: - self._axis.legend((self._state_line, lim), (y_label, limit_label), loc='upper left', numpoints=20) + self._axis.legend( + (self._state_line, lim), + (y_label, limit_label), + loc="upper left", + numpoints=20, + ) self._y_data = [self._state_data, self._ref_data] diff --git a/tests/conf.py b/tests/conf.py index d50214e0..a84597a3 100644 --- a/tests/conf.py +++ b/tests/conf.py @@ -7,140 +7,355 @@ # region parameter definition - u_sup = 450.0 -series_motor_parameter = {'motor_parameter': {'r_a': 3.78, 'r_e': 35, 'l_a': 6.3e-3, 'l_e': 160e-3, 'l_e_prime': 0.95, - 'j_rotor': 0.017, 'u_sup': u_sup}, - 'limit_values': {'omega': 400, 'torque': 50, 'i': 75, 'u': 430}, - 'nominal_values': {'omega': 370, 'torque': 40, 'i': 50, 'u': 430}, - 'reward_weights': {'omega': 1, 'torque': 0, 'i': 0, 'u': 0, 'u_sup': 0}} -series_state_positions = {'omega': 0, 'torque': 1, 'i': 2, 'u': 3, 'u_sup': 4} +series_motor_parameter = { + "motor_parameter": { + "r_a": 3.78, + "r_e": 35, + "l_a": 6.3e-3, + "l_e": 160e-3, + "l_e_prime": 0.95, + "j_rotor": 0.017, + "u_sup": u_sup, + }, + "limit_values": {"omega": 400, "torque": 50, "i": 75, "u": 430}, + "nominal_values": {"omega": 370, "torque": 40, "i": 50, "u": 430}, + "reward_weights": {"omega": 1, "torque": 0, "i": 0, "u": 0, "u_sup": 0}, +} +series_state_positions = {"omega": 0, "torque": 1, "i": 2, "u": 3, "u_sup": 4} series_state_space = Box(low=-1, high=1, shape=(5,), dtype=np.float64) -series_initializer = {'states': {'i': 0.0}, - 'interval': None, - 'random_init': None, - 'random_params': (None, None)} - -shunt_motor_parameter = {'motor_parameter': {'r_a': 3.78, 'r_e': 35, 'l_a': 6.3e-3, 'l_e': 160e-3, 'l_e_prime': 0.95, - 'j_rotor': 0.017, 'u_sup': u_sup}, - 'limit_values': {'omega': 400, 'torque': 50, 'i_a': 75, 'i_e': 15, 'u': 430}, - 'nominal_values': {'omega': 368, 'torque': 40, 'i_a': 50, 'i_e': 5, 'u': 430}, - 'reward_weights': {'omega': 1, 'torque': 0, 'i_a': 0, 'i_e': 0, 'u': 0, 'u_sup': 0}} -shunt_state_positions = {'omega': 0, 'torque': 1, 'i_a': 2, 'i_e': 3, 'u': 4, 'u_sup': 5} +series_initializer = { + "states": {"i": 0.0}, + "interval": None, + "random_init": None, + "random_params": (None, None), +} + +shunt_motor_parameter = { + "motor_parameter": { + "r_a": 3.78, + "r_e": 35, + "l_a": 6.3e-3, + "l_e": 160e-3, + "l_e_prime": 0.95, + "j_rotor": 0.017, + "u_sup": u_sup, + }, + "limit_values": {"omega": 400, "torque": 50, "i_a": 75, "i_e": 15, "u": 430}, + "nominal_values": {"omega": 368, "torque": 40, "i_a": 50, "i_e": 5, "u": 430}, + "reward_weights": {"omega": 1, "torque": 0, "i_a": 0, "i_e": 0, "u": 0, "u_sup": 0}, +} +shunt_state_positions = { + "omega": 0, + "torque": 1, + "i_a": 2, + "i_e": 3, + "u": 4, + "u_sup": 5, +} shunt_state_space = Box(low=-1, high=1, shape=(6,), dtype=np.float64) -shunt_initializer = {'states': {'i_a': 0.0, 'i_e': 0.0}, - 'interval': None, - 'random_init': None, - 'random_params': (None, None)} - -extex_motor_parameter = {'motor_parameter': {'r_a': 3.78, 'r_e': 35, 'l_a': 6.3e-3, 'l_e': 160e-3, 'l_e_prime': 0.95, - 'j_rotor': 0.017, 'u_sup': u_sup}, - 'limit_values': {'omega': 400, 'torque': 50, 'i_a': 75, 'i_e': 15, 'u_a': 460, 'u_e': 460, - 'u': 460}, - 'nominal_values': {'omega': 368, 'torque': 40, 'i_a': 50, 'i_e': 20, 'u_a': 460, 'u_e': 460, - 'u': 460}, - 'reward_weights': {'omega': 1, 'torque': 0, 'i_a': 0, 'i_e': 0, 'u_a': 0, 'u_e': 0, - 'u_sup': 0}} -extex_state_positions = {'omega': 0, 'torque': 1, 'i_a': 2, 'i_e': 3, 'u_a': 4, 'u_e': 5, 'u_sup': 6} +shunt_initializer = { + "states": {"i_a": 0.0, "i_e": 0.0}, + "interval": None, + "random_init": None, + "random_params": (None, None), +} + +extex_motor_parameter = { + "motor_parameter": { + "r_a": 3.78, + "r_e": 35, + "l_a": 6.3e-3, + "l_e": 160e-3, + "l_e_prime": 0.95, + "j_rotor": 0.017, + "u_sup": u_sup, + }, + "limit_values": { + "omega": 400, + "torque": 50, + "i_a": 75, + "i_e": 15, + "u_a": 460, + "u_e": 460, + "u": 460, + }, + "nominal_values": { + "omega": 368, + "torque": 40, + "i_a": 50, + "i_e": 20, + "u_a": 460, + "u_e": 460, + "u": 460, + }, + "reward_weights": { + "omega": 1, + "torque": 0, + "i_a": 0, + "i_e": 0, + "u_a": 0, + "u_e": 0, + "u_sup": 0, + }, +} +extex_state_positions = { + "omega": 0, + "torque": 1, + "i_a": 2, + "i_e": 3, + "u_a": 4, + "u_e": 5, + "u_sup": 6, +} extex_state_space = Box(low=-1, high=1, shape=(7,), dtype=np.float64) -extex_initializer = {'states': {'i_a': 0.0, 'i_e': 0.0}, - 'interval': None, - 'random_init': None, - 'random_params': (None, None)} - - -permex_motor_parameter = {'motor_parameter': {'r_a': 3.78, 'l_a': 6.3e-3, 'psi_e': 160e-3, 'j_rotor': 0.017, - 'u_sup': u_sup}, - 'limit_values': {'omega': 400, 'torque': 50, 'i': 75, 'u': 460}, - 'nominal_values': {'omega': 368, 'torque': 40, 'i': 50, 'u': 460}, - 'reward_weights': {'omega': 1, 'torque': 0, 'i': 0, 'u': 0, 'u_sup': 0}} -permex_state_positions = {'omega': 0, 'torque': 1, 'i': 2, 'u': 3, 'u_sup': 4} +extex_initializer = { + "states": {"i_a": 0.0, "i_e": 0.0}, + "interval": None, + "random_init": None, + "random_params": (None, None), +} + + +permex_motor_parameter = { + "motor_parameter": { + "r_a": 3.78, + "l_a": 6.3e-3, + "psi_e": 160e-3, + "j_rotor": 0.017, + "u_sup": u_sup, + }, + "limit_values": {"omega": 400, "torque": 50, "i": 75, "u": 460}, + "nominal_values": {"omega": 368, "torque": 40, "i": 50, "u": 460}, + "reward_weights": {"omega": 1, "torque": 0, "i": 0, "u": 0, "u_sup": 0}, +} +permex_state_positions = {"omega": 0, "torque": 1, "i": 2, "u": 3, "u_sup": 4} permex_state_space = Box(low=-1, high=1, shape=(5,), dtype=np.float64) -permex_initializer = {'states': {'i': 0.0}, - 'interval': None, - 'random_init': None, - 'random_params': (None, None)} - -pmsm_motor_parameter = {'motor_parameter': {'p': 3, 'l_d': 84e-3, 'l_q': 125e-3, 'j_rotor': 2.61e-3, 'r_s': 5.0, - 'psi_p': 0.171, 'u_sup': u_sup}, - 'limit_values': dict(omega=75, torque=65, i=25, epsilon=math.pi, u=450), - 'nominal_values': dict(omega=65, torque=50, i=20, epsilon=math.pi, u=450), - 'reward_weights': dict(omega=1, torque=0, i_a=0, i_b=0, i_c=0, u_a=0, u_b=0, u_c=0, epsilon=0, - u_sup=0)} -pmsm_state_positions = {'omega': 0, 'torque': 1, 'i_a': 2, 'i_b': 3, 'i_c': 4, - 'i_sq': 5, 'i_sd': 6, 'u_a': 7, 'u_b': 8, 'u_c': 9, - 'u_sq': 10, 'u_sd': 11, 'epsilon': 12, 'u_sup': 13} +permex_initializer = { + "states": {"i": 0.0}, + "interval": None, + "random_init": None, + "random_params": (None, None), +} + +pmsm_motor_parameter = { + "motor_parameter": { + "p": 3, + "l_d": 84e-3, + "l_q": 125e-3, + "j_rotor": 2.61e-3, + "r_s": 5.0, + "psi_p": 0.171, + "u_sup": u_sup, + }, + "limit_values": dict(omega=75, torque=65, i=25, epsilon=math.pi, u=450), + "nominal_values": dict(omega=65, torque=50, i=20, epsilon=math.pi, u=450), + "reward_weights": dict( + omega=1, torque=0, i_a=0, i_b=0, i_c=0, u_a=0, u_b=0, u_c=0, epsilon=0, u_sup=0 + ), +} +pmsm_state_positions = { + "omega": 0, + "torque": 1, + "i_a": 2, + "i_b": 3, + "i_c": 4, + "i_sq": 5, + "i_sd": 6, + "u_a": 7, + "u_b": 8, + "u_c": 9, + "u_sq": 10, + "u_sd": 11, + "epsilon": 12, + "u_sup": 13, +} pmsm_state_space = Box(low=-1, high=1, shape=(14,), dtype=np.float64) -pmsm_initializer = {'states': {'i_sq': 0.0, 'i_sd': 0.0, 'epsilon': 0.0}, - 'interval': None, - 'random_init': None, - 'random_params': (None, None)} +pmsm_initializer = { + "states": {"i_sq": 0.0, "i_sd": 0.0, "epsilon": 0.0}, + "interval": None, + "random_init": None, + "random_params": (None, None), +} synrm_motor_parameter = { - 'motor_parameter': {'p': 3, 'l_d': 70e-3, 'l_q': 8e-3, 'j_rotor': 3e-3, 'r_s': 0.5, 'u_sup': u_sup}, - 'nominal_values': {'i': 60, 'torque': 65, 'omega': 450.0, 'epsilon': np.pi, 'u': 450}, - 'limit_values': {'i': 75, 'torque': 75, 'omega': 550.0, 'epsilon': np.pi, 'u': 450}, - 'reward_weights': dict(omega=1, torque=0, i_a=0, i_b=0, i_c=0, u_a=0, u_b=0, u_c=0, epsilon=0, u_sup=0)} -synrm_state_positions = {'omega': 0, 'torque': 1, 'i_a': 2, 'i_b': 3, 'i_c': 4, - 'i_sq': 5, 'i_sd': 6, 'u_a': 7, 'u_b': 8, 'u_c': 9, - 'u_sq': 10, 'u_sd': 11, 'epsilon': 12, 'u_sup': 13} + "motor_parameter": { + "p": 3, + "l_d": 70e-3, + "l_q": 8e-3, + "j_rotor": 3e-3, + "r_s": 0.5, + "u_sup": u_sup, + }, + "nominal_values": { + "i": 60, + "torque": 65, + "omega": 450.0, + "epsilon": np.pi, + "u": 450, + }, + "limit_values": {"i": 75, "torque": 75, "omega": 550.0, "epsilon": np.pi, "u": 450}, + "reward_weights": dict( + omega=1, torque=0, i_a=0, i_b=0, i_c=0, u_a=0, u_b=0, u_c=0, epsilon=0, u_sup=0 + ), +} +synrm_state_positions = { + "omega": 0, + "torque": 1, + "i_a": 2, + "i_b": 3, + "i_c": 4, + "i_sq": 5, + "i_sd": 6, + "u_a": 7, + "u_b": 8, + "u_c": 9, + "u_sq": 10, + "u_sd": 11, + "epsilon": 12, + "u_sup": 13, +} synrm_state_space = Box(low=-1, high=1, shape=(14,), dtype=np.float64) -synrm_initializer = {'states': {'i_sq': 0.0, 'i_sd': 0.0, 'epsilon': 0.0}, - 'interval': None, - 'random_init': None, - 'random_params': (None, None)} +synrm_initializer = { + "states": {"i_sq": 0.0, "i_sd": 0.0, "epsilon": 0.0}, + "interval": None, + "random_init": None, + "random_params": (None, None), +} sci_motor_parameter = { - 'motor_parameter': {'p': 2, 'l_m': 140e-3, 'l_sigs': 5e-3, 'l_sigr': 5e-3, 'j_rotor': 0.001, 'r_s': 3, 'r_r': 1.5, 'u_sup': u_sup}, - 'nominal_values': {'i': 3.9, 'torque': 4.7, 'omega': 314., 'epsilon': np.pi, 'u': 560}, - 'limit_values': {'i': 5.5, 'torque': 6, 'omega': 350.0, 'epsilon': np.pi, 'u': 560}, - 'reward_weights': dict(omega=1, torque=0, i_sa=0, i_sb=0, i_sc=0, u_sa=0, u_sb=0, u_sc=0, epsilon=0, u_sup=0)} -sci_state_positions = {'omega': 0, 'torque': 1, 'i_sa': 2, 'i_sb': 3, 'i_sc': 4, - 'i_sq': 5, 'i_sd': 6, 'u_sa': 7, 'u_sb': 8, 'u_sc': 9, - 'u_sq': 10, 'u_sd': 11, 'epsilon': 12, 'u_sup': 13} + "motor_parameter": { + "p": 2, + "l_m": 140e-3, + "l_sigs": 5e-3, + "l_sigr": 5e-3, + "j_rotor": 0.001, + "r_s": 3, + "r_r": 1.5, + "u_sup": u_sup, + }, + "nominal_values": { + "i": 3.9, + "torque": 4.7, + "omega": 314.0, + "epsilon": np.pi, + "u": 560, + }, + "limit_values": {"i": 5.5, "torque": 6, "omega": 350.0, "epsilon": np.pi, "u": 560}, + "reward_weights": dict( + omega=1, + torque=0, + i_sa=0, + i_sb=0, + i_sc=0, + u_sa=0, + u_sb=0, + u_sc=0, + epsilon=0, + u_sup=0, + ), +} +sci_state_positions = { + "omega": 0, + "torque": 1, + "i_sa": 2, + "i_sb": 3, + "i_sc": 4, + "i_sq": 5, + "i_sd": 6, + "u_sa": 7, + "u_sb": 8, + "u_sc": 9, + "u_sq": 10, + "u_sd": 11, + "epsilon": 12, + "u_sup": 13, +} sci_state_space = Box(low=-1, high=1, shape=(14,), dtype=np.float64) -sci_initializer = {'states': {'i_salpha': 0.0, 'i_sbeta': 0.0, - 'psi_ralpha': 0.0, 'psi_rbeta': 0.0, - 'epsilon': 0.0}, - 'interval': None, - 'random_init': None, - 'random_params': (None, None)} - -dfim_state_positions = {'omega': 0, 'torque': 1, 'i_sa': 2, 'i_sb': 3, 'i_sc': 4, - 'i_sq': 5, 'i_sd': 6, 'i_ra': 7, 'i_rb': 8, 'i_rc': 9, - 'i_rq': 10, 'i_rd': 11, 'u_sa': 12, 'u_sb': 13, 'u_sc': 14, - 'u_sq': 15, 'u_sd': 16, 'u_ra': 17, 'u_rb': 18, 'u_rc': 19, - 'u_rq': 20, 'u_rd': 21, 'epsilon': 22, 'u_sup': 23} +sci_initializer = { + "states": { + "i_salpha": 0.0, + "i_sbeta": 0.0, + "psi_ralpha": 0.0, + "psi_rbeta": 0.0, + "epsilon": 0.0, + }, + "interval": None, + "random_init": None, + "random_params": (None, None), +} + +dfim_state_positions = { + "omega": 0, + "torque": 1, + "i_sa": 2, + "i_sb": 3, + "i_sc": 4, + "i_sq": 5, + "i_sd": 6, + "i_ra": 7, + "i_rb": 8, + "i_rc": 9, + "i_rq": 10, + "i_rd": 11, + "u_sa": 12, + "u_sb": 13, + "u_sc": 14, + "u_sq": 15, + "u_sd": 16, + "u_ra": 17, + "u_rb": 18, + "u_rc": 19, + "u_rq": 20, + "u_rd": 21, + "epsilon": 22, + "u_sup": 23, +} dfim_state_space = Box(low=-1, high=1, shape=(24,), dtype=np.float64) -dfim_initializer = {'states': {'i_salpha': 0.0, 'i_sbeta': 0.0, - 'psi_ralpha': 0.0, 'psi_rbeta': 0.0, - 'epsilon': 0.0}, - 'interval': None, - 'random_init': None, - 'random_params': (None, None)} - -load_parameter = {'j_load': 0.2, 'state_names': ['omega'], 'j_rot_load': 0.25, 'omega_range': (0, 1), - 'parameter': dict(a=0.12, b=0.13, c=0.4, j_load=0.2)} - -converter_parameter = {'tau': 2E-4, 'interlocking_time': 1E-6} - -test_motor_parameter = {'DcSeries': series_motor_parameter, - 'DcShunt': shunt_motor_parameter, - 'DcPermEx': permex_motor_parameter, - 'DcExtEx': extex_motor_parameter, - 'PMSM': pmsm_motor_parameter, - 'SynRM': synrm_motor_parameter, - 'SCIM': sci_motor_parameter, - 'DFIM': sci_motor_parameter,} - -test_motor_initializer = {'DcSeries': series_initializer, - 'DcShunt': shunt_initializer, - 'DcPermEx': permex_initializer, - 'DcExtEx': extex_initializer, - 'PMSM': pmsm_initializer, - 'SynRM': synrm_initializer, - 'SCIM': sci_initializer, - 'DFIM': dfim_initializer,} +dfim_initializer = { + "states": { + "i_salpha": 0.0, + "i_sbeta": 0.0, + "psi_ralpha": 0.0, + "psi_rbeta": 0.0, + "epsilon": 0.0, + }, + "interval": None, + "random_init": None, + "random_params": (None, None), +} + +load_parameter = { + "j_load": 0.2, + "state_names": ["omega"], + "j_rot_load": 0.25, + "omega_range": (0, 1), + "parameter": dict(a=0.12, b=0.13, c=0.4, j_load=0.2), +} + +converter_parameter = {"tau": 2e-4, "interlocking_time": 1e-6} + +test_motor_parameter = { + "DcSeries": series_motor_parameter, + "DcShunt": shunt_motor_parameter, + "DcPermEx": permex_motor_parameter, + "DcExtEx": extex_motor_parameter, + "PMSM": pmsm_motor_parameter, + "SynRM": synrm_motor_parameter, + "SCIM": sci_motor_parameter, + "DFIM": sci_motor_parameter, +} + +test_motor_initializer = { + "DcSeries": series_initializer, + "DcShunt": shunt_initializer, + "DcPermEx": permex_initializer, + "DcExtEx": extex_initializer, + "PMSM": pmsm_initializer, + "SynRM": synrm_initializer, + "SCIM": sci_initializer, + "DFIM": dfim_initializer, +} # endregion @@ -148,6 +363,7 @@ # region render window turn off # use the following function, that no window is shown while testing if env.render() is called + def monkey_ion_function(): """ function used for plt.ion() @@ -165,7 +381,6 @@ def monkey_pause_function(time=None): pass - def monkey_show_function(args=None): """ function used instead of self._figure.show() @@ -175,7 +390,7 @@ def monkey_show_function(args=None): pass -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def turn_off_windows(monkeypatch): """ This preparation function is run before each test. Due to this, no rendering is performed. @@ -210,8 +425,12 @@ def system(t, state, u): """ x = state[0] y = state[1] - result = np.array([3 * x + 5 * y - 2 * x * y + 3 * x ** 2 - 0.5 * y ** 2, - 10 - 0.6 * x + 0.9 * y ** 2 - 3 * x ** 2 * y + u]) + result = np.array( + [ + 3 * x + 5 * y - 2 * x * y + 3 * x**2 - 0.5 * y**2, + 10 - 0.6 * x + 0.9 * y**2 - 3 * x**2 * y + u, + ] + ) return result @@ -225,9 +444,10 @@ def jacobian(t, state, u): """ x = state[0] y = state[1] - result = np.array([[3 - 2 * y + 6 * x, 5 - 2 * x - y], [-0.6 - 6 * x * y, 1.8 * y - 3 * x ** 2]]) + result = np.array( + [[3 - 2 * y + 6 * x, 5 - 2 * x - y], [-0.6 - 6 * x * y, 1.8 * y - 3 * x**2]] + ) return result # endregion - diff --git a/tests/integration_tests/test_environment_execution.py b/tests/integration_tests/test_environment_execution.py index f9e2b267..0f446c88 100644 --- a/tests/integration_tests/test_environment_execution.py +++ b/tests/integration_tests/test_environment_execution.py @@ -2,19 +2,29 @@ import gym_electric_motor as gem import numpy as np -control_tasks = ['TC', 'SC', 'CC'] -action_types = ['Cont', 'Finite'] -motors = ['SeriesDc', 'PermExDc', 'ExtExDc', 'ShuntDc', 'PMSM', 'EESM', 'SynRM', 'DFIM', 'SCIM'] -versions = ['v0'] +control_tasks = ["TC", "SC", "CC"] +action_types = ["Cont", "Finite"] +motors = [ + "SeriesDc", + "PermExDc", + "ExtExDc", + "ShuntDc", + "PMSM", + "EESM", + "SynRM", + "DFIM", + "SCIM", +] +versions = ["v0"] -@pytest.mark.parametrize('no_of_steps', [100]) -@pytest.mark.parametrize('version', versions) -@pytest.mark.parametrize('dc_motor', motors) -@pytest.mark.parametrize('control_task', control_tasks) -@pytest.mark.parametrize('action_type', action_types) +@pytest.mark.parametrize("no_of_steps", [100]) +@pytest.mark.parametrize("version", versions) +@pytest.mark.parametrize("dc_motor", motors) +@pytest.mark.parametrize("control_task", control_tasks) +@pytest.mark.parametrize("action_type", action_types) def test_execution(dc_motor, control_task, action_type, version, no_of_steps): - env_id = f'{action_type}-{control_task}-{dc_motor}-{version}' + env_id = f"{action_type}-{control_task}-{dc_motor}-{version}" env = gem.make(env_id) terminated = True for i in range(no_of_steps): @@ -24,13 +34,24 @@ def test_execution(dc_motor, control_task, action_type, version, no_of_steps): action = env.action_space.sample() assert action in env.action_space observation, reward, terminated, truncated, info = env.step(action) - assert not np.any(np.isnan(observation[0])), 'An invalid nan-value is in the state.' - assert not np.any(np.isnan(observation[1])), 'An invalid nan-value is in the reference.' + assert not np.any( + np.isnan(observation[0]) + ), "An invalid nan-value is in the state." + assert not np.any( + np.isnan(observation[1]) + ), "An invalid nan-value is in the reference." assert info == {} - assert type(reward) in [float, np.float64, np.float32], 'The Reward is not a scalar floating point value.' - assert not np.isnan(reward), 'Invalid nan-value as reward.' + assert type(reward) in [ + float, + np.float64, + np.float32, + ], "The Reward is not a scalar floating point value." + assert not np.isnan(reward), "Invalid nan-value as reward." # Only the shape is monitored here. The states and references may lay slightly outside of the specified space. # This happens if limits are violated or if some states are not observed to lay within their limits. - assert observation[0].shape == env.observation_space[0].shape, 'The shape of the state is incorrect.' - assert observation[1].shape == env.observation_space[1].shape, 'The shape of the reference is incorrect.' - + assert ( + observation[0].shape == env.observation_space[0].shape + ), "The shape of the state is incorrect." + assert ( + observation[1].shape == env.observation_space[1].shape + ), "The shape of the reference is incorrect." diff --git a/tests/integration_tests/test_environment_seeding.py b/tests/integration_tests/test_environment_seeding.py index c036d437..9332c2dd 100644 --- a/tests/integration_tests/test_environment_seeding.py +++ b/tests/integration_tests/test_environment_seeding.py @@ -2,22 +2,34 @@ import gym_electric_motor as gem import numpy as np -control_tasks = ['TC', 'SC', 'CC'] -action_types = ['Cont', 'Finite'] -motors = ['SeriesDc', 'PermExDc', 'ExtExDc', 'ShuntDc', 'PMSM', 'EESM', 'SynRM', 'DFIM', 'SCIM'] -versions = ['v0'] +control_tasks = ["TC", "SC", "CC"] +action_types = ["Cont", "Finite"] +motors = [ + "SeriesDc", + "PermExDc", + "ExtExDc", + "ShuntDc", + "PMSM", + "EESM", + "SynRM", + "DFIM", + "SCIM", +] +versions = ["v0"] seeds = [123, 456, 789] -@pytest.mark.parametrize('no_of_steps', [100]) -@pytest.mark.parametrize('version', versions) -@pytest.mark.parametrize('dc_motor', motors) -@pytest.mark.parametrize('control_task', control_tasks) -@pytest.mark.parametrize('action_type', action_types) -@pytest.mark.parametrize('seed', seeds) -def test_seeding_same_env(dc_motor, control_task, action_type, version, no_of_steps, seed): +@pytest.mark.parametrize("no_of_steps", [100]) +@pytest.mark.parametrize("version", versions) +@pytest.mark.parametrize("dc_motor", motors) +@pytest.mark.parametrize("control_task", control_tasks) +@pytest.mark.parametrize("action_type", action_types) +@pytest.mark.parametrize("seed", seeds) +def test_seeding_same_env( + dc_motor, control_task, action_type, version, no_of_steps, seed +): """This test assures that an environment that is seeded two times with the same seed generates the same episodes.""" - env_id = f'{action_type}-{control_task}-{dc_motor}-{version}' + env_id = f"{action_type}-{control_task}-{dc_motor}-{version}" env = gem.make(env_id) # Sample actions that are used in both executions actions = [env.action_space.sample() for _ in range(no_of_steps)] @@ -54,21 +66,23 @@ def test_seeding_same_env(dc_motor, control_task, action_type, version, no_of_st # Assure that the epsiodes of the initially and reseeded environment are equal references1 = np.array(references1).flatten() references2 = np.array(references2).flatten() - assert(np.all(np.array(states1) == np.array(states2))) - assert(np.all(np.array(references1).flatten() == np.array(references2).flatten())) - assert(np.all(np.array(rewards1) == np.array(rewards2))) - assert (np.all(np.array(terminated1) == np.array(terminated2))) + assert np.all(np.array(states1) == np.array(states2)) + assert np.all(np.array(references1).flatten() == np.array(references2).flatten()) + assert np.all(np.array(rewards1) == np.array(rewards2)) + assert np.all(np.array(terminated1) == np.array(terminated2)) -@pytest.mark.parametrize('no_of_steps', [100]) -@pytest.mark.parametrize('version', versions) -@pytest.mark.parametrize('dc_motor', motors) -@pytest.mark.parametrize('control_task', control_tasks) -@pytest.mark.parametrize('action_type', action_types) -@pytest.mark.parametrize('seed', seeds) -def test_seeding_new_env(dc_motor, control_task, action_type, version, no_of_steps, seed): +@pytest.mark.parametrize("no_of_steps", [100]) +@pytest.mark.parametrize("version", versions) +@pytest.mark.parametrize("dc_motor", motors) +@pytest.mark.parametrize("control_task", control_tasks) +@pytest.mark.parametrize("action_type", action_types) +@pytest.mark.parametrize("seed", seeds) +def test_seeding_new_env( + dc_motor, control_task, action_type, version, no_of_steps, seed +): """This test assures that two equal environments that are seeded with the same seed generate the same episodes.""" - env_id = f'{action_type}-{control_task}-{dc_motor}-{version}' + env_id = f"{action_type}-{control_task}-{dc_motor}-{version}" env = gem.make(env_id) actions = [env.action_space.sample() for _ in range(no_of_steps)] terminated = True @@ -106,7 +120,7 @@ def test_seeding_new_env(dc_motor, control_task, action_type, version, no_of_ste terminated2.append(terminated) # Assure that the episodes of both environments are equal - assert (np.all(np.array(states1) == np.array(states2))) - assert (np.all(np.array(references1).flatten() == np.array(references2).flatten())) - assert (np.all(np.array(rewards1) == np.array(rewards2))) - assert (np.all(np.array(terminated1) == np.array(terminated2))) + assert np.all(np.array(states1) == np.array(states2)) + assert np.all(np.array(references1).flatten() == np.array(references2).flatten()) + assert np.all(np.array(rewards1) == np.array(rewards2)) + assert np.all(np.array(terminated1) == np.array(terminated2)) diff --git a/tests/integration_tests/test_integration.py b/tests/integration_tests/test_integration.py index 417aa501..80863bb0 100644 --- a/tests/integration_tests/test_integration.py +++ b/tests/integration_tests/test_integration.py @@ -1,10 +1,12 @@ # Following lines of code are needed to be abled to succesfully execute the import in line 7 import sys import os -path = os.getcwd()+'/examples/classic_controllers' + +path = os.getcwd() + "/examples/classic_controllers" sys.path.append(path) from classic_controllers import Controller -#import pytest + +# import pytest import gym_electric_motor as gem from gym_electric_motor.reference_generators import SinusoidalReferenceGenerator @@ -13,27 +15,26 @@ import numpy as np -def simulate_env(seed = None): +def simulate_env(seed=None): + motor_type = "PermExDc" + control_type = "SC" + action_type = "Cont" + version = "v0" - motor_type = 'PermExDc' - control_type = 'SC' - action_type = 'Cont' - version = 'v0' - - env_id = f'{action_type}-{control_type}-{motor_type}-{version}' - + env_id = f"{action_type}-{control_type}-{motor_type}-{version}" # definition of the reference generator - ref_generator = SinusoidalReferenceGenerator(amplitude_range= (1,1), - frequency_range= (5,5), - offset_range = (0,0), - episode_lengths = (10001, 10001)) + ref_generator = SinusoidalReferenceGenerator( + amplitude_range=(1, 1), + frequency_range=(5, 5), + offset_range=(0, 0), + episode_lengths=(10001, 10001), + ) # initialize the gym-electric-motor environment - env = gem.make(env_id, - reference_generator = ref_generator) - + env = gem.make(env_id, reference_generator=ref_generator) + """ initialize the controller @@ -71,33 +72,35 @@ def simulate_env(seed = None): if terminated: env.reset() controller.reset() - - np.savez('./tests/integration_tests/test_data.npz', - states = test_states, references = test_reference, - rewards = test_reward, - terminations = test_term, - truncations = test_trunc) - #env.close() + np.savez( + "./tests/integration_tests/test_data.npz", + states=test_states, + references=test_reference, + rewards=test_reward, + terminations=test_term, + truncations=test_trunc, + ) + + # env.close() + def test_simulate_env(): simulate_env(1337) - test_data = np.load('./tests/integration_tests/test_data.npz') - ref_data = np.load('./tests/integration_tests/ref_data.npz') - + test_data = np.load("./tests/integration_tests/test_data.npz") + ref_data = np.load("./tests/integration_tests/ref_data.npz") + for file in ref_data.files: - assert(np.allclose(ref_data[file], test_data[file], equal_nan= True)) + assert np.allclose(ref_data[file], test_data[file], equal_nan=True) - os.remove('./tests/integration_tests/test_data.npz') + os.remove("./tests/integration_tests/test_data.npz") # Anti test simulate_env(1234) - test_data = np.load('./tests/integration_tests/test_data.npz') + test_data = np.load("./tests/integration_tests/test_data.npz") # test only states, references and rewards for file in ref_data.files[0:3]: - assert((not np.allclose(ref_data[file], test_data[file], equal_nan= True))) + assert not np.allclose(ref_data[file], test_data[file], equal_nan=True) - os.remove('./tests/integration_tests/test_data.npz') - - \ No newline at end of file + os.remove("./tests/integration_tests/test_data.npz") diff --git a/tests/test_callbacks.py b/tests/test_callbacks.py index 4bf8a9b1..7108e35c 100644 --- a/tests/test_callbacks.py +++ b/tests/test_callbacks.py @@ -1,4 +1,7 @@ -from gym_electric_motor.reference_generators import SubepisodedReferenceGenerator, SwitchedReferenceGenerator +from gym_electric_motor.reference_generators import ( + SubepisodedReferenceGenerator, + SwitchedReferenceGenerator, +) from gym_electric_motor.callbacks import RampingLimitMargin from tests.testing_utils import DummyElectricMotorEnvironment, DummyReferenceGenerator import pytest @@ -6,17 +9,21 @@ class TestRampingLimitMargin: test_class = RampingLimitMargin - key = '' + key = "" def test_update(self): # Step updates callback = self.test_class( - initial_limit_margin=(-0.1, 0.1), maximum_limit_margin=(-1, 1), step_size=0.1, update_time='step', - update_freq=100 + initial_limit_margin=(-0.1, 0.1), + maximum_limit_margin=(-1, 1), + step_size=0.1, + update_time="step", + update_freq=100, ) callbacks = [callback] env = DummyElectricMotorEnvironment( - reference_generator=SubepisodedReferenceGenerator('dummy_state_0'), callbacks=callbacks + reference_generator=SubepisodedReferenceGenerator("dummy_state_0"), + callbacks=callbacks, ) # Initial limit margin set assert env.reference_generator._limit_margin == (-0.1, 0.1) @@ -37,12 +44,16 @@ def test_update(self): # Episode updates callback = self.test_class( - initial_limit_margin=(0.0, 0.3), maximum_limit_margin=(0, 1), step_size=0.2, update_time='episode', - update_freq=20 + initial_limit_margin=(0.0, 0.3), + maximum_limit_margin=(0, 1), + step_size=0.2, + update_time="episode", + update_freq=20, ) callbacks = [callback] env = DummyElectricMotorEnvironment( - reference_generator=SubepisodedReferenceGenerator('dummy_state_0'), callbacks=callbacks + reference_generator=SubepisodedReferenceGenerator("dummy_state_0"), + callbacks=callbacks, ) # Initial limit margin set assert env._reference_generator._limit_margin == (0.0, 0.3) @@ -60,13 +71,13 @@ def test_update(self): for i in range(1000): env.reset() assert env._reference_generator._limit_margin == (0, 1) - + def test_initial_values(self): callback = self.test_class() - assert callback._limit_margin == (-0.1,0.1) - assert callback._maximum_limit_margin == (-1,1) + assert callback._limit_margin == (-0.1, 0.1) + assert callback._maximum_limit_margin == (-1, 1) assert callback._step_size == 0.1 - assert callback._update_time == 'episode' + assert callback._update_time == "episode" assert callback._update_freq == 10 def test_update_switched(self): @@ -74,11 +85,14 @@ def test_update_switched(self): callback = self.test_class() callbacks = [callback] sub_generators = [ - SubepisodedReferenceGenerator('dummy_state_0'), SubepisodedReferenceGenerator('dummy_state_0'), - SubepisodedReferenceGenerator('dummy_state_0') + SubepisodedReferenceGenerator("dummy_state_0"), + SubepisodedReferenceGenerator("dummy_state_0"), + SubepisodedReferenceGenerator("dummy_state_0"), ] switched = SwitchedReferenceGenerator(sub_generators) - env = DummyElectricMotorEnvironment(reference_generator=switched, callbacks=callbacks) + env = DummyElectricMotorEnvironment( + reference_generator=switched, callbacks=callbacks + ) # All sub generators get initial limit margin for sub_generator in sub_generators: assert sub_generator._limit_margin == (-0.1, 0.1) @@ -87,20 +101,26 @@ def test_update_switched(self): env.reset() for sub_generator in sub_generators: assert sub_generator._limit_margin == (-0.2, 0.2) - + def test_right_reference(self): callback = self.test_class() callbacks = [callback] # Reference generator has to be a subclass of SubepisodedReferenceGenerator with pytest.raises(AssertionError) as excinfo: - env = DummyElectricMotorEnvironment(reference_generator=DummyReferenceGenerator(), callbacks=callbacks) + env = DummyElectricMotorEnvironment( + reference_generator=DummyReferenceGenerator(), callbacks=callbacks + ) assert "The RampingLimitMargin does only support" in str(excinfo.value) - + # All sub generators have to subclasses of SubepisodedReferenceGenerator - sub_generators = [SubepisodedReferenceGenerator('dummy_state_0'), - SubepisodedReferenceGenerator('dummy_state_0'), DummyReferenceGenerator( - reference_state='dummy_state_0')] + sub_generators = [ + SubepisodedReferenceGenerator("dummy_state_0"), + SubepisodedReferenceGenerator("dummy_state_0"), + DummyReferenceGenerator(reference_state="dummy_state_0"), + ] switched = SwitchedReferenceGenerator(sub_generators) with pytest.raises(AssertionError) as excinfo: - env = DummyElectricMotorEnvironment(reference_generator=switched, callbacks=callbacks) + env = DummyElectricMotorEnvironment( + reference_generator=switched, callbacks=callbacks + ) assert "The RampingLimitMargin does only support" in str(excinfo.value) diff --git a/tests/test_constraints/test_limit_constraint.py b/tests/test_constraints/test_limit_constraint.py index bb6a8cd0..6299f7f7 100644 --- a/tests/test_constraints/test_limit_constraint.py +++ b/tests/test_constraints/test_limit_constraint.py @@ -6,29 +6,60 @@ class TestLimitConstraint: - - @pytest.mark.parametrize(['ps', 'observed_state_names', 'expected_state_names'], [ - [DummyPhysicalSystem(2), ['all_states'], ['dummy_state_0', 'dummy_state_1']], - [DummyPhysicalSystem(3), ['dummy_state_0', 'dummy_state_2'], ['dummy_state_0', 'dummy_state_2']] - ]) + @pytest.mark.parametrize( + ["ps", "observed_state_names", "expected_state_names"], + [ + [ + DummyPhysicalSystem(2), + ["all_states"], + ["dummy_state_0", "dummy_state_1"], + ], + [ + DummyPhysicalSystem(3), + ["dummy_state_0", "dummy_state_2"], + ["dummy_state_0", "dummy_state_2"], + ], + ], + ) def test_initialization(self, ps, observed_state_names, expected_state_names): lc = LimitConstraint(observed_state_names) lc.set_modules(ps) assert lc._observed_state_names == expected_state_names - assert np.all(lc._observed_states == np.array([state in expected_state_names for state in ps.state_names])) + assert np.all( + lc._observed_states + == np.array([state in expected_state_names for state in ps.state_names]) + ) - @pytest.mark.parametrize(['ps', 'observed_state_names', 'state', 'expected_violation'], [ - [DummyPhysicalSystem(2), ['all_states'], np.array([0.0, 1.1]), 1.0], - [DummyPhysicalSystem(2), ['all_states'], np.array([0.0, 0.9]), 0.0], - [DummyPhysicalSystem(2), ['all_states'], np.array([0.0, 1.0]), 0.0], - [DummyPhysicalSystem(2), ['all_states'], np.array([-1.1, 0.9]), 1.0], - [DummyPhysicalSystem(2), ['all_states'], np.array([-0.1, 0.9]), 0.0], - [DummyPhysicalSystem(2), ['all_states'], np.array([-1.1, 1.1]), 1.0], - [DummyPhysicalSystem(2), ['all_states'], np.array([-1.0, 1.0]), 0.0], - [DummyPhysicalSystem(3), ['dummy_state_0', 'dummy_state_2'], np.array([0.0, 1.1, 0.0]), 0.0], - [DummyPhysicalSystem(3), ['dummy_state_0', 'dummy_state_2'], np.array([-1.0, 1.1, 0.0]), 0.0], - [DummyPhysicalSystem(3), ['dummy_state_0', 'dummy_state_2'], np.array([-1.1, 1.1, 0.0]), 1.0], - ]) + @pytest.mark.parametrize( + ["ps", "observed_state_names", "state", "expected_violation"], + [ + [DummyPhysicalSystem(2), ["all_states"], np.array([0.0, 1.1]), 1.0], + [DummyPhysicalSystem(2), ["all_states"], np.array([0.0, 0.9]), 0.0], + [DummyPhysicalSystem(2), ["all_states"], np.array([0.0, 1.0]), 0.0], + [DummyPhysicalSystem(2), ["all_states"], np.array([-1.1, 0.9]), 1.0], + [DummyPhysicalSystem(2), ["all_states"], np.array([-0.1, 0.9]), 0.0], + [DummyPhysicalSystem(2), ["all_states"], np.array([-1.1, 1.1]), 1.0], + [DummyPhysicalSystem(2), ["all_states"], np.array([-1.0, 1.0]), 0.0], + [ + DummyPhysicalSystem(3), + ["dummy_state_0", "dummy_state_2"], + np.array([0.0, 1.1, 0.0]), + 0.0, + ], + [ + DummyPhysicalSystem(3), + ["dummy_state_0", "dummy_state_2"], + np.array([-1.0, 1.1, 0.0]), + 0.0, + ], + [ + DummyPhysicalSystem(3), + ["dummy_state_0", "dummy_state_2"], + np.array([-1.1, 1.1, 0.0]), + 1.0, + ], + ], + ) def test_call(self, ps, observed_state_names, state, expected_violation): lc = LimitConstraint(observed_state_names) lc.set_modules(ps) diff --git a/tests/test_constraints/test_squared_constraint.py b/tests/test_constraints/test_squared_constraint.py index 502c49c7..5325b0e7 100644 --- a/tests/test_constraints/test_squared_constraint.py +++ b/tests/test_constraints/test_squared_constraint.py @@ -6,29 +6,93 @@ class TestSquaredConstraint: - - @pytest.mark.parametrize(['ps', 'observed_state_names', 'expected_state_names'], [ - [DummyPhysicalSystem(2), ['dummy_state_1'], ['dummy_state_1']], - [DummyPhysicalSystem(3), ['dummy_state_0', 'dummy_state_2'], ['dummy_state_0', 'dummy_state_2']] - ]) + @pytest.mark.parametrize( + ["ps", "observed_state_names", "expected_state_names"], + [ + [DummyPhysicalSystem(2), ["dummy_state_1"], ["dummy_state_1"]], + [ + DummyPhysicalSystem(3), + ["dummy_state_0", "dummy_state_2"], + ["dummy_state_0", "dummy_state_2"], + ], + ], + ) def test_initialization(self, ps, observed_state_names, expected_state_names): sc = SquaredConstraint(observed_state_names) sc.set_modules(ps) assert sc._states == expected_state_names - @pytest.mark.parametrize(['ps', 'observed_state_names', 'state', 'expected_violation'], [ - [DummyPhysicalSystem(2), ['dummy_state_0', 'dummy_state_1'], np.array([0.8, 0.8]), 1.0], - [DummyPhysicalSystem(2), ['dummy_state_0', 'dummy_state_1'], np.array([0.0, 0.9]), 0.0], - [DummyPhysicalSystem(2), ['dummy_state_0', 'dummy_state_1'], np.array([0.0, 1.0]), 0.0], - [DummyPhysicalSystem(2), ['dummy_state_0', 'dummy_state_1'], np.array([-0.1, 1.0]), 1.0], - [DummyPhysicalSystem(2), ['dummy_state_0', 'dummy_state_1'], np.array([-1.1, 0.9]), 1.0], - [DummyPhysicalSystem(2), ['dummy_state_0', 'dummy_state_1'], np.array([-0.1, 0.9]), 0.0], - [DummyPhysicalSystem(2), ['dummy_state_0', 'dummy_state_1'], np.array([-1.1, 1.1]), 1.0], - [DummyPhysicalSystem(2), ['dummy_state_0', 'dummy_state_1'], np.array([-1.0, 1.0]), 1.0], - [DummyPhysicalSystem(3), ['dummy_state_0', 'dummy_state_2'], np.array([0.5, 1.1, 0.5]), 0.0], - [DummyPhysicalSystem(3), ['dummy_state_0', 'dummy_state_2'], np.array([-1.0, 1.1, 0.1]), 1.0], - [DummyPhysicalSystem(3), ['dummy_state_0', 'dummy_state_2'], np.array([-1.1, 1.1, 0.0]), 1.0], - ]) + @pytest.mark.parametrize( + ["ps", "observed_state_names", "state", "expected_violation"], + [ + [ + DummyPhysicalSystem(2), + ["dummy_state_0", "dummy_state_1"], + np.array([0.8, 0.8]), + 1.0, + ], + [ + DummyPhysicalSystem(2), + ["dummy_state_0", "dummy_state_1"], + np.array([0.0, 0.9]), + 0.0, + ], + [ + DummyPhysicalSystem(2), + ["dummy_state_0", "dummy_state_1"], + np.array([0.0, 1.0]), + 0.0, + ], + [ + DummyPhysicalSystem(2), + ["dummy_state_0", "dummy_state_1"], + np.array([-0.1, 1.0]), + 1.0, + ], + [ + DummyPhysicalSystem(2), + ["dummy_state_0", "dummy_state_1"], + np.array([-1.1, 0.9]), + 1.0, + ], + [ + DummyPhysicalSystem(2), + ["dummy_state_0", "dummy_state_1"], + np.array([-0.1, 0.9]), + 0.0, + ], + [ + DummyPhysicalSystem(2), + ["dummy_state_0", "dummy_state_1"], + np.array([-1.1, 1.1]), + 1.0, + ], + [ + DummyPhysicalSystem(2), + ["dummy_state_0", "dummy_state_1"], + np.array([-1.0, 1.0]), + 1.0, + ], + [ + DummyPhysicalSystem(3), + ["dummy_state_0", "dummy_state_2"], + np.array([0.5, 1.1, 0.5]), + 0.0, + ], + [ + DummyPhysicalSystem(3), + ["dummy_state_0", "dummy_state_2"], + np.array([-1.0, 1.1, 0.1]), + 1.0, + ], + [ + DummyPhysicalSystem(3), + ["dummy_state_0", "dummy_state_2"], + np.array([-1.1, 1.1, 0.0]), + 1.0, + ], + ], + ) def test_call(self, ps, observed_state_names, state, expected_violation): sc = SquaredConstraint(observed_state_names) sc.set_modules(ps) diff --git a/tests/test_core.py b/tests/test_core.py index 642c09e0..5500be94 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,11 +1,26 @@ import pytest import numpy as np -from tests.testing_utils import DummyPhysicalSystem, DummyReferenceGenerator, DummyRewardFunction, DummyVisualization,\ - DummyCallback, DummyConstraintMonitor, DummyConstraint, mock_instantiate, instantiate_dict +from tests.testing_utils import ( + DummyPhysicalSystem, + DummyReferenceGenerator, + DummyRewardFunction, + DummyVisualization, + DummyCallback, + DummyConstraintMonitor, + DummyConstraint, + mock_instantiate, + instantiate_dict, +) from gymnasium.spaces import Tuple, Box import gym_electric_motor -from gym_electric_motor.core import ElectricMotorEnvironment, RewardFunction, \ - ReferenceGenerator, PhysicalSystem, ConstraintMonitor, Constraint +from gym_electric_motor.core import ( + ElectricMotorEnvironment, + RewardFunction, + ReferenceGenerator, + PhysicalSystem, + ConstraintMonitor, + Constraint, +) from gym_electric_motor.constraints import LimitConstraint import gymnasium import gym_electric_motor as gem @@ -13,7 +28,7 @@ class TestElectricMotorEnvironment: test_class = ElectricMotorEnvironment - key = '' + key = "" @pytest.fixture def env(self): @@ -29,12 +44,12 @@ def env(self): reward_function=rf, visualizations=vs, constraints=cm, - callbacks=[cb] + callbacks=[cb], ) return env def test_make(self): - if self.key != '': + if self.key != "": env = gem.make(self.key) assert type(env) == self.test_class @@ -42,26 +57,41 @@ def test_make(self): "physical_system, reference_generator, reward_function, state_filter, visualization, callbacks", [ ( - DummyPhysicalSystem(), DummyReferenceGenerator(), DummyRewardFunction(), None, (), [] + DummyPhysicalSystem(), + DummyReferenceGenerator(), + DummyRewardFunction(), + None, + (), + [], ), ( - DummyPhysicalSystem(2), DummyReferenceGenerator(), DummyRewardFunction(), ['dummy_state_0'], - (), [DummyCallback()] + DummyPhysicalSystem(2), + DummyReferenceGenerator(), + DummyRewardFunction(), + ["dummy_state_0"], + (), + [DummyCallback()], ), ( DummyPhysicalSystem(10), DummyReferenceGenerator(), - DummyRewardFunction(observed_states=['dummy_state_0']), ['dummy_state_0', 'dummy_state_2'], + DummyRewardFunction(observed_states=["dummy_state_0"]), + ["dummy_state_0", "dummy_state_2"], (), [DummyCallback(), DummyCallback()], ), - ] + ], ) def test_initialization( - self, monkeypatch, physical_system, reference_generator, reward_function, state_filter, visualization, - callbacks + self, + monkeypatch, + physical_system, + reference_generator, + reward_function, + state_filter, + visualization, + callbacks, ): - env = gym_electric_motor.core.ElectricMotorEnvironment( physical_system=physical_system, reference_generator=reference_generator, @@ -77,21 +107,24 @@ def test_initialization( assert reward_function == env.reward_function if state_filter is None: - assert Tuple(( - physical_system.state_space, - reference_generator.reference_space - )) == env.observation_space, 'Wrong observation space' + assert ( + Tuple( + (physical_system.state_space, reference_generator.reference_space) + ) + == env.observation_space + ), "Wrong observation space" else: state_idxs = np.isin(physical_system.state_names, state_filter) state_space = Box( physical_system.state_space.low[state_idxs], physical_system.state_space.high[state_idxs], - dtype=float + dtype=float, ) - assert Tuple( - (state_space, reference_generator.reference_space) - ) == env.observation_space, 'Wrong observation space' - assert env.reward_range == reward_function.reward_range, 'Wrong reward range' + assert ( + Tuple((state_space, reference_generator.reference_space)) + == env.observation_space + ), "Wrong observation space" + assert env.reward_range == reward_function.reward_range, "Wrong reward range" for callback in callbacks: assert callback._env == env @@ -110,14 +143,27 @@ def test_reset(self, env): for callback in cbs: assert callback.reset_begin == 1 assert callback.reset_end == 1 - assert (state, ref) in env.observation_space, 'Returned values not in observation space' - assert np.all(np.all(state == ps.state)), 'Returned state is not the physical systems state' - assert np.all(ref == rg.reference_observation), 'Returned reference is not the reference generators reference' - assert np.all(state == rg.get_reference_state), 'Incorrect state passed to the reference generator' - assert rf.last_state == state, 'Incorrect state passed to the Reward Function' - assert rf.last_reference == rg.reference_array, 'Incorrect Reference passed to the reward function' - - @pytest.mark.parametrize('action, set_done', [(0, False), (-1, False), (1, False), (2, True)]) + assert ( + state, + ref, + ) in env.observation_space, "Returned values not in observation space" + assert np.all( + np.all(state == ps.state) + ), "Returned state is not the physical systems state" + assert np.all( + ref == rg.reference_observation + ), "Returned reference is not the reference generators reference" + assert np.all( + state == rg.get_reference_state + ), "Incorrect state passed to the reference generator" + assert rf.last_state == state, "Incorrect state passed to the Reward Function" + assert ( + rf.last_reference == rg.reference_array + ), "Incorrect Reference passed to the reward function" + + @pytest.mark.parametrize( + "action, set_done", [(0, False), (-1, False), (1, False), (2, True)] + ) def test_step(self, env, action, set_done): ps = env.physical_system rg = env.reference_generator @@ -126,7 +172,7 @@ def test_step(self, env, action, set_done): cm = env.constraint_monitor cm.constraints[0].violation_degree = float(set_done) with pytest.raises(Exception): - env.step(action), 'Environment goes through the step without previous reset' + env.step(action), "Environment goes through the step without previous reset" env.reset() # Callback's step initial step values for callback in cbs: @@ -137,12 +183,18 @@ def test_step(self, env, action, set_done): for callback in cbs: assert callback.step_begin == 1 assert callback.step_end == 1 - assert np.all(state == ps.state[env.state_filter]), 'Returned state and Physical Systems state are not equal' - assert rg.get_reference_state == ps.state,\ - 'State passed to the Reference Generator not equal to Physical System state' - assert rg.get_reference_obs_state == ps.state, \ - 'State passed to the Reference Generator not equal to Physical System state' - assert ps.action == action, 'Action passed to Physical System not equal to selected action' + assert np.all( + state == ps.state[env.state_filter] + ), "Returned state and Physical Systems state are not equal" + assert ( + rg.get_reference_state == ps.state + ), "State passed to the Reference Generator not equal to Physical System state" + assert ( + rg.get_reference_obs_state == ps.state + ), "State passed to the Reference Generator not equal to Physical System state" + assert ( + ps.action == action + ), "Action passed to Physical System not equal to selected action" assert reward == -1 if set_done else 1 assert terminated == set_done # If episode terminated, no further step without reset @@ -162,18 +214,23 @@ def test_close(self, env): # Callback's close function was called on close for callback in cbs: assert callback.close == 1 - assert ps.closed, 'Physical System was not closed' - assert rf.closed, 'Reward Function was not closed' - assert rg.closed, 'Reference Generator was not closed' + assert ps.closed, "Physical System was not closed" + assert rf.closed, "Reward Function was not closed" + assert rg.closed, "Reference Generator was not closed" @pytest.mark.parametrize("reference_generator", (DummyReferenceGenerator(),)) def test_reference_generator_change(self, env, reference_generator): env.reset() env.reference_generator = reference_generator - assert env.reference_generator == reference_generator, 'Reference Generator was not changed' + assert ( + env.reference_generator == reference_generator + ), "Reference Generator was not changed" # Without Reset an Exception has to be thrown with pytest.raises(Exception): - env.step(env.action_space.sample()), 'After Reference Generator change was no reset required' + ( + env.step(env.action_space.sample()), + "After Reference Generator change was no reset required", + ) env.reset() # No Exception raised env.step(env.action_space.sample()) @@ -182,24 +239,29 @@ def test_reference_generator_change(self, env, reference_generator): def test_reward_function_change(self, env, reward_function): env.reset() reward_function.set_modules( - physical_system=env.physical_system, reference_generator=env.reference_generator, - constraint_monitor=env.constraint_monitor + physical_system=env.physical_system, + reference_generator=env.reference_generator, + constraint_monitor=env.constraint_monitor, ) env.reward_function = reward_function - assert env.reward_function == reward_function, 'Reward Function was not changed' + assert env.reward_function == reward_function, "Reward Function was not changed" # Without Reset an Exception has to be thrown with pytest.raises(Exception): - env.step(env.action_space.sample()), 'After Reward Function change was no reset required' + ( + env.step(env.action_space.sample()), + "After Reward Function change was no reset required", + ) env.reset() # No Exception raised env.step(env.action_space.sample()) @pytest.mark.parametrize( - "number_states, state_filter, expected_result", ( - (1, ['dummy_state_0'], [10]), - (3, ['dummy_state_0', 'dummy_state_1', 'dummy_state_2'], [10, 20, 30]), - (3, ['dummy_state_1'], [20]) - ) + "number_states, state_filter, expected_result", + ( + (1, ["dummy_state_0"], [10]), + (3, ["dummy_state_0", "dummy_state_1", "dummy_state_2"], [10, 20, 30]), + (3, ["dummy_state_1"], [20]), + ), ) def test_limits(self, number_states, state_filter, expected_result): ps = DummyPhysicalSystem(state_length=number_states) @@ -213,7 +275,7 @@ def test_limits(self, number_states, state_filter, expected_result): reward_function=rf, visualization=vs, state_filter=state_filter, - constraints=cm + constraints=cm, ) assert all(env.limits == expected_result) @@ -227,8 +289,14 @@ class TestReferenceGenerator: @pytest.fixture def reference_generator(self, monkeypatch): - monkeypatch.setattr(ReferenceGenerator, "get_reference_observation", self.mock_get_reference_observation) - monkeypatch.setattr(ReferenceGenerator, "get_reference", self.mock_get_reference) + monkeypatch.setattr( + ReferenceGenerator, + "get_reference_observation", + self.mock_get_reference_observation, + ) + monkeypatch.setattr( + ReferenceGenerator, "get_reference", self.mock_get_reference + ) rg = ReferenceGenerator() rg._referenced_states = np.array([True, False]) return rg @@ -245,7 +313,10 @@ def mock_get_reference(self, initial_state): def test_reset(self, reference_generator): reference, observation, kwargs = reference_generator.reset(self.initial_state) assert all(reference == reference_generator.get_reference(self.initial_state)) - assert all(observation == reference_generator.get_reference_observation(self.initial_state)) + assert all( + observation + == reference_generator.get_reference_observation(self.initial_state) + ) assert kwargs is None def test_referenced_states(self, reference_generator): @@ -253,11 +324,10 @@ def test_referenced_states(self, reference_generator): class TestPhysicalSystem: - def test_initialization(self): action_space = gymnasium.spaces.Discrete(3) state_space = gymnasium.spaces.Box(-1, 1, shape=(3,)) - state_names = [f'dummy_state_{i}' for i in range(3)] + state_names = [f"dummy_state_{i}" for i in range(3)] tau = 1 ps = PhysicalSystem(action_space, state_space, state_names, tau) assert ps.action_space == action_space @@ -268,89 +338,119 @@ def test_initialization(self): class TestConstraintMonitor: - @pytest.mark.parametrize( - ['ps', 'limit_constraints', 'expected_observed_states'], [ - [DummyPhysicalSystem(3), ['dummy_state_0', 'dummy_state_2'], ['dummy_state_0', 'dummy_state_2']], - [DummyPhysicalSystem(1), ['dummy_state_0'], ['dummy_state_0']], - [DummyPhysicalSystem(2), ['all_states'], ['dummy_state_0', 'dummy_state_1']] - ] - ) - def test_limit_constraint_setting(self, ps, limit_constraints, expected_observed_states): + ["ps", "limit_constraints", "expected_observed_states"], + [ + [ + DummyPhysicalSystem(3), + ["dummy_state_0", "dummy_state_2"], + ["dummy_state_0", "dummy_state_2"], + ], + [DummyPhysicalSystem(1), ["dummy_state_0"], ["dummy_state_0"]], + [ + DummyPhysicalSystem(2), + ["all_states"], + ["dummy_state_0", "dummy_state_1"], + ], + ], + ) + def test_limit_constraint_setting( + self, ps, limit_constraints, expected_observed_states + ): cm = ConstraintMonitor(limit_constraints=limit_constraints) cm.set_modules(ps) assert cm.constraints[0]._observed_state_names == expected_observed_states - @pytest.mark.parametrize('constraints', [ - [lambda state: 0.0, DummyConstraint(), DummyConstraint()] - ]) - @pytest.mark.parametrize('ps', [DummyPhysicalSystem(1)]) + @pytest.mark.parametrize( + "constraints", [[lambda state: 0.0, DummyConstraint(), DummyConstraint()]] + ) + @pytest.mark.parametrize("ps", [DummyPhysicalSystem(1)]) def test_additional_constraint_setting(self, ps, constraints): cm = ConstraintMonitor(additional_constraints=constraints) cm.set_modules(ps) assert all(constraint in cm.constraints for constraint in constraints) - @pytest.mark.parametrize('additional_constraints', [ - [lambda state: 0.0, DummyConstraint(), DummyConstraint()] - ]) - @pytest.mark.parametrize('limit_constraints', [ - ['all_states'], ['dummy_state_0'], [] - ]) - @pytest.mark.parametrize('ps', [DummyPhysicalSystem(1)]) + @pytest.mark.parametrize( + "additional_constraints", + [[lambda state: 0.0, DummyConstraint(), DummyConstraint()]], + ) + @pytest.mark.parametrize( + "limit_constraints", [["all_states"], ["dummy_state_0"], []] + ) + @pytest.mark.parametrize("ps", [DummyPhysicalSystem(1)]) def test_set_modules(self, ps, limit_constraints, additional_constraints): cm = ConstraintMonitor(limit_constraints, additional_constraints) cm.set_modules(ps) assert all( - constraint.modules_set for constraint in cm.constraints if isinstance(constraint, DummyConstraint) + constraint.modules_set + for constraint in cm.constraints + if isinstance(constraint, DummyConstraint) ) assert all( - constraint._observed_states is not None for constraint in cm.constraints + constraint._observed_states is not None + for constraint in cm.constraints if isinstance(constraint, LimitConstraint) ) - @pytest.mark.parametrize(['violations', 'expected_violation_degree'], [ - [(0.5, 0.8, 0.0, 1.0), 1.0], - [(0.5, 0.8, 0.0), 0.8], - [(0.5,), 0.5], - [(0.0,), 0.0], - ]) - @pytest.mark.parametrize('ps', [DummyPhysicalSystem(1)]) + @pytest.mark.parametrize( + ["violations", "expected_violation_degree"], + [ + [(0.5, 0.8, 0.0, 1.0), 1.0], + [(0.5, 0.8, 0.0), 0.8], + [(0.5,), 0.5], + [(0.0,), 0.0], + ], + ) + @pytest.mark.parametrize("ps", [DummyPhysicalSystem(1)]) def test_max_merge_violations(self, ps, violations, expected_violation_degree): - cm = ConstraintMonitor(merge_violations='max') + cm = ConstraintMonitor(merge_violations="max") cm.set_modules(ps) cm._merge_violations(violations) - @pytest.mark.parametrize(['violations', 'expected_violation_degree'], [ - [(0.5, 0.8, 0.0, 1.0), 1.0], - [(0.5, 0.8, 0.0), 0.9], - [(0.5,), 0.5], - [(0.0,), 0.0], - ]) - @pytest.mark.parametrize('ps', [DummyPhysicalSystem(1)]) + @pytest.mark.parametrize( + ["violations", "expected_violation_degree"], + [ + [(0.5, 0.8, 0.0, 1.0), 1.0], + [(0.5, 0.8, 0.0), 0.9], + [(0.5,), 0.5], + [(0.0,), 0.0], + ], + ) + @pytest.mark.parametrize("ps", [DummyPhysicalSystem(1)]) def test_product_merge_violations(self, ps, violations, expected_violation_degree): - cm = ConstraintMonitor(merge_violations='product') + cm = ConstraintMonitor(merge_violations="product") cm.set_modules(ps) cm._merge_violations(violations) - @pytest.mark.parametrize(['merging_fct', 'violations', 'expected_violation_degree'], [ - [lambda *violations: 1.0, (0.5, 0.8, 0.0, 1.0), 1.0], - [lambda *violations: 0.756, (0.5, 0.8, 0.0), 0.756], - [lambda *violations: 0.123, (0.5,), 0.123], - [lambda *violations: 0.0, (0.0,), 0.0], - ]) - @pytest.mark.parametrize('ps', [DummyPhysicalSystem(1)]) - def test_callable_merge_violations(self, ps, merging_fct, violations, expected_violation_degree): + @pytest.mark.parametrize( + ["merging_fct", "violations", "expected_violation_degree"], + [ + [lambda *violations: 1.0, (0.5, 0.8, 0.0, 1.0), 1.0], + [lambda *violations: 0.756, (0.5, 0.8, 0.0), 0.756], + [lambda *violations: 0.123, (0.5,), 0.123], + [lambda *violations: 0.0, (0.0,), 0.0], + ], + ) + @pytest.mark.parametrize("ps", [DummyPhysicalSystem(1)]) + def test_callable_merge_violations( + self, ps, merging_fct, violations, expected_violation_degree + ): cm = ConstraintMonitor(merge_violations=merging_fct) cm.set_modules(ps) cm._merge_violations(violations) - @pytest.mark.parametrize(['violations', 'expected_violation_degree'], [ - [(0.5, 0.8, 0.0, 1.0), 1.0], - [(0.5, 0.8, 0.0), 0.756], - [(0.5,), 0.123], - [(0.0,), 0.0], - ]) - @pytest.mark.parametrize(['ps', 'state'], [[DummyPhysicalSystem(1), np.array([1.0])]]) + @pytest.mark.parametrize( + ["violations", "expected_violation_degree"], + [ + [(0.5, 0.8, 0.0, 1.0), 1.0], + [(0.5, 0.8, 0.0), 0.756], + [(0.5,), 0.123], + [(0.0,), 0.0], + ], + ) + @pytest.mark.parametrize( + ["ps", "state"], [[DummyPhysicalSystem(1), np.array([1.0])]] + ) def test_check_constraints(self, ps, state, violations, expected_violation_degree): passed_violations = [] @@ -359,8 +459,13 @@ def merge_violations(*violation_degrees): return expected_violation_degree constraints = [DummyConstraint(viol_degree) for viol_degree in violations] - cm = ConstraintMonitor(additional_constraints=constraints, merge_violations=merge_violations) + cm = ConstraintMonitor( + additional_constraints=constraints, merge_violations=merge_violations + ) cm.set_modules(ps) degree = cm.check_constraints(state) assert degree == expected_violation_degree - assert all(passed == expected for passed, expected in zip(passed_violations[0][0], violations)) + assert all( + passed == expected + for passed, expected in zip(passed_violations[0][0], violations) + ) diff --git a/tests/test_environments/test_environments.py b/tests/test_environments/test_environments.py index e30821d9..8e7ba6ed 100644 --- a/tests/test_environments/test_environments.py +++ b/tests/test_environments/test_environments.py @@ -4,30 +4,33 @@ import gym_electric_motor as gem -control_tasks = ['TC', 'SC', 'CC'] -action_types = ['Cont', 'Finite'] -ac_motors = ['PMSM', 'SynRM', 'SCIM', 'DFIM'] -dc_motors = ['SeriesDc', 'ShuntDc', 'PermExDc', 'ExtExDc'] -versions = ['v0'] +control_tasks = ["TC", "SC", "CC"] +action_types = ["Cont", "Finite"] +ac_motors = ["PMSM", "SynRM", "SCIM", "DFIM"] +dc_motors = ["SeriesDc", "ShuntDc", "PermExDc", "ExtExDc"] +versions = ["v0"] -@pytest.mark.parametrize('version', versions) -@pytest.mark.parametrize('motor', ac_motors + dc_motors) -@pytest.mark.parametrize('control_task', control_tasks) -@pytest.mark.parametrize(['action_type', 'tau'], zip(action_types, [1e-4, 1e-5, 1e-4])) +@pytest.mark.parametrize("version", versions) +@pytest.mark.parametrize("motor", ac_motors + dc_motors) +@pytest.mark.parametrize("control_task", control_tasks) +@pytest.mark.parametrize(["action_type", "tau"], zip(action_types, [1e-4, 1e-5, 1e-4])) def test_tau(motor, control_task, action_type, version, tau): - env_id = f'{action_type}-{control_task}-{motor}-{version}' + env_id = f"{action_type}-{control_task}-{motor}-{version}" env = gem.make(env_id) assert env.physical_system.tau == tau -@pytest.mark.parametrize('version', versions) -@pytest.mark.parametrize('ac_motor', ac_motors) +@pytest.mark.parametrize("version", versions) +@pytest.mark.parametrize("ac_motor", ac_motors) @pytest.mark.parametrize( - ['control_task', 'referenced_states'], zip(control_tasks, [['torque'], ['omega'], ['i_sd', 'i_sq']]) + ["control_task", "referenced_states"], + zip(control_tasks, [["torque"], ["omega"], ["i_sd", "i_sq"]]), ) -@pytest.mark.parametrize('action_type', action_types) -def test_referenced_states_ac(ac_motor, control_task, action_type, version, referenced_states): - env_id = f'{action_type}-{control_task}-{ac_motor}-{version}' +@pytest.mark.parametrize("action_type", action_types) +def test_referenced_states_ac( + ac_motor, control_task, action_type, version, referenced_states +): + env_id = f"{action_type}-{control_task}-{ac_motor}-{version}" env = gem.make(env_id) assert env.reference_generator.reference_names == referenced_states diff --git a/tests/test_physical_system_wrappers/test_cos_sin_processor.py b/tests/test_physical_system_wrappers/test_cos_sin_processor.py index bffa3cc6..933f0846 100644 --- a/tests/test_physical_system_wrappers/test_cos_sin_processor.py +++ b/tests/test_physical_system_wrappers/test_cos_sin_processor.py @@ -8,16 +8,22 @@ class TestCosSinProcessor(TestPhysicalSystemWrapper): - @pytest.fixture def processor(self, physical_system): - return gem.physical_system_wrappers.CosSinProcessor(angle='dummy_state_0', physical_system=physical_system) + return gem.physical_system_wrappers.CosSinProcessor( + angle="dummy_state_0", physical_system=physical_system + ) def test_limits(self, processor, physical_system): - assert all(processor.limits == np.concatenate((physical_system.limits, [1., 1.]))) + assert all( + processor.limits == np.concatenate((physical_system.limits, [1.0, 1.0])) + ) def test_nominal_state(self, processor, physical_system): - assert all(processor.nominal_state == np.concatenate((physical_system.nominal_state, [1., 1.]))) + assert all( + processor.nominal_state + == np.concatenate((physical_system.nominal_state, [1.0, 1.0])) + ) def test_state_space(self, processor, physical_system): low = np.concatenate((physical_system.state_space.low, [-1, -1])) @@ -26,13 +32,19 @@ def test_state_space(self, processor, physical_system): assert processor.state_space == space def test_reset(self, processor, physical_system): - assert all(processor.reset() == np.concatenate((physical_system.state, [1., 0.]))) + assert all( + processor.reset() == np.concatenate((physical_system.state, [1.0, 0.0])) + ) - @pytest.mark.parametrize('action', [1, 2, 3, 4]) + @pytest.mark.parametrize("action", [1, 2, 3, 4]) def test_simulate(self, processor, physical_system, action): state = processor.simulate(action) ps_state = physical_system.state assert action == physical_system.action cos_sin_state = ps_state[physical_system.state_positions[processor.angle]] - assert all(state == np.concatenate((ps_state, [np.cos(cos_sin_state), np.sin(cos_sin_state)]))) - + assert all( + state + == np.concatenate( + (ps_state, [np.cos(cos_sin_state), np.sin(cos_sin_state)]) + ) + ) diff --git a/tests/test_physical_system_wrappers/test_dead_time_processor.py b/tests/test_physical_system_wrappers/test_dead_time_processor.py index 89e05fba..9ef9be75 100644 --- a/tests/test_physical_system_wrappers/test_dead_time_processor.py +++ b/tests/test_physical_system_wrappers/test_dead_time_processor.py @@ -8,52 +8,64 @@ class TestDeadTimeProcessor(TestPhysicalSystemWrapper): - @pytest.fixture def processor(self, physical_system): - return gem.physical_system_wrappers.DeadTimeProcessor(physical_system=physical_system) + return gem.physical_system_wrappers.DeadTimeProcessor( + physical_system=physical_system + ) @pytest.fixture def unset_processor(self, physical_system): return gem.physical_system_wrappers.DeadTimeProcessor() - @pytest.mark.parametrize('action', [np.array([5.0]), np.array([2.0])]) + @pytest.mark.parametrize("action", [np.array([5.0]), np.array([2.0])]) def test_simulate(self, reset_processor, physical_system, action): state = reset_processor.simulate(action) assert state == physical_system.state assert all(physical_system.action == np.array([0.0])) - @pytest.mark.parametrize('unset_processor', [ - gem.physical_system_wrappers.DeadTimeProcessor(steps=2), - gem.physical_system_wrappers.DeadTimeProcessor(steps=1), - gem.physical_system_wrappers.DeadTimeProcessor(steps=5), - ]) @pytest.mark.parametrize( - ['action_space', 'actions', 'reset_action'], + "unset_processor", + [ + gem.physical_system_wrappers.DeadTimeProcessor(steps=2), + gem.physical_system_wrappers.DeadTimeProcessor(steps=1), + gem.physical_system_wrappers.DeadTimeProcessor(steps=5), + ], + ) + @pytest.mark.parametrize( + ["action_space", "actions", "reset_action"], [ - [ - gymnasium.spaces.Box(-100, 100, shape=(3,)), - [np.array([1., 2., 3.]), np.array([0., 1., 2.]), np.array([-1, 2, 3])], - np.array([0., 0., 0.]) - ], - [ - gymnasium.spaces.Box(-100, 100, shape=(1,)), - [np.array([-1.]), np.array([0.]), np.array([-5.]), np.array([-6.]), np.array([-7.])], - np.array([0.]) - ], - [ - gymnasium.spaces.MultiDiscrete([10, 20, 30]), - [[5, 8, 7], [3, 4, 5], [0, 0, 1], [0, 1, 1], [0, 0, 1]], - [0, 0, 0] - ], - [ - gymnasium.spaces.Discrete(12), - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], - 0 - ] - ] + [ + gymnasium.spaces.Box(-100, 100, shape=(3,)), + [ + np.array([1.0, 2.0, 3.0]), + np.array([0.0, 1.0, 2.0]), + np.array([-1, 2, 3]), + ], + np.array([0.0, 0.0, 0.0]), + ], + [ + gymnasium.spaces.Box(-100, 100, shape=(1,)), + [ + np.array([-1.0]), + np.array([0.0]), + np.array([-5.0]), + np.array([-6.0]), + np.array([-7.0]), + ], + np.array([0.0]), + ], + [ + gymnasium.spaces.MultiDiscrete([10, 20, 30]), + [[5, 8, 7], [3, 4, 5], [0, 0, 1], [0, 1, 1], [0, 0, 1]], + [0, 0, 0], + ], + [gymnasium.spaces.Discrete(12), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 0], + ], ) - def test_execution(self, unset_processor, physical_system, action_space, actions, reset_action): + def test_execution( + self, unset_processor, physical_system, action_space, actions, reset_action + ): expected_actions = [reset_action] * unset_processor.dead_time + actions physical_system._action_space = action_space unset_processor.set_physical_system(physical_system) @@ -65,13 +77,15 @@ def test_execution(self, unset_processor, physical_system, action_space, actions except ValueError: assert all(physical_system.action == expected_actions[i]) - @pytest.mark.parametrize('processor', [gem.physical_system_wrappers.DeadTimeProcessor()]) + @pytest.mark.parametrize( + "processor", [gem.physical_system_wrappers.DeadTimeProcessor()] + ) def test_false_action_space(self, processor, physical_system): physical_system._action_space = gymnasium.spaces.MultiBinary(5) with pytest.raises(AssertionError): assert processor.set_physical_system(physical_system) - @pytest.mark.parametrize('steps', [0, -10]) + @pytest.mark.parametrize("steps", [0, -10]) def test_false_steps(self, steps): with pytest.raises(AssertionError): assert gem.physical_system_wrappers.DeadTimeProcessor(steps) diff --git a/tests/test_physical_system_wrappers/test_dq_to_abc_action_processor.py b/tests/test_physical_system_wrappers/test_dq_to_abc_action_processor.py index 14deaca1..60d24b5c 100644 --- a/tests/test_physical_system_wrappers/test_dq_to_abc_action_processor.py +++ b/tests/test_physical_system_wrappers/test_dq_to_abc_action_processor.py @@ -8,36 +8,46 @@ class TestDqToAbcActionProcessor(TestPhysicalSystemWrapper): - @pytest.fixture def physical_system(self): - ps = DummyPhysicalSystem(state_names=['omega', 'epsilon', 'i']) + ps = DummyPhysicalSystem(state_names=["omega", "epsilon", "i"]) ps.electrical_motor = gem.physical_systems.PermanentMagnetSynchronousMotor() return ps @pytest.fixture def processor(self, physical_system): - return gem.physical_system_wrappers.DqToAbcActionProcessor.make('PMSM', physical_system=physical_system) + return gem.physical_system_wrappers.DqToAbcActionProcessor.make( + "PMSM", physical_system=physical_system + ) def test_action_space(self, processor, physical_system): space = gymnasium.spaces.Box(-1, 1, shape=(2,), dtype=np.float64) assert processor.action_space == space @pytest.mark.parametrize( - ['dq_action', 'state', 'abc_action'], + ["dq_action", "state", "abc_action"], [ - (np.array([0.0, .0]), np.array([0.0, 0.0, 0.0]), np.array([0., 0., 0.])), - (np.array([0.0, 1.0]), np.array([12.8, 0.123, 0.0]), np.array([-0.23752263, 0.96000281, -0.72248018])), - (np.array([0.0, .5]), np.array([-10.0, 0.123, 0.0]), np.array([-0.49324109, 0.31757774, 0.17566335])), - ] + ( + np.array([0.0, 0.0]), + np.array([0.0, 0.0, 0.0]), + np.array([0.0, 0.0, 0.0]), + ), + ( + np.array([0.0, 1.0]), + np.array([12.8, 0.123, 0.0]), + np.array([-0.23752263, 0.96000281, -0.72248018]), + ), + ( + np.array([0.0, 0.5]), + np.array([-10.0, 0.123, 0.0]), + np.array([-0.49324109, 0.31757774, 0.17566335]), + ), + ], ) - def test_simulate(self, reset_processor, physical_system, dq_action, state, abc_action): + def test_simulate( + self, reset_processor, physical_system, dq_action, state, abc_action + ): reset_processor._state = state reset_processor.simulate(dq_action) assert all(np.isclose(reset_processor.action, abc_action)) - - - - - diff --git a/tests/test_physical_system_wrappers/test_flux_observer.py b/tests/test_physical_system_wrappers/test_flux_observer.py index 2a8826fe..b883c7f8 100644 --- a/tests/test_physical_system_wrappers/test_flux_observer.py +++ b/tests/test_physical_system_wrappers/test_flux_observer.py @@ -9,15 +9,19 @@ class TestFluxObserver(TestPhysicalSystemWrapper): - @pytest.fixture def physical_system(self): - ps = DummyPhysicalSystem(state_names=['omega','i_sa', 'i_sb', 'i_sc','i_sd', 'i_sq']) - ps.unwrapped.electrical_motor = gem.physical_systems.electric_motors.SquirrelCageInductionMotor( - limit_values=dict(i=20.0), - motor_parameter=dict(l_m=10.0) + ps = DummyPhysicalSystem( + state_names=["omega", "i_sa", "i_sb", "i_sc", "i_sd", "i_sq"] + ) + ps.unwrapped.electrical_motor = ( + gem.physical_systems.electric_motors.SquirrelCageInductionMotor( + limit_values=dict(i=20.0), motor_parameter=dict(l_m=10.0) + ) ) - ps.unwrapped._limits[ps.state_names.index('i_sd')] = ps.unwrapped.electrical_motor.limits['i_sd'] + ps.unwrapped._limits[ + ps.state_names.index("i_sd") + ] = ps.unwrapped.electrical_motor.limits["i_sd"] return ps @pytest.fixture @@ -27,7 +31,9 @@ def reset_physical_system(self, physical_system): @pytest.fixture def processor(self, physical_system): - return gem.physical_system_wrappers.FluxObserver(physical_system=physical_system) + return gem.physical_system_wrappers.FluxObserver( + physical_system=physical_system + ) @pytest.fixture def reset_processor(self, processor): @@ -35,10 +41,15 @@ def reset_processor(self, processor): return processor def test_limits(self, processor, physical_system): - assert all(processor.limits == np.concatenate((physical_system.limits, [200., np.pi]))) + assert all( + processor.limits == np.concatenate((physical_system.limits, [200.0, np.pi])) + ) def test_nominal_state(self, processor, physical_system): - assert all(processor.nominal_state == np.concatenate((physical_system.nominal_state, [200., np.pi]))) + assert all( + processor.nominal_state + == np.concatenate((physical_system.nominal_state, [200.0, np.pi])) + ) def test_state_space(self, processor, physical_system): psi_abs_max = 200.0 @@ -48,10 +59,11 @@ def test_state_space(self, processor, physical_system): assert processor.state_space == space def test_reset(self, processor, physical_system): - assert all(processor.reset() == np.concatenate((physical_system.state, [0., 0.]))) + assert all( + processor.reset() == np.concatenate((physical_system.state, [0.0, 0.0])) + ) - @pytest.mark.parametrize('action', [1, 2, 3, 4]) + @pytest.mark.parametrize("action", [1, 2, 3, 4]) def test_simulate(self, reset_processor, physical_system, action): state = reset_processor.simulate(action) assert all(state[:-2] == physical_system.state) - diff --git a/tests/test_physical_system_wrappers/test_physical_system_wrapper.py b/tests/test_physical_system_wrappers/test_physical_system_wrapper.py index b4ddef7d..470e7a98 100644 --- a/tests/test_physical_system_wrappers/test_physical_system_wrapper.py +++ b/tests/test_physical_system_wrappers/test_physical_system_wrapper.py @@ -5,14 +5,15 @@ class TestPhysicalSystemWrapper: - @pytest.fixture def physical_system(self): return tu.DummyPhysicalSystem() @pytest.fixture def processor(self, physical_system): - return gem.physical_system_wrappers.PhysicalSystemWrapper(physical_system=physical_system) + return gem.physical_system_wrappers.PhysicalSystemWrapper( + physical_system=physical_system + ) @pytest.fixture def reset_processor(self, processor): @@ -21,7 +22,9 @@ def reset_processor(self, processor): @pytest.fixture def double_wrapped(self, processor): - return gem.physical_system_wrappers.PhysicalSystemWrapper(physical_system=processor) + return gem.physical_system_wrappers.PhysicalSystemWrapper( + physical_system=processor + ) def test_action_space(self, processor, physical_system): assert processor.action_space == physical_system.action_space @@ -44,7 +47,7 @@ def test_limits(self, processor, physical_system): def test_reset(self, processor, physical_system): assert all(processor.reset() == physical_system.state) - @pytest.mark.parametrize(['action'], [[1]]) + @pytest.mark.parametrize(["action"], [[1]]) def test_simulate(self, reset_processor, physical_system, action): state = reset_processor.simulate(action) assert state == physical_system.state diff --git a/tests/test_physical_systems/test_converters.py b/tests/test_physical_systems/test_converters.py index 4096a82c..28d67b66 100644 --- a/tests/test_physical_systems/test_converters.py +++ b/tests/test_physical_systems/test_converters.py @@ -15,7 +15,7 @@ # region definitions # define basic test parameter -g_taus = [1E-3, 1E-4, 1E-5] +g_taus = [1e-3, 1e-4, 1e-5] g_interlocking_times = np.array([0.0, 1 / 20, 1 / 3]) # define test parameter for different cases @@ -30,36 +30,227 @@ g_test_voltages_1qc = np.array([1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0]) # disc 2QC -g_i_ins_2qc = [0, 0.5, -0.5, 0.5, 0.5, 0, -0.5, 0.5, 0.5, 0, -0.5, -0.5, -0.5, 0.5, 0.5, 0.5] +g_i_ins_2qc = [ + 0, + 0.5, + -0.5, + 0.5, + 0.5, + 0, + -0.5, + 0.5, + 0.5, + 0, + -0.5, + -0.5, + -0.5, + 0.5, + 0.5, + 0.5, +] g_actions_2qc = [0, 0, 0, 0, 1, 1, 1, 0, 2, 2, 2, 1, 2, 2, 1, 2] g_times_2qc = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]) g_test_voltages_2qc = np.array([0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0]) -g_test_voltages_2qc_interlocking = np.array([0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0]) +g_test_voltages_2qc_interlocking = np.array( + [0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0] +) # disc 4QC g_times_4qc = np.array( - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28]) -g_i_ins_4qc = [0, 0.5, -0.5, 0.5, 0.5, -0.5, 0, 0.5, 0.5, 0.5, 0.5, -0.5, 0, 0.5, 0.5, 0.5, 0.5, -0.5, 0, 0.5, 0.5, 0.5, - 0.5, 0.5, 0.5, -0.5, -0.5, -0.5, -0.5] -g_actions_4qc = np.array([0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 2, 2, 2, 2, 0, 0, 3, 3, 3, 3, 0, 1, 2, 3, 1, 3, 3, 2, 1]) + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + ] +) +g_i_ins_4qc = [ + 0, + 0.5, + -0.5, + 0.5, + 0.5, + -0.5, + 0, + 0.5, + 0.5, + 0.5, + 0.5, + -0.5, + 0, + 0.5, + 0.5, + 0.5, + 0.5, + -0.5, + 0, + 0.5, + 0.5, + 0.5, + 0.5, + 0.5, + 0.5, + -0.5, + -0.5, + -0.5, + -0.5, +] +g_actions_4qc = np.array( + [ + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 0, + 0, + 2, + 2, + 2, + 2, + 0, + 0, + 3, + 3, + 3, + 3, + 0, + 1, + 2, + 3, + 1, + 3, + 3, + 2, + 1, + ] +) g_test_voltages_4qc = np.array( - [0, 0, 0, 0, 1, 1, 1, 1, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 1, -1, 0, 1, 0, 0, -1, 1]) + [ + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 0, + 0, + -1, + -1, + -1, + -1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + -1, + 0, + 1, + 0, + 0, + -1, + 1, + ] +) g_test_voltages_4qc_interlocking = np.array( - [0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, -1, -1, -1, -1, -1, -1, 0, 0, -1, 0, 0, 0, 0, - -1, 0, 0, 1, -1, -1, -1, 0, 0, 1, 1, 0, 0, 0, -1, 1, 1]) + [ + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + -1, + -1, + -1, + -1, + -1, + -1, + 0, + 0, + -1, + 0, + 0, + 0, + 0, + -1, + 0, + 0, + 1, + -1, + -1, + -1, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + -1, + 1, + 1, + ] +) # combine all test voltages in one vector for each converter g_1qc_test_voltages = [g_test_voltages_1qc, g_test_voltages_1qc] g_2qc_test_voltages = [g_test_voltages_2qc, g_test_voltages_2qc_interlocking] g_4qc_test_voltages = [g_test_voltages_4qc, g_test_voltages_4qc_interlocking] -g_disc_test_voltages = {'Finite-1QC': g_1qc_test_voltages, - 'Finite-2QC': g_2qc_test_voltages, - 'Finite-4QC': g_4qc_test_voltages} -g_disc_test_i_ins = {'Finite-1QC': g_i_ins_1qc, - 'Finite-2QC': g_i_ins_2qc, - 'Finite-4QC': g_i_ins_4qc} -g_disc_test_actions = {'Finite-1QC': g_actions_1qc, - 'Finite-2QC': g_actions_2qc, - 'Finite-4QC': g_actions_4qc} +g_disc_test_voltages = { + "Finite-1QC": g_1qc_test_voltages, + "Finite-2QC": g_2qc_test_voltages, + "Finite-4QC": g_4qc_test_voltages, +} +g_disc_test_i_ins = { + "Finite-1QC": g_i_ins_1qc, + "Finite-2QC": g_i_ins_2qc, + "Finite-4QC": g_i_ins_4qc, +} +g_disc_test_actions = { + "Finite-1QC": g_actions_1qc, + "Finite-2QC": g_actions_2qc, + "Finite-4QC": g_actions_4qc, +} # endregion @@ -69,8 +260,14 @@ def discrete_converter_functions_testing( - converter, action_space_n, times, actions, i_ins, test_voltage_ideal, test_voltage_interlocking_time, - interlocking_time=0.0 + converter, + action_space_n, + times, + actions, + i_ins, + test_voltage_ideal, + test_voltage_interlocking_time, + interlocking_time=0.0, ): """ test of convert function of discrete converter @@ -85,15 +282,23 @@ def discrete_converter_functions_testing( :return: """ action_space = converter.action_space - assert action_space.n == action_space_n # test if the action space has the right size + assert ( + action_space.n == action_space_n + ) # test if the action space has the right size step_counter = 0 - for time, action, i_in in zip(times, actions, i_ins): # apply each action at different times - time_steps = converter.set_action(action, time) # test set action, returns switching times + for time, action, i_in in zip( + times, actions, i_ins + ): # apply each action at different times + time_steps = converter.set_action( + action, time + ) # test set action, returns switching times for index, time_step in enumerate(time_steps): converter_voltage = converter.convert([i_in], time_step) converter.i_sup([i_in]) for u in converter_voltage: - assert converter.voltages.low[0] <= u <= converter.voltages.high[0], "Voltage limits violated" + assert ( + converter.voltages.low[0] <= u <= converter.voltages.high[0] + ), "Voltage limits violated" # test for different cases of (non) ideal behaviour if interlocking_time > 0: test_voltage = test_voltage_interlocking_time[step_counter] @@ -101,18 +306,30 @@ def discrete_converter_functions_testing( else: test_voltage = test_voltage_ideal[step_counter] # g_u_out[step_counter] - assert test_voltage == converter_voltage, "Wrong voltage " + str(step_counter) + assert test_voltage == converter_voltage, "Wrong voltage " + str( + step_counter + ) step_counter += 1 @pytest.mark.parametrize("tau", g_taus) @pytest.mark.parametrize("interlocking_time", g_interlocking_times) -@pytest.mark.parametrize("converter_type, action_space_n, actions, i_ins, test_voltages", - [('Finite-1QC', 2, g_actions_1qc, g_i_ins_1qc, g_1qc_test_voltages), - ('Finite-2QC', 3, g_actions_2qc, g_i_ins_2qc, g_2qc_test_voltages), - ('Finite-4QC', 4, g_actions_4qc, g_i_ins_4qc, g_4qc_test_voltages)]) +@pytest.mark.parametrize( + "converter_type, action_space_n, actions, i_ins, test_voltages", + [ + ("Finite-1QC", 2, g_actions_1qc, g_i_ins_1qc, g_1qc_test_voltages), + ("Finite-2QC", 3, g_actions_2qc, g_i_ins_2qc, g_2qc_test_voltages), + ("Finite-4QC", 4, g_actions_4qc, g_i_ins_4qc, g_4qc_test_voltages), + ], +) def test_discrete_single_power_electronic_converter( - converter_type, action_space_n, actions, i_ins, test_voltages, interlocking_time, tau + converter_type, + action_space_n, + actions, + i_ins, + test_voltages, + interlocking_time, + tau, ): """ test the initialization of all single converters for different tau, interlocking times, @@ -131,7 +348,13 @@ def test_discrete_single_power_electronic_converter( g_times = g_times_4qc times = g_times * converter._tau discrete_converter_functions_testing( - converter, action_space_n, times, actions, i_ins, test_voltages[0], test_voltages[1] + converter, + action_space_n, + times, + actions, + i_ins, + test_voltages[0], + test_voltages[1], ) # define various constants for test @@ -139,27 +362,39 @@ def test_discrete_single_power_electronic_converter( interlocking_time *= tau # setup converter # initialize converter with given parameter - converter = make_module(cv.PowerElectronicConverter, converter_type, tau=tau, - interlocking_time=interlocking_time) + converter = make_module( + cv.PowerElectronicConverter, + converter_type, + tau=tau, + interlocking_time=interlocking_time, + ) assert converter.reset() == [0.0] # test if reset returns 0.0 # test the conversion function of the converter - discrete_converter_functions_testing(converter, action_space_n, times, - actions, - i_ins, - test_voltages[0], - test_voltages[1], - interlocking_time=interlocking_time + discrete_converter_functions_testing( + converter, + action_space_n, + times, + actions, + i_ins, + test_voltages[0], + test_voltages[1], + interlocking_time=interlocking_time, ) -@pytest.mark.parametrize("convert, convert_class", [ - ('Finite-1QC', cv.FiniteOneQuadrantConverter), - ('Finite-2QC', cv.FiniteTwoQuadrantConverter), - ('Finite-4QC', cv.FiniteFourQuadrantConverter) -]) +@pytest.mark.parametrize( + "convert, convert_class", + [ + ("Finite-1QC", cv.FiniteOneQuadrantConverter), + ("Finite-2QC", cv.FiniteTwoQuadrantConverter), + ("Finite-4QC", cv.FiniteFourQuadrantConverter), + ], +) @pytest.mark.parametrize("tau", g_taus) @pytest.mark.parametrize("interlocking_time", g_interlocking_times) -def test_discrete_single_initializations(convert, convert_class, tau, interlocking_time): +def test_discrete_single_initializations( + convert, convert_class, tau, interlocking_time +): """ test of both ways of initialization lead to the same result :param convert: string name of the converter @@ -173,17 +408,23 @@ def test_discrete_single_initializations(convert, convert_class, tau, interlocki # test with different parameters interlocking_time *= tau # initialize converters - converter_1 = make_module(cv.PowerElectronicConverter, convert, tau=tau, - interlocking_time=interlocking_time) - converter_2 = convert_class( - tau=tau, interlocking_time=interlocking_time) + converter_1 = make_module( + cv.PowerElectronicConverter, + convert, + tau=tau, + interlocking_time=interlocking_time, + ) + converter_2 = convert_class(tau=tau, interlocking_time=interlocking_time) parameter = str(tau) + " " + str(interlocking_time) # test if they are equal assert converter_1.reset() == converter_2.reset() assert converter_1.action_space == converter_2.action_space assert converter_1._tau == converter_2._tau == tau, "Error (tau): " + parameter - assert converter_1._interlocking_time == converter_2._interlocking_time == interlocking_time, \ - "Error (interlocking): " + parameter + assert ( + converter_1._interlocking_time + == converter_2._interlocking_time + == interlocking_time + ), "Error (interlocking): " + parameter @pytest.mark.parametrize("tau", g_taus) @@ -194,20 +435,29 @@ def test_discrete_multi_converter_initializations(tau, interlocking_time): :return: """ # define all converter - all_single_disc_converter = ['Finite-1QC', 'Finite-2QC', 'Finite-4QC', 'Finite-B6C'] + all_single_disc_converter = ["Finite-1QC", "Finite-2QC", "Finite-4QC", "Finite-B6C"] interlocking_time *= tau # chose every combination of single converters for conv_1 in all_single_disc_converter: for conv_2 in all_single_disc_converter: converter = make_module( - cv.PowerElectronicConverter, 'Finite-Multi', tau=tau, + cv.PowerElectronicConverter, + "Finite-Multi", + tau=tau, interlocking_time=interlocking_time, - subconverters=[conv_1, conv_2] + subconverters=[conv_1, conv_2], ) # test if both converter have the same parameter - assert converter._sub_converters[0]._tau == converter._sub_converters[1]._tau == tau - assert converter._sub_converters[0]._interlocking_time == converter._sub_converters[1]._interlocking_time \ - == interlocking_time + assert ( + converter._sub_converters[0]._tau + == converter._sub_converters[1]._tau + == tau + ) + assert ( + converter._sub_converters[0]._interlocking_time + == converter._sub_converters[1]._interlocking_time + == interlocking_time + ) @pytest.mark.parametrize("tau", g_taus) @@ -218,26 +468,44 @@ def test_discrete_multi_power_electronic_converter(tau, interlocking_time): :return: """ # define all converter - all_single_disc_converter = ['Finite-1QC', 'Finite-2QC', 'Finite-4QC', 'Finite-B6C'] + all_single_disc_converter = ["Finite-1QC", "Finite-2QC", "Finite-4QC", "Finite-B6C"] interlocking_time *= tau for conv_0 in all_single_disc_converter: for conv_1 in all_single_disc_converter: converter = make_module( - cv.PowerElectronicConverter, 'Finite-Multi', tau=tau, + cv.PowerElectronicConverter, + "Finite-Multi", + tau=tau, + interlocking_time=interlocking_time, + subconverters=[conv_0, conv_1], + ) + comparable_converter_0 = make_module( + cv.PowerElectronicConverter, + conv_0, + tau=tau, + interlocking_time=interlocking_time, + ) + comparable_converter_1 = make_module( + cv.PowerElectronicConverter, + conv_1, + tau=tau, interlocking_time=interlocking_time, - subconverters=[conv_0, conv_1] ) - comparable_converter_0 = make_module(cv.PowerElectronicConverter, conv_0, tau=tau, - interlocking_time=interlocking_time) - comparable_converter_1 = make_module(cv.PowerElectronicConverter, conv_1, tau=tau, - interlocking_time=interlocking_time) action_space_n = converter.action_space.nvec assert np.all( - converter.reset() == - np.concatenate([[-0.5, -0.5, -0.5] if ('Finite-B6C' == conv) else [0] for conv in [conv_0, conv_1]]) + converter.reset() + == np.concatenate( + [ + [-0.5, -0.5, -0.5] if ("Finite-B6C" == conv) else [0] + for conv in [conv_0, conv_1] + ] + ) ) # test if reset returns 0.0 - actions = [[np.random.randint(0, upper_bound) for upper_bound in action_space_n] for _ in range(100)] + actions = [ + [np.random.randint(0, upper_bound) for upper_bound in action_space_n] + for _ in range(100) + ] times = np.arange(100) * tau for action, t in zip(actions, times): time_steps = converter.set_action(action, t) @@ -246,12 +514,20 @@ def test_discrete_multi_power_electronic_converter(tau, interlocking_time): for time_step in time_steps_1 + time_steps_2: assert time_step in time_steps for time_step in time_steps: - i_in_0 = np.random.uniform(-1, 1, 3) if conv_0 == 'Finite-B6C' else [np.random.uniform(-1, 1)] - i_in_1 = np.random.uniform(-1, 1, 3) if conv_1 == 'Finite-B6C' else [np.random.uniform(-1, 1)] + i_in_0 = ( + np.random.uniform(-1, 1, 3) + if conv_0 == "Finite-B6C" + else [np.random.uniform(-1, 1)] + ) + i_in_1 = ( + np.random.uniform(-1, 1, 3) + if conv_1 == "Finite-B6C" + else [np.random.uniform(-1, 1)] + ) i_in = np.concatenate([i_in_0, i_in_1]) voltage = converter.convert(i_in, time_step) # test if the single phase converters work independent and correct for singlephase subsystems - if 'Finite-B6C' not in [conv_0, conv_1]: + if "Finite-B6C" not in [conv_0, conv_1]: voltage_0 = comparable_converter_0.convert(i_in_0, time_step) voltage_1 = comparable_converter_1.convert(i_in_1, time_step) converter.i_sup(i_in) @@ -265,7 +541,7 @@ def test_discrete_multi_power_electronic_converter(tau, interlocking_time): # region continuous converter -@pytest.mark.parametrize("converter_type", ['Cont-1QC', 'Cont-2QC', 'Cont-4QC']) +@pytest.mark.parametrize("converter_type", ["Cont-1QC", "Cont-2QC", "Cont-4QC"]) @pytest.mark.parametrize("tau", g_taus) @pytest.mark.parametrize("interlocking_time", g_interlocking_times) def test_continuous_power_electronic_converter(converter_type, tau, interlocking_time): @@ -276,19 +552,29 @@ def test_continuous_power_electronic_converter(converter_type, tau, interlocking """ interlocking_time *= tau # setup converter - converter = make_module(cv.PowerElectronicConverter, converter_type, tau=tau, - interlocking_time=interlocking_time) + converter = make_module( + cv.PowerElectronicConverter, + converter_type, + tau=tau, + interlocking_time=interlocking_time, + ) assert converter.reset() == [0.0], "Error reset function" action_space = converter.action_space # take 100 random actions seed(123) - actions = [[uniform(action_space.low, action_space.high)] for _ in range(len(g_times_cont))] + actions = [ + [uniform(action_space.low, action_space.high)] for _ in range(len(g_times_cont)) + ] times = g_times_cont * tau - continuous_converter_functions_testing(converter, times, interlocking_time, actions, converter_type) + continuous_converter_functions_testing( + converter, times, interlocking_time, actions, converter_type + ) -def continuous_converter_functions_testing(converter, times, interlocking_time, actions, converter_type): +def continuous_converter_functions_testing( + converter, times, interlocking_time, actions, converter_type +): """ test function for conversion :param converter: instantiated converter @@ -302,35 +588,68 @@ def continuous_converter_functions_testing(converter, times, interlocking_time, last_action = [np.zeros_like(actions[0])] for index, time in enumerate(times): action = actions[index] - parameters = " Error during set action " \ - + str(interlocking_time) + " " + str(action) + " " + str(time) + parameters = ( + " Error during set action " + + str(interlocking_time) + + " " + + str(action) + + " " + + str(time) + ) time_steps = converter.set_action(action, time) for time_step in time_steps: for i_in in g_i_ins_cont: # test if conversion works - if converter_type == 'Cont-1QC': + if converter_type == "Cont-1QC": i_in = abs(i_in) conversion = converter.convert([i_in], time_step) - voltage = comparable_voltage(converter_type, action[0], i_in, tau, interlocking_time, - last_action[0]) - assert abs((voltage[0] - conversion[0])) < 1E-5, "Wrong voltage: " + \ - str([tau, interlocking_time, action, - last_action, time_step, i_in, conversion, - voltage]) - params = parameters + " " + str(i_in) + " " + str(time_step) + " " + str(conversion) - assert (converter.action_space.low.tolist() <= conversion) and \ - (converter.action_space.high.tolist() >= conversion), \ - "Error, does not hold limits:" + str(params) + voltage = comparable_voltage( + converter_type, + action[0], + i_in, + tau, + interlocking_time, + last_action[0], + ) + assert abs((voltage[0] - conversion[0])) < 1e-5, ( + "Wrong voltage: " + + str( + [ + tau, + interlocking_time, + action, + last_action, + time_step, + i_in, + conversion, + voltage, + ] + ) + ) + params = ( + parameters + + " " + + str(i_in) + + " " + + str(time_step) + + " " + + str(conversion) + ) + assert (converter.action_space.low.tolist() <= conversion) and ( + converter.action_space.high.tolist() >= conversion + ), "Error, does not hold limits:" + str(params) last_action = action -def comparable_voltage(converter_type, action, i_in, tau, interlocking_time, last_action): +def comparable_voltage( + converter_type, action, i_in, tau, interlocking_time, last_action +): voltage = np.array([action]) - error = np.array([- np.sign(i_in) / tau * interlocking_time]) - if converter_type == 'Cont-2QC': + error = np.array([-np.sign(i_in) / tau * interlocking_time]) + if converter_type == "Cont-2QC": voltage += error voltage = max(min(voltage, np.array([1])), np.array([0])) - elif converter_type == 'Cont-4QC': + elif converter_type == "Cont-4QC": voltage_1 = (1 + voltage) / 2 + error voltage_2 = (1 - voltage) / 2 + error voltage_1 = max(min(voltage_1, 1), 0) @@ -348,25 +667,41 @@ def test_continuous_multi_power_electronic_converter(tau, interlocking_time): :return: """ # define all converter - all_single_cont_converter = ['Cont-1QC', 'Cont-2QC', 'Cont-4QC', 'Cont-B6C'] + all_single_cont_converter = ["Cont-1QC", "Cont-2QC", "Cont-4QC", "Cont-B6C"] interlocking_time *= tau times = g_times_cont * tau for conv_1 in all_single_cont_converter: for conv_2 in all_single_cont_converter: # setup converter with all possible combinations - converter = make_module(cv.PowerElectronicConverter, 'Cont-Multi', tau=tau, - interlocking_time=interlocking_time, - subconverters=[conv_1, conv_2]) - assert all(converter.reset() == - np.concatenate([[-0.5, -0.5, -0.5] if ('Cont-B6C' == conv) else [0] for conv in [conv_1, conv_2]])) + converter = make_module( + cv.PowerElectronicConverter, + "Cont-Multi", + tau=tau, + interlocking_time=interlocking_time, + subconverters=[conv_1, conv_2], + ) + assert all( + converter.reset() + == np.concatenate( + [ + [-0.5, -0.5, -0.5] if ("Cont-B6C" == conv) else [0] + for conv in [conv_1, conv_2] + ] + ) + ) action_space = converter.action_space seed(123) - actions = [uniform(action_space.low, action_space.high) for _ in range(0, 100)] - continuous_multi_converter_functions_testing(converter, times, interlocking_time, actions, - [conv_1, conv_2]) + actions = [ + uniform(action_space.low, action_space.high) for _ in range(0, 100) + ] + continuous_multi_converter_functions_testing( + converter, times, interlocking_time, actions, [conv_1, conv_2] + ) -def continuous_multi_converter_functions_testing(converter, times, interlocking_time, actions, converter_type): +def continuous_multi_converter_functions_testing( + converter, times, interlocking_time, actions, converter_type +): """ test function for conversion :param converter: instantiated converter @@ -380,32 +715,64 @@ def continuous_multi_converter_functions_testing(converter, times, interlocking_ last_action = np.zeros_like(actions[0]) for index, time in enumerate(times): action = actions[index] - parameters = " Error during set action " \ - + str(interlocking_time) + " " + str(action) + " " + str(time) + parameters = ( + " Error during set action " + + str(interlocking_time) + + " " + + str(action) + + " " + + str(time) + ) time_steps = converter.set_action(action, time) for time_step in time_steps: for i_in in g_i_ins_cont: # test if conversion works - i_in_0 = [i_in] * 3 if converter_type[0] == 'Cont-B6C' else [i_in] - i_in_1 = [-i_in] * 3 if converter_type[1] == 'Cont-B6C' else [i_in] - if converter_type[0] == 'Cont-1QC': + i_in_0 = [i_in] * 3 if converter_type[0] == "Cont-B6C" else [i_in] + i_in_1 = [-i_in] * 3 if converter_type[1] == "Cont-B6C" else [i_in] + if converter_type[0] == "Cont-1QC": i_in_0 = np.abs(i_in_0) - if converter_type[1] == 'Cont-1QC': + if converter_type[1] == "Cont-1QC": i_in_1 = np.abs(i_in_1) - conversion = converter.convert(np.concatenate([i_in_0, i_in_1]), time_step) - params = parameters + " " + str(i_in) + " " + str(time_step) + " " + str(conversion) + conversion = converter.convert( + np.concatenate([i_in_0, i_in_1]), time_step + ) + params = ( + parameters + + " " + + str(i_in) + + " " + + str(time_step) + + " " + + str(conversion) + ) # test if the limits are hold - assert (converter.action_space.low.tolist() <= conversion) and \ - (converter.action_space.high.tolist() >= conversion), \ - "Error, does not hold limits:" + str(params) + assert (converter.action_space.low.tolist() <= conversion) and ( + converter.action_space.high.tolist() >= conversion + ), "Error, does not hold limits:" + str(params) # test if the single phase converters work independent and correct for singlephase subsystems - if 'Cont-B6C' not in converter_type: - voltage_0 = comparable_voltage(converter_type[0], action[0], i_in_0[0], tau, interlocking_time, - last_action[0]) - voltage_1 = comparable_voltage(converter_type[1], action[1], i_in_1[0], tau, interlocking_time, - last_action[1]) - assert abs(voltage_0 - conversion[0]) < 1E-5, "Wrong converter value for armature circuit" - assert abs(voltage_1 - conversion[1]) < 1E-5, "Wrong converter value for excitation circuit" + if "Cont-B6C" not in converter_type: + voltage_0 = comparable_voltage( + converter_type[0], + action[0], + i_in_0[0], + tau, + interlocking_time, + last_action[0], + ) + voltage_1 = comparable_voltage( + converter_type[1], + action[1], + i_in_1[0], + tau, + interlocking_time, + last_action[1], + ) + assert ( + abs(voltage_0 - conversion[0]) < 1e-5 + ), "Wrong converter value for armature circuit" + assert ( + abs(voltage_1 - conversion[1]) < 1e-5 + ), "Wrong converter value for excitation circuit" last_action = action @@ -421,13 +788,13 @@ def test_discrete_b6_bridge(): :return: """ - tau = cf.converter_parameter['tau'] + tau = cf.converter_parameter["tau"] # test default initializations - converter_default_init_1 = make_module(cv.PowerElectronicConverter, 'Finite-B6C') + converter_default_init_1 = make_module(cv.PowerElectronicConverter, "Finite-B6C") converter_default_init_2 = cv.FiniteB6BridgeConverter() - assert converter_default_init_1._tau == 1E-5 + assert converter_default_init_1._tau == 1e-5 for subconverter in converter_default_init_1._sub_converters: - assert subconverter._tau == 1E-5 + assert subconverter._tau == 1e-5 assert subconverter._interlocking_time == 0 # test default initialized converter @@ -455,48 +822,64 @@ def test_discrete_b6_bridge(): for time_step in time_steps: i_in[k] = [i_in_] voltage = converter.convert(i_in, time_step) - assert voltage[k] == 0.5 * u_out[step_counter], "Wrong action " + str(step_counter) + assert voltage[k] == 0.5 * u_out[step_counter], ( + "Wrong action " + str(step_counter) + ) step_counter += 1 # test parametrized converter - converter_init_1 = make_module(cv.PowerElectronicConverter, 'Finite-B6C', **cf.converter_parameter) + converter_init_1 = make_module( + cv.PowerElectronicConverter, "Finite-B6C", **cf.converter_parameter + ) converter_init_2 = cv.FiniteB6BridgeConverter(**cf.converter_parameter) - assert converter_init_1._tau == cf.converter_parameter['tau'] + assert converter_init_1._tau == cf.converter_parameter["tau"] for subconverter in converter_init_1._sub_converters: - assert subconverter._tau == cf.converter_parameter['tau'] - assert subconverter._interlocking_time == cf.converter_parameter['interlocking_time'] + assert subconverter._tau == cf.converter_parameter["tau"] + assert ( + subconverter._interlocking_time + == cf.converter_parameter["interlocking_time"] + ) # set parameter actions = [6, 6, 4, 5, 1, 2, 3, 7, 0, 4] - i_ins = [[[0.5], [0.5], [-0.5]], - [[0], [0.5], [0]], - [[-0.5], [0.5], [-0.5]], - [[0.5], [0.5], [0.5]], - [[0.5], [0.5], [-0.5]], - [[0], [-0.5], [0]], - [[-0.5], [-0.5], [0.5]], - [[-0.5], [-0.5], [-0.5]], - [[0.5], [-0.5], [0]], - [[0.5], [-0.5], [0.5]]] - - expected_voltage = np.array([[1, 1, -1], - [1, 1, -1], - [1, -1, -1], - [1, -1, -1], - [1, -1, -1], - [1, -1, 1], - [-1, -1, 1], - [-1, -1, 1], - [-1, 1, -1], - [-1, 1, -1], - [-1, 1, -1], - [-1, 1, 1], - [1, 1, 1], - [1, 1, 1], - [-1, 1, -1], - [-1, -1, -1], - [-1, -1, -1], - [1, -1, -1], - [1, -1, -1]]) / 2 + i_ins = [ + [[0.5], [0.5], [-0.5]], + [[0], [0.5], [0]], + [[-0.5], [0.5], [-0.5]], + [[0.5], [0.5], [0.5]], + [[0.5], [0.5], [-0.5]], + [[0], [-0.5], [0]], + [[-0.5], [-0.5], [0.5]], + [[-0.5], [-0.5], [-0.5]], + [[0.5], [-0.5], [0]], + [[0.5], [-0.5], [0.5]], + ] + + expected_voltage = ( + np.array( + [ + [1, 1, -1], + [1, 1, -1], + [1, -1, -1], + [1, -1, -1], + [1, -1, -1], + [1, -1, 1], + [-1, -1, 1], + [-1, -1, 1], + [-1, 1, -1], + [-1, 1, -1], + [-1, 1, -1], + [-1, 1, 1], + [1, 1, 1], + [1, 1, 1], + [-1, 1, -1], + [-1, -1, -1], + [-1, -1, -1], + [1, -1, -1], + [1, -1, -1], + ] + ) + / 2 + ) times = np.arange(len(actions)) * tau converters_init = [converter_init_1, converter_init_2] @@ -510,46 +893,54 @@ def test_discrete_b6_bridge(): for time_step in time_steps: voltage = np.array(converter.convert(i_in, time_step)) converter.i_sup(i_in) - assert all(voltage == expected_voltage[step_counter]), "Wrong voltage calculated " + str(step_counter) + assert all(voltage == expected_voltage[step_counter]), ( + "Wrong voltage calculated " + str(step_counter) + ) step_counter += 1 def test_continuous_b6_bridge(): converter_default_init_1 = cv.ContB6BridgeConverter() - converter_default_init_2 = make_module(cv.PowerElectronicConverter, 'Cont-B6C') + converter_default_init_2 = make_module(cv.PowerElectronicConverter, "Cont-B6C") converters_default = [converter_default_init_1, converter_default_init_2] - actions = np.array([[1, -1, 0.65], - [0.75, -0.95, -0.3], - [-0.25, 0.98, -1], - [0.65, 0.5, -0.95], - [-0.3, 0.5, 0.98], - [-1, 1, 0.5], - [-0.95, 0.75, 0.5], - [0.98, -0.25, 1], - [0.5, 0.65, 0.75], - [0.5, -0.3, -0.25]]) - i_ins = [[[0.5], [-0.2], [1]], - [[-0.6], [-0.5], [0.8]], - [[0.3], [0.4], [0.9]], - [[-0.2], [0.7], [0.5]], - [[-0.5], [1], [-0.6]], - [[0.4], [0.8], [0.3]], - [[0.7], [0.9], [-0.2]], - [[1], [0.5], [-0.5]], - [[0.8], [-0.6], [0.4]], - [[0.9], [0.75], [0.7]]] - - times = np.arange(len(actions)) * 1E-4 + actions = np.array( + [ + [1, -1, 0.65], + [0.75, -0.95, -0.3], + [-0.25, 0.98, -1], + [0.65, 0.5, -0.95], + [-0.3, 0.5, 0.98], + [-1, 1, 0.5], + [-0.95, 0.75, 0.5], + [0.98, -0.25, 1], + [0.5, 0.65, 0.75], + [0.5, -0.3, -0.25], + ] + ) + i_ins = [ + [[0.5], [-0.2], [1]], + [[-0.6], [-0.5], [0.8]], + [[0.3], [0.4], [0.9]], + [[-0.2], [0.7], [0.5]], + [[-0.5], [1], [-0.6]], + [[0.4], [0.8], [0.3]], + [[0.7], [0.9], [-0.2]], + [[1], [0.5], [-0.5]], + [[0.8], [-0.6], [0.4]], + [[0.9], [0.75], [0.7]], + ] + + times = np.arange(len(actions)) * 1e-4 for converter in converters_default: # parameter testing - assert converter._tau == 1E-4 + assert converter._tau == 1e-4 assert converter._interlocking_time == 0 assert all(converter.reset() == -0.5 * np.ones(3)) assert converter.action_space.shape == (3,) assert all(converter.action_space.low == -1 * np.ones(3)) assert all(converter.action_space.high == 1 * np.ones(3)) for subconverter in converter._subconverters: - assert subconverter._tau == 1E-4 + assert subconverter._tau == 1e-4 assert subconverter._interlocking_time == 0 # conversion testing for time, action, i_in in zip(times, actions, i_ins): @@ -558,42 +949,53 @@ def test_continuous_b6_bridge(): time_step = converter.set_action(action, time) voltages = converter.convert(i_in, time_step) for voltage, single_action in zip(voltages, action): - assert abs(voltage[0] - single_action / 2) < 1E-9 + assert abs(voltage[0] - single_action / 2) < 1e-9 # testing parametrized converter - expected_voltages = np.array([ - [0.495, -0.495, 0.32], - [0.38, -0.47, -0.155], - [-0.13,0.485, -0.5], - [0.33,0.245, -0.48], - [-0.145, 0.245, 0.495], - [-0.5, 0.495, 0.245], - [-0.48, 0.37, 0.255], - [0.485, -0.13, 0.5], - [0.245, 0.33, 0.37], - [0.245, -0.155, -0.13] - ]) + expected_voltages = np.array( + [ + [0.495, -0.495, 0.32], + [0.38, -0.47, -0.155], + [-0.13, 0.485, -0.5], + [0.33, 0.245, -0.48], + [-0.145, 0.245, 0.495], + [-0.5, 0.495, 0.245], + [-0.48, 0.37, 0.255], + [0.485, -0.13, 0.5], + [0.245, 0.33, 0.37], + [0.245, -0.155, -0.13], + ] + ) converter_init_1 = cv.ContB6BridgeConverter(**cf.converter_parameter) - converter_init_2 = make_module(cv.PowerElectronicConverter, 'Cont-B6C', **cf.converter_parameter) + converter_init_2 = make_module( + cv.PowerElectronicConverter, "Cont-B6C", **cf.converter_parameter + ) converters = [converter_init_1, converter_init_2] for converter in converters: # parameter testing - assert converter._tau == cf.converter_parameter['tau'] - assert converter._interlocking_time == cf.converter_parameter['interlocking_time'] + assert converter._tau == cf.converter_parameter["tau"] + assert ( + converter._interlocking_time == cf.converter_parameter["interlocking_time"] + ) assert all(converter.reset() == -0.5 * np.ones(3)) assert converter.action_space.shape == (3,) assert all(converter.action_space.low == -1 * np.ones(3)) assert all(converter.action_space.high == 1 * np.ones(3)) for subconverter in converter._subconverters: - assert subconverter._tau == cf.converter_parameter['tau'] - assert subconverter._interlocking_time == cf.converter_parameter['interlocking_time'] + assert subconverter._tau == cf.converter_parameter["tau"] + assert ( + subconverter._interlocking_time + == cf.converter_parameter["interlocking_time"] + ) # conversion testing - for time, action, i_in, expected_voltage in zip(times, actions, i_ins, expected_voltages): + for time, action, i_in, expected_voltage in zip( + times, actions, i_ins, expected_voltages + ): i_in = np.array(i_in) time_step = converter.set_action(action.tolist(), time) voltages = converter.convert(i_in, time_step) for voltage, test_voltage in zip(voltages, expected_voltage): - assert abs(voltage - test_voltage) < 1E-9 + assert abs(voltage - test_voltage) < 1e-9 # endregion @@ -606,25 +1008,29 @@ def test_continuous_b6_bridge(): class TestPowerElectronicConverter: - class_to_test = cv.PowerElectronicConverter - key = '' + key = "" @pytest.fixture def converter(self): return self.class_to_test(tau=0, interlocking_time=0) - @pytest.mark.parametrize("tau, interlocking_time", [ - (1, 0.1), - (0.1, 0.0), - ]) + @pytest.mark.parametrize( + "tau, interlocking_time", + [ + (1, 0.1), + (0.1, 0.0), + ], + ) def test_initialization(self, tau, interlocking_time, **kwargs): - converter = self.class_to_test(tau=tau, interlocking_time=interlocking_time, **kwargs) + converter = self.class_to_test( + tau=tau, interlocking_time=interlocking_time, **kwargs + ) assert converter._tau == tau assert converter._interlocking_time == interlocking_time def test_registered(self): - if self.key != '': + if self.key != "": conv = gem.utils.instantiate(cv.PowerElectronicConverter, self.key) assert type(conv) == self.class_to_test @@ -635,7 +1041,9 @@ def test_reset(self, converter): @pytest.mark.parametrize("action_space", [Discrete(3), Box(-1, 1, shape=(1,))]) def test_set_action(self, monkeypatch, converter, action_space): monkeypatch.setattr(converter, "_set_switching_pattern", lambda action: [0.0]) - monkeypatch.setattr(converter, "action_space", converter.action_space or action_space) + monkeypatch.setattr( + converter, "action_space", converter.action_space or action_space + ) next_action = converter.action_space.sample() converter.set_action(next_action, 0) assert np.all(converter._current_action == next_action) @@ -650,7 +1058,6 @@ def test_set_action(self, monkeypatch, converter, action_space): class TestContDynamicallyAveragedConverter(TestPowerElectronicConverter): - class_to_test = cv.ContDynamicallyAveragedConverter @pytest.fixture @@ -658,24 +1065,28 @@ def converter(self, monkeypatch): converter = self.class_to_test(tau=0.1, interlocking_time=0) converter.action_space = converter.action_space or Box(-1, 1, shape=(1,)) if converter.voltages and converter.currents: - converter.voltages = Box(converter.voltages.low[0], converter.voltages.high[0], shape=(1,)) #(converter.voltages.low[0] or -1, converter.voltages.high[0] or -1) - converter.currents = Box(converter.voltages.low[0], converter.voltages.high[0], shape=(1,)) #(converter.currents.low[0] or 0, converter.currents.high[0] or 1) + converter.voltages = Box( + converter.voltages.low[0], converter.voltages.high[0], shape=(1,) + ) # (converter.voltages.low[0] or -1, converter.voltages.high[0] or -1) + converter.currents = Box( + converter.voltages.low[0], converter.voltages.high[0], shape=(1,) + ) # (converter.currents.low[0] or 0, converter.currents.high[0] or 1) else: converter.voltages = Box(-1, -1, shape=(1,)) - converter.currents = Box(0, 1 ,shape=(1,)) + converter.currents = Box(0, 1, shape=(1,)) monkeypatch.setattr(converter, "_convert", lambda i_in, t: i_in[0]) return converter - @pytest.mark.parametrize('i_out', [[-2], [-0.5], [0], [0.5], [2]]) + @pytest.mark.parametrize("i_out", [[-2], [-0.5], [0], [0.5], [2]]) def test_convert(self, monkeypatch, converter, i_out): - monkeypatch.setattr(converter, '_interlocking_time', converter._tau / 10) + monkeypatch.setattr(converter, "_interlocking_time", converter._tau / 10) u = converter.convert(i_out, 0) assert u[0] == min( max( converter._convert(i_out, 0) - converter._interlock(i_out), - converter.voltages.low[0] + converter.voltages.low[0], ), - converter.voltages.high[0] + converter.voltages.high[0], ) @pytest.mark.parametrize("action_space", [Box(-1, 1, shape=(1,))]) @@ -683,10 +1094,14 @@ def test_set_action(self, monkeypatch, converter, action_space): super().test_set_action(monkeypatch, converter, action_space) def test_action_clipping(self, converter): - low_action = converter.action_space.low - np.ones_like(converter.action_space.low) + low_action = converter.action_space.low - np.ones_like( + converter.action_space.low + ) converter.set_action(low_action, 0) assert np.all(converter._current_action == converter.action_space.low) - high_action = converter.action_space.high[0] + np.ones_like(converter.action_space.high) + high_action = converter.action_space.high[0] + np.ones_like( + converter.action_space.high + ) converter.set_action(high_action, 0) assert np.all(converter._current_action == converter.action_space.high) fitting_action = converter.action_space.sample() @@ -696,12 +1111,11 @@ def test_action_clipping(self, converter): @pytest.mark.parametrize("interlocking_time", [0.0, 0.2, 1.0]) def test_interlock(self, monkeypatch, converter, interlocking_time): monkeypatch.setattr(converter, "_interlocking_time", interlocking_time) - assert converter._interlock([-1]) == - interlocking_time / converter._tau + assert converter._interlock([-1]) == -interlocking_time / converter._tau assert converter._interlock([1]) == interlocking_time / converter._tau class TestFiniteConverter(TestPowerElectronicConverter): - class_to_test = cv.FiniteConverter @pytest.mark.parametrize("action_space", [1, 2, 3, 4]) @@ -711,15 +1125,21 @@ def test_set_action(self, monkeypatch, action_space): time = 0 with pytest.raises(AssertionError) as assertText: converter.set_action(-1, time) - assert "-1" in str(assertText.value) and "Discrete(" + str(action_space) + ")" in str(assertText.value) + assert "-1" in str(assertText.value) and "Discrete(" + str( + action_space + ) + ")" in str(assertText.value) with pytest.raises(AssertionError) as assertText: converter.set_action(int(1e9), time) - assert str(int(1e9)) in str(assertText.value) and "Discrete(" + str(action_space) + ")" in str(assertText.value) + assert str(int(1e9)) in str(assertText.value) and "Discrete(" + str( + action_space + ) + ")" in str(assertText.value) with pytest.raises(AssertionError) as assertText: converter.set_action(np.pi, time) - assert str(np.pi) in str(assertText.value) and "Discrete(" + str(action_space) + ")" in str(assertText.value) + assert str(np.pi) in str(assertText.value) and "Discrete(" + str( + action_space + ) + ")" in str(assertText.value) def test_default_init(self): converter = self.class_to_test() @@ -727,9 +1147,8 @@ def test_default_init(self): class TestFiniteOneQuadrantConverter(TestFiniteConverter): - class_to_test = cv.FiniteOneQuadrantConverter - key = 'Finite-1QC' + key = "Finite-1QC" def test_convert(self, converter): action = converter.action_space.sample() @@ -747,7 +1166,7 @@ def test_i_sup(self, converter, i_sup): class TestFiniteTwoQuadrantConverter(TestFiniteConverter): class_to_test = cv.FiniteTwoQuadrantConverter - key = 'Finite-2QC' + key = "Finite-2QC" @pytest.mark.parametrize("interlocking_time", [0.0, 0.1]) def test_set_switching_pattern(self, monkeypatch, converter, interlocking_time): @@ -775,7 +1194,7 @@ def test_set_switching_pattern(self, monkeypatch, converter, interlocking_time): assert switching_times == [converter._tau + 2] assert converter._switching_pattern == [converter._current_action] - @pytest.mark.parametrize('i_out', [[-1], [0], [1]]) + @pytest.mark.parametrize("i_out", [[-1], [0], [1]]) def test_i_sup(self, monkeypatch, converter, i_out): monkeypatch.setattr(converter, "_switching_state", 0) assert converter.i_sup(i_out) == min(i_out[0], 0) @@ -784,11 +1203,11 @@ def test_i_sup(self, monkeypatch, converter, i_out): monkeypatch.setattr(converter, "_switching_state", 2) assert converter.i_sup(i_out) == 0 - @pytest.mark.parametrize('i_out', [[-1], [0], [1]]) + @pytest.mark.parametrize("i_out", [[-1], [0], [1]]) def test_convert(self, monkeypatch, converter, i_out): - monkeypatch.setattr(converter, '_interlocking_time', 0.2) - monkeypatch.setattr(converter, '_action_start_time', 0) - monkeypatch.setattr(converter, '_switching_pattern', [0, 1]) + monkeypatch.setattr(converter, "_interlocking_time", 0.2) + monkeypatch.setattr(converter, "_action_start_time", 0) + monkeypatch.setattr(converter, "_switching_pattern", [0, 1]) for t in np.linspace(0, 1, 10): u = converter.convert(i_out, t) assert converter._switching_state == int(t > converter._interlocking_time) @@ -796,15 +1215,14 @@ def test_convert(self, monkeypatch, converter, i_out): assert u == [int(i_out[0] < 0)] else: assert u == [1] - monkeypatch.setattr(converter, '_switching_pattern', [-1]) + monkeypatch.setattr(converter, "_switching_pattern", [-1]) with pytest.raises(Exception): converter.convert(i_out, 0) class TestFiniteFourQuadrantConverter(TestFiniteConverter): - class_to_test = cv.FiniteFourQuadrantConverter - key = 'Finite-4QC' + key = "Finite-4QC" @pytest.fixture def converter(self): @@ -812,7 +1230,7 @@ def converter(self): tau = converter._tau converter._subconverters = [ PowerElectronicConverterWrapper(converter._subconverters[0], tau=tau), - PowerElectronicConverterWrapper(converter._subconverters[1], tau=tau) + PowerElectronicConverterWrapper(converter._subconverters[1], tau=tau), ] return converter @@ -833,13 +1251,17 @@ def test_set_action(self, converter, *_): with pytest.raises(AssertionError) as assertText: converter.set_action(int(1e9), time) - assert str(int(1e9)) in str(assertText.value) and "Discrete(4)" in str(assertText.value) + assert str(int(1e9)) in str(assertText.value) and "Discrete(4)" in str( + assertText.value + ) with pytest.raises(AssertionError) as assertText: converter.set_action(np.pi, time) - assert str(np.pi) in str(assertText.value) and "Discrete(4)" in str(assertText.value) + assert str(np.pi) in str(assertText.value) and "Discrete(4)" in str( + assertText.value + ) - @pytest.mark.parametrize('i_out', [[-12], [0], [12]]) + @pytest.mark.parametrize("i_out", [[-12], [0], [12]]) def test_convert(self, converter, i_out): t = np.random.rand() converter.set_action(converter.action_space.sample(), t) @@ -848,14 +1270,22 @@ def test_convert(self, converter, i_out): assert converter._subconverters[0].last_i_out == i_out assert converter._subconverters[1].last_t == t assert converter._subconverters[1].last_i_out == [-i_out[0]] - assert u == [converter._subconverters[0].last_u[0] - converter._subconverters[1].last_u[0]] + assert u == [ + converter._subconverters[0].last_u[0] + - converter._subconverters[1].last_u[0] + ] def test_reset(self, converter): reset_calls = [conv.reset_calls for conv in converter._subconverters] super().test_reset(converter) - assert np.all([conv.reset_calls == reset_calls[0] + 1 for conv in converter._subconverters]) + assert np.all( + [ + conv.reset_calls == reset_calls[0] + 1 + for conv in converter._subconverters + ] + ) - @pytest.mark.parametrize('i_out', [[-1], [0], [1]]) + @pytest.mark.parametrize("i_out", [[-1], [0], [1]]) def test_i_sup(self, converter, i_out): for action in range(converter.action_space.n): converter.set_action(action, 0) @@ -863,11 +1293,15 @@ def test_i_sup(self, converter, i_out): i_sup = converter.i_sup(i_out) assert converter._subconverters[0].last_i_out == i_out assert converter._subconverters[1].last_i_out == [-i_out[0]] - assert i_sup == converter._subconverters[0].last_i_sup + converter._subconverters[1].last_i_sup + assert ( + i_sup + == converter._subconverters[0].last_i_sup + + converter._subconverters[1].last_i_sup + ) class TestContOneQuadrantConverter(TestContDynamicallyAveragedConverter): - key = 'Cont-1QC' + key = "Cont-1QC" class_to_test = cv.ContOneQuadrantConverter @pytest.fixture @@ -877,10 +1311,10 @@ def converter(self): def test_interlock(self, monkeypatch, converter, *_): assert converter._interlock(0) == 0 - @pytest.mark.parametrize('i_in', [[-1], [0], [1]]) - @pytest.mark.parametrize('action', [[0], [1]]) + @pytest.mark.parametrize("i_in", [[-1], [0], [1]]) + @pytest.mark.parametrize("action", [[0], [1]]) def test__convert(self, monkeypatch, converter, i_in, action): - monkeypatch.setattr(converter, '_current_action', action) + monkeypatch.setattr(converter, "_current_action", action) u = converter._convert(i_in, 0) if i_in[0] >= 0: assert u == action[0] @@ -890,24 +1324,28 @@ def test__convert(self, monkeypatch, converter, i_in, action): def test_set_action(self, monkeypatch, converter, *_): super().test_set_action(monkeypatch, converter, converter.action_space) - @pytest.mark.parametrize('i_out', [[-1], [1], [0]]) + @pytest.mark.parametrize("i_out", [[-1], [1], [0]]) def test_i_sup(self, monkeypatch, converter, i_out): - monkeypatch.setattr(converter, '_current_action', converter.action_space.sample()) + monkeypatch.setattr( + converter, "_current_action", converter.action_space.sample() + ) assert converter.i_sup(i_out) == converter._current_action[0] * i_out[0] class TestContTwoQuadrantConverter(TestContDynamicallyAveragedConverter): class_to_test = cv.ContTwoQuadrantConverter - key = 'Cont-2QC' + key = "Cont-2QC" - @pytest.mark.parametrize('interlocking_time', [0.0, 0.1, 1]) - @pytest.mark.parametrize('i_out', [[0.0], [0.1], [-1]]) - @pytest.mark.parametrize('tau', [1, 2]) - @pytest.mark.parametrize('action', [[0], [0.5], [1]]) + @pytest.mark.parametrize("interlocking_time", [0.0, 0.1, 1]) + @pytest.mark.parametrize("i_out", [[0.0], [0.1], [-1]]) + @pytest.mark.parametrize("tau", [1, 2]) + @pytest.mark.parametrize("action", [[0], [0.5], [1]]) def test_i_sup(self, monkeypatch, converter, interlocking_time, i_out, tau, action): - monkeypatch.setattr(converter, '_interlocking_time', min(tau, interlocking_time)) - monkeypatch.setattr(converter, '_tau', tau) - monkeypatch.setattr(converter, '_current_action', action) + monkeypatch.setattr( + converter, "_interlocking_time", min(tau, interlocking_time) + ) + monkeypatch.setattr(converter, "_tau", tau) + monkeypatch.setattr(converter, "_current_action", action) i_sup = converter.i_sup(i_out) assert abs(i_sup) <= abs(i_out[0]) if interlocking_time == 0: @@ -920,7 +1358,7 @@ def test_i_sup(self, monkeypatch, converter, interlocking_time, i_out, tau, acti class TestContFourQuadrantConverter(TestContDynamicallyAveragedConverter): class_to_test = cv.ContFourQuadrantConverter - key = 'Cont-4QC' + key = "Cont-4QC" @pytest.fixture def converter(self, monkeypatch): @@ -929,15 +1367,19 @@ def converter(self, monkeypatch): PowerElectronicConverterWrapper(subconverter, tau=converter._tau) for subconverter in converter._subconverters ] - monkeypatch.setattr(converter, '_subconverters', subconverters) + monkeypatch.setattr(converter, "_subconverters", subconverters) return converter - @pytest.mark.parametrize('i_out', [[-2], [-0.5], [0], [0.5], [2]]) + @pytest.mark.parametrize("i_out", [[-2], [-0.5], [0], [0.5], [2]]) def test_convert(self, monkeypatch, converter, i_out): converter.set_action(converter.action_space.sample(), 0) for _ in np.linspace(0, converter._tau): u = converter.convert(i_out, 0) - assert u[0] == converter._subconverters[0].last_u[0] - converter._subconverters[1].last_u[0] + assert ( + u[0] + == converter._subconverters[0].last_u[0] + - converter._subconverters[1].last_u[0] + ) def test_reset(self, converter): u = converter.reset() @@ -955,7 +1397,7 @@ def test_set_action(self, monkeypatch, converter, *_): assert action[0] == 2 * sc1.last_action[0] - 1 assert action[0] == -2 * sc2.last_action[0] + 1 - @pytest.mark.parametrize('i_out', [[0], [1], [-2]]) + @pytest.mark.parametrize("i_out", [[0], [1], [-2]]) def test_i_sup(self, converter, i_out): sc1, sc2 = converter._subconverters converter.set_action(converter.action_space.sample(), np.random.rand()) @@ -967,75 +1409,115 @@ def test_i_sup(self, converter, i_out): class TestFiniteMultiConverter(TestFiniteConverter): class_to_test = cv.FiniteMultiConverter - key = 'Finite-Multi' + key = "Finite-Multi" @pytest.fixture def converter(self): - return self.class_to_test(subconverters=[ - DummyConverter(action_space=Discrete(3), voltages=Box(-1, 1, shape=(1,)), currents=Box(0, 1, shape=(1,))), - DummyConverter(action_space=Discrete(8), voltages=Box(-1, 1, shape=(3,)), currents=Box(-1, 1, shape=(3,))), - DummyConverter(action_space=Discrete(4), voltages=Box(-1, 1, shape=(1,)), currents=Box(-1, 1, shape=(1,))), - ]) - - @pytest.mark.parametrize("tau, interlocking_time, kwargs", [ - (1, 0.1, {'subconverters': ['Finite-1QC', 'Finite-B6C', 'Finite-4QC']}), - (0.1, 0.0, {'subconverters': ['Finite-1QC', 'Finite-B6C', 'Finite-4QC']}), - ]) + return self.class_to_test( + subconverters=[ + DummyConverter( + action_space=Discrete(3), + voltages=Box(-1, 1, shape=(1,)), + currents=Box(0, 1, shape=(1,)), + ), + DummyConverter( + action_space=Discrete(8), + voltages=Box(-1, 1, shape=(3,)), + currents=Box(-1, 1, shape=(3,)), + ), + DummyConverter( + action_space=Discrete(4), + voltages=Box(-1, 1, shape=(1,)), + currents=Box(-1, 1, shape=(1,)), + ), + ] + ) + + @pytest.mark.parametrize( + "tau, interlocking_time, kwargs", + [ + (1, 0.1, {"subconverters": ["Finite-1QC", "Finite-B6C", "Finite-4QC"]}), + (0.1, 0.0, {"subconverters": ["Finite-1QC", "Finite-B6C", "Finite-4QC"]}), + ], + ) def test_initialization(self, tau, interlocking_time, kwargs): super().test_initialization(tau, interlocking_time, **kwargs) - conv = self.class_to_test(tau=tau, interlocking_time=interlocking_time, **kwargs) + conv = self.class_to_test( + tau=tau, interlocking_time=interlocking_time, **kwargs + ) assert np.all( - conv.subsignal_voltage_space_dims == - np.array([(np.squeeze(subconv.voltages.shape) or 1) for subconv in conv._sub_converters]) + conv.subsignal_voltage_space_dims + == np.array( + [ + (np.squeeze(subconv.voltages.shape) or 1) + for subconv in conv._sub_converters + ] + ) ), "Voltage space dims in the multi converter do not fit the subconverters." assert np.all( - conv.subsignal_current_space_dims == - np.array([(np.squeeze(subconv.currents.shape) or 1) for subconv in conv._sub_converters]) + conv.subsignal_current_space_dims + == np.array( + [ + (np.squeeze(subconv.currents.shape) or 1) + for subconv in conv._sub_converters + ] + ) ), "Current space dims in the multi converter do not fit the subconverters." - assert np.all(conv.action_space.nvec == [subconv.action_space.n for subconv in conv._sub_converters] - ), "Action space of the multi converter does not fit the subconverters." + assert np.all( + conv.action_space.nvec + == [subconv.action_space.n for subconv in conv._sub_converters] + ), "Action space of the multi converter does not fit the subconverters." for sc in conv._sub_converters: assert sc._interlocking_time == interlocking_time assert sc._tau == tau def test_registered(self): dummy_converters = [DummyConverter(), DummyConverter(), DummyConverter()] - conv = gem.utils.instantiate(cv.PowerElectronicConverter, self.key, subconverters=dummy_converters) + conv = gem.utils.instantiate( + cv.PowerElectronicConverter, self.key, subconverters=dummy_converters + ) assert type(conv) == self.class_to_test def test_reset(self, converter): u_in = converter.reset() assert u_in == [0.0] * converter.voltages.shape[0] - assert np.all([subconv.reset_counter == 1 for subconv in converter._sub_converters]) + assert np.all( + [subconv.reset_counter == 1 for subconv in converter._sub_converters] + ) def test_set_action(self, monkeypatch, converter, **_): for action in np.ndindex(tuple(converter.action_space.nvec)): sc0 = converter._sub_converters[0] sc1 = converter._sub_converters[1] sc2 = converter._sub_converters[2] - t = (action[0] * sc1.action_space.n * sc2.action_space.n + - action[1] * sc2.action_space.n + action[2])* converter._tau + t = ( + action[0] * sc1.action_space.n * sc2.action_space.n + + action[1] * sc2.action_space.n + + action[2] + ) * converter._tau converter.set_action(action, t) - assert np.all((sc0.action, sc1.action, sc2.action) == action) + assert np.all((sc0.action, sc1.action, sc2.action) == action) assert sc0.action_set_time == t assert sc1.action_set_time == t assert sc2.action_set_time == t def test_default_init(self): - converter = self.class_to_test(subconverters=['Finite-1QC', 'Finite-B6C', 'Finite-2QC']) + converter = self.class_to_test( + subconverters=["Finite-1QC", "Finite-B6C", "Finite-2QC"] + ) assert converter._tau == 1e-5 - @pytest.mark.parametrize('i_out', [[0, 6, 2, 7, 9], [1, 0.5, 2], [-1, 1]]) + @pytest.mark.parametrize("i_out", [[0, 6, 2, 7, 9], [1, 0.5, 2], [-1, 1]]) def test_convert(self, converter, i_out): # Setting of the demanded output voltages from the dummy converters for subconverter in converter._sub_converters: subconverter.action = subconverter.action_space.sample() u = converter.convert(i_out, 0) - assert np.all( - u == [subconv.action for subconv in converter._sub_converters] - ) + assert np.all(u == [subconv.action for subconv in converter._sub_converters]) - @pytest.mark.parametrize('i_out', [[0, 6, 2, 7, 9], [1, 0.5, 2, -7, 50], [-1, 1, 0.01, 16, -42]]) + @pytest.mark.parametrize( + "i_out", [[0, 6, 2, 7, 9], [1, 0.5, 2, -7, 50], [-1, 1, 0.01, 16, -42]] + ) def test_i_sup(self, converter, i_out): sc0, sc1, sc2 = converter._sub_converters converter.set_action(converter.action_space.sample(), np.random.rand()) @@ -1046,51 +1528,82 @@ def test_i_sup(self, converter, i_out): class TestContMultiConverter(TestContDynamicallyAveragedConverter): class_to_test = cv.ContMultiConverter - key = 'Cont-Multi' + key = "Cont-Multi" @pytest.fixture def converter(self): - return self.class_to_test(subconverters=[ - DummyConverter( - action_space=Box(-1, 1, shape=(1,)), voltages=Box(-1, 1, shape=(1,)), currents=Box(-1, 1, shape=(1,)) - ), - DummyConverter( - action_space=Box(-1, 1, shape=(3,)), voltages=Box(-1, 1, shape=(3,)), currents=Box(-1, 1, shape=(3,)) - ), - DummyConverter( - action_space=Box(0, 1, shape=(1,)), voltages=Box(0, 1, shape=(1,)), currents=Box(0, 1, shape=(1,)) - ), - ]) + return self.class_to_test( + subconverters=[ + DummyConverter( + action_space=Box(-1, 1, shape=(1,)), + voltages=Box(-1, 1, shape=(1,)), + currents=Box(-1, 1, shape=(1,)), + ), + DummyConverter( + action_space=Box(-1, 1, shape=(3,)), + voltages=Box(-1, 1, shape=(3,)), + currents=Box(-1, 1, shape=(3,)), + ), + DummyConverter( + action_space=Box(0, 1, shape=(1,)), + voltages=Box(0, 1, shape=(1,)), + currents=Box(0, 1, shape=(1,)), + ), + ] + ) def test_registered(self): dummy_converters = [DummyConverter(), DummyConverter(), DummyConverter()] dummy_converters[0].action_space = Box(-1, 1, shape=(1,)) dummy_converters[1].action_space = Box(-1, 1, shape=(3,)) dummy_converters[2].action_space = Box(0, 1, shape=(1,)) - conv = gem.utils.instantiate(cv.PowerElectronicConverter, self.key, subconverters=dummy_converters) + conv = gem.utils.instantiate( + cv.PowerElectronicConverter, self.key, subconverters=dummy_converters + ) assert type(conv) == self.class_to_test assert conv._sub_converters == dummy_converters - @pytest.mark.parametrize("tau, interlocking_time, kwargs", [ - (1, 0.1, {'subconverters': ['Cont-1QC', 'Cont-B6C', 'Cont-4QC']}), - (0.1, 0.0, {'subconverters': ['Cont-1QC', 'Cont-B6C', 'Cont-4QC']}), - ]) + @pytest.mark.parametrize( + "tau, interlocking_time, kwargs", + [ + (1, 0.1, {"subconverters": ["Cont-1QC", "Cont-B6C", "Cont-4QC"]}), + (0.1, 0.0, {"subconverters": ["Cont-1QC", "Cont-B6C", "Cont-4QC"]}), + ], + ) def test_initialization(self, tau, interlocking_time, kwargs): super().test_initialization(tau, interlocking_time, **kwargs) - conv = self.class_to_test(tau=tau, interlocking_time=interlocking_time, **kwargs) + conv = self.class_to_test( + tau=tau, interlocking_time=interlocking_time, **kwargs + ) assert np.all( - conv.action_space.low == np.concatenate([subconv.action_space.low for subconv in conv._sub_converters]) + conv.action_space.low + == np.concatenate( + [subconv.action_space.low for subconv in conv._sub_converters] + ) ), "Action space lower boundaries in the multi converter do not fit the subconverters." assert np.all( - conv.action_space.high == np.concatenate([subconv.action_space.high for subconv in conv._sub_converters]) + conv.action_space.high + == np.concatenate( + [subconv.action_space.high for subconv in conv._sub_converters] + ) ), "Action space upper boundaries in the multi converter do not fit the subconverters." assert np.all( - conv.subsignal_voltage_space_dims == - np.array([(np.squeeze(subconv.voltages.shape) or 1) for subconv in conv._sub_converters]) + conv.subsignal_voltage_space_dims + == np.array( + [ + (np.squeeze(subconv.voltages.shape) or 1) + for subconv in conv._sub_converters + ] + ) ), "Voltage space dims in the multi converter do not fit the subconverters." assert np.all( - conv.subsignal_current_space_dims == - np.array([(np.squeeze(subconv.currents.shape) or 1) for subconv in conv._sub_converters]) + conv.subsignal_current_space_dims + == np.array( + [ + (np.squeeze(subconv.currents.shape) or 1) + for subconv in conv._sub_converters + ] + ) ), "Current space dims in the multi converter do not fit the subconverters." for sc in conv._sub_converters: assert sc._interlocking_time == interlocking_time @@ -1099,13 +1612,18 @@ def test_initialization(self, tau, interlocking_time, kwargs): def test_reset(self, converter): u_in = converter.reset() assert u_in == [0.0] * converter.voltages.shape[0] - assert np.all([subconv.reset_counter == 1 for subconv in converter._sub_converters]) + assert np.all( + [subconv.reset_counter == 1 for subconv in converter._sub_converters] + ) def test_action_clipping(self, converter): # Done by the subconverters pass - @pytest.mark.parametrize('action', [[0, 0, 0, 0, 0], [0, 0, 1, 1, 1], [-1, 1, -1, 1, -1], [1, 1, 1, 1, 1], []]) + @pytest.mark.parametrize( + "action", + [[0, 0, 0, 0, 0], [0, 0, 1, 1, 1], [-1, 1, -1, 1, -1], [1, 1, 1, 1, 1], []], + ) def test_set_action(self, monkeypatch, converter, action): t = np.random.randint(10) * converter._tau converter.set_action(action, t) @@ -1116,8 +1634,17 @@ def test_set_action(self, monkeypatch, converter, action): assert sc0.action_set_time == t assert sc1.action_set_time == t - @pytest.mark.parametrize('i_out', [[-2, 2, 0, -1, 1], [-0.5, 5, -7, 0, 2], [0, 1, 3, -0.7, -4], - [], [2, 1, -2], [2, 9, 7, -4, 9, 41, 17]]) + @pytest.mark.parametrize( + "i_out", + [ + [-2, 2, 0, -1, 1], + [-0.5, 5, -7, 0, 2], + [0, 1, 3, -0.7, -4], + [], + [2, 1, -2], + [2, 9, 7, -4, 9, 41, 17], + ], + ) def test_convert(self, monkeypatch, converter, i_out): t = np.random.rand() action_space_size = [1, 3, 1] @@ -1125,7 +1652,9 @@ def test_convert(self, monkeypatch, converter, i_out): u = converter.convert(i_out, t) sub_u = [] start_idx = 0 - for subconverter, subaction_space_size in zip(converter._sub_converters, action_space_size): + for subconverter, subaction_space_size in zip( + converter._sub_converters, action_space_size + ): end_idx = start_idx + subaction_space_size assert subconverter.i_out == i_out[start_idx:end_idx] start_idx = end_idx @@ -1136,23 +1665,29 @@ def test_convert(self, monkeypatch, converter, i_out): class TestFiniteB6BridgeConverter(TestFiniteConverter): class_to_test = cv.FiniteB6BridgeConverter - key = 'Finite-B6C' + key = "Finite-B6C" @pytest.fixture def converter(self): conv = self.class_to_test() subconverters = [ - PowerElectronicConverterWrapper(subconverter, tau=conv._tau) for subconverter in conv._sub_converters + PowerElectronicConverterWrapper(subconverter, tau=conv._tau) + for subconverter in conv._sub_converters ] conv._sub_converters = subconverters return conv - @pytest.mark.parametrize("tau, interlocking_time, kwargs", [ - (1, 0.1, {}), - (0.1, 0.0, {}), - ]) + @pytest.mark.parametrize( + "tau, interlocking_time, kwargs", + [ + (1, 0.1, {}), + (0.1, 0.0, {}), + ], + ) def test_subconverter_initialization(self, tau, interlocking_time, kwargs): - conv = self.class_to_test(tau=tau, interlocking_time=interlocking_time, **kwargs) + conv = self.class_to_test( + tau=tau, interlocking_time=interlocking_time, **kwargs + ) for sc in conv._sub_converters: assert sc._interlocking_time == interlocking_time assert sc._tau == tau @@ -1172,7 +1707,7 @@ def test_set_action(self, converter, *_): assert converter._sub_converters[1].last_t == t assert converter._sub_converters[2].last_t == t subactions = [sc.last_action % 2 for sc in converter._sub_converters] - assert action == reduce(lambda x, y: 2*x+y, subactions) + assert action == reduce(lambda x, y: 2 * x + y, subactions) time = 0 with pytest.raises(AssertionError) as assertText: @@ -1181,13 +1716,17 @@ def test_set_action(self, converter, *_): with pytest.raises(AssertionError) as assertText: converter.set_action(int(1e9), time) - assert str(int(1e9)) in str(assertText.value) and "Discrete(8)" in str(assertText.value) + assert str(int(1e9)) in str(assertText.value) and "Discrete(8)" in str( + assertText.value + ) with pytest.raises(AssertionError) as assertText: converter.set_action(np.pi, time) - assert str(np.pi) in str(assertText.value) and "Discrete(8)" in str(assertText.value) + assert str(np.pi) in str(assertText.value) and "Discrete(8)" in str( + assertText.value + ) - @pytest.mark.parametrize('i_out', [[-1, -1, 0], [1, 1, -2], [0, 0, 1]]) + @pytest.mark.parametrize("i_out", [[-1, -1, 0], [1, 1, -2], [0, 0, 1]]) def test_convert(self, converter, i_out): t = np.random.rand() sc1, sc2, sc3 = converter._sub_converters @@ -1200,7 +1739,7 @@ def test_convert(self, converter, i_out): assert sc2.last_u[0] - 0.5 == u_out[1] assert sc3.last_u[0] - 0.5 == u_out[2] - @pytest.mark.parametrize('i_out', [[-1, -1, 0], [1, 1, -2], [0, 0, 1]]) + @pytest.mark.parametrize("i_out", [[-1, -1, 0], [1, 1, -2], [0, 0, 1]]) def test_i_sup(self, converter, i_out): sc1, sc2, sc3 = converter._sub_converters for action in range(converter.action_space.n): @@ -1211,14 +1750,15 @@ def test_i_sup(self, converter, i_out): class TestContB6BridgeConverter(TestContDynamicallyAveragedConverter): - key = 'Cont-B6C' + key = "Cont-B6C" class_to_test = cv.ContB6BridgeConverter @pytest.fixture def converter(self): conv = self.class_to_test() subconverters = [ - PowerElectronicConverterWrapper(subconverter, tau=conv._tau) for subconverter in conv._subconverters + PowerElectronicConverterWrapper(subconverter, tau=conv._tau) + for subconverter in conv._subconverters ] conv._subconverters = subconverters return conv @@ -1234,7 +1774,7 @@ def test_reset(self, converter): ) assert u_init == [-0.5] * 3 - @pytest.mark.parametrize('i_out', [[-1, -1, 0], [1, 1, -2], [0, 0, 1]]) + @pytest.mark.parametrize("i_out", [[-1, -1, 0], [1, 1, -2], [0, 0, 1]]) def test_convert(self, converter, i_out): t = np.random.rand() sc1, sc2, sc3 = converter._subconverters @@ -1259,7 +1799,7 @@ def test_set_action(self, monkeypatch, converter, *_): assert sc2.last_action == action[2] * 0.5 + 0.5 assert sc0.last_t == sc1.last_t == sc2.last_t == t - @pytest.mark.parametrize('i_out', [[-1, -1, 0], [1, 1, -2], [0, 0, 1]]) + @pytest.mark.parametrize("i_out", [[-1, -1, 0], [1, 1, -2], [0, 0, 1]]) def test_i_sup(self, converter, i_out): sc1, sc2, sc3 = converter._subconverters for n in range(10): diff --git a/tests/test_physical_systems/test_load.py b/tests/test_physical_systems/test_load.py index c20d3ba6..47f5b7a1 100644 --- a/tests/test_physical_systems/test_load.py +++ b/tests/test_physical_systems/test_load.py @@ -1,4 +1,7 @@ -from gym_electric_motor.physical_systems.mechanical_loads import PolynomialStaticLoad, MechanicalLoad +from gym_electric_motor.physical_systems.mechanical_loads import ( + PolynomialStaticLoad, + MechanicalLoad, +) from ..conf import * import numpy as np import pytest @@ -6,18 +9,19 @@ # region first version tests + def test_mechanical_load(): """ test mechanical load for different use cases :return: """ - state_names = load_parameter['state_names'] + state_names = load_parameter["state_names"] # example for one random motor state_positions = permex_state_positions - nominal_state = permex_motor_parameter['nominal_values'] - j_load = load_parameter['j_load'] - j_rotor = load_parameter['j_rot_load'] - omega_range = load_parameter['omega_range'] + nominal_state = permex_motor_parameter["nominal_values"] + j_load = load_parameter["j_load"] + j_rotor = load_parameter["j_rot_load"] + omega_range = load_parameter["omega_range"] # initialize loads load_default = MechanicalLoad() load_init = MechanicalLoad(state_names, j_load) @@ -28,11 +32,10 @@ def test_mechanical_load(): # test state space state_space = load.get_state_space(omega_range) reset_state_space = Box(low=0, high=1.0, shape=(1,)) - assert state_space[0]['omega'] == omega_range[0] - assert state_space[1]['omega'] == omega_range[1] + assert state_space[0]["omega"] == omega_range[0] + assert state_space[1]["omega"] == omega_range[1] # test reset - assert 0 == load.reset(reset_state_space, state_positions, - nominal_state) + assert 0 == load.reset(reset_state_space, state_positions, nominal_state) # test not existing mechanical ode with pytest.raises(NotImplementedError): load.mechanical_ode(0, 0, 0) @@ -46,21 +49,24 @@ def test_polynomial_load(): :return: """ - state_names = load_parameter['state_names'] + state_names = load_parameter["state_names"] # example for one random motor state_positions = permex_state_positions - nominal_state = permex_motor_parameter['nominal_values'] - j_rotor = load_parameter['j_rot_load'] - omega_range = load_parameter['omega_range'] + nominal_state = permex_motor_parameter["nominal_values"] + j_rotor = load_parameter["j_rot_load"] + omega_range = load_parameter["omega_range"] # initialization loads in different ways load_default = PolynomialStaticLoad() - load_init = PolynomialStaticLoad(load_parameter=load_parameter['parameter']) + load_init = PolynomialStaticLoad(load_parameter=load_parameter["parameter"]) # test initialization of parametrized load function - assert load_init.load_parameter == load_parameter['parameter'], "Wrong Parameter " + str(load_init.load_parameter) + \ - str(load_parameter['parameter']) - assert load_init._a == load_parameter['parameter']['a'] - assert load_init._b == load_parameter['parameter']['b'] - assert load_init._c == load_parameter['parameter']['c'] + assert load_init.load_parameter == load_parameter["parameter"], ( + "Wrong Parameter " + + str(load_init.load_parameter) + + str(load_parameter["parameter"]) + ) + assert load_init._a == load_parameter["parameter"]["a"] + assert load_init._b == load_parameter["parameter"]["b"] + assert load_init._c == load_parameter["parameter"]["c"] # test different loads loads = [load_default, load_init] for load in loads: @@ -68,8 +74,8 @@ def test_polynomial_load(): state_space = load.get_state_space(omega_range) reset_state_space = Box(low=0, high=1.0, shape=(len(state_names),)) assert 0 == load.reset(reset_state_space, state_positions, nominal_state) - assert state_space[0]['omega'] == omega_range[0] - assert state_space[1]['omega'] == omega_range[1] + assert state_space[0]["omega"] == omega_range[0] + assert state_space[1]["omega"] == omega_range[1] a = load._a b = load._b c = load._c @@ -80,8 +86,9 @@ def test_polynomial_load(): mechanical_state = np.array([omega]) # test load ode for torque in [-3, 0, 5]: - assert load.mechanical_ode(0, mechanical_state, torque) == np.array([(torque - electrical_torque) / - j_total]) + assert load.mechanical_ode(0, mechanical_state, torque) == np.array( + [(torque - electrical_torque) / j_total] + ) # endregion diff --git a/tests/test_physical_systems/test_mechanical_loads.py b/tests/test_physical_systems/test_mechanical_loads.py index d7dfc849..6ef60bd0 100644 --- a/tests/test_physical_systems/test_mechanical_loads.py +++ b/tests/test_physical_systems/test_mechanical_loads.py @@ -1,23 +1,37 @@ import pytest import gym_electric_motor as gem -from gym_electric_motor.physical_systems import PolynomialStaticLoad, MechanicalLoad, ConstantSpeedLoad, ExternalSpeedLoad +from gym_electric_motor.physical_systems import ( + PolynomialStaticLoad, + MechanicalLoad, + ConstantSpeedLoad, + ExternalSpeedLoad, +) from gymnasium.spaces import Box import numpy as np from scipy import signal import math # The load parameter values used in the test -load_parameter1 = {'j_load': 0.2, 'state_names': ['omega'], 'j_rot_load': 0.25, 'omega_range': (0, 1), - 'parameter': dict(a=0.12, b=0.13, c=0.4, j_load=0.2)} +load_parameter1 = { + "j_load": 0.2, + "state_names": ["omega"], + "j_rot_load": 0.25, + "omega_range": (0, 1), + "parameter": dict(a=0.12, b=0.13, c=0.4, j_load=0.2), +} # The different initializers used in test -test_const_initializer = {'states': {'omega': 15.0}, - 'interval': None, - 'random_init': None, - 'random_params': (None, None)} +test_const_initializer = { + "states": {"omega": 15.0}, + "interval": None, + "random_init": None, + "random_params": (None, None), +} # todo random init as testcase ? -test_rand_initializer = { 'interval': None, - 'random_init': None, - 'random_params': (None, None)} +test_rand_initializer = { + "interval": None, + "random_init": None, + "random_params": (None, None), +} # profile_parameter test_amp = 20 test_bias = 10 @@ -25,7 +39,7 @@ def speed_profile_(t, amp, freq, bias): - return amp*signal.sawtooth(2*np.pi*freq*t, width=0.5)+bias + return amp * signal.sawtooth(2 * np.pi * freq * t, width=0.5) + bias @pytest.fixture @@ -44,8 +58,8 @@ def concreteMechanicalLoad(): :return: MechanicalLoad object initialized with concrete values """ # Parameters picked from the parameter dict above - state_names = load_parameter1['state_names'] - j_load = load_parameter1['j_load'] + state_names = load_parameter1["state_names"] + j_load = load_parameter1["j_load"] test_initializer = test_const_initializer return MechanicalLoad(state_names, j_load, load_initializer=test_initializer) @@ -68,7 +82,9 @@ def concretePolynomialLoad(): test_load_params = dict(a=0.01, b=0.05, c=0.1, j_load=0.1) test_initializer = test_const_initializer # x, y are random kwargs - return PolynomialStaticLoad(load_parameter=test_load_params, load_initializer=test_initializer) + return PolynomialStaticLoad( + load_parameter=test_load_params, load_initializer=test_initializer + ) def test_InitMechanicalLoad(defaultMechanicalLoad, concreteMechanicalLoad): @@ -78,12 +94,12 @@ def test_InitMechanicalLoad(defaultMechanicalLoad, concreteMechanicalLoad): :param concreteMechanicalLoad: :return: """ - assert defaultMechanicalLoad.state_names == load_parameter1['state_names'] + assert defaultMechanicalLoad.state_names == load_parameter1["state_names"] assert defaultMechanicalLoad.j_total == 0 assert defaultMechanicalLoad.limits == {} assert defaultMechanicalLoad.initializer == {} - assert concreteMechanicalLoad.state_names == load_parameter1['state_names'] - assert concreteMechanicalLoad.j_total == load_parameter1['j_load'] + assert concreteMechanicalLoad.state_names == load_parameter1["state_names"] + assert concreteMechanicalLoad.j_total == load_parameter1["j_load"] assert concreteMechanicalLoad.limits == {} assert concreteMechanicalLoad.initializer == test_const_initializer @@ -96,7 +112,7 @@ def test_MechanicalLoad_set_j_rotor(concreteMechanicalLoad): """ test_val = 1 concreteMechanicalLoad.set_j_rotor(test_val) - assert concreteMechanicalLoad.j_total == test_val + load_parameter1['j_load'] + assert concreteMechanicalLoad.j_total == test_val + load_parameter1["j_load"] def test_MechanicalLoad_MechanicalOde(concreteMechanicalLoad): @@ -109,6 +125,7 @@ def test_MechanicalLoad_MechanicalOde(concreteMechanicalLoad): with pytest.raises(NotImplementedError): concreteMechanicalLoad.mechanical_ode(*test_args) + def test_MechanicalLoad_reset(concreteMechanicalLoad): """ Test the reset() function @@ -116,17 +133,19 @@ def test_MechanicalLoad_reset(concreteMechanicalLoad): :return: """ # random testcase for the necessary parameters needed for initialization - test_positions = {'omega': 0} + test_positions = {"omega": 0} test_nominal = np.array([80]) # gymnasium.Box state space with random size test_space = Box(low=-1.0, high=1.0, shape=(3,)) # set additional random kwargs - resetVal = concreteMechanicalLoad.reset(a=7, b=9, - state_positions=test_positions, - nominal_state=test_nominal, - state_space=test_space - ) - testVal = np.asarray(list(test_const_initializer['states'].values())) + resetVal = concreteMechanicalLoad.reset( + a=7, + b=9, + state_positions=test_positions, + nominal_state=test_nominal, + state_space=test_space, + ) + testVal = np.asarray(list(test_const_initializer["states"].values())) assert (resetVal == testVal).all() @@ -139,12 +158,14 @@ def test_MechanicalLoad_get_state_space(concreteMechanicalLoad): test_omega_range = 0, 5 test_omega_range_2 = 1, 4, 6, 7 retVal = concreteMechanicalLoad.get_state_space(test_omega_range) - assert retVal[0]['omega'] == test_omega_range[0] - assert retVal[1]['omega'] == test_omega_range[1] + assert retVal[0]["omega"] == test_omega_range[0] + assert retVal[1]["omega"] == test_omega_range[1] - retVal2 = concreteMechanicalLoad.get_state_space(test_omega_range_2) # additional negative test - assert retVal2[0]['omega'] == test_omega_range_2[0] - assert retVal2[1]['omega'] == test_omega_range_2[1] + retVal2 = concreteMechanicalLoad.get_state_space( + test_omega_range_2 + ) # additional negative test + assert retVal2[0]["omega"] == test_omega_range_2[0] + assert retVal2[1]["omega"] == test_omega_range_2[1] def test_InitPolynomialStaticLoad(defaultPolynomialLoad): @@ -153,7 +174,7 @@ def test_InitPolynomialStaticLoad(defaultPolynomialLoad): :param defaultPolynomialLoad: :return: """ - test_default_load_parameterVal = {'a': 0.01, 'b': 0.05, 'c': 0.1, 'j_load': 0.1} + test_default_load_parameterVal = {"a": 0.01, "b": 0.05, "c": 0.1, "j_load": 0.1} assert defaultPolynomialLoad.load_parameter == test_default_load_parameterVal @@ -163,12 +184,16 @@ def test_InitPolynomialStaticLoad(concretePolynomialLoad): :param concretePolynomialLoad: :return: """ - test_concrete_load_parameterVal = {'a': 0.01, 'b': 0.05, 'c': 0.1, 'j_load': 0.1} + test_concrete_load_parameterVal = {"a": 0.01, "b": 0.05, "c": 0.1, "j_load": 0.1} assert concretePolynomialLoad.load_parameter == test_concrete_load_parameterVal -@pytest.mark.parametrize("omega, expected_result", [(-3, 23400), (0, 20000), (5, 11400)]) # to verify all 3 branches -def test_PolynomialStaticLoad_MechanicalOde(concretePolynomialLoad, omega, expected_result): +@pytest.mark.parametrize( + "omega, expected_result", [(-3, 23400), (0, 20000), (5, 11400)] +) # to verify all 3 branches +def test_PolynomialStaticLoad_MechanicalOde( + concretePolynomialLoad, omega, expected_result +): """ test the mechanical_ode() function of the PolynomialStaticLoad class :param concretePolynomialLoad: @@ -183,23 +208,22 @@ def test_PolynomialStaticLoad_MechanicalOde(concretePolynomialLoad, omega, expec op = PolynomialStaticLoad(load_parameter=load_parameter) output_val = op.mechanical_ode(test_t, test_mechanical_state, test_torque) # output_val = concretePolynomialLoad.mechanical_ode(test_t, test_mechanical_state, test_torque) - assert math.isclose(expected_result, output_val, abs_tol=1E-6) + assert math.isclose(expected_result, output_val, abs_tol=1e-6) class TestMechanicalLoad: - key = '' + key = "" class_to_test = MechanicalLoad kwargs = {} def test_registered(self): - if self.key != '': + if self.key != "": load = gem.utils.instantiate(MechanicalLoad, self.key, **self.kwargs) assert type(load) == self.class_to_test class TestConstSpeedLoad(TestMechanicalLoad): - - key = 'ConstSpeedLoad' + key = "ConstSpeedLoad" class_to_test = ConstantSpeedLoad @pytest.fixture @@ -214,22 +238,21 @@ def test_mechanical_ode(self, const_speed_load): assert all(const_speed_load.mechanical_ode() == np.array([0])) def test_reset(self, const_speed_load): - test_positions = {'omega': 0} + test_positions = {"omega": 0} test_nominal = np.array([80]) # gymnasium.Box state space with random size test_space = Box(low=-1.0, high=1.0, shape=(3,)) - reset_val = const_speed_load.reset(state_positions=test_positions, - nominal_state=test_nominal, - state_space=test_space) + reset_val = const_speed_load.reset( + state_positions=test_positions, + nominal_state=test_nominal, + state_space=test_space, + ) # set additional random kwargs assert all(reset_val == np.array([const_speed_load.omega_fixed])) - @pytest.mark.parametrize('omega, omega_fixed, expected', [ - (-0.5, 1000, (0, 0)), - (0, 0, (0, 0)), - (2, -523, (0, 0)), - (2, 0.2, (0, 0)) - ] + @pytest.mark.parametrize( + "omega, omega_fixed, expected", + [(-0.5, 1000, (0, 0)), (0, 0, (0, 0)), (2, -523, (0, 0)), (2, 0.2, (0, 0))], ) def test_jacobian(self, omega, omega_fixed, expected): test_object = self.class_to_test(omega_fixed) @@ -243,66 +266,68 @@ def test_jacobian(self, omega, omega_fixed, expected): class TestExtSpeedLoad(TestMechanicalLoad): - - key = 'ExtSpeedLoad' + key = "ExtSpeedLoad" class_to_test = ExternalSpeedLoad kwargs = dict( speed_profile=speed_profile_, - speed_profile_kwargs=dict( - amp=test_amp, - bias=test_bias, - freq=test_freq - ) + speed_profile_kwargs=dict(amp=test_amp, bias=test_bias, freq=test_freq), ) @pytest.fixture def ext_speed_load(self): return ExternalSpeedLoad( speed_profile=speed_profile_, - speed_profile_kwargs=dict(amp=test_amp, bias=test_bias, freq=test_freq) + speed_profile_kwargs=dict(amp=test_amp, bias=test_bias, freq=test_freq), ) def test_initialization(self): load = ExternalSpeedLoad( speed_profile=speed_profile_, - speed_profile_kwargs=dict(amp=test_amp, bias=test_bias, freq=test_freq) + speed_profile_kwargs=dict(amp=test_amp, bias=test_bias, freq=test_freq), ) assert load._speed_profile == speed_profile_ - assert load.omega == speed_profile_(t=0, amp=test_amp, bias=test_bias, freq=test_freq) - for key in ['amp', 'bias', 'freq']: + assert load.omega == speed_profile_( + t=0, amp=test_amp, bias=test_bias, freq=test_freq + ) + for key in ["amp", "bias", "freq"]: assert key in load.speed_profile_kwargs # to verify all 3 branches - @pytest.mark.parametrize("omega, expected_result", [(-3, -69840.), - (0, -99840.), - (5, -149840.)]) + @pytest.mark.parametrize( + "omega, expected_result", [(-3, -69840.0), (0, -99840.0), (5, -149840.0)] + ) def test_mechanical_ode(self, ext_speed_load, omega, expected_result): test_mechanical_state = np.array([omega]) test_t = 1 op = ext_speed_load output_val = op.mechanical_ode(test_t, test_mechanical_state) - assert math.isclose(expected_result, output_val, abs_tol=1E-6) + assert math.isclose(expected_result, output_val, abs_tol=1e-6) def test_reset(self, ext_speed_load): - test_positions = {'omega': 0} + test_positions = {"omega": 0} test_nominal = np.array([80]) # gymnasium.Box state space with random size test_space = Box(low=-1.0, high=1.0, shape=(3,)) - reset_var = ext_speed_load.reset(state_positions=test_positions, - nominal_state=test_nominal, - state_space=test_space) + reset_var = ext_speed_load.reset( + state_positions=test_positions, + nominal_state=test_nominal, + state_space=test_space, + ) assert all(reset_var == np.array([ext_speed_load._omega_initial])) - @pytest.mark.parametrize('omega, omega_initial, expected', [ - (-0.5, 1000, (None, None)), - (0, 0, (None, None)), - (2, -523, (None, None)), - (2, 0.2, (None, None)) - ] + @pytest.mark.parametrize( + "omega, omega_initial, expected", + [ + (-0.5, 1000, (None, None)), + (0, 0, (None, None)), + (2, -523, (None, None)), + (2, 0.2, (None, None)), + ], ) def test_jacobian(self, omega, omega_initial, expected): test_object = self.class_to_test( - speed_profile_, speed_profile_kwargs=dict(amp=test_amp, bias=test_bias, freq=test_freq) + speed_profile_, + speed_profile_kwargs=dict(amp=test_amp, bias=test_bias, freq=test_freq), ) # 2 Runs to test independence on time and torque @@ -314,16 +339,21 @@ def test_jacobian(self, omega, omega_initial, expected): class TestPolyStaticLoad(TestMechanicalLoad): - class_to_test = PolynomialStaticLoad - key = 'PolyStaticLoad' - - @pytest.mark.parametrize('omega, load_parameter, expected', [ - (-0.5, dict(a=12, b=1, c=0, j_load=1), (np.array([[-1.]]), np.array([[1.]]))), - (0, dict(j_load=0.5), (np.array([[-1000.]]), np.array([[2.]]))), - (2, dict(a=20, b=0, c=2, j_load=0.25), (-32, 4)), - (2, dict(a=20, b=0.125, c=2, j_load=0.25), (-32.5, 4)) - ] + key = "PolyStaticLoad" + + @pytest.mark.parametrize( + "omega, load_parameter, expected", + [ + ( + -0.5, + dict(a=12, b=1, c=0, j_load=1), + (np.array([[-1.0]]), np.array([[1.0]])), + ), + (0, dict(j_load=0.5), (np.array([[-1000.0]]), np.array([[2.0]]))), + (2, dict(a=20, b=0, c=2, j_load=0.25), (-32, 4)), + (2, dict(a=20, b=0.125, c=2, j_load=0.25), (-32.5, 4)), + ], ) def test_jacobian(self, omega, load_parameter, expected): test_object = self.class_to_test(load_parameter) diff --git a/tests/test_physical_systems/test_physical_systems.py b/tests/test_physical_systems/test_physical_systems.py index 1c12f42f..46f6f3f1 100644 --- a/tests/test_physical_systems/test_physical_systems.py +++ b/tests/test_physical_systems/test_physical_systems.py @@ -1,8 +1,21 @@ import numpy as np -from ..testing_utils import DummyConverter, DummyLoad, DummyOdeSolver, DummyVoltageSupply, DummyElectricMotor,\ - mock_instantiate, instantiate_dict -from gym_electric_motor.physical_systems import physical_systems as ps, converters as cv, electric_motors as em,\ - mechanical_loads as ml, voltage_supplies as vs, solvers as sv +from ..testing_utils import ( + DummyConverter, + DummyLoad, + DummyOdeSolver, + DummyVoltageSupply, + DummyElectricMotor, + mock_instantiate, + instantiate_dict, +) +from gym_electric_motor.physical_systems import ( + physical_systems as ps, + converters as cv, + electric_motors as em, + mechanical_loads as ml, + voltage_supplies as vs, + solvers as sv, +) from gymnasium.spaces import Box import pytest @@ -11,19 +24,23 @@ class TestSCMLSystem: """ Base Class to test all PhysicalSystems that derive from SCMLSystem """ + class_to_test = ps.SCMLSystem def mock_build_state(self, motor_state, torque, u_in, u_sup): - """Function to mock an arbitrary build_state function to test the SCMLSystem - """ + """Function to mock an arbitrary build_state function to test the SCMLSystem""" self.motor_state = motor_state self.torque = torque self.u_in = u_in self.u_sup = u_sup - return np.concatenate(( - self.motor_state[:len(DummyLoad.state_names)], [torque], - self.motor_state[len(DummyLoad.state_names):], [u_sup] - )) + return np.concatenate( + ( + self.motor_state[: len(DummyLoad.state_names)], + [torque], + self.motor_state[len(DummyLoad.state_names) :], + [u_sup], + ) + ) @pytest.fixture def scml_system(self, monkeypatch): @@ -32,18 +49,21 @@ def scml_system(self, monkeypatch): """ monkeypatch.setattr( self.class_to_test, - '_build_state_names', - lambda _: - DummyLoad.state_names + ['torque'] + DummyElectricMotor.CURRENTS + DummyElectricMotor.VOLTAGES + ['u_sup'] + "_build_state_names", + lambda _: DummyLoad.state_names + + ["torque"] + + DummyElectricMotor.CURRENTS + + DummyElectricMotor.VOLTAGES + + ["u_sup"], ) monkeypatch.setattr( self.class_to_test, - '_build_state_space', + "_build_state_space", lambda _, state_names: Box( low=np.zeros_like(state_names, dtype=float), high=np.zeros_like(state_names, dtype=float), - dtype=float - ) + dtype=float, + ), ) return self.class_to_test( converter=DummyConverter(), @@ -61,16 +81,28 @@ def test_reset(self, scml_system): state_positions = scml_system.state_positions initial_state = scml_system.reset() target = np.array([0, 0, 0, 0, 0, 0, 560]) / scml_system.limits - assert np.all(initial_state == target), 'Initial states of the system are incorrect' - assert scml_system._t == 0, 'Time of the system was not set to zero after reset' - assert scml_system._k == 0, 'Episode step of the system was not set to zero after reset' - assert scml_system.converter.reset_counter == scml_system.electrical_motor.reset_counter \ - == scml_system.mechanical_load.reset_counter == scml_system.supply.reset_counter,\ - 'The reset was not passed to all components of the SCMLSystem' - assert scml_system._ode_solver.t == 0, 'The ode solver was not reset correctly' - assert all(scml_system._ode_solver.y == np.zeros_like( - scml_system.mechanical_load.state_names + scml_system.electrical_motor.CURRENTS, dtype=float - )), ' The ode solver was not reset correctly' + assert np.all( + initial_state == target + ), "Initial states of the system are incorrect" + assert scml_system._t == 0, "Time of the system was not set to zero after reset" + assert ( + scml_system._k == 0 + ), "Episode step of the system was not set to zero after reset" + assert ( + scml_system.converter.reset_counter + == scml_system.electrical_motor.reset_counter + == scml_system.mechanical_load.reset_counter + == scml_system.supply.reset_counter + ), "The reset was not passed to all components of the SCMLSystem" + assert scml_system._ode_solver.t == 0, "The ode solver was not reset correctly" + assert all( + scml_system._ode_solver.y + == np.zeros_like( + scml_system.mechanical_load.state_names + + scml_system.electrical_motor.CURRENTS, + dtype=float, + ) + ), " The ode solver was not reset correctly" def test_system_equation(self, scml_system): """Tests the system equation function""" @@ -81,11 +113,15 @@ def test_system_equation(self, scml_system): t = np.random.rand() derivative = scml_system._system_equation(t, state, u_in) assert all( - derivative == np.array([torque, -torque, currents[0] - u_in[0], currents[1] - u_in[1]]) - ), 'The system equation return differs from the expected' - assert scml_system.mechanical_load.t == t, 'The time t was not passed through to the mech. load equation' - assert np.all(scml_system.mechanical_load.mechanical_state == state[:2]),\ - 'The mech. state was not returned correctly' + derivative + == np.array([torque, -torque, currents[0] - u_in[0], currents[1] - u_in[1]]) + ), "The system equation return differs from the expected" + assert ( + scml_system.mechanical_load.t == t + ), "The time t was not passed through to the mech. load equation" + assert np.all( + scml_system.mechanical_load.mechanical_state == state[:2] + ), "The mech. state was not returned correctly" def test_simulate(self, scml_system): """Test the simulation function of the SCMLSystem""" @@ -98,8 +134,8 @@ def test_simulate(self, scml_system): scml_system._ode_solver.set_initial_value(ode_state) # Perform the action on the system next_state = scml_system.simulate(action) - solver_state_me = scml_system._ode_solver.y[:len(DummyLoad.state_names)] - solver_state_el = scml_system._ode_solver.y[len(DummyLoad.state_names):] + solver_state_me = scml_system._ode_solver.y[: len(DummyLoad.state_names)] + solver_state_el = scml_system._ode_solver.y[len(DummyLoad.state_names) :] torque = [scml_system.electrical_motor.torque(solver_state_el)] u_sup = [scml_system.supply.u_nominal] u_in = [u * u_sup[0] for u in scml_system.converter.u_in] @@ -109,11 +145,18 @@ def test_simulate(self, scml_system): ) / scml_system.limits # Assertions for correct simulation - assert all(desired_next_state == next_state), 'The calculated next state differs from the expected one' - assert scml_system.converter.action == action, 'The action was not passed correctly to the converter' - assert scml_system.converter.action_set_time == 0, 'The action start time was passed incorrect to the converter' - assert scml_system.converter.last_i_out == scml_system.electrical_motor.i_in(scml_system._ode_solver.last_y[2:]) - + assert all( + desired_next_state == next_state + ), "The calculated next state differs from the expected one" + assert ( + scml_system.converter.action == action + ), "The action was not passed correctly to the converter" + assert ( + scml_system.converter.action_set_time == 0 + ), "The action start time was passed incorrect to the converter" + assert scml_system.converter.last_i_out == scml_system.electrical_motor.i_in( + scml_system._ode_solver.last_y[2:] + ) def test_system_jacobian(self, scml_system): """Tests for the system jacobian function""" @@ -121,7 +164,11 @@ def test_system_jacobian(self, scml_system): el_over_omega = np.arange(4, 6) torque_over_el = np.arange(6, 8) # Set the el. jacobian returns to specified values - scml_system.electrical_motor.electrical_jac_return = (el_jac, el_over_omega, torque_over_el) + scml_system.electrical_motor.electrical_jac_return = ( + el_jac, + el_over_omega, + torque_over_el, + ) me_jac = np.arange(8, 12).reshape(2, 2) me_over_torque = np.arange(12, 14) # Set the mech. jabobian returns to specified values @@ -129,9 +176,12 @@ def test_system_jacobian(self, scml_system): sys_jac = scml_system._system_jacobian(0, np.array([0, 1, 2, 3]), [0, -1]) # - assert np.all(sys_jac[-2:, -2:] == el_jac), 'The el. jacobian is false' - assert np.all(sys_jac[:2, :2] == me_jac), 'The mech. jacobian is false' - assert np.all(sys_jac[2:, 0] == el_over_omega), 'the derivative of the el.state over omega is false' + assert np.all(sys_jac[-2:, -2:] == el_jac), "The el. jacobian is false" + assert np.all(sys_jac[:2, :2] == me_jac), "The mech. jacobian is false" + assert np.all( + sys_jac[2:, 0] == el_over_omega + ), "the derivative of the el.state over omega is false" assert np.all(sys_jac[2:, 1] == np.zeros(2)) - assert np.all(sys_jac[:-2, 2:] == np.array([[72, 84], [78, 91]])), 'The derivative of the mech.state ' \ - 'over the currents is false' + assert np.all(sys_jac[:-2, 2:] == np.array([[72, 84], [78, 91]])), ( + "The derivative of the mech.state " "over the currents is false" + ) diff --git a/tests/test_physical_systems/test_solvers.py b/tests/test_physical_systems/test_solvers.py index 2f470e78..58a4f430 100644 --- a/tests/test_physical_systems/test_solvers.py +++ b/tests/test_physical_systems/test_solvers.py @@ -12,7 +12,7 @@ g_initial_value = np.array([1, 6]) g_initial_time = 1 -g_time_steps = [1E-5, 2E-5, 3E-5, 5E-5, 1E-4, 2E-4, 5E-4, 1E-3, 2E-3, 5E-3, 7E-3, 1E-2] +g_time_steps = [1e-5, 2e-5, 3e-5, 5e-5, 1e-4, 2e-4, 5e-4, 1e-3, 2e-3, 5e-3, 7e-3, 1e-2] def test_euler(): @@ -25,7 +25,7 @@ def test_euler(): integration_testing(solver) -@pytest.mark.parametrize("integrator", ['dopri5', 'dop853']) +@pytest.mark.parametrize("integrator", ["dopri5", "dop853"]) # 'vode', 'zvode', 'lsoda', could be added, but does not work due to wrong integration times def test_scipyode(integrator): """ @@ -34,12 +34,14 @@ def test_scipyode(integrator): :return: """ for nsteps in [1, 5]: - kwargs = {'nsteps': nsteps, } + kwargs = { + "nsteps": nsteps, + } solver = ScipyOdeSolver(integrator, **kwargs) integration_testing(solver) -@pytest.mark.parametrize("integrator", ['RK45', 'RK23', 'Radau', 'BDF', 'LSODA']) +@pytest.mark.parametrize("integrator", ["RK45", "RK23", "Radau", "BDF", "LSODA"]) def test_scipy_ivp(integrator): """ test scipy.solveivp integrator @@ -56,7 +58,9 @@ def test_scipyodeint(): :return: """ for nsteps in [1, 5]: - kwargs = {'nsteps': nsteps, } + kwargs = { + "nsteps": nsteps, + } solver = ScipyOdeIntSolver() integration_testing(solver) @@ -92,8 +96,8 @@ def test_compare_solver(): """ # initialize all solver solver_1 = EulerSolver() - solver_2 = ScipyOdeSolver('dopri5') - solver_3 = ScipySolveIvpSolver(method='Radau') + solver_2 = ScipyOdeSolver("dopri5") + solver_3 = ScipySolveIvpSolver(method="Radau") solver_4 = ScipyOdeIntSolver() solver = [solver_1, solver_2, solver_3, solver_4] # define the integration steps @@ -113,8 +117,15 @@ def test_compare_solver(): # test if all solver integrated for the same time assert solver_1.t == solver_2.t == solver_3.t == solver_4.t # test if the relative difference between the states is small - abs_values = [sum(abs(solver_1.y)), sum(abs(solver_2.y)), sum(abs(solver_3.y)), sum(abs(solver_4.y))] - assert max(abs_values) / min(abs_values) - 1 < 1E-4, "Time step at the error: " + str(steps) + abs_values = [ + sum(abs(solver_1.y)), + sum(abs(solver_2.y)), + sum(abs(solver_3.y)), + sum(abs(solver_4.y)), + ] + assert max(abs_values) / min(abs_values) - 1 < 1e-4, ( + "Time step at the error: " + str(steps) + ) # endregion @@ -122,10 +133,12 @@ def test_compare_solver(): # region second version tests + class TestOdeSolver: """ class for testing OdeSolver """ + # defined test values _initial_time = 0.1 _initial_state = np.array([15, 0.23, 0.35]) @@ -140,10 +153,12 @@ def test_set_initial_value(self): # call function to test test_object.set_initial_value(self._initial_state, self._initial_time) # verify the expected results - assert all(test_object._y == test_object.y) and all(test_object._y == self._initial_state), 'unexpected state' - assert test_object.t == test_object._t == self._initial_time, 'unexpected time' + assert all(test_object._y == test_object.y) and all( + test_object._y == self._initial_state + ), "unexpected state" + assert test_object.t == test_object._t == self._initial_time, "unexpected time" - @pytest.mark.parametrize('jacobian_', [jacobian, None]) + @pytest.mark.parametrize("jacobian_", [jacobian, None]) def test_set_system_equation(self, jacobian_): """ test set_system_equation() @@ -155,10 +170,14 @@ def test_set_system_equation(self, jacobian_): # call function to test test_object.set_system_equation(system, jacobian_) # verify the expected results - assert test_object._system_equation == system, 'system equation is not passed correctly' - assert test_object._system_jacobian == jacobian_, 'jacobian is not passed correctly' - - @pytest.mark.parametrize('args', [[], ['nsteps', 5], 42]) + assert ( + test_object._system_equation == system + ), "system equation is not passed correctly" + assert ( + test_object._system_jacobian == jacobian_ + ), "jacobian is not passed correctly" + + @pytest.mark.parametrize("args", [[], ["nsteps", 5], 42]) def test_set_f_params(self, args): """ test set_f_params() @@ -170,13 +189,16 @@ def test_set_f_params(self, args): # call function to test test_object.set_f_params(args) # verify the expected results - assert test_object._f_params[0] == args, 'arguments are not passed correctly for the system' + assert ( + test_object._f_params[0] == args + ), "arguments are not passed correctly for the system" class TestEulerSolver: """ class for testing EulerSolver """ + # defined test values _t = 5e-4 _state = np.array([1, 6]) @@ -187,10 +209,10 @@ def monkey_integrate(self, t): :param t: time :return: """ - assert t == self._t, 'unexpected time at the end of the integration' + assert t == self._t, "unexpected time at the end of the integration" return self._state - @pytest.mark.parametrize('nsteps, expected_integrate', [(1, 0), (5, 1)]) + @pytest.mark.parametrize("nsteps, expected_integrate", [(1, 0), (5, 1)]) def test_init(self, nsteps, expected_integrate): """ test initialization of EulerSolver @@ -201,9 +223,14 @@ def test_init(self, nsteps, expected_integrate): # call function to test test_object = EulerSolver(nsteps) # verify the expected results - integration_functions = [test_object._integrate_one_step, test_object._integrate_nsteps] - assert test_object._nsteps == nsteps, 'nsteps argument not passed correctly' - assert test_object._integrate == integration_functions[expected_integrate], 'unexpected integrate function' + integration_functions = [ + test_object._integrate_one_step, + test_object._integrate_nsteps, + ] + assert test_object._nsteps == nsteps, "nsteps argument not passed correctly" + assert ( + test_object._integrate == integration_functions[expected_integrate] + ), "unexpected integrate function" def test_integrate(self, monkeypatch): """ @@ -213,14 +240,16 @@ def test_integrate(self, monkeypatch): """ # setup test scenario test_object = EulerSolver() - monkeypatch.setattr(test_object, '_integrate', self.monkey_integrate) + monkeypatch.setattr(test_object, "_integrate", self.monkey_integrate) # call function to test state = test_object.integrate(self._t) # verify the expected results - assert all(state == self._state), 'unexpected state after integration' + assert all(state == self._state), "unexpected state after integration" - @pytest.mark.parametrize('nsteps, expected_state', - [(1, np.array([1.006, 6.0258])), (3, np.array([1.00596811, 6.025793824]))]) + @pytest.mark.parametrize( + "nsteps, expected_state", + [(1, np.array([1.006, 6.0258])), (3, np.array([1.00596811, 6.025793824]))], + ) def test_private_integration(self, nsteps, expected_state): """ test _integration() for different cases @@ -238,16 +267,19 @@ def test_private_integration(self, nsteps, expected_state): # call function to test state = test_object.integrate(self._t + tau) # verify the expected results - assert sum(abs(state - expected_state)) < 1E-6, 'unexpected state after integration' + assert ( + sum(abs(state - expected_state)) < 1e-6 + ), "unexpected state after integration" class TestScipyOdeSolver: """ class for testing ScipyOdeSolver """ + # defined test values - _kwargs = {'nsteps': 5} - _integrator = 'dop853' + _kwargs = {"nsteps": 5} + _integrator = "dop853" _args = 2 _tau = 1e-3 _initial_time = 0.1 @@ -264,8 +296,10 @@ def monkey_set_integrator(self, integrator, **kwargs): :param kwargs: :return: """ - assert integrator == self._integrator, 'integrator not passed correctly' - assert kwargs == self._kwargs, 'unexpected additional arguments. Keep in mind None and {}.' + assert integrator == self._integrator, "integrator not passed correctly" + assert ( + kwargs == self._kwargs + ), "unexpected additional arguments. Keep in mind None and {}." def monkey_super_set_system_equation(self, system_equation, jacobian_): """ @@ -275,8 +309,8 @@ def monkey_super_set_system_equation(self, system_equation, jacobian_): :return: """ self._monkey_super_set_system_equation_counter += 1 - assert system_equation == system, 'unexpected system equation' - assert jacobian_ == jacobian, 'unexpected jacobian' + assert system_equation == system, "unexpected system equation" + assert jacobian_ == jacobian, "unexpected jacobian" def monkey_set_params(self, **args): """ @@ -285,7 +319,9 @@ def monkey_set_params(self, **args): :return: """ self._monkey_set_params_counter += 1 - assert self._args == (args,), 'unexpected additional arguments. Keep the type in mind' + assert self._args == ( + args, + ), "unexpected additional arguments. Keep the type in mind" def test_init(self): """ @@ -295,8 +331,12 @@ def test_init(self): # call function to test test_object = ScipyOdeSolver(integrator=self._integrator, **self._kwargs) assert test_object._solver is None - assert test_object._solver_args == self._kwargs, 'unexpected additional arguments. Keep in mind None and {}.' - assert test_object._integrator == self._integrator, 'unexpected initialization of integrate function' + assert ( + test_object._solver_args == self._kwargs + ), "unexpected additional arguments. Keep in mind None and {}." + assert ( + test_object._integrator == self._integrator + ), "unexpected initialization of integrate function" def test_set_system_equation(self, monkeypatch): """ @@ -305,15 +345,23 @@ def test_set_system_equation(self, monkeypatch): :return: """ # setup test scenario - monkeypatch.setattr(pss, 'ode', DummyScipyOdeSolver) + monkeypatch.setattr(pss, "ode", DummyScipyOdeSolver) test_object = ScipyOdeSolver(integrator=self._integrator, **self._kwargs) - monkeypatch.setattr(OdeSolver, 'set_system_equation', self.monkey_super_set_system_equation) + monkeypatch.setattr( + OdeSolver, "set_system_equation", self.monkey_super_set_system_equation + ) # call function to test test_object.set_system_equation(system, jacobian) # verify the expected results - assert isinstance(test_object._ode, DummyScipyOdeSolver), 'the ode is no DummyScipyOdeSolver' - assert self._monkey_super_set_system_equation_counter == 1, 'super().set_system_equation() is not called once' - assert test_object._ode._set_integrator_counter == 1, 'set_integrator() is not called once' + assert isinstance( + test_object._ode, DummyScipyOdeSolver + ), "the ode is no DummyScipyOdeSolver" + assert ( + self._monkey_super_set_system_equation_counter == 1 + ), "super().set_system_equation() is not called once" + assert ( + test_object._ode._set_integrator_counter == 1 + ), "set_integrator() is not called once" def test_set_initial_value(self, monkeypatch): """ @@ -322,13 +370,15 @@ def test_set_initial_value(self, monkeypatch): :return: """ # setup test scenario - monkeypatch.setattr(pss, 'ode', DummyScipyOdeSolver) + monkeypatch.setattr(pss, "ode", DummyScipyOdeSolver) test_object = ScipyOdeSolver(integrator=self._integrator, **self._kwargs) test_object.set_system_equation(system, jacobian) # call function to test test_object.set_initial_value(self._initial_state, self._initial_time) # verify the expected results - assert test_object._ode._set_initial_value_counter == 1, 'set_initial_value() is not called once' + assert ( + test_object._ode._set_initial_value_counter == 1 + ), "set_initial_value() is not called once" def test_set_f_params(self, monkeypatch): """ @@ -337,13 +387,15 @@ def test_set_f_params(self, monkeypatch): :return: """ # setup test scenario - monkeypatch.setattr(pss, 'ode', DummyScipyOdeSolver) + monkeypatch.setattr(pss, "ode", DummyScipyOdeSolver) test_object = ScipyOdeSolver(integrator=self._integrator, **self._kwargs) test_object.set_system_equation(system, jacobian) # call function to test test_object.set_f_params(self._args) # verify the expected results - assert test_object._ode._set_f_params_counter == 1, 'set_f_params() is not called once' + assert ( + test_object._ode._set_f_params_counter == 1 + ), "set_f_params() is not called once" def test_integrate(self, monkeypatch): """ @@ -352,7 +404,7 @@ def test_integrate(self, monkeypatch): :return: """ # setup test scenario - monkeypatch.setattr(pss, 'ode', DummyScipyOdeSolver) + monkeypatch.setattr(pss, "ode", DummyScipyOdeSolver) test_object = ScipyOdeSolver(integrator=self._integrator, **self._kwargs) test_object.set_system_equation(system, jacobian) test_object.set_initial_value(self._initial_state, self._initial_time) @@ -360,14 +412,19 @@ def test_integrate(self, monkeypatch): # call function to test result = test_object.integrate(self._tau + self._initial_time) # verify the expected results - assert all(result == self._initial_state * 2), 'unexpected result of the integration' - assert test_object._ode._integrate_counter == 1, '_ode._integrate() is not called once()' + assert all( + result == self._initial_state * 2 + ), "unexpected result of the integration" + assert ( + test_object._ode._integrate_counter == 1 + ), "_ode._integrate() is not called once()" class TestScipySolveIvpSolver: """ class for testing ScipySolveIvpSolver """ + # defined test values _state = np.array([1, 6]) _tau = 1e-3 @@ -383,8 +440,8 @@ def monkey_super_set_system_equation(self, system_equation, jac): :return: """ self._monkey_super_set_system_equation_counter += 1 - assert system_equation == system, 'unexpected system passed' - assert jac == jacobian, 'unexpected jacobian passed' + assert system_equation == system, "unexpected system passed" + assert jac == jacobian, "unexpected jacobian passed" def monkey_set_f_params(self): """ @@ -405,14 +462,15 @@ def monkey_solve_ivp(self, function, time, state, t_eval, **kwargs): :param kwargs: :return: """ - assert time == [0.0, self._tau], 'unexpected time passed' - assert all(state == self._state), 'unexpected state passed' - assert t_eval == [self._tau], 'unexpected time for evaluation' + assert time == [0.0, self._tau], "unexpected time passed" + assert all(state == self._state), "unexpected state passed" + assert t_eval == [self._tau], "unexpected time for evaluation" class Result: """ simple class necessary for correct testing """ + y = np.array([[7, 1], [4, 6]]) return Result() @@ -423,11 +481,13 @@ def test_init(self): :return: """ # setup test scenario - _kwargs = {'nsteps': 5} + _kwargs = {"nsteps": 5} # call function to test test_object = ScipySolveIvpSolver(**_kwargs) # verify the expected results - assert test_object._solver_kwargs == _kwargs, 'unexpected additional arguments. Keep in mind None and {}' + assert ( + test_object._solver_kwargs == _kwargs + ), "unexpected additional arguments. Keep in mind None and {}" def test_set_system_equation(self, monkeypatch): """ @@ -436,8 +496,12 @@ def test_set_system_equation(self, monkeypatch): :return: """ # setup test scenario - monkeypatch.setattr(OdeSolver, 'set_system_equation', self.monkey_super_set_system_equation) - monkeypatch.setattr(ScipySolveIvpSolver, 'set_f_params', self.monkey_set_f_params) + monkeypatch.setattr( + OdeSolver, "set_system_equation", self.monkey_super_set_system_equation + ) + monkeypatch.setattr( + ScipySolveIvpSolver, "set_f_params", self.monkey_set_f_params + ) test_object = ScipySolveIvpSolver() # call function to test test_object.set_system_equation(system, jacobian) @@ -457,8 +521,12 @@ def test_set_f_params(self): # call function to test test_object.set_f_params(args) # verify the expected results - assert sum(abs(test_object._system_equation(_tau, _state, args) - _expected_result)) < 1E-6,\ - 'unexpected result after set_f_params()' + assert ( + sum( + abs(test_object._system_equation(_tau, _state, args) - _expected_result) + ) + < 1e-6 + ), "unexpected result after set_f_params()" def test_integrate(self, monkeypatch): """ @@ -467,7 +535,7 @@ def test_integrate(self, monkeypatch): :return: """ # setup test scenario - monkeypatch.setattr(pss, 'solve_ivp', self.monkey_solve_ivp) + monkeypatch.setattr(pss, "solve_ivp", self.monkey_solve_ivp) test_object = ScipySolveIvpSolver() test_object.set_system_equation(system, jacobian) test_object.set_initial_value(self._state, 0.0) @@ -475,13 +543,14 @@ def test_integrate(self, monkeypatch): # call function to test result = test_object.integrate(self._tau) # verify the expected results - assert all(result == self._state), 'unexpected state after integration' + assert all(result == self._state), "unexpected state after integration" class TestScipyOdeIntSolver: """ class for testing ScipyOdeIntSolver """ + # defined test values _state = np.array([1, 6]) _tau = 1e-3 @@ -499,8 +568,8 @@ def monkey_super_set_system_equation(self, system_equation, jac): :return: """ self._monkey_super_set_system_equation_counter += 1 - assert system_equation == system, 'unexpected system passed' - assert jac == jacobian, 'unexpected jacobian passed' + assert system_equation == system, "unexpected system passed" + assert jac == jacobian, "unexpected jacobian passed" def monkey_set_f_params(self): """ @@ -522,9 +591,9 @@ def monkey_ode_int(self, function, y, time, args, Dfun, tfirst, **kwargs): :param kwargs: :return: """ - assert all(y == self._state), 'unexpected state before integration' - assert time == [0, self._tau], 'unexpected time steps for integration' - assert args == (self.args,), 'unexpected arguments' + assert all(y == self._state), "unexpected state before integration" + assert time == [0, self._tau], "unexpected time steps for integration" + assert args == (self.args,), "unexpected arguments" assert tfirst return np.array([[0, 2], self._state]) @@ -534,11 +603,13 @@ def test_init(self): :return: """ # setup test scenario - _kwargs = {'nsteps': 5} + _kwargs = {"nsteps": 5} # call function to test test_object = ScipyOdeIntSolver(**_kwargs) # verify the expected results - assert test_object._solver_args == _kwargs, 'unexpected additional arguments. Keep the type in mind.' + assert ( + test_object._solver_args == _kwargs + ), "unexpected additional arguments. Keep the type in mind." def test_set_system_equation(self, monkeypatch): """ @@ -547,8 +618,10 @@ def test_set_system_equation(self, monkeypatch): :return: """ # setup test scenario - monkeypatch.setattr(OdeSolver, 'set_system_equation', self.monkey_super_set_system_equation) - monkeypatch.setattr(ScipyOdeIntSolver, 'set_f_params', self.monkey_set_f_params) + monkeypatch.setattr( + OdeSolver, "set_system_equation", self.monkey_super_set_system_equation + ) + monkeypatch.setattr(ScipyOdeIntSolver, "set_f_params", self.monkey_set_f_params) test_object = ScipyOdeIntSolver() # call function to test test_object.set_system_equation(system, jacobian) @@ -560,7 +633,7 @@ def test_integrate(self, monkeypatch): :return: """ # setup test scenario - monkeypatch.setattr(pss, 'odeint', self.monkey_ode_int) + monkeypatch.setattr(pss, "odeint", self.monkey_ode_int) test_object = ScipyOdeIntSolver() test_object.set_system_equation(system, jacobian) test_object.set_initial_value(self._state, 0.0) @@ -568,7 +641,7 @@ def test_integrate(self, monkeypatch): # call function to test result = test_object.integrate(self._tau) # verify the expected results - assert all(result == self._state), 'unexpected result after integration' + assert all(result == self._state), "unexpected result after integration" # endregion diff --git a/tests/test_physical_systems/test_voltage_supplies.py b/tests/test_physical_systems/test_voltage_supplies.py index d8ebcfdb..fe80d132 100644 --- a/tests/test_physical_systems/test_voltage_supplies.py +++ b/tests/test_physical_systems/test_voltage_supplies.py @@ -3,14 +3,14 @@ from ..testing_utils import DummyOdeSolver import numpy as np -class TestVoltageSupply: - key = '' +class TestVoltageSupply: + key = "" class_to_test = vs.VoltageSupply def test_registered(self): """If a key is provided, tests if the class can be initialized from registry""" - if self.key != '': + if self.key != "": supply = gem.utils.instantiate(vs.VoltageSupply, self.key) assert type(supply) == self.class_to_test @@ -22,8 +22,7 @@ def test_initialization(self): class TestIdealVoltageSupply(TestVoltageSupply): - - key = 'IdealVoltageSupply' + key = "IdealVoltageSupply" class_to_test = vs.IdealVoltageSupply def test_default_initialization(self): @@ -34,21 +33,21 @@ def test_default_initialization(self): def test_get_voltage(self, u_nominal=450.0): """Test the get voltage function. - It must return u_nominal.""" + It must return u_nominal.""" supply = vs.IdealVoltageSupply(u_nominal) assert supply.get_voltage() == [u_nominal] def test_reset(self, u_nominal=450.0): """Test the reset function. - It must return u_nominal.""" + It must return u_nominal.""" supply = vs.IdealVoltageSupply(u_nominal) assert supply.reset() == [u_nominal] - + + class TestRCVoltageSupply(TestVoltageSupply): - - key = 'RCVoltageSupply' + key = "RCVoltageSupply" class_to_test = vs.RCVoltageSupply - + def test_default_initialization(self): """Test for default initialization values""" voltage_supply = vs.RCVoltageSupply() @@ -57,48 +56,52 @@ def test_default_initialization(self): assert voltage_supply.supply_range == (0.0, 600.0) assert voltage_supply._r == 1 assert voltage_supply._c == 4e-3 - + def test_reset(self, u_nominal=450.0): """Test the reset function. - It must return u_nominal.""" + It must return u_nominal.""" voltage_supply = vs.RCVoltageSupply(u_nominal) assert voltage_supply.reset() == [u_nominal] - def test_initialization(self, u_nominal=450.0,supply_parameter={'R':3,'C':6e-2}): + def test_initialization( + self, u_nominal=450.0, supply_parameter={"R": 3, "C": 6e-2} + ): voltage_supply = vs.RCVoltageSupply(u_nominal, supply_parameter) assert voltage_supply._u_0 == u_nominal assert voltage_supply._u_sup == [u_nominal] - assert voltage_supply.supply_range == (0.0, u_nominal) - assert voltage_supply._r == supply_parameter['R'] - assert voltage_supply._c == supply_parameter['C'] - + assert voltage_supply.supply_range == (0.0, u_nominal) + assert voltage_supply._r == supply_parameter["R"] + assert voltage_supply._c == supply_parameter["C"] + def test_get_voltage(self, monkeypatch, u_nominal=450.0): """Test the get voltage function. - It must return the right voltage added by its change given by the time delta in this example.""" + It must return the right voltage added by its change given by the time delta in this example.""" supply = vs.RCVoltageSupply(u_nominal) solver = DummyOdeSolver() solver._y = np.array([u_nominal]) - monkeypatch.setattr(supply, '_solver', solver) - times = [0.5,1,1.5,1.78,2.1,3] + monkeypatch.setattr(supply, "_solver", solver) + times = [0.5, 1, 1.5, 1.78, 2.1, 3] for time in times: - assert supply.get_voltage(time,0) == [u_nominal + time] + assert supply.get_voltage(time, 0) == [u_nominal + time] assert supply._u_sup == u_nominal + time - + def test_system_equation(self): - """Tests the correct behavior of the system equation by hand calculated values""" - supply = vs.RCVoltageSupply() - system_equation = supply.system_equation - assert system_equation(0,[10],50,1,1,1) == 39 - assert system_equation(0,[20],50,2,2,2) == 6.5 - assert system_equation(0,[30],50,3,3,3) == 1 + 2/9 - #time invariance - assert system_equation(0,[30],50,3,3,3) == system_equation(5,[30],50,3,3,3) + """Tests the correct behavior of the system equation by hand calculated values""" + supply = vs.RCVoltageSupply() + system_equation = supply.system_equation + assert system_equation(0, [10], 50, 1, 1, 1) == 39 + assert system_equation(0, [20], 50, 2, 2, 2) == 6.5 + assert system_equation(0, [30], 50, 3, 3, 3) == 1 + 2 / 9 + # time invariance + assert system_equation(0, [30], 50, 3, 3, 3) == system_equation( + 5, [30], 50, 3, 3, 3 + ) class TestAC1PhaseSupply(TestVoltageSupply): - key = 'AC1PhaseSupply' + key = "AC1PhaseSupply" class_to_test = vs.AC1PhaseSupply - + def test_default_initialization(self): """Test for default initialization values""" voltage_supply = vs.AC1PhaseSupply() @@ -107,23 +110,23 @@ def test_default_initialization(self): assert voltage_supply.supply_range == [-230.0 * np.sqrt(2), 230.0 * np.sqrt(2)] assert voltage_supply._fixed_phi == False assert voltage_supply._f == 50 - + def test_reset(self, u_nominal=230.0): - """Test the reset function for correct behavior on fixed phase""" - supply_parameter = {'frequency': 50} + """Test the reset function for correct behavior on fixed phase""" + supply_parameter = {"frequency": 50} voltage_supply = vs.AC1PhaseSupply(u_nominal, supply_parameter) first_phi = voltage_supply._phi _ = voltage_supply.reset() assert voltage_supply._phi != first_phi - supply_parameter = {'frequency': 50, 'phase': 0} + supply_parameter = {"frequency": 50, "phase": 0} voltage_supply = vs.AC1PhaseSupply(u_nominal, supply_parameter) assert voltage_supply._phi == 0 assert voltage_supply.reset() == [0.0] assert voltage_supply._phi == 0 - def test_initialization(self, u_nominal = 300.0): - supply_parameter = {'frequency': 35, 'phase': 0} + def test_initialization(self, u_nominal=300.0): + supply_parameter = {"frequency": 35, "phase": 0} voltage_supply = vs.AC1PhaseSupply(u_nominal, supply_parameter) assert voltage_supply.u_nominal == 300.0 assert voltage_supply._max_amp == 300.0 * np.sqrt(2) @@ -131,93 +134,108 @@ def test_initialization(self, u_nominal = 300.0): assert voltage_supply._fixed_phi == True assert voltage_supply._phi == 0 assert voltage_supply._f == 35 - + def test_get_voltage(self): """Test the get voltage function for different times t.""" - supply_parameter = {'frequency': 1, 'phase': 0} + supply_parameter = {"frequency": 1, "phase": 0} supply = vs.AC1PhaseSupply(supply_parameter=supply_parameter) - + # Test for default sinus values times = [0, 1, 2] for time in times: assert np.allclose(supply.get_voltage(time), [0.0]) - - times = [1/4, 5/4, 9/4] + + times = [1 / 4, 5 / 4, 9 / 4] for time in times: - assert np.allclose(supply.get_voltage(time),[230.0 * np.sqrt(2)]) - - times = [3/4, 7/4, 11/4] + assert np.allclose(supply.get_voltage(time), [230.0 * np.sqrt(2)]) + + times = [3 / 4, 7 / 4, 11 / 4] for time in times: - assert np.allclose(supply.get_voltage(time),[-230.0 * np.sqrt(2)]) - + assert np.allclose(supply.get_voltage(time), [-230.0 * np.sqrt(2)]) + # manually calculated - supply_parameter = {'frequency': 36, 'phase': 0.5} + supply_parameter = {"frequency": 36, "phase": 0.5} supply = vs.AC1PhaseSupply(supply_parameter=supply_parameter) - assert np.allclose(supply.get_voltage(1/(2*np.pi)),[-303.058731]) - assert np.allclose(supply.get_voltage(2/(2*np.pi)),[-78.381295]) - assert np.allclose(supply.get_voltage(3/(2*np.pi)),[323.118651]) + assert np.allclose(supply.get_voltage(1 / (2 * np.pi)), [-303.058731]) + assert np.allclose(supply.get_voltage(2 / (2 * np.pi)), [-78.381295]) + assert np.allclose(supply.get_voltage(3 / (2 * np.pi)), [323.118651]) class TestAC3PhaseSupply(TestVoltageSupply): - key = 'AC3PhaseSupply' + key = "AC3PhaseSupply" class_to_test = vs.AC3PhaseSupply - + def test_default_initialization(self): """Test for default initialization values""" voltage_supply = vs.AC3PhaseSupply() assert voltage_supply.u_nominal == 400.0 - assert voltage_supply._max_amp == 400.0 / np.sqrt(3) * np.sqrt(2) - assert voltage_supply.supply_range == [-400.0 / np.sqrt(3) * np.sqrt(2), 400.0 / np.sqrt(3) * np.sqrt(2)] + assert voltage_supply._max_amp == 400.0 / np.sqrt(3) * np.sqrt(2) + assert voltage_supply.supply_range == [ + -400.0 / np.sqrt(3) * np.sqrt(2), + 400.0 / np.sqrt(3) * np.sqrt(2), + ] assert voltage_supply._fixed_phi == False assert voltage_supply._f == 50 - + def test_reset(self, u_nominal=400.0): - """Test the reset function for correct behavior on fixed phase""" - supply_parameter = {'frequency': 50} + """Test the reset function for correct behavior on fixed phase""" + supply_parameter = {"frequency": 50} voltage_supply = vs.AC3PhaseSupply(u_nominal, supply_parameter) first_phi = voltage_supply._phi _ = voltage_supply.reset() - assert voltage_supply._phi != first_phi, "Test this again and if this error doesn't appear next time you should consider playing lotto" + assert ( + voltage_supply._phi != first_phi + ), "Test this again and if this error doesn't appear next time you should consider playing lotto" - supply_parameter = {'frequency': 50, 'phase': 0} + supply_parameter = {"frequency": 50, "phase": 0} voltage_supply = vs.AC3PhaseSupply(u_nominal, supply_parameter) assert voltage_supply._phi == 0 _ = voltage_supply.reset() assert voltage_supply._phi == 0 - def test_initialization(self, u_nominal = 300.0): - supply_parameter = {'frequency': 35, 'phase': 0} + def test_initialization(self, u_nominal=300.0): + supply_parameter = {"frequency": 35, "phase": 0} voltage_supply = vs.AC3PhaseSupply(u_nominal, supply_parameter) assert voltage_supply.u_nominal == 300.0 assert voltage_supply._max_amp == 300.0 / np.sqrt(3) * np.sqrt(2) - assert voltage_supply.supply_range == [-300.0 / np.sqrt(3) * np.sqrt(2), 300.0 / np.sqrt(3) * np.sqrt(2)] + assert voltage_supply.supply_range == [ + -300.0 / np.sqrt(3) * np.sqrt(2), + 300.0 / np.sqrt(3) * np.sqrt(2), + ] assert voltage_supply._fixed_phi == True assert voltage_supply._phi == 0 assert voltage_supply._f == 35 - + def test_get_voltage(self): """Test the get voltage function for different times t.""" - supply_parameter = {'frequency': 1, 'phase': 0} + supply_parameter = {"frequency": 1, "phase": 0} supply = vs.AC3PhaseSupply(supply_parameter=supply_parameter) - + assert len(supply.get_voltage(0)) == 3 - + # Test for default sinus values times = [0, 1, 2] for time in times: assert np.allclose(supply.get_voltage(time)[0], [0.0]) - - times = [1/4, 5/4, 9/4] + + times = [1 / 4, 5 / 4, 9 / 4] for time in times: - assert np.allclose(supply.get_voltage(time)[0], 400.0 / np.sqrt(3) * np.sqrt(2)) - - times = [3/4, 7/4, 11/4] + assert np.allclose( + supply.get_voltage(time)[0], 400.0 / np.sqrt(3) * np.sqrt(2) + ) + + times = [3 / 4, 7 / 4, 11 / 4] for time in times: - assert np.allclose(supply.get_voltage(time)[0], -400.0 / np.sqrt(3) * np.sqrt(2)) - + assert np.allclose( + supply.get_voltage(time)[0], -400.0 / np.sqrt(3) * np.sqrt(2) + ) + # Manually calculated - supply_parameter = {'frequency': 41, 'phase': 3.26} + supply_parameter = {"frequency": 41, "phase": 3.26} supply = vs.AC3PhaseSupply(supply_parameter=supply_parameter) - assert np.allclose(supply.get_voltage(1/(2*np.pi)),[89.536111,227.238311, -316.774422]) - assert np.allclose(supply.get_voltage(2/(2*np.pi)),[-138.223662,-187.151049, 325.374712]) - + assert np.allclose( + supply.get_voltage(1 / (2 * np.pi)), [89.536111, 227.238311, -316.774422] + ) + assert np.allclose( + supply.get_voltage(2 / (2 * np.pi)), [-138.223662, -187.151049, 325.374712] + ) diff --git a/tests/test_random_component.py b/tests/test_random_component.py index 9cb20c0f..5511bb47 100644 --- a/tests/test_random_component.py +++ b/tests/test_random_component.py @@ -4,7 +4,6 @@ class TestRandomComponent: - @pytest.fixture def random_component(self): return gem.RandomComponent() @@ -44,11 +43,13 @@ def test_next_generator(self, random_component): # Reseed the environment to the previous state random_component.seed(np.random.SeedSequence(123)) # Test, if the first random numbers of the first episodes are equal - assert(np.all(rands_first_ep[:30] == random_component.random_generator.random(30))),\ - 'The random numbers of the initial and reseeded random component differ.' + assert np.all( + rands_first_ep[:30] == random_component.random_generator.random(30) + ), "The random numbers of the initial and reseeded random component differ." random_component.next_generator() # Also the second episode has to be equal. Therefore, the next generator has to be set np matter how many steps # have been taken in the first episode. - assert(np.all(rands_second_ep == random_component.random_generator.random(64)[:42])),\ - 'The random numbers of the initial and reseeded random component differ.' + assert np.all( + rands_second_ep == random_component.random_generator.random(64)[:42] + ), "The random numbers of the initial and reseeded random component differ." diff --git a/tests/test_reference_generators/test_reference_generators.py b/tests/test_reference_generators/test_reference_generators.py index 94fa46ca..20fe4363 100644 --- a/tests/test_reference_generators/test_reference_generators.py +++ b/tests/test_reference_generators/test_reference_generators.py @@ -4,15 +4,34 @@ import pytest import gym_electric_motor as gem from tests.test_core import TestReferenceGenerator -from gym_electric_motor.reference_generators import ConstReferenceGenerator, MultipleReferenceGenerator -from gym_electric_motor.reference_generators.switched_reference_generator import SwitchedReferenceGenerator -from gym_electric_motor.reference_generators.sawtooth_reference_generator import SawtoothReferenceGenerator -from gym_electric_motor.reference_generators.sinusoidal_reference_generator import SinusoidalReferenceGenerator -from gym_electric_motor.reference_generators.step_reference_generator import StepReferenceGenerator -from gym_electric_motor.reference_generators.triangle_reference_generator import TriangularReferenceGenerator -from gym_electric_motor.reference_generators.wiener_process_reference_generator import WienerProcessReferenceGenerator -from gym_electric_motor.reference_generators.subepisoded_reference_generator import SubepisodedReferenceGenerator -from gym_electric_motor.reference_generators.zero_reference_generator import ZeroReferenceGenerator +from gym_electric_motor.reference_generators import ( + ConstReferenceGenerator, + MultipleReferenceGenerator, +) +from gym_electric_motor.reference_generators.switched_reference_generator import ( + SwitchedReferenceGenerator, +) +from gym_electric_motor.reference_generators.sawtooth_reference_generator import ( + SawtoothReferenceGenerator, +) +from gym_electric_motor.reference_generators.sinusoidal_reference_generator import ( + SinusoidalReferenceGenerator, +) +from gym_electric_motor.reference_generators.step_reference_generator import ( + StepReferenceGenerator, +) +from gym_electric_motor.reference_generators.triangle_reference_generator import ( + TriangularReferenceGenerator, +) +from gym_electric_motor.reference_generators.wiener_process_reference_generator import ( + WienerProcessReferenceGenerator, +) +from gym_electric_motor.reference_generators.subepisoded_reference_generator import ( + SubepisodedReferenceGenerator, +) +from gym_electric_motor.reference_generators.zero_reference_generator import ( + ZeroReferenceGenerator, +) import gym_electric_motor.reference_generators.switched_reference_generator as swrg import gym_electric_motor.reference_generators.subepisoded_reference_generator as srg import gym_electric_motor.reference_generators.sawtooth_reference_generator as sawrg @@ -29,11 +48,12 @@ class TestSwitchedReferenceGenerator: """ class for testing the switched reference generator """ + _reference_generator = [] _physical_system = None _sub_generator = [] # pre defined test values and expected results - _kwargs = {'test': 42} + _kwargs = {"test": 42} _reference = 0.8 _reference_observation = np.array([0.5, 0.6, 0.8, 0.15]) _trajectory = np.ones((4, 15)) @@ -47,7 +67,7 @@ class for testing the switched reference generator _monkey_reset_reference_counter = 0 _monkey_dummy_reset_counter = 0 - @pytest.fixture(scope='function') + @pytest.fixture(scope="function") def setup(self): """ fixture to reset the counter and _reference_generator @@ -65,9 +85,12 @@ def monkey_instantiate(self, superclass, instance, **kwargs): :param kwargs: :return: DummyReferenceGenerator """ - assert superclass == ReferenceGenerator, 'superclass is not ReferenceGenerator as expected' - assert instance == self._sub_generator[self._monkey_instantiate_counter], \ - 'Instance is not the expected reference generator' + assert ( + superclass == ReferenceGenerator + ), "superclass is not ReferenceGenerator as expected" + assert ( + instance == self._sub_generator[self._monkey_instantiate_counter] + ), "Instance is not the expected reference generator" dummy = DummyReferenceGenerator() self._reference_generator.append(dummy) self._monkey_instantiate_counter += 1 @@ -80,7 +103,9 @@ def monkey_super_set_modules(self, physical_system): :return: """ self._monkey_super_set_modules_counter += 1 - assert physical_system == self._physical_system, 'physical system is not the expected instance' + assert ( + physical_system == self._physical_system + ), "physical system is not the expected instance" def monkey_dummy_set_modules(self, physical_system): """ @@ -89,7 +114,9 @@ def monkey_dummy_set_modules(self, physical_system): :return: """ self._monkey_dummy_set_modules_counter += 1 - assert self._physical_system == physical_system, 'physical system is not the expected instance' + assert ( + self._physical_system == physical_system + ), "physical system is not the expected instance" def monkey_reset_reference(self): """ @@ -106,10 +133,16 @@ def monkey_dummy_reset(self, initial_state, initial_reference): :return: """ if type(initial_state == self._initial_state) is bool: - assert initial_state == self._initial_state, 'passed initial state is not the expected one' + assert ( + initial_state == self._initial_state + ), "passed initial state is not the expected one" else: - assert all(initial_state == self._initial_state), 'passed initial state is not the expected one' - assert initial_reference == self._initial_reference, 'passed initial reference is not the expected one' + assert all( + initial_state == self._initial_state + ), "passed initial state is not the expected one" + assert ( + initial_reference == self._initial_reference + ), "passed initial reference is not the expected one" self._monkey_dummy_reset_counter += 1 return self._reference, self._reference_observation, self._trajectory @@ -120,8 +153,10 @@ def monkey_dummy_get_reference(self, state, **kwargs): :param kwargs: :return: """ - assert all(state == self._initial_state), 'passed state is not the expected one' - assert self._kwargs == kwargs, 'Different additional arguments. Keep in mind None and {}.' + assert all(state == self._initial_state), "passed state is not the expected one" + assert ( + self._kwargs == kwargs + ), "Different additional arguments. Keep in mind None and {}." return self._reference def monkey_dummy_get_reference_observation(self, state, **kwargs): @@ -131,8 +166,10 @@ def monkey_dummy_get_reference_observation(self, state, **kwargs): :param kwargs: :return: """ - assert all(state == self._initial_state), 'passed state is not the expected one' - assert self._kwargs == kwargs, 'Different additional arguments. Keep in mind None and {}.' + assert all(state == self._initial_state), "passed state is not the expected one" + assert ( + self._kwargs == kwargs + ), "Different additional arguments. Keep in mind None and {}." return self._reference_observation @pytest.mark.parametrize( @@ -144,19 +181,28 @@ def monkey_dummy_get_reference_observation(self, state, **kwargs): [TriangularReferenceGenerator()], [SawtoothReferenceGenerator()], [ - SinusoidalReferenceGenerator(), WienerProcessReferenceGenerator(), StepReferenceGenerator(), - TriangularReferenceGenerator(), SawtoothReferenceGenerator() + SinusoidalReferenceGenerator(), + WienerProcessReferenceGenerator(), + StepReferenceGenerator(), + TriangularReferenceGenerator(), + SawtoothReferenceGenerator(), ], [SinusoidalReferenceGenerator(), WienerProcessReferenceGenerator()], - [StepReferenceGenerator(), TriangularReferenceGenerator(), SawtoothReferenceGenerator()] - ] + [ + StepReferenceGenerator(), + TriangularReferenceGenerator(), + SawtoothReferenceGenerator(), + ], + ], ) @pytest.mark.parametrize("p", [None, [0.1, 0.2, 0.3, 0.2, 0.1]]) @pytest.mark.parametrize( "super_episode_length, expected_sel", - [((200, 500), (200, 500)), (100, (100, 101)), (500, (500, 501))] + [((200, 500), (200, 500)), (100, (100, 101)), (500, (500, 501))], ) - def test_init(self, monkeypatch, setup, sub_generator, p, super_episode_length, expected_sel): + def test_init( + self, monkeypatch, setup, sub_generator, p, super_episode_length, expected_sel + ): """ test function for the initialization of a switched reference generator with different combinations of reference generators @@ -171,13 +217,23 @@ def test_init(self, monkeypatch, setup, sub_generator, p, super_episode_length, # setup test scenario self._sub_generator = sub_generator # call function to test - test_object = SwitchedReferenceGenerator(sub_generator, p=p, super_episode_length=super_episode_length) + test_object = SwitchedReferenceGenerator( + sub_generator, p=p, super_episode_length=super_episode_length + ) # verify the expected results - assert len(test_object._sub_generators) == len(sub_generator), 'unexpected number of sub generators' - assert test_object._current_episode_length == 0, 'The current episode length is not 0.' - assert test_object._super_episode_length == expected_sel, 'super episode length is not as expected' + assert len(test_object._sub_generators) == len( + sub_generator + ), "unexpected number of sub generators" + assert ( + test_object._current_episode_length == 0 + ), "The current episode length is not 0." + assert ( + test_object._super_episode_length == expected_sel + ), "super episode length is not as expected" assert test_object._current_ref_generator in sub_generator - assert test_object._sub_generators == list(sub_generator), 'Other sub generators than expected' + assert test_object._sub_generators == list( + sub_generator + ), "Other sub generators than expected" def test_set_modules(self, monkeypatch, setup): """ @@ -188,8 +244,8 @@ def test_set_modules(self, monkeypatch, setup): """ # setup test scenario sub_generator = [ - SinusoidalReferenceGenerator(reference_state='dummy_state_0'), - WienerProcessReferenceGenerator(reference_state='dummy_state_0') + SinusoidalReferenceGenerator(reference_state="dummy_state_0"), + WienerProcessReferenceGenerator(reference_state="dummy_state_0"), ] reference_states = [1, 0, 0, 0, 0, 0, 0] # Override reference spaces @@ -201,13 +257,21 @@ def test_set_modules(self, monkeypatch, setup): test_object = SwitchedReferenceGenerator(sub_generator) self._physical_system = DummyPhysicalSystem(7) - self._physical_system._state_space = gymnasium.spaces.Box(-1, 1, shape=self._physical_system.state_space.shape) + self._physical_system._state_space = gymnasium.spaces.Box( + -1, 1, shape=self._physical_system.state_space.shape + ) # call function to test test_object.set_modules(self._physical_system) # verify the expected results - assert all(test_object.reference_space.low == expected_space.low), 'Lower limit of the reference space is not 0' - assert test_object.reference_space.high == expected_space.high, 'Upper limit of the reference space is not 1' - assert np.all(test_object._referenced_states == reference_states), 'referenced states are not the expected ones' + assert all( + test_object.reference_space.low == expected_space.low + ), "Lower limit of the reference space is not 0" + assert ( + test_object.reference_space.high == expected_space.high + ), "Upper limit of the reference space is not 1" + assert np.all( + test_object._referenced_states == reference_states + ), "referenced states are not the expected ones" @pytest.mark.parametrize("initial_state", [None, [0.8, 0.6, 0.4, 0.7]]) @pytest.mark.parametrize("initial_reference", [None, 0.42]) @@ -221,21 +285,33 @@ def test_reset(self, monkeypatch, setup, initial_state, initial_reference): :return: """ # setup test scenario - monkeypatch.setattr(SwitchedReferenceGenerator, '_reset_reference', self.monkey_reset_reference) - sub_generator = [SinusoidalReferenceGenerator(), WienerProcessReferenceGenerator()] + monkeypatch.setattr( + SwitchedReferenceGenerator, "_reset_reference", self.monkey_reset_reference + ) + sub_generator = [ + SinusoidalReferenceGenerator(), + WienerProcessReferenceGenerator(), + ] self._sub_generator = sub_generator test_object = SwitchedReferenceGenerator(sub_generator) - monkeypatch.setattr(test_object._sub_generators[0], 'reset', self.monkey_dummy_reset) + monkeypatch.setattr( + test_object._sub_generators[0], "reset", self.monkey_dummy_reset + ) self._initial_state = initial_state self._initial_reference = initial_reference # call function to test res_0, res_1, res_2 = test_object.reset(initial_state, initial_reference) # verify the expected results - assert self._monkey_dummy_reset_counter == 1, 'reset of sub generators is not called once' - assert res_0 == self._reference, 'reference is not the expected one' - assert all(res_1 == self._reference_observation), 'observation is not the expected one ' - assert sum(sum(abs(res_2 - self._trajectory))) < 1E-6, \ - 'absolute difference of reference trajectory to the expected is larger than 1e-6' + assert ( + self._monkey_dummy_reset_counter == 1 + ), "reset of sub generators is not called once" + assert res_0 == self._reference, "reference is not the expected one" + assert all( + res_1 == self._reference_observation + ), "observation is not the expected one " + assert ( + sum(sum(abs(res_2 - self._trajectory))) < 1e-6 + ), "absolute difference of reference trajectory to the expected is larger than 1e-6" def test_get_reference(self, monkeypatch): """ @@ -247,14 +323,21 @@ def test_get_reference(self, monkeypatch): sub_generator = [DummyReferenceGenerator(), DummyReferenceGenerator()] self._sub_generator = sub_generator test_object = SwitchedReferenceGenerator(sub_generator) - monkeypatch.setattr(DummyReferenceGenerator, 'get_reference', self.monkey_dummy_get_reference) + monkeypatch.setattr( + DummyReferenceGenerator, "get_reference", self.monkey_dummy_get_reference + ) # call function to test reference = test_object.get_reference(self._initial_state, **self._kwargs) # verify the expected results - assert reference == self._reference, 'reference is not the expected one' + assert reference == self._reference, "reference is not the expected one" - @pytest.mark.parametrize("k, current_episode_length, k_new, reset_counter", [(10, 15, 11, 0), (10, 10, 11, 1)]) - def test_get_reference_observation(self, monkeypatch, k, current_episode_length, k_new, reset_counter): + @pytest.mark.parametrize( + "k, current_episode_length, k_new, reset_counter", + [(10, 15, 11, 0), (10, 10, 11, 1)], + ) + def test_get_reference_observation( + self, monkeypatch, k, current_episode_length, k_new, reset_counter + ): """ test get_reference_observation() :param monkeypatch: @@ -265,37 +348,63 @@ def test_get_reference_observation(self, monkeypatch, k, current_episode_length, :return: """ # setup test scenario - sub_generator = [SinusoidalReferenceGenerator(), WienerProcessReferenceGenerator()] + sub_generator = [ + SinusoidalReferenceGenerator(), + WienerProcessReferenceGenerator(), + ] self._sub_generator = sub_generator test_object = SwitchedReferenceGenerator(sub_generator) - monkeypatch.setattr(test_object, '_k', k) - monkeypatch.setattr(test_object, '_current_episode_length', current_episode_length) - monkeypatch.setattr(test_object, '_reset_reference', self.monkey_reset_reference) - monkeypatch.setattr(test_object, '_reference', self._initial_reference) - monkeypatch.setattr(test_object._current_ref_generator, 'reset', self.monkey_dummy_reset) - monkeypatch.setattr(test_object._current_ref_generator, 'get_reference_observation', - self.monkey_dummy_get_reference_observation) + monkeypatch.setattr(test_object, "_k", k) + monkeypatch.setattr( + test_object, "_current_episode_length", current_episode_length + ) + monkeypatch.setattr( + test_object, "_reset_reference", self.monkey_reset_reference + ) + monkeypatch.setattr(test_object, "_reference", self._initial_reference) + monkeypatch.setattr( + test_object._current_ref_generator, "reset", self.monkey_dummy_reset + ) + monkeypatch.setattr( + test_object._current_ref_generator, + "get_reference_observation", + self.monkey_dummy_get_reference_observation, + ) # call function to test - observation = test_object.get_reference_observation(self._initial_state, **self._kwargs) + observation = test_object.get_reference_observation( + self._initial_state, **self._kwargs + ) # verify the expected results - assert all(observation == self._reference_observation), 'observation is not the expected one' - assert test_object._k == k_new, 'unexpected new step in the reference' - assert self._monkey_reset_reference_counter == reset_counter, 'reset_reference called unexpected often' + assert all( + observation == self._reference_observation + ), "observation is not the expected one" + assert test_object._k == k_new, "unexpected new step in the reference" + assert ( + self._monkey_reset_reference_counter == reset_counter + ), "reset_reference called unexpected often" def test_reset_reference(self, monkeypatch): - sub_reference_generators = [DummyReferenceGenerator(), DummyReferenceGenerator(), DummyReferenceGenerator()] + sub_reference_generators = [ + DummyReferenceGenerator(), + DummyReferenceGenerator(), + DummyReferenceGenerator(), + ] probabilities = [0.2, 0.5, 0.3] super_episode_length = (1, 10) test_object = SwitchedReferenceGenerator( - sub_generators=sub_reference_generators, super_episode_length=super_episode_length, p=probabilities + sub_generators=sub_reference_generators, + super_episode_length=super_episode_length, + p=probabilities, ) test_object.seed(np.random.SeedSequence(123)) test_object._reset_reference() assert test_object._k == 0 - assert test_object._super_episode_length[0] \ - <= test_object._current_episode_length\ - <= test_object._super_episode_length[1] + assert ( + test_object._super_episode_length[0] + <= test_object._current_episode_length + <= test_object._super_episode_length[1] + ) assert test_object._current_ref_generator in sub_reference_generators @@ -303,6 +412,7 @@ class TestWienerProcessReferenceGenerator: """ class for testing the wiener process reference generator """ + _kwargs = None _current_value = None @@ -315,7 +425,7 @@ def monkey_get_current_value(self, value): self._monkey_get_current_value_counter += 1 return value - @pytest.mark.parametrize('sigma_range', [(2e-3, 2e-1)]) + @pytest.mark.parametrize("sigma_range", [(2e-3, 2e-1)]) def test_init(self, monkeypatch, sigma_range): """ test init() @@ -328,7 +438,9 @@ def test_init(self, monkeypatch, sigma_range): test_object = WienerProcessReferenceGenerator(sigma_range=sigma_range) # verify the expected results - assert test_object._sigma_range == sigma_range, 'sigma range is not passed correctly' + assert ( + test_object._sigma_range == sigma_range + ), "sigma range is not passed correctly" def test_reset_reference(self, monkeypatch): sigma_range = 1e-2 @@ -343,17 +455,28 @@ class MonkeyRandomGenerator: def normal(self, loc=0, scale=1, size=1): return self._rg.monkey_random_normal(loc, scale, size) - monkeypatch.setattr(SubepisodedReferenceGenerator, '_get_current_value', self.monkey_get_current_value) - test_object = WienerProcessReferenceGenerator(sigma_range=sigma_range, episode_lengths=episode_length, - limit_margin=limit_margin) - monkeypatch.setattr(test_object, '_random_generator', MonkeyRandomGenerator()) + monkeypatch.setattr( + SubepisodedReferenceGenerator, + "_get_current_value", + self.monkey_get_current_value, + ) + test_object = WienerProcessReferenceGenerator( + sigma_range=sigma_range, + episode_lengths=episode_length, + limit_margin=limit_margin, + ) + monkeypatch.setattr(test_object, "_random_generator", MonkeyRandomGenerator()) test_object._reference_value = reference_value self._monkey_get_current_value_counter = 0 self._current_value = np.log10(sigma_range) test_object._reset_reference() - assert sum(abs(test_object._reference - expected_reference)) < 1E-6, 'unexpected reference array' - assert self._monkey_get_current_value_counter == 1, 'get_current_value() not called once' + assert ( + sum(abs(test_object._reference - expected_reference)) < 1e-6 + ), "unexpected reference array" + assert ( + self._monkey_get_current_value_counter == 1 + ), "get_current_value() not called once" class TestFurtherReferenceGenerator: @@ -361,6 +484,7 @@ class TestFurtherReferenceGenerator: class for testing SawtoothReferenceGenerator, SinusoidalReferenceGenerator, StepReferenceGenerator, TriangularReferenceGenerator """ + # defined values for tests _kwargs = {} _physical_system = None @@ -377,20 +501,36 @@ def monkey_super_set_modules(self, physical_system): :return: """ self._monkey_super_set_modules_counter += 1 - assert physical_system == self._physical_system, 'physical system is not the expected instance' + assert ( + physical_system == self._physical_system + ), "physical system is not the expected instance" def monkey_get_current_value(self, value): self._monkey_get_current_value_counter += 1 return value - @pytest.mark.parametrize('amplitude_range', [(0.1, 0.8)]) - @pytest.mark.parametrize('frequency_range', [(2, 150)]) - @pytest.mark.parametrize('offset_range', [(-0.8, 0.5)]) - @pytest.mark.parametrize('kwargs', [{}]) - @pytest.mark.parametrize("reference_class", - [SawtoothReferenceGenerator, SinusoidalReferenceGenerator, StepReferenceGenerator, - TriangularReferenceGenerator]) - def test_init(self, monkeypatch, reference_class, amplitude_range, frequency_range, offset_range, kwargs): + @pytest.mark.parametrize("amplitude_range", [(0.1, 0.8)]) + @pytest.mark.parametrize("frequency_range", [(2, 150)]) + @pytest.mark.parametrize("offset_range", [(-0.8, 0.5)]) + @pytest.mark.parametrize("kwargs", [{}]) + @pytest.mark.parametrize( + "reference_class", + [ + SawtoothReferenceGenerator, + SinusoidalReferenceGenerator, + StepReferenceGenerator, + TriangularReferenceGenerator, + ], + ) + def test_init( + self, + monkeypatch, + reference_class, + amplitude_range, + frequency_range, + offset_range, + kwargs, + ): """ test initialization of different reference generators :param monkeypatch: @@ -405,21 +545,49 @@ def test_init(self, monkeypatch, reference_class, amplitude_range, frequency_ran self._kwargs = kwargs # call function to test - test_object = reference_class(amplitude_range=amplitude_range, frequency_range=frequency_range, - offset_range=offset_range, **kwargs) + test_object = reference_class( + amplitude_range=amplitude_range, + frequency_range=frequency_range, + offset_range=offset_range, + **kwargs, + ) # verify the expected results - assert test_object._amplitude_range == amplitude_range, 'amplitude range is not passed correctly' - assert test_object._frequency_range == frequency_range, 'frequency range is not passed correctly' - assert test_object._offset_range == offset_range, 'offset range is not passed correctly' - - @pytest.mark.parametrize("reference_class", - [SawtoothReferenceGenerator, SinusoidalReferenceGenerator, StepReferenceGenerator, - TriangularReferenceGenerator]) - @pytest.mark.parametrize('amplitude_range, expected_amplitude', - [((0.1, 0.8), (0.1, 0.45)), ((-0.5, 0.35), (0.0, 0.35))]) - @pytest.mark.parametrize('offset_range, expected_offset', [((-0.8, 0.5), (0.0, 0.5)), ((0.1, 0.96), (0.1, 0.9))]) - def test_set_modules(self, monkeypatch, reference_class, amplitude_range, offset_range, - expected_offset, expected_amplitude): + assert ( + test_object._amplitude_range == amplitude_range + ), "amplitude range is not passed correctly" + assert ( + test_object._frequency_range == frequency_range + ), "frequency range is not passed correctly" + assert ( + test_object._offset_range == offset_range + ), "offset range is not passed correctly" + + @pytest.mark.parametrize( + "reference_class", + [ + SawtoothReferenceGenerator, + SinusoidalReferenceGenerator, + StepReferenceGenerator, + TriangularReferenceGenerator, + ], + ) + @pytest.mark.parametrize( + "amplitude_range, expected_amplitude", + [((0.1, 0.8), (0.1, 0.45)), ((-0.5, 0.35), (0.0, 0.35))], + ) + @pytest.mark.parametrize( + "offset_range, expected_offset", + [((-0.8, 0.5), (0.0, 0.5)), ((0.1, 0.96), (0.1, 0.9))], + ) + def test_set_modules( + self, + monkeypatch, + reference_class, + amplitude_range, + offset_range, + expected_offset, + expected_amplitude, + ): """ test set_modules() :param monkeypatch: @@ -432,31 +600,91 @@ def test_set_modules(self, monkeypatch, reference_class, amplitude_range, offset """ # setup test scenario self._physical_system = DummyPhysicalSystem() - monkeypatch.setattr(SubepisodedReferenceGenerator, 'set_modules', self.monkey_super_set_modules) - test_object = reference_class(amplitude_range=amplitude_range, offset_range=offset_range) - monkeypatch.setattr(test_object, '_limit_margin', self._limit_margin) + monkeypatch.setattr( + SubepisodedReferenceGenerator, "set_modules", self.monkey_super_set_modules + ) + test_object = reference_class( + amplitude_range=amplitude_range, offset_range=offset_range + ) + monkeypatch.setattr(test_object, "_limit_margin", self._limit_margin) # call function to test test_object.set_modules(self._physical_system) # verify the expected results - assert all(test_object._amplitude_range == expected_amplitude), 'amplitude range is not as expected' - assert all(test_object._offset_range == expected_offset), 'offset range is not as expected' - assert self._monkey_super_set_modules_counter == 1, 'super().set_modules() is not called once' - - @pytest.mark.parametrize('reference_class, expected_reference, frequency_range', [ - (SawtoothReferenceGenerator, np.array([-0.2, -0.1, 0.0, 0.1, 0.2, 0.3, -0.4, -0.3, -0.2, -0.1]), 1 / 8), - (SinusoidalReferenceGenerator, - np.array([0.4, 0.2828427, 0.0, -0.2828427, -0.4, -0.2828427, 0.0, 0.2828427, 0.4, 0.2828427]), 1 / 8), - (StepReferenceGenerator, np.array([0.4, -0.4, -0.4, -0.4, 0.4, 0.4, 0.4, -0.4, -0.4, -0.4]), 1/6), - (TriangularReferenceGenerator, np.array([0.4, 0.2666667, 0.133333, 0.0, -0.133333, -0.2666667, -0.4, 0.0, 0.4, - 0.2666667]), 1 / 8)]) - def test_reset_reference(self, monkeypatch, reference_class, expected_reference, frequency_range): + assert all( + test_object._amplitude_range == expected_amplitude + ), "amplitude range is not as expected" + assert all( + test_object._offset_range == expected_offset + ), "offset range is not as expected" + assert ( + self._monkey_super_set_modules_counter == 1 + ), "super().set_modules() is not called once" + + @pytest.mark.parametrize( + "reference_class, expected_reference, frequency_range", + [ + ( + SawtoothReferenceGenerator, + np.array([-0.2, -0.1, 0.0, 0.1, 0.2, 0.3, -0.4, -0.3, -0.2, -0.1]), + 1 / 8, + ), + ( + SinusoidalReferenceGenerator, + np.array( + [ + 0.4, + 0.2828427, + 0.0, + -0.2828427, + -0.4, + -0.2828427, + 0.0, + 0.2828427, + 0.4, + 0.2828427, + ] + ), + 1 / 8, + ), + ( + StepReferenceGenerator, + np.array([0.4, -0.4, -0.4, -0.4, 0.4, 0.4, 0.4, -0.4, -0.4, -0.4]), + 1 / 6, + ), + ( + TriangularReferenceGenerator, + np.array( + [ + 0.4, + 0.2666667, + 0.133333, + 0.0, + -0.133333, + -0.2666667, + -0.4, + 0.0, + 0.4, + 0.2666667, + ] + ), + 1 / 8, + ), + ], + ) + def test_reset_reference( + self, monkeypatch, reference_class, expected_reference, frequency_range + ): # setup test scenario amplitude_range = 0.8 offset_range = 0.5 limit_margin = 0.4 episode_length = 10 dummy_physical_system = DummyPhysicalSystem() - monkeypatch.setattr(SubepisodedReferenceGenerator, '_get_current_value', self.monkey_get_current_value) + monkeypatch.setattr( + SubepisodedReferenceGenerator, + "_get_current_value", + self.monkey_get_current_value, + ) class MonkeyRandomGenerator: _rg = DummyRandom() @@ -468,32 +696,38 @@ def triangular(self, left=-1, mode=0, right=1): return self._rg.monkey_random_triangular(left, mode, right) test_object = reference_class( - amplitude_range=amplitude_range, frequency_range=frequency_range, - offset_range=offset_range, limit_margin=limit_margin, - episode_lengths=episode_length, reference_state='dummy_state_0' + amplitude_range=amplitude_range, + frequency_range=frequency_range, + offset_range=offset_range, + limit_margin=limit_margin, + episode_lengths=episode_length, + reference_state="dummy_state_0", ) - monkeypatch.setattr(test_object, '_random_generator', MonkeyRandomGenerator()) + monkeypatch.setattr(test_object, "_random_generator", MonkeyRandomGenerator()) test_object.set_modules(dummy_physical_system) # call function to test test_object._reset_reference() # verify expected results - assert sum(abs(expected_reference - test_object._reference)) < 1E-6, 'unexpected reference' + assert ( + sum(abs(expected_reference - test_object._reference)) < 1e-6 + ), "unexpected reference" class TestSubepisodedReferenceGenerator: """ class to the SubepisodedReferenceGenerator """ + # defined values for tests _episode_length = (10, 50) - _reference_state = 'dummy_1' + _reference_state = "dummy_1" _referenced_states = np.array([0, 1, 0]) _referenced_states = _referenced_states.astype(bool) _value_range = None _current_value = 35 _initial_state = None _physical_system = DummyPhysicalSystem() - _state_names = ['dummy_0', 'dummy_1', 'dummy_2'] + _state_names = ["dummy_0", "dummy_1", "dummy_2"] _nominals = np.array([1, 2, 3]) _limits = np.array([5, 7, 6]) _reference = np.array([0, 1, 0]) @@ -518,7 +752,7 @@ def monkey_get_current_value(self, value_range): :param value_range: :return: """ - assert value_range == self._value_range, 'value range is not as expected' + assert value_range == self._value_range, "value range is not as expected" return self._current_value def monkey_reset_reference(self): @@ -534,7 +768,7 @@ def monkey_super_reset(self, initial_state): :param initial_state: :return: """ - assert initial_state == self._initial_state, 'initial state is not as expected' + assert initial_state == self._initial_state, "initial state is not as expected" self._monkey_super_reset_counter += 1 return self._reference @@ -545,9 +779,10 @@ def monkey_state_array(self, input_values, state_names): :param state_names: :return: """ - assert input_values == {self._reference_state: 1}, \ - 'the input values are not a dict with the reference state and value 1' - assert state_names == self._state_names, 'state names are not as expected' + assert input_values == { + self._reference_state: 1 + }, "the input values are not a dict with the reference state and value 1" + assert state_names == self._state_names, "state names are not as expected" return np.array([0, 1, 0]) def monkey_super_set_modules(self, physical_system): @@ -557,7 +792,9 @@ def monkey_super_set_modules(self, physical_system): :return: """ self._monkey_super_set_modules_counter += 1 - assert physical_system == self._physical_system, 'physical system is not the expected instance' + assert ( + physical_system == self._physical_system + ), "physical system is not the expected instance" @pytest.mark.parametrize("limit_margin", [None, 0.3, (-0.1, 0.8)]) def test_init(self, monkeypatch, limit_margin): @@ -570,20 +807,37 @@ def test_init(self, monkeypatch, limit_margin): # setup test scenario self._value_range = self._episode_length - monkeypatch.setattr(SubepisodedReferenceGenerator, '_get_current_value', self.monkey_get_current_value) + monkeypatch.setattr( + SubepisodedReferenceGenerator, + "_get_current_value", + self.monkey_get_current_value, + ) # call function to test - test_object = SubepisodedReferenceGenerator(reference_state=self._reference_state, - episode_lengths=self._episode_length, limit_margin=limit_margin) + test_object = SubepisodedReferenceGenerator( + reference_state=self._reference_state, + episode_lengths=self._episode_length, + limit_margin=limit_margin, + ) # verify the expected results - assert test_object._limit_margin == limit_margin, 'limit margin is not passed correctly' - assert test_object._reference_value == 0.0, 'the reference value is not 0' - assert test_object._reference_state == self._reference_state, 'reference state is not passed correctly' - assert test_object._episode_len_range == self._episode_length, 'episode length is not passed correctly' - assert test_object._current_episode_length == self._current_value, 'current episode length is not as expected' - assert test_object._k == 0, 'current reference step is not 0' - - @pytest.mark.parametrize("limit_margin, expected_low, expected_high", - [(None, 0, 2 / 7), (0.3, 0.0, 0.3), ((-0.1, 0.8), 0, 0.8)]) + assert ( + test_object._limit_margin == limit_margin + ), "limit margin is not passed correctly" + assert test_object._reference_value == 0.0, "the reference value is not 0" + assert ( + test_object._reference_state == self._reference_state + ), "reference state is not passed correctly" + assert ( + test_object._episode_len_range == self._episode_length + ), "episode length is not passed correctly" + assert ( + test_object._current_episode_length == self._current_value + ), "current episode length is not as expected" + assert test_object._k == 0, "current reference step is not 0" + + @pytest.mark.parametrize( + "limit_margin, expected_low, expected_high", + [(None, 0, 2 / 7), (0.3, 0.0, 0.3), ((-0.1, 0.8), 0, 0.8)], + ) def test_set_modules(self, monkeypatch, limit_margin, expected_low, expected_high): """ test set_modules() @@ -594,23 +848,38 @@ def test_set_modules(self, monkeypatch, limit_margin, expected_low, expected_hig :return: """ # setup test scenario - monkeypatch.setattr(ReferenceGenerator, 'set_modules', self.monkey_super_set_modules) - monkeypatch.setattr(srg, 'set_state_array', self.monkey_state_array) - test_object = SubepisodedReferenceGenerator(reference_state=self._reference_state, limit_margin=limit_margin) + monkeypatch.setattr( + ReferenceGenerator, "set_modules", self.monkey_super_set_modules + ) + monkeypatch.setattr(srg, "set_state_array", self.monkey_state_array) + test_object = SubepisodedReferenceGenerator( + reference_state=self._reference_state, limit_margin=limit_margin + ) # call function to test test_object.set_modules(self._physical_system) # verify the expected results - assert self._monkey_super_set_modules_counter == 1, 'super().set_modules() is not called once' - assert all(test_object.reference_space.low == expected_low), 'lower reference space not as expected' - assert all(test_object.reference_space.high == expected_high), 'upper reference space not as expected' - test_object._limit_margin = ['test'] + assert ( + self._monkey_super_set_modules_counter == 1 + ), "super().set_modules() is not called once" + assert all( + test_object.reference_space.low == expected_low + ), "lower reference space not as expected" + assert all( + test_object.reference_space.high == expected_high + ), "upper reference space not as expected" + test_object._limit_margin = ["test"] # call function to test with pytest.raises(Exception): test_object.set_modules(self._physical_system) - @pytest.mark.parametrize('initial_state', [None, _initial_state]) - @pytest.mark.parametrize('initial_reference, expected_reference', [(np.array([0.2, 0.4, 0.7]), 0.4), (None, 0.0)]) - def test_reset(self, monkeypatch, initial_reference, initial_state, expected_reference): + @pytest.mark.parametrize("initial_state", [None, _initial_state]) + @pytest.mark.parametrize( + "initial_reference, expected_reference", + [(np.array([0.2, 0.4, 0.7]), 0.4), (None, 0.0)], + ) + def test_reset( + self, monkeypatch, initial_reference, initial_state, expected_reference + ): """ test reset() :param monkeypatch: @@ -620,16 +889,22 @@ def test_reset(self, monkeypatch, initial_reference, initial_state, expected_ref :return: """ # setup test scenario - monkeypatch.setattr(ReferenceGenerator, 'reset', self.monkey_super_reset) - test_object = SubepisodedReferenceGenerator(reference_state=self._reference_state) - monkeypatch.setattr(test_object, '_referenced_states', self._referenced_states) + monkeypatch.setattr(ReferenceGenerator, "reset", self.monkey_super_reset) + test_object = SubepisodedReferenceGenerator( + reference_state=self._reference_state + ) + monkeypatch.setattr(test_object, "_referenced_states", self._referenced_states) # call function to test reference = test_object.reset(initial_state, initial_reference) # verify the expected results - assert all(reference == self._reference), 'reference not as expected' - assert test_object._current_episode_length == -1, 'current episode length is not -1' - assert test_object._reference_value == expected_reference, 'unexpected reference value' - assert self._monkey_super_reset_counter == 1, 'super().reset() not called once' + assert all(reference == self._reference), "reference not as expected" + assert ( + test_object._current_episode_length == -1 + ), "current episode length is not -1" + assert ( + test_object._reference_value == expected_reference + ), "unexpected reference value" + assert self._monkey_super_reset_counter == 1, "super().reset() not called once" def test_get_reference(self, monkeypatch): """ @@ -639,16 +914,21 @@ def test_get_reference(self, monkeypatch): """ # setup test scenario test_object = SubepisodedReferenceGenerator() - monkeypatch.setattr(test_object, '_referenced_states', self._referenced_states) - monkeypatch.setattr(test_object, '_reference_value', 0.4) + monkeypatch.setattr(test_object, "_referenced_states", self._referenced_states) + monkeypatch.setattr(test_object, "_reference_value", 0.4) # call function to test reference = test_object.get_reference() # verify the expected results - assert all(reference == np.array([0, 0.4, 0])), 'unexpected reference' + assert all(reference == np.array([0, 0.4, 0])), "unexpected reference" - @pytest.mark.parametrize('k, expected_reference, expected_parameter', [(8, 0.6, (10, 0)), (10, 0.4, (35, 1))]) + @pytest.mark.parametrize( + "k, expected_reference, expected_parameter", + [(8, 0.6, (10, 0)), (10, 0.4, (35, 1))], + ) # setup test scenario - def test_get_reference_observation(self, monkeypatch, k, expected_reference, expected_parameter): + def test_get_reference_observation( + self, monkeypatch, k, expected_reference, expected_parameter + ): """ test get_reference_observation() :param monkeypatch: @@ -657,24 +937,43 @@ def test_get_reference_observation(self, monkeypatch, k, expected_reference, exp :param expected_parameter: expected counter for reset reference :return: """ - monkeypatch.setattr(SubepisodedReferenceGenerator, '_get_current_value', self.monkey_get_current_value) - monkeypatch.setattr(SubepisodedReferenceGenerator, '_reset_reference', self.monkey_reset_reference) + monkeypatch.setattr( + SubepisodedReferenceGenerator, + "_get_current_value", + self.monkey_get_current_value, + ) + monkeypatch.setattr( + SubepisodedReferenceGenerator, + "_reset_reference", + self.monkey_reset_reference, + ) self._value_range = self._episode_length - test_object = SubepisodedReferenceGenerator(episode_lengths=self._episode_length) - monkeypatch.setattr(test_object, '_reference', self._reference_trajectory) - monkeypatch.setattr(test_object, '_k', k) - monkeypatch.setattr(test_object, '_current_episode_length', 10) + test_object = SubepisodedReferenceGenerator( + episode_lengths=self._episode_length + ) + monkeypatch.setattr(test_object, "_reference", self._reference_trajectory) + monkeypatch.setattr(test_object, "_k", k) + monkeypatch.setattr(test_object, "_current_episode_length", 10) # call function to test reference = test_object.get_reference_observation() # verify the expected results - assert reference == np.array([expected_reference]), 'unexpected reference' - assert test_object._current_episode_length == expected_parameter[0], 'unexpected current episode length' - assert self._monkey_reset_reference_counter == expected_parameter[1], \ - 'unexpected number of calls of reset_reference, depends on the setting' + assert reference == np.array([expected_reference]), "unexpected reference" + assert ( + test_object._current_episode_length == expected_parameter[0] + ), "unexpected current episode length" + assert ( + self._monkey_reset_reference_counter == expected_parameter[1] + ), "unexpected number of calls of reset_reference, depends on the setting" @pytest.mark.parametrize( - 'value_range, expected_value', - [(12, 12), (1.0365, 1.0365), ([0, 1.6], 0.4), ((-0.12, 0.4), 0.01), (np.array([-0.5, 0.6]), -0.225)] + "value_range, expected_value", + [ + (12, 12), + (1.0365, 1.0365), + ([0, 1.6], 0.4), + ((-0.12, 0.4), 0.01), + (np.array([-0.5, 0.6]), -0.225), + ], ) def test_get_current_value(self, monkeypatch, value_range, expected_value): # setup test scenario @@ -688,15 +987,16 @@ class MonkeyUniformGenerator: def uniform(self, mu=0, sigma=1): return self._rg.monkey_random_rand() - monkeypatch.setattr(test_object, '_random_generator', MonkeyUniformGenerator()) + monkeypatch.setattr(test_object, "_random_generator", MonkeyUniformGenerator()) val = test_object._get_current_value(value_range) # verify expected results - assert abs(val - expected_value) < 1E-6, 'unexpected value from get_current_value().' + assert ( + abs(val - expected_value) < 1e-6 + ), "unexpected value from get_current_value()." class TestConstReferenceGenerator(TestReferenceGenerator): - - key = 'ConstReference' + key = "ConstReference" class_to_test = ConstReferenceGenerator @pytest.fixture @@ -705,14 +1005,20 @@ def physical_system(self): @pytest.fixture def reference_generator(self, physical_system): - rg = ConstReferenceGenerator(reference_state=physical_system.state_names[1], reference_value=1.0) + rg = ConstReferenceGenerator( + reference_state=physical_system.state_names[1], reference_value=1.0 + ) rg.set_modules(physical_system) return rg - @pytest.mark.parametrize('reference_value, reference_state', [(0.8, 'abc')]) + @pytest.mark.parametrize("reference_value, reference_state", [(0.8, "abc")]) def test_initialization(self, reference_value, reference_state): - rg = ConstReferenceGenerator(reference_value=reference_value, reference_state=reference_state) - assert rg.reference_space == Box(np.array([reference_value]), np.array([reference_value]), dtype=np.float64) + rg = ConstReferenceGenerator( + reference_value=reference_value, reference_state=reference_state + ) + assert rg.reference_space == Box( + np.array([reference_value]), np.array([reference_value]), dtype=np.float64 + ) assert rg._reference_value == reference_value assert rg._reference_state == reference_state @@ -728,21 +1034,20 @@ def test_get_reference_observation(self, reference_generator): assert all(reference_generator.get_reference_observation() == np.array([1])) def test_registered(self): - if self.key != '': + if self.key != "": rg = gem.utils.instantiate(ReferenceGenerator, self.key) assert type(rg) == self.class_to_test class TestMultipleReferenceGenerator(TestReferenceGenerator): - - key = 'MultipleReference' + key = "MultipleReference" class_to_test = MultipleReferenceGenerator @pytest.fixture def reference_generator(self): physical_system = DummyPhysicalSystem(state_length=3) - sub_generator_1 = DummyReferenceGenerator(reference_state='dummy_state_0') - sub_generator_2 = DummyReferenceGenerator(reference_state='dummy_state_1') + sub_generator_1 = DummyReferenceGenerator(reference_state="dummy_state_0") + sub_generator_2 = DummyReferenceGenerator(reference_state="dummy_state_1") rg = self.class_to_test([sub_generator_1, sub_generator_2]) sub_generator_1._referenced_states = np.array([False, False, True]) sub_generator_2._referenced_states = np.array([False, True, False]) @@ -755,8 +1060,8 @@ def reference_generator(self): def test_set_modules(self): physical_system = DummyPhysicalSystem(state_length=3) - sub_generator_1 = DummyReferenceGenerator(reference_state='dummy_state_0') - sub_generator_2 = DummyReferenceGenerator(reference_state='dummy_state_1') + sub_generator_1 = DummyReferenceGenerator(reference_state="dummy_state_0") + sub_generator_2 = DummyReferenceGenerator(reference_state="dummy_state_1") rg = self.class_to_test([sub_generator_1, sub_generator_2]) sub_generator_1._referenced_states = np.array([False, False, True]) sub_generator_2._referenced_states = np.array([False, True, False]) @@ -775,13 +1080,17 @@ def test_reset(self, reference_generator): def test_get_reference(self, reference_generator): assert all(reference_generator.get_reference(0) == np.array([0, 1, 1])) - def test_reference_observation(self,reference_generator): - assert all(reference_generator.get_reference_observation(0) == np.array([-1, 1])) + def test_reference_observation(self, reference_generator): + assert all( + reference_generator.get_reference_observation(0) == np.array([-1, 1]) + ) def test_registered(self): - if self.key != '': - rg = gem.utils.instantiate(ReferenceGenerator, self.key, sub_generators=[DummyReferenceGenerator]) + if self.key != "": + rg = gem.utils.instantiate( + ReferenceGenerator, self.key, sub_generators=[DummyReferenceGenerator] + ) assert type(rg) == self.class_to_test -# endregion +# endregion diff --git a/tests/test_reward_functions/test_reward_functions.py b/tests/test_reward_functions/test_reward_functions.py index 5291cd73..b7e57d4e 100644 --- a/tests/test_reward_functions/test_reward_functions.py +++ b/tests/test_reward_functions/test_reward_functions.py @@ -2,7 +2,11 @@ import pytest from gym_electric_motor import RewardFunction -from ..testing_utils import DummyReferenceGenerator, DummyPhysicalSystem, DummyConstraintMonitor +from ..testing_utils import ( + DummyReferenceGenerator, + DummyPhysicalSystem, + DummyConstraintMonitor, +) class MockRewardFunction(RewardFunction): @@ -13,52 +17,56 @@ def reward(self, state, reference, k=None, action=None, violation_degree=0.0): class TestRewardFunction: - @pytest.fixture def reward_function(self): return MockRewardFunction() - @pytest.mark.parametrize(['state', 'reference'], [ - [np.array([1.0, 2.0, 3.0, 4.0]), np.array([0.0, 2.0, 4.0, 0.0])], - [np.array([1.0, -2.0]), np.array([-1.0, 20.0])], - [np.array([-24.0]), np.array([0.0])] - ]) - @pytest.mark.parametrize('k', [0, 1, 2, 3, 4, 90999]) - @pytest.mark.parametrize('violation_degree', [0.0, 0.25, 0.5, 1.0]) - @pytest.mark.parametrize('action', [ - np.array([0.0, 1.0]), - np.array([-1.0]), - 8, - 0 - ]) + @pytest.mark.parametrize( + ["state", "reference"], + [ + [np.array([1.0, 2.0, 3.0, 4.0]), np.array([0.0, 2.0, 4.0, 0.0])], + [np.array([1.0, -2.0]), np.array([-1.0, 20.0])], + [np.array([-24.0]), np.array([0.0])], + ], + ) + @pytest.mark.parametrize("k", [0, 1, 2, 3, 4, 90999]) + @pytest.mark.parametrize("violation_degree", [0.0, 0.25, 0.5, 1.0]) + @pytest.mark.parametrize("action", [np.array([0.0, 1.0]), np.array([-1.0]), 8, 0]) def test_call(self, reward_function, state, reference, k, action, violation_degree): """Test, if the call function is equal to the reward function""" rew_call = reward_function(state, reference, k, action, violation_degree) rew_fct = reward_function.reward(state, reference, k, action, violation_degree) assert rew_call == rew_fct - @pytest.mark.parametrize(['initial_state', 'initial_reference'], [ - [None, None], - [np.array([1.0, 2.0]), np.array([0.0, -2.0])], - [np.array([1.0, 2.0]), None], - [None, np.array([1.0, 2.0])], - ]) + @pytest.mark.parametrize( + ["initial_state", "initial_reference"], + [ + [None, None], + [np.array([1.0, 2.0]), np.array([0.0, -2.0])], + [np.array([1.0, 2.0]), None], + [None, np.array([1.0, 2.0])], + ], + ) def test_reset_interface(self, reward_function, initial_state, initial_reference): """Test, if the reset function accepts the correct parameters.""" kwargs = dict() if initial_state is not None: - kwargs['initial_state'] = initial_state + kwargs["initial_state"] = initial_state if initial_reference is not None: - kwargs['initial_reference'] = initial_reference + kwargs["initial_reference"] = initial_reference # It has to run through without any error reward_function.reset(**kwargs) - @pytest.mark.parametrize(['physical_system', 'reference_generator', 'constraint_monitor'], [[ - DummyPhysicalSystem(), DummyReferenceGenerator(), DummyConstraintMonitor()] - ]) - def test_set_modules_interface(self, reward_function, physical_system, reference_generator, constraint_monitor): - reward_function.set_modules(physical_system, reference_generator, constraint_monitor) + @pytest.mark.parametrize( + ["physical_system", "reference_generator", "constraint_monitor"], + [[DummyPhysicalSystem(), DummyReferenceGenerator(), DummyConstraintMonitor()]], + ) + def test_set_modules_interface( + self, reward_function, physical_system, reference_generator, constraint_monitor + ): + reward_function.set_modules( + physical_system, reference_generator, constraint_monitor + ) def test_close_interface(self, reward_function): reward_function.close() - diff --git a/tests/test_reward_functions/test_weighted_sum_of_errors.py b/tests/test_reward_functions/test_weighted_sum_of_errors.py index 116fbc82..ccc02af0 100644 --- a/tests/test_reward_functions/test_weighted_sum_of_errors.py +++ b/tests/test_reward_functions/test_weighted_sum_of_errors.py @@ -1,100 +1,151 @@ import pytest import numpy as np -from gym_electric_motor.reward_functions.weighted_sum_of_errors import WeightedSumOfErrors +from gym_electric_motor.reward_functions.weighted_sum_of_errors import ( + WeightedSumOfErrors, +) from .test_reward_functions import TestRewardFunction -from ..testing_utils import DummyPhysicalSystem, DummyReferenceGenerator, DummyConstraintMonitor +from ..testing_utils import ( + DummyPhysicalSystem, + DummyReferenceGenerator, + DummyConstraintMonitor, +) class TestWeightedSumOfErrors(TestRewardFunction): - class_to_test = WeightedSumOfErrors @pytest.fixture def reward_function(self): rf = WeightedSumOfErrors() - rf.set_modules(DummyPhysicalSystem(), DummyReferenceGenerator(), DummyConstraintMonitor()) + rf.set_modules( + DummyPhysicalSystem(), DummyReferenceGenerator(), DummyConstraintMonitor() + ) return rf - @pytest.mark.parametrize(['ps', 'reward_power', 'n'], [ - [DummyPhysicalSystem(state_length=2), [1, 2], np.array([1, 2])], - [DummyPhysicalSystem(state_length=3), 2, np.array([2, 2, 2])], - [DummyPhysicalSystem(state_length=1), [1], np.array([1])], - [DummyPhysicalSystem(state_length=2), dict(dummy_state_0=2), np.array([2, 0])] - ]) - @pytest.mark.parametrize('rg', [DummyReferenceGenerator()]) - @pytest.mark.parametrize('cm', [DummyConstraintMonitor()]) + @pytest.mark.parametrize( + ["ps", "reward_power", "n"], + [ + [DummyPhysicalSystem(state_length=2), [1, 2], np.array([1, 2])], + [DummyPhysicalSystem(state_length=3), 2, np.array([2, 2, 2])], + [DummyPhysicalSystem(state_length=1), [1], np.array([1])], + [ + DummyPhysicalSystem(state_length=2), + dict(dummy_state_0=2), + np.array([2, 0]), + ], + ], + ) + @pytest.mark.parametrize("rg", [DummyReferenceGenerator()]) + @pytest.mark.parametrize("cm", [DummyConstraintMonitor()]) def test_reward_powers(self, ps, rg, cm, reward_power, n): rf = self.class_to_test(reward_power=reward_power) - rf.set_modules( - ps, rg, cm) + rf.set_modules(ps, rg, cm) assert all(rf._n == n) - @pytest.mark.parametrize(['ps', 'reward_weights', 'expected_rw'], [ - [DummyPhysicalSystem(state_length=2), [1, 2], np.array([1, 2])], - [DummyPhysicalSystem(state_length=3), 2, np.array([2, 2, 2])], - [DummyPhysicalSystem(state_length=1), [1], np.array([1])], - [DummyPhysicalSystem(state_length=2), dict(dummy_state_0=2), np.array([2, 0])], - [DummyPhysicalSystem(state_length=2), None, np.array([1.0, 0.0])], - ]) - @pytest.mark.parametrize('rg', [DummyReferenceGenerator()]) - @pytest.mark.parametrize('cm', [DummyConstraintMonitor()]) + @pytest.mark.parametrize( + ["ps", "reward_weights", "expected_rw"], + [ + [DummyPhysicalSystem(state_length=2), [1, 2], np.array([1, 2])], + [DummyPhysicalSystem(state_length=3), 2, np.array([2, 2, 2])], + [DummyPhysicalSystem(state_length=1), [1], np.array([1])], + [ + DummyPhysicalSystem(state_length=2), + dict(dummy_state_0=2), + np.array([2, 0]), + ], + [DummyPhysicalSystem(state_length=2), None, np.array([1.0, 0.0])], + ], + ) + @pytest.mark.parametrize("rg", [DummyReferenceGenerator()]) + @pytest.mark.parametrize("cm", [DummyConstraintMonitor()]) def test_reward_weights(self, ps, rg, cm, reward_weights, expected_rw): rg.set_modules(ps) rf = self.class_to_test(reward_weights=reward_weights) rf.set_modules(ps, rg, cm) assert all(rf._reward_weights == expected_rw) - @pytest.mark.parametrize(['ps', 'violation_reward', 'gamma', 'expected_vr'], [ - [DummyPhysicalSystem(state_length=2), 0.0, 0.9, 0.0], - [DummyPhysicalSystem(state_length=3), -100000, 0.99, -100000], - [DummyPhysicalSystem(state_length=1), None, 0.99, -100], - [DummyPhysicalSystem(state_length=2), None, 0.5, -2], - [DummyPhysicalSystem(state_length=2), 5000, 0.5, 5000], - ]) - @pytest.mark.parametrize('rg', [DummyReferenceGenerator()]) - @pytest.mark.parametrize('cm', [DummyConstraintMonitor()]) + @pytest.mark.parametrize( + ["ps", "violation_reward", "gamma", "expected_vr"], + [ + [DummyPhysicalSystem(state_length=2), 0.0, 0.9, 0.0], + [DummyPhysicalSystem(state_length=3), -100000, 0.99, -100000], + [DummyPhysicalSystem(state_length=1), None, 0.99, -100], + [DummyPhysicalSystem(state_length=2), None, 0.5, -2], + [DummyPhysicalSystem(state_length=2), 5000, 0.5, 5000], + ], + ) + @pytest.mark.parametrize("rg", [DummyReferenceGenerator()]) + @pytest.mark.parametrize("cm", [DummyConstraintMonitor()]) def test_violation_reward(self, ps, rg, cm, violation_reward, gamma, expected_vr): rg.set_modules(ps) rf = self.class_to_test(violation_reward=violation_reward, gamma=gamma) rf.set_modules(ps, rg, cm) assert np.isclose(rf._violation_reward, expected_vr, rtol=1e-4) - @pytest.mark.parametrize(['ps', 'reward_weights', 'normed_rw', 'expected_rw'], [ - [DummyPhysicalSystem(state_length=2), [1, 2], False, np.array([1, 2])], - [DummyPhysicalSystem(state_length=2), [1, 2], True, np.array([1/3, 2/3])], - [DummyPhysicalSystem(state_length=3), 2, True, np.array([1/3, 1/3, 1/3])], - [DummyPhysicalSystem(state_length=1), [1], False, np.array([1])], - [DummyPhysicalSystem(state_length=2), dict(dummy_state_0=2), True, np.array([1, 0])], - ]) - @pytest.mark.parametrize('rg', [DummyReferenceGenerator()]) - @pytest.mark.parametrize('cm', [DummyConstraintMonitor()]) - def test_normed_reward_weights(self, ps, rg, cm, reward_weights, normed_rw, expected_rw): + @pytest.mark.parametrize( + ["ps", "reward_weights", "normed_rw", "expected_rw"], + [ + [DummyPhysicalSystem(state_length=2), [1, 2], False, np.array([1, 2])], + [ + DummyPhysicalSystem(state_length=2), + [1, 2], + True, + np.array([1 / 3, 2 / 3]), + ], + [ + DummyPhysicalSystem(state_length=3), + 2, + True, + np.array([1 / 3, 1 / 3, 1 / 3]), + ], + [DummyPhysicalSystem(state_length=1), [1], False, np.array([1])], + [ + DummyPhysicalSystem(state_length=2), + dict(dummy_state_0=2), + True, + np.array([1, 0]), + ], + ], + ) + @pytest.mark.parametrize("rg", [DummyReferenceGenerator()]) + @pytest.mark.parametrize("cm", [DummyConstraintMonitor()]) + def test_normed_reward_weights( + self, ps, rg, cm, reward_weights, normed_rw, expected_rw + ): rg.set_modules(ps) - rf = self.class_to_test(reward_weights=reward_weights, normed_reward_weights=normed_rw) + rf = self.class_to_test( + reward_weights=reward_weights, normed_reward_weights=normed_rw + ) rf.set_modules(ps, rg, cm) assert all(rf._reward_weights == expected_rw) - @pytest.mark.parametrize(['ps', 'rw', 'bias', 'expected_rr'], [ - [DummyPhysicalSystem(state_length=2), [1, 2], 0.0, (-3.0, 0.0)], - [DummyPhysicalSystem(state_length=2), [1, 0], 1.0, (0.0, 1.0)], - [DummyPhysicalSystem(state_length=3), [5, 4, 3], 9, (-3.0, 9.0)], - ]) - @pytest.mark.parametrize('rg', [DummyReferenceGenerator()]) - @pytest.mark.parametrize('cm', [DummyConstraintMonitor()]) + @pytest.mark.parametrize( + ["ps", "rw", "bias", "expected_rr"], + [ + [DummyPhysicalSystem(state_length=2), [1, 2], 0.0, (-3.0, 0.0)], + [DummyPhysicalSystem(state_length=2), [1, 0], 1.0, (0.0, 1.0)], + [DummyPhysicalSystem(state_length=3), [5, 4, 3], 9, (-3.0, 9.0)], + ], + ) + @pytest.mark.parametrize("rg", [DummyReferenceGenerator()]) + @pytest.mark.parametrize("cm", [DummyConstraintMonitor()]) def test_reward_range(self, ps, rg, cm, rw, bias, expected_rr): rg.set_modules(ps) rf = self.class_to_test(reward_weights=rw, bias=bias) rf.set_modules(ps, rg, cm) assert rf.reward_range == expected_rr - @pytest.mark.parametrize(['ps', 'rw', 'bias', 'expected_bias'], [ - [DummyPhysicalSystem(state_length=2), [2.0, 0.5], 0.9, 0.9], - [DummyPhysicalSystem(state_length=3), 100000, 'positive', 300000], - [DummyPhysicalSystem(state_length=1), [5], 0, 0], - ]) - @pytest.mark.parametrize('rg', [DummyReferenceGenerator()]) - @pytest.mark.parametrize('cm', [DummyConstraintMonitor()]) + @pytest.mark.parametrize( + ["ps", "rw", "bias", "expected_bias"], + [ + [DummyPhysicalSystem(state_length=2), [2.0, 0.5], 0.9, 0.9], + [DummyPhysicalSystem(state_length=3), 100000, "positive", 300000], + [DummyPhysicalSystem(state_length=1), [5], 0, 0], + ], + ) + @pytest.mark.parametrize("rg", [DummyReferenceGenerator()]) + @pytest.mark.parametrize("cm", [DummyConstraintMonitor()]) def test_bias(self, ps, rg, cm, rw, bias, expected_bias): rg.set_modules(ps) rf = self.class_to_test(reward_weights=rw, bias=bias) @@ -102,19 +153,76 @@ def test_bias(self, ps, rg, cm, rw, bias, expected_bias): assert rf._bias == expected_bias @pytest.mark.parametrize( - ['reward_weights', 'violation_reward', 'bias', 'violation_degree', 'state', 'reference', 'expected_rw'], [ - [[1, 2, 0], -10000, 0.0, 0.0, np.array([0, 1, 0]), np.array([0, 1, 5]), 0.0], - [[1, 2, 0], -10000, 10.0, 0.0, np.array([0, 1, 0]), np.array([0, 1, 5]), 10.0], - [[1, 2, 0], -10000, 10.0, 1.0, np.array([0, 1, 0]), np.array([0, 1, 5]), -10000.0], - [[1, 2, 0], -10000, 10.0, 0.5, np.array([0, 1, 0]), np.array([0, 1, 5]), -4995.0], - ]) - @pytest.mark.parametrize('ps', [DummyPhysicalSystem(state_length=3)]) - @pytest.mark.parametrize('rg', [DummyReferenceGenerator()]) - @pytest.mark.parametrize('cm', [DummyConstraintMonitor()]) + [ + "reward_weights", + "violation_reward", + "bias", + "violation_degree", + "state", + "reference", + "expected_rw", + ], + [ + [ + [1, 2, 0], + -10000, + 0.0, + 0.0, + np.array([0, 1, 0]), + np.array([0, 1, 5]), + 0.0, + ], + [ + [1, 2, 0], + -10000, + 10.0, + 0.0, + np.array([0, 1, 0]), + np.array([0, 1, 5]), + 10.0, + ], + [ + [1, 2, 0], + -10000, + 10.0, + 1.0, + np.array([0, 1, 0]), + np.array([0, 1, 5]), + -10000.0, + ], + [ + [1, 2, 0], + -10000, + 10.0, + 0.5, + np.array([0, 1, 0]), + np.array([0, 1, 5]), + -4995.0, + ], + ], + ) + @pytest.mark.parametrize("ps", [DummyPhysicalSystem(state_length=3)]) + @pytest.mark.parametrize("rg", [DummyReferenceGenerator()]) + @pytest.mark.parametrize("cm", [DummyConstraintMonitor()]) def test_reward( - self, ps, rg, cm, reward_weights, bias, violation_reward, violation_degree, state, reference, expected_rw + self, + ps, + rg, + cm, + reward_weights, + bias, + violation_reward, + violation_degree, + state, + reference, + expected_rw, ): rg.set_modules(ps) - rf = self.class_to_test(reward_weights=reward_weights, bias=bias, violation_reward=violation_reward) + rf = self.class_to_test( + reward_weights=reward_weights, bias=bias, violation_reward=violation_reward + ) rf.set_modules(ps, rg, cm) - assert rf.reward(state, reference, violation_degree=violation_degree) == expected_rw + assert ( + rf.reward(state, reference, violation_degree=violation_degree) + == expected_rw + ) diff --git a/tests/test_utils.py b/tests/test_utils.py index 1d312e33..e2a3e863 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -15,31 +15,35 @@ def test_registry(monkeypatch): utils.register_superclass(core.PhysicalSystem) assert registry[core.PhysicalSystem] == {} - utils.register_class(dummies.DummyPhysicalSystem, core.PhysicalSystem, 'DummySystem') - assert registry[core.PhysicalSystem] == {'DummySystem': dummies.DummyPhysicalSystem} + utils.register_class( + dummies.DummyPhysicalSystem, core.PhysicalSystem, "DummySystem" + ) + assert registry[core.PhysicalSystem] == {"DummySystem": dummies.DummyPhysicalSystem} with pytest.raises(KeyError): _ = registry[dummies.DummyPhysicalSystem] with pytest.raises(KeyError): - _ = registry[core.PhysicalSystem]['nonExistingKey'] + _ = registry[core.PhysicalSystem]["nonExistingKey"] def test_make_module(monkeypatch): registry = {} monkeypatch.setattr(utils, "_registry", registry) utils.register_superclass(core.PhysicalSystem) - utils.register_class(dummies.DummyPhysicalSystem, core.PhysicalSystem, 'DummySystem') + utils.register_class( + dummies.DummyPhysicalSystem, core.PhysicalSystem, "DummySystem" + ) kwargs = dict(a=0, b=5) - dummy_sys = utils.make_module(core.PhysicalSystem, 'DummySystem', **kwargs) + dummy_sys = utils.make_module(core.PhysicalSystem, "DummySystem", **kwargs) assert isinstance(dummy_sys, dummies.DummyPhysicalSystem) assert dummy_sys.kwargs == kwargs with pytest.raises(Exception): - _ = registry[core.ReferenceGenerator]['DummySystem'] + _ = registry[core.ReferenceGenerator]["DummySystem"] with pytest.raises(Exception): - _ = registry[core.PhysicalSystem]['NonExistingKey'] + _ = registry[core.PhysicalSystem]["NonExistingKey"] def test_instantiate(monkeypatch): @@ -51,30 +55,45 @@ def test_instantiate(monkeypatch): assert utils.instantiate(core.PhysicalSystem, phys_sys) == phys_sys # Test class instantiation - instance = utils.instantiate(core.PhysicalSystem, dummies.DummyPhysicalSystem, **kwargs) + instance = utils.instantiate( + core.PhysicalSystem, dummies.DummyPhysicalSystem, **kwargs + ) assert type(instance) == dummies.DummyPhysicalSystem assert instance.kwargs == kwargs # Test string instantiation - key = 'DummyKey' - assert utils.instantiate(core.PhysicalSystem, key, **kwargs) == (core.PhysicalSystem, key, kwargs) + key = "DummyKey" + assert utils.instantiate(core.PhysicalSystem, key, **kwargs) == ( + core.PhysicalSystem, + key, + kwargs, + ) # Test Exceptions with pytest.raises(Exception): - utils.instantiate(core.ReferenceGenerator, dummies.DummyPhysicalSystem, **kwargs) + utils.instantiate( + core.ReferenceGenerator, dummies.DummyPhysicalSystem, **kwargs + ) with pytest.raises(Exception): - utils.instantiate(dummies.DummyPhysicalSystem, core.ReferenceGenerator, **kwargs) + utils.instantiate( + dummies.DummyPhysicalSystem, core.ReferenceGenerator, **kwargs + ) with pytest.raises(Exception): - utils.instantiate(dummies.DummyPhysicalSystem(), core.ReferenceGenerator, **kwargs) - - -@pytest.mark.parametrize("state_names", [['a', 'b', 'c', 'd']]) -@pytest.mark.parametrize("state_dict, state_array, target", [ - # Set all Values Test and test for uppercase states - (dict(A=5, b=12, c=10, d=12), [0, 0, 0, 0], [5, 12, 10, 12]), - # Set only subset of values -> Rest stays as it is in the state array - (dict(a=5, b=12, c=10), [0, 1, 2, 5], [5, 12, 10, 5]), -]) + utils.instantiate( + dummies.DummyPhysicalSystem(), core.ReferenceGenerator, **kwargs + ) + + +@pytest.mark.parametrize("state_names", [["a", "b", "c", "d"]]) +@pytest.mark.parametrize( + "state_dict, state_array, target", + [ + # Set all Values Test and test for uppercase states + (dict(A=5, b=12, c=10, d=12), [0, 0, 0, 0], [5, 12, 10, 12]), + # Set only subset of values -> Rest stays as it is in the state array + (dict(a=5, b=12, c=10), [0, 1, 2, 5], [5, 12, 10, 5]), + ], +) def test_state_dict_to_state_array(state_dict, state_array, state_names, target): utils.state_dict_to_state_array(state_dict, state_array, state_names) assert np.all(state_array == target) @@ -83,26 +102,34 @@ def test_state_dict_to_state_array(state_dict, state_array, state_names, target) def test_state_dict_to_state_array_assertion_error(): # Test if the AssertionError is raised when invalid states are passed with pytest.raises(AssertionError): - utils.state_dict_to_state_array({'invalid_name': 0, 'valid_name': 1}, np.zeros(3), ['valid_name', 'a', 'b']) - - -@pytest.mark.parametrize("state_names", [['a', 'b', 'c', 'd']]) -@pytest.mark.parametrize("input_values, target", [ - (dict(a=5, b=12, c=10, d=12), [5, 12, 10, 12]), - (np.array([1, 2, 3, 4]), np.array([1, 2, 3, 4])), - ([1, 2, 3, 4], [1, 2, 3, 4]), - (5, [5, 5, 5, 5]) -]) + utils.state_dict_to_state_array( + {"invalid_name": 0, "valid_name": 1}, np.zeros(3), ["valid_name", "a", "b"] + ) + + +@pytest.mark.parametrize("state_names", [["a", "b", "c", "d"]]) +@pytest.mark.parametrize( + "input_values, target", + [ + (dict(a=5, b=12, c=10, d=12), [5, 12, 10, 12]), + (np.array([1, 2, 3, 4]), np.array([1, 2, 3, 4])), + ([1, 2, 3, 4], [1, 2, 3, 4]), + (5, [5, 5, 5, 5]), + ], +) def test_set_state_array(input_values, state_names, target): assert np.all(utils.set_state_array(input_values, state_names) == target) -@pytest.mark.parametrize("state_names", [['a', 'b', 'c', 'd']]) -@pytest.mark.parametrize("input_values, error", [ - ('a', Exception), - (np.array([1, 2, 3]), AssertionError), - ([1, 2, 3], AssertionError) -]) +@pytest.mark.parametrize("state_names", [["a", "b", "c", "d"]]) +@pytest.mark.parametrize( + "input_values, error", + [ + ("a", Exception), + (np.array([1, 2, 3]), AssertionError), + ([1, 2, 3], AssertionError), + ], +) def test_set_state_array_exceptions(input_values, state_names, error): with pytest.raises(error): utils.set_state_array(input_values, state_names) diff --git a/tests/testing_utils.py b/tests/testing_utils.py index 0272261b..ef08fafa 100644 --- a/tests/testing_utils.py +++ b/tests/testing_utils.py @@ -1,10 +1,20 @@ from tests.conf import * from gym_electric_motor.physical_systems import * from gym_electric_motor.utils import make_module, set_state_array -from gym_electric_motor import ReferenceGenerator, RewardFunction, PhysicalSystem, ElectricMotorVisualization, \ - ConstraintMonitor -from gym_electric_motor.physical_systems import PowerElectronicConverter, MechanicalLoad, ElectricMotor, OdeSolver, \ - VoltageSupply +from gym_electric_motor import ( + ReferenceGenerator, + RewardFunction, + PhysicalSystem, + ElectricMotorVisualization, + ConstraintMonitor, +) +from gym_electric_motor.physical_systems import ( + PowerElectronicConverter, + MechanicalLoad, + ElectricMotor, + OdeSolver, + VoltageSupply, +) import gym_electric_motor.physical_systems.converters as cv from gym_electric_motor.physical_systems.physical_systems import SCMLSystem import numpy as np @@ -18,7 +28,9 @@ # region first version -def setup_physical_system(motor_type, converter_type, subconverters=None, three_phase=False): +def setup_physical_system( + motor_type, converter_type, subconverters=None, three_phase=False +): """ Function to set up a physical system with test parameters :param motor_type: motor name (string) @@ -27,55 +39,90 @@ def setup_physical_system(motor_type, converter_type, subconverters=None, three_ :return: instantiated physical system """ # get test parameter - tau = converter_parameter['tau'] - u_sup = test_motor_parameter[motor_type]['motor_parameter']['u_sup'] - motor_parameter = test_motor_parameter[motor_type]['motor_parameter'] # dict - nominal_values = test_motor_parameter[motor_type]['nominal_values'] # dict - limit_values = test_motor_parameter[motor_type]['limit_values'] # dict + tau = converter_parameter["tau"] + u_sup = test_motor_parameter[motor_type]["motor_parameter"]["u_sup"] + motor_parameter = test_motor_parameter[motor_type]["motor_parameter"] # dict + nominal_values = test_motor_parameter[motor_type]["nominal_values"] # dict + limit_values = test_motor_parameter[motor_type]["limit_values"] # dict # setup load - load = PolynomialStaticLoad(load_parameter=load_parameter['parameter']) + load = PolynomialStaticLoad(load_parameter=load_parameter["parameter"]) # setup voltage supply voltage_supply = IdealVoltageSupply(u_sup) # setup converter - if motor_type == 'DcExtEx': - if 'Disc' in converter_type: - double_converter = 'Disc-Multi' + if motor_type == "DcExtEx": + if "Disc" in converter_type: + double_converter = "Disc-Multi" else: - double_converter = 'Cont-Multi' - converter = make_module(PowerElectronicConverter, double_converter, - subconverters=[converter_type, converter_type], - tau=converter_parameter['tau'], - dead_time=converter_parameter['dead_time'], - interlocking_time=converter_parameter['interlocking_time']) + double_converter = "Cont-Multi" + converter = make_module( + PowerElectronicConverter, + double_converter, + subconverters=[converter_type, converter_type], + tau=converter_parameter["tau"], + dead_time=converter_parameter["dead_time"], + interlocking_time=converter_parameter["interlocking_time"], + ) else: - converter = make_module(PowerElectronicConverter, converter_type, - subconverters=subconverters, - tau=converter_parameter['tau'], - dead_time=converter_parameter['dead_time'], - interlocking_time=converter_parameter['interlocking_time']) + converter = make_module( + PowerElectronicConverter, + converter_type, + subconverters=subconverters, + tau=converter_parameter["tau"], + dead_time=converter_parameter["dead_time"], + interlocking_time=converter_parameter["interlocking_time"], + ) # setup motor - motor = make_module(ElectricMotor, motor_type, motor_parameter=motor_parameter, nominal_values=nominal_values, - limit_values=limit_values) + motor = make_module( + ElectricMotor, + motor_type, + motor_parameter=motor_parameter, + nominal_values=nominal_values, + limit_values=limit_values, + ) # setup solver - solver = ScipySolveIvpSolver(method='RK45') + solver = ScipySolveIvpSolver(method="RK45") # combine all modules to a physical system if three_phase: if motor_type == "SCIM": - physical_system = SquirrelCageInductionMotorSystem(converter=converter, motor=motor, ode_solver=solver, - supply=voltage_supply, load=load, tau=tau) + physical_system = SquirrelCageInductionMotorSystem( + converter=converter, + motor=motor, + ode_solver=solver, + supply=voltage_supply, + load=load, + tau=tau, + ) elif motor_type == "DFIM": - physical_system = DoublyFedInductionMotor(converter=converter, motor=motor, ode_solver=solver, - supply=voltage_supply, load=load, tau=tau) + physical_system = DoublyFedInductionMotor( + converter=converter, + motor=motor, + ode_solver=solver, + supply=voltage_supply, + load=load, + tau=tau, + ) else: - physical_system = SynchronousMotorSystem(converter=converter, motor=motor, ode_solver=solver, - supply=voltage_supply, load=load, tau=tau) + physical_system = SynchronousMotorSystem( + converter=converter, + motor=motor, + ode_solver=solver, + supply=voltage_supply, + load=load, + tau=tau, + ) else: - physical_system = DcMotorSystem(converter=converter, motor=motor, ode_solver=solver, - supply=voltage_supply, load=load, tau=tau) + physical_system = DcMotorSystem( + converter=converter, + motor=motor, + ode_solver=solver, + supply=voltage_supply, + load=load, + tau=tau, + ) return physical_system -def setup_reference_generator(reference_type, physical_system, reference_state='omega'): +def setup_reference_generator(reference_type, physical_system, reference_state="omega"): """ Function to setup the reference generator :param reference_type: name of reference generator @@ -83,15 +130,27 @@ def setup_reference_generator(reference_type, physical_system, reference_state=' :param reference_state: referenced state name (string) :return: instantiated reference generator """ - reference_generator = make_module(ReferenceGenerator, reference_type, reference_state=reference_state) + reference_generator = make_module( + ReferenceGenerator, reference_type, reference_state=reference_state + ) reference_generator.set_modules(physical_system) reference_generator.reset() return reference_generator -def setup_reward_function(reward_function_type, physical_system, reference_generator, reward_weights, observed_states): - reward_function = make_module(RewardFunction, reward_function_type, observed_states=observed_states, - reward_weights=reward_weights) +def setup_reward_function( + reward_function_type, + physical_system, + reference_generator, + reward_weights, + observed_states, +): + reward_function = make_module( + RewardFunction, + reward_function_type, + observed_states=observed_states, + reward_weights=reward_weights, + ) reward_function.set_modules(physical_system, reference_generator) return reward_function @@ -104,30 +163,44 @@ def setup_dc_converter(conv, motor_type, subconverters=None): :param motor_type: motor name (string) :return: initialized converter """ - if motor_type == 'DcExtEx': + if motor_type == "DcExtEx": # setup double converter - if 'Disc' in conv: - double_converter = 'Disc-Multi' + if "Disc" in conv: + double_converter = "Disc-Multi" else: - double_converter = 'Cont-Multi' - converter = make_module(PowerElectronicConverter, double_converter, - interlocking_time=converter_parameter['interlocking_time'], - dead_time=converter_parameter['dead_time'], - subconverters=[make_module(PowerElectronicConverter, conv, - tau=converter_parameter['tau'], - dead_time=converter_parameter['dead_time'], - interlocking_time=converter_parameter['interlocking_time']), - make_module(PowerElectronicConverter, conv, - tau=converter_parameter['tau'], - dead_time=converter_parameter['dead_time'], - interlocking_time=converter_parameter['interlocking_time'])]) + double_converter = "Cont-Multi" + converter = make_module( + PowerElectronicConverter, + double_converter, + interlocking_time=converter_parameter["interlocking_time"], + dead_time=converter_parameter["dead_time"], + subconverters=[ + make_module( + PowerElectronicConverter, + conv, + tau=converter_parameter["tau"], + dead_time=converter_parameter["dead_time"], + interlocking_time=converter_parameter["interlocking_time"], + ), + make_module( + PowerElectronicConverter, + conv, + tau=converter_parameter["tau"], + dead_time=converter_parameter["dead_time"], + interlocking_time=converter_parameter["interlocking_time"], + ), + ], + ) else: # setup single converter - converter = make_module(PowerElectronicConverter, conv, - subconverters=subconverters, - tau=converter_parameter['tau'], - dead_time=converter_parameter['dead_time'], - interlocking_time=converter_parameter['interlocking_time']) + converter = make_module( + PowerElectronicConverter, + conv, + subconverters=subconverters, + tau=converter_parameter["tau"], + dead_time=converter_parameter["dead_time"], + interlocking_time=converter_parameter["interlocking_time"], + ) return converter @@ -141,18 +214,23 @@ def setup_dc_converter(conv, motor_type, subconverters=None): def mock_instantiate(superclass, key, **kwargs): # Instantiate the object and log the passed and returned values to validate correct function calls instantiate_dict[superclass] = {} - instantiate_dict[superclass]['key'] = key + instantiate_dict[superclass]["key"] = key inst = instantiate(superclass, key, **kwargs) - instantiate_dict[superclass]['instance'] = inst + instantiate_dict[superclass]["instance"] = inst return inst class DummyReferenceGenerator(ReferenceGenerator): _reset_counter = 0 - def __init__(self, reference_observation=np.array([1.]), reference_state='dummy_state_0', **kwargs): + def __init__( + self, + reference_observation=np.array([1.0]), + reference_state="dummy_state_0", + **kwargs, + ): super().__init__() - self.reference_space = Box(0., 1., shape=(1,), dtype=np.float64) + self.reference_space = Box(0.0, 1.0, shape=(1,), dtype=np.float64) self.kwargs = kwargs self._reference_names = [reference_state] self.closed = False @@ -192,7 +270,6 @@ def close(self): class DummyRewardFunction(RewardFunction): - def __init__(self, **kwargs): self.last_state = None self.last_reference = None @@ -227,7 +304,6 @@ def _reward(self, state, reference, action): class DummyPhysicalSystem(PhysicalSystem): - @property def limits(self): """ @@ -244,16 +320,17 @@ def nominal_state(self): """ return self._nominal_values - def __init__(self, state_length=None, state_names='dummy_state', **kwargs): - + def __init__(self, state_length=None, state_names="dummy_state", **kwargs): if isinstance(state_names, str): if state_length is None: state_length = 1 - state_names = [f'{state_names}_{i}' for i in range(state_length)] + state_names = [f"{state_names}_{i}" for i in range(state_length)] state_length = len(state_names) super().__init__( - Box(-1, 1, shape=(1,), dtype=np.float64), Box(-1, 1, shape=(state_length,), dtype=np.float64), - state_names, 1 + Box(-1, 1, shape=(1,), dtype=np.float64), + Box(-1, 1, shape=(state_length,), dtype=np.float64), + state_names, + 1, ) self._limits = np.array([10 * (i + 1) for i in range(state_length)]) self._nominal_values = np.array([(i + 1) for i in range(state_length)]) @@ -263,7 +340,7 @@ def __init__(self, state_length=None, state_names='dummy_state', **kwargs): self.kwargs = kwargs def reset(self, initial_state=None): - self.state = np.array([0.] * len(self._state_names)) + self.state = np.array([0.0] * len(self._state_names)) return self.state def simulate(self, action): @@ -277,7 +354,6 @@ def close(self): class DummyVisualization(ElectricMotorVisualization): - def __init__(self, **kwargs): self.closed = False self.state = None @@ -305,7 +381,6 @@ def set_modules(self, physical_system, reference_generator, reward_function): class DummyVoltageSupply(VoltageSupply): - def __init__(self, u_nominal=560, tau=1e-4, **kwargs): super().__init__(u_nominal) self.i_sup = None @@ -329,12 +404,19 @@ def get_voltage(self, i_sup, t, *args, **kwargs): class DummyConverter(PowerElectronicConverter): - voltages = Box(0, 1, shape=(1,), dtype=np.float64) currents = Box(-1, 1, shape=(1,), dtype=np.float64) action_space = Discrete(4) - def __init__(self, tau=2E-4, interlocking_time=0, action_space=None, voltages=None, currents=None, **kwargs): + def __init__( + self, + tau=2e-4, + interlocking_time=0, + action_space=None, + voltages=None, + currents=None, + **kwargs, + ): super().__init__(tau, interlocking_time) self.action_space = action_space or self.action_space self.voltages = voltages or self.voltages @@ -370,21 +452,22 @@ def convert(self, i_out, t): self.i_out = i_out self.t = t self.convert_counter += 1 - self.u_in = [self.action] if type(self.action_space) is Discrete else self.action + self.u_in = ( + [self.action] if type(self.action_space) is Discrete else self.action + ) return self.u_in class DummyElectricMotor(ElectricMotor): - # defined test values - _default_motor_parameter = permex_motor_parameter['motor_parameter'] + _default_motor_parameter = permex_motor_parameter["motor_parameter"] _default_limits = dict(omega=16, torque=26, u=15, i=26, i_0=26, i_1=21, u_0=15) _default_nominal_values = dict(omega=14, torque=20, u=15, i=22, i_0=22, i_1=20) HAS_JACOBIAN = True electrical_jac_return = None CURRENTS_IDX = [0, 1] - CURRENTS = ['i_0', 'i_1'] - VOLTAGES = ['u_0'] + CURRENTS = ["i_0", "i_1"] + VOLTAGES = ["u_0"] def __init__(self, tau=1e-5, **kwargs): self.kwargs = kwargs @@ -411,7 +494,6 @@ def electrical_jacobian(self, state, u_in, omega, *_): class PowerElectronicConverterWrapper(cv.PowerElectronicConverter): - def __init__(self, subconverter, **kwargs): super().__init__(**kwargs) self._converter = subconverter @@ -452,9 +534,10 @@ class DummyScipyOdeSolver(ode): """ Dummy class for ScipyOdeSolver """ + # defined test values - _kwargs = {'nsteps': 5} - _integrator = 'dop853' + _kwargs = {"nsteps": 5} + _integrator = "dop853" _y = np.zeros(2) _y_init = np.array([1, 6]) _t = 0 @@ -507,7 +590,8 @@ class DummyLoad(MechanicalLoad): """ dummy class for mechanical load """ - state_names = ['omega', 'position'] + + state_names = ["omega", "position"] limits = dict(omega=15, position=10) nominal_values = dict(omega=15, position=10) mechanical_state = None @@ -522,7 +606,7 @@ def __init__(self, **kwargs): self.reset_counter = 0 super().__init__(**kwargs) - def reset(self, state_space, state_positions, nominal_state, *_, **__): + def reset(self, state_space, state_positions, nominal_state, *_, **__): self.reset_counter += 1 return np.zeros(2) @@ -540,13 +624,14 @@ def mechanical_jacobian(self, t, mechanical_state, torque): def get_state_space(self, omega_range): self.omega_range = omega_range - return {'omega': 0, 'position': -1}, {'omega': 1, 'position': -1} + return {"omega": 0, "position": -1}, {"omega": 1, "position": -1} class DummyOdeSolver(OdeSolver): """ Dummy class for ode solver """ + def __init__(self, **kwargs): self.kwargs = kwargs super().__init__() @@ -559,7 +644,6 @@ def integrate(self, t): class DummyConstraint(Constraint): - def __init__(self, violation_degree=0.0): super().__init__() self.modules_set = False @@ -574,7 +658,6 @@ def set_modules(self, ps): class DummyConstraintMonitor(ConstraintMonitor): - def __init__(self, no_of_dummy_constraints=1): constraints = [DummyConstraint() for _ in range(no_of_dummy_constraints)] super().__init__(additional_constraints=constraints) @@ -584,6 +667,7 @@ class DummySCMLSystem(SCMLSystem): """ dummy class for SCMLSystem """ + # defined test values OMEGA_IDX = 0 TORQUE_IDX = 1 @@ -598,7 +682,7 @@ class DummySCMLSystem(SCMLSystem): _electrical_motor = None _mechanical_load = None - _state_names = ['omega_me', 'torque', 'u', 'i', 'u_sup'] + _state_names = ["omega_me", "torque", "u", "i", "u_sup"] _state_length = 5 # counter @@ -659,8 +743,19 @@ class DummyRandom: _monkey_random_choice_counter = 0 _monkey_random_normal_counter = 0 - def __init__(self, exp_low=None, exp_high=None, exp_left=None, exp_right=None, exp_mode=None, exp_values=None, - exp_probabilities=None, exp_loc=None, exp_scale=None, exp_size=None): + def __init__( + self, + exp_low=None, + exp_high=None, + exp_left=None, + exp_right=None, + exp_mode=None, + exp_values=None, + exp_probabilities=None, + exp_loc=None, + exp_scale=None, + exp_size=None, + ): """ set expected values :param exp_low: expected lower value @@ -746,25 +841,29 @@ def monkey_random_normal(self, loc=0, scale=1.0, size=None): class DummyElectricMotorEnvironment(ElectricMotorEnvironment): """Dummy environment to test pre implemented callbacks. Extend for further testing cases""" - + def __init__(self, reference_generator=None, callbacks=(), **kwargs): reference_generator = reference_generator or DummyReferenceGenerator() - super().__init__(DummyPhysicalSystem(), reference_generator, DummyRewardFunction(), callbacks=callbacks) - + super().__init__( + DummyPhysicalSystem(), + reference_generator, + DummyRewardFunction(), + callbacks=callbacks, + ) + def step(self): - self._call_callbacks('on_step_begin', 0, 0) - self._call_callbacks('on_step_end', 0, 0, 0, 0, 0) - + self._call_callbacks("on_step_begin", 0, 0) + self._call_callbacks("on_step_end", 0, 0, 0, 0, 0) + def reset(self): - self._call_callbacks('on_reset_begin') - self._call_callbacks('on_reset_end', 0, 0) - + self._call_callbacks("on_reset_begin") + self._call_callbacks("on_reset_end", 0, 0) + def close(self): - self._call_callbacks(self._callbacks, 'on_close') + self._call_callbacks(self._callbacks, "on_close") class DummyCallback(Callback): - def __init__(self): super().__init__() self.reset_begin = 0 @@ -772,7 +871,7 @@ def __init__(self): self.step_begin = 0 self.step_end = 0 self.close = 0 - + def on_reset_begin(self): self.reset_begin += 1 diff --git a/tests/utils/physical_system_test_wrapper.py b/tests/utils/physical_system_test_wrapper.py index 5640abd2..711b3e16 100644 --- a/tests/utils/physical_system_test_wrapper.py +++ b/tests/utils/physical_system_test_wrapper.py @@ -2,7 +2,6 @@ class PhysicalSystemTestWrapper(gem.physical_system_wrappers.PhysicalSystemWrapper): - def __init__(self, **kwargs): super().__init__(**kwargs) self.action = None From 92b1c46f971e5c65ac5fc9f8b41596c51b0e6696 Mon Sep 17 00:00:00 2001 From: Stefan Arndt Date: Fri, 10 Nov 2023 14:37:32 +0100 Subject: [PATCH 02/51] Refactored test dc motor --- ...ation_test_classic_controllers_dc_motor.py | 110 ++++++++++++++---- gym_electric_motor/__init__.py | 110 +++++++++--------- gym_electric_motor/core.py | 46 +++++--- .../physical_systems/physical_systems.py | 2 +- gym_electric_motor/visualization/__init__.py | 1 + .../visualization/new_motor_dashboard.py | 11 ++ 6 files changed, 190 insertions(+), 90 deletions(-) create mode 100644 gym_electric_motor/visualization/new_motor_dashboard.py diff --git a/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py b/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py index 640e12a1..f3967c0c 100644 --- a/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py +++ b/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py @@ -1,42 +1,108 @@ from classic_controllers import Controller from externally_referenced_state_plot import ExternallyReferencedStatePlot import gym_electric_motor as gem -from gym_electric_motor.visualization import MotorDashboard +from gym_electric_motor.visualization import MotorDashboard, NewMotorDashboard from gym_electric_motor.reference_generators import SinusoidalReferenceGenerator import time +from enum import Enum +from dataclasses import dataclass + +MotorType = Enum( + "MotorType", + ["PermanentlyExcitedDcMotor", "ExternallyExcitedDcMotor", "SeriesDc", "ShuntDc"], +) +MotorType.PermanentlyExcitedDcMotor.env_id_tag = "PermExDc" +MotorType.ExternallyExcitedDcMotor.env_id_tag = "ExtExDc" +MotorType.PermanentlyExcitedDcMotor.states = ["omega", "torque", "i", "u"] +MotorType.ExternallyExcitedDcMotor.states = [ + "omega", + "torque", + "i_a", + "i_e", + "u_a", + "u_e", +] +MotorType.SeriesDc.states = ["omega", "torque", "i", "u"] +MotorType.ShuntDc.states = ["omega", "torque", "i_a", "i_e", "u"] + + +ControlType = Enum("ControlType", ["SpeedControl", "TorqueControl", "CurrentControl"]) +ControlType.SpeedControl.env_id_tag = "SC" +ControlType.TorqueControl.env_id_tag = "TC" +ControlType.CurrentControl.env_id_tag = "CC" + +ActionType = Enum("ActionType", ["Continuous", "Finite"]) +ActionType.Continuous.env_id_tag = "Cont" + + +# We need this helper functions to be backwards compatible with env_id string +def _get_env_id_tag(t) -> str: + if hasattr(t, "env_id_tag"): + return t.env_id_tag + else: + return t.name + + +@dataclass +class Motor: + motor_type: MotorType + control_type: ControlType + action_type: ActionType + + def get_env_id(self) -> str: + return ( + _get_env_id_tag(self.action_type) + + "-" + + _get_env_id_tag(self.control_type) + + "-" + + _get_env_id_tag(self.motor_type) + + "-v0" + ) + + def get_state_names(self) -> list[str]: + return self.motor_type.states + + +@dataclass +class Workspace: + t = [] + + +@dataclass +class SimulatedEnvironment: + t: float = 0.0 # Current simulation time + step: int = 0 # Current simulation step + if __name__ == "__main__": """ motor type: 'PermExDc' Permanently Excited DC Motor - 'ExtExDc' Externally Excited MC Motor + 'ExtExDc' Externally Excited DC Motor 'SeriesDc' DC Series Motor 'ShuntDc' DC Shunt Motor - + control type: 'SC' Speed Control 'TC' Torque Control 'CC' Current Control - + action_type: 'Cont' Continuous Action Space 'Finite' Discrete Action Space """ - motor_type = "PermExDc" - control_type = "SC" - action_type = "Cont" - - motor = action_type + "-" + control_type + "-" + motor_type + "-v0" + # motor_type = "PermExDc" + # control_type = "SC" + # action_type = "Cont" - if motor_type in ["PermExDc", "SeriesDc"]: - states = ["omega", "torque", "i", "u"] - elif motor_type == "ShuntDc": - states = ["omega", "torque", "i_a", "i_e", "u"] - elif motor_type == "ExtExDc": - states = ["omega", "torque", "i_a", "i_e", "u_a", "u_e"] - else: - raise KeyError(motor_type + " is not available") + motor = Motor( + MotorType.PermanentlyExcitedDcMotor, + ControlType.SpeedControl, + ActionType.Continuous, + ) # definition of the plotted variables - external_ref_plots = [ExternallyReferencedStatePlot(state) for state in states] + external_ref_plots = [ + ExternallyReferencedStatePlot(state) for state in motor.get_state_names() + ] # definition of the reference generator @@ -49,8 +115,8 @@ # initialize the gym-electric-motor environment env = gem.make( - motor, - visualization=MotorDashboard(additional_plots=external_ref_plots), + motor.get_env_id(), + visualization=NewMotorDashboard(additional_plots=external_ref_plots), scale_plots=True, render_mode="figure", reference_generator=ref_generator, @@ -75,12 +141,16 @@ (state, reference), _ = env.reset(seed=1337) # simulate the environment + s = 0 for i in range(10001): + s += 1 action = controller.control(state, reference) # if i % 100 == 0: # (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) # else: (state, reference), reward, terminated, truncated, _ = env.step(action) + print(f"step{s}") + # viz.render() if terminated: env.reset() diff --git a/gym_electric_motor/__init__.py b/gym_electric_motor/__init__.py index 91e4ce2b..1c2d553e 100644 --- a/gym_electric_motor/__init__.py +++ b/gym_electric_motor/__init__.py @@ -3,6 +3,7 @@ from .core import RewardFunction from .core import ElectricMotorVisualization from .core import ConstraintMonitor +from .core import SimulationEnvironment from .random_component import RandomComponent from .constraints import Constraint, LimitConstraint from .utils import register_superclass @@ -25,6 +26,7 @@ from gymnasium.envs.registration import register import gymnasium from packaging import version + # Add all superclasses of the modules to the registry. # Deactivate the order enforce wrapper that is put around a created env per default from gymnasium-version 0.21.0 onwards @@ -41,285 +43,285 @@ register( id="Finite-SC-PermExDc-v0", entry_point=envs_path + "FiniteSpeedControlDcPermanentlyExcitedMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Cont-SC-PermExDc-v0", entry_point=envs_path + "ContSpeedControlDcPermanentlyExcitedMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Finite-TC-PermExDc-v0", entry_point=envs_path + "FiniteTorqueControlDcPermanentlyExcitedMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Cont-TC-PermExDc-v0", entry_point=envs_path + "ContTorqueControlDcPermanentlyExcitedMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Finite-CC-PermExDc-v0", entry_point=envs_path + "FiniteCurrentControlDcPermanentlyExcitedMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Cont-CC-PermExDc-v0", entry_point=envs_path + "ContCurrentControlDcPermanentlyExcitedMotorEnv", - **registration_kwargs, + **registration_kwargs ) # Externally Excited DC Motor Environments register( id="Finite-SC-ExtExDc-v0", entry_point=envs_path + "FiniteSpeedControlDcExternallyExcitedMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Cont-SC-ExtExDc-v0", entry_point=envs_path + "ContSpeedControlDcExternallyExcitedMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Finite-TC-ExtExDc-v0", entry_point=envs_path + "FiniteTorqueControlDcExternallyExcitedMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Cont-TC-ExtExDc-v0", entry_point=envs_path + "ContTorqueControlDcExternallyExcitedMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Finite-CC-ExtExDc-v0", entry_point=envs_path + "FiniteCurrentControlDcExternallyExcitedMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Cont-CC-ExtExDc-v0", entry_point=envs_path + "ContCurrentControlDcExternallyExcitedMotorEnv", - **registration_kwargs, + **registration_kwargs ) # Series DC Motor Environments register( id="Finite-SC-SeriesDc-v0", entry_point=envs_path + "FiniteSpeedControlDcSeriesMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Cont-SC-SeriesDc-v0", entry_point=envs_path + "ContSpeedControlDcSeriesMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Finite-TC-SeriesDc-v0", entry_point=envs_path + "FiniteTorqueControlDcSeriesMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Cont-TC-SeriesDc-v0", entry_point=envs_path + "ContTorqueControlDcSeriesMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Finite-CC-SeriesDc-v0", entry_point=envs_path + "FiniteCurrentControlDcSeriesMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Cont-CC-SeriesDc-v0", entry_point=envs_path + "ContCurrentControlDcSeriesMotorEnv", - **registration_kwargs, + **registration_kwargs ) # Shunt DC Motor Environments register( id="Finite-SC-ShuntDc-v0", entry_point=envs_path + "FiniteSpeedControlDcShuntMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Cont-SC-ShuntDc-v0", entry_point=envs_path + "ContSpeedControlDcShuntMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Finite-TC-ShuntDc-v0", entry_point=envs_path + "FiniteTorqueControlDcShuntMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Cont-TC-ShuntDc-v0", entry_point=envs_path + "ContTorqueControlDcShuntMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Finite-CC-ShuntDc-v0", entry_point=envs_path + "FiniteCurrentControlDcShuntMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Cont-CC-ShuntDc-v0", entry_point=envs_path + "ContCurrentControlDcShuntMotorEnv", - **registration_kwargs, + **registration_kwargs ) # Permanent Magnet Synchronous Motor Environments register( id="Finite-SC-PMSM-v0", entry_point=envs_path + "FiniteSpeedControlPermanentMagnetSynchronousMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Finite-TC-PMSM-v0", entry_point=envs_path + "FiniteTorqueControlPermanentMagnetSynchronousMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Finite-CC-PMSM-v0", entry_point=envs_path + "FiniteCurrentControlPermanentMagnetSynchronousMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Cont-CC-PMSM-v0", entry_point=envs_path + "ContCurrentControlPermanentMagnetSynchronousMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Cont-TC-PMSM-v0", entry_point=envs_path + "ContTorqueControlPermanentMagnetSynchronousMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Cont-SC-PMSM-v0", entry_point=envs_path + "ContSpeedControlPermanentMagnetSynchronousMotorEnv", - **registration_kwargs, + **registration_kwargs ) # Externally Excited Synchronous Motor Environments register( id="Finite-SC-EESM-v0", entry_point=envs_path + "FiniteSpeedControlExternallyExcitedSynchronousMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Finite-TC-EESM-v0", entry_point=envs_path + "FiniteTorqueControlExternallyExcitedSynchronousMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Finite-CC-EESM-v0", entry_point=envs_path + "FiniteCurrentControlExternallyExcitedSynchronousMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Cont-CC-EESM-v0", entry_point=envs_path + "ContCurrentControlExternallyExcitedSynchronousMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Cont-TC-EESM-v0", entry_point=envs_path + "ContTorqueControlExternallyExcitedSynchronousMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Cont-SC-EESM-v0", entry_point=envs_path + "ContSpeedControlExternallyExcitedSynchronousMotorEnv", - **registration_kwargs, + **registration_kwargs ) # Synchronous Reluctance Motor Environments register( id="Finite-SC-SynRM-v0", entry_point=envs_path + "FiniteSpeedControlSynchronousReluctanceMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Finite-TC-SynRM-v0", entry_point=envs_path + "FiniteTorqueControlSynchronousReluctanceMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Finite-CC-SynRM-v0", entry_point=envs_path + "FiniteCurrentControlSynchronousReluctanceMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Cont-CC-SynRM-v0", entry_point=envs_path + "ContCurrentControlSynchronousReluctanceMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Cont-TC-SynRM-v0", entry_point=envs_path + "ContTorqueControlSynchronousReluctanceMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Cont-SC-SynRM-v0", entry_point=envs_path + "ContSpeedControlSynchronousReluctanceMotorEnv", - **registration_kwargs, + **registration_kwargs ) # Squirrel Cage Induction Motor Environments register( id="Finite-SC-SCIM-v0", entry_point=envs_path + "FiniteSpeedControlSquirrelCageInductionMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Finite-TC-SCIM-v0", entry_point=envs_path + "FiniteTorqueControlSquirrelCageInductionMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Finite-CC-SCIM-v0", entry_point=envs_path + "FiniteCurrentControlSquirrelCageInductionMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Cont-CC-SCIM-v0", entry_point=envs_path + "ContCurrentControlSquirrelCageInductionMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Cont-TC-SCIM-v0", entry_point=envs_path + "ContTorqueControlSquirrelCageInductionMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Cont-SC-SCIM-v0", entry_point=envs_path + "ContSpeedControlSquirrelCageInductionMotorEnv", - **registration_kwargs, + **registration_kwargs ) # Doubly Fed Induction Motor Environments register( id="Finite-SC-DFIM-v0", entry_point=envs_path + "FiniteSpeedControlDoublyFedInductionMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Finite-TC-DFIM-v0", entry_point=envs_path + "FiniteTorqueControlDoublyFedInductionMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Finite-CC-DFIM-v0", entry_point=envs_path + "FiniteCurrentControlDoublyFedInductionMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Cont-CC-DFIM-v0", entry_point=envs_path + "ContCurrentControlDoublyFedInductionMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Cont-TC-DFIM-v0", entry_point=envs_path + "ContTorqueControlDoublyFedInductionMotorEnv", - **registration_kwargs, + **registration_kwargs ) register( id="Cont-SC-DFIM-v0", entry_point=envs_path + "ContSpeedControlDoublyFedInductionMotorEnv", - **registration_kwargs, + **registration_kwargs ) diff --git a/gym_electric_motor/core.py b/gym_electric_motor/core.py index faeb861d..be506a29 100644 --- a/gym_electric_motor/core.py +++ b/gym_electric_motor/core.py @@ -31,6 +31,7 @@ import matplotlib.pyplot from matplotlib.figure import Figure import matplotlib +from dataclasses import dataclass class ElectricMotorEnvironment(gymnasium.core.Env): @@ -343,6 +344,7 @@ def step(self, action): not self._terminated ), "A reset is required before the environment can perform further steps" self._call_callbacks("on_step_begin", self.physical_system.k, action) + print(f"k:{self.physical_system.k}") state = self._physical_system.simulate(action) reference = self.reference_generator.get_reference(state) violation_degree = self._constraint_monitor.check_constraints(state) @@ -366,9 +368,12 @@ def step(self, action): info = {} return ( - state[self.state_filter], - ref_next, - ), reward, self._terminated, self._truncated, info + (state[self.state_filter], ref_next), + reward, + self._terminated, + self._truncated, + info, + ) def _seed(self, seed=None): sg = np.random.SeedSequence(seed) @@ -542,9 +547,11 @@ def reset(self, initial_state=None, initial_reference=None): trajectories(dict(list(float)): If available, \ generated trajectories for the Visualization can be passed here. Otherwise return None. \ """ - return self.get_reference(initial_state), self.get_reference_observation( - initial_state - ), None + return ( + self.get_reference(initial_state), + self.get_reference_observation(initial_state), + None, + ) def close(self): """Called by the environment, when the environment is deleted to close files, store logs, etc.""" @@ -628,15 +635,32 @@ def close(self): pass +@dataclass +class SimulationEnvironment: + tau: float = 0.0 # Simulation interval + step: int = 0 # Current simulation step + + # Current simulation time + @property + def t(self) -> float: + return self.tau * self.step + + +@dataclass class PhysicalSystem: """The Physical System module encapsulates the physical model of the system as well as the simulation from one step to the next.""" + @property + def tau(self): + return self._tau + @property def unwrapped(self): """Returns this instance of the physical system. - If the system is wrapped into multiple PhysicalSystemWrappers this property returns directly the innermost system.""" + If the system is wrapped into multiple PhysicalSystemWrappers this property returns directly the innermost system. + """ return self @property @@ -647,14 +671,6 @@ def k(self): """ return self._k - @property - def tau(self): - """ - Returns: - float: The systems time constant tau. - """ - return self._tau - @property def state_names(self): """ diff --git a/gym_electric_motor/physical_systems/physical_systems.py b/gym_electric_motor/physical_systems/physical_systems.py index 881325b7..74ec3659 100644 --- a/gym_electric_motor/physical_systems/physical_systems.py +++ b/gym_electric_motor/physical_systems/physical_systems.py @@ -201,7 +201,7 @@ def simulate(self, action, *_, **__): 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) + 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]) diff --git a/gym_electric_motor/visualization/__init__.py b/gym_electric_motor/visualization/__init__.py index 2268475b..aae3a4c9 100644 --- a/gym_electric_motor/visualization/__init__.py +++ b/gym_electric_motor/visualization/__init__.py @@ -1,5 +1,6 @@ from .console_printer import ConsolePrinter from .motor_dashboard import MotorDashboard +from .new_motor_dashboard import NewMotorDashboard from ..utils import register_class from .. import ElectricMotorVisualization diff --git a/gym_electric_motor/visualization/new_motor_dashboard.py b/gym_electric_motor/visualization/new_motor_dashboard.py new file mode 100644 index 00000000..88ffdb45 --- /dev/null +++ b/gym_electric_motor/visualization/new_motor_dashboard.py @@ -0,0 +1,11 @@ +from gym_electric_motor.core import ElectricMotorVisualization +from .motor_dashboard_plots import StatePlot, ActionPlot, RewardPlot, TimePlot, EpisodePlot, StepPlot +from .motor_dashboard import MotorDashboard +import matplotlib.pyplot as plt +import gymnasium + + +class NewMotorDashboard(MotorDashboard): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # self.on_reset_begin = None From 5369a37affead48033f92f4cefd7b597b53f9164 Mon Sep 17 00:00:00 2001 From: Stefan Arndt Date: Fri, 10 Nov 2023 15:13:54 +0100 Subject: [PATCH 03/51] starting viz refactor --- ...ation_test_classic_controllers_dc_motor.py | 11 ------- gym_electric_motor/core.py | 33 ++++++++++++------- .../visualization/new_motor_dashboard.py | 16 +++++++-- 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py b/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py index f3967c0c..7d5d6c54 100644 --- a/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py +++ b/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py @@ -63,17 +63,6 @@ def get_state_names(self) -> list[str]: return self.motor_type.states -@dataclass -class Workspace: - t = [] - - -@dataclass -class SimulatedEnvironment: - t: float = 0.0 # Current simulation time - step: int = 0 # Current simulation step - - if __name__ == "__main__": """ motor type: 'PermExDc' Permanently Excited DC Motor diff --git a/gym_electric_motor/core.py b/gym_electric_motor/core.py index be506a29..01684cfb 100644 --- a/gym_electric_motor/core.py +++ b/gym_electric_motor/core.py @@ -34,6 +34,22 @@ from dataclasses import dataclass +@dataclass +class Workspace: + test = [] + + +@dataclass +class SimulationEnvironment: + tau: float = 0.0 # Simulation interval + step: int = 0 # Current simulation step + + # Current simulation time + @property + def t(self) -> float: + return self.tau * self.step + + class ElectricMotorEnvironment(gymnasium.core.Env): """ Description: @@ -91,6 +107,9 @@ class ElectricMotorEnvironment(gymnasium.core.Env): The reward function can terminate an episode, if a physical limit of the motor has been violated. """ + sim = SimulationEnvironment() + workspace = Workspace() + env_id = None metadata = { "render_modes": [None, "figure", "figure_once", "figure_academic"], @@ -216,6 +235,9 @@ def __init__( callbacks(list(Callback)): Callbacks being called in the environment **kwargs: Arguments to be passed to the modules. """ + + self.sim.tau = physical_system.tau + self._physical_system = instantiate(PhysicalSystem, physical_system, **kwargs) self._reference_generator = instantiate( ReferenceGenerator, reference_generator, **kwargs @@ -635,17 +657,6 @@ def close(self): pass -@dataclass -class SimulationEnvironment: - tau: float = 0.0 # Simulation interval - step: int = 0 # Current simulation step - - # Current simulation time - @property - def t(self) -> float: - return self.tau * self.step - - @dataclass class PhysicalSystem: """The Physical System module encapsulates the physical model of the system as well as the simulation from one step diff --git a/gym_electric_motor/visualization/new_motor_dashboard.py b/gym_electric_motor/visualization/new_motor_dashboard.py index 88ffdb45..d2e5241a 100644 --- a/gym_electric_motor/visualization/new_motor_dashboard.py +++ b/gym_electric_motor/visualization/new_motor_dashboard.py @@ -1,11 +1,23 @@ from gym_electric_motor.core import ElectricMotorVisualization -from .motor_dashboard_plots import StatePlot, ActionPlot, RewardPlot, TimePlot, EpisodePlot, StepPlot +from .motor_dashboard_plots import ( + StatePlot, + ActionPlot, + RewardPlot, + TimePlot, + EpisodePlot, + StepPlot, +) from .motor_dashboard import MotorDashboard import matplotlib.pyplot as plt import gymnasium -class NewMotorDashboard(MotorDashboard): +class NewMotorDashboard2(MotorDashboard): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # self.on_reset_begin = None + + +class NewMotorDashboard: + def __init__(self, *args, **kwargs) -> None: + pass From 757d8a5b7d70267443a6691e671eab78421fa12a Mon Sep 17 00:00:00 2001 From: Stefan Arndt Date: Fri, 10 Nov 2023 16:48:49 +0100 Subject: [PATCH 04/51] proxy object for motor dashboard --- ...ation_test_classic_controllers_dc_motor.py | 8 +-- gym_electric_motor/visualization/__init__.py | 1 - .../visualization/motor_dashboard.py | 56 ++++++++++++++----- .../visualization/new_motor_dashboard.py | 11 +--- 4 files changed, 49 insertions(+), 27 deletions(-) diff --git a/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py b/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py index 7d5d6c54..8ab68583 100644 --- a/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py +++ b/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py @@ -1,7 +1,7 @@ from classic_controllers import Controller from externally_referenced_state_plot import ExternallyReferencedStatePlot import gym_electric_motor as gem -from gym_electric_motor.visualization import MotorDashboard, NewMotorDashboard +from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import SinusoidalReferenceGenerator import time from enum import Enum @@ -105,7 +105,7 @@ def get_state_names(self) -> list[str]: # initialize the gym-electric-motor environment env = gem.make( motor.get_env_id(), - visualization=NewMotorDashboard(additional_plots=external_ref_plots), + visualization=MotorDashboard(additional_plots=external_ref_plots), scale_plots=True, render_mode="figure", reference_generator=ref_generator, @@ -113,8 +113,8 @@ def get_state_names(self) -> list[str]: env.metadata["filename_prefix"] = "integration-test" env.metadata["filename_suffix"] = "" - env.metadata["save_figure_on_close"] = False - env.metadata["hold_figure_on_close"] = True + env.metadata["save_figure_on_close"] = True + env.metadata["hold_figure_on_close"] = False """ initialize the controller diff --git a/gym_electric_motor/visualization/__init__.py b/gym_electric_motor/visualization/__init__.py index aae3a4c9..2268475b 100644 --- a/gym_electric_motor/visualization/__init__.py +++ b/gym_electric_motor/visualization/__init__.py @@ -1,6 +1,5 @@ from .console_printer import ConsolePrinter from .motor_dashboard import MotorDashboard -from .new_motor_dashboard import NewMotorDashboard from ..utils import register_class from .. import ElectricMotorVisualization diff --git a/gym_electric_motor/visualization/motor_dashboard.py b/gym_electric_motor/visualization/motor_dashboard.py index 4012e696..402091d2 100644 --- a/gym_electric_motor/visualization/motor_dashboard.py +++ b/gym_electric_motor/visualization/motor_dashboard.py @@ -11,7 +11,7 @@ import gymnasium -class MotorDashboard(ElectricMotorVisualization): +class MotorDashboardClassic(ElectricMotorVisualization): """A dashboard to plot the GEM states into graphs. Every MotorDashboard consists of multiple MotorDashboardPlots that are each responsible for the plots in a single @@ -156,11 +156,14 @@ def on_step_end(self, k, state, reference, reward, terminated): def render(self): """Updates the plots every *update cycle* calls of this method.""" - if not ( - self._time_plot_figure - or self._episodic_plot_figure - or self._step_plot_figure - ) and len(self._plots) > 0: + if ( + not ( + self._time_plot_figure + or self._episodic_plot_figure + or self._step_plot_figure + ) + and len(self._plots) > 0 + ): self.initialize() if self._update_render: self._update() @@ -193,13 +196,10 @@ def set_env(self, env): self._time_plots.append(StatePlot(state)) if len(self._action_plots) > 0: - assert ( - type(env.action_space) - in ( - gymnasium.spaces.Box, - gymnasium.spaces.Discrete, - gymnasium.spaces.MultiDiscrete, - ) + assert type(env.action_space) in ( + gymnasium.spaces.Box, + gymnasium.spaces.Discrete, + gymnasium.spaces.MultiDiscrete, ), f"Action space of type {type(env.action_space)} not supported for plotting." for action in self._action_plots: ap = ActionPlot(action) @@ -324,3 +324,33 @@ def _update(self): # fig.align_ylabels() fig.canvas.draw() fig.canvas.flush_events() + + +# Proxy Object for Refactoring +class MotorDashboard(MotorDashboardClassic): + def __init__(self, *args, **kwargs): + a = args + b = kwargs + test = 222 + super().__init__(*args, **kwargs) + # self.on_reset_begin = None + + def set_env(self, env): + # Pass what you need + myenv = lambda: None + myenv.physical_system = lambda: None + myenv.physical_system.state_names = env.physical_system.state_names + myenv.physical_system.tau = env.physical_system.tau + myenv.physical_system.state_positions = env.physical_system.state_positions + myenv.physical_system.limits = env.physical_system.limits + myenv.physical_system.state_space = env.physical_system.state_space + myenv.physical_system.action_space = env.physical_system.action_space + # myenv.physical_system.action_space = env.physical_system.action_space + + myenv.reference_generator = env.reference_generator + myenv.scale_plots = env.scale_plots + + myenv.action_space = env.action_space + + super().set_env(myenv) + pass diff --git a/gym_electric_motor/visualization/new_motor_dashboard.py b/gym_electric_motor/visualization/new_motor_dashboard.py index d2e5241a..7d3fd0f4 100644 --- a/gym_electric_motor/visualization/new_motor_dashboard.py +++ b/gym_electric_motor/visualization/new_motor_dashboard.py @@ -12,12 +12,5 @@ import gymnasium -class NewMotorDashboard2(MotorDashboard): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - # self.on_reset_begin = None - - -class NewMotorDashboard: - def __init__(self, *args, **kwargs) -> None: - pass +def Object(object): + pass From 9105bc5735ef17be013e6cef7c35160f28a94a2e Mon Sep 17 00:00:00 2001 From: Stefan Arndt Date: Fri, 17 Nov 2023 14:32:53 +0100 Subject: [PATCH 05/51] removed callback --- ...ation_test_classic_controllers_dc_motor.py | 17 +++++--- gym_electric_motor/core.py | 41 +++++++------------ .../motor_dashboard_plots/state_plot.py | 2 + .../visualization/new_motor_dashboard.py | 16 -------- 4 files changed, 28 insertions(+), 48 deletions(-) delete mode 100644 gym_electric_motor/visualization/new_motor_dashboard.py diff --git a/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py b/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py index 8ab68583..6f966834 100644 --- a/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py +++ b/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py @@ -101,15 +101,16 @@ def get_state_names(self) -> list[str]: offset_range=(0, 0), episode_lengths=(10001, 10001), ) - + motor_dashboard = MotorDashboard(additional_plots=external_ref_plots) # initialize the gym-electric-motor environment env = gem.make( motor.get_env_id(), - visualization=MotorDashboard(additional_plots=external_ref_plots), + visualization=motor_dashboard, scale_plots=True, render_mode="figure", reference_generator=ref_generator, ) + motor_dashboard.set_env(env) env.metadata["filename_prefix"] = "integration-test" env.metadata["filename_suffix"] = "" @@ -128,21 +129,27 @@ def get_state_names(self) -> list[str]: """ controller = Controller.make(env, external_ref_plots=external_ref_plots) + motor_dashboard.on_reset_begin() (state, reference), _ = env.reset(seed=1337) + motor_dashboard.on_reset_end(state, reference) # simulate the environment - s = 0 for i in range(10001): - s += 1 action = controller.control(state, reference) # if i % 100 == 0: # (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) # else: + motor_dashboard.on_step_begin(i, action) (state, reference), reward, terminated, truncated, _ = env.step(action) - print(f"step{s}") + motor_dashboard.on_step_end(i, state, reference, reward, terminated) + # viz.render() if terminated: + motor_dashboard.on_reset_begin() env.reset() + motor_dashboard.on_reset_end(state, reference) + controller.reset() env.close() + motor_dashboard.on_close() diff --git a/gym_electric_motor/core.py b/gym_electric_motor/core.py index 01684cfb..ed50e58e 100644 --- a/gym_electric_motor/core.py +++ b/gym_electric_motor/core.py @@ -109,6 +109,7 @@ class ElectricMotorEnvironment(gymnasium.core.Env): sim = SimulationEnvironment() workspace = Workspace() + motor_dashboard = None env_id = None metadata = { @@ -302,9 +303,8 @@ def __init__( self.scale_plots = scale_plots - self._callbacks = list(callbacks) - self._callbacks += list(self._visualizations) - self._call_callbacks("set_env", self) + self.motor_dashboard = self._visualizations[0] + assert self.motor_dashboard is not None def make(env_id, *args, **kwargs): env = gymnasium.make(env_id, *args, **kwargs) @@ -317,9 +317,13 @@ def make(env_id, *args, **kwargs): def _call_callbacks(self, func_name, *args): """Calls each callback's func_name function with *args""" - for callback in self._callbacks: - func = getattr(callback, func_name) - func(*args) + a = func_name + b = args + print(f"a:{a}, {args.__len__()}") + + print("") + + assert False def reset(self, seed=None, *_, **__): """ @@ -331,12 +335,11 @@ def reset(self, seed=None, *_, **__): """ self._seed(seed) - self._call_callbacks("on_reset_begin") + self._terminated = False state = self._physical_system.reset() reference, next_ref, _ = self.reference_generator.reset(state) self._reward_function.reset(state, reference) - self._call_callbacks("on_reset_end", state, reference) observation = (state[self.state_filter], next_ref) info = {} @@ -365,8 +368,7 @@ def step(self, action): assert ( not self._terminated ), "A reset is required before the environment can perform further steps" - self._call_callbacks("on_step_begin", self.physical_system.k, action) - print(f"k:{self.physical_system.k}") + state = self._physical_system.simulate(action) reference = self.reference_generator.get_reference(state) violation_degree = self._constraint_monitor.check_constraints(state) @@ -375,14 +377,6 @@ def step(self, action): ) self._terminated = violation_degree >= 1.0 ref_next = self.reference_generator.get_reference_observation(state) - self._call_callbacks( - "on_step_end", - self.physical_system.k, - state, - reference, - reward, - self._terminated, - ) # Call render code if self.render_mode == "figure": @@ -404,7 +398,7 @@ def _seed(self, seed=None): self._reference_generator, self._reward_function, self._constraint_monitor, - ] + list(self._callbacks) + ] sub_sg = sg.spawn(len(components)) for sub, rc in zip(sub_sg, components): if isinstance(rc, gem.RandomComponent): @@ -448,20 +442,13 @@ def rendering_on_close(self): def close(self): """Called when the environment is deleted. Closes all its modules.""" - self._call_callbacks("on_close") + self._reward_function.close() self._physical_system.close() self._reference_generator.close() self.rendering_on_close() - def figure(self) -> Figure: - """Get main figure (MotorDashboard)""" - assert len(self._visualizations) == 1 - motor_dashboard = self._visualizations[0] - assert len(motor_dashboard._figures) == 1 - return motor_dashboard._figures[0] - class ReferenceGenerator: """The abstract base class for reference generators in gym electric motor environments. diff --git a/gym_electric_motor/visualization/motor_dashboard_plots/state_plot.py b/gym_electric_motor/visualization/motor_dashboard_plots/state_plot.py index 29debfae..348f51f2 100644 --- a/gym_electric_motor/visualization/motor_dashboard_plots/state_plot.py +++ b/gym_electric_motor/visualization/motor_dashboard_plots/state_plot.py @@ -172,6 +172,8 @@ def on_step_end(self, k, state, reference, reward, terminated): super().on_step_end(k, state, reference, reward, terminated) # Write the data to the data containers state_ = state[self._state_idx] + reference = list(reference) + reference += [0, 0, 0, 0] # TODO FIXME ref = reference[self._state_idx] idx = self.data_idx self._x_data[idx] = self._t diff --git a/gym_electric_motor/visualization/new_motor_dashboard.py b/gym_electric_motor/visualization/new_motor_dashboard.py deleted file mode 100644 index 7d3fd0f4..00000000 --- a/gym_electric_motor/visualization/new_motor_dashboard.py +++ /dev/null @@ -1,16 +0,0 @@ -from gym_electric_motor.core import ElectricMotorVisualization -from .motor_dashboard_plots import ( - StatePlot, - ActionPlot, - RewardPlot, - TimePlot, - EpisodePlot, - StepPlot, -) -from .motor_dashboard import MotorDashboard -import matplotlib.pyplot as plt -import gymnasium - - -def Object(object): - pass From 0d83e7243c5f63cdc431b51a7f2606764c4c9707 Mon Sep 17 00:00:00 2001 From: Stefan Arndt Date: Fri, 24 Nov 2023 16:05:35 +0100 Subject: [PATCH 06/51] move figure saving into motor_dashboard --- ...ation_test_classic_controllers_dc_motor.py | 1 + gym_electric_motor/core.py | 45 +++++++++-------- .../visualization/motor_dashboard.py | 46 +++++++++++++++++- motor_dashboard_plots/test.png | Bin 0 -> 241305 bytes 4 files changed, 67 insertions(+), 25 deletions(-) create mode 100644 motor_dashboard_plots/test.png diff --git a/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py b/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py index 6f966834..f3f8e96b 100644 --- a/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py +++ b/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py @@ -152,4 +152,5 @@ def get_state_names(self) -> list[str]: controller.reset() env.close() + motor_dashboard.save_to_file("test") motor_dashboard.on_close() diff --git a/gym_electric_motor/core.py b/gym_electric_motor/core.py index ed50e58e..0380d3fd 100644 --- a/gym_electric_motor/core.py +++ b/gym_electric_motor/core.py @@ -418,27 +418,28 @@ def save_fig(self, figure, filetype="png"): filename = f"{output_folder_name}/{filename_prefix}{filename_suffix}.{filetype}" figure.savefig(filename, dpi=300) - def rendering_on_close(self): - # Figure Mode - if self.render_mode and self.render_mode.startswith("figure"): - # Academic Mode (latex font) - if self.render_mode == "figure_academic": - matplotlib.rcParams.update( - {"text.usetex": True, "font.family": "Helvetica"} - ) - - self.render() - - # Save figure with timestamp as filename - if self.metadata["save_figure_on_close"]: - if self.render_mode == "figure_academic": - self.save_fig(self.figure(), filetype="pdf") - else: - self.save_fig(self.figure()) - - # Blocking plot call to still interactive with it - if self.metadata["hold_figure_on_close"]: - matplotlib.pyplot.show(block=True) + # TODO + # def rendering_on_close(self): + # # Figure Mode + # if self.render_mode and self.render_mode.startswith("figure"): + # # Academic Mode (latex font) + # if self.render_mode == "figure_academic": + # matplotlib.rcParams.update( + # {"text.usetex": True, "font.family": "Helvetica"} + # ) + + # self.render() + + # # Save figure with timestamp as filename + # if self.metadata["save_figure_on_close"]: + # if self.render_mode == "figure_academic": + # self.save_fig(self.figure(), filetype="pdf") + # else: + # self.save_fig(self.figure()) + + # # Blocking plot call to still interactive with it + # if self.metadata["hold_figure_on_close"]: + # matplotlib.pyplot.show(block=True) def close(self): """Called when the environment is deleted. Closes all its modules.""" @@ -447,8 +448,6 @@ def close(self): self._physical_system.close() self._reference_generator.close() - self.rendering_on_close() - class ReferenceGenerator: """The abstract base class for reference generators in gym electric motor environments. diff --git a/gym_electric_motor/visualization/motor_dashboard.py b/gym_electric_motor/visualization/motor_dashboard.py index 402091d2..68093e2b 100644 --- a/gym_electric_motor/visualization/motor_dashboard.py +++ b/gym_electric_motor/visualization/motor_dashboard.py @@ -7,11 +7,14 @@ EpisodePlot, StepPlot, ) +import matplotlib import matplotlib.pyplot as plt import gymnasium +from enum import Enum +import os -class MotorDashboardClassic(ElectricMotorVisualization): +class MotorDashboardLegacy(ElectricMotorVisualization): """A dashboard to plot the GEM states into graphs. Every MotorDashboard consists of multiple MotorDashboardPlots that are each responsible for the plots in a single @@ -327,7 +330,10 @@ def _update(self): # Proxy Object for Refactoring -class MotorDashboard(MotorDashboardClassic): +class MotorDashboard(MotorDashboardLegacy): + RenderMode = Enum("RenderMode", ["Default", "Academic"]) + render_mode = RenderMode.Default + def __init__(self, *args, **kwargs): a = args b = kwargs @@ -354,3 +360,39 @@ def set_env(self, env): super().set_env(myenv) pass + + def figure(self): + """Get main figure (MotorDashboard)""" + assert len(self._figures) == 1 + return self._figures[0] + + def show_blocked(self): + plt.show(block=True) + + def save_to_file(self, filename=None): + # Academic Mode (latex font) + if self.render_mode == self.RenderMode.Academic: + matplotlib.rcParams.update( + {"text.usetex": True, "font.family": "Helvetica"} + ) + + self.render() + self._save_fig(filename) + + def _save_fig(self, filename=None): + """Save figure with timestamped as filename""" + # create output folder if it not exists + + output_folder_name = "motor_dashboard_plots" + if not os.path.exists(output_folder_name): + # Create the folder "gem_output" + os.makedirs(output_folder_name) + + if self.render_mode == self.RenderMode.Academic: + filetype = "pdf" + else: + filetype = "png" + + filepath = f"{output_folder_name}/{filename}.{filetype}" + print(f"Saved figure to file: {filepath}") + self.figure().savefig(filepath, dpi=300) diff --git a/motor_dashboard_plots/test.png b/motor_dashboard_plots/test.png new file mode 100644 index 0000000000000000000000000000000000000000..6728461fed47f1fc7d4a625eff5cc59861d96e69 GIT binary patch literal 241305 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%0A^v5bB0tYxC?vmKe?OGwqoEraJQ-XZSEm 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 Date: Mon, 4 Dec 2023 09:05:25 +0100 Subject: [PATCH 07/51] change plot files folder --- .../visualization/motor_dashboard.py | 5 +---- motor_dashboard_plots/test.png | Bin 241305 -> 0 bytes 2 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 motor_dashboard_plots/test.png diff --git a/gym_electric_motor/visualization/motor_dashboard.py b/gym_electric_motor/visualization/motor_dashboard.py index 68093e2b..473edb5a 100644 --- a/gym_electric_motor/visualization/motor_dashboard.py +++ b/gym_electric_motor/visualization/motor_dashboard.py @@ -335,9 +335,6 @@ class MotorDashboard(MotorDashboardLegacy): render_mode = RenderMode.Default def __init__(self, *args, **kwargs): - a = args - b = kwargs - test = 222 super().__init__(*args, **kwargs) # self.on_reset_begin = None @@ -383,7 +380,7 @@ def _save_fig(self, filename=None): """Save figure with timestamped as filename""" # create output folder if it not exists - output_folder_name = "motor_dashboard_plots" + output_folder_name = "saved_plots" if not os.path.exists(output_folder_name): # Create the folder "gem_output" os.makedirs(output_folder_name) diff --git a/motor_dashboard_plots/test.png b/motor_dashboard_plots/test.png deleted file mode 100644 index 6728461fed47f1fc7d4a625eff5cc59861d96e69..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 241305 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%0A^v5bB0tYxC?vmKe?OGwqoEraJQ-XZSEm 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 Date: Mon, 4 Dec 2023 09:06:10 +0100 Subject: [PATCH 08/51] add plot folder --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 180017c8..be0750cb 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,5 @@ examples/logs/ /my_examples/ .vscode/settings.json .vscode/launch.json -plots/ \ No newline at end of file +plots/ +saved_plots/ \ No newline at end of file From bec5156cb01b1e934cf896db2f6bedebff26894f Mon Sep 17 00:00:00 2001 From: Stefan Arndt Date: Mon, 11 Dec 2023 09:39:39 +0100 Subject: [PATCH 09/51] added callbacks back --- ...ation_test_classic_controllers_dc_motor.py | 7 -- gym_electric_motor/core.py | 86 +++++++++++-------- .../motor_dashboard_plots/state_plot.py | 2 - 3 files changed, 50 insertions(+), 45 deletions(-) diff --git a/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py b/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py index f3f8e96b..2edd1e79 100644 --- a/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py +++ b/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py @@ -129,28 +129,21 @@ def get_state_names(self) -> list[str]: """ controller = Controller.make(env, external_ref_plots=external_ref_plots) - motor_dashboard.on_reset_begin() (state, reference), _ = env.reset(seed=1337) - motor_dashboard.on_reset_end(state, reference) # simulate the environment for i in range(10001): action = controller.control(state, reference) # if i % 100 == 0: # (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) # else: - motor_dashboard.on_step_begin(i, action) (state, reference), reward, terminated, truncated, _ = env.step(action) - motor_dashboard.on_step_end(i, state, reference, reward, terminated) # viz.render() if terminated: - motor_dashboard.on_reset_begin() env.reset() - motor_dashboard.on_reset_end(state, reference) controller.reset() env.close() motor_dashboard.save_to_file("test") - motor_dashboard.on_close() diff --git a/gym_electric_motor/core.py b/gym_electric_motor/core.py index 0380d3fd..01684cfb 100644 --- a/gym_electric_motor/core.py +++ b/gym_electric_motor/core.py @@ -109,7 +109,6 @@ class ElectricMotorEnvironment(gymnasium.core.Env): sim = SimulationEnvironment() workspace = Workspace() - motor_dashboard = None env_id = None metadata = { @@ -303,8 +302,9 @@ def __init__( self.scale_plots = scale_plots - self.motor_dashboard = self._visualizations[0] - assert self.motor_dashboard is not None + self._callbacks = list(callbacks) + self._callbacks += list(self._visualizations) + self._call_callbacks("set_env", self) def make(env_id, *args, **kwargs): env = gymnasium.make(env_id, *args, **kwargs) @@ -317,13 +317,9 @@ def make(env_id, *args, **kwargs): def _call_callbacks(self, func_name, *args): """Calls each callback's func_name function with *args""" - a = func_name - b = args - print(f"a:{a}, {args.__len__()}") - - print("") - - assert False + for callback in self._callbacks: + func = getattr(callback, func_name) + func(*args) def reset(self, seed=None, *_, **__): """ @@ -335,11 +331,12 @@ def reset(self, seed=None, *_, **__): """ self._seed(seed) - + self._call_callbacks("on_reset_begin") self._terminated = False state = self._physical_system.reset() reference, next_ref, _ = self.reference_generator.reset(state) self._reward_function.reset(state, reference) + self._call_callbacks("on_reset_end", state, reference) observation = (state[self.state_filter], next_ref) info = {} @@ -368,7 +365,8 @@ def step(self, action): assert ( not self._terminated ), "A reset is required before the environment can perform further steps" - + self._call_callbacks("on_step_begin", self.physical_system.k, action) + print(f"k:{self.physical_system.k}") state = self._physical_system.simulate(action) reference = self.reference_generator.get_reference(state) violation_degree = self._constraint_monitor.check_constraints(state) @@ -377,6 +375,14 @@ def step(self, action): ) self._terminated = violation_degree >= 1.0 ref_next = self.reference_generator.get_reference_observation(state) + self._call_callbacks( + "on_step_end", + self.physical_system.k, + state, + reference, + reward, + self._terminated, + ) # Call render code if self.render_mode == "figure": @@ -398,7 +404,7 @@ def _seed(self, seed=None): self._reference_generator, self._reward_function, self._constraint_monitor, - ] + ] + list(self._callbacks) sub_sg = sg.spawn(len(components)) for sub, rc in zip(sub_sg, components): if isinstance(rc, gem.RandomComponent): @@ -418,36 +424,44 @@ def save_fig(self, figure, filetype="png"): filename = f"{output_folder_name}/{filename_prefix}{filename_suffix}.{filetype}" figure.savefig(filename, dpi=300) - # TODO - # def rendering_on_close(self): - # # Figure Mode - # if self.render_mode and self.render_mode.startswith("figure"): - # # Academic Mode (latex font) - # if self.render_mode == "figure_academic": - # matplotlib.rcParams.update( - # {"text.usetex": True, "font.family": "Helvetica"} - # ) - - # self.render() - - # # Save figure with timestamp as filename - # if self.metadata["save_figure_on_close"]: - # if self.render_mode == "figure_academic": - # self.save_fig(self.figure(), filetype="pdf") - # else: - # self.save_fig(self.figure()) - - # # Blocking plot call to still interactive with it - # if self.metadata["hold_figure_on_close"]: - # matplotlib.pyplot.show(block=True) + def rendering_on_close(self): + # Figure Mode + if self.render_mode and self.render_mode.startswith("figure"): + # Academic Mode (latex font) + if self.render_mode == "figure_academic": + matplotlib.rcParams.update( + {"text.usetex": True, "font.family": "Helvetica"} + ) + + self.render() + + # Save figure with timestamp as filename + if self.metadata["save_figure_on_close"]: + if self.render_mode == "figure_academic": + self.save_fig(self.figure(), filetype="pdf") + else: + self.save_fig(self.figure()) + + # Blocking plot call to still interactive with it + if self.metadata["hold_figure_on_close"]: + matplotlib.pyplot.show(block=True) def close(self): """Called when the environment is deleted. Closes all its modules.""" - + self._call_callbacks("on_close") self._reward_function.close() self._physical_system.close() self._reference_generator.close() + self.rendering_on_close() + + def figure(self) -> Figure: + """Get main figure (MotorDashboard)""" + assert len(self._visualizations) == 1 + motor_dashboard = self._visualizations[0] + assert len(motor_dashboard._figures) == 1 + return motor_dashboard._figures[0] + class ReferenceGenerator: """The abstract base class for reference generators in gym electric motor environments. diff --git a/gym_electric_motor/visualization/motor_dashboard_plots/state_plot.py b/gym_electric_motor/visualization/motor_dashboard_plots/state_plot.py index 348f51f2..29debfae 100644 --- a/gym_electric_motor/visualization/motor_dashboard_plots/state_plot.py +++ b/gym_electric_motor/visualization/motor_dashboard_plots/state_plot.py @@ -172,8 +172,6 @@ def on_step_end(self, k, state, reference, reward, terminated): super().on_step_end(k, state, reference, reward, terminated) # Write the data to the data containers state_ = state[self._state_idx] - reference = list(reference) - reference += [0, 0, 0, 0] # TODO FIXME ref = reference[self._state_idx] idx = self.data_idx self._x_data[idx] = self._t From 3893168acaa73db57d9ac9ad3983f0749757b5b7 Mon Sep 17 00:00:00 2001 From: Stefan Arndt Date: Fri, 15 Dec 2023 13:25:14 +0100 Subject: [PATCH 10/51] helper enums for render mode and motor id --- ...ation_test_classic_controllers_dc_motor.py | 60 +------------------ gym_electric_motor/core.py | 46 +------------- gym_electric_motor/helper.py | 60 +++++++++++++++++++ 3 files changed, 63 insertions(+), 103 deletions(-) create mode 100644 gym_electric_motor/helper.py diff --git a/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py b/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py index 2edd1e79..d0829b90 100644 --- a/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py +++ b/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py @@ -4,64 +4,7 @@ from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import SinusoidalReferenceGenerator import time -from enum import Enum -from dataclasses import dataclass - -MotorType = Enum( - "MotorType", - ["PermanentlyExcitedDcMotor", "ExternallyExcitedDcMotor", "SeriesDc", "ShuntDc"], -) -MotorType.PermanentlyExcitedDcMotor.env_id_tag = "PermExDc" -MotorType.ExternallyExcitedDcMotor.env_id_tag = "ExtExDc" -MotorType.PermanentlyExcitedDcMotor.states = ["omega", "torque", "i", "u"] -MotorType.ExternallyExcitedDcMotor.states = [ - "omega", - "torque", - "i_a", - "i_e", - "u_a", - "u_e", -] -MotorType.SeriesDc.states = ["omega", "torque", "i", "u"] -MotorType.ShuntDc.states = ["omega", "torque", "i_a", "i_e", "u"] - - -ControlType = Enum("ControlType", ["SpeedControl", "TorqueControl", "CurrentControl"]) -ControlType.SpeedControl.env_id_tag = "SC" -ControlType.TorqueControl.env_id_tag = "TC" -ControlType.CurrentControl.env_id_tag = "CC" - -ActionType = Enum("ActionType", ["Continuous", "Finite"]) -ActionType.Continuous.env_id_tag = "Cont" - - -# We need this helper functions to be backwards compatible with env_id string -def _get_env_id_tag(t) -> str: - if hasattr(t, "env_id_tag"): - return t.env_id_tag - else: - return t.name - - -@dataclass -class Motor: - motor_type: MotorType - control_type: ControlType - action_type: ActionType - - def get_env_id(self) -> str: - return ( - _get_env_id_tag(self.action_type) - + "-" - + _get_env_id_tag(self.control_type) - + "-" - + _get_env_id_tag(self.motor_type) - + "-v0" - ) - - def get_state_names(self) -> list[str]: - return self.motor_type.states - +from gym_electric_motor.helper import * if __name__ == "__main__": """ @@ -146,4 +89,5 @@ def get_state_names(self) -> list[str]: controller.reset() env.close() + motor_dashboard.show() motor_dashboard.save_to_file("test") diff --git a/gym_electric_motor/core.py b/gym_electric_motor/core.py index 01684cfb..a444880b 100644 --- a/gym_electric_motor/core.py +++ b/gym_electric_motor/core.py @@ -366,7 +366,7 @@ def step(self, action): not self._terminated ), "A reset is required before the environment can perform further steps" self._call_callbacks("on_step_begin", self.physical_system.k, action) - print(f"k:{self.physical_system.k}") + state = self._physical_system.simulate(action) reference = self.reference_generator.get_reference(state) violation_degree = self._constraint_monitor.check_constraints(state) @@ -411,41 +411,6 @@ def _seed(self, seed=None): rc.seed(sub) return [sg.entropy] - def save_fig(self, figure, filetype="png"): - """Save figure with timestamped as filename""" - # create output folder if it not exists - output_folder_name = "plots" - if not os.path.exists(output_folder_name): - # Create the folder "gem_output" - os.makedirs(output_folder_name) - - filename_prefix = self.metadata["filename_prefix"] - filename_suffix = self.metadata["filename_suffix"] - filename = f"{output_folder_name}/{filename_prefix}{filename_suffix}.{filetype}" - figure.savefig(filename, dpi=300) - - def rendering_on_close(self): - # Figure Mode - if self.render_mode and self.render_mode.startswith("figure"): - # Academic Mode (latex font) - if self.render_mode == "figure_academic": - matplotlib.rcParams.update( - {"text.usetex": True, "font.family": "Helvetica"} - ) - - self.render() - - # Save figure with timestamp as filename - if self.metadata["save_figure_on_close"]: - if self.render_mode == "figure_academic": - self.save_fig(self.figure(), filetype="pdf") - else: - self.save_fig(self.figure()) - - # Blocking plot call to still interactive with it - if self.metadata["hold_figure_on_close"]: - matplotlib.pyplot.show(block=True) - def close(self): """Called when the environment is deleted. Closes all its modules.""" self._call_callbacks("on_close") @@ -453,15 +418,6 @@ def close(self): self._physical_system.close() self._reference_generator.close() - self.rendering_on_close() - - def figure(self) -> Figure: - """Get main figure (MotorDashboard)""" - assert len(self._visualizations) == 1 - motor_dashboard = self._visualizations[0] - assert len(motor_dashboard._figures) == 1 - return motor_dashboard._figures[0] - class ReferenceGenerator: """The abstract base class for reference generators in gym electric motor environments. diff --git a/gym_electric_motor/helper.py b/gym_electric_motor/helper.py new file mode 100644 index 00000000..79203dfb --- /dev/null +++ b/gym_electric_motor/helper.py @@ -0,0 +1,60 @@ +from enum import Enum +from dataclasses import dataclass + +RenderMode = Enum("RenderMode", ["Figure", "Academic"]) + + +MotorType = Enum( + "MotorType", + ["PermanentlyExcitedDcMotor", "ExternallyExcitedDcMotor", "SeriesDc", "ShuntDc"], +) +MotorType.PermanentlyExcitedDcMotor.env_id_tag = "PermExDc" +MotorType.ExternallyExcitedDcMotor.env_id_tag = "ExtExDc" +MotorType.PermanentlyExcitedDcMotor.states = ["omega", "torque", "i", "u"] +MotorType.ExternallyExcitedDcMotor.states = [ + "omega", + "torque", + "i_a", + "i_e", + "u_a", + "u_e", +] +MotorType.SeriesDc.states = ["omega", "torque", "i", "u"] +MotorType.ShuntDc.states = ["omega", "torque", "i_a", "i_e", "u"] + + +ControlType = Enum("ControlType", ["SpeedControl", "TorqueControl", "CurrentControl"]) +ControlType.SpeedControl.env_id_tag = "SC" +ControlType.TorqueControl.env_id_tag = "TC" +ControlType.CurrentControl.env_id_tag = "CC" + +ActionType = Enum("ActionType", ["Continuous", "Finite"]) +ActionType.Continuous.env_id_tag = "Cont" + + +# We need this helper functions to be backwards compatible with env_id string +def _get_env_id_tag(t) -> str: + if hasattr(t, "env_id_tag"): + return t.env_id_tag + else: + return t.name + + +@dataclass +class Motor: + motor_type: MotorType + control_type: ControlType + action_type: ActionType + + def get_env_id(self) -> str: + return ( + _get_env_id_tag(self.action_type) + + "-" + + _get_env_id_tag(self.control_type) + + "-" + + _get_env_id_tag(self.motor_type) + + "-v0" + ) + + def get_state_names(self) -> list[str]: + return self.motor_type.states From 87a1ba9128038688dbd2aa0590b5b96042923555 Mon Sep 17 00:00:00 2001 From: Stefan Arndt Date: Mon, 18 Dec 2023 09:59:01 +0100 Subject: [PATCH 11/51] moved dashboard configuration out of core.py --- ...ation_test_classic_controllers_dc_motor.py | 12 +++---- gym_electric_motor/core.py | 23 +------------ gym_electric_motor/helper.py | 2 +- .../visualization/motor_dashboard.py | 34 +++++++++++++------ 4 files changed, 31 insertions(+), 40 deletions(-) diff --git a/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py b/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py index d0829b90..179264b0 100644 --- a/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py +++ b/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py @@ -44,21 +44,18 @@ offset_range=(0, 0), episode_lengths=(10001, 10001), ) - motor_dashboard = MotorDashboard(additional_plots=external_ref_plots) + motor_dashboard = MotorDashboard( + additional_plots=external_ref_plots, render_mode=RenderMode.FigureOnce + ) # initialize the gym-electric-motor environment env = gem.make( motor.get_env_id(), visualization=motor_dashboard, scale_plots=True, - render_mode="figure", reference_generator=ref_generator, ) motor_dashboard.set_env(env) - env.metadata["filename_prefix"] = "integration-test" - env.metadata["filename_suffix"] = "" - env.metadata["save_figure_on_close"] = True - env.metadata["hold_figure_on_close"] = False """ initialize the controller @@ -89,5 +86,6 @@ controller.reset() env.close() - motor_dashboard.show() + motor_dashboard.save_to_file("test") + motor_dashboard.show_and_hold() diff --git a/gym_electric_motor/core.py b/gym_electric_motor/core.py index a444880b..9df23790 100644 --- a/gym_electric_motor/core.py +++ b/gym_electric_motor/core.py @@ -111,11 +111,6 @@ class ElectricMotorEnvironment(gymnasium.core.Env): workspace = Workspace() env_id = None - metadata = { - "render_modes": [None, "figure", "figure_once", "figure_academic"], - "save_figure_on_close": False, - "hold_figure_on_close": True, - } @property def physical_system(self): @@ -207,7 +202,6 @@ def __init__( callbacks=(), constraints=(), physical_system_wrappers=(), - render_mode=None, scale_plots=None, **kwargs, ): @@ -231,7 +225,6 @@ def __init__( state_filter(list(str)): Selection of states that are shown in the observation. physical_system_wrappers(iterable(PhysicalSystemWrapper)): PhysicalSystemWrapper instances to be wrapped around the physical system. - render_mode(str) : if visualization is given, render_mode is set to "figure", else render_mode ist set to None callbacks(list(Callback)): Callbacks being called in the environment **kwargs: Arguments to be passed to the modules. """ @@ -296,10 +289,6 @@ def __init__( self._terminated = True self._truncated = False - # Set render mode and metadata - assert render_mode in self.metadata["render_modes"] - self.render_mode = render_mode - self.scale_plots = scale_plots self._callbacks = list(callbacks) @@ -307,13 +296,7 @@ def __init__( self._call_callbacks("set_env", self) def make(env_id, *args, **kwargs): - env = gymnasium.make(env_id, *args, **kwargs) - env.metadata["filename_prefix"] = env_id - - timestamp = datetime.datetime.now().strftime("%Y-%m-%d-%H%M%S") - env.metadata["filename_suffix"] = f"_{timestamp}" - - return env + return gymnasium.make(env_id, *args, **kwargs) def _call_callbacks(self, func_name, *args): """Calls each callback's func_name function with *args""" @@ -384,10 +367,6 @@ def step(self, action): self._terminated, ) - # Call render code - if self.render_mode == "figure": - self.render() - info = {} return ( (state[self.state_filter], ref_next), diff --git a/gym_electric_motor/helper.py b/gym_electric_motor/helper.py index 79203dfb..c9fbde80 100644 --- a/gym_electric_motor/helper.py +++ b/gym_electric_motor/helper.py @@ -1,7 +1,7 @@ from enum import Enum from dataclasses import dataclass -RenderMode = Enum("RenderMode", ["Figure", "Academic"]) +RenderMode = Enum("RenderMode", ["Figure", "FigureOnce"]) MotorType = Enum( diff --git a/gym_electric_motor/visualization/motor_dashboard.py b/gym_electric_motor/visualization/motor_dashboard.py index 473edb5a..9c247f20 100644 --- a/gym_electric_motor/visualization/motor_dashboard.py +++ b/gym_electric_motor/visualization/motor_dashboard.py @@ -1,4 +1,5 @@ from gym_electric_motor.core import ElectricMotorVisualization +from gym_electric_motor.helper import * from .motor_dashboard_plots import ( StatePlot, ActionPlot, @@ -331,11 +332,12 @@ def _update(self): # Proxy Object for Refactoring class MotorDashboard(MotorDashboardLegacy): - RenderMode = Enum("RenderMode", ["Default", "Academic"]) - render_mode = RenderMode.Default + render_mode = RenderMode.Figure - def __init__(self, *args, **kwargs): + def __init__(self, render_mode=RenderMode.Figure, *args, **kwargs): super().__init__(*args, **kwargs) + self.render_mode = render_mode + # self.on_reset_begin = None def set_env(self, env): @@ -356,27 +358,39 @@ def set_env(self, env): myenv.action_space = env.action_space super().set_env(myenv) - pass def figure(self): """Get main figure (MotorDashboard)""" assert len(self._figures) == 1 return self._figures[0] - def show_blocked(self): + def on_close(self): + super().on_close() + if self.render_mode == RenderMode.FigureOnce: + self.render() + + def on_step_end(self, k, state, reference, reward, terminated): + super().on_step_end(k, state, reference, reward, terminated) + if self.render_mode == RenderMode.Figure: + self.render() + + def show(self): + plt.show(block=False) + + def show_and_hold(self): plt.show(block=True) - def save_to_file(self, filename=None): + def save_to_file(self, filename=None, academic_mode=False): # Academic Mode (latex font) - if self.render_mode == self.RenderMode.Academic: + if academic_mode: matplotlib.rcParams.update( {"text.usetex": True, "font.family": "Helvetica"} ) self.render() - self._save_fig(filename) + self._save_fig(filename, academic_mode) - def _save_fig(self, filename=None): + def _save_fig(self, filename, academic_mode): """Save figure with timestamped as filename""" # create output folder if it not exists @@ -385,7 +399,7 @@ def _save_fig(self, filename=None): # Create the folder "gem_output" os.makedirs(output_folder_name) - if self.render_mode == self.RenderMode.Academic: + if academic_mode: filetype = "pdf" else: filetype = "png" From d0158eb3291891a55f20808e54a92aad82e34991 Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Mon, 8 Jan 2024 16:11:21 +0100 Subject: [PATCH 12/51] Reorganized new Motor and RenderMode helpers --- ...ation_test_classic_controllers_dc_motor.py | 14 ++++------- gym_electric_motor/envs/__init__.py | 2 ++ .../{helper.py => envs/motors.py} | 23 +++++++++---------- gym_electric_motor/visualization/__init__.py | 1 + .../visualization/motor_dashboard.py | 17 ++++++++++---- .../motor_dashboard_plots/base_plots.py | 3 +++ .../visualization/render_modes.py | 3 +++ 7 files changed, 38 insertions(+), 25 deletions(-) rename gym_electric_motor/{helper.py => envs/motors.py} (76%) create mode 100644 gym_electric_motor/visualization/render_modes.py diff --git a/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py b/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py index 179264b0..7a40def3 100644 --- a/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py +++ b/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py @@ -1,10 +1,10 @@ from classic_controllers import Controller from externally_referenced_state_plot import ExternallyReferencedStatePlot import gym_electric_motor as gem -from gym_electric_motor.visualization import MotorDashboard +from gym_electric_motor.envs.motors import MotorType, ControlType, ActionType, Motor +from gym_electric_motor.visualization import MotorDashboard, RenderMode from gym_electric_motor.reference_generators import SinusoidalReferenceGenerator import time -from gym_electric_motor.helper import * if __name__ == "__main__": """ @@ -21,10 +21,6 @@ 'Finite' Discrete Action Space """ - # motor_type = "PermExDc" - # control_type = "SC" - # action_type = "Cont" - motor = Motor( MotorType.PermanentlyExcitedDcMotor, ControlType.SpeedControl, @@ -33,7 +29,7 @@ # definition of the plotted variables external_ref_plots = [ - ExternallyReferencedStatePlot(state) for state in motor.get_state_names() + ExternallyReferencedStatePlot(state) for state in motor.state_names() ] # definition of the reference generator @@ -49,7 +45,7 @@ ) # initialize the gym-electric-motor environment env = gem.make( - motor.get_env_id(), + motor.env_id(), visualization=motor_dashboard, scale_plots=True, reference_generator=ref_generator, @@ -87,5 +83,5 @@ env.close() - motor_dashboard.save_to_file("test") + motor_dashboard.save_to_file(academic_mode=True) motor_dashboard.show_and_hold() diff --git a/gym_electric_motor/envs/__init__.py b/gym_electric_motor/envs/__init__.py index e4dbb3dc..be817f3b 100644 --- a/gym_electric_motor/envs/__init__.py +++ b/gym_electric_motor/envs/__init__.py @@ -1,3 +1,5 @@ +from .motors import Motor, MotorType, ControlType, ActionType + # Version 1 from .gym_dcm.permex_dc_motor_env import ContSpeedControlDcPermanentlyExcitedMotorEnv from .gym_dcm.permex_dc_motor_env import FiniteSpeedControlDcPermanentlyExcitedMotorEnv diff --git a/gym_electric_motor/helper.py b/gym_electric_motor/envs/motors.py similarity index 76% rename from gym_electric_motor/helper.py rename to gym_electric_motor/envs/motors.py index c9fbde80..53633aee 100644 --- a/gym_electric_motor/helper.py +++ b/gym_electric_motor/envs/motors.py @@ -1,15 +1,10 @@ from enum import Enum from dataclasses import dataclass -RenderMode = Enum("RenderMode", ["Figure", "FigureOnce"]) - - MotorType = Enum( "MotorType", ["PermanentlyExcitedDcMotor", "ExternallyExcitedDcMotor", "SeriesDc", "ShuntDc"], ) -MotorType.PermanentlyExcitedDcMotor.env_id_tag = "PermExDc" -MotorType.ExternallyExcitedDcMotor.env_id_tag = "ExtExDc" MotorType.PermanentlyExcitedDcMotor.states = ["omega", "torque", "i", "u"] MotorType.ExternallyExcitedDcMotor.states = [ "omega", @@ -23,6 +18,10 @@ MotorType.ShuntDc.states = ["omega", "torque", "i_a", "i_e", "u"] +# add env_id_tag if you dont want to use enum name as env_id +MotorType.PermanentlyExcitedDcMotor.env_id_tag = "PermExDc" +MotorType.ExternallyExcitedDcMotor.env_id_tag = "ExtExDc" + ControlType = Enum("ControlType", ["SpeedControl", "TorqueControl", "CurrentControl"]) ControlType.SpeedControl.env_id_tag = "SC" ControlType.TorqueControl.env_id_tag = "TC" @@ -32,8 +31,8 @@ ActionType.Continuous.env_id_tag = "Cont" -# We need this helper functions to be backwards compatible with env_id string -def _get_env_id_tag(t) -> str: +# Check if we added an env_id_tag and use this instead of the enum name +def _to_env_id(t) -> str: if hasattr(t, "env_id_tag"): return t.env_id_tag else: @@ -46,15 +45,15 @@ class Motor: control_type: ControlType action_type: ActionType - def get_env_id(self) -> str: + def env_id(self) -> str: return ( - _get_env_id_tag(self.action_type) + _to_env_id(self.action_type) + "-" - + _get_env_id_tag(self.control_type) + + _to_env_id(self.control_type) + "-" - + _get_env_id_tag(self.motor_type) + + _to_env_id(self.motor_type) + "-v0" ) - def get_state_names(self) -> list[str]: + def state_names(self) -> list[str]: return self.motor_type.states diff --git a/gym_electric_motor/visualization/__init__.py b/gym_electric_motor/visualization/__init__.py index 2268475b..f7329e21 100644 --- a/gym_electric_motor/visualization/__init__.py +++ b/gym_electric_motor/visualization/__init__.py @@ -1,5 +1,6 @@ from .console_printer import ConsolePrinter from .motor_dashboard import MotorDashboard +from .render_modes import RenderMode from ..utils import register_class from .. import ElectricMotorVisualization diff --git a/gym_electric_motor/visualization/motor_dashboard.py b/gym_electric_motor/visualization/motor_dashboard.py index 9c247f20..4015f4c1 100644 --- a/gym_electric_motor/visualization/motor_dashboard.py +++ b/gym_electric_motor/visualization/motor_dashboard.py @@ -1,5 +1,4 @@ from gym_electric_motor.core import ElectricMotorVisualization -from gym_electric_motor.helper import * from .motor_dashboard_plots import ( StatePlot, ActionPlot, @@ -8,11 +7,13 @@ EpisodePlot, StepPlot, ) +from .render_modes import RenderMode import matplotlib import matplotlib.pyplot as plt import gymnasium from enum import Enum import os +import time class MotorDashboardLegacy(ElectricMotorVisualization): @@ -365,9 +366,9 @@ def figure(self): return self._figures[0] def on_close(self): - super().on_close() if self.render_mode == RenderMode.FigureOnce: self.render() + super().on_close() def on_step_end(self, k, state, reference, reward, terminated): super().on_step_end(k, state, reference, reward, terminated) @@ -380,14 +381,22 @@ def show(self): def show_and_hold(self): plt.show(block=True) + def force_render(self): + self._update_render = True + self.render() + def save_to_file(self, filename=None, academic_mode=False): + if filename is None: + timestamp_string = time.strftime("%Y%m%d-%H%M%S") + filename = f"gem_plot_{timestamp_string}" + # Academic Mode (latex font) if academic_mode: matplotlib.rcParams.update( {"text.usetex": True, "font.family": "Helvetica"} ) - self.render() + self.force_render() self._save_fig(filename, academic_mode) def _save_fig(self, filename, academic_mode): @@ -396,7 +405,7 @@ def _save_fig(self, filename, academic_mode): output_folder_name = "saved_plots" if not os.path.exists(output_folder_name): - # Create the folder "gem_output" + print(f"Creating output folder for plots: {output_folder_name}") os.makedirs(output_folder_name) if academic_mode: diff --git a/gym_electric_motor/visualization/motor_dashboard_plots/base_plots.py b/gym_electric_motor/visualization/motor_dashboard_plots/base_plots.py index 0254e54b..682d39b7 100644 --- a/gym_electric_motor/visualization/motor_dashboard_plots/base_plots.py +++ b/gym_electric_motor/visualization/motor_dashboard_plots/base_plots.py @@ -187,6 +187,9 @@ def _scale_x_axis(self): def _scale_y_axis(self): if self._scale_plots_to_data: + # nanmin does not reraise RuntimeWarning so we to check for NaNs ourselves + if np.isnan(self._y_data[0]).all() or np.isnan(self._y_data[1]).all(): + return y_min = min(np.nanmin(self._y_data[0]), np.nanmin(self._y_data[1])) y_max = max(np.nanmax(self._y_data[0]), np.nanmax(self._y_data[1])) diff --git a/gym_electric_motor/visualization/render_modes.py b/gym_electric_motor/visualization/render_modes.py new file mode 100644 index 00000000..86152914 --- /dev/null +++ b/gym_electric_motor/visualization/render_modes.py @@ -0,0 +1,3 @@ +from enum import Enum + +RenderMode = Enum("RenderMode", ["Figure", "FigureOnce"]) From 09e3eb844031cfc0d73b143bf201795c6283cb61 Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Mon, 15 Jan 2024 18:21:35 +0100 Subject: [PATCH 13/51] changed acadamic mode handling --- .../visualization/motor_dashboard.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/gym_electric_motor/visualization/motor_dashboard.py b/gym_electric_motor/visualization/motor_dashboard.py index 4015f4c1..9e71f5db 100644 --- a/gym_electric_motor/visualization/motor_dashboard.py +++ b/gym_electric_motor/visualization/motor_dashboard.py @@ -342,7 +342,7 @@ def __init__(self, render_mode=RenderMode.Figure, *args, **kwargs): # self.on_reset_begin = None def set_env(self, env): - # Pass what you need + # This is the only data we need from the environment myenv = lambda: None myenv.physical_system = lambda: None myenv.physical_system.state_names = env.physical_system.state_names @@ -351,7 +351,7 @@ def set_env(self, env): myenv.physical_system.limits = env.physical_system.limits myenv.physical_system.state_space = env.physical_system.state_space myenv.physical_system.action_space = env.physical_system.action_space - # myenv.physical_system.action_space = env.physical_system.action_space + # myenv._plots = env._plots myenv.reference_generator = env.reference_generator myenv.scale_plots = env.scale_plots @@ -390,16 +390,23 @@ def save_to_file(self, filename=None, academic_mode=False): timestamp_string = time.strftime("%Y%m%d-%H%M%S") filename = f"gem_plot_{timestamp_string}" - # Academic Mode (latex font) + # Academic Mode (latex font), needs some prerequisites to be installed if academic_mode: matplotlib.rcParams.update( - {"text.usetex": True, "font.family": "Helvetica"} + { + "text.usetex": True, + "font.family": "sans-serif", + "font.sans-serif": "Helvetica", + } ) self.force_render() - self._save_fig(filename, academic_mode) + if academic_mode: + self._save_fig(filename, filetype="pdf") + else: + self._save_fig(filename, filetype="png") - def _save_fig(self, filename, academic_mode): + def _save_fig(self, filename, filetype): """Save figure with timestamped as filename""" # create output folder if it not exists @@ -408,11 +415,6 @@ def _save_fig(self, filename, academic_mode): print(f"Creating output folder for plots: {output_folder_name}") os.makedirs(output_folder_name) - if academic_mode: - filetype = "pdf" - else: - filetype = "png" - filepath = f"{output_folder_name}/{filename}.{filetype}" print(f"Saved figure to file: {filepath}") self.figure().savefig(filepath, dpi=300) From 581ccbba5f4350d3aa4c7d9384ceba4fcb798cbc Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Mon, 15 Jan 2024 18:26:20 +0100 Subject: [PATCH 14/51] added gem_controllers --- ...ation_test_classic_controllers_dc_motor.py | 8 +- gem-control | 1 + gem_controllers/README.md | 75 ++++ gem_controllers/__init__.py | 13 + gem_controllers/block_diagrams/__init__.py | 1 + .../block_diagrams/block_diagram.py | 139 +++++++ .../block_diagrams/stage_blocks/__init__.py | 26 ++ .../block_diagrams/stage_blocks/eesm_cc.py | 192 +++++++++ .../block_diagrams/stage_blocks/eesm_ops.py | 181 +++++++++ .../stage_blocks/eesm_output.py | 49 +++ .../stage_blocks/ext_ex_dc_cc.py | 129 ++++++ .../stage_blocks/ext_ex_dc_ops.py | 92 +++++ .../stage_blocks/ext_ex_dc_output.py | 63 +++ .../stage_blocks/perm_ex_dc_cc.py | 78 ++++ .../stage_blocks/perm_ex_dc_ops.py | 39 ++ .../stage_blocks/perm_ex_dc_output.py | 68 ++++ .../stage_blocks/pi_speed_controller.py | 50 +++ .../block_diagrams/stage_blocks/pmsm_cc.py | 187 +++++++++ .../block_diagrams/stage_blocks/pmsm_ops.py | 177 +++++++++ .../stage_blocks/pmsm_output.py | 43 ++ .../block_diagrams/stage_blocks/pmsm_sc.py | 49 +++ .../block_diagrams/stage_blocks/scim_cc.py | 164 ++++++++ .../block_diagrams/stage_blocks/scim_ops.py | 189 +++++++++ .../stage_blocks/scim_output.py | 42 ++ .../block_diagrams/stage_blocks/scim_sc.py | 48 +++ .../stage_blocks/series_dc_cc.py | 78 ++++ .../stage_blocks/series_dc_ops.py | 43 ++ .../stage_blocks/series_dc_output.py | 68 ++++ .../stage_blocks/shunt_dc_cc.py | 79 ++++ .../stage_blocks/shunt_dc_ops.py | 39 ++ .../stage_blocks/shunt_dc_output.py | 68 ++++ .../block_diagrams/stage_blocks/synrm_cc.py | 179 +++++++++ .../stage_blocks/synrm_output.py | 43 ++ gem_controllers/cascaded_controller.py | 21 + gem_controllers/current_controller.py | 20 + gem_controllers/gem_adapter.py | 126 ++++++ gem_controllers/gem_controller.py | 177 +++++++++ gem_controllers/parameter_reader.py | 358 +++++++++++++++++ gem_controllers/pi_current_controller.py | 200 ++++++++++ gem_controllers/pi_speed_controller.py | 147 +++++++ gem_controllers/reference_plotter.py | 62 +++ gem_controllers/stages/__init__.py | 17 + gem_controllers/stages/abc_transformation.py | 79 ++++ gem_controllers/stages/anti_windup.py | 59 +++ .../stages/base_controllers/__init__.py | 7 + .../base_controllers/base_controller.py | 45 +++ .../e_base_controller_task.py | 30 ++ .../stages/base_controllers/i_controller.py | 121 ++++++ .../stages/base_controllers/p_controller.py | 147 +++++++ .../stages/base_controllers/pi_controller.py | 117 ++++++ .../stages/base_controllers/pid_controller.py | 69 ++++ .../three_point_controller.py | 160 ++++++++ .../stages/base_controllers/utils.py | 15 + .../stages/clipping_stages/__init__.py | 4 + .../absolute_clipping_stage.py | 75 ++++ .../stages/clipping_stages/clipping_stage.py | 42 ++ .../combined_clipping_stage.py | 87 ++++ .../clipping_stages/squared_clipping_stage.py | 85 ++++ gem_controllers/stages/cont_output_stage.py | 39 ++ gem_controllers/stages/disc_output_stage.py | 137 +++++++ gem_controllers/stages/emf_feedforward.py | 110 +++++ .../stages/emf_feedforward_eesm.py | 58 +++ gem_controllers/stages/emf_feedforward_ind.py | 55 +++ gem_controllers/stages/input_stage.py | 58 +++ .../operation_point_selection/__init__.py | 9 + .../operation_point_selection/eesm_ops.py | 254 ++++++++++++ .../operation_point_selection/extex_dc_ttc.py | 98 +++++ .../foc_operation_point_selection.py | 150 +++++++ .../operation_point_selection.py | 44 ++ .../operation_point_selection/ops_utils.py | 18 + .../permex_dc_ops.py | 90 +++++ .../operation_point_selection/pmsm_ops.py | 375 ++++++++++++++++++ .../operation_point_selection/scim_ops.py | 184 +++++++++ .../series_dc_ops.py | 49 +++ .../operation_point_selection/shunt_dc_ops.py | 107 +++++ gem_controllers/stages/stage.py | 27 ++ gem_controllers/torque_controller.py | 164 ++++++++ gem_controllers/utils.py | 51 +++ 78 files changed, 7044 insertions(+), 3 deletions(-) create mode 160000 gem-control create mode 100644 gem_controllers/README.md create mode 100644 gem_controllers/__init__.py create mode 100644 gem_controllers/block_diagrams/__init__.py create mode 100644 gem_controllers/block_diagrams/block_diagram.py create mode 100644 gem_controllers/block_diagrams/stage_blocks/__init__.py create mode 100644 gem_controllers/block_diagrams/stage_blocks/eesm_cc.py create mode 100644 gem_controllers/block_diagrams/stage_blocks/eesm_ops.py create mode 100644 gem_controllers/block_diagrams/stage_blocks/eesm_output.py create mode 100644 gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_cc.py create mode 100644 gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_ops.py create mode 100644 gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_output.py create mode 100644 gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_cc.py create mode 100644 gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_ops.py create mode 100644 gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_output.py create mode 100644 gem_controllers/block_diagrams/stage_blocks/pi_speed_controller.py create mode 100644 gem_controllers/block_diagrams/stage_blocks/pmsm_cc.py create mode 100644 gem_controllers/block_diagrams/stage_blocks/pmsm_ops.py create mode 100644 gem_controllers/block_diagrams/stage_blocks/pmsm_output.py create mode 100644 gem_controllers/block_diagrams/stage_blocks/pmsm_sc.py create mode 100644 gem_controllers/block_diagrams/stage_blocks/scim_cc.py create mode 100644 gem_controllers/block_diagrams/stage_blocks/scim_ops.py create mode 100644 gem_controllers/block_diagrams/stage_blocks/scim_output.py create mode 100644 gem_controllers/block_diagrams/stage_blocks/scim_sc.py create mode 100644 gem_controllers/block_diagrams/stage_blocks/series_dc_cc.py create mode 100644 gem_controllers/block_diagrams/stage_blocks/series_dc_ops.py create mode 100644 gem_controllers/block_diagrams/stage_blocks/series_dc_output.py create mode 100644 gem_controllers/block_diagrams/stage_blocks/shunt_dc_cc.py create mode 100644 gem_controllers/block_diagrams/stage_blocks/shunt_dc_ops.py create mode 100644 gem_controllers/block_diagrams/stage_blocks/shunt_dc_output.py create mode 100644 gem_controllers/block_diagrams/stage_blocks/synrm_cc.py create mode 100644 gem_controllers/block_diagrams/stage_blocks/synrm_output.py create mode 100644 gem_controllers/cascaded_controller.py create mode 100644 gem_controllers/current_controller.py create mode 100644 gem_controllers/gem_adapter.py create mode 100644 gem_controllers/gem_controller.py create mode 100644 gem_controllers/parameter_reader.py create mode 100644 gem_controllers/pi_current_controller.py create mode 100644 gem_controllers/pi_speed_controller.py create mode 100644 gem_controllers/reference_plotter.py create mode 100644 gem_controllers/stages/__init__.py create mode 100644 gem_controllers/stages/abc_transformation.py create mode 100644 gem_controllers/stages/anti_windup.py create mode 100644 gem_controllers/stages/base_controllers/__init__.py create mode 100644 gem_controllers/stages/base_controllers/base_controller.py create mode 100644 gem_controllers/stages/base_controllers/e_base_controller_task.py create mode 100644 gem_controllers/stages/base_controllers/i_controller.py create mode 100644 gem_controllers/stages/base_controllers/p_controller.py create mode 100644 gem_controllers/stages/base_controllers/pi_controller.py create mode 100644 gem_controllers/stages/base_controllers/pid_controller.py create mode 100644 gem_controllers/stages/base_controllers/three_point_controller.py create mode 100644 gem_controllers/stages/base_controllers/utils.py create mode 100644 gem_controllers/stages/clipping_stages/__init__.py create mode 100644 gem_controllers/stages/clipping_stages/absolute_clipping_stage.py create mode 100644 gem_controllers/stages/clipping_stages/clipping_stage.py create mode 100644 gem_controllers/stages/clipping_stages/combined_clipping_stage.py create mode 100644 gem_controllers/stages/clipping_stages/squared_clipping_stage.py create mode 100644 gem_controllers/stages/cont_output_stage.py create mode 100644 gem_controllers/stages/disc_output_stage.py create mode 100644 gem_controllers/stages/emf_feedforward.py create mode 100644 gem_controllers/stages/emf_feedforward_eesm.py create mode 100644 gem_controllers/stages/emf_feedforward_ind.py create mode 100644 gem_controllers/stages/input_stage.py create mode 100644 gem_controllers/stages/operation_point_selection/__init__.py create mode 100644 gem_controllers/stages/operation_point_selection/eesm_ops.py create mode 100644 gem_controllers/stages/operation_point_selection/extex_dc_ttc.py create mode 100644 gem_controllers/stages/operation_point_selection/foc_operation_point_selection.py create mode 100644 gem_controllers/stages/operation_point_selection/operation_point_selection.py create mode 100644 gem_controllers/stages/operation_point_selection/ops_utils.py create mode 100644 gem_controllers/stages/operation_point_selection/permex_dc_ops.py create mode 100644 gem_controllers/stages/operation_point_selection/pmsm_ops.py create mode 100644 gem_controllers/stages/operation_point_selection/scim_ops.py create mode 100644 gem_controllers/stages/operation_point_selection/series_dc_ops.py create mode 100644 gem_controllers/stages/operation_point_selection/shunt_dc_ops.py create mode 100644 gem_controllers/stages/stage.py create mode 100644 gem_controllers/torque_controller.py create mode 100644 gem_controllers/utils.py diff --git a/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py b/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py index 7a40def3..1c9980d6 100644 --- a/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py +++ b/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py @@ -1,5 +1,6 @@ from classic_controllers import Controller from externally_referenced_state_plot import ExternallyReferencedStatePlot +from gem_controllers.gem_controller import GemController import gym_electric_motor as gem from gym_electric_motor.envs.motors import MotorType, ControlType, ActionType, Motor from gym_electric_motor.visualization import MotorDashboard, RenderMode @@ -50,7 +51,6 @@ scale_plots=True, reference_generator=ref_generator, ) - motor_dashboard.set_env(env) """ initialize the controller @@ -63,9 +63,11 @@ a (optional) tuning parameter of the symmetrical optimum (default: 4) """ - controller = Controller.make(env, external_ref_plots=external_ref_plots) + # controller = Controller.make(env, external_ref_plots=external_ref_plots) + controller = GemController.make(env, env_id=motor.env_id()) (state, reference), _ = env.reset(seed=1337) + print("state_names: ", motor.state_names()) # simulate the environment for i in range(10001): action = controller.control(state, reference) @@ -83,5 +85,5 @@ env.close() - motor_dashboard.save_to_file(academic_mode=True) + motor_dashboard.save_to_file(filename="integration_test") motor_dashboard.show_and_hold() diff --git a/gem-control b/gem-control new file mode 160000 index 00000000..bc9ac0a3 --- /dev/null +++ b/gem-control @@ -0,0 +1 @@ +Subproject commit bc9ac0a3161fdd256ece6389a5826bb305cb0fda diff --git a/gem_controllers/README.md b/gem_controllers/README.md new file mode 100644 index 00000000..aa25cab3 --- /dev/null +++ b/gem_controllers/README.md @@ -0,0 +1,75 @@ +# GEM Control + +[**Overview**](#Overview) +| [**Getting Started**](#Getting-Started) +| [**Installation**](#Installation) +| [**Citing**](#Citing) + +[Read the paper (IEEE Xplore)](https://ieeexplore.ieee.org/document/10239044) + +## Overview +This repository will contain controllers for the [gym-electric-motor](https://upb-lea.github.io/gym-electric-motor/) environments. +The following motors are supported: + +- DC Motors: + - Permanently Excited DC Motor + - Externally Excited DC Motor + - Series DC Motor + - Shunt DC Motor + +- Three Phase Motors: + - Permanent Magnet Synchronous Motor + - Synchronous Reluctance Motor + - Externally Exited Synchronous Motor + - Squirrel Cage Induction Motor + +## Getting Started + +An easy way to get started with GEM Control is by playing around with the following interactive notebooks in Google +Colaboratory. Most important features of GEM Control as well as application demonstrations are showcased, and give a kickstart +for engineers in industry and academia. + + - [GEM Control cookbook](https://colab.research.google.com/github/upb-lea/gem-control/blob/main/examples/GEM_Control_Cookbook.ipynb) + - [Example Script](https://github.com/upb-lea/gem-control/blob/sphinx_doc/examples/example.py) + +A basic routine is as simple as: +```py +import gym_electric_motor as gem +import gem_controllers as gc + +env_id = 'Cont-TC-PMSM-v0' +env = gem.make(env_id) +c = gc.GemController.make(env, env_id) + +c.control_environment(env, n_steps=10001, render_env=True) +``` + +## Installation + +- Install from GitHub source: + +``` +git clone git@github.com:upb-lea/gem-control.git +cd gem-control +# Then either +python setup.py install +# or alternatively +pip install -e . + +# or alternatively +pip install git+https://github.com/upb-lea/gem-control +``` + +## Citing +Please cite the corresponding whitepaper when using this package: +``` +@INPROCEEDINGS{10.1109/IEMDC55163.2023.10239044, + author={Book, Felix and Traue, Arne and Schenke, Maximilian and Haucke-Korber, Barnabas and Wallscheid, Oliver}, + booktitle={2023 IEEE International Electric Machines & Drives Conference (IEMDC)}, + title={Gym-Electric-Motor (GEM) Control: An Automated Open-Source Controller Design Suite for Drives}, + year={2023}, + volume={}, + number={}, + pages={1-7}, + doi={10.1109/IEMDC55163.2023.10239044}} +``` diff --git a/gem_controllers/__init__.py b/gem_controllers/__init__.py new file mode 100644 index 00000000..8396c95f --- /dev/null +++ b/gem_controllers/__init__.py @@ -0,0 +1,13 @@ +from . import stages +from . import utils +from . import parameter_reader +from .gem_controller import GemController +from .current_controller import CurrentController +from .cascaded_controller import CascadedController +from .pi_current_controller import PICurrentController +from .torque_controller import TorqueController +from .pi_speed_controller import PISpeedController +from .gem_adapter import GymElectricMotorAdapter +from .reference_plotter import ReferencePlotter +from gem_controllers.block_diagrams.block_diagram import build_block_diagram + diff --git a/gem_controllers/block_diagrams/__init__.py b/gem_controllers/block_diagrams/__init__.py new file mode 100644 index 00000000..0a4c8d9a --- /dev/null +++ b/gem_controllers/block_diagrams/__init__.py @@ -0,0 +1 @@ +from .block_diagram import build_block_diagram diff --git a/gem_controllers/block_diagrams/block_diagram.py b/gem_controllers/block_diagrams/block_diagram.py new file mode 100644 index 00000000..d971320f --- /dev/null +++ b/gem_controllers/block_diagrams/block_diagram.py @@ -0,0 +1,139 @@ +from control_block_diagram import ControllerDiagram +from control_block_diagram.components import Point, Connection +from .stage_blocks import ext_ex_dc_cc, ext_ex_dc_ops, ext_ex_dc_output, perm_ex_dc_cc, perm_ex_dc_ops,\ + perm_ex_dc_output, pi_speed_controller, pmsm_cc, pmsm_ops, pmsm_output, scim_cc, scim_ops,\ + scim_output, series_dc_cc, series_dc_ops, series_dc_output, shunt_dc_cc, shunt_dc_ops, shunt_dc_output,\ + pmsm_speed_controller, scim_speed_controller, eesm_ops, eesm_cc, eesm_output, synrm_cc, synrm_output +import gem_controllers as gc + + +def build_block_diagram(controller, env_id, save_block_diagram_as): + """ + Creates a block diagram of the controller + + Args: + controller: GEMController + env_id: GEM environment id + save_block_diagram_as: string or tuple of strings of the data types to be saved + + Returns: + Control Block Diagram + + """ + + # Get the block building function for all stages + motor_type = gc.utils.get_motor_type(env_id) + control_task = gc.utils.get_control_task(env_id) + stages = get_stages(controller.controller, motor_type) + + # Create a new block diagram + doc = ControllerDiagram() + + # Help parameter + + start = Point(0, 0) + inputs = dict() + outputs = dict() + connections = dict() + connect_to_lines = dict() + + # Build the stage blocks + for stage in stages: + start, inputs_, outputs_, connect_to_lines_, connections_ = stage(start, control_task) + inputs = {**inputs, **inputs_} + outputs = {**outputs, **outputs_} + connect_to_lines = {**connect_to_lines, **connect_to_lines_} + connections = {**connections, **connections_} + + # Connect the different blocks + for key in inputs.keys(): + if key in outputs.keys(): + connections[key] = Connection.connect(outputs[key], inputs[key][0], **inputs[key][1]) + + for key in connect_to_lines.keys(): + if key in connections.keys(): + Connection.connect_to_line(connections[key], connect_to_lines[key][0], **connect_to_lines[key][1]) + + # Save the block diagram + if save_block_diagram_as is not None: + save_block_diagram_as = list(save_block_diagram_as) if isinstance(save_block_diagram_as, (tuple, list)) else [ + save_block_diagram_as] + doc.save(*save_block_diagram_as) + + return doc + + +def get_stages(controller, motor_type): + """ + Function to get all block building functions + + Args: + controller: GEMController + motor_type: type of the motor + + Returns: + list of all block building functions + + """ + + motor_check = motor_type in ['PMSM', 'SCIM', 'EESM', 'SynRM', 'ShuntDc', 'SeriesDc', 'PermExDc', 'ExtExDc'] + stages = [] + + # Add the speed controller block function + if isinstance(controller, gc.PISpeedController): + if motor_type in ['PMSM', 'SCIM', 'EESM', 'SynRM']: + stages.append(build_functions[motor_type + '_Speed_Controller']) + else: + stages.append(build_functions['PI_Speed_Controller']) + controller = controller.torque_controller + + # add the torque controller block function + if isinstance(controller, gc.torque_controller.TorqueController): + if motor_check: + stages.append(build_functions[motor_type + '_OPS']) + controller = controller.current_controller + + # add the current controller block function + if isinstance(controller, gc.PICurrentController): + emf_feedforward = controller.emf_feedforward is not None + if motor_check: + stages.append(build_functions[motor_type + '_CC'](emf_feedforward)) + + # add the output block function + stages.append((build_functions[motor_type + '_Output'](controller.emf_feedforward is not None))) + + return stages + + +# dictonary of all block building functions +build_functions = { + 'PI_Speed_Controller': pi_speed_controller, + 'PMSM_Speed_Controller': pmsm_speed_controller, + 'SCIM_Speed_Controller': scim_speed_controller, + 'EESM_Speed_Controller': pmsm_speed_controller, + 'SynRM_Speed_Controller': pmsm_speed_controller, + 'PMSM_OPS': pmsm_ops, + 'SCIM_OPS': scim_ops, + 'EESM_OPS': eesm_ops, + 'SynRM_OPS': pmsm_ops, + 'SeriesDc_OPS': series_dc_ops, + 'ShuntDc_OPS': shunt_dc_ops, + 'PermExDc_OPS': perm_ex_dc_ops, + 'ExtExDc_OPS': ext_ex_dc_ops, + 'PMSM_CC': pmsm_cc, + 'SCIM_CC': scim_cc, + 'EESM_CC': eesm_cc, + 'SynRM_CC': synrm_cc, + 'SeriesDc_CC': series_dc_cc, + 'ShuntDc_CC': shunt_dc_cc, + 'PermExDc_CC': perm_ex_dc_cc, + 'ExtExDc_CC': ext_ex_dc_cc, + 'PMSM_Output': pmsm_output, + 'SCIM_Output': scim_output, + 'EESM_Output': eesm_output, + 'SynRM_Output': synrm_output, + 'SeriesDc_Output': series_dc_output, + 'ShuntDc_Output': shunt_dc_output, + 'PermExDc_Output': perm_ex_dc_output, + 'ExtExDc_Output': ext_ex_dc_output, +} diff --git a/gem_controllers/block_diagrams/stage_blocks/__init__.py b/gem_controllers/block_diagrams/stage_blocks/__init__.py new file mode 100644 index 00000000..5f1ff9ff --- /dev/null +++ b/gem_controllers/block_diagrams/stage_blocks/__init__.py @@ -0,0 +1,26 @@ +from .ext_ex_dc_cc import ext_ex_dc_cc +from .ext_ex_dc_ops import ext_ex_dc_ops +from .ext_ex_dc_output import ext_ex_dc_output +from .perm_ex_dc_cc import perm_ex_dc_cc +from .perm_ex_dc_ops import perm_ex_dc_ops +from .perm_ex_dc_output import perm_ex_dc_output +from .pi_speed_controller import pi_speed_controller +from .pmsm_cc import pmsm_cc +from .pmsm_ops import pmsm_ops +from .pmsm_output import pmsm_output +from .scim_cc import scim_cc +from .scim_ops import scim_ops +from .scim_output import scim_output +from .series_dc_cc import series_dc_cc +from .series_dc_ops import series_dc_ops +from .series_dc_output import series_dc_output +from .shunt_dc_cc import shunt_dc_cc +from .shunt_dc_ops import shunt_dc_ops +from .shunt_dc_output import shunt_dc_output +from .pmsm_sc import pmsm_speed_controller +from .scim_sc import scim_speed_controller +from .eesm_ops import eesm_ops +from .eesm_cc import eesm_cc +from .eesm_output import eesm_output +from .synrm_cc import synrm_cc +from .synrm_output import synrm_output diff --git a/gem_controllers/block_diagrams/stage_blocks/eesm_cc.py b/gem_controllers/block_diagrams/stage_blocks/eesm_cc.py new file mode 100644 index 00000000..4ef5c11c --- /dev/null +++ b/gem_controllers/block_diagrams/stage_blocks/eesm_cc.py @@ -0,0 +1,192 @@ +from control_block_diagram.components import Point, Box, Connection, Circle +from control_block_diagram.predefined_components import DqToAbcTransformation, AbcToAlphaBetaTransformation,\ + AlphaBetaToDqTransformation, Add, PIController, Multiply, Limit + + +def eesm_cc(emf_feedforward): + """ + Args: + emf_feedforward: Boolean whether emf feedforward stage is included + + Returns: + Function to build the EESM current control block + """ + + def cc_eesm(start, control_task): + """ + Function to build the EESM current control block + Args: + start: Starting point of the block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + # space to the previous block + space = 1 if control_task == 'CC' else 1.5 + + # Add blocks for the i_sd and i_sq references and states + add_i_e = Add(start.add(space - 0.5, 1)) + add_i_sd = Add(start.add_x(space + 0.5)) + add_i_sq = Add(start.add(space, -1)) + + if control_task == 'CC': + # Connections at the inputs + Connection.connect(add_i_sd.input_left[0].sub_x(space + 1), add_i_sd.input_left[0], + text=r'$i^{*}_{\mathrm{sd}}$', text_position='start', text_align='left') + Connection.connect(add_i_sq.input_left[0].sub_x(space + 0.5), add_i_sq.input_left[0], + text=r'$i^{*}_{\mathrm{sq}}$', text_position='start', text_align='left') + Connection.connect(add_i_e.input_left[0].sub_x(space), add_i_e.input_left[0], + text=r'$i^{*}_{\mathrm{e}}$', text_position='start', text_align='left') + + # PI Controllers for the d and q component + pi_i_sd = PIController(add_i_sd.position.add_x(1.2), size=(1, 0.8), input_number=1, output_number=1) + pi_i_sq = PIController(Point.merge(pi_i_sd.position, add_i_sq.position), size=(1, 0.8), input_number=1, + output_number=1) + pi_i_e = PIController(Point.merge(pi_i_sd.position, add_i_e.position), size=(1, 0.8), input_number=1, + output_number=1, text='Current\nController') + + # Connection between the add blocks and the PI Controllers + Connection.connect(add_i_sd.output_right, pi_i_sd.input_left) + Connection.connect(add_i_sq.output_right, pi_i_sq.input_left) + Connection.connect(add_i_e.output_right, pi_i_e.input_left) + + # Add blocks for the EMF Feedforward + add_u_sd = Add(pi_i_sd.position.add_x(1.6)) + add_u_sq = Add(pi_i_sq.position.add_x(1.2)) + add_u_e = Add(pi_i_e.position.add_x(2)) + + # Connections between the PI Controllers and the Add blocks + Connection.connect(pi_i_sd.output_right[0], add_u_sd.input_left[0], text=r'$\Delta u^{*}_{\mathrm{sd}}$', + distance_y=0.28) + Connection.connect(pi_i_sq.output_right[0], add_u_sq.input_left[0], text=r'$\Delta u^{*}_{\mathrm{sq}}$', + distance_y=0.4, move_text=(0.25, 0)) + + # Coordinate transformation from DQ to Abc coordinates + dq_to_abc = DqToAbcTransformation(Point.get_mid(add_u_sd.position, add_u_sq.position).add_x(2), + input_space=1) + + # Connections between the add blocks and the coordinate transformation + Connection.connect(add_u_sd.output_right[0], dq_to_abc.input_left[0], text=r'$u^{*}_{\mathrm{sd}}$', + distance_y=0.28, move_text=(0.18, 0)) + Connection.connect(add_u_sq.output_right[0], dq_to_abc.input_left[1], text=r'$u^{*}_{\mathrm{sq}}$', + distance_y=0.28, move_text=(0.38, 0)) + + # Limit of the input voltages + limit = Limit(dq_to_abc.position.add_x(1.8), size=(1, 1.2), inputs=dict(left=3, left_space=0.3), + outputs=dict(right=3, right_space=0.3)) + + limit_e = Limit(Point.merge(limit.position, pi_i_e.position), size=(1, 1), inputs=dict(left=1), + outputs=dict(right=1)) + + # Connection between the coordinate transformation and the limit block + Connection.connect(dq_to_abc.output_right, limit.input_left) + Connection.connect(pi_i_e.output_right, limit_e.input_left) + + # Pulse width modulation block + pwm = Box(limit.position.add_x(2.5), size=(1.5, 1.2), text='PWM', inputs=dict(left=3, left_space=0.3), + outputs=dict(right=3, right_space=0.3)) + + pwm_e = Box(limit_e.position.add_x(2.5), size=(1.5, 1), text='PWM', inputs=dict(left=1), + outputs=dict(right=1)) + + # Connection between the limit and the PWM block + Connection.connect(limit.output_right, pwm.input_left, + text=[ '', '', r'$u^*_{\mathrm{s a,b,c}}$'], distance_y=0.25, text_align='bottom') + Connection.connect(limit_e.output_right, pwm_e.input_left, text=[r'$u^*_{\mathrm{e}}$']) + + # Coordinate transformation from ABC to AlphaBeta coordinates + abc_to_alpha_beta = AbcToAlphaBetaTransformation(pwm.position.sub(1, 3.5), input='right', output='left') + + # Coordinate transformation from AlphaBeta to DQ coordinates + alpha_beta_to_dq = AlphaBetaToDqTransformation( + Point.merge(dq_to_abc.position, abc_to_alpha_beta.position), input='right', output='left') + + emf = Box(Point.merge(add_u_sd.position, Point.get_mid(dq_to_abc.position, alpha_beta_to_dq.position)), + size=(2, 1), inputs=dict(bottom=4), outputs=dict(top=3, top_space=0.4), text='EMF Feedforward') + + Connection.connect(emf.output_top[0], add_u_sq.input_bottom[0]) + Connection.connect(emf.output_top[1], add_u_sd.input_bottom[0]) + Connection.connect(emf.output_top[2], add_u_e.input_bottom[0]) + + # Connections between the coordinate transformation and the add blocks + con_3 = Connection.connect(alpha_beta_to_dq.output_left[0], add_i_sd.input_bottom[0], text=r'-', + text_position='end', + text_align='right', move_text=(-0.2, -0.2)) + con_4 = Connection.connect(alpha_beta_to_dq.output_left[1], add_i_sq.input_bottom[0], text=r'-', + text_position='end', + text_align='right', move_text=(-0.2, -0.2)) + + # Connections between the previous conncetions and the inductances blocks + Connection.connect_to_line(con_3, emf.input_bottom[3]) + Connection.connect_to_line(con_4, emf.input_bottom[2]) + + # Derivation of the angle + box_d_dt = Box(Point.merge(alpha_beta_to_dq.position, start.sub_y(7.42020066645696)).sub_x(1.5), size=(1, 0.8), + text=r'$\mathrm{d} / \mathrm{d}t$', inputs=dict(right=1), outputs=dict(left=1)) + + # Conncetion between the derivation and multiplication block + con_omega = Connection.connect(box_d_dt.output_left[0], emf.input_bottom[0], space_y=1, + text=r'$\omega_{\mathrm{el}}$', move_text=(0, 1.7), text_align='left', + distance_x=0.3) + + if control_task == 'SC': + # Connector at the previous connection + Circle(con_omega.points[1], radius=0.05, fill='black') + + # Conncetion between the coordinate transformations + Connection.connect(abc_to_alpha_beta.output, alpha_beta_to_dq.input_right, + text=[r'$i_{\mathrm{s} \upalpha}$', r'$i_{\mathrm{s} \upbeta}$']) + + # Add block for the advanced angle + add = Add(Point.get_mid(dq_to_abc.position, alpha_beta_to_dq.position), inputs=dict(bottom=1, right=1), + outputs=dict(top=1)) + + # Connections of the add block + Connection.connect(alpha_beta_to_dq.output_top, add.input_bottom, text=r'$\varepsilon_{\mathrm{el}}$', + text_align='right', move_text=(0, -0.1)) + Connection.connect(add.output_top, dq_to_abc.input_bottom) + + # Calculate the advanced angle + box_t_a = Box(add.position.add_x(1.5), size=(1, 0.8), text=r'$1.5 T_{\mathrm{s}}$', inputs=dict(right=1), + outputs=dict(left=1)) + + # Connections of the advanced angle block + Connection.connect(box_t_a.output, add.input_right, text=r'$\Delta \varepsilon$') + Connection.connect(box_t_a.input[0].add_x(0.5), box_t_a.input[0], text=r'$\omega_{\mathrm{el}}$', + text_position='start', text_align='right', distance_x=0.3) + + start = pwm.position # starting point of the next block + + # Inputs of the stage + inputs = dict(i_d_ref=[add_i_sd.input_left[0], dict(text=r'$i^{*}_{\mathrm{sd}}$', distance_y=0.25, + move_text=(0.3, 0), text_position='start', + text_aglin='top')], + i_q_ref=[add_i_sq.input_left[0], dict(text=r'$i^{*}_{\mathrm{sq}}$', distance_y=0.25, + move_text=(0.3, 0), text_position='start', + text_aglin='top')], + i_e_ref=[add_i_e.input_left[0], dict(text=r'$i^{*}_{\mathrm{e}}$', distance_y=0.25, + move_text=(0.3, 0), text_position='start', + text_aglin='top')], + epsilon=[box_d_dt.input_right[0], dict()], + i_e=[add_i_e.input_bottom[0], dict(text=r'-', text_position='end', text_align='right', + move_text=(-0.2, -0.2))]) + + # Outputs of the stage + outputs = dict(S=pwm.output_right, omega=con_omega.points[1], S_e=pwm_e.output_right[0]) + + # Connections to other lines + connect_to_line = dict(epsilon=[alpha_beta_to_dq.input_bottom[0], dict(text=r'$\varepsilon_{\mathrm{el}}$', + text_position='middle', + text_align='right')], + i=[abc_to_alpha_beta.input_right, dict(radius=0.1, fill=False, + text=[r'$\mathbf{i}_{\mathrm{s a,b,c}}$', '', + ''])], + i_e=[emf.input_bottom[1], dict(radius=0.05, fill='black')]) + connections = dict() # Connections + + return start, inputs, outputs, connect_to_line, connections + + return cc_eesm + diff --git a/gem_controllers/block_diagrams/stage_blocks/eesm_ops.py b/gem_controllers/block_diagrams/stage_blocks/eesm_ops.py new file mode 100644 index 00000000..b156da52 --- /dev/null +++ b/gem_controllers/block_diagrams/stage_blocks/eesm_ops.py @@ -0,0 +1,181 @@ +from control_block_diagram.components import Box, Connection, Text, Point, Circle, Path +from control_block_diagram.predefined_components import Add, Divide, IController, Limit + + +class PsiOptBox(Box): + """Optimal flux block""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # border in x- and y-direction + bx = 0.1 + by = 0.15 + + # Coordinate system + Connection.connect(self.bottom_left.add(self._size_x * bx, self._size_y * by), + self.bottom_right.add(-self._size_x * bx, self._size_y * by)) + Connection.connect(self.bottom.add_y(self._size_y * by), self.top.sub_y(self._size_y * by)) + + # Graph in the coordinate system + Path([self.top_left.add(self._size_x * bx, -self._size_y * (0.1 + by)), + self.bottom.add_y(self._size_y * (0.2 + by)), + self.top_right.sub(self._size_x * bx, self._size_y * (0.1 + by))], + angles=[{'in': 180, 'out': 0}, {'in': 180, 'out': 0}], arrow=False) + + +class TMaxPsiBox(Box): + """Maximum torque block""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # border in x- and y-direction + bx = 0.1 * self._size_x + by = 0.15 * self._size_y + scale = 1.5 + + # Coordinate system + Connection.connect(self.bottom_left.add(bx, by), self.top_left.add(bx, -by)) + Connection.connect(self.left.add_x(bx), self.right.sub_x(bx)) + + # Graph in the coordinate system + Path([self.left.add_x(bx), self.top_right.sub(scale * bx, scale * by)], arrow=False, + angles=[{'in': 180, 'out': 35}]) + Path([self.left.add_x(bx), self.bottom_right.add(-scale * bx, scale * by)], arrow=False, + angles=[{'in': 180, 'out': -35}]) + + +def eesm_ops(start, control_task): + """ + Function to build the EESM operation point selection block + Args: + start: Starting point of the block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + if control_task == 'TC': + # Connection at the input + Connection.connect(start, start.add_x(1), text='$T^{*}$', text_position='start', text_align='left', arrow=False) + + # Limit the torque reference + box_limit = Limit(start.add_x(6), inputs=dict(left=1, bottom=1), size=(1, 1)) + + # Calculate the optimal flux + box_psi_opt = PsiOptBox(start.add(2, -1.7), size=(1.2, 1)) + Text(position=box_psi_opt.top.add_y(0.25), text=r'$\Psi^{*}_{\mathrm{opt}}(T^{*})$') + + # Minimum block + box_min = Box(box_psi_opt.position.add(1.5, -1.3), inputs=dict(left=2, left_space=0.5), size=(0.8, 1), text='min') + + # Calculate the maximum torque + box_t_max = TMaxPsiBox(box_psi_opt.position.add_x(3.1), size=(1.2, 1)) + Text(position=box_t_max.top.add_y(0.25), text=r'$T_{\mathrm{max}}(\Psi)$') + + # Connect the maximum torque and limit block + con_torque = Connection.connect(start.add_x(1), box_limit.input_left[0]) + + # Connection between the input and the optimum flux block + Connection.connect(start.add_x(1), box_psi_opt.input_left[0], start_direction='south') + Circle(start.add_x(1), radius=0.05, fill='black') + + # Connection between the optimum flux and minimum block + Connection.connect(box_psi_opt.output_right[0], box_min.input_left[0]) + + # Conncetion between the minimum and maximum torque block + Connection.connect(box_min.output_right[0].add_x(0.3), box_t_max.input_left[0], start_direction='north') + + # Circle at the connection + Circle(box_min.output_right[0].add_x(0.3), radius=0.05, fill='black') + + # Conncetion between the maximum torque and torque limit block + Connection.connect(box_t_max.output_right[0], box_limit.input_bottom[0]) + + # Optimal operation point function block + box_f_psi_t = Box(box_limit.position.add(2.2, -1.5), size=(1.3, 3.5), inputs=dict(left=2, left_space=3), + outputs=dict(right=3, right_space=1), text=r'\textbf{f}($\Psi$, $T$)') + + # Conncetions to the optimal opertion point function block + Connection.connect(box_min.output_right[0], box_f_psi_t.input_left[1], text=r'$\Psi^{*}_{\mathrm{lim}}$') + Connection.connect(box_limit.output_right[0], box_f_psi_t.input_left[0], text=r'$T^{*}_{\mathrm{lim}}$') + + # Moulation Controller + + # Add the actual flux and delta flux + add_psi = Add(box_min.input_left[1].sub_x(1)) + + # Connection between the add and minimum block + Connection.connect(add_psi.output_right[0], box_min.input_left[1], text=r'$\Psi_{\mathrm{lim}}$', text_align='top', + distance_y=0.25) + + # Limit the delta flux + limit_modulation = Limit(add_psi.input_left[0].sub_x(1.5), size=(1, 1)) + + # I Controller of the modulation controller + i_controller = IController(limit_modulation.input_left[0].sub_x(1), size=(1.2, 1), text='Modulation\nController') + + # Connections of the limit block + Connection.connect(i_controller.output_right, limit_modulation.input_left) + Connection.connect(limit_modulation.output_right[0], add_psi.input_left[0], text=r'$\Delta \Psi$', distance_y=0.25) + + # Add block of the modulation controller + add_a = Add(i_controller.position.sub_x(1.5)) + + # Maximum modulation + box_a_max = Box(add_a.input_left[0].sub_x(1.3), size=(1.5, 0.8), text=r'$a_{\mathrm{max}} \cdot k$') + + # Building the absolute modulation + box_abs = Box(add_a.position.sub_y(1.2), size=(0.8, 0.8), text=r'|\textbf{x}|', inputs=dict(bottom=1), + outputs=dict(top=1)) + + # Conncetions of the add block + Connection.connect(box_a_max.output_right[0], add_a.input_left[0], text='$a^{*}$') + Connection.connect(add_a.output_right[0], i_controller.input_left[0]) + con_a = Connection.connect(box_abs.output_top[0], add_a.input_bottom[0], text='$-$', text_align='right', + text_position='end', move_text=(-0.1, -0.2)) + + # Additional text at the connection + Text(position=Point.get_mid(*con_a.points).sub_x(0.25), text='$a$') + + # divide the actual voltage by the dc voltage + div_a = Divide(box_abs.input_bottom[0].sub_y(1), size=(1, 0.5), inputs='bottom', input_space=0.5) + + # Conncetions of the divide block + Connection.connect(div_a.input_bottom[0].sub_y(0.7), div_a.input_bottom[0], text=r'$\mathbf{u^{*}_{\mathrm{dq}}}$', + text_position='start', text_align='bottom') + Connection.connect(div_a.input_bottom[1].sub_y(0.7), div_a.input_bottom[1], + text=r'$\frac{u_{\mathrm{\mbox{\fontsize{3}{4}\selectfont DC}}}}{2}$', text_position='start', + text_align='bottom') + Connection.connect(div_a.output_top[0], box_abs.input_bottom[0]) + + # Divide the dc voltage by the electrical speed + div_psi = Divide(add_psi.input_bottom[0].sub_y(2), size=(1, 0.5), inputs='bottom', input_space=0.5) + + # Connections of the divide block + Connection.connect(div_psi.output_top[0], add_psi.input_bottom[0], text=r'$\Psi_{\mathrm{max}}$', + text_align='right', distance_x=0.5) + Connection.connect(div_psi.input_bottom[0].sub_y(0.7), div_psi.input_bottom[0], + text=r'$\frac{u_{\mathrm{\mbox{\fontsize{3}{4}\selectfont DC}}}}{\sqrt{3}}$', + text_position='start', text_align='bottom') + + # Limit of the current reference values + limit = Limit(Point.get_mid(box_f_psi_t.output_right[1], box_f_psi_t.output_right[2]).add_x(1), size=(1, 1.5), + inputs=dict(left=2, left_space=1), outputs=dict(right=2, right_space=1)) + limit_e = Limit(box_f_psi_t.output_right[0].add_x(1), size=(1, 1), inputs=dict(left=1), outputs=dict(right=1)) + + # Connections between the optimal opertion point function and limit block + Connection.connect(box_f_psi_t.output_right[0], limit_e.input_left[0]) + Connection.connect(box_f_psi_t.output_right[1], limit.input_left[0]) + Connection.connect(box_f_psi_t.output_right[2], limit.input_left[1]) + + # Inputs of the stage + inputs = dict(t_ref=[con_torque.start, dict(arrow=False, text=r'$T^{*}$')], + omega=[div_psi.input_bottom[1], dict(text=r'$\omega_{\mathrm{el}}$', move_text=(3, 0))]) + # Outputs of the stage + outputs = dict(i_d_ref=limit.output_right[0], i_q_ref=limit.output_right[1], i_e_ref=limit_e.output_right[0]) + connect_to_lines = dict() # Connections to other lines + connections = dict() # Connections of the stage + start = limit.output_right[0] # Starting point of the next stage + + return start, inputs, outputs, connect_to_lines, connections diff --git a/gem_controllers/block_diagrams/stage_blocks/eesm_output.py b/gem_controllers/block_diagrams/stage_blocks/eesm_output.py new file mode 100644 index 00000000..699b034d --- /dev/null +++ b/gem_controllers/block_diagrams/stage_blocks/eesm_output.py @@ -0,0 +1,49 @@ +from control_block_diagram.components import Connection, Circle, Text +from control_block_diagram.predefined_components import EESM, DcConverter + + +def eesm_output(emf_feedforward): + """ + Args: + emf_feedforward: Boolean whether emf feedforward stage is included + + Returns: + Function to build the EESM output block + """ + + def _eesm_output(start, control_task): + """ + Function to build the EESM output block + Args: + start: Starting point of the block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + # Converter with DC input voltage + converter = DcConverter(start.add(2.7, 0.15), input_number=4, input_space=0.3, output_number=5) + + # PMSM block + eesm = EESM(converter.position.sub_y(6), size=1.3, input='top') + + # Connection between the converter and the motor block + con_1 = Connection.connect(converter.output_bottom, eesm.input_top, arrow=False) + output_i_e = Circle(con_1[3].start.sub_y(4.2), radius=0.1, outputs=dict(left=1), fill=None) + con_2 = Connection.connect(output_i_e.output_left[0], output_i_e.output_left[0].sub_x(11), arrow=False) + Text(r'$i_{\mathrm{e}}$', output_i_e.output_left[0].add(-2, 0.25)) + + start = eesm.position # starting point of the next block + # Inputs of the stage + inputs = dict(S=[converter.input_left[1:], dict(text=['', '', r'$\mathbf{S}_{\mathrm{a,b,c}}$'], + distance_y=0.25, text_align='bottom')], + S_e=[converter.input_left[0], dict(text=r'$\mathbf{S}_{\mathrm{e}}$', + distance_y=0.25, text_position='start', move_text=(0.4, 0))]) + outputs = dict(epsilon=eesm.output_left[0], i_e=con_2.end) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict(i=con_1, i_e=con_2) # Connections + + return start, inputs, outputs, connect_to_lines, connections + + return _eesm_output diff --git a/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_cc.py b/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_cc.py new file mode 100644 index 00000000..fa6a64ba --- /dev/null +++ b/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_cc.py @@ -0,0 +1,129 @@ +from control_block_diagram.components import Box, Connection, Point +from control_block_diagram.predefined_components import Add, PIController, Limit + + +def ext_ex_dc_cc(emf_feedforward): + """ + Args: + emf_feedforward: Boolean whether emf feedforward stage is included + + Returns: + Function to build the Externally Excited DC current control block + """ + + def cc_ext_ex_dc(start, control_task): + """ + Function to build the Externally Excited DC current control block + Args: + start: Starting point of the block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + # space to the previous block + space = 1 + + # Add block for the e-current reference and state + add_i_e = Add(start.add_x(space + 0.5)) + + # starting point of the i_a path + start_i_a = start.sub_y(1.2) + + # Add block for the e-current reference and state + add_i_a = Add(start_i_a.add_x(space)) + + if control_task == 'CC': + # Connection at the input of the i_e path + Connection.connect(start, add_i_e.input_left[0], text=r'$i^{*}_{\mathrm{e}}$', text_align='left', + text_position='start') + # Connection at the input of the i_a path + Connection.connect(start_i_a, add_i_a.input_left[0], text=r'$i^{*}_{\mathrm{a}}$', text_align='left', + text_position='start') + + # PI Current Controller of the i_e path + pi_i_e = PIController(add_i_e.position.add_x(1.5), text='Current\nController') + + # Connection between the PI Controller and the add block + Connection.connect(add_i_e.output_right, pi_i_e.input_left) + + # PI Current Controller of the i_a path + pi_i_a = PIController(add_i_a.position.add_x(2)) + + # Connection between the PI Controller and the add block + Connection.connect(add_i_a.output_right, pi_i_a.input_left) + + # Inputs of the stage + inputs = dict(i_e_ref=[add_i_e.input_left[0], dict(text=r'$i^{*}_{\mathrm{e}}$', move_text=(-0.1, 0.1))], + i_a_ref=[add_i_a.input_left[0], dict(text=r'$i^{*}_{\mathrm{a}}$', move_text=(-0.1, 0.1))], + i_a=[add_i_a.input_bottom[0], dict(text=r'-', move_text=(-0.2, -0.2), text_position='end', + text_align='right')], + i_e=[add_i_e.input_bottom[0], dict(text=r'-', move_text=(-0.2, -0.2), text_position='end', + text_align='right')]) + connect_to_lines = dict() # Connections to other lines + + if emf_feedforward: + # Add block of the emf feedforward + add_psi = Add(pi_i_a.position.add_x(2)) + + # Connection between the PI Controller and the add block + Connection.connect(pi_i_a.output_right, add_psi.input_left, text=r'$\Delta u_{\mathrm{a}}$', + distance_y=0.25) + + # Multiplication with the flux + box_psi = Box(add_psi.position.sub_y(2), size=(1, 0.8), text=r"$L'_{\mathrm{e}} i_{\mathrm{e}}$", + inputs=dict(bottom=1), outputs=dict(top=1)) + + # Connection between the multiplication and the add block + Connection.connect(box_psi.output_top, add_psi.input_bottom, text=r'$u_0$', text_position='end', + text_align='right', move_text=(-0.05, -0.25)) + + # Limit of the i_a current + limit_a = Limit(add_psi.position.add_x(1.5), size=(1, 1)) + + # Connection between the add and limit block + Connection.connect(add_psi.output_right, limit_a.input_left) + + # Pulse width modulation block of the i_a path + pwm_a = Box(limit_a.output_right[0].add_x(1.5), size=(1.2, 0.8), text='PWM') + + # Connection between the a-limit and the a-pwm + Connection.connect(limit_a.output_right, pwm_a.input_left, text=r'$u^{*}_{\mathrm{a}}$', distance_y=0.25) + + # Set the input of the emf feedforward + if control_task in ['CC', 'TC']: + inputs['omega'] = [box_psi.input_bottom[0], dict()] + elif control_task == 'SC': + connect_to_lines['omega'] = [box_psi.input_bottom[0], dict(section=0)] + else: + # Limit of the i_a current + limit_a = Limit(pi_i_a.output_right[0].add_x(1), size=(1, 1)) + + # Connection between the add and limit block + Connection.connect(pi_i_a.output_right, limit_a.input_left) + + # Pulse width modulation block of the i_a path + pwm_a = Box(limit_a.position.add_x(2), size=(1.2, 0.8), text='PWM') + + # Connection between the a-limit and the a-pwm + Connection.connect(limit_a.output_right, pwm_a.input_left, text=r'$u^{*}_{\mathrm{a}}$', distance_y=0.25) + + # Limit of the i_a current + limit_e = Limit(Point.merge(limit_a.position, pi_i_e.position), size=(1, 1)) + + # Connection between the PI Controller and the limtit block + Connection.connect(pi_i_e.output_right, limit_e.input_left) + + # Pulse width modulation block of the i_e path + pwm_e = Box(Point.merge(pwm_a.position, pi_i_e.position), size=(1.2, 0.8), text='PWM') + + # Connetion between the e-limit and e-pwm block + Connection.connect(limit_e.output_right, pwm_e.input_left, text=r'$u^{*}_{\mathrm{e}}$', distance_y=0.25) + + start = pwm_e.position # starting point of the next block + outputs = dict(u_e=pwm_e.output_right[0], u_a=pwm_a.output_right[0]) # Outputs of the stage + connections = dict() # Connections + + return start, inputs, outputs, connect_to_lines, connections + return cc_ext_ex_dc diff --git a/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_ops.py b/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_ops.py new file mode 100644 index 00000000..9f5eb634 --- /dev/null +++ b/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_ops.py @@ -0,0 +1,92 @@ +from control_block_diagram.components import Box, Connection, Circle, Point +from control_block_diagram.predefined_components import Multiply, Divide, Limit + + +def ext_ex_dc_ops(start, control_task): + """ + Function to build the Externally Excited DC operation point selection block + Args: + start: Starting point of the block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + # space to the previous block + inp = start.add_x(1) if control_task == 'TC' else start.add_x(1.5) + + # Connector at the input + Circle(inp, radius=0.05, fill='black') + + if control_task == 'TC': + # Connetion at the input + Connection.connect(start, inp, text=r'$T^{*}$', text_align='left', + text_position='start', arrow=False) + + # Calculate absolute value + box_abs = Box(inp.add(1, 0.6), size=(0.8, 0.8), text=r'|x|') + + # Connect the input and the previous block + Connection.connect(inp, box_abs.input_left[0], start_direction='north') + + # Multiply by motor parameters + multiply = Multiply(box_abs.position.add_x(1.5), size=(0.8, 0.8), inputs=dict(left=1, top=1)) + + # Connect the previous blocks + Connection.connect(box_abs.output_right, multiply.input_left) + + # Block of motor parameters + box_rl = Box(multiply.position.add_y(1.2), size=(1.5, 0.8), + text=r"$\sqrt{\frac{R_{\mathrm{a}}}{R_{\mathrm{e}}}}L'_{\mathrm{e}}$", outputs=dict(bottom=1)) + + # Connect the motor parameters block with the multiply block + Connection.connect(box_rl.output_bottom, multiply.input_top) + + # Square root of the product + box_sqrt = Box(multiply.position.add_x(1.5), size=(0.8, 0.8), text=r'$\sqrt{x}$') + + # Connection between multiplication and squareroot + Connection.connect(multiply.output_right, box_sqrt.input_left) + + # Divide the torque reference by l_e prime + divide_1 = Divide(multiply.position.sub_y(1.5), size=(0.8, 1.2), input_space=0.6) + + # Conncet the input and division block + Connection.connect(inp, divide_1.input_left[0], start_direction='south') + + # L_e prime block + box_le = Box(divide_1.input_left[1].sub_x(1), size=(0.8, 0.8), text=r"$L'_{\mathrm{e}}$") + + # Conncet the l_e prime and division blcok + Connection.connect(box_le.output_right[0], divide_1.input_left[1]) + + # Divide the reference for i_e + divide_2 = Divide(divide_1.output_right[0].add(3, 0.3), size=(0.8, 1.2), input_space=0.6) + + # Connection between the two division blocks + Connection.connect(divide_1.output_right[0], divide_2.input_left[1]) + + # Limit the i_e current + limit_i_e = Limit(box_sqrt.output_right[0].add_x(3), size=(1, 1)) + + # Connect between the squareroot and limit block + con_sqrt = Connection.connect(box_sqrt.output_right[0], limit_i_e.input_left[0]) + + # Connect the previous line to the second division block + Connection.connect_to_line(con_sqrt, divide_2.input_left[0].sub_x(0.5), arrow=False) + Connection.connect(divide_2.input_left[0].sub_x(0.5), divide_2.input_left[0]) + + # Limit the i_a current + limit_i_a = Limit(Point.merge(limit_i_e.position, divide_2.position), size=(1, 1)) + + # Connection between the division and limit block + Connection.connect(divide_2.output_right, limit_i_a.input_left) + + inputs = dict(t_ref=[inp, dict(text=r'$T^{*}$', arrow=False)]) # starting point of the next block + outputs = dict(i_e_ref=limit_i_e.output_right[0], i_a_ref=limit_i_a.output_right[0]) # Inputs of the stage + connect_to_lines = dict() # Outputs of the stage + connections = dict() # Connections to other lines + start = limit_i_e.output_right[0] # Connections + + return start, inputs, outputs, connect_to_lines, connections diff --git a/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_output.py b/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_output.py new file mode 100644 index 00000000..2a1a921a --- /dev/null +++ b/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_output.py @@ -0,0 +1,63 @@ +from control_block_diagram.components import Connection +from control_block_diagram.predefined_components import DcConverter, DcExtExMotor + + +def ext_ex_dc_output(emf_feedforward): + """ + Args: + emf_feedforward: Boolean whether emf feedforward stage is included + + Returns: + Function to build the Externally Excited DC output block + """ + + def _ext_ex_dc_output(start, control_task): + """ + Function to build the Externally Excited DC output block + Args: + start: Starting point of the block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + # Converter with DC input voltage and four outputs + converter = DcConverter(start.add(2.5, -0.6), size=1.7, input_number=2, input_space=1.2, output_number=4, + output_space=0.25) + + # Ext Ex DC motor block + dc_ext_ex = DcExtExMotor(converter.position.sub_y(3), size=1.2, input='top', output='left') + + # Connections between converter and motor block + con_conv_a = Connection.connect(converter.output_bottom[0], dc_ext_ex.input_top[0], arrow=False) + Connection.connect(converter.output_bottom[1], dc_ext_ex.input_top[1], arrow=False) + con_conv_e = Connection.connect(converter.output_bottom[2], dc_ext_ex.input_top[2], arrow=False) + Connection.connect(converter.output_bottom[3], dc_ext_ex.input_top[3], arrow=False) + + # Connection to the i_e output + con_e = Connection.connect_to_line(con_conv_e, start.sub_y(2), arrow=False, radius=0.1, fill=False, + text=r'$i_{\mathrm{e}}$', distance_y=0.25) + + # Connection to the i_a output + con_a = Connection.connect_to_line(con_conv_a, start.sub_y(2.5), arrow=False, radius=0.1, fill=False, + text=r'$i_{\mathrm{a}}$', distance_y=0.25, text_align='bottom', + move_text=(0.25, 0)) + + start = converter.position # starting point of the next block + # Inputs of the stage + inputs = dict(u_e=[converter.input_left[0], dict(text=r'$\mathrm{S_e}$', distance_y=0.25)], + u_a=[converter.input_left[1], dict(text=r'$\mathrm{S_a}$', distance_y=0.25)]) + outputs = dict(i_e=con_e.end, i_a=con_a.end) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Connections + + if emf_feedforward or control_task in ['SC']: + # Connection between the motor output and the omega output of the stage + con_omega = Connection.connect(dc_ext_ex.output_left[0].sub_x(2), dc_ext_ex.output_left[0], + text=r'$\omega_{\mathrm{me}}$', arrow=False) + # Add the omega output + outputs['omega'] = con_omega.end + + return start, inputs, outputs, connect_to_lines, connections + return _ext_ex_dc_output diff --git a/gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_cc.py b/gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_cc.py new file mode 100644 index 00000000..07721f15 --- /dev/null +++ b/gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_cc.py @@ -0,0 +1,78 @@ +from control_block_diagram.components import Box, Connection +from control_block_diagram.predefined_components import Add, PIController + + +def perm_ex_dc_cc(emf_feedforward): + """ + Args: + emf_feedforward: Boolean whether emf feedforward stage is included + + Returns: + Function to build the Perm Ex DC current control block + """ + + def cc_perm_ex_dc(start, control_task): + """ + Function to build the Perm Ex DC current control block + Args: + start: Starting point of the block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + # space to the previous block + space = 1 if control_task == 'CC' else 1.5 + + # Add block for the current reference and state + add_current = Add(start.add_x(space)) + + if control_task == 'CC': + # Connection at the input + Connection.connect(start, add_current.input_left[0], text=r'$i^{*}$', text_align='left', + text_position='start') + + # PI Current Controller + pi_current = PIController(add_current.position.add_x(1.5), text='Current\nController') + + # Connection between the add block and pi controller + Connection.connect(add_current.output_right, pi_current.input_left) + + start = pi_current.position # starting point of the next block + # Inputs of the stage + inputs = dict(i_ref=[add_current.input_left[0], dict(text=r'$i^{*}$')], i=[add_current.input_bottom[0], + dict(text=r'-', move_text=(-0.2, -0.2), text_position='end', text_align='right')]) + outputs = dict(u=pi_current.output_right[0]) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Connections + + if emf_feedforward: + # Add block of the emf feedforward + add_emf = Add(pi_current.position.add_x(2)) + + # Connection between pi controller and add block + Connection.connect(pi_current.output_right, add_emf.input_left, text=r'$\Delta u^{*}$') + + # Multiplication with the flux + box_psi = Box(add_emf.position.sub_y(2.5), size=(0.8, 0.8), text=r"$\Psi'_{\mathrm{e}}$", inputs=dict(bottom=1), + outputs=dict(top=1)) + + # Connection between the multiplication and add block + Connection.connect(box_psi.output_top, add_emf.input_bottom, text=r'$u^{0}$', text_position='end', + text_align='right', move_text=(-0.1, -0.2)) + + # Set the input of the emf feedforward + if control_task in ['SC']: + connect_to_lines['omega'] = [box_psi.input_bottom[0], dict(section=0)] + elif control_task in ['CC', 'TC']: + inputs['omega'] = [box_psi.input_bottom[0], dict()] + + # Set the output of the emf feedforward + outputs['u'] = add_emf.output_right[0] + + # Update the position of the next block + start = add_emf.position + + return start, inputs, outputs, connect_to_lines, connections + return cc_perm_ex_dc diff --git a/gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_ops.py b/gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_ops.py new file mode 100644 index 00000000..a21761ab --- /dev/null +++ b/gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_ops.py @@ -0,0 +1,39 @@ +from control_block_diagram.components import Box, Connection +from control_block_diagram.predefined_components import Limit + + +def perm_ex_dc_ops(start, control_task): + """ + Function to build the Perm Ex DC operation point selection block + Args: + start: Starting point of the block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + # space to the previous block + space = 1 if control_task == 'TC' else 2.2 + + # Calculation of the current reference + box_torque = Box(start.add_x(space), size=(0.8, 0.8), text=r"$\frac{1}{\Psi'_{\mathrm{e}}}$") + + # Limit of the current reference + limit = Limit(box_torque.output_right[0].add_x(1), size=(1, 1)) + + # Connection between the calculation and limit block + Connection.connect(box_torque.output_right, limit.input_left) + + if control_task == 'TC': + # Connection at the input + Connection.connect(start, box_torque.input_left[0], text=r'$T^{*}$', text_align='left', + text_position='start') + + start = limit.position # starting point of the next block + inputs = dict(t_ref=[box_torque.input_left[0], dict(text=r'$T^{*}$')]) # Inputs of the stage + outputs = dict(i_ref=limit.output_right[0]) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Connections + + return start, inputs, outputs, connect_to_lines, connections diff --git a/gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_output.py b/gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_output.py new file mode 100644 index 00000000..2cd08bef --- /dev/null +++ b/gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_output.py @@ -0,0 +1,68 @@ +from control_block_diagram.components import Box, Connection +from control_block_diagram.predefined_components import DcPermExMotor, DcConverter, Limit + + +def perm_ex_dc_output(emf_feedforward): + """ + Args: + emf_feedforward: Boolean whether emf feedforward stage is included + + Returns: + Function to build the Perm Ex DC output block + """ + + def _perm_ex_dc_output(start, control_task): + """ + Function to build the Perm Ex DC output block + Args: + start: Starting point of the block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + # space to the previous block + space = 1.5 if emf_feedforward else 2 + + # voltage limit block + limit = Limit(start.add_x(space), size=(1, 1)) + + # pulse width modulation block + pwm = Box(limit.output_right[0].add_x(1.5), size=(1, 0.8), text='PWM') + + # connection between limit and pwm block + Connection.connect(limit.output_right, pwm.input_left) + + # Converter with DC input voltage + converter = DcConverter(pwm.position.add_x(2), size=1.2, input_number=1) + + # Connection between pwm and converter block + Connection.connect(pwm.output_right, converter.input_left, text='$S$') + + # Perm Ex DC motor block + dc_perm_ex = DcPermExMotor(converter.position.sub_y(3), size=1.2, input='top', output='left') + + # Connections between converter and motor block + conv_motor = Connection.connect(converter.output_bottom, dc_perm_ex.input_top, text=['', r'$i$'], + text_align='right', arrow=False) + + # Connection between previous connection and current output + con_i = Connection.connect_to_line(conv_motor[0], pwm.position.sub_y(1.5), text=r'$i$', arrow=False, fill=False, + radius=0.1) + + start = converter.position # starting point of the next block + inputs = dict(u=[limit.input_left[0], dict(text=r'$u^{*}$')]) # Inputs of the stage + outputs = dict(i=con_i.end) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Connections + + if emf_feedforward or control_task in ['SC']: + # Connection between the motor output and the omega output of the stage + con_omega = Connection.connect(dc_perm_ex.output_left[0].sub_x(2), dc_perm_ex.output_left[0], + text=r'$\omega_{\mathrm{me}}$', arrow=False) + # Add the omega output + outputs['omega'] = con_omega.end + + return start, inputs, outputs, connect_to_lines, connections + return _perm_ex_dc_output diff --git a/gem_controllers/block_diagrams/stage_blocks/pi_speed_controller.py b/gem_controllers/block_diagrams/stage_blocks/pi_speed_controller.py new file mode 100644 index 00000000..4c0ca4b1 --- /dev/null +++ b/gem_controllers/block_diagrams/stage_blocks/pi_speed_controller.py @@ -0,0 +1,50 @@ +from control_block_diagram.components import Box, Connection +from control_block_diagram.predefined_components import Add, PIController, Limit + + +def pi_speed_controller(start, control_task): + """ + Function to build the speed controller block + Args: + start: Starting point of the block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + # space to the previous block + space = 1 if control_task == 'SC' else 1.5 + + # Add block for the speed reference and state + add_omega = Add(start.add_x(space)) + + if control_task == 'SC': + # Connection at the input + Connection.connect(start, add_omega.input_left[0], text=r'$\omega^{*}$', text_align='left', + text_position='start') + + # PI Speed Controller + pi_omega = PIController(add_omega.position.add_x(1.5), text='Speed\nController') + + # Connection between the add block and pi controller + Connection.connect(add_omega.output_right, pi_omega.input_left) + + # Limit of the torque reference + limit = Limit(pi_omega.position.add_x(2), size=(1, 1)) + + # Connection between the pi controller and the limit block + Connection.connect(pi_omega.output_right, limit.input_left) + + start = limit.position # starting point of the next block + # Inputs of the stage + inputs = dict(omega_ref=[add_omega.input_left[0], dict(text=r'$\omega^{*}$')], omega=[add_omega.input_bottom[0], + dict(text=r'-', + move_text=(-0.2, -0.2), + text_position='end', + text_align='right')]) + outputs = dict(t_ref=limit.output_right[0]) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Connections + + return start, inputs, outputs, connect_to_lines, connections diff --git a/gem_controllers/block_diagrams/stage_blocks/pmsm_cc.py b/gem_controllers/block_diagrams/stage_blocks/pmsm_cc.py new file mode 100644 index 00000000..a6ea8da9 --- /dev/null +++ b/gem_controllers/block_diagrams/stage_blocks/pmsm_cc.py @@ -0,0 +1,187 @@ +from control_block_diagram.components import Point, Box, Connection, Circle +from control_block_diagram.predefined_components import DqToAbcTransformation, AbcToAlphaBetaTransformation,\ + AlphaBetaToDqTransformation, Add, PIController, Multiply, Limit + + +def pmsm_cc(emf_feedforward): + """ + Args: + emf_feedforward: Boolean whether emf feedforward stage is included + + Returns: + Function to build the PMSM current control block + """ + + def cc_pmsm(start, control_task): + """ + Function to build the PMSM current control block + Args: + start: Starting point of the block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + # space to the previous block + space = 1 if control_task == 'CC' else 1.5 + + # Add blocks for the i_sd and i_sq references and states + add_i_sd = Add(start.add_x(space)) + add_i_sq = Add(add_i_sd.position.sub(0.5, 1)) + + if control_task == 'CC': + # Connections at the inputs + Connection.connect(add_i_sd.input_left[0].sub_x(space), add_i_sd.input_left[0], + text=r'$i^{*}_{\mathrm{sd}}$', text_position='start', text_align='left') + Connection.connect(add_i_sq.input_left[0].sub_x(space - 0.5), add_i_sq.input_left[0], + text=r'$i^{*}_{\mathrm{sq}}$', text_position='start', text_align='left') + + # PI Controllers for the d and q component + pi_i_sd = PIController(add_i_sd.position.add_x(1.2), size=(1, 0.8), input_number=1, output_number=1, + text='Current\nController') + pi_i_sq = PIController(Point.merge(pi_i_sd.position, add_i_sq.position), size=(1, 0.8), input_number=1, + output_number=1) + + # Connection between the add blocks and the PI Controllers + Connection.connect(add_i_sd.output_right, pi_i_sd.input_left) + Connection.connect(add_i_sq.output_right, pi_i_sq.input_left) + + # Add blocks for the EMF Feedforward + add_u_sd = Add(pi_i_sd.position.add_x(2)) + add_u_sq = Add(pi_i_sq.position.add_x(1.2)) + + # Connections between the PI Controllers and the Add blocks + Connection.connect(pi_i_sd.output_right[0], add_u_sd.input_left[0], text=r'$\Delta u^{*}_{\mathrm{sd}}$', + distance_y=0.28) + Connection.connect(pi_i_sq.output_right[0], add_u_sq.input_left[0], text=r'$\Delta u^{*}_{\mathrm{sq}}$', + distance_y=0.4, move_text=(0.25, 0)) + + # Coordinate transformation from DQ to Abc coordinates + dq_to_abc = DqToAbcTransformation(Point.get_mid(add_u_sd.position, add_u_sq.position).add_x(2), + input_space=1) + + # Connections between the add blocks and the coordinate transformation + Connection.connect(add_u_sd.output_right[0], dq_to_abc.input_left[0], text=r'$u^{*}_{\mathrm{sd}}$', + distance_y=0.28) + Connection.connect(add_u_sq.output_right[0], dq_to_abc.input_left[1], text=r'$u^{*}_{\mathrm{sq}}$', + distance_y=0.28, move_text=(0.4, 0)) + + # Limit of the input voltages + limit = Limit(dq_to_abc.position.add_x(1.8), size=(1, 1.2), inputs=dict(left=3, left_space=0.3), + outputs=dict(right=3, right_space=0.3)) + + # Connection between the coordinate transformation and the limit block + Connection.connect(dq_to_abc.output_right, limit.input_left) + + # Pulse width modulation block + pwm = Box(limit.position.add_x(2.5), size=(1.5, 1.2), text='PWM', inputs=dict(left=3, left_space=0.3), + outputs=dict(right=3, right_space=0.3)) + + # Connection between the limit and the PWM block + Connection.connect(limit.output_right, pwm.input_left, + text=[r'$u^*_{\mathrm{s a,b,c}}$', '', ''], distance_y=0.25) + + # Coordinate transformation from ABC to AlphaBeta coordinates + abc_to_alpha_beta = AbcToAlphaBetaTransformation(pwm.position.sub(1, 3.5), input='right', output='left') + + # Coordinate transformation from AlphaBeta to DQ coordinates + alpha_beta_to_dq = AlphaBetaToDqTransformation( + Point.merge(dq_to_abc.position, abc_to_alpha_beta.position), input='right', output='left') + + # Distance of the blocks in the EMF Feedforward path + distance = (add_u_sq.position.y - alpha_beta_to_dq.output_left[0].y) / 4 + + # Multiplications of the EMF Feedforward + multiply_u_sq = Multiply(add_u_sq.position.sub_y(distance), outputs=dict(top=1)) + multiply_u_sd = Multiply(Point.merge(add_u_sd.position, multiply_u_sq.position), outputs=dict(top=1)) + + # Connections between the multiplication and the add blocks + Connection.connect(multiply_u_sq.output_top, add_u_sq.input_bottom) + Connection.connect(multiply_u_sd.output_top, add_u_sd.input_bottom, text=r'-', text_position='end', + text_align='right', move_text=(-0.2, -0.2)) + + # Add block for the permanent flux psi_p + add_psi_p = Add(multiply_u_sq.position.sub_y(distance), outputs=dict(top=1)) + + # Connections of the add block + Connection.connect(add_psi_p.output_top, multiply_u_sq.input_bottom) + Connection.connect(add_psi_p.input_left[0].sub_x(0.3), add_psi_p.input_left[0], text=r'$\Psi_{\mathrm{p}}$', + text_position='start', text_align='left', distance_x=0.25) + + # Multiplication with the inductances + box_ls_1 = Box(add_psi_p.position.sub_y(distance), size=(0.6, 0.6), inputs=dict(bottom=1), outputs=dict(top=1), + text=r'$L_{\mathrm{d}}$') + box_ls_2 = Box(Point.merge(multiply_u_sd.position, box_ls_1.position), size=(0.6, 0.6), inputs=dict(bottom=1), + outputs=dict(top=1), text=r'$L_{\mathrm{q}}$') + + # Connections from the multiplications + Connection.connect(box_ls_1.output_top, add_psi_p.input_bottom) + Connection.connect(multiply_u_sd.input_left[0].sub_x(0.3), multiply_u_sd.input_left[0]) + Connection.connect(box_ls_2.output_top, multiply_u_sd.input_bottom) + + # Connections between the coordinate transformation and the add blocks + con_3 = Connection.connect(alpha_beta_to_dq.output_left[0], add_i_sd.input_bottom[0], text=r'-', + text_position='end', + text_align='right', move_text=(-0.2, -0.2)) + con_4 = Connection.connect(alpha_beta_to_dq.output_left[1], add_i_sq.input_bottom[0], text=r'-', + text_position='end', + text_align='right', move_text=(-0.2, -0.2)) + + # Connections between the previous conncetions and the inductances blocks + Connection.connect_to_line(con_3, box_ls_1.input_bottom[0]) + Connection.connect_to_line(con_4, box_ls_2.input_bottom[0]) + + # Derivation of the angle + box_d_dt = Box(Point.merge(alpha_beta_to_dq.position, start.sub_y(6.57020066645696)).sub_x(2), size=(1, 0.8), + text=r'$\mathrm{d} / \mathrm{d}t$', inputs=dict(right=1), outputs=dict(left=1)) + + # Conncetion between the derivation and multiplication block + con_omega = Connection.connect(box_d_dt.output_left, multiply_u_sq.input_left, space_y=1, + text=r'$\omega_{\mathrm{el}}$', move_text=(0, 2), text_align='left', + distance_x=0.3) + + if control_task == 'SC': + # Connector at the previous connection + Circle(con_omega[0].points[1], radius=0.05, fill='black') + + # Conncetion between the coordinate transformations + Connection.connect(abc_to_alpha_beta.output, alpha_beta_to_dq.input_right, + text=[r'$i_{\mathrm{s} \upalpha}$', r'$i_{\mathrm{s} \upbeta}$']) + + # Add block for the advanced angle + add = Add(Point.get_mid(dq_to_abc.position, alpha_beta_to_dq.position), inputs=dict(bottom=1, right=1), + outputs=dict(top=1)) + + # Connections of the add block + Connection.connect(alpha_beta_to_dq.output_top, add.input_bottom, text=r'$\varepsilon_{\mathrm{el}}$', + text_align='right', move_text=(0, -0.1)) + Connection.connect(add.output_top, dq_to_abc.input_bottom) + + # Calculate the advanced angle + box_t_a = Box(add.position.add_x(1.5), size=(1, 0.8), text=r'$1.5 T_{\mathrm{s}}$', inputs=dict(right=1), + outputs=dict(left=1)) + + # Connections of the advanced angle block + Connection.connect(box_t_a.output, add.input_right, text=r'$\Delta \varepsilon$') + Connection.connect(box_t_a.input[0].add_x(0.5), box_t_a.input[0], text=r'$\omega_{\mathrm{el}}$', + text_position='start', text_align='right', distance_x=0.3) + + start = pwm.position # starting point of the next block + # Inputs of the stage + inputs = dict(i_d_ref=[add_i_sd.input_left[0], dict(text=r'$i^{*}_{\mathrm{sd}}$', distance_y=0.25)], + i_q_ref=[add_i_sq.input_left[0], dict(text=r'$i^{*}_{\mathrm{sq}}$', distance_y=0.25)], + epsilon=[box_d_dt.input_right[0], dict()]) + outputs = dict(S=pwm.output_right, omega=con_omega[0].points[1]) # Outputs of the stage + # Connections to other lines + connect_to_line = dict(epsilon=[alpha_beta_to_dq.input_bottom[0], dict(text=r'$\varepsilon_{\mathrm{el}}$', + text_position='middle', + text_align='right')], + i=[abc_to_alpha_beta.input_right, dict(radius=0.1, fill=False, + text=[r'$\mathbf{i}_{\mathrm{s a,b,c}}$', '', + ''])],) + connections = dict() # Connections + + return start, inputs, outputs, connect_to_line, connections + + return cc_pmsm diff --git a/gem_controllers/block_diagrams/stage_blocks/pmsm_ops.py b/gem_controllers/block_diagrams/stage_blocks/pmsm_ops.py new file mode 100644 index 00000000..6e633248 --- /dev/null +++ b/gem_controllers/block_diagrams/stage_blocks/pmsm_ops.py @@ -0,0 +1,177 @@ +from control_block_diagram.components import Box, Connection, Text, Point, Circle, Path +from control_block_diagram.predefined_components import Add, Divide, IController, Limit + + +class PsiOptBox(Box): + """Optimal flux block""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # border in x- and y-direction + bx = 0.1 + by = 0.15 + + # Coordinate system + Connection.connect(self.bottom_left.add(self._size_x * bx, self._size_y * by), + self.bottom_right.add(-self._size_x * bx, self._size_y * by)) + Connection.connect(self.bottom.add_y(self._size_y * by), self.top.sub_y(self._size_y * by)) + + # Graph in the coordinate system + Path([self.top_left.add(self._size_x * bx, -self._size_y * (0.1 + by)), + self.bottom.add_y(self._size_y * (0.2 + by)), + self.top_right.sub(self._size_x * bx, self._size_y * (0.1 + by))], + angles=[{'in': 180, 'out': 0}, {'in': 180, 'out': 0}], arrow=False) + + +class TMaxPsiBox(Box): + """Maximum torque block""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # border in x- and y-direction + bx = 0.1 * self._size_x + by = 0.15 * self._size_y + scale = 1.5 + + # Coordinate system + Connection.connect(self.bottom_left.add(bx, by), self.top_left.add(bx, -by)) + Connection.connect(self.left.add_x(bx), self.right.sub_x(bx)) + + # Graph in the coordinate system + Path([self.left.add_x(bx), self.top_right.sub(scale * bx, scale * by)], arrow=False, + angles=[{'in': 180, 'out': 35}]) + Path([self.left.add_x(bx), self.bottom_right.add(-scale * bx, scale * by)], arrow=False, + angles=[{'in': 180, 'out': -35}]) + + +def pmsm_ops(start, control_task): + """ + Function to build the PMSM operation point selection block + Args: + start: Starting point of the block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + if control_task == 'TC': + # Connection at the input + Connection.connect(start, start.add_x(1), text='$T^{*}$', text_position='start', text_align='left', arrow=False) + + # Limit the torque reference + box_limit = Limit(start.add_x(6), inputs=dict(left=1, bottom=1), size=(1, 1)) + + # Calculate the optimal flux + box_psi_opt = PsiOptBox(start.add(2, -1.7), size=(1.2, 1)) + Text(position=box_psi_opt.top.add_y(0.25), text=r'$\Psi^{*}_{\mathrm{opt}}(T^{*})$') + + # Minimum block + box_min = Box(box_psi_opt.position.add(1.5, -1.3), inputs=dict(left=2, left_space=0.5), size=(0.8, 1), text='min') + + # Calculate the maximum torque + box_t_max = TMaxPsiBox(box_psi_opt.position.add_x(3.1), size=(1.2, 1)) + Text(position=box_t_max.top.add_y(0.25), text=r'$T_{\mathrm{max}}(\Psi)$') + + # Connect the maximum torque and limit block + con_torque = Connection.connect(start.add_x(1), box_limit.input_left[0]) + + # Connection between the input and the optimum flux block + Connection.connect(start.add_x(1), box_psi_opt.input_left[0], start_direction='south') + Circle(start.add_x(1), radius=0.05, fill='black') + + # Connection between the optimum flux and minimum block + Connection.connect(box_psi_opt.output_right[0], box_min.input_left[0]) + + # Conncetion between the minimum and maximum torque block + Connection.connect(box_min.output_right[0].add_x(0.3), box_t_max.input_left[0], start_direction='north') + + # Circle at the connection + Circle(box_min.output_right[0].add_x(0.3), radius=0.05, fill='black') + + # Conncetion between the maximum torque and torque limit block + Connection.connect(box_t_max.output_right[0], box_limit.input_bottom[0]) + + # Optimal operation point function block + box_f_psi_t = Box(box_limit.position.add(2.2, -1.5), size=(1.3, 3.5), inputs=dict(left=2, left_space=3), + outputs=dict(right=3, right_space=1), text=r'\textbf{f}($\Psi$, $T$)') + + # Conncetions to the optimal opertion point function block + Connection.connect(box_min.output_right[0], box_f_psi_t.input_left[1], text=r'$\Psi^{*}_{\mathrm{lim}}$') + Connection.connect(box_limit.output_right[0], box_f_psi_t.input_left[0], text=r'$T^{*}_{\mathrm{lim}}$') + + # Moulation Controller + + # Add the actual flux and delta flux + add_psi = Add(box_min.input_left[1].sub_x(1)) + + # Connection between the add and minimum block + Connection.connect(add_psi.output_right[0], box_min.input_left[1], text=r'$\Psi_{\mathrm{lim}}$', text_align='top', + distance_y=0.25) + + # Limit the delta flux + limit_modulation = Limit(add_psi.input_left[0].sub_x(1.5), size=(1, 1)) + + # I Controller of the modulation controller + i_controller = IController(limit_modulation.input_left[0].sub_x(1), size=(1.2, 1), text='Modulation\nController') + + # Connections of the limit block + Connection.connect(i_controller.output_right, limit_modulation.input_left) + Connection.connect(limit_modulation.output_right[0], add_psi.input_left[0], text=r'$\Delta \Psi$', distance_y=0.25) + + # Add block of the modulation controller + add_a = Add(i_controller.position.sub_x(1.5)) + + # Maximum modulation + box_a_max = Box(add_a.input_left[0].sub_x(1.3), size=(1.5, 0.8), text=r'$a_{\mathrm{max}} \cdot k$') + + # Building the absolute modulation + box_abs = Box(add_a.position.sub_y(1.2), size=(0.8, 0.8), text=r'|\textbf{x}|', inputs=dict(bottom=1), + outputs=dict(top=1)) + + # Conncetions of the add block + Connection.connect(box_a_max.output_right[0], add_a.input_left[0], text='$a^{*}$') + Connection.connect(add_a.output_right[0], i_controller.input_left[0]) + con_a = Connection.connect(box_abs.output_top[0], add_a.input_bottom[0], text='$-$', text_align='right', + text_position='end', move_text=(-0.1, -0.2)) + + # Additional text at the connection + Text(position=Point.get_mid(*con_a.points).sub_x(0.25), text='$a$') + + # divide the actual voltage by the dc voltage + div_a = Divide(box_abs.input_bottom[0].sub_y(1), size=(1, 0.5), inputs='bottom', input_space=0.5) + + # Conncetions of the divide block + Connection.connect(div_a.input_bottom[0].sub_y(0.7), div_a.input_bottom[0], text=r'$\mathbf{u^{*}_{\mathrm{dq}}}$', + text_position='start', text_align='bottom') + Connection.connect(div_a.input_bottom[1].sub_y(0.7), div_a.input_bottom[1], + text=r'$\frac{u_{\mathrm{\mbox{\fontsize{3}{4}\selectfont DC}}}}{2}$', text_position='start', + text_align='bottom') + Connection.connect(div_a.output_top[0], box_abs.input_bottom[0]) + + # Divide the dc voltage by the electrical speed + div_psi = Divide(add_psi.input_bottom[0].sub_y(2), size=(1, 0.5), inputs='bottom', input_space=0.5) + + # Connections of the divide block + Connection.connect(div_psi.output_top[0], add_psi.input_bottom[0], text=r'$\Psi_{\mathrm{max}}$', + text_align='right', distance_x=0.5) + Connection.connect(div_psi.input_bottom[0].sub_y(0.7), div_psi.input_bottom[0], + text=r'$\frac{u_{\mathrm{\mbox{\fontsize{3}{4}\selectfont DC}}}}{\sqrt{3}}$', + text_position='start', text_align='bottom') + + # Limit of the current reference values + limit = Limit(Point.get_mid(box_f_psi_t.output_right[0], box_f_psi_t.output_right[1]).add_x(1), size=(1, 1.5), + inputs=dict(left=2, left_space=1), outputs=dict(right=2, right_space=1)) + + # Connections between the optimal opertion point function and limit block + Connection.connect(box_f_psi_t.output_right, limit.input_left) + + # Inputs of the stage + inputs = dict(t_ref=[con_torque.start, dict(arrow=False, text=r'$T^{*}$')], + omega=[div_psi.input_bottom[1], dict(text=r'$\omega_{\mathrm{el}}$', move_text=(3, 0))]) + outputs = dict(i_d_ref=limit.output_right[0], i_q_ref=limit.output_right[1]) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Connections of the stage + start = limit.output_right[0] # Starting point of the next stage + + return start, inputs, outputs, connect_to_lines, connections diff --git a/gem_controllers/block_diagrams/stage_blocks/pmsm_output.py b/gem_controllers/block_diagrams/stage_blocks/pmsm_output.py new file mode 100644 index 00000000..75e11f81 --- /dev/null +++ b/gem_controllers/block_diagrams/stage_blocks/pmsm_output.py @@ -0,0 +1,43 @@ +from control_block_diagram.components import Connection +from control_block_diagram.predefined_components import PMSM, DcConverter + + +def pmsm_output(emf_feedforward): + """ + Args: + emf_feedforward: Boolean whether emf feedforward stage is included + + Returns: + Function to build the PMSM output Bblock + """ + + def _pmsm_output(start, control_task): + """ + Function to build the PMSM output block + Args: + start: Starting point of the block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + # Converter with DC input voltage + converter = DcConverter(start.add_x(2.7), input_number=3, input_space=0.3, output_number=3) + + # PMSM block + pmsm = PMSM(converter.position.sub_y(5), size=1.3, input='top') + + # Connection between the converter and the motor block + con_1 = Connection.connect(converter.output_bottom, pmsm.input_top, arrow=False) + + start = pmsm.position # starting point of the next block + # Inputs of the stage + inputs = dict(S=[converter.input_left, dict(text=[r'$\mathbf{S}_{\mathrm{a,b,c}}$', '', ''], distance_y=0.25)]) + outputs = dict(epsilon=pmsm.output_left[0]) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict(i=con_1) # Connections + + return start, inputs, outputs, connect_to_lines, connections + + return _pmsm_output diff --git a/gem_controllers/block_diagrams/stage_blocks/pmsm_sc.py b/gem_controllers/block_diagrams/stage_blocks/pmsm_sc.py new file mode 100644 index 00000000..c16eacf3 --- /dev/null +++ b/gem_controllers/block_diagrams/stage_blocks/pmsm_sc.py @@ -0,0 +1,49 @@ +from control_block_diagram.components import Connection +from control_block_diagram.predefined_components import Add, PIController, Limit + + +def pmsm_speed_controller(start, control_task): + """ + Function to build the speed controller block of the PMSM + Args: + start: Starting point of the block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + # space to the previous block + space = 1 if control_task == 'SC' else 1.5 + + # Add block for the speed reference and state + add_omega = Add(start.add_x(space)) + + # Connection between the mechanical speed input and the add block + Connection.connect(add_omega.input_bottom[0].sub_y(1), add_omega.input_bottom[0], text=r'$\omega_{\mathrm{me}}$', + text_align='bottom', text_position='start') + + if control_task == 'SC': + # Connection at the input + Connection.connect(start, add_omega.input_left[0], text=r'$\omega^{*}$', text_align='left', + text_position='start') + + # PI Speed Controller + pi_omega = PIController(add_omega.position.add_x(1.5), text='Speed\nController') + + # Connection between the add block and pi controller + Connection.connect(add_omega.output_right, pi_omega.input_left) + + # Limit of the torque reference + limit = Limit(pi_omega.output_right[0].add_x(1.5), size=(1, 1)) + + # Connection between the pi controller and the limit block + Connection.connect(pi_omega.output_right[0], limit.input_left[0]) + + start = limit.output_right[0] # starting point of the next block + inputs = dict(omega_ref=[add_omega.input_left[0], dict(text=r'$\omega^{*}$')]) # Inputs of the stage + outputs = dict(t_ref=limit.output_right[0]) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Conncetions of the stage + + return start, inputs, outputs, connect_to_lines, connections diff --git a/gem_controllers/block_diagrams/stage_blocks/scim_cc.py b/gem_controllers/block_diagrams/stage_blocks/scim_cc.py new file mode 100644 index 00000000..fae6243e --- /dev/null +++ b/gem_controllers/block_diagrams/stage_blocks/scim_cc.py @@ -0,0 +1,164 @@ +from control_block_diagram.components import Box, Connection, Text, Point, Circle +from control_block_diagram.predefined_components import Add, PIController, Limit, DqToAbcTransformation,\ + AbcToAlphaBetaTransformation, AlphaBetaToDqTransformation + + +def scim_cc(emf_feedforward): + """ + Args: + emf_feedforward: Boolean whether emf feedforward stage is included + + Returns: + Function to build the SCIM current control block + """ + + def cc_scim(start, control_task): + """ + Function to build the SCIM current control block + Args: + start: Starting Point of the Block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + # space to the previous block + space = 1 if control_task == 'CC' else 1.5 + + # Add blocks for the i_sd and i_sq references and states + add_i_sd = Add(start.add_x(space)) + add_i_sq = Add(add_i_sd.position.sub(0.5, 1)) + + if control_task == 'CC': + # Connections at the inputs + Connection.connect(add_i_sd.input_left[0].sub_x(space), add_i_sd.input_left[0], + text=r'$i^{*}_{\mathrm{sd}}$', text_position='start', text_align='left') + Connection.connect(add_i_sq.input_left[0].sub_x(space - 0.5), add_i_sq.input_left[0], + text=r'$i^{*}_{\mathrm{sq}}$', text_position='start', text_align='left') + + # PI Controllers for the d and q component + pi_i_sd = PIController(add_i_sd.position.add_x(1.2), size=(1, 0.8), input_number=1, output_number=1, + text='Current\nController') + pi_i_sq = PIController(Point.merge(pi_i_sd.position, add_i_sq.position), size=(1, 0.8), input_number=1, + output_number=1) + + # Connection between the add blocks and the PI Controllers + Connection.connect(add_i_sd.output_right, pi_i_sd.input_left) + Connection.connect(add_i_sq.output_right, pi_i_sq.input_left) + + # Add blocks for the EMF Feedforward + add_u_sd = Add(pi_i_sd.position.add_x(2)) + add_u_sq = Add(pi_i_sq.position.add_x(1.2)) + + # Connections between the PI Controllers and the Add blocks + Connection.connect(pi_i_sd.output_right[0], add_u_sd.input_left[0], text=r'$\Delta u^{*}_{\mathrm{sd}}$', + distance_y=0.28) + Connection.connect(pi_i_sq.output_right[0], add_u_sq.input_left[0], text=r'$\Delta u^{*}_{\mathrm{sq}}$', + distance_y=0.4, move_text=(0.25, 0)) + + # Coordinate transformation from DQ to Abc coordinates + dq_to_abc = DqToAbcTransformation(Point.get_mid(add_u_sd.position, add_u_sq.position).add_x(2), + input_space=1) + + # Connections between the add blocks and the coordinate transformation + Connection.connect(add_u_sd.output_right[0], dq_to_abc.input_left[0], text=r'$u^{*}_{\mathrm{sd}}$', + distance_y=0.28) + Connection.connect(add_u_sq.output_right[0], dq_to_abc.input_left[1], text=r'$u^{*}_{\mathrm{sq}}$', + distance_y=0.28, move_text=(0.4, 0)) + + # Limit of the input voltages + limit = Limit(dq_to_abc.position.add_x(2.2), size=(1.5, 1.5), inputs=dict(left=3, left_space=0.3), + outputs=dict(right=3, right_space=0.3)) + + # Connection between the coordinate transformation and the limit block + Connection.connect(dq_to_abc.output_right, limit.input_left) + + # Pulse width modulation block + pwm = Box(limit.position.add_x(2.8), size=(1.5, 1.2), text='PWM', inputs=dict(left=3, left_space=0.3), + outputs=dict(right=3, right_space=0.3)) + + # Connection between the limit and the PWM block + Connection.connect(limit.output_right, pwm.input_left, + text=[r'$u^*_{\mathrm{s a,b,c}}$', '', ''], distance_y=0.25) + + # Coordinate transformation from ABC to AlphaBeta coordinates + abc_to_alpha_beta = AbcToAlphaBetaTransformation(pwm.position.sub(1, 3), input='right', output='left') + + # Flux observer + observer = Box(abc_to_alpha_beta.position.sub(0.5, 2.2), size=(2, 1), text='Flux Observer', + inputs=dict(right=1, top=2, top_space=1.2), outputs=dict(left=2)) + + # Connection of the flux observer + con_omega = Connection.connect(observer.input_right[0].add_x(1), observer.input_right[0]) + Connection.connect(observer.input_top[0].add_y(0.4), observer.input_top[0], + text=r'$\mathbf{i}_{\mathrm{s a,b,c}}$', text_position='start') + Connection.connect(observer.input_top[1].add_y(0.4), observer.input_top[1], + text=r'$\mathbf{u}_{\mathrm{s a,b,c}}$', text_position='start', move_text=(0, -0.05)) + + # Coordinate transformation from AlphaBeta to DQ coordinates + alpha_beta_to_dq = AlphaBetaToDqTransformation( + Point.merge(dq_to_abc.position, abc_to_alpha_beta.position), input='right', output='left') + + # Connections between the coordinate transformation and the add blocks + con_i_sd = Connection.connect(alpha_beta_to_dq.output_left[0], add_i_sd.input_bottom[0], text='-', + text_position='end', text_align='right', move_text=(-0.2, -0.2)) + con_i_sq = Connection.connect(alpha_beta_to_dq.output_left[1], add_i_sq.input_bottom[0], text='-', + text_position='end', text_align='right', move_text=(-0.2, -0.2)) + + # Texts at the previous connections + Text(position=con_i_sd.start.add(-0.25, 0.25), text=r'$i_{\mathrm{sd}}$') + Text(position=con_i_sq.start.add(-0.25, 0.25), text=r'$i_{\mathrm{sq}}$') + + # Connection between the flux observer and the coordinate transformation + Connection.connect(observer.output_left[0], alpha_beta_to_dq.input_bottom[0]) + + # Texts at the outputs of the flux observer + Text(position=observer.output_left[0].add(-0.4, 0.3), text=r'$\angle \hat{\underline{\Psi}}_{\mathrm{r}}$') + Text(position=observer.output_left[1].add(-0.4, -0.3), text=r'$\hat{\Psi}_{\mathrm{r}}$') + + # Connections between the coordinate transformations + Connection.connect(abc_to_alpha_beta.output, alpha_beta_to_dq.input_right, + text=[r'$i_{\mathrm{s} \upalpha}$', r'$i_{\mathrm{s} \upbeta}$']) + Connection.connect(alpha_beta_to_dq.output_top, dq_to_abc.input_bottom, + text=r'$\angle \hat{\underline{\Psi}}_r$', text_align='right') + + # Feedforward block + feedforward = Box(Point.get_mid(add_u_sd.position, add_u_sq.position).sub_y(1.6), size=(2, 0.8), + text='feedforward', inputs=dict(bottom=4, bottom_space=0.3), + outputs=dict(top=2, top_space=0.8)) + + # Connections of the feedforward block + con_omega_2 = Connection.connect(feedforward.input_bottom[0].add(7, -4.0702), feedforward.input_bottom[0], + text=r'$\omega_{\mathrm{me}}$', text_position='start', move_text=(-1, -0.1)) + Connection.connect_to_line(con_omega_2, con_omega.start, arrow=False) + Connection.connect(feedforward.output_top[0], add_u_sq.input_bottom[0]) + Connection.connect(feedforward.output_top[1], add_u_sd.input_bottom[0]) + con_psi_r = Connection.connect(observer.output_left[1], feedforward.input_bottom[1]) + Connection.connect_to_line(con_i_sd, feedforward.input_bottom[2]) + Connection.connect_to_line(con_i_sq, feedforward.input_bottom[3]) + + if control_task in ['TC', 'SC']: + # Circle at the connection start + Circle(con_psi_r.points[1], radius=0.05, draw='black', fill='black') + if control_task == 'SC': + # Circle at the connection start + Circle(con_omega_2.points[1], radius=0.05, draw='black', fill='black') + + start = pwm.position # starting point of the next block + # Inputs of the stage + inputs = dict(i_d_ref=[add_i_sd.input_left[0], dict(text=r'$i^{*}_{\mathrm{sd}}$', distance_y=0.3, + text_position='end', move_text=(-0.85, 0))], + i_q_ref=[add_i_sq.input_left[0], dict(text=r'$i^{*}_{\mathrm{sq}}$', distance_y=0.3, + text_position='end', move_text=(-0.35, 0))], + omega=[con_omega_2.start, dict(arrow=False)]) + # Outputs of the stage + outputs = dict(S=pwm.output_right, psi_r=con_psi_r.points[1], omega_me=con_omega_2.points[1]) + # Connections to other lines + connect_to_line = dict(i=[abc_to_alpha_beta.input_right, dict(radius=0.1, fill=False, + text=[r'$\mathbf{i}_{\mathrm{s a,b,c}}$', '', + ''])]) + connections = dict() # Connections + + return start, inputs, outputs, connect_to_line, connections + return cc_scim diff --git a/gem_controllers/block_diagrams/stage_blocks/scim_ops.py b/gem_controllers/block_diagrams/stage_blocks/scim_ops.py new file mode 100644 index 00000000..2cbdbc4c --- /dev/null +++ b/gem_controllers/block_diagrams/stage_blocks/scim_ops.py @@ -0,0 +1,189 @@ +from control_block_diagram.components import Box, Connection, Text, Circle, Point, Path +from control_block_diagram.predefined_components import Limit, PIController, IController, Add, Divide + + +class PsiOptBox(Box): + """Optimal flux block""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # border in x- and y-direction + bx = 0.1 + by = 0.15 + + # Coordinate system + Connection.connect(self.bottom_left.add(self._size_x * bx, self._size_y * by), + self.bottom_right.add(-self._size_x * bx, self._size_y * by)) + Connection.connect(self.bottom.add_y(self._size_y * by), self.top.sub_y(self._size_y * by)) + + # Graph in the coordinate system + Path([self.top_left.add(self._size_x * bx, -self._size_y * (0.1 + by)), + self.bottom.add_y(self._size_y * by), + self.top_right.sub(self._size_x * bx, self._size_y * (0.1 + by))], + angles=[{'in': 110, 'out': -20}, {'in': 200, 'out': 70}], arrow=False) + + +class TMaxPsiBox(Box): + """Maximum torque block""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # border in x- and y-direction + bx = 0.1 * self._size_x + by = 0.15 * self._size_y + scale = 1.5 + + # Coordinate system + Connection.connect(self.bottom_left.add(bx, by), self.top_left.add(bx, -by)) + Connection.connect(self.left.add_x(bx), self.right.sub_x(bx)) + + # Graph in the coordinate system + Path([self.left.add_x(bx), self.top_right.sub(scale * bx, scale * by)], arrow=False, + angles=[{'in': 180, 'out': 35}]) + Path([self.left.add_x(bx), self.bottom_right.add(-scale * bx, scale * by)], arrow=False, + angles=[{'in': 180, 'out': -35}]) + + +def scim_ops(start, control_task): + """ + Function to build the SCIM operation point selection block + Args: + start: Starting point of the block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + start_add = 0 # distance to the start + + # Torque Controller + if control_task == 'TC': + start_add = 1 # distance to the start + + # Connection at the input + Connection.connect(start, start.add_x(start_add), text='$T^{*}$', text_position='start', text_align='left', + arrow=False) + + # Limit the torque reference + box_limit = Limit(start.add_x(start_add + 5), size=(1, 1), inputs=dict(left=1, top=1)) + + # Connection between the start and the limit block + Connection.connect(start.add_x(start_add), box_limit.input_left[0]) + + # Optimal flux block with text + box_psi_opt = PsiOptBox(start.add(start_add + 1, 1.7), size=(1.2, 1)) + Text(position=box_psi_opt.bottom.sub_y(0.3), text=r'$\Psi^{*}_{\mathrm{opt}}(T^{*})$') + + # Minimum flux block + box_min = Box(box_psi_opt.position.add(1.5, 1.3), inputs=dict(left=2, left_space=0.5), size=(0.8, 1), text='min') + + # Maximum torque block with text + box_t_max = TMaxPsiBox(box_psi_opt.position.add_x(3.1), size=(1.2, 1)) + Text(position=box_t_max.bottom.sub_y(0.3), text=r'$T_{\mathrm{max}}(\Psi)$') + + # Connection between the start and the optimal flux block + Connection.connect(start.add_x(start_add), box_psi_opt.input_left[0], start_direction='north') + Circle(start.add_x(start_add), radius=0.05, fill='black') + + # Connection between the optimal flux and minimum flux block + Connection.connect(box_psi_opt.output_right[0], box_min.input_left[1]) + + # Connection of the maximum torque block + Connection.connect(box_min.output_right[0].add_x(0.3), box_t_max.input_left[0], start_direction='south') + Circle(box_min.output_right[0].add_x(0.3), radius=0.05, fill='black') + Connection.connect(box_t_max.output_right[0], box_limit.input_top[0]) + + # Calculation of the i_sq reference + box_i_sq_ref = Box(box_limit.output_right[0].add_x(1.5), size=(1, 0.8), + text=r'$\frac{2 L_{\mathrm{r}}}{3 p L_{\mathrm{m}}}$') + Connection.connect(box_limit.output_right, box_i_sq_ref.input_left, text=r'$T^{*}_{\mathrm{lim}}$') + divide = Box(box_i_sq_ref.output_right[0].add_x(1), size=(0.5, 0.5), text=r'$\div$', inputs=dict(left=1, bottom=1), + outputs=dict(right=1, top=1)) + Connection.connect(box_i_sq_ref.output_right, divide.input_left) + + # Add block for the flux + add_psi = Add(box_min.output_right[0].add_x(2.5)) + + # Flux Controller + pi_psi = PIController(add_psi.position.add_x(1.5), text='Flux\nController') + + # Connections of the add block + Connection.connect(box_min.output_right, add_psi.input_left, text=r'$\Psi^{*}_{\mathrm{lim}}$') + Connection.connect(divide.output_top, add_psi.input_bottom, text=r'$\hat{\Psi}_{\mathrm{r}}$') + Connection.connect(add_psi.output_right, pi_psi.input_left) + + # Limit the current reference values + limit = Limit(pi_psi.position.add(3.5, -0.5), size=(1.5, 1.5), inputs=dict(left=2, left_space=1), + outputs=dict(right=2, right_space=1)) + + # Connections of the limit block + Connection.connect(pi_psi.output_right[0], limit.input_left[0]) + Connection.connect(divide.output_right[0], limit.input_left[1]) + + # Modulation Controller + + # Add the actual flux and delta flux + add_a = Add(box_min.input_left[0].sub_x(1.5), inputs=dict(left=1, top=1)) + + # Connection between the add and minimum block + Connection.connect(add_a.output_right[0], box_min.input_left[0], text=r'$\Psi_{\mathrm{lim}}$', text_align='bottom', + distance_y=0.25) + + # Calculate the maximum flux + divide_a = Divide(add_a.position.add_y(1), size=(1, 0.5), inputs='top', input_space=0.5) + Connection.connect(divide_a.output_bottom, add_a.input_top, text=r'$\Psi_{\mathrm{max}}$', text_align='right', + distance_x=0.5) + Connection.connect(divide_a.input_top[0].add_y(0.3), divide_a.input_top[0], + text=r'$\frac{u_{\mathrm{\mbox{\fontsize{3}{4}\selectfont DC}}}}{\sqrt{3}}$', + text_position='start', + text_align='top') + Connection.connect(divide_a.input_top[1].add_y(0.3), divide_a.input_top[1], text=r'$\omega_{\mathrm{el}}$', + text_position='start', text_align='top', move_text=(0, -0.05)) + + # Limit the delta flux + limit_psi = Limit(add_a.input_left[0].sub_x(1.5), size=(1, 1)) + + # I Controller of the modulation controller + i_controller = IController(limit_psi.input_left[0].sub_x(1.2), size=(1.2, 1), text='Modulation\nController') + + # Connections of the limit block + Connection.connect(i_controller.output_right, limit_psi.input_left) + Connection.connect(limit_psi.output_right[0], add_a.input_left[0], text=r'$\Delta \Psi$', distance_y=0.25, + text_align='bottom') + + # Add block for the modulation + add_a_max = Add(i_controller.position.sub_x(1.5)) + + # Connection between the add block and the I Controller + Connection.connect(add_a_max.output_right, i_controller.input_left) + + # Maximum modulation block + box_a_max = Box(add_a_max.input_left[0].sub_x(1.3), size=(1.5, 0.8), text=r'$a_{\mathrm{max}} \cdot k$') + + # Connection between the maximum modulation and add block + Connection.connect(box_a_max.output_right[0], add_a_max.input_left[0], text='$a^{*}$', text_align='bottom') + + # Calculation of the actual modulation + box_abs = Box(add_a_max.input_bottom[0].sub_y(1), size=(0.8, 0.8), text=r'|\textbf{x}|', inputs=dict(bottom=1), + outputs=dict(top=1)) + con_a = Connection.connect(box_abs.output_top[0], add_a_max.input_bottom[0], text='$-$', text_align='right', + text_position='end', move_text=(-0.1, -0.1)) + Text(position=Point.get_mid(*con_a.points).sub_x(0.25), text='$a$') + div_a = Divide(box_abs.input_bottom[0].sub_y(0.8), size=(1, 0.5), inputs='bottom', input_space=0.5) + Connection.connect(div_a.input_bottom[1].sub_y(0.4), div_a.input_bottom[1], + text=r'$\frac{u_{\mathrm{\mbox{\fontsize{3}{4}\selectfont DC}}}}{2}$', text_align='bottom', + text_position='start') + Connection.connect(div_a.input_bottom[0].sub_y(0.4), div_a.input_bottom[0], + text=r'$\mathbf{u^{*}_{\mathrm{dq}}}$', text_align='bottom', text_position='start') + Connection.connect(div_a.output_top, box_abs.input_bottom) + + # Inputs of the stage + inputs = dict(t_ref=[start, dict(arrow=False, text=r'$T^{*}$', end_direction='south', move_text=(-0.25, 1.8))], + psi_r=[divide.input_bottom[0], dict()]) + outputs = dict(i_q_ref=limit.output_right[1], i_d_ref=limit.output_right[0]) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Connections of the stage + start = limit.output_right[0] # Starting point of the next stage + + return start, inputs, outputs, connect_to_lines, connections diff --git a/gem_controllers/block_diagrams/stage_blocks/scim_output.py b/gem_controllers/block_diagrams/stage_blocks/scim_output.py new file mode 100644 index 00000000..d6b3844a --- /dev/null +++ b/gem_controllers/block_diagrams/stage_blocks/scim_output.py @@ -0,0 +1,42 @@ +from control_block_diagram.components import Connection +from control_block_diagram.predefined_components import DcConverter, SCIM + + +def scim_output(emf_feedforward): + """ + Args: + emf_feedforward: Boolean whether emf feedforward stage is included + + Returns: + Function to build the SCIM output block + """ + + def _scim_output(start, control_task): + """ + Function to build the SCIM output block + Args: + start: Starting point of the block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + # Converter with DC input voltage + converter = DcConverter(start.add_x(2.7), input_number=3, input_space=0.3, output_number=3) + + # SCIM block + scim = SCIM(converter.position.sub_y(5), size=1.3, input='top') + + # Connection between the converter and the motor block + con_1 = Connection.connect(converter.output_bottom, scim.input_top, arrow=False) + + start = scim.position # starting point of the next block + # Inputs of the stage + inputs = dict(S=[converter.input_left, dict(text=[r'$\mathbf{S}_{\mathrm{a,b,c}}$', '', ''], distance_y=0.25)]) + outputs = dict(omega=scim.output_left[0]) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict(i=con_1) # Connections + + return start, inputs, outputs, connect_to_lines, connections + return _scim_output diff --git a/gem_controllers/block_diagrams/stage_blocks/scim_sc.py b/gem_controllers/block_diagrams/stage_blocks/scim_sc.py new file mode 100644 index 00000000..e09e78c5 --- /dev/null +++ b/gem_controllers/block_diagrams/stage_blocks/scim_sc.py @@ -0,0 +1,48 @@ +from control_block_diagram.components import Connection +from control_block_diagram.predefined_components import Add, PIController, Limit + + +def scim_speed_controller(start, control_task): + """ + Function to build the speed controller block of the SCIM + Args: + start: Starting point of the block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + # space to the previous block + space = 1 if control_task == 'SC' else 1.5 + + # Add block for the speed reference and state + add_omega = Add(start.add_x(space)) + + if control_task == 'SC': + # Connection at the input + Connection.connect(start, add_omega.input_left[0], text=r'$\omega^{*}$', text_align='left', + text_position='start') + + # PI Speed Controller + pi_omega = PIController(add_omega.position.add_x(1.5), text='Speed\nController') + + # Connection between the add block and pi controller + Connection.connect(add_omega.output_right, pi_omega.input_left) + + # Limit of the torque reference + limit = Limit(pi_omega.output_right[0].add_x(1.5), size=(1, 1)) + + # Connection between the pi controller and the limit block + Connection.connect(pi_omega.output_right[0], limit.input_left[0]) + + start = limit.output_right[0].add(0.5, 2) # starting point of the next block + # Inputs of the stage + inputs = dict(omega_ref=[add_omega.input_left[0], dict(text=r'$\omega^{*}$')], + omega_me=[add_omega.input_bottom[0], dict(text='-', text_position='end', text_align='right', + move_text=(-0.2, -0.2))]) + outputs = dict(t_ref=limit.output_right[0]) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Conncetions of the stage + + return start, inputs, outputs, connect_to_lines, connections diff --git a/gem_controllers/block_diagrams/stage_blocks/series_dc_cc.py b/gem_controllers/block_diagrams/stage_blocks/series_dc_cc.py new file mode 100644 index 00000000..72d926c3 --- /dev/null +++ b/gem_controllers/block_diagrams/stage_blocks/series_dc_cc.py @@ -0,0 +1,78 @@ +from control_block_diagram.components import Box, Connection +from control_block_diagram.predefined_components import Add, PIController + + +def series_dc_cc(emf_feedforward): + """ + Args: + emf_feedforward: Boolean whether emf feedforward stage is included + + Returns: + Function to build the Series DC current control block + """ + + def cc_series_dc(start, control_task): + """ + Function to build the Series DC current control block + Args: + start: Starting point of the block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + # space to the previous block + space = 1 if control_task == 'CC' else 1.5 + + # Add block for the current reference and state + add_current = Add(start.add_x(space)) + + if control_task == 'CC': + # Connection at the input + Connection.connect(start, add_current.input_left[0], text=r'$i^{*}$', text_align='left', + text_position='start') + + # PI Current Controller + pi_current = PIController(add_current.position.add_x(1.5), text='Current\nController') + + # Connection between the add block and pi controller + Connection.connect(add_current.output_right, pi_current.input_left) + + start = pi_current.position # starting point of the next block + # Inputs of the stage + inputs = dict(i_ref=[add_current.input_left[0], dict(text=r'$i^{*}$')], i=[add_current.input_bottom[0], + dict(text=r'-', move_text=(-0.2, -0.2), text_position='end', text_align='right')]) + outputs = dict(u=pi_current.output_right[0]) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Connections + + if emf_feedforward: + # Add block of the emf feedforward + add_emf = Add(pi_current.position.add_x(2)) + + # Connection between pi controller and add block + Connection.connect(pi_current.output_right, add_emf.input_left, text=r'$\Delta u^{*}$') + + # Multiplication with the flux + box_psi = Box(add_emf.position.sub_y(2.5), size=(1, 0.8), text=r"$L'_{\mathrm{e}} i$", inputs=dict(bottom=1), + outputs=dict(top=1)) + + # Connection between the multiplication and add block + Connection.connect(box_psi.output_top, add_emf.input_bottom, text=r'$u^{0}$', text_position='end', + text_align='right', move_text=(-0.1, -0.2)) + + # Set the input of the emf feedforward + if control_task in ['SC']: + connect_to_lines['omega'] = [box_psi.input_bottom[0], dict(section=0)] + elif control_task in ['CC', 'TC']: + inputs['omega'] = [box_psi.input_bottom[0], dict()] + + # Set the output of the emf feedforward + outputs['u'] = add_emf.output_right[0] + + # Update the position of the next block + start = add_emf.position + + return start, inputs, outputs, connect_to_lines, connections + return cc_series_dc diff --git a/gem_controllers/block_diagrams/stage_blocks/series_dc_ops.py b/gem_controllers/block_diagrams/stage_blocks/series_dc_ops.py new file mode 100644 index 00000000..afba0ab0 --- /dev/null +++ b/gem_controllers/block_diagrams/stage_blocks/series_dc_ops.py @@ -0,0 +1,43 @@ +from control_block_diagram.components import Box, Connection +from control_block_diagram.predefined_components import Limit + + +def series_dc_ops(start, control_task): + """ + Function to build the Series DC operation point selection block + Args: + start: Starting point of the block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + # space to the previous block + space = 1 if control_task == 'TC' else 2.2 + + # Calculation of the current reference + box_torque = Box(start.add_x(space), size=(0.8, 0.8), text=r"$\frac{1}{L'_{\mathrm{e}}}$") + box_sqrt = Box(box_torque.output_right[0].add_x(1), size=(0.8, 0.8), text=r'$\sqrt{\mathrm{x}}$') + + # Conncetion between the previous blocks + Connection.connect(box_torque.output_right, box_sqrt.input_left) + + # Limit of the current reference + limit = Limit(box_sqrt.output_right[0].add_x(1), size=(1, 1)) + + # Conncetion between the calculation and limit block + Connection.connect(box_sqrt.output_right, limit.input_left) + + if control_task == 'TC': + # Conncetion at the input + Connection.connect(start, box_torque.input_left[0], text=r'$T^{*}$', text_align='left', + text_position='start') + + start = limit.position # starting point of the next block + inputs = dict(t_ref=[box_torque.input_left[0], dict(text=r'$T^{*}$')]) # Inputs of the stage + outputs = dict(i_ref=limit.output_right[0]) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Connections + + return start, inputs, outputs, connect_to_lines, connections diff --git a/gem_controllers/block_diagrams/stage_blocks/series_dc_output.py b/gem_controllers/block_diagrams/stage_blocks/series_dc_output.py new file mode 100644 index 00000000..c2411881 --- /dev/null +++ b/gem_controllers/block_diagrams/stage_blocks/series_dc_output.py @@ -0,0 +1,68 @@ +from control_block_diagram.components import Box, Connection +from control_block_diagram.predefined_components import DcSeriesMotor, DcConverter, Limit + + +def series_dc_output(emf_feedforward): + """ + Args: + emf_feedforward: Boolean whether emf feedforward stage is included + + Returns: + Function to build the Series DC output block + """ + + def _series_dc_output(start, control_task): + """ + Function to build the Series DC output block + Args: + start: Starting point of the block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + # space to the previous block + space = 1.5 if emf_feedforward else 2 + + # voltage limit block + limit = Limit(start.add_x(space), size=(1, 1)) + + # pulse width modulation block + pwm = Box(limit.output_right[0].add_x(1.5), size=(1, 0.8), text='PWM') + + # connection between limit and pwm block + Connection.connect(limit.output_right, pwm.input_left) + + # Converter with DC input voltage + converter = DcConverter(pwm.position.add_x(2), size=1.2, input_number=1) + + # Connection between pwm and converter block + Connection.connect(pwm.output_right, converter.input_left, text='$S$') + + # DC Series motor block + dc_series = DcSeriesMotor(converter.position.sub_y(3), size=1.2, input='top', output='left') + + # Connections between converter and motor block + conv_motor = Connection.connect(converter.output_bottom, dc_series.input_top, text=['', r'$i$'], + text_align='right', arrow=False) + + # Connection between previous connection and current output + con_i = Connection.connect_to_line(conv_motor[0], pwm.position.sub_y(1.5), text=r'$i$', arrow=False, fill=False, + radius=0.1) + + start = converter.position # starting point of the next block + inputs = dict(u=[limit.input_left[0], dict(text=r'$u^{*}$')]) # Inputs of the stage + outputs = dict(i=con_i.end) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Connections + + if emf_feedforward or control_task in ['SC']: + # Connection between the motor output and the omega output of the stage + con_omega = Connection.connect(dc_series.output_left[0].sub_x(2), dc_series.output_left[0], + text=r'$\omega_{\mathrm{me}}$', arrow=False) + # Add the omega output + outputs['omega'] = con_omega.end + + return start, inputs, outputs, connect_to_lines, connections + return _series_dc_output diff --git a/gem_controllers/block_diagrams/stage_blocks/shunt_dc_cc.py b/gem_controllers/block_diagrams/stage_blocks/shunt_dc_cc.py new file mode 100644 index 00000000..6ced1534 --- /dev/null +++ b/gem_controllers/block_diagrams/stage_blocks/shunt_dc_cc.py @@ -0,0 +1,79 @@ +from control_block_diagram.components import Box, Connection +from control_block_diagram.predefined_components import Add, PIController + + +def shunt_dc_cc(emf_feedforward): + """ + Args: + emf_feedforward: Boolean whether emf feedforward stage is included + + Returns: + Function to build the Shunt DC output current control block + """ + + def cc_shunt_dc(start, control_task): + """ + Function to build the Shunt DC output current control block + Args: + start: Starting point of the block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + # space to the previous block + space = 1 if control_task == 'CC' else 1.5 + + # Add block for the current reference and state + add_current = Add(start.add_x(space)) + + if control_task == 'CC': + # Connection at the input + Connection.connect(start, add_current.input_left[0], text=r'$i^{*}_{\mathrm{a}}$', text_align='left', + text_position='start') + + # PI Current Controller + pi_current = PIController(add_current.position.add_x(1.5), text='Current\nController') + + # Connection between the add block and pi controller + Connection.connect(add_current.output_right, pi_current.input_left) + + start = pi_current.position # starting point of the next block + # Inputs of the stage + inputs = dict(i_ref=[add_current.input_left[0], dict(text=r'$i^{*}_{\mathrm{a}}$')], + i=[add_current.input_bottom[0], dict(text=r'-', move_text=(-0.2, -0.2), text_position='end', + text_align='right')]) + outputs = dict(u=pi_current.output_right[0]) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Connections + + if emf_feedforward: + # Add block of the emf feedforward + add_emf = Add(pi_current.position.add_x(2)) + + # Connection between pi controller and add block + Connection.connect(pi_current.output_right, add_emf.input_left, text=r'$\Delta u^{*}$') + + # Multiplication with the flux + box_psi = Box(add_emf.position.sub_y(2.5), size=(1, 0.8), text=r"$L'_{\mathrm{e}} i_{\mathrm{e}}$", + inputs=dict(bottom=1), outputs=dict(top=1)) + + # Connection between the multiplication and add block + Connection.connect(box_psi.output_top, add_emf.input_bottom, text=r'$u^{0}$', + text_position='end', text_align='right', move_text=(-0.1, -0.2)) + + # Set the input of the emf feedforward + if control_task in ['SC']: + connect_to_lines['omega'] = [box_psi.input_bottom[0], dict(section=0)] + elif control_task in ['CC', 'TC']: + inputs['omega'] = [box_psi.input_bottom[0], dict()] + + # Set the output of the emf feedforward + outputs['u'] = add_emf.output_right[0] + + # Update the position of the next block + start = add_emf.position + + return start, inputs, outputs, connect_to_lines, connections + return cc_shunt_dc diff --git a/gem_controllers/block_diagrams/stage_blocks/shunt_dc_ops.py b/gem_controllers/block_diagrams/stage_blocks/shunt_dc_ops.py new file mode 100644 index 00000000..291307a7 --- /dev/null +++ b/gem_controllers/block_diagrams/stage_blocks/shunt_dc_ops.py @@ -0,0 +1,39 @@ +from control_block_diagram.components import Box, Connection +from control_block_diagram.predefined_components import Limit + + +def shunt_dc_ops(start, control_task): + """ + Function to build the Shunt DC operation point selection block + Args: + start: Starting point of the block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + # space to the previous block + space = 1 if control_task == 'TC' else 2.2 + + # Calculation of the current reference + box_torque = Box(start.add_x(space), size=(0.8, 0.8), text=r"$\frac{1}{L'_{\mathrm{e}} i_{\mathrm{e}}}$") + + if control_task == 'TC': + # Connection at the input + Connection.connect(start, box_torque.input_left[0], text=r'$T^{*}$', text_align='left', + text_position='start') + + # Limit of the current reference + limit = Limit(box_torque.output_right[0].add_x(1), size=(1, 1)) + + # Connection between the calculation and limit block + Connection.connect(box_torque.output_right, limit.input_left) + + start = limit.position # starting point of the next block + inputs = dict(t_ref=[box_torque.input_left[0], dict(text=r'$T^{*}$')]) # Inputs of the stage + outputs = dict(i_ref=limit.output_right[0]) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Connections + + return start, inputs, outputs, connect_to_lines, connections diff --git a/gem_controllers/block_diagrams/stage_blocks/shunt_dc_output.py b/gem_controllers/block_diagrams/stage_blocks/shunt_dc_output.py new file mode 100644 index 00000000..b1cdbd04 --- /dev/null +++ b/gem_controllers/block_diagrams/stage_blocks/shunt_dc_output.py @@ -0,0 +1,68 @@ +from control_block_diagram.components import Box, Connection +from control_block_diagram.predefined_components import DcShuntMotor, DcConverter, Limit + + +def shunt_dc_output(emf_feedforward): + """ + Args: + emf_feedforward: Boolean whether emf feedforward stage is included + + Returns: + Function to build the Shunt DC output block + """ + + def _shunt_dc_output(start, control_task): + """ + Function to build the Shunt DC output block + Args: + start: Starting point of the block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + # space to the previous block + space = 1.5 if emf_feedforward else 2 + + # voltage limit block + limit = Limit(start.add_x(space), size=(1, 1)) + + # pulse width modulation block + pwm = Box(limit.output_right[0].add_x(1.5), size=(1, 0.8), text='PWM') + + # connection between limit and pwm block + Connection.connect(limit.output_right, pwm.input_left) + + # Converter with DC input voltage + converter = DcConverter(pwm.position.add_x(2), size=1.2, input_number=1) + + # Connection between pwm and converter block + Connection.connect(pwm.output_right, converter.input_left, text='$S$') + + # DC Shunt motor block + dc_shunt = DcShuntMotor(converter.position.sub_y(3), size=1.2, input='top', output='left') + + # Connections between converter and motor block + conv_motor = Connection.connect(converter.output_bottom, dc_shunt.input_top, text=['', r'$i_{\mathrm{a}}$'], + text_align='right', arrow=False) + + # Connection between previous connection and current output + con_i = Connection.connect_to_line(conv_motor[0], pwm.position.sub_y(1.5), text=r'$i_{\mathrm{a}}$', + arrow=False, fill=False, radius=0.1) + + start = converter.position # starting point of the next block + inputs = dict(u=[limit.input_left[0], dict(text=r'$u^{*}$')]) # Inputs of the stage + outputs = dict(i=con_i.end) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Connections + + if emf_feedforward or control_task in ['SC']: + # Connection between the motor output and the omega output of the stage + con_omega = Connection.connect(dc_shunt.output_left[0].sub_x(2), dc_shunt.output_left[0], + text=r'$\omega_{\mathrm{me}}$', arrow=False) + # Add the omega output + outputs['omega'] = con_omega.end + + return start, inputs, outputs, connect_to_lines, connections + return _shunt_dc_output diff --git a/gem_controllers/block_diagrams/stage_blocks/synrm_cc.py b/gem_controllers/block_diagrams/stage_blocks/synrm_cc.py new file mode 100644 index 00000000..5f2b8877 --- /dev/null +++ b/gem_controllers/block_diagrams/stage_blocks/synrm_cc.py @@ -0,0 +1,179 @@ +from control_block_diagram.components import Point, Box, Connection, Circle +from control_block_diagram.predefined_components import DqToAbcTransformation, AbcToAlphaBetaTransformation,\ + AlphaBetaToDqTransformation, Add, PIController, Multiply, Limit + + +def synrm_cc(emf_feedforward): + """ + Args: + emf_feedforward: Boolean whether emf feedforward stage is included + + Returns: + Function to build the SynRM current control block + """ + + def cc_synrm(start, control_task): + """ + Function to build the SynRM current control block + Args: + start: Starting point of the block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + # space to the previous block + space = 1 if control_task == 'CC' else 1.5 + + # Add blocks for the i_sd and i_sq references and states + add_i_sd = Add(start.add_x(space)) + add_i_sq = Add(add_i_sd.position.sub(0.5, 1)) + + if control_task == 'CC': + # Connections at the inputs + Connection.connect(add_i_sd.input_left[0].sub_x(space), add_i_sd.input_left[0], + text=r'$i^{*}_{\mathrm{sd}}$', text_position='start', text_align='left') + Connection.connect(add_i_sq.input_left[0].sub_x(space - 0.5), add_i_sq.input_left[0], + text=r'$i^{*}_{\mathrm{sq}}$', text_position='start', text_align='left') + + # PI Controllers for the d and q component + pi_i_sd = PIController(add_i_sd.position.add_x(1.2), size=(1, 0.8), input_number=1, output_number=1, + text='Current\nController') + pi_i_sq = PIController(Point.merge(pi_i_sd.position, add_i_sq.position), size=(1, 0.8), input_number=1, + output_number=1) + + # Connection between the add blocks and the PI Controllers + Connection.connect(add_i_sd.output_right, pi_i_sd.input_left) + Connection.connect(add_i_sq.output_right, pi_i_sq.input_left) + + # Add blocks for the EMF Feedforward + add_u_sd = Add(pi_i_sd.position.add_x(2)) + add_u_sq = Add(pi_i_sq.position.add_x(1.2)) + + # Connections between the PI Controllers and the Add blocks + Connection.connect(pi_i_sd.output_right[0], add_u_sd.input_left[0], text=r'$\Delta u^{*}_{\mathrm{sd}}$', + distance_y=0.28) + Connection.connect(pi_i_sq.output_right[0], add_u_sq.input_left[0], text=r'$\Delta u^{*}_{\mathrm{sq}}$', + distance_y=0.4, move_text=(0.25, 0)) + + # Coordinate transformation from DQ to Abc coordinates + dq_to_abc = DqToAbcTransformation(Point.get_mid(add_u_sd.position, add_u_sq.position).add_x(2), + input_space=1) + + # Connections between the add blocks and the coordinate transformation + Connection.connect(add_u_sd.output_right[0], dq_to_abc.input_left[0], text=r'$u^{*}_{\mathrm{sd}}$', + distance_y=0.28) + Connection.connect(add_u_sq.output_right[0], dq_to_abc.input_left[1], text=r'$u^{*}_{\mathrm{sq}}$', + distance_y=0.28, move_text=(0.4, 0)) + + # Limit of the input voltages + limit = Limit(dq_to_abc.position.add_x(1.8), size=(1, 1.2), inputs=dict(left=3, left_space=0.3), + outputs=dict(right=3, right_space=0.3)) + + # Connection between the coordinate transformation and the limit block + Connection.connect(dq_to_abc.output_right, limit.input_left) + + # Pulse width modulation block + pwm = Box(limit.position.add_x(2.5), size=(1.5, 1.2), text='PWM', inputs=dict(left=3, left_space=0.3), + outputs=dict(right=3, right_space=0.3)) + + # Connection between the limit and the PWM block + Connection.connect(limit.output_right, pwm.input_left, + text=[r'$u^*_{\mathrm{s a,b,c}}$', '', ''], distance_y=0.25) + + # Coordinate transformation from ABC to AlphaBeta coordinates + abc_to_alpha_beta = AbcToAlphaBetaTransformation(pwm.position.sub(1, 3.5), input='right', output='left') + + # Coordinate transformation from AlphaBeta to DQ coordinates + alpha_beta_to_dq = AlphaBetaToDqTransformation( + Point.merge(dq_to_abc.position, abc_to_alpha_beta.position), input='right', output='left') + + # Distance of the blocks in the EMF Feedforward path + distance = (add_u_sq.position.y - alpha_beta_to_dq.output_left[0].y) / 3 + + # Multiplications of the EMF Feedforward + multiply_u_sq = Multiply(add_u_sq.position.sub_y(distance), outputs=dict(top=1)) + multiply_u_sd = Multiply(Point.merge(add_u_sd.position, multiply_u_sq.position), outputs=dict(top=1)) + + # Connections between the multiplication and the add blocks + Connection.connect(multiply_u_sq.output_top, add_u_sq.input_bottom) + Connection.connect(multiply_u_sd.output_top, add_u_sd.input_bottom, text=r'-', text_position='end', + text_align='right', move_text=(-0.2, -0.2)) + + # Multiplication with the inductances + box_ls_1 = Box(multiply_u_sq.position.sub_y(distance), size=(0.6, 0.6), inputs=dict(bottom=1), outputs=dict(top=1), + text=r'$L_{\mathrm{d}}$') + box_ls_2 = Box(Point.merge(multiply_u_sd.position, box_ls_1.position), size=(0.6, 0.6), inputs=dict(bottom=1), + outputs=dict(top=1), text=r'$L_{\mathrm{q}}$') + + # Connections from the multiplications + Connection.connect(box_ls_1.output_top, multiply_u_sq.input_bottom) + Connection.connect(multiply_u_sd.input_left[0].sub_x(0.3), multiply_u_sd.input_left[0]) + Connection.connect(box_ls_2.output_top, multiply_u_sd.input_bottom) + + # Connections between the coordinate transformation and the add blocks + con_3 = Connection.connect(alpha_beta_to_dq.output_left[0], add_i_sd.input_bottom[0], text=r'-', + text_position='end', + text_align='right', move_text=(-0.2, -0.2)) + con_4 = Connection.connect(alpha_beta_to_dq.output_left[1], add_i_sq.input_bottom[0], text=r'-', + text_position='end', + text_align='right', move_text=(-0.2, -0.2)) + + # Connections between the previous conncetions and the inductances blocks + Connection.connect_to_line(con_3, box_ls_1.input_bottom[0]) + Connection.connect_to_line(con_4, box_ls_2.input_bottom[0]) + + # Derivation of the angle + box_d_dt = Box(Point.merge(alpha_beta_to_dq.position, start.sub_y(6.57020066645696)).sub_x(2), size=(1, 0.8), + text=r'$\mathrm{d} / \mathrm{d}t$', inputs=dict(right=1), outputs=dict(left=1)) + + # Conncetion between the derivation and multiplication block + con_omega = Connection.connect(box_d_dt.output_left, multiply_u_sq.input_left, space_y=1, + text=r'$\omega_{\mathrm{el}}$', move_text=(0, 2), text_align='left', + distance_x=0.3) + + if control_task == 'SC': + # Connector at the previous connection + Circle(con_omega[0].points[1], radius=0.05, fill='black') + + # Conncetion between the coordinate transformations + Connection.connect(abc_to_alpha_beta.output, alpha_beta_to_dq.input_right, + text=[r'$i_{\mathrm{s} \upalpha}$', r'$i_{\mathrm{s} \upbeta}$']) + + # Add block for the advanced angle + add = Add(Point.get_mid(dq_to_abc.position, alpha_beta_to_dq.position), inputs=dict(bottom=1, right=1), + outputs=dict(top=1)) + + # Connections of the add block + Connection.connect(alpha_beta_to_dq.output_top, add.input_bottom, text=r'$\varepsilon_{\mathrm{el}}$', + text_align='right', move_text=(0, -0.1)) + Connection.connect(add.output_top, dq_to_abc.input_bottom) + + # Calculate the advanced angle + box_t_a = Box(add.position.add_x(1.5), size=(1, 0.8), text=r'$1.5 T_{\mathrm{s}}$', inputs=dict(right=1), + outputs=dict(left=1)) + + # Connections of the advanced angle block + Connection.connect(box_t_a.output, add.input_right, text=r'$\Delta \varepsilon$') + Connection.connect(box_t_a.input[0].add_x(0.5), box_t_a.input[0], text=r'$\omega_{\mathrm{el}}$', + text_position='start', text_align='right', distance_x=0.3) + + start = pwm.position # starting point of the next block + # Inputs of the stage + inputs = dict(i_d_ref=[add_i_sd.input_left[0], dict(text=r'$i^{*}_{\mathrm{sd}}$', distance_y=0.25)], + i_q_ref=[add_i_sq.input_left[0], dict(text=r'$i^{*}_{\mathrm{sq}}$', distance_y=0.25)], + epsilon=[box_d_dt.input_right[0], dict()]) + outputs = dict(S=pwm.output_right, omega=con_omega[0].points[1]) # Outputs of the stage + # Connections to other lines + connect_to_line = dict(epsilon=[alpha_beta_to_dq.input_bottom[0], dict(text=r'$\varepsilon_{\mathrm{el}}$', + text_position='middle', + text_align='right')], + i=[abc_to_alpha_beta.input_right, dict(radius=0.1, fill=False, + text=[r'$\mathbf{i}_{\mathrm{s a,b,c}}$', '', + ''])]) + connections = dict() # Connections + + return start, inputs, outputs, connect_to_line, connections + + return cc_synrm diff --git a/gem_controllers/block_diagrams/stage_blocks/synrm_output.py b/gem_controllers/block_diagrams/stage_blocks/synrm_output.py new file mode 100644 index 00000000..06415c3a --- /dev/null +++ b/gem_controllers/block_diagrams/stage_blocks/synrm_output.py @@ -0,0 +1,43 @@ +from control_block_diagram.components import Connection +from control_block_diagram.predefined_components import SynRM, DcConverter + + +def synrm_output(emf_feedforward): + """ + Args: + emf_feedforward: Boolean whether emf feedforward stage is included + + Returns: + Function to build the PMSM output block + """ + + def _synrm_output(start, control_task): + """ + Function to build the PMSM output block + Args: + start: Starting point of the block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + # Converter with DC input voltage + converter = DcConverter(start.add_x(2.7), input_number=3, input_space=0.3, output_number=3) + + # SynRM block + synrm = SynRM(converter.position.sub_y(5), size=1.3, input='top') + + # Connection between the converter and the motor block + con_1 = Connection.connect(converter.output_bottom, synrm.input_top, arrow=False) + + start = synrm.position # starting point of the next block + # Inputs of the stage + inputs = dict(S=[converter.input_left, dict(text=[r'$\mathbf{S}_{\mathrm{a,b,c}}$', '', ''], distance_y=0.25)]) + outputs = dict(epsilon=synrm.output_left[0]) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict(i=con_1) # Connections + + return start, inputs, outputs, connect_to_lines, connections + + return _synrm_output diff --git a/gem_controllers/cascaded_controller.py b/gem_controllers/cascaded_controller.py new file mode 100644 index 00000000..a3419feb --- /dev/null +++ b/gem_controllers/cascaded_controller.py @@ -0,0 +1,21 @@ +import gem_controllers as gc + + +class CascadedController(gc.GemController): + """The CascadedController class contains a controller with multiple hierarchically structured stages.""" + + def control(self, state, reference): + """ + The function iterates through all the stages to calculate the action. + + Args: + state(np.array): Array that contains the actual state of the environment + reference(np.array): Array that contains the actual references of the referenced states + + Returns: + action + """ + + for stage in self._stages: + reference = stage(state, reference) + return reference diff --git a/gem_controllers/current_controller.py b/gem_controllers/current_controller.py new file mode 100644 index 00000000..c548f369 --- /dev/null +++ b/gem_controllers/current_controller.py @@ -0,0 +1,20 @@ +import numpy as np +import gem_controllers as gc + + +class CurrentController(gc.GemController): + """Base class for a current controller""" + + def control(self, state, reference): + raise NotImplementedError + + def tune(self, env, env_id, **kwargs): + raise NotImplementedError + + @property + def voltage_reference(self) -> np.ndarray: + raise NotImplementedError + + @property + def t_n(self) -> np.ndarray: + raise NotImplementedError diff --git a/gem_controllers/gem_adapter.py b/gem_controllers/gem_adapter.py new file mode 100644 index 00000000..2a8ca610 --- /dev/null +++ b/gem_controllers/gem_adapter.py @@ -0,0 +1,126 @@ +import gym_electric_motor as gem +import numpy as np + +import gem_controllers as gc + + +class GymElectricMotorAdapter(gc.GemController): + """The GymElectricMotorAdapter wraps a GemController to map the inputs and outputs to the environment.""" + + @property + def input_stage(self): + """Input stage of the controller""" + return self._input_stage + + @input_stage.setter + def input_stage(self, value): + self._input_stage = value + + @property + def output_stage(self): + """Output stage of the controller""" + return self._output_stage + + @output_stage.setter + def output_stage(self, value): + self._output_stage = value + + @property + def controller(self): + """Wrapped GemController""" + return self._controller + + @controller.setter + def controller(self, value): + self._controller = value + + @property + def block_diagram(self): + return self._block_diagram + + def __init__( + self, + _env: (gem.core.ElectricMotorEnvironment, None) = None, + env_id: (str, None) = None, + controller: (gc.GemController, None) = None, + ): + """ + Args: + _env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + env_id(str): The corresponding environment-id to specify the concrete environment. + controller(gc.GemController): The GemController that should be wrapped. + """ + + super().__init__() + self._input_stage = None + self._output_stage = None + assert isinstance(controller, gc.GemController) + self._controller = controller + self._input_stage = gc.stages.InputStage() + action_type = gc.utils.get_action_type(env_id) + if action_type == "Finite": + self._output_stage = gc.stages.DiscOutputStage() + else: + self._output_stage = gc.stages.ContOutputStage() + self._block_diagram = None + self._reference_plotter = gc.ReferencePlotter() + + def control(self, state, reference): + """ + Function to calculate the action of the controller for the environment. + + Args: + state(np.array): Array of the state of the environment. + reference(np.array): Array of the references of the referenced states. + + Returns: + action + """ + + # Copy state and reference to be independent from further calculations + state_, reference_ = np.copy(state), np.copy(reference) + + # Denormalize the state and reference + denormalized_ref = self._input_stage(state_, reference_) + + # Iterate through the controller stages to calculate the input voltages + voltage_set_point = self._controller.control(state_, denormalized_ref) + + # Transform and normalize the input voltages + action = self._output_stage(state_, voltage_set_point) + if self.should_plot: + self._reference_plotter.update_plots(self._controller.references) + return action + + def tune(self, env, env_id, tune_controller=True, **kwargs): + """ + Function to set the parameters of the controller stages. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be tuned for. + env_id(str): ID of the ElectricMotorEnvironment. + tune_controller(bool): Flag, if the controller should be tuned. + """ + + self._input_stage.tune(env, env_id) + self._output_stage.tune(env, env_id) + if tune_controller: + self._controller.tune(env, env_id, **kwargs) + + self._reference_plotter.tune( + env, + self._controller.referenced_states, + plot_references=True, + maximum_reference=self._controller.maximum_reference, + ) + + def build_block_diagram(self, env_id, save_block_diagram_as): + self._block_diagram = gc.build_block_diagram( + self, env_id, save_block_diagram_as + ) + + def reset(self): + """Reset all stages of the controller.""" + self._input_stage.reset() + self._controller.reset() + self._output_stage.reset() diff --git a/gem_controllers/gem_controller.py b/gem_controllers/gem_controller.py new file mode 100644 index 00000000..d0edf3aa --- /dev/null +++ b/gem_controllers/gem_controller.py @@ -0,0 +1,177 @@ +import gym_electric_motor.core + +import gem_controllers as gc +import numpy as np + + +class GemController: + """The GemController is the base for all motor controllers in the gem-control package. + + A GemController consists of multiple stages that execute different control tasks like speed-control, a reference + to current set point mapping or input and output processing. + + Furthermore, the GemController has got a `GemController.make` factory function that automatically designs and tunes + a classical cascaded motor controller based on classic control techniques like the proportional-integral (PI) + controller to control a gym-electric-motor environment. + """ + + @property + def signals(self): + """Input signals of the controller""" + return [] + + @property + def signal_names(self): + """Signal names of the controller""" + return [] + + @classmethod + def make( + cls, + env: gym_electric_motor.core.ElectricMotorEnvironment, + env_id: str, + decoupling: bool = True, + current_safety_margin: float = 0.2, + base_current_controller: str = "PI", + base_speed_controller: str = "PI", + a: int = 4, + should_plot: bool = False, + plot_references: bool = True, + block_diagram: bool = True, + save_block_diagram_as: (str, tuple) = None, + ): + """A factory function that generates (and parameterizes) a matching GemController for a given gym-electric-motor + environment `env`. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + env_id(str): The corresponding environment-id to specify the concrete environment. + decoupling(bool): Flag, if a EMF-Feedforward correction stage should be used in the pi current controller. + current_safety_margin(float in [0..1]): The ratio between the maximum current set point + the reference controller generates and the absolute current limit. + base_speed_controller('PI'/'PID'/'P'/'ThreePoint'): Selection of the basic control algorithm for the + speed controller. + base_current_controller('PI'/'PID'/'P'/'ThreePoint'): Selection of the basic control algorithm for the + current controller. + a(float): Tuning parameter of the symmetrical optimum. + plot_references(bool): Flag, if the reference values of the underlying control circuits should be plotted + block_diagram(bool): Selection whether the block diagram should be displayed + save_block_diagram_as(str, tuple): Selection of whether the block diagram should be saved + + Returns: + GemController: An initialized (and tuned) instance of a controller that fits to the specified environment. + """ + cls.should_plot = should_plot + + control_task = gc.utils.get_control_task(env_id) + tuner_kwargs = dict() + + # Initialize the current control stage + controller = gc.PICurrentController( + env, + env_id, + base_current_controller=base_current_controller, + decoupling=decoupling, + ) + tuner_kwargs["a"] = a + tuner_kwargs["plot_references"] = plot_references + if control_task in ["TC", "SC"]: + # Initilize the operation point selection + controller = gc.TorqueController(env, env_id, current_controller=controller) + tuner_kwargs["current_safety_margin"] = current_safety_margin + if control_task == "SC": + # Initilize the speed control stage + controller = gc.PISpeedController( + env, + env_id, + torque_controller=controller, + base_speed_controller=base_speed_controller, + ) + # Wrap the controller with the adapter to map the inputs and outputs to the environment + controller = gc.GymElectricMotorAdapter(env, env_id, controller) + + # Fit the controllers parameters to the environment + controller.tune(env, env_id, **tuner_kwargs) + + if block_diagram: + controller.build_block_diagram(env_id, save_block_diagram_as) + + return controller + + @property + def stages(self): + """Stages of the GEM Controller""" + return self._stages + + def __init__(self): + self._stages = [] + + def get_signal_value(self, signal_name): + """ + Get the value of a signal calling by the signal name. + + Args: + signal_name(str): Name of a signal of the state + + Returns: + float + + """ + + return self.signals[self.signal_names.index(signal_name)] + + def control(self, state, reference): + """ + Calculate the voltage reference. + + Args: + state(np.array): state of the environment + reference(np.array): speed references + + Returns: + np.array: reference voltage + """ + raise NotImplementedError + + def reset(self): + """Reset all stages of the controller""" + for stage in self._stages: + stage.reset() + + def tune(self, env, env_id, **kwargs): + pass + + def control_environment( + self, env, n_steps, max_episode_length=np.inf, render_env=False + ): + """ + Function to control an environment with the GemController. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller should control. + n_steps(int): Number of iteration steps. + max_episode_length(int): Maximum length of an epsiode, after which the environment and controller should be + reset. + render_env(bool): Flag, if the states of the environment should be plotted. + """ + + state, reference = env.reset() + if self.block_diagram: + self.block_diagram.open() + self.reset() + current_episode_length = 0 + for _ in range(n_steps): # Simulate the environment and controller for n steps + if render_env: + env.render() # Plot the states + action = self.control(state, reference) # Calculate the action + (state, reference), _, done, _ = env.step( + action + ) # Simulate one step of the environment + if done or current_episode_length >= max_episode_length: + # Reset the environment and controller + state, reference = env.reset() + self.reset() + current_episode_length = 0 + current_episode_length = current_episode_length + 1 + if self.block_diagram: + self.block_diagram.close() diff --git a/gem_controllers/parameter_reader.py b/gem_controllers/parameter_reader.py new file mode 100644 index 00000000..40f33c6f --- /dev/null +++ b/gem_controllers/parameter_reader.py @@ -0,0 +1,358 @@ +from gym_electric_motor.physical_systems import converters as cv + +import numpy as np + +dc_motors = ['SeriesDc', 'ShuntDc', 'PermExDc', 'ExtExDc'] +synchronous_motors = ['PMSM', 'SynRM', 'EESM'] +induction_motors = ['DFIM', 'SCIM'] +ac_motors = synchronous_motors + induction_motors + +control_tasks_ = ['CC', 'TC', 'SC'] +dc_actions = ['Finite', 'Cont'] +ac_actions = ['Finite', 'DqCont', 'AbcCont'] + + +psi_reader = { + 'SeriesDc': lambda env: np.array([0.0]), + 'ShuntDc': lambda env: np.array([0.0]), + 'PermExDc': lambda env: np.array([env.physical_system.electrical_motor.motor_parameter['psi_e']]), + 'ExtExDc': lambda env: np.array([0.0, 0.0]), + 'PMSM': lambda env: np.array([0.0, env.physical_system.electrical_motor.motor_parameter['psi_p']]), + 'SynRM': lambda env: np.array([0.0, 0.0]), + 'SCIM': lambda env: np.array([0.0, 0.0]), + 'EESM': lambda env: np.array([0.0, 0.0, 0.0]), +} + +p_reader = { + 'SeriesDc': lambda env: 1, + 'ShuntDc': lambda env: 1, + 'ExtExDc': lambda env: 0, + 'PermExDc': lambda env: 0, + 'PMSM': lambda env: env.physical_system.electrical_motor.motor_parameter['p'], + 'SynRM': lambda env: env.physical_system.electrical_motor.motor_parameter['p'], + 'SCIM': lambda env: env.physical_system.electrical_motor.motor_parameter['p'], + 'EESM': lambda env: env.physical_system.electrical_motor.motor_parameter['p'], +} + +l_reader = { + 'SeriesDc': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['l_a'] + + env.physical_system.electrical_motor.motor_parameter['l_e'] + ]), + 'ShuntDc': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['l_a'], + ]), + 'ExtExDc': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['l_a'], + env.physical_system.electrical_motor.motor_parameter['l_e'] + ]), + 'PermExDc': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['l_a'], + ]), + 'PMSM': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['l_d'], + env.physical_system.electrical_motor.motor_parameter['l_q'] + ]), + 'SynRM': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['l_d'], + env.physical_system.electrical_motor.motor_parameter['l_q'] + ]), + 'SCIM': lambda env: np.array([ + (env.physical_system.electrical_motor.motor_parameter['l_sigr'] + + env.physical_system.electrical_motor.motor_parameter['l_m']) / + env.physical_system.electrical_motor.motor_parameter['r_r'], + (env.physical_system.electrical_motor.motor_parameter['l_sigr'] + + env.physical_system.electrical_motor.motor_parameter['l_m']) / + env.physical_system.electrical_motor.motor_parameter['r_r'], + ]), + 'EESM': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['l_d'], + env.physical_system.electrical_motor.motor_parameter['l_q'], + env.physical_system.electrical_motor.motor_parameter['l_e'] + ]), +} + +l_emf_reader = { + 'SeriesDc': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['l_e_prime'] + ]), + 'ShuntDc': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['l_e_prime'], + ]), + 'ExtExDc': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['l_e_prime'], + 0.0 + ]), + 'PermExDc': lambda env: np.array([0.0]), + 'PMSM': lambda env: np.array([ + - env.physical_system.electrical_motor.motor_parameter['l_q'], + env.physical_system.electrical_motor.motor_parameter['l_d'], + ]), + 'SynRM': lambda env: np.array([ + - env.physical_system.electrical_motor.motor_parameter['l_q'], + env.physical_system.electrical_motor.motor_parameter['l_d'], + ]), + 'SCIM': lambda env: np.array([ + -(env.physical_system.electrical_motor.motor_parameter['l_sigs'] * + env.physical_system.electrical_motor.motor_parameter['l_sigr'] + + env.physical_system.electrical_motor.motor_parameter['l_sigs'] * + env.physical_system.electrical_motor.motor_parameter['l_m'] + + env.physical_system.electrical_motor.motor_parameter['l_sigr'] * + env.physical_system.electrical_motor.motor_parameter['l_m']) / + (env.physical_system.electrical_motor.motor_parameter['l_sigr'] + + env.physical_system.electrical_motor.motor_parameter['l_m']), + (env.physical_system.electrical_motor.motor_parameter['l_sigs'] * + env.physical_system.electrical_motor.motor_parameter['l_sigr'] + + env.physical_system.electrical_motor.motor_parameter['l_sigs'] * + env.physical_system.electrical_motor.motor_parameter['l_m'] + + env.physical_system.electrical_motor.motor_parameter['l_sigr'] * + env.physical_system.electrical_motor.motor_parameter['l_m']) / + (env.physical_system.electrical_motor.motor_parameter['l_sigr'] + + env.physical_system.electrical_motor.motor_parameter['l_m']) + ]), + 'EESM': lambda env: np.array([ + - env.physical_system.electrical_motor.motor_parameter['l_q'], + env.physical_system.electrical_motor.motor_parameter['l_d'], + env.physical_system.electrical_motor.motor_parameter['l_m'] * + env.physical_system.electrical_motor.motor_parameter['l_q'] / + env.physical_system.electrical_motor.motor_parameter['l_d'], + ]), +} + +tau_current_loop_reader = { + 'SeriesDc': lambda env: np.array([( + env.physical_system.electrical_motor.motor_parameter['l_e'] + + env.physical_system.electrical_motor.motor_parameter['l_a'] + ) / ( + env.physical_system.electrical_motor.motor_parameter['r_e'] + + env.physical_system.electrical_motor.motor_parameter['r_a'] + ) + ]), + 'ShuntDc': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['l_a'] + / env.physical_system.electrical_motor.motor_parameter['r_a'] + ]), + 'ExtExDc': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['l_a'] + / env.physical_system.electrical_motor.motor_parameter['r_a'], + env.physical_system.electrical_motor.motor_parameter['l_e'] + / env.physical_system.electrical_motor.motor_parameter['r_e'] + ]), + 'PermExDc': lambda env: np.array( + env.physical_system.electrical_motor.motor_parameter['l_a'] + / env.physical_system.electrical_motor.motor_parameter['r_a'] + ), + 'PMSM': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['l_q'] + / env.physical_system.electrical_motor.motor_parameter['r_s'], + env.physical_system.electrical_motor.motor_parameter['l_d'] + / env.physical_system.electrical_motor.motor_parameter['r_s'] + ]), + 'SynRM': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['l_q'] + / env.physical_system.electrical_motor.motor_parameter['r_s'], + env.physical_system.electrical_motor.motor_parameter['l_d'] + / env.physical_system.electrical_motor.motor_parameter['r_s'] + ]), + 'SCIM': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['l_sigs'] + / env.physical_system.electrical_motor.motor_parameter['r_s'], + env.physical_system.electrical_motor.motor_parameter['l_sigr'] + / env.physical_system.electrical_motor.motor_parameter['r_r'], + ]), + 'EESM': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['l_q'] + / env.physical_system.electrical_motor.motor_parameter['r_s'], + env.physical_system.electrical_motor.motor_parameter['l_d'] + / env.physical_system.electrical_motor.motor_parameter['r_s'], + env.physical_system.electrical_motor.motor_parameter['l_e'] + / env.physical_system.electrical_motor.motor_parameter['r_e'] + ]), +} + +r_reader = { + 'SeriesDc': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['r_a'] + + env.physical_system.electrical_motor.motor_parameter['r_e'] + ]), + 'ShuntDc': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['r_a'], + ]), + 'ExtExDc': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['r_a'], + env.physical_system.electrical_motor.motor_parameter['r_e'] + ]), + 'PermExDc': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['r_a'], + ]), + 'PMSM': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['r_s'], + env.physical_system.electrical_motor.motor_parameter['r_s'] + ]), + 'SynRM': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['r_s'], + env.physical_system.electrical_motor.motor_parameter['r_s'] + ]), + 'SCIM': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['r_s'], + env.physical_system.electrical_motor.motor_parameter['r_r'] + ]), + 'EESM': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['r_s'], + env.physical_system.electrical_motor.motor_parameter['r_s'], + env.physical_system.electrical_motor.motor_parameter['r_e'] + ]), +} + +tau_n_reader = { + 'SeriesDc': lambda env: np.array([ + (env.physical_system.electrical_motor.motor_parameter['r_a'] + + env.physical_system.electrical_motor.motor_parameter['r_e']) + / (env.physical_system.electrical_motor.motor_parameter['l_a'] + + env.physical_system.electrical_motor.motor_parameter['l_e']) + ]), + 'ShuntDc': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['r_a'] + / env.physical_system.electrical_motor.motor_parameter['l_a'], + ]), + 'ExtExDc': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['r_a'] + / env.physical_system.electrical_motor.motor_parameter['l_a'], + env.physical_system.electrical_motor.motor_parameter['r_e'] + / env.physical_system.electrical_motor.motor_parameter['l_e'] + ]), + 'PermExDc': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['r_a'] + / env.physical_system.electrical_motor.motor_parameter['l_a'], + ]), + 'PMSM': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['r_s'] + / env.physical_system.electrical_motor.motor_parameter['l_d'], + env.physical_system.electrical_motor.motor_parameter['r_s'] + / env.physical_system.electrical_motor.motor_parameter['l_q'] + ]), + 'SynRM': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['r_s'] + / env.physical_system.electrical_motor.motor_parameter['l_d'], + env.physical_system.electrical_motor.motor_parameter['r_s'] + / env.physical_system.electrical_motor.motor_parameter['l_q'] + ]), + 'SCIM': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['r_s'] + / env.physical_system.electrical_motor.motor_parameter['l_sigs'], + env.physical_system.electrical_motor.motor_parameter['r_r'] + / env.physical_system.electrical_motor.motor_parameter['l_sigr'] + ]), + 'EESM': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['r_s'] + / env.physical_system.electrical_motor.motor_parameter['l_d'], + env.physical_system.electrical_motor.motor_parameter['r_s'] + / env.physical_system.electrical_motor.motor_parameter['l_q'], + env.physical_system.electrical_motor.motor_parameter['r_e'] + / env.physical_system.electrical_motor.motor_parameter['l_e'] + ]), +} + +currents = { + 'SeriesDc': ['i'], + 'ShuntDc': ['i_a'], + 'ExtExDc': ['i_a', 'i_e'], + 'PermExDc': ['i'], + 'PMSM': ['i_sd', 'i_sq'], + 'SynRM': ['i_sd', 'i_sq'], + 'SCIM': ['i_sd', 'i_sq'], + 'EESM': ['i_sd', 'i_sq', 'i_e'], +} +emf_currents = { + 'SeriesDc': ['i'], + 'ShuntDc': ['i_e'], + 'ExtExDc': ['i_e', 'i_a'], + 'PermExDc': ['i'], + 'PMSM': ['i_sq', 'i_sd'], + 'SynRM': ['i_sq', 'i_sd'], + 'SCIM': ['i_sq', 'i_sd'], + 'EESM': ['i_sq', 'i_sd', 'i_sq'], +} + + +voltages = { + 'SeriesDc': ['u'], + 'ShuntDc': ['u'], + 'ExtExDc': ['u_a', 'u_e'], + 'PermExDc': ['u'], + 'PMSM': ['u_sd', 'u_sq'], + 'SynRM': ['u_sd', 'u_sq'], + 'SCIM': ['u_sd', 'u_sq'], + 'EESM': ['u_sd', 'u_sq', 'u_e'], +} + + +def get_output_voltages(motor_type, action_type): + if motor_type in dc_motors: + return voltages[motor_type] + elif motor_type in dc_motors: + return voltages[motor_type] + elif motor_type in dc_motors: + return voltages[motor_type] + elif motor_type in induction_motors: + return ['u_sa', 'u_sb', 'u_sc'] + elif motor_type == 'EESM': + return ['u_a', 'u_b', 'u_c', 'u_sup'] + else: + return ['u_a', 'u_b', 'u_c'] + + +l_prime_reader = { + 'SeriesDc': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['l_e_prime'] + ]), + 'ShuntDc': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['l_e_prime'] + ]), + 'ExtExDc': lambda env: np.array([ + env.physical_system.electrical_motor.motor_parameter['l_e_prime'] + ]), + 'PermExDc': lambda env: np.array([0.0]), + 'PMSM': lambda env: np.array([0.0, 0.0]), + 'SynRM': lambda env: np.array([ + - env.physical_system.electrical_motor.motor_parameter['l_sq'], + env.physical_system.electrical_motor.motor_parameter['l_sd'] + ]), + 'SCIM': lambda env: np.array([0, 0]), + 'EESM': lambda env: np.array([0.0, 0.0, 0.0]), +} + +converter_high_idle_low_action = { + cv.FiniteFourQuadrantConverter: (1, 0, 2), + cv.FiniteTwoQuadrantConverter: (1, 0, 2), + cv.FiniteOneQuadrantConverter: (1, 0, 0), + cv.FiniteB6BridgeConverter: ((1, 0, 2),) * 3, +} + + +class MotorSpecification: + + _motors = dict() + + @staticmethod + def register(motor_key): + def reg(motor_specification): + assert isinstance(motor_specification, MotorSpecification) + assert motor_key not in MotorSpecification._motors.keys() + MotorSpecification._motors[motor_key] = motor_specification + return reg + + @staticmethod + def get(motor_key): + return MotorSpecification._motors[motor_key] + + psi = None + l = None + l_emf = None + tau_current_loop = None + tau_n = None + r = None + l_prime = None + currents = None + emf_currents = None + voltages = None diff --git a/gem_controllers/pi_current_controller.py b/gem_controllers/pi_current_controller.py new file mode 100644 index 00000000..849b7530 --- /dev/null +++ b/gem_controllers/pi_current_controller.py @@ -0,0 +1,200 @@ +import numpy as np + +import gem_controllers as gc + + +class PICurrentController(gc.CurrentController): + """This class forms the PI current controller, for any motor.""" + + @property + def signal_names(self): + """Signal names of the calculated values.""" + return ['u_PI', 'u_ff', 'u_out'] + + @property + def transformation_stage(self): + """Coordinate transformation stage at the output""" + return self._transformation_stage + + @property + def current_base_controller(self) -> gc.stages.BaseController: + """Base controller for the current control""" + return self._current_base_controller + + @current_base_controller.setter + def current_base_controller(self, value: gc.stages.BaseController): + assert isinstance(value, gc.stages.BaseController) + self._current_base_controller = value + + @property + def emf_feedforward(self) -> gc.stages.EMFFeedforward: + """EMF feedforward stage of the current controller""" + return self._emf_feedforward + + @emf_feedforward.setter + def emf_feedforward(self, value: gc.stages.EMFFeedforward): + assert isinstance(value, gc.stages.EMFFeedforward) + self._emf_feedforward = value + + @property + def stages(self): + """List of the stages up to the current controller""" + stages_ = [self._current_base_controller] + if self._decoupling: + stages_.append(self._emf_feedforward) + if self._coordinate_transformation_required: + stages_.append(self._transformation_stage) + stages_.append(self._clipping_stage) + return stages_ + + @property + def voltage_reference(self) -> np.ndarray: + """Reference values for the input voltages""" + return self._voltage_reference + + @property + def clipping_stage(self): + """Clipping stage of the current controller""" + return self._clipping_stage + + @property + def t_n(self): + """Time constant of the current controller""" + if hasattr(self._current_base_controller, 'p_gain') \ + and hasattr(self._current_base_controller, 'i_gain'): + return self._current_base_controller.p_gain / self._current_base_controller.i_gain + else: + return self._tau_current_loop + + @property + def references(self): + """Reference values of the current control stage.""" + return dict() + + @property + def referenced_states(self): + """Referenced states of the current control stage.""" + return np.array([]) + + @property + def maximum_reference(self): + return dict() + + def __init__(self, env, env_id, base_current_controller='PI', decoupling=True): + """ + Initilizes a PI current control stage. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + env_id(str): The corresponding environment-id to specify the concrete environment. + base_current_controller(str): Selection which base controller should be used for the current control stage. + decoupling(bool): Flag, if a EMF-Feedforward correction stage should be used in the PI current controller. + """ + + super().__init__() + self._current_base_controller = None + self._emf_feedforward = None + self._transformation_stage = None + self._tau_current_loop = np.array([0.0]) + self._coordinate_transformation_required = False + self._decoupling = decoupling + self._voltage_reference = np.array([]) + self._transformation_stage = gc.stages.AbcTransformation() + + # Choose the emf feedforward function + if gc.utils.get_motor_type(env_id) in gc.parameter_reader.induction_motors: + self._emf_feedforward = gc.stages.EMFFeedforwardInd() + elif gc.utils.get_motor_type(env_id) == 'EESM': + self._emf_feedforward = gc.stages.EMFFeedforwardEESM() + else: + self._emf_feedforward = gc.stages.EMFFeedforward() + + # Choose the clipping function + if gc.utils.get_motor_type(env_id) == 'EESM': + self._clipping_stage = gc.stages.clipping_stages.CombinedClippingStage('CC') + elif gc.utils.get_motor_type(env_id) in gc.parameter_reader.ac_motors: + self._clipping_stage = gc.stages.clipping_stages.SquaredClippingStage('CC') + else: + self._clipping_stage = gc.stages.clipping_stages.AbsoluteClippingStage('CC') + self._anti_windup_stage = gc.stages.AntiWindup('CC') + self._current_base_controller = gc.stages.base_controllers.get(base_current_controller)('CC') + + def tune(self, env, env_id, a=4, **kwargs): + """ + Tune the components of the current control stage. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + env_id(str): The corresponding environment-id to specify the concrete environment. + a(float): Design parameter of the symmetric optimum for the base controllers + """ + + action_type = gc.utils.get_action_type(env_id) + motor_type = gc.utils.get_motor_type(env_id) + if action_type in ['Finite', 'Cont'] and motor_type in gc.parameter_reader.ac_motors: + self._coordinate_transformation_required = True + if self._coordinate_transformation_required: + self._transformation_stage.tune(env, env_id) + self._emf_feedforward.tune(env, env_id) + self._current_base_controller.tune(env, env_id, a) + self._anti_windup_stage.tune(env, env_id) + self._clipping_stage.tune(env, env_id) + self._voltage_reference = np.zeros( + len(gc.parameter_reader.voltages[gc.utils.get_motor_type(env_id)]), dtype=float + ) + self._tau_current_loop = gc.parameter_reader.tau_current_loop_reader[motor_type](env) + + def current_control(self, state, current_reference): + """ + Calculate the input voltages. + + Args: + state(np.array): state of the environment + current_reference(np.array): current references + + Returns: + np.array: voltage references + """ + + # Calculate the voltage reference by the base controllers + voltage_reference = self._current_base_controller(state, current_reference) + + # Decouple the voltage components + if self._decoupling: + voltage_reference = self._emf_feedforward(state, voltage_reference) + + # Clip the voltage inputs to the action space + self._voltage_reference = self._clipping_stage(state, voltage_reference) + + # Transform the voltages in the correct coordinate system + if self._coordinate_transformation_required: + voltage_reference = self._transformation_stage(state, voltage_reference) + + # Integrate the I-Controllers + if hasattr(self._current_base_controller, 'integrator'): + delta = self._anti_windup_stage( + state, current_reference, self._clipping_stage.clipping_difference + ) + self._current_base_controller.integrator += delta + + return voltage_reference + + def control(self, state, reference): + """ + Claculate the reference values for the input voltages. + + Args: + state(np.array): actual state of the environment + reference(np.array): current references + + Returns: + np.ndarray: voltage references + """ + + self._voltage_reference = self.current_control(state, reference) + return self._voltage_reference + + def reset(self): + """Reset all components of the stage""" + for stage in self.stages: + stage.reset() diff --git a/gem_controllers/pi_speed_controller.py b/gem_controllers/pi_speed_controller.py new file mode 100644 index 00000000..d04392e4 --- /dev/null +++ b/gem_controllers/pi_speed_controller.py @@ -0,0 +1,147 @@ +import numpy as np + +import gym_electric_motor as gem +import gem_controllers as gc + + +class PISpeedController(gc.GemController): + """This class forms the PI speed controller, for any motor.""" + + @property + def speed_control_stage(self) -> gc.stages.BaseController: + """Base controller of the speed controller stage""" + return self._speed_control_stage + + @speed_control_stage.setter + def speed_control_stage(self, value: gc.stages.BaseController): + self._speed_control_stage = value + + @property + def torque_controller(self) -> gc.TorqueController: + """Subordinated torque controller stage""" + return self._torque_controller + + @torque_controller.setter + def torque_controller(self, value: gc.TorqueController): + self._torque_controller = value + + @property + def torque_reference(self) -> np.ndarray: + """Reference values of the torque controller stage""" + return self._torque_reference + + @property + def anti_windup_stage(self): + """Anti windup stage of the speed controller""" + return self._anti_windup_stage + + @property + def clipping_stage(self): + """Clipping stage of the speed controller""" + return self._clipping_stage + + @property + def references(self): + refs = self._torque_controller.references + refs.update(dict(torque=self._torque_reference[0])) + return refs + + @property + def referenced_states(self): + return np.append(self._torque_controller.referenced_states, 'torque') + + @property + def maximum_reference(self): + return self._torque_controller.maximum_reference + + def __init__( + self, + _env: (gem.core.ElectricMotorEnvironment, None) = None, + env_id: (str, None) = None, + torque_controller: (gc.TorqueController, None) = None, + base_speed_controller: str = 'PI' + ): + """ + Initilizes a PI speed control stage. + + Args: + _env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + env_id(str): The corresponding environment-id to specify the concrete environment. + torque_controller(gc.TorqueController): The underlying torque control stage + base_speed_controller(str): Selection which base controller should be used for the speed control stage. + """ + + super().__init__() + self._speed_control_stage = gc.stages.base_controllers.get(base_speed_controller)('SC') + self._torque_controller = torque_controller + if torque_controller is None: + self._torque_controller = gc.TorqueController() + self._torque_reference = np.array([]) + self._anti_windup_stage = gc.stages.AntiWindup('SC') + self._clipping_stage = gc.stages.clipping_stages.AbsoluteClippingStage('SC') + + def tune(self, env, env_id, tune_torque_controller=True, a=4, **kwargs): + """ + Tune the components of the current control stage. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + env_id(str): The corresponding environment-id to specify the concrete environment. + tune_torque_controller(bool): Flag, if the underlying torque control stage should be tuned. + a(float): Design parameter of the symmetric optimum for the base controllers + """ + + if tune_torque_controller: + self._torque_controller.tune(env, env_id, a=a, **kwargs) + self._anti_windup_stage.tune(env, env_id) + self._clipping_stage.tune(env, env_id) + t_n = min(self._torque_controller.t_n) # Get the time constant of the torque control stage + self._speed_control_stage.tune(env, env_id, t_n=t_n, a=a) + + def speed_control(self, state, reference): + """ + Calculate the torque reference. + + Args: + state(np.array): actual state of the environment + reference(np.array): actual speed references + + Returns: + torque_reference(np.array) + """ + + # Calculate the torque reference by the base controller + torque_reference = self._speed_control_stage(state, reference) + + # Clipping the torque reference and integrating the I-Controller + self._torque_reference = self._clipping_stage(state, torque_reference) + if hasattr(self._speed_control_stage, 'integrator'): + delta = self._anti_windup_stage.__call__(state, reference, self._clipping_stage.clipping_difference) + self._speed_control_stage.integrator += delta + + return self._torque_reference + + def control(self, state, reference): + """ + Claculate the reference values for the input voltages. + + Args: + state(np.array): actual state of the environment + reference(np.array): speed references + + Returns: + np.ndarray: voltage reference + """ + + # Calculate the torque reference + reference = self.speed_control(state, reference) + + # Calculate the references of the underlying stages + reference = self._torque_controller.control(state, reference) + + return reference + + def reset(self): + """Reset all components of the speed control stage and the underlying control stages""" + self._torque_controller.reset() + self._speed_control_stage.reset() diff --git a/gem_controllers/reference_plotter.py b/gem_controllers/reference_plotter.py new file mode 100644 index 00000000..995a64f7 --- /dev/null +++ b/gem_controllers/reference_plotter.py @@ -0,0 +1,62 @@ +from gym_electric_motor.visualization.motor_dashboard import StatePlot + + +class ReferencePlotter: + """This class adds the reference values of the subordinate stages to the stage plots of the GEM environment.""" + def __init__(self): + self._referenced_plots = [] + self._referenced_states = [] + self._maximum_reference = None + self._plot_references = None + self._maximum_reference_set = False + + def tune(self, env, referenced_states, plot_references, maximum_reference, **_): + """ + Tune the reference plotter. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + referenced_states(np.ndarray): Array of all referenced states. + plot_references(bool): Flag, if the references of the subordinate stages should be plotted. + maximum_reference(dict): Dict containing all limited reference currents. + + """ + if plot_references: + for visualization in env.visualizations: + for time_plot in visualization._time_plots: + if isinstance(time_plot, StatePlot): + if time_plot.state in referenced_states: + self._referenced_plots.append(time_plot) + self._referenced_states.append(time_plot.state) + + for plot in self._referenced_plots: + plot._referenced = True + + self._maximum_reference = maximum_reference + + def add_maximum_reference(self, state, value): + self._maximum_reference[state] = value + + def update_plots(self, references): + """ + Update the state plots of the GEM environment. + + Args: + references(np.ndarray): Array of all reference values of the subordinate stages. + """ + + if not self._maximum_reference_set: + for plot in self._referenced_plots: + if plot.state in ['i_e', 'i_a', 'i'] and plot.state in self._maximum_reference.keys(): + label = dict(i='$i^*$$_{\mathrm{ max}}$', i_a='$i^*_{a}$$_\mathrm{ max}}$', + i_e='$i^*_{e}$$_\mathrm{ max}}$') + plot._axis.axhline(self._maximum_reference[plot.state][0], c='g', linewidth=0.75, linestyle='--') + plot._axis.axhline(self._maximum_reference[plot.state][1], c='g', linewidth=0.75, linestyle='--') + labels = [legend._text for legend in plot._axis.get_legend().texts] + [label[plot.state]] + lines = plot._axis.lines[0:len(labels)-1] + plot._axis.lines[-1:] + plot._axis.legend(lines, labels, loc='upper left', numpoints=20) + + self._maximum_reference_set = True + + for plot, state in zip(self._referenced_plots, self._referenced_states): + plot._ref_data[plot.data_idx] = references[state] diff --git a/gem_controllers/stages/__init__.py b/gem_controllers/stages/__init__.py new file mode 100644 index 00000000..2a11e126 --- /dev/null +++ b/gem_controllers/stages/__init__.py @@ -0,0 +1,17 @@ +from .stage import Stage +from .cont_output_stage import ContOutputStage +from .disc_output_stage import DiscOutputStage +from .abc_transformation import AbcTransformation +from .base_controllers.i_controller import IController +from .base_controllers.p_controller import PController +from .base_controllers.pi_controller import PIController +from .base_controllers.pid_controller import PIDController +from .base_controllers.three_point_controller import ThreePointController +from .base_controllers.base_controller import BaseController +from .emf_feedforward import EMFFeedforward +from .emf_feedforward_ind import EMFFeedforwardInd +from .emf_feedforward_eesm import EMFFeedforwardEESM +from .operation_point_selection import OperationPointSelection, torque_to_current_function +from .input_stage import InputStage +from . import clipping_stages +from .anti_windup import AntiWindup diff --git a/gem_controllers/stages/abc_transformation.py b/gem_controllers/stages/abc_transformation.py new file mode 100644 index 00000000..04a6adb1 --- /dev/null +++ b/gem_controllers/stages/abc_transformation.py @@ -0,0 +1,79 @@ +from gym_electric_motor.physical_systems.electric_motors import SynchronousMotor +import gem_controllers as gc +import numpy as np +from .stage import Stage +from .. import parameter_reader as reader + + +class AbcTransformation(Stage): + """This class calculates the transformation from the dq-coordinate system to the abc-coordinatesystem for + three-phase motors. Optionally, an advanced factor can be added to the angle to take the dead time of the inverter + and the sampling time into account. + """ + + @property + def advance_factor(self): + """Advance factor of the angle.""" + return self._advance_factor + + @advance_factor.setter + def advance_factor(self, value): + self._advance_factor = float(value) + + @property + def tau(self): + """Sampling time of the system.""" + return self._tau + + @tau.setter + def tau(self, value): + self._tau = float(value) + + def __init__(self): + super().__init__() + self._tau = 1e-4 + self._advance_factor = 0.5 + self.omega_idx = None + self.angle_idx = None + self._output_len = None + + def __call__(self, state, reference): + """ + Args: + state(np.array): state of the environment + reference(np.array): voltage reference values + + Returns: + np.array: reference values for the input voltages + """ + + epsilon_adv = self._angle_advance(state) # calculate the advance angle + output = np.zeros(self._output_len) + output[0:3] = SynchronousMotor.t_32(SynchronousMotor.q(reference[0:2], epsilon_adv)) + if self._output_len > 3: + output[3:] = reference[2:] + return output + + def _angle_advance(self, state): + """Multiply the advance factor with the speed and the sampling time to calculate the advance angle""" + return state[self.angle_idx] + self._advance_factor * self.tau * state[self.omega_idx] + + def tune(self, env, env_id, **_): + """ + Tune the advance factor of the transformation. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + env_id(str): The corresponding environment-id to specify the concrete environment. + """ + if gc.utils.get_motor_type(env_id) in gc.parameter_reader.induction_motors: + self.angle_idx = env.state_names.index('psi_angle') + else: + self.angle_idx = env.state_names.index('epsilon') + self.omega_idx = env.state_names.index('omega') + if hasattr(env.physical_system.converter, 'dead_time'): + self._advance_factor = 1.5 if env.physical_system.converter.dead_time else 0.5 + else: + self._advance_factor = 0.5 + action_type, _, motor_type = gc.utils.split_env_id(env_id) + self._output_len = len(reader.get_output_voltages(motor_type, action_type)) diff --git a/gem_controllers/stages/anti_windup.py b/gem_controllers/stages/anti_windup.py new file mode 100644 index 00000000..d5084855 --- /dev/null +++ b/gem_controllers/stages/anti_windup.py @@ -0,0 +1,59 @@ +import numpy as np + +import gem_controllers as gc + + +class AntiWindup: + """This class should prevent a Windup of a the intgration part of the controller. A windup arises when a reference + variable is in the limit and the I controller is still integrated, so that it takes more time for the controlled + variable to go under the limit again. To prevent this, only the I controllers whose controlled variable is below the + limits are integrated. + """ + + def __init__(self, control_task='CC'): + """ + Args: + control_task(str): Control task of the controller. + """ + self._control_task = control_task + self._state_indices = [] + self._tau = 0.0 + + def tune(self, env, env_id): + """ + Tune the anti windup stage. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + env_id(str): The corresponding environment-id to specify the concrete environment. + """ + self._tau = env.physical_system.tau + motor_type = gc.utils.get_motor_type(env_id) + states = [] + if self._control_task == 'CC': + states = gc.parameter_reader.currents[motor_type] + elif self._control_task == 'TC': + states = ['torque'] + elif self._control_task == 'SC': + states = ['omega'] + self._state_indices = [env.state_names.index(state) for state in states] + + def __call__(self, state, reference, clipping_difference): + """Limits the integrative part in the base-controllers. + + If any output of the controller was clipped, the integration on this path is stopped. + + Args: + state(np.ndarray): The state of the environment. + reference(np.ndarray): The reference that was input into the controller to limit. + clipping_difference(np.ndarray): The amount of clipping that was put on the output action of the + controller. + + Returns: + np.ndarray: The amount how much the integrator-value is altered. + """ + + # np.ndarray(bool): Indicates which actions have been clipped + non_clipped = clipping_difference == 0 + error = reference - state[self._state_indices] + return self._tau * error * non_clipped diff --git a/gem_controllers/stages/base_controllers/__init__.py b/gem_controllers/stages/base_controllers/__init__.py new file mode 100644 index 00000000..b100bcbf --- /dev/null +++ b/gem_controllers/stages/base_controllers/__init__.py @@ -0,0 +1,7 @@ +from .base_controller import BaseController +from .pid_controller import PIDController +from .pi_controller import PIController +from .p_controller import PController +from .i_controller import IController +from .three_point_controller import ThreePointController +from .utils import get diff --git a/gem_controllers/stages/base_controllers/base_controller.py b/gem_controllers/stages/base_controllers/base_controller.py new file mode 100644 index 00000000..340e7fec --- /dev/null +++ b/gem_controllers/stages/base_controllers/base_controller.py @@ -0,0 +1,45 @@ +from ..stage import Stage +from.e_base_controller_task import EBaseControllerTask + + +class BaseController(Stage): + """The base controller is the base class for all dynamic control stages like the P-I-D controllers or the + Three-Point controller. + + In contrast to other stages, the base controllers can be used for multiple tasks e.g. a speed control with a + reference as output or a current control with voltages as output. + """ + + def __init__(self, control_task): + """ + Args: + control_task(str): Control task of the base controller. + """ + self._control_task = EBaseControllerTask.get_control_task(control_task) + + def __call__(self, state, reference): + """ + Calculate the reference value of the base controller. + + Args: + state(np.ndarray): The state of the environment. + reference(np.ndarray): The reference of the state. + + Returns: + np.array: reference values of the next stage + """ + raise NotImplementedError + + def tune(self, env, env_id, **base_controller_kwargs): + """ + Tune a base controller. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + env_id(str): The corresponding environment-id to specify the concrete environment. + **base_controller_kwargs: Keyword arguments, that should be passed to a base controller + """ + pass + + def feedback(self, state, reference, clipped_reference): + pass diff --git a/gem_controllers/stages/base_controllers/e_base_controller_task.py b/gem_controllers/stages/base_controllers/e_base_controller_task.py new file mode 100644 index 00000000..5d2d3f1c --- /dev/null +++ b/gem_controllers/stages/base_controllers/e_base_controller_task.py @@ -0,0 +1,30 @@ +from enum import Enum + + +class EBaseControllerTask(Enum): + CC = 'CC' + CurrentControl = 'CC' + TC = 'TC' + TorqueControl = 'TC' + SC = 'SC' + SpeedControl = 'SC' + FC = 'FC' + FluxControl = 'FC' + + @staticmethod + def equals(value, base_controller_task): + if type(value) is str: + return EBaseControllerTask(value) == base_controller_task + else: + return value == base_controller_task + + @staticmethod + def get_control_task(value): + if type(value) is str: + return EBaseControllerTask(value) + elif isinstance(value, EBaseControllerTask): + return value + else: + raise Exception( + f'The type of the control task has to be string or EBaseControllerTask but is {type(value)}.' + ) diff --git a/gem_controllers/stages/base_controllers/i_controller.py b/gem_controllers/stages/base_controllers/i_controller.py new file mode 100644 index 00000000..c0d1cc11 --- /dev/null +++ b/gem_controllers/stages/base_controllers/i_controller.py @@ -0,0 +1,121 @@ +import numpy as np + +from ..base_controllers import BaseController + + +class IController(BaseController): + """This class represents an integration controller, which can be combined e.g. with a proportional controller to a + PI controller. + """ + + # (float): Additional term to avoid division by zero + epsilon = 1e-6 + + @property + def i_gain(self): + """I gain of the I controller""" + return self._i_gain + + @i_gain.setter + def i_gain(self, value): + # i_gain is at least zero, to avoid unstable behavior + value = np.clip(value, 0.0, np.inf) + self._i_gain = value + + @property + def tau(self): + """Sampling time of the system""" + return self._tau + + @tau.setter + def tau(self, value: [float, int]): + self._tau = float(value) + + @property + def action_range(self): + """Action range of the base controller""" + return self._action_range + + @action_range.setter + def action_range(self, value): + self._action_range = value + + @property + def state_indices(self): + """Indices of the controlled states""" + return self._state_indices + + @state_indices.setter + def state_indices(self, value): + self._state_indices = np.array(value) + + @property + def integrator(self): + """Integrated value of the I controller""" + return self._integrator + + @integrator.setter + def integrator(self, value): + self._integrator = value + + def __init__(self, control_task): + """ + Args: + control_task(str): Control task of the base controller + """ + + super().__init__(control_task) + self._state_indices = np.array([]) + self._action_range = (np.array([]), np.array([])) + self.i_gain = np.array([]) + self._integrator = np.array([0.0]) + self._tau = None + self._clipped = np.array([]) + + def __call__(self, state, reference): + """ + Calculate the reference values of the I controller + + Args: + state(np.ndarray): The state of the environment. + reference(np.ndarray): The reference of the state. + + Returns: + np.array: reference values of the next stage + """ + return self.control(state, reference) + + def _control(self, _state, _reference): + """Calculate the reference for the underlying stage""" + return self._i_gain * self._integrator + + def control(self, state, reference): + """ + Calculate the reference for the underlying stage + + Args: + state(np.ndarray): The state of the environment. + reference(np.ndarray): The reference of the state. + + Returns: + np.array: reference values of the next stage + """ + return self._control(state[self._state_indices], reference) + + def integrate(self, state, reference): + """ + Integrates the control error. + + Args: + state(np.ndarray): The state of the environment. + reference(np.ndarray): The reference of the state. + """ + + error = reference - state + self._integrator = self._integrator + (error * self._tau * ~self._clipped) + + def reset(self): + """Reset the integrated values""" + super().reset() + self._integrator = np.zeros_like(self._i_gain) + self._clipped = np.zeros_like(self._i_gain, dtype=bool) diff --git a/gem_controllers/stages/base_controllers/p_controller.py b/gem_controllers/stages/base_controllers/p_controller.py new file mode 100644 index 00000000..a4f9ebeb --- /dev/null +++ b/gem_controllers/stages/base_controllers/p_controller.py @@ -0,0 +1,147 @@ +import numpy as np + +from .base_controller import BaseController, EBaseControllerTask +from ... import parameter_reader as reader +import gem_controllers as gc + + +class PController(BaseController): + """This class represents an proportional controller, which can be combined e.g. with a integration controller to a + PI controller. + """ + @property + def p_gain(self): + """P gain of the P controller""" + return self._p_gain + + @p_gain.setter + def p_gain(self, value): + self._p_gain = value + + @property + def state_indices(self): + """Indices of the controlled states""" + return self._state_indices + + @state_indices.setter + def state_indices(self, value): + self._state_indices = np.array(value) + + @property + def action_range(self): + """Action range of the base controller""" + return self._action_range + + @action_range.setter + def action_range(self, value): + self._action_range = value + + def __call__(self, state, reference): + """ + Calculate the reference for the underlying stage + + Args: + state(np.ndarray): The state of the environment. + reference(np.ndarray): The reference of the state. + + Returns: + np.array: reference values of the next stage + """ + return self.control(state, reference) + + def __init__(self, control_task, p_gain=np.array([0.0]), action_range=(np.array([0.0]), np.array([0.0]))): + """ + Args: + control_task(str): Control task of the P controller. + p_gain(np.array): Array of p gains of the P controller. + action_range(np.array): Action range of the stage. + """ + BaseController.__init__(self, control_task) + self._p_gain = p_gain + self._action_range = action_range + self._state_indices = np.array([]) + + def _control(self, state, reference): + """Multiply the proportional gain by the current error to get the action value.""" + return self._p_gain * (reference - state) + + def control(self, state, reference): + """ + Calculate the reference for the underlying stage + + Args: + state(np.ndarray): The state of the environment. + reference(np.ndarray): The reference of the state. + + Returns: + np.array: reference values of the next stage + """ + return self._control(state[self._state_indices], reference) + + def tune(self, env, env_id, a=4): + """ + Tune the controller for the desired control task. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + env_id(str): The corresponding environment-id to specify the concrete environment. + a(float): Design parameter of the symmetrical optimum. + """ + + if self._control_task == EBaseControllerTask.CurrentControl: + self._tune_current_controller(env, env_id, a) + elif self._control_task == EBaseControllerTask.SpeedControl: + self._tune_speed_controller(env, env_id, a) + else: + raise Exception(f'No Tuner available for control task{self._control_task}.') + + def _tune_current_controller(self, env, env_id, a): + """ + Tune the P controller for the current control by the symmetrical optimum. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + env_id(str): The corresponding environment-id to specify the concrete environment. + a(float): Design parameter of the symmetrical optimum. + """ + + action_type, control_task, motor_type = gc.utils.split_env_id(env_id) + l_ = reader.l_reader[motor_type](env) + tau = env.physical_system.tau + currents = reader.currents[motor_type] + voltages = reader.voltages[motor_type] + voltage_indices = [env.state_names.index(voltage) for voltage in voltages] + current_indices = [env.state_names.index(current) for current in currents] + voltage_limits = env.limits[voltage_indices] + self.p_gain = l_ / (tau * a) + + self.action_range = ( + env.observation_space[0].low[voltage_indices] * voltage_limits, + env.observation_space[0].high[voltage_indices] * voltage_limits, + ) + self.state_indices = current_indices + + def _tune_speed_controller(self, env, env_id, a=4, t_n=None): + """ + Tune the P controller for the speed control by the symmetrical optimum. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + env_id(str): The corresponding environment-id to specify the concrete environment. + a(float): Design parameter of the symmetrical optimum. + t_n(float): Time constant of the underlying torque controller. + """ + + if t_n is None: + t_n = env.physical_system.tau + j_total = env.physical_system.mechanical_load.j_total + torque_index = env.state_names.index('torque') + speed_index = env.state_names.index('omega') + torque_limit = env.limits[torque_index] + p_gain = j_total / (a * t_n) + self.p_gain = np.array([p_gain]) + self.state_indices = [speed_index] + self.action_range = ( + env.observation_space[0].low[[torque_index]] * np.array([torque_limit]), + env.observation_space[0].high[[torque_index]] * np.array([torque_limit]) + ) diff --git a/gem_controllers/stages/base_controllers/pi_controller.py b/gem_controllers/stages/base_controllers/pi_controller.py new file mode 100644 index 00000000..eb23827a --- /dev/null +++ b/gem_controllers/stages/base_controllers/pi_controller.py @@ -0,0 +1,117 @@ +from .p_controller import PController +from .i_controller import IController +from ... import parameter_reader as reader +from .e_base_controller_task import EBaseControllerTask +import gem_controllers as gc +import numpy as np + + +class PIController(PController, IController): + """This class combines the proportional controller and the integration controller to a PI controller""" + + def __init__(self, control_task): + """ + Args: + control_task(str): Control task of the PI controller + """ + PController.__init__(self, control_task=control_task) + IController.__init__(self, control_task=control_task) + + def control(self, state, reference): + """ + Calculate the reference of the underlying stage by adding the P- and I-component. + + Args: + state(np.ndarray): The state of the environment. + reference(np.ndarray): The reference of the state. + + Returns: + controller_output(np.ndarray): output of the controller stage + """ + + filtered_state = state[self._state_indices] + action = PController._control(self, filtered_state, reference) \ + + IController._control(self, filtered_state, reference) + return action + + def tune(self, env, env_id, a=4, t_n=None): + """ + Tune the components of the controller for the desired task. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + env_id(str): The corresponding environment-id to specify the concrete environment. + a(float): Design parameter of the symmetrical optimum. + t_n(float): Time constant of the underlying controller stage. + """ + + if self._control_task == EBaseControllerTask.CC: + self._tune_current_controller(env, env_id, a) + elif self._control_task == EBaseControllerTask.SC: + self._tune_speed_controller(env, env_id, a, t_n) + elif self._control_task == EBaseControllerTask.FC: + self._tune_flux_controller(env, env_id, a, t_n) + else: + raise Exception(f'No tuning method available.') + + def _tune_current_controller(self, env, env_id, a=4): + """ + Tune the P controller and I controller for the current control by the symmetrical optimum. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + env_id(str): The corresponding environment-id to specify the concrete environment. + a(float): Design parameter of the symmetrical optimum. + """ + + action_type, control_task, motor_type = gc.utils.split_env_id(env_id) + PController._tune_current_controller(self, env, env_id, a) + tau = env.physical_system.tau + currents = reader.currents[motor_type] + voltages = reader.voltages[motor_type] + voltage_indices = [env.state_names.index(voltage) for voltage in voltages] + current_indices = [env.state_names.index(current) for current in currents] + voltage_limits = env.limits[voltage_indices] + i_gain = self.p_gain / (tau * a ** 2) + + action_range = ( + env.observation_space[0].low[voltage_indices] * voltage_limits, + env.observation_space[0].high[voltage_indices] * voltage_limits, + ) + self.i_gain = i_gain + self.action_range = action_range + self.tau = tau + self.state_indices = current_indices + + def _tune_speed_controller(self, env, env_id, a=4, t_n=None): + """ + Tune the P controller and I controller for the speed control by the symmetrical optimum. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + env_id(str): The corresponding environment-id to specify the concrete environment. + a(float): Design parameter of the symmetrical optimum. + t_n(float): Time constant of the underlying torque controller. + """ + + PController._tune_speed_controller(self, env, env_id, a, t_n) + self.i_gain = self.p_gain / (a * t_n) + self.tau = env.physical_system.tau + speed_index = env.state_names.index('omega') + self.state_indices = [speed_index] + + def _tune_flux_controller(self, env, env_id, a=4, t_n=None): + """ + Tune the P controller and I controller for the flux control by the symmetrical optimum. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + env_id(str): The corresponding environment-id to specify the concrete environment. + a(float): Design parameter of the symmetrical optimum. + t_n(float): Time constant of the underlying torque controller. + """ + + self.tau = env.physical_system.tau + self.p_gain = np.array([a * t_n ** 2]) + self.i_gain = self.p_gain / self.tau + self.state_indices = [0] diff --git a/gem_controllers/stages/base_controllers/pid_controller.py b/gem_controllers/stages/base_controllers/pid_controller.py new file mode 100644 index 00000000..3a3d06ba --- /dev/null +++ b/gem_controllers/stages/base_controllers/pid_controller.py @@ -0,0 +1,69 @@ +import numpy as np + +from .pi_controller import PIController + + +class PIDController(PIController): + """This class extends the PI controller by differential controller.""" + + @property + def d_gain(self): + """D gain of the D base controller""" + return self._d_gain + + @d_gain.setter + def d_gain(self, value): + self._d_gain = np.array(value) + self._last_error = np.zeros_like(self._d_gain, dtype=float) + + def __init__(self, control_task): + """ + Args: + control_task(str): Control task of the PID controller + """ + super().__init__(control_task) + self._d_gain = np.array([]) + self._last_error = np.array([]) + + def control(self, state, reference): + """ + Calculate the action of the PID controller. + + Args: + state(np.ndarray): The state of the environment. + reference(np.ndarray): The reference of the state. + + Returns: + action(np.ndarray): The action of the PID controller + """ + pi_action = super().control(state, reference) + current_error = reference - state[self.state_indices] + d_action = self._d_gain * (current_error - self._last_error) / self._tau + self._last_error = current_error + return pi_action + d_action + + def _tune_current_controller(self, env, env_id, a=4): + """Set the gains of the P-, I- and D-component for the current control. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + env_id(str): The corresponding environment-id to specify the concrete environment. + a(float): Design parameter of the symmetrical optimum. + """ + + super()._tune_current_controller(env, env_id, a) + self.d_gain = self.p_gain * self.tau + + def _tune_speed_controller(self, env, env_id, a=4, t_n=None): + """Set the gains of the P-, I- and D-component for the speed control. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + env_id(str): The corresponding environment-id to specify the concrete environment. + a(float): Design parameter of the symmetrical optimum. + t_n(float): Time constant of the underlying torque controller. + """ + + super()._tune_speed_controller(env, env_id, a) + self.d_gain = self.p_gain * self.tau + diff --git a/gem_controllers/stages/base_controllers/three_point_controller.py b/gem_controllers/stages/base_controllers/three_point_controller.py new file mode 100644 index 00000000..065326f0 --- /dev/null +++ b/gem_controllers/stages/base_controllers/three_point_controller.py @@ -0,0 +1,160 @@ +import numpy as np + +from .base_controller import BaseController +from .e_base_controller_task import EBaseControllerTask +from ... import parameter_reader as reader +import gem_controllers as gc + + +class ThreePointController(BaseController): + """This class represents a three point controller, that can be used for discrete action spaces.""" + + @property + def high_action(self): + """High action value of the three point controller""" + return self._high_action + + @high_action.setter + def high_action(self, value): + self._high_action = np.array(value) + + @property + def low_action(self): + """Low action value of the three point controller""" + return self._low_action + + @low_action.setter + def low_action(self, value): + self._low_action = np.array(value) + + @property + def idle_action(self): + """Idle action value of the three point controller""" + return self._idle_action + + @idle_action.setter + def idle_action(self, value): + self._idle_action = np.array(value) + + @property + def referenced_state_indices(self): + """Indices of the controlled states""" + return self._referenced_state_indices + + @referenced_state_indices.setter + def referenced_state_indices(self, value): + self._referenced_state_indices = np.array(value) + + @property + def hysteresis(self): + """Value of the hysteresis level""" + return self._hysteresis + + @hysteresis.setter + def hysteresis(self, value): + self._hysteresis = np.array(value) + + @property + def action_range(self): + """Action range of the base controller""" + return self._action_range + + @action_range.setter + def action_range(self, value): + self._action_range = value + + def __init__(self, control_task): + """ + Args: + control_task(str): Control task of the three point controller + """ + super().__init__(control_task) + self._hysteresis = np.array([]) + self._referenced_state_indices = np.array([]) + self._idle_action = 0.0 + self._high_action = np.array([]) + self._low_action = np.array([]) + self._action_range = (np.array([]), np.array([])) + + def __call__(self, state, reference): + """ + Select one of the three actions. + + Args: + state(np.ndarray): The state of the environment. + reference(np.ndarray): The reference of the state. + + Returns: + action(np.ndarray): Action or reference for the next stage + """ + referenced_states = state[self._referenced_state_indices] + high_actions = referenced_states + self._hysteresis < reference + low_actions = referenced_states - self._hysteresis > reference + return np.select([low_actions, high_actions], [self._low_action, self._high_action], default=self._idle_action) + + def tune(self, env, env_id, **base_controller_kwargs): + """ + Tune a three point controller stage. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + env_id(str): The corresponding environment-id to specify the concrete environment. + """ + + if self._control_task == EBaseControllerTask.SC: + self._tune_speed_controller(env, env_id) + elif self._control_task == EBaseControllerTask.CC: + self._tune_current_controller(env, env_id) + else: + raise Exception(f'No tuner available for control_task {self._control_task}.') + + def _tune_current_controller(self, env, env_id): + """ + Calculate the hysteresis levels of the current control stage and set the action values. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + env_id(str): The corresponding environment-id to specify the concrete environment. + """ + + motor_type = gc.utils.get_motor_type(env_id) + voltages = reader.voltages[motor_type] + currents = reader.currents[motor_type] + voltage_indices = [env.state_names.index(voltage) for voltage in voltages] + current_indices = [env.state_names.index(current) for current in currents] + self.referenced_state_indices = current_indices + voltage_limits = env.limits[voltage_indices] + + action_range = ( + env.observation_space[0].low[voltage_indices] * voltage_limits, + env.observation_space[0].high[voltage_indices] * voltage_limits, + ) + self.action_range = action_range + # Todo: Calculate Hysteresis based on the dynamics of the underlying control plant + self.hysteresis = 0.01 * (action_range[1] - action_range[0]) + self.high_action = action_range[1] + self.low_action = action_range[0] + self.idle_action = np.zeros_like(action_range[1]) + + def _tune_speed_controller(self, env, _env_id): + """ + Calculate the hysteresis levels of the speed control stage and set the torque reference values. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + _env_id(str): The corresponding environment-id to specify the concrete environment. + """ + + torque_index = [env.state_names.index('reference')] + torque_limit = env.limits[torque_index] + self.referenced_state_indices = torque_index + action_range = ( + env.observation_space[0].low[torque_index] * torque_limit, + env.observation_space[0].high[torque_index] * torque_limit, + ) + self.action_range = action_range + # Todo: Calculate Hysteresis based on the dynamics of the underlying control plant + self.hysteresis = 0.01 * (action_range[1] - action_range[0]) + self.high_action = action_range[1] + self.low_action = action_range[0] + self.idle_action = np.zeros_like(action_range[1]) diff --git a/gem_controllers/stages/base_controllers/utils.py b/gem_controllers/stages/base_controllers/utils.py new file mode 100644 index 00000000..aa44c5ec --- /dev/null +++ b/gem_controllers/stages/base_controllers/utils.py @@ -0,0 +1,15 @@ +import gem_controllers.stages.base_controllers as bc + + +def get(base_controller_id: str): + """Returns the class of a base controller called by a string.""" + return _base_controller_registry[base_controller_id] + + +_base_controller_registry = { + 'P': bc.PController, + 'I': bc.IController, + 'PI': bc.PIController, + 'PID': bc.PIDController, + 'ThreePoint': bc.ThreePointController +} \ No newline at end of file diff --git a/gem_controllers/stages/clipping_stages/__init__.py b/gem_controllers/stages/clipping_stages/__init__.py new file mode 100644 index 00000000..336cfd86 --- /dev/null +++ b/gem_controllers/stages/clipping_stages/__init__.py @@ -0,0 +1,4 @@ +from .clipping_stage import ClippingStage +from .absolute_clipping_stage import AbsoluteClippingStage +from .squared_clipping_stage import SquaredClippingStage +from .combined_clipping_stage import CombinedClippingStage diff --git a/gem_controllers/stages/clipping_stages/absolute_clipping_stage.py b/gem_controllers/stages/clipping_stages/absolute_clipping_stage.py new file mode 100644 index 00000000..5b54dc09 --- /dev/null +++ b/gem_controllers/stages/clipping_stages/absolute_clipping_stage.py @@ -0,0 +1,75 @@ +import numpy as np +from typing import Tuple + +import gem_controllers as gc +from . import ClippingStage + + +class AbsoluteClippingStage(ClippingStage): + """This class clips a reference absolute to the limit of the corresponding limit of the state""" + + @property + def clipping_difference(self) -> np.ndarray: + """Difference between the reference and the clipped reference""" + return self._clipping_difference + + @property + def action_range(self) -> Tuple[np.ndarray, np.ndarray]: + """Action range of the controller stage""" + return self._action_range + + def __init__(self, control_task='CC'): + """ + Args: + control_task(str): Control task of the controller stage. + """ + self._action_range = np.array([]), np.array([]) + self._clipping_difference = np.array([]) + self._control_task = control_task + + def __call__(self, state, reference): + """ + Clips a reference to the limits. + + Args: + state(np.ndarray): The state of the environment. + reference(np.ndarray): The reference of the state. + + Returns: + clipped_reference(np.ndarray): The reference of a controller stage clipped to the limit. + """ + + clipped = np.clip(reference, self._action_range[0], self._action_range[1]) + self._clipping_difference = reference - clipped + return clipped + + def tune(self, env, env_id, margin=0.0, **kwargs): + """ + Set the limits for the clipped states. + + Args: + env(gym_electric_motor.ElectricMotorEnvironment): The environment to be controlled. + env_id(str): The id of the environment. + margin(float): Percentage, how far the value should be clipped below the limit. + """ + + motor_type = gc.utils.get_motor_type(env_id) + if self._control_task == 'CC': + action_names = gc.parameter_reader.voltages[motor_type] + elif self._control_task == 'TC': + action_names = gc.parameter_reader.currents[motor_type] + elif self._control_task == 'SC': + action_names = ['torque'] + else: + raise AttributeError(f'Control task is {self._control_task} but has to be one of [SC, TC, CC].') + action_indices = [env.state_names.index(action_name) for action_name in action_names] + limits = env.limits[action_indices] * (1 - margin) + state_space = env.observation_space[0] + lower_action_limit = state_space.low[action_indices] * limits + upper_action_limit = state_space.high[action_indices] * limits + self._action_range = lower_action_limit, upper_action_limit + self._clipping_difference = np.zeros_like(lower_action_limit) + + def reset(self): + """Reset the absolute clipping stage""" + self._clipping_difference = np.zeros_like(self._action_range[0]) diff --git a/gem_controllers/stages/clipping_stages/clipping_stage.py b/gem_controllers/stages/clipping_stages/clipping_stage.py new file mode 100644 index 00000000..da1ab201 --- /dev/null +++ b/gem_controllers/stages/clipping_stages/clipping_stage.py @@ -0,0 +1,42 @@ +import gym_electric_motor.core +import numpy as np + +from ..stage import Stage + + +class ClippingStage(Stage): + """This is the base class for all clipping stages.""" + + @property + def clipping_difference(self) -> np.ndarray: + """Difference between the reference and the clipped reference""" + raise NotImplementedError + + @property + def clipped(self) -> np.ndarray: + """Flag, if the references have been clipped""" + return self.clipping_difference != 0.0 + + def __call__(self, state: np.ndarray, reference: np.ndarray) -> np.ndarray: + """ + Clips a reference to the limits. + + Args: + state(np.ndarray): The state of the environment. + reference(np.ndarray): The reference of the state. + + Returns: + clipped_reference(np.ndarray): The reference of a controller stage clipped to the limit. + """ + raise NotImplementedError + + def tune(self, env: gym_electric_motor.core.ElectricMotorEnvironment, env_id: str, margin: float = 0.0, **kwargs): + """ + Set the limits for the clipped states. + + Args: + env(gym_electric_motor.ElectricMotorEnvironment): The environment to be controlled. + env_id(str): The id of the environment. + margin(float): Percentage, how far the value should be clipped below the limit. + """ + pass diff --git a/gem_controllers/stages/clipping_stages/combined_clipping_stage.py b/gem_controllers/stages/clipping_stages/combined_clipping_stage.py new file mode 100644 index 00000000..4fbec75d --- /dev/null +++ b/gem_controllers/stages/clipping_stages/combined_clipping_stage.py @@ -0,0 +1,87 @@ +import numpy as np +from typing import Tuple + +import gem_controllers as gc +from . import ClippingStage + + +class CombinedClippingStage(ClippingStage): + """This clipping stage combines the absolute clipping and the squared clipping.""" + + @property + def clipping_difference(self) -> np.ndarray: + """Difference between the reference and the clipped reference""" + return self._clipping_difference + + @property + def action_range(self) -> Tuple[np.ndarray, np.ndarray]: + """Action range of the controller stage""" + action_range_low = np.zeros(len(self._squared_clipped_states) + len(self._absolute_clipped_states)) + action_range_high = np.zeros(len(self._squared_clipped_states) + len(self._absolute_clipped_states)) + action_range_low[-1] = self._action_range_absolute[0][0] + action_range_high[-1] = self._action_range_absolute[1][0] + return action_range_low, action_range_high + + def __init__(self, control_task='CC'): + self._action_range_absolute = np.array([]), np.array([]) + self._limit_squred_clipping = np.array([]) + self._clipping_difference = np.array([]) + self._control_task = control_task + self._absolute_clipped_states = np.array([]) + self._squared_clipped_states = np.array([]) + self._margin = None + + def __call__(self, state, reference): + clipped = np.zeros(np.size(self._squared_clipped_states) + np.size(self._absolute_clipped_states)) + relative_reference_length = np.sum((reference[self._squared_clipped_states]/self._limit_squred_clipping)**2) + relative_maximum = 1 - self._margin + clipped[self._squared_clipped_states] = reference[self._squared_clipped_states] \ + if relative_reference_length < relative_maximum**2 \ + else reference[self._squared_clipped_states] / relative_reference_length * relative_maximum + + clipped[self._absolute_clipped_states] = np.clip(reference[self._absolute_clipped_states], + self._action_range_absolute[0], self._action_range_absolute[1]) + + self._clipping_difference = reference - clipped + return clipped + + def tune(self, env, env_id, margin=0.0, squared_clipped_state=np.array([0, 1]), + absoulte_clipped_states=np.array([2])): + """ + Set the limits for the clipped states. + + Args: + env(gym_electric_motor.ElectricMotorEnvironment): The environment to be controlled. + env_id(str): The id of the environment. + margin(float): Percentage, how far the value should be clipped below the limit. + squared_clipped_state(np.ndarray): Indices of the squared clipped states. + absoulte_clipped_states(np.ndarray): Indices of the absolute clipped states. + """ + + self._squared_clipped_states = squared_clipped_state + self._absolute_clipped_states = absoulte_clipped_states + self._margin = margin + + motor_type = gc.utils.get_motor_type(env_id) + if self._control_task == 'CC': + action_names = gc.parameter_reader.voltages[motor_type] + elif self._control_task == 'TC': + action_names = gc.parameter_reader.currents[motor_type] + elif self._control_task == 'SC': + action_names = ['torque'] + else: + raise AttributeError(f'Control task is {self._control_task} but has to be one of [SC, TC, CC].') + action_indices = [env.state_names.index(action_name) for action_name in action_names] + limits = env.limits[action_indices] * (1 - margin) + state_space = env.observation_space[0] + lower_action_limit = state_space.low[action_indices] * limits + upper_action_limit = state_space.high[action_indices] * limits + self._limit_squred_clipping = limits[squared_clipped_state] + self._action_range_absolute = lower_action_limit[absoulte_clipped_states], upper_action_limit[ + absoulte_clipped_states] + self._clipping_difference = np.zeros_like(lower_action_limit) + + def reset(self): + """Reset the combined clipping stage""" + self._clipping_difference = np.zeros( + np.size(self._squared_clipped_states) + np.size(self._absolute_clipped_states)) diff --git a/gem_controllers/stages/clipping_stages/squared_clipping_stage.py b/gem_controllers/stages/clipping_stages/squared_clipping_stage.py new file mode 100644 index 00000000..62855727 --- /dev/null +++ b/gem_controllers/stages/clipping_stages/squared_clipping_stage.py @@ -0,0 +1,85 @@ +import numpy as np + +import gem_controllers as gc +from .clipping_stage import ClippingStage + + +class SquaredClippingStage(ClippingStage): + """This class clips multiple references together, by clipping the vector length of the references to a scalar limit. + """ + + @property + def clipping_difference(self) -> np.ndarray: + """Difference between the reference and the clipped reference""" + return self._clipping_difference + + @property + def limits(self): + """Limits of the controlled states""" + return self._limits + + @property + def margin(self): + """Margin of the controlled states""" + return self._margin + + @property + def action_range(self): + """Action range of the controller stage""" + return [] + + def __init__(self, control_task='CC'): + """ + Args: + control_task(str): Control task of the controller stage. + """ + + self._clipping_difference = np.array([]) + self._margin = 0.0 + self._limits = np.array([]) + self._control_task = control_task + + def __call__(self, state, reference): + """ + Clips a reference to the limits. + + Args: + state(np.ndarray): The state of the environment. + reference(np.ndarray): The reference of the state. + + Returns: + clipped_reference(np.ndarray): The reference of a controller stage clipped to the limit. + """ + + relative_reference_length = np.sum((reference/self._limits)**2) + relative_maximum = 1 - self._margin + clipped = reference \ + if relative_reference_length < relative_maximum**2 \ + else reference / relative_reference_length * relative_maximum + self._clipping_difference = reference - clipped + return clipped + + def tune(self, env, env_id, margin=0.0, **kwargs): + """ + Set the limits for the clipped states. + + Args: + env(gym_electric_motor.ElectricMotorEnvironment): The environment to be controlled. + env_id(str): The id of the environment. + margin(float): Percentage, how far the value should be clipped below the limit. + """ + + motor_type = gc.utils.get_motor_type(env_id) + state_names = [] + if self._control_task == 'CC': + state_names = gc.parameter_reader.voltages[motor_type] + elif self._control_task == 'TC': + state_names = gc.parameter_reader.currents[motor_type] + elif self._control_task == 'SC': + state_names = ['torque'] + state_indices = [env.state_names.index(state_name) for state_name in state_names] + self._limits = env.limits[state_indices] + + def reset(self): + """Reset the squared clipping stage""" + self._clipping_difference = np.zeros_like(self._limits) diff --git a/gem_controllers/stages/cont_output_stage.py b/gem_controllers/stages/cont_output_stage.py new file mode 100644 index 00000000..ea0d7c95 --- /dev/null +++ b/gem_controllers/stages/cont_output_stage.py @@ -0,0 +1,39 @@ +import numpy as np + +from .stage import Stage +from .. import parameter_reader as reader +import gem_controllers as gc + + +class ContOutputStage(Stage): + """This class normalizes continuous input voltages to the volatge limits.""" + + @property + def voltage_limit(self): + """Voltage limit of the motor""" + return self._voltage_limit + + @voltage_limit.setter + def voltage_limit(self, value): + self._voltage_limit = np.array(value, dtype=float) + + def __init__(self): + super().__init__() + self._voltage_limit = np.array([]) + + def __call__(self, state, reference): + """"Divide the input voltages by the limits""" + return reference / self.voltage_limit + + def tune(self, env, env_id, **_): + """ + Set the volatage limits. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + env_id(str): The corresponding environment-id to specify the concrete environment. + """ + action_type, _, motor_type = gc.utils.split_env_id(env_id) + voltages = reader.get_output_voltages(motor_type, action_type) + voltage_indices = [env.state_names.index(voltage) for voltage in voltages] + self.voltage_limit = env.limits[voltage_indices] diff --git a/gem_controllers/stages/disc_output_stage.py b/gem_controllers/stages/disc_output_stage.py new file mode 100644 index 00000000..a5232bda --- /dev/null +++ b/gem_controllers/stages/disc_output_stage.py @@ -0,0 +1,137 @@ +import numpy as np +import gym +from gym_electric_motor.physical_systems import converters as cv + +from .stage import Stage +from ..utils import non_parameterized +from .. import parameter_reader as reader +import gem_controllers as gc + + +class DiscOutputStage(Stage): + """This class maps the discrete input voltages, calculated by the controller, to the scalar inputs of the used + converter. + """ + + @property + def output_stage(self): + """Output stage of the controller""" + return self._output_stage + + @output_stage.setter + def output_stage(self, value): + assert value in [self.to_b6_discrete, self.to_multi_discrete, self.to_discrete] + self._output_stage = value + + def __init__(self): + super().__init__() + self.high_level = 0.0 + self.low_level = 0.0 + self.high_action = 0 + self.low_action = 0 + self.idle_action = 0 + self._output_stage = non_parameterized + + def __call__(self, state, reference): + """ + Maps the input voltages to the scalar inputs of the converter. + + Args: + state(np.ndarray): The state of the environment. + reference(np.ndarray): The reference voltages. + + Returns: + action(int): scalar action of the environment + """ + return self._output_stage(self.to_action(state, reference)) + + @staticmethod + def to_discrete(multi_discrete_action): + """ + Transform multi discrete action to a discrete action. + + Args: + multi_discrete_action(np.array): Array of multi discrete actions + + Returns: + int: discrete action + """ + return multi_discrete_action[0] + + @staticmethod + def to_b6_discrete(multi_discrete_action): + """Returns the multi discrete action for a B6 brigde converter.""" + raise NotImplementedError + + @staticmethod + def to_multi_discrete(multi_discrete_action): + """ + Returns the multi discrete action. + """ + return multi_discrete_action + + def to_action(self, _state, reference): + """ + Map the voltages to a voltage level. + + Args: + _state(np.ndarray): The state of the environment. + reference(np.ndarray): The reference voltages. + + Returns: + action(np.ndarray): volatge vector + """ + conditions = [reference <= self.low_level, reference >= self.high_level] + return np.select(conditions, [self.low_action, self.high_action], default=self.idle_action) + + def tune(self, env, env_id, **__): + """ + Set the values for the low, idle and high action. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + env_id(str): The corresponding environment-id to specify the concrete environment. + """ + + action_type, _, motor_type = gc.utils.split_env_id(env_id) + voltages = reader.get_output_voltages(motor_type, action_type) + voltage_indices = [env.state_names.index(voltage) for voltage in voltages] + voltage_limits = env.limits[voltage_indices] + + voltage_range = ( + env.observation_space[0].low[voltage_indices] * voltage_limits, + env.observation_space[0].high[voltage_indices] * voltage_limits, + ) + + self.low_level = -0.33 * (voltage_range[1] - voltage_range[0]) + self.high_level = 0.33 * (voltage_range[1] - voltage_range[0]) + + if type(env.action_space) == gym.spaces.MultiDiscrete: + self.output_stage = DiscOutputStage.to_multi_discrete + self.low_action = [] + self.idle_action = [] + self.high_action = [] + for n in env.action_space.nvec: + low_action, idle_action, high_action = self._get_actions(n) + self.low_action.append(low_action) + self.idle_action.append(idle_action) + self.high_action.append(high_action) + + elif type(env.action_space) == gym.spaces.Discrete \ + and type(env.physical_system.converter) != cv.FiniteB6BridgeConverter: + self.output_stage = DiscOutputStage.to_discrete + self.low_action, self.idle_action, self.high_action = self._get_actions(env.action_space.n) + elif type(env.physical_system.converter) == cv.FiniteB6BridgeConverter: + self.output_stage = DiscOutputStage.to_b6_discrete + else: + raise Exception(f'No discrete output stage available for action space {env.action_space}.') + + @staticmethod + def _get_actions(n): + high_action = 1 + if n == 2: # OneQuadrantConverter + low_action = 0 + else: # Two and FourQuadrantConverter + low_action = 2 + idle_action = 0 + return low_action, idle_action, high_action diff --git a/gem_controllers/stages/emf_feedforward.py b/gem_controllers/stages/emf_feedforward.py new file mode 100644 index 00000000..d940f7c6 --- /dev/null +++ b/gem_controllers/stages/emf_feedforward.py @@ -0,0 +1,110 @@ +import numpy as np + +from .stage import Stage +from .. import parameter_reader as reader +import gem_controllers as gc + + +class EMFFeedforward(Stage): + """This class calculates the emf feedforward, to decouple the actions.""" + + @property + def inductance(self): + """Inductances of the motor""" + return self._inductance + + @inductance.setter + def inductance(self, value): + self._inductance = np.array(value) + + @property + def psi(self): + """Permanent magnet flux of the motor""" + return self._psi + + @psi.setter + def psi(self, value): + self._psi = np.array(value) + + @property + def current_indices(self): + """Indices of the currents""" + return self._current_indices + + @current_indices.setter + def current_indices(self, value): + self._current_indices = np.array(value) + + @property + def omega_idx(self): + """Index of the rotational speed omega""" + return self._omega_idx + + @omega_idx.setter + def omega_idx(self, value): + self._omega_idx = int(value) + + @property + def action_range(self): + """Action range of the motor""" + return self._action_range + + def omega_el(self, state): + """ + Calculate the electrical speed. + + Args: + state(np.array): state of the environment + + Returns: + float: electrical speed + """ + return state[self._omega_idx] * self._p + + def __init__(self): + super().__init__() + self._p = 1 + self._inductance = np.array([]) + self._psi = np.array([]) + self._current_indices = np.array([]) + self._omega_idx = None + self._action_range = np.array([]), np.array([]) + + def __call__(self, state, reference): + """ + Calculate the emf feedforward voltages and add them to the actions of the current controller. + + Args: + state(np.ndarray): The state of the environment. + reference(np.ndarray): The reference voltages. + + Returns: + input voltages(np.ndarray): decoupled input voltages + """ + action = reference + (self._inductance * state[self._current_indices] + self._psi) * self.omega_el(state) + return action + + def tune(self, env, env_id, **_): + """ + Set all needed motor parameters for the decoupling. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + env_id(str): The corresponding environment-id to specify the concrete environment. + """ + + motor_type = gc.utils.get_motor_type(env_id) + omega_idx = env.state_names.index('omega') + current_indices = [env.state_names.index(current) for current in reader.emf_currents[motor_type]] + self.omega_idx = omega_idx + self.current_indices = current_indices + self.inductance = reader.l_emf_reader[motor_type](env) + self.psi = reader.psi_reader[motor_type](env) + self._p = reader.p_reader[motor_type](env) + voltages = reader.voltages[motor_type] + voltage_indices = [env.state_names.index(voltage) for voltage in voltages] + voltage_limits = env.limits[voltage_indices] + self._action_range = ( + env.observation_space[0].low[voltage_indices] * voltage_limits, + env.observation_space[0].high[voltage_indices] * voltage_limits, + ) diff --git a/gem_controllers/stages/emf_feedforward_eesm.py b/gem_controllers/stages/emf_feedforward_eesm.py new file mode 100644 index 00000000..1fec86d5 --- /dev/null +++ b/gem_controllers/stages/emf_feedforward_eesm.py @@ -0,0 +1,58 @@ +from .emf_feedforward import EMFFeedforward + +import numpy as np + + +class EMFFeedforwardEESM(EMFFeedforward): + """ + This class extends the functions of the EMFFeedforward class to decouple the dq-components of the induction motor. + """ + def __init__(self): + super().__init__() + self._l_m = None + self._i_e_idx = None + self._decoupling_params = None + self._action_decoupling = None + self._currents_idx = None + self._action_idx = None + + def __call__(self, state, reference): + """ + Calculate the emf feedforward voltages and add them to the actions of the current controller. + + Args: + state(np.ndarray): The state of the environment. + reference(np.ndarray): The reference voltages. + + Returns: + input voltages(np.ndarray): decoupled input voltages + """ + + self.psi = np.array([0, self._l_m * state[self._i_e_idx], 0]) + action = super().__call__(state, reference) + action = action + self._decoupling_params * state[self._currents_idx] + action = action + self._action_decoupling * action[self._action_idx] + return action + + def tune(self, env, env_id, **_): + """ + Set all needed motor parameters for the decoupling. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + env_id(str): The corresponding environment-id to specify the concrete environment. + """ + + super().tune(env, env_id, **_) + l_m = env.physical_system.electrical_motor.motor_parameter['l_m'] + l_d = env.physical_system.electrical_motor.motor_parameter['l_d'] + l_e = env.physical_system.electrical_motor.motor_parameter['l_e'] + r_s = env.physical_system.electrical_motor.motor_parameter['r_s'] + r_e = env.physical_system.electrical_motor.motor_parameter['r_e'] + + self._l_m = l_m + self._i_e_idx = env.state_names.index('i_e') + self._decoupling_params = np.array([-l_m * r_e / l_e, 0, -l_m * r_s / l_d]) + self._action_decoupling = np.array([l_m / l_e, 0, l_m / l_d]) + self._currents_idx = [env.state_names.index('i_e'), 0, env.state_names.index('i_sd')] + self._action_idx = [2, 1, 0] diff --git a/gem_controllers/stages/emf_feedforward_ind.py b/gem_controllers/stages/emf_feedforward_ind.py new file mode 100644 index 00000000..2143bc45 --- /dev/null +++ b/gem_controllers/stages/emf_feedforward_ind.py @@ -0,0 +1,55 @@ +from .emf_feedforward import EMFFeedforward +import numpy as np + + +class EMFFeedforwardInd(EMFFeedforward): + """ + This class extends the functions of the EMFFeedforward class to decouple the dq-components of the induction motor. + """ + def __init__(self): + super().__init__() + self.r_r = None + self.l_m = None + self.l_r = None + self.i_sq_idx = None + self.psi_idx = None + self.psi_abs_idx = None + + def __call__(self, state, reference): + """ + Decouple the input voltages of an induction motor. + + Args: + state(np.ndarray): The state of the environment. + reference(np.ndarray): The reference voltages. + + Returns: + np.array: decoupled reference voltages + """ + + # Calculate the stator angular velocity + omega_s = state[self.omega_idx] + self.r_r * self.l_m / self.l_r * state[self.i_sq_idx] / max( + np.abs(state[self.psi_abs_idx]), 1e-4) * np.sign(state[self.psi_abs_idx]) + + # Calculate the decoupling of the components + action = reference + omega_s * self.inductance * state[self.current_indices] + np.array( + [- self.l_m * self.r_r / (self.l_r ** 2), state[self.omega_idx] * self.l_m / self.l_r]) * state[self.psi_abs_idx] + + return action + + def tune(self, env, env_id, **_): + """ + Set all needed motor parameters for the decoupling. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + env_id(str): The corresponding environment-id to specify the concrete environment. + """ + + super().tune(env, env_id, **_) + mp = env.physical_system.electrical_motor.motor_parameter + self.r_r = mp['r_r'] + self.l_m = mp['l_m'] + self.l_r = mp['l_sigr'] + self.l_m + self.i_sq_idx = env.state_names.index('i_sq') + self.psi_abs_idx = env.state_names.index('psi_abs') diff --git a/gem_controllers/stages/input_stage.py b/gem_controllers/stages/input_stage.py new file mode 100644 index 00000000..91a14341 --- /dev/null +++ b/gem_controllers/stages/input_stage.py @@ -0,0 +1,58 @@ +import numpy as np + +from .stage import Stage + + +class InputStage(Stage): + """This class denormalizes the state and reference.""" + + @property + def state_limits(self): + """Limits of the states""" + return self._state_limits + + @state_limits.setter + def state_limits(self, value): + self._state_limits = np.array(value) + + @property + def reference_limits(self): + """Limits of the references""" + return self._reference_limits + + @reference_limits.setter + def reference_limits(self, value): + self._reference_limits = np.array(value) + + def __init__(self): + super().__init__() + self._state_limits = np.array([]) + self._reference_limits = np.array([]) + + def __call__(self, state, reference): + """ + Denormalize the state and the references + + Args: + state(np.ndarray): The state of the environment. + reference(np.ndarray): The reference values at the input. + + Returns: + np.array: denormalized reference values + """ + + state[:] = state * self._state_limits + return reference * self._reference_limits + + def tune(self, env, env_id, **__): + """ + Set the limits of the state and the references. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + env_id(str): The corresponding environment-id to specify the concrete environment. + """ + + self._state_limits = env.limits + reference_indices = [env.state_names.index(reference) for reference in env.reference_names] + self.reference_limits = env.limits[reference_indices] diff --git a/gem_controllers/stages/operation_point_selection/__init__.py b/gem_controllers/stages/operation_point_selection/__init__.py new file mode 100644 index 00000000..5fdc0fe9 --- /dev/null +++ b/gem_controllers/stages/operation_point_selection/__init__.py @@ -0,0 +1,9 @@ +from .ops_utils import torque_to_current_function +from .shunt_dc_ops import ShuntDcOperationPointSelection +from .series_dc_ops import SeriesDcOperationPointSelection +from .permex_dc_ops import PermExDcOperationPointSelection +from .operation_point_selection import OperationPointSelection +from .extex_dc_ttc import ExtExDcOperationPointSelection +from .pmsm_ops import PMSMOperationPointSelection +from .scim_ops import SCIMOperationPointSelection +from .eesm_ops import EESMOperationPointSelection diff --git a/gem_controllers/stages/operation_point_selection/eesm_ops.py b/gem_controllers/stages/operation_point_selection/eesm_ops.py new file mode 100644 index 00000000..aca89ffb --- /dev/null +++ b/gem_controllers/stages/operation_point_selection/eesm_ops.py @@ -0,0 +1,254 @@ +import numpy as np +import scipy.interpolate as sp_interpolate +from .foc_operation_point_selection import FieldOrientedControllerOperationPointSelection + + +class EESMOperationPointSelection(FieldOrientedControllerOperationPointSelection): + """ + This class represents the operation point selection of the torque controller for cascaded control of an + externally synchronous motor. The operating point is selected in the analog to that of the PMSM and SynRM, but + the excitation current is also included in the optimization of the operating point. + """ + + def __init__(self, max_modulation_level: float = 2 / np.sqrt(3), modulation_damping: float = 1.2): + """ + Args: + max_modulation_level(float): Maximum value for the modulation controller. + modulation_damping(float): Damping of the gain of the modulation controller. + """ + + super().__init__(max_modulation_level, modulation_damping) + self.l_d = None + self.l_q = None + self.l_m = None + self.l_e = None + self.r_s = None + self.r_e = None + self.i_e_lim = None + self.t_lim = None + self.i_q_lim = None + + self.t_count = None + self.psi_count = None + self.i_e_count = None + + self.psi_opt = None + self.i_d_opt = None + self.i_q_opt = None + self.i_e_opt = None + self.t_max = None + self.psi_max = None + self.t_max_psi = None + + self.t_grid_count = None + self.psi_grid_count = None + + self.torque_equation = None + self.loss = None + self.poly = None + + def tune(self, env, env_id, current_safety_margin=0.2): + """ + Tune the operation point selcetion stage. + + Args: + env(gym_electric_motor.ElectricMotorEnvironment): The environment to be controlled. + env_id(str): The id of the environment. + current_safety_margin(float): Percentage of the current margin to the current limit. + """ + + super().tune(env, env_id, current_safety_margin) + self.l_d = self.mp['l_d'] + self.l_q = self.mp['l_q'] + self.l_m = self.mp['l_m'] + self.l_e = self.mp['l_e'] + self.r_s = self.mp['r_s'] + self.r_e = self.mp['r_e'] + self.p = self.mp['p'] + self.i_e_lim = env.limits[env.state_names.index('i_e')] * (1 - current_safety_margin) + self.i_q_lim = env.limits[env.state_names.index('i_sq')] * (1 - current_safety_margin) + self.t_lim = env.limits[env.state_names.index('torque')] + + self.t_count = 50 + self.psi_count = 100 + self.i_e_count = 150 + + self.t_grid_count = 200 + self.psi_grid_count = 200 + + self.k_ = 0.953 + self.i_gain = 1 / (self.l_q / (1.25 * self.r_s)) * (self.alpha - 1) / self.alpha ** 2 + + self.psi_high = 0.2 * np.sqrt( + (self.l_m * self.i_e_lim * current_safety_margin + self.l_d * self.i_sq_limit * current_safety_margin) ** 2) + + self.psi_low = -self.psi_high + self.integrated_reset = 0.01 * self.psi_low + + self.torque_equation = lambda i_d, i_q, i_e: 3 / 2 * self.p * ( + self.l_m * i_e + (self.l_d - self.l_q) * i_d) * i_q + + self.loss = lambda i_d, i_q, i_e: np.abs(i_d) * self.r_s + np.abs(i_q) * self.r_s + np.abs(i_e) * self.r_e + + self.poly = lambda i_e, psi, torque: [self.l_d ** 2 * (self.l_d - self.l_q) ** 2, + 2 * self.l_d ** 2 * ( + self.l_d - self.l_q) * self.l_m * i_e + 2 * self.l_d * self.l_m * i_e * ( + self.l_d - self.l_q) ** 2, + self.l_d ** 2 * (self.l_m * i_e) ** 2 + 4 * self.l_d * ( + self.l_m * i_e) ** 2 * (self.l_d - self.l_q) + ( + (self.l_m * i_e) ** 2 - psi ** 2) * ( + self.l_d - self.l_q) ** 2, + 2 * self.l_q * (self.l_m * i_e) ** 3 + 2 * ( + (self.l_m * i_e) ** 2 - psi ** 2) * self.l_m * i_e * ( + self.l_d - self.l_q), + ((self.l_m * i_e) ** 2 - psi ** 2) * (self.l_m * i_e) ** 2 + ( + self.l_q * torque / (3 * self.p)) ** 2] + + self._calculate_luts() + + def solve_analytical(self, torque, psi, i_e): + """ + Assuming linear magnetization characteristics, the optimal currents for given reference, flux and exitation + current can be obtained by solving the reference and flux equations. These lead to a fourth degree polynomial + which can be solved analytically. + + Args: + torque(float): The torque reference value. + psi(float): The optimal flux value. + i_e(float): The excitation current. + + Returns: + i_d(float): optimal i_sd current + i_q(flaot): optimal i_sq current + """ + if torque == 0 and i_e == 0: + return 0, 0 + else: + i_d = np.real(np.roots(self.poly(i_e, psi, torque))[-1]) + i_q = 2 * torque / (3 * self.p * (self.l_m * i_e + (self.l_d - self.l_q) * i_d)) + return i_d, i_q + + def _calculate_luts(self): + """ + Calculates the lookup tables for the maximum torque and the optimal currents for a given torque reference. + """ + minimum_loss = [] + best_params = [] + + minimum_loss_psi = [] + best_params_psi = [] + + self.psi_max = self.l_m * self.i_e_lim + self.l_d * self.i_q_lim + torque = np.linspace(0, self.t_lim, self.t_count) + + self.t_max_psi = np.zeros(self.psi_count) + + for t in torque: + losses = [] + parameter = [] + for idx, psi in enumerate(np.linspace(0, self.psi_max, self.psi_count)): + losses_psi = [] + parameter_psi = [] + for i_e in np.linspace(0, self.i_e_lim, self.i_e_count): + i_d, i_q = self.solve_analytical(t, psi, i_e) + if np.sqrt(i_d ** 2 + i_q ** 2) < self.i_q_lim: + loss = self.loss(i_d, i_q, i_e) + params = np.array([t, psi, i_d, i_q, i_e]) + losses.append(loss) + losses_psi.append(loss) + parameter.append(params) + parameter_psi.append(params) + self.t_max_psi[idx] = t + if len(losses_psi) > 0: + minimum_loss_psi.append(min(losses_psi)) + best_params_psi.append(parameter_psi[losses_psi.index(minimum_loss_psi[-1])]) + if len(losses) > 0: + minimum_loss.append(min(losses)) + best_params.append(parameter[losses.index(minimum_loss[-1])]) + + best_params = np.array(best_params) + best_params_psi = np.array(best_params_psi) + + self.t_max_psi = sp_interpolate.interp1d(np.linspace(0, self.psi_max, self.psi_count), 0.99 * self.t_max_psi, kind='linear') + + self.t_max = np.max(best_params[:, 0]) + self.psi_opt = sp_interpolate.interp1d(best_params[:, 0], best_params[:, 1], kind='cubic') + self.i_d_opt = sp_interpolate.interp1d(best_params[:, 0], best_params[:, 2], kind='cubic') + self.i_q_opt = sp_interpolate.interp1d(best_params[:, 0], best_params[:, 3], kind='cubic') + self.i_e_opt = sp_interpolate.interp1d(best_params[:, 0], best_params[:, 4], kind='cubic') + + self.t_grid, self.psi_grid = np.mgrid[ + 0: self.t_max: np.complex(0, self.t_grid_count), + 0: self.psi_max: np.complex(self.psi_grid_count) + ] + + self.i_d_inter = sp_interpolate.griddata((best_params_psi[:, 0], best_params_psi[:, 1]), best_params_psi[:, 2], + (self.t_grid, self.psi_grid), method='linear') + self.i_q_inter = sp_interpolate.griddata((best_params_psi[:, 0], best_params_psi[:, 1]), best_params_psi[:, 3], + (self.t_grid, self.psi_grid), method='linear') + self.i_e_inter = sp_interpolate.griddata((best_params_psi[:, 0], best_params_psi[:, 1]), best_params_psi[:, 4], + (self.t_grid, self.psi_grid), method='linear') + + def _get_psi_idx(self, psi): + """ + Get the index of the lookup tables for a given flux. + + Args: + psi(float): optimal magnetic flux. + + Returns: + index(int): index of the lookup tables of the currents + """ + + psi = np.clip(psi, 0, self.psi_max) + return int(round(psi / self.psi_max * (self.psi_grid_count - 1))) + + def _get_t_idx(self, torque): + """ + Get the index of the lookup tables for a given torque. + + Args: + torque(float): The clipped torque reference value. + + Returns: + index(int): index of the lookup tables of the currents + """ + + torque = np.clip(torque, 0, self.t_max) + return int(round(torque / self.t_max * (self.t_grid_count - 1))) + + def _select_operating_point(self, state, reference): + """ + Calculate the current operation point for a given torque reference value. + + Args: + state(np.ndarray): The state of the environment. + reference(np.ndarray): The reference of the state. + + Returns: + current_reference(np.ndarray): references for the current control stage + """ + + psi_max = self.modulation_control(state) + t_ref = reference[0] + + t_ref_clip = np.abs(np.clip(t_ref, -self.t_max, self.t_max)) + psi_opt = self.psi_opt(t_ref_clip) + + psi = np.clip(psi_opt, 0, psi_max) + + t_max = self.t_max_psi(psi_opt) + t_ref_clip = np.clip(t_ref_clip, 0, t_max) + + t_idx = self._get_t_idx(t_ref_clip) + psi_idx = self._get_psi_idx(psi) + + i_d_ref = self.i_d_inter[t_idx, psi_idx] + i_q_ref = np.sign(t_ref) * self.i_q_inter[t_idx, psi_idx] + i_e_ref = self.i_e_inter[t_idx, psi_idx] + + return np.array([i_d_ref, i_q_ref, i_e_ref]) + + def reset(self): + """Reset the EESM operation point selection""" + super().reset() diff --git a/gem_controllers/stages/operation_point_selection/extex_dc_ttc.py b/gem_controllers/stages/operation_point_selection/extex_dc_ttc.py new file mode 100644 index 00000000..e7d4c881 --- /dev/null +++ b/gem_controllers/stages/operation_point_selection/extex_dc_ttc.py @@ -0,0 +1,98 @@ +import numpy as np + +from .operation_point_selection import OperationPointSelection +from ... import parameter_reader as reader +import gem_controllers as gc + + +class ExtExDcOperationPointSelection(OperationPointSelection): + """This class comutes the current operation point of a ExtExDc Motor for a given torque reference value.""" + + @property + def cross_inductance(self): + """Cross inducances of the ExtEx Dc motor""" + return self._cross_inductance + + @cross_inductance.setter + def cross_inductance(self, value): + self._cross_inductance = np.array(value, dtype=float) + + @property + def i_e_idx(self): + """Index of the i_e current""" + return self._i_e_idx + + @i_e_idx.setter + def i_e_idx(self, value): + self._i_e_idx = int(value) + + @property + def i_a_idx(self): + """Index of the i_a current""" + return self._i_a_idx + + @i_a_idx.setter + def i_a_idx(self, value): + self._i_a_idx = int(value) + + @property + def i_e_policy(self): + """Policy for calculating the i_e current""" + return self._i_e_policy + + @i_e_policy.setter + def i_e_policy(self, value): + assert callable(value), 'The i_e_policy has to be a callable function.' + self._i_e_policy = value + + def __init__(self): + super().__init__() + self._cross_inductance = np.array([]) + self._i_e_idx = None + self._i_a_idx = None + self._r_a_sqrt = None + self._r_e_sqrt = None + self._l_e_prime = None + self._i_e_policy = self.__i_e_policy + + def __i_e_policy(self, state, reference): + """The policy for the exciting current that is used per default.""" + return np.sqrt(self._r_a_sqrt * abs(reference[0]) / (self._r_e_sqrt * self._l_e_prime)) + + def _select_operating_point(self, state, reference): + """ + Calculate the current refrence values. + + Args: + state(np.ndarray): The state of the environment. + reference(np.ndarray): The reference of the state. + + Returns: + np.array: current reference values + """ + # Select the i_e reference + i_e_ref = self._i_e_policy(state, reference) + + # Calculate the i_a reference + i_a_ref = reference[0] / self._cross_inductance[0] / max(state[self._i_e_idx], 1e-4) + return np.array([i_a_ref, i_e_ref]) + + def tune(self, env, env_id, current_safety_margin=0.2): + """ + Tune the operation point selcetion stage. + + Args: + env(gym_electric_motor.ElectricMotorEnvironment): The environment to be controlled. + env_id(str): The id of the environment. + current_safety_margin(float): Percentage of the current margin to the current limit. + """ + + super().tune(env, env_id, current_safety_margin) + motor_type = gc.utils.get_motor_type(env_id) + self._i_e_idx = env.state_names.index('i_e') + self._i_a_idx = env.state_names.index('i_a') + self._cross_inductance = reader.l_prime_reader[motor_type](env) + mp = env.physical_system.electrical_motor.motor_parameter + self._r_a_sqrt = np.sqrt(mp['r_a']) + self._r_e_sqrt = np.sqrt(mp['r_e']) + self._l_e_prime = mp['l_e_prime'] diff --git a/gem_controllers/stages/operation_point_selection/foc_operation_point_selection.py b/gem_controllers/stages/operation_point_selection/foc_operation_point_selection.py new file mode 100644 index 00000000..fafd6d85 --- /dev/null +++ b/gem_controllers/stages/operation_point_selection/foc_operation_point_selection.py @@ -0,0 +1,150 @@ +from .operation_point_selection import OperationPointSelection +import numpy as np + + +class FieldOrientedControllerOperationPointSelection(OperationPointSelection): + """ + This is the base class for all field-oriented operating point controls. It also includes a function for level + control. + """ + + def __init__(self, max_modulation_level: float = 2 / np.sqrt(3), modulation_damping: float = 1.2): + """ + Operation Point Selection for torque control of synchronous motors + + Args: + max_modulation_level: Maximum modulation of the modulation controller + modulation_damping: Damping of the modulation controller + """ + + # motor parameters and limits + self.mp = None + self.limit = None + self.nominal_value = None + self.i_sq_limit = None + self.i_sd_limit = None + self.p = None + self.tau = None + + # state indices + self.omega_idx = None + self.u_sd_idx = None + self.u_sq_idx = None + self.u_a_idx = None + self.torque_idx = None + self.epsilon_idx = None + self.i_sd_idx = None + self.i_sq_idx = None + + # size of the characteristic diagrams of the operating point control + self.t_count = None + self.psi_count = None + + # parameters of the modulation controller + self.modulation_damping = modulation_damping + self.a_max = max_modulation_level + self.k_ = None # Factor for optimum modulation level + self.alpha = None # dynamic distance between outer and inner control loop + self.i_gain = None # constant i_gain of the modulation controller + self.limited = None # check, if flux is limited + self.u_dc = None # supply voltage + self.integrated = 0 # integration of the flux + self.psi_high = None # maximum delta flux + self.psi_low = None # minimum delta flux + self.integrated_reset = None # reset value integrated flux + + def tune(self, env, env_id, current_safety_margin=0.2): + """ + Tune the operation point selcetion stage. + + Args: + env(gym_electric_motor.ElectricMotorEnvironment): The environment to be controlled. + env_id(str): The id of the environment. + current_safety_margin(float): Percentage of the current margin to the current limit. + """ + super().tune(env, env_id, current_safety_margin) + + # set the state indices + self.omega_idx = env.state_names.index('omega') + self.u_sd_idx = env.state_names.index('u_sd') + self.u_sq_idx = env.state_names.index('u_sq') + self.torque_idx = env.state_names.index('torque') + self.epsilon_idx = env.state_names.index('epsilon') + self.i_sd_idx = env.state_names.index('i_sd') + self.i_sq_idx = env.state_names.index('i_sq') + u_a = 'u_a' if 'u_a' in env.state_names else 'u_sa' + self.u_a_idx = env.state_names.index(u_a) + + # set the motor parameters and limits + self.mp = env.physical_system.electrical_motor.motor_parameter + self.p = self.mp['p'] + self.tau = env.physical_system.tau + self.limit = env.physical_system.limits + self.nominal_value = env.physical_system.nominal_state + self.i_sd_limit = self.limit[self.i_sd_idx] * (1 - current_safety_margin) + self.i_sq_limit = self.limit[self.i_sq_idx] * (1 - current_safety_margin) + + # calculate dynamic distance from damping + self.alpha = self.modulation_damping / (self.modulation_damping - np.sqrt(self.modulation_damping ** 2 - 1)) + self.limited = False + self.u_dc = np.sqrt(3) * self.limit[self.u_a_idx] + self.integrated = self.integrated_reset + + def _select_operating_point(self, state, reference): + """ + Calculate the current refrence values. + + Args: + state(np.ndarray): The state of the environment. + reference(np.ndarray): The reference of the state. + + Returns: + np.array: current reference values + """ + pass + + def modulation_control(self, state): + """ + To ensure the functionality of the current control, a small dynamic manipulated variable reserve to the + voltage limitation must be kept available. This control is performed by this modulation controller. Further + information can be found at https://ieeexplore.ieee.org/document/7409195. + """ + + # Calculate modulation + a = 2 * np.sqrt(state[self.u_sd_idx] ** 2 + state[self.u_sq_idx] ** 2) / self.u_dc + + # Check, if integral part should be reset + if a > 1.1 * self.a_max: + self.integrated = self.integrated_reset + + a_delta = self.k_ * self.a_max - a + omega = max(np.abs(state[self.omega_idx]), 0.0001) + + # Calculate maximum flux for a given speed + psi_max_ = self.u_dc / (np.sqrt(3) * omega * self.p) + + # Calculate gain + k_i = 2 * omega * self.p / self.u_dc + i_gain = self.i_gain / k_i + + psi_delta = i_gain * (a_delta * self.tau + self.integrated) + + # Check, if limits are violated + if self.psi_low <= psi_delta <= self.psi_high: + if self.limited: + self.integrated = self.integrated_reset + self.limited = False + self.integrated += a_delta * self.tau + + else: + psi_delta = np.clip(psi_delta, self.psi_low, self.psi_high) + self.limited = True + + # Calculate output flux of the modulation controller + psi = psi_max_ + psi_delta + + return psi + + def reset(self): + """Reset the FOC operation point selcetion stage""" + self.integrated = self.integrated_reset diff --git a/gem_controllers/stages/operation_point_selection/operation_point_selection.py b/gem_controllers/stages/operation_point_selection/operation_point_selection.py new file mode 100644 index 00000000..8116931a --- /dev/null +++ b/gem_controllers/stages/operation_point_selection/operation_point_selection.py @@ -0,0 +1,44 @@ +from gem_controllers.stages.stage import Stage + + +class OperationPointSelection(Stage): + """Base class for all operation point selections.""" + + def __call__(self, state, reference): + """ + Calculate the current operation point for a given torque reference value. + + Args: + state(np.ndarray): The state of the environment. + reference(np.ndarray): The reference of the state. + + Returns: + current_reference(np.ndarray): references for the current control stage + + """ + return self._select_operating_point(state, reference) + + def _select_operating_point(self, state, reference): + """ + Interal calculation of the operation point + + Args: + state(np.ndarray): The state of the environment. + reference(np.ndarray): The reference of the state. + + Returns: + np.array: current refernce values + """ + + raise NotImplementedError + + def tune(self, env, env_id, current_safety_margin=0.2): + """ + Set the motor parameters, limits and indices of a operation point selection class. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + env_id(str): The corresponding environment-id to specify the concrete environment. + current_safety_margin(float): Percentage of the current margin to the current limit. + """ + pass diff --git a/gem_controllers/stages/operation_point_selection/ops_utils.py b/gem_controllers/stages/operation_point_selection/ops_utils.py new file mode 100644 index 00000000..3ed45497 --- /dev/null +++ b/gem_controllers/stages/operation_point_selection/ops_utils.py @@ -0,0 +1,18 @@ +from .permex_dc_ops import PermExDcOperationPointSelection +from .extex_dc_ttc import ExtExDcOperationPointSelection +from .series_dc_ops import SeriesDcOperationPointSelection +from .shunt_dc_ops import ShuntDcOperationPointSelection +from .pmsm_ops import PMSMOperationPointSelection +from .scim_ops import SCIMOperationPointSelection +from .eesm_ops import EESMOperationPointSelection + +torque_to_current_function = { + 'PermExDc': PermExDcOperationPointSelection, + 'ExtExDc': ExtExDcOperationPointSelection, + 'SeriesDc': SeriesDcOperationPointSelection, + 'ShuntDc': ShuntDcOperationPointSelection, + 'PMSM': PMSMOperationPointSelection, + 'SynRM': PMSMOperationPointSelection, + 'SCIM': SCIMOperationPointSelection, + 'EESM': EESMOperationPointSelection, +} diff --git a/gem_controllers/stages/operation_point_selection/permex_dc_ops.py b/gem_controllers/stages/operation_point_selection/permex_dc_ops.py new file mode 100644 index 00000000..450666e6 --- /dev/null +++ b/gem_controllers/stages/operation_point_selection/permex_dc_ops.py @@ -0,0 +1,90 @@ +import numpy as np + +import gem_controllers as gc +from .operation_point_selection import OperationPointSelection +from ... import parameter_reader as reader + + +class PermExDcOperationPointSelection(OperationPointSelection): + """This class computes the current operation point of a PermExDx Motor for a given torque reference value.""" + + @property + def magnetic_flux(self): + """Permanent magnetic flux of the PermEx Dc motor""" + return self._magnetic_flux + + @magnetic_flux.setter + def magnetic_flux(self, value): + self._magnetic_flux = np.array(value, dtype=float) + + @property + def voltage_limit(self): + """Voltage limit of the the PermEx Dc motor""" + return self._voltage_limit + + @voltage_limit.setter + def voltage_limit(self, value): + self._voltage_limit = np.array(value, dtype=float) + + @property + def omega_index(self): + """Index of the rotational speeda""" + return self._omega_index + + @omega_index.setter + def omega_index(self, value): + self._omega_index = int(value) + + @property + def resistance(self): + """Ohmic resistance of the PermEx Dc motor""" + return self._resistance + + @resistance.setter + def resistance(self, value): + self._resistance = np.array(value, dtype=float) + + def __init__(self): + super().__init__() + self._magnetic_flux = np.array([]) + self._voltage_limit = np.array([]) + self._resistance = np.array([]) + self._omega_index = 0 + + def _select_operating_point(self, state, reference): + """ + Calculate the current refrence values. + + Args: + state(np.ndarray): The state of the environment. + reference(np.ndarray): The reference of the state. + + Returns: + np.array: current reference values + """ + + if state[self._omega_index] > 0: + return min(reference / self._magnetic_flux, self._max_current_per_speed(state)) + else: + return max(reference / self._magnetic_flux, -self._max_current_per_speed(state)) + + def _max_current_per_speed(self, state): + """Calculate the maximum current for a given speed.""" + return self._voltage_limit / (self._resistance + self._magnetic_flux * abs(state[self._omega_index])) + + def tune(self, env, env_id, current_safety_margin=0.2): + """ + Tune the operation point selcetion stage. + + Args: + env(gym_electric_motor.ElectricMotorEnvironment): The environment to be controlled. + env_id(str): The id of the environment. + current_safety_margin(float): Percentage of the current margin to the current limit. + """ + super().tune(env, env_id, current_safety_margin=current_safety_margin) + motor = gc.utils.get_motor_type(env_id) + self._magnetic_flux = reader.psi_reader[motor](env) + voltages = reader.voltages[motor] + voltage_indices = [env.state_names.index(voltage) for voltage in voltages] + self._voltage_limit = env.limits[voltage_indices] + self._omega_index = env.state_names.index('omega') diff --git a/gem_controllers/stages/operation_point_selection/pmsm_ops.py b/gem_controllers/stages/operation_point_selection/pmsm_ops.py new file mode 100644 index 00000000..38bc0f6c --- /dev/null +++ b/gem_controllers/stages/operation_point_selection/pmsm_ops.py @@ -0,0 +1,375 @@ +import gym_electric_motor as gem +import numpy as np +import scipy.interpolate as sp_interpolate + +from .foc_operation_point_selection import FieldOrientedControllerOperationPointSelection + + +class PMSMOperationPointSelection(FieldOrientedControllerOperationPointSelection): + """ + This class represents the operation point selection of the torque controller for cascaded control of synchronous + motors. For low speeds only the current limitation of the motor is important. The current vector to set a + desired torque is selected so that the amount of the current vector is minimum (Maximum Torque per Current). + For higher speeds, the voltage limitation of the synchronous motor or the actuator must also be taken into + account. This is done by converting the available voltage to a speed-dependent maximum flux. An additional + modulation controller is used for the flux control. By limiting the flux and the maximum torque per flux (MTPF), + an operating point for the flux and the torque is obtained. This is then converted into a current operating + point. The conversion can be done by different methods (parameter torque_control). On the one hand, maps can be + determined in advance by interpolation or analytically, or the analytical determination can be done online. + """ + + def __init__( + self, torque_control='online', max_modulation_level: float = 2 / np.sqrt(3), + modulation_damping: float = 1.2 + ): + """ + Args: + torque_control(str): Methode for the operation point selection. + max_modulation_level(float): Maximum value for the modulation controller. + modulation_damping(float): Damping of the gain of the modulation controller. + """ + + super().__init__(max_modulation_level, modulation_damping) + self.torque_control_type = torque_control + self.a_max = max_modulation_level + self.i_count = None + self.max_torque = None + self.invert = None + + self.l_d = None + self.l_q = None + self.psi_p = None + + def _get_mtpc_lookup_table(self): + """Calculate the lookup tables, that maps a pair of torque and flux on a current operation point.""" + + def i_q_(i_d__, torque_, controller): + return torque_ / (i_d__ * (controller.l_d - controller.l_q) + controller.psi_p) / (1.5 * controller.p) + + def i_d_(i_q__, torque_, controller): + return -np.abs(torque_ / (1.5 * controller.p * (controller.l_d - controller.l_q) * i_q__)) + + # calculate the maximum reference + self.max_torque = max( + 1.5 * self.p * (self.psi_p + (self.l_d - self.l_q) * (-self.i_sd_limit)) * self.i_sq_limit, + self.limit[self.torque_idx] + ) + torque = np.linspace(-self.max_torque, self.max_torque, self.t_count) + characteristic = [] + + for t in torque: + if self.psi_p != 0: + if self.l_d == self.l_q: + i_d = 0 + else: + i_d = np.linspace(-2.5 * self.limit[self.i_sd_idx], 0, self.i_count) + i_q = i_q_(i_d, t, self) + else: + i_q = np.linspace(-2.5 * self.limit[self.i_sq_idx], 2.5 * self.limit[self.i_sq_idx], self.i_count) + if self.l_d == self.l_q: + i_d = 0 + else: + i_d = i_d_(i_q, t, self) + + # Different current vectors are determined for each reference and the smallest magnitude is selected + i = np.power(i_d, 2) + np.power(i_q, 2) + min_idx = np.where(i == np.amin(i))[0][0] + if self.l_d == self.l_q: + i_q_ret = i_q + i_d_ret = i_d + else: + i_q_ret = np.sign((self.l_q - self.l_d) * t) * np.abs(i_q[min_idx]) + i_d_ret = i_d[min_idx] + + # The flow is finally calculated from the currents + psi = np.sqrt((self.psi_p + self.l_d * i_d_ret) ** 2 + (self.l_q * i_q_ret) ** 2) + characteristic.append([t, i_d_ret, i_q_ret, psi]) + return np.array(characteristic) + + def _get_mtpf_lookup_table(self): + """Calculate the lookup table that maps a flux on a maximum torque.""" + + # maximum flux is calculated + self.psi_max_mtpf = np.sqrt( + (self.psi_p + self.l_d * self.i_sd_limit) ** 2 + + (self.l_q * self.i_sq_limit) ** 2 + ) + psi = np.linspace(0, self.psi_max_mtpf, self.psi_count) + i_d = np.linspace(-self.i_sd_limit, 0, self.i_count) + i_d_best = 0 + i_q_best = 0 + psi_i_d_q = [] + + # Iterates through all flux values to determine the maximum reference + for psi_ in psi: + if psi_ == 0: + i_d_ = -self.psi_p / self.l_d + i_q = 0 + t = 0 + psi_i_d_q.append([psi_, t, i_d_, i_q]) + + else: + if self.psi_p == 0: + i_q_best = psi_ / np.sqrt(self.l_d ** 2 + self.l_q ** 2) + i_d_best = -i_q_best + t = 1.5 * self.p * (self.psi_p + (self.l_d - self.l_q) * i_d_best) * i_q_best + else: + i_d_idx = np.where(psi_ ** 2 - np.power(self.psi_p + self.l_d * i_d, 2) >= 0) + i_d_ = i_d[i_d_idx] + + # calculate all possible i_q currents for i_d currents + i_q = np.sqrt(psi_ ** 2 - np.power(self.psi_p + self.l_d * i_d_, 2)) / self.l_q + i_idx = np.where(np.sqrt(np.power(i_q / self.i_sq_limit, 2) + np.power( + i_d_ / self.i_sd_limit, 2)) <= 1) + i_d_ = i_d_[i_idx] + i_q = i_q[i_idx] + torque = 1.5 * self.p * (self.psi_p + (self.l_d - self.l_q) * i_d_) * i_q + + # choose the maximum reference + if np.size(torque) > 0: + t = np.amax(torque) + i_idx = np.where(torque == t)[0][0] + i_d_best = i_d_[i_idx] + i_q_best = i_q[i_idx] + if np.sqrt(i_d_best ** 2 + i_q_best ** 2) <= self.i_sq_limit: + psi_i_d_q.append([psi_, t, i_d_best, i_q_best]) + + psi_i_d_q = np.array(psi_i_d_q) + self.psi_max_mtpf = np.max(psi_i_d_q[:, 0]) + psi_i_d_q_neg = np.rot90(np.array([psi_i_d_q[:, 0], -psi_i_d_q[:, 1], psi_i_d_q[:, 2], -psi_i_d_q[:, 3]])) + psi_i_d_q = np.append(psi_i_d_q_neg, psi_i_d_q, axis=0) + + return np.array(psi_i_d_q) + + def tune(self, env: gem.core.ElectricMotorEnvironment, env_id: str, current_safety_margin: float = 0.2): + """ + Tune the operation point selcetion stage. + + Args: + env(gym_electric_motor.ElectricMotorEnvironment): The environment to be controlled. + env_id(str): The id of the environment. + current_safety_margin(float): Percentage of the current margin to the current limit. + """ + + super().tune(env, env_id, current_safety_margin) + + self.t_count = 250 # number of torque values in the lookup tables + self.psi_count = 250 # number of flux values in the lookup tables + self.i_count = 500 # number of current values in the lookup tables + + self.l_d = self.mp['l_d'] + self.l_q = self.mp['l_q'] + self.psi_p = self.mp.get('psi_p', 0) + self.invert = -1 if (self.psi_p == 0 and self.l_q < self.l_d) else 1 + + # Set the parameters of the modulation controller + self.k_ = 0.953 + self.i_gain = 1 / (self.mp['l_q'] / (1.25 * self.mp['r_s'])) * (self.alpha - 1) / self.alpha ** 2 + self.psi_high = 0.2 * np.sqrt( + (self.psi_p + self.l_d * self.i_sd_limit) ** 2 + + (self.l_q * self.i_sq_limit) ** 2 + ) + self.psi_low = -self.psi_high + self.integrated_reset = 0.01 * self.psi_low # Reset value of the modulation controller + self.max_torque = max( + 1.5 * self.p * (self.psi_p + (self.l_d - self.l_q) * (-self.limit[self.i_sd_idx])) + * self.i_sq_limit, self.limit[self.torque_idx]) + + # Calculate the mtpc and mtpf lookup tables + self.psi_max_mtpf = 0.0 + self.mtpc = self._get_mtpc_lookup_table() + self.mtpf = self._get_mtpf_lookup_table() + self.psi_t = np.sqrt( + np.power(self.psi_p + self.l_d * self.mtpc[:, 1], 2) + np.power(self.l_q * self.mtpc[:, 2], 2)) + self.psi_t = np.array([self.mtpc[:, 0], self.psi_t]) + + # Define the grid for the currents + self.i_q_max = np.linspace(-self.i_sq_limit, self.i_sq_limit, self.i_count) + self.i_d_max = -np.sqrt(self.i_sq_limit ** 2 - np.power(self.i_q_max, 2)) + i_count_mgrid = self.i_count * 1j + i_d, i_q = np.mgrid[ + -self.limit[self.i_sd_idx]: 0: i_count_mgrid, + -self.limit[self.i_sq_idx]: self.limit[self.i_sq_idx]: i_count_mgrid / 2 + ] + i_d = i_d.flatten() + i_q = i_q.flatten() + + # Calculate the possible combinations of i_sd and i_sq + if self.l_d != self.l_q: + idx = np.where( + np.sign(self.psi_p + i_d * self.l_d) * np.power(self.psi_p + i_d * self.l_d, 2) + + np.power(i_q * self.l_q, 2) + > 0 + ) + else: + idx = np.where(self.psi_p + i_d * self.l_d > 0) + i_d = i_d[idx] + i_q = i_q[idx] + + # Calculate the corresponding torques and fluxes + t = self.p * 1.5 * (self.psi_p + (self.l_d - self.l_q) * i_d) * i_q + psi = np.sqrt(np.power(self.l_d * i_d + self.psi_p, 2) + np.power(self.l_q * i_q, 2)) + self.t_min = np.amin(t) + self.t_max = np.amax(t) + self.psi_min = np.amin(psi) + self.psi_max = np.amax(psi) + + # Calculate the lookup table that maps the torque and flux on a current operation point + if self.torque_control_type == 'analytical': + # Calculate the lookup table by solving the equations analyticaly + + # Solve the equations for every point in the grid of torques and fluxes + res = [] + for psi in np.linspace(self.psi_min, self.psi_max, self.psi_count): + ret = [] + for T in np.linspace(self.t_min, self.t_max, self.t_count): + i_d_, i_q_ = self.solve_analytical(T, psi) + ret.append([T, psi, i_d_, i_q_]) + res.append(ret) + res = np.array(res) + self.t_grid = res[:, :, 0] + self.psi_grid = res[:, :, 1] + self.i_d_inter = res[:, :, 2].T + self.i_q_inter = res[:, :, 3].T + + elif self.torque_control_type == 'interpolate': + # Calculate the lookup table by interpolation + + # Define the grid for the torque and fluxes + self.t_grid, self.psi_grid = np.mgrid[np.amin(t): np.amax(t): np.complex(0, self.t_count), + self.psi_min: self.psi_max: np.complex(self.psi_count)] + + # Interpolate the functions + self.i_q_inter = sp_interpolate.griddata((t, psi), i_q, (self.t_grid, self.psi_grid), method='linear') + self.i_d_inter = sp_interpolate.griddata((t, psi), i_d, (self.t_grid, self.psi_grid), method='linear') + + elif self.torque_control_type != 'online': + raise NotImplementedError + + def solve_analytical(self, torque, psi): + """ + Assuming linear magnetization characteristics, the optimal currents for given reference and flux can be + obtained by solving the reference and flux equations. These lead to a fourth degree polynomial which can be + solved analytically. There are two ways to use this analytical solution for control. On the one hand, the + currents can be determined in advance as in the case of interpolation for different torques and fluxes and + stored in a LUT (torque_control='analytical'). On the other hand, the solution can be calculated at runtime + with the given reference and flux (torque_control='online'). + + Args: + torque(float): The torque reference value. + psi(float): The optimal flux value. + + Returns: + i_d(float): optimal $i_{sd}$ current + i_q(flaot): optimal $i_{sq}$ current + """ + + poly = [ + self.l_d ** 2 * (self.l_d - self.l_q) ** 2, + + 2 * self.l_d ** 2 * (self.l_d - self.l_q) * self.psi_p + + 2 * self.l_d * self.psi_p * (self.l_d - self.l_q) ** 2, + + self.l_d ** 2 * self.psi_p ** 2 + + 4 * self.l_d * self.psi_p ** 2 * (self.l_d - self.l_q) + + (self.psi_p ** 2 - psi ** 2) * (self.l_d - self.l_q) ** 2, + + 2 * self.l_q * self.psi_p ** 3 + + 2 * (self.psi_p ** 2 - psi ** 2) * self.psi_p * (self.l_d - self.l_q), + + (self.psi_p ** 2 - psi ** 2) * self.psi_p ** 2 + + (self.l_q * 2 * torque / (3 * self.p)) ** 2 + ] + + sol = np.roots(poly) # Solve the equation + i_d = np.real(sol[-1]) # Select the correct solution for the i_sd current + i_q = 2 * torque / (3 * self.p * (self.psi_p + (self.l_d - self.l_q) * i_d)) # Calculate the i_sq current + return i_d, i_q + + def _get_i_d_q(self, torque, psi, psi_idx): + """Get the i_d and i_q current from the MTPC lut""" + + i_d, i_q = self.solve_analytical(torque, psi) + if i_d > self.mtpc[psi_idx, 1]: + i_d = self.mtpc[psi_idx, 1] + i_q = self.mtpc[psi_idx, 2] + return i_d, i_q + + def _get_t_idx(self, torque): + """Get the index of the torque.""" + torque = np.clip(torque, self.t_min, self.t_max) + return int(round((torque[0] - self.t_min) / (self.t_max - self.t_min) * (self.t_count - 1))) + + def _get_psi_idx(self, psi): + """Get the index of the flux.""" + psi = np.clip(psi, self.psi_min, self.psi_max) + return int(round((psi - self.psi_min) / (self.psi_max - self.psi_min) * (self.psi_count - 1))) + + def _get_psi_idx_mtpf(self, psi): + """Get the index of the flux of the MTPF lookup table.""" + return np.clip( + int((self.psi_count - 1) - round(psi / self.psi_max_mtpf * (self.psi_count - 1))), 0, self.psi_count) + + def _get_t_idx_mtpc(self, torque): + """Get the index of the torque of the mtpc lookup table.""" + return np.clip( + int(round((torque[0] + self.max_torque) / (2 * self.max_torque) * (self.t_count - 1))), 0, self.t_count) + + def _select_operating_point(self, state, reference): + """ + Calculate the current operation point for a given torque reference value. + + Args: + state(np.ndarray): The state of the environment. + reference(np.ndarray): The reference of the state. + + Returns: + current_reference(np.ndarray): references for the current control stage + """ + # get the optimal psi for a given reference from the mtpc characteristic + psi_idx_ = self._get_t_idx_mtpc(reference) + psi_opt = self.mtpc[psi_idx_, 3] + + # limit the flux to keep the voltage limit using the modulation controller + psi_max_ = self.modulation_control(state) + psi_max = min(psi_opt, psi_max_) + + # get the maximum reference for a given flux from the mtpf characteristic + psi_max_idx = self._get_psi_idx_mtpf(psi_max) + t_max = np.abs(self.mtpf[psi_max_idx, 1]) + if np.abs(reference) > t_max: + reference = np.sign(reference) * t_max + + # calculate the currents online + if self.torque_control_type == 'online': + i_d, i_q = self._get_i_d_q(reference[0], psi_max, psi_idx_) + + # get the currents from a LUT + else: + t_idx = self._get_t_idx(reference) + psi_idx = self._get_psi_idx(psi_max) + + if self.i_d_inter[t_idx, psi_idx] <= self.mtpf[psi_max_idx, 2]: + i_d = self.mtpf[psi_max_idx, 2] + i_q = np.sign(reference[0]) * np.abs(self.mtpf[psi_max_idx, 3]) + reference = np.sign(reference[0]) * t_max + else: + i_d = self.i_d_inter[t_idx, psi_idx] + i_q = self.i_q_inter[t_idx, psi_idx] + if i_d > self.mtpc[psi_idx_, 1]: + i_d = self.mtpc[psi_idx_, 1] + i_q = np.sign(reference[0]) * np.abs(self.mtpc[psi_idx_, 2]) + + # ensure that the mtpf characteristic curve is observed + if i_d < self.mtpf[psi_max_idx, 2]: + i_d = self.mtpf[psi_max_idx, 2] + i_q = np.sign(reference[0]) * np.abs(self.mtpf[psi_max_idx, 3]) + + # invert the i_q if necessary + i_q = self.invert * i_q + + return np.array([i_d, i_q]) + + def reset(self): + """Reset the PMSM operation point selection""" + super().reset() diff --git a/gem_controllers/stages/operation_point_selection/scim_ops.py b/gem_controllers/stages/operation_point_selection/scim_ops.py new file mode 100644 index 00000000..bb6279fa --- /dev/null +++ b/gem_controllers/stages/operation_point_selection/scim_ops.py @@ -0,0 +1,184 @@ +import gym_electric_motor as gem +import numpy as np +from .foc_operation_point_selection import FieldOrientedControllerOperationPointSelection +from ..base_controllers import PIController + + +class SCIMOperationPointSelection(FieldOrientedControllerOperationPointSelection): + """ + This class represents the operation point selection of the torque controller for the cascaded control of + induction motors. The torque controller uses LUT to find an appropriate operating point for the flux and torque. + The flux is limited by a modulation controller. A reference value for the i_sd current is then determined using + the operating point of the flux and a PI controller. In addition, a reference for the i_sq current is calculated + based on the current flux and the operating point of the torque. + """ + + def __init__(self, max_modulation_level: float = 2 / np.sqrt(3), modulation_damping: float = 1.2, + psi_controller=PIController(control_task='FC')): + """ + Args: + max_modulation_level(float): Maximum value for the modulation controller. + modulation_damping(float): Damping of the gain of the modulation controller. + psi_controller(gc.stages.BaseController): Flux controller for the i_d current. + """ + + super().__init__(max_modulation_level, modulation_damping) + + self.l_m = None + self.l_r = None + self.l_s = None + self.r_r = None + self.r_s = None + self.psi_abs_idx = None + + self.i_sd_count = None + self.i_sq_count = None + + self.psi_controller = psi_controller + + def _psi_opt(self): + """Calculate the optimal flux for a given torque""" + psi_opt_t = [] + i_sd = np.linspace(0, self.limit[self.i_sd_idx], self.i_sd_count) + for t in np.linspace(self.t_minimum, self.t_maximum, self.t_count): + if t != 0: + i_sq = t / (3/2 * self.p * self.l_m ** 2 / self.l_r * i_sd[1:]) + pv = 3 / 2 * (self.r_s * np.power(i_sd[1:], 2) + ( + self.r_s + self.r_r * self.l_m ** 2 / self.l_r ** 2) * np.power(i_sq, 2)) # Calculate losses + + i_idx = np.argmin(pv) # Minimize losses + i_sd_opt = i_sd[i_idx] + i_sq_opt = i_sq[i_idx] + else: + i_sq_opt = 0 + i_sd_opt = 0 + + psi_opt = self.l_m * i_sd_opt + psi_opt_t.append([t, psi_opt, i_sd_opt, i_sq_opt]) + return np.array(psi_opt_t).T + + def _t_max(self): + """Calculate a lookup table with the maximum torque for a given flux.""" + + # The resulting torque and currents lists + t_val = [] + i_sd_val = [] + i_sq_val = [] + psi = np.linspace(self.psi_max, 0, self.psi_count) + + # Calculate the maximum torque for a given flux + for psi_ in psi: + i_sd = psi_ / self.l_m + i_sq = np.sqrt(self.nominal_value[self.u_sd_idx] ** 2 / ( + self.nominal_value[self.omega_idx] ** 2 * self.l_s ** 2) - i_sd ** 2) + + t = 3 / 2 * self.p * self.l_m / self.l_r * psi_ * i_sq + t_val.append(t) + i_sd_val.append(i_sd) + i_sq_val.append(i_sq) + + # The characteristic is symmetrical for positive and negative torques. + t_val.extend(list(-np.array(t_val[::-1]))) + psi = np.append(psi, psi[::-1]) + i_sd_val.extend(i_sd_val[::-1]) + i_sq_val.extend(list(-np.array(i_sq_val[::-1]))) + + return np.array([t_val, psi, i_sd_val, i_sq_val]) + + def tune(self, env: gem.core.ElectricMotorEnvironment, env_id: str, current_safety_margin: float = 0.2): + """ + Tune the operation point selcetion stage. + + Args: + env(gym_electric_motor.ElectricMotorEnvironment): The environment to be controlled. + env_id(str): The id of the environment. + current_safety_margin(float): Percentage of the current margin to the current limit. + """ + + super().tune(env, env_id, current_safety_margin) + + self.t_count = 1001 + self.psi_count = 1000 + self.i_sd_count = 500 + self.i_sq_count = 1000 + + self.psi_abs_idx = env.state_names.index('psi_abs') + + self.t_minimum = -self.limit[self.torque_idx] + self.t_maximum = self.limit[self.torque_idx] + + self.l_m = self.mp['l_m'] + self.l_r = self.l_m + self.mp['l_sigr'] + self.l_s = self.l_m + self.mp['l_sigs'] + self.r_r = self.mp['r_r'] + self.r_s = self.mp['r_s'] + self.p = self.mp['p'] + self.tau = env.physical_system.tau + self.psi_controller.tune(env, env_id, 4, self.l_s / self.r_s) + + self.psi_opt_t = self._psi_opt() + self.psi_max = np.max(self.psi_opt_t[1]) + self.t_max_psi = self._t_max() + + # Set the parameters of the modulation controller + self.i_gain = 1 / (self.l_s / (1.25 * self.r_s)) * (self.alpha - 1) / self.alpha ** 2 + self.k_ = 0.8 + self.psi_high = 0.1 * self.psi_max + self.psi_low = -self.psi_max + self.integrated_reset = 0.5 * self.psi_low # Reset value of the modulation controller + + # Methods to get the indices of the lists for maximum torque and optimal flux + def _get_psi_opt(self, torque): + """Get the optimal flux for a given torque""" + torque = np.clip(torque, self.t_minimum, self.t_maximum) + return int(round((torque - self.t_minimum) / (self.t_maximum - self.t_minimum) * (self.t_count - 1))) + + def _get_t_max(self, psi): + """Get the maximum torque for a given flux""" + psi = np.clip(psi, 0, self.psi_max) + return int(round(psi / self.psi_max * (self.psi_count - 1))) + + def _select_operating_point(self, state, reference): + """ + Calculate the current operation point for a given torque reference value. + + Args: + state(np.ndarray): The state of the environment. + reference(np.ndarray): The reference of the state. + + Returns: + current_reference(np.ndarray): references for the current control stage + """ + + psi = state[self.psi_abs_idx] + + # Get the optimal flux + psi_opt = self.psi_opt_t[1, self._get_psi_opt(reference[0])] + + # Limit the flux by the modulation controller + psi_max = self.modulation_control(state) + psi_opt = min(psi_opt, psi_max) + + # Limit the torque + t_max = self.t_max_psi[0, self.psi_count - self._get_t_max(psi_opt)] + torque = np.clip(reference[0], -np.abs(t_max), np.abs(t_max)) + + # Control the i_sd current with a PI-Controller + i_sd_ = self.psi_controller(np.array([psi]), np.array([psi_opt])) + i_sd = np.clip(i_sd_, -self.i_sd_limit, self.i_sd_limit) + if i_sd_ == i_sd: + self.psi_controller.integrate(np.array([psi]), np.array([psi_opt])) + i_sd = i_sd[0] + + # Calculate and limit the i_sq current + i_sq = np.clip(torque / max(psi, 0.001) * 2 / 3 / self.p * self.l_r / self.l_m, -self.i_sq_limit, + self.i_sq_limit) + if self.i_sd_limit < np.sqrt(i_sq ** 2 + i_sd ** 2): + i_sq = np.sign(i_sq) * np.sqrt(self.i_sd_limit ** 2 - i_sd ** 2) + + return np.array([i_sd, i_sq]) + + def reset(self): + """Reset the SCIM operation point selection""" + super().reset() + self.psi_controller.reset() diff --git a/gem_controllers/stages/operation_point_selection/series_dc_ops.py b/gem_controllers/stages/operation_point_selection/series_dc_ops.py new file mode 100644 index 00000000..8b5cbea1 --- /dev/null +++ b/gem_controllers/stages/operation_point_selection/series_dc_ops.py @@ -0,0 +1,49 @@ +import numpy as np + +import gem_controllers as gc +from .operation_point_selection import OperationPointSelection +from ... import parameter_reader as reader + + +class SeriesDcOperationPointSelection(OperationPointSelection): + """This class computes the current operation point of a SeriesDc Motor for a given torque reference value.""" + + @property + def cross_inductance(self): + """Cross inductance of the Series Dc motor""" + return self._cross_inductance + + @cross_inductance.setter + def cross_inductance(self, value): + self._cross_inductance = np.array(value, dtype=float) + + def __init__(self): + super().__init__() + self._cross_inductance = np.array([]) + + def _select_operating_point(self, state, reference): + """ + Calculate the current refrence values. + + Args: + state(np.ndarray): The state of the environment. + reference(np.ndarray): The reference of the state. + + Returns: + np.array: current reference values + """ + return np.sqrt(reference / self._cross_inductance) + + def tune(self, env, env_id, current_safety_margin=0.2): + """ + Tune the operation point selcetion stage. + + Args: + env(gym_electric_motor.ElectricMotorEnvironment): The environment to be controlled. + env_id(str): The id of the environment. + current_safety_margin(float): Percentage of the current margin to the current limit. + """ + + super().tune(env, env_id, current_safety_margin=current_safety_margin) + motor = gc.utils.get_motor_type(env_id) + self._cross_inductance = reader.l_prime_reader[motor](env) diff --git a/gem_controllers/stages/operation_point_selection/shunt_dc_ops.py b/gem_controllers/stages/operation_point_selection/shunt_dc_ops.py new file mode 100644 index 00000000..0dbf15aa --- /dev/null +++ b/gem_controllers/stages/operation_point_selection/shunt_dc_ops.py @@ -0,0 +1,107 @@ +import numpy as np + +from .operation_point_selection import OperationPointSelection +from ... import parameter_reader as reader + + +class ShuntDcOperationPointSelection(OperationPointSelection): + """This class computes the current operation point of a ShuntDc Motor for a given torque reference value.""" + + @property + def cross_inductance(self): + """Cross inductance of the Shunt Dc motor""" + return self._cross_inductance + + @cross_inductance.setter + def cross_inductance(self, value): + self._cross_inductance = np.array(value, dtype=float) + + @property + def i_e_idx(self): + """Index of the i_e current""" + return self._i_e_idx + + @i_e_idx.setter + def i_e_idx(self, value): + self._i_e_idx = int(value) + + @property + def i_a_idx(self): + """Index of the i_a current""" + return self._i_a_idx + + @i_a_idx.setter + def i_a_idx(self, value): + self._i_a_idx = int(value) + + @property + def i_a_limit(self): + """Limit of the i_a current""" + return self._i_a_limit + + @i_a_limit.setter + def i_a_limit(self, value): + self._i_a_limit = np.array(value, dtype=float) + + @property + def i_e_limit(self): + """Limit of the i_e current""" + return self._i_e_limit + + @i_e_limit.setter + def i_e_limit(self, value): + self._i_e_limit = np.array(value, dtype=float) + + def __init__(self): + super().__init__() + self._cross_inductance = np.array([]) + self._i_e_idx = None + self._i_a_idx = None + self._i_a_limit = np.array([]) + self._i_e_limit = np.array([]) + self._r = 0.0 + self._u_max = 0.0 + self._omega_idx = 0 + + def _select_operating_point(self, state, reference): + """ + Calculate the current refrence values. + + Args: + state(np.ndarray): The state of the environment. + reference(np.ndarray): The reference of the state. + + Returns: + np.array: current reference values + """ + + # If i_e is too high, set i_a current_reference to 0 to also lower i_e again. + if state[self._i_e_idx] > self._i_e_limit: + return -self._i_a_limit + if state[self._i_e_idx] < -self._i_e_limit: + return self._i_a_limit + + i_e = state[self._i_e_idx] + # avoid division by zero + if 0.0 <= i_e < 1e-4: + i_e = 1e-4 + elif 0.0 > i_e > -1e-4: + i_e = -1e-4 + return reference / self._cross_inductance / i_e + + def tune(self, env, env_id, current_safety_margin=0.2): + """ + Tune the operation point selcetion stage. + + Args: + env(gym_electric_motor.ElectricMotorEnvironment): The environment to be controlled. + env_id(str): The id of the environment. + current_safety_margin(float): Percentage of the current margin to the current limit. + """ + + super().tune(env, env_id, current_safety_margin) + self._cross_inductance = reader.l_prime_reader['ShuntDc'](env) + self._i_e_idx = env.state_names.index('i_e') + self._i_a_idx = env.state_names.index('i_a') + self._i_a_limit = env.limits[self._i_a_idx] * (1 - current_safety_margin) + self._i_e_limit = env.limits[self._i_e_idx] * (1 - current_safety_margin) diff --git a/gem_controllers/stages/stage.py b/gem_controllers/stages/stage.py new file mode 100644 index 00000000..f3192e64 --- /dev/null +++ b/gem_controllers/stages/stage.py @@ -0,0 +1,27 @@ +class Stage: + """The stage is the basic module in the gem-controller structure. """ + + def __call__(self, state, reference): + """The stages control function. + + Args: + state(numpy.ndarray): The denormalized state of the environment. + reference(numpy.ndarray): The actual reference value for this stage. + Returns: + numpy.ndarray: The new reference-value for the next state. + """ + raise NotImplementedError + + def reset(self): + """Resets the stage to an initial state (e.g. before a new episode starts).""" + pass + + def tune(self, env, env_id, **kwargs): + """Fits the stages parameters to the passed environment. + + Args: + env(gym_electric_motor.ElectricMotorEnvironment): The environment to be controlled. + env_id(str): The id of the environment. + **kwargs(dict): Optional further parameters to tune the stages. + """ + pass diff --git a/gem_controllers/torque_controller.py b/gem_controllers/torque_controller.py new file mode 100644 index 00000000..b548f1ab --- /dev/null +++ b/gem_controllers/torque_controller.py @@ -0,0 +1,164 @@ +import numpy as np + +import gym_electric_motor as gem +import gem_controllers as gc + + +class TorqueController(gc.GemController): + """This class forms the torque controller, for any motor.""" + + @property + def torque_to_current_stage(self) -> gc.stages.OperationPointSelection: + """Operation point selection stage""" + return self._operation_point_selection + + @torque_to_current_stage.setter + def torque_to_current_stage(self, value: gc.stages.OperationPointSelection): + self._operation_point_selection = value + + @property + def current_controller(self) -> gc.CurrentController: + """Subordinated current controller stage""" + return self._current_controller + + @current_controller.setter + def current_controller(self, value: gc.CurrentController): + self._current_controller = value + + @property + def current_reference(self) -> np.ndarray: + """Reference values of the current controller stage""" + return self._current_reference + + @property + def clipping_stage(self) -> gc.stages.clipping_stages.ClippingStage: + """Clipping stage of the torque controller stage""" + return self._clipping_stage + + @clipping_stage.setter + def clipping_stage(self, value: gc.stages.clipping_stages.ClippingStage): + self._clipping_stage = value + + @property + def t_n(self): + """Time constant of the current controller stage""" + return self._current_controller.t_n + + @property + def references(self): + refs = self._current_controller.references + refs.update(dict(zip(self._referenced_currents, self._current_reference))) + return refs + + @property + def referenced_states(self): + return np.append(self._current_controller.referenced_states, self._referenced_currents) + + @property + def maximum_reference(self): + return self._maximum_reference + + def __init__( + self, + env: (gem.core.ElectricMotorEnvironment, None) = None, + env_id: (str, None) = None, + current_controller: (gc.CurrentController, None) = None, + torque_to_current_stage: (gc.stages.OperationPointSelection, None) = None, + clipping_stage: (gc.stages.clipping_stages.ClippingStage, None) = None + ): + """ + Initilizes a torque control stage. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + env_id(str): The corresponding environment-id to specify the concrete environment. + current_controller(gc.CurrentController): The underlying current control stage. + torque_to_current_stage(gc.stages.OperationPointSelection): The operation point selection class of the + torque contol stage. + clipping_stage(gc.stages.clipping_stages.ClippingStage): Clipping stage of the torque control stage. + """ + + super().__init__() + + self._operation_point_selection = torque_to_current_stage + if env_id is not None and torque_to_current_stage is None: + self._operation_point_selection = gc.stages.torque_to_current_function[gc.utils.get_motor_type(env_id)]() + self._current_controller = current_controller + if env_id is not None and current_controller is None: + self._current_controller = gc.PICurrentController(env, env_id) + if env_id is not None and clipping_stage is None: + if gc.utils.get_motor_type(env_id) in gc.parameter_reader.dc_motors: + self._clipping_stage = gc.stages.clipping_stages.AbsoluteClippingStage('TC') + elif gc.utils.get_motor_type(env_id) == 'EESM': + self._clipping_stage = gc.stages.clipping_stages.CombinedClippingStage('TC') + else: # motor in ac_motors + self._clipping_stage = gc.stages.clipping_stages.SquaredClippingStage('TC') + self._current_reference = np.array([]) + self._referenced_currents = np.array([]) + self._maximum_reference = dict() + + def tune(self, env, env_id, current_safety_margin=0.2, tune_current_controller=True, **kwargs): + """ + Tune the components of the current control stage. + + Args: + env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. + env_id(str): The corresponding environment-id to specify the concrete environment. + current_safety_margin(float): Percentage indicating the maximum value for the current reference. + tune_current_controller(bool): Flag, if the underlying current control stage should be tuned. + """ + + if tune_current_controller: + self._current_controller.tune(env, env_id, **kwargs) + self._clipping_stage.tune(env, env_id, margin=current_safety_margin) + self._operation_point_selection.tune(env, env_id, current_safety_margin) + self._referenced_currents = gc.parameter_reader.currents[gc.utils.get_motor_type(env_id)] + for current, action_range_low, action_range_high in zip(self._referenced_currents, + self._clipping_stage.action_range[0], + self._clipping_stage.action_range[1]): + if current in ['i', 'i_a', 'i_e']: + self._maximum_reference[current] = [action_range_low, action_range_high] + + def torque_control(self, state, reference): + """ + Calculate the current refrences. + + Args: + state(np.array): actual state of the environment + reference(np.array): actual torque references + + Returns: + current references(np.array) + """ + + self._current_reference = self._operation_point_selection(state, reference) + return self._current_reference + + def control(self, state, reference): + """ + Claculate the reference values for the input voltages. + + Args: + state(np.array): state of the environment + reference(np.array): torque references + + Returns: + np.ndarray: voltage reference + """ + + # Calculate the current references + self._current_reference = self.torque_control(state, reference) + + # Clipping the current references + self._current_reference = self._clipping_stage(state, self._current_reference) + + # Calculate the voltage reference + reference = self._current_controller.current_control(state, self._current_reference) + + return reference + + def reset(self): + """Reset all components of the torque control stage and the underlying stages""" + self._current_controller.reset() + self._operation_point_selection.reset() + self._clipping_stage.reset() diff --git a/gem_controllers/utils.py b/gem_controllers/utils.py new file mode 100644 index 00000000..88937c66 --- /dev/null +++ b/gem_controllers/utils.py @@ -0,0 +1,51 @@ +import numpy as np + +import gym_electric_motor.physical_systems.converters as cv + + +def non_parameterized(*args, **kwargs): + raise Exception('Component not parameterized. Call the tune() function before the first control cycle.') + + +def disc_converter_actions(converter): + """Calculates the high, idle and low switching actions for each from a given finite converter for each + of the converters output voltages. + + Args: + converter(PowerElectronicConverter): The converter to read the switching levels from. + Returns: + high_action(np.ndarray): The high switching actions + idle_action(np.ndarray): The idle switching actions + low_action(np.ndarray): The low switching actions + + """ + if type(converter) is cv.FiniteMultiConverter: + high_actions = [] + idle_actions = [] + low_actions = [] + for subconverter in converter.subconverters: + high_actions.append(_converter_actions[subconverter]) + + +_converter_actions = { + cv.FiniteOneQuadrantConverter: np.array([[1], [0], [0]]), + cv.FiniteTwoQuadrantConverter: np.array([[1], [0], [2]]), + cv.FiniteFourQuadrantConverter: np.array([[1], [0], [2]]), + cv.FiniteB6BridgeConverter: np.array([[1, 1, 1], [0, 0, 0], [2, 2, 2]]) +} + + +def split_env_id(env_id: str): + return env_id.split('-')[:3] + + +def get_action_type(env_id: str): + return split_env_id(env_id)[0] + + +def get_control_task(env_id: str): + return split_env_id(env_id)[1] + + +def get_motor_type(env_id: str): + return split_env_id(env_id)[2] From dc592376343495c7de7fee9625522b00b01cb9be Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Mon, 22 Jan 2024 09:58:26 +0100 Subject: [PATCH 15/51] default for scale plots --- gym_electric_motor/core.py | 2 +- .../visualization/motor_dashboard_plots/base_plots.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/gym_electric_motor/core.py b/gym_electric_motor/core.py index 9df23790..41fe1430 100644 --- a/gym_electric_motor/core.py +++ b/gym_electric_motor/core.py @@ -202,7 +202,7 @@ def __init__( callbacks=(), constraints=(), physical_system_wrappers=(), - scale_plots=None, + scale_plots=False, **kwargs, ): """ diff --git a/gym_electric_motor/visualization/motor_dashboard_plots/base_plots.py b/gym_electric_motor/visualization/motor_dashboard_plots/base_plots.py index 682d39b7..8967ec34 100644 --- a/gym_electric_motor/visualization/motor_dashboard_plots/base_plots.py +++ b/gym_electric_motor/visualization/motor_dashboard_plots/base_plots.py @@ -140,6 +140,7 @@ def set_width(self, width): def set_env(self, env): super().set_env(env) self._tau = env.physical_system.tau + self._scale_plots_to_data = env.scale_plots self.reset_data() def reset_data(self): From c7610a2fcdb5cd847916eab0d413746dcfd34972 Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Mon, 22 Jan 2024 09:58:39 +0100 Subject: [PATCH 16/51] fixing test with newest numpy version --- .../controllers/torque_to_current_conversion.py | 5 +++-- .../integration_test_classic_controllers_dc_motor.py | 4 ++-- requirements.txt | 2 +- tests/test_physical_systems/test_mechanical_loads.py | 6 +++--- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/examples/classic_controllers/controllers/torque_to_current_conversion.py b/examples/classic_controllers/controllers/torque_to_current_conversion.py index a60a5a49..ca1f19e2 100644 --- a/examples/classic_controllers/controllers/torque_to_current_conversion.py +++ b/examples/classic_controllers/controllers/torque_to_current_conversion.py @@ -1,7 +1,6 @@ import numpy as np from matplotlib import pyplot as plt from matplotlib import cm -from mpl_toolkits.mplot3d import Axes3D from scipy.interpolate import griddata @@ -175,7 +174,9 @@ def mtpf(): # calculate all possible i_q currents for i_d currents i_q = ( - np.sqrt(psi_**2 - np.power(self.psi_p + self.l_d * i_d_, 2)) + np.sqrt( + psi_**2 - np.power(self.psi_p + self.l_d * i_d_, 2) + ) / self.l_q ) i_idx = np.where( diff --git a/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py b/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py index 1c9980d6..17ce3232 100644 --- a/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py +++ b/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py @@ -63,8 +63,8 @@ a (optional) tuning parameter of the symmetrical optimum (default: 4) """ - # controller = Controller.make(env, external_ref_plots=external_ref_plots) - controller = GemController.make(env, env_id=motor.env_id()) + controller = Controller.make(env, external_ref_plots=external_ref_plots) + # controller = GemController.make(env, env_id=motor.env_id()) (state, reference), _ = env.reset(seed=1337) print("state_names: ", motor.state_names()) diff --git a/requirements.txt b/requirements.txt index 428fdccb..8251cad4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ matplotlib>=3.1.2 numpy>=1.16.4 scipy>=1.4.1 -gym<0.24.0,>=0.15.4 +gym>=0.21.0 pytest>=5.2.2 pytest-cov gymnasium>=0.29.0 \ No newline at end of file diff --git a/tests/test_physical_systems/test_mechanical_loads.py b/tests/test_physical_systems/test_mechanical_loads.py index 6ef60bd0..24bc3eba 100644 --- a/tests/test_physical_systems/test_mechanical_loads.py +++ b/tests/test_physical_systems/test_mechanical_loads.py @@ -206,7 +206,7 @@ def test_PolynomialStaticLoad_MechanicalOde( test_torque = 2 load_parameter = dict(j_load=1e-4, a=0.01, b=0.02, c=0.03) op = PolynomialStaticLoad(load_parameter=load_parameter) - output_val = op.mechanical_ode(test_t, test_mechanical_state, test_torque) + output_val = op.mechanical_ode(test_t, test_mechanical_state, test_torque)[0] # output_val = concretePolynomialLoad.mechanical_ode(test_t, test_mechanical_state, test_torque) assert math.isclose(expected_result, output_val, abs_tol=1e-6) @@ -235,7 +235,7 @@ def test_initialization(self): assert load.omega_fixed == 60 def test_mechanical_ode(self, const_speed_load): - assert all(const_speed_load.mechanical_ode() == np.array([0])) + assert all(const_speed_load.mechanical_ode() == 0) def test_reset(self, const_speed_load): test_positions = {"omega": 0} @@ -300,7 +300,7 @@ def test_mechanical_ode(self, ext_speed_load, omega, expected_result): test_mechanical_state = np.array([omega]) test_t = 1 op = ext_speed_load - output_val = op.mechanical_ode(test_t, test_mechanical_state) + output_val = op.mechanical_ode(test_t, test_mechanical_state)[0] assert math.isclose(expected_result, output_val, abs_tol=1e-6) def test_reset(self, ext_speed_load): From e203255602301be23112e9244eadd18ba5a8ac9e Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Mon, 29 Jan 2024 16:22:06 +0100 Subject: [PATCH 17/51] WIP --- ...ation_test_classic_controllers_dc_motor.py | 6 +-- gem_controllers/stages/disc_output_stage.py | 22 +++++++---- gym_electric_motor/__init__.py | 13 ++++--- .../visualization/motor_dashboard.py | 39 +++++-------------- pyproject.toml | 37 ++++++++++++++++++ requirements.txt | 1 - .../test_environment_seeding.py | 8 ++-- 7 files changed, 76 insertions(+), 50 deletions(-) create mode 100644 pyproject.toml diff --git a/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py b/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py index 17ce3232..5b44dc5b 100644 --- a/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py +++ b/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py @@ -1,11 +1,11 @@ from classic_controllers import Controller from externally_referenced_state_plot import ExternallyReferencedStatePlot -from gem_controllers.gem_controller import GemController + +# from gem_controllers.gem_controller import GemController import gym_electric_motor as gem from gym_electric_motor.envs.motors import MotorType, ControlType, ActionType, Motor from gym_electric_motor.visualization import MotorDashboard, RenderMode from gym_electric_motor.reference_generators import SinusoidalReferenceGenerator -import time if __name__ == "__main__": """ @@ -85,5 +85,5 @@ env.close() - motor_dashboard.save_to_file(filename="integration_test") + # motor_dashboard.save_to_file(filename="integration_test") motor_dashboard.show_and_hold() diff --git a/gem_controllers/stages/disc_output_stage.py b/gem_controllers/stages/disc_output_stage.py index a5232bda..a214bc0e 100644 --- a/gem_controllers/stages/disc_output_stage.py +++ b/gem_controllers/stages/disc_output_stage.py @@ -1,5 +1,5 @@ import numpy as np -import gym +import gymnasium from gym_electric_motor.physical_systems import converters as cv from .stage import Stage @@ -82,7 +82,9 @@ def to_action(self, _state, reference): action(np.ndarray): volatge vector """ conditions = [reference <= self.low_level, reference >= self.high_level] - return np.select(conditions, [self.low_action, self.high_action], default=self.idle_action) + return np.select( + conditions, [self.low_action, self.high_action], default=self.idle_action + ) def tune(self, env, env_id, **__): """ @@ -106,7 +108,7 @@ def tune(self, env, env_id, **__): self.low_level = -0.33 * (voltage_range[1] - voltage_range[0]) self.high_level = 0.33 * (voltage_range[1] - voltage_range[0]) - if type(env.action_space) == gym.spaces.MultiDiscrete: + if type(env.action_space) == gymnasium.spaces.MultiDiscrete: self.output_stage = DiscOutputStage.to_multi_discrete self.low_action = [] self.idle_action = [] @@ -117,14 +119,20 @@ def tune(self, env, env_id, **__): self.idle_action.append(idle_action) self.high_action.append(high_action) - elif type(env.action_space) == gym.spaces.Discrete \ - and type(env.physical_system.converter) != cv.FiniteB6BridgeConverter: + elif ( + type(env.action_space) == gymnasium.spaces.Discrete + and type(env.physical_system.converter) != cv.FiniteB6BridgeConverter + ): self.output_stage = DiscOutputStage.to_discrete - self.low_action, self.idle_action, self.high_action = self._get_actions(env.action_space.n) + self.low_action, self.idle_action, self.high_action = self._get_actions( + env.action_space.n + ) elif type(env.physical_system.converter) == cv.FiniteB6BridgeConverter: self.output_stage = DiscOutputStage.to_b6_discrete else: - raise Exception(f'No discrete output stage available for action space {env.action_space}.') + raise Exception( + f"No discrete output stage available for action space {env.action_space}." + ) @staticmethod def _get_actions(n): diff --git a/gym_electric_motor/__init__.py b/gym_electric_motor/__init__.py index 1c2d553e..a6fd0278 100644 --- a/gym_electric_motor/__init__.py +++ b/gym_electric_motor/__init__.py @@ -30,12 +30,13 @@ # Add all superclasses of the modules to the registry. # Deactivate the order enforce wrapper that is put around a created env per default from gymnasium-version 0.21.0 onwards -registration_kwargs = ( - dict(order_enforce=False) - if version.parse(gymnasium.__version__) >= version.parse("0.21.0") - else dict() -) -registration_kwargs["disable_env_checker"] = True +# registration_kwargs = ( +# dict(order_enforce=False) +# if version.parse(gymnasium.__version__) >= version.parse("0.21.0") +# else dict() +# ) +# registration_kwargs["disable_env_checker"] = True +registration_kwargs = dict() envs_path = "gym_electric_motor.envs:" diff --git a/gym_electric_motor/visualization/motor_dashboard.py b/gym_electric_motor/visualization/motor_dashboard.py index 9e71f5db..c2519422 100644 --- a/gym_electric_motor/visualization/motor_dashboard.py +++ b/gym_electric_motor/visualization/motor_dashboard.py @@ -73,9 +73,7 @@ def __init__( """ # Basic assertions assert type(reward_plot) is bool - assert all( - isinstance(ap, (TimePlot, EpisodePlot, StepPlot)) for ap in additional_plots - ) + assert all(isinstance(ap, (TimePlot, EpisodePlot, StepPlot)) for ap in additional_plots) assert type(update_interval) in [int, float] assert update_interval > 0 assert type(time_plot_width) in [int, float] @@ -100,12 +98,8 @@ def __init__( self._reward_plot = reward_plot # Separate the additional plots into StepPlots, EpisodicPlots and StepPlots - self._custom_time_plots = [ - p for p in additional_plots if isinstance(p, TimePlot) - ] - self._episodic_plots = [ - p for p in additional_plots if isinstance(p, EpisodePlot) - ] + self._custom_time_plots = [p for p in additional_plots if isinstance(p, TimePlot)] + self._episodic_plots = [p for p in additional_plots if isinstance(p, EpisodePlot)] self._step_plots = [p for p in additional_plots if isinstance(p, StepPlot)] self._time_plots = [] @@ -162,11 +156,7 @@ def on_step_end(self, k, state, reference, reward, terminated): def render(self): """Updates the plots every *update cycle* calls of this method.""" if ( - not ( - self._time_plot_figure - or self._episodic_plot_figure - or self._step_plot_figure - ) + not (self._time_plot_figure or self._episodic_plot_figure or self._step_plot_figure) and len(self._plots) > 0 ): self.initialize() @@ -232,9 +222,7 @@ def reset_figures(self): a jupyter notebook and the figures shall be plotted below a new cell.""" for plot in self._plots: plot.reset_data() - self._episodic_plot_figure = ( - self._time_plot_figure - ) = self._step_plot_figure = None + self._episodic_plot_figure = self._time_plot_figure = self._step_plot_figure = None self._figures = [] def initialize(self): @@ -251,9 +239,7 @@ def initialize(self): def _initialize_figures_notebook(self): # Create all plots below each other: First Time then Episode then Step Plots - no_of_plots = ( - len(self._episodic_plots) + len(self._step_plots) + len(self._time_plots) - ) + no_of_plots = len(self._episodic_plots) + len(self._step_plots) + len(self._time_plots) if no_of_plots == 0: return @@ -284,9 +270,7 @@ def _initialize_figures_notebook(self): def _initialize_figures_window(self): # create separate figures for time based, step and episode based plots if len(self._episodic_plots) > 0: - self._episodic_plot_figure, axes_ep = plt.subplots( - len(self._episodic_plots), sharex=True - ) + self._episodic_plot_figure, axes_ep = plt.subplots(len(self._episodic_plots), sharex=True) axes_ep = [axes_ep] if len(self._episodic_plots) == 1 else axes_ep self._episodic_plot_figure.subplots_adjust(wspace=0.0, hspace=0.02) self._episodic_plot_figure.canvas.manager.set_window_title("Episodic Plots") @@ -296,9 +280,7 @@ def _initialize_figures_window(self): plot.initialize(axis) if len(self._step_plots) > 0: - self._step_plot_figure, axes_int = plt.subplots( - len(self._step_plots), sharex=True - ) + self._step_plot_figure, axes_int = plt.subplots(len(self._step_plots), sharex=True) axes_int = [axes_int] if len(self._step_plots) == 1 else axes_int self._step_plot_figure.canvas.manager.set_window_title("Step Plots") self._step_plot_figure.subplots_adjust(wspace=0.0, hspace=0.02) @@ -308,9 +290,7 @@ def _initialize_figures_window(self): plot.initialize(axis) if len(self._time_plots) > 0: - self._time_plot_figure, axes_step = plt.subplots( - len(self._time_plots), sharex=True - ) + self._time_plot_figure, axes_step = plt.subplots(len(self._time_plots), sharex=True) self._time_plot_figure.canvas.manager.set_window_title("Time Plots") axes_step = [axes_step] if len(self._time_plots) == 1 else axes_step self._time_plot_figure.subplots_adjust(wspace=0.0, hspace=0.2) @@ -379,6 +359,7 @@ def show(self): plt.show(block=False) def show_and_hold(self): + self.force_render() plt.show(block=True) def force_render(self): diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..4eb391ee --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,37 @@ +[project] +name = "gym_eletric_motor" +version = "2.1.0" +authors = [ + "Arne Traue", + "Gerrit Book", + "Praneeth Balakrishna", + "Pascal Peters", + "Pramod Manjunatha", + "Darius Jakobeit", + "Felix Book", + "Max Schenke", + "Wilhelm Kirchgässner", + "Oliver Wallscheid", + "Barnabas Haucke-Korber", + "Stefan Arndt", + "Marius Köhler", +] +description = "A Farama Gymnasium environment for electric motor control." +readme = "README.md" +requires-python = ">=3.8" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] + +[project.urls] +Homepage = "https://github.com/upb-lea/gym-electric-motor" +Issues = "https://github.com/upb-lea/gym-electric-motor/issues" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.ruff] +line-length = 120 diff --git a/requirements.txt b/requirements.txt index 8251cad4..5d2561d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ matplotlib>=3.1.2 numpy>=1.16.4 scipy>=1.4.1 -gym>=0.21.0 pytest>=5.2.2 pytest-cov gymnasium>=0.29.0 \ No newline at end of file diff --git a/tests/integration_tests/test_environment_seeding.py b/tests/integration_tests/test_environment_seeding.py index 9332c2dd..23da3554 100644 --- a/tests/integration_tests/test_environment_seeding.py +++ b/tests/integration_tests/test_environment_seeding.py @@ -41,7 +41,7 @@ def test_seeding_same_env( # Execute the env for i in range(no_of_steps): if terminated: - state, reference = env.reset(seed) + state, reference = env.reset(seed=seed) (state, reference), reward, terminated, truncated, info = env.step(actions[i]) rewards1.append(reward) states1.append(state) @@ -56,7 +56,7 @@ def test_seeding_same_env( # Execute the environment again for i in range(no_of_steps): if terminated: - state, reference = env.reset(seed) + state, reference = env.reset(seed=seed) (state, reference), reward, terminated, truncated, info = env.step(actions[i]) rewards2.append(reward) states2.append(state) @@ -94,7 +94,7 @@ def test_seeding_new_env( for i in range(no_of_steps): if terminated: - state, reference = env.reset(seed) + state, reference = env.reset(seed=seed) (state, reference), reward, terminated, truncated, info = env.step(actions[i]) rewards1.append(reward) states1.append(state) @@ -110,7 +110,7 @@ def test_seeding_new_env( for i in range(no_of_steps): if terminated: - state, reference = env.reset(seed) + state, reference = env.reset(seed=seed) action = env.action_space.sample() assert action in env.action_space (state, reference), reward, terminated, truncated, info = env.step(actions[i]) From dfe181c22e045f783dd7be5bb52a5174df040672 Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Sun, 4 Feb 2024 11:57:14 +0100 Subject: [PATCH 18/51] Moved to src directory and reformated all files with ruff --- DEVELOPMENT.md | 5 + .../block_diagrams/stage_blocks/eesm_cc.py | 192 -------- .../block_diagrams/stage_blocks/pmsm_cc.py | 187 -------- .../block_diagrams/stage_blocks/scim_cc.py | 164 ------- .../block_diagrams/stage_blocks/synrm_cc.py | 179 ------- gem_controllers/parameter_reader.py | 358 -------------- pyproject.toml | 30 +- requirements.txt | 3 +- .../gem_controllers}/README.md | 0 .../gem_controllers}/__init__.py | 1 - .../block_diagrams/__init__.py | 0 .../block_diagrams/block_diagram.py | 111 +++-- .../block_diagrams/stage_blocks/__init__.py | 0 .../block_diagrams/stage_blocks/eesm_cc.py | 326 +++++++++++++ .../block_diagrams/stage_blocks/eesm_ops.py | 158 ++++--- .../stage_blocks/eesm_output.py | 26 +- .../stage_blocks/ext_ex_dc_cc.py | 83 ++-- .../stage_blocks/ext_ex_dc_ops.py | 35 +- .../stage_blocks/ext_ex_dc_output.py | 48 +- .../stage_blocks/perm_ex_dc_cc.py | 60 ++- .../stage_blocks/perm_ex_dc_ops.py | 15 +- .../stage_blocks/perm_ex_dc_output.py | 37 +- .../stage_blocks/pi_speed_controller.py | 29 +- .../block_diagrams/stage_blocks/pmsm_cc.py | 299 ++++++++++++ .../block_diagrams/stage_blocks/pmsm_ops.py | 160 ++++--- .../stage_blocks/pmsm_output.py | 12 +- .../block_diagrams/stage_blocks/pmsm_sc.py | 32 +- .../block_diagrams/stage_blocks/scim_cc.py | 256 ++++++++++ .../block_diagrams/stage_blocks/scim_ops.py | 194 +++++--- .../stage_blocks/scim_output.py | 13 +- .../block_diagrams/stage_blocks/scim_sc.py | 29 +- .../stage_blocks/series_dc_cc.py | 60 ++- .../stage_blocks/series_dc_ops.py | 17 +- .../stage_blocks/series_dc_output.py | 34 +- .../stage_blocks/shunt_dc_cc.py | 61 ++- .../stage_blocks/shunt_dc_ops.py | 15 +- .../stage_blocks/shunt_dc_output.py | 34 +- .../block_diagrams/stage_blocks/synrm_cc.py | 285 ++++++++++++ .../stage_blocks/synrm_output.py | 12 +- .../gem_controllers}/cascaded_controller.py | 0 .../gem_controllers}/current_controller.py | 0 .../gem_controllers}/gem_adapter.py | 4 +- .../gem_controllers}/gem_controller.py | 8 +- src/gem_controllers/parameter_reader.py | 439 ++++++++++++++++++ .../gem_controllers}/pi_current_controller.py | 29 +- .../gem_controllers}/pi_speed_controller.py | 20 +- .../gem_controllers}/reference_plotter.py | 16 +- .../gem_controllers}/stages/__init__.py | 0 .../stages/abc_transformation.py | 10 +- .../gem_controllers}/stages/anti_windup.py | 12 +- .../stages/base_controllers/__init__.py | 0 .../base_controllers/base_controller.py | 2 +- .../e_base_controller_task.py | 18 +- .../stages/base_controllers/i_controller.py | 0 .../stages/base_controllers/p_controller.py | 9 +- .../stages/base_controllers/pi_controller.py | 13 +- .../stages/base_controllers/pid_controller.py | 1 - .../three_point_controller.py | 4 +- .../stages/base_controllers/utils.py | 12 +- .../stages/clipping_stages/__init__.py | 0 .../absolute_clipping_stage.py | 12 +- .../stages/clipping_stages/clipping_stage.py | 0 .../combined_clipping_stage.py | 39 +- .../clipping_stages/squared_clipping_stage.py | 21 +- .../stages/cont_output_stage.py | 2 +- .../stages/disc_output_stage.py | 12 +- .../stages/emf_feedforward.py | 2 +- .../stages/emf_feedforward_eesm.py | 15 +- .../stages/emf_feedforward_ind.py | 22 +- .../gem_controllers}/stages/input_stage.py | 0 .../operation_point_selection/__init__.py | 0 .../operation_point_selection/eesm_ops.py | 99 ++-- .../operation_point_selection/extex_dc_ttc.py | 12 +- .../foc_operation_point_selection.py | 38 +- .../operation_point_selection.py | 0 .../operation_point_selection/ops_utils.py | 16 +- .../permex_dc_ops.py | 4 +- .../operation_point_selection/pmsm_ops.py | 120 +++-- .../operation_point_selection/scim_ops.py | 65 +-- .../series_dc_ops.py | 0 .../operation_point_selection/shunt_dc_ops.py | 6 +- .../gem_controllers}/stages/stage.py | 2 +- .../gem_controllers}/torque_controller.py | 28 +- .../gem_controllers}/utils.py | 6 +- .../gym_electric_motor}/__init__.py | 168 +++---- .../gym_electric_motor}/callbacks.py | 39 +- .../gym_electric_motor}/constraints.py | 16 +- .../gym_electric_motor}/core.py | 55 +-- .../gym_electric_motor}/envs/__init__.py | 0 .../envs/gym_dcm/__init__.py | 0 .../gym_dcm/extex_dc_motor_env/__init__.py | 0 .../cont_cc_extex_dc_env.py | 12 +- .../cont_sc_extex_dc_env.py | 8 +- .../cont_tc_extex_dc_env.py | 12 +- .../finite_cc_extex_dc_env.py | 12 +- .../finite_sc_extex_dc_env.py | 8 +- .../finite_tc_extex_dc_env.py | 12 +- .../gym_dcm/permex_dc_motor_env/__init__.py | 0 .../cont_cc_permex_dc_env.py | 12 +- .../cont_sc_permex_dc_env.py | 8 +- .../cont_tc_permex_dc_env.py | 12 +- .../finite_cc_permex_dc_env.py | 12 +- .../finite_sc_permex_dc_env.py | 8 +- .../finite_tc_permex_dc_env.py | 12 +- .../gym_dcm/series_dc_motor_env/__init__.py | 0 .../cont_cc_series_dc_env.py | 8 +- .../cont_sc_series_dc_env.py | 4 +- .../cont_tc_series_dc_env.py | 8 +- .../finite_cc_series_dc_env.py | 8 +- .../finite_sc_series_dc_env.py | 4 +- .../finite_tc_series_dc_env.py | 8 +- .../gym_dcm/shunt_dc_motor_env/__init__.py | 0 .../cont_cc_shunt_dc_env.py | 11 +- .../cont_sc_shunt_dc_env.py | 7 +- .../cont_tc_shunt_dc_env.py | 11 +- .../finite_cc_shunt_dc_env.py | 11 +- .../finite_sc_shunt_dc_env.py | 7 +- .../finite_tc_shunt_dc_env.py | 11 +- .../envs/gym_eesm/__init__.py | 0 .../envs/gym_eesm/cont_cc_eesm_env.py | 12 +- .../envs/gym_eesm/cont_sc_eesm_env.py | 8 +- .../envs/gym_eesm/cont_tc_eesm_env.py | 12 +- .../envs/gym_eesm/finite_cc_eesm_env.py | 16 +- .../envs/gym_eesm/finite_sc_eesm_env.py | 8 +- .../envs/gym_eesm/finite_tc_eesm_env.py | 12 +- .../envs/gym_im/__init__.py | 0 .../__init__.py | 0 .../cont_cc_dfim_env.py | 12 +- .../cont_sc_dfim_env.py | 8 +- .../cont_tc_dfim_env.py | 12 +- .../finite_cc_dfim_env.py | 12 +- .../finite_sc_dfim_env.py | 8 +- .../finite_tc_dfim_env.py | 12 +- .../__init__.py | 0 .../cont_cc_scim_env.py | 16 +- .../cont_sc_scim_env.py | 12 +- .../cont_tc_scim_env.py | 16 +- .../finite_cc_scim_env.py | 12 +- .../finite_sc_scim_env.py | 8 +- .../finite_tc_scim_env.py | 12 +- .../envs/gym_pmsm/__init__.py | 0 .../envs/gym_pmsm/cont_cc_pmsm_env.py | 16 +- .../envs/gym_pmsm/cont_sc_pmsm_env.py | 12 +- .../envs/gym_pmsm/cont_tc_pmsm_env.py | 16 +- .../envs/gym_pmsm/finite_cc_pmsm_env.py | 12 +- .../envs/gym_pmsm/finite_sc_pmsm_env.py | 8 +- .../envs/gym_pmsm/finite_tc_pmsm_env.py | 12 +- .../gym_srm/srm_continuous_control_env.py | 0 .../envs/gym_srm/srm_finite_control_env.py | 0 .../envs/gym_synrm/__init__.py | 0 .../envs/gym_synrm/cont_cc_synrm_env.py | 16 +- .../envs/gym_synrm/cont_sc_synrm_env.py | 12 +- .../envs/gym_synrm/cont_tc_synrm_env.py | 16 +- .../envs/gym_synrm/finite_cc_synrm_env.py | 12 +- .../envs/gym_synrm/finite_sc_synrm_env.py | 8 +- .../envs/gym_synrm/finite_tc_synrm_env.py | 12 +- .../gym_electric_motor}/envs/motors.py | 0 .../physical_system_wrappers/__init__.py | 0 .../cos_sin_processor.py | 27 +- .../current_sum_processor.py | 12 +- .../dead_time_processor.py | 4 +- .../dq_to_abc_action_processor.py | 21 +- .../physical_system_wrappers/flux_observer.py | 32 +- .../physical_system_wrapper.py | 4 +- .../state_noise_processor.py | 12 +- .../physical_systems/__init__.py | 8 +- .../physical_systems/converters.py | 85 +--- .../electric_motors/__init__.py | 0 .../dc_externally_excited_motor.py | 3 +- .../electric_motors/dc_motor.py | 44 +- .../dc_permanently_excited_motor.py | 4 +- .../electric_motors/dc_series_motor.py | 25 +- .../electric_motors/dc_shunt_motor.py | 11 +- .../doubly_fed_induction_motor.py | 20 +- .../electric_motors/electric_motor.py | 59 +-- .../externally_excited_synchronous_motor.py | 38 +- .../electric_motors/induction_motor.py | 18 +- .../permanent_magnet_synchronous_motor.py | 28 +- .../squirrel_cage_induction_motor.py | 24 +- .../electric_motors/synchronous_motor.py | 12 +- .../synchronous_reluctance_motor.py | 19 +- .../electric_motors/three_phase_motor.py | 4 +- .../mechanical_loads/__init__.py | 0 .../mechanical_loads/constant_speed_load.py | 0 .../mechanical_loads/external_speed_load.py | 4 +- .../mechanical_loads/mechanical_load.py | 39 +- .../ornstein_uhlenbeck_load.py | 8 +- .../polynomial_static_load.py | 24 +- .../physical_systems/physical_systems.py | 189 ++------ .../physical_systems/solvers.py | 12 +- .../physical_systems/voltage_supplies.py | 28 +- .../gym_electric_motor}/random_component.py | 0 .../reference_generators/__init__.py | 8 +- .../const_reference_generator.py | 8 +- .../laplace_process_reference_generator.py | 4 +- .../multiple_reference_generator.py | 38 +- .../sawtooth_reference_generator.py | 17 +- .../sinusoidal_reference_generator.py | 13 +- .../step_reference_generator.py | 12 +- .../subepisoded_reference_generator.py | 24 +- .../switched_reference_generator.py | 14 +- .../triangle_reference_generator.py | 16 +- .../wiener_process_reference_generator.py | 4 +- .../zero_reference_generator.py | 4 +- .../reward_functions/__init__.py | 0 .../weighted_sum_of_errors.py | 12 +- .../gym_electric_motor}/utils.py | 12 +- .../visualization/__init__.py | 0 .../visualization/console_printer.py | 0 .../visualization/motor_dashboard.py | 7 +- .../motor_dashboard_plots/__init__.py | 0 .../motor_dashboard_plots/action_plot.py | 0 .../motor_dashboard_plots/base_plots.py | 4 +- .../cumulative_constraint_violation_plot.py | 0 .../episode_length_plot.py | 0 .../mean_episode_reward_plot.py | 12 +- .../motor_dashboard_plots/reward_plot.py | 4 +- .../motor_dashboard_plots/state_plot.py | 32 +- .../visualization/render_modes.py | 0 .../test_environment_execution.py | 18 +- .../test_environment_seeding.py | 14 +- .../test_physical_systems/test_converters.py | 385 ++++----------- .../test_reward_functions.py | 12 +- 223 files changed, 3368 insertions(+), 3666 deletions(-) create mode 100644 DEVELOPMENT.md delete mode 100644 gem_controllers/block_diagrams/stage_blocks/eesm_cc.py delete mode 100644 gem_controllers/block_diagrams/stage_blocks/pmsm_cc.py delete mode 100644 gem_controllers/block_diagrams/stage_blocks/scim_cc.py delete mode 100644 gem_controllers/block_diagrams/stage_blocks/synrm_cc.py delete mode 100644 gem_controllers/parameter_reader.py rename {gem_controllers => src/gem_controllers}/README.md (100%) rename {gem_controllers => src/gem_controllers}/__init__.py (99%) rename {gem_controllers => src/gem_controllers}/block_diagrams/__init__.py (100%) rename {gem_controllers => src/gem_controllers}/block_diagrams/block_diagram.py (56%) rename {gem_controllers => src/gem_controllers}/block_diagrams/stage_blocks/__init__.py (100%) create mode 100644 src/gem_controllers/block_diagrams/stage_blocks/eesm_cc.py rename {gem_controllers => src/gem_controllers}/block_diagrams/stage_blocks/eesm_ops.py (55%) rename {gem_controllers => src/gem_controllers}/block_diagrams/stage_blocks/eesm_output.py (67%) rename {gem_controllers => src/gem_controllers}/block_diagrams/stage_blocks/ext_ex_dc_cc.py (63%) rename {gem_controllers => src/gem_controllers}/block_diagrams/stage_blocks/ext_ex_dc_ops.py (77%) rename {gem_controllers => src/gem_controllers}/block_diagrams/stage_blocks/ext_ex_dc_output.py (56%) rename {gem_controllers => src/gem_controllers}/block_diagrams/stage_blocks/perm_ex_dc_cc.py (53%) rename {gem_controllers => src/gem_controllers}/block_diagrams/stage_blocks/perm_ex_dc_ops.py (71%) rename {gem_controllers => src/gem_controllers}/block_diagrams/stage_blocks/perm_ex_dc_output.py (65%) rename {gem_controllers => src/gem_controllers}/block_diagrams/stage_blocks/pi_speed_controller.py (54%) create mode 100644 src/gem_controllers/block_diagrams/stage_blocks/pmsm_cc.py rename {gem_controllers => src/gem_controllers}/block_diagrams/stage_blocks/pmsm_ops.py (53%) rename {gem_controllers => src/gem_controllers}/block_diagrams/stage_blocks/pmsm_output.py (75%) rename {gem_controllers => src/gem_controllers}/block_diagrams/stage_blocks/pmsm_sc.py (63%) create mode 100644 src/gem_controllers/block_diagrams/stage_blocks/scim_cc.py rename {gem_controllers => src/gem_controllers}/block_diagrams/stage_blocks/scim_ops.py (50%) rename {gem_controllers => src/gem_controllers}/block_diagrams/stage_blocks/scim_output.py (75%) rename {gem_controllers => src/gem_controllers}/block_diagrams/stage_blocks/scim_sc.py (59%) rename {gem_controllers => src/gem_controllers}/block_diagrams/stage_blocks/series_dc_cc.py (53%) rename {gem_controllers => src/gem_controllers}/block_diagrams/stage_blocks/series_dc_ops.py (72%) rename {gem_controllers => src/gem_controllers}/block_diagrams/stage_blocks/series_dc_output.py (65%) rename {gem_controllers => src/gem_controllers}/block_diagrams/stage_blocks/shunt_dc_cc.py (52%) rename {gem_controllers => src/gem_controllers}/block_diagrams/stage_blocks/shunt_dc_ops.py (71%) rename {gem_controllers => src/gem_controllers}/block_diagrams/stage_blocks/shunt_dc_output.py (64%) create mode 100644 src/gem_controllers/block_diagrams/stage_blocks/synrm_cc.py rename {gem_controllers => src/gem_controllers}/block_diagrams/stage_blocks/synrm_output.py (74%) rename {gem_controllers => src/gem_controllers}/cascaded_controller.py (100%) rename {gem_controllers => src/gem_controllers}/current_controller.py (100%) rename {gem_controllers => src/gem_controllers}/gem_adapter.py (97%) rename {gem_controllers => src/gem_controllers}/gem_controller.py (96%) create mode 100644 src/gem_controllers/parameter_reader.py rename {gem_controllers => src/gem_controllers}/pi_current_controller.py (89%) rename {gem_controllers => src/gem_controllers}/pi_speed_controller.py (92%) rename {gem_controllers => src/gem_controllers}/reference_plotter.py (82%) rename {gem_controllers => src/gem_controllers}/stages/__init__.py (100%) rename {gem_controllers => src/gem_controllers}/stages/abc_transformation.py (88%) rename {gem_controllers => src/gem_controllers}/stages/anti_windup.py (90%) rename {gem_controllers => src/gem_controllers}/stages/base_controllers/__init__.py (100%) rename {gem_controllers => src/gem_controllers}/stages/base_controllers/base_controller.py (96%) rename {gem_controllers => src/gem_controllers}/stages/base_controllers/e_base_controller_task.py (67%) rename {gem_controllers => src/gem_controllers}/stages/base_controllers/i_controller.py (100%) rename {gem_controllers => src/gem_controllers}/stages/base_controllers/p_controller.py (96%) rename {gem_controllers => src/gem_controllers}/stages/base_controllers/pi_controller.py (94%) rename {gem_controllers => src/gem_controllers}/stages/base_controllers/pid_controller.py (99%) rename {gem_controllers => src/gem_controllers}/stages/base_controllers/three_point_controller.py (97%) rename {gem_controllers => src/gem_controllers}/stages/base_controllers/utils.py (62%) rename {gem_controllers => src/gem_controllers}/stages/clipping_stages/__init__.py (100%) rename {gem_controllers => src/gem_controllers}/stages/clipping_stages/absolute_clipping_stage.py (89%) rename {gem_controllers => src/gem_controllers}/stages/clipping_stages/clipping_stage.py (100%) rename {gem_controllers => src/gem_controllers}/stages/clipping_stages/combined_clipping_stage.py (75%) rename {gem_controllers => src/gem_controllers}/stages/clipping_stages/squared_clipping_stage.py (84%) rename {gem_controllers => src/gem_controllers}/stages/cont_output_stage.py (95%) rename {gem_controllers => src/gem_controllers}/stages/disc_output_stage.py (93%) rename {gem_controllers => src/gem_controllers}/stages/emf_feedforward.py (98%) rename {gem_controllers => src/gem_controllers}/stages/emf_feedforward_eesm.py (91%) rename {gem_controllers => src/gem_controllers}/stages/emf_feedforward_ind.py (71%) rename {gem_controllers => src/gem_controllers}/stages/input_stage.py (100%) rename {gem_controllers => src/gem_controllers}/stages/operation_point_selection/__init__.py (100%) rename {gem_controllers => src/gem_controllers}/stages/operation_point_selection/eesm_ops.py (70%) rename {gem_controllers => src/gem_controllers}/stages/operation_point_selection/extex_dc_ttc.py (90%) rename {gem_controllers => src/gem_controllers}/stages/operation_point_selection/foc_operation_point_selection.py (80%) rename {gem_controllers => src/gem_controllers}/stages/operation_point_selection/operation_point_selection.py (100%) rename {gem_controllers => src/gem_controllers}/stages/operation_point_selection/ops_utils.py (53%) rename {gem_controllers => src/gem_controllers}/stages/operation_point_selection/permex_dc_ops.py (97%) rename {gem_controllers => src/gem_controllers}/stages/operation_point_selection/pmsm_ops.py (75%) rename {gem_controllers => src/gem_controllers}/stages/operation_point_selection/scim_ops.py (74%) rename {gem_controllers => src/gem_controllers}/stages/operation_point_selection/series_dc_ops.py (100%) rename {gem_controllers => src/gem_controllers}/stages/operation_point_selection/shunt_dc_ops.py (94%) rename {gem_controllers => src/gem_controllers}/stages/stage.py (98%) rename {gem_controllers => src/gem_controllers}/torque_controller.py (88%) rename {gem_controllers => src/gem_controllers}/utils.py (89%) rename {gym_electric_motor => src/gym_electric_motor}/__init__.py (65%) rename {gym_electric_motor => src/gym_electric_motor}/callbacks.py (79%) rename {gym_electric_motor => src/gym_electric_motor}/constraints.py (88%) rename {gym_electric_motor => src/gym_electric_motor}/core.py (92%) rename {gym_electric_motor => src/gym_electric_motor}/envs/__init__.py (100%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_dcm/__init__.py (100%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_dcm/extex_dc_motor_env/__init__.py (100%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_dcm/extex_dc_motor_env/cont_cc_extex_dc_env.py (95%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_dcm/extex_dc_motor_env/cont_sc_extex_dc_env.py (97%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_dcm/extex_dc_motor_env/cont_tc_extex_dc_env.py (95%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_dcm/extex_dc_motor_env/finite_cc_extex_dc_env.py (95%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_dcm/extex_dc_motor_env/finite_sc_extex_dc_env.py (97%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_dcm/extex_dc_motor_env/finite_tc_extex_dc_env.py (95%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_dcm/permex_dc_motor_env/__init__.py (100%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_dcm/permex_dc_motor_env/cont_cc_permex_dc_env.py (95%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_dcm/permex_dc_motor_env/cont_sc_permex_dc_env.py (96%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_dcm/permex_dc_motor_env/cont_tc_permex_dc_env.py (95%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_dcm/permex_dc_motor_env/finite_cc_permex_dc_env.py (95%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_dcm/permex_dc_motor_env/finite_sc_permex_dc_env.py (96%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_dcm/permex_dc_motor_env/finite_tc_permex_dc_env.py (95%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_dcm/series_dc_motor_env/__init__.py (100%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_dcm/series_dc_motor_env/cont_cc_series_dc_env.py (96%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_dcm/series_dc_motor_env/cont_sc_series_dc_env.py (98%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_dcm/series_dc_motor_env/cont_tc_series_dc_env.py (96%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_dcm/series_dc_motor_env/finite_cc_series_dc_env.py (96%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_dcm/series_dc_motor_env/finite_sc_series_dc_env.py (98%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_dcm/series_dc_motor_env/finite_tc_series_dc_env.py (96%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_dcm/shunt_dc_motor_env/__init__.py (100%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_dcm/shunt_dc_motor_env/cont_cc_shunt_dc_env.py (96%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_dcm/shunt_dc_motor_env/cont_sc_shunt_dc_env.py (97%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_dcm/shunt_dc_motor_env/cont_tc_shunt_dc_env.py (96%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_dcm/shunt_dc_motor_env/finite_cc_shunt_dc_env.py (96%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_dcm/shunt_dc_motor_env/finite_sc_shunt_dc_env.py (97%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_dcm/shunt_dc_motor_env/finite_tc_shunt_dc_env.py (96%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_eesm/__init__.py (100%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_eesm/cont_cc_eesm_env.py (95%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_eesm/cont_sc_eesm_env.py (97%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_eesm/cont_tc_eesm_env.py (95%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_eesm/finite_cc_eesm_env.py (94%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_eesm/finite_sc_eesm_env.py (96%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_eesm/finite_tc_eesm_env.py (95%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_im/__init__.py (100%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_im/doubly_fed_induction_motor_envs/__init__.py (100%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_im/doubly_fed_induction_motor_envs/cont_cc_dfim_env.py (95%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_im/doubly_fed_induction_motor_envs/cont_sc_dfim_env.py (97%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_im/doubly_fed_induction_motor_envs/cont_tc_dfim_env.py (95%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_im/doubly_fed_induction_motor_envs/finite_cc_dfim_env.py (95%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_im/doubly_fed_induction_motor_envs/finite_sc_dfim_env.py (97%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_im/doubly_fed_induction_motor_envs/finite_tc_dfim_env.py (95%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_im/squirrel_cage_induction_motor_envs/__init__.py (100%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_im/squirrel_cage_induction_motor_envs/cont_cc_scim_env.py (93%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_im/squirrel_cage_induction_motor_envs/cont_sc_scim_env.py (95%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_im/squirrel_cage_induction_motor_envs/cont_tc_scim_env.py (93%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_im/squirrel_cage_induction_motor_envs/finite_cc_scim_env.py (95%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_im/squirrel_cage_induction_motor_envs/finite_sc_scim_env.py (96%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_im/squirrel_cage_induction_motor_envs/finite_tc_scim_env.py (95%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_pmsm/__init__.py (100%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_pmsm/cont_cc_pmsm_env.py (93%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_pmsm/cont_sc_pmsm_env.py (95%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_pmsm/cont_tc_pmsm_env.py (93%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_pmsm/finite_cc_pmsm_env.py (95%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_pmsm/finite_sc_pmsm_env.py (96%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_pmsm/finite_tc_pmsm_env.py (95%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_srm/srm_continuous_control_env.py (100%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_srm/srm_finite_control_env.py (100%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_synrm/__init__.py (100%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_synrm/cont_cc_synrm_env.py (93%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_synrm/cont_sc_synrm_env.py (95%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_synrm/cont_tc_synrm_env.py (93%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_synrm/finite_cc_synrm_env.py (95%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_synrm/finite_sc_synrm_env.py (96%) rename {gym_electric_motor => src/gym_electric_motor}/envs/gym_synrm/finite_tc_synrm_env.py (95%) rename {gym_electric_motor => src/gym_electric_motor}/envs/motors.py (100%) rename {gym_electric_motor => src/gym_electric_motor}/physical_system_wrappers/__init__.py (100%) rename {gym_electric_motor => src/gym_electric_motor}/physical_system_wrappers/cos_sin_processor.py (77%) rename {gym_electric_motor => src/gym_electric_motor}/physical_system_wrappers/current_sum_processor.py (87%) rename {gym_electric_motor => src/gym_electric_motor}/physical_system_wrappers/dead_time_processor.py (96%) rename {gym_electric_motor => src/gym_electric_motor}/physical_system_wrappers/dq_to_abc_action_processor.py (90%) rename {gym_electric_motor => src/gym_electric_motor}/physical_system_wrappers/flux_observer.py (80%) rename {gym_electric_motor => src/gym_electric_motor}/physical_system_wrappers/physical_system_wrapper.py (97%) rename {gym_electric_motor => src/gym_electric_motor}/physical_system_wrappers/state_noise_processor.py (91%) rename {gym_electric_motor => src/gym_electric_motor}/physical_systems/__init__.py (92%) rename {gym_electric_motor => src/gym_electric_motor}/physical_systems/converters.py (89%) rename {gym_electric_motor => src/gym_electric_motor}/physical_systems/electric_motors/__init__.py (100%) rename {gym_electric_motor => src/gym_electric_motor}/physical_systems/electric_motors/dc_externally_excited_motor.py (91%) rename {gym_electric_motor => src/gym_electric_motor}/physical_systems/electric_motors/dc_motor.py (81%) rename {gym_electric_motor => src/gym_electric_motor}/physical_systems/electric_motors/dc_permanently_excited_motor.py (97%) rename {gym_electric_motor => src/gym_electric_motor}/physical_systems/electric_motors/dc_series_motor.py (87%) rename {gym_electric_motor => src/gym_electric_motor}/physical_systems/electric_motors/dc_shunt_motor.py (93%) rename {gym_electric_motor => src/gym_electric_motor}/physical_systems/electric_motors/doubly_fed_induction_motor.py (91%) rename {gym_electric_motor => src/gym_electric_motor}/physical_systems/electric_motors/electric_motor.py (86%) rename {gym_electric_motor => src/gym_electric_motor}/physical_systems/electric_motors/externally_excited_synchronous_motor.py (89%) rename {gym_electric_motor => src/gym_electric_motor}/physical_systems/electric_motors/induction_motor.py (96%) rename {gym_electric_motor => src/gym_electric_motor}/physical_systems/electric_motors/permanent_magnet_synchronous_motor.py (89%) rename {gym_electric_motor => src/gym_electric_motor}/physical_systems/electric_motors/squirrel_cage_induction_motor.py (89%) rename {gym_electric_motor => src/gym_electric_motor}/physical_systems/electric_motors/synchronous_motor.py (94%) rename {gym_electric_motor => src/gym_electric_motor}/physical_systems/electric_motors/synchronous_reluctance_motor.py (92%) rename {gym_electric_motor => src/gym_electric_motor}/physical_systems/electric_motors/three_phase_motor.py (98%) rename {gym_electric_motor => src/gym_electric_motor}/physical_systems/mechanical_loads/__init__.py (100%) rename {gym_electric_motor => src/gym_electric_motor}/physical_systems/mechanical_loads/constant_speed_load.py (100%) rename {gym_electric_motor => src/gym_electric_motor}/physical_systems/mechanical_loads/external_speed_load.py (95%) rename {gym_electric_motor => src/gym_electric_motor}/physical_systems/mechanical_loads/mechanical_load.py (87%) rename {gym_electric_motor => src/gym_electric_motor}/physical_systems/mechanical_loads/ornstein_uhlenbeck_load.py (89%) rename {gym_electric_motor => src/gym_electric_motor}/physical_systems/mechanical_loads/polynomial_static_load.py (86%) rename {gym_electric_motor => src/gym_electric_motor}/physical_systems/physical_systems.py (85%) rename {gym_electric_motor => src/gym_electric_motor}/physical_systems/solvers.py (92%) rename {gym_electric_motor => src/gym_electric_motor}/physical_systems/voltage_supplies.py (86%) rename {gym_electric_motor => src/gym_electric_motor}/random_component.py (100%) rename {gym_electric_motor => src/gym_electric_motor}/reference_generators/__init__.py (86%) rename {gym_electric_motor => src/gym_electric_motor}/reference_generators/const_reference_generator.py (81%) rename {gym_electric_motor => src/gym_electric_motor}/reference_generators/laplace_process_reference_generator.py (91%) rename {gym_electric_motor => src/gym_electric_motor}/reference_generators/multiple_reference_generator.py (74%) rename {gym_electric_motor => src/gym_electric_motor}/reference_generators/sawtooth_reference_generator.py (81%) rename {gym_electric_motor => src/gym_electric_motor}/reference_generators/sinusoidal_reference_generator.py (82%) rename {gym_electric_motor => src/gym_electric_motor}/reference_generators/step_reference_generator.py (84%) rename {gym_electric_motor => src/gym_electric_motor}/reference_generators/subepisoded_reference_generator.py (86%) rename {gym_electric_motor => src/gym_electric_motor}/reference_generators/switched_reference_generator.py (92%) rename {gym_electric_motor => src/gym_electric_motor}/reference_generators/triangle_reference_generator.py (83%) rename {gym_electric_motor => src/gym_electric_motor}/reference_generators/wiener_process_reference_generator.py (92%) rename {gym_electric_motor => src/gym_electric_motor}/reference_generators/zero_reference_generator.py (83%) rename {gym_electric_motor => src/gym_electric_motor}/reward_functions/__init__.py (100%) rename {gym_electric_motor => src/gym_electric_motor}/reward_functions/weighted_sum_of_errors.py (91%) rename {gym_electric_motor => src/gym_electric_motor}/utils.py (94%) rename {gym_electric_motor => src/gym_electric_motor}/visualization/__init__.py (100%) rename {gym_electric_motor => src/gym_electric_motor}/visualization/console_printer.py (100%) rename {gym_electric_motor => src/gym_electric_motor}/visualization/motor_dashboard.py (99%) rename {gym_electric_motor => src/gym_electric_motor}/visualization/motor_dashboard_plots/__init__.py (100%) rename {gym_electric_motor => src/gym_electric_motor}/visualization/motor_dashboard_plots/action_plot.py (100%) rename {gym_electric_motor => src/gym_electric_motor}/visualization/motor_dashboard_plots/base_plots.py (98%) rename {gym_electric_motor => src/gym_electric_motor}/visualization/motor_dashboard_plots/cumulative_constraint_violation_plot.py (100%) rename {gym_electric_motor => src/gym_electric_motor}/visualization/motor_dashboard_plots/episode_length_plot.py (100%) rename {gym_electric_motor => src/gym_electric_motor}/visualization/motor_dashboard_plots/mean_episode_reward_plot.py (83%) rename {gym_electric_motor => src/gym_electric_motor}/visualization/motor_dashboard_plots/reward_plot.py (91%) rename {gym_electric_motor => src/gym_electric_motor}/visualization/motor_dashboard_plots/state_plot.py (85%) rename {gym_electric_motor => src/gym_electric_motor}/visualization/render_modes.py (100%) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 00000000..1cffdecc --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,5 @@ +# Linter and Formater +Ruff: https://docs.astral.sh/ruff/installation/ + +# Install editable local package +pip install -e . \ No newline at end of file diff --git a/gem_controllers/block_diagrams/stage_blocks/eesm_cc.py b/gem_controllers/block_diagrams/stage_blocks/eesm_cc.py deleted file mode 100644 index 4ef5c11c..00000000 --- a/gem_controllers/block_diagrams/stage_blocks/eesm_cc.py +++ /dev/null @@ -1,192 +0,0 @@ -from control_block_diagram.components import Point, Box, Connection, Circle -from control_block_diagram.predefined_components import DqToAbcTransformation, AbcToAlphaBetaTransformation,\ - AlphaBetaToDqTransformation, Add, PIController, Multiply, Limit - - -def eesm_cc(emf_feedforward): - """ - Args: - emf_feedforward: Boolean whether emf feedforward stage is included - - Returns: - Function to build the EESM current control block - """ - - def cc_eesm(start, control_task): - """ - Function to build the EESM current control block - Args: - start: Starting point of the block - control_task: Control task of the controller - - Returns: - endpoint, inputs, outputs, connection to other lines, connections - """ - - # space to the previous block - space = 1 if control_task == 'CC' else 1.5 - - # Add blocks for the i_sd and i_sq references and states - add_i_e = Add(start.add(space - 0.5, 1)) - add_i_sd = Add(start.add_x(space + 0.5)) - add_i_sq = Add(start.add(space, -1)) - - if control_task == 'CC': - # Connections at the inputs - Connection.connect(add_i_sd.input_left[0].sub_x(space + 1), add_i_sd.input_left[0], - text=r'$i^{*}_{\mathrm{sd}}$', text_position='start', text_align='left') - Connection.connect(add_i_sq.input_left[0].sub_x(space + 0.5), add_i_sq.input_left[0], - text=r'$i^{*}_{\mathrm{sq}}$', text_position='start', text_align='left') - Connection.connect(add_i_e.input_left[0].sub_x(space), add_i_e.input_left[0], - text=r'$i^{*}_{\mathrm{e}}$', text_position='start', text_align='left') - - # PI Controllers for the d and q component - pi_i_sd = PIController(add_i_sd.position.add_x(1.2), size=(1, 0.8), input_number=1, output_number=1) - pi_i_sq = PIController(Point.merge(pi_i_sd.position, add_i_sq.position), size=(1, 0.8), input_number=1, - output_number=1) - pi_i_e = PIController(Point.merge(pi_i_sd.position, add_i_e.position), size=(1, 0.8), input_number=1, - output_number=1, text='Current\nController') - - # Connection between the add blocks and the PI Controllers - Connection.connect(add_i_sd.output_right, pi_i_sd.input_left) - Connection.connect(add_i_sq.output_right, pi_i_sq.input_left) - Connection.connect(add_i_e.output_right, pi_i_e.input_left) - - # Add blocks for the EMF Feedforward - add_u_sd = Add(pi_i_sd.position.add_x(1.6)) - add_u_sq = Add(pi_i_sq.position.add_x(1.2)) - add_u_e = Add(pi_i_e.position.add_x(2)) - - # Connections between the PI Controllers and the Add blocks - Connection.connect(pi_i_sd.output_right[0], add_u_sd.input_left[0], text=r'$\Delta u^{*}_{\mathrm{sd}}$', - distance_y=0.28) - Connection.connect(pi_i_sq.output_right[0], add_u_sq.input_left[0], text=r'$\Delta u^{*}_{\mathrm{sq}}$', - distance_y=0.4, move_text=(0.25, 0)) - - # Coordinate transformation from DQ to Abc coordinates - dq_to_abc = DqToAbcTransformation(Point.get_mid(add_u_sd.position, add_u_sq.position).add_x(2), - input_space=1) - - # Connections between the add blocks and the coordinate transformation - Connection.connect(add_u_sd.output_right[0], dq_to_abc.input_left[0], text=r'$u^{*}_{\mathrm{sd}}$', - distance_y=0.28, move_text=(0.18, 0)) - Connection.connect(add_u_sq.output_right[0], dq_to_abc.input_left[1], text=r'$u^{*}_{\mathrm{sq}}$', - distance_y=0.28, move_text=(0.38, 0)) - - # Limit of the input voltages - limit = Limit(dq_to_abc.position.add_x(1.8), size=(1, 1.2), inputs=dict(left=3, left_space=0.3), - outputs=dict(right=3, right_space=0.3)) - - limit_e = Limit(Point.merge(limit.position, pi_i_e.position), size=(1, 1), inputs=dict(left=1), - outputs=dict(right=1)) - - # Connection between the coordinate transformation and the limit block - Connection.connect(dq_to_abc.output_right, limit.input_left) - Connection.connect(pi_i_e.output_right, limit_e.input_left) - - # Pulse width modulation block - pwm = Box(limit.position.add_x(2.5), size=(1.5, 1.2), text='PWM', inputs=dict(left=3, left_space=0.3), - outputs=dict(right=3, right_space=0.3)) - - pwm_e = Box(limit_e.position.add_x(2.5), size=(1.5, 1), text='PWM', inputs=dict(left=1), - outputs=dict(right=1)) - - # Connection between the limit and the PWM block - Connection.connect(limit.output_right, pwm.input_left, - text=[ '', '', r'$u^*_{\mathrm{s a,b,c}}$'], distance_y=0.25, text_align='bottom') - Connection.connect(limit_e.output_right, pwm_e.input_left, text=[r'$u^*_{\mathrm{e}}$']) - - # Coordinate transformation from ABC to AlphaBeta coordinates - abc_to_alpha_beta = AbcToAlphaBetaTransformation(pwm.position.sub(1, 3.5), input='right', output='left') - - # Coordinate transformation from AlphaBeta to DQ coordinates - alpha_beta_to_dq = AlphaBetaToDqTransformation( - Point.merge(dq_to_abc.position, abc_to_alpha_beta.position), input='right', output='left') - - emf = Box(Point.merge(add_u_sd.position, Point.get_mid(dq_to_abc.position, alpha_beta_to_dq.position)), - size=(2, 1), inputs=dict(bottom=4), outputs=dict(top=3, top_space=0.4), text='EMF Feedforward') - - Connection.connect(emf.output_top[0], add_u_sq.input_bottom[0]) - Connection.connect(emf.output_top[1], add_u_sd.input_bottom[0]) - Connection.connect(emf.output_top[2], add_u_e.input_bottom[0]) - - # Connections between the coordinate transformation and the add blocks - con_3 = Connection.connect(alpha_beta_to_dq.output_left[0], add_i_sd.input_bottom[0], text=r'-', - text_position='end', - text_align='right', move_text=(-0.2, -0.2)) - con_4 = Connection.connect(alpha_beta_to_dq.output_left[1], add_i_sq.input_bottom[0], text=r'-', - text_position='end', - text_align='right', move_text=(-0.2, -0.2)) - - # Connections between the previous conncetions and the inductances blocks - Connection.connect_to_line(con_3, emf.input_bottom[3]) - Connection.connect_to_line(con_4, emf.input_bottom[2]) - - # Derivation of the angle - box_d_dt = Box(Point.merge(alpha_beta_to_dq.position, start.sub_y(7.42020066645696)).sub_x(1.5), size=(1, 0.8), - text=r'$\mathrm{d} / \mathrm{d}t$', inputs=dict(right=1), outputs=dict(left=1)) - - # Conncetion between the derivation and multiplication block - con_omega = Connection.connect(box_d_dt.output_left[0], emf.input_bottom[0], space_y=1, - text=r'$\omega_{\mathrm{el}}$', move_text=(0, 1.7), text_align='left', - distance_x=0.3) - - if control_task == 'SC': - # Connector at the previous connection - Circle(con_omega.points[1], radius=0.05, fill='black') - - # Conncetion between the coordinate transformations - Connection.connect(abc_to_alpha_beta.output, alpha_beta_to_dq.input_right, - text=[r'$i_{\mathrm{s} \upalpha}$', r'$i_{\mathrm{s} \upbeta}$']) - - # Add block for the advanced angle - add = Add(Point.get_mid(dq_to_abc.position, alpha_beta_to_dq.position), inputs=dict(bottom=1, right=1), - outputs=dict(top=1)) - - # Connections of the add block - Connection.connect(alpha_beta_to_dq.output_top, add.input_bottom, text=r'$\varepsilon_{\mathrm{el}}$', - text_align='right', move_text=(0, -0.1)) - Connection.connect(add.output_top, dq_to_abc.input_bottom) - - # Calculate the advanced angle - box_t_a = Box(add.position.add_x(1.5), size=(1, 0.8), text=r'$1.5 T_{\mathrm{s}}$', inputs=dict(right=1), - outputs=dict(left=1)) - - # Connections of the advanced angle block - Connection.connect(box_t_a.output, add.input_right, text=r'$\Delta \varepsilon$') - Connection.connect(box_t_a.input[0].add_x(0.5), box_t_a.input[0], text=r'$\omega_{\mathrm{el}}$', - text_position='start', text_align='right', distance_x=0.3) - - start = pwm.position # starting point of the next block - - # Inputs of the stage - inputs = dict(i_d_ref=[add_i_sd.input_left[0], dict(text=r'$i^{*}_{\mathrm{sd}}$', distance_y=0.25, - move_text=(0.3, 0), text_position='start', - text_aglin='top')], - i_q_ref=[add_i_sq.input_left[0], dict(text=r'$i^{*}_{\mathrm{sq}}$', distance_y=0.25, - move_text=(0.3, 0), text_position='start', - text_aglin='top')], - i_e_ref=[add_i_e.input_left[0], dict(text=r'$i^{*}_{\mathrm{e}}$', distance_y=0.25, - move_text=(0.3, 0), text_position='start', - text_aglin='top')], - epsilon=[box_d_dt.input_right[0], dict()], - i_e=[add_i_e.input_bottom[0], dict(text=r'-', text_position='end', text_align='right', - move_text=(-0.2, -0.2))]) - - # Outputs of the stage - outputs = dict(S=pwm.output_right, omega=con_omega.points[1], S_e=pwm_e.output_right[0]) - - # Connections to other lines - connect_to_line = dict(epsilon=[alpha_beta_to_dq.input_bottom[0], dict(text=r'$\varepsilon_{\mathrm{el}}$', - text_position='middle', - text_align='right')], - i=[abc_to_alpha_beta.input_right, dict(radius=0.1, fill=False, - text=[r'$\mathbf{i}_{\mathrm{s a,b,c}}$', '', - ''])], - i_e=[emf.input_bottom[1], dict(radius=0.05, fill='black')]) - connections = dict() # Connections - - return start, inputs, outputs, connect_to_line, connections - - return cc_eesm - diff --git a/gem_controllers/block_diagrams/stage_blocks/pmsm_cc.py b/gem_controllers/block_diagrams/stage_blocks/pmsm_cc.py deleted file mode 100644 index a6ea8da9..00000000 --- a/gem_controllers/block_diagrams/stage_blocks/pmsm_cc.py +++ /dev/null @@ -1,187 +0,0 @@ -from control_block_diagram.components import Point, Box, Connection, Circle -from control_block_diagram.predefined_components import DqToAbcTransformation, AbcToAlphaBetaTransformation,\ - AlphaBetaToDqTransformation, Add, PIController, Multiply, Limit - - -def pmsm_cc(emf_feedforward): - """ - Args: - emf_feedforward: Boolean whether emf feedforward stage is included - - Returns: - Function to build the PMSM current control block - """ - - def cc_pmsm(start, control_task): - """ - Function to build the PMSM current control block - Args: - start: Starting point of the block - control_task: Control task of the controller - - Returns: - endpoint, inputs, outputs, connection to other lines, connections - """ - - # space to the previous block - space = 1 if control_task == 'CC' else 1.5 - - # Add blocks for the i_sd and i_sq references and states - add_i_sd = Add(start.add_x(space)) - add_i_sq = Add(add_i_sd.position.sub(0.5, 1)) - - if control_task == 'CC': - # Connections at the inputs - Connection.connect(add_i_sd.input_left[0].sub_x(space), add_i_sd.input_left[0], - text=r'$i^{*}_{\mathrm{sd}}$', text_position='start', text_align='left') - Connection.connect(add_i_sq.input_left[0].sub_x(space - 0.5), add_i_sq.input_left[0], - text=r'$i^{*}_{\mathrm{sq}}$', text_position='start', text_align='left') - - # PI Controllers for the d and q component - pi_i_sd = PIController(add_i_sd.position.add_x(1.2), size=(1, 0.8), input_number=1, output_number=1, - text='Current\nController') - pi_i_sq = PIController(Point.merge(pi_i_sd.position, add_i_sq.position), size=(1, 0.8), input_number=1, - output_number=1) - - # Connection between the add blocks and the PI Controllers - Connection.connect(add_i_sd.output_right, pi_i_sd.input_left) - Connection.connect(add_i_sq.output_right, pi_i_sq.input_left) - - # Add blocks for the EMF Feedforward - add_u_sd = Add(pi_i_sd.position.add_x(2)) - add_u_sq = Add(pi_i_sq.position.add_x(1.2)) - - # Connections between the PI Controllers and the Add blocks - Connection.connect(pi_i_sd.output_right[0], add_u_sd.input_left[0], text=r'$\Delta u^{*}_{\mathrm{sd}}$', - distance_y=0.28) - Connection.connect(pi_i_sq.output_right[0], add_u_sq.input_left[0], text=r'$\Delta u^{*}_{\mathrm{sq}}$', - distance_y=0.4, move_text=(0.25, 0)) - - # Coordinate transformation from DQ to Abc coordinates - dq_to_abc = DqToAbcTransformation(Point.get_mid(add_u_sd.position, add_u_sq.position).add_x(2), - input_space=1) - - # Connections between the add blocks and the coordinate transformation - Connection.connect(add_u_sd.output_right[0], dq_to_abc.input_left[0], text=r'$u^{*}_{\mathrm{sd}}$', - distance_y=0.28) - Connection.connect(add_u_sq.output_right[0], dq_to_abc.input_left[1], text=r'$u^{*}_{\mathrm{sq}}$', - distance_y=0.28, move_text=(0.4, 0)) - - # Limit of the input voltages - limit = Limit(dq_to_abc.position.add_x(1.8), size=(1, 1.2), inputs=dict(left=3, left_space=0.3), - outputs=dict(right=3, right_space=0.3)) - - # Connection between the coordinate transformation and the limit block - Connection.connect(dq_to_abc.output_right, limit.input_left) - - # Pulse width modulation block - pwm = Box(limit.position.add_x(2.5), size=(1.5, 1.2), text='PWM', inputs=dict(left=3, left_space=0.3), - outputs=dict(right=3, right_space=0.3)) - - # Connection between the limit and the PWM block - Connection.connect(limit.output_right, pwm.input_left, - text=[r'$u^*_{\mathrm{s a,b,c}}$', '', ''], distance_y=0.25) - - # Coordinate transformation from ABC to AlphaBeta coordinates - abc_to_alpha_beta = AbcToAlphaBetaTransformation(pwm.position.sub(1, 3.5), input='right', output='left') - - # Coordinate transformation from AlphaBeta to DQ coordinates - alpha_beta_to_dq = AlphaBetaToDqTransformation( - Point.merge(dq_to_abc.position, abc_to_alpha_beta.position), input='right', output='left') - - # Distance of the blocks in the EMF Feedforward path - distance = (add_u_sq.position.y - alpha_beta_to_dq.output_left[0].y) / 4 - - # Multiplications of the EMF Feedforward - multiply_u_sq = Multiply(add_u_sq.position.sub_y(distance), outputs=dict(top=1)) - multiply_u_sd = Multiply(Point.merge(add_u_sd.position, multiply_u_sq.position), outputs=dict(top=1)) - - # Connections between the multiplication and the add blocks - Connection.connect(multiply_u_sq.output_top, add_u_sq.input_bottom) - Connection.connect(multiply_u_sd.output_top, add_u_sd.input_bottom, text=r'-', text_position='end', - text_align='right', move_text=(-0.2, -0.2)) - - # Add block for the permanent flux psi_p - add_psi_p = Add(multiply_u_sq.position.sub_y(distance), outputs=dict(top=1)) - - # Connections of the add block - Connection.connect(add_psi_p.output_top, multiply_u_sq.input_bottom) - Connection.connect(add_psi_p.input_left[0].sub_x(0.3), add_psi_p.input_left[0], text=r'$\Psi_{\mathrm{p}}$', - text_position='start', text_align='left', distance_x=0.25) - - # Multiplication with the inductances - box_ls_1 = Box(add_psi_p.position.sub_y(distance), size=(0.6, 0.6), inputs=dict(bottom=1), outputs=dict(top=1), - text=r'$L_{\mathrm{d}}$') - box_ls_2 = Box(Point.merge(multiply_u_sd.position, box_ls_1.position), size=(0.6, 0.6), inputs=dict(bottom=1), - outputs=dict(top=1), text=r'$L_{\mathrm{q}}$') - - # Connections from the multiplications - Connection.connect(box_ls_1.output_top, add_psi_p.input_bottom) - Connection.connect(multiply_u_sd.input_left[0].sub_x(0.3), multiply_u_sd.input_left[0]) - Connection.connect(box_ls_2.output_top, multiply_u_sd.input_bottom) - - # Connections between the coordinate transformation and the add blocks - con_3 = Connection.connect(alpha_beta_to_dq.output_left[0], add_i_sd.input_bottom[0], text=r'-', - text_position='end', - text_align='right', move_text=(-0.2, -0.2)) - con_4 = Connection.connect(alpha_beta_to_dq.output_left[1], add_i_sq.input_bottom[0], text=r'-', - text_position='end', - text_align='right', move_text=(-0.2, -0.2)) - - # Connections between the previous conncetions and the inductances blocks - Connection.connect_to_line(con_3, box_ls_1.input_bottom[0]) - Connection.connect_to_line(con_4, box_ls_2.input_bottom[0]) - - # Derivation of the angle - box_d_dt = Box(Point.merge(alpha_beta_to_dq.position, start.sub_y(6.57020066645696)).sub_x(2), size=(1, 0.8), - text=r'$\mathrm{d} / \mathrm{d}t$', inputs=dict(right=1), outputs=dict(left=1)) - - # Conncetion between the derivation and multiplication block - con_omega = Connection.connect(box_d_dt.output_left, multiply_u_sq.input_left, space_y=1, - text=r'$\omega_{\mathrm{el}}$', move_text=(0, 2), text_align='left', - distance_x=0.3) - - if control_task == 'SC': - # Connector at the previous connection - Circle(con_omega[0].points[1], radius=0.05, fill='black') - - # Conncetion between the coordinate transformations - Connection.connect(abc_to_alpha_beta.output, alpha_beta_to_dq.input_right, - text=[r'$i_{\mathrm{s} \upalpha}$', r'$i_{\mathrm{s} \upbeta}$']) - - # Add block for the advanced angle - add = Add(Point.get_mid(dq_to_abc.position, alpha_beta_to_dq.position), inputs=dict(bottom=1, right=1), - outputs=dict(top=1)) - - # Connections of the add block - Connection.connect(alpha_beta_to_dq.output_top, add.input_bottom, text=r'$\varepsilon_{\mathrm{el}}$', - text_align='right', move_text=(0, -0.1)) - Connection.connect(add.output_top, dq_to_abc.input_bottom) - - # Calculate the advanced angle - box_t_a = Box(add.position.add_x(1.5), size=(1, 0.8), text=r'$1.5 T_{\mathrm{s}}$', inputs=dict(right=1), - outputs=dict(left=1)) - - # Connections of the advanced angle block - Connection.connect(box_t_a.output, add.input_right, text=r'$\Delta \varepsilon$') - Connection.connect(box_t_a.input[0].add_x(0.5), box_t_a.input[0], text=r'$\omega_{\mathrm{el}}$', - text_position='start', text_align='right', distance_x=0.3) - - start = pwm.position # starting point of the next block - # Inputs of the stage - inputs = dict(i_d_ref=[add_i_sd.input_left[0], dict(text=r'$i^{*}_{\mathrm{sd}}$', distance_y=0.25)], - i_q_ref=[add_i_sq.input_left[0], dict(text=r'$i^{*}_{\mathrm{sq}}$', distance_y=0.25)], - epsilon=[box_d_dt.input_right[0], dict()]) - outputs = dict(S=pwm.output_right, omega=con_omega[0].points[1]) # Outputs of the stage - # Connections to other lines - connect_to_line = dict(epsilon=[alpha_beta_to_dq.input_bottom[0], dict(text=r'$\varepsilon_{\mathrm{el}}$', - text_position='middle', - text_align='right')], - i=[abc_to_alpha_beta.input_right, dict(radius=0.1, fill=False, - text=[r'$\mathbf{i}_{\mathrm{s a,b,c}}$', '', - ''])],) - connections = dict() # Connections - - return start, inputs, outputs, connect_to_line, connections - - return cc_pmsm diff --git a/gem_controllers/block_diagrams/stage_blocks/scim_cc.py b/gem_controllers/block_diagrams/stage_blocks/scim_cc.py deleted file mode 100644 index fae6243e..00000000 --- a/gem_controllers/block_diagrams/stage_blocks/scim_cc.py +++ /dev/null @@ -1,164 +0,0 @@ -from control_block_diagram.components import Box, Connection, Text, Point, Circle -from control_block_diagram.predefined_components import Add, PIController, Limit, DqToAbcTransformation,\ - AbcToAlphaBetaTransformation, AlphaBetaToDqTransformation - - -def scim_cc(emf_feedforward): - """ - Args: - emf_feedforward: Boolean whether emf feedforward stage is included - - Returns: - Function to build the SCIM current control block - """ - - def cc_scim(start, control_task): - """ - Function to build the SCIM current control block - Args: - start: Starting Point of the Block - control_task: Control task of the controller - - Returns: - endpoint, inputs, outputs, connection to other lines, connections - """ - - # space to the previous block - space = 1 if control_task == 'CC' else 1.5 - - # Add blocks for the i_sd and i_sq references and states - add_i_sd = Add(start.add_x(space)) - add_i_sq = Add(add_i_sd.position.sub(0.5, 1)) - - if control_task == 'CC': - # Connections at the inputs - Connection.connect(add_i_sd.input_left[0].sub_x(space), add_i_sd.input_left[0], - text=r'$i^{*}_{\mathrm{sd}}$', text_position='start', text_align='left') - Connection.connect(add_i_sq.input_left[0].sub_x(space - 0.5), add_i_sq.input_left[0], - text=r'$i^{*}_{\mathrm{sq}}$', text_position='start', text_align='left') - - # PI Controllers for the d and q component - pi_i_sd = PIController(add_i_sd.position.add_x(1.2), size=(1, 0.8), input_number=1, output_number=1, - text='Current\nController') - pi_i_sq = PIController(Point.merge(pi_i_sd.position, add_i_sq.position), size=(1, 0.8), input_number=1, - output_number=1) - - # Connection between the add blocks and the PI Controllers - Connection.connect(add_i_sd.output_right, pi_i_sd.input_left) - Connection.connect(add_i_sq.output_right, pi_i_sq.input_left) - - # Add blocks for the EMF Feedforward - add_u_sd = Add(pi_i_sd.position.add_x(2)) - add_u_sq = Add(pi_i_sq.position.add_x(1.2)) - - # Connections between the PI Controllers and the Add blocks - Connection.connect(pi_i_sd.output_right[0], add_u_sd.input_left[0], text=r'$\Delta u^{*}_{\mathrm{sd}}$', - distance_y=0.28) - Connection.connect(pi_i_sq.output_right[0], add_u_sq.input_left[0], text=r'$\Delta u^{*}_{\mathrm{sq}}$', - distance_y=0.4, move_text=(0.25, 0)) - - # Coordinate transformation from DQ to Abc coordinates - dq_to_abc = DqToAbcTransformation(Point.get_mid(add_u_sd.position, add_u_sq.position).add_x(2), - input_space=1) - - # Connections between the add blocks and the coordinate transformation - Connection.connect(add_u_sd.output_right[0], dq_to_abc.input_left[0], text=r'$u^{*}_{\mathrm{sd}}$', - distance_y=0.28) - Connection.connect(add_u_sq.output_right[0], dq_to_abc.input_left[1], text=r'$u^{*}_{\mathrm{sq}}$', - distance_y=0.28, move_text=(0.4, 0)) - - # Limit of the input voltages - limit = Limit(dq_to_abc.position.add_x(2.2), size=(1.5, 1.5), inputs=dict(left=3, left_space=0.3), - outputs=dict(right=3, right_space=0.3)) - - # Connection between the coordinate transformation and the limit block - Connection.connect(dq_to_abc.output_right, limit.input_left) - - # Pulse width modulation block - pwm = Box(limit.position.add_x(2.8), size=(1.5, 1.2), text='PWM', inputs=dict(left=3, left_space=0.3), - outputs=dict(right=3, right_space=0.3)) - - # Connection between the limit and the PWM block - Connection.connect(limit.output_right, pwm.input_left, - text=[r'$u^*_{\mathrm{s a,b,c}}$', '', ''], distance_y=0.25) - - # Coordinate transformation from ABC to AlphaBeta coordinates - abc_to_alpha_beta = AbcToAlphaBetaTransformation(pwm.position.sub(1, 3), input='right', output='left') - - # Flux observer - observer = Box(abc_to_alpha_beta.position.sub(0.5, 2.2), size=(2, 1), text='Flux Observer', - inputs=dict(right=1, top=2, top_space=1.2), outputs=dict(left=2)) - - # Connection of the flux observer - con_omega = Connection.connect(observer.input_right[0].add_x(1), observer.input_right[0]) - Connection.connect(observer.input_top[0].add_y(0.4), observer.input_top[0], - text=r'$\mathbf{i}_{\mathrm{s a,b,c}}$', text_position='start') - Connection.connect(observer.input_top[1].add_y(0.4), observer.input_top[1], - text=r'$\mathbf{u}_{\mathrm{s a,b,c}}$', text_position='start', move_text=(0, -0.05)) - - # Coordinate transformation from AlphaBeta to DQ coordinates - alpha_beta_to_dq = AlphaBetaToDqTransformation( - Point.merge(dq_to_abc.position, abc_to_alpha_beta.position), input='right', output='left') - - # Connections between the coordinate transformation and the add blocks - con_i_sd = Connection.connect(alpha_beta_to_dq.output_left[0], add_i_sd.input_bottom[0], text='-', - text_position='end', text_align='right', move_text=(-0.2, -0.2)) - con_i_sq = Connection.connect(alpha_beta_to_dq.output_left[1], add_i_sq.input_bottom[0], text='-', - text_position='end', text_align='right', move_text=(-0.2, -0.2)) - - # Texts at the previous connections - Text(position=con_i_sd.start.add(-0.25, 0.25), text=r'$i_{\mathrm{sd}}$') - Text(position=con_i_sq.start.add(-0.25, 0.25), text=r'$i_{\mathrm{sq}}$') - - # Connection between the flux observer and the coordinate transformation - Connection.connect(observer.output_left[0], alpha_beta_to_dq.input_bottom[0]) - - # Texts at the outputs of the flux observer - Text(position=observer.output_left[0].add(-0.4, 0.3), text=r'$\angle \hat{\underline{\Psi}}_{\mathrm{r}}$') - Text(position=observer.output_left[1].add(-0.4, -0.3), text=r'$\hat{\Psi}_{\mathrm{r}}$') - - # Connections between the coordinate transformations - Connection.connect(abc_to_alpha_beta.output, alpha_beta_to_dq.input_right, - text=[r'$i_{\mathrm{s} \upalpha}$', r'$i_{\mathrm{s} \upbeta}$']) - Connection.connect(alpha_beta_to_dq.output_top, dq_to_abc.input_bottom, - text=r'$\angle \hat{\underline{\Psi}}_r$', text_align='right') - - # Feedforward block - feedforward = Box(Point.get_mid(add_u_sd.position, add_u_sq.position).sub_y(1.6), size=(2, 0.8), - text='feedforward', inputs=dict(bottom=4, bottom_space=0.3), - outputs=dict(top=2, top_space=0.8)) - - # Connections of the feedforward block - con_omega_2 = Connection.connect(feedforward.input_bottom[0].add(7, -4.0702), feedforward.input_bottom[0], - text=r'$\omega_{\mathrm{me}}$', text_position='start', move_text=(-1, -0.1)) - Connection.connect_to_line(con_omega_2, con_omega.start, arrow=False) - Connection.connect(feedforward.output_top[0], add_u_sq.input_bottom[0]) - Connection.connect(feedforward.output_top[1], add_u_sd.input_bottom[0]) - con_psi_r = Connection.connect(observer.output_left[1], feedforward.input_bottom[1]) - Connection.connect_to_line(con_i_sd, feedforward.input_bottom[2]) - Connection.connect_to_line(con_i_sq, feedforward.input_bottom[3]) - - if control_task in ['TC', 'SC']: - # Circle at the connection start - Circle(con_psi_r.points[1], radius=0.05, draw='black', fill='black') - if control_task == 'SC': - # Circle at the connection start - Circle(con_omega_2.points[1], radius=0.05, draw='black', fill='black') - - start = pwm.position # starting point of the next block - # Inputs of the stage - inputs = dict(i_d_ref=[add_i_sd.input_left[0], dict(text=r'$i^{*}_{\mathrm{sd}}$', distance_y=0.3, - text_position='end', move_text=(-0.85, 0))], - i_q_ref=[add_i_sq.input_left[0], dict(text=r'$i^{*}_{\mathrm{sq}}$', distance_y=0.3, - text_position='end', move_text=(-0.35, 0))], - omega=[con_omega_2.start, dict(arrow=False)]) - # Outputs of the stage - outputs = dict(S=pwm.output_right, psi_r=con_psi_r.points[1], omega_me=con_omega_2.points[1]) - # Connections to other lines - connect_to_line = dict(i=[abc_to_alpha_beta.input_right, dict(radius=0.1, fill=False, - text=[r'$\mathbf{i}_{\mathrm{s a,b,c}}$', '', - ''])]) - connections = dict() # Connections - - return start, inputs, outputs, connect_to_line, connections - return cc_scim diff --git a/gem_controllers/block_diagrams/stage_blocks/synrm_cc.py b/gem_controllers/block_diagrams/stage_blocks/synrm_cc.py deleted file mode 100644 index 5f2b8877..00000000 --- a/gem_controllers/block_diagrams/stage_blocks/synrm_cc.py +++ /dev/null @@ -1,179 +0,0 @@ -from control_block_diagram.components import Point, Box, Connection, Circle -from control_block_diagram.predefined_components import DqToAbcTransformation, AbcToAlphaBetaTransformation,\ - AlphaBetaToDqTransformation, Add, PIController, Multiply, Limit - - -def synrm_cc(emf_feedforward): - """ - Args: - emf_feedforward: Boolean whether emf feedforward stage is included - - Returns: - Function to build the SynRM current control block - """ - - def cc_synrm(start, control_task): - """ - Function to build the SynRM current control block - Args: - start: Starting point of the block - control_task: Control task of the controller - - Returns: - endpoint, inputs, outputs, connection to other lines, connections - """ - - # space to the previous block - space = 1 if control_task == 'CC' else 1.5 - - # Add blocks for the i_sd and i_sq references and states - add_i_sd = Add(start.add_x(space)) - add_i_sq = Add(add_i_sd.position.sub(0.5, 1)) - - if control_task == 'CC': - # Connections at the inputs - Connection.connect(add_i_sd.input_left[0].sub_x(space), add_i_sd.input_left[0], - text=r'$i^{*}_{\mathrm{sd}}$', text_position='start', text_align='left') - Connection.connect(add_i_sq.input_left[0].sub_x(space - 0.5), add_i_sq.input_left[0], - text=r'$i^{*}_{\mathrm{sq}}$', text_position='start', text_align='left') - - # PI Controllers for the d and q component - pi_i_sd = PIController(add_i_sd.position.add_x(1.2), size=(1, 0.8), input_number=1, output_number=1, - text='Current\nController') - pi_i_sq = PIController(Point.merge(pi_i_sd.position, add_i_sq.position), size=(1, 0.8), input_number=1, - output_number=1) - - # Connection between the add blocks and the PI Controllers - Connection.connect(add_i_sd.output_right, pi_i_sd.input_left) - Connection.connect(add_i_sq.output_right, pi_i_sq.input_left) - - # Add blocks for the EMF Feedforward - add_u_sd = Add(pi_i_sd.position.add_x(2)) - add_u_sq = Add(pi_i_sq.position.add_x(1.2)) - - # Connections between the PI Controllers and the Add blocks - Connection.connect(pi_i_sd.output_right[0], add_u_sd.input_left[0], text=r'$\Delta u^{*}_{\mathrm{sd}}$', - distance_y=0.28) - Connection.connect(pi_i_sq.output_right[0], add_u_sq.input_left[0], text=r'$\Delta u^{*}_{\mathrm{sq}}$', - distance_y=0.4, move_text=(0.25, 0)) - - # Coordinate transformation from DQ to Abc coordinates - dq_to_abc = DqToAbcTransformation(Point.get_mid(add_u_sd.position, add_u_sq.position).add_x(2), - input_space=1) - - # Connections between the add blocks and the coordinate transformation - Connection.connect(add_u_sd.output_right[0], dq_to_abc.input_left[0], text=r'$u^{*}_{\mathrm{sd}}$', - distance_y=0.28) - Connection.connect(add_u_sq.output_right[0], dq_to_abc.input_left[1], text=r'$u^{*}_{\mathrm{sq}}$', - distance_y=0.28, move_text=(0.4, 0)) - - # Limit of the input voltages - limit = Limit(dq_to_abc.position.add_x(1.8), size=(1, 1.2), inputs=dict(left=3, left_space=0.3), - outputs=dict(right=3, right_space=0.3)) - - # Connection between the coordinate transformation and the limit block - Connection.connect(dq_to_abc.output_right, limit.input_left) - - # Pulse width modulation block - pwm = Box(limit.position.add_x(2.5), size=(1.5, 1.2), text='PWM', inputs=dict(left=3, left_space=0.3), - outputs=dict(right=3, right_space=0.3)) - - # Connection between the limit and the PWM block - Connection.connect(limit.output_right, pwm.input_left, - text=[r'$u^*_{\mathrm{s a,b,c}}$', '', ''], distance_y=0.25) - - # Coordinate transformation from ABC to AlphaBeta coordinates - abc_to_alpha_beta = AbcToAlphaBetaTransformation(pwm.position.sub(1, 3.5), input='right', output='left') - - # Coordinate transformation from AlphaBeta to DQ coordinates - alpha_beta_to_dq = AlphaBetaToDqTransformation( - Point.merge(dq_to_abc.position, abc_to_alpha_beta.position), input='right', output='left') - - # Distance of the blocks in the EMF Feedforward path - distance = (add_u_sq.position.y - alpha_beta_to_dq.output_left[0].y) / 3 - - # Multiplications of the EMF Feedforward - multiply_u_sq = Multiply(add_u_sq.position.sub_y(distance), outputs=dict(top=1)) - multiply_u_sd = Multiply(Point.merge(add_u_sd.position, multiply_u_sq.position), outputs=dict(top=1)) - - # Connections between the multiplication and the add blocks - Connection.connect(multiply_u_sq.output_top, add_u_sq.input_bottom) - Connection.connect(multiply_u_sd.output_top, add_u_sd.input_bottom, text=r'-', text_position='end', - text_align='right', move_text=(-0.2, -0.2)) - - # Multiplication with the inductances - box_ls_1 = Box(multiply_u_sq.position.sub_y(distance), size=(0.6, 0.6), inputs=dict(bottom=1), outputs=dict(top=1), - text=r'$L_{\mathrm{d}}$') - box_ls_2 = Box(Point.merge(multiply_u_sd.position, box_ls_1.position), size=(0.6, 0.6), inputs=dict(bottom=1), - outputs=dict(top=1), text=r'$L_{\mathrm{q}}$') - - # Connections from the multiplications - Connection.connect(box_ls_1.output_top, multiply_u_sq.input_bottom) - Connection.connect(multiply_u_sd.input_left[0].sub_x(0.3), multiply_u_sd.input_left[0]) - Connection.connect(box_ls_2.output_top, multiply_u_sd.input_bottom) - - # Connections between the coordinate transformation and the add blocks - con_3 = Connection.connect(alpha_beta_to_dq.output_left[0], add_i_sd.input_bottom[0], text=r'-', - text_position='end', - text_align='right', move_text=(-0.2, -0.2)) - con_4 = Connection.connect(alpha_beta_to_dq.output_left[1], add_i_sq.input_bottom[0], text=r'-', - text_position='end', - text_align='right', move_text=(-0.2, -0.2)) - - # Connections between the previous conncetions and the inductances blocks - Connection.connect_to_line(con_3, box_ls_1.input_bottom[0]) - Connection.connect_to_line(con_4, box_ls_2.input_bottom[0]) - - # Derivation of the angle - box_d_dt = Box(Point.merge(alpha_beta_to_dq.position, start.sub_y(6.57020066645696)).sub_x(2), size=(1, 0.8), - text=r'$\mathrm{d} / \mathrm{d}t$', inputs=dict(right=1), outputs=dict(left=1)) - - # Conncetion between the derivation and multiplication block - con_omega = Connection.connect(box_d_dt.output_left, multiply_u_sq.input_left, space_y=1, - text=r'$\omega_{\mathrm{el}}$', move_text=(0, 2), text_align='left', - distance_x=0.3) - - if control_task == 'SC': - # Connector at the previous connection - Circle(con_omega[0].points[1], radius=0.05, fill='black') - - # Conncetion between the coordinate transformations - Connection.connect(abc_to_alpha_beta.output, alpha_beta_to_dq.input_right, - text=[r'$i_{\mathrm{s} \upalpha}$', r'$i_{\mathrm{s} \upbeta}$']) - - # Add block for the advanced angle - add = Add(Point.get_mid(dq_to_abc.position, alpha_beta_to_dq.position), inputs=dict(bottom=1, right=1), - outputs=dict(top=1)) - - # Connections of the add block - Connection.connect(alpha_beta_to_dq.output_top, add.input_bottom, text=r'$\varepsilon_{\mathrm{el}}$', - text_align='right', move_text=(0, -0.1)) - Connection.connect(add.output_top, dq_to_abc.input_bottom) - - # Calculate the advanced angle - box_t_a = Box(add.position.add_x(1.5), size=(1, 0.8), text=r'$1.5 T_{\mathrm{s}}$', inputs=dict(right=1), - outputs=dict(left=1)) - - # Connections of the advanced angle block - Connection.connect(box_t_a.output, add.input_right, text=r'$\Delta \varepsilon$') - Connection.connect(box_t_a.input[0].add_x(0.5), box_t_a.input[0], text=r'$\omega_{\mathrm{el}}$', - text_position='start', text_align='right', distance_x=0.3) - - start = pwm.position # starting point of the next block - # Inputs of the stage - inputs = dict(i_d_ref=[add_i_sd.input_left[0], dict(text=r'$i^{*}_{\mathrm{sd}}$', distance_y=0.25)], - i_q_ref=[add_i_sq.input_left[0], dict(text=r'$i^{*}_{\mathrm{sq}}$', distance_y=0.25)], - epsilon=[box_d_dt.input_right[0], dict()]) - outputs = dict(S=pwm.output_right, omega=con_omega[0].points[1]) # Outputs of the stage - # Connections to other lines - connect_to_line = dict(epsilon=[alpha_beta_to_dq.input_bottom[0], dict(text=r'$\varepsilon_{\mathrm{el}}$', - text_position='middle', - text_align='right')], - i=[abc_to_alpha_beta.input_right, dict(radius=0.1, fill=False, - text=[r'$\mathbf{i}_{\mathrm{s a,b,c}}$', '', - ''])]) - connections = dict() # Connections - - return start, inputs, outputs, connect_to_line, connections - - return cc_synrm diff --git a/gem_controllers/parameter_reader.py b/gem_controllers/parameter_reader.py deleted file mode 100644 index 40f33c6f..00000000 --- a/gem_controllers/parameter_reader.py +++ /dev/null @@ -1,358 +0,0 @@ -from gym_electric_motor.physical_systems import converters as cv - -import numpy as np - -dc_motors = ['SeriesDc', 'ShuntDc', 'PermExDc', 'ExtExDc'] -synchronous_motors = ['PMSM', 'SynRM', 'EESM'] -induction_motors = ['DFIM', 'SCIM'] -ac_motors = synchronous_motors + induction_motors - -control_tasks_ = ['CC', 'TC', 'SC'] -dc_actions = ['Finite', 'Cont'] -ac_actions = ['Finite', 'DqCont', 'AbcCont'] - - -psi_reader = { - 'SeriesDc': lambda env: np.array([0.0]), - 'ShuntDc': lambda env: np.array([0.0]), - 'PermExDc': lambda env: np.array([env.physical_system.electrical_motor.motor_parameter['psi_e']]), - 'ExtExDc': lambda env: np.array([0.0, 0.0]), - 'PMSM': lambda env: np.array([0.0, env.physical_system.electrical_motor.motor_parameter['psi_p']]), - 'SynRM': lambda env: np.array([0.0, 0.0]), - 'SCIM': lambda env: np.array([0.0, 0.0]), - 'EESM': lambda env: np.array([0.0, 0.0, 0.0]), -} - -p_reader = { - 'SeriesDc': lambda env: 1, - 'ShuntDc': lambda env: 1, - 'ExtExDc': lambda env: 0, - 'PermExDc': lambda env: 0, - 'PMSM': lambda env: env.physical_system.electrical_motor.motor_parameter['p'], - 'SynRM': lambda env: env.physical_system.electrical_motor.motor_parameter['p'], - 'SCIM': lambda env: env.physical_system.electrical_motor.motor_parameter['p'], - 'EESM': lambda env: env.physical_system.electrical_motor.motor_parameter['p'], -} - -l_reader = { - 'SeriesDc': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['l_a'] - + env.physical_system.electrical_motor.motor_parameter['l_e'] - ]), - 'ShuntDc': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['l_a'], - ]), - 'ExtExDc': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['l_a'], - env.physical_system.electrical_motor.motor_parameter['l_e'] - ]), - 'PermExDc': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['l_a'], - ]), - 'PMSM': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['l_d'], - env.physical_system.electrical_motor.motor_parameter['l_q'] - ]), - 'SynRM': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['l_d'], - env.physical_system.electrical_motor.motor_parameter['l_q'] - ]), - 'SCIM': lambda env: np.array([ - (env.physical_system.electrical_motor.motor_parameter['l_sigr'] + - env.physical_system.electrical_motor.motor_parameter['l_m']) / - env.physical_system.electrical_motor.motor_parameter['r_r'], - (env.physical_system.electrical_motor.motor_parameter['l_sigr'] + - env.physical_system.electrical_motor.motor_parameter['l_m']) / - env.physical_system.electrical_motor.motor_parameter['r_r'], - ]), - 'EESM': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['l_d'], - env.physical_system.electrical_motor.motor_parameter['l_q'], - env.physical_system.electrical_motor.motor_parameter['l_e'] - ]), -} - -l_emf_reader = { - 'SeriesDc': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['l_e_prime'] - ]), - 'ShuntDc': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['l_e_prime'], - ]), - 'ExtExDc': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['l_e_prime'], - 0.0 - ]), - 'PermExDc': lambda env: np.array([0.0]), - 'PMSM': lambda env: np.array([ - - env.physical_system.electrical_motor.motor_parameter['l_q'], - env.physical_system.electrical_motor.motor_parameter['l_d'], - ]), - 'SynRM': lambda env: np.array([ - - env.physical_system.electrical_motor.motor_parameter['l_q'], - env.physical_system.electrical_motor.motor_parameter['l_d'], - ]), - 'SCIM': lambda env: np.array([ - -(env.physical_system.electrical_motor.motor_parameter['l_sigs'] * - env.physical_system.electrical_motor.motor_parameter['l_sigr'] + - env.physical_system.electrical_motor.motor_parameter['l_sigs'] * - env.physical_system.electrical_motor.motor_parameter['l_m'] + - env.physical_system.electrical_motor.motor_parameter['l_sigr'] * - env.physical_system.electrical_motor.motor_parameter['l_m']) / - (env.physical_system.electrical_motor.motor_parameter['l_sigr'] + - env.physical_system.electrical_motor.motor_parameter['l_m']), - (env.physical_system.electrical_motor.motor_parameter['l_sigs'] * - env.physical_system.electrical_motor.motor_parameter['l_sigr'] + - env.physical_system.electrical_motor.motor_parameter['l_sigs'] * - env.physical_system.electrical_motor.motor_parameter['l_m'] + - env.physical_system.electrical_motor.motor_parameter['l_sigr'] * - env.physical_system.electrical_motor.motor_parameter['l_m']) / - (env.physical_system.electrical_motor.motor_parameter['l_sigr'] + - env.physical_system.electrical_motor.motor_parameter['l_m']) - ]), - 'EESM': lambda env: np.array([ - - env.physical_system.electrical_motor.motor_parameter['l_q'], - env.physical_system.electrical_motor.motor_parameter['l_d'], - env.physical_system.electrical_motor.motor_parameter['l_m'] * - env.physical_system.electrical_motor.motor_parameter['l_q'] / - env.physical_system.electrical_motor.motor_parameter['l_d'], - ]), -} - -tau_current_loop_reader = { - 'SeriesDc': lambda env: np.array([( - env.physical_system.electrical_motor.motor_parameter['l_e'] - + env.physical_system.electrical_motor.motor_parameter['l_a'] - ) / ( - env.physical_system.electrical_motor.motor_parameter['r_e'] - + env.physical_system.electrical_motor.motor_parameter['r_a'] - ) - ]), - 'ShuntDc': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['l_a'] - / env.physical_system.electrical_motor.motor_parameter['r_a'] - ]), - 'ExtExDc': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['l_a'] - / env.physical_system.electrical_motor.motor_parameter['r_a'], - env.physical_system.electrical_motor.motor_parameter['l_e'] - / env.physical_system.electrical_motor.motor_parameter['r_e'] - ]), - 'PermExDc': lambda env: np.array( - env.physical_system.electrical_motor.motor_parameter['l_a'] - / env.physical_system.electrical_motor.motor_parameter['r_a'] - ), - 'PMSM': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['l_q'] - / env.physical_system.electrical_motor.motor_parameter['r_s'], - env.physical_system.electrical_motor.motor_parameter['l_d'] - / env.physical_system.electrical_motor.motor_parameter['r_s'] - ]), - 'SynRM': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['l_q'] - / env.physical_system.electrical_motor.motor_parameter['r_s'], - env.physical_system.electrical_motor.motor_parameter['l_d'] - / env.physical_system.electrical_motor.motor_parameter['r_s'] - ]), - 'SCIM': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['l_sigs'] - / env.physical_system.electrical_motor.motor_parameter['r_s'], - env.physical_system.electrical_motor.motor_parameter['l_sigr'] - / env.physical_system.electrical_motor.motor_parameter['r_r'], - ]), - 'EESM': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['l_q'] - / env.physical_system.electrical_motor.motor_parameter['r_s'], - env.physical_system.electrical_motor.motor_parameter['l_d'] - / env.physical_system.electrical_motor.motor_parameter['r_s'], - env.physical_system.electrical_motor.motor_parameter['l_e'] - / env.physical_system.electrical_motor.motor_parameter['r_e'] - ]), -} - -r_reader = { - 'SeriesDc': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['r_a'] - + env.physical_system.electrical_motor.motor_parameter['r_e'] - ]), - 'ShuntDc': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['r_a'], - ]), - 'ExtExDc': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['r_a'], - env.physical_system.electrical_motor.motor_parameter['r_e'] - ]), - 'PermExDc': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['r_a'], - ]), - 'PMSM': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['r_s'], - env.physical_system.electrical_motor.motor_parameter['r_s'] - ]), - 'SynRM': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['r_s'], - env.physical_system.electrical_motor.motor_parameter['r_s'] - ]), - 'SCIM': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['r_s'], - env.physical_system.electrical_motor.motor_parameter['r_r'] - ]), - 'EESM': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['r_s'], - env.physical_system.electrical_motor.motor_parameter['r_s'], - env.physical_system.electrical_motor.motor_parameter['r_e'] - ]), -} - -tau_n_reader = { - 'SeriesDc': lambda env: np.array([ - (env.physical_system.electrical_motor.motor_parameter['r_a'] - + env.physical_system.electrical_motor.motor_parameter['r_e']) - / (env.physical_system.electrical_motor.motor_parameter['l_a'] - + env.physical_system.electrical_motor.motor_parameter['l_e']) - ]), - 'ShuntDc': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['r_a'] - / env.physical_system.electrical_motor.motor_parameter['l_a'], - ]), - 'ExtExDc': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['r_a'] - / env.physical_system.electrical_motor.motor_parameter['l_a'], - env.physical_system.electrical_motor.motor_parameter['r_e'] - / env.physical_system.electrical_motor.motor_parameter['l_e'] - ]), - 'PermExDc': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['r_a'] - / env.physical_system.electrical_motor.motor_parameter['l_a'], - ]), - 'PMSM': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['r_s'] - / env.physical_system.electrical_motor.motor_parameter['l_d'], - env.physical_system.electrical_motor.motor_parameter['r_s'] - / env.physical_system.electrical_motor.motor_parameter['l_q'] - ]), - 'SynRM': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['r_s'] - / env.physical_system.electrical_motor.motor_parameter['l_d'], - env.physical_system.electrical_motor.motor_parameter['r_s'] - / env.physical_system.electrical_motor.motor_parameter['l_q'] - ]), - 'SCIM': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['r_s'] - / env.physical_system.electrical_motor.motor_parameter['l_sigs'], - env.physical_system.electrical_motor.motor_parameter['r_r'] - / env.physical_system.electrical_motor.motor_parameter['l_sigr'] - ]), - 'EESM': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['r_s'] - / env.physical_system.electrical_motor.motor_parameter['l_d'], - env.physical_system.electrical_motor.motor_parameter['r_s'] - / env.physical_system.electrical_motor.motor_parameter['l_q'], - env.physical_system.electrical_motor.motor_parameter['r_e'] - / env.physical_system.electrical_motor.motor_parameter['l_e'] - ]), -} - -currents = { - 'SeriesDc': ['i'], - 'ShuntDc': ['i_a'], - 'ExtExDc': ['i_a', 'i_e'], - 'PermExDc': ['i'], - 'PMSM': ['i_sd', 'i_sq'], - 'SynRM': ['i_sd', 'i_sq'], - 'SCIM': ['i_sd', 'i_sq'], - 'EESM': ['i_sd', 'i_sq', 'i_e'], -} -emf_currents = { - 'SeriesDc': ['i'], - 'ShuntDc': ['i_e'], - 'ExtExDc': ['i_e', 'i_a'], - 'PermExDc': ['i'], - 'PMSM': ['i_sq', 'i_sd'], - 'SynRM': ['i_sq', 'i_sd'], - 'SCIM': ['i_sq', 'i_sd'], - 'EESM': ['i_sq', 'i_sd', 'i_sq'], -} - - -voltages = { - 'SeriesDc': ['u'], - 'ShuntDc': ['u'], - 'ExtExDc': ['u_a', 'u_e'], - 'PermExDc': ['u'], - 'PMSM': ['u_sd', 'u_sq'], - 'SynRM': ['u_sd', 'u_sq'], - 'SCIM': ['u_sd', 'u_sq'], - 'EESM': ['u_sd', 'u_sq', 'u_e'], -} - - -def get_output_voltages(motor_type, action_type): - if motor_type in dc_motors: - return voltages[motor_type] - elif motor_type in dc_motors: - return voltages[motor_type] - elif motor_type in dc_motors: - return voltages[motor_type] - elif motor_type in induction_motors: - return ['u_sa', 'u_sb', 'u_sc'] - elif motor_type == 'EESM': - return ['u_a', 'u_b', 'u_c', 'u_sup'] - else: - return ['u_a', 'u_b', 'u_c'] - - -l_prime_reader = { - 'SeriesDc': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['l_e_prime'] - ]), - 'ShuntDc': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['l_e_prime'] - ]), - 'ExtExDc': lambda env: np.array([ - env.physical_system.electrical_motor.motor_parameter['l_e_prime'] - ]), - 'PermExDc': lambda env: np.array([0.0]), - 'PMSM': lambda env: np.array([0.0, 0.0]), - 'SynRM': lambda env: np.array([ - - env.physical_system.electrical_motor.motor_parameter['l_sq'], - env.physical_system.electrical_motor.motor_parameter['l_sd'] - ]), - 'SCIM': lambda env: np.array([0, 0]), - 'EESM': lambda env: np.array([0.0, 0.0, 0.0]), -} - -converter_high_idle_low_action = { - cv.FiniteFourQuadrantConverter: (1, 0, 2), - cv.FiniteTwoQuadrantConverter: (1, 0, 2), - cv.FiniteOneQuadrantConverter: (1, 0, 0), - cv.FiniteB6BridgeConverter: ((1, 0, 2),) * 3, -} - - -class MotorSpecification: - - _motors = dict() - - @staticmethod - def register(motor_key): - def reg(motor_specification): - assert isinstance(motor_specification, MotorSpecification) - assert motor_key not in MotorSpecification._motors.keys() - MotorSpecification._motors[motor_key] = motor_specification - return reg - - @staticmethod - def get(motor_key): - return MotorSpecification._motors[motor_key] - - psi = None - l = None - l_emf = None - tau_current_loop = None - tau_n = None - r = None - l_prime = None - currents = None - emf_currents = None - voltages = None diff --git a/pyproject.toml b/pyproject.toml index 4eb391ee..3a22ef3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,19 +2,19 @@ name = "gym_eletric_motor" version = "2.1.0" authors = [ - "Arne Traue", - "Gerrit Book", - "Praneeth Balakrishna", - "Pascal Peters", - "Pramod Manjunatha", - "Darius Jakobeit", - "Felix Book", - "Max Schenke", - "Wilhelm Kirchgässner", - "Oliver Wallscheid", - "Barnabas Haucke-Korber", - "Stefan Arndt", - "Marius Köhler", + { name = "Arne Traue" }, + { name = "Gerrit Book" }, + { name = "Praneeth Balakrishna" }, + { name = "Pascal Peters" }, + { name = "Pramod Manjunatha" }, + { name = "Darius Jakobeit" }, + { name = "Felix Book" }, + { name = "Max Schenke" }, + { name = "Wilhelm Kirchgässner" }, + { name = "Oliver Wallscheid" }, + { name = "Barnabas Haucke-Korber" }, + { name = "Stefan Arndt" }, + { name = "Marius Köhler" }, ] description = "A Farama Gymnasium environment for electric motor control." readme = "README.md" @@ -33,5 +33,9 @@ Issues = "https://github.com/upb-lea/gym-electric-motor/issues" requires = ["hatchling"] build-backend = "hatchling.build" +[tool.hatch.build.targets.wheel] +packages = ["/src/gym_electric_motor", "/src/gem_controllers"] + [tool.ruff] +src = ["src"] line-length = 120 diff --git a/requirements.txt b/requirements.txt index 5d2561d4..4c93b6d7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ numpy>=1.16.4 scipy>=1.4.1 pytest>=5.2.2 pytest-cov -gymnasium>=0.29.0 \ No newline at end of file +gymnasium>=0.29.0 +pathspec>=0.12.0 \ No newline at end of file diff --git a/gem_controllers/README.md b/src/gem_controllers/README.md similarity index 100% rename from gem_controllers/README.md rename to src/gem_controllers/README.md diff --git a/gem_controllers/__init__.py b/src/gem_controllers/__init__.py similarity index 99% rename from gem_controllers/__init__.py rename to src/gem_controllers/__init__.py index 8396c95f..3c478396 100644 --- a/gem_controllers/__init__.py +++ b/src/gem_controllers/__init__.py @@ -10,4 +10,3 @@ from .gem_adapter import GymElectricMotorAdapter from .reference_plotter import ReferencePlotter from gem_controllers.block_diagrams.block_diagram import build_block_diagram - diff --git a/gem_controllers/block_diagrams/__init__.py b/src/gem_controllers/block_diagrams/__init__.py similarity index 100% rename from gem_controllers/block_diagrams/__init__.py rename to src/gem_controllers/block_diagrams/__init__.py diff --git a/gem_controllers/block_diagrams/block_diagram.py b/src/gem_controllers/block_diagrams/block_diagram.py similarity index 56% rename from gem_controllers/block_diagrams/block_diagram.py rename to src/gem_controllers/block_diagrams/block_diagram.py index d971320f..2ba22fd0 100644 --- a/gem_controllers/block_diagrams/block_diagram.py +++ b/src/gem_controllers/block_diagrams/block_diagram.py @@ -1,9 +1,33 @@ from control_block_diagram import ControllerDiagram from control_block_diagram.components import Point, Connection -from .stage_blocks import ext_ex_dc_cc, ext_ex_dc_ops, ext_ex_dc_output, perm_ex_dc_cc, perm_ex_dc_ops,\ - perm_ex_dc_output, pi_speed_controller, pmsm_cc, pmsm_ops, pmsm_output, scim_cc, scim_ops,\ - scim_output, series_dc_cc, series_dc_ops, series_dc_output, shunt_dc_cc, shunt_dc_ops, shunt_dc_output,\ - pmsm_speed_controller, scim_speed_controller, eesm_ops, eesm_cc, eesm_output, synrm_cc, synrm_output +from .stage_blocks import ( + ext_ex_dc_cc, + ext_ex_dc_ops, + ext_ex_dc_output, + perm_ex_dc_cc, + perm_ex_dc_ops, + perm_ex_dc_output, + pi_speed_controller, + pmsm_cc, + pmsm_ops, + pmsm_output, + scim_cc, + scim_ops, + scim_output, + series_dc_cc, + series_dc_ops, + series_dc_output, + shunt_dc_cc, + shunt_dc_ops, + shunt_dc_output, + pmsm_speed_controller, + scim_speed_controller, + eesm_ops, + eesm_cc, + eesm_output, + synrm_cc, + synrm_output, +) import gem_controllers as gc @@ -20,7 +44,7 @@ def build_block_diagram(controller, env_id, save_block_diagram_as): Control Block Diagram """ - + # Get the block building function for all stages motor_type = gc.utils.get_motor_type(env_id) control_task = gc.utils.get_control_task(env_id) @@ -56,8 +80,9 @@ def build_block_diagram(controller, env_id, save_block_diagram_as): # Save the block diagram if save_block_diagram_as is not None: - save_block_diagram_as = list(save_block_diagram_as) if isinstance(save_block_diagram_as, (tuple, list)) else [ - save_block_diagram_as] + save_block_diagram_as = ( + list(save_block_diagram_as) if isinstance(save_block_diagram_as, (tuple, list)) else [save_block_diagram_as] + ) doc.save(*save_block_diagram_as) return doc @@ -76,64 +101,64 @@ def get_stages(controller, motor_type): """ - motor_check = motor_type in ['PMSM', 'SCIM', 'EESM', 'SynRM', 'ShuntDc', 'SeriesDc', 'PermExDc', 'ExtExDc'] + motor_check = motor_type in ["PMSM", "SCIM", "EESM", "SynRM", "ShuntDc", "SeriesDc", "PermExDc", "ExtExDc"] stages = [] # Add the speed controller block function if isinstance(controller, gc.PISpeedController): - if motor_type in ['PMSM', 'SCIM', 'EESM', 'SynRM']: - stages.append(build_functions[motor_type + '_Speed_Controller']) + if motor_type in ["PMSM", "SCIM", "EESM", "SynRM"]: + stages.append(build_functions[motor_type + "_Speed_Controller"]) else: - stages.append(build_functions['PI_Speed_Controller']) + stages.append(build_functions["PI_Speed_Controller"]) controller = controller.torque_controller # add the torque controller block function if isinstance(controller, gc.torque_controller.TorqueController): if motor_check: - stages.append(build_functions[motor_type + '_OPS']) + stages.append(build_functions[motor_type + "_OPS"]) controller = controller.current_controller # add the current controller block function if isinstance(controller, gc.PICurrentController): emf_feedforward = controller.emf_feedforward is not None if motor_check: - stages.append(build_functions[motor_type + '_CC'](emf_feedforward)) + stages.append(build_functions[motor_type + "_CC"](emf_feedforward)) # add the output block function - stages.append((build_functions[motor_type + '_Output'](controller.emf_feedforward is not None))) + stages.append((build_functions[motor_type + "_Output"](controller.emf_feedforward is not None))) return stages # dictonary of all block building functions build_functions = { - 'PI_Speed_Controller': pi_speed_controller, - 'PMSM_Speed_Controller': pmsm_speed_controller, - 'SCIM_Speed_Controller': scim_speed_controller, - 'EESM_Speed_Controller': pmsm_speed_controller, - 'SynRM_Speed_Controller': pmsm_speed_controller, - 'PMSM_OPS': pmsm_ops, - 'SCIM_OPS': scim_ops, - 'EESM_OPS': eesm_ops, - 'SynRM_OPS': pmsm_ops, - 'SeriesDc_OPS': series_dc_ops, - 'ShuntDc_OPS': shunt_dc_ops, - 'PermExDc_OPS': perm_ex_dc_ops, - 'ExtExDc_OPS': ext_ex_dc_ops, - 'PMSM_CC': pmsm_cc, - 'SCIM_CC': scim_cc, - 'EESM_CC': eesm_cc, - 'SynRM_CC': synrm_cc, - 'SeriesDc_CC': series_dc_cc, - 'ShuntDc_CC': shunt_dc_cc, - 'PermExDc_CC': perm_ex_dc_cc, - 'ExtExDc_CC': ext_ex_dc_cc, - 'PMSM_Output': pmsm_output, - 'SCIM_Output': scim_output, - 'EESM_Output': eesm_output, - 'SynRM_Output': synrm_output, - 'SeriesDc_Output': series_dc_output, - 'ShuntDc_Output': shunt_dc_output, - 'PermExDc_Output': perm_ex_dc_output, - 'ExtExDc_Output': ext_ex_dc_output, + "PI_Speed_Controller": pi_speed_controller, + "PMSM_Speed_Controller": pmsm_speed_controller, + "SCIM_Speed_Controller": scim_speed_controller, + "EESM_Speed_Controller": pmsm_speed_controller, + "SynRM_Speed_Controller": pmsm_speed_controller, + "PMSM_OPS": pmsm_ops, + "SCIM_OPS": scim_ops, + "EESM_OPS": eesm_ops, + "SynRM_OPS": pmsm_ops, + "SeriesDc_OPS": series_dc_ops, + "ShuntDc_OPS": shunt_dc_ops, + "PermExDc_OPS": perm_ex_dc_ops, + "ExtExDc_OPS": ext_ex_dc_ops, + "PMSM_CC": pmsm_cc, + "SCIM_CC": scim_cc, + "EESM_CC": eesm_cc, + "SynRM_CC": synrm_cc, + "SeriesDc_CC": series_dc_cc, + "ShuntDc_CC": shunt_dc_cc, + "PermExDc_CC": perm_ex_dc_cc, + "ExtExDc_CC": ext_ex_dc_cc, + "PMSM_Output": pmsm_output, + "SCIM_Output": scim_output, + "EESM_Output": eesm_output, + "SynRM_Output": synrm_output, + "SeriesDc_Output": series_dc_output, + "ShuntDc_Output": shunt_dc_output, + "PermExDc_Output": perm_ex_dc_output, + "ExtExDc_Output": ext_ex_dc_output, } diff --git a/gem_controllers/block_diagrams/stage_blocks/__init__.py b/src/gem_controllers/block_diagrams/stage_blocks/__init__.py similarity index 100% rename from gem_controllers/block_diagrams/stage_blocks/__init__.py rename to src/gem_controllers/block_diagrams/stage_blocks/__init__.py diff --git a/src/gem_controllers/block_diagrams/stage_blocks/eesm_cc.py b/src/gem_controllers/block_diagrams/stage_blocks/eesm_cc.py new file mode 100644 index 00000000..9563bb85 --- /dev/null +++ b/src/gem_controllers/block_diagrams/stage_blocks/eesm_cc.py @@ -0,0 +1,326 @@ +from control_block_diagram.components import Point, Box, Connection, Circle +from control_block_diagram.predefined_components import ( + DqToAbcTransformation, + AbcToAlphaBetaTransformation, + AlphaBetaToDqTransformation, + Add, + PIController, + Multiply, + Limit, +) + + +def eesm_cc(emf_feedforward): + """ + Args: + emf_feedforward: Boolean whether emf feedforward stage is included + + Returns: + Function to build the EESM current control block + """ + + def cc_eesm(start, control_task): + """ + Function to build the EESM current control block + Args: + start: Starting point of the block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + # space to the previous block + space = 1 if control_task == "CC" else 1.5 + + # Add blocks for the i_sd and i_sq references and states + add_i_e = Add(start.add(space - 0.5, 1)) + add_i_sd = Add(start.add_x(space + 0.5)) + add_i_sq = Add(start.add(space, -1)) + + if control_task == "CC": + # Connections at the inputs + Connection.connect( + add_i_sd.input_left[0].sub_x(space + 1), + add_i_sd.input_left[0], + text=r"$i^{*}_{\mathrm{sd}}$", + text_position="start", + text_align="left", + ) + Connection.connect( + add_i_sq.input_left[0].sub_x(space + 0.5), + add_i_sq.input_left[0], + text=r"$i^{*}_{\mathrm{sq}}$", + text_position="start", + text_align="left", + ) + Connection.connect( + add_i_e.input_left[0].sub_x(space), + add_i_e.input_left[0], + text=r"$i^{*}_{\mathrm{e}}$", + text_position="start", + text_align="left", + ) + + # PI Controllers for the d and q component + pi_i_sd = PIController(add_i_sd.position.add_x(1.2), size=(1, 0.8), input_number=1, output_number=1) + pi_i_sq = PIController( + Point.merge(pi_i_sd.position, add_i_sq.position), size=(1, 0.8), input_number=1, output_number=1 + ) + pi_i_e = PIController( + Point.merge(pi_i_sd.position, add_i_e.position), + size=(1, 0.8), + input_number=1, + output_number=1, + text="Current\nController", + ) + + # Connection between the add blocks and the PI Controllers + Connection.connect(add_i_sd.output_right, pi_i_sd.input_left) + Connection.connect(add_i_sq.output_right, pi_i_sq.input_left) + Connection.connect(add_i_e.output_right, pi_i_e.input_left) + + # Add blocks for the EMF Feedforward + add_u_sd = Add(pi_i_sd.position.add_x(1.6)) + add_u_sq = Add(pi_i_sq.position.add_x(1.2)) + add_u_e = Add(pi_i_e.position.add_x(2)) + + # Connections between the PI Controllers and the Add blocks + Connection.connect( + pi_i_sd.output_right[0], add_u_sd.input_left[0], text=r"$\Delta u^{*}_{\mathrm{sd}}$", distance_y=0.28 + ) + Connection.connect( + pi_i_sq.output_right[0], + add_u_sq.input_left[0], + text=r"$\Delta u^{*}_{\mathrm{sq}}$", + distance_y=0.4, + move_text=(0.25, 0), + ) + + # Coordinate transformation from DQ to Abc coordinates + dq_to_abc = DqToAbcTransformation(Point.get_mid(add_u_sd.position, add_u_sq.position).add_x(2), input_space=1) + + # Connections between the add blocks and the coordinate transformation + Connection.connect( + add_u_sd.output_right[0], + dq_to_abc.input_left[0], + text=r"$u^{*}_{\mathrm{sd}}$", + distance_y=0.28, + move_text=(0.18, 0), + ) + Connection.connect( + add_u_sq.output_right[0], + dq_to_abc.input_left[1], + text=r"$u^{*}_{\mathrm{sq}}$", + distance_y=0.28, + move_text=(0.38, 0), + ) + + # Limit of the input voltages + limit = Limit( + dq_to_abc.position.add_x(1.8), + size=(1, 1.2), + inputs=dict(left=3, left_space=0.3), + outputs=dict(right=3, right_space=0.3), + ) + + limit_e = Limit( + Point.merge(limit.position, pi_i_e.position), size=(1, 1), inputs=dict(left=1), outputs=dict(right=1) + ) + + # Connection between the coordinate transformation and the limit block + Connection.connect(dq_to_abc.output_right, limit.input_left) + Connection.connect(pi_i_e.output_right, limit_e.input_left) + + # Pulse width modulation block + pwm = Box( + limit.position.add_x(2.5), + size=(1.5, 1.2), + text="PWM", + inputs=dict(left=3, left_space=0.3), + outputs=dict(right=3, right_space=0.3), + ) + + pwm_e = Box(limit_e.position.add_x(2.5), size=(1.5, 1), text="PWM", inputs=dict(left=1), outputs=dict(right=1)) + + # Connection between the limit and the PWM block + Connection.connect( + limit.output_right, + pwm.input_left, + text=["", "", r"$u^*_{\mathrm{s a,b,c}}$"], + distance_y=0.25, + text_align="bottom", + ) + Connection.connect(limit_e.output_right, pwm_e.input_left, text=[r"$u^*_{\mathrm{e}}$"]) + + # Coordinate transformation from ABC to AlphaBeta coordinates + abc_to_alpha_beta = AbcToAlphaBetaTransformation(pwm.position.sub(1, 3.5), input="right", output="left") + + # Coordinate transformation from AlphaBeta to DQ coordinates + alpha_beta_to_dq = AlphaBetaToDqTransformation( + Point.merge(dq_to_abc.position, abc_to_alpha_beta.position), input="right", output="left" + ) + + emf = Box( + Point.merge(add_u_sd.position, Point.get_mid(dq_to_abc.position, alpha_beta_to_dq.position)), + size=(2, 1), + inputs=dict(bottom=4), + outputs=dict(top=3, top_space=0.4), + text="EMF Feedforward", + ) + + Connection.connect(emf.output_top[0], add_u_sq.input_bottom[0]) + Connection.connect(emf.output_top[1], add_u_sd.input_bottom[0]) + Connection.connect(emf.output_top[2], add_u_e.input_bottom[0]) + + # Connections between the coordinate transformation and the add blocks + con_3 = Connection.connect( + alpha_beta_to_dq.output_left[0], + add_i_sd.input_bottom[0], + text=r"-", + text_position="end", + text_align="right", + move_text=(-0.2, -0.2), + ) + con_4 = Connection.connect( + alpha_beta_to_dq.output_left[1], + add_i_sq.input_bottom[0], + text=r"-", + text_position="end", + text_align="right", + move_text=(-0.2, -0.2), + ) + + # Connections between the previous conncetions and the inductances blocks + Connection.connect_to_line(con_3, emf.input_bottom[3]) + Connection.connect_to_line(con_4, emf.input_bottom[2]) + + # Derivation of the angle + box_d_dt = Box( + Point.merge(alpha_beta_to_dq.position, start.sub_y(7.42020066645696)).sub_x(1.5), + size=(1, 0.8), + text=r"$\mathrm{d} / \mathrm{d}t$", + inputs=dict(right=1), + outputs=dict(left=1), + ) + + # Conncetion between the derivation and multiplication block + con_omega = Connection.connect( + box_d_dt.output_left[0], + emf.input_bottom[0], + space_y=1, + text=r"$\omega_{\mathrm{el}}$", + move_text=(0, 1.7), + text_align="left", + distance_x=0.3, + ) + + if control_task == "SC": + # Connector at the previous connection + Circle(con_omega.points[1], radius=0.05, fill="black") + + # Conncetion between the coordinate transformations + Connection.connect( + abc_to_alpha_beta.output, + alpha_beta_to_dq.input_right, + text=[r"$i_{\mathrm{s} \upalpha}$", r"$i_{\mathrm{s} \upbeta}$"], + ) + + # Add block for the advanced angle + add = Add( + Point.get_mid(dq_to_abc.position, alpha_beta_to_dq.position), + inputs=dict(bottom=1, right=1), + outputs=dict(top=1), + ) + + # Connections of the add block + Connection.connect( + alpha_beta_to_dq.output_top, + add.input_bottom, + text=r"$\varepsilon_{\mathrm{el}}$", + text_align="right", + move_text=(0, -0.1), + ) + Connection.connect(add.output_top, dq_to_abc.input_bottom) + + # Calculate the advanced angle + box_t_a = Box( + add.position.add_x(1.5), + size=(1, 0.8), + text=r"$1.5 T_{\mathrm{s}}$", + inputs=dict(right=1), + outputs=dict(left=1), + ) + + # Connections of the advanced angle block + Connection.connect(box_t_a.output, add.input_right, text=r"$\Delta \varepsilon$") + Connection.connect( + box_t_a.input[0].add_x(0.5), + box_t_a.input[0], + text=r"$\omega_{\mathrm{el}}$", + text_position="start", + text_align="right", + distance_x=0.3, + ) + + start = pwm.position # starting point of the next block + + # Inputs of the stage + inputs = dict( + i_d_ref=[ + add_i_sd.input_left[0], + dict( + text=r"$i^{*}_{\mathrm{sd}}$", + distance_y=0.25, + move_text=(0.3, 0), + text_position="start", + text_aglin="top", + ), + ], + i_q_ref=[ + add_i_sq.input_left[0], + dict( + text=r"$i^{*}_{\mathrm{sq}}$", + distance_y=0.25, + move_text=(0.3, 0), + text_position="start", + text_aglin="top", + ), + ], + i_e_ref=[ + add_i_e.input_left[0], + dict( + text=r"$i^{*}_{\mathrm{e}}$", + distance_y=0.25, + move_text=(0.3, 0), + text_position="start", + text_aglin="top", + ), + ], + epsilon=[box_d_dt.input_right[0], dict()], + i_e=[ + add_i_e.input_bottom[0], + dict(text=r"-", text_position="end", text_align="right", move_text=(-0.2, -0.2)), + ], + ) + + # Outputs of the stage + outputs = dict(S=pwm.output_right, omega=con_omega.points[1], S_e=pwm_e.output_right[0]) + + # Connections to other lines + connect_to_line = dict( + epsilon=[ + alpha_beta_to_dq.input_bottom[0], + dict(text=r"$\varepsilon_{\mathrm{el}}$", text_position="middle", text_align="right"), + ], + i=[ + abc_to_alpha_beta.input_right, + dict(radius=0.1, fill=False, text=[r"$\mathbf{i}_{\mathrm{s a,b,c}}$", "", ""]), + ], + i_e=[emf.input_bottom[1], dict(radius=0.05, fill="black")], + ) + connections = dict() # Connections + + return start, inputs, outputs, connect_to_line, connections + + return cc_eesm diff --git a/gem_controllers/block_diagrams/stage_blocks/eesm_ops.py b/src/gem_controllers/block_diagrams/stage_blocks/eesm_ops.py similarity index 55% rename from gem_controllers/block_diagrams/stage_blocks/eesm_ops.py rename to src/gem_controllers/block_diagrams/stage_blocks/eesm_ops.py index b156da52..fcd5720a 100644 --- a/gem_controllers/block_diagrams/stage_blocks/eesm_ops.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/eesm_ops.py @@ -4,6 +4,7 @@ class PsiOptBox(Box): """Optimal flux block""" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -12,19 +13,27 @@ def __init__(self, *args, **kwargs): by = 0.15 # Coordinate system - Connection.connect(self.bottom_left.add(self._size_x * bx, self._size_y * by), - self.bottom_right.add(-self._size_x * bx, self._size_y * by)) + Connection.connect( + self.bottom_left.add(self._size_x * bx, self._size_y * by), + self.bottom_right.add(-self._size_x * bx, self._size_y * by), + ) Connection.connect(self.bottom.add_y(self._size_y * by), self.top.sub_y(self._size_y * by)) # Graph in the coordinate system - Path([self.top_left.add(self._size_x * bx, -self._size_y * (0.1 + by)), - self.bottom.add_y(self._size_y * (0.2 + by)), - self.top_right.sub(self._size_x * bx, self._size_y * (0.1 + by))], - angles=[{'in': 180, 'out': 0}, {'in': 180, 'out': 0}], arrow=False) + Path( + [ + self.top_left.add(self._size_x * bx, -self._size_y * (0.1 + by)), + self.bottom.add_y(self._size_y * (0.2 + by)), + self.top_right.sub(self._size_x * bx, self._size_y * (0.1 + by)), + ], + angles=[{"in": 180, "out": 0}, {"in": 180, "out": 0}], + arrow=False, + ) class TMaxPsiBox(Box): """Maximum torque block""" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -38,10 +47,16 @@ def __init__(self, *args, **kwargs): Connection.connect(self.left.add_x(bx), self.right.sub_x(bx)) # Graph in the coordinate system - Path([self.left.add_x(bx), self.top_right.sub(scale * bx, scale * by)], arrow=False, - angles=[{'in': 180, 'out': 35}]) - Path([self.left.add_x(bx), self.bottom_right.add(-scale * bx, scale * by)], arrow=False, - angles=[{'in': 180, 'out': -35}]) + Path( + [self.left.add_x(bx), self.top_right.sub(scale * bx, scale * by)], + arrow=False, + angles=[{"in": 180, "out": 35}], + ) + Path( + [self.left.add_x(bx), self.bottom_right.add(-scale * bx, scale * by)], + arrow=False, + angles=[{"in": 180, "out": -35}], + ) def eesm_ops(start, control_task): @@ -55,50 +70,55 @@ def eesm_ops(start, control_task): endpoint, inputs, outputs, connection to other lines, connections """ - if control_task == 'TC': + if control_task == "TC": # Connection at the input - Connection.connect(start, start.add_x(1), text='$T^{*}$', text_position='start', text_align='left', arrow=False) + Connection.connect(start, start.add_x(1), text="$T^{*}$", text_position="start", text_align="left", arrow=False) # Limit the torque reference box_limit = Limit(start.add_x(6), inputs=dict(left=1, bottom=1), size=(1, 1)) # Calculate the optimal flux box_psi_opt = PsiOptBox(start.add(2, -1.7), size=(1.2, 1)) - Text(position=box_psi_opt.top.add_y(0.25), text=r'$\Psi^{*}_{\mathrm{opt}}(T^{*})$') + Text(position=box_psi_opt.top.add_y(0.25), text=r"$\Psi^{*}_{\mathrm{opt}}(T^{*})$") # Minimum block - box_min = Box(box_psi_opt.position.add(1.5, -1.3), inputs=dict(left=2, left_space=0.5), size=(0.8, 1), text='min') + box_min = Box(box_psi_opt.position.add(1.5, -1.3), inputs=dict(left=2, left_space=0.5), size=(0.8, 1), text="min") # Calculate the maximum torque box_t_max = TMaxPsiBox(box_psi_opt.position.add_x(3.1), size=(1.2, 1)) - Text(position=box_t_max.top.add_y(0.25), text=r'$T_{\mathrm{max}}(\Psi)$') + Text(position=box_t_max.top.add_y(0.25), text=r"$T_{\mathrm{max}}(\Psi)$") # Connect the maximum torque and limit block con_torque = Connection.connect(start.add_x(1), box_limit.input_left[0]) # Connection between the input and the optimum flux block - Connection.connect(start.add_x(1), box_psi_opt.input_left[0], start_direction='south') - Circle(start.add_x(1), radius=0.05, fill='black') + Connection.connect(start.add_x(1), box_psi_opt.input_left[0], start_direction="south") + Circle(start.add_x(1), radius=0.05, fill="black") # Connection between the optimum flux and minimum block Connection.connect(box_psi_opt.output_right[0], box_min.input_left[0]) # Conncetion between the minimum and maximum torque block - Connection.connect(box_min.output_right[0].add_x(0.3), box_t_max.input_left[0], start_direction='north') + Connection.connect(box_min.output_right[0].add_x(0.3), box_t_max.input_left[0], start_direction="north") # Circle at the connection - Circle(box_min.output_right[0].add_x(0.3), radius=0.05, fill='black') + Circle(box_min.output_right[0].add_x(0.3), radius=0.05, fill="black") # Conncetion between the maximum torque and torque limit block Connection.connect(box_t_max.output_right[0], box_limit.input_bottom[0]) # Optimal operation point function block - box_f_psi_t = Box(box_limit.position.add(2.2, -1.5), size=(1.3, 3.5), inputs=dict(left=2, left_space=3), - outputs=dict(right=3, right_space=1), text=r'\textbf{f}($\Psi$, $T$)') + box_f_psi_t = Box( + box_limit.position.add(2.2, -1.5), + size=(1.3, 3.5), + inputs=dict(left=2, left_space=3), + outputs=dict(right=3, right_space=1), + text=r"\textbf{f}($\Psi$, $T$)", + ) # Conncetions to the optimal opertion point function block - Connection.connect(box_min.output_right[0], box_f_psi_t.input_left[1], text=r'$\Psi^{*}_{\mathrm{lim}}$') - Connection.connect(box_limit.output_right[0], box_f_psi_t.input_left[0], text=r'$T^{*}_{\mathrm{lim}}$') + Connection.connect(box_min.output_right[0], box_f_psi_t.input_left[1], text=r"$\Psi^{*}_{\mathrm{lim}}$") + Connection.connect(box_limit.output_right[0], box_f_psi_t.input_left[0], text=r"$T^{*}_{\mathrm{lim}}$") # Moulation Controller @@ -106,62 +126,92 @@ def eesm_ops(start, control_task): add_psi = Add(box_min.input_left[1].sub_x(1)) # Connection between the add and minimum block - Connection.connect(add_psi.output_right[0], box_min.input_left[1], text=r'$\Psi_{\mathrm{lim}}$', text_align='top', - distance_y=0.25) + Connection.connect( + add_psi.output_right[0], box_min.input_left[1], text=r"$\Psi_{\mathrm{lim}}$", text_align="top", distance_y=0.25 + ) # Limit the delta flux limit_modulation = Limit(add_psi.input_left[0].sub_x(1.5), size=(1, 1)) # I Controller of the modulation controller - i_controller = IController(limit_modulation.input_left[0].sub_x(1), size=(1.2, 1), text='Modulation\nController') + i_controller = IController(limit_modulation.input_left[0].sub_x(1), size=(1.2, 1), text="Modulation\nController") # Connections of the limit block Connection.connect(i_controller.output_right, limit_modulation.input_left) - Connection.connect(limit_modulation.output_right[0], add_psi.input_left[0], text=r'$\Delta \Psi$', distance_y=0.25) + Connection.connect(limit_modulation.output_right[0], add_psi.input_left[0], text=r"$\Delta \Psi$", distance_y=0.25) # Add block of the modulation controller add_a = Add(i_controller.position.sub_x(1.5)) # Maximum modulation - box_a_max = Box(add_a.input_left[0].sub_x(1.3), size=(1.5, 0.8), text=r'$a_{\mathrm{max}} \cdot k$') + box_a_max = Box(add_a.input_left[0].sub_x(1.3), size=(1.5, 0.8), text=r"$a_{\mathrm{max}} \cdot k$") # Building the absolute modulation - box_abs = Box(add_a.position.sub_y(1.2), size=(0.8, 0.8), text=r'|\textbf{x}|', inputs=dict(bottom=1), - outputs=dict(top=1)) + box_abs = Box( + add_a.position.sub_y(1.2), size=(0.8, 0.8), text=r"|\textbf{x}|", inputs=dict(bottom=1), outputs=dict(top=1) + ) # Conncetions of the add block - Connection.connect(box_a_max.output_right[0], add_a.input_left[0], text='$a^{*}$') + Connection.connect(box_a_max.output_right[0], add_a.input_left[0], text="$a^{*}$") Connection.connect(add_a.output_right[0], i_controller.input_left[0]) - con_a = Connection.connect(box_abs.output_top[0], add_a.input_bottom[0], text='$-$', text_align='right', - text_position='end', move_text=(-0.1, -0.2)) + con_a = Connection.connect( + box_abs.output_top[0], + add_a.input_bottom[0], + text="$-$", + text_align="right", + text_position="end", + move_text=(-0.1, -0.2), + ) # Additional text at the connection - Text(position=Point.get_mid(*con_a.points).sub_x(0.25), text='$a$') + Text(position=Point.get_mid(*con_a.points).sub_x(0.25), text="$a$") # divide the actual voltage by the dc voltage - div_a = Divide(box_abs.input_bottom[0].sub_y(1), size=(1, 0.5), inputs='bottom', input_space=0.5) + div_a = Divide(box_abs.input_bottom[0].sub_y(1), size=(1, 0.5), inputs="bottom", input_space=0.5) # Conncetions of the divide block - Connection.connect(div_a.input_bottom[0].sub_y(0.7), div_a.input_bottom[0], text=r'$\mathbf{u^{*}_{\mathrm{dq}}}$', - text_position='start', text_align='bottom') - Connection.connect(div_a.input_bottom[1].sub_y(0.7), div_a.input_bottom[1], - text=r'$\frac{u_{\mathrm{\mbox{\fontsize{3}{4}\selectfont DC}}}}{2}$', text_position='start', - text_align='bottom') + Connection.connect( + div_a.input_bottom[0].sub_y(0.7), + div_a.input_bottom[0], + text=r"$\mathbf{u^{*}_{\mathrm{dq}}}$", + text_position="start", + text_align="bottom", + ) + Connection.connect( + div_a.input_bottom[1].sub_y(0.7), + div_a.input_bottom[1], + text=r"$\frac{u_{\mathrm{\mbox{\fontsize{3}{4}\selectfont DC}}}}{2}$", + text_position="start", + text_align="bottom", + ) Connection.connect(div_a.output_top[0], box_abs.input_bottom[0]) # Divide the dc voltage by the electrical speed - div_psi = Divide(add_psi.input_bottom[0].sub_y(2), size=(1, 0.5), inputs='bottom', input_space=0.5) + div_psi = Divide(add_psi.input_bottom[0].sub_y(2), size=(1, 0.5), inputs="bottom", input_space=0.5) # Connections of the divide block - Connection.connect(div_psi.output_top[0], add_psi.input_bottom[0], text=r'$\Psi_{\mathrm{max}}$', - text_align='right', distance_x=0.5) - Connection.connect(div_psi.input_bottom[0].sub_y(0.7), div_psi.input_bottom[0], - text=r'$\frac{u_{\mathrm{\mbox{\fontsize{3}{4}\selectfont DC}}}}{\sqrt{3}}$', - text_position='start', text_align='bottom') + Connection.connect( + div_psi.output_top[0], + add_psi.input_bottom[0], + text=r"$\Psi_{\mathrm{max}}$", + text_align="right", + distance_x=0.5, + ) + Connection.connect( + div_psi.input_bottom[0].sub_y(0.7), + div_psi.input_bottom[0], + text=r"$\frac{u_{\mathrm{\mbox{\fontsize{3}{4}\selectfont DC}}}}{\sqrt{3}}$", + text_position="start", + text_align="bottom", + ) # Limit of the current reference values - limit = Limit(Point.get_mid(box_f_psi_t.output_right[1], box_f_psi_t.output_right[2]).add_x(1), size=(1, 1.5), - inputs=dict(left=2, left_space=1), outputs=dict(right=2, right_space=1)) + limit = Limit( + Point.get_mid(box_f_psi_t.output_right[1], box_f_psi_t.output_right[2]).add_x(1), + size=(1, 1.5), + inputs=dict(left=2, left_space=1), + outputs=dict(right=2, right_space=1), + ) limit_e = Limit(box_f_psi_t.output_right[0].add_x(1), size=(1, 1), inputs=dict(left=1), outputs=dict(right=1)) # Connections between the optimal opertion point function and limit block @@ -170,12 +220,14 @@ def eesm_ops(start, control_task): Connection.connect(box_f_psi_t.output_right[2], limit.input_left[1]) # Inputs of the stage - inputs = dict(t_ref=[con_torque.start, dict(arrow=False, text=r'$T^{*}$')], - omega=[div_psi.input_bottom[1], dict(text=r'$\omega_{\mathrm{el}}$', move_text=(3, 0))]) + inputs = dict( + t_ref=[con_torque.start, dict(arrow=False, text=r"$T^{*}$")], + omega=[div_psi.input_bottom[1], dict(text=r"$\omega_{\mathrm{el}}$", move_text=(3, 0))], + ) # Outputs of the stage outputs = dict(i_d_ref=limit.output_right[0], i_q_ref=limit.output_right[1], i_e_ref=limit_e.output_right[0]) - connect_to_lines = dict() # Connections to other lines - connections = dict() # Connections of the stage - start = limit.output_right[0] # Starting point of the next stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Connections of the stage + start = limit.output_right[0] # Starting point of the next stage return start, inputs, outputs, connect_to_lines, connections diff --git a/gem_controllers/block_diagrams/stage_blocks/eesm_output.py b/src/gem_controllers/block_diagrams/stage_blocks/eesm_output.py similarity index 67% rename from gem_controllers/block_diagrams/stage_blocks/eesm_output.py rename to src/gem_controllers/block_diagrams/stage_blocks/eesm_output.py index 699b034d..695d92d5 100644 --- a/gem_controllers/block_diagrams/stage_blocks/eesm_output.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/eesm_output.py @@ -26,23 +26,29 @@ def _eesm_output(start, control_task): converter = DcConverter(start.add(2.7, 0.15), input_number=4, input_space=0.3, output_number=5) # PMSM block - eesm = EESM(converter.position.sub_y(6), size=1.3, input='top') + eesm = EESM(converter.position.sub_y(6), size=1.3, input="top") # Connection between the converter and the motor block con_1 = Connection.connect(converter.output_bottom, eesm.input_top, arrow=False) output_i_e = Circle(con_1[3].start.sub_y(4.2), radius=0.1, outputs=dict(left=1), fill=None) con_2 = Connection.connect(output_i_e.output_left[0], output_i_e.output_left[0].sub_x(11), arrow=False) - Text(r'$i_{\mathrm{e}}$', output_i_e.output_left[0].add(-2, 0.25)) + Text(r"$i_{\mathrm{e}}$", output_i_e.output_left[0].add(-2, 0.25)) - start = eesm.position # starting point of the next block + start = eesm.position # starting point of the next block # Inputs of the stage - inputs = dict(S=[converter.input_left[1:], dict(text=['', '', r'$\mathbf{S}_{\mathrm{a,b,c}}$'], - distance_y=0.25, text_align='bottom')], - S_e=[converter.input_left[0], dict(text=r'$\mathbf{S}_{\mathrm{e}}$', - distance_y=0.25, text_position='start', move_text=(0.4, 0))]) - outputs = dict(epsilon=eesm.output_left[0], i_e=con_2.end) # Outputs of the stage - connect_to_lines = dict() # Connections to other lines - connections = dict(i=con_1, i_e=con_2) # Connections + inputs = dict( + S=[ + converter.input_left[1:], + dict(text=["", "", r"$\mathbf{S}_{\mathrm{a,b,c}}$"], distance_y=0.25, text_align="bottom"), + ], + S_e=[ + converter.input_left[0], + dict(text=r"$\mathbf{S}_{\mathrm{e}}$", distance_y=0.25, text_position="start", move_text=(0.4, 0)), + ], + ) + outputs = dict(epsilon=eesm.output_left[0], i_e=con_2.end) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict(i=con_1, i_e=con_2) # Connections return start, inputs, outputs, connect_to_lines, connections diff --git a/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_cc.py b/src/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_cc.py similarity index 63% rename from gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_cc.py rename to src/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_cc.py index fa6a64ba..4fc8dfce 100644 --- a/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_cc.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_cc.py @@ -34,16 +34,18 @@ def cc_ext_ex_dc(start, control_task): # Add block for the e-current reference and state add_i_a = Add(start_i_a.add_x(space)) - if control_task == 'CC': + if control_task == "CC": # Connection at the input of the i_e path - Connection.connect(start, add_i_e.input_left[0], text=r'$i^{*}_{\mathrm{e}}$', text_align='left', - text_position='start') + Connection.connect( + start, add_i_e.input_left[0], text=r"$i^{*}_{\mathrm{e}}$", text_align="left", text_position="start" + ) # Connection at the input of the i_a path - Connection.connect(start_i_a, add_i_a.input_left[0], text=r'$i^{*}_{\mathrm{a}}$', text_align='left', - text_position='start') + Connection.connect( + start_i_a, add_i_a.input_left[0], text=r"$i^{*}_{\mathrm{a}}$", text_align="left", text_position="start" + ) # PI Current Controller of the i_e path - pi_i_e = PIController(add_i_e.position.add_x(1.5), text='Current\nController') + pi_i_e = PIController(add_i_e.position.add_x(1.5), text="Current\nController") # Connection between the PI Controller and the add block Connection.connect(add_i_e.output_right, pi_i_e.input_left) @@ -55,29 +57,47 @@ def cc_ext_ex_dc(start, control_task): Connection.connect(add_i_a.output_right, pi_i_a.input_left) # Inputs of the stage - inputs = dict(i_e_ref=[add_i_e.input_left[0], dict(text=r'$i^{*}_{\mathrm{e}}$', move_text=(-0.1, 0.1))], - i_a_ref=[add_i_a.input_left[0], dict(text=r'$i^{*}_{\mathrm{a}}$', move_text=(-0.1, 0.1))], - i_a=[add_i_a.input_bottom[0], dict(text=r'-', move_text=(-0.2, -0.2), text_position='end', - text_align='right')], - i_e=[add_i_e.input_bottom[0], dict(text=r'-', move_text=(-0.2, -0.2), text_position='end', - text_align='right')]) - connect_to_lines = dict() # Connections to other lines + inputs = dict( + i_e_ref=[add_i_e.input_left[0], dict(text=r"$i^{*}_{\mathrm{e}}$", move_text=(-0.1, 0.1))], + i_a_ref=[add_i_a.input_left[0], dict(text=r"$i^{*}_{\mathrm{a}}$", move_text=(-0.1, 0.1))], + i_a=[ + add_i_a.input_bottom[0], + dict(text=r"-", move_text=(-0.2, -0.2), text_position="end", text_align="right"), + ], + i_e=[ + add_i_e.input_bottom[0], + dict(text=r"-", move_text=(-0.2, -0.2), text_position="end", text_align="right"), + ], + ) + connect_to_lines = dict() # Connections to other lines if emf_feedforward: # Add block of the emf feedforward add_psi = Add(pi_i_a.position.add_x(2)) # Connection between the PI Controller and the add block - Connection.connect(pi_i_a.output_right, add_psi.input_left, text=r'$\Delta u_{\mathrm{a}}$', - distance_y=0.25) + Connection.connect( + pi_i_a.output_right, add_psi.input_left, text=r"$\Delta u_{\mathrm{a}}$", distance_y=0.25 + ) # Multiplication with the flux - box_psi = Box(add_psi.position.sub_y(2), size=(1, 0.8), text=r"$L'_{\mathrm{e}} i_{\mathrm{e}}$", - inputs=dict(bottom=1), outputs=dict(top=1)) + box_psi = Box( + add_psi.position.sub_y(2), + size=(1, 0.8), + text=r"$L'_{\mathrm{e}} i_{\mathrm{e}}$", + inputs=dict(bottom=1), + outputs=dict(top=1), + ) # Connection between the multiplication and the add block - Connection.connect(box_psi.output_top, add_psi.input_bottom, text=r'$u_0$', text_position='end', - text_align='right', move_text=(-0.05, -0.25)) + Connection.connect( + box_psi.output_top, + add_psi.input_bottom, + text=r"$u_0$", + text_position="end", + text_align="right", + move_text=(-0.05, -0.25), + ) # Limit of the i_a current limit_a = Limit(add_psi.position.add_x(1.5), size=(1, 1)) @@ -86,16 +106,16 @@ def cc_ext_ex_dc(start, control_task): Connection.connect(add_psi.output_right, limit_a.input_left) # Pulse width modulation block of the i_a path - pwm_a = Box(limit_a.output_right[0].add_x(1.5), size=(1.2, 0.8), text='PWM') + pwm_a = Box(limit_a.output_right[0].add_x(1.5), size=(1.2, 0.8), text="PWM") # Connection between the a-limit and the a-pwm - Connection.connect(limit_a.output_right, pwm_a.input_left, text=r'$u^{*}_{\mathrm{a}}$', distance_y=0.25) + Connection.connect(limit_a.output_right, pwm_a.input_left, text=r"$u^{*}_{\mathrm{a}}$", distance_y=0.25) # Set the input of the emf feedforward - if control_task in ['CC', 'TC']: - inputs['omega'] = [box_psi.input_bottom[0], dict()] - elif control_task == 'SC': - connect_to_lines['omega'] = [box_psi.input_bottom[0], dict(section=0)] + if control_task in ["CC", "TC"]: + inputs["omega"] = [box_psi.input_bottom[0], dict()] + elif control_task == "SC": + connect_to_lines["omega"] = [box_psi.input_bottom[0], dict(section=0)] else: # Limit of the i_a current limit_a = Limit(pi_i_a.output_right[0].add_x(1), size=(1, 1)) @@ -104,10 +124,10 @@ def cc_ext_ex_dc(start, control_task): Connection.connect(pi_i_a.output_right, limit_a.input_left) # Pulse width modulation block of the i_a path - pwm_a = Box(limit_a.position.add_x(2), size=(1.2, 0.8), text='PWM') + pwm_a = Box(limit_a.position.add_x(2), size=(1.2, 0.8), text="PWM") # Connection between the a-limit and the a-pwm - Connection.connect(limit_a.output_right, pwm_a.input_left, text=r'$u^{*}_{\mathrm{a}}$', distance_y=0.25) + Connection.connect(limit_a.output_right, pwm_a.input_left, text=r"$u^{*}_{\mathrm{a}}$", distance_y=0.25) # Limit of the i_a current limit_e = Limit(Point.merge(limit_a.position, pi_i_e.position), size=(1, 1)) @@ -116,14 +136,15 @@ def cc_ext_ex_dc(start, control_task): Connection.connect(pi_i_e.output_right, limit_e.input_left) # Pulse width modulation block of the i_e path - pwm_e = Box(Point.merge(pwm_a.position, pi_i_e.position), size=(1.2, 0.8), text='PWM') + pwm_e = Box(Point.merge(pwm_a.position, pi_i_e.position), size=(1.2, 0.8), text="PWM") # Connetion between the e-limit and e-pwm block - Connection.connect(limit_e.output_right, pwm_e.input_left, text=r'$u^{*}_{\mathrm{e}}$', distance_y=0.25) + Connection.connect(limit_e.output_right, pwm_e.input_left, text=r"$u^{*}_{\mathrm{e}}$", distance_y=0.25) start = pwm_e.position # starting point of the next block - outputs = dict(u_e=pwm_e.output_right[0], u_a=pwm_a.output_right[0]) # Outputs of the stage - connections = dict() # Connections + outputs = dict(u_e=pwm_e.output_right[0], u_a=pwm_a.output_right[0]) # Outputs of the stage + connections = dict() # Connections return start, inputs, outputs, connect_to_lines, connections + return cc_ext_ex_dc diff --git a/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_ops.py b/src/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_ops.py similarity index 77% rename from gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_ops.py rename to src/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_ops.py index 9f5eb634..65a50914 100644 --- a/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_ops.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_ops.py @@ -14,21 +14,20 @@ def ext_ex_dc_ops(start, control_task): """ # space to the previous block - inp = start.add_x(1) if control_task == 'TC' else start.add_x(1.5) + inp = start.add_x(1) if control_task == "TC" else start.add_x(1.5) # Connector at the input - Circle(inp, radius=0.05, fill='black') + Circle(inp, radius=0.05, fill="black") - if control_task == 'TC': + if control_task == "TC": # Connetion at the input - Connection.connect(start, inp, text=r'$T^{*}$', text_align='left', - text_position='start', arrow=False) + Connection.connect(start, inp, text=r"$T^{*}$", text_align="left", text_position="start", arrow=False) # Calculate absolute value - box_abs = Box(inp.add(1, 0.6), size=(0.8, 0.8), text=r'|x|') + box_abs = Box(inp.add(1, 0.6), size=(0.8, 0.8), text=r"|x|") # Connect the input and the previous block - Connection.connect(inp, box_abs.input_left[0], start_direction='north') + Connection.connect(inp, box_abs.input_left[0], start_direction="north") # Multiply by motor parameters multiply = Multiply(box_abs.position.add_x(1.5), size=(0.8, 0.8), inputs=dict(left=1, top=1)) @@ -37,14 +36,18 @@ def ext_ex_dc_ops(start, control_task): Connection.connect(box_abs.output_right, multiply.input_left) # Block of motor parameters - box_rl = Box(multiply.position.add_y(1.2), size=(1.5, 0.8), - text=r"$\sqrt{\frac{R_{\mathrm{a}}}{R_{\mathrm{e}}}}L'_{\mathrm{e}}$", outputs=dict(bottom=1)) + box_rl = Box( + multiply.position.add_y(1.2), + size=(1.5, 0.8), + text=r"$\sqrt{\frac{R_{\mathrm{a}}}{R_{\mathrm{e}}}}L'_{\mathrm{e}}$", + outputs=dict(bottom=1), + ) # Connect the motor parameters block with the multiply block Connection.connect(box_rl.output_bottom, multiply.input_top) # Square root of the product - box_sqrt = Box(multiply.position.add_x(1.5), size=(0.8, 0.8), text=r'$\sqrt{x}$') + box_sqrt = Box(multiply.position.add_x(1.5), size=(0.8, 0.8), text=r"$\sqrt{x}$") # Connection between multiplication and squareroot Connection.connect(multiply.output_right, box_sqrt.input_left) @@ -53,7 +56,7 @@ def ext_ex_dc_ops(start, control_task): divide_1 = Divide(multiply.position.sub_y(1.5), size=(0.8, 1.2), input_space=0.6) # Conncet the input and division block - Connection.connect(inp, divide_1.input_left[0], start_direction='south') + Connection.connect(inp, divide_1.input_left[0], start_direction="south") # L_e prime block box_le = Box(divide_1.input_left[1].sub_x(1), size=(0.8, 0.8), text=r"$L'_{\mathrm{e}}$") @@ -83,10 +86,10 @@ def ext_ex_dc_ops(start, control_task): # Connection between the division and limit block Connection.connect(divide_2.output_right, limit_i_a.input_left) - inputs = dict(t_ref=[inp, dict(text=r'$T^{*}$', arrow=False)]) # starting point of the next block - outputs = dict(i_e_ref=limit_i_e.output_right[0], i_a_ref=limit_i_a.output_right[0]) # Inputs of the stage - connect_to_lines = dict() # Outputs of the stage - connections = dict() # Connections to other lines - start = limit_i_e.output_right[0] # Connections + inputs = dict(t_ref=[inp, dict(text=r"$T^{*}$", arrow=False)]) # starting point of the next block + outputs = dict(i_e_ref=limit_i_e.output_right[0], i_a_ref=limit_i_a.output_right[0]) # Inputs of the stage + connect_to_lines = dict() # Outputs of the stage + connections = dict() # Connections to other lines + start = limit_i_e.output_right[0] # Connections return start, inputs, outputs, connect_to_lines, connections diff --git a/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_output.py b/src/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_output.py similarity index 56% rename from gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_output.py rename to src/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_output.py index 2a1a921a..e0d5e003 100644 --- a/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_output.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_output.py @@ -23,11 +23,12 @@ def _ext_ex_dc_output(start, control_task): """ # Converter with DC input voltage and four outputs - converter = DcConverter(start.add(2.5, -0.6), size=1.7, input_number=2, input_space=1.2, output_number=4, - output_space=0.25) + converter = DcConverter( + start.add(2.5, -0.6), size=1.7, input_number=2, input_space=1.2, output_number=4, output_space=0.25 + ) # Ext Ex DC motor block - dc_ext_ex = DcExtExMotor(converter.position.sub_y(3), size=1.2, input='top', output='left') + dc_ext_ex = DcExtExMotor(converter.position.sub_y(3), size=1.2, input="top", output="left") # Connections between converter and motor block con_conv_a = Connection.connect(converter.output_bottom[0], dc_ext_ex.input_top[0], arrow=False) @@ -36,28 +37,41 @@ def _ext_ex_dc_output(start, control_task): Connection.connect(converter.output_bottom[3], dc_ext_ex.input_top[3], arrow=False) # Connection to the i_e output - con_e = Connection.connect_to_line(con_conv_e, start.sub_y(2), arrow=False, radius=0.1, fill=False, - text=r'$i_{\mathrm{e}}$', distance_y=0.25) + con_e = Connection.connect_to_line( + con_conv_e, start.sub_y(2), arrow=False, radius=0.1, fill=False, text=r"$i_{\mathrm{e}}$", distance_y=0.25 + ) # Connection to the i_a output - con_a = Connection.connect_to_line(con_conv_a, start.sub_y(2.5), arrow=False, radius=0.1, fill=False, - text=r'$i_{\mathrm{a}}$', distance_y=0.25, text_align='bottom', - move_text=(0.25, 0)) + con_a = Connection.connect_to_line( + con_conv_a, + start.sub_y(2.5), + arrow=False, + radius=0.1, + fill=False, + text=r"$i_{\mathrm{a}}$", + distance_y=0.25, + text_align="bottom", + move_text=(0.25, 0), + ) start = converter.position # starting point of the next block # Inputs of the stage - inputs = dict(u_e=[converter.input_left[0], dict(text=r'$\mathrm{S_e}$', distance_y=0.25)], - u_a=[converter.input_left[1], dict(text=r'$\mathrm{S_a}$', distance_y=0.25)]) - outputs = dict(i_e=con_e.end, i_a=con_a.end) # Outputs of the stage - connect_to_lines = dict() # Connections to other lines - connections = dict() # Connections + inputs = dict( + u_e=[converter.input_left[0], dict(text=r"$\mathrm{S_e}$", distance_y=0.25)], + u_a=[converter.input_left[1], dict(text=r"$\mathrm{S_a}$", distance_y=0.25)], + ) + outputs = dict(i_e=con_e.end, i_a=con_a.end) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Connections - if emf_feedforward or control_task in ['SC']: + if emf_feedforward or control_task in ["SC"]: # Connection between the motor output and the omega output of the stage - con_omega = Connection.connect(dc_ext_ex.output_left[0].sub_x(2), dc_ext_ex.output_left[0], - text=r'$\omega_{\mathrm{me}}$', arrow=False) + con_omega = Connection.connect( + dc_ext_ex.output_left[0].sub_x(2), dc_ext_ex.output_left[0], text=r"$\omega_{\mathrm{me}}$", arrow=False + ) # Add the omega output - outputs['omega'] = con_omega.end + outputs["omega"] = con_omega.end return start, inputs, outputs, connect_to_lines, connections + return _ext_ex_dc_output diff --git a/gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_cc.py b/src/gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_cc.py similarity index 53% rename from gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_cc.py rename to src/gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_cc.py index 07721f15..6881ce04 100644 --- a/gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_cc.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_cc.py @@ -23,56 +23,74 @@ def cc_perm_ex_dc(start, control_task): """ # space to the previous block - space = 1 if control_task == 'CC' else 1.5 + space = 1 if control_task == "CC" else 1.5 # Add block for the current reference and state add_current = Add(start.add_x(space)) - if control_task == 'CC': + if control_task == "CC": # Connection at the input - Connection.connect(start, add_current.input_left[0], text=r'$i^{*}$', text_align='left', - text_position='start') + Connection.connect( + start, add_current.input_left[0], text=r"$i^{*}$", text_align="left", text_position="start" + ) # PI Current Controller - pi_current = PIController(add_current.position.add_x(1.5), text='Current\nController') + pi_current = PIController(add_current.position.add_x(1.5), text="Current\nController") # Connection between the add block and pi controller Connection.connect(add_current.output_right, pi_current.input_left) - start = pi_current.position # starting point of the next block + start = pi_current.position # starting point of the next block # Inputs of the stage - inputs = dict(i_ref=[add_current.input_left[0], dict(text=r'$i^{*}$')], i=[add_current.input_bottom[0], - dict(text=r'-', move_text=(-0.2, -0.2), text_position='end', text_align='right')]) - outputs = dict(u=pi_current.output_right[0]) # Outputs of the stage - connect_to_lines = dict() # Connections to other lines - connections = dict() # Connections + inputs = dict( + i_ref=[add_current.input_left[0], dict(text=r"$i^{*}$")], + i=[ + add_current.input_bottom[0], + dict(text=r"-", move_text=(-0.2, -0.2), text_position="end", text_align="right"), + ], + ) + outputs = dict(u=pi_current.output_right[0]) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Connections if emf_feedforward: # Add block of the emf feedforward add_emf = Add(pi_current.position.add_x(2)) # Connection between pi controller and add block - Connection.connect(pi_current.output_right, add_emf.input_left, text=r'$\Delta u^{*}$') + Connection.connect(pi_current.output_right, add_emf.input_left, text=r"$\Delta u^{*}$") # Multiplication with the flux - box_psi = Box(add_emf.position.sub_y(2.5), size=(0.8, 0.8), text=r"$\Psi'_{\mathrm{e}}$", inputs=dict(bottom=1), - outputs=dict(top=1)) + box_psi = Box( + add_emf.position.sub_y(2.5), + size=(0.8, 0.8), + text=r"$\Psi'_{\mathrm{e}}$", + inputs=dict(bottom=1), + outputs=dict(top=1), + ) # Connection between the multiplication and add block - Connection.connect(box_psi.output_top, add_emf.input_bottom, text=r'$u^{0}$', text_position='end', - text_align='right', move_text=(-0.1, -0.2)) + Connection.connect( + box_psi.output_top, + add_emf.input_bottom, + text=r"$u^{0}$", + text_position="end", + text_align="right", + move_text=(-0.1, -0.2), + ) # Set the input of the emf feedforward - if control_task in ['SC']: - connect_to_lines['omega'] = [box_psi.input_bottom[0], dict(section=0)] - elif control_task in ['CC', 'TC']: - inputs['omega'] = [box_psi.input_bottom[0], dict()] + if control_task in ["SC"]: + connect_to_lines["omega"] = [box_psi.input_bottom[0], dict(section=0)] + elif control_task in ["CC", "TC"]: + inputs["omega"] = [box_psi.input_bottom[0], dict()] # Set the output of the emf feedforward - outputs['u'] = add_emf.output_right[0] + outputs["u"] = add_emf.output_right[0] # Update the position of the next block start = add_emf.position return start, inputs, outputs, connect_to_lines, connections + return cc_perm_ex_dc diff --git a/gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_ops.py b/src/gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_ops.py similarity index 71% rename from gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_ops.py rename to src/gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_ops.py index a21761ab..6fe75ff8 100644 --- a/gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_ops.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_ops.py @@ -14,7 +14,7 @@ def perm_ex_dc_ops(start, control_task): """ # space to the previous block - space = 1 if control_task == 'TC' else 2.2 + space = 1 if control_task == "TC" else 2.2 # Calculation of the current reference box_torque = Box(start.add_x(space), size=(0.8, 0.8), text=r"$\frac{1}{\Psi'_{\mathrm{e}}}$") @@ -25,15 +25,14 @@ def perm_ex_dc_ops(start, control_task): # Connection between the calculation and limit block Connection.connect(box_torque.output_right, limit.input_left) - if control_task == 'TC': + if control_task == "TC": # Connection at the input - Connection.connect(start, box_torque.input_left[0], text=r'$T^{*}$', text_align='left', - text_position='start') + Connection.connect(start, box_torque.input_left[0], text=r"$T^{*}$", text_align="left", text_position="start") start = limit.position # starting point of the next block - inputs = dict(t_ref=[box_torque.input_left[0], dict(text=r'$T^{*}$')]) # Inputs of the stage - outputs = dict(i_ref=limit.output_right[0]) # Outputs of the stage - connect_to_lines = dict() # Connections to other lines - connections = dict() # Connections + inputs = dict(t_ref=[box_torque.input_left[0], dict(text=r"$T^{*}$")]) # Inputs of the stage + outputs = dict(i_ref=limit.output_right[0]) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Connections return start, inputs, outputs, connect_to_lines, connections diff --git a/gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_output.py b/src/gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_output.py similarity index 65% rename from gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_output.py rename to src/gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_output.py index 2cd08bef..a3d26dcd 100644 --- a/gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_output.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_output.py @@ -29,7 +29,7 @@ def _perm_ex_dc_output(start, control_task): limit = Limit(start.add_x(space), size=(1, 1)) # pulse width modulation block - pwm = Box(limit.output_right[0].add_x(1.5), size=(1, 0.8), text='PWM') + pwm = Box(limit.output_right[0].add_x(1.5), size=(1, 0.8), text="PWM") # connection between limit and pwm block Connection.connect(limit.output_right, pwm.input_left) @@ -38,31 +38,38 @@ def _perm_ex_dc_output(start, control_task): converter = DcConverter(pwm.position.add_x(2), size=1.2, input_number=1) # Connection between pwm and converter block - Connection.connect(pwm.output_right, converter.input_left, text='$S$') + Connection.connect(pwm.output_right, converter.input_left, text="$S$") # Perm Ex DC motor block - dc_perm_ex = DcPermExMotor(converter.position.sub_y(3), size=1.2, input='top', output='left') + dc_perm_ex = DcPermExMotor(converter.position.sub_y(3), size=1.2, input="top", output="left") # Connections between converter and motor block - conv_motor = Connection.connect(converter.output_bottom, dc_perm_ex.input_top, text=['', r'$i$'], - text_align='right', arrow=False) + conv_motor = Connection.connect( + converter.output_bottom, dc_perm_ex.input_top, text=["", r"$i$"], text_align="right", arrow=False + ) # Connection between previous connection and current output - con_i = Connection.connect_to_line(conv_motor[0], pwm.position.sub_y(1.5), text=r'$i$', arrow=False, fill=False, - radius=0.1) + con_i = Connection.connect_to_line( + conv_motor[0], pwm.position.sub_y(1.5), text=r"$i$", arrow=False, fill=False, radius=0.1 + ) start = converter.position # starting point of the next block - inputs = dict(u=[limit.input_left[0], dict(text=r'$u^{*}$')]) # Inputs of the stage - outputs = dict(i=con_i.end) # Outputs of the stage - connect_to_lines = dict() # Connections to other lines - connections = dict() # Connections + inputs = dict(u=[limit.input_left[0], dict(text=r"$u^{*}$")]) # Inputs of the stage + outputs = dict(i=con_i.end) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Connections - if emf_feedforward or control_task in ['SC']: + if emf_feedforward or control_task in ["SC"]: # Connection between the motor output and the omega output of the stage - con_omega = Connection.connect(dc_perm_ex.output_left[0].sub_x(2), dc_perm_ex.output_left[0], - text=r'$\omega_{\mathrm{me}}$', arrow=False) + con_omega = Connection.connect( + dc_perm_ex.output_left[0].sub_x(2), + dc_perm_ex.output_left[0], + text=r"$\omega_{\mathrm{me}}$", + arrow=False, + ) # Add the omega output - outputs['omega'] = con_omega.end + outputs["omega"] = con_omega.end return start, inputs, outputs, connect_to_lines, connections + return _perm_ex_dc_output diff --git a/gem_controllers/block_diagrams/stage_blocks/pi_speed_controller.py b/src/gem_controllers/block_diagrams/stage_blocks/pi_speed_controller.py similarity index 54% rename from gem_controllers/block_diagrams/stage_blocks/pi_speed_controller.py rename to src/gem_controllers/block_diagrams/stage_blocks/pi_speed_controller.py index 4c0ca4b1..802d8fa3 100644 --- a/gem_controllers/block_diagrams/stage_blocks/pi_speed_controller.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/pi_speed_controller.py @@ -14,18 +14,19 @@ def pi_speed_controller(start, control_task): """ # space to the previous block - space = 1 if control_task == 'SC' else 1.5 + space = 1 if control_task == "SC" else 1.5 # Add block for the speed reference and state add_omega = Add(start.add_x(space)) - if control_task == 'SC': + if control_task == "SC": # Connection at the input - Connection.connect(start, add_omega.input_left[0], text=r'$\omega^{*}$', text_align='left', - text_position='start') + Connection.connect( + start, add_omega.input_left[0], text=r"$\omega^{*}$", text_align="left", text_position="start" + ) # PI Speed Controller - pi_omega = PIController(add_omega.position.add_x(1.5), text='Speed\nController') + pi_omega = PIController(add_omega.position.add_x(1.5), text="Speed\nController") # Connection between the add block and pi controller Connection.connect(add_omega.output_right, pi_omega.input_left) @@ -38,13 +39,15 @@ def pi_speed_controller(start, control_task): start = limit.position # starting point of the next block # Inputs of the stage - inputs = dict(omega_ref=[add_omega.input_left[0], dict(text=r'$\omega^{*}$')], omega=[add_omega.input_bottom[0], - dict(text=r'-', - move_text=(-0.2, -0.2), - text_position='end', - text_align='right')]) - outputs = dict(t_ref=limit.output_right[0]) # Outputs of the stage - connect_to_lines = dict() # Connections to other lines - connections = dict() # Connections + inputs = dict( + omega_ref=[add_omega.input_left[0], dict(text=r"$\omega^{*}$")], + omega=[ + add_omega.input_bottom[0], + dict(text=r"-", move_text=(-0.2, -0.2), text_position="end", text_align="right"), + ], + ) + outputs = dict(t_ref=limit.output_right[0]) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Connections return start, inputs, outputs, connect_to_lines, connections diff --git a/src/gem_controllers/block_diagrams/stage_blocks/pmsm_cc.py b/src/gem_controllers/block_diagrams/stage_blocks/pmsm_cc.py new file mode 100644 index 00000000..944e046c --- /dev/null +++ b/src/gem_controllers/block_diagrams/stage_blocks/pmsm_cc.py @@ -0,0 +1,299 @@ +from control_block_diagram.components import Point, Box, Connection, Circle +from control_block_diagram.predefined_components import ( + DqToAbcTransformation, + AbcToAlphaBetaTransformation, + AlphaBetaToDqTransformation, + Add, + PIController, + Multiply, + Limit, +) + + +def pmsm_cc(emf_feedforward): + """ + Args: + emf_feedforward: Boolean whether emf feedforward stage is included + + Returns: + Function to build the PMSM current control block + """ + + def cc_pmsm(start, control_task): + """ + Function to build the PMSM current control block + Args: + start: Starting point of the block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + # space to the previous block + space = 1 if control_task == "CC" else 1.5 + + # Add blocks for the i_sd and i_sq references and states + add_i_sd = Add(start.add_x(space)) + add_i_sq = Add(add_i_sd.position.sub(0.5, 1)) + + if control_task == "CC": + # Connections at the inputs + Connection.connect( + add_i_sd.input_left[0].sub_x(space), + add_i_sd.input_left[0], + text=r"$i^{*}_{\mathrm{sd}}$", + text_position="start", + text_align="left", + ) + Connection.connect( + add_i_sq.input_left[0].sub_x(space - 0.5), + add_i_sq.input_left[0], + text=r"$i^{*}_{\mathrm{sq}}$", + text_position="start", + text_align="left", + ) + + # PI Controllers for the d and q component + pi_i_sd = PIController( + add_i_sd.position.add_x(1.2), size=(1, 0.8), input_number=1, output_number=1, text="Current\nController" + ) + pi_i_sq = PIController( + Point.merge(pi_i_sd.position, add_i_sq.position), size=(1, 0.8), input_number=1, output_number=1 + ) + + # Connection between the add blocks and the PI Controllers + Connection.connect(add_i_sd.output_right, pi_i_sd.input_left) + Connection.connect(add_i_sq.output_right, pi_i_sq.input_left) + + # Add blocks for the EMF Feedforward + add_u_sd = Add(pi_i_sd.position.add_x(2)) + add_u_sq = Add(pi_i_sq.position.add_x(1.2)) + + # Connections between the PI Controllers and the Add blocks + Connection.connect( + pi_i_sd.output_right[0], add_u_sd.input_left[0], text=r"$\Delta u^{*}_{\mathrm{sd}}$", distance_y=0.28 + ) + Connection.connect( + pi_i_sq.output_right[0], + add_u_sq.input_left[0], + text=r"$\Delta u^{*}_{\mathrm{sq}}$", + distance_y=0.4, + move_text=(0.25, 0), + ) + + # Coordinate transformation from DQ to Abc coordinates + dq_to_abc = DqToAbcTransformation(Point.get_mid(add_u_sd.position, add_u_sq.position).add_x(2), input_space=1) + + # Connections between the add blocks and the coordinate transformation + Connection.connect( + add_u_sd.output_right[0], dq_to_abc.input_left[0], text=r"$u^{*}_{\mathrm{sd}}$", distance_y=0.28 + ) + Connection.connect( + add_u_sq.output_right[0], + dq_to_abc.input_left[1], + text=r"$u^{*}_{\mathrm{sq}}$", + distance_y=0.28, + move_text=(0.4, 0), + ) + + # Limit of the input voltages + limit = Limit( + dq_to_abc.position.add_x(1.8), + size=(1, 1.2), + inputs=dict(left=3, left_space=0.3), + outputs=dict(right=3, right_space=0.3), + ) + + # Connection between the coordinate transformation and the limit block + Connection.connect(dq_to_abc.output_right, limit.input_left) + + # Pulse width modulation block + pwm = Box( + limit.position.add_x(2.5), + size=(1.5, 1.2), + text="PWM", + inputs=dict(left=3, left_space=0.3), + outputs=dict(right=3, right_space=0.3), + ) + + # Connection between the limit and the PWM block + Connection.connect( + limit.output_right, pwm.input_left, text=[r"$u^*_{\mathrm{s a,b,c}}$", "", ""], distance_y=0.25 + ) + + # Coordinate transformation from ABC to AlphaBeta coordinates + abc_to_alpha_beta = AbcToAlphaBetaTransformation(pwm.position.sub(1, 3.5), input="right", output="left") + + # Coordinate transformation from AlphaBeta to DQ coordinates + alpha_beta_to_dq = AlphaBetaToDqTransformation( + Point.merge(dq_to_abc.position, abc_to_alpha_beta.position), input="right", output="left" + ) + + # Distance of the blocks in the EMF Feedforward path + distance = (add_u_sq.position.y - alpha_beta_to_dq.output_left[0].y) / 4 + + # Multiplications of the EMF Feedforward + multiply_u_sq = Multiply(add_u_sq.position.sub_y(distance), outputs=dict(top=1)) + multiply_u_sd = Multiply(Point.merge(add_u_sd.position, multiply_u_sq.position), outputs=dict(top=1)) + + # Connections between the multiplication and the add blocks + Connection.connect(multiply_u_sq.output_top, add_u_sq.input_bottom) + Connection.connect( + multiply_u_sd.output_top, + add_u_sd.input_bottom, + text=r"-", + text_position="end", + text_align="right", + move_text=(-0.2, -0.2), + ) + + # Add block for the permanent flux psi_p + add_psi_p = Add(multiply_u_sq.position.sub_y(distance), outputs=dict(top=1)) + + # Connections of the add block + Connection.connect(add_psi_p.output_top, multiply_u_sq.input_bottom) + Connection.connect( + add_psi_p.input_left[0].sub_x(0.3), + add_psi_p.input_left[0], + text=r"$\Psi_{\mathrm{p}}$", + text_position="start", + text_align="left", + distance_x=0.25, + ) + + # Multiplication with the inductances + box_ls_1 = Box( + add_psi_p.position.sub_y(distance), + size=(0.6, 0.6), + inputs=dict(bottom=1), + outputs=dict(top=1), + text=r"$L_{\mathrm{d}}$", + ) + box_ls_2 = Box( + Point.merge(multiply_u_sd.position, box_ls_1.position), + size=(0.6, 0.6), + inputs=dict(bottom=1), + outputs=dict(top=1), + text=r"$L_{\mathrm{q}}$", + ) + + # Connections from the multiplications + Connection.connect(box_ls_1.output_top, add_psi_p.input_bottom) + Connection.connect(multiply_u_sd.input_left[0].sub_x(0.3), multiply_u_sd.input_left[0]) + Connection.connect(box_ls_2.output_top, multiply_u_sd.input_bottom) + + # Connections between the coordinate transformation and the add blocks + con_3 = Connection.connect( + alpha_beta_to_dq.output_left[0], + add_i_sd.input_bottom[0], + text=r"-", + text_position="end", + text_align="right", + move_text=(-0.2, -0.2), + ) + con_4 = Connection.connect( + alpha_beta_to_dq.output_left[1], + add_i_sq.input_bottom[0], + text=r"-", + text_position="end", + text_align="right", + move_text=(-0.2, -0.2), + ) + + # Connections between the previous conncetions and the inductances blocks + Connection.connect_to_line(con_3, box_ls_1.input_bottom[0]) + Connection.connect_to_line(con_4, box_ls_2.input_bottom[0]) + + # Derivation of the angle + box_d_dt = Box( + Point.merge(alpha_beta_to_dq.position, start.sub_y(6.57020066645696)).sub_x(2), + size=(1, 0.8), + text=r"$\mathrm{d} / \mathrm{d}t$", + inputs=dict(right=1), + outputs=dict(left=1), + ) + + # Conncetion between the derivation and multiplication block + con_omega = Connection.connect( + box_d_dt.output_left, + multiply_u_sq.input_left, + space_y=1, + text=r"$\omega_{\mathrm{el}}$", + move_text=(0, 2), + text_align="left", + distance_x=0.3, + ) + + if control_task == "SC": + # Connector at the previous connection + Circle(con_omega[0].points[1], radius=0.05, fill="black") + + # Conncetion between the coordinate transformations + Connection.connect( + abc_to_alpha_beta.output, + alpha_beta_to_dq.input_right, + text=[r"$i_{\mathrm{s} \upalpha}$", r"$i_{\mathrm{s} \upbeta}$"], + ) + + # Add block for the advanced angle + add = Add( + Point.get_mid(dq_to_abc.position, alpha_beta_to_dq.position), + inputs=dict(bottom=1, right=1), + outputs=dict(top=1), + ) + + # Connections of the add block + Connection.connect( + alpha_beta_to_dq.output_top, + add.input_bottom, + text=r"$\varepsilon_{\mathrm{el}}$", + text_align="right", + move_text=(0, -0.1), + ) + Connection.connect(add.output_top, dq_to_abc.input_bottom) + + # Calculate the advanced angle + box_t_a = Box( + add.position.add_x(1.5), + size=(1, 0.8), + text=r"$1.5 T_{\mathrm{s}}$", + inputs=dict(right=1), + outputs=dict(left=1), + ) + + # Connections of the advanced angle block + Connection.connect(box_t_a.output, add.input_right, text=r"$\Delta \varepsilon$") + Connection.connect( + box_t_a.input[0].add_x(0.5), + box_t_a.input[0], + text=r"$\omega_{\mathrm{el}}$", + text_position="start", + text_align="right", + distance_x=0.3, + ) + + start = pwm.position # starting point of the next block + # Inputs of the stage + inputs = dict( + i_d_ref=[add_i_sd.input_left[0], dict(text=r"$i^{*}_{\mathrm{sd}}$", distance_y=0.25)], + i_q_ref=[add_i_sq.input_left[0], dict(text=r"$i^{*}_{\mathrm{sq}}$", distance_y=0.25)], + epsilon=[box_d_dt.input_right[0], dict()], + ) + outputs = dict(S=pwm.output_right, omega=con_omega[0].points[1]) # Outputs of the stage + # Connections to other lines + connect_to_line = dict( + epsilon=[ + alpha_beta_to_dq.input_bottom[0], + dict(text=r"$\varepsilon_{\mathrm{el}}$", text_position="middle", text_align="right"), + ], + i=[ + abc_to_alpha_beta.input_right, + dict(radius=0.1, fill=False, text=[r"$\mathbf{i}_{\mathrm{s a,b,c}}$", "", ""]), + ], + ) + connections = dict() # Connections + + return start, inputs, outputs, connect_to_line, connections + + return cc_pmsm diff --git a/gem_controllers/block_diagrams/stage_blocks/pmsm_ops.py b/src/gem_controllers/block_diagrams/stage_blocks/pmsm_ops.py similarity index 53% rename from gem_controllers/block_diagrams/stage_blocks/pmsm_ops.py rename to src/gem_controllers/block_diagrams/stage_blocks/pmsm_ops.py index 6e633248..378040bf 100644 --- a/gem_controllers/block_diagrams/stage_blocks/pmsm_ops.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/pmsm_ops.py @@ -4,6 +4,7 @@ class PsiOptBox(Box): """Optimal flux block""" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -12,19 +13,27 @@ def __init__(self, *args, **kwargs): by = 0.15 # Coordinate system - Connection.connect(self.bottom_left.add(self._size_x * bx, self._size_y * by), - self.bottom_right.add(-self._size_x * bx, self._size_y * by)) + Connection.connect( + self.bottom_left.add(self._size_x * bx, self._size_y * by), + self.bottom_right.add(-self._size_x * bx, self._size_y * by), + ) Connection.connect(self.bottom.add_y(self._size_y * by), self.top.sub_y(self._size_y * by)) # Graph in the coordinate system - Path([self.top_left.add(self._size_x * bx, -self._size_y * (0.1 + by)), - self.bottom.add_y(self._size_y * (0.2 + by)), - self.top_right.sub(self._size_x * bx, self._size_y * (0.1 + by))], - angles=[{'in': 180, 'out': 0}, {'in': 180, 'out': 0}], arrow=False) + Path( + [ + self.top_left.add(self._size_x * bx, -self._size_y * (0.1 + by)), + self.bottom.add_y(self._size_y * (0.2 + by)), + self.top_right.sub(self._size_x * bx, self._size_y * (0.1 + by)), + ], + angles=[{"in": 180, "out": 0}, {"in": 180, "out": 0}], + arrow=False, + ) class TMaxPsiBox(Box): """Maximum torque block""" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -38,10 +47,16 @@ def __init__(self, *args, **kwargs): Connection.connect(self.left.add_x(bx), self.right.sub_x(bx)) # Graph in the coordinate system - Path([self.left.add_x(bx), self.top_right.sub(scale * bx, scale * by)], arrow=False, - angles=[{'in': 180, 'out': 35}]) - Path([self.left.add_x(bx), self.bottom_right.add(-scale * bx, scale * by)], arrow=False, - angles=[{'in': 180, 'out': -35}]) + Path( + [self.left.add_x(bx), self.top_right.sub(scale * bx, scale * by)], + arrow=False, + angles=[{"in": 180, "out": 35}], + ) + Path( + [self.left.add_x(bx), self.bottom_right.add(-scale * bx, scale * by)], + arrow=False, + angles=[{"in": 180, "out": -35}], + ) def pmsm_ops(start, control_task): @@ -55,50 +70,55 @@ def pmsm_ops(start, control_task): endpoint, inputs, outputs, connection to other lines, connections """ - if control_task == 'TC': + if control_task == "TC": # Connection at the input - Connection.connect(start, start.add_x(1), text='$T^{*}$', text_position='start', text_align='left', arrow=False) + Connection.connect(start, start.add_x(1), text="$T^{*}$", text_position="start", text_align="left", arrow=False) # Limit the torque reference box_limit = Limit(start.add_x(6), inputs=dict(left=1, bottom=1), size=(1, 1)) # Calculate the optimal flux box_psi_opt = PsiOptBox(start.add(2, -1.7), size=(1.2, 1)) - Text(position=box_psi_opt.top.add_y(0.25), text=r'$\Psi^{*}_{\mathrm{opt}}(T^{*})$') + Text(position=box_psi_opt.top.add_y(0.25), text=r"$\Psi^{*}_{\mathrm{opt}}(T^{*})$") # Minimum block - box_min = Box(box_psi_opt.position.add(1.5, -1.3), inputs=dict(left=2, left_space=0.5), size=(0.8, 1), text='min') + box_min = Box(box_psi_opt.position.add(1.5, -1.3), inputs=dict(left=2, left_space=0.5), size=(0.8, 1), text="min") # Calculate the maximum torque box_t_max = TMaxPsiBox(box_psi_opt.position.add_x(3.1), size=(1.2, 1)) - Text(position=box_t_max.top.add_y(0.25), text=r'$T_{\mathrm{max}}(\Psi)$') + Text(position=box_t_max.top.add_y(0.25), text=r"$T_{\mathrm{max}}(\Psi)$") # Connect the maximum torque and limit block con_torque = Connection.connect(start.add_x(1), box_limit.input_left[0]) # Connection between the input and the optimum flux block - Connection.connect(start.add_x(1), box_psi_opt.input_left[0], start_direction='south') - Circle(start.add_x(1), radius=0.05, fill='black') + Connection.connect(start.add_x(1), box_psi_opt.input_left[0], start_direction="south") + Circle(start.add_x(1), radius=0.05, fill="black") # Connection between the optimum flux and minimum block Connection.connect(box_psi_opt.output_right[0], box_min.input_left[0]) # Conncetion between the minimum and maximum torque block - Connection.connect(box_min.output_right[0].add_x(0.3), box_t_max.input_left[0], start_direction='north') + Connection.connect(box_min.output_right[0].add_x(0.3), box_t_max.input_left[0], start_direction="north") # Circle at the connection - Circle(box_min.output_right[0].add_x(0.3), radius=0.05, fill='black') + Circle(box_min.output_right[0].add_x(0.3), radius=0.05, fill="black") # Conncetion between the maximum torque and torque limit block Connection.connect(box_t_max.output_right[0], box_limit.input_bottom[0]) # Optimal operation point function block - box_f_psi_t = Box(box_limit.position.add(2.2, -1.5), size=(1.3, 3.5), inputs=dict(left=2, left_space=3), - outputs=dict(right=3, right_space=1), text=r'\textbf{f}($\Psi$, $T$)') + box_f_psi_t = Box( + box_limit.position.add(2.2, -1.5), + size=(1.3, 3.5), + inputs=dict(left=2, left_space=3), + outputs=dict(right=3, right_space=1), + text=r"\textbf{f}($\Psi$, $T$)", + ) # Conncetions to the optimal opertion point function block - Connection.connect(box_min.output_right[0], box_f_psi_t.input_left[1], text=r'$\Psi^{*}_{\mathrm{lim}}$') - Connection.connect(box_limit.output_right[0], box_f_psi_t.input_left[0], text=r'$T^{*}_{\mathrm{lim}}$') + Connection.connect(box_min.output_right[0], box_f_psi_t.input_left[1], text=r"$\Psi^{*}_{\mathrm{lim}}$") + Connection.connect(box_limit.output_right[0], box_f_psi_t.input_left[0], text=r"$T^{*}_{\mathrm{lim}}$") # Moulation Controller @@ -106,72 +126,104 @@ def pmsm_ops(start, control_task): add_psi = Add(box_min.input_left[1].sub_x(1)) # Connection between the add and minimum block - Connection.connect(add_psi.output_right[0], box_min.input_left[1], text=r'$\Psi_{\mathrm{lim}}$', text_align='top', - distance_y=0.25) + Connection.connect( + add_psi.output_right[0], box_min.input_left[1], text=r"$\Psi_{\mathrm{lim}}$", text_align="top", distance_y=0.25 + ) # Limit the delta flux limit_modulation = Limit(add_psi.input_left[0].sub_x(1.5), size=(1, 1)) # I Controller of the modulation controller - i_controller = IController(limit_modulation.input_left[0].sub_x(1), size=(1.2, 1), text='Modulation\nController') + i_controller = IController(limit_modulation.input_left[0].sub_x(1), size=(1.2, 1), text="Modulation\nController") # Connections of the limit block Connection.connect(i_controller.output_right, limit_modulation.input_left) - Connection.connect(limit_modulation.output_right[0], add_psi.input_left[0], text=r'$\Delta \Psi$', distance_y=0.25) + Connection.connect(limit_modulation.output_right[0], add_psi.input_left[0], text=r"$\Delta \Psi$", distance_y=0.25) # Add block of the modulation controller add_a = Add(i_controller.position.sub_x(1.5)) # Maximum modulation - box_a_max = Box(add_a.input_left[0].sub_x(1.3), size=(1.5, 0.8), text=r'$a_{\mathrm{max}} \cdot k$') + box_a_max = Box(add_a.input_left[0].sub_x(1.3), size=(1.5, 0.8), text=r"$a_{\mathrm{max}} \cdot k$") # Building the absolute modulation - box_abs = Box(add_a.position.sub_y(1.2), size=(0.8, 0.8), text=r'|\textbf{x}|', inputs=dict(bottom=1), - outputs=dict(top=1)) + box_abs = Box( + add_a.position.sub_y(1.2), size=(0.8, 0.8), text=r"|\textbf{x}|", inputs=dict(bottom=1), outputs=dict(top=1) + ) # Conncetions of the add block - Connection.connect(box_a_max.output_right[0], add_a.input_left[0], text='$a^{*}$') + Connection.connect(box_a_max.output_right[0], add_a.input_left[0], text="$a^{*}$") Connection.connect(add_a.output_right[0], i_controller.input_left[0]) - con_a = Connection.connect(box_abs.output_top[0], add_a.input_bottom[0], text='$-$', text_align='right', - text_position='end', move_text=(-0.1, -0.2)) + con_a = Connection.connect( + box_abs.output_top[0], + add_a.input_bottom[0], + text="$-$", + text_align="right", + text_position="end", + move_text=(-0.1, -0.2), + ) # Additional text at the connection - Text(position=Point.get_mid(*con_a.points).sub_x(0.25), text='$a$') + Text(position=Point.get_mid(*con_a.points).sub_x(0.25), text="$a$") # divide the actual voltage by the dc voltage - div_a = Divide(box_abs.input_bottom[0].sub_y(1), size=(1, 0.5), inputs='bottom', input_space=0.5) + div_a = Divide(box_abs.input_bottom[0].sub_y(1), size=(1, 0.5), inputs="bottom", input_space=0.5) # Conncetions of the divide block - Connection.connect(div_a.input_bottom[0].sub_y(0.7), div_a.input_bottom[0], text=r'$\mathbf{u^{*}_{\mathrm{dq}}}$', - text_position='start', text_align='bottom') - Connection.connect(div_a.input_bottom[1].sub_y(0.7), div_a.input_bottom[1], - text=r'$\frac{u_{\mathrm{\mbox{\fontsize{3}{4}\selectfont DC}}}}{2}$', text_position='start', - text_align='bottom') + Connection.connect( + div_a.input_bottom[0].sub_y(0.7), + div_a.input_bottom[0], + text=r"$\mathbf{u^{*}_{\mathrm{dq}}}$", + text_position="start", + text_align="bottom", + ) + Connection.connect( + div_a.input_bottom[1].sub_y(0.7), + div_a.input_bottom[1], + text=r"$\frac{u_{\mathrm{\mbox{\fontsize{3}{4}\selectfont DC}}}}{2}$", + text_position="start", + text_align="bottom", + ) Connection.connect(div_a.output_top[0], box_abs.input_bottom[0]) # Divide the dc voltage by the electrical speed - div_psi = Divide(add_psi.input_bottom[0].sub_y(2), size=(1, 0.5), inputs='bottom', input_space=0.5) + div_psi = Divide(add_psi.input_bottom[0].sub_y(2), size=(1, 0.5), inputs="bottom", input_space=0.5) # Connections of the divide block - Connection.connect(div_psi.output_top[0], add_psi.input_bottom[0], text=r'$\Psi_{\mathrm{max}}$', - text_align='right', distance_x=0.5) - Connection.connect(div_psi.input_bottom[0].sub_y(0.7), div_psi.input_bottom[0], - text=r'$\frac{u_{\mathrm{\mbox{\fontsize{3}{4}\selectfont DC}}}}{\sqrt{3}}$', - text_position='start', text_align='bottom') + Connection.connect( + div_psi.output_top[0], + add_psi.input_bottom[0], + text=r"$\Psi_{\mathrm{max}}$", + text_align="right", + distance_x=0.5, + ) + Connection.connect( + div_psi.input_bottom[0].sub_y(0.7), + div_psi.input_bottom[0], + text=r"$\frac{u_{\mathrm{\mbox{\fontsize{3}{4}\selectfont DC}}}}{\sqrt{3}}$", + text_position="start", + text_align="bottom", + ) # Limit of the current reference values - limit = Limit(Point.get_mid(box_f_psi_t.output_right[0], box_f_psi_t.output_right[1]).add_x(1), size=(1, 1.5), - inputs=dict(left=2, left_space=1), outputs=dict(right=2, right_space=1)) + limit = Limit( + Point.get_mid(box_f_psi_t.output_right[0], box_f_psi_t.output_right[1]).add_x(1), + size=(1, 1.5), + inputs=dict(left=2, left_space=1), + outputs=dict(right=2, right_space=1), + ) # Connections between the optimal opertion point function and limit block Connection.connect(box_f_psi_t.output_right, limit.input_left) # Inputs of the stage - inputs = dict(t_ref=[con_torque.start, dict(arrow=False, text=r'$T^{*}$')], - omega=[div_psi.input_bottom[1], dict(text=r'$\omega_{\mathrm{el}}$', move_text=(3, 0))]) - outputs = dict(i_d_ref=limit.output_right[0], i_q_ref=limit.output_right[1]) # Outputs of the stage - connect_to_lines = dict() # Connections to other lines - connections = dict() # Connections of the stage - start = limit.output_right[0] # Starting point of the next stage + inputs = dict( + t_ref=[con_torque.start, dict(arrow=False, text=r"$T^{*}$")], + omega=[div_psi.input_bottom[1], dict(text=r"$\omega_{\mathrm{el}}$", move_text=(3, 0))], + ) + outputs = dict(i_d_ref=limit.output_right[0], i_q_ref=limit.output_right[1]) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Connections of the stage + start = limit.output_right[0] # Starting point of the next stage return start, inputs, outputs, connect_to_lines, connections diff --git a/gem_controllers/block_diagrams/stage_blocks/pmsm_output.py b/src/gem_controllers/block_diagrams/stage_blocks/pmsm_output.py similarity index 75% rename from gem_controllers/block_diagrams/stage_blocks/pmsm_output.py rename to src/gem_controllers/block_diagrams/stage_blocks/pmsm_output.py index 75e11f81..ebcbe321 100644 --- a/gem_controllers/block_diagrams/stage_blocks/pmsm_output.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/pmsm_output.py @@ -26,17 +26,17 @@ def _pmsm_output(start, control_task): converter = DcConverter(start.add_x(2.7), input_number=3, input_space=0.3, output_number=3) # PMSM block - pmsm = PMSM(converter.position.sub_y(5), size=1.3, input='top') + pmsm = PMSM(converter.position.sub_y(5), size=1.3, input="top") # Connection between the converter and the motor block con_1 = Connection.connect(converter.output_bottom, pmsm.input_top, arrow=False) - start = pmsm.position # starting point of the next block + start = pmsm.position # starting point of the next block # Inputs of the stage - inputs = dict(S=[converter.input_left, dict(text=[r'$\mathbf{S}_{\mathrm{a,b,c}}$', '', ''], distance_y=0.25)]) - outputs = dict(epsilon=pmsm.output_left[0]) # Outputs of the stage - connect_to_lines = dict() # Connections to other lines - connections = dict(i=con_1) # Connections + inputs = dict(S=[converter.input_left, dict(text=[r"$\mathbf{S}_{\mathrm{a,b,c}}$", "", ""], distance_y=0.25)]) + outputs = dict(epsilon=pmsm.output_left[0]) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict(i=con_1) # Connections return start, inputs, outputs, connect_to_lines, connections diff --git a/gem_controllers/block_diagrams/stage_blocks/pmsm_sc.py b/src/gem_controllers/block_diagrams/stage_blocks/pmsm_sc.py similarity index 63% rename from gem_controllers/block_diagrams/stage_blocks/pmsm_sc.py rename to src/gem_controllers/block_diagrams/stage_blocks/pmsm_sc.py index c16eacf3..05123379 100644 --- a/gem_controllers/block_diagrams/stage_blocks/pmsm_sc.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/pmsm_sc.py @@ -14,22 +14,28 @@ def pmsm_speed_controller(start, control_task): """ # space to the previous block - space = 1 if control_task == 'SC' else 1.5 + space = 1 if control_task == "SC" else 1.5 # Add block for the speed reference and state add_omega = Add(start.add_x(space)) # Connection between the mechanical speed input and the add block - Connection.connect(add_omega.input_bottom[0].sub_y(1), add_omega.input_bottom[0], text=r'$\omega_{\mathrm{me}}$', - text_align='bottom', text_position='start') - - if control_task == 'SC': + Connection.connect( + add_omega.input_bottom[0].sub_y(1), + add_omega.input_bottom[0], + text=r"$\omega_{\mathrm{me}}$", + text_align="bottom", + text_position="start", + ) + + if control_task == "SC": # Connection at the input - Connection.connect(start, add_omega.input_left[0], text=r'$\omega^{*}$', text_align='left', - text_position='start') + Connection.connect( + start, add_omega.input_left[0], text=r"$\omega^{*}$", text_align="left", text_position="start" + ) # PI Speed Controller - pi_omega = PIController(add_omega.position.add_x(1.5), text='Speed\nController') + pi_omega = PIController(add_omega.position.add_x(1.5), text="Speed\nController") # Connection between the add block and pi controller Connection.connect(add_omega.output_right, pi_omega.input_left) @@ -40,10 +46,10 @@ def pmsm_speed_controller(start, control_task): # Connection between the pi controller and the limit block Connection.connect(pi_omega.output_right[0], limit.input_left[0]) - start = limit.output_right[0] # starting point of the next block - inputs = dict(omega_ref=[add_omega.input_left[0], dict(text=r'$\omega^{*}$')]) # Inputs of the stage - outputs = dict(t_ref=limit.output_right[0]) # Outputs of the stage - connect_to_lines = dict() # Connections to other lines - connections = dict() # Conncetions of the stage + start = limit.output_right[0] # starting point of the next block + inputs = dict(omega_ref=[add_omega.input_left[0], dict(text=r"$\omega^{*}$")]) # Inputs of the stage + outputs = dict(t_ref=limit.output_right[0]) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Conncetions of the stage return start, inputs, outputs, connect_to_lines, connections diff --git a/src/gem_controllers/block_diagrams/stage_blocks/scim_cc.py b/src/gem_controllers/block_diagrams/stage_blocks/scim_cc.py new file mode 100644 index 00000000..85b49127 --- /dev/null +++ b/src/gem_controllers/block_diagrams/stage_blocks/scim_cc.py @@ -0,0 +1,256 @@ +from control_block_diagram.components import Box, Connection, Text, Point, Circle +from control_block_diagram.predefined_components import ( + Add, + PIController, + Limit, + DqToAbcTransformation, + AbcToAlphaBetaTransformation, + AlphaBetaToDqTransformation, +) + + +def scim_cc(emf_feedforward): + """ + Args: + emf_feedforward: Boolean whether emf feedforward stage is included + + Returns: + Function to build the SCIM current control block + """ + + def cc_scim(start, control_task): + """ + Function to build the SCIM current control block + Args: + start: Starting Point of the Block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + # space to the previous block + space = 1 if control_task == "CC" else 1.5 + + # Add blocks for the i_sd and i_sq references and states + add_i_sd = Add(start.add_x(space)) + add_i_sq = Add(add_i_sd.position.sub(0.5, 1)) + + if control_task == "CC": + # Connections at the inputs + Connection.connect( + add_i_sd.input_left[0].sub_x(space), + add_i_sd.input_left[0], + text=r"$i^{*}_{\mathrm{sd}}$", + text_position="start", + text_align="left", + ) + Connection.connect( + add_i_sq.input_left[0].sub_x(space - 0.5), + add_i_sq.input_left[0], + text=r"$i^{*}_{\mathrm{sq}}$", + text_position="start", + text_align="left", + ) + + # PI Controllers for the d and q component + pi_i_sd = PIController( + add_i_sd.position.add_x(1.2), size=(1, 0.8), input_number=1, output_number=1, text="Current\nController" + ) + pi_i_sq = PIController( + Point.merge(pi_i_sd.position, add_i_sq.position), size=(1, 0.8), input_number=1, output_number=1 + ) + + # Connection between the add blocks and the PI Controllers + Connection.connect(add_i_sd.output_right, pi_i_sd.input_left) + Connection.connect(add_i_sq.output_right, pi_i_sq.input_left) + + # Add blocks for the EMF Feedforward + add_u_sd = Add(pi_i_sd.position.add_x(2)) + add_u_sq = Add(pi_i_sq.position.add_x(1.2)) + + # Connections between the PI Controllers and the Add blocks + Connection.connect( + pi_i_sd.output_right[0], add_u_sd.input_left[0], text=r"$\Delta u^{*}_{\mathrm{sd}}$", distance_y=0.28 + ) + Connection.connect( + pi_i_sq.output_right[0], + add_u_sq.input_left[0], + text=r"$\Delta u^{*}_{\mathrm{sq}}$", + distance_y=0.4, + move_text=(0.25, 0), + ) + + # Coordinate transformation from DQ to Abc coordinates + dq_to_abc = DqToAbcTransformation(Point.get_mid(add_u_sd.position, add_u_sq.position).add_x(2), input_space=1) + + # Connections between the add blocks and the coordinate transformation + Connection.connect( + add_u_sd.output_right[0], dq_to_abc.input_left[0], text=r"$u^{*}_{\mathrm{sd}}$", distance_y=0.28 + ) + Connection.connect( + add_u_sq.output_right[0], + dq_to_abc.input_left[1], + text=r"$u^{*}_{\mathrm{sq}}$", + distance_y=0.28, + move_text=(0.4, 0), + ) + + # Limit of the input voltages + limit = Limit( + dq_to_abc.position.add_x(2.2), + size=(1.5, 1.5), + inputs=dict(left=3, left_space=0.3), + outputs=dict(right=3, right_space=0.3), + ) + + # Connection between the coordinate transformation and the limit block + Connection.connect(dq_to_abc.output_right, limit.input_left) + + # Pulse width modulation block + pwm = Box( + limit.position.add_x(2.8), + size=(1.5, 1.2), + text="PWM", + inputs=dict(left=3, left_space=0.3), + outputs=dict(right=3, right_space=0.3), + ) + + # Connection between the limit and the PWM block + Connection.connect( + limit.output_right, pwm.input_left, text=[r"$u^*_{\mathrm{s a,b,c}}$", "", ""], distance_y=0.25 + ) + + # Coordinate transformation from ABC to AlphaBeta coordinates + abc_to_alpha_beta = AbcToAlphaBetaTransformation(pwm.position.sub(1, 3), input="right", output="left") + + # Flux observer + observer = Box( + abc_to_alpha_beta.position.sub(0.5, 2.2), + size=(2, 1), + text="Flux Observer", + inputs=dict(right=1, top=2, top_space=1.2), + outputs=dict(left=2), + ) + + # Connection of the flux observer + con_omega = Connection.connect(observer.input_right[0].add_x(1), observer.input_right[0]) + Connection.connect( + observer.input_top[0].add_y(0.4), + observer.input_top[0], + text=r"$\mathbf{i}_{\mathrm{s a,b,c}}$", + text_position="start", + ) + Connection.connect( + observer.input_top[1].add_y(0.4), + observer.input_top[1], + text=r"$\mathbf{u}_{\mathrm{s a,b,c}}$", + text_position="start", + move_text=(0, -0.05), + ) + + # Coordinate transformation from AlphaBeta to DQ coordinates + alpha_beta_to_dq = AlphaBetaToDqTransformation( + Point.merge(dq_to_abc.position, abc_to_alpha_beta.position), input="right", output="left" + ) + + # Connections between the coordinate transformation and the add blocks + con_i_sd = Connection.connect( + alpha_beta_to_dq.output_left[0], + add_i_sd.input_bottom[0], + text="-", + text_position="end", + text_align="right", + move_text=(-0.2, -0.2), + ) + con_i_sq = Connection.connect( + alpha_beta_to_dq.output_left[1], + add_i_sq.input_bottom[0], + text="-", + text_position="end", + text_align="right", + move_text=(-0.2, -0.2), + ) + + # Texts at the previous connections + Text(position=con_i_sd.start.add(-0.25, 0.25), text=r"$i_{\mathrm{sd}}$") + Text(position=con_i_sq.start.add(-0.25, 0.25), text=r"$i_{\mathrm{sq}}$") + + # Connection between the flux observer and the coordinate transformation + Connection.connect(observer.output_left[0], alpha_beta_to_dq.input_bottom[0]) + + # Texts at the outputs of the flux observer + Text(position=observer.output_left[0].add(-0.4, 0.3), text=r"$\angle \hat{\underline{\Psi}}_{\mathrm{r}}$") + Text(position=observer.output_left[1].add(-0.4, -0.3), text=r"$\hat{\Psi}_{\mathrm{r}}$") + + # Connections between the coordinate transformations + Connection.connect( + abc_to_alpha_beta.output, + alpha_beta_to_dq.input_right, + text=[r"$i_{\mathrm{s} \upalpha}$", r"$i_{\mathrm{s} \upbeta}$"], + ) + Connection.connect( + alpha_beta_to_dq.output_top, + dq_to_abc.input_bottom, + text=r"$\angle \hat{\underline{\Psi}}_r$", + text_align="right", + ) + + # Feedforward block + feedforward = Box( + Point.get_mid(add_u_sd.position, add_u_sq.position).sub_y(1.6), + size=(2, 0.8), + text="feedforward", + inputs=dict(bottom=4, bottom_space=0.3), + outputs=dict(top=2, top_space=0.8), + ) + + # Connections of the feedforward block + con_omega_2 = Connection.connect( + feedforward.input_bottom[0].add(7, -4.0702), + feedforward.input_bottom[0], + text=r"$\omega_{\mathrm{me}}$", + text_position="start", + move_text=(-1, -0.1), + ) + Connection.connect_to_line(con_omega_2, con_omega.start, arrow=False) + Connection.connect(feedforward.output_top[0], add_u_sq.input_bottom[0]) + Connection.connect(feedforward.output_top[1], add_u_sd.input_bottom[0]) + con_psi_r = Connection.connect(observer.output_left[1], feedforward.input_bottom[1]) + Connection.connect_to_line(con_i_sd, feedforward.input_bottom[2]) + Connection.connect_to_line(con_i_sq, feedforward.input_bottom[3]) + + if control_task in ["TC", "SC"]: + # Circle at the connection start + Circle(con_psi_r.points[1], radius=0.05, draw="black", fill="black") + if control_task == "SC": + # Circle at the connection start + Circle(con_omega_2.points[1], radius=0.05, draw="black", fill="black") + + start = pwm.position # starting point of the next block + # Inputs of the stage + inputs = dict( + i_d_ref=[ + add_i_sd.input_left[0], + dict(text=r"$i^{*}_{\mathrm{sd}}$", distance_y=0.3, text_position="end", move_text=(-0.85, 0)), + ], + i_q_ref=[ + add_i_sq.input_left[0], + dict(text=r"$i^{*}_{\mathrm{sq}}$", distance_y=0.3, text_position="end", move_text=(-0.35, 0)), + ], + omega=[con_omega_2.start, dict(arrow=False)], + ) + # Outputs of the stage + outputs = dict(S=pwm.output_right, psi_r=con_psi_r.points[1], omega_me=con_omega_2.points[1]) + # Connections to other lines + connect_to_line = dict( + i=[ + abc_to_alpha_beta.input_right, + dict(radius=0.1, fill=False, text=[r"$\mathbf{i}_{\mathrm{s a,b,c}}$", "", ""]), + ] + ) + connections = dict() # Connections + + return start, inputs, outputs, connect_to_line, connections + + return cc_scim diff --git a/gem_controllers/block_diagrams/stage_blocks/scim_ops.py b/src/gem_controllers/block_diagrams/stage_blocks/scim_ops.py similarity index 50% rename from gem_controllers/block_diagrams/stage_blocks/scim_ops.py rename to src/gem_controllers/block_diagrams/stage_blocks/scim_ops.py index 2cbdbc4c..830bb209 100644 --- a/gem_controllers/block_diagrams/stage_blocks/scim_ops.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/scim_ops.py @@ -4,6 +4,7 @@ class PsiOptBox(Box): """Optimal flux block""" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -12,19 +13,27 @@ def __init__(self, *args, **kwargs): by = 0.15 # Coordinate system - Connection.connect(self.bottom_left.add(self._size_x * bx, self._size_y * by), - self.bottom_right.add(-self._size_x * bx, self._size_y * by)) + Connection.connect( + self.bottom_left.add(self._size_x * bx, self._size_y * by), + self.bottom_right.add(-self._size_x * bx, self._size_y * by), + ) Connection.connect(self.bottom.add_y(self._size_y * by), self.top.sub_y(self._size_y * by)) # Graph in the coordinate system - Path([self.top_left.add(self._size_x * bx, -self._size_y * (0.1 + by)), - self.bottom.add_y(self._size_y * by), - self.top_right.sub(self._size_x * bx, self._size_y * (0.1 + by))], - angles=[{'in': 110, 'out': -20}, {'in': 200, 'out': 70}], arrow=False) + Path( + [ + self.top_left.add(self._size_x * bx, -self._size_y * (0.1 + by)), + self.bottom.add_y(self._size_y * by), + self.top_right.sub(self._size_x * bx, self._size_y * (0.1 + by)), + ], + angles=[{"in": 110, "out": -20}, {"in": 200, "out": 70}], + arrow=False, + ) class TMaxPsiBox(Box): """Maximum torque block""" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -38,10 +47,16 @@ def __init__(self, *args, **kwargs): Connection.connect(self.left.add_x(bx), self.right.sub_x(bx)) # Graph in the coordinate system - Path([self.left.add_x(bx), self.top_right.sub(scale * bx, scale * by)], arrow=False, - angles=[{'in': 180, 'out': 35}]) - Path([self.left.add_x(bx), self.bottom_right.add(-scale * bx, scale * by)], arrow=False, - angles=[{'in': 180, 'out': -35}]) + Path( + [self.left.add_x(bx), self.top_right.sub(scale * bx, scale * by)], + arrow=False, + angles=[{"in": 180, "out": 35}], + ) + Path( + [self.left.add_x(bx), self.bottom_right.add(-scale * bx, scale * by)], + arrow=False, + angles=[{"in": 180, "out": -35}], + ) def scim_ops(start, control_task): @@ -55,15 +70,16 @@ def scim_ops(start, control_task): endpoint, inputs, outputs, connection to other lines, connections """ - start_add = 0 # distance to the start + start_add = 0 # distance to the start # Torque Controller - if control_task == 'TC': - start_add = 1 # distance to the start + if control_task == "TC": + start_add = 1 # distance to the start # Connection at the input - Connection.connect(start, start.add_x(start_add), text='$T^{*}$', text_position='start', text_align='left', - arrow=False) + Connection.connect( + start, start.add_x(start_add), text="$T^{*}$", text_position="start", text_align="left", arrow=False + ) # Limit the torque reference box_limit = Limit(start.add_x(start_add + 5), size=(1, 1), inputs=dict(left=1, top=1)) @@ -73,49 +89,59 @@ def scim_ops(start, control_task): # Optimal flux block with text box_psi_opt = PsiOptBox(start.add(start_add + 1, 1.7), size=(1.2, 1)) - Text(position=box_psi_opt.bottom.sub_y(0.3), text=r'$\Psi^{*}_{\mathrm{opt}}(T^{*})$') + Text(position=box_psi_opt.bottom.sub_y(0.3), text=r"$\Psi^{*}_{\mathrm{opt}}(T^{*})$") # Minimum flux block - box_min = Box(box_psi_opt.position.add(1.5, 1.3), inputs=dict(left=2, left_space=0.5), size=(0.8, 1), text='min') + box_min = Box(box_psi_opt.position.add(1.5, 1.3), inputs=dict(left=2, left_space=0.5), size=(0.8, 1), text="min") # Maximum torque block with text box_t_max = TMaxPsiBox(box_psi_opt.position.add_x(3.1), size=(1.2, 1)) - Text(position=box_t_max.bottom.sub_y(0.3), text=r'$T_{\mathrm{max}}(\Psi)$') + Text(position=box_t_max.bottom.sub_y(0.3), text=r"$T_{\mathrm{max}}(\Psi)$") # Connection between the start and the optimal flux block - Connection.connect(start.add_x(start_add), box_psi_opt.input_left[0], start_direction='north') - Circle(start.add_x(start_add), radius=0.05, fill='black') + Connection.connect(start.add_x(start_add), box_psi_opt.input_left[0], start_direction="north") + Circle(start.add_x(start_add), radius=0.05, fill="black") # Connection between the optimal flux and minimum flux block Connection.connect(box_psi_opt.output_right[0], box_min.input_left[1]) # Connection of the maximum torque block - Connection.connect(box_min.output_right[0].add_x(0.3), box_t_max.input_left[0], start_direction='south') - Circle(box_min.output_right[0].add_x(0.3), radius=0.05, fill='black') + Connection.connect(box_min.output_right[0].add_x(0.3), box_t_max.input_left[0], start_direction="south") + Circle(box_min.output_right[0].add_x(0.3), radius=0.05, fill="black") Connection.connect(box_t_max.output_right[0], box_limit.input_top[0]) # Calculation of the i_sq reference - box_i_sq_ref = Box(box_limit.output_right[0].add_x(1.5), size=(1, 0.8), - text=r'$\frac{2 L_{\mathrm{r}}}{3 p L_{\mathrm{m}}}$') - Connection.connect(box_limit.output_right, box_i_sq_ref.input_left, text=r'$T^{*}_{\mathrm{lim}}$') - divide = Box(box_i_sq_ref.output_right[0].add_x(1), size=(0.5, 0.5), text=r'$\div$', inputs=dict(left=1, bottom=1), - outputs=dict(right=1, top=1)) + box_i_sq_ref = Box( + box_limit.output_right[0].add_x(1.5), size=(1, 0.8), text=r"$\frac{2 L_{\mathrm{r}}}{3 p L_{\mathrm{m}}}$" + ) + Connection.connect(box_limit.output_right, box_i_sq_ref.input_left, text=r"$T^{*}_{\mathrm{lim}}$") + divide = Box( + box_i_sq_ref.output_right[0].add_x(1), + size=(0.5, 0.5), + text=r"$\div$", + inputs=dict(left=1, bottom=1), + outputs=dict(right=1, top=1), + ) Connection.connect(box_i_sq_ref.output_right, divide.input_left) # Add block for the flux add_psi = Add(box_min.output_right[0].add_x(2.5)) # Flux Controller - pi_psi = PIController(add_psi.position.add_x(1.5), text='Flux\nController') + pi_psi = PIController(add_psi.position.add_x(1.5), text="Flux\nController") # Connections of the add block - Connection.connect(box_min.output_right, add_psi.input_left, text=r'$\Psi^{*}_{\mathrm{lim}}$') - Connection.connect(divide.output_top, add_psi.input_bottom, text=r'$\hat{\Psi}_{\mathrm{r}}$') + Connection.connect(box_min.output_right, add_psi.input_left, text=r"$\Psi^{*}_{\mathrm{lim}}$") + Connection.connect(divide.output_top, add_psi.input_bottom, text=r"$\hat{\Psi}_{\mathrm{r}}$") Connection.connect(add_psi.output_right, pi_psi.input_left) # Limit the current reference values - limit = Limit(pi_psi.position.add(3.5, -0.5), size=(1.5, 1.5), inputs=dict(left=2, left_space=1), - outputs=dict(right=2, right_space=1)) + limit = Limit( + pi_psi.position.add(3.5, -0.5), + size=(1.5, 1.5), + inputs=dict(left=2, left_space=1), + outputs=dict(right=2, right_space=1), + ) # Connections of the limit block Connection.connect(pi_psi.output_right[0], limit.input_left[0]) @@ -127,30 +153,46 @@ def scim_ops(start, control_task): add_a = Add(box_min.input_left[0].sub_x(1.5), inputs=dict(left=1, top=1)) # Connection between the add and minimum block - Connection.connect(add_a.output_right[0], box_min.input_left[0], text=r'$\Psi_{\mathrm{lim}}$', text_align='bottom', - distance_y=0.25) + Connection.connect( + add_a.output_right[0], + box_min.input_left[0], + text=r"$\Psi_{\mathrm{lim}}$", + text_align="bottom", + distance_y=0.25, + ) # Calculate the maximum flux - divide_a = Divide(add_a.position.add_y(1), size=(1, 0.5), inputs='top', input_space=0.5) - Connection.connect(divide_a.output_bottom, add_a.input_top, text=r'$\Psi_{\mathrm{max}}$', text_align='right', - distance_x=0.5) - Connection.connect(divide_a.input_top[0].add_y(0.3), divide_a.input_top[0], - text=r'$\frac{u_{\mathrm{\mbox{\fontsize{3}{4}\selectfont DC}}}}{\sqrt{3}}$', - text_position='start', - text_align='top') - Connection.connect(divide_a.input_top[1].add_y(0.3), divide_a.input_top[1], text=r'$\omega_{\mathrm{el}}$', - text_position='start', text_align='top', move_text=(0, -0.05)) + divide_a = Divide(add_a.position.add_y(1), size=(1, 0.5), inputs="top", input_space=0.5) + Connection.connect( + divide_a.output_bottom, add_a.input_top, text=r"$\Psi_{\mathrm{max}}$", text_align="right", distance_x=0.5 + ) + Connection.connect( + divide_a.input_top[0].add_y(0.3), + divide_a.input_top[0], + text=r"$\frac{u_{\mathrm{\mbox{\fontsize{3}{4}\selectfont DC}}}}{\sqrt{3}}$", + text_position="start", + text_align="top", + ) + Connection.connect( + divide_a.input_top[1].add_y(0.3), + divide_a.input_top[1], + text=r"$\omega_{\mathrm{el}}$", + text_position="start", + text_align="top", + move_text=(0, -0.05), + ) # Limit the delta flux limit_psi = Limit(add_a.input_left[0].sub_x(1.5), size=(1, 1)) # I Controller of the modulation controller - i_controller = IController(limit_psi.input_left[0].sub_x(1.2), size=(1.2, 1), text='Modulation\nController') + i_controller = IController(limit_psi.input_left[0].sub_x(1.2), size=(1.2, 1), text="Modulation\nController") # Connections of the limit block Connection.connect(i_controller.output_right, limit_psi.input_left) - Connection.connect(limit_psi.output_right[0], add_a.input_left[0], text=r'$\Delta \Psi$', distance_y=0.25, - text_align='bottom') + Connection.connect( + limit_psi.output_right[0], add_a.input_left[0], text=r"$\Delta \Psi$", distance_y=0.25, text_align="bottom" + ) # Add block for the modulation add_a_max = Add(i_controller.position.sub_x(1.5)) @@ -159,31 +201,53 @@ def scim_ops(start, control_task): Connection.connect(add_a_max.output_right, i_controller.input_left) # Maximum modulation block - box_a_max = Box(add_a_max.input_left[0].sub_x(1.3), size=(1.5, 0.8), text=r'$a_{\mathrm{max}} \cdot k$') + box_a_max = Box(add_a_max.input_left[0].sub_x(1.3), size=(1.5, 0.8), text=r"$a_{\mathrm{max}} \cdot k$") # Connection between the maximum modulation and add block - Connection.connect(box_a_max.output_right[0], add_a_max.input_left[0], text='$a^{*}$', text_align='bottom') + Connection.connect(box_a_max.output_right[0], add_a_max.input_left[0], text="$a^{*}$", text_align="bottom") # Calculation of the actual modulation - box_abs = Box(add_a_max.input_bottom[0].sub_y(1), size=(0.8, 0.8), text=r'|\textbf{x}|', inputs=dict(bottom=1), - outputs=dict(top=1)) - con_a = Connection.connect(box_abs.output_top[0], add_a_max.input_bottom[0], text='$-$', text_align='right', - text_position='end', move_text=(-0.1, -0.1)) - Text(position=Point.get_mid(*con_a.points).sub_x(0.25), text='$a$') - div_a = Divide(box_abs.input_bottom[0].sub_y(0.8), size=(1, 0.5), inputs='bottom', input_space=0.5) - Connection.connect(div_a.input_bottom[1].sub_y(0.4), div_a.input_bottom[1], - text=r'$\frac{u_{\mathrm{\mbox{\fontsize{3}{4}\selectfont DC}}}}{2}$', text_align='bottom', - text_position='start') - Connection.connect(div_a.input_bottom[0].sub_y(0.4), div_a.input_bottom[0], - text=r'$\mathbf{u^{*}_{\mathrm{dq}}}$', text_align='bottom', text_position='start') + box_abs = Box( + add_a_max.input_bottom[0].sub_y(1), + size=(0.8, 0.8), + text=r"|\textbf{x}|", + inputs=dict(bottom=1), + outputs=dict(top=1), + ) + con_a = Connection.connect( + box_abs.output_top[0], + add_a_max.input_bottom[0], + text="$-$", + text_align="right", + text_position="end", + move_text=(-0.1, -0.1), + ) + Text(position=Point.get_mid(*con_a.points).sub_x(0.25), text="$a$") + div_a = Divide(box_abs.input_bottom[0].sub_y(0.8), size=(1, 0.5), inputs="bottom", input_space=0.5) + Connection.connect( + div_a.input_bottom[1].sub_y(0.4), + div_a.input_bottom[1], + text=r"$\frac{u_{\mathrm{\mbox{\fontsize{3}{4}\selectfont DC}}}}{2}$", + text_align="bottom", + text_position="start", + ) + Connection.connect( + div_a.input_bottom[0].sub_y(0.4), + div_a.input_bottom[0], + text=r"$\mathbf{u^{*}_{\mathrm{dq}}}$", + text_align="bottom", + text_position="start", + ) Connection.connect(div_a.output_top, box_abs.input_bottom) # Inputs of the stage - inputs = dict(t_ref=[start, dict(arrow=False, text=r'$T^{*}$', end_direction='south', move_text=(-0.25, 1.8))], - psi_r=[divide.input_bottom[0], dict()]) - outputs = dict(i_q_ref=limit.output_right[1], i_d_ref=limit.output_right[0]) # Outputs of the stage - connect_to_lines = dict() # Connections to other lines - connections = dict() # Connections of the stage - start = limit.output_right[0] # Starting point of the next stage + inputs = dict( + t_ref=[start, dict(arrow=False, text=r"$T^{*}$", end_direction="south", move_text=(-0.25, 1.8))], + psi_r=[divide.input_bottom[0], dict()], + ) + outputs = dict(i_q_ref=limit.output_right[1], i_d_ref=limit.output_right[0]) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Connections of the stage + start = limit.output_right[0] # Starting point of the next stage return start, inputs, outputs, connect_to_lines, connections diff --git a/gem_controllers/block_diagrams/stage_blocks/scim_output.py b/src/gem_controllers/block_diagrams/stage_blocks/scim_output.py similarity index 75% rename from gem_controllers/block_diagrams/stage_blocks/scim_output.py rename to src/gem_controllers/block_diagrams/stage_blocks/scim_output.py index d6b3844a..50844d4f 100644 --- a/gem_controllers/block_diagrams/stage_blocks/scim_output.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/scim_output.py @@ -26,17 +26,18 @@ def _scim_output(start, control_task): converter = DcConverter(start.add_x(2.7), input_number=3, input_space=0.3, output_number=3) # SCIM block - scim = SCIM(converter.position.sub_y(5), size=1.3, input='top') + scim = SCIM(converter.position.sub_y(5), size=1.3, input="top") # Connection between the converter and the motor block con_1 = Connection.connect(converter.output_bottom, scim.input_top, arrow=False) - start = scim.position # starting point of the next block + start = scim.position # starting point of the next block # Inputs of the stage - inputs = dict(S=[converter.input_left, dict(text=[r'$\mathbf{S}_{\mathrm{a,b,c}}$', '', ''], distance_y=0.25)]) - outputs = dict(omega=scim.output_left[0]) # Outputs of the stage - connect_to_lines = dict() # Connections to other lines - connections = dict(i=con_1) # Connections + inputs = dict(S=[converter.input_left, dict(text=[r"$\mathbf{S}_{\mathrm{a,b,c}}$", "", ""], distance_y=0.25)]) + outputs = dict(omega=scim.output_left[0]) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict(i=con_1) # Connections return start, inputs, outputs, connect_to_lines, connections + return _scim_output diff --git a/gem_controllers/block_diagrams/stage_blocks/scim_sc.py b/src/gem_controllers/block_diagrams/stage_blocks/scim_sc.py similarity index 59% rename from gem_controllers/block_diagrams/stage_blocks/scim_sc.py rename to src/gem_controllers/block_diagrams/stage_blocks/scim_sc.py index e09e78c5..7c280603 100644 --- a/gem_controllers/block_diagrams/stage_blocks/scim_sc.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/scim_sc.py @@ -14,18 +14,19 @@ def scim_speed_controller(start, control_task): """ # space to the previous block - space = 1 if control_task == 'SC' else 1.5 + space = 1 if control_task == "SC" else 1.5 # Add block for the speed reference and state add_omega = Add(start.add_x(space)) - if control_task == 'SC': + if control_task == "SC": # Connection at the input - Connection.connect(start, add_omega.input_left[0], text=r'$\omega^{*}$', text_align='left', - text_position='start') + Connection.connect( + start, add_omega.input_left[0], text=r"$\omega^{*}$", text_align="left", text_position="start" + ) # PI Speed Controller - pi_omega = PIController(add_omega.position.add_x(1.5), text='Speed\nController') + pi_omega = PIController(add_omega.position.add_x(1.5), text="Speed\nController") # Connection between the add block and pi controller Connection.connect(add_omega.output_right, pi_omega.input_left) @@ -36,13 +37,17 @@ def scim_speed_controller(start, control_task): # Connection between the pi controller and the limit block Connection.connect(pi_omega.output_right[0], limit.input_left[0]) - start = limit.output_right[0].add(0.5, 2) # starting point of the next block + start = limit.output_right[0].add(0.5, 2) # starting point of the next block # Inputs of the stage - inputs = dict(omega_ref=[add_omega.input_left[0], dict(text=r'$\omega^{*}$')], - omega_me=[add_omega.input_bottom[0], dict(text='-', text_position='end', text_align='right', - move_text=(-0.2, -0.2))]) - outputs = dict(t_ref=limit.output_right[0]) # Outputs of the stage - connect_to_lines = dict() # Connections to other lines - connections = dict() # Conncetions of the stage + inputs = dict( + omega_ref=[add_omega.input_left[0], dict(text=r"$\omega^{*}$")], + omega_me=[ + add_omega.input_bottom[0], + dict(text="-", text_position="end", text_align="right", move_text=(-0.2, -0.2)), + ], + ) + outputs = dict(t_ref=limit.output_right[0]) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Conncetions of the stage return start, inputs, outputs, connect_to_lines, connections diff --git a/gem_controllers/block_diagrams/stage_blocks/series_dc_cc.py b/src/gem_controllers/block_diagrams/stage_blocks/series_dc_cc.py similarity index 53% rename from gem_controllers/block_diagrams/stage_blocks/series_dc_cc.py rename to src/gem_controllers/block_diagrams/stage_blocks/series_dc_cc.py index 72d926c3..400ebfad 100644 --- a/gem_controllers/block_diagrams/stage_blocks/series_dc_cc.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/series_dc_cc.py @@ -23,56 +23,74 @@ def cc_series_dc(start, control_task): """ # space to the previous block - space = 1 if control_task == 'CC' else 1.5 + space = 1 if control_task == "CC" else 1.5 # Add block for the current reference and state add_current = Add(start.add_x(space)) - if control_task == 'CC': + if control_task == "CC": # Connection at the input - Connection.connect(start, add_current.input_left[0], text=r'$i^{*}$', text_align='left', - text_position='start') + Connection.connect( + start, add_current.input_left[0], text=r"$i^{*}$", text_align="left", text_position="start" + ) # PI Current Controller - pi_current = PIController(add_current.position.add_x(1.5), text='Current\nController') + pi_current = PIController(add_current.position.add_x(1.5), text="Current\nController") # Connection between the add block and pi controller Connection.connect(add_current.output_right, pi_current.input_left) - start = pi_current.position # starting point of the next block + start = pi_current.position # starting point of the next block # Inputs of the stage - inputs = dict(i_ref=[add_current.input_left[0], dict(text=r'$i^{*}$')], i=[add_current.input_bottom[0], - dict(text=r'-', move_text=(-0.2, -0.2), text_position='end', text_align='right')]) - outputs = dict(u=pi_current.output_right[0]) # Outputs of the stage - connect_to_lines = dict() # Connections to other lines - connections = dict() # Connections + inputs = dict( + i_ref=[add_current.input_left[0], dict(text=r"$i^{*}$")], + i=[ + add_current.input_bottom[0], + dict(text=r"-", move_text=(-0.2, -0.2), text_position="end", text_align="right"), + ], + ) + outputs = dict(u=pi_current.output_right[0]) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Connections if emf_feedforward: # Add block of the emf feedforward add_emf = Add(pi_current.position.add_x(2)) # Connection between pi controller and add block - Connection.connect(pi_current.output_right, add_emf.input_left, text=r'$\Delta u^{*}$') + Connection.connect(pi_current.output_right, add_emf.input_left, text=r"$\Delta u^{*}$") # Multiplication with the flux - box_psi = Box(add_emf.position.sub_y(2.5), size=(1, 0.8), text=r"$L'_{\mathrm{e}} i$", inputs=dict(bottom=1), - outputs=dict(top=1)) + box_psi = Box( + add_emf.position.sub_y(2.5), + size=(1, 0.8), + text=r"$L'_{\mathrm{e}} i$", + inputs=dict(bottom=1), + outputs=dict(top=1), + ) # Connection between the multiplication and add block - Connection.connect(box_psi.output_top, add_emf.input_bottom, text=r'$u^{0}$', text_position='end', - text_align='right', move_text=(-0.1, -0.2)) + Connection.connect( + box_psi.output_top, + add_emf.input_bottom, + text=r"$u^{0}$", + text_position="end", + text_align="right", + move_text=(-0.1, -0.2), + ) # Set the input of the emf feedforward - if control_task in ['SC']: - connect_to_lines['omega'] = [box_psi.input_bottom[0], dict(section=0)] - elif control_task in ['CC', 'TC']: - inputs['omega'] = [box_psi.input_bottom[0], dict()] + if control_task in ["SC"]: + connect_to_lines["omega"] = [box_psi.input_bottom[0], dict(section=0)] + elif control_task in ["CC", "TC"]: + inputs["omega"] = [box_psi.input_bottom[0], dict()] # Set the output of the emf feedforward - outputs['u'] = add_emf.output_right[0] + outputs["u"] = add_emf.output_right[0] # Update the position of the next block start = add_emf.position return start, inputs, outputs, connect_to_lines, connections + return cc_series_dc diff --git a/gem_controllers/block_diagrams/stage_blocks/series_dc_ops.py b/src/gem_controllers/block_diagrams/stage_blocks/series_dc_ops.py similarity index 72% rename from gem_controllers/block_diagrams/stage_blocks/series_dc_ops.py rename to src/gem_controllers/block_diagrams/stage_blocks/series_dc_ops.py index afba0ab0..c03555e8 100644 --- a/gem_controllers/block_diagrams/stage_blocks/series_dc_ops.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/series_dc_ops.py @@ -14,11 +14,11 @@ def series_dc_ops(start, control_task): """ # space to the previous block - space = 1 if control_task == 'TC' else 2.2 + space = 1 if control_task == "TC" else 2.2 # Calculation of the current reference box_torque = Box(start.add_x(space), size=(0.8, 0.8), text=r"$\frac{1}{L'_{\mathrm{e}}}$") - box_sqrt = Box(box_torque.output_right[0].add_x(1), size=(0.8, 0.8), text=r'$\sqrt{\mathrm{x}}$') + box_sqrt = Box(box_torque.output_right[0].add_x(1), size=(0.8, 0.8), text=r"$\sqrt{\mathrm{x}}$") # Conncetion between the previous blocks Connection.connect(box_torque.output_right, box_sqrt.input_left) @@ -29,15 +29,14 @@ def series_dc_ops(start, control_task): # Conncetion between the calculation and limit block Connection.connect(box_sqrt.output_right, limit.input_left) - if control_task == 'TC': + if control_task == "TC": # Conncetion at the input - Connection.connect(start, box_torque.input_left[0], text=r'$T^{*}$', text_align='left', - text_position='start') + Connection.connect(start, box_torque.input_left[0], text=r"$T^{*}$", text_align="left", text_position="start") start = limit.position # starting point of the next block - inputs = dict(t_ref=[box_torque.input_left[0], dict(text=r'$T^{*}$')]) # Inputs of the stage - outputs = dict(i_ref=limit.output_right[0]) # Outputs of the stage - connect_to_lines = dict() # Connections to other lines - connections = dict() # Connections + inputs = dict(t_ref=[box_torque.input_left[0], dict(text=r"$T^{*}$")]) # Inputs of the stage + outputs = dict(i_ref=limit.output_right[0]) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Connections return start, inputs, outputs, connect_to_lines, connections diff --git a/gem_controllers/block_diagrams/stage_blocks/series_dc_output.py b/src/gem_controllers/block_diagrams/stage_blocks/series_dc_output.py similarity index 65% rename from gem_controllers/block_diagrams/stage_blocks/series_dc_output.py rename to src/gem_controllers/block_diagrams/stage_blocks/series_dc_output.py index c2411881..31d38a24 100644 --- a/gem_controllers/block_diagrams/stage_blocks/series_dc_output.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/series_dc_output.py @@ -29,7 +29,7 @@ def _series_dc_output(start, control_task): limit = Limit(start.add_x(space), size=(1, 1)) # pulse width modulation block - pwm = Box(limit.output_right[0].add_x(1.5), size=(1, 0.8), text='PWM') + pwm = Box(limit.output_right[0].add_x(1.5), size=(1, 0.8), text="PWM") # connection between limit and pwm block Connection.connect(limit.output_right, pwm.input_left) @@ -38,31 +38,35 @@ def _series_dc_output(start, control_task): converter = DcConverter(pwm.position.add_x(2), size=1.2, input_number=1) # Connection between pwm and converter block - Connection.connect(pwm.output_right, converter.input_left, text='$S$') + Connection.connect(pwm.output_right, converter.input_left, text="$S$") # DC Series motor block - dc_series = DcSeriesMotor(converter.position.sub_y(3), size=1.2, input='top', output='left') + dc_series = DcSeriesMotor(converter.position.sub_y(3), size=1.2, input="top", output="left") # Connections between converter and motor block - conv_motor = Connection.connect(converter.output_bottom, dc_series.input_top, text=['', r'$i$'], - text_align='right', arrow=False) + conv_motor = Connection.connect( + converter.output_bottom, dc_series.input_top, text=["", r"$i$"], text_align="right", arrow=False + ) # Connection between previous connection and current output - con_i = Connection.connect_to_line(conv_motor[0], pwm.position.sub_y(1.5), text=r'$i$', arrow=False, fill=False, - radius=0.1) + con_i = Connection.connect_to_line( + conv_motor[0], pwm.position.sub_y(1.5), text=r"$i$", arrow=False, fill=False, radius=0.1 + ) start = converter.position # starting point of the next block - inputs = dict(u=[limit.input_left[0], dict(text=r'$u^{*}$')]) # Inputs of the stage - outputs = dict(i=con_i.end) # Outputs of the stage - connect_to_lines = dict() # Connections to other lines - connections = dict() # Connections + inputs = dict(u=[limit.input_left[0], dict(text=r"$u^{*}$")]) # Inputs of the stage + outputs = dict(i=con_i.end) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Connections - if emf_feedforward or control_task in ['SC']: + if emf_feedforward or control_task in ["SC"]: # Connection between the motor output and the omega output of the stage - con_omega = Connection.connect(dc_series.output_left[0].sub_x(2), dc_series.output_left[0], - text=r'$\omega_{\mathrm{me}}$', arrow=False) + con_omega = Connection.connect( + dc_series.output_left[0].sub_x(2), dc_series.output_left[0], text=r"$\omega_{\mathrm{me}}$", arrow=False + ) # Add the omega output - outputs['omega'] = con_omega.end + outputs["omega"] = con_omega.end return start, inputs, outputs, connect_to_lines, connections + return _series_dc_output diff --git a/gem_controllers/block_diagrams/stage_blocks/shunt_dc_cc.py b/src/gem_controllers/block_diagrams/stage_blocks/shunt_dc_cc.py similarity index 52% rename from gem_controllers/block_diagrams/stage_blocks/shunt_dc_cc.py rename to src/gem_controllers/block_diagrams/stage_blocks/shunt_dc_cc.py index 6ced1534..c323eefc 100644 --- a/gem_controllers/block_diagrams/stage_blocks/shunt_dc_cc.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/shunt_dc_cc.py @@ -23,57 +23,74 @@ def cc_shunt_dc(start, control_task): """ # space to the previous block - space = 1 if control_task == 'CC' else 1.5 + space = 1 if control_task == "CC" else 1.5 # Add block for the current reference and state add_current = Add(start.add_x(space)) - if control_task == 'CC': + if control_task == "CC": # Connection at the input - Connection.connect(start, add_current.input_left[0], text=r'$i^{*}_{\mathrm{a}}$', text_align='left', - text_position='start') + Connection.connect( + start, add_current.input_left[0], text=r"$i^{*}_{\mathrm{a}}$", text_align="left", text_position="start" + ) # PI Current Controller - pi_current = PIController(add_current.position.add_x(1.5), text='Current\nController') + pi_current = PIController(add_current.position.add_x(1.5), text="Current\nController") # Connection between the add block and pi controller Connection.connect(add_current.output_right, pi_current.input_left) - start = pi_current.position # starting point of the next block + start = pi_current.position # starting point of the next block # Inputs of the stage - inputs = dict(i_ref=[add_current.input_left[0], dict(text=r'$i^{*}_{\mathrm{a}}$')], - i=[add_current.input_bottom[0], dict(text=r'-', move_text=(-0.2, -0.2), text_position='end', - text_align='right')]) - outputs = dict(u=pi_current.output_right[0]) # Outputs of the stage - connect_to_lines = dict() # Connections to other lines - connections = dict() # Connections + inputs = dict( + i_ref=[add_current.input_left[0], dict(text=r"$i^{*}_{\mathrm{a}}$")], + i=[ + add_current.input_bottom[0], + dict(text=r"-", move_text=(-0.2, -0.2), text_position="end", text_align="right"), + ], + ) + outputs = dict(u=pi_current.output_right[0]) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Connections if emf_feedforward: # Add block of the emf feedforward add_emf = Add(pi_current.position.add_x(2)) # Connection between pi controller and add block - Connection.connect(pi_current.output_right, add_emf.input_left, text=r'$\Delta u^{*}$') + Connection.connect(pi_current.output_right, add_emf.input_left, text=r"$\Delta u^{*}$") # Multiplication with the flux - box_psi = Box(add_emf.position.sub_y(2.5), size=(1, 0.8), text=r"$L'_{\mathrm{e}} i_{\mathrm{e}}$", - inputs=dict(bottom=1), outputs=dict(top=1)) + box_psi = Box( + add_emf.position.sub_y(2.5), + size=(1, 0.8), + text=r"$L'_{\mathrm{e}} i_{\mathrm{e}}$", + inputs=dict(bottom=1), + outputs=dict(top=1), + ) # Connection between the multiplication and add block - Connection.connect(box_psi.output_top, add_emf.input_bottom, text=r'$u^{0}$', - text_position='end', text_align='right', move_text=(-0.1, -0.2)) + Connection.connect( + box_psi.output_top, + add_emf.input_bottom, + text=r"$u^{0}$", + text_position="end", + text_align="right", + move_text=(-0.1, -0.2), + ) # Set the input of the emf feedforward - if control_task in ['SC']: - connect_to_lines['omega'] = [box_psi.input_bottom[0], dict(section=0)] - elif control_task in ['CC', 'TC']: - inputs['omega'] = [box_psi.input_bottom[0], dict()] + if control_task in ["SC"]: + connect_to_lines["omega"] = [box_psi.input_bottom[0], dict(section=0)] + elif control_task in ["CC", "TC"]: + inputs["omega"] = [box_psi.input_bottom[0], dict()] # Set the output of the emf feedforward - outputs['u'] = add_emf.output_right[0] + outputs["u"] = add_emf.output_right[0] # Update the position of the next block start = add_emf.position return start, inputs, outputs, connect_to_lines, connections + return cc_shunt_dc diff --git a/gem_controllers/block_diagrams/stage_blocks/shunt_dc_ops.py b/src/gem_controllers/block_diagrams/stage_blocks/shunt_dc_ops.py similarity index 71% rename from gem_controllers/block_diagrams/stage_blocks/shunt_dc_ops.py rename to src/gem_controllers/block_diagrams/stage_blocks/shunt_dc_ops.py index 291307a7..a71eee66 100644 --- a/gem_controllers/block_diagrams/stage_blocks/shunt_dc_ops.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/shunt_dc_ops.py @@ -14,15 +14,14 @@ def shunt_dc_ops(start, control_task): """ # space to the previous block - space = 1 if control_task == 'TC' else 2.2 + space = 1 if control_task == "TC" else 2.2 # Calculation of the current reference box_torque = Box(start.add_x(space), size=(0.8, 0.8), text=r"$\frac{1}{L'_{\mathrm{e}} i_{\mathrm{e}}}$") - if control_task == 'TC': + if control_task == "TC": # Connection at the input - Connection.connect(start, box_torque.input_left[0], text=r'$T^{*}$', text_align='left', - text_position='start') + Connection.connect(start, box_torque.input_left[0], text=r"$T^{*}$", text_align="left", text_position="start") # Limit of the current reference limit = Limit(box_torque.output_right[0].add_x(1), size=(1, 1)) @@ -31,9 +30,9 @@ def shunt_dc_ops(start, control_task): Connection.connect(box_torque.output_right, limit.input_left) start = limit.position # starting point of the next block - inputs = dict(t_ref=[box_torque.input_left[0], dict(text=r'$T^{*}$')]) # Inputs of the stage - outputs = dict(i_ref=limit.output_right[0]) # Outputs of the stage - connect_to_lines = dict() # Connections to other lines - connections = dict() # Connections + inputs = dict(t_ref=[box_torque.input_left[0], dict(text=r"$T^{*}$")]) # Inputs of the stage + outputs = dict(i_ref=limit.output_right[0]) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Connections return start, inputs, outputs, connect_to_lines, connections diff --git a/gem_controllers/block_diagrams/stage_blocks/shunt_dc_output.py b/src/gem_controllers/block_diagrams/stage_blocks/shunt_dc_output.py similarity index 64% rename from gem_controllers/block_diagrams/stage_blocks/shunt_dc_output.py rename to src/gem_controllers/block_diagrams/stage_blocks/shunt_dc_output.py index b1cdbd04..84b52505 100644 --- a/gem_controllers/block_diagrams/stage_blocks/shunt_dc_output.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/shunt_dc_output.py @@ -29,7 +29,7 @@ def _shunt_dc_output(start, control_task): limit = Limit(start.add_x(space), size=(1, 1)) # pulse width modulation block - pwm = Box(limit.output_right[0].add_x(1.5), size=(1, 0.8), text='PWM') + pwm = Box(limit.output_right[0].add_x(1.5), size=(1, 0.8), text="PWM") # connection between limit and pwm block Connection.connect(limit.output_right, pwm.input_left) @@ -38,31 +38,35 @@ def _shunt_dc_output(start, control_task): converter = DcConverter(pwm.position.add_x(2), size=1.2, input_number=1) # Connection between pwm and converter block - Connection.connect(pwm.output_right, converter.input_left, text='$S$') + Connection.connect(pwm.output_right, converter.input_left, text="$S$") # DC Shunt motor block - dc_shunt = DcShuntMotor(converter.position.sub_y(3), size=1.2, input='top', output='left') + dc_shunt = DcShuntMotor(converter.position.sub_y(3), size=1.2, input="top", output="left") # Connections between converter and motor block - conv_motor = Connection.connect(converter.output_bottom, dc_shunt.input_top, text=['', r'$i_{\mathrm{a}}$'], - text_align='right', arrow=False) + conv_motor = Connection.connect( + converter.output_bottom, dc_shunt.input_top, text=["", r"$i_{\mathrm{a}}$"], text_align="right", arrow=False + ) # Connection between previous connection and current output - con_i = Connection.connect_to_line(conv_motor[0], pwm.position.sub_y(1.5), text=r'$i_{\mathrm{a}}$', - arrow=False, fill=False, radius=0.1) + con_i = Connection.connect_to_line( + conv_motor[0], pwm.position.sub_y(1.5), text=r"$i_{\mathrm{a}}$", arrow=False, fill=False, radius=0.1 + ) start = converter.position # starting point of the next block - inputs = dict(u=[limit.input_left[0], dict(text=r'$u^{*}$')]) # Inputs of the stage - outputs = dict(i=con_i.end) # Outputs of the stage - connect_to_lines = dict() # Connections to other lines - connections = dict() # Connections + inputs = dict(u=[limit.input_left[0], dict(text=r"$u^{*}$")]) # Inputs of the stage + outputs = dict(i=con_i.end) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict() # Connections - if emf_feedforward or control_task in ['SC']: + if emf_feedforward or control_task in ["SC"]: # Connection between the motor output and the omega output of the stage - con_omega = Connection.connect(dc_shunt.output_left[0].sub_x(2), dc_shunt.output_left[0], - text=r'$\omega_{\mathrm{me}}$', arrow=False) + con_omega = Connection.connect( + dc_shunt.output_left[0].sub_x(2), dc_shunt.output_left[0], text=r"$\omega_{\mathrm{me}}$", arrow=False + ) # Add the omega output - outputs['omega'] = con_omega.end + outputs["omega"] = con_omega.end return start, inputs, outputs, connect_to_lines, connections + return _shunt_dc_output diff --git a/src/gem_controllers/block_diagrams/stage_blocks/synrm_cc.py b/src/gem_controllers/block_diagrams/stage_blocks/synrm_cc.py new file mode 100644 index 00000000..38a9cd3e --- /dev/null +++ b/src/gem_controllers/block_diagrams/stage_blocks/synrm_cc.py @@ -0,0 +1,285 @@ +from control_block_diagram.components import Point, Box, Connection, Circle +from control_block_diagram.predefined_components import ( + DqToAbcTransformation, + AbcToAlphaBetaTransformation, + AlphaBetaToDqTransformation, + Add, + PIController, + Multiply, + Limit, +) + + +def synrm_cc(emf_feedforward): + """ + Args: + emf_feedforward: Boolean whether emf feedforward stage is included + + Returns: + Function to build the SynRM current control block + """ + + def cc_synrm(start, control_task): + """ + Function to build the SynRM current control block + Args: + start: Starting point of the block + control_task: Control task of the controller + + Returns: + endpoint, inputs, outputs, connection to other lines, connections + """ + + # space to the previous block + space = 1 if control_task == "CC" else 1.5 + + # Add blocks for the i_sd and i_sq references and states + add_i_sd = Add(start.add_x(space)) + add_i_sq = Add(add_i_sd.position.sub(0.5, 1)) + + if control_task == "CC": + # Connections at the inputs + Connection.connect( + add_i_sd.input_left[0].sub_x(space), + add_i_sd.input_left[0], + text=r"$i^{*}_{\mathrm{sd}}$", + text_position="start", + text_align="left", + ) + Connection.connect( + add_i_sq.input_left[0].sub_x(space - 0.5), + add_i_sq.input_left[0], + text=r"$i^{*}_{\mathrm{sq}}$", + text_position="start", + text_align="left", + ) + + # PI Controllers for the d and q component + pi_i_sd = PIController( + add_i_sd.position.add_x(1.2), size=(1, 0.8), input_number=1, output_number=1, text="Current\nController" + ) + pi_i_sq = PIController( + Point.merge(pi_i_sd.position, add_i_sq.position), size=(1, 0.8), input_number=1, output_number=1 + ) + + # Connection between the add blocks and the PI Controllers + Connection.connect(add_i_sd.output_right, pi_i_sd.input_left) + Connection.connect(add_i_sq.output_right, pi_i_sq.input_left) + + # Add blocks for the EMF Feedforward + add_u_sd = Add(pi_i_sd.position.add_x(2)) + add_u_sq = Add(pi_i_sq.position.add_x(1.2)) + + # Connections between the PI Controllers and the Add blocks + Connection.connect( + pi_i_sd.output_right[0], add_u_sd.input_left[0], text=r"$\Delta u^{*}_{\mathrm{sd}}$", distance_y=0.28 + ) + Connection.connect( + pi_i_sq.output_right[0], + add_u_sq.input_left[0], + text=r"$\Delta u^{*}_{\mathrm{sq}}$", + distance_y=0.4, + move_text=(0.25, 0), + ) + + # Coordinate transformation from DQ to Abc coordinates + dq_to_abc = DqToAbcTransformation(Point.get_mid(add_u_sd.position, add_u_sq.position).add_x(2), input_space=1) + + # Connections between the add blocks and the coordinate transformation + Connection.connect( + add_u_sd.output_right[0], dq_to_abc.input_left[0], text=r"$u^{*}_{\mathrm{sd}}$", distance_y=0.28 + ) + Connection.connect( + add_u_sq.output_right[0], + dq_to_abc.input_left[1], + text=r"$u^{*}_{\mathrm{sq}}$", + distance_y=0.28, + move_text=(0.4, 0), + ) + + # Limit of the input voltages + limit = Limit( + dq_to_abc.position.add_x(1.8), + size=(1, 1.2), + inputs=dict(left=3, left_space=0.3), + outputs=dict(right=3, right_space=0.3), + ) + + # Connection between the coordinate transformation and the limit block + Connection.connect(dq_to_abc.output_right, limit.input_left) + + # Pulse width modulation block + pwm = Box( + limit.position.add_x(2.5), + size=(1.5, 1.2), + text="PWM", + inputs=dict(left=3, left_space=0.3), + outputs=dict(right=3, right_space=0.3), + ) + + # Connection between the limit and the PWM block + Connection.connect( + limit.output_right, pwm.input_left, text=[r"$u^*_{\mathrm{s a,b,c}}$", "", ""], distance_y=0.25 + ) + + # Coordinate transformation from ABC to AlphaBeta coordinates + abc_to_alpha_beta = AbcToAlphaBetaTransformation(pwm.position.sub(1, 3.5), input="right", output="left") + + # Coordinate transformation from AlphaBeta to DQ coordinates + alpha_beta_to_dq = AlphaBetaToDqTransformation( + Point.merge(dq_to_abc.position, abc_to_alpha_beta.position), input="right", output="left" + ) + + # Distance of the blocks in the EMF Feedforward path + distance = (add_u_sq.position.y - alpha_beta_to_dq.output_left[0].y) / 3 + + # Multiplications of the EMF Feedforward + multiply_u_sq = Multiply(add_u_sq.position.sub_y(distance), outputs=dict(top=1)) + multiply_u_sd = Multiply(Point.merge(add_u_sd.position, multiply_u_sq.position), outputs=dict(top=1)) + + # Connections between the multiplication and the add blocks + Connection.connect(multiply_u_sq.output_top, add_u_sq.input_bottom) + Connection.connect( + multiply_u_sd.output_top, + add_u_sd.input_bottom, + text=r"-", + text_position="end", + text_align="right", + move_text=(-0.2, -0.2), + ) + + # Multiplication with the inductances + box_ls_1 = Box( + multiply_u_sq.position.sub_y(distance), + size=(0.6, 0.6), + inputs=dict(bottom=1), + outputs=dict(top=1), + text=r"$L_{\mathrm{d}}$", + ) + box_ls_2 = Box( + Point.merge(multiply_u_sd.position, box_ls_1.position), + size=(0.6, 0.6), + inputs=dict(bottom=1), + outputs=dict(top=1), + text=r"$L_{\mathrm{q}}$", + ) + + # Connections from the multiplications + Connection.connect(box_ls_1.output_top, multiply_u_sq.input_bottom) + Connection.connect(multiply_u_sd.input_left[0].sub_x(0.3), multiply_u_sd.input_left[0]) + Connection.connect(box_ls_2.output_top, multiply_u_sd.input_bottom) + + # Connections between the coordinate transformation and the add blocks + con_3 = Connection.connect( + alpha_beta_to_dq.output_left[0], + add_i_sd.input_bottom[0], + text=r"-", + text_position="end", + text_align="right", + move_text=(-0.2, -0.2), + ) + con_4 = Connection.connect( + alpha_beta_to_dq.output_left[1], + add_i_sq.input_bottom[0], + text=r"-", + text_position="end", + text_align="right", + move_text=(-0.2, -0.2), + ) + + # Connections between the previous conncetions and the inductances blocks + Connection.connect_to_line(con_3, box_ls_1.input_bottom[0]) + Connection.connect_to_line(con_4, box_ls_2.input_bottom[0]) + + # Derivation of the angle + box_d_dt = Box( + Point.merge(alpha_beta_to_dq.position, start.sub_y(6.57020066645696)).sub_x(2), + size=(1, 0.8), + text=r"$\mathrm{d} / \mathrm{d}t$", + inputs=dict(right=1), + outputs=dict(left=1), + ) + + # Conncetion between the derivation and multiplication block + con_omega = Connection.connect( + box_d_dt.output_left, + multiply_u_sq.input_left, + space_y=1, + text=r"$\omega_{\mathrm{el}}$", + move_text=(0, 2), + text_align="left", + distance_x=0.3, + ) + + if control_task == "SC": + # Connector at the previous connection + Circle(con_omega[0].points[1], radius=0.05, fill="black") + + # Conncetion between the coordinate transformations + Connection.connect( + abc_to_alpha_beta.output, + alpha_beta_to_dq.input_right, + text=[r"$i_{\mathrm{s} \upalpha}$", r"$i_{\mathrm{s} \upbeta}$"], + ) + + # Add block for the advanced angle + add = Add( + Point.get_mid(dq_to_abc.position, alpha_beta_to_dq.position), + inputs=dict(bottom=1, right=1), + outputs=dict(top=1), + ) + + # Connections of the add block + Connection.connect( + alpha_beta_to_dq.output_top, + add.input_bottom, + text=r"$\varepsilon_{\mathrm{el}}$", + text_align="right", + move_text=(0, -0.1), + ) + Connection.connect(add.output_top, dq_to_abc.input_bottom) + + # Calculate the advanced angle + box_t_a = Box( + add.position.add_x(1.5), + size=(1, 0.8), + text=r"$1.5 T_{\mathrm{s}}$", + inputs=dict(right=1), + outputs=dict(left=1), + ) + + # Connections of the advanced angle block + Connection.connect(box_t_a.output, add.input_right, text=r"$\Delta \varepsilon$") + Connection.connect( + box_t_a.input[0].add_x(0.5), + box_t_a.input[0], + text=r"$\omega_{\mathrm{el}}$", + text_position="start", + text_align="right", + distance_x=0.3, + ) + + start = pwm.position # starting point of the next block + # Inputs of the stage + inputs = dict( + i_d_ref=[add_i_sd.input_left[0], dict(text=r"$i^{*}_{\mathrm{sd}}$", distance_y=0.25)], + i_q_ref=[add_i_sq.input_left[0], dict(text=r"$i^{*}_{\mathrm{sq}}$", distance_y=0.25)], + epsilon=[box_d_dt.input_right[0], dict()], + ) + outputs = dict(S=pwm.output_right, omega=con_omega[0].points[1]) # Outputs of the stage + # Connections to other lines + connect_to_line = dict( + epsilon=[ + alpha_beta_to_dq.input_bottom[0], + dict(text=r"$\varepsilon_{\mathrm{el}}$", text_position="middle", text_align="right"), + ], + i=[ + abc_to_alpha_beta.input_right, + dict(radius=0.1, fill=False, text=[r"$\mathbf{i}_{\mathrm{s a,b,c}}$", "", ""]), + ], + ) + connections = dict() # Connections + + return start, inputs, outputs, connect_to_line, connections + + return cc_synrm diff --git a/gem_controllers/block_diagrams/stage_blocks/synrm_output.py b/src/gem_controllers/block_diagrams/stage_blocks/synrm_output.py similarity index 74% rename from gem_controllers/block_diagrams/stage_blocks/synrm_output.py rename to src/gem_controllers/block_diagrams/stage_blocks/synrm_output.py index 06415c3a..585196c9 100644 --- a/gem_controllers/block_diagrams/stage_blocks/synrm_output.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/synrm_output.py @@ -26,17 +26,17 @@ def _synrm_output(start, control_task): converter = DcConverter(start.add_x(2.7), input_number=3, input_space=0.3, output_number=3) # SynRM block - synrm = SynRM(converter.position.sub_y(5), size=1.3, input='top') + synrm = SynRM(converter.position.sub_y(5), size=1.3, input="top") # Connection between the converter and the motor block con_1 = Connection.connect(converter.output_bottom, synrm.input_top, arrow=False) - start = synrm.position # starting point of the next block + start = synrm.position # starting point of the next block # Inputs of the stage - inputs = dict(S=[converter.input_left, dict(text=[r'$\mathbf{S}_{\mathrm{a,b,c}}$', '', ''], distance_y=0.25)]) - outputs = dict(epsilon=synrm.output_left[0]) # Outputs of the stage - connect_to_lines = dict() # Connections to other lines - connections = dict(i=con_1) # Connections + inputs = dict(S=[converter.input_left, dict(text=[r"$\mathbf{S}_{\mathrm{a,b,c}}$", "", ""], distance_y=0.25)]) + outputs = dict(epsilon=synrm.output_left[0]) # Outputs of the stage + connect_to_lines = dict() # Connections to other lines + connections = dict(i=con_1) # Connections return start, inputs, outputs, connect_to_lines, connections diff --git a/gem_controllers/cascaded_controller.py b/src/gem_controllers/cascaded_controller.py similarity index 100% rename from gem_controllers/cascaded_controller.py rename to src/gem_controllers/cascaded_controller.py diff --git a/gem_controllers/current_controller.py b/src/gem_controllers/current_controller.py similarity index 100% rename from gem_controllers/current_controller.py rename to src/gem_controllers/current_controller.py diff --git a/gem_controllers/gem_adapter.py b/src/gem_controllers/gem_adapter.py similarity index 97% rename from gem_controllers/gem_adapter.py rename to src/gem_controllers/gem_adapter.py index 2a8ca610..53f273ad 100644 --- a/gem_controllers/gem_adapter.py +++ b/src/gem_controllers/gem_adapter.py @@ -115,9 +115,7 @@ def tune(self, env, env_id, tune_controller=True, **kwargs): ) def build_block_diagram(self, env_id, save_block_diagram_as): - self._block_diagram = gc.build_block_diagram( - self, env_id, save_block_diagram_as - ) + self._block_diagram = gc.build_block_diagram(self, env_id, save_block_diagram_as) def reset(self): """Reset all stages of the controller.""" diff --git a/gem_controllers/gem_controller.py b/src/gem_controllers/gem_controller.py similarity index 96% rename from gem_controllers/gem_controller.py rename to src/gem_controllers/gem_controller.py index d0edf3aa..7814e67c 100644 --- a/gem_controllers/gem_controller.py +++ b/src/gem_controllers/gem_controller.py @@ -141,9 +141,7 @@ def reset(self): def tune(self, env, env_id, **kwargs): pass - def control_environment( - self, env, n_steps, max_episode_length=np.inf, render_env=False - ): + def control_environment(self, env, n_steps, max_episode_length=np.inf, render_env=False): """ Function to control an environment with the GemController. @@ -164,9 +162,7 @@ def control_environment( if render_env: env.render() # Plot the states action = self.control(state, reference) # Calculate the action - (state, reference), _, done, _ = env.step( - action - ) # Simulate one step of the environment + (state, reference), _, done, _ = env.step(action) # Simulate one step of the environment if done or current_episode_length >= max_episode_length: # Reset the environment and controller state, reference = env.reset() diff --git a/src/gem_controllers/parameter_reader.py b/src/gem_controllers/parameter_reader.py new file mode 100644 index 00000000..4a27e78d --- /dev/null +++ b/src/gem_controllers/parameter_reader.py @@ -0,0 +1,439 @@ +from gym_electric_motor.physical_systems import converters as cv + +import numpy as np + +dc_motors = ["SeriesDc", "ShuntDc", "PermExDc", "ExtExDc"] +synchronous_motors = ["PMSM", "SynRM", "EESM"] +induction_motors = ["DFIM", "SCIM"] +ac_motors = synchronous_motors + induction_motors + +control_tasks_ = ["CC", "TC", "SC"] +dc_actions = ["Finite", "Cont"] +ac_actions = ["Finite", "DqCont", "AbcCont"] + + +psi_reader = { + "SeriesDc": lambda env: np.array([0.0]), + "ShuntDc": lambda env: np.array([0.0]), + "PermExDc": lambda env: np.array([env.physical_system.electrical_motor.motor_parameter["psi_e"]]), + "ExtExDc": lambda env: np.array([0.0, 0.0]), + "PMSM": lambda env: np.array([0.0, env.physical_system.electrical_motor.motor_parameter["psi_p"]]), + "SynRM": lambda env: np.array([0.0, 0.0]), + "SCIM": lambda env: np.array([0.0, 0.0]), + "EESM": lambda env: np.array([0.0, 0.0, 0.0]), +} + +p_reader = { + "SeriesDc": lambda env: 1, + "ShuntDc": lambda env: 1, + "ExtExDc": lambda env: 0, + "PermExDc": lambda env: 0, + "PMSM": lambda env: env.physical_system.electrical_motor.motor_parameter["p"], + "SynRM": lambda env: env.physical_system.electrical_motor.motor_parameter["p"], + "SCIM": lambda env: env.physical_system.electrical_motor.motor_parameter["p"], + "EESM": lambda env: env.physical_system.electrical_motor.motor_parameter["p"], +} + +l_reader = { + "SeriesDc": lambda env: np.array( + [ + env.physical_system.electrical_motor.motor_parameter["l_a"] + + env.physical_system.electrical_motor.motor_parameter["l_e"] + ] + ), + "ShuntDc": lambda env: np.array( + [ + env.physical_system.electrical_motor.motor_parameter["l_a"], + ] + ), + "ExtExDc": lambda env: np.array( + [ + env.physical_system.electrical_motor.motor_parameter["l_a"], + env.physical_system.electrical_motor.motor_parameter["l_e"], + ] + ), + "PermExDc": lambda env: np.array( + [ + env.physical_system.electrical_motor.motor_parameter["l_a"], + ] + ), + "PMSM": lambda env: np.array( + [ + env.physical_system.electrical_motor.motor_parameter["l_d"], + env.physical_system.electrical_motor.motor_parameter["l_q"], + ] + ), + "SynRM": lambda env: np.array( + [ + env.physical_system.electrical_motor.motor_parameter["l_d"], + env.physical_system.electrical_motor.motor_parameter["l_q"], + ] + ), + "SCIM": lambda env: np.array( + [ + ( + env.physical_system.electrical_motor.motor_parameter["l_sigr"] + + env.physical_system.electrical_motor.motor_parameter["l_m"] + ) + / env.physical_system.electrical_motor.motor_parameter["r_r"], + ( + env.physical_system.electrical_motor.motor_parameter["l_sigr"] + + env.physical_system.electrical_motor.motor_parameter["l_m"] + ) + / env.physical_system.electrical_motor.motor_parameter["r_r"], + ] + ), + "EESM": lambda env: np.array( + [ + env.physical_system.electrical_motor.motor_parameter["l_d"], + env.physical_system.electrical_motor.motor_parameter["l_q"], + env.physical_system.electrical_motor.motor_parameter["l_e"], + ] + ), +} + +l_emf_reader = { + "SeriesDc": lambda env: np.array([env.physical_system.electrical_motor.motor_parameter["l_e_prime"]]), + "ShuntDc": lambda env: np.array( + [ + env.physical_system.electrical_motor.motor_parameter["l_e_prime"], + ] + ), + "ExtExDc": lambda env: np.array([env.physical_system.electrical_motor.motor_parameter["l_e_prime"], 0.0]), + "PermExDc": lambda env: np.array([0.0]), + "PMSM": lambda env: np.array( + [ + -env.physical_system.electrical_motor.motor_parameter["l_q"], + env.physical_system.electrical_motor.motor_parameter["l_d"], + ] + ), + "SynRM": lambda env: np.array( + [ + -env.physical_system.electrical_motor.motor_parameter["l_q"], + env.physical_system.electrical_motor.motor_parameter["l_d"], + ] + ), + "SCIM": lambda env: np.array( + [ + -( + env.physical_system.electrical_motor.motor_parameter["l_sigs"] + * env.physical_system.electrical_motor.motor_parameter["l_sigr"] + + env.physical_system.electrical_motor.motor_parameter["l_sigs"] + * env.physical_system.electrical_motor.motor_parameter["l_m"] + + env.physical_system.electrical_motor.motor_parameter["l_sigr"] + * env.physical_system.electrical_motor.motor_parameter["l_m"] + ) + / ( + env.physical_system.electrical_motor.motor_parameter["l_sigr"] + + env.physical_system.electrical_motor.motor_parameter["l_m"] + ), + ( + env.physical_system.electrical_motor.motor_parameter["l_sigs"] + * env.physical_system.electrical_motor.motor_parameter["l_sigr"] + + env.physical_system.electrical_motor.motor_parameter["l_sigs"] + * env.physical_system.electrical_motor.motor_parameter["l_m"] + + env.physical_system.electrical_motor.motor_parameter["l_sigr"] + * env.physical_system.electrical_motor.motor_parameter["l_m"] + ) + / ( + env.physical_system.electrical_motor.motor_parameter["l_sigr"] + + env.physical_system.electrical_motor.motor_parameter["l_m"] + ), + ] + ), + "EESM": lambda env: np.array( + [ + -env.physical_system.electrical_motor.motor_parameter["l_q"], + env.physical_system.electrical_motor.motor_parameter["l_d"], + env.physical_system.electrical_motor.motor_parameter["l_m"] + * env.physical_system.electrical_motor.motor_parameter["l_q"] + / env.physical_system.electrical_motor.motor_parameter["l_d"], + ] + ), +} + +tau_current_loop_reader = { + "SeriesDc": lambda env: np.array( + [ + ( + env.physical_system.electrical_motor.motor_parameter["l_e"] + + env.physical_system.electrical_motor.motor_parameter["l_a"] + ) + / ( + env.physical_system.electrical_motor.motor_parameter["r_e"] + + env.physical_system.electrical_motor.motor_parameter["r_a"] + ) + ] + ), + "ShuntDc": lambda env: np.array( + [ + env.physical_system.electrical_motor.motor_parameter["l_a"] + / env.physical_system.electrical_motor.motor_parameter["r_a"] + ] + ), + "ExtExDc": lambda env: np.array( + [ + env.physical_system.electrical_motor.motor_parameter["l_a"] + / env.physical_system.electrical_motor.motor_parameter["r_a"], + env.physical_system.electrical_motor.motor_parameter["l_e"] + / env.physical_system.electrical_motor.motor_parameter["r_e"], + ] + ), + "PermExDc": lambda env: np.array( + env.physical_system.electrical_motor.motor_parameter["l_a"] + / env.physical_system.electrical_motor.motor_parameter["r_a"] + ), + "PMSM": lambda env: np.array( + [ + env.physical_system.electrical_motor.motor_parameter["l_q"] + / env.physical_system.electrical_motor.motor_parameter["r_s"], + env.physical_system.electrical_motor.motor_parameter["l_d"] + / env.physical_system.electrical_motor.motor_parameter["r_s"], + ] + ), + "SynRM": lambda env: np.array( + [ + env.physical_system.electrical_motor.motor_parameter["l_q"] + / env.physical_system.electrical_motor.motor_parameter["r_s"], + env.physical_system.electrical_motor.motor_parameter["l_d"] + / env.physical_system.electrical_motor.motor_parameter["r_s"], + ] + ), + "SCIM": lambda env: np.array( + [ + env.physical_system.electrical_motor.motor_parameter["l_sigs"] + / env.physical_system.electrical_motor.motor_parameter["r_s"], + env.physical_system.electrical_motor.motor_parameter["l_sigr"] + / env.physical_system.electrical_motor.motor_parameter["r_r"], + ] + ), + "EESM": lambda env: np.array( + [ + env.physical_system.electrical_motor.motor_parameter["l_q"] + / env.physical_system.electrical_motor.motor_parameter["r_s"], + env.physical_system.electrical_motor.motor_parameter["l_d"] + / env.physical_system.electrical_motor.motor_parameter["r_s"], + env.physical_system.electrical_motor.motor_parameter["l_e"] + / env.physical_system.electrical_motor.motor_parameter["r_e"], + ] + ), +} + +r_reader = { + "SeriesDc": lambda env: np.array( + [ + env.physical_system.electrical_motor.motor_parameter["r_a"] + + env.physical_system.electrical_motor.motor_parameter["r_e"] + ] + ), + "ShuntDc": lambda env: np.array( + [ + env.physical_system.electrical_motor.motor_parameter["r_a"], + ] + ), + "ExtExDc": lambda env: np.array( + [ + env.physical_system.electrical_motor.motor_parameter["r_a"], + env.physical_system.electrical_motor.motor_parameter["r_e"], + ] + ), + "PermExDc": lambda env: np.array( + [ + env.physical_system.electrical_motor.motor_parameter["r_a"], + ] + ), + "PMSM": lambda env: np.array( + [ + env.physical_system.electrical_motor.motor_parameter["r_s"], + env.physical_system.electrical_motor.motor_parameter["r_s"], + ] + ), + "SynRM": lambda env: np.array( + [ + env.physical_system.electrical_motor.motor_parameter["r_s"], + env.physical_system.electrical_motor.motor_parameter["r_s"], + ] + ), + "SCIM": lambda env: np.array( + [ + env.physical_system.electrical_motor.motor_parameter["r_s"], + env.physical_system.electrical_motor.motor_parameter["r_r"], + ] + ), + "EESM": lambda env: np.array( + [ + env.physical_system.electrical_motor.motor_parameter["r_s"], + env.physical_system.electrical_motor.motor_parameter["r_s"], + env.physical_system.electrical_motor.motor_parameter["r_e"], + ] + ), +} + +tau_n_reader = { + "SeriesDc": lambda env: np.array( + [ + ( + env.physical_system.electrical_motor.motor_parameter["r_a"] + + env.physical_system.electrical_motor.motor_parameter["r_e"] + ) + / ( + env.physical_system.electrical_motor.motor_parameter["l_a"] + + env.physical_system.electrical_motor.motor_parameter["l_e"] + ) + ] + ), + "ShuntDc": lambda env: np.array( + [ + env.physical_system.electrical_motor.motor_parameter["r_a"] + / env.physical_system.electrical_motor.motor_parameter["l_a"], + ] + ), + "ExtExDc": lambda env: np.array( + [ + env.physical_system.electrical_motor.motor_parameter["r_a"] + / env.physical_system.electrical_motor.motor_parameter["l_a"], + env.physical_system.electrical_motor.motor_parameter["r_e"] + / env.physical_system.electrical_motor.motor_parameter["l_e"], + ] + ), + "PermExDc": lambda env: np.array( + [ + env.physical_system.electrical_motor.motor_parameter["r_a"] + / env.physical_system.electrical_motor.motor_parameter["l_a"], + ] + ), + "PMSM": lambda env: np.array( + [ + env.physical_system.electrical_motor.motor_parameter["r_s"] + / env.physical_system.electrical_motor.motor_parameter["l_d"], + env.physical_system.electrical_motor.motor_parameter["r_s"] + / env.physical_system.electrical_motor.motor_parameter["l_q"], + ] + ), + "SynRM": lambda env: np.array( + [ + env.physical_system.electrical_motor.motor_parameter["r_s"] + / env.physical_system.electrical_motor.motor_parameter["l_d"], + env.physical_system.electrical_motor.motor_parameter["r_s"] + / env.physical_system.electrical_motor.motor_parameter["l_q"], + ] + ), + "SCIM": lambda env: np.array( + [ + env.physical_system.electrical_motor.motor_parameter["r_s"] + / env.physical_system.electrical_motor.motor_parameter["l_sigs"], + env.physical_system.electrical_motor.motor_parameter["r_r"] + / env.physical_system.electrical_motor.motor_parameter["l_sigr"], + ] + ), + "EESM": lambda env: np.array( + [ + env.physical_system.electrical_motor.motor_parameter["r_s"] + / env.physical_system.electrical_motor.motor_parameter["l_d"], + env.physical_system.electrical_motor.motor_parameter["r_s"] + / env.physical_system.electrical_motor.motor_parameter["l_q"], + env.physical_system.electrical_motor.motor_parameter["r_e"] + / env.physical_system.electrical_motor.motor_parameter["l_e"], + ] + ), +} + +currents = { + "SeriesDc": ["i"], + "ShuntDc": ["i_a"], + "ExtExDc": ["i_a", "i_e"], + "PermExDc": ["i"], + "PMSM": ["i_sd", "i_sq"], + "SynRM": ["i_sd", "i_sq"], + "SCIM": ["i_sd", "i_sq"], + "EESM": ["i_sd", "i_sq", "i_e"], +} +emf_currents = { + "SeriesDc": ["i"], + "ShuntDc": ["i_e"], + "ExtExDc": ["i_e", "i_a"], + "PermExDc": ["i"], + "PMSM": ["i_sq", "i_sd"], + "SynRM": ["i_sq", "i_sd"], + "SCIM": ["i_sq", "i_sd"], + "EESM": ["i_sq", "i_sd", "i_sq"], +} + + +voltages = { + "SeriesDc": ["u"], + "ShuntDc": ["u"], + "ExtExDc": ["u_a", "u_e"], + "PermExDc": ["u"], + "PMSM": ["u_sd", "u_sq"], + "SynRM": ["u_sd", "u_sq"], + "SCIM": ["u_sd", "u_sq"], + "EESM": ["u_sd", "u_sq", "u_e"], +} + + +def get_output_voltages(motor_type, action_type): + if motor_type in dc_motors: + return voltages[motor_type] + elif motor_type in dc_motors: + return voltages[motor_type] + elif motor_type in dc_motors: + return voltages[motor_type] + elif motor_type in induction_motors: + return ["u_sa", "u_sb", "u_sc"] + elif motor_type == "EESM": + return ["u_a", "u_b", "u_c", "u_sup"] + else: + return ["u_a", "u_b", "u_c"] + + +l_prime_reader = { + "SeriesDc": lambda env: np.array([env.physical_system.electrical_motor.motor_parameter["l_e_prime"]]), + "ShuntDc": lambda env: np.array([env.physical_system.electrical_motor.motor_parameter["l_e_prime"]]), + "ExtExDc": lambda env: np.array([env.physical_system.electrical_motor.motor_parameter["l_e_prime"]]), + "PermExDc": lambda env: np.array([0.0]), + "PMSM": lambda env: np.array([0.0, 0.0]), + "SynRM": lambda env: np.array( + [ + -env.physical_system.electrical_motor.motor_parameter["l_sq"], + env.physical_system.electrical_motor.motor_parameter["l_sd"], + ] + ), + "SCIM": lambda env: np.array([0, 0]), + "EESM": lambda env: np.array([0.0, 0.0, 0.0]), +} + +converter_high_idle_low_action = { + cv.FiniteFourQuadrantConverter: (1, 0, 2), + cv.FiniteTwoQuadrantConverter: (1, 0, 2), + cv.FiniteOneQuadrantConverter: (1, 0, 0), + cv.FiniteB6BridgeConverter: ((1, 0, 2),) * 3, +} + + +class MotorSpecification: + _motors = dict() + + @staticmethod + def register(motor_key): + def reg(motor_specification): + assert isinstance(motor_specification, MotorSpecification) + assert motor_key not in MotorSpecification._motors.keys() + MotorSpecification._motors[motor_key] = motor_specification + + return reg + + @staticmethod + def get(motor_key): + return MotorSpecification._motors[motor_key] + + psi = None + l = None + l_emf = None + tau_current_loop = None + tau_n = None + r = None + l_prime = None + currents = None + emf_currents = None + voltages = None diff --git a/gem_controllers/pi_current_controller.py b/src/gem_controllers/pi_current_controller.py similarity index 89% rename from gem_controllers/pi_current_controller.py rename to src/gem_controllers/pi_current_controller.py index 849b7530..b43e710f 100644 --- a/gem_controllers/pi_current_controller.py +++ b/src/gem_controllers/pi_current_controller.py @@ -9,7 +9,7 @@ class PICurrentController(gc.CurrentController): @property def signal_names(self): """Signal names of the calculated values.""" - return ['u_PI', 'u_ff', 'u_out'] + return ["u_PI", "u_ff", "u_out"] @property def transformation_stage(self): @@ -60,8 +60,7 @@ def clipping_stage(self): @property def t_n(self): """Time constant of the current controller""" - if hasattr(self._current_base_controller, 'p_gain') \ - and hasattr(self._current_base_controller, 'i_gain'): + if hasattr(self._current_base_controller, "p_gain") and hasattr(self._current_base_controller, "i_gain"): return self._current_base_controller.p_gain / self._current_base_controller.i_gain else: return self._tau_current_loop @@ -80,7 +79,7 @@ def referenced_states(self): def maximum_reference(self): return dict() - def __init__(self, env, env_id, base_current_controller='PI', decoupling=True): + def __init__(self, env, env_id, base_current_controller="PI", decoupling=True): """ Initilizes a PI current control stage. @@ -104,20 +103,20 @@ def __init__(self, env, env_id, base_current_controller='PI', decoupling=True): # Choose the emf feedforward function if gc.utils.get_motor_type(env_id) in gc.parameter_reader.induction_motors: self._emf_feedforward = gc.stages.EMFFeedforwardInd() - elif gc.utils.get_motor_type(env_id) == 'EESM': + elif gc.utils.get_motor_type(env_id) == "EESM": self._emf_feedforward = gc.stages.EMFFeedforwardEESM() else: self._emf_feedforward = gc.stages.EMFFeedforward() # Choose the clipping function - if gc.utils.get_motor_type(env_id) == 'EESM': - self._clipping_stage = gc.stages.clipping_stages.CombinedClippingStage('CC') + if gc.utils.get_motor_type(env_id) == "EESM": + self._clipping_stage = gc.stages.clipping_stages.CombinedClippingStage("CC") elif gc.utils.get_motor_type(env_id) in gc.parameter_reader.ac_motors: - self._clipping_stage = gc.stages.clipping_stages.SquaredClippingStage('CC') + self._clipping_stage = gc.stages.clipping_stages.SquaredClippingStage("CC") else: - self._clipping_stage = gc.stages.clipping_stages.AbsoluteClippingStage('CC') - self._anti_windup_stage = gc.stages.AntiWindup('CC') - self._current_base_controller = gc.stages.base_controllers.get(base_current_controller)('CC') + self._clipping_stage = gc.stages.clipping_stages.AbsoluteClippingStage("CC") + self._anti_windup_stage = gc.stages.AntiWindup("CC") + self._current_base_controller = gc.stages.base_controllers.get(base_current_controller)("CC") def tune(self, env, env_id, a=4, **kwargs): """ @@ -131,7 +130,7 @@ def tune(self, env, env_id, a=4, **kwargs): action_type = gc.utils.get_action_type(env_id) motor_type = gc.utils.get_motor_type(env_id) - if action_type in ['Finite', 'Cont'] and motor_type in gc.parameter_reader.ac_motors: + if action_type in ["Finite", "Cont"] and motor_type in gc.parameter_reader.ac_motors: self._coordinate_transformation_required = True if self._coordinate_transformation_required: self._transformation_stage.tune(env, env_id) @@ -171,10 +170,8 @@ def current_control(self, state, current_reference): voltage_reference = self._transformation_stage(state, voltage_reference) # Integrate the I-Controllers - if hasattr(self._current_base_controller, 'integrator'): - delta = self._anti_windup_stage( - state, current_reference, self._clipping_stage.clipping_difference - ) + if hasattr(self._current_base_controller, "integrator"): + delta = self._anti_windup_stage(state, current_reference, self._clipping_stage.clipping_difference) self._current_base_controller.integrator += delta return voltage_reference diff --git a/gem_controllers/pi_speed_controller.py b/src/gem_controllers/pi_speed_controller.py similarity index 92% rename from gem_controllers/pi_speed_controller.py rename to src/gem_controllers/pi_speed_controller.py index d04392e4..d6d3eee4 100644 --- a/gem_controllers/pi_speed_controller.py +++ b/src/gem_controllers/pi_speed_controller.py @@ -48,18 +48,18 @@ def references(self): @property def referenced_states(self): - return np.append(self._torque_controller.referenced_states, 'torque') + return np.append(self._torque_controller.referenced_states, "torque") @property def maximum_reference(self): return self._torque_controller.maximum_reference def __init__( - self, - _env: (gem.core.ElectricMotorEnvironment, None) = None, - env_id: (str, None) = None, - torque_controller: (gc.TorqueController, None) = None, - base_speed_controller: str = 'PI' + self, + _env: (gem.core.ElectricMotorEnvironment, None) = None, + env_id: (str, None) = None, + torque_controller: (gc.TorqueController, None) = None, + base_speed_controller: str = "PI", ): """ Initilizes a PI speed control stage. @@ -72,13 +72,13 @@ def __init__( """ super().__init__() - self._speed_control_stage = gc.stages.base_controllers.get(base_speed_controller)('SC') + self._speed_control_stage = gc.stages.base_controllers.get(base_speed_controller)("SC") self._torque_controller = torque_controller if torque_controller is None: self._torque_controller = gc.TorqueController() self._torque_reference = np.array([]) - self._anti_windup_stage = gc.stages.AntiWindup('SC') - self._clipping_stage = gc.stages.clipping_stages.AbsoluteClippingStage('SC') + self._anti_windup_stage = gc.stages.AntiWindup("SC") + self._clipping_stage = gc.stages.clipping_stages.AbsoluteClippingStage("SC") def tune(self, env, env_id, tune_torque_controller=True, a=4, **kwargs): """ @@ -115,7 +115,7 @@ def speed_control(self, state, reference): # Clipping the torque reference and integrating the I-Controller self._torque_reference = self._clipping_stage(state, torque_reference) - if hasattr(self._speed_control_stage, 'integrator'): + if hasattr(self._speed_control_stage, "integrator"): delta = self._anti_windup_stage.__call__(state, reference, self._clipping_stage.clipping_difference) self._speed_control_stage.integrator += delta diff --git a/gem_controllers/reference_plotter.py b/src/gem_controllers/reference_plotter.py similarity index 82% rename from gem_controllers/reference_plotter.py rename to src/gem_controllers/reference_plotter.py index 995a64f7..76c892cd 100644 --- a/gem_controllers/reference_plotter.py +++ b/src/gem_controllers/reference_plotter.py @@ -3,6 +3,7 @@ class ReferencePlotter: """This class adds the reference values of the subordinate stages to the stage plots of the GEM environment.""" + def __init__(self): self._referenced_plots = [] self._referenced_states = [] @@ -47,14 +48,15 @@ def update_plots(self, references): if not self._maximum_reference_set: for plot in self._referenced_plots: - if plot.state in ['i_e', 'i_a', 'i'] and plot.state in self._maximum_reference.keys(): - label = dict(i='$i^*$$_{\mathrm{ max}}$', i_a='$i^*_{a}$$_\mathrm{ max}}$', - i_e='$i^*_{e}$$_\mathrm{ max}}$') - plot._axis.axhline(self._maximum_reference[plot.state][0], c='g', linewidth=0.75, linestyle='--') - plot._axis.axhline(self._maximum_reference[plot.state][1], c='g', linewidth=0.75, linestyle='--') + if plot.state in ["i_e", "i_a", "i"] and plot.state in self._maximum_reference.keys(): + label = dict( + i="$i^*$$_{\mathrm{ max}}$", i_a="$i^*_{a}$$_\mathrm{ max}}$", i_e="$i^*_{e}$$_\mathrm{ max}}$" + ) + plot._axis.axhline(self._maximum_reference[plot.state][0], c="g", linewidth=0.75, linestyle="--") + plot._axis.axhline(self._maximum_reference[plot.state][1], c="g", linewidth=0.75, linestyle="--") labels = [legend._text for legend in plot._axis.get_legend().texts] + [label[plot.state]] - lines = plot._axis.lines[0:len(labels)-1] + plot._axis.lines[-1:] - plot._axis.legend(lines, labels, loc='upper left', numpoints=20) + lines = plot._axis.lines[0 : len(labels) - 1] + plot._axis.lines[-1:] + plot._axis.legend(lines, labels, loc="upper left", numpoints=20) self._maximum_reference_set = True diff --git a/gem_controllers/stages/__init__.py b/src/gem_controllers/stages/__init__.py similarity index 100% rename from gem_controllers/stages/__init__.py rename to src/gem_controllers/stages/__init__.py diff --git a/gem_controllers/stages/abc_transformation.py b/src/gem_controllers/stages/abc_transformation.py similarity index 88% rename from gem_controllers/stages/abc_transformation.py rename to src/gem_controllers/stages/abc_transformation.py index 04a6adb1..119ecca5 100644 --- a/gem_controllers/stages/abc_transformation.py +++ b/src/gem_controllers/stages/abc_transformation.py @@ -47,7 +47,7 @@ def __call__(self, state, reference): np.array: reference values for the input voltages """ - epsilon_adv = self._angle_advance(state) # calculate the advance angle + epsilon_adv = self._angle_advance(state) # calculate the advance angle output = np.zeros(self._output_len) output[0:3] = SynchronousMotor.t_32(SynchronousMotor.q(reference[0:2], epsilon_adv)) if self._output_len > 3: @@ -67,11 +67,11 @@ def tune(self, env, env_id, **_): env_id(str): The corresponding environment-id to specify the concrete environment. """ if gc.utils.get_motor_type(env_id) in gc.parameter_reader.induction_motors: - self.angle_idx = env.state_names.index('psi_angle') + self.angle_idx = env.state_names.index("psi_angle") else: - self.angle_idx = env.state_names.index('epsilon') - self.omega_idx = env.state_names.index('omega') - if hasattr(env.physical_system.converter, 'dead_time'): + self.angle_idx = env.state_names.index("epsilon") + self.omega_idx = env.state_names.index("omega") + if hasattr(env.physical_system.converter, "dead_time"): self._advance_factor = 1.5 if env.physical_system.converter.dead_time else 0.5 else: self._advance_factor = 0.5 diff --git a/gem_controllers/stages/anti_windup.py b/src/gem_controllers/stages/anti_windup.py similarity index 90% rename from gem_controllers/stages/anti_windup.py rename to src/gem_controllers/stages/anti_windup.py index d5084855..9ad24622 100644 --- a/gem_controllers/stages/anti_windup.py +++ b/src/gem_controllers/stages/anti_windup.py @@ -10,7 +10,7 @@ class AntiWindup: limits are integrated. """ - def __init__(self, control_task='CC'): + def __init__(self, control_task="CC"): """ Args: control_task(str): Control task of the controller. @@ -30,12 +30,12 @@ def tune(self, env, env_id): self._tau = env.physical_system.tau motor_type = gc.utils.get_motor_type(env_id) states = [] - if self._control_task == 'CC': + if self._control_task == "CC": states = gc.parameter_reader.currents[motor_type] - elif self._control_task == 'TC': - states = ['torque'] - elif self._control_task == 'SC': - states = ['omega'] + elif self._control_task == "TC": + states = ["torque"] + elif self._control_task == "SC": + states = ["omega"] self._state_indices = [env.state_names.index(state) for state in states] def __call__(self, state, reference, clipping_difference): diff --git a/gem_controllers/stages/base_controllers/__init__.py b/src/gem_controllers/stages/base_controllers/__init__.py similarity index 100% rename from gem_controllers/stages/base_controllers/__init__.py rename to src/gem_controllers/stages/base_controllers/__init__.py diff --git a/gem_controllers/stages/base_controllers/base_controller.py b/src/gem_controllers/stages/base_controllers/base_controller.py similarity index 96% rename from gem_controllers/stages/base_controllers/base_controller.py rename to src/gem_controllers/stages/base_controllers/base_controller.py index 340e7fec..c3dc2bb4 100644 --- a/gem_controllers/stages/base_controllers/base_controller.py +++ b/src/gem_controllers/stages/base_controllers/base_controller.py @@ -1,5 +1,5 @@ from ..stage import Stage -from.e_base_controller_task import EBaseControllerTask +from .e_base_controller_task import EBaseControllerTask class BaseController(Stage): diff --git a/gem_controllers/stages/base_controllers/e_base_controller_task.py b/src/gem_controllers/stages/base_controllers/e_base_controller_task.py similarity index 67% rename from gem_controllers/stages/base_controllers/e_base_controller_task.py rename to src/gem_controllers/stages/base_controllers/e_base_controller_task.py index 5d2d3f1c..0b15dce7 100644 --- a/gem_controllers/stages/base_controllers/e_base_controller_task.py +++ b/src/gem_controllers/stages/base_controllers/e_base_controller_task.py @@ -2,14 +2,14 @@ class EBaseControllerTask(Enum): - CC = 'CC' - CurrentControl = 'CC' - TC = 'TC' - TorqueControl = 'TC' - SC = 'SC' - SpeedControl = 'SC' - FC = 'FC' - FluxControl = 'FC' + CC = "CC" + CurrentControl = "CC" + TC = "TC" + TorqueControl = "TC" + SC = "SC" + SpeedControl = "SC" + FC = "FC" + FluxControl = "FC" @staticmethod def equals(value, base_controller_task): @@ -26,5 +26,5 @@ def get_control_task(value): return value else: raise Exception( - f'The type of the control task has to be string or EBaseControllerTask but is {type(value)}.' + f"The type of the control task has to be string or EBaseControllerTask but is {type(value)}." ) diff --git a/gem_controllers/stages/base_controllers/i_controller.py b/src/gem_controllers/stages/base_controllers/i_controller.py similarity index 100% rename from gem_controllers/stages/base_controllers/i_controller.py rename to src/gem_controllers/stages/base_controllers/i_controller.py diff --git a/gem_controllers/stages/base_controllers/p_controller.py b/src/gem_controllers/stages/base_controllers/p_controller.py similarity index 96% rename from gem_controllers/stages/base_controllers/p_controller.py rename to src/gem_controllers/stages/base_controllers/p_controller.py index a4f9ebeb..b95a8b85 100644 --- a/gem_controllers/stages/base_controllers/p_controller.py +++ b/src/gem_controllers/stages/base_controllers/p_controller.py @@ -9,6 +9,7 @@ class PController(BaseController): """This class represents an proportional controller, which can be combined e.g. with a integration controller to a PI controller. """ + @property def p_gain(self): """P gain of the P controller""" @@ -93,7 +94,7 @@ def tune(self, env, env_id, a=4): elif self._control_task == EBaseControllerTask.SpeedControl: self._tune_speed_controller(env, env_id, a) else: - raise Exception(f'No Tuner available for control task{self._control_task}.') + raise Exception(f"No Tuner available for control task{self._control_task}.") def _tune_current_controller(self, env, env_id, a): """ @@ -135,13 +136,13 @@ def _tune_speed_controller(self, env, env_id, a=4, t_n=None): if t_n is None: t_n = env.physical_system.tau j_total = env.physical_system.mechanical_load.j_total - torque_index = env.state_names.index('torque') - speed_index = env.state_names.index('omega') + torque_index = env.state_names.index("torque") + speed_index = env.state_names.index("omega") torque_limit = env.limits[torque_index] p_gain = j_total / (a * t_n) self.p_gain = np.array([p_gain]) self.state_indices = [speed_index] self.action_range = ( env.observation_space[0].low[[torque_index]] * np.array([torque_limit]), - env.observation_space[0].high[[torque_index]] * np.array([torque_limit]) + env.observation_space[0].high[[torque_index]] * np.array([torque_limit]), ) diff --git a/gem_controllers/stages/base_controllers/pi_controller.py b/src/gem_controllers/stages/base_controllers/pi_controller.py similarity index 94% rename from gem_controllers/stages/base_controllers/pi_controller.py rename to src/gem_controllers/stages/base_controllers/pi_controller.py index eb23827a..4d77a1b3 100644 --- a/gem_controllers/stages/base_controllers/pi_controller.py +++ b/src/gem_controllers/stages/base_controllers/pi_controller.py @@ -30,8 +30,9 @@ def control(self, state, reference): """ filtered_state = state[self._state_indices] - action = PController._control(self, filtered_state, reference) \ - + IController._control(self, filtered_state, reference) + action = PController._control(self, filtered_state, reference) + IController._control( + self, filtered_state, reference + ) return action def tune(self, env, env_id, a=4, t_n=None): @@ -52,7 +53,7 @@ def tune(self, env, env_id, a=4, t_n=None): elif self._control_task == EBaseControllerTask.FC: self._tune_flux_controller(env, env_id, a, t_n) else: - raise Exception(f'No tuning method available.') + raise Exception(f"No tuning method available.") def _tune_current_controller(self, env, env_id, a=4): """ @@ -72,7 +73,7 @@ def _tune_current_controller(self, env, env_id, a=4): voltage_indices = [env.state_names.index(voltage) for voltage in voltages] current_indices = [env.state_names.index(current) for current in currents] voltage_limits = env.limits[voltage_indices] - i_gain = self.p_gain / (tau * a ** 2) + i_gain = self.p_gain / (tau * a**2) action_range = ( env.observation_space[0].low[voltage_indices] * voltage_limits, @@ -97,7 +98,7 @@ def _tune_speed_controller(self, env, env_id, a=4, t_n=None): PController._tune_speed_controller(self, env, env_id, a, t_n) self.i_gain = self.p_gain / (a * t_n) self.tau = env.physical_system.tau - speed_index = env.state_names.index('omega') + speed_index = env.state_names.index("omega") self.state_indices = [speed_index] def _tune_flux_controller(self, env, env_id, a=4, t_n=None): @@ -112,6 +113,6 @@ def _tune_flux_controller(self, env, env_id, a=4, t_n=None): """ self.tau = env.physical_system.tau - self.p_gain = np.array([a * t_n ** 2]) + self.p_gain = np.array([a * t_n**2]) self.i_gain = self.p_gain / self.tau self.state_indices = [0] diff --git a/gem_controllers/stages/base_controllers/pid_controller.py b/src/gem_controllers/stages/base_controllers/pid_controller.py similarity index 99% rename from gem_controllers/stages/base_controllers/pid_controller.py rename to src/gem_controllers/stages/base_controllers/pid_controller.py index 3a3d06ba..f91f6099 100644 --- a/gem_controllers/stages/base_controllers/pid_controller.py +++ b/src/gem_controllers/stages/base_controllers/pid_controller.py @@ -66,4 +66,3 @@ def _tune_speed_controller(self, env, env_id, a=4, t_n=None): super()._tune_speed_controller(env, env_id, a) self.d_gain = self.p_gain * self.tau - diff --git a/gem_controllers/stages/base_controllers/three_point_controller.py b/src/gem_controllers/stages/base_controllers/three_point_controller.py similarity index 97% rename from gem_controllers/stages/base_controllers/three_point_controller.py rename to src/gem_controllers/stages/base_controllers/three_point_controller.py index 065326f0..db01cde3 100644 --- a/gem_controllers/stages/base_controllers/three_point_controller.py +++ b/src/gem_controllers/stages/base_controllers/three_point_controller.py @@ -106,7 +106,7 @@ def tune(self, env, env_id, **base_controller_kwargs): elif self._control_task == EBaseControllerTask.CC: self._tune_current_controller(env, env_id) else: - raise Exception(f'No tuner available for control_task {self._control_task}.') + raise Exception(f"No tuner available for control_task {self._control_task}.") def _tune_current_controller(self, env, env_id): """ @@ -145,7 +145,7 @@ def _tune_speed_controller(self, env, _env_id): _env_id(str): The corresponding environment-id to specify the concrete environment. """ - torque_index = [env.state_names.index('reference')] + torque_index = [env.state_names.index("reference")] torque_limit = env.limits[torque_index] self.referenced_state_indices = torque_index action_range = ( diff --git a/gem_controllers/stages/base_controllers/utils.py b/src/gem_controllers/stages/base_controllers/utils.py similarity index 62% rename from gem_controllers/stages/base_controllers/utils.py rename to src/gem_controllers/stages/base_controllers/utils.py index aa44c5ec..a61cdf9c 100644 --- a/gem_controllers/stages/base_controllers/utils.py +++ b/src/gem_controllers/stages/base_controllers/utils.py @@ -7,9 +7,9 @@ def get(base_controller_id: str): _base_controller_registry = { - 'P': bc.PController, - 'I': bc.IController, - 'PI': bc.PIController, - 'PID': bc.PIDController, - 'ThreePoint': bc.ThreePointController -} \ No newline at end of file + "P": bc.PController, + "I": bc.IController, + "PI": bc.PIController, + "PID": bc.PIDController, + "ThreePoint": bc.ThreePointController, +} diff --git a/gem_controllers/stages/clipping_stages/__init__.py b/src/gem_controllers/stages/clipping_stages/__init__.py similarity index 100% rename from gem_controllers/stages/clipping_stages/__init__.py rename to src/gem_controllers/stages/clipping_stages/__init__.py diff --git a/gem_controllers/stages/clipping_stages/absolute_clipping_stage.py b/src/gem_controllers/stages/clipping_stages/absolute_clipping_stage.py similarity index 89% rename from gem_controllers/stages/clipping_stages/absolute_clipping_stage.py rename to src/gem_controllers/stages/clipping_stages/absolute_clipping_stage.py index 5b54dc09..97e5ad1a 100644 --- a/gem_controllers/stages/clipping_stages/absolute_clipping_stage.py +++ b/src/gem_controllers/stages/clipping_stages/absolute_clipping_stage.py @@ -18,7 +18,7 @@ def action_range(self) -> Tuple[np.ndarray, np.ndarray]: """Action range of the controller stage""" return self._action_range - def __init__(self, control_task='CC'): + def __init__(self, control_task="CC"): """ Args: control_task(str): Control task of the controller stage. @@ -54,14 +54,14 @@ def tune(self, env, env_id, margin=0.0, **kwargs): """ motor_type = gc.utils.get_motor_type(env_id) - if self._control_task == 'CC': + if self._control_task == "CC": action_names = gc.parameter_reader.voltages[motor_type] - elif self._control_task == 'TC': + elif self._control_task == "TC": action_names = gc.parameter_reader.currents[motor_type] - elif self._control_task == 'SC': - action_names = ['torque'] + elif self._control_task == "SC": + action_names = ["torque"] else: - raise AttributeError(f'Control task is {self._control_task} but has to be one of [SC, TC, CC].') + raise AttributeError(f"Control task is {self._control_task} but has to be one of [SC, TC, CC].") action_indices = [env.state_names.index(action_name) for action_name in action_names] limits = env.limits[action_indices] * (1 - margin) state_space = env.observation_space[0] diff --git a/gem_controllers/stages/clipping_stages/clipping_stage.py b/src/gem_controllers/stages/clipping_stages/clipping_stage.py similarity index 100% rename from gem_controllers/stages/clipping_stages/clipping_stage.py rename to src/gem_controllers/stages/clipping_stages/clipping_stage.py diff --git a/gem_controllers/stages/clipping_stages/combined_clipping_stage.py b/src/gem_controllers/stages/clipping_stages/combined_clipping_stage.py similarity index 75% rename from gem_controllers/stages/clipping_stages/combined_clipping_stage.py rename to src/gem_controllers/stages/clipping_stages/combined_clipping_stage.py index 4fbec75d..573a687e 100644 --- a/gem_controllers/stages/clipping_stages/combined_clipping_stage.py +++ b/src/gem_controllers/stages/clipping_stages/combined_clipping_stage.py @@ -22,7 +22,7 @@ def action_range(self) -> Tuple[np.ndarray, np.ndarray]: action_range_high[-1] = self._action_range_absolute[1][0] return action_range_low, action_range_high - def __init__(self, control_task='CC'): + def __init__(self, control_task="CC"): self._action_range_absolute = np.array([]), np.array([]) self._limit_squred_clipping = np.array([]) self._clipping_difference = np.array([]) @@ -33,20 +33,24 @@ def __init__(self, control_task='CC'): def __call__(self, state, reference): clipped = np.zeros(np.size(self._squared_clipped_states) + np.size(self._absolute_clipped_states)) - relative_reference_length = np.sum((reference[self._squared_clipped_states]/self._limit_squred_clipping)**2) + relative_reference_length = np.sum((reference[self._squared_clipped_states] / self._limit_squred_clipping) ** 2) relative_maximum = 1 - self._margin - clipped[self._squared_clipped_states] = reference[self._squared_clipped_states] \ - if relative_reference_length < relative_maximum**2 \ + clipped[self._squared_clipped_states] = ( + reference[self._squared_clipped_states] + if relative_reference_length < relative_maximum**2 else reference[self._squared_clipped_states] / relative_reference_length * relative_maximum + ) - clipped[self._absolute_clipped_states] = np.clip(reference[self._absolute_clipped_states], - self._action_range_absolute[0], self._action_range_absolute[1]) + clipped[self._absolute_clipped_states] = np.clip( + reference[self._absolute_clipped_states], self._action_range_absolute[0], self._action_range_absolute[1] + ) self._clipping_difference = reference - clipped return clipped - def tune(self, env, env_id, margin=0.0, squared_clipped_state=np.array([0, 1]), - absoulte_clipped_states=np.array([2])): + def tune( + self, env, env_id, margin=0.0, squared_clipped_state=np.array([0, 1]), absoulte_clipped_states=np.array([2]) + ): """ Set the limits for the clipped states. @@ -63,25 +67,28 @@ def tune(self, env, env_id, margin=0.0, squared_clipped_state=np.array([0, 1]), self._margin = margin motor_type = gc.utils.get_motor_type(env_id) - if self._control_task == 'CC': + if self._control_task == "CC": action_names = gc.parameter_reader.voltages[motor_type] - elif self._control_task == 'TC': + elif self._control_task == "TC": action_names = gc.parameter_reader.currents[motor_type] - elif self._control_task == 'SC': - action_names = ['torque'] + elif self._control_task == "SC": + action_names = ["torque"] else: - raise AttributeError(f'Control task is {self._control_task} but has to be one of [SC, TC, CC].') + raise AttributeError(f"Control task is {self._control_task} but has to be one of [SC, TC, CC].") action_indices = [env.state_names.index(action_name) for action_name in action_names] limits = env.limits[action_indices] * (1 - margin) state_space = env.observation_space[0] lower_action_limit = state_space.low[action_indices] * limits upper_action_limit = state_space.high[action_indices] * limits self._limit_squred_clipping = limits[squared_clipped_state] - self._action_range_absolute = lower_action_limit[absoulte_clipped_states], upper_action_limit[ - absoulte_clipped_states] + self._action_range_absolute = ( + lower_action_limit[absoulte_clipped_states], + upper_action_limit[absoulte_clipped_states], + ) self._clipping_difference = np.zeros_like(lower_action_limit) def reset(self): """Reset the combined clipping stage""" self._clipping_difference = np.zeros( - np.size(self._squared_clipped_states) + np.size(self._absolute_clipped_states)) + np.size(self._squared_clipped_states) + np.size(self._absolute_clipped_states) + ) diff --git a/gem_controllers/stages/clipping_stages/squared_clipping_stage.py b/src/gem_controllers/stages/clipping_stages/squared_clipping_stage.py similarity index 84% rename from gem_controllers/stages/clipping_stages/squared_clipping_stage.py rename to src/gem_controllers/stages/clipping_stages/squared_clipping_stage.py index 62855727..dcfa73af 100644 --- a/gem_controllers/stages/clipping_stages/squared_clipping_stage.py +++ b/src/gem_controllers/stages/clipping_stages/squared_clipping_stage.py @@ -5,8 +5,7 @@ class SquaredClippingStage(ClippingStage): - """This class clips multiple references together, by clipping the vector length of the references to a scalar limit. - """ + """This class clips multiple references together, by clipping the vector length of the references to a scalar limit.""" @property def clipping_difference(self) -> np.ndarray: @@ -28,7 +27,7 @@ def action_range(self): """Action range of the controller stage""" return [] - def __init__(self, control_task='CC'): + def __init__(self, control_task="CC"): """ Args: control_task(str): Control task of the controller stage. @@ -51,11 +50,13 @@ def __call__(self, state, reference): clipped_reference(np.ndarray): The reference of a controller stage clipped to the limit. """ - relative_reference_length = np.sum((reference/self._limits)**2) + relative_reference_length = np.sum((reference / self._limits) ** 2) relative_maximum = 1 - self._margin - clipped = reference \ - if relative_reference_length < relative_maximum**2 \ + clipped = ( + reference + if relative_reference_length < relative_maximum**2 else reference / relative_reference_length * relative_maximum + ) self._clipping_difference = reference - clipped return clipped @@ -71,12 +72,12 @@ def tune(self, env, env_id, margin=0.0, **kwargs): motor_type = gc.utils.get_motor_type(env_id) state_names = [] - if self._control_task == 'CC': + if self._control_task == "CC": state_names = gc.parameter_reader.voltages[motor_type] - elif self._control_task == 'TC': + elif self._control_task == "TC": state_names = gc.parameter_reader.currents[motor_type] - elif self._control_task == 'SC': - state_names = ['torque'] + elif self._control_task == "SC": + state_names = ["torque"] state_indices = [env.state_names.index(state_name) for state_name in state_names] self._limits = env.limits[state_indices] diff --git a/gem_controllers/stages/cont_output_stage.py b/src/gem_controllers/stages/cont_output_stage.py similarity index 95% rename from gem_controllers/stages/cont_output_stage.py rename to src/gem_controllers/stages/cont_output_stage.py index ea0d7c95..cca096fe 100644 --- a/gem_controllers/stages/cont_output_stage.py +++ b/src/gem_controllers/stages/cont_output_stage.py @@ -22,7 +22,7 @@ def __init__(self): self._voltage_limit = np.array([]) def __call__(self, state, reference): - """"Divide the input voltages by the limits""" + """ "Divide the input voltages by the limits""" return reference / self.voltage_limit def tune(self, env, env_id, **_): diff --git a/gem_controllers/stages/disc_output_stage.py b/src/gem_controllers/stages/disc_output_stage.py similarity index 93% rename from gem_controllers/stages/disc_output_stage.py rename to src/gem_controllers/stages/disc_output_stage.py index a214bc0e..8deafde1 100644 --- a/gem_controllers/stages/disc_output_stage.py +++ b/src/gem_controllers/stages/disc_output_stage.py @@ -82,9 +82,7 @@ def to_action(self, _state, reference): action(np.ndarray): volatge vector """ conditions = [reference <= self.low_level, reference >= self.high_level] - return np.select( - conditions, [self.low_action, self.high_action], default=self.idle_action - ) + return np.select(conditions, [self.low_action, self.high_action], default=self.idle_action) def tune(self, env, env_id, **__): """ @@ -124,15 +122,11 @@ def tune(self, env, env_id, **__): and type(env.physical_system.converter) != cv.FiniteB6BridgeConverter ): self.output_stage = DiscOutputStage.to_discrete - self.low_action, self.idle_action, self.high_action = self._get_actions( - env.action_space.n - ) + self.low_action, self.idle_action, self.high_action = self._get_actions(env.action_space.n) elif type(env.physical_system.converter) == cv.FiniteB6BridgeConverter: self.output_stage = DiscOutputStage.to_b6_discrete else: - raise Exception( - f"No discrete output stage available for action space {env.action_space}." - ) + raise Exception(f"No discrete output stage available for action space {env.action_space}.") @staticmethod def _get_actions(n): diff --git a/gem_controllers/stages/emf_feedforward.py b/src/gem_controllers/stages/emf_feedforward.py similarity index 98% rename from gem_controllers/stages/emf_feedforward.py rename to src/gem_controllers/stages/emf_feedforward.py index d940f7c6..2f1b1bba 100644 --- a/gem_controllers/stages/emf_feedforward.py +++ b/src/gem_controllers/stages/emf_feedforward.py @@ -94,7 +94,7 @@ def tune(self, env, env_id, **_): """ motor_type = gc.utils.get_motor_type(env_id) - omega_idx = env.state_names.index('omega') + omega_idx = env.state_names.index("omega") current_indices = [env.state_names.index(current) for current in reader.emf_currents[motor_type]] self.omega_idx = omega_idx self.current_indices = current_indices diff --git a/gem_controllers/stages/emf_feedforward_eesm.py b/src/gem_controllers/stages/emf_feedforward_eesm.py similarity index 91% rename from gem_controllers/stages/emf_feedforward_eesm.py rename to src/gem_controllers/stages/emf_feedforward_eesm.py index 1fec86d5..06802707 100644 --- a/gem_controllers/stages/emf_feedforward_eesm.py +++ b/src/gem_controllers/stages/emf_feedforward_eesm.py @@ -7,6 +7,7 @@ class EMFFeedforwardEESM(EMFFeedforward): """ This class extends the functions of the EMFFeedforward class to decouple the dq-components of the induction motor. """ + def __init__(self): super().__init__() self._l_m = None @@ -44,15 +45,15 @@ def tune(self, env, env_id, **_): """ super().tune(env, env_id, **_) - l_m = env.physical_system.electrical_motor.motor_parameter['l_m'] - l_d = env.physical_system.electrical_motor.motor_parameter['l_d'] - l_e = env.physical_system.electrical_motor.motor_parameter['l_e'] - r_s = env.physical_system.electrical_motor.motor_parameter['r_s'] - r_e = env.physical_system.electrical_motor.motor_parameter['r_e'] + l_m = env.physical_system.electrical_motor.motor_parameter["l_m"] + l_d = env.physical_system.electrical_motor.motor_parameter["l_d"] + l_e = env.physical_system.electrical_motor.motor_parameter["l_e"] + r_s = env.physical_system.electrical_motor.motor_parameter["r_s"] + r_e = env.physical_system.electrical_motor.motor_parameter["r_e"] self._l_m = l_m - self._i_e_idx = env.state_names.index('i_e') + self._i_e_idx = env.state_names.index("i_e") self._decoupling_params = np.array([-l_m * r_e / l_e, 0, -l_m * r_s / l_d]) self._action_decoupling = np.array([l_m / l_e, 0, l_m / l_d]) - self._currents_idx = [env.state_names.index('i_e'), 0, env.state_names.index('i_sd')] + self._currents_idx = [env.state_names.index("i_e"), 0, env.state_names.index("i_sd")] self._action_idx = [2, 1, 0] diff --git a/gem_controllers/stages/emf_feedforward_ind.py b/src/gem_controllers/stages/emf_feedforward_ind.py similarity index 71% rename from gem_controllers/stages/emf_feedforward_ind.py rename to src/gem_controllers/stages/emf_feedforward_ind.py index 2143bc45..7267ba8f 100644 --- a/gem_controllers/stages/emf_feedforward_ind.py +++ b/src/gem_controllers/stages/emf_feedforward_ind.py @@ -6,6 +6,7 @@ class EMFFeedforwardInd(EMFFeedforward): """ This class extends the functions of the EMFFeedforward class to decouple the dq-components of the induction motor. """ + def __init__(self): super().__init__() self.r_r = None @@ -29,11 +30,16 @@ def __call__(self, state, reference): # Calculate the stator angular velocity omega_s = state[self.omega_idx] + self.r_r * self.l_m / self.l_r * state[self.i_sq_idx] / max( - np.abs(state[self.psi_abs_idx]), 1e-4) * np.sign(state[self.psi_abs_idx]) + np.abs(state[self.psi_abs_idx]), 1e-4 + ) * np.sign(state[self.psi_abs_idx]) # Calculate the decoupling of the components - action = reference + omega_s * self.inductance * state[self.current_indices] + np.array( - [- self.l_m * self.r_r / (self.l_r ** 2), state[self.omega_idx] * self.l_m / self.l_r]) * state[self.psi_abs_idx] + action = ( + reference + + omega_s * self.inductance * state[self.current_indices] + + np.array([-self.l_m * self.r_r / (self.l_r**2), state[self.omega_idx] * self.l_m / self.l_r]) + * state[self.psi_abs_idx] + ) return action @@ -48,8 +54,8 @@ def tune(self, env, env_id, **_): super().tune(env, env_id, **_) mp = env.physical_system.electrical_motor.motor_parameter - self.r_r = mp['r_r'] - self.l_m = mp['l_m'] - self.l_r = mp['l_sigr'] + self.l_m - self.i_sq_idx = env.state_names.index('i_sq') - self.psi_abs_idx = env.state_names.index('psi_abs') + self.r_r = mp["r_r"] + self.l_m = mp["l_m"] + self.l_r = mp["l_sigr"] + self.l_m + self.i_sq_idx = env.state_names.index("i_sq") + self.psi_abs_idx = env.state_names.index("psi_abs") diff --git a/gem_controllers/stages/input_stage.py b/src/gem_controllers/stages/input_stage.py similarity index 100% rename from gem_controllers/stages/input_stage.py rename to src/gem_controllers/stages/input_stage.py diff --git a/gem_controllers/stages/operation_point_selection/__init__.py b/src/gem_controllers/stages/operation_point_selection/__init__.py similarity index 100% rename from gem_controllers/stages/operation_point_selection/__init__.py rename to src/gem_controllers/stages/operation_point_selection/__init__.py diff --git a/gem_controllers/stages/operation_point_selection/eesm_ops.py b/src/gem_controllers/stages/operation_point_selection/eesm_ops.py similarity index 70% rename from gem_controllers/stages/operation_point_selection/eesm_ops.py rename to src/gem_controllers/stages/operation_point_selection/eesm_ops.py index aca89ffb..51e6e62a 100644 --- a/gem_controllers/stages/operation_point_selection/eesm_ops.py +++ b/src/gem_controllers/stages/operation_point_selection/eesm_ops.py @@ -58,16 +58,16 @@ def tune(self, env, env_id, current_safety_margin=0.2): """ super().tune(env, env_id, current_safety_margin) - self.l_d = self.mp['l_d'] - self.l_q = self.mp['l_q'] - self.l_m = self.mp['l_m'] - self.l_e = self.mp['l_e'] - self.r_s = self.mp['r_s'] - self.r_e = self.mp['r_e'] - self.p = self.mp['p'] - self.i_e_lim = env.limits[env.state_names.index('i_e')] * (1 - current_safety_margin) - self.i_q_lim = env.limits[env.state_names.index('i_sq')] * (1 - current_safety_margin) - self.t_lim = env.limits[env.state_names.index('torque')] + self.l_d = self.mp["l_d"] + self.l_q = self.mp["l_q"] + self.l_m = self.mp["l_m"] + self.l_e = self.mp["l_e"] + self.r_s = self.mp["r_s"] + self.r_e = self.mp["r_e"] + self.p = self.mp["p"] + self.i_e_lim = env.limits[env.state_names.index("i_e")] * (1 - current_safety_margin) + self.i_q_lim = env.limits[env.state_names.index("i_sq")] * (1 - current_safety_margin) + self.t_lim = env.limits[env.state_names.index("torque")] self.t_count = 50 self.psi_count = 100 @@ -77,32 +77,32 @@ def tune(self, env, env_id, current_safety_margin=0.2): self.psi_grid_count = 200 self.k_ = 0.953 - self.i_gain = 1 / (self.l_q / (1.25 * self.r_s)) * (self.alpha - 1) / self.alpha ** 2 + self.i_gain = 1 / (self.l_q / (1.25 * self.r_s)) * (self.alpha - 1) / self.alpha**2 self.psi_high = 0.2 * np.sqrt( - (self.l_m * self.i_e_lim * current_safety_margin + self.l_d * self.i_sq_limit * current_safety_margin) ** 2) + (self.l_m * self.i_e_lim * current_safety_margin + self.l_d * self.i_sq_limit * current_safety_margin) ** 2 + ) self.psi_low = -self.psi_high self.integrated_reset = 0.01 * self.psi_low - self.torque_equation = lambda i_d, i_q, i_e: 3 / 2 * self.p * ( - self.l_m * i_e + (self.l_d - self.l_q) * i_d) * i_q + self.torque_equation = ( + lambda i_d, i_q, i_e: 3 / 2 * self.p * (self.l_m * i_e + (self.l_d - self.l_q) * i_d) * i_q + ) self.loss = lambda i_d, i_q, i_e: np.abs(i_d) * self.r_s + np.abs(i_q) * self.r_s + np.abs(i_e) * self.r_e - self.poly = lambda i_e, psi, torque: [self.l_d ** 2 * (self.l_d - self.l_q) ** 2, - 2 * self.l_d ** 2 * ( - self.l_d - self.l_q) * self.l_m * i_e + 2 * self.l_d * self.l_m * i_e * ( - self.l_d - self.l_q) ** 2, - self.l_d ** 2 * (self.l_m * i_e) ** 2 + 4 * self.l_d * ( - self.l_m * i_e) ** 2 * (self.l_d - self.l_q) + ( - (self.l_m * i_e) ** 2 - psi ** 2) * ( - self.l_d - self.l_q) ** 2, - 2 * self.l_q * (self.l_m * i_e) ** 3 + 2 * ( - (self.l_m * i_e) ** 2 - psi ** 2) * self.l_m * i_e * ( - self.l_d - self.l_q), - ((self.l_m * i_e) ** 2 - psi ** 2) * (self.l_m * i_e) ** 2 + ( - self.l_q * torque / (3 * self.p)) ** 2] + self.poly = lambda i_e, psi, torque: [ + self.l_d**2 * (self.l_d - self.l_q) ** 2, + 2 * self.l_d**2 * (self.l_d - self.l_q) * self.l_m * i_e + + 2 * self.l_d * self.l_m * i_e * (self.l_d - self.l_q) ** 2, + self.l_d**2 * (self.l_m * i_e) ** 2 + + 4 * self.l_d * (self.l_m * i_e) ** 2 * (self.l_d - self.l_q) + + ((self.l_m * i_e) ** 2 - psi**2) * (self.l_d - self.l_q) ** 2, + 2 * self.l_q * (self.l_m * i_e) ** 3 + + 2 * ((self.l_m * i_e) ** 2 - psi**2) * self.l_m * i_e * (self.l_d - self.l_q), + ((self.l_m * i_e) ** 2 - psi**2) * (self.l_m * i_e) ** 2 + (self.l_q * torque / (3 * self.p)) ** 2, + ] self._calculate_luts() @@ -151,7 +151,7 @@ def _calculate_luts(self): parameter_psi = [] for i_e in np.linspace(0, self.i_e_lim, self.i_e_count): i_d, i_q = self.solve_analytical(t, psi, i_e) - if np.sqrt(i_d ** 2 + i_q ** 2) < self.i_q_lim: + if np.sqrt(i_d**2 + i_q**2) < self.i_q_lim: loss = self.loss(i_d, i_q, i_e) params = np.array([t, psi, i_d, i_q, i_e]) losses.append(loss) @@ -169,25 +169,38 @@ def _calculate_luts(self): best_params = np.array(best_params) best_params_psi = np.array(best_params_psi) - self.t_max_psi = sp_interpolate.interp1d(np.linspace(0, self.psi_max, self.psi_count), 0.99 * self.t_max_psi, kind='linear') + self.t_max_psi = sp_interpolate.interp1d( + np.linspace(0, self.psi_max, self.psi_count), 0.99 * self.t_max_psi, kind="linear" + ) self.t_max = np.max(best_params[:, 0]) - self.psi_opt = sp_interpolate.interp1d(best_params[:, 0], best_params[:, 1], kind='cubic') - self.i_d_opt = sp_interpolate.interp1d(best_params[:, 0], best_params[:, 2], kind='cubic') - self.i_q_opt = sp_interpolate.interp1d(best_params[:, 0], best_params[:, 3], kind='cubic') - self.i_e_opt = sp_interpolate.interp1d(best_params[:, 0], best_params[:, 4], kind='cubic') + self.psi_opt = sp_interpolate.interp1d(best_params[:, 0], best_params[:, 1], kind="cubic") + self.i_d_opt = sp_interpolate.interp1d(best_params[:, 0], best_params[:, 2], kind="cubic") + self.i_q_opt = sp_interpolate.interp1d(best_params[:, 0], best_params[:, 3], kind="cubic") + self.i_e_opt = sp_interpolate.interp1d(best_params[:, 0], best_params[:, 4], kind="cubic") self.t_grid, self.psi_grid = np.mgrid[ - 0: self.t_max: np.complex(0, self.t_grid_count), - 0: self.psi_max: np.complex(self.psi_grid_count) - ] - - self.i_d_inter = sp_interpolate.griddata((best_params_psi[:, 0], best_params_psi[:, 1]), best_params_psi[:, 2], - (self.t_grid, self.psi_grid), method='linear') - self.i_q_inter = sp_interpolate.griddata((best_params_psi[:, 0], best_params_psi[:, 1]), best_params_psi[:, 3], - (self.t_grid, self.psi_grid), method='linear') - self.i_e_inter = sp_interpolate.griddata((best_params_psi[:, 0], best_params_psi[:, 1]), best_params_psi[:, 4], - (self.t_grid, self.psi_grid), method='linear') + 0 : self.t_max : np.complex(0, self.t_grid_count), 0 : self.psi_max : np.complex(self.psi_grid_count) + ] + + self.i_d_inter = sp_interpolate.griddata( + (best_params_psi[:, 0], best_params_psi[:, 1]), + best_params_psi[:, 2], + (self.t_grid, self.psi_grid), + method="linear", + ) + self.i_q_inter = sp_interpolate.griddata( + (best_params_psi[:, 0], best_params_psi[:, 1]), + best_params_psi[:, 3], + (self.t_grid, self.psi_grid), + method="linear", + ) + self.i_e_inter = sp_interpolate.griddata( + (best_params_psi[:, 0], best_params_psi[:, 1]), + best_params_psi[:, 4], + (self.t_grid, self.psi_grid), + method="linear", + ) def _get_psi_idx(self, psi): """ diff --git a/gem_controllers/stages/operation_point_selection/extex_dc_ttc.py b/src/gem_controllers/stages/operation_point_selection/extex_dc_ttc.py similarity index 90% rename from gem_controllers/stages/operation_point_selection/extex_dc_ttc.py rename to src/gem_controllers/stages/operation_point_selection/extex_dc_ttc.py index e7d4c881..f14fd53b 100644 --- a/gem_controllers/stages/operation_point_selection/extex_dc_ttc.py +++ b/src/gem_controllers/stages/operation_point_selection/extex_dc_ttc.py @@ -42,7 +42,7 @@ def i_e_policy(self): @i_e_policy.setter def i_e_policy(self, value): - assert callable(value), 'The i_e_policy has to be a callable function.' + assert callable(value), "The i_e_policy has to be a callable function." self._i_e_policy = value def __init__(self): @@ -89,10 +89,10 @@ def tune(self, env, env_id, current_safety_margin=0.2): super().tune(env, env_id, current_safety_margin) motor_type = gc.utils.get_motor_type(env_id) - self._i_e_idx = env.state_names.index('i_e') - self._i_a_idx = env.state_names.index('i_a') + self._i_e_idx = env.state_names.index("i_e") + self._i_a_idx = env.state_names.index("i_a") self._cross_inductance = reader.l_prime_reader[motor_type](env) mp = env.physical_system.electrical_motor.motor_parameter - self._r_a_sqrt = np.sqrt(mp['r_a']) - self._r_e_sqrt = np.sqrt(mp['r_e']) - self._l_e_prime = mp['l_e_prime'] + self._r_a_sqrt = np.sqrt(mp["r_a"]) + self._r_e_sqrt = np.sqrt(mp["r_e"]) + self._l_e_prime = mp["l_e_prime"] diff --git a/gem_controllers/stages/operation_point_selection/foc_operation_point_selection.py b/src/gem_controllers/stages/operation_point_selection/foc_operation_point_selection.py similarity index 80% rename from gem_controllers/stages/operation_point_selection/foc_operation_point_selection.py rename to src/gem_controllers/stages/operation_point_selection/foc_operation_point_selection.py index fafd6d85..5743be25 100644 --- a/gem_controllers/stages/operation_point_selection/foc_operation_point_selection.py +++ b/src/gem_controllers/stages/operation_point_selection/foc_operation_point_selection.py @@ -43,15 +43,15 @@ def __init__(self, max_modulation_level: float = 2 / np.sqrt(3), modulation_damp # parameters of the modulation controller self.modulation_damping = modulation_damping self.a_max = max_modulation_level - self.k_ = None # Factor for optimum modulation level - self.alpha = None # dynamic distance between outer and inner control loop - self.i_gain = None # constant i_gain of the modulation controller - self.limited = None # check, if flux is limited - self.u_dc = None # supply voltage - self.integrated = 0 # integration of the flux - self.psi_high = None # maximum delta flux - self.psi_low = None # minimum delta flux - self.integrated_reset = None # reset value integrated flux + self.k_ = None # Factor for optimum modulation level + self.alpha = None # dynamic distance between outer and inner control loop + self.i_gain = None # constant i_gain of the modulation controller + self.limited = None # check, if flux is limited + self.u_dc = None # supply voltage + self.integrated = 0 # integration of the flux + self.psi_high = None # maximum delta flux + self.psi_low = None # minimum delta flux + self.integrated_reset = None # reset value integrated flux def tune(self, env, env_id, current_safety_margin=0.2): """ @@ -65,19 +65,19 @@ def tune(self, env, env_id, current_safety_margin=0.2): super().tune(env, env_id, current_safety_margin) # set the state indices - self.omega_idx = env.state_names.index('omega') - self.u_sd_idx = env.state_names.index('u_sd') - self.u_sq_idx = env.state_names.index('u_sq') - self.torque_idx = env.state_names.index('torque') - self.epsilon_idx = env.state_names.index('epsilon') - self.i_sd_idx = env.state_names.index('i_sd') - self.i_sq_idx = env.state_names.index('i_sq') - u_a = 'u_a' if 'u_a' in env.state_names else 'u_sa' + self.omega_idx = env.state_names.index("omega") + self.u_sd_idx = env.state_names.index("u_sd") + self.u_sq_idx = env.state_names.index("u_sq") + self.torque_idx = env.state_names.index("torque") + self.epsilon_idx = env.state_names.index("epsilon") + self.i_sd_idx = env.state_names.index("i_sd") + self.i_sq_idx = env.state_names.index("i_sq") + u_a = "u_a" if "u_a" in env.state_names else "u_sa" self.u_a_idx = env.state_names.index(u_a) # set the motor parameters and limits self.mp = env.physical_system.electrical_motor.motor_parameter - self.p = self.mp['p'] + self.p = self.mp["p"] self.tau = env.physical_system.tau self.limit = env.physical_system.limits self.nominal_value = env.physical_system.nominal_state @@ -85,7 +85,7 @@ def tune(self, env, env_id, current_safety_margin=0.2): self.i_sq_limit = self.limit[self.i_sq_idx] * (1 - current_safety_margin) # calculate dynamic distance from damping - self.alpha = self.modulation_damping / (self.modulation_damping - np.sqrt(self.modulation_damping ** 2 - 1)) + self.alpha = self.modulation_damping / (self.modulation_damping - np.sqrt(self.modulation_damping**2 - 1)) self.limited = False self.u_dc = np.sqrt(3) * self.limit[self.u_a_idx] self.integrated = self.integrated_reset diff --git a/gem_controllers/stages/operation_point_selection/operation_point_selection.py b/src/gem_controllers/stages/operation_point_selection/operation_point_selection.py similarity index 100% rename from gem_controllers/stages/operation_point_selection/operation_point_selection.py rename to src/gem_controllers/stages/operation_point_selection/operation_point_selection.py diff --git a/gem_controllers/stages/operation_point_selection/ops_utils.py b/src/gem_controllers/stages/operation_point_selection/ops_utils.py similarity index 53% rename from gem_controllers/stages/operation_point_selection/ops_utils.py rename to src/gem_controllers/stages/operation_point_selection/ops_utils.py index 3ed45497..8ccb23f5 100644 --- a/gem_controllers/stages/operation_point_selection/ops_utils.py +++ b/src/gem_controllers/stages/operation_point_selection/ops_utils.py @@ -7,12 +7,12 @@ from .eesm_ops import EESMOperationPointSelection torque_to_current_function = { - 'PermExDc': PermExDcOperationPointSelection, - 'ExtExDc': ExtExDcOperationPointSelection, - 'SeriesDc': SeriesDcOperationPointSelection, - 'ShuntDc': ShuntDcOperationPointSelection, - 'PMSM': PMSMOperationPointSelection, - 'SynRM': PMSMOperationPointSelection, - 'SCIM': SCIMOperationPointSelection, - 'EESM': EESMOperationPointSelection, + "PermExDc": PermExDcOperationPointSelection, + "ExtExDc": ExtExDcOperationPointSelection, + "SeriesDc": SeriesDcOperationPointSelection, + "ShuntDc": ShuntDcOperationPointSelection, + "PMSM": PMSMOperationPointSelection, + "SynRM": PMSMOperationPointSelection, + "SCIM": SCIMOperationPointSelection, + "EESM": EESMOperationPointSelection, } diff --git a/gem_controllers/stages/operation_point_selection/permex_dc_ops.py b/src/gem_controllers/stages/operation_point_selection/permex_dc_ops.py similarity index 97% rename from gem_controllers/stages/operation_point_selection/permex_dc_ops.py rename to src/gem_controllers/stages/operation_point_selection/permex_dc_ops.py index 450666e6..7870453e 100644 --- a/gem_controllers/stages/operation_point_selection/permex_dc_ops.py +++ b/src/gem_controllers/stages/operation_point_selection/permex_dc_ops.py @@ -54,7 +54,7 @@ def __init__(self): def _select_operating_point(self, state, reference): """ Calculate the current refrence values. - + Args: state(np.ndarray): The state of the environment. reference(np.ndarray): The reference of the state. @@ -87,4 +87,4 @@ def tune(self, env, env_id, current_safety_margin=0.2): voltages = reader.voltages[motor] voltage_indices = [env.state_names.index(voltage) for voltage in voltages] self._voltage_limit = env.limits[voltage_indices] - self._omega_index = env.state_names.index('omega') + self._omega_index = env.state_names.index("omega") diff --git a/gem_controllers/stages/operation_point_selection/pmsm_ops.py b/src/gem_controllers/stages/operation_point_selection/pmsm_ops.py similarity index 75% rename from gem_controllers/stages/operation_point_selection/pmsm_ops.py rename to src/gem_controllers/stages/operation_point_selection/pmsm_ops.py index 38bc0f6c..ff1ca1ea 100644 --- a/gem_controllers/stages/operation_point_selection/pmsm_ops.py +++ b/src/gem_controllers/stages/operation_point_selection/pmsm_ops.py @@ -7,20 +7,19 @@ class PMSMOperationPointSelection(FieldOrientedControllerOperationPointSelection): """ - This class represents the operation point selection of the torque controller for cascaded control of synchronous - motors. For low speeds only the current limitation of the motor is important. The current vector to set a - desired torque is selected so that the amount of the current vector is minimum (Maximum Torque per Current). - For higher speeds, the voltage limitation of the synchronous motor or the actuator must also be taken into - account. This is done by converting the available voltage to a speed-dependent maximum flux. An additional - modulation controller is used for the flux control. By limiting the flux and the maximum torque per flux (MTPF), - an operating point for the flux and the torque is obtained. This is then converted into a current operating - point. The conversion can be done by different methods (parameter torque_control). On the one hand, maps can be - determined in advance by interpolation or analytically, or the analytical determination can be done online. + This class represents the operation point selection of the torque controller for cascaded control of synchronous + motors. For low speeds only the current limitation of the motor is important. The current vector to set a + desired torque is selected so that the amount of the current vector is minimum (Maximum Torque per Current). + For higher speeds, the voltage limitation of the synchronous motor or the actuator must also be taken into + account. This is done by converting the available voltage to a speed-dependent maximum flux. An additional + modulation controller is used for the flux control. By limiting the flux and the maximum torque per flux (MTPF), + an operating point for the flux and the torque is obtained. This is then converted into a current operating + point. The conversion can be done by different methods (parameter torque_control). On the one hand, maps can be + determined in advance by interpolation or analytically, or the analytical determination can be done online. """ def __init__( - self, torque_control='online', max_modulation_level: float = 2 / np.sqrt(3), - modulation_damping: float = 1.2 + self, torque_control="online", max_modulation_level: float = 2 / np.sqrt(3), modulation_damping: float = 1.2 ): """ Args: @@ -52,7 +51,7 @@ def i_d_(i_q__, torque_, controller): # calculate the maximum reference self.max_torque = max( 1.5 * self.p * (self.psi_p + (self.l_d - self.l_q) * (-self.i_sd_limit)) * self.i_sq_limit, - self.limit[self.torque_idx] + self.limit[self.torque_idx], ) torque = np.linspace(-self.max_torque, self.max_torque, self.t_count) characteristic = [] @@ -90,10 +89,7 @@ def _get_mtpf_lookup_table(self): """Calculate the lookup table that maps a flux on a maximum torque.""" # maximum flux is calculated - self.psi_max_mtpf = np.sqrt( - (self.psi_p + self.l_d * self.i_sd_limit) ** 2 - + (self.l_q * self.i_sq_limit) ** 2 - ) + self.psi_max_mtpf = np.sqrt((self.psi_p + self.l_d * self.i_sd_limit) ** 2 + (self.l_q * self.i_sq_limit) ** 2) psi = np.linspace(0, self.psi_max_mtpf, self.psi_count) i_d = np.linspace(-self.i_sd_limit, 0, self.i_count) i_d_best = 0 @@ -110,17 +106,18 @@ def _get_mtpf_lookup_table(self): else: if self.psi_p == 0: - i_q_best = psi_ / np.sqrt(self.l_d ** 2 + self.l_q ** 2) + i_q_best = psi_ / np.sqrt(self.l_d**2 + self.l_q**2) i_d_best = -i_q_best t = 1.5 * self.p * (self.psi_p + (self.l_d - self.l_q) * i_d_best) * i_q_best else: - i_d_idx = np.where(psi_ ** 2 - np.power(self.psi_p + self.l_d * i_d, 2) >= 0) + i_d_idx = np.where(psi_**2 - np.power(self.psi_p + self.l_d * i_d, 2) >= 0) i_d_ = i_d[i_d_idx] # calculate all possible i_q currents for i_d currents - i_q = np.sqrt(psi_ ** 2 - np.power(self.psi_p + self.l_d * i_d_, 2)) / self.l_q - i_idx = np.where(np.sqrt(np.power(i_q / self.i_sq_limit, 2) + np.power( - i_d_ / self.i_sd_limit, 2)) <= 1) + i_q = np.sqrt(psi_**2 - np.power(self.psi_p + self.l_d * i_d_, 2)) / self.l_q + i_idx = np.where( + np.sqrt(np.power(i_q / self.i_sq_limit, 2) + np.power(i_d_ / self.i_sd_limit, 2)) <= 1 + ) i_d_ = i_d_[i_idx] i_q = i_q[i_idx] torque = 1.5 * self.p * (self.psi_p + (self.l_d - self.l_q) * i_d_) * i_q @@ -131,7 +128,7 @@ def _get_mtpf_lookup_table(self): i_idx = np.where(torque == t)[0][0] i_d_best = i_d_[i_idx] i_q_best = i_q[i_idx] - if np.sqrt(i_d_best ** 2 + i_q_best ** 2) <= self.i_sq_limit: + if np.sqrt(i_d_best**2 + i_q_best**2) <= self.i_sq_limit: psi_i_d_q.append([psi_, t, i_d_best, i_q_best]) psi_i_d_q = np.array(psi_i_d_q) @@ -153,43 +150,44 @@ def tune(self, env: gem.core.ElectricMotorEnvironment, env_id: str, current_safe super().tune(env, env_id, current_safety_margin) - self.t_count = 250 # number of torque values in the lookup tables - self.psi_count = 250 # number of flux values in the lookup tables - self.i_count = 500 # number of current values in the lookup tables + self.t_count = 250 # number of torque values in the lookup tables + self.psi_count = 250 # number of flux values in the lookup tables + self.i_count = 500 # number of current values in the lookup tables - self.l_d = self.mp['l_d'] - self.l_q = self.mp['l_q'] - self.psi_p = self.mp.get('psi_p', 0) + self.l_d = self.mp["l_d"] + self.l_q = self.mp["l_q"] + self.psi_p = self.mp.get("psi_p", 0) self.invert = -1 if (self.psi_p == 0 and self.l_q < self.l_d) else 1 # Set the parameters of the modulation controller self.k_ = 0.953 - self.i_gain = 1 / (self.mp['l_q'] / (1.25 * self.mp['r_s'])) * (self.alpha - 1) / self.alpha ** 2 + self.i_gain = 1 / (self.mp["l_q"] / (1.25 * self.mp["r_s"])) * (self.alpha - 1) / self.alpha**2 self.psi_high = 0.2 * np.sqrt( - (self.psi_p + self.l_d * self.i_sd_limit) ** 2 - + (self.l_q * self.i_sq_limit) ** 2 + (self.psi_p + self.l_d * self.i_sd_limit) ** 2 + (self.l_q * self.i_sq_limit) ** 2 ) self.psi_low = -self.psi_high self.integrated_reset = 0.01 * self.psi_low # Reset value of the modulation controller self.max_torque = max( - 1.5 * self.p * (self.psi_p + (self.l_d - self.l_q) * (-self.limit[self.i_sd_idx])) - * self.i_sq_limit, self.limit[self.torque_idx]) + 1.5 * self.p * (self.psi_p + (self.l_d - self.l_q) * (-self.limit[self.i_sd_idx])) * self.i_sq_limit, + self.limit[self.torque_idx], + ) # Calculate the mtpc and mtpf lookup tables self.psi_max_mtpf = 0.0 self.mtpc = self._get_mtpc_lookup_table() self.mtpf = self._get_mtpf_lookup_table() self.psi_t = np.sqrt( - np.power(self.psi_p + self.l_d * self.mtpc[:, 1], 2) + np.power(self.l_q * self.mtpc[:, 2], 2)) + np.power(self.psi_p + self.l_d * self.mtpc[:, 1], 2) + np.power(self.l_q * self.mtpc[:, 2], 2) + ) self.psi_t = np.array([self.mtpc[:, 0], self.psi_t]) # Define the grid for the currents self.i_q_max = np.linspace(-self.i_sq_limit, self.i_sq_limit, self.i_count) - self.i_d_max = -np.sqrt(self.i_sq_limit ** 2 - np.power(self.i_q_max, 2)) + self.i_d_max = -np.sqrt(self.i_sq_limit**2 - np.power(self.i_q_max, 2)) i_count_mgrid = self.i_count * 1j i_d, i_q = np.mgrid[ - -self.limit[self.i_sd_idx]: 0: i_count_mgrid, - -self.limit[self.i_sq_idx]: self.limit[self.i_sq_idx]: i_count_mgrid / 2 + -self.limit[self.i_sd_idx] : 0 : i_count_mgrid, + -self.limit[self.i_sq_idx] : self.limit[self.i_sq_idx] : i_count_mgrid / 2, ] i_d = i_d.flatten() i_q = i_q.flatten() @@ -215,7 +213,7 @@ def tune(self, env: gem.core.ElectricMotorEnvironment, env_id: str, current_safe self.psi_max = np.amax(psi) # Calculate the lookup table that maps the torque and flux on a current operation point - if self.torque_control_type == 'analytical': + if self.torque_control_type == "analytical": # Calculate the lookup table by solving the equations analyticaly # Solve the equations for every point in the grid of torques and fluxes @@ -232,18 +230,20 @@ def tune(self, env: gem.core.ElectricMotorEnvironment, env_id: str, current_safe self.i_d_inter = res[:, :, 2].T self.i_q_inter = res[:, :, 3].T - elif self.torque_control_type == 'interpolate': + elif self.torque_control_type == "interpolate": # Calculate the lookup table by interpolation # Define the grid for the torque and fluxes - self.t_grid, self.psi_grid = np.mgrid[np.amin(t): np.amax(t): np.complex(0, self.t_count), - self.psi_min: self.psi_max: np.complex(self.psi_count)] + self.t_grid, self.psi_grid = np.mgrid[ + np.amin(t) : np.amax(t) : np.complex(0, self.t_count), + self.psi_min : self.psi_max : np.complex(self.psi_count), + ] # Interpolate the functions - self.i_q_inter = sp_interpolate.griddata((t, psi), i_q, (self.t_grid, self.psi_grid), method='linear') - self.i_d_inter = sp_interpolate.griddata((t, psi), i_d, (self.t_grid, self.psi_grid), method='linear') + self.i_q_inter = sp_interpolate.griddata((t, psi), i_q, (self.t_grid, self.psi_grid), method="linear") + self.i_d_inter = sp_interpolate.griddata((t, psi), i_d, (self.t_grid, self.psi_grid), method="linear") - elif self.torque_control_type != 'online': + elif self.torque_control_type != "online": raise NotImplementedError def solve_analytical(self, torque, psi): @@ -265,25 +265,19 @@ def solve_analytical(self, torque, psi): """ poly = [ - self.l_d ** 2 * (self.l_d - self.l_q) ** 2, - - 2 * self.l_d ** 2 * (self.l_d - self.l_q) * self.psi_p + self.l_d**2 * (self.l_d - self.l_q) ** 2, + 2 * self.l_d**2 * (self.l_d - self.l_q) * self.psi_p + 2 * self.l_d * self.psi_p * (self.l_d - self.l_q) ** 2, - - self.l_d ** 2 * self.psi_p ** 2 - + 4 * self.l_d * self.psi_p ** 2 * (self.l_d - self.l_q) - + (self.psi_p ** 2 - psi ** 2) * (self.l_d - self.l_q) ** 2, - - 2 * self.l_q * self.psi_p ** 3 - + 2 * (self.psi_p ** 2 - psi ** 2) * self.psi_p * (self.l_d - self.l_q), - - (self.psi_p ** 2 - psi ** 2) * self.psi_p ** 2 - + (self.l_q * 2 * torque / (3 * self.p)) ** 2 + self.l_d**2 * self.psi_p**2 + + 4 * self.l_d * self.psi_p**2 * (self.l_d - self.l_q) + + (self.psi_p**2 - psi**2) * (self.l_d - self.l_q) ** 2, + 2 * self.l_q * self.psi_p**3 + 2 * (self.psi_p**2 - psi**2) * self.psi_p * (self.l_d - self.l_q), + (self.psi_p**2 - psi**2) * self.psi_p**2 + (self.l_q * 2 * torque / (3 * self.p)) ** 2, ] - sol = np.roots(poly) # Solve the equation + sol = np.roots(poly) # Solve the equation i_d = np.real(sol[-1]) # Select the correct solution for the i_sd current - i_q = 2 * torque / (3 * self.p * (self.psi_p + (self.l_d - self.l_q) * i_d)) # Calculate the i_sq current + i_q = 2 * torque / (3 * self.p * (self.psi_p + (self.l_d - self.l_q) * i_d)) # Calculate the i_sq current return i_d, i_q def _get_i_d_q(self, torque, psi, psi_idx): @@ -308,12 +302,14 @@ def _get_psi_idx(self, psi): def _get_psi_idx_mtpf(self, psi): """Get the index of the flux of the MTPF lookup table.""" return np.clip( - int((self.psi_count - 1) - round(psi / self.psi_max_mtpf * (self.psi_count - 1))), 0, self.psi_count) + int((self.psi_count - 1) - round(psi / self.psi_max_mtpf * (self.psi_count - 1))), 0, self.psi_count + ) def _get_t_idx_mtpc(self, torque): """Get the index of the torque of the mtpc lookup table.""" return np.clip( - int(round((torque[0] + self.max_torque) / (2 * self.max_torque) * (self.t_count - 1))), 0, self.t_count) + int(round((torque[0] + self.max_torque) / (2 * self.max_torque) * (self.t_count - 1))), 0, self.t_count + ) def _select_operating_point(self, state, reference): """ @@ -341,7 +337,7 @@ def _select_operating_point(self, state, reference): reference = np.sign(reference) * t_max # calculate the currents online - if self.torque_control_type == 'online': + if self.torque_control_type == "online": i_d, i_q = self._get_i_d_q(reference[0], psi_max, psi_idx_) # get the currents from a LUT diff --git a/gem_controllers/stages/operation_point_selection/scim_ops.py b/src/gem_controllers/stages/operation_point_selection/scim_ops.py similarity index 74% rename from gem_controllers/stages/operation_point_selection/scim_ops.py rename to src/gem_controllers/stages/operation_point_selection/scim_ops.py index bb6279fa..d2cc5142 100644 --- a/gem_controllers/stages/operation_point_selection/scim_ops.py +++ b/src/gem_controllers/stages/operation_point_selection/scim_ops.py @@ -6,15 +6,19 @@ class SCIMOperationPointSelection(FieldOrientedControllerOperationPointSelection): """ - This class represents the operation point selection of the torque controller for the cascaded control of - induction motors. The torque controller uses LUT to find an appropriate operating point for the flux and torque. - The flux is limited by a modulation controller. A reference value for the i_sd current is then determined using - the operating point of the flux and a PI controller. In addition, a reference for the i_sq current is calculated - based on the current flux and the operating point of the torque. + This class represents the operation point selection of the torque controller for the cascaded control of + induction motors. The torque controller uses LUT to find an appropriate operating point for the flux and torque. + The flux is limited by a modulation controller. A reference value for the i_sd current is then determined using + the operating point of the flux and a PI controller. In addition, a reference for the i_sq current is calculated + based on the current flux and the operating point of the torque. """ - def __init__(self, max_modulation_level: float = 2 / np.sqrt(3), modulation_damping: float = 1.2, - psi_controller=PIController(control_task='FC')): + def __init__( + self, + max_modulation_level: float = 2 / np.sqrt(3), + modulation_damping: float = 1.2, + psi_controller=PIController(control_task="FC"), + ): """ Args: max_modulation_level(float): Maximum value for the modulation controller. @@ -42,11 +46,17 @@ def _psi_opt(self): i_sd = np.linspace(0, self.limit[self.i_sd_idx], self.i_sd_count) for t in np.linspace(self.t_minimum, self.t_maximum, self.t_count): if t != 0: - i_sq = t / (3/2 * self.p * self.l_m ** 2 / self.l_r * i_sd[1:]) - pv = 3 / 2 * (self.r_s * np.power(i_sd[1:], 2) + ( - self.r_s + self.r_r * self.l_m ** 2 / self.l_r ** 2) * np.power(i_sq, 2)) # Calculate losses - - i_idx = np.argmin(pv) # Minimize losses + i_sq = t / (3 / 2 * self.p * self.l_m**2 / self.l_r * i_sd[1:]) + pv = ( + 3 + / 2 + * ( + self.r_s * np.power(i_sd[1:], 2) + + (self.r_s + self.r_r * self.l_m**2 / self.l_r**2) * np.power(i_sq, 2) + ) + ) # Calculate losses + + i_idx = np.argmin(pv) # Minimize losses i_sd_opt = i_sd[i_idx] i_sq_opt = i_sq[i_idx] else: @@ -69,8 +79,10 @@ def _t_max(self): # Calculate the maximum torque for a given flux for psi_ in psi: i_sd = psi_ / self.l_m - i_sq = np.sqrt(self.nominal_value[self.u_sd_idx] ** 2 / ( - self.nominal_value[self.omega_idx] ** 2 * self.l_s ** 2) - i_sd ** 2) + i_sq = np.sqrt( + self.nominal_value[self.u_sd_idx] ** 2 / (self.nominal_value[self.omega_idx] ** 2 * self.l_s**2) + - i_sd**2 + ) t = 3 / 2 * self.p * self.l_m / self.l_r * psi_ * i_sq t_val.append(t) @@ -102,17 +114,17 @@ def tune(self, env: gem.core.ElectricMotorEnvironment, env_id: str, current_safe self.i_sd_count = 500 self.i_sq_count = 1000 - self.psi_abs_idx = env.state_names.index('psi_abs') + self.psi_abs_idx = env.state_names.index("psi_abs") self.t_minimum = -self.limit[self.torque_idx] self.t_maximum = self.limit[self.torque_idx] - self.l_m = self.mp['l_m'] - self.l_r = self.l_m + self.mp['l_sigr'] - self.l_s = self.l_m + self.mp['l_sigs'] - self.r_r = self.mp['r_r'] - self.r_s = self.mp['r_s'] - self.p = self.mp['p'] + self.l_m = self.mp["l_m"] + self.l_r = self.l_m + self.mp["l_sigr"] + self.l_s = self.l_m + self.mp["l_sigs"] + self.r_r = self.mp["r_r"] + self.r_s = self.mp["r_s"] + self.p = self.mp["p"] self.tau = env.physical_system.tau self.psi_controller.tune(env, env_id, 4, self.l_s / self.r_s) @@ -121,7 +133,7 @@ def tune(self, env: gem.core.ElectricMotorEnvironment, env_id: str, current_safe self.t_max_psi = self._t_max() # Set the parameters of the modulation controller - self.i_gain = 1 / (self.l_s / (1.25 * self.r_s)) * (self.alpha - 1) / self.alpha ** 2 + self.i_gain = 1 / (self.l_s / (1.25 * self.r_s)) * (self.alpha - 1) / self.alpha**2 self.k_ = 0.8 self.psi_high = 0.1 * self.psi_max self.psi_low = -self.psi_max @@ -171,10 +183,11 @@ def _select_operating_point(self, state, reference): i_sd = i_sd[0] # Calculate and limit the i_sq current - i_sq = np.clip(torque / max(psi, 0.001) * 2 / 3 / self.p * self.l_r / self.l_m, -self.i_sq_limit, - self.i_sq_limit) - if self.i_sd_limit < np.sqrt(i_sq ** 2 + i_sd ** 2): - i_sq = np.sign(i_sq) * np.sqrt(self.i_sd_limit ** 2 - i_sd ** 2) + i_sq = np.clip( + torque / max(psi, 0.001) * 2 / 3 / self.p * self.l_r / self.l_m, -self.i_sq_limit, self.i_sq_limit + ) + if self.i_sd_limit < np.sqrt(i_sq**2 + i_sd**2): + i_sq = np.sign(i_sq) * np.sqrt(self.i_sd_limit**2 - i_sd**2) return np.array([i_sd, i_sq]) diff --git a/gem_controllers/stages/operation_point_selection/series_dc_ops.py b/src/gem_controllers/stages/operation_point_selection/series_dc_ops.py similarity index 100% rename from gem_controllers/stages/operation_point_selection/series_dc_ops.py rename to src/gem_controllers/stages/operation_point_selection/series_dc_ops.py diff --git a/gem_controllers/stages/operation_point_selection/shunt_dc_ops.py b/src/gem_controllers/stages/operation_point_selection/shunt_dc_ops.py similarity index 94% rename from gem_controllers/stages/operation_point_selection/shunt_dc_ops.py rename to src/gem_controllers/stages/operation_point_selection/shunt_dc_ops.py index 0dbf15aa..d4c6833d 100644 --- a/gem_controllers/stages/operation_point_selection/shunt_dc_ops.py +++ b/src/gem_controllers/stages/operation_point_selection/shunt_dc_ops.py @@ -100,8 +100,8 @@ def tune(self, env, env_id, current_safety_margin=0.2): """ super().tune(env, env_id, current_safety_margin) - self._cross_inductance = reader.l_prime_reader['ShuntDc'](env) - self._i_e_idx = env.state_names.index('i_e') - self._i_a_idx = env.state_names.index('i_a') + self._cross_inductance = reader.l_prime_reader["ShuntDc"](env) + self._i_e_idx = env.state_names.index("i_e") + self._i_a_idx = env.state_names.index("i_a") self._i_a_limit = env.limits[self._i_a_idx] * (1 - current_safety_margin) self._i_e_limit = env.limits[self._i_e_idx] * (1 - current_safety_margin) diff --git a/gem_controllers/stages/stage.py b/src/gem_controllers/stages/stage.py similarity index 98% rename from gem_controllers/stages/stage.py rename to src/gem_controllers/stages/stage.py index f3192e64..5a81eb55 100644 --- a/gem_controllers/stages/stage.py +++ b/src/gem_controllers/stages/stage.py @@ -1,5 +1,5 @@ class Stage: - """The stage is the basic module in the gem-controller structure. """ + """The stage is the basic module in the gem-controller structure.""" def __call__(self, state, reference): """The stages control function. diff --git a/gem_controllers/torque_controller.py b/src/gem_controllers/torque_controller.py similarity index 88% rename from gem_controllers/torque_controller.py rename to src/gem_controllers/torque_controller.py index b548f1ab..34910a40 100644 --- a/gem_controllers/torque_controller.py +++ b/src/gem_controllers/torque_controller.py @@ -59,12 +59,12 @@ def maximum_reference(self): return self._maximum_reference def __init__( - self, - env: (gem.core.ElectricMotorEnvironment, None) = None, - env_id: (str, None) = None, - current_controller: (gc.CurrentController, None) = None, - torque_to_current_stage: (gc.stages.OperationPointSelection, None) = None, - clipping_stage: (gc.stages.clipping_stages.ClippingStage, None) = None + self, + env: (gem.core.ElectricMotorEnvironment, None) = None, + env_id: (str, None) = None, + current_controller: (gc.CurrentController, None) = None, + torque_to_current_stage: (gc.stages.OperationPointSelection, None) = None, + clipping_stage: (gc.stages.clipping_stages.ClippingStage, None) = None, ): """ Initilizes a torque control stage. @@ -88,11 +88,11 @@ def __init__( self._current_controller = gc.PICurrentController(env, env_id) if env_id is not None and clipping_stage is None: if gc.utils.get_motor_type(env_id) in gc.parameter_reader.dc_motors: - self._clipping_stage = gc.stages.clipping_stages.AbsoluteClippingStage('TC') - elif gc.utils.get_motor_type(env_id) == 'EESM': - self._clipping_stage = gc.stages.clipping_stages.CombinedClippingStage('TC') + self._clipping_stage = gc.stages.clipping_stages.AbsoluteClippingStage("TC") + elif gc.utils.get_motor_type(env_id) == "EESM": + self._clipping_stage = gc.stages.clipping_stages.CombinedClippingStage("TC") else: # motor in ac_motors - self._clipping_stage = gc.stages.clipping_stages.SquaredClippingStage('TC') + self._clipping_stage = gc.stages.clipping_stages.SquaredClippingStage("TC") self._current_reference = np.array([]) self._referenced_currents = np.array([]) self._maximum_reference = dict() @@ -113,10 +113,10 @@ def tune(self, env, env_id, current_safety_margin=0.2, tune_current_controller=T self._clipping_stage.tune(env, env_id, margin=current_safety_margin) self._operation_point_selection.tune(env, env_id, current_safety_margin) self._referenced_currents = gc.parameter_reader.currents[gc.utils.get_motor_type(env_id)] - for current, action_range_low, action_range_high in zip(self._referenced_currents, - self._clipping_stage.action_range[0], - self._clipping_stage.action_range[1]): - if current in ['i', 'i_a', 'i_e']: + for current, action_range_low, action_range_high in zip( + self._referenced_currents, self._clipping_stage.action_range[0], self._clipping_stage.action_range[1] + ): + if current in ["i", "i_a", "i_e"]: self._maximum_reference[current] = [action_range_low, action_range_high] def torque_control(self, state, reference): diff --git a/gem_controllers/utils.py b/src/gem_controllers/utils.py similarity index 89% rename from gem_controllers/utils.py rename to src/gem_controllers/utils.py index 88937c66..fabf1edc 100644 --- a/gem_controllers/utils.py +++ b/src/gem_controllers/utils.py @@ -4,7 +4,7 @@ def non_parameterized(*args, **kwargs): - raise Exception('Component not parameterized. Call the tune() function before the first control cycle.') + raise Exception("Component not parameterized. Call the tune() function before the first control cycle.") def disc_converter_actions(converter): @@ -31,12 +31,12 @@ def disc_converter_actions(converter): cv.FiniteOneQuadrantConverter: np.array([[1], [0], [0]]), cv.FiniteTwoQuadrantConverter: np.array([[1], [0], [2]]), cv.FiniteFourQuadrantConverter: np.array([[1], [0], [2]]), - cv.FiniteB6BridgeConverter: np.array([[1, 1, 1], [0, 0, 0], [2, 2, 2]]) + cv.FiniteB6BridgeConverter: np.array([[1, 1, 1], [0, 0, 0], [2, 2, 2]]), } def split_env_id(env_id: str): - return env_id.split('-')[:3] + return env_id.split("-")[:3] def get_action_type(env_id: str): diff --git a/gym_electric_motor/__init__.py b/src/gym_electric_motor/__init__.py similarity index 65% rename from gym_electric_motor/__init__.py rename to src/gym_electric_motor/__init__.py index a6fd0278..1b7e0e9f 100644 --- a/gym_electric_motor/__init__.py +++ b/src/gym_electric_motor/__init__.py @@ -30,13 +30,11 @@ # Add all superclasses of the modules to the registry. # Deactivate the order enforce wrapper that is put around a created env per default from gymnasium-version 0.21.0 onwards -# registration_kwargs = ( -# dict(order_enforce=False) -# if version.parse(gymnasium.__version__) >= version.parse("0.21.0") -# else dict() -# ) -# registration_kwargs["disable_env_checker"] = True -registration_kwargs = dict() +registration_kwargs = ( + dict(order_enforce=False) if version.parse(gymnasium.__version__) >= version.parse("0.21.0") else dict() +) +registration_kwargs["disable_env_checker"] = True +# registration_kwargs = dict() envs_path = "gym_electric_motor.envs:" @@ -44,285 +42,237 @@ register( id="Finite-SC-PermExDc-v0", entry_point=envs_path + "FiniteSpeedControlDcPermanentlyExcitedMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Cont-SC-PermExDc-v0", entry_point=envs_path + "ContSpeedControlDcPermanentlyExcitedMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Finite-TC-PermExDc-v0", entry_point=envs_path + "FiniteTorqueControlDcPermanentlyExcitedMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Cont-TC-PermExDc-v0", entry_point=envs_path + "ContTorqueControlDcPermanentlyExcitedMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Finite-CC-PermExDc-v0", entry_point=envs_path + "FiniteCurrentControlDcPermanentlyExcitedMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Cont-CC-PermExDc-v0", entry_point=envs_path + "ContCurrentControlDcPermanentlyExcitedMotorEnv", - **registration_kwargs + **registration_kwargs, ) # Externally Excited DC Motor Environments register( id="Finite-SC-ExtExDc-v0", entry_point=envs_path + "FiniteSpeedControlDcExternallyExcitedMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Cont-SC-ExtExDc-v0", entry_point=envs_path + "ContSpeedControlDcExternallyExcitedMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Finite-TC-ExtExDc-v0", entry_point=envs_path + "FiniteTorqueControlDcExternallyExcitedMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Cont-TC-ExtExDc-v0", entry_point=envs_path + "ContTorqueControlDcExternallyExcitedMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Finite-CC-ExtExDc-v0", entry_point=envs_path + "FiniteCurrentControlDcExternallyExcitedMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Cont-CC-ExtExDc-v0", entry_point=envs_path + "ContCurrentControlDcExternallyExcitedMotorEnv", - **registration_kwargs + **registration_kwargs, ) # Series DC Motor Environments register( - id="Finite-SC-SeriesDc-v0", - entry_point=envs_path + "FiniteSpeedControlDcSeriesMotorEnv", - **registration_kwargs -) -register( - id="Cont-SC-SeriesDc-v0", - entry_point=envs_path + "ContSpeedControlDcSeriesMotorEnv", - **registration_kwargs -) -register( - id="Finite-TC-SeriesDc-v0", - entry_point=envs_path + "FiniteTorqueControlDcSeriesMotorEnv", - **registration_kwargs + id="Finite-SC-SeriesDc-v0", entry_point=envs_path + "FiniteSpeedControlDcSeriesMotorEnv", **registration_kwargs ) +register(id="Cont-SC-SeriesDc-v0", entry_point=envs_path + "ContSpeedControlDcSeriesMotorEnv", **registration_kwargs) register( - id="Cont-TC-SeriesDc-v0", - entry_point=envs_path + "ContTorqueControlDcSeriesMotorEnv", - **registration_kwargs + id="Finite-TC-SeriesDc-v0", entry_point=envs_path + "FiniteTorqueControlDcSeriesMotorEnv", **registration_kwargs ) +register(id="Cont-TC-SeriesDc-v0", entry_point=envs_path + "ContTorqueControlDcSeriesMotorEnv", **registration_kwargs) register( - id="Finite-CC-SeriesDc-v0", - entry_point=envs_path + "FiniteCurrentControlDcSeriesMotorEnv", - **registration_kwargs -) -register( - id="Cont-CC-SeriesDc-v0", - entry_point=envs_path + "ContCurrentControlDcSeriesMotorEnv", - **registration_kwargs + id="Finite-CC-SeriesDc-v0", entry_point=envs_path + "FiniteCurrentControlDcSeriesMotorEnv", **registration_kwargs ) +register(id="Cont-CC-SeriesDc-v0", entry_point=envs_path + "ContCurrentControlDcSeriesMotorEnv", **registration_kwargs) # Shunt DC Motor Environments +register(id="Finite-SC-ShuntDc-v0", entry_point=envs_path + "FiniteSpeedControlDcShuntMotorEnv", **registration_kwargs) +register(id="Cont-SC-ShuntDc-v0", entry_point=envs_path + "ContSpeedControlDcShuntMotorEnv", **registration_kwargs) +register(id="Finite-TC-ShuntDc-v0", entry_point=envs_path + "FiniteTorqueControlDcShuntMotorEnv", **registration_kwargs) +register(id="Cont-TC-ShuntDc-v0", entry_point=envs_path + "ContTorqueControlDcShuntMotorEnv", **registration_kwargs) register( - id="Finite-SC-ShuntDc-v0", - entry_point=envs_path + "FiniteSpeedControlDcShuntMotorEnv", - **registration_kwargs -) -register( - id="Cont-SC-ShuntDc-v0", - entry_point=envs_path + "ContSpeedControlDcShuntMotorEnv", - **registration_kwargs -) -register( - id="Finite-TC-ShuntDc-v0", - entry_point=envs_path + "FiniteTorqueControlDcShuntMotorEnv", - **registration_kwargs -) -register( - id="Cont-TC-ShuntDc-v0", - entry_point=envs_path + "ContTorqueControlDcShuntMotorEnv", - **registration_kwargs -) -register( - id="Finite-CC-ShuntDc-v0", - entry_point=envs_path + "FiniteCurrentControlDcShuntMotorEnv", - **registration_kwargs -) -register( - id="Cont-CC-ShuntDc-v0", - entry_point=envs_path + "ContCurrentControlDcShuntMotorEnv", - **registration_kwargs + id="Finite-CC-ShuntDc-v0", entry_point=envs_path + "FiniteCurrentControlDcShuntMotorEnv", **registration_kwargs ) +register(id="Cont-CC-ShuntDc-v0", entry_point=envs_path + "ContCurrentControlDcShuntMotorEnv", **registration_kwargs) # Permanent Magnet Synchronous Motor Environments register( id="Finite-SC-PMSM-v0", entry_point=envs_path + "FiniteSpeedControlPermanentMagnetSynchronousMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Finite-TC-PMSM-v0", entry_point=envs_path + "FiniteTorqueControlPermanentMagnetSynchronousMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Finite-CC-PMSM-v0", entry_point=envs_path + "FiniteCurrentControlPermanentMagnetSynchronousMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Cont-CC-PMSM-v0", entry_point=envs_path + "ContCurrentControlPermanentMagnetSynchronousMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Cont-TC-PMSM-v0", entry_point=envs_path + "ContTorqueControlPermanentMagnetSynchronousMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Cont-SC-PMSM-v0", entry_point=envs_path + "ContSpeedControlPermanentMagnetSynchronousMotorEnv", - **registration_kwargs + **registration_kwargs, ) # Externally Excited Synchronous Motor Environments register( id="Finite-SC-EESM-v0", entry_point=envs_path + "FiniteSpeedControlExternallyExcitedSynchronousMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Finite-TC-EESM-v0", entry_point=envs_path + "FiniteTorqueControlExternallyExcitedSynchronousMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Finite-CC-EESM-v0", entry_point=envs_path + "FiniteCurrentControlExternallyExcitedSynchronousMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Cont-CC-EESM-v0", entry_point=envs_path + "ContCurrentControlExternallyExcitedSynchronousMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Cont-TC-EESM-v0", entry_point=envs_path + "ContTorqueControlExternallyExcitedSynchronousMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Cont-SC-EESM-v0", entry_point=envs_path + "ContSpeedControlExternallyExcitedSynchronousMotorEnv", - **registration_kwargs + **registration_kwargs, ) # Synchronous Reluctance Motor Environments register( id="Finite-SC-SynRM-v0", entry_point=envs_path + "FiniteSpeedControlSynchronousReluctanceMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Finite-TC-SynRM-v0", entry_point=envs_path + "FiniteTorqueControlSynchronousReluctanceMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Finite-CC-SynRM-v0", entry_point=envs_path + "FiniteCurrentControlSynchronousReluctanceMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Cont-CC-SynRM-v0", entry_point=envs_path + "ContCurrentControlSynchronousReluctanceMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Cont-TC-SynRM-v0", entry_point=envs_path + "ContTorqueControlSynchronousReluctanceMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Cont-SC-SynRM-v0", entry_point=envs_path + "ContSpeedControlSynchronousReluctanceMotorEnv", - **registration_kwargs + **registration_kwargs, ) # Squirrel Cage Induction Motor Environments register( id="Finite-SC-SCIM-v0", entry_point=envs_path + "FiniteSpeedControlSquirrelCageInductionMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Finite-TC-SCIM-v0", entry_point=envs_path + "FiniteTorqueControlSquirrelCageInductionMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Finite-CC-SCIM-v0", entry_point=envs_path + "FiniteCurrentControlSquirrelCageInductionMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Cont-CC-SCIM-v0", entry_point=envs_path + "ContCurrentControlSquirrelCageInductionMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Cont-TC-SCIM-v0", entry_point=envs_path + "ContTorqueControlSquirrelCageInductionMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( - id="Cont-SC-SCIM-v0", - entry_point=envs_path + "ContSpeedControlSquirrelCageInductionMotorEnv", - **registration_kwargs + id="Cont-SC-SCIM-v0", entry_point=envs_path + "ContSpeedControlSquirrelCageInductionMotorEnv", **registration_kwargs ) # Doubly Fed Induction Motor Environments register( id="Finite-SC-DFIM-v0", entry_point=envs_path + "FiniteSpeedControlDoublyFedInductionMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Finite-TC-DFIM-v0", entry_point=envs_path + "FiniteTorqueControlDoublyFedInductionMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( id="Finite-CC-DFIM-v0", entry_point=envs_path + "FiniteCurrentControlDoublyFedInductionMotorEnv", - **registration_kwargs + **registration_kwargs, ) register( - id="Cont-CC-DFIM-v0", - entry_point=envs_path + "ContCurrentControlDoublyFedInductionMotorEnv", - **registration_kwargs + id="Cont-CC-DFIM-v0", entry_point=envs_path + "ContCurrentControlDoublyFedInductionMotorEnv", **registration_kwargs ) register( - id="Cont-TC-DFIM-v0", - entry_point=envs_path + "ContTorqueControlDoublyFedInductionMotorEnv", - **registration_kwargs + id="Cont-TC-DFIM-v0", entry_point=envs_path + "ContTorqueControlDoublyFedInductionMotorEnv", **registration_kwargs ) register( - id="Cont-SC-DFIM-v0", - entry_point=envs_path + "ContSpeedControlDoublyFedInductionMotorEnv", - **registration_kwargs + id="Cont-SC-DFIM-v0", entry_point=envs_path + "ContSpeedControlDoublyFedInductionMotorEnv", **registration_kwargs ) diff --git a/gym_electric_motor/callbacks.py b/src/gym_electric_motor/callbacks.py similarity index 79% rename from gym_electric_motor/callbacks.py rename to src/gym_electric_motor/callbacks.py index d530345c..b8ccf7ee 100644 --- a/gym_electric_motor/callbacks.py +++ b/src/gym_electric_motor/callbacks.py @@ -47,27 +47,20 @@ def __init__( """ super().__init__() - assert ( - update_time in ["step", "episode"] - ), "Chose an option of either 'step' or 'episode' for updating on cumulative steps or episodes" + assert update_time in [ + "step", + "episode", + ], "Chose an option of either 'step' or 'episode' for updating on cumulative steps or episodes" assert ( initial_limit_margin[1] > initial_limit_margin[0] ), "First element of limit margin has to be smaller than second" assert ( maximum_limit_margin[1] > maximum_limit_margin[0] ), "First element of limit margin has to be smaller than second" - assert ( - initial_limit_margin[0] >= -1 - ), "Lower limit margin has to be bigger than or equal to -1" - assert ( - maximum_limit_margin[0] >= -1 - ), "Lower limit margin has to be bigger than or equal to -1" - assert ( - initial_limit_margin[1] <= 1 - ), "Upper limit margin has to be smaller than or equal to 1" - assert ( - maximum_limit_margin[1] <= 1 - ), "Upper limit margin has to be smaller than or equal to 1" + assert initial_limit_margin[0] >= -1, "Lower limit margin has to be bigger than or equal to -1" + assert maximum_limit_margin[0] >= -1, "Lower limit margin has to be bigger than or equal to -1" + assert initial_limit_margin[1] <= 1, "Upper limit margin has to be smaller than or equal to 1" + assert maximum_limit_margin[1] <= 1, "Upper limit margin has to be smaller than or equal to 1" self._limit_margin = initial_limit_margin self._maximum_limit_margin = maximum_limit_margin @@ -84,13 +77,9 @@ def set_env(self, env): # Assertions added to check for the right reference generator if isinstance(env.reference_generator, SwitchedReferenceGenerator): for sub_generator in env.reference_generator._sub_generators: - assert issubclass( - type(sub_generator), SubepisodedReferenceGenerator - ), self.__CLASS_ERROR__ + assert issubclass(type(sub_generator), SubepisodedReferenceGenerator), self.__CLASS_ERROR__ else: - assert issubclass( - type(env.reference_generator), SubepisodedReferenceGenerator - ), self.__CLASS_ERROR__ + assert issubclass(type(env.reference_generator), SubepisodedReferenceGenerator), self.__CLASS_ERROR__ self._env = env # Initial image margin added to the reference generator @@ -121,15 +110,11 @@ def _update_limit_margin(self): if self._limit_margin != self._maximum_limit_margin: new_lower_limit = self._limit_margin[0] - self._step_size new_lower_limit = ( - new_lower_limit - if new_lower_limit > self._maximum_limit_margin[0] - else self._maximum_limit_margin[0] + new_lower_limit if new_lower_limit > self._maximum_limit_margin[0] else self._maximum_limit_margin[0] ) new_upper_limit = self._limit_margin[1] + self._step_size new_upper_limit = ( - new_upper_limit - if new_upper_limit < self._maximum_limit_margin[1] - else self._maximum_limit_margin[1] + new_upper_limit if new_upper_limit < self._maximum_limit_margin[1] else self._maximum_limit_margin[1] ) self._limit_margin = (new_lower_limit, new_upper_limit) if isinstance(self._env.reference_generator, SwitchedReferenceGenerator): diff --git a/gym_electric_motor/constraints.py b/src/gym_electric_motor/constraints.py similarity index 88% rename from gym_electric_motor/constraints.py rename to src/gym_electric_motor/constraints.py index 3a81ef41..8f66ba83 100644 --- a/gym_electric_motor/constraints.py +++ b/src/gym_electric_motor/constraints.py @@ -63,9 +63,9 @@ def set_modules(self, ps): self._observed_state_names = ps.state_names if self._observed_state_names is None: self._observed_state_names = [] - self._observed_states = set_state_array( - dict.fromkeys(self._observed_state_names, 1), ps.state_names - ).astype(bool) + self._observed_states = set_state_array(dict.fromkeys(self._observed_state_names, 1), ps.state_names).astype( + bool + ) class SquaredConstraint(Constraint): @@ -91,14 +91,8 @@ def __init__(self, states=()): def set_modules(self, ps): self._state_indices = [ps.state_positions[state] for state in self._states] self._limits = ps.limits[self._state_indices] - self._normalized = not np.all( - ps.state_space.high[self._state_indices] == self._limits - ) + self._normalized = not np.all(ps.state_space.high[self._state_indices] == self._limits) def __call__(self, state): - state_ = ( - state[self._state_indices] - if self._normalized - else state[self._state_indices] / self._limits - ) + state_ = state[self._state_indices] if self._normalized else state[self._state_indices] / self._limits return float(np.sum(state_**2) > 1.0) diff --git a/gym_electric_motor/core.py b/src/gym_electric_motor/core.py similarity index 92% rename from gym_electric_motor/core.py rename to src/gym_electric_motor/core.py index 41fe1430..4a513531 100644 --- a/gym_electric_motor/core.py +++ b/src/gym_electric_motor/core.py @@ -232,57 +232,36 @@ def __init__( self.sim.tau = physical_system.tau self._physical_system = instantiate(PhysicalSystem, physical_system, **kwargs) - self._reference_generator = instantiate( - ReferenceGenerator, reference_generator, **kwargs - ) + self._reference_generator = instantiate(ReferenceGenerator, reference_generator, **kwargs) self._reward_function = instantiate(RewardFunction, reward_function, **kwargs) - if type(visualization) is str or isinstance( - visualization, ElectricMotorVisualization - ): + if type(visualization) is str or isinstance(visualization, ElectricMotorVisualization): visualization = [visualization] if visualization is None: visualization = [] visualizations = list(visualization) - self._visualizations = [ - instantiate(ElectricMotorVisualization, visu, **kwargs) - for visu in visualizations - ] + self._visualizations = [instantiate(ElectricMotorVisualization, visu, **kwargs) for visu in visualizations] if isinstance(constraints, ConstraintMonitor): cm = constraints else: - limit_constraints = [ - constraint for constraint in constraints if type(constraint) is str - ] - additional_constraints = [ - constraint - for constraint in constraints - if isinstance(constraint, Constraint) - ] + limit_constraints = [constraint for constraint in constraints if type(constraint) is str] + additional_constraints = [constraint for constraint in constraints if isinstance(constraint, Constraint)] cm = ConstraintMonitor(limit_constraints, additional_constraints) self._constraint_monitor = cm # Announcement of the modules among each other for physical_system_wrapper in physical_system_wrappers: - self._physical_system = physical_system_wrapper.set_physical_system( - self._physical_system - ) + self._physical_system = physical_system_wrapper.set_physical_system(self._physical_system) self._reference_generator.set_modules(self.physical_system) self._constraint_monitor.set_modules(self.physical_system) - self._reward_function.set_modules( - self.physical_system, self._reference_generator, self._constraint_monitor - ) + self._reward_function.set_modules(self.physical_system, self._reference_generator, self._constraint_monitor) # Initialization of the state filter and the spaces state_filter = state_filter or self._physical_system.state_names - self.state_filter = [ - self._physical_system.state_names.index(s) for s in state_filter - ] + self.state_filter = [self._physical_system.state_names.index(s) for s in state_filter] states_low = self._physical_system.state_space.low[self.state_filter] states_high = self._physical_system.state_space.high[self.state_filter] state_space = Box(states_low, states_high, dtype=np.float64) - self.observation_space = gymnasium.spaces.Tuple( - (state_space, self._reference_generator.reference_space) - ) + self.observation_space = gymnasium.spaces.Tuple((state_space, self._reference_generator.reference_space)) self.action_space = self.physical_system.action_space self.reward_range = self._reward_function.reward_range # new API splits done into two attributes @@ -345,17 +324,13 @@ def step(self, action): info(dict): Auxiliary information (optional) """ - assert ( - not self._terminated - ), "A reset is required before the environment can perform further steps" + assert not self._terminated, "A reset is required before the environment can perform further steps" self._call_callbacks("on_step_begin", self.physical_system.k, action) state = self._physical_system.simulate(action) reference = self.reference_generator.get_reference(state) violation_degree = self._constraint_monitor.check_constraints(state) - reward = self._reward_function.reward( - state, reference, self._physical_system.k, action, violation_degree - ) + reward = self._reward_function.reward(state, reference, self._physical_system.k, action, violation_degree) self._terminated = violation_degree >= 1.0 ref_next = self.reference_generator.get_reference_observation(state) self._call_callbacks( @@ -677,9 +652,7 @@ def __init__(self, action_space, state_space, state_names, tau): self._action_space = action_space self._state_space = state_space self._state_names = state_names - self._state_positions = { - key: index for index, key in enumerate(self._state_names) - } + self._state_positions = {key: index for index, key in enumerate(self._state_names)} self._tau = tau self._k = 0 @@ -788,9 +761,7 @@ def constraints(self): """Returns the list of all constraints the ConstraintMonitor observes.""" return self._constraints - def __init__( - self, limit_constraints=(), additional_constraints=(), merge_violations="max" - ): + def __init__(self, limit_constraints=(), additional_constraints=(), merge_violations="max"): """ Args: limit_constraints(list(str)/'all_states'): diff --git a/gym_electric_motor/envs/__init__.py b/src/gym_electric_motor/envs/__init__.py similarity index 100% rename from gym_electric_motor/envs/__init__.py rename to src/gym_electric_motor/envs/__init__.py diff --git a/gym_electric_motor/envs/gym_dcm/__init__.py b/src/gym_electric_motor/envs/gym_dcm/__init__.py similarity index 100% rename from gym_electric_motor/envs/gym_dcm/__init__.py rename to src/gym_electric_motor/envs/gym_dcm/__init__.py diff --git a/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/__init__.py b/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/__init__.py similarity index 100% rename from gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/__init__.py rename to src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/__init__.py diff --git a/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_cc_extex_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_cc_extex_dc_env.py similarity index 95% rename from gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_cc_extex_dc_env.py rename to src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_cc_extex_dc_env.py index aff8ef97..64e6e090 100644 --- a/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_cc_extex_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_cc_extex_dc_env.py @@ -149,21 +149,15 @@ def __init__( ps.ContFourQuadrantConverter(), ) physical_system = DcMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.ContMultiConverter, dict(subconverters=default_subconverters), ), - motor=initialize( - ps.ElectricMotor, motor, ps.DcExternallyExcitedMotor, dict() - ), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100) - ), + motor=initialize(ps.ElectricMotor, motor, ps.DcExternallyExcitedMotor, dict()), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_sc_extex_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_sc_extex_dc_env.py similarity index 97% rename from gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_sc_extex_dc_env.py rename to src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_sc_extex_dc_env.py index 86db9baf..0137a143 100644 --- a/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_sc_extex_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_sc_extex_dc_env.py @@ -146,18 +146,14 @@ def __init__( ps.ContFourQuadrantConverter(), ) physical_system = DcMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.ContMultiConverter, dict(subconverters=default_subconverters), ), - motor=initialize( - ps.ElectricMotor, motor, ps.DcExternallyExcitedMotor, dict() - ), + motor=initialize(ps.ElectricMotor, motor, ps.DcExternallyExcitedMotor, dict()), load=initialize( ps.MechanicalLoad, load, diff --git a/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_tc_extex_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_tc_extex_dc_env.py similarity index 95% rename from gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_tc_extex_dc_env.py rename to src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_tc_extex_dc_env.py index 97b8e31d..3c4d8a54 100644 --- a/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_tc_extex_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_tc_extex_dc_env.py @@ -149,21 +149,15 @@ def __init__( ps.ContFourQuadrantConverter(), ) physical_system = DcMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.ContMultiConverter, dict(subconverters=default_subconverters), ), - motor=initialize( - ps.ElectricMotor, motor, ps.DcExternallyExcitedMotor, dict() - ), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + motor=initialize(ps.ElectricMotor, motor, ps.DcExternallyExcitedMotor, dict()), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_cc_extex_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_cc_extex_dc_env.py similarity index 95% rename from gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_cc_extex_dc_env.py rename to src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_cc_extex_dc_env.py index 8b7db04b..498618ad 100644 --- a/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_cc_extex_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_cc_extex_dc_env.py @@ -149,21 +149,15 @@ def __init__( ps.FiniteFourQuadrantConverter(), ) physical_system = DcMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.FiniteMultiConverter, dict(subconverters=default_subconverters), ), - motor=initialize( - ps.ElectricMotor, motor, ps.DcExternallyExcitedMotor, dict() - ), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + motor=initialize(ps.ElectricMotor, motor, ps.DcExternallyExcitedMotor, dict()), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_sc_extex_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_sc_extex_dc_env.py similarity index 97% rename from gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_sc_extex_dc_env.py rename to src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_sc_extex_dc_env.py index 0c2ab760..5eb64456 100644 --- a/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_sc_extex_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_sc_extex_dc_env.py @@ -147,18 +147,14 @@ def __init__( ) physical_system = DcMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.FiniteMultiConverter, dict(subconverters=default_subconverters), ), - motor=initialize( - ps.ElectricMotor, motor, ps.DcExternallyExcitedMotor, dict() - ), + motor=initialize(ps.ElectricMotor, motor, ps.DcExternallyExcitedMotor, dict()), load=initialize( ps.MechanicalLoad, load, diff --git a/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_tc_extex_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_tc_extex_dc_env.py similarity index 95% rename from gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_tc_extex_dc_env.py rename to src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_tc_extex_dc_env.py index d6be4412..06551a5d 100644 --- a/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_tc_extex_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_tc_extex_dc_env.py @@ -147,21 +147,15 @@ def __init__( ) physical_system = DcMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.FiniteMultiConverter, dict(subconverters=default_subconverters), ), - motor=initialize( - ps.ElectricMotor, motor, ps.DcExternallyExcitedMotor, dict() - ), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + motor=initialize(ps.ElectricMotor, motor, ps.DcExternallyExcitedMotor, dict()), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/__init__.py b/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/__init__.py similarity index 100% rename from gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/__init__.py rename to src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/__init__.py diff --git a/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_cc_permex_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_cc_permex_dc_env.py similarity index 95% rename from gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_cc_permex_dc_env.py rename to src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_cc_permex_dc_env.py index ce9186fa..1d9cd5c6 100644 --- a/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_cc_permex_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_cc_permex_dc_env.py @@ -144,21 +144,15 @@ def __init__( The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = DcMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.ContFourQuadrantConverter, dict(), ), - motor=initialize( - ps.ElectricMotor, motor, ps.DcPermanentlyExcitedMotor, dict() - ), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + motor=initialize(ps.ElectricMotor, motor, ps.DcPermanentlyExcitedMotor, dict()), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_sc_permex_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_sc_permex_dc_env.py similarity index 96% rename from gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_sc_permex_dc_env.py rename to src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_sc_permex_dc_env.py index 7eb0b12b..794740d1 100644 --- a/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_sc_permex_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_sc_permex_dc_env.py @@ -144,18 +144,14 @@ def __init__( The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = DcMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.ContFourQuadrantConverter, dict(), ), - motor=initialize( - ps.ElectricMotor, motor, ps.DcPermanentlyExcitedMotor, dict() - ), + motor=initialize(ps.ElectricMotor, motor, ps.DcPermanentlyExcitedMotor, dict()), load=initialize( ps.MechanicalLoad, load, diff --git a/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_tc_permex_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_tc_permex_dc_env.py similarity index 95% rename from gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_tc_permex_dc_env.py rename to src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_tc_permex_dc_env.py index b6681ddf..a4e41cf7 100644 --- a/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_tc_permex_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_tc_permex_dc_env.py @@ -145,21 +145,15 @@ def __init__( The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = DcMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.ContFourQuadrantConverter, dict(), ), - motor=initialize( - ps.ElectricMotor, motor, ps.DcPermanentlyExcitedMotor, dict() - ), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + motor=initialize(ps.ElectricMotor, motor, ps.DcPermanentlyExcitedMotor, dict()), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_cc_permex_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_cc_permex_dc_env.py similarity index 95% rename from gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_cc_permex_dc_env.py rename to src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_cc_permex_dc_env.py index f5574764..95963398 100644 --- a/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_cc_permex_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_cc_permex_dc_env.py @@ -144,21 +144,15 @@ def __init__( The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = DcMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.FiniteFourQuadrantConverter, dict(), ), - motor=initialize( - ps.ElectricMotor, motor, ps.DcPermanentlyExcitedMotor, dict() - ), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + motor=initialize(ps.ElectricMotor, motor, ps.DcPermanentlyExcitedMotor, dict()), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_sc_permex_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_sc_permex_dc_env.py similarity index 96% rename from gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_sc_permex_dc_env.py rename to src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_sc_permex_dc_env.py index 0dd43b03..7b343798 100644 --- a/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_sc_permex_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_sc_permex_dc_env.py @@ -145,18 +145,14 @@ def __init__( """ physical_system = DcMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.FiniteFourQuadrantConverter, dict(), ), - motor=initialize( - ps.ElectricMotor, motor, ps.DcPermanentlyExcitedMotor, dict() - ), + motor=initialize(ps.ElectricMotor, motor, ps.DcPermanentlyExcitedMotor, dict()), load=initialize( ps.MechanicalLoad, load, diff --git a/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_tc_permex_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_tc_permex_dc_env.py similarity index 95% rename from gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_tc_permex_dc_env.py rename to src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_tc_permex_dc_env.py index 0741d593..175946cc 100644 --- a/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_tc_permex_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_tc_permex_dc_env.py @@ -144,21 +144,15 @@ def __init__( The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = DcMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.FiniteFourQuadrantConverter, dict(), ), - motor=initialize( - ps.ElectricMotor, motor, ps.DcPermanentlyExcitedMotor, dict() - ), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + motor=initialize(ps.ElectricMotor, motor, ps.DcPermanentlyExcitedMotor, dict()), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/__init__.py b/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/__init__.py similarity index 100% rename from gym_electric_motor/envs/gym_dcm/series_dc_motor_env/__init__.py rename to src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/__init__.py diff --git a/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_cc_series_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_cc_series_dc_env.py similarity index 96% rename from gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_cc_series_dc_env.py rename to src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_cc_series_dc_env.py index 7d0341a6..b0eed92b 100644 --- a/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_cc_series_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_cc_series_dc_env.py @@ -143,9 +143,7 @@ def __init__( The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = DcMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), converter=initialize( ps.PowerElectronicConverter, converter, @@ -153,9 +151,7 @@ def __init__( dict(), ), motor=initialize(ps.ElectricMotor, motor, ps.DcSeriesMotor, dict()), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100) - ), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_sc_series_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_sc_series_dc_env.py similarity index 98% rename from gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_sc_series_dc_env.py rename to src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_sc_series_dc_env.py index 6d272c3c..09d7d94d 100644 --- a/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_sc_series_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_sc_series_dc_env.py @@ -142,9 +142,7 @@ def __init__( The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = DcMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), converter=initialize( ps.PowerElectronicConverter, converter, diff --git a/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_tc_series_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_tc_series_dc_env.py similarity index 96% rename from gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_tc_series_dc_env.py rename to src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_tc_series_dc_env.py index d89173a8..600fe924 100644 --- a/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_tc_series_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_tc_series_dc_env.py @@ -142,9 +142,7 @@ def __init__( The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = DcMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), converter=initialize( ps.PowerElectronicConverter, converter, @@ -152,9 +150,7 @@ def __init__( dict(), ), motor=initialize(ps.ElectricMotor, motor, ps.DcSeriesMotor, dict()), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_cc_series_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_cc_series_dc_env.py similarity index 96% rename from gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_cc_series_dc_env.py rename to src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_cc_series_dc_env.py index 4b926a01..b7eb65af 100644 --- a/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_cc_series_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_cc_series_dc_env.py @@ -142,9 +142,7 @@ def __init__( The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = DcMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), converter=initialize( ps.PowerElectronicConverter, converter, @@ -152,9 +150,7 @@ def __init__( dict(), ), motor=initialize(ps.ElectricMotor, motor, ps.DcSeriesMotor, dict()), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_sc_series_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_sc_series_dc_env.py similarity index 98% rename from gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_sc_series_dc_env.py rename to src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_sc_series_dc_env.py index f100534c..b6d3ce66 100644 --- a/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_sc_series_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_sc_series_dc_env.py @@ -142,9 +142,7 @@ def __init__( The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = DcMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), converter=initialize( ps.PowerElectronicConverter, converter, diff --git a/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_tc_series_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_tc_series_dc_env.py similarity index 96% rename from gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_tc_series_dc_env.py rename to src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_tc_series_dc_env.py index b7beac0f..21657a30 100644 --- a/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_tc_series_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_tc_series_dc_env.py @@ -143,9 +143,7 @@ def __init__( """ physical_system = DcMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), converter=initialize( ps.PowerElectronicConverter, converter, @@ -153,9 +151,7 @@ def __init__( dict(), ), motor=initialize(ps.ElectricMotor, motor, ps.DcSeriesMotor, dict()), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/__init__.py b/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/__init__.py similarity index 100% rename from gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/__init__.py rename to src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/__init__.py diff --git a/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_cc_shunt_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_cc_shunt_dc_env.py similarity index 96% rename from gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_cc_shunt_dc_env.py rename to src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_cc_shunt_dc_env.py index 592c450e..fb07bb42 100644 --- a/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_cc_shunt_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_cc_shunt_dc_env.py @@ -144,9 +144,7 @@ def __init__( """ physical_system = DcMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), converter=initialize( ps.PowerElectronicConverter, converter, @@ -154,9 +152,7 @@ def __init__( dict(), ), motor=initialize(ps.ElectricMotor, motor, ps.DcShuntMotor, dict()), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100) - ), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, @@ -188,7 +184,6 @@ def __init__( visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=tuple(physical_system_wrappers) - + (CurrentSumProcessor(("i_a", "i_e")),), + physical_system_wrappers=tuple(physical_system_wrappers) + (CurrentSumProcessor(("i_a", "i_e")),), **kwargs, ) diff --git a/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_sc_shunt_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_sc_shunt_dc_env.py similarity index 97% rename from gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_sc_shunt_dc_env.py rename to src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_sc_shunt_dc_env.py index 5b639d2e..652cf48e 100644 --- a/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_sc_shunt_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_sc_shunt_dc_env.py @@ -144,9 +144,7 @@ def __init__( """ physical_system = DcMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), converter=initialize( ps.PowerElectronicConverter, converter, @@ -190,7 +188,6 @@ def __init__( visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=tuple(physical_system_wrappers) - + (CurrentSumProcessor(("i_a", "i_e")),), + physical_system_wrappers=tuple(physical_system_wrappers) + (CurrentSumProcessor(("i_a", "i_e")),), **kwargs, ) diff --git a/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_tc_shunt_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_tc_shunt_dc_env.py similarity index 96% rename from gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_tc_shunt_dc_env.py rename to src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_tc_shunt_dc_env.py index 4a7bb088..740c5676 100644 --- a/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_tc_shunt_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_tc_shunt_dc_env.py @@ -144,9 +144,7 @@ def __init__( """ physical_system = DcMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), converter=initialize( ps.PowerElectronicConverter, converter, @@ -154,9 +152,7 @@ def __init__( dict(), ), motor=initialize(ps.ElectricMotor, motor, ps.DcShuntMotor, dict()), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=230.0) - ), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=230.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, @@ -187,7 +183,6 @@ def __init__( visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=tuple(physical_system_wrappers) - + (CurrentSumProcessor(("i_a", "i_e")),), + physical_system_wrappers=tuple(physical_system_wrappers) + (CurrentSumProcessor(("i_a", "i_e")),), **kwargs, ) diff --git a/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_cc_shunt_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_cc_shunt_dc_env.py similarity index 96% rename from gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_cc_shunt_dc_env.py rename to src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_cc_shunt_dc_env.py index 61485e29..26035750 100644 --- a/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_cc_shunt_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_cc_shunt_dc_env.py @@ -144,9 +144,7 @@ def __init__( """ physical_system = DcMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), converter=initialize( ps.PowerElectronicConverter, converter, @@ -154,9 +152,7 @@ def __init__( dict(), ), motor=initialize(ps.ElectricMotor, motor, ps.DcShuntMotor, dict()), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, @@ -193,7 +189,6 @@ def __init__( visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=tuple(physical_system_wrappers) - + (CurrentSumProcessor(("i_a", "i_e")),), + physical_system_wrappers=tuple(physical_system_wrappers) + (CurrentSumProcessor(("i_a", "i_e")),), **kwargs, ) diff --git a/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_sc_shunt_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_sc_shunt_dc_env.py similarity index 97% rename from gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_sc_shunt_dc_env.py rename to src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_sc_shunt_dc_env.py index 1239833c..3ba5d5ff 100644 --- a/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_sc_shunt_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_sc_shunt_dc_env.py @@ -144,9 +144,7 @@ def __init__( """ physical_system = DcMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), converter=initialize( ps.PowerElectronicConverter, converter, @@ -190,7 +188,6 @@ def __init__( visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=tuple(physical_system_wrappers) - + (CurrentSumProcessor(("i_a", "i_e")),), + physical_system_wrappers=tuple(physical_system_wrappers) + (CurrentSumProcessor(("i_a", "i_e")),), **kwargs, ) diff --git a/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_tc_shunt_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_tc_shunt_dc_env.py similarity index 96% rename from gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_tc_shunt_dc_env.py rename to src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_tc_shunt_dc_env.py index e9a5436b..97d55b15 100644 --- a/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_tc_shunt_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_tc_shunt_dc_env.py @@ -143,9 +143,7 @@ def __init__( The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = DcMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), converter=initialize( ps.PowerElectronicConverter, converter, @@ -153,9 +151,7 @@ def __init__( dict(), ), motor=initialize(ps.ElectricMotor, motor, ps.DcShuntMotor, dict()), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, @@ -186,7 +182,6 @@ def __init__( visualization=visualization, state_filter=state_filter, callbacks=callbacks, - physical_system_wrappers=tuple(physical_system_wrappers) - + (CurrentSumProcessor(("i_a", "i_e")),), + physical_system_wrappers=tuple(physical_system_wrappers) + (CurrentSumProcessor(("i_a", "i_e")),), **kwargs, ) diff --git a/gym_electric_motor/envs/gym_eesm/__init__.py b/src/gym_electric_motor/envs/gym_eesm/__init__.py similarity index 100% rename from gym_electric_motor/envs/gym_eesm/__init__.py rename to src/gym_electric_motor/envs/gym_eesm/__init__.py diff --git a/gym_electric_motor/envs/gym_eesm/cont_cc_eesm_env.py b/src/gym_electric_motor/envs/gym_eesm/cont_cc_eesm_env.py similarity index 95% rename from gym_electric_motor/envs/gym_eesm/cont_cc_eesm_env.py rename to src/gym_electric_motor/envs/gym_eesm/cont_cc_eesm_env.py index 4b35f7df..bbaf4639 100644 --- a/gym_electric_motor/envs/gym_eesm/cont_cc_eesm_env.py +++ b/src/gym_electric_motor/envs/gym_eesm/cont_cc_eesm_env.py @@ -157,21 +157,15 @@ def __init__( ps.ContFourQuadrantConverter(), ) physical_system = ExternallyExcitedSynchronousMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=300.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=300.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.ContMultiConverter, dict(subconverters=default_subconverters), ), - motor=initialize( - ps.ElectricMotor, motor, ps.ExternallyExcitedSynchronousMotor, dict() - ), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + motor=initialize(ps.ElectricMotor, motor, ps.ExternallyExcitedSynchronousMotor, dict()), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_eesm/cont_sc_eesm_env.py b/src/gym_electric_motor/envs/gym_eesm/cont_sc_eesm_env.py similarity index 97% rename from gym_electric_motor/envs/gym_eesm/cont_sc_eesm_env.py rename to src/gym_electric_motor/envs/gym_eesm/cont_sc_eesm_env.py index 748f1187..8bfd1f70 100644 --- a/gym_electric_motor/envs/gym_eesm/cont_sc_eesm_env.py +++ b/src/gym_electric_motor/envs/gym_eesm/cont_sc_eesm_env.py @@ -150,18 +150,14 @@ def __init__( ps.ContFourQuadrantConverter(), ) physical_system = ExternallyExcitedSynchronousMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.ContMultiConverter, dict(subconverters=default_subconverters), ), - motor=initialize( - ps.ElectricMotor, motor, ps.ExternallyExcitedSynchronousMotor, dict() - ), + motor=initialize(ps.ElectricMotor, motor, ps.ExternallyExcitedSynchronousMotor, dict()), load=initialize( ps.MechanicalLoad, load, diff --git a/gym_electric_motor/envs/gym_eesm/cont_tc_eesm_env.py b/src/gym_electric_motor/envs/gym_eesm/cont_tc_eesm_env.py similarity index 95% rename from gym_electric_motor/envs/gym_eesm/cont_tc_eesm_env.py rename to src/gym_electric_motor/envs/gym_eesm/cont_tc_eesm_env.py index ed7bdedd..e76ca7ac 100644 --- a/gym_electric_motor/envs/gym_eesm/cont_tc_eesm_env.py +++ b/src/gym_electric_motor/envs/gym_eesm/cont_tc_eesm_env.py @@ -154,21 +154,15 @@ def __init__( ps.ContFourQuadrantConverter(), ) physical_system = ExternallyExcitedSynchronousMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.ContMultiConverter, dict(subconverters=default_subconverters), ), - motor=initialize( - ps.ElectricMotor, motor, ps.ExternallyExcitedSynchronousMotor, dict() - ), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + motor=initialize(ps.ElectricMotor, motor, ps.ExternallyExcitedSynchronousMotor, dict()), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_eesm/finite_cc_eesm_env.py b/src/gym_electric_motor/envs/gym_eesm/finite_cc_eesm_env.py similarity index 94% rename from gym_electric_motor/envs/gym_eesm/finite_cc_eesm_env.py rename to src/gym_electric_motor/envs/gym_eesm/finite_cc_eesm_env.py index d46319e8..53a40d54 100644 --- a/gym_electric_motor/envs/gym_eesm/finite_cc_eesm_env.py +++ b/src/gym_electric_motor/envs/gym_eesm/finite_cc_eesm_env.py @@ -18,9 +18,7 @@ from gym_electric_motor.constraints import LimitConstraint, SquaredConstraint -class FiniteCurrentControlExternallyExcitedSynchronousMotorEnv( - ElectricMotorEnvironment -): +class FiniteCurrentControlExternallyExcitedSynchronousMotorEnv(ElectricMotorEnvironment): """ Description: Environment to simulate a finite control set current controlled externally excited synchronous motor. @@ -159,21 +157,15 @@ def __init__( ps.FiniteFourQuadrantConverter(), ) physical_system = ExternallyExcitedSynchronousMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.FiniteMultiConverter, dict(subconverters=default_subconverters), ), - motor=initialize( - ps.ElectricMotor, motor, ps.ExternallyExcitedSynchronousMotor, dict() - ), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + motor=initialize(ps.ElectricMotor, motor, ps.ExternallyExcitedSynchronousMotor, dict()), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_eesm/finite_sc_eesm_env.py b/src/gym_electric_motor/envs/gym_eesm/finite_sc_eesm_env.py similarity index 96% rename from gym_electric_motor/envs/gym_eesm/finite_sc_eesm_env.py rename to src/gym_electric_motor/envs/gym_eesm/finite_sc_eesm_env.py index ef8e39b8..304f7bf1 100644 --- a/gym_electric_motor/envs/gym_eesm/finite_sc_eesm_env.py +++ b/src/gym_electric_motor/envs/gym_eesm/finite_sc_eesm_env.py @@ -149,18 +149,14 @@ def __init__( ps.FiniteFourQuadrantConverter(), ) physical_system = ExternallyExcitedSynchronousMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.FiniteMultiConverter, dict(subconverters=default_subconverters), ), - motor=initialize( - ps.ElectricMotor, motor, ps.ExternallyExcitedSynchronousMotor, dict() - ), + motor=initialize(ps.ElectricMotor, motor, ps.ExternallyExcitedSynchronousMotor, dict()), load=initialize(ps.MechanicalLoad, load, ps.PolynomialStaticLoad, dict()), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, diff --git a/gym_electric_motor/envs/gym_eesm/finite_tc_eesm_env.py b/src/gym_electric_motor/envs/gym_eesm/finite_tc_eesm_env.py similarity index 95% rename from gym_electric_motor/envs/gym_eesm/finite_tc_eesm_env.py rename to src/gym_electric_motor/envs/gym_eesm/finite_tc_eesm_env.py index f2129ad1..3c01d334 100644 --- a/gym_electric_motor/envs/gym_eesm/finite_tc_eesm_env.py +++ b/src/gym_electric_motor/envs/gym_eesm/finite_tc_eesm_env.py @@ -149,21 +149,15 @@ def __init__( ps.FiniteFourQuadrantConverter(), ) physical_system = ExternallyExcitedSynchronousMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.FiniteMultiConverter, dict(subconverters=default_subconverters), ), - motor=initialize( - ps.ElectricMotor, motor, ps.ExternallyExcitedSynchronousMotor, dict() - ), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + motor=initialize(ps.ElectricMotor, motor, ps.ExternallyExcitedSynchronousMotor, dict()), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_im/__init__.py b/src/gym_electric_motor/envs/gym_im/__init__.py similarity index 100% rename from gym_electric_motor/envs/gym_im/__init__.py rename to src/gym_electric_motor/envs/gym_im/__init__.py diff --git a/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/__init__.py b/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/__init__.py similarity index 100% rename from gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/__init__.py rename to src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/__init__.py diff --git a/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_cc_dfim_env.py b/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_cc_dfim_env.py similarity index 95% rename from gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_cc_dfim_env.py rename to src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_cc_dfim_env.py index 70742aed..0aa3cd04 100644 --- a/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_cc_dfim_env.py +++ b/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_cc_dfim_env.py @@ -164,21 +164,15 @@ def __init__( ) physical_system = DoublyFedInductionMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.ContMultiConverter, dict(subconverters=default_sub_converters), ), - motor=initialize( - ps.ElectricMotor, motor, ps.DoublyFedInductionMotor, dict() - ), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + motor=initialize(ps.ElectricMotor, motor, ps.DoublyFedInductionMotor, dict()), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_sc_dfim_env.py b/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_sc_dfim_env.py similarity index 97% rename from gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_sc_dfim_env.py rename to src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_sc_dfim_env.py index d15f1c73..49540574 100644 --- a/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_sc_dfim_env.py +++ b/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_sc_dfim_env.py @@ -156,18 +156,14 @@ def __init__( ps.ContB6BridgeConverter(), ) physical_system = DoublyFedInductionMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.ContMultiConverter, dict(subconverters=default_sub_converters), ), - motor=initialize( - ps.ElectricMotor, motor, ps.DoublyFedInductionMotor, dict() - ), + motor=initialize(ps.ElectricMotor, motor, ps.DoublyFedInductionMotor, dict()), load=initialize( ps.MechanicalLoad, load, diff --git a/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_tc_dfim_env.py b/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_tc_dfim_env.py similarity index 95% rename from gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_tc_dfim_env.py rename to src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_tc_dfim_env.py index 10f36343..ec7ba00a 100644 --- a/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_tc_dfim_env.py +++ b/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_tc_dfim_env.py @@ -159,21 +159,15 @@ def __init__( ps.ContB6BridgeConverter(), ) physical_system = DoublyFedInductionMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.ContMultiConverter, dict(subconverters=default_sub_converters), ), - motor=initialize( - ps.ElectricMotor, motor, ps.DoublyFedInductionMotor, dict() - ), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + motor=initialize(ps.ElectricMotor, motor, ps.DoublyFedInductionMotor, dict()), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_cc_dfim_env.py b/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_cc_dfim_env.py similarity index 95% rename from gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_cc_dfim_env.py rename to src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_cc_dfim_env.py index 833aab36..0a23f5ee 100644 --- a/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_cc_dfim_env.py +++ b/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_cc_dfim_env.py @@ -164,21 +164,15 @@ def __init__( ) physical_system = DoublyFedInductionMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.FiniteMultiConverter, dict(subconverters=default_sub_converters), ), - motor=initialize( - ps.ElectricMotor, motor, ps.DoublyFedInductionMotor, dict() - ), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + motor=initialize(ps.ElectricMotor, motor, ps.DoublyFedInductionMotor, dict()), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_sc_dfim_env.py b/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_sc_dfim_env.py similarity index 97% rename from gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_sc_dfim_env.py rename to src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_sc_dfim_env.py index b91890dc..a608f09c 100644 --- a/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_sc_dfim_env.py +++ b/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_sc_dfim_env.py @@ -156,18 +156,14 @@ def __init__( ps.FiniteB6BridgeConverter(), ) physical_system = DoublyFedInductionMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.FiniteMultiConverter, dict(subconverters=default_subconverters), ), - motor=initialize( - ps.ElectricMotor, motor, ps.DoublyFedInductionMotor, dict() - ), + motor=initialize(ps.ElectricMotor, motor, ps.DoublyFedInductionMotor, dict()), load=initialize( ps.MechanicalLoad, load, diff --git a/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_tc_dfim_env.py b/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_tc_dfim_env.py similarity index 95% rename from gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_tc_dfim_env.py rename to src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_tc_dfim_env.py index 9b58c465..00972705 100644 --- a/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_tc_dfim_env.py +++ b/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_tc_dfim_env.py @@ -156,21 +156,15 @@ def __init__( ps.FiniteB6BridgeConverter(), ) physical_system = DoublyFedInductionMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.FiniteMultiConverter, dict(subconverters=default_sub_converters), ), - motor=initialize( - ps.ElectricMotor, motor, ps.DoublyFedInductionMotor, dict() - ), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + motor=initialize(ps.ElectricMotor, motor, ps.DoublyFedInductionMotor, dict()), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/__init__.py b/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/__init__.py similarity index 100% rename from gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/__init__.py rename to src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/__init__.py diff --git a/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_cc_scim_env.py b/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_cc_scim_env.py similarity index 93% rename from gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_cc_scim_env.py rename to src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_cc_scim_env.py index e82d846c..0bed9193 100644 --- a/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_cc_scim_env.py +++ b/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_cc_scim_env.py @@ -158,18 +158,10 @@ def __init__( ) physical_system = SquirrelCageInductionMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), - converter=initialize( - ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict() - ), - motor=initialize( - ps.ElectricMotor, motor, ps.SquirrelCageInductionMotor, dict() - ), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), + converter=initialize(ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict()), + motor=initialize(ps.ElectricMotor, motor, ps.SquirrelCageInductionMotor, dict()), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_sc_scim_env.py b/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_sc_scim_env.py similarity index 95% rename from gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_sc_scim_env.py rename to src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_sc_scim_env.py index 02a51b18..63289f1f 100644 --- a/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_sc_scim_env.py +++ b/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_sc_scim_env.py @@ -151,15 +151,9 @@ def __init__( """ physical_system = SquirrelCageInductionMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), - converter=initialize( - ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict() - ), - motor=initialize( - ps.ElectricMotor, motor, ps.SquirrelCageInductionMotor, dict() - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), + converter=initialize(ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict()), + motor=initialize(ps.ElectricMotor, motor, ps.SquirrelCageInductionMotor, dict()), load=initialize( ps.MechanicalLoad, load, diff --git a/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_tc_scim_env.py b/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_tc_scim_env.py similarity index 93% rename from gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_tc_scim_env.py rename to src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_tc_scim_env.py index 65e90033..b8a56439 100644 --- a/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_tc_scim_env.py +++ b/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_tc_scim_env.py @@ -153,18 +153,10 @@ def __init__( The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = SquirrelCageInductionMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), - converter=initialize( - ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict() - ), - motor=initialize( - ps.ElectricMotor, motor, ps.SquirrelCageInductionMotor, dict() - ), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), + converter=initialize(ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict()), + motor=initialize(ps.ElectricMotor, motor, ps.SquirrelCageInductionMotor, dict()), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_cc_scim_env.py b/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_cc_scim_env.py similarity index 95% rename from gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_cc_scim_env.py rename to src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_cc_scim_env.py index f3fb5d73..672fc53e 100644 --- a/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_cc_scim_env.py +++ b/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_cc_scim_env.py @@ -158,21 +158,15 @@ def __init__( ) physical_system = SquirrelCageInductionMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.FiniteB6BridgeConverter, dict(), ), - motor=initialize( - ps.ElectricMotor, motor, ps.SquirrelCageInductionMotor, dict() - ), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + motor=initialize(ps.ElectricMotor, motor, ps.SquirrelCageInductionMotor, dict()), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_sc_scim_env.py b/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_sc_scim_env.py similarity index 96% rename from gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_sc_scim_env.py rename to src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_sc_scim_env.py index be1987c7..7777a162 100644 --- a/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_sc_scim_env.py +++ b/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_sc_scim_env.py @@ -151,18 +151,14 @@ def __init__( """ physical_system = SquirrelCageInductionMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.FiniteB6BridgeConverter, dict(), ), - motor=initialize( - ps.ElectricMotor, motor, ps.SquirrelCageInductionMotor, dict() - ), + motor=initialize(ps.ElectricMotor, motor, ps.SquirrelCageInductionMotor, dict()), load=initialize( ps.MechanicalLoad, load, diff --git a/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_tc_scim_env.py b/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_tc_scim_env.py similarity index 95% rename from gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_tc_scim_env.py rename to src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_tc_scim_env.py index cf1fa749..2ec0ae76 100644 --- a/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_tc_scim_env.py +++ b/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_tc_scim_env.py @@ -150,21 +150,15 @@ def __init__( The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = SquirrelCageInductionMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.FiniteB6BridgeConverter, dict(), ), - motor=initialize( - ps.ElectricMotor, motor, ps.SquirrelCageInductionMotor, dict() - ), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + motor=initialize(ps.ElectricMotor, motor, ps.SquirrelCageInductionMotor, dict()), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_pmsm/__init__.py b/src/gym_electric_motor/envs/gym_pmsm/__init__.py similarity index 100% rename from gym_electric_motor/envs/gym_pmsm/__init__.py rename to src/gym_electric_motor/envs/gym_pmsm/__init__.py diff --git a/gym_electric_motor/envs/gym_pmsm/cont_cc_pmsm_env.py b/src/gym_electric_motor/envs/gym_pmsm/cont_cc_pmsm_env.py similarity index 93% rename from gym_electric_motor/envs/gym_pmsm/cont_cc_pmsm_env.py rename to src/gym_electric_motor/envs/gym_pmsm/cont_cc_pmsm_env.py index c8c2e898..d525f1aa 100644 --- a/gym_electric_motor/envs/gym_pmsm/cont_cc_pmsm_env.py +++ b/src/gym_electric_motor/envs/gym_pmsm/cont_cc_pmsm_env.py @@ -151,18 +151,10 @@ def __init__( ) physical_system = SynchronousMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=300.0) - ), - converter=initialize( - ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict() - ), - motor=initialize( - ps.ElectricMotor, motor, ps.PermanentMagnetSynchronousMotor, dict() - ), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=300.0)), + converter=initialize(ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict()), + motor=initialize(ps.ElectricMotor, motor, ps.PermanentMagnetSynchronousMotor, dict()), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_pmsm/cont_sc_pmsm_env.py b/src/gym_electric_motor/envs/gym_pmsm/cont_sc_pmsm_env.py similarity index 95% rename from gym_electric_motor/envs/gym_pmsm/cont_sc_pmsm_env.py rename to src/gym_electric_motor/envs/gym_pmsm/cont_sc_pmsm_env.py index 408fd8b7..333a7d32 100644 --- a/gym_electric_motor/envs/gym_pmsm/cont_sc_pmsm_env.py +++ b/src/gym_electric_motor/envs/gym_pmsm/cont_sc_pmsm_env.py @@ -143,15 +143,9 @@ def __init__( The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = SynchronousMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), - converter=initialize( - ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict() - ), - motor=initialize( - ps.ElectricMotor, motor, ps.PermanentMagnetSynchronousMotor, dict() - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), + converter=initialize(ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict()), + motor=initialize(ps.ElectricMotor, motor, ps.PermanentMagnetSynchronousMotor, dict()), load=initialize( ps.MechanicalLoad, load, diff --git a/gym_electric_motor/envs/gym_pmsm/cont_tc_pmsm_env.py b/src/gym_electric_motor/envs/gym_pmsm/cont_tc_pmsm_env.py similarity index 93% rename from gym_electric_motor/envs/gym_pmsm/cont_tc_pmsm_env.py rename to src/gym_electric_motor/envs/gym_pmsm/cont_tc_pmsm_env.py index 89e17db6..0fe37952 100644 --- a/gym_electric_motor/envs/gym_pmsm/cont_tc_pmsm_env.py +++ b/src/gym_electric_motor/envs/gym_pmsm/cont_tc_pmsm_env.py @@ -146,18 +146,10 @@ def __init__( The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = SynchronousMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), - converter=initialize( - ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict() - ), - motor=initialize( - ps.ElectricMotor, motor, ps.PermanentMagnetSynchronousMotor, dict() - ), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), + converter=initialize(ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict()), + motor=initialize(ps.ElectricMotor, motor, ps.PermanentMagnetSynchronousMotor, dict()), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_pmsm/finite_cc_pmsm_env.py b/src/gym_electric_motor/envs/gym_pmsm/finite_cc_pmsm_env.py similarity index 95% rename from gym_electric_motor/envs/gym_pmsm/finite_cc_pmsm_env.py rename to src/gym_electric_motor/envs/gym_pmsm/finite_cc_pmsm_env.py index 46f55259..7b237f78 100644 --- a/gym_electric_motor/envs/gym_pmsm/finite_cc_pmsm_env.py +++ b/src/gym_electric_motor/envs/gym_pmsm/finite_cc_pmsm_env.py @@ -151,21 +151,15 @@ def __init__( ) physical_system = SynchronousMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.FiniteB6BridgeConverter, dict(), ), - motor=initialize( - ps.ElectricMotor, motor, ps.PermanentMagnetSynchronousMotor, dict() - ), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + motor=initialize(ps.ElectricMotor, motor, ps.PermanentMagnetSynchronousMotor, dict()), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_pmsm/finite_sc_pmsm_env.py b/src/gym_electric_motor/envs/gym_pmsm/finite_sc_pmsm_env.py similarity index 96% rename from gym_electric_motor/envs/gym_pmsm/finite_sc_pmsm_env.py rename to src/gym_electric_motor/envs/gym_pmsm/finite_sc_pmsm_env.py index 1dc0e60e..48451cdc 100644 --- a/gym_electric_motor/envs/gym_pmsm/finite_sc_pmsm_env.py +++ b/src/gym_electric_motor/envs/gym_pmsm/finite_sc_pmsm_env.py @@ -143,18 +143,14 @@ def __init__( The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = SynchronousMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.FiniteB6BridgeConverter, dict(), ), - motor=initialize( - ps.ElectricMotor, motor, ps.PermanentMagnetSynchronousMotor, dict() - ), + motor=initialize(ps.ElectricMotor, motor, ps.PermanentMagnetSynchronousMotor, dict()), load=initialize( ps.MechanicalLoad, load, diff --git a/gym_electric_motor/envs/gym_pmsm/finite_tc_pmsm_env.py b/src/gym_electric_motor/envs/gym_pmsm/finite_tc_pmsm_env.py similarity index 95% rename from gym_electric_motor/envs/gym_pmsm/finite_tc_pmsm_env.py rename to src/gym_electric_motor/envs/gym_pmsm/finite_tc_pmsm_env.py index 64ff98cc..36442016 100644 --- a/gym_electric_motor/envs/gym_pmsm/finite_tc_pmsm_env.py +++ b/src/gym_electric_motor/envs/gym_pmsm/finite_tc_pmsm_env.py @@ -143,21 +143,15 @@ def __init__( The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = SynchronousMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.FiniteB6BridgeConverter, dict(), ), - motor=initialize( - ps.ElectricMotor, motor, ps.PermanentMagnetSynchronousMotor, dict() - ), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + motor=initialize(ps.ElectricMotor, motor, ps.PermanentMagnetSynchronousMotor, dict()), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_srm/srm_continuous_control_env.py b/src/gym_electric_motor/envs/gym_srm/srm_continuous_control_env.py similarity index 100% rename from gym_electric_motor/envs/gym_srm/srm_continuous_control_env.py rename to src/gym_electric_motor/envs/gym_srm/srm_continuous_control_env.py diff --git a/gym_electric_motor/envs/gym_srm/srm_finite_control_env.py b/src/gym_electric_motor/envs/gym_srm/srm_finite_control_env.py similarity index 100% rename from gym_electric_motor/envs/gym_srm/srm_finite_control_env.py rename to src/gym_electric_motor/envs/gym_srm/srm_finite_control_env.py diff --git a/gym_electric_motor/envs/gym_synrm/__init__.py b/src/gym_electric_motor/envs/gym_synrm/__init__.py similarity index 100% rename from gym_electric_motor/envs/gym_synrm/__init__.py rename to src/gym_electric_motor/envs/gym_synrm/__init__.py diff --git a/gym_electric_motor/envs/gym_synrm/cont_cc_synrm_env.py b/src/gym_electric_motor/envs/gym_synrm/cont_cc_synrm_env.py similarity index 93% rename from gym_electric_motor/envs/gym_synrm/cont_cc_synrm_env.py rename to src/gym_electric_motor/envs/gym_synrm/cont_cc_synrm_env.py index 2dbd3e5e..63cf2b9e 100644 --- a/gym_electric_motor/envs/gym_synrm/cont_cc_synrm_env.py +++ b/src/gym_electric_motor/envs/gym_synrm/cont_cc_synrm_env.py @@ -151,18 +151,10 @@ def __init__( ) physical_system = SynchronousMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), - converter=initialize( - ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict() - ), - motor=initialize( - ps.ElectricMotor, motor, ps.SynchronousReluctanceMotor, dict() - ), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), + converter=initialize(ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict()), + motor=initialize(ps.ElectricMotor, motor, ps.SynchronousReluctanceMotor, dict()), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_synrm/cont_sc_synrm_env.py b/src/gym_electric_motor/envs/gym_synrm/cont_sc_synrm_env.py similarity index 95% rename from gym_electric_motor/envs/gym_synrm/cont_sc_synrm_env.py rename to src/gym_electric_motor/envs/gym_synrm/cont_sc_synrm_env.py index e53641ce..74142f13 100644 --- a/gym_electric_motor/envs/gym_synrm/cont_sc_synrm_env.py +++ b/src/gym_electric_motor/envs/gym_synrm/cont_sc_synrm_env.py @@ -143,15 +143,9 @@ def __init__( The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = SynchronousMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), - converter=initialize( - ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict() - ), - motor=initialize( - ps.ElectricMotor, motor, ps.SynchronousReluctanceMotor, dict() - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), + converter=initialize(ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict()), + motor=initialize(ps.ElectricMotor, motor, ps.SynchronousReluctanceMotor, dict()), load=initialize( ps.MechanicalLoad, load, diff --git a/gym_electric_motor/envs/gym_synrm/cont_tc_synrm_env.py b/src/gym_electric_motor/envs/gym_synrm/cont_tc_synrm_env.py similarity index 93% rename from gym_electric_motor/envs/gym_synrm/cont_tc_synrm_env.py rename to src/gym_electric_motor/envs/gym_synrm/cont_tc_synrm_env.py index 5fe42660..2a4dddf1 100644 --- a/gym_electric_motor/envs/gym_synrm/cont_tc_synrm_env.py +++ b/src/gym_electric_motor/envs/gym_synrm/cont_tc_synrm_env.py @@ -146,18 +146,10 @@ def __init__( The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = SynchronousMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), - converter=initialize( - ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict() - ), - motor=initialize( - ps.ElectricMotor, motor, ps.SynchronousReluctanceMotor, dict() - ), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), + converter=initialize(ps.PowerElectronicConverter, converter, ps.ContB6BridgeConverter, dict()), + motor=initialize(ps.ElectricMotor, motor, ps.SynchronousReluctanceMotor, dict()), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_synrm/finite_cc_synrm_env.py b/src/gym_electric_motor/envs/gym_synrm/finite_cc_synrm_env.py similarity index 95% rename from gym_electric_motor/envs/gym_synrm/finite_cc_synrm_env.py rename to src/gym_electric_motor/envs/gym_synrm/finite_cc_synrm_env.py index c041d8b4..e62965bd 100644 --- a/gym_electric_motor/envs/gym_synrm/finite_cc_synrm_env.py +++ b/src/gym_electric_motor/envs/gym_synrm/finite_cc_synrm_env.py @@ -151,21 +151,15 @@ def __init__( ) physical_system = SynchronousMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.FiniteB6BridgeConverter, dict(), ), - motor=initialize( - ps.ElectricMotor, motor, ps.SynchronousReluctanceMotor, dict() - ), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + motor=initialize(ps.ElectricMotor, motor, ps.SynchronousReluctanceMotor, dict()), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/gym_synrm/finite_sc_synrm_env.py b/src/gym_electric_motor/envs/gym_synrm/finite_sc_synrm_env.py similarity index 96% rename from gym_electric_motor/envs/gym_synrm/finite_sc_synrm_env.py rename to src/gym_electric_motor/envs/gym_synrm/finite_sc_synrm_env.py index 3017e757..9bb68f4b 100644 --- a/gym_electric_motor/envs/gym_synrm/finite_sc_synrm_env.py +++ b/src/gym_electric_motor/envs/gym_synrm/finite_sc_synrm_env.py @@ -144,18 +144,14 @@ def __init__( The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = SynchronousMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.FiniteB6BridgeConverter, dict(), ), - motor=initialize( - ps.ElectricMotor, motor, ps.SynchronousReluctanceMotor, dict() - ), + motor=initialize(ps.ElectricMotor, motor, ps.SynchronousReluctanceMotor, dict()), load=initialize( ps.MechanicalLoad, load, diff --git a/gym_electric_motor/envs/gym_synrm/finite_tc_synrm_env.py b/src/gym_electric_motor/envs/gym_synrm/finite_tc_synrm_env.py similarity index 95% rename from gym_electric_motor/envs/gym_synrm/finite_tc_synrm_env.py rename to src/gym_electric_motor/envs/gym_synrm/finite_tc_synrm_env.py index 62174a6b..ef675c0d 100644 --- a/gym_electric_motor/envs/gym_synrm/finite_tc_synrm_env.py +++ b/src/gym_electric_motor/envs/gym_synrm/finite_tc_synrm_env.py @@ -143,21 +143,15 @@ def __init__( The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = SynchronousMotorSystem( - supply=initialize( - ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0) - ), + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=420.0)), converter=initialize( ps.PowerElectronicConverter, converter, ps.FiniteB6BridgeConverter, dict(), ), - motor=initialize( - ps.ElectricMotor, motor, ps.SynchronousReluctanceMotor, dict() - ), - load=initialize( - ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0) - ), + motor=initialize(ps.ElectricMotor, motor, ps.SynchronousReluctanceMotor, dict()), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, diff --git a/gym_electric_motor/envs/motors.py b/src/gym_electric_motor/envs/motors.py similarity index 100% rename from gym_electric_motor/envs/motors.py rename to src/gym_electric_motor/envs/motors.py diff --git a/gym_electric_motor/physical_system_wrappers/__init__.py b/src/gym_electric_motor/physical_system_wrappers/__init__.py similarity index 100% rename from gym_electric_motor/physical_system_wrappers/__init__.py rename to src/gym_electric_motor/physical_system_wrappers/__init__.py diff --git a/gym_electric_motor/physical_system_wrappers/cos_sin_processor.py b/src/gym_electric_motor/physical_system_wrappers/cos_sin_processor.py similarity index 77% rename from gym_electric_motor/physical_system_wrappers/cos_sin_processor.py rename to src/gym_electric_motor/physical_system_wrappers/cos_sin_processor.py index c332f81f..b8e30bd1 100644 --- a/gym_electric_motor/physical_system_wrappers/cos_sin_processor.py +++ b/src/gym_electric_motor/physical_system_wrappers/cos_sin_processor.py @@ -36,26 +36,17 @@ def set_physical_system(self, physical_system): self._angle_index = physical_system.state_positions[self._angle] self._remove_idx = self._angle_index if self._remove_angle else [] - low = np.concatenate( - (np.delete(physical_system.state_space.low, self._remove_idx), [-1.0, -1.0]) - ) - high = np.concatenate( - (np.delete(physical_system.state_space.high, self._remove_idx), [1.0, 1.0]) - ) + low = np.concatenate((np.delete(physical_system.state_space.low, self._remove_idx), [-1.0, -1.0])) + high = np.concatenate((np.delete(physical_system.state_space.high, self._remove_idx), [1.0, 1.0])) self.state_space = gymnasium.spaces.Box(low, high, dtype=np.float64) - self._limits = np.concatenate( - (np.delete(physical_system.limits, self._remove_idx), [1.0, 1.0]) - ) - self._nominal_state = np.concatenate( - (np.delete(physical_system.nominal_state, self._remove_idx), [1.0, 1.0]) - ) - self._state_names = list( - np.delete(physical_system.state_names, self._remove_idx) - ) + [f"cos({self._angle})", f"sin({self._angle})"] - self._state_positions = { - key: index for index, key in enumerate(self._state_names) - } + self._limits = np.concatenate((np.delete(physical_system.limits, self._remove_idx), [1.0, 1.0])) + self._nominal_state = np.concatenate((np.delete(physical_system.nominal_state, self._remove_idx), [1.0, 1.0])) + self._state_names = list(np.delete(physical_system.state_names, self._remove_idx)) + [ + f"cos({self._angle})", + f"sin({self._angle})", + ] + self._state_positions = {key: index for index, key in enumerate(self._state_names)} return self def reset(self): diff --git a/gym_electric_motor/physical_system_wrappers/current_sum_processor.py b/src/gym_electric_motor/physical_system_wrappers/current_sum_processor.py similarity index 87% rename from gym_electric_motor/physical_system_wrappers/current_sum_processor.py rename to src/gym_electric_motor/physical_system_wrappers/current_sum_processor.py index c7e3e20b..5873c88e 100644 --- a/gym_electric_motor/physical_system_wrappers/current_sum_processor.py +++ b/src/gym_electric_motor/physical_system_wrappers/current_sum_processor.py @@ -24,9 +24,7 @@ def __init__(self, currents, limit="max", physical_system=None): def set_physical_system(self, physical_system): # Docstring of superclass super().set_physical_system(physical_system) - self._current_indices = [ - physical_system.state_positions[current] for current in self._currents - ] + self._current_indices = [physical_system.state_positions[current] for current in self._currents] # Define the new state space as concatenation of the old state space and [-1,1] for i_sum low = np.concatenate((physical_system.state_space.low, [-1.0])) @@ -35,13 +33,9 @@ def set_physical_system(self, physical_system): # Set the new limits / nominal values of the state vector current_limit = self._limit(physical_system.limits[self._current_indices]) - current_nominal_value = self._limit( - physical_system.nominal_state[self._current_indices] - ) + current_nominal_value = self._limit(physical_system.nominal_state[self._current_indices]) self._limits = np.concatenate((physical_system.limits, [current_limit])) - self._nominal_state = np.concatenate( - (physical_system.nominal_state, [current_nominal_value]) - ) + self._nominal_state = np.concatenate((physical_system.nominal_state, [current_nominal_value])) # Append the new state to the state name vector and the state positions dictionary self._state_names = physical_system.state_names + ["i_sum"] diff --git a/gym_electric_motor/physical_system_wrappers/dead_time_processor.py b/src/gym_electric_motor/physical_system_wrappers/dead_time_processor.py similarity index 96% rename from gym_electric_motor/physical_system_wrappers/dead_time_processor.py rename to src/gym_electric_motor/physical_system_wrappers/dead_time_processor.py index cdb7c5c4..de4cde92 100644 --- a/gym_electric_motor/physical_system_wrappers/dead_time_processor.py +++ b/src/gym_electric_motor/physical_system_wrappers/dead_time_processor.py @@ -32,9 +32,7 @@ def __init__(self, steps=1, reset_action=None, physical_system=None): """ self._reset_actions = reset_action self._steps = int(steps) - assert ( - self._steps > 0 - ), f'The number of steps has to be greater than 0. A "{steps}" has been passed.' + assert self._steps > 0, f'The number of steps has to be greater than 0. A "{steps}" has been passed.' self._action_deque = deque(maxlen=steps) super().__init__(physical_system) diff --git a/gym_electric_motor/physical_system_wrappers/dq_to_abc_action_processor.py b/src/gym_electric_motor/physical_system_wrappers/dq_to_abc_action_processor.py similarity index 90% rename from gym_electric_motor/physical_system_wrappers/dq_to_abc_action_processor.py rename to src/gym_electric_motor/physical_system_wrappers/dq_to_abc_action_processor.py index 2482cdf4..e592686c 100644 --- a/gym_electric_motor/physical_system_wrappers/dq_to_abc_action_processor.py +++ b/src/gym_electric_motor/physical_system_wrappers/dq_to_abc_action_processor.py @@ -35,9 +35,7 @@ def wrapper(callable_): @classmethod def make(cls, motor_type, *args, **kwargs): - assert ( - motor_type in cls._registry.keys() - ), f"Not supported motor_type {motor_type}." + assert motor_type in cls._registry.keys(), f"Not supported motor_type {motor_type}." class_ = cls._registry[motor_type] inst = class_(*args, **kwargs) return inst @@ -88,10 +86,7 @@ def reset(self, **kwargs): return normalized_state def _advance_angle(self, state): - return ( - state[self._angle_index] - + self._angle_advance * self._physical_system.tau * state[self._omega_index] - ) + return state[self._angle_index] + self._angle_advance * self._physical_system.tau * state[self._omega_index] class _ClassicDqToAbcActionProcessor(DqToAbcActionProcessor): @@ -109,15 +104,11 @@ def simulate(self, action): DqToAbcActionProcessor.register_transformation(["PMSM"])( - lambda angle_name="epsilon", *args, **kwargs: _ClassicDqToAbcActionProcessor( - angle_name, *args, **kwargs - ) + lambda angle_name="epsilon", *args, **kwargs: _ClassicDqToAbcActionProcessor(angle_name, *args, **kwargs) ) DqToAbcActionProcessor.register_transformation(["SCIM"])( - lambda angle_name="psi_angle", *args, **kwargs: _ClassicDqToAbcActionProcessor( - angle_name, *args, **kwargs - ) + lambda angle_name="psi_angle", *args, **kwargs: _ClassicDqToAbcActionProcessor(angle_name, *args, **kwargs) ) @@ -144,9 +135,7 @@ def simulate(self, action): dq_action_stator = action[:2] dq_action_rotor = action[2:] abc_action_stator = self._transformation(dq_action_stator, advanced_angle) - abc_action_rotor = self._transformation( - dq_action_rotor, self._state[self._flux_angle_index] - advanced_angle - ) + abc_action_rotor = self._transformation(dq_action_rotor, self._state[self._flux_angle_index] - advanced_angle) abc_action = np.concatenate((abc_action_stator, abc_action_rotor)) normalized_state = self._physical_system.simulate(abc_action) self._state = normalized_state * self._physical_system.limits diff --git a/gym_electric_motor/physical_system_wrappers/flux_observer.py b/src/gym_electric_motor/physical_system_wrappers/flux_observer.py similarity index 80% rename from gym_electric_motor/physical_system_wrappers/flux_observer.py rename to src/gym_electric_motor/physical_system_wrappers/flux_observer.py index 06eff3a8..22418b0e 100644 --- a/gym_electric_motor/physical_system_wrappers/flux_observer.py +++ b/src/gym_electric_motor/physical_system_wrappers/flux_observer.py @@ -62,28 +62,17 @@ def set_physical_system(self, physical_system): self._l_r = mp["l_m"] + mp["l_sigr"] # Induction of the rotor self._r_r = mp["r_r"] # Rotor resistance self._p = mp["p"] # Pole pair number - psi_limit = ( - self._l_m - * physical_system.limits[physical_system.state_names.index("i_sd")] - ) + psi_limit = self._l_m * physical_system.limits[physical_system.state_names.index("i_sd")] low = np.concatenate((physical_system.state_space.low, [-psi_limit, -np.pi])) high = np.concatenate((physical_system.state_space.high, [psi_limit, np.pi])) self.state_space = gymnasium.spaces.Box(low, high, dtype=np.float64) - self._current_indices = [ - physical_system.state_positions[name] for name in self._current_names - ] + self._current_indices = [physical_system.state_positions[name] for name in self._current_names] self._limits = np.concatenate((physical_system.limits, [psi_limit, np.pi])) - self._nominal_state = np.concatenate( - (physical_system.nominal_state, [psi_limit, np.pi]) - ) + self._nominal_state = np.concatenate((physical_system.nominal_state, [psi_limit, np.pi])) self._state_names = physical_system.state_names + ["psi_abs", "psi_angle"] - self._state_positions = { - key: index for index, key in enumerate(self._state_names) - } + self._state_positions = {key: index for index, key in enumerate(self._state_names)} - self._i_s_idx = [ - physical_system.state_positions[name] for name in self._current_names - ] + self._i_s_idx = [physical_system.state_positions[name] for name in self._current_names] self._omega_idx = physical_system.state_positions["omega"] return self @@ -103,17 +92,10 @@ def simulate(self, action): [i_s_alpha, i_s_beta] = self._abc_to_alphabeta_transformation(i_s) # Calculate delta flux - delta_psi = complex( - i_s_alpha, i_s_beta - ) * self._r_r * self._l_m / self._l_r - self._integrated * complex( + delta_psi = complex(i_s_alpha, i_s_beta) * self._r_r * self._l_m / self._l_r - self._integrated * complex( self._r_r / self._l_r, -omega ) # Integrate the flux self._integrated += delta_psi * self._physical_system.tau - return ( - np.concatenate( - (state, [np.abs(self._integrated), np.angle(self._integrated)]) - ) - / self._limits - ) + return np.concatenate((state, [np.abs(self._integrated), np.angle(self._integrated)])) / self._limits diff --git a/gym_electric_motor/physical_system_wrappers/physical_system_wrapper.py b/src/gym_electric_motor/physical_system_wrappers/physical_system_wrapper.py similarity index 97% rename from gym_electric_motor/physical_system_wrappers/physical_system_wrapper.py rename to src/gym_electric_motor/physical_system_wrappers/physical_system_wrapper.py index 8b8ce183..92396fee 100644 --- a/gym_electric_motor/physical_system_wrappers/physical_system_wrapper.py +++ b/src/gym_electric_motor/physical_system_wrappers/physical_system_wrapper.py @@ -99,9 +99,7 @@ def set_physical_system(self, physical_system): self._physical_system = physical_system self._action_space = physical_system.action_space self._state_names = physical_system.state_names - self._state_positions = { - key: index for index, key in enumerate(self._state_names) - } + self._state_positions = {key: index for index, key in enumerate(self._state_names)} self._tau = physical_system.tau return self diff --git a/gym_electric_motor/physical_system_wrappers/state_noise_processor.py b/src/gym_electric_motor/physical_system_wrappers/state_noise_processor.py similarity index 91% rename from gym_electric_motor/physical_system_wrappers/state_noise_processor.py rename to src/gym_electric_motor/physical_system_wrappers/state_noise_processor.py index 88710703..76d86632 100644 --- a/gym_electric_motor/physical_system_wrappers/state_noise_processor.py +++ b/src/gym_electric_motor/physical_system_wrappers/state_noise_processor.py @@ -65,9 +65,7 @@ def __init__( def set_physical_system(self, physical_system): # Docstring from super class super().set_physical_system(physical_system) - self._state_indices = [ - physical_system.state_positions[state_name] for state_name in self._states - ] + self._state_indices = [physical_system.state_positions[state_name] for state_name in self._states] return self def reset(self): @@ -89,9 +87,7 @@ def _add_noise(self, state): Returns: numpy.ndarray[float]): The state with additional noise. """ - state[self._state_indices] = ( - state[self._state_indices] + self._noise[self._random_pointer] - ) + state[self._state_indices] = state[self._state_indices] + self._noise[self._random_pointer] self._random_pointer += 1 return state @@ -99,6 +95,4 @@ def _new_noise(self): """Samples new noise from the random distribution for the next steps.""" self._random_pointer = 0 fct = getattr(self._random_generator, self._random_dist) - self._noise = fct( - size=(self._random_length, len(self._state_indices)), **self._random_kwargs - ) + self._noise = fct(size=(self._random_length, len(self._state_indices)), **self._random_kwargs) diff --git a/gym_electric_motor/physical_systems/__init__.py b/src/gym_electric_motor/physical_systems/__init__.py similarity index 92% rename from gym_electric_motor/physical_systems/__init__.py rename to src/gym_electric_motor/physical_systems/__init__.py index 32cad4bd..f25c6a7f 100644 --- a/gym_electric_motor/physical_systems/__init__.py +++ b/src/gym_electric_motor/physical_systems/__init__.py @@ -75,12 +75,8 @@ register_class(DcMotorSystem, PhysicalSystem, "DcMotorSystem") register_class(SynchronousMotorSystem, PhysicalSystem, "SyncMotorSystem") -register_class( - SquirrelCageInductionMotorSystem, PhysicalSystem, "SquirrelCageInductionMotorSystem" -) -register_class( - DoublyFedInductionMotorSystem, PhysicalSystem, "DoublyFedInductionMotorSystem" -) +register_class(SquirrelCageInductionMotorSystem, PhysicalSystem, "SquirrelCageInductionMotorSystem") +register_class(DoublyFedInductionMotorSystem, PhysicalSystem, "DoublyFedInductionMotorSystem") register_class(FiniteOneQuadrantConverter, PowerElectronicConverter, "Finite-1QC") register_class(ContOneQuadrantConverter, PowerElectronicConverter, "Cont-1QC") diff --git a/gym_electric_motor/physical_systems/converters.py b/src/gym_electric_motor/physical_systems/converters.py similarity index 89% rename from gym_electric_motor/physical_systems/converters.py rename to src/gym_electric_motor/physical_systems/converters.py index 6144916f..d45afb92 100644 --- a/gym_electric_motor/physical_systems/converters.py +++ b/src/gym_electric_motor/physical_systems/converters.py @@ -145,9 +145,7 @@ def __init__(self, tau=1e-4, **kwargs): def set_action(self, action, t): # Docstring in base class - return super().set_action( - min(max(action, self.action_space.low), self.action_space.high), t - ) + return super().set_action(min(max(action, self.action_space.low), self.action_space.high), t) def convert(self, i_out, t): # Docstring in base class @@ -303,12 +301,7 @@ def i_sup(self, i_out): def _set_switching_pattern(self, action): # Docstring in base class - if ( - action == 0 - or self._switching_state == 0 - or action == self._switching_state - or self._interlocking_time == 0 - ): + if action == 0 or self._switching_state == 0 or action == self._switching_state or self._interlocking_time == 0: self._switching_pattern = [action] return [self._action_start_time + self._tau] else: @@ -358,10 +351,7 @@ def reset(self): def convert(self, i_out, t): # Docstring in base class - return [ - self._subconverters[0].convert(i_out, t)[0] - - self._subconverters[1].convert([-i_out[0]], t)[0] - ] + return [self._subconverters[0].convert(i_out, t)[0] - self._subconverters[1].convert([-i_out[0]], t)[0]] def set_action(self, action, t): # Docstring in base class @@ -377,9 +367,7 @@ def set_action(self, action, t): def i_sup(self, i_out): # Docstring in base class - return self._subconverters[0].i_sup(i_out) + self._subconverters[1].i_sup( - [-i_out[0]] - ) + return self._subconverters[0].i_sup(i_out) + self._subconverters[1].i_sup([-i_out[0]]) class ContOneQuadrantConverter(ContDynamicallyAveragedConverter): @@ -445,9 +433,7 @@ def i_sup(self, i_out): interlocking_current = 1 if i_out[0] < 0 else 0 return ( self._current_action[0] - + self._interlocking_time - / self._tau - * (interlocking_current - self._current_action[0]) + + self._interlocking_time / self._tau * (interlocking_current - self._current_action[0]) ) * i_out[0] @@ -496,10 +482,7 @@ def reset(self): def convert(self, i_out, t): # Docstring in base class - return [ - self._subconverters[0].convert(i_out, t)[0] - - self._subconverters[1].convert(i_out, t)[0] - ] + return [self._subconverters[0].convert(i_out, t)[0] - self._subconverters[1].convert(i_out, t)[0]] def set_action(self, action, t): # Docstring in base class @@ -511,9 +494,7 @@ def set_action(self, action, t): def i_sup(self, i_out): # Docstring in base class - return self._subconverters[0].i_sup(i_out) + self._subconverters[1].i_sup( - [-i_out[0]] - ) + return self._subconverters[0].i_sup(i_out) + self._subconverters[1].i_sup([-i_out[0]]) class FiniteMultiConverter(FiniteConverter): @@ -556,8 +537,7 @@ def __init__(self, subconverters, **kwargs): """ super().__init__(**kwargs) self._sub_converters = [ - instantiate(PowerElectronicConverter, subconverter, **kwargs) - for subconverter in subconverters + instantiate(PowerElectronicConverter, subconverter, **kwargs) for subconverter in subconverters ] self.subsignal_current_space_dims = [] self.subsignal_voltage_space_dims = [] @@ -569,12 +549,8 @@ def __init__(self, subconverters, **kwargs): # get the limits and space dims from each subconverter for subconverter in self._sub_converters: - self.subsignal_current_space_dims.append( - np.squeeze(subconverter.currents.shape) or 1 - ) - self.subsignal_voltage_space_dims.append( - np.squeeze(subconverter.voltages.shape) or 1 - ) + self.subsignal_current_space_dims.append(np.squeeze(subconverter.currents.shape) or 1) + self.subsignal_voltage_space_dims.append(np.squeeze(subconverter.voltages.shape) or 1) self.action_space.append(subconverter.action_space.n) @@ -603,9 +579,7 @@ def convert(self, i_out, t): # Docstring in base class u_in = [] subsignal_idx_low = 0 - for subconverter, subsignal_space_size in zip( - self._sub_converters, self.subsignal_voltage_space_dims - ): + for subconverter, subsignal_space_size in zip(self._sub_converters, self.subsignal_voltage_space_dims): subsignal_idx_high = subsignal_idx_low + subsignal_space_size u_in += subconverter.convert(i_out[subsignal_idx_low:subsignal_idx_high], t) subsignal_idx_low = subsignal_idx_high @@ -629,9 +603,7 @@ def i_sup(self, i_out): # Docstring in base class i_sup = 0 subsignal_idx_low = 0 - for subconverter, subsignal_space_size in zip( - self._sub_converters, self.subsignal_current_space_dims - ): + for subconverter, subsignal_space_size in zip(self._sub_converters, self.subsignal_current_space_dims): subsignal_idx_high = subsignal_idx_low + subsignal_space_size i_sup += subconverter.i_sup(i_out[subsignal_idx_low:subsignal_idx_high]) subsignal_idx_low = subsignal_idx_high @@ -676,8 +648,7 @@ def __init__(self, subconverters, **kwargs): """ super().__init__(**kwargs) self._sub_converters = [ - instantiate(PowerElectronicConverter, subconverter, **kwargs) - for subconverter in subconverters + instantiate(PowerElectronicConverter, subconverter, **kwargs) for subconverter in subconverters ] self.subsignal_current_space_dims = [] @@ -691,12 +662,8 @@ def __init__(self, subconverters, **kwargs): # get the limits and space dims from each subconverter for subconverter in self._sub_converters: - self.subsignal_current_space_dims.append( - np.squeeze(subconverter.currents.shape) or 1 - ) - self.subsignal_voltage_space_dims.append( - np.squeeze(subconverter.voltages.shape) or 1 - ) + self.subsignal_current_space_dims.append(np.squeeze(subconverter.currents.shape) or 1) + self.subsignal_voltage_space_dims.append(np.squeeze(subconverter.voltages.shape) or 1) action_space_low.append(subconverter.action_space.low) action_space_high.append(subconverter.action_space.high) @@ -746,9 +713,7 @@ def convert(self, i_out, t): # Docstring in base class u_in = [] subsignal_idx_low = 0 - for subconverter, subsignal_space_size in zip( - self._sub_converters, self.subsignal_voltage_space_dims - ): + for subconverter, subsignal_space_size in zip(self._sub_converters, self.subsignal_voltage_space_dims): subsignal_idx_high = subsignal_idx_low + subsignal_space_size u_in += subconverter.convert(i_out[subsignal_idx_low:subsignal_idx_high], t) subsignal_idx_low = subsignal_idx_high @@ -762,9 +727,7 @@ def i_sup(self, i_out): # Docstring in base class i_sup = 0 subsignal_idx_low = 0 - for subconverter, subsignal_space_size in zip( - self._sub_converters, self.subsignal_current_space_dims - ): + for subconverter, subsignal_space_size in zip(self._sub_converters, self.subsignal_current_space_dims): subsignal_idx_high = subsignal_idx_low + subsignal_space_size i_sup += subconverter.i_sup(i_out[subsignal_idx_low:subsignal_idx_high]) subsignal_idx_low = subsignal_idx_high @@ -868,12 +831,7 @@ def set_action(self, action, t): def i_sup(self, i_out): # Docstring in base class - return sum( - [ - subconverter.i_sup([i_out_]) - for subconverter, i_out_ in zip(self._sub_converters, i_out) - ] - ) + return sum([subconverter.i_sup([i_out_]) for subconverter, i_out_ in zip(self._sub_converters, i_out)]) class ContB6BridgeConverter(ContDynamicallyAveragedConverter): @@ -945,9 +903,4 @@ def _convert(self, i_in, t): def i_sup(self, i_out): # Docstring in base class - return sum( - [ - subconverter.i_sup([i_out_]) - for subconverter, i_out_ in zip(self._subconverters, i_out) - ] - ) + return sum([subconverter.i_sup([i_out_]) for subconverter, i_out_ in zip(self._subconverters, i_out)]) diff --git a/gym_electric_motor/physical_systems/electric_motors/__init__.py b/src/gym_electric_motor/physical_systems/electric_motors/__init__.py similarity index 100% rename from gym_electric_motor/physical_systems/electric_motors/__init__.py rename to src/gym_electric_motor/physical_systems/electric_motors/__init__.py diff --git a/gym_electric_motor/physical_systems/electric_motors/dc_externally_excited_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/dc_externally_excited_motor.py similarity index 91% rename from gym_electric_motor/physical_systems/electric_motors/dc_externally_excited_motor.py rename to src/gym_electric_motor/physical_systems/electric_motors/dc_externally_excited_motor.py index ca3f85dd..1f9f103c 100644 --- a/gym_electric_motor/physical_systems/electric_motors/dc_externally_excited_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/dc_externally_excited_motor.py @@ -35,7 +35,6 @@ def _update_limits(self): "u_a": self._default_limits["u"], "u_e": self._default_limits["u"], "i_a": self._limits.get("i", None) or self._limits["u"] / r_a, - "i_e": self._limits.get("i", None) - or self._limits["u"] / self.motor_parameter["r_e"], + "i_e": self._limits.get("i", None) or self._limits["u"] / self.motor_parameter["r_e"], } super()._update_limits(limit_agenda) diff --git a/gym_electric_motor/physical_systems/electric_motors/dc_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/dc_motor.py similarity index 81% rename from gym_electric_motor/physical_systems/electric_motors/dc_motor.py rename to src/gym_electric_motor/physical_systems/electric_motors/dc_motor.py index 7d83cf9d..874f0e43 100644 --- a/gym_electric_motor/physical_systems/electric_motors/dc_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/dc_motor.py @@ -70,12 +70,8 @@ class DcMotor(ElectricMotor): "l_e": 5.4e-3, "j_rotor": 0.0025, } - _default_nominal_values = dict( - omega=300, torque=16.0, i=97, i_a=97, i_e=97, u=60, u_a=60, u_e=60 - ) - _default_limits = dict( - omega=400, torque=38.0, i=210, i_a=210, i_e=210, u=60, u_a=60, u_e=60 - ) + _default_nominal_values = dict(omega=300, torque=16.0, i=97, i_a=97, i_e=97, u=60, u_a=60, u_e=60) + _default_limits = dict(omega=400, torque=38.0, i=210, i_a=210, i_e=210, u=60, u_a=60, u_e=60) _default_initializer = { "states": {"i_a": 0.0, "i_e": 0.0}, "interval": None, @@ -91,9 +87,7 @@ def __init__( motor_initializer=None, ): # Docstring of superclass - super().__init__( - motor_parameter, nominal_values, limit_values, motor_initializer - ) + super().__init__(motor_parameter, nominal_values, limit_values, motor_initializer) #: Matrix that contains the constant parameters of the systems equation for faster computation self._model_constants = None self._update_model() @@ -105,23 +99,13 @@ def _update_model(self): Called internally when the motor parameters are changed or the motor is initialized. """ mp = self._motor_parameter - self._model_constants = np.array( - [[-mp["r_a"], 0, -mp["l_e_prime"], 1, 0], [0, -mp["r_e"], 0, 0, 1]] - ) - self._model_constants[self.I_A_IDX] = ( - self._model_constants[self.I_A_IDX] / mp["l_a"] - ) - self._model_constants[self.I_E_IDX] = ( - self._model_constants[self.I_E_IDX] / mp["l_e"] - ) + self._model_constants = np.array([[-mp["r_a"], 0, -mp["l_e_prime"], 1, 0], [0, -mp["r_e"], 0, 0, 1]]) + self._model_constants[self.I_A_IDX] = self._model_constants[self.I_A_IDX] / mp["l_a"] + self._model_constants[self.I_E_IDX] = self._model_constants[self.I_E_IDX] / mp["l_e"] def torque(self, currents): # Docstring of superclass - return ( - self._motor_parameter["l_e_prime"] - * currents[self.I_A_IDX] - * currents[self.I_E_IDX] - ) + return self._motor_parameter["l_e_prime"] * currents[self.I_A_IDX] * currents[self.I_E_IDX] def i_in(self, currents): # Docstring of superclass @@ -156,14 +140,8 @@ def get_state_space(self, input_currents, input_voltages): a_converter = 0 e_converter = 1 low = { - "omega": -1 - if input_voltages.low[a_converter] == -1 - or input_voltages.low[e_converter] == -1 - else 0, - "torque": -1 - if input_currents.low[a_converter] == -1 - or input_currents.low[e_converter] == -1 - else 0, + "omega": -1 if input_voltages.low[a_converter] == -1 or input_voltages.low[e_converter] == -1 else 0, + "torque": -1 if input_currents.low[a_converter] == -1 or input_currents.low[e_converter] == -1 else 0, "i_a": -1 if input_currents.low[a_converter] == -1 else 0, "i_e": -1 if input_currents.low[e_converter] == -1 else 0, "u_a": -1 if input_voltages.low[a_converter] == -1 else 0, @@ -178,7 +156,5 @@ def _update_limits(self, limits_d=None, nominal_d=None): limits_d = dict() # torque is replaced the same way for all DC motors - limits_d.update( - dict(torque=self.torque([self._limits[state] for state in self.CURRENTS])) - ) + limits_d.update(dict(torque=self.torque([self._limits[state] for state in self.CURRENTS]))) super()._update_limits(limits_d) diff --git a/gym_electric_motor/physical_systems/electric_motors/dc_permanently_excited_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/dc_permanently_excited_motor.py similarity index 97% rename from gym_electric_motor/physical_systems/electric_motors/dc_permanently_excited_motor.py rename to src/gym_electric_motor/physical_systems/electric_motors/dc_permanently_excited_motor.py index 4c1f2a94..e144a754 100644 --- a/gym_electric_motor/physical_systems/electric_motors/dc_permanently_excited_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/dc_permanently_excited_motor.py @@ -80,9 +80,7 @@ def i_in(self, state): def electrical_ode(self, state, u_in, omega, *_): # Docstring of superclass - self._ode_placeholder[:] = ( - [omega] + np.atleast_1d(state[self.I_IDX]).tolist() + [u_in[0]] - ) + self._ode_placeholder[:] = [omega] + np.atleast_1d(state[self.I_IDX]).tolist() + [u_in[0]] return np.matmul(self._model_constants, self._ode_placeholder) def electrical_jacobian(self, state, u_in, omega, *_): diff --git a/gym_electric_motor/physical_systems/electric_motors/dc_series_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/dc_series_motor.py similarity index 87% rename from gym_electric_motor/physical_systems/electric_motors/dc_series_motor.py rename to src/gym_electric_motor/physical_systems/electric_motors/dc_series_motor.py index 72f300d0..aa948a18 100644 --- a/gym_electric_motor/physical_systems/electric_motors/dc_series_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/dc_series_motor.py @@ -56,12 +56,8 @@ class DcSeriesMotor(DcMotor): "l_e": 5.4e-3, "j_rotor": 0.0025, } - _default_nominal_values = dict( - omega=300, torque=16.0, i=97, i_a=97, i_e=97, u=60, u_a=60, u_e=60 - ) - _default_limits = dict( - omega=400, torque=38.0, i=210, i_a=210, i_e=210, u=60, u_a=60, u_e=60 - ) + _default_nominal_values = dict(omega=300, torque=16.0, i=97, i_a=97, i_e=97, u=60, u_a=60, u_e=60) + _default_limits = dict(omega=400, torque=38.0, i=210, i_a=210, i_e=210, u=60, u_a=60, u_e=60) _default_initializer = { "states": {"i": 0.0}, "interval": None, @@ -72,12 +68,8 @@ class DcSeriesMotor(DcMotor): def _update_model(self): # Docstring of superclass mp = self._motor_parameter - self._model_constants = np.array( - [[-mp["r_a"] - mp["r_e"], -mp["l_e_prime"], 1]] - ) - self._model_constants[self.I_IDX] = self._model_constants[self.I_IDX] / ( - mp["l_a"] + mp["l_e"] - ) + self._model_constants = np.array([[-mp["r_a"] - mp["r_e"], -mp["l_e_prime"], 1]]) + self._model_constants[self.I_IDX] = self._model_constants[self.I_IDX] / (mp["l_a"] + mp["l_e"]) def torque(self, currents): # Docstring of superclass @@ -125,14 +117,7 @@ def get_state_space(self, input_currents, input_voltages): def electrical_jacobian(self, state, u_in, omega, *_): mp = self._motor_parameter return ( - np.array( - [ - [ - -(mp["r_a"] + mp["r_e"] + mp["l_e_prime"] * omega) - / (mp["l_a"] + mp["l_e"]) - ] - ] - ), + np.array([[-(mp["r_a"] + mp["r_e"] + mp["l_e_prime"] * omega) / (mp["l_a"] + mp["l_e"])]]), np.array([-mp["l_e_prime"] * state[self.I_IDX] / (mp["l_a"] + mp["l_e"])]), np.array([2 * mp["l_e_prime"] * state[self.I_IDX]]), ) diff --git a/gym_electric_motor/physical_systems/electric_motors/dc_shunt_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/dc_shunt_motor.py similarity index 93% rename from gym_electric_motor/physical_systems/electric_motors/dc_shunt_motor.py rename to src/gym_electric_motor/physical_systems/electric_motors/dc_shunt_motor.py index 12276050..6d5f8bad 100644 --- a/gym_electric_motor/physical_systems/electric_motors/dc_shunt_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/dc_shunt_motor.py @@ -56,12 +56,8 @@ class DcShuntMotor(DcMotor): "l_e": 5.4e-3, "j_rotor": 0.0025, } - _default_nominal_values = dict( - omega=300, torque=16.0, i=97, i_a=97, i_e=97, u=60, u_a=60, u_e=60 - ) - _default_limits = dict( - omega=400, torque=38.0, i=210, i_a=210, i_e=210, u=60, u_a=60, u_e=60 - ) + _default_nominal_values = dict(omega=300, torque=16.0, i=97, i_a=97, i_e=97, u=60, u_a=60, u_e=60) + _default_limits = dict(omega=400, torque=38.0, i=210, i_a=210, i_e=210, u=60, u_a=60, u_e=60) _default_initializer = { "states": {"i_a": 0.0, "i_e": 0.0}, "interval": None, @@ -133,8 +129,7 @@ def _update_limits(self, limits_d=None, nominal_d=None): limit_agenda = { "u": self._default_limits["u"], "i_a": self._limits.get("i", None) or self._limits["u"] / r_a, - "i_e": self._limits.get("i", None) - or self._limits["u"] / self.motor_parameter["r_e"], + "i_e": self._limits.get("i", None) or self._limits["u"] / self.motor_parameter["r_e"], } super()._update_limits(limit_agenda) diff --git a/gym_electric_motor/physical_systems/electric_motors/doubly_fed_induction_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/doubly_fed_induction_motor.py similarity index 91% rename from gym_electric_motor/physical_systems/electric_motors/doubly_fed_induction_motor.py rename to src/gym_electric_motor/physical_systems/electric_motors/doubly_fed_induction_motor.py index e4a5c6f4..a670df78 100644 --- a/gym_electric_motor/physical_systems/electric_motors/doubly_fed_induction_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/doubly_fed_induction_motor.py @@ -111,12 +111,8 @@ class DoublyFedInductionMotor(InductionMotor): "r_r": 3.51, } - _default_limits = dict( - omega=1800 * np.pi / 30, torque=0.0, i=9, epsilon=math.pi, u=720 - ) - _default_nominal_values = dict( - omega=1650 * np.pi / 30, torque=0.0, i=7.5, epsilon=math.pi, u=720 - ) + _default_limits = dict(omega=1800 * np.pi / 30, torque=0.0, i=9, epsilon=math.pi, u=720) + _default_nominal_values = dict(omega=1650 * np.pi / 30, torque=0.0, i=7.5, epsilon=math.pi, u=720) _default_initializer = { "states": { "i_salpha": 0.0, @@ -148,13 +144,9 @@ def _update_limits(self, limit_values={}, nominal_values={}): ): limits_agenda[u] = voltage_limit nominal_agenda[u] = voltage_nominal - limits_agenda[i] = ( - self._limits.get("i", None) - or self._limits[u] / self._motor_parameter["r_r"] - ) + limits_agenda[i] = self._limits.get("i", None) or self._limits[u] / self._motor_parameter["r_r"] nominal_agenda[i] = ( - self._nominal_values.get("i", None) - or self._nominal_values[u] / self._motor_parameter["r_r"] + self._nominal_values.get("i", None) or self._nominal_values[u] / self._motor_parameter["r_r"] ) super()._update_limits(limits_agenda, nominal_agenda) @@ -168,7 +160,5 @@ def _update_initial_limits(self, nominal_new={}, omega=None): u_q_max=self._nominal_values["u_sq"], u_rq_max=self._nominal_values["u_rq"], ) - flux_nominal_limits = { - state: value for state, value in zip(self.FLUXES, flux_alphabeta_limits) - } + flux_nominal_limits = {state: value for state, value in zip(self.FLUXES, flux_alphabeta_limits)} super()._update_initial_limits(flux_nominal_limits) diff --git a/gym_electric_motor/physical_systems/electric_motors/electric_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/electric_motor.py similarity index 86% rename from gym_electric_motor/physical_systems/electric_motors/electric_motor.py rename to src/gym_electric_motor/physical_systems/electric_motors/electric_motor.py index 6173c57d..9e0c0e2c 100644 --- a/gym_electric_motor/physical_systems/electric_motors/electric_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/electric_motor.py @@ -124,19 +124,13 @@ def __init__( RandomComponent.__init__(self) motor_parameter = motor_parameter or {} self._motor_parameter = self._default_motor_parameter.copy() - self._motor_parameter = update_parameter_dict( - self._default_motor_parameter, motor_parameter - ) + self._motor_parameter = update_parameter_dict(self._default_motor_parameter, motor_parameter) limit_values = limit_values or {} self._limits = update_parameter_dict(self._default_limits, limit_values) nominal_values = nominal_values or {} - self._nominal_values = update_parameter_dict( - self._default_nominal_values, nominal_values - ) + self._nominal_values = update_parameter_dict(self._default_nominal_values, nominal_values) motor_initializer = motor_initializer or {} - self._initializer = update_parameter_dict( - self._default_initializer, motor_initializer - ) + self._initializer = update_parameter_dict(self._default_initializer, motor_initializer) self._initial_states = {} if self._initializer["states"] is not None: self._initial_states.update(self._initializer["states"]) @@ -218,42 +212,27 @@ def initialize(self, state_space, state_positions, **__): lower_bound = upper_bound * state_space_low else: if isinstance(self._nominal_values, dict): - nominal_values_ = [ - self._nominal_values[state] for state in self._initial_states.keys() - ] + nominal_values_ = [self._nominal_values[state] for state in self._initial_states.keys()] nominal_values_ = np.asarray(nominal_values_) else: nominal_values_ = np.asarray(self._nominal_values) - state_space_idx = [ - state_positions[state] for state in self._initial_states.keys() - ] + state_space_idx = [state_positions[state] for state in self._initial_states.keys()] upper_bound = np.asarray(nominal_values_, dtype=float) - lower_bound = ( - upper_bound * np.asarray(state_space.low, dtype=float)[state_space_idx] - ) + lower_bound = upper_bound * np.asarray(state_space.low, dtype=float)[state_space_idx] # clip nominal boundaries to user defined if interval is not None: - lower_bound = np.clip( - lower_bound, a_min=np.asarray(interval, dtype=float).T[0], a_max=None - ) - upper_bound = np.clip( - upper_bound, a_min=None, a_max=np.asarray(interval, dtype=float).T[1] - ) + lower_bound = np.clip(lower_bound, a_min=np.asarray(interval, dtype=float).T[0], a_max=None) + upper_bound = np.clip(upper_bound, a_min=None, a_max=np.asarray(interval, dtype=float).T[1]) # random initialization for each motor state (current, epsilon) if random_dist is not None: if random_dist == "uniform": - initial_value = ( - upper_bound - lower_bound - ) * self._random_generator.uniform( + initial_value = (upper_bound - lower_bound) * self._random_generator.uniform( size=len(self._initial_states.keys()) ) + lower_bound # writing initial values in initial_states dict - random_states = { - state: initial_value[idx] - for idx, state in enumerate(self._initial_states.keys()) - } + random_states = {state: initial_value[idx] for idx, state in enumerate(self._initial_states.keys())} self._initial_states.update(random_states) elif random_dist in ["normal", "gaussian"]: @@ -270,10 +249,7 @@ def initialize(self, state_space, state_positions, **__): random_state=self.seed_sequence.pool[0], ) # writing initial values in initial_states dict - random_states = { - state: initial_value[idx] - for idx, state in enumerate(self._initial_states.keys()) - } + random_states = {state: initial_value[idx] for idx, state in enumerate(self._initial_states.keys())} self._initial_states.update(random_states) else: @@ -282,18 +258,11 @@ def initialize(self, state_space, state_positions, **__): elif self._initial_states is not None: initial_value = np.atleast_1d(list(self._initial_states.values())) # check init_value meets interval boundaries - if (lower_bound <= initial_value).all() and ( - initial_value <= upper_bound - ).all(): - initial_states_ = { - state: initial_value[idx] - for idx, state in enumerate(self._initial_states.keys()) - } + if (lower_bound <= initial_value).all() and (initial_value <= upper_bound).all(): + initial_states_ = {state: initial_value[idx] for idx, state in enumerate(self._initial_states.keys())} self._initial_states.update(initial_states_) else: - raise Exception( - "Initialization value has to be within nominal boundaries" - ) + raise Exception("Initialization value has to be within nominal boundaries") else: raise Exception("No matching Initialization Case") diff --git a/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py similarity index 89% rename from gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py rename to src/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py index ad8d4bf3..32303997 100644 --- a/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py @@ -107,12 +107,8 @@ class ExternallyExcitedSynchronousMotor(SynchronousMotor): "r_e": 7.2e-3, } HAS_JACOBIAN = True - _default_limits = dict( - omega=12e3 * np.pi / 30, torque=0.0, i=150, i_e=150, epsilon=math.pi, u=320 - ) - _default_nominal_values = dict( - omega=4.3e3 * np.pi / 30, torque=0.0, i=120, i_e=150, epsilon=math.pi, u=320 - ) + _default_limits = dict(omega=12e3 * np.pi / 30, torque=0.0, i=150, i_e=150, epsilon=math.pi, u=320) + _default_nominal_values = dict(omega=4.3e3 * np.pi / 30, torque=0.0, i=120, i_e=150, epsilon=math.pi, u=320) _default_initializer = { "states": {"i_sq": 0.0, "i_sd": 0.0, "i_e": 0.0, "epsilon": 0.0}, "interval": None, @@ -170,15 +166,9 @@ def _update_model(self): ] ) - self._model_constants[self.I_SD_IDX] = ( - self._model_constants[self.I_SD_IDX] / mp["l_d"] - ) - self._model_constants[self.I_SQ_IDX] = ( - self._model_constants[self.I_SQ_IDX] / mp["l_q"] - ) - self._model_constants[self.I_E_IDX] = ( - self._model_constants[self.I_E_IDX] / mp["l_e"] - ) + self._model_constants[self.I_SD_IDX] = self._model_constants[self.I_SD_IDX] / mp["l_d"] + self._model_constants[self.I_SQ_IDX] = self._model_constants[self.I_SQ_IDX] / mp["l_q"] + self._model_constants[self.I_E_IDX] = self._model_constants[self.I_E_IDX] / mp["l_e"] def electrical_ode(self, state, u_dq, omega, *_): """ @@ -232,10 +222,7 @@ def torque(self, currents): return ( 1.5 * mp["p"] - * ( - mp["l_m"] * currents[self.I_E_IDX] - + (mp["l_d"] - mp["l_q"]) * currents[self.I_SD_IDX] - ) + * (mp["l_m"] * currents[self.I_E_IDX] + (mp["l_d"] - mp["l_q"]) * currents[self.I_SD_IDX]) * currents[self.I_SQ_IDX] ) @@ -259,11 +246,7 @@ def electrical_jacobian(self, state, u_in, omega, *args): ], [ mp["l_m"] * mp["r_s"] / (sigma * mp["l_d"] * mp["l_e"]), - -omega - * mp["p"] - * mp["l_m"] - * mp["l_q"] - / (sigma * mp["l_d"] * mp["l_e"]), + -omega * mp["p"] * mp["l_m"] * mp["l_q"] / (sigma * mp["l_d"] * mp["l_e"]), -mp["r_e"] / mp["l_e"], 0, ], @@ -282,12 +265,7 @@ def electrical_jacobian(self, state, u_in, omega, *args): np.array( [ # dT/dx 1.5 * mp["p"] * (mp["l_d"] - mp["l_q"]) * state[self.I_SQ_IDX], - 1.5 - * mp["p"] - * ( - mp["l_e"] * state[self.I_E_IDX] - + (mp["l_d"] - mp["l_q"]) * state[self.I_SD_IDX] - ), + 1.5 * mp["p"] * (mp["l_e"] * state[self.I_E_IDX] + (mp["l_d"] - mp["l_q"]) * state[self.I_SD_IDX]), 1.5 * mp["p"] * mp["l_e"] * state[self.I_SQ_IDX], 0, ] diff --git a/gym_electric_motor/physical_systems/electric_motors/induction_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/induction_motor.py similarity index 96% rename from gym_electric_motor/physical_systems/electric_motors/induction_motor.py rename to src/gym_electric_motor/physical_systems/electric_motors/induction_motor.py index 69d5985d..44ce0853 100644 --- a/gym_electric_motor/physical_systems/electric_motors/induction_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/induction_motor.py @@ -114,12 +114,8 @@ class InductionMotor(ThreePhaseMotor): "r_r": 1.355, } - _default_limits = dict( - omega=4e3 * np.pi / 30, torque=0.0, i=5.5, epsilon=math.pi, u=560 - ) - _default_nominal_values = dict( - omega=3e3 * np.pi / 30, torque=0.0, i=3.9, epsilon=math.pi, u=560 - ) + _default_limits = dict(omega=4e3 * np.pi / 30, torque=0.0, i=5.5, epsilon=math.pi, u=560) + _default_nominal_values = dict(omega=3e3 * np.pi / 30, torque=0.0, i=3.9, epsilon=math.pi, u=560) _model_constants = None _default_initializer = { "states": { @@ -402,14 +398,8 @@ def electrical_jacobian(self, state, u_in, omega, *args): ), np.array( [ # dx'/dw - mp["l_m"] - * mp["p"] - / (sigma * l_r * l_s) - * state[self.PSI_RBETA_IDX], - -mp["l_m"] - * mp["p"] - / (sigma * l_r * l_s) - * state[self.PSI_RALPHA_IDX], + mp["l_m"] * mp["p"] / (sigma * l_r * l_s) * state[self.PSI_RBETA_IDX], + -mp["l_m"] * mp["p"] / (sigma * l_r * l_s) * state[self.PSI_RALPHA_IDX], -mp["p"] * state[self.PSI_RBETA_IDX], mp["p"] * state[self.PSI_RALPHA_IDX], mp["p"], diff --git a/gym_electric_motor/physical_systems/electric_motors/permanent_magnet_synchronous_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/permanent_magnet_synchronous_motor.py similarity index 89% rename from gym_electric_motor/physical_systems/electric_motors/permanent_magnet_synchronous_motor.py rename to src/gym_electric_motor/physical_systems/electric_motors/permanent_magnet_synchronous_motor.py index 3cf8641b..33abd0b7 100644 --- a/gym_electric_motor/physical_systems/electric_motors/permanent_magnet_synchronous_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/permanent_magnet_synchronous_motor.py @@ -91,12 +91,8 @@ class PermanentMagnetSynchronousMotor(SynchronousMotor): "psi_p": 66e-3, } HAS_JACOBIAN = True - _default_limits = dict( - omega=4e3 * np.pi / 30, torque=0.0, i=400, epsilon=math.pi, u=300 - ) - _default_nominal_values = dict( - omega=3e3 * np.pi / 30, torque=0.0, i=240, epsilon=math.pi, u=300 - ) + _default_limits = dict(omega=4e3 * np.pi / 30, torque=0.0, i=400, epsilon=math.pi, u=300) + _default_nominal_values = dict(omega=3e3 * np.pi / 30, torque=0.0, i=240, epsilon=math.pi, u=300) _default_initializer = { "states": {"i_sq": 0.0, "i_sd": 0.0, "epsilon": 0.0}, "interval": None, @@ -119,12 +115,8 @@ def _update_model(self): ] ) - self._model_constants[self.I_SD_IDX] = ( - self._model_constants[self.I_SD_IDX] / mp["l_d"] - ) - self._model_constants[self.I_SQ_IDX] = ( - self._model_constants[self.I_SQ_IDX] / mp["l_q"] - ) + self._model_constants[self.I_SD_IDX] = self._model_constants[self.I_SD_IDX] / mp["l_d"] + self._model_constants[self.I_SQ_IDX] = self._model_constants[self.I_SQ_IDX] / mp["l_q"] def _torque_limit(self): # Docstring of superclass @@ -143,10 +135,7 @@ def torque(self, currents): # Docstring of superclass mp = self._motor_parameter return ( - 1.5 - * mp["p"] - * (mp["psi_p"] + (mp["l_d"] - mp["l_q"]) * currents[self.I_SD_IDX]) - * currents[self.I_SQ_IDX] + 1.5 * mp["p"] * (mp["psi_p"] + (mp["l_d"] - mp["l_q"]) * currents[self.I_SD_IDX]) * currents[self.I_SQ_IDX] ) def electrical_jacobian(self, state, u_in, omega, *args): @@ -170,17 +159,14 @@ def electrical_jacobian(self, state, u_in, omega, *args): np.array( [ # dx'/dw mp["p"] * mp["l_q"] / mp["l_d"] * state[self.I_SQ_IDX], - -mp["p"] * mp["l_d"] / mp["l_q"] * state[self.I_SD_IDX] - - mp["p"] * mp["psi_p"] / mp["l_q"], + -mp["p"] * mp["l_d"] / mp["l_q"] * state[self.I_SD_IDX] - mp["p"] * mp["psi_p"] / mp["l_q"], mp["p"], ] ), np.array( [ # dT/dx 1.5 * mp["p"] * (mp["l_d"] - mp["l_q"]) * state[self.I_SQ_IDX], - 1.5 - * mp["p"] - * (mp["psi_p"] + (mp["l_d"] - mp["l_q"]) * state[self.I_SD_IDX]), + 1.5 * mp["p"] * (mp["psi_p"] + (mp["l_d"] - mp["l_q"]) * state[self.I_SD_IDX]), 0, ] ), diff --git a/gym_electric_motor/physical_systems/electric_motors/squirrel_cage_induction_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/squirrel_cage_induction_motor.py similarity index 89% rename from gym_electric_motor/physical_systems/electric_motors/squirrel_cage_induction_motor.py rename to src/gym_electric_motor/physical_systems/electric_motors/squirrel_cage_induction_motor.py index cc3e7ea1..e4258775 100644 --- a/gym_electric_motor/physical_systems/electric_motors/squirrel_cage_induction_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/squirrel_cage_induction_motor.py @@ -102,12 +102,8 @@ class SquirrelCageInductionMotor(InductionMotor): "r_r": 1.355, } - _default_limits = dict( - omega=4e3 * np.pi / 30, torque=0.0, i=5.5, epsilon=math.pi, u=560 - ) - _default_nominal_values = dict( - omega=3e3 * np.pi / 30, torque=0.0, i=3.9, epsilon=math.pi, u=560 - ) + _default_limits = dict(omega=4e3 * np.pi / 30, torque=0.0, i=5.5, epsilon=math.pi, u=560) + _default_nominal_values = dict(omega=3e3 * np.pi / 30, torque=0.0, i=3.9, epsilon=math.pi, u=560) _default_initializer = { "states": { "i_salpha": 0.0, @@ -140,13 +136,9 @@ def _update_limits(self, limit_values={}, nominal_values={}): for u, i in zip(self.IO_VOLTAGES, self.IO_CURRENTS): limits_agenda[u] = voltage_limit nominal_agenda[u] = voltage_nominal - limits_agenda[i] = ( - self._limits.get("i", None) - or self._limits[u] / self._motor_parameter["r_s"] - ) + limits_agenda[i] = self._limits.get("i", None) or self._limits[u] / self._motor_parameter["r_s"] nominal_agenda[i] = ( - self._nominal_values.get("i", None) - or self._nominal_values[u] / self._motor_parameter["r_s"] + self._nominal_values.get("i", None) or self._nominal_values[u] / self._motor_parameter["r_s"] ) super()._update_limits(limits_agenda, nominal_agenda) @@ -154,15 +146,11 @@ def _update_initial_limits(self, nominal_new={}, omega=None): # Docstring of superclass # draw a sample magnetic field angle from [-pi,pi] eps_mag = 2 * np.pi * np.random.random_sample() - np.pi - flux_alphabeta_limits = self._flux_limit( - omega=omega, eps_mag=eps_mag, u_q_max=self._nominal_values["u_sq"] - ) + flux_alphabeta_limits = self._flux_limit(omega=omega, eps_mag=eps_mag, u_q_max=self._nominal_values["u_sq"]) # using absolute value, because limits should describe upper limit # after abs-operator, norm of alphabeta flux still equal to # d-component of flux flux_alphabeta_limits = np.abs(flux_alphabeta_limits) - flux_nominal_limits = { - state: value for state, value in zip(self.FLUXES, flux_alphabeta_limits) - } + flux_nominal_limits = {state: value for state, value in zip(self.FLUXES, flux_alphabeta_limits)} flux_nominal_limits.update(nominal_new) super()._update_initial_limits(flux_nominal_limits) diff --git a/gym_electric_motor/physical_systems/electric_motors/synchronous_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/synchronous_motor.py similarity index 94% rename from gym_electric_motor/physical_systems/electric_motors/synchronous_motor.py rename to src/gym_electric_motor/physical_systems/electric_motors/synchronous_motor.py index 81709764..f6b25d1c 100644 --- a/gym_electric_motor/physical_systems/electric_motors/synchronous_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/synchronous_motor.py @@ -105,9 +105,7 @@ def __init__( # Docstring of superclass nominal_values = nominal_values or {} limit_values = limit_values or {} - super().__init__( - motor_parameter, nominal_values, limit_values, motor_initializer - ) + super().__init__(motor_parameter, nominal_values, limit_values, motor_initializer) self._update_model() self._update_limits() @@ -184,12 +182,8 @@ def _update_limits(self): for u, i in zip(self.IO_VOLTAGES, self.IO_CURRENTS): limits_agenda[u] = voltage_limit nominal_agenda[u] = voltage_nominal - limits_agenda[i] = ( - self._limits.get("i", None) - or self._limits[u] / self._motor_parameter["r_s"] - ) + limits_agenda[i] = self._limits.get("i", None) or self._limits[u] / self._motor_parameter["r_s"] nominal_agenda[i] = ( - self._nominal_values.get("i", None) - or self._nominal_values[u] / self._motor_parameter["r_s"] + self._nominal_values.get("i", None) or self._nominal_values[u] / self._motor_parameter["r_s"] ) super()._update_limits(limits_agenda, nominal_agenda) diff --git a/gym_electric_motor/physical_systems/electric_motors/synchronous_reluctance_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/synchronous_reluctance_motor.py similarity index 92% rename from gym_electric_motor/physical_systems/electric_motors/synchronous_reluctance_motor.py rename to src/gym_electric_motor/physical_systems/electric_motors/synchronous_reluctance_motor.py index b6fec909..e34be75f 100644 --- a/gym_electric_motor/physical_systems/electric_motors/synchronous_reluctance_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/synchronous_reluctance_motor.py @@ -127,28 +127,17 @@ def _update_model(self): ] ) - self._model_constants[self.I_SD_IDX] = ( - self._model_constants[self.I_SD_IDX] / mp["l_d"] - ) - self._model_constants[self.I_SQ_IDX] = ( - self._model_constants[self.I_SQ_IDX] / mp["l_q"] - ) + self._model_constants[self.I_SD_IDX] = self._model_constants[self.I_SD_IDX] / mp["l_d"] + self._model_constants[self.I_SQ_IDX] = self._model_constants[self.I_SQ_IDX] / mp["l_q"] def _torque_limit(self): # Docstring of superclass - return self.torque( - [self._limits["i_sd"] / np.sqrt(2), self._limits["i_sq"] / np.sqrt(2), 0] - ) + return self.torque([self._limits["i_sd"] / np.sqrt(2), self._limits["i_sq"] / np.sqrt(2), 0]) def torque(self, currents): # Docstring of superclass mp = self._motor_parameter - return ( - 1.5 - * mp["p"] - * ((mp["l_d"] - mp["l_q"]) * currents[self.I_SD_IDX]) - * currents[self.I_SQ_IDX] - ) + return 1.5 * mp["p"] * ((mp["l_d"] - mp["l_q"]) * currents[self.I_SD_IDX]) * currents[self.I_SQ_IDX] def electrical_jacobian(self, state, u_in, omega, *_): mp = self._motor_parameter diff --git a/gym_electric_motor/physical_systems/electric_motors/three_phase_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/three_phase_motor.py similarity index 98% rename from gym_electric_motor/physical_systems/electric_motors/three_phase_motor.py rename to src/gym_electric_motor/physical_systems/electric_motors/three_phase_motor.py index ad1ea275..441e5706 100644 --- a/gym_electric_motor/physical_systems/electric_motors/three_phase_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/three_phase_motor.py @@ -58,9 +58,7 @@ def q(quantities, epsilon): """ cos = math.cos(epsilon) sin = math.sin(epsilon) - return cos * quantities[0] - sin * quantities[1], sin * quantities[ - 0 - ] + cos * quantities[1] + return cos * quantities[0] - sin * quantities[1], sin * quantities[0] + cos * quantities[1] @staticmethod def q_inv(quantities, epsilon): diff --git a/gym_electric_motor/physical_systems/mechanical_loads/__init__.py b/src/gym_electric_motor/physical_systems/mechanical_loads/__init__.py similarity index 100% rename from gym_electric_motor/physical_systems/mechanical_loads/__init__.py rename to src/gym_electric_motor/physical_systems/mechanical_loads/__init__.py diff --git a/gym_electric_motor/physical_systems/mechanical_loads/constant_speed_load.py b/src/gym_electric_motor/physical_systems/mechanical_loads/constant_speed_load.py similarity index 100% rename from gym_electric_motor/physical_systems/mechanical_loads/constant_speed_load.py rename to src/gym_electric_motor/physical_systems/mechanical_loads/constant_speed_load.py diff --git a/gym_electric_motor/physical_systems/mechanical_loads/external_speed_load.py b/src/gym_electric_motor/physical_systems/mechanical_loads/external_speed_load.py similarity index 95% rename from gym_electric_motor/physical_systems/mechanical_loads/external_speed_load.py rename to src/gym_electric_motor/physical_systems/mechanical_loads/external_speed_load.py index 37f1baa1..c0d8b868 100644 --- a/gym_electric_motor/physical_systems/mechanical_loads/external_speed_load.py +++ b/src/gym_electric_motor/physical_systems/mechanical_loads/external_speed_load.py @@ -64,9 +64,7 @@ def mechanical_ode(self, t, mechanical_state, torque=None): omega_next = self._speed_profile(t=t + self._tau, **self.speed_profile_kwargs) # calculated T out of euler-forward, given omega_next and # actual omega give from system - return np.array( - [(1 / self._tau) * (omega_next - mechanical_state[self.OMEGA_IDX])] - ) + return np.array([(1 / self._tau) * (omega_next - mechanical_state[self.OMEGA_IDX])]) def mechanical_jacobian(self, t, mechanical_state, torque): # Docstring of superclass diff --git a/gym_electric_motor/physical_systems/mechanical_loads/mechanical_load.py b/src/gym_electric_motor/physical_systems/mechanical_loads/mechanical_load.py similarity index 87% rename from gym_electric_motor/physical_systems/mechanical_loads/mechanical_load.py rename to src/gym_electric_motor/physical_systems/mechanical_loads/mechanical_load.py index f6ebbbd9..afc6c6de 100644 --- a/gym_electric_motor/physical_systems/mechanical_loads/mechanical_load.py +++ b/src/gym_electric_motor/physical_systems/mechanical_loads/mechanical_load.py @@ -94,9 +94,7 @@ def __init__(self, state_names=None, j_load=0.0, load_initializer=None): load_initializer = load_initializer or {} self._initializer = self._default_initializer.copy() self._initializer.update(load_initializer) - self._initial_states = self._initializer.get( - "states", {state: 0.0 for state in self._state_names} - ) + self._initial_states = self._initializer.get("states", {state: 0.0 for state in self._state_names}) def initialize(self, state_space, state_positions, nominal_state, **__): """Initializes the state of the load on an episode start. @@ -116,9 +114,7 @@ def initialize(self, state_space, state_positions, nominal_state, **__): if isinstance(nominal_state, (list, np.ndarray)): nominal_state = np.asarray(nominal_state, dtype=float) elif isinstance(self._nominal_values, dict): - nominal_state = [ - nominal_state[state] for state in self._initial_states.keys() - ] + nominal_state = [nominal_state[state] for state in self._initial_states.keys()] nominal_state = np.asarray(nominal_state) # setting nominal values as interval limits state_idx = [state_positions[state] for state in self._initial_states.keys()] @@ -126,26 +122,17 @@ def initialize(self, state_space, state_positions, nominal_state, **__): lower_bound = upper_bound * np.asarray(state_space.low, dtype=float)[state_idx] # clip nominal boundaries to user defined if interval is not None: - lower_bound = np.clip( - lower_bound, a_min=np.asarray(interval, dtype=float).T[0], a_max=None - ) - upper_bound = np.clip( - upper_bound, a_min=None, a_max=np.asarray(interval, dtype=float).T[1] - ) + lower_bound = np.clip(lower_bound, a_min=np.asarray(interval, dtype=float).T[0], a_max=None) + upper_bound = np.clip(upper_bound, a_min=None, a_max=np.asarray(interval, dtype=float).T[1]) else: pass # random initialization for each load state (omega) if random_dist is not None: if random_dist == "uniform": - initial_value = ( - upper_bound - lower_bound - ) * self.random_generator.uniform( + initial_value = (upper_bound - lower_bound) * self.random_generator.uniform( size=len(self._initial_states.keys()) ) + lower_bound - random_states = { - state: initial_value[idx] - for idx, state in enumerate(self._initial_states.keys()) - } + random_states = {state: initial_value[idx] for idx, state in enumerate(self._initial_states.keys())} self._initial_states.update(random_states) elif random_dist in ["normal", "gaussian"]: @@ -162,10 +149,7 @@ def initialize(self, state_space, state_positions, nominal_state, **__): size=(len(self._initial_states.keys())), random_state=self.seed_sequence.pool[0], ) - random_states = { - state: initial_value[idx] - for idx, state in enumerate(self._initial_states.keys()) - } + random_states = {state: initial_value[idx] for idx, state in enumerate(self._initial_states.keys())} self._initial_states.update(random_states) else: raise NotImplementedError @@ -173,13 +157,8 @@ def initialize(self, state_space, state_positions, nominal_state, **__): elif self._initial_states is not None: initial_value = np.atleast_1d(list(self._initial_states.values())) # check init_value meets interval boundaries - if (lower_bound <= initial_value).all() and ( - initial_value <= upper_bound - ).all(): - initial_states_ = { - state: initial_value[idx] - for idx, state in enumerate(self._initial_states.keys()) - } + if (lower_bound <= initial_value).all() and (initial_value <= upper_bound).all(): + initial_states_ = {state: initial_value[idx] for idx, state in enumerate(self._initial_states.keys())} self._initial_states.update(initial_states_) else: raise Exception("Initialization Value have to be in nominal boundaries") diff --git a/gym_electric_motor/physical_systems/mechanical_loads/ornstein_uhlenbeck_load.py b/src/gym_electric_motor/physical_systems/mechanical_loads/ornstein_uhlenbeck_load.py similarity index 89% rename from gym_electric_motor/physical_systems/mechanical_loads/ornstein_uhlenbeck_load.py rename to src/gym_electric_motor/physical_systems/mechanical_loads/ornstein_uhlenbeck_load.py index 0ce70b23..18d0dca9 100644 --- a/gym_electric_motor/physical_systems/mechanical_loads/ornstein_uhlenbeck_load.py +++ b/src/gym_electric_motor/physical_systems/mechanical_loads/ornstein_uhlenbeck_load.py @@ -8,9 +8,7 @@ class OrnsteinUhlenbeckLoad(MechanicalLoad): HAS_JACOBIAN = False - def __init__( - self, mu=0, sigma=1e-4, theta=1, tau=1e-4, omega_range=(-200.0, 200.0), **kwargs - ): + def __init__(self, mu=0, sigma=1e-4, theta=1, tau=1e-4, omega_range=(-200.0, 200.0), **kwargs): """ Args: mu(float): Mean value of the underlying gaussian distribution of the OU-Process. @@ -32,9 +30,7 @@ def mechanical_ode(self, t, mechanical_state, torque): omega = mechanical_state max_diff = (self._omega_range[1] - omega) / self.tau min_diff = (self._omega_range[0] - omega) / self.tau - diff = self.theta * (self.mu - omega) * self.tau + self.sigma * np.sqrt( - self.tau - ) * np.random.normal(size=1) + diff = self.theta * (self.mu - omega) * self.tau + self.sigma * np.sqrt(self.tau) * np.random.normal(size=1) np.clip(diff, min_diff, max_diff, out=diff) return diff diff --git a/gym_electric_motor/physical_systems/mechanical_loads/polynomial_static_load.py b/src/gym_electric_motor/physical_systems/mechanical_loads/polynomial_static_load.py similarity index 86% rename from gym_electric_motor/physical_systems/mechanical_loads/polynomial_static_load.py rename to src/gym_electric_motor/physical_systems/mechanical_loads/polynomial_static_load.py index 3d8807eb..4d8eb4ce 100644 --- a/gym_electric_motor/physical_systems/mechanical_loads/polynomial_static_load.py +++ b/src/gym_electric_motor/physical_systems/mechanical_loads/polynomial_static_load.py @@ -72,12 +72,8 @@ def __init__(self, load_parameter=None, limits=None, load_initializer=None): load_initializer(dict): Dictionary to parameterize the initializer. """ load_parameter = load_parameter if load_parameter is not None else dict() - self._load_parameter = update_parameter_dict( - self._load_parameter, load_parameter - ) - super().__init__( - j_load=self._load_parameter["j_load"], load_initializer=load_initializer - ) + self._load_parameter = update_parameter_dict(self._load_parameter, load_parameter) + super().__init__(j_load=self._load_parameter["j_load"], load_initializer=load_initializer) self._limits.update(limits or {}) self._a = self._load_parameter["a"] self._b = self._load_parameter["b"] @@ -91,11 +87,7 @@ def _static_load(self, omega): """Calculation of the load torque for a given speed omega.""" sign = 1 if omega > 0 else -1 if omega < -0 else 0 # Limit the constant load term 'a' for velocities around zero for a more stable integration - a = ( - sign * self._a - if abs(omega) > self._omega_lim - else self._omega_linear_factor * omega - ) + a = sign * self._a if abs(omega) > self._omega_lim else self._omega_linear_factor * omega return sign * self._c * omega**2 + self._b * omega + a def mechanical_ode(self, t, mechanical_state, torque): @@ -110,11 +102,5 @@ def mechanical_jacobian(self, t, mechanical_state, torque): omega = mechanical_state[self.OMEGA_IDX] sign = 1 if omega > 0 else -1 if omega < 0 else 0 # Linear region of the constant load term 'a' ? - a = ( - 0 - if abs(omega) > self._a * self.tau_decay / self._j_total - else self._j_total / self.tau_decay - ) - return np.array( - [[(-self._b - 2 * sign * self._c * omega - a) / self._j_total]] - ), np.array([1 / self._j_total]) + a = 0 if abs(omega) > self._a * self.tau_decay / self._j_total else self._j_total / self.tau_decay + return np.array([[(-self._b - 2 * sign * self._c * omega - a) / self._j_total]]), np.array([1 / self._j_total]) diff --git a/gym_electric_motor/physical_systems/physical_systems.py b/src/gym_electric_motor/physical_systems/physical_systems.py similarity index 85% rename from gym_electric_motor/physical_systems/physical_systems.py rename to src/gym_electric_motor/physical_systems/physical_systems.py index 74ec3659..2458b1e5 100644 --- a/gym_electric_motor/physical_systems/physical_systems.py +++ b/src/gym_electric_motor/physical_systems/physical_systems.py @@ -49,9 +49,7 @@ 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 - ): + def __init__(self, converter, motor, load, supply, ode_solver, tau=1e-4, calc_jacobian=None): """ Args: converter(PowerElectronicConverter): Converter for the physical system @@ -71,27 +69,16 @@ def __init__( 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 - ): + 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" - ) + 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._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) @@ -170,9 +157,7 @@ def _set_indices(self): 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) - ) + self.U_SUP_IDX = list(range(voltages_upper, voltages_upper + self._supply.voltage_len)) def seed(self, seed=None): RandomComponent.seed(self, seed) @@ -210,9 +195,7 @@ def simulate(self, action, *_, **__): 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.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 @@ -233,28 +216,18 @@ def _system_equation(self, t, state, u_in, **__): """ 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] - ) + 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) - ) + 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( + 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( + self._system_eq_placeholder[self._load_deriv_size :] = self._electrical_motor.electrical_ode( motor_state, u_in, state[self._omega_ode_idx] ).ravel() @@ -266,19 +239,13 @@ def _system_jacobian(self, t, state, u_in, **__): motor_jac, el_state_over_omega, torque_over_el_state, - ) = self._electrical_motor.electrical_jacobian( - motor_state, u_in, state[self._omega_ode_idx] - ) + ) = 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 - ) + 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[-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) ) @@ -292,9 +259,7 @@ def reset(self, *_): 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 - ) + 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, @@ -337,12 +302,8 @@ def _build_state_names(self): 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, 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 @@ -399,9 +360,7 @@ def abc_to_dq_space(self, abc_quantities, epsilon_el, normed_epsilon=False): """ if normed_epsilon: epsilon_el *= np.pi - dq_quantity = self._electrical_motor.q_inv( - self._electrical_motor.t_23(abc_quantities), epsilon_el - ) + 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): @@ -418,13 +377,9 @@ def dq_to_abc_space(self, dq_quantities, epsilon_el, normed_epsilon=False): """ if normed_epsilon: epsilon_el *= np.pi - return self._electrical_motor.t_32( - self._electrical_motor.q(dq_quantities, epsilon_el) - ) + 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 - ): + def alphabeta_to_dq_space(self, alphabeta_quantities, epsilon_el, normed_epsilon=False): """ Transformation from alphabeta to dq space @@ -524,9 +479,7 @@ def _set_indices(self): 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.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, *_, **__): @@ -535,9 +488,7 @@ def simulate(self, action, *_, **__): 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 - ) + 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]: @@ -549,9 +500,7 @@ def simulate(self, action, *_, **__): 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_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) @@ -570,16 +519,12 @@ def simulate(self, action, *_, **__): 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) - ) + system_state = np.concatenate((mechanical_state, [torque], i_abc, i_dq, u_in, u_dq, [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 - ) + 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, @@ -666,9 +611,7 @@ def _set_indices(self): 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.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, *_, **__): @@ -676,9 +619,7 @@ def simulate(self, action, *_, **__): 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:] - ) + 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]: @@ -710,16 +651,12 @@ def simulate(self, action, *_, **__): 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) - ) + 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 - ) + 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, @@ -808,8 +745,7 @@ def _set_indices(self): 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._electrical_motor.PSI_RALPHA_IDX : self._electrical_motor.PSI_RBETA_IDX + 1 ] self.OMEGA_IDX = self.mechanical_load.OMEGA_IDX @@ -821,9 +757,7 @@ def _set_indices(self): 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.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): @@ -841,9 +775,7 @@ def simulate(self, action, *_, **__): 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]) - ) + 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]: @@ -855,9 +787,7 @@ def simulate(self, action, *_, **__): 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_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) @@ -878,9 +808,7 @@ def simulate(self, action, *_, **__): 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) - ) + system_state = np.concatenate((mechanical_state, [torque], i_abc, i_dq, u_in, u_dq, [eps], u_sup)) return system_state / self._limits def reset(self, *_): @@ -913,9 +841,7 @@ def reset(self, *_): 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] - ) + system_state = np.concatenate([mechanical_state, [torque], i_abc, i_dq, u_abc, u_dq, [eps], u_sup]) return system_state / self._limits @@ -934,17 +860,13 @@ def __init__(self, ode_solver="scipy.ode", **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.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] + self.rotor_voltage_low_idx + self._converter.subsignal_voltage_space_dims[self.rotor_voltage_space_idx] ) def _set_limits(self): @@ -1004,8 +926,7 @@ def _set_indices(self): 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._electrical_motor.PSI_RALPHA_IDX : self._electrical_motor.PSI_RBETA_IDX + 1 ] self.OMEGA_IDX = self.mechanical_load.OMEGA_IDX @@ -1017,9 +938,7 @@ def _set_indices(self): 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.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] @@ -1061,18 +980,14 @@ def simulate(self, action, *_, **__): 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_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 = 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] @@ -1086,18 +1001,12 @@ def simulate(self, action, *_, **__): 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_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 = 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] @@ -1117,9 +1026,7 @@ def simulate(self, action, *_, **__): 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_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) @@ -1178,9 +1085,7 @@ def reset(self, *_): 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_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) diff --git a/gym_electric_motor/physical_systems/solvers.py b/src/gym_electric_motor/physical_systems/solvers.py similarity index 92% rename from gym_electric_motor/physical_systems/solvers.py rename to src/gym_electric_motor/physical_systems/solvers.py index a9dd85fb..dc4265c9 100644 --- a/gym_electric_motor/physical_systems/solvers.py +++ b/src/gym_electric_motor/physical_systems/solvers.py @@ -94,9 +94,7 @@ def __init__(self, nsteps=1): but take also longer to compute. """ self._nsteps = nsteps - self._integrate = ( - self._integrate_one_step if nsteps == 1 else self._integrate_nsteps - ) + self._integrate = self._integrate_one_step if nsteps == 1 else self._integrate_nsteps def integrate(self, t): # Docstring of superclass @@ -133,9 +131,7 @@ def _integrate_one_step(self, t): Returns: ndarray(float):The new state of the system. """ - self._y = self._y + self._system_equation(self._t, self._y, *self._f_params) * ( - t - self._t - ) + self._y = self._y + self._system_equation(self._t, self._y, *self._f_params) * (t - self._t) self._t = t return self._y @@ -171,9 +167,7 @@ def __init__(self, integrator="dopri5", **kwargs): def set_system_equation(self, system_equation, jac=None): # Docstring of superclass super().set_system_equation(system_equation, jac) - self._ode = ode(system_equation, jac).set_integrator( - self._integrator, **self._solver_args - ) + self._ode = ode(system_equation, jac).set_integrator(self._integrator, **self._solver_args) def set_initial_value(self, initial_value, t=0): # Docstring of superclass diff --git a/gym_electric_motor/physical_systems/voltage_supplies.py b/src/gym_electric_motor/physical_systems/voltage_supplies.py similarity index 86% rename from gym_electric_motor/physical_systems/voltage_supplies.py rename to src/gym_electric_motor/physical_systems/voltage_supplies.py index d431a74e..7442fc52 100644 --- a/gym_electric_motor/physical_systems/voltage_supplies.py +++ b/src/gym_electric_motor/physical_systems/voltage_supplies.py @@ -88,12 +88,8 @@ def __init__(self, u_nominal=600.0, supply_parameter=None): super().__init__(u_nominal) supply_parameter = supply_parameter or {"R": 1, "C": 4e-3} # Supply range is between 0 - capacitor completely unloaded - and u_nominal - capacitor is completely loaded - assert ( - "R" in supply_parameter.keys() - ), "Pass key 'R' for Resistance in your dict" - assert ( - "C" in supply_parameter.keys() - ), "Pass key 'C' for Capacitance in your dict" + assert "R" in supply_parameter.keys(), "Pass key 'R' for Resistance in your dict" + assert "C" in supply_parameter.keys(), "Pass key 'C' for Capacitance in your dict" self.supply_range = (0, u_nominal) self._r = supply_parameter["R"] self._c = supply_parameter["C"] @@ -139,12 +135,8 @@ def __init__(self, u_nominal=230, supply_parameter=None): self._fixed_phi = False if supply_parameter is not None: - assert isinstance( - supply_parameter, dict - ), "supply_parameter should be a dict" - assert ( - "frequency" in supply_parameter.keys() - ), "Pass key 'frequency' for frequency f in Hz in your dict" + assert isinstance(supply_parameter, dict), "supply_parameter should be a dict" + assert "frequency" in supply_parameter.keys(), "Pass key 'frequency' for frequency f in Hz in your dict" if "phase" in supply_parameter.keys(): assert ( 0 <= supply_parameter["phase"] < 2 * np.pi @@ -187,12 +179,8 @@ def __init__(self, u_nominal=400, supply_parameter=None): super().__init__(u_nominal) self._fixed_phi = False if supply_parameter is not None: - assert isinstance( - supply_parameter, dict - ), "supply_parameter should be a dict" - assert ( - "frequency" in supply_parameter.keys() - ), "Pass key 'frequency' for frequency f in Hz in your dict" + assert isinstance(supply_parameter, dict), "supply_parameter should be a dict" + assert "frequency" in supply_parameter.keys(), "Pass key 'frequency' for frequency f in Hz in your dict" if "phase" in supply_parameter.keys(): assert ( 0 <= supply_parameter["phase"] < 2 * np.pi @@ -218,8 +206,6 @@ def reset(self): def get_voltage(self, t, *_, **__): # Docstring of superclass self._u_sup = [ - self._max_amp - * np.sin(2 * np.pi * self._f * t + self._phi + 2 / 3 * np.pi * i) - for i in range(3) + self._max_amp * np.sin(2 * np.pi * self._f * t + self._phi + 2 / 3 * np.pi * i) for i in range(3) ] return self._u_sup diff --git a/gym_electric_motor/random_component.py b/src/gym_electric_motor/random_component.py similarity index 100% rename from gym_electric_motor/random_component.py rename to src/gym_electric_motor/random_component.py diff --git a/gym_electric_motor/reference_generators/__init__.py b/src/gym_electric_motor/reference_generators/__init__.py similarity index 86% rename from gym_electric_motor/reference_generators/__init__.py rename to src/gym_electric_motor/reference_generators/__init__.py index a0643277..956225ce 100644 --- a/gym_electric_motor/reference_generators/__init__.py +++ b/src/gym_electric_motor/reference_generators/__init__.py @@ -12,9 +12,7 @@ from ..utils import register_class from ..core import ReferenceGenerator -register_class( - WienerProcessReferenceGenerator, ReferenceGenerator, "WienerProcessReference" -) +register_class(WienerProcessReferenceGenerator, ReferenceGenerator, "WienerProcessReference") register_class(SwitchedReferenceGenerator, ReferenceGenerator, "SwitchedReference") register_class(StepReferenceGenerator, ReferenceGenerator, "StepReference") register_class(SinusoidalReferenceGenerator, ReferenceGenerator, "SinusReference") @@ -22,6 +20,4 @@ register_class(SawtoothReferenceGenerator, ReferenceGenerator, "SawtoothReference") register_class(ConstReferenceGenerator, ReferenceGenerator, "ConstReference") register_class(MultipleReferenceGenerator, ReferenceGenerator, "MultipleReference") -register_class( - SubepisodedReferenceGenerator, ReferenceGenerator, "SubepisodedReference" -) +register_class(SubepisodedReferenceGenerator, ReferenceGenerator, "SubepisodedReference") diff --git a/gym_electric_motor/reference_generators/const_reference_generator.py b/src/gym_electric_motor/reference_generators/const_reference_generator.py similarity index 81% rename from gym_electric_motor/reference_generators/const_reference_generator.py rename to src/gym_electric_motor/reference_generators/const_reference_generator.py index 8bab6c35..db1e6f2d 100644 --- a/gym_electric_motor/reference_generators/const_reference_generator.py +++ b/src/gym_electric_motor/reference_generators/const_reference_generator.py @@ -19,17 +19,13 @@ def __init__(self, reference_state="omega", reference_value=0.5, **kwargs): super().__init__(**kwargs) self._reference_value = reference_value self._reference_state = reference_state.lower() - self.reference_space = Box( - np.array([reference_value]), np.array([reference_value]), dtype=np.float64 - ) + self.reference_space = Box(np.array([reference_value]), np.array([reference_value]), dtype=np.float64) self._reference_names = self._reference_state def set_modules(self, physical_system): # docstring from superclass super().set_modules(physical_system) - self._referenced_states = set_state_array( - {self._reference_state: 1}, physical_system.state_names - ).astype(bool) + self._referenced_states = set_state_array({self._reference_state: 1}, physical_system.state_names).astype(bool) def get_reference(self, *_, **__): # docstring from superclass diff --git a/gym_electric_motor/reference_generators/laplace_process_reference_generator.py b/src/gym_electric_motor/reference_generators/laplace_process_reference_generator.py similarity index 91% rename from gym_electric_motor/reference_generators/laplace_process_reference_generator.py rename to src/gym_electric_motor/reference_generators/laplace_process_reference_generator.py index 018044cf..2173b2e3 100644 --- a/gym_electric_motor/reference_generators/laplace_process_reference_generator.py +++ b/src/gym_electric_motor/reference_generators/laplace_process_reference_generator.py @@ -23,9 +23,7 @@ def __init__(self, sigma_range=(1e-3, 1e-1), **kwargs): def _reset_reference(self): self._current_sigma = 10 ** self._get_current_value(np.log10(self._sigma_range)) - random_values = self.random_generator.laplace( - 0, self._current_sigma, self._current_episode_length - ) + random_values = self.random_generator.laplace(0, self._current_sigma, self._current_episode_length) self._reference = np.zeros_like(random_values) reference_value = self._reference_value for i in range(self._current_episode_length): diff --git a/gym_electric_motor/reference_generators/multiple_reference_generator.py b/src/gym_electric_motor/reference_generators/multiple_reference_generator.py similarity index 74% rename from gym_electric_motor/reference_generators/multiple_reference_generator.py rename to src/gym_electric_motor/reference_generators/multiple_reference_generator.py index 74e67552..1306d097 100644 --- a/gym_electric_motor/reference_generators/multiple_reference_generator.py +++ b/src/gym_electric_motor/reference_generators/multiple_reference_generator.py @@ -48,27 +48,11 @@ def set_modules(self, physical_system): # Ensure that all referenced states are different assert all( - sum( - [ - sub_generator.referenced_states.astype(int) - for sub_generator in self._sub_generators - ] - ) - < 2 + sum([sub_generator.referenced_states.astype(int) for sub_generator in self._sub_generators]) < 2 ), "Some of the passed reference generators share the same reference variable" - ref_space_low = np.concatenate( - [ - sub_generator.reference_space.low - for sub_generator in self._sub_generators - ] - ) - ref_space_high = np.concatenate( - [ - sub_generator.reference_space.high - for sub_generator in self._sub_generators - ] - ) + ref_space_low = np.concatenate([sub_generator.reference_space.low for sub_generator in self._sub_generators]) + ref_space_high = np.concatenate([sub_generator.reference_space.high for sub_generator in self._sub_generators]) self.reference_space = Box(ref_space_low, ref_space_high, dtype=np.float64) self._referenced_states = np.sum( [sub_generator.referenced_states for sub_generator in self._sub_generators], @@ -81,29 +65,19 @@ def reset(self, initial_state=None, initial_reference=None): refs = np.zeros_like(self._physical_system.state_names, dtype=float) ref_obs = np.array([]) for sub_generator in self._sub_generators: - ref, ref_observation, _ = sub_generator.reset( - initial_state, initial_reference - ) + ref, ref_observation, _ = sub_generator.reset(initial_state, initial_reference) refs += ref ref_obs = np.concatenate((ref_obs, ref_observation)) return refs, ref_obs, None def get_reference(self, state, **kwargs): # docstring from superclass - return sum( - [ - sub_generator.get_reference(state, **kwargs) - for sub_generator in self._sub_generators - ] - ) + return sum([sub_generator.get_reference(state, **kwargs) for sub_generator in self._sub_generators]) def get_reference_observation(self, state, *_, **kwargs): # docstring from superclass return np.concatenate( - [ - sub_generator.get_reference_observation(state, **kwargs) - for sub_generator in self._sub_generators - ] + [sub_generator.get_reference_observation(state, **kwargs) for sub_generator in self._sub_generators] ) def seed(self, seed=None): diff --git a/gym_electric_motor/reference_generators/sawtooth_reference_generator.py b/src/gym_electric_motor/reference_generators/sawtooth_reference_generator.py similarity index 81% rename from gym_electric_motor/reference_generators/sawtooth_reference_generator.py rename to src/gym_electric_motor/reference_generators/sawtooth_reference_generator.py index c9a9af57..6c5aa8b7 100644 --- a/gym_electric_motor/reference_generators/sawtooth_reference_generator.py +++ b/src/gym_electric_motor/reference_generators/sawtooth_reference_generator.py @@ -13,9 +13,7 @@ class SawtoothReferenceGenerator(SubepisodedReferenceGenerator): _frequency = 0 _offset = 0 - def __init__( - self, amplitude_range=None, frequency_range=(1, 10), offset_range=None, **kwargs - ): + def __init__(self, amplitude_range=None, frequency_range=(1, 10), offset_range=None, **kwargs): """ Args: amplitude_range(tuple(float,float)): Lower and upper limit for the amplitude. @@ -37,9 +35,7 @@ def set_modules(self, physical_system): (self._limit_margin[1] - self._limit_margin[0]) / 2, ) # limit_margin will range from(-1, 1) but amplitude cannot exceed 1 - self._offset_range = np.clip( - self._offset_range, self._limit_margin[0], self._limit_margin[1] - ) + self._offset_range = np.clip(self._offset_range, self._limit_margin[0], self._limit_margin[1]) def _reset_reference(self): # get absolute values of amplitude, frequency and offset @@ -59,10 +55,5 @@ def _reset_reference(self): ) phase = self.random_generator.uniform() * 2 * np.pi # note: in the scipy implementation of sawtooth() 1 time-period corresponds to a phase of 2pi - self._reference = ( - self._amplitude * sg.sawtooth(2 * np.pi * self._frequency * t + phase) - + self._offset - ) - self._reference = np.clip( - self._reference, self._limit_margin[0], self._limit_margin[1] - ) + self._reference = self._amplitude * sg.sawtooth(2 * np.pi * self._frequency * t + phase) + self._offset + self._reference = np.clip(self._reference, self._limit_margin[0], self._limit_margin[1]) diff --git a/gym_electric_motor/reference_generators/sinusoidal_reference_generator.py b/src/gym_electric_motor/reference_generators/sinusoidal_reference_generator.py similarity index 82% rename from gym_electric_motor/reference_generators/sinusoidal_reference_generator.py rename to src/gym_electric_motor/reference_generators/sinusoidal_reference_generator.py index 1458bb4f..10afba42 100644 --- a/gym_electric_motor/reference_generators/sinusoidal_reference_generator.py +++ b/src/gym_electric_motor/reference_generators/sinusoidal_reference_generator.py @@ -40,9 +40,7 @@ def set_modules(self, physical_system): 0, (self._limit_margin[1] - self._limit_margin[0]) / 2, ) - self._offset_range = np.clip( - self._offset_range, self._limit_margin[0], self._limit_margin[1] - ) + self._offset_range = np.clip(self._offset_range, self._limit_margin[0], self._limit_margin[1]) def _reset_reference(self): self._amplitude = self._get_current_value(self._amplitude_range) @@ -59,10 +57,5 @@ def _reset_reference(self): self._current_episode_length, ) phase = self.random_generator.uniform() * 2 * np.pi - self._reference = ( - self._amplitude * np.sin(2 * np.pi * self._frequency * t + phase) - + self._offset - ) - self._reference = np.clip( - self._reference, self._limit_margin[0], self._limit_margin[1] - ) + self._reference = self._amplitude * np.sin(2 * np.pi * self._frequency * t + phase) + self._offset + self._reference = np.clip(self._reference, self._limit_margin[0], self._limit_margin[1]) diff --git a/gym_electric_motor/reference_generators/step_reference_generator.py b/src/gym_electric_motor/reference_generators/step_reference_generator.py similarity index 84% rename from gym_electric_motor/reference_generators/step_reference_generator.py rename to src/gym_electric_motor/reference_generators/step_reference_generator.py index d4fd4701..4ebfd3d4 100644 --- a/gym_electric_motor/reference_generators/step_reference_generator.py +++ b/src/gym_electric_motor/reference_generators/step_reference_generator.py @@ -13,9 +13,7 @@ class StepReferenceGenerator(SubepisodedReferenceGenerator): _frequency = 0 _offset = 0 - def __init__( - self, amplitude_range=None, frequency_range=(1, 10), offset_range=None, **kwargs - ): + def __init__(self, amplitude_range=None, frequency_range=(1, 10), offset_range=None, **kwargs): """ Args: amplitude_range(tuple(float,float)): Lower and upper limit for the amplitude. @@ -35,9 +33,7 @@ def set_modules(self, physical_system): 0, (self._limit_margin[1] - self._limit_margin[0]) / 2, ) - self._offset_range = np.clip( - self._offset_range, self._limit_margin[0], self._limit_margin[1] - ) + self._offset_range = np.clip(self._offset_range, self._limit_margin[0], self._limit_margin[1]) def _reset_reference(self): self._amplitude = self._get_current_value(self._amplitude_range) @@ -61,6 +57,4 @@ def _reset_reference(self): steps_per_period = 1 / self._frequency / self._physical_system.tau x = np.roll(x, int(steps_per_period * phase)) self._reference = self._amplitude * x + self._offset - self._reference = np.clip( - self._reference, self._limit_margin[0], self._limit_margin[1] - ) + self._reference = np.clip(self._reference, self._limit_margin[0], self._limit_margin[1]) diff --git a/gym_electric_motor/reference_generators/subepisoded_reference_generator.py b/src/gym_electric_motor/reference_generators/subepisoded_reference_generator.py similarity index 86% rename from gym_electric_motor/reference_generators/subepisoded_reference_generator.py rename to src/gym_electric_motor/reference_generators/subepisoded_reference_generator.py index 74143045..56e9e822 100644 --- a/gym_electric_motor/reference_generators/subepisoded_reference_generator.py +++ b/src/gym_electric_motor/reference_generators/subepisoded_reference_generator.py @@ -44,18 +44,12 @@ def __init__( def set_modules(self, physical_system): super().set_modules(physical_system) - self._referenced_states = set_state_array( - {self._reference_state: 1}, physical_system.state_names - ).astype(bool) + self._referenced_states = set_state_array({self._reference_state: 1}, physical_system.state_names).astype(bool) rs = self._referenced_states ps = physical_system if self._limit_margin is None: - upper_margin = (ps.nominal_state[rs] / ps.limits[rs])[ - 0 - ] * ps.state_space.high[rs] - lower_margin = (ps.nominal_state[rs] / ps.limits[rs])[ - 0 - ] * ps.state_space.low[rs] + upper_margin = (ps.nominal_state[rs] / ps.limits[rs])[0] * ps.state_space.high[rs] + lower_margin = (ps.nominal_state[rs] / ps.limits[rs])[0] * ps.state_space.low[rs] self._limit_margin = lower_margin[0], upper_margin[0] elif type(self._limit_margin) in [float, int]: upper_margin = self._limit_margin * ps.state_space.high[rs] @@ -67,9 +61,7 @@ def set_modules(self, physical_system): self._limit_margin = lower_margin[0], upper_margin[0] else: raise Exception("Unknown type for the limit margin.") - self.reference_space = Box( - lower_margin[0], upper_margin[0], shape=(1,), dtype=np.float64 - ) + self.reference_space = Box(lower_margin[0], upper_margin[0], shape=(1,), dtype=np.float64) def reset(self, initial_state=None, initial_reference=None): """ @@ -101,9 +93,7 @@ def get_reference(self, *_, **__): def get_reference_observation(self, *_, **__): if self._k >= self._current_episode_length: self._k = 0 - self._current_episode_length = int( - self._get_current_value(self._episode_len_range) - ) + self._current_episode_length = int(self._get_current_value(self._episode_len_range)) self._reset_reference() self._reference_value = self._reference[self._k] self._k += 1 @@ -126,6 +116,4 @@ def _get_current_value(self, value_range): if type(value_range) in [int, float]: return value_range elif type(value_range) in [list, tuple, np.ndarray]: - return ( - value_range[1] - value_range[0] - ) * self._random_generator.uniform() + value_range[0] + return (value_range[1] - value_range[0]) * self._random_generator.uniform() + value_range[0] diff --git a/gym_electric_motor/reference_generators/switched_reference_generator.py b/src/gym_electric_motor/reference_generators/switched_reference_generator.py similarity index 92% rename from gym_electric_motor/reference_generators/switched_reference_generator.py rename to src/gym_electric_motor/reference_generators/switched_reference_generator.py index 2772ed6c..b2c1704b 100644 --- a/gym_electric_motor/reference_generators/switched_reference_generator.py +++ b/src/gym_electric_motor/reference_generators/switched_reference_generator.py @@ -46,17 +46,11 @@ def set_modules(self, physical_system): for sub_generator in self._sub_generators: sub_generator.set_modules(physical_system) ref_space_low = np.min( - [ - sub_generator.reference_space.low - for sub_generator in self._sub_generators - ], + [sub_generator.reference_space.low for sub_generator in self._sub_generators], axis=0, ) ref_space_high = np.max( - [ - sub_generator.reference_space.high - for sub_generator in self._sub_generators - ], + [sub_generator.reference_space.high for sub_generator in self._sub_generators], axis=0, ) self.reference_space = Box(ref_space_low, ref_space_high, dtype=float) @@ -92,9 +86,7 @@ def _reset_reference(self): self._super_episode_length[0], self._super_episode_length[1] ) self._k = 0 - self._current_ref_generator = self.random_generator.choice( - self._sub_generators, p=self._probabilities - ) + self._current_ref_generator = self.random_generator.choice(self._sub_generators, p=self._probabilities) def seed(self, seed=None): super().seed(seed) diff --git a/gym_electric_motor/reference_generators/triangle_reference_generator.py b/src/gym_electric_motor/reference_generators/triangle_reference_generator.py similarity index 83% rename from gym_electric_motor/reference_generators/triangle_reference_generator.py rename to src/gym_electric_motor/reference_generators/triangle_reference_generator.py index 0aa0fa42..0daa45a5 100644 --- a/gym_electric_motor/reference_generators/triangle_reference_generator.py +++ b/src/gym_electric_motor/reference_generators/triangle_reference_generator.py @@ -41,9 +41,7 @@ def set_modules(self, physical_system): 0, (self._limit_margin[1] - self._limit_margin[0]) / 2, ) - self._offset_range = np.clip( - self._offset_range, self._limit_margin[0], self._limit_margin[1] - ) + self._offset_range = np.clip(self._offset_range, self._limit_margin[0], self._limit_margin[1]) def _reset_reference(self): # get absolute values of amplitude, frequency and offset @@ -65,13 +63,11 @@ def _reset_reference(self): self._random_generator.uniform() * 2 * np.pi ) # note: in the scipy implementation of sawtooth() 1 time-period # corresponds to a phase of 2pi - ref_width = self._random_generator.uniform() # a random value between 0,1 that creates asymmetry in the triangular reference + ref_width = ( + self._random_generator.uniform() + ) # a random value between 0,1 that creates asymmetry in the triangular reference # wave ref_width=1 creates a sawtooth waveform self._reference = ( - self._amplitude - * sg.sawtooth(2 * np.pi * self._frequency * t + phase, ref_width) - + self._offset - ) - self._reference = np.clip( - self._reference, self._limit_margin[0], self._limit_margin[1] + self._amplitude * sg.sawtooth(2 * np.pi * self._frequency * t + phase, ref_width) + self._offset ) + self._reference = np.clip(self._reference, self._limit_margin[0], self._limit_margin[1]) diff --git a/gym_electric_motor/reference_generators/wiener_process_reference_generator.py b/src/gym_electric_motor/reference_generators/wiener_process_reference_generator.py similarity index 92% rename from gym_electric_motor/reference_generators/wiener_process_reference_generator.py rename to src/gym_electric_motor/reference_generators/wiener_process_reference_generator.py index 749afeba..1e5aa1e8 100644 --- a/gym_electric_motor/reference_generators/wiener_process_reference_generator.py +++ b/src/gym_electric_motor/reference_generators/wiener_process_reference_generator.py @@ -29,9 +29,7 @@ def set_modules(self, physical_system): def _reset_reference(self): self._current_sigma = 10 ** self._get_current_value(np.log10(self._sigma_range)) - random_values = self._random_generator.normal( - 0, self._current_sigma, self._current_episode_length - ) + random_values = self._random_generator.normal(0, self._current_sigma, self._current_episode_length) self._reference = np.zeros_like(random_values) reference_value = self._reference_value for i in range(self._current_episode_length): diff --git a/gym_electric_motor/reference_generators/zero_reference_generator.py b/src/gym_electric_motor/reference_generators/zero_reference_generator.py similarity index 83% rename from gym_electric_motor/reference_generators/zero_reference_generator.py rename to src/gym_electric_motor/reference_generators/zero_reference_generator.py index 150f311f..fe10190b 100644 --- a/gym_electric_motor/reference_generators/zero_reference_generator.py +++ b/src/gym_electric_motor/reference_generators/zero_reference_generator.py @@ -14,9 +14,7 @@ def __init__(self): def set_modules(self, physical_system): super().set_modules(physical_system) - self._referenced_states = np.zeros_like( - self._physical_system.state_names, dtype=bool - ) + self._referenced_states = np.zeros_like(self._physical_system.state_names, dtype=bool) def get_reference(self, state=None, *_, **__): return np.zeros_like(self._physical_system.state_names, dtype=float) diff --git a/gym_electric_motor/reward_functions/__init__.py b/src/gym_electric_motor/reward_functions/__init__.py similarity index 100% rename from gym_electric_motor/reward_functions/__init__.py rename to src/gym_electric_motor/reward_functions/__init__.py diff --git a/gym_electric_motor/reward_functions/weighted_sum_of_errors.py b/src/gym_electric_motor/reward_functions/weighted_sum_of_errors.py similarity index 91% rename from gym_electric_motor/reward_functions/weighted_sum_of_errors.py rename to src/gym_electric_motor/reward_functions/weighted_sum_of_errors.py index 810a4395..8d2c3350 100644 --- a/gym_electric_motor/reward_functions/weighted_sum_of_errors.py +++ b/src/gym_electric_motor/reward_functions/weighted_sum_of_errors.py @@ -122,15 +122,7 @@ def set_modules(self, physical_system, reference_generator, constraint_monitor): self._violation_reward = min(self.reward_range[0] / (1.0 - self._gamma), 0) def reward(self, state, reference, k=None, action=None, violation_degree=0.0): - return (1.0 - violation_degree) * self._wse_reward( - state, reference - ) + violation_degree * self._violation_reward + return (1.0 - violation_degree) * self._wse_reward(state, reference) + violation_degree * self._violation_reward def _wse_reward(self, state, reference): - return ( - -np.sum( - self._reward_weights - * (abs(state - reference) / self._state_length) ** self._n - ) - + self._bias - ) + return -np.sum(self._reward_weights * (abs(state - reference) / self._state_length) ** self._n) + self._bias diff --git a/gym_electric_motor/utils.py b/src/gym_electric_motor/utils.py similarity index 94% rename from gym_electric_motor/utils.py rename to src/gym_electric_motor/utils.py index c9dfb8ba..0d1f12f1 100644 --- a/gym_electric_motor/utils.py +++ b/src/gym_electric_motor/utils.py @@ -15,9 +15,7 @@ def state_dict_to_state_array(state_dict, state_array, state_names): state_names(list/ndarray(str)): List of the state names. """ state_dict = dict((key.lower(), v) for key, v in state_dict.items()) - assert all( - key in state_names for key in state_dict.keys() - ), f"A state name in {state_dict.keys()} is invalid." + assert all(key in state_names for key in state_dict.keys()), f"A state name in {state_dict.keys()} is invalid." for ind, key in enumerate(state_names): try: state_array[ind] = state_dict[key] @@ -116,9 +114,7 @@ def make_module(superclass, keystring, **kwargs): try: return _registry[superclass][keystring](**kwargs) except KeyError: - raise Exception( - f"Key {keystring} or baseclass {superclass.__name__} not found in the registry." - ) + raise Exception(f"Key {keystring} or baseclass {superclass.__name__} not found in the registry.") def register_superclass(superclass): @@ -156,9 +152,7 @@ def update_parameter_dict(source_dict, update_dict, copy=True): source_keys = source_dict.keys() for key in update_dict.keys(): if key not in source_keys: - raise KeyError( - f'Cannot update_dict the source_dict. The key "{key}" is not available.' - ) + raise KeyError(f'Cannot update_dict the source_dict. The key "{key}" is not available.') new_dict = source_dict.copy() if copy else source_dict new_dict.update(update_dict) return new_dict diff --git a/gym_electric_motor/visualization/__init__.py b/src/gym_electric_motor/visualization/__init__.py similarity index 100% rename from gym_electric_motor/visualization/__init__.py rename to src/gym_electric_motor/visualization/__init__.py diff --git a/gym_electric_motor/visualization/console_printer.py b/src/gym_electric_motor/visualization/console_printer.py similarity index 100% rename from gym_electric_motor/visualization/console_printer.py rename to src/gym_electric_motor/visualization/console_printer.py diff --git a/gym_electric_motor/visualization/motor_dashboard.py b/src/gym_electric_motor/visualization/motor_dashboard.py similarity index 99% rename from gym_electric_motor/visualization/motor_dashboard.py rename to src/gym_electric_motor/visualization/motor_dashboard.py index c2519422..e7d14087 100644 --- a/gym_electric_motor/visualization/motor_dashboard.py +++ b/src/gym_electric_motor/visualization/motor_dashboard.py @@ -155,10 +155,9 @@ def on_step_end(self, k, state, reference, reward, terminated): def render(self): """Updates the plots every *update cycle* calls of this method.""" - if ( - not (self._time_plot_figure or self._episodic_plot_figure or self._step_plot_figure) - and len(self._plots) > 0 - ): + if not (self._time_plot_figure or self._episodic_plot_figure or self._step_plot_figure) and len( + self._plots + ) > 0: self.initialize() if self._update_render: self._update() diff --git a/gym_electric_motor/visualization/motor_dashboard_plots/__init__.py b/src/gym_electric_motor/visualization/motor_dashboard_plots/__init__.py similarity index 100% rename from gym_electric_motor/visualization/motor_dashboard_plots/__init__.py rename to src/gym_electric_motor/visualization/motor_dashboard_plots/__init__.py diff --git a/gym_electric_motor/visualization/motor_dashboard_plots/action_plot.py b/src/gym_electric_motor/visualization/motor_dashboard_plots/action_plot.py similarity index 100% rename from gym_electric_motor/visualization/motor_dashboard_plots/action_plot.py rename to src/gym_electric_motor/visualization/motor_dashboard_plots/action_plot.py diff --git a/gym_electric_motor/visualization/motor_dashboard_plots/base_plots.py b/src/gym_electric_motor/visualization/motor_dashboard_plots/base_plots.py similarity index 98% rename from gym_electric_motor/visualization/motor_dashboard_plots/base_plots.py rename to src/gym_electric_motor/visualization/motor_dashboard_plots/base_plots.py index 8967ec34..03df2ffc 100644 --- a/gym_electric_motor/visualization/motor_dashboard_plots/base_plots.py +++ b/src/gym_electric_motor/visualization/motor_dashboard_plots/base_plots.py @@ -149,9 +149,7 @@ def reset_data(self): self._t = 0 self._reset_memory = [] self._violation_memory = [] - self._x_data = np.linspace( - 0, self._x_width * self._tau, self._x_width, endpoint=False - ) + self._x_data = np.linspace(0, self._x_width * self._tau, self._x_width, endpoint=False) self._x_lim = (0, self._x_data[-1]) def on_reset_begin(self): diff --git a/gym_electric_motor/visualization/motor_dashboard_plots/cumulative_constraint_violation_plot.py b/src/gym_electric_motor/visualization/motor_dashboard_plots/cumulative_constraint_violation_plot.py similarity index 100% rename from gym_electric_motor/visualization/motor_dashboard_plots/cumulative_constraint_violation_plot.py rename to src/gym_electric_motor/visualization/motor_dashboard_plots/cumulative_constraint_violation_plot.py diff --git a/gym_electric_motor/visualization/motor_dashboard_plots/episode_length_plot.py b/src/gym_electric_motor/visualization/motor_dashboard_plots/episode_length_plot.py similarity index 100% rename from gym_electric_motor/visualization/motor_dashboard_plots/episode_length_plot.py rename to src/gym_electric_motor/visualization/motor_dashboard_plots/episode_length_plot.py diff --git a/gym_electric_motor/visualization/motor_dashboard_plots/mean_episode_reward_plot.py b/src/gym_electric_motor/visualization/motor_dashboard_plots/mean_episode_reward_plot.py similarity index 83% rename from gym_electric_motor/visualization/motor_dashboard_plots/mean_episode_reward_plot.py rename to src/gym_electric_motor/visualization/motor_dashboard_plots/mean_episode_reward_plot.py index 199d2cf7..ac6c4717 100644 --- a/gym_electric_motor/visualization/motor_dashboard_plots/mean_episode_reward_plot.py +++ b/src/gym_electric_motor/visualization/motor_dashboard_plots/mean_episode_reward_plot.py @@ -20,9 +20,7 @@ def initialize(self, axis): super().initialize(axis) self._reward_data = [] self._y_data.append(self._reward_data) - self._lines.append( - self._axis.plot([], self._reward_data, color=self._colors[0])[0] - ) + self._lines.append(self._axis.plot([], self._reward_data, color=self._colors[0])[0]) def on_step_end(self, k, state, reference, reward, terminated): super().on_step_end(k, state, reference, reward, terminated) @@ -46,10 +44,6 @@ def _set_y_data(self): self._reset = True def _scale_y_axis(self): - if len(self._reward_data) > 1 and self._axis.get_ylim() != tuple( - self._reward_range - ): + if len(self._reward_data) > 1 and self._axis.get_ylim() != tuple(self._reward_range): spacing = 0.1 * (self._reward_range[1] - self._reward_range[0]) - self._axis.set_ylim( - self._reward_range[0] - spacing, self._reward_range[1] + spacing - ) + self._axis.set_ylim(self._reward_range[0] - spacing, self._reward_range[1] + spacing) diff --git a/gym_electric_motor/visualization/motor_dashboard_plots/reward_plot.py b/src/gym_electric_motor/visualization/motor_dashboard_plots/reward_plot.py similarity index 91% rename from gym_electric_motor/visualization/motor_dashboard_plots/reward_plot.py rename to src/gym_electric_motor/visualization/motor_dashboard_plots/reward_plot.py index bdd8f857..9bf36019 100644 --- a/gym_electric_motor/visualization/motor_dashboard_plots/reward_plot.py +++ b/src/gym_electric_motor/visualization/motor_dashboard_plots/reward_plot.py @@ -16,9 +16,7 @@ def __init__(self): def initialize(self, axis): super().initialize(axis) - (self._reward_line,) = self._axis.plot( - self._x_data, self._reward_data, **self._reward_line_cfg - ) + (self._reward_line,) = self._axis.plot(self._x_data, self._reward_data, **self._reward_line_cfg) self._lines.append(self._reward_line) def set_env(self, env): diff --git a/gym_electric_motor/visualization/motor_dashboard_plots/state_plot.py b/src/gym_electric_motor/visualization/motor_dashboard_plots/state_plot.py similarity index 85% rename from gym_electric_motor/visualization/motor_dashboard_plots/state_plot.py rename to src/gym_electric_motor/visualization/motor_dashboard_plots/state_plot.py index 29debfae..ca483315 100644 --- a/gym_electric_motor/visualization/motor_dashboard_plots/state_plot.py +++ b/src/gym_electric_motor/visualization/motor_dashboard_plots/state_plot.py @@ -88,16 +88,8 @@ def set_env(self, env): self._normalized = self._limits != self._state_space[1] self.reset_data() - min_limit = ( - self._limits * self._state_space[0] - if self._normalized - else self._state_space[0] - ) - max_limit = ( - self._limits * self._state_space[1] - if self._normalized - else self._state_space[1] - ) + min_limit = self._limits * self._state_space[0] if self._normalized else self._state_space[0] + max_limit = self._limits * self._state_space[1] if self._normalized else self._state_space[1] spacing = 0.1 * (max_limit - min_limit) # Set the y-axis limits to fixed initital values @@ -119,27 +111,15 @@ def initialize(self, axis): super().initialize(axis) # Line to plot the state data - (self._state_line,) = self._axis.plot( - self._x_data, self._state_data, **self._state_line_config, zorder=2 - ) + (self._state_line,) = self._axis.plot(self._x_data, self._state_data, **self._state_line_config, zorder=2) self._lines = [self._state_line] # If the state is referenced plot also the reference line if self._referenced: - (self._reference_line,) = self._axis.plot( - self._x_data, self._ref_data, **self._ref_line_config, zorder=1 - ) + (self._reference_line,) = self._axis.plot(self._x_data, self._ref_data, **self._ref_line_config, zorder=1) self._lines.append(self._reference_line) - min_limit = ( - self._limits * self._state_space[0] - if self._normalized - else self._state_space[0] - ) - max_limit = ( - self._limits * self._state_space[1] - if self._normalized - else self._state_space[1] - ) + min_limit = self._limits * self._state_space[0] if self._normalized else self._state_space[0] + max_limit = self._limits * self._state_space[1] if self._normalized else self._state_space[1] if self._state_space[0] < 0: self._axis.axhline(min_limit, **self._limit_line_config) lim = self._axis.axhline(max_limit, **self._limit_line_config) diff --git a/gym_electric_motor/visualization/render_modes.py b/src/gym_electric_motor/visualization/render_modes.py similarity index 100% rename from gym_electric_motor/visualization/render_modes.py rename to src/gym_electric_motor/visualization/render_modes.py diff --git a/tests/integration_tests/test_environment_execution.py b/tests/integration_tests/test_environment_execution.py index 0f446c88..ce44bbcf 100644 --- a/tests/integration_tests/test_environment_execution.py +++ b/tests/integration_tests/test_environment_execution.py @@ -18,7 +18,7 @@ versions = ["v0"] -@pytest.mark.parametrize("no_of_steps", [100]) +@pytest.mark.parametrize("no_of_steps", [10]) @pytest.mark.parametrize("version", versions) @pytest.mark.parametrize("dc_motor", motors) @pytest.mark.parametrize("control_task", control_tasks) @@ -34,12 +34,8 @@ def test_execution(dc_motor, control_task, action_type, version, no_of_steps): action = env.action_space.sample() assert action in env.action_space observation, reward, terminated, truncated, info = env.step(action) - assert not np.any( - np.isnan(observation[0]) - ), "An invalid nan-value is in the state." - assert not np.any( - np.isnan(observation[1]) - ), "An invalid nan-value is in the reference." + assert not np.any(np.isnan(observation[0])), "An invalid nan-value is in the state." + assert not np.any(np.isnan(observation[1])), "An invalid nan-value is in the reference." assert info == {} assert type(reward) in [ float, @@ -49,9 +45,5 @@ def test_execution(dc_motor, control_task, action_type, version, no_of_steps): assert not np.isnan(reward), "Invalid nan-value as reward." # Only the shape is monitored here. The states and references may lay slightly outside of the specified space. # This happens if limits are violated or if some states are not observed to lay within their limits. - assert ( - observation[0].shape == env.observation_space[0].shape - ), "The shape of the state is incorrect." - assert ( - observation[1].shape == env.observation_space[1].shape - ), "The shape of the reference is incorrect." + assert observation[0].shape == env.observation_space[0].shape, "The shape of the state is incorrect." + assert observation[1].shape == env.observation_space[1].shape, "The shape of the reference is incorrect." diff --git a/tests/integration_tests/test_environment_seeding.py b/tests/integration_tests/test_environment_seeding.py index 23da3554..4bc59e46 100644 --- a/tests/integration_tests/test_environment_seeding.py +++ b/tests/integration_tests/test_environment_seeding.py @@ -16,18 +16,16 @@ "SCIM", ] versions = ["v0"] -seeds = [123, 456, 789] +seeds = [123] -@pytest.mark.parametrize("no_of_steps", [100]) +@pytest.mark.parametrize("no_of_steps", [10]) @pytest.mark.parametrize("version", versions) @pytest.mark.parametrize("dc_motor", motors) @pytest.mark.parametrize("control_task", control_tasks) @pytest.mark.parametrize("action_type", action_types) @pytest.mark.parametrize("seed", seeds) -def test_seeding_same_env( - dc_motor, control_task, action_type, version, no_of_steps, seed -): +def test_seeding_same_env(dc_motor, control_task, action_type, version, no_of_steps, seed): """This test assures that an environment that is seeded two times with the same seed generates the same episodes.""" env_id = f"{action_type}-{control_task}-{dc_motor}-{version}" env = gem.make(env_id) @@ -72,15 +70,13 @@ def test_seeding_same_env( assert np.all(np.array(terminated1) == np.array(terminated2)) -@pytest.mark.parametrize("no_of_steps", [100]) +@pytest.mark.parametrize("no_of_steps", [10]) @pytest.mark.parametrize("version", versions) @pytest.mark.parametrize("dc_motor", motors) @pytest.mark.parametrize("control_task", control_tasks) @pytest.mark.parametrize("action_type", action_types) @pytest.mark.parametrize("seed", seeds) -def test_seeding_new_env( - dc_motor, control_task, action_type, version, no_of_steps, seed -): +def test_seeding_new_env(dc_motor, control_task, action_type, version, no_of_steps, seed): """This test assures that two equal environments that are seeded with the same seed generate the same episodes.""" env_id = f"{action_type}-{control_task}-{dc_motor}-{version}" env = gem.make(env_id) diff --git a/tests/test_physical_systems/test_converters.py b/tests/test_physical_systems/test_converters.py index 28d67b66..cc049ab8 100644 --- a/tests/test_physical_systems/test_converters.py +++ b/tests/test_physical_systems/test_converters.py @@ -51,9 +51,7 @@ g_actions_2qc = [0, 0, 0, 0, 1, 1, 1, 0, 2, 2, 2, 1, 2, 2, 1, 2] g_times_2qc = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]) g_test_voltages_2qc = np.array([0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0]) -g_test_voltages_2qc_interlocking = np.array( - [0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0] -) +g_test_voltages_2qc_interlocking = np.array([0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0]) # disc 4QC g_times_4qc = np.array( @@ -282,23 +280,15 @@ def discrete_converter_functions_testing( :return: """ action_space = converter.action_space - assert ( - action_space.n == action_space_n - ) # test if the action space has the right size + assert action_space.n == action_space_n # test if the action space has the right size step_counter = 0 - for time, action, i_in in zip( - times, actions, i_ins - ): # apply each action at different times - time_steps = converter.set_action( - action, time - ) # test set action, returns switching times + for time, action, i_in in zip(times, actions, i_ins): # apply each action at different times + time_steps = converter.set_action(action, time) # test set action, returns switching times for index, time_step in enumerate(time_steps): converter_voltage = converter.convert([i_in], time_step) converter.i_sup([i_in]) for u in converter_voltage: - assert ( - converter.voltages.low[0] <= u <= converter.voltages.high[0] - ), "Voltage limits violated" + assert converter.voltages.low[0] <= u <= converter.voltages.high[0], "Voltage limits violated" # test for different cases of (non) ideal behaviour if interlocking_time > 0: test_voltage = test_voltage_interlocking_time[step_counter] @@ -306,9 +296,7 @@ def discrete_converter_functions_testing( else: test_voltage = test_voltage_ideal[step_counter] # g_u_out[step_counter] - assert test_voltage == converter_voltage, "Wrong voltage " + str( - step_counter - ) + assert test_voltage == converter_voltage, "Wrong voltage " + str(step_counter) step_counter += 1 @@ -392,9 +380,7 @@ def test_discrete_single_power_electronic_converter( ) @pytest.mark.parametrize("tau", g_taus) @pytest.mark.parametrize("interlocking_time", g_interlocking_times) -def test_discrete_single_initializations( - convert, convert_class, tau, interlocking_time -): +def test_discrete_single_initializations(convert, convert_class, tau, interlocking_time): """ test of both ways of initialization lead to the same result :param convert: string name of the converter @@ -420,11 +406,9 @@ def test_discrete_single_initializations( assert converter_1.reset() == converter_2.reset() assert converter_1.action_space == converter_2.action_space assert converter_1._tau == converter_2._tau == tau, "Error (tau): " + parameter - assert ( - converter_1._interlocking_time - == converter_2._interlocking_time - == interlocking_time - ), "Error (interlocking): " + parameter + assert converter_1._interlocking_time == converter_2._interlocking_time == interlocking_time, ( + "Error (interlocking): " + parameter + ) @pytest.mark.parametrize("tau", g_taus) @@ -448,11 +432,7 @@ def test_discrete_multi_converter_initializations(tau, interlocking_time): subconverters=[conv_1, conv_2], ) # test if both converter have the same parameter - assert ( - converter._sub_converters[0]._tau - == converter._sub_converters[1]._tau - == tau - ) + assert converter._sub_converters[0]._tau == converter._sub_converters[1]._tau == tau assert ( converter._sub_converters[0]._interlocking_time == converter._sub_converters[1]._interlocking_time @@ -495,17 +475,9 @@ def test_discrete_multi_power_electronic_converter(tau, interlocking_time): action_space_n = converter.action_space.nvec assert np.all( converter.reset() - == np.concatenate( - [ - [-0.5, -0.5, -0.5] if ("Finite-B6C" == conv) else [0] - for conv in [conv_0, conv_1] - ] - ) + == np.concatenate([[-0.5, -0.5, -0.5] if ("Finite-B6C" == conv) else [0] for conv in [conv_0, conv_1]]) ) # test if reset returns 0.0 - actions = [ - [np.random.randint(0, upper_bound) for upper_bound in action_space_n] - for _ in range(100) - ] + actions = [[np.random.randint(0, upper_bound) for upper_bound in action_space_n] for _ in range(100)] times = np.arange(100) * tau for action, t in zip(actions, times): time_steps = converter.set_action(action, t) @@ -514,16 +486,8 @@ def test_discrete_multi_power_electronic_converter(tau, interlocking_time): for time_step in time_steps_1 + time_steps_2: assert time_step in time_steps for time_step in time_steps: - i_in_0 = ( - np.random.uniform(-1, 1, 3) - if conv_0 == "Finite-B6C" - else [np.random.uniform(-1, 1)] - ) - i_in_1 = ( - np.random.uniform(-1, 1, 3) - if conv_1 == "Finite-B6C" - else [np.random.uniform(-1, 1)] - ) + i_in_0 = np.random.uniform(-1, 1, 3) if conv_0 == "Finite-B6C" else [np.random.uniform(-1, 1)] + i_in_1 = np.random.uniform(-1, 1, 3) if conv_1 == "Finite-B6C" else [np.random.uniform(-1, 1)] i_in = np.concatenate([i_in_0, i_in_1]) voltage = converter.convert(i_in, time_step) # test if the single phase converters work independent and correct for singlephase subsystems @@ -562,19 +526,13 @@ def test_continuous_power_electronic_converter(converter_type, tau, interlocking action_space = converter.action_space # take 100 random actions seed(123) - actions = [ - [uniform(action_space.low, action_space.high)] for _ in range(len(g_times_cont)) - ] + actions = [[uniform(action_space.low, action_space.high)] for _ in range(len(g_times_cont))] times = g_times_cont * tau - continuous_converter_functions_testing( - converter, times, interlocking_time, actions, converter_type - ) + continuous_converter_functions_testing(converter, times, interlocking_time, actions, converter_type) -def continuous_converter_functions_testing( - converter, times, interlocking_time, actions, converter_type -): +def continuous_converter_functions_testing(converter, times, interlocking_time, actions, converter_type): """ test function for conversion :param converter: instantiated converter @@ -588,14 +546,7 @@ def continuous_converter_functions_testing( last_action = [np.zeros_like(actions[0])] for index, time in enumerate(times): action = actions[index] - parameters = ( - " Error during set action " - + str(interlocking_time) - + " " - + str(action) - + " " - + str(time) - ) + parameters = " Error during set action " + str(interlocking_time) + " " + str(action) + " " + str(time) time_steps = converter.set_action(action, time) for time_step in time_steps: for i_in in g_i_ins_cont: @@ -611,39 +562,26 @@ def continuous_converter_functions_testing( interlocking_time, last_action[0], ) - assert abs((voltage[0] - conversion[0])) < 1e-5, ( - "Wrong voltage: " - + str( - [ - tau, - interlocking_time, - action, - last_action, - time_step, - i_in, - conversion, - voltage, - ] - ) - ) - params = ( - parameters - + " " - + str(i_in) - + " " - + str(time_step) - + " " - + str(conversion) + assert abs((voltage[0] - conversion[0])) < 1e-5, "Wrong voltage: " + str( + [ + tau, + interlocking_time, + action, + last_action, + time_step, + i_in, + conversion, + voltage, + ] ) + params = parameters + " " + str(i_in) + " " + str(time_step) + " " + str(conversion) assert (converter.action_space.low.tolist() <= conversion) and ( converter.action_space.high.tolist() >= conversion ), "Error, does not hold limits:" + str(params) last_action = action -def comparable_voltage( - converter_type, action, i_in, tau, interlocking_time, last_action -): +def comparable_voltage(converter_type, action, i_in, tau, interlocking_time, last_action): voltage = np.array([action]) error = np.array([-np.sign(i_in) / tau * interlocking_time]) if converter_type == "Cont-2QC": @@ -682,26 +620,15 @@ def test_continuous_multi_power_electronic_converter(tau, interlocking_time): ) assert all( converter.reset() - == np.concatenate( - [ - [-0.5, -0.5, -0.5] if ("Cont-B6C" == conv) else [0] - for conv in [conv_1, conv_2] - ] - ) + == np.concatenate([[-0.5, -0.5, -0.5] if ("Cont-B6C" == conv) else [0] for conv in [conv_1, conv_2]]) ) action_space = converter.action_space seed(123) - actions = [ - uniform(action_space.low, action_space.high) for _ in range(0, 100) - ] - continuous_multi_converter_functions_testing( - converter, times, interlocking_time, actions, [conv_1, conv_2] - ) + actions = [uniform(action_space.low, action_space.high) for _ in range(0, 100)] + continuous_multi_converter_functions_testing(converter, times, interlocking_time, actions, [conv_1, conv_2]) -def continuous_multi_converter_functions_testing( - converter, times, interlocking_time, actions, converter_type -): +def continuous_multi_converter_functions_testing(converter, times, interlocking_time, actions, converter_type): """ test function for conversion :param converter: instantiated converter @@ -715,14 +642,7 @@ def continuous_multi_converter_functions_testing( last_action = np.zeros_like(actions[0]) for index, time in enumerate(times): action = actions[index] - parameters = ( - " Error during set action " - + str(interlocking_time) - + " " - + str(action) - + " " - + str(time) - ) + parameters = " Error during set action " + str(interlocking_time) + " " + str(action) + " " + str(time) time_steps = converter.set_action(action, time) for time_step in time_steps: for i_in in g_i_ins_cont: @@ -733,18 +653,8 @@ def continuous_multi_converter_functions_testing( i_in_0 = np.abs(i_in_0) if converter_type[1] == "Cont-1QC": i_in_1 = np.abs(i_in_1) - conversion = converter.convert( - np.concatenate([i_in_0, i_in_1]), time_step - ) - params = ( - parameters - + " " - + str(i_in) - + " " - + str(time_step) - + " " - + str(conversion) - ) + conversion = converter.convert(np.concatenate([i_in_0, i_in_1]), time_step) + params = parameters + " " + str(i_in) + " " + str(time_step) + " " + str(conversion) # test if the limits are hold assert (converter.action_space.low.tolist() <= conversion) and ( converter.action_space.high.tolist() >= conversion @@ -767,12 +677,8 @@ def continuous_multi_converter_functions_testing( interlocking_time, last_action[1], ) - assert ( - abs(voltage_0 - conversion[0]) < 1e-5 - ), "Wrong converter value for armature circuit" - assert ( - abs(voltage_1 - conversion[1]) < 1e-5 - ), "Wrong converter value for excitation circuit" + assert abs(voltage_0 - conversion[0]) < 1e-5, "Wrong converter value for armature circuit" + assert abs(voltage_1 - conversion[1]) < 1e-5, "Wrong converter value for excitation circuit" last_action = action @@ -822,23 +728,16 @@ def test_discrete_b6_bridge(): for time_step in time_steps: i_in[k] = [i_in_] voltage = converter.convert(i_in, time_step) - assert voltage[k] == 0.5 * u_out[step_counter], ( - "Wrong action " + str(step_counter) - ) + assert voltage[k] == 0.5 * u_out[step_counter], "Wrong action " + str(step_counter) step_counter += 1 # test parametrized converter - converter_init_1 = make_module( - cv.PowerElectronicConverter, "Finite-B6C", **cf.converter_parameter - ) + converter_init_1 = make_module(cv.PowerElectronicConverter, "Finite-B6C", **cf.converter_parameter) converter_init_2 = cv.FiniteB6BridgeConverter(**cf.converter_parameter) assert converter_init_1._tau == cf.converter_parameter["tau"] for subconverter in converter_init_1._sub_converters: assert subconverter._tau == cf.converter_parameter["tau"] - assert ( - subconverter._interlocking_time - == cf.converter_parameter["interlocking_time"] - ) + assert subconverter._interlocking_time == cf.converter_parameter["interlocking_time"] # set parameter actions = [6, 6, 4, 5, 1, 2, 3, 7, 0, 4] i_ins = [ @@ -893,9 +792,7 @@ def test_discrete_b6_bridge(): for time_step in time_steps: voltage = np.array(converter.convert(i_in, time_step)) converter.i_sup(i_in) - assert all(voltage == expected_voltage[step_counter]), ( - "Wrong voltage calculated " + str(step_counter) - ) + assert all(voltage == expected_voltage[step_counter]), "Wrong voltage calculated " + str(step_counter) step_counter += 1 @@ -967,30 +864,21 @@ def test_continuous_b6_bridge(): ] ) converter_init_1 = cv.ContB6BridgeConverter(**cf.converter_parameter) - converter_init_2 = make_module( - cv.PowerElectronicConverter, "Cont-B6C", **cf.converter_parameter - ) + converter_init_2 = make_module(cv.PowerElectronicConverter, "Cont-B6C", **cf.converter_parameter) converters = [converter_init_1, converter_init_2] for converter in converters: # parameter testing assert converter._tau == cf.converter_parameter["tau"] - assert ( - converter._interlocking_time == cf.converter_parameter["interlocking_time"] - ) + assert converter._interlocking_time == cf.converter_parameter["interlocking_time"] assert all(converter.reset() == -0.5 * np.ones(3)) assert converter.action_space.shape == (3,) assert all(converter.action_space.low == -1 * np.ones(3)) assert all(converter.action_space.high == 1 * np.ones(3)) for subconverter in converter._subconverters: assert subconverter._tau == cf.converter_parameter["tau"] - assert ( - subconverter._interlocking_time - == cf.converter_parameter["interlocking_time"] - ) + assert subconverter._interlocking_time == cf.converter_parameter["interlocking_time"] # conversion testing - for time, action, i_in, expected_voltage in zip( - times, actions, i_ins, expected_voltages - ): + for time, action, i_in, expected_voltage in zip(times, actions, i_ins, expected_voltages): i_in = np.array(i_in) time_step = converter.set_action(action.tolist(), time) voltages = converter.convert(i_in, time_step) @@ -1023,9 +911,7 @@ def converter(self): ], ) def test_initialization(self, tau, interlocking_time, **kwargs): - converter = self.class_to_test( - tau=tau, interlocking_time=interlocking_time, **kwargs - ) + converter = self.class_to_test(tau=tau, interlocking_time=interlocking_time, **kwargs) assert converter._tau == tau assert converter._interlocking_time == interlocking_time @@ -1041,9 +927,7 @@ def test_reset(self, converter): @pytest.mark.parametrize("action_space", [Discrete(3), Box(-1, 1, shape=(1,))]) def test_set_action(self, monkeypatch, converter, action_space): monkeypatch.setattr(converter, "_set_switching_pattern", lambda action: [0.0]) - monkeypatch.setattr( - converter, "action_space", converter.action_space or action_space - ) + monkeypatch.setattr(converter, "action_space", converter.action_space or action_space) next_action = converter.action_space.sample() converter.set_action(next_action, 0) assert np.all(converter._current_action == next_action) @@ -1094,14 +978,10 @@ def test_set_action(self, monkeypatch, converter, action_space): super().test_set_action(monkeypatch, converter, action_space) def test_action_clipping(self, converter): - low_action = converter.action_space.low - np.ones_like( - converter.action_space.low - ) + low_action = converter.action_space.low - np.ones_like(converter.action_space.low) converter.set_action(low_action, 0) assert np.all(converter._current_action == converter.action_space.low) - high_action = converter.action_space.high[0] + np.ones_like( - converter.action_space.high - ) + high_action = converter.action_space.high[0] + np.ones_like(converter.action_space.high) converter.set_action(high_action, 0) assert np.all(converter._current_action == converter.action_space.high) fitting_action = converter.action_space.sample() @@ -1125,21 +1005,15 @@ def test_set_action(self, monkeypatch, action_space): time = 0 with pytest.raises(AssertionError) as assertText: converter.set_action(-1, time) - assert "-1" in str(assertText.value) and "Discrete(" + str( - action_space - ) + ")" in str(assertText.value) + assert "-1" in str(assertText.value) and "Discrete(" + str(action_space) + ")" in str(assertText.value) with pytest.raises(AssertionError) as assertText: converter.set_action(int(1e9), time) - assert str(int(1e9)) in str(assertText.value) and "Discrete(" + str( - action_space - ) + ")" in str(assertText.value) + assert str(int(1e9)) in str(assertText.value) and "Discrete(" + str(action_space) + ")" in str(assertText.value) with pytest.raises(AssertionError) as assertText: converter.set_action(np.pi, time) - assert str(np.pi) in str(assertText.value) and "Discrete(" + str( - action_space - ) + ")" in str(assertText.value) + assert str(np.pi) in str(assertText.value) and "Discrete(" + str(action_space) + ")" in str(assertText.value) def test_default_init(self): converter = self.class_to_test() @@ -1251,15 +1125,11 @@ def test_set_action(self, converter, *_): with pytest.raises(AssertionError) as assertText: converter.set_action(int(1e9), time) - assert str(int(1e9)) in str(assertText.value) and "Discrete(4)" in str( - assertText.value - ) + assert str(int(1e9)) in str(assertText.value) and "Discrete(4)" in str(assertText.value) with pytest.raises(AssertionError) as assertText: converter.set_action(np.pi, time) - assert str(np.pi) in str(assertText.value) and "Discrete(4)" in str( - assertText.value - ) + assert str(np.pi) in str(assertText.value) and "Discrete(4)" in str(assertText.value) @pytest.mark.parametrize("i_out", [[-12], [0], [12]]) def test_convert(self, converter, i_out): @@ -1270,20 +1140,12 @@ def test_convert(self, converter, i_out): assert converter._subconverters[0].last_i_out == i_out assert converter._subconverters[1].last_t == t assert converter._subconverters[1].last_i_out == [-i_out[0]] - assert u == [ - converter._subconverters[0].last_u[0] - - converter._subconverters[1].last_u[0] - ] + assert u == [converter._subconverters[0].last_u[0] - converter._subconverters[1].last_u[0]] def test_reset(self, converter): reset_calls = [conv.reset_calls for conv in converter._subconverters] super().test_reset(converter) - assert np.all( - [ - conv.reset_calls == reset_calls[0] + 1 - for conv in converter._subconverters - ] - ) + assert np.all([conv.reset_calls == reset_calls[0] + 1 for conv in converter._subconverters]) @pytest.mark.parametrize("i_out", [[-1], [0], [1]]) def test_i_sup(self, converter, i_out): @@ -1293,11 +1155,7 @@ def test_i_sup(self, converter, i_out): i_sup = converter.i_sup(i_out) assert converter._subconverters[0].last_i_out == i_out assert converter._subconverters[1].last_i_out == [-i_out[0]] - assert ( - i_sup - == converter._subconverters[0].last_i_sup - + converter._subconverters[1].last_i_sup - ) + assert i_sup == converter._subconverters[0].last_i_sup + converter._subconverters[1].last_i_sup class TestContOneQuadrantConverter(TestContDynamicallyAveragedConverter): @@ -1326,9 +1184,7 @@ def test_set_action(self, monkeypatch, converter, *_): @pytest.mark.parametrize("i_out", [[-1], [1], [0]]) def test_i_sup(self, monkeypatch, converter, i_out): - monkeypatch.setattr( - converter, "_current_action", converter.action_space.sample() - ) + monkeypatch.setattr(converter, "_current_action", converter.action_space.sample()) assert converter.i_sup(i_out) == converter._current_action[0] * i_out[0] @@ -1341,9 +1197,7 @@ class TestContTwoQuadrantConverter(TestContDynamicallyAveragedConverter): @pytest.mark.parametrize("tau", [1, 2]) @pytest.mark.parametrize("action", [[0], [0.5], [1]]) def test_i_sup(self, monkeypatch, converter, interlocking_time, i_out, tau, action): - monkeypatch.setattr( - converter, "_interlocking_time", min(tau, interlocking_time) - ) + monkeypatch.setattr(converter, "_interlocking_time", min(tau, interlocking_time)) monkeypatch.setattr(converter, "_tau", tau) monkeypatch.setattr(converter, "_current_action", action) i_sup = converter.i_sup(i_out) @@ -1375,11 +1229,7 @@ def test_convert(self, monkeypatch, converter, i_out): converter.set_action(converter.action_space.sample(), 0) for _ in np.linspace(0, converter._tau): u = converter.convert(i_out, 0) - assert ( - u[0] - == converter._subconverters[0].last_u[0] - - converter._subconverters[1].last_u[0] - ) + assert u[0] == converter._subconverters[0].last_u[0] - converter._subconverters[1].last_u[0] def test_reset(self, converter): u = converter.reset() @@ -1442,30 +1292,17 @@ def converter(self): ) def test_initialization(self, tau, interlocking_time, kwargs): super().test_initialization(tau, interlocking_time, **kwargs) - conv = self.class_to_test( - tau=tau, interlocking_time=interlocking_time, **kwargs - ) + conv = self.class_to_test(tau=tau, interlocking_time=interlocking_time, **kwargs) assert np.all( conv.subsignal_voltage_space_dims - == np.array( - [ - (np.squeeze(subconv.voltages.shape) or 1) - for subconv in conv._sub_converters - ] - ) + == np.array([(np.squeeze(subconv.voltages.shape) or 1) for subconv in conv._sub_converters]) ), "Voltage space dims in the multi converter do not fit the subconverters." assert np.all( conv.subsignal_current_space_dims - == np.array( - [ - (np.squeeze(subconv.currents.shape) or 1) - for subconv in conv._sub_converters - ] - ) + == np.array([(np.squeeze(subconv.currents.shape) or 1) for subconv in conv._sub_converters]) ), "Current space dims in the multi converter do not fit the subconverters." assert np.all( - conv.action_space.nvec - == [subconv.action_space.n for subconv in conv._sub_converters] + conv.action_space.nvec == [subconv.action_space.n for subconv in conv._sub_converters] ), "Action space of the multi converter does not fit the subconverters." for sc in conv._sub_converters: assert sc._interlocking_time == interlocking_time @@ -1473,17 +1310,13 @@ def test_initialization(self, tau, interlocking_time, kwargs): def test_registered(self): dummy_converters = [DummyConverter(), DummyConverter(), DummyConverter()] - conv = gem.utils.instantiate( - cv.PowerElectronicConverter, self.key, subconverters=dummy_converters - ) + conv = gem.utils.instantiate(cv.PowerElectronicConverter, self.key, subconverters=dummy_converters) assert type(conv) == self.class_to_test def test_reset(self, converter): u_in = converter.reset() assert u_in == [0.0] * converter.voltages.shape[0] - assert np.all( - [subconv.reset_counter == 1 for subconv in converter._sub_converters] - ) + assert np.all([subconv.reset_counter == 1 for subconv in converter._sub_converters]) def test_set_action(self, monkeypatch, converter, **_): for action in np.ndindex(tuple(converter.action_space.nvec)): @@ -1491,9 +1324,7 @@ def test_set_action(self, monkeypatch, converter, **_): sc1 = converter._sub_converters[1] sc2 = converter._sub_converters[2] t = ( - action[0] * sc1.action_space.n * sc2.action_space.n - + action[1] * sc2.action_space.n - + action[2] + action[0] * sc1.action_space.n * sc2.action_space.n + action[1] * sc2.action_space.n + action[2] ) * converter._tau converter.set_action(action, t) assert np.all((sc0.action, sc1.action, sc2.action) == action) @@ -1502,9 +1333,7 @@ def test_set_action(self, monkeypatch, converter, **_): assert sc2.action_set_time == t def test_default_init(self): - converter = self.class_to_test( - subconverters=["Finite-1QC", "Finite-B6C", "Finite-2QC"] - ) + converter = self.class_to_test(subconverters=["Finite-1QC", "Finite-B6C", "Finite-2QC"]) assert converter._tau == 1e-5 @pytest.mark.parametrize("i_out", [[0, 6, 2, 7, 9], [1, 0.5, 2], [-1, 1]]) @@ -1515,9 +1344,7 @@ def test_convert(self, converter, i_out): u = converter.convert(i_out, 0) assert np.all(u == [subconv.action for subconv in converter._sub_converters]) - @pytest.mark.parametrize( - "i_out", [[0, 6, 2, 7, 9], [1, 0.5, 2, -7, 50], [-1, 1, 0.01, 16, -42]] - ) + @pytest.mark.parametrize("i_out", [[0, 6, 2, 7, 9], [1, 0.5, 2, -7, 50], [-1, 1, 0.01, 16, -42]]) def test_i_sup(self, converter, i_out): sc0, sc1, sc2 = converter._sub_converters converter.set_action(converter.action_space.sample(), np.random.rand()) @@ -1557,9 +1384,7 @@ def test_registered(self): dummy_converters[0].action_space = Box(-1, 1, shape=(1,)) dummy_converters[1].action_space = Box(-1, 1, shape=(3,)) dummy_converters[2].action_space = Box(0, 1, shape=(1,)) - conv = gem.utils.instantiate( - cv.PowerElectronicConverter, self.key, subconverters=dummy_converters - ) + conv = gem.utils.instantiate(cv.PowerElectronicConverter, self.key, subconverters=dummy_converters) assert type(conv) == self.class_to_test assert conv._sub_converters == dummy_converters @@ -1572,38 +1397,20 @@ def test_registered(self): ) def test_initialization(self, tau, interlocking_time, kwargs): super().test_initialization(tau, interlocking_time, **kwargs) - conv = self.class_to_test( - tau=tau, interlocking_time=interlocking_time, **kwargs - ) + conv = self.class_to_test(tau=tau, interlocking_time=interlocking_time, **kwargs) assert np.all( - conv.action_space.low - == np.concatenate( - [subconv.action_space.low for subconv in conv._sub_converters] - ) + conv.action_space.low == np.concatenate([subconv.action_space.low for subconv in conv._sub_converters]) ), "Action space lower boundaries in the multi converter do not fit the subconverters." assert np.all( - conv.action_space.high - == np.concatenate( - [subconv.action_space.high for subconv in conv._sub_converters] - ) + conv.action_space.high == np.concatenate([subconv.action_space.high for subconv in conv._sub_converters]) ), "Action space upper boundaries in the multi converter do not fit the subconverters." assert np.all( conv.subsignal_voltage_space_dims - == np.array( - [ - (np.squeeze(subconv.voltages.shape) or 1) - for subconv in conv._sub_converters - ] - ) + == np.array([(np.squeeze(subconv.voltages.shape) or 1) for subconv in conv._sub_converters]) ), "Voltage space dims in the multi converter do not fit the subconverters." assert np.all( conv.subsignal_current_space_dims - == np.array( - [ - (np.squeeze(subconv.currents.shape) or 1) - for subconv in conv._sub_converters - ] - ) + == np.array([(np.squeeze(subconv.currents.shape) or 1) for subconv in conv._sub_converters]) ), "Current space dims in the multi converter do not fit the subconverters." for sc in conv._sub_converters: assert sc._interlocking_time == interlocking_time @@ -1612,9 +1419,7 @@ def test_initialization(self, tau, interlocking_time, kwargs): def test_reset(self, converter): u_in = converter.reset() assert u_in == [0.0] * converter.voltages.shape[0] - assert np.all( - [subconv.reset_counter == 1 for subconv in converter._sub_converters] - ) + assert np.all([subconv.reset_counter == 1 for subconv in converter._sub_converters]) def test_action_clipping(self, converter): # Done by the subconverters @@ -1652,9 +1457,7 @@ def test_convert(self, monkeypatch, converter, i_out): u = converter.convert(i_out, t) sub_u = [] start_idx = 0 - for subconverter, subaction_space_size in zip( - converter._sub_converters, action_space_size - ): + for subconverter, subaction_space_size in zip(converter._sub_converters, action_space_size): end_idx = start_idx + subaction_space_size assert subconverter.i_out == i_out[start_idx:end_idx] start_idx = end_idx @@ -1671,8 +1474,7 @@ class TestFiniteB6BridgeConverter(TestFiniteConverter): def converter(self): conv = self.class_to_test() subconverters = [ - PowerElectronicConverterWrapper(subconverter, tau=conv._tau) - for subconverter in conv._sub_converters + PowerElectronicConverterWrapper(subconverter, tau=conv._tau) for subconverter in conv._sub_converters ] conv._sub_converters = subconverters return conv @@ -1685,18 +1487,14 @@ def converter(self): ], ) def test_subconverter_initialization(self, tau, interlocking_time, kwargs): - conv = self.class_to_test( - tau=tau, interlocking_time=interlocking_time, **kwargs - ) + conv = self.class_to_test(tau=tau, interlocking_time=interlocking_time, **kwargs) for sc in conv._sub_converters: assert sc._interlocking_time == interlocking_time assert sc._tau == tau def test_reset(self, converter): u_init = converter.reset() - assert np.all( - subconv.reset_counter == 1 for subconv in converter._sub_converters - ) + assert np.all(subconv.reset_counter == 1 for subconv in converter._sub_converters) assert u_init == [-0.5] * 3 def test_set_action(self, converter, *_): @@ -1716,15 +1514,11 @@ def test_set_action(self, converter, *_): with pytest.raises(AssertionError) as assertText: converter.set_action(int(1e9), time) - assert str(int(1e9)) in str(assertText.value) and "Discrete(8)" in str( - assertText.value - ) + assert str(int(1e9)) in str(assertText.value) and "Discrete(8)" in str(assertText.value) with pytest.raises(AssertionError) as assertText: converter.set_action(np.pi, time) - assert str(np.pi) in str(assertText.value) and "Discrete(8)" in str( - assertText.value - ) + assert str(np.pi) in str(assertText.value) and "Discrete(8)" in str(assertText.value) @pytest.mark.parametrize("i_out", [[-1, -1, 0], [1, 1, -2], [0, 0, 1]]) def test_convert(self, converter, i_out): @@ -1757,8 +1551,7 @@ class TestContB6BridgeConverter(TestContDynamicallyAveragedConverter): def converter(self): conv = self.class_to_test() subconverters = [ - PowerElectronicConverterWrapper(subconverter, tau=conv._tau) - for subconverter in conv._subconverters + PowerElectronicConverterWrapper(subconverter, tau=conv._tau) for subconverter in conv._subconverters ] conv._subconverters = subconverters return conv @@ -1769,9 +1562,7 @@ def test_action_clipping(self, converter): def test_reset(self, converter): u_init = converter.reset() - assert np.all( - subconv.reset_counter == 1 for subconv in converter._subconverters - ) + assert np.all(subconv.reset_counter == 1 for subconv in converter._subconverters) assert u_init == [-0.5] * 3 @pytest.mark.parametrize("i_out", [[-1, -1, 0], [1, 1, -2], [0, 0, 1]]) diff --git a/tests/test_reward_functions/test_reward_functions.py b/tests/test_reward_functions/test_reward_functions.py index b7e57d4e..7ebff754 100644 --- a/tests/test_reward_functions/test_reward_functions.py +++ b/tests/test_reward_functions/test_reward_functions.py @@ -25,11 +25,9 @@ def reward_function(self): ["state", "reference"], [ [np.array([1.0, 2.0, 3.0, 4.0]), np.array([0.0, 2.0, 4.0, 0.0])], - [np.array([1.0, -2.0]), np.array([-1.0, 20.0])], - [np.array([-24.0]), np.array([0.0])], ], ) - @pytest.mark.parametrize("k", [0, 1, 2, 3, 4, 90999]) + @pytest.mark.parametrize("k", [0, 1, 4, 90999]) @pytest.mark.parametrize("violation_degree", [0.0, 0.25, 0.5, 1.0]) @pytest.mark.parametrize("action", [np.array([0.0, 1.0]), np.array([-1.0]), 8, 0]) def test_call(self, reward_function, state, reference, k, action, violation_degree): @@ -61,12 +59,8 @@ def test_reset_interface(self, reward_function, initial_state, initial_reference ["physical_system", "reference_generator", "constraint_monitor"], [[DummyPhysicalSystem(), DummyReferenceGenerator(), DummyConstraintMonitor()]], ) - def test_set_modules_interface( - self, reward_function, physical_system, reference_generator, constraint_monitor - ): - reward_function.set_modules( - physical_system, reference_generator, constraint_monitor - ) + def test_set_modules_interface(self, reward_function, physical_system, reference_generator, constraint_monitor): + reward_function.set_modules(physical_system, reference_generator, constraint_monitor) def test_close_interface(self, reward_function): reward_function.close() From b4848f5b2c3a063d7112a14eb3957851a9816554 Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Sun, 4 Feb 2024 16:21:03 +0100 Subject: [PATCH 19/51] ruff check --fix --- DEVELOPMENT.md | 14 +- pyproject.toml | 10 + src/gem_controllers/__init__.py | 15 +- .../block_diagrams/block_diagram.py | 16 +- .../block_diagrams/stage_blocks/__init__.py | 10 +- .../block_diagrams/stage_blocks/eesm_cc.py | 10 +- .../block_diagrams/stage_blocks/eesm_ops.py | 2 +- .../stage_blocks/eesm_output.py | 2 +- .../stage_blocks/ext_ex_dc_cc.py | 2 +- .../stage_blocks/ext_ex_dc_ops.py | 4 +- .../stage_blocks/perm_ex_dc_output.py | 2 +- .../stage_blocks/pi_speed_controller.py | 2 +- .../block_diagrams/stage_blocks/pmsm_cc.py | 10 +- .../block_diagrams/stage_blocks/pmsm_ops.py | 2 +- .../block_diagrams/stage_blocks/pmsm_sc.py | 2 +- .../block_diagrams/stage_blocks/scim_cc.py | 10 +- .../block_diagrams/stage_blocks/scim_ops.py | 4 +- .../stage_blocks/scim_output.py | 2 +- .../block_diagrams/stage_blocks/scim_sc.py | 2 +- .../stage_blocks/series_dc_output.py | 2 +- .../stage_blocks/shunt_dc_output.py | 2 +- .../block_diagrams/stage_blocks/synrm_cc.py | 10 +- .../stage_blocks/synrm_output.py | 2 +- src/gem_controllers/cascaded_controller.py | 4 +- src/gem_controllers/current_controller.py | 5 +- src/gem_controllers/gem_adapter.py | 12 +- src/gem_controllers/gem_controller.py | 4 +- src/gem_controllers/parameter_reader.py | 6 +- src/gem_controllers/pi_speed_controller.py | 15 +- src/gem_controllers/stages/__init__.py | 16 +- .../stages/abc_transformation.py | 8 +- .../stages/base_controllers/__init__.py | 6 +- .../e_base_controller_task.py | 4 +- .../stages/base_controllers/p_controller.py | 5 +- .../stages/base_controllers/pi_controller.py | 12 +- .../three_point_controller.py | 5 +- .../stages/clipping_stages/__init__.py | 4 +- .../absolute_clipping_stage.py | 6 +- .../stages/clipping_stages/clipping_stage.py | 3 +- .../combined_clipping_stage.py | 4 +- .../clipping_stages/squared_clipping_stage.py | 1 + .../stages/cont_output_stage.py | 5 +- .../stages/disc_output_stage.py | 9 +- src/gem_controllers/stages/emf_feedforward.py | 5 +- .../stages/emf_feedforward_eesm.py | 4 +- .../stages/emf_feedforward_ind.py | 3 +- .../operation_point_selection/__init__.py | 10 +- .../operation_point_selection/eesm_ops.py | 1 + .../operation_point_selection/extex_dc_ttc.py | 5 +- .../foc_operation_point_selection.py | 3 +- .../operation_point_selection/ops_utils.py | 8 +- .../permex_dc_ops.py | 3 +- .../operation_point_selection/pmsm_ops.py | 3 +- .../operation_point_selection/scim_ops.py | 6 +- .../series_dc_ops.py | 3 +- .../operation_point_selection/shunt_dc_ops.py | 2 +- src/gem_controllers/torque_controller.py | 2 +- src/gem_controllers/utils.py | 4 +- src/gym_electric_motor/__init__.py | 41 ++- src/gym_electric_motor/callbacks.py | 3 +- src/gym_electric_motor/core.py | 20 +- src/gym_electric_motor/envs/__init__.py | 140 ++++---- .../envs/gym_dcm/__init__.py | 59 ++-- .../cont_cc_extex_dc_env.py | 8 +- .../cont_sc_extex_dc_env.py | 6 +- .../cont_tc_extex_dc_env.py | 6 +- .../finite_cc_extex_dc_env.py | 8 +- .../finite_sc_extex_dc_env.py | 6 +- .../finite_tc_extex_dc_env.py | 6 +- .../gym_dcm/permex_dc_motor_env/__init__.py | 6 +- .../cont_cc_permex_dc_env.py | 6 +- .../cont_sc_permex_dc_env.py | 6 +- .../cont_tc_permex_dc_env.py | 6 +- .../finite_cc_permex_dc_env.py | 6 +- .../finite_sc_permex_dc_env.py | 6 +- .../finite_tc_permex_dc_env.py | 6 +- .../cont_cc_series_dc_env.py | 6 +- .../cont_sc_series_dc_env.py | 6 +- .../cont_tc_series_dc_env.py | 6 +- .../finite_cc_series_dc_env.py | 6 +- .../finite_sc_series_dc_env.py | 6 +- .../finite_tc_series_dc_env.py | 6 +- .../cont_cc_shunt_dc_env.py | 8 +- .../cont_sc_shunt_dc_env.py | 8 +- .../cont_tc_shunt_dc_env.py | 8 +- .../finite_cc_shunt_dc_env.py | 8 +- .../finite_sc_shunt_dc_env.py | 8 +- .../finite_tc_shunt_dc_env.py | 8 +- .../envs/gym_eesm/__init__.py | 6 +- .../envs/gym_eesm/cont_cc_eesm_env.py | 10 +- .../envs/gym_eesm/cont_sc_eesm_env.py | 8 +- .../envs/gym_eesm/cont_tc_eesm_env.py | 10 +- .../envs/gym_eesm/finite_cc_eesm_env.py | 10 +- .../envs/gym_eesm/finite_sc_eesm_env.py | 8 +- .../envs/gym_eesm/finite_tc_eesm_env.py | 8 +- .../envs/gym_im/__init__.py | 33 +- .../__init__.py | 6 +- .../cont_cc_dfim_env.py | 10 +- .../cont_sc_dfim_env.py | 8 +- .../cont_tc_dfim_env.py | 10 +- .../finite_cc_dfim_env.py | 10 +- .../finite_sc_dfim_env.py | 8 +- .../finite_tc_dfim_env.py | 8 +- .../__init__.py | 6 +- .../cont_cc_scim_env.py | 10 +- .../cont_sc_scim_env.py | 8 +- .../cont_tc_scim_env.py | 10 +- .../finite_cc_scim_env.py | 10 +- .../finite_sc_scim_env.py | 8 +- .../finite_tc_scim_env.py | 8 +- .../envs/gym_pmsm/__init__.py | 6 +- .../envs/gym_pmsm/cont_cc_pmsm_env.py | 10 +- .../envs/gym_pmsm/cont_sc_pmsm_env.py | 8 +- .../envs/gym_pmsm/cont_tc_pmsm_env.py | 10 +- .../envs/gym_pmsm/finite_cc_pmsm_env.py | 10 +- .../envs/gym_pmsm/finite_sc_pmsm_env.py | 8 +- .../envs/gym_pmsm/finite_tc_pmsm_env.py | 8 +- .../envs/gym_synrm/__init__.py | 6 +- .../envs/gym_synrm/cont_cc_synrm_env.py | 10 +- .../envs/gym_synrm/cont_sc_synrm_env.py | 8 +- .../envs/gym_synrm/cont_tc_synrm_env.py | 10 +- .../envs/gym_synrm/finite_cc_synrm_env.py | 10 +- .../envs/gym_synrm/finite_sc_synrm_env.py | 8 +- .../envs/gym_synrm/finite_tc_synrm_env.py | 8 +- src/gym_electric_motor/envs/motors.py | 2 +- .../physical_system_wrappers/__init__.py | 8 +- .../cos_sin_processor.py | 2 +- .../current_sum_processor.py | 2 +- .../dead_time_processor.py | 5 +- .../dq_to_abc_action_processor.py | 3 +- .../physical_system_wrappers/flux_observer.py | 3 +- .../physical_system_wrapper.py | 8 +- .../physical_systems/__init__.py | 84 ++--- .../physical_systems/converters.py | 23 +- .../electric_motors/__init__.py | 20 +- .../dc_permanently_excited_motor.py | 1 - .../electric_motors/dc_series_motor.py | 1 - .../electric_motors/dc_shunt_motor.py | 1 - .../doubly_fed_induction_motor.py | 1 + .../electric_motors/electric_motor.py | 3 +- .../externally_excited_synchronous_motor.py | 1 + .../electric_motors/induction_motor.py | 1 + .../permanent_magnet_synchronous_motor.py | 1 + .../squirrel_cage_induction_motor.py | 1 + .../electric_motors/three_phase_motor.py | 1 + .../mechanical_loads/__init__.py | 4 +- .../mechanical_loads/external_speed_load.py | 3 +- .../mechanical_loads/mechanical_load.py | 3 +- .../polynomial_static_load.py | 3 +- .../physical_systems/physical_systems.py | 6 +- .../physical_systems/solvers.py | 2 +- .../physical_systems/voltage_supplies.py | 4 +- .../reference_generators/__init__.py | 20 +- .../const_reference_generator.py | 3 +- .../multiple_reference_generator.py | 10 +- .../sawtooth_reference_generator.py | 1 + .../subepisoded_reference_generator.py | 2 +- .../switched_reference_generator.py | 2 +- .../triangle_reference_generator.py | 1 + .../reward_functions/__init__.py | 4 +- .../weighted_sum_of_errors.py | 3 +- src/gym_electric_motor/utils.py | 88 +---- .../visualization/__init__.py | 5 +- .../visualization/console_printer.py | 5 +- .../visualization/motor_dashboard.py | 34 +- .../motor_dashboard_plots/__init__.py | 2 +- .../motor_dashboard_plots/action_plot.py | 2 +- .../motor_dashboard_plots/base_plots.py | 1 + .../test_environment_execution.py | 3 +- .../test_environment_seeding.py | 3 +- .../test_physical_systems/test_converters.py | 198 +++-------- .../test_mechanical_loads.py | 5 - .../test_voltage_supplies.py | 6 - .../test_reference_generators.py | 307 +++++------------- .../test_reward_functions.py | 5 +- .../test_weighted_sum_of_errors.py | 28 +- tests/test_utils.py | 79 ----- 177 files changed, 879 insertions(+), 1230 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 1cffdecc..149fca86 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -1,5 +1,17 @@ +# No poetry +Some complex dependecies system don't work good with poetry + # Linter and Formater Ruff: https://docs.astral.sh/ruff/installation/ # Install editable local package -pip install -e . \ No newline at end of file +pip install -e . + +Check correct package install directory with python interpreter +run `python` + +``` +>>> import gym_electric_motor as gem +>>> gem + +``` \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 3a22ef3d..b2a1bb9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,3 +39,13 @@ packages = ["/src/gym_electric_motor", "/src/gem_controllers"] [tool.ruff] src = ["src"] line-length = 120 +exclude = ["tests"] + +[tool.ruff.lint] +# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. +# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or +# McCabe complexity (`C901`) by default. +# I for isort +select = ["E4", "E7", "E9", "F", "I"] +ignore = ["F401"] # imported but unused +ignore-init-module-imports = true # ignore imports in __init__.py diff --git a/src/gem_controllers/__init__.py b/src/gem_controllers/__init__.py index 3c478396..456c91cb 100644 --- a/src/gem_controllers/__init__.py +++ b/src/gem_controllers/__init__.py @@ -1,12 +1,11 @@ -from . import stages -from . import utils -from . import parameter_reader -from .gem_controller import GemController -from .current_controller import CurrentController +from gem_controllers.block_diagrams.block_diagram import build_block_diagram + +from . import parameter_reader, stages, utils from .cascaded_controller import CascadedController +from .current_controller import CurrentController +from .gem_adapter import GymElectricMotorAdapter +from .gem_controller import GemController from .pi_current_controller import PICurrentController -from .torque_controller import TorqueController from .pi_speed_controller import PISpeedController -from .gem_adapter import GymElectricMotorAdapter from .reference_plotter import ReferencePlotter -from gem_controllers.block_diagrams.block_diagram import build_block_diagram +from .torque_controller import TorqueController diff --git a/src/gem_controllers/block_diagrams/block_diagram.py b/src/gem_controllers/block_diagrams/block_diagram.py index 2ba22fd0..2f101feb 100644 --- a/src/gem_controllers/block_diagrams/block_diagram.py +++ b/src/gem_controllers/block_diagrams/block_diagram.py @@ -1,6 +1,12 @@ from control_block_diagram import ControllerDiagram -from control_block_diagram.components import Point, Connection +from control_block_diagram.components import Connection, Point + +import gem_controllers as gc + from .stage_blocks import ( + eesm_cc, + eesm_ops, + eesm_output, ext_ex_dc_cc, ext_ex_dc_ops, ext_ex_dc_output, @@ -11,24 +17,20 @@ pmsm_cc, pmsm_ops, pmsm_output, + pmsm_speed_controller, scim_cc, scim_ops, scim_output, + scim_speed_controller, series_dc_cc, series_dc_ops, series_dc_output, shunt_dc_cc, shunt_dc_ops, shunt_dc_output, - pmsm_speed_controller, - scim_speed_controller, - eesm_ops, - eesm_cc, - eesm_output, synrm_cc, synrm_output, ) -import gem_controllers as gc def build_block_diagram(controller, env_id, save_block_diagram_as): diff --git a/src/gem_controllers/block_diagrams/stage_blocks/__init__.py b/src/gem_controllers/block_diagrams/stage_blocks/__init__.py index 5f1ff9ff..069d8bf1 100644 --- a/src/gem_controllers/block_diagrams/stage_blocks/__init__.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/__init__.py @@ -1,3 +1,6 @@ +from .eesm_cc import eesm_cc +from .eesm_ops import eesm_ops +from .eesm_output import eesm_output from .ext_ex_dc_cc import ext_ex_dc_cc from .ext_ex_dc_ops import ext_ex_dc_ops from .ext_ex_dc_output import ext_ex_dc_output @@ -8,19 +11,16 @@ from .pmsm_cc import pmsm_cc from .pmsm_ops import pmsm_ops from .pmsm_output import pmsm_output +from .pmsm_sc import pmsm_speed_controller from .scim_cc import scim_cc from .scim_ops import scim_ops from .scim_output import scim_output +from .scim_sc import scim_speed_controller from .series_dc_cc import series_dc_cc from .series_dc_ops import series_dc_ops from .series_dc_output import series_dc_output from .shunt_dc_cc import shunt_dc_cc from .shunt_dc_ops import shunt_dc_ops from .shunt_dc_output import shunt_dc_output -from .pmsm_sc import pmsm_speed_controller -from .scim_sc import scim_speed_controller -from .eesm_ops import eesm_ops -from .eesm_cc import eesm_cc -from .eesm_output import eesm_output from .synrm_cc import synrm_cc from .synrm_output import synrm_output diff --git a/src/gem_controllers/block_diagrams/stage_blocks/eesm_cc.py b/src/gem_controllers/block_diagrams/stage_blocks/eesm_cc.py index 9563bb85..86115b93 100644 --- a/src/gem_controllers/block_diagrams/stage_blocks/eesm_cc.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/eesm_cc.py @@ -1,12 +1,12 @@ -from control_block_diagram.components import Point, Box, Connection, Circle +from control_block_diagram.components import Box, Circle, Connection, Point from control_block_diagram.predefined_components import ( - DqToAbcTransformation, AbcToAlphaBetaTransformation, - AlphaBetaToDqTransformation, Add, - PIController, - Multiply, + AlphaBetaToDqTransformation, + DqToAbcTransformation, Limit, + Multiply, + PIController, ) diff --git a/src/gem_controllers/block_diagrams/stage_blocks/eesm_ops.py b/src/gem_controllers/block_diagrams/stage_blocks/eesm_ops.py index fcd5720a..46c372e0 100644 --- a/src/gem_controllers/block_diagrams/stage_blocks/eesm_ops.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/eesm_ops.py @@ -1,4 +1,4 @@ -from control_block_diagram.components import Box, Connection, Text, Point, Circle, Path +from control_block_diagram.components import Box, Circle, Connection, Path, Point, Text from control_block_diagram.predefined_components import Add, Divide, IController, Limit diff --git a/src/gem_controllers/block_diagrams/stage_blocks/eesm_output.py b/src/gem_controllers/block_diagrams/stage_blocks/eesm_output.py index 695d92d5..a2c4a9b9 100644 --- a/src/gem_controllers/block_diagrams/stage_blocks/eesm_output.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/eesm_output.py @@ -1,4 +1,4 @@ -from control_block_diagram.components import Connection, Circle, Text +from control_block_diagram.components import Circle, Connection, Text from control_block_diagram.predefined_components import EESM, DcConverter diff --git a/src/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_cc.py b/src/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_cc.py index 4fc8dfce..a8ae846e 100644 --- a/src/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_cc.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_cc.py @@ -1,5 +1,5 @@ from control_block_diagram.components import Box, Connection, Point -from control_block_diagram.predefined_components import Add, PIController, Limit +from control_block_diagram.predefined_components import Add, Limit, PIController def ext_ex_dc_cc(emf_feedforward): diff --git a/src/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_ops.py b/src/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_ops.py index 65a50914..74674928 100644 --- a/src/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_ops.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/ext_ex_dc_ops.py @@ -1,5 +1,5 @@ -from control_block_diagram.components import Box, Connection, Circle, Point -from control_block_diagram.predefined_components import Multiply, Divide, Limit +from control_block_diagram.components import Box, Circle, Connection, Point +from control_block_diagram.predefined_components import Divide, Limit, Multiply def ext_ex_dc_ops(start, control_task): diff --git a/src/gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_output.py b/src/gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_output.py index a3d26dcd..9109dc6f 100644 --- a/src/gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_output.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/perm_ex_dc_output.py @@ -1,5 +1,5 @@ from control_block_diagram.components import Box, Connection -from control_block_diagram.predefined_components import DcPermExMotor, DcConverter, Limit +from control_block_diagram.predefined_components import DcConverter, DcPermExMotor, Limit def perm_ex_dc_output(emf_feedforward): diff --git a/src/gem_controllers/block_diagrams/stage_blocks/pi_speed_controller.py b/src/gem_controllers/block_diagrams/stage_blocks/pi_speed_controller.py index 802d8fa3..ea5d6b10 100644 --- a/src/gem_controllers/block_diagrams/stage_blocks/pi_speed_controller.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/pi_speed_controller.py @@ -1,5 +1,5 @@ from control_block_diagram.components import Box, Connection -from control_block_diagram.predefined_components import Add, PIController, Limit +from control_block_diagram.predefined_components import Add, Limit, PIController def pi_speed_controller(start, control_task): diff --git a/src/gem_controllers/block_diagrams/stage_blocks/pmsm_cc.py b/src/gem_controllers/block_diagrams/stage_blocks/pmsm_cc.py index 944e046c..3fa04fc0 100644 --- a/src/gem_controllers/block_diagrams/stage_blocks/pmsm_cc.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/pmsm_cc.py @@ -1,12 +1,12 @@ -from control_block_diagram.components import Point, Box, Connection, Circle +from control_block_diagram.components import Box, Circle, Connection, Point from control_block_diagram.predefined_components import ( - DqToAbcTransformation, AbcToAlphaBetaTransformation, - AlphaBetaToDqTransformation, Add, - PIController, - Multiply, + AlphaBetaToDqTransformation, + DqToAbcTransformation, Limit, + Multiply, + PIController, ) diff --git a/src/gem_controllers/block_diagrams/stage_blocks/pmsm_ops.py b/src/gem_controllers/block_diagrams/stage_blocks/pmsm_ops.py index 378040bf..f1d7c358 100644 --- a/src/gem_controllers/block_diagrams/stage_blocks/pmsm_ops.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/pmsm_ops.py @@ -1,4 +1,4 @@ -from control_block_diagram.components import Box, Connection, Text, Point, Circle, Path +from control_block_diagram.components import Box, Circle, Connection, Path, Point, Text from control_block_diagram.predefined_components import Add, Divide, IController, Limit diff --git a/src/gem_controllers/block_diagrams/stage_blocks/pmsm_sc.py b/src/gem_controllers/block_diagrams/stage_blocks/pmsm_sc.py index 05123379..1bf9ac09 100644 --- a/src/gem_controllers/block_diagrams/stage_blocks/pmsm_sc.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/pmsm_sc.py @@ -1,5 +1,5 @@ from control_block_diagram.components import Connection -from control_block_diagram.predefined_components import Add, PIController, Limit +from control_block_diagram.predefined_components import Add, Limit, PIController def pmsm_speed_controller(start, control_task): diff --git a/src/gem_controllers/block_diagrams/stage_blocks/scim_cc.py b/src/gem_controllers/block_diagrams/stage_blocks/scim_cc.py index 85b49127..dee6e410 100644 --- a/src/gem_controllers/block_diagrams/stage_blocks/scim_cc.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/scim_cc.py @@ -1,11 +1,11 @@ -from control_block_diagram.components import Box, Connection, Text, Point, Circle +from control_block_diagram.components import Box, Circle, Connection, Point, Text from control_block_diagram.predefined_components import ( - Add, - PIController, - Limit, - DqToAbcTransformation, AbcToAlphaBetaTransformation, + Add, AlphaBetaToDqTransformation, + DqToAbcTransformation, + Limit, + PIController, ) diff --git a/src/gem_controllers/block_diagrams/stage_blocks/scim_ops.py b/src/gem_controllers/block_diagrams/stage_blocks/scim_ops.py index 830bb209..6a659112 100644 --- a/src/gem_controllers/block_diagrams/stage_blocks/scim_ops.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/scim_ops.py @@ -1,5 +1,5 @@ -from control_block_diagram.components import Box, Connection, Text, Circle, Point, Path -from control_block_diagram.predefined_components import Limit, PIController, IController, Add, Divide +from control_block_diagram.components import Box, Circle, Connection, Path, Point, Text +from control_block_diagram.predefined_components import Add, Divide, IController, Limit, PIController class PsiOptBox(Box): diff --git a/src/gem_controllers/block_diagrams/stage_blocks/scim_output.py b/src/gem_controllers/block_diagrams/stage_blocks/scim_output.py index 50844d4f..0517dcca 100644 --- a/src/gem_controllers/block_diagrams/stage_blocks/scim_output.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/scim_output.py @@ -1,5 +1,5 @@ from control_block_diagram.components import Connection -from control_block_diagram.predefined_components import DcConverter, SCIM +from control_block_diagram.predefined_components import SCIM, DcConverter def scim_output(emf_feedforward): diff --git a/src/gem_controllers/block_diagrams/stage_blocks/scim_sc.py b/src/gem_controllers/block_diagrams/stage_blocks/scim_sc.py index 7c280603..d1f7e960 100644 --- a/src/gem_controllers/block_diagrams/stage_blocks/scim_sc.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/scim_sc.py @@ -1,5 +1,5 @@ from control_block_diagram.components import Connection -from control_block_diagram.predefined_components import Add, PIController, Limit +from control_block_diagram.predefined_components import Add, Limit, PIController def scim_speed_controller(start, control_task): diff --git a/src/gem_controllers/block_diagrams/stage_blocks/series_dc_output.py b/src/gem_controllers/block_diagrams/stage_blocks/series_dc_output.py index 31d38a24..7d42e85d 100644 --- a/src/gem_controllers/block_diagrams/stage_blocks/series_dc_output.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/series_dc_output.py @@ -1,5 +1,5 @@ from control_block_diagram.components import Box, Connection -from control_block_diagram.predefined_components import DcSeriesMotor, DcConverter, Limit +from control_block_diagram.predefined_components import DcConverter, DcSeriesMotor, Limit def series_dc_output(emf_feedforward): diff --git a/src/gem_controllers/block_diagrams/stage_blocks/shunt_dc_output.py b/src/gem_controllers/block_diagrams/stage_blocks/shunt_dc_output.py index 84b52505..fa50bfe8 100644 --- a/src/gem_controllers/block_diagrams/stage_blocks/shunt_dc_output.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/shunt_dc_output.py @@ -1,5 +1,5 @@ from control_block_diagram.components import Box, Connection -from control_block_diagram.predefined_components import DcShuntMotor, DcConverter, Limit +from control_block_diagram.predefined_components import DcConverter, DcShuntMotor, Limit def shunt_dc_output(emf_feedforward): diff --git a/src/gem_controllers/block_diagrams/stage_blocks/synrm_cc.py b/src/gem_controllers/block_diagrams/stage_blocks/synrm_cc.py index 38a9cd3e..c23a9b33 100644 --- a/src/gem_controllers/block_diagrams/stage_blocks/synrm_cc.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/synrm_cc.py @@ -1,12 +1,12 @@ -from control_block_diagram.components import Point, Box, Connection, Circle +from control_block_diagram.components import Box, Circle, Connection, Point from control_block_diagram.predefined_components import ( - DqToAbcTransformation, AbcToAlphaBetaTransformation, - AlphaBetaToDqTransformation, Add, - PIController, - Multiply, + AlphaBetaToDqTransformation, + DqToAbcTransformation, Limit, + Multiply, + PIController, ) diff --git a/src/gem_controllers/block_diagrams/stage_blocks/synrm_output.py b/src/gem_controllers/block_diagrams/stage_blocks/synrm_output.py index 585196c9..42761097 100644 --- a/src/gem_controllers/block_diagrams/stage_blocks/synrm_output.py +++ b/src/gem_controllers/block_diagrams/stage_blocks/synrm_output.py @@ -1,5 +1,5 @@ from control_block_diagram.components import Connection -from control_block_diagram.predefined_components import SynRM, DcConverter +from control_block_diagram.predefined_components import DcConverter, SynRM def synrm_output(emf_feedforward): diff --git a/src/gem_controllers/cascaded_controller.py b/src/gem_controllers/cascaded_controller.py index a3419feb..187a7b51 100644 --- a/src/gem_controllers/cascaded_controller.py +++ b/src/gem_controllers/cascaded_controller.py @@ -1,7 +1,7 @@ -import gem_controllers as gc +from .gem_controller import GemController -class CascadedController(gc.GemController): +class CascadedController(GemController): """The CascadedController class contains a controller with multiple hierarchically structured stages.""" def control(self, state, reference): diff --git a/src/gem_controllers/current_controller.py b/src/gem_controllers/current_controller.py index c548f369..645964ed 100644 --- a/src/gem_controllers/current_controller.py +++ b/src/gem_controllers/current_controller.py @@ -1,8 +1,9 @@ import numpy as np -import gem_controllers as gc +from .gem_controller import GemController -class CurrentController(gc.GemController): + +class CurrentController(GemController): """Base class for a current controller""" def control(self, state, reference): diff --git a/src/gem_controllers/gem_adapter.py b/src/gem_controllers/gem_adapter.py index 53f273ad..4ed66280 100644 --- a/src/gem_controllers/gem_adapter.py +++ b/src/gem_controllers/gem_adapter.py @@ -1,10 +1,12 @@ -import gym_electric_motor as gem import numpy as np import gem_controllers as gc +import gym_electric_motor as gem + +from .gem_controller import GemController -class GymElectricMotorAdapter(gc.GemController): +class GymElectricMotorAdapter(GemController): """The GymElectricMotorAdapter wraps a GemController to map the inputs and outputs to the environment.""" @property @@ -42,19 +44,19 @@ def __init__( self, _env: (gem.core.ElectricMotorEnvironment, None) = None, env_id: (str, None) = None, - controller: (gc.GemController, None) = None, + controller: (GemController, None) = None, ): """ Args: _env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. env_id(str): The corresponding environment-id to specify the concrete environment. - controller(gc.GemController): The GemController that should be wrapped. + controller(GemController): The GemController that should be wrapped. """ super().__init__() self._input_stage = None self._output_stage = None - assert isinstance(controller, gc.GemController) + assert isinstance(controller, GemController) self._controller = controller self._input_stage = gc.stages.InputStage() action_type = gc.utils.get_action_type(env_id) diff --git a/src/gem_controllers/gem_controller.py b/src/gem_controllers/gem_controller.py index 7814e67c..228a3d4d 100644 --- a/src/gem_controllers/gem_controller.py +++ b/src/gem_controllers/gem_controller.py @@ -1,7 +1,7 @@ -import gym_electric_motor.core +import numpy as np import gem_controllers as gc -import numpy as np +import gym_electric_motor.core class GemController: diff --git a/src/gem_controllers/parameter_reader.py b/src/gem_controllers/parameter_reader.py index 4a27e78d..7f4f626c 100644 --- a/src/gem_controllers/parameter_reader.py +++ b/src/gem_controllers/parameter_reader.py @@ -1,7 +1,7 @@ -from gym_electric_motor.physical_systems import converters as cv - import numpy as np +from gym_electric_motor.physical_systems import converters as cv + dc_motors = ["SeriesDc", "ShuntDc", "PermExDc", "ExtExDc"] synchronous_motors = ["PMSM", "SynRM", "EESM"] induction_motors = ["DFIM", "SCIM"] @@ -428,7 +428,7 @@ def get(motor_key): return MotorSpecification._motors[motor_key] psi = None - l = None + l = None # noqa: E741 l_emf = None tau_current_loop = None tau_n = None diff --git a/src/gem_controllers/pi_speed_controller.py b/src/gem_controllers/pi_speed_controller.py index d6d3eee4..e0115fa1 100644 --- a/src/gem_controllers/pi_speed_controller.py +++ b/src/gem_controllers/pi_speed_controller.py @@ -1,7 +1,10 @@ import numpy as np -import gym_electric_motor as gem import gem_controllers as gc +import gym_electric_motor as gem + +from .stages import BaseController +from .torque_controller import TorqueController class PISpeedController(gc.GemController): @@ -17,12 +20,12 @@ def speed_control_stage(self, value: gc.stages.BaseController): self._speed_control_stage = value @property - def torque_controller(self) -> gc.TorqueController: + def torque_controller(self) -> TorqueController: """Subordinated torque controller stage""" return self._torque_controller @torque_controller.setter - def torque_controller(self, value: gc.TorqueController): + def torque_controller(self, value: TorqueController): self._torque_controller = value @property @@ -58,7 +61,7 @@ def __init__( self, _env: (gem.core.ElectricMotorEnvironment, None) = None, env_id: (str, None) = None, - torque_controller: (gc.TorqueController, None) = None, + torque_controller: (TorqueController, None) = None, base_speed_controller: str = "PI", ): """ @@ -67,7 +70,7 @@ def __init__( Args: _env(ElectricMotorEnvironment): The GEM-Environment that the controller shall be created for. env_id(str): The corresponding environment-id to specify the concrete environment. - torque_controller(gc.TorqueController): The underlying torque control stage + torque_controller(TorqueController): The underlying torque control stage base_speed_controller(str): Selection which base controller should be used for the speed control stage. """ @@ -75,7 +78,7 @@ def __init__( self._speed_control_stage = gc.stages.base_controllers.get(base_speed_controller)("SC") self._torque_controller = torque_controller if torque_controller is None: - self._torque_controller = gc.TorqueController() + self._torque_controller = TorqueController() self._torque_reference = np.array([]) self._anti_windup_stage = gc.stages.AntiWindup("SC") self._clipping_stage = gc.stages.clipping_stages.AbsoluteClippingStage("SC") diff --git a/src/gem_controllers/stages/__init__.py b/src/gem_controllers/stages/__init__.py index 2a11e126..f4e64ca8 100644 --- a/src/gem_controllers/stages/__init__.py +++ b/src/gem_controllers/stages/__init__.py @@ -1,17 +1,17 @@ -from .stage import Stage -from .cont_output_stage import ContOutputStage -from .disc_output_stage import DiscOutputStage +from . import clipping_stages from .abc_transformation import AbcTransformation +from .anti_windup import AntiWindup +from .base_controllers.base_controller import BaseController from .base_controllers.i_controller import IController from .base_controllers.p_controller import PController from .base_controllers.pi_controller import PIController from .base_controllers.pid_controller import PIDController from .base_controllers.three_point_controller import ThreePointController -from .base_controllers.base_controller import BaseController +from .cont_output_stage import ContOutputStage +from .disc_output_stage import DiscOutputStage from .emf_feedforward import EMFFeedforward -from .emf_feedforward_ind import EMFFeedforwardInd from .emf_feedforward_eesm import EMFFeedforwardEESM -from .operation_point_selection import OperationPointSelection, torque_to_current_function +from .emf_feedforward_ind import EMFFeedforwardInd from .input_stage import InputStage -from . import clipping_stages -from .anti_windup import AntiWindup +from .operation_point_selection import OperationPointSelection, torque_to_current_function +from .stage import Stage diff --git a/src/gem_controllers/stages/abc_transformation.py b/src/gem_controllers/stages/abc_transformation.py index 119ecca5..59d51ecc 100644 --- a/src/gem_controllers/stages/abc_transformation.py +++ b/src/gem_controllers/stages/abc_transformation.py @@ -1,8 +1,10 @@ -from gym_electric_motor.physical_systems.electric_motors import SynchronousMotor -import gem_controllers as gc import numpy as np -from .stage import Stage + +import gem_controllers as gc +from gym_electric_motor.physical_systems.electric_motors import SynchronousMotor + from .. import parameter_reader as reader +from .stage import Stage class AbcTransformation(Stage): diff --git a/src/gem_controllers/stages/base_controllers/__init__.py b/src/gem_controllers/stages/base_controllers/__init__.py index b100bcbf..9d999fce 100644 --- a/src/gem_controllers/stages/base_controllers/__init__.py +++ b/src/gem_controllers/stages/base_controllers/__init__.py @@ -1,7 +1,7 @@ from .base_controller import BaseController -from .pid_controller import PIDController -from .pi_controller import PIController -from .p_controller import PController from .i_controller import IController +from .p_controller import PController +from .pi_controller import PIController +from .pid_controller import PIDController from .three_point_controller import ThreePointController from .utils import get diff --git a/src/gem_controllers/stages/base_controllers/e_base_controller_task.py b/src/gem_controllers/stages/base_controllers/e_base_controller_task.py index 0b15dce7..a3f38cee 100644 --- a/src/gem_controllers/stages/base_controllers/e_base_controller_task.py +++ b/src/gem_controllers/stages/base_controllers/e_base_controller_task.py @@ -13,14 +13,14 @@ class EBaseControllerTask(Enum): @staticmethod def equals(value, base_controller_task): - if type(value) is str: + if isinstance(value, str): return EBaseControllerTask(value) == base_controller_task else: return value == base_controller_task @staticmethod def get_control_task(value): - if type(value) is str: + if isinstance(value, str): return EBaseControllerTask(value) elif isinstance(value, EBaseControllerTask): return value diff --git a/src/gem_controllers/stages/base_controllers/p_controller.py b/src/gem_controllers/stages/base_controllers/p_controller.py index b95a8b85..57d1f2b2 100644 --- a/src/gem_controllers/stages/base_controllers/p_controller.py +++ b/src/gem_controllers/stages/base_controllers/p_controller.py @@ -1,9 +1,10 @@ import numpy as np -from .base_controller import BaseController, EBaseControllerTask -from ... import parameter_reader as reader import gem_controllers as gc +from ... import parameter_reader as reader +from .base_controller import BaseController, EBaseControllerTask + class PController(BaseController): """This class represents an proportional controller, which can be combined e.g. with a integration controller to a diff --git a/src/gem_controllers/stages/base_controllers/pi_controller.py b/src/gem_controllers/stages/base_controllers/pi_controller.py index 4d77a1b3..948f3a8f 100644 --- a/src/gem_controllers/stages/base_controllers/pi_controller.py +++ b/src/gem_controllers/stages/base_controllers/pi_controller.py @@ -1,9 +1,11 @@ -from .p_controller import PController -from .i_controller import IController +import numpy as np + +import gem_controllers as gc + from ... import parameter_reader as reader from .e_base_controller_task import EBaseControllerTask -import gem_controllers as gc -import numpy as np +from .i_controller import IController +from .p_controller import PController class PIController(PController, IController): @@ -53,7 +55,7 @@ def tune(self, env, env_id, a=4, t_n=None): elif self._control_task == EBaseControllerTask.FC: self._tune_flux_controller(env, env_id, a, t_n) else: - raise Exception(f"No tuning method available.") + raise Exception("No tuning method available.") def _tune_current_controller(self, env, env_id, a=4): """ diff --git a/src/gem_controllers/stages/base_controllers/three_point_controller.py b/src/gem_controllers/stages/base_controllers/three_point_controller.py index db01cde3..e20f77d8 100644 --- a/src/gem_controllers/stages/base_controllers/three_point_controller.py +++ b/src/gem_controllers/stages/base_controllers/three_point_controller.py @@ -1,9 +1,10 @@ import numpy as np +import gem_controllers as gc + +from ... import parameter_reader as reader from .base_controller import BaseController from .e_base_controller_task import EBaseControllerTask -from ... import parameter_reader as reader -import gem_controllers as gc class ThreePointController(BaseController): diff --git a/src/gem_controllers/stages/clipping_stages/__init__.py b/src/gem_controllers/stages/clipping_stages/__init__.py index 336cfd86..aab4abaf 100644 --- a/src/gem_controllers/stages/clipping_stages/__init__.py +++ b/src/gem_controllers/stages/clipping_stages/__init__.py @@ -1,4 +1,4 @@ -from .clipping_stage import ClippingStage from .absolute_clipping_stage import AbsoluteClippingStage -from .squared_clipping_stage import SquaredClippingStage +from .clipping_stage import ClippingStage from .combined_clipping_stage import CombinedClippingStage +from .squared_clipping_stage import SquaredClippingStage diff --git a/src/gem_controllers/stages/clipping_stages/absolute_clipping_stage.py b/src/gem_controllers/stages/clipping_stages/absolute_clipping_stage.py index 97e5ad1a..5621148d 100644 --- a/src/gem_controllers/stages/clipping_stages/absolute_clipping_stage.py +++ b/src/gem_controllers/stages/clipping_stages/absolute_clipping_stage.py @@ -1,8 +1,10 @@ -import numpy as np from typing import Tuple +import numpy as np + import gem_controllers as gc -from . import ClippingStage + +from .clipping_stage import ClippingStage class AbsoluteClippingStage(ClippingStage): diff --git a/src/gem_controllers/stages/clipping_stages/clipping_stage.py b/src/gem_controllers/stages/clipping_stages/clipping_stage.py index da1ab201..df688a66 100644 --- a/src/gem_controllers/stages/clipping_stages/clipping_stage.py +++ b/src/gem_controllers/stages/clipping_stages/clipping_stage.py @@ -1,6 +1,7 @@ -import gym_electric_motor.core import numpy as np +import gym_electric_motor.core + from ..stage import Stage diff --git a/src/gem_controllers/stages/clipping_stages/combined_clipping_stage.py b/src/gem_controllers/stages/clipping_stages/combined_clipping_stage.py index 573a687e..8396bc33 100644 --- a/src/gem_controllers/stages/clipping_stages/combined_clipping_stage.py +++ b/src/gem_controllers/stages/clipping_stages/combined_clipping_stage.py @@ -1,7 +1,9 @@ -import numpy as np from typing import Tuple +import numpy as np + import gem_controllers as gc + from . import ClippingStage diff --git a/src/gem_controllers/stages/clipping_stages/squared_clipping_stage.py b/src/gem_controllers/stages/clipping_stages/squared_clipping_stage.py index dcfa73af..d7fede1a 100644 --- a/src/gem_controllers/stages/clipping_stages/squared_clipping_stage.py +++ b/src/gem_controllers/stages/clipping_stages/squared_clipping_stage.py @@ -1,6 +1,7 @@ import numpy as np import gem_controllers as gc + from .clipping_stage import ClippingStage diff --git a/src/gem_controllers/stages/cont_output_stage.py b/src/gem_controllers/stages/cont_output_stage.py index cca096fe..e78585e0 100644 --- a/src/gem_controllers/stages/cont_output_stage.py +++ b/src/gem_controllers/stages/cont_output_stage.py @@ -1,9 +1,10 @@ import numpy as np -from .stage import Stage -from .. import parameter_reader as reader import gem_controllers as gc +from .. import parameter_reader as reader +from .stage import Stage + class ContOutputStage(Stage): """This class normalizes continuous input voltages to the volatge limits.""" diff --git a/src/gem_controllers/stages/disc_output_stage.py b/src/gem_controllers/stages/disc_output_stage.py index 8deafde1..cd5ba11f 100644 --- a/src/gem_controllers/stages/disc_output_stage.py +++ b/src/gem_controllers/stages/disc_output_stage.py @@ -1,11 +1,12 @@ -import numpy as np import gymnasium +import numpy as np + +import gem_controllers as gc from gym_electric_motor.physical_systems import converters as cv -from .stage import Stage -from ..utils import non_parameterized from .. import parameter_reader as reader -import gem_controllers as gc +from ..utils import non_parameterized +from .stage import Stage class DiscOutputStage(Stage): diff --git a/src/gem_controllers/stages/emf_feedforward.py b/src/gem_controllers/stages/emf_feedforward.py index 2f1b1bba..7d99d033 100644 --- a/src/gem_controllers/stages/emf_feedforward.py +++ b/src/gem_controllers/stages/emf_feedforward.py @@ -1,9 +1,10 @@ import numpy as np -from .stage import Stage -from .. import parameter_reader as reader import gem_controllers as gc +from .. import parameter_reader as reader +from .stage import Stage + class EMFFeedforward(Stage): """This class calculates the emf feedforward, to decouple the actions.""" diff --git a/src/gem_controllers/stages/emf_feedforward_eesm.py b/src/gem_controllers/stages/emf_feedforward_eesm.py index 06802707..83f1013f 100644 --- a/src/gem_controllers/stages/emf_feedforward_eesm.py +++ b/src/gem_controllers/stages/emf_feedforward_eesm.py @@ -1,7 +1,7 @@ -from .emf_feedforward import EMFFeedforward - import numpy as np +from .emf_feedforward import EMFFeedforward + class EMFFeedforwardEESM(EMFFeedforward): """ diff --git a/src/gem_controllers/stages/emf_feedforward_ind.py b/src/gem_controllers/stages/emf_feedforward_ind.py index 7267ba8f..6b2c64b0 100644 --- a/src/gem_controllers/stages/emf_feedforward_ind.py +++ b/src/gem_controllers/stages/emf_feedforward_ind.py @@ -1,6 +1,7 @@ -from .emf_feedforward import EMFFeedforward import numpy as np +from .emf_feedforward import EMFFeedforward + class EMFFeedforwardInd(EMFFeedforward): """ diff --git a/src/gem_controllers/stages/operation_point_selection/__init__.py b/src/gem_controllers/stages/operation_point_selection/__init__.py index 5fdc0fe9..b53cfdf1 100644 --- a/src/gem_controllers/stages/operation_point_selection/__init__.py +++ b/src/gem_controllers/stages/operation_point_selection/__init__.py @@ -1,9 +1,9 @@ +from .eesm_ops import EESMOperationPointSelection +from .extex_dc_ttc import ExtExDcOperationPointSelection +from .operation_point_selection import OperationPointSelection from .ops_utils import torque_to_current_function -from .shunt_dc_ops import ShuntDcOperationPointSelection -from .series_dc_ops import SeriesDcOperationPointSelection from .permex_dc_ops import PermExDcOperationPointSelection -from .operation_point_selection import OperationPointSelection -from .extex_dc_ttc import ExtExDcOperationPointSelection from .pmsm_ops import PMSMOperationPointSelection from .scim_ops import SCIMOperationPointSelection -from .eesm_ops import EESMOperationPointSelection +from .series_dc_ops import SeriesDcOperationPointSelection +from .shunt_dc_ops import ShuntDcOperationPointSelection diff --git a/src/gem_controllers/stages/operation_point_selection/eesm_ops.py b/src/gem_controllers/stages/operation_point_selection/eesm_ops.py index 51e6e62a..3e84c95c 100644 --- a/src/gem_controllers/stages/operation_point_selection/eesm_ops.py +++ b/src/gem_controllers/stages/operation_point_selection/eesm_ops.py @@ -1,5 +1,6 @@ import numpy as np import scipy.interpolate as sp_interpolate + from .foc_operation_point_selection import FieldOrientedControllerOperationPointSelection diff --git a/src/gem_controllers/stages/operation_point_selection/extex_dc_ttc.py b/src/gem_controllers/stages/operation_point_selection/extex_dc_ttc.py index f14fd53b..6c83a466 100644 --- a/src/gem_controllers/stages/operation_point_selection/extex_dc_ttc.py +++ b/src/gem_controllers/stages/operation_point_selection/extex_dc_ttc.py @@ -1,9 +1,10 @@ import numpy as np -from .operation_point_selection import OperationPointSelection -from ... import parameter_reader as reader import gem_controllers as gc +from ... import parameter_reader as reader +from .operation_point_selection import OperationPointSelection + class ExtExDcOperationPointSelection(OperationPointSelection): """This class comutes the current operation point of a ExtExDc Motor for a given torque reference value.""" diff --git a/src/gem_controllers/stages/operation_point_selection/foc_operation_point_selection.py b/src/gem_controllers/stages/operation_point_selection/foc_operation_point_selection.py index 5743be25..ca605cf3 100644 --- a/src/gem_controllers/stages/operation_point_selection/foc_operation_point_selection.py +++ b/src/gem_controllers/stages/operation_point_selection/foc_operation_point_selection.py @@ -1,6 +1,7 @@ -from .operation_point_selection import OperationPointSelection import numpy as np +from .operation_point_selection import OperationPointSelection + class FieldOrientedControllerOperationPointSelection(OperationPointSelection): """ diff --git a/src/gem_controllers/stages/operation_point_selection/ops_utils.py b/src/gem_controllers/stages/operation_point_selection/ops_utils.py index 8ccb23f5..68c537b2 100644 --- a/src/gem_controllers/stages/operation_point_selection/ops_utils.py +++ b/src/gem_controllers/stages/operation_point_selection/ops_utils.py @@ -1,10 +1,10 @@ -from .permex_dc_ops import PermExDcOperationPointSelection +from .eesm_ops import EESMOperationPointSelection from .extex_dc_ttc import ExtExDcOperationPointSelection -from .series_dc_ops import SeriesDcOperationPointSelection -from .shunt_dc_ops import ShuntDcOperationPointSelection +from .permex_dc_ops import PermExDcOperationPointSelection from .pmsm_ops import PMSMOperationPointSelection from .scim_ops import SCIMOperationPointSelection -from .eesm_ops import EESMOperationPointSelection +from .series_dc_ops import SeriesDcOperationPointSelection +from .shunt_dc_ops import ShuntDcOperationPointSelection torque_to_current_function = { "PermExDc": PermExDcOperationPointSelection, diff --git a/src/gem_controllers/stages/operation_point_selection/permex_dc_ops.py b/src/gem_controllers/stages/operation_point_selection/permex_dc_ops.py index 7870453e..aaafc52d 100644 --- a/src/gem_controllers/stages/operation_point_selection/permex_dc_ops.py +++ b/src/gem_controllers/stages/operation_point_selection/permex_dc_ops.py @@ -1,8 +1,9 @@ import numpy as np import gem_controllers as gc -from .operation_point_selection import OperationPointSelection + from ... import parameter_reader as reader +from .operation_point_selection import OperationPointSelection class PermExDcOperationPointSelection(OperationPointSelection): diff --git a/src/gem_controllers/stages/operation_point_selection/pmsm_ops.py b/src/gem_controllers/stages/operation_point_selection/pmsm_ops.py index ff1ca1ea..6a828b11 100644 --- a/src/gem_controllers/stages/operation_point_selection/pmsm_ops.py +++ b/src/gem_controllers/stages/operation_point_selection/pmsm_ops.py @@ -1,7 +1,8 @@ -import gym_electric_motor as gem import numpy as np import scipy.interpolate as sp_interpolate +import gym_electric_motor as gem + from .foc_operation_point_selection import FieldOrientedControllerOperationPointSelection diff --git a/src/gem_controllers/stages/operation_point_selection/scim_ops.py b/src/gem_controllers/stages/operation_point_selection/scim_ops.py index d2cc5142..16d27073 100644 --- a/src/gem_controllers/stages/operation_point_selection/scim_ops.py +++ b/src/gem_controllers/stages/operation_point_selection/scim_ops.py @@ -1,7 +1,9 @@ -import gym_electric_motor as gem import numpy as np -from .foc_operation_point_selection import FieldOrientedControllerOperationPointSelection + +import gym_electric_motor as gem + from ..base_controllers import PIController +from .foc_operation_point_selection import FieldOrientedControllerOperationPointSelection class SCIMOperationPointSelection(FieldOrientedControllerOperationPointSelection): diff --git a/src/gem_controllers/stages/operation_point_selection/series_dc_ops.py b/src/gem_controllers/stages/operation_point_selection/series_dc_ops.py index 8b5cbea1..bb5e06e1 100644 --- a/src/gem_controllers/stages/operation_point_selection/series_dc_ops.py +++ b/src/gem_controllers/stages/operation_point_selection/series_dc_ops.py @@ -1,8 +1,9 @@ import numpy as np import gem_controllers as gc -from .operation_point_selection import OperationPointSelection + from ... import parameter_reader as reader +from .operation_point_selection import OperationPointSelection class SeriesDcOperationPointSelection(OperationPointSelection): diff --git a/src/gem_controllers/stages/operation_point_selection/shunt_dc_ops.py b/src/gem_controllers/stages/operation_point_selection/shunt_dc_ops.py index d4c6833d..32514181 100644 --- a/src/gem_controllers/stages/operation_point_selection/shunt_dc_ops.py +++ b/src/gem_controllers/stages/operation_point_selection/shunt_dc_ops.py @@ -1,7 +1,7 @@ import numpy as np -from .operation_point_selection import OperationPointSelection from ... import parameter_reader as reader +from .operation_point_selection import OperationPointSelection class ShuntDcOperationPointSelection(OperationPointSelection): diff --git a/src/gem_controllers/torque_controller.py b/src/gem_controllers/torque_controller.py index 34910a40..c063cc8b 100644 --- a/src/gem_controllers/torque_controller.py +++ b/src/gem_controllers/torque_controller.py @@ -1,7 +1,7 @@ import numpy as np -import gym_electric_motor as gem import gem_controllers as gc +import gym_electric_motor as gem class TorqueController(gc.GemController): diff --git a/src/gem_controllers/utils.py b/src/gem_controllers/utils.py index fabf1edc..5d982709 100644 --- a/src/gem_controllers/utils.py +++ b/src/gem_controllers/utils.py @@ -21,8 +21,8 @@ def disc_converter_actions(converter): """ if type(converter) is cv.FiniteMultiConverter: high_actions = [] - idle_actions = [] - low_actions = [] + # idle_actions = [] + # low_actions = [] for subconverter in converter.subconverters: high_actions.append(_converter_actions[subconverter]) diff --git a/src/gym_electric_motor/__init__.py b/src/gym_electric_motor/__init__.py index 1b7e0e9f..78d805db 100644 --- a/src/gym_electric_motor/__init__.py +++ b/src/gym_electric_motor/__init__.py @@ -1,11 +1,26 @@ -from .core import ReferenceGenerator -from .core import PhysicalSystem -from .core import RewardFunction -from .core import ElectricMotorVisualization -from .core import ConstraintMonitor -from .core import SimulationEnvironment -from .random_component import RandomComponent +import gymnasium +from gymnasium.envs.registration import register +from packaging import version + +import gym_electric_motor.core +import gym_electric_motor.envs +import gym_electric_motor.physical_system_wrappers +import gym_electric_motor.physical_systems +import gym_electric_motor.reference_generators +import gym_electric_motor.reward_functions +import gym_electric_motor.visualization + from .constraints import Constraint, LimitConstraint +from .core import ( + ConstraintMonitor, + ElectricMotorEnvironment, + ElectricMotorVisualization, + PhysicalSystem, + ReferenceGenerator, + RewardFunction, + SimulationEnvironment, +) +from .random_component import RandomComponent from .utils import register_superclass register_superclass(RewardFunction) @@ -13,19 +28,9 @@ register_superclass(ReferenceGenerator) register_superclass(PhysicalSystem) -import gym_electric_motor.reference_generators -import gym_electric_motor.reward_functions -import gym_electric_motor.visualization -import gym_electric_motor.physical_systems -import gym_electric_motor.envs -import gym_electric_motor.physical_system_wrappers -import gym_electric_motor.core -make = core.ElectricMotorEnvironment.make +make = ElectricMotorEnvironment.make -from gymnasium.envs.registration import register -import gymnasium -from packaging import version # Add all superclasses of the modules to the registry. diff --git a/src/gym_electric_motor/callbacks.py b/src/gym_electric_motor/callbacks.py index b8ccf7ee..71b82d6d 100644 --- a/src/gym_electric_motor/callbacks.py +++ b/src/gym_electric_motor/callbacks.py @@ -1,11 +1,12 @@ """This module introduces predefined callbacks for the GEM environment.""" -from .core import Callback from gym_electric_motor.reference_generators import ( SubepisodedReferenceGenerator, SwitchedReferenceGenerator, ) +from .core import Callback + class RampingLimitMargin(Callback): """Callback used to adapt the limit margin of a reference generator during runtime. diff --git a/src/gym_electric_motor/core.py b/src/gym_electric_motor/core.py index 4a513531..fe01f5f8 100644 --- a/src/gym_electric_motor/core.py +++ b/src/gym_electric_motor/core.py @@ -17,21 +17,21 @@ - Visualization of the PhysicalSystems state, reference and reward for the user. """ -import os import datetime +import os +from dataclasses import dataclass import gymnasium +import matplotlib +import matplotlib.pyplot import numpy as np from gymnasium.spaces import Box +from matplotlib.figure import Figure -from .utils import instantiate -from .random_component import RandomComponent -from .constraints import Constraint, LimitConstraint import gym_electric_motor as gem -import matplotlib.pyplot -from matplotlib.figure import Figure -import matplotlib -from dataclasses import dataclass + +from .constraints import Constraint, LimitConstraint +from .utils import instantiate @dataclass @@ -234,7 +234,7 @@ def __init__( self._physical_system = instantiate(PhysicalSystem, physical_system, **kwargs) self._reference_generator = instantiate(ReferenceGenerator, reference_generator, **kwargs) self._reward_function = instantiate(RewardFunction, reward_function, **kwargs) - if type(visualization) is str or isinstance(visualization, ElectricMotorVisualization): + if isinstance(visualization, str) or isinstance(visualization, ElectricMotorVisualization): visualization = [visualization] if visualization is None: visualization = [] @@ -243,7 +243,7 @@ def __init__( if isinstance(constraints, ConstraintMonitor): cm = constraints else: - limit_constraints = [constraint for constraint in constraints if type(constraint) is str] + limit_constraints = [constraint for constraint in constraints if isinstance(constraint, str)] additional_constraints = [constraint for constraint in constraints if isinstance(constraint, Constraint)] cm = ConstraintMonitor(limit_constraints, additional_constraints) self._constraint_monitor = cm diff --git a/src/gym_electric_motor/envs/__init__.py b/src/gym_electric_motor/envs/__init__.py index be817f3b..475000ff 100644 --- a/src/gym_electric_motor/envs/__init__.py +++ b/src/gym_electric_motor/envs/__init__.py @@ -1,44 +1,68 @@ -from .motors import Motor, MotorType, ControlType, ActionType +from .gym_dcm.extex_dc_motor_env import ( + ContCurrentControlDcExternallyExcitedMotorEnv, + ContSpeedControlDcExternallyExcitedMotorEnv, + ContTorqueControlDcExternallyExcitedMotorEnv, + FiniteCurrentControlDcExternallyExcitedMotorEnv, + FiniteSpeedControlDcExternallyExcitedMotorEnv, + FiniteTorqueControlDcExternallyExcitedMotorEnv, +) # Version 1 -from .gym_dcm.permex_dc_motor_env import ContSpeedControlDcPermanentlyExcitedMotorEnv -from .gym_dcm.permex_dc_motor_env import FiniteSpeedControlDcPermanentlyExcitedMotorEnv -from .gym_dcm.permex_dc_motor_env import ContTorqueControlDcPermanentlyExcitedMotorEnv -from .gym_dcm.permex_dc_motor_env import FiniteTorqueControlDcPermanentlyExcitedMotorEnv -from .gym_dcm.permex_dc_motor_env import ContCurrentControlDcPermanentlyExcitedMotorEnv from .gym_dcm.permex_dc_motor_env import ( + ContCurrentControlDcPermanentlyExcitedMotorEnv, + ContSpeedControlDcPermanentlyExcitedMotorEnv, + ContTorqueControlDcPermanentlyExcitedMotorEnv, FiniteCurrentControlDcPermanentlyExcitedMotorEnv, + FiniteSpeedControlDcPermanentlyExcitedMotorEnv, + FiniteTorqueControlDcPermanentlyExcitedMotorEnv, +) +from .gym_dcm.series_dc_motor_env import ( + ContCurrentControlDcSeriesMotorEnv, + ContSpeedControlDcSeriesMotorEnv, + ContTorqueControlDcSeriesMotorEnv, + FiniteCurrentControlDcSeriesMotorEnv, + FiniteSpeedControlDcSeriesMotorEnv, + FiniteTorqueControlDcSeriesMotorEnv, +) +from .gym_dcm.shunt_dc_motor_env import ( + ContCurrentControlDcShuntMotorEnv, + ContSpeedControlDcShuntMotorEnv, + ContTorqueControlDcShuntMotorEnv, + FiniteCurrentControlDcShuntMotorEnv, + FiniteSpeedControlDcShuntMotorEnv, + FiniteTorqueControlDcShuntMotorEnv, ) - -from .gym_dcm.extex_dc_motor_env import ContSpeedControlDcExternallyExcitedMotorEnv -from .gym_dcm.extex_dc_motor_env import FiniteSpeedControlDcExternallyExcitedMotorEnv -from .gym_dcm.extex_dc_motor_env import ContTorqueControlDcExternallyExcitedMotorEnv -from .gym_dcm.extex_dc_motor_env import FiniteTorqueControlDcExternallyExcitedMotorEnv -from .gym_dcm.extex_dc_motor_env import ContCurrentControlDcExternallyExcitedMotorEnv -from .gym_dcm.extex_dc_motor_env import FiniteCurrentControlDcExternallyExcitedMotorEnv - -from .gym_dcm.series_dc_motor_env import ContSpeedControlDcSeriesMotorEnv -from .gym_dcm.series_dc_motor_env import FiniteSpeedControlDcSeriesMotorEnv -from .gym_dcm.series_dc_motor_env import ContTorqueControlDcSeriesMotorEnv -from .gym_dcm.series_dc_motor_env import FiniteTorqueControlDcSeriesMotorEnv -from .gym_dcm.series_dc_motor_env import ContCurrentControlDcSeriesMotorEnv -from .gym_dcm.series_dc_motor_env import FiniteCurrentControlDcSeriesMotorEnv - -from .gym_dcm.shunt_dc_motor_env import ContSpeedControlDcShuntMotorEnv -from .gym_dcm.shunt_dc_motor_env import FiniteSpeedControlDcShuntMotorEnv -from .gym_dcm.shunt_dc_motor_env import ContTorqueControlDcShuntMotorEnv -from .gym_dcm.shunt_dc_motor_env import FiniteTorqueControlDcShuntMotorEnv -from .gym_dcm.shunt_dc_motor_env import ContCurrentControlDcShuntMotorEnv -from .gym_dcm.shunt_dc_motor_env import FiniteCurrentControlDcShuntMotorEnv - -from .gym_pmsm.finite_sc_pmsm_env import ( - FiniteSpeedControlPermanentMagnetSynchronousMotorEnv, +from .gym_eesm.cont_cc_eesm_env import ( + ContCurrentControlExternallyExcitedSynchronousMotorEnv, ) -from .gym_pmsm.finite_cc_pmsm_env import ( - FiniteCurrentControlPermanentMagnetSynchronousMotorEnv, +from .gym_eesm.cont_sc_eesm_env import ( + ContSpeedControlExternallyExcitedSynchronousMotorEnv, ) -from .gym_pmsm.finite_tc_pmsm_env import ( - FiniteTorqueControlPermanentMagnetSynchronousMotorEnv, +from .gym_eesm.cont_tc_eesm_env import ( + ContTorqueControlExternallyExcitedSynchronousMotorEnv, +) +from .gym_eesm.finite_cc_eesm_env import ( + FiniteCurrentControlExternallyExcitedSynchronousMotorEnv, +) +from .gym_eesm.finite_sc_eesm_env import ( + FiniteSpeedControlExternallyExcitedSynchronousMotorEnv, +) +from .gym_eesm.finite_tc_eesm_env import ( + FiniteTorqueControlExternallyExcitedSynchronousMotorEnv, +) +from .gym_im import ( + ContCurrentControlDoublyFedInductionMotorEnv, + ContCurrentControlSquirrelCageInductionMotorEnv, + ContSpeedControlDoublyFedInductionMotorEnv, + ContSpeedControlSquirrelCageInductionMotorEnv, + ContTorqueControlDoublyFedInductionMotorEnv, + ContTorqueControlSquirrelCageInductionMotorEnv, + FiniteCurrentControlDoublyFedInductionMotorEnv, + FiniteCurrentControlSquirrelCageInductionMotorEnv, + FiniteSpeedControlDoublyFedInductionMotorEnv, + FiniteSpeedControlSquirrelCageInductionMotorEnv, + FiniteTorqueControlDoublyFedInductionMotorEnv, + FiniteTorqueControlSquirrelCageInductionMotorEnv, ) from .gym_pmsm.cont_cc_pmsm_env import ( ContCurrentControlPermanentMagnetSynchronousMotorEnv, @@ -49,49 +73,25 @@ from .gym_pmsm.cont_tc_pmsm_env import ( ContTorqueControlPermanentMagnetSynchronousMotorEnv, ) - -from .gym_eesm.finite_sc_eesm_env import ( - FiniteSpeedControlExternallyExcitedSynchronousMotorEnv, -) -from .gym_eesm.finite_cc_eesm_env import ( - FiniteCurrentControlExternallyExcitedSynchronousMotorEnv, -) -from .gym_eesm.finite_tc_eesm_env import ( - FiniteTorqueControlExternallyExcitedSynchronousMotorEnv, +from .gym_pmsm.finite_cc_pmsm_env import ( + FiniteCurrentControlPermanentMagnetSynchronousMotorEnv, ) -from .gym_eesm.cont_cc_eesm_env import ( - ContCurrentControlExternallyExcitedSynchronousMotorEnv, +from .gym_pmsm.finite_sc_pmsm_env import ( + FiniteSpeedControlPermanentMagnetSynchronousMotorEnv, ) -from .gym_eesm.cont_sc_eesm_env import ( - ContSpeedControlExternallyExcitedSynchronousMotorEnv, +from .gym_pmsm.finite_tc_pmsm_env import ( + FiniteTorqueControlPermanentMagnetSynchronousMotorEnv, ) -from .gym_eesm.cont_tc_eesm_env import ( - ContTorqueControlExternallyExcitedSynchronousMotorEnv, +from .gym_synrm.cont_cc_synrm_env import ContCurrentControlSynchronousReluctanceMotorEnv +from .gym_synrm.cont_sc_synrm_env import ContSpeedControlSynchronousReluctanceMotorEnv +from .gym_synrm.cont_tc_synrm_env import ContTorqueControlSynchronousReluctanceMotorEnv +from .gym_synrm.finite_cc_synrm_env import ( + FiniteCurrentControlSynchronousReluctanceMotorEnv, ) - from .gym_synrm.finite_sc_synrm_env import ( FiniteSpeedControlSynchronousReluctanceMotorEnv, ) -from .gym_synrm.finite_cc_synrm_env import ( - FiniteCurrentControlSynchronousReluctanceMotorEnv, -) from .gym_synrm.finite_tc_synrm_env import ( FiniteTorqueControlSynchronousReluctanceMotorEnv, ) -from .gym_synrm.cont_tc_synrm_env import ContTorqueControlSynchronousReluctanceMotorEnv -from .gym_synrm.cont_cc_synrm_env import ContCurrentControlSynchronousReluctanceMotorEnv -from .gym_synrm.cont_sc_synrm_env import ContSpeedControlSynchronousReluctanceMotorEnv - -from .gym_im import ContSpeedControlSquirrelCageInductionMotorEnv -from .gym_im import ContCurrentControlSquirrelCageInductionMotorEnv -from .gym_im import ContTorqueControlSquirrelCageInductionMotorEnv -from .gym_im import FiniteSpeedControlSquirrelCageInductionMotorEnv -from .gym_im import FiniteCurrentControlSquirrelCageInductionMotorEnv -from .gym_im import FiniteTorqueControlSquirrelCageInductionMotorEnv - -from .gym_im import ContSpeedControlDoublyFedInductionMotorEnv -from .gym_im import ContCurrentControlDoublyFedInductionMotorEnv -from .gym_im import ContTorqueControlDoublyFedInductionMotorEnv -from .gym_im import FiniteSpeedControlDoublyFedInductionMotorEnv -from .gym_im import FiniteCurrentControlDoublyFedInductionMotorEnv -from .gym_im import FiniteTorqueControlDoublyFedInductionMotorEnv +from .motors import ActionType, ControlType, Motor, MotorType diff --git a/src/gym_electric_motor/envs/gym_dcm/__init__.py b/src/gym_electric_motor/envs/gym_dcm/__init__.py index 702bb59b..5ddc663a 100644 --- a/src/gym_electric_motor/envs/gym_dcm/__init__.py +++ b/src/gym_electric_motor/envs/gym_dcm/__init__.py @@ -1,27 +1,32 @@ -from .permex_dc_motor_env import ContSpeedControlDcPermanentlyExcitedMotorEnv -from .permex_dc_motor_env import FiniteSpeedControlDcPermanentlyExcitedMotorEnv -from .permex_dc_motor_env import ContTorqueControlDcPermanentlyExcitedMotorEnv -from .permex_dc_motor_env import FiniteTorqueControlDcPermanentlyExcitedMotorEnv -from .permex_dc_motor_env import ContCurrentControlDcPermanentlyExcitedMotorEnv -from .permex_dc_motor_env import FiniteCurrentControlDcPermanentlyExcitedMotorEnv - -from .extex_dc_motor_env import ContSpeedControlDcExternallyExcitedMotorEnv -from .extex_dc_motor_env import FiniteSpeedControlDcExternallyExcitedMotorEnv -from .extex_dc_motor_env import ContTorqueControlDcExternallyExcitedMotorEnv -from .extex_dc_motor_env import FiniteTorqueControlDcExternallyExcitedMotorEnv -from .extex_dc_motor_env import ContCurrentControlDcExternallyExcitedMotorEnv -from .extex_dc_motor_env import FiniteCurrentControlDcExternallyExcitedMotorEnv - -from .series_dc_motor_env import ContSpeedControlDcSeriesMotorEnv -from .series_dc_motor_env import FiniteSpeedControlDcSeriesMotorEnv -from .series_dc_motor_env import ContTorqueControlDcSeriesMotorEnv -from .series_dc_motor_env import FiniteTorqueControlDcSeriesMotorEnv -from .series_dc_motor_env import ContCurrentControlDcSeriesMotorEnv -from .series_dc_motor_env import FiniteCurrentControlDcSeriesMotorEnv - -from .shunt_dc_motor_env import ContSpeedControlDcShuntMotorEnv -from .shunt_dc_motor_env import FiniteSpeedControlDcShuntMotorEnv -from .shunt_dc_motor_env import ContTorqueControlDcShuntMotorEnv -from .shunt_dc_motor_env import FiniteTorqueControlDcShuntMotorEnv -from .shunt_dc_motor_env import ContCurrentControlDcShuntMotorEnv -from .shunt_dc_motor_env import FiniteCurrentControlDcShuntMotorEnv +from .extex_dc_motor_env import ( + ContCurrentControlDcExternallyExcitedMotorEnv, + ContSpeedControlDcExternallyExcitedMotorEnv, + ContTorqueControlDcExternallyExcitedMotorEnv, + FiniteCurrentControlDcExternallyExcitedMotorEnv, + FiniteSpeedControlDcExternallyExcitedMotorEnv, + FiniteTorqueControlDcExternallyExcitedMotorEnv, +) +from .permex_dc_motor_env import ( + ContCurrentControlDcPermanentlyExcitedMotorEnv, + ContSpeedControlDcPermanentlyExcitedMotorEnv, + ContTorqueControlDcPermanentlyExcitedMotorEnv, + FiniteCurrentControlDcPermanentlyExcitedMotorEnv, + FiniteSpeedControlDcPermanentlyExcitedMotorEnv, + FiniteTorqueControlDcPermanentlyExcitedMotorEnv, +) +from .series_dc_motor_env import ( + ContCurrentControlDcSeriesMotorEnv, + ContSpeedControlDcSeriesMotorEnv, + ContTorqueControlDcSeriesMotorEnv, + FiniteCurrentControlDcSeriesMotorEnv, + FiniteSpeedControlDcSeriesMotorEnv, + FiniteTorqueControlDcSeriesMotorEnv, +) +from .shunt_dc_motor_env import ( + ContCurrentControlDcShuntMotorEnv, + ContSpeedControlDcShuntMotorEnv, + ContTorqueControlDcShuntMotorEnv, + FiniteCurrentControlDcShuntMotorEnv, + FiniteSpeedControlDcShuntMotorEnv, + FiniteTorqueControlDcShuntMotorEnv, +) diff --git a/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_cc_extex_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_cc_extex_dc_env.py index 64e6e090..80eadf84 100644 --- a/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_cc_extex_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_cc_extex_dc_env.py @@ -1,18 +1,18 @@ +from gym_electric_motor import physical_systems as ps from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import ( - WienerProcessReferenceGenerator, MultipleReferenceGenerator, + WienerProcessReferenceGenerator, ) -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize +from gym_electric_motor.visualization import MotorDashboard class ContCurrentControlDcExternallyExcitedMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_sc_extex_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_sc_extex_dc_env.py index 0137a143..05d08e90 100644 --- a/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_sc_extex_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_sc_extex_dc_env.py @@ -1,15 +1,15 @@ +from gym_electric_motor import physical_systems as ps from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize +from gym_electric_motor.visualization import MotorDashboard class ContSpeedControlDcExternallyExcitedMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_tc_extex_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_tc_extex_dc_env.py index 3c4d8a54..0e1c03f8 100644 --- a/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_tc_extex_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/cont_tc_extex_dc_env.py @@ -1,15 +1,15 @@ +from gym_electric_motor import physical_systems as ps from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize +from gym_electric_motor.visualization import MotorDashboard class ContTorqueControlDcExternallyExcitedMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_cc_extex_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_cc_extex_dc_env.py index 498618ad..0fcf92fa 100644 --- a/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_cc_extex_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_cc_extex_dc_env.py @@ -1,18 +1,18 @@ +from gym_electric_motor import physical_systems as ps from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import ( - WienerProcessReferenceGenerator, MultipleReferenceGenerator, + WienerProcessReferenceGenerator, ) -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize +from gym_electric_motor.visualization import MotorDashboard class FiniteCurrentControlDcExternallyExcitedMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_sc_extex_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_sc_extex_dc_env.py index 5eb64456..fc0f74dc 100644 --- a/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_sc_extex_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_sc_extex_dc_env.py @@ -1,15 +1,15 @@ +from gym_electric_motor import physical_systems as ps from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize +from gym_electric_motor.visualization import MotorDashboard class FiniteSpeedControlDcExternallyExcitedMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_tc_extex_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_tc_extex_dc_env.py index 06551a5d..12216442 100644 --- a/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_tc_extex_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/extex_dc_motor_env/finite_tc_extex_dc_env.py @@ -1,15 +1,15 @@ +from gym_electric_motor import physical_systems as ps from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize +from gym_electric_motor.visualization import MotorDashboard class FiniteTorqueControlDcExternallyExcitedMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/__init__.py b/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/__init__.py index a4a111ac..11565da8 100644 --- a/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/__init__.py +++ b/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/__init__.py @@ -1,6 +1,6 @@ from .cont_cc_permex_dc_env import ContCurrentControlDcPermanentlyExcitedMotorEnv -from .cont_tc_permex_dc_env import ContTorqueControlDcPermanentlyExcitedMotorEnv from .cont_sc_permex_dc_env import ContSpeedControlDcPermanentlyExcitedMotorEnv -from .finite_tc_permex_dc_env import FiniteTorqueControlDcPermanentlyExcitedMotorEnv -from .finite_sc_permex_dc_env import FiniteSpeedControlDcPermanentlyExcitedMotorEnv +from .cont_tc_permex_dc_env import ContTorqueControlDcPermanentlyExcitedMotorEnv from .finite_cc_permex_dc_env import FiniteCurrentControlDcPermanentlyExcitedMotorEnv +from .finite_sc_permex_dc_env import FiniteSpeedControlDcPermanentlyExcitedMotorEnv +from .finite_tc_permex_dc_env import FiniteTorqueControlDcPermanentlyExcitedMotorEnv diff --git a/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_cc_permex_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_cc_permex_dc_env.py index 1d9cd5c6..50c5406f 100644 --- a/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_cc_permex_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_cc_permex_dc_env.py @@ -1,17 +1,17 @@ +from gym_electric_motor import physical_systems as ps from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators.wiener_process_reference_generator import ( WienerProcessReferenceGenerator, ) -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize +from gym_electric_motor.visualization import MotorDashboard class ContCurrentControlDcPermanentlyExcitedMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_sc_permex_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_sc_permex_dc_env.py index 794740d1..61530b50 100644 --- a/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_sc_permex_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_sc_permex_dc_env.py @@ -1,17 +1,17 @@ +from gym_electric_motor import physical_systems as ps from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators.wiener_process_reference_generator import ( WienerProcessReferenceGenerator, ) -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize +from gym_electric_motor.visualization import MotorDashboard class ContSpeedControlDcPermanentlyExcitedMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_tc_permex_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_tc_permex_dc_env.py index a4e41cf7..d5458f41 100644 --- a/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_tc_permex_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/cont_tc_permex_dc_env.py @@ -1,17 +1,17 @@ +from gym_electric_motor import physical_systems as ps from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators.wiener_process_reference_generator import ( WienerProcessReferenceGenerator, ) -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize +from gym_electric_motor.visualization import MotorDashboard class ContTorqueControlDcPermanentlyExcitedMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_cc_permex_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_cc_permex_dc_env.py index 95963398..edab490b 100644 --- a/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_cc_permex_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_cc_permex_dc_env.py @@ -1,17 +1,17 @@ +from gym_electric_motor import physical_systems as ps from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators.wiener_process_reference_generator import ( WienerProcessReferenceGenerator, ) -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize +from gym_electric_motor.visualization import MotorDashboard class FiniteCurrentControlDcPermanentlyExcitedMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_sc_permex_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_sc_permex_dc_env.py index 7b343798..8d36b87c 100644 --- a/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_sc_permex_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_sc_permex_dc_env.py @@ -1,17 +1,17 @@ +from gym_electric_motor import physical_systems as ps from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators.wiener_process_reference_generator import ( WienerProcessReferenceGenerator, ) -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize +from gym_electric_motor.visualization import MotorDashboard class FiniteSpeedControlDcPermanentlyExcitedMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_tc_permex_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_tc_permex_dc_env.py index 175946cc..760d1809 100644 --- a/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_tc_permex_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/permex_dc_motor_env/finite_tc_permex_dc_env.py @@ -1,17 +1,17 @@ +from gym_electric_motor import physical_systems as ps from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators.wiener_process_reference_generator import ( WienerProcessReferenceGenerator, ) -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize +from gym_electric_motor.visualization import MotorDashboard class FiniteTorqueControlDcPermanentlyExcitedMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_cc_series_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_cc_series_dc_env.py index b0eed92b..f704c3ef 100644 --- a/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_cc_series_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_cc_series_dc_env.py @@ -1,15 +1,15 @@ +from gym_electric_motor import physical_systems as ps from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize +from gym_electric_motor.visualization import MotorDashboard class ContCurrentControlDcSeriesMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_sc_series_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_sc_series_dc_env.py index 09d7d94d..0af911f0 100644 --- a/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_sc_series_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_sc_series_dc_env.py @@ -1,15 +1,15 @@ +from gym_electric_motor import physical_systems as ps from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize +from gym_electric_motor.visualization import MotorDashboard class ContSpeedControlDcSeriesMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_tc_series_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_tc_series_dc_env.py index 600fe924..b9b6ba2f 100644 --- a/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_tc_series_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_tc_series_dc_env.py @@ -1,15 +1,15 @@ +from gym_electric_motor import physical_systems as ps from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize +from gym_electric_motor.visualization import MotorDashboard class ContTorqueControlDcSeriesMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_cc_series_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_cc_series_dc_env.py index b7eb65af..7e1726f7 100644 --- a/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_cc_series_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_cc_series_dc_env.py @@ -1,15 +1,15 @@ +from gym_electric_motor import physical_systems as ps from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize +from gym_electric_motor.visualization import MotorDashboard class FiniteCurrentControlDcSeriesMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_sc_series_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_sc_series_dc_env.py index b6d3ce66..decc60fa 100644 --- a/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_sc_series_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_sc_series_dc_env.py @@ -1,15 +1,15 @@ +from gym_electric_motor import physical_systems as ps from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize +from gym_electric_motor.visualization import MotorDashboard class FiniteSpeedControlDcSeriesMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_tc_series_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_tc_series_dc_env.py index 21657a30..06e67f3d 100644 --- a/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_tc_series_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/finite_tc_series_dc_env.py @@ -1,15 +1,15 @@ +from gym_electric_motor import physical_systems as ps from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize +from gym_electric_motor.visualization import MotorDashboard class FiniteTorqueControlDcSeriesMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_cc_shunt_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_cc_shunt_dc_env.py index fb07bb42..fbc460b2 100644 --- a/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_cc_shunt_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_cc_shunt_dc_env.py @@ -1,16 +1,16 @@ +from gym_electric_motor import physical_systems as ps from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) +from gym_electric_motor.physical_system_wrappers import CurrentSumProcessor from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.physical_system_wrappers import CurrentSumProcessor +from gym_electric_motor.visualization import MotorDashboard class ContCurrentControlDcShuntMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_sc_shunt_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_sc_shunt_dc_env.py index 652cf48e..657a7097 100644 --- a/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_sc_shunt_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_sc_shunt_dc_env.py @@ -1,16 +1,16 @@ +from gym_electric_motor import physical_systems as ps from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) -from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.physical_system_wrappers import CurrentSumProcessor -from gym_electric_motor.visualization import MotorDashboard +from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize +from gym_electric_motor.visualization import MotorDashboard class ContSpeedControlDcShuntMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_tc_shunt_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_tc_shunt_dc_env.py index 740c5676..d254c24c 100644 --- a/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_tc_shunt_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/cont_tc_shunt_dc_env.py @@ -1,16 +1,16 @@ +from gym_electric_motor import physical_systems as ps from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) +from gym_electric_motor.physical_system_wrappers import CurrentSumProcessor from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.physical_system_wrappers import CurrentSumProcessor +from gym_electric_motor.visualization import MotorDashboard class ContTorqueControlDcShuntMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_cc_shunt_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_cc_shunt_dc_env.py index 26035750..88ad5b74 100644 --- a/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_cc_shunt_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_cc_shunt_dc_env.py @@ -1,16 +1,16 @@ +from gym_electric_motor import physical_systems as ps from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) -from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.physical_system_wrappers import CurrentSumProcessor -from gym_electric_motor.visualization import MotorDashboard +from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize +from gym_electric_motor.visualization import MotorDashboard class FiniteCurrentControlDcShuntMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_sc_shunt_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_sc_shunt_dc_env.py index 3ba5d5ff..9a12809d 100644 --- a/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_sc_shunt_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_sc_shunt_dc_env.py @@ -1,16 +1,16 @@ +from gym_electric_motor import physical_systems as ps from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) -from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.physical_system_wrappers import CurrentSumProcessor -from gym_electric_motor.visualization import MotorDashboard +from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize +from gym_electric_motor.visualization import MotorDashboard class FiniteSpeedControlDcShuntMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_tc_shunt_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_tc_shunt_dc_env.py index 97d55b15..46017519 100644 --- a/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_tc_shunt_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/shunt_dc_motor_env/finite_tc_shunt_dc_env.py @@ -1,16 +1,16 @@ +from gym_electric_motor import physical_systems as ps from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) -from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.physical_system_wrappers import CurrentSumProcessor -from gym_electric_motor.visualization import MotorDashboard +from gym_electric_motor.physical_systems.physical_systems import DcMotorSystem from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize +from gym_electric_motor.visualization import MotorDashboard class FiniteTorqueControlDcShuntMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_eesm/__init__.py b/src/gym_electric_motor/envs/gym_eesm/__init__.py index be7ece13..dd88b430 100644 --- a/src/gym_electric_motor/envs/gym_eesm/__init__.py +++ b/src/gym_electric_motor/envs/gym_eesm/__init__.py @@ -1,6 +1,6 @@ +from .cont_cc_eesm_env import ContCurrentControlExternallyExcitedSynchronousMotorEnv from .cont_sc_eesm_env import ContSpeedControlExternallyExcitedSynchronousMotorEnv from .cont_tc_eesm_env import ContTorqueControlExternallyExcitedSynchronousMotorEnv -from .cont_cc_eesm_env import ContCurrentControlExternallyExcitedSynchronousMotorEnv -from .finite_tc_eesm_env import FiniteTorqueControlExternallyExcitedSynchronousMotorEnv -from .finite_sc_eesm_env import FiniteSpeedControlExternallyExcitedSynchronousMotorEnv from .finite_cc_eesm_env import FiniteCurrentControlExternallyExcitedSynchronousMotorEnv +from .finite_sc_eesm_env import FiniteSpeedControlExternallyExcitedSynchronousMotorEnv +from .finite_tc_eesm_env import FiniteTorqueControlExternallyExcitedSynchronousMotorEnv diff --git a/src/gym_electric_motor/envs/gym_eesm/cont_cc_eesm_env.py b/src/gym_electric_motor/envs/gym_eesm/cont_cc_eesm_env.py index bbaf4639..06b842fd 100644 --- a/src/gym_electric_motor/envs/gym_eesm/cont_cc_eesm_env.py +++ b/src/gym_electric_motor/envs/gym_eesm/cont_cc_eesm_env.py @@ -1,21 +1,21 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import LimitConstraint, SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import ( ExternallyExcitedSynchronousMotorSystem, ) -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import ( - WienerProcessReferenceGenerator, MultipleReferenceGenerator, + WienerProcessReferenceGenerator, ) -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import LimitConstraint, SquaredConstraint +from gym_electric_motor.visualization import MotorDashboard class ContCurrentControlExternallyExcitedSynchronousMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_eesm/cont_sc_eesm_env.py b/src/gym_electric_motor/envs/gym_eesm/cont_sc_eesm_env.py index 8bfd1f70..cb01c0a2 100644 --- a/src/gym_electric_motor/envs/gym_eesm/cont_sc_eesm_env.py +++ b/src/gym_electric_motor/envs/gym_eesm/cont_sc_eesm_env.py @@ -1,19 +1,19 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import LimitConstraint, SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import ( ExternallyExcitedSynchronousMotorSystem, SynchronousMotorSystem, ) -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import SquaredConstraint, LimitConstraint +from gym_electric_motor.visualization import MotorDashboard class ContSpeedControlExternallyExcitedSynchronousMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_eesm/cont_tc_eesm_env.py b/src/gym_electric_motor/envs/gym_eesm/cont_tc_eesm_env.py index e76ca7ac..e8f4be5a 100644 --- a/src/gym_electric_motor/envs/gym_eesm/cont_tc_eesm_env.py +++ b/src/gym_electric_motor/envs/gym_eesm/cont_tc_eesm_env.py @@ -1,22 +1,22 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import LimitConstraint, SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import ( ExternallyExcitedSynchronousMotorSystem, SynchronousMotorSystem, ) -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import ( - WienerProcessReferenceGenerator, MultipleReferenceGenerator, + WienerProcessReferenceGenerator, ) -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import LimitConstraint, SquaredConstraint +from gym_electric_motor.visualization import MotorDashboard class ContTorqueControlExternallyExcitedSynchronousMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_eesm/finite_cc_eesm_env.py b/src/gym_electric_motor/envs/gym_eesm/finite_cc_eesm_env.py index 53a40d54..c165d8f5 100644 --- a/src/gym_electric_motor/envs/gym_eesm/finite_cc_eesm_env.py +++ b/src/gym_electric_motor/envs/gym_eesm/finite_cc_eesm_env.py @@ -1,21 +1,21 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import LimitConstraint, SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import ( ExternallyExcitedSynchronousMotorSystem, ) -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import ( - WienerProcessReferenceGenerator, MultipleReferenceGenerator, + WienerProcessReferenceGenerator, ) -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import LimitConstraint, SquaredConstraint +from gym_electric_motor.visualization import MotorDashboard class FiniteCurrentControlExternallyExcitedSynchronousMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_eesm/finite_sc_eesm_env.py b/src/gym_electric_motor/envs/gym_eesm/finite_sc_eesm_env.py index 304f7bf1..a8296e21 100644 --- a/src/gym_electric_motor/envs/gym_eesm/finite_sc_eesm_env.py +++ b/src/gym_electric_motor/envs/gym_eesm/finite_sc_eesm_env.py @@ -1,18 +1,18 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import LimitConstraint, SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import ( ExternallyExcitedSynchronousMotorSystem, ) -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import LimitConstraint, SquaredConstraint +from gym_electric_motor.visualization import MotorDashboard class FiniteSpeedControlExternallyExcitedSynchronousMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_eesm/finite_tc_eesm_env.py b/src/gym_electric_motor/envs/gym_eesm/finite_tc_eesm_env.py index 3c01d334..701dca99 100644 --- a/src/gym_electric_motor/envs/gym_eesm/finite_tc_eesm_env.py +++ b/src/gym_electric_motor/envs/gym_eesm/finite_tc_eesm_env.py @@ -1,18 +1,18 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import LimitConstraint, SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import ( ExternallyExcitedSynchronousMotorSystem, ) -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import LimitConstraint, SquaredConstraint +from gym_electric_motor.visualization import MotorDashboard class FiniteTorqueControlExternallyExcitedSynchronousMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_im/__init__.py b/src/gym_electric_motor/envs/gym_im/__init__.py index c693998e..f4cbdb00 100644 --- a/src/gym_electric_motor/envs/gym_im/__init__.py +++ b/src/gym_electric_motor/envs/gym_im/__init__.py @@ -1,33 +1,16 @@ -from .squirrel_cage_induction_motor_envs import ( - ContCurrentControlSquirrelCageInductionMotorEnv, +from .doubly_fed_induction_motor_envs import ( + ContCurrentControlDoublyFedInductionMotorEnv, + ContSpeedControlDoublyFedInductionMotorEnv, + ContTorqueControlDoublyFedInductionMotorEnv, + FiniteCurrentControlDoublyFedInductionMotorEnv, + FiniteSpeedControlDoublyFedInductionMotorEnv, + FiniteTorqueControlDoublyFedInductionMotorEnv, ) from .squirrel_cage_induction_motor_envs import ( + ContCurrentControlSquirrelCageInductionMotorEnv, ContSpeedControlSquirrelCageInductionMotorEnv, -) -from .squirrel_cage_induction_motor_envs import ( ContTorqueControlSquirrelCageInductionMotorEnv, -) -from .squirrel_cage_induction_motor_envs import ( FiniteCurrentControlSquirrelCageInductionMotorEnv, -) -from .squirrel_cage_induction_motor_envs import ( FiniteSpeedControlSquirrelCageInductionMotorEnv, -) -from .squirrel_cage_induction_motor_envs import ( FiniteTorqueControlSquirrelCageInductionMotorEnv, ) - -from .doubly_fed_induction_motor_envs import ( - ContCurrentControlDoublyFedInductionMotorEnv, -) -from .doubly_fed_induction_motor_envs import ContSpeedControlDoublyFedInductionMotorEnv -from .doubly_fed_induction_motor_envs import ContTorqueControlDoublyFedInductionMotorEnv -from .doubly_fed_induction_motor_envs import ( - FiniteCurrentControlDoublyFedInductionMotorEnv, -) -from .doubly_fed_induction_motor_envs import ( - FiniteSpeedControlDoublyFedInductionMotorEnv, -) -from .doubly_fed_induction_motor_envs import ( - FiniteTorqueControlDoublyFedInductionMotorEnv, -) diff --git a/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/__init__.py b/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/__init__.py index d7ce4715..52e47cde 100644 --- a/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/__init__.py +++ b/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/__init__.py @@ -1,6 +1,6 @@ +from .cont_cc_dfim_env import ContCurrentControlDoublyFedInductionMotorEnv from .cont_sc_dfim_env import ContSpeedControlDoublyFedInductionMotorEnv from .cont_tc_dfim_env import ContTorqueControlDoublyFedInductionMotorEnv -from .cont_cc_dfim_env import ContCurrentControlDoublyFedInductionMotorEnv -from .finite_tc_dfim_env import FiniteTorqueControlDoublyFedInductionMotorEnv -from .finite_sc_dfim_env import FiniteSpeedControlDoublyFedInductionMotorEnv from .finite_cc_dfim_env import FiniteCurrentControlDoublyFedInductionMotorEnv +from .finite_sc_dfim_env import FiniteSpeedControlDoublyFedInductionMotorEnv +from .finite_tc_dfim_env import FiniteTorqueControlDoublyFedInductionMotorEnv diff --git a/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_cc_dfim_env.py b/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_cc_dfim_env.py index 0aa3cd04..0e7785c5 100644 --- a/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_cc_dfim_env.py +++ b/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_cc_dfim_env.py @@ -1,21 +1,21 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import ( DoublyFedInductionMotorSystem, ) -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import ( - WienerProcessReferenceGenerator, MultipleReferenceGenerator, + WienerProcessReferenceGenerator, ) -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import SquaredConstraint +from gym_electric_motor.visualization import MotorDashboard class ContCurrentControlDoublyFedInductionMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_sc_dfim_env.py b/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_sc_dfim_env.py index 49540574..7ec1a13f 100644 --- a/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_sc_dfim_env.py +++ b/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_sc_dfim_env.py @@ -1,18 +1,18 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import ( DoublyFedInductionMotorSystem, ) -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import SquaredConstraint +from gym_electric_motor.visualization import MotorDashboard class ContSpeedControlDoublyFedInductionMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_tc_dfim_env.py b/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_tc_dfim_env.py index ec7ba00a..04586ad6 100644 --- a/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_tc_dfim_env.py +++ b/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/cont_tc_dfim_env.py @@ -1,21 +1,21 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import ( DoublyFedInductionMotorSystem, ) -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import ( - WienerProcessReferenceGenerator, MultipleReferenceGenerator, + WienerProcessReferenceGenerator, ) -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import SquaredConstraint +from gym_electric_motor.visualization import MotorDashboard class ContTorqueControlDoublyFedInductionMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_cc_dfim_env.py b/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_cc_dfim_env.py index 0a23f5ee..c5834ba6 100644 --- a/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_cc_dfim_env.py +++ b/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_cc_dfim_env.py @@ -1,21 +1,21 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import ( DoublyFedInductionMotorSystem, ) -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import ( - WienerProcessReferenceGenerator, MultipleReferenceGenerator, + WienerProcessReferenceGenerator, ) -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import SquaredConstraint +from gym_electric_motor.visualization import MotorDashboard class FiniteCurrentControlDoublyFedInductionMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_sc_dfim_env.py b/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_sc_dfim_env.py index a608f09c..b4d46862 100644 --- a/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_sc_dfim_env.py +++ b/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_sc_dfim_env.py @@ -1,18 +1,18 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import ( DoublyFedInductionMotorSystem, ) -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import SquaredConstraint +from gym_electric_motor.visualization import MotorDashboard class FiniteSpeedControlDoublyFedInductionMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_tc_dfim_env.py b/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_tc_dfim_env.py index 00972705..c4e12416 100644 --- a/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_tc_dfim_env.py +++ b/src/gym_electric_motor/envs/gym_im/doubly_fed_induction_motor_envs/finite_tc_dfim_env.py @@ -1,18 +1,18 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import ( DoublyFedInductionMotorSystem, ) -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import SquaredConstraint +from gym_electric_motor.visualization import MotorDashboard class FiniteTorqueControlDoublyFedInductionMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/__init__.py b/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/__init__.py index f48e17bb..7adcc359 100644 --- a/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/__init__.py +++ b/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/__init__.py @@ -1,6 +1,6 @@ +from .cont_cc_scim_env import ContCurrentControlSquirrelCageInductionMotorEnv from .cont_sc_scim_env import ContSpeedControlSquirrelCageInductionMotorEnv from .cont_tc_scim_env import ContTorqueControlSquirrelCageInductionMotorEnv -from .cont_cc_scim_env import ContCurrentControlSquirrelCageInductionMotorEnv -from .finite_tc_scim_env import FiniteTorqueControlSquirrelCageInductionMotorEnv -from .finite_sc_scim_env import FiniteSpeedControlSquirrelCageInductionMotorEnv from .finite_cc_scim_env import FiniteCurrentControlSquirrelCageInductionMotorEnv +from .finite_sc_scim_env import FiniteSpeedControlSquirrelCageInductionMotorEnv +from .finite_tc_scim_env import FiniteTorqueControlSquirrelCageInductionMotorEnv diff --git a/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_cc_scim_env.py b/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_cc_scim_env.py index 0bed9193..bf7b9521 100644 --- a/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_cc_scim_env.py +++ b/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_cc_scim_env.py @@ -1,21 +1,21 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import ( SquirrelCageInductionMotorSystem, ) -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import ( - WienerProcessReferenceGenerator, MultipleReferenceGenerator, + WienerProcessReferenceGenerator, ) -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import SquaredConstraint +from gym_electric_motor.visualization import MotorDashboard class ContCurrentControlSquirrelCageInductionMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_sc_scim_env.py b/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_sc_scim_env.py index 63289f1f..c0cb3e21 100644 --- a/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_sc_scim_env.py +++ b/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_sc_scim_env.py @@ -1,18 +1,18 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import ( SquirrelCageInductionMotorSystem, ) -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import SquaredConstraint +from gym_electric_motor.visualization import MotorDashboard class ContSpeedControlSquirrelCageInductionMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_tc_scim_env.py b/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_tc_scim_env.py index b8a56439..d4edb811 100644 --- a/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_tc_scim_env.py +++ b/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/cont_tc_scim_env.py @@ -1,21 +1,21 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import ( SquirrelCageInductionMotorSystem, ) -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import ( - WienerProcessReferenceGenerator, MultipleReferenceGenerator, + WienerProcessReferenceGenerator, ) -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import SquaredConstraint +from gym_electric_motor.visualization import MotorDashboard class ContTorqueControlSquirrelCageInductionMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_cc_scim_env.py b/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_cc_scim_env.py index 672fc53e..e867fe38 100644 --- a/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_cc_scim_env.py +++ b/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_cc_scim_env.py @@ -1,21 +1,21 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import ( SquirrelCageInductionMotorSystem, ) -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import ( - WienerProcessReferenceGenerator, MultipleReferenceGenerator, + WienerProcessReferenceGenerator, ) -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import SquaredConstraint +from gym_electric_motor.visualization import MotorDashboard class FiniteCurrentControlSquirrelCageInductionMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_sc_scim_env.py b/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_sc_scim_env.py index 7777a162..426345c0 100644 --- a/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_sc_scim_env.py +++ b/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_sc_scim_env.py @@ -1,18 +1,18 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import ( SquirrelCageInductionMotorSystem, ) -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import SquaredConstraint +from gym_electric_motor.visualization import MotorDashboard class FiniteSpeedControlSquirrelCageInductionMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_tc_scim_env.py b/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_tc_scim_env.py index 2ec0ae76..8c7fa360 100644 --- a/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_tc_scim_env.py +++ b/src/gym_electric_motor/envs/gym_im/squirrel_cage_induction_motor_envs/finite_tc_scim_env.py @@ -1,18 +1,18 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import ( SquirrelCageInductionMotorSystem, ) -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import SquaredConstraint +from gym_electric_motor.visualization import MotorDashboard class FiniteTorqueControlSquirrelCageInductionMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_pmsm/__init__.py b/src/gym_electric_motor/envs/gym_pmsm/__init__.py index 37930dfc..df684696 100644 --- a/src/gym_electric_motor/envs/gym_pmsm/__init__.py +++ b/src/gym_electric_motor/envs/gym_pmsm/__init__.py @@ -1,6 +1,6 @@ +from .cont_cc_pmsm_env import ContCurrentControlPermanentMagnetSynchronousMotorEnv from .cont_sc_pmsm_env import ContSpeedControlPermanentMagnetSynchronousMotorEnv from .cont_tc_pmsm_env import ContTorqueControlPermanentMagnetSynchronousMotorEnv -from .cont_cc_pmsm_env import ContCurrentControlPermanentMagnetSynchronousMotorEnv -from .finite_tc_pmsm_env import FiniteTorqueControlPermanentMagnetSynchronousMotorEnv -from .finite_sc_pmsm_env import FiniteSpeedControlPermanentMagnetSynchronousMotorEnv from .finite_cc_pmsm_env import FiniteCurrentControlPermanentMagnetSynchronousMotorEnv +from .finite_sc_pmsm_env import FiniteSpeedControlPermanentMagnetSynchronousMotorEnv +from .finite_tc_pmsm_env import FiniteTorqueControlPermanentMagnetSynchronousMotorEnv diff --git a/src/gym_electric_motor/envs/gym_pmsm/cont_cc_pmsm_env.py b/src/gym_electric_motor/envs/gym_pmsm/cont_cc_pmsm_env.py index d525f1aa..6c7cf021 100644 --- a/src/gym_electric_motor/envs/gym_pmsm/cont_cc_pmsm_env.py +++ b/src/gym_electric_motor/envs/gym_pmsm/cont_cc_pmsm_env.py @@ -1,19 +1,19 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import SynchronousMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import ( - WienerProcessReferenceGenerator, MultipleReferenceGenerator, + WienerProcessReferenceGenerator, ) -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import SquaredConstraint +from gym_electric_motor.visualization import MotorDashboard class ContCurrentControlPermanentMagnetSynchronousMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_pmsm/cont_sc_pmsm_env.py b/src/gym_electric_motor/envs/gym_pmsm/cont_sc_pmsm_env.py index 333a7d32..8362ee4a 100644 --- a/src/gym_electric_motor/envs/gym_pmsm/cont_sc_pmsm_env.py +++ b/src/gym_electric_motor/envs/gym_pmsm/cont_sc_pmsm_env.py @@ -1,16 +1,16 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import SynchronousMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import SquaredConstraint +from gym_electric_motor.visualization import MotorDashboard class ContSpeedControlPermanentMagnetSynchronousMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_pmsm/cont_tc_pmsm_env.py b/src/gym_electric_motor/envs/gym_pmsm/cont_tc_pmsm_env.py index 0fe37952..e46afb5c 100644 --- a/src/gym_electric_motor/envs/gym_pmsm/cont_tc_pmsm_env.py +++ b/src/gym_electric_motor/envs/gym_pmsm/cont_tc_pmsm_env.py @@ -1,19 +1,19 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import SynchronousMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import ( - WienerProcessReferenceGenerator, MultipleReferenceGenerator, + WienerProcessReferenceGenerator, ) -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import SquaredConstraint +from gym_electric_motor.visualization import MotorDashboard class ContTorqueControlPermanentMagnetSynchronousMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_pmsm/finite_cc_pmsm_env.py b/src/gym_electric_motor/envs/gym_pmsm/finite_cc_pmsm_env.py index 7b237f78..bd517fcf 100644 --- a/src/gym_electric_motor/envs/gym_pmsm/finite_cc_pmsm_env.py +++ b/src/gym_electric_motor/envs/gym_pmsm/finite_cc_pmsm_env.py @@ -1,19 +1,19 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import SynchronousMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import ( - WienerProcessReferenceGenerator, MultipleReferenceGenerator, + WienerProcessReferenceGenerator, ) -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import SquaredConstraint +from gym_electric_motor.visualization import MotorDashboard class FiniteCurrentControlPermanentMagnetSynchronousMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_pmsm/finite_sc_pmsm_env.py b/src/gym_electric_motor/envs/gym_pmsm/finite_sc_pmsm_env.py index 48451cdc..f49f0cbb 100644 --- a/src/gym_electric_motor/envs/gym_pmsm/finite_sc_pmsm_env.py +++ b/src/gym_electric_motor/envs/gym_pmsm/finite_sc_pmsm_env.py @@ -1,16 +1,16 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import SynchronousMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import SquaredConstraint +from gym_electric_motor.visualization import MotorDashboard class FiniteSpeedControlPermanentMagnetSynchronousMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_pmsm/finite_tc_pmsm_env.py b/src/gym_electric_motor/envs/gym_pmsm/finite_tc_pmsm_env.py index 36442016..7303ff84 100644 --- a/src/gym_electric_motor/envs/gym_pmsm/finite_tc_pmsm_env.py +++ b/src/gym_electric_motor/envs/gym_pmsm/finite_tc_pmsm_env.py @@ -1,16 +1,16 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import SynchronousMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import SquaredConstraint +from gym_electric_motor.visualization import MotorDashboard class FiniteTorqueControlPermanentMagnetSynchronousMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_synrm/__init__.py b/src/gym_electric_motor/envs/gym_synrm/__init__.py index ed631ae0..da861d29 100644 --- a/src/gym_electric_motor/envs/gym_synrm/__init__.py +++ b/src/gym_electric_motor/envs/gym_synrm/__init__.py @@ -1,6 +1,6 @@ +from .cont_cc_synrm_env import ContCurrentControlSynchronousReluctanceMotorEnv from .cont_sc_synrm_env import ContSpeedControlSynchronousReluctanceMotorEnv from .cont_tc_synrm_env import ContTorqueControlSynchronousReluctanceMotorEnv -from .cont_cc_synrm_env import ContCurrentControlSynchronousReluctanceMotorEnv -from .finite_tc_synrm_env import FiniteTorqueControlSynchronousReluctanceMotorEnv -from .finite_sc_synrm_env import FiniteSpeedControlSynchronousReluctanceMotorEnv from .finite_cc_synrm_env import FiniteCurrentControlSynchronousReluctanceMotorEnv +from .finite_sc_synrm_env import FiniteSpeedControlSynchronousReluctanceMotorEnv +from .finite_tc_synrm_env import FiniteTorqueControlSynchronousReluctanceMotorEnv diff --git a/src/gym_electric_motor/envs/gym_synrm/cont_cc_synrm_env.py b/src/gym_electric_motor/envs/gym_synrm/cont_cc_synrm_env.py index 63cf2b9e..275210d4 100644 --- a/src/gym_electric_motor/envs/gym_synrm/cont_cc_synrm_env.py +++ b/src/gym_electric_motor/envs/gym_synrm/cont_cc_synrm_env.py @@ -1,19 +1,19 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import SynchronousMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import ( - WienerProcessReferenceGenerator, MultipleReferenceGenerator, + WienerProcessReferenceGenerator, ) -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import SquaredConstraint +from gym_electric_motor.visualization import MotorDashboard class ContCurrentControlSynchronousReluctanceMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_synrm/cont_sc_synrm_env.py b/src/gym_electric_motor/envs/gym_synrm/cont_sc_synrm_env.py index 74142f13..0375940d 100644 --- a/src/gym_electric_motor/envs/gym_synrm/cont_sc_synrm_env.py +++ b/src/gym_electric_motor/envs/gym_synrm/cont_sc_synrm_env.py @@ -1,16 +1,16 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import SynchronousMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import SquaredConstraint +from gym_electric_motor.visualization import MotorDashboard class ContSpeedControlSynchronousReluctanceMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_synrm/cont_tc_synrm_env.py b/src/gym_electric_motor/envs/gym_synrm/cont_tc_synrm_env.py index 2a4dddf1..21959b96 100644 --- a/src/gym_electric_motor/envs/gym_synrm/cont_tc_synrm_env.py +++ b/src/gym_electric_motor/envs/gym_synrm/cont_tc_synrm_env.py @@ -1,19 +1,19 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import SynchronousMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import ( - WienerProcessReferenceGenerator, MultipleReferenceGenerator, + WienerProcessReferenceGenerator, ) -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import SquaredConstraint +from gym_electric_motor.visualization import MotorDashboard class ContTorqueControlSynchronousReluctanceMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_synrm/finite_cc_synrm_env.py b/src/gym_electric_motor/envs/gym_synrm/finite_cc_synrm_env.py index e62965bd..6cd4e181 100644 --- a/src/gym_electric_motor/envs/gym_synrm/finite_cc_synrm_env.py +++ b/src/gym_electric_motor/envs/gym_synrm/finite_cc_synrm_env.py @@ -1,19 +1,19 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import SynchronousMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import ( - WienerProcessReferenceGenerator, MultipleReferenceGenerator, + WienerProcessReferenceGenerator, ) -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import SquaredConstraint +from gym_electric_motor.visualization import MotorDashboard class FiniteCurrentControlSynchronousReluctanceMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_synrm/finite_sc_synrm_env.py b/src/gym_electric_motor/envs/gym_synrm/finite_sc_synrm_env.py index 9bb68f4b..a02a9255 100644 --- a/src/gym_electric_motor/envs/gym_synrm/finite_sc_synrm_env.py +++ b/src/gym_electric_motor/envs/gym_synrm/finite_sc_synrm_env.py @@ -1,16 +1,16 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import SynchronousMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import SquaredConstraint +from gym_electric_motor.visualization import MotorDashboard class FiniteSpeedControlSynchronousReluctanceMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/gym_synrm/finite_tc_synrm_env.py b/src/gym_electric_motor/envs/gym_synrm/finite_tc_synrm_env.py index ef675c0d..afabb6ae 100644 --- a/src/gym_electric_motor/envs/gym_synrm/finite_tc_synrm_env.py +++ b/src/gym_electric_motor/envs/gym_synrm/finite_tc_synrm_env.py @@ -1,16 +1,16 @@ +from gym_electric_motor import physical_systems as ps +from gym_electric_motor.constraints import SquaredConstraint from gym_electric_motor.core import ( ElectricMotorEnvironment, + ElectricMotorVisualization, ReferenceGenerator, RewardFunction, - ElectricMotorVisualization, ) from gym_electric_motor.physical_systems.physical_systems import SynchronousMotorSystem -from gym_electric_motor.visualization import MotorDashboard from gym_electric_motor.reference_generators import WienerProcessReferenceGenerator -from gym_electric_motor import physical_systems as ps from gym_electric_motor.reward_functions import WeightedSumOfErrors from gym_electric_motor.utils import initialize -from gym_electric_motor.constraints import SquaredConstraint +from gym_electric_motor.visualization import MotorDashboard class FiniteTorqueControlSynchronousReluctanceMotorEnv(ElectricMotorEnvironment): diff --git a/src/gym_electric_motor/envs/motors.py b/src/gym_electric_motor/envs/motors.py index 53633aee..d7e232c8 100644 --- a/src/gym_electric_motor/envs/motors.py +++ b/src/gym_electric_motor/envs/motors.py @@ -1,5 +1,5 @@ -from enum import Enum from dataclasses import dataclass +from enum import Enum MotorType = Enum( "MotorType", diff --git a/src/gym_electric_motor/physical_system_wrappers/__init__.py b/src/gym_electric_motor/physical_system_wrappers/__init__.py index dffce13f..91e3aba9 100644 --- a/src/gym_electric_motor/physical_system_wrappers/__init__.py +++ b/src/gym_electric_motor/physical_system_wrappers/__init__.py @@ -1,7 +1,7 @@ -from .physical_system_wrapper import PhysicalSystemWrapper +from .cos_sin_processor import CosSinProcessor from .current_sum_processor import CurrentSumProcessor -from .flux_observer import FluxObserver +from .dead_time_processor import DeadTimeProcessor from .dq_to_abc_action_processor import DqToAbcActionProcessor -from .cos_sin_processor import CosSinProcessor +from .flux_observer import FluxObserver +from .physical_system_wrapper import PhysicalSystemWrapper from .state_noise_processor import StateNoiseProcessor -from .dead_time_processor import DeadTimeProcessor diff --git a/src/gym_electric_motor/physical_system_wrappers/cos_sin_processor.py b/src/gym_electric_motor/physical_system_wrappers/cos_sin_processor.py index b8e30bd1..fd18dadb 100644 --- a/src/gym_electric_motor/physical_system_wrappers/cos_sin_processor.py +++ b/src/gym_electric_motor/physical_system_wrappers/cos_sin_processor.py @@ -1,7 +1,7 @@ import gymnasium import numpy as np -from gym_electric_motor.physical_system_wrappers import PhysicalSystemWrapper +from .physical_system_wrapper import PhysicalSystemWrapper class CosSinProcessor(PhysicalSystemWrapper): diff --git a/src/gym_electric_motor/physical_system_wrappers/current_sum_processor.py b/src/gym_electric_motor/physical_system_wrappers/current_sum_processor.py index 5873c88e..7efc20df 100644 --- a/src/gym_electric_motor/physical_system_wrappers/current_sum_processor.py +++ b/src/gym_electric_motor/physical_system_wrappers/current_sum_processor.py @@ -1,7 +1,7 @@ import gymnasium import numpy as np -from gym_electric_motor.physical_system_wrappers import PhysicalSystemWrapper +from .physical_system_wrapper import PhysicalSystemWrapper class CurrentSumProcessor(PhysicalSystemWrapper): diff --git a/src/gym_electric_motor/physical_system_wrappers/dead_time_processor.py b/src/gym_electric_motor/physical_system_wrappers/dead_time_processor.py index de4cde92..44c70904 100644 --- a/src/gym_electric_motor/physical_system_wrappers/dead_time_processor.py +++ b/src/gym_electric_motor/physical_system_wrappers/dead_time_processor.py @@ -1,8 +1,9 @@ from collections import deque -import numpy as np import gymnasium.spaces -from gym_electric_motor.physical_system_wrappers import PhysicalSystemWrapper +import numpy as np + +from .physical_system_wrapper import PhysicalSystemWrapper class DeadTimeProcessor(PhysicalSystemWrapper): diff --git a/src/gym_electric_motor/physical_system_wrappers/dq_to_abc_action_processor.py b/src/gym_electric_motor/physical_system_wrappers/dq_to_abc_action_processor.py index e592686c..2d5a1da6 100644 --- a/src/gym_electric_motor/physical_system_wrappers/dq_to_abc_action_processor.py +++ b/src/gym_electric_motor/physical_system_wrappers/dq_to_abc_action_processor.py @@ -2,7 +2,8 @@ import numpy as np import gym_electric_motor.physical_systems as ps -from gym_electric_motor.physical_system_wrappers import PhysicalSystemWrapper + +from .physical_system_wrapper import PhysicalSystemWrapper class DqToAbcActionProcessor(PhysicalSystemWrapper): diff --git a/src/gym_electric_motor/physical_system_wrappers/flux_observer.py b/src/gym_electric_motor/physical_system_wrappers/flux_observer.py index 22418b0e..0bd8380f 100644 --- a/src/gym_electric_motor/physical_system_wrappers/flux_observer.py +++ b/src/gym_electric_motor/physical_system_wrappers/flux_observer.py @@ -2,7 +2,8 @@ import numpy as np import gym_electric_motor as gem -from gym_electric_motor.physical_system_wrappers import PhysicalSystemWrapper + +from .physical_system_wrapper import PhysicalSystemWrapper class FluxObserver(PhysicalSystemWrapper): diff --git a/src/gym_electric_motor/physical_system_wrappers/physical_system_wrapper.py b/src/gym_electric_motor/physical_system_wrappers/physical_system_wrapper.py index 92396fee..bb4332ca 100644 --- a/src/gym_electric_motor/physical_system_wrappers/physical_system_wrapper.py +++ b/src/gym_electric_motor/physical_system_wrappers/physical_system_wrapper.py @@ -1,7 +1,9 @@ import gym_electric_motor as gem +from ..random_component import RandomComponent -class PhysicalSystemWrapper(gem.core.PhysicalSystem, gem.core.RandomComponent): + +class PhysicalSystemWrapper(gem.core.PhysicalSystem, RandomComponent): """A PhysicalSystemWrapper is a wrapper around the PhysicalSystem of a gem-environment. It may be used to modify its states and actions. In contrast to gym-wrappers which are put around the whole @@ -83,7 +85,7 @@ def __init__(self, physical_system=None): set_physical_system-method after the initialization. """ gem.core.PhysicalSystem.__init__(self, None, None, (), None) - gem.core.RandomComponent.__init__(self) + RandomComponent.__init__(self) self._physical_system = physical_system self._limits = None self._nominal_state = None @@ -105,7 +107,7 @@ def set_physical_system(self, physical_system): def seed(self, seed=None): # docstring of super class RandomComponent - if isinstance(self._physical_system, gem.core.RandomComponent): + if isinstance(self._physical_system, RandomComponent): self._physical_system.seed(seed) def __getattr__(self, name): diff --git a/src/gym_electric_motor/physical_systems/__init__.py b/src/gym_electric_motor/physical_systems/__init__.py index f25c6a7f..a9f3afa3 100644 --- a/src/gym_electric_motor/physical_systems/__init__.py +++ b/src/gym_electric_motor/physical_systems/__init__.py @@ -1,71 +1,73 @@ -from .physical_systems import ( - DcMotorSystem, - SynchronousMotorSystem, - SquirrelCageInductionMotorSystem, - DoublyFedInductionMotorSystem, - ExternallyExcitedSynchronousMotorSystem, - ThreePhaseMotorSystem, - SCMLSystem, -) - +from ..core import PhysicalSystem +from ..utils import register_class, register_superclass from .converters import ( - PowerElectronicConverter, - FiniteOneQuadrantConverter, - FiniteTwoQuadrantConverter, - FiniteFourQuadrantConverter, - FiniteMultiConverter, - FiniteB6BridgeConverter, - ContOneQuadrantConverter, - ContTwoQuadrantConverter, + ContB6BridgeConverter, ContFourQuadrantConverter, ContMultiConverter, - ContB6BridgeConverter, + ContOneQuadrantConverter, + ContTwoQuadrantConverter, + FiniteB6BridgeConverter, + FiniteFourQuadrantConverter, + FiniteMultiConverter, + FiniteOneQuadrantConverter, + FiniteTwoQuadrantConverter, NoConverter, + PowerElectronicConverter, ) - +from .converters import ContB6BridgeConverter as ContB6C +from .converters import ContFourQuadrantConverter as Cont4QC +from .converters import ContMultiConverter as ContMulti +from .converters import ContOneQuadrantConverter as Cont1QC +from .converters import ContTwoQuadrantConverter as Cont2QC +from .converters import FiniteB6BridgeConverter as FiniteB6C +from .converters import FiniteFourQuadrantConverter as Finite4QC +from .converters import FiniteMultiConverter as FiniteMulti +from .converters import FiniteOneQuadrantConverter as Finite1QC +from .converters import FiniteTwoQuadrantConverter as Finite2QC from .electric_motors import ( DcExternallyExcitedMotor, - DcSeriesMotor, DcPermanentlyExcitedMotor, + DcSeriesMotor, DcShuntMotor, - PermanentMagnetSynchronousMotor, - ElectricMotor, - SynchronousReluctanceMotor, - SquirrelCageInductionMotor, DoublyFedInductionMotor, + ElectricMotor, ExternallyExcitedSynchronousMotor, + PermanentMagnetSynchronousMotor, + SquirrelCageInductionMotor, + SynchronousReluctanceMotor, ThreePhaseMotor, ) - - from .mechanical_loads import ( - MechanicalLoad, - PolynomialStaticLoad, - ExternalSpeedLoad, ConstantSpeedLoad, + ExternalSpeedLoad, + MechanicalLoad, OrnsteinUhlenbeckLoad, + PolynomialStaticLoad, +) +from .physical_systems import ( + DcMotorSystem, + DoublyFedInductionMotorSystem, + ExternallyExcitedSynchronousMotorSystem, + SCMLSystem, + SquirrelCageInductionMotorSystem, + SynchronousMotorSystem, + ThreePhaseMotorSystem, ) - from .solvers import ( - OdeSolver, EulerSolver, + OdeSolver, ScipyOdeIntSolver, - ScipySolveIvpSolver, ScipyOdeSolver, + ScipySolveIvpSolver, ) - from .voltage_supplies import ( - VoltageSupply, - IdealVoltageSupply, - RCVoltageSupply, AC1PhaseSupply, AC3PhaseSupply, + IdealVoltageSupply, + RCVoltageSupply, + VoltageSupply, ) - -from ..utils import register_class, register_superclass -from .. import PhysicalSystem - register_superclass(PowerElectronicConverter) register_superclass(MechanicalLoad) register_superclass(ElectricMotor) diff --git a/src/gym_electric_motor/physical_systems/converters.py b/src/gym_electric_motor/physical_systems/converters.py index d45afb92..05d93b87 100644 --- a/src/gym_electric_motor/physical_systems/converters.py +++ b/src/gym_electric_motor/physical_systems/converters.py @@ -1,5 +1,5 @@ import numpy as np -from gymnasium.spaces import Discrete, Box, MultiDiscrete +from gymnasium.spaces import Box, Discrete, MultiDiscrete from ..utils import instantiate @@ -373,7 +373,7 @@ def i_sup(self, i_out): class ContOneQuadrantConverter(ContDynamicallyAveragedConverter): """ Key: - 'Cont-1QC' + 'Cont1QC' Action: Duty Cycle of the Transistor in [0,1]. @@ -536,9 +536,13 @@ def __init__(self, subconverters, **kwargs): kwargs(dict): Parameters to pass to the Subconverters and the superclass """ super().__init__(**kwargs) - self._sub_converters = [ - instantiate(PowerElectronicConverter, subconverter, **kwargs) for subconverter in subconverters - ] + self._sub_converters = [] + for subconverter in subconverters: + assert not isinstance(subconverter, str) + if isinstance(subconverter, type): + subconverter = subconverter(**kwargs) + self._sub_converters.append(subconverter) + self.subsignal_current_space_dims = [] self.subsignal_voltage_space_dims = [] self.action_space = [] @@ -647,9 +651,12 @@ def __init__(self, subconverters, **kwargs): kwargs(dict): Parameters to pass to the Subconverters """ super().__init__(**kwargs) - self._sub_converters = [ - instantiate(PowerElectronicConverter, subconverter, **kwargs) for subconverter in subconverters - ] + self._sub_converters = [] + for subconverter in subconverters: + assert not isinstance(subconverter, str) + if isinstance(subconverter, type): + subconverter = subconverter(**kwargs) + self._sub_converters.append(subconverter) self.subsignal_current_space_dims = [] self.subsignal_voltage_space_dims = [] diff --git a/src/gym_electric_motor/physical_systems/electric_motors/__init__.py b/src/gym_electric_motor/physical_systems/electric_motors/__init__.py index afa6813c..e6a096dd 100644 --- a/src/gym_electric_motor/physical_systems/electric_motors/__init__.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/__init__.py @@ -1,23 +1,23 @@ # Electric Motor Base Class -from .electric_motor import ElectricMotor +from .dc_externally_excited_motor import DcExternallyExcitedMotor # DC Motors from .dc_motor import DcMotor -from .dc_externally_excited_motor import DcExternallyExcitedMotor from .dc_permanently_excited_motor import DcPermanentlyExcitedMotor from .dc_series_motor import DcSeriesMotor from .dc_shunt_motor import DcShuntMotor +from .doubly_fed_induction_motor import DoublyFedInductionMotor +from .electric_motor import ElectricMotor +from .externally_excited_synchronous_motor import ExternallyExcitedSynchronousMotor -# Three Phase Motors -from .three_phase_motor import ThreePhaseMotor +# Induction Motors +from .induction_motor import InductionMotor +from .permanent_magnet_synchronous_motor import PermanentMagnetSynchronousMotor +from .squirrel_cage_induction_motor import SquirrelCageInductionMotor # Synchronous Motors from .synchronous_motor import SynchronousMotor from .synchronous_reluctance_motor import SynchronousReluctanceMotor -from .permanent_magnet_synchronous_motor import PermanentMagnetSynchronousMotor -from .externally_excited_synchronous_motor import ExternallyExcitedSynchronousMotor -# Induction Motors -from .induction_motor import InductionMotor -from .squirrel_cage_induction_motor import SquirrelCageInductionMotor -from .doubly_fed_induction_motor import DoublyFedInductionMotor +# Three Phase Motors +from .three_phase_motor import ThreePhaseMotor diff --git a/src/gym_electric_motor/physical_systems/electric_motors/dc_permanently_excited_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/dc_permanently_excited_motor.py index e144a754..6727bc7f 100644 --- a/src/gym_electric_motor/physical_systems/electric_motors/dc_permanently_excited_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/dc_permanently_excited_motor.py @@ -105,7 +105,6 @@ def _update_limits(self): def get_state_space(self, input_currents, input_voltages): # Docstring of superclass - lower_limit = 0 low = { "omega": -1 if input_voltages.low[0] == -1 else 0, "torque": -1 if input_currents.low[0] == -1 else 0, diff --git a/src/gym_electric_motor/physical_systems/electric_motors/dc_series_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/dc_series_motor.py index aa948a18..bb9dbfb3 100644 --- a/src/gym_electric_motor/physical_systems/electric_motors/dc_series_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/dc_series_motor.py @@ -99,7 +99,6 @@ def _update_limits(self): def get_state_space(self, input_currents, input_voltages): # Docstring of superclass - lower_limit = 0 low = { "omega": 0, "torque": 0, diff --git a/src/gym_electric_motor/physical_systems/electric_motors/dc_shunt_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/dc_shunt_motor.py index 6d5f8bad..edc41d9a 100644 --- a/src/gym_electric_motor/physical_systems/electric_motors/dc_shunt_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/dc_shunt_motor.py @@ -102,7 +102,6 @@ def get_state_space(self, input_currents, input_voltages): Returns: tuple(dict,dict): Dictionaries defining if positive and negative values are possible for each motors state. """ - lower_limit = 0 low = { "omega": 0, diff --git a/src/gym_electric_motor/physical_systems/electric_motors/doubly_fed_induction_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/doubly_fed_induction_motor.py index a670df78..1eebccc2 100644 --- a/src/gym_electric_motor/physical_systems/electric_motors/doubly_fed_induction_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/doubly_fed_induction_motor.py @@ -1,4 +1,5 @@ import math + import numpy as np from .induction_motor import InductionMotor diff --git a/src/gym_electric_motor/physical_systems/electric_motors/electric_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/electric_motor.py index 9e0c0e2c..e3ad6d8e 100644 --- a/src/gym_electric_motor/physical_systems/electric_motors/electric_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/electric_motor.py @@ -1,9 +1,10 @@ import numpy as np from scipy.stats import truncnorm -from ...random_component import RandomComponent from gym_electric_motor.utils import update_parameter_dict +from ...random_component import RandomComponent + class ElectricMotor(RandomComponent): """Base class for all technical electrical motor models. diff --git a/src/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py index 32303997..c35ef654 100644 --- a/src/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py @@ -1,4 +1,5 @@ import math + import numpy as np from .synchronous_motor import SynchronousMotor diff --git a/src/gym_electric_motor/physical_systems/electric_motors/induction_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/induction_motor.py index 44ce0853..b2416967 100644 --- a/src/gym_electric_motor/physical_systems/electric_motors/induction_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/induction_motor.py @@ -1,4 +1,5 @@ import math + import numpy as np from .three_phase_motor import ThreePhaseMotor diff --git a/src/gym_electric_motor/physical_systems/electric_motors/permanent_magnet_synchronous_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/permanent_magnet_synchronous_motor.py index 33abd0b7..3fd927e8 100644 --- a/src/gym_electric_motor/physical_systems/electric_motors/permanent_magnet_synchronous_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/permanent_magnet_synchronous_motor.py @@ -1,4 +1,5 @@ import math + import numpy as np from .synchronous_motor import SynchronousMotor diff --git a/src/gym_electric_motor/physical_systems/electric_motors/squirrel_cage_induction_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/squirrel_cage_induction_motor.py index e4258775..7730357f 100644 --- a/src/gym_electric_motor/physical_systems/electric_motors/squirrel_cage_induction_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/squirrel_cage_induction_motor.py @@ -1,4 +1,5 @@ import math + import numpy as np from .induction_motor import InductionMotor diff --git a/src/gym_electric_motor/physical_systems/electric_motors/three_phase_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/three_phase_motor.py index 441e5706..dc6c885f 100644 --- a/src/gym_electric_motor/physical_systems/electric_motors/three_phase_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/three_phase_motor.py @@ -1,4 +1,5 @@ import math + import numpy as np from .electric_motor import ElectricMotor diff --git a/src/gym_electric_motor/physical_systems/mechanical_loads/__init__.py b/src/gym_electric_motor/physical_systems/mechanical_loads/__init__.py index 83eaeebd..d94d1e9f 100644 --- a/src/gym_electric_motor/physical_systems/mechanical_loads/__init__.py +++ b/src/gym_electric_motor/physical_systems/mechanical_loads/__init__.py @@ -1,5 +1,5 @@ -from .mechanical_load import MechanicalLoad from .constant_speed_load import ConstantSpeedLoad +from .external_speed_load import ExternalSpeedLoad +from .mechanical_load import MechanicalLoad from .ornstein_uhlenbeck_load import OrnsteinUhlenbeckLoad from .polynomial_static_load import PolynomialStaticLoad -from .external_speed_load import ExternalSpeedLoad diff --git a/src/gym_electric_motor/physical_systems/mechanical_loads/external_speed_load.py b/src/gym_electric_motor/physical_systems/mechanical_loads/external_speed_load.py index c0d8b868..6c18fdfc 100644 --- a/src/gym_electric_motor/physical_systems/mechanical_loads/external_speed_load.py +++ b/src/gym_electric_motor/physical_systems/mechanical_loads/external_speed_load.py @@ -1,6 +1,7 @@ -import numpy as np import warnings +import numpy as np + from .mechanical_load import MechanicalLoad diff --git a/src/gym_electric_motor/physical_systems/mechanical_loads/mechanical_load.py b/src/gym_electric_motor/physical_systems/mechanical_loads/mechanical_load.py index afc6c6de..d1580707 100644 --- a/src/gym_electric_motor/physical_systems/mechanical_loads/mechanical_load.py +++ b/src/gym_electric_motor/physical_systems/mechanical_loads/mechanical_load.py @@ -1,6 +1,7 @@ +import warnings + import numpy as np from scipy.stats import truncnorm -import warnings from ...random_component import RandomComponent diff --git a/src/gym_electric_motor/physical_systems/mechanical_loads/polynomial_static_load.py b/src/gym_electric_motor/physical_systems/mechanical_loads/polynomial_static_load.py index 4d8eb4ce..108faa02 100644 --- a/src/gym_electric_motor/physical_systems/mechanical_loads/polynomial_static_load.py +++ b/src/gym_electric_motor/physical_systems/mechanical_loads/polynomial_static_load.py @@ -1,8 +1,9 @@ import numpy as np -from .mechanical_load import MechanicalLoad from gym_electric_motor.utils import update_parameter_dict +from .mechanical_load import MechanicalLoad + class PolynomialStaticLoad(MechanicalLoad): """Mechanical system that models the Mechanical-ODE based on a static polynomial load torque. diff --git a/src/gym_electric_motor/physical_systems/physical_systems.py b/src/gym_electric_motor/physical_systems/physical_systems.py index 2458b1e5..5e8640e9 100644 --- a/src/gym_electric_motor/physical_systems/physical_systems.py +++ b/src/gym_electric_motor/physical_systems/physical_systems.py @@ -1,10 +1,12 @@ +import warnings + import numpy as np from gymnasium.spaces import Box -import warnings import gym_electric_motor as gem -from ..random_component import RandomComponent + from ..core import PhysicalSystem +from ..random_component import RandomComponent from ..utils import set_state_array diff --git a/src/gym_electric_motor/physical_systems/solvers.py b/src/gym_electric_motor/physical_systems/solvers.py index dc4265c9..31748df9 100644 --- a/src/gym_electric_motor/physical_systems/solvers.py +++ b/src/gym_electric_motor/physical_systems/solvers.py @@ -1,4 +1,4 @@ -from scipy.integrate import ode, solve_ivp, odeint +from scipy.integrate import ode, odeint, solve_ivp class OdeSolver: diff --git a/src/gym_electric_motor/physical_systems/voltage_supplies.py b/src/gym_electric_motor/physical_systems/voltage_supplies.py index 7442fc52..9dcba41e 100644 --- a/src/gym_electric_motor/physical_systems/voltage_supplies.py +++ b/src/gym_electric_motor/physical_systems/voltage_supplies.py @@ -1,7 +1,9 @@ -from gym_electric_motor.physical_systems.solvers import EulerSolver import warnings + import numpy as np +from gym_electric_motor.physical_systems.solvers import EulerSolver + class VoltageSupply: """Base class for all VoltageSupplies to be used in a SCMLSystem. diff --git a/src/gym_electric_motor/reference_generators/__init__.py b/src/gym_electric_motor/reference_generators/__init__.py index 956225ce..216123da 100644 --- a/src/gym_electric_motor/reference_generators/__init__.py +++ b/src/gym_electric_motor/reference_generators/__init__.py @@ -1,16 +1,16 @@ -from .wiener_process_reference_generator import WienerProcessReferenceGenerator -from .switched_reference_generator import SwitchedReferenceGenerator -from .zero_reference_generator import ZeroReferenceGenerator -from .sinusoidal_reference_generator import SinusoidalReferenceGenerator -from .step_reference_generator import StepReferenceGenerator -from .triangle_reference_generator import TriangularReferenceGenerator -from .sawtooth_reference_generator import SawtoothReferenceGenerator +from ..core import ReferenceGenerator +from ..utils import register_class from .const_reference_generator import ConstReferenceGenerator +from .laplace_process_reference_generator import LaplaceProcessReferenceGenerator from .multiple_reference_generator import MultipleReferenceGenerator +from .sawtooth_reference_generator import SawtoothReferenceGenerator +from .sinusoidal_reference_generator import SinusoidalReferenceGenerator +from .step_reference_generator import StepReferenceGenerator from .subepisoded_reference_generator import SubepisodedReferenceGenerator -from .laplace_process_reference_generator import LaplaceProcessReferenceGenerator -from ..utils import register_class -from ..core import ReferenceGenerator +from .switched_reference_generator import SwitchedReferenceGenerator +from .triangle_reference_generator import TriangularReferenceGenerator +from .wiener_process_reference_generator import WienerProcessReferenceGenerator +from .zero_reference_generator import ZeroReferenceGenerator register_class(WienerProcessReferenceGenerator, ReferenceGenerator, "WienerProcessReference") register_class(SwitchedReferenceGenerator, ReferenceGenerator, "SwitchedReference") diff --git a/src/gym_electric_motor/reference_generators/const_reference_generator.py b/src/gym_electric_motor/reference_generators/const_reference_generator.py index db1e6f2d..82cd42a4 100644 --- a/src/gym_electric_motor/reference_generators/const_reference_generator.py +++ b/src/gym_electric_motor/reference_generators/const_reference_generator.py @@ -1,7 +1,8 @@ import numpy as np +from gymnasium.spaces import Box + from gym_electric_motor.core import ReferenceGenerator from gym_electric_motor.utils import set_state_array -from gymnasium.spaces import Box class ConstReferenceGenerator(ReferenceGenerator): diff --git a/src/gym_electric_motor/reference_generators/multiple_reference_generator.py b/src/gym_electric_motor/reference_generators/multiple_reference_generator.py index 1306d097..60165f69 100644 --- a/src/gym_electric_motor/reference_generators/multiple_reference_generator.py +++ b/src/gym_electric_motor/reference_generators/multiple_reference_generator.py @@ -2,11 +2,11 @@ from gymnasium.spaces import Box from ..core import ReferenceGenerator +from ..random_component import RandomComponent from ..utils import instantiate -import gym_electric_motor as gem -class MultipleReferenceGenerator(ReferenceGenerator, gem.RandomComponent): +class MultipleReferenceGenerator(ReferenceGenerator, RandomComponent): """Reference Generator that combines multiple sub reference generators that all have to reference different state variables. """ @@ -20,9 +20,9 @@ def __init__(self, sub_generators, sub_args=None, **kwargs): kwargs: All kwargs of the environment. Passed to the sub_generators, if no sub_args are passed. """ ReferenceGenerator.__init__(self, **kwargs) - gem.RandomComponent.__init__(self) + RandomComponent.__init__(self) self.reference_space = Box(-1, 1, shape=(1,), dtype=np.float64) - if type(sub_args) is dict: + if isinstance(sub_args, dict): sub_arguments = [sub_args] * len(sub_generators) elif hasattr(sub_args, "__iter__"): assert len(sub_args) == len(sub_generators) @@ -83,6 +83,6 @@ def get_reference_observation(self, state, *_, **kwargs): def seed(self, seed=None): super().seed(seed) for sub_generator in self._sub_generators: - if isinstance(sub_generator, gem.RandomComponent): + if isinstance(sub_generator, RandomComponent): seed = self._seed_sequence.spawn(1)[0] sub_generator.seed(seed) diff --git a/src/gym_electric_motor/reference_generators/sawtooth_reference_generator.py b/src/gym_electric_motor/reference_generators/sawtooth_reference_generator.py index 6c5aa8b7..0a6856a8 100644 --- a/src/gym_electric_motor/reference_generators/sawtooth_reference_generator.py +++ b/src/gym_electric_motor/reference_generators/sawtooth_reference_generator.py @@ -1,5 +1,6 @@ import numpy as np from scipy import signal as sg + from .subepisoded_reference_generator import SubepisodedReferenceGenerator diff --git a/src/gym_electric_motor/reference_generators/subepisoded_reference_generator.py b/src/gym_electric_motor/reference_generators/subepisoded_reference_generator.py index 56e9e822..6cf31d83 100644 --- a/src/gym_electric_motor/reference_generators/subepisoded_reference_generator.py +++ b/src/gym_electric_motor/reference_generators/subepisoded_reference_generator.py @@ -1,8 +1,8 @@ import numpy as np from gymnasium.spaces import Box -from ..random_component import RandomComponent from ..core import ReferenceGenerator +from ..random_component import RandomComponent from ..utils import set_state_array diff --git a/src/gym_electric_motor/reference_generators/switched_reference_generator.py b/src/gym_electric_motor/reference_generators/switched_reference_generator.py index b2c1704b..1bab4a57 100644 --- a/src/gym_electric_motor/reference_generators/switched_reference_generator.py +++ b/src/gym_electric_motor/reference_generators/switched_reference_generator.py @@ -1,8 +1,8 @@ import numpy as np from gymnasium.spaces import Box -from ..random_component import RandomComponent from ..core import ReferenceGenerator +from ..random_component import RandomComponent from ..utils import instantiate diff --git a/src/gym_electric_motor/reference_generators/triangle_reference_generator.py b/src/gym_electric_motor/reference_generators/triangle_reference_generator.py index 0daa45a5..32fb3b33 100644 --- a/src/gym_electric_motor/reference_generators/triangle_reference_generator.py +++ b/src/gym_electric_motor/reference_generators/triangle_reference_generator.py @@ -1,5 +1,6 @@ import numpy as np from scipy import signal as sg + from .subepisoded_reference_generator import SubepisodedReferenceGenerator diff --git a/src/gym_electric_motor/reward_functions/__init__.py b/src/gym_electric_motor/reward_functions/__init__.py index a520040b..85d47997 100644 --- a/src/gym_electric_motor/reward_functions/__init__.py +++ b/src/gym_electric_motor/reward_functions/__init__.py @@ -1,5 +1,5 @@ -from .weighted_sum_of_errors import WeightedSumOfErrors +from ..core import RewardFunction from ..utils import register_class -from .. import RewardFunction +from .weighted_sum_of_errors import WeightedSumOfErrors register_class(WeightedSumOfErrors, RewardFunction, "WSE") diff --git a/src/gym_electric_motor/reward_functions/weighted_sum_of_errors.py b/src/gym_electric_motor/reward_functions/weighted_sum_of_errors.py index 8d2c3350..27cd620d 100644 --- a/src/gym_electric_motor/reward_functions/weighted_sum_of_errors.py +++ b/src/gym_electric_motor/reward_functions/weighted_sum_of_errors.py @@ -1,8 +1,9 @@ +import warnings + import numpy as np from ..core import RewardFunction from ..utils import set_state_array -import warnings class WeightedSumOfErrors(RewardFunction): diff --git a/src/gym_electric_motor/utils.py b/src/gym_electric_motor/utils.py index 0d1f12f1..cb4b8bcc 100644 --- a/src/gym_electric_motor/utils.py +++ b/src/gym_electric_motor/utils.py @@ -1,5 +1,5 @@ -import numpy as np import gymnasium +import numpy as np def state_dict_to_state_array(state_dict, state_array, state_names): @@ -40,100 +40,22 @@ def set_state_array(input_values, state_names): the state_names and zero otherwise. """ - if type(input_values) is dict: + if isinstance(input_values, dict): state_array = np.zeros_like(state_names, dtype=float) state_dict_to_state_array(input_values, state_array, state_names) - elif type(input_values) is np.ndarray: + elif isinstance(input_values, np.ndarray): assert len(input_values) == len(state_names) state_array = input_values - elif type(input_values) is list: + elif isinstance(input_values, list): assert len(input_values) == len(state_names) state_array = np.array(input_values) - elif type(input_values) is float or type(input_values) is int: + elif isinstance(input_values, float) or isinstance(input_values, int): state_array = input_values * np.ones_like(state_names, dtype=float) else: raise Exception("Incorrect type for the input values.") return state_array -def initialize(base_class, arg, default_class, default_args): - if arg is None: - return default_class(**default_args) - elif isinstance(arg, base_class): - return arg - elif type(arg) is str: - return _registry[base_class][arg]() - elif type(arg) is dict: - default_args.update(arg) - return default_class(**default_args) - - -def instantiate(superclass, instance, **kwargs): - """ - Instantiation of an instance that inherits from the passed superclass. - - The instance can be passed as a key-string, a class pointer or an already instantiated object. In the latter case - the same object will be simply returned. If a string is passed the corresponding class will be taken from the - registry and instantiated with the given kwargs. If a class pointer is passed, then the class is instantiated with - with given kwargs, directly. - - Args: - superclass(class): Superclass pointer for registry access - instance(str, class, object): Instance to instantiate - kwargs: Arguments for the instantiation of the object - - Returns: - An instantiated object. - """ - if type(instance) is type and issubclass(instance, superclass): - return instance(**kwargs) - elif isinstance(instance, superclass): - return instance - elif type(instance) is str: - return make_module(superclass, instance, **kwargs) - else: - raise Exception("Instantiation Error.") - - -# Registry dictionary that stores the keys to instantiate the components with the keystrings -_registry = {} - - -def make_module(superclass, keystring, **kwargs): - """ - Instantiation by an object that is specified by the key-string an its superclass from the registry. - - Args: - superclass(class): Superclass pointer for registry access - keystring(str): String to access the class pointer in the registry. - kwargs: Arguments for the instantiation of the object. - - Returns: - An instantiated object. - """ - try: - return _registry[superclass][keystring](**kwargs) - except KeyError: - raise Exception(f"Key {keystring} or baseclass {superclass.__name__} not found in the registry.") - - -def register_superclass(superclass): - """ - Method to register a new superclass that can contain several key-strings for instantiation in the registry. - - Basically, all superclasses in GEM are already registered like the Physical Systems, Reference Generators, ... - """ - _registry[superclass] = {} - - -def register_class(subclass, superclass, keystring): - """ - Method to register a new class with a key-string into the registry of the superclass - to be instantiable with the key-string. - """ - _registry[superclass][keystring] = subclass - - def update_parameter_dict(source_dict, update_dict, copy=True): """Merges two dictionaries (source and update) together. diff --git a/src/gym_electric_motor/visualization/__init__.py b/src/gym_electric_motor/visualization/__init__.py index f7329e21..78f7da84 100644 --- a/src/gym_electric_motor/visualization/__init__.py +++ b/src/gym_electric_motor/visualization/__init__.py @@ -1,9 +1,8 @@ +from ..core import ElectricMotorVisualization +from ..utils import register_class from .console_printer import ConsolePrinter from .motor_dashboard import MotorDashboard from .render_modes import RenderMode -from ..utils import register_class -from .. import ElectricMotorVisualization - register_class(ConsolePrinter, ElectricMotorVisualization, "ConsolePrinter") register_class(MotorDashboard, ElectricMotorVisualization, "MotorDashboard") diff --git a/src/gym_electric_motor/visualization/console_printer.py b/src/gym_electric_motor/visualization/console_printer.py index 135baae8..df1d756a 100644 --- a/src/gym_electric_motor/visualization/console_printer.py +++ b/src/gym_electric_motor/visualization/console_printer.py @@ -1,6 +1,7 @@ -from ..core import ElectricMotorVisualization import numpy as np +from ..core import ElectricMotorVisualization + class ConsolePrinter(ElectricMotorVisualization): """Prints current training values of the environment on the console. @@ -69,7 +70,7 @@ def render(self): if self._reset: print( f"\nEpisode {self._episode} ", - f"Constraint Violation! " if self._done else "External Reset. ", + "Constraint Violation! " if self._done else "External Reset. ", f"Number of steps: {self._k: 8d} ", f"Cumulative Reward: {self._cum_reward:7.3f}\n", ) diff --git a/src/gym_electric_motor/visualization/motor_dashboard.py b/src/gym_electric_motor/visualization/motor_dashboard.py index e7d14087..7f26ffe8 100644 --- a/src/gym_electric_motor/visualization/motor_dashboard.py +++ b/src/gym_electric_motor/visualization/motor_dashboard.py @@ -1,19 +1,22 @@ +import os +import time +from enum import Enum + +import gymnasium +import matplotlib +import matplotlib.pyplot as plt + from gym_electric_motor.core import ElectricMotorVisualization + from .motor_dashboard_plots import ( - StatePlot, ActionPlot, - RewardPlot, - TimePlot, EpisodePlot, + RewardPlot, + StatePlot, StepPlot, + TimePlot, ) from .render_modes import RenderMode -import matplotlib -import matplotlib.pyplot as plt -import gymnasium -from enum import Enum -import os -import time class MotorDashboardLegacy(ElectricMotorVisualization): @@ -72,7 +75,7 @@ def __init__( Default: None (the already selected style) """ # Basic assertions - assert type(reward_plot) is bool + assert isinstance(reward_plot, bool) assert all(isinstance(ap, (TimePlot, EpisodePlot, StepPlot)) for ap in additional_plots) assert type(update_interval) in [int, float] assert update_interval > 0 @@ -155,9 +158,10 @@ def on_step_end(self, k, state, reference, reward, terminated): def render(self): """Updates the plots every *update cycle* calls of this method.""" - if not (self._time_plot_figure or self._episodic_plot_figure or self._step_plot_figure) and len( - self._plots - ) > 0: + if ( + not (self._time_plot_figure or self._episodic_plot_figure or self._step_plot_figure) + and len(self._plots) > 0 + ): self.initialize() if self._update_render: self._update() @@ -322,7 +326,9 @@ def __init__(self, render_mode=RenderMode.Figure, *args, **kwargs): def set_env(self, env): # This is the only data we need from the environment - myenv = lambda: None + def myenv(): + return None + myenv.physical_system = lambda: None myenv.physical_system.state_names = env.physical_system.state_names myenv.physical_system.tau = env.physical_system.tau diff --git a/src/gym_electric_motor/visualization/motor_dashboard_plots/__init__.py b/src/gym_electric_motor/visualization/motor_dashboard_plots/__init__.py index 71e70c1b..c58909d7 100644 --- a/src/gym_electric_motor/visualization/motor_dashboard_plots/__init__.py +++ b/src/gym_electric_motor/visualization/motor_dashboard_plots/__init__.py @@ -1,5 +1,5 @@ from .action_plot import ActionPlot -from .base_plots import MotorDashboardPlot, TimePlot, EpisodePlot, StepPlot +from .base_plots import EpisodePlot, MotorDashboardPlot, StepPlot, TimePlot from .cumulative_constraint_violation_plot import CumulativeConstraintViolationPlot from .episode_length_plot import EpisodeLengthPlot from .mean_episode_reward_plot import MeanEpisodeRewardPlot diff --git a/src/gym_electric_motor/visualization/motor_dashboard_plots/action_plot.py b/src/gym_electric_motor/visualization/motor_dashboard_plots/action_plot.py index 7eab305c..58d1b647 100644 --- a/src/gym_electric_motor/visualization/motor_dashboard_plots/action_plot.py +++ b/src/gym_electric_motor/visualization/motor_dashboard_plots/action_plot.py @@ -1,5 +1,5 @@ -from gymnasium.spaces import Box, Discrete, MultiDiscrete import numpy as np +from gymnasium.spaces import Box, Discrete, MultiDiscrete from .base_plots import TimePlot diff --git a/src/gym_electric_motor/visualization/motor_dashboard_plots/base_plots.py b/src/gym_electric_motor/visualization/motor_dashboard_plots/base_plots.py index 03df2ffc..e2036181 100644 --- a/src/gym_electric_motor/visualization/motor_dashboard_plots/base_plots.py +++ b/src/gym_electric_motor/visualization/motor_dashboard_plots/base_plots.py @@ -1,5 +1,6 @@ import matplotlib.pyplot as plt import numpy as np + from gym_electric_motor.core import Callback diff --git a/tests/integration_tests/test_environment_execution.py b/tests/integration_tests/test_environment_execution.py index ce44bbcf..19cfb995 100644 --- a/tests/integration_tests/test_environment_execution.py +++ b/tests/integration_tests/test_environment_execution.py @@ -1,6 +1,7 @@ +import numpy as np import pytest + import gym_electric_motor as gem -import numpy as np control_tasks = ["TC", "SC", "CC"] action_types = ["Cont", "Finite"] diff --git a/tests/integration_tests/test_environment_seeding.py b/tests/integration_tests/test_environment_seeding.py index 4bc59e46..e372fea8 100644 --- a/tests/integration_tests/test_environment_seeding.py +++ b/tests/integration_tests/test_environment_seeding.py @@ -1,6 +1,7 @@ +import numpy as np import pytest + import gym_electric_motor as gem -import numpy as np control_tasks = ["TC", "SC", "CC"] action_types = ["Cont", "Finite"] diff --git a/tests/test_physical_systems/test_converters.py b/tests/test_physical_systems/test_converters.py index cc049ab8..3be43f1c 100644 --- a/tests/test_physical_systems/test_converters.py +++ b/tests/test_physical_systems/test_converters.py @@ -8,6 +8,7 @@ from gym_electric_motor.utils import make_module from random import seed, uniform, randint from gymnasium.spaces import Discrete, Box +from gym_electric_motor.physical_systems import * # region first version tests @@ -305,9 +306,9 @@ def discrete_converter_functions_testing( @pytest.mark.parametrize( "converter_type, action_space_n, actions, i_ins, test_voltages", [ - ("Finite-1QC", 2, g_actions_1qc, g_i_ins_1qc, g_1qc_test_voltages), - ("Finite-2QC", 3, g_actions_2qc, g_i_ins_2qc, g_2qc_test_voltages), - ("Finite-4QC", 4, g_actions_4qc, g_i_ins_4qc, g_4qc_test_voltages), + (Finite1QC, 2, g_actions_1qc, g_i_ins_1qc, g_1qc_test_voltages), + (Finite2QC, 3, g_actions_2qc, g_i_ins_2qc, g_2qc_test_voltages), + (Finite4QC, 4, g_actions_4qc, g_i_ins_4qc, g_4qc_test_voltages), ], ) def test_discrete_single_power_electronic_converter( @@ -332,7 +333,7 @@ def test_discrete_single_power_electronic_converter( :return: """ # test with default initialization - converter = make_module(cv.PowerElectronicConverter, converter_type) + converter = converter_type() g_times = g_times_4qc times = g_times * converter._tau discrete_converter_functions_testing( @@ -350,12 +351,8 @@ def test_discrete_single_power_electronic_converter( interlocking_time *= tau # setup converter # initialize converter with given parameter - converter = make_module( - cv.PowerElectronicConverter, - converter_type, - tau=tau, - interlocking_time=interlocking_time, - ) + converter = converter_type(tau=tau, interlocking_time=interlocking_time) + assert converter.reset() == [0.0] # test if reset returns 0.0 # test the conversion function of the converter discrete_converter_functions_testing( @@ -370,76 +367,6 @@ def test_discrete_single_power_electronic_converter( ) -@pytest.mark.parametrize( - "convert, convert_class", - [ - ("Finite-1QC", cv.FiniteOneQuadrantConverter), - ("Finite-2QC", cv.FiniteTwoQuadrantConverter), - ("Finite-4QC", cv.FiniteFourQuadrantConverter), - ], -) -@pytest.mark.parametrize("tau", g_taus) -@pytest.mark.parametrize("interlocking_time", g_interlocking_times) -def test_discrete_single_initializations(convert, convert_class, tau, interlocking_time): - """ - test of both ways of initialization lead to the same result - :param convert: string name of the converter - :param convert_class: class name of the converter - :return: - """ - # test default initialization - converter_1 = make_module(cv.PowerElectronicConverter, convert) - converter_2 = convert_class() - assert converter_1._tau == converter_2._tau - # test with different parameters - interlocking_time *= tau - # initialize converters - converter_1 = make_module( - cv.PowerElectronicConverter, - convert, - tau=tau, - interlocking_time=interlocking_time, - ) - converter_2 = convert_class(tau=tau, interlocking_time=interlocking_time) - parameter = str(tau) + " " + str(interlocking_time) - # test if they are equal - assert converter_1.reset() == converter_2.reset() - assert converter_1.action_space == converter_2.action_space - assert converter_1._tau == converter_2._tau == tau, "Error (tau): " + parameter - assert converter_1._interlocking_time == converter_2._interlocking_time == interlocking_time, ( - "Error (interlocking): " + parameter - ) - - -@pytest.mark.parametrize("tau", g_taus) -@pytest.mark.parametrize("interlocking_time", g_interlocking_times) -def test_discrete_multi_converter_initializations(tau, interlocking_time): - """ - tests different initializations of the converters - :return: - """ - # define all converter - all_single_disc_converter = ["Finite-1QC", "Finite-2QC", "Finite-4QC", "Finite-B6C"] - interlocking_time *= tau - # chose every combination of single converters - for conv_1 in all_single_disc_converter: - for conv_2 in all_single_disc_converter: - converter = make_module( - cv.PowerElectronicConverter, - "Finite-Multi", - tau=tau, - interlocking_time=interlocking_time, - subconverters=[conv_1, conv_2], - ) - # test if both converter have the same parameter - assert converter._sub_converters[0]._tau == converter._sub_converters[1]._tau == tau - assert ( - converter._sub_converters[0]._interlocking_time - == converter._sub_converters[1]._interlocking_time - == interlocking_time - ) - - @pytest.mark.parametrize("tau", g_taus) @pytest.mark.parametrize("interlocking_time", g_interlocking_times) def test_discrete_multi_power_electronic_converter(tau, interlocking_time): @@ -448,34 +375,20 @@ def test_discrete_multi_power_electronic_converter(tau, interlocking_time): :return: """ # define all converter - all_single_disc_converter = ["Finite-1QC", "Finite-2QC", "Finite-4QC", "Finite-B6C"] + all_single_disc_converter = [Finite1QC, Finite2QC, Finite4QC, FiniteB6C] interlocking_time *= tau for conv_0 in all_single_disc_converter: for conv_1 in all_single_disc_converter: - converter = make_module( - cv.PowerElectronicConverter, - "Finite-Multi", - tau=tau, - interlocking_time=interlocking_time, - subconverters=[conv_0, conv_1], - ) - comparable_converter_0 = make_module( - cv.PowerElectronicConverter, - conv_0, - tau=tau, - interlocking_time=interlocking_time, - ) - comparable_converter_1 = make_module( - cv.PowerElectronicConverter, - conv_1, - tau=tau, - interlocking_time=interlocking_time, - ) + converter = FiniteMulti(tau=tau, interlocking_time=interlocking_time, subconverters=[conv_0, conv_1]) + + comparable_converter_0 = conv_0(tau=tau, interlocking_time=interlocking_time) + comparable_converter_1 = conv_1(tau=tau, interlocking_time=interlocking_time) + action_space_n = converter.action_space.nvec assert np.all( converter.reset() - == np.concatenate([[-0.5, -0.5, -0.5] if ("Finite-B6C" == conv) else [0] for conv in [conv_0, conv_1]]) + == np.concatenate([[-0.5, -0.5, -0.5] if (conv is FiniteB6C) else [0] for conv in [conv_0, conv_1]]) ) # test if reset returns 0.0 actions = [[np.random.randint(0, upper_bound) for upper_bound in action_space_n] for _ in range(100)] times = np.arange(100) * tau @@ -486,12 +399,12 @@ def test_discrete_multi_power_electronic_converter(tau, interlocking_time): for time_step in time_steps_1 + time_steps_2: assert time_step in time_steps for time_step in time_steps: - i_in_0 = np.random.uniform(-1, 1, 3) if conv_0 == "Finite-B6C" else [np.random.uniform(-1, 1)] - i_in_1 = np.random.uniform(-1, 1, 3) if conv_1 == "Finite-B6C" else [np.random.uniform(-1, 1)] + i_in_0 = np.random.uniform(-1, 1, 3) if conv_0 is FiniteB6C else [np.random.uniform(-1, 1)] + i_in_1 = np.random.uniform(-1, 1, 3) if conv_1 is FiniteB6C else [np.random.uniform(-1, 1)] i_in = np.concatenate([i_in_0, i_in_1]) voltage = converter.convert(i_in, time_step) # test if the single phase converters work independent and correct for singlephase subsystems - if "Finite-B6C" not in [conv_0, conv_1]: + if FiniteB6C not in [conv_0, conv_1]: voltage_0 = comparable_converter_0.convert(i_in_0, time_step) voltage_1 = comparable_converter_1.convert(i_in_1, time_step) converter.i_sup(i_in) @@ -504,8 +417,7 @@ def test_discrete_multi_power_electronic_converter(tau, interlocking_time): # region continuous converter - -@pytest.mark.parametrize("converter_type", ["Cont-1QC", "Cont-2QC", "Cont-4QC"]) +@pytest.mark.parametrize("converter_type", [Cont1QC, Cont2QC, Cont4QC]) @pytest.mark.parametrize("tau", g_taus) @pytest.mark.parametrize("interlocking_time", g_interlocking_times) def test_continuous_power_electronic_converter(converter_type, tau, interlocking_time): @@ -516,12 +428,7 @@ def test_continuous_power_electronic_converter(converter_type, tau, interlocking """ interlocking_time *= tau # setup converter - converter = make_module( - cv.PowerElectronicConverter, - converter_type, - tau=tau, - interlocking_time=interlocking_time, - ) + converter = converter_type(tau=tau, interlocking_time=interlocking_time) assert converter.reset() == [0.0], "Error reset function" action_space = converter.action_space # take 100 random actions @@ -551,7 +458,7 @@ def continuous_converter_functions_testing(converter, times, interlocking_time, for time_step in time_steps: for i_in in g_i_ins_cont: # test if conversion works - if converter_type == "Cont-1QC": + if converter_type is Cont1QC: i_in = abs(i_in) conversion = converter.convert([i_in], time_step) voltage = comparable_voltage( @@ -584,10 +491,10 @@ def continuous_converter_functions_testing(converter, times, interlocking_time, def comparable_voltage(converter_type, action, i_in, tau, interlocking_time, last_action): voltage = np.array([action]) error = np.array([-np.sign(i_in) / tau * interlocking_time]) - if converter_type == "Cont-2QC": + if converter_type is Cont2QC: voltage += error voltage = max(min(voltage, np.array([1])), np.array([0])) - elif converter_type == "Cont-4QC": + elif converter_type is Cont4QC: voltage_1 = (1 + voltage) / 2 + error voltage_2 = (1 - voltage) / 2 + error voltage_1 = max(min(voltage_1, 1), 0) @@ -605,22 +512,17 @@ def test_continuous_multi_power_electronic_converter(tau, interlocking_time): :return: """ # define all converter - all_single_cont_converter = ["Cont-1QC", "Cont-2QC", "Cont-4QC", "Cont-B6C"] + all_single_cont_converter = [Cont1QC, Cont2QC, Cont4QC, ContB6C] interlocking_time *= tau times = g_times_cont * tau for conv_1 in all_single_cont_converter: for conv_2 in all_single_cont_converter: # setup converter with all possible combinations - converter = make_module( - cv.PowerElectronicConverter, - "Cont-Multi", - tau=tau, - interlocking_time=interlocking_time, - subconverters=[conv_1, conv_2], - ) + converter = ContMulti(tau=tau, interlocking_time=interlocking_time, subconverters=[conv_1, conv_2]) + assert all( converter.reset() - == np.concatenate([[-0.5, -0.5, -0.5] if ("Cont-B6C" == conv) else [0] for conv in [conv_1, conv_2]]) + == np.concatenate([[-0.5, -0.5, -0.5] if (conv is ContB6C) else [0] for conv in [conv_1, conv_2]]) ) action_space = converter.action_space seed(123) @@ -647,11 +549,11 @@ def continuous_multi_converter_functions_testing(converter, times, interlocking_ for time_step in time_steps: for i_in in g_i_ins_cont: # test if conversion works - i_in_0 = [i_in] * 3 if converter_type[0] == "Cont-B6C" else [i_in] - i_in_1 = [-i_in] * 3 if converter_type[1] == "Cont-B6C" else [i_in] - if converter_type[0] == "Cont-1QC": + i_in_0 = [i_in] * 3 if converter_type[0] is ContB6C else [i_in] + i_in_1 = [-i_in] * 3 if converter_type[1] is ContB6C else [i_in] + if converter_type[0] is Cont1QC: i_in_0 = np.abs(i_in_0) - if converter_type[1] == "Cont-1QC": + if converter_type[1] is Cont1QC: i_in_1 = np.abs(i_in_1) conversion = converter.convert(np.concatenate([i_in_0, i_in_1]), time_step) params = parameters + " " + str(i_in) + " " + str(time_step) + " " + str(conversion) @@ -660,7 +562,7 @@ def continuous_multi_converter_functions_testing(converter, times, interlocking_ converter.action_space.high.tolist() >= conversion ), "Error, does not hold limits:" + str(params) # test if the single phase converters work independent and correct for singlephase subsystems - if "Cont-B6C" not in converter_type: + if ContB6C not in converter_type: voltage_0 = comparable_voltage( converter_type[0], action[0], @@ -696,7 +598,7 @@ def test_discrete_b6_bridge(): tau = cf.converter_parameter["tau"] # test default initializations - converter_default_init_1 = make_module(cv.PowerElectronicConverter, "Finite-B6C") + converter_default_init_1 = FiniteB6C() converter_default_init_2 = cv.FiniteB6BridgeConverter() assert converter_default_init_1._tau == 1e-5 for subconverter in converter_default_init_1._sub_converters: @@ -732,7 +634,7 @@ def test_discrete_b6_bridge(): step_counter += 1 # test parametrized converter - converter_init_1 = make_module(cv.PowerElectronicConverter, "Finite-B6C", **cf.converter_parameter) + converter_init_1 = FiniteB6C(**cf.converter_parameter) converter_init_2 = cv.FiniteB6BridgeConverter(**cf.converter_parameter) assert converter_init_1._tau == cf.converter_parameter["tau"] for subconverter in converter_init_1._sub_converters: @@ -797,8 +699,8 @@ def test_discrete_b6_bridge(): def test_continuous_b6_bridge(): - converter_default_init_1 = cv.ContB6BridgeConverter() - converter_default_init_2 = make_module(cv.PowerElectronicConverter, "Cont-B6C") + converter_default_init_1 = ContB6C() + converter_default_init_2 = ContB6C() converters_default = [converter_default_init_1, converter_default_init_2] actions = np.array( [ @@ -864,7 +766,7 @@ def test_continuous_b6_bridge(): ] ) converter_init_1 = cv.ContB6BridgeConverter(**cf.converter_parameter) - converter_init_2 = make_module(cv.PowerElectronicConverter, "Cont-B6C", **cf.converter_parameter) + converter_init_2 = ContB6C(**cf.converter_parameter) converters = [converter_init_1, converter_init_2] for converter in converters: # parameter testing @@ -915,11 +817,6 @@ def test_initialization(self, tau, interlocking_time, **kwargs): assert converter._tau == tau assert converter._interlocking_time == interlocking_time - def test_registered(self): - if self.key != "": - conv = gem.utils.instantiate(cv.PowerElectronicConverter, self.key) - assert type(conv) == self.class_to_test - def test_reset(self, converter): assert converter._action_start_time == 0 assert np.all(converter.reset() == np.array([0])) @@ -1286,8 +1183,8 @@ def converter(self): @pytest.mark.parametrize( "tau, interlocking_time, kwargs", [ - (1, 0.1, {"subconverters": ["Finite-1QC", "Finite-B6C", "Finite-4QC"]}), - (0.1, 0.0, {"subconverters": ["Finite-1QC", "Finite-B6C", "Finite-4QC"]}), + (1, 0.1, {"subconverters": [Finite1QC, FiniteB6C, Finite4QC]}), + (0.1, 0.0, {"subconverters": [Finite1QC, FiniteB6C, Finite4QC]}), ], ) def test_initialization(self, tau, interlocking_time, kwargs): @@ -1308,11 +1205,6 @@ def test_initialization(self, tau, interlocking_time, kwargs): assert sc._interlocking_time == interlocking_time assert sc._tau == tau - def test_registered(self): - dummy_converters = [DummyConverter(), DummyConverter(), DummyConverter()] - conv = gem.utils.instantiate(cv.PowerElectronicConverter, self.key, subconverters=dummy_converters) - assert type(conv) == self.class_to_test - def test_reset(self, converter): u_in = converter.reset() assert u_in == [0.0] * converter.voltages.shape[0] @@ -1333,7 +1225,7 @@ def test_set_action(self, monkeypatch, converter, **_): assert sc2.action_set_time == t def test_default_init(self): - converter = self.class_to_test(subconverters=["Finite-1QC", "Finite-B6C", "Finite-2QC"]) + converter = self.class_to_test(subconverters=[Finite1QC, FiniteB6C, Finite2QC]) assert converter._tau == 1e-5 @pytest.mark.parametrize("i_out", [[0, 6, 2, 7, 9], [1, 0.5, 2], [-1, 1]]) @@ -1379,20 +1271,12 @@ def converter(self): ] ) - def test_registered(self): - dummy_converters = [DummyConverter(), DummyConverter(), DummyConverter()] - dummy_converters[0].action_space = Box(-1, 1, shape=(1,)) - dummy_converters[1].action_space = Box(-1, 1, shape=(3,)) - dummy_converters[2].action_space = Box(0, 1, shape=(1,)) - conv = gem.utils.instantiate(cv.PowerElectronicConverter, self.key, subconverters=dummy_converters) - assert type(conv) == self.class_to_test - assert conv._sub_converters == dummy_converters @pytest.mark.parametrize( "tau, interlocking_time, kwargs", [ - (1, 0.1, {"subconverters": ["Cont-1QC", "Cont-B6C", "Cont-4QC"]}), - (0.1, 0.0, {"subconverters": ["Cont-1QC", "Cont-B6C", "Cont-4QC"]}), + (1, 0.1, {"subconverters": [Cont1QC, ContB6C, Cont4QC]}), + (0.1, 0.0, {"subconverters": [Cont1QC, ContB6C, Cont4QC]}), ], ) def test_initialization(self, tau, interlocking_time, kwargs): diff --git a/tests/test_physical_systems/test_mechanical_loads.py b/tests/test_physical_systems/test_mechanical_loads.py index 24bc3eba..78ba8d22 100644 --- a/tests/test_physical_systems/test_mechanical_loads.py +++ b/tests/test_physical_systems/test_mechanical_loads.py @@ -216,11 +216,6 @@ class TestMechanicalLoad: class_to_test = MechanicalLoad kwargs = {} - def test_registered(self): - if self.key != "": - load = gem.utils.instantiate(MechanicalLoad, self.key, **self.kwargs) - assert type(load) == self.class_to_test - class TestConstSpeedLoad(TestMechanicalLoad): key = "ConstSpeedLoad" diff --git a/tests/test_physical_systems/test_voltage_supplies.py b/tests/test_physical_systems/test_voltage_supplies.py index fe80d132..cb2216ad 100644 --- a/tests/test_physical_systems/test_voltage_supplies.py +++ b/tests/test_physical_systems/test_voltage_supplies.py @@ -8,12 +8,6 @@ class TestVoltageSupply: key = "" class_to_test = vs.VoltageSupply - def test_registered(self): - """If a key is provided, tests if the class can be initialized from registry""" - if self.key != "": - supply = gem.utils.instantiate(vs.VoltageSupply, self.key) - assert type(supply) == self.class_to_test - def test_initialization(self): """Test the initalization and correct setting of values""" u_nominal = 600.0 diff --git a/tests/test_reference_generators/test_reference_generators.py b/tests/test_reference_generators/test_reference_generators.py index 20fe4363..321484d5 100644 --- a/tests/test_reference_generators/test_reference_generators.py +++ b/tests/test_reference_generators/test_reference_generators.py @@ -1,16 +1,19 @@ import gymnasium -from numpy.random import seed +import numpy as np import numpy.random as rd import pytest +from numpy.random import seed + import gym_electric_motor as gem -from tests.test_core import TestReferenceGenerator +import gym_electric_motor.reference_generators.sawtooth_reference_generator as sawrg +import gym_electric_motor.reference_generators.subepisoded_reference_generator as srg +import gym_electric_motor.reference_generators.switched_reference_generator as swrg +import gym_electric_motor.reference_generators.wiener_process_reference_generator as wrg +from gym_electric_motor.core import ReferenceGenerator from gym_electric_motor.reference_generators import ( ConstReferenceGenerator, MultipleReferenceGenerator, ) -from gym_electric_motor.reference_generators.switched_reference_generator import ( - SwitchedReferenceGenerator, -) from gym_electric_motor.reference_generators.sawtooth_reference_generator import ( SawtoothReferenceGenerator, ) @@ -20,26 +23,24 @@ from gym_electric_motor.reference_generators.step_reference_generator import ( StepReferenceGenerator, ) +from gym_electric_motor.reference_generators.subepisoded_reference_generator import ( + SubepisodedReferenceGenerator, +) +from gym_electric_motor.reference_generators.switched_reference_generator import ( + SwitchedReferenceGenerator, +) from gym_electric_motor.reference_generators.triangle_reference_generator import ( TriangularReferenceGenerator, ) from gym_electric_motor.reference_generators.wiener_process_reference_generator import ( WienerProcessReferenceGenerator, ) -from gym_electric_motor.reference_generators.subepisoded_reference_generator import ( - SubepisodedReferenceGenerator, -) from gym_electric_motor.reference_generators.zero_reference_generator import ( ZeroReferenceGenerator, ) -import gym_electric_motor.reference_generators.switched_reference_generator as swrg -import gym_electric_motor.reference_generators.subepisoded_reference_generator as srg -import gym_electric_motor.reference_generators.sawtooth_reference_generator as sawrg -import gym_electric_motor.reference_generators.wiener_process_reference_generator as wrg -from gym_electric_motor.core import ReferenceGenerator -from ..testing_utils import * -import numpy as np +from tests.test_core import TestReferenceGenerator +from ..testing_utils import * # region second version @@ -85,9 +86,7 @@ def monkey_instantiate(self, superclass, instance, **kwargs): :param kwargs: :return: DummyReferenceGenerator """ - assert ( - superclass == ReferenceGenerator - ), "superclass is not ReferenceGenerator as expected" + assert superclass == ReferenceGenerator, "superclass is not ReferenceGenerator as expected" assert ( instance == self._sub_generator[self._monkey_instantiate_counter] ), "Instance is not the expected reference generator" @@ -103,9 +102,7 @@ def monkey_super_set_modules(self, physical_system): :return: """ self._monkey_super_set_modules_counter += 1 - assert ( - physical_system == self._physical_system - ), "physical system is not the expected instance" + assert physical_system == self._physical_system, "physical system is not the expected instance" def monkey_dummy_set_modules(self, physical_system): """ @@ -114,9 +111,7 @@ def monkey_dummy_set_modules(self, physical_system): :return: """ self._monkey_dummy_set_modules_counter += 1 - assert ( - self._physical_system == physical_system - ), "physical system is not the expected instance" + assert self._physical_system == physical_system, "physical system is not the expected instance" def monkey_reset_reference(self): """ @@ -133,16 +128,10 @@ def monkey_dummy_reset(self, initial_state, initial_reference): :return: """ if type(initial_state == self._initial_state) is bool: - assert ( - initial_state == self._initial_state - ), "passed initial state is not the expected one" + assert initial_state == self._initial_state, "passed initial state is not the expected one" else: - assert all( - initial_state == self._initial_state - ), "passed initial state is not the expected one" - assert ( - initial_reference == self._initial_reference - ), "passed initial reference is not the expected one" + assert all(initial_state == self._initial_state), "passed initial state is not the expected one" + assert initial_reference == self._initial_reference, "passed initial reference is not the expected one" self._monkey_dummy_reset_counter += 1 return self._reference, self._reference_observation, self._trajectory @@ -154,9 +143,7 @@ def monkey_dummy_get_reference(self, state, **kwargs): :return: """ assert all(state == self._initial_state), "passed state is not the expected one" - assert ( - self._kwargs == kwargs - ), "Different additional arguments. Keep in mind None and {}." + assert self._kwargs == kwargs, "Different additional arguments. Keep in mind None and {}." return self._reference def monkey_dummy_get_reference_observation(self, state, **kwargs): @@ -167,9 +154,7 @@ def monkey_dummy_get_reference_observation(self, state, **kwargs): :return: """ assert all(state == self._initial_state), "passed state is not the expected one" - assert ( - self._kwargs == kwargs - ), "Different additional arguments. Keep in mind None and {}." + assert self._kwargs == kwargs, "Different additional arguments. Keep in mind None and {}." return self._reference_observation @pytest.mark.parametrize( @@ -200,9 +185,7 @@ def monkey_dummy_get_reference_observation(self, state, **kwargs): "super_episode_length, expected_sel", [((200, 500), (200, 500)), (100, (100, 101)), (500, (500, 501))], ) - def test_init( - self, monkeypatch, setup, sub_generator, p, super_episode_length, expected_sel - ): + def test_init(self, monkeypatch, setup, sub_generator, p, super_episode_length, expected_sel): """ test function for the initialization of a switched reference generator with different combinations of reference generators @@ -217,23 +200,13 @@ def test_init( # setup test scenario self._sub_generator = sub_generator # call function to test - test_object = SwitchedReferenceGenerator( - sub_generator, p=p, super_episode_length=super_episode_length - ) + test_object = SwitchedReferenceGenerator(sub_generator, p=p, super_episode_length=super_episode_length) # verify the expected results - assert len(test_object._sub_generators) == len( - sub_generator - ), "unexpected number of sub generators" - assert ( - test_object._current_episode_length == 0 - ), "The current episode length is not 0." - assert ( - test_object._super_episode_length == expected_sel - ), "super episode length is not as expected" + assert len(test_object._sub_generators) == len(sub_generator), "unexpected number of sub generators" + assert test_object._current_episode_length == 0, "The current episode length is not 0." + assert test_object._super_episode_length == expected_sel, "super episode length is not as expected" assert test_object._current_ref_generator in sub_generator - assert test_object._sub_generators == list( - sub_generator - ), "Other sub generators than expected" + assert test_object._sub_generators == list(sub_generator), "Other sub generators than expected" def test_set_modules(self, monkeypatch, setup): """ @@ -257,21 +230,13 @@ def test_set_modules(self, monkeypatch, setup): test_object = SwitchedReferenceGenerator(sub_generator) self._physical_system = DummyPhysicalSystem(7) - self._physical_system._state_space = gymnasium.spaces.Box( - -1, 1, shape=self._physical_system.state_space.shape - ) + self._physical_system._state_space = gymnasium.spaces.Box(-1, 1, shape=self._physical_system.state_space.shape) # call function to test test_object.set_modules(self._physical_system) # verify the expected results - assert all( - test_object.reference_space.low == expected_space.low - ), "Lower limit of the reference space is not 0" - assert ( - test_object.reference_space.high == expected_space.high - ), "Upper limit of the reference space is not 1" - assert np.all( - test_object._referenced_states == reference_states - ), "referenced states are not the expected ones" + assert all(test_object.reference_space.low == expected_space.low), "Lower limit of the reference space is not 0" + assert test_object.reference_space.high == expected_space.high, "Upper limit of the reference space is not 1" + assert np.all(test_object._referenced_states == reference_states), "referenced states are not the expected ones" @pytest.mark.parametrize("initial_state", [None, [0.8, 0.6, 0.4, 0.7]]) @pytest.mark.parametrize("initial_reference", [None, 0.42]) @@ -285,30 +250,22 @@ def test_reset(self, monkeypatch, setup, initial_state, initial_reference): :return: """ # setup test scenario - monkeypatch.setattr( - SwitchedReferenceGenerator, "_reset_reference", self.monkey_reset_reference - ) + monkeypatch.setattr(SwitchedReferenceGenerator, "_reset_reference", self.monkey_reset_reference) sub_generator = [ SinusoidalReferenceGenerator(), WienerProcessReferenceGenerator(), ] self._sub_generator = sub_generator test_object = SwitchedReferenceGenerator(sub_generator) - monkeypatch.setattr( - test_object._sub_generators[0], "reset", self.monkey_dummy_reset - ) + monkeypatch.setattr(test_object._sub_generators[0], "reset", self.monkey_dummy_reset) self._initial_state = initial_state self._initial_reference = initial_reference # call function to test res_0, res_1, res_2 = test_object.reset(initial_state, initial_reference) # verify the expected results - assert ( - self._monkey_dummy_reset_counter == 1 - ), "reset of sub generators is not called once" + assert self._monkey_dummy_reset_counter == 1, "reset of sub generators is not called once" assert res_0 == self._reference, "reference is not the expected one" - assert all( - res_1 == self._reference_observation - ), "observation is not the expected one " + assert all(res_1 == self._reference_observation), "observation is not the expected one " assert ( sum(sum(abs(res_2 - self._trajectory))) < 1e-6 ), "absolute difference of reference trajectory to the expected is larger than 1e-6" @@ -323,9 +280,7 @@ def test_get_reference(self, monkeypatch): sub_generator = [DummyReferenceGenerator(), DummyReferenceGenerator()] self._sub_generator = sub_generator test_object = SwitchedReferenceGenerator(sub_generator) - monkeypatch.setattr( - DummyReferenceGenerator, "get_reference", self.monkey_dummy_get_reference - ) + monkeypatch.setattr(DummyReferenceGenerator, "get_reference", self.monkey_dummy_get_reference) # call function to test reference = test_object.get_reference(self._initial_state, **self._kwargs) # verify the expected results @@ -335,9 +290,7 @@ def test_get_reference(self, monkeypatch): "k, current_episode_length, k_new, reset_counter", [(10, 15, 11, 0), (10, 10, 11, 1)], ) - def test_get_reference_observation( - self, monkeypatch, k, current_episode_length, k_new, reset_counter - ): + def test_get_reference_observation(self, monkeypatch, k, current_episode_length, k_new, reset_counter): """ test get_reference_observation() :param monkeypatch: @@ -356,33 +309,21 @@ def test_get_reference_observation( test_object = SwitchedReferenceGenerator(sub_generator) monkeypatch.setattr(test_object, "_k", k) - monkeypatch.setattr( - test_object, "_current_episode_length", current_episode_length - ) - monkeypatch.setattr( - test_object, "_reset_reference", self.monkey_reset_reference - ) + monkeypatch.setattr(test_object, "_current_episode_length", current_episode_length) + monkeypatch.setattr(test_object, "_reset_reference", self.monkey_reset_reference) monkeypatch.setattr(test_object, "_reference", self._initial_reference) - monkeypatch.setattr( - test_object._current_ref_generator, "reset", self.monkey_dummy_reset - ) + monkeypatch.setattr(test_object._current_ref_generator, "reset", self.monkey_dummy_reset) monkeypatch.setattr( test_object._current_ref_generator, "get_reference_observation", self.monkey_dummy_get_reference_observation, ) # call function to test - observation = test_object.get_reference_observation( - self._initial_state, **self._kwargs - ) + observation = test_object.get_reference_observation(self._initial_state, **self._kwargs) # verify the expected results - assert all( - observation == self._reference_observation - ), "observation is not the expected one" + assert all(observation == self._reference_observation), "observation is not the expected one" assert test_object._k == k_new, "unexpected new step in the reference" - assert ( - self._monkey_reset_reference_counter == reset_counter - ), "reset_reference called unexpected often" + assert self._monkey_reset_reference_counter == reset_counter, "reset_reference called unexpected often" def test_reset_reference(self, monkeypatch): sub_reference_generators = [ @@ -438,9 +379,7 @@ def test_init(self, monkeypatch, sigma_range): test_object = WienerProcessReferenceGenerator(sigma_range=sigma_range) # verify the expected results - assert ( - test_object._sigma_range == sigma_range - ), "sigma range is not passed correctly" + assert test_object._sigma_range == sigma_range, "sigma range is not passed correctly" def test_reset_reference(self, monkeypatch): sigma_range = 1e-2 @@ -471,12 +410,8 @@ def normal(self, loc=0, scale=1, size=1): self._monkey_get_current_value_counter = 0 self._current_value = np.log10(sigma_range) test_object._reset_reference() - assert ( - sum(abs(test_object._reference - expected_reference)) < 1e-6 - ), "unexpected reference array" - assert ( - self._monkey_get_current_value_counter == 1 - ), "get_current_value() not called once" + assert sum(abs(test_object._reference - expected_reference)) < 1e-6, "unexpected reference array" + assert self._monkey_get_current_value_counter == 1, "get_current_value() not called once" class TestFurtherReferenceGenerator: @@ -501,9 +436,7 @@ def monkey_super_set_modules(self, physical_system): :return: """ self._monkey_super_set_modules_counter += 1 - assert ( - physical_system == self._physical_system - ), "physical system is not the expected instance" + assert physical_system == self._physical_system, "physical system is not the expected instance" def monkey_get_current_value(self, value): self._monkey_get_current_value_counter += 1 @@ -552,15 +485,9 @@ def test_init( **kwargs, ) # verify the expected results - assert ( - test_object._amplitude_range == amplitude_range - ), "amplitude range is not passed correctly" - assert ( - test_object._frequency_range == frequency_range - ), "frequency range is not passed correctly" - assert ( - test_object._offset_range == offset_range - ), "offset range is not passed correctly" + assert test_object._amplitude_range == amplitude_range, "amplitude range is not passed correctly" + assert test_object._frequency_range == frequency_range, "frequency range is not passed correctly" + assert test_object._offset_range == offset_range, "offset range is not passed correctly" @pytest.mark.parametrize( "reference_class", @@ -600,25 +527,15 @@ def test_set_modules( """ # setup test scenario self._physical_system = DummyPhysicalSystem() - monkeypatch.setattr( - SubepisodedReferenceGenerator, "set_modules", self.monkey_super_set_modules - ) - test_object = reference_class( - amplitude_range=amplitude_range, offset_range=offset_range - ) + monkeypatch.setattr(SubepisodedReferenceGenerator, "set_modules", self.monkey_super_set_modules) + test_object = reference_class(amplitude_range=amplitude_range, offset_range=offset_range) monkeypatch.setattr(test_object, "_limit_margin", self._limit_margin) # call function to test test_object.set_modules(self._physical_system) # verify the expected results - assert all( - test_object._amplitude_range == expected_amplitude - ), "amplitude range is not as expected" - assert all( - test_object._offset_range == expected_offset - ), "offset range is not as expected" - assert ( - self._monkey_super_set_modules_counter == 1 - ), "super().set_modules() is not called once" + assert all(test_object._amplitude_range == expected_amplitude), "amplitude range is not as expected" + assert all(test_object._offset_range == expected_offset), "offset range is not as expected" + assert self._monkey_super_set_modules_counter == 1, "super().set_modules() is not called once" @pytest.mark.parametrize( "reference_class, expected_reference, frequency_range", @@ -671,9 +588,7 @@ def test_set_modules( ), ], ) - def test_reset_reference( - self, monkeypatch, reference_class, expected_reference, frequency_range - ): + def test_reset_reference(self, monkeypatch, reference_class, expected_reference, frequency_range): # setup test scenario amplitude_range = 0.8 offset_range = 0.5 @@ -708,9 +623,7 @@ def triangular(self, left=-1, mode=0, right=1): # call function to test test_object._reset_reference() # verify expected results - assert ( - sum(abs(expected_reference - test_object._reference)) < 1e-6 - ), "unexpected reference" + assert sum(abs(expected_reference - test_object._reference)) < 1e-6, "unexpected reference" class TestSubepisodedReferenceGenerator: @@ -792,9 +705,7 @@ def monkey_super_set_modules(self, physical_system): :return: """ self._monkey_super_set_modules_counter += 1 - assert ( - physical_system == self._physical_system - ), "physical system is not the expected instance" + assert physical_system == self._physical_system, "physical system is not the expected instance" @pytest.mark.parametrize("limit_margin", [None, 0.3, (-0.1, 0.8)]) def test_init(self, monkeypatch, limit_margin): @@ -819,19 +730,11 @@ def test_init(self, monkeypatch, limit_margin): limit_margin=limit_margin, ) # verify the expected results - assert ( - test_object._limit_margin == limit_margin - ), "limit margin is not passed correctly" + assert test_object._limit_margin == limit_margin, "limit margin is not passed correctly" assert test_object._reference_value == 0.0, "the reference value is not 0" - assert ( - test_object._reference_state == self._reference_state - ), "reference state is not passed correctly" - assert ( - test_object._episode_len_range == self._episode_length - ), "episode length is not passed correctly" - assert ( - test_object._current_episode_length == self._current_value - ), "current episode length is not as expected" + assert test_object._reference_state == self._reference_state, "reference state is not passed correctly" + assert test_object._episode_len_range == self._episode_length, "episode length is not passed correctly" + assert test_object._current_episode_length == self._current_value, "current episode length is not as expected" assert test_object._k == 0, "current reference step is not 0" @pytest.mark.parametrize( @@ -848,25 +751,15 @@ def test_set_modules(self, monkeypatch, limit_margin, expected_low, expected_hig :return: """ # setup test scenario - monkeypatch.setattr( - ReferenceGenerator, "set_modules", self.monkey_super_set_modules - ) + monkeypatch.setattr(ReferenceGenerator, "set_modules", self.monkey_super_set_modules) monkeypatch.setattr(srg, "set_state_array", self.monkey_state_array) - test_object = SubepisodedReferenceGenerator( - reference_state=self._reference_state, limit_margin=limit_margin - ) + test_object = SubepisodedReferenceGenerator(reference_state=self._reference_state, limit_margin=limit_margin) # call function to test test_object.set_modules(self._physical_system) # verify the expected results - assert ( - self._monkey_super_set_modules_counter == 1 - ), "super().set_modules() is not called once" - assert all( - test_object.reference_space.low == expected_low - ), "lower reference space not as expected" - assert all( - test_object.reference_space.high == expected_high - ), "upper reference space not as expected" + assert self._monkey_super_set_modules_counter == 1, "super().set_modules() is not called once" + assert all(test_object.reference_space.low == expected_low), "lower reference space not as expected" + assert all(test_object.reference_space.high == expected_high), "upper reference space not as expected" test_object._limit_margin = ["test"] # call function to test with pytest.raises(Exception): @@ -877,9 +770,7 @@ def test_set_modules(self, monkeypatch, limit_margin, expected_low, expected_hig "initial_reference, expected_reference", [(np.array([0.2, 0.4, 0.7]), 0.4), (None, 0.0)], ) - def test_reset( - self, monkeypatch, initial_reference, initial_state, expected_reference - ): + def test_reset(self, monkeypatch, initial_reference, initial_state, expected_reference): """ test reset() :param monkeypatch: @@ -890,20 +781,14 @@ def test_reset( """ # setup test scenario monkeypatch.setattr(ReferenceGenerator, "reset", self.monkey_super_reset) - test_object = SubepisodedReferenceGenerator( - reference_state=self._reference_state - ) + test_object = SubepisodedReferenceGenerator(reference_state=self._reference_state) monkeypatch.setattr(test_object, "_referenced_states", self._referenced_states) # call function to test reference = test_object.reset(initial_state, initial_reference) # verify the expected results assert all(reference == self._reference), "reference not as expected" - assert ( - test_object._current_episode_length == -1 - ), "current episode length is not -1" - assert ( - test_object._reference_value == expected_reference - ), "unexpected reference value" + assert test_object._current_episode_length == -1, "current episode length is not -1" + assert test_object._reference_value == expected_reference, "unexpected reference value" assert self._monkey_super_reset_counter == 1, "super().reset() not called once" def test_get_reference(self, monkeypatch): @@ -926,9 +811,7 @@ def test_get_reference(self, monkeypatch): [(8, 0.6, (10, 0)), (10, 0.4, (35, 1))], ) # setup test scenario - def test_get_reference_observation( - self, monkeypatch, k, expected_reference, expected_parameter - ): + def test_get_reference_observation(self, monkeypatch, k, expected_reference, expected_parameter): """ test get_reference_observation() :param monkeypatch: @@ -948,9 +831,7 @@ def test_get_reference_observation( self.monkey_reset_reference, ) self._value_range = self._episode_length - test_object = SubepisodedReferenceGenerator( - episode_lengths=self._episode_length - ) + test_object = SubepisodedReferenceGenerator(episode_lengths=self._episode_length) monkeypatch.setattr(test_object, "_reference", self._reference_trajectory) monkeypatch.setattr(test_object, "_k", k) monkeypatch.setattr(test_object, "_current_episode_length", 10) @@ -958,9 +839,7 @@ def test_get_reference_observation( reference = test_object.get_reference_observation() # verify the expected results assert reference == np.array([expected_reference]), "unexpected reference" - assert ( - test_object._current_episode_length == expected_parameter[0] - ), "unexpected current episode length" + assert test_object._current_episode_length == expected_parameter[0], "unexpected current episode length" assert ( self._monkey_reset_reference_counter == expected_parameter[1] ), "unexpected number of calls of reset_reference, depends on the setting" @@ -990,9 +869,7 @@ def uniform(self, mu=0, sigma=1): monkeypatch.setattr(test_object, "_random_generator", MonkeyUniformGenerator()) val = test_object._get_current_value(value_range) # verify expected results - assert ( - abs(val - expected_value) < 1e-6 - ), "unexpected value from get_current_value()." + assert abs(val - expected_value) < 1e-6, "unexpected value from get_current_value()." class TestConstReferenceGenerator(TestReferenceGenerator): @@ -1005,20 +882,14 @@ def physical_system(self): @pytest.fixture def reference_generator(self, physical_system): - rg = ConstReferenceGenerator( - reference_state=physical_system.state_names[1], reference_value=1.0 - ) + rg = ConstReferenceGenerator(reference_state=physical_system.state_names[1], reference_value=1.0) rg.set_modules(physical_system) return rg @pytest.mark.parametrize("reference_value, reference_state", [(0.8, "abc")]) def test_initialization(self, reference_value, reference_state): - rg = ConstReferenceGenerator( - reference_value=reference_value, reference_state=reference_state - ) - assert rg.reference_space == Box( - np.array([reference_value]), np.array([reference_value]), dtype=np.float64 - ) + rg = ConstReferenceGenerator(reference_value=reference_value, reference_state=reference_state) + assert rg.reference_space == Box(np.array([reference_value]), np.array([reference_value]), dtype=np.float64) assert rg._reference_value == reference_value assert rg._reference_state == reference_state @@ -1033,11 +904,6 @@ def test_get_reference(self, reference_generator): def test_get_reference_observation(self, reference_generator): assert all(reference_generator.get_reference_observation() == np.array([1])) - def test_registered(self): - if self.key != "": - rg = gem.utils.instantiate(ReferenceGenerator, self.key) - assert type(rg) == self.class_to_test - class TestMultipleReferenceGenerator(TestReferenceGenerator): key = "MultipleReference" @@ -1081,16 +947,7 @@ def test_get_reference(self, reference_generator): assert all(reference_generator.get_reference(0) == np.array([0, 1, 1])) def test_reference_observation(self, reference_generator): - assert all( - reference_generator.get_reference_observation(0) == np.array([-1, 1]) - ) - - def test_registered(self): - if self.key != "": - rg = gem.utils.instantiate( - ReferenceGenerator, self.key, sub_generators=[DummyReferenceGenerator] - ) - assert type(rg) == self.class_to_test + assert all(reference_generator.get_reference_observation(0) == np.array([-1, 1])) # endregion diff --git a/tests/test_reward_functions/test_reward_functions.py b/tests/test_reward_functions/test_reward_functions.py index 7ebff754..8de2abc5 100644 --- a/tests/test_reward_functions/test_reward_functions.py +++ b/tests/test_reward_functions/test_reward_functions.py @@ -2,10 +2,11 @@ import pytest from gym_electric_motor import RewardFunction + from ..testing_utils import ( - DummyReferenceGenerator, - DummyPhysicalSystem, DummyConstraintMonitor, + DummyPhysicalSystem, + DummyReferenceGenerator, ) diff --git a/tests/test_reward_functions/test_weighted_sum_of_errors.py b/tests/test_reward_functions/test_weighted_sum_of_errors.py index ccc02af0..969f3732 100644 --- a/tests/test_reward_functions/test_weighted_sum_of_errors.py +++ b/tests/test_reward_functions/test_weighted_sum_of_errors.py @@ -1,15 +1,16 @@ -import pytest import numpy as np +import pytest from gym_electric_motor.reward_functions.weighted_sum_of_errors import ( WeightedSumOfErrors, ) -from .test_reward_functions import TestRewardFunction + from ..testing_utils import ( + DummyConstraintMonitor, DummyPhysicalSystem, DummyReferenceGenerator, - DummyConstraintMonitor, ) +from .test_reward_functions import TestRewardFunction class TestWeightedSumOfErrors(TestRewardFunction): @@ -18,9 +19,7 @@ class TestWeightedSumOfErrors(TestRewardFunction): @pytest.fixture def reward_function(self): rf = WeightedSumOfErrors() - rf.set_modules( - DummyPhysicalSystem(), DummyReferenceGenerator(), DummyConstraintMonitor() - ) + rf.set_modules(DummyPhysicalSystem(), DummyReferenceGenerator(), DummyConstraintMonitor()) return rf @pytest.mark.parametrize( @@ -110,13 +109,9 @@ def test_violation_reward(self, ps, rg, cm, violation_reward, gamma, expected_vr ) @pytest.mark.parametrize("rg", [DummyReferenceGenerator()]) @pytest.mark.parametrize("cm", [DummyConstraintMonitor()]) - def test_normed_reward_weights( - self, ps, rg, cm, reward_weights, normed_rw, expected_rw - ): + def test_normed_reward_weights(self, ps, rg, cm, reward_weights, normed_rw, expected_rw): rg.set_modules(ps) - rf = self.class_to_test( - reward_weights=reward_weights, normed_reward_weights=normed_rw - ) + rf = self.class_to_test(reward_weights=reward_weights, normed_reward_weights=normed_rw) rf.set_modules(ps, rg, cm) assert all(rf._reward_weights == expected_rw) @@ -218,11 +213,6 @@ def test_reward( expected_rw, ): rg.set_modules(ps) - rf = self.class_to_test( - reward_weights=reward_weights, bias=bias, violation_reward=violation_reward - ) + rf = self.class_to_test(reward_weights=reward_weights, bias=bias, violation_reward=violation_reward) rf.set_modules(ps, rg, cm) - assert ( - rf.reward(state, reference, violation_degree=violation_degree) - == expected_rw - ) + assert rf.reward(state, reference, violation_degree=violation_degree) == expected_rw diff --git a/tests/test_utils.py b/tests/test_utils.py index e2a3e863..c61329f7 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -5,85 +5,6 @@ import numpy as np -def mock_make_module(superclass, instance, **kwargs): - return superclass, instance, kwargs - - -def test_registry(monkeypatch): - registry = {} - monkeypatch.setattr(utils, "_registry", registry) - utils.register_superclass(core.PhysicalSystem) - assert registry[core.PhysicalSystem] == {} - - utils.register_class( - dummies.DummyPhysicalSystem, core.PhysicalSystem, "DummySystem" - ) - assert registry[core.PhysicalSystem] == {"DummySystem": dummies.DummyPhysicalSystem} - - with pytest.raises(KeyError): - _ = registry[dummies.DummyPhysicalSystem] - - with pytest.raises(KeyError): - _ = registry[core.PhysicalSystem]["nonExistingKey"] - - -def test_make_module(monkeypatch): - registry = {} - monkeypatch.setattr(utils, "_registry", registry) - utils.register_superclass(core.PhysicalSystem) - utils.register_class( - dummies.DummyPhysicalSystem, core.PhysicalSystem, "DummySystem" - ) - kwargs = dict(a=0, b=5) - dummy_sys = utils.make_module(core.PhysicalSystem, "DummySystem", **kwargs) - assert isinstance(dummy_sys, dummies.DummyPhysicalSystem) - assert dummy_sys.kwargs == kwargs - - with pytest.raises(Exception): - _ = registry[core.ReferenceGenerator]["DummySystem"] - - with pytest.raises(Exception): - _ = registry[core.PhysicalSystem]["NonExistingKey"] - - -def test_instantiate(monkeypatch): - monkeypatch.setattr(utils, "make_module", mock_make_module) - kwargs = dict(a=0, b=5) - phys_sys = dummies.DummyPhysicalSystem(**kwargs) - - # Test object instantiation - assert utils.instantiate(core.PhysicalSystem, phys_sys) == phys_sys - - # Test class instantiation - instance = utils.instantiate( - core.PhysicalSystem, dummies.DummyPhysicalSystem, **kwargs - ) - assert type(instance) == dummies.DummyPhysicalSystem - assert instance.kwargs == kwargs - - # Test string instantiation - key = "DummyKey" - assert utils.instantiate(core.PhysicalSystem, key, **kwargs) == ( - core.PhysicalSystem, - key, - kwargs, - ) - - # Test Exceptions - with pytest.raises(Exception): - utils.instantiate( - core.ReferenceGenerator, dummies.DummyPhysicalSystem, **kwargs - ) - with pytest.raises(Exception): - utils.instantiate( - dummies.DummyPhysicalSystem, core.ReferenceGenerator, **kwargs - ) - with pytest.raises(Exception): - utils.instantiate( - dummies.DummyPhysicalSystem(), core.ReferenceGenerator, **kwargs - ) - - @pytest.mark.parametrize("state_names", [["a", "b", "c", "d"]]) @pytest.mark.parametrize( "state_dict, state_array, target", From 19db5e4df096a54f56b6287b13c0fd3599d61445 Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Sun, 4 Feb 2024 16:21:28 +0100 Subject: [PATCH 20/51] working test --- src/gym_electric_motor/utils.py | 80 +++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/src/gym_electric_motor/utils.py b/src/gym_electric_motor/utils.py index cb4b8bcc..23fc71eb 100644 --- a/src/gym_electric_motor/utils.py +++ b/src/gym_electric_motor/utils.py @@ -56,6 +56,86 @@ def set_state_array(input_values, state_names): return state_array +def initialize(base_class, arg, default_class, default_args): + if arg is None: + return default_class(**default_args) + elif isinstance(arg, base_class): + return arg + elif isinstance(arg, str): + return _registry[base_class][arg]() + elif isinstance(arg, dict): + default_args.update(arg) + return default_class(**default_args) + + +def instantiate(superclass, instance, **kwargs): + """ + Instantiation of an instance that inherits from the passed superclass. + + The instance can be passed as a key-string, a class pointer or an already instantiated object. In the latter case + the same object will be simply returned. If a string is passed the corresponding class will be taken from the + registry and instantiated with the given kwargs. If a class pointer is passed, then the class is instantiated with + with given kwargs, directly. + + Args: + superclass(class): Superclass pointer for registry access + instance(str, class, object): Instance to instantiate + kwargs: Arguments for the instantiation of the object + + Returns: + An instantiated object. + """ + if type(instance) is type and issubclass(instance, superclass): + return instance(**kwargs) + elif isinstance(instance, superclass): + return instance + elif isinstance(instance, str): + return make_module(superclass, instance, **kwargs) + else: + raise Exception("Instantiation Error.") + + +# Registry dictionary that stores the keys to instantiate the components with the keystrings +_registry = {} + + +def make_module(superclass, keystring, **kwargs): + """ + Instantiation by an object that is specified by the key-string an its superclass from the registry. + + Args: + superclass(class): Superclass pointer for registry access + keystring(str): String to access the class pointer in the registry. + kwargs: Arguments for the instantiation of the object. + + Returns: + An instantiated object. + """ + try: + return _registry[superclass][keystring](**kwargs) + except KeyError: + raise Exception(f"Key {keystring} or baseclass {superclass.__name__} not found in the registry.") + + +def register_superclass(superclass): + """ + Method to register a new superclass that can contain several key-strings for instantiation in the registry. + + Basically, all superclasses in GEM are already registered like the Physical Systems, Reference Generators, ... + """ + return + _registry[superclass] = {} + + +def register_class(subclass, superclass, keystring): + """ + Method to register a new class with a key-string into the registry of the superclass + to be instantiable with the key-string. + """ + return + _registry[superclass][keystring] = subclass + + def update_parameter_dict(source_dict, update_dict, copy=True): """Merges two dictionaries (source and update) together. From 5c8654652e51e56c1829f468bac8e87f2b2d2489 Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Sun, 4 Feb 2024 17:36:15 +0100 Subject: [PATCH 21/51] removed class init hack --- src/gym_electric_motor/__init__.py | 7 -- src/gym_electric_motor/core.py | 27 ++++-- .../physical_systems/__init__.py | 50 ---------- .../physical_systems/converters.py | 2 - .../reference_generators/__init__.py | 11 --- .../multiple_reference_generator.py | 14 ++- .../switched_reference_generator.py | 1 - .../reward_functions/__init__.py | 3 - src/gym_electric_motor/utils.py | 96 ++++--------------- .../visualization/__init__.py | 4 - .../test_physical_systems/test_converters.py | 1 - tests/testing_utils.py | 3 +- 12 files changed, 45 insertions(+), 174 deletions(-) diff --git a/src/gym_electric_motor/__init__.py b/src/gym_electric_motor/__init__.py index 78d805db..544febaa 100644 --- a/src/gym_electric_motor/__init__.py +++ b/src/gym_electric_motor/__init__.py @@ -21,13 +21,6 @@ SimulationEnvironment, ) from .random_component import RandomComponent -from .utils import register_superclass - -register_superclass(RewardFunction) -register_superclass(ElectricMotorVisualization) -register_superclass(ReferenceGenerator) -register_superclass(PhysicalSystem) - make = ElectricMotorEnvironment.make diff --git a/src/gym_electric_motor/core.py b/src/gym_electric_motor/core.py index fe01f5f8..57e09117 100644 --- a/src/gym_electric_motor/core.py +++ b/src/gym_electric_motor/core.py @@ -31,7 +31,6 @@ import gym_electric_motor as gem from .constraints import Constraint, LimitConstraint -from .utils import instantiate @dataclass @@ -231,15 +230,27 @@ def __init__( self.sim.tau = physical_system.tau - self._physical_system = instantiate(PhysicalSystem, physical_system, **kwargs) - self._reference_generator = instantiate(ReferenceGenerator, reference_generator, **kwargs) - self._reward_function = instantiate(RewardFunction, reward_function, **kwargs) + # TODO physical_system etc should be initialized by the caller + self._physical_system = physical_system(**kwargs) if isinstance(physical_system, type) else physical_system + self._reference_generator = ( + reference_generator(**kwargs) if isinstance(reference_generator, type) else reference_generator + ) + self._reward_function = reward_function(**kwargs) if isinstance(reward_function, type) else reward_function + if isinstance(visualization, str) or isinstance(visualization, ElectricMotorVisualization): visualization = [visualization] if visualization is None: visualization = [] visualizations = list(visualization) - self._visualizations = [instantiate(ElectricMotorVisualization, visu, **kwargs) for visu in visualizations] + + self._visualizations = [] + for visu in visualizations: + if isinstance(visu, str): + raise Exception + if isinstance(visu, type): + visu = visu(**kwargs) + self._visualizations.append(visu) + if isinstance(constraints, ConstraintMonitor): cm = constraints else: @@ -251,9 +262,9 @@ def __init__( # Announcement of the modules among each other for physical_system_wrapper in physical_system_wrappers: self._physical_system = physical_system_wrapper.set_physical_system(self._physical_system) - self._reference_generator.set_modules(self.physical_system) - self._constraint_monitor.set_modules(self.physical_system) - self._reward_function.set_modules(self.physical_system, self._reference_generator, self._constraint_monitor) + self._reference_generator.set_modules(self._physical_system) + self._constraint_monitor.set_modules(self._physical_system) + self._reward_function.set_modules(self._physical_system, self._reference_generator, self._constraint_monitor) # Initialization of the state filter and the spaces state_filter = state_filter or self._physical_system.state_names diff --git a/src/gym_electric_motor/physical_systems/__init__.py b/src/gym_electric_motor/physical_systems/__init__.py index a9f3afa3..b86a8037 100644 --- a/src/gym_electric_motor/physical_systems/__init__.py +++ b/src/gym_electric_motor/physical_systems/__init__.py @@ -1,5 +1,4 @@ from ..core import PhysicalSystem -from ..utils import register_class, register_superclass from .converters import ( ContB6BridgeConverter, ContFourQuadrantConverter, @@ -67,52 +66,3 @@ RCVoltageSupply, VoltageSupply, ) - -register_superclass(PowerElectronicConverter) -register_superclass(MechanicalLoad) -register_superclass(ElectricMotor) -register_superclass(OdeSolver) -register_superclass(VoltageSupply) - - -register_class(DcMotorSystem, PhysicalSystem, "DcMotorSystem") -register_class(SynchronousMotorSystem, PhysicalSystem, "SyncMotorSystem") -register_class(SquirrelCageInductionMotorSystem, PhysicalSystem, "SquirrelCageInductionMotorSystem") -register_class(DoublyFedInductionMotorSystem, PhysicalSystem, "DoublyFedInductionMotorSystem") - -register_class(FiniteOneQuadrantConverter, PowerElectronicConverter, "Finite-1QC") -register_class(ContOneQuadrantConverter, PowerElectronicConverter, "Cont-1QC") -register_class(FiniteTwoQuadrantConverter, PowerElectronicConverter, "Finite-2QC") -register_class(ContTwoQuadrantConverter, PowerElectronicConverter, "Cont-2QC") -register_class(FiniteFourQuadrantConverter, PowerElectronicConverter, "Finite-4QC") -register_class(ContFourQuadrantConverter, PowerElectronicConverter, "Cont-4QC") -register_class(FiniteMultiConverter, PowerElectronicConverter, "Finite-Multi") -register_class(ContMultiConverter, PowerElectronicConverter, "Cont-Multi") -register_class(FiniteB6BridgeConverter, PowerElectronicConverter, "Finite-B6C") -register_class(ContB6BridgeConverter, PowerElectronicConverter, "Cont-B6C") -register_class(NoConverter, PowerElectronicConverter, "NoConverter") - -register_class(PolynomialStaticLoad, MechanicalLoad, "PolyStaticLoad") -register_class(ConstantSpeedLoad, MechanicalLoad, "ConstSpeedLoad") -register_class(ExternalSpeedLoad, MechanicalLoad, "ExtSpeedLoad") - -register_class(EulerSolver, OdeSolver, "euler") -register_class(ScipyOdeSolver, OdeSolver, "scipy.ode") -register_class(ScipySolveIvpSolver, OdeSolver, "scipy.solve_ivp") -register_class(ScipyOdeIntSolver, OdeSolver, "scipy.odeint") - -register_class(DcSeriesMotor, ElectricMotor, "DcSeries") -register_class(DcPermanentlyExcitedMotor, ElectricMotor, "DcPermEx") -register_class(DcExternallyExcitedMotor, ElectricMotor, "DcExtEx") -register_class(DcShuntMotor, ElectricMotor, "DcShunt") -register_class(PermanentMagnetSynchronousMotor, ElectricMotor, "PMSM") -register_class(ExternallyExcitedSynchronousMotor, ElectricMotor, "EESM") -register_class(SynchronousReluctanceMotor, ElectricMotor, "SynRM") -register_class(SquirrelCageInductionMotor, ElectricMotor, "SCIM") -register_class(DoublyFedInductionMotor, ElectricMotor, "DFIM") - - -register_class(IdealVoltageSupply, VoltageSupply, "IdealVoltageSupply") -register_class(RCVoltageSupply, VoltageSupply, "RCVoltageSupply") -register_class(AC1PhaseSupply, VoltageSupply, "AC1PhaseSupply") -register_class(AC3PhaseSupply, VoltageSupply, "AC3PhaseSupply") diff --git a/src/gym_electric_motor/physical_systems/converters.py b/src/gym_electric_motor/physical_systems/converters.py index 05d93b87..2327ec4b 100644 --- a/src/gym_electric_motor/physical_systems/converters.py +++ b/src/gym_electric_motor/physical_systems/converters.py @@ -1,8 +1,6 @@ import numpy as np from gymnasium.spaces import Box, Discrete, MultiDiscrete -from ..utils import instantiate - class PowerElectronicConverter: """ diff --git a/src/gym_electric_motor/reference_generators/__init__.py b/src/gym_electric_motor/reference_generators/__init__.py index 216123da..451fa64a 100644 --- a/src/gym_electric_motor/reference_generators/__init__.py +++ b/src/gym_electric_motor/reference_generators/__init__.py @@ -1,5 +1,4 @@ from ..core import ReferenceGenerator -from ..utils import register_class from .const_reference_generator import ConstReferenceGenerator from .laplace_process_reference_generator import LaplaceProcessReferenceGenerator from .multiple_reference_generator import MultipleReferenceGenerator @@ -11,13 +10,3 @@ from .triangle_reference_generator import TriangularReferenceGenerator from .wiener_process_reference_generator import WienerProcessReferenceGenerator from .zero_reference_generator import ZeroReferenceGenerator - -register_class(WienerProcessReferenceGenerator, ReferenceGenerator, "WienerProcessReference") -register_class(SwitchedReferenceGenerator, ReferenceGenerator, "SwitchedReference") -register_class(StepReferenceGenerator, ReferenceGenerator, "StepReference") -register_class(SinusoidalReferenceGenerator, ReferenceGenerator, "SinusReference") -register_class(TriangularReferenceGenerator, ReferenceGenerator, "TriangleReference") -register_class(SawtoothReferenceGenerator, ReferenceGenerator, "SawtoothReference") -register_class(ConstReferenceGenerator, ReferenceGenerator, "ConstReference") -register_class(MultipleReferenceGenerator, ReferenceGenerator, "MultipleReference") -register_class(SubepisodedReferenceGenerator, ReferenceGenerator, "SubepisodedReference") diff --git a/src/gym_electric_motor/reference_generators/multiple_reference_generator.py b/src/gym_electric_motor/reference_generators/multiple_reference_generator.py index 60165f69..b0d9d01f 100644 --- a/src/gym_electric_motor/reference_generators/multiple_reference_generator.py +++ b/src/gym_electric_motor/reference_generators/multiple_reference_generator.py @@ -3,7 +3,6 @@ from ..core import ReferenceGenerator from ..random_component import RandomComponent -from ..utils import instantiate class MultipleReferenceGenerator(ReferenceGenerator, RandomComponent): @@ -29,10 +28,15 @@ def __init__(self, sub_generators, sub_args=None, **kwargs): sub_arguments = sub_args else: sub_arguments = [kwargs] * len(sub_generators) - self._sub_generators = [ - instantiate(ReferenceGenerator, sub_generator, **sub_arg) - for sub_generator, sub_arg in zip(sub_generators, sub_arguments) - ] + + self._sub_generators = [] + for sub_generator, sub_arg in zip(sub_generators, sub_arguments): + if isinstance(sub_generator, str): + raise Exception + if isinstance(sub_generator, type): + sub_generator = sub_generator(**sub_arg) + self._sub_generators.append(sub_generator) + self._reference_names = [] for sub_gen in self._sub_generators: self._reference_names += sub_gen.reference_names diff --git a/src/gym_electric_motor/reference_generators/switched_reference_generator.py b/src/gym_electric_motor/reference_generators/switched_reference_generator.py index 1bab4a57..bb6d5852 100644 --- a/src/gym_electric_motor/reference_generators/switched_reference_generator.py +++ b/src/gym_electric_motor/reference_generators/switched_reference_generator.py @@ -3,7 +3,6 @@ from ..core import ReferenceGenerator from ..random_component import RandomComponent -from ..utils import instantiate class SwitchedReferenceGenerator(ReferenceGenerator, RandomComponent): diff --git a/src/gym_electric_motor/reward_functions/__init__.py b/src/gym_electric_motor/reward_functions/__init__.py index 85d47997..b8ab633c 100644 --- a/src/gym_electric_motor/reward_functions/__init__.py +++ b/src/gym_electric_motor/reward_functions/__init__.py @@ -1,5 +1,2 @@ from ..core import RewardFunction -from ..utils import register_class from .weighted_sum_of_errors import WeightedSumOfErrors - -register_class(WeightedSumOfErrors, RewardFunction, "WSE") diff --git a/src/gym_electric_motor/utils.py b/src/gym_electric_motor/utils.py index 23fc71eb..4ac6b719 100644 --- a/src/gym_electric_motor/utils.py +++ b/src/gym_electric_motor/utils.py @@ -2,6 +2,22 @@ import numpy as np +# Dummy for refactoring TODO +def initialize(base_class, arg, default_class, default_args): + if arg is None: + return default_class(**default_args) + if isinstance(arg, type): + raise Exception("Need init") + elif isinstance(arg, base_class): + return arg + elif isinstance(arg, str): + raise Exception + elif isinstance(arg, dict): + raise Exception + default_args.update(arg) + return default_class(**default_args) + + def state_dict_to_state_array(state_dict, state_array, state_names): """ Mapping of a passed state dictionary to a fitting state array. @@ -56,86 +72,6 @@ def set_state_array(input_values, state_names): return state_array -def initialize(base_class, arg, default_class, default_args): - if arg is None: - return default_class(**default_args) - elif isinstance(arg, base_class): - return arg - elif isinstance(arg, str): - return _registry[base_class][arg]() - elif isinstance(arg, dict): - default_args.update(arg) - return default_class(**default_args) - - -def instantiate(superclass, instance, **kwargs): - """ - Instantiation of an instance that inherits from the passed superclass. - - The instance can be passed as a key-string, a class pointer or an already instantiated object. In the latter case - the same object will be simply returned. If a string is passed the corresponding class will be taken from the - registry and instantiated with the given kwargs. If a class pointer is passed, then the class is instantiated with - with given kwargs, directly. - - Args: - superclass(class): Superclass pointer for registry access - instance(str, class, object): Instance to instantiate - kwargs: Arguments for the instantiation of the object - - Returns: - An instantiated object. - """ - if type(instance) is type and issubclass(instance, superclass): - return instance(**kwargs) - elif isinstance(instance, superclass): - return instance - elif isinstance(instance, str): - return make_module(superclass, instance, **kwargs) - else: - raise Exception("Instantiation Error.") - - -# Registry dictionary that stores the keys to instantiate the components with the keystrings -_registry = {} - - -def make_module(superclass, keystring, **kwargs): - """ - Instantiation by an object that is specified by the key-string an its superclass from the registry. - - Args: - superclass(class): Superclass pointer for registry access - keystring(str): String to access the class pointer in the registry. - kwargs: Arguments for the instantiation of the object. - - Returns: - An instantiated object. - """ - try: - return _registry[superclass][keystring](**kwargs) - except KeyError: - raise Exception(f"Key {keystring} or baseclass {superclass.__name__} not found in the registry.") - - -def register_superclass(superclass): - """ - Method to register a new superclass that can contain several key-strings for instantiation in the registry. - - Basically, all superclasses in GEM are already registered like the Physical Systems, Reference Generators, ... - """ - return - _registry[superclass] = {} - - -def register_class(subclass, superclass, keystring): - """ - Method to register a new class with a key-string into the registry of the superclass - to be instantiable with the key-string. - """ - return - _registry[superclass][keystring] = subclass - - def update_parameter_dict(source_dict, update_dict, copy=True): """Merges two dictionaries (source and update) together. diff --git a/src/gym_electric_motor/visualization/__init__.py b/src/gym_electric_motor/visualization/__init__.py index 78f7da84..c6b8e0e7 100644 --- a/src/gym_electric_motor/visualization/__init__.py +++ b/src/gym_electric_motor/visualization/__init__.py @@ -1,8 +1,4 @@ from ..core import ElectricMotorVisualization -from ..utils import register_class from .console_printer import ConsolePrinter from .motor_dashboard import MotorDashboard from .render_modes import RenderMode - -register_class(ConsolePrinter, ElectricMotorVisualization, "ConsolePrinter") -register_class(MotorDashboard, ElectricMotorVisualization, "MotorDashboard") diff --git a/tests/test_physical_systems/test_converters.py b/tests/test_physical_systems/test_converters.py index 3be43f1c..95623509 100644 --- a/tests/test_physical_systems/test_converters.py +++ b/tests/test_physical_systems/test_converters.py @@ -5,7 +5,6 @@ import pytest import numpy as np import tests.conf as cf -from gym_electric_motor.utils import make_module from random import seed, uniform, randint from gymnasium.spaces import Discrete, Box from gym_electric_motor.physical_systems import * diff --git a/tests/testing_utils.py b/tests/testing_utils.py index ef08fafa..1a348409 100644 --- a/tests/testing_utils.py +++ b/tests/testing_utils.py @@ -1,6 +1,6 @@ from tests.conf import * from gym_electric_motor.physical_systems import * -from gym_electric_motor.utils import make_module, set_state_array +from gym_electric_motor.utils import set_state_array from gym_electric_motor import ( ReferenceGenerator, RewardFunction, @@ -21,7 +21,6 @@ from gymnasium.spaces import Box, Discrete from scipy.integrate import ode from tests.conf import system, jacobian, permex_motor_parameter -from gym_electric_motor.utils import instantiate from gym_electric_motor.core import Callback From 94585a0790b8ebd092f68b331c88545a6bca6416 Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Sun, 4 Feb 2024 17:40:10 +0100 Subject: [PATCH 22/51] changed one env to pythonic init, but not worth to do it for all envs --- .../cont_tc_series_dc_env.py | 50 ++++++------------- src/gym_electric_motor/utils.py | 2 +- 2 files changed, 15 insertions(+), 37 deletions(-) diff --git a/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_tc_series_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_tc_series_dc_env.py index b9b6ba2f..83011407 100644 --- a/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_tc_series_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_tc_series_dc_env.py @@ -89,14 +89,14 @@ class ContTorqueControlDcSeriesMotorEnv(ElectricMotorEnvironment): def __init__( self, - supply=None, - converter=None, - motor=None, - load=None, - ode_solver=None, - reward_function=None, - reference_generator=None, - visualization=None, + supply=ps.IdealVoltageSupply(u_nominal=60.0), + converter=ps.ContFourQuadrantConverter(), + motor=ps.DcSeriesMotor(), + load=ps.ConstantSpeedLoad(omega_fixed=100.0), + ode_solver=ps.ScipyOdeSolver(), + reward_function=WeightedSumOfErrors(reward_weights=dict(torque=1.0)), + reference_generator=WienerProcessReferenceGenerator(reference_state="torque"), + visualization=MotorDashboard(state_plots=("torque",), action_plots="all"), state_filter=None, callbacks=(), constraints=("i",), @@ -142,37 +142,15 @@ def __init__( The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = DcMotorSystem( - supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), - converter=initialize( - ps.PowerElectronicConverter, - converter, - ps.ContFourQuadrantConverter, - dict(), - ), - motor=initialize(ps.ElectricMotor, motor, ps.DcSeriesMotor, dict()), - load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), - ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), + supply=supply, + converter=converter, + motor=motor, + load=load, + ode_solver=ode_solver, calc_jacobian=calc_jacobian, tau=tau, ) - reference_generator = initialize( - ReferenceGenerator, - reference_generator, - WienerProcessReferenceGenerator, - dict(reference_state="torque"), - ) - reward_function = initialize( - RewardFunction, - reward_function, - WeightedSumOfErrors, - dict(reward_weights=dict(torque=1.0)), - ) - visualization = initialize( - ElectricMotorVisualization, - visualization, - MotorDashboard, - dict(state_plots=("torque",), action_plots="all"), - ) + super().__init__( physical_system=physical_system, reference_generator=reference_generator, diff --git a/src/gym_electric_motor/utils.py b/src/gym_electric_motor/utils.py index 4ac6b719..49ffeca0 100644 --- a/src/gym_electric_motor/utils.py +++ b/src/gym_electric_motor/utils.py @@ -2,7 +2,7 @@ import numpy as np -# Dummy for refactoring TODO +# This hacky function stays for now because, all envs use it and refectoring would be a pain TODO: fix this def initialize(base_class, arg, default_class, default_args): if arg is None: return default_class(**default_args) From 3451a3874ef79004cee34b31d9a9f0f874db5e90 Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Mon, 5 Feb 2024 08:16:16 +0100 Subject: [PATCH 23/51] removed some unused util testing code --- tests/test_core.py | 2 - .../test_physical_systems.py | 2 - tests/testing_utils.py | 196 ------------------ 3 files changed, 200 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index 5500be94..950cb17c 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -8,8 +8,6 @@ DummyCallback, DummyConstraintMonitor, DummyConstraint, - mock_instantiate, - instantiate_dict, ) from gymnasium.spaces import Tuple, Box import gym_electric_motor diff --git a/tests/test_physical_systems/test_physical_systems.py b/tests/test_physical_systems/test_physical_systems.py index 46f6f3f1..d17bcbe1 100644 --- a/tests/test_physical_systems/test_physical_systems.py +++ b/tests/test_physical_systems/test_physical_systems.py @@ -5,8 +5,6 @@ DummyOdeSolver, DummyVoltageSupply, DummyElectricMotor, - mock_instantiate, - instantiate_dict, ) from gym_electric_motor.physical_systems import ( physical_systems as ps, diff --git a/tests/testing_utils.py b/tests/testing_utils.py index 1a348409..853fa290 100644 --- a/tests/testing_utils.py +++ b/tests/testing_utils.py @@ -23,202 +23,6 @@ from tests.conf import system, jacobian, permex_motor_parameter from gym_electric_motor.core import Callback - -# region first version - - -def setup_physical_system( - motor_type, converter_type, subconverters=None, three_phase=False -): - """ - Function to set up a physical system with test parameters - :param motor_type: motor name (string) - :param converter_type: converter name (string) - :param three_phase: if True, than a synchronous motor system will be instantiated - :return: instantiated physical system - """ - # get test parameter - tau = converter_parameter["tau"] - u_sup = test_motor_parameter[motor_type]["motor_parameter"]["u_sup"] - motor_parameter = test_motor_parameter[motor_type]["motor_parameter"] # dict - nominal_values = test_motor_parameter[motor_type]["nominal_values"] # dict - limit_values = test_motor_parameter[motor_type]["limit_values"] # dict - # setup load - load = PolynomialStaticLoad(load_parameter=load_parameter["parameter"]) - # setup voltage supply - voltage_supply = IdealVoltageSupply(u_sup) - # setup converter - if motor_type == "DcExtEx": - if "Disc" in converter_type: - double_converter = "Disc-Multi" - else: - double_converter = "Cont-Multi" - converter = make_module( - PowerElectronicConverter, - double_converter, - subconverters=[converter_type, converter_type], - tau=converter_parameter["tau"], - dead_time=converter_parameter["dead_time"], - interlocking_time=converter_parameter["interlocking_time"], - ) - else: - converter = make_module( - PowerElectronicConverter, - converter_type, - subconverters=subconverters, - tau=converter_parameter["tau"], - dead_time=converter_parameter["dead_time"], - interlocking_time=converter_parameter["interlocking_time"], - ) - # setup motor - motor = make_module( - ElectricMotor, - motor_type, - motor_parameter=motor_parameter, - nominal_values=nominal_values, - limit_values=limit_values, - ) - # setup solver - solver = ScipySolveIvpSolver(method="RK45") - # combine all modules to a physical system - if three_phase: - if motor_type == "SCIM": - physical_system = SquirrelCageInductionMotorSystem( - converter=converter, - motor=motor, - ode_solver=solver, - supply=voltage_supply, - load=load, - tau=tau, - ) - elif motor_type == "DFIM": - physical_system = DoublyFedInductionMotor( - converter=converter, - motor=motor, - ode_solver=solver, - supply=voltage_supply, - load=load, - tau=tau, - ) - else: - physical_system = SynchronousMotorSystem( - converter=converter, - motor=motor, - ode_solver=solver, - supply=voltage_supply, - load=load, - tau=tau, - ) - else: - physical_system = DcMotorSystem( - converter=converter, - motor=motor, - ode_solver=solver, - supply=voltage_supply, - load=load, - tau=tau, - ) - return physical_system - - -def setup_reference_generator(reference_type, physical_system, reference_state="omega"): - """ - Function to setup the reference generator - :param reference_type: name of reference generator - :param physical_system: instantiated physical system - :param reference_state: referenced state name (string) - :return: instantiated reference generator - """ - reference_generator = make_module( - ReferenceGenerator, reference_type, reference_state=reference_state - ) - reference_generator.set_modules(physical_system) - reference_generator.reset() - return reference_generator - - -def setup_reward_function( - reward_function_type, - physical_system, - reference_generator, - reward_weights, - observed_states, -): - reward_function = make_module( - RewardFunction, - reward_function_type, - observed_states=observed_states, - reward_weights=reward_weights, - ) - reward_function.set_modules(physical_system, reference_generator) - return reward_function - - -def setup_dc_converter(conv, motor_type, subconverters=None): - """ - This function initializes the converter. - It differentiates between single and double converter and can be used for discrete and continuous converters. - :param conv: converter name (string) - :param motor_type: motor name (string) - :return: initialized converter - """ - if motor_type == "DcExtEx": - # setup double converter - if "Disc" in conv: - double_converter = "Disc-Multi" - else: - double_converter = "Cont-Multi" - converter = make_module( - PowerElectronicConverter, - double_converter, - interlocking_time=converter_parameter["interlocking_time"], - dead_time=converter_parameter["dead_time"], - subconverters=[ - make_module( - PowerElectronicConverter, - conv, - tau=converter_parameter["tau"], - dead_time=converter_parameter["dead_time"], - interlocking_time=converter_parameter["interlocking_time"], - ), - make_module( - PowerElectronicConverter, - conv, - tau=converter_parameter["tau"], - dead_time=converter_parameter["dead_time"], - interlocking_time=converter_parameter["interlocking_time"], - ), - ], - ) - else: - # setup single converter - converter = make_module( - PowerElectronicConverter, - conv, - subconverters=subconverters, - tau=converter_parameter["tau"], - dead_time=converter_parameter["dead_time"], - interlocking_time=converter_parameter["interlocking_time"], - ) - return converter - - -# endregion - -# region second version - -instantiate_dict = {} - - -def mock_instantiate(superclass, key, **kwargs): - # Instantiate the object and log the passed and returned values to validate correct function calls - instantiate_dict[superclass] = {} - instantiate_dict[superclass]["key"] = key - inst = instantiate(superclass, key, **kwargs) - instantiate_dict[superclass]["instance"] = inst - return inst - - class DummyReferenceGenerator(ReferenceGenerator): _reset_counter = 0 From f12593dda2811089c211d88aa909dd81503712ca Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Mon, 5 Feb 2024 08:16:30 +0100 Subject: [PATCH 24/51] ruff format --- .../electric_motors/externally_excited_synchronous_motor.py | 2 +- .../electric_motors/permanent_magnet_synchronous_motor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py index c35ef654..1ae2cae9 100644 --- a/src/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py @@ -209,7 +209,7 @@ def _torque_limit(self): else: i_n = self.nominal_values["i"] _p = mp["l_m"] * i_n / (2 * (mp["l_d"] - mp["l_q"])) - _q = -i_n**2 / 2 + _q = -(i_n**2) / 2 if mp["l_d"] < mp["l_q"]: i_d_opt = -_p / 2 - np.sqrt((_p / 2) ** 2 - _q) else: diff --git a/src/gym_electric_motor/physical_systems/electric_motors/permanent_magnet_synchronous_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/permanent_magnet_synchronous_motor.py index 3fd927e8..11353963 100644 --- a/src/gym_electric_motor/physical_systems/electric_motors/permanent_magnet_synchronous_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/permanent_magnet_synchronous_motor.py @@ -127,7 +127,7 @@ def _torque_limit(self): else: i_n = self.nominal_values["i"] _p = mp["psi_p"] / (2 * (mp["l_d"] - mp["l_q"])) - _q = -i_n**2 / 2 + _q = -(i_n**2) / 2 i_d_opt = -_p / 2 - np.sqrt((_p / 2) ** 2 - _q) i_q_opt = np.sqrt(i_n**2 - i_d_opt**2) return self.torque([i_d_opt, i_q_opt, 0]) From 282b53dc8b3081685ca489a123da397fdc3ef4b1 Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Mon, 5 Feb 2024 09:07:54 +0100 Subject: [PATCH 25/51] removed gem-control --- gem-control | 1 - 1 file changed, 1 deletion(-) delete mode 160000 gem-control diff --git a/gem-control b/gem-control deleted file mode 160000 index bc9ac0a3..00000000 --- a/gem-control +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bc9ac0a3161fdd256ece6389a5826bb305cb0fda From 3b975c9725e8cdc0cee7a78aef6db5f468df1894 Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Mon, 5 Feb 2024 09:44:06 +0100 Subject: [PATCH 26/51] packaging for pypi added --- DEVELOPMENT.md | 33 +++++++++++++++++++++++++++------ pyproject.toml | 3 +++ setup.py | 33 --------------------------------- 3 files changed, 30 insertions(+), 39 deletions(-) delete mode 100644 setup.py diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 149fca86..b490b62f 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -1,17 +1,38 @@ -# No poetry -Some complex dependecies system don't work good with poetry +# Packaging +Update build tool: `python -m pip install --upgrade build` + +Build: `python -m build` + +# Testing +Run: `pytest --sw` +> --sw, --stepwise Exit on test failure and continue from last failing test next time # Linter and Formater Ruff: https://docs.astral.sh/ruff/installation/ -# Install editable local package -pip install -e . +Use `ruff check src/` for linting +or `ruff format src/` for formatting + + +# Install package for local development +Run: `pip install -e .` +> -e, --editable Install a project in editable mode (i.e. setuptools "develop mode") from a local project path or a VCS url Check correct package install directory with python interpreter -run `python` +Run: `python` ``` >>> import gym_electric_motor as gem >>> gem -``` \ No newline at end of file +``` + +# Sidenotes +``` +python -V +Python 3.10.13 +``` + + +## No poetry +Some complex dependency systems don't work good with poetry (e.g. pytorch) (https://python-poetry.org/) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index b2a1bb9c..ef082349 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,9 @@ build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["/src/gym_electric_motor", "/src/gem_controllers"] +[tool.hatch.build.targets.sdist] +packages = ["/src/gym_electric_motor", "/src/gem_controllers"] + [tool.ruff] src = ["src"] line-length = 120 diff --git a/setup.py b/setup.py deleted file mode 100644 index 17486dd6..00000000 --- a/setup.py +++ /dev/null @@ -1,33 +0,0 @@ -import setuptools - - -AUTHORS = [ - 'Arne Traue', 'Gerrit Book', 'Praneeth Balakrishna', - 'Pascal Peters', 'Pramod Manjunatha', 'Darius Jakobeit', 'Felix Book', - 'Max Schenke', 'Wilhelm Kirchgässner', 'Oliver Wallscheid', 'Barnabas Haucke-Korber', - 'Stefan Arndt', 'Marius Köhler' -] - -with open('requirements.txt', 'r') as f: - requirements = f.read().splitlines() - -with open("README.md", "r") as fh: - long_description = fh.read() - -setuptools.setup( - name='gym_electric_motor', - version='2.0.0', - description='A Farama Gymnasium environment for electric motor control.', - packages=setuptools.find_packages(), - install_requires=requirements, - python_requires='>=3.8', - extras_require={'examples': [ - 'keras-rl2', - 'stable-baselines3', - 'gekko'] - }, - author=', '.join(sorted(AUTHORS, key=lambda n: n.split()[-1].lower())), - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/upb-lea/gym-electric-motor", - ) From 848cd4673f95be7fe4e5504691263cecd163ad8c Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Mon, 12 Feb 2024 13:16:16 +0100 Subject: [PATCH 27/51] added requirements.txt --- pyproject.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index ef082349..bf524848 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,11 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ] +dynamic = ["dependencies", "optional-dependencies"] + +[tool.setuptools.dynamic] +dependencies = { file = ["requirements.txt"] } +optional-dependencies = { dev = { file = ["requirements-dev.txt"] } } [project.urls] Homepage = "https://github.com/upb-lea/gym-electric-motor" From 3fa9a23aebdb96513abc087037fdccb1a49ef5f0 Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Mon, 12 Feb 2024 14:35:59 +0100 Subject: [PATCH 28/51] WIP Observer Pattern --- .../classic_controllers_dc_motor_example.py | 9 +- ...ation_test_classic_controllers_dc_motor.py | 17 ++-- examples/observers/state_observer_example.py | 91 +++++++++++++++++++ pyproject.toml | 7 +- src/gym_electric_motor/envs/motors.py | 2 +- src/gym_electric_motor/observers/__init__.py | 1 + src/gym_electric_motor/observers/observer.py | 5 + .../observers/state_observer.py | 19 ++++ 8 files changed, 136 insertions(+), 15 deletions(-) create mode 100644 examples/observers/state_observer_example.py create mode 100644 src/gym_electric_motor/observers/__init__.py create mode 100644 src/gym_electric_motor/observers/observer.py create mode 100644 src/gym_electric_motor/observers/state_observer.py diff --git a/examples/classic_controllers/classic_controllers_dc_motor_example.py b/examples/classic_controllers/classic_controllers_dc_motor_example.py index f647c36d..0a5a72ea 100644 --- a/examples/classic_controllers/classic_controllers_dc_motor_example.py +++ b/examples/classic_controllers/classic_controllers_dc_motor_example.py @@ -1,7 +1,9 @@ from classic_controllers import Controller from externally_referenced_state_plot import ExternallyReferencedStatePlot + import gym_electric_motor as gem from gym_electric_motor.visualization import MotorDashboard +from gym_electric_motor.visualization.render_modes import RenderMode if __name__ == "__main__": """ @@ -36,11 +38,11 @@ # definition of the plotted variables external_ref_plots = [ExternallyReferencedStatePlot(state) for state in states] + motor_dashboard = MotorDashboard(additional_plots=external_ref_plots, render_mode=RenderMode.Figure) # initialize the gym-electric-motor environment env = gem.make( motor, - visualization=MotorDashboard(additional_plots=external_ref_plots), - render_mode="figure_once", + visualization=motor_dashboard, ) """ initialize the controller @@ -53,7 +55,7 @@ a (optional) tuning parameter of the symmetrical optimum (default: 4) """ - visualization = MotorDashboard(additional_plots=external_ref_plots) + controller = Controller.make(env, external_ref_plots=external_ref_plots) (state, reference), _ = env.reset(seed=None) @@ -65,4 +67,5 @@ env.reset() controller.reset() + motor_dashboard.show_and_hold() env.close() diff --git a/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py b/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py index 5b44dc5b..b22dc7ce 100644 --- a/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py +++ b/examples/classic_controllers/integration_test_classic_controllers_dc_motor.py @@ -3,9 +3,10 @@ # from gem_controllers.gem_controller import GemController import gym_electric_motor as gem -from gym_electric_motor.envs.motors import MotorType, ControlType, ActionType, Motor -from gym_electric_motor.visualization import MotorDashboard, RenderMode +from gym_electric_motor.envs.motors import ActionType, ControlType, Motor, MotorType from gym_electric_motor.reference_generators import SinusoidalReferenceGenerator +from gym_electric_motor.visualization import MotorDashboard, RenderMode +from gym_electric_motor.visualization.motor_dashboard_plots import StatePlot if __name__ == "__main__": """ @@ -29,10 +30,7 @@ ) # definition of the plotted variables - external_ref_plots = [ - ExternallyReferencedStatePlot(state) for state in motor.state_names() - ] - + external_ref_plots = [ExternallyReferencedStatePlot(state) for state in motor.states()] # definition of the reference generator ref_generator = SinusoidalReferenceGenerator( @@ -41,9 +39,7 @@ offset_range=(0, 0), episode_lengths=(10001, 10001), ) - motor_dashboard = MotorDashboard( - additional_plots=external_ref_plots, render_mode=RenderMode.FigureOnce - ) + motor_dashboard = MotorDashboard(additional_plots=external_ref_plots, render_mode=RenderMode.Figure) # initialize the gym-electric-motor environment env = gem.make( motor.env_id(), @@ -63,11 +59,12 @@ a (optional) tuning parameter of the symmetrical optimum (default: 4) """ + controller = Controller.make(env, external_ref_plots=external_ref_plots) # controller = GemController.make(env, env_id=motor.env_id()) (state, reference), _ = env.reset(seed=1337) - print("state_names: ", motor.state_names()) + print("state_names: ", motor.states()) # simulate the environment for i in range(10001): action = controller.control(state, reference) diff --git a/examples/observers/state_observer_example.py b/examples/observers/state_observer_example.py new file mode 100644 index 00000000..1df1bc5a --- /dev/null +++ b/examples/observers/state_observer_example.py @@ -0,0 +1,91 @@ +import os +import sys + +import gym_electric_motor as gem +from gem_controllers import GemController +from gym_electric_motor.envs.motors import ActionType, ControlType, Motor, MotorType +from gym_electric_motor.observers import StateObserver +from gym_electric_motor.reference_generators import SinusoidalReferenceGenerator +from gym_electric_motor.visualization import MotorDashboard, RenderMode + +path = os.getcwd() + "/examples/classic_controllers" +sys.path.append(path) +from classic_controllers import Controller # noqa: E402 +from externally_referenced_state_plot import ExternallyReferencedStatePlot # noqa: E402 + +if __name__ == "__main__": + """ + motor type: 'PermExDc' Permanently Excited DC Motor + 'ExtExDc' Externally Excited DC Motor + 'SeriesDc' DC Series Motor + 'ShuntDc' DC Shunt Motor + + control type: 'SC' Speed Control + 'TC' Torque Control + 'CC' Current Control + + action_type: 'Cont' Continuous Action Space + 'Finite' Discrete Action Space + """ + + motor = Motor( + MotorType.PermanentlyExcitedDcMotor, + ControlType.SpeedControl, + ActionType.Continuous, + ) + + # definition of the plotted variables + external_ref_plots = [ExternallyReferencedStatePlot(state) for state in motor.states()] + + # definition of the reference generator + + ref_generator = SinusoidalReferenceGenerator( + amplitude_range=(1, 1), + frequency_range=(5, 5), + offset_range=(0, 0), + episode_lengths=(10001, 10001), + ) + motor_dashboard = MotorDashboard(additional_plots=external_ref_plots, render_mode=RenderMode.FigureOnce) + # initialize the gym-electric-motor environment + env = gem.make( + motor.env_id(), + visualization=motor_dashboard, + scale_plots=True, + reference_generator=ref_generator, + ) + + """ + initialize the controller + + Args: + environment gym-electric-motor environment + external_ref_plots (optional) plots of the environment, to plot all reference values + stages (optional) structure of the controller + automated_gain (optional) if True (default), the controller will be tuned automatically + a (optional) tuning parameter of the symmetrical optimum (default: 4) + + """ + controller = Controller.make(env, external_ref_plots=external_ref_plots) + # controller = GemController.make(env, env_id=motor.env_id()) + + (state, reference), _ = env.reset(seed=1337) + print("state_names: ", motor.states()) + # simulate the environment + for i in range(10001): + action = controller.control(state, reference) + print(f"{reference}") + # if i % 100 == 0: + # (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) + # else: + (state, reference), reward, terminated, truncated, _ = env.step(action) + + # viz.render() + + if terminated: + env.reset() + + controller.reset() + + env.close() + + motor_dashboard.show_and_hold() diff --git a/pyproject.toml b/pyproject.toml index bf524848..e016119c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,12 @@ build-backend = "hatchling.build" packages = ["/src/gym_electric_motor", "/src/gem_controllers"] [tool.hatch.build.targets.sdist] -packages = ["/src/gym_electric_motor", "/src/gem_controllers"] +packages = [ + "/src/gym_electric_motor", + "/src/gem_controllers", + "/examples/classic_controllers", + "/tests", +] [tool.ruff] src = ["src"] diff --git a/src/gym_electric_motor/envs/motors.py b/src/gym_electric_motor/envs/motors.py index d7e232c8..29a933d3 100644 --- a/src/gym_electric_motor/envs/motors.py +++ b/src/gym_electric_motor/envs/motors.py @@ -55,5 +55,5 @@ def env_id(self) -> str: + "-v0" ) - def state_names(self) -> list[str]: + def states(self) -> list[str]: return self.motor_type.states diff --git a/src/gym_electric_motor/observers/__init__.py b/src/gym_electric_motor/observers/__init__.py new file mode 100644 index 00000000..756e7f56 --- /dev/null +++ b/src/gym_electric_motor/observers/__init__.py @@ -0,0 +1 @@ +from .state_observer import StateObserver diff --git a/src/gym_electric_motor/observers/observer.py b/src/gym_electric_motor/observers/observer.py new file mode 100644 index 00000000..63b5c3dd --- /dev/null +++ b/src/gym_electric_motor/observers/observer.py @@ -0,0 +1,5 @@ +import abc + + +class Observer: + pass diff --git a/src/gym_electric_motor/observers/state_observer.py b/src/gym_electric_motor/observers/state_observer.py new file mode 100644 index 00000000..1d344283 --- /dev/null +++ b/src/gym_electric_motor/observers/state_observer.py @@ -0,0 +1,19 @@ +from ..core import ElectricMotorEnvironment +from .observer import Observer + + +class StateObserver(Observer): + env = None + + def __init__(self, env: ElectricMotorEnvironment): + self.env = env + + def observe(self, state: str) -> any: + state_value = self.pull(state) + self.push(state_value) + + def pull(self, state: str) -> any: + pass + + def push(self, value): + print(f"PUSH | {value} \n") From ba71716f415995bf7f70cad597011e2e26444883 Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Mon, 12 Feb 2024 14:39:52 +0100 Subject: [PATCH 29/51] fix typo --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e016119c..790ac3e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "gym_eletric_motor" +name = "gym_electric_motor" version = "2.1.0" authors = [ { name = "Arne Traue" }, From e1c69a82f4e63fcfe38ede9c14c9ef720b623d8d Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Mon, 12 Feb 2024 16:08:32 +0100 Subject: [PATCH 30/51] Revert changes because constructor of __init__ args are getting called on import --- .../cont_tc_series_dc_env.py | 50 +++++++++++++------ 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_tc_series_dc_env.py b/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_tc_series_dc_env.py index 83011407..b9b6ba2f 100644 --- a/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_tc_series_dc_env.py +++ b/src/gym_electric_motor/envs/gym_dcm/series_dc_motor_env/cont_tc_series_dc_env.py @@ -89,14 +89,14 @@ class ContTorqueControlDcSeriesMotorEnv(ElectricMotorEnvironment): def __init__( self, - supply=ps.IdealVoltageSupply(u_nominal=60.0), - converter=ps.ContFourQuadrantConverter(), - motor=ps.DcSeriesMotor(), - load=ps.ConstantSpeedLoad(omega_fixed=100.0), - ode_solver=ps.ScipyOdeSolver(), - reward_function=WeightedSumOfErrors(reward_weights=dict(torque=1.0)), - reference_generator=WienerProcessReferenceGenerator(reference_state="torque"), - visualization=MotorDashboard(state_plots=("torque",), action_plots="all"), + supply=None, + converter=None, + motor=None, + load=None, + ode_solver=None, + reward_function=None, + reference_generator=None, + visualization=None, state_filter=None, callbacks=(), constraints=("i",), @@ -142,15 +142,37 @@ def __init__( The available strings can be looked up in the documentation. (e.g. ``converter='Finite-2QC'``) """ physical_system = DcMotorSystem( - supply=supply, - converter=converter, - motor=motor, - load=load, - ode_solver=ode_solver, + supply=initialize(ps.VoltageSupply, supply, ps.IdealVoltageSupply, dict(u_nominal=60.0)), + converter=initialize( + ps.PowerElectronicConverter, + converter, + ps.ContFourQuadrantConverter, + dict(), + ), + motor=initialize(ps.ElectricMotor, motor, ps.DcSeriesMotor, dict()), + load=initialize(ps.MechanicalLoad, load, ps.ConstantSpeedLoad, dict(omega_fixed=100.0)), + ode_solver=initialize(ps.OdeSolver, ode_solver, ps.ScipyOdeSolver, dict()), calc_jacobian=calc_jacobian, tau=tau, ) - + reference_generator = initialize( + ReferenceGenerator, + reference_generator, + WienerProcessReferenceGenerator, + dict(reference_state="torque"), + ) + reward_function = initialize( + RewardFunction, + reward_function, + WeightedSumOfErrors, + dict(reward_weights=dict(torque=1.0)), + ) + visualization = initialize( + ElectricMotorVisualization, + visualization, + MotorDashboard, + dict(state_plots=("torque",), action_plots="all"), + ) super().__init__( physical_system=physical_system, reference_generator=reference_generator, From 6d0686304233734d9719cd1b56586f95fb716c2f Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Fri, 1 Mar 2024 11:34:35 +0100 Subject: [PATCH 31/51] added state oberserver --- examples/observers/state_observer_example.py | 5 +- src/gym_electric_motor/core.py | 9 +++- src/gym_electric_motor/observers/__init__.py | 2 +- src/gym_electric_motor/observers/observer.py | 51 ++++++++++++++++++- .../observers/state_observer.py | 19 ------- 5 files changed, 62 insertions(+), 24 deletions(-) delete mode 100644 src/gym_electric_motor/observers/state_observer.py diff --git a/examples/observers/state_observer_example.py b/examples/observers/state_observer_example.py index 1df1bc5a..46bf1d35 100644 --- a/examples/observers/state_observer_example.py +++ b/examples/observers/state_observer_example.py @@ -70,10 +70,11 @@ (state, reference), _ = env.reset(seed=1337) print("state_names: ", motor.states()) + + state_observer = StateObserver(env) # simulate the environment - for i in range(10001): + for i in range(100): action = controller.control(state, reference) - print(f"{reference}") # if i % 100 == 0: # (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) # else: diff --git a/src/gym_electric_motor/core.py b/src/gym_electric_motor/core.py index 57e09117..f3ab684d 100644 --- a/src/gym_electric_motor/core.py +++ b/src/gym_electric_motor/core.py @@ -17,6 +17,7 @@ - Visualization of the PhysicalSystems state, reference and reward for the user. """ + import datetime import os from dataclasses import dataclass @@ -108,8 +109,10 @@ class ElectricMotorEnvironment(gymnasium.core.Env): sim = SimulationEnvironment() workspace = Workspace() - env_id = None + current_state = None + current_reference = None + current_next_reference = None @property def physical_system(self): @@ -339,11 +342,15 @@ def step(self, action): self._call_callbacks("on_step_begin", self.physical_system.k, action) state = self._physical_system.simulate(action) + self.current_state = state reference = self.reference_generator.get_reference(state) + violation_degree = self._constraint_monitor.check_constraints(state) reward = self._reward_function.reward(state, reference, self._physical_system.k, action, violation_degree) self._terminated = violation_degree >= 1.0 ref_next = self.reference_generator.get_reference_observation(state) + self.current_state + [ref_next] + self._call_callbacks( "on_step_end", self.physical_system.k, diff --git a/src/gym_electric_motor/observers/__init__.py b/src/gym_electric_motor/observers/__init__.py index 756e7f56..1d9b335b 100644 --- a/src/gym_electric_motor/observers/__init__.py +++ b/src/gym_electric_motor/observers/__init__.py @@ -1 +1 @@ -from .state_observer import StateObserver +from .observer import StateObserver diff --git a/src/gym_electric_motor/observers/observer.py b/src/gym_electric_motor/observers/observer.py index 63b5c3dd..0dac9e85 100644 --- a/src/gym_electric_motor/observers/observer.py +++ b/src/gym_electric_motor/observers/observer.py @@ -1,5 +1,54 @@ import abc +from ..core import ElectricMotorEnvironment + class Observer: - pass + env = None + + def __init__(self, env: ElectricMotorEnvironment): + self.env = env + + def observe(self, *args, **kwargs) -> any: + result = self.pull(*args, **kwargs) + self.push(result) + + @abc.abstractmethod + def pull(self, *args, **kwargs) -> any: + pass + + @abc.abstractmethod + def push(self, result): + pass + + +class StateObserver(Observer): + state_names = None + + def __init__(self, env: ElectricMotorEnvironment): + super().__init__(env) + self.fuse_state_and_next_reference() + + def fuse_state_and_next_reference(self): + state_names = self.env.state_names + reference_names = self.env.reference_names + for ref_name in reference_names: + state_names.append(ref_name + "_ref") + self.state_names = state_names + + def pull(self, state: str) -> any: + # Integrate reference into states + + state_names = self.state_names + + if state in state_names: + state_value = self.env.current_state[state_names.index(state)] + if state_value is not None: + return (state, state_value) + else: + raise ValueError(f"State '{state}' is None") + else: + raise ValueError(f"State '{state}' not found in state_names, allowed states are {state_names}") + + def push(self, value): + print(f"PUSH | {value} \n") diff --git a/src/gym_electric_motor/observers/state_observer.py b/src/gym_electric_motor/observers/state_observer.py deleted file mode 100644 index 1d344283..00000000 --- a/src/gym_electric_motor/observers/state_observer.py +++ /dev/null @@ -1,19 +0,0 @@ -from ..core import ElectricMotorEnvironment -from .observer import Observer - - -class StateObserver(Observer): - env = None - - def __init__(self, env: ElectricMotorEnvironment): - self.env = env - - def observe(self, state: str) -> any: - state_value = self.pull(state) - self.push(state_value) - - def pull(self, state: str) -> any: - pass - - def push(self, value): - print(f"PUSH | {value} \n") From 0484d9963e5d9a49c912a770c249e4eee3fda2d1 Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Fri, 1 Mar 2024 12:01:38 +0100 Subject: [PATCH 32/51] enable env checker --- .../classic_controllers.py | 343 +++++------------- .../controllers/cascaded_controller.py | 119 ++---- .../controllers/pi_controller.py | 12 +- src/gym_electric_motor/__init__.py | 10 +- src/gym_electric_motor/core.py | 2 +- src/gym_electric_motor/observers/observer.py | 4 +- 6 files changed, 146 insertions(+), 344 deletions(-) diff --git a/examples/classic_controllers/classic_controllers.py b/examples/classic_controllers/classic_controllers.py index e24ce6b4..2f39c4fb 100644 --- a/examples/classic_controllers/classic_controllers.py +++ b/examples/classic_controllers/classic_controllers.py @@ -1,41 +1,37 @@ -from gymnasium.spaces import Discrete, Box, MultiDiscrete +import numpy as np +from controllers.cascaded_controller import CascadedController +from controllers.cascaded_foc_controller import CascadedFieldOrientedController +from controllers.continuous_action_controller import ContinuousActionController +from controllers.continuous_controller import ContinuousController +from controllers.dicrete_action_controller import DiscreteActionController +from controllers.discrete_controller import DiscreteController +from controllers.foc_controller import FieldOrientedController +from controllers.induction_motor_cascaded_foc import ( + InductionMotorCascadedFieldOrientedController, +) +from controllers.induction_motor_foc import InductionMotorFieldOrientedController +from controllers.on_off_controller import OnOffController +from controllers.pi_controller import PIController +from controllers.pid_controller import PIDController +from controllers.three_point_controller import ThreePointController +from external_plot import ExternalPlot +from externally_referenced_state_plot import ExternallyReferencedStatePlot +from gymnasium.spaces import Box, Discrete, MultiDiscrete + +from gym_electric_motor import envs from gym_electric_motor.physical_systems import ( - SynchronousMotorSystem, + DcExternallyExcitedMotor, DcMotorSystem, DcSeriesMotor, - DcExternallyExcitedMotor, DoublyFedInductionMotorSystem, SquirrelCageInductionMotorSystem, + SynchronousMotorSystem, ) from gym_electric_motor.reference_generators import ( MultipleReferenceGenerator, SwitchedReferenceGenerator, ) from gym_electric_motor.visualization import MotorDashboard -from gym_electric_motor import envs - -from controllers.continuous_controller import ContinuousController -from controllers.pi_controller import PIController -from controllers.pid_controller import PIDController - -from controllers.discrete_controller import DiscreteController -from controllers.on_off_controller import OnOffController -from controllers.three_point_controller import ThreePointController - -from controllers.continuous_action_controller import ContinuousActionController -from controllers.dicrete_action_controller import DiscreteActionController -from controllers.cascaded_controller import CascadedController -from controllers.foc_controller import FieldOrientedController -from controllers.cascaded_foc_controller import CascadedFieldOrientedController -from controllers.induction_motor_foc import InductionMotorFieldOrientedController -from controllers.induction_motor_cascaded_foc import ( - InductionMotorCascadedFieldOrientedController, -) - -from externally_referenced_state_plot import ExternallyReferencedStatePlot -from external_plot import ExternalPlot - -import numpy as np class Controller: @@ -79,37 +75,21 @@ def make(cls, environment, stages=None, **controller_kwargs): "foc_controller": [FieldOrientedController], "cascaded_foc_controller": [CascadedFieldOrientedController], "foc_rotor_flux_observer": [InductionMotorFieldOrientedController], - "cascaded_foc_rotor_flux_observer": [ - InductionMotorCascadedFieldOrientedController - ], + "cascaded_foc_rotor_flux_observer": [InductionMotorCascadedFieldOrientedController], } controller_kwargs = cls.reference_states(environment, **controller_kwargs) controller_kwargs = cls.get_visualization(environment, **controller_kwargs) if stages is not None: - controller_type, stages = cls.find_controller_type( - environment, stages, **controller_kwargs - ) - assert ( - controller_type in _controllers.keys() - ), f"Controller {controller_type} unknown" - stages = cls.automated_gain( - environment, stages, controller_type, _controllers, **controller_kwargs - ) - controller = _controllers[controller_type][0]( - environment, stages, _controllers, **controller_kwargs - ) + controller_type, stages = cls.find_controller_type(environment, stages, **controller_kwargs) + assert controller_type in _controllers.keys(), f"Controller {controller_type} unknown" + stages = cls.automated_gain(environment, stages, controller_type, _controllers, **controller_kwargs) + controller = _controllers[controller_type][0](environment, stages, _controllers, **controller_kwargs) else: - controller_type, stages = cls.automated_controller_design( - environment, **controller_kwargs - ) - stages = cls.automated_gain( - environment, stages, controller_type, _controllers, **controller_kwargs - ) - controller = _controllers[controller_type][0]( - environment, stages, _controllers, **controller_kwargs - ) + controller_type, stages = cls.automated_controller_design(environment, **controller_kwargs) + stages = cls.automated_gain(environment, stages, controller_type, _controllers, **controller_kwargs) + controller = _controllers[controller_type][0](environment, stages, _controllers, **controller_kwargs) return controller @staticmethod @@ -126,7 +106,7 @@ def get_visualization(environment, **controller_kwargs): controller_kwargs["external_plot"] = ext_plot controller_kwargs["external_ref_plots"] = ref_plot - for visualization in environment.visualizations: + for visualization in environment.unwrapped.visualizations: if isinstance(visualization, MotorDashboard): controller_kwargs["update_interval"] = visualization.update_interval controller_kwargs["visualization"] = True @@ -138,19 +118,17 @@ def get_visualization(environment, **controller_kwargs): def reference_states(environment, **controller_kwargs): """This method searches the environment for all referenced states and writes them to an array.""" ref_states = [] - if isinstance(environment.reference_generator, MultipleReferenceGenerator): - for rg in environment.reference_generator._sub_generators: + if isinstance(environment.unwrapped.reference_generator, MultipleReferenceGenerator): + for rg in environment.unwrapped.reference_generator._sub_generators: if isinstance(rg, SwitchedReferenceGenerator): ref_states.append(rg._sub_generators[0]._reference_state) else: ref_states.append(rg._reference_state) - elif isinstance(environment.reference_generator, SwitchedReferenceGenerator): - ref_states.append( - environment.reference_generator._sub_generators[0]._reference_state - ) + elif isinstance(environment.unwrapped.reference_generator, SwitchedReferenceGenerator): + ref_states.append(environment.unwrapped.reference_generator._sub_generators[0]._reference_state) else: - ref_states.append(environment.reference_generator._reference_state) + ref_states.append(environment.unwrapped.reference_generator._reference_state) controller_kwargs["ref_states"] = np.array(ref_states) return controller_kwargs @@ -185,9 +163,7 @@ def find_controller_type(environment, stages, **controller_kwargs): else: controller_type = "cascaded_foc_controller" - elif isinstance( - environment.physical_system.unwrapped, SquirrelCageInductionMotorSystem - ): + elif isinstance(environment.physical_system.unwrapped, SquirrelCageInductionMotorSystem): if len(stages) == 2: if len(stages[1]) == 1 and "i_sq" in controller_kwargs["ref_states"]: controller_type = "foc_rotor_flux_observer" @@ -196,9 +172,7 @@ def find_controller_type(environment, stages, **controller_kwargs): else: controller_type = "cascaded_foc_rotor_flux_observer" - elif isinstance( - environment.physical_system.unwrapped, DoublyFedInductionMotorSystem - ): + elif isinstance(environment.physical_system.unwrapped, DoublyFedInductionMotorSystem): if len(stages) == 2: if len(stages[1]) == 1 and "i_sq" in controller_kwargs["ref_states"]: controller_type = "foc_rotor_flux_observer" @@ -216,24 +190,18 @@ def automated_controller_design(environment, **controller_kwargs): action_space_type = type(environment.action_space) ref_states = controller_kwargs["ref_states"] stages = [] - if isinstance( - environment.physical_system.unwrapped, DcMotorSystem - ): # Checking type of motor + if isinstance(environment.unwrapped.physical_system.unwrapped, DcMotorSystem): # Checking type of motor if "omega" in ref_states or "torque" in ref_states: # Checking control task controller_type = "cascaded_controller" for i in range(len(stages), 2): if i == 0: - if ( - action_space_type is Box - ): # Checking type of output stage (finite / cont) + if action_space_type is Box: # Checking type of output stage (finite / cont) stages.append({"controller_type": "pi_controller"}) else: stages.append({"controller_type": "three_point"}) else: - stages.append( - {"controller_type": "pi_controller"} - ) # Adding PI-Controller for overlaid stages + stages.append({"controller_type": "pi_controller"}) # Adding PI-Controller for overlaid stages elif "i" in ref_states or "i_a" in ref_states: # Checking type of output stage (finite / cont) @@ -244,21 +212,15 @@ def automated_controller_design(environment, **controller_kwargs): controller_type = stages[0]["controller_type"] # Add stage for i_e current of the ExtExDC - if isinstance( - environment.physical_system.electrical_motor, DcExternallyExcitedMotor - ): + if isinstance(environment.unwrapped.physical_system.electrical_motor, DcExternallyExcitedMotor): if action_space_type is Box: stages = [stages, [{"controller_type": "pi_controller"}]] else: stages = [stages, [{"controller_type": "three_point"}]] - elif isinstance(environment.physical_system.unwrapped, SynchronousMotorSystem): + elif isinstance(environment.unwrapped.physical_system.unwrapped, SynchronousMotorSystem): if "i_sq" in ref_states or "torque" in ref_states: # Checking control task - controller_type = ( - "foc_controller" - if "i_sq" in ref_states - else "cascaded_foc_controller" - ) + controller_type = "foc_controller" if "i_sq" in ref_states else "cascaded_foc_controller" if action_space_type is Discrete: stages = [ [{"controller_type": "on_off"}], @@ -296,9 +258,7 @@ def automated_controller_design(environment, **controller_kwargs): ): if "i_sq" in ref_states or "torque" in ref_states: controller_type = ( - "foc_rotor_flux_observer" - if "i_sq" in ref_states - else "cascaded_foc_rotor_flux_observer" + "foc_rotor_flux_observer" if "i_sq" in ref_states else "cascaded_foc_rotor_flux_observer" ) if action_space_type is Discrete: stages = [ @@ -336,9 +296,7 @@ def automated_controller_design(environment, **controller_kwargs): return controller_type, stages @staticmethod - def automated_gain( - environment, stages, controller_type, _controllers, **controller_kwargs - ): + def automated_gain(environment, stages, controller_type, _controllers, **controller_kwargs): """ This method automatically parameterizes a given controller design if the parameter automated_gain is True (default True), based on the design according to the symmetric optimum (SO). Further information about the @@ -356,14 +314,14 @@ def automated_gain( """ ref_states = controller_kwargs["ref_states"] - mp = environment.physical_system.electrical_motor.motor_parameter - limits = environment.physical_system.limits - omega_lim = limits[environment.state_names.index("omega")] - if isinstance(environment.physical_system.unwrapped, DcMotorSystem): - i_a_lim = limits[environment.physical_system.CURRENTS_IDX[0]] - i_e_lim = limits[environment.physical_system.CURRENTS_IDX[-1]] - u_a_lim = limits[environment.physical_system.VOLTAGES_IDX[0]] - u_e_lim = limits[environment.physical_system.VOLTAGES_IDX[-1]] + mp = environment.unwrapped.physical_system.electrical_motor.motor_parameter + limits = environment.unwrapped.physical_system.limits + omega_lim = limits[environment.unwrapped.state_names.index("omega")] + if isinstance(environment.unwrapped.physical_system.unwrapped, DcMotorSystem): + i_a_lim = limits[environment.unwrapped.physical_system.CURRENTS_IDX[0]] + i_e_lim = limits[environment.unwrapped.physical_system.CURRENTS_IDX[-1]] + u_a_lim = limits[environment.unwrapped.physical_system.VOLTAGES_IDX[0]] + u_e_lim = limits[environment.unwrapped.physical_system.VOLTAGES_IDX[-1]] elif isinstance(environment.physical_system.unwrapped, SynchronousMotorSystem): i_sd_lim = limits[environment.state_names.index("i_sd")] @@ -383,9 +341,9 @@ def automated_gain( a = controller_kwargs.get("a", 4) automated_gain = controller_kwargs.get("automated_gain", True) - if isinstance(environment.physical_system.electrical_motor, DcSeriesMotor): + if isinstance(environment.unwrapped.physical_system.electrical_motor, DcSeriesMotor): mp["l"] = mp["l_a"] + mp["l_e"] - elif isinstance(environment.physical_system.unwrapped, DcMotorSystem): + elif isinstance(environment.unwrapped.physical_system.unwrapped, DcMotorSystem): mp["l"] = mp["l_a"] if "automated_gain" not in controller_kwargs.keys() or automated_gain: @@ -403,12 +361,7 @@ def automated_gain( stages_a = stages[0] stages_e = stages[1] - p_gain = ( - mp["l_e"] - / (environment.physical_system.tau * a) - / u_e_lim - * i_e_lim - ) + p_gain = mp["l_e"] / (environment.physical_system.tau * a) / u_e_lim * i_e_lim i_gain = p_gain / (environment.physical_system.tau * a**2) stages_e[0]["p_gain"] = stages_e[0].get("p_gain", p_gain) @@ -426,12 +379,7 @@ def automated_gain( if _controllers[controller_type][0] == ContinuousActionController: if "i" in ref_states or "i_a" in ref_states or "torque" in ref_states: - p_gain = ( - mp["l"] - / (environment.physical_system.tau * a) - / u_a_lim - * i_a_lim - ) + p_gain = mp["l"] / (environment.physical_system.tau * a) / u_a_lim * i_a_lim i_gain = p_gain / (environment.physical_system.tau * a**2) stages_a[0]["p_gain"] = stages_a[0].get("p_gain", p_gain) @@ -462,28 +410,15 @@ def automated_gain( for i in range(len(stages)): if type(stages_a[i]) is list: if ( - _controllers[stages_a[i][0]["controller_type"]][1] - == ContinuousController + _controllers[stages_a[i][0]["controller_type"]][1] == ContinuousController ): # had to add [0] to make dict in list acessable if i == 0: - p_gain = ( - mp["l"] - / (environment.physical_system.tau * a) - / u_a_lim - * i_a_lim - ) - i_gain = p_gain / ( - environment.physical_system.tau * a**2 - ) + p_gain = mp["l"] / (environment.physical_system.tau * a) / u_a_lim * i_a_lim + i_gain = p_gain / (environment.physical_system.tau * a**2) - if ( - _controllers[stages_a[i][0]["controller_type"]][2] - == PIDController - ): + if _controllers[stages_a[i][0]["controller_type"]][2] == PIDController: d_gain = p_gain * environment.physical_system.tau - stages_a[i][0]["d_gain"] = stages_a[i][0].get( - "d_gain", d_gain - ) + stages_a[i][0]["d_gain"] = stages_a[i][0].get("d_gain", d_gain) elif i == 1: t_n = environment.physical_system.tau * a**2 @@ -494,71 +429,40 @@ def automated_gain( * omega_lim ) i_gain = p_gain / (a * t_n) - if ( - _controllers[stages_a[i][0]["controller_type"]][2] - == PIDController - ): + if _controllers[stages_a[i][0]["controller_type"]][2] == PIDController: d_gain = p_gain * environment.physical_system.tau - stages_a[i][0]["d_gain"] = stages_a[i][0].get( - "d_gain", d_gain - ) + stages_a[i][0]["d_gain"] = stages_a[i][0].get("d_gain", d_gain) - stages_a[i][0]["p_gain"] = stages_a[i][0].get( - "p_gain", p_gain - ) # ? - stages_a[i][0]["i_gain"] = stages_a[i][0].get( - "i_gain", i_gain - ) # ? + stages_a[i][0]["p_gain"] = stages_a[i][0].get("p_gain", p_gain) # ? + stages_a[i][0]["i_gain"] = stages_a[i][0].get("i_gain", i_gain) # ? elif type(stages_a[i]) is dict: if ( - _controllers[stages_a[i]["controller_type"]][1] - == ContinuousController + _controllers[stages_a[i]["controller_type"]][1] == ContinuousController ): # had to add [0] to make dict in list acessable if i == 0: - p_gain = ( - mp["l"] - / (environment.physical_system.tau * a) - / u_a_lim - * i_a_lim - ) - i_gain = p_gain / ( - environment.physical_system.tau * a**2 - ) + p_gain = mp["l"] / (environment.unwrapped.physical_system.tau * a) / u_a_lim * i_a_lim + i_gain = p_gain / (environment.unwrapped.physical_system.tau * a**2) - if ( - _controllers[stages_a[i]["controller_type"]][2] - == PIDController - ): + if _controllers[stages_a[i]["controller_type"]][2] == PIDController: d_gain = p_gain * environment.physical_system.tau - stages_a[i]["d_gain"] = stages_a[i].get( - "d_gain", d_gain - ) + stages_a[i]["d_gain"] = stages_a[i].get("d_gain", d_gain) elif i == 1: - t_n = environment.physical_system.tau * a**2 + t_n = environment.unwrapped.physical_system.tau * a**2 p_gain = ( - environment.physical_system.mechanical_load.j_total + environment.unwrapped.physical_system.mechanical_load.j_total / (a * t_n) / i_a_lim * omega_lim ) i_gain = p_gain / (a * t_n) - if ( - _controllers[stages_a[i]["controller_type"]][2] - == PIDController - ): - d_gain = p_gain * environment.physical_system.tau - stages_a[i]["d_gain"] = stages_a[i].get( - "d_gain", d_gain - ) + if _controllers[stages_a[i]["controller_type"]][2] == PIDController: + d_gain = p_gain * environment.unwrapped.physical_system.tau + stages_a[i]["d_gain"] = stages_a[i].get("d_gain", d_gain) - stages_a[i]["p_gain"] = stages_a[i].get( - "p_gain", p_gain - ) # ? - stages_a[i]["i_gain"] = stages_a[i].get( - "i_gain", i_gain - ) # ? + stages_a[i]["p_gain"] = stages_a[i].get("p_gain", p_gain) # ? + stages_a[i]["i_gain"] = stages_a[i].get("i_gain", i_gain) # ? stages = stages_a if not stages_e else [stages_a, stages_e] @@ -566,30 +470,12 @@ def automated_gain( if type(environment.action_space) == Box: stage_d = stages[0][0] stage_q = stages[0][1] - if ( - "i_sq" in ref_states - and _controllers[stage_q["controller_type"]][1] - == ContinuousController - ): - p_gain_d = ( - mp["l_d"] - / (1.5 * environment.physical_system.tau * a) - / u_sd_lim - * i_sd_lim - ) - i_gain_d = p_gain_d / ( - 1.5 * environment.physical_system.tau * a**2 - ) + if "i_sq" in ref_states and _controllers[stage_q["controller_type"]][1] == ContinuousController: + p_gain_d = mp["l_d"] / (1.5 * environment.physical_system.tau * a) / u_sd_lim * i_sd_lim + i_gain_d = p_gain_d / (1.5 * environment.physical_system.tau * a**2) - p_gain_q = ( - mp["l_q"] - / (1.5 * environment.physical_system.tau * a) - / u_sq_lim - * i_sq_lim - ) - i_gain_q = p_gain_q / ( - 1.5 * environment.physical_system.tau * a**2 - ) + p_gain_q = mp["l_q"] / (1.5 * environment.physical_system.tau * a) / u_sq_lim * i_sq_lim + i_gain_q = p_gain_q / (1.5 * environment.physical_system.tau * a**2) stage_d["p_gain"] = stage_d.get("p_gain", p_gain_d) stage_d["i_gain"] = stage_d.get("i_gain", i_gain_d) @@ -613,20 +499,10 @@ def automated_gain( if "torque" not in controller_kwargs["ref_states"]: overlaid = stages[1] - p_gain_d = ( - mp["l_d"] - / (1.5 * environment.physical_system.tau * a) - / u_sd_lim - * i_sd_lim - ) + p_gain_d = mp["l_d"] / (1.5 * environment.physical_system.tau * a) / u_sd_lim * i_sd_lim i_gain_d = p_gain_d / (1.5 * environment.physical_system.tau * a**2) - p_gain_q = ( - mp["l_q"] - / (1.5 * environment.physical_system.tau * a) - / u_sq_lim - * i_sq_lim - ) + p_gain_q = mp["l_q"] / (1.5 * environment.physical_system.tau * a) / u_sq_lim * i_sq_lim i_gain_q = p_gain_q / (1.5 * environment.physical_system.tau * a**2) stage_d["p_gain"] = stage_d.get("p_gain", p_gain_d) @@ -645,8 +521,7 @@ def automated_gain( if ( "torque" not in controller_kwargs["ref_states"] - and _controllers[overlaid[0]["controller_type"]][1] - == ContinuousController + and _controllers[overlaid[0]["controller_type"]][1] == ContinuousController ): t_n = p_gain_d / i_gain_d j_total = environment.physical_system.mechanical_load.j_total @@ -656,10 +531,7 @@ def automated_gain( overlaid[0]["p_gain"] = overlaid[0].get("p_gain", p_gain) overlaid[0]["i_gain"] = overlaid[0].get("i_gain", i_gain) - if ( - _controllers[overlaid[0]["controller_type"]][2] - == PIDController - ): + if _controllers[overlaid[0]["controller_type"]][2] == PIDController: d_gain = p_gain * environment.physical_system.tau overlaid[0]["d_gain"] = overlaid[0].get("d_gain", d_gain) @@ -671,8 +543,7 @@ def automated_gain( else: if ( "omega" in ref_states - and _controllers[stages[3][0]["controller_type"]][1] - == ContinuousController + and _controllers[stages[3][0]["controller_type"]][1] == ContinuousController ): p_gain = ( environment.physical_system.mechanical_load.j_total @@ -685,25 +556,15 @@ def automated_gain( stages[3][0]["p_gain"] = stages[3][0].get("p_gain", p_gain) stages[3][0]["i_gain"] = stages[3][0].get("i_gain", i_gain) - if ( - _controllers[stages[3][0]["controller_type"]][2] - == PIDController - ): + if _controllers[stages[3][0]["controller_type"]][2] == PIDController: d_gain = p_gain * environment.physical_system.tau stages[3][0]["d_gain"] = stages[3][0].get("d_gain", d_gain) - elif ( - _controllers[controller_type][0] - == InductionMotorFieldOrientedController - ): + elif _controllers[controller_type][0] == InductionMotorFieldOrientedController: mp["l_s"] = mp["l_m"] + mp["l_sigs"] mp["l_r"] = mp["l_m"] + mp["l_sigr"] - sigma = (mp["l_s"] * mp["l_r"] - mp["l_m"] ** 2) / ( - mp["l_s"] * mp["l_r"] - ) - tau_sigma = (sigma * mp["l_s"]) / ( - mp["r_s"] + mp["r_r"] * mp["l_m"] ** 2 / mp["l_r"] ** 2 - ) + sigma = (mp["l_s"] * mp["l_r"] - mp["l_m"] ** 2) / (mp["l_s"] * mp["l_r"]) + tau_sigma = (sigma * mp["l_s"]) / (mp["r_s"] + mp["r_r"] * mp["l_m"] ** 2 / mp["l_r"] ** 2) tau_r = mp["l_r"] / mp["r_r"] p_gain = tau_r / tau_sigma i_gain = p_gain / tau_sigma @@ -721,21 +582,14 @@ def automated_gain( d_gain = p_gain * tau_sigma stages[0][1]["d_gain"] = stages[0][1].get("d_gain", d_gain) - elif ( - _controllers[controller_type][0] - == InductionMotorCascadedFieldOrientedController - ): + elif _controllers[controller_type][0] == InductionMotorCascadedFieldOrientedController: if "torque" not in controller_kwargs["ref_states"]: overlaid = stages[1] mp["l_s"] = mp["l_m"] + mp["l_sigs"] mp["l_r"] = mp["l_m"] + mp["l_sigr"] - sigma = (mp["l_s"] * mp["l_r"] - mp["l_m"] ** 2) / ( - mp["l_s"] * mp["l_r"] - ) - tau_sigma = (sigma * mp["l_s"]) / ( - mp["r_s"] + mp["r_r"] * mp["l_m"] ** 2 / mp["l_r"] ** 2 - ) + sigma = (mp["l_s"] * mp["l_r"] - mp["l_m"] ** 2) / (mp["l_s"] * mp["l_r"]) + tau_sigma = (sigma * mp["l_s"]) / (mp["r_s"] + mp["r_r"] * mp["l_m"] ** 2 / mp["l_r"] ** 2) tau_r = mp["l_r"] / mp["r_r"] p_gain = tau_r / tau_sigma i_gain = p_gain / tau_sigma @@ -755,8 +609,7 @@ def automated_gain( if ( "torque" not in controller_kwargs["ref_states"] - and _controllers[overlaid[0]["controller_type"]][1] - == ContinuousController + and _controllers[overlaid[0]["controller_type"]][1] == ContinuousController ): t_n = p_gain / i_gain j_total = environment.physical_system.mechanical_load.j_total diff --git a/examples/classic_controllers/controllers/cascaded_controller.py b/examples/classic_controllers/controllers/cascaded_controller.py index 612976bf..e5b8e968 100644 --- a/examples/classic_controllers/controllers/cascaded_controller.py +++ b/examples/classic_controllers/controllers/cascaded_controller.py @@ -1,9 +1,10 @@ -from .continuous_controller import ContinuousController +import numpy as np from gymnasium.spaces import Box, Discrete, MultiDiscrete + from gym_electric_motor.physical_systems import DcExternallyExcitedMotor -from .plot_external_data import plot -import numpy as np +from .continuous_controller import ContinuousController +from .plot_external_data import plot class CascadedController: @@ -27,31 +28,27 @@ def __init__( self.env = environment self.visualization = visualization self.action_space = environment.action_space - self.state_space = environment.physical_system.state_space - self.state_names = environment.state_names - - self.i_e_idx = environment.physical_system.CURRENTS_IDX[-1] - self.i_a_idx = environment.physical_system.CURRENTS_IDX[0] - self.u_idx = environment.physical_system.VOLTAGES_IDX[-1] - self.omega_idx = environment.state_names.index("omega") - self.torque_idx = environment.state_names.index("torque") + self.state_space = environment.unwrapped.physical_system.state_space + self.state_names = environment.unwrapped.state_names + + self.i_e_idx = environment.unwrapped.physical_system.CURRENTS_IDX[-1] + self.i_a_idx = environment.unwrapped.physical_system.CURRENTS_IDX[0] + self.u_idx = environment.unwrapped.physical_system.VOLTAGES_IDX[-1] + self.omega_idx = environment.unwrapped.state_names.index("omega") + self.torque_idx = environment.unwrapped.state_names.index("torque") self.ref_idx = np.where(ref_states != "i_e")[0][0] self.ref_state_idx = [ self.i_a_idx, - environment.state_names.index(ref_states[self.ref_idx]), + environment.unwrapped.state_names.index(ref_states[self.ref_idx]), ] - self.limit = environment.physical_system.limits[environment.state_filter] - self.nominal_values = environment.physical_system.nominal_state[ - environment.state_filter - ] + self.limit = environment.unwrapped.physical_system.limits[environment.unwrapped.state_filter] + self.nominal_values = environment.unwrapped.physical_system.nominal_state[environment.unwrapped.state_filter] - self.control_e = isinstance( - environment.physical_system.electrical_motor, DcExternallyExcitedMotor - ) + self.control_e = isinstance(environment.unwrapped.physical_system.electrical_motor, DcExternallyExcitedMotor) self.control_omega = 0 - mp = environment.physical_system.electrical_motor.motor_parameter + mp = environment.unwrapped.physical_system.electrical_motor.motor_parameter self.psi_e = mp.get("psie_e", False) self.l_e = mp.get("l_e_prime", False) self.r_e = mp.get("r_e", None) @@ -59,15 +56,9 @@ def __init__( # Set the action limits if type(self.action_space) is Box: - self.action_limit_low = ( - self.action_space.low[0] - * self.nominal_values[self.u_idx] - / self.limit[self.u_idx] - ) + self.action_limit_low = self.action_space.low[0] * self.nominal_values[self.u_idx] / self.limit[self.u_idx] self.action_limit_high = ( - self.action_space.high[0] - * self.nominal_values[self.u_idx] - / self.limit[self.u_idx] + self.action_space.high[0] * self.nominal_values[self.u_idx] / self.limit[self.u_idx] ) # Set the state limits @@ -77,11 +68,7 @@ def __init__( # Initialize i_e Controller if needed if self.control_e: assert len(stages) == 2, "Controller design is incomplete" - self.ref_e_idx = ( - False - if "i_e" not in ref_states - else np.where(ref_states == "i_e")[0][0] - ) + self.ref_e_idx = False if "i_e" not in ref_states else np.where(ref_states == "i_e")[0][0] self.control_e_idx = 1 if self.omega_idx in self.ref_state_idx: @@ -101,15 +88,9 @@ def __init__( # Set action limit for u_e if type(self.action_space) is Box: - self.action_e_limit_low = ( - self.action_space.low[1] - * self.nominal_values[u_e_idx] - / self.limit[u_e_idx] - ) + self.action_e_limit_low = self.action_space.low[1] * self.nominal_values[u_e_idx] / self.limit[u_e_idx] self.action_e_limit_high = ( - self.action_space.high[1] - * self.nominal_values[u_e_idx] - / self.limit[u_e_idx] + self.action_space.high[1] * self.nominal_values[u_e_idx] / self.limit[u_e_idx] ) else: @@ -117,10 +98,7 @@ def __init__( assert len(ref_states) <= 1, "Too many referenced states" # Check of the stages are using continuous or discrete controller - self.stage_type = [ - _controllers[stage["controller_type"]][1] == ContinuousController - for stage in stages - ] + self.stage_type = [_controllers[stage["controller_type"]][1] == ContinuousController for stage in stages] # Initialize Controller stages self.controller_stages = [ @@ -132,23 +110,17 @@ def __init__( # Set up the plots self.external_ref_plots = external_ref_plots - internal_refs = np.array( - [environment.state_names[i] for i in self.ref_state_idx] - ) + internal_refs = np.array([environment.unwrapped.state_names[i] for i in self.ref_state_idx]) ref_states_plotted = np.unique(np.append(ref_states, internal_refs)) for external_plots in self.external_ref_plots: external_plots.set_reference(ref_states_plotted) - assert ( - type(self.action_space) is Box or not self.stage_type[0] - ), "No suitable inner controller" + assert type(self.action_space) is Box or not self.stage_type[0], "No suitable inner controller" assert ( type(self.action_space) in [Discrete, MultiDiscrete] or self.stage_type[0] ), "No suitable inner controller" - self.ref = np.zeros( - len(self.controller_stages) + self.control_e_idx + self.control_omega - ) + self.ref = np.zeros(len(self.controller_stages) + self.control_e_idx + self.control_omega) def control(self, state, reference): """ @@ -176,19 +148,13 @@ def control(self, state, reference): state_idx = self.ref_state_idx[ref_idx] # Calculate reference for lower stage - self.ref[ref_idx] = self.controller_stages[i].control( - state[state_idx], self.ref[ref_idx + 1] - ) + self.ref[ref_idx] = self.controller_stages[i].control(state[state_idx], self.ref[ref_idx + 1]) # Check limits and integrate if ( - self.state_limit_low[state_idx] - <= self.ref[ref_idx] - <= self.state_limit_high[state_idx] + self.state_limit_low[state_idx] <= self.ref[ref_idx] <= self.state_limit_high[state_idx] ) and self.stage_type[i]: - self.controller_stages[i].integrate( - state[self.ref_state_idx[i + self.control_omega]], reference[0] - ) + self.controller_stages[i].integrate(state[self.ref_state_idx[i + self.control_omega]], reference[0]) elif self.stage_type[i]: self.ref[ref_idx] = np.clip( @@ -201,9 +167,7 @@ def control(self, state, reference): if self.control_e: i_e = np.clip( np.power( - self.r_a - * (self.ref[1] * self.limit[self.torque_idx]) ** 2 - / (self.r_e * self.l_e**2), + self.r_a * (self.ref[1] * self.limit[self.torque_idx]) ** 2 / (self.r_e * self.l_e**2), 1 / 4, ), self.state_space.low[self.i_e_idx] * self.limit[self.i_e_idx], @@ -218,9 +182,7 @@ def control(self, state, reference): self.ref[0] = i_a / self.limit[self.i_a_idx] # Calculate action for u_a - action = self.controller_stages[0].control( - state[self.ref_state_idx[0]], self.ref[0] - ) + action = self.controller_stages[0].control(state[self.ref_state_idx[0]], self.ref[0]) # Check if stage is continuous if self.stage_type[0]: @@ -228,14 +190,10 @@ def control(self, state, reference): # Check limits and integrate if self.action_limit_low <= action <= self.action_limit_high: - self.controller_stages[0].integrate( - state[self.ref_state_idx[0]], self.ref[0] - ) + self.controller_stages[0].integrate(state[self.ref_state_idx[0]], self.ref[0]) action = [action] else: - action = np.clip( - [action], self.action_limit_low, self.action_limit_high - ) + action = np.clip([action], self.action_limit_low, self.action_limit_high) # Calculate action for u_e if needed if self.control_e: @@ -248,9 +206,7 @@ def control(self, state, reference): action = np.append(action, action_u_e) if self.action_e_limit_low <= action[1] <= self.action_e_limit_high: self.controller_e.integrate(state[self.i_e_idx], self.ref[-1]) - action = np.clip( - action, self.action_e_limit_low, self.action_e_limit_high - ) + action = np.clip(action, self.action_e_limit_low, self.action_e_limit_high) else: action = np.array([action, action_u_e], dtype="object") @@ -268,13 +224,10 @@ def control(self, state, reference): def feedforward(self, state): # EMF compensation psi_e = max( - self.psi_e - or self.l_e * state[self.i_e_idx] * self.nominal_values[self.i_e_idx], + self.psi_e or self.l_e * state[self.i_e_idx] * self.nominal_values[self.i_e_idx], 1e-6, ) - return ( - state[self.omega_idx] * self.nominal_values[self.omega_idx] * psi_e - ) / self.nominal_values[self.u_idx] + return (state[self.omega_idx] * self.nominal_values[self.omega_idx] * psi_e) / self.nominal_values[self.u_idx] def get_plot_data(self): # Getting the external data that should be plotted diff --git a/examples/classic_controllers/controllers/pi_controller.py b/examples/classic_controllers/controllers/pi_controller.py index 246f41d4..a07c67a5 100644 --- a/examples/classic_controllers/controllers/pi_controller.py +++ b/examples/classic_controllers/controllers/pi_controller.py @@ -1,4 +1,4 @@ -from .continuous_controller import PController, IController +from .continuous_controller import IController, PController class PIController(PController, IController): @@ -8,10 +8,8 @@ class PIController(PController, IController): the I-component of the controller accordingly. """ - def __init__( - self, environment, p_gain=5, i_gain=5, param_dict={}, **controller_kwargs - ): - self.tau = environment.physical_system.tau + def __init__(self, environment, p_gain=5, i_gain=5, param_dict={}, **controller_kwargs): + self.tau = environment.unwrapped.physical_system.tau p_gain = param_dict.get("p_gain", p_gain) i_gain = param_dict.get("i_gain", i_gain) @@ -19,9 +17,7 @@ def __init__( IController.__init__(self, i_gain) def control(self, state, reference): - return self.p_gain * (reference - state) + self.i_gain * ( - self.integrated + (reference - state) * self.tau - ) + return self.p_gain * (reference - state) + self.i_gain * (self.integrated + (reference - state) * self.tau) def reset(self): self.integrated = 0 diff --git a/src/gym_electric_motor/__init__.py b/src/gym_electric_motor/__init__.py index 544febaa..bed713e5 100644 --- a/src/gym_electric_motor/__init__.py +++ b/src/gym_electric_motor/__init__.py @@ -28,11 +28,11 @@ # Add all superclasses of the modules to the registry. # Deactivate the order enforce wrapper that is put around a created env per default from gymnasium-version 0.21.0 onwards -registration_kwargs = ( - dict(order_enforce=False) if version.parse(gymnasium.__version__) >= version.parse("0.21.0") else dict() -) -registration_kwargs["disable_env_checker"] = True -# registration_kwargs = dict() +# registration_kwargs = ( +# dict(order_enforce=False) if version.parse(gymnasium.__version__) >= version.parse("0.21.0") else dict() +# ) +# registration_kwargs["disable_env_checker"] = True +registration_kwargs = dict() envs_path = "gym_electric_motor.envs:" diff --git a/src/gym_electric_motor/core.py b/src/gym_electric_motor/core.py index f3ab684d..6e820983 100644 --- a/src/gym_electric_motor/core.py +++ b/src/gym_electric_motor/core.py @@ -297,7 +297,7 @@ def _call_callbacks(self, func_name, *args): func = getattr(callback, func_name) func(*args) - def reset(self, seed=None, *_, **__): + def reset(self, seed=None, options=None, *_, **__): """ Reset of the environment and all its modules to an initial state. diff --git a/src/gym_electric_motor/observers/observer.py b/src/gym_electric_motor/observers/observer.py index 0dac9e85..c46e5034 100644 --- a/src/gym_electric_motor/observers/observer.py +++ b/src/gym_electric_motor/observers/observer.py @@ -30,8 +30,8 @@ def __init__(self, env: ElectricMotorEnvironment): self.fuse_state_and_next_reference() def fuse_state_and_next_reference(self): - state_names = self.env.state_names - reference_names = self.env.reference_names + state_names = self.env.unwrapped.state_names + reference_names = self.env.unwrapped.reference_names for ref_name in reference_names: state_names.append(ref_name + "_ref") self.state_names = state_names From 96dc713973d212caac15aa765f912204a13d10c1 Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Fri, 1 Mar 2024 12:41:51 +0100 Subject: [PATCH 33/51] fix test --- src/gym_electric_motor/core.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/gym_electric_motor/core.py b/src/gym_electric_motor/core.py index 6e820983..75bffbd2 100644 --- a/src/gym_electric_motor/core.py +++ b/src/gym_electric_motor/core.py @@ -297,7 +297,7 @@ def _call_callbacks(self, func_name, *args): func = getattr(callback, func_name) func(*args) - def reset(self, seed=None, options=None, *_, **__): + def reset(self, seed=None, *_, **__): """ Reset of the environment and all its modules to an initial state. @@ -349,7 +349,8 @@ def step(self, action): reward = self._reward_function.reward(state, reference, self._physical_system.k, action, violation_degree) self._terminated = violation_degree >= 1.0 ref_next = self.reference_generator.get_reference_observation(state) - self.current_state + [ref_next] + + self.current_observation = (state, ref_next) self._call_callbacks( "on_step_end", From df6b27ea36e0fc475dd4cc4fbf507241f63406c2 Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Thu, 7 Mar 2024 20:03:19 +0100 Subject: [PATCH 34/51] Working tests with env checker. See warnings for further refactoring --- src/gym_electric_motor/core.py | 2 +- tests/integration_tests/test_integration.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gym_electric_motor/core.py b/src/gym_electric_motor/core.py index 75bffbd2..47b5f2aa 100644 --- a/src/gym_electric_motor/core.py +++ b/src/gym_electric_motor/core.py @@ -297,7 +297,7 @@ def _call_callbacks(self, func_name, *args): func = getattr(callback, func_name) func(*args) - def reset(self, seed=None, *_, **__): + def reset(self, seed=None, options=None, *_, **__): """ Reset of the environment and all its modules to an initial state. diff --git a/tests/integration_tests/test_integration.py b/tests/integration_tests/test_integration.py index 80863bb0..f3c38e27 100644 --- a/tests/integration_tests/test_integration.py +++ b/tests/integration_tests/test_integration.py @@ -48,7 +48,7 @@ def simulate_env(seed=None): """ controller = Controller.make(env) - (state, reference), _ = env.reset(seed) + (state, reference), _ = env.reset(seed=seed) test_states = [] test_reference = [] From 540b0ad727887d5545cf0971f14e8839a1265be5 Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Thu, 7 Mar 2024 20:23:32 +0100 Subject: [PATCH 35/51] Changes for release 2.0.1 --- CHANGELOG.md | 12 ++++++++++++ examples/observers/state_observer_example.py | 3 ++- pyproject.toml | 2 +- src/gym_electric_motor/observers/observer.py | 3 ++- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0150ba1a..1e17190d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Changed ## Fixed +## [2.0.1] - Unreleased +## Added +- Ruff: Python linter & formatter (see [DEVELOPMENT.md](DEVELOPMENT.md)) +- StateObserver: An easy way to get state values with error checking [example](examples/observers/state_observer_example.py) +- Integrated gem_controls repository into gem. classic_controllers will be removed in further version +- Using pyproject.toml, dropping deprecated setup.py +- Enabled Gymnasium env checker [see here](https://gymnasium.farama.org/api/experimental/wrappers/#gymnasium.experimental.wrappers.PassiveEnvCheckerV0) +## Changed +- Linted and formatted all files +- Changed max. steps in some test files to improve test speed by 30% +## Fixed + ## [2.0.0] - 2023-08-15 ## Added - Support for Python 3.10 diff --git a/examples/observers/state_observer_example.py b/examples/observers/state_observer_example.py index 46bf1d35..443837c3 100644 --- a/examples/observers/state_observer_example.py +++ b/examples/observers/state_observer_example.py @@ -79,7 +79,8 @@ # (state, reference), reward, terminated, truncated, _ = env.step(env.action_space.sample()) # else: (state, reference), reward, terminated, truncated, _ = env.step(action) - + torque = state_observer.observe("torque") + print(f"Toque: {torque}") # viz.render() if terminated: diff --git a/pyproject.toml b/pyproject.toml index 790ac3e2..74207a76 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "gym_electric_motor" -version = "2.1.0" +version = "2.0.1" authors = [ { name = "Arne Traue" }, { name = "Gerrit Book" }, diff --git a/src/gym_electric_motor/observers/observer.py b/src/gym_electric_motor/observers/observer.py index c46e5034..eb489ae2 100644 --- a/src/gym_electric_motor/observers/observer.py +++ b/src/gym_electric_motor/observers/observer.py @@ -12,6 +12,7 @@ def __init__(self, env: ElectricMotorEnvironment): def observe(self, *args, **kwargs) -> any: result = self.pull(*args, **kwargs) self.push(result) + return result @abc.abstractmethod def pull(self, *args, **kwargs) -> any: @@ -51,4 +52,4 @@ def pull(self, state: str) -> any: raise ValueError(f"State '{state}' not found in state_names, allowed states are {state_names}") def push(self, value): - print(f"PUSH | {value} \n") + pass From 40c336732bcd921e5df5ec1e200c2f1b56013244 Mon Sep 17 00:00:00 2001 From: Maximilian Schenke Date: Tue, 23 Apr 2024 10:39:17 +0200 Subject: [PATCH 36/51] introduce transfer ratio k between rotor and stator windings --- .../externally_excited_synchronous_motor.py | 83 ++++++++++--------- 1 file changed, 46 insertions(+), 37 deletions(-) diff --git a/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py b/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py index 077da978..63012956 100644 --- a/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py +++ b/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py @@ -6,9 +6,9 @@ class ExternallyExcitedSynchronousMotor(SynchronousMotor): """ - ===================== ========== ============= =========================================== + ===================== ========== ============= ================================================= Motor Parameter Unit Default Value Description - ===================== ========== ============= =========================================== + ===================== ========== ============= ================================================= r_s mOhm 15.55 Stator resistance r_e mOhm 7.2 Excitation resistance l_d mH 1.66 Direct axis inductance @@ -16,12 +16,13 @@ class ExternallyExcitedSynchronousMotor(SynchronousMotor): l_m mH 1.589 Mutual inductance l_e mH 1.74 Excitation inductance p 1 3 Pole pair number + k 1 65.21 Transfer ratio of number of windings (N_s / N_e) j_rotor kg/m^2 0.3883 Moment of inertia of the rotor - ===================== ========== ============= =========================================== + ===================== ========== ============= ================================================= - =============== ====== ============================================= + =============== ====== =========================================================================== Motor Currents Unit Description - =============== ====== ============================================= + =============== ====== =========================================================================== i_sd A Direct axis current i_sq A Quadrature axis current i_e A Excitation current @@ -30,10 +31,10 @@ class ExternallyExcitedSynchronousMotor(SynchronousMotor): i_c A Current through branch c i_alpha A Current in alpha axis i_beta A Current in beta axis - =============== ====== ============================================= - =============== ====== ============================================= + =============== ====== =========================================================================== + =============== ====== =========================================================================== Motor Voltages Unit Description - =============== ====== ============================================= + =============== ====== =========================================================================== u_sd V Direct axis voltage u_sq V Quadrature axis voltage u_e V Exciting voltage @@ -42,13 +43,13 @@ class ExternallyExcitedSynchronousMotor(SynchronousMotor): u_c V Voltage through branch c u_alpha V Voltage in alpha axis u_beta V Voltage in beta axis - =============== ====== ============================================= + =============== ====== =========================================================================== - ======== =========================================================== + ======== ========================================================================================= Limits / Nominal Value Dictionary Entries: - -------- ----------------------------------------------------------- + -------- ----------------------------------------------------------------------------------------- Entry Description - ======== =========================================================== + ======== ========================================================================================= i General current limit / nominal value i_a Current in phase a i_b Current in phase b @@ -69,7 +70,7 @@ class ExternallyExcitedSynchronousMotor(SynchronousMotor): u_sd Voltage in direct axis u_sq Voltage in quadrature axis u_e Voltage in excitation circuit - ======== =========================================================== + ======== ========================================================================================= Note: @@ -105,6 +106,7 @@ class ExternallyExcitedSynchronousMotor(SynchronousMotor): 'j_rotor': 0.3883, 'r_s': 15.55e-3, 'r_e': 7.2e-3, + 'k': 65.21 } HAS_JACOBIAN = True _default_limits = dict(omega=12e3 * np.pi / 30, torque=0.0, i=150, i_e=150, epsilon=math.pi, u=320) @@ -122,20 +124,28 @@ class ExternallyExcitedSynchronousMotor(SynchronousMotor): def _update_model(self): # Docstring of superclass mp = self._motor_parameter - sigma = 1 - mp['l_m'] ** 2 / (mp['l_d'] * mp['l_e']) + + # Transform rotor quantities to stator side (uppercase index denotes transformation to stator side) + mp['r_E'] = mp['k'] ** 2 * 3/2 * mp['r_e'] + mp['l_M'] = mp['k'] * 3/2 * mp['l_m'] + mp['l_E'] = mp['k'] ** 2 * 3/2 * mp['l_e'] + + mp['i_k_rs'] = 2 / 3 / mp['k'] # ratio (i_E / i_e) + mp['sigma'] = 1 - mp['l_M'] ** 2 / (mp['l_d'] * mp['l_E']) * 77 + self._model_constants = np.array([ - # omega, i_d, i_q, i_e, u_d, u_q, u_e, omega * i_d, omega * i_q, omega * i_e - [ 0, -mp['r_s'] / sigma, 0, mp['l_m'] * mp['r_e'] / (sigma * mp['l_e']), 1 / sigma, 0, -mp['l_m'] / (sigma * mp['l_e']), 0, mp['l_q'] * mp['p'] / sigma, 0], - [ 0, 0, -mp['r_s'], 0, 0, 1, 0, -mp['l_d'] * mp['p'], 0, -mp['p'] * mp['l_m']], - [ 0, mp['l_m'] * mp['r_s'] / (sigma * mp['l_d']), 0, -mp['r_e'] / sigma, -mp['l_m'] / (sigma * mp['l_d']), 0, 1 / sigma, 0, -mp['p'] * mp['l_m'] * mp['l_q'] / (sigma * mp['l_d']), 0], - [ mp['p'], 0, 0, 0, 0, 0, 0, 0, 0, 0], + # omega, i_d, i_q, i_e, u_d, u_q, u_e, omega * i_d, omega * i_q, omega * i_e + [ 0, -mp['r_s'] / mp['sigma'], 0, mp['l_M'] * mp['r_E'] / (mp['sigma'] * mp['l_E']) * mp['i_k_rs'], 1 / mp['sigma'], 0, -mp['l_M'] * mp['k'] / (mp['sigma'] * mp['l_E']), 0, mp['l_q'] * mp['p'] / mp['sigma'], 0], + [ 0, 0, -mp['r_s'], 0, 0, 1, 0, -mp['l_d'] * mp['p'], 0, -mp['p'] * mp['l_M'] * mp['i_k_rs']], + [ 0, mp['l_M'] * mp['r_s'] / (mp['sigma'] * mp['l_d']), 0, -mp['r_E'] / mp['sigma'] * mp['i_k_rs'], -mp['l_M'] / (mp['sigma'] * mp['l_d']), 0, mp['k'] / mp['sigma'], 0, -mp['p'] * mp['l_M'] * mp['l_q'] / (mp['sigma'] * mp['l_d']), 0], + [ mp['p'], 0, 0, 0, 0, 0, 0, 0, 0, 0], ]) self._model_constants[self.I_SD_IDX] = self._model_constants[self.I_SD_IDX] / mp['l_d'] self._model_constants[self.I_SQ_IDX] = self._model_constants[self.I_SQ_IDX] / mp['l_q'] - self._model_constants[self.I_E_IDX] = self._model_constants[self.I_E_IDX] / mp['l_e'] + self._model_constants[self.I_E_IDX] = self._model_constants[self.I_E_IDX] / mp['l_E'] / mp['i_k_rs'] - def electrical_ode(self, state, u_dq, omega, *_): + def electrical_ode(self, state, u_dqe, omega, *_): """ The differential equation of the Synchronous Motor. @@ -152,9 +162,9 @@ def electrical_ode(self, state, u_dq, omega, *_): state[self.I_SD_IDX], state[self.I_SQ_IDX], state[self.I_E_IDX], - u_dq[0], - u_dq[1], - u_dq[2], + u_dqe[0], + u_dqe[1], + u_dqe[2], omega * state[self.I_SD_IDX], omega * state[self.I_SQ_IDX], omega * state[self.I_E_IDX] @@ -167,40 +177,39 @@ def _torque_limit(self): return self.torque([0, self._limits['i_sq'], self._limits['i_e'], 0]) else: i_n = self.nominal_values['i'] - _p = mp['l_m'] * i_n / (2 * (mp['l_d'] - mp['l_q'])) + _p = mp['l_M'] * i_n / (2 * (mp['l_d'] - mp['l_q'])) _q = - i_n ** 2 / 2 if mp['l_d'] < mp['l_q']: - i_d_opt = - _p / 2 - np.sqrt( (_p / 2) ** 2 - _q) + i_d_opt = - _p / 2 - np.sqrt((_p / 2) ** 2 - _q) else: - i_d_opt = - _p / 2 + np.sqrt( (_p / 2) ** 2 - _q) + i_d_opt = - _p / 2 + np.sqrt((_p / 2) ** 2 - _q) i_q_opt = np.sqrt(i_n ** 2 - i_d_opt ** 2) return self.torque([i_d_opt, i_q_opt, self._limits['i_e'], 0]) def torque(self, currents): # Docstring of superclass mp = self._motor_parameter - return 1.5 * mp['p'] * (mp['l_m'] * currents[self.I_E_IDX] + (mp['l_d'] - mp['l_q']) * currents[self.I_SD_IDX]) * currents[self.I_SQ_IDX] + return 1.5 * mp['p'] * (mp['l_M'] * currents[self.I_E_IDX] * mp['i_k_rs'] + (mp['l_d'] - mp['l_q']) * currents[self.I_SD_IDX]) * currents[self.I_SQ_IDX] def electrical_jacobian(self, state, u_in, omega, *args): mp = self._motor_parameter - sigma = 1 - mp['l_m'] ** 2 / (mp['l_d'] * mp['l_e']) return ( np.array([ # dx'/dx - [ -mp['r_s'] / mp['l_d'], mp['l_q'] / (sigma * mp['l_d']) * omega * mp['p'], mp['l_m'] * mp['r_e'] / (sigma * mp['l_d'] * mp['l_e']), 0], - [ -mp['l_d'] / mp['l_q'] * omega * mp['p'], -mp['r_s'] / mp['l_q'], -omega * mp['p'] * mp['l_e'] / mp['l_q'], 0], - [mp['l_m'] * mp['r_s'] / (sigma * mp['l_d'] * mp['l_e']), -omega * mp['p'] * mp['l_m'] * mp['l_q'] / (sigma * mp['l_d'] * mp['l_e']), -mp['r_e'] / mp['l_e'], 0], - [ 0, 0, 0, 0], + [ -mp['r_s'] / (mp['l_d'] * mp['sigma']), mp['l_q'] / (mp['sigma'] * mp['l_d']) * omega * mp['p'], mp['l_M'] * mp['r_e'] / (mp['sigma'] * mp['l_d'] * mp['l_E']) * mp['i_k_rs'], 0], + [ -mp['l_d'] / mp['l_q'] * omega * mp['p'], -mp['r_s'] / mp['l_q'], -omega * mp['p'] * mp['l_E'] / mp['l_q'] * mp['i_k_rs'], 0], + [mp['l_M'] * mp['r_s'] / (mp['sigma'] * mp['l_d'] * mp['l_E']), -omega * mp['p'] * mp['l_M'] * mp['l_q'] / (mp['sigma'] * mp['l_d'] * mp['l_E']), -mp['r_E'] / mp['l_E'] * mp['i_k_rs'], 0], + [ 0, 0, 0, 0], ]), np.array([ # dx'/dw mp['p'] * mp['l_q'] / mp['l_d'] * state[self.I_SQ_IDX], - -mp['p'] * mp['l_d'] / mp['l_q'] * state[self.I_SD_IDX] - mp['p'] * mp['l_m'] / mp['l_q'] * state[self.I_E_IDX], - -mp['p'], + -mp['p'] * mp['l_d'] / mp['l_q'] * state[self.I_SD_IDX] - mp['p'] * mp['l_M'] / mp['l_q'] * state[self.I_E_IDX] * mp['i_k_rs'], + -mp['p'] * mp['l_M'] * mp['l_q'] / (mp['sigma'] * mp['l_d'] * mp['l_E']), mp['p'], ]), np.array([ # dT/dx 1.5 * mp['p'] * (mp['l_d'] - mp['l_q']) * state[self.I_SQ_IDX], - 1.5 * mp['p'] * (mp['l_e'] * state[self.I_E_IDX] + (mp['l_d'] - mp['l_q']) * state[self.I_SD_IDX]), - 1.5 * mp['p'] * mp['l_e'] * state[self.I_SQ_IDX], + 1.5 * mp['p'] * (mp['l_E'] * state[self.I_E_IDX] * mp['i_k_rs'] + (mp['l_d'] - mp['l_q']) * state[self.I_SD_IDX]), + 1.5 * mp['p'] * mp['l_E'] * state[self.I_SQ_IDX], 0, ]) ) From 1389548cf7fe60b487fa8a2f1d5610610735e19e Mon Sep 17 00:00:00 2001 From: XyDrKRulof Date: Thu, 25 Apr 2024 10:17:55 +0200 Subject: [PATCH 37/51] (Hopefully) fixed a bug where the Sphinx build failed due to an old version of Sphinx being pre-installed --- .github/workflows/build_and_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 9760afc9..b75c9996 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -41,7 +41,7 @@ jobs: - name: Build Sphinx documentation uses: ammaraskar/sphinx-action@master with: - pre-build-command: "python -m pip install sphinx m2r2 sphinx_rtd_theme && python -m pip install -r requirements.txt & python -m pip install ." + pre-build-command: "python -m pip install --upgrade sphinx m2r2 sphinx_rtd_theme && python -m pip install -r requirements.txt & python -m pip install ." docs-folder: "docs/" # Publish built docs to gh-pages branch. # =============================== From 5f52f8d25cdb195ff5d8082d67caed8400fd08b2 Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Mon, 6 May 2024 09:01:51 +0200 Subject: [PATCH 38/51] fixed gui pop up in tests --- .../visualization/motor_dashboard.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/gym_electric_motor/visualization/motor_dashboard.py b/src/gym_electric_motor/visualization/motor_dashboard.py index 7f26ffe8..3ce54382 100644 --- a/src/gym_electric_motor/visualization/motor_dashboard.py +++ b/src/gym_electric_motor/visualization/motor_dashboard.py @@ -8,14 +8,7 @@ from gym_electric_motor.core import ElectricMotorVisualization -from .motor_dashboard_plots import ( - ActionPlot, - EpisodePlot, - RewardPlot, - StatePlot, - StepPlot, - TimePlot, -) +from .motor_dashboard_plots import ActionPlot, EpisodePlot, RewardPlot, StatePlot, StepPlot, TimePlot from .render_modes import RenderMode @@ -316,9 +309,9 @@ def _update(self): # Proxy Object for Refactoring class MotorDashboard(MotorDashboardLegacy): - render_mode = RenderMode.Figure + render_mode = None - def __init__(self, render_mode=RenderMode.Figure, *args, **kwargs): + def __init__(self, render_mode=None, *args, **kwargs): super().__init__(*args, **kwargs) self.render_mode = render_mode From 62b0dd2773f10c7b228d894e9d05394a7a303083 Mon Sep 17 00:00:00 2001 From: XyDrKRulof Date: Fri, 10 May 2024 11:33:14 +0200 Subject: [PATCH 39/51] Potential fix to the Sphinx Doc disappearence error --- .github/workflows/build_and_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index b75c9996..7dd79c3c 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -41,7 +41,7 @@ jobs: - name: Build Sphinx documentation uses: ammaraskar/sphinx-action@master with: - pre-build-command: "python -m pip install --upgrade sphinx m2r2 sphinx_rtd_theme && python -m pip install -r requirements.txt & python -m pip install ." + pre-build-command: "python -m pip install sphinx m2r2 sphinx_rtd_theme==1.3.0 && python -m pip install -r requirements.txt & python -m pip install ." docs-folder: "docs/" # Publish built docs to gh-pages branch. # =============================== From 3977a58fe9455f92be18d1895b26ea72b99feee3 Mon Sep 17 00:00:00 2001 From: Maximilian Schenke Date: Mon, 13 May 2024 09:35:03 +0200 Subject: [PATCH 40/51] remove magic number --- .../electric_motors/externally_excited_synchronous_motor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py b/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py index 63012956..a6ddd9b4 100644 --- a/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py +++ b/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py @@ -131,7 +131,7 @@ def _update_model(self): mp['l_E'] = mp['k'] ** 2 * 3/2 * mp['l_e'] mp['i_k_rs'] = 2 / 3 / mp['k'] # ratio (i_E / i_e) - mp['sigma'] = 1 - mp['l_M'] ** 2 / (mp['l_d'] * mp['l_E']) * 77 + mp['sigma'] = 1 - mp['l_M'] ** 2 / (mp['l_d'] * mp['l_E']) self._model_constants = np.array([ # omega, i_d, i_q, i_e, u_d, u_q, u_e, omega * i_d, omega * i_q, omega * i_e From 4e690511164f1f3b85cab5082b04895c5e8a59bc Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Mon, 13 May 2024 15:47:09 +0200 Subject: [PATCH 41/51] python 3.8 deprecated, added python 3.11 --- .github/workflows/build_and_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 9760afc9..6ffc7855 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.8, 3.9, "3.10"] + python-version: [3.9, "3.10", "3.11"] steps: - uses: actions/checkout@v3 From d22d30bccbd082d74c255e85a4ed377f014554ed Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Tue, 14 May 2024 07:31:38 +0200 Subject: [PATCH 42/51] manual formatting for some matrices --- .../externally_excited_synchronous_motor.py | 112 +++++------------- .../permanent_magnet_synchronous_motor.py | 17 ++- .../synchronous_reluctance_motor.py | 17 ++- .../electric_motors/three_phase_motor.py | 13 +- 4 files changed, 56 insertions(+), 103 deletions(-) diff --git a/src/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py index 1ae2cae9..8d6d2af9 100644 --- a/src/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py @@ -124,49 +124,15 @@ def _update_model(self): # Docstring of superclass mp = self._motor_parameter sigma = 1 - mp["l_m"] ** 2 / (mp["l_d"] * mp["l_e"]) - self._model_constants = np.array( - [ - # omega, i_d, i_q, i_e, u_d, u_q, u_e, omega * i_d, omega * i_q, omega * i_e - [ - 0, - -mp["r_s"] / sigma, - 0, - mp["l_m"] * mp["r_e"] / (sigma * mp["l_e"]), - 1 / sigma, - 0, - -mp["l_m"] / (sigma * mp["l_e"]), - 0, - mp["l_q"] * mp["p"] / sigma, - 0, - ], - [ - 0, - 0, - -mp["r_s"], - 0, - 0, - 1, - 0, - -mp["l_d"] * mp["p"], - 0, - -mp["p"] * mp["l_m"], - ], - [ - 0, - mp["l_m"] * mp["r_s"] / (sigma * mp["l_d"]), - 0, - -mp["r_e"] / sigma, - -mp["l_m"] / (sigma * mp["l_d"]), - 0, - 1 / sigma, - 0, - -mp["p"] * mp["l_m"] * mp["l_q"] / (sigma * mp["l_d"]), - 0, - ], - [mp["p"], 0, 0, 0, 0, 0, 0, 0, 0, 0], - ] - ) - + # fmt: off + self._model_constants = np.array([ + # omega, i_d, i_q, i_e, u_d, u_q, u_e, omega * i_d, omega * i_q, omega * i_e + [ 0, -mp['r_s'] / sigma, 0, mp['l_m'] * mp['r_e'] / (sigma * mp['l_e']), 1 / sigma, 0, -mp['l_m'] / (sigma * mp['l_e']), 0, mp['l_q'] * mp['p'] / sigma, 0], + [ 0, 0, -mp['r_s'], 0, 0, 1, 0, -mp['l_d'] * mp['p'], 0, -mp['p'] * mp['l_m']], + [ 0, mp['l_m'] * mp['r_s'] / (sigma * mp['l_d']), 0, -mp['r_e'] / sigma, -mp['l_m'] / (sigma * mp['l_d']), 0, 1 / sigma, 0, -mp['p'] * mp['l_m'] * mp['l_q'] / (sigma * mp['l_d']), 0], + [ mp['p'], 0, 0, 0, 0, 0, 0, 0, 0, 0], + ]) + # fmt: on self._model_constants[self.I_SD_IDX] = self._model_constants[self.I_SD_IDX] / mp["l_d"] self._model_constants[self.I_SQ_IDX] = self._model_constants[self.I_SQ_IDX] / mp["l_q"] self._model_constants[self.I_E_IDX] = self._model_constants[self.I_E_IDX] / mp["l_e"] @@ -230,45 +196,25 @@ def torque(self, currents): def electrical_jacobian(self, state, u_in, omega, *args): mp = self._motor_parameter sigma = 1 - mp["l_m"] ** 2 / (mp["l_d"] * mp["l_e"]) + # fmt: off return ( - np.array( - [ # dx'/dx - [ - -mp["r_s"] / mp["l_d"], - mp["l_q"] / (sigma * mp["l_d"]) * omega * mp["p"], - mp["l_m"] * mp["r_e"] / (sigma * mp["l_d"] * mp["l_e"]), - 0, - ], - [ - -mp["l_d"] / mp["l_q"] * omega * mp["p"], - -mp["r_s"] / mp["l_q"], - -omega * mp["p"] * mp["l_e"] / mp["l_q"], - 0, - ], - [ - mp["l_m"] * mp["r_s"] / (sigma * mp["l_d"] * mp["l_e"]), - -omega * mp["p"] * mp["l_m"] * mp["l_q"] / (sigma * mp["l_d"] * mp["l_e"]), - -mp["r_e"] / mp["l_e"], - 0, - ], - [0, 0, 0, 0], - ] - ), - np.array( - [ # dx'/dw - mp["p"] * mp["l_q"] / mp["l_d"] * state[self.I_SQ_IDX], - -mp["p"] * mp["l_d"] / mp["l_q"] * state[self.I_SD_IDX] - - mp["p"] * mp["l_m"] / mp["l_q"] * state[self.I_E_IDX], - -mp["p"], - mp["p"], - ] - ), - np.array( - [ # dT/dx - 1.5 * mp["p"] * (mp["l_d"] - mp["l_q"]) * state[self.I_SQ_IDX], - 1.5 * mp["p"] * (mp["l_e"] * state[self.I_E_IDX] + (mp["l_d"] - mp["l_q"]) * state[self.I_SD_IDX]), - 1.5 * mp["p"] * mp["l_e"] * state[self.I_SQ_IDX], - 0, - ] - ), + np.array([ # dx'/dx + [ -mp['r_s'] / mp['l_d'], mp['l_q'] / (sigma * mp['l_d']) * omega * mp['p'], mp['l_m'] * mp['r_e'] / (sigma * mp['l_d'] * mp['l_e']), 0], + [ -mp['l_d'] / mp['l_q'] * omega * mp['p'], -mp['r_s'] / mp['l_q'], -omega * mp['p'] * mp['l_e'] / mp['l_q'], 0], + [mp['l_m'] * mp['r_s'] / (sigma * mp['l_d'] * mp['l_e']), -omega * mp['p'] * mp['l_m'] * mp['l_q'] / (sigma * mp['l_d'] * mp['l_e']), -mp['r_e'] / mp['l_e'], 0], + [ 0, 0, 0, 0], + ]), + np.array([ # dx'/dw + mp['p'] * mp['l_q'] / mp['l_d'] * state[self.I_SQ_IDX], + -mp['p'] * mp['l_d'] / mp['l_q'] * state[self.I_SD_IDX] - mp['p'] * mp['l_m'] / mp['l_q'] * state[self.I_E_IDX], + -mp['p'], + mp['p'], + ]), + np.array([ # dT/dx + 1.5 * mp['p'] * (mp['l_d'] - mp['l_q']) * state[self.I_SQ_IDX], + 1.5 * mp['p'] * (mp['l_e'] * state[self.I_E_IDX] + (mp['l_d'] - mp['l_q']) * state[self.I_SD_IDX]), + 1.5 * mp['p'] * mp['l_e'] * state[self.I_SQ_IDX], + 0, + ]) ) + # fmt: on diff --git a/src/gym_electric_motor/physical_systems/electric_motors/permanent_magnet_synchronous_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/permanent_magnet_synchronous_motor.py index 11353963..c2e098f2 100644 --- a/src/gym_electric_motor/physical_systems/electric_motors/permanent_magnet_synchronous_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/permanent_magnet_synchronous_motor.py @@ -107,15 +107,14 @@ class PermanentMagnetSynchronousMotor(SynchronousMotor): def _update_model(self): # Docstring of superclass mp = self._motor_parameter - self._model_constants = np.array( - [ - # omega, i_d, i_q, u_d, u_q, omega * i_d, omega * i_q - [0, -mp["r_s"], 0, 1, 0, 0, mp["l_q"] * mp["p"]], - [-mp["psi_p"] * mp["p"], 0, -mp["r_s"], 0, 1, -mp["l_d"] * mp["p"], 0], - [mp["p"], 0, 0, 0, 0, 0, 0], - ] - ) - + # fmt: off + self._model_constants = np.array([ + # omega, i_d, i_q, u_d, u_q, omega * i_d, omega * i_q + [ 0, -mp['r_s'], 0, 1, 0, 0, mp['l_q'] * mp['p']], + [-mp['psi_p'] * mp['p'], 0, -mp['r_s'], 0, 1, -mp['l_d'] * mp['p'], 0], + [ mp['p'], 0, 0, 0, 0, 0, 0], + ]) + # fmt: on self._model_constants[self.I_SD_IDX] = self._model_constants[self.I_SD_IDX] / mp["l_d"] self._model_constants[self.I_SQ_IDX] = self._model_constants[self.I_SQ_IDX] / mp["l_q"] diff --git a/src/gym_electric_motor/physical_systems/electric_motors/synchronous_reluctance_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/synchronous_reluctance_motor.py index e34be75f..603b953a 100644 --- a/src/gym_electric_motor/physical_systems/electric_motors/synchronous_reluctance_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/synchronous_reluctance_motor.py @@ -118,15 +118,14 @@ def _update_model(self): # Docstring of superclass mp = self._motor_parameter - self._model_constants = np.array( - [ - # omega, i_sd, i_sq, u_sd, u_sq, omega * i_sd, omega * i_sq - [0, -mp["r_s"], 0, 1, 0, 0, mp["l_q"] * mp["p"]], - [0, 0, -mp["r_s"], 0, 1, -mp["l_d"] * mp["p"], 0], - [mp["p"], 0, 0, 0, 0, 0, 0], - ] - ) - + # fmt: off + self._model_constants = np.array([ + # omega, i_sd, i_sq, u_sd, u_sq, omega * i_sd, omega * i_sq + [ 0, -mp['r_s'], 0, 1, 0, 0, mp['l_q'] * mp['p']], + [ 0, 0, -mp['r_s'], 0, 1, -mp['l_d'] * mp['p'], 0], + [mp['p'], 0, 0, 0, 0, 0, 0] + ]) + # fmt: on self._model_constants[self.I_SD_IDX] = self._model_constants[self.I_SD_IDX] / mp["l_d"] self._model_constants[self.I_SQ_IDX] = self._model_constants[self.I_SQ_IDX] / mp["l_q"] diff --git a/src/gym_electric_motor/physical_systems/electric_motors/three_phase_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/three_phase_motor.py index dc6c885f..9b1460ee 100644 --- a/src/gym_electric_motor/physical_systems/electric_motors/three_phase_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/three_phase_motor.py @@ -13,11 +13,20 @@ class ThreePhaseMotor(ElectricMotor): as well as limits and bandwidth. """ + # fmt: off # transformation matrix from abc to alpha-beta representation - _t23 = 2 / 3 * np.array([[1, -0.5, -0.5], [0, 0.5 * np.sqrt(3), -0.5 * np.sqrt(3)]]) + _t23 = 2 / 3 * np.array([ + [1, -0.5, -0.5], + [0, 0.5 * np.sqrt(3), -0.5 * np.sqrt(3)] + ]) # transformation matrix from alpha-beta to abc representation - _t32 = np.array([[1, 0], [-0.5, 0.5 * np.sqrt(3)], [-0.5, -0.5 * np.sqrt(3)]]) + _t32 = np.array([ + [1, 0], + [-0.5, 0.5 * np.sqrt(3)], + [-0.5, -0.5 * np.sqrt(3)] + ]) + # fmt: on @staticmethod def t_23(quantities): From 652a84a4361e0c30432aa2197b881d049c0cf76c Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Tue, 14 May 2024 08:42:48 +0200 Subject: [PATCH 43/51] fixed unwrapped warning in tests --- tests/test_environments/test_environments.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_environments/test_environments.py b/tests/test_environments/test_environments.py index 8e7ba6ed..f8c268c2 100644 --- a/tests/test_environments/test_environments.py +++ b/tests/test_environments/test_environments.py @@ -18,7 +18,7 @@ def test_tau(motor, control_task, action_type, version, tau): env_id = f"{action_type}-{control_task}-{motor}-{version}" env = gem.make(env_id) - assert env.physical_system.tau == tau + assert env.unwrapped.physical_system.tau == tau @pytest.mark.parametrize("version", versions) @@ -33,4 +33,4 @@ def test_referenced_states_ac( ): env_id = f"{action_type}-{control_task}-{ac_motor}-{version}" env = gem.make(env_id) - assert env.reference_generator.reference_names == referenced_states + assert env.unwrapped.reference_generator.reference_names == referenced_states From a6723106ba1feb6d45b0490482af969f2ce8c682 Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Tue, 14 May 2024 08:43:30 +0200 Subject: [PATCH 44/51] fixed state space warnings --- DEVELOPMENT.md | 3 +++ src/gym_electric_motor/__init__.py | 7 +++++++ .../integration_tests/test_environment_execution.py | 13 ++++++++++--- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index b490b62f..4c1f38c0 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -7,6 +7,9 @@ Build: `python -m build` Run: `pytest --sw` > --sw, --stepwise Exit on test failure and continue from last failing test next time +Warning as error: +> python -W error -m pytest --sw -v + # Linter and Formater Ruff: https://docs.astral.sh/ruff/installation/ diff --git a/src/gym_electric_motor/__init__.py b/src/gym_electric_motor/__init__.py index bed713e5..828f1163 100644 --- a/src/gym_electric_motor/__init__.py +++ b/src/gym_electric_motor/__init__.py @@ -1,3 +1,5 @@ +import sys + import gymnasium from gymnasium.envs.registration import register from packaging import version @@ -32,7 +34,12 @@ # dict(order_enforce=False) if version.parse(gymnasium.__version__) >= version.parse("0.21.0") else dict() # ) # registration_kwargs["disable_env_checker"] = True + registration_kwargs = dict() +# disable the environment checker for pytest +if "pytest" in sys.modules: + registration_kwargs["disable_env_checker"] = True + envs_path = "gym_electric_motor.envs:" diff --git a/tests/integration_tests/test_environment_execution.py b/tests/integration_tests/test_environment_execution.py index 19cfb995..fae8d256 100644 --- a/tests/integration_tests/test_environment_execution.py +++ b/tests/integration_tests/test_environment_execution.py @@ -28,13 +28,20 @@ def test_execution(dc_motor, control_task, action_type, version, no_of_steps): env_id = f"{action_type}-{control_task}-{dc_motor}-{version}" env = gem.make(env_id) terminated = True + for i in range(no_of_steps): if terminated: - observation = env.reset() - state, reference = observation + (state, reference), _ = env.reset() + observation = (state, reference) action = env.action_space.sample() assert action in env.action_space - observation, reward, terminated, truncated, info = env.step(action) + try: + (state, reference), reward, terminated, truncated, info = env.step(action) + observation = (state, reference) + except Exception as e: + print (f"Step {i}: {observation}, {reward}, {terminated}, {truncated}, {info}") + raise e + assert not np.any(np.isnan(observation[0])), "An invalid nan-value is in the state." assert not np.any(np.isnan(observation[1])), "An invalid nan-value is in the reference." assert info == {} From d936980f414f258ddaf9db92d38fdb5ed8c31115 Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Tue, 14 May 2024 10:23:20 +0200 Subject: [PATCH 45/51] fixed integration test on windows --- tests/integration_tests/test_integration.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/integration_tests/test_integration.py b/tests/integration_tests/test_integration.py index f3c38e27..c195eedd 100644 --- a/tests/integration_tests/test_integration.py +++ b/tests/integration_tests/test_integration.py @@ -93,6 +93,7 @@ def test_simulate_env(): for file in ref_data.files: assert np.allclose(ref_data[file], test_data[file], equal_nan=True) + test_data.close() os.remove("./tests/integration_tests/test_data.npz") # Anti test @@ -103,4 +104,5 @@ def test_simulate_env(): for file in ref_data.files[0:3]: assert not np.allclose(ref_data[file], test_data[file], equal_nan=True) + test_data.close() os.remove("./tests/integration_tests/test_data.npz") From 8839c226d392f5ea206f2e929678fc1c605af41a Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Tue, 14 May 2024 10:29:25 +0200 Subject: [PATCH 46/51] added python 3.11 in changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e17190d..3bca1fad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [2.0.1] - Unreleased ## Added +- Support for Python 3.11 - Ruff: Python linter & formatter (see [DEVELOPMENT.md](DEVELOPMENT.md)) - StateObserver: An easy way to get state values with error checking [example](examples/observers/state_observer_example.py) - Integrated gem_controls repository into gem. classic_controllers will be removed in further version From 2e0703b5af80d43b18728c87e70b64ab5aefe28a Mon Sep 17 00:00:00 2001 From: "S.A" <8891249+devandt@users.noreply.github.com> Date: Tue, 14 May 2024 10:43:30 +0200 Subject: [PATCH 47/51] fixed formatting --- .../electric_motors/induction_motor.py | 90 ++++--------------- 1 file changed, 18 insertions(+), 72 deletions(-) diff --git a/src/gym_electric_motor/physical_systems/electric_motors/induction_motor.py b/src/gym_electric_motor/physical_systems/electric_motors/induction_motor.py index b2416967..843cce8d 100644 --- a/src/gym_electric_motor/physical_systems/electric_motors/induction_motor.py +++ b/src/gym_electric_motor/physical_systems/electric_motors/induction_motor.py @@ -293,78 +293,24 @@ def _update_model(self): tau_r = l_r / mp["r_r"] tau_sig = sigma * l_s / (mp["r_s"] + mp["r_r"] * (mp["l_m"] ** 2) / (l_r**2)) - self._model_constants = np.array( - [ - # omega, i_alpha, i_beta, psi_ralpha, psi_rbeta, omega * psi_ralpha, omega * psi_rbeta, u_salpha, u_sbeta, u_ralpha, u_rbeta, - [ - 0, - -1 / tau_sig, - 0, - mp["l_m"] * mp["r_r"] / (sigma * l_s * l_r**2), - 0, - 0, - +mp["l_m"] * mp["p"] / (sigma * l_r * l_s), - 1 / (sigma * l_s), - 0, - -mp["l_m"] / (sigma * l_r * l_s), - 0, - ], # i_ralpha_dot - [ - 0, - 0, - -1 / tau_sig, - 0, - mp["l_m"] * mp["r_r"] / (sigma * l_s * l_r**2), - -mp["l_m"] * mp["p"] / (sigma * l_r * l_s), - 0, - 0, - 1 / (sigma * l_s), - 0, - -mp["l_m"] / (sigma * l_r * l_s), - ], - # i_rbeta_dot - [ - 0, - mp["l_m"] / tau_r, - 0, - -1 / tau_r, - 0, - 0, - -mp["p"], - 0, - 0, - 1, - 0, - ], # psi_ralpha_dot - [ - 0, - 0, - mp["l_m"] / tau_r, - 0, - -1 / tau_r, - mp["p"], - 0, - 0, - 0, - 0, - 1, - ], - # psi_rbeta_dot - [ - mp["p"], - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - ], # epsilon_dot - ] - ) + # fmt: off + self._model_constants = np.array([ + # omega, i_alpha, i_beta, psi_ralpha, psi_rbeta, omega * psi_ralpha, omega * psi_rbeta, u_salpha, u_sbeta, u_ralpha, u_rbeta, + [0, -1 / tau_sig, 0,mp['l_m'] * mp['r_r'] / (sigma * l_s * l_r ** 2), 0, 0, + +mp['l_m'] * mp['p'] / (sigma * l_r * l_s), 1 / (sigma * l_s), 0, + -mp['l_m'] / (sigma * l_r * l_s), 0, ], # i_ralpha_dot + [0, 0, -1 / tau_sig, 0, + mp['l_m'] * mp['r_r'] / (sigma * l_s * l_r ** 2), + -mp['l_m'] * mp['p'] / (sigma * l_r * l_s), 0, 0, + 1 / (sigma * l_s), 0, -mp['l_m'] / (sigma * l_r * l_s), ], + # i_rbeta_dot + [0, mp['l_m'] / tau_r, 0, -1 / tau_r, 0, 0, -mp['p'], 0, 0, 1, + 0, ], # psi_ralpha_dot + [0, 0, mp['l_m'] / tau_r, 0, -1 / tau_r, mp['p'], 0, 0, 0, 0, 1, ], + # psi_rbeta_dot + [mp['p'], 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], # epsilon_dot + ]) + # fmt: on def electrical_jacobian(self, state, u_in, omega, *args): mp = self._motor_parameter From 124b7b67b373c683812d456c91c683f24ed4455d Mon Sep 17 00:00:00 2001 From: Maximilian Schenke Date: Wed, 15 May 2024 09:13:22 +0200 Subject: [PATCH 48/51] fixes to EESM ODE and jacobian --- .../externally_excited_synchronous_motor.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py b/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py index a6ddd9b4..144d3952 100644 --- a/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py +++ b/gym_electric_motor/physical_systems/electric_motors/externally_excited_synchronous_motor.py @@ -130,7 +130,7 @@ def _update_model(self): mp['l_M'] = mp['k'] * 3/2 * mp['l_m'] mp['l_E'] = mp['k'] ** 2 * 3/2 * mp['l_e'] - mp['i_k_rs'] = 2 / 3 / mp['k'] # ratio (i_E / i_e) + mp['i_k_rs'] = 2 / 3 / mp['k'] # ratio (i_E / i_e) mp['sigma'] = 1 - mp['l_M'] ** 2 / (mp['l_d'] * mp['l_E']) self._model_constants = np.array([ @@ -195,21 +195,21 @@ def electrical_jacobian(self, state, u_in, omega, *args): mp = self._motor_parameter return ( np.array([ # dx'/dx - [ -mp['r_s'] / (mp['l_d'] * mp['sigma']), mp['l_q'] / (mp['sigma'] * mp['l_d']) * omega * mp['p'], mp['l_M'] * mp['r_e'] / (mp['sigma'] * mp['l_d'] * mp['l_E']) * mp['i_k_rs'], 0], - [ -mp['l_d'] / mp['l_q'] * omega * mp['p'], -mp['r_s'] / mp['l_q'], -omega * mp['p'] * mp['l_E'] / mp['l_q'] * mp['i_k_rs'], 0], - [mp['l_M'] * mp['r_s'] / (mp['sigma'] * mp['l_d'] * mp['l_E']), -omega * mp['p'] * mp['l_M'] * mp['l_q'] / (mp['sigma'] * mp['l_d'] * mp['l_E']), -mp['r_E'] / mp['l_E'] * mp['i_k_rs'], 0], - [ 0, 0, 0, 0], + [ -mp['r_s'] / (mp['l_d'] * mp['sigma']), mp['l_q'] / (mp['sigma'] * mp['l_d']) * omega * mp['p'], mp['l_M'] * mp['r_E'] / (mp['sigma'] * mp['l_d'] * mp['l_E']) * mp['i_k_rs'], 0], + [ -mp['l_d'] / mp['l_q'] * omega * mp['p'], -mp['r_s'] / mp['l_q'], -omega * mp['p'] * mp['l_M'] / mp['l_q'] * mp['i_k_rs'], 0], + [mp['l_M'] * mp['r_s'] / (mp['sigma'] * mp['l_d'] * mp['l_E'] * mp["i_k_rs"]), -omega * mp['p'] * mp['l_M'] * mp['l_q'] / (mp['sigma'] * mp['l_d'] * mp['l_E'] * mp["i_k_rs"]), -mp['r_E'] / (mp['sigma'] * mp['l_E']), 0], + [ 0, 0, 0, 0], ]), np.array([ # dx'/dw - mp['p'] * mp['l_q'] / mp['l_d'] * state[self.I_SQ_IDX], + mp['p'] * mp['l_q'] / (mp['l_d'] * mp['sigma']) * state[self.I_SQ_IDX], -mp['p'] * mp['l_d'] / mp['l_q'] * state[self.I_SD_IDX] - mp['p'] * mp['l_M'] / mp['l_q'] * state[self.I_E_IDX] * mp['i_k_rs'], - -mp['p'] * mp['l_M'] * mp['l_q'] / (mp['sigma'] * mp['l_d'] * mp['l_E']), + -mp['p'] * mp['l_M'] * mp['l_q'] / (mp['sigma'] * mp['l_d'] * mp['l_E'] * mp['i_k_rs']) * state[self.I_SQ_IDX], mp['p'], ]), np.array([ # dT/dx 1.5 * mp['p'] * (mp['l_d'] - mp['l_q']) * state[self.I_SQ_IDX], - 1.5 * mp['p'] * (mp['l_E'] * state[self.I_E_IDX] * mp['i_k_rs'] + (mp['l_d'] - mp['l_q']) * state[self.I_SD_IDX]), - 1.5 * mp['p'] * mp['l_E'] * state[self.I_SQ_IDX], + 1.5 * mp['p'] * (mp['l_M'] * state[self.I_E_IDX] * mp['i_k_rs'] + (mp['l_d'] - mp['l_q']) * state[self.I_SD_IDX]), + 1.5 * mp['p'] * mp['l_M'] * mp['i_k_rs'] * state[self.I_SQ_IDX], 0, ]) ) From 54f8babc5b7f6daae3de1f0fad051c2ab78d2495 Mon Sep 17 00:00:00 2001 From: Maximilian Schenke <61693487+max-schenke@users.noreply.github.com> Date: Thu, 16 May 2024 09:22:18 +0200 Subject: [PATCH 49/51] update python version support from >=3.8 to >=3.9 in pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 74207a76..ab1af267 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ authors = [ ] description = "A Farama Gymnasium environment for electric motor control." readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", From eb002234d9193a1467684e6cfe5ac319777c9567 Mon Sep 17 00:00:00 2001 From: Maximilian Schenke Date: Thu, 16 May 2024 09:24:14 +0200 Subject: [PATCH 50/51] update python version requirement to >=3.9 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 74207a76..ab1af267 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ authors = [ ] description = "A Farama Gymnasium environment for electric motor control." readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", From 4ffb09645a36c0b40d9480ee43336001601a6022 Mon Sep 17 00:00:00 2001 From: devandt <8891249+devandt@users.noreply.github.com> Date: Mon, 20 May 2024 23:09:42 +0200 Subject: [PATCH 51/51] Dont raise exception on dict input --- src/gym_electric_motor/utils.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/gym_electric_motor/utils.py b/src/gym_electric_motor/utils.py index 49ffeca0..56a10865 100644 --- a/src/gym_electric_motor/utils.py +++ b/src/gym_electric_motor/utils.py @@ -2,18 +2,17 @@ import numpy as np -# This hacky function stays for now because, all envs use it and refectoring would be a pain TODO: fix this +# This hacky function stays for now because all envs uses it and refectoring would be a pain def initialize(base_class, arg, default_class, default_args): if arg is None: return default_class(**default_args) if isinstance(arg, type): - raise Exception("Need init") + raise Exception("Need initialization value") elif isinstance(arg, base_class): return arg - elif isinstance(arg, str): - raise Exception - elif isinstance(arg, dict): - raise Exception + elif type(arg) is str: + raise Exception("Deprecated in version 2.0") + elif type(arg) is dict: default_args.update(arg) return default_class(**default_args)