diff --git a/docs/source/EM_analyzers/bspm_mach_constants_analyzer.rst b/docs/source/EM_analyzers/bspm_mach_constants_analyzer.rst new file mode 100644 index 00000000..d2ab207a --- /dev/null +++ b/docs/source/EM_analyzers/bspm_mach_constants_analyzer.rst @@ -0,0 +1,348 @@ +BSPM Machine Constants Analyzer +######################################################################## + +This analyzer determines the machine constants (:math:`k_t, k_f, k_\delta,` and :math:`k_\Phi`) of a given BSPM machine design. + +Model Background +**************** + +This analyzer utilizes scripts within eMach to generate ``BSPM_Machine`` and ``BSPM_Machine_Oper_Pt`` objects for performing machine constant analysis. +In each analysis, linear polynomials are fitted to torque (or force) data as the current (or displacement) varies in order to obtain the corresponding machine constant. + +Torque Contant :math:`k_t` +------------------------------------ +The machine torque constant, :math:`k_t`, can be computed using the following expression: + +.. math:: + + \tau = k_t i_q + +where :math:`\tau` is torque and :math:`i_q` is the torque current. + +Suspension Force :math:`k_f` & Displacement Stiffness Constant :math:`k_\delta` +-------------------------------------------------------------------------------------------------- +The suspension force constant, :math:`k_f`, and displacement stiffness constant, :math:`k_\delta`, can be computed using the following expression: + +.. math:: + + \vec{F} = k_f \vec{i_s}+k_\delta \vec{\delta} + +where :math:`\vec{i_s}` is the suspension current, and :math:`\vec{\delta}` is the displacement of the rotor from the magnetic center. + + +Back-EMF Constant :math:`k_\Phi` +------------------------------------ +The machine back-EMF constant :math:`k_\Phi` can be expressed using the following equation: + +.. math:: + + \vec{v_m} = k_\Phi\omega + +where, :math:`\omega` is angular velocity in rad/s and :math:`\vec{v_m}` is the peak value of the induced phase voltage. + +Input from User +********************************* + +To define the problem class, the user needs to provide the ``BSPM_Machine`` and ``BSPM_Machine_Oper_Pt`` objects, which specify both the properties and the operating +point of the BSPM machine intended for evaluation. For defining these objects, the user can refer to the :doc:`BSPM Design <../machines/bspm/index>` page. The inputs +for each are summarized below: + +.. csv-table:: Input for BSPM machine constants problem class + :file: input_bspm_mach_constants_problem.csv + :widths: 20, 20, 30 + :header-rows: 1 + +.. csv-table:: Input for BSPM machine constants analyzer class + :file: input_bspm_mach_constants_analyzer.csv + :widths: 20, 20, 30 + :header-rows: 1 + +Import Modules +------------------------------------ +The following code imports all the required modules for performing a BSPM machine constants analysis. Users can paste this code into their scripts and execute it +to ensure the modules can imported properly: + +.. code-block:: python + + import mach_eval.analyzers.electromagnetic.bspm.machine_constant.bspm_mach_constants as bmc + from mach_eval.machines.materials.electric_steels import Arnon5 + from mach_eval.machines.materials.jmag_library_magnets import N40H + from mach_eval.machines.materials.miscellaneous_materials import ( + CarbonFiber, + Steel, + Copper, + Hub, + Air, + ) + from mach_eval.machines.bspm import BSPM_Machine + from mach_eval.machines.bspm.bspm_oper_pt import BSPM_Machine_Oper_Pt + from mach_eval.analyzers.electromagnetic.bspm.jmag_2d_config import JMAG_2D_Config + import os + +Define and Create ``BSPM Machine`` Object +------------------------------------------ + +The user can paste the following sample BSPM machine design to create the ``BSPM_machine`` object: + +.. code-block:: python + + ######################################################### + # CREATE BSPM MACHINE OBJECT + ######################################################### + + ################ DEFINE BP4 ################ + bspm_dimensions = { + "alpha_st": 31.7088, #[deg] + "d_so": 2.02334e-3, #[m] + "w_st": 5.95805e-3, #[m] + "d_st": 18.4967e-3, #[m] + "d_sy": 5.81374e-3, #[m] + "alpha_m": 180, #[m] + "d_m": 3e-3, #[m] + "d_mp": 0, #[m] + "d_ri": 0.1e-3, #[m] + "alpha_so": 15.5, #[deg] + "d_sp": 2.05e-3, #[m] + "r_si": 16.9737e-3, #[m] + "alpha_ms": 180, #[deg] + "d_ms": 0, #[m] + "r_sh": 8.9e-3, #[m] + "l_st": 25e-3, #[m] + "d_sl": 1e-3, #[m] + "delta_sl": 9.63e-5, #[m] + } + + bspm_parameters = { + "p": 1, # number of pole pairs + "ps": 2, # number of suspension pole pairs + "n_m": 1, # + "Q": 6, # number of slots + "rated_speed": 16755.16, #[rad/s] + "rated_power": 8e3, # [W] + "rated_voltage": 8e3/18, # [V_rms] + "rated_current": 18, # [I_rms] + "name": "BP4" + } + + bspm_materials = { + "air_mat": Air, + "rotor_iron_mat": Arnon5, + "stator_iron_mat": Arnon5, + "magnet_mat": N40H, + "rotor_sleeve_mat": CarbonFiber, + "coil_mat": Copper, + "shaft_mat": Steel, + "rotor_hub": Hub, + } + + bspm_winding = { + "no_of_layers": 2, + # layer_phases is a list of lists, the number of lists = no_of_layers + # first list corresponds to coil sides in first layer + # second list corresponds to coil sides in second layer + # the index indicates the slot opening corresponding to the coil side + # string characters are used to represent the phases + "layer_phases": [["U", "W", "V", "U", "W", "V"], + ["V", "U", "W", "V", "U", "W"]], + # layer_polarity is a list of lists, the number of lists = no_of_layers + # first list corresponds to coil side direction in first layer + # second list corresponds to coil side direction in second layer + # the index indicates the slot opening corresponding to the coil side + # + indicates coil side goes into the page, - indicates coil side comes out of page + "layer_polarity": [["+", "-", "+", "-", "+", "-"], + ["+", "-", "+", "-", "+", "-"]], + # coil_groups are a unique property of DPNV windings + # coil group is assigned corresponding to the 1st winding layer + "coil_groups": ["b", "a", "b", "a", "b", "a"], + "pitch": 1, + "Z_q": 45, + "Kov": 1.8, + "Kcu": 0.5, + # add phase current offset to know relative rotor / current angle for creating Iq + "phase_current_offset": -30 + } + + bp4 = BSPM_Machine( + bspm_dimensions, bspm_parameters, bspm_materials, bspm_winding + ) + +Define and Create ``BSPM_Machine_Oper_Pt`` Object +------------------------------------------------- + +The users can paste the provided sample BSPM operating point code to instantiate the ``BSPM_Machine_Oper_Pt`` object: + +.. code-block:: python + + ######################################################### + # DEFINE BSPM OPERATING POINT + ######################################################### + bp4_op_pt = BSPM_Machine_Oper_Pt( + Id=0, # I_pu + Iq=0.95, # I_pu + Ix=0, # I_pu + Iy=0.05, # I_pu + speed=160000, # RPM + ambient_temp=25, # C + rotor_temp_rise=55, # K + ) + +Define and Create ``JMAG_2D_Config`` Object +------------------------------------------- + +For performing simualtion in JMAG, an instance of ``JMAG_2D_Config`` must be provided (For more information, see :doc:`BSPM JMAG 2D FEA Analyzer `.) +Users can paste the provided sample pf the JMAG configuration code to instantiate the ``JMAG_2D_Config`` object: + +.. code-block:: python + + ######################################################### + # DEFINE BSPM JMAG SETTINGS + ######################################################### + jmag_config = JMAG_2D_Config( + no_of_rev_1TS=1, + no_of_rev_2TS=2, + no_of_steps_per_rev_1TS=36, + no_of_steps_per_rev_2TS=360, + mesh_size=2e-3, + magnet_mesh_size=1e-3, + airgap_mesh_radial_div=7, + airgap_mesh_circum_div=720, + mesh_air_region_scale=1.15, + only_table_results=False, + csv_results=r"Torque;Force;FEMCoilFlux;LineCurrent;TerminalVoltage;JouleLoss;TotalDisplacementAngle;JouleLoss_IronLoss;IronLoss_IronLoss;HysteresisLoss_IronLoss", + del_results_after_calc=False, + run_folder=os.path.dirname(__file__) + "/run_data/", + jmag_csv_folder=os.path.dirname(__file__) + "/run_data/JMAG_csv/", + max_nonlinear_iterations=50, + multiple_cpus=True, + num_cpus=4, + jmag_scheduler=False, + jmag_visible=False, + jmag_version = '23', + ) + +.. note:: + + The step and mesh size could significantly affect the results. The user should consider making these values to be more fine. + +Define Problem and Analyzer Object +------------------------------------ + +Use the following code to define the problem and analyzer object: + +.. code-block:: python + + ######################################################### + # DEFINE BSPM OPERATING POINTS + ######################################################### + + # List of BSPM operating points for Kf evaluation + Kf_op_pt = [ + BSPM_Machine_Oper_Pt( + Id=0, + Iq=0, + Ix=0, + Iy=Is_pu, + speed=160000, + ambient_temp=25, + rotor_temp_rise=55, + ) + + for Is_pu in np.linspace(0,1,10) + ] + + # List of BSPM operating points for Kt evaluation + Kt_op_pt = [ + BSPM_Machine_Oper_Pt( + Id=0, + Iq=Iq_pu, + Ix=0, + Iy=0, + speed=160000, + ambient_temp=25, + rotor_temp_rise=55, + ) + for Iq_pu in np.linspace(0,1,10) + ] + + # List of BSPM operating points for Kphi evaluation + Kphi_op_pt = [ + BSPM_Machine_Oper_Pt( + Id=0, + Iq=0, + Ix=0, + Iy=0, + speed=speed, + ambient_temp=25, + rotor_temp_rise=55, + ) + for speed in np.linspace(0,160000,10) + ] + + # List of coordinates for Kdelta evaluation + Kdelta_coords = [ + [x, y] + for x in np.linspace(-0.3,0.3,3) + for y in np.linspace(-0.3,0.3,3) + ] + + ######################################################### + # DEFINE BSPM MACHINE CONSTANTS PROBLEM + ######################################################### + problem = BSPMMachineConstantProblem( + machine=bp4, + nominal_op_pt=bp4_op_pt, + Kf_op_pt, + Kt_op_pt, + Kphi_op_pt, + Kdelta_coords + ) + + ######################################################### + # DEFINE BSPM MACHINE CONSTANTS ANALYZER + ######################################################### + analyzer = bmc.BSPMMachineConstantAnalyzer(jmag_config) + + +Output to User +********************************** + +The attributes of the results class can be summarized in the table below: + +.. csv-table:: Results of BSPM machine constants analyzer + :file: result_bspm_mach_constants.csv + :widths: 30, 70, 30 + :header-rows: 1 + +Use the following code to run the example analysis: + +.. code-block:: python + + ######################################################### + # SOLVE BSPM MACHINE CONSTANTS PROBLEM + ######################################################### + result = analyzer.analyze(problem) + print(f"Kf = {result.Kf}") + print(f"Kt = {result.Kt}") + print(f"Kdelta = {result.Kdelta}") + print(f"Kphi = {result.Kphi}") + +.. note:: + + The user can install the ``tqdm`` library for a visual progress bar on your terminal when the simulations are running. + +.. note:: + + Depending on the number of evaluation steps specified, a full analysis could take upwards of **one to two hours** to complete. + +Running the example case returns the following: + +.. code-block:: python + + 1.8052182451902197 + 0.01911529534112125 + 6935.763575553303 + 0.006449054670613704 + +The results indicate that the example BSPM machine design has a suspension force constant of :math:`k_f = 1.805\; [\frac{N}{A_{pk}}]`, a torque constant of +:math:`k_t = 0.0191 \; [\frac{Nm}{A_{pk}}]`, a displacement stiffness constant of :math:`k_\delta = 6935.76\; [\frac{N}{m}]`, and back-EMF constant of +:math:`k_\phi = 0.00645\; [\frac{V_{pk}}{rad/s}]`. \ No newline at end of file diff --git a/docs/source/EM_analyzers/index.rst b/docs/source/EM_analyzers/index.rst index 3d319c8c..cd7547a8 100644 --- a/docs/source/EM_analyzers/index.rst +++ b/docs/source/EM_analyzers/index.rst @@ -13,6 +13,7 @@ This section provides documentation for the electromagnetic analyzers supported Force Data Stator Winding Resistance BSPM JMAG 2D FEA + BSPM Machine Constants Winding Factors SynR JMAG 2D FEA - Inductance/Saliency \ No newline at end of file + Inductance/Saliency diff --git a/docs/source/EM_analyzers/input_bspm_mach_constants_analyzer.csv b/docs/source/EM_analyzers/input_bspm_mach_constants_analyzer.csv new file mode 100644 index 00000000..8459835c --- /dev/null +++ b/docs/source/EM_analyzers/input_bspm_mach_constants_analyzer.csv @@ -0,0 +1,3 @@ +Arguments, Type, Description +config, ``JMAG_2D_Config``," object of type JMAG_2D_Config describing time step and mesh setting, etc." + diff --git a/docs/source/EM_analyzers/input_bspm_mach_constants_problem.csv b/docs/source/EM_analyzers/input_bspm_mach_constants_problem.csv new file mode 100644 index 00000000..90680322 --- /dev/null +++ b/docs/source/EM_analyzers/input_bspm_mach_constants_problem.csv @@ -0,0 +1,7 @@ +Arguments, Type, Description +machine, ``BSPM_machine``, instance of ``BSPM_machine`` +nominial_op_pt,``BSPM_Machine_Oper_Pt``, nominal operating point of BSPM Machine under evaluation +Kf_op_pt, list (default - *None*), list containing instances of `BSPM_Machine_Oper_Pt` for Kf simulations +Kt_op_pt, list (default - *None*), list containing instances of `BSPM_Machine_Oper_Pt` for Kt simulations +Kphi_op_pt, list (default - *None*), list containing instances of `BSPM_Machine_Oper_Pt` for Kphi simulations +Kdelta_coords, list (default - *None*),list of [x y] coordinates in mm to run Kdelta simulations \ No newline at end of file diff --git a/docs/source/EM_analyzers/result_bspm_mach_constants.csv b/docs/source/EM_analyzers/result_bspm_mach_constants.csv new file mode 100644 index 00000000..ffde9030 --- /dev/null +++ b/docs/source/EM_analyzers/result_bspm_mach_constants.csv @@ -0,0 +1,5 @@ +Attribute,Description,Units +Kf, Suspension force constant, N/A_pk +Kt, Torque constant, Nm/A_pk +Kdelta, Displacement stiffness constant, N/m +Kphi, Back-EMF constant , V_pk/rad/s diff --git a/mach_eval/analyzers/electromagnetic/bspm/jmag_2d.py b/mach_eval/analyzers/electromagnetic/bspm/jmag_2d.py index 490c1545..d6c00eea 100644 --- a/mach_eval/analyzers/electromagnetic/bspm/jmag_2d.py +++ b/mach_eval/analyzers/electromagnetic/bspm/jmag_2d.py @@ -4,11 +4,18 @@ import pandas as pd import sys -from eMach.mach_eval.analyzers.electromagnetic.bspm.electrical_analysis import ( + +from .electrical_analysis import ( CrossSectInnerNotchedRotor as CrossSectInnerNotchedRotor, ) -from eMach.mach_eval.analyzers.electromagnetic.bspm.electrical_analysis import CrossSectStator as CrossSectStator -from eMach.mach_eval.analyzers.electromagnetic.bspm.electrical_analysis.Location2D import Location2D +from .electrical_analysis import CrossSectStator as CrossSectStator +from .electrical_analysis.Location2D import Location2D + +# from eMach.mach_eval.analyzers.electromagnetic.bspm.electrical_analysis import ( +# CrossSectInnerNotchedRotor as CrossSectInnerNotchedRotor, +# ) +# from eMach.mach_eval.analyzers.electromagnetic.bspm.electrical_analysis import CrossSectStator as CrossSectStator +# from eMach.mach_eval.analyzers.electromagnetic.bspm.electrical_analysis.Location2D import Location2D sys.path.append(os.path.dirname(__file__) + "/../../../..") from mach_opt import InvalidDesign @@ -305,7 +312,7 @@ def group(name, id_list): id_shaft = part_ID_list[1] partIDRange_Magnet = part_ID_list[2 : int(2 + self.machine_variant.p * 2)] # id_sleeve = part_ID_list[int(2 + self.machine_variant.p * 2)] - id_statorCore = part_ID_list[int(2 + self.machine_variant.p * 2) + 1] + id_statorCore = part_ID_list[int(2 + self.machine_variant.p * 2)] partIDRange_Coil = part_ID_list[ int(1 + self.machine_variant.p * 2) + 2 : int(2 + self.machine_variant.p * 2) @@ -318,6 +325,12 @@ def group(name, id_list): group("Magnet", partIDRange_Magnet) group("Coils", partIDRange_Coil) + # """ Set Parts names """ + + app.GetModel(0).SetPartName(id_backiron, u"NotchedRotor") + app.GetModel(0).SetPartName(id_shaft, u"Shaft") + app.GetModel(0).SetPartName(id_statorCore, u"StatorCore") + """ Add Part to Set for later references """ def add_part_to_set(name, x, y, ID=None): diff --git a/mach_eval/analyzers/electromagnetic/bspm/machine_constant/bspm_mach_constants.py b/mach_eval/analyzers/electromagnetic/bspm/machine_constant/bspm_mach_constants.py new file mode 100644 index 00000000..eb6e2eea --- /dev/null +++ b/mach_eval/analyzers/electromagnetic/bspm/machine_constant/bspm_mach_constants.py @@ -0,0 +1,544 @@ +import numpy as np +import pandas as pd +import win32com.client +from .....machines.bspm import BSPM_Machine, BSPM_Machine_Oper_Pt +from ...bspm.jmag_2d_config import JMAG_2D_Config +from ...bspm.jmag_2d import BSPM_EM_Analyzer +from typing import Tuple, Union +from dataclasses import dataclass + +try: + from tqdm import tqdm +except ImportError: + def tqdm(iterator, *args, **kwargs): + return iterator + +########################################################################### +from functools import cached_property, lru_cache +# @lru_cache decoractor saves the return value of the method, +# method will run once and the return value will be saved, from then everytime +# the method is called it will simply return the saved return value, +# sigificantly reducing computation time +########################################################################### + +from ....force_vector_data import ( + ProcessForceDataProblem, + ProcessForceDataAnalyzer +) + +from ....torque_data import ( + ProcessTorqueDataProblem, + ProcessTorqueDataAnalyzer +) + +class BSPMMachineConstantProblem: + def __init__( + self, + machine:BSPM_Machine, + nominal_op_pt: BSPM_Machine_Oper_Pt, + Kf_op_pt: list = None, + Kt_op_pt: list = None, + Kphi_op_pt: list = None, + Kdelta_coords: list = None, + ) -> 'BSPMMachineConstantProblem': + """BSPMMachineConstantProblem Class + + Args: + machine (BSPM_Machine): instance of `BSPM_Machine` + nominial_op_pt (BSPM_Machine_Oper_Pt): instance of `BSPM_Machine_Oper_Pt` at nominial + Kf_op_pt (List): list containing instances of `BSPM_Machine_Oper_Pt` for Kf simulations + Kt_op_pt (List): list containing instances of `BSPM_Machine_Oper_Pt` for Kt simulations + Kphi_op_pt (List): list containing instances of `BSPM_Machine_Oper_Pt` for Kphi simulations + Kdelta_coords (List): list of [x,y] coordinates to run Kdelta simulations + + Returns: + BSPMMachineConstantProblem: instance of BSPMMachineConstantProblem + """ + self.machine = machine + self.operating_point = nominal_op_pt + self.Kf_op_pt = Kf_op_pt + self.Kt_op_pt = Kt_op_pt + self.Kphi_op_pt = Kphi_op_pt + self.Kdelta_coords = Kdelta_coords + self._validate_attr() + + def _validate_attr(self): + if not isinstance(self.machine,BSPM_Machine): + raise TypeError( + 'Invalid machine type, must be BSPM_Machine.' + ) + + if not isinstance(self.operating_point, BSPM_Machine_Oper_Pt): + raise TypeError( + 'Invalid settings type, must be BSPM_Machine_Oper_Pt.' + ) + + for attr_name in ['Kf_op_pt', 'Kt_op_pt', 'Kphi_op_pt']: + attr_value = getattr(self, attr_name) + if attr_value is not None: + if not isinstance(attr_value, list): + raise TypeError(f'{attr_name} must be a list or None.') + if not all(isinstance(item, BSPM_Machine_Oper_Pt) for item in attr_value): + raise TypeError(f'All elements in {attr_name} must be instances of BSPM_Machine_Oper_Pt.') + + +class BSPMMachineConstantAnalyzer(BSPM_EM_Analyzer): + def __init__(self, configuration: JMAG_2D_Config): + # Kf_Kt_case: int = 10, + # Kdelta_coords: list = [[x, y] for x in np.linspace(-0.3,0.3,3) + # for y in np.linspace(-0.3,0.3,3)], + # Kphi_case: int = 10, + self.configuration = configuration + super().__init__(self.configuration) + + def __getstate__(self): + """Magic method for pickling""" + # Remove problematic objects to avoid pickling error + odict = self.__dict__.copy() + del_key_list = ['toolJd','init_model','init_study','init_properties'] + for key in del_key_list: + del odict[key] + return odict + + def __setstate__(self, state): + """Magic method for unpickling""" + self.__dict__ = state + + def analyze(self, problem: BSPMMachineConstantProblem): + """Analyze BSPMMachineConstantProblem + + Args: + problem (BSPMMachineConstantProblem): BSPMMachineConstantProblem + + Returns: + BSPMMachineConstantResult: Result class of BSPMMachineConstantProblem + """ + + self.problem = problem + self.machine = problem.machine + self.nominial_op_pt = problem.operating_point + self.Kf_op_pt = problem.Kf_op_pt + self.Kt_op_pt = problem.Kt_op_pt + self.Kphi_op_pt = problem.Kphi_op_pt + self.Kdelta_coords = problem.Kdelta_coords + + # Run initial analysis to build the model + # super().analyze == BSPM_EM_Analyzer.analyzer + print('==============================================================') + print('Performing initial run...') + super().analyze(self.problem) + print('Initial run complete...') + + # open .jproj file and obtain initial model and study properties + print('==============================================================') + print(f'Re-opening {self.project_name}.jproj') + self.toolJd = self._open_JMAG_file() + (self.init_model, + self.init_study, + self.init_study_name, + self.init_properties) = self._get_init_study(self.toolJd) + + return BSPMMachineConstantResult( + Kf=self.Kf, + Kt=self.Kt, + Kphi=self.Kphi, + Kdelta=self.Kdelta + ) + + @cached_property + def Kf(self)-> Union[float, None]: + "Machine Suspension Force Constant [N/A]" + if self.Kf_op_pt is not None: + Is_list, force = self.Kf_data + Kf,_ = np.polyfit(Is_list,force,deg=1) + return Kf + else: + return None + + @cached_property + def Kt(self)->Union[float, None]: + "Machine Torque Constant [N-m/A_pk]" + if self.Kt_op_pt is not None: + Iq_list, torque = self.Kt_data + Kt,_ = np.polyfit(Iq_list,torque,deg=1) + return Kt + else: + return None + + @cached_property + def Kphi(self)->Union[float, None]: + "Machine Back-EMF Constant [V_rms/rad/s]" + if self.Kphi_op_pt is not None: + speed, bemf = self.Kphi_data + Kphi,_ = np.polyfit(speed,bemf,deg=1) + return Kphi + # AC + else: + return None + + @cached_property + def Kdelta(self)->Union[float, None]: + "Machine Displacement Stiffness Constant [N/m]" + if self.Kdelta_coords is not None: + disp, force = self.Kdelta_data + Kdelta,_ = np.polyfit(disp,force,deg=1) + return Kdelta + else: + return None + + @cached_property + def Kf_data(self)->Tuple[list,list]: + """Data used in evaluating machine force constant Kf + + Returns: + Tuple[list,list]: (suspension currents [A], force values [N]) + """ + # run simulations and extract data from JMAG + _,Is_list,force_df_list = self.run_Kf_simulations() + + # determine average torque in each simulation run + force = np.zeros(len(force_df_list)) + for idx, force_df in enumerate(force_df_list): + force_prob = ProcessForceDataProblem( + Fx=force_df["ForCon:1st"].iloc[self.idx_ignore:].to_numpy(), + Fy=force_df["ForCon:2nd"].iloc[self.idx_ignore:].to_numpy() + ) + force_ana = ProcessForceDataAnalyzer() + _,_,f_abs_avg,_,_ = force_ana.analyze(force_prob) + force[idx] = f_abs_avg + + return Is_list, force + + @cached_property + def Kt_data(self)->Tuple[list,list]: + """Data used in evaluating machine torque constant Kt + + Returns: + Tuple[list,list]: (torque currents [A], torque values [N-m]) + """ + # run simulations and extract data from JMAG + Iq_list, _, torque_df_list = self.run_Kt_simulations() + + # determine average torque in each simulation run + torque = np.zeros(len(torque_df_list)) + for idx, torque_df in enumerate(torque_df_list): + torq_prob = ProcessTorqueDataProblem( + torque_df["TorCon"].iloc[self.idx_ignore:].to_numpy() + ) + torq_analyzer = ProcessTorqueDataAnalyzer() + torq_avg,_ = torq_analyzer.analyze(torq_prob) + torque[idx] = torq_avg + + return Iq_list, torque + + @cached_property + def Kdelta_data(self)->list: + """Data used in evaluating machine displacement constant Kdelta + + Returns: + Tuple[list,list]: (displacment magnitudes [m], force values [N]) + """ + # run simulations and extract data from JMAG + force_df_list = self.run_Kdelta_simulations() + + force = [] + for force_df in force_df_list: + force_prob = ProcessForceDataProblem( + Fx=force_df["ForCon:1st"].iloc[self.idx_ignore:].to_numpy(), + Fy=force_df["ForCon:2nd"].iloc[self.idx_ignore:].to_numpy()) + force_ana = ProcessForceDataAnalyzer() + _,_,f_abs_avg,_,_ = force_ana.analyze(force_prob) + force.append(f_abs_avg) + disp = [np.linalg.norm(coord)/1000 for coord in self.Kdelta_coords] + + # combine and sort force and displacement data and unzip into + # two seperate list after sorting + disp, force = (list(t) for t in zip(*sorted(zip(disp,force)))) + return disp, force + + @cached_property + def Kphi_data(self): + """Data used in evaluating machine back-emf constant Kphi + + Returns: + Tuple[list,list]: (speed values [rad/s], back-emf voltage [V_rms]) + """ + bemf_df_list = self.run_Kphi_simulations() + + bemf = [] + for bemf_df in bemf_df_list: + phase_voltage = bemf_df["Terminal_Wt"].iloc[self.idx_ignore:] + if len(phase_voltage) == 0: + rms_voltage = 0 + else: + rms_voltage = np.sqrt( + sum(np.square(phase_voltage)) / len(phase_voltage)) + bemf.append(rms_voltage*np.sqrt(2)) + + return [speed*(2*np.pi/60) for speed in self.Kphi_speed], bemf + + @cached_property + def Kphi_speed(self): + print([op_pt.speed for op_pt in self.Kphi_op_pt]) + return [op_pt.speed for op_pt in self.Kphi_op_pt] + + @lru_cache + def run_Kf_simulations(self)->Tuple[list, list, list]: + """Script to perform Kf and Kt simulations in JMAG. + + Returns: + Tuple[list, list, list]: ( + torque currents [A], + suspension currents [A], + force_df_list + ) + """ + + # define torque and suspension current for simulation + Iq_list = [np.sqrt(2)*self.machine.Rated_current*op_pt.Iq for op_pt in self.Kf_op_pt] + Is_list = [np.sqrt(2)*self.machine.Rated_current*op_pt.Iy for op_pt in self.Kf_op_pt] + force_df_list = [] + + print('==============================================================') + print('Running Kf simulations ......') + for idx, (Iq_val,Is_val) in enumerate( + tqdm(zip(Iq_list,Is_list),total=len(Iq_list))): + + # duplicate initial study + self.init_model.DuplicateStudyName(self.init_study_name, + f"{self.init_study_name}_Kf_case{idx}",True) + + present_study = self.toolJd.GetCurrentStudy() + + # obtain handle to circuit for present study + circuit = present_study.GetCircuit() + self._set_circuit_current_value( + circuit, + ampT=2*Iq_val, + ampS=Is_val, + freq=super().excitation_freq) + present_study.RunAllCases() + + # extract FEA results from CSV + force_df = self._extract_csv_results(present_study.GetName(), "Force") + force_df_list.append(force_df) + + return Iq_list, Is_list, force_df_list + + @lru_cache + def run_Kt_simulations(self)->Tuple[list, list, list]: + """Script to perform Kt simulations in JMAG. + + Returns: + Tuple[list, list, list]: ( + torque currents [A], + suspension currents [A], + torque_df_list + ) + """ + + # define torque and suspension current for simulation + Iq_list = [np.sqrt(2)*self.machine.Rated_current*op_pt.Iq for op_pt in self.Kt_op_pt] + Is_list = [np.sqrt(2)*self.machine.Rated_current*op_pt.Iy for op_pt in self.Kt_op_pt] + torque_df_list = [] + + print('==============================================================') + print('Running Kt simulations ......') + for idx, (Iq_val,Is_val) in enumerate( + tqdm(zip(Iq_list,Is_list),total=len(Iq_list))): + + # duplicate initial study + self.init_model.DuplicateStudyName(self.init_study_name, + f"{self.init_study_name}_Kt_case{idx}",True) + + present_study = self.toolJd.GetCurrentStudy() + + # obtain handle to circuit for present study + circuit = present_study.GetCircuit() + self._set_circuit_current_value( + circuit, + ampT=2*Iq_val, + ampS=Is_val, + freq=super().excitation_freq) + present_study.RunAllCases() + + # extract FEA results from CSV + torque_df = self._extract_csv_results(present_study.GetName(),"Torque") + torque_df_list.append(torque_df) + + return Iq_list, Is_list, torque_df_list + + @lru_cache + def run_Kdelta_simulations(self)->list: + """Script to perform Kdelta simulations in JMAG. + + Returns: + list: list of pd.Dataframes containing force results from simulations + """ + + print('==============================================================') + print('Running Kdelta simulations ......') + + force_df_list = [] + for coord in tqdm(self.Kdelta_coords): + + study_name = f'{self.init_study_name}_Kdelta_X{coord[0]}_Y{coord[1]}' + study_name = study_name.replace(".", "_") + + # duplicate initial study + self.init_model.DuplicateStudyName( + self.init_study_name, + study_name, + True) + present_study = self.toolJd.GetCurrentStudy() + + # set rotor displacment + present_study.GetCondition(0).SetValue("UseEccentricity", 1) + present_study.GetCondition(0).SetXYZPoint( + "PartOffset",coord[0],coord[1],0) + present_study.GetCondition(0).SetXYZPoint( + "AxisOffset",coord[0],coord[1],0) + + # obtain handle to circuit for present study + circuit = present_study.GetCircuit() + function1 = self.toolJd.FunctionFactory().Constant(0) + circuit.GetComponent("CS_t-1").SetFunction(function1) + circuit.GetComponent("CS_t-2").SetFunction(function1) + circuit.GetComponent("CS_t-3").SetFunction(function1) + circuit.GetComponent("CS_s-1").SetFunction(function1) + circuit.GetComponent("CS_s-2").SetFunction(function1) + circuit.GetComponent("CS_s-3").SetFunction(function1) + present_study.RunAllCases() + + # extract FEA results from CSV + force_df = self._extract_csv_results( + present_study.GetName(),"Force") + force_df_list.append(force_df) + + return force_df_list + + @lru_cache + def run_Kphi_simulations(self)->list: + """Script to run Kdelta simulations in JMAG + + Returns: + list: list of pd.Dataframes containing voltage results from simulations + """ + + print('==============================================================') + print("Running Kphi simulations ......") + + bemf_df_list = [] + for speed in tqdm(self.Kphi_speed): + + # duplicate initial study + self.init_model.DuplicateStudyName( + self.init_study_name, + f'{self.init_study_name}_Kphi_{str(round(speed,6)).replace(".", "_")}', + True) + + # set rotor speed + present_study = self.toolJd.GetCurrentStudy() + present_study.GetCondition(0).SetValue("AngularVelocity",speed) + + # obtain handle to circuit for present study + circuit = present_study.GetCircuit() + function1 = self.toolJd.FunctionFactory().Constant(0) + circuit.GetComponent("CS_t-1").SetFunction(function1) + circuit.GetComponent("CS_t-2").SetFunction(function1) + circuit.GetComponent("CS_t-3").SetFunction(function1) + circuit.GetComponent("CS_s-1").SetFunction(function1) + circuit.GetComponent("CS_s-2").SetFunction(function1) + circuit.GetComponent("CS_s-3").SetFunction(function1) + present_study.RunAllCases() + + # extract FEA results from CSV + bemf_df = self._extract_csv_results(present_study.GetName(),"Voltage") + bemf_df_list.append(bemf_df) + + return bemf_df_list + + @cached_property + def idx_ignore(self)->int: + """Number of initial data points to ignore for first time step 1TS""" + idx_ignore = int(self.configuration.no_of_rev_1TS + *self.configuration.no_of_steps_per_rev_1TS) + return idx_ignore + + @cached_property + def project_file_path(self)->str: + """Path to JMAG .jproj file""" + path = f'{self.configuration.run_folder}{self.project_name}.jproj' + return path + + def _set_circuit_current_value(self,circuit,ampT,ampS,freq): + """Set DPNV circuit current sources to specified values""" + + torq_current_source = ['CS_t-1', 'CS_t-2', 'CS_t-3'] + sus_current_source = ['CS_s-1', 'CS_s-2', 'CS_s-3'] + + for idx, source in enumerate(torq_current_source): + func = self.toolJd.FunctionFactory().Composite() + f1 = self.toolJd.FunctionFactory().Sin(ampT, freq, -120*idx) + func.AddFunction(f1) + circuit.GetComponent(source).SetFunction(func) + + for idx, source in enumerate(sus_current_source): + func = self.toolJd.FunctionFactory().Composite() + f1 = self.toolJd.FunctionFactory().Sin( + ampS, freq, 120*idx) + f2 = self.toolJd.FunctionFactory().Sin( + -ampT / 2, freq, -120*idx) + func.AddFunction(f1) + func.AddFunction(f2) + circuit.GetComponent(source).SetFunction(func) + + def _extract_csv_results(self, study_name, type:str): + """Extract JMAG output from .csv file""" + csv_type_dict = {'Force':'_force.csv', + 'Torque':'_torque.csv', + 'Flux':'_flux_of_fem_coil.csv', + 'Voltage':'_circuit_voltage.csv' + } + path = self.configuration.jmag_csv_folder + study_name + csv_type_dict[type] + df = pd.read_csv(path, skiprows=6) + return df + + def _open_JMAG_file(self): + """Open and load specified JMAG file""" + toolJd = win32com.client.Dispatch("designer.Application") + toolJd.Show() + toolJd.load(self.project_file_path) + return toolJd + + def _get_init_study(self,toolJd): + """Obtain initial JMAG study and properties""" + toolJd.SetCurrentModel(0) + model = toolJd.GetModel(0) + study = toolJd.GetStudy(0) + study_name = study.GetName() + properties = study.GetStudyProperties() + return model,study,study_name,properties + + def _validate_attr(self): + """Validate input attributes""" + if not isinstance(self.problem, BSPMMachineConstantProblem): + raise TypeError( + 'Invalid problem type, must be BSPMMachineConstantProblem.' + ) + + if not isinstance(self.configuration, JMAG_2D_Config): + raise TypeError( + 'Invalid configuration type, must be JMAG_2D_Config.' + ) + +@dataclass +class BSPMMachineConstantResult: + Kf: float + Kt: float + Kphi: float + Kdelta: float + + + +