From 58441a751a4875fc5a631414b16d40b0ab76a6a2 Mon Sep 17 00:00:00 2001 From: Sam Morrell Date: Mon, 10 Jan 2022 15:43:54 +0000 Subject: [PATCH] Migrated `geom::cast` module. The `geom::cast` module has been migrated across. To support this I have also migrated the CSV reader to read into a Table object. This commit does not include documentation or unit tests. The progress of these can be tracked on issues #24 and #25 respectively. --- src/fs/extensions/csv.rs | 41 +++++++++++ src/fs/extensions/mod.rs | 3 +- src/geom/cast/camera.rs | 114 ++++++++++++++++++++++++++++ src/geom/cast/camera_builder.rs | 98 ++++++++++++++++++++++++ src/geom/cast/emitter.rs | 127 ++++++++++++++++++++++++++++++++ src/geom/cast/emitter_loader.rs | 84 +++++++++++++++++++++ src/geom/cast/mod.rs | 8 ++ src/geom/mod.rs | 3 +- 8 files changed, 476 insertions(+), 2 deletions(-) create mode 100644 src/fs/extensions/csv.rs create mode 100644 src/geom/cast/camera.rs create mode 100644 src/geom/cast/camera_builder.rs create mode 100644 src/geom/cast/emitter.rs create mode 100644 src/geom/cast/emitter_loader.rs create mode 100644 src/geom/cast/mod.rs diff --git a/src/fs/extensions/csv.rs b/src/fs/extensions/csv.rs new file mode 100644 index 0000000..6a0d134 --- /dev/null +++ b/src/fs/extensions/csv.rs @@ -0,0 +1,41 @@ +//! Comma-Separated-Variable file handling. + +use crate::{data::Table, err::Error, fs::File}; +use std::{ + io::{BufRead, BufReader}, + path::Path, + str::FromStr, +}; + +impl File for Table { + #[inline] + fn load(path: &Path) -> Result { + // Load all of the lines into a vector of lines. + let mut lines: Vec<_> = BufReader::new(std::fs::File::open(path)?) + .lines() + .map(Result::unwrap) + .filter(|line| !line.starts_with("//")) + .collect(); + + // As we know the number of rows, we can pre-allocate the rows vector. + let mut rows = Vec::with_capacity(lines.len()); + // We make the reasonable assumption that the CSV file has a header on the first row. + let headings = lines + .remove(0) + .split(',') + .map(|s| (*s).to_string()) + .collect(); + // Now iterate the remaining lines, attempt to parse them and push them onto the rows vec. + for mut line in lines { + line.retain(|c| !c.is_whitespace()); + let row = line + .split(',') + .map(str::parse) + .filter_map(Result::ok) + .collect(); + rows.push(row); + } + + Ok(Self::new(headings, rows)) + } +} diff --git a/src/fs/extensions/mod.rs b/src/fs/extensions/mod.rs index b461765..ed4adbb 100644 --- a/src/fs/extensions/mod.rs +++ b/src/fs/extensions/mod.rs @@ -8,8 +8,9 @@ //! Please see the documentation in the appropriate module for specifics on each //! format. +pub mod csv; pub mod json; pub mod netcdf; pub mod wavefront; -pub use self::{json::*, netcdf::*, wavefront::*}; +pub use self::{csv::*, json::*, netcdf::*, wavefront::*}; diff --git a/src/geom/cast/camera.rs b/src/geom/cast/camera.rs new file mode 100644 index 0000000..d05b9c5 --- /dev/null +++ b/src/geom/cast/camera.rs @@ -0,0 +1,114 @@ +//! Camera structure. + +use crate::{ + access, clone, fmt_report, + geom::{Orient, Ray}, + math::{Point3, Rot3, Vec3}, + ord::{X, Y}, +}; +use std::fmt::{Display, Error, Formatter}; + +/// Tracer emission structure. +pub struct Camera { + /// Orientation. + orient: Orient, + /// Rotation delta. + half_delta_theta: f64, + /// Resolution. + res: [usize; 2], + /// Super sampling power. + ss_power: usize, +} + +impl Camera { + access!(res: [usize; 2]); + clone!(ss_power: usize); + + /// Construct a new instance. + #[inline] + #[must_use] + pub fn new(orient: Orient, fov: f64, res: [usize; 2], ss_power: usize) -> Self { + debug_assert!(fov > 0.0); + debug_assert!(res[X] > 0); + debug_assert!(res[Y] > 0); + debug_assert!(ss_power > 0); + + let half_delta_theta = fov / ((2 * (ss_power * (res[X] - 1))) as f64); + + Self { + orient, + half_delta_theta, + res, + ss_power, + } + } + + /// Reference the camera's position. + #[inline] + #[must_use] + pub const fn pos(&self) -> &Point3 { + self.orient.pos() + } + + /// Calculate the total number of samples. + #[inline] + #[must_use] + pub const fn num_pixels(&self) -> usize { + self.res[X] * self.res[Y] + } + + /// Calculate the total number of super samples per pixel. + #[inline] + #[must_use] + pub const fn num_super_samples(&self) -> usize { + self.ss_power * self.ss_power + } + + /// Calculate the total number of samples. + #[inline] + #[must_use] + pub const fn num_samples(&self) -> usize { + self.num_super_samples() * self.num_pixels() as usize + } + + /// Emit a ray for the given pixel and super-sample. + #[inline] + #[must_use] + pub fn emit(&self, pixel: [usize; 2], ss: [usize; 2]) -> Ray { + debug_assert!(pixel[X] < self.res[X]); + debug_assert!(pixel[Y] < self.res[Y]); + debug_assert!(ss[X] < self.ss_power); + debug_assert!(ss[Y] < self.ss_power); + + let mut theta = + self.half_delta_theta * (1 + (2 * (ss[X] + (pixel[X] * self.ss_power)))) as f64; + let mut phi = + self.half_delta_theta * (1 + (2 * (ss[Y] + (pixel[Y] * self.ss_power)))) as f64; + + theta -= self.half_delta_theta * (self.res[X] * self.ss_power) as f64; + phi -= self.half_delta_theta * (self.res[Y] * self.ss_power) as f64; + + let mut ray = self.orient.forward_ray(); + *ray.dir_mut() = Rot3::from_axis_angle(&Vec3::from(self.orient.down()), theta) + * Rot3::from_axis_angle(&Vec3::from(*self.orient.right()), phi) + * ray.dir(); + + ray + } +} + +impl Display for Camera { + #[inline] + fn fmt(&self, fmt: &mut Formatter) -> Result<(), Error> { + writeln!(fmt, "...")?; + fmt_report!(fmt, self.orient, "orientation"); + fmt_report!(fmt, self.half_delta_theta.to_degrees(), "dTheta/2 (deg)"); + fmt_report!( + fmt, + &format!("[{} x {}]", self.res[X], self.res[Y]), + "resolution" + ); + fmt_report!(fmt, self.ss_power, "super sampling power"); + Ok(()) + } +} diff --git a/src/geom/cast/camera_builder.rs b/src/geom/cast/camera_builder.rs new file mode 100644 index 0000000..90df064 --- /dev/null +++ b/src/geom/cast/camera_builder.rs @@ -0,0 +1,98 @@ +//! Camera builder structure. + +use crate::{ + fmt_report, + geom::{Camera, Orient}, + math::{Point3, Vec3}, + ord::{Build, X, Y}, +}; +use arctk_attr::file; +use std::fmt::{Display, Error, Formatter}; + +/// Loadable camera structure. +#[file] +#[derive(Clone)] +pub struct CameraBuilder { + /// Position. + pos: Point3, + /// Target. + tar: Point3, + /// Horizontal field-of-view (deg). + fov: f64, + /// Image resolution. + res: [usize; 2], + /// Optional super-sampling power. + ss_power: Option, +} + +impl CameraBuilder { + /// Construct a new instance. + #[inline] + #[must_use] + pub fn new(pos: Point3, tar: Point3, fov: f64, res: [usize; 2], ss_power: Option) -> Self { + debug_assert!(fov > 0.0); + debug_assert!(res[X] > 0); + debug_assert!(res[Y] > 0); + debug_assert!(ss_power.is_none() || ss_power.unwrap() > 1); + + Self { + pos, + tar, + fov, + res, + ss_power, + } + } + + /// Move the camera. + #[inline] + pub fn travel(&mut self, d: Vec3) { + self.pos += d; + } +} + +impl Build for CameraBuilder { + type Inst = Camera; + + #[inline] + fn build(self) -> Self::Inst { + Self::Inst::new( + Orient::new_tar(self.pos, &self.tar), + self.fov.to_radians(), + self.res, + self.ss_power.map_or(1, |ss| ss), + ) + } +} + +impl Display for CameraBuilder { + #[inline] + fn fmt(&self, fmt: &mut Formatter) -> Result<(), Error> { + writeln!(fmt, "...")?; + fmt_report!( + fmt, + &format!("({}, {}, {})", self.pos.x(), self.pos.y(), self.pos.z()), + "position (m)" + ); + fmt_report!( + fmt, + &format!("({}, {}, {})", self.tar.x(), self.tar.y(), self.tar.z()), + "target (m)" + ); + fmt_report!(fmt, self.fov, "field of view (deg)"); + fmt_report!( + fmt, + &format!("[{} x {}]", self.res[X], self.res[Y]), + "resolution" + ); + + let ss_power = if let Some(n) = self.ss_power { + format!("{} sub-samples", n * n) + } else { + "OFF".to_owned() + }; + fmt_report!(fmt, ss_power, "super sampling"); + + Ok(()) + } +} diff --git a/src/geom/cast/emitter.rs b/src/geom/cast/emitter.rs new file mode 100644 index 0000000..a61c076 --- /dev/null +++ b/src/geom/cast/emitter.rs @@ -0,0 +1,127 @@ +//! Optical material. + +use crate::{ + geom::{Emit, Grid, Mesh, Ray}, + math::{rand_isotropic_dir, Point3}, + tools::linear_to_three_dim, +}; +use ndarray::Array3; +use rand::Rng; +use std::fmt::{Display, Error, Formatter}; + +/// Ray emission structure. +pub enum Emitter { + /// Single beam. + Beam(Ray), + /// Points. + Points(Vec), + /// Weighted points. + WeightedPoints(Vec, Vec), + /// Surface mesh. + Surface(Mesh), + /// Volume map. + Volume(Array3, Grid), +} + +impl Emitter { + /// Construct a new beam instance. + #[inline] + #[must_use] + pub const fn new_beam(ray: Ray) -> Self { + Self::Beam(ray) + } + + /// Construct a new points instance. + #[inline] + #[must_use] + pub fn new_points(points: Vec) -> Self { + debug_assert!(!points.is_empty()); + + Self::Points(points) + } + + /// Construct a new points instance. + #[inline] + #[must_use] + pub fn new_weighted_points(points: Vec, weights: &[f64]) -> Self { + debug_assert!(!points.is_empty()); + debug_assert!(points.len() == weights.len()); + + let sum: f64 = weights.iter().sum(); + let mut cumulative_weight = Vec::with_capacity(weights.len()); + let mut total = 0.0; + for w in weights { + total += w; + cumulative_weight.push(total / sum); + } + + Self::WeightedPoints(points, cumulative_weight) + } + + /// Construct a new surface instance. + #[inline] + #[must_use] + pub const fn new_surface(mesh: Mesh) -> Self { + Self::Surface(mesh) + } + + /// Construct a new volume instance. + #[inline] + #[must_use] + pub fn new_volume(map: Array3, grid: Grid) -> Self { + debug_assert!(map.sum() > 0.0); + debug_assert!(!map.is_empty()); + + Self::Volume(map, grid) + } + + /// Emit a new ray. + #[inline] + #[must_use] + pub fn emit(&self, rng: &mut R) -> Ray { + match *self { + Self::Beam(ref ray) => ray.clone(), + Self::Points(ref ps) => { + Ray::new(ps[rng.gen_range(0..ps.len())], rand_isotropic_dir(rng)) + } + Self::WeightedPoints(ref ps, ref ws) => { + let r: f64 = rng.gen(); + for (p, w) in ps.iter().zip(ws) { + if r <= *w { + return Ray::new(*p, rand_isotropic_dir(rng)); + } + } + unreachable!("Failed to determine weighted point to emit from."); + } + Self::Surface(ref mesh) => mesh.cast(rng), + Self::Volume(ref map, ref grid) => { + let r = rng.gen_range(0.0..map.sum()); + let mut total = 0.0; + for n in 0..map.len() { + let index = linear_to_three_dim(n, grid.res()); + total += map[index]; + if total >= r { + let pos = grid.gen_voxel(&index).rand_pos(rng); + let dir = rand_isotropic_dir(rng); + return Ray::new(pos, dir); + } + } + panic!("Failed to emit ray from volume.") + } + } + } +} + +impl Display for Emitter { + #[inline] + fn fmt(&self, fmt: &mut Formatter) -> Result<(), Error> { + let kind = match *self { + Self::Beam { .. } => "Beam", + Self::Points { .. } => "Points", + Self::WeightedPoints { .. } => "WeightedPoints", + Self::Surface { .. } => "Surface", + Self::Volume { .. } => "Volume", + }; + write!(fmt, "{}", kind) + } +} diff --git a/src/geom/cast/emitter_loader.rs b/src/geom/cast/emitter_loader.rs new file mode 100644 index 0000000..771f73f --- /dev/null +++ b/src/geom/cast/emitter_loader.rs @@ -0,0 +1,84 @@ +//! Optical material. + +use crate::{ + data::Table, + err::Error, + fs::{File, Load, Redirect}, + geom::{Emitter, GridBuilder, MeshLoader, Ray}, + math::{Dir3, Point3}, + ord::{Build, X, Y, Z}, +}; +use arctk_attr::file; +use ndarray::Array3; +use std::{ + fmt::{Display, Formatter}, + path::{Path, PathBuf}, +}; + +/// Ray emission structure. +#[file] +pub enum EmitterLoader { + /// Single beam. + Beam(Point3, Dir3), + /// Point list. + Points(PathBuf), + /// Weighted point list. + WeightedPoints(PathBuf, PathBuf), + /// Surface mesh. + Surface(MeshLoader), + /// Volume map. + Volume(PathBuf, Redirect), +} + +impl Load for EmitterLoader { + type Inst = Emitter; + + #[inline] + fn load(self, in_dir: &Path) -> Result { + Ok(match self { + Self::Beam(pos, dir) => Self::Inst::new_beam(Ray::new(pos, dir)), + Self::Points(points_path) => { + let table = Table::new_from_file(&in_dir.join(points_path))?; + let points = table + .into_inner() + .iter() + .map(|row| Point3::new(row[X], row[Y], row[Z])) + .collect(); + + Self::Inst::new_points(points) + } + Self::WeightedPoints(points_path, weight_path) => { + let points_data = Table::new_from_file(&in_dir.join(points_path))?; + let points = points_data + .into_inner() + .iter() + .map(|row| Point3::new(row[X], row[Y], row[Z])) + .collect(); + + let weights_data = Table::new_from_file(&in_dir.join(weight_path))?; + let weights: Vec<_> = weights_data.into_inner().iter().map(|row| row[X]).collect(); + + Self::Inst::new_weighted_points(points, &weights) + } + Self::Surface(mesh) => Self::Inst::new_surface(mesh.load(in_dir)?), + Self::Volume(spatial_map, grid) => { + let spatial_map: Array3 = Array3::new_from_file(&in_dir.join(spatial_map))?; + Self::Inst::new_volume(spatial_map, grid.load(in_dir)?.build()) + } + }) + } +} + +impl Display for EmitterLoader { + #[inline] + fn fmt(&self, fmt: &mut Formatter) -> Result<(), std::fmt::Error> { + let kind = match *self { + Self::Beam { .. } => "Beam", + Self::Points { .. } => "Points", + Self::WeightedPoints { .. } => "WeightedPoints", + Self::Surface { .. } => "Surface", + Self::Volume { .. } => "Volume", + }; + write!(fmt, "{}", kind) + } +} diff --git a/src/geom/cast/mod.rs b/src/geom/cast/mod.rs new file mode 100644 index 0000000..89226e1 --- /dev/null +++ b/src/geom/cast/mod.rs @@ -0,0 +1,8 @@ +//! Ray-casting module. + +pub mod camera; +pub mod camera_builder; +pub mod emitter; +pub mod emitter_loader; + +pub use self::{camera::*, camera_builder::*, emitter::*, emitter_loader::*}; diff --git a/src/geom/mod.rs b/src/geom/mod.rs index 50c61aa..5a86e95 100644 --- a/src/geom/mod.rs +++ b/src/geom/mod.rs @@ -1,6 +1,7 @@ +pub mod cast; pub mod domain; pub mod properties; pub mod rt; pub mod shape; -pub use self::{domain::*, properties::*, rt::*, shape::*}; +pub use self::{cast::*, domain::*, properties::*, rt::*, shape::*};