Files
solvecraft/crates/solidcore/src/kernel.rs
T
bherbruck d646586d30 port: geom.context (SmContext, SmNewMarkAndLock, SM_API context)
Adds geom::context (Context with marks/locks, Boolean flag, notify
callbacks, own model size / zone tolerance; MarkLock; MassPropertyBehavior;
create/get_or_create_context), Kernel::context, and wires the
UNPORTED(geom.context) SmTol context overloads (Option<&Context>).
Difftest: 2013 cases, all bit-identical.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 15:59:25 -04:00

228 lines
7.9 KiB
Rust

//! The [`Kernel`] value: tolerances, deterministic RNG and caches, passed explicitly to
//! operations in place of upstream's process-global context.
//!
//! Upstream: `source/SMLib/inc/SmContext.h` (`SmContext`), `SmTol.h` (`SmTol`,
//! `SmTol3d`), the tolerance defaults of `SmMath.h`, the `SmCacheMgr*` classes, and
//! `SM_API`'s `SmApiGetOrCreateContext()`.
//!
//! | Rust | Upstream |
//! |----------------------------------------|-------------------------------------------|
//! | [`Kernel`] | `SmContext` |
//! | [`Kernel::context`] | `SmContext` marks, flags and own tolerance (see [`crate::geom::context`]) |
//! | [`Tolerances::zone_tol_3d`] | `SmContext::m_sThisZoneTol3d`, `SM_ZONE_TOL_3D` |
//! | [`Tolerances::xsect_tol_3d`] | `SM_XSECT_TOL_3D` |
//! | [`Tolerances::approx_tol_3d`] | `SM_APPROX_TOL_3D` |
//! | [`Tolerances::ang_tol_deg`] | `SM_ANG_TOL_DEG` |
//! | [`Tolerances::tol_gap_gain`] | `SM_TOL_GAP_GAIN` |
//! | [`Tolerances::model_size_estimate`] | `SmContext::m_dThisModelSizeEstimate`, `SM_MODEL_SIZE_ESTIMATE` |
//! | [`Tolerances::large_small_size_ratio`] | `SM_LARGE_SMALL_SIZE_RATIO` |
//! | [`Rng`] | C `rand()` / `srand(time(NULL))` (e.g. `SmShell.cpp`) |
//! | [`Caches`] | `SmCacheMgr`, `SmCacheMgrCrv/Srf/TSrf/Brep` |
//!
//! Upstream seeds `rand()` from the clock in a few places; the port replaces that with
//! [`Rng`], seeded from [`KernelOptions::seed`], so runs are reproducible. Results that
//! depend on random draws therefore cannot match the C++ bit for bit; difftests of such
//! code must compare outcomes that are invariant to the draw (and say so in
//! `plan/findings.md`).
use crate::geom::context::Context;
/// Geometric tolerances in model units (upstream defaults from `SmMath.h`).
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Tolerances {
/// Local-neighbourhood size: points closer than this are coincident
/// (`SM_ZONE_TOL_3D` = `1e-5`).
pub zone_tol_3d: f64,
/// Default gap / intersection tolerance (`SM_XSECT_TOL_3D` = `2 * zone`).
pub xsect_tol_3d: f64,
/// Default approximation tolerance (`SM_APPROX_TOL_3D` = `zone / 2`).
pub approx_tol_3d: f64,
/// Maximum angle between parallel vectors, degrees (`SM_ANG_TOL_DEG` = `0.5`).
pub ang_tol_deg: f64,
/// Over-size factor applied above an object's max gap (`SM_TOL_GAP_GAIN` = `1.1`).
pub tol_gap_gain: f64,
/// Expected model size used to pick tolerances
/// (`SM_MODEL_SIZE_ESTIMATE` = `50.0`).
pub model_size_estimate: f64,
/// Ratio of largest to smallest feature size
/// (`SM_LARGE_SMALL_SIZE_RATIO` = `1000`).
pub large_small_size_ratio: f64,
}
impl Default for Tolerances {
fn default() -> Self {
let zone = 1.0e-5;
Self {
zone_tol_3d: zone,
xsect_tol_3d: 2.0 * zone,
approx_tol_3d: zone / 2.0,
ang_tol_deg: 0.5,
tol_gap_gain: 1.1,
model_size_estimate: 50.0,
large_small_size_ratio: 1000.0,
}
}
}
impl Tolerances {
/// [`Tolerances::ang_tol_deg`] in radians (`SM_ANG_TOL_RAD`).
#[must_use]
pub fn ang_tol_rad(&self) -> f64 {
self.ang_tol_deg * crate::math::PI / 180.0
}
}
/// Deterministic pseudo-random generator: `SplitMix64` (Steele, Lea & Flood,
/// "Fast splittable pseudorandom number generators", OOPSLA 2014).
///
/// Chosen because it is tiny, has no dependencies, passes `BigCrush`, behaves the same
/// on every target (including wasm32) and is `Send`. It is not cryptographic.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Rng {
state: u64,
}
impl Rng {
/// Default seed used by [`KernelOptions::default`].
pub const DEFAULT_SEED: u64 = 0x5EED_5EED_5EED_5EED;
/// Creates a generator from a seed. Equal seeds give equal sequences.
#[must_use]
pub const fn new(seed: u64) -> Self {
Self { state: seed }
}
/// Next 64 random bits.
pub fn next_u64(&mut self) -> u64 {
self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
/// Uniform `f64` in `[0, 1)` built from the top 53 bits.
pub fn next_f64(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 * (1.0 / (1u64 << 53) as f64)
}
/// Uniform `f64` in `[-1, 1)`; the port of upstream's
/// `2.0*((double)rand()/RAND_MAX) - 1.0` idiom.
pub fn next_signed_unit(&mut self) -> f64 {
2.0 * self.next_f64() - 1.0
}
}
/// Memoisation caches owned by a [`Kernel`] (upstream `SmCacheMgr*`).
///
/// Empty until the cache units are ported; clearing must never change results, only
/// speed.
#[derive(Debug, Default)]
#[non_exhaustive]
pub struct Caches {}
impl Caches {
/// Drops all cached data.
pub fn clear(&mut self) {
*self = Self::default();
}
}
/// Options for [`Kernel::new`].
#[derive(Debug, Clone, PartialEq)]
pub struct KernelOptions {
/// Tolerances to use.
pub tolerances: Tolerances,
/// Seed for the kernel's [`Rng`].
pub seed: u64,
}
impl Default for KernelOptions {
fn default() -> Self {
Self {
tolerances: Tolerances::default(),
seed: Rng::DEFAULT_SEED,
}
}
}
/// Per-session kernel state passed to operations (upstream `SmContext`).
///
/// A `Kernel` is `Send` (it can move between threads) and holds no global state, so
/// independent kernels never interfere.
#[derive(Debug)]
pub struct Kernel {
/// Geometric tolerances.
pub tolerances: Tolerances,
/// Deterministic random-number generator.
pub rng: Rng,
/// Memoisation caches.
pub caches: Caches,
/// The session's modeling context (marks, Boolean flag, callbacks, its own
/// model size estimate); created with the defaults, as `SmApiGetOrCreateContext`.
pub context: Context,
}
impl Default for Kernel {
fn default() -> Self {
Self::new(&KernelOptions::default())
}
}
impl Kernel {
/// Creates a kernel from options.
#[must_use]
pub fn new(options: &KernelOptions) -> Self {
Self {
tolerances: options.tolerances,
rng: Rng::new(options.seed),
caches: Caches::default(),
context: Context::default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kernel_is_send() {
fn assert_send<T: Send>() {}
assert_send::<Kernel>();
}
#[test]
fn default_tolerances_match_smmath() {
let t = Tolerances::default();
assert_eq!(t.zone_tol_3d, 1.0e-5);
assert_eq!(t.xsect_tol_3d, 2.0e-5);
assert_eq!(t.approx_tol_3d, 5.0e-6);
assert_eq!(t.ang_tol_rad(), 0.5 * crate::math::PI / 180.0);
}
#[test]
fn splitmix64_reference_sequence() {
// Reference values of SplitMix64 seeded with 0 (Vigna's splitmix64.c).
let mut r = Rng::new(0);
assert_eq!(r.next_u64(), 0xE220_A839_7B1D_CDAF);
assert_eq!(r.next_u64(), 0x6E78_9E6A_A1B9_65F4);
assert_eq!(r.next_u64(), 0x06C4_5D18_8009_454F);
}
#[test]
fn rng_is_deterministic_and_in_range() {
let mut k1 = Kernel::default();
let mut k2 = Kernel::new(&KernelOptions::default());
for _ in 0..1000 {
let a = k1.rng.next_f64();
assert_eq!(a, k2.rng.next_f64());
assert!((0.0..1.0).contains(&a));
let s = k1.rng.next_signed_unit();
assert_eq!(s, k2.rng.next_signed_unit());
assert!((-1.0..1.0).contains(&s));
}
k1.caches.clear();
}
}