1use crate::blocks;
20use crate::search::SearchResult;
21use crate::verify::Check;
22use crate::{RunOptions, Spec};
23use comnum_core::Error;
24use comnum_sim::loggers::value_json;
25use comnum_sim::runner::ConditionResult;
26use comnum_sim::stopping::{Progress, Stop};
27use serde_json::{Map, Value as Json, json};
28use std::time::Duration;
29
30pub const DEFAULT_BUDGET_S: f64 = 300.0;
32
33pub fn budget(max_seconds: Option<f64>, json: bool) -> Option<f64> {
36 match max_seconds {
37 Some(s) if s > 0.0 => Some(s),
38 Some(_) => None,
39 None => json.then_some(DEFAULT_BUDGET_S),
40 }
41}
42
43pub fn apply_budget(spec: &mut Spec, seconds: f64) -> Stop {
48 let original = spec.stop.clone();
49 let evaluations = spec.search.as_ref().map_or(1, |s| s.max_evaluations);
50 let n = (spec.conditions().len() * evaluations).max(1);
51 spec.stop = original.clone().or(Stop::max_time(Duration::from_secs_f64(seconds / n as f64)));
52 original
53}
54
55pub fn provenance(spec: &Spec, opts: &RunOptions) -> Json {
58 json!({
59 "comnum": env!("CARGO_PKG_VERSION"),
60 "seed": spec.seed,
61 "threads": opts.threads.unwrap_or(spec.threads),
62 "confidence": spec.confidence,
63 "fingerprint": fingerprint(&spec.source.to_string()),
64 "reports": {
65 "csv": opts.csv.as_ref().map(|p| p.display().to_string()),
66 "json": opts.json.as_ref().map(|p| p.display().to_string()),
67 },
68 })
69}
70
71pub fn fingerprint(text: &str) -> String {
74 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
75 for b in text.bytes() {
76 h ^= u64::from(b);
77 h = h.wrapping_mul(0x0100_0000_01b3);
78 }
79 format!("{h:016x}")
80}
81
82pub fn condition(r: &ConditionResult, confidence: f64, original: Option<&Stop>) -> Json {
86 let values: Map<String, Json> = r
87 .condition
88 .entries()
89 .iter()
90 .map(|(k, v)| (k.clone(), serde_json::from_str(&value_json(v)).unwrap_or(Json::Null)))
91 .collect();
92 let metrics: Map<String, Json> = r
93 .record
94 .iter()
95 .map(|(k, e)| {
96 let (lo, hi) = e.interval(confidence);
97 (k.clone(), json!({"value": finite(e.value()), "lo": finite(lo), "hi": finite(hi), "count": e.count(), "total": e.total()}))
98 })
99 .collect();
100 let time_limited = original.is_some_and(|s| {
101 let p = Progress {
102 condition: r.index,
103 trials: r.trials,
104 record: &r.record,
105 elapsed: Duration::from_secs_f64(r.wall_s),
106 };
107 r.status.as_str() == "completed" && !s.is_met(&p)
108 });
109 json!({
110 "condition": values,
111 "status": r.status.as_str(),
112 "trials": r.trials,
113 "wall_s": r.wall_s,
114 "time_limited": time_limited,
115 "metrics": metrics,
116 })
117}
118
119fn finite(x: f64) -> Json {
121 if x.is_finite() { json!(x) } else { Json::Null }
122}
123
124pub fn operating_point(r: &SearchResult) -> Json {
126 let values: Map<String, Json> = r
127 .condition
128 .entries()
129 .iter()
130 .map(|(k, v)| (k.clone(), serde_json::from_str(&value_json(v)).unwrap_or(Json::Null)))
131 .collect();
132 json!({
133 "condition": values,
134 "along": r.along,
135 "metric": r.metric,
136 "target": r.target,
137 "value": finite(r.value),
138 "lo": finite(r.lo),
139 "hi": finite(r.hi),
140 "converged": r.converged,
141 "evaluations": r.evaluations.len(),
142 "trials": r.evaluations.iter().map(|e| e.trials).sum::<u64>(),
143 })
144}
145
146pub fn check_json(c: &Check) -> Json {
148 json!({
149 "name": c.name,
150 "source": c.source,
151 "condition": c.condition,
152 "metric": c.metric,
153 "expected": finite(c.expected),
154 "measured": finite(c.measured.value),
155 "lo": finite(c.measured.lo),
156 "hi": finite(c.measured.hi),
157 "passed": c.passed,
158 })
159}
160
161pub fn error_kind(e: &Error) -> &'static str {
164 match e {
165 Error::Config(_) => "config",
166 Error::Shape(_) => "shape",
167 Error::Numerical(_) => "numerical",
168 Error::Condition(_) => "condition",
169 Error::Io(_) => "io",
170 _ => "other",
171 }
172}
173
174pub fn error(kind: &str, message: &str) -> Json {
177 let (hint, suggestion) = advice(message);
178 json!({"kind": kind, "message": message, "hint": hint, "suggestion": suggestion})
179}
180
181fn advice(message: &str) -> (String, Option<String>) {
183 let all = blocks::registry();
184 if let Some(name) = quoted_after(message, "unknown block \"", '"') {
185 let names: Vec<&str> = all.iter().map(|b| b.name).collect();
186 let best = closest(&name, &names).map(|b| format!("type: {b}"));
187 return ("`comnum blocks --json` lists the blocks and their parameters".into(), best);
188 }
189 if let (Some(block), Some(field)) =
190 (quoted_after(message, "block \"", '"'), quoted_after(message, "unknown field `", '`'))
191 {
192 let hint = format!("`comnum blocks {block} --json` documents the parameters of {block}");
193 let own: Vec<&str> =
194 all.iter().find(|b| b.name == block).map(|b| b.params.iter().map(|p| p.name).collect()).unwrap_or_default();
195 if let Some(best) = closest(&field, &own) {
196 return (hint, Some(format!("{best} (a parameter of {block})")));
197 }
198 let others: Vec<&str> = all
200 .iter()
201 .filter(|b| b.name != block && b.params.iter().any(|p| p.name == field))
202 .map(|b| b.name)
203 .collect();
204 return (hint, others.first().map(|b| format!("type: {b} (it takes {field})")));
205 }
206 if ["YAML:", "JSON:", "TOML:"].iter().any(|f| message.contains(f)) {
207 return (
208 "a specification is a mapping with name, grid, link (a list of blocks), metrics and stop; `comnum schema` gives the JSON Schema of a block".into(),
209 None,
210 );
211 }
212 if ["unknown option", "unknown command", "no specification given", "needs a", "is not a number", "must be a number"]
213 .iter()
214 .any(|m| message.contains(m))
215 {
216 return ("`comnum --help` prints the usage".into(), None);
217 }
218 if message.contains("No such file") || message.contains("not found") {
219 return ("check the path of the specification, or pass `-` to read it from standard input".into(), None);
220 }
221 ("`comnum check SPEC` builds every condition and runs one trial, without writing anything".into(), None)
222}
223
224fn quoted_after(message: &str, prefix: &str, end: char) -> Option<String> {
226 let start = message.find(prefix)? + prefix.len();
227 let rest = &message[start..];
228 Some(rest[..rest.find(end)?].to_string())
229}
230
231fn closest<'a>(word: &str, names: &[&'a str]) -> Option<&'a str> {
234 let limit = (word.chars().count() / 3).max(2);
235 names.iter().map(|n| (edit_distance(word, n), *n)).filter(|(d, _)| *d <= limit).min().map(|(_, n)| n)
236}
237
238fn edit_distance(a: &str, b: &str) -> usize {
240 let b: Vec<char> = b.chars().collect();
241 let mut row: Vec<usize> = (0..=b.len()).collect();
242 for (i, ca) in a.chars().enumerate() {
243 let mut prev = row[0];
244 row[0] = i + 1;
245 for (j, cb) in b.iter().enumerate() {
246 let cur = row[j + 1];
247 row[j + 1] = (prev + usize::from(ca != *cb)).min(row[j] + 1).min(cur + 1);
248 prev = cur;
249 }
250 }
251 row[b.len()]
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn a_misspelt_block_or_parameter_gets_the_closest_name() {
260 let e = error("config", "link block 2: unknown block \"awgnn\": one of source, awgn");
261 assert_eq!(e["suggestion"], "type: awgn");
262 let e = error("config", "link block 2: block \"awgn\": unknown field `ebn0_db`, expected one of `snr_db`");
264 assert_eq!(e["suggestion"], "type: awgn_ebn0 (it takes ebn0_db)");
265 let e = error("config", "link block 2: block \"awgn\": unknown field `snr_bd`, expected one of `snr_db`");
266 assert_eq!(e["suggestion"], "snr_db (a parameter of awgn)");
267 let e = error("config", "b.yaml: YAML: did not find expected node content");
268 assert!(e["hint"].as_str().unwrap().contains("comnum schema") && e["suggestion"].is_null());
269 assert!(error("usage", "unknown option --bogus")["hint"].as_str().unwrap().contains("--help"));
270 }
271
272 #[test]
273 fn the_budget_is_300_s_in_json_mode_and_0_removes_it() {
274 assert_eq!(budget(None, true), Some(DEFAULT_BUDGET_S));
275 assert_eq!(budget(None, false), None);
276 assert_eq!(budget(Some(12.0), false), Some(12.0));
277 assert_eq!(budget(Some(0.0), true), None);
278 }
279
280 #[test]
281 fn the_fingerprint_is_fnv_1a() {
282 assert_eq!(fingerprint(""), "cbf29ce484222325");
284 assert_eq!(fingerprint("a"), "af63dc4c8601ec8c");
285 assert_eq!(edit_distance("kitten", "sitting"), 3);
286 }
287}