mod chunk_cache;
mod config;
use std::path::{Path, PathBuf};
use std::time::Instant;
use anyhow::{anyhow, Context};
use clap::{Parser, Subcommand};
use tomoxide::io::DatasetReader;
use tomoxide::Algorithm;
use tomoxide::Center;
use tomoxide::{
Angles, BackendKind, Dtype, Engine, FilterName, Geometry, PhaseMethod, PrepOptions,
ReconParams, StripeMethod,
};
use crate::config::Config;
#[derive(Parser, Debug)]
#[command(name = "tomoxide", version, about)]
struct Cli {
#[arg(long, global = true, default_value = "auto")]
backend: String,
#[arg(long, short, global = true)]
verbose: bool,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand, Debug)]
#[command(rename_all = "snake_case")] enum Command {
Init {
#[arg(long, default_value = "tomoxide.toml")]
config: PathBuf,
},
Status {
#[arg(long)]
config: Option<PathBuf>,
},
Recon {
#[command(flatten)]
common: CommonRecon,
#[arg(long)]
chunk: Option<usize>,
#[arg(long)]
start_row: Option<usize>,
#[arg(long)]
end_row: Option<usize>,
#[arg(long)]
lamino_angle: Option<f32>,
#[arg(long)]
lamino_rh: Option<usize>,
},
ReconSteps {
#[command(flatten)]
common: CommonRecon,
#[arg(long)]
chunk: Option<usize>,
},
TuneChunk {
file: PathBuf,
#[arg(long, default_value = "fbp")]
algorithm: String,
#[arg(long)]
center: Option<f32>,
#[arg(long, default_value = "float32")]
dtype: String,
},
}
#[derive(clap::Args, Debug)]
#[command(rename_all = "snake_case")]
struct CommonRecon {
file: PathBuf,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
algorithm: Option<String>,
#[arg(long)]
center: Option<f32>,
#[arg(long, default_value = "float32")]
dtype: String,
#[arg(long)]
save_format: Option<String>,
#[arg(long)]
filter: Option<String>,
#[arg(long)]
remove_stripe: Option<String>,
#[arg(long)]
retrieve_phase: Option<String>,
#[arg(long)]
num_iter: Option<usize>,
#[arg(long)]
reg_par: Option<String>,
#[arg(long)]
fw_sigma: Option<f32>,
#[arg(long)]
fw_level: Option<usize>,
#[arg(long)]
ti_nblock: Option<usize>,
#[arg(long)]
ti_beta: Option<f32>,
#[arg(long)]
sf_size: Option<usize>,
#[arg(long)]
vo_snr: Option<f32>,
#[arg(long)]
vo_la_size: Option<usize>,
#[arg(long)]
vo_sm_size: Option<usize>,
#[arg(long)]
pixel_size: Option<f32>,
#[arg(long)]
propagation_distance: Option<f32>,
#[arg(long)]
energy: Option<f32>,
#[arg(long)]
alpha: Option<f32>,
#[arg(long)]
db: Option<f32>,
#[arg(long)]
w: Option<f32>,
}
struct ReconPlan {
algorithm: String,
chain: Vec<Algorithm>,
algo: Algorithm,
center: Option<f32>,
dtype: Dtype,
save_format: tomoxide::io::SaveFormat,
save_format_str: String,
filter: FilterName,
filter_str: String,
num_iter: usize,
reg_par: Vec<f32>,
prep: PrepOptions,
stripe_str: String,
phase_str: String,
stripe_params: StripeParams,
phase_params: PhaseParams,
}
#[derive(Clone, Copy)]
struct StripeParams {
fw_sigma: f32,
fw_level: Option<usize>,
ti_nblock: usize,
ti_beta: f32,
sf_size: usize,
vo_snr: f32,
vo_la_size: usize,
vo_sm_size: usize,
}
#[derive(Clone, Copy)]
struct PhaseParams {
pixel_size: f32,
dist: f32,
energy: f32,
alpha: f32,
db: f32,
w: f32,
}
fn build_stripe(name: &str, p: &StripeParams) -> anyhow::Result<StripeMethod> {
Ok(match name.to_ascii_lowercase().as_str() {
"none" => StripeMethod::None,
"fw" => StripeMethod::Fw {
sigma: p.fw_sigma,
level: p.fw_level,
},
"ti" => StripeMethod::Ti {
nblock: p.ti_nblock,
beta: p.ti_beta,
},
"sf" => StripeMethod::Sf { size: p.sf_size },
"vo-all" | "vo_all" | "voall" => StripeMethod::VoAll {
snr: p.vo_snr,
la_size: p.vo_la_size,
sm_size: p.vo_sm_size,
},
other => {
return Err(anyhow!(
"unknown stripe method '{other}' (none|fw|ti|sf|vo-all)"
))
}
})
}
fn build_phase(name: &str, p: &PhaseParams) -> anyhow::Result<PhaseMethod> {
Ok(match name.to_ascii_lowercase().as_str() {
"none" => PhaseMethod::None,
"paganin" => PhaseMethod::Paganin {
pixel_size: p.pixel_size,
dist: p.dist,
energy: p.energy,
alpha: p.alpha,
},
"gpaganin" => PhaseMethod::GPaganin {
pixel_size: p.pixel_size,
dist: p.dist,
energy: p.energy,
db: p.db,
w: p.w,
},
"farago" => PhaseMethod::Farago {
pixel_size: p.pixel_size,
dist: p.dist,
energy: p.energy,
db: p.db,
},
other => {
return Err(anyhow!(
"unknown phase method '{other}' (none|paganin|Gpaganin|farago)"
))
}
})
}
fn parse_f32_list(s: &str) -> anyhow::Result<Vec<f32>> {
s.split(',')
.map(str::trim)
.filter(|t| !t.is_empty())
.map(|t| {
t.parse::<f32>()
.map_err(|e| anyhow!("bad value '{t}': {e}"))
})
.collect()
}
fn resolve(c: &CommonRecon) -> anyhow::Result<(ReconPlan, Config)> {
let cfg = match &c.config {
Some(p) => Config::load(p).with_context(|| format!("loading {}", p.display()))?,
None => Config::default(),
};
let algorithm = c.algorithm.clone().unwrap_or_else(|| cfg.algorithm.clone());
let chain: Vec<Algorithm> = algorithm
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(|s| s.parse::<Algorithm>().map_err(|e| anyhow!("{e}")))
.collect::<anyhow::Result<Vec<_>>>()?;
let algo = *chain
.first()
.ok_or_else(|| anyhow!("no algorithm given (empty --algorithm)"))?;
let center = c.center.or(cfg.rotation_axis);
let dtype: Dtype = c.dtype.parse().map_err(|e| anyhow!("{e}"))?;
let save_format_str = c
.save_format
.clone()
.unwrap_or_else(|| cfg.save_format.clone());
let save_format: tomoxide::io::SaveFormat =
save_format_str.parse().map_err(|e| anyhow!("{e}"))?;
let filter_str = c.filter.clone().unwrap_or_else(|| cfg.filter_name.clone());
let filter: FilterName = filter_str.parse().map_err(|e| anyhow!("{e}"))?;
let num_iter = c.num_iter.unwrap_or(cfg.num_iter);
let reg_par = match &c.reg_par {
Some(s) => parse_f32_list(s)?,
None => cfg.reg_par.clone(),
};
let stripe_str = c
.remove_stripe
.clone()
.unwrap_or_else(|| cfg.remove_stripe_method.clone());
let phase_str = c
.retrieve_phase
.clone()
.unwrap_or_else(|| cfg.retrieve_phase_method.clone());
let stripe_params = StripeParams {
fw_sigma: c.fw_sigma.unwrap_or(cfg.fw_sigma),
fw_level: {
let l = c.fw_level.unwrap_or(cfg.fw_level);
(l != 0).then_some(l)
},
ti_nblock: c.ti_nblock.unwrap_or(cfg.ti_nblock),
ti_beta: c.ti_beta.unwrap_or(cfg.ti_beta),
sf_size: c.sf_size.unwrap_or(cfg.sf_size),
vo_snr: c.vo_snr.unwrap_or(cfg.vo_snr),
vo_la_size: c.vo_la_size.unwrap_or(cfg.vo_la_size),
vo_sm_size: c.vo_sm_size.unwrap_or(cfg.vo_sm_size),
};
let phase_params = PhaseParams {
pixel_size: c.pixel_size.unwrap_or(cfg.pixel_size as f32),
dist: c
.propagation_distance
.unwrap_or(cfg.propagation_distance as f32),
energy: c.energy.unwrap_or(cfg.energy as f32),
alpha: c.alpha.unwrap_or(cfg.alpha as f32),
db: c.db.unwrap_or(cfg.db as f32),
w: c.w.unwrap_or(cfg.w as f32),
};
let prep = PrepOptions {
stripe: build_stripe(&stripe_str, &stripe_params)?,
phase: build_phase(&phase_str, &phase_params)?,
};
Ok((
ReconPlan {
algorithm,
chain,
algo,
center,
dtype,
save_format,
save_format_str,
filter,
filter_str,
num_iter,
reg_par,
prep,
stripe_str,
phase_str,
stripe_params,
phase_params,
},
cfg,
))
}
const DEFAULT_PIPELINE_CHUNK: usize = 8;
const MULTI_GPU_MIN_NX: usize = 2048;
fn pipelines_well(engine: &Engine, algo: Algorithm) -> bool {
engine.name() == "cuda"
&& matches!(
algo,
Algorithm::Fbp | Algorithm::Linerec | Algorithm::Fourierrec | Algorithm::Lprec
)
}
fn parse_backend(s: &str) -> anyhow::Result<BackendKind> {
Ok(match s {
"auto" => BackendKind::Auto,
"cpu" => BackendKind::Cpu,
"cuda" => BackendKind::Cuda,
"wgpu" => BackendKind::Wgpu,
other => return Err(anyhow!("unknown backend '{other}' (auto|cpu|cuda|wgpu)")),
})
}
fn resolve_backend(flag: &str, cfg: &Config) -> anyhow::Result<BackendKind> {
let chosen = if flag == "auto" { &cfg.backend } else { flag };
parse_backend(chosen)
}
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
let level = if cli.verbose { "debug" } else { "info" };
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(level)).init();
let backend_kind = parse_backend(&cli.backend)?;
let backend_flag = cli.backend.clone();
match cli.command {
Command::Init { config } => {
let cfg = Config::default();
cfg.write(&config)
.with_context(|| format!("writing {}", config.display()))?;
println!("wrote default config to {}", config.display());
}
Command::Status { config } => {
let engine = Engine::new(backend_kind)?;
println!("tomoxide {}", env!("CARGO_PKG_VERSION"));
println!("backend (requested {}): {}", cli.backend, engine.name());
if let Some(path) = config {
let cfg =
Config::load(&path).with_context(|| format!("loading {}", path.display()))?;
println!("config: {cfg:#?}");
}
}
Command::Recon {
common,
chunk,
start_row,
end_row,
lamino_angle,
lamino_rh,
} => {
let (plan, cfg) = resolve(&common)?;
let engine = Engine::new(resolve_backend(&backend_flag, &cfg)?)?;
let file = common.file.clone();
let dtype = plan.dtype;
let save_format = plan.save_format;
println!(
"recon: file={} algorithm={:?} center={:?} dtype={} filter={} stripe={} phase={} backend={}",
file.display(),
plan.chain,
plan.center,
dtype.as_str(),
plan.filter_str,
plan.stripe_str,
plan.phase_str,
engine.name()
);
log::debug!(
"resolved prep={:?} num_iter={} reg_par={:?}",
plan.prep,
plan.num_iter,
plan.reg_par
);
let out = recon_out_path(&file);
if plan.chain.len() > 1 {
let mut reader = tomoxide::io::open_dxchange(&file.to_string_lossy())?;
let mut geom = geometry_from_reader(reader.as_mut(), plan.center)?;
if let Some(deg) = lamino_angle {
use std::f32::consts::PI;
geom.beam = tomoxide::Beam::Laminography {
phi: PI / 2.0 + deg * PI / 180.0,
};
println!(" laminography: tilt={deg}° rh={lamino_rh:?}");
}
let ds = reader.read_all()?;
let vol = reconstruct_chain(ds, &geom, &plan, lamino_rh, &engine)?;
let mut writer = tomoxide::io::create_writer(&out, save_format)?;
let nz = vol.dims().0;
writer.reserve(nz)?;
writer.write_chunk(&vol, 0, nz)?;
} else if pipelines_well(&engine, plan.algo) && lamino_angle.is_none() {
let mut probe = tomoxide::io::open_dxchange(&file.to_string_lossy())?;
let (nproj, nz, nx, _nflat, _ndark) = probe.read_sizes()?;
drop(probe);
let (chunk, source) =
resolve_chunk(chunk, &file, &plan.algorithm, dtype, nx, nproj, nz);
println!(" chunk: {chunk} ({source})");
let devices = tomoxide::cuda::selected_devices();
let top_level = start_row.is_none() && end_row.is_none();
let shardable = engine.name() == "cuda"
&& matches!(save_format, tomoxide::io::SaveFormat::Tiff)
&& top_level
&& devices.len() > 1
&& nz > devices.len()
&& nx >= MULTI_GPU_MIN_NX;
if shardable {
run_sharded_subprocesses(&file, &plan, chunk, nz, &devices)?;
} else {
run_pipelined(
&file,
&out,
plan.algo,
plan.center,
dtype,
save_format,
chunk,
start_row,
end_row,
plan.filter,
plan.num_iter,
plan.reg_par.clone(),
plan.prep,
&engine,
)?;
}
} else {
let mut reader = tomoxide::io::open_dxchange(&file.to_string_lossy())?;
let mut geom = geometry_from_reader(reader.as_mut(), plan.center)?;
let mut params = recon_params(
&geom,
dtype,
plan.filter,
plan.num_iter,
plan.reg_par.clone(),
);
if let Some(deg) = lamino_angle {
use std::f32::consts::PI;
geom.beam = tomoxide::Beam::Laminography {
phi: PI / 2.0 + deg * PI / 180.0,
};
params.lamino_rh = lamino_rh;
println!(" laminography: tilt={deg}° rh={lamino_rh:?}");
}
let ds = reader.read_all()?;
let vol =
tomoxide::reconstruct(ds, &geom, plan.algo, ¶ms, &plan.prep, &engine)?;
let mut writer = tomoxide::io::create_writer(&out, save_format)?;
let nz = vol.dims().0;
writer.reserve(nz)?;
writer.write_chunk(&vol, 0, nz)?;
}
println!("wrote reconstruction to {out}");
}
Command::ReconSteps { common, chunk } => {
let (plan, cfg) = resolve(&common)?;
if plan.chain.len() > 1 {
return Err(anyhow!(
"algorithm chaining ({}) needs the whole prior volume to \
warm-start; it is supported only by `recon` (whole-volume), \
not the streaming `recon_steps`",
plan.algorithm
));
}
let engine = Engine::new(resolve_backend(&backend_flag, &cfg)?)?;
let file = common.file.clone();
let chunk = chunk.unwrap_or(cfg.nsino_per_chunk);
println!(
"recon_steps: file={} algorithm={:?} center={:?} dtype={} filter={} stripe={} phase={} chunk={} backend={}",
file.display(),
plan.algo,
plan.center,
plan.dtype.as_str(),
plan.filter_str,
plan.stripe_str,
plan.phase_str,
chunk,
engine.name()
);
log::debug!(
"resolved prep={:?} num_iter={} reg_par={:?}",
plan.prep,
plan.num_iter,
plan.reg_par
);
let out = recon_out_path(&file);
run_pipelined(
&file,
&out,
plan.algo,
plan.center,
plan.dtype,
plan.save_format,
chunk,
None,
None,
plan.filter,
plan.num_iter,
plan.reg_par,
plan.prep,
&engine,
)?;
println!("wrote streamed reconstruction to {out}");
}
Command::TuneChunk {
file,
algorithm,
center,
dtype,
} => {
let engine = Engine::new(backend_kind)?;
let algo: Algorithm = algorithm.parse().map_err(|e| anyhow!("{e}"))?;
let dtype: Dtype = dtype.parse().map_err(|e| anyhow!("{e}"))?;
if !pipelines_well(&engine, algo) {
return Err(anyhow!(
"tune_chunk applies only to CUDA pipelined algorithms \
(fbp, linerec, fourierrec, lprec); {:?} on backend {} uses the \
whole-volume path, where --chunk has no effect",
algo,
engine.name()
));
}
tune_chunk(&file, &algorithm, algo, center, dtype, &engine)?;
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn run_pipelined(
file: &Path,
out: &str,
algo: Algorithm,
center: Option<f32>,
dtype: Dtype,
save_format: tomoxide::io::SaveFormat,
chunk: usize,
start_row: Option<usize>,
end_row: Option<usize>,
filter: FilterName,
num_iter: usize,
reg_par: Vec<f32>,
prep: PrepOptions,
engine: &Engine,
) -> anyhow::Result<()> {
let path = file.to_string_lossy().into_owned();
let mut probe = tomoxide::io::open_dxchange(&path)?;
let geom = geometry_from_reader(probe.as_mut(), center)?;
drop(probe);
let params = recon_params(&geom, dtype, filter, num_iter, reg_par);
let read_path = path;
let write_path = out.to_string();
let z_start = start_row.unwrap_or(0);
let z_end = end_row.unwrap_or(usize::MAX);
tomoxide::ReconSteps::new(chunk).run_streaming_pipelined_range(
z_start,
z_end,
move || tomoxide::io::open_dxchange(&read_path),
move || tomoxide::io::create_writer(&write_path, save_format),
&geom,
algo,
¶ms,
&prep,
engine,
)?;
Ok(())
}
fn run_sharded_subprocesses(
file: &Path,
plan: &ReconPlan,
chunk: usize,
nz: usize,
devices: &[i32],
) -> anyhow::Result<()> {
let exe = std::env::current_exe().context("locating current executable")?;
let n = devices.len();
let base = nz / n;
let rem = nz % n;
println!(" multi-GPU: {n} shards across devices {devices:?}");
let mut children = Vec::with_capacity(n);
let mut z0 = 0usize;
for (i, &dev) in devices.iter().enumerate() {
let rows = base + if i < rem { 1 } else { 0 };
let z1 = z0 + rows;
let mut cmd = std::process::Command::new(&exe);
cmd.arg("--backend")
.arg("cuda")
.arg("recon")
.arg(file)
.arg("--algorithm")
.arg(&plan.algorithm)
.arg("--dtype")
.arg(plan.dtype.as_str())
.arg("--save_format")
.arg(&plan.save_format_str)
.arg("--filter")
.arg(&plan.filter_str)
.arg("--remove_stripe")
.arg(&plan.stripe_str)
.arg("--retrieve_phase")
.arg(&plan.phase_str)
.arg("--num_iter")
.arg(plan.num_iter.to_string())
.arg("--chunk")
.arg(chunk.to_string())
.arg("--start_row")
.arg(z0.to_string())
.arg("--end_row")
.arg(z1.to_string());
if !plan.reg_par.is_empty() {
let csv = plan
.reg_par
.iter()
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(",");
cmd.arg("--reg_par").arg(csv);
}
let sp = &plan.stripe_params;
match plan.stripe_str.to_ascii_lowercase().as_str() {
"fw" => {
cmd.arg("--fw_sigma").arg(sp.fw_sigma.to_string());
if let Some(l) = sp.fw_level {
cmd.arg("--fw_level").arg(l.to_string());
}
}
"ti" => {
cmd.arg("--ti_nblock")
.arg(sp.ti_nblock.to_string())
.arg("--ti_beta")
.arg(sp.ti_beta.to_string());
}
"sf" => {
cmd.arg("--sf_size").arg(sp.sf_size.to_string());
}
"vo-all" | "vo_all" | "voall" => {
cmd.arg("--vo_snr")
.arg(sp.vo_snr.to_string())
.arg("--vo_la_size")
.arg(sp.vo_la_size.to_string())
.arg("--vo_sm_size")
.arg(sp.vo_sm_size.to_string());
}
_ => {}
}
if !plan.phase_str.eq_ignore_ascii_case("none") {
let pp = &plan.phase_params;
cmd.arg("--pixel_size")
.arg(pp.pixel_size.to_string())
.arg("--propagation_distance")
.arg(pp.dist.to_string())
.arg("--energy")
.arg(pp.energy.to_string())
.arg("--alpha")
.arg(pp.alpha.to_string())
.arg("--db")
.arg(pp.db.to_string())
.arg("--w")
.arg(pp.w.to_string());
}
if let Some(c) = plan.center {
cmd.arg("--center").arg(c.to_string());
}
cmd.env("CUDA_VISIBLE_DEVICES", dev.to_string())
.env("TOMOXIDE_CUDA_DEVICES", "0")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped());
let child = cmd
.spawn()
.with_context(|| format!("spawning shard {i} on device {dev}"))?;
children.push((i, dev, z0, z1, child));
z0 = z1;
}
let mut failures = Vec::new();
for (i, dev, s, e, child) in children {
let output = child
.wait_with_output()
.with_context(|| format!("waiting for shard {i} on device {dev}"))?;
if !output.status.success() {
let reason = subprocess_failure_reason(&output);
failures.push(format!(
"shard {i} (device {dev}, rows [{s}, {e})): {reason}"
));
}
}
if !failures.is_empty() {
return Err(anyhow!(
"multi-GPU recon failed:\n {}",
failures.join("\n ")
));
}
Ok(())
}
fn resolve_chunk(
explicit: Option<usize>,
file: &Path,
algorithm: &str,
dtype: Dtype,
nx: usize,
nproj: usize,
nz: usize,
) -> (usize, &'static str) {
if let Some(c) = explicit {
return (c.max(1), "--chunk");
}
let gpu = tomoxide::cuda::device_name().unwrap_or_else(|| "unknown".into());
let key = chunk_cache::key(file, algorithm, dtype.as_str(), &gpu);
if let Some(c) = chunk_cache::ChunkCache::load().get(&key, nx, nproj, nz) {
return (c, "from cache");
}
(DEFAULT_PIPELINE_CHUNK, "default")
}
fn chunk_candidates(nz: usize) -> Vec<usize> {
let mut c = Vec::new();
let mut k = DEFAULT_PIPELINE_CHUNK;
while k <= nz / 2 {
c.push(k);
k *= 2;
}
if c.is_empty() {
c.push(nz.max(1));
}
c
}
fn tune_scratch_dir(file: &Path) -> PathBuf {
let parent = file
.parent()
.filter(|p| !p.as_os_str().is_empty())
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
parent.join(format!(".tomoxide_tune_{}", std::process::id()))
}
#[allow(clippy::too_many_arguments)]
fn measure_chunk_subprocess(
exe: &Path,
backend: &str,
link: &Path,
algorithm: &str,
center: Option<f32>,
dtype: Dtype,
chunk: usize,
) -> anyhow::Result<f64> {
let mut cmd = std::process::Command::new(exe);
cmd.arg("--backend")
.arg(backend)
.arg("recon")
.arg(link)
.arg("--algorithm")
.arg(algorithm)
.arg("--dtype")
.arg(dtype.as_str())
.arg("--chunk")
.arg(chunk.to_string());
if let Some(c) = center {
cmd.arg("--center").arg(c.to_string());
}
cmd.env("TOMOXIDE_CUDA_DEVICES", "0")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped());
let t = Instant::now();
let output = cmd
.output()
.with_context(|| format!("spawning {} recon", exe.display()))?;
let secs = t.elapsed().as_secs_f64();
if !output.status.success() {
return Err(anyhow!("{}", subprocess_failure_reason(&output)));
}
Ok(secs)
}
fn subprocess_failure_reason(output: &std::process::Output) -> String {
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
if let Some(sig) = output.status.signal() {
return format!("killed by signal {sig} (does not fit in device memory)");
}
}
let stderr = String::from_utf8_lossy(&output.stderr);
if let Some(e) = stderr
.lines()
.rev()
.find(|l| l.trim_start().starts_with("Error:"))
{
return e
.trim_start()
.trim_start_matches("Error:")
.trim()
.to_string();
}
stderr
.lines()
.rev()
.map(str::trim)
.find(|l| !l.is_empty() && !l.starts_with('['))
.unwrap_or("recon subprocess failed")
.to_string()
}
fn tune_chunk(
file: &Path,
algorithm: &str,
algo: Algorithm,
center: Option<f32>,
dtype: Dtype,
engine: &Engine,
) -> anyhow::Result<()> {
let mut probe = tomoxide::io::open_dxchange(&file.to_string_lossy())?;
let (nproj, nz, nx, _nflat, _ndark) = probe.read_sizes()?;
drop(probe);
let gpu = tomoxide::cuda::device_name().unwrap_or_else(|| "unknown".into());
let candidates = chunk_candidates(nz);
println!(
"tune_chunk: file={} algorithm={:?} dtype={} gpu={} dims=(nx={} nproj={} nz={})",
file.display(),
algo,
dtype.as_str(),
gpu,
nx,
nproj,
nz
);
println!(" candidates (powers of two, ≥2 chunks): {candidates:?}");
let exe = std::env::current_exe().context("locating the tomoxide executable")?;
let scratch = tune_scratch_dir(file);
std::fs::create_dir_all(&scratch)
.with_context(|| format!("creating scratch dir {}", scratch.display()))?;
let link = scratch.join("in.h5");
std::fs::hard_link(file, &link)
.with_context(|| format!("hard-linking {} -> {}", file.display(), link.display()))?;
let cand_out = recon_out_path(&link);
let mut results: Vec<(usize, f64)> = Vec::new();
for &c in &candidates {
match measure_chunk_subprocess(&exe, engine.name(), &link, algorithm, center, dtype, c) {
Ok(secs) => {
println!(" chunk={c:>4}: {secs:.2}s (wall, incl. process+CUDA init)");
results.push((c, secs));
}
Err(e) => println!(" chunk={c:>4}: skipped ({e})"),
}
let _ = std::fs::remove_dir_all(&cand_out);
let _ = std::fs::remove_file(&cand_out);
}
let _ = std::fs::remove_dir_all(&scratch);
let (best_chunk, best_secs) = results
.iter()
.copied()
.min_by(|a, b| a.1.total_cmp(&b.1))
.ok_or_else(|| anyhow!("all chunk candidates failed to run (see skip reasons above)"))?;
let key = chunk_cache::key(file, algorithm, dtype.as_str(), &gpu);
let mut cache = chunk_cache::ChunkCache::load();
cache.insert(
key,
chunk_cache::Entry {
chunk: best_chunk,
nx,
nproj,
nz,
},
);
cache.save()?;
println!(
" best chunk = {best_chunk} ({best_secs:.2}s) → cached to {}",
chunk_cache::CACHE_FILE
);
Ok(())
}
fn geometry_from_reader(
reader: &mut dyn DatasetReader,
center: Option<f32>,
) -> anyhow::Result<Geometry> {
let (_nproj, nz, nx, _nflat, _ndark) = reader.read_sizes()?;
let theta = reader.read_theta()?;
let mut geom = Geometry::parallel(Angles(theta), nx, nz, 1.0);
if let Some(c) = center {
geom.center = Center::Scalar(c);
}
Ok(geom)
}
fn recon_params(
geom: &Geometry,
dtype: Dtype,
filter_name: FilterName,
num_iter: usize,
reg_par: Vec<f32>,
) -> ReconParams {
ReconParams {
num_gridx: Some(geom.detector.width),
dtype,
filter_name,
num_iter,
reg_par,
..Default::default()
}
}
fn reconstruct_chain(
mut ds: tomoxide::Dataset<f32>,
geom: &Geometry,
plan: &ReconPlan,
lamino_rh: Option<usize>,
engine: &Engine,
) -> anyhow::Result<tomoxide::Volume<f32>> {
let backend = engine.backend();
tomoxide::prep::normalize_dataset(&mut ds, backend)?;
tomoxide::prep::retrieve_phase(&mut ds.data, plan.prep.phase, backend)?;
let mut sino = ds.data.to_layout(tomoxide::Layout::Sinogram);
tomoxide::prep::remove_stripe(&mut sino, plan.prep.stripe)?;
let n = plan.chain.len();
let mut init: Option<tomoxide::Volume<f32>> = None;
for (i, &algo) in plan.chain.iter().enumerate() {
let mut params = recon_params(
geom,
plan.dtype,
plan.filter,
plan.num_iter,
plan.reg_par.clone(),
);
params.lamino_rh = lamino_rh;
params.init = init.take();
let warm = params.init.is_some();
let vol = tomoxide::recon::recon(&sino, geom, algo, ¶ms, backend)?;
println!(
" chain [{}/{n}] {algo:?}{}",
i + 1,
if warm { " (warm-started)" } else { "" }
);
init = Some(vol);
}
init.ok_or_else(|| anyhow!("empty algorithm chain"))
}
fn recon_out_path(file: &Path) -> String {
format!("{}_rec", file.with_extension("").display())
}