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;
|
2024-12-06 14:35:30 -08: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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-06 14:35:30 -08:00
|
|
|
// --- configuration subspaces ---
|
|
|
|
|
2024-12-08 20:10:55 -08:00
|
|
|
#[derive(Clone)]
|
2024-12-17 21:24:38 -08:00
|
|
|
pub struct ConfigSubspace {
|
|
|
|
assembly_dim: usize,
|
|
|
|
basis: Vec<DMatrix<f64>>
|
|
|
|
}
|
2024-12-06 14:35:30 -08:00
|
|
|
|
|
|
|
impl ConfigSubspace {
|
2024-12-17 21:24:38 -08:00
|
|
|
pub fn zero(assembly_dim: usize) -> ConfigSubspace {
|
|
|
|
ConfigSubspace {
|
|
|
|
assembly_dim: assembly_dim,
|
|
|
|
basis: Vec::new()
|
|
|
|
}
|
2024-12-06 14:35:30 -08: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`
|
|
|
|
fn symmetric_kernel(a: DMatrix<f64>, assembly_dim: usize) -> ConfigSubspace {
|
|
|
|
const ELEMENT_DIM: usize = 5;
|
2024-12-08 20:10:55 -08:00
|
|
|
const THRESHOLD: f64 = 1.0e-4;
|
2024-12-06 14:35:30 -08:00
|
|
|
let eig = SymmetricEigen::new(a);
|
|
|
|
let eig_vecs = eig.eigenvectors.column_iter();
|
|
|
|
let eig_pairs = eig.eigenvalues.iter().zip(eig_vecs);
|
|
|
|
let basis = eig_pairs.filter_map(
|
|
|
|
|(λ, v)| (λ.abs() < THRESHOLD).then_some(
|
|
|
|
Into::<DMatrix<f64>>::into(
|
|
|
|
v.reshape_generic(Dyn(ELEMENT_DIM), Dyn(assembly_dim))
|
|
|
|
)
|
|
|
|
)
|
|
|
|
);
|
2024-12-11 12:59:57 -08:00
|
|
|
|
|
|
|
/* DEBUG */
|
|
|
|
// print the eigenvalues
|
|
|
|
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
|
2024-12-08 20:10:55 -08:00
|
|
|
console::log_1(&JsValue::from(
|
2024-12-11 12:59:57 -08:00
|
|
|
format!("Eigenvalues used to find kernel: {}", eig.eigenvalues)
|
|
|
|
));
|
|
|
|
|
2024-12-17 21:24:38 -08:00
|
|
|
ConfigSubspace {
|
|
|
|
assembly_dim: assembly_dim,
|
|
|
|
basis: basis.collect()
|
|
|
|
}
|
2024-12-06 14:35:30 -08:00
|
|
|
}
|
|
|
|
|
2024-12-08 20:10:55 -08:00
|
|
|
pub fn dim(&self) -> usize {
|
2024-12-17 21:24:38 -08:00
|
|
|
self.basis.len()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn assembly_dim(&self) -> usize {
|
|
|
|
self.assembly_dim
|
2024-12-08 20:10:55 -08:00
|
|
|
}
|
|
|
|
|
2024-12-06 14:35:30 -08:00
|
|
|
// find the projection onto this subspace of the motion where the element
|
|
|
|
// with the given column index has velocity `v`
|
2024-12-08 20:10:55 -08:00
|
|
|
pub fn proj(&self, v: &DVectorView<f64>, column_index: usize) -> DMatrix<f64> {
|
2024-12-18 00:25:15 -08:00
|
|
|
if self.dim() == 0 {
|
|
|
|
const ELEMENT_DIM: usize = 5;
|
|
|
|
DMatrix::zeros(ELEMENT_DIM, self.assembly_dim)
|
|
|
|
} else {
|
|
|
|
self.basis.iter().map(
|
|
|
|
|b| b.column(column_index).dot(&v) * b
|
|
|
|
).sum()
|
|
|
|
}
|
2024-12-06 14:35:30 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
Integrate engine into application prototype (#15)
Port the engine prototype to Rust, integrate it into the application prototype, and use it to enforce the constraints.
### Features
To see the engine in action:
1. Add a constraint by shift-clicking to select two spheres in the outline view and then hitting the 🔗 button
2. Click a summary arrow to see the outline item for the new constraint
2. Set the constraint's Lorentz product by entering a value in the text field at the right end of the outline item
* *The display should update as soon as you press* Enter *or focus away from the text field*
The checkbox at the left end of a constraint outline item controls whether the constraint is active. Activating a constraint triggers a solution update. (Deactivating a constraint doesn't, since the remaining active constraints are still satisfied.)
### Precision
The Julia prototype of the engine uses a generic scalar type, so you can pass in any type the linear algebra functions are implemented for. The examples use the [adjustable-precision](https://docs.julialang.org/en/v1/base/numbers/#Base.MPFR.setprecision) `BigFloat` type.
In the Rust port of the engine, the scalar type is currently fixed at `f64`. Switching to generic scalars shouldn't be too hard, but I haven't looked into [which other types](https://www.nalgebra.org/docs/user_guide/generic_programming) the linear algebra functions are implemented for.
### Testing
To confirm quantitatively that the Rust port of the engine is working, you can go to the `app-proto` folder and:
* Run some automated tests by calling `cargo test`.
* Inspect the optimization process in a few examples calling the `run-examples` script. The first example that prints is the same as the Irisawa hexlet example from the engine prototype. If you go into `engine-proto/gram-test`, launch Julia, and then
```
include("irisawa-hexlet.jl")
for (step, scaled_loss) in enumerate(history_alt.scaled_loss)
println(rpad(step-1, 4), " | ", scaled_loss)
end
```
you should see that it prints basically the same loss history until the last few steps, when the lower default precision of the Rust engine really starts to show.
### A small engine revision
The Rust port of the engine improves on the Julia prototype in one part of the constraint-solving routine: projecting the Hessian onto the subspace where the frozen entries stay constant. The Julia prototype does this by removing the rows and columns of the Hessian that correspond to the frozen entries, finding the Newton step from the resulting "compressed" Hessian, and then adding zero entries to the Newton step in the appropriate places. The Rust port instead replaces each frozen row and column with its corresponding standard unit vector, avoiding the finicky compressing and decompressing steps.
To confirm that this version of the constraint-solving routine works the same as the original, I implemented it in Julia as `realize_gram_alt_proj`. The solutions we get from this routine match the ones we get from the original `realize_gram` to very high precision, and in the simplest examples (`sphere-in-tetrahedron.jl` and `tetrahedron-radius-ratio.jl`), the descent paths also match to very high precision. In a more complicated example (`irisawa-hexlet.jl`), the descent paths diverge about a quarter of the way into the search, even though they end up in the same place.
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/15
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2024-11-12 00:46:16 +00:00
|
|
|
// --- 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
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
2024-12-06 14:35:30 -08: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);
|
2024-12-06 14:35:30 -08: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>));
|
|
|
|
}
|
|
|
|
}
|
2024-12-06 14:35:30 -08: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;
|
|
|
|
}
|
|
|
|
|
2024-12-06 14:35:30 -08: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
|
|
|
|
*/
|
2024-12-06 14:35:30 -08: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);
|
|
|
|
},
|
2024-12-17 21:24:38 -08: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
|
|
|
};
|
|
|
|
}
|
2024-12-06 14:35:30 -08:00
|
|
|
let success = state.loss < tol;
|
|
|
|
let tangent = if success {
|
|
|
|
ConfigSubspace::symmetric_kernel(hess, assembly_dim)
|
|
|
|
} else {
|
2024-12-17 21:24:38 -08:00
|
|
|
ConfigSubspace::zero(assembly_dim)
|
2024-12-06 14:35:30 -08:00
|
|
|
};
|
|
|
|
(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::*;
|
|
|
|
|
2024-12-06 14:35:30 -08: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 {
|
|
|
|
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;
|
2024-12-06 14:35:30 -08: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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-06 14:35:30 -08:00
|
|
|
#[test]
|
|
|
|
fn tangent_test() {
|
|
|
|
const SCALED_TOL: f64 = 1.0e-12;
|
|
|
|
const ELEMENT_DIM: usize = 5;
|
|
|
|
const ASSEMBLY_DIM: usize = 3;
|
|
|
|
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
|
|
|
|
let ConfigSubspace(ref tangent_basis) = tangent;
|
|
|
|
assert_eq!(tangent_basis.len(), 5);
|
|
|
|
|
|
|
|
// 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 tangent_motions = 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, 3, &[
|
|
|
|
0.0, 0.0, 0.0, 0.0, 0.0,
|
|
|
|
0.0, 0.0, -1.0, -0.25, -1.0,
|
|
|
|
0.0, 0.0, -1.0, 0.25, 1.0
|
|
|
|
])
|
|
|
|
];
|
|
|
|
let tol_sq = ((ELEMENT_DIM * ASSEMBLY_DIM) as f64) * SCALED_TOL * SCALED_TOL;
|
|
|
|
for motion in tangent_motions {
|
|
|
|
let motion_proj: DMatrix<_> = motion.column_iter().enumerate().map(
|
|
|
|
|(k, v)| tangent.proj(&v, k)
|
|
|
|
).sum();
|
|
|
|
assert!((motion - motion_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!();
|
2024-12-06 14:35:30 -08: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
|
|
|
}
|