1#![forbid(unsafe_code)]
2use std::{
9 any::Any,
10 collections::BTreeSet,
11 hash::{Hash, Hasher},
12 sync::Arc,
13 time::Duration,
14};
15
16use sim_kernel::{
17 AbiVersion, Args, Callable, ClassRef, Cx, DefaultFactory, Dependency, Expr, Factory, Lib,
18 LibManifest, LibTarget, Linker, Object, RawArgs, Result as KernelResult, Symbol, Value,
19 Version,
20};
21
22use crate::{FemmError, FemmResult};
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
30pub struct StableId(pub u64);
31
32impl StableId {
33 pub fn from_hashable<T: Hash>(value: &T) -> Self {
35 let mut hasher = std::collections::hash_map::DefaultHasher::new();
36 value.hash(&mut hasher);
37 Self(hasher.finish())
38 }
39}
40
41#[derive(Clone, Debug, PartialEq, Eq, Hash)]
46pub enum PhysicsKind {
47 Magnetostatic,
49 MagneticsHarmonic,
51 Electrostatic,
53 HeatSteady,
55 CurrentSteady,
57}
58
59#[derive(Clone, Debug, PartialEq, Eq, Hash)]
61pub enum Formulation {
62 Planar,
64 Axisymmetric,
66}
67
68#[derive(Clone, Debug, PartialEq, Eq, Hash)]
70pub enum LengthUnit {
71 Meter,
73 Millimeter,
75 Inch,
77 Custom(Symbol),
79}
80
81#[derive(Clone, Debug, PartialEq, Eq, Hash)]
86pub enum ParamRole {
87 Design,
89 Excitation,
91 OdeState,
93 Time,
95 Geometry,
97 Material,
99}
100
101#[derive(Clone, Debug)]
106pub struct ParamSpec {
107 pub name: Symbol,
109 pub default: Option<Value>,
111 pub unit: Option<Symbol>,
113 pub role: ParamRole,
115}
116
117#[derive(Clone, Debug, Default)]
137pub struct ParamSet {
138 pub entries: Vec<(Symbol, Value)>,
140}
141
142impl ParamSet {
143 pub fn new(entries: Vec<(Symbol, Value)>) -> Self {
145 Self { entries }
146 }
147
148 pub fn get(&self, name: &Symbol) -> Option<&Value> {
150 self.entries
151 .iter()
152 .find(|(symbol, _)| symbol == name)
153 .map(|(_, value)| value)
154 }
155
156 pub fn symbols(&self) -> BTreeSet<Symbol> {
158 self.entries
159 .iter()
160 .map(|(symbol, _)| symbol.clone())
161 .collect()
162 }
163
164 pub fn fingerprint(&self, cx: &mut Cx) -> StableId {
169 let mut text = String::new();
170 for (symbol, value) in &self.entries {
171 let display = value
172 .object()
173 .display(cx)
174 .unwrap_or_else(|_| "#<display-error>".to_owned());
175 text.push_str(&symbol.to_string());
176 text.push('=');
177 text.push_str(&display);
178 text.push(';');
179 }
180 StableId::from_hashable(&text)
181 }
182}
183
184#[derive(Clone, Debug, PartialEq, Eq, Hash)]
189pub struct FemmLimits {
190 pub max_nodes: usize,
192 pub max_elements: usize,
194 pub max_nnz: usize,
196 pub max_solve_iters: usize,
198 pub max_output_samples: usize,
200 pub max_femm_solves: usize,
202 pub max_wall_ms: u64,
204}
205
206impl Default for FemmLimits {
207 fn default() -> Self {
208 Self {
209 max_nodes: 10_000,
210 max_elements: 20_000,
211 max_nnz: 200_000,
212 max_solve_iters: 4_000,
213 max_output_samples: 20_000,
214 max_femm_solves: 1_000,
215 max_wall_ms: Duration::from_secs(30).as_millis() as u64,
216 }
217 }
218}
219
220pub fn femm_capabilities(
226 installed_field: bool,
227 installed_ptc: bool,
228 installed_adjoint: bool,
229) -> Vec<String> {
230 let mut values = vec![
231 "Magnetostatic".to_owned(),
232 "MagneticsHarmonic".to_owned(),
233 "Electrostatic".to_owned(),
234 "HeatSteady".to_owned(),
235 "CurrentSteady".to_owned(),
236 ];
237 values.push(
238 if installed_ptc {
239 "femm-ptc:installed"
240 } else {
241 "femm-ptc:unavailable"
242 }
243 .to_owned(),
244 );
245 values.push(
246 if installed_adjoint {
247 "femm-adjoint:installed"
248 } else {
249 "femm-adjoint:unavailable"
250 }
251 .to_owned(),
252 );
253 values.push(
254 if installed_field {
255 "numbers/field:installed"
256 } else {
257 "numbers/field:unavailable"
258 }
259 .to_owned(),
260 );
261 values
262}
263
264pub fn parse_finite_number(text: &str) -> Option<f64> {
281 let value = if let Some((num, den)) = text.split_once('/') {
282 let num = num.parse::<f64>().ok()?;
283 let den = den.parse::<f64>().ok()?;
284 if den == 0.0 {
285 return None;
286 }
287 num / den
288 } else {
289 text.parse::<f64>().ok()?
290 };
291 value.is_finite().then_some(value)
292}
293
294pub fn parse_displayed_number(text: &str) -> Option<f64> {
299 parse_finite_number(text)
300}
301
302pub fn value_as_f64(cx: &mut Cx, value: &Value) -> FemmResult<f64> {
307 let display = value
308 .object()
309 .display(cx)
310 .map_err(|err| FemmError::InvalidGeometry(err.to_string()))?;
311 parse_displayed_number(&display)
312 .ok_or_else(|| FemmError::InvalidGeometry(format!("expected scalar number, got {display}")))
313}
314
315pub fn stable_summary(name: &str, fields: &[(&str, String)]) -> String {
320 let mut out = format!("{name}(");
321 for (index, (field, value)) in fields.iter().enumerate() {
322 if index > 0 {
323 out.push_str(", ");
324 }
325 out.push_str(field);
326 out.push('=');
327 out.push_str(value);
328 }
329 out.push(')');
330 out
331}
332
333fn version_symbol() -> Symbol {
334 Symbol::qualified("femm", "version")
335}
336
337fn capabilities_symbol() -> Symbol {
338 Symbol::qualified("femm", "capabilities")
339}
340
341#[derive(Clone)]
342struct FemmCoreFunction {
343 symbol: Symbol,
344}
345
346impl Object for FemmCoreFunction {
347 fn display(&self, _cx: &mut Cx) -> KernelResult<String> {
348 Ok(format!("#<function {}>", self.symbol))
349 }
350
351 fn as_any(&self) -> &dyn Any {
352 self
353 }
354}
355
356impl sim_kernel::ObjectCompat for FemmCoreFunction {
357 fn class(&self, cx: &mut Cx) -> KernelResult<ClassRef> {
358 if let Some(class) = cx
359 .registry()
360 .class_by_symbol(&Symbol::qualified("core", "Function"))
361 {
362 return Ok(class.clone());
363 }
364 DefaultFactory.class_stub(
365 sim_kernel::CORE_FUNCTION_CLASS_ID,
366 Symbol::qualified("core", "Function"),
367 )
368 }
369 fn as_expr(&self, _cx: &mut Cx) -> KernelResult<Expr> {
370 Ok(Expr::Symbol(self.symbol.clone()))
371 }
372 fn as_callable(&self) -> Option<&dyn Callable> {
373 Some(self)
374 }
375}
376
377impl Callable for FemmCoreFunction {
378 fn call(&self, cx: &mut Cx, _args: Args) -> KernelResult<Value> {
379 if self.symbol == version_symbol() {
380 return cx.factory().string("0.1.0".to_owned());
381 }
382 let installed_field = cx
383 .registry()
384 .number_domain_by_symbol(&Symbol::qualified("numbers", "field"))
385 .is_some();
386 let installed_ptc = sim_lib_numbers_numeric::global_numeric_registry()
387 .read()
388 .map(|registry| registry.ode_fixed(&Symbol::new("femm-ptc")).is_some())
389 .unwrap_or(false);
390 let installed_adjoint = sim_lib_numbers_numeric::global_numeric_registry()
391 .read()
392 .map(|registry| {
393 registry
394 .differentiator(&Symbol::new("femm-adjoint"))
395 .is_some()
396 })
397 .unwrap_or(false);
398 let values = femm_capabilities(installed_field, installed_ptc, installed_adjoint)
399 .into_iter()
400 .map(|item| cx.factory().string(item))
401 .collect::<KernelResult<Vec<_>>>()?;
402 cx.factory().list(values)
403 }
404
405 fn call_exprs(&self, cx: &mut Cx, _args: RawArgs) -> KernelResult<Value> {
406 self.call(cx, Args::default())
407 }
408}
409
410pub struct FemmCoreLib;
416
417impl FemmCoreLib {
418 pub fn new() -> Self {
420 Self
421 }
422}
423
424impl Default for FemmCoreLib {
425 fn default() -> Self {
426 Self::new()
427 }
428}
429
430impl Lib for FemmCoreLib {
431 fn manifest(&self) -> LibManifest {
432 LibManifest {
433 id: Symbol::qualified("femm", "core"),
434 version: Version(env!("CARGO_PKG_VERSION").to_owned()),
435 abi: AbiVersion { major: 0, minor: 1 },
436 target: LibTarget::HostRegistered,
437 requires: vec![Dependency {
438 id: Symbol::qualified("numbers", "numeric"),
439 minimum_version: None,
440 }],
441 capabilities: Vec::new(),
442 exports: vec![
443 sim_kernel::Export::Function {
444 symbol: version_symbol(),
445 function_id: None,
446 },
447 sim_kernel::Export::Function {
448 symbol: capabilities_symbol(),
449 function_id: None,
450 },
451 ],
452 }
453 }
454
455 fn load(&self, _cx: &mut sim_kernel::LoadCx, linker: &mut Linker<'_>) -> KernelResult<()> {
456 for symbol in [version_symbol(), capabilities_symbol()] {
457 linker.function_value(
458 symbol.clone(),
459 DefaultFactory.opaque(Arc::new(FemmCoreFunction { symbol }))?,
460 )?;
461 }
462 Ok(())
463 }
464}
465
466#[cfg(test)]
467mod tests;