From 39a334b4b0430ddc32c55e66709af87f1e754062 Mon Sep 17 00:00:00 2001
From: Aayush Sabharwal <aayush.sabharwal@gmail.com>
Date: Fri, 31 Jan 2025 18:11:33 +0530
Subject: [PATCH 1/6] refactor: remove `HomotopyContinuationProblem`

---
 .../nonlinear/homotopy_continuation.jl        | 90 -------------------
 1 file changed, 90 deletions(-)

diff --git a/src/systems/nonlinear/homotopy_continuation.jl b/src/systems/nonlinear/homotopy_continuation.jl
index 09e11199e8..692b0719cd 100644
--- a/src/systems/nonlinear/homotopy_continuation.jl
+++ b/src/systems/nonlinear/homotopy_continuation.jl
@@ -1,93 +1,3 @@
-"""
-$(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
-
-"""
-    $(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...)
-    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
-
 function contains_variable(x, wrt)
     any(y -> occursin(y, x), wrt)
 end

From 945999e86fa621ce78a4b29dc66f17871efd357e Mon Sep 17 00:00:00 2001
From: Aayush Sabharwal <aayush.sabharwal@gmail.com>
Date: Fri, 31 Jan 2025 18:14:04 +0530
Subject: [PATCH 2/6] refactor: rearrange code

---
 .../nonlinear/homotopy_continuation.jl        | 46 +++++++++----------
 1 file changed, 23 insertions(+), 23 deletions(-)

diff --git a/src/systems/nonlinear/homotopy_continuation.jl b/src/systems/nonlinear/homotopy_continuation.jl
index 692b0719cd..714f6bec7b 100644
--- a/src/systems/nonlinear/homotopy_continuation.jl
+++ b/src/systems/nonlinear/homotopy_continuation.jl
@@ -45,29 +45,6 @@ function display_reason(reason::NonPolynomialReason.T, sym)
     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
@@ -138,6 +115,29 @@ function Base.showerror(io::IO, err::NotPolynomialError)
     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)
+
 function is_polynomial!(data, y, wrt)
     process_polynomial!(data, y, wrt)
     isempty(data.reasons)

From d39727e535489c2a1baa8de67edb5bbda421057e Mon Sep 17 00:00:00 2001
From: Aayush Sabharwal <aayush.sabharwal@gmail.com>
Date: Fri, 31 Jan 2025 18:42:11 +0530
Subject: [PATCH 3/6] fixup! refactor: rearrange code

---
 src/systems/nonlinear/homotopy_continuation.jl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/systems/nonlinear/homotopy_continuation.jl b/src/systems/nonlinear/homotopy_continuation.jl
index 714f6bec7b..757dfeb1bb 100644
--- a/src/systems/nonlinear/homotopy_continuation.jl
+++ b/src/systems/nonlinear/homotopy_continuation.jl
@@ -96,7 +96,7 @@ end
 struct NotPolynomialError <: Exception
     transformation_err::Union{PolynomialTransformationError, Nothing}
     eq::Vector{Equation}
-    data::Vector{PolynomialData}
+    data::Vector{Any} # PolynomialData but then I have to put that struct above this
 end
 
 function Base.showerror(io::IO, err::NotPolynomialError)

From cf2d5c658912378d5d48e136100101db35ef1c79 Mon Sep 17 00:00:00 2001
From: Aayush Sabharwal <aayush.sabharwal@gmail.com>
Date: Sat, 1 Feb 2025 00:55:39 +0530
Subject: [PATCH 4/6] feat: record all variables used in polynomial terms in
 `PolynomialData`

---
 src/systems/nonlinear/homotopy_continuation.jl | 14 +++++++++++---
 1 file changed, 11 insertions(+), 3 deletions(-)

diff --git a/src/systems/nonlinear/homotopy_continuation.jl b/src/systems/nonlinear/homotopy_continuation.jl
index 757dfeb1bb..2ef1120d2f 100644
--- a/src/systems/nonlinear/homotopy_continuation.jl
+++ b/src/systems/nonlinear/homotopy_continuation.jl
@@ -121,6 +121,11 @@ end
 Information about an expression about its polynomial nature.
 """
 mutable struct PolynomialData
+    """
+    A list of the variables in the expression being searched that occur outside of
+    any non-polynomial terms.
+    """
+    solo_terms::Set{BasicSymbolic}
     """
     A list of all non-polynomial terms in the expression.
     """
@@ -136,7 +141,7 @@ mutable struct PolynomialData
     has_parametric_exponent::Bool
 end
 
-PolynomialData() = PolynomialData(BasicSymbolic[], NonPolynomialReason.T[], false)
+PolynomialData() = PolynomialData(Set{BasicSymbolic}(), BasicSymbolic[], NonPolynomialReason.T[], false)
 
 function is_polynomial!(data, y, wrt)
     process_polynomial!(data, y, wrt)
@@ -152,9 +157,12 @@ 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 any(isequal(x), wrt)
+        push!(data.solo_terms, x)
+        return true
+    end
+    iscall(x) || return true
 
     if operation(x) in (*, +, -, /)
         # `map` because `all` will early exit, but we want to search

From b790dfbb6a7be172f515f95acd07076dbc4a0d4e Mon Sep 17 00:00:00 2001
From: Aayush Sabharwal <aayush.sabharwal@gmail.com>
Date: Sat, 1 Feb 2025 12:38:08 +0530
Subject: [PATCH 5/6] feat: add significantly improved polynomial
 transformation

---
 .../nonlinear/homotopy_continuation.jl        | 277 +++++++++++-------
 1 file changed, 164 insertions(+), 113 deletions(-)

diff --git a/src/systems/nonlinear/homotopy_continuation.jl b/src/systems/nonlinear/homotopy_continuation.jl
index 2ef1120d2f..4b23a742f6 100644
--- a/src/systems/nonlinear/homotopy_continuation.jl
+++ b/src/systems/nonlinear/homotopy_continuation.jl
@@ -47,50 +47,25 @@ end
 
 abstract type PolynomialTransformationError <: Exception end
 
-struct MultivarTerm <: PolynomialTransformationError
-    term::Any
-    vars::Any
+struct UnmatchedUnknowns <: PolynomialTransformationError
+    unmatched::Vector{BasicSymbolic}
 end
 
-function Base.showerror(io::IO, err::MultivarTerm)
+function Base.showerror(io::IO, err::UnmatchedUnknowns)
     println(io,
-        "Cannot convert system to polynomial: Found term $(err.term) which is a function of multiple unknowns $(err.vars).")
+        "Cannot convert system to polynomial: could not find terms to solve for unknowns $(err.unmatched).")
 end
 
-struct MultipleTermsOfSameVar <: PolynomialTransformationError
-    terms::Any
-    var::Any
+struct UnmatchedTerms <: PolynomialTransformationError
+    unmatched::Vector{BasicSymbolic}
 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
+function Base.showerror(io::IO, err::UnmatchedTerms)
+    println(io, "Cannot convert system to polynomial: too many non-polynomial terms in system. Unmatched terms are $(err.unmatched).")
 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.")
+function no_nemo_warning()
+    @warn "ModelingToolkit may be able to symbolically solve some non-polynomial terms in this system for all roots if `Nemo` is loaded. Run `import Nemo` and try again to enable this functionality and possibly obtain additional roots."
 end
 
 struct NotPolynomialError <: Exception
@@ -216,6 +191,24 @@ struct PolynomialTransformationData
     inv_term::Vector{BasicSymbolic}
 end
 
+"""
+    $(TYPEDEF)
+
+Information for how to solve for unknowns involved in non-symbolically-solvable
+non-polynomial terms to turn the system into a polynomial. Used in
+`PolynomialTransformation`.
+"""
+struct NonlinearSolveTransformation
+    """
+    The system which solves for the unknowns of the parent system.
+    """
+    sys::NonlinearSystem
+    """
+    The input variables to this system representing solutions of non-polynomial terms.
+    """
+    inputvars::Vector{BasicSymbolic}
+end
+
 """
     $(TYPEDEF)
 
@@ -224,20 +217,19 @@ 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.
+    The stages in which to recover the solution in terms of the original unknowns, in
+    order.
     """
-    substitution_rules::Dict{BasicSymbolic, BasicSymbolic}
+    solve_stages::Vector{Any}
     """
-    A vector of expressions involving unknowns of the transformed system, mapping
-    back to solutions of the original system.
+    The (previous) stages each stage depends on.
     """
-    all_solutions::Vector{Vector{BasicSymbolic}}
+    stage_dependencies::Vector{Vector{Int}}
     """
-    The new unknowns of the transformed system.
+    Mapping from terms to new unknowns they are replaced by. The system is a
+    polynomial in the new unknowns.
     """
-    new_dvs::Vector{BasicSymbolic}
+    substitution_rules::Dict{BasicSymbolic, BasicSymbolic}
     """
     The polynomial data for each equation.
     """
@@ -270,76 +262,135 @@ function PolynomialTransformation(sys::NonlinearSystem)
         d -> d.non_polynomial_terms, vcat, polydata; init = BasicSymbolic[])
     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}()
+    all_solo_vars = mapreduce(d -> d.solo_terms, union, polydata; init = Set{BasicSymbolic}())
+
+    # Graph matches variables to candidates for unknowns of the polynomial system that
+    # they occur in. These unknowns can be solo variables that appear outside of
+    # non-polynomial terms in the system, or non-polynomials.
+    graph = BipartiteGraph(length(dvs), 0)
+    # all solo variables are candidates for unknowns
+    graph_srcs = dvs
+    graph_dsts = BasicSymbolic[]
+    for (i, var) in enumerate(dvs)
+        var in all_solo_vars || continue
+        push!(graph_dsts, var)
+        vert = add_vertex!(graph, DST)
+        add_edge!(graph, i, vert)
+    end
 
-    is_poly = true
-    transformation_err = nothing
+    # buffer to prevent reallocations
+    dvs_in_term = Set()
+    # for efficient queries
+    dvs_to_src = Dict(graph_srcs .=> eachindex(graph_srcs))
+    # build out graph with other non-polynomial terms
     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
+        empty!(dvs_in_term)
+        vars!(dvs_in_term, t)
+        intersect!(dvs_in_term, dvs)
+        push!(graph_dsts, t)
+        vert = add_vertex!(graph, DST)
+        for var in dvs_in_term
+            add_edge!(graph, dvs_to_src[var], vert)
         end
-        var_to_nonpoly[var] = PolynomialTransformationData(new_var, t, invterm)
     end
 
+    # Match variables to the candidate unknown we'll use to solve for them.
+    # This is a poor man's version of `structural_simplify`, but if we create
+    # and simplify a `NonlinearSystem` it makes doing symbolic solving more
+    # annoying.
+    matching = BipartiteGraphs.complete(maximal_matching(graph))
+    inv_matching = invview(matching)
+    # matching is from destination to source vertices
+    unassigned_dsts = filter(i -> matching[i] == unassigned, 𝑠vertices(graph))
+    unassigned_srcs = filter(i -> inv_matching[i] == unassigned, 𝑑vertices(graph))
+
     # 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)
+    if !isempty(unassigned_srcs)
+        return NotPolynomialError(UnmatchedUnknowns(graph_srcs[unassigned_srcs]), eqs, polydata)
+    end
+    if !isempty(unassigned_dsts)
+        return NotPolynomialError(UnmatchedTerms(graph_dsts[unassigned_dsts]), eqs, polydata)
+    end
+
+    # At this point, the matching is perfect. Find the SCCs so we know
+    # which terms to solve for which variables.
+    digraph = DiCMOBiGraph{false}(graph, matching)
+    var_sccs = Graphs.strongly_connected_components(digraph)
+    foreach(sort!, var_sccs)
+    # construct a condensation graph of the SCCs so we can topologically sort them
+    scc_graph = MatchedCondensationGraph(digraph, var_sccs)
+    toporder = topological_sort(scc_graph)
+    var_sccs = var_sccs[toporder]
+    # get the corresponding terms
+    term_sccs = map(var_sccs) do scc
+        map(scc) do src
+            inv_matching[src]
+        end
     end
 
+    # keep track of which previous SCCs each SCC depends on
+    dependencies = Vector{Int}[]
+    # the method to solve each stage
+    solve_stages = []
+    # mapping from terms to the new unknowns they are replaced by
     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)
+    # if we've already emitted the no nemo warning
+    warned_no_nemo = false
+    for (i, (vscc, tscc)) in enumerate(zip(var_sccs, term_sccs))
+        # dependencies are simply outneighbors
+        push!(dependencies, collect(Graphs.outneighbors(scc_graph, i)))
+
+        # whether the SCC is solvable with a single variable
+        single_scc_solvable = length(vscc) == 1
+        # for single-variable SCCs, we use `ia_solve`
+        if single_scc_solvable
+            varidx = vscc[]
+            termidx = tscc[]
+            var = graph_srcs[varidx]
+            t = graph_dsts[termidx]
+            # Create a new variable and representing the non-polynomial term...
+            new_var = unwrap(similar_variable(var, Symbol(var)))
+            # ...and solve for `var` in terms of this new variable.
+            invterm = Symbolics.ia_solve(t - new_var, var; complex_roots = false, periodic_roots = false, warns = false)
+            # `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(),))
+            # if `ia_solve` returns `nothing`, the broadcast above turns it into `(nothing,)`
+            if invterm === (nothing,) || isempty(invterm)
+                # if we can't invert it, quit
+                single_scc_solvable = false
+            elseif any(x -> iscall(x) && operation(x) == Symbolics.RootsOf, invterm)
+                # RootsOf implies Symbolics couldn't solve the inner polynomial because
+                # `Nemo` wasn't loaded.
+                warned_no_nemo || no_nemo_warning()
+                warned_no_nemo = true
+                single_scc_solvable = false
+            else
+                subrules[t] = new_var
+                push!(solve_stages, PolynomialTransformationData(new_var, t, invterm))
+            end
+        end
+
+        # the SCC was solved with a single variable
+        single_scc_solvable && continue
+
+        # Solve using a `NonlinearSolve`.
+        vars = graph_srcs[vscc]
+        ts = graph_dsts[tscc]
+        # the new variables are inputs to the system, so they're parameters
+        new_vars = map(vars) do var
+            toparam.(unwrap.(similar_variable(var, Symbol(var))))
+        end
+        eqs = collect(0 .~ (ts .- new_vars))
+        scc_sys = complete(NonlinearSystem(eqs; name = Symbol(:scc_, i)))
+        push!(solve_stages, NonlinearSolveTransformation(scc_sys, new_vars))
+        for (new_var, t) in zip(new_vars, ts)
+            subrules[t] = new_var
         end
     end
-    all_solutions = vec(collect.(collect(Iterators.product(combinations...))))
-    return PolynomialTransformation(subrules, all_solutions, new_dvs, polydata)
+
+    return PolynomialTransformation(solve_stages, dependencies, subrules, polydata)
 end
 
 """
@@ -352,6 +403,15 @@ in the equations, to rule out invalid roots.
 struct PolynomialTransformationResult
     sys::NonlinearSystem
     denominators::Vector{BasicSymbolic}
+    """
+    The stages in which to recover the solution in terms of the original unknowns, in
+    order.
+    """
+    solve_stages::Vector{Any}
+    """
+    The (previous) stages each stage depends on.
+    """
+    stage_dependencies::Vector{Vector{Int}}
 end
 
 """
@@ -367,22 +427,13 @@ function transform_system(sys::NonlinearSystem, transformation::PolynomialTransf
     dvs = unknowns(sys)
     eqs = full_equations(sys)
     polydata = transformation.polydata
-    new_dvs = transformation.new_dvs
-    all_solutions = transformation.all_solutions
+    new_dvs = collect(values(subrules))
 
     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; fraction_cancel_fn)
         # make factors different elements, otherwise the nonzero factors artificially
         # inflate the error of the zero factor.
@@ -403,7 +454,7 @@ function transform_system(sys::NonlinearSystem, transformation::PolynomialTransf
     # remove observed equations to avoid adding them in codegen
     @set! sys2.observed = Equation[]
     @set! sys2.substitutions = nothing
-    return PolynomialTransformationResult(sys2, denoms)
+    return PolynomialTransformationResult(sys2, denoms, transformation.solve_stages, transformation.stage_dependencies)
 end
 
 """

From 108b87ab0a53cbf1394907e8abfed9fc6783476b Mon Sep 17 00:00:00 2001
From: Aayush Sabharwal <aayush.sabharwal@gmail.com>
Date: Sat, 1 Feb 2025 12:40:37 +0530
Subject: [PATCH 6/6] refactor: remove `MTKHomotopyContinuationExt`

---
 Project.toml                             |   3 -
 ext/MTKHomotopyContinuationExt.jl        | 225 -----------------------
 src/systems/nonlinear/nonlinearsystem.jl |   9 +-
 3 files changed, 1 insertion(+), 236 deletions(-)
 delete mode 100644 ext/MTKHomotopyContinuationExt.jl

diff --git a/Project.toml b/Project.toml
index e4b2238170..f086d8c088 100644
--- a/Project.toml
+++ b/Project.toml
@@ -65,7 +65,6 @@ BifurcationKit = "0f109fa4-8a5d-4b75-95aa-f515264e7665"
 ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
 DeepDiffs = "ab62b9b5-e342-54a8-a765-a90f495de1a6"
 FMI = "14a09403-18e3-468f-ad8a-74f8dda2d9ac"
-HomotopyContinuation = "f213a82b-91d6-5c5d-acf7-10f1c761b327"
 InfiniteOpt = "20393b10-9daf-11e9-18c9-8db751c92c57"
 LabelledArrays = "2ee39098-c373-598a-b85f-a56591580800"
 
@@ -74,7 +73,6 @@ MTKBifurcationKitExt = "BifurcationKit"
 MTKChainRulesCoreExt = "ChainRulesCore"
 MTKDeepDiffsExt = "DeepDiffs"
 MTKFMIExt = "FMI"
-MTKHomotopyContinuationExt = "HomotopyContinuation"
 MTKInfiniteOptExt = "InfiniteOpt"
 MTKLabelledArraysExt = "LabelledArrays"
 
@@ -110,7 +108,6 @@ FunctionWrappers = "1.1"
 FunctionWrappersWrappers = "0.1"
 FMI = "0.14"
 Graphs = "1.5.2"
-HomotopyContinuation = "2.11"
 InfiniteOpt = "0.5"
 InteractiveUtils = "1"
 JuliaFormatter = "1.0.47"
diff --git a/ext/MTKHomotopyContinuationExt.jl b/ext/MTKHomotopyContinuationExt.jl
deleted file mode 100644
index 8f17c05b18..0000000000
--- a/ext/MTKHomotopyContinuationExt.jl
+++ /dev/null
@@ -1,225 +0,0 @@
-module MTKHomotopyContinuationExt
-
-using ModelingToolkit
-using ModelingToolkit.SciMLBase
-using ModelingToolkit.Symbolics: unwrap, symtype, BasicSymbolic, simplify_fractions
-using ModelingToolkit.SymbolicIndexingInterface
-using ModelingToolkit.DocStringExtensions
-using HomotopyContinuation
-using ModelingToolkit: iscomplete, parameters, has_index_cache, get_index_cache, get_u0,
-                       get_u0_p, check_eqs_u0, CommonSolve
-
-const MTK = ModelingToolkit
-
-"""
-$(TYPEDSIGNATURES)
-
-Convert `expr` from a symbolics expression to one that uses `HomotopyContinuation.ModelKit`.
-"""
-function symbolics_to_hc(expr)
-    if iscall(expr)
-        if operation(expr) == getindex
-            args = arguments(expr)
-            return ModelKit.Variable(getname(args[1]), args[2:end]...)
-        else
-            return operation(expr)(symbolics_to_hc.(arguments(expr))...)
-        end
-    elseif symbolic_type(expr) == NotSymbolic()
-        return expr
-    else
-        return ModelKit.Variable(getname(expr))
-    end
-end
-
-"""
-$(TYPEDEF)
-
-A subtype of `HomotopyContinuation.AbstractSystem` used to solve `HomotopyContinuationProblem`s.
-"""
-struct MTKHomotopySystem{F, P, J, V} <: HomotopyContinuation.AbstractSystem
-    """
-    The generated function for the residual of the polynomial system. In-place.
-    """
-    f::F
-    """
-    The parameter object.
-    """
-    p::P
-    """
-    The generated function for the jacobian of the polynomial system. In-place.
-    """
-    jac::J
-    """
-    The `HomotopyContinuation.ModelKit.Variable` representation of the unknowns of
-    the system.
-    """
-    vars::V
-    """
-    The number of polynomials in the system. Must also be equal to `length(vars)`.
-    """
-    nexprs::Int
-end
-
-Base.size(sys::MTKHomotopySystem) = (sys.nexprs, length(sys.vars))
-ModelKit.variables(sys::MTKHomotopySystem) = sys.vars
-
-function (sys::MTKHomotopySystem)(x, p = nothing)
-    sys.f(x, sys.p)
-end
-
-function ModelKit.evaluate!(u, sys::MTKHomotopySystem, x, p = nothing)
-    sys.f(u, x, sys.p)
-end
-
-function ModelKit.evaluate_and_jacobian!(u, U, sys::MTKHomotopySystem, x, p = nothing)
-    sys.f(u, x, sys.p)
-    sys.jac(U, x, sys.p)
-end
-
-SymbolicIndexingInterface.parameter_values(s::MTKHomotopySystem) = s.p
-
-"""
-    $(TYPEDSIGNATURES)
-
-Create a `HomotopyContinuationProblem` from a `NonlinearSystem` with polynomial equations.
-The problem will be solved by HomotopyContinuation.jl. The resultant `NonlinearSolution`
-will contain the polynomial root closest to the point specified by `u0map` (if real roots
-exist for the system).
-
-Keyword arguments:
-- `eval_expression`: Whether to `eval` the generated functions or use a `RuntimeGeneratedFunction`.
-- `eval_module`: The module to use for `eval`/`@RuntimeGeneratedFunction`
-- `warn_parametric_exponent`: Whether to warn if the system contains a parametric
-  exponent preventing the homotopy from being cached.
-
-All other keyword arguments are forwarded to `HomotopyContinuation.solver_startsystems`.
-"""
-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;
-        fraction_cancel_fn = SymbolicUtils.simplify_fractions, 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
-        return transformation
-    end
-    result = MTK.transform_system(sys, transformation; fraction_cancel_fn)
-    if result isa MTK.NotPolynomialError
-        return result
-    end
-    MTK.HomotopyContinuationProblem(sys, transformation, result, u0map, parammap; kwargs...)
-end
-
-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)
-    nlfn = NonlinearFunction{true}(sys2; jac = true, eval_expression, eval_module)
-
-    denominator = MTK.build_explicit_observed_function(sys2, denoms)
-    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(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 """
-            The system has parametric exponents, preventing caching of the homotopy. \
-            This will cause `solve` to be slower. Pass `warn_parametric_exponent \
-            = false` to turn off this warning
-            """
-        end
-        solver_and_starts = nothing
-    else
-        solver_and_starts = HomotopyContinuation.solver_startsolutions(mtkhsys; kwargs...)
-    end
-    return MTK.HomotopyContinuationProblem(
-        u0, mtkhsys, denominator, sys, obsfn, solver_and_starts, unpack_solution)
-end
-
-"""
-$(TYPEDSIGNATURES)
-
-Solve a `HomotopyContinuationProblem`. Ignores the algorithm passed to it, and always
-uses `HomotopyContinuation.jl`. The original solution as returned by
-`HomotopyContinuation.jl` will be available in the `.original` field of the returned
-`NonlinearSolution`.
-
-All keyword arguments except the ones listed below are forwarded to
-`HomotopyContinuation.solve`. Note that the solver and start solutions are precomputed,
-and only keyword arguments related to the solve process are valid. All keyword
-arguments have their default values in HomotopyContinuation.jl, except `show_progress`
-which defaults to `false`.
-
-Extra keyword arguments:
-- `denominator_abstol`: In case `prob` is solving a rational function, roots which cause
-  the denominator to be below `denominator_abstol` will be discarded.
-"""
-function CommonSolve.solve(prob::MTK.HomotopyContinuationProblem,
-        alg = nothing; show_progress = false, denominator_abstol = 1e-7, kwargs...)
-    if prob.solver_and_starts === nothing
-        sol = HomotopyContinuation.solve(
-            prob.homotopy_continuation_system; show_progress, kwargs...)
-    else
-        solver, starts = prob.solver_and_starts
-        sol = HomotopyContinuation.solve(solver, starts; show_progress, kwargs...)
-    end
-    realsols = HomotopyContinuation.results(sol; only_real = true)
-    if isempty(realsols)
-        u = state_values(prob)
-        retcode = SciMLBase.ReturnCode.ConvergenceFailure
-        resid = prob.homotopy_continuation_system(u)
-    else
-        T = eltype(state_values(prob))
-        distance = T(Inf)
-        u = state_values(prob)
-        resid = nothing
-        for result in realsols
-            if any(<=(denominator_abstol),
-                prob.denominator(real.(result.solution), parameter_values(prob)))
-                continue
-            end
-            for truesol in prob.unpack_solution(result.solution, parameter_values(prob))
-                dist = norm(truesol - state_values(prob))
-                if dist < distance
-                    distance = dist
-                    u = T.(real.(truesol))
-                    resid = T.(real.(prob.homotopy_continuation_system(result.solution)))
-                end
-            end
-        end
-        # all roots cause denominator to be zero
-        if isinf(distance)
-            u = state_values(prob)
-            resid = prob.homotopy_continuation_system(u)
-            retcode = SciMLBase.ReturnCode.Infeasible
-        else
-            retcode = SciMLBase.ReturnCode.Success
-        end
-    end
-
-    return SciMLBase.build_solution(
-        prob, :HomotopyContinuation, u, resid; retcode, original = sol)
-end
-
-end
diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl
index 89663f7dbc..3463a9a714 100644
--- a/src/systems/nonlinear/nonlinearsystem.jl
+++ b/src/systems/nonlinear/nonlinearsystem.jl
@@ -527,17 +527,10 @@ end
 
 function DiffEqBase.NonlinearProblem{iip}(sys::NonlinearSystem, u0map,
         parammap = DiffEqBase.NullParameters();
-        check_length = true, use_homotopy_continuation = false, kwargs...) where {iip}
+        check_length = 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
-    if use_homotopy_continuation
-        prob = safe_HomotopyContinuationProblem(
-            sys, u0map, parammap; check_length, kwargs...)
-        if prob isa HomotopyContinuationProblem
-            return prob
-        end
-    end
     f, u0, p = process_SciMLProblem(NonlinearFunction{iip}, sys, u0map, parammap;
         check_length, kwargs...)
     pt = something(get_metadata(sys), StandardNonlinearProblem())