diff --git a/docs/src/tutorials/disturbance_modeling.md b/docs/src/tutorials/disturbance_modeling.md index 598697c71c..f77fe60f41 100644 --- a/docs/src/tutorials/disturbance_modeling.md +++ b/docs/src/tutorials/disturbance_modeling.md @@ -203,11 +203,7 @@ using Test but we may also generate the functions ``f`` and ``g`` for state estimation: -!!! warning "Example currently disabled" - - This example is currently disabled due to compatibility issues with `generate_control_function` and analysis points in the current ModelingToolkit stack. - -```julia +```@example DISTURBANCE_MODELING inputs = [ssys.u] disturbance_inputs = [ssys.d1, ssys.d2] P = ssys.system_model diff --git a/lib/ModelingToolkitBase/src/systems/analysis_points.jl b/lib/ModelingToolkitBase/src/systems/analysis_points.jl index defd11aa91..ec1a723649 100644 --- a/lib/ModelingToolkitBase/src/systems/analysis_points.jl +++ b/lib/ModelingToolkitBase/src/systems/analysis_points.jl @@ -865,9 +865,11 @@ function open_loop(sys, ap::Union{Symbol, AnalysisPoint}; system_modifier = iden end """ - generate_control_function(sys::ModelingToolkitBase.AbstractSystem, input_ap_name::Union{Symbol, Vector{Symbol}, AnalysisPoint, Vector{AnalysisPoint}}, dist_ap_name::Union{Symbol, Vector{Symbol}, AnalysisPoint, Vector{AnalysisPoint}}; system_modifier = identity, kwargs) + generate_control_function(sys::ModelingToolkitBase.AbstractSystem, input_ap_name::Union{Symbol, Vector{Symbol}, AnalysisPoint, Vector{AnalysisPoint}}, dist_ap_name::Union{Symbol, Vector{Symbol}, AnalysisPoint, Vector{AnalysisPoint}}; system_modifier = identity, known_disturbance_inputs = nothing, kwargs) When called with analysis points as input arguments, we assume that all analysis points corresponds to connections that should be opened (broken). The use case for this is to get rid of input signal blocks, such as `Step` or `Sine`, since these are useful for simulation but are not needed when using the plant model in a controller or state estimator. + +The `known_disturbance_inputs` keyword accepts analysis points (or symbols) for disturbances that should be treated as known (i.e., provided as an additional function argument `w`). The loops at these analysis points are opened and the resulting variables are forwarded to the base `generate_control_function`. """ function generate_control_function( sys::ModelingToolkitBase.AbstractSystem, input_ap_name::Union{ @@ -877,6 +879,9 @@ function generate_control_function( Nothing, Symbol, Vector{Symbol}, AnalysisPoint, Vector{AnalysisPoint}, } = nothing; system_modifier = identity, + known_disturbance_inputs::Union{ + Nothing, Symbol, Vector{Symbol}, AnalysisPoint, Vector{AnalysisPoint}, + } = nothing, kwargs... ) input_ap_name = canonicalize_ap(sys, input_ap_name) @@ -885,16 +890,35 @@ function generate_control_function( sys, (du, _) = open_loop(sys, input_ap) push!(u, du) end - if dist_ap_name === nothing - return ModelingToolkitBase.generate_control_function(system_modifier(sys), u; kwargs...) + + # Open loops for known disturbance APs and collect their variables. + # Use append! (not push!) because open_loop returns a Vector{SymbolicT} + # from Break, and the base function expects a flat vector of symbolic vars + known_dist_vars = nothing + if known_disturbance_inputs !== nothing + known_disturbance_inputs = canonicalize_ap(sys, known_disturbance_inputs) + known_dist_vars = [] + for kd_ap in known_disturbance_inputs + sys, (du, _) = open_loop(sys, kd_ap) + append!(known_dist_vars, du) + end end - dist_ap_name = canonicalize_ap(sys, dist_ap_name) - d = [] - for dist_ap in dist_ap_name - sys, (du, _) = open_loop(sys, dist_ap) - push!(d, du) + # Open loops for unknown disturbance APs (positional dist_ap_name) + d = nothing + if dist_ap_name !== nothing + dist_ap_name = canonicalize_ap(sys, dist_ap_name) + d = [] + for dist_ap in dist_ap_name + sys, (du, _) = open_loop(sys, dist_ap) + push!(d, du) + end end - return ModelingToolkitBase.generate_control_function(system_modifier(sys), u, d; kwargs...) + # Always pass disturbance_inputs explicitly to prevent the base function + # from defaulting to disturbances(sys), which would overlap with variables + # we already opened and get substituted to zero. + return ModelingToolkitBase.generate_control_function( + system_modifier(sys), u, d; + known_disturbance_inputs = known_dist_vars, kwargs...) end diff --git a/test/downstream/test_disturbance_model.jl b/test/downstream/test_disturbance_model.jl index 897add70c9..7a23e918fc 100644 --- a/test/downstream/test_disturbance_model.jl +++ b/test/downstream/test_disturbance_model.jl @@ -241,6 +241,27 @@ d = [0, 0, 1] @test measurement2(x, u, p, 0.0, d) == [1] # We have now disturbed the output @test measurement3(x, u, p, 0.0, d) == [1] # Test new interface +## Test known_disturbance_inputs with analysis points (issue #4215) ================ +# This exercises the AP override's known_disturbance_inputs keyword using symbols +f_known, x_sym_known, + p_sym_known, + io_sys_known = ModelingToolkit.generate_control_function( + model_with_disturbance, [:u]; + known_disturbance_inputs = [:d1, :d2, :dy], split = false +) + +op_known = ModelingToolkit.inputs(io_sys_known) .=> 0 +x0_known = ModelingToolkit.get_u0(io_sys_known, op_known) +p_known = ModelingToolkit.get_p(io_sys_known, op_known) +x_known = zeros(length(x_sym_known)) +u_known = zeros(1) +d_known = zeros(3) +@test f_known[1](x_known, u_known, p_known, t, d_known) == zeros(length(x_sym_known)) + +# Non-zero known disturbance +d_known = [1, 0, 0] +@test sort(f_known[1](x_known, u_known, p_known, 0.0, d_known)) == [0, 0, 0, 1, 1] + ## Further downstream tests that the functions generated above actually have the properties required to use for state estimation # # using LowLevelParticleFilters, SeeToDee