stem_material/iron_losses/jordan_model.rs
1#![doc = r#"
2An implementation of the Jordan model for iron losses in the core lamination.
3
4The Jordan loss model for iron losses offers a simple calculation heuristic for
5a sinusoidal flux density change over time. It separates iron losses into static
6hysteresis losses and dynamic eddy current losses via the following formula:
7
8`p = kh * f * B² + kec * (f * B)²`,
9
10where `f` is the frequency and `B` is the amplitude of the flux density. The
11hysteresis loss factor `kh` and the eddy current loss factor `kec` are derived
12by fitting measured loss curves. See \[1\] and \[2\] for more.
13
14This module offers the [`JordanModel`] struct, a simple container for the two
15loss coefficients which provides the formula given above via its
16[`JordanModel::losses`] method. The struct implements [`IsQuantityFunction`] and
17can therefore be used as the
18[iron loss model](crate::material::Material::iron_losses) of a
19[`Material`](crate::material::Material).
20
21The coefficients can be obtained from measured loss curves by constructing an
22[`IronLossData`] instance out of them and then fallibly converting it via
23[`TryFrom`] into a [`JordanModel`]. Under the hood, the curves are fitted to the
24loss equation using a least-square optimization with the coefficients being the
25variables. The [`FailedCoefficientCalculation`] error type is returned in case
26the fitting failed for some reason. Lastly, the types
27[`IronLossCharacteristic`] and [`FluxDensityLossPair`] are used within the
28construction of [`IronLossData`] to guard against bad input data on the type
29level.
30
31# Example
32
33The image below shows a comparison between raw loss data and the fitted
34[`JordanModel`] from `examples/jordan_model.rs`. While the model can represent
35the loss behaviour at lower frequencies very well, it fails at higher
36frequencies for this particular set of data points.
37
38![Jordan model][jordan_model]
39
40"#]
41#![cfg_attr(feature = "doc-images",
42cfg_attr(all(),
43doc = ::embed_doc_image::embed_image!("jordan_model", "docs/img/jordan_model.svg"),
44))]
45#![cfg_attr(
46 not(feature = "doc-images"),
47 doc = "**Doc images not enabled**. Compile docs with `cargo doc --features 'doc-images'` and Rust version >= 1.54."
48)]
49#![doc = r#"
50
51# Literature
52
53> \[1\] Krings, A. and Soulard, J.: Overview and comparison of iron loss models
54for electrical machines. EVRE Monaco, March 2010. URL:
55<https://www.researchgate.net/profile/Andreas-Krings/publication/228490936_Overview_and_Comparison_of_Iron_Loss_Models_for_Electrical_Machines/links/02e7e51935e2728dda000000/Overview-and-Comparison-of-Iron-Loss-Models-for-Electrical-Machines.pdf>
56
57> \[2\] Graham, C. D.: Physical origin of losses in conducting ferromagnetic
58materials. Journal of Applied Physics, vol. 53, no. 11, pp. 8276-8280, Nov.1982
59"#]
60
61use argmin::{
62 core::{CostFunction, State},
63 solver::neldermead::NelderMead,
64};
65use var_quantity::DynQuantity;
66
67#[cfg(feature = "serde")]
68use serde::{Deserialize, Serialize};
69
70#[cfg(feature = "serde")]
71use var_quantity::deserialize_quantity;
72
73use var_quantity::IsQuantityFunction;
74use var_quantity::uom::si::{
75 f64::*, frequency::hertz, magnetic_flux_density::tesla, ratio::ratio,
76 specific_power::watt_per_kilogram,
77};
78
79/**
80Implementation of the Jordan iron loss model.
81
82As discussed in the
83[module-level documentation](crate::iron_losses::jordan_model), this struct
84contains the hysteresis and eddy current loss coefficients of the Jordan iron
85loss model:
86
87`p = kh * f * B² + kec * (f * B)²`.
88
89This model is valid for a magnetic flux density which changes sinusoidally over
90time with the frequency `f` (normalized to 50 Hz) and the amplitude `B`
91(normalized to 1.5 T). The [`losses`](JordanModel::losses) method uses this very
92formula, dividing input flux density by 1.5 (see
93[`JordanModel::reference_flux_density`]) and frequency by 50 (see
94[`JordanModel::reference_frequency`]).
95These normalization factors correspond to those usually used in literature, see
96e.g. eq. (6.4.10) and (6.4.11) in \[1]\.
97
98# Constructing a Jordan loss model
99
100If the coefficients are known, a [`JordanModel`] can be constructed via the
101default field assignment constructor (the
102[One True Constructor](https://doc.rust-lang.org/nomicon/constructors.html)).
103Alternatively, the coefficients can be derived by fitting loss curves into the
104loss equation. This is done by first creating an [`IronLossData`] struct and
105then fallibly converting it into a [`JordanModel`] using [`TryFrom`]. Under the
106hood, a least-square minimization / optimization is performed during conversion
107to find the coefficients which match the given curves the best. See
108[`IronLossData`] for more.
109
110# Usage in `Material`
111
112This struct is meant to be used for the
113[`Material::iron_losses`](crate::material::Material::iron_losses), hence it
114implements [`IsQuantityFunction`]. Inside the [`IsQuantityFunction::call`]
115function, the input conditions are searched for an entry whose unit corresponds
116to that of the magnetic flux density and another one which matches that of the
117frequency. If either one cannot be found, a value of zero is assumed, which
118means that the returned losses are zero as well:
119
120```
121use stem_material::prelude::*;
122
123let model = JordanModel {
124 hysteresis_coefficient: SpecificPower::new::<watt_per_kilogram>(1.0),
125 eddy_current_coefficient: SpecificPower::new::<watt_per_kilogram>(0.5),
126};
127
128let conditions = &[ThermodynamicTemperature::new::<degree_celsius>(20.0).into()];
129assert_eq!(model.call(conditions).value, 0.0);
130
131// This call returns the sum of the coefficients, because the input matches
132// the reference values and therefore the resulting `f` and `B` are 1
133let conditions = &[MagneticFluxDensity::new::<tesla>(1.5).into(), Frequency::new::<hertz>(50.0).into()];
134assert_eq!(model.call(conditions).value, 1.5);
135```
136
137# Serialization and deserialization
138
139A [`JordanModel`] is serialized as one would expect: A struct with two fields.
140However, it can be deserialized both from said two-fields representation and
141from that of [`IronLossData`]. In case of the latter, the serialized data is
142first deserialized into [`IronLossData`], which is then converted into a
143[`JordanModel`]. Since an untagged enum is used for deserialization, it is not
144necessary to use a tag in the latter case. This is shown below using the
145yaml-format:
146
147```ignore
148JordanModel:
149 hysteresis_coefficient: 1 W/kg
150 eddy_current_coefficient: 1 W/kg
151```
152
153or
154
155```ignore
156JordanModel:
157 - frequency: 50.0 Hz
158 characteristic:
159 - flux_density: 0.5 T
160 specific_loss: 0.86 W/kg
161 - flux_density: 0.6 T
162 specific_loss: 1.16 W/kg
163... (more entries for the loss curves)
164```
165both result in a [`JordanModel`], provided the conversion doesn't fail in case
166of the latter.
167
168# Literature
169
170> \[1\] Müller, G., Vogt, K. and Ponick, B.: Berechnung elektrischer Maschinen,
1716th edition, Wiley-VCH, 2008.
172 */
173#[derive(Debug, Clone, PartialEq)]
174#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
175#[cfg_attr(feature = "serde", serde(try_from = "serde_impl::JordanModelDeEnum"))]
176pub struct JordanModel {
177 /// Static hysteresis loss coefficient `kh`.
178 #[cfg_attr(feature = "serde", serde(deserialize_with = "deserialize_quantity"))]
179 pub hysteresis_coefficient: SpecificPower,
180 /// Dynamic eddy current loss coefficient `kec`.
181 #[cfg_attr(feature = "serde", serde(deserialize_with = "deserialize_quantity"))]
182 pub eddy_current_coefficient: SpecificPower,
183}
184
185impl JordanModel {
186 /**
187 Creates a new [`JordanModel`] from its coefficients.
188 */
189 pub fn new(
190 hysteresis_coefficient: SpecificPower,
191 eddy_current_coefficient: SpecificPower,
192 ) -> Self {
193 return Self {
194 hysteresis_coefficient,
195 eddy_current_coefficient,
196 };
197 }
198
199 /**
200 Returns the "reference frequency" of 50 Hz used in the model.
201
202 A frequency input to [`JordanModel::losses`] or [`JordanModel::call`] is
203 divided by this value before being inserted into the model equation.
204
205 # Examples
206
207 ```
208 use stem_material::prelude::*;
209
210 assert_eq!(JordanModel::reference_frequency().get::<hertz>(), 50.0);
211 ```
212 */
213 pub fn reference_frequency() -> Frequency {
214 return Frequency::new::<hertz>(50.0);
215 }
216
217 /**
218 Returns the "reference flux density" of 1.5 T used in the model.
219
220 A flux density input to [`JordanModel::losses`] or [`JordanModel::call`] is
221 divided by this value before being inserted into the model equation.
222
223 # Examples
224
225 ```
226 use stem_material::prelude::*;
227
228 assert_eq!(JordanModel::reference_flux_density().get::<tesla>(), 1.50);
229 ```
230 */
231 pub fn reference_flux_density() -> MagneticFluxDensity {
232 return MagneticFluxDensity::new::<tesla>(1.5);
233 }
234
235 /**
236 Returns the specific losses for a sinusoidal changing magnetic flux density
237 with the amplitude `magnetic_flux_density` and the specified `frequency`.
238
239 This function returns the result ``p of the equation:
240
241 `p = kh * f * B² + kec * (f * B)²`,
242
243 where `kh` corresponds to [`JordanModel::hysteresis_coefficient`] and `kec`
244 corresponds to [`JordanModel::eddy_current_coefficient`]. The arguments
245 are normalized using [`JordanModel::reference_frequency`] and
246 [`JordanModel::reference_flux_density`]:
247
248 `B = magnetic_flux_density / JordanModel::reference_flux_density()`
249
250 `f = frequency / JordanModel::reference_frequency()`
251
252 The [`IsQuantityFunction::call`] implementation for [`JordanModel`] uses
253 this function after identifying `magnetic_flux_density` and `frequency` from
254 the `conditions`.
255
256 # Examples
257
258 ```
259 use stem_material::prelude::*;
260
261 let model = JordanModel {
262 hysteresis_coefficient: SpecificPower::new::<watt_per_kilogram>(1.0),
263 eddy_current_coefficient: SpecificPower::new::<watt_per_kilogram>(0.5),
264 };
265
266 // This call returns the sum of the coefficients, because the input matches
267 // the reference values and therefore the resulting `f` and `B` are 1
268 assert_eq!(model.losses(MagneticFluxDensity::new::<tesla>(1.5), Frequency::new::<hertz>(50.0)).value, 1.5);
269
270 // Double the frequency - Losses rise drastically (nonlinear dependency)
271 assert_eq!(model.losses(MagneticFluxDensity::new::<tesla>(1.5), Frequency::new::<hertz>(100.0)).value, 5.0);
272 ```
273 */
274 pub fn losses(
275 &self,
276 magnetic_flux_density: MagneticFluxDensity,
277 frequency: Frequency,
278 ) -> SpecificPower {
279 return losses(
280 magnetic_flux_density,
281 frequency,
282 self.eddy_current_coefficient,
283 self.hysteresis_coefficient,
284 );
285 }
286}
287
288#[cfg_attr(feature = "serde", typetag::serde)]
289impl IsQuantityFunction for JordanModel {
290 fn call(&self, conditions: &[DynQuantity<f64>]) -> DynQuantity<f64> {
291 let mut flux_density = MagneticFluxDensity::new::<tesla>(0.0);
292 let mut frequency = Frequency::new::<hertz>(0.0);
293 for factor in conditions {
294 if let Ok(fd) = MagneticFluxDensity::try_from(*factor) {
295 flux_density = fd;
296 } else if let Ok(f) = Frequency::try_from(*factor) {
297 frequency = f;
298 }
299 }
300 return self.losses(flux_density, frequency).into();
301 }
302
303 fn dyn_eq(&self, other: &dyn IsQuantityFunction) -> bool {
304 (other as &dyn std::any::Any).downcast_ref::<Self>() == Some(self)
305 }
306}
307
308/**
309Actual loss calculation function. Factored out from the [`JordanModel`] method
310of the same name because it is also used in [`TryFrom<IronLossData>`].s
311 */
312fn losses(
313 flux_density: MagneticFluxDensity,
314 frequency: Frequency,
315 hysteresis_coefficient: SpecificPower,
316 eddy_current_coefficient: SpecificPower,
317) -> SpecificPower {
318 let f_norm = JordanModel::reference_frequency();
319 let b_norm = JordanModel::reference_flux_density();
320
321 return hysteresis_coefficient
322 * (frequency / f_norm)
323 * (flux_density / b_norm).get::<ratio>().powi(2)
324 + eddy_current_coefficient
325 * (frequency / f_norm).get::<ratio>().powi(2)
326 * (flux_density / b_norm).get::<ratio>().powi(2);
327}
328
329impl Default for JordanModel {
330 fn default() -> Self {
331 Self {
332 hysteresis_coefficient: SpecificPower::new::<watt_per_kilogram>(0.0),
333 eddy_current_coefficient: SpecificPower::new::<watt_per_kilogram>(0.0),
334 }
335 }
336}
337
338// =============================================================================
339
340/**
341This struct is a "flattened" version of [`IronLossData`]. It is not meant to be
342used on its own and is just exposed so the optimization result of
343[`IronLossData::solve_for_coefficients`] can be examined. See its docstring for
344more.
345 */
346pub struct FitLossCurve {
347 frequencies: Vec<Frequency>,
348 flux_densities: Vec<MagneticFluxDensity>,
349 specific_losses: Vec<SpecificPower>,
350}
351
352impl CostFunction for FitLossCurve {
353 type Param = Vec<f64>;
354 type Output = f64;
355
356 fn cost(&self, p: &Self::Param) -> Result<Self::Output, argmin::core::Error> {
357 let mut err = 0.0; // W/kg
358
359 // Convert to SI units
360 let hysteresis_coefficient = SpecificPower::new::<watt_per_kilogram>(p[0]);
361 let eddy_current_coefficient = SpecificPower::new::<watt_per_kilogram>(p[1]);
362
363 for (fi, (bi, pi)) in self
364 .frequencies
365 .iter()
366 .zip(self.flux_densities.iter().zip(self.specific_losses.iter()))
367 {
368 err = err
369 + (*pi - losses(*bi, *fi, hysteresis_coefficient, eddy_current_coefficient))
370 .get::<watt_per_kilogram>()
371 .powi(2);
372 }
373 Ok(err)
374 }
375}
376
377/**
378A container for multiple [`IronLossCharacteristic`]s.
379
380This struct represents a full dataset of multiple loss characteristics at
381different frequencies obtained from either a manufacturer data sheet or from own
382measurements. Its main purpose is to be used for the calculation of the
383[`JordanModel`] coefficients via the
384[`solve_for_coefficients`](IronLossData::solve_for_coefficients) method. This
385method returns the raw result of the underlying fitting as an
386[`argmin::core::OptimizationResult`], which contains the coefficients. For
387convenience, a [`TryFrom<IronLossData>`] implementation exists for
388[`JordanModel`], which calls
389[`solve_for_coefficients`](IronLossData::solve_for_coefficients) and then
390unpacks the coefficients.
391 */
392#[derive(Debug, Clone)]
393#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
394pub struct IronLossData(pub Vec<IronLossCharacteristic>);
395
396impl IronLossData {
397 /**
398 Performs least-square fitting of all the datapoints in `self` into the loss
399 equation using the [`argmin`]. If the fitting succeeds, the raw
400 [`argmin::core::OptimizationResult`] is returned, which can then be
401 examined. In particular, the coefficients can be retrieved with the
402 [`State::get_best_param`](`argmin::core::State::get_best_param`). As a
403 convencience wrapper, a [`TryFrom<IronLossData>`] implementation exists for
404 [`JordanModel`], which calls
405 [`solve_for_coefficients`](IronLossData::solve_for_coefficients) and then
406 unpacks the coefficients.
407
408 # Examples
409
410 ```
411 use stem_material::prelude::*;
412
413 // Expose the get_best_param method
414 use argmin::core::State;
415
416 // First characteristic
417 let frequency = Frequency::new::<hertz>(50.0);
418 let mut datapoints = Vec::new();
419 datapoints.push(FluxDensityLossPair::new(
420 MagneticFluxDensity::new::<tesla>(0.5),
421 SpecificPower::new::<watt_per_kilogram>(2.0)
422 ));
423 datapoints.push(FluxDensityLossPair::new(
424 MagneticFluxDensity::new::<tesla>(0.6),
425 SpecificPower::new::<watt_per_kilogram>(2.5)
426 ));
427 datapoints.push(FluxDensityLossPair::new(
428 MagneticFluxDensity::new::<tesla>(0.7),
429 SpecificPower::new::<watt_per_kilogram>(3.2)
430 ));
431 datapoints.push(FluxDensityLossPair::new(
432 MagneticFluxDensity::new::<tesla>(0.8),
433 SpecificPower::new::<watt_per_kilogram>(4.0)
434 ));
435 let lc_50 = IronLossCharacteristic::new(frequency, datapoints);
436
437 // Second characteristic
438 let frequency = Frequency::new::<hertz>(100.0);
439 let mut datapoints = Vec::new();
440 datapoints.push(FluxDensityLossPair::new(
441 MagneticFluxDensity::new::<tesla>(0.5),
442 SpecificPower::new::<watt_per_kilogram>(5.0)
443 ));
444 datapoints.push(FluxDensityLossPair::new(
445 MagneticFluxDensity::new::<tesla>(0.6),
446 SpecificPower::new::<watt_per_kilogram>(6.0)
447 ));
448 datapoints.push(FluxDensityLossPair::new(
449 MagneticFluxDensity::new::<tesla>(0.7),
450 SpecificPower::new::<watt_per_kilogram>(8.0)
451 ));
452 datapoints.push(FluxDensityLossPair::new(
453 MagneticFluxDensity::new::<tesla>(0.8),
454 SpecificPower::new::<watt_per_kilogram>(12.0)
455 ));
456 let lc_100 = IronLossCharacteristic::new(frequency, datapoints);
457
458 let iron_loss_data = IronLossData(vec![lc_50, lc_100]);
459 let res = iron_loss_data.solve_for_coefficients().expect("fitting succeded");
460 let c = res.state.get_best_param().expect("must contain coefficients");
461
462 // First element is the hysteresis coefficient
463 approx::assert_abs_diff_eq!(c[0], 9.528, epsilon=1e-3);
464
465 // Second element is the eddy current coefficient
466 approx::assert_abs_diff_eq!(c[1], 5.265, epsilon=1e-3);
467 ```
468 */
469 pub fn solve_for_coefficients(
470 &self,
471 ) -> Result<
472 argmin::core::OptimizationResult<
473 FitLossCurve,
474 NelderMead<Vec<f64>, f64>,
475 argmin::core::IterState<Vec<f64>, (), (), (), (), f64>,
476 >,
477 FailedCoefficientCalculation,
478 > {
479 // Concatenate all vectors
480 let mut num_elems: usize = 0;
481 for characteristic in self.0.iter() {
482 num_elems += characteristic.characteristic.len();
483 }
484 let mut frequencies_flat: Vec<Frequency> = Vec::with_capacity(num_elems);
485 let mut flux_density_flat: Vec<MagneticFluxDensity> = Vec::with_capacity(num_elems);
486 let mut specific_losses_flat: Vec<SpecificPower> = Vec::with_capacity(num_elems);
487
488 for characteristic in self.0.iter() {
489 let frequency = characteristic.frequency;
490
491 for flux_density_and_specific_loss in characteristic.characteristic.iter().cloned() {
492 frequencies_flat.push(frequency);
493 flux_density_flat.push(flux_density_and_specific_loss.flux_density);
494 specific_losses_flat.push(flux_density_and_specific_loss.specific_loss);
495 }
496 }
497
498 let fit = FitLossCurve {
499 frequencies: frequencies_flat,
500 flux_densities: flux_density_flat,
501 specific_losses: specific_losses_flat,
502 };
503
504 // All values in W/kg
505 let start_values = vec![
506 vec![3.0f64, 3.0f64],
507 vec![2.0f64, 1.5f64],
508 vec![1.0f64, 0.5f64],
509 ];
510
511 let solver = NelderMead::new(start_values)
512 .with_sd_tolerance(0.0001)
513 .map_err(|error| FailedCoefficientCalculation(Some(error)))?;
514
515 // Run solver
516 return argmin::core::Executor::new(fit, solver)
517 .configure(|state| state.max_iters(200))
518 .run()
519 .map_err(|error| FailedCoefficientCalculation(Some(error)));
520 }
521}
522
523impl TryFrom<IronLossData> for JordanModel {
524 type Error = FailedCoefficientCalculation;
525 fn try_from(value: IronLossData) -> Result<Self, Self::Error> {
526 return (&value).try_into();
527 }
528}
529
530impl TryFrom<&IronLossData> for JordanModel {
531 type Error = FailedCoefficientCalculation;
532
533 fn try_from(value: &IronLossData) -> Result<Self, Self::Error> {
534 let res = value.solve_for_coefficients()?;
535 let solution = res
536 .state
537 .get_best_param()
538 .ok_or(FailedCoefficientCalculation(None))?;
539
540 let hysteresis_coefficient = SpecificPower::new::<watt_per_kilogram>(solution[0]);
541 let eddy_current_coefficient = SpecificPower::new::<watt_per_kilogram>(solution[1]);
542
543 return Ok(JordanModel {
544 hysteresis_coefficient,
545 eddy_current_coefficient,
546 });
547 }
548}
549
550/**
551A iron loss characteristic for a specific frequency.
552
553This struct contains the iron loss characteristic (relationship between
554sinusoidal magnetic flux density amplitude and losses) for a single frequency.
555This characteristic is usually taken from the datasheet of the lamination
556manufacturer or measured by applying a sinusoidal magnetic field at a given
557frequency with different amplitudes to a sample. The losses within the sample
558are then measured and form a [`FluxDensityLossPair`] datapoint together with the
559corresponding amplitude.
560
561One or more of these characteristics form an [`IronLossData`] dataset, which is
562essentially just a vector of [`IronLossCharacteristic`]s. The dataset can then
563be used to derive the coefficients of the [`JordanModel`].
564
565# Examples
566
567```
568use stem_material::prelude::*;
569
570// These datapoints might come from a manufacturer sheet.
571
572// All datapoints were measured at this frequency
573let frequency = Frequency::new::<hertz>(50.0);
574
575// List of the individual datapoints as flux density - loss pairs.
576let mut datapoints = Vec::new();
577datapoints.push(FluxDensityLossPair::new(
578 MagneticFluxDensity::new::<tesla>(0.5),
579 SpecificPower::new::<watt_per_kilogram>(2.0)
580));
581datapoints.push(FluxDensityLossPair::new(
582 MagneticFluxDensity::new::<tesla>(0.6),
583 SpecificPower::new::<watt_per_kilogram>(2.5)
584));
585datapoints.push(FluxDensityLossPair::new(
586 MagneticFluxDensity::new::<tesla>(0.7),
587 SpecificPower::new::<watt_per_kilogram>(3.2)
588));
589datapoints.push(FluxDensityLossPair::new(
590 MagneticFluxDensity::new::<tesla>(0.8),
591 SpecificPower::new::<watt_per_kilogram>(4.0)
592));
593let loss_charactistic = IronLossCharacteristic::new(frequency, datapoints);
594```
595 */
596#[derive(Debug, Clone)]
597#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
598pub struct IronLossCharacteristic {
599 /// Frequency at which the charactistic has been measured. Should be a
600 /// positive value (a negative frequency makes no sense from a physics point
601 /// of view and at zero frequency the losses are also zero).
602 #[cfg_attr(feature = "serde", serde(deserialize_with = "deserialize_quantity"))]
603 pub frequency: Frequency,
604 /// Collection of amplitude - losses datapoints. The order of these
605 /// datapoints does not matter.
606 pub characteristic: Vec<FluxDensityLossPair>,
607}
608
609impl IronLossCharacteristic {
610 /**
611 Creates a new [`IronLossCharacteristic`] from its fields.
612 */
613 pub fn new(frequency: Frequency, characteristic: Vec<FluxDensityLossPair>) -> Self {
614 return Self {
615 frequency,
616 characteristic,
617 };
618 }
619
620 /**
621 Creates a new [`IronLossCharacteristic`] from its frequency, a slice of
622 flux densities and one of specific losses.
623
624 Each entry of the `flux_densities` vector is paired with the same-index
625 entry of `specific_losses` to form a [`FluxDensityLossPair`]. If one slice
626 is longer than the other, the surplus entries are discarded.
627 */
628 pub fn from_vecs(
629 frequency: Frequency,
630 flux_densities: &[MagneticFluxDensity],
631 specific_losses: &[SpecificPower],
632 ) -> Self {
633 let mut characteristic = Vec::with_capacity(flux_densities.len());
634 for (flux_density, specific_loss) in
635 flux_densities.into_iter().zip(specific_losses.into_iter())
636 {
637 characteristic.push(FluxDensityLossPair::new(
638 flux_density.clone(),
639 specific_loss.clone(),
640 ));
641 }
642
643 return Self::new(frequency, characteristic);
644 }
645}
646
647/**
648A single datapoint of an [`IronLossCharacteristic`].
649
650This struct represents the specific losses in a lamination sheet created by a
651sinusoidal magnetic field with the amplitude
652[`FluxDensityLossPair::flux_density`] at a given frequency. It is meant to be
653a building block of a [`IronLossCharacteristic`], where also the aforementioned
654frequency is specified. See the docstring of [`IronLossCharacteristic`] for
655examples.
656 */
657#[derive(Debug, Clone)]
658#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
659pub struct FluxDensityLossPair {
660 /// Flux density of the datapoint.
661 #[cfg_attr(feature = "serde", serde(deserialize_with = "deserialize_quantity"))]
662 pub flux_density: MagneticFluxDensity,
663 /// Specific losses of the datapoint.
664 #[cfg_attr(feature = "serde", serde(deserialize_with = "deserialize_quantity"))]
665 pub specific_loss: SpecificPower,
666}
667
668impl FluxDensityLossPair {
669 /**
670 Creates a new [`FluxDensityLossPair`] from its fields.
671 */
672 pub fn new(flux_density: MagneticFluxDensity, specific_loss: SpecificPower) -> Self {
673 return Self {
674 flux_density,
675 specific_loss,
676 };
677 }
678}
679
680#[cfg(feature = "serde")]
681mod serde_impl {
682 use super::*;
683 use deserialize_untagged_verbose_error::DeserializeUntaggedVerboseError;
684
685 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
686 pub(super) struct JordanModelAlias {
687 #[cfg_attr(feature = "serde", serde(deserialize_with = "deserialize_quantity"))]
688 hysteresis_coefficient: SpecificPower,
689 #[cfg_attr(feature = "serde", serde(deserialize_with = "deserialize_quantity"))]
690 eddy_current_coefficient: SpecificPower,
691 }
692
693 #[derive(DeserializeUntaggedVerboseError)]
694 pub(super) enum JordanModelDeEnum {
695 JordanModelAlias(JordanModelAlias),
696 IronLossData(IronLossData),
697 }
698
699 impl TryFrom<JordanModelDeEnum> for JordanModel {
700 type Error = FailedCoefficientCalculation;
701
702 fn try_from(value: JordanModelDeEnum) -> Result<Self, Self::Error> {
703 match value {
704 JordanModelDeEnum::JordanModelAlias(alias) => Ok(JordanModel {
705 hysteresis_coefficient: alias.hysteresis_coefficient,
706 eddy_current_coefficient: alias.eddy_current_coefficient,
707 }),
708 JordanModelDeEnum::IronLossData(iron_loss_data) => iron_loss_data.try_into(),
709 }
710 }
711 }
712}
713
714/**
715A struct representing a failed [`JordanModel`] coefficient calculation attempt.
716
717Calculating the coefficients of a [`JordanModel`] may fail due to a bad dataset.
718The calculation uses a least-square minimization algorithm provided by the
719[`argmin`] crate, which returns a [`argmin::core::Error`] when the calculation
720fails. Even if no such error is created, the returned coefficient might still
721be empty - this is represented by `FailedCoefficientCalculation(None)`.
722 */
723#[derive(Debug)]
724pub struct FailedCoefficientCalculation(pub Option<argmin::core::Error>);
725
726impl std::fmt::Display for FailedCoefficientCalculation {
727 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
728 match &self.0 {
729 Some(cause) => {
730 let original_message = cause.to_string();
731 write!(
732 f,
733 "The calculation of the hysteresis loss coefficients failed,
734 likely due to bad input data. Original message: {original_message}."
735 )
736 }
737 None => write!(
738 f,
739 "The calculation of the hysteresis loss coefficients failed,
740 likely due to bad input data."
741 ),
742 }
743 }
744}
745
746impl std::error::Error for FailedCoefficientCalculation {}