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 lazy_static::lazy_static;
|
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
|
|
|
use nalgebra::{Const, DMatrix, DVector, DVectorView, Dyn, SymmetricEigen};
|
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 */
|
|
|
|
|
|
|
|
// --- elements ---
|
|
|
|
|
2024-11-26 00:32:50 +00:00
|
|
|
#[cfg(feature = "dev")]
|
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 fn point(x: f64, y: f64, z: f64) -> DVector<f64> {
|
|
|
|
DVector::from_column_slice(&[x, y, z, 0.5, 0.5*(x*x + y*y + z*z)])
|
|
|
|
}
|
2024-10-21 23:38:27 +00:00
|
|
|
|
|
|
|
// the sphere with the given center and radius, with inward-pointing normals
|
|
|
|
pub fn sphere(center_x: f64, center_y: f64, center_z: f64, radius: f64) -> DVector<f64> {
|
|
|
|
let center_norm_sq = center_x * center_x + center_y * center_y + center_z * center_z;
|
|
|
|
DVector::from_column_slice(&[
|
|
|
|
center_x / radius,
|
|
|
|
center_y / radius,
|
|
|
|
center_z / radius,
|
|
|
|
0.5 / radius,
|
|
|
|
0.5 * (center_norm_sq / radius - radius)
|
|
|
|
])
|
|
|
|
}
|
|
|
|
|
|
|
|
// the sphere of curvature `curv` whose closest point to the origin has position
|
|
|
|
// `off * dir` and normal `dir`, where `dir` is a unit vector. setting the
|
|
|
|
// curvature to zero gives a plane
|
|
|
|
pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f64) -> DVector<f64> {
|
|
|
|
let norm_sp = 1.0 + off * curv;
|
|
|
|
DVector::from_column_slice(&[
|
|
|
|
norm_sp * dir_x,
|
|
|
|
norm_sp * dir_y,
|
|
|
|
norm_sp * dir_z,
|
|
|
|
0.5 * curv,
|
|
|
|
off * (1.0 + 0.5 * off * curv)
|
|
|
|
])
|
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
|
|
|
}
|
|
|
|
|
|
|
|
// --- partial matrices ---
|
|
|
|
|
|
|
|
struct MatrixEntry {
|
|
|
|
index: (usize, usize),
|
|
|
|
value: f64
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct PartialMatrix(Vec<MatrixEntry>);
|
|
|
|
|
|
|
|
impl PartialMatrix {
|
|
|
|
pub fn new() -> PartialMatrix {
|
|
|
|
PartialMatrix(Vec::<MatrixEntry>::new())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn push_sym(&mut self, row: usize, col: usize, value: f64) {
|
|
|
|
let PartialMatrix(entries) = self;
|
|
|
|
entries.push(MatrixEntry { index: (row, col), value: value });
|
|
|
|
if row != col {
|
|
|
|
entries.push(MatrixEntry { index: (col, row), value: value });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* DEBUG */
|
|
|
|
pub fn log_to_console(&self) {
|
|
|
|
let PartialMatrix(entries) = self;
|
|
|
|
for ent in entries {
|
|
|
|
let ent_str = format!(" {} {} {}", ent.index.0, ent.index.1, ent.value);
|
|
|
|
console::log_1(&JsValue::from(ent_str.as_str()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn proj(&self, a: &DMatrix<f64>) -> DMatrix<f64> {
|
|
|
|
let mut result = DMatrix::<f64>::zeros(a.nrows(), a.ncols());
|
|
|
|
let PartialMatrix(entries) = self;
|
|
|
|
for ent in entries {
|
|
|
|
result[ent.index] = a[ent.index];
|
|
|
|
}
|
|
|
|
result
|
|
|
|
}
|
|
|
|
|
|
|
|
fn sub_proj(&self, rhs: &DMatrix<f64>) -> DMatrix<f64> {
|
|
|
|
let mut result = DMatrix::<f64>::zeros(rhs.nrows(), rhs.ncols());
|
|
|
|
let PartialMatrix(entries) = self;
|
|
|
|
for ent in entries {
|
|
|
|
result[ent.index] = ent.value - rhs[ent.index];
|
|
|
|
}
|
|
|
|
result
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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
|
|
|
// --- configuration subspaces ---
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct ConfigSubspace {
|
|
|
|
assembly_dim: usize,
|
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
|
|
|
basis_std: Vec<DMatrix<f64>>,
|
|
|
|
basis_proj: Vec<DMatrix<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
|
|
|
}
|
|
|
|
|
|
|
|
impl ConfigSubspace {
|
|
|
|
pub fn zero(assembly_dim: usize) -> ConfigSubspace {
|
|
|
|
ConfigSubspace {
|
|
|
|
assembly_dim: assembly_dim,
|
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
|
|
|
basis_proj: Vec::new(),
|
|
|
|
basis_std: Vec::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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// approximate the kernel of a symmetric endomorphism of the configuration
|
|
|
|
// space for `assembly_dim` elements. we consider an eigenvector to be part
|
|
|
|
// of the kernel if its eigenvalue is smaller than the constant `THRESHOLD`
|
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
|
|
|
fn symmetric_kernel(a: DMatrix<f64>, proj_to_std: DMatrix<f64>, assembly_dim: usize) -> ConfigSubspace {
|
|
|
|
// find a basis for the kernel. the basis is expressed in the projection
|
|
|
|
// coordinates, and it's orthonormal with respect to the projection
|
|
|
|
// inner product
|
|
|
|
const THRESHOLD: f64 = 0.1;
|
|
|
|
let eig = SymmetricEigen::new(proj_to_std.tr_mul(&a) * &proj_to_std);
|
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 eig_vecs = eig.eigenvectors.column_iter();
|
|
|
|
let eig_pairs = eig.eigenvalues.iter().zip(eig_vecs);
|
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
|
|
|
let basis_proj = DMatrix::from_columns(
|
|
|
|
eig_pairs.filter_map(
|
|
|
|
|(λ, v)| (λ.abs() < THRESHOLD).then_some(v)
|
|
|
|
).collect::<Vec<_>>().as_slice()
|
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
|
|
|
);
|
|
|
|
|
|
|
|
/* DEBUG */
|
|
|
|
// print the eigenvalues
|
|
|
|
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
|
|
|
|
console::log_1(&JsValue::from(
|
|
|
|
format!("Eigenvalues used to find kernel:{}", eig.eigenvalues)
|
|
|
|
));
|
|
|
|
|
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
|
|
|
// express the basis in the standard coordinates
|
|
|
|
let basis_std = proj_to_std * &basis_proj;
|
|
|
|
|
|
|
|
const ELEMENT_DIM: usize = 5;
|
|
|
|
const UNIFORM_DIM: usize = 4;
|
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
|
|
|
ConfigSubspace {
|
|
|
|
assembly_dim: assembly_dim,
|
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
|
|
|
basis_std: basis_std.column_iter().map(
|
|
|
|
|v| Into::<DMatrix<f64>>::into(
|
|
|
|
v.reshape_generic(Dyn(ELEMENT_DIM), Dyn(assembly_dim))
|
|
|
|
)
|
|
|
|
).collect(),
|
|
|
|
basis_proj: basis_proj.column_iter().map(
|
|
|
|
|v| Into::<DMatrix<f64>>::into(
|
|
|
|
v.reshape_generic(Dyn(UNIFORM_DIM), Dyn(assembly_dim))
|
|
|
|
)
|
|
|
|
).collect()
|
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 fn dim(&self) -> usize {
|
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
|
|
|
self.basis_std.len()
|
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 fn assembly_dim(&self) -> usize {
|
|
|
|
self.assembly_dim
|
|
|
|
}
|
|
|
|
|
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
|
|
|
// find the projection onto this subspace of the motion where the element
|
|
|
|
// with the given column index has velocity `v`. the velocity is given in
|
|
|
|
// projection coordinates, and the projection is done with respect to the
|
|
|
|
// projection inner product
|
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 fn proj(&self, v: &DVectorView<f64>, column_index: usize) -> DMatrix<f64> {
|
|
|
|
if self.dim() == 0 {
|
|
|
|
const ELEMENT_DIM: usize = 5;
|
|
|
|
DMatrix::zeros(ELEMENT_DIM, self.assembly_dim)
|
|
|
|
} else {
|
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
|
|
|
self.basis_proj.iter().zip(self.basis_std.iter()).map(
|
|
|
|
|(b_proj, b_std)| b_proj.column(column_index).dot(&v) * b_std
|
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
|
|
|
).sum()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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
|
|
|
// --- descent history ---
|
|
|
|
|
|
|
|
pub struct DescentHistory {
|
|
|
|
pub config: Vec<DMatrix<f64>>,
|
|
|
|
pub scaled_loss: Vec<f64>,
|
|
|
|
pub neg_grad: Vec<DMatrix<f64>>,
|
|
|
|
pub min_eigval: Vec<f64>,
|
|
|
|
pub base_step: Vec<DMatrix<f64>>,
|
|
|
|
pub backoff_steps: Vec<i32>
|
|
|
|
}
|
|
|
|
|
|
|
|
impl DescentHistory {
|
|
|
|
fn new() -> DescentHistory {
|
|
|
|
DescentHistory {
|
|
|
|
config: Vec::<DMatrix<f64>>::new(),
|
|
|
|
scaled_loss: Vec::<f64>::new(),
|
|
|
|
neg_grad: Vec::<DMatrix<f64>>::new(),
|
|
|
|
min_eigval: Vec::<f64>::new(),
|
|
|
|
base_step: Vec::<DMatrix<f64>>::new(),
|
|
|
|
backoff_steps: Vec::<i32>::new(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// --- gram matrix realization ---
|
|
|
|
|
|
|
|
// the Lorentz form
|
|
|
|
lazy_static! {
|
2024-11-26 00:32:50 +00:00
|
|
|
pub static ref Q: DMatrix<f64> = DMatrix::from_row_slice(5, 5, &[
|
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
|
|
|
1.0, 0.0, 0.0, 0.0, 0.0,
|
|
|
|
0.0, 1.0, 0.0, 0.0, 0.0,
|
|
|
|
0.0, 0.0, 1.0, 0.0, 0.0,
|
|
|
|
0.0, 0.0, 0.0, 0.0, -2.0,
|
|
|
|
0.0, 0.0, 0.0, -2.0, 0.0
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
struct SearchState {
|
|
|
|
config: DMatrix<f64>,
|
|
|
|
err_proj: DMatrix<f64>,
|
|
|
|
loss: f64
|
|
|
|
}
|
|
|
|
|
|
|
|
impl SearchState {
|
|
|
|
fn from_config(gram: &PartialMatrix, config: DMatrix<f64>) -> SearchState {
|
|
|
|
let err_proj = gram.sub_proj(&(config.tr_mul(&*Q) * &config));
|
|
|
|
let loss = err_proj.norm_squared();
|
|
|
|
SearchState {
|
|
|
|
config: config,
|
|
|
|
err_proj: err_proj,
|
|
|
|
loss: loss
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn basis_matrix(index: (usize, usize), nrows: usize, ncols: usize) -> DMatrix<f64> {
|
|
|
|
let mut result = DMatrix::<f64>::zeros(nrows, ncols);
|
|
|
|
result[index] = 1.0;
|
|
|
|
result
|
|
|
|
}
|
|
|
|
|
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
|
|
|
// given a normalized vector `v` representing an element, build a basis for the
|
|
|
|
// element's linear configuration space consisting of:
|
|
|
|
// - the unit translation motions of the element
|
|
|
|
// - the unit shrinking motion of the element, if it's a sphere
|
|
|
|
// - one or two vectors whose coefficients vanish on the tangent space of the
|
|
|
|
// normalization variety
|
|
|
|
pub fn local_unif_to_std(v: DVectorView<f64>) -> DMatrix<f64> {
|
|
|
|
const ELEMENT_DIM: usize = 5;
|
|
|
|
const UNIFORM_DIM: usize = 4;
|
|
|
|
let curv = 2.0*v[3];
|
|
|
|
if v.dot(&(&*Q * v)) < 0.5 {
|
|
|
|
// `v` represents a point. the normalization condition says that the
|
|
|
|
// curvature component of `v` is 1/2
|
|
|
|
DMatrix::from_column_slice(ELEMENT_DIM, UNIFORM_DIM, &[
|
|
|
|
curv, 0.0, 0.0, 0.0, v[0],
|
|
|
|
0.0, curv, 0.0, 0.0, v[1],
|
|
|
|
0.0, 0.0, curv, 0.0, v[2],
|
|
|
|
0.0, 0.0, 0.0, 0.0, 1.0
|
|
|
|
])
|
|
|
|
} else {
|
|
|
|
// `v` represents a sphere. the normalization condition says that the
|
|
|
|
// Lorentz product of `v` with itself is 1
|
|
|
|
DMatrix::from_column_slice(ELEMENT_DIM, UNIFORM_DIM, &[
|
|
|
|
curv, 0.0, 0.0, 0.0, v[0],
|
|
|
|
0.0, curv, 0.0, 0.0, v[1],
|
|
|
|
0.0, 0.0, curv, 0.0, v[2],
|
|
|
|
curv*v[0], curv*v[1], curv*v[2], curv*v[3], curv*v[4] + 1.0
|
|
|
|
])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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 backtracking line search to find a better configuration
|
|
|
|
fn seek_better_config(
|
|
|
|
gram: &PartialMatrix,
|
|
|
|
state: &SearchState,
|
|
|
|
base_step: &DMatrix<f64>,
|
|
|
|
base_target_improvement: f64,
|
|
|
|
min_efficiency: f64,
|
|
|
|
backoff: f64,
|
|
|
|
max_backoff_steps: i32
|
|
|
|
) -> Option<(SearchState, i32)> {
|
|
|
|
let mut rate = 1.0;
|
|
|
|
for backoff_steps in 0..max_backoff_steps {
|
|
|
|
let trial_config = &state.config + rate * base_step;
|
|
|
|
let trial_state = SearchState::from_config(gram, trial_config);
|
|
|
|
let improvement = state.loss - trial_state.loss;
|
|
|
|
if improvement >= min_efficiency * rate * base_target_improvement {
|
|
|
|
return Some((trial_state, backoff_steps));
|
|
|
|
}
|
|
|
|
rate *= backoff;
|
|
|
|
}
|
|
|
|
None
|
|
|
|
}
|
|
|
|
|
|
|
|
// seek a matrix `config` for which `config' * Q * config` matches the partial
|
|
|
|
// matrix `gram`. use gradient descent starting from `guess`
|
|
|
|
pub fn realize_gram(
|
|
|
|
gram: &PartialMatrix,
|
|
|
|
guess: DMatrix<f64>,
|
|
|
|
frozen: &[(usize, usize)],
|
|
|
|
scaled_tol: f64,
|
|
|
|
min_efficiency: f64,
|
|
|
|
backoff: f64,
|
|
|
|
reg_scale: f64,
|
|
|
|
max_descent_steps: i32,
|
|
|
|
max_backoff_steps: i32
|
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
|
|
|
) -> (DMatrix<f64>, ConfigSubspace, bool, DescentHistory) {
|
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
|
|
|
// start the descent history
|
|
|
|
let mut history = DescentHistory::new();
|
|
|
|
|
|
|
|
// find the dimension of the search space
|
|
|
|
let element_dim = guess.nrows();
|
|
|
|
let assembly_dim = guess.ncols();
|
|
|
|
let total_dim = element_dim * assembly_dim;
|
|
|
|
|
|
|
|
// scale the tolerance
|
|
|
|
let scale_adjustment = (gram.0.len() as f64).sqrt();
|
|
|
|
let tol = scale_adjustment * scaled_tol;
|
|
|
|
|
|
|
|
// convert the frozen indices to stacked format
|
|
|
|
let frozen_stacked: Vec<usize> = frozen.into_iter().map(
|
|
|
|
|index| index.1*element_dim + index.0
|
|
|
|
).collect();
|
|
|
|
|
|
|
|
// use Newton's method with backtracking and gradient descent backup
|
|
|
|
let mut state = SearchState::from_config(gram, guess);
|
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 hess = DMatrix::zeros(element_dim, assembly_dim);
|
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
|
|
|
for _ in 0..max_descent_steps {
|
|
|
|
// find the negative gradient of the loss function
|
|
|
|
let neg_grad = 4.0 * &*Q * &state.config * &state.err_proj;
|
|
|
|
let mut neg_grad_stacked = neg_grad.clone().reshape_generic(Dyn(total_dim), Const::<1>);
|
|
|
|
history.neg_grad.push(neg_grad.clone());
|
|
|
|
|
|
|
|
// find the negative Hessian of the loss function
|
|
|
|
let mut hess_cols = Vec::<DVector<f64>>::with_capacity(total_dim);
|
|
|
|
for col in 0..assembly_dim {
|
|
|
|
for row in 0..element_dim {
|
|
|
|
let index = (row, col);
|
|
|
|
let basis_mat = basis_matrix(index, element_dim, assembly_dim);
|
|
|
|
let neg_d_err =
|
|
|
|
basis_mat.tr_mul(&*Q) * &state.config
|
|
|
|
+ state.config.tr_mul(&*Q) * &basis_mat;
|
|
|
|
let neg_d_err_proj = gram.proj(&neg_d_err);
|
|
|
|
let deriv_grad = 4.0 * &*Q * (
|
|
|
|
-&basis_mat * &state.err_proj
|
|
|
|
+ &state.config * &neg_d_err_proj
|
|
|
|
);
|
|
|
|
hess_cols.push(deriv_grad.reshape_generic(Dyn(total_dim), Const::<1>));
|
|
|
|
}
|
|
|
|
}
|
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
|
|
|
hess = DMatrix::from_columns(hess_cols.as_slice());
|
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
|
|
|
|
|
|
|
// regularize the Hessian
|
|
|
|
let min_eigval = hess.symmetric_eigenvalues().min();
|
|
|
|
if min_eigval <= 0.0 {
|
|
|
|
hess -= reg_scale * min_eigval * DMatrix::identity(total_dim, total_dim);
|
|
|
|
}
|
|
|
|
history.min_eigval.push(min_eigval);
|
|
|
|
|
|
|
|
// project the negative gradient and negative Hessian onto the
|
|
|
|
// orthogonal complement of the frozen subspace
|
|
|
|
let zero_col = DVector::zeros(total_dim);
|
|
|
|
let zero_row = zero_col.transpose();
|
|
|
|
for &k in &frozen_stacked {
|
|
|
|
neg_grad_stacked[k] = 0.0;
|
|
|
|
hess.set_row(k, &zero_row);
|
|
|
|
hess.set_column(k, &zero_col);
|
|
|
|
hess[(k, k)] = 1.0;
|
|
|
|
}
|
|
|
|
|
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
|
|
|
// stop if the loss is tolerably low
|
|
|
|
history.config.push(state.config.clone());
|
|
|
|
history.scaled_loss.push(state.loss / scale_adjustment);
|
|
|
|
if state.loss < tol { break; }
|
|
|
|
|
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
|
|
|
// compute the Newton step
|
|
|
|
/*
|
|
|
|
we need to either handle or eliminate the case where the minimum
|
|
|
|
eigenvalue of the Hessian is zero, so the regularized Hessian is
|
|
|
|
singular. right now, this causes the Cholesky decomposition to return
|
|
|
|
`None`, leading to a panic when we unrap
|
|
|
|
*/
|
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 base_step_stacked = hess.clone().cholesky().unwrap().solve(&neg_grad_stacked);
|
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
|
|
|
let base_step = base_step_stacked.reshape_generic(Dyn(element_dim), Dyn(assembly_dim));
|
|
|
|
history.base_step.push(base_step.clone());
|
|
|
|
|
|
|
|
// use backtracking line search to find a better configuration
|
|
|
|
match seek_better_config(
|
|
|
|
gram, &state, &base_step, neg_grad.dot(&base_step),
|
|
|
|
min_efficiency, backoff, max_backoff_steps
|
|
|
|
) {
|
|
|
|
Some((better_state, backoff_steps)) => {
|
|
|
|
state = better_state;
|
|
|
|
history.backoff_steps.push(backoff_steps);
|
|
|
|
},
|
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 => return (state.config, ConfigSubspace::zero(assembly_dim), false, history)
|
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
|
|
|
};
|
|
|
|
}
|
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 success = state.loss < tol;
|
|
|
|
let tangent = if success {
|
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
|
|
|
// express the uniform basis in the standard basis
|
|
|
|
const UNIFORM_DIM: usize = 4;
|
|
|
|
let total_dim_unif = UNIFORM_DIM * assembly_dim;
|
|
|
|
let mut unif_to_std = DMatrix::<f64>::zeros(total_dim, total_dim_unif);
|
|
|
|
for n in 0..assembly_dim {
|
|
|
|
let block_start = (element_dim * n, UNIFORM_DIM * n);
|
|
|
|
unif_to_std
|
|
|
|
.view_mut(block_start, (element_dim, UNIFORM_DIM))
|
|
|
|
.copy_from(&local_unif_to_std(state.config.column(n)));
|
|
|
|
}
|
|
|
|
|
|
|
|
// find the kernel of the Hessian. give it the uniform inner product
|
|
|
|
ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim)
|
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
|
|
|
} else {
|
|
|
|
ConfigSubspace::zero(assembly_dim)
|
|
|
|
};
|
|
|
|
(state.config, tangent, success, history)
|
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
|
|
|
}
|
|
|
|
|
|
|
|
// --- tests ---
|
|
|
|
|
2024-11-26 00:32:50 +00:00
|
|
|
// this problem is from a sangaku by Irisawa Shintarō Hiroatsu. the article
|
|
|
|
// below includes a nice translation of the problem statement, which was
|
|
|
|
// recorded in Uchida Itsumi's book _Kokon sankan_ (_Mathematics, Past and
|
|
|
|
// Present_)
|
|
|
|
//
|
|
|
|
// "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki
|
|
|
|
// https://www.nippon.com/en/japan-topics/c12801/
|
|
|
|
//
|
|
|
|
#[cfg(feature = "dev")]
|
|
|
|
pub mod irisawa {
|
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 std::{array, f64::consts::PI};
|
|
|
|
|
|
|
|
use super::*;
|
|
|
|
|
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 fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix<f64>, ConfigSubspace, bool, DescentHistory) {
|
2024-11-26 00:32:50 +00:00
|
|
|
let gram = {
|
|
|
|
let mut gram_to_be = PartialMatrix::new();
|
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
|
|
|
for s in 0..9 {
|
|
|
|
// each sphere is represented by a spacelike vector
|
2024-11-26 00:32:50 +00:00
|
|
|
gram_to_be.push_sym(s, s, 1.0);
|
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
|
|
|
|
|
|
|
// the circumscribing sphere is tangent to all of the other
|
|
|
|
// spheres, with matching orientation
|
|
|
|
if s > 0 {
|
2024-11-26 00:32:50 +00:00
|
|
|
gram_to_be.push_sym(0, s, 1.0);
|
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
|
|
|
}
|
|
|
|
|
|
|
|
if s > 2 {
|
|
|
|
// each chain sphere is tangent to the "sun" and "moon"
|
|
|
|
// spheres, with opposing orientation
|
|
|
|
for n in 1..3 {
|
2024-11-26 00:32:50 +00:00
|
|
|
gram_to_be.push_sym(s, n, -1.0);
|
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
|
|
|
}
|
|
|
|
|
|
|
|
// each chain sphere is tangent to the next chain sphere,
|
|
|
|
// with opposing orientation
|
|
|
|
let s_next = 3 + (s-2) % 6;
|
2024-11-26 00:32:50 +00:00
|
|
|
gram_to_be.push_sym(s, s_next, -1.0);
|
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-11-26 00:32:50 +00:00
|
|
|
gram_to_be
|
|
|
|
};
|
|
|
|
|
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
|
|
|
let guess = DMatrix::from_columns(
|
|
|
|
[
|
|
|
|
sphere(0.0, 0.0, 0.0, 15.0),
|
|
|
|
sphere(0.0, 0.0, -9.0, 5.0),
|
|
|
|
sphere(0.0, 0.0, 11.0, 3.0)
|
|
|
|
].into_iter().chain(
|
|
|
|
(1..=6).map(
|
|
|
|
|k| {
|
|
|
|
let ang = (k as f64) * PI/3.0;
|
|
|
|
sphere(9.0 * ang.cos(), 9.0 * ang.sin(), 0.0, 2.5)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
).collect::<Vec<_>>().as_slice()
|
|
|
|
);
|
2024-11-26 00:32:50 +00:00
|
|
|
|
|
|
|
// the frozen entries fix the radii of the circumscribing sphere, the
|
|
|
|
// "sun" and "moon" spheres, and one of the chain spheres
|
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
|
|
|
let frozen: [(usize, usize); 4] = array::from_fn(|k| (3, k));
|
2024-11-26 00:32:50 +00:00
|
|
|
|
|
|
|
realize_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
|
|
|
&gram, guess, &frozen,
|
2024-11-26 00:32:50 +00:00
|
|
|
scaled_tol, 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
|
|
|
}
|
2024-11-26 00:32:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
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
|
|
|
use nalgebra::Vector3;
|
|
|
|
use std::{array, f64::consts::{FRAC_1_SQRT_2, PI}, iter};
|
|
|
|
|
2024-11-26 00:32:50 +00:00
|
|
|
use super::{*, irisawa::realize_irisawa_hexlet};
|
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-11-26 00:32:50 +00:00
|
|
|
#[test]
|
|
|
|
fn sub_proj_test() {
|
|
|
|
let target = PartialMatrix(vec![
|
|
|
|
MatrixEntry { index: (0, 0), value: 19.0 },
|
|
|
|
MatrixEntry { index: (0, 2), value: 39.0 },
|
|
|
|
MatrixEntry { index: (1, 1), value: 59.0 },
|
|
|
|
MatrixEntry { index: (1, 2), value: 69.0 }
|
|
|
|
]);
|
|
|
|
let attempt = DMatrix::<f64>::from_row_slice(2, 3, &[
|
|
|
|
1.0, 2.0, 3.0,
|
|
|
|
4.0, 5.0, 6.0
|
|
|
|
]);
|
|
|
|
let expected_result = DMatrix::<f64>::from_row_slice(2, 3, &[
|
|
|
|
18.0, 0.0, 36.0,
|
|
|
|
0.0, 54.0, 63.0
|
|
|
|
]);
|
|
|
|
assert_eq!(target.sub_proj(&attempt), expected_result);
|
|
|
|
}
|
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
|
|
|
|
|
|
|
#[test]
|
2024-11-26 00:32:50 +00:00
|
|
|
fn zero_loss_test() {
|
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
|
|
|
let gram = PartialMatrix({
|
|
|
|
let mut entries = Vec::<MatrixEntry>::new();
|
|
|
|
for j in 0..3 {
|
|
|
|
for k in 0..3 {
|
|
|
|
entries.push(MatrixEntry {
|
|
|
|
index: (j, k),
|
|
|
|
value: if j == k { 1.0 } else { -1.0 }
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
entries
|
|
|
|
});
|
2024-11-26 00:32:50 +00:00
|
|
|
let config = {
|
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
|
|
|
let a: f64 = 0.75_f64.sqrt();
|
|
|
|
DMatrix::from_columns(&[
|
2024-11-26 00:32:50 +00:00
|
|
|
sphere(1.0, 0.0, 0.0, a),
|
|
|
|
sphere(-0.5, a, 0.0, a),
|
|
|
|
sphere(-0.5, -a, 0.0, a)
|
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-11-26 00:32:50 +00:00
|
|
|
let state = SearchState::from_config(&gram, config);
|
|
|
|
assert!(state.loss.abs() < f64::EPSILON);
|
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
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2024-11-26 00:32:50 +00:00
|
|
|
fn irisawa_hexlet_test() {
|
|
|
|
// solve Irisawa's problem
|
|
|
|
const SCALED_TOL: f64 = 1.0e-12;
|
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 (config, _, _, _) = realize_irisawa_hexlet(SCALED_TOL);
|
2024-11-26 00:32:50 +00:00
|
|
|
|
|
|
|
// check against Irisawa's solution
|
|
|
|
let entry_tol = SCALED_TOL.sqrt();
|
|
|
|
let solution_diams = [30.0, 10.0, 6.0, 5.0, 15.0, 10.0, 3.75, 2.5, 2.0 + 8.0/11.0];
|
|
|
|
for (k, diam) in solution_diams.into_iter().enumerate() {
|
|
|
|
assert!((config[(3, k)] - 1.0 / diam).abs() < entry_tol);
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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
|
|
|
#[test]
|
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
|
|
|
fn tangent_test_three_spheres() {
|
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
|
|
|
const SCALED_TOL: f64 = 1.0e-12;
|
|
|
|
let gram = {
|
|
|
|
let mut gram_to_be = PartialMatrix::new();
|
|
|
|
for j in 0..3 {
|
|
|
|
for k in j..3 {
|
|
|
|
gram_to_be.push_sym(j, k, if j == k { 1.0 } else { -1.0 });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
gram_to_be
|
|
|
|
};
|
|
|
|
let guess = DMatrix::from_columns(&[
|
|
|
|
sphere(0.0, 0.0, 0.0, -2.0),
|
|
|
|
sphere(0.0, 0.0, 1.0, 1.0),
|
|
|
|
sphere(0.0, 0.0, -1.0, 1.0)
|
|
|
|
]);
|
|
|
|
let frozen: [_; 5] = std::array::from_fn(|k| (k, 0));
|
|
|
|
let (config, tangent, success, history) = realize_gram(
|
|
|
|
&gram, guess.clone(), &frozen,
|
|
|
|
SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
|
|
|
|
);
|
|
|
|
assert_eq!(config, guess);
|
|
|
|
assert_eq!(success, true);
|
|
|
|
assert_eq!(history.scaled_loss.len(), 1);
|
|
|
|
|
|
|
|
// confirm that the tangent space has dimension five or less
|
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
|
|
|
assert_eq!(tangent.basis_std.len(), 5);
|
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
|
|
|
|
|
|
|
// confirm that the tangent space contains all the motions we expect it
|
|
|
|
// to. since we've already bounded the dimension of the tangent space,
|
|
|
|
// this confirms that the tangent space is what we expect it to be
|
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
|
|
|
const UNIFORM_DIM: usize = 4;
|
|
|
|
let element_dim = guess.nrows();
|
|
|
|
let assembly_dim = guess.ncols();
|
|
|
|
let tangent_motions_unif = vec![
|
|
|
|
basis_matrix((0, 1), UNIFORM_DIM, assembly_dim),
|
|
|
|
basis_matrix((1, 1), UNIFORM_DIM, assembly_dim),
|
|
|
|
basis_matrix((0, 2), UNIFORM_DIM, assembly_dim),
|
|
|
|
basis_matrix((1, 2), UNIFORM_DIM, assembly_dim),
|
|
|
|
DMatrix::<f64>::from_column_slice(UNIFORM_DIM, assembly_dim, &[
|
|
|
|
0.0, 0.0, 0.0, 0.0,
|
|
|
|
0.0, 0.0, -0.5, -0.5,
|
|
|
|
0.0, 0.0, -0.5, 0.5
|
|
|
|
])
|
|
|
|
];
|
|
|
|
let tangent_motions_std = vec![
|
|
|
|
basis_matrix((0, 1), element_dim, assembly_dim),
|
|
|
|
basis_matrix((1, 1), element_dim, assembly_dim),
|
|
|
|
basis_matrix((0, 2), element_dim, assembly_dim),
|
|
|
|
basis_matrix((1, 2), element_dim, assembly_dim),
|
|
|
|
DMatrix::<f64>::from_column_slice(element_dim, assembly_dim, &[
|
|
|
|
0.0, 0.0, 0.0, 0.00, 0.0,
|
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
|
|
|
0.0, 0.0, -1.0, -0.25, -1.0,
|
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
|
|
|
0.0, 0.0, -1.0, 0.25, 1.0
|
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
|
|
|
])
|
|
|
|
];
|
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
|
|
|
let tol_sq = ((element_dim * assembly_dim) as f64) * SCALED_TOL * SCALED_TOL;
|
|
|
|
for (motion_unif, motion_std) in tangent_motions_unif.into_iter().zip(tangent_motions_std) {
|
|
|
|
let motion_proj: DMatrix<_> = motion_unif.column_iter().enumerate().map(
|
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
|
|
|
|(k, v)| tangent.proj(&v, k)
|
|
|
|
).sum();
|
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
|
|
|
assert!((motion_std - motion_proj).norm_squared() < tol_sq);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn translation_motion_unif(vel: &Vector3<f64>, assembly_dim: usize) -> Vec<DVector<f64>> {
|
|
|
|
let mut elt_motion = DVector::zeros(4);
|
|
|
|
elt_motion.fixed_rows_mut::<3>(0).copy_from(vel);
|
|
|
|
iter::repeat(elt_motion).take(assembly_dim).collect()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn rotation_motion_unif(ang_vel: &Vector3<f64>, points: Vec<DVectorView<f64>>) -> Vec<DVector<f64>> {
|
|
|
|
points.into_iter().map(
|
|
|
|
|pt| {
|
|
|
|
let vel = ang_vel.cross(&pt.fixed_rows::<3>(0));
|
|
|
|
let mut elt_motion = DVector::zeros(4);
|
|
|
|
elt_motion.fixed_rows_mut::<3>(0).copy_from(&vel);
|
|
|
|
elt_motion
|
|
|
|
}
|
|
|
|
).collect()
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn tangent_test_kaleidocycle() {
|
|
|
|
// set up a kaleidocycle, made of points with fixed distances between
|
|
|
|
// them, and find its tangent space
|
|
|
|
const N_POINTS: usize = 12;
|
|
|
|
const N_HINGES: usize = 6;
|
|
|
|
const SCALED_TOL: f64 = 1.0e-12;
|
|
|
|
let gram = {
|
|
|
|
let mut gram_to_be = PartialMatrix::new();
|
|
|
|
for block in (0..N_POINTS).step_by(2) {
|
|
|
|
let block_next = (block + 2) % N_POINTS;
|
|
|
|
for j in 0..2 {
|
|
|
|
// diagonal and hinge edges
|
|
|
|
for k in j..2 {
|
|
|
|
gram_to_be.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 });
|
|
|
|
}
|
|
|
|
|
|
|
|
// non-hinge edges
|
|
|
|
for k in 0..2 {
|
|
|
|
gram_to_be.push_sym(block + j, block_next + k, -0.625);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
gram_to_be
|
|
|
|
};
|
|
|
|
let guess = {
|
|
|
|
let guess_elts = (0..N_HINGES).step_by(2).flat_map(
|
|
|
|
|n| {
|
|
|
|
let ang_hor = (n as f64) * PI/3.0;
|
|
|
|
let ang_vert = ((n + 1) as f64) * PI/3.0;
|
|
|
|
let x_vert = ang_vert.cos();
|
|
|
|
let y_vert = ang_vert.sin();
|
|
|
|
[
|
|
|
|
point(0.0, 0.0, 0.0),
|
|
|
|
point(ang_hor.cos(), ang_hor.sin(), 0.0),
|
|
|
|
point(x_vert, y_vert, -0.5),
|
|
|
|
point(x_vert, y_vert, 0.5)
|
|
|
|
]
|
|
|
|
}
|
|
|
|
).collect::<Vec<_>>();
|
|
|
|
DMatrix::from_columns(&guess_elts)
|
|
|
|
};
|
|
|
|
let frozen: [_; N_POINTS] = array::from_fn(|k| (3, k));
|
|
|
|
let (config, tangent, success, history) = realize_gram(
|
|
|
|
&gram, guess.clone(), &frozen,
|
|
|
|
SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
|
|
|
|
);
|
|
|
|
assert_eq!(config, guess);
|
|
|
|
assert_eq!(success, true);
|
|
|
|
assert_eq!(history.scaled_loss.len(), 1);
|
|
|
|
|
|
|
|
// list some motions that should form a basis for the tangent space of
|
|
|
|
// the solution variety
|
|
|
|
let element_dim = guess.nrows();
|
|
|
|
let assembly_dim = guess.ncols();
|
|
|
|
let tangent_motions_unif = vec![
|
|
|
|
// the translations along the coordinate axes
|
|
|
|
translation_motion_unif(&Vector3::new(1.0, 0.0, 0.0), assembly_dim),
|
|
|
|
translation_motion_unif(&Vector3::new(0.0, 1.0, 0.0), assembly_dim),
|
|
|
|
translation_motion_unif(&Vector3::new(0.0, 0.0, 1.0), assembly_dim),
|
|
|
|
|
|
|
|
// the rotations about the coordinate axes
|
|
|
|
rotation_motion_unif(&Vector3::new(1.0, 0.0, 0.0), guess.column_iter().collect()),
|
|
|
|
rotation_motion_unif(&Vector3::new(0.0, 1.0, 0.0), guess.column_iter().collect()),
|
|
|
|
rotation_motion_unif(&Vector3::new(0.0, 0.0, 1.0), guess.column_iter().collect()),
|
|
|
|
|
|
|
|
// the twist motion. more precisely: a motion that keeps the center
|
|
|
|
// of mass stationary and preserves the distances between the
|
|
|
|
// vertices to first order. this has to be the twist as long as:
|
|
|
|
// - twisting is the kaleidocycle's only internal degree of
|
|
|
|
// freedom
|
|
|
|
// - every first-order motion of the kaleidocycle comes from an
|
|
|
|
// actual motion
|
|
|
|
(0..N_HINGES).step_by(2).flat_map(
|
|
|
|
|n| {
|
|
|
|
let ang_vert = ((n + 1) as f64) * PI/3.0;
|
|
|
|
let vel_vert_x = 4.0 * ang_vert.cos();
|
|
|
|
let vel_vert_y = 4.0 * ang_vert.sin();
|
|
|
|
[
|
|
|
|
DVector::from_column_slice(&[0.0, 0.0, 5.0, 0.0]),
|
|
|
|
DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]),
|
|
|
|
DVector::from_column_slice(&[-vel_vert_x, -vel_vert_y, -3.0, 0.0]),
|
|
|
|
DVector::from_column_slice(&[vel_vert_x, vel_vert_y, -3.0, 0.0])
|
|
|
|
]
|
|
|
|
}
|
|
|
|
).collect::<Vec<_>>()
|
|
|
|
];
|
|
|
|
let tangent_motions_std = tangent_motions_unif.iter().map(
|
|
|
|
|motion| DMatrix::from_columns(
|
|
|
|
&guess.column_iter().zip(motion).map(
|
|
|
|
|(v, elt_motion)| local_unif_to_std(v) * elt_motion
|
|
|
|
).collect::<Vec<_>>()
|
|
|
|
)
|
|
|
|
).collect::<Vec<_>>();
|
|
|
|
|
|
|
|
// confirm that the dimension of the tangent space is no greater than
|
|
|
|
// expected
|
|
|
|
assert_eq!(tangent.basis_std.len(), tangent_motions_unif.len());
|
|
|
|
|
|
|
|
// confirm that the tangent space contains all the motions we expect it
|
|
|
|
// to. since we've already bounded the dimension of the tangent space,
|
|
|
|
// this confirms that the tangent space is what we expect it to be
|
|
|
|
let tol_sq = ((element_dim * assembly_dim) as f64) * SCALED_TOL * SCALED_TOL;
|
|
|
|
for (motion_unif, motion_std) in tangent_motions_unif.into_iter().zip(tangent_motions_std) {
|
|
|
|
let motion_proj: DMatrix<_> = motion_unif.into_iter().enumerate().map(
|
|
|
|
|(k, v)| tangent.proj(&v.as_view(), k)
|
|
|
|
).sum();
|
|
|
|
assert!((motion_std - motion_proj).norm_squared() < tol_sq);
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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
|
|
|
fn translation(dis: Vector3<f64>) -> DMatrix<f64> {
|
|
|
|
const ELEMENT_DIM: usize = 5;
|
|
|
|
DMatrix::from_column_slice(ELEMENT_DIM, ELEMENT_DIM, &[
|
|
|
|
1.0, 0.0, 0.0, 0.0, dis[0],
|
|
|
|
0.0, 1.0, 0.0, 0.0, dis[1],
|
|
|
|
0.0, 0.0, 1.0, 0.0, dis[2],
|
|
|
|
2.0*dis[0], 2.0*dis[1], 2.0*dis[2], 1.0, dis.norm_squared(),
|
|
|
|
0.0, 0.0, 0.0, 0.0, 1.0
|
|
|
|
])
|
|
|
|
}
|
|
|
|
|
|
|
|
// confirm that projection onto a configuration subspace is equivariant with
|
|
|
|
// respect to Euclidean motions
|
|
|
|
#[test]
|
|
|
|
fn proj_equivar_test() {
|
|
|
|
// find a pair of spheres that meet at 120°
|
|
|
|
const SCALED_TOL: f64 = 1.0e-12;
|
|
|
|
let gram = {
|
|
|
|
let mut gram_to_be = PartialMatrix::new();
|
|
|
|
gram_to_be.push_sym(0, 0, 1.0);
|
|
|
|
gram_to_be.push_sym(1, 1, 1.0);
|
|
|
|
gram_to_be.push_sym(0, 1, 0.5);
|
|
|
|
gram_to_be
|
|
|
|
};
|
|
|
|
let guess_orig = DMatrix::from_columns(&[
|
|
|
|
sphere(0.0, 0.0, 0.5, 1.0),
|
|
|
|
sphere(0.0, 0.0, -0.5, 1.0)
|
|
|
|
]);
|
|
|
|
let (config_orig, tangent_orig, success_orig, history_orig) = realize_gram(
|
|
|
|
&gram, guess_orig.clone(), &[],
|
|
|
|
SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
|
|
|
|
);
|
|
|
|
assert_eq!(config_orig, guess_orig);
|
|
|
|
assert_eq!(success_orig, true);
|
|
|
|
assert_eq!(history_orig.scaled_loss.len(), 1);
|
|
|
|
|
|
|
|
// find another pair of spheres that meet at 120°. we'll think of this
|
|
|
|
// solution as a transformed version of the original one
|
|
|
|
let guess_tfm = {
|
|
|
|
let a = 0.5 * FRAC_1_SQRT_2;
|
|
|
|
DMatrix::from_columns(&[
|
|
|
|
sphere(a, 0.0, 7.0 + a, 1.0),
|
|
|
|
sphere(-a, 0.0, 7.0 - a, 1.0)
|
|
|
|
])
|
|
|
|
};
|
|
|
|
let (config_tfm, tangent_tfm, success_tfm, history_tfm) = realize_gram(
|
|
|
|
&gram, guess_tfm.clone(), &[],
|
|
|
|
SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
|
|
|
|
);
|
|
|
|
assert_eq!(config_tfm, guess_tfm);
|
|
|
|
assert_eq!(success_tfm, true);
|
|
|
|
assert_eq!(history_tfm.scaled_loss.len(), 1);
|
|
|
|
|
|
|
|
// project a nudge to the tangent space of the solution variety at the
|
|
|
|
// original solution
|
|
|
|
let motion_orig = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]);
|
|
|
|
let motion_orig_proj = tangent_orig.proj(&motion_orig.as_view(), 0);
|
|
|
|
|
|
|
|
// project the equivalent nudge to the tangent space of the solution
|
|
|
|
// variety at the transformed solution
|
|
|
|
let motion_tfm = DVector::from_column_slice(&[FRAC_1_SQRT_2, 0.0, FRAC_1_SQRT_2, 0.0]);
|
|
|
|
let motion_tfm_proj = tangent_tfm.proj(&motion_tfm.as_view(), 0);
|
|
|
|
|
|
|
|
// take the transformation that sends the original solution to the
|
|
|
|
// transformed solution and apply it to the motion that the original
|
|
|
|
// solution makes in response to the nudge
|
|
|
|
const ELEMENT_DIM: usize = 5;
|
|
|
|
let rot = DMatrix::from_column_slice(ELEMENT_DIM, ELEMENT_DIM, &[
|
|
|
|
FRAC_1_SQRT_2, 0.0, -FRAC_1_SQRT_2, 0.0, 0.0,
|
|
|
|
0.0, 1.0, 0.0, 0.0, 0.0,
|
|
|
|
FRAC_1_SQRT_2, 0.0, FRAC_1_SQRT_2, 0.0, 0.0,
|
|
|
|
0.0, 0.0, 0.0, 1.0, 0.0,
|
|
|
|
0.0, 0.0, 0.0, 0.0, 1.0
|
|
|
|
]);
|
|
|
|
let transl = translation(Vector3::new(0.0, 0.0, 7.0));
|
|
|
|
let motion_proj_tfm = transl * rot * motion_orig_proj;
|
|
|
|
|
|
|
|
// confirm that the projection of the nudge is equivariant. we loosen
|
|
|
|
// the comparison tolerance because the transformation seems to
|
|
|
|
// introduce some numerical error
|
|
|
|
const SCALED_TOL_TFM: f64 = 1.0e-9;
|
|
|
|
let tol_sq = ((guess_orig.nrows() * guess_orig.ncols()) as f64) * SCALED_TOL_TFM * SCALED_TOL_TFM;
|
|
|
|
assert!((motion_proj_tfm - motion_tfm_proj).norm_squared() < tol_sq);
|
|
|
|
}
|
|
|
|
|
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
|
|
|
// at the frozen indices, the optimization steps should have exact zeros,
|
|
|
|
// and the realized configuration should match the initial guess
|
|
|
|
#[test]
|
|
|
|
fn frozen_entry_test() {
|
|
|
|
let gram = {
|
|
|
|
let mut gram_to_be = PartialMatrix::new();
|
|
|
|
for j in 0..2 {
|
|
|
|
for k in j..2 {
|
|
|
|
gram_to_be.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
gram_to_be
|
|
|
|
};
|
|
|
|
let guess = DMatrix::from_columns(&[
|
|
|
|
point(0.0, 0.0, 2.0),
|
|
|
|
sphere(0.0, 0.0, 0.0, 1.0)
|
|
|
|
]);
|
|
|
|
let frozen = [(3, 0), (3, 1)];
|
|
|
|
println!();
|
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 (config, _, success, history) = realize_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
|
|
|
&gram, guess.clone(), &frozen,
|
|
|
|
1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
|
|
|
);
|
|
|
|
assert_eq!(success, true);
|
|
|
|
for base_step in history.base_step.into_iter() {
|
|
|
|
for index in frozen {
|
|
|
|
assert_eq!(base_step[index], 0.0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for index in frozen {
|
|
|
|
assert_eq!(config[index], guess[index]);
|
|
|
|
}
|
|
|
|
}
|
2024-10-21 23:38:27 +00:00
|
|
|
}
|