Composing Standard Library Components
In this demo we will show how to:
build a DC motor model using Dyad standard library components
test the model with imposed boundary conditions
add a controller to the model and simulate varying the controller parameters
In this example, we will develop a simple model of a DC motor using standard electrical and rotational components. Following the implementation of the model, we will test the motor and also implement a controller.
The image below shows a schematic of the simple motor model:
To get started, create a new component library, as indicated on the "Getting started" page.
Adding the Standard Libraries
We will create the DC motor model using electrical, rotational, and block components from the Dyad standard libraries. These libraries and the various components, parameters, interfaces, etc. are documented at [TODO: INSERT LINKS. This will come once the stdlib docs are integrated here.] For illustration purposes, we will show how to add the library packages to your environment even though the step above already included them. From the REPL, issue the following commands:
using Pkg
Pkg.add([
"BlockComponents",
"RotationalComponents",
"ElectricalComponents"
])
DC Motor
Now that the required libraries are in place, let's create the simple DC motor component.
The interface for the DC motor model consists of two electrical pin s p and n, the mechanical spline shaft, and a mechanical spline housing. The motor is modeled as a resistance and an inductance. The coupling between the electrical and rotational domains occurs via an electro-motive force (EMF) component. The voltage across the EMF is proportional to the angular velocity and the current is proportional to the torque. The motor mechanical component include viscous friction in, e.g., a bearing and the inertia of the shaft. The reaction torques on the friction and emf components are connected to the housing connector.
The code below is the motor model in dc_motor.dyad
in the dyad folder.
component DCMotor
p = Pin()
n = Pin()
shaft = Spline()
housing = Spline()
R1 = ElectricalComponents.Resistor(R=R)
L1 = ElectricalComponents.Inductor(L=L)
emf = ElectricalComponents.RotationalEMF(k=k)
inertia = RotationalComponents.Inertia(J=J)
friction = RotationalComponents.Damper(d=f)
# Armature resistance
parameter R::Resistance = 0.5
# Armature inductance
parameter L::Inductance = 4.5e-3
# Motor constant
parameter k::ElectricalTorqueConstant = 0.5
# Motor inertia
parameter J::Inertia = 0.02
# Motor friction factor
parameter f::RotationalDampingConstant = 0.01
relations
connect(p, R1.p)
connect(R1.n, L1.p)
connect(L1.n, emf.p)
connect(emf.n, n)
connect(emf.rotor, inertia.spline_a, friction.spline_a)
connect(friction.spline_b, emf.housing, housing)
connect(inertia.spline_b, shaft)
end
Testing the Motor
Now that we have constructed the DC motor model, let's create a test for it. In this test, we will apply a constant voltage across the motor electrical pins. We will apply the motor load as a prescribed torque with a step input.
The code below is in test_dc_motor_load.dyad
in the dyad folder. It includes the test model TestDCMotorLoad
along with the analysis DCMotorLoad
that specifies the transient test simulation and allows access to the parameter V in the analysis to set the motor voltage in the model.
analysis DCMotorLoadAnalysis
extends TransientAnalysis(stop=4, abstol=1m, reltol=1m)
model = TestDCMotorLoad(V_motor=V)
parameter V::Voltage = 1.0
end
component TestDCMotorLoad
motor = DCMotor()
ground = ElectricalComponents.Ground()
source = ElectricalComponents.VoltageSource()
fixed = RotationalComponents.Fixed()
load = RotationalComponents.TorqueSource()
load_source = BlockComponents.Step(height=tau_load, start_time=load_step_start_time)
voltage_source = BlockComponents.Constant(k=V_motor)
# Motor voltage
parameter V_motor::Voltage = 1
# Amplitude of load torque step
parameter tau_load::Torque = -0.3
# Load step start time
parameter load_step_start_time::Time = 2
relations
initial motor.L1.i = 0
initial motor.inertia.w = 0
connect(voltage_source.y, source.V)
connect(load_source.y, load.tau)
connect(source.p, motor.p)
connect(motor.n, source.n, ground.g)
connect(motor.shaft, load.spline)
connect(motor.housing, load.support, fixed.spline)
end
We can now run the DCMotorLoad
analysis that we defined above. This analysis includes the start time, stop time, tolerances, etc. You can also modify parameters of the model when executing the analysis. For the first simulation, issuing the command below will run the analysis and generate some plots of the state variables in the system. At the beginning of the sim, there is no load and the motor acceleratse to a steady state speed (recall there is friction in the model). Once the load kicks in, the motor current increases to match the load torque and the speed drops.
using Plots
plot(DCMotorLoadAnalysis())
If you want to see each plot on a separate graph, you can add some layout options to the plots. Note that you can use any of the options from Plots.jl or even use a different plotting backend like Makie.jl.
plot(DCMotorLoadAnalysis(), layout=(3,1), size=(400, 400))
If you would like to plot other variables, you can use the symbolic indexing to reference the variables by name. For example, the command below will plot the motor torque:
using DyadInterface
result = DCMotorLoadAnalysis()
model = artifacts(result, :SimplifiedSystem)
plot(result; idxs = model.motor.emf.tau, size=(400, 400))
Controlling the Motor
In the previous model, we applied a voltage to the motor and a load and saw the resulting speed response. Now let's add a controller to control the speed. To do so, we will use a PID controller and also wire a speed sensor to provide the motor speed to the controller. The desired motor speed is set as a constant.
The code below is in test_dc_motor_load_controlled.dyad
in the dyad folder. It includes the test model TestDCMotorLoadControlled
along with the analysis DCMotorLoadControlled
that specifies the test simulation and allows easy access to vary the controller parameters in the analysis.
analysis DCMotorLoadControlledAnalysis
extends TransientAnalysis(stop=4, abstol=1m, reltol=1m)
model = TestDCMotorLoadControlled(k=k, Ti=Ti, Td=Td, Nd=Nd)
# Controller gain
parameter k::Real = 0.5
# Controller time constant of the integrator block
parameter Ti::Time = 0.1
# Controller Time constant of the derivative block
parameter Td::Time = 1e5
# Controller filter parameter
parameter Nd::Real = 10
end
component TestDCMotorLoadControlled
motor = DCMotor()
ground = ElectricalComponents.Ground()
source = ElectricalComponents.VoltageSource()
fixed = RotationalComponents.Fixed()
load = RotationalComponents.TorqueSource()
load_source = BlockComponents.Step(height=tau_load, start_time=load_step_start_time)
speed_reference = BlockComponents.Constant(k=w_motor)
controller = BlockComponents.LimPID(k=k, Ti=Ti, Td=Td, Nd=Nd, y_max=5, y_min=-5)
signal_ff = BlockComponents.Constant(k=0)
speed_sensor = RotationalComponents.VelocitySensor()
# Motor desired speed
parameter w_motor::AngularVelocity = 1
# Amplitude of load torque step
parameter tau_load::Torque = -0.3
# Load step start time
parameter load_step_start_time::Time = 2
# Controller gain
parameter k::Real = 0.5
# Controller time constant of the integrator block
parameter Ti::Time = 0.1
# Controller Time constant of the derivative block
parameter Td::Time = 1e5
# Maximum derivative gain
parameter Nd::Real = 10
relations
initial motor.L1.i = 0
initial motor.inertia.w = 0
u: analysis_point(controller.y, source.V)
y: analysis_point(speed_sensor.w, controller.u_m)
r: analysis_point(speed_reference.y, controller.u_s)
connect(load_source.y, load.tau)
connect(source.p, motor.p)
connect(motor.n, source.n, ground.g)
connect(motor.shaft, load.spline)
connect(motor.housing, load.support, fixed.spline)
connect(speed_reference.y, controller.u_s)
connect(speed_sensor.w, controller.u_m)
connect(controller.y, source.V)
connect(controller.u_ff, signal_ff.y)
connect(speed_sensor.spline, motor.shaft)
end
Let's explore the influence of the controller parameters on the resulting speed response. Here is the analysis run with a relatively low controller gain.
result = DCMotorLoadControlledAnalysis(k=0.1)
model = result.spec.model
kwargs = (idxs = [model.motor.inertia.w, model.controller.y], size=(800, 500), layout=(2,1))
plot(result; kwargs...)
Now let's re-run the analysis with a higher controller gain. Observe the much better speed control response.
plot(DCMotorLoadControlledAnalysis(k=0.5); kwargs...)
To compare the two simulations, we can use the following command (note the plot! syntax for the second simulation to reuse the existing plot).
plot(DCMotorLoadControlledAnalysis(k=0.1); kwargs...)
plot!(DCMotorLoadControlledAnalysis(k=0.5); kwargs...)
Tuning the controller automatically
To tune the controller parameters, we can use the PID autotuning analysis:
analysis DCMotorTuningAnalysis
extends DyadControlSystems.PIDAutotuningAnalysis(
measurement = "y",
control_input = "u",
Ts = 0.001,
duration = 0.1,
Ms = 1.2,
Mt = 1.2,
Mks = 400,
wl = 1,
wu = 1e4,
tol = 1e-8
)
model = TestDCMotorLoadControlled()
end
asol = DCMotorTuningAnalysis() # Run the tuning analysis
opt_p = artifacts(asol, :OptimizedParameters)
k, Ti, Td, Tf = [opt_p[!, par][] for par in [:Kp_standard, :Ti_standard, :Td_standard, :Tf]]
Nd = Td / Tf
plot(DCMotorLoadControlledAnalysis(k=0.5); kwargs..., title=permutedims(kwargs.idxs), lab="Initial")
plot!(DCMotorLoadControlledAnalysis(; k, Ti, Td, Nd); kwargs..., lab="Tuned")
To learn more about controls design features in Dyad, check out DyadControlSystems as well as the Dyad control analyses.
Conclusion
In this tutorial, we have shown how to use components from the Dyad standard library to build a simple DC motor and add a controller. We also illustrated how easy it is to vary parameters in the simulation via the analysis specification.