diff --git a/hoomd-bevy/katex.html b/hoomd-bevy/katex.html~trunk similarity index 100% rename from hoomd-bevy/katex.html rename to hoomd-bevy/katex.html~trunk diff --git a/hoomd-bevy/src/representation/hyperbolic_polygon.wgsl b/hoomd-bevy/src/representation/hyperbolic_polygon.wgsl index 42c192a4d..c5d9bb694 100644 --- a/hoomd-bevy/src/representation/hyperbolic_polygon.wgsl +++ b/hoomd-bevy/src/representation/hyperbolic_polygon.wgsl @@ -19,7 +19,11 @@ const PI: f32 = 3.141592653589793238462643; @group(2) @binding(4) var image_color_texture: texture_2d; @group(2) @binding(5) var image_color_sampler: sampler; -@group(2) @binding(6) var n_sides: f32; +struct PolygonParams { + n_sides: f32, +}; +@group(2) @binding(6) +var polygon: PolygonParams; struct VertexOutput { // this is `clip position` when the struct is used as a vertex stage output @@ -75,7 +79,7 @@ fn vertex(vertex: Vertex) -> VertexOutput { vec4(vertex.position, 1.0) ); out.position = mesh_functions::mesh2d_position_world_to_clip(out.world_position); - out.n_sides = n_sides; + out.n_sides = polygon.n_sides; #endif #ifdef VERTEX_NORMALS diff --git a/hoomd-derive/katex.html b/hoomd-derive/katex.html~trunk similarity index 100% rename from hoomd-derive/katex.html rename to hoomd-derive/katex.html~trunk diff --git a/hoomd-geometry/katex.html b/hoomd-geometry/katex.html~trunk similarity index 100% rename from hoomd-geometry/katex.html rename to hoomd-geometry/katex.html~trunk diff --git a/hoomd-gsd/katex.html b/hoomd-gsd/katex.html~trunk similarity index 100% rename from hoomd-gsd/katex.html rename to hoomd-gsd/katex.html~trunk diff --git a/hoomd-interaction/katex.html b/hoomd-interaction/katex.html~trunk similarity index 100% rename from hoomd-interaction/katex.html rename to hoomd-interaction/katex.html~trunk diff --git a/hoomd-linear-algebra/katex.html b/hoomd-linear-algebra/katex.html~trunk similarity index 100% rename from hoomd-linear-algebra/katex.html rename to hoomd-linear-algebra/katex.html~trunk diff --git a/hoomd-manifold/katex.html b/hoomd-manifold/katex.html~trunk similarity index 100% rename from hoomd-manifold/katex.html rename to hoomd-manifold/katex.html~trunk diff --git a/hoomd-manifold/src/sphere.rs b/hoomd-manifold/src/sphere.rs index 9145b7ced..eaa91a6c5 100644 --- a/hoomd-manifold/src/sphere.rs +++ b/hoomd-manifold/src/sphere.rs @@ -5,14 +5,14 @@ use approxim::{approx_derive::RelativeEq, assert_relative_eq}; use rand::{ - Rng, + Rng, RngExt, distr::{Distribution, Uniform}, }; use serde::{Deserialize, Serialize}; use std::f64::consts::PI; use hoomd_utility::valid::PositiveReal; -use hoomd_vector::{Cartesian, InnerProduct, Metric}; +use hoomd_vector::{Cartesian, InnerProduct, Metric, Quaternion, Rotate, Versor}; /// Point on the surface of a sphere. /// @@ -53,8 +53,8 @@ impl Spherical { assert_relative_eq!(rad, 1.0_f64, epsilon = 1e-6); Spherical { point } } - - /// Implements a stereographic projection from the N-sphere to an N-dimensional plane. + /// Implements a stereographic projection from the 2-sphere to an 2-dimensional + /// plane by projecting through the $`(0,0,1)`$ axis. /// /// # Example /// ``` @@ -100,7 +100,7 @@ impl Spherical<3> { } impl Spherical<4> { - /// Create a 3-sphere from spherical coordinates + /// Create a point on a unit-radius 3-sphere from a unit quaternion. #[inline] #[must_use] pub fn from_polar_coordinates(theta: f64, phi_1: f64, phi_2: f64) -> Spherical<4> { @@ -115,6 +115,80 @@ impl Spherical<4> { ]); Spherical::from_cartesian_coordinates(point) } + /// Create a point on a unit-radius 3-sphere from a unit quaternion. + #[inline] + #[must_use] + pub fn from_versor(versor: Versor) -> Spherical<4> { + let (a, b, c, d) = versor.get_components(); + Spherical::<4>::from_cartesian_coordinates(Cartesian::from([a, b, c, d])) + } + /// Create a versor which maps $`(1,0,0,0)`$ to the target `Spherical<4>` point. + /// # Example + /// ``` + /// use approxim::assert_relative_eq; + /// use hoomd_manifold::Spherical; + /// use hoomd_vector::{Cartesian, Quaternion, Versor}; + /// use std::f64::consts::PI; + /// + /// # fn main() -> Result<(), Box> { + /// let radius = 1.0; + /// let x = Spherical::<4>::from_polar_coordinates( + /// PI / 4.0, + /// PI / 8.0, + /// 5.0 * PI / 4.0, + /// ); + /// let x_versor = x.to_versor(); + /// let pole_versor = Quaternion::from([1.0, 0.0, 0.0, 0.0]) + /// .to_versor() + /// .expect("not a null vector"); + /// let transformation = + /// (*x_versor.get() * *pole_versor.get() * *x_versor.get()) + /// .to_versor() + /// .expect("Hard-coded example is valid"); + /// let mapped_pole = Spherical::<4>::from_versor(transformation); + /// + /// assert_relative_eq!( + /// mapped_pole.coordinates()[0], + /// x.coordinates()[0], + /// epsilon = 1e-12 + /// ); + /// assert_relative_eq!( + /// mapped_pole.coordinates()[1], + /// x.coordinates()[1], + /// epsilon = 1e-12 + /// ); + /// assert_relative_eq!( + /// mapped_pole.coordinates()[2], + /// x.coordinates()[2], + /// epsilon = 1e-12 + /// ); + /// assert_relative_eq!( + /// mapped_pole.coordinates()[3], + /// x.coordinates()[3], + /// epsilon = 1e-12 + /// ); + /// # Ok(()) + /// # } + /// ``` + #[inline] + #[must_use] + pub fn to_versor(&self) -> Versor { + let phi = self.coordinates()[3].atan2(self.coordinates()[2]); + let theta = ((self.coordinates()[3].powi(2) + self.coordinates()[2].powi(2)).sqrt()) + .atan2(self.coordinates()[1]); + let psi = ((self.coordinates()[3].powi(2) + + self.coordinates()[2].powi(2) + + self.coordinates()[1].powi(2)) + .sqrt()) + .atan2(self.coordinates()[0]); + let n_hat = Cartesian::from([ + theta.cos(), + (theta.sin()) * (phi.cos()), + (theta.sin()) * (phi.sin()), + ]) + .to_unit_unchecked(); + Versor::from_axis_angle(n_hat.0, psi) + } } impl Metric for Spherical<3> { @@ -158,7 +232,8 @@ impl Metric for Spherical<4> { #[inline] fn distance(&self, other: &Self) -> f64 { let arg = Cartesian::dot(&self.point, &other.point); - arg.acos() + let arg_clipped = arg.clamp(-1.0, 1.0); + arg_clipped.acos() } #[inline] fn distance_squared(&self, other: &Self) -> f64 { @@ -206,11 +281,11 @@ impl Metric for Spherical<4> { /// # } /// ``` #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct SphericalDisk { +pub struct SphericalDisk { /// Max distance away from point. pub disk_radius: PositiveReal, /// The center of the disk. - pub point: Spherical<3>, + pub point: Spherical, } impl Default for Spherical { @@ -222,7 +297,7 @@ impl Default for Spherical { } } -impl Distribution> for SphericalDisk { +impl Distribution> for SphericalDisk<3> { #[inline] fn sample(&self, rng: &mut R) -> Spherical<3> { let max_angle = self.disk_radius.get(); @@ -274,6 +349,34 @@ impl Distribution> for SphericalDisk { } } +impl Distribution> for SphericalDisk<4> { + /// Translates 3-dimensional cartesian vector named "point" along the + /// surface of a sphere by maximum distance of r. + #[inline] + fn sample(&self, rng: &mut R) -> Spherical<4> { + let max_trans = self.disk_radius.get(); + let point = self.point; + // generate random unit cartesian vector + let v: Versor = rng.random(); + let b_hat = v + .rotate(&Cartesian::from([1.0, 0.0, 0.0])) + .to_unit() + .expect("hard coded non-null vector"); + let eta = Uniform::new(0.0, max_trans).expect("hard coded non-negative"); + let translation_versor = Versor::from_axis_angle(b_hat.0, eta.sample(rng)); + + let position_versor = Quaternion::from(*point.coordinates()) + .to_versor() + .expect("spherical points cannot be null"); + let transformation = + ((*translation_versor.get()) * (*position_versor.get()) * (*translation_versor.get())) + .to_versor() + .expect("sperical points cannot be null"); + let sphere_point = Spherical::<4>::from_versor(transformation); + Spherical::<4>::from_cartesian_coordinates(*sphere_point.point()) + } +} + #[cfg(test)] mod tests { use super::*; @@ -329,7 +432,7 @@ mod tests { } #[test] - fn random_sphere() { + fn random_two_sphere() { // Generate ten random points on the Hyperbolic let mut rng = StdRng::seed_from_u64(42); let d = 0.1; @@ -350,4 +453,26 @@ mod tests { assert!(d > distance); } } + #[test] + fn random_three_sphere() { + // Generate ten random points on the Hyperbolic + let mut rng = StdRng::seed_from_u64(42); + let d = 0.1; + let n_pole = Spherical::from_cartesian_coordinates(Cartesian::from([1.0, 0.0, 0.0, 0.0])); + for _n in 0..10 { + let disk = SphericalDisk { + disk_radius: d.try_into().expect("hard-coded positive number"), + point: n_pole, + }; + let random_point: Spherical<4> = disk.sample(&mut rng); + + // check that points remain on Sphere + let rho = random_point.point.norm_squared(); + assert_relative_eq!(rho, 1.0, epsilon = 1e-12); + + // check that points are within distance d of north pole + let distance = random_point.point().distance(n_pole.point()); + assert!(d > distance); + } + } } diff --git a/hoomd-mc/katex.html b/hoomd-mc/katex.html~trunk similarity index 100% rename from hoomd-mc/katex.html rename to hoomd-mc/katex.html~trunk diff --git a/hoomd-mc/src/translate/sphere.rs b/hoomd-mc/src/translate/sphere.rs index 9c44dfa7e..edf6930bb 100644 --- a/hoomd-mc/src/translate/sphere.rs +++ b/hoomd-mc/src/translate/sphere.rs @@ -3,12 +3,13 @@ //! Implement Translation moves on curved surfaces -use rand::{Rng, distr::Distribution}; +use rand::{Rng, RngExt}; +use rand_distr::StandardNormal; use crate::{LocalTrial, Translate}; -use hoomd_manifold::{Spherical, SphericalDisk}; +use hoomd_manifold::Spherical; use hoomd_microstate::property::{Point, Position}; -use hoomd_vector::InnerProduct; +use hoomd_vector::{Cartesian, InnerProduct}; impl LocalTrial>> for Translate>> { /// Propose local trial moves for a body on the surface of a sphere @@ -25,7 +26,7 @@ impl LocalTrial>> for Translate>> { /// # fn main() -> Result<(), Box> { /// let mut rng = StdRng::seed_from_u64(14); /// let initial_point = Point::new(Spherical::from_cartesian_coordinates( - /// [0.5_f64.sqrt(), 0.5_f64.sqrt(), 0.0].into(), + /// [0.5_f64.sqrt(), 0.0, 0.5_f64.sqrt()].into(), /// )); /// let d = 0.1; /// let translate = Translate::with_maximum_distance(d.try_into()?); @@ -52,16 +53,104 @@ impl LocalTrial>> for Translate>> { body_properties: Point>, ) -> Point> { let mut trial = body_properties; - let disk = SphericalDisk { - disk_radius: *self.maximum_distance(), - point: *trial.position_mut(), - }; - *trial.position_mut() = disk.sample(rng); - let rescale = 1.0 / trial.position().point().norm(); - *trial.position_mut() = - Spherical::from_cartesian_coordinates(*trial.position().point() * rescale); + let displacement = (self.maximum_distance().get()) * rng.sample::(StandardNormal); + let (sn, cs) = (displacement.sin(), displacement.cos()); + let vec = Cartesian::<3>::from(std::array::from_fn(|_| rng.sample(StandardNormal))); + let proj = vec.dot(trial.position.point()); + let tangent = Cartesian::from([ + vec[0] - proj * trial.position.coordinates()[0], + vec[1] - proj * trial.position.coordinates()[1], + vec[2] - proj * trial.position.coordinates()[2], + ]); + let (unit, _norm) = tangent.to_unit().expect("cannot be null"); + let new = Cartesian::from([ + trial.position.coordinates()[0] * cs + unit.get().coordinates[0] * sn, + trial.position.coordinates()[1] * cs + unit.get().coordinates[1] * sn, + trial.position.coordinates()[2] * cs + unit.get().coordinates[2] * sn, + ]); + *trial.position_mut() = Spherical::from_cartesian_coordinates(new); trial + // let mut trial = body_properties; + // let disk = SphericalDisk { + // disk_radius: *self.maximum_distance(), + // point: *trial.position_mut(), + // }; + // trial.position_mut() = disk.sample(rng); + // let rescale = 1.0 / trial.position().point().norm(); + // trial.position_mut() = + // Spherical::from_cartesian_coordinates(*trial.position().point() * rescale); + // trial } } -// TODO: test +impl LocalTrial>> for Translate>> { + /// Propose local trial moves for a body on the surface of a 3-sphere + /// + /// # Example + /// ``` + /// use approxim::assert_relative_eq; + /// use hoomd_manifold::{Spherical, SphericalDisk}; + /// use hoomd_mc::{LocalTrial, Translate}; + /// use hoomd_microstate::property::{Point, Position}; + /// use hoomd_vector::{Cartesian, InnerProduct, Metric, Vector}; + /// use rand::{Rng, SeedableRng, rngs::StdRng}; + /// use std::f64::consts::PI; + /// + /// # fn main() -> Result<(), Box> { + /// let mut rng = StdRng::seed_from_u64(14); + /// let initial_point = Point::new(Spherical::<4>::from_polar_coordinates( + /// PI / 4.0, + /// PI / 10.0, + /// 5.0 * PI / 4.0, + /// )); + /// let d = 0.1; + /// let translate = Translate::with_maximum_distance(d.try_into()?); + /// + /// let new_body_properties = translate.propose(&mut rng, initial_point); + /// + /// // Translation move keeps point on the surface of the sphere + /// assert_relative_eq!( + /// new_body_properties.position().point().norm(), + /// 1.0_f64, + /// epsilon = 1e-8 + /// ); + /// + /// // Translation move does not translate the point more than a distance d away + /// assert!( + /// d > new_body_properties + /// .position() + /// .distance(&initial_point.position()) + /// ); + /// # Ok(()) + /// # } + /// ``` + #[inline] + fn propose( + &self, + rng: &mut R, + body_properties: Point>, + ) -> Point> { + let mut trial = body_properties; + let displacement = (self.maximum_distance().get()) * rng.sample::(StandardNormal); + let (sn, cs) = ((displacement.abs()).sin(), (displacement.abs()).cos()); + let vec = Cartesian::<4>::from(std::array::from_fn(|_| rng.sample(StandardNormal))); + let proj = vec.dot(trial.position.point()); + let tangent = Cartesian::from([ + vec[0] - proj * trial.position.coordinates()[0], + vec[1] - proj * trial.position.coordinates()[1], + vec[2] - proj * trial.position.coordinates()[2], + vec[3] - proj * trial.position.coordinates()[3], + ]); + let (unit, _norm) = tangent.to_unit().expect("cannot be null"); + let new = Cartesian::from([ + trial.position.coordinates()[0] * cs + unit.get().coordinates[0] * sn, + trial.position.coordinates()[1] * cs + unit.get().coordinates[1] * sn, + trial.position.coordinates()[2] * cs + unit.get().coordinates[2] * sn, + trial.position.coordinates()[3] * cs + unit.get().coordinates[3] * sn, + ]); + *trial.position_mut() = Spherical::from_cartesian_coordinates(new); + trial + } +} + +// TODO: test code diff --git a/hoomd-microstate/src/append.rs b/hoomd-microstate/src/append.rs index f4ba09d2a..be50ef7dc 100644 --- a/hoomd-microstate/src/append.rs +++ b/hoomd-microstate/src/append.rs @@ -5,12 +5,13 @@ use hoomd_geometry::shape::Hypercuboid; use hoomd_gsd::hoomd::{AppendError, Dimensions, Frame, HoomdGsdFile}; +use hoomd_manifold::{Hyperbolic, Spherical}; use hoomd_vector::{Angle, Cartesian, Versor}; use crate::{ AppendMicrostate, Microstate, - boundary::{Closed, Periodic}, - property::{OrientedPoint, Point}, + boundary::{Closed, Open, Periodic}, + property::{OrientedHyperbolicPoint, OrientedPoint, Point}, }; impl AppendMicrostate>, X, Closed>> for HoomdGsdFile { @@ -202,15 +203,81 @@ impl AppendMicrostate, Versor>, X, Periodic< } } +impl AppendMicrostate>, X, Open> for HoomdGsdFile { + #[inline] + fn append_microstate( + &mut self, + microstate: &Microstate>, X, Open>, + ) -> Result, AppendError> { + self.append_frame(microstate.step())? + .configuration_box([5.0, 5.0, 5.0, 0.0, 0.0, 0.0])? + .configuration_dimensions(Dimensions::Three)? + .particles_position(microstate.iter_sites_tag_order().map(|s| { + let proj: Vec = s.properties.position.stereographic_projection(); + [proj[0], proj[1], proj[2]].into() + })) + } +} + +impl AppendMicrostate, X, C> for HoomdGsdFile { + #[inline] + fn append_microstate( + &mut self, + microstate: &Microstate, X, C>, + ) -> Result, AppendError> { + self.append_frame(microstate.step())? + .configuration_box([2.0, 2.0, 2.0, 0.0, 0.0, 0.0])? + .configuration_dimensions(Dimensions::Two)? + .particles_position( + microstate + .iter_sites_tag_order() + .map(|s| s.properties.position.to_poincare()) + .map(|p| [p[0], p[1], 0.0].into()), + )? + .particles_orientation( + microstate + .iter_sites_tag_order() + .map(|s| s.properties.orientation.theta) + .map(|a| { + Versor::from_axis_angle( + [0.0, 0.0, 1.0] + .try_into() + .expect("hard-coded vector can be normalized"), + a, + ) + }), + ) + } +} + +impl AppendMicrostate>, X, C> for HoomdGsdFile { + #[inline] + fn append_microstate( + &mut self, + microstate: &Microstate>, X, C>, + ) -> Result, AppendError> { + self.append_frame(microstate.step())? + .configuration_box([2.0, 2.0, 2.0, 0.0, 0.0, 0.0])? + .configuration_dimensions(Dimensions::Two)? + .particles_position( + microstate + .iter_sites_tag_order() + .map(|s| s.properties.position.to_poincare()) + .map(|p| [p[0], p[1], 0.0].into()), + ) + } +} + #[cfg(test)] mod test { + use approxim::assert_relative_eq; use assert2::assert; use std::f64::consts::PI; use tempfile::tempdir; use super::*; use crate::Body; - use hoomd_geometry::shape::Rectangle; + use hoomd_geometry::shape::{EightEight, Rectangle}; use hoomd_gsd::file_layer::{GsdFile, Mode}; #[test] @@ -298,6 +365,273 @@ mod test { Ok(()) } + #[test] + fn point_spherical_3d() -> anyhow::Result<()> { + let microstate = Microstate::builder() + .boundary(Open) + .bodies([ + Body::point(Spherical::from_cartesian_coordinates(Cartesian::from([ + 1.0, 0.0, 0.0, 0.0, + ]))), + Body::point(Spherical::from_cartesian_coordinates(Cartesian::from([ + 0.0, 1.0, 0.0, 0.0, + ]))), + Body::point(Spherical::from_cartesian_coordinates(Cartesian::from([ + 0.0, 0.0, 1.0, 0.0, + ]))), + ]) + .step(1234) + .try_build()?; + + let tmp_dir = tempdir()?; + let path = tmp_dir.path().join("test.gsd"); + let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?; + hoomd_gsd_file.append_microstate(µstate)?; + + drop(hoomd_gsd_file); + + let gsd_file = GsdFile::open(path, Mode::Read)?; + + assert!(gsd_file.n_frames() == 1); + + let step = gsd_file.iter_scalars::(0, "configuration/step")?; + itertools::assert_equal(step, [1234]); + + let positions: Vec<[f32; 3]> = gsd_file + .iter_arrays::(0, "particles/position")? + .collect(); + assert_relative_eq!(positions[0][0], 1.0); + assert_relative_eq!(positions[0][1], 0.0); + assert_relative_eq!(positions[0][2], 0.0); + assert_relative_eq!(positions[1][0], 0.0); + assert_relative_eq!(positions[1][1], 1.0); + assert_relative_eq!(positions[1][2], 0.0); + assert_relative_eq!(positions[2][0], 0.0); + assert_relative_eq!(positions[2][1], 0.0); + assert_relative_eq!(positions[2][2], 1.0); + Ok(()) + } + + #[test] + fn point_hyperbolic_2d_open() -> anyhow::Result<()> { + let microstate = Microstate::builder() + .boundary(Open) + .bodies([ + Body::point(Hyperbolic::<3>::from_polar_coordinates(1.2, 0.0)), + Body::point(Hyperbolic::<3>::from_polar_coordinates(0.6, 1.5)), + ]) + .step(1234) + .try_build()?; + + let tmp_dir = tempdir()?; + let path = tmp_dir.path().join("test.gsd"); + let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?; + hoomd_gsd_file.append_microstate(µstate)?; + + drop(hoomd_gsd_file); + + let gsd_file = GsdFile::open(path, Mode::Read)?; + + assert!(gsd_file.n_frames() == 1); + + let step = gsd_file.iter_scalars::(0, "configuration/step")?; + itertools::assert_equal(step, [1234]); + + let positions: Vec<[f32; 3]> = gsd_file + .iter_arrays::(0, "particles/position")? + .collect(); + assert_relative_eq!(positions[0][0], (f32::sinh(1.2)) / (1.0 + f32::cosh(1.2))); + assert_relative_eq!(positions[0][1], 0.0); + assert_relative_eq!( + positions[1][0], + (f32::sinh(0.6) * f32::cos(1.5)) / (1.0 + f32::cosh(0.6)) + ); + assert_relative_eq!( + positions[1][1], + (f32::sinh(0.6) * f32::sin(1.5)) / (1.0 + f32::cosh(0.6)) + ); + Ok(()) + } + + #[test] + fn point_hyperbolic_2d_eighteight_periodic() -> anyhow::Result<()> { + let boundary = Periodic::new(0.6, EightEight {})?; + let microstate = Microstate::builder() + .boundary(boundary) + .bodies([ + Body::point(Hyperbolic::<3>::from_polar_coordinates(0.2, 0.3)), + Body::point(Hyperbolic::<3>::from_polar_coordinates(0.6, 0.7)), + ]) + .step(1234) + .try_build()?; + + let tmp_dir = tempdir()?; + let path = tmp_dir.path().join("test.gsd"); + let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?; + hoomd_gsd_file.append_microstate(µstate)?; + + drop(hoomd_gsd_file); + + let gsd_file = GsdFile::open(path, Mode::Read)?; + + assert!(gsd_file.n_frames() == 1); + + let step = gsd_file.iter_scalars::(0, "configuration/step")?; + itertools::assert_equal(step, [1234]); + + let positions: Vec<[f32; 3]> = gsd_file + .iter_arrays::(0, "particles/position")? + .collect(); + assert_relative_eq!( + positions[0][0], + (f32::sinh(0.2) * f32::cos(0.3)) / (1.0 + f32::cosh(0.2)) + ); + assert_relative_eq!( + positions[0][1], + (f32::sinh(0.2) * f32::sin(0.3)) / (1.0 + f32::cosh(0.2)) + ); + assert_relative_eq!( + positions[1][0], + (f32::sinh(0.6) * f32::cos(0.7)) / (1.0 + f32::cosh(0.6)) + ); + assert_relative_eq!( + positions[1][1], + (f32::sinh(0.6) * f32::sin(0.7)) / (1.0 + f32::cosh(0.6)) + ); + Ok(()) + } + + #[test] + fn oriented_point_hyperbolic_2d_open() -> anyhow::Result<()> { + let microstate = Microstate::builder() + .boundary(Open) + .bodies([ + Body { + properties: OrientedHyperbolicPoint { + position: Hyperbolic::<3>::from_polar_coordinates(0.5, 0.6), + orientation: Angle::from(0.3), + }, + sites: vec![OrientedHyperbolicPoint { + position: Hyperbolic::<3>::default(), + orientation: Angle::default(), + }], + }, + Body { + properties: OrientedHyperbolicPoint { + position: Hyperbolic::<3>::from_polar_coordinates(0.9, 0.3), + orientation: Angle::from(1.2), + }, + sites: vec![OrientedHyperbolicPoint { + position: Hyperbolic::<3>::default(), + orientation: Angle::default(), + }], + }, + ]) + .step(1234) + .try_build()?; + + let tmp_dir = tempdir()?; + let path = tmp_dir.path().join("test.gsd"); + let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?; + hoomd_gsd_file.append_microstate(µstate)?; + + drop(hoomd_gsd_file); + + let gsd_file = GsdFile::open(path, Mode::Read)?; + + assert!(gsd_file.n_frames() == 1); + + let step = gsd_file.iter_scalars::(0, "configuration/step")?; + itertools::assert_equal(step, [1234]); + + let positions: Vec<[f32; 3]> = gsd_file + .iter_arrays::(0, "particles/position")? + .collect(); + assert_relative_eq!( + positions[0][0], + (f32::sinh(0.5) * f32::cos(0.6)) / (1.0 + f32::cosh(0.5)) + ); + assert_relative_eq!( + positions[0][1], + (f32::sinh(0.5) * f32::sin(0.6)) / (1.0 + f32::cosh(0.5)) + ); + assert_relative_eq!( + positions[1][0], + (f32::sinh(0.9) * f32::cos(0.3)) / (1.0 + f32::cosh(0.9)) + ); + assert_relative_eq!( + positions[1][1], + (f32::sinh(0.9) * f32::sin(0.3)) / (1.0 + f32::cosh(0.9)) + ); + Ok(()) + } + + #[test] + fn oriented_point_hyperbolic_2d_periodic_eighteight() -> anyhow::Result<()> { + let boundary = Periodic::new(0.6, EightEight {})?; + let microstate = Microstate::builder() + .boundary(boundary) + .bodies([ + Body { + properties: OrientedHyperbolicPoint { + position: Hyperbolic::<3>::from_polar_coordinates(0.5, 0.6), + orientation: Angle::from(0.3), + }, + sites: vec![OrientedHyperbolicPoint { + position: Hyperbolic::<3>::default(), + orientation: Angle::default(), + }], + }, + Body { + properties: OrientedHyperbolicPoint { + position: Hyperbolic::<3>::from_polar_coordinates(0.9, 0.3), + orientation: Angle::from(1.2), + }, + sites: vec![OrientedHyperbolicPoint { + position: Hyperbolic::<3>::default(), + orientation: Angle::default(), + }], + }, + ]) + .step(1234) + .try_build()?; + + let tmp_dir = tempdir()?; + let path = tmp_dir.path().join("test.gsd"); + let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?; + hoomd_gsd_file.append_microstate(µstate)?; + + drop(hoomd_gsd_file); + + let gsd_file = GsdFile::open(path, Mode::Read)?; + + assert!(gsd_file.n_frames() == 1); + + let step = gsd_file.iter_scalars::(0, "configuration/step")?; + itertools::assert_equal(step, [1234]); + + let positions: Vec<[f32; 3]> = gsd_file + .iter_arrays::(0, "particles/position")? + .collect(); + assert_relative_eq!( + positions[0][0], + (f32::sinh(0.5) * f32::cos(0.6)) / (1.0 + f32::cosh(0.5)) + ); + assert_relative_eq!( + positions[0][1], + (f32::sinh(0.5) * f32::sin(0.6)) / (1.0 + f32::cosh(0.5)) + ); + assert_relative_eq!( + positions[1][0], + (f32::sinh(0.9) * f32::cos(0.3)) / (1.0 + f32::cosh(0.9)) + ); + assert_relative_eq!( + positions[1][1], + (f32::sinh(0.9) * f32::sin(0.3)) / (1.0 + f32::cosh(0.9)) + ); + Ok(()) + } + #[test] fn oriented_point_closed_rectangle_2d() -> anyhow::Result<()> { let boundary = Closed(Rectangle { diff --git a/hoomd-microstate/src/property/point.rs b/hoomd-microstate/src/property/point.rs index df19713c4..c289cfac6 100644 --- a/hoomd-microstate/src/property/point.rs +++ b/hoomd-microstate/src/property/point.rs @@ -222,7 +222,7 @@ impl Transform>> for Point> { /// Move `Point>` properties from the local body frame to the /// system frame. /// - /// All positions on the 3-sphere are associated with some $`SO(4)`$ + /// All positions on the 3-sphere are associated with some $`SU(2)`$ /// transformation which translates the origin to that position. The local /// body frame is the frame in which the body position is the origin. The /// position of the sites in the system frame is obtained by applying the @@ -230,6 +230,7 @@ impl Transform>> for Point> { /// local body frame. #[inline] fn transform(&self, site_properties: &Point>) -> Point> { + // TODO: implement quaternion representation of SO(4) let body_point = self.position.coordinates(); let body_phi_1 = (body_point[2].powi(2) + body_point[1].powi(2)) .sqrt() diff --git a/hoomd-rand/katex.html b/hoomd-rand/katex.html~trunk similarity index 100% rename from hoomd-rand/katex.html rename to hoomd-rand/katex.html~trunk diff --git a/hoomd-simulation/katex.html b/hoomd-simulation/katex.html~trunk similarity index 100% rename from hoomd-simulation/katex.html rename to hoomd-simulation/katex.html~trunk diff --git a/hoomd-spatial/katex.html b/hoomd-spatial/katex.html~trunk similarity index 100% rename from hoomd-spatial/katex.html rename to hoomd-spatial/katex.html~trunk diff --git a/hoomd-utility/katex.html b/hoomd-utility/katex.html~trunk similarity index 100% rename from hoomd-utility/katex.html rename to hoomd-utility/katex.html~trunk diff --git a/hoomd-vector/katex.html b/hoomd-vector/katex.html~trunk similarity index 100% rename from hoomd-vector/katex.html rename to hoomd-vector/katex.html~trunk diff --git a/hoomd-vector/src/quaternion.rs b/hoomd-vector/src/quaternion.rs index ed0ecfd4e..94094a019 100644 --- a/hoomd-vector/src/quaternion.rs +++ b/hoomd-vector/src/quaternion.rs @@ -542,6 +542,20 @@ impl Versor { }) } + /// Get the components of a [`Versor`]. + #[inline] + #[must_use] + pub fn get_components(&self) -> (f64, f64, f64, f64) { + let quaternion = self.get(); + let vec_components = quaternion.vector; + ( + quaternion.scalar, + vec_components[0], + vec_components[1], + vec_components[2], + ) + } + /// Normalize the versor. /// /// Nominally, all [`Versor`] instances retain a unit norm. Due to limited diff --git a/prek.toml b/prek.toml index 47067d757..2a2c4e585 100644 --- a/prek.toml +++ b/prek.toml @@ -39,14 +39,14 @@ hooks = [ [[repos]] repo = "https://github.com/rhysd/actionlint" -rev = "v1.7.11" +rev = "v1.7.12" hooks = [ { id = "actionlint" } ] [[repos]] repo = "https://github.com/mit-d/check-unicode" -rev = "v0.5.0" +rev = "v0.6.0" hooks = [ { id = "check-unicode", args = [