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, typical_mag: f32) -> Vec2 { let new_mag = scale_unsigned(x.mag(), typical_mag); x.with_mag(new_mag).unwrap_or_default() } fn im_size(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], /// 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], 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], 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], 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(d: &T) -> &[u8] { std::slice::from_raw_parts(d as *const T as *const u8, std::mem::size_of::()) } 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(&mut self, color: Rgb, typical: f32, measure: F) where F: Fn(&Sample<'_, S::Real, S::Material>) -> Vec2 { 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(&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(&self, xidx: u32, yidx: u32, size: u32, measure: &F) -> Vec2 where F: Fn(&Sample<'_, S::Real, S::Material>) -> Vec2 { 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, rel: Vec2, color: Rgb, alpha: f32); } impl ImageRenderExt for RgbImage { fn draw_field_arrow(&mut self, center: Vec2, rel: Vec2, color: Rgb, 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: Send + Sync { fn render_z_slice(&self, state: &S, z: u32, measurements: &[&dyn AbstractMeasurement], config: RenderConfig); // { // self.render_with_image(state, &RenderSteps::render(state, measurements, z), measurements); // } fn render(&self, state: &S, measurements: &[&dyn AbstractMeasurement], 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], config: RenderConfig) { self.render(state, measurements, config); } } fn default_render_z_slice>( me: &R, state: &S, z: u32, measurements: &[&dyn AbstractMeasurement], config: RenderConfig, ) { me.render_with_image(state, &RenderSteps::render(state, measurements, z), measurements, config); } fn default_render>( me: &R, state: &S, measurements: &[&dyn AbstractMeasurement], 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]) { // 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 Renderer for ColorTermRenderer { fn render(&self, state: &S, measurements: &[&dyn AbstractMeasurement], config: RenderConfig) { default_render(self, state, measurements, config) } fn render_z_slice( &self, state: &S, z: u32, measurements: &[&dyn AbstractMeasurement], 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>>, } impl Y4MRenderer { pub fn new>(output: P) -> Self { Self { out_path: output.into(), encoder: Mutex::new(None), } } } impl Renderer for Y4MRenderer { fn render_z_slice(&self, state: &S, z: u32, measurements: &[&dyn AbstractMeasurement], config: RenderConfig) { default_render_z_slice(self, state, z, measurements, config) } fn render(&self, state: &S, measurements: &[&dyn AbstractMeasurement], config: RenderConfig) { default_render(self, state, measurements, config) } fn render_with_image(&self, _state: &S, im: &RgbImage, _meas: &[&dyn AbstractMeasurement], _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 { step_frequency: u64, step_limit: Option, renderer: Box>, } impl MultiRendererElement { 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 { 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 { renderers: RwLock>>, } impl Default for MultiRenderer { fn default() -> Self { Self { renderers: RwLock::new(Vec::new()) } } } impl MultiRenderer { pub fn new() -> Self { Default::default() } pub fn push + 'static>(&self, renderer: R, step_frequency: u64, step_limit: Option) { self.renderers.write().unwrap().push(MultiRendererElement { step_frequency, step_limit, renderer: Box::new(renderer), }); } pub fn with + 'static>(self, renderer: R, step_frequency: u64, step_limit: Option) -> 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 { self.renderers.read().unwrap().iter().flat_map(|m| m.next_frame_for_work(after)).min() } } impl Renderer for MultiRenderer { fn render_z_slice(&self, state: &S, z: u32, measurements: &[&dyn AbstractMeasurement], config: RenderConfig) { default_render_z_slice(self, state, z, measurements, config) } fn render(&self, state: &S, measurements: &[&dyn AbstractMeasurement], 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], 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 { pub state: S, /// although not generally necessary to load the sim, saving the measurements is beneficial for /// post-processing. pub measurements: Vec, } impl SerializedFrame { pub fn to_generic(self) -> SerializedFrame> { 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(&self, state: &S, measurements: Vec) { 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 Deserialize<'a>>(&self) -> Option> { let mut reader = BufReader::new(File::open(&*self.fmt_str).ok()?); bincode::deserialize_from(&mut reader).ok() } } impl Renderer for SerializerRenderer { fn render_z_slice(&self, state: &S, z: u32, measurements: &[&dyn AbstractMeasurement], config: RenderConfig) { default_render_z_slice(self, state, z, measurements, config) } fn render(&self, state: &S, measurements: &[&dyn AbstractMeasurement], _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>), Writing(csv::Writer>), } pub struct CsvRenderer { state: Mutex>, } impl CsvRenderer { pub fn new>(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 { 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 { self.read_column(header).into_iter().map(|s| s.parse().unwrap()).collect() } } impl Renderer for CsvRenderer { fn render_z_slice(&self, state: &S, z: u32, measurements: &[&dyn AbstractMeasurement], config: RenderConfig) { default_render_z_slice(self, state, z, measurements, config) } fn render(&self, state: &S, measurements: &[&dyn AbstractMeasurement], _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::() { 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)); } }