diff --git a/Cargo.toml b/Cargo.toml index 860fc9c..5635c7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ edition = "2018" [dependencies] ansi_term = "0.12" decorum = "0.3" +dyn-clone = "1.0" enum_dispatch = "0.3" env_logger = "0.7" font8x8 = "0.2" @@ -18,7 +19,8 @@ lazy_static = "1.4" log = "0.4" ndarray = { version = "0.13", features = ["rayon"] } piecewise-linear = "0.1" -plotly = { version = "0.6", features = ["kaleido", "plotly_ndarray"] } +plotly = { version = "0.6", features = ["kaleido", "plotly_ndarray"], path = "../plotly/plotly" } +threadpool = "1.8" y4m = "0.7" [dev-dependencies] diff --git a/examples/toroid25d.rs b/examples/toroid25d.rs index e9e01b3..61308ec 100644 --- a/examples/toroid25d.rs +++ b/examples/toroid25d.rs @@ -31,23 +31,31 @@ fn main() { let depth_px = from_m(depth); let size_px = Index((width_px, width_px, depth_px).into()); let mut driver = Driver::new(size_px, feat_size); - // driver.set_steps_per_frame(8); + //driver.set_steps_per_frame(8); + //driver.set_steps_per_frame(20); //driver.set_steps_per_frame(40); //driver.set_steps_per_frame(80); - driver.set_steps_per_frame(120); + //driver.set_steps_per_frame(120); + driver.set_steps_per_frame(160); //driver.set_steps_per_frame(200); - // driver.add_y4m_renderer(&*format!("toroid25d.5-flt{}-{}-feat{}um-{:.1e}A-{:.1e}s--radii{}um-{}um-{}um-{}um.y4m", - // std::mem::size_of::() * 8, - // *size_px, - // m_to_um(feat_size), - // peak_current, - // current_duration, - // m_to_um(conductor_outer_rad), - // m_to_um(ferro_inner_rad), - // m_to_um(ferro_outer_rad), - // m_to_um(ferro_depth), - // )); - driver.add_plotly_renderer(); + let base = "toroid25d-7"; + let _ = std::fs::create_dir(base); + let prefix = format!("{}/{}-flt{}-{}-feat{}um-{}mA-{}ps--radii{}um-{}um-{}um-{}um", + base, + base, + std::mem::size_of::() * 8, + *size_px, + m_to_um(feat_size), + (peak_current * 1e3) as i64, + (current_duration * 1e12) as i64, + m_to_um(conductor_outer_rad), + m_to_um(ferro_inner_rad), + m_to_um(ferro_outer_rad), + m_to_um(ferro_depth), + ); + let _ = std::fs::create_dir(&prefix); + driver.add_y4m_renderer(&*format!("{}.y4m", prefix)); + driver.add_plotly_renderer(&*format!("{}/frame-", prefix)); let conductor_region = CylinderZ::new( Vec2::new(half_width, half_width), conductor_outer_rad); diff --git a/src/driver.rs b/src/driver.rs index 0b39569..86e42a9 100644 --- a/src/driver.rs +++ b/src/driver.rs @@ -8,32 +8,43 @@ use crate::stim::AbstractStimulus; use log::{info, debug, trace}; use std::path::PathBuf; -use std::time::{Duration, SystemTime}; +use std::sync::{Arc, Mutex}; +use std::sync::mpsc::{sync_channel, SyncSender, Receiver}; +use std::time::{Duration, Instant}; +use threadpool::ThreadPool; pub struct Driver { pub state: SimState, - renderer: MultiRenderer, + renderer: Arc, + render_pool: ThreadPool, + render_channel: (SyncSender<()>, Receiver<()>), steps_per_frame: u64, time_spent_stepping: Duration, time_spent_on_stimuli: Duration, - time_spent_rendering: Duration, + time_spent_blocked_on_render: Duration, + time_spent_rendering: Arc>, measurements: Vec>, stimuli: Vec>, - start_time: SystemTime, + start_time: Instant, + last_diag_time: Instant, } impl Driver { pub fn new(size: C, feature_size: Flt) -> Self { Driver { state: SimState::new(size.to_index(feature_size), feature_size), - renderer: Default::default(), + renderer: Arc::new(MultiRenderer::new()), + render_pool: ThreadPool::new(3), + render_channel: sync_channel(0), steps_per_frame: 1, time_spent_stepping: Default::default(), time_spent_on_stimuli: Default::default(), + time_spent_blocked_on_render: Default::default(), time_spent_rendering: Default::default(), measurements: vec![Box::new(meas::Time), Box::new(meas::Meta), Box::new(meas::Energy)], stimuli: vec![], - start_time: SystemTime::now(), + start_time: Instant::now(), + last_diag_time: Instant::now(), } } @@ -64,8 +75,8 @@ impl Driver { self.add_renderer(render::Y4MRenderer::new(output), &*name); } - pub fn add_plotly_renderer(&mut self) { - self.add_renderer(render::PlotlyRenderer, "plotly"); + pub fn add_plotly_renderer(&mut self, out_base: &str) { + self.add_renderer(render::PlotlyRenderer::new(out_base), out_base); } pub fn add_term_renderer(&mut self) { @@ -150,44 +161,62 @@ impl Driver { } } + fn render(&mut self) { + 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 || { + sender.send(()).unwrap(); + trace!("render begin"); + let start_time = Instant::now(); + renderer.render(&their_state, &*their_measurements); + *time_spent_rendering.lock().unwrap() += start_time.elapsed(); + trace!("render end"); + }); + let block_start = Instant::now(); + self.render_channel.1.recv().unwrap(); + self.time_spent_blocked_on_render += block_start.elapsed(); + } + pub fn step(&mut self) { if self.state.step_no() % self.steps_per_frame == 0 { - trace!("render begin"); - let start_time = SystemTime::now(); - self.renderer.render(&self.state, &*self.measurements); - self.time_spent_rendering += start_time.elapsed().unwrap(); - trace!("render end"); + self.render(); } { trace!("stimuli begin"); - let start_time = SystemTime::now(); + let start_time = Instant::now(); for stim in &mut *self.stimuli { stim.apply(&mut self.state); } - self.time_spent_on_stimuli += start_time.elapsed().unwrap(); + self.time_spent_on_stimuli += start_time.elapsed(); } trace!("step begin"); - let start_time = SystemTime::now(); + let start_time = Instant::now(); self.state.step(); - self.time_spent_stepping += start_time.elapsed().unwrap(); + self.time_spent_stepping += start_time.elapsed(); trace!("step end"); - let step = self.state.step_no(); - if step % (10*self.steps_per_frame) == 0 { + 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.as_secs_f64(); - let overall_time = self.start_time.elapsed().unwrap().as_secs_f64(); + let render_time = self.time_spent_rendering.lock().unwrap().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 sim_time = self.state.time() as f64; info!( - "t={:.2e} frame {:06} fps: {:6.2} (sim: {:.1}s, stim: {:.1}s, render: {:.1}s, other: {:.1}s)", + "t={:.2e} frame {:06} fps: {:6.2} (sim: {:.1}s, stim: {:.1}s, render: {:.1}s, blocked: {:.1}s, other: {:.1}s)", sim_time, step, fps, step_time, stim_time, render_time, + block_time, overall_time - step_time - stim_time - render_time ); } diff --git a/src/meas.rs b/src/meas.rs index 016d718..eebd581 100644 --- a/src/meas.rs +++ b/src/meas.rs @@ -2,13 +2,16 @@ use crate::flt::Flt; use crate::geom::{Meters, Region}; use crate::mat::Material as _; use crate::sim::{Cell, GenericSim}; +use dyn_clone::{self, DynClone}; use std::fmt::Display; use std::iter::Sum; -pub trait AbstractMeasurement { +pub trait AbstractMeasurement: Send + DynClone { fn eval(&self, state: &dyn GenericSim) -> String; } +dyn_clone::clone_trait_object!(AbstractMeasurement); +#[derive(Clone)] pub struct Time; impl AbstractMeasurement for Time { @@ -17,6 +20,7 @@ impl AbstractMeasurement for Time { } } +#[derive(Clone)] pub struct Meta; impl AbstractMeasurement for Meta { @@ -25,6 +29,7 @@ impl AbstractMeasurement for Meta { } } +#[derive(Clone)] pub struct Label(pub String); impl Label { @@ -50,9 +55,10 @@ fn sum_over_region, R: Region, F: Fn(Meters, &Cell) -> T>(st }) } +#[derive(Clone)] pub struct Current(pub R); -impl AbstractMeasurement for Current { +impl AbstractMeasurement for Current { fn eval(&self, state: &dyn GenericSim) -> String { let current = sum_over_region(state, &self.0, |coord, _cell| state.current(coord)); format!("I({}): ({:.2e}, {:.2e}, {:.2e})", self.0, current.x(), current.y(), current.z()) @@ -65,6 +71,7 @@ fn loc(v: Meters) -> String { } /// M +#[derive(Clone)] pub struct Magnetization(pub Meters); impl AbstractMeasurement for Magnetization { @@ -75,6 +82,7 @@ impl AbstractMeasurement for Magnetization { } /// B +#[derive(Clone)] pub struct MagneticFlux(pub Meters); impl AbstractMeasurement for MagneticFlux { @@ -85,6 +93,7 @@ impl AbstractMeasurement for MagneticFlux { } /// H +#[derive(Clone)] pub struct MagneticStrength(pub Meters); impl AbstractMeasurement for MagneticStrength { @@ -94,6 +103,7 @@ impl AbstractMeasurement for MagneticStrength { } } +#[derive(Clone)] pub struct ElectricField(pub Meters); impl AbstractMeasurement for ElectricField { @@ -103,6 +113,7 @@ impl AbstractMeasurement for ElectricField { } } +#[derive(Clone)] pub struct Energy; impl AbstractMeasurement for Energy { diff --git a/src/render.rs b/src/render.rs index 045f216..787c3a4 100644 --- a/src/render.rs +++ b/src/render.rs @@ -6,12 +6,12 @@ use crate::sim::{Cell, GenericSim}; use crate::meas::AbstractMeasurement; use font8x8::{BASIC_FONTS, GREEK_FONTS, UnicodeFonts as _}; use log::{trace, info}; -use plotly::{Plot, ImageFormat}; -use plotly::heat_map::HeatMap; +use plotly; use image::{RgbImage, Rgb}; use imageproc::{pixelops, drawing}; use std::fs::File; use std::path::PathBuf; +use std::sync::{Mutex, RwLock}; use y4m; /// Accept a value from (-\inf, \inf) and return a value in (-1, 1). @@ -54,6 +54,17 @@ fn scale_vector(x: Vec2, typical_mag: Flt) -> Vec2 { x.with_mag(new_mag) } +fn im_size(state: &dyn GenericSim, max_w: u32, max_h: u32) -> (u32, u32) { + let mut width = max_w; + let mut height = width * state.height() / state.width(); + if height > max_h { + let stretch = max_h as f32 / height as f32; + width = (width as f32 * stretch) as _; + height = max_h; + } + (width, height) +} + struct RenderSteps<'a> { im: RgbImage, sim: &'a dyn GenericSim, @@ -64,14 +75,7 @@ struct RenderSteps<'a> { impl<'a> RenderSteps<'a> { fn render(state: &'a dyn GenericSim, measurements: &'a [Box], z: u32) -> RgbImage { - let mut width = 640; - let max_height = 480; - let mut height = width * state.height() / state.width(); - if height > max_height { - let stretch = max_height as f32 / height as f32; - width = (width as f32 * stretch) as _; - height = max_height; - } + let (width, height) = im_size(state, 640, 480); trace!("rendering at {}x{} with z={}", width, height, z); let mut me = Self::new(state, measurements, width, height, z); me.render_scalar_field(10.0, false, 2, |cell| { @@ -230,11 +234,13 @@ impl ImageRenderExt for RgbImage { } } -pub trait Renderer { - fn render(&mut self, state: &dyn GenericSim, measurements: &[Box]) { +pub trait Renderer: Send + Sync { + fn render(&self, state: &dyn GenericSim, measurements: &[Box]) { self.render_with_image(state, &RenderSteps::render(state, measurements, state.depth() / 2)); } - fn render_with_image(&mut self, state: &dyn GenericSim, _im: &RgbImage) { + /// Not intended to be called directly by users; implement this if you want the image to be + /// computed using default settings and you just manage where to display/save it. + fn render_with_image(&self, state: &dyn GenericSim, _im: &RgbImage) { self.render(state, &[]); } } @@ -262,7 +268,7 @@ pub trait Renderer { pub struct ColorTermRenderer; impl Renderer for ColorTermRenderer { - fn render_with_image(&mut self, _state: &dyn GenericSim, im: &RgbImage) { + fn render_with_image(&self, _state: &dyn GenericSim, im: &RgbImage) { let square = "█"; let buf: String = im .enumerate_rows() @@ -279,27 +285,30 @@ impl Renderer for ColorTermRenderer { pub struct Y4MRenderer { out_path: PathBuf, - encoder: Option>, + encoder: Mutex>>, } impl Y4MRenderer { pub fn new>(output: S) -> Self { Self { out_path: output.into(), - encoder: None, + encoder: Mutex::new(None), } } } impl Renderer for Y4MRenderer { - fn render_with_image(&mut self, _state: &dyn GenericSim, im: &RgbImage) { - if self.encoder.is_none() { - let writer = File::create(&self.out_path).unwrap(); - self.encoder = Some(y4m::encode(im.width() as usize, im.height() as usize, y4m::Ratio::new(30, 1)) - .with_colorspace(y4m::Colorspace::C444) - .write_header(writer) - .unwrap() - ); + fn render_with_image(&self, _state: &dyn GenericSim, im: &RgbImage) { + { + let mut enc = self.encoder.lock().unwrap(); + if enc.is_none() { + let writer = File::create(&self.out_path).unwrap(); + *enc = Some(y4m::encode(im.width() as usize, im.height() as usize, y4m::Ratio::new(30, 1)) + .with_colorspace(y4m::Colorspace::C444) + .write_header(writer) + .unwrap() + ); + } } let mut pix_y = Vec::new(); @@ -318,7 +327,8 @@ impl Renderer for Y4MRenderer { } let frame = y4m::Frame::new([&*pix_y, &*pix_u, &*pix_v], None); - let enc = self.encoder.as_mut().unwrap(); + let mut lock = self.encoder.lock().unwrap(); + let enc = lock.as_mut().unwrap(); trace!("write_frame begin"); let ret = enc.write_frame(&frame).unwrap(); trace!("write_frame end"); @@ -326,44 +336,112 @@ impl Renderer for Y4MRenderer { } } -pub struct PlotlyRenderer; +pub struct PlotlyRenderer { + out_base: String, +} + +fn add_scatter(plot: &mut plotly::Plot, xv: &mut Vec, yv: &mut Vec, zv: &mut Vec, colors: &mut Vec) { + let xv = std::mem::replace(xv, Vec::new()); + let yv = std::mem::replace(yv, Vec::new()); + let zv = std::mem::replace(zv, Vec::new()); + let colors = std::mem::replace(colors, Vec::new()); + let scatter = plotly::Scatter::new3(xv, yv, zv) + .mode(plotly::common::Mode::Markers) + .marker(plotly::common::Marker::new() + .opacity(0.01) + //.size_array(sizes) + //.opacity_array(opacities) + .color_array(colors) + ); + plot.add_trace(scatter); +} + +impl PlotlyRenderer { + pub fn new(out_base: &str) -> Self { + Self { + out_base: out_base.into(), + } + } +} impl Renderer for PlotlyRenderer { - fn render(&mut self, state: &dyn GenericSim, measurements: &[Box]) { + fn render(&self, state: &dyn GenericSim, measurements: &[Box]) { + use plotly::{ImageFormat, Plot, Rgba, Scatter}; + use plotly::common::Marker; + use plotly::layout::{AspectMode, Axis, Layout, LayoutScene}; let mut plot = Plot::new(); + let scene = LayoutScene::new() + .x_axis(Axis::new().range(vec![0, state.width() as i32])) + .y_axis(Axis::new().range(vec![0, state.height() as i32])) + .z_axis(Axis::new().range(vec![0, state.depth() as i32])) + .aspect_mode(AspectMode::Cube); + let layout = Layout::new() + .scene(scene); + plot.set_layout(layout); + let mut xv = Vec::new(); let mut yv = Vec::new(); let mut zv = Vec::new(); + // let mut opacities = Vec::new(); + let mut colors = Vec::new(); for z in 0..state.depth() { + if xv.len() >= 120000 { + add_scatter(&mut plot, &mut xv, &mut yv, &mut zv, &mut colors); + } for y in 0..state.height() { for x in 0..state.width() { + // if x%5 == 0 || y%5 == 0 || z%5 == 0 { + // continue; + // } let cell = state.get(Index(Vec3u::new(x, y, z))); - if cell.e().mag() > 10.0 { - xv.push(x); - yv.push(y); - zv.push(z); - } + xv.push(x); + yv.push(y); + zv.push(z); + // opacities.push((cell.e().mag() * 0.1).min(1.0) as f64) + let mat = cell.mat().conductivity().mag() + if cell.mat().is_vacuum() { + 0.0 + } else { + 5.0 + }; + //let g = scale_unsigned_to_u8(mat, 10.0); + //let r = scale_unsigned_to_u8(cell.mat().m().mag(), 100.0); + //let b = scale_unsigned_to_u8(cell.e().mag(), 1e2); + let r = scale_unsigned_to_u8(cell.mat().m().mag(), 100.0); + let g = scale_unsigned_to_u8(cell.e().mag(), 1e2); + let b = scale_unsigned_to_u8(mat, 10.0); + let alpha = 1.0; + colors.push(Rgba::new(r, g, b, alpha)); } } + // let scatter = plotly::Scatter::new3(xv, yv, zv) + // .mode(plotly::common::Mode::Markers) + // .marker(plotly::common::Marker::new() + // //.opacity(0.2) + // //.size_array(sizes) + // //.opacity_array(opacities) + // .color_array(colors) + // ); + // plot.add_trace(scatter); } - let heat_map = HeatMap::new(xv, yv, zv); - plot.add_trace(heat_map); - let name = format!("frame{}", state.step_no()); - plot.save(&*name, ImageFormat::PNG, state.width() as _, state.height() as _, 1.0); + add_scatter(&mut plot, &mut xv, &mut yv, &mut zv, &mut colors); + + let name = format!("{}{}", self.out_base, state.step_no()); + let (im_w, im_h) = im_size(state, 2048, 2048); + plot.save(&*name, ImageFormat::PNG, im_w as _, im_h as _, 1.0); } } #[derive(Default)] pub struct MultiRenderer { - renderers: Vec>, + renderers: RwLock>>, } impl MultiRenderer { pub fn new() -> Self { Default::default() } - pub fn push(&mut self, r: R) { - self.renderers.push(Box::new(r)); + pub fn push(&self, r: R) { + self.renderers.write().unwrap().push(Box::new(r)); } pub fn with(mut self, r: R) -> Self { self.push(r); @@ -372,14 +450,14 @@ impl MultiRenderer { } impl Renderer for MultiRenderer { - fn render(&mut self, state: &dyn GenericSim, measurements: &[Box]) { - if self.renderers.len() != 0 { + fn render(&self, state: &dyn GenericSim, measurements: &[Box]) { + if self.renderers.read().unwrap().len() != 0 { self.render_with_image(state, &RenderSteps::render(state, measurements, state.depth() / 2)); } } - fn render_with_image(&mut self, state: &dyn GenericSim, im: &RgbImage) { - for r in &mut self.renderers { + fn render_with_image(&self, state: &dyn GenericSim, im: &RgbImage) { + for r in &*self.renderers.read().unwrap() { r.render_with_image(state, im); } } diff --git a/src/sim.rs b/src/sim.rs index 33a81ae..4909442 100644 --- a/src/sim.rs +++ b/src/sim.rs @@ -1,6 +1,7 @@ use crate::{flt::{Flt, Real}, consts}; use crate::geom::{Coord, Index, Meters, Vec3, Vec3u}; use crate::mat::{self, GenericMaterial, Material}; +use dyn_clone::{self, DynClone}; use log::trace; use ndarray::{Array3, Zip}; @@ -8,7 +9,7 @@ use ndarray::parallel::prelude::*; use std::convert::From; use std::iter::Sum; -pub trait GenericSim { +pub trait GenericSim: Send + Sync + DynClone { fn sample(&self, pos: Meters) -> Cell; fn impulse_e_meters(&mut self, pos: Meters, amount: Vec3); fn impulse_h_meters(&mut self, pos: Meters, amount: Vec3); @@ -29,7 +30,20 @@ pub trait GenericSim { fn time(&self) -> Flt { self.timestep() * self.step_no() as Flt } + + /// Take a "snapshot" of the simulation, dropping all material-specific information. + fn to_static(&self) -> SimState { + let mut state = SimState::new(self.size(), self.feature_size()); + Zip::from(ndarray::indices_of(&state.cells)).par_apply_assign_into( + &mut state.cells, + |(z, y, x)| { + let idx = Index((x as u32, y as u32, z as u32).into()); + self.sample(idx.to_meters(self.feature_size())) + }); + state + } } +dyn_clone::clone_trait_object!(GenericSim); impl<'a> dyn GenericSim + 'a { pub fn get(&self, at: C) -> Cell { @@ -82,7 +96,7 @@ impl<'a> dyn GenericSim + 'a { } } -#[derive(Default)] +#[derive(Default, Clone)] pub struct SimState { cells: Array3>, scratch: Array3>, @@ -101,7 +115,7 @@ impl SimState { } } -impl SimState { +impl SimState { pub fn step(&mut self) { use consts::real::*; let time_step = Real::from_inner(self.timestep()); @@ -134,7 +148,7 @@ impl SimState { } } -impl GenericSim for SimState { +impl GenericSim for SimState { fn sample(&self, pos: Meters) -> Cell { // TODO: smarter sampling than nearest neighbor? let pos_sim = pos.to_index(self.feature_size());