A C++ toolkit for nonlinear equation solving, made of three parts:
NLESolver— interfaces and algorithms for nonlinear equations, nonlinear least squares, scalar equations, array equations and fixed-point problems.ModelEnv— a modeling environment underCommon/Math/ModelEnvthat owns variables, residual constraints, parameters, transforms and scaling, partial masking and Jacobian assembly. It turns a physical model into the vectors and derivatives a solver can consume.Common— Eigen linear algebra adapters, Jacobian evaluation, line search, trust region, scaling, logging and utility code shared byNLESolverandModelEnv.
- One interface, many algorithms — create solvers through
CreateSolver,CreateScalarSolverandCreateArraySolver; switching algorithm does not require rewriting the problem definition. - Several Jacobian modes — analytic dense Jacobians, numerical differences and sparse Jacobians, so the cost model can follow the problem size.
- Many solver strategies — Newton, Dogleg, line search, Broyden and trust-region quasi-Newton methods, selectable by stiffness, size and Jacobian availability.
- Production constraints built in — variable bounds, fixed variables, automatic scaling, damping, non-monotone steps, trust-region control and iteration callbacks.
- Diagnosable results —
Summaryreports residuals, iteration counts, function/Jacobian evaluations, timings and the final exit status. - Clean component boundary —
Commonkeeps only the math and infrastructure that the solver needs, so it can be used on its own. - Modeling separated from solving —
ModelEnvdecides what the model is made of,NLESolverdecides how to iterate; changing the model structure does not touch the solving loop.
NLESolver supports these problem forms:
r(x) = 0
min 0.5 * ||r(x)||²
f(x) = 0 scalar or array problems
x = f(x) fixed-point problems
Vector problems can use analytic dense, numerical or sparse Jacobians. The interface also supports variable bounds, fixed variables, automatic scaling, damping, non-monotone steps, iteration callbacks, weights and a solve summary.
The current implementation provides these solvers:
| Category | Solvers |
|---|---|
| Quasi-Newton | Broyden, Thomas, InverseColumn |
| Trust-region quasi-Newton | TRBroyden, TRCUM |
| Newton / line search | Newton, Tensor |
| Dogleg | VanillaDogleg, ExactDogleg, TensorDogleg |
| Scalar | ITP, ScalarNewton |
| Fixed point | SuccessiveSubstitution, Wegstein, AAI, QuasiNewton |
ExactDogleg suits well-behaved problems with a reliable Jacobian. Without a Jacobian, small
problems can use the solver's numerical differences; medium and large problems are usually better
served by the quasi-Newton methods.
ModelEnv lives in Common/Math/ModelEnv and is the modeling layer of this toolkit: it answers
"what is the model made of", while NLESolver answers "how do we iterate". Packing and unpacking
variables, evaluating constraints, assembling Jacobians, transforms and scaling, converting
constraint senses and partial masking are all handled by the environment, so a solver only needs a
variable vector, a residual vector and a Jacobian.
| Entity | Role | Creation APIs | Notes |
|---|---|---|---|
Variables |
unknowns x | AddVariable, AddScalarVariable, AddVariables |
slice, transform, scaling and bounds |
Residuals |
constraints and objective | AddResidual, AddScalarResidual, AddResiduals, plus the specialized residual APIs |
the lhs/rhs relationship |
Parameters |
parameters | AddParameter, AddScalarParameter, AddParameters |
used in evaluation only; not packed with transform or scaling |
Every entity is represented by a shared_ptr wrapper returned by the environment, and wrappers are
not copyable. The underlying storage can be owned by the wrapper (AddVariable, AddResidual,
AddParameter) or by the caller through a pointer or Eigen object (AddVariables, AddResiduals).
In the latter case the caller must keep the pointer alive, and after any resize of the external Eigen
object the wrapper's ResetCount must be called before re-running FinalizeIndexes.
A constraint reads lhs sense rhs, where sense is Sense::Equal, Sense::GreaterEqual or
Sense::LessEqual; residuals with an rhs are converted to a "greater or equal to zero" form as
lhs - rhs (rhs - lhs for LessEqual). Transforms and scaling apply per value: first
value / scaling_factor, then Transform::Linear, Log, Log1p, SoftOne or Arctan. The
environment does not validate the domain of Log, Log1p or SoftOne — use bounds to guarantee it.
| Creation API | Type | Value evaluation | Jacobian |
|---|---|---|---|
AddResidual, AddScalarResidual, AddResiduals |
General |
the caller writes lhs (and rhs if present) | inserted manually via Insert*DerivativeToJacobian |
AddLinearResidual |
Linear |
computed by the environment from the variable mapping and coefficients | generated automatically from the current mapping |
AddADResidual |
ADResiduals |
computed by the environment from a functor plus autodiff | generated automatically |
AddScriptResidual |
Script |
computed by the environment from an exprtk expression | generated automatically |
AddComplementarityResidual |
Complementarity |
computed by the environment from an NCP smoothing function | generated automatically |
For everything except General, the environment evaluates values and derivatives; manual insertion
of their derivatives is ignored. Complementarity expresses constraints of the form
(a + ...) * (b + ...) = 0; the smoothing function and its parameters are set through
SetNCPOptions, and the variables involved must have a lower bound of zero. Script uses exprtk
expressions, cannot be used as an objective, and must be removed and rebuilt when its mapping
changes.
An objective is selected with SetObjective. It must be a scalar Equal General or Linear
residual, at most one per environment. Objectives and constraints use separate, zero-based row
spaces and are consumed through EvaluateObjective/GetGradient and PackResiduals/GetJacobian
respectively.
- Declare the required variables, residuals and parameters as class members and keep their wrapper pointers.
- Add them to the environment with the
Add*APIs. - Read read-only snapshots with
GetInfo; change configuration through controlled APIs such asSetConfiguration,SetEnabled,SetObjectiveandSetNCPOptions. - Optionally mask ranges of variables or residuals with
PartiallyDisable, or enable/disable a whole entity withSetEnabled. - Call
FinalizeIndexesonce after the model is built; the environment assigns variable, residual and parameter indices. - Get the solver's initial point from
PackVariables, and bounds fromPackLowerBound/PackUpperBound. - Whenever the solver updates x, call
UnpackVariables, evaluate the residuals, thenPackResidualsorEvaluateObjectiveto refresh the value caches. - Call
ReserveNNZ, insert manual derivatives withInsert*DerivativeToJacobian, then consume them withGetJacobianorGetGradient. - For debugging, use
PrintValueandPrintJacobian; in debug buildsGenerateDebugTableproduces an entity table.
- Must be held by a
shared_ptr—ModelEnvderives fromstd::enable_shared_from_thisandFinalizeIndexes,Mergeand theAdd*APIs callshared_from_this. Stack allocation is forbidden and copying or moving is disabled. - Indices can be invalidated — adding, removing, enabling, resizing or changing structural
configuration invalidates indices; call
FinalizeIndexesagain before the next consumption. - Evaluation order — after the variable values change, constraints must be refreshed with
PackResidualsbeforeGetJacobian, and objectives withEvaluateObjectivebeforeGetGradient. AD, Complementarity and some Script residuals reuse the caches built byPackResiduals; the framework does not detect skipped evaluations or stale caches. - Concurrency — neither
ModelEnvnor its wrappers are thread-safe. Structural operations (Add,Remove,Merge,FinalizeIndexes,PartiallyDisable,Clear, ...) must run exclusively, and an evaluation cycle fromUnpackVariablestoGetJacobianmust not interleave with another. Only manual derivative production may be parallelized by the caller; concurrent writes to one residual require holdingresidual->triplet_mutex_, as the insertion APIs do not lock. Mergesemantics —Mergefolds another environment into this one and shares the wrappers; no copies are made andFinalizeIndexesmust be called afterwards. The two environments are not independent snapshots that can be interleaved.Fetchis disabled; there is no environment-to- environment copy.- Size limits — a single wrapper or slice holds at most 10,000 elements, and the total element count of variables, residuals and parameters in one environment is limited to 1,000,000.
- Return value semantics — the evaluation and derivative APIs returning
boolonly report whether structure, state and process satisfy the contract; they do not guarantee finite numbers. NaN/Inf produced by model values, transforms or floating-point arithmetic propagate to the output.
This repository does not currently ship an adapter that turns a ModelEnv into an NLE::Problem.
To integrate one, call UnpackVariables, PackResiduals and GetJacobian from the problem's
residual and Jacobian callbacks.
#include <Common/Math/ModelEnv/ModelEnv.hpp>
int main()
{
// ModelEnv must be held by a shared_ptr, never allocated on the stack
auto env = std::make_shared<ModelEnv::ModelEnv>();
// The variable storage is owned by the caller; the environment records the slice,
// transform, scaling and bounds
Eigen::VectorXd x(2);
auto x_var = env->AddVariables(&x, 0, 2, "x",
ModelEnv::Transform::Linear, 1.0, -10.0, 10.0);
// Constraint residual: the caller writes the lhs value, the environment applies
// sense, transform and scaling
Eigen::VectorXd rx_lhs(2);
auto rx_var = env->AddResiduals(&rx_lhs, ModelEnv::Sense::Equal, 0, 2, "rx");
if (VariablesInvalid(x_var) || ResidualsInvalid(rx_var))
return 1;
// Finalize once after the model is built, before packing or derivatives
if (!env->FinalizeIndexes())
return 1;
// Initial point for the solver; a value can also be assigned directly
Eigen::VectorXd x0(2);
env->PackVariables(x0);
x0 << 2.0, 2.0;
// Repeat this evaluation cycle whenever the solver updates x0
Eigen::VectorXd rx_packed(2);
env->UnpackVariables(x0); // write the iterate back into the variables
rx_lhs(0) = x(0) * x(0) + x(1) - 11.0;
rx_lhs(1) = x(0) + x(1) * x(1) - 7.0;
env->PackResiduals(rx_packed); // the constraint residuals handed to the solver
// Manual derivatives: reserve space, insert d(lhs)/d(x), then read the Jacobian
env->ReserveNNZ(4);
env->InsertDerivativeToJacobian(2.0 * x(0), ModelEnv::Side::Lhs, rx_var, 0, x_var, 0);
env->InsertDerivativeToJacobian(1.0, ModelEnv::Side::Lhs, rx_var, 0, x_var, 1);
env->InsertDerivativeToJacobian(1.0, ModelEnv::Side::Lhs, rx_var, 1, x_var, 0);
env->InsertDerivativeToJacobian(2.0 * x(1), ModelEnv::Side::Lhs, rx_var, 1, x_var, 1);
Eigen::MatrixXd jacobian(2, 2);
if (!env->GetJacobian(jacobian))
return 1;
return 0;
}Inserted derivatives are the raw partials without transform or scaling, and they ignore partial masking — the environment applies both.
Clone the repository and enter the directory that contains the solution file:
git clone https://github.com/LiuZhexuan/NLESolver.git
cd NLESolverBuild the test program with Visual Studio 2022:
& "$env:ProgramFiles\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\amd64\MSBuild.exe" `
.\NLESolver.sln `
/t:Build /p:Configuration=test_rel /p:Platform=x64 /mRun it:
& .\exe\NLESolver_mt_rel_x64.exeTo use NLESolver from your own C++ project, add the repository root and the include directory to
the header search path, and link:
lib\NLESolver_mt_rel_x64.lib
lib\Common_mt_rel_x64.lib
For the DLL configuration use dll_rel or dll_dbg; at run time the generated NLESolver DLL must be
on a path the program can search.
Common/
EigenRelated/ linear algebra and matrix containers
Math/ Jacobians, line search, Dogleg, trust region, scaling, ...
Math/ModelEnv/ modeling environment: variables, residuals, parameters, transforms,
scaling and Jacobian assembly
Json/ Eigen <-> JSON helpers
Utilities/ bounds checks, logging, export macros and shared utilities
NLESolver/
Interface/ Problem, Options, Summary and the public factory interfaces
Solvers/ Newton, Dogleg, quasi-Newton and scalar solvers
FixedPoint/ fixed-point solvers
ExampleAndTest/ example problems and the combined test program
include/ vendored third-party header libraries (see THIRD_PARTY_NOTICES.md)
docs/ local patch records for vendored libraries
Files under Common/Math/ModelEnv:
| File | Contents |
|---|---|
ModelEnv.hpp, ModelEnv-inl.hpp |
the environment: entity add/remove/find, masking, indices, packing and derivative assembly |
Variables.hpp |
variable wrapper |
Residuals.hpp |
residual wrapper and General residuals |
Parameters.hpp |
parameter wrapper |
LinearResiduals.hpp |
linear residuals |
ADResiduals.hpp |
automatic-differentiation residuals |
Script.hpp |
exprtk expression residuals |
Complementarity.hpp |
complementarity (NCP) residuals |
Transform.hpp, Utilities.hpp |
transform/scaling adapters and internal helpers |
The project targets Windows, Visual Studio 2022, MSVC v143, C++17 and x64. The required Eigen, fmt,
spdlog, magic_enum, autodiff and exprtk headers are vendored in include/, so no package manager is
needed; the Visual Studio C++ toolset, the Windows SDK and OpenMP support are still required.
autodiff backs ModelEnv::ADResiduals and exprtk backs ModelEnv::Script.
| Vendored library | Upstream | Version / commit | Pinned | License |
|---|---|---|---|---|
| Eigen | https://gitlab.com/libeigen/eigen | 5.0 (5.0.0-dev+master snapshot) |
2025-09-21 | MPL-2.0 |
| fmt | https://github.com/fmtlib/fmt | 10.0.0 |
2023-05-10 | MIT |
| spdlog | https://github.com/gabime/spdlog | 1.12.0 |
2023-07-11 | MIT |
| nlohmann/json | https://github.com/nlohmann/json | b451735fe7bb3283336f934b3f6cd3f484f73649 |
2025-06-24 | MIT |
| magic_enum | https://github.com/Neargye/magic_enum | a733a2ea665ca5d72b7270f0334bf2e7b82bd0cc |
2025-06-24 | MIT |
| ExprTk | https://github.com/ArashPartow/exprtk | no tag (snapshot) | 2023-09-07 | MIT |
| autodiff | https://github.com/autodiff/autodiff | 1.1.2 |
— | MIT |
Each library keeps its license text inside its include/<library>/ directory. See
THIRD_PARTY_NOTICES.md for the license summary and the upgrade procedure,
and docs/LOCAL_PATCHES.md for the patches applied to spdlog,
nlohmann/json, magic_enum and autodiff.
The solution configuration selects how the code is built:
lib_mt_rel/lib_mt_dbg— static library,/MT.lib_md_rel/lib_md_dbg— static library,/MD.dll_rel/dll_dbg— NLESolver dynamic library,/MD.test_rel/test_dbg— NLESolver test program;Commonmaps to the matching static library configuration.
To build the static libraries only, change the configuration:
/p:Configuration=lib_mt_relThe output files are:
lib\Common_mt_rel_x64.lib
lib\NLESolver_mt_rel_x64.lib
For debug builds replace rel with dbg; for the dynamic library use dll_rel or dll_dbg.
#include <NLESolver/Interface/INLESolver.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/spdlog.h>
int main()
{
auto logger = spdlog::stdout_color_mt("nle");
NLE::InitializeSolverLogger(logger);
auto problem = NLE::CreateFunctorProblem(
2,
[](const auto& x, auto rx) -> bool {
rx(0) = x(0) * x(0) + x(1) - 11.0;
rx(1) = x(0) + x(1) * x(1) - 7.0;
return true;
},
[](const auto& x, auto jacobian) -> bool {
jacobian << 2.0 * x(0), 1.0,
1.0, 2.0 * x(1);
return true;
});
auto solver = NLE::CreateSolver(NLE::SolverType::ExactDogleg, problem);
Eigen::VectorXd x(2);
x << 2.0, 2.0;
NLE::Options options;
NLE::Summary summary;
const auto status = solver->Solve(x, options, summary);
const bool converged =
status == NLE::SolverReturnStatus::SolutionReached ||
status == NLE::SolverReturnStatus::UserSuccess;
return converged ? 0 : 1;
}Without an analytic Jacobian, pass nullptr as the third argument and the solver computes a
numerical Jacobian according to Options::numerical_jacobian_method_ and the difference step.
For complex problems, derive from NLE::Problem and implement the residual function and
GetJacobian. Put problem bounds in lower_bound_ and upper_bound_, and use
fixed_variables_indexes_ with fixed_variables_values_ for fixed variables rather than identical
bounds.
Check both the return status and the final residual:
if (status == NLE::SolverReturnStatus::SolutionReached ||
status == NLE::SolverReturnStatus::UserSuccess) {
// converged
} else if (!NLE::IsSolverReturnErr(status)) {
// non-fatal exits such as MaxIterCountReach, StuckInLocalMinimum, CloseToBound
} else {
// errors such as BoundIllegal, JacobianNotNormal, StepLengthTooSmall
}InitializeSolverLogger must be called before CreateSolver or Solve. In DLL mode, memory for
step information must be pre-allocated as required by the Summary interface, and the NLESolver DLL
must be on a path the program can search.
NLESolver/ExampleAndTest/NLESolverTest.cpp contains the NLE, scalar and fixed-point tests. Run
them directly:
& .\exe\NLESolver_mt_rel_x64.exe > .\build\nlesolver-test.logThe process exit code only indicates that the test program ran to completion; it does not mean every algorithm converged from every starting point.
This project is licensed under the MIT License — see LICENSE.
Vendored third-party libraries remain under their own licenses; see
THIRD_PARTY_NOTICES.md and the license file in each include/ directory.