Skip to content

Commit e1b4822

Browse files
committed
Fix stabilizer set CW/CCW rotation direction
1 parent dc7a32c commit e1b4822

7 files changed

Lines changed: 131 additions & 195 deletions

File tree

crates/hypermath/src/vector.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,14 @@ impl Vector {
377377
ret
378378
}
379379

380+
/// Returns an arbitrary vector perpendicular to all vectors in `vectors`,
381+
/// or `None` if there is none within the given number of dimensions.
382+
pub fn arbitrary_perpendicular_to(ndim: u8, vectors: &[Vector]) -> Option<Self> {
383+
(0..ndim)
384+
.map(Vector::unit)
385+
.find_map(|v| v.rejected_from_all(vectors.iter().cloned()).normalize())
386+
}
387+
380388
/// Resizes the vector in-place, padding with zeros.
381389
pub fn resize(&mut self, ndim: u8) {
382390
self.0.resize(ndim as _, 0.0);

crates/hyperpuzzle_impl_symmetric/src/builder/twists.rs

Lines changed: 103 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,9 @@ impl TwistSystemProduct {
305305
stabilized_points.transform_by_group_element(&named_point_action, deorbiter),
306306
orbit.gizmo_pole_distance,
307307
));
308+
if orbit.gizmo_pole_distance <= 0.0 {
309+
bail!("stabilizer twist gizmo_pole_distance cannot be negative")
310+
}
308311
}
309312

310313
let mut named_point_set_orbits: Vec<(NamedPointSet, Float)> = vec![];
@@ -323,6 +326,9 @@ impl TwistSystemProduct {
323326
.map(|s| names.named_point_from_name(s))
324327
.try_collect()?;
325328
named_point_set_orbits.push((NamedPointSet::new(points)?, orbit.gizmo_pole_distance));
329+
if orbit.gizmo_pole_distance <= 0.0 {
330+
bail!("named point set gizmo_pole_distance cannot be negative")
331+
}
326332
}
327333

328334
// Assemble jumble moves.
@@ -704,12 +710,7 @@ impl TwistSystemProduct {
704710

705711
build_ctx.push_task("computing stabilized coset");
706712
let coset = subgroup_solver
707-
.solve(&hypergroup::ConstraintSet::from_iter(
708-
secondary
709-
.iter()
710-
.circular_tuple_windows()
711-
.map(|(from, to)| hypergroup::Constraint { from, to }),
712-
))
713+
.solve(&cycle_constraints(secondary.iter()))
713714
.ok_or_else(|| {
714715
eyre!(
715716
"stabilizer twist {:?} imposes unsatisfiable constraints \
@@ -722,11 +723,11 @@ impl TwistSystemProduct {
722723

723724
build_ctx.push_task("computing unit twist transform");
724725
let unit_twist_transform = if secondary.is_empty() {
725-
unit_twist_transform(&self.group, &coset, &[first_axis_vector])
726+
unit_twist_transform(&self.group, &coset, &[first_axis_vector.clone()])
726727
} else {
727728
let secondary_vector = secondary.vector(&self.named_point_vectors);
728-
let stabilized_vectors = &[first_axis_vector, &secondary_vector];
729-
unit_twist_transform(&self.group, &coset, stabilized_vectors)
729+
let stabilized_vectors = [first_axis_vector.clone(), secondary_vector];
730+
unit_twist_transform(&self.group, &coset, &stabilized_vectors)
730731
}
731732
.wrap_err_with(|| {
732733
format!(
@@ -812,7 +813,7 @@ impl TwistSystemProduct {
812813
fn unit_twist_transform(
813814
group: &IsometryGroup,
814815
stabilizer_coset: &ConjugateCoset,
815-
stabilized_vectors: &[&Vector],
816+
stabilized_vectors: &[Vector],
816817
) -> Result<UniqueMinimalClockwiseGenerator> {
817818
if stabilized_vectors.len() + 2 != group.ndim() as usize {
818819
bail!("`stabilized_vectors` must have length ndim-2");
@@ -823,30 +824,20 @@ fn unit_twist_transform(
823824
.filter(|&e| e != GroupElementId::IDENTITY)
824825
.filter(|&e| !group.is_reflection(e))
825826
.collect_vec();
826-
let order =
827-
NonZeroI32::new(nontrivial_rotations.len() as i32 + 1).ok_or_eyre("math is broken")?;
828827
let (mut min_group_element, min_rotation) = nontrivial_rotations
829828
.iter()
830829
.filter_map(|&e| Some((e, group.motor(e).normalize()?)))
831830
.max_by_float_key(|(_e, m)| m.scalar().abs())
832831
.ok_or_eyre("empty coset")?;
833-
let arbitrary_nonparallel_vector = Vector::unit(
834-
(0..group.ndim())
835-
.min_by_float_key(|&i| {
836-
stabilized_vectors
837-
.iter()
838-
.map(|v| v.get(i).abs())
839-
.max_float()
840-
.unwrap_or(0.0)
841-
})
842-
.unwrap_or(0),
843-
);
832+
let arbitrary_perpendicular_vector =
833+
Vector::arbitrary_perpendicular_to(group.ndim(), stabilized_vectors)
834+
.ok_or_eyre("stabilized vectors cannot span all of space")?;
844835
let orientation = Matrix::from_cols(
845836
std::iter::chain(
846-
stabilized_vectors.iter().copied(),
837+
stabilized_vectors,
847838
[
848-
&arbitrary_nonparallel_vector,
849-
&min_rotation.transform(&arbitrary_nonparallel_vector),
839+
&arbitrary_perpendicular_vector,
840+
&min_rotation.transform(&arbitrary_perpendicular_vector),
850841
],
851842
)
852843
.collect_vec(), // Chain does not impl ExactSizeIterator
@@ -855,8 +846,90 @@ fn unit_twist_transform(
855846
if orientation > 0.0 {
856847
min_group_element = group.inverse(min_group_element);
857848
}
858-
Ok(UniqueMinimalClockwiseGenerator {
859-
element: min_group_element,
860-
order,
861-
})
849+
850+
Ok(dbg!(UniqueMinimalClockwiseGenerator::new(
851+
group.abstract_group(),
852+
min_group_element,
853+
)))
854+
}
855+
856+
/// Constructs a constraint set for a cycle of points.
857+
fn cycle_constraints(
858+
points: impl Iterator<Item = NamedPoint> + Clone + ExactSizeIterator,
859+
) -> hypergroup::ConstraintSet<NamedPoint> {
860+
hypergroup::ConstraintSet::from_iter(
861+
points
862+
.circular_tuple_windows()
863+
.map(|(from, to)| hypergroup::Constraint { from, to }),
864+
)
865+
}
866+
867+
#[cfg(test)]
868+
mod tests {
869+
use hypergroup::GeneratorId;
870+
use hypermath::APPROX;
871+
872+
use super::*;
873+
874+
#[test]
875+
fn test_unit_twist_transform() -> Result<()> {
876+
let h3 = hypergroup::CoxeterMatrix::H3();
877+
let group =
878+
hypergroup::CoxeterMatrix::direct_product(&h3, &hypergroup::CoxeterMatrix::A(1)?)?
879+
.isometry_group()?;
880+
881+
let named_point_vectors: PerNamedPoint<Vector> = group
882+
.orbit_geometric(
883+
h3.mirror_basis()?.col(2).to_vector(),
884+
hypergroup::ORBIT_LIMIT,
885+
)?
886+
.into_iter()
887+
.map(|(_, v)| v)
888+
.collect();
889+
let points: PerNamedPoint<Point> = named_point_vectors.map_ref(|_, v| Point(v.clone()));
890+
891+
let named_point_action = group.action_on_points(&points)?;
892+
893+
let w = Vector::unit(3);
894+
895+
let subgroup_action = SubgroupAction::from_subgroup_predicate(&named_point_action, |e| {
896+
APPROX.eq(&group.motor(e).transform(&w), &w) // stabilize W axis
897+
})?;
898+
let mut subgroup_solver = SubgroupConstraintSolver::new(subgroup_action);
899+
900+
let g1 = group.generators()[GeneratorId(1)];
901+
let g2 = group.generators()[GeneratorId(2)];
902+
903+
let f = NamedPoint(0);
904+
let u = named_point_action.act(g2, f);
905+
let r = named_point_action.act(g1, u);
906+
907+
let mut check_unit_twist_transform = |stab: &[NamedPoint], period: usize| -> Result<()> {
908+
println!("Testing setwise stabilizer {:?} with period {period}", stab);
909+
910+
let secondary_vector: Vector = stab.iter().map(|&p| &named_point_vectors[p]).sum();
911+
let stabilized_vectors = [w.clone(), secondary_vector];
912+
913+
// unit_twist_transform() should always produce a clockwise
914+
// rotation, regardless of the input cycle direction.
915+
let forward_coset = subgroup_solver
916+
.solve(&cycle_constraints(stab.iter().copied()))
917+
.expect("unsat");
918+
let reverse_coset = subgroup_solver
919+
.solve(&cycle_constraints(stab.iter().rev().copied()))
920+
.expect("unsat");
921+
let unit1 = unit_twist_transform(&group, &forward_coset, &stabilized_vectors)?;
922+
let unit2 = unit_twist_transform(&group, &reverse_coset, &stabilized_vectors)?;
923+
assert_eq!(group.abstract_group().period(unit1.element), period);
924+
assert_eq!(group.abstract_group().period(unit2.element), period);
925+
assert_eq!(unit1, unit2);
926+
Ok(())
927+
};
928+
929+
check_unit_twist_transform(&[u], 5)?; // face twist
930+
check_unit_twist_transform(&[u, r], 2)?; // edge twist
931+
check_unit_twist_transform(&[u, r, f], 3)?; // vertex twist
932+
933+
Ok(())
934+
}
862935
}

crates/hyperpuzzle_impl_symmetric/src/twist_system.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,18 @@ pub struct UniqueMinimalClockwiseGenerator {
465465
pub order: NonZeroI32,
466466
}
467467

468+
impl UniqueMinimalClockwiseGenerator {
469+
/// Constructs a unique minimal clockwise generator struct using the given
470+
/// element, inferring the order of the group.
471+
pub fn new(group: &hypergroup::Group, element: GroupElementId) -> Self {
472+
Self {
473+
element,
474+
order: NonZeroI32::new(group.period(element) as i32)
475+
.expect("group element period is zero"),
476+
}
477+
}
478+
}
479+
468480
#[derive(thiserror::Error, Debug, Clone)]
469481
pub enum TwistError {
470482
#[error("unknown axis: {0:?}")]

crates/hypuz_notation/src/charsets.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ pub fn classify(c: char) -> Option<CharSet> {
2929
'ε' | 'η' | 'κ' | 'μ' | 'π' | 'τ' | 'φ' | 'ψ' | 'ω' => {
3030
Some(CharSet::ShortLowercaseGreek)
3131
}
32+
'_' => Some(CharSet::Underscore),
3233
_ => None,
3334
}
3435
}

hps/product/3d_catalan.hps

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ add_twist_system(
4242
]
4343
#twists.stabilizer_sets = [
4444
[["R", "U"], 1],
45-
[["R", "U", "F"], (1-2*√2)/√3], // TODO: why backwards???
45+
[["R", "U", "F"], (2*√2-1)/√3],
4646
]
4747
#twists.jumble_moves.UF = #{ j = "UR->RF" }
4848
#twists.jumble_stops.UF = ["j"]

hps/product/3d_platonic.hps

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,6 @@ add_twist_system(id = "icosahedron_ft@0", name = "FT Icosahedron",
195195
#twists.stabilizer_sets = [
196196
[["F", "R"], lerp(shape.edge.mag, dodeca.edge.mag/dodeca.vertex.mag, GIZMO_EDGE_FACTOR)],
197197
[["U", "F", "R", "E", "G"], lerp(shape.vertex.mag, dodeca.facet.mag/dodeca.vertex.mag, GIZMO_EDGE_FACTOR)],
198-
// [["R", "F", "U", "G", "E"], dodeca.facet.mag/dodeca.vertex.mag], // TODO: why wrong direction?
199-
// TODO: move input doesn't work for these twists? e.g., `bB_aU_aF_aR_aE_aG`
200198
]
201199

202200
#twists.jumble_moves.F = #{ j = "G->E" }
@@ -548,7 +546,7 @@ def_ft_icosahedron(
548546
tags = #{ author = ["Andrew Farkas", "Jason White"] },
549547
)
550548

551-
// TODO: Radio 1 needs piece deletion
549+
// TODO: Radio 1 needs piece deletion or conical cuts
552550
// def_ft_icosahedron(
553551
// 0.8,
554552
// id = "radio_1@0", name = ["Radiolarian 1"],

0 commit comments

Comments
 (0)