use pyo3::prelude::*;
use numpy::{PyArray1, PyArray2, PyReadonlyArray1, PyReadonlyArray2, PyReadwriteArray1};
pub mod core;
pub mod propagators;
pub mod coordinates;
pub mod maneuvers;
pub mod satellite;
pub mod utils;
#[cfg(test)]
pub mod test_utils;
pub use core::{PoliastroError, PoliastroResult};
#[pymodule]
fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
let constants = PyModule::new_bound(m.py(), "constants")?;
add_constants(&constants)?;
m.add_submodule(&constants)?;
let numpy_ops = PyModule::new_bound(m.py(), "numpy_ops")?;
add_numpy_ops(&numpy_ops)?;
m.add_submodule(&numpy_ops)?;
m.add_class::<core::state::CartesianState>()?;
m.add_class::<core::time::Epoch>()?;
m.add_class::<core::time::Duration>()?;
m.add_class::<core::elements::OrbitalElements>()?;
m.add_class::<core::elements::EquinoctialElements>()?;
m.add_class::<coordinates::frames::ICRS>()?;
m.add_class::<coordinates::frames::GCRS>()?;
m.add_class::<coordinates::frames::J2000>()?;
m.add_class::<coordinates::frames::ITRS>()?;
m.add_class::<coordinates::frames::TEME>()?;
m.add_class::<coordinates::frames::Perifocal>()?;
m.add_function(wrap_pyfunction!(py_rv_to_coe, m)?)?;
m.add_function(wrap_pyfunction!(py_coe_to_rv, m)?)?;
m.add_function(wrap_pyfunction!(py_coe_to_equinoctial, m)?)?;
m.add_function(wrap_pyfunction!(py_equinoctial_to_coe, m)?)?;
m.add_function(wrap_pyfunction!(py_rv_to_equinoctial, m)?)?;
m.add_function(wrap_pyfunction!(py_equinoctial_to_rv, m)?)?;
m.add_function(wrap_pyfunction!(py_mean_to_eccentric_anomaly, m)?)?;
m.add_function(wrap_pyfunction!(py_eccentric_to_mean_anomaly, m)?)?;
m.add_function(wrap_pyfunction!(py_eccentric_to_true_anomaly, m)?)?;
m.add_function(wrap_pyfunction!(py_true_to_eccentric_anomaly, m)?)?;
m.add_function(wrap_pyfunction!(py_mean_to_true_anomaly, m)?)?;
m.add_function(wrap_pyfunction!(py_true_to_mean_anomaly, m)?)?;
m.add_function(wrap_pyfunction!(py_mean_to_hyperbolic_anomaly, m)?)?;
m.add_function(wrap_pyfunction!(py_hyperbolic_to_mean_anomaly, m)?)?;
m.add_function(wrap_pyfunction!(py_hyperbolic_to_true_anomaly, m)?)?;
m.add_function(wrap_pyfunction!(py_true_to_hyperbolic_anomaly, m)?)?;
m.add_function(wrap_pyfunction!(py_mean_to_true_anomaly_hyperbolic, m)?)?;
m.add_function(wrap_pyfunction!(py_true_to_mean_anomaly_hyperbolic, m)?)?;
m.add_function(wrap_pyfunction!(py_mean_to_true_anomaly_parabolic, m)?)?;
m.add_function(wrap_pyfunction!(py_true_to_mean_anomaly_parabolic, m)?)?;
m.add_function(wrap_pyfunction!(py_batch_mean_to_eccentric_anomaly, m)?)?;
m.add_function(wrap_pyfunction!(py_batch_mean_to_true_anomaly, m)?)?;
m.add_function(wrap_pyfunction!(py_batch_true_to_mean_anomaly, m)?)?;
m.add_function(wrap_pyfunction!(py_batch_mean_to_hyperbolic_anomaly, m)?)?;
m.add_function(wrap_pyfunction!(py_batch_mean_to_true_anomaly_hyperbolic, m)?)?;
m.add_function(wrap_pyfunction!(py_batch_mean_to_true_anomaly_parabolic, m)?)?;
m.add_function(wrap_pyfunction!(py_propagate_keplerian, m)?)?;
m.add_function(wrap_pyfunction!(py_propagate_keplerian_duration, m)?)?;
m.add_function(wrap_pyfunction!(py_propagate_state_keplerian, m)?)?;
m.add_function(wrap_pyfunction!(py_propagate_lagrange, m)?)?;
m.add_function(wrap_pyfunction!(py_batch_propagate_states, m)?)?;
m.add_function(wrap_pyfunction!(py_batch_propagate_lagrange, m)?)?;
m.add_function(wrap_pyfunction!(py_j2_perturbation, m)?)?;
m.add_function(wrap_pyfunction!(py_propagate_j2_rk4, m)?)?;
m.add_function(wrap_pyfunction!(py_propagate_j2_dopri5, m)?)?;
m.add_function(wrap_pyfunction!(py_propagate_j2_dop853, m)?)?;
m.add_function(wrap_pyfunction!(py_propagate_stm_rk4, m)?)?;
m.add_function(wrap_pyfunction!(py_propagate_stm_dopri5, m)?)?;
m.add_function(wrap_pyfunction!(py_propagate_stm_j2_rk4, m)?)?;
m.add_function(wrap_pyfunction!(py_propagate_j2_rk4_static, m)?)?;
m.add_function(wrap_pyfunction!(py_propagate_j2_j3_j4_rk4_static, m)?)?;
m.add_function(wrap_pyfunction!(py_exponential_density, m)?)?;
m.add_function(wrap_pyfunction!(py_drag_acceleration, m)?)?;
m.add_function(wrap_pyfunction!(py_propagate_drag_rk4, m)?)?;
m.add_function(wrap_pyfunction!(py_propagate_drag_dopri5, m)?)?;
m.add_function(wrap_pyfunction!(py_propagate_j2_drag_rk4, m)?)?;
m.add_function(wrap_pyfunction!(py_propagate_j2_drag_dopri5, m)?)?;
m.add_function(wrap_pyfunction!(py_sun_position_simple, m)?)?;
m.add_function(wrap_pyfunction!(py_moon_position_simple, m)?)?;
m.add_function(wrap_pyfunction!(py_third_body_perturbation, m)?)?;
m.add_function(wrap_pyfunction!(py_sun_moon_perturbation, m)?)?;
m.add_function(wrap_pyfunction!(py_propagate_thirdbody_rk4, m)?)?;
m.add_function(wrap_pyfunction!(py_propagate_thirdbody_dopri5, m)?)?;
m.add_function(wrap_pyfunction!(py_shadow_function, m)?)?;
m.add_function(wrap_pyfunction!(py_srp_acceleration, m)?)?;
m.add_function(wrap_pyfunction!(py_propagate_srp_rk4, m)?)?;
m.add_function(wrap_pyfunction!(py_propagate_srp_dopri5, m)?)?;
m.add_function(wrap_pyfunction!(py_batch_gcrs_to_itrs, m)?)?;
m.add_function(wrap_pyfunction!(py_batch_itrs_to_gcrs, m)?)?;
m.add_function(wrap_pyfunction!(py_batch_gcrs_to_teme, m)?)?;
m.add_function(wrap_pyfunction!(py_batch_teme_to_gcrs, m)?)?;
m.add_function(wrap_pyfunction!(py_batch_teme_to_itrs, m)?)?;
m.add_function(wrap_pyfunction!(py_batch_itrs_to_teme, m)?)?;
m.add_function(wrap_pyfunction!(py_hohmann_transfer, m)?)?;
m.add_function(wrap_pyfunction!(py_hohmann_phase_angle, m)?)?;
m.add_function(wrap_pyfunction!(py_hohmann_synodic_period, m)?)?;
m.add_function(wrap_pyfunction!(py_hohmann_time_to_window, m)?)?;
m.add_function(wrap_pyfunction!(py_bielliptic_transfer, m)?)?;
m.add_function(wrap_pyfunction!(py_compare_bielliptic_hohmann, m)?)?;
m.add_function(wrap_pyfunction!(py_find_optimal_bielliptic, m)?)?;
m.add_function(wrap_pyfunction!(py_pure_plane_change, m)?)?;
m.add_function(wrap_pyfunction!(py_combined_plane_change, m)?)?;
m.add_function(wrap_pyfunction!(py_optimal_plane_change_location, m)?)?;
m.add_function(wrap_pyfunction!(py_phasing_orbit, m)?)?;
m.add_function(wrap_pyfunction!(py_coorbital_rendezvous, m)?)?;
m.add_function(wrap_pyfunction!(py_coplanar_rendezvous, m)?)?;
m.add_function(wrap_pyfunction!(py_delta_v_budget, m)?)?;
m.add_function(wrap_pyfunction!(py_gravity_assist, m)?)?;
m.add_function(wrap_pyfunction!(py_periapsis_from_b_parameter, m)?)?;
m.add_function(wrap_pyfunction!(py_lambert_solve, m)?)?;
m.add_function(wrap_pyfunction!(py_lambert_solve_batch, m)?)?;
m.add_function(wrap_pyfunction!(py_lambert_solve_batch_parallel, m)?)?;
m.add_function(wrap_pyfunction!(py_propagate_tle, m)?)?;
m.add_function(wrap_pyfunction!(py_propagate_tle_batch, m)?)?;
m.add_function(wrap_pyfunction!(py_propagate_omm, m)?)?;
m.add_function(wrap_pyfunction!(py_compute_azimuth_elevation, m)?)?;
m.add_function(wrap_pyfunction!(py_compute_azimuth_elevation_rate, m)?)?;
m.add_function(wrap_pyfunction!(py_is_visible, m)?)?;
m.add_function(wrap_pyfunction!(py_find_satellite_passes, m)?)?;
m.add_function(wrap_pyfunction!(py_ecef_to_geodetic, m)?)?;
m.add_function(wrap_pyfunction!(py_compute_ground_track, m)?)?;
m.add_function(wrap_pyfunction!(py_maximum_ground_range, m)?)?;
m.add_function(wrap_pyfunction!(py_calculate_swath_width, m)?)?;
m.add_function(wrap_pyfunction!(py_visibility_circle, m)?)?;
m.add_function(wrap_pyfunction!(py_coverage_area, m)?)?;
m.add_function(wrap_pyfunction!(py_coverage_percentage, m)?)?;
m.add_function(wrap_pyfunction!(py_compute_eclipse_state, m)?)?;
m.add_function(wrap_pyfunction!(py_solar_beta_angle, m)?)?;
m.add_function(wrap_pyfunction!(py_solar_beta_angle_precise, m)?)?;
m.add_function(wrap_pyfunction!(py_sun_synchronous_inclination, m)?)?;
m.add_function(wrap_pyfunction!(py_eclipse_duration, m)?)?;
m.add_function(wrap_pyfunction!(py_estimate_satellite_lifetime, m)?)?;
m.add_function(wrap_pyfunction!(py_estimate_decay_rate, m)?)?;
m.add_function(wrap_pyfunction!(py_compute_conjunction, m)?)?;
m.add_function(wrap_pyfunction!(py_closest_approach_distance, m)?)?;
Ok(())
}
fn add_constants(m: &Bound<'_, PyModule>) -> PyResult<()> {
use crate::core::constants::*;
m.add("G", G)?;
m.add("C", C)?;
m.add("AU", AU)?;
m.add("GM_SUN", GM_SUN)?;
m.add("R_SUN", R_SUN)?;
m.add("R_MEAN_SUN", R_MEAN_SUN)?;
m.add("J2_SUN", J2_SUN)?;
m.add("SOLAR_IRRADIANCE", SOLAR_IRRADIANCE)?;
m.add("SOLAR_RADIATION_PRESSURE", SOLAR_RADIATION_PRESSURE)?;
m.add("GM_MERCURY", GM_MERCURY)?;
m.add("R_MERCURY", R_MERCURY)?;
m.add("R_POLAR_MERCURY", R_POLAR_MERCURY)?;
m.add("R_MEAN_MERCURY", R_MEAN_MERCURY)?;
m.add("GM_VENUS", GM_VENUS)?;
m.add("R_VENUS", R_VENUS)?;
m.add("R_POLAR_VENUS", R_POLAR_VENUS)?;
m.add("R_MEAN_VENUS", R_MEAN_VENUS)?;
m.add("J2_VENUS", J2_VENUS)?;
m.add("J3_VENUS", J3_VENUS)?;
m.add("GM_EARTH", GM_EARTH)?;
m.add("R_EARTH", R_EARTH)?;
m.add("R_POLAR_EARTH", R_POLAR_EARTH)?;
m.add("R_MEAN_EARTH", R_MEAN_EARTH)?;
m.add("J2_EARTH", J2_EARTH)?;
m.add("J3_EARTH", J3_EARTH)?;
m.add("J4_EARTH", J4_EARTH)?;
m.add("J5_EARTH", J5_EARTH)?;
m.add("J6_EARTH", J6_EARTH)?;
m.add("H0_EARTH", H0_EARTH)?;
m.add("RHO0_EARTH", RHO0_EARTH)?;
m.add("GM_MOON", GM_MOON)?;
m.add("R_MOON", R_MOON)?;
m.add("R_POLAR_MOON", R_POLAR_MOON)?;
m.add("R_MEAN_MOON", R_MEAN_MOON)?;
m.add("GM_IO", GM_IO)?;
m.add("R_IO", R_IO)?;
m.add("GM_EUROPA", GM_EUROPA)?;
m.add("R_EUROPA", R_EUROPA)?;
m.add("GM_GANYMEDE", GM_GANYMEDE)?;
m.add("R_GANYMEDE", R_GANYMEDE)?;
m.add("GM_CALLISTO", GM_CALLISTO)?;
m.add("R_CALLISTO", R_CALLISTO)?;
m.add("GM_TITAN", GM_TITAN)?;
m.add("R_TITAN", R_TITAN)?;
m.add("GM_TRITON", GM_TRITON)?;
m.add("R_TRITON", R_TRITON)?;
m.add("GM_MARS", GM_MARS)?;
m.add("R_MARS", R_MARS)?;
m.add("R_POLAR_MARS", R_POLAR_MARS)?;
m.add("R_MEAN_MARS", R_MEAN_MARS)?;
m.add("J2_MARS", J2_MARS)?;
m.add("J3_MARS", J3_MARS)?;
m.add("GM_JUPITER", GM_JUPITER)?;
m.add("R_JUPITER", R_JUPITER)?;
m.add("R_POLAR_JUPITER", R_POLAR_JUPITER)?;
m.add("R_MEAN_JUPITER", R_MEAN_JUPITER)?;
m.add("GM_SATURN", GM_SATURN)?;
m.add("R_SATURN", R_SATURN)?;
m.add("R_POLAR_SATURN", R_POLAR_SATURN)?;
m.add("R_MEAN_SATURN", R_MEAN_SATURN)?;
m.add("GM_URANUS", GM_URANUS)?;
m.add("R_URANUS", R_URANUS)?;
m.add("R_POLAR_URANUS", R_POLAR_URANUS)?;
m.add("R_MEAN_URANUS", R_MEAN_URANUS)?;
m.add("GM_NEPTUNE", GM_NEPTUNE)?;
m.add("R_NEPTUNE", R_NEPTUNE)?;
m.add("R_POLAR_NEPTUNE", R_POLAR_NEPTUNE)?;
m.add("R_MEAN_NEPTUNE", R_MEAN_NEPTUNE)?;
m.add("GM_PLUTO", GM_PLUTO)?;
m.add("R_PLUTO", R_PLUTO)?;
m.add("R_POLAR_PLUTO", R_POLAR_PLUTO)?;
m.add("R_MEAN_PLUTO", R_MEAN_PLUTO)?;
m.add("A_MERCURY", A_MERCURY)?;
m.add("A_VENUS", A_VENUS)?;
m.add("A_EARTH", A_EARTH)?;
m.add("A_MARS", A_MARS)?;
m.add("A_JUPITER", A_JUPITER)?;
m.add("A_SATURN", A_SATURN)?;
m.add("A_URANUS", A_URANUS)?;
m.add("A_NEPTUNE", A_NEPTUNE)?;
m.add("R_SOI_MERCURY", R_SOI_MERCURY)?;
m.add("R_SOI_VENUS", R_SOI_VENUS)?;
m.add("R_SOI_EARTH", R_SOI_EARTH)?;
m.add("R_SOI_MARS", R_SOI_MARS)?;
m.add("R_SOI_JUPITER", R_SOI_JUPITER)?;
m.add("R_SOI_SATURN", R_SOI_SATURN)?;
m.add("R_SOI_URANUS", R_SOI_URANUS)?;
m.add("R_SOI_NEPTUNE", R_SOI_NEPTUNE)?;
m.add("KM_TO_M", KM_TO_M)?;
m.add("M_TO_KM", M_TO_KM)?;
m.add("DEG_TO_RAD", DEG_TO_RAD)?;
m.add("RAD_TO_DEG", RAD_TO_DEG)?;
m.add("DAY_TO_SEC", DAY_TO_SEC)?;
m.add("SEC_TO_DAY", SEC_TO_DAY)?;
m.add("J2000_TT", J2000_TT)?;
m.add("J2000_MJD", J2000_MJD)?;
m.add_function(wrap_pyfunction!(py_km_to_m, m)?)?;
m.add_function(wrap_pyfunction!(py_m_to_km, m)?)?;
m.add_function(wrap_pyfunction!(py_deg_to_rad, m)?)?;
m.add_function(wrap_pyfunction!(py_rad_to_deg, m)?)?;
m.add_function(wrap_pyfunction!(py_days_to_sec, m)?)?;
m.add_function(wrap_pyfunction!(py_sec_to_days, m)?)?;
Ok(())
}
#[pyfunction]
#[pyo3(name = "km_to_m")]
fn py_km_to_m(km: f64) -> f64 {
core::constants::km_to_m(km)
}
#[pyfunction]
#[pyo3(name = "m_to_km")]
fn py_m_to_km(m: f64) -> f64 {
core::constants::m_to_km(m)
}
#[pyfunction]
#[pyo3(name = "deg_to_rad")]
fn py_deg_to_rad(deg: f64) -> f64 {
core::constants::deg_to_rad(deg)
}
#[pyfunction]
#[pyo3(name = "rad_to_deg")]
fn py_rad_to_deg(rad: f64) -> f64 {
core::constants::rad_to_deg(rad)
}
#[pyfunction]
#[pyo3(name = "days_to_sec")]
fn py_days_to_sec(days: f64) -> f64 {
core::constants::days_to_sec(days)
}
#[pyfunction]
#[pyo3(name = "sec_to_days")]
fn py_sec_to_days(sec: f64) -> f64 {
core::constants::sec_to_days(sec)
}
fn add_numpy_ops(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(py_sum_array, m)?)?;
m.add_function(wrap_pyfunction!(py_multiply_scalar, m)?)?;
m.add_function(wrap_pyfunction!(py_multiply_scalar_inplace, m)?)?;
m.add_function(wrap_pyfunction!(py_dot_product, m)?)?;
m.add_function(wrap_pyfunction!(py_cross_product, m)?)?;
m.add_function(wrap_pyfunction!(py_add_arrays, m)?)?;
m.add_function(wrap_pyfunction!(py_normalize_vector, m)?)?;
m.add_function(wrap_pyfunction!(py_vector_magnitude, m)?)?;
m.add_function(wrap_pyfunction!(py_apply_polynomial, m)?)?;
m.add_function(wrap_pyfunction!(py_matrix_vector_multiply, m)?)?;
m.add_function(wrap_pyfunction!(py_matrix_multiply, m)?)?;
m.add_function(wrap_pyfunction!(py_transpose_matrix, m)?)?;
m.add_function(wrap_pyfunction!(py_identity_matrix, m)?)?;
m.add_function(wrap_pyfunction!(py_batch_normalize_vectors, m)?)?;
Ok(())
}
#[pyfunction]
#[pyo3(name = "sum_array")]
fn py_sum_array(arr: PyReadonlyArray1<f64>) -> f64 {
core::numpy_integration::sum_array(arr.as_array())
}
#[pyfunction]
#[pyo3(name = "multiply_scalar")]
fn py_multiply_scalar<'py>(
py: Python<'py>,
arr: PyReadonlyArray1<f64>,
scalar: f64,
) -> Bound<'py, PyArray1<f64>> {
let result = core::numpy_integration::multiply_scalar(arr.as_array(), scalar);
PyArray1::from_owned_array_bound(py, result)
}
#[pyfunction]
#[pyo3(name = "multiply_scalar_inplace")]
fn py_multiply_scalar_inplace(mut arr: PyReadwriteArray1<f64>, scalar: f64) {
core::numpy_integration::multiply_scalar_inplace(arr.as_array_mut(), scalar);
}
#[pyfunction]
#[pyo3(name = "dot_product")]
fn py_dot_product(a: PyReadonlyArray1<f64>, b: PyReadonlyArray1<f64>) -> PyResult<f64> {
core::numpy_integration::dot_product(a.as_array(), b.as_array())
.map_err(|e| e.into())
}
#[pyfunction]
#[pyo3(name = "cross_product")]
fn py_cross_product<'py>(
py: Python<'py>,
a: PyReadonlyArray1<f64>,
b: PyReadonlyArray1<f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let result = core::numpy_integration::cross_product(a.as_array(), b.as_array())?;
Ok(PyArray1::from_owned_array_bound(py, result))
}
#[pyfunction]
#[pyo3(name = "add_arrays")]
fn py_add_arrays<'py>(
py: Python<'py>,
a: PyReadonlyArray1<f64>,
b: PyReadonlyArray1<f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let result = core::numpy_integration::add_arrays(a.as_array(), b.as_array())?;
Ok(PyArray1::from_owned_array_bound(py, result))
}
#[pyfunction]
#[pyo3(name = "normalize_vector")]
fn py_normalize_vector<'py>(
py: Python<'py>,
vec: PyReadonlyArray1<f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let result = core::numpy_integration::normalize_vector(vec.as_array())?;
Ok(PyArray1::from_owned_array_bound(py, result))
}
#[pyfunction]
#[pyo3(name = "vector_magnitude")]
fn py_vector_magnitude(vec: PyReadonlyArray1<f64>) -> f64 {
core::numpy_integration::vector_magnitude(vec.as_array())
}
#[pyfunction]
#[pyo3(name = "apply_polynomial")]
fn py_apply_polynomial<'py>(
py: Python<'py>,
arr: PyReadonlyArray1<f64>,
) -> Bound<'py, PyArray1<f64>> {
let result = core::numpy_integration::apply_polynomial(arr.as_array());
PyArray1::from_owned_array_bound(py, result)
}
#[pyfunction]
#[pyo3(name = "matrix_vector_multiply")]
fn py_matrix_vector_multiply<'py>(
py: Python<'py>,
matrix: PyReadonlyArray2<f64>,
vector: PyReadonlyArray1<f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let result = core::numpy_integration::matrix_vector_multiply(
matrix.as_array(),
vector.as_array(),
)?;
Ok(PyArray1::from_owned_array_bound(py, result))
}
#[pyfunction]
#[pyo3(name = "matrix_multiply")]
fn py_matrix_multiply<'py>(
py: Python<'py>,
a: PyReadonlyArray2<f64>,
b: PyReadonlyArray2<f64>,
) -> PyResult<Bound<'py, PyArray2<f64>>> {
let result = core::numpy_integration::matrix_multiply(a.as_array(), b.as_array())?;
Ok(PyArray2::from_owned_array_bound(py, result))
}
#[pyfunction]
#[pyo3(name = "transpose_matrix")]
fn py_transpose_matrix<'py>(
py: Python<'py>,
matrix: PyReadonlyArray2<f64>,
) -> Bound<'py, PyArray2<f64>> {
let result = core::numpy_integration::transpose_matrix(matrix.as_array());
PyArray2::from_owned_array_bound(py, result)
}
#[pyfunction]
#[pyo3(name = "identity_matrix")]
fn py_identity_matrix(py: Python<'_>, size: usize) -> PyResult<Bound<'_, PyArray2<f64>>> {
let result = core::numpy_integration::identity_matrix(size)?;
Ok(PyArray2::from_owned_array_bound(py, result))
}
#[pyfunction]
#[pyo3(name = "batch_normalize_vectors")]
fn py_batch_normalize_vectors<'py>(
py: Python<'py>,
vectors: PyReadonlyArray2<f64>,
) -> PyResult<Bound<'py, PyArray2<f64>>> {
let result = core::numpy_integration::batch_normalize_vectors(vectors.as_array())?;
Ok(PyArray2::from_owned_array_bound(py, result))
}
#[pyfunction]
#[pyo3(name = "rv_to_coe", signature = (r, v, mu, tol=1e-8))]
fn py_rv_to_coe(
r: PyReadonlyArray1<f64>,
v: PyReadonlyArray1<f64>,
mu: f64,
tol: f64,
) -> PyResult<core::elements::OrbitalElements> {
let r_array = r.as_array();
let v_array = v.as_array();
if r_array.len() != 3 || v_array.len() != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position and velocity vectors must have exactly 3 components"
));
}
let r_vec = core::linalg::Vector3::new(r_array[0], r_array[1], r_array[2]);
let v_vec = core::linalg::Vector3::new(v_array[0], v_array[1], v_array[2]);
core::elements::rv_to_coe(&r_vec, &v_vec, mu, tol).map_err(|e| e.into())
}
#[pyfunction]
#[pyo3(name = "coe_to_rv")]
fn py_coe_to_rv<'py>(
py: Python<'py>,
elements: &core::elements::OrbitalElements,
mu: f64,
) -> (Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>) {
let (r_vec, v_vec) = core::elements::coe_to_rv(elements, mu);
let r_array = ndarray::arr1(&[r_vec.x, r_vec.y, r_vec.z]);
let v_array = ndarray::arr1(&[v_vec.x, v_vec.y, v_vec.z]);
(
PyArray1::from_owned_array_bound(py, r_array),
PyArray1::from_owned_array_bound(py, v_array),
)
}
#[pyfunction]
#[pyo3(name = "coe_to_equinoctial")]
fn py_coe_to_equinoctial(
elements: &core::elements::OrbitalElements,
) -> core::elements::EquinoctialElements {
core::elements::coe_to_equinoctial(elements)
}
#[pyfunction]
#[pyo3(name = "equinoctial_to_coe", signature = (elements, tol=1e-8))]
fn py_equinoctial_to_coe(
elements: &core::elements::EquinoctialElements,
tol: f64,
) -> PyResult<core::elements::OrbitalElements> {
core::elements::equinoctial_to_coe(elements, tol).map_err(|e| e.into())
}
#[pyfunction]
#[pyo3(name = "rv_to_equinoctial", signature = (r, v, mu, tol=1e-8))]
fn py_rv_to_equinoctial(
r: PyReadonlyArray1<f64>,
v: PyReadonlyArray1<f64>,
mu: f64,
tol: f64,
) -> PyResult<core::elements::EquinoctialElements> {
let r_array = r.as_array();
let v_array = v.as_array();
if r_array.len() != 3 || v_array.len() != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position and velocity vectors must have exactly 3 components"
));
}
let r_vec = core::linalg::Vector3::new(r_array[0], r_array[1], r_array[2]);
let v_vec = core::linalg::Vector3::new(v_array[0], v_array[1], v_array[2]);
core::elements::rv_to_equinoctial(&r_vec, &v_vec, mu, tol).map_err(|e| e.into())
}
#[pyfunction]
#[pyo3(name = "equinoctial_to_rv", signature = (elements, mu, tol=1e-8))]
fn py_equinoctial_to_rv<'py>(
py: Python<'py>,
elements: &core::elements::EquinoctialElements,
mu: f64,
tol: f64,
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
let (r_vec, v_vec) = core::elements::equinoctial_to_rv(elements, mu, tol)?;
let r_array = ndarray::arr1(&[r_vec.x, r_vec.y, r_vec.z]);
let v_array = ndarray::arr1(&[v_vec.x, v_vec.y, v_vec.z]);
Ok((
PyArray1::from_owned_array_bound(py, r_array),
PyArray1::from_owned_array_bound(py, v_array),
))
}
#[pyfunction]
#[pyo3(name = "mean_to_eccentric_anomaly", signature = (mean_anomaly, eccentricity, tol=None, max_iter=None))]
fn py_mean_to_eccentric_anomaly(
mean_anomaly: f64,
eccentricity: f64,
tol: Option<f64>,
max_iter: Option<usize>,
) -> PyResult<f64> {
core::anomaly::mean_to_eccentric_anomaly(mean_anomaly, eccentricity, tol, max_iter)
.map_err(|e| e.into())
}
#[pyfunction]
#[pyo3(name = "eccentric_to_mean_anomaly")]
fn py_eccentric_to_mean_anomaly(eccentric_anomaly: f64, eccentricity: f64) -> PyResult<f64> {
core::anomaly::eccentric_to_mean_anomaly(eccentric_anomaly, eccentricity)
.map_err(|e| e.into())
}
#[pyfunction]
#[pyo3(name = "eccentric_to_true_anomaly")]
fn py_eccentric_to_true_anomaly(eccentric_anomaly: f64, eccentricity: f64) -> PyResult<f64> {
core::anomaly::eccentric_to_true_anomaly(eccentric_anomaly, eccentricity)
.map_err(|e| e.into())
}
#[pyfunction]
#[pyo3(name = "true_to_eccentric_anomaly")]
fn py_true_to_eccentric_anomaly(true_anomaly: f64, eccentricity: f64) -> PyResult<f64> {
core::anomaly::true_to_eccentric_anomaly(true_anomaly, eccentricity)
.map_err(|e| e.into())
}
#[pyfunction]
#[pyo3(name = "mean_to_true_anomaly", signature = (mean_anomaly, eccentricity, tol=None, max_iter=None))]
fn py_mean_to_true_anomaly(
mean_anomaly: f64,
eccentricity: f64,
tol: Option<f64>,
max_iter: Option<usize>,
) -> PyResult<f64> {
core::anomaly::mean_to_true_anomaly(mean_anomaly, eccentricity, tol, max_iter)
.map_err(|e| e.into())
}
#[pyfunction]
#[pyo3(name = "true_to_mean_anomaly")]
fn py_true_to_mean_anomaly(true_anomaly: f64, eccentricity: f64) -> PyResult<f64> {
core::anomaly::true_to_mean_anomaly(true_anomaly, eccentricity)
.map_err(|e| e.into())
}
#[pyfunction]
#[pyo3(name = "mean_to_hyperbolic_anomaly", signature = (mean_anomaly, eccentricity, tol=None, max_iter=None))]
fn py_mean_to_hyperbolic_anomaly(
mean_anomaly: f64,
eccentricity: f64,
tol: Option<f64>,
max_iter: Option<usize>,
) -> PyResult<f64> {
core::anomaly::mean_to_hyperbolic_anomaly(mean_anomaly, eccentricity, tol, max_iter)
.map_err(|e| e.into())
}
#[pyfunction]
#[pyo3(name = "hyperbolic_to_mean_anomaly")]
fn py_hyperbolic_to_mean_anomaly(hyperbolic_anomaly: f64, eccentricity: f64) -> PyResult<f64> {
core::anomaly::hyperbolic_to_mean_anomaly(hyperbolic_anomaly, eccentricity)
.map_err(|e| e.into())
}
#[pyfunction]
#[pyo3(name = "hyperbolic_to_true_anomaly")]
fn py_hyperbolic_to_true_anomaly(hyperbolic_anomaly: f64, eccentricity: f64) -> PyResult<f64> {
core::anomaly::hyperbolic_to_true_anomaly(hyperbolic_anomaly, eccentricity)
.map_err(|e| e.into())
}
#[pyfunction]
#[pyo3(name = "true_to_hyperbolic_anomaly")]
fn py_true_to_hyperbolic_anomaly(true_anomaly: f64, eccentricity: f64) -> PyResult<f64> {
core::anomaly::true_to_hyperbolic_anomaly(true_anomaly, eccentricity)
.map_err(|e| e.into())
}
#[pyfunction]
#[pyo3(name = "mean_to_true_anomaly_hyperbolic", signature = (mean_anomaly, eccentricity, tol=None, max_iter=None))]
fn py_mean_to_true_anomaly_hyperbolic(
mean_anomaly: f64,
eccentricity: f64,
tol: Option<f64>,
max_iter: Option<usize>,
) -> PyResult<f64> {
core::anomaly::mean_to_true_anomaly_hyperbolic(mean_anomaly, eccentricity, tol, max_iter)
.map_err(|e| e.into())
}
#[pyfunction]
#[pyo3(name = "true_to_mean_anomaly_hyperbolic")]
fn py_true_to_mean_anomaly_hyperbolic(true_anomaly: f64, eccentricity: f64) -> PyResult<f64> {
core::anomaly::true_to_mean_anomaly_hyperbolic(true_anomaly, eccentricity)
.map_err(|e| e.into())
}
#[pyfunction]
#[pyo3(name = "mean_to_true_anomaly_parabolic")]
fn py_mean_to_true_anomaly_parabolic(mean_anomaly: f64) -> PyResult<f64> {
core::anomaly::mean_to_true_anomaly_parabolic(mean_anomaly)
.map_err(|e| e.into())
}
#[pyfunction]
#[pyo3(name = "true_to_mean_anomaly_parabolic")]
fn py_true_to_mean_anomaly_parabolic(true_anomaly: f64) -> PyResult<f64> {
core::anomaly::true_to_mean_anomaly_parabolic(true_anomaly)
.map_err(|e| e.into())
}
#[pyfunction]
#[pyo3(name = "batch_mean_to_eccentric_anomaly", signature = (mean_anomalies, eccentricities, tol=None, max_iter=None))]
fn py_batch_mean_to_eccentric_anomaly<'py>(
py: Python<'py>,
mean_anomalies: PyReadonlyArray1<f64>,
eccentricities: PyReadonlyArray1<f64>,
tol: Option<f64>,
max_iter: Option<usize>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let m_array = mean_anomalies.as_slice()?;
let e_array = eccentricities.as_slice()?;
let results = core::anomaly::batch_mean_to_eccentric_anomaly(m_array, e_array, tol, max_iter)
.map_err(Into::<PyErr>::into)?;
Ok(PyArray1::from_vec_bound(py, results))
}
#[pyfunction]
#[pyo3(name = "batch_mean_to_true_anomaly", signature = (mean_anomalies, eccentricities, tol=None, max_iter=None))]
fn py_batch_mean_to_true_anomaly<'py>(
py: Python<'py>,
mean_anomalies: PyReadonlyArray1<f64>,
eccentricities: PyReadonlyArray1<f64>,
tol: Option<f64>,
max_iter: Option<usize>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let m_array = mean_anomalies.as_slice()?;
let e_array = eccentricities.as_slice()?;
let results = core::anomaly::batch_mean_to_true_anomaly(m_array, e_array, tol, max_iter)
.map_err(Into::<PyErr>::into)?;
Ok(PyArray1::from_vec_bound(py, results))
}
#[pyfunction]
#[pyo3(name = "batch_true_to_mean_anomaly")]
fn py_batch_true_to_mean_anomaly<'py>(
py: Python<'py>,
true_anomalies: PyReadonlyArray1<f64>,
eccentricities: PyReadonlyArray1<f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let nu_array = true_anomalies.as_slice()?;
let e_array = eccentricities.as_slice()?;
let results = core::anomaly::batch_true_to_mean_anomaly(nu_array, e_array)
.map_err(Into::<PyErr>::into)?;
Ok(PyArray1::from_vec_bound(py, results))
}
#[pyfunction]
#[pyo3(name = "batch_mean_to_hyperbolic_anomaly", signature = (mean_anomalies, eccentricities, tol=None, max_iter=None))]
fn py_batch_mean_to_hyperbolic_anomaly<'py>(
py: Python<'py>,
mean_anomalies: PyReadonlyArray1<f64>,
eccentricities: PyReadonlyArray1<f64>,
tol: Option<f64>,
max_iter: Option<usize>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let m_array = mean_anomalies.as_slice()?;
let e_array = eccentricities.as_slice()?;
let results = core::anomaly::batch_mean_to_hyperbolic_anomaly(m_array, e_array, tol, max_iter)
.map_err(Into::<PyErr>::into)?;
Ok(PyArray1::from_vec_bound(py, results))
}
#[pyfunction]
#[pyo3(name = "batch_mean_to_true_anomaly_hyperbolic", signature = (mean_anomalies, eccentricities, tol=None, max_iter=None))]
fn py_batch_mean_to_true_anomaly_hyperbolic<'py>(
py: Python<'py>,
mean_anomalies: PyReadonlyArray1<f64>,
eccentricities: PyReadonlyArray1<f64>,
tol: Option<f64>,
max_iter: Option<usize>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let m_array = mean_anomalies.as_slice()?;
let e_array = eccentricities.as_slice()?;
let results = core::anomaly::batch_mean_to_true_anomaly_hyperbolic(m_array, e_array, tol, max_iter)
.map_err(Into::<PyErr>::into)?;
Ok(PyArray1::from_vec_bound(py, results))
}
#[pyfunction]
#[pyo3(name = "batch_mean_to_true_anomaly_parabolic")]
fn py_batch_mean_to_true_anomaly_parabolic<'py>(
py: Python<'py>,
mean_anomalies: PyReadonlyArray1<f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let m_array = mean_anomalies.as_slice()?;
let results = core::anomaly::batch_mean_to_true_anomaly_parabolic(m_array)
.map_err(Into::<PyErr>::into)?;
Ok(PyArray1::from_vec_bound(py, results))
}
#[pyfunction]
#[pyo3(name = "propagate_keplerian")]
fn py_propagate_keplerian(
elements: &core::elements::OrbitalElements,
dt: f64,
mu: f64,
) -> PyResult<core::elements::OrbitalElements> {
propagators::keplerian::propagate_keplerian(elements, dt, mu)
.map_err(|e| e.into())
}
#[pyfunction]
#[pyo3(name = "propagate_keplerian_duration")]
fn py_propagate_keplerian_duration(
elements: &core::elements::OrbitalElements,
duration: &core::time::Duration,
mu: f64,
) -> PyResult<core::elements::OrbitalElements> {
propagators::keplerian::propagate_keplerian_duration(elements, duration, mu)
.map_err(|e| e.into())
}
#[pyfunction]
#[pyo3(name = "propagate_state_keplerian")]
fn py_propagate_state_keplerian<'py>(
py: Python<'py>,
r0: PyReadonlyArray1<f64>,
v0: PyReadonlyArray1<f64>,
dt: f64,
mu: f64,
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
let r0_array = r0.as_array();
let v0_array = v0.as_array();
if r0_array.len() != 3 || v0_array.len() != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position and velocity vectors must have exactly 3 components"
));
}
let r0_vec = core::linalg::Vector3::new(r0_array[0], r0_array[1], r0_array[2]);
let v0_vec = core::linalg::Vector3::new(v0_array[0], v0_array[1], v0_array[2]);
let (r_vec, v_vec) = propagators::keplerian::propagate_state_keplerian(&r0_vec, &v0_vec, dt, mu)?;
let r_array = ndarray::arr1(&[r_vec.x, r_vec.y, r_vec.z]);
let v_array = ndarray::arr1(&[v_vec.x, v_vec.y, v_vec.z]);
Ok((
PyArray1::from_owned_array_bound(py, r_array),
PyArray1::from_owned_array_bound(py, v_array),
))
}
#[pyfunction]
#[pyo3(name = "propagate_lagrange")]
fn py_propagate_lagrange<'py>(
py: Python<'py>,
r0: PyReadonlyArray1<f64>,
v0: PyReadonlyArray1<f64>,
dt: f64,
mu: f64,
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
let r0_array = r0.as_array();
let v0_array = v0.as_array();
if r0_array.len() != 3 || v0_array.len() != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position and velocity vectors must have exactly 3 components"
));
}
let r0_vec = core::linalg::Vector3::new(r0_array[0], r0_array[1], r0_array[2]);
let v0_vec = core::linalg::Vector3::new(v0_array[0], v0_array[1], v0_array[2]);
let (r_vec, v_vec) = propagators::keplerian::propagate_lagrange(&r0_vec, &v0_vec, dt, mu)?;
let r_array = ndarray::arr1(&[r_vec.x, r_vec.y, r_vec.z]);
let v_array = ndarray::arr1(&[v_vec.x, v_vec.y, v_vec.z]);
Ok((
PyArray1::from_owned_array_bound(py, r_array),
PyArray1::from_owned_array_bound(py, v_array),
))
}
#[pyfunction]
#[pyo3(name = "batch_propagate_states")]
fn py_batch_propagate_states<'py>(
py: Python<'py>,
states: PyReadonlyArray2<f64>,
time_steps: &Bound<'_, pyo3::types::PyAny>,
mu: f64,
) -> PyResult<Bound<'py, PyArray2<f64>>> {
let states_array = states.as_array();
let dt_vec: Vec<f64> = if let Ok(single_dt) = time_steps.extract::<f64>() {
vec![single_dt]
} else if let Ok(dt_array) = time_steps.extract::<PyReadonlyArray1<f64>>() {
dt_array.as_array().to_vec()
} else {
return Err(pyo3::exceptions::PyTypeError::new_err(
"time_steps must be either a float or a 1D NumPy array of floats"
));
};
let result = propagators::keplerian::batch_propagate_states(
states_array,
&dt_vec,
mu
)?;
Ok(PyArray2::from_owned_array_bound(py, result))
}
#[pyfunction]
#[pyo3(name = "batch_propagate_lagrange")]
fn py_batch_propagate_lagrange<'py>(
py: Python<'py>,
states: PyReadonlyArray2<f64>,
time_steps: &Bound<'_, pyo3::types::PyAny>,
mu: f64,
) -> PyResult<Bound<'py, PyArray2<f64>>> {
let states_array = states.as_array();
let dt_vec: Vec<f64> = if let Ok(single_dt) = time_steps.extract::<f64>() {
vec![single_dt]
} else if let Ok(dt_array) = time_steps.extract::<PyReadonlyArray1<f64>>() {
dt_array.as_array().to_vec()
} else {
return Err(pyo3::exceptions::PyTypeError::new_err(
"time_steps must be either a float or a 1D NumPy array of floats"
));
};
let result = propagators::keplerian::batch_propagate_lagrange(
states_array,
&dt_vec,
mu
)?;
Ok(PyArray2::from_owned_array_bound(py, result))
}
#[pyfunction]
#[pyo3(name = "j2_perturbation")]
fn py_j2_perturbation<'py>(
py: Python<'py>,
r: PyReadonlyArray1<f64>,
mu: f64,
j2: f64,
R: f64,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let r_array = r.as_array();
if r_array.len() != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position vector must have exactly 3 components"
));
}
let r_vec = core::linalg::Vector3::new(r_array[0], r_array[1], r_array[2]);
let acc = propagators::perturbations::j2_perturbation(&r_vec, mu, j2, R);
let acc_array = ndarray::arr1(&[acc.x, acc.y, acc.z]);
Ok(PyArray1::from_owned_array_bound(py, acc_array))
}
#[pyfunction]
#[pyo3(name = "propagate_j2_rk4", signature = (r0, v0, dt, mu, j2, R, n_steps=None))]
fn py_propagate_j2_rk4<'py>(
py: Python<'py>,
r0: PyReadonlyArray1<f64>,
v0: PyReadonlyArray1<f64>,
dt: f64,
mu: f64,
j2: f64,
R: f64,
n_steps: Option<usize>,
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
let r0_array = r0.as_array();
let v0_array = v0.as_array();
if r0_array.len() != 3 || v0_array.len() != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position and velocity vectors must have exactly 3 components"
));
}
let r0_vec = core::linalg::Vector3::new(r0_array[0], r0_array[1], r0_array[2]);
let v0_vec = core::linalg::Vector3::new(v0_array[0], v0_array[1], v0_array[2]);
let (r_vec, v_vec) = propagators::perturbations::propagate_j2_rk4(
&r0_vec, &v0_vec, dt, mu, j2, R, n_steps
)?;
let r_array = ndarray::arr1(&[r_vec.x, r_vec.y, r_vec.z]);
let v_array = ndarray::arr1(&[v_vec.x, v_vec.y, v_vec.z]);
Ok((
PyArray1::from_owned_array_bound(py, r_array),
PyArray1::from_owned_array_bound(py, v_array),
))
}
#[pyfunction]
#[pyo3(name = "propagate_j2_dopri5", signature = (r0, v0, dt, mu, j2, R, tol=None))]
fn py_propagate_j2_dopri5<'py>(
py: Python<'py>,
r0: PyReadonlyArray1<f64>,
v0: PyReadonlyArray1<f64>,
dt: f64,
mu: f64,
j2: f64,
R: f64,
tol: Option<f64>,
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
let r0_array = r0.as_array();
let v0_array = v0.as_array();
if r0_array.len() != 3 || v0_array.len() != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position and velocity vectors must have exactly 3 components"
));
}
let r0_vec = core::linalg::Vector3::new(r0_array[0], r0_array[1], r0_array[2]);
let v0_vec = core::linalg::Vector3::new(v0_array[0], v0_array[1], v0_array[2]);
let (r_vec, v_vec) = propagators::perturbations::propagate_j2_dopri5(
&r0_vec, &v0_vec, dt, mu, j2, R, tol
)?;
let r_array = ndarray::arr1(&[r_vec.x, r_vec.y, r_vec.z]);
let v_array = ndarray::arr1(&[v_vec.x, v_vec.y, v_vec.z]);
Ok((
PyArray1::from_owned_array_bound(py, r_array),
PyArray1::from_owned_array_bound(py, v_array),
))
}
#[pyfunction]
#[pyo3(name = "propagate_j2_dop853", signature = (r0, v0, dt, mu, j2, R, tol=None))]
fn py_propagate_j2_dop853<'py>(
py: Python<'py>,
r0: PyReadonlyArray1<f64>,
v0: PyReadonlyArray1<f64>,
dt: f64,
mu: f64,
j2: f64,
R: f64,
tol: Option<f64>,
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
let r0_array = r0.as_array();
let v0_array = v0.as_array();
if r0_array.len() != 3 || v0_array.len() != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position and velocity vectors must have exactly 3 components"
));
}
let r0_vec = core::linalg::Vector3::new(r0_array[0], r0_array[1], r0_array[2]);
let v0_vec = core::linalg::Vector3::new(v0_array[0], v0_array[1], v0_array[2]);
let (r_vec, v_vec) = propagators::perturbations::propagate_j2_dop853(
&r0_vec, &v0_vec, dt, mu, j2, R, tol
)?;
let r_array = ndarray::arr1(&[r_vec.x, r_vec.y, r_vec.z]);
let v_array = ndarray::arr1(&[v_vec.x, v_vec.y, v_vec.z]);
Ok((
PyArray1::from_owned_array_bound(py, r_array),
PyArray1::from_owned_array_bound(py, v_array),
))
}
#[pyfunction]
#[pyo3(name = "propagate_j2_rk4_static")]
fn py_propagate_j2_rk4_static<'py>(
py: Python<'py>,
r0: PyReadonlyArray1<f64>,
v0: PyReadonlyArray1<f64>,
dt: f64,
mu: f64,
j2: f64,
R: f64,
n_steps: usize,
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
use core::integrators_static::{propagate_rk4_final_only, StateVector6};
use propagators::perturbations_static::j2_dynamics;
let r0_array = r0.as_array();
let v0_array = v0.as_array();
if r0_array.len() != 3 || v0_array.len() != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position and velocity vectors must have exactly 3 components"
));
}
let state0 = StateVector6::new(
r0_array[0], r0_array[1], r0_array[2],
v0_array[0], v0_array[1], v0_array[2],
);
let dynamics = j2_dynamics(mu, j2, R);
let state_final = propagate_rk4_final_only(dynamics, 0.0, &state0, dt, n_steps);
let r_array = ndarray::arr1(&[state_final[0], state_final[1], state_final[2]]);
let v_array = ndarray::arr1(&[state_final[3], state_final[4], state_final[5]]);
Ok((
PyArray1::from_owned_array_bound(py, r_array),
PyArray1::from_owned_array_bound(py, v_array),
))
}
#[pyfunction]
#[pyo3(name = "propagate_j2_j3_j4_rk4_static")]
fn py_propagate_j2_j3_j4_rk4_static<'py>(
py: Python<'py>,
r0: PyReadonlyArray1<f64>,
v0: PyReadonlyArray1<f64>,
dt: f64,
mu: f64,
j2: f64,
j3: f64,
j4: f64,
R: f64,
n_steps: usize,
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
use core::integrators_static::{propagate_rk4_final_only, StateVector6};
use propagators::perturbations_static::j2_j3_j4_dynamics;
let r0_array = r0.as_array();
let v0_array = v0.as_array();
if r0_array.len() != 3 || v0_array.len() != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position and velocity vectors must have exactly 3 components"
));
}
let state0 = StateVector6::new(
r0_array[0], r0_array[1], r0_array[2],
v0_array[0], v0_array[1], v0_array[2],
);
let dynamics = j2_j3_j4_dynamics(mu, j2, j3, j4, R);
let state_final = propagate_rk4_final_only(dynamics, 0.0, &state0, dt, n_steps);
let r_array = ndarray::arr1(&[state_final[0], state_final[1], state_final[2]]);
let v_array = ndarray::arr1(&[state_final[3], state_final[4], state_final[5]]);
Ok((
PyArray1::from_owned_array_bound(py, r_array),
PyArray1::from_owned_array_bound(py, v_array),
))
}
#[pyfunction]
#[pyo3(name = "exponential_density")]
fn py_exponential_density(altitude: f64, rho0: f64, H0: f64) -> f64 {
propagators::perturbations::exponential_density(altitude, rho0, H0)
}
#[pyfunction]
#[pyo3(name = "drag_acceleration")]
fn py_drag_acceleration<'py>(
py: Python<'py>,
r: PyReadonlyArray1<f64>,
v: PyReadonlyArray1<f64>,
R: f64,
rho0: f64,
H0: f64,
B: f64,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let r_array = r.as_array();
let v_array = v.as_array();
if r_array.len() != 3 || v_array.len() != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position and velocity vectors must have exactly 3 components"
));
}
let r_vec = core::linalg::Vector3::new(r_array[0], r_array[1], r_array[2]);
let v_vec = core::linalg::Vector3::new(v_array[0], v_array[1], v_array[2]);
let a_vec = propagators::perturbations::drag_acceleration(&r_vec, &v_vec, R, rho0, H0, B);
let a_array = ndarray::arr1(&[a_vec.x, a_vec.y, a_vec.z]);
Ok(PyArray1::from_owned_array_bound(py, a_array))
}
#[pyfunction]
#[pyo3(name = "propagate_drag_rk4", signature = (r0, v0, dt, mu, R, rho0, H0, B, n_steps=None))]
fn py_propagate_drag_rk4<'py>(
py: Python<'py>,
r0: PyReadonlyArray1<f64>,
v0: PyReadonlyArray1<f64>,
dt: f64,
mu: f64,
R: f64,
rho0: f64,
H0: f64,
B: f64,
n_steps: Option<usize>,
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
let r0_array = r0.as_array();
let v0_array = v0.as_array();
if r0_array.len() != 3 || v0_array.len() != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position and velocity vectors must have exactly 3 components"
));
}
let r0_vec = core::linalg::Vector3::new(r0_array[0], r0_array[1], r0_array[2]);
let v0_vec = core::linalg::Vector3::new(v0_array[0], v0_array[1], v0_array[2]);
let (r_vec, v_vec) = propagators::perturbations::propagate_drag_rk4(
&r0_vec, &v0_vec, dt, mu, R, rho0, H0, B, n_steps
)?;
let r_array = ndarray::arr1(&[r_vec.x, r_vec.y, r_vec.z]);
let v_array = ndarray::arr1(&[v_vec.x, v_vec.y, v_vec.z]);
Ok((
PyArray1::from_owned_array_bound(py, r_array),
PyArray1::from_owned_array_bound(py, v_array),
))
}
#[pyfunction]
#[pyo3(name = "propagate_drag_dopri5", signature = (r0, v0, dt, mu, R, rho0, H0, B, tol=None))]
fn py_propagate_drag_dopri5<'py>(
py: Python<'py>,
r0: PyReadonlyArray1<f64>,
v0: PyReadonlyArray1<f64>,
dt: f64,
mu: f64,
R: f64,
rho0: f64,
H0: f64,
B: f64,
tol: Option<f64>,
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
let r0_array = r0.as_array();
let v0_array = v0.as_array();
if r0_array.len() != 3 || v0_array.len() != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position and velocity vectors must have exactly 3 components"
));
}
let r0_vec = core::linalg::Vector3::new(r0_array[0], r0_array[1], r0_array[2]);
let v0_vec = core::linalg::Vector3::new(v0_array[0], v0_array[1], v0_array[2]);
let (r_vec, v_vec) = propagators::perturbations::propagate_drag_dopri5(
&r0_vec, &v0_vec, dt, mu, R, rho0, H0, B, tol
)?;
let r_array = ndarray::arr1(&[r_vec.x, r_vec.y, r_vec.z]);
let v_array = ndarray::arr1(&[v_vec.x, v_vec.y, v_vec.z]);
Ok((
PyArray1::from_owned_array_bound(py, r_array),
PyArray1::from_owned_array_bound(py, v_array),
))
}
#[pyfunction]
#[pyo3(name = "propagate_j2_drag_rk4", signature = (r0, v0, dt, mu, j2, R, rho0, H0, B, n_steps=None))]
fn py_propagate_j2_drag_rk4<'py>(
py: Python<'py>,
r0: PyReadonlyArray1<f64>,
v0: PyReadonlyArray1<f64>,
dt: f64,
mu: f64,
j2: f64,
R: f64,
rho0: f64,
H0: f64,
B: f64,
n_steps: Option<usize>,
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
let r0_array = r0.as_array();
let v0_array = v0.as_array();
if r0_array.len() != 3 || v0_array.len() != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position and velocity vectors must have exactly 3 components"
));
}
let r0_vec = core::linalg::Vector3::new(r0_array[0], r0_array[1], r0_array[2]);
let v0_vec = core::linalg::Vector3::new(v0_array[0], v0_array[1], v0_array[2]);
let (r_vec, v_vec) = propagators::perturbations::propagate_j2_drag_rk4(
&r0_vec, &v0_vec, dt, mu, j2, R, rho0, H0, B, n_steps
)?;
let r_array = ndarray::arr1(&[r_vec.x, r_vec.y, r_vec.z]);
let v_array = ndarray::arr1(&[v_vec.x, v_vec.y, v_vec.z]);
Ok((
PyArray1::from_owned_array_bound(py, r_array),
PyArray1::from_owned_array_bound(py, v_array),
))
}
#[pyfunction]
#[pyo3(name = "propagate_j2_drag_dopri5", signature = (r0, v0, dt, mu, j2, R, rho0, H0, B, tol=None))]
fn py_propagate_j2_drag_dopri5<'py>(
py: Python<'py>,
r0: PyReadonlyArray1<f64>,
v0: PyReadonlyArray1<f64>,
dt: f64,
mu: f64,
j2: f64,
R: f64,
rho0: f64,
H0: f64,
B: f64,
tol: Option<f64>,
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
let r0_array = r0.as_array();
let v0_array = v0.as_array();
if r0_array.len() != 3 || v0_array.len() != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position and velocity vectors must have exactly 3 components"
));
}
let r0_vec = core::linalg::Vector3::new(r0_array[0], r0_array[1], r0_array[2]);
let v0_vec = core::linalg::Vector3::new(v0_array[0], v0_array[1], v0_array[2]);
let (r_vec, v_vec) = propagators::perturbations::propagate_j2_drag_dopri5(
&r0_vec, &v0_vec, dt, mu, j2, R, rho0, H0, B, tol
)?;
let r_array = ndarray::arr1(&[r_vec.x, r_vec.y, r_vec.z]);
let v_array = ndarray::arr1(&[v_vec.x, v_vec.y, v_vec.z]);
Ok((
PyArray1::from_owned_array_bound(py, r_array),
PyArray1::from_owned_array_bound(py, v_array),
))
}
#[pyfunction]
#[pyo3(name = "sun_position_simple")]
fn py_sun_position_simple(py: Python, t: f64) -> Bound<PyArray1<f64>> {
let r_sun = propagators::perturbations::sun_position_simple(t);
let r_array = ndarray::arr1(&[r_sun.x, r_sun.y, r_sun.z]);
PyArray1::from_owned_array_bound(py, r_array)
}
#[pyfunction]
#[pyo3(name = "moon_position_simple")]
fn py_moon_position_simple(py: Python, t: f64) -> Bound<PyArray1<f64>> {
let r_moon = propagators::perturbations::moon_position_simple(t);
let r_array = ndarray::arr1(&[r_moon.x, r_moon.y, r_moon.z]);
PyArray1::from_owned_array_bound(py, r_array)
}
#[pyfunction]
#[pyo3(name = "third_body_perturbation")]
fn py_third_body_perturbation<'py>(
py: Python<'py>,
r: PyReadonlyArray1<f64>,
r_third: PyReadonlyArray1<f64>,
mu_third: f64,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let r_array = r.as_array();
let r_third_array = r_third.as_array();
if r_array.len() != 3 || r_third_array.len() != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position vectors must have exactly 3 components"
));
}
let r_vec = core::linalg::Vector3::new(r_array[0], r_array[1], r_array[2]);
let r_third_vec = core::linalg::Vector3::new(r_third_array[0], r_third_array[1], r_third_array[2]);
let a_vec = propagators::perturbations::third_body_perturbation(&r_vec, &r_third_vec, mu_third);
let a_array = ndarray::arr1(&[a_vec.x, a_vec.y, a_vec.z]);
Ok(PyArray1::from_owned_array_bound(py, a_array))
}
#[pyfunction]
#[pyo3(name = "sun_moon_perturbation")]
fn py_sun_moon_perturbation<'py>(
py: Python<'py>,
r: PyReadonlyArray1<f64>,
t: f64,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let r_array = r.as_array();
if r_array.len() != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position vector must have exactly 3 components"
));
}
let r_vec = core::linalg::Vector3::new(r_array[0], r_array[1], r_array[2]);
let a_vec = propagators::perturbations::sun_moon_perturbation(&r_vec, t);
let a_array = ndarray::arr1(&[a_vec.x, a_vec.y, a_vec.z]);
Ok(PyArray1::from_owned_array_bound(py, a_array))
}
#[pyfunction]
#[pyo3(name = "propagate_thirdbody_rk4", signature = (r0, v0, dt, mu, t0, include_sun=true, include_moon=true, n_steps=None))]
fn py_propagate_thirdbody_rk4<'py>(
py: Python<'py>,
r0: PyReadonlyArray1<f64>,
v0: PyReadonlyArray1<f64>,
dt: f64,
mu: f64,
t0: f64,
include_sun: bool,
include_moon: bool,
n_steps: Option<usize>,
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
let r0_array = r0.as_array();
let v0_array = v0.as_array();
if r0_array.len() != 3 || v0_array.len() != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position and velocity vectors must have exactly 3 components"
));
}
let r0_vec = core::linalg::Vector3::new(r0_array[0], r0_array[1], r0_array[2]);
let v0_vec = core::linalg::Vector3::new(v0_array[0], v0_array[1], v0_array[2]);
let (r_vec, v_vec) = propagators::perturbations::propagate_thirdbody_rk4(
&r0_vec, &v0_vec, dt, mu, t0, include_sun, include_moon, n_steps
)?;
let r_array = ndarray::arr1(&[r_vec.x, r_vec.y, r_vec.z]);
let v_array = ndarray::arr1(&[v_vec.x, v_vec.y, v_vec.z]);
Ok((
PyArray1::from_owned_array_bound(py, r_array),
PyArray1::from_owned_array_bound(py, v_array),
))
}
#[pyfunction]
#[pyo3(name = "propagate_thirdbody_dopri5", signature = (r0, v0, dt, mu, t0, include_sun=true, include_moon=true, tol=None))]
fn py_propagate_thirdbody_dopri5<'py>(
py: Python<'py>,
r0: PyReadonlyArray1<f64>,
v0: PyReadonlyArray1<f64>,
dt: f64,
mu: f64,
t0: f64,
include_sun: bool,
include_moon: bool,
tol: Option<f64>,
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
let r0_array = r0.as_array();
let v0_array = v0.as_array();
if r0_array.len() != 3 || v0_array.len() != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position and velocity vectors must have exactly 3 components"
));
}
let r0_vec = core::linalg::Vector3::new(r0_array[0], r0_array[1], r0_array[2]);
let v0_vec = core::linalg::Vector3::new(v0_array[0], v0_array[1], v0_array[2]);
let (r_vec, v_vec) = propagators::perturbations::propagate_thirdbody_dopri5(
&r0_vec, &v0_vec, dt, mu, t0, include_sun, include_moon, tol
)?;
let r_array = ndarray::arr1(&[r_vec.x, r_vec.y, r_vec.z]);
let v_array = ndarray::arr1(&[v_vec.x, v_vec.y, v_vec.z]);
Ok((
PyArray1::from_owned_array_bound(py, r_array),
PyArray1::from_owned_array_bound(py, v_array),
))
}
#[pyfunction]
#[pyo3(name = "shadow_function")]
fn py_shadow_function(
r_sat: PyReadonlyArray1<f64>,
r_sun: PyReadonlyArray1<f64>,
R_earth: f64,
) -> PyResult<f64> {
let r_sat_array = r_sat.as_array();
let r_sun_array = r_sun.as_array();
if r_sat_array.len() != 3 || r_sun_array.len() != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position vectors must have exactly 3 components"
));
}
let r_sat_vec = core::linalg::Vector3::new(r_sat_array[0], r_sat_array[1], r_sat_array[2]);
let r_sun_vec = core::linalg::Vector3::new(r_sun_array[0], r_sun_array[1], r_sun_array[2]);
let k = propagators::perturbations::shadow_function(&r_sat_vec, &r_sun_vec, R_earth);
Ok(k)
}
#[pyfunction]
#[pyo3(name = "srp_acceleration")]
fn py_srp_acceleration<'py>(
py: Python<'py>,
r_sat: PyReadonlyArray1<f64>,
r_sun: PyReadonlyArray1<f64>,
area_mass_ratio: f64,
C_r: f64,
R_earth: f64,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let r_sat_array = r_sat.as_array();
let r_sun_array = r_sun.as_array();
if r_sat_array.len() != 3 || r_sun_array.len() != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position vectors must have exactly 3 components"
));
}
let r_sat_vec = core::linalg::Vector3::new(r_sat_array[0], r_sat_array[1], r_sat_array[2]);
let r_sun_vec = core::linalg::Vector3::new(r_sun_array[0], r_sun_array[1], r_sun_array[2]);
let a_vec = propagators::perturbations::srp_acceleration(&r_sat_vec, &r_sun_vec, area_mass_ratio, C_r, R_earth);
let a_array = ndarray::arr1(&[a_vec.x, a_vec.y, a_vec.z]);
Ok(PyArray1::from_owned_array_bound(py, a_array))
}
#[pyfunction]
#[pyo3(name = "propagate_srp_rk4", signature = (r0, v0, dt, mu, area_mass_ratio, C_r, R_earth, t0, n_steps=None))]
fn py_propagate_srp_rk4<'py>(
py: Python<'py>,
r0: PyReadonlyArray1<f64>,
v0: PyReadonlyArray1<f64>,
dt: f64,
mu: f64,
area_mass_ratio: f64,
C_r: f64,
R_earth: f64,
t0: f64,
n_steps: Option<usize>,
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
let r0_array = r0.as_array();
let v0_array = v0.as_array();
if r0_array.len() != 3 || v0_array.len() != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position and velocity vectors must have exactly 3 components"
));
}
let r0_vec = core::linalg::Vector3::new(r0_array[0], r0_array[1], r0_array[2]);
let v0_vec = core::linalg::Vector3::new(v0_array[0], v0_array[1], v0_array[2]);
let (r_vec, v_vec) = propagators::perturbations::propagate_srp_rk4(
&r0_vec, &v0_vec, dt, mu, area_mass_ratio, C_r, R_earth, t0, n_steps
)?;
let r_array = ndarray::arr1(&[r_vec.x, r_vec.y, r_vec.z]);
let v_array = ndarray::arr1(&[v_vec.x, v_vec.y, v_vec.z]);
Ok((
PyArray1::from_owned_array_bound(py, r_array),
PyArray1::from_owned_array_bound(py, v_array),
))
}
#[pyfunction]
#[pyo3(name = "propagate_srp_dopri5", signature = (r0, v0, dt, mu, area_mass_ratio, C_r, R_earth, t0, tol=None))]
fn py_propagate_srp_dopri5<'py>(
py: Python<'py>,
r0: PyReadonlyArray1<f64>,
v0: PyReadonlyArray1<f64>,
dt: f64,
mu: f64,
area_mass_ratio: f64,
C_r: f64,
R_earth: f64,
t0: f64,
tol: Option<f64>,
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
let r0_array = r0.as_array();
let v0_array = v0.as_array();
if r0_array.len() != 3 || v0_array.len() != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position and velocity vectors must have exactly 3 components"
));
}
let r0_vec = core::linalg::Vector3::new(r0_array[0], r0_array[1], r0_array[2]);
let v0_vec = core::linalg::Vector3::new(v0_array[0], v0_array[1], v0_array[2]);
let (r_vec, v_vec) = propagators::perturbations::propagate_srp_dopri5(
&r0_vec, &v0_vec, dt, mu, area_mass_ratio, C_r, R_earth, t0, tol
)?;
let r_array = ndarray::arr1(&[r_vec.x, r_vec.y, r_vec.z]);
let v_array = ndarray::arr1(&[v_vec.x, v_vec.y, v_vec.z]);
Ok((
PyArray1::from_owned_array_bound(py, r_array),
PyArray1::from_owned_array_bound(py, v_array),
))
}
#[pyfunction]
#[pyo3(name = "propagate_stm_rk4", signature = (r0, v0, dt, mu, n_steps=100))]
fn py_propagate_stm_rk4<'py>(
py: Python<'py>,
r0: PyReadonlyArray1<f64>,
v0: PyReadonlyArray1<f64>,
dt: f64,
mu: f64,
n_steps: usize,
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>, Bound<'py, PyArray2<f64>>)> {
let r0_array = r0.as_array();
let v0_array = v0.as_array();
if r0_array.len() != 3 || v0_array.len() != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position and velocity vectors must have exactly 3 components"
));
}
let r0_vec = core::linalg::Vector3::new(r0_array[0], r0_array[1], r0_array[2]);
let v0_vec = core::linalg::Vector3::new(v0_array[0], v0_array[1], v0_array[2]);
let (r_vec, v_vec, stm) = propagators::stm::propagate_stm_rk4(
&r0_vec, &v0_vec, dt, mu, n_steps
)?;
let r_array = ndarray::arr1(&[r_vec.x, r_vec.y, r_vec.z]);
let v_array = ndarray::arr1(&[v_vec.x, v_vec.y, v_vec.z]);
let mut stm_array = ndarray::Array2::<f64>::zeros((6, 6));
for i in 0..6 {
for j in 0..6 {
stm_array[[i, j]] = stm[(i, j)];
}
}
Ok((
PyArray1::from_owned_array_bound(py, r_array),
PyArray1::from_owned_array_bound(py, v_array),
PyArray2::from_owned_array_bound(py, stm_array),
))
}
#[pyfunction]
#[pyo3(name = "propagate_stm_dopri5", signature = (r0, v0, dt, mu, tol=None))]
fn py_propagate_stm_dopri5<'py>(
py: Python<'py>,
r0: PyReadonlyArray1<f64>,
v0: PyReadonlyArray1<f64>,
dt: f64,
mu: f64,
tol: Option<f64>,
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>, Bound<'py, PyArray2<f64>>)> {
let r0_array = r0.as_array();
let v0_array = v0.as_array();
if r0_array.len() != 3 || v0_array.len() != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position and velocity vectors must have exactly 3 components"
));
}
let r0_vec = core::linalg::Vector3::new(r0_array[0], r0_array[1], r0_array[2]);
let v0_vec = core::linalg::Vector3::new(v0_array[0], v0_array[1], v0_array[2]);
let (r_vec, v_vec, stm) = propagators::stm::propagate_stm_dopri5(
&r0_vec, &v0_vec, dt, mu, tol
)?;
let r_array = ndarray::arr1(&[r_vec.x, r_vec.y, r_vec.z]);
let v_array = ndarray::arr1(&[v_vec.x, v_vec.y, v_vec.z]);
let mut stm_array = ndarray::Array2::<f64>::zeros((6, 6));
for i in 0..6 {
for j in 0..6 {
stm_array[[i, j]] = stm[(i, j)];
}
}
Ok((
PyArray1::from_owned_array_bound(py, r_array),
PyArray1::from_owned_array_bound(py, v_array),
PyArray2::from_owned_array_bound(py, stm_array),
))
}
#[pyfunction]
#[pyo3(name = "propagate_stm_j2_rk4", signature = (r0, v0, dt, mu, j2, R, n_steps=100))]
fn py_propagate_stm_j2_rk4<'py>(
py: Python<'py>,
r0: PyReadonlyArray1<f64>,
v0: PyReadonlyArray1<f64>,
dt: f64,
mu: f64,
j2: f64,
R: f64,
n_steps: usize,
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>, Bound<'py, PyArray2<f64>>)> {
let r0_array = r0.as_array();
let v0_array = v0.as_array();
if r0_array.len() != 3 || v0_array.len() != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position and velocity vectors must have exactly 3 components"
));
}
let r0_vec = core::linalg::Vector3::new(r0_array[0], r0_array[1], r0_array[2]);
let v0_vec = core::linalg::Vector3::new(v0_array[0], v0_array[1], v0_array[2]);
let (r_vec, v_vec, stm) = propagators::stm::propagate_stm_j2_rk4(
&r0_vec, &v0_vec, dt, mu, j2, R, n_steps
)?;
let r_array = ndarray::arr1(&[r_vec.x, r_vec.y, r_vec.z]);
let v_array = ndarray::arr1(&[v_vec.x, v_vec.y, v_vec.z]);
let mut stm_array = ndarray::Array2::<f64>::zeros((6, 6));
for i in 0..6 {
for j in 0..6 {
stm_array[[i, j]] = stm[(i, j)];
}
}
Ok((
PyArray1::from_owned_array_bound(py, r_array),
PyArray1::from_owned_array_bound(py, v_array),
PyArray2::from_owned_array_bound(py, stm_array),
))
}
#[pyfunction]
#[pyo3(name = "batch_gcrs_to_itrs")]
fn py_batch_gcrs_to_itrs<'py>(
py: Python<'py>,
positions: PyReadonlyArray2<f64>,
velocities: PyReadonlyArray2<f64>,
obstimes: Vec<core::time::Epoch>,
) -> PyResult<(Bound<'py, PyArray2<f64>>, Bound<'py, PyArray2<f64>>)> {
let pos_array = positions.as_array();
let vel_array = velocities.as_array();
if pos_array.shape()[1] != 3 || vel_array.shape()[1] != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position and velocity arrays must have shape (N, 3)"
));
}
if pos_array.shape()[0] != obstimes.len() {
return Err(pyo3::exceptions::PyValueError::new_err(
"Number of positions must match number of epochs"
));
}
let pos_vec: Vec<nalgebra::Vector3<f64>> = (0..pos_array.shape()[0])
.map(|i| nalgebra::Vector3::new(pos_array[[i, 0]], pos_array[[i, 1]], pos_array[[i, 2]]))
.collect();
let vel_vec: Vec<nalgebra::Vector3<f64>> = (0..vel_array.shape()[0])
.map(|i| nalgebra::Vector3::new(vel_array[[i, 0]], vel_array[[i, 1]], vel_array[[i, 2]]))
.collect();
let (itrs_pos, itrs_vel) = coordinates::frames::batch_gcrs_to_itrs(&pos_vec, &vel_vec, &obstimes)?;
let n = itrs_pos.len();
let mut pos_out = ndarray::Array2::<f64>::zeros((n, 3));
let mut vel_out = ndarray::Array2::<f64>::zeros((n, 3));
for (i, (p, v)) in itrs_pos.iter().zip(itrs_vel.iter()).enumerate() {
pos_out[[i, 0]] = p.x;
pos_out[[i, 1]] = p.y;
pos_out[[i, 2]] = p.z;
vel_out[[i, 0]] = v.x;
vel_out[[i, 1]] = v.y;
vel_out[[i, 2]] = v.z;
}
Ok((
PyArray2::from_owned_array_bound(py, pos_out),
PyArray2::from_owned_array_bound(py, vel_out),
))
}
#[pyfunction]
#[pyo3(name = "batch_itrs_to_gcrs")]
fn py_batch_itrs_to_gcrs<'py>(
py: Python<'py>,
positions: PyReadonlyArray2<f64>,
velocities: PyReadonlyArray2<f64>,
obstimes: Vec<core::time::Epoch>,
) -> PyResult<(Bound<'py, PyArray2<f64>>, Bound<'py, PyArray2<f64>>)> {
let pos_array = positions.as_array();
let vel_array = velocities.as_array();
if pos_array.shape()[1] != 3 || vel_array.shape()[1] != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position and velocity arrays must have shape (N, 3)"
));
}
if pos_array.shape()[0] != obstimes.len() {
return Err(pyo3::exceptions::PyValueError::new_err(
"Number of positions must match number of epochs"
));
}
let pos_vec: Vec<nalgebra::Vector3<f64>> = (0..pos_array.shape()[0])
.map(|i| nalgebra::Vector3::new(pos_array[[i, 0]], pos_array[[i, 1]], pos_array[[i, 2]]))
.collect();
let vel_vec: Vec<nalgebra::Vector3<f64>> = (0..vel_array.shape()[0])
.map(|i| nalgebra::Vector3::new(vel_array[[i, 0]], vel_array[[i, 1]], vel_array[[i, 2]]))
.collect();
let (gcrs_pos, gcrs_vel) = coordinates::frames::batch_itrs_to_gcrs(&pos_vec, &vel_vec, &obstimes)?;
let n = gcrs_pos.len();
let mut pos_out = ndarray::Array2::<f64>::zeros((n, 3));
let mut vel_out = ndarray::Array2::<f64>::zeros((n, 3));
for (i, (p, v)) in gcrs_pos.iter().zip(gcrs_vel.iter()).enumerate() {
pos_out[[i, 0]] = p.x;
pos_out[[i, 1]] = p.y;
pos_out[[i, 2]] = p.z;
vel_out[[i, 0]] = v.x;
vel_out[[i, 1]] = v.y;
vel_out[[i, 2]] = v.z;
}
Ok((
PyArray2::from_owned_array_bound(py, pos_out),
PyArray2::from_owned_array_bound(py, vel_out),
))
}
#[pyfunction]
#[pyo3(name = "batch_gcrs_to_teme")]
fn py_batch_gcrs_to_teme<'py>(
py: Python<'py>,
positions: PyReadonlyArray2<f64>,
velocities: PyReadonlyArray2<f64>,
obstimes: Vec<core::time::Epoch>,
) -> PyResult<(Bound<'py, PyArray2<f64>>, Bound<'py, PyArray2<f64>>)> {
let pos_array = positions.as_array();
let vel_array = velocities.as_array();
if pos_array.shape()[1] != 3 || vel_array.shape()[1] != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position and velocity arrays must have shape (N, 3)"
));
}
if pos_array.shape()[0] != obstimes.len() {
return Err(pyo3::exceptions::PyValueError::new_err(
"Number of positions must match number of epochs"
));
}
let pos_vec: Vec<nalgebra::Vector3<f64>> = (0..pos_array.shape()[0])
.map(|i| nalgebra::Vector3::new(pos_array[[i, 0]], pos_array[[i, 1]], pos_array[[i, 2]]))
.collect();
let vel_vec: Vec<nalgebra::Vector3<f64>> = (0..vel_array.shape()[0])
.map(|i| nalgebra::Vector3::new(vel_array[[i, 0]], vel_array[[i, 1]], vel_array[[i, 2]]))
.collect();
let (teme_pos, teme_vel) = coordinates::frames::batch_gcrs_to_teme(&pos_vec, &vel_vec, &obstimes)?;
let n = teme_pos.len();
let mut pos_out = ndarray::Array2::<f64>::zeros((n, 3));
let mut vel_out = ndarray::Array2::<f64>::zeros((n, 3));
for (i, (p, v)) in teme_pos.iter().zip(teme_vel.iter()).enumerate() {
pos_out[[i, 0]] = p.x;
pos_out[[i, 1]] = p.y;
pos_out[[i, 2]] = p.z;
vel_out[[i, 0]] = v.x;
vel_out[[i, 1]] = v.y;
vel_out[[i, 2]] = v.z;
}
Ok((
PyArray2::from_owned_array_bound(py, pos_out),
PyArray2::from_owned_array_bound(py, vel_out),
))
}
#[pyfunction]
#[pyo3(name = "batch_teme_to_gcrs")]
fn py_batch_teme_to_gcrs<'py>(
py: Python<'py>,
positions: PyReadonlyArray2<f64>,
velocities: PyReadonlyArray2<f64>,
obstimes: Vec<core::time::Epoch>,
) -> PyResult<(Bound<'py, PyArray2<f64>>, Bound<'py, PyArray2<f64>>)> {
let pos_array = positions.as_array();
let vel_array = velocities.as_array();
if pos_array.shape()[1] != 3 || vel_array.shape()[1] != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position and velocity arrays must have shape (N, 3)"
));
}
if pos_array.shape()[0] != obstimes.len() {
return Err(pyo3::exceptions::PyValueError::new_err(
"Number of positions must match number of epochs"
));
}
let pos_vec: Vec<nalgebra::Vector3<f64>> = (0..pos_array.shape()[0])
.map(|i| nalgebra::Vector3::new(pos_array[[i, 0]], pos_array[[i, 1]], pos_array[[i, 2]]))
.collect();
let vel_vec: Vec<nalgebra::Vector3<f64>> = (0..vel_array.shape()[0])
.map(|i| nalgebra::Vector3::new(vel_array[[i, 0]], vel_array[[i, 1]], vel_array[[i, 2]]))
.collect();
let (gcrs_pos, gcrs_vel) = coordinates::frames::batch_teme_to_gcrs(&pos_vec, &vel_vec, &obstimes)?;
let n = gcrs_pos.len();
let mut pos_out = ndarray::Array2::<f64>::zeros((n, 3));
let mut vel_out = ndarray::Array2::<f64>::zeros((n, 3));
for (i, (p, v)) in gcrs_pos.iter().zip(gcrs_vel.iter()).enumerate() {
pos_out[[i, 0]] = p.x;
pos_out[[i, 1]] = p.y;
pos_out[[i, 2]] = p.z;
vel_out[[i, 0]] = v.x;
vel_out[[i, 1]] = v.y;
vel_out[[i, 2]] = v.z;
}
Ok((
PyArray2::from_owned_array_bound(py, pos_out),
PyArray2::from_owned_array_bound(py, vel_out),
))
}
#[pyfunction]
#[pyo3(name = "batch_teme_to_itrs")]
fn py_batch_teme_to_itrs<'py>(
py: Python<'py>,
positions: PyReadonlyArray2<f64>,
velocities: PyReadonlyArray2<f64>,
obstimes: Vec<core::time::Epoch>,
) -> PyResult<(Bound<'py, PyArray2<f64>>, Bound<'py, PyArray2<f64>>)> {
let pos_array = positions.as_array();
let vel_array = velocities.as_array();
if pos_array.shape()[1] != 3 || vel_array.shape()[1] != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position and velocity arrays must have shape (N, 3)"
));
}
if pos_array.shape()[0] != obstimes.len() {
return Err(pyo3::exceptions::PyValueError::new_err(
"Number of positions must match number of epochs"
));
}
let pos_vec: Vec<nalgebra::Vector3<f64>> = (0..pos_array.shape()[0])
.map(|i| nalgebra::Vector3::new(pos_array[[i, 0]], pos_array[[i, 1]], pos_array[[i, 2]]))
.collect();
let vel_vec: Vec<nalgebra::Vector3<f64>> = (0..vel_array.shape()[0])
.map(|i| nalgebra::Vector3::new(vel_array[[i, 0]], vel_array[[i, 1]], vel_array[[i, 2]]))
.collect();
let (itrs_pos, itrs_vel) = coordinates::frames::batch_teme_to_itrs(&pos_vec, &vel_vec, &obstimes)?;
let n = itrs_pos.len();
let mut pos_out = ndarray::Array2::<f64>::zeros((n, 3));
let mut vel_out = ndarray::Array2::<f64>::zeros((n, 3));
for (i, (p, v)) in itrs_pos.iter().zip(itrs_vel.iter()).enumerate() {
pos_out[[i, 0]] = p.x;
pos_out[[i, 1]] = p.y;
pos_out[[i, 2]] = p.z;
vel_out[[i, 0]] = v.x;
vel_out[[i, 1]] = v.y;
vel_out[[i, 2]] = v.z;
}
Ok((
PyArray2::from_owned_array_bound(py, pos_out),
PyArray2::from_owned_array_bound(py, vel_out),
))
}
#[pyfunction]
#[pyo3(name = "batch_itrs_to_teme")]
fn py_batch_itrs_to_teme<'py>(
py: Python<'py>,
positions: PyReadonlyArray2<f64>,
velocities: PyReadonlyArray2<f64>,
obstimes: Vec<core::time::Epoch>,
) -> PyResult<(Bound<'py, PyArray2<f64>>, Bound<'py, PyArray2<f64>>)> {
let pos_array = positions.as_array();
let vel_array = velocities.as_array();
if pos_array.shape()[1] != 3 || vel_array.shape()[1] != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position and velocity arrays must have shape (N, 3)"
));
}
if pos_array.shape()[0] != obstimes.len() {
return Err(pyo3::exceptions::PyValueError::new_err(
"Number of positions must match number of epochs"
));
}
let pos_vec: Vec<nalgebra::Vector3<f64>> = (0..pos_array.shape()[0])
.map(|i| nalgebra::Vector3::new(pos_array[[i, 0]], pos_array[[i, 1]], pos_array[[i, 2]]))
.collect();
let vel_vec: Vec<nalgebra::Vector3<f64>> = (0..vel_array.shape()[0])
.map(|i| nalgebra::Vector3::new(vel_array[[i, 0]], vel_array[[i, 1]], vel_array[[i, 2]]))
.collect();
let (teme_pos, teme_vel) = coordinates::frames::batch_itrs_to_teme(&pos_vec, &vel_vec, &obstimes)?;
let n = teme_pos.len();
let mut pos_out = ndarray::Array2::<f64>::zeros((n, 3));
let mut vel_out = ndarray::Array2::<f64>::zeros((n, 3));
for (i, (p, v)) in teme_pos.iter().zip(teme_vel.iter()).enumerate() {
pos_out[[i, 0]] = p.x;
pos_out[[i, 1]] = p.y;
pos_out[[i, 2]] = p.z;
vel_out[[i, 0]] = v.x;
vel_out[[i, 1]] = v.y;
vel_out[[i, 2]] = v.z;
}
Ok((
PyArray2::from_owned_array_bound(py, pos_out),
PyArray2::from_owned_array_bound(py, vel_out),
))
}
#[pyfunction]
#[pyo3(name = "hohmann_transfer")]
fn py_hohmann_transfer(
py: Python<'_>,
r_initial: f64,
r_final: f64,
mu: f64,
) -> PyResult<PyObject> {
let result = maneuvers::HohmannTransfer::calculate(r_initial, r_final, mu)?;
let dict = pyo3::types::PyDict::new_bound(py);
dict.set_item("r_initial", result.r_initial)?;
dict.set_item("r_final", result.r_final)?;
dict.set_item("mu", result.mu)?;
dict.set_item("delta_v1", result.delta_v1)?;
dict.set_item("delta_v2", result.delta_v2)?;
dict.set_item("delta_v_total", result.delta_v_total)?;
dict.set_item("transfer_time", result.transfer_time)?;
dict.set_item("transfer_sma", result.transfer_sma)?;
dict.set_item("transfer_eccentricity", result.transfer_eccentricity)?;
dict.set_item("v_initial", result.v_initial)?;
dict.set_item("v_final", result.v_final)?;
dict.set_item("v_transfer_periapsis", result.v_transfer_periapsis)?;
dict.set_item("v_transfer_apoapsis", result.v_transfer_apoapsis)?;
Ok(dict.into())
}
#[pyfunction]
#[pyo3(name = "hohmann_phase_angle")]
fn py_hohmann_phase_angle(
r_initial: f64,
r_final: f64,
mu: f64,
) -> PyResult<f64> {
maneuvers::HohmannTransfer::phase_angle(r_initial, r_final, mu)
.map_err(|e| e.into())
}
#[pyfunction]
#[pyo3(name = "hohmann_synodic_period")]
fn py_hohmann_synodic_period(
r_initial: f64,
r_final: f64,
mu: f64,
) -> PyResult<f64> {
maneuvers::HohmannTransfer::synodic_period(r_initial, r_final, mu)
.map_err(|e| e.into())
}
#[pyfunction]
#[pyo3(name = "hohmann_time_to_window")]
fn py_hohmann_time_to_window(
current_phase: f64,
r_initial: f64,
r_final: f64,
mu: f64,
) -> PyResult<f64> {
maneuvers::HohmannTransfer::time_to_transfer_window(
current_phase,
r_initial,
r_final,
mu,
)
.map_err(|e| e.into())
}
#[pyfunction]
#[pyo3(name = "bielliptic_transfer")]
fn py_bielliptic_transfer(
py: Python<'_>,
r_initial: f64,
r_final: f64,
r_intermediate: f64,
mu: f64,
) -> PyResult<PyObject> {
let result = maneuvers::BiellipticTransfer::calculate(r_initial, r_final, r_intermediate, mu)?;
let dict = pyo3::types::PyDict::new_bound(py);
dict.set_item("r_initial", result.r_initial)?;
dict.set_item("r_final", result.r_final)?;
dict.set_item("r_intermediate", result.r_intermediate)?;
dict.set_item("mu", result.mu)?;
dict.set_item("delta_v1", result.delta_v1)?;
dict.set_item("delta_v2", result.delta_v2)?;
dict.set_item("delta_v3", result.delta_v3)?;
dict.set_item("delta_v_total", result.delta_v_total)?;
dict.set_item("transfer_time", result.transfer_time)?;
dict.set_item("transfer1_sma", result.transfer1_sma)?;
dict.set_item("transfer2_sma", result.transfer2_sma)?;
dict.set_item("transfer1_eccentricity", result.transfer1_eccentricity)?;
dict.set_item("transfer2_eccentricity", result.transfer2_eccentricity)?;
dict.set_item("v_initial", result.v_initial)?;
dict.set_item("v_final", result.v_final)?;
dict.set_item("v_transfer1_periapsis", result.v_transfer1_periapsis)?;
dict.set_item("v_transfer1_apoapsis", result.v_transfer1_apoapsis)?;
dict.set_item("v_transfer2_apoapsis", result.v_transfer2_apoapsis)?;
dict.set_item("v_transfer2_periapsis", result.v_transfer2_periapsis)?;
Ok(dict.into())
}
#[pyfunction]
#[pyo3(name = "compare_bielliptic_hohmann")]
fn py_compare_bielliptic_hohmann(
py: Python<'_>,
r_initial: f64,
r_final: f64,
r_intermediate: f64,
mu: f64,
) -> PyResult<PyObject> {
let (bielliptic, hohmann, dv_savings, time_penalty) =
maneuvers::BiellipticTransfer::compare_with_hohmann(r_initial, r_final, r_intermediate, mu)?;
let bielliptic_dict = pyo3::types::PyDict::new_bound(py);
bielliptic_dict.set_item("r_initial", bielliptic.r_initial)?;
bielliptic_dict.set_item("r_final", bielliptic.r_final)?;
bielliptic_dict.set_item("r_intermediate", bielliptic.r_intermediate)?;
bielliptic_dict.set_item("delta_v1", bielliptic.delta_v1)?;
bielliptic_dict.set_item("delta_v2", bielliptic.delta_v2)?;
bielliptic_dict.set_item("delta_v3", bielliptic.delta_v3)?;
bielliptic_dict.set_item("delta_v_total", bielliptic.delta_v_total)?;
bielliptic_dict.set_item("transfer_time", bielliptic.transfer_time)?;
let hohmann_dict = pyo3::types::PyDict::new_bound(py);
hohmann_dict.set_item("r_initial", hohmann.r_initial)?;
hohmann_dict.set_item("r_final", hohmann.r_final)?;
hohmann_dict.set_item("delta_v1", hohmann.delta_v1)?;
hohmann_dict.set_item("delta_v2", hohmann.delta_v2)?;
hohmann_dict.set_item("delta_v_total", hohmann.delta_v_total)?;
hohmann_dict.set_item("transfer_time", hohmann.transfer_time)?;
let dict = pyo3::types::PyDict::new_bound(py);
dict.set_item("bielliptic", bielliptic_dict)?;
dict.set_item("hohmann", hohmann_dict)?;
dict.set_item("dv_savings", dv_savings)?;
dict.set_item("time_penalty", time_penalty)?;
Ok(dict.into())
}
#[pyfunction]
#[pyo3(name = "find_optimal_bielliptic")]
fn py_find_optimal_bielliptic(
py: Python<'_>,
r_initial: f64,
r_final: f64,
mu: f64,
search_limit_factor: f64,
) -> PyResult<PyObject> {
let (r_opt, result) = maneuvers::BiellipticTransfer::find_optimal_intermediate(
r_initial,
r_final,
mu,
search_limit_factor,
)?;
let result_dict = pyo3::types::PyDict::new_bound(py);
result_dict.set_item("r_initial", result.r_initial)?;
result_dict.set_item("r_final", result.r_final)?;
result_dict.set_item("r_intermediate", result.r_intermediate)?;
result_dict.set_item("delta_v1", result.delta_v1)?;
result_dict.set_item("delta_v2", result.delta_v2)?;
result_dict.set_item("delta_v3", result.delta_v3)?;
result_dict.set_item("delta_v_total", result.delta_v_total)?;
result_dict.set_item("transfer_time", result.transfer_time)?;
let tuple = pyo3::types::PyTuple::new_bound(py, &[r_opt.to_object(py), result_dict.into()]);
Ok(tuple.into())
}
#[pyfunction]
#[pyo3(name = "pure_plane_change")]
fn py_pure_plane_change(
py: Python<'_>,
velocity: f64,
delta_angle: f64,
) -> PyResult<PyObject> {
let result = maneuvers::PlaneChange::pure_plane_change(velocity, delta_angle)?;
let dict = pyo3::types::PyDict::new_bound(py);
dict.set_item("velocity", result.velocity)?;
dict.set_item("delta_angle", result.delta_angle)?;
dict.set_item("delta_v", result.delta_v)?;
Ok(dict.into())
}
#[pyfunction]
#[pyo3(name = "combined_plane_change")]
fn py_combined_plane_change(
py: Python<'_>,
v_initial: f64,
v_final: f64,
delta_angle: f64,
) -> PyResult<PyObject> {
let result =
maneuvers::PlaneChange::combined_plane_change(v_initial, v_final, delta_angle)?;
let dict = pyo3::types::PyDict::new_bound(py);
dict.set_item("v_initial", result.v_initial)?;
dict.set_item("v_final", result.v_final)?;
dict.set_item("delta_angle", result.delta_angle)?;
dict.set_item("delta_v", result.delta_v)?;
dict.set_item("delta_v_orbit_only", result.delta_v_orbit_only)?;
dict.set_item("delta_v_plane_only", result.delta_v_plane_only)?;
dict.set_item("delta_v_penalty", result.delta_v_penalty)?;
Ok(dict.into())
}
#[pyfunction]
#[pyo3(name = "optimal_plane_change_location")]
fn py_optimal_plane_change_location(
py: Python<'_>,
v_low: f64,
v_high: f64,
v_transfer_low: f64,
v_transfer_high: f64,
total_angle: f64,
) -> PyResult<PyObject> {
let result = maneuvers::PlaneChange::optimal_plane_change_location(
v_low,
v_high,
v_transfer_low,
v_transfer_high,
total_angle,
)?;
let dict = pyo3::types::PyDict::new_bound(py);
dict.set_item("total_angle", result.total_angle)?;
dict.set_item("angle_at_first", result.angle_at_first)?;
dict.set_item("angle_at_second", result.angle_at_second)?;
dict.set_item("v_first", result.v_first)?;
dict.set_item("v_second", result.v_second)?;
dict.set_item("delta_v_total", result.delta_v_total)?;
dict.set_item("delta_v_first", result.delta_v_first)?;
dict.set_item("delta_v_second", result.delta_v_second)?;
dict.set_item("delta_v_saved", result.delta_v_saved)?;
dict.set_item("delta_v_saved_vs_low", result.delta_v_saved_vs_low)?;
Ok(dict.into())
}
#[pyfunction]
#[pyo3(name = "phasing_orbit")]
fn py_phasing_orbit(
py: Python<'_>,
r_original: f64,
phase_change: f64,
num_orbits: f64,
mu: f64,
) -> PyResult<PyObject> {
let result = maneuvers::Rendezvous::phasing_orbit(r_original, phase_change, num_orbits, mu)?;
let dict = pyo3::types::PyDict::new_bound(py);
dict.set_item("r_original", result.r_original)?;
dict.set_item("a_phasing", result.a_phasing)?;
dict.set_item("r_phasing_periapsis", result.r_phasing_periapsis)?;
dict.set_item("r_phasing_apoapsis", result.r_phasing_apoapsis)?;
dict.set_item("e_phasing", result.e_phasing)?;
dict.set_item("mu", result.mu)?;
dict.set_item("v_original", result.v_original)?;
dict.set_item("v_phasing_periapsis", result.v_phasing_periapsis)?;
dict.set_item("v_phasing_apoapsis", result.v_phasing_apoapsis)?;
dict.set_item("delta_v_enter", result.delta_v_enter)?;
dict.set_item("delta_v_exit", result.delta_v_exit)?;
dict.set_item("delta_v_total", result.delta_v_total)?;
dict.set_item("period_original", result.period_original)?;
dict.set_item("period_phasing", result.period_phasing)?;
dict.set_item("num_phasing_orbits", result.num_phasing_orbits)?;
dict.set_item("phasing_time", result.phasing_time)?;
dict.set_item("phase_change_per_orbit", result.phase_change_per_orbit)?;
dict.set_item("total_phase_change", result.total_phase_change)?;
Ok(dict.into())
}
#[pyfunction]
#[pyo3(name = "coorbital_rendezvous")]
fn py_coorbital_rendezvous(
py: Python<'_>,
r_orbit: f64,
phase_difference: f64,
num_phasing_orbits: f64,
mu: f64,
) -> PyResult<PyObject> {
let result = maneuvers::Rendezvous::coorbital_rendezvous(
r_orbit,
phase_difference,
num_phasing_orbits,
mu,
)?;
let phasing_dict = pyo3::types::PyDict::new_bound(py);
phasing_dict.set_item("r_original", result.phasing.r_original)?;
phasing_dict.set_item("a_phasing", result.phasing.a_phasing)?;
phasing_dict.set_item("r_phasing_periapsis", result.phasing.r_phasing_periapsis)?;
phasing_dict.set_item("r_phasing_apoapsis", result.phasing.r_phasing_apoapsis)?;
phasing_dict.set_item("e_phasing", result.phasing.e_phasing)?;
phasing_dict.set_item("mu", result.phasing.mu)?;
phasing_dict.set_item("v_original", result.phasing.v_original)?;
phasing_dict.set_item("v_phasing_periapsis", result.phasing.v_phasing_periapsis)?;
phasing_dict.set_item("v_phasing_apoapsis", result.phasing.v_phasing_apoapsis)?;
phasing_dict.set_item("delta_v_enter", result.phasing.delta_v_enter)?;
phasing_dict.set_item("delta_v_exit", result.phasing.delta_v_exit)?;
phasing_dict.set_item("delta_v_total", result.phasing.delta_v_total)?;
phasing_dict.set_item("period_original", result.phasing.period_original)?;
phasing_dict.set_item("period_phasing", result.phasing.period_phasing)?;
phasing_dict.set_item("num_phasing_orbits", result.phasing.num_phasing_orbits)?;
phasing_dict.set_item("phasing_time", result.phasing.phasing_time)?;
phasing_dict.set_item("phase_change_per_orbit", result.phasing.phase_change_per_orbit)?;
phasing_dict.set_item("total_phase_change", result.phasing.total_phase_change)?;
let dict = pyo3::types::PyDict::new_bound(py);
dict.set_item("r_orbit", result.r_orbit)?;
dict.set_item("mu", result.mu)?;
dict.set_item("initial_phase_difference", result.initial_phase_difference)?;
dict.set_item("chaser_ahead", result.chaser_ahead)?;
dict.set_item("phasing", phasing_dict)?;
Ok(dict.into())
}
#[pyfunction]
#[pyo3(name = "coplanar_rendezvous")]
fn py_coplanar_rendezvous(
py: Python<'_>,
r_chaser: f64,
r_target: f64,
current_phase: f64,
mu: f64,
) -> PyResult<PyObject> {
let result =
maneuvers::Rendezvous::coplanar_rendezvous(r_chaser, r_target, current_phase, mu)?;
let dict = pyo3::types::PyDict::new_bound(py);
dict.set_item("r_chaser", result.r_chaser)?;
dict.set_item("r_target", result.r_target)?;
dict.set_item("mu", result.mu)?;
dict.set_item("a_transfer", result.a_transfer)?;
dict.set_item("e_transfer", result.e_transfer)?;
dict.set_item("transfer_time", result.transfer_time)?;
dict.set_item("lead_angle", result.lead_angle)?;
dict.set_item("required_phase_angle", result.required_phase_angle)?;
dict.set_item("current_phase_angle", result.current_phase_angle)?;
dict.set_item("wait_time", result.wait_time)?;
dict.set_item("wait_orbits", result.wait_orbits)?;
dict.set_item("delta_v1", result.delta_v1)?;
dict.set_item("delta_v2", result.delta_v2)?;
dict.set_item("delta_v_total", result.delta_v_total)?;
Ok(dict.into())
}
#[pyfunction]
#[pyo3(
name = "delta_v_budget",
signature = (mission_name, maneuvers, contingency_margin=0.0, dry_mass=None, specific_impulse=None)
)]
fn py_delta_v_budget(
py: Python<'_>,
mission_name: String,
maneuvers: Vec<(String, f64)>,
contingency_margin: f64,
dry_mass: Option<f64>,
specific_impulse: Option<f64>,
) -> PyResult<PyObject> {
let mut budget = if contingency_margin > 0.0 {
maneuvers::DeltaVBudget::with_contingency(&mission_name, contingency_margin)?
} else {
maneuvers::DeltaVBudget::new(&mission_name)
};
for (name, delta_v) in maneuvers {
budget.add(&name, delta_v)?;
}
let result = budget.result(dry_mass, specific_impulse)?;
let dict = pyo3::types::PyDict::new_bound(py);
dict.set_item("mission_name", result.mission_name)?;
dict.set_item("total_delta_v", result.total_delta_v)?;
dict.set_item("contingency_margin", result.contingency_margin)?;
dict.set_item("total_with_contingency", result.total_with_contingency)?;
dict.set_item("num_maneuvers", result.num_maneuvers)?;
if let Some(dm) = result.dry_mass {
dict.set_item("dry_mass", dm)?;
}
if let Some(isp) = result.specific_impulse {
dict.set_item("specific_impulse", isp)?;
}
if let Some(pm) = result.propellant_mass {
dict.set_item("propellant_mass", pm)?;
}
if let Some(pf) = result.propellant_fraction {
dict.set_item("propellant_fraction", pf)?;
}
if let Some(tm) = result.total_mass {
dict.set_item("total_mass", tm)?;
}
Ok(dict.into())
}
#[pyfunction]
#[pyo3(name = "gravity_assist")]
fn py_gravity_assist(
py: Python<'_>,
v_infinity: f64,
r_periapsis: f64,
mu: f64,
) -> PyResult<PyObject> {
let result = maneuvers::GravityAssist::from_periapsis(v_infinity, r_periapsis, mu)?;
let dict = pyo3::types::PyDict::new_bound(py);
dict.set_item("v_infinity", result.v_infinity)?;
dict.set_item("r_periapsis", result.r_periapsis)?;
dict.set_item("mu", result.mu)?;
dict.set_item("eccentricity", result.eccentricity)?;
dict.set_item("delta", result.delta)?;
dict.set_item("delta_v_magnitude", result.delta_v_magnitude)?;
dict.set_item("semi_major_axis", result.semi_major_axis)?;
dict.set_item("theta_infinity", result.theta_infinity)?;
dict.set_item("b_parameter", result.b_parameter)?;
dict.set_item("specific_energy", result.specific_energy)?;
Ok(dict.into())
}
#[pyfunction]
#[pyo3(name = "periapsis_from_b_parameter")]
fn py_periapsis_from_b_parameter(
v_infinity: f64,
b_parameter: f64,
mu: f64,
) -> f64 {
maneuvers::GravityAssist::periapsis_from_b_parameter(v_infinity, b_parameter, mu)
}
#[pyfunction]
#[pyo3(name = "lambert_solve", signature = (r1, r2, tof, mu, short_way=true, revs=0))]
fn py_lambert_solve<'py>(
py: Python<'py>,
r1: PyReadonlyArray1<f64>,
r2: PyReadonlyArray1<f64>,
tof: f64,
mu: f64,
short_way: bool,
revs: u32,
) -> PyResult<PyObject> {
let r1_array = r1.as_array();
let r2_array = r2.as_array();
if r1_array.len() != 3 || r2_array.len() != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position vectors must have exactly 3 components",
));
}
let r1_vec = nalgebra::Vector3::new(r1_array[0], r1_array[1], r1_array[2]);
let r2_vec = nalgebra::Vector3::new(r2_array[0], r2_array[1], r2_array[2]);
let transfer_kind = if short_way {
maneuvers::TransferKind::ShortWay
} else {
maneuvers::TransferKind::LongWay
};
let solution = maneuvers::Lambert::solve(r1_vec, r2_vec, tof, mu, transfer_kind, revs)?;
let dict = pyo3::types::PyDict::new_bound(py);
let r1_out = ndarray::arr1(&[solution.r1[0], solution.r1[1], solution.r1[2]]);
let r2_out = ndarray::arr1(&[solution.r2[0], solution.r2[1], solution.r2[2]]);
let v1_out = ndarray::arr1(&[solution.v1[0], solution.v1[1], solution.v1[2]]);
let v2_out = ndarray::arr1(&[solution.v2[0], solution.v2[1], solution.v2[2]]);
dict.set_item("r1", PyArray1::from_owned_array_bound(py, r1_out))?;
dict.set_item("r2", PyArray1::from_owned_array_bound(py, r2_out))?;
dict.set_item("tof", solution.tof)?;
dict.set_item("v1", PyArray1::from_owned_array_bound(py, v1_out))?;
dict.set_item("v2", PyArray1::from_owned_array_bound(py, v2_out))?;
dict.set_item("mu", solution.mu)?;
dict.set_item("a", solution.a)?;
dict.set_item("e", solution.e)?;
dict.set_item("revs", solution.revs)?;
dict.set_item("short_way", solution.short_way)?;
Ok(dict.into())
}
#[pyfunction]
#[pyo3(name = "lambert_solve_batch", signature = (r1, r2, tofs, mu, short_way=true, revs=0))]
fn py_lambert_solve_batch<'py>(
py: Python<'py>,
r1: PyReadonlyArray1<f64>,
r2: PyReadonlyArray1<f64>,
tofs: PyReadonlyArray1<f64>,
mu: f64,
short_way: bool,
revs: u32,
) -> PyResult<Vec<PyObject>> {
let r1_array = r1.as_array();
let r2_array = r2.as_array();
let tofs_array = tofs.as_array();
if r1_array.len() != 3 || r2_array.len() != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position vectors must have exactly 3 components",
));
}
let r1_vec = nalgebra::Vector3::new(r1_array[0], r1_array[1], r1_array[2]);
let r2_vec = nalgebra::Vector3::new(r2_array[0], r2_array[1], r2_array[2]);
let transfer_kind = if short_way {
maneuvers::TransferKind::ShortWay
} else {
maneuvers::TransferKind::LongWay
};
let tofs_vec: Vec<f64> = tofs_array.to_vec();
let solutions = maneuvers::Lambert::solve_batch(r1_vec, r2_vec, &tofs_vec, mu, transfer_kind, revs)?;
solutions
.into_iter()
.map(|solution| {
let dict = pyo3::types::PyDict::new_bound(py);
let r1_out = ndarray::arr1(&[solution.r1[0], solution.r1[1], solution.r1[2]]);
let r2_out = ndarray::arr1(&[solution.r2[0], solution.r2[1], solution.r2[2]]);
let v1_out = ndarray::arr1(&[solution.v1[0], solution.v1[1], solution.v1[2]]);
let v2_out = ndarray::arr1(&[solution.v2[0], solution.v2[1], solution.v2[2]]);
dict.set_item("r1", PyArray1::from_owned_array_bound(py, r1_out))?;
dict.set_item("r2", PyArray1::from_owned_array_bound(py, r2_out))?;
dict.set_item("tof", solution.tof)?;
dict.set_item("v1", PyArray1::from_owned_array_bound(py, v1_out))?;
dict.set_item("v2", PyArray1::from_owned_array_bound(py, v2_out))?;
dict.set_item("mu", solution.mu)?;
dict.set_item("a", solution.a)?;
dict.set_item("e", solution.e)?;
dict.set_item("revs", solution.revs)?;
dict.set_item("short_way", solution.short_way)?;
Ok(dict.into())
})
.collect()
}
#[pyfunction]
#[pyo3(name = "lambert_solve_batch_parallel", signature = (r1s, r2s, tofs, mu, short_way=true, revs=0))]
fn py_lambert_solve_batch_parallel<'py>(
py: Python<'py>,
r1s: PyReadonlyArray2<f64>,
r2s: PyReadonlyArray2<f64>,
tofs: PyReadonlyArray1<f64>,
mu: f64,
short_way: bool,
revs: u32,
) -> PyResult<Vec<PyObject>> {
let r1s_array = r1s.as_array();
let r2s_array = r2s.as_array();
let tofs_array = tofs.as_array();
let n = r1s_array.shape()[0];
if r1s_array.shape()[1] != 3 || r2s_array.shape()[1] != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"Position vectors must have 3 components",
));
}
if r2s_array.shape()[0] != n || tofs_array.len() != n {
return Err(pyo3::exceptions::PyValueError::new_err(
"All arrays must have the same first dimension",
));
}
let r1s_vec: Vec<nalgebra::Vector3<f64>> = (0..n)
.map(|i| nalgebra::Vector3::new(r1s_array[[i, 0]], r1s_array[[i, 1]], r1s_array[[i, 2]]))
.collect();
let r2s_vec: Vec<nalgebra::Vector3<f64>> = (0..n)
.map(|i| nalgebra::Vector3::new(r2s_array[[i, 0]], r2s_array[[i, 1]], r2s_array[[i, 2]]))
.collect();
let tofs_vec: Vec<f64> = tofs_array.to_vec();
let transfer_kind = if short_way {
maneuvers::TransferKind::ShortWay
} else {
maneuvers::TransferKind::LongWay
};
let solutions = maneuvers::Lambert::solve_batch_parallel(&r1s_vec, &r2s_vec, &tofs_vec, mu, transfer_kind, revs)?;
solutions
.into_iter()
.map(|solution| {
let dict = pyo3::types::PyDict::new_bound(py);
let r1_out = ndarray::arr1(&[solution.r1[0], solution.r1[1], solution.r1[2]]);
let r2_out = ndarray::arr1(&[solution.r2[0], solution.r2[1], solution.r2[2]]);
let v1_out = ndarray::arr1(&[solution.v1[0], solution.v1[1], solution.v1[2]]);
let v2_out = ndarray::arr1(&[solution.v2[0], solution.v2[1], solution.v2[2]]);
dict.set_item("r1", PyArray1::from_owned_array_bound(py, r1_out))?;
dict.set_item("r2", PyArray1::from_owned_array_bound(py, r2_out))?;
dict.set_item("tof", solution.tof)?;
dict.set_item("v1", PyArray1::from_owned_array_bound(py, v1_out))?;
dict.set_item("v2", PyArray1::from_owned_array_bound(py, v2_out))?;
dict.set_item("mu", solution.mu)?;
dict.set_item("a", solution.a)?;
dict.set_item("e", solution.e)?;
dict.set_item("revs", solution.revs)?;
dict.set_item("short_way", solution.short_way)?;
Ok(dict.into())
})
.collect()
}
#[pyfunction]
fn py_propagate_tle<'py>(
py: Python<'py>,
tle_string: &str,
time_offset_minutes: f64,
) -> PyResult<PyObject> {
use crate::satellite::{parse_tle, propagate_from_elements};
let elements = parse_tle(tle_string)?;
let state = propagate_from_elements(&elements, time_offset_minutes)?;
let dict = pyo3::types::PyDict::new_bound(py);
let position = ndarray::arr1(&state.position);
let velocity = ndarray::arr1(&state.velocity);
dict.set_item("position", PyArray1::from_owned_array_bound(py, position))?;
dict.set_item("velocity", PyArray1::from_owned_array_bound(py, velocity))?;
dict.set_item("time_offset_minutes", state.time_offset_minutes)?;
dict.set_item("altitude_km", state.position_magnitude() - 6378.137)?; dict.set_item("speed_km_s", state.velocity_magnitude())?;
Ok(dict.into())
}
#[pyfunction]
fn py_propagate_tle_batch<'py>(
py: Python<'py>,
tle_string: &str,
time_offsets_minutes: PyReadonlyArray1<f64>,
) -> PyResult<Vec<PyObject>> {
use crate::satellite::{parse_tle, propagate_batch};
let elements = parse_tle(tle_string)?;
let time_offsets = time_offsets_minutes.as_array().to_vec();
let states = propagate_batch(&elements, &time_offsets)?;
states
.into_iter()
.map(|state| {
let dict = pyo3::types::PyDict::new_bound(py);
let position = ndarray::arr1(&state.position);
let velocity = ndarray::arr1(&state.velocity);
dict.set_item("position", PyArray1::from_owned_array_bound(py, position))?;
dict.set_item("velocity", PyArray1::from_owned_array_bound(py, velocity))?;
dict.set_item("time_offset_minutes", state.time_offset_minutes)?;
dict.set_item("altitude_km", state.position_magnitude() - 6378.137)?;
dict.set_item("speed_km_s", state.velocity_magnitude())?;
Ok(dict.into())
})
.collect()
}
#[pyfunction]
fn py_propagate_omm<'py>(
py: Python<'py>,
omm_json: &str,
time_offset_minutes: f64,
) -> PyResult<PyObject> {
use crate::satellite::{parse_omm, propagate_from_elements};
let elements = parse_omm(omm_json)?;
let state = propagate_from_elements(&elements, time_offset_minutes)?;
let dict = pyo3::types::PyDict::new_bound(py);
let position = ndarray::arr1(&state.position);
let velocity = ndarray::arr1(&state.velocity);
dict.set_item("position", PyArray1::from_owned_array_bound(py, position))?;
dict.set_item("velocity", PyArray1::from_owned_array_bound(py, velocity))?;
dict.set_item("time_offset_minutes", state.time_offset_minutes)?;
dict.set_item("altitude_km", state.position_magnitude() - 6378.137)?;
dict.set_item("speed_km_s", state.velocity_magnitude())?;
Ok(dict.into())
}
#[pyfunction]
fn py_compute_azimuth_elevation<'py>(
py: Python<'py>,
sat_ecef: [f64; 3],
observer_lat_deg: f64,
observer_lon_deg: f64,
observer_alt_km: f64,
) -> PyResult<PyObject> {
use crate::satellite::visibility::{Observer, compute_azimuth_elevation};
let observer = Observer::new(
observer_lat_deg.to_radians(),
observer_lon_deg.to_radians(),
observer_alt_km,
);
let topo = compute_azimuth_elevation(&sat_ecef, &observer);
let dict = pyo3::types::PyDict::new_bound(py);
dict.set_item("azimuth_deg", topo.azimuth.to_degrees())?;
dict.set_item("elevation_deg", topo.elevation.to_degrees())?;
dict.set_item("range_km", topo.range)?;
dict.set_item("enu_east_km", topo.enu[0])?;
dict.set_item("enu_north_km", topo.enu[1])?;
dict.set_item("enu_up_km", topo.enu[2])?;
Ok(dict.into())
}
#[pyfunction]
fn py_compute_azimuth_elevation_rate<'py>(
py: Python<'py>,
sat_ecef: [f64; 3],
vel_ecef: [f64; 3],
observer_lat_deg: f64,
observer_lon_deg: f64,
observer_alt_km: f64,
) -> PyResult<PyObject> {
use crate::satellite::visibility::{Observer, compute_azimuth_elevation_rate};
let observer = Observer::new(
observer_lat_deg.to_radians(),
observer_lon_deg.to_radians(),
observer_alt_km,
);
let topo = compute_azimuth_elevation_rate(&sat_ecef, &vel_ecef, &observer);
let dict = pyo3::types::PyDict::new_bound(py);
dict.set_item("azimuth_deg", topo.azimuth.to_degrees())?;
dict.set_item("elevation_deg", topo.elevation.to_degrees())?;
dict.set_item("range_km", topo.range)?;
dict.set_item("range_rate_km_s", topo.range_rate.unwrap_or(0.0))?;
dict.set_item("enu_east_km", topo.enu[0])?;
dict.set_item("enu_north_km", topo.enu[1])?;
dict.set_item("enu_up_km", topo.enu[2])?;
Ok(dict.into())
}
#[pyfunction]
fn py_is_visible(
sat_ecef: [f64; 3],
observer_lat_deg: f64,
observer_lon_deg: f64,
observer_alt_km: f64,
min_elevation_deg: f64,
) -> PyResult<bool> {
use crate::satellite::visibility::{Observer, is_visible};
let observer = Observer::new(
observer_lat_deg.to_radians(),
observer_lon_deg.to_radians(),
observer_alt_km,
);
Ok(is_visible(&sat_ecef, &observer, min_elevation_deg.to_radians()))
}
#[pyfunction]
fn py_find_satellite_passes<'py>(
py: Python<'py>,
tle_string: &str,
observer_lat_deg: f64,
observer_lon_deg: f64,
observer_alt_km: f64,
start_time_minutes: f64,
end_time_minutes: f64,
min_elevation_deg: f64,
time_step_minutes: f64,
) -> PyResult<PyObject> {
use crate::satellite::{parse_tle, propagate_from_elements, visibility::{Observer, find_all_passes}};
let elements = parse_tle(tle_string)?;
let observer = Observer::new(
observer_lat_deg.to_radians(),
observer_lon_deg.to_radians(),
observer_alt_km,
);
let propagate_fn = |t_minutes: f64| -> [f64; 3] {
let state = propagate_from_elements(&elements, t_minutes)
.expect("SGP4 propagation failed");
state.position
};
let passes = find_all_passes(
&propagate_fn,
&observer,
start_time_minutes,
end_time_minutes,
min_elevation_deg.to_radians(),
time_step_minutes,
);
let py_list = pyo3::types::PyList::empty_bound(py);
for pass in passes {
let dict = pyo3::types::PyDict::new_bound(py);
dict.set_item("rise_time_minutes", pass.rise_time)?;
dict.set_item("set_time_minutes", pass.set_time)?;
dict.set_item("max_elevation_time_minutes", pass.max_elevation_time)?;
dict.set_item("max_elevation_deg", pass.max_elevation.to_degrees())?;
dict.set_item("rise_azimuth_deg", pass.rise_azimuth.to_degrees())?;
dict.set_item("set_azimuth_deg", pass.set_azimuth.to_degrees())?;
dict.set_item("duration_minutes", pass.duration)?;
py_list.append(dict)?;
}
Ok(py_list.into())
}
#[pyfunction(name = "ecef_to_geodetic")]
fn py_ecef_to_geodetic<'py>(
py: Python<'py>,
ecef_position: PyReadonlyArray1<f64>,
) -> PyResult<PyObject> {
use crate::satellite::groundtrack::ecef_to_geodetic;
let ecef = ecef_position.as_array();
if ecef.len() != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"ECEF position must be a 3-element array [x, y, z]"
));
}
let ecef_arr = [ecef[0], ecef[1], ecef[2]];
let geodetic = ecef_to_geodetic(&ecef_arr);
let dict = pyo3::types::PyDict::new_bound(py);
dict.set_item("latitude_deg", geodetic.latitude.to_degrees())?;
dict.set_item("longitude_deg", geodetic.longitude.to_degrees())?;
dict.set_item("altitude_km", geodetic.altitude)?;
Ok(dict.into())
}
#[pyfunction(name = "compute_ground_track")]
fn py_compute_ground_track<'py>(
py: Python<'py>,
ecef_positions: PyReadonlyArray2<f64>,
times_minutes: PyReadonlyArray1<f64>,
) -> PyResult<PyObject> {
use crate::satellite::groundtrack::compute_ground_track;
let positions = ecef_positions.as_array();
let times = times_minutes.as_array();
if positions.shape()[1] != 3 {
return Err(pyo3::exceptions::PyValueError::new_err(
"ECEF positions must have shape (N, 3)"
));
}
if positions.shape()[0] != times.len() {
return Err(pyo3::exceptions::PyValueError::new_err(
format!("Number of positions ({}) must match number of times ({})",
positions.shape()[0], times.len())
));
}
let n = positions.shape()[0];
let mut pos_vec = Vec::with_capacity(n);
for i in 0..n {
pos_vec.push([positions[[i, 0]], positions[[i, 1]], positions[[i, 2]]]);
}
let times_vec: Vec<f64> = times.to_vec();
let track = compute_ground_track(&pos_vec, ×_vec);
let n_points = track.len();
let mut lats = Vec::with_capacity(n_points);
let mut lons = Vec::with_capacity(n_points);
let mut alts = Vec::with_capacity(n_points);
let mut times_out = Vec::with_capacity(n_points);
for point in track {
lats.push(point.latitude.to_degrees());
lons.push(point.longitude.to_degrees());
alts.push(point.altitude);
times_out.push(point.time);
}
let dict = pyo3::types::PyDict::new_bound(py);
dict.set_item("latitudes_deg", numpy::PyArray1::from_vec_bound(py, lats))?;
dict.set_item("longitudes_deg", numpy::PyArray1::from_vec_bound(py, lons))?;
dict.set_item("altitudes_km", numpy::PyArray1::from_vec_bound(py, alts))?;
dict.set_item("times_minutes", numpy::PyArray1::from_vec_bound(py, times_out))?;
Ok(dict.into())
}
#[pyfunction(name = "maximum_ground_range")]
fn py_maximum_ground_range(altitude_km: f64) -> f64 {
use crate::satellite::groundtrack::maximum_ground_range;
maximum_ground_range(altitude_km)
}
#[pyfunction(name = "calculate_swath_width")]
fn py_calculate_swath_width(altitude_km: f64, min_elevation_deg: f64) -> f64 {
use crate::satellite::groundtrack::calculate_swath_width;
calculate_swath_width(altitude_km, min_elevation_deg.to_radians())
}
#[pyfunction(name = "visibility_circle")]
fn py_visibility_circle<'py>(
py: Python<'py>,
lat_sub_deg: f64,
lon_sub_deg: f64,
altitude_km: f64,
min_elevation_deg: f64,
num_points: Option<usize>,
) -> PyResult<Bound<'py, pyo3::types::PyDict>> {
use crate::satellite::coverage::visibility_circle;
use numpy::PyArray1;
let num_pts = num_points.unwrap_or(64);
let circle = visibility_circle(lat_sub_deg, lon_sub_deg, altitude_km, min_elevation_deg, num_pts);
let latitudes: Vec<f64> = circle.iter().map(|p| p.latitude).collect();
let longitudes: Vec<f64> = circle.iter().map(|p| p.longitude).collect();
let r_sat = 6371.0 + altitude_km;
let elev_rad = min_elevation_deg.to_radians();
let sin_eta = elev_rad.cos() * 6371.0 / r_sat;
let eta = sin_eta.asin();
let lambda = (std::f64::consts::PI / 2.0) - elev_rad - eta;
let angular_radius_deg = lambda.to_degrees();
let result = pyo3::types::PyDict::new_bound(py);
result.set_item("latitudes", PyArray1::from_vec_bound(py, latitudes))?;
result.set_item("longitudes", PyArray1::from_vec_bound(py, longitudes))?;
result.set_item("num_points", num_pts)?;
result.set_item("angular_radius_deg", angular_radius_deg)?;
Ok(result)
}
#[pyfunction(name = "coverage_area")]
fn py_coverage_area(altitude_km: f64, min_elevation_deg: f64) -> f64 {
use crate::satellite::coverage::coverage_area;
coverage_area(altitude_km, min_elevation_deg)
}
#[pyfunction(name = "coverage_percentage")]
fn py_coverage_percentage(total_access_time_minutes: f64, time_span_minutes: f64) -> f64 {
use crate::satellite::coverage::coverage_percentage;
coverage_percentage(total_access_time_minutes, time_span_minutes)
}
#[pyfunction(name = "compute_eclipse_state")]
fn py_compute_eclipse_state(r_sat_km: [f64; 3], r_sun_km: [f64; 3]) -> &'static str {
use crate::satellite::eclipse::{compute_eclipse_state, EclipseState};
use nalgebra::Vector3;
let r_sat = Vector3::new(r_sat_km[0] * 1000.0, r_sat_km[1] * 1000.0, r_sat_km[2] * 1000.0);
let r_sun = Vector3::new(r_sun_km[0] * 1000.0, r_sun_km[1] * 1000.0, r_sun_km[2] * 1000.0);
let state = compute_eclipse_state(&r_sat, &r_sun);
match state {
EclipseState::Sunlit => "Sunlit",
EclipseState::Penumbra => "Penumbra",
EclipseState::Umbra => "Umbra",
}
}
#[pyfunction(name = "solar_beta_angle")]
fn py_solar_beta_angle(
inclination_deg: f64,
raan_deg: f64,
solar_longitude_deg: f64,
) -> f64 {
use crate::satellite::eclipse::solar_beta_angle;
let i = inclination_deg.to_radians();
let raan = raan_deg.to_radians();
let solar_lon = solar_longitude_deg.to_radians();
solar_beta_angle(i, raan, solar_lon).to_degrees()
}
#[pyfunction(name = "solar_beta_angle_precise")]
fn py_solar_beta_angle_precise(
inclination_deg: f64,
raan_deg: f64,
solar_longitude_deg: f64,
) -> f64 {
use crate::satellite::eclipse::solar_beta_angle_precise;
let i = inclination_deg.to_radians();
let raan = raan_deg.to_radians();
let solar_lon = solar_longitude_deg.to_radians();
solar_beta_angle_precise(i, raan, solar_lon).to_degrees()
}
#[pyfunction(name = "sun_synchronous_inclination", signature = (altitude_km, eccentricity=0.0))]
fn py_sun_synchronous_inclination(altitude_km: f64, eccentricity: f64) -> PyResult<f64> {
use crate::satellite::eclipse::sun_synchronous_inclination;
use crate::core::constants::{GM_EARTH, R_EARTH, J2_EARTH};
let semi_major_axis = R_EARTH + altitude_km * 1000.0;
match sun_synchronous_inclination(semi_major_axis, eccentricity, J2_EARTH, R_EARTH, GM_EARTH) {
Ok(inclination) => Ok(inclination.to_degrees()),
Err(e) => Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
format!("Sun-synchronous orbit not possible: {e}")
)),
}
}
#[pyfunction(name = "eclipse_duration")]
fn py_eclipse_duration(altitude_km: f64, beta_angle_deg: f64) -> PyResult<f64> {
use crate::satellite::eclipse::eclipse_duration;
use crate::core::constants::{GM_EARTH, R_EARTH};
let semi_major_axis = R_EARTH + altitude_km * 1000.0;
let beta_angle = beta_angle_deg.to_radians();
match eclipse_duration(semi_major_axis, beta_angle, GM_EARTH) {
Ok(duration_sec) => Ok(duration_sec / 60.0), Err(e) => Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
format!("Eclipse duration calculation failed: {e}")
)),
}
}
#[pyfunction]
#[pyo3(name = "estimate_satellite_lifetime")]
fn py_estimate_satellite_lifetime(
r_km: [f64; 3],
v_km_s: [f64; 3],
ballistic_coeff: f64,
terminal_altitude_km: f64,
max_time_days: f64,
) -> PyResult<f64> {
use crate::satellite::lifetime::estimate_lifetime;
use crate::core::linalg::Vector3;
let r = Vector3::new(r_km[0] * 1000.0, r_km[1] * 1000.0, r_km[2] * 1000.0);
let v = Vector3::new(v_km_s[0] * 1000.0, v_km_s[1] * 1000.0, v_km_s[2] * 1000.0);
let terminal_altitude = terminal_altitude_km * 1000.0;
let max_time = max_time_days * 86400.0;
match estimate_lifetime(&r, &v, ballistic_coeff, terminal_altitude, max_time, 60.0) {
Ok(lifetime_sec) => Ok(lifetime_sec / 86400.0), Err(e) => Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
format!("Lifetime estimation failed: {e}")
)),
}
}
#[pyfunction]
#[pyo3(name = "estimate_decay_rate")]
fn py_estimate_decay_rate(altitude_km: f64, ballistic_coeff: f64) -> f64 {
use crate::satellite::lifetime::estimate_decay_rate;
let altitude = altitude_km * 1000.0; let decay_rate_m_per_day = estimate_decay_rate(altitude, ballistic_coeff);
decay_rate_m_per_day / 1000.0 }
#[pyfunction]
#[pyo3(name = "compute_conjunction")]
fn py_compute_conjunction(
r1_km: [f64; 3],
v1_km_s: [f64; 3],
r2_km: [f64; 3],
v2_km_s: [f64; 3],
search_window_hours: f64,
collision_threshold_km: f64,
) -> PyResult<(f64, f64, bool)> {
use crate::satellite::conjunction::compute_conjunction;
use crate::core::linalg::Vector3;
use crate::core::constants::GM_EARTH;
let r1 = Vector3::new(r1_km[0] * 1000.0, r1_km[1] * 1000.0, r1_km[2] * 1000.0);
let v1 = Vector3::new(v1_km_s[0] * 1000.0, v1_km_s[1] * 1000.0, v1_km_s[2] * 1000.0);
let r2 = Vector3::new(r2_km[0] * 1000.0, r2_km[1] * 1000.0, r2_km[2] * 1000.0);
let v2 = Vector3::new(v2_km_s[0] * 1000.0, v2_km_s[1] * 1000.0, v2_km_s[2] * 1000.0);
let search_window = search_window_hours * 3600.0; let collision_threshold = collision_threshold_km * 1000.0;
match compute_conjunction(&r1, &v1, &r2, &v2, GM_EARTH, search_window, collision_threshold) {
Ok(result) => Ok((
result.tca / 60.0, result.miss_distance / 1000.0, result.collision_risk,
)),
Err(e) => Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
format!("Conjunction analysis failed: {e}")
)),
}
}
#[pyfunction]
#[pyo3(name = "closest_approach_distance")]
fn py_closest_approach_distance(
r1_km: [f64; 3],
v1_km_s: [f64; 3],
r2_km: [f64; 3],
v2_km_s: [f64; 3],
search_window_hours: f64,
) -> PyResult<f64> {
use crate::satellite::conjunction::closest_approach_distance;
use crate::core::linalg::Vector3;
use crate::core::constants::GM_EARTH;
let r1 = Vector3::new(r1_km[0] * 1000.0, r1_km[1] * 1000.0, r1_km[2] * 1000.0);
let v1 = Vector3::new(v1_km_s[0] * 1000.0, v1_km_s[1] * 1000.0, v1_km_s[2] * 1000.0);
let r2 = Vector3::new(r2_km[0] * 1000.0, r2_km[1] * 1000.0, r2_km[2] * 1000.0);
let v2 = Vector3::new(v2_km_s[0] * 1000.0, v2_km_s[1] * 1000.0, v2_km_s[2] * 1000.0);
let search_window = search_window_hours * 3600.0;
match closest_approach_distance(&r1, &v1, &r2, &v2, GM_EARTH, search_window) {
Ok(distance) => Ok(distance / 1000.0), Err(e) => Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
format!("Closest approach calculation failed: {e}")
)),
}
}