From 73daad1904da5c6d44af94d6272ba6e2416777d1 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 19 Nov 2024 19:09:41 +0530 Subject: [PATCH 1/6] feat: add function to parse variable from string --- src/systems/abstractsystem.jl | 73 +++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index db2fcb4f10..9c427a2a12 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -3285,6 +3285,79 @@ function dump_unknowns(sys::AbstractSystem) end end +# syntax: +# varname = "D(" varname ")" | arrvar | maybe_dummy_var +# arrvar = maybe_dummy_var "[idxs...]" +# idxs = int | int "," idxs +# maybe_dummy_var = namespacedvar | namespacedvar "(t)" | +# namespacedvar "(t)" "ˍ" ts | namespacedvar "ˍ" ts | +# namespacedvar "ˍ" ts "(t)" +# ts = "t" | "t" ts +# namespacedvar = ident "₊" namespacedvar | ident "." namespacedvar | ident +# +# I'd write a regex to validate this, but https://xkcd.com/1171/ +function parse_variable(sys::AbstractSystem, str::AbstractString) + str = strip(str) + derivative_level = 0 + while startswith(str, "D(") && endswith(str, ")") + derivative_level += 1 + str = _string_view_inner(str, 2, 1) + end + + arr_idxs = nothing + if endswith(str, ']') + open_idx = only(findfirst('[', str)) + idxs_range = nextind(str, open_idx):prevind(str, lastindex(str)) + idxs_str = view(str, idxs_range) + str = view(str, firstindex(str):prevind(str, open_idx)) + arr_idxs = map(Base.Fix1(parse, Int), eachsplit(idxs_str, ",")) + end + + if endswith(str, "(t)") + str = _string_view_inner(str, 0, 3) + end + + dummyderivative_level = 0 + if (dd_idx = findfirst('ˍ', str)) !== nothing + t_idx = nextind(str, dd_idx) + while checkbounds(Bool, str, t_idx) + if str[t_idx] != 't' + throw(ArgumentError("Dummy derivative must be 'ˍ' followed by one or more 't'.")) + end + dummyderivative_level += 1 + t_idx = nextind(str, t_idx) + end + str = view(str, firstindex(str):prevind(str, dd_idx)) + end + + if endswith(str, "(t)") + str = _string_view_inner(str, 0, 3) + end + + cur = sys + for ident in eachsplit(str, ('.', NAMESPACE_SEPARATOR)) + ident = Symbol(ident) + hasproperty(cur, ident) || + throw(ArgumentError("System $(nameof(cur)) does not have a subsystem/variable named $(ident)")) + cur = getproperty(cur, ident) + end + + if arr_idxs !== nothing + cur = cur[arr_idxs...] + end + + for i in 1:(derivative_level + dummyderivative_level) + cur = Differential(get_iv(sys))(cur) + end + + return cur +end + +function _string_view_inner(str, startoffset, endoffset) + view(str, + nextind(str, firstindex(str), startoffset):prevind(str, lastindex(str), endoffset)) +end + ### Functions for accessing algebraic/differential equations in systems ### """ From fc13ecd8e726694042be7ca823a04941b3f6236a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 19 Nov 2024 19:23:54 +0530 Subject: [PATCH 2/6] test: add tests for `parse_variable` --- test/variable_utils.jl | 99 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/test/variable_utils.jl b/test/variable_utils.jl index d76f2f1209..100e2bc1ad 100644 --- a/test/variable_utils.jl +++ b/test/variable_utils.jl @@ -1,6 +1,7 @@ using ModelingToolkit, Test -using ModelingToolkit: value, vars +using ModelingToolkit: value, vars, parse_variable using SymbolicUtils: <ₑ + @parameters α β δ expr = (((1 / β - 1) + δ) / α)^(1 / (α - 1)) ref = sort([β, δ, α], lt = <ₑ) @@ -41,3 +42,99 @@ ts = collect_ivs([eq]) res = vars(fn([x, y], z)) @test length(res) == 3 end + +@testset "parse_variable" begin + @mtkmodel Lorenz begin + @variables begin + x(t) + y(t) + z(t) + end + @parameters begin + σ + ρ + β + end + @equations begin + D(D(x)) ~ σ * (y - x) + D(y) ~ x * (ρ - z) - y + D(z) ~ x * y - β * z + end + end + @mtkmodel ArrSys begin + @variables begin + x(t)[1:2] + end + @parameters begin + p[1:2, 1:2] + end + @equations begin + D(D(x)) ~ p * x + end + end + @mtkmodel Outer begin + @components begin + 😄 = Lorenz() + arr = ArrSys() + end + end + + @mtkbuild sys = Outer() + for (str, var) in [ + # unicode system, scalar variable + ("😄.x", sys.😄.x), + ("😄.x(t)", sys.😄.x), + ("😄₊x", sys.😄.x), + ("😄₊x(t)", sys.😄.x), + # derivative + ("D(😄.x)", D(sys.😄.x)), + ("D(😄.x(t))", D(sys.😄.x)), + ("D(😄₊x)", D(sys.😄.x)), + ("D(😄₊x(t))", D(sys.😄.x)), + # other derivative + ("😄.xˍt", D(sys.😄.x)), + ("😄.x(t)ˍt", D(sys.😄.x)), + ("😄₊xˍt", D(sys.😄.x)), + ("😄₊x(t)ˍt", D(sys.😄.x)), + # scalar parameter + ("😄.σ", sys.😄.σ), + ("😄₊σ", sys.😄.σ), + # array variable + ("arr.x", sys.arr.x), + ("arr₊x", sys.arr.x), + ("arr.x(t)", sys.arr.x), + ("arr₊x(t)", sys.arr.x), + # getindex + ("arr.x[1]", sys.arr.x[1]), + ("arr₊x[1]", sys.arr.x[1]), + ("arr.x(t)[1]", sys.arr.x[1]), + ("arr₊x(t)[1]", sys.arr.x[1]), + # derivative + ("D(arr.x(t))", D(sys.arr.x)), + ("D(arr₊x(t))", D(sys.arr.x)), + ("D(arr.x[1])", D(sys.arr.x[1])), + ("D(arr₊x[1])", D(sys.arr.x[1])), + ("D(arr.x(t)[1])", D(sys.arr.x[1])), + ("D(arr₊x(t)[1])", D(sys.arr.x[1])), + # other derivative + ("arr.xˍt", D(sys.arr.x)), + ("arr₊xˍt", D(sys.arr.x)), + ("arr.xˍt(t)", D(sys.arr.x)), + ("arr₊xˍt(t)", D(sys.arr.x)), + ("arr.xˍt[1]", D(sys.arr.x[1])), + ("arr₊xˍt[1]", D(sys.arr.x[1])), + ("arr.xˍt(t)[1]", D(sys.arr.x[1])), + ("arr₊xˍt(t)[1]", D(sys.arr.x[1])), + ("arr.x(t)ˍt", D(sys.arr.x)), + ("arr₊x(t)ˍt", D(sys.arr.x)), + ("arr.x(t)ˍt[1]", D(sys.arr.x[1])), + ("arr₊x(t)ˍt[1]", D(sys.arr.x[1])), + # array parameter + ("arr.p", sys.arr.p), + ("arr₊p", sys.arr.p), + ("arr.p[1, 2]", sys.arr.p[1, 2]), + ("arr₊p[1, 2]", sys.arr.p[1, 2]) + ] + isequal(parse_variable(sys, str), var) + end +end From 8712f991b7096d8715e131a9edf1c72fdf9b8ac2 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 19 Nov 2024 19:26:30 +0530 Subject: [PATCH 3/6] docs: add docstring for `parse_variable` --- src/systems/abstractsystem.jl | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 9c427a2a12..ba78a40a58 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -3285,18 +3285,25 @@ function dump_unknowns(sys::AbstractSystem) end end -# syntax: -# varname = "D(" varname ")" | arrvar | maybe_dummy_var -# arrvar = maybe_dummy_var "[idxs...]" -# idxs = int | int "," idxs -# maybe_dummy_var = namespacedvar | namespacedvar "(t)" | -# namespacedvar "(t)" "ˍ" ts | namespacedvar "ˍ" ts | -# namespacedvar "ˍ" ts "(t)" -# ts = "t" | "t" ts -# namespacedvar = ident "₊" namespacedvar | ident "." namespacedvar | ident -# -# I'd write a regex to validate this, but https://xkcd.com/1171/ +""" + $(TYPEDSIGNATURES) + +Return the variable in `sys` referred to by its string representation `str`. +Roughly supports the following CFG: + +``` +varname = "D(" varname ")" | arrvar | maybe_dummy_var +arrvar = maybe_dummy_var "[idxs...]" +idxs = int | int "," idxs +maybe_dummy_var = namespacedvar | namespacedvar "(t)" | + namespacedvar "(t)" "ˍ" ts | namespacedvar "ˍ" ts | + namespacedvar "ˍ" ts "(t)" +ts = "t" | "t" ts +namespacedvar = ident "₊" namespacedvar | ident "." namespacedvar | ident +``` +""" function parse_variable(sys::AbstractSystem, str::AbstractString) + # I'd write a regex to validate `str`, but https://xkcd.com/1171/ str = strip(str) derivative_level = 0 while startswith(str, "D(") && endswith(str, ")") From 3c728a399b8edf733d7be568750c32b68d828d09 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 20 Nov 2024 11:53:25 +0530 Subject: [PATCH 4/6] docs: add `parse_variable` to documentation --- docs/src/basics/FAQ.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/src/basics/FAQ.md b/docs/src/basics/FAQ.md index bb3d01fc30..10671299c6 100644 --- a/docs/src/basics/FAQ.md +++ b/docs/src/basics/FAQ.md @@ -82,6 +82,16 @@ parameter_index(sys, sym) Note that while the variable index will be an integer, the parameter index is a struct of type `ParameterIndex` whose internals should not be relied upon. +## Can I index with strings? + +Strings are not considered symbolic variables, and thus cannot directly be used for symbolic +indexing. However, ModelingToolkit does provide a method to parse the string representation of +a variable, given the system in which that variable exists. + +```@docs +ModelingToolkit.parse_variable +``` + ## Transforming value maps to arrays ModelingToolkit.jl allows (and recommends) input maps like `[x => 2.0, y => 3.0]` From afd55cbe3f197836a438163fba084e06bc9cc623 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 20 Nov 2024 11:53:57 +0530 Subject: [PATCH 5/6] feat: support arbitrary independent variables in `parse_variable` --- src/systems/abstractsystem.jl | 31 ++++++------ test/variable_utils.jl | 95 +++++++++++++++++------------------ 2 files changed, 62 insertions(+), 64 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index ba78a40a58..36bd5ac800 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -3295,14 +3295,18 @@ Roughly supports the following CFG: varname = "D(" varname ")" | arrvar | maybe_dummy_var arrvar = maybe_dummy_var "[idxs...]" idxs = int | int "," idxs -maybe_dummy_var = namespacedvar | namespacedvar "(t)" | - namespacedvar "(t)" "ˍ" ts | namespacedvar "ˍ" ts | - namespacedvar "ˍ" ts "(t)" -ts = "t" | "t" ts +maybe_dummy_var = namespacedvar | namespacedvar "(" iv ")" | + namespacedvar "(" iv ")" "ˍ" ts | namespacedvar "ˍ" ts | + namespacedvar "ˍ" ts "(" iv ")" +ts = iv | iv ts namespacedvar = ident "₊" namespacedvar | ident "." namespacedvar | ident ``` + +Where `iv` is the independent variable, `int` is an integer and `ident` is an identifier. """ function parse_variable(sys::AbstractSystem, str::AbstractString) + iv = has_iv(sys) ? string(getname(get_iv(sys))) : nothing + # I'd write a regex to validate `str`, but https://xkcd.com/1171/ str = strip(str) derivative_level = 0 @@ -3320,25 +3324,22 @@ function parse_variable(sys::AbstractSystem, str::AbstractString) arr_idxs = map(Base.Fix1(parse, Int), eachsplit(idxs_str, ",")) end - if endswith(str, "(t)") - str = _string_view_inner(str, 0, 3) + if iv !== nothing && endswith(str, "($iv)") + str = _string_view_inner(str, 0, 2 + length(iv)) end dummyderivative_level = 0 - if (dd_idx = findfirst('ˍ', str)) !== nothing - t_idx = nextind(str, dd_idx) - while checkbounds(Bool, str, t_idx) - if str[t_idx] != 't' - throw(ArgumentError("Dummy derivative must be 'ˍ' followed by one or more 't'.")) - end + if iv !== nothing && (dd_idx = findfirst('ˍ', str)) !== nothing + t_idx = findnext(iv, str, dd_idx) + while t_idx !== nothing dummyderivative_level += 1 - t_idx = nextind(str, t_idx) + t_idx = findnext(iv, str, nextind(str, last(t_idx))) end str = view(str, firstindex(str):prevind(str, dd_idx)) end - if endswith(str, "(t)") - str = _string_view_inner(str, 0, 3) + if iv !== nothing && endswith(str, "($iv)") + str = _string_view_inner(str, 0, 2 + length(iv)) end cur = sys diff --git a/test/variable_utils.jl b/test/variable_utils.jl index 100e2bc1ad..ebbc3c0d3b 100644 --- a/test/variable_utils.jl +++ b/test/variable_utils.jl @@ -43,98 +43,95 @@ ts = collect_ivs([eq]) @test length(res) == 3 end -@testset "parse_variable" begin - @mtkmodel Lorenz begin +@testset "parse_variable with iv: $iv" for iv in [t, only(@independent_variables tt)] + D = Differential(iv) + function Lorenz(; name) @variables begin - x(t) - y(t) - z(t) + x(iv) + y(iv) + z(iv) end @parameters begin σ ρ β end - @equations begin - D(D(x)) ~ σ * (y - x) - D(y) ~ x * (ρ - z) - y - D(z) ~ x * y - β * z - end + sys = ODESystem( + [D(D(x)) ~ σ * (y - x) + D(y) ~ x * (ρ - z) - y + D(z) ~ x * y - β * z], iv; name) end - @mtkmodel ArrSys begin + function ArrSys(; name) @variables begin - x(t)[1:2] + x(iv)[1:2] end @parameters begin p[1:2, 1:2] end - @equations begin - D(D(x)) ~ p * x - end + sys = ODESystem([D(D(x)) ~ p * x], iv; name) end - @mtkmodel Outer begin - @components begin - 😄 = Lorenz() - arr = ArrSys() - end + function Outer(; name) + @named 😄 = Lorenz() + @named arr = ArrSys() + sys = ODESystem(Equation[], iv; name, systems = [😄, arr]) end @mtkbuild sys = Outer() for (str, var) in [ # unicode system, scalar variable ("😄.x", sys.😄.x), - ("😄.x(t)", sys.😄.x), + ("😄.x($iv)", sys.😄.x), ("😄₊x", sys.😄.x), - ("😄₊x(t)", sys.😄.x), + ("😄₊x($iv)", sys.😄.x), # derivative ("D(😄.x)", D(sys.😄.x)), - ("D(😄.x(t))", D(sys.😄.x)), + ("D(😄.x($iv))", D(sys.😄.x)), ("D(😄₊x)", D(sys.😄.x)), - ("D(😄₊x(t))", D(sys.😄.x)), + ("D(😄₊x($iv))", D(sys.😄.x)), # other derivative - ("😄.xˍt", D(sys.😄.x)), - ("😄.x(t)ˍt", D(sys.😄.x)), - ("😄₊xˍt", D(sys.😄.x)), - ("😄₊x(t)ˍt", D(sys.😄.x)), + ("😄.xˍ$iv", D(sys.😄.x)), + ("😄.x($iv)ˍ$iv", D(sys.😄.x)), + ("😄₊xˍ$iv", D(sys.😄.x)), + ("😄₊x($iv)ˍ$iv", D(sys.😄.x)), # scalar parameter ("😄.σ", sys.😄.σ), ("😄₊σ", sys.😄.σ), # array variable ("arr.x", sys.arr.x), ("arr₊x", sys.arr.x), - ("arr.x(t)", sys.arr.x), - ("arr₊x(t)", sys.arr.x), + ("arr.x($iv)", sys.arr.x), + ("arr₊x($iv)", sys.arr.x), # getindex ("arr.x[1]", sys.arr.x[1]), ("arr₊x[1]", sys.arr.x[1]), - ("arr.x(t)[1]", sys.arr.x[1]), - ("arr₊x(t)[1]", sys.arr.x[1]), + ("arr.x($iv)[1]", sys.arr.x[1]), + ("arr₊x($iv)[1]", sys.arr.x[1]), # derivative - ("D(arr.x(t))", D(sys.arr.x)), - ("D(arr₊x(t))", D(sys.arr.x)), + ("D(arr.x($iv))", D(sys.arr.x)), + ("D(arr₊x($iv))", D(sys.arr.x)), ("D(arr.x[1])", D(sys.arr.x[1])), ("D(arr₊x[1])", D(sys.arr.x[1])), - ("D(arr.x(t)[1])", D(sys.arr.x[1])), - ("D(arr₊x(t)[1])", D(sys.arr.x[1])), + ("D(arr.x($iv)[1])", D(sys.arr.x[1])), + ("D(arr₊x($iv)[1])", D(sys.arr.x[1])), # other derivative - ("arr.xˍt", D(sys.arr.x)), - ("arr₊xˍt", D(sys.arr.x)), - ("arr.xˍt(t)", D(sys.arr.x)), - ("arr₊xˍt(t)", D(sys.arr.x)), - ("arr.xˍt[1]", D(sys.arr.x[1])), - ("arr₊xˍt[1]", D(sys.arr.x[1])), - ("arr.xˍt(t)[1]", D(sys.arr.x[1])), - ("arr₊xˍt(t)[1]", D(sys.arr.x[1])), - ("arr.x(t)ˍt", D(sys.arr.x)), - ("arr₊x(t)ˍt", D(sys.arr.x)), - ("arr.x(t)ˍt[1]", D(sys.arr.x[1])), - ("arr₊x(t)ˍt[1]", D(sys.arr.x[1])), + ("arr.xˍ$iv", D(sys.arr.x)), + ("arr₊xˍ$iv", D(sys.arr.x)), + ("arr.xˍ$iv($iv)", D(sys.arr.x)), + ("arr₊xˍ$iv($iv)", D(sys.arr.x)), + ("arr.xˍ$iv[1]", D(sys.arr.x[1])), + ("arr₊xˍ$iv[1]", D(sys.arr.x[1])), + ("arr.xˍ$iv($iv)[1]", D(sys.arr.x[1])), + ("arr₊xˍ$iv($iv)[1]", D(sys.arr.x[1])), + ("arr.x($iv)ˍ$iv", D(sys.arr.x)), + ("arr₊x($iv)ˍ$iv", D(sys.arr.x)), + ("arr.x($iv)ˍ$iv[1]", D(sys.arr.x[1])), + ("arr₊x($iv)ˍ$iv[1]", D(sys.arr.x[1])), # array parameter ("arr.p", sys.arr.p), ("arr₊p", sys.arr.p), ("arr.p[1, 2]", sys.arr.p[1, 2]), ("arr₊p[1, 2]", sys.arr.p[1, 2]) ] - isequal(parse_variable(sys, str), var) + @test isequal(parse_variable(sys, str), var) end end From 2186298df1c35bffe38ea34be5ebcd097441486f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 20 Nov 2024 18:24:42 +0530 Subject: [PATCH 6/6] feat: support `Differential(t)` syntax in `parse_variable` --- src/systems/abstractsystem.jl | 15 ++++++++++++--- test/variable_utils.jl | 10 ++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 36bd5ac800..37a7a74f2d 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -3292,7 +3292,7 @@ Return the variable in `sys` referred to by its string representation `str`. Roughly supports the following CFG: ``` -varname = "D(" varname ")" | arrvar | maybe_dummy_var +varname = "D(" varname ")" | "Differential(" iv ")(" varname ")" | arrvar | maybe_dummy_var arrvar = maybe_dummy_var "[idxs...]" idxs = int | int "," idxs maybe_dummy_var = namespacedvar | namespacedvar "(" iv ")" | @@ -3310,9 +3310,18 @@ function parse_variable(sys::AbstractSystem, str::AbstractString) # I'd write a regex to validate `str`, but https://xkcd.com/1171/ str = strip(str) derivative_level = 0 - while startswith(str, "D(") && endswith(str, ")") + while ((cond1 = startswith(str, "D(")) || startswith(str, "Differential(")) && endswith(str, ")") + if cond1 + derivative_level += 1 + str = _string_view_inner(str, 2, 1) + continue + end + _tmpstr = _string_view_inner(str, 13, 1) + if !startswith(_tmpstr, "$iv)(") + throw(ArgumentError("Expected differential with respect to independent variable $iv in $str")) + end derivative_level += 1 - str = _string_view_inner(str, 2, 1) + str = _string_view_inner(_tmpstr, length(iv) + 2, 0) end arr_idxs = nothing diff --git a/test/variable_utils.jl b/test/variable_utils.jl index ebbc3c0d3b..ecc2421955 100644 --- a/test/variable_utils.jl +++ b/test/variable_utils.jl @@ -88,6 +88,10 @@ end ("D(😄.x($iv))", D(sys.😄.x)), ("D(😄₊x)", D(sys.😄.x)), ("D(😄₊x($iv))", D(sys.😄.x)), + ("Differential($iv)(😄.x)", D(sys.😄.x)), + ("Differential($iv)(😄.x($iv))", D(sys.😄.x)), + ("Differential($iv)(😄₊x)", D(sys.😄.x)), + ("Differential($iv)(😄₊x($iv))", D(sys.😄.x)), # other derivative ("😄.xˍ$iv", D(sys.😄.x)), ("😄.x($iv)ˍ$iv", D(sys.😄.x)), @@ -113,6 +117,12 @@ end ("D(arr₊x[1])", D(sys.arr.x[1])), ("D(arr.x($iv)[1])", D(sys.arr.x[1])), ("D(arr₊x($iv)[1])", D(sys.arr.x[1])), + ("Differential($iv)(arr.x($iv))", D(sys.arr.x)), + ("Differential($iv)(arr₊x($iv))", D(sys.arr.x)), + ("Differential($iv)(arr.x[1])", D(sys.arr.x[1])), + ("Differential($iv)(arr₊x[1])", D(sys.arr.x[1])), + ("Differential($iv)(arr.x($iv)[1])", D(sys.arr.x[1])), + ("Differential($iv)(arr₊x($iv)[1])", D(sys.arr.x[1])), # other derivative ("arr.xˍ$iv", D(sys.arr.x)), ("arr₊xˍ$iv", D(sys.arr.x)),