40 Commits

Author SHA1 Message Date
084c5bc342 Region: remove Clone from the trait, and also parameterize everything
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.
2022-08-12 01:42:19 -07:00
d5fbb4e9b2 Region: remove the Serialization requirement 2022-08-12 00:57:01 -07:00
090b1ca09a BUGFIX: Torus: don't normalize the cross section normal
this would have led to incorrectly scaled current measurements
(but not incorrect current generation). we were likely severely
over-estimating the current.
2022-08-12 00:41:25 -07:00
ae1eb861be instrument the stimulus evaluation in our sim
... stimulus evaluation accounts for like 80% of the execution time 🤦
2022-08-11 22:57:43 -07:00
09bc7492ed expose diagnostics into the Sim, and capture stimuli evaluation
this isn't publicly exposed yet.
2022-08-11 22:43:07 -07:00
e7cc78a947 diagnostics: split into their own file 2022-08-11 22:31:05 -07:00
d379a7b0ee app: multi_core_inverter: try a related experiment where S0 is initialized to logic low 2022-08-11 22:24:19 -07:00
aa8f474f52 driver: Diagnostics: clean up the impl a bit 2022-08-11 19:04:12 -07:00
4a33912164 driver: abstract the render time measurements behind a Diagnostics api 2022-08-11 18:58:22 -07:00
f7b72a72be driver: abstract the step diagnostics measurements 2022-08-11 18:41:41 -07:00
a413a4d391 driver: move last_diag_time out of the Diagnostics object 2022-08-11 18:38:36 -07:00
0c9f04981a driver: relegate more diagnostics formatting to the Diagnostics impl 2022-08-11 18:36:35 -07:00
6f1e1557b3 driver: diagnostics: track the actual number of frames stepped
this allows fps-related diagnostics to be meaningful after
serialization / restarts.
2022-08-11 18:29:58 -07:00
e85d38d415 driver: split the Diagnostics out into their own object
more diagnostic refactoring to come
2022-08-11 18:27:30 -07:00
831cbfa76c app: multi_core_inverter: tune the state serializations
less frequent (for less disk space), and also save state
in a recoverable manner
2022-08-11 18:22:55 -07:00
1928ad71cd serializer renderer: gracefully handle the case where we run out of disk space
we might still run out of space when writing CSVs and other outputs...
but that's at least less likely,
as long as we serialize the rendering.
2022-08-11 18:21:46 -07:00
c83a44299f app: multi-core-inverter: fix S4 drive signal specification
there was a spuriuous high -> low transition
2022-08-11 15:24:26 -07:00
e23ab9efd7 app: multi_core_inverter: try a 5-stage inverter (each stage inverts)
we're diverging from the blog pretty far now.
but it turns out that, because of the inversion in Maxwell's
$\nabla x E = -dB/dT$ equation, the trivial wiring actually leads to
natural inverters.
2022-08-11 03:01:08 -07:00
652621e47a app: multi_core_inverter: more precise clock management
try to control the edges when the clock is release to prevent ringing.
2022-08-10 16:39:56 -07:00
59a4419130 app: multi_core_inverter: more detailed drive cycle 2022-08-10 15:47:28 -07:00
2f91418095 post: add doc-comments for these tools 2022-08-10 14:28:20 -07:00
46a53a4dde app: multi_core_inverter: fix up the drive sequence
see the code comment for explanation.
2022-08-10 01:43:36 -07:00
3998d72d02 app: multi_core_inverter: drive all four cores for four clock cycles 2022-08-10 01:35:42 -07:00
4fe8be8951 when writing Measurements to a CSV, format them in a machine-readable manner
i haven't tested the ones which contains commas -- hopefully the CSV
encoder deals with these :-)
2022-08-10 01:34:37 -07:00
8a3a64face meas: correctly render SI prefixes for negative numbers
the previous implementation treated negative numbers as effectively
having unknown magnitude, rendering them without any adjustment.
2022-08-10 01:17:49 -07:00
e08c6dbaa3 stim: backfill tests for CurlStimulus 2022-08-09 22:54:21 -07:00
520e9d9f68 CurlStimulus: re-use the HasCrossSection trait code
i believe this inverts the sign, but it also looks more correct this way
so i'm not immediately correcting that in this patch.
will backfill tests to verify.
2022-08-09 22:14:38 -07:00
1771973c6d CurlStimulus: take axis and center directly from the Region
by taking more from the region, we'll be able to reuse common code
and also make this more testable
2022-08-09 22:10:16 -07:00
7d1ee0ad50 meas: backfill tests for CurrentLoop 2022-08-05 17:35:55 -07:00
06379ffd30 CurrentLoop: use a better justified measurement algorithm
'course the best way to justify it is with tests: hopefully those will
come shortly.
2022-08-01 06:12:16 -07:00
527814e38a 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.
2022-08-01 05:17:35 -07:00
cc876d72d6 CurrentLoop: factor out the tangent calculation 2022-08-01 00:50:02 -07:00
723fed4786 rename meas::{eval_multiple_kv -> eval_multiple} 2022-07-31 23:27:37 -07:00
0e0945f744 measurement: remove the eval method 2022-07-31 23:26:53 -07:00
d5d8402c3d gitignore: don't ignore vim swap files
have the dev put them somewhere else
2022-07-31 17:15:42 -07:00
5362dacf3a Measurement: don't use SI prefix if there's no unit 2022-07-30 21:21:46 -07:00
b5c58c03ce meas: add a missing unit to the Energy measurement 2022-07-30 21:21:02 -07:00
530ab890e6 meas: render the SI prefix 2022-07-30 21:15:51 -07:00
542d700f69 meas: finish porting to a concrete type.
this will in future let me more easily test each individual measurement
type
2022-07-30 20:56:19 -07:00
60840aec36 WIP: make the measurement type concrete 2022-07-30 20:33:03 -07:00
24 changed files with 1020 additions and 572 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,2 @@
out/
target/
*.swp

72
Cargo.lock generated
View File

@@ -317,7 +317,6 @@ dependencies = [
"crossterm",
"csv",
"dashmap",
"dyn-clone",
"env_logger",
"float_eq",
"font8x8",
@@ -339,7 +338,6 @@ dependencies = [
"spirv_backend",
"spirv_backend_runner",
"threadpool",
"typetag",
"wgpu",
"y4m",
]
@@ -502,16 +500,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "ctor"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f877be4f7c9f246b183111634f75baa039715e3f46ce860677d3b19a69fb229c"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "cty"
version = "0.2.2"
@@ -565,12 +553,6 @@ dependencies = [
"libc",
]
[[package]]
name = "dyn-clone"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "140206b78fb2bc3edbcfc9b5ccbd0b30699cfe8d348b8b31b330e47df5291a5a"
[[package]]
name = "either"
version = "1.7.0"
@@ -590,15 +572,6 @@ dependencies = [
"termcolor",
]
[[package]]
name = "erased-serde"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81d013529d5574a60caeda29e179e695125448e5de52e3874f7b4c1d7360e18e"
dependencies = [
"serde",
]
[[package]]
name = "exr"
version = "1.4.2"
@@ -802,17 +775,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "ghost"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b93490550b1782c589a350f2211fff2e34682e25fed17ef53fc4fa8fe184975e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "gif"
version = "0.11.4"
@@ -1021,16 +983,6 @@ dependencies = [
"web-sys",
]
[[package]]
name = "inventory"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0257e268c91daba296499206db5dd5a058875936bec3d8429b5f3e20ec9dc9a"
dependencies = [
"ctor",
"ghost",
]
[[package]]
name = "itertools"
version = "0.10.3"
@@ -2236,30 +2188,6 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]]
name = "typetag"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36afde536989c95b0c35d2b6720618ed268b271a002fa1ebd97e821cf9ff28c6"
dependencies = [
"erased-serde",
"inventory",
"once_cell",
"serde",
"typetag-impl",
]
[[package]]
name = "typetag-impl"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c935ee7eb11a684e863c6a3332ff17caa2a46fc855719a90c6493aee213852b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.1"

View File

@@ -3,7 +3,20 @@
//! search for the conditions which maximize energy transfer from the one core to the other.
use coremem::{Driver, mat, meas};
use coremem::geom::{region, Cube, Dilate, Memoize, Meters, Spiral, SwapYZ, Torus, Translate, Wrap};
use coremem::geom::Meters;
use coremem::geom::region::{
self,
Cube,
Dilate,
Intersection,
InvertedRegion,
Memoize,
Spiral,
SwapYZ,
Torus,
Translate,
Wrap
};
use coremem::mat::{Ferroxcube3R1MH, IsoConductorOr};
use coremem::real::{R32, Real as _};
use coremem::render::CsvRenderer;
@@ -14,7 +27,6 @@ use log::{error, info, warn};
use serde::{Deserialize, Serialize};
mod cache;
use cache::DiskCache;
type Mat = IsoConductorOr<f32, Ferroxcube3R1MH>;
@@ -149,7 +161,11 @@ struct Geometries {
ferro2_region: Torus,
set1_region: Torus,
set2_region: Torus,
coupling_region: region::Union,
coupling_region: region::Union3<
Memoize<Dilate<Wrap<Translate<SwapYZ<Intersection<Spiral, Cube>>>>>>,
Memoize<Dilate<Wrap<Translate<SwapYZ<Intersection<Spiral, InvertedRegion<Cube>>>>>>>,
region::Union3<Cube, Cube, region::Union4<Cube, Cube, Cube, Cube>>
>,
coupling_wire_top: Cube,
coupling_wire_bot: Cube,
wrap1_len: f32,
@@ -285,29 +301,29 @@ fn derive_geometries(p: GeomParams) -> Option<Geometries> {
wrap2_bot - feat_sizes*2.0,
wrap2_bot.with_y(coupling_wire_bot.top()) + feat_sizes*2.0,
);
let coupling_stubs = region::Union::new()
.with(coupling_stub_top_left.clone())
.with(coupling_stub_top_right.clone())
.with(coupling_stub_bot_left.clone())
.with(coupling_stub_bot_right.clone())
;
let coupling_stubs = region::Union::new4(
coupling_stub_top_left.clone(),
coupling_stub_top_right.clone(),
coupling_stub_bot_left.clone(),
coupling_stub_bot_right.clone(),
);
let coupling_wires = region::Union::new()
.with(coupling_wire_top.clone())
.with(coupling_wire_bot.clone())
.with(coupling_stubs.clone())
;
let coupling_wires = region::Union::new3(
coupling_wire_top.clone(),
coupling_wire_bot.clone(),
coupling_stubs.clone(),
);
let coupling_region = region::Union::new()
.with(coupling_region1.clone())
.with(coupling_region2.clone())
.with(coupling_wires.clone())
;
let coupling_region = region::Union::new3(
coupling_region1.clone(),
coupling_region2.clone(),
coupling_wires.clone(),
);
let wrap1_with_coupling = region::union(
let wrap1_with_coupling = region::Union::new2(
coupling_region1.clone(), coupling_wires.clone()
);
let wrap2_with_coupling = region::union(
let wrap2_with_coupling = region::Union::new2(
coupling_region2.clone(), coupling_wires.clone()
);
@@ -429,8 +445,6 @@ fn run_sim(id: u32, p: Params, g: Geometries) -> Results {
driver.add_stimulus(CurlStimulus::new(
region.clone(),
wave.clone(),
region.center(),
region.axis()
));
};
@@ -439,8 +453,6 @@ fn run_sim(id: u32, p: Params, g: Geometries) -> Results {
driver.add_stimulus(CurlStimulus::new(
region.clone(),
wave.clone(),
region.center(),
region.axis()
));
};
@@ -449,8 +461,6 @@ fn run_sim(id: u32, p: Params, g: Geometries) -> Results {
driver.add_stimulus(CurlStimulus::new(
region.clone(),
wave.clone(),
region.center(),
region.axis()
));
};

View File

@@ -42,134 +42,227 @@ use coremem::meas;
use coremem::real::{self, Real as _};
use coremem::sim::spirv::{self, SpirvSim};
use coremem::sim::units::Seconds;
use coremem::stim::{CurlStimulus, Gated, Sinusoid1, TimeVarying as _};
use coremem::stim::{CurlStimulus, Exp, Gated, Sinusoid1, TimeVarying as _};
use coremem::Driver;
type R = real::R32;
type Mat = IsoConductorOr<R, Ferroxcube3R1MH>;
// type Backend = spirv::CpuBackend;
type Backend = spirv::WgpuBackend;
type Sim = SpirvSim::<R, Mat, Backend>;
#[derive(Clone, Copy)]
enum DriveType {
HoldHigh,
ReleaseHigh,
HoldLow,
ReleaseLow,
Float,
}
impl DriveType {
fn direction(&self) -> i32 {
use DriveType::*;
match self {
HoldHigh | ReleaseHigh => 1,
HoldLow | ReleaseLow => -1,
Float => 0,
}
}
fn is_hold(&self) -> bool {
match self {
Self::HoldHigh | Self::HoldLow => true,
_ => false,
}
}
fn is_release(&self) -> bool {
match self {
Self::ReleaseHigh | Self::ReleaseLow => true,
_ => false,
}
}
}
fn main() {
coremem::init_logging();
coremem::init_debug();
// coremem::init_debug();
let um = |n| n as f32 * 1e-6;
// let ns = |n| n as f32 * 1e-9;
let ps = |n| n as f32 * 1e-12;
let feat_size = um(10);
let input_magnitude = 1.0e9;
let clock_phase_duration = ps(1000);
let clock_phase_duration = ps(2000);
// 's' = core (ferromagnetic part)
let s_major = um(160);
let s_minor = um(30);
// 'io' = drive/control wire
let io_major = um(80);
let io_minor = um(30);
let coupling_major = um(130);
let coupling_minor = um(30);
// coords for core 'n'
let sx = |n| um((n+1) * 400);
let sy = um(400);
let sz = um(280);
// x-coord for the loop coupling core 'n' to core 'n+1'
let couplingx = |n| sx(n) + um(200);
let sim_bounds = Meters::new(sx(4), sy * 2.0, sz * 2.0);
let sim_bounds = |num_cores| Meters::new(sx(num_cores), sy * 2.0, sz * 2.0);
let sim_padding = Meters::new(um(80), um(80), um(80));
let drive0 = Torus::new_xz(Meters::new(sx(0) - s_major, sy, sz), io_major, io_minor);
let sense3 = Torus::new_xz(Meters::new(sx(3) + s_major, sy, sz), io_major, io_minor);
// core '0' gets an external input in place of its coupling loop
let input0 = Torus::new_xz(Meters::new(sx(0) - s_major, sy, sz), io_major, io_minor);
// the last core gets an external output in place of its coupling loop
let sense = |n| Torus::new_xz(Meters::new(sx(n) + s_major, sy, sz), io_major, io_minor);
// control loop for core n (alternately called "drive" loop)
let ctl = |n| Torus::new_yz(Meters::new(sx(n), sy + s_major, sz), io_major, io_minor);
let s = |n| Torus::new_xy(Meters::new(sx(n), sy, sz), s_major, s_minor);
// coupling(n) is the wire which couples core n into core n+1
let coupling = |n| Torus::new_xz(Meters::new(couplingx(n), sy, sz), coupling_major, coupling_minor);
let input = |region: &Torus, cycle: u32, direction: i32| {
let control_signal = |driver: &mut Driver<Sim>, region: &Torus, cycle: u32, ty: DriveType| {
let area = region.cross_section();
let amp = direction as f32 * input_magnitude / area;
let amp = ty.direction() as f32 * input_magnitude / area;
let start = clock_phase_duration * cycle as f32;
let wave = Gated::new(amp, start, start + clock_phase_duration);
if ty.is_hold() {
// simple square wave:
let wave = Gated::new(amp, start, start + clock_phase_duration);
let stim = CurlStimulus::new(
region.clone(),
wave.clone(),
);
driver.add_stimulus(stim);
} else if ty.is_release() {
// decaying exponential wave:
let wave = Exp::new_at(amp, start, 0.125 * clock_phase_duration /* half-life */);
let stim = CurlStimulus::new(
region.clone(),
wave.clone(),
);
driver.add_stimulus(stim);
} // else: Float
// sine wave (untested):
// let wave = Sinusoid1::from_wavelength(direction as f32 * input_magnitude / area, clock_phase_duration * 2.0)
// .half_cycle()
// .shifted(clock_phase_duration * cycle as f32);
CurlStimulus::new(
region.clone(),
wave.clone(),
region.center(),
region.axis(),
)
};
//////// define the control signals/transitions
// each row N denotes the drive currents at clock cycle N.
// each col M denotes the drive current at core M.
use DriveType::*;
// let s0_init_state = HoldHigh; // logic high
let s0_init_state = HoldLow; // logic low
let s0_rel_state = ReleaseLow;
let drive_map = [
// charge each device to '1'
[s0_init_state,HoldHigh, HoldHigh, HoldHigh, HoldHigh],
// this is when we'd ordinarily open S0 for write
[s0_rel_state, HoldHigh, HoldHigh, HoldHigh, HoldHigh],
// this is when S0 would ordinarily receive a write
[Float, HoldHigh, HoldHigh, HoldHigh, HoldHigh],
// open S1 for write
[Float, ReleaseHigh, HoldHigh, HoldHigh, HoldHigh],
// write S0' => S1
[HoldLow, Float, HoldHigh, HoldHigh, HoldHigh],
// open S2 for write
[HoldLow, Float, ReleaseHigh, HoldHigh, HoldHigh],
// write S1' => S2
[HoldLow, HoldLow, Float, HoldHigh, HoldHigh],
// open S3 for write
[HoldLow, HoldLow, Float, ReleaseHigh, HoldHigh],
// write S2' => S3; RESET S0
[HoldHigh, HoldLow, HoldLow, Float, HoldHigh],
// open S4 for write
[HoldHigh, HoldLow, HoldLow, Float, ReleaseHigh],
// write S3' => S4; RESET S1
[HoldHigh, HoldHigh, HoldLow, HoldLow, Float],
// open S0 for write
[ReleaseHigh, HoldHigh, HoldLow, HoldLow, Float],
// write S4' => output; RESET S2
[Float, HoldHigh, HoldHigh, HoldLow, HoldLow],
// open S1 for write
[Float, ReleaseHigh, HoldHigh, HoldLow, HoldLow],
];
let wire_mat = IsomorphicConductor::new(1e6f32.cast::<R>());
// let ferro_mat = wire_mat;
let ferro_mat = Ferroxcube3R1MH::new();
let mut driver = Driver::new(SpirvSim::<R, Mat, Backend>::new(
sim_bounds.to_index(feat_size), feat_size,
let num_cores = drive_map[0].len();
let last_core = num_cores - 1;
let mut driver = Driver::new(Sim::new(
sim_bounds(num_cores).to_index(feat_size), feat_size,
));
driver.add_classical_boundary_explicit::<R, _>(sim_padding);
//////// create the wires and toroids
driver.fill_region(&drive0, wire_mat);
driver.fill_region(&sense3, wire_mat);
for core in 0..4 {
driver.fill_region(&input0, wire_mat);
driver.fill_region(&sense(last_core), wire_mat);
for core in 0..num_cores {
driver.fill_region(&s(core), ferro_mat);
driver.fill_region(&ctl(core), wire_mat);
if core != 3 {
if core != last_core {
driver.fill_region(&coupling(core), wire_mat);
}
}
//////// monitor some measurements
// driver.add_measurement(meas::CurrentLoop::new("drv0", drive0.clone()));
for core in 0..4 {
// driver.add_measurement(meas::CurrentLoop::new("input0", input0.clone()));
for core in 0..num_cores {
driver.add_measurement(meas::CurrentLoop::new(
&format!("drive{}", core),
ctl(core),
));
}
for core in 0..3 {
driver.add_measurement(meas::CurrentLoop::new(
&format!("sense{}", core),
coupling(core),
));
for core in 0..num_cores {
let name = format!("sense{}", core);
if core != last_core {
driver.add_measurement(meas::CurrentLoop::new(
&name, coupling(core),
));
} else {
driver.add_measurement(meas::CurrentLoop::new(
&name, sense(core),
));
}
}
for core in 0..4 {
for core in 0..num_cores {
driver.add_measurement(meas::MagneticLoop::new(
&format!("state{}", core),
s(core),
));
}
driver.add_measurement(meas::CurrentLoop::new("sense3", sense3.clone()));
//////// create the stimuli
// CTL{n} effectively leads CTL{n-1}
// or: at time t, CTL{n} is at cycle[t+n]
// where cycle[t] is defined by CTL[0](t):
// 0, +Vdd, +Vdd, +Vdd
// TODO: this is wrong (as is the diagram in the blog)! CTL0, being an inverter,
// needs -Vdd to recharge to +polarization
let cycles = 1;
let duration = Seconds(clock_phase_duration * (cycles + 2) as f32);
let cycles = drive_map.len() as u32;
let duration = Seconds(clock_phase_duration * (cycles + 3) as f32);
for cycle in 0..cycles {
for core in 0..1 { // TODO: core 0
let dir = 1;
//let dir = if (cycle+core) % 4 == 0 {
// 0
//} else {
// 1
//};
if dir != 0 {
// micro opt/safety: don't place zero-magnitude stimuli
driver.add_stimulus(input(&ctl(core), cycle, dir));
}
for core in 0..num_cores {
// current polarity of the drive wires is inverted in this model v.s. the blog article,
// so invert our currents in order to achieve the magnetic states of the article.
let sig = drive_map[cycle as usize][core as usize];
control_signal(&mut driver, &ctl(core), cycle, sig);
}
}
let prefix = "out/applications/multi_core_inverter/2/";
let prefix = "out/applications/multi_core_inverter/11/";
let _ = std::fs::create_dir_all(&prefix);
// driver.add_state_file(&*format!("{}state.bc", prefix), 9600);
driver.add_serializer_renderer(&*format!("{}frame-", prefix), 400, None);
driver.add_csv_renderer(&*format!("{}meas.csv", prefix), 100, None);
driver.add_state_file(&*format!("{}state.bc", prefix), 6400);
driver.add_serializer_renderer(&*format!("{}frame-", prefix), 3200, None);
driver.add_csv_renderer(&*format!("{}meas-detailed.csv", prefix), 100, None);
driver.add_csv_renderer(&*format!("{}meas.csv", prefix), 800, None);
driver.set_steps_per_stim(100);
driver.step_until(duration);

View File

@@ -84,8 +84,6 @@ fn main() {
driver.add_stimulus(CurlStimulus::new(
region.clone(),
wave.clone(),
region.center(),
region.axis()
));
};

View File

@@ -15,7 +15,6 @@ common_macros = "0.1" # MIT or Apache 2.0
crossterm = "0.24" # MIT
csv = "1.1" # MIT or Unlicense
dashmap = "5.3" # MIT
dyn-clone = "1.0" # MIT or Apache 2.0
env_logger = "0.9" # MIT or Apache 2.0
float_eq = "1.0" # MIT or Apache 2.0
font8x8 = "0.3" # MIT
@@ -33,7 +32,6 @@ rand = "0.8" # MIT or Apache 2.0
rayon = "1.5" # MIT or Apache 2.0
serde = "1.0" # MIT or Apache 2.0
threadpool = "1.8" # MIT or Apache 2.0
typetag = "0.2" # MIT or Apache 2.0
y4m = "0.7" # MIT
wgpu = "0.12"

View File

@@ -0,0 +1,111 @@
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
/// this is just a big dumb bag of perf-related metrics,
/// gathered with the intention of identifying areas for optimization
pub struct Diagnostics {
frames_completed: u64,
time_spent_stepping: Duration,
time_spent_on_stimuli: Duration,
time_spent_prepping_render: Duration,
time_spent_blocked_on_render: Duration,
time_spent_rendering: Duration,
start_time: Instant,
}
#[derive(Clone, Default)]
pub struct SyncDiagnostics(Arc<Mutex<Diagnostics>>);
impl Default for Diagnostics {
fn default() -> Self {
Self::new()
}
}
impl Diagnostics {
pub fn new() -> Self {
Self {
frames_completed: 0,
time_spent_stepping: Default::default(),
time_spent_on_stimuli: Default::default(),
time_spent_prepping_render: Default::default(),
time_spent_blocked_on_render: Default::default(),
time_spent_rendering: Default::default(),
start_time: Instant::now(),
}
}
pub fn format(&self) -> String {
let step_time = self.time_spent_stepping.as_secs_f64();
let stim_time = self.time_spent_on_stimuli.as_secs_f64();
let render_time = self.time_spent_rendering.as_secs_f64();
let render_prep_time = self.time_spent_prepping_render.as_secs_f64();
let block_time = self.time_spent_blocked_on_render.as_secs_f64();
let overall_time = self.start_time.elapsed().as_secs_f64();
let fps = (self.frames_completed as f64) / overall_time;
format!("fps: {:6.2} (sim: {:.1}s, stim: {:.1}s, [render: {:.1}s], blocked: {:.1}s, render_prep: {:.1}s, other: {:.1}s)",
fps,
step_time,
stim_time,
render_time,
block_time,
render_prep_time,
overall_time - step_time /*- stim_time*/ - block_time - render_prep_time,
)
}
}
impl SyncDiagnostics {
pub fn new() -> Self {
Self(Arc::new(Mutex::new(Diagnostics::new())))
}
pub fn format(&self) -> String {
self.0.lock().unwrap().format()
}
/// measure the duration of some arbitrary chunk of code.
/// used internally.
pub fn measure<R, F: FnOnce() -> R>(f: F) -> (Duration, R) {
let start = Instant::now();
let r = f();
(start.elapsed(), r)
}
/// record the duration of the sim step operation.
pub fn instrument_step<R, F: FnOnce() -> R>(&self, frames: u64, f: F) -> R {
let (elapsed, ret) = Self::measure(f);
let mut me = self.0.lock().unwrap();
me.time_spent_stepping += elapsed;
me.frames_completed += frames as u64;
ret
}
/// record the duration spent preparing for render (i.e. cloning stuff and moving it into a
/// render pool).
pub fn instrument_render_prep<R, F: FnOnce() -> R>(&self, f: F) -> R {
let (elapsed, ret) = Self::measure(f);
self.0.lock().unwrap().time_spent_prepping_render += elapsed;
ret
}
/// record the duration actually spent doing CPU render work
pub fn instrument_render_cpu_side<R, F: FnOnce() -> R>(&self, f: F) -> R {
let (elapsed, ret) = Self::measure(f);
self.0.lock().unwrap().time_spent_rendering += elapsed;
ret
}
/// record the duration spent blocking the simulation because the render queue is full.
pub fn instrument_render_blocked<R, F: FnOnce() -> R>(&self, f: F) -> R{
let (elapsed, ret) = Self::measure(f);
self.0.lock().unwrap().time_spent_blocked_on_render += elapsed;
ret
}
pub fn instrument_stimuli<R, F: FnOnce() -> R>(&self, f: F) -> R {
let (elapsed, ret) = Self::measure(f);
self.0.lock().unwrap().time_spent_on_stimuli += elapsed;
ret
}
}

View File

@@ -1,3 +1,4 @@
use crate::diagnostics::SyncDiagnostics;
use crate::geom::{Coord, Index, Meters, Region};
use crate::mat;
use crate::meas::{self, AbstractMeasurement};
@@ -10,9 +11,9 @@ use crate::stim::{self, AbstractStimulus};
use log::{info, trace};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::sync::Arc;
use std::sync::mpsc::{sync_channel, SyncSender, Receiver};
use std::time::{Duration, Instant};
use std::time::Instant;
use threadpool::ThreadPool;
pub struct Driver<S> {
@@ -21,32 +22,23 @@ pub struct Driver<S> {
// TODO: use Rayon's thread pool?
render_pool: ThreadPool,
render_channel: (SyncSender<()>, Receiver<()>),
// TODO: split out into a Diagnostics struct
time_spent_stepping: Duration,
time_spent_on_stimuli: Duration,
time_spent_prepping_render: Duration,
time_spent_blocked_on_render: Duration,
time_spent_rendering: Arc<Mutex<Duration>>,
measurements: Vec<Arc<dyn AbstractMeasurement<S>>>,
stimuli: StimuliAdapter,
start_time: Instant,
last_diag_time: Instant,
/// simulation end time
sim_end_time: Option<Frame>,
diag: SyncDiagnostics,
last_diag_time: Instant,
}
impl<S: AbstractSim> Driver<S> {
pub fn new(state: S) -> Self {
pub fn new(mut state: S) -> Self {
let diag = SyncDiagnostics::new();
state.use_diagnostics(diag.clone());
Self {
state,
renderer: Arc::new(MultiRenderer::new()),
render_pool: ThreadPool::new(3),
render_channel: sync_channel(0),
time_spent_stepping: Default::default(),
time_spent_on_stimuli: Default::default(),
time_spent_prepping_render: Default::default(),
time_spent_blocked_on_render: Default::default(),
time_spent_rendering: Default::default(),
measurements: vec![
Arc::new(meas::Time),
Arc::new(meas::Meta),
@@ -54,9 +46,9 @@ impl<S: AbstractSim> Driver<S> {
Arc::new(meas::Power::world()),
],
stimuli: StimuliAdapter::new(),
start_time: Instant::now(),
last_diag_time: Instant::now(),
sim_end_time: None,
diag,
last_diag_time: Instant::now(),
}
}
@@ -135,6 +127,7 @@ impl<S: AbstractSim + Send + Sync + Serialize + for<'a> Deserialize<'a> + 'stati
let ser = render::SerializerRenderer::new(state_file);
let loaded = ser.try_load().map(|s| {
self.state = s.state;
self.state.use_diagnostics(self.diag.clone());
}).is_some();
self.add_renderer(ser, state_file, snapshot_frequency, None);
loaded
@@ -143,26 +136,26 @@ impl<S: AbstractSim + Send + Sync + Serialize + for<'a> Deserialize<'a> + 'stati
impl<S: AbstractSim + Clone + Default + Send + 'static> Driver<S> {
fn render(&mut self) {
let prep_start = Instant::now();
let their_state = self.state.clone();
let their_measurements = self.measurements.clone();
let renderer = self.renderer.clone();
let time_spent_rendering = self.time_spent_rendering.clone();
let sender = self.render_channel.0.clone();
self.render_pool.execute(move || {
// unblock the main thread (this limits the number of renders in-flight at any time
sender.send(()).unwrap();
trace!("render begin");
let start_time = Instant::now();
let meas: Vec<&dyn AbstractMeasurement<S>> = their_measurements.iter().map(|m| &**m).collect();
renderer.render(&their_state, &*meas, Default::default());
*time_spent_rendering.lock().unwrap() += start_time.elapsed();
trace!("render end");
self.diag.instrument_render_prep(|| {
let diag_handle = self.diag.clone();
let their_state = self.state.clone();
let their_measurements = self.measurements.clone();
let renderer = self.renderer.clone();
let sender = self.render_channel.0.clone();
self.render_pool.execute(move || {
// unblock the main thread (this limits the number of renders in-flight at any time
sender.send(()).unwrap();
trace!("render begin");
diag_handle.instrument_render_cpu_side(|| {
let meas: Vec<&dyn AbstractMeasurement<S>> = their_measurements.iter().map(|m| &**m).collect();
renderer.render(&their_state, &*meas, Default::default());
});
trace!("render end");
});
});
self.diag.instrument_render_blocked(|| {
self.render_channel.1.recv().unwrap();
});
self.time_spent_prepping_render += prep_start.elapsed();
let block_start = Instant::now();
self.render_channel.1.recv().unwrap();
self.time_spent_blocked_on_render += block_start.elapsed();
}
/// Return the number of steps actually stepped
fn step_at_most(&mut self, at_most: u32) -> u32 {
@@ -183,37 +176,22 @@ impl<S: AbstractSim + Clone + Default + Send + 'static> Driver<S> {
can_step += 1;
}
trace!("step begin");
let start_time = Instant::now();
self.state.step_multiple(can_step, &self.stimuli);
self.time_spent_stepping += start_time.elapsed();
self.diag.instrument_step(can_step as u64, || {
self.state.step_multiple(can_step, &self.stimuli);
});
trace!("step end");
if self.last_diag_time.elapsed().as_secs_f64() >= 5.0 {
self.last_diag_time = Instant::now();
let step = self.state.step_no();
let step_time = self.time_spent_stepping.as_secs_f64();
let stim_time = self.time_spent_on_stimuli.as_secs_f64();
let render_time = self.time_spent_rendering.lock().unwrap().as_secs_f64();
let render_prep_time = self.time_spent_prepping_render.as_secs_f64();
let block_time = self.time_spent_blocked_on_render.as_secs_f64();
let overall_time = self.start_time.elapsed().as_secs_f64();
let fps = (self.state.step_no() as f64) / overall_time;
let diagstr = self.diag.format();
let sim_time = self.state.time() as f64;
let percent_complete = match self.sim_end_time {
Some(t) => format!("[{:.1}%] ", 100.0 * self.state.time() / *t.to_seconds(self.timestep())),
None => "".to_owned(),
};
info!(
"{}t={:.2e} frame {:06} fps: {:6.2} (sim: {:.1}s, stim: {:.1}s, [render: {:.1}s], blocked: {:.1}s, render_prep: {:.1}s, other: {:.1}s)",
percent_complete,
sim_time,
step,
fps,
step_time,
stim_time,
render_time,
block_time,
render_prep_time,
overall_time - step_time - stim_time - block_time - render_prep_time,
"{}t={:.2e} frame {:06} {}",
percent_complete, sim_time, step, diagstr
);
}
can_step as u32

View File

@@ -6,7 +6,22 @@ mod units;
pub use line::Line2d;
pub use polygon::Polygon2d;
pub use region::{
Cube, CylinderZ, Dilate, InvertedRegion, Memoize, Region, Sphere, Spiral, SwapXZ, SwapYZ, Torus, Translate, Union, WorldRegion, Wrap
Cube,
CylinderZ,
Dilate,
HasCrossSection,
InvertedRegion,
Memoize,
Region,
Sphere,
Spiral,
SwapXZ,
SwapYZ,
Torus,
Translate,
Union,
WorldRegion,
Wrap,
};
pub use units::{Coord, Meters, OrdMeters, Index};

View File

@@ -1,6 +1,7 @@
use crate::geom::{Coord, Meters, OrdMeters};
use dyn_clone::{self, DynClone};
use coremem_cross::vec::Vec3;
use rayon::prelude::*;
use serde::{Serialize, Deserialize};
use std::collections::BTreeSet;
@@ -10,22 +11,27 @@ mod primitives;
pub use primitives::*;
#[typetag::serde(tag = "type")]
pub trait Region: Send + Sync + DynClone {
pub trait Region: Send + Sync {
fn contains(&self, p: Meters) -> bool;
}
dyn_clone::clone_trait_object!(Region);
pub fn and<T1: Region + 'static, T2: Region + 'static>(r1: T1, r2: T2) -> Intersection {
Intersection::new().and(r1).and(r2)
/// some (volume) which has a tangent vector everywhere inside/on it.
/// for example, a cylinder has tangents everywhere except its axis.
/// the returned vector should represent the area of the cross section.
pub trait HasCrossSection {
fn cross_section_normal(&self, p: Meters) -> Vec3<f32>;
}
pub fn and_not<T1: Region + 'static, T2: Region + 'static>(r1: T1, r2: T2) -> Intersection {
pub fn and<T1: Region + 'static, T2: Region + 'static>(r1: T1, r2: T2) -> Intersection<T1, T2> {
Intersection::new2(r1, r2)
}
pub fn and_not<T1: Region + 'static, T2: Region + 'static>(r1: T1, r2: T2) -> Intersection<T1, InvertedRegion<T2>> {
and(r1, InvertedRegion::new(r2))
}
pub fn union<T1: Region + 'static, T2: Region + 'static>(r1: T1, r2: T2) -> Union {
Union::new().with(r1).with(r2)
pub fn union<T1: Region + 'static, T2: Region + 'static>(r1: T1, r2: T2) -> Union<T1, T2> {
Union::new2(r1, r2)
}
/// returns true if there's a path (via the cardinal directions) from p0 to p1 within this region.
@@ -67,137 +73,124 @@ pub fn distance_to<R: Region, C: Coord>(r: &R, p0: C, p1: C, feat_size: f32) ->
}
/// Region describing the entire simulation space
#[derive(Copy, Clone, Serialize, Deserialize)]
#[derive(Copy, Clone, Default, Serialize, Deserialize)]
pub struct WorldRegion;
#[typetag::serde]
impl Region for WorldRegion {
fn contains(&self, _: Meters) -> bool {
true
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct InvertedRegion(Box<dyn Region>);
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct InvertedRegion<R>(R);
impl InvertedRegion {
pub fn new<R: Region + 'static>(r: R) -> Self {
Self(Box::new(r))
impl<R> InvertedRegion<R> {
pub fn new(r: R) -> Self {
Self(r)
}
}
#[typetag::serde]
impl Region for InvertedRegion {
impl<R: Region> Region for InvertedRegion<R> {
fn contains(&self, p: Meters) -> bool {
!self.0.contains(p)
}
}
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct Union(Vec<Box<dyn Region>>);
pub struct Union<R1, R2>(R1, R2);
impl Union {
pub fn new() -> Self {
Self(Vec::new())
pub type Union3<R1, R2, R3> = Union<Union<R1, R2>, R3>;
pub type Union4<R1, R2, R3, R4> = Union<Union3<R1, R2, R3>, R4>;
impl<R1, R2> Union<R1, R2> {
pub fn with<R: Region>(self, r: R) -> Union<Self, R> {
Union::new2(self, r)
}
pub fn new_with<R: Region + 'static>(r: R) -> Self {
Self::new().with(r)
pub fn new2(r1: R1, r2: R2) -> Self {
Self(r1, r2)
}
pub fn with<R: Region + 'static>(self, r: R) -> Self {
self.with_box(Box::new(r))
pub fn new3<R3: Region>(r1: R1, r2: R2, r3: R3) -> Union<Self, R3> {
Union::new2(r1, r2).with(r3)
}
pub fn with_box(mut self, r: Box<dyn Region>) -> Self {
self.0.push(r);
self
pub fn new4<R3: Region, R4: Region>(r1: R1, r2: R2, r3: R3, r4: R4) -> Union<Union<Self, R3>, R4> {
Union::new2(r1, r2).with(r3).with(r4)
}
}
#[typetag::serde]
impl Region for Union {
impl<R1: Region, R2: Region> Region for Union<R1, R2> {
fn contains(&self, p: Meters) -> bool {
self.0.iter().any(|r| r.contains(p))
self.0.contains(p) || self.1.contains(p)
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Intersection(Vec<Box<dyn Region>>);
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct Intersection<R1, R2>(R1, R2);
impl Intersection {
pub fn new() -> Self {
Self(Vec::new())
impl<R1, R2> Intersection<R1, R2> {
pub fn and<R3: Region>(self, r: R3) -> Intersection<Self, R3> {
Intersection::new2(self, r)
}
pub fn new_with<R: Region + 'static>(r: R) -> Self {
Self::new().and(r)
}
pub fn and<R: Region + 'static>(self, r: R) -> Self {
self.and_box(Box::new(r))
}
pub fn and_box(mut self, r: Box<dyn Region>) -> Self {
self.0.push(r);
self
pub fn new2(r1: R1, r2: R2) -> Self {
Self(r1, r2)
}
}
#[typetag::serde]
impl Region for Intersection {
impl<R1: Region, R2: Region> Region for Intersection<R1, R2> {
fn contains(&self, p: Meters) -> bool {
self.0.iter().all(|r| r.contains(p))
self.0.contains(p) && self.1.contains(p)
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Translate {
inner: Box<dyn Region>,
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct Translate<R> {
inner: R,
shift: Meters,
}
impl Translate {
pub fn new<T: Region + 'static>(inner: T, shift: Meters) -> Self {
Self { inner: Box::new(inner), shift }
impl<R> Translate<R> {
pub fn new(inner: R, shift: Meters) -> Self {
Self { inner, shift }
}
}
#[typetag::serde]
impl Region for Translate {
impl<R: Region> Region for Translate<R> {
fn contains(&self, p: Meters) -> bool {
self.inner.contains(p - self.shift)
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct SwapXZ {
inner: Box<dyn Region>,
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct SwapXZ<R> {
inner: R,
}
impl SwapXZ {
pub fn new<T: Region + 'static>(inner: T) -> Self {
Self { inner: Box::new(inner) }
impl<R> SwapXZ<R> {
pub fn new(inner: R) -> Self {
Self { inner }
}
}
#[typetag::serde]
impl Region for SwapXZ {
impl<R: Region> Region for SwapXZ<R> {
fn contains(&self, p: Meters) -> bool {
let p = Meters::new(p.z(), p.y(), p.z());
self.inner.contains(p)
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct SwapYZ {
inner: Box<dyn Region>,
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct SwapYZ<R> {
inner: R,
}
impl SwapYZ {
pub fn new<T: Region + 'static>(inner: T) -> Self {
Self { inner: Box::new(inner) }
impl<R> SwapYZ<R> {
pub fn new(inner: R) -> Self {
Self { inner }
}
}
#[typetag::serde]
impl Region for SwapYZ {
impl<R: Region> Region for SwapYZ<R> {
fn contains(&self, p: Meters) -> bool {
let mapped = Meters::new(p.x(), p.z(), p.y());
self.inner.contains(mapped)
@@ -210,20 +203,20 @@ impl Region for SwapYZ {
/// the resulting region is mapped onto the original region y=[0, y_max]. x is just the radius
/// so that (0, 0) is mapped to (0, 0), and (1, 0) is mapped to (1, 0) and (0, 1) is mapped to
/// (1, 0.5*y_max) and (-5, 0) is mapped to (5, 0.5*y_max).
#[derive(Clone, Serialize, Deserialize)]
pub struct Wrap {
inner: Box<dyn Region>,
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct Wrap<R> {
inner: R,
y_max: f32,
about: Meters,
}
impl Wrap {
pub fn new<T: Region + 'static>(inner: T, y_max: f32) -> Self {
impl<R> Wrap<R> {
pub fn new(inner: R, y_max: f32) -> Self {
Self::new_about(inner, y_max, Meters::new(0.0, 0.0, 0.0))
}
pub fn new_about<T: Region + 'static>(inner: T, y_max: f32, about: Meters) -> Self {
Self { inner: Box::new(inner), y_max, about }
pub fn new_about(inner: R, y_max: f32, about: Meters) -> Self {
Self { inner, y_max, about }
}
fn map(&self, p: Meters) -> Meters {
@@ -235,28 +228,26 @@ impl Wrap {
}
}
#[typetag::serde]
impl Region for Wrap {
impl<R: Region> Region for Wrap<R> {
fn contains(&self, p: Meters) -> bool {
self.inner.contains(self.map(p))
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Dilate {
inner: Box<dyn Region>,
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct Dilate<R> {
inner: R,
rad: f32,
res: f32,
}
impl Dilate {
pub fn new<T: Region + 'static>(inner: T, rad: f32, res: f32) -> Self {
Self { inner: Box::new(inner), rad, res }
impl<R> Dilate<R> {
pub fn new(inner: R, rad: f32, res: f32) -> Self {
Self { inner, rad, res }
}
}
#[typetag::serde]
impl Region for Dilate {
impl<R: Region> Region for Dilate<R> {
fn contains(&self, p: Meters) -> bool {
let rad_iters = (self.rad / self.res).ceil() as i32;
let rad_range = -rad_iters..=rad_iters;
@@ -279,24 +270,23 @@ impl Region for Dilate {
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Memoize {
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct Memoize<R> {
#[serde(skip)]
lut: Arc<dashmap::DashMap<OrdMeters, bool>>,
inner: Box<dyn Region>,
inner: R,
}
impl Memoize {
pub fn new<R: Region + 'static>(inner: R) -> Self {
impl<R> Memoize<R> {
pub fn new(inner: R) -> Self {
Self {
lut: Arc::new(dashmap::DashMap::new()),
inner: Box::new(inner),
inner,
}
}
}
#[typetag::serde]
impl Region for Memoize {
impl<R: Region> Region for Memoize<R> {
fn contains(&self, p: Meters) -> bool {
*self.lut.entry(OrdMeters(p)).or_insert_with(|| self.inner.contains(p))
}
@@ -307,7 +297,7 @@ mod test {
use super::*;
use float_eq::assert_float_eq;
fn assert_map(w: &Wrap, from: Meters, to: Meters) {
fn assert_map<R>(w: &Wrap<R>, from: Meters, to: Meters) {
let mapped = w.map(from);
assert_float_eq!(mapped.x(), to.x(), abs <= 0.01);
assert_float_eq!(mapped.y(), to.y(), abs <= 0.01);

View File

@@ -6,9 +6,9 @@ use serde::{Serialize, Deserialize};
use std::fmt::{self, Display};
use std::ops::Range;
use super::Region;
use super::{HasCrossSection, Region};
#[derive(Copy, Clone, Serialize, Deserialize)]
#[derive(Copy, Clone, Default, Serialize, Deserialize)]
pub struct CylinderZ {
center: Vec2<f32>,
radius: f32,
@@ -24,7 +24,6 @@ impl CylinderZ {
}
}
#[typetag::serde]
impl Region for CylinderZ {
fn contains(&self, p: Meters) -> bool {
p.xy().distance_sq(self.center) <= self.radius * self.radius
@@ -77,7 +76,6 @@ impl Torus {
}
}
#[typetag::serde]
impl Region for Torus {
fn contains(&self, p: Meters) -> bool {
// a torus is the set of all points < distance `r` from the circle of radius `R`,
@@ -104,7 +102,16 @@ impl Region for Torus {
}
}
#[derive(Copy, Clone, Serialize, Deserialize)]
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 normal which always points "counter-clockwise" along the shape
axis.cross(to_coord)
}
}
#[derive(Copy, Clone, Default, Serialize, Deserialize)]
pub struct Sphere {
center: Meters,
rad: f32,
@@ -119,7 +126,6 @@ impl Sphere {
}
}
#[typetag::serde]
impl Region for Sphere {
fn contains(&self, p: Meters) -> bool {
p.distance_sq(*self.center) < self.rad * self.rad
@@ -228,7 +234,6 @@ impl Cube {
}
}
#[typetag::serde]
impl Region for Cube {
fn contains(&self, p: Meters) -> bool {
self.x_range().contains(&p.x()) &&
@@ -238,7 +243,7 @@ impl Region for Cube {
}
/// a Spiral traces out a circle on the xy plane as z increases.
#[derive(Copy, Clone, Serialize, Deserialize)]
#[derive(Copy, Clone, Default, Serialize, Deserialize)]
pub struct Spiral {
/// radius of the spiral
major: f32,
@@ -257,7 +262,6 @@ impl Spiral {
}
}
#[typetag::serde]
impl Region for Spiral {
fn contains(&self, p: Meters) -> bool {
let revs = p.z() / self.period;

View File

@@ -7,6 +7,7 @@
use log::info;
mod diagnostics;
pub mod driver;
pub mod geom;
pub mod meas;

View File

@@ -1,13 +1,13 @@
use crate::geom::{Meters, Region, Torus, WorldRegion};
use crate::geom::{HasCrossSection, Meters, Region, Torus, WorldRegion};
use crate::real::{Real as _, ToFloat as _};
use crate::cross::vec::Vec3;
use crate::cross::vec::{Vec3, Vec3u};
use crate::sim::AbstractSim;
use indexmap::IndexMap;
use serde::{Serialize, Deserialize};
use std::ops::AddAssign;
// TODO: do we really need both Send and Sync?
pub trait AbstractMeasurement<S>: Send + Sync {
fn eval(&self, state: &S) -> String;
fn key_value(&self, state: &S) -> IndexMap<String, String>;
fn key_value(&self, state: &S) -> Vec<Measurement>;
}
pub fn as_dyn_measurements<S, M: AbstractMeasurement<S>>(meas: &[M]) -> Vec<&dyn AbstractMeasurement<S>> {
@@ -15,20 +15,104 @@ pub fn as_dyn_measurements<S, M: AbstractMeasurement<S>>(meas: &[M]) -> Vec<&dyn
}
/// create one IndexMap out of several measurements
pub fn eval_multiple_kv<S>(state: &S, meas: &[&dyn AbstractMeasurement<S>]) -> IndexMap<String, String> {
let mut r = IndexMap::new();
for m in meas {
let other = m.key_value(state);
r.extend(other.into_iter());
}
r
/// 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()
}
pub fn eval_to_vec<S>(state: &S, meas: &[&dyn AbstractMeasurement<S>]) -> Vec<Evaluated> {
eval_multiple_kv(state, meas).into_iter().map(|(k, v)| {
Evaluated::new(k, v)
}).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 {
@@ -46,7 +130,7 @@ enum SiScale {
impl SiScale {
fn for_value(v: f32) -> Self {
use SiScale::*;
match v {
match v.abs() {
v if v < 1e-12 => Unit,
v if v < 1e-9 => Pico,
v if v < 1e-6 => Nano,
@@ -99,7 +183,7 @@ impl SiScale {
/// 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 = si.scale() * v;
let scaled = v / si.scale();
format!("{:.2} {}{}", scaled, si.shortcode(), unit)
}
}
@@ -108,14 +192,11 @@ impl SiScale {
pub struct Time;
impl<S: AbstractSim> AbstractMeasurement<S> for Time {
fn eval(&self, state: &S) -> String {
format!("{} (step {})", SiScale::format_short(state.time(), "s"), state.step_no())
}
fn key_value(&self, state: &S) -> IndexMap<String, String> {
[
("step".to_string(), state.step_no().to_string()),
("time".to_string(), state.time().to_string()),
].into_iter().collect()
fn key_value(&self, state: &S) -> Vec<Measurement> {
vec![
Measurement::new_unitless("step", state.step_no()),
Measurement::new("time", state.time(), "s"),
]
}
}
@@ -123,49 +204,14 @@ impl<S: AbstractSim> AbstractMeasurement<S> for Time {
pub struct Meta;
impl<S: AbstractSim> AbstractMeasurement<S> for Meta {
fn eval(&self, state: &S) -> String {
format!("{}x{}x{} feat: {:.1e}m", state.width(), state.height(), state.depth(), state.feature_size())
}
fn key_value(&self, state: &S) -> IndexMap<String, String> {
[
("width".to_string(), state.width().to_string()),
("height".to_string(), state.height().to_string()),
("depth".to_string(), state.depth().to_string()),
("feature_size".to_string(), state.feature_size().to_string()),
].into_iter().collect()
fn key_value(&self, state: &S) -> Vec<Measurement> {
vec![
Measurement::new_unitless("dim", state.size().0),
Measurement::new("feature_size", state.feature_size(), "m"),
]
}
}
/// some measurement which has already been evaluated.
/// this is used particularly if we need to monomorphize a measurement (e.g. for serialization)
/// and know it won't be applied to a new/different state.
#[derive(Clone, Serialize, Deserialize)]
pub struct Evaluated(String, String);
impl Evaluated {
pub fn new<S1: Into<String>, S2: Into<String>>(key: S1, value: S2) -> Self {
Self(key.into(), value.into())
}
pub fn key(&self) -> &str {
&*self.0
}
pub fn value(&self) -> &str {
&*self.1
}
}
impl<S> AbstractMeasurement<S> for Evaluated {
fn eval(&self, _state: &S) -> String {
format!("{}: {}", self.0, self.1)
}
fn key_value(&self, _state: &S) -> IndexMap<String, String> {
[
(self.0.clone(), self.1.clone()),
].into_iter().collect()
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Volume {
name: String,
region: Box<dyn Region>,
@@ -187,20 +233,13 @@ impl Volume {
}
impl<S: AbstractSim> AbstractMeasurement<S> for Volume {
fn eval(&self, state: &S) -> String {
format!("Vol({}): {:.2e} um^3",
self.name,
self.data(state),
)
}
fn key_value(&self, state: &S) -> IndexMap<String, String> {
[
(format!("Vol({})", self.name), self.data(state).to_string()),
].into_iter().collect()
fn key_value(&self, state: &S) -> Vec<Measurement> {
vec![
Measurement::new(&format!("Vol({})", self.name), self.data(state), "um^3"),
]
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Current {
name: String,
region: Box<dyn Region>,
@@ -224,6 +263,24 @@ impl Current {
}
}
// 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>);
@@ -270,62 +327,78 @@ impl std::iter::Sum for FieldSamples<[FieldSample; 3]> {
}
impl<S: AbstractSim> AbstractMeasurement<S> for Current {
fn eval(&self, state: &S) -> String {
fn key_value(&self, state: &S) -> Vec<Measurement> {
let (mean_current_mag, mean_current_vec) = self.data(state);
format!("I/cell({}): {:.2e} {:.2e}",
self.name,
mean_current_mag,
mean_current_vec)
}
fn key_value(&self, state: &S) -> IndexMap<String, String> {
let (mean_current_mag, mean_current_vec) = self.data(state);
[
(format!("Imag/cell({})", self.name), mean_current_mag.to_string()),
(format!("I/cell({})", self.name), mean_current_vec.to_string()),
].into_iter().collect()
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 {
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 normal = self.region.axis();
let to_coord = *coord - *self.region.center();
let tangent = normal.cross(to_coord).norm();
let current = state.current(coord);
let directed_current = current.dot(tangent.cast());
FieldSample(1, directed_current.cast(), current.cast())
// - 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_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 = net_current.cast::<f32>() / cross_sections;
mean_cross_sectional_current
}
}
impl<S: AbstractSim> AbstractMeasurement<S> for CurrentLoop {
fn eval(&self, state: &S) -> String {
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);
format!("I({}): {:.2e}", self.name, cross_sectional_current)
}
fn key_value(&self, state: &S) -> IndexMap<String, String> {
let cross_sectional_current = self.data(state);
[
(format!("I({})", self.name), cross_sectional_current.to_string()),
].into_iter().collect()
vec![
Measurement::new(
&format!("I({})", self.name),
cross_sectional_current,
"A"
),
]
}
}
@@ -385,27 +458,17 @@ impl MagneticLoop {
}
impl<S: AbstractSim> AbstractMeasurement<S> for MagneticLoop {
fn eval(&self, state: &S) -> String {
fn key_value(&self, state: &S) -> Vec<Measurement> {
let (mean_directed_m, mean_directed_b, mean_directed_h) = self.data(state);
format!(
"M({}): {:.2e}; B({}): {:.2e}; H({}): {:.2e}",
self.name, mean_directed_m,
self.name, mean_directed_b,
self.name, mean_directed_h,
)
}
fn key_value(&self, state: &S) -> IndexMap<String, String> {
let (mean_directed_m, mean_directed_b, mean_directed_h) = self.data(state);
[
(format!("M({})", self.name), mean_directed_m.to_string()),
(format!("B({})", self.name), mean_directed_b.to_string()),
(format!("H({})", self.name), mean_directed_h.to_string()),
].into_iter().collect()
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
#[derive(Clone, Serialize, Deserialize)]
pub struct MagneticFlux {
name: String,
region: Box<dyn Region>,
@@ -430,20 +493,18 @@ impl MagneticFlux {
}
impl<S: AbstractSim> AbstractMeasurement<S> for MagneticFlux {
fn eval(&self, state: &S) -> String {
fn key_value(&self, state: &S) -> Vec<Measurement> {
let mean_mag = self.data(state);
format!("Bavg({}): {:.2e}", self.name, mean_mag)
}
fn key_value(&self, state: &S) -> IndexMap<String, String> {
let mean_mag = self.data(state);
[
(format!("Bavg({})", self.name), mean_mag.to_string()),
].into_iter().collect()
vec![
Measurement::new_unitless(
&format!("Bavg({})", self.name),
mean_mag,
)
]
}
}
/// mean B over a region
#[derive(Clone, Serialize, Deserialize)]
pub struct Magnetization {
name: String,
region: Box<dyn Region>,
@@ -468,15 +529,13 @@ impl Magnetization {
}
impl<S: AbstractSim> AbstractMeasurement<S> for Magnetization {
fn eval(&self, state: &S) -> String {
fn key_value(&self, state: &S) -> Vec<Measurement> {
let mean_mag = self.data(state);
format!("Mavg({}): {:.2e}", self.name, mean_mag)
}
fn key_value(&self, state: &S) -> IndexMap<String, String> {
let mean_mag = self.data(state);
[
(format!("Mavg({})", self.name), mean_mag.to_string()),
].into_iter().collect()
vec![
Measurement::new_unitless(
&format!("Mavg({})", self.name), mean_mag
),
]
}
}
@@ -489,15 +548,11 @@ fn loc(v: Meters) -> String {
pub struct MagnetizationAt(pub Meters);
impl<S: AbstractSim> AbstractMeasurement<S> for MagnetizationAt {
fn eval(&self, state: &S) -> String {
fn key_value(&self, state: &S) -> Vec<Measurement> {
let m = state.sample(self.0).m();
format!("M{}: {:.2e}", loc(self.0), m)
}
fn key_value(&self, state: &S) -> IndexMap<String, String> {
let m = state.sample(self.0).m();
[
(format!("M{}", loc(self.0)), m.to_string()),
].into_iter().collect()
vec![
Measurement::new_unitless(&format!("M{}", loc(self.0)), m.cast())
]
}
}
@@ -506,15 +561,13 @@ impl<S: AbstractSim> AbstractMeasurement<S> for MagnetizationAt {
pub struct MagneticFluxAt(pub Meters);
impl<S: AbstractSim> AbstractMeasurement<S> for MagneticFluxAt {
fn eval(&self, state: &S) -> String {
fn key_value(&self, state: &S) -> Vec<Measurement> {
let b = state.sample(self.0).b();
format!("B{}: {:.2e}", loc(self.0), b)
}
fn key_value(&self, state: &S) -> IndexMap<String, String> {
let b = state.sample(self.0).b();
[
(format!("B{}", loc(self.0)), b.to_string()),
].into_iter().collect()
vec![
Measurement::new_unitless(
&format!("B{}", loc(self.0)), b.cast()
)
]
}
}
@@ -523,15 +576,13 @@ impl<S: AbstractSim> AbstractMeasurement<S> for MagneticFluxAt {
pub struct MagneticStrengthAt(pub Meters);
impl<S: AbstractSim> AbstractMeasurement<S> for MagneticStrengthAt {
fn eval(&self, state: &S) -> String {
fn key_value(&self, state: &S) -> Vec<Measurement> {
let h = state.sample(self.0).h();
format!("H{}: {:.2e}", loc(self.0), h)
}
fn key_value(&self, state: &S) -> IndexMap<String, String> {
let h = state.sample(self.0).h();
[
(format!("H{}", loc(self.0)), h.to_string()),
].into_iter().collect()
vec![
Measurement::new_unitless(
&format!("H{}", loc(self.0)), h.cast()
)
]
}
}
@@ -539,19 +590,16 @@ impl<S: AbstractSim> AbstractMeasurement<S> for MagneticStrengthAt {
pub struct ElectricField(pub Meters);
impl<S: AbstractSim> AbstractMeasurement<S> for ElectricField {
fn eval(&self, state: &S) -> String {
fn key_value(&self, state: &S) -> Vec<Measurement> {
let e = state.sample(self.0).e();
format!("E{}: {}", loc(self.0), e)
}
fn key_value(&self, state: &S) -> IndexMap<String, String> {
let e = state.sample(self.0).e();
[
(format!("E{}", loc(self.0)), e.to_string()),
].into_iter().collect()
vec![
Measurement::new_unitless(
&format!("E{}", loc(self.0)), e.cast()
)
]
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Energy {
name: String,
region: Box<dyn Region>,
@@ -585,19 +633,16 @@ impl Energy {
}
impl<S: AbstractSim> AbstractMeasurement<S> for Energy {
fn eval(&self, state: &S) -> String {
fn key_value(&self, state: &S) -> Vec<Measurement> {
let e = self.data(state);
format!("U({}): {}", self.name, SiScale::format_short(e, "J"))
}
fn key_value(&self, state: &S) -> IndexMap<String, String> {
let e = self.data(state);
[
(format!("U({})", self.name), e.to_string()),
].into_iter().collect()
vec![
Measurement::new(
&format!("U({})", self.name), e, "J"
)
]
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Power {
name: String,
region: Box<dyn Region>
@@ -626,14 +671,188 @@ impl Power {
}
impl<S: AbstractSim> AbstractMeasurement<S> for Power {
fn eval(&self, state: &S) -> String {
fn key_value(&self, state: &S) -> Vec<Measurement> {
let power = self.data(state);
format!("P({}): {}", self.name, SiScale::format_short(power, "W"))
}
fn key_value(&self, state: &S) -> IndexMap<String, String> {
let power = self.data(state);
[
(format!("P({})", self.name), power.to_string()),
].into_iter().collect()
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);
}
}

View File

@@ -2,7 +2,7 @@ use crate::geom::Index;
use crate::real::ToFloat as _;
use crate::cross::vec::{Vec2, Vec3};
use crate::sim::{AbstractSim, GenericSim, Sample};
use crate::meas::{self, AbstractMeasurement};
use crate::meas::{self, AbstractMeasurement, Measurement};
use crossterm::{cursor, QueueableCommand as _};
use crossterm::style::{style, Color, PrintStyledContent, Stylize as _};
use font8x8::{BASIC_FONTS, GREEK_FONTS, UnicodeFonts as _};
@@ -294,8 +294,8 @@ impl<'a, S: AbstractSim> RenderSteps<'a, S> {
self.im = im;
}
fn render_measurements(&mut self) {
for (meas_no, m) in self.meas.iter().enumerate() {
let meas_string = m.eval(self.sim);
for (meas_no, m) in meas::eval_multiple(self.sim, &self.meas).into_iter().enumerate() {
let meas_string = m.pretty_print();
for (i, c) in meas_string.chars().enumerate() {
let glyph = BASIC_FONTS.get(c)
.or_else(|| GREEK_FONTS.get(c))
@@ -425,6 +425,7 @@ impl<S: AbstractSim> Renderer<S> for ColorTermRenderer {
measurements: &[&dyn AbstractMeasurement<S>],
config: RenderConfig,
) {
let measurements = meas::eval_multiple(state, measurements);
let (max_w, mut max_h) = crossterm::terminal::size().unwrap();
max_h = max_h.saturating_sub(2 + measurements.len() as u16);
let im = RenderSteps::render_configured(state, &[], z, (max_w as _, max_h as _), config);
@@ -453,7 +454,7 @@ impl<S: AbstractSim> Renderer<S> for ColorTermRenderer {
for m in measurements {
// Measurements can be slow to compute
stdout.flush().unwrap();
let meas_string = m.eval(state);
let meas_string = m.pretty_print();
stdout.queue(cursor::MoveDown(1)).unwrap();
stdout.queue(cursor::MoveToColumn(1)).unwrap();
stdout.queue(PrintStyledContent(style(meas_string))).unwrap();
@@ -592,7 +593,7 @@ pub struct SerializedFrame<S> {
pub state: S,
/// although not generally necessary to load the sim, saving the measurements is beneficial for
/// post-processing.
pub measurements: Vec<meas::Evaluated>,
pub measurements: Vec<Measurement>,
}
impl<S: AbstractSim> SerializedFrame<S> {
@@ -633,15 +634,18 @@ impl SerializerRenderer {
}
impl SerializerRenderer {
fn serialize<S: AbstractSim + Serialize>(&self, state: &S, measurements: Vec<meas::Evaluated>) {
fn serialize<S: AbstractSim + Serialize>(&self, state: &S, measurements: Vec<Measurement>) {
let frame = SerializedFrame {
state,
measurements,
};
let name = self.fmt_str.replace("{step_no}", &*frame.state.step_no().to_string());
let out = BufWriter::new(File::create(name).unwrap());
//serde_cbor::to_writer(out, &snap).unwrap();
// serialize to a temporary file -- in case we run out of disk space, etc.
let temp_name = format!("{}.incomplete", name);
let out = BufWriter::new(File::create(&temp_name).unwrap());
bincode::serialize_into(out, &frame).unwrap();
// atomically complete the write.
std::fs::rename(temp_name, name).unwrap();
}
pub fn try_load<S: AbstractSim + for <'a> Deserialize<'a>>(&self) -> Option<SerializedFrame<S>> {
@@ -656,9 +660,9 @@ impl<S: AbstractSim + Serialize> Renderer<S> for SerializerRenderer {
}
fn render(&self, state: &S, measurements: &[&dyn AbstractMeasurement<S>], _config: RenderConfig) {
if self.prefer_generic {
self.serialize(&state.to_generic(), meas::eval_to_vec(state, measurements));
self.serialize(&state.to_generic(), meas::eval_multiple(state, measurements));
} else {
self.serialize(state, meas::eval_to_vec(state, measurements));
self.serialize(state, meas::eval_multiple(state, measurements));
}
}
}
@@ -698,7 +702,7 @@ impl<S: AbstractSim> Renderer<S> for CsvRenderer {
default_render_z_slice(self, state, z, measurements, config)
}
fn render(&self, state: &S, measurements: &[&dyn AbstractMeasurement<S>], _config: RenderConfig) {
let row = meas::eval_multiple_kv(state, measurements);
let row = meas::eval_multiple(state, measurements);
let step = state.step_no();
let mut lock = self.state.lock().unwrap();
let mut writer = match lock.take().unwrap() {
@@ -732,13 +736,13 @@ impl<S: AbstractSim> Renderer<S> for CsvRenderer {
file.set_len(0).unwrap();
let mut writer = csv::Writer::from_writer(BufWriter::new(file));
// write the header
writer.write_record(row.keys()).unwrap();
writer.write_record(row.iter().map(|m| m.name())).unwrap();
writer
}
},
CsvState::Writing(writer) => writer,
};
writer.write_record(row.values()).unwrap();
writer.write_record(row.iter().map(|m| m.machine_readable())).unwrap();
writer.flush().unwrap();
*lock = Some(CsvState::Writing(writer));
}

View File

@@ -1,3 +1,4 @@
use crate::diagnostics::SyncDiagnostics;
use crate::geom::{Coord, Cube, Index, InvertedRegion, Region};
use crate::cross::mat::{FullyGenericMaterial, Material, Vacuum};
use crate::cross::real::Real;
@@ -209,6 +210,9 @@ pub trait AbstractSim: Sync {
/// Take a "snapshot" of the simulation, dropping all material-specific information.
fn to_static(&self) -> StaticSim;
fn to_generic(&self) -> GenericSim<Self::Real>;
fn use_diagnostics(&mut self, _diag: SyncDiagnostics) {
// optional
}
//--- HELPER METHODS below (derived) ---//
@@ -324,8 +328,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)

View File

@@ -2,6 +2,7 @@ use ndarray::{self, Array3};
use serde::{Deserialize, Serialize};
use log::{info, trace, warn};
use crate::diagnostics::SyncDiagnostics;
use crate::geom::{Coord, Index};
use crate::real::Real;
use crate::sim::{AbstractSim, Fields, GenericSim, StaticSim};
@@ -49,6 +50,8 @@ where M: 'static
step_no: u64,
#[serde(skip)]
backend: B,
#[serde(skip)]
diag: SyncDiagnostics,
}
// B isn't always clonable (e.g. gpu backend) so rust can't auto-derive this.
@@ -65,6 +68,7 @@ impl<R: Clone, M: Clone, B: Default> Clone for SpirvSim<R, M, B> {
// we require that the caller explicitly init the backend if they need this.
// TODO: this probably shouldn't be a `Clone` method, but like "read_only_clone()".
backend: Default::default(),
diag: self.diag.clone(),
}
}
}
@@ -131,6 +135,7 @@ where
mat,
step_no: self.step_no,
backend: Default::default(),
diag: self.diag.clone(),
}
}
@@ -144,8 +149,13 @@ where
mat,
step_no: self.step_no,
backend: Default::default(),
diag: self.diag.clone(),
}
}
fn use_diagnostics(&mut self, diag: SyncDiagnostics) {
self.diag = diag;
}
}
impl<R: Real, M: Default, B> SpirvSim<R, M, B>
@@ -174,6 +184,7 @@ impl<R: Real, M: Default, B> SpirvSim<R, M, B>
mat,
step_no: 0,
backend,
diag: SyncDiagnostics::new(),
}
}
}
@@ -217,27 +228,30 @@ where
-> (Array3<Vec3<R>>, Array3<Vec3<R>>)
{
trace!("eval_stimulus begin");
let dim = self.size();
let feature_size = self.feature_size();
let t_sec = self.time();
let timestep = self.meta.time_step;
let (e, h) = self.diag.instrument_stimuli(|| {
let dim = self.size();
let feature_size = self.feature_size();
let t_sec = self.time();
let timestep = self.meta.time_step;
// TODO(perf): do this in one loop!
let e = ndarray::Zip::from(ndarray::indices(
[dim.z() as usize, dim.y() as usize, dim.x() as usize]
)).par_map_collect(|(z, y, x)| {
let pos_idx = Index::new(x as _, y as _, z as _);
let pos_meters = pos_idx.to_meters(feature_size);
let densities = stim.at(t_sec, pos_meters);
densities.e.cast::<R>() * timestep
});
let h = ndarray::Zip::from(ndarray::indices(
[dim.z() as usize, dim.y() as usize, dim.x() as usize]
)).par_map_collect(|(z, y, x)| {
let pos_idx = Index::new(x as _, y as _, z as _);
let pos_meters = pos_idx.to_meters(feature_size);
let densities = stim.at(t_sec, pos_meters);
densities.h.cast::<R>() * timestep
// TODO(perf): do this in one loop!
let e = ndarray::Zip::from(ndarray::indices(
[dim.z() as usize, dim.y() as usize, dim.x() as usize]
)).par_map_collect(|(z, y, x)| {
let pos_idx = Index::new(x as _, y as _, z as _);
let pos_meters = pos_idx.to_meters(feature_size);
let densities = stim.at(t_sec, pos_meters);
densities.e.cast::<R>() * timestep
});
let h = ndarray::Zip::from(ndarray::indices(
[dim.z() as usize, dim.y() as usize, dim.x() as usize]
)).par_map_collect(|(z, y, x)| {
let pos_idx = Index::new(x as _, y as _, z as _);
let pos_meters = pos_idx.to_meters(feature_size);
let densities = stim.at(t_sec, pos_meters);
densities.h.cast::<R>() * timestep
});
(e, h)
});
trace!("eval_stimulus end");
(e, h)

View File

@@ -1,6 +1,6 @@
use crate::real::*;
use crate::real::Real as _;
use crate::cross::vec::Vec3;
use crate::geom::{Meters, Region};
use crate::geom::{HasCrossSection, Meters, Region};
use rand;
/// field densities
@@ -150,23 +150,19 @@ impl<R: Region + Sync, T: TimeVarying3 + Sync> AbstractStimulus for Stimulus<R,
pub struct CurlStimulus<R, T> {
region: R,
stim: T,
center: Meters,
axis: Meters,
}
impl<R, T> CurlStimulus<R, T> {
pub fn new(region: R, stim: T, center: Meters, axis: Meters) -> Self {
Self { region, stim, center, axis }
pub fn new(region: R, stim: T) -> Self {
Self { region, stim }
}
}
impl<R: Region + Sync, T: TimeVarying1 + Sync> AbstractStimulus for CurlStimulus<R, T> {
impl<R: Region + HasCrossSection, T: TimeVarying1 + Sync> AbstractStimulus for CurlStimulus<R, T> {
fn at(&self, t_sec: f32, pos: Meters) -> Fields {
if self.region.contains(pos) {
let FieldMags { e, h } = self.stim.at(t_sec);
let from_center_to_point = *pos - *self.center;
// TODO: is this inverted?
let rotational = from_center_to_point.cross(*self.axis);
let rotational = self.region.cross_section_normal(pos).norm();
let impulse_e = rotational.with_mag(e.cast()).unwrap_or_default();
let impulse_h = rotational.with_mag(h.cast()).unwrap_or_default();
Fields {
@@ -398,4 +394,60 @@ mod test {
assert_approx_eq!(s.at(0.00050), Vec3::zero(), Vec3::zero());
assert_approx_eq!(s.at(0.00075), Vec3::new(-10.0, -1.0, 100.0), Vec3::zero());
}
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 curl_stimulus_trivial() {
let region = MockRegion {
normal: Vec3::new(1.0, 0.0, 0.0)
};
// stim acts as e: 1.0, h: 0.0
let stim = CurlStimulus::new(region, 1.0);
assert_eq!(stim.at(0.0, Meters::new(0.0, 0.0, 0.0)), Fields {
e: Vec3::new(1.0, 0.0, 0.0),
h: Vec3::new(0.0, 0.0, 0.0),
});
}
#[test]
fn curl_stimulus_scales_right() {
let region = MockRegion {
// the magnitude of this normal shouldn't influence the curl.
// though, maybe it would be more useful if it *did*?
normal: Vec3::new(5.0, 0.0, 0.0)
};
// stim acts as e: -3.0, h: 0.0
let stim = CurlStimulus::new(region, -3.0);
assert_eq!(stim.at(0.0, Meters::new(0.0, 0.0, 0.0)), Fields {
e: Vec3::new(-3.0, 0.0, 0.0),
h: Vec3::new(0.0, 0.0, 0.0),
});
}
#[test]
fn curl_stimulus_multi_axis() {
let region = MockRegion {
normal: Vec3::new(0.0, -1.0, 1.0)
};
// stim acts as e: 2.0, h: 0.0
let stim = CurlStimulus::new(region, 2.0);
let Fields { e, h: _ } = stim.at(0.0, Meters::new(0.0, 0.0, 0.0));
assert!(e.distance(
Vec3::new(0.0, -1.0, 1.0).with_mag(2.0).unwrap()
) < 1e-6
);
}
}

View File

@@ -107,6 +107,10 @@ pub trait Real:
self == Self::zero()
}
fn inv(self) -> Self {
Self::one() / self
}
fn zero() -> Self;
fn one() -> Self;
fn two() -> Self;

View File

@@ -20,6 +20,17 @@ pub struct SimMeta<R> {
pub feature_size: R,
}
impl<R: Real> SimMeta<R> {
pub fn new(dim: Vec3u, feature_size: R, time_step: R) -> Self {
Self {
dim,
inv_feature_size: feature_size.inv(),
time_step,
feature_size
}
}
}
impl<R: Copy> SimMeta<R> {
pub fn dim(&self) -> Vec3u {
self.dim

View File

@@ -333,6 +333,8 @@ impl<R: Real> Vec3<R> {
Self::new(self.x().exp(), self.y().exp(), self.z().exp())
}
/// the only condition upon which this returns `None` is if the current magnitude is zero
/// and the new magnitude and NON-zero.
pub fn with_mag(&self, new_mag: R) -> Option<Self> {
if new_mag.is_zero() {
// avoid div-by-zero if self.mag() == 0 and new_mag == 0

View File

@@ -1,4 +1,5 @@
// use crate::{Loader, LoaderCache};
//! extracts Measurements from rendered .bc files and dumps them into a CSV
use coremem_post::{Loader, LoaderCache};
use std::path::PathBuf;
use structopt::StructOpt;
@@ -15,13 +16,13 @@ fn main() {
let mut frame = cache.load_first();
for meas in frame.measurements() {
print!("\"{}\",", meas.key());
print!("\"{}\",", meas.name());
}
println!("");
loop {
for meas in frame.measurements() {
print!("\"{}\",", meas.value());
print!("\"{}\",", meas.machine_readable().replace(",", "\\,"));
}
println!("");

View File

@@ -1,4 +1,5 @@
// use crate::Loader;
//! "decimates" the binary rendered form of a simulation
//! by deleting all but every N rendered files
use coremem_post::Loader;
use std::fs;
use std::path::PathBuf;

View File

@@ -1,3 +1,7 @@
//! interactive CLI viewer for .bc files.
//! navigate through the simulation over time and space,
//! and toggle views to see more detail over material, electric, or magnetic changes.
use coremem_post::{Loader, Viewer};
use std::path::PathBuf;
use std::io::Write as _;

View File

@@ -36,7 +36,7 @@ pub struct Frame {
}
impl Frame {
pub fn measurements(&self) -> &[meas::Evaluated] {
pub fn measurements(&self) -> &[meas::Measurement] {
&*self.data.measurements
}
pub fn sim(&self) -> &GenericSim<f32> {