From bc2e5fb5b7e9fb63f150657cac5f40f75871db2d Mon Sep 17 00:00:00 2001 From: Ryan Berger Date: Tue, 10 Feb 2026 23:00:57 -0800 Subject: [PATCH] feat: add angle subdivision cli option We currently only calculate 360 different lines of sight with our azimuthal projection. This can potentially lead to a lack of coverage, especially in very long lines of sight. This adds a command line option --angle-subdivisions which allows for a user-configured angle subdivision to get coverage up to .01 of a degree --- crates/total-viewsheds/src/config.rs | 4 ++++ crates/total-viewsheds/src/main.rs | 14 ++++++++++++++ crates/total-viewsheds/src/run/compute.rs | 5 ++++- crates/total-viewsheds/src/run/parallel.rs | 14 ++++++++++---- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/crates/total-viewsheds/src/config.rs b/crates/total-viewsheds/src/config.rs index 034ba2d..4bf7fac 100644 --- a/crates/total-viewsheds/src/config.rs +++ b/crates/total-viewsheds/src/config.rs @@ -112,6 +112,10 @@ pub struct Compute { /// Controls line of sight and total viewshed image generation #[arg(long, value_name = "render image", default_value = "false")] pub disable_image_render: bool, + + /// Subdivides 360 degrees into a `angle_subdivisions` number of subdivisions + #[arg(long, value_name = "number of angle divisions", default_value = "1")] + pub angle_subdivisions: u8, } #[derive(clap::Parser, Debug)] diff --git a/crates/total-viewsheds/src/main.rs b/crates/total-viewsheds/src/main.rs index a271f52..d77ef99 100644 --- a/crates/total-viewsheds/src/main.rs +++ b/crates/total-viewsheds/src/main.rs @@ -103,6 +103,19 @@ fn setup_logging() -> Result<()> { /// Run computations fn compute(config: &config::Compute) -> Result<()> { + // for now, only the CPU backend knows how to interpret more than one angle division + if !matches!(config.backend, config::Backend::CPU) && config.angle_subdivisions > 1 { + color_eyre::eyre::bail!( + "An angle division higher than 1 is only supported on the `cpu` backend" + ) + } + + // we can't subdivide rotation into more than 182 rotations or else we overflow the u16 + // we use for angles, so 100 is an artificial limit + if config.angle_subdivisions > 100 { + color_eyre::eyre::bail!("The maximum angle divisions is 100") + } + let tile = bt::BinaryTerrain::read(&config.input)?; let scale = config.scale.unwrap_or_else(|| tile.scale()); @@ -142,6 +155,7 @@ fn compute(config: &config::Compute) -> Result<()> { refraction: config.refraction, thread_count: config.thread_count, disable_render_image: config.disable_image_render, + angle_subdivisions: config.angle_subdivisions, }; let mut compute = run::compute::Compute::new(compute_config, &mut dem)?; compute.run()?; diff --git a/crates/total-viewsheds/src/run/compute.rs b/crates/total-viewsheds/src/run/compute.rs index 106aa16..42dbf84 100644 --- a/crates/total-viewsheds/src/run/compute.rs +++ b/crates/total-viewsheds/src/run/compute.rs @@ -43,12 +43,14 @@ pub struct Config { pub rings_per_km: f32, /// How to normalise the heatmap data. pub heatmap: crate::config::HeatmapNormalisation, - /// Refractoin coefficient + /// Refraction coefficient pub refraction: f32, /// Number of threads for computation pub thread_count: usize, /// Disables the rendering of PNG images (good for long runs) pub disable_render_image: bool, + /// Subdivides 360 degrees into a `angle_subdivisions` number of subdivisions + pub angle_subdivisions: u8, } impl<'compute> Compute<'compute> { @@ -320,6 +322,7 @@ pub mod test { refraction: refraction_override.unwrap_or(0.13f32), thread_count: 1, // single thread it for consistency disable_render_image: false, + angle_subdivisions: 1, }; let mut compute = Compute::new(config, dem).unwrap(); diff --git a/crates/total-viewsheds/src/run/parallel.rs b/crates/total-viewsheds/src/run/parallel.rs index ef80456..4d4d8dd 100644 --- a/crates/total-viewsheds/src/run/parallel.rs +++ b/crates/total-viewsheds/src/run/parallel.rs @@ -43,25 +43,31 @@ impl super::compute::Compute<'_> { let refraction = self.config.refraction; let scale = self.config.scale; let observer_height = self.config.observer_height; + let angle_subdivisions = u16::from(self.config.angle_subdivisions); pool.install(move || { - (0u16..360u16) + (0u16..360u16 * angle_subdivisions) .into_par_iter() .map(|angle| { let start = std::time::Instant::now(); - tracing::info!("starting angle: {angle}"); + tracing::info!( + "starting angle: {}", + f32::from(angle) / f32::from(angle_subdivisions) + ); let output = crate::cpu::kernel( elevations, max_los, - f32::from(angle), + f32::from(angle) / f32::from(angle_subdivisions), refraction, scale, observer_height, ); tracing::info!("finished angle in {:?}", start.elapsed()); - (angle, output) + + #[expect(clippy::integer_division, reason = "truncating the angle is fine")] + (angle / angle_subdivisions, output) }) .for_each(|(angle, output)| { let result = accumulating.handle_parallel_per_angle_output(angle, output);