761 lines
28 KiB
Rust
761 lines
28 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 = format!("{}: \t{}", m.name(), 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,
|
|
}
|
|
}
|
|
fn next_frame_for_work(&self, after: u64) -> Option<u64> {
|
|
let max_frame = after + self.step_frequency;
|
|
let max_frame = max_frame - max_frame % self.step_frequency;
|
|
match self.step_limit {
|
|
None => Some(max_frame),
|
|
Some(end) => Some(max_frame).filter(|&f| f < 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))
|
|
}
|
|
pub fn next_frame_for_work(&self, after: u64) -> Option<u64> {
|
|
self.renderers.read().unwrap().iter().flat_map(|m| m.next_frame_for_work(after)).min()
|
|
}
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|