Skip to main content

KRONOS

An algorithm for solving ill-conditioned nonlinear programs.

KRONOS solves

$$ \begin{aligned} \min_{x} \quad & f(x) \ \textrm{s.t.} \quad & h(x) = 0 \ & g(x) \le 0 \ & x^{L} \le x \le x^{U} \end{aligned} $$

by forming the full KKT system and driving its residual to zero iteratively, taking the minimum-norm least-squares solution of the KKT linear system as the step. Inequalities and variable bounds are converted to equalities using squared slack variables.

Each returned point is classified as first-order KKT certified, certified with the second-order sufficient conditions verified, or uncertified.

The method is described in KRONOS: An algorithm for solving ill-conditioned nonlinear programs; see Citation.

Install

pip install kronos-v1

The package imports as kronos. It requires Python 3.9 or later and installs numpy, scipy, sympy and casadi.

Optional extras add matplotlib for figures, or JAX as a third backend:

pip install "kronos-v1[plot]"
pip install "kronos-v1[all]"

To install the current state of the main branch instead of the last release:

pip install git+https://github.com/SOULS-TAMU/KRONOS.git

For development:

git clone https://github.com/SOULS-TAMU/KRONOS.git
cd KRONOS
pip install -e ".[dev]"
pytest -q

Verify the installation:

import kronos
print(kronos.__version__, len(kronos.problem_names()))     # 0.4.7 244

Quick start

Solve one of the built-in problems:

from kronos import load_problem, solve

p = load_problem("hs053")
r = solve(p, multi_start=True, ms_num_starts=25)
print(r.summary())

Define a problem by supplying the objective, the constraints and the bounds.

from kronos import Problem, solve

p = Problem.build(
    "my_problem", ["x1", "x2", "x3", "x4"],
    objective    = "x1*x4*(x1 + x2 + x3) + x3",
    equalities   = ["x1**2 + x2**2 + x3**2 + x4**2 - 40"],   # == 0
    inequalities = ["25 - x1*x2*x3*x4"],                     # <= 0
    lb = [1, 1, 1, 1],
    ub = [5, 5, 5, 5],
    x0 = [1, 5, 5, 1],
)

r = solve(p, multi_start=True, ms_num_starts=25)
print(r.fval)      # 17.014017
print(r.theta[:4]) # [1.0, 4.743, 3.8211503, 1.3794082]

Set inequality_sense=">=" for constraints of the form g(x) >= 0, or pass a list with one entry per row. Expressions may be given as strings or as SymPy objects. Problems can be written to and read from JSON with p.save(path) and Problem.load(path).

Reading the result

r.summary() reports the outcome:

==============================================================
  KRONOS  |  hs053   n=15  m=13
==============================================================
  converged   : 25/25  (100.0%)
  reached f*  : 25/25  (100.0%)
  time        : 0.659 s   (0.026 s/run)
  ---- best run ----
  status      : converged (KKT certified)
  objective   : 4.093023256      f* = 4.093
  x*          : theta1     = -0.7674418605
                theta2     = 0.2558139535
                theta3     = 0.6279069767
                theta4     = -0.1162790698
                theta5     = 0.2558139535
  iterations  : 3
==============================================================

The first three lines summarise all the multistart runs; everything below best run describes the single run that produced the reported solution. Its status states whether that run is KKT certified, merely stationary for the reformulated system, or did not converge at all, so a reported objective is never mistaken for a solution.

x* lists the problem's own variables; the slack variables introduced for the inequalities and bounds are internal and not shown.

Converged means KKT certified: the residual test is satisfied and the inequality multipliers have the correct signs. Because inequalities are carried as squared slacks, a run can drive the residual to zero and still end with a wrong-signed multiplier. Such a point is stationary for the reformulated system but is not a KKT point of the original problem, and is not counted as converged. That looser count is available separately:

r.n_conv                        # converged, i.e. KKT certified
r.n_reformulated_stationary     # stationary for the reformulated system

The report is plain ASCII with no colour codes or terminal control characters, so it appears identically in a terminal, in a Jupyter or VS Code notebook, and in a redirected file. Lines are at most 62 characters.

Supplying a known optimum

If the optimal objective value is known in advance, supply it as fstar. The report then adds how many runs reached it, and r.global_hits() becomes available. Without it those lines are omitted, and everything else is unchanged: fstar is used only for reporting and never influences the solve.

Three equivalent ways:

p = Problem.build(..., fstar=17.0140173)    # stored with the problem
r = solve(p, fstar=17.0140173)              # for one solve
print(r.summary(fstar=17.0140173))          # for one report

A run counts as having reached f* when it is certified and

|f - f*| <= max(1e-4, 1e-3 * max(1, |f*|))

so the test is absolute for small optima and relative for large ones. Query it directly with:

r.global_hits(17.0140173)      # number of certified runs that reached it

The built-in problems already carry their known optima, so load_problem supplies fstar automatically.

The same quantities are available programmatically:

attribute meaning
r.fval, r.theta best objective and the corresponding point
r.n_conv converged and certified runs (r.n_kkt is an alias)
r.n_residual_conv runs satisfying the residual test, certified or not
r.global_hits(fstar) certified runs that reached a known optimum
r.runs one RunResult per multistart run
r.all_fvals, r.all_conv, r.all_kkt per-run arrays

Each RunResult provides converged, reformulated_stationary, min_lam_strict, iterations, max_r and max_h.

Strict local minima

A certified KKT point is not necessarily a minimum; it can be a maximum or a saddle. The second-order sufficient condition separates them, and is checked automatically. To count how many runs are proven minima:

r = solve(p, multi_start=True, ms_num_starts=25)
print(r.n_local)          # runs proven to be strict local minima

The gap between n_conv and n_local splits two ways:

print(f"converged {r.n_conv}, minima {r.n_local}, "
      f"failed SOSC {r.n_stationary}, untested {r.n_sosc_unmeasured}")
attribute meaning
r.n_local certified and second-order test passed
r.n_stationary certified, test ran, point is not a minimum
r.n_sosc_unmeasured certified, test not performed

Those three always sum to r.n_conv. Runs solved by the Fischer-Burmeister formulation fall into the third category, because that path does not form a reduced Hessian; not measured is not the same as failed.

Per run, the same information is available as sosc_pass, sosc_measured and lam_min_red, the last being the smallest eigenvalue of the Hessian of the Lagrangian restricted to the null space of the active constraint Jacobian. A positive value means a strict local minimum:

b = r.runs[r.best_run]
b.sosc_pass, b.sosc_measured, b.lam_min_red

check_sosc=False skips the test; sosc_tol sets the eigenvalue threshold.

Built-in problems

244 problems are included, drawn from the Hock-Schittkowski collection, CUTEst, and standard global-optimisation test functions. They range from 2 to 1000 variables and each carries its known optimum.

from kronos import problem_names, find, load_problem

problem_names()                        # all 244
find("hs")                             # search by name
find(max_n=10, constrained=True)       # filter by size and constraints
find(constrained=False)                # unconstrained only
p = load_problem("hs110")
kronos list                            # full list with n, m and f*
kronos list --max-n 10

Sizes: 2-4: 97, 5-9: 72, 10-19: 41, 20-49: 18, 50+: 16. Of these, 96 are unconstrained.

a01_beale              a02_bohachevsky1       a03_bohachevsky2       a04_bohachevsky3       a05_branin_rcos
a06_colville           a07_dixon_price        a08_hump               a09_matyas             a10_perm
a11_powell_singular    a13_sphere             a14_sum_squares        a15_trid               a16_zakharov
a17_branin_rcos2       a18_ackley1            a19_ackley2            a20_camel3             a21_booth
a22_brown              a24_exponential        a25_freudenstein_roth  a26_miele_cantrell     a27_quadratic
a28_rotated_ellipse    a29_rump               aircrfta               aircrftb               aircrtfb
allinitu               alsotame               argauss                arglina                arglinb
arglinc                avgasa                 b03                    b06                    b08
b09                    b11                    b12                    b13                    b14
b15                    b16                    b17                    b18                    b22
b24                    b30                    bard                   bdvalue                beale
biggs3                 biggs5                 biggs6                 booth                  box2
box3                   bqp1var                brkmcc                 brownal                brownden
bt1                    bt10                   bt11                   bt12                   bt13
bt2                    bt3                    bt5                    bt6                    bt7
bt8                    byrdsphr               camel6                 catena                 chnrosnb
cliff                  cluster                coolhans               cube                   denschna
denschnb               denschnc               denschnd               denschne               denschnf
dixchlng               dixon3dq               eg1                    eigencco               eigminc
engval2                errinros               expfit                 extrasim               fccu
fletcbv2               fletchcr               genhs28                genhumps               gottfr
gridneth               growth                 growthls               hairy                  hatfldc
hatfldd                hatflde                hatfldf                hatfldg                heart6
heart8                 helix                  hilberta               hilbertb               himmelba
himmelbb               himmelbc               himmelbe               himmelbf               himmelbg
himmelbh               himmelp1               hong                   hs001                  hs003
hs004                  hs005                  hs006                  hs007                  hs008
hs009                  hs010                  hs012                  hs014                  hs015
hs017                  hs018                  hs019                  hs020                  hs023
hs026                  hs027                  hs028                  hs029                  hs030
hs031                  hs032                  hs033                  hs034                  hs038
hs040                  hs042                  hs043                  hs046                  hs047
hs048                  hs049                  hs050                  hs051                  hs052
hs053                  hs054                  hs055                  hs056                  hs060
hs061                  hs063                  hs065                  hs066                  hs071
hs077                  hs078                  hs079                  hs080                  hs081
hs100nlp               hs107                  hs108                  hs110                  hs113
hs3mod                 humps                  hypcir                 jensmp                 kowosb
lch                    lewispol               loghairy               lotschd                lsnnodoc
madsen                 maratos                mconcon                mexhat                 mwright
nasty                  orthregb               osbornea               oslbqp                 palmer1c
palmer1d               palmer2c               palmer3c               palmer3e               palmer4c
palmer5b               palmer5c               palmer5d               palmer6c               palmer6e
palmer7c               palmer7e               palmer8c               palmer8e               penalty2
pfit4ls                pk3                    powellsq               power                  pspdoc
qrtquad                rk23                   rosenbr                sim2bqp                simbqp
sineval                sisser                 ssnlbeam               supersim               tame
tointqor               try_b                  twobars                vardim                 yfit
yfitu                  zangwil2               zangwil3               zecevic3

Plots

from kronos.plotting import plot_runs
fig = plot_runs(r, fstar=p.fstar)      # per-run objective, coloured by status
fig.savefig("runs.png", dpi=150)

Green marks converged (KKT-certified) runs, amber runs that met the residual test without certification, and grey failed runs.

Command line

kronos list                            # built-in problems
kronos solve hs110 -K 25 --plot runs.png
kronos bench hs001 hs053 -K 25 --out bench.csv
kronos options                         # every setting, grouped

Options

Options may be passed as keyword arguments to solve or collected in an Options object. The convergence tolerances are the most commonly adjusted:

r = solve(p, tol_r=1e-8, tol_h=1e-8, maxIter=5000)   # default is 1e-5
from kronos import Options
opts = Options(tol_r=1e-8, multi_start=True, ms_num_starts=50)
r = solve(p, opts)
option default meaning
tol_r, tol_h 1e-5 tolerances on the KKT residual and the constraint violation
maxIter 2000 Newton iterations per run
multi_start False solve from several starting points
ms_num_starts, ms_seed 25, 42 number of starting points and the random seed
use_adam_warmup True first-order warm-up before Newton
adam_mode "C" how the warm-up is applied across starting points, see below
backend "auto" SymPy below 20 variables, CasADi at or above
fstar None known optimum, enabling the reached f* lines
max_kkt_kicks 5 retries when a run converges without certification

When many runs converge without being certified, increasing max_kkt_kicks often helps.

adam_mode

The solve has three stages: a first-order warm-up, a pre-feasibility pass, and the main Newton solve. With K starting points, adam_mode decides how the first two are distributed across them.

mode warm-up pre-feasibility main solve
"A" every starting point first point only one call receiving all K points
"B" first point only first point only one call, which scatters K fresh points around it
"C" every starting point every starting point one call per starting point

"C" is the default and the most thorough: each starting point goes through the whole pipeline independently. "B" is the cheapest, but note that it scatters its K-1 remaining points after the warm-up, so those never benefit from it.

The choice matters on harder problems. On dixchlng, for example, "A" and "C" reach the known optimum from all 25 starts while "B" reaches it from one.

All 58 options are documented in place:

from kronos import Options
print(Options.describe())              # all options
print(Options.describe("Convergence")) # a single group
kronos options
kronos options certification

The groups are Convergence, Multistart, Line search and robustness, Warm-up and pre-feasibility, Certification, Numerics, Backend, Output, and Advanced multiplier handling.

Backends

With backend="auto" the solver uses SymPy below 20 variables and CasADi at or above that threshold. JAX is available as a third option. The backends produce mathematically equivalent derivatives; floating-point evaluation differs between them, so a run may follow a slightly different trajectory.

from kronos import get_backend
b = get_backend(p, "casadi")     # compile once
r = solve(p, backend=b)          # reuse across repeated solves

Compiling the derivatives is often the dominant one-off cost, so reusing a backend is worthwhile when solving the same problem many times.

When running several solves in parallel, limit BLAS threads to avoid oversubscription:

OMP_NUM_THREADS=1 python my_script.py

Citation

Ahmed, M G T. and Hasan, M. M. F. (2026) 'KRONOS: An algorithm for solving ill-conditioned nonlinear programs', Computers & Chemical Engineering, 215, 109839. doi: 10.1016/j.compchemeng.2026.109839.

M G Toufik Ahmed, M.M. Faruque Hasan,
KRONOS: An algorithm for solving ill-conditioned nonlinear programs,
Computers & Chemical Engineering,
Volume 215,
2026,
109839,
ISSN 0098-1354,
https://doi.org/10.1016/j.compchemeng.2026.109839.
@article{ahmed2026kronos,
  title   = {KRONOS: An algorithm for solving ill-conditioned nonlinear programs},
  author  = {Ahmed, M G Toufik and Hasan, M. M. Faruque},
  journal = {Computers \& Chemical Engineering},
  volume  = {215},
  pages   = {109839},
  year    = {2026},
  issn    = {0098-1354},
  doi     = {10.1016/j.compchemeng.2026.109839},
}

Tutorial

A worked introduction is provided in docs/tutorial.ipynb, covering problem definition, the certification output, backends, plotting and the built-in problem set.

Tests

pytest -q

License

MIT. See LICENSE.

Copyright (c) 2026 M G Toufik Ahmed and M. M. Faruque Hasan.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

kronos_v1-0.4.7.tar.gz (371.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

kronos_v1-0.4.7-py3-none-any.whl (507.5 kB view details)

Uploaded Python 3

File details

Details for the file kronos_v1-0.4.7.tar.gz.

File metadata

  • Download URL: kronos_v1-0.4.7.tar.gz
  • Upload date:
  • Size: 371.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.7

File hashes

Hashes for kronos_v1-0.4.7.tar.gz
Algorithm Hash digest
SHA256 aec4105995b6ad5b21c65461ba5c0af2ec287aaea9831debc55015ccd31c7054
MD5 ed68f3e3db0ca796f17025db1eea7d33
BLAKE2b-256 017a37f1ae38fe219b80ea278c2e31422c5ab236752fc13824b8c4db095b167a

See more details on using hashes here.

File details

Details for the file kronos_v1-0.4.7-py3-none-any.whl.

File metadata

  • Download URL: kronos_v1-0.4.7-py3-none-any.whl
  • Upload date:
  • Size: 507.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.7

File hashes

Hashes for kronos_v1-0.4.7-py3-none-any.whl
Algorithm Hash digest
SHA256 d9e0572a41a23a2262b3794b094c8f1937b61cc0cebb5ea786981144ec7b65e5
MD5 aa736aae8bc48f0b9fd8dde36eb5c1c8
BLAKE2b-256 52ce44085a14b3e04a7e095f00a13ad237bcf4bca22df347fbd372ebd031f21c

See more details on using hashes here.

Release history Release notifications | RSS feed

0.4.8

2 files

This release

0.4.7 This release

2 files

0.4.6

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page