Skip to content

Migration guide: NLsolve.nlsolve with manual Jacobian and autoscale for kite power system #836

@ChrisRackauckas-Claude

Description

@ChrisRackauckas-Claude

Context

KiteModels.jl uses NLsolve.jl to find the steady-state equilibrium of a 4-point kite power system (3D mass-spring network with aerodynamic forces). We'd like to migrate to NonlinearSolve.jl but the current code relies on several NLsolve-specific features.

Current NLsolve usage

The core call pattern is:

using NLsolve

# Manual central finite-difference Jacobian with subnormal flushing
function make_jac(f!, n_vars)
    _F1 = zeros(Float64, n_vars)
    _F2 = zeros(Float64, n_vars)
    _xp = zeros(Float64, n_vars)
    _xm = zeros(Float64, n_vars)
    threshold = sqrt(floatmin(Float64))
    function jac!(J, x)
        h_factor = cbrt(eps(Float64))
        for j in 1:n_vars
            copyto!(_xp, x); copyto!(_xm, x)
            h = max(abs(x[j]), one(Float64)) * h_factor
            _xp[j] += h; _xm[j] -= h
            f!(_F1, _xp); f!(_F2, _xm)
            @views J[:, j] .= (_F1 .- _F2) ./ (2h)
        end
        @. J = ifelse(abs(J) < threshold, zero(Float64), J)
    end
    return jac!
end

n_unknowns = 20  # 2*(segments + kite_particles - 1) + 2
X00 = zeros(Float64, n_unknowns)
jac! = make_jac(f!, n_unknowns)
results = nlsolve(f!, jac!, X00;
                  autoscale=true, xtol=4e-7, ftol=4e-7, iterations=200)
if NLsolve.converged(results)
    solution = results.zero
end

NLsolve features we depend on

  1. Manual Jacobian (jac!): Needed because NLSolversBase >= 7.10 produces subnormal Jacobian entries via DifferentiationInterface, which cause NaN in NLsolve's autoscale (subnormal column norms squared underflow to zero). The manual Jacobian flushes near-zero entries.

  2. autoscale=true: Critical for convergence — the system variables have very different scales (positions in meters vs. small perturbations).

  3. Separate xtol and ftol: Both set to 4e-7.

  4. iterations limit: Set to 200.

  5. results.zero and converged(results): Solution extraction and convergence check.

Questions

  1. What is the equivalent NonlinearSolve.jl API for this call pattern?
  2. Does NonlinearSolve handle autoscaling? (I see #425 is open for trust region autoscaling.)
  3. Does NonlinearSolve's automatic differentiation avoid the subnormal Jacobian issue, or would we still need a manual Jacobian?
  4. What's the recommended solver algorithm for this kind of problem (20-variable nonlinear system from physics, starting from a reasonable initial guess)?

Self-contained MWE

Below is a complete, self-contained MWE (~960 lines) that reproduces the KiteModels.jl steady-state test without any kite-specific package dependencies. It models a 4-point kite on a segmented tether with aerodynamic forces, spring/damping, and a winch. The nonlinear solve finds particle positions where all accelerations vanish (steady-state equilibrium).

Dependencies: StaticArrays, LinearAlgebra, NLsolve, Dierckx, Test

Click to expand full MWE (mwe_steady_state_kps4.jl)
$(cat /home/crackauc/sandbox/tmp_20260219_005256_56738/mwe_steady_state_kps4.jl)

Running the MWE

] add StaticArrays NLsolve Dierckx
include("mwe_steady_state_kps4.jl")

Expected output (all 15 tests pass):

  l=100.0: tether_length=100.92196416459542, pre_tension=1.0003226874576083 ✓
  l=200.0: tether_length=202.59541320501268, pre_tension=1.0004541973108771 ✓
  l=392.0: tether_length=399.15270436705833, pre_tension=1.0006386343184874 ✓
Test Summary:                | Pass  Total   Time
test_find_steady_state (MWE) |   15     15  15.3s

Environment

  • Julia 1.11
  • NLsolve v4.5.1
  • StaticArrays v1.9.16

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions