From 16df161fe761e7c550282ff02916e6f089ff17b6 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 24 Oct 2024 19:51:10 -0700 Subject: [PATCH 01/29] Test alternate projection technique --- engine-proto/gram-test/Engine.jl | 133 +++++++++++++++++- engine-proto/gram-test/irisawa-hexlet.jl | 11 +- .../gram-test/sphere-in-tetrahedron.jl | 11 +- .../gram-test/tetrahedron-radius-ratio.jl | 11 +- 4 files changed, 159 insertions(+), 7 deletions(-) diff --git a/engine-proto/gram-test/Engine.jl b/engine-proto/gram-test/Engine.jl index 22f5914..1eb72f5 100644 --- a/engine-proto/gram-test/Engine.jl +++ b/engine-proto/gram-test/Engine.jl @@ -8,7 +8,8 @@ using Optim export rand_on_shell, Q, DescentHistory, - realize_gram_gradient, realize_gram_newton, realize_gram_optim, realize_gram + realize_gram_gradient, realize_gram_newton, realize_gram_optim, + realize_gram_alt_proj, realize_gram # === guessing === @@ -143,7 +144,7 @@ function realize_gram_gradient( break end - # find negative gradient of loss function + # find the negative gradient of the loss function neg_grad = 4*Q*L*Δ_proj slope = norm(neg_grad) dir = neg_grad / slope @@ -232,7 +233,7 @@ function realize_gram_newton( break end - # find the negative gradient of loss function + # find the negative gradient of the loss function neg_grad = 4*Q*L*Δ_proj # find the negative Hessian of the loss function @@ -313,6 +314,130 @@ function realize_gram_optim( ) end +# seek a matrix `L` for which `L'QL` matches the sparse matrix `gram` at every +# explicit entry of `gram`. use gradient descent starting from `guess`, with an +# alternate technique for finding the projected base step from the unprojected +# Hessian +function realize_gram_alt_proj( + gram::SparseMatrixCSC{T, <:Any}, + guess::Matrix{T}, + frozen = CartesianIndex[]; + scaled_tol = 1e-30, + min_efficiency = 0.5, + init_rate = 1.0, + backoff = 0.9, + reg_scale = 1.1, + max_descent_steps = 200, + max_backoff_steps = 110 +) where T <: Number + # start history + history = DescentHistory{T}() + + # find the dimension of the search space + dims = size(guess) + element_dim, construction_dim = dims + total_dim = element_dim * construction_dim + + # list the constrained entries of the gram matrix + J, K, _ = findnz(gram) + constrained = zip(J, K) + + # scale the tolerance + scale_adjustment = sqrt(T(length(constrained))) + tol = scale_adjustment * scaled_tol + + # convert the frozen indices to stacked format + frozen_stacked = [(index[2]-1)*element_dim + index[1] for index in frozen] + + # initialize variables + grad_rate = init_rate + L = copy(guess) + + # use Newton's method with backtracking and gradient descent backup + Δ_proj = proj_diff(gram, L'*Q*L) + loss = dot(Δ_proj, Δ_proj) + for step in 1:max_descent_steps + # stop if the loss is tolerably low + if loss < tol + break + end + + # find the negative gradient of the loss function + neg_grad = 4*Q*L*Δ_proj + + # find the negative Hessian of the loss function + hess = Matrix{T}(undef, total_dim, total_dim) + indices = [(j, k) for k in 1:construction_dim for j in 1:element_dim] + for (j, k) in indices + basis_mat = basis_matrix(T, j, k, dims) + neg_dΔ = basis_mat'*Q*L + L'*Q*basis_mat + neg_dΔ_proj = proj_to_entries(neg_dΔ, constrained) + deriv_grad = 4*Q*(-basis_mat*Δ_proj + L*neg_dΔ_proj) + hess[:, (k-1)*element_dim + j] = reshape(deriv_grad, total_dim) + end + hess_sym = Hermitian(hess) + push!(history.hess, hess_sym) + + # regularize the Hessian + min_eigval = minimum(eigvals(hess_sym)) + push!(history.positive, min_eigval > 0) + if min_eigval <= 0 + hess -= reg_scale * min_eigval * I + end + + # compute the Newton step + neg_grad_stacked = reshape(neg_grad, total_dim) + for k in frozen_stacked + neg_grad_stacked[k] = 0 + hess[k, :] .= 0 + hess[:, k] .= 0 + hess[k, k] = 1 + end + base_step_stacked = Hermitian(hess) \ neg_grad_stacked + base_step = reshape(base_step_stacked, dims) + push!(history.base_step, base_step) + + # store the current position, loss, and slope + L_last = L + loss_last = loss + push!(history.scaled_loss, loss / scale_adjustment) + push!(history.neg_grad, neg_grad) + push!(history.slope, norm(neg_grad)) + + # find a good step size using backtracking line search + push!(history.stepsize, 0) + push!(history.backoff_steps, max_backoff_steps) + empty!(history.last_line_L) + empty!(history.last_line_loss) + rate = one(T) + step_success = false + for backoff_steps in 0:max_backoff_steps + history.stepsize[end] = rate + L = L_last + rate * base_step + Δ_proj = proj_diff(gram, L'*Q*L) + loss = dot(Δ_proj, Δ_proj) + improvement = loss_last - loss + push!(history.last_line_L, L) + push!(history.last_line_loss, loss / scale_adjustment) + if improvement >= min_efficiency * rate * dot(neg_grad, base_step) + history.backoff_steps[end] = backoff_steps + step_success = true + break + end + rate *= backoff + end + + # if we've hit a wall, quit + if !step_success + return L_last, false, history + end + end + + # return the factorization and its history + push!(history.scaled_loss, loss / scale_adjustment) + L, loss < tol, history +end + # seek a matrix `L` for which `L'QL` matches the sparse matrix `gram` at every # explicit entry of `gram`. use gradient descent starting from `guess` function realize_gram( @@ -365,7 +490,7 @@ function realize_gram( break end - # find the negative gradient of loss function + # find the negative gradient of the loss function neg_grad = 4*Q*L*Δ_proj # find the negative Hessian of the loss function diff --git a/engine-proto/gram-test/irisawa-hexlet.jl b/engine-proto/gram-test/irisawa-hexlet.jl index 67def8c..607db61 100644 --- a/engine-proto/gram-test/irisawa-hexlet.jl +++ b/engine-proto/gram-test/irisawa-hexlet.jl @@ -74,4 +74,13 @@ if success for k in 5:9 println(" ", 1 / L[4,k], " sun") end -end \ No newline at end of file +end + +# test an alternate technique for finding the projected base step from the +# unprojected Hessian +L_alt, success_alt, history_alt = Engine.realize_gram_alt_proj(gram, guess, frozen) +completed_gram_alt = L_alt'*Engine.Q*L_alt +println("\nDifference in result using alternate projection:\n") +display(completed_gram_alt - completed_gram) +println("\nDifference in steps: ", size(history_alt.scaled_loss, 1) - size(history.scaled_loss, 1)) +println("Difference in loss: ", history_alt.scaled_loss[end] - history.scaled_loss[end], "\n") \ No newline at end of file diff --git a/engine-proto/gram-test/sphere-in-tetrahedron.jl b/engine-proto/gram-test/sphere-in-tetrahedron.jl index 97f0720..5d479cf 100644 --- a/engine-proto/gram-test/sphere-in-tetrahedron.jl +++ b/engine-proto/gram-test/sphere-in-tetrahedron.jl @@ -64,4 +64,13 @@ else println("\nFailed to reach target accuracy") end println("Steps: ", size(history.scaled_loss, 1)) -println("Loss: ", history.scaled_loss[end], "\n") \ No newline at end of file +println("Loss: ", history.scaled_loss[end], "\n") + +# test an alternate technique for finding the projected base step from the +# unprojected Hessian +L_alt, success_alt, history_alt = Engine.realize_gram_alt_proj(gram, guess, frozen) +completed_gram_alt = L_alt'*Engine.Q*L_alt +println("\nDifference in result using alternate projection:\n") +display(completed_gram_alt - completed_gram) +println("\nDifference in steps: ", size(history_alt.scaled_loss, 1) - size(history.scaled_loss, 1)) +println("Difference in loss: ", history_alt.scaled_loss[end] - history.scaled_loss[end], "\n") \ No newline at end of file diff --git a/engine-proto/gram-test/tetrahedron-radius-ratio.jl b/engine-proto/gram-test/tetrahedron-radius-ratio.jl index 7ceb794..9fec28e 100644 --- a/engine-proto/gram-test/tetrahedron-radius-ratio.jl +++ b/engine-proto/gram-test/tetrahedron-radius-ratio.jl @@ -93,4 +93,13 @@ if success infty = BigFloat[0, 0, 0, 0, 1] radius_ratio = dot(infty, Engine.Q * L[:,5]) / dot(infty, Engine.Q * L[:,6]) println("\nCircumradius / inradius: ", radius_ratio) -end \ No newline at end of file +end + +# test an alternate technique for finding the projected base step from the +# unprojected Hessian +L_alt, success_alt, history_alt = Engine.realize_gram_alt_proj(gram, guess, frozen) +completed_gram_alt = L_alt'*Engine.Q*L_alt +println("\nDifference in result using alternate projection:\n") +display(completed_gram_alt - completed_gram) +println("\nDifference in steps: ", size(history_alt.scaled_loss, 1) - size(history.scaled_loss, 1)) +println("Difference in loss: ", history_alt.scaled_loss[end] - history.scaled_loss[end], "\n") \ No newline at end of file -- 2.34.1 From e59d60bf7745dfb25ca054a5beecb08bc8f29b2f Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 25 Oct 2024 17:17:49 -0700 Subject: [PATCH 02/29] Reorganize search state; remove unused variables --- engine-proto/gram-test/Engine.jl | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/engine-proto/gram-test/Engine.jl b/engine-proto/gram-test/Engine.jl index 1eb72f5..6dfb6e9 100644 --- a/engine-proto/gram-test/Engine.jl +++ b/engine-proto/gram-test/Engine.jl @@ -324,7 +324,6 @@ function realize_gram_alt_proj( frozen = CartesianIndex[]; scaled_tol = 1e-30, min_efficiency = 0.5, - init_rate = 1.0, backoff = 0.9, reg_scale = 1.1, max_descent_steps = 200, @@ -349,13 +348,12 @@ function realize_gram_alt_proj( # convert the frozen indices to stacked format frozen_stacked = [(index[2]-1)*element_dim + index[1] for index in frozen] - # initialize variables - grad_rate = init_rate + # initialize search state L = copy(guess) - - # use Newton's method with backtracking and gradient descent backup Δ_proj = proj_diff(gram, L'*Q*L) loss = dot(Δ_proj, Δ_proj) + + # use Newton's method with backtracking and gradient descent backup for step in 1:max_descent_steps # stop if the loss is tolerably low if loss < tol @@ -411,6 +409,7 @@ function realize_gram_alt_proj( empty!(history.last_line_loss) rate = one(T) step_success = false + base_target_improvement = dot(neg_grad, base_step) for backoff_steps in 0:max_backoff_steps history.stepsize[end] = rate L = L_last + rate * base_step @@ -419,7 +418,7 @@ function realize_gram_alt_proj( improvement = loss_last - loss push!(history.last_line_L, L) push!(history.last_line_loss, loss / scale_adjustment) - if improvement >= min_efficiency * rate * dot(neg_grad, base_step) + if improvement >= min_efficiency * rate * base_target_improvement history.backoff_steps[end] = backoff_steps step_success = true break @@ -446,7 +445,6 @@ function realize_gram( frozen = nothing; scaled_tol = 1e-30, min_efficiency = 0.5, - init_rate = 1.0, backoff = 0.9, reg_scale = 1.1, max_descent_steps = 200, @@ -477,13 +475,12 @@ function realize_gram( unfrozen_stacked = reshape(is_unfrozen, total_dim) end - # initialize variables - grad_rate = init_rate + # initialize search state L = copy(guess) - - # use Newton's method with backtracking and gradient descent backup Δ_proj = proj_diff(gram, L'*Q*L) loss = dot(Δ_proj, Δ_proj) + + # use Newton's method with backtracking and gradient descent backup for step in 1:max_descent_steps # stop if the loss is tolerably low if loss < tol @@ -545,6 +542,7 @@ function realize_gram( empty!(history.last_line_loss) rate = one(T) step_success = false + base_target_improvement = dot(neg_grad, base_step) for backoff_steps in 0:max_backoff_steps history.stepsize[end] = rate L = L_last + rate * base_step @@ -553,7 +551,7 @@ function realize_gram( improvement = loss_last - loss push!(history.last_line_L, L) push!(history.last_line_loss, loss / scale_adjustment) - if improvement >= min_efficiency * rate * dot(neg_grad, base_step) + if improvement >= min_efficiency * rate * base_target_improvement history.backoff_steps[end] = backoff_steps step_success = true break -- 2.34.1 From 9fe03264ab15201180c6759d03fe6094d0a25eac Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 25 Oct 2024 17:34:29 -0700 Subject: [PATCH 03/29] Port the Gram matrix realization routine to Rust Validate with the process inspection example tests, which print out their results and optimization histories when run one at a time in `--nocapture` mode. --- app-proto/Cargo.toml | 1 + app-proto/run-examples | 7 + app-proto/src/engine.rs | 321 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 328 insertions(+), 1 deletion(-) create mode 100755 app-proto/run-examples diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 920469a..04ea271 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -10,6 +10,7 @@ default = ["console_error_panic_hook"] [dependencies] itertools = "0.13.0" js-sys = "0.3.70" +lazy_static = "1.5.0" nalgebra = "0.33.0" rustc-hash = "2.0.0" slab = "0.4.9" diff --git a/app-proto/run-examples b/app-proto/run-examples new file mode 100755 index 0000000..b5c3de5 --- /dev/null +++ b/app-proto/run-examples @@ -0,0 +1,7 @@ +# based on "Enabling print statements in Cargo tests", by Jon Almeida +# +# https://jonalmeida.com/posts/2015/01/23/print-cargo/ +# + +cargo test -- --nocapture engine::tests::three_spheres_example +cargo test -- --nocapture engine::tests::point_on_sphere_example diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 79668bb..f5ad19a 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -1,4 +1,11 @@ -use nalgebra::DVector; +use lazy_static::lazy_static; +use nalgebra::{Const, DMatrix, DVector, Dyn}; + +// --- elements --- + +pub fn point(x: f64, y: f64, z: f64) -> DVector { + DVector::from_column_slice(&[x, y, z, 0.5, 0.5*(x*x + y*y + z*z)]) +} // the sphere with the given center and radius, with inward-pointing normals pub fn sphere(center_x: f64, center_y: f64, center_z: f64, radius: f64) -> DVector { @@ -24,4 +31,316 @@ pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f6 0.5 * curv, off * (1.0 + 0.5 * off * curv) ]) +} + +// --- partial matrices --- + +struct MatrixEntry { + index: (usize, usize), + val: f64 +} + +struct PartialMatrix(Vec); + +impl PartialMatrix { + fn proj(&self, a: &DMatrix) -> DMatrix { + let mut result = DMatrix::::zeros(a.nrows(), a.ncols()); + let PartialMatrix(entries) = self; + for ent in entries { + result[ent.index] = a[ent.index]; + } + result + } + + fn sub_proj(&self, rhs: &DMatrix) -> DMatrix { + let mut result = DMatrix::::zeros(rhs.nrows(), rhs.ncols()); + let PartialMatrix(entries) = self; + for ent in entries { + result[ent.index] = ent.val - rhs[ent.index]; + } + result + } +} + +// --- gram matrix realization --- + +// the Lorentz form +lazy_static! { + static ref Q: DMatrix = DMatrix::from_row_slice(5, 5, &[ + 1.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, -2.0, + 0.0, 0.0, 0.0, -2.0, 0.0 + ]); +} + +struct SearchState { + config: DMatrix, + err_proj: DMatrix, + loss: f64 +} + +impl SearchState { + fn from_config(gram: &PartialMatrix, config: DMatrix) -> SearchState { + let err_proj = gram.sub_proj(&(config.tr_mul(&*Q) * &config)); + let loss = err_proj.norm_squared(); + SearchState { + config: config, + err_proj: err_proj, + loss: loss + } + } +} + +fn basis_matrix(index: (usize, usize), nrows: usize, ncols: usize) -> DMatrix { + let mut result = DMatrix::::zeros(nrows, ncols); + result[index] = 1.0; + result +} + +// use backtracking line search to find a better configuration +fn seek_better_config( + gram: &PartialMatrix, + state: &SearchState, + base_step: &DMatrix, + base_target_improvement: f64, + min_efficiency: f64, + backoff: f64, + max_backoff_steps: i32 +) -> Option { + let mut rate = 1.0; + for _ in 0..max_backoff_steps { + let trial_config = &state.config + rate * base_step; + let trial_state = SearchState::from_config(gram, trial_config); + let improvement = state.loss - trial_state.loss; + if improvement >= min_efficiency * rate * base_target_improvement { + return Some(trial_state); + } + rate *= backoff; + } + None +} + +// seek a matrix `config` for which `config' * Q * config` matches the partial +// matrix `gram`. use gradient descent starting from `guess` +fn realize_gram( + gram: &PartialMatrix, + guess: DMatrix, + frozen: &[(usize, usize)], + scaled_tol: f64, + min_efficiency: f64, + backoff: f64, + reg_scale: f64, + max_descent_steps: i32, + max_backoff_steps: i32 +) -> (DMatrix, bool) { + // find the dimension of the search space + let element_dim = guess.nrows(); + let assembly_dim = guess.ncols(); + let total_dim = element_dim * assembly_dim; + + // scale the tolerance + let scale_adjustment = ((guess.ncols() - frozen.len()) as f64).sqrt(); + let tol = scale_adjustment * scaled_tol; + + // convert the frozen indices to stacked format + let frozen_stacked: Vec = frozen.into_iter().map( + |index| index.1*element_dim + index.0 + ).collect(); + + // use Newton's method with backtracking and gradient descent backup + let mut state = SearchState::from_config(gram, guess); + for _ in 0..max_descent_steps { + // stop if the loss is tolerably low + println!("loss: {}", state.loss); + /*println!("projected error: {}", state.err_proj);*/ + if state.loss < tol { break; } + + // find the negative gradient of the loss function + 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>); + + // find the negative Hessian of the loss function + let mut hess_cols = Vec::>::with_capacity(total_dim); + for col in 0..assembly_dim { + for row in 0..element_dim { + let index = (row, col); + let basis_mat = basis_matrix(index, element_dim, assembly_dim); + let neg_d_err = + basis_mat.tr_mul(&*Q) * &state.config + + state.config.tr_mul(&*Q) * &basis_mat; + let neg_d_err_proj = gram.proj(&neg_d_err); + let deriv_grad = 4.0 * &*Q * ( + -&basis_mat * &state.err_proj + + &state.config * &neg_d_err_proj + ); + hess_cols.push(deriv_grad.reshape_generic(Dyn(total_dim), Const::<1>)); + } + } + let mut hess = DMatrix::from_columns(hess_cols.as_slice()); + + // regularize the Hessian + let min_eigval = hess.symmetric_eigenvalues().min(); + if min_eigval <= 0.0 { + hess -= reg_scale * min_eigval * DMatrix::identity(total_dim, total_dim); + } + + // project the negative gradient and negative Hessian onto the + // orthogonal complement of the frozen subspace + let zero_col = DVector::zeros(total_dim); + let zero_row = zero_col.transpose(); + for &k in &frozen_stacked { + neg_grad_stacked[k] = 0.0; + hess.set_row(k, &zero_row); + hess.set_column(k, &zero_col); + hess[(k, k)] = 1.0; + } + + // compute the Newton step + 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)); + + // use backtracking line search to find a better configuration + match seek_better_config( + gram, &state, &base_step, neg_grad.dot(&base_step), + min_efficiency, backoff, max_backoff_steps + ) { + Some(better_state) => state = better_state, + None => return (state.config, false) + }; + } + (state.config, state.loss < tol) +} + +// --- tests --- + +#[cfg(test)] +mod tests { + use std::f64; + + use super::*; + + #[test] + fn sub_proj_test() { + let target = PartialMatrix(vec![ + MatrixEntry { index: (0, 0), val: 19.0 }, + MatrixEntry { index: (0, 2), val: 39.0 }, + MatrixEntry { index: (1, 1), val: 59.0 }, + MatrixEntry { index: (1, 2), val: 69.0 } + ]); + let attempt = DMatrix::::from_row_slice(2, 3, &[ + 1.0, 2.0, 3.0, + 4.0, 5.0, 6.0 + ]); + let expected_result = DMatrix::::from_row_slice(2, 3, &[ + 18.0, 0.0, 36.0, + 0.0, 54.0, 63.0 + ]); + assert_eq!(target.sub_proj(&attempt), expected_result); + } + + #[test] + fn zero_loss_test() { + let gram = PartialMatrix({ + let mut entries = Vec::::new(); + for j in 0..3 { + for k in 0..3 { + entries.push(MatrixEntry { + index: (j, k), + val: if j == k { 1.0 } else { -1.0 } + }); + } + } + entries + }); + let config = { + let a: f64 = 0.75_f64.sqrt(); + DMatrix::from_columns(&[ + sphere(1.0, 0.0, 0.0, a), + sphere(-0.5, a, 0.0, a), + sphere(-0.5, -a, 0.0, a) + ]) + }; + let state = SearchState::from_config(&gram, config); + assert!(state.loss.abs() < f64::EPSILON); + } + + // --- process inspection examples --- + + // these tests are meant for human inspection, not automated use. run them + // one at a time in `--nocapture` mode and read through the results and + // optimization histories that they print out. the `run-examples` script + // will run all of them + + #[test] + fn three_spheres_example() { + let gram = PartialMatrix({ + let mut entries = Vec::::new(); + for j in 0..3 { + for k in 0..3 { + entries.push(MatrixEntry { + index: (j, k), + val: if j == k { 1.0 } else { -1.0 } + }); + } + } + entries + }); + let guess = { + let a: f64 = 0.75_f64.sqrt(); + DMatrix::from_columns(&[ + 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) + ]) + }; + println!(); + let (config, success) = realize_gram( + &gram, guess, &[], + 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + ); + print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); + let final_state = SearchState::from_config(&gram, config); + if success { + println!("Target accuracy achieved!"); + } else { + println!("Failed to reach target accuracy"); + } + println!("Loss: {}", final_state.loss); + } + + #[test] + fn point_on_sphere_example() { + let gram = PartialMatrix({ + let mut entries = Vec::::new(); + for j in 0..2 { + for k in 0..2 { + entries.push(MatrixEntry { + index: (j, k), + val: if (j, k) == (1, 1) { 1.0 } else { 0.0 } + }); + } + } + entries + }); + let guess = DMatrix::from_columns(&[ + point(0.0, 0.0, 2.0), + sphere(0.0, 0.0, 0.0, 1.0) + ]); + println!(); + let (config, success) = realize_gram( + &gram, guess, &[(3, 0)], + 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + ); + print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); + print!("Configuration:{}", config); + let final_state = SearchState::from_config(&gram, config); + if success { + println!("Target accuracy achieved!"); + } else { + println!("Failed to reach target accuracy"); + } + println!("Loss: {}", final_state.loss); + } } \ No newline at end of file -- 2.34.1 From 9f8632efb34977393ccb9fc315317f5d69b81bf4 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 25 Oct 2024 21:43:53 -0700 Subject: [PATCH 04/29] Port the Irisawa hexlet test to Rust In the process, notice that the tolerance scale adjustment was ported wrong, and correct it. --- app-proto/run-examples | 1 + app-proto/src/engine.rs | 118 +++++++++++++++++++++++++++++++++++----- 2 files changed, 105 insertions(+), 14 deletions(-) diff --git a/app-proto/run-examples b/app-proto/run-examples index b5c3de5..6a5e3ae 100755 --- a/app-proto/run-examples +++ b/app-proto/run-examples @@ -3,5 +3,6 @@ # https://jonalmeida.com/posts/2015/01/23/print-cargo/ # +cargo test -- --nocapture engine::tests::irisawa_hexlet_test cargo test -- --nocapture engine::tests::three_spheres_example cargo test -- --nocapture engine::tests::point_on_sphere_example diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index f5ad19a..6f2952c 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -37,7 +37,7 @@ pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f6 struct MatrixEntry { index: (usize, usize), - val: f64 + value: f64 } struct PartialMatrix(Vec); @@ -56,7 +56,7 @@ impl PartialMatrix { let mut result = DMatrix::::zeros(rhs.nrows(), rhs.ncols()); let PartialMatrix(entries) = self; for ent in entries { - result[ent.index] = ent.val - rhs[ent.index]; + result[ent.index] = ent.value - rhs[ent.index]; } result } @@ -141,7 +141,7 @@ fn realize_gram( let total_dim = element_dim * assembly_dim; // scale the tolerance - let scale_adjustment = ((guess.ncols() - frozen.len()) as f64).sqrt(); + let scale_adjustment = (gram.0.len() as f64).sqrt(); let tol = scale_adjustment * scaled_tol; // convert the frozen indices to stacked format @@ -153,8 +153,8 @@ fn realize_gram( let mut state = SearchState::from_config(gram, guess); for _ in 0..max_descent_steps { // stop if the loss is tolerably low - println!("loss: {}", state.loss); - /*println!("projected error: {}", state.err_proj);*/ + println!("scaled loss: {}", state.loss / scale_adjustment); + /* println!("projected error: {}", state.err_proj); */ if state.loss < tol { break; } // find the negative gradient of the loss function @@ -182,6 +182,7 @@ fn realize_gram( // regularize the Hessian let min_eigval = hess.symmetric_eigenvalues().min(); + /* println!("lowest eigenvalue: {}", min_eigval); */ if min_eigval <= 0.0 { hess -= reg_scale * min_eigval * DMatrix::identity(total_dim, total_dim); } @@ -198,6 +199,12 @@ fn realize_gram( } // compute the Newton step + /* + we need to either handle or eliminate the case where the minimum + eigenvalue of the Hessian is zero, so the regularized Hessian is + singular. right now, this causes the Cholesky decomposition to return + `None`, leading to a panic when we unrap + */ 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)); @@ -217,17 +224,17 @@ fn realize_gram( #[cfg(test)] mod tests { - use std::f64; + use std::{array, f64::consts::PI}; use super::*; #[test] fn sub_proj_test() { let target = PartialMatrix(vec![ - MatrixEntry { index: (0, 0), val: 19.0 }, - MatrixEntry { index: (0, 2), val: 39.0 }, - MatrixEntry { index: (1, 1), val: 59.0 }, - MatrixEntry { index: (1, 2), val: 69.0 } + MatrixEntry { index: (0, 0), value: 19.0 }, + MatrixEntry { index: (0, 2), value: 39.0 }, + MatrixEntry { index: (1, 1), value: 59.0 }, + MatrixEntry { index: (1, 2), value: 69.0 } ]); let attempt = DMatrix::::from_row_slice(2, 3, &[ 1.0, 2.0, 3.0, @@ -248,7 +255,7 @@ mod tests { for k in 0..3 { entries.push(MatrixEntry { index: (j, k), - val: if j == k { 1.0 } else { -1.0 } + value: if j == k { 1.0 } else { -1.0 } }); } } @@ -266,6 +273,88 @@ mod tests { assert!(state.loss.abs() < f64::EPSILON); } + // this problem is from a sangaku by Irisawa Shintarō Hiroatsu. the article + // below includes a nice translation of the problem statement, which was + // recorded in Uchida Itsumi's book _Kokon sankan_ (_Mathematics, Past and + // Present_) + // + // "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki + // https://www.nippon.com/en/japan-topics/c12801/ + // + #[test] + fn irisawa_hexlet_test() { + let gram = PartialMatrix({ + let mut entries = Vec::::new(); + for s in 0..9 { + // each sphere is represented by a spacelike vector + entries.push(MatrixEntry { index: (s, s), value: 1.0 }); + + // the circumscribing sphere is tangent to all of the other + // spheres, with matching orientation + if s > 0 { + entries.push(MatrixEntry { index: (0, s), value: 1.0 }); + entries.push(MatrixEntry { index: (s, 0), value: 1.0 }); + } + + if s > 2 { + // each chain sphere is tangent to the "sun" and "moon" + // spheres, with opposing orientation + for n in 1..3 { + entries.push(MatrixEntry { index: (s, n), value: -1.0 }); + entries.push(MatrixEntry { index: (n, s), value: -1.0 }); + } + + // each chain sphere is tangent to the next chain sphere, + // with opposing orientation + let s_next = 3 + (s-2) % 6; + entries.push(MatrixEntry { index: (s, s_next), value: -1.0 }); + entries.push(MatrixEntry { index: (s_next, s), value: -1.0 }); + } + } + entries + }); + let guess = DMatrix::from_columns( + [ + sphere(0.0, 0.0, 0.0, 15.0), + sphere(0.0, 0.0, -9.0, 5.0), + sphere(0.0, 0.0, 11.0, 3.0) + ].into_iter().chain( + (1..=6).map( + |k| { + let ang = (k as f64) * PI/3.0; + sphere(9.0 * ang.cos(), 9.0 * ang.sin(), 0.0, 2.5) + } + ) + ).collect::>().as_slice() + ); + let frozen: [(usize, usize); 4] = array::from_fn(|k| (3, k)); + const SCALED_TOL: f64 = 1.0e-12; + let (config, success) = realize_gram( + &gram, guess, &frozen, + SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 + ); + print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); + let final_state = SearchState::from_config(&gram, config); + if success { + println!("Target accuracy achieved!"); + } else { + println!("Failed to reach target accuracy"); + } + println!("Loss: {}", final_state.loss); + if success { + println!("\nChain diameters:"); + println!(" {} sun (given)", 1.0 / final_state.config[(3, 3)]); + for k in 4..9 { + println!(" {} sun", 1.0 / final_state.config[(3, k)]); + } + } + let entry_tol = SCALED_TOL.sqrt(); + let solution_diams = [30.0, 10.0, 6.0, 5.0, 15.0, 10.0, 3.75, 2.5, 2.0 + 8.0/11.0]; + for (k, diam) in solution_diams.into_iter().enumerate() { + assert!((final_state.config[(3, k)] - 1.0 / diam).abs() < entry_tol); + } + } + // --- process inspection examples --- // these tests are meant for human inspection, not automated use. run them @@ -281,7 +370,7 @@ mod tests { for k in 0..3 { entries.push(MatrixEntry { index: (j, k), - val: if j == k { 1.0 } else { -1.0 } + value: if j == k { 1.0 } else { -1.0 } }); } } @@ -318,7 +407,7 @@ mod tests { for k in 0..2 { entries.push(MatrixEntry { index: (j, k), - val: if (j, k) == (1, 1) { 1.0 } else { 0.0 } + value: if (j, k) == (1, 1) { 1.0 } else { 0.0 } }); } } @@ -328,9 +417,10 @@ mod tests { point(0.0, 0.0, 2.0), sphere(0.0, 0.0, 0.0, 1.0) ]); + let frozen = [(3, 0)]; println!(); let (config, success) = realize_gram( - &gram, guess, &[(3, 0)], + &gram, guess, &frozen, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); -- 2.34.1 From ce33bbf41877094c6a2985e6b34f73ff50c55bd1 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 26 Oct 2024 00:02:06 -0700 Subject: [PATCH 05/29] Record optimization history --- app-proto/src/engine.rs | 94 ++++++++++++++++++++++++++++++----------- 1 file changed, 69 insertions(+), 25 deletions(-) diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 6f2952c..a4d89a1 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -62,6 +62,30 @@ impl PartialMatrix { } } +// --- descent history --- + +struct DescentHistory { + config: Vec>, + scaled_loss: Vec, + neg_grad: Vec>, + min_eigval: Vec, + base_step: Vec>, + backoff_steps: Vec +} + +impl DescentHistory { + fn new() -> DescentHistory { + DescentHistory { + config: Vec::>::new(), + scaled_loss: Vec::::new(), + neg_grad: Vec::>::new(), + min_eigval: Vec::::new(), + base_step: Vec::>::new(), + backoff_steps: Vec::::new(), + } + } +} + // --- gram matrix realization --- // the Lorentz form @@ -108,14 +132,14 @@ fn seek_better_config( min_efficiency: f64, backoff: f64, max_backoff_steps: i32 -) -> Option { +) -> Option<(SearchState, i32)> { let mut rate = 1.0; - for _ in 0..max_backoff_steps { + for backoff_steps in 0..max_backoff_steps { let trial_config = &state.config + rate * base_step; let trial_state = SearchState::from_config(gram, trial_config); let improvement = state.loss - trial_state.loss; if improvement >= min_efficiency * rate * base_target_improvement { - return Some(trial_state); + return Some((trial_state, backoff_steps)); } rate *= backoff; } @@ -134,7 +158,10 @@ fn realize_gram( reg_scale: f64, max_descent_steps: i32, max_backoff_steps: i32 -) -> (DMatrix, bool) { +) -> (DMatrix, bool, DescentHistory) { + // start the descent history + let mut history = DescentHistory::new(); + // find the dimension of the search space let element_dim = guess.nrows(); let assembly_dim = guess.ncols(); @@ -153,13 +180,14 @@ fn realize_gram( let mut state = SearchState::from_config(gram, guess); for _ in 0..max_descent_steps { // stop if the loss is tolerably low - println!("scaled loss: {}", state.loss / scale_adjustment); - /* println!("projected error: {}", state.err_proj); */ + 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 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>); + history.neg_grad.push(neg_grad.clone()); // find the negative Hessian of the loss function let mut hess_cols = Vec::>::with_capacity(total_dim); @@ -182,10 +210,10 @@ fn realize_gram( // regularize the Hessian let min_eigval = hess.symmetric_eigenvalues().min(); - /* println!("lowest eigenvalue: {}", min_eigval); */ if min_eigval <= 0.0 { hess -= reg_scale * min_eigval * DMatrix::identity(total_dim, total_dim); } + history.min_eigval.push(min_eigval); // project the negative gradient and negative Hessian onto the // orthogonal complement of the frozen subspace @@ -207,17 +235,21 @@ fn realize_gram( */ 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)); + history.base_step.push(base_step.clone()); // use backtracking line search to find a better configuration match seek_better_config( gram, &state, &base_step, neg_grad.dot(&base_step), min_efficiency, backoff, max_backoff_steps ) { - Some(better_state) => state = better_state, - None => return (state.config, false) + Some((better_state, backoff_steps)) => { + state = better_state; + history.backoff_steps.push(backoff_steps); + }, + None => return (state.config, false, history) }; } - (state.config, state.loss < tol) + (state.config, state.loss < tol, history) } // --- tests --- @@ -329,29 +361,33 @@ mod tests { ); let frozen: [(usize, usize); 4] = array::from_fn(|k| (3, k)); const SCALED_TOL: f64 = 1.0e-12; - let (config, success) = realize_gram( + let (config, success, history) = realize_gram( &gram, guess, &frozen, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); + let entry_tol = SCALED_TOL.sqrt(); + let solution_diams = [30.0, 10.0, 6.0, 5.0, 15.0, 10.0, 3.75, 2.5, 2.0 + 8.0/11.0]; + for (k, diam) in solution_diams.into_iter().enumerate() { + assert!((config[(3, k)] - 1.0 / diam).abs() < entry_tol); + } print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); - let final_state = SearchState::from_config(&gram, config); if success { println!("Target accuracy achieved!"); } else { println!("Failed to reach target accuracy"); } - println!("Loss: {}", final_state.loss); + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}", history.scaled_loss.last().unwrap()); if success { println!("\nChain diameters:"); - println!(" {} sun (given)", 1.0 / final_state.config[(3, 3)]); + println!(" {} sun (given)", 1.0 / config[(3, 3)]); for k in 4..9 { - println!(" {} sun", 1.0 / final_state.config[(3, k)]); + println!(" {} sun", 1.0 / config[(3, k)]); } } - let entry_tol = SCALED_TOL.sqrt(); - let solution_diams = [30.0, 10.0, 6.0, 5.0, 15.0, 10.0, 3.75, 2.5, 2.0 + 8.0/11.0]; - for (k, diam) in solution_diams.into_iter().enumerate() { - assert!((final_state.config[(3, k)] - 1.0 / diam).abs() < entry_tol); + println!("\nStep │ Loss\n─────┼────────────────────────────────"); + for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { + println!("{:<4} │ {}", step, scaled_loss); } } @@ -385,18 +421,22 @@ mod tests { ]) }; println!(); - let (config, success) = realize_gram( + let (config, success, history) = realize_gram( &gram, guess, &[], 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); - let final_state = SearchState::from_config(&gram, config); if success { println!("Target accuracy achieved!"); } else { println!("Failed to reach target accuracy"); } - println!("Loss: {}", final_state.loss); + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}", history.scaled_loss.last().unwrap()); + println!("\nStep │ Loss\n─────┼────────────────────────────────"); + for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { + println!("{:<4} │ {}", step, scaled_loss); + } } #[test] @@ -419,18 +459,22 @@ mod tests { ]); let frozen = [(3, 0)]; println!(); - let (config, success) = realize_gram( + let (config, success, history) = realize_gram( &gram, guess, &frozen, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); print!("Configuration:{}", config); - let final_state = SearchState::from_config(&gram, config); if success { println!("Target accuracy achieved!"); } else { println!("Failed to reach target accuracy"); } - println!("Loss: {}", final_state.loss); + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}", history.scaled_loss.last().unwrap()); + println!("\nStep │ Loss\n─────┼────────────────────────────────"); + for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { + println!("{:<4} │ {}", step, scaled_loss); + } } } \ No newline at end of file -- 2.34.1 From a37c71153d198e64895ced18e71cd39ebee3bc64 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 26 Oct 2024 23:51:27 -0700 Subject: [PATCH 06/29] Enforce constraints in the editor --- app-proto/src/add_remove.rs | 53 +++++++++++---------- app-proto/src/assembly.rs | 94 +++++++++++++++++++++++++++++++++++-- app-proto/src/engine.rs | 40 ++++++++++++---- 3 files changed, 151 insertions(+), 36 deletions(-) diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index ab5db70..7066089 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -12,7 +12,8 @@ fn load_gen_assemb(assembly: &Assembly) { label: String::from("Castor"), color: [1.00_f32, 0.25_f32, 0.00_f32], rep: engine::sphere(0.5, 0.5, 0.0, 1.0), - constraints: BTreeSet::default() + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -21,7 +22,8 @@ fn load_gen_assemb(assembly: &Assembly) { label: String::from("Pollux"), color: [0.00_f32, 0.25_f32, 1.00_f32], rep: engine::sphere(-0.5, -0.5, 0.0, 1.0), - constraints: BTreeSet::default() + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -30,7 +32,8 @@ fn load_gen_assemb(assembly: &Assembly) { label: String::from("Ursa major"), color: [0.25_f32, 0.00_f32, 1.00_f32], rep: engine::sphere(-0.5, 0.5, 0.0, 0.75), - constraints: BTreeSet::default() + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -39,7 +42,8 @@ fn load_gen_assemb(assembly: &Assembly) { label: String::from("Ursa minor"), color: [0.25_f32, 1.00_f32, 0.00_f32], rep: engine::sphere(0.5, -0.5, 0.0, 0.5), - constraints: BTreeSet::default() + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -48,7 +52,8 @@ fn load_gen_assemb(assembly: &Assembly) { label: String::from("Deimos"), color: [0.75_f32, 0.75_f32, 0.00_f32], rep: engine::sphere(0.0, 0.15, 1.0, 0.25), - constraints: BTreeSet::default() + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -57,17 +62,8 @@ fn load_gen_assemb(assembly: &Assembly) { label: String::from("Phobos"), color: [0.00_f32, 0.75_f32, 0.50_f32], rep: engine::sphere(0.0, -0.15, -1.0, 0.25), - constraints: BTreeSet::default() - } - ); - assembly.insert_constraint( - Constraint { - args: ( - assembly.elements_by_id.with_untracked(|elts_by_id| elts_by_id["gemini_a"]), - assembly.elements_by_id.with_untracked(|elts_by_id| elts_by_id["gemini_b"]) - ), - rep: 0.5, - active: create_signal(true) + constraints: BTreeSet::default(), + index: 0 } ); } @@ -81,7 +77,8 @@ fn load_low_curv_assemb(assembly: &Assembly) { label: "Central".to_string(), color: [0.75_f32, 0.75_f32, 0.75_f32], rep: engine::sphere(0.0, 0.0, 0.0, 1.0), - constraints: BTreeSet::default() + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -90,7 +87,8 @@ fn load_low_curv_assemb(assembly: &Assembly) { label: "Assembly plane".to_string(), color: [0.75_f32, 0.75_f32, 0.75_f32], rep: engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0), - constraints: BTreeSet::default() + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -99,7 +97,8 @@ fn load_low_curv_assemb(assembly: &Assembly) { label: "Side 1".to_string(), color: [1.00_f32, 0.00_f32, 0.25_f32], rep: engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0), - constraints: BTreeSet::default() + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -108,7 +107,8 @@ fn load_low_curv_assemb(assembly: &Assembly) { label: "Side 2".to_string(), color: [0.25_f32, 1.00_f32, 0.00_f32], rep: engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0), - constraints: BTreeSet::default() + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -117,7 +117,8 @@ fn load_low_curv_assemb(assembly: &Assembly) { label: "Side 3".to_string(), color: [0.00_f32, 0.25_f32, 1.00_f32], rep: engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0), - constraints: BTreeSet::default() + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -126,7 +127,8 @@ fn load_low_curv_assemb(assembly: &Assembly) { label: "Corner 1".to_string(), color: [0.75_f32, 0.75_f32, 0.75_f32], rep: engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0), - constraints: BTreeSet::default() + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -135,7 +137,8 @@ fn load_low_curv_assemb(assembly: &Assembly) { label: "Corner 2".to_string(), color: [0.75_f32, 0.75_f32, 0.75_f32], rep: engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0), - constraints: BTreeSet::default() + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -144,7 +147,8 @@ fn load_low_curv_assemb(assembly: &Assembly) { label: String::from("Corner 3"), color: [0.75_f32, 0.75_f32, 0.75_f32], rep: engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0), - constraints: BTreeSet::default() + constraints: BTreeSet::default(), + index: 0 } ); } @@ -215,6 +219,7 @@ pub fn AddRemove() -> View { rep: 0.0, active: create_signal(true) }); + state.assembly.realize(); state.selection.update(|sel| sel.clear()); /* DEBUG */ diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index e8dab79..228357e 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,8 +1,11 @@ -use nalgebra::DVector; +use nalgebra::{DMatrix, DVector}; use rustc_hash::FxHashMap; use slab::Slab; use std::collections::BTreeSet; use sycamore::prelude::*; +use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ + +use crate::engine::{realize_gram, PartialMatrix}; #[derive(Clone, PartialEq)] pub struct Element { @@ -10,7 +13,10 @@ pub struct Element { pub label: String, pub color: [f32; 3], pub rep: DVector, - pub constraints: BTreeSet + pub constraints: BTreeSet, + + // internal properties, not reflected in any view + pub index: usize } #[derive(Clone)] @@ -40,6 +46,8 @@ impl Assembly { } } + // --- inserting elements and constraints --- + // insert an element into the assembly without checking whether we already // have an element with the same identifier. any element that does have the // same identifier will get kicked out of the `elements_by_id` index @@ -77,7 +85,8 @@ impl Assembly { label: format!("Sphere {}", id_num), color: [0.75_f32, 0.75_f32, 0.75_f32], rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5]), - constraints: BTreeSet::default() + constraints: BTreeSet::default(), + index: 0 } ); } @@ -90,4 +99,83 @@ impl Assembly { elts[args.1].constraints.insert(key); }) } + + // --- realization --- + + pub fn realize(&self) { + // index the elements + self.elements.update_silent(|elts| { + for (index, (_, elt)) in elts.into_iter().enumerate() { + elt.index = index; + } + }); + + // set up the Gram matrix and the initial configuration matrix + let (gram, guess) = self.elements.with_untracked(|elts| { + // set up the off-diagonal part of the Gram matrix + let mut gram_to_be = PartialMatrix::new(); + self.constraints.with_untracked(|csts| { + for (_, cst) in csts { + let args = cst.args; + let row = elts[args.0].index; + let col = elts[args.1].index; + gram_to_be.push_sym(row, col, cst.rep); + } + }); + + // set up the initial configuration matrix and the diagonal of the + // Gram matrix + let mut guess_to_be = DMatrix::::zeros(5, elts.len()); + for (_, elt) in elts { + let index = elt.index; + gram_to_be.push_sym(index, index, 1.0); + guess_to_be.set_column(index, &elt.rep); + } + + (gram_to_be, guess_to_be) + }); + + /* DEBUG */ + // log the Gram matrix + console::log_1(&JsValue::from("Gram matrix:")); + gram.log_to_console(); + + /* DEBUG */ + // log the initial configuration matrix + console::log_1(&JsValue::from("old configuration:")); + for j in 0..guess.nrows() { + let mut row_str = String::new(); + for k in 0..guess.ncols() { + row_str.push_str(format!(" {:>8.3}", guess[(j, k)]).as_str()); + } + console::log_1(&JsValue::from(row_str)); + } + + // look for a configuration with the given Gram matrix + let (config, success, history) = realize_gram( + &gram, guess, &[], + 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + ); + + /* DEBUG */ + // report the outcome of the search + console::log_1(&JsValue::from( + if success { + "Target accuracy achieved!" + } else { + "Failed to reach target accuracy" + } + )); + 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())); + + if success { + // read out the solution + self.elements.update(|elts| { + for (_, elt) in elts.iter_mut() { + elt.rep.set_column(0, &config.column(elt.index)); + } + }); + } + } } \ No newline at end of file diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index a4d89a1..2971750 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -1,5 +1,6 @@ use lazy_static::lazy_static; use nalgebra::{Const, DMatrix, DVector, Dyn}; +use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ // --- elements --- @@ -40,9 +41,30 @@ struct MatrixEntry { value: f64 } -struct PartialMatrix(Vec); +pub struct PartialMatrix(Vec); impl PartialMatrix { + pub fn new() -> PartialMatrix { + PartialMatrix(Vec::::new()) + } + + pub fn push_sym(&mut self, row: usize, col: usize, value: f64) { + let PartialMatrix(entries) = self; + entries.push(MatrixEntry { index: (row, col), value: value }); + if row != col { + entries.push(MatrixEntry { index: (col, row), value: value }); + } + } + + /* DEBUG */ + pub fn log_to_console(&self) { + let PartialMatrix(entries) = self; + for ent in entries { + let ent_str = format!("{} {} {}", ent.index.0, ent.index.1, ent.value); + console::log_1(&JsValue::from(ent_str.as_str())); + } + } + fn proj(&self, a: &DMatrix) -> DMatrix { let mut result = DMatrix::::zeros(a.nrows(), a.ncols()); let PartialMatrix(entries) = self; @@ -64,13 +86,13 @@ impl PartialMatrix { // --- descent history --- -struct DescentHistory { - config: Vec>, - scaled_loss: Vec, - neg_grad: Vec>, - min_eigval: Vec, - base_step: Vec>, - backoff_steps: Vec +pub struct DescentHistory { + pub config: Vec>, + pub scaled_loss: Vec, + pub neg_grad: Vec>, + pub min_eigval: Vec, + pub base_step: Vec>, + pub backoff_steps: Vec } impl DescentHistory { @@ -148,7 +170,7 @@ fn seek_better_config( // seek a matrix `config` for which `config' * Q * config` matches the partial // matrix `gram`. use gradient descent starting from `guess` -fn realize_gram( +pub fn realize_gram( gram: &PartialMatrix, guess: DMatrix, frozen: &[(usize, usize)], -- 2.34.1 From e5f4d523f90161a4615c5d4c51d83cb6e2a6cb1c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 29 Oct 2024 13:46:15 -0700 Subject: [PATCH 07/29] Update the realization when a constraint is activated Sycamore probably has a better way to do this, but this way works for now. --- app-proto/src/add_remove.rs | 10 +++++++++- app-proto/src/assembly.rs | 12 +++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index 7066089..00b63f8 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -214,10 +214,11 @@ pub fn AddRemove() -> View { (arg_vec[0].clone(), arg_vec[1].clone()) } ); + let active = create_signal(true); state.assembly.insert_constraint(Constraint { args: args, rep: 0.0, - active: create_signal(true) + active: active }); state.assembly.realize(); state.selection.update(|sel| sel.clear()); @@ -236,6 +237,13 @@ pub fn AddRemove() -> View { ); } }); + + // make constraint activation trigger a realization update + create_effect(move || { + if active.get() { + state.assembly.realize(); + } + }); } ) { "🔗" } select(bind:value=assembly_name) { /* DEBUG */ diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 228357e..648d0ef 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -97,7 +97,7 @@ impl Assembly { self.elements.update(|elts| { elts[args.0].constraints.insert(key); elts[args.1].constraints.insert(key); - }) + }); } // --- realization --- @@ -116,10 +116,12 @@ impl Assembly { let mut gram_to_be = PartialMatrix::new(); self.constraints.with_untracked(|csts| { for (_, cst) in csts { - let args = cst.args; - let row = elts[args.0].index; - let col = elts[args.1].index; - gram_to_be.push_sym(row, col, cst.rep); + if cst.active.get_untracked() { + let args = cst.args; + let row = elts[args.0].index; + let col = elts[args.1].index; + gram_to_be.push_sym(row, col, cst.rep); + } } }); -- 2.34.1 From e0880d2ad2d314c06fbc324bcf2bbe8aa7ba0b8d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 29 Oct 2024 22:32:00 -0700 Subject: [PATCH 08/29] Make constraints editable --- app-proto/Cargo.toml | 1 + app-proto/main.css | 11 +++++++++-- app-proto/src/add_remove.rs | 17 +++++++++++++---- app-proto/src/assembly.rs | 8 +++++--- app-proto/src/outline.rs | 25 +++++++++++++++++++++---- 5 files changed, 49 insertions(+), 13 deletions(-) diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 04ea271..e5bc05e 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -26,6 +26,7 @@ console_error_panic_hook = { version = "0.1.7", optional = true } version = "0.3.69" features = [ 'HtmlCanvasElement', + 'HtmlInputElement', 'Performance', 'WebGl2RenderingContext', 'WebGlBuffer', diff --git a/app-proto/main.css b/app-proto/main.css index bdbacfb..44dc7a1 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -93,7 +93,7 @@ details[open]:has(li) .elt-switch::after { display: flex; } -.elt-rep > div, .cst-rep { +.elt-rep > div { padding: 2px 0px 0px 0px; font-size: 10pt; text-align: center; @@ -104,10 +104,17 @@ details[open]:has(li) .elt-switch::after { font-style: italic; } -.cst > input { +.cst > input[type=checkbox] { margin: 0px 8px 0px 0px; } +.cst > input[type=number] { + color: #fcfcfc; + background-color: inherit; + border: 1px solid #555; + border-radius: 2px; +} + /* display */ canvas { diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index 00b63f8..5435418 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -214,11 +214,14 @@ pub fn AddRemove() -> View { (arg_vec[0].clone(), arg_vec[1].clone()) } ); + let rep = create_signal(0.0); let active = create_signal(true); state.assembly.insert_constraint(Constraint { args: args, - rep: 0.0, - active: active + rep: rep, + rep_text: create_signal(String::new()), + rep_valid: create_signal(false), + active: active, }); state.assembly.realize(); state.selection.update(|sel| sel.clear()); @@ -233,13 +236,19 @@ pub fn AddRemove() -> View { &JsValue::from(cst.args.0), &JsValue::from(cst.args.1), &JsValue::from(":"), - &JsValue::from(cst.rep) + &JsValue::from(cst.rep.get_untracked()) ); } }); - // make constraint activation trigger a realization update + // update the realization when the constraint activated, or + // edited while active create_effect(move || { + rep.track(); + console::log_2( + &JsValue::from("Constraint rep updated to"), + &JsValue::from(rep.get_untracked()) + ); if active.get() { state.assembly.realize(); } diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 648d0ef..62f5405 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -22,7 +22,9 @@ pub struct Element { #[derive(Clone)] pub struct Constraint { pub args: (usize, usize), - pub rep: f64, + pub rep: Signal, + pub rep_text: Signal, + pub rep_valid: Signal, pub active: Signal } @@ -116,11 +118,11 @@ impl Assembly { let mut gram_to_be = PartialMatrix::new(); self.constraints.with_untracked(|csts| { for (_, cst) in csts { - if cst.active.get_untracked() { + if cst.active.get_untracked() && cst.rep_valid.get_untracked() { let args = cst.args; let row = elts[args.0].index; let col = elts[args.1].index; - gram_to_be.push_sym(row, col, cst.rep); + gram_to_be.push_sym(row, col, cst.rep.get_untracked()); } } }); diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 4e4de9c..d6b0390 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -1,6 +1,7 @@ use itertools::Itertools; use sycamore::{prelude::*, web::tags::div}; -use web_sys::{Element, KeyboardEvent, MouseEvent, wasm_bindgen::JsCast}; +use web_sys::{Element, Event, HtmlInputElement, KeyboardEvent, MouseEvent, wasm_bindgen::JsCast}; +use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ use crate::AppState; @@ -51,8 +52,6 @@ pub fn Outline() -> View { let constrained = elt.constraints.len() > 0; let details_node = create_node_ref(); view! { - /* [TO DO] switch to integer-valued parameters whenever - that becomes possible again */ li { details(ref=details_node) { summary( @@ -138,7 +137,25 @@ pub fn Outline() -> View { li(class="cst") { input(r#type="checkbox", bind:checked=cst.active) div(class="cst-label") { (other_arg_label) } - div(class="cst-rep") { (cst.rep) } + input( + r#type="number", + step="0.01", + bind:value=cst.rep_text, + on:change=move |event: Event| { + let target: HtmlInputElement = event.target().unwrap().unchecked_into(); + let rep_valid = target.check_validity() && !target.value().is_empty(); + batch(|| { + cst.rep_valid.set(rep_valid); + if rep_valid { + console::log_2( + &JsValue::from("Constraint rep parsed to"), + &JsValue::from(target.value_as_number()) + ); + cst.rep.set(target.value_as_number()); + } + }); + } + ) } } }, -- 2.34.1 From a46ef2c8d63eaca964fceea6c1bf547a89944261 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 29 Oct 2024 22:53:48 -0700 Subject: [PATCH 09/29] Work around data binding bug in number input Setting `bind:value` or `bind:valueAsNumber` for a number input seems to restrict what you can type in it. We work around this by switching to text inputs for now. We should probably switch back to number inputs if we can, though, because they let us take advantage of the browser's parsing and validation. --- app-proto/main.css | 2 +- app-proto/src/outline.rs | 19 +++++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/app-proto/main.css b/app-proto/main.css index 44dc7a1..32ae5bf 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -108,7 +108,7 @@ details[open]:has(li) .elt-switch::after { margin: 0px 8px 0px 0px; } -.cst > input[type=number] { +.cst > input[type=text] { color: #fcfcfc; background-color: inherit; border: 1px solid #555; diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index d6b0390..62bc529 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -138,22 +138,21 @@ pub fn Outline() -> View { input(r#type="checkbox", bind:checked=cst.active) div(class="cst-label") { (other_arg_label) } input( - r#type="number", - step="0.01", + r#type="text", bind:value=cst.rep_text, on:change=move |event: Event| { let target: HtmlInputElement = event.target().unwrap().unchecked_into(); - let rep_valid = target.check_validity() && !target.value().is_empty(); - batch(|| { - cst.rep_valid.set(rep_valid); - if rep_valid { + match target.value().parse::() { + Ok(rep) => batch(|| { + cst.rep.set(rep); + cst.rep_valid.set(true); console::log_2( &JsValue::from("Constraint rep parsed to"), - &JsValue::from(target.value_as_number()) + &JsValue::from(rep) ); - cst.rep.set(target.value_as_number()); - } - }); + }), + Err(_) => cst.rep_valid.set(false) + }; } ) } -- 2.34.1 From 76ad4245d5346c4e6bafac5eeee2544608452723 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 29 Oct 2024 23:43:41 -0700 Subject: [PATCH 10/29] Factor out Lorentz product input --- app-proto/src/outline.rs | 52 +++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 62bc529..fcf983f 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -3,11 +3,36 @@ use sycamore::{prelude::*, web::tags::div}; use web_sys::{Element, Event, HtmlInputElement, KeyboardEvent, MouseEvent, wasm_bindgen::JsCast}; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ -use crate::AppState; +use crate::{AppState, assembly::Constraint}; -// this component lists the elements of the assembly, showing the constraints -// on each element as a collapsible sub-list. its implementation is based on -// Kate Morley's HTML + CSS tree views: +// an editable view of the Lorentz product representing a constraint +#[component(inline_props)] +fn LorentzProductInput(constraint: Constraint) -> View { + view! { + input( + r#type="text", + bind:value=constraint.rep_text, + on:change=move |event: Event| { + let target: HtmlInputElement = event.target().unwrap().unchecked_into(); + match target.value().parse::() { + Ok(rep) => batch(|| { + constraint.rep.set(rep); + constraint.rep_valid.set(true); + console::log_2( + &JsValue::from("Constraint rep parsed to"), + &JsValue::from(rep) + ); + }), + Err(_) => constraint.rep_valid.set(false) + }; + } + ) + } +} + +// a component that lists the elements of the current assembly, showing the +// constraints on each element as a collapsible sub-list. its implementation +// is based on Kate Morley's HTML + CSS tree views: // // https://iamkate.com/code/tree-views/ // @@ -137,24 +162,7 @@ pub fn Outline() -> View { li(class="cst") { input(r#type="checkbox", bind:checked=cst.active) div(class="cst-label") { (other_arg_label) } - input( - r#type="text", - bind:value=cst.rep_text, - on:change=move |event: Event| { - let target: HtmlInputElement = event.target().unwrap().unchecked_into(); - match target.value().parse::() { - Ok(rep) => batch(|| { - cst.rep.set(rep); - cst.rep_valid.set(true); - console::log_2( - &JsValue::from("Constraint rep parsed to"), - &JsValue::from(rep) - ); - }), - Err(_) => cst.rep_valid.set(false) - }; - } - ) + LorentzProductInput(constraint=cst) } } }, -- 2.34.1 From c2e3c64d4a3750c03fceb789bc8111cd71a67d5c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 30 Oct 2024 00:16:34 -0700 Subject: [PATCH 11/29] Remove debug log from Lorentz product input --- app-proto/src/outline.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index fcf983f..6497fdd 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -1,7 +1,6 @@ use itertools::Itertools; use sycamore::{prelude::*, web::tags::div}; use web_sys::{Element, Event, HtmlInputElement, KeyboardEvent, MouseEvent, wasm_bindgen::JsCast}; -use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ use crate::{AppState, assembly::Constraint}; @@ -18,10 +17,6 @@ fn LorentzProductInput(constraint: Constraint) -> View { Ok(rep) => batch(|| { constraint.rep.set(rep); constraint.rep_valid.set(true); - console::log_2( - &JsValue::from("Constraint rep parsed to"), - &JsValue::from(rep) - ); }), Err(_) => constraint.rep_valid.set(false) }; -- 2.34.1 From 9e31037e17d1b3f21ac95b8fa7407487a257e18f Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 30 Oct 2024 00:19:44 -0700 Subject: [PATCH 12/29] Spread web-sys imports over multiple lines --- app-proto/src/outline.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 6497fdd..8edbe07 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -1,6 +1,13 @@ use itertools::Itertools; use sycamore::{prelude::*, web::tags::div}; -use web_sys::{Element, Event, HtmlInputElement, KeyboardEvent, MouseEvent, wasm_bindgen::JsCast}; +use web_sys::{ + Element, + Event, + HtmlInputElement, + KeyboardEvent, + MouseEvent, + wasm_bindgen::JsCast +}; use crate::{AppState, assembly::Constraint}; -- 2.34.1 From 9c191ae586369e1217b9c809b52ad7f7253c8cf4 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 30 Oct 2024 00:27:16 -0700 Subject: [PATCH 13/29] Polish log messages --- app-proto/src/add_remove.rs | 4 ++-- app-proto/src/assembly.rs | 2 +- app-proto/src/engine.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index 5435418..92ae4be 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -228,7 +228,7 @@ pub fn AddRemove() -> View { /* DEBUG */ // print updated constraint list - console::log_1(&JsValue::from("constraints:")); + console::log_1(&JsValue::from("Constraints:")); state.assembly.constraints.with(|csts| { for (_, cst) in csts.into_iter() { console::log_5( @@ -246,7 +246,7 @@ pub fn AddRemove() -> View { create_effect(move || { rep.track(); console::log_2( - &JsValue::from("Constraint rep updated to"), + &JsValue::from("Lorentz product updated to"), &JsValue::from(rep.get_untracked()) ); if active.get() { diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 62f5405..0970932 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -146,7 +146,7 @@ impl Assembly { /* DEBUG */ // 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() { let mut row_str = String::new(); for k in 0..guess.ncols() { diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 2971750..2978a9a 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -60,7 +60,7 @@ impl PartialMatrix { pub fn log_to_console(&self) { let PartialMatrix(entries) = self; for ent in entries { - let ent_str = format!("{} {} {}", ent.index.0, ent.index.1, ent.value); + let ent_str = format!(" {} {} {}", ent.index.0, ent.index.1, ent.value); console::log_1(&JsValue::from(ent_str.as_str())); } } -- 2.34.1 From 7f595ff27a1a52410fe638afb2831d4db4c05ba6 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 30 Oct 2024 15:49:01 -0700 Subject: [PATCH 14/29] Factor out constraint outline item --- app-proto/src/outline.rs | 45 ++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 8edbe07..944e54a 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -32,6 +32,27 @@ fn LorentzProductInput(constraint: Constraint) -> View { } } +// a list item that shows a constraint in an outline view of an element +#[component(inline_props)] +fn ConstraintOutlineItem(constraint_key: usize, element_key: usize) -> View { + let state = use_context::(); + let assembly = &state.assembly; + let constraint = assembly.constraints.with(|csts| csts[constraint_key].clone()); + let other_arg = if constraint.args.0 == element_key { + constraint.args.1 + } else { + constraint.args.0 + }; + let other_arg_label = assembly.elements.with(|elts| elts[other_arg].label.clone()); + view! { + li(class="cst") { + input(r#type="checkbox", bind:checked=constraint.active) + div(class="cst-label") { (other_arg_label) } + LorentzProductInput(constraint=constraint) + } + } +} + // a component that lists the elements of the current assembly, showing the // constraints on each element as a collapsible sub-list. its implementation // is based on Kate Morley's HTML + CSS tree views: @@ -150,25 +171,13 @@ pub fn Outline() -> View { ul(class="constraints") { Keyed( list=elt.constraints.into_iter().collect::>(), - view=move |c_key: usize| { - let c_state = use_context::(); - let assembly = &c_state.assembly; - let cst = assembly.constraints.with(|csts| csts[c_key].clone()); - let other_arg = if cst.args.0 == key { - cst.args.1 - } else { - cst.args.0 - }; - let other_arg_label = assembly.elements.with(|elts| elts[other_arg].label.clone()); - view! { - li(class="cst") { - input(r#type="checkbox", bind:checked=cst.active) - div(class="cst-label") { (other_arg_label) } - LorentzProductInput(constraint=cst) - } - } + view=move |cst_key| view! { + ConstraintOutlineItem( + constraint_key=cst_key, + element_key=key + ) }, - key=|c_key| c_key.clone() + key=|cst_key| cst_key.clone() ) } } -- 2.34.1 From df6db983ba950b18727305368c928b335851d71c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 30 Oct 2024 16:01:19 -0700 Subject: [PATCH 15/29] Factor out element outline item --- app-proto/src/outline.rs | 213 ++++++++++++++++++++------------------- 1 file changed, 109 insertions(+), 104 deletions(-) diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 944e54a..d716301 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -1,7 +1,6 @@ use itertools::Itertools; use sycamore::{prelude::*, web::tags::div}; use web_sys::{ - Element, Event, HtmlInputElement, KeyboardEvent, @@ -9,7 +8,7 @@ use web_sys::{ wasm_bindgen::JsCast }; -use crate::{AppState, assembly::Constraint}; +use crate::{AppState, assembly, assembly::Constraint}; // an editable view of the Lorentz product representing a constraint #[component(inline_props)] @@ -53,6 +52,112 @@ fn ConstraintOutlineItem(constraint_key: usize, element_key: usize) -> View { } } +// a list item that shows an element in an outline view of an assembly +#[component(inline_props)] +fn ElementOutlineItem(key: usize, element: assembly::Element) -> View { + let state = use_context::(); + let class = create_memo({ + move || { + if state.selection.with(|sel| sel.contains(&key)) { + "selected" + } else { + "" + } + } + }); + let label = element.label.clone(); + let rep_components = element.rep.iter().map(|u| { + let u_coord = u.to_string().replace("-", "\u{2212}"); + View::from(div().children(u_coord)) + }).collect::>(); + let constrained = element.constraints.len() > 0; + let details_node = create_node_ref(); + view! { + li { + details(ref=details_node) { + summary( + class=class.get(), + on:keydown={ + move |event: KeyboardEvent| { + match event.key().as_str() { + "Enter" => { + if event.shift_key() { + state.selection.update(|sel| { + if !sel.remove(&key) { + sel.insert(key); + } + }); + } else { + state.selection.update(|sel| { + sel.clear(); + sel.insert(key); + }); + } + event.prevent_default(); + }, + "ArrowRight" if constrained => { + let _ = details_node + .get() + .unchecked_into::() + .set_attribute("open", ""); + }, + "ArrowLeft" => { + let _ = details_node + .get() + .unchecked_into::() + .remove_attribute("open"); + }, + _ => () + } + } + } + ) { + div( + class="elt-switch", + on:click=|event: MouseEvent| event.stop_propagation() + ) + div( + class="elt", + on:click={ + move |event: MouseEvent| { + if event.shift_key() { + state.selection.update(|sel| { + if !sel.remove(&key) { + sel.insert(key); + } + }); + } else { + state.selection.update(|sel| { + sel.clear(); + sel.insert(key); + }); + } + event.stop_propagation(); + event.prevent_default(); + } + } + ) { + div(class="elt-label") { (label) } + div(class="elt-rep") { (rep_components) } + } + } + ul(class="constraints") { + Keyed( + list=element.constraints.into_iter().collect::>(), + view=move |cst_key| view! { + ConstraintOutlineItem( + constraint_key=cst_key, + element_key=key + ) + }, + key=|cst_key| cst_key.clone() + ) + } + } + } + } +} + // a component that lists the elements of the current assembly, showing the // constraints on each element as a collapsible sub-list. its implementation // is based on Kate Morley's HTML + CSS tree views: @@ -81,108 +186,8 @@ pub fn Outline() -> View { ) { Keyed( list=elements_sorted, - view=|(key, elt)| { - let state = use_context::(); - let class = create_memo({ - move || { - if state.selection.with(|sel| sel.contains(&key)) { - "selected" - } else { - "" - } - } - }); - let label = elt.label.clone(); - let rep_components = elt.rep.iter().map(|u| { - let u_coord = u.to_string().replace("-", "\u{2212}"); - View::from(div().children(u_coord)) - }).collect::>(); - let constrained = elt.constraints.len() > 0; - let details_node = create_node_ref(); - view! { - li { - details(ref=details_node) { - summary( - class=class.get(), - on:keydown={ - move |event: KeyboardEvent| { - match event.key().as_str() { - "Enter" => { - if event.shift_key() { - state.selection.update(|sel| { - if !sel.remove(&key) { - sel.insert(key); - } - }); - } else { - state.selection.update(|sel| { - sel.clear(); - sel.insert(key); - }); - } - event.prevent_default(); - }, - "ArrowRight" if constrained => { - let _ = details_node - .get() - .unchecked_into::() - .set_attribute("open", ""); - }, - "ArrowLeft" => { - let _ = details_node - .get() - .unchecked_into::() - .remove_attribute("open"); - }, - _ => () - } - } - } - ) { - div( - class="elt-switch", - on:click=|event: MouseEvent| event.stop_propagation() - ) - div( - class="elt", - on:click={ - move |event: MouseEvent| { - if event.shift_key() { - state.selection.update(|sel| { - if !sel.remove(&key) { - sel.insert(key); - } - }); - } else { - state.selection.update(|sel| { - sel.clear(); - sel.insert(key); - }); - } - event.stop_propagation(); - event.prevent_default(); - } - } - ) { - div(class="elt-label") { (label) } - div(class="elt-rep") { (rep_components) } - } - } - ul(class="constraints") { - Keyed( - list=elt.constraints.into_iter().collect::>(), - view=move |cst_key| view! { - ConstraintOutlineItem( - constraint_key=cst_key, - element_key=key - ) - }, - key=|cst_key| cst_key.clone() - ) - } - } - } - } + view=|(key, elt)| view! { + ElementOutlineItem(key=key, element=elt) }, key=|(key, elt)| ( key.clone(), -- 2.34.1 From 9555d8f78440e120ee5ec75f4a77bfeb9c317d78 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 30 Oct 2024 16:06:38 -0700 Subject: [PATCH 16/29] Update title and authors --- app-proto/Cargo.toml | 4 ++-- app-proto/index.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index e5bc05e..e623b26 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "sketch-outline" +name = "dyna3" version = "0.1.0" -authors = ["Aaron"] +authors = ["Aaron Fenyes", "Glen Whitney"] edition = "2021" [features] diff --git a/app-proto/index.html b/app-proto/index.html index 5474fe9..cee4b1b 100644 --- a/app-proto/index.html +++ b/app-proto/index.html @@ -2,7 +2,7 @@ - Sketch outline + dyna3 -- 2.34.1 From 0a13c062f42c3fb1ec2156ab2a3688ea3932f2bc Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 30 Oct 2024 21:12:40 -0700 Subject: [PATCH 17/29] Flag constraints with invalid input --- app-proto/main.css | 22 +++++++++++++++++++++- app-proto/src/outline.rs | 23 +++++++++++++++-------- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/app-proto/main.css b/app-proto/main.css index 32ae5bf..937714e 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -104,17 +104,37 @@ details[open]:has(li) .elt-switch::after { font-style: italic; } +.cst.invalid { + color: #f58fc2; +} + .cst > input[type=checkbox] { margin: 0px 8px 0px 0px; } .cst > input[type=text] { - color: #fcfcfc; + color: inherit; background-color: inherit; border: 1px solid #555; border-radius: 2px; } +.cst.invalid > input[type=text] { + border-color: #70495c; +} + +.status { + width: 20px; + padding-left: 4px; + text-align: center; + font-style: normal; +} + +.invalid > .status::after, details:has(.invalid):not([open]) .status::after { + content: '⚠'; + color: #f58fc2; +} + /* display */ canvas { diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index d716301..976b9a3 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -43,11 +43,19 @@ fn ConstraintOutlineItem(constraint_key: usize, element_key: usize) -> View { constraint.args.0 }; let other_arg_label = assembly.elements.with(|elts| elts[other_arg].label.clone()); + let class = create_memo(move || { + if constraint.rep_valid.get() { + "cst" + } else { + "cst invalid" + } + }); view! { - li(class="cst") { + li(class=class.get()) { input(r#type="checkbox", bind:checked=constraint.active) div(class="cst-label") { (other_arg_label) } LorentzProductInput(constraint=constraint) + div(class="status") } } } @@ -56,13 +64,11 @@ fn ConstraintOutlineItem(constraint_key: usize, element_key: usize) -> View { #[component(inline_props)] fn ElementOutlineItem(key: usize, element: assembly::Element) -> View { let state = use_context::(); - let class = create_memo({ - move || { - if state.selection.with(|sel| sel.contains(&key)) { - "selected" - } else { - "" - } + let class = create_memo(move || { + if state.selection.with(|sel| sel.contains(&key)) { + "selected" + } else { + "" } }); let label = element.label.clone(); @@ -139,6 +145,7 @@ fn ElementOutlineItem(key: usize, element: assembly::Element) -> View { ) { div(class="elt-label") { (label) } div(class="elt-rep") { (rep_components) } + div(class="status") } } ul(class="constraints") { -- 2.34.1 From 35d3e4a6f814bc26dde34b81d078b8914666ffb3 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 30 Oct 2024 23:29:48 -0700 Subject: [PATCH 18/29] Specify fonts This should help the interface look more consistent across platforms. The font choices are just placeholders: consistency is the main goal. --- app-proto/index.html | 2 ++ app-proto/main.css | 7 +++++++ app-proto/src/add_remove.rs | 1 + 3 files changed, 10 insertions(+) diff --git a/app-proto/index.html b/app-proto/index.html index cee4b1b..941a153 100644 --- a/app-proto/index.html +++ b/app-proto/index.html @@ -4,6 +4,8 @@ dyna3 + + diff --git a/app-proto/main.css b/app-proto/main.css index 937714e..030f56a 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -2,6 +2,7 @@ body { margin: 0px; color: #fcfcfc; background-color: #222; + font-family: 'Lato'; } /* sidebar */ @@ -33,6 +34,11 @@ body { font-size: large; } +/* KLUDGE */ +#add-remove > button.emoji { + font-family: 'Noto Emoji'; +} + /* outline */ #outline { @@ -127,6 +133,7 @@ details[open]:has(li) .elt-switch::after { width: 20px; padding-left: 4px; text-align: center; + font-family: 'Noto Emoji'; font-style: normal; } diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index 92ae4be..ead66dd 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -202,6 +202,7 @@ pub fn AddRemove() -> View { } ) { "+" } button( + class="emoji", /* KLUDGE */ disabled={ let state = use_context::(); state.selection.with(|sel| sel.len() != 2) -- 2.34.1 From 1f3a6eea3ba7cf4ba3e785425b853f8319c81527 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 30 Oct 2024 23:57:15 -0700 Subject: [PATCH 19/29] Round element vectors to three decimal places --- app-proto/main.css | 2 +- app-proto/src/outline.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app-proto/main.css b/app-proto/main.css index 030f56a..ba8a20b 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -102,7 +102,7 @@ details[open]:has(li) .elt-switch::after { .elt-rep > div { padding: 2px 0px 0px 0px; font-size: 10pt; - text-align: center; + text-align: right; width: 56px; } diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 976b9a3..4a2b36a 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -73,7 +73,7 @@ fn ElementOutlineItem(key: usize, element: assembly::Element) -> View { }); let label = element.label.clone(); let rep_components = element.rep.iter().map(|u| { - let u_coord = u.to_string().replace("-", "\u{2212}"); + let u_coord = format!("{:.3}", u).replace("-", "\u{2212}"); View::from(div().children(u_coord)) }).collect::>(); let constrained = element.constraints.len() > 0; -- 2.34.1 From 5b522c12eeacf13cc1fc5cc9af7069438801519d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 31 Oct 2024 01:23:22 -0700 Subject: [PATCH 20/29] Include vector representation in element diff key --- app-proto/src/outline.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 4a2b36a..66b50f4 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -200,6 +200,7 @@ pub fn Outline() -> View { key.clone(), elt.id.clone(), elt.label.clone(), + elt.rep.into_iter().map(|u| u.to_bits()).collect::>(), elt.constraints.clone() ) ) -- 2.34.1 From a3fce9d29829aa4ee1ec8ae7554fc647fd243ef5 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 31 Oct 2024 01:24:06 -0700 Subject: [PATCH 21/29] Correct typo in comment --- app-proto/src/add_remove.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index ead66dd..15d30f2 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -242,8 +242,8 @@ pub fn AddRemove() -> View { } }); - // update the realization when the constraint activated, or - // edited while active + // update the realization when the constraint is activated, + // or edited while active create_effect(move || { rep.track(); console::log_2( -- 2.34.1 From fb292d8b5b6848cc47cb01a4e49c0efedfc8f8fa Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 1 Nov 2024 04:25:03 -0700 Subject: [PATCH 22/29] Render constraint lists dynamically --- app-proto/src/add_remove.rs | 28 ++++++++++++++-------------- app-proto/src/assembly.rs | 13 +++++++------ app-proto/src/outline.rs | 12 +++++++----- 3 files changed, 28 insertions(+), 25 deletions(-) diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index 15d30f2..e9a3515 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -12,7 +12,7 @@ fn load_gen_assemb(assembly: &Assembly) { label: String::from("Castor"), color: [1.00_f32, 0.25_f32, 0.00_f32], rep: engine::sphere(0.5, 0.5, 0.0, 1.0), - constraints: BTreeSet::default(), + constraints: create_signal(BTreeSet::default()), index: 0 } ); @@ -22,7 +22,7 @@ fn load_gen_assemb(assembly: &Assembly) { label: String::from("Pollux"), color: [0.00_f32, 0.25_f32, 1.00_f32], rep: engine::sphere(-0.5, -0.5, 0.0, 1.0), - constraints: BTreeSet::default(), + constraints: create_signal(BTreeSet::default()), index: 0 } ); @@ -32,7 +32,7 @@ fn load_gen_assemb(assembly: &Assembly) { label: String::from("Ursa major"), color: [0.25_f32, 0.00_f32, 1.00_f32], rep: engine::sphere(-0.5, 0.5, 0.0, 0.75), - constraints: BTreeSet::default(), + constraints: create_signal(BTreeSet::default()), index: 0 } ); @@ -42,7 +42,7 @@ fn load_gen_assemb(assembly: &Assembly) { label: String::from("Ursa minor"), color: [0.25_f32, 1.00_f32, 0.00_f32], rep: engine::sphere(0.5, -0.5, 0.0, 0.5), - constraints: BTreeSet::default(), + constraints: create_signal(BTreeSet::default()), index: 0 } ); @@ -52,7 +52,7 @@ fn load_gen_assemb(assembly: &Assembly) { label: String::from("Deimos"), color: [0.75_f32, 0.75_f32, 0.00_f32], rep: engine::sphere(0.0, 0.15, 1.0, 0.25), - constraints: BTreeSet::default(), + constraints: create_signal(BTreeSet::default()), index: 0 } ); @@ -62,7 +62,7 @@ fn load_gen_assemb(assembly: &Assembly) { label: String::from("Phobos"), color: [0.00_f32, 0.75_f32, 0.50_f32], rep: engine::sphere(0.0, -0.15, -1.0, 0.25), - constraints: BTreeSet::default(), + constraints: create_signal(BTreeSet::default()), index: 0 } ); @@ -77,7 +77,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { label: "Central".to_string(), color: [0.75_f32, 0.75_f32, 0.75_f32], rep: engine::sphere(0.0, 0.0, 0.0, 1.0), - constraints: BTreeSet::default(), + constraints: create_signal(BTreeSet::default()), index: 0 } ); @@ -87,7 +87,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { label: "Assembly plane".to_string(), color: [0.75_f32, 0.75_f32, 0.75_f32], rep: engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0), - constraints: BTreeSet::default(), + constraints: create_signal(BTreeSet::default()), index: 0 } ); @@ -97,7 +97,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { label: "Side 1".to_string(), color: [1.00_f32, 0.00_f32, 0.25_f32], rep: engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0), - constraints: BTreeSet::default(), + constraints: create_signal(BTreeSet::default()), index: 0 } ); @@ -107,7 +107,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { label: "Side 2".to_string(), color: [0.25_f32, 1.00_f32, 0.00_f32], rep: engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0), - constraints: BTreeSet::default(), + constraints: create_signal(BTreeSet::default()), index: 0 } ); @@ -117,7 +117,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { label: "Side 3".to_string(), color: [0.00_f32, 0.25_f32, 1.00_f32], rep: engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0), - constraints: BTreeSet::default(), + constraints: create_signal(BTreeSet::default()), index: 0 } ); @@ -127,7 +127,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { label: "Corner 1".to_string(), color: [0.75_f32, 0.75_f32, 0.75_f32], rep: engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0), - constraints: BTreeSet::default(), + constraints: create_signal(BTreeSet::default()), index: 0 } ); @@ -137,7 +137,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { label: "Corner 2".to_string(), color: [0.75_f32, 0.75_f32, 0.75_f32], rep: engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0), - constraints: BTreeSet::default(), + constraints: create_signal(BTreeSet::default()), index: 0 } ); @@ -147,7 +147,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { label: String::from("Corner 3"), color: [0.75_f32, 0.75_f32, 0.75_f32], rep: engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0), - constraints: BTreeSet::default(), + constraints: create_signal(BTreeSet::default()), index: 0 } ); diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 0970932..64f1b04 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -13,7 +13,7 @@ pub struct Element { pub label: String, pub color: [f32; 3], pub rep: DVector, - pub constraints: BTreeSet, + pub constraints: Signal>, // internal properties, not reflected in any view pub index: usize @@ -87,7 +87,7 @@ impl Assembly { label: format!("Sphere {}", id_num), color: [0.75_f32, 0.75_f32, 0.75_f32], rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5]), - constraints: BTreeSet::default(), + constraints: create_signal(BTreeSet::default()), index: 0 } ); @@ -96,10 +96,11 @@ impl Assembly { pub fn insert_constraint(&self, constraint: Constraint) { let args = constraint.args; let key = self.constraints.update(|csts| csts.insert(constraint)); - self.elements.update(|elts| { - elts[args.0].constraints.insert(key); - elts[args.1].constraints.insert(key); - }); + let arg_constraints = self.elements.with( + |elts| (elts[args.0].constraints, elts[args.1].constraints) + ); + arg_constraints.0.update(|csts| csts.insert(key)); + arg_constraints.1.update(|csts| csts.insert(key)); } // --- realization --- diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 66b50f4..e29781b 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -76,7 +76,10 @@ fn ElementOutlineItem(key: usize, element: assembly::Element) -> View { let u_coord = format!("{:.3}", u).replace("-", "\u{2212}"); View::from(div().children(u_coord)) }).collect::>(); - let constrained = element.constraints.len() > 0; + let constrained = element.constraints.map(|csts| csts.len() > 0); + let constraint_list = element.constraints.map( + |csts| csts.clone().into_iter().collect() + ); let details_node = create_node_ref(); view! { li { @@ -101,7 +104,7 @@ fn ElementOutlineItem(key: usize, element: assembly::Element) -> View { } event.prevent_default(); }, - "ArrowRight" if constrained => { + "ArrowRight" if constrained.get() => { let _ = details_node .get() .unchecked_into::() @@ -150,7 +153,7 @@ fn ElementOutlineItem(key: usize, element: assembly::Element) -> View { } ul(class="constraints") { Keyed( - list=element.constraints.into_iter().collect::>(), + list=constraint_list, view=move |cst_key| view! { ConstraintOutlineItem( constraint_key=cst_key, @@ -200,8 +203,7 @@ pub fn Outline() -> View { key.clone(), elt.id.clone(), elt.label.clone(), - elt.rep.into_iter().map(|u| u.to_bits()).collect::>(), - elt.constraints.clone() + elt.rep.into_iter().map(|u| u.to_bits()).collect::>() ) ) } -- 2.34.1 From bbeebe4464d0d9eac34732e08e6847f0adc2a56a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 1 Nov 2024 04:43:30 -0700 Subject: [PATCH 23/29] Simplify memos --- app-proto/src/outline.rs | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index e29781b..99a8dd6 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -43,13 +43,9 @@ fn ConstraintOutlineItem(constraint_key: usize, element_key: usize) -> View { constraint.args.0 }; let other_arg_label = assembly.elements.with(|elts| elts[other_arg].label.clone()); - let class = create_memo(move || { - if constraint.rep_valid.get() { - "cst" - } else { - "cst invalid" - } - }); + let class = constraint.rep_valid.map( + |&rep_valid| if rep_valid { "cst" } else { "cst invalid" } + ); view! { li(class=class.get()) { input(r#type="checkbox", bind:checked=constraint.active) @@ -64,13 +60,9 @@ fn ConstraintOutlineItem(constraint_key: usize, element_key: usize) -> View { #[component(inline_props)] fn ElementOutlineItem(key: usize, element: assembly::Element) -> View { let state = use_context::(); - let class = create_memo(move || { - if state.selection.with(|sel| sel.contains(&key)) { - "selected" - } else { - "" - } - }); + let class = state.selection.map( + move |sel| if sel.contains(&key) { "selected" } else { "" } + ); let label = element.label.clone(); let rep_components = element.rep.iter().map(|u| { let u_coord = format!("{:.3}", u).replace("-", "\u{2212}"); @@ -176,15 +168,16 @@ fn ElementOutlineItem(key: usize, element: assembly::Element) -> View { // #[component] pub fn Outline() -> View { - // sort the elements alphabetically by ID - let elements_sorted = create_memo(|| { - let state = use_context::(); - state.assembly.elements - .get_clone() + let state = use_context::(); + + // list the elements alphabetically by ID + let element_list = state.assembly.elements.map( + |elts| elts + .clone() .into_iter() .sorted_by_key(|(_, elt)| elt.id.clone()) .collect() - }); + ); view! { ul( @@ -195,7 +188,7 @@ pub fn Outline() -> View { } ) { Keyed( - list=elements_sorted, + list=element_list, view=|(key, elt)| view! { ElementOutlineItem(key=key, element=elt) }, -- 2.34.1 From e42b8da89720525c040df9084939f44bc8c85f05 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 1 Nov 2024 05:07:34 -0700 Subject: [PATCH 24/29] Add an element constructor --- app-proto/src/add_remove.rs | 197 +++++++++++++++--------------------- app-proto/src/assembly.rs | 33 ++++-- 2 files changed, 109 insertions(+), 121 deletions(-) diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index e9a3515..9db281e 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeSet; /* DEBUG */ use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; @@ -7,64 +6,52 @@ use crate::{engine, AppState, assembly::{Assembly, Constraint, Element}}; /* DEBUG */ fn load_gen_assemb(assembly: &Assembly) { let _ = assembly.try_insert_element( - Element { - id: String::from("gemini_a"), - label: String::from("Castor"), - color: [1.00_f32, 0.25_f32, 0.00_f32], - rep: engine::sphere(0.5, 0.5, 0.0, 1.0), - constraints: create_signal(BTreeSet::default()), - index: 0 - } + Element::new( + String::from("gemini_a"), + String::from("Castor"), + [1.00_f32, 0.25_f32, 0.00_f32], + engine::sphere(0.5, 0.5, 0.0, 1.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: String::from("gemini_b"), - label: String::from("Pollux"), - color: [0.00_f32, 0.25_f32, 1.00_f32], - rep: engine::sphere(-0.5, -0.5, 0.0, 1.0), - constraints: create_signal(BTreeSet::default()), - index: 0 - } + Element::new( + String::from("gemini_b"), + String::from("Pollux"), + [0.00_f32, 0.25_f32, 1.00_f32], + engine::sphere(-0.5, -0.5, 0.0, 1.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: String::from("ursa_major"), - label: String::from("Ursa major"), - color: [0.25_f32, 0.00_f32, 1.00_f32], - rep: engine::sphere(-0.5, 0.5, 0.0, 0.75), - constraints: create_signal(BTreeSet::default()), - index: 0 - } + Element::new( + String::from("ursa_major"), + String::from("Ursa major"), + [0.25_f32, 0.00_f32, 1.00_f32], + engine::sphere(-0.5, 0.5, 0.0, 0.75) + ) ); let _ = assembly.try_insert_element( - Element { - id: String::from("ursa_minor"), - label: String::from("Ursa minor"), - color: [0.25_f32, 1.00_f32, 0.00_f32], - rep: engine::sphere(0.5, -0.5, 0.0, 0.5), - constraints: create_signal(BTreeSet::default()), - index: 0 - } + Element::new( + String::from("ursa_minor"), + String::from("Ursa minor"), + [0.25_f32, 1.00_f32, 0.00_f32], + engine::sphere(0.5, -0.5, 0.0, 0.5) + ) ); let _ = assembly.try_insert_element( - Element { - id: String::from("moon_deimos"), - label: String::from("Deimos"), - color: [0.75_f32, 0.75_f32, 0.00_f32], - rep: engine::sphere(0.0, 0.15, 1.0, 0.25), - constraints: create_signal(BTreeSet::default()), - index: 0 - } + Element::new( + String::from("moon_deimos"), + String::from("Deimos"), + [0.75_f32, 0.75_f32, 0.00_f32], + engine::sphere(0.0, 0.15, 1.0, 0.25) + ) ); let _ = assembly.try_insert_element( - Element { - id: String::from("moon_phobos"), - label: String::from("Phobos"), - color: [0.00_f32, 0.75_f32, 0.50_f32], - rep: engine::sphere(0.0, -0.15, -1.0, 0.25), - constraints: create_signal(BTreeSet::default()), - index: 0 - } + Element::new( + String::from("moon_phobos"), + String::from("Phobos"), + [0.00_f32, 0.75_f32, 0.50_f32], + engine::sphere(0.0, -0.15, -1.0, 0.25) + ) ); } @@ -72,84 +59,68 @@ fn load_gen_assemb(assembly: &Assembly) { fn load_low_curv_assemb(assembly: &Assembly) { let a = 0.75_f64.sqrt(); let _ = assembly.try_insert_element( - Element { - id: "central".to_string(), - label: "Central".to_string(), - color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: engine::sphere(0.0, 0.0, 0.0, 1.0), - constraints: create_signal(BTreeSet::default()), - index: 0 - } + Element::new( + "central".to_string(), + "Central".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(0.0, 0.0, 0.0, 1.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: "assemb_plane".to_string(), - label: "Assembly plane".to_string(), - color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0), - constraints: create_signal(BTreeSet::default()), - index: 0 - } + Element::new( + "assemb_plane".to_string(), + "Assembly plane".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: "side1".to_string(), - label: "Side 1".to_string(), - color: [1.00_f32, 0.00_f32, 0.25_f32], - rep: engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0), - constraints: create_signal(BTreeSet::default()), - index: 0 - } + Element::new( + "side1".to_string(), + "Side 1".to_string(), + [1.00_f32, 0.00_f32, 0.25_f32], + engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: "side2".to_string(), - label: "Side 2".to_string(), - color: [0.25_f32, 1.00_f32, 0.00_f32], - rep: engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0), - constraints: create_signal(BTreeSet::default()), - index: 0 - } + Element::new( + "side2".to_string(), + "Side 2".to_string(), + [0.25_f32, 1.00_f32, 0.00_f32], + engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: "side3".to_string(), - label: "Side 3".to_string(), - color: [0.00_f32, 0.25_f32, 1.00_f32], - rep: engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0), - constraints: create_signal(BTreeSet::default()), - index: 0 - } + Element::new( + "side3".to_string(), + "Side 3".to_string(), + [0.00_f32, 0.25_f32, 1.00_f32], + engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: "corner1".to_string(), - label: "Corner 1".to_string(), - color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0), - constraints: create_signal(BTreeSet::default()), - index: 0 - } + Element::new( + "corner1".to_string(), + "Corner 1".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: "corner2".to_string(), - label: "Corner 2".to_string(), - color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0), - constraints: create_signal(BTreeSet::default()), - index: 0 - } + Element::new( + "corner2".to_string(), + "Corner 2".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: String::from("corner3"), - label: String::from("Corner 3"), - color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0), - constraints: create_signal(BTreeSet::default()), - index: 0 - } + Element::new( + String::from("corner3"), + String::from("Corner 3"), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0) + ) ); } diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 64f1b04..d681dc2 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -19,6 +19,25 @@ pub struct Element { pub index: usize } +impl Element { + pub fn new( + id: String, + label: String, + color: [f32; 3], + rep: DVector + ) -> Element { + Element { + id: id, + label: label, + color: color, + rep: rep, + constraints: create_signal(BTreeSet::default()), + index: 0 + } + } +} + + #[derive(Clone)] pub struct Constraint { pub args: (usize, usize), @@ -82,14 +101,12 @@ impl Assembly { // create and insert a new element self.insert_element_unchecked( - Element { - id: id, - label: format!("Sphere {}", id_num), - color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5]), - constraints: create_signal(BTreeSet::default()), - index: 0 - } + Element::new( + id, + format!("Sphere {}", id_num), + [0.75_f32, 0.75_f32, 0.75_f32], + DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5]) + ) ); } -- 2.34.1 From 5ce5f855d54d681422aae1acb6c7d2333d24b2f6 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 1 Nov 2024 19:01:14 -0700 Subject: [PATCH 25/29] Make element vectors reactive --- app-proto/src/assembly.rs | 16 +++++------ app-proto/src/display.rs | 59 ++++++++++++++++++++++++++------------- app-proto/src/outline.rs | 27 ++++++++++-------- 3 files changed, 63 insertions(+), 39 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index d681dc2..6119939 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -12,7 +12,7 @@ pub struct Element { pub id: String, pub label: String, pub color: [f32; 3], - pub rep: DVector, + pub rep: Signal>, pub constraints: Signal>, // internal properties, not reflected in any view @@ -30,7 +30,7 @@ impl Element { id: id, label: label, color: color, - rep: rep, + rep: create_signal(rep), constraints: create_signal(BTreeSet::default()), index: 0 } @@ -151,7 +151,7 @@ impl Assembly { for (_, elt) in elts { let index = elt.index; gram_to_be.push_sym(index, index, 1.0); - guess_to_be.set_column(index, &elt.rep); + guess_to_be.set_column(index, &elt.rep.get_clone()); } (gram_to_be, guess_to_be) @@ -193,11 +193,11 @@ impl Assembly { if success { // read out the solution - self.elements.update(|elts| { - for (_, elt) in elts.iter_mut() { - elt.rep.set_column(0, &config.column(elt.index)); - } - }); + for (_, elt) in self.elements.get_clone() { + elt.rep.update( + |rep| rep.set_column(0, &config.column(elt.index)) + ); + } } } } \ No newline at end of file diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index c32b470..ce1655d 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -103,7 +103,11 @@ pub fn Display() -> View { // change listener let scene_changed = create_signal(true); create_effect(move || { - state.assembly.elements.track(); + state.assembly.elements.with(|elts| { + for (_, elt) in elts { + elt.rep.track(); + } + }); state.selection.track(); scene_changed.set(true); }); @@ -295,23 +299,40 @@ pub fn Display() -> View { let assembly_to_world = &location * &orientation; // get the assembly - let elements = state.assembly.elements.get_clone(); - let element_iter = (&elements).into_iter(); - let reps_world: Vec<_> = element_iter.clone().map(|(_, elt)| &assembly_to_world * &elt.rep).collect(); - let colors: Vec<_> = element_iter.clone().map(|(key, elt)| - if state.selection.with(|sel| sel.contains(&key)) { - elt.color.map(|ch| 0.2 + 0.8*ch) - } else { - elt.color - } - ).collect(); - let highlights: Vec<_> = element_iter.map(|(key, _)| - if state.selection.with(|sel| sel.contains(&key)) { - 1.0_f32 - } else { - HIGHLIGHT - } - ).collect(); + let ( + elt_cnt, + reps_world, + colors, + highlights + ) = state.assembly.elements.with(|elts| { + ( + // number of elements + elts.len() as i32, + + // representation vectors in world coordinates + elts.iter().map( + |(_, elt)| elt.rep.with(|rep| &assembly_to_world * rep) + ).collect::>(), + + // colors + elts.iter().map(|(key, elt)| { + if state.selection.with(|sel| sel.contains(&key)) { + elt.color.map(|ch| 0.2 + 0.8*ch) + } else { + elt.color + } + }).collect::>(), + + // highlight levels + elts.iter().map(|(key, _)| { + if state.selection.with(|sel| sel.contains(&key)) { + 1.0_f32 + } else { + HIGHLIGHT + } + }).collect::>() + ) + }); // set the resolution let width = canvas.width() as f32; @@ -320,7 +341,7 @@ pub fn Display() -> View { ctx.uniform1f(shortdim_loc.as_ref(), width.min(height)); // pass the assembly - ctx.uniform1i(sphere_cnt_loc.as_ref(), elements.len() as i32); + ctx.uniform1i(sphere_cnt_loc.as_ref(), elt_cnt); for n in 0..reps_world.len() { let v = &reps_world[n]; ctx.uniform3f( diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 99a8dd6..11cc061 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -1,5 +1,5 @@ use itertools::Itertools; -use sycamore::{prelude::*, web::tags::div}; +use sycamore::prelude::*; use web_sys::{ Event, HtmlInputElement, @@ -64,10 +64,11 @@ fn ElementOutlineItem(key: usize, element: assembly::Element) -> View { move |sel| if sel.contains(&key) { "selected" } else { "" } ); let label = element.label.clone(); - let rep_components = element.rep.iter().map(|u| { - let u_coord = format!("{:.3}", u).replace("-", "\u{2212}"); - View::from(div().children(u_coord)) - }).collect::>(); + let rep_components = element.rep.map( + |rep| rep.iter().map( + |u| format!("{:.3}", u).replace("-", "\u{2212}") + ).collect() + ); let constrained = element.constraints.map(|csts| csts.len() > 0); let constraint_list = element.constraints.map( |csts| csts.clone().into_iter().collect() @@ -139,7 +140,14 @@ fn ElementOutlineItem(key: usize, element: assembly::Element) -> View { } ) { div(class="elt-label") { (label) } - div(class="elt-rep") { (rep_components) } + div(class="elt-rep") { + Indexed( + list=rep_components, + view=|coord_str| view! { + div { (coord_str) } + } + ) + } div(class="status") } } @@ -192,12 +200,7 @@ pub fn Outline() -> View { view=|(key, elt)| view! { ElementOutlineItem(key=key, element=elt) }, - key=|(key, elt)| ( - key.clone(), - elt.id.clone(), - elt.label.clone(), - elt.rep.into_iter().map(|u| u.to_bits()).collect::>() - ) + key=|(key, _)| key.clone() ) } } -- 2.34.1 From e12f4332feb58e61dc160b59b380159d994dc975 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 1 Nov 2024 19:11:33 -0700 Subject: [PATCH 26/29] Use tabular numbers for element vectors --- app-proto/main.css | 1 + 1 file changed, 1 insertion(+) diff --git a/app-proto/main.css b/app-proto/main.css index ba8a20b..80b55f2 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -102,6 +102,7 @@ details[open]:has(li) .elt-switch::after { .elt-rep > div { padding: 2px 0px 0px 0px; font-size: 10pt; + font-variant-numeric: tabular-nums; text-align: right; width: 56px; } -- 2.34.1 From 327a1267d5ae661016a8cac607714805c9377063 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 1 Nov 2024 20:40:25 -0700 Subject: [PATCH 27/29] Test representation validity in realization effect --- app-proto/src/add_remove.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index 9db281e..d135449 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -187,15 +187,15 @@ pub fn AddRemove() -> View { } ); let rep = create_signal(0.0); + let rep_valid = create_signal(false); let active = create_signal(true); state.assembly.insert_constraint(Constraint { args: args, rep: rep, rep_text: create_signal(String::new()), - rep_valid: create_signal(false), + rep_valid: rep_valid, active: active, }); - state.assembly.realize(); state.selection.update(|sel| sel.clear()); /* DEBUG */ @@ -213,15 +213,14 @@ pub fn AddRemove() -> View { } }); - // update the realization when the constraint is activated, - // or edited while active + // update the realization when the constraint becomes active + // and valid, or is edited while active and valid create_effect(move || { + console::log_1(&JsValue::from( + format!("Constraint ({}, {}) updated", args.0, args.1) + )); rep.track(); - console::log_2( - &JsValue::from("Lorentz product updated to"), - &JsValue::from(rep.get_untracked()) - ); - if active.get() { + if active.get() && rep_valid.get() { state.assembly.realize(); } }); -- 2.34.1 From 6e42681b719d7ec97c4225ca321225979bf87b56 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 1 Nov 2024 20:49:00 -0700 Subject: [PATCH 28/29] Stop `Assembly::realize` from reacting to itself Previously, `realize` both tracked and updated the element vectors, so calling it in a reactive context could start a feedback loop. --- app-proto/src/assembly.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 6119939..7b7c015 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -151,7 +151,7 @@ impl Assembly { for (_, elt) in elts { let index = elt.index; gram_to_be.push_sym(index, index, 1.0); - guess_to_be.set_column(index, &elt.rep.get_clone()); + guess_to_be.set_column(index, &elt.rep.get_clone_untracked()); } (gram_to_be, guess_to_be) @@ -193,7 +193,7 @@ impl Assembly { if success { // read out the solution - for (_, elt) in self.elements.get_clone() { + for (_, elt) in self.elements.get_clone_untracked() { elt.rep.update( |rep| rep.set_column(0, &config.column(elt.index)) ); -- 2.34.1 From fc39f2a5f31958764ddab9d2f1b08783acae07b8 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 1 Nov 2024 23:58:45 -0700 Subject: [PATCH 29/29] Switch font to Fira Sans It has tabular numbers, and it's nice and big too. --- app-proto/index.html | 2 +- app-proto/main.css | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app-proto/index.html b/app-proto/index.html index 941a153..92238f4 100644 --- a/app-proto/index.html +++ b/app-proto/index.html @@ -4,7 +4,7 @@ dyna3 - + diff --git a/app-proto/main.css b/app-proto/main.css index 80b55f2..204c8c8 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -2,7 +2,7 @@ body { margin: 0px; color: #fcfcfc; background-color: #222; - font-family: 'Lato'; + font-family: 'Fira Sans', sans-serif; } /* sidebar */ @@ -36,7 +36,7 @@ body { /* KLUDGE */ #add-remove > button.emoji { - font-family: 'Noto Emoji'; + font-family: 'Noto Emoji', sans-serif; } /* outline */ -- 2.34.1