test(simulation): post-adversarial workshop benches — population survey, chunk rung, S2 density, global tier
hydrology_equilibrium_bench: bench_population_survey_all_committed_bodies (all 267 real heightmaps solved independently — zero carved cells population- wide, ~0.86s total, byte-exact determinism; closes Troblum B1) + bench_per_basin_size_distribution_real_population (22,270 basins: dense wins 100% for lake carriers). bmv_gridunit_bench: chunk-64m per-cell + deep-step canvas benches (1,838 ns/cell, ~1.7s full 4K; Option D's missing row) + S2 courses-density benches (+38-87% at chunk/block, mechanism traced to station spacing scaling with rung cutoff). bmv_global_tier_bench (new): real per-body radii from systems.db via BodyParamsReader — global tier ~8.85MB PNG across the population (supersedes the ~174MB mis-priced figure), rung-0 derive ~16-21ms/body measured. All #[ignore]d release tests, stability re-run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -228,6 +228,17 @@ fn determinism_at_330k_cells() {
|
||||
/// solved across the Rayon pool, at the 512×256 production grid size —
|
||||
/// directly answering the workshop's red-flag-2-adjacent question of
|
||||
/// whether per-body-open hydrology is affordable at scale.
|
||||
///
|
||||
/// **Scope note (population-survey gap, closed by
|
||||
/// [`bench_population_survey_all_committed_bodies`] below):** this bench
|
||||
/// solves ONE real body (GJ1c) 273 times — it proves the *wall-clock*
|
||||
/// affordability of running 273 independent solves in parallel (the
|
||||
/// question it was built to answer), but the 273 solves are not 273
|
||||
/// *distinct* bodies, so it says nothing about how basin/endorheic/carved-
|
||||
/// cell distributions vary across the real body population. That is a
|
||||
/// different question, answered by the population survey, not this bench —
|
||||
/// left as-is (not rewritten) since it still correctly answers the
|
||||
/// question it was designed for.
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn bench_parallel_273_bodies_at_512x256() {
|
||||
@@ -258,3 +269,429 @@ fn bench_parallel_273_bodies_at_512x256() {
|
||||
elapsed.as_secs_f64() * 1000.0 / body_count as f64
|
||||
);
|
||||
}
|
||||
|
||||
/// **POPULATION SURVEY (post-adversarial, Troblum finding).** Loads and
|
||||
/// solves EVERY real committed heightmap PNG in the repo (267 distinct
|
||||
/// bodies as of this bench, `wiki/star-systems/*/bodies/*/heightmap.png`),
|
||||
/// each downsampled to the real 512×256 production working grid with its
|
||||
/// own PNG-embedded `sea_level`, run independently in parallel across the
|
||||
/// Rayon pool — the actual population-scale question
|
||||
/// [`bench_parallel_273_bodies_at_512x256`] does not answer (that bench
|
||||
/// solves ONE body 273 times, not 273 distinct bodies; see its doc comment
|
||||
/// above). This closes the gap: total wall time for the REAL population,
|
||||
/// and — the load-bearing part for the cliff Phase-4 ruling — the
|
||||
/// distribution of basins/endorheic-basins/carved-cliff-edge-cells ACROSS
|
||||
/// the population, not just on GJ1c.
|
||||
///
|
||||
/// **Known scope limit, stated explicitly (not hidden):** every body uses
|
||||
/// the SAME `ClimateInputs { moisture_q: 55 }` (`default_climate()`) — the
|
||||
/// module's own doc already states real per-body moisture requires wiring
|
||||
/// in `BodyWorldState.districts`/`regions` climate context, which is out of
|
||||
/// scope for this survey (same limitation the original T-1177 prototype
|
||||
/// documented, "No per-basin moisture/climate lookup"). This means the
|
||||
/// endorheic classification specifically is measured under a uniform
|
||||
/// climate assumption, not each body's real hydrosphere/atmosphere-derived
|
||||
/// moisture — a real per-body climate wiring pass could shift the
|
||||
/// endorheic counts. It does NOT limit the carved-cliff-edge finding below:
|
||||
/// carving depends on ELEVATION geometry (the narrow two-basin-saddle
|
||||
/// condition documented in the module), not on `moisture_q` at all — the
|
||||
/// climate input only gates the endorheic/overflow classification of an
|
||||
/// already-identified basin, never whether carving occurs.
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn bench_population_survey_all_committed_bodies() {
|
||||
use rayon::prelude::*;
|
||||
use settled_reach_server::atlas::hydrology_equilibrium::{BasinOutcome, DownstreamTarget};
|
||||
|
||||
let wiki_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../wiki/star-systems");
|
||||
|
||||
// Discover every committed heightmap PNG (deterministic ordering: sort
|
||||
// by path so the survey's own reporting order is stable run-to-run —
|
||||
// not load-bearing for solve() itself, which is pure per-body, but
|
||||
// keeps the printed output diffable).
|
||||
let mut heightmap_paths: Vec<std::path::PathBuf> = Vec::new();
|
||||
fn walk(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
|
||||
let Ok(entries) = std::fs::read_dir(dir) else {
|
||||
return;
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
walk(&path, out);
|
||||
} else if path.file_name().and_then(|n| n.to_str()) == Some("heightmap.png") {
|
||||
out.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(&wiki_root, &mut heightmap_paths);
|
||||
heightmap_paths.sort();
|
||||
|
||||
assert!(
|
||||
heightmap_paths.len() > 200,
|
||||
"expected the real committed body population (~267 as of this bench), found {} — \
|
||||
did the wiki_root path resolve correctly? ({wiki_root:?})",
|
||||
heightmap_paths.len()
|
||||
);
|
||||
|
||||
struct BodySurvey {
|
||||
body_id: String,
|
||||
basins: usize,
|
||||
overflow: usize,
|
||||
endorheic: usize,
|
||||
lake_cells: usize,
|
||||
carved_cells: usize,
|
||||
cliff_edge_cells: usize,
|
||||
solve_ms: f64,
|
||||
}
|
||||
|
||||
let t0 = Instant::now();
|
||||
let surveys: Vec<BodySurvey> = heightmap_paths
|
||||
.par_iter()
|
||||
.map(|path| {
|
||||
// body_id = the directory name one level up from heightmap.png
|
||||
// (wiki/star-systems/<system>/bodies/<body_id>/heightmap.png).
|
||||
let body_id = path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("UNKNOWN")
|
||||
.to_string();
|
||||
|
||||
let heightmap = settled_reach_server::atlas::heightmap::load_heightmap_png(
|
||||
path, &body_id, 0.3,
|
||||
)
|
||||
.unwrap_or_else(|e| panic!("decode committed heightmap for {body_id} ({path:?}): {e}"));
|
||||
let small = heightmap.downsample(512, 256);
|
||||
|
||||
let t_solve = Instant::now();
|
||||
let result = solve(&small.data, 512, 256, small.sea_level, default_climate());
|
||||
let solve_ms = t_solve.elapsed().as_secs_f64() * 1000.0;
|
||||
|
||||
let lake_cells: usize = result.basins.iter().map(|b| b.cells.len()).sum();
|
||||
let carved_cells: usize = result
|
||||
.basins
|
||||
.iter()
|
||||
.filter(|b| {
|
||||
matches!(
|
||||
b.outcome,
|
||||
BasinOutcome::Overflow {
|
||||
downstream_target: DownstreamTarget::Sea
|
||||
| DownstreamTarget::Basin(_)
|
||||
| DownstreamTarget::OpenSpillway,
|
||||
..
|
||||
}
|
||||
) && result.channel_depth_scaled[b.spill_cell] > 0
|
||||
})
|
||||
.count();
|
||||
let cliff_edge_cells = result.cliff_edge.iter().filter(|&&c| c).count();
|
||||
let endorheic = result
|
||||
.basins
|
||||
.iter()
|
||||
.filter(|b| matches!(b.outcome, BasinOutcome::Endorheic { .. }))
|
||||
.count();
|
||||
let overflow = result
|
||||
.basins
|
||||
.iter()
|
||||
.filter(|b| matches!(b.outcome, BasinOutcome::Overflow { .. }))
|
||||
.count();
|
||||
|
||||
BodySurvey {
|
||||
body_id,
|
||||
basins: result.basins.len(),
|
||||
overflow,
|
||||
endorheic,
|
||||
lake_cells,
|
||||
carved_cells,
|
||||
cliff_edge_cells,
|
||||
solve_ms,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let elapsed = t0.elapsed();
|
||||
|
||||
let body_count = surveys.len();
|
||||
let total_basins: usize = surveys.iter().map(|s| s.basins).sum();
|
||||
let total_overflow: usize = surveys.iter().map(|s| s.overflow).sum();
|
||||
let total_endorheic: usize = surveys.iter().map(|s| s.endorheic).sum();
|
||||
let total_lake_cells: usize = surveys.iter().map(|s| s.lake_cells).sum();
|
||||
let total_carved_cells: usize = surveys.iter().map(|s| s.carved_cells).sum();
|
||||
let total_cliff_edge_cells: usize = surveys.iter().map(|s| s.cliff_edge_cells).sum();
|
||||
let bodies_with_any_carving = surveys.iter().filter(|s| s.cliff_edge_cells > 0).count();
|
||||
|
||||
let mut by_cliff_edge: Vec<&BodySurvey> = surveys.iter().collect();
|
||||
by_cliff_edge.sort_by(|a, b| b.cliff_edge_cells.cmp(&a.cliff_edge_cells));
|
||||
|
||||
println!(
|
||||
"\n=== POPULATION SURVEY: {body_count} real committed bodies x 512x256, Rayon par_iter ({} threads available) ===",
|
||||
std::thread::available_parallelism()
|
||||
.map(|n| n.get())
|
||||
.unwrap_or(0)
|
||||
);
|
||||
println!(
|
||||
" {:>9.2} ms total wall time, {:>7.3} ms/body average",
|
||||
elapsed.as_secs_f64() * 1000.0,
|
||||
elapsed.as_secs_f64() * 1000.0 / body_count as f64
|
||||
);
|
||||
println!(
|
||||
" basins: {total_basins} total across population ({total_overflow} overflow, {total_endorheic} endorheic)"
|
||||
);
|
||||
println!(" lake cells (summed across population): {total_lake_cells}");
|
||||
println!(
|
||||
" carved-outlet basins (channel_depth_scaled > 0 at spill cell, summed): {total_carved_cells}"
|
||||
);
|
||||
println!(
|
||||
" cliff_edge=true cells (summed across population): {total_cliff_edge_cells}"
|
||||
);
|
||||
println!(
|
||||
" bodies with ANY cliff_edge cell: {bodies_with_any_carving} / {body_count} ({:.2}%)",
|
||||
100.0 * bodies_with_any_carving as f64 / body_count as f64
|
||||
);
|
||||
println!("\n Top 15 bodies by cliff_edge cell count (the max-carve outliers):");
|
||||
for s in by_cliff_edge.iter().take(15) {
|
||||
println!(
|
||||
" {:>10} cliff_edge_cells={:>4} carved_basins={:>2} basins={:>3} (overflow={:>3}, endorheic={:>2}) lake_cells={:>6} solve={:.2}ms",
|
||||
s.body_id, s.cliff_edge_cells, s.carved_cells, s.basins, s.overflow, s.endorheic, s.lake_cells, s.solve_ms
|
||||
);
|
||||
}
|
||||
let zero_carve_bodies = surveys.iter().filter(|s| s.cliff_edge_cells == 0).count();
|
||||
println!(
|
||||
"\n Bodies with ZERO cliff_edge cells: {zero_carve_bodies} / {body_count} ({:.2}%)",
|
||||
100.0 * zero_carve_bodies as f64 / body_count as f64
|
||||
);
|
||||
|
||||
// Determinism spot-check on the max-carve outlier (if any carving was
|
||||
// found at all) — re-solve it once more and confirm byte-identical
|
||||
// cliff_edge/channel_depth output, so the survey's headline outlier
|
||||
// isn't itself an artifact of non-determinism.
|
||||
if let Some(top) = by_cliff_edge.first() {
|
||||
if top.cliff_edge_cells > 0 {
|
||||
let path = heightmap_paths
|
||||
.iter()
|
||||
.find(|p| {
|
||||
p.parent()
|
||||
.and_then(|d| d.file_name())
|
||||
.and_then(|n| n.to_str())
|
||||
== Some(top.body_id.as_str())
|
||||
})
|
||||
.expect("outlier body path must exist (found via the same walk above)");
|
||||
let heightmap = settled_reach_server::atlas::heightmap::load_heightmap_png(
|
||||
path,
|
||||
&top.body_id,
|
||||
0.3,
|
||||
)
|
||||
.expect("re-decode outlier heightmap for determinism spot-check");
|
||||
let small = heightmap.downsample(512, 256);
|
||||
let r1 = solve(&small.data, 512, 256, small.sea_level, default_climate());
|
||||
let r2 = solve(&small.data, 512, 256, small.sea_level, default_climate());
|
||||
assert_eq!(
|
||||
r1.cliff_edge, r2.cliff_edge,
|
||||
"max-carve outlier {} must be deterministic (cliff_edge)",
|
||||
top.body_id
|
||||
);
|
||||
assert_eq!(
|
||||
r1.channel_depth_scaled, r2.channel_depth_scaled,
|
||||
"max-carve outlier {} must be deterministic (channel_depth_scaled)",
|
||||
top.body_id
|
||||
);
|
||||
println!(
|
||||
"\n Determinism spot-check on max-carve outlier ({}): PASS (byte-identical across two solves)",
|
||||
top.body_id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// **PER-BASIN SIZE DISTRIBUTION (body-map-viewer workshop, Araminta's
|
||||
/// endorheic-carrier crossover question, routed via the coordinator).**
|
||||
/// [`bench_population_survey_all_committed_bodies`] above reports only
|
||||
/// AGGREGATE lake-cell totals (2,694,012 summed across 22,270 basins) — it
|
||||
/// does not report individual basin SIZES, which is exactly what a
|
||||
/// sparse-list-vs-dense-field crossover analysis needs (a sparse per-cell
|
||||
/// list is cheap for small basins and expensive for large ones; the
|
||||
/// crossover point depends on the actual size DISTRIBUTION, not the
|
||||
/// aggregate). This bench solves every real committed body at the real
|
||||
/// 512×256 production working grid (same population, same grid size as the
|
||||
/// population survey above) and records every individual basin's cell
|
||||
/// count, then reports the full distribution: histogram buckets, percentiles,
|
||||
/// and the largest basins found, so the crossover call is made from a real
|
||||
/// distribution rather than the single synthetic 8.3M-cell grid's one
|
||||
/// data point (412,700 cells — T-1177's original measured appendix, a
|
||||
/// SYNTHETIC ridged-terrain grid at a canvas size no production path
|
||||
/// actually derives hydrology at synchronously, not a real-population
|
||||
/// statistic).
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn bench_per_basin_size_distribution_real_population() {
|
||||
use rayon::prelude::*;
|
||||
|
||||
let wiki_root =
|
||||
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../wiki/star-systems");
|
||||
let mut heightmap_paths: Vec<std::path::PathBuf> = Vec::new();
|
||||
fn walk(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
|
||||
let Ok(entries) = std::fs::read_dir(dir) else {
|
||||
return;
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
walk(&path, out);
|
||||
} else if path.file_name().and_then(|n| n.to_str()) == Some("heightmap.png") {
|
||||
out.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(&wiki_root, &mut heightmap_paths);
|
||||
heightmap_paths.sort();
|
||||
assert!(
|
||||
heightmap_paths.len() > 200,
|
||||
"expected the real committed body population (~267), found {}",
|
||||
heightmap_paths.len()
|
||||
);
|
||||
|
||||
// (body_id, basin_cell_count, outcome_label) for EVERY basin across the
|
||||
// whole population — the raw material for the distribution below.
|
||||
let all_basins: Vec<(String, usize, &'static str)> = heightmap_paths
|
||||
.par_iter()
|
||||
.flat_map(|path| {
|
||||
let body_id = path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("UNKNOWN")
|
||||
.to_string();
|
||||
let heightmap = settled_reach_server::atlas::heightmap::load_heightmap_png(
|
||||
path, &body_id, 0.3,
|
||||
)
|
||||
.unwrap_or_else(|e| panic!("decode committed heightmap for {body_id} ({path:?}): {e}"));
|
||||
let small = heightmap.downsample(512, 256);
|
||||
let result = solve(&small.data, 512, 256, small.sea_level, default_climate());
|
||||
result
|
||||
.basins
|
||||
.iter()
|
||||
.map(|b| {
|
||||
let outcome_label = match b.outcome {
|
||||
settled_reach_server::atlas::hydrology_equilibrium::BasinOutcome::Overflow {
|
||||
..
|
||||
} => "overflow",
|
||||
settled_reach_server::atlas::hydrology_equilibrium::BasinOutcome::Endorheic {
|
||||
..
|
||||
} => "endorheic",
|
||||
};
|
||||
(body_id.clone(), b.cells.len(), outcome_label)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
all_basins.len(),
|
||||
22_270,
|
||||
"basin count must match the population survey's own total exactly \
|
||||
(same population, same grid size, same solver — a mismatch here \
|
||||
means the two benches drifted, not that either is wrong)"
|
||||
);
|
||||
|
||||
let mut sizes: Vec<usize> = all_basins.iter().map(|(_, n, _)| *n).collect();
|
||||
sizes.sort_unstable();
|
||||
|
||||
let total: usize = sizes.iter().sum();
|
||||
let n = sizes.len();
|
||||
let percentile = |p: f64| -> usize {
|
||||
let idx = ((p / 100.0) * (n - 1) as f64).round() as usize;
|
||||
sizes[idx.min(n - 1)]
|
||||
};
|
||||
|
||||
// Histogram buckets chosen to bracket plausible sparse-vs-dense
|
||||
// crossover points: a Vec<(u16,u16)> cell entry is 4 raw bytes/cell
|
||||
// (MessagePack-framed, somewhat less) vs. the dense `water` field's
|
||||
// PNG-per-field rate of ~1.9 bytes/cell (T-1179) applied ONLY to that
|
||||
// basin's own bounding extent — the buckets below span from "clearly
|
||||
// sparse wins" (tiny basins) through "clearly dense wins" (the largest
|
||||
// basins in this population).
|
||||
let buckets: [(usize, usize, &str); 7] = [
|
||||
(0, 10, "1-10 cells"),
|
||||
(11, 50, "11-50 cells"),
|
||||
(51, 200, "51-200 cells"),
|
||||
(201, 1_000, "201-1,000 cells"),
|
||||
(1_001, 5_000, "1,001-5,000 cells"),
|
||||
(5_001, 20_000, "5,001-20,000 cells"),
|
||||
(20_001, usize::MAX, "20,001+ cells"),
|
||||
];
|
||||
let mut bucket_counts = [0usize; 7];
|
||||
let mut bucket_cell_totals = [0usize; 7];
|
||||
for &size in &sizes {
|
||||
for (i, (lo, hi, _)) in buckets.iter().enumerate() {
|
||||
if size >= *lo && size <= *hi {
|
||||
bucket_counts[i] += 1;
|
||||
bucket_cell_totals[i] += size;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut by_size: Vec<&(String, usize, &str)> = all_basins.iter().collect();
|
||||
by_size.sort_by(|a, b| b.1.cmp(&a.1));
|
||||
|
||||
println!("\n=== PER-BASIN SIZE DISTRIBUTION: {n} basins, real 267-body population, 512x256 working grid ===");
|
||||
println!(" total lake cells (cross-check vs population survey's 2,694,012): {total}");
|
||||
println!(" min={}, max={}, mean={:.1}, median(p50)={}",
|
||||
sizes[0], sizes[n - 1], total as f64 / n as f64, percentile(50.0));
|
||||
println!(" percentiles: p10={} p25={} p50={} p75={} p90={} p95={} p99={} p99.9={}",
|
||||
percentile(10.0), percentile(25.0), percentile(50.0), percentile(75.0),
|
||||
percentile(90.0), percentile(95.0), percentile(99.0), percentile(99.9));
|
||||
|
||||
println!("\n Histogram (basin cell-count buckets):");
|
||||
for (i, (_, _, label)) in buckets.iter().enumerate() {
|
||||
println!(
|
||||
" {label:<18}: {:>6} basins ({:>5.2}% of basins), {:>9} cells total ({:>5.2}% of lake cells)",
|
||||
bucket_counts[i],
|
||||
100.0 * bucket_counts[i] as f64 / n as f64,
|
||||
bucket_cell_totals[i],
|
||||
100.0 * bucket_cell_totals[i] as f64 / total as f64
|
||||
);
|
||||
}
|
||||
|
||||
println!("\n Top 20 largest basins (real population, real bodies):");
|
||||
for (body_id, size, outcome) in by_size.iter().take(20) {
|
||||
println!(" {body_id:>12} cells={size:>7} outcome={outcome}");
|
||||
}
|
||||
|
||||
// Bytes-if-sparse vs bytes-if-dense-per-basin-extent crossover, using
|
||||
// T-1179's measured PNG-per-field rate (638,382 B / 331,776 cells) as
|
||||
// the dense-encoding baseline, and 4 raw bytes/cell (u16,u16 pair,
|
||||
// MessagePack framing not applied — a conservative/pessimistic sparse
|
||||
// estimate, since MessagePack's compact array framing would cost less,
|
||||
// per T-1179's own "6.00 not 7 bytes/cell" finding for a different
|
||||
// field shape) for the sparse Vec<(u16,u16)> carrier.
|
||||
const PNG_BYTES_PER_CELL: f64 = 638_382.0 / 331_776.0;
|
||||
const SPARSE_BYTES_PER_CELL: f64 = 4.0;
|
||||
let mut crossover_basin_count = 0usize;
|
||||
let mut sparse_wins_cells = 0usize;
|
||||
let mut dense_wins_cells = 0usize;
|
||||
for &size in &sizes {
|
||||
let sparse_cost = size as f64 * SPARSE_BYTES_PER_CELL;
|
||||
let dense_cost = size as f64 * PNG_BYTES_PER_CELL;
|
||||
if sparse_cost <= dense_cost {
|
||||
crossover_basin_count += 1;
|
||||
sparse_wins_cells += size;
|
||||
} else {
|
||||
dense_wins_cells += size;
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"\n Sparse-vs-dense crossover (sparse={SPARSE_BYTES_PER_CELL} B/cell raw vs dense={:.3} B/cell PNG-per-field):",
|
||||
PNG_BYTES_PER_CELL
|
||||
);
|
||||
println!(
|
||||
" sparse cheaper for {crossover_basin_count} / {n} basins ({:.2}%), covering {sparse_wins_cells} cells ({:.2}% of lake cells)",
|
||||
100.0 * crossover_basin_count as f64 / n as f64,
|
||||
100.0 * sparse_wins_cells as f64 / total as f64
|
||||
);
|
||||
println!(
|
||||
" dense cheaper for {} / {n} basins ({:.2}%), covering {dense_wins_cells} cells ({:.2}% of lake cells)",
|
||||
n - crossover_basin_count,
|
||||
100.0 * (n - crossover_basin_count) as f64 / n as f64,
|
||||
100.0 * dense_wins_cells as f64 / total as f64
|
||||
);
|
||||
println!();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user