-
-
Notifications
You must be signed in to change notification settings - Fork 233
Support for Inputs Feature #3998
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
d2c10ea
2d088e6
f2d80da
f49800b
4ff3759
41e262a
c2050d3
85e13e5
d51854f
ac9d232
9f04501
9301b01
3c003c9
0c2dd02
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| """ | ||
| Input(var, data::Vector{<:Real}, time::Vector{<:Real}) | ||
| Create an `Input` object that specifies predetermined input values for a variable at specific time points. | ||
| # Arguments | ||
| - `var`: The symbolic variable (marked with `[input=true]` metadata) to be used as an input. | ||
| - `data`: A vector of real values that the input variable should take at the corresponding time points. | ||
| - `time`: A vector of time points at which the input values should be applied. Must be the same length as `data`. | ||
| # Description | ||
| The `Input` struct is used with the extended `solve` method to provide time-varying inputs to a system | ||
| during simulation. When passed to `solve(prob, [input1, input2, ...], alg)`, the solver will automatically | ||
| set the input variable to the specified values at the specified times using discrete callbacks. | ||
| This provides a "determinate form" of input handling where all input values are known a priori, | ||
| as opposed to setting inputs manually during integration with [`set_input!`](@ref). | ||
| See also [`set_input!`](@ref), [`finalize!`](@ref) | ||
| """ | ||
| struct Input | ||
| var::Num | ||
| data::SVector | ||
| time::SVector | ||
|
Comment on lines
+23
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why specifically |
||
| end | ||
|
|
||
| function Input(var, data::Vector{<:Real}, time::Vector{<:Real}) | ||
| n = length(data) | ||
| return Input(var, SVector{n}(data), SVector{n}(time)) | ||
|
Comment on lines
+28
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is type-unstable. |
||
| end | ||
|
|
||
| struct InputFunctions{S, O} | ||
| events::Tuple{SymbolicDiscreteCallback} | ||
| vars::Tuple{SymbolicUtils.BasicSymbolic{Real}} | ||
|
Comment on lines
+33
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not concrete There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not fixed. Tuples are only concrete if you also choose a size for them There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fairly minor, but |
||
| setters::Tuple{SymbolicIndexingInterface.ParameterHookWrapper{S, O}} | ||
| end | ||
|
|
||
| function InputFunctions(events::Vector, vars::Vector, setters::Vector) | ||
| InputFunctions(Tuple(events), Tuple(vars), Tuple(setters)) | ||
| end | ||
|
|
||
| """ | ||
| set_input!(integrator, var, value::Real) | ||
| Set the value of an input variable during integration. | ||
| # Arguments | ||
| - `integrator`: An ODE integrator object (from `init(prob, alg)` or available in callbacks). | ||
| - `var`: The symbolic input variable to set (must be marked with `[input=true]` metadata and included in the `inputs` keyword of `@mtkcompile`). | ||
| - `value`: The new real-valued input to assign to the variable. | ||
| - `input_funs` (optional): The `InputFunctions` object associated with the system. If not provided, it will be retrieved from `integrator.f.sys`. | ||
| # Description | ||
| This function allows you to manually set input values during integration, providing an "indeterminate form" | ||
| of input handling where inputs can be computed on-the-fly. This is useful when input values depend on | ||
| runtime conditions, external data sources, or interactive user input. | ||
| After setting input values with `set_input!`, you must call [`finalize!`](@ref) at the end of integration | ||
| to ensure all discrete callbacks are properly saved. | ||
| # Example | ||
| ```julia | ||
| @variables x(t) [input=true] | ||
| @variables y(t) = 0 | ||
| eqs = [D(y) ~ x] | ||
| @mtkcompile sys = System(eqs, t, [x, y], []) inputs=[x] | ||
| prob = ODEProblem(sys, [], (0, 4)) | ||
| integrator = init(prob, Tsit5()) | ||
| # Set input and step forward | ||
| set_input!(integrator, sys.x, 1.0) | ||
| step!(integrator, 1.0, true) | ||
| set_input!(integrator, sys.x, 2.0) | ||
| step!(integrator, 1.0, true) | ||
| # Must call finalize! at the end | ||
| finalize!(integrator) | ||
| ``` | ||
| See also [`finalize!`](@ref), [`Input`](@ref) | ||
| """ | ||
| function set_input!(input_funs::InputFunctions, integrator::OrdinaryDiffEqCore.ODEIntegrator, var, value::Real) | ||
| i = findfirst(isequal(var), input_funs.vars) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't quite make sense. Why |
||
| setter = input_funs.setters[i] | ||
| event = input_funs.events[i] | ||
|
|
||
| setter(integrator, value) | ||
| save_callback_discretes!(integrator, event) | ||
| u_modified!(integrator, true) | ||
| return nothing | ||
| end | ||
| function set_input!(integrator, var, value::Real) | ||
| set_input!(get_input_functions(integrator.f.sys), integrator, var, value) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Accessing |
||
| end | ||
|
|
||
| """ | ||
| finalize!(integrator) | ||
| Finalize all input callbacks at the end of integration. | ||
| # Arguments | ||
| - `integrator`: An ODE integrator object (from `init(prob, alg)` or available in callbacks). | ||
| - `input_funs` (optional): The `InputFunctions` object associated with the system. If not provided, it will be retrieved from `integrator.f.sys`. | ||
| # Description | ||
| This function must be called after using [`set_input!`](@ref) to manually set input values during integration. | ||
| It ensures that all discrete callbacks associated with input variables are properly saved in the solution, | ||
| making the input values accessible when querying the solution at specific time points. | ||
| Without calling `finalize!`, input values set with `set_input!` may not be correctly recorded in the | ||
| final solution object, leading to incorrect results when indexing the solution. | ||
| See also [`set_input!`](@ref), [`Input`](@ref) | ||
| """ | ||
| function finalize!(input_funs::InputFunctions, integrator) | ||
| for i in eachindex(input_funs.vars) | ||
| save_callback_discretes!(integrator, input_funs.events[i]) | ||
| end | ||
|
|
||
| return nothing | ||
| end | ||
| finalize!(integrator) = finalize!(get_input_functions(integrator.f.sys), integrator) | ||
|
|
||
| function (input_funs::InputFunctions)(integrator, var, value::Real) | ||
| set_input!(input_funs, integrator, var, value) | ||
| end | ||
| (input_funs::InputFunctions)(integrator) = finalize!(input_funs, integrator) | ||
|
|
||
| function build_input_functions(sys, inputs) | ||
|
|
||
| # Here we ensure the inputs have metadata marking the discrete variables as parameters. In some | ||
| # cases the inputs can be fed to this function before they are converted to parameters by mtkcompile. | ||
| vars = SymbolicUtils.BasicSymbolic[isparameter(x) ? x : toparam(x) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think |
||
| for x in unwrap.(inputs)] | ||
| setters = [] | ||
| events = SymbolicDiscreteCallback[] | ||
| defaults = get_defaults(sys) | ||
| if !isempty(vars) | ||
| for x in vars | ||
| affect = ImperativeAffect((m, o, c, i)->m, modified = (; x)) | ||
| sdc = SymbolicDiscreteCallback(Inf, affect) | ||
|
|
||
| push!(events, sdc) | ||
|
|
||
| # ensure that the ODEProblem does not complain about missing parameter map | ||
| if !haskey(defaults, x) | ||
| push!(defaults, x => 0.0) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is mutating the defaults stored in |
||
| end | ||
| end | ||
|
|
||
| @set! sys.discrete_events = events | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would overwrite any pre-existing discrete events in the system. |
||
| @set! sys.index_cache = ModelingToolkit.IndexCache(sys) | ||
| @set! sys.defaults = defaults | ||
|
|
||
| setters = [SymbolicIndexingInterface.setsym(sys, x) for x in vars] | ||
|
|
||
| @set! sys.input_functions = InputFunctions(events, vars, setters) | ||
| end | ||
|
|
||
| return sys | ||
| end | ||
|
|
||
| function CommonSolve.solve(prob::SciMLBase.AbstractDEProblem, inputs::Vector{Input}, args...; kwargs...) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It still violates the dispatch contract that SciML does. The second argument should be the algorithm, if we do this. But I don't think it makes sense to do this as a solve, it violates a few other principles of solve There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I want to keep the data fully separate from the |
||
| tstops = Float64[] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not guaranteed that the time is |
||
| callbacks = DiscreteCallback[] | ||
|
|
||
| # set_input! | ||
| for input::Input in inputs | ||
| tstops = union(tstops, input.time) | ||
| condition = (u, t, integrator) -> any(t .== input.time) | ||
| affect! = function (integrator) | ||
| @inbounds begin | ||
| i = findfirst(integrator.t .== input.time) | ||
| set_input!(integrator, input.var, input.data[i]) | ||
| end | ||
| end | ||
| push!(callbacks, DiscreteCallback(condition, affect!)) | ||
|
|
||
| # DiscreteCallback doesn't hit on t==0, workaround... | ||
| if input.time[1] == 0 | ||
| prob.ps[input.var] = input.data[1] | ||
| end | ||
| end | ||
|
Comment on lines
+171
to
+186
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This shouldn't be done here. This should instead be done by registering discontinuities for the interpolation function if it is a discontinuous one. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no interpolation here, this is explicitly a discrete input. The point is to not have interpolation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is ConstantInterpolation |
||
|
|
||
| # finalize! | ||
| t_end = prob.tspan[2] | ||
| condition = (u, t, integrator) -> (t == t_end) | ||
| affect! = (integrator) -> finalize!(integrator) | ||
| push!(callbacks, DiscreteCallback(condition, affect!)) | ||
| push!(tstops, t_end) | ||
|
|
||
| return solve(prob, args...; tstops, callback = CallbackSet(callbacks...), kwargs...) | ||
| end | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this actually required? The inputs are specified
anyway. No other input-using functionality, like linearization or generation of input-dependent functions, rely on the
inputmetadata