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.
750 lines
27 KiB
Rust
750 lines
27 KiB
Rust
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, Measurement};
|
|
use crossterm::{cursor, QueueableCommand as _};
|
|
use crossterm::style::{style, Color, PrintStyledContent, Stylize as _};
|
|
use font8x8::{BASIC_FONTS, GREEK_FONTS, UnicodeFonts as _};
|
|
use log::trace;
|
|
use num::integer::Integer; // TODO: remove?
|
|
use image::{RgbImage, Rgb};
|
|
use imageproc::{pixelops, drawing};
|
|
use rayon::prelude::*;
|
|
use serde::{Serialize, Deserialize};
|
|
use std::collections::hash_map::DefaultHasher;
|
|
use std::hash::Hasher;
|
|
use std::fs::{File, OpenOptions};
|
|
use std::io::{BufReader, BufWriter, Seek as _, SeekFrom, Write as _};
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::{Mutex, RwLock};
|
|
use y4m;
|
|
|
|
/// Accept a value from (-\inf, \inf) and return a value in (-1, 1).
|
|
/// If the input is equal to `typical`, it will be mapped to 0.5.
|
|
/// If the input is equal to -`typical`, it will be mapped to -0.5.
|
|
fn scale_signed(x: f32, typical: f32) -> f32 {
|
|
if x >= 0.0 {
|
|
scale_unsigned(x, typical)
|
|
} else {
|
|
-scale_unsigned(-x, typical)
|
|
}
|
|
}
|
|
|
|
/// Accept a value from [0, \inf) and return a value in [0, 1).
|
|
/// If the input is equal to `typical`, it will be mapped to 0.5.
|
|
fn scale_unsigned(x: f32, typical: f32) -> f32 {
|
|
// f(0) => 0
|
|
// f(1) => 0.5
|
|
// f(\inf) => 1
|
|
// f(x) = 1 - 1/(x+1)
|
|
1.0 - 1.0/(x/typical + 1.0)
|
|
}
|
|
|
|
fn scale_signed_to_u8(x: f32, typ: f32) -> u8 {
|
|
let norm = 128.0 + 128.0*scale_signed(x, typ);
|
|
norm as _
|
|
}
|
|
|
|
fn scale_unsigned_to_u8(x: f32, typ: f32) -> u8 {
|
|
let norm = 256.0*scale_unsigned(x, typ);
|
|
norm as _
|
|
}
|
|
|
|
/// Scale a vector to have magnitude between [0, 1).
|
|
fn scale_vector(x: Vec2<f32>, typical_mag: f32) -> Vec2<f32> {
|
|
let new_mag = scale_unsigned(x.mag(), typical_mag);
|
|
x.with_mag(new_mag).unwrap_or_default()
|
|
}
|
|
|
|
fn im_size<S: AbstractSim>(state: &S, 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).round() as _;
|
|
height = max_h;
|
|
}
|
|
(width, height)
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
|
pub enum FieldDisplayMode {
|
|
BzExy,
|
|
EzBxy,
|
|
BCurrent,
|
|
M,
|
|
Material,
|
|
}
|
|
|
|
impl FieldDisplayMode {
|
|
pub fn next(self) -> Self {
|
|
use FieldDisplayMode::*;
|
|
match self {
|
|
BzExy => EzBxy,
|
|
EzBxy => BCurrent,
|
|
BCurrent => M,
|
|
M => Material,
|
|
Material => BzExy,
|
|
}
|
|
}
|
|
|
|
pub fn prev(self) -> Self {
|
|
use FieldDisplayMode::*;
|
|
match self {
|
|
BzExy => Material,
|
|
EzBxy => BzExy,
|
|
BCurrent => EzBxy,
|
|
M => BCurrent,
|
|
Material => M,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, PartialEq)]
|
|
pub struct RenderConfig {
|
|
mode: FieldDisplayMode,
|
|
scale: f32,
|
|
}
|
|
|
|
impl Default for RenderConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
mode: FieldDisplayMode::BzExy,
|
|
scale: 1.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl RenderConfig {
|
|
pub fn next_mode(&mut self) {
|
|
self.mode = self.mode.next();
|
|
}
|
|
pub fn prev_mode(&mut self) {
|
|
self.mode = self.mode.prev();
|
|
}
|
|
pub fn increase_range(&mut self) {
|
|
self.scale *= 2.0;
|
|
}
|
|
pub fn decrease_range(&mut self) {
|
|
self.scale *= 0.5;
|
|
}
|
|
}
|
|
|
|
struct RenderSteps<'a, S> {
|
|
im: RgbImage,
|
|
sim: &'a S,
|
|
meas: &'a [&'a dyn AbstractMeasurement<S>],
|
|
/// Simulation z coordinate to sample
|
|
z: u32,
|
|
}
|
|
|
|
impl<'a, S: AbstractSim> RenderSteps<'a, S> {
|
|
// TODO: this could probably be a single measurement, and we just let collections of
|
|
// measurements also behave as measurements
|
|
/// Render using default configuration constants
|
|
fn render(state: &'a S, measurements: &'a [&'a dyn AbstractMeasurement<S>], z: u32) -> RgbImage {
|
|
Self::render_configured(state, measurements, z, (640, 480), RenderConfig::default())
|
|
}
|
|
/// Render, controlling things like the size.
|
|
fn render_configured(
|
|
state: &'a S,
|
|
measurements: &'a [&'a dyn AbstractMeasurement<S>],
|
|
z: u32,
|
|
max_size: (u32, u32),
|
|
config: RenderConfig,
|
|
) -> RgbImage {
|
|
let (width, height) = im_size(state, max_size.0, max_size.1);
|
|
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| {
|
|
let is_vacuum = cell.conductivity() != Vec3::zero() || cell.m() != Vec3::zero();
|
|
cell.conductivity().mag().to_f32() + if is_vacuum {
|
|
0.0
|
|
} else {
|
|
5.0
|
|
}
|
|
});
|
|
match config.mode {
|
|
FieldDisplayMode::BzExy => {
|
|
me.render_b_z_field(config.scale);
|
|
me.render_e_xy_field(config.scale);
|
|
},
|
|
FieldDisplayMode::EzBxy => {
|
|
me.render_e_z_field(config.scale);
|
|
me.render_b_xy_field(config.scale);
|
|
},
|
|
FieldDisplayMode::BCurrent => {
|
|
me.render_b(config.scale);
|
|
me.render_current(config.scale);
|
|
}
|
|
FieldDisplayMode::M => {
|
|
me.render_m(config.scale);
|
|
}
|
|
FieldDisplayMode::Material => {
|
|
me.render_mat(config.scale);
|
|
}
|
|
}
|
|
me.render_measurements();
|
|
me.im
|
|
}
|
|
fn new(sim: &'a S, meas: &'a [&'a dyn AbstractMeasurement<S>], width: u32, height: u32, z: u32) -> Self {
|
|
RenderSteps {
|
|
im: RgbImage::new(width, height),
|
|
sim,
|
|
meas,
|
|
z
|
|
}
|
|
}
|
|
|
|
fn get_at_px<'b>(&'b self, x_px: u32, y_px: u32) -> Sample<'b, S::Real, S::Material> {
|
|
let x_idx = x_px * self.sim.width() / self.im.width();
|
|
let y_idx = y_px * self.sim.height() / self.im.height();
|
|
self.sim.sample(Index::new(x_idx, y_idx, self.z))
|
|
}
|
|
|
|
////////////// Ex/Ey/Bz configuration ////////////
|
|
fn render_b_z_field(&mut self, scale: f32) {
|
|
self.render_scalar_field(1.0e-4 * scale, true, 1, |cell| cell.b().z().to_f32());
|
|
}
|
|
fn render_e_xy_field(&mut self, scale: f32) {
|
|
self.render_vector_field(Rgb([0xff, 0xff, 0xff]), 100.0 * scale, |cell| cell.e().xy().to_f32());
|
|
// current
|
|
self.render_vector_field(Rgb([0x00, 0xa0, 0x30]), 1.0e-12 * scale, |cell| {
|
|
cell.e().elem_mul(cell.conductivity()).xy().to_f32()
|
|
});
|
|
}
|
|
////////////// Magnitude configuration /////////////
|
|
fn render_b(&mut self, scale: f32) {
|
|
self.render_scalar_field(1.0e-3 * scale, false, 1, |cell| cell.b().mag().to_f32());
|
|
}
|
|
fn render_current(&mut self, scale: f32) {
|
|
self.render_scalar_field(1.0e1 * scale, false, 0, |cell| {
|
|
cell.e().elem_mul(cell.conductivity()).mag().to_f32()
|
|
});
|
|
}
|
|
|
|
////////////// Bx/By/Ez configuration ////////////
|
|
fn render_e_z_field(&mut self, scale: f32) {
|
|
self.render_scalar_field(1e4 * scale, true, 1, |cell| cell.e().z().to_f32());
|
|
}
|
|
fn render_b_xy_field(&mut self, scale: f32) {
|
|
self.render_vector_field(Rgb([0xff, 0xff, 0xff]), 1.0e-9 * scale, |cell| cell.b().xy().to_f32());
|
|
}
|
|
|
|
fn render_m(&mut self, scale: f32) {
|
|
self.render_scalar_field(1.0e5 * scale, false, 1, |cell| cell.m().mag().to_f32());
|
|
self.render_vector_field(Rgb([0xff, 0xff, 0xff]), 1.0e5 * scale, |cell| cell.m().xy().to_f32());
|
|
}
|
|
|
|
fn render_mat(&mut self, scale: f32) {
|
|
unsafe fn to_bytes<T>(d: &T) -> &[u8] {
|
|
std::slice::from_raw_parts(d as *const T as *const u8, std::mem::size_of::<T>())
|
|
}
|
|
self.render_scalar_field(scale, false, 1, |cell| {
|
|
let mut hasher = DefaultHasher::new();
|
|
let as_bytes = unsafe { to_bytes(cell.material()) };
|
|
std::hash::Hash::hash_slice(as_bytes, &mut hasher);
|
|
hasher.finish() as f32 / (-1i64 as u64 as f32)
|
|
});
|
|
}
|
|
|
|
fn render_vector_field<F>(&mut self, color: Rgb<u8>, typical: f32, measure: F)
|
|
where
|
|
F: Fn(&Sample<'_, S::Real, S::Material>) -> Vec2<f32>
|
|
{
|
|
let w = self.im.width();
|
|
let h = self.im.height();
|
|
let vec_spacing = 10;
|
|
for y in (0..h).into_iter().step_by(vec_spacing as _) {
|
|
for x in (0..w).into_iter().step_by(vec_spacing as _) {
|
|
let vec = self.field_vector(x, y, vec_spacing, &measure);
|
|
let norm_vec = scale_vector(vec, typical);
|
|
let alpha = 0.7*scale_unsigned(vec.mag_sq(), typical * 5.0);
|
|
let vec = norm_vec * (vec_spacing as f32);
|
|
let center = Vec2::new(x as f32, y as f32) + Vec2::new(vec_spacing as f32, vec_spacing as f32)*0.5;
|
|
self.im.draw_field_arrow(center, vec, color, alpha as f32);
|
|
}
|
|
}
|
|
}
|
|
fn render_scalar_field<F>(&mut self, typical: f32, signed: bool, slot: u32, measure: F)
|
|
where
|
|
F: Fn(&Sample<'_, S::Real, S::Material>) -> f32 + Sync
|
|
{
|
|
// XXX: get_at_px borrows self, so we need to clone the image to operate on it mutably.
|
|
let mut im = self.im.clone();
|
|
let w = im.width();
|
|
let h = im.height();
|
|
let samples = im.as_flat_samples_mut();
|
|
assert_eq!(samples.layout.channel_stride, 1);
|
|
assert_eq!(samples.layout.width_stride, 3);
|
|
assert_eq!(samples.layout.height_stride, 3*w as usize);
|
|
let pixel_buf: &mut [[u8; 3]] = unsafe { std::mem::transmute(samples.samples) };
|
|
pixel_buf[..(w*h) as usize].par_iter_mut().enumerate().for_each(|(idx, px)| {
|
|
let (y, x) = (idx as u32).div_rem(&w);
|
|
let cell = self.get_at_px(x, y);
|
|
let value = measure(&cell);
|
|
let scaled = if signed {
|
|
scale_signed_to_u8(value, typical)
|
|
} else {
|
|
scale_unsigned_to_u8(value, typical)
|
|
};
|
|
px[slot as usize] = scaled;
|
|
});
|
|
self.im = im;
|
|
}
|
|
fn render_measurements(&mut self) {
|
|
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))
|
|
.unwrap_or_else(|| BASIC_FONTS.get('?').unwrap());
|
|
for (y, bmp) in glyph.iter().enumerate() {
|
|
for x in 0..8 {
|
|
if (bmp & 1 << x) != 0 {
|
|
let real_x = 2 + i as u32*8 + x;
|
|
if let Some(real_y) = (y as u32 + self.im.height()).checked_sub(10 + meas_no as u32 * 8) {
|
|
if real_x < self.im.width() {
|
|
self.im.put_pixel(real_x, real_y, Rgb([0, 0, 0]));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn field_vector<F>(&self, xidx: u32, yidx: u32, size: u32, measure: &F) -> Vec2<f32>
|
|
where
|
|
F: Fn(&Sample<'_, S::Real, S::Material>) -> Vec2<f32>
|
|
{
|
|
let mut field = Vec2::default();
|
|
let w = self.im.width();
|
|
let h = self.im.height();
|
|
let xstart = xidx.min(w);
|
|
let ystart = yidx.min(h);
|
|
let xend = (xstart + size).min(w);
|
|
let yend = (ystart + size).min(h);
|
|
for y in ystart..yend {
|
|
for x in xstart..xend {
|
|
field += measure(&self.get_at_px(x, y));
|
|
}
|
|
}
|
|
let xw = xend - xstart;
|
|
let yw = yend - ystart;
|
|
if xw == 0 || yw == 0 {
|
|
// avoid division by zero
|
|
Vec2::new(0.0, 0.0)
|
|
} else {
|
|
field * (1.0 / ((xw*yw) as f32))
|
|
}
|
|
}
|
|
}
|
|
|
|
trait ImageRenderExt {
|
|
fn draw_field_arrow(&mut self, center: Vec2<f32>, rel: Vec2<f32>, color: Rgb<u8>, alpha: f32);
|
|
}
|
|
|
|
impl ImageRenderExt for RgbImage {
|
|
fn draw_field_arrow(&mut self, center: Vec2<f32>, rel: Vec2<f32>, color: Rgb<u8>, alpha: f32) {
|
|
let start = (center - rel * 0.5).round();
|
|
let end = (center + rel * 0.5).round();
|
|
let i_start = (start.x().round() as _, start.y().round() as _);
|
|
let i_end = (end.x().round() as _, end.y().round() as _);
|
|
let interpolate_with_alpha = |left, right, left_weight| {
|
|
pixelops::interpolate(left, right, left_weight*alpha)
|
|
};
|
|
drawing::draw_antialiased_line_segment_mut(self, i_start, i_end, color, interpolate_with_alpha);
|
|
if i_start != i_end
|
|
&& (0..self.width() as i32).contains(&i_end.0)
|
|
&& (0..self.height() as i32).contains(&i_end.1)
|
|
{
|
|
self.put_pixel(i_end.0 as _, i_end.1 as _, Rgb([0xff, 0, 0]));
|
|
}
|
|
//drawing::draw_line_segment_mut(self, i_start, i_end, color);
|
|
}
|
|
}
|
|
|
|
pub trait Renderer<S>: Send + Sync {
|
|
fn render_z_slice(&self, state: &S, z: u32, measurements: &[&dyn AbstractMeasurement<S>], config: RenderConfig);
|
|
// {
|
|
// self.render_with_image(state, &RenderSteps::render(state, measurements, z), measurements);
|
|
// }
|
|
fn render(&self, state: &S, measurements: &[&dyn AbstractMeasurement<S>], config: RenderConfig);
|
|
/// 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: &S, _im: &RgbImage, measurements: &[&dyn AbstractMeasurement<S>], config: RenderConfig) {
|
|
self.render(state, measurements, config);
|
|
}
|
|
}
|
|
|
|
fn default_render_z_slice<S: AbstractSim, R: Renderer<S>>(
|
|
me: &R, state: &S, z: u32, measurements: &[&dyn AbstractMeasurement<S>], config: RenderConfig,
|
|
) {
|
|
me.render_with_image(state, &RenderSteps::render(state, measurements, z), measurements, config);
|
|
}
|
|
fn default_render<S: AbstractSim, R: Renderer<S>>(
|
|
me: &R, state: &S, measurements: &[&dyn AbstractMeasurement<S>], config: RenderConfig
|
|
) {
|
|
me.render_z_slice(state, state.depth() / 2, measurements, config);
|
|
}
|
|
|
|
// pub struct NumericTermRenderer;
|
|
//
|
|
// impl Renderer for NumericTermRenderer {
|
|
// fn render(&mut self, state: &SimSnapshot, _measurements: &[&dyn AbstractMeasurement<S>]) {
|
|
// for y in 0..state.height() {
|
|
// for x in 0..state.width() {
|
|
// let cell = state.get((x, y).into());
|
|
// print!(" {:>10.1e}", cell.ex());
|
|
// }
|
|
// print!("\n");
|
|
// for x in 0..state.width() {
|
|
// let cell = state.get((x, y).into());
|
|
// print!("{:>10.1e} {:>10.1e}", cell.ey(), cell.bz());
|
|
// }
|
|
// print!("\n");
|
|
// }
|
|
// print!("\n");
|
|
// }
|
|
// }
|
|
|
|
#[derive(Default)]
|
|
pub struct ColorTermRenderer;
|
|
|
|
impl<S: AbstractSim> Renderer<S> for ColorTermRenderer {
|
|
fn render(&self, state: &S, measurements: &[&dyn AbstractMeasurement<S>], config: RenderConfig) {
|
|
default_render(self, state, measurements, config)
|
|
}
|
|
fn render_z_slice(
|
|
&self,
|
|
state: &S,
|
|
z: u32,
|
|
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);
|
|
let mut stdout = std::io::stdout();
|
|
|
|
// TODO: consider clearing line-by-line for less tearing?
|
|
stdout.queue(crossterm::terminal::Clear(crossterm::terminal::ClearType::All)).unwrap();
|
|
stdout.queue(cursor::MoveTo(0, 0)).unwrap();
|
|
|
|
for row in im.rows() {
|
|
for p in row {
|
|
stdout.queue(PrintStyledContent(style(" ").on(Color::Rgb {
|
|
r: p.0[0],
|
|
g: p.0[1],
|
|
b: p.0[2],
|
|
}))).unwrap();
|
|
}
|
|
stdout.queue(cursor::MoveDown(1)).unwrap();
|
|
stdout.queue(cursor::MoveToColumn(0)).unwrap();
|
|
}
|
|
|
|
stdout.queue(PrintStyledContent(style(format!("fields: {:?} scale: {}", config.mode, config.scale)))).unwrap();
|
|
stdout.queue(cursor::MoveDown(1)).unwrap();
|
|
stdout.queue(cursor::MoveToColumn(1)).unwrap();
|
|
stdout.queue(PrintStyledContent(style(format!("z: {}", z)))).unwrap();
|
|
for m in measurements {
|
|
// Measurements can be slow to compute
|
|
stdout.flush().unwrap();
|
|
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();
|
|
}
|
|
stdout.flush().unwrap();
|
|
}
|
|
}
|
|
|
|
pub struct Y4MRenderer {
|
|
out_path: PathBuf,
|
|
encoder: Mutex<Option<y4m::Encoder<File>>>,
|
|
}
|
|
|
|
impl Y4MRenderer {
|
|
pub fn new<P: Into<PathBuf>>(output: P) -> Self {
|
|
Self {
|
|
out_path: output.into(),
|
|
encoder: Mutex::new(None),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<S: AbstractSim> Renderer<S> for Y4MRenderer {
|
|
fn render_z_slice(&self, state: &S, z: u32, measurements: &[&dyn AbstractMeasurement<S>], config: RenderConfig) {
|
|
default_render_z_slice(self, state, z, measurements, config)
|
|
}
|
|
fn render(&self, state: &S, measurements: &[&dyn AbstractMeasurement<S>], config: RenderConfig) {
|
|
default_render(self, state, measurements, config)
|
|
}
|
|
fn render_with_image(&self, _state: &S, im: &RgbImage, _meas: &[&dyn AbstractMeasurement<S>], _config: RenderConfig) {
|
|
{
|
|
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();
|
|
let mut pix_u = Vec::new();
|
|
let mut pix_v = Vec::new();
|
|
for &Rgb([r, g, b]) in im.pixels() {
|
|
let r = (r as f32) / (256.0);
|
|
let g = (g as f32) / (256.0);
|
|
let b = (b as f32) / (256.0);
|
|
let y = (0.299 * r) + (0.587 * g) + (0.114 * b);
|
|
let cb = 0.5 - (0.168_736 * r) - (0.331_264 * g) + (0.5 * b);
|
|
let cr = 0.5 + (0.5 * r) - (0.418_688 * g) - (0.081_312 * b);
|
|
pix_y.push((y * 256.0) as _);
|
|
pix_u.push((cb * 256.0) as _);
|
|
pix_v.push((cr * 256.0) as _);
|
|
}
|
|
|
|
let frame = y4m::Frame::new([&*pix_y, &*pix_u, &*pix_v], None);
|
|
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");
|
|
ret
|
|
}
|
|
}
|
|
|
|
struct MultiRendererElement<S> {
|
|
step_frequency: u64,
|
|
step_limit: Option<u64>,
|
|
renderer: Box<dyn Renderer<S>>,
|
|
}
|
|
|
|
impl<S> MultiRendererElement<S> {
|
|
fn work_this_frame(&self, frame: u64) -> bool {
|
|
frame % self.step_frequency == 0 && match self.step_limit {
|
|
None => true,
|
|
Some(end) => frame < end,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct MultiRenderer<S> {
|
|
renderers: RwLock<Vec<MultiRendererElement<S>>>,
|
|
}
|
|
|
|
impl<S> Default for MultiRenderer<S> {
|
|
fn default() -> Self {
|
|
Self {
|
|
renderers: RwLock::new(Vec::new())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<S> MultiRenderer<S> {
|
|
pub fn new() -> Self {
|
|
Default::default()
|
|
}
|
|
pub fn push<R: Renderer<S> + 'static>(&self, renderer: R, step_frequency: u64, step_limit: Option<u64>) {
|
|
self.renderers.write().unwrap().push(MultiRendererElement {
|
|
step_frequency,
|
|
step_limit,
|
|
renderer: Box::new(renderer),
|
|
});
|
|
}
|
|
pub fn with<R: Renderer<S> + 'static>(self, renderer: R, step_frequency: u64, step_limit: Option<u64>) -> Self {
|
|
self.push(renderer, step_frequency, step_limit);
|
|
self
|
|
}
|
|
pub fn any_work_for_frame(&self, frame: u64) -> bool {
|
|
self.renderers.read().unwrap().iter().any(|m| m.work_this_frame(frame))
|
|
}
|
|
}
|
|
|
|
impl<S: AbstractSim> Renderer<S> for MultiRenderer<S> {
|
|
fn render_z_slice(&self, state: &S, z: u32, measurements: &[&dyn AbstractMeasurement<S>], config: RenderConfig) {
|
|
default_render_z_slice(self, state, z, measurements, config)
|
|
}
|
|
fn render(&self, state: &S, measurements: &[&dyn AbstractMeasurement<S>], config: RenderConfig) {
|
|
if self.renderers.read().unwrap().len() != 0 {
|
|
self.render_with_image(state, &RenderSteps::render(state, measurements, state.depth() / 2), measurements, config);
|
|
}
|
|
}
|
|
|
|
fn render_with_image(&self, state: &S, im: &RgbImage, measurements: &[&dyn AbstractMeasurement<S>], config: RenderConfig) {
|
|
for r in &*self.renderers.read().unwrap() {
|
|
if r.work_this_frame(state.step_no()) {
|
|
r.renderer.render_with_image(state, im, measurements, config);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
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<Measurement>,
|
|
}
|
|
|
|
impl<S: AbstractSim> SerializedFrame<S> {
|
|
pub fn to_generic(self) -> SerializedFrame<GenericSim<S::Real>> {
|
|
SerializedFrame {
|
|
state: AbstractSim::to_generic(&self.state),
|
|
measurements: self.measurements,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// this serializes the simulation state plus measurements to disk.
|
|
/// it can either convert the state to a generic, material-agnostic format (generic)
|
|
/// or dump it as-is.
|
|
pub struct SerializerRenderer {
|
|
fmt_str: String,
|
|
prefer_generic: bool,
|
|
}
|
|
|
|
impl SerializerRenderer {
|
|
/// `fmt_str` is a format string which will be formatted with
|
|
/// `{step_no}` set to the relevant simulation step.
|
|
pub fn new(fmt_str: &str) -> Self {
|
|
Self {
|
|
fmt_str: fmt_str.into(),
|
|
prefer_generic: false,
|
|
}
|
|
}
|
|
|
|
/// Same as `new`, but cast to GenericSim before serializing. This yields a file that's easier
|
|
/// for post-processing.
|
|
pub fn new_generic(fmt_str: &str) -> Self {
|
|
Self {
|
|
fmt_str: fmt_str.into(),
|
|
prefer_generic: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl SerializerRenderer {
|
|
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());
|
|
// 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>> {
|
|
let mut reader = BufReader::new(File::open(&*self.fmt_str).ok()?);
|
|
bincode::deserialize_from(&mut reader).ok()
|
|
}
|
|
}
|
|
|
|
impl<S: AbstractSim + Serialize> Renderer<S> for SerializerRenderer {
|
|
fn render_z_slice(&self, state: &S, z: u32, measurements: &[&dyn AbstractMeasurement<S>], config: RenderConfig) {
|
|
default_render_z_slice(self, state, z, measurements, config)
|
|
}
|
|
fn render(&self, state: &S, measurements: &[&dyn AbstractMeasurement<S>], _config: RenderConfig) {
|
|
if self.prefer_generic {
|
|
self.serialize(&state.to_generic(), meas::eval_multiple(state, measurements));
|
|
} else {
|
|
self.serialize(state, meas::eval_multiple(state, measurements));
|
|
}
|
|
}
|
|
}
|
|
|
|
enum CsvState {
|
|
Reading(csv::Reader<BufReader<File>>),
|
|
Writing(csv::Writer<BufWriter<File>>),
|
|
}
|
|
|
|
pub struct CsvRenderer {
|
|
state: Mutex<Option<CsvState>>,
|
|
}
|
|
|
|
impl CsvRenderer {
|
|
pub fn new<P: AsRef<Path>>(path: P) -> Self {
|
|
let f = OpenOptions::new().read(true).write(true).create(true).open(path).unwrap();
|
|
let reader = csv::Reader::from_reader(BufReader::new(f));
|
|
Self {
|
|
state: Mutex::new(Some(CsvState::Reading(reader))),
|
|
}
|
|
}
|
|
pub fn read_column(self, header: &str) -> Vec<String> {
|
|
let mut rd = match self.state.into_inner().unwrap() {
|
|
Some(CsvState::Reading(rd)) => rd,
|
|
_ => panic!("not reading!"),
|
|
};
|
|
let colno = rd.headers().unwrap().iter().position(|it| it == header).unwrap();
|
|
rd.into_records().map(|items| items.unwrap().get(colno).unwrap().to_owned()).collect()
|
|
}
|
|
pub fn read_column_as_f32(self, header: &str) -> Vec<f32> {
|
|
self.read_column(header).into_iter().map(|s| s.parse().unwrap()).collect()
|
|
}
|
|
}
|
|
|
|
impl<S: AbstractSim> Renderer<S> for CsvRenderer {
|
|
fn render_z_slice(&self, state: &S, z: u32, measurements: &[&dyn AbstractMeasurement<S>], config: RenderConfig) {
|
|
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(state, measurements);
|
|
let step = state.step_no();
|
|
let mut lock = self.state.lock().unwrap();
|
|
let mut writer = match lock.take().unwrap() {
|
|
CsvState::Reading(mut reader) => {
|
|
let headers = reader.headers().unwrap();
|
|
let has_header = headers.get(0) == Some("step");
|
|
if has_header {
|
|
// read until we get a row whose step is >= this one
|
|
let mut seek_pos = None;
|
|
for record in reader.records() {
|
|
let record = record.unwrap();
|
|
if let Some(step_str) = record.get(0) {
|
|
if let Ok(step_num) = step_str.parse::<u64>() {
|
|
if step_num >= step {
|
|
// truncate csv here
|
|
seek_pos = record.position().map(|p| p.byte());
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let mut file = reader.into_inner().into_inner();
|
|
if let Some(pos) = seek_pos {
|
|
file.seek(SeekFrom::Start(pos)).unwrap();
|
|
file.set_len(pos).unwrap();
|
|
}
|
|
csv::Writer::from_writer(BufWriter::new(file))
|
|
} else { // no header
|
|
let mut file = reader.into_inner().into_inner();
|
|
file.seek(SeekFrom::Start(0)).unwrap();
|
|
file.set_len(0).unwrap();
|
|
let mut writer = csv::Writer::from_writer(BufWriter::new(file));
|
|
// write the header
|
|
writer.write_record(row.iter().map(|m| m.name())).unwrap();
|
|
writer
|
|
}
|
|
},
|
|
CsvState::Writing(writer) => writer,
|
|
};
|
|
writer.write_record(row.iter().map(|m| m.machine_readable())).unwrap();
|
|
writer.flush().unwrap();
|
|
*lock = Some(CsvState::Writing(writer));
|
|
}
|
|
}
|