feat: Engine diagnostics #92

Merged
glen merged 16 commits from Vectornaut/dyna3:diagnostics into main 2025-07-21 04:18:50 +00:00
7 changed files with 53 additions and 47 deletions
Showing only changes of commit f1865f85a1 - Show all commits

View file

@ -2,14 +2,14 @@
use nalgebra::DMatrix;
use dyna3::engine::{Q, DescentHistory, RealizationResult};
use dyna3::engine::{Q, DescentHistory, Realization};
pub fn title(title: &str) {
println!("─── {title} ───");
}
pub fn realization_diagnostics(realization_result: &RealizationResult) {
let RealizationResult { result, history } = realization_result;
pub fn realization_diagnostics(realization: &Realization) {
let Realization { result, history } = realization;
println!();
if let Err(ref message) = result {
println!("❌️ {message}");

View file

@ -1,14 +1,14 @@
#[path = "common/print.rs"]
mod print;
use dyna3::engine::{Realization, examples::realize_irisawa_hexlet};
use dyna3::engine::{ConfigNeighborhood, examples::realize_irisawa_hexlet};
fn main() {
const SCALED_TOL: f64 = 1.0e-12;
let realization_result = realize_irisawa_hexlet(SCALED_TOL);
let realization = realize_irisawa_hexlet(SCALED_TOL);
print::title("Irisawa hexlet");
print::realization_diagnostics(&realization_result);
if let Ok(Realization { config, .. }) = realization_result.result {
print::realization_diagnostics(&realization);
if let Ok(ConfigNeighborhood { config, .. }) = realization.result {
// print the diameters of the chain spheres
println!("\nChain diameters:");
println!(" {} sun (given)", 1.0 / config[(3, 3)]);
@ -19,5 +19,5 @@ fn main() {
// print the completed Gram matrix
print::gram_matrix(&config);
}
print::loss_history(&realization_result.history);
print::loss_history(&realization.history);
}

View file

@ -3,14 +3,14 @@ mod print;
use nalgebra::{DMatrix, DVector};
use dyna3::engine::{Realization, examples::realize_kaleidocycle};
use dyna3::engine::{ConfigNeighborhood, examples::realize_kaleidocycle};
fn main() {
const SCALED_TOL: f64 = 1.0e-12;
let realization_result = realize_kaleidocycle(SCALED_TOL);
let realization = realize_kaleidocycle(SCALED_TOL);
print::title("Kaleidocycle");
print::realization_diagnostics(&realization_result);
if let Ok(Realization { config, tangent }) = realization_result.result {
print::realization_diagnostics(&realization);
if let Ok(ConfigNeighborhood { config, nbhd: tangent }) = realization.result {
// print the completed Gram matrix and the realized configuration
print::gram_matrix(&config);
print::config(&config);

View file

@ -5,8 +5,8 @@ use dyna3::engine::{
point,
realize_gram,
sphere,
ConstraintProblem,
Realization
ConfigNeighborhood,
ConstraintProblem
};
fn main() {
@ -20,14 +20,14 @@ fn main() {
}
}
problem.frozen.push(3, 0, problem.guess[(3, 0)]);
let realization_result = realize_gram(
let realization = realize_gram(
&problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
);
print::title("Point on a sphere");
print::realization_diagnostics(&realization_result);
if let Ok(Realization{ config, .. }) = realization_result.result {
print::realization_diagnostics(&realization);
if let Ok(ConfigNeighborhood{ config, .. }) = realization.result {
print::gram_matrix(&config);
print::config(&config);
}
print::loss_history(&realization_result.history);
print::loss_history(&realization.history);
}

View file

@ -1,7 +1,12 @@
#[path = "common/print.rs"]
mod print;
use dyna3::engine::{realize_gram, sphere, ConstraintProblem, Realization};
use dyna3::engine::{
realize_gram,
sphere,
ConfigNeighborhood,
ConstraintProblem
};
fn main() {
let mut problem = ConstraintProblem::from_guess({
@ -17,13 +22,13 @@ fn main() {
problem.gram.push_sym(j, k, if j == k { 1.0 } else { -1.0 });
}
}
let realization_result = realize_gram(
let realization = realize_gram(
&problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
);
print::title("Three spheres");
print::realization_diagnostics(&realization_result);
if let Ok(Realization{ config, .. }) = realization_result.result {
print::realization_diagnostics(&realization);
if let Ok(ConfigNeighborhood{ config, .. }) = realization.result {
print::gram_matrix(&config);
}
print::loss_history(&realization_result.history);
print::loss_history(&realization.history);
}

View file

@ -23,11 +23,11 @@ use crate::{
project_sphere_to_normalized,
realize_gram,
sphere,
ConfigNeighborhood,
ConfigSubspace,
ConstraintProblem,
DescentHistory,
Realization,
RealizationResult
Realization
},
outline::OutlineItem,
specified::SpecifiedValue
@ -696,7 +696,7 @@ impl Assembly {
console_log!("Old configuration:{:>8.3}", problem.guess);
// look for a configuration with the given Gram matrix
let RealizationResult { result, history } = realize_gram(
let Realization { result, history } = realize_gram(
&problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
);
@ -714,7 +714,7 @@ impl Assembly {
self.descent_history.set(history);
match result {
Ok(Realization { config, tangent }) => {
Ok(ConfigNeighborhood { config, nbhd: tangent }) => {

Is nbhd: tangent just a syntax for in essence renaming the nbhd member of a ConfigNeighborhood to tangent for use as a local variable? Is the fact that you are inclined to do so an indication that perhaps config and tangent would be better names for the members of a ConfigNeighborhood rather than config and nbhd? In other words, you seem comfortable enough with config just to use it here, but felt motivated to use the nbhd under the name tangent...

Is `nbhd: tangent` just a syntax for in essence renaming the `nbhd` member of a ConfigNeighborhood to `tangent` for use as a local variable? Is the fact that you are inclined to do so an indication that perhaps `config` and `tangent` would be better names for the members of a ConfigNeighborhood rather than `config` and `nbhd`? In other words, you seem comfortable enough with config just to use it here, but felt motivated to use the `nbhd` under the name `tangent`...

Is nbhd: tangent just a syntax for in essence renaming the nbhd member of a ConfigNeighborhood to tangent for use as a local variable?

Yes—this code destructures result into local variables called config and tangent, which hold the values of result.config and result.nbhd respectively.

Is the fact that you are inclined to do so an indication that perhaps config and tangent would be better names for the members of a ConfigNeighborhood rather than config and nbhd?

That field was indeed called tangent before the naming improvement commit. I renamed it to nbhd because the ConfigNeighborhood structure seems well-designed to represent any first-order neighborhood of a point in configuration space, regardless of whether we're thinking of it as the tangent space of a submanifold. To me, this aligns with our renaming of the ConfigNeighborhood structure itself, which recognized that this structure is well-designed to represent things other than the result of a realization.

I'd be open to switching to a name like ConfigTangent or ConfigTangentPlane for the structure and going back to tangent for the field in that context. I still feel like there must be a better name for the structure, like maybe:

  • A name for what you get when you take a distribution on a vector bundle and evaluate it at a point.
  • Something related to the notion of a first-order neighborhood used in algebraic geometry.

However, I haven't been able to find one.

> Is nbhd: tangent just a syntax for in essence renaming the nbhd member of a ConfigNeighborhood to tangent for use as a local variable? Yes—this code [*destructures*](https://doc.rust-lang.org/rust-by-example/flow_control/match/destructuring/destructure_structures.html) `result` into local variables called `config` and `tangent`, which hold the values of `result.config` and `result.nbhd` respectively. > Is the fact that you are inclined to do so an indication that perhaps `config` and `tangent` would be better names for the members of a ConfigNeighborhood rather than `config` and `nbhd`? That field was indeed called `tangent` before the [naming improvement commit](https://code.studioinfinity.org/StudioInfinity/dyna3/commit/f1865f8#diff-1357a04457079bf2e3ec43e8436b5f77f70471c1). I renamed it to `nbhd` because the `ConfigNeighborhood` structure seems well-designed to represent any first-order neighborhood of a point in configuration space, regardless of whether we're thinking of it as the tangent space of a submanifold. To me, this aligns with our renaming of the `ConfigNeighborhood` structure itself, which recognized that this structure is well-designed to represent things other than the result of a realization. I'd be open to switching to a name like `ConfigTangent` or `ConfigTangentPlane` for the structure and going back to `tangent` for the field in that context. I still feel like there must be a better name for the structure, like maybe: - A name for what you get when you take a [distribution](https://en.wikipedia.org/wiki/Distribution_(differential_geometry)) on a vector bundle and evaluate it at a point. - Something related to the notion of a [first-order neighborhood](https://mathoverflow.net/questions/78313/first-order-infinitesimal-neighborhood-of-a-point) used in algebraic geometry. However, I haven't been able to find one.
/* DEBUG */
// report the tangent dimension
console_log!("Tangent dimension: {}", tangent.dim());

View file

@ -393,13 +393,14 @@ fn seek_better_config(
None
}
pub struct Realization {
// a first-order neighborhood of a configuration
pub struct ConfigNeighborhood {
pub config: DMatrix<f64>,
pub tangent: ConfigSubspace
pub nbhd: ConfigSubspace
}
glen marked this conversation as resolved

The difference between a "Realization" and a "RealizationResult" is not at all clear to me. I guess that this is some Rust-ism, in that a Result is some customary wrapper around a value, in this case a Realization -- is that right? Do you think the two names are completely idiomatic in a Rust world? Or do you think there might be some plain-english names that would better convey what's going on? (Intuitively to me, it would seem a Realization would be the natural outcome of trying to realize an Assembly...)

The difference between a "Realization" and a "RealizationResult" is not at all clear to me. I guess that this is some Rust-ism, in that a Result is some customary wrapper around a value, in this case a Realization -- is that right? Do you think the two names are completely idiomatic in a Rust world? Or do you think there might be some plain-english names that would better convey what's going on? (Intuitively to me, it would seem a Realization would be the natural outcome of trying to realize an Assembly...)

Intuitively to me, it would seem a Realization would be the natural outcome of trying to realize an Assembly...

Trying to realize an assembly doesn't always produce a realization (because it can fail), and it also produces additional data (the descent history and other diagnostic information). Are you hoping for a better way to organize this data, or just a more descriptive name for the data structure? If the latter, what do you think of RealizationOutcome or RealizationOutput?

I guess that this is some Rust-ism, in that a Result is some customary wrapper around a value, in this case a Realization -- is that right? Do you think the two names are completely idiomatic in a Rust world?

No, I don't think the current naming is idiomatic. In fact, I find it a little misleading, because I'd expect something called RealizationResult to just be or contain a Result<Realization, _>, with no additional data.

> Intuitively to me, it would seem a Realization would be the natural outcome of trying to realize an Assembly... Trying to realize an assembly doesn't always produce a realization (because it can fail), and it also produces additional data (the descent history and other diagnostic information). Are you hoping for a better way to organize this data, or just a more descriptive name for the data structure? If the latter, what do you think of `RealizationOutcome` or `RealizationOutput`? > I guess that this is some Rust-ism, in that a Result is some customary wrapper around a value, in this case a Realization -- is that right? Do you think the two names are completely idiomatic in a Rust world? No, I don't think the current naming is idiomatic. In fact, I find it a little misleading, because I'd expect something called `RealizationResult` to just be or contain a `Result<Realization, _>`, with no additional data.

OK, so currently a Realization is just a set of coordinates for everything, is that right? The other gadget is all the stuff that can be produced from a call to a "realize" function that tries to create a Realization that satisfies all the constraints, but (a) might fail, and (b) produces additional info, correct?

So yes, I guess all I am looking for are better names for these two things. Here are some options:

(a) What is currently called a Realization should perhaps just be called a Configuration -- if it is just coordinates, there's no guarantee that this data structure "realizes" any particular set of constraints. That would free up "Realization" to be, quite naturally, the result of calling realize() -- and such a thing would naturally include a Configuration if it managed to find one. (But again, I don't quite understand why it wouldn't always include a Configuration (the best it could do) and a flag to tell you whether this configuration actually realized all the constraints.)

(b) If you feel strongly that the collection of coordinates thing should absolutely be called a Realization (although to me that only makes sense if there is something inherent in the data structure that ensures its contents realize some specific constraings), then the gadget returned by having the engine try to realize() could be named something altogether different, like "EngineRun" or "EngineData."

I guess what I am mostly saying is to choose quite distinct stem words for the two structures, to reflect the fact that this is a containment relationship, rather than an is-a or flavor-of relationship: an output-of-realize gadget optionally contains one of the collection-of-coordinates things, but it is an altogether different entity.

Hopefully I am making sense.

OK, so currently a Realization is just a set of coordinates for everything, is that right? The other gadget is all the stuff that can be produced from a call to a "realize" function that tries to create a Realization that satisfies all the constraints, but (a) might fail, and (b) produces additional info, correct? So yes, I guess all I am looking for are better names for these two things. Here are some options: (a) What is currently called a Realization should perhaps just be called a Configuration -- if it is just coordinates, there's no guarantee that this data structure "realizes" any particular set of constraints. That would free up "Realization" to be, quite naturally, the result of calling `realize()` -- and such a thing would naturally include a Configuration if it managed to find one. (But again, I don't quite understand why it wouldn't _always_ include a Configuration (the best it could do) and a flag to tell you whether this configuration actually realized all the constraints.) (b) If you feel strongly that the collection of coordinates thing should absolutely be called a Realization (although to me that only makes sense if there is something inherent in the data structure that ensures its contents realize some specific constraings), then the gadget returned by having the engine try to realize() could be named something altogether different, like "EngineRun" or "EngineData." I guess what I am mostly saying is to choose quite distinct stem words for the two structures, to reflect the fact that this is a containment relationship, rather than an is-a or flavor-of relationship: an output-of-realize gadget optionally contains one of the collection-of-coordinates things, but it is an altogether different entity. Hopefully I am making sense.

What is currently called a Realization should perhaps just be called a Configuration -- if it is just coordinates, there's no guarantee that this data structure "realizes" any particular set of constraints. That would free up "Realization" to be, quite naturally, the result of calling realize() -- and such a thing would naturally include a Configuration if it managed to find one.

That makes the most sense to me! The structures would become something like this:

pub struct Configuration {
    pub config: DMatrix<f64>, /* rename? */
    pub tangent: ConfigSubspace
}

pub struct Realization {
    pub config: Result<Configuration, String>,
    pub history: DescentHistory
}
> What is currently called a Realization should perhaps just be called a Configuration -- if it is just coordinates, there's no guarantee that this data structure "realizes" any particular set of constraints. That would free up "Realization" to be, quite naturally, the result of calling `realize()` -- and such a thing would naturally include a Configuration if it managed to find one. That makes the most sense to me! The structures would become something like this: ```rust pub struct Configuration { pub config: DMatrix<f64>, /* rename? */ pub tangent: ConfigSubspace } pub struct Realization { pub config: Result<Configuration, String>, pub history: DescentHistory } ```

During our meeting, we decided to name the structures like this:

pub type Configuration = DMatrix<f64>;

// a first-order neighborhood of a configuration
pub struct ConfigNeighborhood {
    pub config: Configuration
    pub neighborhood: ConfigSubspace
}

pub struct Realization {
    pub config_neighborhood: Result<ConfigNeighborhood, String>,
    pub history: DescentHistory
}

In the future, we might want the engine to return a best guess even if realization fails. Ways to do that include:

  • Splitting config into a ConfigNeighborhood and a success indicator.
  • Changing the type of config to something like this:
    Result<ConfigNeighborhood, (ConfigNeighborhood, String)>
    
    The ConfigNeighborhood in the Err variant would be the best guess.
During our meeting, we decided to name the structures like this: ```rust pub type Configuration = DMatrix<f64>; // a first-order neighborhood of a configuration pub struct ConfigNeighborhood { pub config: Configuration pub neighborhood: ConfigSubspace } pub struct Realization { pub config_neighborhood: Result<ConfigNeighborhood, String>, pub history: DescentHistory } ``` In the future, we might want the engine to return a best guess even if realization fails. Ways to do that include: - Splitting `config` into a `ConfigNeighborhood` and a success indicator. - Changing the type of `config` to something like this: ```rust Result<ConfigNeighborhood, (ConfigNeighborhood, String)> ``` The `ConfigNeighborhood` in the `Err` variant would be the best guess.

Two thoughts:

  • In the new Realization structure, the ConfigNeighborhood member could be called "outcome" or "output" or something -- it's name needn't just reiterate its type.

  • In the possible "best guess" future world, we may still want to allow the engine sometimes to throw up its hands completely. So that would mean the two ways you suggest would become having the outcome be an Option<ConfigNeighboorhood> together with a success indicator, or having it be something like Result<ConfigNeighboorhood, (Option<ConfigNeighborhood>, String). But either way (optional best guess or mandatory best guess), having a ConfigNeighborhood on both sides of the Result feels a bit redundant.

Two thoughts: * In the new Realization structure, the ConfigNeighborhood member could be called "outcome" or "output" or something -- it's name needn't just reiterate its type. * In the possible "best guess" future world, we may still want to allow the engine sometimes to throw up its hands completely. So that would mean the two ways you suggest would become having the outcome be an `Option<ConfigNeighboorhood>` together with a success indicator, or having it be something like `Result<ConfigNeighboorhood, (Option<ConfigNeighborhood>, String)`. But either way (optional best guess or mandatory best guess), having a ConfigNeighborhood on both sides of the Result _feels_ a bit redundant.

In the new Realization structure, the ConfigNeighborhood member could be called "outcome" or "output" or something -- it's name needn't just reiterate its type.

How about keeping the name result? This both hints that it has a Result type and accurately describes that it's the main result of trying to realize (in contrast to the extra data in history). To me, it feels similar to outcome, but a little better because it provides the type hint.

> In the new Realization structure, the ConfigNeighborhood member could be called "outcome" or "output" or something -- it's name needn't just reiterate its type. How about keeping the name `result`? This both hints that it has a `Result` type and accurately describes that it's the main result of trying to realize (in contrast to the extra data in `history`). To me, it feels similar to `outcome`, but a little better because it provides the type hint.

Yup, result is fine by me.

Yup, result is fine by me.

Renaming done (commit f1865f8). I decided not to add the type alias Configuration for DMatrix<f64>, as discussed below

Renaming done (commit f1865f8). I decided not to add the type alias `Configuration` for `DMatrix<f64>`, as discussed [below](#issuecomment-3044)

These names seem fine, so closing, but note I did raise one question about the name of a member of ConfigNeighborhood.

These names seem fine, so closing, but note I did raise one question about the name of a member of ConfigNeighborhood.
pub struct RealizationResult {
pub result: Result<Realization, String>,
pub struct Realization {
pub result: Result<ConfigNeighborhood, String>,
pub history: DescentHistory
}
@ -415,7 +416,7 @@ pub fn realize_gram(
reg_scale: f64,
max_descent_steps: i32,
max_backoff_steps: i32
) -> RealizationResult {
) -> Realization {
// destructure the problem data
let ConstraintProblem {
gram, guess, frozen
@ -501,7 +502,7 @@ pub fn realize_gram(
*/
let hess_cholesky = match hess.clone().cholesky() {
Some(cholesky) => cholesky,
None => return RealizationResult {
None => return Realization {
result: Err("Cholesky decomposition failed".to_string()),
history
}
@ -518,7 +519,7 @@ pub fn realize_gram(
state = better_state;
history.backoff_steps.push(backoff_steps);
} else {
return RealizationResult {
return Realization {
result: Err("Line search failed".to_string()),
history
}
@ -539,11 +540,11 @@ pub fn realize_gram(
// find the kernel of the Hessian. give it the uniform inner product
let tangent = ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim);
Ok(Realization { config: state.config, tangent })
Ok(ConfigNeighborhood { config: state.config, nbhd: tangent })
} else {
Err("Failed to reach target accuracy".to_string())
};
RealizationResult{ result, history }
Realization { result, history }
}
// --- tests ---
@ -562,7 +563,7 @@ pub mod examples {
// "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki
// https://www.nippon.com/en/japan-topics/c12801/
//
pub fn realize_irisawa_hexlet(scaled_tol: f64) -> RealizationResult {
pub fn realize_irisawa_hexlet(scaled_tol: f64) -> Realization {
let mut problem = ConstraintProblem::from_guess(
[
sphere(0.0, 0.0, 0.0, 15.0),
@ -613,7 +614,7 @@ pub mod examples {
// set up a kaleidocycle, made of points with fixed distances between them,
// and find its tangent space
pub fn realize_kaleidocycle(scaled_tol: f64) -> RealizationResult {
pub fn realize_kaleidocycle(scaled_tol: f64) -> Realization {
const N_HINGES: usize = 6;
let mut problem = ConstraintProblem::from_guess(
(0..N_HINGES).step_by(2).flat_map(
@ -737,7 +738,7 @@ mod tests {
}
problem.frozen.push(3, 0, problem.guess[(3, 0)]);
problem.frozen.push(3, 1, 0.5);
let RealizationResult { result, history } = realize_gram(
let Realization { result, history } = realize_gram(
&problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
);
let config = result.unwrap().config;
@ -782,10 +783,10 @@ mod tests {
for n in 0..ELEMENT_DIM {
problem.frozen.push(n, 0, problem.guess[(n, 0)]);
}
let RealizationResult { result, history } = realize_gram(
let Realization { result, history } = realize_gram(
&problem, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
);
let Realization { config, tangent } = result.unwrap();
let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap();
assert_eq!(config, problem.guess);
assert_eq!(history.scaled_loss.len(), 1);
@ -854,8 +855,8 @@ mod tests {
fn tangent_test_kaleidocycle() {
// set up a kaleidocycle and find its tangent space
const SCALED_TOL: f64 = 1.0e-12;
let RealizationResult { result, history } = realize_kaleidocycle(SCALED_TOL);
let Realization { config, tangent } = result.unwrap();
let Realization { result, history } = realize_kaleidocycle(SCALED_TOL);
let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap();
assert_eq!(history.scaled_loss.len(), 1);
// list some motions that should form a basis for the tangent space of
@ -943,10 +944,10 @@ mod tests {
problem_orig.gram.push_sym(0, 0, 1.0);
problem_orig.gram.push_sym(1, 1, 1.0);
problem_orig.gram.push_sym(0, 1, 0.5);
let RealizationResult { result: result_orig, history: history_orig } = realize_gram(
let Realization { result: result_orig, history: history_orig } = realize_gram(
&problem_orig, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
);
let Realization { config: config_orig, tangent: tangent_orig } = result_orig.unwrap();
let ConfigNeighborhood { config: config_orig, nbhd: tangent_orig } = result_orig.unwrap();
assert_eq!(config_orig, problem_orig.guess);
assert_eq!(history_orig.scaled_loss.len(), 1);
@ -964,10 +965,10 @@ mod tests {
guess: guess_tfm,
frozen: problem_orig.frozen
};
let RealizationResult { result: result_tfm, history: history_tfm } = realize_gram(
let Realization { result: result_tfm, history: history_tfm } = realize_gram(
&problem_tfm, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
);
let Realization { config: config_tfm, tangent: tangent_tfm } = result_tfm.unwrap();
let ConfigNeighborhood { config: config_tfm, nbhd: tangent_tfm } = result_tfm.unwrap();
assert_eq!(config_tfm, problem_tfm.guess);
assert_eq!(history_tfm.scaled_loss.len(), 1);