Skip to main content

sim_lib_femm_codec/
values.rs

1//! Lisp/JSON summary forms for FEMM models, solutions, and fields.
2//!
3//! Encodes models, solutions, and fields to their textual summary forms and
4//! parses those forms back into the corresponding summary records.
5
6use sim_lib_femm_core::{FemmError, FemmResult};
7use sim_lib_femm_field::{Field, Projection};
8use sim_lib_femm_mesh::FemmModel;
9use sim_lib_femm_post::FemmSolution;
10
11use crate::support::{
12    formulation_name, parse_atom_field, parse_json_params, parse_json_string_field,
13    parse_json_u64_field, parse_lisp_params, parse_u64_field, physics_name,
14};
15
16/// Parsed summary form of a [`FemmModel`].
17///
18/// The record a model's Lisp or JSON summary string decodes into, and the
19/// source the encoders read from.
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct ModelSummary {
22    /// Model id.
23    pub id: u64,
24    /// Model name.
25    pub name: String,
26    /// Physics kind name.
27    pub physics: String,
28    /// Formulation name.
29    pub formulation: String,
30    /// Input parameter names.
31    pub params: Vec<String>,
32}
33
34/// Parsed summary form of a [`FemmSolution`].
35///
36/// The record a solution's Lisp or JSON summary string decodes into.
37#[derive(Clone, Debug, PartialEq, Eq)]
38pub struct SolutionSummary {
39    /// Solution id.
40    pub id: u64,
41    /// Id of the model the solution was produced from.
42    pub model_id: u64,
43    /// Physics kind name.
44    pub physics: String,
45    /// Formulation name.
46    pub formulation: String,
47    /// Solve parameter names.
48    pub params: Vec<String>,
49}
50
51/// Parsed summary form of a solved [`Field`].
52///
53/// The record a field's read-construct string decodes into.
54#[derive(Clone, Debug, PartialEq, Eq)]
55pub struct FieldSummary {
56    /// Id of the solution the field was projected from.
57    pub solution_id: u64,
58    /// Name of the projected quantity.
59    pub projection: String,
60}
61
62/// Encodes a [`FemmModel`] to its parenthesized Lisp summary form.
63pub fn model_to_lisp(model: &FemmModel) -> String {
64    format!(
65        "(femm/model :id {} :name {} :physics {} :formulation {} :params ({}))",
66        model.id.0,
67        model.name,
68        physics_name(&model.physics),
69        formulation_name(&model.formulation),
70        model
71            .inputs
72            .iter()
73            .map(|param| param.name.to_string())
74            .collect::<Vec<_>>()
75            .join(" ")
76    )
77}
78
79/// Decodes a model's Lisp summary form into a [`ModelSummary`].
80///
81/// # Examples
82///
83/// ```
84/// use sim_lib_femm_codec::model_from_lisp;
85///
86/// let text = "(femm/model :id 7 :name plate :physics electrostatic \
87///              :formulation planar :params (gap-mm))";
88/// let summary = model_from_lisp(text).unwrap();
89/// assert_eq!(summary.id, 7);
90/// assert_eq!(summary.name, "plate");
91/// assert_eq!(summary.params, vec!["gap-mm".to_owned()]);
92/// ```
93pub fn model_from_lisp(text: &str) -> FemmResult<ModelSummary> {
94    Ok(ModelSummary {
95        id: parse_u64_field(text, ":id ")?,
96        name: parse_atom_field(text, ":name ")?,
97        physics: parse_atom_field(text, ":physics ")?,
98        formulation: parse_atom_field(text, ":formulation ")?,
99        params: parse_lisp_params(text)?,
100    })
101}
102
103/// Encodes a [`FemmModel`] to its JSON summary form.
104pub fn model_to_json(model: &FemmModel) -> String {
105    let params = model
106        .inputs
107        .iter()
108        .map(|param| format!("\"{}\"", param.name))
109        .collect::<Vec<_>>()
110        .join(",");
111    format!(
112        "{{\"id\":{},\"name\":\"{}\",\"physics\":\"{}\",\"formulation\":\"{}\",\"params\":[{}]}}",
113        model.id.0,
114        model.name,
115        physics_name(&model.physics),
116        formulation_name(&model.formulation),
117        params
118    )
119}
120
121/// Decodes a model's JSON summary form into a [`ModelSummary`].
122///
123/// # Examples
124///
125/// ```
126/// use sim_lib_femm_codec::model_from_json;
127///
128/// let text = "{\"id\":7,\"name\":\"plate\",\"physics\":\"electrostatic\",\
129///              \"formulation\":\"planar\",\"params\":[\"gap-mm\"]}";
130/// let summary = model_from_json(text).unwrap();
131/// assert_eq!(summary.id, 7);
132/// assert_eq!(summary.formulation, "planar");
133/// assert_eq!(summary.params, vec!["gap-mm".to_owned()]);
134/// ```
135pub fn model_from_json(text: &str) -> FemmResult<ModelSummary> {
136    Ok(ModelSummary {
137        id: parse_json_u64_field(text, "\"id\":")?,
138        name: parse_json_string_field(text, "\"name\":\"")?,
139        physics: parse_json_string_field(text, "\"physics\":\"")?,
140        formulation: parse_json_string_field(text, "\"formulation\":\"")?,
141        params: parse_json_params(text)?,
142    })
143}
144
145/// Accepts a binary frame tag in the reserved FEMM range, rejecting any other.
146///
147/// FEMM binary frames use tags `0xF0..=0xF7`; any tag outside that range is an
148/// [`FemmError::InvalidGeometry`].
149///
150/// # Examples
151///
152/// ```
153/// use sim_lib_femm_codec::reject_unknown_binary_tag;
154///
155/// assert!(reject_unknown_binary_tag(0xF0).is_ok());
156/// assert!(reject_unknown_binary_tag(0x00).is_err());
157/// ```
158pub fn reject_unknown_binary_tag(tag: u8) -> FemmResult<()> {
159    match tag {
160        0xF0..=0xF7 => Ok(()),
161        _ => Err(FemmError::InvalidGeometry(format!(
162            "unknown frame tag {tag:#x}"
163        ))),
164    }
165}
166
167/// Encodes a [`FemmSolution`] to its parenthesized Lisp summary form.
168pub fn solution_to_lisp(solution: &FemmSolution) -> String {
169    format!(
170        "(femm/solution :id {} :model {} :physics {} :formulation {} :params ({}))",
171        solution.id.0,
172        solution.model_id.0,
173        physics_name(&solution.physics),
174        formulation_name(&solution.formulation),
175        solution
176            .params
177            .entries
178            .iter()
179            .map(|(param, _)| param.to_string())
180            .collect::<Vec<_>>()
181            .join(" ")
182    )
183}
184
185/// Decodes a solution's Lisp summary form into a [`SolutionSummary`].
186pub fn solution_from_lisp(text: &str) -> FemmResult<SolutionSummary> {
187    Ok(SolutionSummary {
188        id: parse_u64_field(text, ":id ")?,
189        model_id: parse_u64_field(text, ":model ")?,
190        physics: parse_atom_field(text, ":physics ")?,
191        formulation: parse_atom_field(text, ":formulation ")?,
192        params: parse_lisp_params(text)?,
193    })
194}
195
196/// Encodes a [`FemmSolution`] to its JSON summary form.
197pub fn solution_to_json(solution: &FemmSolution) -> String {
198    let params = solution
199        .params
200        .entries
201        .iter()
202        .map(|(param, _)| format!("\"{param}\""))
203        .collect::<Vec<_>>()
204        .join(",");
205    format!(
206        "{{\"id\":{},\"model\":{},\"physics\":\"{}\",\"formulation\":\"{}\",\"params\":[{}]}}",
207        solution.id.0,
208        solution.model_id.0,
209        physics_name(&solution.physics),
210        formulation_name(&solution.formulation),
211        params
212    )
213}
214
215/// Decodes a solution's JSON summary form into a [`SolutionSummary`].
216pub fn solution_from_json(text: &str) -> FemmResult<SolutionSummary> {
217    Ok(SolutionSummary {
218        id: parse_json_u64_field(text, "\"id\":")?,
219        model_id: parse_json_u64_field(text, "\"model\":")?,
220        physics: parse_json_string_field(text, "\"physics\":\"")?,
221        formulation: parse_json_string_field(text, "\"formulation\":\"")?,
222        params: parse_json_params(text)?,
223    })
224}
225
226/// Encodes a solved [`Field`] to its versioned `#(femm/Field ...)` read-construct.
227pub fn field_read_construct(field: &Field) -> String {
228    format!(
229        "#(femm/Field v1 {} \"{}\")",
230        field.solution_id().0,
231        projection_name(&field.projection())
232    )
233}
234
235/// Decodes a field read-construct into a [`FieldSummary`].
236///
237/// Accepts both the versioned `#(femm/Field v1 ...)` form and the legacy
238/// `#(femm/field ...)` form.
239///
240/// # Examples
241///
242/// ```
243/// use sim_lib_femm_codec::field_from_read_construct;
244///
245/// let summary = field_from_read_construct("#(femm/Field v1 3 \"bmag\")").unwrap();
246/// assert_eq!(summary.solution_id, 3);
247/// assert_eq!(summary.projection, "bmag");
248/// ```
249pub fn field_from_read_construct(text: &str) -> FemmResult<FieldSummary> {
250    let (rest, versioned) = if let Some(rest) = text
251        .strip_prefix("#(femm/Field v1 ")
252        .and_then(|rest| rest.strip_suffix(')'))
253    {
254        (rest, true)
255    } else {
256        let rest = text
257            .strip_prefix("#(femm/field ")
258            .and_then(|rest| rest.strip_suffix(')'))
259            .ok_or_else(|| FemmError::InvalidGeometry("bad field read-construct".to_owned()))?;
260        (rest, false)
261    };
262    let mut parts = rest.split_whitespace();
263    let solution_id = parts
264        .next()
265        .and_then(|part| part.parse::<u64>().ok())
266        .ok_or_else(|| FemmError::InvalidGeometry("bad field solution id".to_owned()))?;
267    let projection = parts
268        .next()
269        .ok_or_else(|| FemmError::InvalidGeometry("missing field projection".to_owned()))?;
270    Ok(FieldSummary {
271        solution_id,
272        projection: if versioned {
273            projection.trim_matches('"').to_owned()
274        } else {
275            projection.to_owned()
276        },
277    })
278}
279
280fn projection_name(projection: &Projection) -> String {
281    match projection {
282        Projection::Potential => "potential".to_owned(),
283        Projection::Bx => "bx".to_owned(),
284        Projection::By => "by".to_owned(),
285        Projection::Bmag => "bmag".to_owned(),
286        Projection::Ex => "ex".to_owned(),
287        Projection::Ey => "ey".to_owned(),
288        Projection::Emag => "emag".to_owned(),
289        Projection::HeatFluxMag => "heat-flux-mag".to_owned(),
290        Projection::Custom(symbol) => symbol.to_string(),
291    }
292}