PWM using Discrete Callbacks in Modeling Toolkit

I’m trying to create a PWM voltage source using modeling toolkit. I tried used the @discrete_events macro within the @mtkmodel block but doing so throws an error: ERROR: KeyError: key pwm₊output₊u(t) not found when running prob = ODEProblem(sys, Pair[], (0, 1.0e-3))

Here is the full code I’m trying to run:

using ModelingToolkit, OrdinaryDiffEq, Plots
using ModelingToolkitStandardLibrary.Electrical
using ModelingToolkitStandardLibrary.Blocks
using ModelingToolkit: t_nounits as t, D_nounits as D


@mtkmodel PWM begin
    # @extend v, i = oneport = OnePort()
    @parameters begin
        T = 0.2e-3
        duty = 0.5
        Vcc = 5
    end
    @components begin
        output = RealOutput()
    end
    @equations begin
        output.u ~ Vcc
    end
    @discrete_events begin
        (t == T*duty) => [output.u ~ 0]
        (t == T) => [output.u ~ Vcc]
    end
end

@mtkmodel PWM_test begin
    @parameters begin
        R = 1.0
        V = 5.0
    end
    @components begin
        resistor = Resistor(R = R)
        VDD = Voltage()
        pwm = PWM(Vcc = V)
        ground = Ground()
    end
    @equations begin
        connect(pwm.output, VDD.V)
        connect(VDD.p, resistor.p)
        connect(ground.g, VDD.n, resistor.n)
    end

end
@mtkbuild sys = PWM_test()
unknowns(sys)
prob = ODEProblem(sys, Pair[], (0, 1.0e-3))
sol = solve(prob)

plot(sol, idxs = [sys.resistor.i],
    title = "Circuit Demonstration")

Any ideas on what is going wrong here?

You should replace

    @equations begin
        output.u ~ Vcc
    end

with

    @equations begin
        D(output.u) ~ 0
    end

As the callback should handle the changing output.u value. Otherwise this equation would immediately pull the voltage back.
And you should use a continous callback. Full example:

using ModelingToolkit, OrdinaryDiffEq, Plots
using ModelingToolkitStandardLibrary.Electrical
using ModelingToolkitStandardLibrary.Blocks
using ModelingToolkit: t_nounits as t, D_nounits as D


@mtkmodel PWM begin
    # @extend v, i = oneport = OnePort()
    @parameters begin
        T = 0.1
        duty = 0.5
        Vcc = 5
    end
    @components begin
        output = RealOutput(u_start=Vcc)
    end
    @equations begin
        D(output.u) ~ 0
    end
    @continuous_events begin
        (t ~ T*duty) => [output.u ~ 0]
        (t ~ T) => [output.u ~ Vcc]
    end
end

@mtkmodel PWM_test begin
    @parameters begin
        R = 1.0
        V = 5.0
    end
    @components begin
        resistor = Resistor(R = R)
        VDD = Voltage()
        pwm = PWM(Vcc = V)
        ground = Ground()
    end
    @equations begin
        connect(pwm.output, VDD.V)
        connect(VDD.p, resistor.p)
        connect(ground.g, VDD.n, resistor.n)
    end

end
@mtkbuild sys = PWM_test()
unknowns(sys)
prob = ODEProblem(sys, [sys.pwm.output.u => 0.0], (0, 1.0); dt=0.001)
sol = solve(prob, Tsit5())

plot(sol, idxs = [sys.resistor.i],
    title = "Circuit Demonstration")

Thanks for the quick reply!

This does solve my problem, but I quickly run into another; I cannot make these steps periodic. From the docs it seems periodic callbacks are only available using @discrete_events

My end goal is to make a PWM source with all the knobs to turn available in LTSpice.

  • V_initial
  • V_on
  • T_rise
  • T_fall
  • T_on
  • T_period

This supply would oscillate into t = infinity

With that in mind, what is the recommended approach?

You could maybe use a modulo block, where the remainder of the time divided by T_rise is your trigger for turning on the pwm, and the remainder of the time divided by T_fall is the trigger for going down again. Or you can make a trigger by having a variable called trigger and equation D(trigger) ~ 1, and when its value reaches T_rise, you set it back to zero with a continuous callback and trigger the PWM.

Hi twarczi, I am also new to ModelingToolkit but I found your problem interesting.

I played with using continous callbacks with a slope varible as well like D(output.u) ~ slope. That way I was able to make the rising and falling sections of the pulse. But It keep getting MaxIters warnings and stopping. I think what might of happened is it was sort of overdefined, like the slope set to non zero and with the output still defined as a constant and it would conflict, maybe just at the discontinuity.

I assume there is some reason you are not using an external julia function to generate the PWM (and avoiding callbacks alltogether). Do you want a PWM mtkmodel to allow control of the block by other blocks? I think that is still possible by using callbacks to update the parameters going to the external function, here is my idea:

using ModelingToolkit, OrdinaryDiffEq, Plots
using ModelingToolkitStandardLibrary.Electrical
using ModelingToolkitStandardLibrary.Blocks
using ModelingToolkit: t_nounits as t, D_nounits as D

function PWM_fun(t, T, duty, Vcc)
    t_off = duty*T
    t_cycle = t % T

    if(t_cycle < t_off)
        return Vcc
    else
        return 0
    end
end

@register_symbolic PWM_fun(t, T, duty, Vcc)

@mtkmodel PWM begin
    @parameters begin
        #RC rise tim
        t_rise_10_90=0.1
        # 10 to 90 rise time is about 2.2 τ
        τ = t_rise_10_90/2.2
        Vcc= 5
        T = 1
        duty=0.3

    end
    @components begin
        output = RealOutput(u_start=Vcc)
    end
    @variables begin
        V(t)
    end
    @equations begin
        V ~ PWM_fun(t, T, duty, Vcc)
        D(output.u) ~ (V - output.u) / τ
    end
    @continuous_events begin
        # we can change the parameters of the
        # external PWM function on the fly
        [t ~ 3] => [duty ~ 0.8]
        [t ~ 6] => [duty ~ 0.2]
    end
end

@mtkmodel PWM_test begin
    @parameters begin
        R = 1.0
        V = 5.0
    end
    @components begin
        resistor = Resistor(R = R)
        VDD = Voltage()
        pwm = PWM(Vcc = V)
        ground = Ground()
    end
    @equations begin
        connect(pwm.output, VDD.V)
        connect(VDD.p, resistor.p)
        connect(ground.g, VDD.n, resistor.n)
    end

end
@mtkbuild sys = PWM_test()
unknowns(sys)
prob = ODEProblem(sys, [sys.pwm.output.u => 0.0], (0, 10.0))
sol = solve(prob, Tsit5())

plot(sol, idxs = [sys.resistor.i],
     xticks=0:1:10,
     title = "Circuit Demonstration",
     minorgrid=true)

I used an external function to make the raw square wave PWM and then a single order lag / low pass filter to allow you to set the rise/fall time in a more realistic way.

I also built an external function to immatate the exact LTspice style “trapezoid” pulse.

function PWM_fun(t, T, duty, t_rise, t_fall, Vcc)
    rise_slope = Vcc/t_rise
    fall_slope = -Vcc/t_fall
    t_off = duty*T

    t_cycle = t % T

    if(t_cycle < t_rise)
        return t_cycle*rise_slope
    elseif(t_cycle < t_off)
        return Vcc
    elseif(t_cycle < t_off + t_fall)
        return Vcc + (t_cycle - t_off)*fall_slope
    else
        return 0
    end
end

I was able to plug this in to the above example and use a fast rise time so that I only had a small smoothing of the corners of the pulse.

I feel like you should be able to just use the external functon to define the output directly without low a pass filter but I can’t seem to get that to work. Intuitively I guess that makes sense, it’s like I am trying to force a system to a certain state but it doesn’t actually have any degrees of freedom to allow it to reach that state, as if it is infinitly stiff. It seems like the low pass filter gives it enough slop to allow it to figure itself out.

Have you tried using a stiff solver like FBDF()? In the example you are using Tsit5(), which isn’t necessarily good for stiff problems.