From ea27690a3c1fa3e6b37d8d3ab3776eb0683fcc5a Mon Sep 17 00:00:00 2001 From: Dar Dahlen Date: Tue, 10 Feb 2026 17:37:52 -0800 Subject: [PATCH] Release v2.1.6 --- CHANGELOG.md | 2 +- Cargo.toml | 2 +- pyproject.toml | 2 +- src/kete/rust/flux/common.rs | 15 ++++++++---- src/kete/rust/flux/models.rs | 38 +++++++++++++++++++------------ src/kete/rust/flux/reflected.rs | 12 ++++++---- src/kete/rust/fovs/definitions.rs | 12 ++++++---- src/kete/rust/spice/mod.rs | 37 +++++++++++++++++++++++------- src/kete/rust/spice/spk.rs | 32 +++++++++++++++++++------- 9 files changed, 107 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 721ca16..66873fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [v2.1.6] ### Added diff --git a/Cargo.toml b/Cargo.toml index 6fc799b..9f5a203 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ members = ["src/kete_core", "src/kete_stats"] default-members = ["src/kete_core", "src/kete_stats"] [workspace.package] -version = "2.1.5" +version = "2.1.6" edition = "2024" rust-version = "1.90" license = "BSD-3-Clause" diff --git a/pyproject.toml b/pyproject.toml index e59ad62..a6e13e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kete" -version = "2.1.5" +version = "2.1.6" description = "Kete Asteroid Survey Tools" readme = "README.md" authors = [{name = "Dar Dahlen", email = "dardahlen@gmail.com"}, diff --git a/src/kete/rust/flux/common.rs b/src/kete/rust/flux/common.rs index 02a0044..0745d5c 100644 --- a/src/kete/rust/flux/common.rs +++ b/src/kete/rust/flux/common.rs @@ -244,8 +244,7 @@ pub fn neatm_thermal_py( Some(C_V), Some(v_albedo), Some(diameter), - ) - .unwrap(); + )?; let params = NeatmParams { obs_bands: vec![BandInfo::new(wavelength, 1.0, f64::NAN, None)], band_albedos: vec![0.0], @@ -253,7 +252,11 @@ pub fn neatm_thermal_py( emissivity, beaming, }; - Ok(params.apparent_thermal_flux(&sun2obj, &sun2obs).unwrap()[0]) + params.apparent_thermal_flux(&sun2obj, &sun2obs) + .and_then(|fluxes| fluxes.first().copied()) + .ok_or_else(|| pyo3::exceptions::PyValueError::new_err( + "Failed to compute thermal flux. Check input parameters." + )) } /// Calculate the flux from an object using the FRM thermal model in Jansky. @@ -315,7 +318,11 @@ pub fn frm_thermal_py( hg_params, emissivity, }; - Ok(params.apparent_thermal_flux(&sun2obj, &sun2obs).unwrap()[0]) + params.apparent_thermal_flux(&sun2obj, &sun2obs) + .and_then(|fluxes| fluxes.first().copied()) + .ok_or_else(|| pyo3::exceptions::PyValueError::new_err( + "Failed to compute thermal flux. Check input parameters." + )) } /// Given the M1/K1 and M2/K2 values, compute the apparent Comet visible magnitudes. diff --git a/src/kete/rust/flux/models.rs b/src/kete/rust/flux/models.rs index 10fdca5..d307dd5 100644 --- a/src/kete/rust/flux/models.rs +++ b/src/kete/rust/flux/models.rs @@ -320,7 +320,7 @@ impl PyNeatmParams { &self, sun2obj_vecs: Vec, sun2obs_vecs: Vec, - ) -> Vec { + ) -> PyResult> { sun2obj_vecs .into_par_iter() .zip(sun2obs_vecs) @@ -330,8 +330,10 @@ impl PyNeatmParams { self.0 .apparent_total_flux(&sun2obj, &sun2obs) - .unwrap() - .into() + .ok_or_else(|| pyo3::exceptions::PyValueError::new_err( + "Failed to compute flux. Ensure diameter and visible albedo are available." + )) + .map(|r| r.into()) }) .collect() } @@ -363,13 +365,13 @@ impl PyNeatmParams { /// Diameter of the object in km. #[getter] pub fn diam(&self) -> f64 { - self.0.hg_params.diam().unwrap() + self.0.hg_params.diam().unwrap_or(f64::NAN) } /// Albedo in V band. #[getter] pub fn vis_albedo(&self) -> f64 { - self.0.hg_params.vis_albedo().unwrap() + self.0.hg_params.vis_albedo().unwrap_or(f64::NAN) } /// Beaming parameter. @@ -611,7 +613,7 @@ impl PyFrmParams { &self, sun2obj_vecs: Vec, sun2obs_vecs: Vec, - ) -> Vec { + ) -> PyResult> { sun2obj_vecs .into_par_iter() .zip(sun2obs_vecs) @@ -621,8 +623,10 @@ impl PyFrmParams { self.0 .apparent_total_flux(&sun2obj, &sun2obs) - .unwrap() - .into() + .ok_or_else(|| pyo3::exceptions::PyValueError::new_err( + "Failed to compute flux. Ensure diameter and visible albedo are available." + )) + .map(|r| r.into()) }) .collect() } @@ -653,14 +657,20 @@ impl PyFrmParams { /// Diameter of the object in km. #[getter] - pub fn diam(&self) -> f64 { - self.0.hg_params.diam().unwrap() + pub fn diam(&self) -> PyResult { + self.0.hg_params.diam() + .ok_or_else(|| pyo3::exceptions::PyValueError::new_err( + "Diameter is not available for this object. Provide diameter or (h_mag + vis_albedo) to compute it." + )) } /// Albedo in V band. #[getter] - pub fn vis_albedo(&self) -> f64 { - self.0.hg_params.vis_albedo().unwrap() + pub fn vis_albedo(&self) -> PyResult { + self.0.hg_params.vis_albedo() + .ok_or_else(|| pyo3::exceptions::PyValueError::new_err( + "Visible albedo is not available for this object. Provide vis_albedo or (h_mag + diameter) to compute it." + )) } /// G Phase parameter. @@ -689,8 +699,8 @@ impl PyFrmParams { self.band_wavelength(), self.band_albedos(), self.h_mag(), - self.diam(), - self.vis_albedo(), + self.diam().ok(), + self.vis_albedo().ok(), self.g_param(), self.emissivity(), self.zero_mags(), diff --git a/src/kete/rust/flux/reflected.rs b/src/kete/rust/flux/reflected.rs index 3063585..f6af7c6 100644 --- a/src/kete/rust/flux/reflected.rs +++ b/src/kete/rust/flux/reflected.rs @@ -3,6 +3,7 @@ use kete_core::constants; use kete_core::flux::HGParams; use kete_core::flux::cometary_dust_phase_curve_correction; use kete_core::flux::hg_phase_curve_correction; +use pyo3::PyResult; use pyo3::pyfunction; /// This computes the phase curve correction in the IAU format. @@ -102,7 +103,7 @@ pub fn hg_apparent_flux_py( h_mag: Option, diameter: Option, c_hg: Option, -) -> f64 { +) -> PyResult { let c_hg = c_hg.unwrap_or(constants::C_V); let sun2obj = sun2obj.into_vector(PyFrames::Equatorial).into(); let sun2obs = sun2obs.into_vector(PyFrames::Equatorial).into(); @@ -113,11 +114,14 @@ pub fn hg_apparent_flux_py( Some(c_hg), Some(v_albedo), diameter, - ) - .unwrap(); + )?; params .apparent_flux(&sun2obj, &sun2obs, wavelength, v_albedo) - .unwrap() + .ok_or_else(|| { + pyo3::exceptions::PyValueError::new_err( + "Failed to compute apparent flux. Ensure h_mag or diameter is provided.", + ) + }) } /// Compute the apparent magnitude of an object using the absolute magnitude H, G, and diff --git a/src/kete/rust/fovs/definitions.rs b/src/kete/rust/fovs/definitions.rs index e31bbc8..e926443 100644 --- a/src/kete/rust/fovs/definitions.rs +++ b/src/kete/rust/fovs/definitions.rs @@ -1044,8 +1044,10 @@ impl PyZtfField { /// List containing all of the CCD FOVs. /// These must have matching metadata. #[new] - pub fn new(ztf_ccd_fields: Vec) -> Self { - PyZtfField(fov::ZtfField::new(ztf_ccd_fields.into_iter().map(|x| x.0).collect()).unwrap()) + pub fn new(ztf_ccd_fields: Vec) -> PyResult { + Ok(PyZtfField(fov::ZtfField::new( + ztf_ccd_fields.into_iter().map(|x| x.0).collect(), + )?)) } /// State of the observer for this FOV. @@ -1270,8 +1272,10 @@ impl PyPtfField { /// List containing all of the CCD FOVs. /// These must have matching metadata. #[new] - pub fn new(ptf_ccd_fields: Vec) -> Self { - PyPtfField(fov::PtfField::new(ptf_ccd_fields.into_iter().map(|x| x.0).collect()).unwrap()) + pub fn new(ptf_ccd_fields: Vec) -> PyResult { + Ok(PyPtfField(fov::PtfField::new( + ptf_ccd_fields.into_iter().map(|x| x.0).collect(), + )?)) } /// State of the observer for this FOV. diff --git a/src/kete/rust/spice/mod.rs b/src/kete/rust/spice/mod.rs index 17714b5..e9554b2 100644 --- a/src/kete/rust/spice/mod.rs +++ b/src/kete/rust/spice/mod.rs @@ -87,8 +87,12 @@ pub fn find_obs_code_py(name: &str) -> PyResult<(f64, f64, f64, String, String)> /// Predict the state of an object in earths orbit from the two line elements #[pyfunction] -pub fn predict_tle(line1: String, line2: String, time: PyTime) -> ([f64; 3], [f64; 3]) { - let elements = sgp4::Elements::from_tle(None, line1.as_bytes(), line2.as_bytes()).unwrap(); +pub fn predict_tle(line1: String, line2: String, time: PyTime) -> PyResult<([f64; 3], [f64; 3])> { + let elements = + sgp4::Elements::from_tle(None, line1.as_bytes(), line2.as_bytes()).map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Invalid TLE format: {}", e)) + })?; + let orbit = sgp4::Orbit::from_kozai_elements( &sgp4::WGS84, elements.inclination * (core::f64::consts::PI / 180.0), @@ -98,7 +102,9 @@ pub fn predict_tle(line1: String, line2: String, time: PyTime) -> ([f64; 3], [f6 elements.mean_anomaly * (core::f64::consts::PI / 180.0), elements.mean_motion * (core::f64::consts::PI / 720.0), ) - .expect("Failed to load orbit values"); + .map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Failed to create orbit from TLE: {}", e)) + })?; let constants = Constants::new( sgp4::WGS84, @@ -107,14 +113,29 @@ pub fn predict_tle(line1: String, line2: String, time: PyTime) -> ([f64; 3], [f6 elements.drag_term, orbit, ) - .expect("Failed to load orbit values"); + .map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!( + "Failed to initialize SGP4 constants: {}", + e + )) + })?; let time = time.0.utc(); + let naive_time = time + .to_datetime() + .map_err(|_| pyo3::exceptions::PyValueError::new_err("Failed to convert time to datetime"))? + .naive_utc(); + let min_diff = elements - .datetime_to_minutes_since_epoch(&time.to_datetime().unwrap().naive_utc()) - .unwrap(); + .datetime_to_minutes_since_epoch(&naive_time) + .map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Failed to convert time: {}", e)) + })?; + + let state = constants.propagate(min_diff).map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Failed to propagate TLE: {}", e)) + })?; - let state = constants.propagate(min_diff).expect("Failed to propagate"); - (state.position, state.velocity) + Ok((state.position, state.velocity)) } diff --git a/src/kete/rust/spice/spk.rs b/src/kete/rust/spice/spk.rs index 3db2e81..8639ea1 100644 --- a/src/kete/rust/spice/spk.rs +++ b/src/kete/rust/spice/spk.rs @@ -70,29 +70,45 @@ pub fn spk_loaded_objects_py() -> Vec { /// Reset the contents of the SPK shared memory. #[pyfunction] #[pyo3(name = "spk_reset")] -pub fn spk_reset_py() { - LOADED_SPK.write().unwrap().reset() +pub fn spk_reset_py() -> PyResult<()> { + LOADED_SPK + .write() + .map_err(|_| pyo3::exceptions::PyRuntimeError::new_err("SPK lock poisoned"))? + .reset(); + Ok(()) } /// Reload the core SPK files. #[pyfunction] #[pyo3(name = "spk_load_core")] -pub fn spk_load_core_py() { - LOADED_SPK.write().unwrap().load_core().unwrap() +pub fn spk_load_core_py() -> PyResult<()> { + LOADED_SPK + .write() + .map_err(|_| pyo3::exceptions::PyRuntimeError::new_err("SPK lock poisoned"))? + .load_core()?; + Ok(()) } /// Reload the core PCK files. #[pyfunction] #[pyo3(name = "pck_load_core")] -pub fn pck_load_core_py() { - LOADED_PCK.write().unwrap().load_core().unwrap() +pub fn pck_load_core_py() -> PyResult<()> { + LOADED_PCK + .write() + .map_err(|_| pyo3::exceptions::PyRuntimeError::new_err("PCK lock poisoned"))? + .load_core()?; + Ok(()) } /// Reload the cache SPK files. #[pyfunction] #[pyo3(name = "spk_load_cache")] -pub fn spk_load_cache_py() { - LOADED_SPK.write().unwrap().load_cache().unwrap() +pub fn spk_load_cache_py() -> PyResult<()> { + LOADED_SPK + .write() + .map_err(|_| pyo3::exceptions::PyRuntimeError::new_err("SPK lock poisoned"))? + .load_cache()?; + Ok(()) } /// Calculates the :class:`~kete.State` of the target object at the