convert HasTangent -> HasCrossSection

i believe the current loop algorithm (which i'm just preserving here) is
actually not correct. i'll work through it more.
This commit is contained in:
2022-08-01 05:17:35 -07:00
parent cc876d72d6
commit 527814e38a
5 changed files with 53 additions and 25 deletions

View File

@@ -9,7 +9,7 @@ pub use region::{
Cube,
CylinderZ,
Dilate,
HasTangent,
HasCrossSection,
InvertedRegion,
Memoize,
Region,

View File

@@ -21,8 +21,8 @@ dyn_clone::clone_trait_object!(Region);
/// some (volume) which has a tangent vector everywhere inside/on it.
/// for example, a cylinder has tangents everywhere except its axis.
/// the return vector should be normalized, or zero.
pub trait HasTangent {
fn tangent(&self, p: Meters) -> Vec3<f32>;
pub trait HasCrossSection {
fn cross_section_normal(&self, p: Meters) -> Vec3<f32>;
}
pub fn and<T1: Region + 'static, T2: Region + 'static>(r1: T1, r2: T2) -> Intersection {

View File

@@ -6,7 +6,7 @@ use serde::{Serialize, Deserialize};
use std::fmt::{self, Display};
use std::ops::Range;
use super::{HasTangent, Region};
use super::{HasCrossSection, Region};
#[derive(Copy, Clone, Serialize, Deserialize)]
pub struct CylinderZ {
@@ -104,12 +104,12 @@ impl Region for Torus {
}
}
impl HasTangent for Torus {
fn tangent(&self, coord: Meters) -> Vec3<f32> {
let normal = self.axis();
impl HasCrossSection for Torus {
fn cross_section_normal(&self, coord: Meters) -> Vec3<f32> {
let axis = self.axis();
let to_coord = *coord - *self.center();
// this creates a tangent which always points "counter-clockwise" along the shape
normal.cross(to_coord).norm()
// this creates a normal which always points "counter-clockwise" along the shape
axis.cross(to_coord).norm()
}
}

View File

@@ -1,9 +1,10 @@
use crate::geom::{HasTangent as _, Meters, Region, Torus, WorldRegion};
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};
// TODO: do we really need both Send and Sync?
pub trait AbstractMeasurement<S>: Send + Sync {
fn key_value(&self, state: &S) -> Vec<Measurement>;
}
@@ -307,33 +308,53 @@ impl<S: AbstractSim> AbstractMeasurement<S> for Current {
/// Measures the current directed around a closed loop
#[derive(Clone, Serialize, Deserialize)]
pub struct CurrentLoop {
pub struct CurrentLoop<R> {
name: String,
region: Torus,
region: R,
}
impl CurrentLoop {
pub fn new(name: &str, r: Torus) -> Self {
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 {
let FieldSample(volume, directed_current, _current_vec) = state.map_sum_over_enumerated(&self.region, |coord: Meters, _cell| {
let tangent = self.region.tangent(coord);
let current = state.current(coord);
let directed_current = current.dot(tangent.cast());
FieldSample(1, directed_current.cast(), current.cast())
// i use a statistical lens for this:
// 1. current is the rate of flow of charge into a surface.
// 2. in any context where it makes sense to think of current, the current through each
// cross-sectional **is the same**.
// 3. each point in our 3d region belongs to exactly one cross-sectional surface.
// 4. so, given a point: what's the expected current through the cross section it belongs to?
// - answer: that point's current density times the cross section's area.
// 5. average the above over the whole volume, and you get an "average current".
//
// we're sampling uniformly over the cell space -- not the set of cross sections.
// - however, if all cross sections have equal area, this is equivalent.
// sampling all points (instead of just a single point):
// 1) removes bias from step #4: current *within* a cross section is not uniform, but if
// we sample every point within the cross section and weight them equally, then the
// average is the truth.
// 2) probably combats grid quantization / artifacting.
let FieldSample(num_samples, sum_cross_sectional_current, _current_vec) = state.map_sum_over_enumerated(&self.region, |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]
let current_density = state.current_density(coord); // [A/m^2]
// now we have an estimation of the entire current flowing through the cross section
// this cell belongs to.
let cross_sectional_current = current_density.dot(normal.cast()); // [A]
FieldSample(1, cross_sectional_current.cast(), current_density.cast())
});
let mean_directed_current = directed_current.cast::<f32>() / f32::from_primitive(volume);
let cross_section = self.region.cross_section() / (state.feature_size() * state.feature_size());
let cross_sectional_current = mean_directed_current * cross_section;
cross_sectional_current
let mean_cross_sectional_current = sum_cross_sectional_current.cast::<f32>() / f32::from_primitive(num_samples);
mean_cross_sectional_current
}
}
impl<S: AbstractSim> AbstractMeasurement<S> for CurrentLoop {
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![

View File

@@ -324,8 +324,15 @@ pub trait AbstractSim: Sync {
}))).flatten().flatten().sum()
}
/// returns the directed current at `c`, in `A / m^2`
fn current_density<C: Coord>(&self, c: C) -> Vec3<f32> {
self.sample(c).current_density().cast::<f32>()
}
/// returns the directed current at `c` in absolute units, `A`, or rather, `A` per cell, since
/// this looks at just a single cell. you probably want to use `current_density`.
fn current<C: Coord>(&self, c: C) -> Vec3<f32> {
self.sample(c).current_density().cast::<f32>() * self.feature_size() * self.feature_size()
self.current_density(c) * self.feature_size() * self.feature_size()
}
fn fill_region_using<C, Reg, F, M>(&mut self, region: &Reg, f: F)