From 82198b7f49cb707b31b06af6d49dd0a7ecd33160 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sat, 30 Nov 2024 14:09:11 +0530 Subject: [PATCH 1/7] refactor: move HomtopyContinuation-related code to its own file --- src/ModelingToolkit.jl | 1 + .../nonlinear/homotopy_continuation.jl | 68 ++++++++++++++++++ src/systems/nonlinear/nonlinearsystem.jl | 69 ------------------- 3 files changed, 69 insertions(+), 69 deletions(-) create mode 100644 src/systems/nonlinear/homotopy_continuation.jl diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 59be358349..de9a06d774 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -150,6 +150,7 @@ include("systems/callbacks.jl") include("systems/problem_utils.jl") include("systems/nonlinear/nonlinearsystem.jl") +include("systems/nonlinear/homotopy_continuation.jl") include("systems/diffeqs/odesystem.jl") include("systems/diffeqs/sdesystem.jl") include("systems/diffeqs/abstractodesystem.jl") diff --git a/src/systems/nonlinear/homotopy_continuation.jl b/src/systems/nonlinear/homotopy_continuation.jl new file mode 100644 index 0000000000..e41a3f0ce7 --- /dev/null +++ b/src/systems/nonlinear/homotopy_continuation.jl @@ -0,0 +1,68 @@ +""" +$(TYPEDEF) + +A type of Nonlinear problem which specializes on polynomial systems and uses +HomotopyContinuation.jl to solve the system. Requires importing HomotopyContinuation.jl to +create and solve. +""" +struct HomotopyContinuationProblem{uType, H, D, O, SS, U} <: + SciMLBase.AbstractNonlinearProblem{uType, true} + """ + The initial values of states in the system. If there are multiple real roots of + the system, the one closest to this point is returned. + """ + u0::uType + """ + A subtype of `HomotopyContinuation.AbstractSystem` to solve. Also contains the + parameter object. + """ + homotopy_continuation_system::H + """ + A function with signature `(u, p) -> resid`. In case of rational functions, this + is used to rule out roots of the system which would cause the denominator to be + zero. + """ + denominator::D + """ + The `NonlinearSystem` used to create this problem. Used for symbolic indexing. + """ + sys::NonlinearSystem + """ + A function which generates and returns observed expressions for the given system. + """ + obsfn::O + """ + The HomotopyContinuation.jl solver and start system, obtained through + `HomotopyContinuation.solver_startsystems`. + """ + solver_and_starts::SS + """ + A function which takes a solution of the transformed system, and returns a vector + of solutions for the original system. This is utilized when converting systems + to polynomials. + """ + unpack_solution::U +end + +function HomotopyContinuationProblem(::AbstractSystem, _u0, _p; kwargs...) + error("HomotopyContinuation.jl is required to create and solve `HomotopyContinuationProblem`s. Please run `Pkg.add(\"HomotopyContinuation\")` to continue.") +end + +SymbolicIndexingInterface.symbolic_container(p::HomotopyContinuationProblem) = p.sys +SymbolicIndexingInterface.state_values(p::HomotopyContinuationProblem) = p.u0 +function SymbolicIndexingInterface.set_state!(p::HomotopyContinuationProblem, args...) + set_state!(p.u0, args...) +end +function SymbolicIndexingInterface.parameter_values(p::HomotopyContinuationProblem) + parameter_values(p.homotopy_continuation_system) +end +function SymbolicIndexingInterface.set_parameter!(p::HomotopyContinuationProblem, args...) + set_parameter!(parameter_values(p), args...) +end +function SymbolicIndexingInterface.observed(p::HomotopyContinuationProblem, sym) + if p.obsfn !== nothing + return p.obsfn(sym) + else + return SymbolicIndexingInterface.observed(p.sys, sym) + end +end diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 7826e06f76..ff4a7d0eb2 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -866,72 +866,3 @@ function Base.:(==)(sys1::NonlinearSystem, sys2::NonlinearSystem) _eq_unordered(get_ps(sys1), get_ps(sys2)) && all(s1 == s2 for (s1, s2) in zip(get_systems(sys1), get_systems(sys2))) end - -""" -$(TYPEDEF) - -A type of Nonlinear problem which specializes on polynomial systems and uses -HomotopyContinuation.jl to solve the system. Requires importing HomotopyContinuation.jl to -create and solve. -""" -struct HomotopyContinuationProblem{uType, H, D, O, SS, U} <: - SciMLBase.AbstractNonlinearProblem{uType, true} - """ - The initial values of states in the system. If there are multiple real roots of - the system, the one closest to this point is returned. - """ - u0::uType - """ - A subtype of `HomotopyContinuation.AbstractSystem` to solve. Also contains the - parameter object. - """ - homotopy_continuation_system::H - """ - A function with signature `(u, p) -> resid`. In case of rational functions, this - is used to rule out roots of the system which would cause the denominator to be - zero. - """ - denominator::D - """ - The `NonlinearSystem` used to create this problem. Used for symbolic indexing. - """ - sys::NonlinearSystem - """ - A function which generates and returns observed expressions for the given system. - """ - obsfn::O - """ - The HomotopyContinuation.jl solver and start system, obtained through - `HomotopyContinuation.solver_startsystems`. - """ - solver_and_starts::SS - """ - A function which takes a solution of the transformed system, and returns a vector - of solutions for the original system. This is utilized when converting systems - to polynomials. - """ - unpack_solution::U -end - -function HomotopyContinuationProblem(::AbstractSystem, _u0, _p; kwargs...) - error("HomotopyContinuation.jl is required to create and solve `HomotopyContinuationProblem`s. Please run `Pkg.add(\"HomotopyContinuation\")` to continue.") -end - -SymbolicIndexingInterface.symbolic_container(p::HomotopyContinuationProblem) = p.sys -SymbolicIndexingInterface.state_values(p::HomotopyContinuationProblem) = p.u0 -function SymbolicIndexingInterface.set_state!(p::HomotopyContinuationProblem, args...) - set_state!(p.u0, args...) -end -function SymbolicIndexingInterface.parameter_values(p::HomotopyContinuationProblem) - parameter_values(p.homotopy_continuation_system) -end -function SymbolicIndexingInterface.set_parameter!(p::HomotopyContinuationProblem, args...) - set_parameter!(parameter_values(p), args...) -end -function SymbolicIndexingInterface.observed(p::HomotopyContinuationProblem, sym) - if p.obsfn !== nothing - return p.obsfn(sym) - else - return SymbolicIndexingInterface.observed(p.sys, sym) - end -end From 2401aa22fc2ef382589efb57e75bede4a8ed14a3 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sat, 30 Nov 2024 16:08:38 +0530 Subject: [PATCH 2/7] refactor: move HomotopyContinuation internals to MTK --- ext/MTKHomotopyContinuationExt.jl | 355 +------------- .../nonlinear/homotopy_continuation.jl | 437 ++++++++++++++++++ 2 files changed, 458 insertions(+), 334 deletions(-) diff --git a/ext/MTKHomotopyContinuationExt.jl b/ext/MTKHomotopyContinuationExt.jl index fa5d1bfcd4..586d608156 100644 --- a/ext/MTKHomotopyContinuationExt.jl +++ b/ext/MTKHomotopyContinuationExt.jl @@ -11,217 +11,6 @@ using ModelingToolkit: iscomplete, parameters, has_index_cache, get_index_cache, const MTK = ModelingToolkit -function contains_variable(x, wrt) - any(y -> occursin(y, x), wrt) -end - -""" -Possible reasons why a term is not polynomial -""" -MTK.EnumX.@enumx NonPolynomialReason begin - NonIntegerExponent - ExponentContainsUnknowns - BaseNotPolynomial - UnrecognizedOperation -end - -function display_reason(reason::NonPolynomialReason.T, sym) - if reason == NonPolynomialReason.NonIntegerExponent - pow = arguments(sym)[2] - "In $sym: Exponent $pow is not an integer" - elseif reason == NonPolynomialReason.ExponentContainsUnknowns - pow = arguments(sym)[2] - "In $sym: Exponent $pow contains unknowns of the system" - elseif reason == NonPolynomialReason.BaseNotPolynomial - base = arguments(sym)[1] - "In $sym: Base $base is not a polynomial in the unknowns" - elseif reason == NonPolynomialReason.UnrecognizedOperation - op = operation(sym) - """ - In $sym: Operation $op is not recognized. Allowed polynomial operations are \ - `*, /, +, -, ^`. - """ - else - error("This should never happen. Please open an issue in ModelingToolkit.jl.") - end -end - -mutable struct PolynomialData - non_polynomial_terms::Vector{BasicSymbolic} - reasons::Vector{NonPolynomialReason.T} - has_parametric_exponent::Bool -end - -PolynomialData() = PolynomialData(BasicSymbolic[], NonPolynomialReason.T[], false) - -abstract type PolynomialTransformationError <: Exception end - -struct MultivarTerm <: PolynomialTransformationError - term::Any - vars::Any -end - -function Base.showerror(io::IO, err::MultivarTerm) - println(io, - "Cannot convert system to polynomial: Found term $(err.term) which is a function of multiple unknowns $(err.vars).") -end - -struct MultipleTermsOfSameVar <: PolynomialTransformationError - terms::Any - var::Any -end - -function Base.showerror(io::IO, err::MultipleTermsOfSameVar) - println(io, - "Cannot convert system to polynomial: Found multiple non-polynomial terms $(err.terms) involving the same unknown $(err.var).") -end - -struct SymbolicSolveFailure <: PolynomialTransformationError - term::Any - var::Any -end - -function Base.showerror(io::IO, err::SymbolicSolveFailure) - println(io, - "Cannot convert system to polynomial: Unable to symbolically solve $(err.term) for $(err.var).") -end - -struct NemoNotLoaded <: PolynomialTransformationError end - -function Base.showerror(io::IO, err::NemoNotLoaded) - println(io, - "ModelingToolkit may be able to solve this system as a polynomial system if `Nemo` is loaded. Run `import Nemo` and try again.") -end - -struct VariablesAsPolyAndNonPoly <: PolynomialTransformationError - vars::Any -end - -function Base.showerror(io::IO, err::VariablesAsPolyAndNonPoly) - println(io, - "Cannot convert convert system to polynomial: Variables $(err.vars) occur in both polynomial and non-polynomial terms in the system.") -end - -struct NotPolynomialError <: Exception - transformation_err::Union{PolynomialTransformationError, Nothing} - eq::Vector{Equation} - data::Vector{PolynomialData} -end - -function Base.showerror(io::IO, err::NotPolynomialError) - if err.transformation_err !== nothing - Base.showerror(io, err.transformation_err) - end - for (eq, data) in zip(err.eq, err.data) - if isempty(data.non_polynomial_terms) - continue - end - println(io, - "Equation $(eq) is not a polynomial in the unknowns for the following reasons:") - for (term, reason) in zip(data.non_polynomial_terms, data.reasons) - println(io, display_reason(reason, term)) - end - end -end - -function is_polynomial!(data, y, wrt) - process_polynomial!(data, y, wrt) - isempty(data.reasons) -end - -""" -$(TYPEDSIGNATURES) - -Return information about the polynmial `x` with respect to variables in `wrt`, -writing said information to `data`. -""" -function process_polynomial!(data::PolynomialData, x, wrt) - x = unwrap(x) - symbolic_type(x) == NotSymbolic() && return true - iscall(x) || return true - contains_variable(x, wrt) || return true - any(isequal(x), wrt) && return true - - if operation(x) in (*, +, -, /) - # `map` because `all` will early exit, but we want to search - # through everything to get all the non-polynomial terms - return all(map(y -> is_polynomial!(data, y, wrt), arguments(x))) - end - if operation(x) == (^) - b, p = arguments(x) - is_pow_integer = symtype(p) <: Integer - if !is_pow_integer - push!(data.non_polynomial_terms, x) - push!(data.reasons, NonPolynomialReason.NonIntegerExponent) - end - if symbolic_type(p) != NotSymbolic() - data.has_parametric_exponent = true - end - - exponent_has_unknowns = contains_variable(p, wrt) - if exponent_has_unknowns - push!(data.non_polynomial_terms, x) - push!(data.reasons, NonPolynomialReason.ExponentContainsUnknowns) - end - base_polynomial = is_polynomial!(data, b, wrt) - return base_polynomial && !exponent_has_unknowns && is_pow_integer - end - push!(data.non_polynomial_terms, x) - push!(data.reasons, NonPolynomialReason.UnrecognizedOperation) - return false -end - -""" -$(TYPEDSIGNATURES) - -Given a `x`, a polynomial in variables in `wrt` which may contain rational functions, -express `x` as a single rational function with polynomial `num` and denominator `den`. -Return `(num, den)`. -""" -function handle_rational_polynomials(x, wrt) - x = unwrap(x) - symbolic_type(x) == NotSymbolic() && return x, 1 - iscall(x) || return x, 1 - contains_variable(x, wrt) || return x, 1 - any(isequal(x), wrt) && return x, 1 - - # simplify_fractions cancels out some common factors - # and expands (a / b)^c to a^c / b^c, so we only need - # to handle these cases - x = simplify_fractions(x) - op = operation(x) - args = arguments(x) - - if op == / - # numerator and denominator are trivial - num, den = args - # but also search for rational functions in numerator - n, d = handle_rational_polynomials(num, wrt) - num, den = n, den * d - elseif op == + - num = 0 - den = 1 - - # we don't need to do common denominator - # because we don't care about cases where denominator - # is zero. The expression is zero when all the numerators - # are zero. - for arg in args - n, d = handle_rational_polynomials(arg, wrt) - num += n - den *= d - end - else - return x, 1 - end - # if the denominator isn't a polynomial in `wrt`, better to not include it - # to reduce the size of the gcd polynomial - if !contains_variable(den, wrt) - return num / den, 1 - end - return num, den -end - """ $(TYPEDSIGNATURES) @@ -289,12 +78,6 @@ end SymbolicIndexingInterface.parameter_values(s::MTKHomotopySystem) = s.p -struct PolynomialTransformationData - new_var::BasicSymbolic - term::BasicSymbolic - inv_term::Vector -end - """ $(TYPEDSIGNATURES) @@ -312,128 +95,31 @@ Keyword arguments: All other keyword arguments are forwarded to `HomotopyContinuation.solver_startsystems`. """ function MTK.HomotopyContinuationProblem( - sys::NonlinearSystem, u0map, parammap = nothing; eval_expression = false, - eval_module = ModelingToolkit, warn_parametric_exponent = true, kwargs...) + sys::NonlinearSystem, u0map, parammap = nothing; kwargs...) if !iscomplete(sys) error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `HomotopyContinuationProblem`") end - - dvs = unknowns(sys) - # we need to consider `full_equations` because observed also should be - # polynomials (if used in equations) and we don't know if observed is used - # in denominator. - # This is not the most efficient, and would be improved significantly with - # CSE/hashconsing. - eqs = full_equations(sys) - - polydata = map(eqs) do eq - data = PolynomialData() - process_polynomial!(data, eq.lhs, dvs) - process_polynomial!(data, eq.rhs, dvs) - data - end - - has_parametric_exponents = any(d -> d.has_parametric_exponent, polydata) - - all_non_poly_terms = mapreduce(d -> d.non_polynomial_terms, vcat, polydata) - unique!(all_non_poly_terms) - - var_to_nonpoly = Dict{BasicSymbolic, PolynomialTransformationData}() - - is_poly = true - transformation_err = nothing - for t in all_non_poly_terms - # if the term involves multiple unknowns, we can't invert it - dvs_in_term = map(x -> occursin(x, t), dvs) - if count(dvs_in_term) > 1 - transformation_err = MultivarTerm(t, dvs[dvs_in_term]) - is_poly = false - break - end - # we already have a substitution solving for `var` - var = dvs[findfirst(dvs_in_term)] - if haskey(var_to_nonpoly, var) && !isequal(var_to_nonpoly[var].term, t) - transformation_err = MultipleTermsOfSameVar([t, var_to_nonpoly[var].term], var) - is_poly = false - break - end - # we want to solve `term - new_var` for `var` - new_var = gensym(Symbol(var)) - new_var = unwrap(only(@variables $new_var)) - invterm = Symbolics.ia_solve( - t - new_var, var; complex_roots = false, periodic_roots = false, warns = false) - # if we can't invert it, quit - if invterm === nothing || isempty(invterm) - transformation_err = SymbolicSolveFailure(t, var) - is_poly = false - break - end - # `ia_solve` returns lazy terms i.e. `asin(1.0)` instead of `pi/2` - # this just evaluates the constant expressions - invterm = Symbolics.substitute.(invterm, (Dict(),)) - # RootsOf implies Symbolics couldn't solve the inner polynomial because - # `Nemo` wasn't loaded. - if any(x -> MTK.iscall(x) && MTK.operation(x) == Symbolics.RootsOf, invterm) - transformation_err = NemoNotLoaded() - is_poly = false - break - end - var_to_nonpoly[var] = PolynomialTransformationData(new_var, t, invterm) + transformation = MTK.PolynomialTransformation(sys) + if transformation isa MTK.NotPolynomialError + throw(transformation) end - - if !is_poly - throw(NotPolynomialError(transformation_err, eqs, polydata)) - end - - subrules = Dict() - combinations = Vector[] - new_dvs = [] - for x in dvs - if haskey(var_to_nonpoly, x) - _data = var_to_nonpoly[x] - subrules[_data.term] = _data.new_var - push!(combinations, _data.inv_term) - push!(new_dvs, _data.new_var) - else - push!(combinations, [x]) - push!(new_dvs, x) - end - end - all_solutions = collect.(collect(Iterators.product(combinations...))) - - denoms = [] - eqs2 = map(eqs) do eq - t = eq.rhs - eq.lhs - t = Symbolics.fixpoint_sub(t, subrules; maxiters = length(dvs)) - # the substituted variable occurs outside the substituted term - poly_and_nonpoly = map(dvs) do x - haskey(var_to_nonpoly, x) && occursin(x, t) - end - if any(poly_and_nonpoly) - throw(NotPolynomialError( - VariablesAsPolyAndNonPoly(dvs[poly_and_nonpoly]), eqs, polydata)) - end - - num, den = handle_rational_polynomials(t, new_dvs) - # make factors different elements, otherwise the nonzero factors artificially - # inflate the error of the zero factor. - if iscall(den) && operation(den) == * - for arg in arguments(den) - # ignore constant factors - symbolic_type(arg) == NotSymbolic() && continue - push!(denoms, abs(arg)) - end - elseif symbolic_type(den) != NotSymbolic() - push!(denoms, abs(den)) - end - return 0 ~ num + result = MTK.transform_system(sys, transformation) + if result isa MTK.NotPolynomialError + throw(result) end + MTK.HomotopyContinuationProblem(sys, transformation, result, u0map, parammap; kwargs...) +end - sys2 = MTK.@set sys.eqs = eqs2 - MTK.@set! sys2.unknowns = new_dvs - # remove observed equations to avoid adding them in codegen - MTK.@set! sys2.observed = Equation[] - MTK.@set! sys2.substitutions = nothing +function MTK.HomotopyContinuationProblem( + sys::MTK.NonlinearSystem, transformation::MTK.PolynomialTransformation, + result::MTK.PolynomialTransformationResult, u0map, + parammap = nothing; eval_expression = false, + eval_module = ModelingToolkit, warn_parametric_exponent = true, kwargs...) + sys2 = result.sys + denoms = result.denominators + polydata = transformation.polydata + new_dvs = transformation.new_dvs + all_solutions = transformation.all_solutions _, u0, p = MTK.process_SciMLProblem( MTK.EmptySciMLFunction, sys, u0map, parammap; eval_expression, eval_module) @@ -443,10 +129,11 @@ function MTK.HomotopyContinuationProblem( unpack_solution = MTK.build_explicit_observed_function(sys2, all_solutions) hvars = symbolics_to_hc.(new_dvs) - mtkhsys = MTKHomotopySystem(nlfn.f, p, nlfn.jac, hvars, length(eqs)) + mtkhsys = MTKHomotopySystem(nlfn.f, p, nlfn.jac, hvars, length(new_dvs)) obsfn = MTK.ObservedFunctionCache(sys; eval_expression, eval_module) + has_parametric_exponents = any(d -> d.has_parametric_exponent, polydata) if has_parametric_exponents if warn_parametric_exponent @warn """ diff --git a/src/systems/nonlinear/homotopy_continuation.jl b/src/systems/nonlinear/homotopy_continuation.jl index e41a3f0ce7..044f44f70f 100644 --- a/src/systems/nonlinear/homotopy_continuation.jl +++ b/src/systems/nonlinear/homotopy_continuation.jl @@ -66,3 +66,440 @@ function SymbolicIndexingInterface.observed(p::HomotopyContinuationProblem, sym) return SymbolicIndexingInterface.observed(p.sys, sym) end end + +function contains_variable(x, wrt) + any(y -> occursin(y, x), wrt) +end + +""" +Possible reasons why a term is not polynomial +""" +EnumX.@enumx NonPolynomialReason begin + NonIntegerExponent + ExponentContainsUnknowns + BaseNotPolynomial + UnrecognizedOperation +end + +function display_reason(reason::NonPolynomialReason.T, sym) + if reason == NonPolynomialReason.NonIntegerExponent + pow = arguments(sym)[2] + "In $sym: Exponent $pow is not an integer" + elseif reason == NonPolynomialReason.ExponentContainsUnknowns + pow = arguments(sym)[2] + "In $sym: Exponent $pow contains unknowns of the system" + elseif reason == NonPolynomialReason.BaseNotPolynomial + base = arguments(sym)[1] + "In $sym: Base $base is not a polynomial in the unknowns" + elseif reason == NonPolynomialReason.UnrecognizedOperation + op = operation(sym) + """ + In $sym: Operation $op is not recognized. Allowed polynomial operations are \ + `*, /, +, -, ^`. + """ + else + error("This should never happen. Please open an issue in ModelingToolkit.jl.") + end +end + +""" + $(TYPEDEF) + +Information about an expression about its polynomial nature. +""" +mutable struct PolynomialData + """ + A list of all non-polynomial terms in the expression. + """ + non_polynomial_terms::Vector{BasicSymbolic} + """ + Corresponding to `non_polynomial_terms`, a list of reasons why they are + not polynomial. + """ + reasons::Vector{NonPolynomialReason.T} + """ + Whether the polynomial contains parametric exponents of unknowns. + """ + has_parametric_exponent::Bool +end + +PolynomialData() = PolynomialData(BasicSymbolic[], NonPolynomialReason.T[], false) + +abstract type PolynomialTransformationError <: Exception end + +struct MultivarTerm <: PolynomialTransformationError + term::Any + vars::Any +end + +function Base.showerror(io::IO, err::MultivarTerm) + println(io, + "Cannot convert system to polynomial: Found term $(err.term) which is a function of multiple unknowns $(err.vars).") +end + +struct MultipleTermsOfSameVar <: PolynomialTransformationError + terms::Any + var::Any +end + +function Base.showerror(io::IO, err::MultipleTermsOfSameVar) + println(io, + "Cannot convert system to polynomial: Found multiple non-polynomial terms $(err.terms) involving the same unknown $(err.var).") +end + +struct SymbolicSolveFailure <: PolynomialTransformationError + term::Any + var::Any +end + +function Base.showerror(io::IO, err::SymbolicSolveFailure) + println(io, + "Cannot convert system to polynomial: Unable to symbolically solve $(err.term) for $(err.var).") +end + +struct NemoNotLoaded <: PolynomialTransformationError end + +function Base.showerror(io::IO, err::NemoNotLoaded) + println(io, + "ModelingToolkit may be able to solve this system as a polynomial system if `Nemo` is loaded. Run `import Nemo` and try again.") +end + +struct VariablesAsPolyAndNonPoly <: PolynomialTransformationError + vars::Any +end + +function Base.showerror(io::IO, err::VariablesAsPolyAndNonPoly) + println(io, + "Cannot convert convert system to polynomial: Variables $(err.vars) occur in both polynomial and non-polynomial terms in the system.") +end + +struct NotPolynomialError <: Exception + transformation_err::Union{PolynomialTransformationError, Nothing} + eq::Vector{Equation} + data::Vector{PolynomialData} +end + +function Base.showerror(io::IO, err::NotPolynomialError) + if err.transformation_err !== nothing + Base.showerror(io, err.transformation_err) + end + for (eq, data) in zip(err.eq, err.data) + if isempty(data.non_polynomial_terms) + continue + end + println(io, + "Equation $(eq) is not a polynomial in the unknowns for the following reasons:") + for (term, reason) in zip(data.non_polynomial_terms, data.reasons) + println(io, display_reason(reason, term)) + end + end +end + +function is_polynomial!(data, y, wrt) + process_polynomial!(data, y, wrt) + isempty(data.reasons) +end + +""" +$(TYPEDSIGNATURES) + +Return information about the polynmial `x` with respect to variables in `wrt`, +writing said information to `data`. +""" +function process_polynomial!(data::PolynomialData, x, wrt) + x = unwrap(x) + symbolic_type(x) == NotSymbolic() && return true + iscall(x) || return true + contains_variable(x, wrt) || return true + any(isequal(x), wrt) && return true + + if operation(x) in (*, +, -, /) + # `map` because `all` will early exit, but we want to search + # through everything to get all the non-polynomial terms + return all(map(y -> is_polynomial!(data, y, wrt), arguments(x))) + end + if operation(x) == (^) + b, p = arguments(x) + is_pow_integer = symtype(p) <: Integer + if !is_pow_integer + push!(data.non_polynomial_terms, x) + push!(data.reasons, NonPolynomialReason.NonIntegerExponent) + end + if symbolic_type(p) != NotSymbolic() + data.has_parametric_exponent = true + end + + exponent_has_unknowns = contains_variable(p, wrt) + if exponent_has_unknowns + push!(data.non_polynomial_terms, x) + push!(data.reasons, NonPolynomialReason.ExponentContainsUnknowns) + end + base_polynomial = is_polynomial!(data, b, wrt) + return base_polynomial && !exponent_has_unknowns && is_pow_integer + end + push!(data.non_polynomial_terms, x) + push!(data.reasons, NonPolynomialReason.UnrecognizedOperation) + return false +end + +""" + $(TYPEDEF) + +Information about how an unknown in the system is substituted for a non-polynomial +expression to turn the system into a polynomial. Used in `PolynomialTransformation`. +""" +struct PolynomialTransformationData + """ + The new variable to use as an unknown of the transformed system. + """ + new_var::BasicSymbolic + """ + The non-polynomial expression being substituted. + """ + term::BasicSymbolic + """ + A vector of expressions corresponding to the solutions of + the non-polynomial expression `term` in terms of the new unknown `new_var`, + used to backsolve for the original unknown of the system. + """ + inv_term::Vector{BasicSymbolic} +end + +""" + $(TYPEDEF) + +Information representing how to transform a `NonlinearSystem` into a polynomial +system. +""" +struct PolynomialTransformation + """ + Substitutions mapping non-polynomial terms to temporary unknowns. The system + is a polynomial in the new unknowns. Currently, each non-polynomial term is a + function of a single unknown of the original system. + """ + substitution_rules::Dict{BasicSymbolic, BasicSymbolic} + """ + A vector of expressions involving unknowns of the transformed system, mapping + back to solutions of the original system. + """ + all_solutions::Vector{Vector{BasicSymbolic}} + """ + The new unknowns of the transformed system. + """ + new_dvs::Vector{BasicSymbolic} + """ + The polynomial data for each equation. + """ + polydata::Vector{PolynomialData} +end + +function PolynomialTransformation(sys::NonlinearSystem) + # we need to consider `full_equations` because observed also should be + # polynomials (if used in equations) and we don't know if observed is used + # in denominator. + # This is not the most efficient, and would be improved significantly with + # CSE/hashconsing. + eqs = full_equations(sys) + dvs = unknowns(sys) + + # Collect polynomial information about all equations + polydata = map(eqs) do eq + data = PolynomialData() + process_polynomial!(data, eq.lhs, dvs) + process_polynomial!(data, eq.rhs, dvs) + data + end + + # Get all unique non-polynomial terms + # NOTE: + # Is there a better way to check for uniqueness? `simplify` is relatively slow + # (maybe use the threaded version?) and `expand` can blow up expression size. + # Could metatheory help? + all_non_poly_terms = mapreduce(d -> d.non_polynomial_terms, vcat, polydata) + unique!(all_non_poly_terms) + + # each variable can only be replaced by one non-polynomial expression involving + # that variable. Keep track of this mapping. + var_to_nonpoly = Dict{BasicSymbolic, PolynomialTransformationData}() + + is_poly = true + transformation_err = nothing + for t in all_non_poly_terms + # if the term involves multiple unknowns, we can't invert it + dvs_in_term = map(x -> occursin(x, t), dvs) + if count(dvs_in_term) > 1 + transformation_err = MultivarTerm(t, dvs[dvs_in_term]) + is_poly = false + break + end + # we already have a substitution solving for `var` + var = dvs[findfirst(dvs_in_term)] + if haskey(var_to_nonpoly, var) && !isequal(var_to_nonpoly[var].term, t) + transformation_err = MultipleTermsOfSameVar([t, var_to_nonpoly[var].term], var) + is_poly = false + break + end + # we want to solve `term - new_var` for `var` + new_var = gensym(Symbol(var)) + new_var = unwrap(only(@variables $new_var)) + invterm = Symbolics.ia_solve( + t - new_var, var; complex_roots = false, periodic_roots = false, warns = false) + # if we can't invert it, quit + if invterm === nothing || isempty(invterm) + transformation_err = SymbolicSolveFailure(t, var) + is_poly = false + break + end + # `ia_solve` returns lazy terms i.e. `asin(1.0)` instead of `pi/2` + # this just evaluates the constant expressions + invterm = Symbolics.substitute.(invterm, (Dict(),)) + # RootsOf implies Symbolics couldn't solve the inner polynomial because + # `Nemo` wasn't loaded. + if any(x -> iscall(x) && operation(x) == Symbolics.RootsOf, invterm) + transformation_err = NemoNotLoaded() + is_poly = false + break + end + var_to_nonpoly[var] = PolynomialTransformationData(new_var, t, invterm) + end + + # return the error instead of throwing it, so the user can choose what to do + # without having to catch the exception + if !is_poly + return NotPolynomialError(transformation_err, eqs, polydata) + end + + subrules = Dict{BasicSymbolic, BasicSymbolic}() + # corresponding to each unknown in `dvs`, the list of its possible solutions + # in terms of the new unknown. + combinations = Vector{BasicSymbolic}[] + new_dvs = BasicSymbolic[] + for x in dvs + if haskey(var_to_nonpoly, x) + _data = var_to_nonpoly[x] + # map term to new unknown + subrules[_data.term] = _data.new_var + push!(combinations, _data.inv_term) + push!(new_dvs, _data.new_var) + else + push!(combinations, BasicSymbolic[x]) + push!(new_dvs, x) + end + end + all_solutions = vec(collect.(collect(Iterators.product(combinations...)))) + return PolynomialTransformation(subrules, all_solutions, new_dvs, polydata) +end + +""" + $(TYPEDEF) + +A struct containing the result of transforming a system into a polynomial system +using the appropriate `PolynomialTransformation`. Also contains the denominators +in the equations, to rule out invalid roots. +""" +struct PolynomialTransformationResult + sys::NonlinearSystem + denominators::Vector{BasicSymbolic} +end + +""" + $(TYPEDSIGNATURES) + +Transform the system `sys` with `transformation` and return a +`PolynomialTransformationResult`, or a `NotPolynomialError` if the system cannot +be transformed. +""" +function transform_system(sys::NonlinearSystem, transformation::PolynomialTransformation) + subrules = transformation.substitution_rules + dvs = unknowns(sys) + eqs = full_equations(sys) + polydata = transformation.polydata + new_dvs = transformation.new_dvs + all_solutions = transformation.all_solutions + + eqs2 = Equation[] + denoms = BasicSymbolic[] + for eq in eqs + t = eq.rhs - eq.lhs + t = Symbolics.fixpoint_sub(t, subrules; maxiters = length(dvs)) + # the substituted variable occurs outside the substituted term + poly_and_nonpoly = map(dvs) do x + all(!isequal(x), new_dvs) && occursin(x, t) + end + if any(poly_and_nonpoly) + return NotPolynomialError( + VariablesAsPolyAndNonPoly(dvs[poly_and_nonpoly]), eqs, polydata) + end + num, den = handle_rational_polynomials(t, new_dvs) + # make factors different elements, otherwise the nonzero factors artificially + # inflate the error of the zero factor. + if iscall(den) && operation(den) == * + for arg in arguments(den) + # ignore constant factors + symbolic_type(arg) == NotSymbolic() && continue + push!(denoms, abs(arg)) + end + elseif symbolic_type(den) != NotSymbolic() + push!(denoms, abs(den)) + end + push!(eqs2, 0 ~ num) + end + + sys2 = @set sys.eqs = eqs2 + @set! sys2.unknowns = new_dvs + # remove observed equations to avoid adding them in codegen + @set! sys2.observed = Equation[] + @set! sys2.substitutions = nothing + return PolynomialTransformationResult(sys2, denoms) +end + +""" +$(TYPEDSIGNATURES) + +Given a `x`, a polynomial in variables in `wrt` which may contain rational functions, +express `x` as a single rational function with polynomial `num` and denominator `den`. +Return `(num, den)`. +""" +function handle_rational_polynomials(x, wrt) + x = unwrap(x) + symbolic_type(x) == NotSymbolic() && return x, 1 + iscall(x) || return x, 1 + contains_variable(x, wrt) || return x, 1 + any(isequal(x), wrt) && return x, 1 + + # simplify_fractions cancels out some common factors + # and expands (a / b)^c to a^c / b^c, so we only need + # to handle these cases + x = simplify_fractions(x) + op = operation(x) + args = arguments(x) + + if op == / + # numerator and denominator are trivial + num, den = args + # but also search for rational functions in numerator + n, d = handle_rational_polynomials(num, wrt) + num, den = n, den * d + elseif op == + + num = 0 + den = 1 + + # we don't need to do common denominator + # because we don't care about cases where denominator + # is zero. The expression is zero when all the numerators + # are zero. + for arg in args + n, d = handle_rational_polynomials(arg, wrt) + num += n + den *= d + end + else + return x, 1 + end + # if the denominator isn't a polynomial in `wrt`, better to not include it + # to reduce the size of the gcd polynomial + if !contains_variable(den, wrt) + return num / den, 1 + end + return num, den +end From 669bc84e41897d8c3a0a270adf941829be1cdd10 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sat, 30 Nov 2024 19:53:48 +0530 Subject: [PATCH 3/7] feat: add `safe_HomotopyContinuationProblem` --- ext/MTKHomotopyContinuationExt.jl | 10 +++++++-- .../nonlinear/homotopy_continuation.jl | 21 +++++++++++++++++++ test/extensions/homotopy_continuation.jl | 20 ++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/ext/MTKHomotopyContinuationExt.jl b/ext/MTKHomotopyContinuationExt.jl index 586d608156..c4a090d9a8 100644 --- a/ext/MTKHomotopyContinuationExt.jl +++ b/ext/MTKHomotopyContinuationExt.jl @@ -96,16 +96,22 @@ All other keyword arguments are forwarded to `HomotopyContinuation.solver_starts """ function MTK.HomotopyContinuationProblem( sys::NonlinearSystem, u0map, parammap = nothing; kwargs...) + prob = MTK._safe_HomotopyContinuationProblem(sys, u0map, parammap; kwargs...) + prob isa MTK.HomotopyContinuationProblem || throw(prob) + return prob +end + +function MTK._safe_HomotopyContinuationProblem(sys, u0map, parammap = nothing; kwargs...) if !iscomplete(sys) error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `HomotopyContinuationProblem`") end transformation = MTK.PolynomialTransformation(sys) if transformation isa MTK.NotPolynomialError - throw(transformation) + return transformation end result = MTK.transform_system(sys, transformation) if result isa MTK.NotPolynomialError - throw(result) + return result end MTK.HomotopyContinuationProblem(sys, transformation, result, u0map, parammap; kwargs...) end diff --git a/src/systems/nonlinear/homotopy_continuation.jl b/src/systems/nonlinear/homotopy_continuation.jl index 044f44f70f..ed69db1aee 100644 --- a/src/systems/nonlinear/homotopy_continuation.jl +++ b/src/systems/nonlinear/homotopy_continuation.jl @@ -48,6 +48,27 @@ function HomotopyContinuationProblem(::AbstractSystem, _u0, _p; kwargs...) error("HomotopyContinuation.jl is required to create and solve `HomotopyContinuationProblem`s. Please run `Pkg.add(\"HomotopyContinuation\")` to continue.") end +""" + $(TYPEDSIGNATURES) + +Utility function for `safe_HomotopyContinuationProblem`, implemented in the extension. +""" +function _safe_HomotopyContinuationProblem end + +""" + $(TYPEDSIGNATURES) + +Return a `HomotopyContinuationProblem` if the extension is loaded and the system is +polynomial. If the extension is not loaded, return `nothing`. If the system is not +polynomial, return the appropriate `NotPolynomialError`. +""" +function safe_HomotopyContinuationProblem(sys::NonlinearSystem, args...; kwargs...) + if Base.get_extension(ModelingToolkit, :MTKHomotopyContinuationExt) === nothing + return nothing + end + return _safe_HomotopyContinuationProblem(sys, args...; kwargs...) +end + SymbolicIndexingInterface.symbolic_container(p::HomotopyContinuationProblem) = p.sys SymbolicIndexingInterface.state_values(p::HomotopyContinuationProblem) = p.u0 function SymbolicIndexingInterface.set_state!(p::HomotopyContinuationProblem, args...) diff --git a/test/extensions/homotopy_continuation.jl b/test/extensions/homotopy_continuation.jl index 81c252c84a..3bdfa06a5d 100644 --- a/test/extensions/homotopy_continuation.jl +++ b/test/extensions/homotopy_continuation.jl @@ -1,6 +1,19 @@ using ModelingToolkit, NonlinearSolve, SymbolicIndexingInterface +import ModelingToolkit as MTK using LinearAlgebra using Test + +@testset "Safe HCProblem" begin + @variables x y z + eqs = [0 ~ x^2 + y^2 + 2x * y + 0 ~ x^2 + 4x + 4 + 0 ~ y * z + 4x^2] + @mtkbuild sys = NonlinearSystem(eqs) + prob = MTK.safe_HomotopyContinuationProblem(sys, [x => 1.0, y => 1.0, z => 1.0], []) + @test prob === nothing +end + + import HomotopyContinuation @testset "No parameters" begin @@ -78,30 +91,37 @@ end @test_throws ["Cannot convert", "Unable", "symbolically solve", "Exponent", "not an integer", "not a polynomial"] HomotopyContinuationProblem( sys, []) + @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError @mtkbuild sys = NonlinearSystem([x^x - x ~ 0]) @test_throws ["Cannot convert", "Unable", "symbolically solve", "Exponent", "unknowns", "not a polynomial"] HomotopyContinuationProblem( sys, []) + @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError @mtkbuild sys = NonlinearSystem([((x^2) / sin(x))^2 + x ~ 0]) @test_throws ["Cannot convert", "both polynomial", "non-polynomial", "recognized", "sin", "not a polynomial"] HomotopyContinuationProblem( sys, []) + @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError @variables y = 2.0 @mtkbuild sys = NonlinearSystem([x^2 + y^2 + 2 ~ 0, y ~ sin(x)]) @test_throws ["Cannot convert", "recognized", "sin", "not a polynomial"] HomotopyContinuationProblem( sys, []) + @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError @mtkbuild sys = NonlinearSystem([x^2 + y^2 - 2 ~ 0, sin(x + y) ~ 0]) @test_throws ["Cannot convert", "function of multiple unknowns"] HomotopyContinuationProblem( sys, []) + @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError @mtkbuild sys = NonlinearSystem([sin(x)^2 + 1 ~ 0, cos(y) - cos(x) - 1 ~ 0]) @test_throws ["Cannot convert", "multiple non-polynomial terms", "same unknown"] HomotopyContinuationProblem( sys, []) + @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError @mtkbuild sys = NonlinearSystem([sin(x^2)^2 + sin(x^2) - 1 ~ 0]) @test_throws ["import Nemo"] HomotopyContinuationProblem(sys, []) + @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError end import Nemo From 1ef12170e0206694ac627639b28589871e2128a4 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sat, 30 Nov 2024 19:58:00 +0530 Subject: [PATCH 4/7] feat: use `HomotopyContinuationProblem` in `NonlinearProblem` if possible --- src/systems/nonlinear/nonlinearsystem.jl | 6 +++++- test/extensions/homotopy_continuation.jl | 18 ++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index ff4a7d0eb2..3cb68853aa 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -496,10 +496,14 @@ end function DiffEqBase.NonlinearProblem{iip}(sys::NonlinearSystem, u0map, parammap = DiffEqBase.NullParameters(); - check_length = true, kwargs...) where {iip} + check_length = true, use_homotopy_continuation = true, kwargs...) where {iip} if !iscomplete(sys) error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `NonlinearProblem`") end + prob = safe_HomotopyContinuationProblem(sys, u0map, parammap; check_length, kwargs...) + if prob isa HomotopyContinuationProblem + return prob + end f, u0, p = process_SciMLProblem(NonlinearFunction{iip}, sys, u0map, parammap; check_length, kwargs...) pt = something(get_metadata(sys), StandardNonlinearProblem()) diff --git a/test/extensions/homotopy_continuation.jl b/test/extensions/homotopy_continuation.jl index 3bdfa06a5d..e57cd0b2f7 100644 --- a/test/extensions/homotopy_continuation.jl +++ b/test/extensions/homotopy_continuation.jl @@ -13,7 +13,6 @@ using Test @test prob === nothing end - import HomotopyContinuation @testset "No parameters" begin @@ -22,12 +21,19 @@ import HomotopyContinuation 0 ~ x^2 + 4x + 4 0 ~ y * z + 4x^2] @mtkbuild sys = NonlinearSystem(eqs) - prob = HomotopyContinuationProblem(sys, [x => 1.0, y => 1.0, z => 1.0], []) + u0 = [x => 1.0, y => 1.0, z => 1.0] + prob = HomotopyContinuationProblem(sys, u0) @test prob[x] == prob[y] == prob[z] == 1.0 @test prob[x + y] == 2.0 sol = solve(prob; threading = false) @test SciMLBase.successful_retcode(sol) @test norm(sol.resid)≈0.0 atol=1e-10 + + prob2 = NonlinearProblem(sys, u0) + @test prob2 isa HomotopyContinuationProblem + sol = solve(prob2; threading = false) + @test SciMLBase.successful_retcode(sol) + @test norm(sol.resid)≈0.0 atol=1e-10 end struct Wrapper @@ -92,36 +98,44 @@ end "Exponent", "not an integer", "not a polynomial"] HomotopyContinuationProblem( sys, []) @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError + @test NonlinearProblem(sys, []) isa NonlinearProblem + @mtkbuild sys = NonlinearSystem([x^x - x ~ 0]) @test_throws ["Cannot convert", "Unable", "symbolically solve", "Exponent", "unknowns", "not a polynomial"] HomotopyContinuationProblem( sys, []) @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError + @test NonlinearProblem(sys, []) isa NonlinearProblem @mtkbuild sys = NonlinearSystem([((x^2) / sin(x))^2 + x ~ 0]) @test_throws ["Cannot convert", "both polynomial", "non-polynomial", "recognized", "sin", "not a polynomial"] HomotopyContinuationProblem( sys, []) @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError + @test NonlinearProblem(sys, []) isa NonlinearProblem @variables y = 2.0 @mtkbuild sys = NonlinearSystem([x^2 + y^2 + 2 ~ 0, y ~ sin(x)]) @test_throws ["Cannot convert", "recognized", "sin", "not a polynomial"] HomotopyContinuationProblem( sys, []) @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError + @test NonlinearProblem(sys, []) isa NonlinearProblem @mtkbuild sys = NonlinearSystem([x^2 + y^2 - 2 ~ 0, sin(x + y) ~ 0]) @test_throws ["Cannot convert", "function of multiple unknowns"] HomotopyContinuationProblem( sys, []) @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError + @test NonlinearProblem(sys, []) isa NonlinearProblem @mtkbuild sys = NonlinearSystem([sin(x)^2 + 1 ~ 0, cos(y) - cos(x) - 1 ~ 0]) @test_throws ["Cannot convert", "multiple non-polynomial terms", "same unknown"] HomotopyContinuationProblem( sys, []) @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError + @test NonlinearProblem(sys, []) isa NonlinearProblem @mtkbuild sys = NonlinearSystem([sin(x^2)^2 + sin(x^2) - 1 ~ 0]) @test_throws ["import Nemo"] HomotopyContinuationProblem(sys, []) @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError + @test NonlinearProblem(sys, []) isa NonlinearProblem end import Nemo From aa0e08ad700d5c9f52c0a6fd258ee11c3a5add3e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sat, 30 Nov 2024 23:16:17 +0530 Subject: [PATCH 5/7] docs: add docstrings to `NonPolynomialReason` enum variants --- src/systems/nonlinear/homotopy_continuation.jl | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/systems/nonlinear/homotopy_continuation.jl b/src/systems/nonlinear/homotopy_continuation.jl index ed69db1aee..03aeed1edf 100644 --- a/src/systems/nonlinear/homotopy_continuation.jl +++ b/src/systems/nonlinear/homotopy_continuation.jl @@ -96,9 +96,21 @@ end Possible reasons why a term is not polynomial """ EnumX.@enumx NonPolynomialReason begin + """ + Exponent of an expression involving unknowns is not an integer. + """ NonIntegerExponent + """ + Exponent is an expression containing unknowns. + """ ExponentContainsUnknowns + """ + The base of an exponent is not a polynomial in the unknowns. + """ BaseNotPolynomial + """ + An expression involves a non-polynomial operation involving unknowns. + """ UnrecognizedOperation end From b9b396ddb80537b71d9c06dca20df22e44eb6bc3 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sun, 8 Dec 2024 10:56:17 +0530 Subject: [PATCH 6/7] test: add logs to debug tests --- test/extensions/homotopy_continuation.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/extensions/homotopy_continuation.jl b/test/extensions/homotopy_continuation.jl index e57cd0b2f7..f9724692e7 100644 --- a/test/extensions/homotopy_continuation.jl +++ b/test/extensions/homotopy_continuation.jl @@ -160,6 +160,9 @@ end @test prob[x] ≈ 0.25 @test prob[y] ≈ 0.125 sol = solve(prob; threading = false) + # can't replicate the solve failure locally, so CI logs might help + @show sol.u HomotopyContinuation.real_solutions(sol.original) + @test SciMLBase.successful_retcode(sol) @test sol[a]≈0.5 atol=1e-6 @test sol[b]≈0.25 atol=1e-6 end From f428df4bc0dbc298a0f15280bb980679d7f895ef Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 9 Dec 2024 12:41:19 +0530 Subject: [PATCH 7/7] fixup! test: add logs to debug tests --- test/extensions/homotopy_continuation.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/extensions/homotopy_continuation.jl b/test/extensions/homotopy_continuation.jl index f9724692e7..554f9e1e1d 100644 --- a/test/extensions/homotopy_continuation.jl +++ b/test/extensions/homotopy_continuation.jl @@ -161,7 +161,7 @@ end @test prob[y] ≈ 0.125 sol = solve(prob; threading = false) # can't replicate the solve failure locally, so CI logs might help - @show sol.u HomotopyContinuation.real_solutions(sol.original) + @show sol.u sol.original.path_results @test SciMLBase.successful_retcode(sol) @test sol[a]≈0.5 atol=1e-6 @test sol[b]≈0.25 atol=1e-6