i didn't necessarily *want* to parameterize it all, but it turned out to be easier to do that than to force all users to workaround the lack of Clone.
859 lines
26 KiB
Rust
859 lines
26 KiB
Rust
use crate::geom::{HasCrossSection, Meters, Region, Torus, WorldRegion};
|
|
use crate::real::{Real as _, ToFloat as _};
|
|
use crate::cross::vec::{Vec3, Vec3u};
|
|
use crate::sim::AbstractSim;
|
|
use serde::{Serialize, Deserialize};
|
|
use std::ops::AddAssign;
|
|
|
|
// TODO: do we really need both Send and Sync?
|
|
pub trait AbstractMeasurement<S>: Send + Sync {
|
|
fn key_value(&self, state: &S) -> Vec<Measurement>;
|
|
}
|
|
|
|
pub fn as_dyn_measurements<S, M: AbstractMeasurement<S>>(meas: &[M]) -> Vec<&dyn AbstractMeasurement<S>> {
|
|
meas.into_iter().map(|m| m as &dyn AbstractMeasurement<S>).collect()
|
|
}
|
|
|
|
|
|
/// combine several measurements
|
|
pub fn eval_multiple<S>(state: &S, meas: &[&dyn AbstractMeasurement<S>]) -> Vec<Measurement> {
|
|
meas.into_iter().flat_map(|m| m.key_value(state).into_iter()).collect()
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
|
pub enum MeasurementValue {
|
|
Field(Vec3<f32>),
|
|
Float(f32),
|
|
Int(u64),
|
|
Dim(Vec3u),
|
|
}
|
|
|
|
impl From<Vec3<f32>> for MeasurementValue {
|
|
fn from(v: Vec3<f32>) -> Self {
|
|
Self::Field(v)
|
|
}
|
|
}
|
|
impl From<f32> for MeasurementValue {
|
|
fn from(v: f32) -> Self {
|
|
Self::Float(v)
|
|
}
|
|
}
|
|
impl From<u64> for MeasurementValue {
|
|
fn from(v: u64) -> Self {
|
|
Self::Int(v)
|
|
}
|
|
}
|
|
impl From<Vec3u> for MeasurementValue {
|
|
fn from(v: Vec3u) -> Self {
|
|
Self::Dim(v)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
pub struct Measurement {
|
|
name: String,
|
|
value: MeasurementValue,
|
|
/// e.g. "A" for Amps
|
|
unit: String,
|
|
}
|
|
|
|
impl Measurement {
|
|
fn new<T: Into<MeasurementValue>>(name: &str, value: T, unit: &str) -> Self {
|
|
Self {
|
|
name: name.to_owned(),
|
|
value: value.into(),
|
|
unit: unit.to_owned(),
|
|
}
|
|
}
|
|
fn new_unitless<T: Into<MeasurementValue>>(name: &str, value: T) -> Self {
|
|
Self::new(name, value, "")
|
|
}
|
|
|
|
pub fn name(&self) -> &str {
|
|
&self.name
|
|
}
|
|
|
|
pub fn pretty_print(&self) -> String {
|
|
use MeasurementValue::*;
|
|
match self.value {
|
|
Field(v) => format!("{}{}", v, self.unit),
|
|
Float(f) => if self.unit != "" {
|
|
SiScale::format_short(f, &self.unit)
|
|
} else {
|
|
f.to_string()
|
|
},
|
|
Int(u) => format!("{}{}", u, self.unit),
|
|
Dim(v) => format!("{}x{}x{}{}", v.x(), v.y(), v.z(), self.unit),
|
|
}
|
|
}
|
|
|
|
/// format the Measurement in a way that could be parseable later.
|
|
/// one major use case for this is in dumping the type to a CSV.
|
|
pub fn machine_readable(&self) -> String {
|
|
use MeasurementValue::*;
|
|
match self.value {
|
|
Field(v) => format!("{},{},{}", v.x(), v.y(), v.z()),
|
|
Float(f) => f.to_string(),
|
|
Int(u) => u.to_string(),
|
|
Dim(v) => format!("{},{},{}", v.x(), v.y(), v.z()),
|
|
}
|
|
}
|
|
|
|
/// retrieve the float value of this measurement -- if it's of float type.
|
|
/// useful for tests
|
|
pub fn get_float(&self) -> Option<f32> {
|
|
match self.value {
|
|
MeasurementValue::Float(f) => Some(f),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<S> AbstractMeasurement<S> for Measurement {
|
|
fn key_value(&self, _state: &S) -> Vec<Measurement> {
|
|
vec![self.clone()]
|
|
}
|
|
}
|
|
|
|
enum SiScale {
|
|
Pico,
|
|
Nano,
|
|
Micro,
|
|
Milli,
|
|
Unit,
|
|
Kilo,
|
|
Mega,
|
|
Giga,
|
|
Terra,
|
|
}
|
|
|
|
impl SiScale {
|
|
fn for_value(v: f32) -> Self {
|
|
use SiScale::*;
|
|
match v.abs() {
|
|
v if v < 1e-12 => Unit,
|
|
v if v < 1e-9 => Pico,
|
|
v if v < 1e-6 => Nano,
|
|
v if v < 1e-3 => Micro,
|
|
v if v < 1e0 => Milli,
|
|
v if v < 1e3 => Unit,
|
|
v if v < 1e6 => Kilo,
|
|
v if v < 1e9 => Mega,
|
|
v if v < 1e12 => Giga,
|
|
v if v < 1e15 => Terra,
|
|
_ => Unit
|
|
}
|
|
}
|
|
|
|
/// return the numerical scale of this prefix.
|
|
/// e.g. `scale(&Pico) -> 1e-12
|
|
fn scale(&self) -> f32 {
|
|
use SiScale::*;
|
|
match *self {
|
|
Pico => 1e-12,
|
|
Nano => 1e-9,
|
|
Micro => 1e-6,
|
|
Milli => 1e-3,
|
|
Unit => 1.0,
|
|
Kilo => 1e3,
|
|
Mega => 1e6,
|
|
Giga => 1e9,
|
|
Terra => 1e12,
|
|
}
|
|
}
|
|
|
|
/// return the short string for this scale.
|
|
/// e.g. `shortcode(Pico) -> "p"`
|
|
fn shortcode(&self) -> &'static str {
|
|
use SiScale::*;
|
|
match *self {
|
|
Pico => "p",
|
|
Nano => "n",
|
|
Micro => "u",
|
|
Milli => "m",
|
|
Unit => "",
|
|
Kilo => "k",
|
|
Mega => "M",
|
|
Giga => "G",
|
|
Terra => "T",
|
|
}
|
|
}
|
|
|
|
/// format `v`, with the provided unit.
|
|
/// e.g. `format_short(1234, "A") -> "1.23 kA"
|
|
fn format_short(v: f32, unit: &str) -> String {
|
|
let si = SiScale::for_value(v);
|
|
let scaled = v / si.scale();
|
|
format!("{:.2} {}{}", scaled, si.shortcode(), unit)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Serialize, Deserialize)]
|
|
pub struct Time;
|
|
|
|
impl<S: AbstractSim> AbstractMeasurement<S> for Time {
|
|
fn key_value(&self, state: &S) -> Vec<Measurement> {
|
|
vec![
|
|
Measurement::new_unitless("step", state.step_no()),
|
|
Measurement::new("time", state.time(), "s"),
|
|
]
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Serialize, Deserialize)]
|
|
pub struct Meta;
|
|
|
|
impl<S: AbstractSim> AbstractMeasurement<S> for Meta {
|
|
fn key_value(&self, state: &S) -> Vec<Measurement> {
|
|
vec![
|
|
Measurement::new_unitless("dim", state.size().0),
|
|
Measurement::new("feature_size", state.feature_size(), "m"),
|
|
]
|
|
}
|
|
}
|
|
|
|
pub struct Volume {
|
|
name: String,
|
|
region: Box<dyn Region>,
|
|
}
|
|
|
|
impl Volume {
|
|
pub fn new<R: Region + 'static>(name: &str, r: R) -> Self {
|
|
Self {
|
|
name: name.into(),
|
|
region: Box::new(r)
|
|
}
|
|
}
|
|
/// Returns the volume of the region, in units of um^3
|
|
fn data<S: AbstractSim>(&self, state: &S) -> f32 {
|
|
let feat_um = state.feature_size() as f64 * 1e6;
|
|
|
|
(state.volume_of_region(&*self.region) as f64 * feat_um * feat_um * feat_um) as f32
|
|
}
|
|
}
|
|
|
|
impl<S: AbstractSim> AbstractMeasurement<S> for Volume {
|
|
fn key_value(&self, state: &S) -> Vec<Measurement> {
|
|
vec![
|
|
Measurement::new(&format!("Vol({})", self.name), self.data(state), "um^3"),
|
|
]
|
|
}
|
|
}
|
|
|
|
pub struct Current {
|
|
name: String,
|
|
region: Box<dyn Region>,
|
|
}
|
|
|
|
impl Current {
|
|
pub fn new<R: Region + 'static>(name: &str, r: R) -> Self {
|
|
Self {
|
|
name: name.into(),
|
|
region: Box::new(r)
|
|
}
|
|
}
|
|
fn data<S: AbstractSim>(&self, state: &S) -> (f32, Vec3<f32>) {
|
|
let FieldSample(volume, current_mag, current_vec) = state.map_sum_over_enumerated(&*self.region, |coord: Meters, _cell| {
|
|
let current = state.current(coord);
|
|
FieldSample(1, current.mag().cast(), current.cast())
|
|
});
|
|
let mean_current_mag = current_mag.to_f32() / (f32::from_primitive(volume));
|
|
let mean_current_vec = current_vec.cast::<f32>() / (f32::from_primitive(volume));
|
|
(mean_current_mag, mean_current_vec.cast())
|
|
}
|
|
}
|
|
|
|
// TODO: clean up these FieldSample types
|
|
#[derive(Default)]
|
|
struct TupleSum<T>(T);
|
|
|
|
impl<T0: Default + AddAssign, T1: Default + AddAssign> std::iter::Sum for TupleSum<(T0, T1)> {
|
|
fn sum<I>(iter: I) -> Self
|
|
where I: Iterator<Item = Self>
|
|
{
|
|
let mut s = Self::default();
|
|
for TupleSum((a0, a1)) in iter {
|
|
s.0.0 += a0;
|
|
s.0.1 += a1;
|
|
}
|
|
s
|
|
}
|
|
}
|
|
|
|
|
|
#[derive(Default)]
|
|
struct FieldSample(u32, f64, Vec3<f64>);
|
|
|
|
impl std::iter::Sum for FieldSample {
|
|
fn sum<I>(iter: I) -> Self
|
|
where I: Iterator<Item = Self>
|
|
{
|
|
let mut s = FieldSample::default();
|
|
for FieldSample(a, b, c) in iter {
|
|
s = FieldSample(s.0 + a, s.1 + b, s.2 + c);
|
|
}
|
|
s
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct FieldSamples<T>(T);
|
|
|
|
impl std::iter::Sum for FieldSamples<[FieldSample; 2]> {
|
|
fn sum<I>(iter: I) -> Self
|
|
where I: Iterator<Item = Self>
|
|
{
|
|
let mut s = Self::default();
|
|
for p in iter {
|
|
s.0[0] = FieldSample(s.0[0].0 + p.0[0].0, s.0[0].1 + p.0[0].1, s.0[0].2 + p.0[0].2);
|
|
s.0[1] = FieldSample(s.0[1].0 + p.0[1].0, s.0[1].1 + p.0[1].1, s.0[1].2 + p.0[1].2);
|
|
}
|
|
s
|
|
}
|
|
}
|
|
|
|
impl std::iter::Sum for FieldSamples<[FieldSample; 3]> {
|
|
fn sum<I>(iter: I) -> Self
|
|
where I: Iterator<Item = Self>
|
|
{
|
|
let mut s = Self::default();
|
|
for p in iter {
|
|
s.0[0] = FieldSample(s.0[0].0 + p.0[0].0, s.0[0].1 + p.0[0].1, s.0[0].2 + p.0[0].2);
|
|
s.0[1] = FieldSample(s.0[1].0 + p.0[1].0, s.0[1].1 + p.0[1].1, s.0[1].2 + p.0[1].2);
|
|
s.0[2] = FieldSample(s.0[2].0 + p.0[2].0, s.0[2].1 + p.0[2].1, s.0[2].2 + p.0[2].2);
|
|
}
|
|
s
|
|
}
|
|
}
|
|
|
|
impl<S: AbstractSim> AbstractMeasurement<S> for Current {
|
|
fn key_value(&self, state: &S) -> Vec<Measurement> {
|
|
let (mean_current_mag, mean_current_vec) = self.data(state);
|
|
vec![
|
|
Measurement::new(
|
|
&format!("Imag/cell({})", self.name),
|
|
mean_current_mag,
|
|
"A",
|
|
),
|
|
Measurement::new(
|
|
&format!("I/cell({})", self.name),
|
|
mean_current_vec,
|
|
"A",
|
|
),
|
|
]
|
|
}
|
|
}
|
|
|
|
/// Measures the current directed around a closed loop
|
|
#[derive(Clone, Serialize, Deserialize)]
|
|
pub struct CurrentLoop<R> {
|
|
name: String,
|
|
region: R,
|
|
}
|
|
|
|
impl<R> CurrentLoop<R> {
|
|
pub fn new(name: &str, r: R) -> Self {
|
|
Self {
|
|
name: name.into(),
|
|
region: r,
|
|
}
|
|
}
|
|
}
|
|
impl<R: Region + HasCrossSection> CurrentLoop<R> {
|
|
fn data<S: AbstractSim>(&self, state: &S) -> f32 {
|
|
// - current exists as a property of a 2d surface.
|
|
// - the user has provided us a 3d volume which behaves as though it's an extruded surface:
|
|
// for any point in the volume we can query the normal vector of the cross section
|
|
// containing that point.
|
|
// - we choose that measuring the "current" on such a volume means to measure the average
|
|
// current through all its cross sections (for most boring materials, each
|
|
// cross section has nearly identical current).
|
|
// - therefore, enumerate the entire volume and compute the "net" current (the sum over
|
|
// each cell of whatever current in that cell is along the cross-section normal).
|
|
// then divide by the number of complete cross sections we measured, to average.
|
|
let feature_area = state.feature_size() * state.feature_size();
|
|
let TupleSum((net_current, cross_sections)) = state.map_sum_over_enumerated(&self.region, move |coord: Meters, _cell| {
|
|
// `normal` represents both the size of the cross section (m^2) this cell belongs to,
|
|
// and the normal direction of the cross section.
|
|
let normal = self.region.cross_section_normal(coord); // [m^2]
|
|
// calculate the amount of normal current through this specific cell
|
|
let current_density = state.current_density(coord); // [A/m^2]
|
|
let cross_sectional_current = feature_area * current_density.dot(normal.norm()); // [A]
|
|
// keep track of how many cross sections we enumerate, since each additional cross
|
|
// sections represents a double-count of the current.
|
|
let num_cross_sections_filled = feature_area / normal.mag();
|
|
TupleSum((cross_sectional_current, num_cross_sections_filled))
|
|
});
|
|
let mean_cross_sectional_current = net_current.cast::<f32>() / cross_sections;
|
|
mean_cross_sectional_current
|
|
}
|
|
}
|
|
|
|
impl<R: Region + HasCrossSection, S: AbstractSim> AbstractMeasurement<S> for CurrentLoop<R> {
|
|
fn key_value(&self, state: &S) -> Vec<Measurement> {
|
|
let cross_sectional_current = self.data(state);
|
|
vec![
|
|
Measurement::new(
|
|
&format!("I({})", self.name),
|
|
cross_sectional_current,
|
|
"A"
|
|
),
|
|
]
|
|
}
|
|
}
|
|
|
|
|
|
/// Measures the M, B field directed around a closed loop
|
|
#[derive(Clone, Serialize, Deserialize)]
|
|
pub struct MagneticLoop {
|
|
name: String,
|
|
region: Torus
|
|
}
|
|
|
|
impl MagneticLoop {
|
|
pub fn new(name: &str, r: Torus) -> Self {
|
|
Self {
|
|
name: name.into(),
|
|
region: r,
|
|
}
|
|
}
|
|
fn data<S: AbstractSim>(&self, state: &S) -> (f32, f32, f32) {
|
|
let FieldSamples([
|
|
FieldSample(volume, directed_m, _m_vec),
|
|
FieldSample(_, directed_b, _b_vec),
|
|
FieldSample(_, directed_h, _h_vec),
|
|
]) = state.map_sum_over_enumerated(&self.region, |coord: Meters, cell|
|
|
{
|
|
let normal = self.region.axis();
|
|
let to_coord = *coord - *self.region.center();
|
|
let tangent = normal.cross(to_coord).norm();
|
|
|
|
let m = cell.m();
|
|
let directed_m = m.dot(tangent.cast());
|
|
let b = cell.b();
|
|
let directed_b = b.dot(tangent.cast());
|
|
let h = cell.h();
|
|
let directed_h = h.dot(tangent.cast());
|
|
FieldSamples([
|
|
FieldSample(1, directed_m.cast(), m.cast()),
|
|
FieldSample(1, directed_b.cast(), b.cast()),
|
|
FieldSample(1, directed_h.cast(), h.cast()),
|
|
])
|
|
});
|
|
// let cross_section = self.region.cross_section() / (state.feature_size() * state.feature_size());
|
|
let mean_directed_m = directed_m.cast::<f32>() / f32::from_primitive(volume);
|
|
// let cross_sectional_m = mean_directed_m * cross_section;
|
|
let mean_directed_b = directed_b.cast::<f32>() / f32::from_primitive(volume);
|
|
// let cross_sectional_b = mean_directed_b * cross_section;
|
|
let mean_directed_h = directed_h.cast::<f32>() / f32::from_primitive(volume);
|
|
// let cross_sectional_h = mean_directed_h * cross_section;
|
|
// format!(
|
|
// "M({}): {:.2e}; B({}): {:.2e}; H({}): {:.2e}",
|
|
// self.name, cross_sectional_m,
|
|
// self.name, cross_sectional_b,
|
|
// self.name, cross_sectional_h,
|
|
// )
|
|
(mean_directed_m, mean_directed_b, mean_directed_h)
|
|
}
|
|
}
|
|
|
|
impl<S: AbstractSim> AbstractMeasurement<S> for MagneticLoop {
|
|
fn key_value(&self, state: &S) -> Vec<Measurement> {
|
|
let (mean_directed_m, mean_directed_b, mean_directed_h) = self.data(state);
|
|
vec![
|
|
Measurement::new_unitless(&format!("M({})", self.name), mean_directed_m),
|
|
Measurement::new_unitless(&format!("B({})", self.name), mean_directed_b),
|
|
Measurement::new_unitless(&format!("H({})", self.name), mean_directed_h),
|
|
]
|
|
}
|
|
}
|
|
|
|
/// mean M over a region
|
|
pub struct MagneticFlux {
|
|
name: String,
|
|
region: Box<dyn Region>,
|
|
}
|
|
|
|
impl MagneticFlux {
|
|
pub fn new<R: Region + 'static>(name: &str, r: R) -> Self {
|
|
Self {
|
|
name: name.into(),
|
|
region: Box::new(r)
|
|
}
|
|
}
|
|
fn data<S: AbstractSim>(&self, state: &S) -> Vec3<f32> {
|
|
let FieldSample(volume, _directed_mag, mag_vec) = state.map_sum_over(&*self.region, |cell| {
|
|
let b = cell.b();
|
|
let mag = b.mag();
|
|
FieldSample(1, mag.cast(), b.cast())
|
|
});
|
|
let mean_mag = mag_vec.cast() / f32::from_primitive(volume);
|
|
mean_mag
|
|
}
|
|
}
|
|
|
|
impl<S: AbstractSim> AbstractMeasurement<S> for MagneticFlux {
|
|
fn key_value(&self, state: &S) -> Vec<Measurement> {
|
|
let mean_mag = self.data(state);
|
|
vec![
|
|
Measurement::new_unitless(
|
|
&format!("Bavg({})", self.name),
|
|
mean_mag,
|
|
)
|
|
]
|
|
}
|
|
}
|
|
|
|
/// mean B over a region
|
|
pub struct Magnetization {
|
|
name: String,
|
|
region: Box<dyn Region>,
|
|
}
|
|
|
|
impl Magnetization {
|
|
pub fn new<R: Region + 'static>(name: &str, r: R) -> Self {
|
|
Self {
|
|
name: name.into(),
|
|
region: Box::new(r)
|
|
}
|
|
}
|
|
fn data<S: AbstractSim>(&self, state: &S) -> Vec3<f32> {
|
|
let FieldSample(volume, _directed_mag, mag_vec) = state.map_sum_over(&*self.region, |cell| {
|
|
let m = cell.m();
|
|
let mag = m.mag();
|
|
FieldSample(1, mag.cast(), m.cast())
|
|
});
|
|
let mean_mag = mag_vec.cast() / f32::from_primitive(volume);
|
|
mean_mag
|
|
}
|
|
}
|
|
|
|
impl<S: AbstractSim> AbstractMeasurement<S> for Magnetization {
|
|
fn key_value(&self, state: &S) -> Vec<Measurement> {
|
|
let mean_mag = self.data(state);
|
|
vec![
|
|
Measurement::new_unitless(
|
|
&format!("Mavg({})", self.name), mean_mag
|
|
),
|
|
]
|
|
}
|
|
}
|
|
|
|
fn loc(v: Meters) -> String {
|
|
format!("{:.0} um", *v * f32::from_primitive(1_000_000))
|
|
}
|
|
|
|
/// M
|
|
#[derive(Clone, Serialize, Deserialize)]
|
|
pub struct MagnetizationAt(pub Meters);
|
|
|
|
impl<S: AbstractSim> AbstractMeasurement<S> for MagnetizationAt {
|
|
fn key_value(&self, state: &S) -> Vec<Measurement> {
|
|
let m = state.sample(self.0).m();
|
|
vec![
|
|
Measurement::new_unitless(&format!("M{}", loc(self.0)), m.cast())
|
|
]
|
|
}
|
|
}
|
|
|
|
/// B
|
|
#[derive(Clone, Serialize, Deserialize)]
|
|
pub struct MagneticFluxAt(pub Meters);
|
|
|
|
impl<S: AbstractSim> AbstractMeasurement<S> for MagneticFluxAt {
|
|
fn key_value(&self, state: &S) -> Vec<Measurement> {
|
|
let b = state.sample(self.0).b();
|
|
vec![
|
|
Measurement::new_unitless(
|
|
&format!("B{}", loc(self.0)), b.cast()
|
|
)
|
|
]
|
|
}
|
|
}
|
|
|
|
/// H
|
|
#[derive(Clone, Serialize, Deserialize)]
|
|
pub struct MagneticStrengthAt(pub Meters);
|
|
|
|
impl<S: AbstractSim> AbstractMeasurement<S> for MagneticStrengthAt {
|
|
fn key_value(&self, state: &S) -> Vec<Measurement> {
|
|
let h = state.sample(self.0).h();
|
|
vec![
|
|
Measurement::new_unitless(
|
|
&format!("H{}", loc(self.0)), h.cast()
|
|
)
|
|
]
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Serialize, Deserialize)]
|
|
pub struct ElectricField(pub Meters);
|
|
|
|
impl<S: AbstractSim> AbstractMeasurement<S> for ElectricField {
|
|
fn key_value(&self, state: &S) -> Vec<Measurement> {
|
|
let e = state.sample(self.0).e();
|
|
vec![
|
|
Measurement::new_unitless(
|
|
&format!("E{}", loc(self.0)), e.cast()
|
|
)
|
|
]
|
|
}
|
|
}
|
|
|
|
pub struct Energy {
|
|
name: String,
|
|
region: Box<dyn Region>,
|
|
}
|
|
|
|
impl Energy {
|
|
pub fn world() -> Self {
|
|
Self::new("World", WorldRegion)
|
|
}
|
|
pub fn new<R: Region + 'static>(name: &str, region: R) -> Self {
|
|
Self {
|
|
name: name.into(),
|
|
region: Box::new(region),
|
|
}
|
|
}
|
|
fn data<S: AbstractSim>(&self, state: &S) -> f32 {
|
|
// Potential energy stored in a E/M field:
|
|
// https://en.wikipedia.org/wiki/Magnetic_energy
|
|
// https://en.wikipedia.org/wiki/Electric_potential_energy#Energy_stored_in_an_electrostatic_field_distribution
|
|
// TODO: consider the M field? https://en.wikipedia.org/wiki/Potential_energy#Magnetic_potential_energy
|
|
// U(B) = 1/2 \int H . B dV
|
|
// U(E) = 1/2 \int E . D dV
|
|
#[allow(non_snake_case)]
|
|
let dV = state.feature_volume();
|
|
let e = f64::from_primitive(0.5 * dV) * state.map_sum_over(&*self.region, |cell| {
|
|
// E . D = E . (E + P) = E.E since we don't model polarization fields
|
|
cell.h().dot(cell.b()).to_f64() + cell.e().mag_sq().to_f64()
|
|
});
|
|
e.cast()
|
|
}
|
|
}
|
|
|
|
impl<S: AbstractSim> AbstractMeasurement<S> for Energy {
|
|
fn key_value(&self, state: &S) -> Vec<Measurement> {
|
|
let e = self.data(state);
|
|
vec![
|
|
Measurement::new(
|
|
&format!("U({})", self.name), e, "J"
|
|
)
|
|
]
|
|
}
|
|
}
|
|
|
|
pub struct Power {
|
|
name: String,
|
|
region: Box<dyn Region>
|
|
}
|
|
|
|
impl Power {
|
|
pub fn world() -> Self {
|
|
Self::new("World", WorldRegion)
|
|
}
|
|
pub fn new<R: Region + 'static>(name: &str, region: R) -> Self {
|
|
Self {
|
|
name: name.into(),
|
|
region: Box::new(region),
|
|
}
|
|
}
|
|
fn data<S: AbstractSim>(&self, state: &S) -> f32 {
|
|
// Power is P = IV = A*J*V = L^2*J.(LE) = L^3 J.E
|
|
// where L is feature size.
|
|
#[allow(non_snake_case)]
|
|
let dV = state.feature_volume();
|
|
let power = f64::from_primitive(dV) * state.map_sum_over(&*self.region, |cell| {
|
|
cell.current_density().dot(cell.e()).to_f64()
|
|
});
|
|
power.cast()
|
|
}
|
|
}
|
|
|
|
impl<S: AbstractSim> AbstractMeasurement<S> for Power {
|
|
fn key_value(&self, state: &S) -> Vec<Measurement> {
|
|
let power = self.data(state);
|
|
vec![
|
|
Measurement::new(
|
|
&format!("P({})", self.name), power, "W"
|
|
)
|
|
]
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub mod test {
|
|
use super::*;
|
|
use crate::cross::mat::AnisomorphicConductor;
|
|
use crate::cross::step::SimMeta;
|
|
use crate::geom::Index;
|
|
use crate::sim::{Fields, GenericSim, StaticSim};
|
|
use crate::stim::AbstractStimulus;
|
|
|
|
struct MockSim {
|
|
e_field: Vec3<f32>,
|
|
dim: Vec3u,
|
|
feature_size: f32,
|
|
mat: AnisomorphicConductor<f32>,
|
|
}
|
|
impl AbstractSim for MockSim {
|
|
type Real = f32;
|
|
type Material = AnisomorphicConductor<f32>;
|
|
|
|
fn meta(&self) -> SimMeta<f32> {
|
|
SimMeta::new(self.dim, self.feature_size, 1e-9)
|
|
}
|
|
fn step_no(&self) -> u64 {
|
|
unimplemented!()
|
|
}
|
|
fn fields_at_index(&self, _pos: Index) -> Fields<Self::Real> {
|
|
Fields::new(self.e_field, Vec3::zero(), Vec3::zero())
|
|
}
|
|
fn get_material_index(&self, _at: Index) -> &Self::Material {
|
|
&self.mat
|
|
}
|
|
fn put_material_index(&mut self, _at: Index, _m: Self::Material) {
|
|
unimplemented!()
|
|
}
|
|
fn step_multiple<S: AbstractStimulus>(&mut self, _num_steps: u32, _s: &S) {
|
|
unimplemented!()
|
|
}
|
|
fn to_static(&self) -> StaticSim {
|
|
unimplemented!()
|
|
}
|
|
fn to_generic(&self) -> GenericSim<Self::Real> {
|
|
unimplemented!()
|
|
}
|
|
}
|
|
|
|
struct MockRegion {
|
|
normal: Vec3<f32>,
|
|
}
|
|
impl HasCrossSection for MockRegion {
|
|
fn cross_section_normal(&self, _p: Meters) -> Vec3<f32> {
|
|
self.normal
|
|
}
|
|
}
|
|
impl Region for MockRegion {
|
|
fn contains(&self, _p: Meters) -> bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn current_loop_trivial() {
|
|
let sim = MockSim {
|
|
e_field: Vec3::new(1.0, 0.0, 0.0),
|
|
dim: Vec3u::new(1, 1, 1),
|
|
feature_size: 1.0,
|
|
mat: AnisomorphicConductor::new(Vec3::new(1.0, 1.0, 1.0)),
|
|
};
|
|
let region = MockRegion {
|
|
normal: Vec3::new(1.0, 0.0, 0.0),
|
|
};
|
|
|
|
let kv = CurrentLoop::new("test", region).key_value(&sim);
|
|
assert_eq!(kv.len(), 1);
|
|
// measured area is 1 m^2
|
|
// region cross-section is 1 m^2
|
|
// conductivity is 1 S/m
|
|
assert_eq!(kv[0].get_float().unwrap(), 1.0);
|
|
}
|
|
#[test]
|
|
fn current_loop_multi_cell() {
|
|
let sim = MockSim {
|
|
e_field: Vec3::new(1.0, 0.0, 0.0),
|
|
dim: Vec3u::new(4, 4, 4),
|
|
feature_size: 0.25,
|
|
mat: AnisomorphicConductor::new(Vec3::new(1.0, 1.0, 1.0)),
|
|
};
|
|
let region = MockRegion {
|
|
normal: Vec3::new(1.0, 0.0, 0.0),
|
|
};
|
|
|
|
let kv = CurrentLoop::new("test", region).key_value(&sim);
|
|
assert_eq!(kv.len(), 1);
|
|
// measured area is 1 m^2
|
|
// region cross-section is 1 m^2
|
|
// conductivity is 1 S/m
|
|
assert_eq!(kv[0].get_float().unwrap(), 1.0);
|
|
}
|
|
#[test]
|
|
fn current_loop_off_conductor() {
|
|
let sim = MockSim {
|
|
e_field: Vec3::new(1.0, 1.0, 1.0),
|
|
dim: Vec3u::new(4, 4, 4),
|
|
feature_size: 0.25,
|
|
mat: AnisomorphicConductor::new(Vec3::new(0.0, 1.0, 1.0)),
|
|
};
|
|
let region = MockRegion {
|
|
normal: Vec3::new(1.0, 0.0, 0.0),
|
|
};
|
|
|
|
let kv = CurrentLoop::new("test", region).key_value(&sim);
|
|
assert_eq!(kv.len(), 1);
|
|
// material is not conductive in the direction being queried
|
|
assert_eq!(kv[0].get_float().unwrap(), 0.0);
|
|
}
|
|
#[test]
|
|
fn current_loop_e_field() {
|
|
let sim = MockSim {
|
|
e_field: Vec3::new(4.0, 2.0, 1.0),
|
|
dim: Vec3u::new(4, 4, 4),
|
|
feature_size: 0.25,
|
|
mat: AnisomorphicConductor::new(Vec3::new(1.0, 1.0, 1.0)),
|
|
};
|
|
let region = MockRegion {
|
|
normal: Vec3::new(1.0, 0.0, 0.0),
|
|
};
|
|
|
|
let kv = CurrentLoop::new("test", region).key_value(&sim);
|
|
assert_eq!(kv.len(), 1);
|
|
// measured area is 1 m^2
|
|
// region cross-section is 1 m^2
|
|
// conductivity is 1 S/m
|
|
// e field is 4 V/m
|
|
assert_eq!(kv[0].get_float().unwrap(), 4.0);
|
|
}
|
|
#[test]
|
|
fn current_loop_conductivity() {
|
|
let sim = MockSim {
|
|
e_field: Vec3::new(4.0, 2.0, 1.0),
|
|
dim: Vec3u::new(4, 4, 4),
|
|
feature_size: 0.25,
|
|
mat: AnisomorphicConductor::new(Vec3::new(3.0, 1.0, 1.0)),
|
|
};
|
|
let region = MockRegion {
|
|
normal: Vec3::new(1.0, 0.0, 0.0),
|
|
};
|
|
|
|
let kv = CurrentLoop::new("test", region).key_value(&sim);
|
|
assert_eq!(kv.len(), 1);
|
|
// measured area is 1 m^2
|
|
// region cross-section is 1 m^2
|
|
// conductivity is 3 S/m
|
|
// e field is 4 V/m
|
|
assert_eq!(kv[0].get_float().unwrap(), 3.0*4.0);
|
|
}
|
|
#[test]
|
|
fn current_loop_cross_section() {
|
|
let sim = MockSim {
|
|
e_field: Vec3::new(4.0, 2.0, 1.0),
|
|
dim: Vec3u::new(4, 4, 4),
|
|
feature_size: 0.5,
|
|
mat: AnisomorphicConductor::new(Vec3::new(3.0, 1.0, 1.0)),
|
|
};
|
|
let region = MockRegion {
|
|
normal: Vec3::new(16.0, 0.0, 0.0),
|
|
};
|
|
|
|
let kv = CurrentLoop::new("test", region).key_value(&sim);
|
|
assert_eq!(kv.len(), 1);
|
|
// measured area is 2 m^2
|
|
// region cross-section is 16 m^2
|
|
// conductivity is 3 S/m
|
|
// e field is 4 V/m
|
|
assert_eq!(kv[0].get_float().unwrap(), 3.0*4.0*16.0);
|
|
}
|
|
}
|