#![doc = r#"
An implementation of the Jordan model for iron losses in the core lamination.
The Jordan loss model for iron losses offers a simple calculation heuristic for
a sinusoidal flux density change over time. It separates iron losses into static
hysteresis losses and dynamic eddy current losses via the following formula:
`p = kh * f * B² + kec * (f * B)²`,
where `f` is the frequency and `B` is the amplitude of the flux density. The
hysteresis loss factor `kh` and the eddy current loss factor `kec` are derived
by fitting measured loss curves. See \[1\] and \[2\] for more.
This module offers the [`JordanModel`] struct, a simple container for the two
loss coefficients which provides the formula given above via its
[`JordanModel::losses`] method. The struct implements [`IsQuantityFunction`] and
can therefore be used as the
[iron loss model](crate::material::Material::iron_losses) of a
[`Material`](crate::material::Material).
The coefficients can be obtained from measured loss curves by constructing an
[`IronLossData`] instance out of them and then fallibly converting it via
[`TryFrom`] into a [`JordanModel`]. Under the hood, the curves are fitted to the
loss equation using a least-square optimization with the coefficients being the
variables. The [`FailedCoefficientCalculation`] error type is returned in case
the fitting failed for some reason. Lastly, the types
[`IronLossCharacteristic`] and [`FluxDensityLossPair`] are used within the
construction of [`IronLossData`] to guard against bad input data on the type
level.
# Example
The image below shows a comparison between raw loss data and the fitted
[`JordanModel`] from `examples/jordan_model.rs`. While the model can represent
the loss behaviour at lower frequencies very well, it fails at higher
frequencies for this particular set of data points.
![Jordan model][jordan_model]
"#]
#![cfg_attr(feature = "doc-images",
cfg_attr(all(),
doc = ::embed_doc_image::embed_image!("jordan_model", "docs/img/jordan_model.svg"),
))]
#![cfg_attr(
not(feature = "doc-images"),
doc = "**Doc images not enabled**. Compile docs with `cargo doc --features 'doc-images'` and Rust version >= 1.54."
)]
#![doc = r#"
# Literature
> \[1\] Krings, A. and Soulard, J.: Overview and comparison of iron loss models
for electrical machines. EVRE Monaco, March 2010. URL:
<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>
> \[2\] Graham, C. D.: Physical origin of losses in conducting ferromagnetic
materials. Journal of Applied Physics, vol. 53, no. 11, pp. 8276-8280, Nov.1982
"#]
use argmin::{
core::{CostFunction, State},
solver::neldermead::NelderMead,
};
use var_quantity::DynQuantity;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "serde")]
use var_quantity::deserialize_quantity;
use var_quantity::IsQuantityFunction;
use var_quantity::uom::si::{
f64::*, frequency::hertz, magnetic_flux_density::tesla, ratio::ratio,
specific_power::watt_per_kilogram,
};
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(try_from = "serde_impl::JordanModelDeEnum"))]
pub struct JordanModel {
#[cfg_attr(feature = "serde", serde(deserialize_with = "deserialize_quantity"))]
pub hysteresis_coefficient: SpecificPower,
#[cfg_attr(feature = "serde", serde(deserialize_with = "deserialize_quantity"))]
pub eddy_current_coefficient: SpecificPower,
}
impl JordanModel {
pub fn new(
hysteresis_coefficient: SpecificPower,
eddy_current_coefficient: SpecificPower,
) -> Self {
return Self {
hysteresis_coefficient,
eddy_current_coefficient,
};
}
pub fn reference_frequency() -> Frequency {
return Frequency::new::<hertz>(50.0);
}
pub fn reference_flux_density() -> MagneticFluxDensity {
return MagneticFluxDensity::new::<tesla>(1.5);
}
pub fn losses(
&self,
magnetic_flux_density: MagneticFluxDensity,
frequency: Frequency,
) -> SpecificPower {
return losses(
magnetic_flux_density,
frequency,
self.eddy_current_coefficient,
self.hysteresis_coefficient,
);
}
}
#[cfg_attr(feature = "serde", typetag::serde)]
impl IsQuantityFunction for JordanModel {
fn call(&self, conditions: &[DynQuantity<f64>]) -> DynQuantity<f64> {
let mut flux_density = MagneticFluxDensity::new::<tesla>(0.0);
let mut frequency = Frequency::new::<hertz>(0.0);
for factor in conditions {
if let Ok(fd) = MagneticFluxDensity::try_from(*factor) {
flux_density = fd;
} else if let Ok(f) = Frequency::try_from(*factor) {
frequency = f;
}
}
return self.losses(flux_density, frequency).into();
}
fn dyn_eq(&self, other: &dyn IsQuantityFunction) -> bool {
(other as &dyn std::any::Any).downcast_ref::<Self>() == Some(self)
}
}
fn losses(
flux_density: MagneticFluxDensity,
frequency: Frequency,
hysteresis_coefficient: SpecificPower,
eddy_current_coefficient: SpecificPower,
) -> SpecificPower {
let f_norm = JordanModel::reference_frequency();
let b_norm = JordanModel::reference_flux_density();
return hysteresis_coefficient
* (frequency / f_norm)
* (flux_density / b_norm).get::<ratio>().powi(2)
+ eddy_current_coefficient
* (frequency / f_norm).get::<ratio>().powi(2)
* (flux_density / b_norm).get::<ratio>().powi(2);
}
impl Default for JordanModel {
fn default() -> Self {
Self {
hysteresis_coefficient: SpecificPower::new::<watt_per_kilogram>(0.0),
eddy_current_coefficient: SpecificPower::new::<watt_per_kilogram>(0.0),
}
}
}
pub struct FitLossCurve {
frequencies: Vec<Frequency>,
flux_densities: Vec<MagneticFluxDensity>,
specific_losses: Vec<SpecificPower>,
}
impl CostFunction for FitLossCurve {
type Param = Vec<f64>;
type Output = f64;
fn cost(&self, p: &Self::Param) -> Result<Self::Output, argmin::core::Error> {
let mut err = 0.0;
let hysteresis_coefficient = SpecificPower::new::<watt_per_kilogram>(p[0]);
let eddy_current_coefficient = SpecificPower::new::<watt_per_kilogram>(p[1]);
for (fi, (bi, pi)) in self
.frequencies
.iter()
.zip(self.flux_densities.iter().zip(self.specific_losses.iter()))
{
err = err
+ (*pi - losses(*bi, *fi, hysteresis_coefficient, eddy_current_coefficient))
.get::<watt_per_kilogram>()
.powi(2);
}
Ok(err)
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct IronLossData(pub Vec<IronLossCharacteristic>);
impl IronLossData {
pub fn solve_for_coefficients(
&self,
) -> Result<
argmin::core::OptimizationResult<
FitLossCurve,
NelderMead<Vec<f64>, f64>,
argmin::core::IterState<Vec<f64>, (), (), (), (), f64>,
>,
FailedCoefficientCalculation,
> {
let mut num_elems: usize = 0;
for characteristic in self.0.iter() {
num_elems += characteristic.characteristic.len();
}
let mut frequencies_flat: Vec<Frequency> = Vec::with_capacity(num_elems);
let mut flux_density_flat: Vec<MagneticFluxDensity> = Vec::with_capacity(num_elems);
let mut specific_losses_flat: Vec<SpecificPower> = Vec::with_capacity(num_elems);
for characteristic in self.0.iter() {
let frequency = characteristic.frequency;
for flux_density_and_specific_loss in characteristic.characteristic.iter().cloned() {
frequencies_flat.push(frequency);
flux_density_flat.push(flux_density_and_specific_loss.flux_density);
specific_losses_flat.push(flux_density_and_specific_loss.specific_loss);
}
}
let fit = FitLossCurve {
frequencies: frequencies_flat,
flux_densities: flux_density_flat,
specific_losses: specific_losses_flat,
};
let start_values = vec![
vec![3.0f64, 3.0f64],
vec![2.0f64, 1.5f64],
vec![1.0f64, 0.5f64],
];
let solver = NelderMead::new(start_values)
.with_sd_tolerance(0.0001)
.map_err(|error| FailedCoefficientCalculation(Some(error)))?;
return argmin::core::Executor::new(fit, solver)
.configure(|state| state.max_iters(200))
.run()
.map_err(|error| FailedCoefficientCalculation(Some(error)));
}
}
impl TryFrom<IronLossData> for JordanModel {
type Error = FailedCoefficientCalculation;
fn try_from(value: IronLossData) -> Result<Self, Self::Error> {
return (&value).try_into();
}
}
impl TryFrom<&IronLossData> for JordanModel {
type Error = FailedCoefficientCalculation;
fn try_from(value: &IronLossData) -> Result<Self, Self::Error> {
let res = value.solve_for_coefficients()?;
let solution = res
.state
.get_best_param()
.ok_or(FailedCoefficientCalculation(None))?;
let hysteresis_coefficient = SpecificPower::new::<watt_per_kilogram>(solution[0]);
let eddy_current_coefficient = SpecificPower::new::<watt_per_kilogram>(solution[1]);
return Ok(JordanModel {
hysteresis_coefficient,
eddy_current_coefficient,
});
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct IronLossCharacteristic {
#[cfg_attr(feature = "serde", serde(deserialize_with = "deserialize_quantity"))]
pub frequency: Frequency,
pub characteristic: Vec<FluxDensityLossPair>,
}
impl IronLossCharacteristic {
pub fn new(frequency: Frequency, characteristic: Vec<FluxDensityLossPair>) -> Self {
return Self {
frequency,
characteristic,
};
}
pub fn from_vecs(
frequency: Frequency,
flux_densities: &[MagneticFluxDensity],
specific_losses: &[SpecificPower],
) -> Self {
let mut characteristic = Vec::with_capacity(flux_densities.len());
for (flux_density, specific_loss) in
flux_densities.into_iter().zip(specific_losses.into_iter())
{
characteristic.push(FluxDensityLossPair::new(
flux_density.clone(),
specific_loss.clone(),
));
}
return Self::new(frequency, characteristic);
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct FluxDensityLossPair {
#[cfg_attr(feature = "serde", serde(deserialize_with = "deserialize_quantity"))]
pub flux_density: MagneticFluxDensity,
#[cfg_attr(feature = "serde", serde(deserialize_with = "deserialize_quantity"))]
pub specific_loss: SpecificPower,
}
impl FluxDensityLossPair {
pub fn new(flux_density: MagneticFluxDensity, specific_loss: SpecificPower) -> Self {
return Self {
flux_density,
specific_loss,
};
}
}
#[cfg(feature = "serde")]
mod serde_impl {
use super::*;
use deserialize_untagged_verbose_error::DeserializeUntaggedVerboseError;
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub(super) struct JordanModelAlias {
#[cfg_attr(feature = "serde", serde(deserialize_with = "deserialize_quantity"))]
hysteresis_coefficient: SpecificPower,
#[cfg_attr(feature = "serde", serde(deserialize_with = "deserialize_quantity"))]
eddy_current_coefficient: SpecificPower,
}
#[derive(DeserializeUntaggedVerboseError)]
pub(super) enum JordanModelDeEnum {
JordanModelAlias(JordanModelAlias),
IronLossData(IronLossData),
}
impl TryFrom<JordanModelDeEnum> for JordanModel {
type Error = FailedCoefficientCalculation;
fn try_from(value: JordanModelDeEnum) -> Result<Self, Self::Error> {
match value {
JordanModelDeEnum::JordanModelAlias(alias) => Ok(JordanModel {
hysteresis_coefficient: alias.hysteresis_coefficient,
eddy_current_coefficient: alias.eddy_current_coefficient,
}),
JordanModelDeEnum::IronLossData(iron_loss_data) => iron_loss_data.try_into(),
}
}
}
}
#[derive(Debug)]
pub struct FailedCoefficientCalculation(pub Option<argmin::core::Error>);
impl std::fmt::Display for FailedCoefficientCalculation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.0 {
Some(cause) => {
let original_message = cause.to_string();
write!(
f,
"The calculation of the hysteresis loss coefficients failed,
likely due to bad input data. Original message: {original_message}."
)
}
None => write!(
f,
"The calculation of the hysteresis loss coefficients failed,
likely due to bad input data."
),
}
}
}
impl std::error::Error for FailedCoefficientCalculation {}