2. Analytical Machine Design Tutorial

  • Goal: Understand the base mach_eval classes

  • Complexity 3/5

  • Estimated Time 30 min

This tutorial demonstrates how to set up a MachineDesigner and implement EvaluationSteps from the mach_eval module. By the end of this tutorial you will be able to:

  • Create a MachineDesigner for modeling an electric machine.

  • Define EvaluationSteps to describe an evaluation process for an electric machine

The example classes used in this tutorial are chosen to illustrate the machine topology specification and evaluation process in a simple manner and are not intended to accurately model any physical details.

2.1. Tutorial Requirements

Prior to starting this tutorial, the user must configure their system with the following:

  1. All required Python packages are installed on system. (See Pre-requisites)

  2. eMach installed as a sub-module in a root folder of a git repository (See Rectangle Example)

2.2. Step 1: Create Python file for tutorial

In the root folder of your private Git repository (the repository that houses eMach as a submodule), create a new Python file mach_eval_tutorial.py to hold the code for this tutorial.

2.3. Step 2: Define import statements

Add the following import statements to the newly created mach_eval_tutorial.py file to load the required modules for this tutorial:

import numpy as np
from matplotlib import pyplot as plt
from eMach import mach_eval as me
from copy import deepcopy

2.4. Step 3: Define Machine Class

In this step, the Machine class is defined. This class is intended to act as a “Digital Twin” of a physical machine, which means it is designed to hold all the relevant information about a physical machine (i.e., geometric, material, and nameplate information). This class can be thought of as “what is on the desk.” Items such as operating conditions and other information needed to perform analysis are housed in the Settings class (defined in a later step).

The creation of the Machine class is split into five sub-steps: initialization, class constant parameters, input defined parameters, derived parameters, and auxiliary functions.

2.4.1. Step 3.1: Initialization

Copy and paste the following code block into your mach_eval_tutorial.py file to create a Machine class entitled ExampleMachineQ6p1y3. This code is used to initialize an object of the class. It takes in a set of geometric variables and material dictionaries and saves them to local variables within the object.

This class has been named ExampleMachineQ6p1y3 to tell the user that it describes a machine with 6 stator slots, 2 rotor poles, and a coil span of 3 slots. Descriptive names like this are helpful for creating clean and understandable code.

Notice the use of the _ character at the start of the local variable names. This is a naming convention in Python that informs users that these variables should be considered private and should therefore not be editted by code that resides outside of the ExampleMachineQ6p1y3 class.

Finally, note that this class is implementing the protocol me.Machine.

class ExampleMachineQ6p1y3(me.Machine):
    def __init__(self,r_ro,d_m,d_ag,l_tooth,
                    w_tooth,d_yoke,z_q,l_st,
                    magnet_mat,core_mat,coil_mat):
        self._r_ro = r_ro
        self._d_m = d_m
        self._d_ag = d_ag
        self._l_tooth=l_tooth
        self._w_tooth = w_tooth
        self._d_yoke = d_yoke
        self._z_q = z_q
        self._l_st = l_st
        self._magnet_mat= magnet_mat
        self._core_mat = core_mat
        self._coil_mat = coil_mat

2.4.2. Step 3.2: Class Constant Parameters

Copy and paste the following code block into your ExampleMachineQ6p1y3 class to create read-only parameters for the class. This code should be at the same indent level as the __init__ function. The step illustrates adding constant parameters to the Machine class.

When creating Machine classes, users may desire to create read-only, constant values for the machine. In this example, the number of slots Q, pole-pairs p, and the coil span y of the machine are constant. To accomplish this, the @property decorator is used to define these values to make these “read-only.” By coding in literal return values (instead of variable names), these properties are constants.

@property
def Q(self):
    return 6
@property
def p(self):
    return 1
@property
def y(self):
    return 3

2.4.3. Step 3.3: Input Defined Parameters

Copy and paste the following code block into the ExampleMachineQ6p1y3 class. This step demonstrates how the @property decorator can be used to expose “read-only” variables.

In step 3.1, the inputs to the initialization function were defined so that they were assigned to a self._ property. The code that you have copy-and-pasted in this step uses property decorators to allow reading the values of these variables.

@property
def r_ro(self):
    return self._r_ro
@property
def d_m(self):
    return self._d_m
@property
def d_ag(self):
    return self._d_ag
@property
def l_tooth(self):
    return self._l_tooth
@property
def w_tooth(self):
    return self._w_tooth
@property
def d_yoke(self):
    return self._d_yoke
@property
def z_q(self):
    return self._z_q
@property
def l_st(self):
    return self._l_st
@property
def magnet_mat(self):
    return self._magnet_mat
@property
def core_mat(self):
    return self._core_mat
@property
def coil_mat(self):
    return self._coil_mat

2.4.4. Step 3.4: Derived Parameters

Copy and paste the following code block into to the ExampleMachineQ6p1y3 class. This code demonstrates how the @property decorator can also be used to expose parameters that are defined as a function of multiple variables.

It is frequently convenient to define certain machine parameters in terms of other parameters. For example, while the geometry of a machine stator can be defined strictly based on the variables passed into the initializer (Step 3.1), this can be cumbersome to interpret and it can be useful to have quick access to derived properties, such as the inner stator radius (r_si below).

@property
def r_si(self):
    return self._r_ro+self._d_ag
@property
def r_sy(self):
    return self.r_si+self._l_tooth
@property
def r_so(self):
    return self.r_sy+self._d_yoke
@property
def B_delta(self):
    return self.d_m*self.magnet_mat['B_r']/(self.magnet_mat['mu_r']*self.d_ag+self.d_m)
@property
def B_sy(self):
    return np.pi*self.B_delta*self.r_si/(2*self.p*(self.d_yoke))
@property
def B_th(self):
    return self.B_delta*self.r_si*self.alpha_q/(self.w_tooth)
@property
def k_w(self):
    alpha=np.pi*((self.Q-2*self.y)/(self.Q*self.p))
    n=self.Q/(2*self.p)
    m=self.Q/(6*self.p)
    Beta=np.pi/n
    k_w=np.cos(alpha/2)*(np.sin(m*Beta/2))/(m*np.sin(Beta/2))
    self._k_w=k_w
    return self._k_w
@property
def A_slot(self):
    return np.pi*(self.r_sy**2-self.r_si**2)/self.Q - \
        self.w_tooth*(self.r_sy-self.r_si)
@property
def alpha_q(self):
    return 2*np.pi/self.Q

2.4.5. Step 3.5: Auxiliary Functions

Copy and paste the following code block into to the ExampleMachineQ6p1y3 class. This code illustrates the use-case for auxiliary functions added to a Machine class to facilitate calculation of performance properties.

There are several useful machine performance calculations which require combining information from within a Machine class and information that a Machine class does not contain. Auxiliary functions can be added to facilitate easy implementation of these calculations. Examples of this include electric loading A_hat and tip speed v_tip, both of which depend on outside information (i.e. current and speed).

def A_hat(self,I):
    N=self.Q/3
    A_hat=3*self.z_q*N*self.k_w*I/(np.pi*self.r_si)
    return A_hat
def v_tip(self,Omega):
    v_tip=Omega*self.r_ro
    return v_tip

2.5. Step 4: Define Settings Class

Copy and paste the following code block to create a settings class that can be used alongside the ExampleMachineQ6p1y3 machine.

mach_eval uses settings clases to hold information necessary for analyzing the machine, such as the current operating condition. In this tutorial, the settings class simply holds the rotational speed Omega and the motor phase current I.

class ExampleSettings:
    def __init__(self,Omega,I):
        self.Omega=Omega
        self.I=I

2.6. Step 5: Define the Architect

The Architect class of the mach_eval module is described in detail here. The purpose of the Architect is to convert an input tuple (which is presumably set up to compactly encode the free variables of an optimization) into a Machine object (which likely requires far more information than is contained by the free variables). For this example, the input tuple is defined using the following:

  • x[0] = r_ro Outer rotor radius

  • x[1] = d_m_norm Normalized magnet thickness

  • x[2] = l_st_norm Normalized stack length

  • x[3] = r_sy_norm Normalized stator yoke radius

  • x[4] = r_so_norm Normalized outer rotor radius

  • x[5] = w_tooth_norm Normalized tooth width

  • x[6] = z_q Number of turns

  • x[7] = I Stator current

Copy the following code into the Python file to implement the example architect.

The create_new_design method demonstrates how the input tuple values are interpretted to initialize an instance of the ExampleMachineQ6p1y3 class. Notice that material dictionaries (magnet_mat, core_mat, and coil_mat) are provided to the ExampleMotorArchitect upon initialization. This is the typical programming pattern for providing information that is required to create a Machine class but is not contained in the input tuple.

class ExampleMotorArchitect(me.Architect):
    """Class converts input tuple x into a machine object"""
    def __init__(self,magnet_mat,core_mat,
                    coil_mat):
        self.magnet_mat=magnet_mat
        self.core_mat=core_mat
        self.coil_mat=coil_mat
    def create_new_design(self,x:tuple):
        r_ro=x[0]
        d_m_norm=x[1]
        d_m=d_m_norm*r_ro
        l_st=x[2]*r_ro
        r_sy_norm=x[3]
        r_so_norm=x[4]
        w_tooth_norm=x[5]
        z_q=x[6]

        d_ag=.002
        Q=6

        r_si=r_ro+d_ag
        alpha_q=2*np.pi/Q
        w_tooth=2*r_si*np.sin(w_tooth_norm*alpha_q/2)
        r_so=r_so_norm*r_si
        r_sy=r_sy_norm*(r_so-r_si)+r_si
        d_yoke=r_so-r_sy
        l_tooth=r_sy-r_si


        machine=ExampleMachineQ6p1y3(r_ro,d_m,d_ag,l_tooth,
                    w_tooth,d_yoke,z_q,l_st,
                    self.magnet_mat,self.core_mat,self.coil_mat)

        return machine

2.7. Step 6: Define the SettingsHandler

The SettingsHandler class of the mach_eval module is also described in detail in the user guide. The SettingsHandler has a similar purpose to the Architect (step 5) in that it is responsible for converting the input tuple into the settings object.

Copy the following code into the Python file to implement the example SettingsHandler. In this tutorial, the SettingsHandler takes in a rotational speed Omega on initialization and extracts the current from the input tuple to create the ExampleSettings.

class ExampleSettingsHandler(me.SettingsHandler):
    """Settings handler for design creation"""
    def __init__(self,Omega):
        self.Omega=Omega
    def get_settings(self,x:tuple):
        I=x[7]
        settings = ExampleSettings(self.Omega,I)
        return settings

2.8. Step 7: Define the EvaluationStep s

The EvaluationStep protocol of the mach_eval module defines a function signature called step. This is the base level for an evaluation in the mach_eval module and is used to define an evaluation that is performed on a design. A detailed explanation of the EvaluationStep protocol and the associated State class is provided here.

Copy and paste the following code to add two evaluation steps. These steps are used to calculate the total power of the machine and the expected losses. Per the EvaluationStep protocol, each step class must contain a step method that takes in a state variable, performs some analysis, and returns the results along with an output state. The deepcopy method is used to provide a copy of the state which can be updated with new information without changing the input state.

class PowerEvalStep(me.EvaluationStep):
    def step(self,state_in):
        #unpack the input state
        B_delta=state_in.design.machine.B_delta
        r_ro=state_in.design.machine.r_ro
        l_st=state_in.design.machine.l_st
        I=state_in.design.settings.I
        A_hat=state_in.design.machine.A_hat(I)
        Omega=state_in.design.settings.Omega

        #perform evaluation
        V_r=np.pi*r_ro**2*l_st
        Power=Omega*V_r*B_delta*A_hat

        #write the state out
        state_out=deepcopy(state_in)
        state_out.conditions.Power=Power
        return [Power,state_out]

class LossesEvalStep(me.EvaluationStep):
    def step(self,state_in):
        w_tooth=state_in.design.machine.w_tooth
        l_tooth=state_in.design.machine.l_tooth
        alpha_q=state_in.design.machine.alpha_q
        r_si=state_in.design.machine.r_si
        r_so=state_in.design.machine.r_so
        r_sy=state_in.design.machine.r_sy
        I=state_in.design.settings.I
        z_q=state_in.design.machine.z_q
        A_slot=state_in.design.machine.A_slot
        k_fill=state_in.design.machine.coil_mat['k_fill']
        sigma=state_in.design.machine.coil_mat['sigma']
        k_ov=state_in.design.machine.coil_mat['k_ov']
        l_st=state_in.design.machine.l_st
        Omega=state_in.design.settings.Omega
        p=state_in.design.machine.p
        y=state_in.design.machine.y
        Q=state_in.design.machine.Q
        K_h=state_in.design.machine.core_mat['core_ironloss_Kh']
        b=state_in.design.machine.core_mat['core_ironloss_b']
        a=state_in.design.machine.core_mat['core_ironloss_a']
        K_e=state_in.design.machine.core_mat['core_ironloss_Ke']
        k_stack=state_in.design.machine.core_mat['core_stacking_factor']
        B_sy=state_in.design.machine.B_sy
        B_tooth=state_in.design.machine.B_th

        l_turn=2*l_st+y*alpha_q*(r_si+r_sy)*k_ov
        f=p*Omega/(2*np.pi)
        g_sy=(K_h*(f**a)*(B_sy**b) + K_e*(f*B_sy)**2)*k_stack
        g_th=(K_h*(f**a)*(B_tooth**b) + K_e*(f*B_tooth)**2)*k_stack
        A_cond=k_fill*A_slot/z_q
        J_hat=I/A_cond
        Q_tooth=g_th*w_tooth*l_st*l_tooth*Q
        Q_sy=g_sy*np.pi*(r_so**2-r_sy**2)*l_st
        Q_coil= (J_hat**2)*l_turn*k_fill*A_slot/(sigma*2)
        state_out=deepcopy(state_in)
        state_out.conditions.losses=[Q_tooth,Q_sy,Q_coil]
        return [[Q_tooth,Q_sy,Q_coil],state_out]

2.9. Step 8: Define Material Dictionaries

Copy and paste the following material dictionaries into mach_eval_tutorial.py. These dictionaries hold standard material information needed to model that machine.

core_mat = {
    'core_material'              : 'M19Gauge29',
    'core_material_density'      : 7650, # kg/m3
    'core_youngs_modulus'        : 185E9, # Pa
    'core_poission_ratio'        : .3,
    'core_material_cost'         : 17087, # $/m3
    'core_ironloss_a'            : 1.193,# freq
    'core_ironloss_b'            : 1.918,# field
    'core_ironloss_Kh'           : 55.1565, # W/m3
    'core_ironloss_Ke'           : 0.050949, # W/m3
    'core_therm_conductivity'    : 28, # W/m-k
    'core_stacking_factor'       : .96, # percentage
    'core_saturation_feild'      : 1.6 #T
    }

coil_mat = {
    'Max_temp'                   : 150, # Rise C
    'k_ov'                       : 1.8,
    'sigma'                      : 5.80E7,
    'k_fill'                     : .38}
magnet_mat = {
    'magnet_material'            : "Arnold/Reversible/N40H",
    'magnet_material_density'    : 7450, # kg/m3
    'magnet_youngs_modulus'      : 160E9, # Pa
    'magnet_poission_ratio'      :.24,
    'magnet_material_cost'       : 712756, # $/m3
    'magnetization_direction'    : 'Parallel',
    'B_r'                        : 1.285, # Tesla, magnet residual flux density
    'mu_r'                       : 1.062, # magnet relative permeability
    'magnet_max_temperature'     : 80, # deg C
    'magnet_max_rad_stress'      : 0, # Mpa
    'magnet_therm_conductivity'  : 8.95, # W/m-k
    }

2.10. Step 9: Creating MachineDesigner

The next step is to create an object of the MachineDesigner class. This is a concrete class provided by mach_eval to hold an Architect (created in step 5) and a SettingsHandler (created in step 6). The MachineDesigner.create_design() method receives an input tuple (the free variables) and uses the Architect and SettingsHandler to create a Machine and Settings object. The function returns a Design object containing the Machine and Settings (design.machine and design.setttings).

Copy and paste this code into the bottom of the Python file.

Omega=100
arch=ExampleMotorArchitect(magnet_mat,core_mat,coil_mat)
settings_handler=ExampleSettingsHandler(Omega)
des=me.MachineDesigner(arch,settings_handler)
r_ro=.1
d_m_norm=.0025
l_st_norm=5
r_sy_norm=.25
r_so_norm=10
w_tooth_norm=.8
z_q=100
I=20
x=[r_ro,d_m_norm,l_st_norm,r_sy_norm,r_so_norm,w_tooth_norm,z_q,I]
design=des.create_design(x)

2.11. Step 10: Creating MachineEvaluator

Like the MachineDesigner in the previous step, the MachineEvaluator is a concrete class provided by mach_eval. This class takes in an ordered list of EvaluationSteps on initialization. When the evaluate method is called the MachineEvaluator will loop over the step functions of the provided EvaluationSteps in order. The results of the evaluate method will be an ordered list of [state_in,results,state_out] for each step provided. This gives a useful log of how the design and state objects have changed over the evaluation process.

The following code implements the two example EvaluationSteps provided, and demonstrates how to initialize the MachineEvaluator. Copy this code into the bottom of the Python file and hit run. The results object from the evaluation of the machine should be printed in the console.

power_step=PowerEvalStep()
loss_step=LossesEvalStep()
evaluator=me.MachineEvaluator([power_step,loss_step])
results=evaluator.evaluate(design)
print(results)

2.12. Step 11: Interpreting Results

The results of the optimization printed in the console are interpreted in this step. The results object is an ordered list of input states, results, and output states corresponding to each evaluation step. The output state of a step and the input state of the next step are identical, this provides an accounting of how the state object may change during the optimization.

Trial1

The results of the example code should look like the following. The form shown in the image above can be seen here, for example for the first evaluation step it is input state, results of power evaluation step of 7.96kW then output state. The same can be seen for the second step, where the losses are provided as [Q_tooth, Q_sy , Q_coil]

[[<eMach.mach_eval.mach_eval.State object at 0x000001CF936D7100>, 7960.007929035136, <eMach.mach_eval.mach_eval.State object at 0x000001CF936D7370>],
[<eMach.mach_eval.mach_eval.State object at 0x000001CF936D7850>, [2.8976596216446304, 2.78480754750738, 9.476525268802455],
<eMach.mach_eval.mach_eval.State object at 0x000001CF936D7AF0>]]

2.13. Conclusion

You have successfully completed this tutorial of the base capabilities of the mach_eval module. The following tasks are provided to demonstrate you understand how these classes work:

  • Create a new EvaluationStep which calculates the motor efficiency

  • Copy and modify the example Machine and Architect classes to analyze a Q12p2y3 machine, could these classes be modified to use the same architect?

  • Bonus task: Using the skills learned in the Previous tutorial, can you create a simple optimization using the provided MachineDesigner and MachineEvaluator?