#![forbid(unsafe_code)]
#![deny(dead_code_pub_in_binary)]
#[path = "../shared/cli_output.rs"]
mod cli_output;
use std::{
fmt::{self, Display},
fs::{self, File, OpenOptions},
io::{self, BufWriter, Write},
num::NonZeroUsize,
path::{Component, Path, PathBuf},
process::ExitCode,
time::Instant,
};
use clap::{Parser, ValueEnum};
use delaunay::{
InvariantError,
prelude::{
construction::{
ConstructionOptions, DelaunayTriangulationBuilder,
DelaunayTriangulationConstructionError, RetryPolicy, TopologyGuarantee, Vertex, vertex,
},
generators::{RandomPointGenerationError, generate_random_points_in_range_seeded},
geometry::{
CoordinateConversionError, CoordinateRange, CoordinateRangeError, ExactPredicates,
RobustKernel,
},
pachner::{
EdgeKey, FacetHandle, FlipError, PachnerMove, PachnerMoveResult, PachnerMoves,
RidgeHandle, SimplexKey, TriangleHandle, TriangleHandleError, VertexKey,
},
query::QueryError,
tds::{FacetError, TdsError},
triangulation::Triangulation,
},
try_vertices_from_points,
};
use num_traits::ToPrimitive;
use rand::{Rng, RngExt, SeedableRng, rngs::StdRng};
use serde::Serialize;
use crate::cli_output::{ArtifactOutputError, ArtifactPath, write_json_output};
type PachnerStressTriangulation<const D: usize> = Triangulation<RobustKernel<f64>, (), (), D>;
const DEFAULT_3D_VERTICES: usize = 10_000;
const DEFAULT_4D_VERTICES: usize = 1_000;
const DEFAULT_ATTEMPTS: usize = 100_000;
const DEFAULT_KEY_REFRESH_EVERY: usize = 256;
const DEFAULT_RETRY_ATTEMPTS: usize = 24;
const DEFAULT_VALIDATE_EVERY: usize = 1_000;
const PACHNER_STRESS_VALIDATION_SCOPE_LABEL: &str = "topology";
const PACHNER_STRESS_EXPORT_SCHEMA: &str = "delaunay.pachner_stress";
const PACHNER_STRESS_EXPORT_SCHEMA_VERSION: u32 = 2;
const ARTIFACT_COLLISION_PROBE_ATTEMPTS: u32 = 128;
const DEFAULT_VERTEX_GROWTH_DIVISOR: usize = 10;
const DEFAULT_VERTEX_SHRINK_DIVISOR: usize = 20;
#[derive(Debug, Parser)]
#[command(
name = "pachner-stress",
version,
about = "Run validated Pachner-move stress diagnostics"
)]
struct PachnerStressArgs {
#[arg(long, value_enum, default_value = "3d")]
dimension: PachnerStressDimension,
#[arg(long, value_enum, default_value = "round-trip")]
mode: PachnerStressMode,
#[arg(long)]
vertices: Option<usize>,
#[arg(long, default_value_t = DEFAULT_ATTEMPTS)]
attempts: usize,
#[arg(long, default_value_t = DEFAULT_VALIDATE_EVERY)]
validate_every: usize,
#[arg(long, default_value_t = DEFAULT_KEY_REFRESH_EVERY)]
key_refresh_every: usize,
#[arg(long, default_value_t = DEFAULT_RETRY_ATTEMPTS)]
retry_attempts: usize,
#[arg(long)]
seed: Option<u64>,
#[arg(long)]
progress_csv: Option<PathBuf>,
#[arg(long)]
summary_json: Option<PathBuf>,
#[arg(long)]
quiet: bool,
}
impl PachnerStressArgs {
fn into_validated(self) -> Result<PachnerStressCommand, PachnerStressError> {
let config = PachnerStressConfig::try_new(PachnerStressConfigInput {
mode: self.mode,
dimension: self.dimension,
vertex_count: self
.vertices
.unwrap_or_else(|| self.dimension.default_vertices()),
move_attempts: positive_nonzero(PachnerStressCountArgument::Attempts, self.attempts)?,
validate_every: positive_nonzero(
PachnerStressCountArgument::ValidateEvery,
self.validate_every,
)?,
key_refresh_every: positive_nonzero(
PachnerStressCountArgument::KeyRefreshEvery,
self.key_refresh_every,
)?,
retry_attempts: positive_nonzero(
PachnerStressCountArgument::RetryAttempts,
self.retry_attempts,
)?,
seed: self.seed.unwrap_or_else(|| self.dimension.default_seed()),
})?;
let artifacts =
PachnerStressArtifacts::try_new(self.progress_csv, self.summary_json, !self.quiet)?;
Ok(PachnerStressCommand { config, artifacts })
}
}
fn main() -> ExitCode {
let command = match PachnerStressArgs::parse().into_validated() {
Ok(command) => command,
Err(error) => return exit_with_error(error),
};
run(&command).map_or_else(exit_with_error, |()| ExitCode::SUCCESS)
}
fn exit_with_error(error: impl Display) -> ExitCode {
let stderr = io::stderr();
let mut handle = stderr.lock();
let _ = writeln!(handle, "error: {error}");
ExitCode::FAILURE
}
#[derive(Debug)]
struct PachnerStressCommand {
config: PachnerStressConfig,
artifacts: PachnerStressArtifacts,
}
fn run(command: &PachnerStressCommand) -> Result<(), PachnerStressError> {
run_pachner_stress(command.config, &command.artifacts).map(|_| ())
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
enum PachnerStressDimension {
#[value(name = "3d", alias = "3")]
Three,
#[value(name = "4d", alias = "4")]
Four,
}
impl PachnerStressDimension {
const fn value(self) -> usize {
match self {
Self::Three => 3,
Self::Four => 4,
}
}
const fn label(self) -> &'static str {
match self {
Self::Three => "3d",
Self::Four => "4d",
}
}
const fn default_vertices(self) -> usize {
match self {
Self::Three => DEFAULT_3D_VERTICES,
Self::Four => DEFAULT_4D_VERTICES,
}
}
const fn default_seed(self) -> u64 {
match self {
Self::Three => 0x0253_0000_0000_0003,
Self::Four => 0x0253_0000_0000_0004,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
enum PachnerStressMode {
#[value(name = "round-trip")]
RoundTrip,
#[value(name = "random-walk")]
RandomWalk,
}
impl PachnerStressMode {
const fn label(self) -> &'static str {
match self {
Self::RoundTrip => "round-trip",
Self::RandomWalk => "random-walk",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
enum PachnerStressCountArgument {
Attempts,
ValidateEvery,
KeyRefreshEvery,
RetryAttempts,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
enum PachnerStressInsertedFaceContext {
ForwardMove,
K2Move,
}
impl Display for PachnerStressInsertedFaceContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self {
Self::ForwardMove => "forward Pachner move",
Self::K2Move => "k=2",
};
f.write_str(label)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
enum PachnerStressInsertedFaceArity {
InvertibleForwardMove,
Edge,
}
impl Display for PachnerStressInsertedFaceArity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self {
Self::InvertibleForwardMove => "1, 2, or 3",
Self::Edge => "2",
};
f.write_str(label)
}
}
impl PachnerStressCountArgument {
const fn as_str(self) -> &'static str {
match self {
Self::Attempts => "--attempts",
Self::ValidateEvery => "--validate-every",
Self::KeyRefreshEvery => "--key-refresh-every",
Self::RetryAttempts => "--retry-attempts",
}
}
}
impl Display for PachnerStressCountArgument {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
enum PachnerStressArtifactProbeOperation {
CreateDirectory,
CreateParent,
CreateFile,
InspectAlias,
CompareIdentity,
RemoveDirectory,
}
impl Display for PachnerStressArtifactProbeOperation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self {
Self::CreateDirectory => "create probe directory",
Self::CreateParent => "create probe parent",
Self::CreateFile => "create probe file",
Self::InspectAlias => "inspect probe alias",
Self::CompareIdentity => "compare probe identity",
Self::RemoveDirectory => "remove probe directory",
};
f.write_str(label)
}
}
#[derive(Clone, Copy, Debug)]
struct PachnerStressConfig {
mode: PachnerStressMode,
dimension: PachnerStressDimension,
pub vertex_count: NonZeroUsize,
move_attempts: NonZeroUsize,
validate_every: NonZeroUsize,
key_refresh_every: NonZeroUsize,
retry_attempts: NonZeroUsize,
min_vertex_count: usize,
max_vertex_count: usize,
seed: u64,
}
#[derive(Clone, Copy, Debug)]
struct PachnerStressConfigInput {
pub mode: PachnerStressMode,
pub dimension: PachnerStressDimension,
pub vertex_count: usize,
pub move_attempts: NonZeroUsize,
pub validate_every: NonZeroUsize,
pub key_refresh_every: NonZeroUsize,
pub retry_attempts: NonZeroUsize,
pub seed: u64,
}
impl PachnerStressConfig {
fn try_new(input: PachnerStressConfigInput) -> Result<Self, PachnerStressError> {
let minimum_vertices = input.dimension.value() + 1;
let vertex_count = validated_nonzero_count(
input.vertex_count,
|vertex_count| vertex_count.get() >= minimum_vertices,
|| PachnerStressError::TooFewVertices {
dimension: input.dimension.value(),
vertices: input.vertex_count,
minimum: minimum_vertices,
},
)?;
let validate_every = input.validate_every.min(input.move_attempts);
let growth_slack =
(vertex_count.get() / DEFAULT_VERTEX_GROWTH_DIVISOR).max(input.dimension.value() + 1);
let shrink_slack = vertex_count.get() / DEFAULT_VERTEX_SHRINK_DIVISOR;
Ok(Self {
mode: input.mode,
dimension: input.dimension,
vertex_count,
move_attempts: input.move_attempts,
validate_every,
key_refresh_every: input.key_refresh_every,
retry_attempts: input.retry_attempts,
min_vertex_count: vertex_count
.get()
.saturating_sub(shrink_slack)
.max(input.dimension.value() + 1),
max_vertex_count: vertex_count.get().saturating_add(growth_slack),
seed: input.seed,
})
}
pub const fn move_attempts(self) -> NonZeroUsize {
self.move_attempts
}
pub const fn validate_every(self) -> NonZeroUsize {
self.validate_every
}
pub const fn key_refresh_every(self) -> NonZeroUsize {
self.key_refresh_every
}
pub const fn retry_attempts(self) -> NonZeroUsize {
self.retry_attempts
}
const fn label(self) -> &'static str {
self.dimension.label()
}
}
#[derive(Debug)]
struct PachnerStressArtifacts {
progress_csv: Option<ArtifactPath>,
summary_json: Option<ArtifactPath>,
stdout: bool,
}
impl PachnerStressArtifacts {
fn try_new(
progress_csv: Option<PathBuf>,
summary_json: Option<PathBuf>,
stdout: bool,
) -> Result<Self, PachnerStressError> {
let progress_csv = progress_csv.map(ArtifactPath::try_new).transpose()?;
let summary_json = summary_json.map(ArtifactPath::try_new).transpose()?;
if let (Some(progress_csv), Some(summary_json)) = (&progress_csv, &summary_json)
&& artifact_paths_conflict(progress_csv, summary_json)?
{
return Err(PachnerStressError::DuplicateArtifactPath {
progress_path: progress_csv.as_path().to_owned(),
summary_path: summary_json.as_path().to_owned(),
});
}
Ok(Self {
progress_csv,
summary_json,
stdout,
})
}
}
fn artifact_paths_conflict(
first: &ArtifactPath,
second: &ArtifactPath,
) -> Result<bool, PachnerStressError> {
let first_identity = artifact_path_identity(first.as_path())?;
let second_identity = artifact_path_identity(second.as_path())?;
let first_exists = try_artifact_exists(first.as_path())?;
let second_exists = try_artifact_exists(second.as_path())?;
if first_exists && second_exists {
return same_file::is_same_file(first.as_path(), second.as_path()).map_err(|source| {
PachnerStressError::ArtifactCompareIdentity {
first: first.as_path().to_owned(),
second: second.as_path().to_owned(),
source,
}
});
}
if first_identity == second_identity {
return Ok(true);
}
if first_exists || second_exists {
return Ok(false);
}
if !existing_prefixes_match(&first_identity, &second_identity)? {
return Ok(false);
}
probe_missing_artifact_collision(
&first_identity.existing_prefix,
&first_identity.missing_suffix,
&second_identity.missing_suffix,
)
}
#[derive(Debug, Eq, PartialEq)]
struct ArtifactPathIdentity {
existing_prefix: PathBuf,
missing_suffix: PathBuf,
}
fn artifact_path_identity(path: &Path) -> Result<ArtifactPathIdentity, PachnerStressError> {
let absolute = if path.is_absolute() {
path.to_owned()
} else {
let working_directory = std::env::current_dir()
.map_err(|source| PachnerStressError::ArtifactWorkingDirectory { source })?;
working_directory.join(path)
};
canonicalize_existing_prefix(&absolute)
}
fn canonicalize_existing_prefix(path: &Path) -> Result<ArtifactPathIdentity, PachnerStressError> {
let mut existing_prefix = PathBuf::new();
let mut missing_suffix = PathBuf::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
if missing_suffix.as_os_str().is_empty() {
existing_prefix.pop();
} else {
missing_suffix.pop();
}
}
Component::Prefix(_) | Component::RootDir => {
existing_prefix.push(component.as_os_str());
}
Component::Normal(name) if missing_suffix.as_os_str().is_empty() => {
let candidate = existing_prefix.join(name);
match candidate.try_exists() {
Ok(true) => {
existing_prefix = fs::canonicalize(&candidate).map_err(|source| {
PachnerStressError::ArtifactResolveIdentity {
path: candidate,
source,
}
})?;
}
Ok(false) => missing_suffix.push(name),
Err(source) => {
return Err(PachnerStressError::ArtifactInspect {
path: candidate,
source,
});
}
}
}
Component::Normal(name) => missing_suffix.push(name),
}
}
if existing_prefix.as_os_str().is_empty() {
return Err(PachnerStressError::ArtifactUnresolvablePath {
path: path.to_owned(),
});
}
Ok(ArtifactPathIdentity {
existing_prefix,
missing_suffix,
})
}
fn existing_prefixes_match(
first: &ArtifactPathIdentity,
second: &ArtifactPathIdentity,
) -> Result<bool, PachnerStressError> {
if first.existing_prefix == second.existing_prefix {
return Ok(true);
}
same_file::is_same_file(&first.existing_prefix, &second.existing_prefix).map_err(|source| {
PachnerStressError::ArtifactCompareIdentity {
first: first.existing_prefix.clone(),
second: second.existing_prefix.clone(),
source,
}
})
}
fn probe_missing_artifact_collision(
existing_prefix: &Path,
first_suffix: &Path,
second_suffix: &Path,
) -> Result<bool, PachnerStressError> {
if first_suffix == second_suffix {
return Ok(true);
}
if first_suffix.as_os_str().is_empty() || second_suffix.as_os_str().is_empty() {
return Ok(false);
}
if !fs::metadata(existing_prefix)
.map_err(|source| PachnerStressError::ArtifactInspect {
path: existing_prefix.to_owned(),
source,
})?
.is_dir()
{
return Ok(false);
}
let probe = ArtifactCollisionProbe::try_new(existing_prefix)?;
let first = probe.directory().join(first_suffix);
if let Some(parent) = first.parent() {
fs::create_dir_all(parent).map_err(|source| {
PachnerStressError::ArtifactCollisionProbe {
operation: PachnerStressArtifactProbeOperation::CreateParent,
path: parent.to_owned(),
source,
}
})?;
}
let first_file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&first)
.map_err(|source| PachnerStressError::ArtifactCollisionProbe {
operation: PachnerStressArtifactProbeOperation::CreateFile,
path: first.clone(),
source,
})?;
let second = probe.directory().join(second_suffix);
let conflict = match second.try_exists() {
Ok(true) => same_file::is_same_file(&first, &second).map_err(|source| {
PachnerStressError::ArtifactCollisionProbe {
operation: PachnerStressArtifactProbeOperation::CompareIdentity,
path: second,
source,
}
}),
Ok(false) => Ok(false),
Err(source) => Err(PachnerStressError::ArtifactCollisionProbe {
operation: PachnerStressArtifactProbeOperation::InspectAlias,
path: second,
source,
}),
};
drop(first_file);
let conflict = conflict?;
probe.finish()?;
Ok(conflict)
}
struct ArtifactCollisionProbe {
directory: PathBuf,
cleanup_pending: bool,
}
impl ArtifactCollisionProbe {
fn try_new(parent: &Path) -> Result<Self, PachnerStressError> {
for attempt in 0..ARTIFACT_COLLISION_PROBE_ATTEMPTS {
let directory = parent.join(format!(
".pachner-stress-path-probe-{}-{attempt}",
std::process::id()
));
match fs::create_dir(&directory) {
Ok(()) => {
return Ok(Self {
directory,
cleanup_pending: true,
});
}
Err(source) if source.kind() == io::ErrorKind::AlreadyExists => {}
Err(source) => {
return Err(PachnerStressError::ArtifactCollisionProbe {
operation: PachnerStressArtifactProbeOperation::CreateDirectory,
path: directory,
source,
});
}
}
}
Err(PachnerStressError::ArtifactCollisionProbeExhausted {
directory: parent.to_owned(),
attempts: ARTIFACT_COLLISION_PROBE_ATTEMPTS,
})
}
fn directory(&self) -> &Path {
&self.directory
}
fn finish(mut self) -> Result<(), PachnerStressError> {
fs::remove_dir_all(&self.directory).map_err(|source| {
PachnerStressError::ArtifactCollisionProbe {
operation: PachnerStressArtifactProbeOperation::RemoveDirectory,
path: self.directory.clone(),
source,
}
})?;
self.cleanup_pending = false;
Ok(())
}
}
impl Drop for ArtifactCollisionProbe {
fn drop(&mut self) {
if self.cleanup_pending {
let _ = fs::remove_dir_all(&self.directory);
}
}
}
fn try_artifact_exists(path: &Path) -> Result<bool, PachnerStressError> {
path.try_exists()
.map_err(|source| PachnerStressError::ArtifactInspect {
path: path.to_owned(),
source,
})
}
#[derive(Clone, Copy, Debug, Serialize)]
struct PachnerStressSource {
dimension: usize,
label: &'static str,
mode: &'static str,
validation_scope: &'static str,
vertices: usize,
simplices: usize,
seed: u64,
}
#[derive(Clone, Copy, Debug, Serialize)]
struct PachnerStressReport {
sequence: usize,
completed_steps: usize,
proposal_attempts: u128,
accepted_mutations: u128,
candidate_misses: usize,
proposal_rejections: u128,
validations: usize,
validation_nanos: u128,
elapsed_nanos: u128,
proposals_per_second: u128,
final_vertices: usize,
final_simplices: usize,
}
#[derive(Clone, Debug, Serialize)]
struct PachnerStressSummary {
schema: &'static str,
schema_version: u32,
dimension: usize,
label: &'static str,
mode: &'static str,
validation_scope: &'static str,
configured_vertices: usize,
configured_steps: usize,
validate_every: usize,
key_refresh_every: usize,
retry_attempts: usize,
min_vertex_count: usize,
max_vertex_count: usize,
seed: u64,
source: PachnerStressSource,
report: PachnerStressReport,
}
#[derive(Clone, Copy)]
struct PachnerStressProgress {
sequence: usize,
completed_steps: usize,
configured_steps: usize,
proposal_attempts: u128,
accepted_mutations: u128,
candidate_misses: usize,
proposal_rejections: u128,
validations: usize,
validation_nanos: u128,
acceptance_rate: f64,
vertices: usize,
simplices: usize,
}
#[derive(Clone, Copy, Debug, Default)]
struct PachnerStressCounters {
proposal_attempts: u128,
accepted_mutations: u128,
candidate_misses: usize,
proposal_rejections: u128,
validations: usize,
validation_nanos: u128,
}
impl PachnerStressCounters {
const fn record_accepted_mutation(&mut self) {
self.proposal_attempts = self.proposal_attempts.saturating_add(1);
self.accepted_mutations = self.accepted_mutations.saturating_add(1);
}
const fn record_proposal_rejection(&mut self) {
self.proposal_attempts = self.proposal_attempts.saturating_add(1);
self.proposal_rejections = self.proposal_rejections.saturating_add(1);
}
}
struct PachnerStressReporter {
stdout: bool,
progress: Option<ProgressArtifact>,
}
struct ProgressArtifact {
path: ArtifactPath,
writer: BufWriter<File>,
}
impl PachnerStressReporter {
fn try_new(artifacts: &PachnerStressArtifacts) -> Result<Self, PachnerStressError> {
let progress = artifacts
.progress_csv
.as_ref()
.map(|path| {
Ok::<_, PachnerStressError>(ProgressArtifact {
path: path.clone(),
writer: create_progress_writer(path)?,
})
})
.transpose()?;
Ok(Self {
stdout: artifacts.stdout,
progress,
})
}
fn emit_source(&self, source: PachnerStressSource) -> Result<(), PachnerStressError> {
if self.stdout {
let stdout = io::stdout();
let mut handle = stdout.lock();
writeln!(
handle,
"pachner_stress_source dimension={} label={} mode={} validation_scope={} vertices={} simplices={} seed={}",
source.dimension,
source.label,
source.mode,
source.validation_scope,
source.vertices,
source.simplices,
source.seed
)
.map_err(PachnerStressError::stdout)?;
handle.flush().map_err(PachnerStressError::stdout)?;
}
Ok(())
}
fn emit_stage(
&self,
config: PachnerStressConfig,
stage: &'static str,
vertices: Option<usize>,
simplices: Option<usize>,
) -> Result<(), PachnerStressError> {
if self.stdout {
let stdout = io::stdout();
let mut handle = stdout.lock();
write!(
handle,
"pachner_stress_stage dimension={} label={} mode={} validation_scope={} stage={stage}",
config.dimension.value(),
config.label(),
config.mode.label(),
PACHNER_STRESS_VALIDATION_SCOPE_LABEL
)
.map_err(PachnerStressError::stdout)?;
if let Some(vertices) = vertices {
write!(handle, " vertices={vertices}").map_err(PachnerStressError::stdout)?;
}
if let Some(simplices) = simplices {
write!(handle, " simplices={simplices}").map_err(PachnerStressError::stdout)?;
}
writeln!(handle).map_err(PachnerStressError::stdout)?;
handle.flush().map_err(PachnerStressError::stdout)?;
}
Ok(())
}
fn emit_progress(
&mut self,
config: PachnerStressConfig,
progress: PachnerStressProgress,
) -> Result<(), PachnerStressError> {
if self.stdout {
let stdout = io::stdout();
let mut handle = stdout.lock();
writeln!(
handle,
"pachner_stress_progress schema_version={} dimension={} label={} mode={} validation_scope={} sequence={} completed_steps={} configured_steps={} \
proposal_attempts={} accepted_mutations={} candidate_misses={} proposal_rejections={} validations={} \
validation_nanos={} acceptance_rate={:.6} vertices={} simplices={}",
PACHNER_STRESS_EXPORT_SCHEMA_VERSION,
config.dimension.value(),
config.label(),
config.mode.label(),
PACHNER_STRESS_VALIDATION_SCOPE_LABEL,
progress.sequence,
progress.completed_steps,
progress.configured_steps,
progress.proposal_attempts,
progress.accepted_mutations,
progress.candidate_misses,
progress.proposal_rejections,
progress.validations,
progress.validation_nanos,
progress.acceptance_rate,
progress.vertices,
progress.simplices
)
.map_err(PachnerStressError::stdout)?;
handle.flush().map_err(PachnerStressError::stdout)?;
}
if let Some(progress_artifact) = &mut self.progress {
writeln!(
progress_artifact.writer,
"{},{},{},{},{},{},{},{},{},{},{},{},{},{},{:.6},{},{}",
PACHNER_STRESS_EXPORT_SCHEMA_VERSION,
config.dimension.value(),
config.label(),
config.mode.label(),
PACHNER_STRESS_VALIDATION_SCOPE_LABEL,
progress.sequence,
progress.completed_steps,
progress.configured_steps,
progress.proposal_attempts,
progress.accepted_mutations,
progress.candidate_misses,
progress.proposal_rejections,
progress.validations,
progress.validation_nanos,
progress.acceptance_rate,
progress.vertices,
progress.simplices
)
.map_err(|source| PachnerStressError::ArtifactWrite {
path: progress_artifact.path.as_path().to_owned(),
source,
})?;
progress_artifact
.writer
.flush()
.map_err(|source| progress_artifact.path.flush_error(source))?;
}
Ok(())
}
fn emit_report(
&self,
config: PachnerStressConfig,
report: PachnerStressReport,
) -> Result<(), PachnerStressError> {
if self.stdout {
let stdout = io::stdout();
let mut handle = stdout.lock();
writeln!(
handle,
"pachner_stress_metric schema_version={} dimension={} label={} mode={} validation_scope={} sequence={} completed_steps={} \
proposal_attempts={} accepted_mutations={} candidate_misses={} proposal_rejections={} validations={} validation_nanos={} \
elapsed_nanos={} proposals_per_second={} final_vertices={} final_simplices={}",
PACHNER_STRESS_EXPORT_SCHEMA_VERSION,
config.dimension.value(),
config.label(),
config.mode.label(),
PACHNER_STRESS_VALIDATION_SCOPE_LABEL,
report.sequence,
report.completed_steps,
report.proposal_attempts,
report.accepted_mutations,
report.candidate_misses,
report.proposal_rejections,
report.validations,
report.validation_nanos,
report.elapsed_nanos,
report.proposals_per_second,
report.final_vertices,
report.final_simplices
)
.map_err(PachnerStressError::stdout)?;
handle.flush().map_err(PachnerStressError::stdout)?;
}
Ok(())
}
fn finish(&mut self) -> Result<(), PachnerStressError> {
if let Some(progress_artifact) = &mut self.progress {
progress_artifact
.writer
.flush()
.map_err(|source| progress_artifact.path.flush_error(source))?;
}
Ok(())
}
}
#[derive(Default)]
struct MoveSampler {
simplex_keys: Vec<SimplexKey>,
vertex_keys: Vec<VertexKey>,
facet_handles: Vec<FacetHandle>,
edge_keys: Vec<EdgeKey>,
ridge_handles: Vec<RidgeHandle>,
}
impl MoveSampler {
fn try_from_triangulation<const D: usize>(
dt: &PachnerStressTriangulation<D>,
) -> Result<Self, PachnerStressError> {
let mut sampler = Self::default();
sampler.refresh(dt)?;
Ok(sampler)
}
fn refresh<const D: usize>(
&mut self,
dt: &PachnerStressTriangulation<D>,
) -> Result<(), PachnerStressError> {
self.simplex_keys.clear();
self.simplex_keys
.extend(dt.simplices().map(|(simplex_key, _)| simplex_key));
self.vertex_keys.clear();
self.vertex_keys
.extend(dt.vertices().map(|(vertex_key, _)| vertex_key));
self.facet_handles.clear();
for facet in dt.facets() {
self.facet_handles.push(facet?.handle());
}
self.edge_keys.clear();
self.edge_keys.extend(dt.edges());
self.ridge_handles.clear();
for ridge in dt.ridge_handles() {
self.ridge_handles.push(ridge?);
}
Ok(())
}
fn random_simplex_key(&self, rng: &mut (impl Rng + ?Sized)) -> Option<SimplexKey> {
random_cached(&self.simplex_keys, rng)
}
fn random_vertex_key(&self, rng: &mut (impl Rng + ?Sized)) -> Option<VertexKey> {
random_cached(&self.vertex_keys, rng)
}
fn random_facet(&self, rng: &mut (impl Rng + ?Sized)) -> Option<FacetHandle> {
random_cached(&self.facet_handles, rng)
}
fn random_edge(&self, rng: &mut (impl Rng + ?Sized)) -> Option<EdgeKey> {
random_cached(&self.edge_keys, rng)
}
fn random_ridge(&self, rng: &mut (impl Rng + ?Sized)) -> Option<RidgeHandle> {
random_cached(&self.ridge_handles, rng)
}
}
fn random_cached<T: Copy>(values: &[T], rng: &mut (impl Rng + ?Sized)) -> Option<T> {
if values.is_empty() {
return None;
}
let index = rng.random_range(0..values.len());
Some(values[index])
}
fn run_pachner_stress(
config: PachnerStressConfig,
artifacts: &PachnerStressArtifacts,
) -> Result<PachnerStressSummary, PachnerStressError> {
match config.dimension {
PachnerStressDimension::Three => run_pachner_stress_dimension::<3>(config, artifacts),
PachnerStressDimension::Four => run_pachner_stress_dimension::<4>(config, artifacts),
}
}
fn run_pachner_stress_dimension<const D: usize>(
config: PachnerStressConfig,
artifacts: &PachnerStressArtifacts,
) -> Result<PachnerStressSummary, PachnerStressError>
where
RobustKernel<f64>: ExactPredicates<D>,
{
let mut reporter = PachnerStressReporter::try_new(artifacts)?;
let mut tri = build_pachner_stress_dt::<D>(config, &reporter)?;
let source = PachnerStressSource {
dimension: D,
label: config.label(),
mode: config.mode.label(),
validation_scope: PACHNER_STRESS_VALIDATION_SCOPE_LABEL,
vertices: tri.number_of_vertices(),
simplices: tri.number_of_simplices(),
seed: config.seed,
};
reporter.emit_source(source)?;
let start = Instant::now();
let counters = match config.mode {
PachnerStressMode::RoundTrip => {
run_pachner_round_trip_sequence(&mut tri, config, 1, Some(&mut reporter))?
}
PachnerStressMode::RandomWalk => {
run_pachner_random_walk_sequence(&mut tri, config, 1, Some(&mut reporter))?
}
};
let elapsed = start.elapsed();
let report = PachnerStressReport {
sequence: 1,
completed_steps: config.move_attempts().get(),
proposal_attempts: counters.proposal_attempts,
accepted_mutations: counters.accepted_mutations,
candidate_misses: counters.candidate_misses,
proposal_rejections: counters.proposal_rejections,
validations: counters.validations,
validation_nanos: counters.validation_nanos,
elapsed_nanos: elapsed.as_nanos(),
proposals_per_second: counters.proposal_attempts.saturating_mul(1_000_000_000)
/ elapsed.as_nanos().max(1),
final_vertices: tri.number_of_vertices(),
final_simplices: tri.number_of_simplices(),
};
reporter.emit_report(config, report)?;
reporter.finish()?;
let summary = PachnerStressSummary {
schema: PACHNER_STRESS_EXPORT_SCHEMA,
schema_version: PACHNER_STRESS_EXPORT_SCHEMA_VERSION,
dimension: D,
label: config.label(),
mode: config.mode.label(),
validation_scope: PACHNER_STRESS_VALIDATION_SCOPE_LABEL,
configured_vertices: config.vertex_count.get(),
configured_steps: config.move_attempts().get(),
validate_every: config.validate_every().get(),
key_refresh_every: config.key_refresh_every().get(),
retry_attempts: config.retry_attempts().get(),
min_vertex_count: config.min_vertex_count,
max_vertex_count: config.max_vertex_count,
seed: config.seed,
source,
report,
};
if let Some(path) = &artifacts.summary_json {
write_summary_json(path, &summary)?;
}
Ok(summary)
}
fn build_pachner_stress_dt<const D: usize>(
config: PachnerStressConfig,
reporter: &PachnerStressReporter,
) -> Result<PachnerStressTriangulation<D>, PachnerStressError>
where
RobustKernel<f64>: ExactPredicates<D>,
{
reporter.emit_stage(
config,
"generate_points_start",
Some(config.vertex_count.get()),
None,
)?;
let points = generate_random_points_in_range_seeded::<D>(
config.vertex_count.get(),
stress_bounds()?,
config.seed,
)?;
reporter.emit_stage(config, "convert_vertices_start", Some(points.len()), None)?;
let vertices = try_vertices_from_points(&points)?;
let options = ConstructionOptions::default().with_retry_policy(RetryPolicy::Shuffled {
attempts: config.retry_attempts(),
base_seed: Some(config.seed ^ 0xC0DE_0253_C0DE_0253),
});
reporter.emit_stage(config, "construction_start", Some(vertices.len()), None)?;
let dt = DelaunayTriangulationBuilder::new(&vertices)
.topology_guarantee(TopologyGuarantee::PLManifold)
.construction_options(options)
.build_with_kernel(&RobustKernel::new())?;
let tri = dt.into_triangulation();
reporter.emit_stage(
config,
"initial_topology_validation_start",
Some(tri.number_of_vertices()),
Some(tri.number_of_simplices()),
)?;
validate_stress_topology_state(&tri, || {
format!(
"initial Pachner stress state dimension={D} label={} mode={} seed={}",
config.label(),
config.mode.label(),
config.seed
)
})?;
reporter.emit_stage(
config,
"initial_topology_validation_done",
Some(tri.number_of_vertices()),
Some(tri.number_of_simplices()),
)?;
Ok(tri)
}
fn run_pachner_round_trip_sequence<const D: usize>(
dt: &mut PachnerStressTriangulation<D>,
config: PachnerStressConfig,
sequence: usize,
mut reporter: Option<&mut PachnerStressReporter>,
) -> Result<PachnerStressCounters, PachnerStressError> {
let mut rng = StdRng::seed_from_u64(config.seed ^ 0x0253_0253_5252_2525);
let mut sampler = MoveSampler::try_from_triangulation(dt)?;
let mut counters = PachnerStressCounters::default();
for step in 1..=config.move_attempts().get() {
if step > 1 && step.is_multiple_of(config.key_refresh_every().get()) {
sampler.refresh(dt)?;
}
let Some(request) = random_round_trip_move(dt, &sampler, &mut rng) else {
counters.candidate_misses = counters.candidate_misses.saturating_add(1);
maybe_validate_stress_step(dt, config, step, &mut counters, &mut reporter, sequence)?;
continue;
};
match dt.propose_pachner(request) {
Ok(proposal) => {
let Ok(forward) = proposal.attempt_on(dt) else {
counters.record_proposal_rejection();
maybe_validate_stress_step(
dt,
config,
step,
&mut counters,
&mut reporter,
sequence,
)?;
continue;
};
counters.record_accepted_mutation();
let inverse = inverse_move_from_forward_result(dt, &forward)?;
let _inverse_result = dt.propose_pachner(inverse)?.attempt_on(dt)?;
counters.record_accepted_mutation();
}
Err(_) => {
counters.record_proposal_rejection();
}
}
maybe_validate_stress_step(dt, config, step, &mut counters, &mut reporter, sequence)?;
}
validate_final_stress_step(dt, config, &mut counters, &mut reporter, sequence)?;
Ok(counters)
}
fn run_pachner_random_walk_sequence<const D: usize>(
dt: &mut PachnerStressTriangulation<D>,
config: PachnerStressConfig,
sequence: usize,
mut reporter: Option<&mut PachnerStressReporter>,
) -> Result<PachnerStressCounters, PachnerStressError> {
let mut rng = StdRng::seed_from_u64(config.seed ^ 0x0253_0253_0253_0253);
let mut sampler = MoveSampler::try_from_triangulation(dt)?;
let mut counters = PachnerStressCounters::default();
for step in 1..=config.move_attempts().get() {
if step > 1 && step.is_multiple_of(config.key_refresh_every().get()) {
sampler.refresh(dt)?;
}
let Some(request) = random_pachner_move(dt, &sampler, &mut rng, config) else {
counters.candidate_misses = counters.candidate_misses.saturating_add(1);
maybe_validate_stress_step(dt, config, step, &mut counters, &mut reporter, sequence)?;
continue;
};
match dt.propose_pachner(request) {
Ok(proposal) => match proposal.attempt_on(dt) {
Ok(_) => {
counters.record_accepted_mutation();
}
Err(_) => {
counters.record_proposal_rejection();
}
},
Err(_) => {
counters.record_proposal_rejection();
}
}
maybe_validate_stress_step(dt, config, step, &mut counters, &mut reporter, sequence)?;
}
validate_final_stress_step(dt, config, &mut counters, &mut reporter, sequence)?;
Ok(counters)
}
fn maybe_validate_stress_step<const D: usize>(
dt: &PachnerStressTriangulation<D>,
config: PachnerStressConfig,
step: usize,
counters: &mut PachnerStressCounters,
reporter: &mut Option<&mut PachnerStressReporter>,
sequence: usize,
) -> Result<(), PachnerStressError> {
if step.is_multiple_of(config.validate_every().get()) {
validate_stress_step(dt, config, step, counters, reporter, sequence)?;
}
Ok(())
}
fn validate_final_stress_step<const D: usize>(
dt: &PachnerStressTriangulation<D>,
config: PachnerStressConfig,
counters: &mut PachnerStressCounters,
reporter: &mut Option<&mut PachnerStressReporter>,
sequence: usize,
) -> Result<(), PachnerStressError> {
let final_step = config.move_attempts().get();
if !final_step.is_multiple_of(config.validate_every().get()) {
validate_stress_step(dt, config, final_step, counters, reporter, sequence)?;
}
Ok(())
}
fn validate_stress_step<const D: usize>(
dt: &PachnerStressTriangulation<D>,
config: PachnerStressConfig,
step: usize,
counters: &mut PachnerStressCounters,
reporter: &mut Option<&mut PachnerStressReporter>,
sequence: usize,
) -> Result<(), PachnerStressError> {
let validation_start = Instant::now();
validate_stress_topology_state(dt, || stress_validation_context(config, step, *counters))?;
counters.validation_nanos = counters
.validation_nanos
.saturating_add(validation_start.elapsed().as_nanos());
counters.validations = counters.validations.saturating_add(1);
if let Some(reporter) = reporter.as_mut() {
reporter.emit_progress(
config,
PachnerStressProgress {
sequence,
completed_steps: step,
configured_steps: config.move_attempts().get(),
proposal_attempts: counters.proposal_attempts,
accepted_mutations: counters.accepted_mutations,
candidate_misses: counters.candidate_misses,
proposal_rejections: counters.proposal_rejections,
validations: counters.validations,
validation_nanos: counters.validation_nanos,
acceptance_rate: stress_acceptance_rate(*counters)?,
vertices: dt.number_of_vertices(),
simplices: dt.number_of_simplices(),
},
)?;
}
Ok(())
}
fn stress_bounds() -> Result<CoordinateRange<f64>, CoordinateRangeError<f64>> {
CoordinateRange::try_new(0.0_f64, 1.0)
}
fn validate_stress_topology_state<const D: usize>(
dt: &PachnerStressTriangulation<D>,
context: impl FnOnce() -> String,
) -> Result<(), PachnerStressError> {
if let Err(source) = dt.validate() {
return Err(PachnerStressError::TopologyValidation {
context: context(),
source: Box::new(source),
});
}
Ok(())
}
fn stress_acceptance_rate(counters: PachnerStressCounters) -> Result<f64, PachnerStressError> {
if counters.proposal_attempts == 0 {
Ok(0.0)
} else {
let accepted =
counters
.accepted_mutations
.to_f64()
.ok_or(PachnerStressError::CounterConversion {
value: counters.accepted_mutations,
})?;
let proposals =
counters
.proposal_attempts
.to_f64()
.ok_or(PachnerStressError::CounterConversion {
value: counters.proposal_attempts,
})?;
Ok(accepted / proposals)
}
}
fn stress_validation_context(
config: PachnerStressConfig,
step: usize,
counters: PachnerStressCounters,
) -> String {
format!(
"Pachner stress validation label={} mode={} completed_steps={} configured_steps={} \
proposal_attempts={} accepted_mutations={} candidate_misses={} proposal_rejections={}",
config.label(),
config.mode.label(),
step,
config.move_attempts().get(),
counters.proposal_attempts,
counters.accepted_mutations,
counters.candidate_misses,
counters.proposal_rejections
)
}
fn random_round_trip_move<const D: usize>(
dt: &PachnerStressTriangulation<D>,
sampler: &MoveSampler,
rng: &mut (impl Rng + ?Sized),
) -> Option<PachnerMove<(), D>> {
let move_kind_count = if D >= 4 { 3 } else { 2 };
match rng.random_range(0..move_kind_count) {
0 => random_k1_insert(dt, sampler, rng),
1 => sampler
.random_facet(rng)
.map(|facet| PachnerMove::K2 { facet }),
2 => sampler
.random_ridge(rng)
.map(|ridge| PachnerMove::K3 { ridge }),
_ => None,
}
}
fn random_pachner_move<const D: usize>(
dt: &PachnerStressTriangulation<D>,
sampler: &MoveSampler,
rng: &mut (impl Rng + ?Sized),
config: PachnerStressConfig,
) -> Option<PachnerMove<(), D>> {
let move_kind_count = if D >= 4 { 6 } else { 5 };
let mut move_kind = rng.random_range(0..move_kind_count);
let vertex_count = dt.number_of_vertices();
if vertex_count >= config.max_vertex_count && move_kind == 0 {
move_kind = 1;
} else if vertex_count <= config.min_vertex_count && move_kind == 1 {
move_kind = 0;
}
match move_kind {
0 => random_k1_insert(dt, sampler, rng),
1 => sampler
.random_vertex_key(rng)
.map(|vertex_key| PachnerMove::K1Remove { vertex_key }),
2 => sampler
.random_facet(rng)
.map(|facet| PachnerMove::K2 { facet }),
3 => sampler
.random_edge(rng)
.map(|edge| PachnerMove::K2Inverse { edge }),
4 => sampler
.random_ridge(rng)
.map(|ridge| PachnerMove::K3 { ridge }),
5 => random_k3_inverse(dt, sampler, rng),
_ => None,
}
}
fn random_k1_insert<const D: usize>(
dt: &PachnerStressTriangulation<D>,
sampler: &MoveSampler,
rng: &mut (impl Rng + ?Sized),
) -> Option<PachnerMove<(), D>> {
let simplex_key = sampler.random_simplex_key(rng)?;
let coords = random_simplex_centroid(dt, simplex_key)?;
let vertex: Vertex<(), D> = vertex!(coords).ok()?;
Some(PachnerMove::K1Insert {
simplex_key,
vertex,
})
}
fn random_k3_inverse<const D: usize>(
dt: &PachnerStressTriangulation<D>,
sampler: &MoveSampler,
rng: &mut (impl Rng + ?Sized),
) -> Option<PachnerMove<(), D>> {
let simplex_key = sampler.random_simplex_key(rng)?;
let vertices = dt.simplex_vertices(simplex_key).ok()?;
let [a, b, c] = three_distinct_indices(rng, vertices.len())?;
let triangle = TriangleHandle::try_new(vertices[a], vertices[b], vertices[c]).ok()?;
Some(PachnerMove::K3Inverse { triangle })
}
fn inverse_move_from_forward_result<const D: usize>(
dt: &PachnerStressTriangulation<D>,
result: &PachnerMoveResult<D>,
) -> Result<PachnerMove<(), D>, PachnerStressError> {
match result.inserted_face_vertices.as_slice() {
[vertex_key] => Ok(PachnerMove::K1Remove {
vertex_key: *vertex_key,
}),
vertices @ [_, _] => Ok(PachnerMove::K2Inverse {
edge: inserted_edge(dt, vertices)?,
}),
[a, b, c] => Ok(PachnerMove::K3Inverse {
triangle: TriangleHandle::try_new(*a, *b, *c)?,
}),
vertices => Err(PachnerStressError::InsertedFaceArity {
context: PachnerStressInsertedFaceContext::ForwardMove,
expected: PachnerStressInsertedFaceArity::InvertibleForwardMove,
actual: vertices.len(),
}),
}
}
fn inserted_edge<const D: usize>(
dt: &PachnerStressTriangulation<D>,
vertices: &[VertexKey],
) -> Result<EdgeKey, PachnerStressError> {
let [a, b] = vertices else {
return Err(PachnerStressError::InsertedFaceArity {
context: PachnerStressInsertedFaceContext::K2Move,
expected: PachnerStressInsertedFaceArity::Edge,
actual: vertices.len(),
});
};
dt.edges()
.find(|edge| {
let (first, second) = edge.endpoints();
(first == *a && second == *b) || (first == *b && second == *a)
})
.ok_or(PachnerStressError::InsertedEdgeMissing {
left: *a,
right: *b,
})
}
fn random_simplex_centroid<const D: usize>(
dt: &PachnerStressTriangulation<D>,
simplex_key: SimplexKey,
) -> Option<[f64; D]> {
let vertices = dt.simplex_vertices(simplex_key).ok()?;
let mut coords = [0.0; D];
for &vertex_key in vertices {
let vertex_coords = dt.vertex_coords(vertex_key)?;
for (coord, value) in coords.iter_mut().zip(vertex_coords) {
*coord += *value;
}
}
let vertex_count = f64::from(u32::try_from(vertices.len()).ok()?);
for coord in &mut coords {
*coord /= vertex_count;
}
Some(coords)
}
fn three_distinct_indices(rng: &mut (impl Rng + ?Sized), len: usize) -> Option<[usize; 3]> {
if len < 3 {
return None;
}
let first = rng.random_range(0..len);
let mut second = rng.random_range(0..len);
while second == first {
second = rng.random_range(0..len);
}
let mut third = rng.random_range(0..len);
while third == first || third == second {
third = rng.random_range(0..len);
}
Some([first, second, third])
}
fn create_progress_writer(path: &ArtifactPath) -> Result<BufWriter<File>, PachnerStressError> {
let mut writer = path.create_writer()?;
writeln!(
writer,
"schema_version,dimension,label,mode,validation_scope,sequence,completed_steps,configured_steps,\
proposal_attempts,accepted_mutations,candidate_misses,proposal_rejections,validations,\
validation_nanos,acceptance_rate,vertices,simplices"
)
.map_err(|source| PachnerStressError::ArtifactWrite {
path: path.as_path().to_owned(),
source,
})?;
writer.flush().map_err(|source| path.flush_error(source))?;
Ok(writer)
}
fn write_summary_json(
path: &ArtifactPath,
summary: &PachnerStressSummary,
) -> Result<(), PachnerStressError> {
write_json_output(summary, Some(path))?;
Ok(())
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
enum PachnerStressError {
#[error("{argument} must be positive, got {value}")]
NonPositive {
argument: PachnerStressCountArgument,
value: usize,
},
#[error("{dimension}D stress requires at least {minimum} vertices, got {vertices}")]
TooFewVertices {
dimension: usize,
vertices: usize,
minimum: usize,
},
#[error(transparent)]
Artifact {
#[from]
source: ArtifactOutputError,
},
#[error("failed to resolve the working directory for an artifact path: {source}")]
ArtifactWorkingDirectory {
#[source]
source: io::Error,
},
#[error("failed to inspect artifact path {path:?}: {source}")]
ArtifactInspect {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("artifact path has no resolvable existing ancestor: {path:?}")]
ArtifactUnresolvablePath {
path: PathBuf,
},
#[error("failed to resolve artifact path identity for {path:?}: {source}")]
ArtifactResolveIdentity {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("failed to compare artifact paths {first:?} and {second:?}: {source}")]
ArtifactCompareIdentity {
first: PathBuf,
second: PathBuf,
#[source]
source: io::Error,
},
#[error("failed to {operation} at {path:?}: {source}")]
ArtifactCollisionProbe {
operation: PachnerStressArtifactProbeOperation,
path: PathBuf,
#[source]
source: io::Error,
},
#[error(
"failed to reserve an artifact collision probe under {directory:?} after {attempts} attempts"
)]
ArtifactCollisionProbeExhausted {
directory: PathBuf,
attempts: u32,
},
#[error("failed to write artifact {path:?}: {source}")]
ArtifactWrite {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("failed to write Pachner stress telemetry to stdout: {source}")]
Stdout {
#[source]
source: io::Error,
},
#[error("failed to build random point coordinate range: {source}")]
CoordinateRange {
#[from]
source: CoordinateRangeError<f64>,
},
#[error("failed to generate random stress points: {source}")]
PointGeneration {
#[from]
source: RandomPointGenerationError,
},
#[error("failed to convert random points into vertices: {source}")]
CoordinateConversion {
#[from]
source: CoordinateConversionError,
},
#[error("failed to construct initial Delaunay triangulation: {source}")]
Construction {
#[from]
source: DelaunayTriangulationConstructionError,
},
#[error("Pachner stress topology query failed: {source}")]
Query {
#[from]
source: QueryError,
},
#[error("Pachner stress TDS lookup failed: {source}")]
Tds {
#[from]
source: TdsError,
},
#[error("{context}: topology validation failed: {source}")]
TopologyValidation {
context: String,
#[source]
source: Box<InvariantError>,
},
#[error("Pachner stress facet query failed: {source}")]
Facet {
#[from]
source: FacetError,
},
#[error("Pachner proposal commit failed: {source}")]
Flip {
#[from]
source: FlipError,
},
#[error("Pachner stress diagnostic counter {value} cannot be represented in rate telemetry")]
CounterConversion {
value: u128,
},
#[error("{context} inserted face should have {expected} vertices, got {actual}")]
InsertedFaceArity {
context: PachnerStressInsertedFaceContext,
expected: PachnerStressInsertedFaceArity,
actual: usize,
},
#[error("inserted k=2 edge {left:?}-{right:?} is missing")]
InsertedEdgeMissing {
left: VertexKey,
right: VertexKey,
},
#[error("failed to build inverse k=3 triangle handle: {source}")]
TriangleHandle {
#[from]
source: TriangleHandleError,
},
#[error(
"progress CSV and summary JSON must use different paths; got {progress_path:?} and {summary_path:?}"
)]
DuplicateArtifactPath {
progress_path: PathBuf,
summary_path: PathBuf,
},
}
impl PachnerStressError {
const fn stdout(source: io::Error) -> Self {
Self::Stdout { source }
}
}
fn positive_nonzero(
argument: PachnerStressCountArgument,
value: usize,
) -> Result<NonZeroUsize, PachnerStressError> {
NonZeroUsize::new(value).ok_or(PachnerStressError::NonPositive { argument, value })
}
fn validated_nonzero_count<E>(
value: usize,
is_valid: impl FnOnce(NonZeroUsize) -> bool,
error: impl FnOnce() -> E,
) -> Result<NonZeroUsize, E> {
NonZeroUsize::new(value)
.filter(|count| is_valid(*count))
.ok_or_else(error)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[cfg(unix)]
use std::{
ffi::OsString,
os::unix::{ffi::OsStringExt, fs::symlink},
};
use std::{
fs,
path::Path,
time::{SystemTime, UNIX_EPOCH},
};
fn target_artifact_path(label: &str, extension: &str) -> PathBuf {
let stamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock should be after UNIX epoch")
.as_nanos();
PathBuf::from("target")
.join("pachner-stress-tests")
.join(format!("{label}-{stamp}.{extension}"))
}
#[test]
fn positive_count_errors_preserve_typed_argument() {
let error = positive_nonzero(PachnerStressCountArgument::ValidateEvery, 0)
.expect_err("zero should fail positive-count validation");
let PachnerStressError::NonPositive { argument, value } = error else {
panic!("expected NonPositive error, got {error:?}");
};
assert_eq!(argument, PachnerStressCountArgument::ValidateEvery);
assert_eq!(argument.to_string(), "--validate-every");
assert_eq!(value, 0);
}
#[test]
fn zero_vertices_preserve_typed_too_few_vertices_error() {
let error = PachnerStressConfig::try_new(PachnerStressConfigInput {
mode: PachnerStressMode::RoundTrip,
dimension: PachnerStressDimension::Three,
vertex_count: 0,
move_attempts: NonZeroUsize::new(2).expect("literal is nonzero"),
validate_every: NonZeroUsize::new(1).expect("literal is nonzero"),
key_refresh_every: NonZeroUsize::new(7).expect("literal is nonzero"),
retry_attempts: NonZeroUsize::new(4).expect("literal is nonzero"),
seed: 42,
})
.expect_err("zero vertices should fail Pachner stress validation");
let PachnerStressError::TooFewVertices {
dimension,
vertices,
minimum,
} = error
else {
panic!("expected TooFewVertices error, got {error:?}");
};
assert_eq!(dimension, 3);
assert_eq!(vertices, 0);
assert_eq!(minimum, 4);
}
#[test]
fn inserted_face_arity_errors_preserve_typed_context() {
let error = PachnerStressError::InsertedFaceArity {
context: PachnerStressInsertedFaceContext::ForwardMove,
expected: PachnerStressInsertedFaceArity::InvertibleForwardMove,
actual: 4,
};
let PachnerStressError::InsertedFaceArity {
context,
expected,
actual,
} = error
else {
panic!("expected InsertedFaceArity error, got {error:?}");
};
assert_eq!(context, PachnerStressInsertedFaceContext::ForwardMove);
assert_eq!(
expected,
PachnerStressInsertedFaceArity::InvertibleForwardMove
);
assert_eq!(context.to_string(), "forward Pachner move");
assert_eq!(expected.to_string(), "1, 2, or 3");
assert_eq!(actual, 4);
}
#[test]
fn artifacts_reject_duplicate_paths_before_storage() {
let path = PathBuf::from("target/notebooks/pachner/shared.csv");
let error = PachnerStressArtifacts::try_new(Some(path.clone()), Some(path), true)
.expect_err("duplicate artifact paths should fail validation");
let PachnerStressError::DuplicateArtifactPath {
progress_path,
summary_path,
} = error
else {
panic!("expected DuplicateArtifactPath error, got {error:?}");
};
assert_eq!(
progress_path,
Path::new("target/notebooks/pachner/shared.csv")
);
assert_eq!(
summary_path,
Path::new("target/notebooks/pachner/shared.csv")
);
}
#[test]
fn lexical_aliases_share_one_diagnostic_artifact_identity() {
let directory = target_artifact_path("lexical-alias", "dir");
let direct = ArtifactPath::try_new(directory.join("summary.json"))
.expect("direct output path should validate");
let alias = ArtifactPath::try_new(directory.join("nested/../summary.json"))
.expect("aliased output path should validate");
assert!(
artifact_paths_conflict(&direct, &alias)
.expect("diagnostic artifact identities should compare")
);
}
#[test]
fn distinct_missing_names_keep_distinct_artifact_identities() {
let directory = target_artifact_path("distinct-missing", "dir");
fs::create_dir_all(&directory).expect("scratch directory should be created");
let first = ArtifactPath::try_new(directory.join("progress.csv"))
.expect("first output path should validate");
let second = ArtifactPath::try_new(directory.join("summary.json"))
.expect("second output path should validate");
assert!(
!artifact_paths_conflict(&first, &second)
.expect("distinct diagnostic artifact identities should compare")
);
fs::remove_dir_all(directory).expect("scratch directory should be removed");
}
#[cfg(unix)]
#[test]
fn non_utf8_missing_names_keep_exact_artifact_identities() {
let directory = target_artifact_path("non-utf8-missing", "dir");
fs::create_dir_all(&directory).expect("scratch directory should be created");
let first_name = OsString::from_vec(vec![b'a', 0xfe, b'.', b'c', b's', b'v']);
let second_name = OsString::from_vec(vec![b'a', 0xff, b'.', b'c', b's', b'v']);
let first = directory.join(first_name);
let second = directory.join(second_name);
assert_ne!(
artifact_path_identity(&first)
.expect("first non-UTF-8 artifact identity should resolve"),
artifact_path_identity(&second)
.expect("second non-UTF-8 artifact identity should resolve")
);
fs::remove_dir_all(directory).expect("scratch directory should be removed");
}
#[test]
fn missing_name_collisions_follow_the_destination_filesystem_case_rules() {
let directory = target_artifact_path("case-sensitive-missing", "dir");
fs::create_dir_all(&directory).expect("scratch directory should be created");
let case_probe_upper = directory.join("CaseProbe");
let case_probe_lower = directory.join("caseprobe");
fs::write(&case_probe_upper, b"upper").expect("case probe should be created");
let case_sensitive = match OpenOptions::new()
.write(true)
.create_new(true)
.open(&case_probe_lower)
{
Ok(file) => {
drop(file);
fs::remove_file(&case_probe_lower).expect("lower case probe should be removed");
true
}
Err(source) if source.kind() == io::ErrorKind::AlreadyExists => false,
Err(source) => panic!("lower case probe should be created: {source}"),
};
fs::remove_file(case_probe_upper).expect("upper case probe should be removed");
let upper = ArtifactPath::try_new(directory.join("Progress.csv"))
.expect("upper-case output path should validate");
let lower = ArtifactPath::try_new(directory.join("progress.csv"))
.expect("lower-case output path should validate");
assert_eq!(
artifact_paths_conflict(&upper, &lower)
.expect("filesystem-aware diagnostic artifact identities should compare"),
!case_sensitive
);
fs::remove_dir_all(directory).expect("scratch directory should be removed");
}
#[test]
fn existing_hard_link_aliases_share_one_diagnostic_artifact_identity() {
let directory = target_artifact_path("hard-link-alias", "dir");
fs::create_dir_all(&directory).expect("scratch directory should be created");
let original = directory.join("original.json");
let alias = directory.join("alias.json");
fs::write(&original, b"fixture").expect("scratch artifact should be written");
fs::hard_link(&original, &alias).expect("scratch hard link should be created");
let original = ArtifactPath::try_new(original).expect("original path should validate");
let alias = ArtifactPath::try_new(alias).expect("hard-link path should validate");
assert!(
artifact_paths_conflict(&original, &alias)
.expect("existing diagnostic artifact identities should compare")
);
fs::remove_dir_all(directory).expect("scratch hard-link fixture should be removed");
}
#[cfg(unix)]
#[test]
fn symlinked_prefixes_share_one_missing_diagnostic_artifact_identity() {
let directory = target_artifact_path("symlink-prefix-alias", "dir");
let real_directory = directory.join("real");
let alias_directory = directory.join("alias");
fs::create_dir_all(&real_directory).expect("real scratch directory should be created");
symlink(Path::new("real"), &alias_directory)
.expect("scratch directory symlink should be created");
let direct = ArtifactPath::try_new(real_directory.join("summary.json"))
.expect("direct missing-file path should validate");
let alias = ArtifactPath::try_new(alias_directory.join("summary.json"))
.expect("symlinked missing-file path should validate");
assert!(
artifact_paths_conflict(&direct, &alias)
.expect("symlinked diagnostic artifact identities should compare")
);
fs::remove_dir_all(directory).expect("scratch symlink fixture should be removed");
}
#[cfg(unix)]
#[test]
fn parent_traversal_resolves_existing_symlink_components_first() {
let directory = target_artifact_path("symlink-parent-alias", "dir");
let real_directory = directory.join("real");
let nested_directory = real_directory.join("nested");
let alias_directory = directory.join("alias");
fs::create_dir_all(&nested_directory).expect("nested scratch directory should be created");
symlink(Path::new("real/nested"), &alias_directory)
.expect("scratch directory symlink should be created");
let direct = ArtifactPath::try_new(real_directory.join("summary.json"))
.expect("direct missing-file path should validate");
let alias = ArtifactPath::try_new(alias_directory.join("../summary.json"))
.expect("symlink-parent missing-file path should validate");
assert!(
artifact_paths_conflict(&direct, &alias)
.expect("symlink-parent diagnostic artifact identities should compare")
);
fs::remove_dir_all(directory).expect("scratch symlink fixture should be removed");
}
#[test]
fn progress_writer_flushes_header_on_create() {
let path = target_artifact_path("progress-header", "csv");
let artifact_path =
ArtifactPath::try_new(path.clone()).expect("progress path should validate");
let writer = create_progress_writer(&artifact_path)
.expect("progress writer should create parent directories and header");
let visible_len = fs::metadata(&path)
.expect("progress CSV should exist while writer is alive")
.len();
assert!(
visible_len > 0,
"progress CSV header should be visible before the run finishes"
);
drop(writer);
let header = fs::read_to_string(&path).expect("progress CSV header should be readable");
assert_eq!(
header,
"schema_version,dimension,label,mode,validation_scope,sequence,completed_steps,configured_steps,\
proposal_attempts,accepted_mutations,candidate_misses,proposal_rejections,validations,\
validation_nanos,acceptance_rate,vertices,simplices\n"
);
fs::remove_file(path).expect("progress CSV fixture should be removed");
}
#[test]
fn config_clamps_validate_every_to_move_attempts() {
let config = PachnerStressConfig::try_new(PachnerStressConfigInput {
mode: PachnerStressMode::RoundTrip,
dimension: PachnerStressDimension::Three,
vertex_count: 5,
move_attempts: NonZeroUsize::new(2).expect("literal is nonzero"),
validate_every: NonZeroUsize::new(100).expect("literal is nonzero"),
key_refresh_every: NonZeroUsize::new(7).expect("literal is nonzero"),
retry_attempts: NonZeroUsize::new(4).expect("literal is nonzero"),
seed: 42,
})
.expect("valid 3D Pachner stress config should build");
assert_eq!(config.vertex_count.get(), 5);
assert_eq!(config.move_attempts().get(), 2);
assert_eq!(config.validate_every().get(), 2);
assert_eq!(config.key_refresh_every().get(), 7);
assert_eq!(config.retry_attempts().get(), 4);
}
#[test]
fn attempts_accept_the_full_usize_domain_in_both_modes() {
let maximum = usize::MAX.to_string();
for mode in ["round-trip", "random-walk"] {
let args = PachnerStressArgs::try_parse_from([
"pachner-stress",
"--mode",
mode,
"--vertices",
"4",
"--attempts",
maximum.as_str(),
])
.expect("usize::MAX should parse as a raw attempt count");
let command = args
.into_validated()
.expect("usize::MAX should remain valid through semantic parsing");
assert_eq!(command.config.move_attempts().get(), usize::MAX);
}
}
#[test]
fn acceptance_rate_uses_completed_proposals_not_configured_steps() {
let counters = PachnerStressCounters {
proposal_attempts: 2,
accepted_mutations: 1,
..PachnerStressCounters::default()
};
assert_relative_eq!(
stress_acceptance_rate(counters).expect("bounded counters should convert"),
0.5
);
}
#[test]
fn acceptance_rate_covers_maximum_round_trip_proposal_domain() {
let maximum_steps = u128::try_from(usize::MAX)
.expect("the platform usize domain should fit u128 telemetry");
let counters = PachnerStressCounters {
proposal_attempts: maximum_steps * 2,
accepted_mutations: maximum_steps * 2 - 1,
..PachnerStressCounters::default()
};
let rate = stress_acceptance_rate(counters)
.expect("the accepted usize counter domain should convert end-to-end");
assert!(rate.is_finite());
assert!((0.0..=1.0).contains(&rate));
}
}