# and `µr` being
the relative permeability of the substance the field is passing through. For a
ferromagnetic material, `µr` itself is a function of `B` and therefore also of
`H`.
For real materials, this function cannot be expressed analytically and is
usually approximated by interpolating between measured datapoints. For physical
simulations which often involve iterative solvers, it is important that the
interpolation is "smooth", meaning that its derivatives do not "jump". For that
reason, spline interpolations are often used here.
This module offers the [`FerromagneticPermeability`] struct, which is
essentially a wrapper around two [`AkimaSpline`]s, one for `µr(H)` and one for
`µr(B)`. The struct is meant to constructed from measured datapoints provided
by the containers [`MagnetizationCurve`] and [`PolarizationCurve`]. The splines
are optimized for fast and stable numerical calculations when e.g. using an
iterative solver to determine the magnetization of an electrical motor. In
particular, this means the following:
1) The splines are strictly monotonously decreasing with higher `B` or `H`
values. For very low values of `B` or `H`, this leads to a small error. For
electric machines in particular, the saturation is usually high enough that this
error can be neglected (which is why e.g. commercial FEM software also modifies
the permability curves given by the user accordingly).
2) Datasheets or measurements usually have no datapoints for very high `B` or
`H` values (simply because these extrema don't usually occur in nature).
However, at the start of an iterative solver, extreme values may occur.
To guarantee a stable iteration, the aforementioned strictly monotonously
decreasing behaviour should still be guaranteed and in addition, no nonsensical
values (e.g. negative permeability) should be calculated. For this reasons, the
splines are extrapolated and clamped to guarantee these properties.
Measurement data from manufacturers usually refers to the permeability of a
massive block of material. However, the cores of electric machines are usually
made from laminated sheets. The glue and insulation usually accounts for 2 % to
8 % of the sheet thickness and reduces the permeability of the lamination stack
(since its relative permeability is around 1). The constructor argument
`iron_fill_factor` (see [`MagnetizationCurve::new`] /
[`PolarizationCurve::new`] can be used to model that reduction.
In the image below, the modifications (1) and (2) are clearly visible.
Additionally, it shows the comparison between an iron fill factor of 100 %
(massive material) and a typical lamination ratio of 95 % iron, 5 % glue and
insulation.
![Relative permeability][relative_permeability]
"#]
#![cfg_attr(feature = "doc-images",
cfg_attr(all(),
doc = ::embed_doc_image::embed_image!("relative_permeability", "docs/img/relative_permeability.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."
)]
use std::f64::INFINITY;
use akima_spline::AkimaSpline;
use var_quantity::uom::si::f64::*;
use var_quantity::uom::si::magnetic_field_strength::ampere_per_meter;
use var_quantity::uom::si::magnetic_flux_density::tesla;
use var_quantity::{DynQuantity, PredefUnit, Unit};
use var_quantity::{IsQuantityFunction, QuantityFunction};
#[cfg(feature = "serde")]
use var_quantity::deserialize_vec_of_quantities;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::material::{VACUUM_PERMEABILITY, VACUUM_PERMEABILITY_UNITLESS};
#[derive(Clone, Debug, PartialEq)]
pub enum RelativePermeability {
Constant(f64),
FerromagneticPermeability(FerromagneticPermeability),
Function(QuantityFunction<f64>),
}
#[cfg(feature = "serde")]
impl serde::Serialize for RelativePermeability {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
#[derive(Serialize)]
enum FerromagneticPermeabilityEnum<'a> {
FerromagneticPermeability(&'a FerromagneticPermeability),
}
#[derive(Serialize)]
#[serde(untagged)]
enum RelativePermeabilitySerde<'a> {
Constant(f64),
FerromagneticPermeabilityEnum(FerromagneticPermeabilityEnum<'a>),
Function(&'a QuantityFunction<f64>),
}
let rp = match self {
RelativePermeability::Constant(v) => RelativePermeabilitySerde::Constant(*v),
RelativePermeability::FerromagneticPermeability(fp) => {
RelativePermeabilitySerde::FerromagneticPermeabilityEnum(
FerromagneticPermeabilityEnum::FerromagneticPermeability(fp),
)
}
RelativePermeability::Function(quantity_function) => {
RelativePermeabilitySerde::Function(quantity_function)
}
};
rp.serialize(serializer)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for RelativePermeability {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::Deserialize;
#[derive(Deserialize)]
enum FerromagneticPermeabilityEnum {
FerromagneticPermeability(FerromagneticPermeability),
}
#[derive(deserialize_untagged_verbose_error::DeserializeUntaggedVerboseError)]
enum RelativePermeabilitySerde {
Constant(f64),
FerromagneticPermeabilityEnum(FerromagneticPermeabilityEnum),
Function(QuantityFunction<f64>),
}
let rp_de = RelativePermeabilitySerde::deserialize(deserializer)?;
let rp = match rp_de {
RelativePermeabilitySerde::Constant(v) => RelativePermeability::Constant(v),
RelativePermeabilitySerde::FerromagneticPermeabilityEnum(fp) => match fp {
FerromagneticPermeabilityEnum::FerromagneticPermeability(jordan_model) => {
RelativePermeability::FerromagneticPermeability(jordan_model)
}
},
RelativePermeabilitySerde::Function(quantity_function) => {
RelativePermeability::Function(quantity_function)
}
};
return Ok(rp);
}
}
impl RelativePermeability {
pub fn get(&self, conditions: &[DynQuantity<f64>]) -> f64 {
match self {
Self::Constant(val) => val.clone(),
Self::FerromagneticPermeability(model) => model.call(conditions).try_into().expect("implementation of FerromagneticPermeability makes sure the returned value is always a f64"),
Self::Function(fun) => fun.call(conditions),
}
}
pub fn function(&self) -> Option<&dyn IsQuantityFunction> {
match self {
Self::Function(quantity_function) => return Some(quantity_function.as_ref()),
_ => return None,
}
}
}
impl TryFrom<Box<dyn IsQuantityFunction>> for RelativePermeability {
type Error = var_quantity::UnitsNotEqual;
fn try_from(value: Box<dyn IsQuantityFunction>) -> Result<Self, Self::Error> {
let wrapper = QuantityFunction::new(value)?;
return Ok(Self::Function(wrapper));
}
}
impl From<f64> for RelativePermeability {
fn from(value: f64) -> Self {
return Self::Constant(value);
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(
feature = "serde",
serde(try_from = "serde_impl::FerromagneticPermeabilityDeEnum")
)]
pub struct FerromagneticPermeability {
pub from_field_strength: AkimaSpline,
pub from_flux_density: AkimaSpline,
}
impl FerromagneticPermeability {
pub fn from_magnetization(raw_curve: MagnetizationCurve) -> Result<Self, InvalidInputData> {
let (field_strength, flux_density) = sample_bh_curve(
raw_curve.field_strength.as_slice(),
raw_curve.flux_density.as_slice(),
0.02,
)?;
let mut induction: Vec<f64> = Vec::with_capacity(field_strength.len());
let mut permeability: Vec<f64> = Vec::with_capacity(field_strength.len());
let mut field_strength_spline: Vec<f64> = Vec::with_capacity(field_strength.len());
for (hi, bi) in field_strength
.iter()
.map(|value| value.get::<ampere_per_meter>().clone())
.zip(
flux_density
.iter()
.map(|value| value.get::<tesla>().clone()),
)
{
if hi != 0.0 {
let b_red = bi * raw_curve.iron_fill_factor
+ (1.0 - raw_curve.iron_fill_factor) * hi * VACUUM_PERMEABILITY_UNITLESS;
let mu_r = b_red / (hi * VACUUM_PERMEABILITY_UNITLESS);
field_strength_spline.push(hi);
induction.push(b_red);
permeability.push(mu_r);
}
}
let mut idx_max = None;
let mut min_value = std::f64::NEG_INFINITY;
for (idx, value) in permeability.iter().enumerate() {
if *value > min_value {
min_value = *value;
idx_max = Some(idx);
}
}
let idx_max = idx_max.expect("Guaranteed to have at least one value by the constructor");
let field_strength_right_of_maximum = &field_strength_spline[idx_max..];
let induction_right_of_maximum = &induction[idx_max..];
let permeability_right_of_maximum = &permeability[idx_max..];
let field_strength = field_strength_right_of_maximum.to_vec();
let induction = induction_right_of_maximum.to_vec();
let mut permeability = permeability_right_of_maximum.to_vec();
if permeability.len() > 2 {
for idx in (0..(permeability.len() - 2)).rev() {
if permeability[idx] < permeability[idx + 1] {
let m = (permeability[idx + 1] - permeability[idx + 2])
/ (induction[idx + 1] - induction[idx + 2]);
permeability[idx] =
permeability[idx + 1] + m * (induction[idx + 1] - induction[idx + 2]);
}
}
}
let induction_1 = *induction
.last()
.expect("Guaranteed to have at least one value by the constructor");
let induction_2 = 100.0;
let permeability_1 = *permeability
.last()
.expect("Guaranteed to have at least one value by the constructor");
let permeability_2 = 1.0;
let field_strength_1 = induction_1 / (VACUUM_PERMEABILITY_UNITLESS * permeability_1);
let field_strength_2 = induction_2 / (VACUUM_PERMEABILITY_UNITLESS * permeability_2);
let mr = (permeability_2 - permeability_1) / (field_strength_2 - field_strength_1);
let ml = 0.0;
let extrapl = Some(vec![ml]);
let extrapr = Some(vec![mr]);
let from_field_strength =
AkimaSpline::new(field_strength, permeability.clone(), extrapl, extrapr)
.expect("values are guaranteed to be in ascending order");
let mr = (permeability_2 - permeability_1) / (induction_2 - induction_1);
let ml = 0.0;
let extrapl = Some(vec![ml]);
let extrapr = Some(vec![mr]);
let from_flux_density = AkimaSpline::new(induction, permeability, extrapl, extrapr)?;
return Ok(Self {
from_field_strength,
from_flux_density,
});
}
pub fn from_polarization(raw_curve: PolarizationCurve) -> Result<Self, InvalidInputData> {
return raw_curve.try_into();
}
pub fn get<T: FieldStrengthOrFluxDensity>(&self, value: T) -> f64 {
return value.permeability(&self);
}
}
#[cfg_attr(feature = "serde", typetag::serde)]
impl IsQuantityFunction for FerromagneticPermeability {
fn call(&self, conditions: &[DynQuantity<f64>]) -> DynQuantity<f64> {
for f in conditions {
if f.unit == Unit::from(PredefUnit::MagneticFieldStrength) {
return self
.from_field_strength
.eval_infallible(f.value.abs())
.clamp(1.0, INFINITY)
.into();
} else if f.unit == Unit::from(PredefUnit::MagneticFluxDensity) {
return self
.from_flux_density
.eval_infallible(f.value.abs())
.clamp(1.0, INFINITY)
.into();
}
}
return self.from_flux_density.eval_infallible(0.0).into();
}
fn dyn_eq(&self, other: &dyn IsQuantityFunction) -> bool {
(other as &dyn std::any::Any).downcast_ref::<Self>() == Some(self)
}
}
pub trait FieldStrengthOrFluxDensity: private::Sealed {
fn permeability(self, permeability: &FerromagneticPermeability) -> f64;
}
impl private::Sealed for MagneticFieldStrength {}
impl FieldStrengthOrFluxDensity for MagneticFieldStrength {
fn permeability(self, permeability: &FerromagneticPermeability) -> f64 {
let raw = self.get::<ampere_per_meter>();
return permeability.from_field_strength.eval_infallible(raw);
}
}
impl private::Sealed for MagneticFluxDensity {}
impl FieldStrengthOrFluxDensity for MagneticFluxDensity {
fn permeability(self, permeability: &FerromagneticPermeability) -> f64 {
let raw = self.get::<tesla>();
return permeability.from_flux_density.eval_infallible(raw);
}
}
mod private {
pub trait Sealed {}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MagnetizationCurve {
#[cfg_attr(
feature = "serde",
serde(deserialize_with = "deserialize_vec_of_quantities")
)]
field_strength: Vec<MagneticFieldStrength>,
#[cfg_attr(
feature = "serde",
serde(deserialize_with = "deserialize_vec_of_quantities")
)]
flux_density: Vec<MagneticFluxDensity>,
iron_fill_factor: f64,
}
impl MagnetizationCurve {
pub fn new(
field_strength: Vec<MagneticFieldStrength>,
flux_density: Vec<MagneticFluxDensity>,
iron_fill_factor: f64,
) -> Result<Self, InvalidInputData> {
let data = MagnetizationCurve {
field_strength,
flux_density,
iron_fill_factor,
};
data.check()?;
return Ok(data);
}
fn check(&self) -> Result<(), InvalidInputData> {
if self.iron_fill_factor > 1.0 || self.iron_fill_factor < 0.0 {
return Err(InvalidInputData::IronFillFactor(self.iron_fill_factor));
}
if self.field_strength.len() != self.flux_density.len() {
return Err(InvalidInputData::IneqNumElementsFluxDensity {
field_strength: self.field_strength.len(),
flux_density: self.flux_density.len(),
});
}
return Ok(());
}
}
impl TryFrom<MagnetizationCurve> for FerromagneticPermeability {
type Error = InvalidInputData;
fn try_from(value: MagnetizationCurve) -> Result<Self, Self::Error> {
return FerromagneticPermeability::from_magnetization(value);
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct PolarizationCurve {
#[cfg_attr(
feature = "serde",
serde(deserialize_with = "deserialize_vec_of_quantities")
)]
field_strength: Vec<MagneticFieldStrength>,
#[cfg_attr(
feature = "serde",
serde(deserialize_with = "deserialize_vec_of_quantities")
)]
polarization: Vec<MagneticFluxDensity>,
iron_fill_factor: f64,
}
impl PolarizationCurve {
pub fn new(
field_strength: Vec<MagneticFieldStrength>,
polarization: Vec<MagneticFluxDensity>,
iron_fill_factor: f64,
) -> Result<Self, InvalidInputData> {
let data = PolarizationCurve {
field_strength,
polarization,
iron_fill_factor,
};
data.check()?;
return Ok(data);
}
fn check(&self) -> Result<(), InvalidInputData> {
if self.iron_fill_factor > 1.0 || self.iron_fill_factor < 0.0 {
return Err(InvalidInputData::IronFillFactor(self.iron_fill_factor));
}
if self.field_strength.len() != self.polarization.len() {
return Err(InvalidInputData::IneqNumElementsPolarization {
field_strength: self.field_strength.len(),
polarization: self.polarization.len(),
});
}
return Ok(());
}
}
impl TryFrom<PolarizationCurve> for MagnetizationCurve {
type Error = InvalidInputData;
fn try_from(value: PolarizationCurve) -> Result<Self, InvalidInputData> {
let mut flux_density = value.polarization;
flux_density
.iter_mut()
.zip(value.field_strength.iter())
.for_each(|(b, h)| {
*b = *b + *h * *VACUUM_PERMEABILITY;
});
let data = MagnetizationCurve {
field_strength: value.field_strength,
flux_density,
iron_fill_factor: value.iron_fill_factor,
};
data.check()?;
return Ok(data);
}
}
impl TryFrom<PolarizationCurve> for FerromagneticPermeability {
type Error = InvalidInputData;
fn try_from(value: PolarizationCurve) -> Result<Self, InvalidInputData> {
let magnetization_curve = MagnetizationCurve::try_from(value)?;
return magnetization_curve.try_into();
}
}
#[derive(Debug)]
pub enum InvalidInputData {
IronFillFactor(f64),
IneqNumElementsFluxDensity {
field_strength: usize,
flux_density: usize,
},
IneqNumElementsPolarization {
field_strength: usize,
polarization: usize,
},
AkimaBuildError(akima_spline::BuildError),
}
impl From<akima_spline::BuildError> for InvalidInputData {
fn from(value: akima_spline::BuildError) -> Self {
return Self::AkimaBuildError(value);
}
}
impl std::fmt::Display for InvalidInputData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
InvalidInputData::IronFillFactor(value) => write!(
f,
"iron fill factor must be between 0 and 1 (0 % and 100 %), is {value}."
),
InvalidInputData::IneqNumElementsFluxDensity {
field_strength,
flux_density,
} => write!(
f,
"got {field_strength} values for field strength, but
{flux_density} values for flux density (should be equal)."
),
InvalidInputData::IneqNumElementsPolarization {
field_strength,
polarization,
} => write!(
f,
"got {field_strength} values for field strength, but
{polarization} values for polarization (should be equal)."
),
InvalidInputData::AkimaBuildError(error) => return error.fmt(f),
}
}
}
impl std::error::Error for InvalidInputData {}
fn sample_bh_curve(
field_strength: &[MagneticFieldStrength],
flux_density: &[MagneticFluxDensity],
change_tol: f64,
) -> Result<(Vec<MagneticFieldStrength>, Vec<MagneticFluxDensity>), InvalidInputData> {
let sample_step_width = MagneticFieldStrength::new::<ampere_per_meter>(10.0);
let max_field_strength = field_strength
.iter()
.cloned()
.reduce(|first, second| if first > second { first } else { second })
.expect("must have at least one element");
let extrapl = Some(vec![VACUUM_PERMEABILITY_UNITLESS]);
let extrapr = Some(vec![VACUUM_PERMEABILITY_UNITLESS]);
let bh_curve = AkimaSpline::new(
field_strength
.iter()
.map(|val| val.get::<ampere_per_meter>())
.collect(),
flux_density.iter().map(|val| val.get::<tesla>()).collect(),
extrapl,
extrapr,
)?;
let mut h_sampled: Vec<MagneticFieldStrength> = Vec::with_capacity(1000);
let mut b_sampled: Vec<MagneticFluxDensity> = Vec::with_capacity(1000);
h_sampled.push(MagneticFieldStrength::new::<ampere_per_meter>(0.0));
b_sampled.push(MagneticFluxDensity::new::<tesla>(0.0));
h_sampled.push(sample_step_width);
b_sampled.push(MagneticFluxDensity::new::<tesla>(
bh_curve.eval_infallible(sample_step_width.get::<ampere_per_meter>()),
));
let mut current_field_strength = 2.0 * sample_step_width;
while current_field_strength < max_field_strength {
let mu_prev = b_sampled
.last()
.expect("b_sampled has at least one element")
.clone()
/ h_sampled
.last()
.expect("h_sampled has at least one element")
.clone();
let current_flux_density = MagneticFluxDensity::new::<tesla>(
bh_curve.eval_infallible(current_field_strength.get::<ampere_per_meter>()),
);
let mu_curr = current_flux_density / current_field_strength;
if f64::from((mu_prev - mu_curr).abs() / mu_prev) > change_tol {
h_sampled.push(current_field_strength);
b_sampled.push(current_flux_density);
}
current_field_strength = current_field_strength + sample_step_width;
}
return Ok((h_sampled, b_sampled));
}
#[cfg(feature = "serde")]
mod serde_impl {
use deserialize_untagged_verbose_error::DeserializeUntaggedVerboseError;
use super::*;
#[derive(Deserialize)]
pub(super) struct FerromagneticPermeabilityDeserializeAlias {
from_field_strength: AkimaSpline,
from_flux_density: AkimaSpline,
}
#[derive(DeserializeUntaggedVerboseError)]
pub(super) enum FerromagneticPermeabilityDeEnum {
FerromagneticPermeability(FerromagneticPermeabilityDeserializeAlias),
MagnetizationCurve(MagnetizationCurve),
PolarizationCurve(PolarizationCurve),
}
impl TryFrom<FerromagneticPermeabilityDeEnum> for FerromagneticPermeability {
type Error = InvalidInputData;
fn try_from(value: FerromagneticPermeabilityDeEnum) -> Result<Self, InvalidInputData> {
match value {
FerromagneticPermeabilityDeEnum::FerromagneticPermeability(val) => {
Ok(FerromagneticPermeability {
from_field_strength: val.from_field_strength,
from_flux_density: val.from_flux_density,
})
}
FerromagneticPermeabilityDeEnum::MagnetizationCurve(val) => {
FerromagneticPermeability::try_from(val)
}
FerromagneticPermeabilityDeEnum::PolarizationCurve(val) => {
FerromagneticPermeability::try_from(val)
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx;
#[test]
fn test_sample_bh_curve() {
let field_strength: Vec<MagneticFieldStrength> = vec![
0.0, 11.57, 22.11, 31.71, 40.47, 48.50, 55.29, 64.02, 75.66, 89.24, 107.67, 134.83,
179.45, 276.45, 582.98, 1583.11, 3578.65, 6665.91, 11303.32, 18871.00, 29765.16,
45905.16, 69372.42, 102918.79, 150142.01, 215692.99, 219224.15,
]
.into_iter()
.map(MagneticFieldStrength::new::<ampere_per_meter>)
.collect();
let flux_density: Vec<MagneticFluxDensity> = vec![
0.0, 0.0970, 0.1940, 0.2910, 0.3880, 0.4851, 0.5821, 0.6791, 0.7761, 0.8731, 0.9701,
1.0672, 1.1642, 1.2614, 1.3588, 1.4571, 1.5566, 1.6576, 1.7606, 1.8674, 1.9674, 2.0674,
2.1674, 2.2674, 2.3674, 2.4674, 2.4720,
]
.into_iter()
.map(MagneticFluxDensity::new::<tesla>)
.collect();
let (h, b) =
sample_bh_curve(field_strength.as_slice(), flux_density.as_slice(), 0.02).unwrap();
let len = 300;
assert_eq!(h.len(), len);
assert_eq!(h.len(), len);
approx::assert_abs_diff_eq!(h[0].get::<ampere_per_meter>(), 0.0, epsilon = 0.001);
approx::assert_abs_diff_eq!(h[1].get::<ampere_per_meter>(), 10.0, epsilon = 0.001);
approx::assert_abs_diff_eq!(h[2].get::<ampere_per_meter>(), 20.0, epsilon = 0.001);
approx::assert_abs_diff_eq!(h[50].get::<ampere_per_meter>(), 580.0, epsilon = 0.001);
approx::assert_abs_diff_eq!(h[150].get::<ampere_per_meter>(), 7040.0, epsilon = 0.001);
approx::assert_abs_diff_eq!(h[299].get::<ampere_per_meter>(), 217110.0, epsilon = 0.001);
approx::assert_abs_diff_eq!(b[0].get::<tesla>(), 0.0, epsilon = 0.001);
approx::assert_abs_diff_eq!(b[1].get::<tesla>(), 0.08142, epsilon = 0.001);
approx::assert_abs_diff_eq!(b[2].get::<tesla>(), 0.17399, epsilon = 0.001);
approx::assert_abs_diff_eq!(b[50].get::<tesla>(), 1.35845, epsilon = 0.001);
approx::assert_abs_diff_eq!(b[150].get::<tesla>(), 1.66712, epsilon = 0.001);
approx::assert_abs_diff_eq!(b[299].get::<tesla>(), 2.46926, epsilon = 0.001);
}
}