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 CbfFile {
155 path: String,
156 #[serde(default, skip_serializing_if = "Option::is_none")]
157 size_bytes: Option<u64>,
158 },
159 Builtin {
160 name: String,
161 },
162 TnlpDirect,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct ProblemInfo {
167 pub n_variables: i32,
168 pub n_constraints: i32,
169 pub n_objectives: i32,
170 pub minimize: bool,
171 #[serde(default, skip_serializing_if = "Option::is_none")]
172 pub nnz_jac_g: Option<i32>,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
174 pub nnz_h_lag: Option<i32>,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct SolutionInfo {
179 pub status: String,
185 pub solve_result_num: i32,
186 pub objective: f64,
187 #[serde(default)]
188 pub x: Vec<f64>,
189 #[serde(default)]
190 pub lambda: Vec<f64>,
191 #[serde(default)]
192 pub suffixes: Vec<SolutionSuffix>,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct SolutionSuffix {
197 pub name: String,
198 pub target: String,
199 pub kind: String,
200 #[serde(default)]
201 pub values: Vec<f64>,
202 #[serde(default)]
203 pub int_values: Vec<i32>,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct StatisticsInfo {
208 pub iteration_count: i32,
209 pub final_objective: f64,
210 pub final_scaled_objective: f64,
211 pub final_dual_inf: f64,
212 pub final_constr_viol: f64,
213 pub final_compl: f64,
214 pub final_kkt_error: f64,
215 pub num_obj_evals: i32,
216 pub num_constr_evals: i32,
217 pub num_obj_grad_evals: i32,
218 pub num_constr_jac_evals: i32,
219 pub num_hess_evals: i32,
220 pub total_wallclock_time_secs: f64,
221 #[serde(default)]
225 pub restoration_calls: i32,
226 #[serde(default)]
227 pub restoration_inner_iters: i32,
228 #[serde(default)]
229 pub restoration_outer_iters: i32,
230 #[serde(default)]
231 pub restoration_wall_secs: f64,
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct LinearSolverSummaryInfo {
242 pub solver_name: String,
243 pub n_factors: u64,
244 pub n_pattern_reuse: u64,
245 pub n_pattern_changes: u64,
246 #[serde(default, skip_serializing_if = "Option::is_none")]
247 pub max_fill_ratio: Option<f64>,
248 #[serde(default, skip_serializing_if = "Option::is_none")]
249 pub min_abs_pivot: Option<f64>,
250 #[serde(default, skip_serializing_if = "Option::is_none")]
251 pub max_abs_pivot: Option<f64>,
252 #[serde(default, skip_serializing_if = "Option::is_none")]
254 pub last_inertia: Option<(usize, usize, usize)>,
255 #[serde(default, skip_serializing_if = "Option::is_none")]
256 pub last_nnz_a: Option<usize>,
257 #[serde(default, skip_serializing_if = "Option::is_none")]
258 pub last_nnz_l: Option<usize>,
259}
260
261#[derive(Debug, Default, Clone, Serialize, Deserialize)]
264pub struct IterRecord {
265 pub iter: i32,
266 pub objective: f64,
267 pub inf_pr: f64,
268 pub inf_du: f64,
269 pub mu: f64,
270 pub d_norm: f64,
271 pub regularization: f64,
272 pub alpha_dual: f64,
273 pub alpha_primal: f64,
274 pub alpha_primal_char: char,
277 pub ls_trials: i32,
278}