Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

POUNCE solve-report schema, v1

Schema tag: pounce.solve-report/v1

This document is the canonical reference for the JSON solve report emitted by pounce --json-output PATH and pounce_sens --json-output PATH. The report carries everything an AMPL .sol file holds — status, primal x, dual lambda, suffix blocks — plus FAIR-aligned provenance metadata and (optionally) the per-iteration trajectory.

Implementation: the serde structs live in crates/pounce-solve-report/src/lib.rs (per-iteration IterRecord in crates/pounce-nlp/src/solve_statistics.rs); crates/pounce-cli/src/solve_report.rs wires them to the CLI.

Why a structured solve report?

Production NLP workflows often need to (a) capture which solve produced which numbers for audit / reproducibility, (b) feed solver output into downstream tooling (notebooks, dashboards, ML pipelines) that don’t want to parse a free-form .sol file, and (c) compare runs across versions of pounce. Both upstream Ipopt’s stdout summary and AMPL’s .sol were designed for human consumption and AMPL’s reader respectively — neither carries provenance metadata, neither is schema-versioned, and neither is trivially machine-parseable across ecosystems.

A versioned JSON schema with FAIR-aligned provenance solves all three.

FAIR alignment

The fair_metadata block maps onto the four FAIR principles (Wilkinson et al. 2016, “The FAIR Guiding Principles for scientific data management and stewardship”, Scientific Data 3, 160018, DOI 10.1038/sdata.2016.18; citation verified via Crossref on 2026-05-14):

PrincipleMapping in this schema
Findableresult_id (<unix_nanos>-<pid>, globally unique and time-ordered), created_at_iso, created_at_unix_nanos.
AccessiblePlain-text JSON on disk; no protocol gating; UTF-8. Same trust model as the .sol file.
InteroperableSchema-versioned (pounce.solve-report/v1); JSON primitives only (no binary blobs); units documented per-field below; solution.status is the enum-variant string for cross-language consumption.
Reusablesolver (name + version + git commit + target triple), license, input (kind + path + size) capture enough provenance to reproduce a solve.

Versioning policy

schema is the version tag. Compatibility rules:

  • Adding fields is non-breaking. Consumers MUST tolerate unknown fields. New optional fields land between versions; the major version doesn’t bump.
  • Removing or renaming fields bumps the major version (v1v2). Consumers should pin against a major version (schema starts_with "pounce.solve-report/v1").
  • Changing field semantics without a rename is forbidden. If semantics need to change, add a new field and deprecate the old.

The pre-1.0 phase of POUNCE itself does NOT relax this rule for the schema. Once a solve-report version ships, its field set is frozen even while the rest of the solver is under churn.

Top-level shape

{
  "schema": "pounce.solve-report/v1",
  "fair_metadata": { ... },
  "problem":       { ... },
  "solution":      { ... },
  "statistics":    { ... },
  "iterations":    [ ... ],  // optional, omitted when empty
  "linear_solver": { ... }   // optional, omitted when backend did not report
}

Fields

schema (string, required)

Identifier for this schema version. Always "pounce.solve-report/v1" for v1. Major-version bumps change the prefix; minor / patch (additive) changes do not.

fair_metadata (object, required)

FieldTypeNotes
result_idstringFormat: <unix_nanos>-<process_id>. Monotonically ordered within a process, globally unique across processes. No external UUID library needed.
created_at_isostringSolve start time as ISO-8601 UTC: YYYY-MM-DDTHH:MM:SS.sssZ.
created_at_unix_nanosintegerSame instant as Unix nanoseconds since 1970-01-01 UTC. Provided alongside the ISO string for consumers that prefer integer arithmetic.
elapsed_secondsfloatWallclock seconds the solve took (matches statistics.total_wallclock_time_secs modulo float precision).
solverobjectSee below.
licensestringSPDX identifier. Always "EPL-2.0" for this version.
inputobjectSee Input descriptor below.

solver sub-object

FieldTypeNotes
namestringAlways "pounce".
versionstringCrate version (e.g. "0.1.0"). Read from CARGO_PKG_VERSION at build time.
git_commitstring | omittedBuild-time git revision. Omitted when the build environment did not set POUNCE_GIT_COMMIT (e.g. development builds). Set via POUNCE_GIT_COMMIT=$(git rev-parse HEAD) cargo build to populate.
target_triplestringBuild target triple (e.g. "x86_64-apple-darwin"); falls back to "unknown" when Cargo did not expose TARGET at build time.

Input descriptor (input)

Tagged enum keyed on kind. Possible shapes:

{ "kind": "nl-file", "path": "/path/to/foo.nl", "size_bytes": 366 }
{ "kind": "builtin", "name": "rosenbrock" }
{ "kind": "tnlp-direct" }
  • nl-file — the input came from .nl file at path. size_bytes is present when the file’s metadata is readable; consumers that want bit-exact provenance can hash the file themselves.
  • builtin — the input was a built-in problem named by name (e.g. pounce --problem rosenbrock).
  • tnlp-direct — used by library callers building a TNLP in-process without a .nl round-trip.

problem (object, required)

Problem dimensions reported by the TNLP at get_nlp_info().

FieldTypeNotes
n_variablesintegerNumber of primal variables.
n_constraintsintegerNumber of constraints (equalities + inequalities).
n_objectivesintegerNumber of objectives. The IPM uses objective 0; extras are read but ignored.
minimizebooleantrue for minimization (the AMPL default).
nnz_jac_ginteger | omittedNumber of declared non-zeros in the constraint Jacobian.
nnz_h_laginteger | omittedNumber of declared non-zeros in the lower triangle of the Lagrangian Hessian.

solution (object, required)

FieldTypeNotes
statusstringApplicationReturnStatus enum variant name verbatim (e.g. "SolveSucceeded", "MaximumIterationsExceeded").
solve_result_numintegerAMPL-style solve-result code (Gay 2005, “Hooking Your Solver to AMPL” §5, p. 23 table): 0 = solved, 100-range = warning, 200-range = infeasible, 400-range = limit reached, 500-range = failure.
objectivefloatFinal unscaled objective value. 0.0 (not NaN) when the solve never completed; check statistics.iteration_count > 0 to distinguish.
xarray of float | emptyPrimal vector, length problem.n_variables. Empty when the binary doesn’t capture the final iterate (currently: pounce on the newton_driver fast-path). Omitted from JSON when empty.
lambdaarray of float | emptyConstraint multipliers, length problem.n_constraints. Same omission convention as x.
suffixesarray of object | emptysIPOPT-style suffix blocks; emitted only at --json-detail full. See below.

Suffix entries

{
  "name": "sens_sol_state_1",
  "target": "var",
  "kind": "real",
  "values": [0.576..., 0.378..., -0.046..., 4.5, 1.0]
}
FieldTypeNotes
namestringAMPL suffix name.
targetstringOne of "var", "con", "obj", "problem". Matches AMPL’s Sufkind_* enum.
kindstring"real" or "int". Selects which payload array is populated.
valuesarray of floatDense values (length = target dimension). Present when kind = "real".
int_valuesarray of integerPresent when kind = "int".

statistics (object, required)

Projection of pounce_nlp::solve_statistics::SolveStatistics minus the per-iteration history (which lives at the top level when present).

FieldTypeNotes
iteration_countintegerNumber of accepted outer iterations.
final_objectivefloatUnscaled. Matches solution.objective.
final_scaled_objectivefloatScaled by the IPM’s internal NLP scaling. Equal to final_objective when no scaling is in effect.
final_dual_inffloat`
final_constr_violfloat`
final_complfloatMax complementarity over the four bound blocks.
final_kkt_errorfloatOverall KKT error reported by the convergence check.
num_obj_evalsintegereval_f call count.
num_constr_evalsintegereval_g call count.
num_obj_grad_evalsintegereval_grad_f count.
num_constr_jac_evalsintegereval_jac_g count.
num_hess_evalsintegereval_h count.
total_wallclock_time_secsfloatWall time spent inside optimize_*.
restoration_callsintegerNumber of restoration-phase entries (pounce#12).
restoration_inner_itersintegerCumulative inner-IPM iterations across all restoration calls.
restoration_outer_itersintegerOuter iterations that ran in restoration mode (R-line equivalents).
restoration_wall_secsfloatWall time spent inside perform_restoration.

Eval counters (num_*_evals) populate only on the .nl-file path because the pounce binary’s CountingTnlp wrapper tracks them. Library callers using IpoptApplication::optimize_tnlp directly see zeros there; the underlying counts are still available through upstream’s IpoptCalculatedQuantities if needed.

iterations (array of object, optional)

Per-iteration trajectory. Emitted only at --json-detail full (when IpoptApplication::enable_iter_history() was called). Omitted from JSON entirely when empty.

Each row maps to one line of the upstream-formatted console iter table. Fields:

FieldTypeNotes
iterinteger0-based iteration index.
objectivefloatf(x_k) at the start of iter k (unscaled).
inf_prfloatPrimal infeasibility `
inf_dufloatDual infeasibility `
mufloatBarrier parameter μ_k (not log10; consumers can take log10 if they want the console format).
d_normfloat`
regularizationfloatHessian regularization δ_w applied this iter; 0.0 when none was needed.
alpha_dualfloatDual step length.
alpha_primalfloatPrimal step length.
alpha_primal_charstring (1 char)Single-character tag (f, h, r, etc.) matching the alpha-primal column of upstream’s iter table.
ls_trialsintegerNumber of backtracking line-search trials this iter.

linear_solver (object, optional)

Aggregate post-mortem from the symmetric-indefinite linear backend that solved the KKT systems. Populated only when the backend self-instruments (the default FERAL backend does; HSL MA57 and custom backends plugged through set_linear_backend_factory do not). Omitted from JSON when no backend reported.

FieldTypeNotes
solver_namestringBackend identifier (e.g. "feral").
n_factorsintegerTotal numeric factorizations performed.
n_pattern_reuseintegerFactor calls that reused the existing symbolic pattern.
n_pattern_changesintegerFactor calls that triggered a re-analysis.
max_fill_ratiofloat | omittedPeak nnz(L) / nnz(A) observed across all factorizations.
min_abs_pivotfloat | omittedSmallest absolute pivot magnitude seen across all factorizations (diagnostic for near-singularity).
max_abs_pivotfloat | omittedLargest absolute pivot magnitude.
last_inertia[int, int, int] | omitted(positive, negative, zero) inertia of the final factor. Should match (n, m, 0) at a regular KKT optimum.
last_nnz_ainteger | omittedNon-zero count of the assembled KKT matrix at the final factor.
last_nnz_linteger | omittedNon-zero count of the L-factor at the final factor.

Detail levels

The --json-detail LEVEL flag selects how much detail is emitted. Levels map to verbosity in the same spirit as upstream’s print_level (0 silent → 12 maximum debug):

LevelWhat’s emittedWhat’s omitted
summary (default)FAIR metadata, problem, solution scalars + arrays, aggregate statisticsiterations, solution.suffixes
fullAll of the above plus per-iteration trajectory and suffix blocksnothing — full detail

summary is the right choice for production logs and batch runs. full is the debugging equivalent of upstream’s print_level=8.

Worked example

pounce_sens crates/pounce-cli/tests/fixtures/parametric.nl out.sol --json-output result.json --json-detail full produces (truncated for brevity):

{
  "schema": "pounce.solve-report/v1",
  "fair_metadata": {
    "result_id": "1778777029606881000-76543",
    "created_at_iso": "2026-05-14T16:43:49.606Z",
    "created_at_unix_nanos": 1778777029606881000,
    "elapsed_seconds": 0.011,
    "solver": {
      "name": "pounce",
      "version": "0.1.0",
      "target_triple": "x86_64-apple-darwin"
    },
    "license": "EPL-2.0",
    "input": {
      "kind": "nl-file",
      "path": "crates/pounce-cli/tests/fixtures/parametric.nl",
      "size_bytes": 366
    }
  },
  "problem": { "n_variables": 5, "n_constraints": 4, "n_objectives": 1, "minimize": true },
  "solution": {
    "status": "SolveSucceeded",
    "solve_result_num": 0,
    "objective": 0.5510204081632656,
    "x":      [0.6326530575201161, 0.3877551079678144, 0.020408165487930466, 5.0, 1.0],
    "lambda": [-0.16326530000405073, -0.28571431357898697, -0.16326530000405073, 0.18075803406303625],
    "suffixes": [{
      "name": "sens_sol_state_1",
      "target": "var",
      "kind": "real",
      "values": [0.5765305974643309, 0.3775510440570709, -0.04591835847859835, 4.5, 1.0]
    }]
  },
  "statistics": { "iteration_count": 9, "final_dual_inf": 2.89e-14, "...": "..." },
  "iterations": [
    { "iter": 0, "objective": 0.0451, "inf_pr": 5.0, "inf_du": 0.407, "mu": 0.1,
      "d_norm": 0.0, "regularization": 0.0, "alpha_dual": 0.0, "alpha_primal": 0.0,
      "alpha_primal_char": " ", "ls_trials": 0 },
    { "iter": 1, "objective": 0.957, "inf_pr": 0.212, "...": "..." }
  ]
}

Consumer guidance

  • Pin the major version. Check schema.startswith("pounce.solve-report/v1") before consuming.
  • Tolerate unknown fields. New optional fields will land between minor versions of pounce. Use serde(default) / equivalent.
  • Distinguish “no solve” from “solve produced zero”. Pre-solve, scalar fields are 0.0 (not NaN, because JSON has no NaN literal). statistics.iteration_count == 0 is the signal that no solve occurred.
  • solution.x / solution.lambda may be empty. When the binary couldn’t capture the final iterate (currently: the pounce binary on its newton_driver fast-path for m=0, n≤1000 problems), the arrays are empty and the keys are omitted from JSON entirely. pounce_sens always populates them.

References

  • Wilkinson et al. (2016). “The FAIR Guiding Principles for scientific data management and stewardship.” Scientific Data 3, 160018. DOI 10.1038/sdata.2016.18. (Verified via Crossref 2026-05-14.)
  • Gay (2005). “Hooking Your Solver to AMPL.” https://ampl.com/REFS/hooking2.pdf. §5 (Returning Results to AMPL) for the .sol baseline this schema is structured around.
  • SPDX license identifiers: https://spdx.org/licenses/.