Skip to main content

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 {}