2025-05-01 19:25:13 +00:00
|
|
|
use nalgebra::{DMatrix, DVector, DVectorView};
|
|
|
|
use std::{
|
|
|
|
cell::Cell,
|
2025-05-06 19:17:30 +00:00
|
|
|
cmp::Ordering,
|
2025-08-04 23:34:33 +00:00
|
|
|
collections::{BTreeMap, BTreeSet},
|
2025-05-06 19:17:30 +00:00
|
|
|
fmt,
|
|
|
|
fmt::{Debug, Formatter},
|
|
|
|
hash::{Hash, Hasher},
|
2025-05-01 19:25:13 +00:00
|
|
|
rc::Rc,
|
2025-08-04 23:34:33 +00:00
|
|
|
sync::{atomic, atomic::AtomicU64},
|
2025-05-01 19:25:13 +00:00
|
|
|
};
|
2024-10-21 23:38:27 +00:00
|
|
|
use sycamore::prelude::*;
|
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 web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
|
|
|
|
|
2025-03-10 23:43:24 +00:00
|
|
|
use crate::{
|
2025-07-22 22:01:37 +00:00
|
|
|
components::{display::DisplayItem, outline::OutlineItem},
|
2025-04-21 23:40:42 +00:00
|
|
|
engine::{
|
|
|
|
Q,
|
|
|
|
local_unif_to_std,
|
2025-05-01 19:25:13 +00:00
|
|
|
point,
|
2025-06-04 21:01:12 +00:00
|
|
|
project_point_to_normalized,
|
|
|
|
project_sphere_to_normalized,
|
2025-04-21 23:40:42 +00:00
|
|
|
realize_gram,
|
|
|
|
sphere,
|
2025-07-21 04:18:49 +00:00
|
|
|
ConfigNeighborhood,
|
2025-04-21 23:40:42 +00:00
|
|
|
ConfigSubspace,
|
2025-07-21 04:18:49 +00:00
|
|
|
ConstraintProblem,
|
|
|
|
DescentHistory,
|
2025-08-04 23:34:33 +00:00
|
|
|
Realization,
|
2025-04-21 23:40:42 +00:00
|
|
|
},
|
2025-08-04 23:34:33 +00:00
|
|
|
specified::SpecifiedValue,
|
2025-03-10 23:43:24 +00:00
|
|
|
};
|
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
|
|
|
|
|
|
|
pub type ElementColor = [f32; 3];
|
2024-10-21 23:38:27 +00:00
|
|
|
|
2024-11-22 02:25:10 +00:00
|
|
|
/* KLUDGE */
|
|
|
|
// we should reconsider this design when we build a system for switching between
|
|
|
|
// assemblies. at that point, we might want to switch to hierarchical keys,
|
2025-05-06 19:17:30 +00:00
|
|
|
// where each each item has a key that identifies it within its assembly and
|
2024-11-22 02:25:10 +00:00
|
|
|
// each assembly has a key that identifies it within the sesssion
|
2025-05-06 19:17:30 +00:00
|
|
|
static NEXT_SERIAL: AtomicU64 = AtomicU64::new(0);
|
|
|
|
|
|
|
|
pub trait Serial {
|
|
|
|
// a serial number that uniquely identifies this element
|
|
|
|
fn serial(&self) -> u64;
|
|
|
|
|
|
|
|
// take the next serial number, panicking if that was the last one left
|
|
|
|
fn next_serial() -> u64 where Self: Sized {
|
|
|
|
// the technique we use to panic on overflow is taken from _Rust Atomics
|
|
|
|
// and Locks_, by Mara Bos
|
|
|
|
//
|
|
|
|
// https://marabos.nl/atomics/atomics.html#example-handle-overflow
|
|
|
|
//
|
|
|
|
NEXT_SERIAL.fetch_update(
|
|
|
|
atomic::Ordering::SeqCst, atomic::Ordering::SeqCst,
|
|
|
|
|serial| serial.checked_add(1)
|
|
|
|
).expect("Out of serial numbers for elements")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Hash for dyn Serial {
|
|
|
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
|
|
self.serial().hash(state)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl PartialEq for dyn Serial {
|
|
|
|
fn eq(&self, other: &Self) -> bool {
|
|
|
|
self.serial() == other.serial()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Eq for dyn Serial {}
|
|
|
|
|
|
|
|
impl PartialOrd for dyn Serial {
|
|
|
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
|
|
Some(self.cmp(other))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Ord for dyn Serial {
|
|
|
|
fn cmp(&self, other: &Self) -> Ordering {
|
|
|
|
self.serial().cmp(&other.serial())
|
|
|
|
}
|
|
|
|
}
|
2024-11-22 02:25:10 +00:00
|
|
|
|
2025-04-21 23:40:42 +00:00
|
|
|
pub trait ProblemPoser {
|
2025-05-06 19:17:30 +00:00
|
|
|
fn pose(&self, problem: &mut ConstraintProblem);
|
2025-04-21 23:40:42 +00:00
|
|
|
}
|
|
|
|
|
2025-05-06 19:17:30 +00:00
|
|
|
pub trait Element: Serial + ProblemPoser + DisplayItem {
|
2025-05-01 19:25:13 +00:00
|
|
|
// the default identifier for an element of this type
|
|
|
|
fn default_id() -> String where Self: Sized;
|
|
|
|
|
2025-05-06 19:17:30 +00:00
|
|
|
// the default example of an element of this type
|
2025-05-01 19:25:13 +00:00
|
|
|
fn default(id: String, id_num: u64) -> Self where Self: Sized;
|
|
|
|
|
2025-05-06 19:17:30 +00:00
|
|
|
// the default regulators that come with this element
|
|
|
|
fn default_regulators(self: Rc<Self>) -> Vec<Rc<dyn Regulator>> {
|
2025-05-01 19:25:13 +00:00
|
|
|
Vec::new()
|
|
|
|
}
|
2025-03-10 23:43:24 +00:00
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
fn id(&self) -> &String;
|
|
|
|
fn label(&self) -> &String;
|
|
|
|
fn representation(&self) -> Signal<DVector<f64>>;
|
2025-06-02 15:56:06 +00:00
|
|
|
fn ghost(&self) -> Signal<bool>;
|
2025-05-01 19:25:13 +00:00
|
|
|
|
|
|
|
// the regulators the element is subject to. the assembly that owns the
|
2025-04-21 23:40:42 +00:00
|
|
|
// element is responsible for keeping this set up to date
|
2025-05-06 19:17:30 +00:00
|
|
|
fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>>;
|
2025-05-01 19:25:13 +00:00
|
|
|
|
2025-06-04 21:01:12 +00:00
|
|
|
// project a representation vector for this kind of element onto its
|
|
|
|
// normalization variety
|
|
|
|
fn project_to_normalized(&self, rep: &mut DVector<f64>);
|
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
// the configuration matrix column index that was assigned to the element
|
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
|
|
|
// last time the assembly was realized, or `None` if the element has never
|
|
|
|
// been through a realization
|
2025-05-01 19:25:13 +00:00
|
|
|
fn column_index(&self) -> Option<usize>;
|
|
|
|
|
|
|
|
// assign the element a configuration matrix column index. this method must
|
|
|
|
// be used carefully to preserve invariant (1), described in the comment on
|
|
|
|
// the `tangent` field of the `Assembly` structure
|
|
|
|
fn set_column_index(&self, index: usize);
|
|
|
|
}
|
|
|
|
|
2025-05-06 19:17:30 +00:00
|
|
|
impl Debug for dyn Element {
|
|
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
|
|
|
|
self.id().fmt(f)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Hash for dyn Element {
|
|
|
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
|
|
<dyn Serial>::hash(self, state)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl PartialEq for dyn Element {
|
|
|
|
fn eq(&self, other: &Self) -> bool {
|
|
|
|
<dyn Serial>::eq(self, other)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Eq for dyn Element {}
|
|
|
|
|
|
|
|
impl PartialOrd for dyn Element {
|
|
|
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
|
|
<dyn Serial>::partial_cmp(self, other)
|
|
|
|
}
|
|
|
|
}
|
2025-05-01 19:25:13 +00:00
|
|
|
|
2025-05-06 19:17:30 +00:00
|
|
|
impl Ord for dyn Element {
|
|
|
|
fn cmp(&self, other: &Self) -> Ordering {
|
|
|
|
<dyn Serial>::cmp(self, other)
|
2025-05-01 19:25:13 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct Sphere {
|
|
|
|
pub id: String,
|
|
|
|
pub label: String,
|
|
|
|
pub color: ElementColor,
|
|
|
|
pub representation: Signal<DVector<f64>>,
|
2025-06-02 15:56:06 +00:00
|
|
|
pub ghost: Signal<bool>,
|
2025-05-06 19:17:30 +00:00
|
|
|
pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>,
|
|
|
|
serial: u64,
|
2025-08-04 23:34:33 +00:00
|
|
|
column_index: Cell<Option<usize>>,
|
2024-10-21 23:38:27 +00:00
|
|
|
}
|
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
impl Sphere {
|
2025-04-21 23:40:42 +00:00
|
|
|
const CURVATURE_COMPONENT: usize = 3;
|
|
|
|
|
2024-11-15 03:32:47 +00:00
|
|
|
pub fn new(
|
|
|
|
id: String,
|
|
|
|
label: String,
|
|
|
|
color: ElementColor,
|
2025-08-04 23:34:33 +00:00
|
|
|
representation: DVector<f64>,
|
2025-08-07 23:24:07 +00:00
|
|
|
) -> Self {
|
|
|
|
Self {
|
2025-08-04 23:34:33 +00:00
|
|
|
id,
|
|
|
|
label,
|
|
|
|
color,
|
2024-11-15 03:32:47 +00:00
|
|
|
representation: create_signal(representation),
|
2025-06-02 15:56:06 +00:00
|
|
|
ghost: create_signal(false),
|
2025-05-06 19:17:30 +00:00
|
|
|
regulators: create_signal(BTreeSet::new()),
|
2025-05-01 19:25:13 +00:00
|
|
|
serial: Self::next_serial(),
|
2025-08-04 23:34:33 +00:00
|
|
|
column_index: None.into(),
|
2024-11-15 03:32:47 +00:00
|
|
|
}
|
|
|
|
}
|
2025-05-01 19:25:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Element for Sphere {
|
|
|
|
fn default_id() -> String {
|
|
|
|
"sphere".to_string()
|
|
|
|
}
|
2024-11-27 05:02:06 +00:00
|
|
|
|
2025-08-07 23:24:07 +00:00
|
|
|
fn default(id: String, id_num: u64) -> Self {
|
|
|
|
Self::new(
|
2025-05-01 19:25:13 +00:00
|
|
|
id,
|
|
|
|
format!("Sphere {id_num}"),
|
|
|
|
[0.75_f32, 0.75_f32, 0.75_f32],
|
2025-08-04 23:34:33 +00:00
|
|
|
sphere(0.0, 0.0, 0.0, 1.0),
|
2025-05-01 19:25:13 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2025-05-06 19:17:30 +00:00
|
|
|
fn default_regulators(self: Rc<Self>) -> Vec<Rc<dyn Regulator>> {
|
|
|
|
vec![Rc::new(HalfCurvatureRegulator::new(self))]
|
2025-05-01 19:25:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn id(&self) -> &String {
|
|
|
|
&self.id
|
|
|
|
}
|
|
|
|
|
|
|
|
fn label(&self) -> &String {
|
|
|
|
&self.label
|
|
|
|
}
|
|
|
|
|
|
|
|
fn representation(&self) -> Signal<DVector<f64>> {
|
|
|
|
self.representation
|
|
|
|
}
|
|
|
|
|
2025-06-02 15:56:06 +00:00
|
|
|
fn ghost(&self) -> Signal<bool> {
|
|
|
|
self.ghost
|
|
|
|
}
|
|
|
|
|
2025-05-06 19:17:30 +00:00
|
|
|
fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>> {
|
2025-05-01 19:25:13 +00:00
|
|
|
self.regulators
|
|
|
|
}
|
|
|
|
|
2025-06-04 21:01:12 +00:00
|
|
|
fn project_to_normalized(&self, rep: &mut DVector<f64>) {
|
|
|
|
project_sphere_to_normalized(rep);
|
|
|
|
}
|
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
fn column_index(&self) -> Option<usize> {
|
|
|
|
self.column_index.get()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn set_column_index(&self, index: usize) {
|
|
|
|
self.column_index.set(Some(index));
|
2024-11-27 05:02:06 +00:00
|
|
|
}
|
2024-11-15 03:32:47 +00:00
|
|
|
}
|
|
|
|
|
2025-05-06 19:17:30 +00:00
|
|
|
impl Serial for Sphere {
|
|
|
|
fn serial(&self) -> u64 {
|
|
|
|
self.serial
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
impl ProblemPoser for Sphere {
|
2025-05-06 19:17:30 +00:00
|
|
|
fn pose(&self, problem: &mut ConstraintProblem) {
|
2025-05-01 19:25:13 +00:00
|
|
|
let index = self.column_index().expect(
|
|
|
|
format!("Sphere \"{}\" should be indexed before writing problem data", self.id).as_str()
|
2025-04-21 23:40:42 +00:00
|
|
|
);
|
|
|
|
problem.gram.push_sym(index, index, 1.0);
|
|
|
|
problem.guess.set_column(index, &self.representation.get_clone_untracked());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
pub struct Point {
|
|
|
|
pub id: String,
|
|
|
|
pub label: String,
|
|
|
|
pub color: ElementColor,
|
|
|
|
pub representation: Signal<DVector<f64>>,
|
2025-06-02 15:56:06 +00:00
|
|
|
pub ghost: Signal<bool>,
|
2025-05-06 19:17:30 +00:00
|
|
|
pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>,
|
|
|
|
serial: u64,
|
2025-08-04 23:34:33 +00:00
|
|
|
column_index: Cell<Option<usize>>,
|
2025-05-01 19:25:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Point {
|
|
|
|
const WEIGHT_COMPONENT: usize = 3;
|
|
|
|
|
|
|
|
pub fn new(
|
|
|
|
id: String,
|
|
|
|
label: String,
|
|
|
|
color: ElementColor,
|
2025-08-04 23:34:33 +00:00
|
|
|
representation: DVector<f64>,
|
2025-08-07 23:24:07 +00:00
|
|
|
) -> Self {
|
|
|
|
Self {
|
2025-05-01 19:25:13 +00:00
|
|
|
id,
|
|
|
|
label,
|
|
|
|
color,
|
|
|
|
representation: create_signal(representation),
|
2025-06-02 15:56:06 +00:00
|
|
|
ghost: create_signal(false),
|
2025-05-06 19:17:30 +00:00
|
|
|
regulators: create_signal(BTreeSet::new()),
|
2025-05-01 19:25:13 +00:00
|
|
|
serial: Self::next_serial(),
|
2025-08-04 23:34:33 +00:00
|
|
|
column_index: None.into(),
|
2025-05-01 19:25:13 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Element for Point {
|
|
|
|
fn default_id() -> String {
|
|
|
|
"point".to_string()
|
|
|
|
}
|
|
|
|
|
2025-08-07 23:24:07 +00:00
|
|
|
fn default(id: String, id_num: u64) -> Self {
|
|
|
|
Self::new(
|
2025-05-01 19:25:13 +00:00
|
|
|
id,
|
|
|
|
format!("Point {id_num}"),
|
|
|
|
[0.75_f32, 0.75_f32, 0.75_f32],
|
2025-08-04 23:34:33 +00:00
|
|
|
point(0.0, 0.0, 0.0),
|
2025-05-01 19:25:13 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn id(&self) -> &String {
|
|
|
|
&self.id
|
|
|
|
}
|
|
|
|
|
|
|
|
fn label(&self) -> &String {
|
|
|
|
&self.label
|
|
|
|
}
|
|
|
|
|
|
|
|
fn representation(&self) -> Signal<DVector<f64>> {
|
|
|
|
self.representation
|
|
|
|
}
|
|
|
|
|
2025-06-02 15:56:06 +00:00
|
|
|
fn ghost(&self) -> Signal<bool> {
|
|
|
|
self.ghost
|
|
|
|
}
|
|
|
|
|
2025-05-06 19:17:30 +00:00
|
|
|
fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>> {
|
2025-05-01 19:25:13 +00:00
|
|
|
self.regulators
|
|
|
|
}
|
|
|
|
|
2025-06-04 21:01:12 +00:00
|
|
|
fn project_to_normalized(&self, rep: &mut DVector<f64>) {
|
|
|
|
project_point_to_normalized(rep);
|
|
|
|
}
|
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
fn column_index(&self) -> Option<usize> {
|
|
|
|
self.column_index.get()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn set_column_index(&self, index: usize) {
|
|
|
|
self.column_index.set(Some(index));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-06 19:17:30 +00:00
|
|
|
impl Serial for Point {
|
|
|
|
fn serial(&self) -> u64 {
|
|
|
|
self.serial
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
impl ProblemPoser for Point {
|
2025-05-06 19:17:30 +00:00
|
|
|
fn pose(&self, problem: &mut ConstraintProblem) {
|
2025-05-01 19:25:13 +00:00
|
|
|
let index = self.column_index().expect(
|
|
|
|
format!("Point \"{}\" should be indexed before writing problem data", self.id).as_str()
|
|
|
|
);
|
|
|
|
problem.gram.push_sym(index, index, 0.0);
|
2025-08-07 23:24:07 +00:00
|
|
|
problem.frozen.push(Self::WEIGHT_COMPONENT, index, 0.5);
|
2025-05-01 19:25:13 +00:00
|
|
|
problem.guess.set_column(index, &self.representation.get_clone_untracked());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-06 19:17:30 +00:00
|
|
|
pub trait Regulator: Serial + ProblemPoser + OutlineItem {
|
|
|
|
fn subjects(&self) -> Vec<Rc<dyn Element>>;
|
2025-04-21 23:40:42 +00:00
|
|
|
fn measurement(&self) -> ReadSignal<f64>;
|
|
|
|
fn set_point(&self) -> Signal<SpecifiedValue>;
|
|
|
|
}
|
|
|
|
|
2025-05-06 19:17:30 +00:00
|
|
|
impl Hash for dyn Regulator {
|
|
|
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
|
|
<dyn Serial>::hash(self, state)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl PartialEq for dyn Regulator {
|
|
|
|
fn eq(&self, other: &Self) -> bool {
|
|
|
|
<dyn Serial>::eq(self, other)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Eq for dyn Regulator {}
|
|
|
|
|
|
|
|
impl PartialOrd for dyn Regulator {
|
|
|
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
|
|
<dyn Serial>::partial_cmp(self, other)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Ord for dyn Regulator {
|
|
|
|
fn cmp(&self, other: &Self) -> Ordering {
|
|
|
|
<dyn Serial>::cmp(self, other)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-04-21 23:40:42 +00:00
|
|
|
pub struct InversiveDistanceRegulator {
|
2025-05-06 19:17:30 +00:00
|
|
|
pub subjects: [Rc<dyn Element>; 2],
|
2025-04-21 23:40:42 +00:00
|
|
|
pub measurement: ReadSignal<f64>,
|
2025-05-06 19:17:30 +00:00
|
|
|
pub set_point: Signal<SpecifiedValue>,
|
2025-08-04 23:34:33 +00:00
|
|
|
serial: u64,
|
2025-04-21 23:40:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl InversiveDistanceRegulator {
|
2025-08-07 23:24:07 +00:00
|
|
|
pub fn new(subjects: [Rc<dyn Element>; 2]) -> Self {
|
2025-05-06 19:17:30 +00:00
|
|
|
let representations = subjects.each_ref().map(|subj| subj.representation());
|
|
|
|
let measurement = create_memo(move || {
|
|
|
|
representations[0].with(|rep_0|
|
|
|
|
representations[1].with(|rep_1|
|
|
|
|
rep_0.dot(&(&*Q * rep_1))
|
2025-04-21 23:40:42 +00:00
|
|
|
)
|
2025-05-06 19:17:30 +00:00
|
|
|
)
|
|
|
|
});
|
2025-04-21 23:40:42 +00:00
|
|
|
|
|
|
|
let set_point = create_signal(SpecifiedValue::from_empty_spec());
|
2025-05-06 19:17:30 +00:00
|
|
|
let serial = Self::next_serial();
|
2025-04-21 23:40:42 +00:00
|
|
|
|
2025-08-07 23:24:07 +00:00
|
|
|
Self { subjects, measurement, set_point, serial }
|
2025-04-21 23:40:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Regulator for InversiveDistanceRegulator {
|
2025-05-06 19:17:30 +00:00
|
|
|
fn subjects(&self) -> Vec<Rc<dyn Element>> {
|
|
|
|
self.subjects.clone().into()
|
2025-04-21 23:40:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn measurement(&self) -> ReadSignal<f64> {
|
|
|
|
self.measurement
|
|
|
|
}
|
|
|
|
|
|
|
|
fn set_point(&self) -> Signal<SpecifiedValue> {
|
|
|
|
self.set_point
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-06 19:17:30 +00:00
|
|
|
impl Serial for InversiveDistanceRegulator {
|
|
|
|
fn serial(&self) -> u64 {
|
|
|
|
self.serial
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-04-21 23:40:42 +00:00
|
|
|
impl ProblemPoser for InversiveDistanceRegulator {
|
2025-05-06 19:17:30 +00:00
|
|
|
fn pose(&self, problem: &mut ConstraintProblem) {
|
2025-04-21 23:40:42 +00:00
|
|
|
self.set_point.with_untracked(|set_pt| {
|
|
|
|
if let Some(val) = set_pt.value {
|
2025-05-06 19:17:30 +00:00
|
|
|
let [row, col] = self.subjects.each_ref().map(
|
|
|
|
|subj| subj.column_index().expect(
|
2025-04-21 23:40:42 +00:00
|
|
|
"Subjects should be indexed before inversive distance regulator writes problem data"
|
|
|
|
)
|
|
|
|
);
|
|
|
|
problem.gram.push_sym(row, col, val);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct HalfCurvatureRegulator {
|
2025-05-06 19:17:30 +00:00
|
|
|
pub subject: Rc<dyn Element>,
|
2025-03-10 23:43:24 +00:00
|
|
|
pub measurement: ReadSignal<f64>,
|
2025-05-06 19:17:30 +00:00
|
|
|
pub set_point: Signal<SpecifiedValue>,
|
2025-08-04 23:34:33 +00:00
|
|
|
serial: u64,
|
2024-10-21 23:38:27 +00:00
|
|
|
}
|
|
|
|
|
2025-04-21 23:40:42 +00:00
|
|
|
impl HalfCurvatureRegulator {
|
2025-08-07 23:24:07 +00:00
|
|
|
pub fn new(subject: Rc<dyn Element>) -> Self {
|
2025-05-06 19:17:30 +00:00
|
|
|
let measurement = subject.representation().map(
|
|
|
|
|rep| rep[Sphere::CURVATURE_COMPONENT]
|
2025-04-21 23:40:42 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
let set_point = create_signal(SpecifiedValue::from_empty_spec());
|
2025-05-06 19:17:30 +00:00
|
|
|
let serial = Self::next_serial();
|
2025-04-21 23:40:42 +00:00
|
|
|
|
2025-08-07 23:24:07 +00:00
|
|
|
Self { subject, measurement, set_point, serial }
|
2025-04-21 23:40:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Regulator for HalfCurvatureRegulator {
|
2025-05-06 19:17:30 +00:00
|
|
|
fn subjects(&self) -> Vec<Rc<dyn Element>> {
|
|
|
|
vec![self.subject.clone()]
|
2025-04-21 23:40:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn measurement(&self) -> ReadSignal<f64> {
|
|
|
|
self.measurement
|
|
|
|
}
|
|
|
|
|
|
|
|
fn set_point(&self) -> Signal<SpecifiedValue> {
|
|
|
|
self.set_point
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-06 19:17:30 +00:00
|
|
|
impl Serial for HalfCurvatureRegulator {
|
|
|
|
fn serial(&self) -> u64 {
|
|
|
|
self.serial
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-04-21 23:40:42 +00:00
|
|
|
impl ProblemPoser for HalfCurvatureRegulator {
|
2025-05-06 19:17:30 +00:00
|
|
|
fn pose(&self, problem: &mut ConstraintProblem) {
|
2025-04-21 23:40:42 +00:00
|
|
|
self.set_point.with_untracked(|set_pt| {
|
|
|
|
if let Some(val) = set_pt.value {
|
2025-05-06 19:17:30 +00:00
|
|
|
let col = self.subject.column_index().expect(
|
2025-04-21 23:40:42 +00:00
|
|
|
"Subject should be indexed before half-curvature regulator writes problem data"
|
|
|
|
);
|
2025-05-01 19:25:13 +00:00
|
|
|
problem.frozen.push(Sphere::CURVATURE_COMPONENT, col, val);
|
2025-04-21 23:40:42 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
Switch to Euclidean-invariant projection onto tangent space of solution variety (#34)
This pull request addresses issues #32 and #33 by projecting nudges onto the tangent space of the solution variety using a Euclidean-invariant inner product, which I'm calling the *uniform* inner product.
### Definition of the uniform inner product
For spheres and planes, the uniform inner product is defined on the tangent space of the hyperboloid $\langle v, v \rangle = 1$. For points, it's defined on the tangent space of the paraboloid $\langle v, v \rangle = 0,\; \langle v, I_\infty \rangle = 1$.
The tangent space of an assembly can be expressed as the direct sum of the tangent spaces of the elements. We extend the uniform inner product to assemblies by declaring the tangent spaces of different elements to be orthogonal.
#### For spheres and planes
If $v = [x, y, z, b, c]^\top$ is on the hyperboloid $\langle v, v \rangle = 1$, the vectors
$$\left[ \begin{array}{c} 2b \\ \cdot \\ \cdot \\ \cdot \\ x \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ 2b \\ \cdot \\ \cdot \\ y \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ \cdot \\ 2b \\ \cdot \\ z \end{array} \right],\;\left[ \begin{array}{l} 2bx \\ 2by \\ 2bz \\ 2b^2 \\ 2bc + 1 \end{array} \right]$$
form a basis for the tangent space of hyperboloid at $v$. We declare this basis to be orthonormal with respect to the uniform inner product.
The first three vectors in the basis are unit-speed translations along the coordinate axes. The last vector moves the surface at unit speed along its normal field. For spheres, this increases the radius at unit rate. For planes, this translates the plane parallel to itself at unit speed. This description makes it clear that the uniform inner product is invariant under Euclidean motions.
#### For points
If $v = [x, y, z, b, c]^\top$ is on the paraboloid $\langle v, v \rangle = 0,\; \langle v, I_\infty \rangle = 1$, the vectors
$$\left[ \begin{array}{c} 2b \\ \cdot \\ \cdot \\ \cdot \\ x \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ 2b \\ \cdot \\ \cdot \\ y \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ \cdot \\ 2b \\ \cdot \\ z \end{array} \right]$$
form a basis for the tangent space of paraboloid at $v$. We declare this basis to be orthonormal with respect to the uniform inner product.
The meanings of the basis vectors, and the argument that the uniform inner product is Euclidean-invariant, are the same as for spheres and planes. In the engine, we pad the basis with $[0, 0, 0, 0, 1]^\top$ to keep the number of uniform coordinates consistent across element types.
### Confirmation of intended behavior
Two new tests confirm that we've corrected the misbehaviors described in issues #32 and #33.
Issue | Test
---|---
#32 | `proj_equivar_test`
#33 | `tangent_test_kaleidocycle`
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/34
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2025-01-31 19:34:33 +00:00
|
|
|
// the velocity is expressed in uniform coordinates
|
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
|
|
|
pub struct ElementMotion<'a> {
|
2025-05-06 19:17:30 +00:00
|
|
|
pub element: Rc<dyn Element>,
|
2025-08-04 23:34:33 +00:00
|
|
|
pub velocity: DVectorView<'a, f64>,
|
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
|
|
|
}
|
|
|
|
|
|
|
|
type AssemblyMotion<'a> = Vec<ElementMotion<'a>>;
|
|
|
|
|
2024-10-21 23:38:27 +00:00
|
|
|
// a complete, view-independent description of an assembly
|
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct Assembly {
|
2025-03-10 23:43:24 +00:00
|
|
|
// elements and regulators
|
2025-05-06 19:17:30 +00:00
|
|
|
pub elements: Signal<BTreeSet<Rc<dyn Element>>>,
|
|
|
|
pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>,
|
2024-10-21 23:38:27 +00:00
|
|
|
|
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
|
|
|
// solution variety tangent space. the basis vectors are stored in
|
|
|
|
// configuration matrix format, ordered according to the elements' column
|
|
|
|
// indices. when you realize the assembly, every element that's present
|
|
|
|
// during realization gets a column index and is reflected in the tangent
|
|
|
|
// space. since the methods in this module never assign column indices
|
|
|
|
// without later realizing the assembly, we get the following invariant:
|
|
|
|
//
|
|
|
|
// (1) if an element has a column index, its tangent motions can be found
|
|
|
|
// in that column of the tangent space basis matrices
|
|
|
|
//
|
|
|
|
pub tangent: Signal<ConfigSubspace>,
|
|
|
|
|
2024-10-21 23:38:27 +00:00
|
|
|
// indexing
|
2025-07-21 04:18:49 +00:00
|
|
|
pub elements_by_id: Signal<BTreeMap<String, Rc<dyn Element>>>,
|
|
|
|
|
2025-07-22 22:01:37 +00:00
|
|
|
// realization control
|
2025-07-31 22:21:32 +00:00
|
|
|
pub realization_trigger: Signal<()>,
|
2025-07-22 22:01:37 +00:00
|
|
|
|
2025-07-21 04:18:49 +00:00
|
|
|
// realization diagnostics
|
|
|
|
pub realization_status: Signal<Result<(), String>>,
|
2025-08-04 23:34:33 +00:00
|
|
|
pub descent_history: Signal<DescentHistory>,
|
2024-10-21 23:38:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Assembly {
|
|
|
|
pub fn new() -> Assembly {
|
2025-07-22 22:01:37 +00:00
|
|
|
// create an assembly
|
|
|
|
let assembly = Assembly {
|
2025-05-06 19:17:30 +00:00
|
|
|
elements: create_signal(BTreeSet::new()),
|
|
|
|
regulators: create_signal(BTreeSet::new()),
|
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
|
|
|
tangent: create_signal(ConfigSubspace::zero(0)),
|
2025-07-21 04:18:49 +00:00
|
|
|
elements_by_id: create_signal(BTreeMap::default()),
|
2025-07-31 22:21:32 +00:00
|
|
|
realization_trigger: create_signal(()),
|
2025-07-21 04:18:49 +00:00
|
|
|
realization_status: create_signal(Ok(())),
|
2025-08-04 23:34:33 +00:00
|
|
|
descent_history: create_signal(DescentHistory::new()),
|
2025-07-22 22:01:37 +00:00
|
|
|
};
|
|
|
|
|
2025-07-31 22:21:32 +00:00
|
|
|
// realize the assembly whenever the element list, the regulator list,
|
|
|
|
// a regulator's set point, or the realization trigger is updated
|
2025-07-22 22:01:37 +00:00
|
|
|
let assembly_for_effect = assembly.clone();
|
|
|
|
create_effect(move || {
|
2025-07-31 22:21:32 +00:00
|
|
|
assembly_for_effect.elements.track();
|
|
|
|
assembly_for_effect.regulators.with(
|
|
|
|
|regs| for reg in regs {
|
|
|
|
reg.set_point().track();
|
|
|
|
}
|
|
|
|
);
|
|
|
|
assembly_for_effect.realization_trigger.track();
|
|
|
|
assembly_for_effect.realize();
|
2025-07-22 22:01:37 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
assembly
|
2024-10-21 23:38:27 +00:00
|
|
|
}
|
|
|
|
|
2025-03-10 23:43:24 +00:00
|
|
|
// --- inserting elements and regulators ---
|
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
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
// insert an element into the assembly without checking whether we already
|
2024-10-21 23:38:27 +00:00
|
|
|
// 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
|
2025-05-06 19:17:30 +00:00
|
|
|
fn insert_element_unchecked(&self, elt: impl Element + 'static) {
|
2025-05-01 19:25:13 +00:00
|
|
|
// insert the element
|
|
|
|
let id = elt.id().clone();
|
2025-05-06 19:17:30 +00:00
|
|
|
let elt_rc = Rc::new(elt);
|
|
|
|
self.elements.update(|elts| elts.insert(elt_rc.clone()));
|
|
|
|
self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, elt_rc.clone()));
|
2025-04-21 23:40:42 +00:00
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
// create and insert the element's default regulators
|
2025-05-06 19:17:30 +00:00
|
|
|
for reg in elt_rc.default_regulators() {
|
2025-05-01 19:25:13 +00:00
|
|
|
self.insert_regulator(reg);
|
|
|
|
}
|
2024-10-21 23:38:27 +00:00
|
|
|
}
|
|
|
|
|
2025-05-06 19:17:30 +00:00
|
|
|
pub fn try_insert_element(&self, elt: impl Element + 'static) -> bool {
|
2024-10-21 23:38:27 +00:00
|
|
|
let can_insert = self.elements_by_id.with_untracked(
|
2025-05-01 19:25:13 +00:00
|
|
|
|elts_by_id| !elts_by_id.contains_key(elt.id())
|
2024-10-21 23:38:27 +00:00
|
|
|
);
|
|
|
|
if can_insert {
|
2025-05-06 19:17:30 +00:00
|
|
|
self.insert_element_unchecked(elt);
|
2024-10-21 23:38:27 +00:00
|
|
|
}
|
2025-05-06 19:17:30 +00:00
|
|
|
can_insert
|
2024-10-21 23:38:27 +00:00
|
|
|
}
|
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
pub fn insert_element_default<T: Element + 'static>(&self) {
|
2024-10-21 23:38:27 +00:00
|
|
|
// find the next unused identifier in the default sequence
|
2025-05-01 19:25:13 +00:00
|
|
|
let default_id = T::default_id();
|
2024-10-21 23:38:27 +00:00
|
|
|
let mut id_num = 1;
|
2025-05-01 19:25:13 +00:00
|
|
|
let mut id = format!("{default_id}{id_num}");
|
2024-10-21 23:38:27 +00:00
|
|
|
while self.elements_by_id.with_untracked(
|
|
|
|
|elts_by_id| elts_by_id.contains_key(&id)
|
|
|
|
) {
|
|
|
|
id_num += 1;
|
2025-05-01 19:25:13 +00:00
|
|
|
id = format!("{default_id}{id_num}");
|
2024-10-21 23:38:27 +00:00
|
|
|
}
|
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
// create and insert the default example of `T`
|
|
|
|
let _ = self.insert_element_unchecked(T::default(id, id_num));
|
2024-10-21 23:38:27 +00:00
|
|
|
}
|
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
pub fn insert_regulator(&self, regulator: Rc<dyn Regulator>) {
|
2025-04-21 23:40:42 +00:00
|
|
|
// add the regulator to the assembly's regulator list
|
2025-05-06 19:17:30 +00:00
|
|
|
self.regulators.update(
|
2025-05-01 19:25:13 +00:00
|
|
|
|regs| regs.insert(regulator.clone())
|
2025-03-10 23:43:24 +00:00
|
|
|
);
|
2025-04-21 23:40:42 +00:00
|
|
|
|
|
|
|
// add the regulator to each subject's regulator list
|
2025-05-06 19:17:30 +00:00
|
|
|
let subject_regulators: Vec<_> = regulator.subjects().into_iter().map(
|
|
|
|
|subj| subj.regulators()
|
|
|
|
).collect();
|
2025-04-21 23:40:42 +00:00
|
|
|
for regulators in subject_regulators {
|
2025-05-06 19:17:30 +00:00
|
|
|
regulators.update(|regs| regs.insert(regulator.clone()));
|
2025-04-21 23:40:42 +00:00
|
|
|
}
|
|
|
|
|
2025-03-10 23:43:24 +00:00
|
|
|
/* DEBUG */
|
|
|
|
// print an updated list of regulators
|
2025-06-04 21:01:12 +00:00
|
|
|
console_log!("Regulators:");
|
2025-04-21 23:40:42 +00:00
|
|
|
self.regulators.with_untracked(|regs| {
|
2025-05-06 19:17:30 +00:00
|
|
|
for reg in regs.into_iter() {
|
2025-06-04 21:01:12 +00:00
|
|
|
console_log!(
|
2025-04-21 23:40:42 +00:00
|
|
|
" {:?}: {}",
|
|
|
|
reg.subjects(),
|
|
|
|
reg.set_point().with_untracked(
|
|
|
|
|set_pt| {
|
|
|
|
let spec = &set_pt.spec;
|
|
|
|
if spec.is_empty() {
|
|
|
|
"__".to_string()
|
|
|
|
} else {
|
|
|
|
spec.clone()
|
|
|
|
}
|
|
|
|
}
|
2025-03-10 23:43:24 +00:00
|
|
|
)
|
2025-06-04 21:01:12 +00:00
|
|
|
);
|
2025-03-10 23:43:24 +00:00
|
|
|
}
|
|
|
|
});
|
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
|
|
|
}
|
|
|
|
|
|
|
|
// --- realization ---
|
|
|
|
|
|
|
|
pub fn realize(&self) {
|
|
|
|
// index the elements
|
|
|
|
self.elements.update_silent(|elts| {
|
2025-05-06 19:17:30 +00:00
|
|
|
for (index, elt) in elts.iter().enumerate() {
|
2025-05-01 19:25:13 +00:00
|
|
|
elt.set_column_index(index);
|
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
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2025-04-21 23:40:42 +00:00
|
|
|
// set up the constraint problem
|
|
|
|
let problem = self.elements.with_untracked(|elts| {
|
|
|
|
let mut problem = ConstraintProblem::new(elts.len());
|
2025-05-06 19:17:30 +00:00
|
|
|
for elt in elts {
|
|
|
|
elt.pose(&mut problem);
|
2025-04-21 23:40:42 +00:00
|
|
|
}
|
2025-03-10 23:43:24 +00:00
|
|
|
self.regulators.with_untracked(|regs| {
|
2025-05-06 19:17:30 +00:00
|
|
|
for reg in regs {
|
|
|
|
reg.pose(&mut problem);
|
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
|
|
|
}
|
|
|
|
});
|
2025-04-21 23:40:42 +00:00
|
|
|
problem
|
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
|
|
|
});
|
|
|
|
|
|
|
|
/* DEBUG */
|
|
|
|
// log the Gram matrix
|
2025-06-04 21:01:12 +00:00
|
|
|
console_log!("Gram matrix:\n{}", problem.gram);
|
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
|
|
|
|
|
|
|
/* DEBUG */
|
|
|
|
// log the initial configuration matrix
|
2025-06-04 21:01:12 +00:00
|
|
|
console_log!("Old configuration:{:>8.3}", problem.guess);
|
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
|
|
|
|
|
|
|
// look for a configuration with the given Gram matrix
|
2025-07-21 04:18:49 +00:00
|
|
|
let Realization { result, history } = realize_gram(
|
2025-04-21 23:40:42 +00:00
|
|
|
&problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
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
|
|
|
);
|
|
|
|
|
|
|
|
/* DEBUG */
|
2025-07-21 04:18:49 +00:00
|
|
|
// report the outcome of the search in the browser console
|
|
|
|
if let Err(ref message) = result {
|
|
|
|
console_log!("❌️ {message}");
|
2025-06-04 21:01:12 +00:00
|
|
|
} else {
|
2025-07-21 04:18:49 +00:00
|
|
|
console_log!("✅️ Target accuracy achieved!");
|
2025-06-04 21:01:12 +00:00
|
|
|
}
|
2025-07-31 22:21:32 +00:00
|
|
|
if history.scaled_loss.len() > 0 {
|
|
|
|
console_log!("Steps: {}", history.scaled_loss.len() - 1);
|
|
|
|
console_log!("Loss: {}", history.scaled_loss.last().unwrap());
|
|
|
|
}
|
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
|
|
|
|
2025-07-21 04:18:49 +00:00
|
|
|
// report the loss history
|
|
|
|
self.descent_history.set(history);
|
|
|
|
|
|
|
|
match result {
|
|
|
|
Ok(ConfigNeighborhood { config, nbhd: tangent }) => {
|
|
|
|
/* DEBUG */
|
|
|
|
// report the tangent dimension
|
|
|
|
console_log!("Tangent dimension: {}", tangent.dim());
|
|
|
|
|
|
|
|
// report the realization status
|
|
|
|
self.realization_status.set(Ok(()));
|
|
|
|
|
|
|
|
// read out the solution
|
|
|
|
for elt in self.elements.get_clone_untracked() {
|
|
|
|
elt.representation().update(
|
|
|
|
|rep| rep.set_column(0, &config.column(elt.column_index().unwrap()))
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// save the tangent space
|
|
|
|
self.tangent.set_silent(tangent);
|
|
|
|
},
|
|
|
|
Err(message) => {
|
|
|
|
// report the realization status. the `Err(message)` we're
|
|
|
|
// setting the status to has a different type than the
|
|
|
|
// `Err(message)` we received from the match: we're changing the
|
|
|
|
// `Ok` type from `Realization` to `()`
|
|
|
|
self.realization_status.set(Err(message))
|
2025-08-04 23:34:33 +00:00
|
|
|
},
|
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
|
|
|
}
|
2024-10-21 23:38:27 +00:00
|
|
|
}
|
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
|
|
|
|
|
|
|
// --- deformation ---
|
|
|
|
|
|
|
|
// project the given motion to the tangent space of the solution variety and
|
|
|
|
// move the assembly along it. the implementation is based on invariant (1)
|
|
|
|
// from above and the following additional invariant:
|
|
|
|
//
|
|
|
|
// (2) if an element is affected by a constraint, it has a column index
|
|
|
|
//
|
|
|
|
// we have this invariant because the assembly gets realized each time you
|
|
|
|
// add a constraint
|
|
|
|
pub fn deform(&self, motion: AssemblyMotion) {
|
|
|
|
/* KLUDGE */
|
|
|
|
// when the tangent space is zero, deformation won't do anything, but
|
|
|
|
// the attempt to deform should be registered in the UI. this console
|
|
|
|
// message will do for now
|
|
|
|
if self.tangent.with(|tan| tan.dim() <= 0 && tan.assembly_dim() > 0) {
|
|
|
|
console::log_1(&JsValue::from("The assembly is rigid"));
|
|
|
|
}
|
|
|
|
|
|
|
|
// give a column index to each moving element that doesn't have one yet.
|
|
|
|
// this temporarily breaks invariant (1), but the invariant will be
|
|
|
|
// restored when we realize the assembly at the end of the deformation.
|
|
|
|
// in the process, we find out how many matrix columns we'll need to
|
|
|
|
// hold the deformation
|
|
|
|
let realized_dim = self.tangent.with(|tan| tan.assembly_dim());
|
2025-05-06 19:17:30 +00:00
|
|
|
let motion_dim = {
|
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
|
|
|
let mut next_column_index = realized_dim;
|
|
|
|
for elt_motion in motion.iter() {
|
2025-05-06 19:17:30 +00:00
|
|
|
let moving_elt = &elt_motion.element;
|
2025-05-01 19:25:13 +00:00
|
|
|
if moving_elt.column_index().is_none() {
|
|
|
|
moving_elt.set_column_index(next_column_index);
|
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
|
|
|
next_column_index += 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
next_column_index
|
2025-05-06 19:17:30 +00:00
|
|
|
};
|
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
|
|
|
|
|
|
|
// project the element motions onto the tangent space of the solution
|
|
|
|
// variety and sum them to get a deformation of the whole assembly. the
|
|
|
|
// matrix `motion_proj` that holds the deformation has extra columns for
|
|
|
|
// any moving elements that aren't reflected in the saved tangent space
|
|
|
|
const ELEMENT_DIM: usize = 5;
|
|
|
|
let mut motion_proj = DMatrix::zeros(ELEMENT_DIM, motion_dim);
|
|
|
|
for elt_motion in motion {
|
|
|
|
// we can unwrap the column index because we know that every moving
|
|
|
|
// element has one at this point
|
2025-05-06 19:17:30 +00:00
|
|
|
let column_index = elt_motion.element.column_index().unwrap();
|
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
|
|
|
|
|
|
|
if column_index < realized_dim {
|
|
|
|
// this element had a column index when we started, so by
|
|
|
|
// invariant (1), it's reflected in the tangent space
|
|
|
|
let mut target_columns = motion_proj.columns_mut(0, realized_dim);
|
|
|
|
target_columns += self.tangent.with(
|
|
|
|
|tan| tan.proj(&elt_motion.velocity, column_index)
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
// this element didn't have a column index when we started, so
|
|
|
|
// by invariant (2), it's unconstrained
|
|
|
|
let mut target_column = motion_proj.column_mut(column_index);
|
2025-05-06 19:17:30 +00:00
|
|
|
let unif_to_std = elt_motion.element.representation().with_untracked(
|
|
|
|
|rep| local_unif_to_std(rep.as_view())
|
Switch to Euclidean-invariant projection onto tangent space of solution variety (#34)
This pull request addresses issues #32 and #33 by projecting nudges onto the tangent space of the solution variety using a Euclidean-invariant inner product, which I'm calling the *uniform* inner product.
### Definition of the uniform inner product
For spheres and planes, the uniform inner product is defined on the tangent space of the hyperboloid $\langle v, v \rangle = 1$. For points, it's defined on the tangent space of the paraboloid $\langle v, v \rangle = 0,\; \langle v, I_\infty \rangle = 1$.
The tangent space of an assembly can be expressed as the direct sum of the tangent spaces of the elements. We extend the uniform inner product to assemblies by declaring the tangent spaces of different elements to be orthogonal.
#### For spheres and planes
If $v = [x, y, z, b, c]^\top$ is on the hyperboloid $\langle v, v \rangle = 1$, the vectors
$$\left[ \begin{array}{c} 2b \\ \cdot \\ \cdot \\ \cdot \\ x \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ 2b \\ \cdot \\ \cdot \\ y \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ \cdot \\ 2b \\ \cdot \\ z \end{array} \right],\;\left[ \begin{array}{l} 2bx \\ 2by \\ 2bz \\ 2b^2 \\ 2bc + 1 \end{array} \right]$$
form a basis for the tangent space of hyperboloid at $v$. We declare this basis to be orthonormal with respect to the uniform inner product.
The first three vectors in the basis are unit-speed translations along the coordinate axes. The last vector moves the surface at unit speed along its normal field. For spheres, this increases the radius at unit rate. For planes, this translates the plane parallel to itself at unit speed. This description makes it clear that the uniform inner product is invariant under Euclidean motions.
#### For points
If $v = [x, y, z, b, c]^\top$ is on the paraboloid $\langle v, v \rangle = 0,\; \langle v, I_\infty \rangle = 1$, the vectors
$$\left[ \begin{array}{c} 2b \\ \cdot \\ \cdot \\ \cdot \\ x \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ 2b \\ \cdot \\ \cdot \\ y \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ \cdot \\ 2b \\ \cdot \\ z \end{array} \right]$$
form a basis for the tangent space of paraboloid at $v$. We declare this basis to be orthonormal with respect to the uniform inner product.
The meanings of the basis vectors, and the argument that the uniform inner product is Euclidean-invariant, are the same as for spheres and planes. In the engine, we pad the basis with $[0, 0, 0, 0, 1]^\top$ to keep the number of uniform coordinates consistent across element types.
### Confirmation of intended behavior
Two new tests confirm that we've corrected the misbehaviors described in issues #32 and #33.
Issue | Test
---|---
#32 | `proj_equivar_test`
#33 | `tangent_test_kaleidocycle`
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/34
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2025-01-31 19:34:33 +00:00
|
|
|
);
|
|
|
|
target_column += unif_to_std * elt_motion.velocity;
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-02-06 22:53:41 +00:00
|
|
|
// step the assembly along the deformation. this changes the elements'
|
|
|
|
// normalizations, so we restore those afterward
|
2025-05-06 19:17:30 +00:00
|
|
|
for elt in self.elements.get_clone_untracked() {
|
2025-05-01 19:25:13 +00:00
|
|
|
elt.representation().update_silent(|rep| {
|
|
|
|
match elt.column_index() {
|
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
|
|
|
Some(column_index) => {
|
2025-06-04 21:01:12 +00:00
|
|
|
// step the element along the deformation and then
|
|
|
|
// restore its normalization
|
2025-02-06 22:53:41 +00:00
|
|
|
*rep += motion_proj.column(column_index);
|
2025-06-04 21:01:12 +00:00
|
|
|
elt.project_to_normalized(rep);
|
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
|
|
|
},
|
|
|
|
None => {
|
2025-06-04 21:01:12 +00:00
|
|
|
console_log!("No velocity to unpack for fresh element \"{}\"", elt.id())
|
2025-08-04 23:34:33 +00:00
|
|
|
},
|
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
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2025-07-31 22:21:32 +00:00
|
|
|
// trigger a realization to bring the configuration back onto the
|
2025-07-22 22:01:37 +00:00
|
|
|
// solution variety. this also gets the elements' column indices and the
|
|
|
|
// saved tangent space back in sync
|
2025-07-31 22:21:32 +00:00
|
|
|
self.realization_trigger.set(());
|
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
|
|
|
}
|
2025-04-21 23:40:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
2025-06-04 21:01:12 +00:00
|
|
|
use crate::engine;
|
|
|
|
|
2025-04-21 23:40:42 +00:00
|
|
|
#[test]
|
2025-05-01 19:25:13 +00:00
|
|
|
#[should_panic(expected = "Sphere \"sphere\" should be indexed before writing problem data")]
|
2025-04-21 23:40:42 +00:00
|
|
|
fn unindexed_element_test() {
|
|
|
|
let _ = create_root(|| {
|
2025-05-01 19:25:13 +00:00
|
|
|
let elt = Sphere::default("sphere".to_string(), 0);
|
2025-05-06 19:17:30 +00:00
|
|
|
elt.pose(&mut ConstraintProblem::new(1));
|
2025-04-21 23:40:42 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
#[should_panic(expected = "Subjects should be indexed before inversive distance regulator writes problem data")]
|
|
|
|
fn unindexed_subject_test_inversive_distance() {
|
|
|
|
let _ = create_root(|| {
|
2025-05-06 19:17:30 +00:00
|
|
|
let subjects = [0, 1].map(
|
|
|
|
|k| Rc::new(Sphere::default(format!("sphere{k}"), k)) as Rc<dyn Element>
|
|
|
|
);
|
|
|
|
subjects[0].set_column_index(0);
|
2025-04-21 23:40:42 +00:00
|
|
|
InversiveDistanceRegulator {
|
|
|
|
subjects: subjects,
|
|
|
|
measurement: create_memo(|| 0.0),
|
2025-05-06 19:17:30 +00:00
|
|
|
set_point: create_signal(SpecifiedValue::try_from("0.0".to_string()).unwrap()),
|
|
|
|
serial: InversiveDistanceRegulator::next_serial()
|
|
|
|
}.pose(&mut ConstraintProblem::new(2));
|
2025-04-21 23:40:42 +00:00
|
|
|
});
|
|
|
|
}
|
2025-06-04 21:01:12 +00:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn curvature_drift_test() {
|
|
|
|
const INITIAL_RADIUS: f64 = 0.25;
|
|
|
|
let _ = create_root(|| {
|
|
|
|
// set up an assembly containing a single sphere centered at the
|
|
|
|
// origin
|
|
|
|
let assembly = Assembly::new();
|
|
|
|
let sphere_id = "sphere0";
|
|
|
|
let _ = assembly.try_insert_element(
|
|
|
|
// we create the sphere by hand for two reasons: to choose the
|
|
|
|
// curvature (which can affect drift rate) and to make the test
|
|
|
|
// independent of `Sphere::default`
|
|
|
|
Sphere::new(
|
|
|
|
String::from(sphere_id),
|
|
|
|
String::from("Sphere 0"),
|
|
|
|
[0.75_f32, 0.75_f32, 0.75_f32],
|
2025-08-04 23:34:33 +00:00
|
|
|
engine::sphere(0.0, 0.0, 0.0, INITIAL_RADIUS),
|
2025-06-04 21:01:12 +00:00
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
// nudge the sphere repeatedly along the `z` axis
|
|
|
|
const STEP_SIZE: f64 = 0.0025;
|
|
|
|
const STEP_CNT: usize = 400;
|
|
|
|
let sphere = assembly.elements_by_id.with(|elts_by_id| elts_by_id[sphere_id].clone());
|
|
|
|
let velocity = DVector::from_column_slice(&[0.0, 0.0, STEP_SIZE, 0.0]);
|
|
|
|
for _ in 0..STEP_CNT {
|
|
|
|
assembly.deform(
|
|
|
|
vec![
|
|
|
|
ElementMotion {
|
|
|
|
element: sphere.clone(),
|
2025-08-04 23:34:33 +00:00
|
|
|
velocity: velocity.as_view(),
|
2025-06-04 21:01:12 +00:00
|
|
|
}
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// check how much the sphere's curvature has drifted
|
|
|
|
const INITIAL_HALF_CURV: f64 = 0.5 / INITIAL_RADIUS;
|
|
|
|
const DRIFT_TOL: f64 = 0.015;
|
|
|
|
let final_half_curv = sphere.representation().with_untracked(
|
|
|
|
|rep| rep[Sphere::CURVATURE_COMPONENT]
|
|
|
|
);
|
|
|
|
assert!((final_half_curv / INITIAL_HALF_CURV - 1.0).abs() < DRIFT_TOL);
|
|
|
|
});
|
|
|
|
}
|
2024-10-21 23:38:27 +00:00
|
|
|
}
|