forked from glen/dyna3
Compare commits
2 commits
main
...
select-in-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f4c9d3d7f4 | ||
![]() |
17d4ed86e4 |
10 changed files with 47 additions and 770 deletions
|
@ -2,7 +2,7 @@ use dyna3::engine::{Q, irisawa::realize_irisawa_hexlet};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
const SCALED_TOL: f64 = 1.0e-12;
|
const SCALED_TOL: f64 = 1.0e-12;
|
||||||
let (config, _, success, history) = realize_irisawa_hexlet(SCALED_TOL);
|
let (config, success, history) = realize_irisawa_hexlet(SCALED_TOL);
|
||||||
print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config);
|
print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config);
|
||||||
if success {
|
if success {
|
||||||
println!("Target accuracy achieved!");
|
println!("Target accuracy achieved!");
|
||||||
|
|
|
@ -1,72 +0,0 @@
|
||||||
use nalgebra::{DMatrix, DVector};
|
|
||||||
use std::{array, f64::consts::PI};
|
|
||||||
|
|
||||||
use dyna3::engine::{Q, point, realize_gram, PartialMatrix};
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
// set up a kaleidocycle, made of points with fixed distances between them,
|
|
||||||
// and find its tangent space
|
|
||||||
const N_POINTS: usize = 12;
|
|
||||||
let gram = {
|
|
||||||
let mut gram_to_be = PartialMatrix::new();
|
|
||||||
for block in (0..N_POINTS).step_by(2) {
|
|
||||||
let block_next = (block + 2) % N_POINTS;
|
|
||||||
for j in 0..2 {
|
|
||||||
// diagonal and hinge edges
|
|
||||||
for k in j..2 {
|
|
||||||
gram_to_be.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// non-hinge edges
|
|
||||||
for k in 0..2 {
|
|
||||||
gram_to_be.push_sym(block + j, block_next + k, -0.625);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
gram_to_be
|
|
||||||
};
|
|
||||||
let guess = {
|
|
||||||
const N_HINGES: usize = 6;
|
|
||||||
let guess_elts = (0..N_HINGES).step_by(2).flat_map(
|
|
||||||
|n| {
|
|
||||||
let ang_hor = (n as f64) * PI/3.0;
|
|
||||||
let ang_vert = ((n + 1) as f64) * PI/3.0;
|
|
||||||
let x_vert = ang_vert.cos();
|
|
||||||
let y_vert = ang_vert.sin();
|
|
||||||
[
|
|
||||||
point(0.0, 0.0, 0.0),
|
|
||||||
point(ang_hor.cos(), ang_hor.sin(), 0.0),
|
|
||||||
point(x_vert, y_vert, -0.5),
|
|
||||||
point(x_vert, y_vert, 0.5)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
).collect::<Vec<_>>();
|
|
||||||
DMatrix::from_columns(&guess_elts)
|
|
||||||
};
|
|
||||||
let frozen: [_; N_POINTS] = array::from_fn(|k| (3, k));
|
|
||||||
let (config, tangent, success, history) = realize_gram(
|
|
||||||
&gram, guess, &frozen,
|
|
||||||
1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
|
||||||
);
|
|
||||||
print!("Completed Gram matrix:{}", config.tr_mul(&*Q) * &config);
|
|
||||||
print!("Configuration:{}", config);
|
|
||||||
if success {
|
|
||||||
println!("Target accuracy achieved!");
|
|
||||||
} else {
|
|
||||||
println!("Failed to reach target accuracy");
|
|
||||||
}
|
|
||||||
println!("Steps: {}", history.scaled_loss.len() - 1);
|
|
||||||
println!("Loss: {}\n", history.scaled_loss.last().unwrap());
|
|
||||||
|
|
||||||
// find the kaleidocycle's twist motion
|
|
||||||
let up = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]);
|
|
||||||
let down = -&up;
|
|
||||||
let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map(
|
|
||||||
|n| [
|
|
||||||
tangent.proj(&up.as_view(), n),
|
|
||||||
tangent.proj(&down.as_view(), n+1)
|
|
||||||
]
|
|
||||||
).sum();
|
|
||||||
let normalization = 5.0 / twist_motion[(2, 0)];
|
|
||||||
print!("Twist motion:{}", normalization * twist_motion);
|
|
||||||
}
|
|
|
@ -18,7 +18,7 @@ fn main() {
|
||||||
]);
|
]);
|
||||||
let frozen = [(3, 0)];
|
let frozen = [(3, 0)];
|
||||||
println!();
|
println!();
|
||||||
let (config, _, success, history) = realize_gram(
|
let (config, success, history) = realize_gram(
|
||||||
&gram, guess, &frozen,
|
&gram, guess, &frozen,
|
||||||
1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
||||||
);
|
);
|
||||||
|
|
|
@ -21,7 +21,7 @@ fn main() {
|
||||||
])
|
])
|
||||||
};
|
};
|
||||||
println!();
|
println!();
|
||||||
let (config, _, success, history) = realize_gram(
|
let (config, success, history) = realize_gram(
|
||||||
&gram, guess, &[],
|
&gram, guess, &[],
|
||||||
1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
||||||
);
|
);
|
||||||
|
|
|
@ -9,4 +9,3 @@
|
||||||
cargo run --example irisawa-hexlet
|
cargo run --example irisawa-hexlet
|
||||||
cargo run --example three-spheres
|
cargo run --example three-spheres
|
||||||
cargo run --example point-on-sphere
|
cargo run --example point-on-sphere
|
||||||
cargo run --example kaleidocycle
|
|
|
@ -1,11 +1,11 @@
|
||||||
use nalgebra::{DMatrix, DVector, DVectorView, Vector3};
|
use nalgebra::{DMatrix, DVector, Vector3};
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use slab::Slab;
|
use slab::Slab;
|
||||||
use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}};
|
use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}};
|
||||||
use sycamore::prelude::*;
|
use sycamore::prelude::*;
|
||||||
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
|
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
|
||||||
|
|
||||||
use crate::engine::{realize_gram, local_unif_to_std, ConfigSubspace, PartialMatrix};
|
use crate::engine::{realize_gram, PartialMatrix};
|
||||||
|
|
||||||
// the types of the keys we use to access an assembly's elements and constraints
|
// the types of the keys we use to access an assembly's elements and constraints
|
||||||
pub type ElementKey = usize;
|
pub type ElementKey = usize;
|
||||||
|
@ -33,9 +33,8 @@ pub struct Element {
|
||||||
pub serial: u64,
|
pub serial: u64,
|
||||||
|
|
||||||
// the configuration matrix column index that was assigned to this element
|
// the configuration matrix column index that was assigned to this element
|
||||||
// last time the assembly was realized, or `None` if the element has never
|
// last time the assembly was realized
|
||||||
// been through a realization
|
column_index: usize
|
||||||
column_index: Option<usize>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Element {
|
impl Element {
|
||||||
|
@ -63,7 +62,7 @@ impl Element {
|
||||||
representation: create_signal(representation),
|
representation: create_signal(representation),
|
||||||
constraints: create_signal(BTreeSet::default()),
|
constraints: create_signal(BTreeSet::default()),
|
||||||
serial: serial,
|
serial: serial,
|
||||||
column_index: None
|
column_index: 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,6 +110,7 @@ impl Element {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Constraint {
|
pub struct Constraint {
|
||||||
pub subjects: (ElementKey, ElementKey),
|
pub subjects: (ElementKey, ElementKey),
|
||||||
|
@ -120,14 +120,6 @@ pub struct Constraint {
|
||||||
pub active: Signal<bool>
|
pub active: Signal<bool>
|
||||||
}
|
}
|
||||||
|
|
||||||
// the velocity is expressed in uniform coordinates
|
|
||||||
pub struct ElementMotion<'a> {
|
|
||||||
pub key: ElementKey,
|
|
||||||
pub velocity: DVectorView<'a, f64>
|
|
||||||
}
|
|
||||||
|
|
||||||
type AssemblyMotion<'a> = Vec<ElementMotion<'a>>;
|
|
||||||
|
|
||||||
// a complete, view-independent description of an assembly
|
// a complete, view-independent description of an assembly
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Assembly {
|
pub struct Assembly {
|
||||||
|
@ -135,18 +127,6 @@ pub struct Assembly {
|
||||||
pub elements: Signal<Slab<Element>>,
|
pub elements: Signal<Slab<Element>>,
|
||||||
pub constraints: Signal<Slab<Constraint>>,
|
pub constraints: Signal<Slab<Constraint>>,
|
||||||
|
|
||||||
// solution variety tangent space. the basis vectors are stored in
|
|
||||||
// configuration matrix format, ordered according to the elements' column
|
|
||||||
// indices. when you realize the assembly, every element that's present
|
|
||||||
// during realization gets a column index and is reflected in the tangent
|
|
||||||
// space. since the methods in this module never assign column indices
|
|
||||||
// without later realizing the assembly, we get the following invariant:
|
|
||||||
//
|
|
||||||
// (1) if an element has a column index, its tangent motions can be found
|
|
||||||
// in that column of the tangent space basis matrices
|
|
||||||
//
|
|
||||||
pub tangent: Signal<ConfigSubspace>,
|
|
||||||
|
|
||||||
// indexing
|
// indexing
|
||||||
pub elements_by_id: Signal<FxHashMap<String, ElementKey>>
|
pub elements_by_id: Signal<FxHashMap<String, ElementKey>>
|
||||||
}
|
}
|
||||||
|
@ -156,7 +136,6 @@ impl Assembly {
|
||||||
Assembly {
|
Assembly {
|
||||||
elements: create_signal(Slab::new()),
|
elements: create_signal(Slab::new()),
|
||||||
constraints: create_signal(Slab::new()),
|
constraints: create_signal(Slab::new()),
|
||||||
tangent: create_signal(ConfigSubspace::zero(0)),
|
|
||||||
elements_by_id: create_signal(FxHashMap::default())
|
elements_by_id: create_signal(FxHashMap::default())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -220,7 +199,7 @@ impl Assembly {
|
||||||
// index the elements
|
// index the elements
|
||||||
self.elements.update_silent(|elts| {
|
self.elements.update_silent(|elts| {
|
||||||
for (index, (_, elt)) in elts.into_iter().enumerate() {
|
for (index, (_, elt)) in elts.into_iter().enumerate() {
|
||||||
elt.column_index = Some(index);
|
elt.column_index = index;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -232,8 +211,8 @@ impl Assembly {
|
||||||
for (_, cst) in csts {
|
for (_, cst) in csts {
|
||||||
if cst.active.get_untracked() && cst.lorentz_prod_valid.get_untracked() {
|
if cst.active.get_untracked() && cst.lorentz_prod_valid.get_untracked() {
|
||||||
let subjects = cst.subjects;
|
let subjects = cst.subjects;
|
||||||
let row = elts[subjects.0].column_index.unwrap();
|
let row = elts[subjects.0].column_index;
|
||||||
let col = elts[subjects.1].column_index.unwrap();
|
let col = elts[subjects.1].column_index;
|
||||||
gram_to_be.push_sym(row, col, cst.lorentz_prod.get_untracked());
|
gram_to_be.push_sym(row, col, cst.lorentz_prod.get_untracked());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -243,7 +222,7 @@ impl Assembly {
|
||||||
// Gram matrix
|
// Gram matrix
|
||||||
let mut guess_to_be = DMatrix::<f64>::zeros(5, elts.len());
|
let mut guess_to_be = DMatrix::<f64>::zeros(5, elts.len());
|
||||||
for (_, elt) in elts {
|
for (_, elt) in elts {
|
||||||
let index = elt.column_index.unwrap();
|
let index = elt.column_index;
|
||||||
gram_to_be.push_sym(index, index, 1.0);
|
gram_to_be.push_sym(index, index, 1.0);
|
||||||
guess_to_be.set_column(index, &elt.representation.get_clone_untracked());
|
guess_to_be.set_column(index, &elt.representation.get_clone_untracked());
|
||||||
}
|
}
|
||||||
|
@ -268,7 +247,7 @@ impl Assembly {
|
||||||
}
|
}
|
||||||
|
|
||||||
// look for a configuration with the given Gram matrix
|
// look for a configuration with the given Gram matrix
|
||||||
let (config, tangent, success, history) = realize_gram(
|
let (config, success, history) = realize_gram(
|
||||||
&gram, guess, &[],
|
&gram, guess, &[],
|
||||||
1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
||||||
);
|
);
|
||||||
|
@ -284,125 +263,14 @@ impl Assembly {
|
||||||
));
|
));
|
||||||
console::log_2(&JsValue::from("Steps:"), &JsValue::from(history.scaled_loss.len() - 1));
|
console::log_2(&JsValue::from("Steps:"), &JsValue::from(history.scaled_loss.len() - 1));
|
||||||
console::log_2(&JsValue::from("Loss:"), &JsValue::from(*history.scaled_loss.last().unwrap()));
|
console::log_2(&JsValue::from("Loss:"), &JsValue::from(*history.scaled_loss.last().unwrap()));
|
||||||
console::log_2(&JsValue::from("Tangent dimension:"), &JsValue::from(tangent.dim()));
|
|
||||||
|
|
||||||
if success {
|
if success {
|
||||||
// read out the solution
|
// read out the solution
|
||||||
for (_, elt) in self.elements.get_clone_untracked() {
|
for (_, elt) in self.elements.get_clone_untracked() {
|
||||||
elt.representation.update(
|
elt.representation.update(
|
||||||
|rep| rep.set_column(0, &config.column(elt.column_index.unwrap()))
|
|rep| rep.set_column(0, &config.column(elt.column_index))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// save the tangent space
|
|
||||||
self.tangent.set_silent(tangent);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- deformation ---
|
|
||||||
|
|
||||||
// project the given motion to the tangent space of the solution variety and
|
|
||||||
// move the assembly along it. the implementation is based on invariant (1)
|
|
||||||
// from above and the following additional invariant:
|
|
||||||
//
|
|
||||||
// (2) if an element is affected by a constraint, it has a column index
|
|
||||||
//
|
|
||||||
// we have this invariant because the assembly gets realized each time you
|
|
||||||
// add a constraint
|
|
||||||
pub fn deform(&self, motion: AssemblyMotion) {
|
|
||||||
/* KLUDGE */
|
|
||||||
// when the tangent space is zero, deformation won't do anything, but
|
|
||||||
// the attempt to deform should be registered in the UI. this console
|
|
||||||
// message will do for now
|
|
||||||
if self.tangent.with(|tan| tan.dim() <= 0 && tan.assembly_dim() > 0) {
|
|
||||||
console::log_1(&JsValue::from("The assembly is rigid"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// give a column index to each moving element that doesn't have one yet.
|
|
||||||
// this temporarily breaks invariant (1), but the invariant will be
|
|
||||||
// restored when we realize the assembly at the end of the deformation.
|
|
||||||
// in the process, we find out how many matrix columns we'll need to
|
|
||||||
// hold the deformation
|
|
||||||
let realized_dim = self.tangent.with(|tan| tan.assembly_dim());
|
|
||||||
let motion_dim = self.elements.update_silent(|elts| {
|
|
||||||
let mut next_column_index = realized_dim;
|
|
||||||
for elt_motion in motion.iter() {
|
|
||||||
let moving_elt = &mut elts[elt_motion.key];
|
|
||||||
if moving_elt.column_index.is_none() {
|
|
||||||
moving_elt.column_index = Some(next_column_index);
|
|
||||||
next_column_index += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
next_column_index
|
|
||||||
});
|
|
||||||
|
|
||||||
// project the element motions onto the tangent space of the solution
|
|
||||||
// variety and sum them to get a deformation of the whole assembly. the
|
|
||||||
// matrix `motion_proj` that holds the deformation has extra columns for
|
|
||||||
// any moving elements that aren't reflected in the saved tangent space
|
|
||||||
const ELEMENT_DIM: usize = 5;
|
|
||||||
let mut motion_proj = DMatrix::zeros(ELEMENT_DIM, motion_dim);
|
|
||||||
for elt_motion in motion {
|
|
||||||
// we can unwrap the column index because we know that every moving
|
|
||||||
// element has one at this point
|
|
||||||
let column_index = self.elements.with_untracked(
|
|
||||||
|elts| elts[elt_motion.key].column_index.unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
if column_index < realized_dim {
|
|
||||||
// this element had a column index when we started, so by
|
|
||||||
// invariant (1), it's reflected in the tangent space
|
|
||||||
let mut target_columns = motion_proj.columns_mut(0, realized_dim);
|
|
||||||
target_columns += self.tangent.with(
|
|
||||||
|tan| tan.proj(&elt_motion.velocity, column_index)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// this element didn't have a column index when we started, so
|
|
||||||
// by invariant (2), it's unconstrained
|
|
||||||
let mut target_column = motion_proj.column_mut(column_index);
|
|
||||||
let unif_to_std = self.elements.with_untracked(
|
|
||||||
|elts| {
|
|
||||||
elts[elt_motion.key].representation.with_untracked(
|
|
||||||
|rep| local_unif_to_std(rep.as_view())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
target_column += unif_to_std * elt_motion.velocity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// step the assembly along the deformation. this changes the elements'
|
|
||||||
// normalizations, so we restore those afterward
|
|
||||||
/* KLUDGE */
|
|
||||||
// since our test assemblies only include spheres, we assume that every
|
|
||||||
// element is on the 1 mass shell
|
|
||||||
for (_, elt) in self.elements.get_clone_untracked() {
|
|
||||||
elt.representation.update_silent(|rep| {
|
|
||||||
match elt.column_index {
|
|
||||||
Some(column_index) => {
|
|
||||||
// step the assembly along the deformation
|
|
||||||
*rep += motion_proj.column(column_index);
|
|
||||||
|
|
||||||
// restore normalization by contracting toward the last
|
|
||||||
// coordinate axis
|
|
||||||
let q_sp = rep.fixed_rows::<3>(0).norm_squared();
|
|
||||||
let half_q_lt = -2.0 * rep[3] * rep[4];
|
|
||||||
let half_q_lt_sq = half_q_lt * half_q_lt;
|
|
||||||
let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt();
|
|
||||||
rep.fixed_rows_mut::<4>(0).scale_mut(1.0 / scaling);
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
console::log_1(&JsValue::from(
|
|
||||||
format!("No velocity to unpack for fresh element \"{}\"", elt.id)
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// bring the configuration back onto the solution variety. this also
|
|
||||||
// gets the elements' column indices and the saved tangent space back in
|
|
||||||
// sync
|
|
||||||
self.realize();
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
use core::array;
|
use core::array;
|
||||||
use nalgebra::{DMatrix, DVector, Rotation3, Vector3};
|
use nalgebra::{DMatrix, Rotation3, Vector3};
|
||||||
use sycamore::{prelude::*, motion::create_raf};
|
use sycamore::{prelude::*, motion::create_raf};
|
||||||
use web_sys::{
|
use web_sys::{
|
||||||
console,
|
console,
|
||||||
|
@ -14,7 +14,7 @@ use web_sys::{
|
||||||
wasm_bindgen::{JsCast, JsValue}
|
wasm_bindgen::{JsCast, JsValue}
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{AppState, assembly::{ElementKey, ElementMotion}};
|
use crate::{AppState, assembly::ElementKey};
|
||||||
|
|
||||||
fn compile_shader(
|
fn compile_shader(
|
||||||
context: &WebGl2RenderingContext,
|
context: &WebGl2RenderingContext,
|
||||||
|
@ -123,16 +123,6 @@ pub fn Display() -> View {
|
||||||
let zoom_out = create_signal(0.0);
|
let zoom_out = create_signal(0.0);
|
||||||
let turntable = create_signal(false); /* BENCHMARKING */
|
let turntable = create_signal(false); /* BENCHMARKING */
|
||||||
|
|
||||||
// manipulation
|
|
||||||
let translate_neg_x = create_signal(0.0);
|
|
||||||
let translate_pos_x = create_signal(0.0);
|
|
||||||
let translate_neg_y = create_signal(0.0);
|
|
||||||
let translate_pos_y = create_signal(0.0);
|
|
||||||
let translate_neg_z = create_signal(0.0);
|
|
||||||
let translate_pos_z = create_signal(0.0);
|
|
||||||
let shrink_neg = create_signal(0.0);
|
|
||||||
let shrink_pos = create_signal(0.0);
|
|
||||||
|
|
||||||
// change listener
|
// change listener
|
||||||
let scene_changed = create_signal(true);
|
let scene_changed = create_signal(true);
|
||||||
create_effect(move || {
|
create_effect(move || {
|
||||||
|
@ -151,7 +141,6 @@ pub fn Display() -> View {
|
||||||
let mut frames_since_last_sample = 0;
|
let mut frames_since_last_sample = 0;
|
||||||
let mean_frame_interval = create_signal(0.0);
|
let mean_frame_interval = create_signal(0.0);
|
||||||
|
|
||||||
let assembly_for_raf = state.assembly.clone();
|
|
||||||
on_mount(move || {
|
on_mount(move || {
|
||||||
// timing
|
// timing
|
||||||
let mut last_time = 0.0;
|
let mut last_time = 0.0;
|
||||||
|
@ -164,10 +153,6 @@ pub fn Display() -> View {
|
||||||
let mut rotation = DMatrix::<f64>::identity(5, 5);
|
let mut rotation = DMatrix::<f64>::identity(5, 5);
|
||||||
let mut location_z: f64 = 5.0;
|
let mut location_z: f64 = 5.0;
|
||||||
|
|
||||||
// manipulation
|
|
||||||
const TRANSLATION_SPEED: f64 = 0.15; // in length units per second
|
|
||||||
const SHRINKING_SPEED: f64 = 0.15; // in length units per second
|
|
||||||
|
|
||||||
// display parameters
|
// display parameters
|
||||||
const OPACITY: f32 = 0.5; /* SCAFFOLDING */
|
const OPACITY: f32 = 0.5; /* SCAFFOLDING */
|
||||||
const HIGHLIGHT: f32 = 0.2; /* SCAFFOLDING */
|
const HIGHLIGHT: f32 = 0.2; /* SCAFFOLDING */
|
||||||
|
@ -288,16 +273,6 @@ pub fn Display() -> View {
|
||||||
let zoom_out_val = zoom_out.get();
|
let zoom_out_val = zoom_out.get();
|
||||||
let turntable_val = turntable.get(); /* BENCHMARKING */
|
let turntable_val = turntable.get(); /* BENCHMARKING */
|
||||||
|
|
||||||
// get the manipulation state
|
|
||||||
let translate_neg_x_val = translate_neg_x.get();
|
|
||||||
let translate_pos_x_val = translate_pos_x.get();
|
|
||||||
let translate_neg_y_val = translate_neg_y.get();
|
|
||||||
let translate_pos_y_val = translate_pos_y.get();
|
|
||||||
let translate_neg_z_val = translate_neg_z.get();
|
|
||||||
let translate_pos_z_val = translate_pos_z.get();
|
|
||||||
let shrink_neg_val = shrink_neg.get();
|
|
||||||
let shrink_pos_val = shrink_pos.get();
|
|
||||||
|
|
||||||
// update the assembly's orientation
|
// update the assembly's orientation
|
||||||
let ang_vel = {
|
let ang_vel = {
|
||||||
let pitch = pitch_up_val - pitch_down_val;
|
let pitch = pitch_up_val - pitch_down_val;
|
||||||
|
@ -323,44 +298,6 @@ pub fn Display() -> View {
|
||||||
let zoom = zoom_out_val - zoom_in_val;
|
let zoom = zoom_out_val - zoom_in_val;
|
||||||
location_z *= (time_step * ZOOM_SPEED * zoom).exp();
|
location_z *= (time_step * ZOOM_SPEED * zoom).exp();
|
||||||
|
|
||||||
// manipulate the assembly
|
|
||||||
if state.selection.with(|sel| sel.len() == 1) {
|
|
||||||
let sel = state.selection.with(
|
|
||||||
|sel| *sel.into_iter().next().unwrap()
|
|
||||||
);
|
|
||||||
let translate_x = translate_pos_x_val - translate_neg_x_val;
|
|
||||||
let translate_y = translate_pos_y_val - translate_neg_y_val;
|
|
||||||
let translate_z = translate_pos_z_val - translate_neg_z_val;
|
|
||||||
let shrink = shrink_pos_val - shrink_neg_val;
|
|
||||||
let translating =
|
|
||||||
translate_x != 0.0
|
|
||||||
|| translate_y != 0.0
|
|
||||||
|| translate_z != 0.0;
|
|
||||||
if translating || shrink != 0.0 {
|
|
||||||
let elt_motion = {
|
|
||||||
let u = if translating {
|
|
||||||
TRANSLATION_SPEED * Vector3::new(
|
|
||||||
translate_x, translate_y, translate_z
|
|
||||||
).normalize()
|
|
||||||
} else {
|
|
||||||
Vector3::zeros()
|
|
||||||
};
|
|
||||||
time_step * DVector::from_column_slice(
|
|
||||||
&[u[0], u[1], u[2], SHRINKING_SPEED * shrink]
|
|
||||||
)
|
|
||||||
};
|
|
||||||
assembly_for_raf.deform(
|
|
||||||
vec![
|
|
||||||
ElementMotion {
|
|
||||||
key: sel,
|
|
||||||
velocity: elt_motion.as_view()
|
|
||||||
}
|
|
||||||
]
|
|
||||||
);
|
|
||||||
scene_changed.set(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if scene_changed.get() {
|
if scene_changed.get() {
|
||||||
/* INSTRUMENTS */
|
/* INSTRUMENTS */
|
||||||
// measure mean frame interval
|
// measure mean frame interval
|
||||||
|
@ -479,7 +416,7 @@ pub fn Display() -> View {
|
||||||
start_animation_loop();
|
start_animation_loop();
|
||||||
});
|
});
|
||||||
|
|
||||||
let set_nav_signal = move |event: &KeyboardEvent, value: f64| {
|
let set_nav_signal = move |event: KeyboardEvent, value: f64| {
|
||||||
let mut navigating = true;
|
let mut navigating = true;
|
||||||
let shift = event.shift_key();
|
let shift = event.shift_key();
|
||||||
match event.key().as_str() {
|
match event.key().as_str() {
|
||||||
|
@ -499,25 +436,6 @@ pub fn Display() -> View {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let set_manip_signal = move |event: &KeyboardEvent, value: f64| {
|
|
||||||
let mut manipulating = true;
|
|
||||||
let shift = event.shift_key();
|
|
||||||
match event.key().as_str() {
|
|
||||||
"d" | "D" => translate_pos_x.set(value),
|
|
||||||
"a" | "A" => translate_neg_x.set(value),
|
|
||||||
"w" | "W" if shift => translate_neg_z.set(value),
|
|
||||||
"s" | "S" if shift => translate_pos_z.set(value),
|
|
||||||
"w" | "W" => translate_pos_y.set(value),
|
|
||||||
"s" | "S" => translate_neg_y.set(value),
|
|
||||||
"]" | "}" => shrink_neg.set(value),
|
|
||||||
"[" | "{" => shrink_pos.set(value),
|
|
||||||
_ => manipulating = false
|
|
||||||
};
|
|
||||||
if manipulating {
|
|
||||||
event.prevent_default();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
/* TO DO */
|
/* TO DO */
|
||||||
// switch back to integer-valued parameters when that becomes possible
|
// switch back to integer-valued parameters when that becomes possible
|
||||||
|
@ -529,7 +447,6 @@ pub fn Display() -> View {
|
||||||
tabindex="0",
|
tabindex="0",
|
||||||
on:keydown=move |event: KeyboardEvent| {
|
on:keydown=move |event: KeyboardEvent| {
|
||||||
if event.key() == "Shift" {
|
if event.key() == "Shift" {
|
||||||
// swap navigation inputs
|
|
||||||
roll_cw.set(yaw_right.get());
|
roll_cw.set(yaw_right.get());
|
||||||
roll_ccw.set(yaw_left.get());
|
roll_ccw.set(yaw_left.get());
|
||||||
zoom_in.set(pitch_up.get());
|
zoom_in.set(pitch_up.get());
|
||||||
|
@ -538,24 +455,16 @@ pub fn Display() -> View {
|
||||||
yaw_left.set(0.0);
|
yaw_left.set(0.0);
|
||||||
pitch_up.set(0.0);
|
pitch_up.set(0.0);
|
||||||
pitch_down.set(0.0);
|
pitch_down.set(0.0);
|
||||||
|
|
||||||
// swap manipulation inputs
|
|
||||||
translate_pos_z.set(translate_neg_y.get());
|
|
||||||
translate_neg_z.set(translate_pos_y.get());
|
|
||||||
translate_pos_y.set(0.0);
|
|
||||||
translate_neg_y.set(0.0);
|
|
||||||
} else {
|
} else {
|
||||||
if event.key() == "Enter" { /* BENCHMARKING */
|
if event.key() == "Enter" { /* BENCHMARKING */
|
||||||
turntable.set_fn(|turn| !turn);
|
turntable.set_fn(|turn| !turn);
|
||||||
scene_changed.set(true);
|
scene_changed.set(true);
|
||||||
}
|
}
|
||||||
set_nav_signal(&event, 1.0);
|
set_nav_signal(event, 1.0);
|
||||||
set_manip_signal(&event, 1.0);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
on:keyup=move |event: KeyboardEvent| {
|
on:keyup=move |event: KeyboardEvent| {
|
||||||
if event.key() == "Shift" {
|
if event.key() == "Shift" {
|
||||||
// swap navigation inputs
|
|
||||||
yaw_right.set(roll_cw.get());
|
yaw_right.set(roll_cw.get());
|
||||||
yaw_left.set(roll_ccw.get());
|
yaw_left.set(roll_ccw.get());
|
||||||
pitch_up.set(zoom_in.get());
|
pitch_up.set(zoom_in.get());
|
||||||
|
@ -564,15 +473,8 @@ pub fn Display() -> View {
|
||||||
roll_ccw.set(0.0);
|
roll_ccw.set(0.0);
|
||||||
zoom_in.set(0.0);
|
zoom_in.set(0.0);
|
||||||
zoom_out.set(0.0);
|
zoom_out.set(0.0);
|
||||||
|
|
||||||
// swap manipulation inputs
|
|
||||||
translate_pos_y.set(translate_neg_z.get());
|
|
||||||
translate_neg_y.set(translate_pos_z.get());
|
|
||||||
translate_pos_z.set(0.0);
|
|
||||||
translate_neg_z.set(0.0);
|
|
||||||
} else {
|
} else {
|
||||||
set_nav_signal(&event, 0.0);
|
set_nav_signal(event, 0.0);
|
||||||
set_manip_signal(&event, 0.0);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
on:blur=move |_| {
|
on:blur=move |_| {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use nalgebra::{Const, DMatrix, DVector, DVectorView, Dyn, SymmetricEigen};
|
use nalgebra::{Const, DMatrix, DVector, Dyn};
|
||||||
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
|
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
|
||||||
|
|
||||||
// --- elements ---
|
// --- elements ---
|
||||||
|
@ -85,92 +85,6 @@ impl PartialMatrix {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- configuration subspaces ---
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct ConfigSubspace {
|
|
||||||
assembly_dim: usize,
|
|
||||||
basis_std: Vec<DMatrix<f64>>,
|
|
||||||
basis_proj: Vec<DMatrix<f64>>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConfigSubspace {
|
|
||||||
pub fn zero(assembly_dim: usize) -> ConfigSubspace {
|
|
||||||
ConfigSubspace {
|
|
||||||
assembly_dim: assembly_dim,
|
|
||||||
basis_proj: Vec::new(),
|
|
||||||
basis_std: Vec::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// approximate the kernel of a symmetric endomorphism of the configuration
|
|
||||||
// space for `assembly_dim` elements. we consider an eigenvector to be part
|
|
||||||
// of the kernel if its eigenvalue is smaller than the constant `THRESHOLD`
|
|
||||||
fn symmetric_kernel(a: DMatrix<f64>, proj_to_std: DMatrix<f64>, assembly_dim: usize) -> ConfigSubspace {
|
|
||||||
// find a basis for the kernel. the basis is expressed in the projection
|
|
||||||
// coordinates, and it's orthonormal with respect to the projection
|
|
||||||
// inner product
|
|
||||||
const THRESHOLD: f64 = 0.1;
|
|
||||||
let eig = SymmetricEigen::new(proj_to_std.tr_mul(&a) * &proj_to_std);
|
|
||||||
let eig_vecs = eig.eigenvectors.column_iter();
|
|
||||||
let eig_pairs = eig.eigenvalues.iter().zip(eig_vecs);
|
|
||||||
let basis_proj = DMatrix::from_columns(
|
|
||||||
eig_pairs.filter_map(
|
|
||||||
|(λ, v)| (λ.abs() < THRESHOLD).then_some(v)
|
|
||||||
).collect::<Vec<_>>().as_slice()
|
|
||||||
);
|
|
||||||
|
|
||||||
/* DEBUG */
|
|
||||||
// print the eigenvalues
|
|
||||||
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
|
|
||||||
console::log_1(&JsValue::from(
|
|
||||||
format!("Eigenvalues used to find kernel:{}", eig.eigenvalues)
|
|
||||||
));
|
|
||||||
|
|
||||||
// express the basis in the standard coordinates
|
|
||||||
let basis_std = proj_to_std * &basis_proj;
|
|
||||||
|
|
||||||
const ELEMENT_DIM: usize = 5;
|
|
||||||
const UNIFORM_DIM: usize = 4;
|
|
||||||
ConfigSubspace {
|
|
||||||
assembly_dim: assembly_dim,
|
|
||||||
basis_std: basis_std.column_iter().map(
|
|
||||||
|v| Into::<DMatrix<f64>>::into(
|
|
||||||
v.reshape_generic(Dyn(ELEMENT_DIM), Dyn(assembly_dim))
|
|
||||||
)
|
|
||||||
).collect(),
|
|
||||||
basis_proj: basis_proj.column_iter().map(
|
|
||||||
|v| Into::<DMatrix<f64>>::into(
|
|
||||||
v.reshape_generic(Dyn(UNIFORM_DIM), Dyn(assembly_dim))
|
|
||||||
)
|
|
||||||
).collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dim(&self) -> usize {
|
|
||||||
self.basis_std.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn assembly_dim(&self) -> usize {
|
|
||||||
self.assembly_dim
|
|
||||||
}
|
|
||||||
|
|
||||||
// find the projection onto this subspace of the motion where the element
|
|
||||||
// with the given column index has velocity `v`. the velocity is given in
|
|
||||||
// projection coordinates, and the projection is done with respect to the
|
|
||||||
// projection inner product
|
|
||||||
pub fn proj(&self, v: &DVectorView<f64>, column_index: usize) -> DMatrix<f64> {
|
|
||||||
if self.dim() == 0 {
|
|
||||||
const ELEMENT_DIM: usize = 5;
|
|
||||||
DMatrix::zeros(ELEMENT_DIM, self.assembly_dim)
|
|
||||||
} else {
|
|
||||||
self.basis_proj.iter().zip(self.basis_std.iter()).map(
|
|
||||||
|(b_proj, b_std)| b_proj.column(column_index).dot(&v) * b_std
|
|
||||||
).sum()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- descent history ---
|
// --- descent history ---
|
||||||
|
|
||||||
pub struct DescentHistory {
|
pub struct DescentHistory {
|
||||||
|
@ -232,37 +146,6 @@ fn basis_matrix(index: (usize, usize), nrows: usize, ncols: usize) -> DMatrix<f6
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
// given a normalized vector `v` representing an element, build a basis for the
|
|
||||||
// element's linear configuration space consisting of:
|
|
||||||
// - the unit translation motions of the element
|
|
||||||
// - the unit shrinking motion of the element, if it's a sphere
|
|
||||||
// - one or two vectors whose coefficients vanish on the tangent space of the
|
|
||||||
// normalization variety
|
|
||||||
pub fn local_unif_to_std(v: DVectorView<f64>) -> DMatrix<f64> {
|
|
||||||
const ELEMENT_DIM: usize = 5;
|
|
||||||
const UNIFORM_DIM: usize = 4;
|
|
||||||
let curv = 2.0*v[3];
|
|
||||||
if v.dot(&(&*Q * v)) < 0.5 {
|
|
||||||
// `v` represents a point. the normalization condition says that the
|
|
||||||
// curvature component of `v` is 1/2
|
|
||||||
DMatrix::from_column_slice(ELEMENT_DIM, UNIFORM_DIM, &[
|
|
||||||
curv, 0.0, 0.0, 0.0, v[0],
|
|
||||||
0.0, curv, 0.0, 0.0, v[1],
|
|
||||||
0.0, 0.0, curv, 0.0, v[2],
|
|
||||||
0.0, 0.0, 0.0, 0.0, 1.0
|
|
||||||
])
|
|
||||||
} else {
|
|
||||||
// `v` represents a sphere. the normalization condition says that the
|
|
||||||
// Lorentz product of `v` with itself is 1
|
|
||||||
DMatrix::from_column_slice(ELEMENT_DIM, UNIFORM_DIM, &[
|
|
||||||
curv, 0.0, 0.0, 0.0, v[0],
|
|
||||||
0.0, curv, 0.0, 0.0, v[1],
|
|
||||||
0.0, 0.0, curv, 0.0, v[2],
|
|
||||||
curv*v[0], curv*v[1], curv*v[2], curv*v[3], curv*v[4] + 1.0
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// use backtracking line search to find a better configuration
|
// use backtracking line search to find a better configuration
|
||||||
fn seek_better_config(
|
fn seek_better_config(
|
||||||
gram: &PartialMatrix,
|
gram: &PartialMatrix,
|
||||||
|
@ -298,7 +181,7 @@ pub fn realize_gram(
|
||||||
reg_scale: f64,
|
reg_scale: f64,
|
||||||
max_descent_steps: i32,
|
max_descent_steps: i32,
|
||||||
max_backoff_steps: i32
|
max_backoff_steps: i32
|
||||||
) -> (DMatrix<f64>, ConfigSubspace, bool, DescentHistory) {
|
) -> (DMatrix<f64>, bool, DescentHistory) {
|
||||||
// start the descent history
|
// start the descent history
|
||||||
let mut history = DescentHistory::new();
|
let mut history = DescentHistory::new();
|
||||||
|
|
||||||
|
@ -318,8 +201,12 @@ pub fn realize_gram(
|
||||||
|
|
||||||
// use Newton's method with backtracking and gradient descent backup
|
// use Newton's method with backtracking and gradient descent backup
|
||||||
let mut state = SearchState::from_config(gram, guess);
|
let mut state = SearchState::from_config(gram, guess);
|
||||||
let mut hess = DMatrix::zeros(element_dim, assembly_dim);
|
|
||||||
for _ in 0..max_descent_steps {
|
for _ in 0..max_descent_steps {
|
||||||
|
// stop if the loss is tolerably low
|
||||||
|
history.config.push(state.config.clone());
|
||||||
|
history.scaled_loss.push(state.loss / scale_adjustment);
|
||||||
|
if state.loss < tol { break; }
|
||||||
|
|
||||||
// find the negative gradient of the loss function
|
// find the negative gradient of the loss function
|
||||||
let neg_grad = 4.0 * &*Q * &state.config * &state.err_proj;
|
let neg_grad = 4.0 * &*Q * &state.config * &state.err_proj;
|
||||||
let mut neg_grad_stacked = neg_grad.clone().reshape_generic(Dyn(total_dim), Const::<1>);
|
let mut neg_grad_stacked = neg_grad.clone().reshape_generic(Dyn(total_dim), Const::<1>);
|
||||||
|
@ -342,7 +229,7 @@ pub fn realize_gram(
|
||||||
hess_cols.push(deriv_grad.reshape_generic(Dyn(total_dim), Const::<1>));
|
hess_cols.push(deriv_grad.reshape_generic(Dyn(total_dim), Const::<1>));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
hess = DMatrix::from_columns(hess_cols.as_slice());
|
let mut hess = DMatrix::from_columns(hess_cols.as_slice());
|
||||||
|
|
||||||
// regularize the Hessian
|
// regularize the Hessian
|
||||||
let min_eigval = hess.symmetric_eigenvalues().min();
|
let min_eigval = hess.symmetric_eigenvalues().min();
|
||||||
|
@ -362,11 +249,6 @@ pub fn realize_gram(
|
||||||
hess[(k, k)] = 1.0;
|
hess[(k, k)] = 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// stop if the loss is tolerably low
|
|
||||||
history.config.push(state.config.clone());
|
|
||||||
history.scaled_loss.push(state.loss / scale_adjustment);
|
|
||||||
if state.loss < tol { break; }
|
|
||||||
|
|
||||||
// compute the Newton step
|
// compute the Newton step
|
||||||
/*
|
/*
|
||||||
we need to either handle or eliminate the case where the minimum
|
we need to either handle or eliminate the case where the minimum
|
||||||
|
@ -374,7 +256,7 @@ pub fn realize_gram(
|
||||||
singular. right now, this causes the Cholesky decomposition to return
|
singular. right now, this causes the Cholesky decomposition to return
|
||||||
`None`, leading to a panic when we unrap
|
`None`, leading to a panic when we unrap
|
||||||
*/
|
*/
|
||||||
let base_step_stacked = hess.clone().cholesky().unwrap().solve(&neg_grad_stacked);
|
let base_step_stacked = hess.cholesky().unwrap().solve(&neg_grad_stacked);
|
||||||
let base_step = base_step_stacked.reshape_generic(Dyn(element_dim), Dyn(assembly_dim));
|
let base_step = base_step_stacked.reshape_generic(Dyn(element_dim), Dyn(assembly_dim));
|
||||||
history.base_step.push(base_step.clone());
|
history.base_step.push(base_step.clone());
|
||||||
|
|
||||||
|
@ -387,28 +269,10 @@ pub fn realize_gram(
|
||||||
state = better_state;
|
state = better_state;
|
||||||
history.backoff_steps.push(backoff_steps);
|
history.backoff_steps.push(backoff_steps);
|
||||||
},
|
},
|
||||||
None => return (state.config, ConfigSubspace::zero(assembly_dim), false, history)
|
None => return (state.config, false, history)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
let success = state.loss < tol;
|
(state.config, state.loss < tol, history)
|
||||||
let tangent = if success {
|
|
||||||
// express the uniform basis in the standard basis
|
|
||||||
const UNIFORM_DIM: usize = 4;
|
|
||||||
let total_dim_unif = UNIFORM_DIM * assembly_dim;
|
|
||||||
let mut unif_to_std = DMatrix::<f64>::zeros(total_dim, total_dim_unif);
|
|
||||||
for n in 0..assembly_dim {
|
|
||||||
let block_start = (element_dim * n, UNIFORM_DIM * n);
|
|
||||||
unif_to_std
|
|
||||||
.view_mut(block_start, (element_dim, UNIFORM_DIM))
|
|
||||||
.copy_from(&local_unif_to_std(state.config.column(n)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// find the kernel of the Hessian. give it the uniform inner product
|
|
||||||
ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim)
|
|
||||||
} else {
|
|
||||||
ConfigSubspace::zero(assembly_dim)
|
|
||||||
};
|
|
||||||
(state.config, tangent, success, history)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- tests ---
|
// --- tests ---
|
||||||
|
@ -427,7 +291,7 @@ pub mod irisawa {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix<f64>, ConfigSubspace, bool, DescentHistory) {
|
pub fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix<f64>, bool, DescentHistory) {
|
||||||
let gram = {
|
let gram = {
|
||||||
let mut gram_to_be = PartialMatrix::new();
|
let mut gram_to_be = PartialMatrix::new();
|
||||||
for s in 0..9 {
|
for s in 0..9 {
|
||||||
|
@ -484,9 +348,6 @@ pub mod irisawa {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use nalgebra::Vector3;
|
|
||||||
use std::{array, f64::consts::{FRAC_1_SQRT_2, PI}, iter};
|
|
||||||
|
|
||||||
use super::{*, irisawa::realize_irisawa_hexlet};
|
use super::{*, irisawa::realize_irisawa_hexlet};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -538,7 +399,7 @@ mod tests {
|
||||||
fn irisawa_hexlet_test() {
|
fn irisawa_hexlet_test() {
|
||||||
// solve Irisawa's problem
|
// solve Irisawa's problem
|
||||||
const SCALED_TOL: f64 = 1.0e-12;
|
const SCALED_TOL: f64 = 1.0e-12;
|
||||||
let (config, _, _, _) = realize_irisawa_hexlet(SCALED_TOL);
|
let (config, _, _) = realize_irisawa_hexlet(SCALED_TOL);
|
||||||
|
|
||||||
// check against Irisawa's solution
|
// check against Irisawa's solution
|
||||||
let entry_tol = SCALED_TOL.sqrt();
|
let entry_tol = SCALED_TOL.sqrt();
|
||||||
|
@ -548,285 +409,6 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tangent_test_three_spheres() {
|
|
||||||
const SCALED_TOL: f64 = 1.0e-12;
|
|
||||||
let gram = {
|
|
||||||
let mut gram_to_be = PartialMatrix::new();
|
|
||||||
for j in 0..3 {
|
|
||||||
for k in j..3 {
|
|
||||||
gram_to_be.push_sym(j, k, if j == k { 1.0 } else { -1.0 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
gram_to_be
|
|
||||||
};
|
|
||||||
let guess = DMatrix::from_columns(&[
|
|
||||||
sphere(0.0, 0.0, 0.0, -2.0),
|
|
||||||
sphere(0.0, 0.0, 1.0, 1.0),
|
|
||||||
sphere(0.0, 0.0, -1.0, 1.0)
|
|
||||||
]);
|
|
||||||
let frozen: [_; 5] = std::array::from_fn(|k| (k, 0));
|
|
||||||
let (config, tangent, success, history) = realize_gram(
|
|
||||||
&gram, guess.clone(), &frozen,
|
|
||||||
SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
|
|
||||||
);
|
|
||||||
assert_eq!(config, guess);
|
|
||||||
assert_eq!(success, true);
|
|
||||||
assert_eq!(history.scaled_loss.len(), 1);
|
|
||||||
|
|
||||||
// confirm that the tangent space has dimension five or less
|
|
||||||
assert_eq!(tangent.basis_std.len(), 5);
|
|
||||||
|
|
||||||
// confirm that the tangent space contains all the motions we expect it
|
|
||||||
// to. since we've already bounded the dimension of the tangent space,
|
|
||||||
// this confirms that the tangent space is what we expect it to be
|
|
||||||
const UNIFORM_DIM: usize = 4;
|
|
||||||
let element_dim = guess.nrows();
|
|
||||||
let assembly_dim = guess.ncols();
|
|
||||||
let tangent_motions_unif = vec![
|
|
||||||
basis_matrix((0, 1), UNIFORM_DIM, assembly_dim),
|
|
||||||
basis_matrix((1, 1), UNIFORM_DIM, assembly_dim),
|
|
||||||
basis_matrix((0, 2), UNIFORM_DIM, assembly_dim),
|
|
||||||
basis_matrix((1, 2), UNIFORM_DIM, assembly_dim),
|
|
||||||
DMatrix::<f64>::from_column_slice(UNIFORM_DIM, assembly_dim, &[
|
|
||||||
0.0, 0.0, 0.0, 0.0,
|
|
||||||
0.0, 0.0, -0.5, -0.5,
|
|
||||||
0.0, 0.0, -0.5, 0.5
|
|
||||||
])
|
|
||||||
];
|
|
||||||
let tangent_motions_std = vec![
|
|
||||||
basis_matrix((0, 1), element_dim, assembly_dim),
|
|
||||||
basis_matrix((1, 1), element_dim, assembly_dim),
|
|
||||||
basis_matrix((0, 2), element_dim, assembly_dim),
|
|
||||||
basis_matrix((1, 2), element_dim, assembly_dim),
|
|
||||||
DMatrix::<f64>::from_column_slice(element_dim, assembly_dim, &[
|
|
||||||
0.0, 0.0, 0.0, 0.00, 0.0,
|
|
||||||
0.0, 0.0, -1.0, -0.25, -1.0,
|
|
||||||
0.0, 0.0, -1.0, 0.25, 1.0
|
|
||||||
])
|
|
||||||
];
|
|
||||||
let tol_sq = ((element_dim * assembly_dim) as f64) * SCALED_TOL * SCALED_TOL;
|
|
||||||
for (motion_unif, motion_std) in tangent_motions_unif.into_iter().zip(tangent_motions_std) {
|
|
||||||
let motion_proj: DMatrix<_> = motion_unif.column_iter().enumerate().map(
|
|
||||||
|(k, v)| tangent.proj(&v, k)
|
|
||||||
).sum();
|
|
||||||
assert!((motion_std - motion_proj).norm_squared() < tol_sq);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn translation_motion_unif(vel: &Vector3<f64>, assembly_dim: usize) -> Vec<DVector<f64>> {
|
|
||||||
let mut elt_motion = DVector::zeros(4);
|
|
||||||
elt_motion.fixed_rows_mut::<3>(0).copy_from(vel);
|
|
||||||
iter::repeat(elt_motion).take(assembly_dim).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rotation_motion_unif(ang_vel: &Vector3<f64>, points: Vec<DVectorView<f64>>) -> Vec<DVector<f64>> {
|
|
||||||
points.into_iter().map(
|
|
||||||
|pt| {
|
|
||||||
let vel = ang_vel.cross(&pt.fixed_rows::<3>(0));
|
|
||||||
let mut elt_motion = DVector::zeros(4);
|
|
||||||
elt_motion.fixed_rows_mut::<3>(0).copy_from(&vel);
|
|
||||||
elt_motion
|
|
||||||
}
|
|
||||||
).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tangent_test_kaleidocycle() {
|
|
||||||
// set up a kaleidocycle, made of points with fixed distances between
|
|
||||||
// them, and find its tangent space
|
|
||||||
const N_POINTS: usize = 12;
|
|
||||||
const N_HINGES: usize = 6;
|
|
||||||
const SCALED_TOL: f64 = 1.0e-12;
|
|
||||||
let gram = {
|
|
||||||
let mut gram_to_be = PartialMatrix::new();
|
|
||||||
for block in (0..N_POINTS).step_by(2) {
|
|
||||||
let block_next = (block + 2) % N_POINTS;
|
|
||||||
for j in 0..2 {
|
|
||||||
// diagonal and hinge edges
|
|
||||||
for k in j..2 {
|
|
||||||
gram_to_be.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// non-hinge edges
|
|
||||||
for k in 0..2 {
|
|
||||||
gram_to_be.push_sym(block + j, block_next + k, -0.625);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
gram_to_be
|
|
||||||
};
|
|
||||||
let guess = {
|
|
||||||
let guess_elts = (0..N_HINGES).step_by(2).flat_map(
|
|
||||||
|n| {
|
|
||||||
let ang_hor = (n as f64) * PI/3.0;
|
|
||||||
let ang_vert = ((n + 1) as f64) * PI/3.0;
|
|
||||||
let x_vert = ang_vert.cos();
|
|
||||||
let y_vert = ang_vert.sin();
|
|
||||||
[
|
|
||||||
point(0.0, 0.0, 0.0),
|
|
||||||
point(ang_hor.cos(), ang_hor.sin(), 0.0),
|
|
||||||
point(x_vert, y_vert, -0.5),
|
|
||||||
point(x_vert, y_vert, 0.5)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
).collect::<Vec<_>>();
|
|
||||||
DMatrix::from_columns(&guess_elts)
|
|
||||||
};
|
|
||||||
let frozen: [_; N_POINTS] = array::from_fn(|k| (3, k));
|
|
||||||
let (config, tangent, success, history) = realize_gram(
|
|
||||||
&gram, guess.clone(), &frozen,
|
|
||||||
SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
|
|
||||||
);
|
|
||||||
assert_eq!(config, guess);
|
|
||||||
assert_eq!(success, true);
|
|
||||||
assert_eq!(history.scaled_loss.len(), 1);
|
|
||||||
|
|
||||||
// list some motions that should form a basis for the tangent space of
|
|
||||||
// the solution variety
|
|
||||||
let element_dim = guess.nrows();
|
|
||||||
let assembly_dim = guess.ncols();
|
|
||||||
let tangent_motions_unif = vec![
|
|
||||||
// the translations along the coordinate axes
|
|
||||||
translation_motion_unif(&Vector3::new(1.0, 0.0, 0.0), assembly_dim),
|
|
||||||
translation_motion_unif(&Vector3::new(0.0, 1.0, 0.0), assembly_dim),
|
|
||||||
translation_motion_unif(&Vector3::new(0.0, 0.0, 1.0), assembly_dim),
|
|
||||||
|
|
||||||
// the rotations about the coordinate axes
|
|
||||||
rotation_motion_unif(&Vector3::new(1.0, 0.0, 0.0), guess.column_iter().collect()),
|
|
||||||
rotation_motion_unif(&Vector3::new(0.0, 1.0, 0.0), guess.column_iter().collect()),
|
|
||||||
rotation_motion_unif(&Vector3::new(0.0, 0.0, 1.0), guess.column_iter().collect()),
|
|
||||||
|
|
||||||
// the twist motion. more precisely: a motion that keeps the center
|
|
||||||
// of mass stationary and preserves the distances between the
|
|
||||||
// vertices to first order. this has to be the twist as long as:
|
|
||||||
// - twisting is the kaleidocycle's only internal degree of
|
|
||||||
// freedom
|
|
||||||
// - every first-order motion of the kaleidocycle comes from an
|
|
||||||
// actual motion
|
|
||||||
(0..N_HINGES).step_by(2).flat_map(
|
|
||||||
|n| {
|
|
||||||
let ang_vert = ((n + 1) as f64) * PI/3.0;
|
|
||||||
let vel_vert_x = 4.0 * ang_vert.cos();
|
|
||||||
let vel_vert_y = 4.0 * ang_vert.sin();
|
|
||||||
[
|
|
||||||
DVector::from_column_slice(&[0.0, 0.0, 5.0, 0.0]),
|
|
||||||
DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]),
|
|
||||||
DVector::from_column_slice(&[-vel_vert_x, -vel_vert_y, -3.0, 0.0]),
|
|
||||||
DVector::from_column_slice(&[vel_vert_x, vel_vert_y, -3.0, 0.0])
|
|
||||||
]
|
|
||||||
}
|
|
||||||
).collect::<Vec<_>>()
|
|
||||||
];
|
|
||||||
let tangent_motions_std = tangent_motions_unif.iter().map(
|
|
||||||
|motion| DMatrix::from_columns(
|
|
||||||
&guess.column_iter().zip(motion).map(
|
|
||||||
|(v, elt_motion)| local_unif_to_std(v) * elt_motion
|
|
||||||
).collect::<Vec<_>>()
|
|
||||||
)
|
|
||||||
).collect::<Vec<_>>();
|
|
||||||
|
|
||||||
// confirm that the dimension of the tangent space is no greater than
|
|
||||||
// expected
|
|
||||||
assert_eq!(tangent.basis_std.len(), tangent_motions_unif.len());
|
|
||||||
|
|
||||||
// confirm that the tangent space contains all the motions we expect it
|
|
||||||
// to. since we've already bounded the dimension of the tangent space,
|
|
||||||
// this confirms that the tangent space is what we expect it to be
|
|
||||||
let tol_sq = ((element_dim * assembly_dim) as f64) * SCALED_TOL * SCALED_TOL;
|
|
||||||
for (motion_unif, motion_std) in tangent_motions_unif.into_iter().zip(tangent_motions_std) {
|
|
||||||
let motion_proj: DMatrix<_> = motion_unif.into_iter().enumerate().map(
|
|
||||||
|(k, v)| tangent.proj(&v.as_view(), k)
|
|
||||||
).sum();
|
|
||||||
assert!((motion_std - motion_proj).norm_squared() < tol_sq);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn translation(dis: Vector3<f64>) -> DMatrix<f64> {
|
|
||||||
const ELEMENT_DIM: usize = 5;
|
|
||||||
DMatrix::from_column_slice(ELEMENT_DIM, ELEMENT_DIM, &[
|
|
||||||
1.0, 0.0, 0.0, 0.0, dis[0],
|
|
||||||
0.0, 1.0, 0.0, 0.0, dis[1],
|
|
||||||
0.0, 0.0, 1.0, 0.0, dis[2],
|
|
||||||
2.0*dis[0], 2.0*dis[1], 2.0*dis[2], 1.0, dis.norm_squared(),
|
|
||||||
0.0, 0.0, 0.0, 0.0, 1.0
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
// confirm that projection onto a configuration subspace is equivariant with
|
|
||||||
// respect to Euclidean motions
|
|
||||||
#[test]
|
|
||||||
fn proj_equivar_test() {
|
|
||||||
// find a pair of spheres that meet at 120°
|
|
||||||
const SCALED_TOL: f64 = 1.0e-12;
|
|
||||||
let gram = {
|
|
||||||
let mut gram_to_be = PartialMatrix::new();
|
|
||||||
gram_to_be.push_sym(0, 0, 1.0);
|
|
||||||
gram_to_be.push_sym(1, 1, 1.0);
|
|
||||||
gram_to_be.push_sym(0, 1, 0.5);
|
|
||||||
gram_to_be
|
|
||||||
};
|
|
||||||
let guess_orig = DMatrix::from_columns(&[
|
|
||||||
sphere(0.0, 0.0, 0.5, 1.0),
|
|
||||||
sphere(0.0, 0.0, -0.5, 1.0)
|
|
||||||
]);
|
|
||||||
let (config_orig, tangent_orig, success_orig, history_orig) = realize_gram(
|
|
||||||
&gram, guess_orig.clone(), &[],
|
|
||||||
SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
|
|
||||||
);
|
|
||||||
assert_eq!(config_orig, guess_orig);
|
|
||||||
assert_eq!(success_orig, true);
|
|
||||||
assert_eq!(history_orig.scaled_loss.len(), 1);
|
|
||||||
|
|
||||||
// find another pair of spheres that meet at 120°. we'll think of this
|
|
||||||
// solution as a transformed version of the original one
|
|
||||||
let guess_tfm = {
|
|
||||||
let a = 0.5 * FRAC_1_SQRT_2;
|
|
||||||
DMatrix::from_columns(&[
|
|
||||||
sphere(a, 0.0, 7.0 + a, 1.0),
|
|
||||||
sphere(-a, 0.0, 7.0 - a, 1.0)
|
|
||||||
])
|
|
||||||
};
|
|
||||||
let (config_tfm, tangent_tfm, success_tfm, history_tfm) = realize_gram(
|
|
||||||
&gram, guess_tfm.clone(), &[],
|
|
||||||
SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
|
|
||||||
);
|
|
||||||
assert_eq!(config_tfm, guess_tfm);
|
|
||||||
assert_eq!(success_tfm, true);
|
|
||||||
assert_eq!(history_tfm.scaled_loss.len(), 1);
|
|
||||||
|
|
||||||
// project a nudge to the tangent space of the solution variety at the
|
|
||||||
// original solution
|
|
||||||
let motion_orig = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]);
|
|
||||||
let motion_orig_proj = tangent_orig.proj(&motion_orig.as_view(), 0);
|
|
||||||
|
|
||||||
// project the equivalent nudge to the tangent space of the solution
|
|
||||||
// variety at the transformed solution
|
|
||||||
let motion_tfm = DVector::from_column_slice(&[FRAC_1_SQRT_2, 0.0, FRAC_1_SQRT_2, 0.0]);
|
|
||||||
let motion_tfm_proj = tangent_tfm.proj(&motion_tfm.as_view(), 0);
|
|
||||||
|
|
||||||
// take the transformation that sends the original solution to the
|
|
||||||
// transformed solution and apply it to the motion that the original
|
|
||||||
// solution makes in response to the nudge
|
|
||||||
const ELEMENT_DIM: usize = 5;
|
|
||||||
let rot = DMatrix::from_column_slice(ELEMENT_DIM, ELEMENT_DIM, &[
|
|
||||||
FRAC_1_SQRT_2, 0.0, -FRAC_1_SQRT_2, 0.0, 0.0,
|
|
||||||
0.0, 1.0, 0.0, 0.0, 0.0,
|
|
||||||
FRAC_1_SQRT_2, 0.0, FRAC_1_SQRT_2, 0.0, 0.0,
|
|
||||||
0.0, 0.0, 0.0, 1.0, 0.0,
|
|
||||||
0.0, 0.0, 0.0, 0.0, 1.0
|
|
||||||
]);
|
|
||||||
let transl = translation(Vector3::new(0.0, 0.0, 7.0));
|
|
||||||
let motion_proj_tfm = transl * rot * motion_orig_proj;
|
|
||||||
|
|
||||||
// confirm that the projection of the nudge is equivariant. we loosen
|
|
||||||
// the comparison tolerance because the transformation seems to
|
|
||||||
// introduce some numerical error
|
|
||||||
const SCALED_TOL_TFM: f64 = 1.0e-9;
|
|
||||||
let tol_sq = ((guess_orig.nrows() * guess_orig.ncols()) as f64) * SCALED_TOL_TFM * SCALED_TOL_TFM;
|
|
||||||
assert!((motion_proj_tfm - motion_tfm_proj).norm_squared() < tol_sq);
|
|
||||||
}
|
|
||||||
|
|
||||||
// at the frozen indices, the optimization steps should have exact zeros,
|
// at the frozen indices, the optimization steps should have exact zeros,
|
||||||
// and the realized configuration should match the initial guess
|
// and the realized configuration should match the initial guess
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -846,7 +428,7 @@ mod tests {
|
||||||
]);
|
]);
|
||||||
let frozen = [(3, 0), (3, 1)];
|
let frozen = [(3, 0), (3, 1)];
|
||||||
println!();
|
println!();
|
||||||
let (config, _, success, history) = realize_gram(
|
let (config, success, history) = realize_gram(
|
||||||
&gram, guess.clone(), &frozen,
|
&gram, guess.clone(), &frozen,
|
||||||
1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
||||||
);
|
);
|
||||||
|
|
|
@ -46,10 +46,6 @@ impl AppState {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// set the console error panic hook
|
|
||||||
#[cfg(feature = "console_error_panic_hook")]
|
|
||||||
console_error_panic_hook::set_once();
|
|
||||||
|
|
||||||
sycamore::render(|| {
|
sycamore::render(|| {
|
||||||
provide_context(AppState::new());
|
provide_context(AppState::new());
|
||||||
|
|
||||||
|
|
|
@ -64,16 +64,11 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View {
|
||||||
move |sel| if sel.contains(&key) { "selected" } else { "" }
|
move |sel| if sel.contains(&key) { "selected" } else { "" }
|
||||||
);
|
);
|
||||||
let label = element.label.clone();
|
let label = element.label.clone();
|
||||||
let rep_components = move || {
|
let rep_components = element.representation.map(
|
||||||
element.representation.with(
|
|rep| rep.iter().map(
|
||||||
|rep| rep.iter().map(
|
|u| format!("{:.3}", u).replace("-", "\u{2212}")
|
||||||
|u| {
|
).collect()
|
||||||
let u_str = format!("{:.3}", u).replace("-", "\u{2212}");
|
);
|
||||||
view! { div { (u_str) } }
|
|
||||||
}
|
|
||||||
).collect::<Vec<_>>()
|
|
||||||
)
|
|
||||||
};
|
|
||||||
let constrained = element.constraints.map(|csts| csts.len() > 0);
|
let constrained = element.constraints.map(|csts| csts.len() > 0);
|
||||||
let constraint_list = element.constraints.map(
|
let constraint_list = element.constraints.map(
|
||||||
|csts| csts.clone().into_iter().collect()
|
|csts| csts.clone().into_iter().collect()
|
||||||
|
@ -134,7 +129,14 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View {
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
div(class="element-label") { (label) }
|
div(class="element-label") { (label) }
|
||||||
div(class="element-representation") { (rep_components) }
|
div(class="element-representation") {
|
||||||
|
Indexed(
|
||||||
|
list=rep_components,
|
||||||
|
view=|coord_str| view! {
|
||||||
|
div { (coord_str) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
div(class="status")
|
div(class="status")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue