New interpolation method ModelingToolkit

I am trying to use the new interpolation method via parameters (Callable parameters and interpolating data · ModelingToolkit.jl)

However I am struggling to understand how I should use it. This is my model:

@mtkmodel SingleDiode begin
    @parameters begin
        (I_L::Function)(..), [description = "Irradiance current (A)"]
        n::Float64 = 1.0, [description = "Ideality factor"]
        I_s::Float64 = 1e-6, [description = "Saturation current (A)"]
        R_sh_val::Float64 = 1e5, [description = "Shunt resistance (Ohm)"]
        R_s_val::Float64 = 1e-1, [description = "Series resistance (Ohm)"]
    end

    @components begin
        R_sh = Resistor(R = R_sh_val)
        R_s = Resistor(R = R_s_val)
        diode = HeatingDiode(n = n, Is = I_s)
        source = Current()
        temp = FixedTemperature(T = 300.0)
        pos = Pin()
        neg = Pin()
    end
    @equations begin
        source.I.u ~ I_L(t)

        # positive side
        connect(source.n, diode.p)
        connect(source.n, R_sh.p)
        connect(source.n, R_s.p)
        connect(R_s.n, pos)

        # negative side
        connect(source.p, diode.n)
        connect(source.p, R_sh.n)
        connect(R_sh.n, neg)

        # temperature 
        connect(temp.port, diode.port)
    end
end

Trying to initialize this results in:

ERROR: LoadError: MethodError: Cannot convert an object of type Expr to an object of type Symbol

Without the interpolation the model runs fine.

I got rid of the explicit parameter method and replaced it by ParametrizedInterpolation:

@mtkmodel HeatingDiode begin
    begin
        k = 1.380649e-23 # Boltzmann constant (J/K)
        q = 1.602176634e-19 # Elementary charge (C)
    end

    @extend v, i = oneport = OnePort(; v = 0.0)
    @components begin
        port = HeatPort()
    end
    @parameters begin
        Is = 1e-6, [description = "Saturation current (A)"]
        n = 1, [description = "Ideality factor"]
    end
    @variables begin
        Vt(t), [description = "Thermal voltage"]
    end
    @equations begin
        Vt ~ k * port.T / q  # Thermal voltage equation
        i ~ Is * (exp(v / (n * Vt)) - 1)  # Shockley diode equation
        port.Q_flow ~ -v * i  # -LossPower
    end
end

function SingleDiode(data, time)
    @named I_L = ParametrizedInterpolation(LinearInterpolation, data, time)

    @mtkmodel Single_Diode begin
        @parameters begin
            n::Float64 = 1.0, [description = "Ideality factor"]
            I_s::Float64 = 1e-6, [description = "Saturation current (A)"]
            R_sh_val::Float64 = 1e5, [description = "Shunt resistance (Ohm)"]
            R_s_val::Float64 = 1e-1, [description = "Series resistance (Ohm)"]
        end

        @components begin
            R_sh = Resistor(R = R_sh_val)
            R_s = Resistor(R = R_s_val)
            diode = HeatingDiode(n = n, Is = I_s)
            source = Current()
            temp = FixedTemperature(T = 300.0)
            pos = Pin()
            neg = Pin()
        end

        @equations begin

            # positive side
            connect(I_L.output, source.I)
            connect(source.n, diode.p)
            connect(source.n, R_sh.p)
            connect(source.n, R_s.p)
            connect(R_s.n, pos)

            # negative side
            connect(source.p, diode.n)
            connect(source.p, R_sh.n)
            connect(source.p, neg)

            # temperature 
            connect(temp.port, diode.port)
        end
    end

    @mtkbuild sys = Single_Diode()
    return sys
end

Which gives:

ExtraVariablesSystemException: The system is unbalanced. There are 35 highest order derivative variables and 34 equations.
More variables than equations, here are the potential extra variable(s):
pos₊v(t)
Note that the process of determining extra variables is a best-effort heuristic. The true extra variables are dependent on the model and may not be in this list.

I haven’t been able to figure out what’s missing yet. Any suggestions are welcome

Regarding your root comment using the explicit interpolation parameter:

Could you give an example for how you’re creating the model? The code snippet only includes the model definition. In addition, I would recommend specifying a more concrete type than Function for type stability. If you want to swap out the function after building the model, you could go the FunctionWrapper route and declare it as I_L(::Float64)::Float64.


As for the second approach using ParametrizedInterpolation:

I got rid of the explicit parameter method and replaced it by ParametrizedInterpolation:

There are two problems with this model:

ParametrizedInterpolation is created outside the @mtkmodel and used inside. This doesn’t include it as a subcomponent. It seems that the way ParametrizedInterpolation is defined prevents it from being used inside @components, which is something the standard library should probably fix. In the meantime, the last few lines of function SingleDiode should be:

@named sys = Single_Diode()
sys = compose(sys, [I_L])
structural_simplify(sys)

The second problem is the reason your system is unbalanced. The ParametrizedInterpolation needs an input, using which it samples the data. It does not sample with respect to time by default. If you wish to sample with respect to time, you can add I_L.input.u ~ t to the equations.

An additional performance enhancement for this approach:

Judging from the root comment, it seems you do not need the interpolation data as a parameter. In such a case, you can replace ParametrizedInterpolation with Interpolation, which simply stores the interpolant as a callable symbolic.

1 Like

Thank you very much for your detailed comments, it’s working now. I put the code at the bottom of this comment for reference, if anyone is reading in the future.

For context, I am trying to build the single-diode PV equivalent circuit: Single Diode Equivalent Circuit Models – PV Performance Modeling Collaborative (PVPMC).

The HeatingDiode is from my PR here: Add diode component by langestefan · Pull Request #343 · SciML/ModelingToolkitStandardLibrary.jl · GitHub

This is the init, just for testing purposes:

dt = 1
t_end = 10.0
time = 0:dt:t_end
data = rand(length(time))
sys = SingleDiode(data, time)

prob = ODEProblem(sys, Pair[], (0.0, t_end))
sol = solve(prob)

The goal was to support any function, analytical or based on data. I will look into FunctionWrapper, thank you.

Yes exactly, that gives naming errors so that’s why I put it outside initially. I can make a PR to fix that.

So initially I was just trying to figure out how to use the new interpolation functionality. But I read this part in the docs on SampledData Component · ModelingToolkitStandardLibrary.jl

The main advantage of this block over the Interpolation one is that one can use it for optimization problems. Currently, this supports forward mode AD via ForwardDiff, but due to the increased flexibility of the types in the component, this is not as fast as the Interpolation block, so it is recommended to use only when the added flexibility is required.

I definitely want to support AD. That’s one of the main goals of the package I am trying to build (A PV simulator that supports AD). So I think I should stick to ParametrizedInterpolation for now?

Code for reference:

function SingleDiode(data, time)
    @named I_L = ParametrizedInterpolation(LinearInterpolation, data, time)

    @mtkmodel Single_Diode begin
        @parameters begin
            n::Float64 = 1.0, [description = "Ideality factor"]
            I_s::Float64 = 1e-6, [description = "Saturation current (A)"]
            R_sh_val::Float64 = 1e5, [description = "Shunt resistance (Ohm)"]
            R_s_val::Float64 = 1e-2, [description = "Series resistance (Ohm)"]
        end

        @components begin
            R_sh = Resistor(R = R_sh_val)
            R_s = Resistor(R = R_s_val)
            diode = HeatingDiode(n = n, Is = I_s)
            source = Current()
            temp = FixedTemperature(T = 300.0)
            pos = Pin()
            neg = Pin()
        end


        @equations begin
            # current from irradiance
            I_L.input.u ~ t
            connect(I_L.output, source.I)

            # positive side
            connect(source.n, diode.p)
            connect(source.n, R_sh.p)
            connect(source.n, R_s.p)
            connect(R_s.n, pos)

            # negative side
            connect(source.p, diode.n)
            connect(source.p, R_sh.n)
            connect(source.p, neg)

            # temperature 
            connect(temp.port, diode.port)
        end
    end

    @named sys = Single_Diode()
    sys = compose(sys, [I_L])
    structural_simplify(sys)
end

As a side note, is there a way to bring (data, time) directly into the initialization of this block? Then I could get rid of the function SingleDiode(), which is just a wrapper.

@mtkmodel Single_Diode begin
...
end

I was reading here: ModelingToolkit Language: Modeling with @mtkmodel, @connectors and @mtkbuild · ModelingToolkit.jl

And it seems that I should be able to do something like this:

SingleDiode(; I_L.data = data, I_L.time = time)

However this results in initialization errors of ParametrizedInterpolation because the arguments to it are key valued, instead of ordered.