ModelingToolkit.jl icon indicating copy to clipboard operation
ModelingToolkit.jl copied to clipboard

sol[inlet.conc] does not work in flow connector

Open cstjean opened this issue 10 months ago • 9 comments

Describe the bug 🐞

If I have a stream connector for a concentration, sol[inlet.concentration] should work. It doesn't, but it works for sol.outlet.concentration.

Minimal Reproducible Example 👇

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

@connector SolutionPin begin
    flow(t), [connect=Flow]
    conc(t), [connect=Stream]
    dummy(t)
end

@mtkmodel Source begin
    @components begin
        outlet = SolutionPin()
    end
    @equations begin
        outlet.flow ~ -10
        outlet.conc ~ 0.3
    end
end

@mtkmodel Tank begin
    @components begin
        inlet = SolutionPin()
    end
    @variables begin
        product_mass(t) = 0
        conc(t)
    end
    @equations begin
        D(product_mass) ~ inlet.flow * instream(inlet.conc)
        conc ~ instream(inlet.conc)  # obviously not physically correct
    end
end

@mtkmodel Room begin
    @components begin
        src = Source()
        tank = Tank()
    end
    @equations begin
        connect(src.outlet, tank.inlet)
    end
end

@mtkbuild room = Room()
prob = ODEProblem(room, [], (0, 10.0), [])
sol = solve(prob)

sol[room.src.outlet.conc]  # works
sol[room.src.inlet.flow]  # works
sol[room.tank.inlet.conc] # ERROR: ArgumentError: tank₊inlet₊conc(t) is neither an observed nor an unknown 
sol[room.tank.conc] # works

Adding instream() does not work either (it shouldn't be required anyway).

cstjean avatar Feb 25 '25 06:02 cstjean

The problem is that tank.inlet.conc is not present in the simplified system, so MTK doesn't know how to compute it.

julia> room
Model room:
Equations (1):
  1 standard: see equations(room)
Unknowns (1): see unknowns(room)
  tank₊product_mass(t) [defaults to 0]
Observed (6): see observed(room)

julia> observed(room)
6-element Vector{Equation}:
 src₊outlet₊flow(t) ~ -10.0
 src₊outlet₊conc(t) ~ 0.3
 tank₊inlet₊dummy(t) ~ -0.0
 tank₊inlet₊flow(t) ~ -src₊outlet₊flow(t)
 tank₊conc(t) ~ src₊outlet₊conc(t)
 src₊outlet₊dummy(t) ~ tank₊inlet₊dummy(t)

julia> equations(room)
1-element Vector{Equation}:
 Differential(t)(tank₊product_mass(t)) ~ tank₊inlet₊flow(t)*src₊outlet₊conc(t)

AayushSabharwal avatar Feb 28 '25 11:02 AayushSabharwal

Seems like a stream connector bug?

ChrisRackauckas avatar Mar 01 '25 18:03 ChrisRackauckas

Yeah likely. I'm looking into it.

AayushSabharwal avatar Mar 02 '25 07:03 AayushSabharwal

The problem is that tank.inlet.conc is not present in the simplified system, so MTK doesn't know how to compute it.

That's correct. room.tank.inlet.conc is not present in the original system either, so in that sense ModelingToolkit is not wrong for my posted MWE.

Nevertheless, there is still a problem: there is no way to get instream(room.tank.inlet.conc). plot(sol, idxs=[instream(room.tank.inlet.conc)]) fails with ERROR: MethodError: no method matching iterate(::SymbolicUtils.BasicSymbolic{Float64}). actualStream would also work for us, but it is not implemented (https://github.com/SciML/ModelingToolkit.jl/issues/1666).

cstjean avatar Jun 06 '25 07:06 cstjean

julia> sol(10, idxs=[instream(test_room.reactor.inlet.conc)])
1-element Vector{SymbolicUtils.BasicSymbolic{Float64}}:
 instream(-10.0)

MTK treated instream as a wrapper, and evaluated test_room.reactor.inlet.conc to -10, whereas it should be treating instream(test_room.reactor.inlet.conc) as a variable in itself, distinct from test_room.reactor.inlet.conc

cstjean avatar Jun 06 '25 07:06 cstjean

I believe I am encountering the same issue, it's really frustrating, did anyone find a workaround? (My case is pretty much the mixing tank example from above)

AV-BD avatar Jul 18 '25 16:07 AV-BD

I did! I stopped using stream connectors and my life got so much better. If your flow is unidirectional, you can do it too! Just replace connect with connect_flow, and adapt the code below to cover whatever variables are in your connectors.

""" Connect the `from` components to the `to` components. _The flow must be in the `from -> to` direction_.
"""
connect_flow(from, to::Vector) =  # Connect one component to N components
    vcat([from.conc ~ t.conc for t in to],
         from.flow + sum(t.flow for t in to) ~ 0)
connect_flow(from::Vector, to) =  # Connect N components to one component
    vcat([to.conc ~ sum(f.conc * f.flow for f in from) / sum(f.flow for f in from)],
         to.flow + sum(f.flow for f in from) ~ 0)
connect_flow(from, to) = connect_flow([from], to) # (from, [to]) would also be fine

cstjean avatar Jul 21 '25 06:07 cstjean

Another work-around is to introduce an inlet_conc ~ instream(inlet.conc) variable (observable)

cstjean avatar Jul 21 '25 06:07 cstjean

I see! That's relatively elegant, even if it's frustrating to have to resort to these workarounds It'd be nice to be ablew to use the normal Stream connectors, but well, things are moving

Talking about directionality, I was also struggling with switching mechanism (aka AON valves in my case), I realized that "discrete variables" (@parameters with (t) ) together with callbacks worked pretty well. Not sure you'll find this one useful, but maybe someone else that'll read :)

AV-BD avatar Jul 21 '25 08:07 AV-BD