dyna3/app-proto/src/main.rs

65 lines
1.4 KiB
Rust
Raw Normal View History

mod add_remove;
mod assembly;
mod display;
mod engine;
mod outline;
mod specified;
use rustc_hash::FxHashSet;
use sycamore::prelude::*;
use add_remove::AddRemove;
Integrate engine into application prototype (#15) Port the engine prototype to Rust, integrate it into the application prototype, and use it to enforce the constraints. ### Features To see the engine in action: 1. Add a constraint by shift-clicking to select two spheres in the outline view and then hitting the 🔗 button 2. Click a summary arrow to see the outline item for the new constraint 2. Set the constraint's Lorentz product by entering a value in the text field at the right end of the outline item * *The display should update as soon as you press* Enter *or focus away from the text field* The checkbox at the left end of a constraint outline item controls whether the constraint is active. Activating a constraint triggers a solution update. (Deactivating a constraint doesn't, since the remaining active constraints are still satisfied.) ### Precision The Julia prototype of the engine uses a generic scalar type, so you can pass in any type the linear algebra functions are implemented for. The examples use the [adjustable-precision](https://docs.julialang.org/en/v1/base/numbers/#Base.MPFR.setprecision) `BigFloat` type. In the Rust port of the engine, the scalar type is currently fixed at `f64`. Switching to generic scalars shouldn't be too hard, but I haven't looked into [which other types](https://www.nalgebra.org/docs/user_guide/generic_programming) the linear algebra functions are implemented for. ### Testing To confirm quantitatively that the Rust port of the engine is working, you can go to the `app-proto` folder and: * Run some automated tests by calling `cargo test`. * Inspect the optimization process in a few examples calling the `run-examples` script. The first example that prints is the same as the Irisawa hexlet example from the engine prototype. If you go into `engine-proto/gram-test`, launch Julia, and then ``` include("irisawa-hexlet.jl") for (step, scaled_loss) in enumerate(history_alt.scaled_loss) println(rpad(step-1, 4), " | ", scaled_loss) end ``` you should see that it prints basically the same loss history until the last few steps, when the lower default precision of the Rust engine really starts to show. ### A small engine revision The Rust port of the engine improves on the Julia prototype in one part of the constraint-solving routine: projecting the Hessian onto the subspace where the frozen entries stay constant. The Julia prototype does this by removing the rows and columns of the Hessian that correspond to the frozen entries, finding the Newton step from the resulting "compressed" Hessian, and then adding zero entries to the Newton step in the appropriate places. The Rust port instead replaces each frozen row and column with its corresponding standard unit vector, avoiding the finicky compressing and decompressing steps. To confirm that this version of the constraint-solving routine works the same as the original, I implemented it in Julia as `realize_gram_alt_proj`. The solutions we get from this routine match the ones we get from the original `realize_gram` to very high precision, and in the simplest examples (`sphere-in-tetrahedron.jl` and `tetrahedron-radius-ratio.jl`), the descent paths also match to very high precision. In a more complicated example (`irisawa-hexlet.jl`), the descent paths diverge about a quarter of the way into the search, even though they end up in the same place. Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo> Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/15 Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net> Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2024-11-12 00:46:16 +00:00
use assembly::{Assembly, ElementKey};
use display::Display;
use outline::Outline;
#[derive(Clone)]
struct AppState {
assembly: Assembly,
Integrate engine into application prototype (#15) Port the engine prototype to Rust, integrate it into the application prototype, and use it to enforce the constraints. ### Features To see the engine in action: 1. Add a constraint by shift-clicking to select two spheres in the outline view and then hitting the 🔗 button 2. Click a summary arrow to see the outline item for the new constraint 2. Set the constraint's Lorentz product by entering a value in the text field at the right end of the outline item * *The display should update as soon as you press* Enter *or focus away from the text field* The checkbox at the left end of a constraint outline item controls whether the constraint is active. Activating a constraint triggers a solution update. (Deactivating a constraint doesn't, since the remaining active constraints are still satisfied.) ### Precision The Julia prototype of the engine uses a generic scalar type, so you can pass in any type the linear algebra functions are implemented for. The examples use the [adjustable-precision](https://docs.julialang.org/en/v1/base/numbers/#Base.MPFR.setprecision) `BigFloat` type. In the Rust port of the engine, the scalar type is currently fixed at `f64`. Switching to generic scalars shouldn't be too hard, but I haven't looked into [which other types](https://www.nalgebra.org/docs/user_guide/generic_programming) the linear algebra functions are implemented for. ### Testing To confirm quantitatively that the Rust port of the engine is working, you can go to the `app-proto` folder and: * Run some automated tests by calling `cargo test`. * Inspect the optimization process in a few examples calling the `run-examples` script. The first example that prints is the same as the Irisawa hexlet example from the engine prototype. If you go into `engine-proto/gram-test`, launch Julia, and then ``` include("irisawa-hexlet.jl") for (step, scaled_loss) in enumerate(history_alt.scaled_loss) println(rpad(step-1, 4), " | ", scaled_loss) end ``` you should see that it prints basically the same loss history until the last few steps, when the lower default precision of the Rust engine really starts to show. ### A small engine revision The Rust port of the engine improves on the Julia prototype in one part of the constraint-solving routine: projecting the Hessian onto the subspace where the frozen entries stay constant. The Julia prototype does this by removing the rows and columns of the Hessian that correspond to the frozen entries, finding the Newton step from the resulting "compressed" Hessian, and then adding zero entries to the Newton step in the appropriate places. The Rust port instead replaces each frozen row and column with its corresponding standard unit vector, avoiding the finicky compressing and decompressing steps. To confirm that this version of the constraint-solving routine works the same as the original, I implemented it in Julia as `realize_gram_alt_proj`. The solutions we get from this routine match the ones we get from the original `realize_gram` to very high precision, and in the simplest examples (`sphere-in-tetrahedron.jl` and `tetrahedron-radius-ratio.jl`), the descent paths also match to very high precision. In a more complicated example (`irisawa-hexlet.jl`), the descent paths diverge about a quarter of the way into the search, even though they end up in the same place. Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo> Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/15 Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net> Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2024-11-12 00:46:16 +00:00
selection: Signal<FxHashSet<ElementKey>>
}
impl AppState {
fn new() -> AppState {
AppState {
assembly: Assembly::new(),
selection: create_signal(FxHashSet::default())
}
}
// in single-selection mode, select the element with the given key. in
// multiple-selection mode, toggle whether the element with the given key
// is selected
fn select(&self, key: ElementKey, multi: bool) {
if multi {
self.selection.update(|sel| {
if !sel.remove(&key) {
sel.insert(key);
}
});
} else {
self.selection.update(|sel| {
sel.clear();
sel.insert(key);
});
}
}
}
fn main() {
Manipulate the assembly (#29) feat: Find tangent space of solution variety, use for perturbations ### Tangent space #### Implementation The structure `engine::ConfigSubspace` represents a subspace of the configuration vector space $\operatorname{Hom}(\mathbb{R}^n, \mathbb{R}^5)$. It holds a basis for the subspace which is orthonormal with respect to the Euclidean inner product. The method `ConfigSubspace::symmetric_kernel` takes an endomorphism of the configuration vector space, which must be symmetric with respect to the Euclidean inner product, and returns its approximate kernel in the form of a `ConfigSubspace`. At the end of `engine::realize_gram`, we use the computed Hessian to find the tangent space of the solution variety, and we return it alongside the realization. Since altering the constraints can change the tangent space without changing the solution, we compute the tangent space even when the guess passed to the realization routine is already a solution. After `Assembly::realize` calls `engine::realize_gram`, it saves the returned tangent space in the assembly's `tangent` signal. The basis vectors are stored in configuration matrix format, ordered according to the elements' column indices. To help maintain consistency between the storage layout of the tangent space and the elements' column indices, we switch the column index data type from `usize` to `Option<usize>` and enforce the following invariants: 1. If an element has a column index, its tangent motions can be found in that column of the tangent space basis matrices. 2. If an element is affected by a constraint, it has a column index. The comments in `assembly.rs` state the invariants and describe how they're enforced. #### Automated testing The test `engine::tests::tangent_test` builds a simple assembly with a known tangent space, runs the realization routine, and checks the returned tangent space against a hand-computed basis. #### Limitations The method `ConfigSubspace::symmetric_kernel` approximates the kernel by taking all the eigenspaces whose eigenvalues are smaller than a hard-coded threshold size. We may need a more flexible system eventually. ### Deformation #### Implementation The main purpose of this implementation is to confirm that deformation works as we'd hoped. The code is messy, and the deformation routine has at least one numerical quirk. For simplicity, the keyboard commands that manipulate the assembly are handled by the display, just like the keyboard commands that control the camera. Deformation happens at the beginning of the animation loop. The function `Assembly::deform` works like this: 1. Take a list of element motions 2. Project them onto the tangent space of the solution variety 3. Sum them to get a deformation $v$ of the whole assembly 4. Step the assembly along the "mass shell" geodesic tangent to $v$ * This step stays on the solution variety to first order 5. Call `realize` to bring the assembly back onto the solution variety #### Manual testing To manipulate the assembly: 1. Select a sphere 2. Make sure the display has focus 3. Hold the following keys: * **A**/**D** for $x$ translation * **W**/**S** for $y$ translation * **shift**+**W**/**S** for $z$ translation #### Limitations Because the manipulation commands are handled by the display, you can only manipulate the assembly when the display has focus. Since our test assemblies only include spheres, we assume in `Assembly::deform` that every element is a sphere. When the tangent space is zero, `Assembly::deform` does nothing except print "The assembly is rigid" to the console. During a deformation, the curvature and co-curvature components of a sphere's vector representation can exhibit weird discontinuous "swaps" that don't visibly affect how the sphere is drawn. *[I'll write more about this in an issue.]* Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo> Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/29 Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net> Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2024-12-30 22:53:07 +00:00
// set the console error panic hook
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
sycamore::render(|| {
provide_context(AppState::new());
view! {
div(id="sidebar") {
AddRemove {}
Outline {}
}
Display {}
}
});
}