Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
df37962
Double quaternion/versor code
janbridley Jun 2, 2026
6d918a5
properly export
janbridley Jun 2, 2026
3d3fe33
from, l/r, etc
janbridley Jun 2, 2026
53e18cf
add l/r initialization
janbridley Jun 2, 2026
791ebbe
pub and docs
janbridley Jun 9, 2026
7ba6df8
must_use
janbridley Jun 9, 2026
fb80995
Correct text
janbridley Jun 11, 2026
e8462c2
Implement conversions from Matrix44 to RotationMatrix<4>
janbridley Jun 11, 2026
04618a7
Implement the direct rotation form for double quaternion rotations
janbridley Jun 11, 2026
f5307ce
Bugfix
janbridley Jun 11, 2026
4c2cd4d
Very basic test for roations
janbridley Jun 11, 2026
b086d1a
Fix from_l/r
janbridley Jun 11, 2026
e36084b
WIP on distributions
janbridley Jun 11, 2026
4d1c5f4
Rng
janbridley Jun 11, 2026
1fa8f5b
Add missing expect
janbridley Jun 11, 2026
b4dcce4
Try implementing DoubleQuaternion trial moves
janbridley Jun 12, 2026
9f6bdf7
Crate docs
janbridley Jun 12, 2026
f137e9d
lint
janbridley Jun 12, 2026
3d60d1e
Add missing derives
janbridley Jun 12, 2026
76dcd77
Better tests
janbridley Jun 12, 2026
ab34576
Remove redundant test
janbridley Jun 12, 2026
4b9dace
Fix combination convention
janbridley Jun 12, 2026
1129da7
Test combine then rotate and invert().invert()
janbridley Jun 12, 2026
d3ed37f
missing import
janbridley Jun 12, 2026
c211b09
Implement Metric for double versor
janbridley Jun 12, 2026
254a327
More docs
janbridley Jun 12, 2026
3f721ea
Remove decimals from math
janbridley Jun 12, 2026
f4e68bd
Swap internal helper to a method on quaternion
janbridley Jun 12, 2026
3fe2c17
Properly move to chordal distance
janbridley Jun 12, 2026
00f25cc
Make dot as cartesian pub crate
janbridley Jun 12, 2026
9abe980
Correct implementation of intrinsic distance
janbridley Jun 12, 2026
a353d7d
lints
janbridley Jun 12, 2026
f308eb2
Correct equations
janbridley Jun 12, 2026
cbf616b
Tests for orienation trial moves in SO(4)
janbridley Jun 12, 2026
ea4a396
lint
janbridley Jun 30, 2026
e598e9e
clippy
janbridley Jun 30, 2026
2c03cfb
lint
janbridley Jun 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions hoomd-mc/src/rotate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use std::{fmt, marker::PhantomData};
use hoomd_utility::valid::PositiveReal;

mod angle;
mod doubleversor;
mod versor;

/// Change the orientation of a body by a small amount.
Expand Down
176 changes: 176 additions & 0 deletions hoomd-mc/src/rotate/doubleversor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// Copyright (c) 2024-2026 The Regents of the University of Michigan.
// Part of hoomd-rs, released under the BSD 3-Clause License.

//! Implement [`DoubleVersor`] trial moves for 4-dimensional orientable bodies.

use std::f64::consts::PI;

use hoomd_microstate::property::Orientation;
use hoomd_utility::valid::PositiveReal;
use hoomd_vector::{DoubleVersor, Rotation};
use rand::Rng;
use rand_distr::Distribution;

use crate::{Adjust, LocalTrial, Rotate, rotate::versor::VersorDisplacement};

/// A normal distribution of random [`DoubleVersor`]s, centered on a mean with
/// some standard deviation.
#[derive(Debug, Clone, Copy)]
pub(crate) struct DoubleVersorDisplacement {
/// The standard deviation of the normal distribution of quaternions around the mean.
std_dev: f64,
}

impl From<f64> for DoubleVersorDisplacement {
#[inline]
fn from(value: f64) -> Self {
Self { std_dev: value }
}
}
impl Distribution<DoubleVersor> for DoubleVersorDisplacement {
/// Sample a random [`DoubleVersor`] displacement from a provided mean.
///
/// Mathematically, we sample from a 6-dimensional Normal distribution
/// in the tangent space of SO(4), lift to the manifold, then rotate to center on
/// the mean of the [`DoubleVersorDisplacement`]. The result is a small displacement
/// from a rotational input, with fast decay in the tails that make large
/// displacements unlikely. This is desirable for Monte Carlo, as large moves are
/// very likely to be rejected.
#[inline]
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> DoubleVersor {
let single_displacement = VersorDisplacement::from(self.std_dev);
// Sample two independent small rotations
let q_l = single_displacement.sample(rng);
let q_r = single_displacement.sample(rng);

// Combine them into a DoubleVersor
(q_l, q_r).into()
}
}

impl<B> LocalTrial<B> for Rotate<DoubleVersor>
where
B: Orientation<Rotation = DoubleVersor>,
{
/// Perturb a body's orientation by a random amount.
#[inline]
fn propose<R: Rng>(&self, rng: &mut R, body_properties: B) -> B {
let mut trial = body_properties;
let displacement = DoubleVersorDisplacement {
std_dev: self.maximum_rotation.get(),
};

let delta_quat = displacement.sample(rng);
*trial.orientation_mut() = delta_quat.combine(trial.orientation());

trial
}
}

impl Adjust for Rotate<DoubleVersor> {
/// Change the maximum trial move size by the given scale factor.
#[inline]
fn adjust(&mut self, factor: PositiveReal) {
self.maximum_rotation *= factor;

if self.maximum_rotation.get() > PI / 2.0 {
self.maximum_rotation = (PI / 2.0)
.try_into()
.expect("PI/2.0 should be a positive real");
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use assert2::check;
use hoomd_microstate::property::OrientedPoint;
use hoomd_vector::{Cartesian, DoubleVersor, Metric};
use rand::{SeedableRng, rngs::StdRng};
use rstest::*;

/// Number of trial moves to test.
const N: usize = 262_144;

/// Expected geodesic distance for small a:E[χ₆] = sqrt2*Γ(7/2)/Γ(3) = 15sqrt(2π)/16
#[inline]
fn expected_chi6_mean() -> f64 {
15.0 * (2.0 * PI).sqrt() / 16.0
}

#[rstest]
fn rotate(#[values(0.1, 0.5)] a: f64) {
let mut rng = StdRng::seed_from_u64(1);
let body = OrientedPoint {
position: Cartesian::from([0.0, 0.0, 0.0]),
orientation: DoubleVersor::default(),
};
let rotate = Rotate::with_maximum_rotation(
a.try_into()
.expect("hard-coded constant should be a positive real"),
);

let mut delta_thetas = Vec::with_capacity(N);
for _ in 0..N {
let trial = rotate.propose(&mut rng, body);
delta_thetas.push(trial.orientation.distance(&body.orientation));
}

let mean = delta_thetas.iter().sum::<f64>() / N as f64;
let expected = expected_chi6_mean() * a;
assert!(
(mean - expected).abs() < 0.01 * a, // Should be within a few percent
"mean = {mean}, expected = {expected}, diff = {}",
(mean - expected).abs(),
);
}

#[rstest]
fn rotate_mean_distance(#[values(1e-3, 0.1)] a: f64) {
// Two independent versor displacements give distance:
// sqrt(|sl|² + |sr|²) = a*\chi_6 for small displacements, so
// E[distance] = a * sqrt(2) * (15*sqrt(pi)/8)/2 = a * 15sqrt(2π)/16.
let expected = expected_chi6_mean() * a;

let mut rng = StdRng::seed_from_u64(1);
let body = OrientedPoint {
position: Cartesian::from([0.0, 0.0, 0.0]),
orientation: DoubleVersor::default(),
};
let rotate = Rotate::with_maximum_rotation(
a.try_into()
.expect("hard-coded constant should be a positive real"),
);

let sum: f64 = (0..N)
.map(|_| {
let trial = rotate.propose(&mut rng, body);
trial.orientation.distance(&body.orientation)
})
.sum();

let mean = sum / N as f64;
assert!(
(mean - expected).abs() < 0.001 * a,
"mean = {mean}, expected = {expected}, diff = {}",
(mean - expected).abs(),
);
}

#[test]
fn test_adjust() -> anyhow::Result<()> {
let mut rotate = Rotate::<DoubleVersor>::with_maximum_rotation(0.5.try_into()?);

rotate.adjust(2.0.try_into()?);
check!(rotate.maximum_rotation().get() == 1.0);

rotate.adjust(0.5.try_into()?);
check!(rotate.maximum_rotation().get() == 0.5);

rotate.adjust(10.0.try_into()?);
check!(rotate.maximum_rotation().get() == PI / 2.0);

Ok(())
}
}
9 changes: 9 additions & 0 deletions hoomd-vector/src/cartesian.rs
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,15 @@ impl<const N: usize> From<RotationMatrix<N>> for Matrix<N, N> {
}
}

impl<const N: usize> From<Matrix<N, N>> for RotationMatrix<N> {
#[inline]
fn from(value: Matrix<N, N>) -> Self {
Self {
rows: value.rows.map(Cartesian::from),
}
}
}

impl<const N: usize> RotationMatrix<N> {
/// Get the rows of the rotation matrix.
///
Expand Down
Loading