1use super::*;
4
5fn basis_brief(b: &simulations::SimulationBasis) -> Value {
6 json!({
7 "role": b.role,
8 "item": b.basis_number,
9 "revision": b.basis_code,
10 "name": b.basis_name,
11 "type": b.basis_type,
12 "state": b.basis_state,
13 "file": (!b.file_name.is_empty()).then_some(&b.file_name),
14 "file_version": b.file_version,
15 "file_current_version": b.file_current_version,
16 "newest_released_revision": b.released_code,
17 "note": b.note,
18 "out_of_date": b.out_of_date,
19 })
20}
21
22fn result_brief(r: &simulations::SimulationResult) -> Value {
23 json!({
24 "name": r.name,
25 "value": r.value,
26 "unit": r.unit,
27 "lower_limit": r.lower_limit,
28 "upper_limit": r.upper_limit,
29 "load_case": r.load_case,
30 "status": r.status.map(|s| s.as_str()),
31 "solver": r.solver,
32 "solver_version": r.solver_version,
33 "note": r.note,
34 })
35}
36
37pub fn group_based(rows: &[simulations::BasedSimulation]) -> Vec<Value> {
40 let mut out: Vec<Value> = Vec::new();
41 for row in rows {
42 let same = out
43 .last()
44 .is_some_and(|v| v["simulation"] == row.simulation_number.as_str());
45 if !same {
46 out.push(json!({
47 "simulation": row.simulation_number,
48 "revision": row.simulation_code,
49 "name": row.simulation_name,
50 "state": row.simulation_state,
51 "result_count": row.result_count,
52 "failed_count": row.failed_count,
53 "check_open": false,
54 "out_of_date": false,
55 "bases": [],
56 }));
57 }
58 let entry = out.last_mut().expect("pushed above");
59 if row.open_impact_id.is_some() {
60 entry["check_open"] = json!(true);
61 }
62 if row.basis.out_of_date.is_some() {
63 entry["out_of_date"] = json!(true);
64 }
65 if let Some(bases) = entry["bases"].as_array_mut() {
66 bases.push(basis_brief(&row.basis));
67 }
68 }
69 out
70}
71
72pub(super) async fn simulations_of(
74 st: &AppState,
75 me: &Actor,
76 item_id: uuid::Uuid,
77) -> AppResult<(Vec<Value>, bool)> {
78 let (rows, _) = simulations::based_on_item(&st.db, me, item_id).await?;
79 Ok(capped(group_based(&rows), MAX_RELATED))
80}
81
82fn ensure_simulation(rev: &Revision) -> AppResult<()> {
83 if rev.item_type == ItemType::Simulation.as_str() {
84 Ok(())
85 } else {
86 Err(bad(format!(
87 "{} is a {}, not a simulation; list_simulations with item={} lists the simulations based on it.",
88 revisions::label(rev),
89 rev.item_type,
90 rev.item_number
91 )))
92 }
93}
94
95pub(super) async fn simulation_state(
97 st: &AppState,
98 me: &Actor,
99 rev: &Revision,
100) -> AppResult<Value> {
101 let (bases, results) = tokio::try_join!(
102 simulations::bases(&st.db, me, rev.id),
103 simulations::results(&st.db, me, rev.id),
104 )?;
105 let reasons: Vec<&String> = bases
106 .iter()
107 .filter_map(|b| b.out_of_date.as_ref())
108 .collect();
109 let (shown, results_cut) = capped(results.iter().collect::<Vec<_>>(), simulations::MAX_RESULTS);
110 Ok(json!({
111 "out_of_date": !reasons.is_empty(),
112 "out_of_date_reasons": reasons,
113 "summary": simulations::summarize(&results),
114 "bases": bases.iter().map(basis_brief).collect::<Vec<_>>(),
115 "results": shown.into_iter().map(result_brief).collect::<Vec<_>>(),
116 "results_truncated": results_cut,
117 }))
118}
119
120pub(super) async fn list_simulations(st: &AppState, me: &Actor, a: &Args<'_>) -> AppResult<Value> {
121 let out_of_date_only = a.flag("out_of_date_only", false)?;
122 let limit = a.limit("limit", 20, MAX_SIMULATIONS as i64)? as usize;
123 if let Some(number) = a.opt_str("item")? {
124 let item = items::get_by_number(&st.db, me, &number).await?;
125 let (rows, _) = simulations::based_on_item(&st.db, me, item.id).await?;
126 let mut list = group_based(&rows);
127 if out_of_date_only {
128 list.retain(|s| s["out_of_date"] == true);
129 }
130 let (list, truncated) = capped(list, limit);
131 let mut value = json!({
132 "based_on": item.number,
133 "simulations": list,
134 "truncated": truncated,
135 });
136 if item.item_type == ItemType::Simulation.as_str() {
137 value["hint"] = json!(format!(
138 "{} is itself a simulation: get_simulation shows its bases and results.",
139 item.number
140 ));
141 }
142 return Ok(value);
143 }
144 let project_id = match a.opt_str("project")? {
145 Some(code) => Some(projects::get_by_code(&st.db, me, &code).await?.id),
146 None => None,
147 };
148 let scan = if out_of_date_only {
150 MAX_SIMULATIONS
151 } else {
152 limit
153 };
154 let filter = items::ItemFilter {
155 query: a.opt_str("q")?,
156 item_type: Some(ItemType::Simulation),
157 project_id,
158 state: None,
159 system: None,
160 ..Default::default()
161 };
162 let (rows, total) =
163 items::search(&st.db, me, &filter, Page::new(Some(scan as i64), None)).await?;
164 let mut list = Vec::new();
165 for sim in &rows {
166 let (bases, results) = tokio::try_join!(
167 simulations::bases(&st.db, me, sim.latest_revision_id),
168 simulations::results(&st.db, me, sim.latest_revision_id),
169 )?;
170 let reasons: Vec<String> = bases.iter().filter_map(|b| b.out_of_date.clone()).collect();
171 if out_of_date_only && reasons.is_empty() {
172 continue;
173 }
174 list.push(json!({
175 "simulation": sim.number,
176 "revision": sim.latest_code,
177 "name": sim.name,
178 "state": sim.latest_state,
179 "project": sim.project_code,
180 "out_of_date": !reasons.is_empty(),
181 "out_of_date_reasons": reasons,
182 "based_on": bases.iter().map(|b| format!("{}/{}", b.basis_number, b.basis_code)).collect::<Vec<_>>(),
183 "results": simulations::summarize(&results),
184 }));
185 }
186 let (list, cut) = capped(list, limit);
187 Ok(json!({
188 "total_simulations": total,
189 "checked": rows.len(),
190 "simulations": list,
191 "truncated": cut || total > rows.len() as i64,
192 }))
193}
194
195pub(super) async fn get_simulation(st: &AppState, me: &Actor, a: &Args<'_>) -> AppResult<Value> {
196 let rev = revision_arg(st, me, a).await?;
197 ensure_simulation(&rev)?;
198 let mut value = simulation_state(st, me, &rev).await?;
199 value["simulation"] = rev_brief(&rev);
200 value["url"] = json!(web_url(st, &format!("/items/{}", rev.item_id)));
201 Ok(value)
202}
203
204pub fn diff_brief(d: &simulations::ResultDiff) -> Value {
206 let side = |r: &Option<simulations::SimulationResult>| {
207 r.as_ref().map(|r| {
208 json!({
209 "value": r.value,
210 "unit": r.unit,
211 "lower_limit": r.lower_limit,
212 "upper_limit": r.upper_limit,
213 "status": r.status.map(|s| s.as_str()),
214 })
215 })
216 };
217 json!({
218 "change": d.change,
219 "name": d.name,
220 "load_case": d.load_case,
221 "unit": d.unit,
222 "before": side(&d.before),
223 "after": side(&d.after),
224 "delta": d.delta,
225 "delta_percent": d.delta_percent.map(|p| (p * 100.0).round() / 100.0),
226 })
227}
228
229pub(super) async fn compare_simulation_results(
230 st: &AppState,
231 me: &Actor,
232 a: &Args<'_>,
233) -> AppResult<Value> {
234 let (from, to) = revision_pair(st, me, a).await?;
235 ensure_simulation(&to)?;
236 let changes_only = a.flag("changes_only", false)?;
237 let diffs = simulations::compare(&st.db, &from, &to).await?;
238 let summary = simulations::compare_summary(&diffs);
239 let shown: Vec<&simulations::ResultDiff> = diffs
240 .iter()
241 .filter(|d| !changes_only || d.change != crate::services::bom_compare::Change::Unchanged)
242 .collect();
243 let (shown, truncated) = capped(shown, simulations::MAX_RESULTS);
244 Ok(json!({
245 "from": revisions::label(&from),
246 "to": revisions::label(&to),
247 "from_state": from.state,
248 "to_state": to.state,
249 "summary": summary,
250 "diffs": shown.into_iter().map(diff_brief).collect::<Vec<_>>(),
251 "truncated": truncated,
252 }))
253}
254
255pub(super) async fn propose_result_values(
257 st: &AppState,
258 me: &Actor,
259 a: &Args<'_>,
260) -> AppResult<Value> {
261 let rev = revision_arg(st, me, a).await?;
262 let Some(Value::Array(list)) = a.get("values") else {
263 return Err(bad("`values` must be a list of values."));
264 };
265 let mut inputs = Vec::with_capacity(list.len());
266 for (i, v) in list.iter().enumerate() {
267 let input: result_proposals::ProposalInput =
268 serde_json::from_value(v.clone()).map_err(|e| bad(format!("values[{i}]: {e}")))?;
269 inputs.push(input);
270 }
271 let proposals = result_proposals::propose(&st.db, me, rev.id, inputs).await?;
272 Ok(json!({
273 "item": rev.item_number,
274 "revision": rev.code,
275 "proposed": proposals.iter().map(|p| json!({
276 "name": p.name, "value": p.value, "unit": p.unit, "load_case": p.load_case,
277 "file": p.source_file, "version": p.source_version, "status": p.status,
278 })).collect::<Vec<_>>(),
279 "note": "These are drafts. A CAD editor accepts or rejects them on the simulation page in TeamCraft \
280 (accepting needs the revision checked out). You cannot accept them – tell the user where \
281 to find them.",
282 "url": web_url(st, &format!("/revisions/{}#result-proposals", rev.id)),
283 }))
284}