#![allow(unsafe_op_in_unsafe_fn)]
use numpy::{IntoPyArray, PyReadonlyArray2};
use pyo3::exceptions::{PyUserWarning, PyValueError};
use pyo3::prelude::*;
use pyo3::types::PyType;
#[cfg(feature = "stubgen")]
use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pyfunction, gen_stub_pymethods};
use crate::beam::{Beam, gauss_factor as rust_gauss_factor};
use crate::common_beam::common_beam as rust_common_beam;
use crate::convolve_uv::FftFloat;
use crate::smooth::{BrightnessUnit, smooth as rust_smooth};
fn value_error(e: impl std::fmt::Display) -> PyErr {
PyValueError::new_err(e.to_string())
}
#[cfg_attr(feature = "stubgen", gen_stub_pyclass)]
#[pyclass(name = "Beam", subclass)]
#[derive(Clone)]
pub struct PyBeam {
pub inner: Beam,
}
#[cfg_attr(feature = "stubgen", gen_stub_pymethods)]
#[pymethods]
impl PyBeam {
#[new]
fn new(major_deg: f64, minor_deg: f64, pa_deg: f64) -> PyResult<Self> {
Beam::new(major_deg, minor_deg, pa_deg)
.map(|inner| Self { inner })
.map_err(value_error)
}
#[classmethod]
fn from_arcsec<'py>(
cls: &Bound<'py, PyType>,
major_arcsec: f64,
minor_arcsec: f64,
pa_deg: f64,
) -> PyResult<Bound<'py, PyBeam>> {
let beam = Beam::from_arcsec(major_arcsec, minor_arcsec, pa_deg).map_err(value_error)?;
new_instance(cls, beam)
}
#[getter]
fn major_deg(&self) -> f64 {
self.inner.major_deg()
}
#[getter]
fn minor_deg(&self) -> f64 {
self.inner.minor_deg()
}
#[getter]
fn pa_deg(&self) -> f64 {
self.inner.pa_deg()
}
#[getter]
fn major_arcsec(&self) -> f64 {
self.inner.major_arcsec()
}
#[getter]
fn minor_arcsec(&self) -> f64 {
self.inner.minor_arcsec()
}
fn area_sr(&self) -> f64 {
self.inner.area_sr()
}
fn deconvolve<'py>(slf: &Bound<'py, Self>, other: &PyBeam) -> PyResult<Bound<'py, PyBeam>> {
let beam = slf
.borrow()
.inner
.deconvolve(&other.inner)
.map_err(value_error)?;
new_instance(&slf.get_type(), beam)
}
fn convolve<'py>(slf: &Bound<'py, Self>, other: &PyBeam) -> PyResult<Bound<'py, PyBeam>> {
let beam = slf.borrow().inner.convolve(&other.inner);
new_instance(&slf.get_type(), beam)
}
fn __repr__(&self) -> String {
format!(
"Beam(major_deg={}, minor_deg={}, pa_deg={})",
self.inner.major_deg(),
self.inner.minor_deg(),
self.inner.pa_deg()
)
}
fn __str__(&self) -> String {
self.inner.to_string()
}
}
fn new_instance<'py>(cls: &Bound<'py, PyType>, beam: Beam) -> PyResult<Bound<'py, PyBeam>> {
Ok(cls
.call1((beam.major_deg(), beam.minor_deg(), beam.pa_deg()))?
.downcast_into::<PyBeam>()?)
}
#[cfg_attr(feature = "stubgen", gen_stub_pyfunction)]
#[pyfunction]
#[pyo3(signature = (beams, tolerance=1e-4, nsamps=200, epsilon=5e-4))]
fn common_beam<'py>(
beams: Vec<Bound<'py, PyBeam>>,
tolerance: f64,
nsamps: usize,
epsilon: f64,
) -> PyResult<Bound<'py, PyBeam>> {
let rust_beams: Vec<Beam> = beams.iter().map(|b| b.borrow().inner).collect();
let beam = rust_common_beam(&rust_beams, tolerance, nsamps, epsilon).map_err(value_error)?;
new_instance(&beams[0].get_type(), beam)
}
fn resolve_unit(py: Python<'_>, bunit: Option<&str>) -> PyResult<BrightnessUnit> {
let Some(s) = bunit else {
return Ok(BrightnessUnit::default());
};
if let Some(unit) = BrightnessUnit::parse(s) {
return Ok(unit);
}
let msg = std::ffi::CString::new(BrightnessUnit::unrecognised_message(s))?;
PyErr::warn(py, &py.get_type::<PyUserWarning>(), &msg, 2)?;
Ok(BrightnessUnit::JyPerBeam)
}
fn smooth_typed<'py, T: FftFloat + numpy::Element>(
py: Python<'py>,
arr: PyReadonlyArray2<'py, T>,
beams: (&Beam, &Beam),
(dx_deg, dy_deg): (f64, f64),
unit: BrightnessUnit,
) -> PyResult<Bound<'py, PyAny>> {
let view = arr.as_array();
let out = py
.detach(|| rust_smooth(view, beams.0, beams.1, dx_deg, dy_deg, unit, None))
.map_err(value_error)?;
Ok(out.into_pyarray(py).into_any())
}
fn native_byte_order<'py>(image: &Bound<'py, PyAny>) -> PyResult<Bound<'py, PyAny>> {
let Ok(dtype) = image.getattr("dtype") else {
return Ok(image.clone());
};
if dtype.getattr("isnative")?.is_truthy()? {
return Ok(image.clone());
}
let native = dtype.call_method1("newbyteorder", ("=",))?;
image.call_method1("astype", (native,))
}
#[cfg_attr(feature = "stubgen", gen_stub_pyfunction)]
#[pyfunction]
#[pyo3(signature = (image, old_beam, new_beam, dx_deg, dy_deg, cutoff_arcsec=None, bunit=None))]
#[allow(clippy::too_many_arguments)]
fn smooth<'py>(
py: Python<'py>,
image: &Bound<'py, PyAny>,
old_beam: &PyBeam,
new_beam: &PyBeam,
dx_deg: f64,
dy_deg: f64,
cutoff_arcsec: Option<f64>,
bunit: Option<&str>,
) -> PyResult<Bound<'py, PyAny>> {
let bmaj = old_beam.inner.major_arcsec();
if let Some(cutoff) = cutoff_arcsec
&& bmaj > cutoff
{
return Err(PyValueError::new_err(format!(
"old_beam BMAJ ({bmaj:.2}\") exceeds the cutoff ({cutoff}\")"
)));
}
let unit = resolve_unit(py, bunit)?;
let image = &native_byte_order(image)?;
let beams = (&old_beam.inner, &new_beam.inner);
if let Ok(arr) = image.extract::<PyReadonlyArray2<f32>>() {
return smooth_typed(py, arr, beams, (dx_deg, dy_deg), unit);
}
if let Ok(arr) = image.extract::<PyReadonlyArray2<f64>>() {
return smooth_typed(py, arr, beams, (dx_deg, dy_deg), unit);
}
Err(PyValueError::new_err(
"image must be a 2-D float32 or float64 numpy array",
))
}
#[cfg_attr(feature = "stubgen", gen_stub_pyfunction)]
#[pyfunction]
fn gauss_factor(
conv_beam: &PyBeam,
orig_beam: &PyBeam,
dx_arcsec: f64,
dy_arcsec: f64,
) -> (f64, f64, f64, f64, f64) {
let fac = rust_gauss_factor(&conv_beam.inner, &orig_beam.inner, dx_arcsec, dy_arcsec);
let amp = dx_arcsec.abs() * dy_arcsec.abs() / fac;
let out = orig_beam.inner.convolve(&conv_beam.inner);
(
fac,
amp,
out.major_arcsec(),
out.minor_arcsec(),
out.pa_deg(),
)
}
#[cfg(feature = "stubgen")]
pyo3_stub_gen::define_stub_info_gatherer!(stub_info);
#[cfg(feature = "stubgen")]
#[pyfunction]
fn _generate_stubs() -> PyResult<()> {
if std::env::var("CARGO_MANIFEST_DIR").is_err() {
let cwd = std::env::current_dir().map_err(value_error)?;
#[allow(unused_unsafe)]
unsafe {
std::env::set_var("CARGO_MANIFEST_DIR", cwd);
}
}
stub_info()
.and_then(|s| s.generate())
.map_err(|e| PyValueError::new_err(format!("stub generation failed: {e}")))
}
#[pymodule]
pub fn _convolve_rs(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyBeam>()?;
m.add_function(wrap_pyfunction!(common_beam, m)?)?;
m.add_function(wrap_pyfunction!(smooth, m)?)?;
m.add_function(wrap_pyfunction!(gauss_factor, m)?)?;
#[cfg(feature = "stubgen")]
m.add_function(wrap_pyfunction!(_generate_stubs, m)?)?;
Ok(())
}