1#![forbid(unsafe_code)]
2use std::{any::Any, sync::Arc};
8
9use sim_kernel::{
10 ClassId, ClassRef, Cx, DefaultFactory, Expr, Factory, Object, ObjectEncode, ObjectEncoding,
11 Result as KernelResult, Symbol, Value,
12};
13use sim_lib_femm_core::{FemmError, FemmLimits, FemmResult, ParamSet, StableId, value_as_f64};
14use sim_lib_femm_field::{Field, Projection, field_as_func};
15use sim_lib_femm_material::{BoundaryKind, Source};
16use sim_lib_femm_mesh::FemmModel;
17use sim_lib_femm_post::{Excitation, FemmSolution, QuantitySpec, quantity};
18use sim_lib_femm_solve::{GradientTrust, SolveCertificate, SteadySolve, solve_steady};
19use sim_lib_numbers_func::{Func, FuncMetadata};
20
21#[derive(Clone, Debug)]
27pub struct FemmCall {
28 pub params: ParamSet,
30 pub query: OutputQuery,
32 pub want_grad: Option<Vec<Symbol>>,
34 pub limits: FemmLimits,
36}
37
38#[derive(Clone, Debug)]
40pub enum OutputQuery {
41 Quantity(QuantitySpec),
43 Field(Projection),
45 Solution,
47}
48
49#[derive(Clone, Debug)]
51pub struct FemmEval {
52 pub value: Value,
54 pub gradient: Option<Vec<(Symbol, f64)>>,
56 pub diagnostics: Vec<sim_kernel::Diagnostic>,
58}
59
60#[derive(Clone, Debug)]
62pub struct QualityAnswer {
63 pub value: f64,
65 pub certificate: SolveCertificate,
67 pub gradient: Option<(Vec<f64>, GradientTrust)>,
69}
70
71#[derive(Clone)]
76pub struct FemmFuncPayload {
77 pub model: FemmModel,
79 pub vars: Vec<Symbol>,
81 pub query: OutputQuery,
83}
84
85impl Object for FemmFuncPayload {
86 fn display(&self, _cx: &mut Cx) -> KernelResult<String> {
87 Ok(format!(
88 "#<femm-payload model={} query={}>",
89 self.model.id.0,
90 describe_query(&self.query)
91 ))
92 }
93
94 fn as_any(&self) -> &dyn Any {
95 self
96 }
97}
98
99impl sim_kernel::ObjectCompat for FemmFuncPayload {
100 fn class(&self, cx: &mut Cx) -> KernelResult<ClassRef> {
101 if let Some(class) = cx
102 .registry()
103 .class_by_symbol(&Symbol::qualified("femm", "FuncPayload"))
104 {
105 return Ok(class.clone());
106 }
107 DefaultFactory.class_stub(ClassId(33), Symbol::qualified("femm", "FuncPayload"))
108 }
109 fn as_expr(&self, cx: &mut Cx) -> KernelResult<Expr> {
110 sim_citizen::constructor_expr(cx, self)
111 }
112 fn as_object_encoder(&self) -> Option<&dyn ObjectEncode> {
113 Some(self)
114 }
115}
116
117impl ObjectEncode for FemmFuncPayload {
118 fn object_encoding(&self, _cx: &mut Cx) -> KernelResult<ObjectEncoding> {
119 Ok(ObjectEncoding::Constructor {
120 class: func_payload_class_symbol(),
121 args: payload_constructor_args(self),
122 })
123 }
124}
125
126impl sim_citizen::Citizen for FemmFuncPayload {
127 fn citizen_symbol() -> Symbol {
128 func_payload_class_symbol()
129 }
130
131 fn citizen_version() -> u32 {
132 1
133 }
134
135 fn citizen_arity() -> usize {
136 3
137 }
138
139 fn citizen_fields() -> &'static [&'static str] {
140 &["model_id", "query", "vars"]
141 }
142}
143
144fn func_payload_class_symbol() -> Symbol {
145 Symbol::qualified("femm", "FuncPayload")
146}
147
148fn payload_constructor_args(payload: &FemmFuncPayload) -> Vec<Expr> {
149 vec![
150 Expr::Symbol(Symbol::new("v1")),
151 int_expr(payload.model.id.0),
152 Expr::String(describe_query(&payload.query)),
153 Expr::List(
154 payload
155 .vars
156 .iter()
157 .map(|name| Expr::String(name.to_string()))
158 .collect(),
159 ),
160 ]
161}
162
163fn int_expr(value: impl ToString) -> Expr {
164 Expr::Number(sim_kernel::NumberLiteral {
165 domain: Symbol::qualified("citizen", "int"),
166 canonical: value.to_string(),
167 })
168}
169
170pub trait FemmCallable {
172 fn eval(&self, cx: &mut Cx, call: FemmCall) -> FemmResult<FemmEval>;
174}
175
176#[derive(Clone)]
181pub struct ModelCallable {
182 pub model: FemmModel,
184}
185
186impl ModelCallable {
187 fn resolve_params(&self, params: &ParamSet) -> FemmResult<ParamSet> {
188 let mut entries = params.entries.clone();
189 for input in &self.model.inputs {
190 if entries.iter().all(|(name, _)| name != &input.name) {
191 if let Some(default) = &input.default {
192 entries.push((input.name.clone(), default.clone()));
193 } else {
194 return Err(FemmError::UnknownFemmParameter(input.name.to_string()));
195 }
196 }
197 }
198 Ok(ParamSet::new(entries))
199 }
200
201 fn solve_solution(
202 &self,
203 cx: &mut Cx,
204 params: &ParamSet,
205 limits: &FemmLimits,
206 ) -> FemmResult<Arc<FemmSolution>> {
207 let resolved = self.resolve_params(params)?;
208 solve_steady(cx, &self.model, &resolved, limits, None).map(|out| out.solution)
209 }
210}
211
212impl FemmCallable for ModelCallable {
213 fn eval(&self, cx: &mut Cx, call: FemmCall) -> FemmResult<FemmEval> {
214 let resolved = self.resolve_params(&call.params)?;
215 match call.query {
216 OutputQuery::Quantity(QuantitySpec::Custom { expr, .. }) => {
217 let value = sim_lib_femm_geometry::eval_expr_f64(cx, &expr, &resolved, &[])?;
218 Ok(FemmEval {
219 value: cx
220 .factory()
221 .number_literal(Symbol::qualified("numbers", "f64"), value.to_string())
222 .map_err(|err| FemmError::SensitivityUnavailable(err.to_string()))?,
223 gradient: None,
224 diagnostics: Vec::new(),
225 })
226 }
227 OutputQuery::Quantity(spec) => {
228 let solution = self.solve_solution(cx, &resolved, &call.limits)?;
229 let excitation = resolve_excitation(cx, &self.model, &resolved, &spec)?;
230 let scalar = quantity(&solution, &spec, &excitation)?;
231 Ok(FemmEval {
232 value: cx
233 .factory()
234 .number_literal(Symbol::qualified("numbers", "f64"), scalar.to_string())
235 .map_err(|err| FemmError::SensitivityUnavailable(err.to_string()))?,
236 gradient: None,
237 diagnostics: Vec::new(),
238 })
239 }
240 OutputQuery::Field(projection) => {
241 let solution = self.solve_solution(cx, &resolved, &call.limits)?;
242 let field = Field::new(solution, projection);
243 Ok(FemmEval {
244 value: cx
245 .factory()
246 .opaque(Arc::new(field))
247 .map_err(|err| FemmError::SensitivityUnavailable(err.to_string()))?,
248 gradient: None,
249 diagnostics: Vec::new(),
250 })
251 }
252 OutputQuery::Solution => {
253 let solution = self.solve_solution(cx, &resolved, &call.limits)?;
254 Ok(FemmEval {
255 value: cx
256 .factory()
257 .opaque(solution)
258 .map_err(|err| FemmError::SensitivityUnavailable(err.to_string()))?,
259 gradient: None,
260 diagnostics: Vec::new(),
261 })
262 }
263 }
264 }
265}
266
267pub fn resolve_excitation(
276 cx: &mut Cx,
277 model: &FemmModel,
278 params: &ParamSet,
279 spec: &QuantitySpec,
280) -> FemmResult<Excitation> {
281 match spec {
282 QuantitySpec::Inductance { circuit } | QuantitySpec::FluxLinkage { circuit } => {
283 Ok(coil_current(cx, model, params, circuit)?
284 .map(Excitation::with_current)
285 .unwrap_or_else(Excitation::none))
286 }
287 QuantitySpec::Capacitance { conductor } => {
288 Ok(conductor_potential(cx, model, params, conductor)?
289 .map(Excitation::with_potential)
290 .unwrap_or_else(Excitation::none))
291 }
292 _ => Ok(Excitation::none()),
293 }
294}
295
296fn coil_current(
297 cx: &mut Cx,
298 model: &FemmModel,
299 params: &ParamSet,
300 circuit: &Symbol,
301) -> FemmResult<Option<f64>> {
302 for source in &model.sources {
303 if let Source::CircuitCoil { name, current, .. } = source
304 && name == circuit
305 {
306 return sim_lib_femm_geometry::eval_expr_f64(cx, current, params, &[]).map(Some);
307 }
308 }
309 Ok(None)
310}
311
312fn conductor_potential(
313 cx: &mut Cx,
314 model: &FemmModel,
315 params: &ParamSet,
316 conductor: &Symbol,
317) -> FemmResult<Option<f64>> {
318 for boundary in &model.boundaries {
319 if &boundary.name == conductor && matches!(boundary.kind, BoundaryKind::Dirichlet) {
320 return sim_lib_femm_geometry::eval_expr_f64(cx, &boundary.value, params, &[])
321 .map(Some);
322 }
323 }
324 Ok(None)
325}
326
327pub fn quality(
333 cx: &mut Cx,
334 solve: &SteadySolve,
335 quantity_spec: &QuantitySpec,
336 wrt: Option<&[Symbol]>,
337) -> FemmResult<QualityAnswer> {
338 let excitation = resolve_excitation(cx, &solve.model, &solve.solution.params, quantity_spec)?;
339 let value = quantity(&solve.solution, quantity_spec, &excitation)?;
340 let mut certificate = solve.certificate.clone();
341 let gradient = match wrt {
342 None => None,
343 Some(params) => {
344 let (values, trust) =
345 finite_difference_quality_gradient(cx, solve, quantity_spec, params)?;
346 certificate.set_gradient_trust(trust.clone());
347 Some((values, trust))
348 }
349 };
350 Ok(QualityAnswer {
351 value,
352 certificate,
353 gradient,
354 })
355}
356
357fn finite_difference_quality_gradient(
358 cx: &mut Cx,
359 solve: &SteadySolve,
360 quantity_spec: &QuantitySpec,
361 wrt: &[Symbol],
362) -> FemmResult<(Vec<f64>, GradientTrust)> {
363 let callable = ModelCallable {
364 model: solve.model.clone(),
365 };
366 let base_params = callable.resolve_params(&solve.solution.params)?;
367 let mut out = Vec::with_capacity(wrt.len());
368 for symbol in wrt {
369 let base_value = base_params
370 .get(symbol)
371 .ok_or_else(|| FemmError::UnknownFemmParameter(symbol.to_string()))?;
372 let x = value_as_f64(cx, base_value)?;
373 if !x.is_finite() {
374 return Err(FemmError::SensitivityUnavailable(format!(
375 "non-finite FEMM parameter {symbol}"
376 )));
377 }
378 let h = fd_step(x);
379 let plus = replace_param_value(cx, &base_params, symbol, x + h)?;
380 let minus = replace_param_value(cx, &base_params, symbol, x - h)?;
381 let plus_value = quality_at_params(cx, &solve.model, plus, quantity_spec)?;
382 let minus_value = quality_at_params(cx, &solve.model, minus, quantity_spec)?;
383 out.push((plus_value - minus_value) / (2.0 * h));
384 }
385 Ok((out, GradientTrust::FiniteDifferenceOnly))
386}
387
388fn quality_at_params(
389 cx: &mut Cx,
390 model: &FemmModel,
391 params: ParamSet,
392 quantity_spec: &QuantitySpec,
393) -> FemmResult<f64> {
394 let solved = solve_steady(cx, model, ¶ms, &FemmLimits::default(), None)?;
395 let excitation = resolve_excitation(cx, model, ¶ms, quantity_spec)?;
396 quantity(&solved.solution, quantity_spec, &excitation)
397}
398
399fn replace_param_value(
400 cx: &mut Cx,
401 params: &ParamSet,
402 name: &Symbol,
403 value: f64,
404) -> FemmResult<ParamSet> {
405 let mut found = false;
406 let mut entries = params.entries.clone();
407 for (symbol, slot) in &mut entries {
408 if symbol == name {
409 *slot = cx
410 .factory()
411 .number_literal(Symbol::qualified("numbers", "f64"), value.to_string())
412 .map_err(|err| FemmError::SensitivityUnavailable(err.to_string()))?;
413 found = true;
414 }
415 }
416 if found {
417 Ok(ParamSet::new(entries))
418 } else {
419 Err(FemmError::UnknownFemmParameter(name.to_string()))
420 }
421}
422
423fn fd_step(value: f64) -> f64 {
424 1.0e-6 * value.abs().max(1.0)
425}
426
427pub fn femm_as_func(model: FemmModel, vars: Vec<Symbol>, query: OutputQuery) -> Func {
450 let callable = ModelCallable {
451 model: model.clone(),
452 };
453 let closure_vars = vars.clone();
454 let payload_vars = closure_vars.clone();
455 let closure_query = query.clone();
456 Func {
457 vars,
458 body_cas: None,
459 body_native: Some(Arc::new(move |cx, args| {
460 let params = ParamSet::new(
461 closure_vars
462 .iter()
463 .cloned()
464 .zip(args.iter().cloned())
465 .collect::<Vec<_>>(),
466 );
467 callable
468 .eval(
469 cx,
470 FemmCall {
471 params,
472 query: closure_query.clone(),
473 want_grad: None,
474 limits: FemmLimits::default(),
475 },
476 )
477 .map(|out| out.value)
478 .map_err(sim_kernel::Error::from)
479 })),
480 metadata: FuncMetadata {
481 source: Some(Symbol::qualified("femm", "model")),
482 differentiator_hint: Some(Symbol::new("femm-adjoint")),
483 payload: DefaultFactory
484 .opaque(Arc::new(FemmFuncPayload {
485 model: model.clone(),
486 vars: payload_vars,
487 query: query.clone(),
488 }))
489 .ok(),
490 },
491 }
492}
493
494pub fn femm_field_func(model: FemmModel) -> Func {
500 let field = Arc::new(FemmSolution {
501 id: StableId(model.id.0 + 1),
502 model_id: model.id,
503 physics: model.physics.clone(),
504 formulation: model.formulation.clone(),
505 params: ParamSet::default(),
506 mesh: sim_lib_femm_mesh::FemMesh2 {
507 xy: vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
508 tri: vec![[0, 1, 2]],
509 elem_region: vec![Symbol::new("air")],
510 edge_boundary: Vec::new(),
511 },
512 u: vec![0.0, 1.0, 1.0],
513 diagnostics: sim_lib_femm_flow::SolveDiagnostics {
514 method: Symbol::new("femm-ptc"),
515 converged: true,
516 iterations: 1,
517 final_residual: 0.0,
518 events: Vec::new(),
519 diagnostics: Vec::new(),
520 },
521 });
522 field_as_func(Field::new(field, Projection::Potential))
523}
524
525pub(crate) fn describe_query(query: &OutputQuery) -> String {
526 match query {
527 OutputQuery::Quantity(QuantitySpec::Custom { name, .. }) => format!("quantity:{name}"),
528 OutputQuery::Quantity(_) => "quantity".to_owned(),
529 OutputQuery::Field(projection) => format!("field:{projection:?}"),
530 OutputQuery::Solution => "solution".to_owned(),
531 }
532}