Encapsulate the constraint problem data

This will make it easier for elements and regulators to write themselves
into the constraint problem.
This commit is contained in:
Aaron Fenyes 2025-03-24 23:21:55 -04:00
parent d243f19e25
commit c6e6e7be9f
4 changed files with 172 additions and 167 deletions

View file

@ -1,26 +1,19 @@
use nalgebra::DMatrix; use dyna3::engine::{Q, point, realize_gram, sphere, ConstraintProblem};
use dyna3::engine::{Q, point, realize_gram, sphere, PartialMatrix};
fn main() { fn main() {
let gram = { let mut problem = ConstraintProblem::from_guess(&[
let mut gram_to_be = PartialMatrix::new();
for j in 0..2 {
for k in j..2 {
gram_to_be.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 });
}
}
gram_to_be
};
let guess = DMatrix::from_columns(&[
point(0.0, 0.0, 2.0), point(0.0, 0.0, 2.0),
sphere(0.0, 0.0, 0.0, 1.0) sphere(0.0, 0.0, 0.0, 1.0)
]); ]);
let frozen = [(3, 0)]; for j in 0..2 {
for k in j..2 {
problem.gram.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 });
}
}
problem.frozen.push((3, 0));
println!(); println!();
let (config, _, success, history) = realize_gram( let (config, _, success, history) = realize_gram(
&gram, guess, &frozen, &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
1.0e-12, 0.5, 0.9, 1.1, 200, 110
); );
print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config);
print!("Configuration:{}", config); print!("Configuration:{}", config);

View file

@ -1,29 +1,22 @@
use nalgebra::DMatrix; use dyna3::engine::{Q, realize_gram, sphere, ConstraintProblem};
use dyna3::engine::{Q, realize_gram, sphere, PartialMatrix};
fn main() { fn main() {
let gram = { let mut problem = ConstraintProblem::from_guess({
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 = {
let a: f64 = 0.75_f64.sqrt(); let a: f64 = 0.75_f64.sqrt();
DMatrix::from_columns(&[ &[
sphere(1.0, 0.0, 0.0, 1.0), sphere(1.0, 0.0, 0.0, 1.0),
sphere(-0.5, a, 0.0, 1.0), sphere(-0.5, a, 0.0, 1.0),
sphere(-0.5, -a, 0.0, 1.0) sphere(-0.5, -a, 0.0, 1.0)
]) ]
}; });
for j in 0..3 {
for k in j..3 {
problem.gram.push_sym(j, k, if j == k { 1.0 } else { -1.0 });
}
}
println!(); println!();
let (config, _, success, history) = realize_gram( let (config, _, success, history) = realize_gram(
&gram, guess, &[], &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
1.0e-12, 0.5, 0.9, 1.1, 200, 110
); );
print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config);
if success { if success {

View file

@ -6,7 +6,13 @@ use sycamore::prelude::*;
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
use crate::{ use crate::{
engine::{Q, local_unif_to_std, realize_gram, ConfigSubspace, PartialMatrix}, engine::{
Q,
local_unif_to_std,
realize_gram,
ConfigSubspace,
ConstraintProblem
},
specified::SpecifiedValue specified::SpecifiedValue
}; };
@ -268,6 +274,11 @@ impl Assembly {
// --- realization --- // --- realization ---
pub fn realize(&self) { pub fn realize(&self) {
// create a blank constraint problem
let mut problem = ConstraintProblem::new(
self.elements.with_untracked(|elts| elts.len())
);
// 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() {
@ -276,9 +287,8 @@ impl Assembly {
}); });
// set up the Gram matrix and the initial configuration matrix // set up the Gram matrix and the initial configuration matrix
let (gram, guess) = self.elements.with_untracked(|elts| { self.elements.with_untracked(|elts| {
// set up the off-diagonal part of the Gram matrix // set up the off-diagonal part of the Gram matrix
let mut gram_to_be = PartialMatrix::new();
self.regulators.with_untracked(|regs| { self.regulators.with_untracked(|regs| {
for (_, reg) in regs { for (_, reg) in regs {
reg.set_point.with_untracked(|set_pt| { reg.set_point.with_untracked(|set_pt| {
@ -286,7 +296,7 @@ impl Assembly {
let subjects = reg.subjects; let subjects = reg.subjects;
let row = elts[subjects.0].column_index.unwrap(); let row = elts[subjects.0].column_index.unwrap();
let col = elts[subjects.1].column_index.unwrap(); let col = elts[subjects.1].column_index.unwrap();
gram_to_be.push_sym(row, col, val); problem.gram.push_sym(row, col, val);
} }
}); });
} }
@ -294,36 +304,32 @@ impl Assembly {
// set up the initial configuration matrix and the diagonal of the // set up the initial configuration matrix and the diagonal of the
// Gram matrix // Gram matrix
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.unwrap();
gram_to_be.push_sym(index, index, 1.0); problem.gram.push_sym(index, index, 1.0);
guess_to_be.set_column(index, &elt.representation.get_clone_untracked()); problem.guess.set_column(index, &elt.representation.get_clone_untracked());
} }
(gram_to_be, guess_to_be)
}); });
/* DEBUG */ /* DEBUG */
// log the Gram matrix // log the Gram matrix
console::log_1(&JsValue::from("Gram matrix:")); console::log_1(&JsValue::from("Gram matrix:"));
gram.log_to_console(); problem.gram.log_to_console();
/* DEBUG */ /* DEBUG */
// log the initial configuration matrix // log the initial configuration matrix
console::log_1(&JsValue::from("Old configuration:")); console::log_1(&JsValue::from("Old configuration:"));
for j in 0..guess.nrows() { for j in 0..problem.guess.nrows() {
let mut row_str = String::new(); let mut row_str = String::new();
for k in 0..guess.ncols() { for k in 0..problem.guess.ncols() {
row_str.push_str(format!(" {:>8.3}", guess[(j, k)]).as_str()); row_str.push_str(format!(" {:>8.3}", problem.guess[(j, k)]).as_str());
} }
console::log_1(&JsValue::from(row_str)); console::log_1(&JsValue::from(row_str));
} }
// 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, tangent, success, history) = realize_gram(
&gram, guess, &[], &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
1.0e-12, 0.5, 0.9, 1.1, 200, 110
); );
/* DEBUG */ /* DEBUG */

View file

@ -195,6 +195,34 @@ impl DescentHistory {
} }
} }
// --- constraint problems ---
pub struct ConstraintProblem {
pub gram: PartialMatrix,
pub guess: DMatrix<f64>,
pub frozen: Vec<(usize, usize)>
}
impl ConstraintProblem {
pub fn new(element_count: usize) -> ConstraintProblem {
const ELEMENT_DIM: usize = 5;
ConstraintProblem {
gram: PartialMatrix::new(),
guess: DMatrix::<f64>::zeros(ELEMENT_DIM, element_count),
frozen: Vec::new()
}
}
#[cfg(feature = "dev")]
pub fn from_guess(guess_columns: &[DVector<f64>]) -> ConstraintProblem {
ConstraintProblem {
gram: PartialMatrix::new(),
guess: DMatrix::from_columns(guess_columns),
frozen: Vec::new()
}
}
}
// --- gram matrix realization --- // --- gram matrix realization ---
// the Lorentz form // the Lorentz form
@ -289,9 +317,7 @@ fn seek_better_config(
// seek a matrix `config` for which `config' * Q * config` matches the partial // seek a matrix `config` for which `config' * Q * config` matches the partial
// matrix `gram`. use gradient descent starting from `guess` // matrix `gram`. use gradient descent starting from `guess`
pub fn realize_gram( pub fn realize_gram(
gram: &PartialMatrix, problem: &ConstraintProblem,
guess: DMatrix<f64>,
frozen: &[(usize, usize)],
scaled_tol: f64, scaled_tol: f64,
min_efficiency: f64, min_efficiency: f64,
backoff: f64, backoff: f64,
@ -299,6 +325,11 @@ pub fn realize_gram(
max_descent_steps: i32, max_descent_steps: i32,
max_backoff_steps: i32 max_backoff_steps: i32
) -> (DMatrix<f64>, ConfigSubspace, bool, DescentHistory) { ) -> (DMatrix<f64>, ConfigSubspace, bool, DescentHistory) {
// destructure the problem data
let ConstraintProblem {
gram, guess, frozen
} = problem;
// start the descent history // start the descent history
let mut history = DescentHistory::new(); let mut history = DescentHistory::new();
@ -317,7 +348,7 @@ pub fn realize_gram(
).collect(); ).collect();
// 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.clone());
let mut hess = DMatrix::zeros(element_dim, assembly_dim); let mut hess = DMatrix::zeros(element_dim, assembly_dim);
for _ in 0..max_descent_steps { for _ in 0..max_descent_steps {
// find the negative gradient of the loss function // find the negative gradient of the loss function
@ -415,7 +446,7 @@ pub fn realize_gram(
#[cfg(feature = "dev")] #[cfg(feature = "dev")]
pub mod examples { pub mod examples {
use std::{array, f64::consts::PI}; use std::f64::consts::PI;
use super::*; use super::*;
@ -428,35 +459,7 @@ pub mod examples {
// https://www.nippon.com/en/japan-topics/c12801/ // https://www.nippon.com/en/japan-topics/c12801/
// //
pub fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix<f64>, ConfigSubspace, bool, DescentHistory) { pub fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix<f64>, ConfigSubspace, bool, DescentHistory) {
let gram = { let mut problem = ConstraintProblem::from_guess(
let mut gram_to_be = PartialMatrix::new();
for s in 0..9 {
// each sphere is represented by a spacelike vector
gram_to_be.push_sym(s, s, 1.0);
// the circumscribing sphere is tangent to all of the other
// spheres, with matching orientation
if s > 0 {
gram_to_be.push_sym(0, s, 1.0);
}
if s > 2 {
// each chain sphere is tangent to the "sun" and "moon"
// spheres, with opposing orientation
for n in 1..3 {
gram_to_be.push_sym(s, n, -1.0);
}
// each chain sphere is tangent to the next chain sphere,
// with opposing orientation
let s_next = 3 + (s-2) % 6;
gram_to_be.push_sym(s, s_next, -1.0);
}
}
gram_to_be
};
let guess = DMatrix::from_columns(
[ [
sphere(0.0, 0.0, 0.0, 15.0), sphere(0.0, 0.0, 0.0, 15.0),
sphere(0.0, 0.0, -9.0, 5.0), sphere(0.0, 0.0, -9.0, 5.0),
@ -471,42 +474,45 @@ pub mod examples {
).collect::<Vec<_>>().as_slice() ).collect::<Vec<_>>().as_slice()
); );
for s in 0..9 {
// each sphere is represented by a spacelike vector
problem.gram.push_sym(s, s, 1.0);
// the circumscribing sphere is tangent to all of the other
// spheres, with matching orientation
if s > 0 {
problem.gram.push_sym(0, s, 1.0);
}
if s > 2 {
// each chain sphere is tangent to the "sun" and "moon"
// spheres, with opposing orientation
for n in 1..3 {
problem.gram.push_sym(s, n, -1.0);
}
// each chain sphere is tangent to the next chain sphere,
// with opposing orientation
let s_next = 3 + (s-2) % 6;
problem.gram.push_sym(s, s_next, -1.0);
}
}
// the frozen entries fix the radii of the circumscribing sphere, the // the frozen entries fix the radii of the circumscribing sphere, the
// "sun" and "moon" spheres, and one of the chain spheres // "sun" and "moon" spheres, and one of the chain spheres
let frozen: [(usize, usize); 4] = array::from_fn(|k| (3, k)); for k in 0..4 {
problem.frozen.push((3, k))
}
realize_gram( realize_gram(&problem, scaled_tol, 0.5, 0.9, 1.1, 200, 110)
&gram, guess, &frozen,
scaled_tol, 0.5, 0.9, 1.1, 200, 110
)
} }
// set up a kaleidocycle, made of points with fixed distances between them, // set up a kaleidocycle, made of points with fixed distances between them,
// and find its tangent space // and find its tangent space
pub fn realize_kaleidocycle(scaled_tol: f64) -> (DMatrix<f64>, ConfigSubspace, bool, DescentHistory) { pub fn realize_kaleidocycle(scaled_tol: f64) -> (DMatrix<f64>, ConfigSubspace, bool, DescentHistory) {
const N_POINTS: usize = 12; const N_HINGES: usize = 6;
let gram = { let mut problem = ConstraintProblem::from_guess(
let mut gram_to_be = PartialMatrix::new(); (0..N_HINGES).step_by(2).flat_map(
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| { |n| {
let ang_hor = (n as f64) * PI/3.0; let ang_hor = (n as f64) * PI/3.0;
let ang_vert = ((n + 1) as f64) * PI/3.0; let ang_vert = ((n + 1) as f64) * PI/3.0;
@ -519,16 +525,30 @@ pub mod examples {
point(x_vert, y_vert, 0.5) point(x_vert, y_vert, 0.5)
] ]
} }
).collect::<Vec<_>>(); ).collect::<Vec<_>>().as_slice()
DMatrix::from_columns(&guess_elts) );
};
let frozen: [_; N_POINTS] = array::from_fn(|k| (3, k)); const N_POINTS: usize = 2 * N_HINGES;
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 {
problem.gram.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 });
}
// non-hinge edges
for k in 0..2 {
problem.gram.push_sym(block + j, block_next + k, -0.625);
}
}
}
realize_gram( for k in 0..N_POINTS {
&gram, guess, &frozen, problem.frozen.push((3, k))
scaled_tol, 0.5, 0.9, 1.1, 200, 110 }
)
realize_gram(&problem, scaled_tol, 0.5, 0.9, 1.1, 200, 110)
} }
} }
@ -588,33 +608,29 @@ mod tests {
// and the realized configuration should match the initial guess // and the realized configuration should match the initial guess
#[test] #[test]
fn frozen_entry_test() { fn frozen_entry_test() {
let gram = { let mut problem = ConstraintProblem::from_guess(&[
let mut gram_to_be = PartialMatrix::new();
for j in 0..2 {
for k in j..2 {
gram_to_be.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 });
}
}
gram_to_be
};
let guess = DMatrix::from_columns(&[
point(0.0, 0.0, 2.0), point(0.0, 0.0, 2.0),
sphere(0.0, 0.0, 0.0, 1.0) sphere(0.0, 0.0, 0.0, 1.0)
]); ]);
let frozen = [(3, 0), (3, 1)]; for j in 0..2 {
println!(); for k in j..2 {
problem.gram.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 });
}
}
for k in 0..2 {
problem.frozen.push((3, k));
}
let (config, _, success, history) = realize_gram( let (config, _, success, history) = realize_gram(
&gram, guess.clone(), &frozen, &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
1.0e-12, 0.5, 0.9, 1.1, 200, 110
); );
assert_eq!(success, true); assert_eq!(success, true);
for base_step in history.base_step.into_iter() { for base_step in history.base_step.into_iter() {
for index in frozen { for &index in &problem.frozen {
assert_eq!(base_step[index], 0.0); assert_eq!(base_step[index], 0.0);
} }
} }
for index in frozen { for index in problem.frozen {
assert_eq!(config[index], guess[index]); assert_eq!(config[index], problem.guess[index]);
} }
} }
@ -635,34 +651,32 @@ mod tests {
#[test] #[test]
fn tangent_test_three_spheres() { fn tangent_test_three_spheres() {
const SCALED_TOL: f64 = 1.0e-12; const SCALED_TOL: f64 = 1.0e-12;
let gram = { const ELEMENT_DIM: usize = 5;
let mut gram_to_be = PartialMatrix::new(); let mut problem = ConstraintProblem::from_guess(&[
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, 0.0, -2.0),
sphere(0.0, 0.0, 1.0, 1.0), sphere(0.0, 0.0, 1.0, 1.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)); for j in 0..3 {
for k in j..3 {
problem.gram.push_sym(j, k, if j == k { 1.0 } else { -1.0 });
}
}
for n in 0..ELEMENT_DIM {
problem.frozen.push((n, 0));
}
let (config, tangent, success, history) = realize_gram( let (config, tangent, success, history) = realize_gram(
&gram, guess.clone(), &frozen, &problem, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
); );
assert_eq!(config, guess); assert_eq!(config, problem.guess);
assert_eq!(success, true); assert_eq!(success, true);
assert_eq!(history.scaled_loss.len(), 1); assert_eq!(history.scaled_loss.len(), 1);
// list some motions that should form a basis for the tangent space of // list some motions that should form a basis for the tangent space of
// the solution variety // the solution variety
const UNIFORM_DIM: usize = 4; const UNIFORM_DIM: usize = 4;
let element_dim = guess.nrows(); let element_dim = problem.guess.nrows();
let assembly_dim = guess.ncols(); let assembly_dim = problem.guess.ncols();
let tangent_motions_unif = vec![ let tangent_motions_unif = vec![
basis_matrix((0, 1), UNIFORM_DIM, assembly_dim), basis_matrix((0, 1), UNIFORM_DIM, assembly_dim),
basis_matrix((1, 1), UNIFORM_DIM, assembly_dim), basis_matrix((1, 1), UNIFORM_DIM, assembly_dim),
@ -805,22 +819,17 @@ mod tests {
fn proj_equivar_test() { fn proj_equivar_test() {
// find a pair of spheres that meet at 120° // find a pair of spheres that meet at 120°
const SCALED_TOL: f64 = 1.0e-12; const SCALED_TOL: f64 = 1.0e-12;
let gram = { let mut problem_orig = ConstraintProblem::from_guess(&[
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),
sphere(0.0, 0.0, -0.5, 1.0) sphere(0.0, 0.0, -0.5, 1.0)
]); ]);
problem_orig.gram.push_sym(0, 0, 1.0);
problem_orig.gram.push_sym(1, 1, 1.0);
problem_orig.gram.push_sym(0, 1, 0.5);
let (config_orig, tangent_orig, success_orig, history_orig) = realize_gram( let (config_orig, tangent_orig, success_orig, history_orig) = realize_gram(
&gram, guess_orig.clone(), &[], &problem_orig, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
); );
assert_eq!(config_orig, guess_orig); assert_eq!(config_orig, problem_orig.guess);
assert_eq!(success_orig, true); assert_eq!(success_orig, true);
assert_eq!(history_orig.scaled_loss.len(), 1); assert_eq!(history_orig.scaled_loss.len(), 1);
@ -833,11 +842,15 @@ mod tests {
sphere(-a, 0.0, 7.0 - a, 1.0) sphere(-a, 0.0, 7.0 - a, 1.0)
]) ])
}; };
let problem_tfm = ConstraintProblem {
gram: problem_orig.gram,
guess: guess_tfm,
frozen: problem_orig.frozen
};
let (config_tfm, tangent_tfm, success_tfm, history_tfm) = realize_gram( let (config_tfm, tangent_tfm, success_tfm, history_tfm) = realize_gram(
&gram, guess_tfm.clone(), &[], &problem_tfm, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
); );
assert_eq!(config_tfm, guess_tfm); assert_eq!(config_tfm, problem_tfm.guess);
assert_eq!(success_tfm, true); assert_eq!(success_tfm, true);
assert_eq!(history_tfm.scaled_loss.len(), 1); assert_eq!(history_tfm.scaled_loss.len(), 1);
@ -869,7 +882,7 @@ mod tests {
// the comparison tolerance because the transformation seems to // the comparison tolerance because the transformation seems to
// introduce some numerical error // introduce some numerical error
const SCALED_TOL_TFM: f64 = 1.0e-9; 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; let tol_sq = ((problem_orig.guess.nrows() * problem_orig.guess.ncols()) as f64) * SCALED_TOL_TFM * SCALED_TOL_TFM;
assert!((motion_proj_tfm - motion_tfm_proj).norm_squared() < tol_sq); assert!((motion_proj_tfm - motion_tfm_proj).norm_squared() < tol_sq);
} }
} }