1#![forbid(unsafe_code)]
2use std::{any::Any, sync::Arc};
5
6use sim_kernel::{
7 ClassId, ClassRef, Cx, DefaultFactory, Expr, Factory, Object, ObjectEncode, ObjectEncoding,
8 Result as KernelResult, Symbol, Value,
9};
10use sim_lib_femm_core::{FemmError, FemmLimits, FemmResult, ParamSet};
11use sim_lib_femm_field::{Field, Projection};
12use sim_lib_femm_material::{BoundaryKind, Source};
13use sim_lib_femm_mesh::FemmModel;
14use sim_lib_femm_post::{Excitation, FemmSolution, QuantitySpec, quantity};
15use sim_lib_femm_solve::solve_steady;
16use sim_lib_numbers_func::{Func, FuncMetadata};
17
18#[derive(Clone, Debug)]
24pub struct FemmCall {
25 pub params: ParamSet,
27 pub query: OutputQuery,
29 pub want_grad: Option<Vec<Symbol>>,
31 pub limits: FemmLimits,
33}
34
35#[derive(Clone, Debug)]
37pub enum OutputQuery {
38 Quantity(QuantitySpec),
40 Field(Projection),
42 Solution,
44}
45
46#[derive(Clone, Debug)]
48pub struct FemmEval {
49 pub value: Value,
51 pub gradient: Option<Vec<(Symbol, f64)>>,
53 pub diagnostics: Vec<sim_kernel::Diagnostic>,
55}
56
57#[derive(Clone)]
62pub struct FemmFuncPayload {
63 pub model: FemmModel,
65 pub vars: Vec<Symbol>,
67 pub query: OutputQuery,
69}
70
71impl Object for FemmFuncPayload {
72 fn display(&self, _cx: &mut Cx) -> KernelResult<String> {
73 Ok(format!(
74 "#<femm-payload model={} query={}>",
75 self.model.id.0,
76 describe_query(&self.query)
77 ))
78 }
79
80 fn as_any(&self) -> &dyn Any {
81 self
82 }
83}
84
85impl sim_kernel::ObjectCompat for FemmFuncPayload {
86 fn class(&self, cx: &mut Cx) -> KernelResult<ClassRef> {
87 if let Some(class) = cx
88 .registry()
89 .class_by_symbol(&Symbol::qualified("femm", "FuncPayload"))
90 {
91 return Ok(class.clone());
92 }
93 DefaultFactory.class_stub(ClassId(33), Symbol::qualified("femm", "FuncPayload"))
94 }
95 fn as_expr(&self, cx: &mut Cx) -> KernelResult<Expr> {
96 sim_citizen::constructor_expr(cx, self)
97 }
98 fn as_object_encoder(&self) -> Option<&dyn ObjectEncode> {
99 Some(self)
100 }
101}
102
103impl ObjectEncode for FemmFuncPayload {
104 fn object_encoding(&self, _cx: &mut Cx) -> KernelResult<ObjectEncoding> {
105 Ok(ObjectEncoding::Constructor {
106 class: func_payload_class_symbol(),
107 args: payload_constructor_args(self),
108 })
109 }
110}
111
112impl sim_citizen::Citizen for FemmFuncPayload {
113 fn citizen_symbol() -> Symbol {
114 func_payload_class_symbol()
115 }
116
117 fn citizen_version() -> u32 {
118 1
119 }
120
121 fn citizen_arity() -> usize {
122 3
123 }
124
125 fn citizen_fields() -> &'static [&'static str] {
126 &["model_id", "query", "vars"]
127 }
128}
129
130fn func_payload_class_symbol() -> Symbol {
131 Symbol::qualified("femm", "FuncPayload")
132}
133
134fn payload_constructor_args(payload: &FemmFuncPayload) -> Vec<Expr> {
135 vec![
136 Expr::Symbol(Symbol::new("v1")),
137 int_expr(payload.model.id.0),
138 Expr::String(describe_query(&payload.query)),
139 Expr::List(
140 payload
141 .vars
142 .iter()
143 .map(|name| Expr::String(name.to_string()))
144 .collect(),
145 ),
146 ]
147}
148
149fn int_expr(value: impl ToString) -> Expr {
150 Expr::Number(sim_kernel::NumberLiteral {
151 domain: Symbol::qualified("citizen", "int"),
152 canonical: value.to_string(),
153 })
154}
155
156pub trait FemmCallable {
158 fn eval(&self, cx: &mut Cx, call: FemmCall) -> FemmResult<FemmEval>;
160}
161
162#[derive(Clone)]
167pub struct ModelCallable {
168 pub model: FemmModel,
170}
171
172impl ModelCallable {
173 fn solve_solution(
174 &self,
175 cx: &mut Cx,
176 params: &ParamSet,
177 limits: &FemmLimits,
178 ) -> FemmResult<Arc<FemmSolution>> {
179 let resolved = resolve_model_params(&self.model, params.clone())?;
180 solve_steady(cx, &self.model, &resolved, limits, None).map(|out| out.solution)
181 }
182}
183
184impl FemmCallable for ModelCallable {
185 fn eval(&self, cx: &mut Cx, call: FemmCall) -> FemmResult<FemmEval> {
186 let resolved = resolve_model_params(&self.model, call.params)?;
187 match call.query {
188 OutputQuery::Quantity(QuantitySpec::Custom { expr, .. }) => {
189 let value = sim_lib_femm_geometry::eval_expr_f64(cx, &expr, &resolved, &[])?;
190 Ok(FemmEval {
191 value: cx
192 .factory()
193 .number_literal(Symbol::qualified("numbers", "f64"), value.to_string())
194 .map_err(|err| FemmError::SensitivityUnavailable(err.to_string()))?,
195 gradient: None,
196 diagnostics: Vec::new(),
197 })
198 }
199 OutputQuery::Quantity(spec) => {
200 let solution = self.solve_solution(cx, &resolved, &call.limits)?;
201 let excitation = resolve_excitation(cx, &self.model, &resolved, &spec)?;
202 let scalar = quantity(&solution, &spec, &excitation)?;
203 Ok(FemmEval {
204 value: cx
205 .factory()
206 .number_literal(Symbol::qualified("numbers", "f64"), scalar.to_string())
207 .map_err(|err| FemmError::SensitivityUnavailable(err.to_string()))?,
208 gradient: None,
209 diagnostics: Vec::new(),
210 })
211 }
212 OutputQuery::Field(projection) => {
213 let solution = self.solve_solution(cx, &resolved, &call.limits)?;
214 let field = Field::new(solution, projection);
215 Ok(FemmEval {
216 value: cx
217 .factory()
218 .opaque(Arc::new(field))
219 .map_err(|err| FemmError::SensitivityUnavailable(err.to_string()))?,
220 gradient: None,
221 diagnostics: Vec::new(),
222 })
223 }
224 OutputQuery::Solution => {
225 let solution = self.solve_solution(cx, &resolved, &call.limits)?;
226 Ok(FemmEval {
227 value: cx
228 .factory()
229 .opaque(solution)
230 .map_err(|err| FemmError::SensitivityUnavailable(err.to_string()))?,
231 gradient: None,
232 diagnostics: Vec::new(),
233 })
234 }
235 }
236 }
237}
238
239pub fn resolve_model_params(model: &FemmModel, params: ParamSet) -> FemmResult<ParamSet> {
244 let mut entries = params.entries;
245 for input in &model.inputs {
246 if entries.iter().all(|(name, _)| name != &input.name) {
247 let Some(default) = &input.default else {
248 return Err(FemmError::UnknownFemmParameter(input.name.to_string()));
249 };
250 entries.push((input.name.clone(), default.clone()));
251 }
252 }
253 Ok(ParamSet::new(entries))
254}
255
256pub fn resolve_excitation(
265 cx: &mut Cx,
266 model: &FemmModel,
267 params: &ParamSet,
268 spec: &QuantitySpec,
269) -> FemmResult<Excitation> {
270 match spec {
271 QuantitySpec::Inductance { circuit } | QuantitySpec::FluxLinkage { circuit } => {
272 Ok(coil_current(cx, model, params, circuit)?
273 .map(Excitation::with_current)
274 .unwrap_or_else(Excitation::none))
275 }
276 QuantitySpec::Capacitance { conductor } => {
277 Ok(conductor_potential(cx, model, params, conductor)?
278 .map(Excitation::with_potential)
279 .unwrap_or_else(Excitation::none))
280 }
281 _ => Ok(Excitation::none()),
282 }
283}
284
285fn coil_current(
286 cx: &mut Cx,
287 model: &FemmModel,
288 params: &ParamSet,
289 circuit: &Symbol,
290) -> FemmResult<Option<f64>> {
291 for source in &model.sources {
292 if let Source::CircuitCoil { name, current, .. } = source
293 && name == circuit
294 {
295 return sim_lib_femm_geometry::eval_expr_f64(cx, current, params, &[]).map(Some);
296 }
297 }
298 Ok(None)
299}
300
301fn conductor_potential(
302 cx: &mut Cx,
303 model: &FemmModel,
304 params: &ParamSet,
305 conductor: &Symbol,
306) -> FemmResult<Option<f64>> {
307 for boundary in &model.boundaries {
308 if &boundary.name == conductor && matches!(boundary.kind, BoundaryKind::Dirichlet) {
309 return sim_lib_femm_geometry::eval_expr_f64(cx, &boundary.value, params, &[])
310 .map(Some);
311 }
312 }
313 Ok(None)
314}
315
316pub fn femm_as_func(model: FemmModel, vars: Vec<Symbol>, query: OutputQuery) -> Func {
339 let callable = ModelCallable {
340 model: model.clone(),
341 };
342 let closure_vars = vars.clone();
343 let payload_vars = closure_vars.clone();
344 let closure_query = query.clone();
345 let mut func = Func::native(
346 vars,
347 Arc::new(move |cx, args| {
348 let params = ParamSet::new(
349 closure_vars
350 .iter()
351 .cloned()
352 .zip(args.iter().cloned())
353 .collect::<Vec<_>>(),
354 );
355 callable
356 .eval(
357 cx,
358 FemmCall {
359 params,
360 query: closure_query.clone(),
361 want_grad: None,
362 limits: FemmLimits::default(),
363 },
364 )
365 .map(|out| out.value)
366 .map_err(sim_kernel::Error::from)
367 }),
368 );
369 func.metadata = FuncMetadata {
370 source: Some(Symbol::qualified("femm", "model")),
371 differentiator_hint: Some(Symbol::new("femm-adjoint")),
372 payload: DefaultFactory
373 .opaque(Arc::new(FemmFuncPayload {
374 model: model.clone(),
375 vars: payload_vars,
376 query: query.clone(),
377 }))
378 .ok(),
379 };
380 func
381}
382
383pub fn femm_field_func(model: FemmModel) -> Func {
390 Func::native(
391 vec![Symbol::new("x"), Symbol::new("y")],
392 Arc::new(move |cx, args| {
393 let x =
394 sim_lib_femm_core::value_as_f64(cx, &args[0]).map_err(sim_kernel::Error::from)?;
395 let y =
396 sim_lib_femm_core::value_as_f64(cx, &args[1]).map_err(sim_kernel::Error::from)?;
397 let solution = solve_steady(
398 cx,
399 &model,
400 &ParamSet::default(),
401 &FemmLimits::default(),
402 None,
403 )
404 .map_err(sim_kernel::Error::from)?
405 .solution;
406 let field = Field::new(solution, Projection::Potential);
407 cx.factory().number_literal(
408 Symbol::qualified("numbers", "f64"),
409 field.at(x, y).map_err(sim_kernel::Error::from)?.to_string(),
410 )
411 }),
412 )
413}
414
415pub fn describe_query(query: &OutputQuery) -> String {
417 match query {
418 OutputQuery::Quantity(QuantitySpec::Custom { name, .. }) => format!("quantity:{name}"),
419 OutputQuery::Quantity(_) => "quantity".to_owned(),
420 OutputQuery::Field(projection) => format!("field:{projection:?}"),
421 OutputQuery::Solution => "solution".to_owned(),
422 }
423}