1use serde::{Deserialize, Serialize};
26
27pub const SOLVE_REPORT_SCHEMA: &str = "pounce.solve-report/v1";
30
31#[derive(Debug)]
32pub enum Error {
33 Json(serde_json::Error),
35 SchemaMismatch { found: String },
37 IterOutOfRange { k: usize, n: usize },
39 NoIterations,
42 IterDump(String),
45}
46
47impl std::fmt::Display for Error {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 match self {
50 Self::Json(e) => write!(f, "invalid JSON: {e}"),
51 Self::SchemaMismatch { found } => write!(
52 f,
53 "unexpected schema {found:?} (expected {SOLVE_REPORT_SCHEMA:?})",
54 ),
55 Self::IterOutOfRange { k, n } => {
56 write!(f, "iter {k} out of range; report has {n} iterations")
57 }
58 Self::NoIterations => write!(
59 f,
60 "report has no iteration history (rerun with --json-detail full)",
61 ),
62 Self::IterDump(msg) => write!(f, "iter-dump parse error: {msg}"),
63 }
64 }
65}
66
67impl std::error::Error for Error {
68 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
69 match self {
70 Self::Json(e) => Some(e),
71 _ => None,
72 }
73 }
74}
75
76impl From<serde_json::Error> for Error {
77 fn from(e: serde_json::Error) -> Self {
78 Self::Json(e)
79 }
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct SolveReport {
84 pub schema: String,
85 pub fair_metadata: FairMetadata,
86 pub problem: ProblemInfo,
87 pub solution: SolutionInfo,
88 pub statistics: StatisticsInfo,
89 #[serde(default)]
90 pub iterations: Vec<IterRecord>,
91 #[serde(default)]
95 pub linear_solver: Option<LinearSolverSummaryInfo>,
96}
97
98impl SolveReport {
99 pub fn from_json_slice(bytes: &[u8]) -> Result<Self, Error> {
104 #[derive(Deserialize)]
105 struct SchemaProbe {
106 schema: Option<String>,
107 }
108 let probe: SchemaProbe = serde_json::from_slice(bytes)?;
109 let found = probe.schema.unwrap_or_default();
110 if found != SOLVE_REPORT_SCHEMA {
111 return Err(Error::SchemaMismatch { found });
112 }
113 Ok(serde_json::from_slice(bytes)?)
114 }
115
116 pub fn from_json_str(s: &str) -> Result<Self, Error> {
118 Self::from_json_slice(s.as_bytes())
119 }
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct FairMetadata {
124 pub result_id: String,
125 pub created_at_iso: String,
126 pub created_at_unix_nanos: i128,
127 pub elapsed_seconds: f64,
128 pub solver: SolverIdentity,
129 pub license: String,
130 pub input: InputDescriptor,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct SolverIdentity {
135 pub name: String,
136 pub version: String,
137 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub git_commit: Option<String>,
139 pub target_triple: String,
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
143#[serde(tag = "kind", rename_all = "kebab-case")]
144pub enum InputDescriptor {
145 NlFile {
146 path: String,
147 #[serde(default, skip_serializing_if = "Option::is_none")]
148 size_bytes: Option<u64>,
149 },
150 Builtin {
151 name: String,
152 },
153 TnlpDirect,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct ProblemInfo {
158 pub n_variables: i32,
159 pub n_constraints: i32,
160 pub n_objectives: i32,
161 pub minimize: bool,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub nnz_jac_g: Option<i32>,
164 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub nnz_h_lag: Option<i32>,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct SolutionInfo {
170 pub status: String,
176 pub solve_result_num: i32,
177 pub objective: f64,
178 #[serde(default)]
179 pub x: Vec<f64>,
180 #[serde(default)]
181 pub lambda: Vec<f64>,
182 #[serde(default)]
183 pub suffixes: Vec<SolutionSuffix>,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct SolutionSuffix {
188 pub name: String,
189 pub target: String,
190 pub kind: String,
191 #[serde(default)]
192 pub values: Vec<f64>,
193 #[serde(default)]
194 pub int_values: Vec<i32>,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct StatisticsInfo {
199 pub iteration_count: i32,
200 pub final_objective: f64,
201 pub final_scaled_objective: f64,
202 pub final_dual_inf: f64,
203 pub final_constr_viol: f64,
204 pub final_compl: f64,
205 pub final_kkt_error: f64,
206 pub num_obj_evals: i32,
207 pub num_constr_evals: i32,
208 pub num_obj_grad_evals: i32,
209 pub num_constr_jac_evals: i32,
210 pub num_hess_evals: i32,
211 pub total_wallclock_time_secs: f64,
212 #[serde(default)]
216 pub restoration_calls: i32,
217 #[serde(default)]
218 pub restoration_inner_iters: i32,
219 #[serde(default)]
220 pub restoration_outer_iters: i32,
221 #[serde(default)]
222 pub restoration_wall_secs: f64,
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct LinearSolverSummaryInfo {
233 pub solver_name: String,
234 pub n_factors: u64,
235 pub n_pattern_reuse: u64,
236 pub n_pattern_changes: u64,
237 #[serde(default, skip_serializing_if = "Option::is_none")]
238 pub max_fill_ratio: Option<f64>,
239 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub min_abs_pivot: Option<f64>,
241 #[serde(default, skip_serializing_if = "Option::is_none")]
242 pub max_abs_pivot: Option<f64>,
243 #[serde(default, skip_serializing_if = "Option::is_none")]
245 pub last_inertia: Option<(usize, usize, usize)>,
246 #[serde(default, skip_serializing_if = "Option::is_none")]
247 pub last_nnz_a: Option<usize>,
248 #[serde(default, skip_serializing_if = "Option::is_none")]
249 pub last_nnz_l: Option<usize>,
250}
251
252#[derive(Debug, Default, Clone, Serialize, Deserialize)]
255pub struct IterRecord {
256 pub iter: i32,
257 pub objective: f64,
258 pub inf_pr: f64,
259 pub inf_du: f64,
260 pub mu: f64,
261 pub d_norm: f64,
262 pub regularization: f64,
263 pub alpha_dual: f64,
264 pub alpha_primal: f64,
265 pub alpha_primal_char: char,
268 pub ls_trials: i32,
269}