1use sim_citizen_derive::Citizen;
7use sim_kernel::Symbol;
8use sim_lib_femm_field::{Field, Projection};
9use sim_lib_femm_function::{FemmFuncPayload, OutputQuery};
10use sim_lib_femm_mesh::{FemMesh2, FemmModel};
11use sim_lib_femm_post::{FemmSolution, QuantitySpec};
12
13use crate::support::{formulation_name, physics_name};
14
15#[derive(Clone, Debug, PartialEq, Citizen)]
20#[citizen(symbol = "femm/Field", version = 1)]
21pub struct FemmFieldDescriptor {
22 pub solution_id: u64,
24 #[citizen(with = "descriptor_text")]
26 pub projection: String,
27}
28
29#[derive(Clone, Debug, PartialEq, Citizen)]
34#[citizen(symbol = "femm/Geometry", version = 1)]
35pub struct FemmGeometryDescriptor {
36 pub region_count: usize,
38 pub boundary_count: usize,
40 #[citizen(with = "descriptor_text")]
42 pub artifact_ref: String,
43}
44
45#[derive(Clone, Debug, PartialEq, Citizen)]
50#[citizen(symbol = "femm/Material", version = 1)]
51pub struct FemmMaterialDescriptor {
52 #[citizen(with = "descriptor_text")]
54 pub name: String,
55 pub properties: Vec<String>,
57}
58
59#[derive(Clone, Debug, PartialEq, Citizen)]
64#[citizen(symbol = "femm/Mesh", version = 1)]
65pub struct FemmMeshDescriptor {
66 pub nodes: usize,
68 pub elements: usize,
70 #[citizen(with = "descriptor_text")]
72 pub artifact_ref: String,
73}
74
75#[derive(Clone, Debug, PartialEq, Citizen)]
80#[citizen(symbol = "femm/Space", version = 1)]
81pub struct FemmSpaceDescriptor {
82 pub element_count: usize,
84 #[citizen(with = "descriptor_text")]
86 pub formulation: String,
87}
88
89#[derive(Clone, Debug, PartialEq, Citizen)]
94#[citizen(symbol = "femm/Physics", version = 1)]
95pub struct FemmPhysicsDescriptor {
96 #[citizen(with = "descriptor_text")]
98 pub physics: String,
99 #[citizen(with = "descriptor_text")]
101 pub formulation: String,
102}
103
104#[derive(Clone, Debug, PartialEq, Citizen)]
109#[citizen(symbol = "femm/Solve", version = 1)]
110pub struct FemmSolveDescriptor {
111 #[citizen(with = "descriptor_text")]
113 pub method: String,
114 #[citizen(with = "descriptor_text")]
116 pub matrix_ref: String,
117}
118
119#[derive(Clone, Debug, PartialEq, Citizen)]
124#[citizen(symbol = "femm/Solution", version = 1)]
125pub struct FemmSolutionDescriptor {
126 pub id: u64,
128 pub model_id: u64,
130 #[citizen(with = "descriptor_text")]
132 pub physics: String,
133 #[citizen(with = "descriptor_text")]
135 pub formulation: String,
136 pub params: Vec<String>,
138 pub nodes: usize,
140 pub elements: usize,
142}
143
144#[derive(Clone, Debug, PartialEq, Citizen)]
149#[citizen(symbol = "femm/Post", version = 1)]
150pub struct FemmPostDescriptor {
151 #[citizen(with = "descriptor_text")]
153 pub quantity: String,
154 #[citizen(with = "descriptor_text")]
156 pub target: String,
157}
158
159#[derive(Clone, Debug, PartialEq, Citizen)]
164#[citizen(symbol = "femm/Function", version = 1)]
165pub struct FemmFunctionDescriptor {
166 pub model_id: u64,
168 #[citizen(with = "descriptor_text")]
170 pub query: String,
171 pub vars: Vec<String>,
173}
174
175#[derive(Clone, Debug, PartialEq, Citizen)]
180#[citizen(symbol = "femm/FuncPayload", version = 1)]
181pub struct FemmFuncPayloadDescriptor {
182 pub model_id: u64,
184 #[citizen(with = "descriptor_text")]
186 pub query: String,
187 pub vars: Vec<String>,
189}
190
191#[derive(Clone, Debug, PartialEq, Citizen)]
196#[citizen(symbol = "femm/Model", version = 1)]
197pub struct FemmModelDescriptor {
198 pub id: u64,
200 #[citizen(with = "descriptor_text")]
202 pub name: String,
203 #[citizen(with = "descriptor_text")]
205 pub physics: String,
206 #[citizen(with = "descriptor_text")]
208 pub formulation: String,
209 pub params: Vec<String>,
211}
212
213#[derive(Clone, Debug, PartialEq, Citizen)]
218#[citizen(symbol = "femm/Sensitivity", version = 1)]
219pub struct FemmSensitivityDescriptor {
220 #[citizen(with = "descriptor_text")]
222 pub path: String,
223 pub wrt: Vec<String>,
225}
226
227#[derive(Clone, Debug, PartialEq, Citizen)]
232#[citizen(symbol = "femm/Tape", version = 1)]
233pub struct FemmTapeDescriptor {
234 pub factors: usize,
236 pub solutions: usize,
238 #[citizen(with = "descriptor_text")]
240 pub artifact_ref: String,
241}
242
243#[derive(Clone, Debug, PartialEq, Citizen)]
248#[citizen(symbol = "femm/Ode", version = 1)]
249pub struct FemmOdeDescriptor {
250 pub state_vars: Vec<String>,
252 pub quantity_needs: Vec<String>,
254}
255
256impl FemmFieldDescriptor {
257 pub fn from_field(field: &Field) -> Self {
259 Self {
260 solution_id: field.solution_id().0,
261 projection: projection_name(&field.projection()),
262 }
263 }
264}
265
266impl FemmMeshDescriptor {
267 pub fn from_mesh(mesh: &FemMesh2, artifact_ref: impl Into<String>) -> Self {
269 Self {
270 nodes: mesh.xy.len(),
271 elements: mesh.tri.len(),
272 artifact_ref: artifact_ref.into(),
273 }
274 }
275}
276
277impl FemmSolutionDescriptor {
278 pub fn from_solution(solution: &FemmSolution) -> Self {
280 Self {
281 id: solution.id.0,
282 model_id: solution.model_id.0,
283 physics: physics_name(&solution.physics).to_owned(),
284 formulation: formulation_name(&solution.formulation).to_owned(),
285 params: solution
286 .params
287 .entries
288 .iter()
289 .map(|(name, _)| name.to_string())
290 .collect(),
291 nodes: solution.mesh.xy.len(),
292 elements: solution.mesh.tri.len(),
293 }
294 }
295}
296
297impl FemmFunctionDescriptor {
298 pub fn from_payload(payload: &FemmFuncPayload) -> Self {
300 Self {
301 model_id: payload.model.id.0,
302 query: query_name(&payload.query),
303 vars: payload.vars.iter().map(ToString::to_string).collect(),
304 }
305 }
306}
307
308impl FemmFuncPayloadDescriptor {
309 pub fn from_payload(payload: &FemmFuncPayload) -> Self {
311 let descriptor = FemmFunctionDescriptor::from_payload(payload);
312 Self {
313 model_id: descriptor.model_id,
314 query: descriptor.query,
315 vars: descriptor.vars,
316 }
317 }
318}
319
320impl FemmModelDescriptor {
321 pub fn from_model(model: &FemmModel) -> Self {
323 Self {
324 id: model.id.0,
325 name: model.name.to_string(),
326 physics: physics_name(&model.physics).to_owned(),
327 formulation: formulation_name(&model.formulation).to_owned(),
328 params: model
329 .inputs
330 .iter()
331 .map(|param| param.name.to_string())
332 .collect(),
333 }
334 }
335}
336
337impl Default for FemmFieldDescriptor {
338 fn default() -> Self {
339 Self {
340 solution_id: 1,
341 projection: "potential".to_owned(),
342 }
343 }
344}
345
346impl Default for FemmGeometryDescriptor {
347 fn default() -> Self {
348 Self {
349 region_count: 1,
350 boundary_count: 0,
351 artifact_ref: "table:femm/geometry/citizen".to_owned(),
352 }
353 }
354}
355
356impl Default for FemmMaterialDescriptor {
357 fn default() -> Self {
358 Self {
359 name: "air".to_owned(),
360 properties: vec!["epsilon-r".to_owned(), "mu-r".to_owned()],
361 }
362 }
363}
364
365impl Default for FemmMeshDescriptor {
366 fn default() -> Self {
367 Self {
368 nodes: 3,
369 elements: 1,
370 artifact_ref: "table:femm/mesh/citizen".to_owned(),
371 }
372 }
373}
374
375impl Default for FemmSpaceDescriptor {
376 fn default() -> Self {
377 Self {
378 element_count: 1,
379 formulation: "planar".to_owned(),
380 }
381 }
382}
383
384impl Default for FemmPhysicsDescriptor {
385 fn default() -> Self {
386 Self {
387 physics: "electrostatic".to_owned(),
388 formulation: "planar".to_owned(),
389 }
390 }
391}
392
393impl Default for FemmSolveDescriptor {
394 fn default() -> Self {
395 Self {
396 method: "sparse-lu".to_owned(),
397 matrix_ref: "stable:femm/matrix/citizen".to_owned(),
398 }
399 }
400}
401
402impl Default for FemmSolutionDescriptor {
403 fn default() -> Self {
404 Self {
405 id: 1,
406 model_id: 1,
407 physics: "electrostatic".to_owned(),
408 formulation: "planar".to_owned(),
409 params: Vec::new(),
410 nodes: 3,
411 elements: 1,
412 }
413 }
414}
415
416impl Default for FemmPostDescriptor {
417 fn default() -> Self {
418 Self {
419 quantity: "energy".to_owned(),
420 target: "region:air".to_owned(),
421 }
422 }
423}
424
425impl Default for FemmFunctionDescriptor {
426 fn default() -> Self {
427 Self {
428 model_id: 1,
429 query: "quantity:energy".to_owned(),
430 vars: vec!["gap".to_owned()],
431 }
432 }
433}
434
435impl Default for FemmFuncPayloadDescriptor {
436 fn default() -> Self {
437 Self {
438 model_id: 1,
439 query: "quantity:energy".to_owned(),
440 vars: vec!["gap".to_owned()],
441 }
442 }
443}
444
445impl Default for FemmModelDescriptor {
446 fn default() -> Self {
447 Self {
448 id: 1,
449 name: "parallel-plate-capacitor".to_owned(),
450 physics: "electrostatic".to_owned(),
451 formulation: "planar".to_owned(),
452 params: vec!["gap-mm".to_owned()],
453 }
454 }
455}
456
457impl Default for FemmSensitivityDescriptor {
458 fn default() -> Self {
459 Self {
460 path: "direct-exact".to_owned(),
461 wrt: vec!["gap".to_owned()],
462 }
463 }
464}
465
466impl Default for FemmTapeDescriptor {
467 fn default() -> Self {
468 Self {
469 factors: 1,
470 solutions: 1,
471 artifact_ref: "stable:femm/tape/citizen".to_owned(),
472 }
473 }
474}
475
476impl Default for FemmOdeDescriptor {
477 fn default() -> Self {
478 Self {
479 state_vars: vec!["x".to_owned(), "v".to_owned()],
480 quantity_needs: vec!["energy".to_owned()],
481 }
482 }
483}
484
485macro_rules! class_symbol_fn {
486 ($name:ident, $class:literal) => {
487 #[doc = concat!("Citizen class [`Symbol`] `femm/", $class, "` for the matching descriptor.")]
488 pub fn $name() -> Symbol {
489 Symbol::qualified("femm", $class)
490 }
491 };
492}
493
494class_symbol_fn!(femm_field_class_symbol, "Field");
495class_symbol_fn!(femm_geometry_class_symbol, "Geometry");
496class_symbol_fn!(femm_material_class_symbol, "Material");
497class_symbol_fn!(femm_mesh_class_symbol, "Mesh");
498class_symbol_fn!(femm_space_class_symbol, "Space");
499class_symbol_fn!(femm_physics_class_symbol, "Physics");
500class_symbol_fn!(femm_solve_class_symbol, "Solve");
501class_symbol_fn!(femm_solution_class_symbol, "Solution");
502class_symbol_fn!(femm_post_class_symbol, "Post");
503class_symbol_fn!(femm_function_class_symbol, "Function");
504class_symbol_fn!(femm_func_payload_class_symbol, "FuncPayload");
505class_symbol_fn!(femm_model_class_symbol, "Model");
506class_symbol_fn!(femm_sensitivity_class_symbol, "Sensitivity");
507class_symbol_fn!(femm_tape_class_symbol, "Tape");
508class_symbol_fn!(femm_ode_class_symbol, "Ode");
509
510pub(crate) mod descriptor_text {
511 use sim_kernel::{Error, Expr, Result};
512
513 pub fn encode(text: &str) -> Expr {
514 Expr::String(text.to_owned())
515 }
516
517 pub fn decode(expr: &Expr) -> Result<String> {
518 let Expr::String(text) = expr else {
519 return Err(Error::Eval(
520 "FEMM descriptor text must be a string".to_owned(),
521 ));
522 };
523 validate_descriptor_text(text)?;
524 Ok(text.clone())
525 }
526
527 fn validate_descriptor_text(text: &str) -> Result<()> {
528 if text.trim().is_empty() {
529 return Err(Error::Eval(
530 "FEMM descriptor text cannot be empty".to_owned(),
531 ));
532 }
533 if !text.is_ascii() {
534 return Err(Error::Eval("FEMM descriptor text must be ASCII".to_owned()));
535 }
536 Ok(())
537 }
538}
539
540fn projection_name(projection: &Projection) -> String {
541 match projection {
542 Projection::Potential => "potential".to_owned(),
543 Projection::Bx => "bx".to_owned(),
544 Projection::By => "by".to_owned(),
545 Projection::Bmag => "bmag".to_owned(),
546 Projection::Ex => "ex".to_owned(),
547 Projection::Ey => "ey".to_owned(),
548 Projection::Emag => "emag".to_owned(),
549 Projection::HeatFluxMag => "heat-flux-mag".to_owned(),
550 Projection::Custom(symbol) => symbol.to_string(),
551 }
552}
553
554fn query_name(query: &OutputQuery) -> String {
555 match query {
556 OutputQuery::Quantity(spec) => format!("quantity:{}", quantity_name(spec)),
557 OutputQuery::Field(projection) => format!("field:{}", projection_name(projection)),
558 OutputQuery::Solution => "solution".to_owned(),
559 }
560}
561
562fn quantity_name(spec: &QuantitySpec) -> String {
563 match spec {
564 QuantitySpec::Energy { region } => optional_region("energy", region.as_ref()),
565 QuantitySpec::Coenergy { region } => optional_region("coenergy", region.as_ref()),
566 QuantitySpec::ForceY { region } => format!("force-y:{region}"),
567 QuantitySpec::Torque { region, .. } => format!("torque:{region}"),
568 QuantitySpec::FluxLinkage { circuit } => format!("flux-linkage:{circuit}"),
569 QuantitySpec::Inductance { circuit } => format!("inductance:{circuit}"),
570 QuantitySpec::Capacitance { conductor } => format!("capacitance:{conductor}"),
571 QuantitySpec::JouleLoss { region } => optional_region("joule-loss", region.as_ref()),
572 QuantitySpec::FieldAt { field, .. } => format!("field-at:{field}"),
573 QuantitySpec::Custom { name, .. } => format!("custom:{name}"),
574 }
575}
576
577fn optional_region(prefix: &str, region: Option<&Symbol>) -> String {
578 region
579 .map(|region| format!("{prefix}:{region}"))
580 .unwrap_or_else(|| prefix.to_owned())
581}