Unexpected(?) behavior using ModelingToolkit.jl, discrete_events and irreducible

Hi everyone,

I am running into an issues using discrete events and irreducible variables in ModelingToolkit.jl. I hope the following example is sufficent as an MWE. It is a composite model of two tanks in sequence which are connected with valves. In the future, I want to set the valve position S from a soft-controller in a discrete event. For now, I just use a sine function. Furthermore, a level sensor should send back the level in the tank L to the soft-controller, which is “simulated” by println() within the Callback.

using ModelingToolkit
using ModelingToolkit: t_nounits as t, D_nounits as D
using DifferentialEquations
using Plots

# Create a simple Tank model with reading and writing events
function affect!(integ,vars,pars,_)
    integ.ps[pars.S] = 0.5 + 0.5*sin(2*3.14*integ.ps[pars.freq]*integ.t) 
    println("$(integ.u[vars.L])") # Read L using the passed identifier
end

@mtkmodel Tank begin
    @constants begin
        g::Float64 = 9.81, [description = "Gravitational acceleration."]
    end
    @parameters begin
        S(t)::Float64 = 0.0
        k_V::Float64 = 10.0
        rho::Float64 = 1000.0
        A::Float64 = 1.0
        freq::Float64 = 0.1
    end
    @variables begin
        F_in(t)::Float64, [guess = 0.0]
        F_out(t)::Float64, [guess = 0.0]
        L(t)::Float64, [guess = 0.0, irreducible = true] # Making L irreducible
        V(t)::Float64 = 1.0
        Dp(t)::Float64, [guess = 0.0]
    end
    @equations begin
        D(V) ~ F_in - F_out
        V ~ A*L
        Dp ~ rho*g*L
        F_out ~ S*k_V*sign(Dp)*sqrt(abs(Dp)/100000*1000/rho)
    end
    @discrete_events begin
        1.0 => (affect!,[L],[freq,S],[S],nothing) # Pass indentfier for L to affect! function
    end
end

# Create composite model
@mtkmodel SimulateTankSystem begin
    @components begin
        tank1 = Tank(;A=1.0)
        tank2 = Tank(;A=2.0,freq=0.2)
    end
    @equations begin
        tank1.F_in ~ 2.0
        tank2.F_in ~ tank1.F_out
    end
end

# Main function
function main()
    @mtkbuild sys = SimulateTankSystem()

    prob = ODEProblem(sys, [], (0.0, 10.0);tstops=0:0.1:10.0)

    sol = solve(prob,QNDF()) # A DAE solver is needed here, as the equation for L remains the same!

    # Plotting
    plot(sol;idxs=sys.tank1.S)
    plot!(sol;idxs=sys.tank2.S)
    plot(sol;idxs=sys.tank1.L)
    plot!(sol;idxs=sys.tank2.L)
end

main()

Now the issue is, that structural_simplify() in mtkbuild() does eliminate L, if you do not apply a workarround. I studied multiple issues on this topic, e.g. #1646, #1797. They are still open but have been arround for quite a while already. They suggest to use irreducible = true to avoid that the required variables are eliminated from the system.

Now my problem: If I use the code above the volumes V and the levels L are set back to the consistent initial conditions each time the callback is called. Is this intentional? What am I doing wrong here?
image

Furthermore: Have other solutions to this problem perhaps been worked out in the meantime?

  • I saw one suggest like reconstructing observables without using irreducible. I could hardcode this in the callback using integ.sol[integ.f.sys.tank1.L][end], but I am not sure what this does to performance of the simulation for a growing number of timesteps saved within sol. I have the impression that the whole time history is calculated each time I call integ.sol. Furthermore, I would like to construct more complex systems in future and am not sure yet, how to get the path to the specific tank in a generic form into the callback.
  • Or are discrete-time variables (SampledData) the way to go?

Thank you for your help :slight_smile: !

The default behavior in DiffEq is to run a re-initialization scheme following a change in variables. To avoid that you can add modify you affect! function as:

function affect!(integ,vars,pars,_)
    integ.ps[pars.S] = 0.5 + 0.5*sin(2*3.14*integ.ps[pars.freq]*integ.t) 
    println("$(integ.u[vars.L])") # Read L using the passed identifier
    # force DiffEq to not re-initialize the variables
    u_modified!(integ,false)
end

I hope this is what you were looking for.

Yes, that solves my issue :slight_smile: Thank you very much!