diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..305a9b0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.ipynb filter=nbstripout +*.ipynb diff=ipynb diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 418a8bb..be02415 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -46,16 +46,23 @@ jobs: path: astro-data key: astro-data-v1 + - name: Install system dependencies for cartopy + run: sudo apt-get update && sudo apt-get install -y libgeos-dev libproj-dev proj-data + - name: Install satkit run: pip install -e . - name: Install docs dependencies run: pip install -r docs/requirements.txt + - name: Generate static plots + env: + SATKIT_DATA: astro-data + run: python docs/examples/gen_plots.py + - name: Build docs env: SATKIT_DATA: astro-data - PLOTLY_RENDERER: notebook run: mkdocs build - name: Upload pages artifact diff --git a/.gitignore b/.gitignore index 9c3b612..eecba4a 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ wheel # MkDocs build output site + +# Generated doc plots (built by docs/examples/gen_plots.py) +docs/images/*.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index f3030cb..335b05e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,108 @@ # Changelog +## 0.14.1 - 2026-03-21 + +### Lambert Targeting + +- Add Lambert's problem solver using Izzo's algorithm (2015) with Householder 4th-order iteration +- Handles all orbit types (elliptic, parabolic, hyperbolic) and 180-degree transfers +- Multi-revolution solution support +- Rust API: `satkit::lambert::lambert()` in new `lambert` module +- Python API: `satkit.lambert()` with full type stub and docstring +- 9 Rust tests, 6 Python tests + +### Documentation Overhaul + +- Switch all tutorial plots from Plotly to matplotlib with SciencePlots +- STIX serif fonts, SVG output, colorblind-friendly palette matching numeris docs +- Match numeris site theme: blue grey header with navigation tabs +- Muted steel blue link color (#4a7c96) +- Static SVG plots (density, forces) generated at build time via `docs/examples/gen_plots.py` +- Add Lambert Targeting tutorial with delta-v analysis, pork-chop plot, and orbit visualization +- Add Lambert solver to User Guide and API Reference +- Fix docstring formatting across all Python stubs: bullet style, Returns sections, clickable URLs, Notes admonitions +- Add concrete defs for all `@typing.overload` functions so mkdocstrings renders them +- Add `@typing.overload` signatures to `propresult.interp` for all call patterns +- CI/CD: add cartopy/certifi system deps, generate plots before build, remove plotly + +### Internal + +- 114 Rust tests pass (9 new Lambert) +- 58 Python tests pass (6 new Lambert) + +## 0.14.0 - 2026-03-20 + +### Breaking: Replace nalgebra with numeris + +The `nalgebra` dependency has been replaced with `numeris` 0.5.6 for all linear algebra. The built-in ODE solver module (`src/ode/`) has been removed in favor of the ODE solvers provided by `numeris`. This is a **breaking change** for Rust API consumers; the Python API is unchanged. + +### Rust API Changes + +- **Math types** (`satkit::mathtypes`): All type aliases now point to `numeris` types instead of `nalgebra`. `Vector`, `Matrix`, `Quaternion`, and `DMatrix` remain available with the same names. +- **Vector construction**: `Vector3::new(x, y, z)` is replaced by `numeris::vector![x, y, z]` +- **Matrix construction**: `Matrix3::new(a,b,c,d,e,f,g,h,i)` is replaced by `Matrix3::new([[a,b,c],[d,e,f],[g,h,i]])` +- **Identity matrix**: `Matrix::identity()` is replaced by `Matrix::eye()` +- **Quaternion axis rotations**: `Quaternion::from_axis_angle(&Vector3::z_axis(), θ)` is replaced by `Quaternion::rotz(θ)` (also `rotx`, `roty`) +- **Quaternion vector rotation**: `q.transform_vector(&v)` is replaced by `q * v` +- **Quaternion storage order**: Changed from nalgebra's `[x,y,z,w]` to numeris `[w,x,y,z]` (scalar-first). Component access uses `.w`, `.x`, `.y`, `.z` fields. +- **Block extraction**: `m.fixed_view::(i,j)` is replaced by `m.block::(i,j)` (returns owned copy) +- **Block insertion**: `m.fixed_view_mut::(i,j).copy_from(&src)` is replaced by `m.set_block(i, j, &src)` +- **Matrix inverse**: `m.try_inverse()` (returning `Option`) is replaced by `m.inverse()` (returning `Result`) +- **Cholesky**: `m.cholesky()` now returns `Result` instead of `Option` + +### ODE Module Removed + +The `satkit::ode` module (7 adaptive solvers, Rosenbrock, ODEState trait, ~2,500 lines) has been removed. Orbit propagation now uses `numeris::ode` solvers directly. The same solver algorithms are available (RKF45, RKTS54, RKV65, RKV87, RKV98, RKV98NoInterp, RODAS4). The `PropSettings` API and `Integrator` enum are unchanged. + +### nalgebra Interoperability + +If you need nalgebra types for interoperability with other crates, enable the `nalgebra` feature on `numeris`: + +```toml +numeris = { version = "0.5.6", features = ["nalgebra"] } +``` + +This provides zero-cost `From`/`Into` conversions between numeris and nalgebra matrix, vector, and dynamic matrix types. Both libraries use identical column-major storage, so conversions are a `memcpy`. + +### Python Bindings + +- No breaking changes to the Python API +- All internal conversions updated for numeris types +- Quaternion component order handling updated internally (transparent to Python users) + +### Code Simplification + +- Use `numeris::vector!` macro throughout instead of `Vector3::from_array([...])` +- Use `Quaternion::rotation_between()` from numeris instead of hand-rolled helper +- Remove `qrot_xcoord`/`qrot_ycoord`/`qrot_zcoord` wrappers; use `Quaternion::rotx`/`roty`/`rotz` directly +- Remove `satkit::filters` module (UKF); use `numeris::estimate::Ukf` instead +- Enable `estimate` feature on numeris + +### Dependency Cleanup + +- Remove `nalgebra` dependency entirely +- Remove `ndarray` dependency (unused; `numpy` crate re-exports it for Python bindings) +- Remove `cty` dependency; use `std::ffi::{c_double, c_int}` instead +- Remove `once_cell` dependency; use `std::sync::OnceLock` (stable since Rust 1.70) +- Remove redundant reference-based `Add`/`Sub` operator impls from `ITRFCoord` (both types are `Copy`) + +### Internal + +- Net reduction of ~11,000 lines of code (removed ODE module and filters module) +- 141 Rust tests pass (105 lib + 36 doc) +- 52 Python tests pass + + +## 0.13.0 - 2026-03-15 + +### Integrator and Gravity Model Selection + +- Add `Integrator` enum to `PropSettings` for selecting ODE solver (RKV98, RKV87, RKV65, RKTS54, RODAS4) +- Add `GravityModel` enum for selecting Earth gravity model (JGM3, JGM2, EGM96, ITU GRACE16) +- Optimize ODE solver hot paths + + ## 0.12.0 - 2026-03-02 ### API Improvements diff --git a/Cargo.toml b/Cargo.toml index 3b0d9e2..104b92c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = [".", "python"] [package] name = "satkit" -version = "0.13.0" +version = "0.14.1" edition = "2021" description = "Satellite Toolkit" readme = "README.md" @@ -22,12 +22,9 @@ name = "satkit" [dependencies] -nalgebra = { version = "0.34.0", features = ["serde-serialize"] } -ndarray = "0.17.1" -cty = "0.2.2" +numeris = { version = "0.5.6", features = ["serde", "estimate", "ode"] } num-traits = "0.2.19" thiserror = "2.0" -once_cell = "1.21" ureq = "3.1.2" json = "0.12.4" process_path = "0.1.4" @@ -41,7 +38,6 @@ rmpfit = "0.3.0" chrono = { version = "0.4", optional = true } [build-dependencies] -cc = { version = "1.2", features = ["parallel"] } chrono = "0.4" [dev-dependencies] diff --git a/README.md b/README.md index cb78e4a..4a4a187 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,14 @@ Seamless conversion between UTC, TAI, TT, TDB, UT1, and GPS time scales with ful - Fast analytical Sun/Moon models for lower-precision work - Sunrise/sunset and Moon phase calculations +### Linear Algebra + +SatKit uses [numeris](https://crates.io/crates/numeris) for all linear algebra (vectors, matrices, quaternions, ODE integration). If you also use nalgebra in your project, enable the `nalgebra` feature on numeris for zero-cost `From`/`Into` conversions between types: + +```toml +numeris = { version = "0.5.5", features = ["nalgebra"] } +``` + ### Cargo Features | Feature | Default | Description | @@ -165,7 +173,7 @@ The library is validated against: - **ICGEM** reference values for gravity field calculations - **GPS SP3** precise ephemerides for multi-day numerical propagation -106 unit tests and 36 doc-tests run on every commit across Linux, macOS, and Windows. +142 tests (106 unit + 36 doc-tests) run on every commit across Linux, macOS, and Windows. ## Documentation diff --git a/build.rs b/build.rs index 32c2163..943c61a 100644 --- a/build.rs +++ b/build.rs @@ -1,10 +1,7 @@ use std::process::Command; fn main() { - cc::Build::new() - .file("extern/nrlmsise/nrlmsise-00.c") - .file("extern/nrlmsise/nrlmsise-00_data.c") - .compile("nrlmsise"); + // NRLMSISE-00 is now pure Rust (src/nrlmsise.rs), no C compilation needed // Record git hash to compile-time environment variable let output = Command::new("git") diff --git a/docs/api/lambert.md b/docs/api/lambert.md new file mode 100644 index 0000000..9c6b91e --- /dev/null +++ b/docs/api/lambert.md @@ -0,0 +1,3 @@ +# Lambert Solver + +::: satkit.lambert diff --git a/docs/examples/gen_plots.py b/docs/examples/gen_plots.py new file mode 100644 index 0000000..4dfad96 --- /dev/null +++ b/docs/examples/gen_plots.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +"""Generate SVG plots for MkDocs documentation using matplotlib + SciencePlots. + + python docs/examples/gen_plots.py + +Writes SVG files to docs/images/. +""" + +import os +from pathlib import Path + +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import numpy as np + +import scienceplots # noqa: F401 + +plt.style.use(["science", "no-latex"]) + +plt.rcParams.update({ + "mathtext.fontset": "stix", + "font.family": "serif", + "font.serif": ["STIX Two Text", "STIXGeneral", "DejaVu Serif"], + "font.size": 13, + "axes.labelsize": 14, + "axes.titlesize": 15, + "xtick.labelsize": 12, + "ytick.labelsize": 12, + "legend.fontsize": 12, + "axes.formatter.use_mathtext": True, + "svg.fonttype": "none", + "axes.grid": True, + "grid.alpha": 0.3, + "grid.linestyle": "--", + "axes.prop_cycle": plt.cycler(color=[ + "#0077BB", "#EE7733", "#009988", "#CC3311", + "#33BBEE", "#EE3377", "#BBBBBB", + ]), +}) + +IMAGES = Path(__file__).resolve().parent.parent / "images" + +COLORS = [ + "#0077BB", # blue + "#EE7733", # orange + "#009988", # teal + "#CC3311", # red + "#33BBEE", # cyan + "#EE3377", # magenta + "#BBBBBB", # grey +] + + +def savefig(fig, name): + path = IMAGES / f"{name}.svg" + fig.savefig(path, format="svg", bbox_inches="tight") + plt.close(fig) + print(f" -> {path}") + + +# ── Air Density vs Solar Cycle ───────────────────────────────────────────── + +def make_density_plot(): + import satkit as sk + + start = sk.time(1995, 1, 1) + end = sk.time(2022, 12, 31) + duration = end - start + timearray = [start + sk.duration(days=x) + for x in np.linspace(0, duration.days, 4000)] + + rho_400 = [sk.density.nrlmsise(400e3, 0, 0, x)[0] for x in timearray] + rho_500 = [sk.density.nrlmsise(500e3, 0, 0, x)[0] for x in timearray] + + fig, ax = plt.subplots(figsize=(8, 4.5)) + dates = [t.as_datetime() for t in timearray] + ax.semilogy(dates, rho_400, color=COLORS[0], linewidth=1, + label="Altitude = 400 km") + ax.semilogy(dates, rho_500, color=COLORS[1], linewidth=1, + linestyle="--", label="Altitude = 500 km") + ax.set_xlabel("Year") + ax.set_ylabel(r"Density [kg/m$^3$]") + ax.set_title("Air Density Changes with Solar Cycle") + ax.legend() + fig.autofmt_xdate() + savefig(fig, "density_vs_solar_cycle") + + +# ── Satellite Forces vs Altitude ─────────────────────────────────────────── + +def make_force_plot(): + import satkit as sk + import math as m + + N = 1000 + range_arr = np.logspace(m.log10(6378.2e3 + 100e3), m.log10(50e6), N) + + grav1v = np.array([sk.gravity(np.array([a, 0, 0]), order=1) + for a in range_arr]) + grav1 = np.linalg.norm(grav1v, axis=1) + + grav2v = np.array([sk.gravity(np.array([a, 0, 0]), order=2) + for a in range_arr]) + grav2 = np.linalg.norm(grav2v - grav1v, axis=1) + + grav6v = np.array([sk.gravity(np.array([a, 0, 0]), order=6) + for a in range_arr]) + grav5v = np.array([sk.gravity(np.array([a, 0, 0]), order=5) + for a in range_arr]) + grav6 = np.linalg.norm(grav6v - grav5v, axis=1) + + aoverm = 0.01 + Cd = 2.2 + Cr = 1.0 + + didx = np.argwhere(range_arr - sk.consts.earth_radius < 800e3).flatten() + tm_max = sk.time(2001, 12, 1) + tm_min = sk.time(1996, 12, 1) + rho_max = np.array([sk.density.nrlmsise(a - sk.consts.earth_radius, 0, 0, tm_max)[0] + for a in range_arr[didx]]) + rho_min = np.array([sk.density.nrlmsise(a - sk.consts.earth_radius, 0, 0, tm_min)[0] + for a in range_arr[didx]]) + varr = np.sqrt(sk.consts.mu_earth / (range_arr + sk.consts.earth_radius)) + drag_max = 0.5 * rho_max * varr[didx]**2 * Cd * aoverm + drag_min = 0.5 * rho_min * varr[didx]**2 * Cd * aoverm + + moon_range = np.linalg.norm( + sk.jplephem.geocentric_pos(sk.solarsystem.Moon, sk.time(2023, 1, 1))) + moon = sk.consts.mu_moon * ( + (moon_range - range_arr)**(-2) - moon_range**(-2)) + sun = sk.consts.mu_sun * ( + (sk.consts.au - range_arr)**(-2) - sk.consts.au**(-2)) + + a_radiation = 4.56e-6 * 0.5 * Cr * aoverm * np.ones(range_arr.shape) + + def add_line(ax, x, y, text, frac=0.5, dx=-20, dy=-20): + ax.loglog(x, y, "k-", linewidth=1.5) + idx = int(len(x) * frac) + ax.annotate(text, xy=(x[idx], y[idx]), fontsize=10, + xytext=(dx, dy), textcoords="offset points", + arrowprops=dict(arrowstyle="->", color="gray")) + + fig, ax = plt.subplots(figsize=(8, 8)) + add_line(ax, range_arr / 1e3, grav1 / 1e3, "Gravity") + add_line(ax, range_arr / 1e3, grav2 / 1e3, "J2", 0.2, 0, -15) + add_line(ax, range_arr / 1e3, grav6 / 1e3, "J6", 0.8, 0, -15) + add_line(ax, range_arr[didx] / 1e3, drag_max / 1e3, + "Drag Max", 0.7, 30, 0) + add_line(ax, range_arr[didx] / 1e3, drag_min / 1e3, + "Drag Min", 0.8, 10, 30) + add_line(ax, range_arr / 1e3, moon / 1e3, "Moon", 0.8, -10, -15) + add_line(ax, range_arr / 1e3, sun / 1e3, "Sun", 0.7, -10, 15) + add_line(ax, range_arr / 1e3, a_radiation / 1e3, + "Radiation\nPressure", 0.3, -10, 15) + ax.set_xlabel("Distance from Earth Origin [km]") + ax.set_ylabel(r"Acceleration [km/s$^2$]") + ax.set_title("Satellite Forces vs Altitude") + ax.set_xlim(6378.1, 50e3) + savefig(fig, "force_vs_altitude") + + +if __name__ == "__main__": + os.makedirs(IMAGES, exist_ok=True) + print("Generating SVG plots...") + make_density_plot() + make_force_plot() + print("Done.") diff --git a/docs/guide/lambert.md b/docs/guide/lambert.md new file mode 100644 index 0000000..16fd6f2 --- /dev/null +++ b/docs/guide/lambert.md @@ -0,0 +1,158 @@ +# Lambert's Problem + +Lambert's problem is one of the fundamental problems in orbital mechanics: given two position vectors and the time of flight between them, determine the orbit that connects them. The solution yields the departure and arrival velocity vectors, which are essential for trajectory design, rendezvous planning, and interplanetary mission analysis. + +## Problem Statement + +Given: + +- $\vec{r}_1$ — position at departure (meters) +- $\vec{r}_2$ — position at arrival (meters) +- $\Delta t$ — time of flight (seconds) +- $\mu$ — gravitational parameter of the central body (m$^3$/s$^2$) + +Find the velocity vectors $\vec{v}_1$ and $\vec{v}_2$ such that a Keplerian orbit passes through $\vec{r}_1$ at time $t_0$ with velocity $\vec{v}_1$ and through $\vec{r}_2$ at time $t_0 + \Delta t$ with velocity $\vec{v}_2$. + +## Transfer Geometry + +The transfer angle $\Delta\theta$ between the two position vectors determines the geometry of the transfer. An important special case is the **180-degree transfer** (e.g., Hohmann transfer), where $\vec{r}_1$ and $\vec{r}_2$ are anti-parallel and the orbit plane is not uniquely defined by the two positions alone. In this case, the `prograde` parameter determines the orbit plane. + +### Prograde vs. Retrograde + +The `prograde` flag resolves the short-way / long-way ambiguity: + +- **Prograde** (`prograde=True`): the satellite moves counterclockwise when viewed from above the orbital plane (positive angular momentum in the $z$-direction). This is the standard direction for most Earth-orbiting satellites. +- **Retrograde** (`prograde=False`): the satellite moves clockwise. + +## Algorithm + +The `satkit` Lambert solver implements **Izzo's algorithm** (2015), the contemporary standard used by ESA's pykep, poliastro, and most modern astrodynamics libraries. + +### Lancaster-Blanchard Formulation + +The problem is parameterized by a single variable $x \in (-1, 1)$ for elliptic orbits, with $x > 1$ for hyperbolic transfers. The geometry is captured by the parameter $\lambda$: + +$$ +\lambda^2 = 1 - \frac{c}{s}, \quad s = \frac{|\vec{r}_1| + |\vec{r}_2| + c}{2} +$$ + +where $c = |\vec{r}_2 - \vec{r}_1|$ is the chord length and $s$ is the semiperimeter. An auxiliary variable $y$ relates $x$ and $\lambda$: + +$$ +y(x) = \sqrt{1 - \lambda^2(1 - x^2)} +$$ + +### Time-of-Flight Equation + +The non-dimensional time of flight $T = \Delta t \sqrt{2\mu / s^3}$ is expressed as: + +$$ +T(x) = \frac{\psi + M\pi}{\sqrt{|1-x^2|}} \cdot \frac{1}{1-x^2} + \frac{\lambda y - x}{1-x^2} +$$ + +where $\psi = \cos^{-1}(xy + \lambda(1-x^2))$ for elliptic orbits and $M$ is the number of complete revolutions. Near the parabolic boundary ($x \approx 1$), a Battin hypergeometric series avoids the numerical singularity. + +### Householder Iteration + +The equation $T(x) = T_\text{target}$ is solved using **Householder's 4th-order method**, which typically converges in 2-3 iterations. The first three derivatives of $T(x)$ are computed via Izzo's recurrence relations: + +$$ +T' = \frac{3Tx - 2 + 2\lambda^3 x / y}{1 - x^2} +$$ + +$$ +T'' = \frac{3T + 5xT' + 2(1-\lambda^2)\lambda^3 / y^3}{1 - x^2} +$$ + +$$ +T''' = \frac{7xT'' + 8T' - 6(1-\lambda^2)\lambda^5 x / y^5}{1-x^2} +$$ + +### Velocity Reconstruction + +Velocities are decomposed into radial and tangential components using the solution $x$: + +$$ +v_{r,1} = \frac{\gamma}{r_1}\left[(\lambda y - x) - \rho(\lambda y + x)\right], \quad v_{t} = \frac{\gamma \sigma (y + \lambda x)}{r} +$$ + +where $\gamma = \sqrt{\mu s / 2}$, $\rho = (r_1 - r_2)/c$, and $\sigma = \sqrt{1 - \rho^2}$. Angular momentum conservation is guaranteed by construction since the tangential momentum $r \cdot v_t = \gamma \sigma (y + \lambda x)$ is the same at both endpoints. + +### Multi-Revolution Solutions + +For sufficiently long times of flight, multiple orbits can connect the same two positions with different numbers of complete revolutions $M$. For each $M \geq 1$, up to two solutions exist (short-period and long-period), provided $T > T_\text{min}(M)$. The solver automatically finds all valid multi-revolution solutions. + +## Usage + +```python +import satkit +import numpy as np + +# Position vectors (meters) +r1 = np.array([7000e3, 0, 0]) +r2 = np.array([0, 7000e3, 0]) + +# Solve Lambert's problem (1-hour transfer) +solutions = satkit.lambert(r1, r2, 3600.0) +v1, v2 = solutions[0] + +print(f"Departure velocity: {v1} m/s") +print(f"Arrival velocity: {v2} m/s") +``` + +### Hohmann Transfer + +```python +import satkit +import numpy as np + +r1_mag = 7000e3 # LEO radius (meters) +r2_mag = 42164e3 # GEO radius (meters) + +r1 = np.array([r1_mag, 0, 0]) +r2 = np.array([-r2_mag, 0, 0]) # 180-degree transfer + +# Hohmann transfer time +a_transfer = (r1_mag + r2_mag) / 2 +tof = np.pi * np.sqrt(a_transfer**3 / satkit.consts.mu_earth) + +solutions = satkit.lambert(r1, r2, tof) +v1, v2 = solutions[0] + +# Compute delta-v +v_circ_1 = np.sqrt(satkit.consts.mu_earth / r1_mag) +v_circ_2 = np.sqrt(satkit.consts.mu_earth / r2_mag) +dv1 = np.linalg.norm(v1) - v_circ_1 +dv2 = v_circ_2 - np.linalg.norm(v2) +print(f"Delta-v at departure: {dv1:.1f} m/s") +print(f"Delta-v at arrival: {dv2:.1f} m/s") +print(f"Total delta-v: {dv1 + dv2:.1f} m/s") +``` + +### Parameters + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `r1` | array (3,) | *required* | Departure position vector (meters) | +| `r2` | array (3,) | *required* | Arrival position vector (meters) | +| `tof` | float | *required* | Time of flight (seconds), must be positive | +| `mu` | float | Earth $\mu$ | Gravitational parameter (m$^3$/s$^2$) | +| `prograde` | bool | `True` | Prograde (counterclockwise) or retrograde transfer | + +### Return Value + +A list of `(v1, v2)` tuples, where `v1` and `v2` are 3-element numpy arrays representing the departure and arrival velocity vectors in m/s. Currently returns the zero-revolution solution. + +## Applications + +- **Orbit transfer design**: compute the delta-v budget for orbit maneuvers +- **Rendezvous planning**: determine phasing and approach trajectories +- **Interplanetary trajectories**: by using the Sun's $\mu$ and heliocentric positions +- **Pork-chop plots**: sweep over departure/arrival dates to find optimal launch windows +- **Initial orbit determination**: given multiple position observations, estimate the orbit + +## References + +- D. Izzo, "Revisiting Lambert's problem," *Celestial Mechanics and Dynamical Astronomy*, vol. 121, pp. 1-15, 2015. +- R.H. Battin, *An Introduction to the Mathematics and Methods of Astrodynamics*, AIAA, 1999. +- D. Vallado, *Fundamentals of Astrodynamics and Applications*, 4th ed., Microcosm Press, 2013, Chapter 7. diff --git a/docs/images/density_vs_solar_cycle.svg b/docs/images/density_vs_solar_cycle.svg deleted file mode 100644 index 53ce31e..0000000 --- a/docs/images/density_vs_solar_cycle.svg +++ /dev/null @@ -1 +0,0 @@ -1995200020052010201520202.00e−145.00e−141.00e−132.00e−135.00e−131.00e−122.00e−125.00e−121.00e−112.00e−11Altitude = 400kmAltitude = 500kmAir Density Changes with Solar CycleYearDensity [kg/m​3] \ No newline at end of file diff --git a/docs/images/force_vs_altitude.svg b/docs/images/force_vs_altitude.svg deleted file mode 100644 index 8939e9f..0000000 --- a/docs/images/force_vs_altitude.svg +++ /dev/null @@ -1 +0,0 @@ -10k15k20k25k30k35k40k45k50k1.0e−141.0e−121.0e−101.0e−81.0e−61.0e−41.0e−2Satellite Forces vs AltitudeDistance from Earth Origin [km]Acceleration [km/s​2]GravityJ2J6Drag MaxDrag MinMoonSunRadiation Pressure \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt index 3d9a06e..bdf38df 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -6,10 +6,12 @@ mkdocs-jupyter>=0.25 # Notebook execution dependencies satkit-data>=0.9.0 numpy -plotly +matplotlib +scienceplots +cartopy +certifi ipykernel xmltodict pandas scipy requests -xmltodict diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 5029e29..ae79374 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -1,3 +1,13 @@ +/* --- Link colors --- */ + +:root { + --md-typeset-a-color: #4a7c96; +} + +[data-md-color-scheme="slate"] { + --md-typeset-a-color: #6fa8c4; +} + /* --- Navigation sidebar contrast --- */ /* Section titles (Getting Started, User Guide, etc.) */ @@ -53,19 +63,13 @@ width: auto; } -/* Constrain Plotly and other large embedded figures */ +/* Constrain large embedded figures */ .jp-OutputArea-output > div, .cell_output > div { max-width: 100%; overflow-x: auto; } -/* Plotly figures: scale to fit container */ -.plotly-graph-div { - max-width: 100% !important; - height: auto !important; -} - /* Images inside notebook outputs */ .jp-RenderedHTMLCommon img, .cell_output img { diff --git a/docs/tutorials/Atmospheric Density.ipynb b/docs/tutorials/Atmospheric Density.ipynb new file mode 100644 index 0000000..af42b85 --- /dev/null +++ b/docs/tutorials/Atmospheric Density.ipynb @@ -0,0 +1,309 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Atmospheric Density with NRLMSISE-00\n", + "\n", + "The [NRLMSISE-00](https://en.wikipedia.org/wiki/NRLMSISE-00) (Naval Research Laboratory Mass Spectrometer and Incoherent Scatter Radar Exosphere) model is the standard empirical model of Earth's upper atmosphere. It provides neutral temperature and density profiles from the ground to the exosphere (~1000 km).\n", + "\n", + "Atmospheric density is the dominant source of orbital drag for satellites in low Earth orbit (LEO). Even though the atmosphere above 200 km is extremely tenuous, the high orbital velocities (~7.5 km/s) produce significant drag forces that cause orbital decay. Accurate density estimates are essential for:\n", + "\n", + "- **Orbit prediction**: Drag is the largest non-gravitational perturbation for LEO satellites\n", + "- **Satellite lifetime estimation**: Determines how long a satellite remains in orbit\n", + "- **Conjunction screening**: Accurate propagation is needed for collision avoidance\n", + "- **Re-entry prediction**: Density uncertainty dominates re-entry timing errors\n", + "\n", + "This tutorial demonstrates how to use satkit's `nrlmsise00` function to query atmospheric density and explore how it varies with altitude, geographic location, and solar activity." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import satkit as sk\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import scienceplots # noqa: F401\n", + "\n", + "plt.style.use([\"science\", \"no-latex\"])\n", + "plt.rcParams.update({\n", + " \"mathtext.fontset\": \"stix\",\n", + " \"font.family\": \"serif\",\n", + " \"font.serif\": [\"STIX Two Text\", \"STIXGeneral\", \"DejaVu Serif\"],\n", + " \"font.size\": 13,\n", + " \"axes.labelsize\": 14,\n", + " \"axes.titlesize\": 15,\n", + " \"xtick.labelsize\": 12,\n", + " \"ytick.labelsize\": 12,\n", + " \"legend.fontsize\": 12,\n", + " \"axes.formatter.use_mathtext\": True,\n", + " \"svg.fonttype\": \"none\",\n", + " \"axes.grid\": True,\n", + " \"grid.alpha\": 0.3,\n", + " \"grid.linestyle\": \"--\",\n", + " \"axes.prop_cycle\": plt.cycler(color=[\n", + " \"#0077BB\", \"#EE7733\", \"#009988\", \"#CC3311\",\n", + " \"#33BBEE\", \"#EE3377\", \"#BBBBBB\",\n", + " ]),\n", + "})\n", + "%config InlineBackend.figure_formats = ['svg']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Basic Density Query\n", + "\n", + "The `sk.nrlmsise00` function returns a tuple of `(density, temperature)` where density is in kg/m^3 and temperature is in Kelvin.\n", + "\n", + "The function takes altitude in kilometers as a positional argument, with optional keyword arguments for latitude, longitude, time, and whether to use space weather data.\n", + "\n", + "When `use_spaceweather=True` (the default), the model uses historical or forecast F10.7 solar flux and geomagnetic Ap indices from satkit's built-in space weather database. When time or space weather data are not provided, it falls back to moderate default values (F10.7 = 150, Ap = 4)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Query density at 400 km altitude (typical ISS orbit)\n", + "# at latitude 40 deg N, longitude -75 deg W\n", + "# at a specific time\n", + "tm = sk.time(2024, 6, 15, 12, 0, 0)\n", + "\n", + "rho, temp = sk.nrlmsise00(\n", + " 400.0,\n", + " latitude_deg=40.0,\n", + " longitude_deg=-75.0,\n", + " time=tm,\n", + ")\n", + "\n", + "print(f\"Altitude: 400 km\")\n", + "print(f\"Density: {rho:.4e} kg/m^3\")\n", + "print(f\"Temperature: {temp:.1f} K\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Without specifying location or time, the model uses defaults\n", + "# (lat=0, lon=0, moderate solar activity)\n", + "rho_default, temp_default = sk.nrlmsise00(400.0, use_spaceweather=False)\n", + "\n", + "print(f\"Default density at 400 km: {rho_default:.4e} kg/m^3\")\n", + "print(f\"Default temperature at 400 km: {temp_default:.1f} K\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Density vs. Altitude\n", + "\n", + "Atmospheric density drops roughly exponentially with altitude. The plot below shows density from 100 km (the Karman line) to 1000 km on a logarithmic scale. Note that the density spans many orders of magnitude across this range." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Compute density at altitudes from 100 to 1000 km\n", + "altitudes = np.linspace(100, 1000, 200)\n", + "tm = sk.time(2024, 6, 15, 12, 0, 0)\n", + "\n", + "densities = np.array([\n", + " sk.nrlmsise00(alt, latitude_deg=40.0, longitude_deg=-75.0, time=tm)[0]\n", + " for alt in altitudes\n", + "])\n", + "\n", + "fig, ax = plt.subplots(figsize=(8, 5))\n", + "ax.semilogy(altitudes, densities)\n", + "ax.set_xlabel(\"Altitude (km)\", fontsize=13)\n", + "ax.set_ylabel(\"Density (kg/m$^3$)\", fontsize=13)\n", + "ax.set_title(\"NRLMSISE-00 Atmospheric Density vs. Altitude\", fontsize=14)\n", + "ax.grid(True, which=\"both\", alpha=0.3)\n", + "\n", + "# Annotate key altitudes\n", + "for label, h in [(\"ISS (~400 km)\", 400), (\"Hubble (~540 km)\", 540), (\"Starlink (~550 km)\", 550)]:\n", + " rho_ann = sk.nrlmsise00(float(h), latitude_deg=40.0, longitude_deg=-75.0, time=tm)[0]\n", + " ax.axhline(rho_ann, color=\"gray\", linestyle=\"--\", alpha=0.4)\n", + " ax.annotate(label, xy=(h, rho_ann), fontsize=10,\n", + " xytext=(h + 50, rho_ann * 3), arrowprops=dict(arrowstyle=\"->\", color=\"gray\"))\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Solar Activity Effects\n", + "\n", + "The upper atmosphere expands dramatically during periods of high solar activity. The F10.7 solar radio flux (measured in solar flux units, SFU) is the primary driver:\n", + "\n", + "- **Solar minimum**: F10.7 ~ 70 SFU\n", + "- **Solar maximum**: F10.7 ~ 200+ SFU\n", + "\n", + "At 400 km, density can vary by an order of magnitude between solar minimum and solar maximum. This makes solar activity forecasting critical for orbit prediction.\n", + "\n", + "We demonstrate this by querying density at two different times corresponding to recent solar minimum and solar maximum conditions. With `use_spaceweather=True`, the model automatically looks up the historical F10.7 and Ap values for the given time." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Solar minimum (late 2019) vs. solar maximum (late 2024)\n", + "tm_solar_min = sk.time(2019, 12, 15, 12, 0, 0)\n", + "tm_solar_max = sk.time(2024, 10, 15, 12, 0, 0)\n", + "\n", + "altitudes = np.linspace(150, 800, 150)\n", + "\n", + "rho_min = np.array([\n", + " sk.nrlmsise00(alt, latitude_deg=40.0, longitude_deg=0.0, time=tm_solar_min)[0]\n", + " for alt in altitudes\n", + "])\n", + "\n", + "rho_max = np.array([\n", + " sk.nrlmsise00(alt, latitude_deg=40.0, longitude_deg=0.0, time=tm_solar_max)[0]\n", + " for alt in altitudes\n", + "])\n", + "\n", + "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 5))\n", + "\n", + "# Left panel: density profiles\n", + "ax1.semilogy(altitudes, rho_min, label=\"Solar Min (Dec 2019)\", color=\"steelblue\")\n", + "ax1.semilogy(altitudes, rho_max, label=\"Solar Max (Oct 2024)\", color=\"firebrick\")\n", + "ax1.set_xlabel(\"Altitude (km)\", fontsize=13)\n", + "ax1.set_ylabel(\"Density (kg/m$^3$)\", fontsize=13)\n", + "ax1.set_title(\"Density: Solar Min vs. Max\", fontsize=14)\n", + "ax1.legend(fontsize=11)\n", + "ax1.grid(True, which=\"both\", alpha=0.3)\n", + "\n", + "# Right panel: ratio of solar max to solar min density\n", + "ratio = rho_max / rho_min\n", + "ax2.plot(altitudes, ratio, color=\"darkgreen\")\n", + "ax2.set_xlabel(\"Altitude (km)\", fontsize=13)\n", + "ax2.set_ylabel(\"Density Ratio (Max / Min)\", fontsize=13)\n", + "ax2.set_title(\"Solar Max / Solar Min Density Ratio\", fontsize=14)\n", + "ax2.grid(True, alpha=0.3)\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Practical Impact: Orbital Decay at Different Altitudes\n", + "\n", + "To see the practical effect of atmospheric drag, we propagate two LEO satellites at different altitudes and compare how their semi-major axes evolve over time. The higher satellite experiences far less drag due to the exponentially lower density.\n", + "\n", + "We use satkit's high-precision orbit propagator with the `satproperties_static` object to set the satellite's ballistic coefficient (Cd * A / m)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Earth parameters\n", + "R_EARTH = 6378.137e3 # meters\n", + "MU_EARTH = 3.986004418e14 # m^3/s^2\n", + "\n", + "def circular_orbit_state(alt_km):\n", + " \"\"\"Return GCRF state vector [x, y, z, vx, vy, vz] for a circular orbit at given altitude.\"\"\"\n", + " r = R_EARTH + alt_km * 1e3\n", + " v = np.sqrt(MU_EARTH / r)\n", + " # Place satellite at (r, 0, 0) with velocity (0, v, 0) for equatorial circular orbit\n", + " return np.array([r, 0.0, 0.0, 0.0, v, 0.0])\n", + "\n", + "def semi_major_axis(state):\n", + " \"\"\"Compute semi-major axis from Cartesian state vector.\"\"\"\n", + " r = np.linalg.norm(state[0:3])\n", + " v = np.linalg.norm(state[3:6])\n", + " return 1.0 / (2.0 / r - v**2 / MU_EARTH)\n", + "\n", + "\n", + "# Set up two satellites at 300 km and 500 km\n", + "# Typical ballistic coefficient: Cd*A/m ~ 0.01 m^2/kg\n", + "sp = sk.satproperties_static(cdaoverm=0.01)\n", + "\n", + "t0 = sk.time(2024, 6, 15, 0, 0, 0)\n", + "duration_days = 2.0\n", + "t_end = t0 + sk.duration(days=duration_days)\n", + "\n", + "settings = sk.propsettings(abs_error=1e-8, rel_error=1e-8)\n", + "settings.precompute_terms(t0, t_end)\n", + "\n", + "altitudes_prop = [300, 500]\n", + "colors = [\"firebrick\", \"steelblue\"]\n", + "\n", + "fig, ax = plt.subplots(figsize=(9, 5))\n", + "\n", + "for alt, color in zip(altitudes_prop, colors):\n", + " state0 = circular_orbit_state(alt)\n", + "\n", + " # Propagate with drag\n", + " result = sk.propagate(\n", + " state0, t0, end=t_end,\n", + " propsettings=settings,\n", + " satproperties=sp,\n", + " )\n", + "\n", + " # Sample the trajectory at regular intervals\n", + " n_samples = 500\n", + " times = [t0 + sk.duration(days=d) for d in np.linspace(0, duration_days, n_samples)]\n", + " sma = np.array([semi_major_axis(result.interp(t)) for t in times])\n", + " sma_alt = (sma - R_EARTH) / 1e3 # Convert to altitude in km\n", + " days = np.linspace(0, duration_days, n_samples)\n", + "\n", + " ax.plot(days, sma_alt, label=f\"{alt} km initial altitude\", color=color)\n", + "\n", + "ax.set_xlabel(\"Time (days)\", fontsize=13)\n", + "ax.set_ylabel(\"Semi-Major Axis Altitude (km)\", fontsize=13)\n", + "ax.set_title(\"Orbital Decay Due to Atmospheric Drag\", fontsize=14)\n", + "ax.legend(fontsize=11)\n", + "ax.grid(True, alpha=0.3)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The satellite at 300 km experiences substantially more drag than the one at 500 km, owing to the exponential density profile. This is why most operational LEO satellites orbit above 400 km -- lower altitudes require frequent orbit-raising maneuvers to maintain their orbit." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/tutorials/Coordinate Frame Transforms.ipynb b/docs/tutorials/Coordinate Frame Transforms.ipynb new file mode 100644 index 0000000..61654a5 --- /dev/null +++ b/docs/tutorials/Coordinate Frame Transforms.ipynb @@ -0,0 +1,362 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Coordinate Frame Transforms\n", + "\n", + "Satellite astrodynamics requires working in several coordinate reference frames. The three most common are:\n", + "\n", + "- **GCRF** (Geocentric Celestial Reference Frame) -- An Earth-centered inertial frame aligned with the J2000 equinox and equator. Equations of motion are simplest in this frame because it does not rotate with the Earth. Used for orbit propagation, conjunction analysis, and any dynamics computation.\n", + "\n", + "- **ITRF** (International Terrestrial Reference Frame) -- An Earth-centered, Earth-fixed frame that rotates with the Earth. Ground station positions, GPS coordinates, and geodetic quantities (latitude, longitude, altitude) are expressed in this frame.\n", + "\n", + "- **TEME** (True Equator, Mean Equinox) -- A quasi-inertial frame that is the native output of the SGP4 orbit propagator used with Two-Line Element sets (TLEs). It accounts for precession and a simplified nutation model but not the full IAU reduction. Positions from SGP4 must be rotated out of TEME before they can be compared with GCRF or ITRF data.\n", + "\n", + "All frame rotations in satkit are represented as unit quaternions. Applying a rotation to a 3-vector is done with the `*` operator: `v_out = q * v_in`.\n", + "\n", + "This tutorial demonstrates the rotation functions in `satkit.frametransform`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import satkit as sk\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import scienceplots # noqa: F401\n", + "\n", + "plt.style.use([\"science\", \"no-latex\"])\n", + "plt.rcParams.update({\n", + " \"mathtext.fontset\": \"stix\",\n", + " \"font.family\": \"serif\",\n", + " \"font.serif\": [\"STIX Two Text\", \"STIXGeneral\", \"DejaVu Serif\"],\n", + " \"font.size\": 13,\n", + " \"axes.labelsize\": 14,\n", + " \"axes.titlesize\": 15,\n", + " \"xtick.labelsize\": 12,\n", + " \"ytick.labelsize\": 12,\n", + " \"legend.fontsize\": 12,\n", + " \"axes.formatter.use_mathtext\": True,\n", + " \"svg.fonttype\": \"none\",\n", + " \"axes.grid\": True,\n", + " \"grid.alpha\": 0.3,\n", + " \"grid.linestyle\": \"--\",\n", + " \"axes.prop_cycle\": plt.cycler(color=[\n", + " \"#0077BB\", \"#EE7733\", \"#009988\", \"#CC3311\",\n", + " \"#33BBEE\", \"#EE3377\", \"#BBBBBB\",\n", + " ]),\n", + "})\n", + "%config InlineBackend.figure_formats = ['svg']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Basic Frame Rotations\n", + "\n", + "The `satkit.frametransform` module provides quaternion rotation functions between the major frames. Each function takes a time (or array of times) and returns the corresponding quaternion(s).\n", + "\n", + "| Function | Rotation |\n", + "|---|---|\n", + "| `qitrf2gcrf(t)` | ITRF -> GCRF (full IAU-2006) |\n", + "| `qgcrf2itrf(t)` | GCRF -> ITRF (full IAU-2006) |\n", + "| `qitrf2gcrf_approx(t)` | ITRF -> GCRF (approx., ~1 arcsec) |\n", + "| `qgcrf2itrf_approx(t)` | GCRF -> ITRF (approx., ~1 arcsec) |\n", + "| `qteme2gcrf(t)` | TEME -> GCRF |\n", + "| `qteme2itrf(t)` | TEME -> ITRF |\n", + "\n", + "Let's start with a concrete example: rotating a ground station's ITRF position into the GCRF inertial frame." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Define a ground station using geodetic coordinates\n", + "station = sk.itrfcoord(latitude_deg=42.0, longitude_deg=-71.0, altitude=100)\n", + "print(f'Station ITRF position: {station}')\n", + "print(f'ITRF Cartesian vector (m): {station.vector}')\n", + "\n", + "# Pick a specific time\n", + "t = sk.time(2024, 6, 15, 12, 0, 0)\n", + "\n", + "# Get the ITRF -> GCRF rotation quaternion at this time\n", + "q = sk.frametransform.qitrf2gcrf(t)\n", + "\n", + "# Rotate the ITRF position vector to GCRF\n", + "pos_gcrf = q * station.vector\n", + "print(f'\\nGCRF position at {t}: {pos_gcrf}')\n", + "print(f'Position magnitude: {np.linalg.norm(pos_gcrf):.1f} m (unchanged by rotation)')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that the magnitude of the position vector is preserved -- quaternion rotations are rigid-body rotations.\n", + "\n", + "The inverse rotation (GCRF back to ITRF) should recover the original vector:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Rotate back from GCRF to ITRF\n", + "q_inv = sk.frametransform.qgcrf2itrf(t)\n", + "pos_roundtrip = q_inv * pos_gcrf\n", + "\n", + "print(f'Original ITRF vector: {station.vector}')\n", + "print(f'Round-trip ITRF vector: {pos_roundtrip}')\n", + "print(f'Difference (m): {np.linalg.norm(pos_roundtrip - station.vector):.2e}')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Approximate vs Full IAU-2006 Reduction\n", + "\n", + "The full IAU-2006/2010 ITRF-GCRF rotation accounts for precession, nutation, Earth rotation angle, and polar motion using the complete IERS conventions. This is computationally expensive.\n", + "\n", + "The `_approx` variants use a simplified model that is accurate to approximately 1 arcsecond -- more than sufficient for many applications. Let's compare them." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Compare full and approximate ITRF -> GCRF rotation\n", + "t = sk.time(2024, 6, 15, 12, 0, 0)\n", + "\n", + "q_full = sk.frametransform.qitrf2gcrf(t)\n", + "q_approx = sk.frametransform.qitrf2gcrf_approx(t)\n", + "\n", + "# Rotate the ground station position with both\n", + "pos_full = q_full * station.vector\n", + "pos_approx = q_approx * station.vector\n", + "\n", + "# Position difference in meters\n", + "pos_diff = np.linalg.norm(pos_full - pos_approx)\n", + "print(f'Position difference: {pos_diff:.3f} m')\n", + "\n", + "# Convert to angular difference\n", + "# The angular error (in radians) can be estimated from the cross product of the\n", + "# unit vectors, or equivalently from the position difference at the Earth's surface\n", + "R_earth = 6371e3 # mean Earth radius, meters\n", + "angular_diff_arcsec = np.degrees(pos_diff / R_earth) * 3600\n", + "print(f'Angular difference: {angular_diff_arcsec:.4f} arcsec')\n", + "\n", + "# Compute the angle directly from the quaternion difference\n", + "# q_diff = q_full * q_approx.conjugate represents the rotation between the two\n", + "q_diff = q_full * q_approx.conjugate\n", + "angle_arcsec = np.degrees(q_diff.angle) * 3600\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The difference is well under 1 arcsecond, confirming the documentation. For applications where sub-arcsecond accuracy is not needed (e.g., ground track plotting, pass prediction), the approximate version is a good choice. For high-precision orbit determination, use the full reduction." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Time Dependence: Earth Rotation Over 24 Hours\n", + "\n", + "The ITRF-to-GCRF rotation changes continuously as the Earth rotates. The dominant component is the Earth Rotation Angle (ERA), which completes roughly one full cycle per sidereal day (~23h 56m).\n", + "\n", + "Let's visualize this by tracking how a ground station's GCRF position traces out a circle over 24 hours, and plot the Earth Rotation Angle directly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create a time array spanning 24 hours at 1-minute intervals\n", + "t0 = sk.time(2024, 6, 15, 0, 0, 0)\n", + "minutes = np.arange(0, 24 * 60 + 1)\n", + "times = np.array([t0 + sk.duration.from_minutes(m) for m in minutes])\n", + "\n", + "# Get the Earth Rotation Angle at each time\n", + "era = sk.frametransform.earth_rotation_angle(times)\n", + "\n", + "# Plot ERA over 24 hours\n", + "hours = minutes / 60.0\n", + "fig, ax = plt.subplots(figsize=(10, 4))\n", + "ax.plot(hours, np.degrees(era) % 360)\n", + "ax.set_xlabel('Hours since midnight UTC')\n", + "ax.set_ylabel('Earth Rotation Angle (deg)')\n", + "ax.set_title('Earth Rotation Angle Over 24 Hours')\n", + "ax.set_xlim(0, 24)\n", + "ax.grid(True, alpha=0.3)\n", + "plt.tight_layout()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Rotate the ground station position to GCRF at each time step\n", + "# and show how the inertial X-Y coordinates trace out a circle\n", + "q_arr = sk.frametransform.qitrf2gcrf(times)\n", + "pos_gcrf_arr = np.array([q * station.vector for q in q_arr])\n", + "\n", + "fig, ax = plt.subplots(figsize=(6, 6))\n", + "ax.plot(pos_gcrf_arr[:, 0] / 1e6, pos_gcrf_arr[:, 1] / 1e6, linewidth=0.5)\n", + "ax.plot(pos_gcrf_arr[0, 0] / 1e6, pos_gcrf_arr[0, 1] / 1e6, 'go', markersize=8, label='Start (00:00 UTC)')\n", + "ax.plot(pos_gcrf_arr[-1, 0] / 1e6, pos_gcrf_arr[-1, 1] / 1e6, 'rs', markersize=8, label='End (24:00 UTC)')\n", + "ax.set_xlabel('GCRF X (thousands of km)')\n", + "ax.set_ylabel('GCRF Y (thousands of km)')\n", + "ax.set_title('Ground Station GCRF Position Over 24 Hours')\n", + "ax.set_aspect('equal')\n", + "ax.legend()\n", + "ax.grid(True, alpha=0.3)\n", + "plt.tight_layout()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The start and end points do not quite overlap because 24 solar hours is slightly longer than one sidereal day. The Earth completes just over one full rotation in 24 hours of UTC." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## SGP4 Output Frame: TEME to GCRF Pipeline\n", + "\n", + "The SGP4 propagator outputs position and velocity in the TEME frame, which is specific to the SGP4/TLE mathematical model. To use SGP4 results alongside data in standard frames, they must be rotated.\n", + "\n", + "The typical pipeline is:\n", + "- **TEME -> GCRF**: Use `qteme2gcrf` for inertial comparisons or further orbit analysis\n", + "- **TEME -> ITRF**: Use `qteme2itrf` to compute ground tracks, visibility, or geodetic sub-satellite points\n", + "\n", + "Below, we propagate the ISS using SGP4 and convert the results to both GCRF and ITRF." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# ISS TLE (example epoch)\n", + "line1 = '1 25544U 98067A 24167.50000000 .00016717 00000-0 10270-3 0 9002'\n", + "line2 = '2 25544 51.6400 200.0000 0001000 90.0000 270.0000 15.49000000400000'\n", + "tle = sk.TLE.from_lines([line1, line2])\n", + "print(f'TLE epoch: {tle.epoch}')\n", + "\n", + "# Propagate at a single time near epoch\n", + "t = tle.epoch\n", + "pos_teme, vel_teme = sk.sgp4(tle, t)\n", + "print(f'\\nTEME position (m): {pos_teme}')\n", + "print(f'TEME velocity (m/s): {vel_teme}')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Rotate TEME position and velocity to GCRF\n", + "q_teme2gcrf = sk.frametransform.qteme2gcrf(t)\n", + "pos_gcrf = q_teme2gcrf * pos_teme\n", + "vel_gcrf = q_teme2gcrf * vel_teme\n", + "print(f'GCRF position (m): {pos_gcrf}')\n", + "print(f'GCRF velocity (m/s): {vel_gcrf}')\n", + "\n", + "# Rotate TEME position to ITRF and get geodetic coordinates\n", + "q_teme2itrf = sk.frametransform.qteme2itrf(t)\n", + "pos_itrf = q_teme2itrf * pos_teme\n", + "subsatpoint = sk.itrfcoord(pos_itrf)\n", + "print(f'\\nITRF position (m): {pos_itrf}')\n", + "print(f'Sub-satellite point: {subsatpoint}')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Propagate over one full orbit and plot the ground track\n", + "period_minutes = 1440 / tle.mean_motion # period in minutes\n", + "prop_minutes = np.linspace(0, period_minutes, 300)\n", + "prop_times = [tle.epoch + sk.duration.from_minutes(float(m)) for m in prop_minutes]\n", + "\n", + "# Convert to geodetic coordinates via TEME -> ITRF\n", + "lats = []\n", + "lons = []\n", + "for t in prop_times:\n", + " pos_teme, _ = sk.sgp4(tle, t)\n", + " q = sk.frametransform.qteme2itrf(t)\n", + " pos_itrf = q * pos_teme\n", + " coord = sk.itrfcoord(pos_itrf)\n", + " lats.append(coord.geodetic.latitude_deg)\n", + " lons.append(coord.geodetic.longitude_deg)\n", + "\n", + "# Plot ground track\n", + "fig, ax = plt.subplots(figsize=(12, 6))\n", + "ax.scatter(lons, lats, c=prop_minutes, cmap='viridis', s=2)\n", + "ax.set_xlabel('Longitude (deg)')\n", + "ax.set_ylabel('Latitude (deg)')\n", + "ax.set_title('ISS Ground Track (One Orbit, TEME -> ITRF)')\n", + "ax.set_xlim(-180, 180)\n", + "ax.set_ylim(-90, 90)\n", + "ax.grid(True, alpha=0.3)\n", + "plt.tight_layout()\n", + "plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "The `satkit.frametransform` module provides all the rotations needed to convert between the standard coordinate frames used in satellite astrodynamics:\n", + "\n", + "- Use `qitrf2gcrf` / `qgcrf2itrf` for converting between Earth-fixed and inertial frames (full IAU-2006 accuracy)\n", + "- Use the `_approx` variants when sub-arcsecond accuracy is not required\n", + "- Use `qteme2gcrf` and `qteme2itrf` to convert SGP4/TLE outputs to standard frames\n", + "- All functions accept both scalar times and arrays, returning quaternion(s) accordingly\n", + "- Apply rotations to 3-vectors with the `*` operator: `v_out = q * v_in`" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/tutorials/Covariance Propagation.ipynb b/docs/tutorials/Covariance Propagation.ipynb new file mode 100644 index 0000000..5c9b6c2 --- /dev/null +++ b/docs/tutorials/Covariance Propagation.ipynb @@ -0,0 +1,370 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Covariance Propagation\n", + "\n", + "This tutorial demonstrates how to propagate state covariance alongside position and velocity using satkit's `satstate` class. Covariance propagation is essential for:\n", + "\n", + "- **Uncertainty quantification**: Understanding how errors in an initial state estimate grow over time\n", + "- **Conjunction assessment**: Determining the probability of collision between two objects requires knowing the uncertainty in both objects' positions\n", + "- **Sensor tasking**: Deciding when and where to point a sensor to re-acquire a tracked object\n", + "\n", + "Under the hood, satkit propagates the 6x6 state transition matrix $\\Phi(t, t_0)$ alongside the equations of motion. The covariance at time $t$ is then computed as:\n", + "\n", + "$$P(t) = \\Phi(t, t_0) \\, P(t_0) \\, \\Phi(t, t_0)^T$$\n", + "\n", + "where $P(t_0)$ is the initial covariance matrix." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import satkit as sk\n", + "import numpy as np\n", + "import math\n", + "import matplotlib.pyplot as plt\n", + "import scienceplots # noqa: F401\n", + "\n", + "plt.style.use([\"science\", \"no-latex\"])\n", + "plt.rcParams.update({\n", + " \"mathtext.fontset\": \"stix\",\n", + " \"font.family\": \"serif\",\n", + " \"font.serif\": [\"STIX Two Text\", \"STIXGeneral\", \"DejaVu Serif\"],\n", + " \"font.size\": 13,\n", + " \"axes.labelsize\": 14,\n", + " \"axes.titlesize\": 15,\n", + " \"xtick.labelsize\": 12,\n", + " \"ytick.labelsize\": 12,\n", + " \"legend.fontsize\": 12,\n", + " \"axes.formatter.use_mathtext\": True,\n", + " \"svg.fonttype\": \"none\",\n", + " \"axes.grid\": True,\n", + " \"grid.alpha\": 0.3,\n", + " \"grid.linestyle\": \"--\",\n", + " \"axes.prop_cycle\": plt.cycler(color=[\n", + " \"#0077BB\", \"#EE7733\", \"#009988\", \"#CC3311\",\n", + " \"#33BBEE\", \"#EE3377\", \"#BBBBBB\",\n", + " ]),\n", + "})\n", + "%config InlineBackend.figure_formats = ['svg']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setting Up a Satellite State\n", + "\n", + "We create a satellite in a near-circular LEO orbit at roughly 400 km altitude. The position and velocity are specified in the GCRF (Geocentric Celestial Reference Frame) inertial frame." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Epoch\n", + "t0 = sk.time(2024, 6, 15, 12, 0, 0)\n", + "\n", + "# LEO orbit: ~400 km altitude, near-circular\n", + "r = 6.778e6 # meters (Earth radius + 400 km)\n", + "v = math.sqrt(sk.consts.mu_earth / r) # circular orbit speed\n", + "\n", + "pos = np.array([r, 0.0, 0.0]) # meters, GCRF\n", + "vel = np.array([0.0, v, 0.0]) # m/s, GCRF\n", + "\n", + "state = sk.satstate(t0, pos, vel)\n", + "print(f\"Position: {state.pos} m\")\n", + "print(f\"Velocity: {state.vel} m/s\")\n", + "print(f\"Altitude: {np.linalg.norm(state.pos) - 6.378e6:.0f} m\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setting Initial Covariance\n", + "\n", + "Uncertainty is most naturally expressed in the LVLH (Local Vertical, Local Horizontal) frame, where the axes are aligned with the orbit geometry. The `set_lvlh_pos_uncertainty` method sets 1-sigma position uncertainties in this frame and internally rotates them into GCRF for propagation.\n", + "\n", + "For velocity uncertainty, we construct the full 6x6 covariance matrix directly. The position block comes from `set_lvlh_pos_uncertainty`, and we add a velocity uncertainty block on the diagonal." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Set 1-sigma position uncertainty in LVLH frame\n", + "# [along-track, cross-track, radial] in meters\n", + "sigma_pos_lvlh = np.array([50.0, 20.0, 10.0])\n", + "state.set_lvlh_pos_uncertainty(sigma_pos_lvlh)\n", + "\n", + "# The position covariance is now set (in GCRF internally).\n", + "# To also include velocity uncertainty, we build the full 6x6 matrix.\n", + "# First, grab the position covariance block that was just set:\n", + "cov = state.cov.copy()\n", + "print(\"Position covariance block (GCRF), meters^2:\")\n", + "print(cov[0:3, 0:3])\n", + "\n", + "# Add velocity uncertainty: 1-sigma = [0.05, 0.02, 0.01] m/s in LVLH\n", + "# Rotate to GCRF using the qgcrf2lvlh quaternion (inverse rotates LVLH -> GCRF)\n", + "sigma_vel_lvlh = np.array([0.05, 0.02, 0.01]) # m/s\n", + "vel_cov_lvlh = np.diag(sigma_vel_lvlh**2)\n", + "\n", + "# Rotate velocity covariance from LVLH to GCRF\n", + "# qgcrf2lvlh rotates GCRF -> LVLH, so its conjugate rotates LVLH -> GCRF\n", + "q = state.qgcrf2lvlh\n", + "# Build rotation matrix from the quaternion (GCRF -> LVLH)\n", + "# Apply: C_gcrf_lvlh * cov_lvlh * C_gcrf_lvlh^T\n", + "# We rotate each basis vector to construct the DCM\n", + "ex = q.conjugate * np.array([1.0, 0.0, 0.0])\n", + "ey = q.conjugate * np.array([0.0, 1.0, 0.0])\n", + "ez = q.conjugate * np.array([0.0, 0.0, 1.0])\n", + "R_lvlh2gcrf = np.column_stack([ex, ey, ez])\n", + "\n", + "vel_cov_gcrf = R_lvlh2gcrf @ vel_cov_lvlh @ R_lvlh2gcrf.T\n", + "cov[3:6, 3:6] = vel_cov_gcrf\n", + "\n", + "# Set the full covariance\n", + "state.cov = cov\n", + "\n", + "print(\"\\nFull 6x6 covariance matrix (GCRF):\")\n", + "print(state.cov)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Propagating with Covariance\n", + "\n", + "When a `satstate` has a covariance matrix set, calling `propagate` will propagate both the orbit and the covariance using the state transition matrix. Let's propagate forward by one orbital period and examine how the covariance changes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Orbital period\n", + "a = np.linalg.norm(state.pos) # semi-major axis (circular orbit)\n", + "period_sec = 2.0 * math.pi * math.sqrt(a**3 / sk.consts.mu_earth)\n", + "print(f\"Orbital period: {period_sec / 60:.1f} minutes\")\n", + "\n", + "# Propagate forward by one orbit\n", + "state_1orbit = state.propagate(sk.duration.from_seconds(period_sec))\n", + "\n", + "print(f\"\\nInitial 1-sigma position uncertainty (GCRF diagonal), meters:\")\n", + "print(f\" {np.sqrt(np.diag(state.cov[0:3, 0:3]))}\")\n", + "\n", + "print(f\"\\nAfter 1 orbit, 1-sigma position uncertainty (GCRF diagonal), meters:\")\n", + "print(f\" {np.sqrt(np.diag(state_1orbit.cov[0:3, 0:3]))}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Examining Uncertainty Growth Over Time\n", + "\n", + "To understand how uncertainty evolves, we propagate the state at regular intervals over 24 hours and extract the total position uncertainty (RSS of the 1-sigma values) at each step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Propagate at 5-minute intervals over 24 hours\n", + "dt_minutes = 5\n", + "num_steps = int(24 * 60 / dt_minutes)\n", + "\n", + "times_hr = np.zeros(num_steps)\n", + "sigma_pos_rss = np.zeros(num_steps) # RSS position uncertainty\n", + "sigma_x = np.zeros(num_steps)\n", + "sigma_y = np.zeros(num_steps)\n", + "sigma_z = np.zeros(num_steps)\n", + "\n", + "for i in range(num_steps):\n", + " dt = sk.duration.from_minutes(dt_minutes * (i + 1))\n", + " s = state.propagate(dt)\n", + " \n", + " times_hr[i] = dt_minutes * (i + 1) / 60.0\n", + " pos_cov = s.cov[0:3, 0:3]\n", + " sigmas = np.sqrt(np.diag(pos_cov))\n", + " sigma_x[i] = sigmas[0]\n", + " sigma_y[i] = sigmas[1]\n", + " sigma_z[i] = sigmas[2]\n", + " sigma_pos_rss[i] = np.sqrt(np.trace(pos_cov))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8), sharex=True)\n", + "\n", + "# Total RSS position uncertainty\n", + "ax1.plot(times_hr, sigma_pos_rss / 1e3, linewidth=1.5)\n", + "ax1.set_ylabel(\"RSS Position 1-sigma (km)\")\n", + "ax1.set_title(\"Position Uncertainty Growth Over 24 Hours\")\n", + "ax1.grid(True, alpha=0.3)\n", + "\n", + "# Per-component uncertainty (GCRF)\n", + "ax2.plot(times_hr, sigma_x / 1e3, label=\"X (GCRF)\", linewidth=1.0)\n", + "ax2.plot(times_hr, sigma_y / 1e3, label=\"Y (GCRF)\", linewidth=1.0)\n", + "ax2.plot(times_hr, sigma_z / 1e3, label=\"Z (GCRF)\", linewidth=1.0)\n", + "ax2.set_xlabel(\"Time (hours)\")\n", + "ax2.set_ylabel(\"Position 1-sigma (km)\")\n", + "ax2.set_title(\"Per-Component Position Uncertainty (GCRF)\")\n", + "ax2.legend()\n", + "ax2.grid(True, alpha=0.3)\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The LVLH Frame\n", + "\n", + "The LVLH (Local Vertical, Local Horizontal) frame is a coordinate system attached to the orbiting satellite, defined as:\n", + "\n", + "- **x-axis**: approximately along-track (in the direction of velocity)\n", + "- **y-axis**: cross-track (opposite to the orbital angular momentum vector $\\mathbf{h} = \\mathbf{r} \\times \\mathbf{v}$)\n", + "- **z-axis**: nadir (pointing toward Earth center, i.e., $-\\hat{r}$)\n", + "\n", + "This frame is useful because orbital uncertainties have a natural physical interpretation in LVLH: along-track errors tend to grow fastest (due to period uncertainty), while radial errors remain relatively bounded.\n", + "\n", + "The `qgcrf2lvlh` property on `satstate` provides the quaternion that rotates vectors from the GCRF inertial frame into the LVLH frame." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Demonstrate the LVLH quaternion\n", + "q_lvlh = state.qgcrf2lvlh\n", + "print(\"Quaternion GCRF -> LVLH:\", q_lvlh)\n", + "\n", + "# Verify the LVLH axes:\n", + "# z-axis should point toward nadir (-r_hat)\n", + "r_hat = state.pos / np.linalg.norm(state.pos)\n", + "z_lvlh = q_lvlh * r_hat # Should be [0, 0, -1]\n", + "print(f\"\\nr_hat in LVLH (expect ~[0, 0, -1]): {z_lvlh}\")\n", + "\n", + "# y-axis should be along -h_hat\n", + "h = np.cross(state.pos, state.vel)\n", + "h_hat = h / np.linalg.norm(h)\n", + "y_lvlh = q_lvlh * h_hat # Should be [0, -1, 0]\n", + "print(f\"h_hat in LVLH (expect ~[0, -1, 0]): {y_lvlh}\")\n", + "\n", + "# x-axis should be approximately along velocity\n", + "v_hat = state.vel / np.linalg.norm(state.vel)\n", + "x_lvlh = q_lvlh * v_hat # Should be ~[1, 0, 0]\n", + "print(f\"v_hat in LVLH (expect ~[1, 0, 0]): {x_lvlh}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Covariance in the LVLH Frame\n", + "\n", + "While satkit stores and propagates the covariance in GCRF, we can rotate it into the LVLH frame at any point to examine the uncertainty in physically meaningful orbit-relative directions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def cov_gcrf_to_lvlh(sat):\n", + " \"\"\"Rotate the 3x3 position covariance from GCRF to LVLH.\"\"\"\n", + " q = sat.qgcrf2lvlh\n", + " # Build rotation matrix by rotating basis vectors\n", + " R = np.column_stack([\n", + " q * np.array([1.0, 0.0, 0.0]),\n", + " q * np.array([0.0, 1.0, 0.0]),\n", + " q * np.array([0.0, 0.0, 1.0]),\n", + " ])\n", + " pos_cov_gcrf = sat.cov[0:3, 0:3]\n", + " return R @ pos_cov_gcrf @ R.T\n", + "\n", + "\n", + "# Propagate and extract LVLH uncertainties\n", + "sigma_along = np.zeros(num_steps)\n", + "sigma_cross = np.zeros(num_steps)\n", + "sigma_radial = np.zeros(num_steps)\n", + "\n", + "for i in range(num_steps):\n", + " dt = sk.duration.from_minutes(dt_minutes * (i + 1))\n", + " s = state.propagate(dt)\n", + " cov_lvlh = cov_gcrf_to_lvlh(s)\n", + " sigmas = np.sqrt(np.diag(cov_lvlh))\n", + " sigma_along[i] = sigmas[0] # x = along-track\n", + " sigma_cross[i] = sigmas[1] # y = cross-track\n", + " sigma_radial[i] = sigmas[2] # z = radial" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=(10, 5))\n", + "\n", + "ax.plot(times_hr, sigma_along / 1e3, label=\"Along-track\", linewidth=1.5)\n", + "ax.plot(times_hr, sigma_cross / 1e3, label=\"Cross-track\", linewidth=1.5)\n", + "ax.plot(times_hr, sigma_radial / 1e3, label=\"Radial\", linewidth=1.5)\n", + "\n", + "ax.set_xlabel(\"Time (hours)\")\n", + "ax.set_ylabel(\"Position 1-sigma (km)\")\n", + "ax.set_title(\"Position Uncertainty in LVLH Frame Over 24 Hours\")\n", + "ax.legend()\n", + "ax.grid(True, alpha=0.3)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The LVLH-frame plot reveals the characteristic pattern of orbital uncertainty growth: **along-track uncertainty dominates** and grows roughly linearly with time, because a small error in velocity (and hence orbital period) causes the satellite to drift ahead or behind its predicted position. Cross-track and radial uncertainties remain comparatively bounded." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/tutorials/Eclipse.ipynb b/docs/tutorials/Eclipse.ipynb index 241211b..9c410c1 100644 --- a/docs/tutorials/Eclipse.ipynb +++ b/docs/tutorials/Eclipse.ipynb @@ -81,34 +81,47 @@ "metadata": {}, "outputs": [], "source": [ - "import plotly.graph_objects as go\n", + "import ssl, certifi\n", + "ssl._create_default_https_context = lambda: ssl.create_default_context(cafile=certifi.where())\n", + "import matplotlib.pyplot as plt\n", + "import scienceplots # noqa: F401\n", + "\n", + "plt.style.use([\"science\", \"no-latex\"])\n", + "plt.rcParams.update({\n", + " \"mathtext.fontset\": \"stix\",\n", + " \"font.family\": \"serif\",\n", + " \"font.serif\": [\"STIX Two Text\", \"STIXGeneral\", \"DejaVu Serif\"],\n", + " \"font.size\": 13,\n", + " \"axes.labelsize\": 14,\n", + " \"axes.titlesize\": 15,\n", + " \"xtick.labelsize\": 12,\n", + " \"ytick.labelsize\": 12,\n", + " \"legend.fontsize\": 12,\n", + " \"axes.formatter.use_mathtext\": True,\n", + " \"svg.fonttype\": \"none\",\n", + " \"axes.grid\": True,\n", + " \"grid.alpha\": 0.3,\n", + " \"grid.linestyle\": \"--\",\n", + " \"axes.prop_cycle\": plt.cycler(color=[\n", + " \"#0077BB\", \"#EE7733\", \"#009988\", \"#CC3311\",\n", + " \"#33BBEE\", \"#EE3377\", \"#BBBBBB\",\n", + " ]),\n", + "})\n", + "%config InlineBackend.figure_formats = ['svg']\n", + "import cartopy.crs as ccrs\n", + "import cartopy.feature as cfeature\n", "\n", "lat, lon = zip(*[(c.latitude_deg, c.longitude_deg) for c in coords])\n", "\n", - "fig = go.Figure()\n", - "fig.add_trace(\n", - " go.Scattergeo(\n", - " locationmode = 'USA-states',\n", - " lon = lon,\n", - " lat = lat,\n", - " mode = 'lines',\n", - " line = dict(width = 2,color = 'red'),\n", - " )\n", - ")\n", - "fig.update_layout(\n", - " title_text = 'Centerline of 2024 Solar Eclipse',\n", - " showlegend = False,\n", - " geo = dict(\n", - " projection_type = 'mercator',\n", - " resolution=50,\n", - " lataxis_range=[20,55],\n", - " lonaxis_range=[-140,-40],\n", - " showcountries=True,\n", - " countrycolor='black',\n", - " ),\n", - " width=650,\n", - " height=500,\n", - ")\n" + "fig, ax = plt.subplots(figsize=(10, 6), subplot_kw={'projection': ccrs.Mercator()})\n", + "ax.set_extent([-140, -40, 20, 55], crs=ccrs.PlateCarree())\n", + "ax.add_feature(cfeature.LAND, facecolor='lightgray')\n", + "ax.add_feature(cfeature.BORDERS, linewidth=0.5)\n", + "ax.add_feature(cfeature.COASTLINE, linewidth=0.5)\n", + "ax.plot(lon, lat, color='red', linewidth=2, transform=ccrs.PlateCarree())\n", + "ax.set_title(\"Centerline of 2024 Solar Eclipse\")\n", + "plt.tight_layout()\n", + "plt.show()" ] }, { @@ -323,4 +336,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} \ No newline at end of file +} diff --git a/docs/tutorials/High Precision Propagation.ipynb b/docs/tutorials/High Precision Propagation.ipynb index 6b40556..f028443 100644 --- a/docs/tutorials/High Precision Propagation.ipynb +++ b/docs/tutorials/High Precision Propagation.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "markdown", - "id": "4f4bfde1", + "id": "0", "metadata": {}, "source": [ "# High-Precision Propagation\n", @@ -31,7 +31,7 @@ { "cell_type": "code", "execution_count": null, - "id": "e97dd803", + "id": "1", "metadata": {}, "outputs": [], "source": [ @@ -41,12 +41,36 @@ "import math as m\n", "import numpy.typing as npt\n", "from scipy.optimize import minimize_scalar\n", - "import plotly.graph_objects as go" + "import matplotlib.pyplot as plt\n", + "import scienceplots # noqa: F401\n", + "\n", + "plt.style.use([\"science\", \"no-latex\"])\n", + "plt.rcParams.update({\n", + " \"mathtext.fontset\": \"stix\",\n", + " \"font.family\": \"serif\",\n", + " \"font.serif\": [\"STIX Two Text\", \"STIXGeneral\", \"DejaVu Serif\"],\n", + " \"font.size\": 13,\n", + " \"axes.labelsize\": 14,\n", + " \"axes.titlesize\": 15,\n", + " \"xtick.labelsize\": 12,\n", + " \"ytick.labelsize\": 12,\n", + " \"legend.fontsize\": 12,\n", + " \"axes.formatter.use_mathtext\": True,\n", + " \"svg.fonttype\": \"none\",\n", + " \"axes.grid\": True,\n", + " \"grid.alpha\": 0.3,\n", + " \"grid.linestyle\": \"--\",\n", + " \"axes.prop_cycle\": plt.cycler(color=[\n", + " \"#0077BB\", \"#EE7733\", \"#009988\", \"#CC3311\",\n", + " \"#33BBEE\", \"#EE3377\", \"#BBBBBB\",\n", + " ]),\n", + "})\n", + "%config InlineBackend.figure_formats = ['svg']" ] }, { "cell_type": "markdown", - "id": "m45734c40h", + "id": "2", "metadata": {}, "source": [ "## Setup and SP3 File Reader\n", @@ -57,7 +81,7 @@ { "cell_type": "code", "execution_count": null, - "id": "f318566b", + "id": "3", "metadata": {}, "outputs": [], "source": [ @@ -102,7 +126,7 @@ { "cell_type": "code", "execution_count": null, - "id": "15735e98", + "id": "4", "metadata": {}, "outputs": [], "source": [ @@ -218,33 +242,24 @@ ")\n", "\n", "# Plot position error\n", - "fig = go.Figure()\n", - "# Add markers for each component of the position error\n", - "fig.add_trace(\n", - " go.Scatter(x=[t.datetime() for t in timearr], y=perr[:, 0], mode=\"lines\", name=\"X\")\n", - ")\n", - "fig.add_trace(\n", - " go.Scatter(x=[t.datetime() for t in timearr], y=perr[:, 1], mode=\"lines\", name=\"Y\")\n", - ")\n", - "fig.add_trace(\n", - " go.Scatter(x=[t.datetime() for t in timearr], y=perr[:, 2], mode=\"lines\", name=\"Z\")\n", - ")\n", - "fig.update_layout(\n", - " title=\"Propagation Error vs SP3 Truth for GPS Satellite\",\n", - " title_font_size=18,\n", - " xaxis_title=\"Time\",\n", - " yaxis_title=\"Position Error (m)\",\n", - " xaxis_title_font=dict(size=16),\n", - " yaxis_title_font=dict(size=16),\n", - " legend_font=dict(size=14),\n", - " font=dict(size=14),\n", - ")\n", - "fig.show()" + "# Plot position error\n", + "fig, ax = plt.subplots(figsize=(10, 5))\n", + "dates = [t.datetime() for t in timearr]\n", + "ax.plot(dates, perr[:, 0], label=\"X\")\n", + "ax.plot(dates, perr[:, 1], label=\"Y\")\n", + "ax.plot(dates, perr[:, 2], label=\"Z\")\n", + "ax.set_xlabel(\"Time\")\n", + "ax.set_ylabel(\"Position Error (m)\")\n", + "ax.set_title(\"Propagation Error vs SP3 Truth for GPS Satellite\")\n", + "ax.legend()\n", + "fig.autofmt_xdate()\n", + "plt.tight_layout()\n", + "plt.show()" ] }, { "cell_type": "markdown", - "id": "5bg9zfh65rm", + "id": "5", "metadata": {}, "source": [ "## Fit Initial State and Validate\n", @@ -256,7 +271,7 @@ }, { "cell_type": "markdown", - "id": "m3x0oo7thzg", + "id": "6", "metadata": {}, "source": [ "## Comparing Integrators\n", @@ -269,7 +284,7 @@ { "cell_type": "code", "execution_count": null, - "id": "i20wmiw3d69", + "id": "7", "metadata": {}, "outputs": [], "source": [ diff --git a/docs/tutorials/Keplerian Elements.ipynb b/docs/tutorials/Keplerian Elements.ipynb new file mode 100644 index 0000000..6cfb77a --- /dev/null +++ b/docs/tutorials/Keplerian Elements.ipynb @@ -0,0 +1,378 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Keplerian Orbital Elements\n", + "\n", + "This tutorial demonstrates how to work with Keplerian orbital elements in satkit. Keplerian elements are a set of six parameters that uniquely describe the shape, orientation, and current position of a satellite orbit.\n", + "\n", + "## The Six Keplerian Elements\n", + "\n", + "| Element | Symbol | Description |\n", + "|---|---|---|\n", + "| Semi-major axis | $a$ | Half the longest diameter of the orbital ellipse; sets the orbit size |\n", + "| Eccentricity | $e$ | Shape of the ellipse: 0 = circular, 0 < $e$ < 1 = elliptical |\n", + "| Inclination | $i$ | Tilt of the orbit plane relative to the equatorial plane |\n", + "| Right Ascension of Ascending Node | $\\Omega$ (RAAN) | Angle from the vernal equinox to where the orbit crosses the equator going north |\n", + "| Argument of Perigee | $\\omega$ | Angle from the ascending node to the closest point of the orbit (perigee) |\n", + "| True Anomaly | $\\nu$ | Current angular position of the satellite along the orbit, measured from perigee |\n", + "\n", + "The first two elements ($a$, $e$) define the orbit shape, the next two ($i$, $\\Omega$) define the orbit plane orientation, $\\omega$ orients the ellipse within the plane, and $\\nu$ locates the satellite on the orbit." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import satkit as sk\n", + "import numpy as np\n", + "import math\n", + "import matplotlib.pyplot as plt\n", + "import scienceplots # noqa: F401\n", + "\n", + "plt.style.use([\"science\", \"no-latex\"])\n", + "plt.rcParams.update({\n", + " \"mathtext.fontset\": \"stix\",\n", + " \"font.family\": \"serif\",\n", + " \"font.serif\": [\"STIX Two Text\", \"STIXGeneral\", \"DejaVu Serif\"],\n", + " \"font.size\": 13,\n", + " \"axes.labelsize\": 14,\n", + " \"axes.titlesize\": 15,\n", + " \"xtick.labelsize\": 12,\n", + " \"ytick.labelsize\": 12,\n", + " \"legend.fontsize\": 12,\n", + " \"axes.formatter.use_mathtext\": True,\n", + " \"svg.fonttype\": \"none\",\n", + " \"axes.grid\": True,\n", + " \"grid.alpha\": 0.3,\n", + " \"grid.linestyle\": \"--\",\n", + " \"axes.prop_cycle\": plt.cycler(color=[\n", + " \"#0077BB\", \"#EE7733\", \"#009988\", \"#CC3311\",\n", + " \"#33BBEE\", \"#EE3377\", \"#BBBBBB\",\n", + " ]),\n", + "})\n", + "%config InlineBackend.figure_formats = ['svg']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Creating Keplerian Elements\n", + "\n", + "The `sk.kepler` constructor takes six parameters: semi-major axis (meters), eccentricity, inclination (radians), RAAN (radians), argument of perigee (radians), and anomaly (radians).\n", + "\n", + "By default, the sixth argument is the true anomaly. You can alternatively specify the anomaly using keyword arguments `mean_anomaly` or `eccentric_anomaly`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Define an ISS-like orbit: ~400 km altitude, 51.6 deg inclination, nearly circular\n", + "altitude = 400e3 # meters\n", + "a = sk.consts.earth_radius + altitude\n", + "e = 0.001\n", + "i = math.radians(51.6)\n", + "raan = math.radians(45.0)\n", + "w = math.radians(30.0)\n", + "nu = math.radians(0.0)\n", + "\n", + "# Create with true anomaly (default)\n", + "kep = sk.kepler(a, e, i, raan, w, nu)\n", + "print(\"Created with true anomaly:\")\n", + "print(kep)\n", + "print(f\" Semi-major axis: {kep.a / 1e3:.1f} km\")\n", + "print(f\" Eccentricity: {kep.eccen:.4f}\")\n", + "print(f\" Inclination: {math.degrees(kep.inclination):.1f} deg\")\n", + "print(f\" RAAN: {math.degrees(kep.raan):.1f} deg\")\n", + "print(f\" Arg of Perigee: {math.degrees(kep.w):.1f} deg\")\n", + "print(f\" True Anomaly: {math.degrees(kep.nu):.1f} deg\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create with mean anomaly (keyword argument)\n", + "kep_mean = sk.kepler(a, e, i, raan, w, mean_anomaly=math.radians(10.0))\n", + "print(\"Created with mean anomaly = 10 deg:\")\n", + "print(f\" True anomaly: {math.degrees(kep_mean.true_anomaly):.4f} deg\")\n", + "print(f\" Mean anomaly: {math.degrees(kep_mean.mean_anomaly):.4f} deg\")\n", + "print(f\" Eccentric anomaly: {math.degrees(kep_mean.eccentric_anomaly):.4f} deg\")\n", + "\n", + "# Create with eccentric anomaly\n", + "kep_ecc = sk.kepler(a, e, i, raan, w, eccentric_anomaly=math.radians(10.0))\n", + "print(\"\\nCreated with eccentric anomaly = 10 deg:\")\n", + "print(f\" True anomaly: {math.degrees(kep_ecc.true_anomaly):.4f} deg\")\n", + "print(f\" Mean anomaly: {math.degrees(kep_ecc.mean_anomaly):.4f} deg\")\n", + "print(f\" Eccentric anomaly: {math.degrees(kep_ecc.eccentric_anomaly):.4f} deg\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Converting to Cartesian Coordinates\n", + "\n", + "Use `to_pv()` to convert Keplerian elements to position and velocity vectors in the perifocal/inertial frame. The position is in meters and velocity in meters per second." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pos, vel = kep.to_pv()\n", + "\n", + "print(f\"Position (km): [{pos[0]/1e3:.3f}, {pos[1]/1e3:.3f}, {pos[2]/1e3:.3f}]\")\n", + "print(f\"Velocity (km/s): [{vel[0]/1e3:.4f}, {vel[1]/1e3:.4f}, {vel[2]/1e3:.4f}]\")\n", + "print(f\"\\nPosition magnitude: {np.linalg.norm(pos)/1e3:.1f} km\")\n", + "print(f\"Velocity magnitude: {np.linalg.norm(vel)/1e3:.4f} km/s\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Converting from Cartesian Coordinates\n", + "\n", + "Use `sk.kepler.from_pv()` to recover Keplerian elements from position and velocity vectors. This is the inverse of `to_pv()`.\n", + "\n", + "The example below uses a test case from Vallado's *Fundamentals of Astrodynamics and Applications*, Example 2-6." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Vallado Example 2-6\n", + "r = np.array([6524.834, 6862.875, 6448.296]) * 1e3 # meters\n", + "v = np.array([4.901327, 5.533756, -1.976341]) * 1e3 # m/s\n", + "\n", + "kep_from_pv = sk.kepler.from_pv(r, v)\n", + "\n", + "print(f\"Semi-major axis: {kep_from_pv.a / 1e3:.1f} km\")\n", + "print(f\"Eccentricity: {kep_from_pv.eccen:.5f}\")\n", + "print(f\"Inclination: {math.degrees(kep_from_pv.inclination):.2f} deg\")\n", + "print(f\"RAAN: {math.degrees(kep_from_pv.raan):.2f} deg\")\n", + "print(f\"Arg of Perigee: {math.degrees(kep_from_pv.w):.2f} deg\")\n", + "print(f\"True Anomaly: {math.degrees(kep_from_pv.nu):.3f} deg\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Round-trip test: convert to PV and back\n", + "pos2, vel2 = kep_from_pv.to_pv()\n", + "kep_roundtrip = sk.kepler.from_pv(pos2, vel2)\n", + "\n", + "print(\"Round-trip consistency check:\")\n", + "print(f\" a difference: {abs(kep_from_pv.a - kep_roundtrip.a):.6e} m\")\n", + "print(f\" e difference: {abs(kep_from_pv.eccen - kep_roundtrip.eccen):.6e}\")\n", + "print(f\" i difference: {abs(kep_from_pv.inclination - kep_roundtrip.inclination):.6e} rad\")\n", + "print(f\" RAAN difference: {abs(kep_from_pv.raan - kep_roundtrip.raan):.6e} rad\")\n", + "print(f\" w difference: {abs(kep_from_pv.w - kep_roundtrip.w):.6e} rad\")\n", + "print(f\" nu difference: {abs(kep_from_pv.nu - kep_roundtrip.nu):.6e} rad\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Orbital Period and Mean Motion\n", + "\n", + "The `period` property returns the orbital period in seconds, and `mean_motion` returns the mean angular rate in radians per second." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"Orbital period: {kep.period:.1f} seconds = {kep.period / 60:.2f} minutes\")\n", + "print(f\"Mean motion: {kep.mean_motion:.6e} rad/s\")\n", + "print(f\"Mean motion: {kep.mean_motion * 86400 / (2 * math.pi):.4f} revs/day\")\n", + "\n", + "# Verify the relationship: period = 2*pi / mean_motion\n", + "print(f\"\\n2*pi / mean_motion = {2 * math.pi / kep.mean_motion:.1f} seconds (should match period)\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Keplerian Propagation\n", + "\n", + "The `propagate()` method advances the orbit forward in time using two-body (Keplerian) dynamics. Only the true anomaly changes; all other elements remain constant. This is an analytical solution, so it is exact (within the two-body assumption) and very fast.\n", + "\n", + "Below we propagate an orbit over one full period, sampling positions to visualize the orbit." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Propagate over one orbit, sampling 200 points\n", + "npts = 200\n", + "T = kep.period\n", + "times = np.linspace(0, T, npts)\n", + "\n", + "positions = np.zeros((npts, 3))\n", + "for idx, dt in enumerate(times):\n", + " kep_t = kep.propagate(dt)\n", + " pos_t, _ = kep_t.to_pv()\n", + " positions[idx, :] = pos_t / 1e3 # convert to km\n", + "\n", + "# 3D orbit plot\n", + "fig = plt.figure(figsize=(9, 9))\n", + "ax = fig.add_subplot(111, projection='3d')\n", + "ax.plot(positions[:, 0], positions[:, 1], positions[:, 2], 'b-', linewidth=1.5)\n", + "ax.scatter(*positions[0, :], color='green', s=80, label='Start (perigee)', zorder=5)\n", + "\n", + "# Draw Earth sphere\n", + "u = np.linspace(0, 2 * np.pi, 40)\n", + "v = np.linspace(0, np.pi, 20)\n", + "Re = sk.consts.earth_radius / 1e3\n", + "x_e = Re * np.outer(np.cos(u), np.sin(v))\n", + "y_e = Re * np.outer(np.sin(u), np.sin(v))\n", + "z_e = Re * np.outer(np.ones(np.size(u)), np.cos(v))\n", + "ax.plot_surface(x_e, y_e, z_e, alpha=0.2, color='skyblue')\n", + "\n", + "ax.set_xlabel('X (km)')\n", + "ax.set_ylabel('Y (km)')\n", + "ax.set_zlabel('Z (km)')\n", + "ax.set_title('ISS-like Orbit (One Period)')\n", + "ax.legend()\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Comparing Kepler vs. Numerical Propagation\n", + "\n", + "Keplerian propagation assumes a point-mass Earth with no perturbations. In reality, the Earth's oblateness ($J_2$ and higher-order gravity harmonics), atmospheric drag, third-body gravity from the Sun and Moon, and solar radiation pressure all perturb the orbit.\n", + "\n", + "Here we propagate the same initial orbit using both:\n", + "- **Kepler** (two-body analytical)\n", + "- **High-precision numerical** propagation with full force models\n", + "\n", + "and plot the position difference over time to visualize the perturbation effects." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Define orbit: 550 km circular, 45 deg inclination\n", + "alt = 550e3\n", + "a_circ = sk.consts.earth_radius + alt\n", + "kep0 = sk.kepler(a_circ, 0.001, math.radians(45.0), 0.0, 0.0, 0.0)\n", + "\n", + "# Get initial state vector for numerical propagation\n", + "state0 = np.concatenate(kep0.to_pv())\n", + "\n", + "# Propagation epoch and duration (6 hours)\n", + "epoch = sk.time(2025, 6, 15, 0, 0, 0)\n", + "prop_duration = sk.duration(hours=6)\n", + "\n", + "# Numerical propagation with high-precision force model\n", + "settings = sk.propsettings(\n", + " abs_error=1e-12,\n", + " rel_error=1e-12,\n", + " gravity_degree=10,\n", + ")\n", + "settings.precompute_terms(epoch, epoch + prop_duration)\n", + "\n", + "result = sk.propagate(\n", + " state0,\n", + " epoch,\n", + " epoch + prop_duration,\n", + " propsettings=settings,\n", + ")\n", + "\n", + "# Sample at 1-minute intervals\n", + "npts = 361\n", + "dt_seconds = np.linspace(0, prop_duration.seconds, npts)\n", + "time_array = [epoch + sk.duration(seconds=float(dt)) for dt in dt_seconds]\n", + "\n", + "# Get positions from both propagators\n", + "pos_kepler = np.zeros((npts, 3))\n", + "pos_numerical = np.zeros((npts, 3))\n", + "\n", + "for idx, (dt, t) in enumerate(zip(dt_seconds, time_array)):\n", + " # Kepler propagation\n", + " kep_t = kep0.propagate(dt)\n", + " pos_k, _ = kep_t.to_pv()\n", + " pos_kepler[idx, :] = pos_k\n", + "\n", + " # Numerical propagation (interpolated)\n", + " state_num = result.interp(t)\n", + " pos_numerical[idx, :] = state_num[0:3]\n", + "\n", + "# Compute position difference\n", + "pos_diff = pos_numerical - pos_kepler\n", + "pos_diff_mag = np.linalg.norm(pos_diff, axis=1)\n", + "\n", + "# Plot\n", + "hours = dt_seconds / 3600\n", + "\n", + "fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8), sharex=True)\n", + "\n", + "ax1.plot(hours, pos_diff[:, 0] / 1e3, label='X')\n", + "ax1.plot(hours, pos_diff[:, 1] / 1e3, label='Y')\n", + "ax1.plot(hours, pos_diff[:, 2] / 1e3, label='Z')\n", + "ax1.set_ylabel('Position Difference (km)')\n", + "ax1.set_title('Numerical - Keplerian Position Difference (per component)')\n", + "ax1.legend()\n", + "ax1.grid(True, alpha=0.3)\n", + "\n", + "ax2.plot(hours, pos_diff_mag / 1e3, 'k-', linewidth=1.5)\n", + "ax2.set_xlabel('Time (hours)')\n", + "ax2.set_ylabel('Position Difference (km)')\n", + "ax2.set_title('Total Position Difference Magnitude')\n", + "ax2.grid(True, alpha=0.3)\n", + "\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "print(f\"Maximum position difference over 6 hours: {pos_diff_mag[-1]/1e3:.2f} km\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/tutorials/Lambert Targeting.ipynb b/docs/tutorials/Lambert Targeting.ipynb new file mode 100644 index 0000000..fefeb99 --- /dev/null +++ b/docs/tutorials/Lambert Targeting.ipynb @@ -0,0 +1,394 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Lambert Targeting" + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "This tutorial demonstrates how to use satkit's Lambert solver to design orbit transfers.\n", + "We'll compute the delta-v required for a satellite to move between orbits and visualize the transfer geometry." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "import scienceplots # noqa: F401\n", + "\n", + "plt.style.use([\"science\", \"no-latex\"])\n", + "plt.rcParams.update({\n", + " \"mathtext.fontset\": \"stix\",\n", + " \"font.family\": \"serif\",\n", + " \"font.serif\": [\"STIX Two Text\", \"STIXGeneral\", \"DejaVu Serif\"],\n", + " \"font.size\": 13,\n", + " \"axes.labelsize\": 14,\n", + " \"axes.titlesize\": 15,\n", + " \"xtick.labelsize\": 12,\n", + " \"ytick.labelsize\": 12,\n", + " \"legend.fontsize\": 12,\n", + " \"axes.formatter.use_mathtext\": True,\n", + " \"svg.fonttype\": \"none\",\n", + " \"axes.grid\": True,\n", + " \"grid.alpha\": 0.3,\n", + " \"grid.linestyle\": \"--\",\n", + " \"axes.prop_cycle\": plt.cycler(color=[\n", + " \"#0077BB\", \"#EE7733\", \"#009988\", \"#CC3311\",\n", + " \"#33BBEE\", \"#EE3377\", \"#BBBBBB\",\n", + " ]),\n", + "})\n", + "%config InlineBackend.figure_formats = ['svg']\n", + "\n", + "import satkit as sk\n", + "import numpy as np" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "## Scenario: LEO to MEO Transfer\n", + "\n", + "A satellite is in a 400 km circular orbit (LEO) and needs to transfer to a 2000 km circular orbit (MEO).\n", + "We'll compute the optimal transfer for a range of transfer angles and find the one that minimizes total delta-v." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "# Orbit parameters\n", + "alt_1 = 400e3 # departure altitude (m)\n", + "alt_2 = 2000e3 # arrival altitude (m)\n", + "r_earth = sk.consts.earth_radius\n", + "mu = sk.consts.mu_earth\n", + "\n", + "r1_mag = r_earth + alt_1\n", + "r2_mag = r_earth + alt_2\n", + "\n", + "# Circular velocities at each orbit\n", + "v_circ_1 = np.sqrt(mu / r1_mag)\n", + "v_circ_2 = np.sqrt(mu / r2_mag)\n", + "\n", + "print(f\"Departure orbit: {alt_1/1e3:.0f} km altitude, v = {v_circ_1:.1f} m/s\")\n", + "print(f\"Arrival orbit: {alt_2/1e3:.0f} km altitude, v = {v_circ_2:.1f} m/s\")" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "## Delta-v vs Transfer Angle\n", + "\n", + "We place the satellite at a fixed departure point and sweep the arrival point around the higher orbit,\n", + "computing the Lambert solution and delta-v for each transfer angle." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "# Departure position (on x-axis)\n", + "r1 = np.array([r1_mag, 0.0, 0.0])\n", + "\n", + "# Sweep transfer angles from 30 to 170 degrees\n", + "angles_deg = np.linspace(30, 180, 200)\n", + "angles_rad = np.radians(angles_deg)\n", + "\n", + "# Hohmann transfer time (reference)\n", + "a_transfer = (r1_mag + r2_mag) / 2\n", + "t_hohmann = np.pi * np.sqrt(a_transfer**3 / mu)\n", + "\n", + "dv1_arr = []\n", + "dv2_arr = []\n", + "dv_total_arr = []\n", + "\n", + "for theta in angles_rad:\n", + " r2 = np.array([r2_mag * np.cos(theta), r2_mag * np.sin(theta), 0.0])\n", + "\n", + " # Scale TOF with transfer angle: fraction of Hohmann time\n", + " tof = t_hohmann * (theta / np.pi)\n", + "\n", + " solutions = sk.lambert(r1, r2, tof)\n", + " v1_transfer, v2_transfer = solutions[0]\n", + "\n", + " # Departure: satellite in circular orbit, moving in +y direction\n", + " v1_circular = np.array([0.0, v_circ_1, 0.0])\n", + "\n", + " # Arrival: circular velocity tangent to orbit at arrival point\n", + " v2_circular = np.array([\n", + " -v_circ_2 * np.sin(theta),\n", + " v_circ_2 * np.cos(theta),\n", + " 0.0,\n", + " ])\n", + "\n", + " dv1 = np.linalg.norm(v1_transfer - v1_circular)\n", + " dv2 = np.linalg.norm(v2_transfer - v2_circular)\n", + "\n", + " dv1_arr.append(dv1)\n", + " dv2_arr.append(dv2)\n", + " dv_total_arr.append(dv1 + dv2)\n", + "\n", + "dv1_arr = np.array(dv1_arr)\n", + "dv2_arr = np.array(dv2_arr)\n", + "dv_total_arr = np.array(dv_total_arr)\n", + "\n", + "# Find minimum total delta-v\n", + "idx_min = np.argmin(dv_total_arr)\n", + "print(f\"Minimum total delta-v: {dv_total_arr[idx_min]:.1f} m/s at {angles_deg[idx_min]:.1f} degrees\")\n", + "print(f\" Departure burn: {dv1_arr[idx_min]:.1f} m/s\")\n", + "print(f\" Arrival burn: {dv2_arr[idx_min]:.1f} m/s\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=(8, 5))\n", + "ax.plot(angles_deg, dv1_arr, label=r\"$\\Delta v_1$ (departure)\")\n", + "ax.plot(angles_deg, dv2_arr, label=r\"$\\Delta v_2$ (arrival)\")\n", + "ax.plot(angles_deg, dv_total_arr, label=r\"$\\Delta v_{total}$\", linewidth=2)\n", + "ax.axvline(angles_deg[idx_min], color=\"#BBBBBB\", linestyle=\":\", linewidth=1)\n", + "ax.set_xlabel(\"Transfer Angle (degrees)\")\n", + "ax.set_ylabel(r\"$\\Delta v$ (m/s)\")\n", + "ax.set_title(\"LEO to MEO Transfer\")\n", + "ax.legend()\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "## Pork-Chop Plot: Delta-v vs Transfer Angle and Time\n", + "\n", + "A **pork-chop plot** shows total delta-v as a function of both transfer angle and time of flight,\n", + "revealing the trade-off between fuel cost and transfer duration." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "# Sweep both transfer angle and time of flight\n", + "angles = np.linspace(60, 300, 120)\n", + "tofs = np.linspace(0.4 * t_hohmann, 3.0 * t_hohmann, 100)\n", + "\n", + "DV = np.full((len(tofs), len(angles)), np.nan)\n", + "\n", + "for i, t in enumerate(tofs):\n", + " for j, theta_deg in enumerate(angles):\n", + " theta = np.radians(theta_deg)\n", + " r2 = np.array([r2_mag * np.cos(theta), r2_mag * np.sin(theta), 0.0])\n", + "\n", + " try:\n", + " solutions = sk.lambert(r1, r2, t)\n", + " v1_t, v2_t = solutions[0]\n", + "\n", + " v1_c = np.array([0.0, v_circ_1, 0.0])\n", + " v2_c = np.array([\n", + " -v_circ_2 * np.sin(theta),\n", + " v_circ_2 * np.cos(theta),\n", + " 0.0,\n", + " ])\n", + "\n", + " dv = np.linalg.norm(v1_t - v1_c) + np.linalg.norm(v2_t - v2_c)\n", + " if dv < 15000:\n", + " DV[i, j] = dv\n", + " except Exception:\n", + " pass\n", + "\n", + "fig, ax = plt.subplots(figsize=(9, 6))\n", + "levels = np.linspace(500, 8000, 20)\n", + "cs = ax.contourf(angles, tofs / 60, DV, levels=levels, cmap=\"viridis\", extend=\"max\")\n", + "ax.contour(angles, tofs / 60, DV, levels=levels, colors=\"white\", linewidths=0.3)\n", + "cbar = fig.colorbar(cs, ax=ax, label=r\"Total $\\Delta v$ (m/s)\")\n", + "ax.set_xlabel(\"Transfer Angle (degrees)\")\n", + "ax.set_ylabel(\"Time of Flight (minutes)\")\n", + "ax.set_title(\"LEO to MEO Transfer: Pork-Chop Plot\")\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "## Transfer Orbit Visualization\n", + "\n", + "Let's visualize the optimal transfer orbit alongside the departure and arrival orbits." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "# Optimal transfer\n", + "theta_opt = np.radians(angles_deg[idx_min])\n", + "r2_opt = np.array([r2_mag * np.cos(theta_opt), r2_mag * np.sin(theta_opt), 0.0])\n", + "\n", + "tof_opt = t_hohmann * (theta_opt / np.pi)\n", + "solutions = sk.lambert(r1, r2_opt, tof_opt)\n", + "v1_transfer, v2_transfer = solutions[0]\n", + "\n", + "# Propagate transfer orbit via two-body integration\n", + "from scipy.integrate import solve_ivp\n", + "\n", + "def twobody(t, state):\n", + " r = state[:3]\n", + " v = state[3:]\n", + " r_norm = np.linalg.norm(r)\n", + " a = -mu / r_norm**3 * r\n", + " return np.concatenate([v, a])\n", + "\n", + "state0 = np.concatenate([r1, v1_transfer])\n", + "sol = solve_ivp(twobody, [0, tof_opt], state0, rtol=1e-10, atol=1e-12,\n", + " dense_output=True)\n", + "t_plot = np.linspace(0, tof_opt, 500)\n", + "positions = sol.sol(t_plot)[:3].T\n", + "\n", + "# Draw orbits\n", + "fig, ax = plt.subplots(figsize=(8, 8))\n", + "theta_circ = np.linspace(0, 2 * np.pi, 500)\n", + "\n", + "# Earth\n", + "earth = plt.Circle((0, 0), r_earth / 1e3, color=\"lightblue\", ec=\"steelblue\", linewidth=1)\n", + "ax.add_patch(earth)\n", + "\n", + "# Departure orbit\n", + "ax.plot(r1_mag * np.cos(theta_circ) / 1e3, r1_mag * np.sin(theta_circ) / 1e3,\n", + " \"--\", color=\"#BBBBBB\", linewidth=1, label=f\"LEO ({alt_1/1e3:.0f} km)\")\n", + "\n", + "# Arrival orbit\n", + "ax.plot(r2_mag * np.cos(theta_circ) / 1e3, r2_mag * np.sin(theta_circ) / 1e3,\n", + " \"--\", color=\"#BBBBBB\", linewidth=1, label=f\"MEO ({alt_2/1e3:.0f} km)\")\n", + "\n", + "# Transfer arc\n", + "ax.plot(positions[:, 0] / 1e3, positions[:, 1] / 1e3,\n", + " linewidth=2, color=\"#CC3311\", label=\"Transfer orbit\")\n", + "\n", + "# Endpoints\n", + "ax.plot(r1[0] / 1e3, r1[1] / 1e3, \"o\", color=\"#0077BB\", markersize=8, zorder=5)\n", + "ax.annotate(\"Departure\", xy=(r1[0] / 1e3, r1[1] / 1e3),\n", + " xytext=(15, 15), textcoords=\"offset points\", fontsize=11)\n", + "ax.plot(r2_opt[0] / 1e3, r2_opt[1] / 1e3, \"s\", color=\"#009988\", markersize=8, zorder=5)\n", + "ax.annotate(\"Arrival\", xy=(r2_opt[0] / 1e3, r2_opt[1] / 1e3),\n", + " xytext=(15, -20), textcoords=\"offset points\", fontsize=11)\n", + "\n", + "ax.set_xlabel(\"x (km)\")\n", + "ax.set_ylabel(\"y (km)\")\n", + "ax.set_title(f\"Transfer Orbit ({angles_deg[idx_min]:.0f}°, \" + r\"$\\Delta v$ = \" + f\"{dv_total_arr[idx_min]:.0f} m/s)\")\n", + "ax.set_aspect(\"equal\")\n", + "ax.legend(loc=\"lower left\")\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "## Interplanetary Example: Earth to Mars\n", + "\n", + "Lambert targeting also works for interplanetary transfers by using the Sun's gravitational parameter and heliocentric positions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "mu_sun = sk.consts.mu_sun\n", + "au = sk.consts.au\n", + "\n", + "# Simplified circular coplanar orbits\n", + "r_earth_orbit = 1.0 * au\n", + "r_mars_orbit = 1.524 * au\n", + "\n", + "# Earth at departure\n", + "r1_helio = np.array([r_earth_orbit, 0, 0])\n", + "\n", + "# Mars at arrival (~135 degree transfer)\n", + "transfer_angle = np.radians(135)\n", + "r2_helio = np.array([\n", + " r_mars_orbit * np.cos(transfer_angle),\n", + " r_mars_orbit * np.sin(transfer_angle),\n", + " 0,\n", + "])\n", + "\n", + "# Transfer time: ~8 months\n", + "tof_days = 245\n", + "tof_interp = tof_days * 86400\n", + "\n", + "solutions = sk.lambert(r1_helio, r2_helio, tof_interp, mu=mu_sun)\n", + "v1_t, v2_t = solutions[0]\n", + "\n", + "# Circular velocities\n", + "v_earth = np.sqrt(mu_sun / r_earth_orbit)\n", + "v_mars = np.sqrt(mu_sun / r_mars_orbit)\n", + "\n", + "v1_earth = np.array([0, v_earth, 0])\n", + "v2_mars = np.array([\n", + " -v_mars * np.sin(transfer_angle),\n", + " v_mars * np.cos(transfer_angle),\n", + " 0,\n", + "])\n", + "\n", + "dv_depart = np.linalg.norm(v1_t - v1_earth)\n", + "dv_arrive = np.linalg.norm(v2_t - v2_mars)\n", + "\n", + "print(f\"Earth-Mars transfer ({tof_days} days):\")\n", + "print(f\" Departure delta-v: {dv_depart/1e3:.2f} km/s\")\n", + "print(f\" Arrival delta-v: {dv_arrive/1e3:.2f} km/s\")\n", + "print(f\" Total delta-v: {(dv_depart + dv_arrive)/1e3:.2f} km/s\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/tutorials/Optical Observations of Satellites.ipynb b/docs/tutorials/Optical Observations of Satellites.ipynb index 10f8b08..5eae459 100644 --- a/docs/tutorials/Optical Observations of Satellites.ipynb +++ b/docs/tutorials/Optical Observations of Satellites.ipynb @@ -4,5942 +4,217 @@ "cell_type": "markdown", "id": "d8bb6046", "metadata": {}, - "source": "# Optical Observations of Satellites\n\nGround-based telescopes can observe sunlit satellites against the night sky, measuring their angular position (right ascension and declination). These line-of-sight measurements can then refine the satellite's orbit. This technique is particularly valuable for satellites in MEO and GEO orbits, which are visible for long periods at night and lack easy access to GPS signals. Organizations like the US Space Force and commercial companies such as [ExoAnalytic](https://exoanalytic.com/space-intelligence/) routinely use optical tracking to maintain satellite catalogs.\n\n## Approach\n\nThis example uses a **batch least-squares** (Gauss-Newton) technique to update an initial state estimate by minimizing the difference between observed and predicted line-of-sight unit vectors:\n\n$$\n\\hat{\\mathbf x}_0\n=\n\\arg\\min_{\\mathbf x_0\\in\\mathbb R^6}\n\\sum_{k=1}^{N}\n\\left\\|\nE_k\\Big(\\hat{\\mathbf u}_m(\\alpha_k,\\delta_k)-\\hat{\\mathbf u}(\\phi(t_k,t_0;\\mathbf x_0),\\mathbf r_{0}(t_k))\\Big)\n\\right\\|_{R^{-1}}^{2}\n$$\n\nwhere:\n\n- $\\hat{\\mathbf u}_m(\\alpha,\\delta) = [\\cos\\delta\\cos\\alpha,\\; \\cos\\delta\\sin\\alpha,\\; \\sin\\delta]^T$ is the measured unit vector from right ascension $\\alpha$ and declination $\\delta$\n- $\\hat{\\mathbf u}(\\mathbf x_k, \\mathbf r_0) = \\frac{\\mathbf r_k - \\mathbf r_0}{\\|\\mathbf r_k - \\mathbf r_0\\|}$ is the predicted unit vector from observer at $\\mathbf r_0$ to satellite at $\\mathbf r_k$\n- $\\mathbf x_k = \\phi(t_k, t_0; \\mathbf x_0)$ is the propagated state at time $t_k$ given initial state $\\mathbf x_0$\n- $E_k$ projects the 3D residual into the local tangent plane (RA/Dec directions) to keep the measurement space 2D and avoid singularities\n\nThe state transition matrix $\\Phi$ from the high-precision propagator maps initial state perturbations to perturbations at each measurement time, forming the Jacobian of the linearized system." - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a53d5d64", - "metadata": {}, - "outputs": [], "source": [ - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "s", - "a", - "t", - "k", - "i", - "t", - " ", - "a", - "s", - " ", - "s", - "k", - "\n", - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "n", - "u", - "m", - "p", - "y", - " ", - "a", - "s", - " ", - "n", - "p", - "\n", - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "m", - "a", - "t", - "h", - " ", - "a", - "s", - " ", - "m", - "\n", - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "d", - "a", - "t", - "e", - "t", - "i", - "m", - "e", - "\n", - "\n", - "#", - " ", - "T", - "a", - "k", - "e", - " ", - "o", - "u", - "r", - " ", - "o", - "b", - "s", - "e", - "r", - "v", - "e", - "r", - " ", - "t", - "o", - " ", - "b", - "e", - " ", - "t", - "h", - "e", - " ", - "S", - "p", - "a", - "c", - "e", - " ", - "S", - "u", - "r", - "v", - "e", - "i", - "l", - "l", - "a", - "n", - "c", - "e", - " ", - "T", - "e", - "l", - "e", - "s", - "c", - "o", - "p", - "e", - " ", - "(", - "S", - "S", - "T", - ")", - "\n", - "#", - " ", - "i", - "n", - " ", - "E", - "x", - "m", - "o", - "u", - "t", - "h", - ",", - " ", - "W", - "e", - "s", - "t", - "e", - "r", - "n", - " ", - "A", - "u", - "s", - "t", - "r", - "a", - "l", - "i", - "a", - "\n", - "s", - "s", - "t", - "_", - "l", - "a", - "t", - " ", - "=", - " ", - "-", - "2", - "2", - ".", - "6", - "7", - "5", - "\n", - "s", - "s", - "t", - "_", - "l", - "o", - "n", - " ", - "=", - " ", - "1", - "1", - "4", - ".", - "2", - "3", - "5", - "\n", - "s", - "s", - "t", - "_", - "a", - "l", - "t", - " ", - "=", - " ", - "0", - ".", - "5", - " ", - "#", - " ", - "k", - "m", - "\n", - "s", - "s", - "t", - " ", - "=", - " ", - "s", - "k", - ".", - "i", - "t", - "r", - "f", - "c", - "o", - "o", - "r", - "d", - "(", - "l", - "a", - "t", - "i", - "t", - "u", - "d", - "e", - "_", - "d", - "e", - "g", - "=", - "s", - "s", - "t", - "_", - "l", - "a", - "t", - ",", - " ", - "l", - "o", - "n", - "g", - "i", - "t", - "u", - "d", - "e", - "_", - "d", - "e", - "g", - "=", - "s", - "s", - "t", - "_", - "l", - "o", - "n", - ",", - " ", - "a", - "l", - "t", - "_", - "m", - "=", - "s", - "s", - "t", - "_", - "a", - "l", - "t", - "*", - "1", - "e", - "3", - ")", - "\n", - "\n", - "#", - " ", - "L", - "e", - "t", - "s", - " ", - "l", - "o", - "o", - "k", - " ", - "a", - "t", - " ", - "I", - "N", - "T", - "E", - "L", - "S", - "A", - "T", - " ", - "1", - "9", - " ", - "(", - "I", - "S", - "-", - "1", - "9", - ")", - ",", - " ", - "a", - " ", - "g", - "e", - "o", - "s", - "t", - "a", - "t", - "i", - "o", - "n", - "a", - "r", - "y", - " ", - "c", - "o", - "m", - "m", - "u", - "n", - "i", - "c", - "a", - "t", - "i", - "o", - "n", - "s", - " ", - "s", - "a", - "t", - "e", - "l", - "l", - "i", - "t", - "e", - ".", - "\n", - "#", - " ", - "I", - "t", - " ", - "i", - "s", - " ", - "c", - "u", - "r", - "r", - "e", - "n", - "t", - "l", - "y", - " ", - "l", - "o", - "c", - "a", - "t", - "e", - "d", - " ", - "a", - "t", - " ", - "1", - "6", - "6", - "E", - " ", - "l", - "o", - "n", - "g", - "i", - "t", - "u", - "d", - "e", - ",", - " ", - "s", - "o", - " ", - "i", - "t", - " ", - "s", - "h", - "o", - "u", - "l", - "d", - " ", - "b", - "e", - " ", - "v", - "i", - "s", - "i", - "b", - "l", - "e", - " ", - "f", - "r", - "o", - "m", - " ", - "t", - "h", - "e", - " ", - "S", - "S", - "T", - ".", - "\n", - "#", - " ", - "W", - "e", - " ", - "c", - "a", - "n", - " ", - "g", - "e", - "t", - " ", - "t", - "h", - "e", - " ", - "T", - "L", - "E", - " ", - "f", - "o", - "r", - " ", - "I", - "N", - "T", - "E", - "L", - "S", - "A", - "T", - " ", - "1", - "9", - " ", - "f", - "r", - "o", - "m", - " ", - "C", - "e", - "l", - "e", - "s", - "t", - "r", - "a", - "k", - ":", - "\n", - "#", - " ", - "h", - "t", - "t", - "p", - "s", - ":", - "/", - "/", - "c", - "e", - "l", - "e", - "s", - "t", - "r", - "a", - "k", - ".", - "c", - "o", - "m", - "/", - "N", - "O", - "R", - "A", - "D", - "/", - "e", - "l", - "e", - "m", - "e", - "n", - "t", - "s", - "/", - "g", - "e", - "o", - ".", - "t", - "x", - "t", - "\n", - "t", - "l", - "e", - " ", - "=", - " ", - "s", - "k", - ".", - "T", - "L", - "E", - ".", - "f", - "r", - "o", - "m", - "_", - "l", - "i", - "n", - "e", - "s", - "(", - "[", - "\n", - " ", - " ", - " ", - " ", - "\"", - "I", - "N", - "T", - "E", - "L", - "S", - "A", - "T", - " ", - "1", - "9", - " ", - "(", - "I", - "S", - "-", - "1", - "9", - ")", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - "\"", - "1", - " ", - "3", - "8", - "3", - "5", - "6", - "U", - " ", - "1", - "2", - "0", - "3", - "0", - "A", - " ", - " ", - " ", - "2", - "6", - "0", - "4", - "5", - ".", - "5", - "1", - "8", - "5", - "0", - "4", - "1", - "1", - " ", - "-", - ".", - "0", - "0", - "0", - "0", - "0", - "0", - "5", - "1", - " ", - " ", - "0", - "0", - "0", - "0", - "0", - "+", - "0", - " ", - " ", - "0", - "0", - "0", - "0", - "0", - "+", - "0", - " ", - "0", - " ", - " ", - "9", - "9", - "9", - "8", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - "\"", - "2", - " ", - "3", - "8", - "3", - "5", - "6", - " ", - " ", - " ", - "0", - ".", - "0", - "1", - "5", - "0", - " ", - " ", - "9", - "7", - ".", - "8", - "4", - "4", - "1", - " ", - "0", - "0", - "0", - "2", - "8", - "7", - "7", - " ", - "2", - "3", - "3", - ".", - "5", - "0", - "9", - "6", - " ", - "1", - "6", - "5", - ".", - "8", - "2", - "5", - "7", - " ", - " ", - "1", - ".", - "0", - "0", - "2", - "7", - "2", - "3", - "2", - "5", - " ", - "4", - "9", - "2", - "2", - "1", - "\"", - "\n", - "]", - ")", - "\n", - "e", - "p", - "o", - "c", - "h", - " ", - "=", - " ", - "t", - "l", - "e", - ".", - "e", - "p", - "o", - "c", - "h", - "\n", - "\n", - "#", - " ", - "L", - "e", - "t", - "s", - " ", - "m", - "a", - "k", - "e", - " ", - "t", - "h", - "e", - " ", - "i", - "n", - "i", - "t", - "i", - "a", - "l", - " ", - "\"", - "t", - "r", - "u", - "e", - "\"", - " ", - "s", - "t", - "a", - "t", - "e", - " ", - "t", - "h", - "e", - " ", - "t", - "l", - "e", - "-", - "p", - "r", - "e", - "d", - "i", - "c", - "t", - "e", - "d", - " ", - "s", - "t", - "a", - "t", - "e", - " ", - "a", - "t", - " ", - "e", - "p", - "o", - "c", - "h", - "\n", - "p", - "t", - "e", - "m", - "e", - ",", - " ", - "v", - "t", - "e", - "m", - "e", - " ", - "=", - " ", - "s", - "k", - ".", - "s", - "g", - "p", - "4", - "(", - "t", - "l", - "e", - ",", - " ", - "e", - "p", - "o", - "c", - "h", - ")", - "\n", - "q", - " ", - "=", - " ", - "s", - "k", - ".", - "f", - "r", - "a", - "m", - "e", - "t", - "r", - "a", - "n", - "s", - "f", - "o", - "r", - "m", - ".", - "q", - "t", - "e", - "m", - "e", - "2", - "g", - "c", - "r", - "f", - "(", - "e", - "p", - "o", - "c", - "h", - ")", - "\n", - "p", - "g", - "c", - "r", - "f", - " ", - "=", - " ", - "q", - " ", - "*", - " ", - "p", - "t", - "e", - "m", - "e", - "\n", - "v", - "g", - "c", - "r", - "f", - " ", - "=", - " ", - "q", - " ", - "*", - " ", - "v", - "t", - "e", - "m", - "e", - "\n", - "s", - "t", - "a", - "t", - "e", - "0", - " ", - "=", - " ", - "n", - "p", - ".", - "h", - "s", - "t", - "a", - "c", - "k", - "(", - "(", - "p", - "g", - "c", - "r", - "f", - ",", - " ", - "v", - "g", - "c", - "r", - "f", - ")", - ")", - "\n", - "p", - "r", - "i", - "n", - "t", - "(", - "e", - "p", - "o", - "c", - "h", - ")", - "\n", - "b", - "e", - "g", - "i", - "n", - "_", - "t", - "i", - "m", - "e", - " ", - "=", - " ", - "e", - "p", - "o", - "c", - "h", - "\n", - "e", - "n", - "d", - "_", - "t", - "i", - "m", - "e", - " ", - "=", - " ", - "e", - "p", - "o", - "c", - "h", - " ", - "+", - " ", - "s", - "k", - ".", - "d", - "u", - "r", - "a", - "t", - "i", - "o", - "n", - "(", - "d", - "a", - "y", - "s", - "=", - "2", - ")", - "\n", - "s", - "e", - "t", - "t", - "i", - "n", - "g", - "s", - " ", - "=", - " ", - "s", - "k", - ".", - "p", - "r", - "o", - "p", - "s", - "e", - "t", - "t", - "i", - "n", - "g", - "s", - "(", - ")", - "\n", - "s", - "e", - "t", - "t", - "i", - "n", - "g", - "s", - ".", - "p", - "r", - "e", - "c", - "o", - "m", - "p", - "u", - "t", - "e", - "_", - "t", - "e", - "r", - "m", - "s", - "(", - "b", - "e", - "g", - "i", - "n", - "_", - "t", - "i", - "m", - "e", - ",", - " ", - "e", - "n", - "d", - "_", - "t", - "i", - "m", - "e", - ")", - "\n", - "#", - " ", - "T", - "r", - "u", - "e", - " ", - "s", - "t", - "a", - "t", - "e", - " ", - "o", - "v", - "e", - "r", - " ", - "1", - " ", - "d", - "a", - "y", - "\n", - "t", - "r", - "u", - "t", - "h", - " ", - "=", - " ", - "s", - "k", - ".", - "p", - "r", - "o", - "p", - "a", - "g", - "a", - "t", - "e", - "(", - "s", - "t", - "a", - "t", - "e", - "0", - ",", - " ", - "b", - "e", - "g", - "i", - "n", - "_", - "t", - "i", - "m", - "e", - ",", - " ", - "e", - "n", - "d", - "_", - "t", - "i", - "m", - "e", - ",", - " ", - "p", - "r", - "o", - "p", - "s", - "e", - "t", - "t", - "i", - "n", - "g", - "s", - "=", - "s", - "e", - "t", - "t", - "i", - "n", - "g", - "s", - ")", - "\n", - "\n", - "#", - " ", - "A", - "s", - "s", - "u", - "m", - "e", - " ", - "p", - "o", - "s", - "i", - "t", - "i", - "o", - "n", - " ", - "k", - "n", - "o", - "w", - "l", - "e", - "d", - "g", - "e", - " ", - "t", - "o", - " ", - "1", - "0", - " ", - "k", - "m", - ",", - " ", - "v", - "e", - "l", - "o", - "c", - "i", - "t", - "y", - " ", - "t", - "o", - " ", - "5", - " ", - "c", - "m", - "/", - "s", - "\n", - "p", - "o", - "s", - "_", - "n", - "o", - "i", - "s", - "e", - " ", - "=", - " ", - "1", - "0", - "e", - "3", - "\n", - "v", - "e", - "l", - "_", - "n", - "o", - "i", - "s", - "e", - " ", - "=", - " ", - "0", - ".", - "0", - "5", - "\n", - "\n", - "#", - " ", - "i", - "n", - "i", - "t", - "i", - "a", - "l", - " ", - "s", - "t", - "a", - "t", - "e", - " ", - "e", - "s", - "t", - "i", - "m", - "a", - "t", - "e", - "\n", - "s", - "t", - "a", - "t", - "e", - "0", - "_", - "e", - "s", - "t", - " ", - "=", - " ", - "s", - "t", - "a", - "t", - "e", - "0", - " ", - "+", - " ", - "n", - "p", - ".", - "r", - "a", - "n", - "d", - "o", - "m", - ".", - "n", - "o", - "r", - "m", - "a", - "l", - "(", - "s", - "c", - "a", - "l", - "e", - "=", - "[", - "p", - "o", - "s", - "_", - "n", - "o", - "i", - "s", - "e", - "]", - "*", - "3", - " ", - "+", - " ", - "[", - "v", - "e", - "l", - "_", - "n", - "o", - "i", - "s", - "e", - "]", - "*", - "3", - ")", - "\n", - "s", - "t", - "a", - "t", - "e", - "0", - "_", - "e", - "s", - "t", - "_", - "p", - "r", - "i", - "o", - "r", - " ", - "=", - " ", - "s", - "t", - "a", - "t", - "e", - "0", - "_", - "e", - "s", - "t", - ".", - "c", - "o", - "p", - "y", - "(", - ")", - "\n", - "\n", - "#", - " ", - "s", - "a", - "m", - "p", - "l", - "e", - " ", - "t", - "i", - "m", - "e", - "s", - " ", - "f", - "o", - "r", - " ", - "m", - "e", - "a", - "s", - "u", - "r", - "e", - "m", - "e", - "n", - "t", - "s", - ".", - " ", - " ", - "E", - "v", - "e", - "r", - "y", - " ", - "5", - " ", - "m", - "i", - "n", - "u", - "e", - "s", - " ", - "f", - "o", - "r", - " ", - "6", - " ", - "h", - "o", - "u", - "r", - "s", - " ", - "b", - "e", - "g", - "i", - "n", - "n", - "i", - "n", - "g", - " ", - "a", - "t", - " ", - "l", - "o", - "c", - "a", - "l", - " ", - "9", - "p", - "m", - " ", - "a", - "n", - "d", - " ", - "g", - "o", - "i", - "n", - "g", - " ", - "t", - "o", - " ", - "l", - "o", - "c", - "a", - "l", - " ", - "4", - "a", - "m", - "\n", - "s", - "a", - "m", - "p", - "l", - "e", - "_", - "s", - "t", - "a", - "r", - "t", - " ", - "=", - " ", - "s", - "k", - ".", - "t", - "i", - "m", - "e", - "(", - "2", - "0", - "2", - "6", - ",", - " ", - "2", - ",", - " ", - "1", - "5", - ",", - " ", - "2", - "0", - ",", - " ", - "0", - ",", - " ", - "0", - ")", - " ", - "#", - " ", - "l", - "o", - "c", - "a", - "l", - " ", - "9", - "p", - "m", - " ", - "a", - "t", - " ", - "S", - "S", - "T", - "\n", - "s", - "a", - "m", - "p", - "l", - "e", - "_", - "e", - "n", - "d", - " ", - "=", - " ", - "s", - "a", - "m", - "p", - "l", - "e", - "_", - "s", - "t", - "a", - "r", - "t", - " ", - "+", - " ", - "s", - "k", - ".", - "d", - "u", - "r", - "a", - "t", - "i", - "o", - "n", - "(", - "h", - "o", - "u", - "r", - "s", - "=", - "7", - ")", - "\n", - "s", - "a", - "m", - "p", - "l", - "e", - "_", - "t", - "i", - "m", - "e", - "s", - " ", - "=", - " ", - "[", - "s", - "a", - "m", - "p", - "l", - "e", - "_", - "s", - "t", - "a", - "r", - "t", - " ", - "+", - " ", - "s", - "k", - ".", - "d", - "u", - "r", - "a", - "t", - "i", - "o", - "n", - "(", - "m", - "i", - "n", - "u", - "t", - "e", - "s", - "=", - "5", - ")", - "*", - "i", - " ", - "f", - "o", - "r", - " ", - "i", - " ", - "i", - "n", - " ", - "r", - "a", - "n", - "g", - "e", - "(", - "7", - "*", - "6", - "0", - "/", - "/", - "5", - ")", - "]", - "\n", - "\n", - "#", - " ", - "T", - "r", - "u", - "t", - "h", - " ", - "a", - "t", - " ", - "f", - "i", - "r", - "s", - "t", - " ", - "s", - "a", - "m", - "p", - "l", - "e", - " ", - "t", - "i", - "m", - "e", - "\n", - "s", - "t", - "a", - "t", - "e", - "0", - "_", - "t", - "r", - "u", - "t", - "h", - " ", - "=", - " ", - "t", - "r", - "u", - "t", - "h", - ".", - "i", - "n", - "t", - "e", - "r", - "p", - "(", - "s", - "a", - "m", - "p", - "l", - "e", - "_", - "t", - "i", - "m", - "e", - "s", - "[", - "0", - "]", - ")", - "\n", - "\n", - "#", - " ", - "t", - "h", - "e", - " ", - "o", - "p", - "t", - "i", - "c", - "a", - "l", - " ", - "m", - "e", - "a", - "s", - "u", - "r", - "e", - "m", - "e", - "n", - "t", - " ", - "i", - "s", - " ", - "a", - " ", - "l", - "i", - "n", - "e", - "-", - "o", - "f", - "-", - "s", - "i", - "g", - "h", - "t", - " ", - "v", - "e", - "c", - "t", - "o", - "r", - " ", - "f", - "r", - "o", - "m", - " ", - "o", - "b", - "s", - "e", - "r", - "v", - "e", - "r", - " ", - "t", - "o", - " ", - "s", - "a", - "t", - "e", - "l", - "l", - "i", - "t", - "e", - " ", - "i", - "n", - " ", - "t", - "h", - "e", - " ", - "i", - "n", - "e", - "r", - "t", - "i", - "a", - "l", - " ", - "f", - "r", - "a", - "m", - "e", - "\n", - "s", - "s", - "t", - "_", - "p", - "o", - "s", - " ", - "=", - " ", - "[", - "s", - "k", - ".", - "f", - "r", - "a", - "m", - "e", - "t", - "r", - "a", - "n", - "s", - "f", - "o", - "r", - "m", - ".", - "q", - "i", - "t", - "r", - "f", - "2", - "g", - "c", - "r", - "f", - "(", - "t", - ")", - " ", - "*", - " ", - "s", - "s", - "t", - ".", - "v", - "e", - "c", - "t", - "o", - "r", - " ", - "f", - "o", - "r", - " ", - "t", - " ", - "i", - "n", - " ", - "s", - "a", - "m", - "p", - "l", - "e", - "_", - "t", - "i", - "m", - "e", - "s", - "]", - "\n", - "s", - "a", - "t", - "_", - "s", - "t", - "a", - "t", - "e", - " ", - "=", - " ", - "[", - "t", - "r", - "u", - "t", - "h", - ".", - "i", - "n", - "t", - "e", - "r", - "p", - "(", - "t", - ")", - " ", - "f", - "o", - "r", - " ", - "t", - " ", - "i", - "n", - " ", - "s", - "a", - "m", - "p", - "l", - "e", - "_", - "t", - "i", - "m", - "e", - "s", - "]", - "\n", - "l", - "o", - "s", - "_", - "m", - "e", - "a", - "s", - " ", - "=", - " ", - "[", - "s", - "a", - "t", - "_", - "s", - "t", - "a", - "t", - "e", - "[", - "i", - "d", - "x", - "]", - "[", - "0", - ":", - "3", - "]", - " ", - "-", - " ", - "s", - "s", - "t", - "_", - "p", - "o", - "s", - "[", - "i", - "d", - "x", - "]", - " ", - "f", - "o", - "r", - " ", - "i", - "d", - "x", - " ", - "i", - "n", - " ", - "r", - "a", - "n", - "g", - "e", - "(", - "l", - "e", - "n", - "(", - "s", - "a", - "m", - "p", - "l", - "e", - "_", - "t", - "i", - "m", - "e", - "s", - ")", - ")", - "]", - "\n", - "#", - " ", - "N", - "o", - "r", - "m", - "a", - "l", - "i", - "z", - "e", - " ", - "t", - "h", - "e", - " ", - "l", - "i", - "n", - "e", - "-", - "o", - "f", - "-", - "s", - "i", - "g", - "h", - "t", - " ", - "m", - "e", - "a", - "s", - "u", - "r", - "e", - "m", - "e", - "n", - "t", - "s", - " ", - "t", - "o", - " ", - "g", - "e", - "t", - " ", - "u", - "n", - "i", - "t", - " ", - "v", - "e", - "c", - "t", - "o", - "r", - "s", - "\n", - "l", - "o", - "s", - "_", - "m", - "e", - "a", - "s", - " ", - "=", - " ", - "[", - "l", - "o", - "s", - " ", - "/", - " ", - "n", - "p", - ".", - "l", - "i", - "n", - "a", - "l", - "g", - ".", - "n", - "o", - "r", - "m", - "(", - "l", - "o", - "s", - ")", - " ", - "f", - "o", - "r", - " ", - "l", - "o", - "s", - " ", - "i", - "n", - " ", - "l", - "o", - "s", - "_", - "m", - "e", - "a", - "s", - "]", - "\n", - "\n", - "#", - " ", - "a", - "d", - "d", - " ", - "n", - "o", - "i", - "s", - "e", - " ", - "t", - "o", - " ", - "t", - "h", - "e", - " ", - "m", - "e", - "a", - "s", - "u", - "r", - "e", - "m", - "e", - "n", - "t", - "s", - ".", - " ", - " ", - "A", - "s", - "s", - "u", - "m", - "e", - " ", - "3", - "0", - " ", - "m", - "i", - "c", - "r", - "o", - "r", - "a", - "d", - "i", - "a", - "n", - "s", - " ", - "o", - "f", - " ", - "a", - "n", - "g", - "u", - "l", - "a", - "r", - " ", - "n", - "o", - "i", - "s", - "e", - "\n", - "#", - " ", - "N", - "o", - "t", - "e", - ":", - " ", - "I", - " ", - "m", - "a", - "d", - "e", - " ", - "u", - "p", - " ", - "3", - "0", - " ", - "m", - "i", - "c", - "r", - "o", - "r", - "a", - "d", - "i", - "a", - "n", - "s", - " ", - "(", - "6", - " ", - "a", - "r", - "c", - "s", - "e", - "c", - "o", - "n", - "d", - "s", - ")", - ".", - "\n", - "a", - "n", - "g", - "_", - "n", - "o", - "i", - "s", - "e", - " ", - "=", - " ", - "3", - "0", - "e", - "-", - "6", - "\n", - "\n", - "d", - "e", - "f", - " ", - "a", - "d", - "d", - "_", - "a", - "n", - "g", - "_", - "n", - "o", - "i", - "s", - "e", - "(", - "u", - ",", - " ", - "a", - "n", - "g", - "_", - "n", - "o", - "i", - "s", - "e", - ")", - ":", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "a", - "d", - "d", - " ", - "n", - "o", - "i", - "s", - "e", - " ", - "i", - "n", - " ", - "a", - " ", - "r", - "a", - "n", - "d", - "o", - "m", - " ", - "d", - "i", - "r", - "e", - "c", - "t", - "i", - "o", - "n", - " ", - "p", - "e", - "r", - "p", - "e", - "n", - "d", - "i", - "c", - "u", - "l", - "a", - "r", - " ", - "t", - "o", - " ", - "t", - "h", - "e", - " ", - "l", - "i", - "n", - "e", - " ", - "o", - "f", - " ", - "s", - "i", - "g", - "h", - "t", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "f", - "i", - "r", - "s", - "t", - " ", - "g", - "e", - "t", - " ", - "a", - " ", - "r", - "a", - "n", - "d", - "o", - "m", - " ", - "v", - "e", - "c", - "t", - "o", - "r", - "\n", - " ", - " ", - " ", - " ", - "r", - "a", - "n", - "d", - "_", - "v", - "e", - "c", - " ", - "=", - " ", - "n", - "p", - ".", - "r", - "a", - "n", - "d", - "o", - "m", - ".", - "n", - "o", - "r", - "m", - "a", - "l", - "(", - "s", - "i", - "z", - "e", - "=", - "3", - ")", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "m", - "a", - "k", - "e", - " ", - "i", - "t", - " ", - "p", - "e", - "r", - "p", - "e", - "n", - "d", - "i", - "c", - "u", - "l", - "a", - "r", - " ", - "t", - "o", - " ", - "t", - "h", - "e", - " ", - "l", - "i", - "n", - "e", - " ", - "o", - "f", - " ", - "s", - "i", - "g", - "h", - "t", - "\n", - " ", - " ", - " ", - " ", - "r", - "a", - "n", - "d", - "_", - "v", - "e", - "c", - " ", - "-", - "=", - " ", - "r", - "a", - "n", - "d", - "_", - "v", - "e", - "c", - ".", - "d", - "o", - "t", - "(", - "u", - ")", - " ", - "*", - " ", - "u", - "\n", - " ", - " ", - " ", - " ", - "r", - "a", - "n", - "d", - "_", - "v", - "e", - "c", - " ", - "/", - "=", - " ", - "n", - "p", - ".", - "l", - "i", - "n", - "a", - "l", - "g", - ".", - "n", - "o", - "r", - "m", - "(", - "r", - "a", - "n", - "d", - "_", - "v", - "e", - "c", - ")", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "r", - "o", - "t", - "a", - "t", - "e", - " ", - "t", - "h", - "e", - " ", - "l", - "i", - "n", - "e", - " ", - "o", - "f", - " ", - "s", - "i", - "g", - "h", - "t", - " ", - "b", - "y", - " ", - "t", - "h", - "e", - " ", - "n", - "o", - "i", - "s", - "e", - " ", - "a", - "n", - "g", - "l", - "e", - " ", - "i", - "n", - " ", - "t", - "h", - "e", - " ", - "d", - "i", - "r", - "e", - "c", - "t", - "i", - "o", - "n", - " ", - "o", - "f", - " ", - "t", - "h", - "e", - " ", - "r", - "a", - "n", - "d", - "o", - "m", - " ", - "v", - "e", - "c", - "t", - "o", - "r", - "\n", - " ", - " ", - " ", - " ", - "t", - "h", - "e", - "t", - "a", - " ", - "=", - " ", - "n", - "p", - ".", - "r", - "a", - "n", - "d", - "o", - "m", - ".", - "n", - "o", - "r", - "m", - "a", - "l", - "(", - "s", - "c", - "a", - "l", - "e", - "=", - "a", - "n", - "g", - "_", - "n", - "o", - "i", - "s", - "e", - ")", - "\n", - " ", - " ", - " ", - " ", - "u", - "_", - "n", - "o", - "i", - "s", - "y", - " ", - "=", - " ", - "u", - " ", - "*", - " ", - "m", - ".", - "c", - "o", - "s", - "(", - "t", - "h", - "e", - "t", - "a", - ")", - " ", - "+", - " ", - "n", - "p", - ".", - "c", - "r", - "o", - "s", - "s", - "(", - "r", - "a", - "n", - "d", - "_", - "v", - "e", - "c", - ",", - " ", - "u", - ")", - " ", - "*", - " ", - "\\", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "m", - ".", - "s", - "i", - "n", - "(", - "t", - "h", - "e", - "t", - "a", - ")", - " ", - "+", - " ", - "r", - "a", - "n", - "d", - "_", - "v", - "e", - "c", - " ", - "*", - " ", - "r", - "a", - "n", - "d", - "_", - "v", - "e", - "c", - ".", - "d", - "o", - "t", - "(", - "u", - ")", - " ", - "*", - " ", - "(", - "1", - " ", - "-", - " ", - "m", - ".", - "c", - "o", - "s", - "(", - "t", - "h", - "e", - "t", - "a", - ")", - ")", - "\n", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "u", - "_", - "n", - "o", - "i", - "s", - "y", - "\n", + "# Optical Observations of Satellites\n", "\n", - "l", - "o", - "s", - "_", - "m", - "e", - "a", - "s", - "_", - "n", - "o", - "i", - "s", - "y", - " ", - "=", - " ", - "[", - "a", - "d", - "d", - "_", - "a", - "n", - "g", - "_", - "n", - "o", - "i", - "s", - "e", - "(", - "l", - "o", - "s", - ",", - " ", - "a", - "n", - "g", - "_", - "n", - "o", - "i", - "s", - "e", - ")", - " ", - "f", - "o", - "r", - " ", - "l", - "o", - "s", - " ", - "i", - "n", - " ", - "l", - "o", - "s", - "_", - "m", - "e", - "a", - "s", - "]", + "Ground-based telescopes can observe sunlit satellites against the night sky, measuring their angular position (right ascension and declination). These line-of-sight measurements can then refine the satellite's orbit. This technique is particularly valuable for satellites in MEO and GEO orbits, which are visible for long periods at night and lack easy access to GPS signals. Organizations like the US Space Force and commercial companies such as [ExoAnalytic](https://exoanalytic.com/space-intelligence/) routinely use optical tracking to maintain satellite catalogs.\n", "\n", + "## Approach\n", "\n", + "This example uses a **batch least-squares** (Gauss-Newton) technique to update an initial state estimate by minimizing the difference between observed and predicted line-of-sight unit vectors:\n", "\n", - "#", - " ", - "N", - "o", - "w", - ",", - " ", - "m", - "a", - "k", - "e", - " ", - "u", - "p", - " ", - "a", - "n", - " ", - "i", - "n", - "i", - "t", - "i", - "a", - "l", - " ", - "s", - "t", - "a", - "t", - "e", - " ", - "e", - "s", - "t", - "i", - "m", - "a", - "t", - "e", - " ", - "t", - "h", - "a", - "t", - " ", - "w", - "e", - " ", - "a", - "r", - "e", - " ", - "g", - "o", - "i", - "n", - "g", - " ", - "t", - "o", - " ", - "r", - "e", - "f", - "i", - "n", - "e", - " ", - "w", - "i", - "t", - "h", - " ", - "m", - "e", - "a", - "s", - "u", - "r", - "e", - "m", - "e", - "n", - "t", - "s", + "$$\n", + "\\hat{\\mathbf x}_0\n", + "=\n", + "\\arg\\min_{\\mathbf x_0\\in\\mathbb R^6}\n", + "\\sum_{k=1}^{N}\n", + "\\left\\|\n", + "E_k\\Big(\\hat{\\mathbf u}_m(\\alpha_k,\\delta_k)-\\hat{\\mathbf u}(\\phi(t_k,t_0;\\mathbf x_0),\\mathbf r_{0}(t_k))\\Big)\n", + "\\right\\|_{R^{-1}}^{2}\n", + "$$\n", "\n", - "s", - "t", - "a", - "t", - "e", - "0", - "_", - "e", - "s", - "t", - " ", - "=", - " ", - "t", - "r", - "u", - "t", - "h", - ".", - "i", - "n", - "t", - "e", - "r", - "p", - "(", - "s", - "a", - "m", - "p", - "l", - "e", - "_", - "t", - "i", - "m", - "e", - "s", - "[", - "0", - "]", - ")", - " ", - "+", - " ", - "n", - "p", - ".", - "r", - "a", - "n", - "d", - "o", - "m", - ".", - "n", - "o", - "r", - "m", - "a", - "l", - "(", - "s", - "c", - "a", - "l", - "e", - "=", - "[", - "p", - "o", - "s", - "_", - "n", - "o", - "i", - "s", - "e", - "]", - "*", - "3", - " ", - "+", - " ", - "[", - "v", - "e", - "l", - "_", - "n", - "o", - "i", - "s", - "e", - "]", - "*", - "3", - ")", + "where:\n", "\n", + "- $\\hat{\\mathbf u}_m(\\alpha,\\delta) = [\\cos\\delta\\cos\\alpha,\\; \\cos\\delta\\sin\\alpha,\\; \\sin\\delta]^T$ is the measured unit vector from right ascension $\\alpha$ and declination $\\delta$\n", + "- $\\hat{\\mathbf u}(\\mathbf x_k, \\mathbf r_0) = \\frac{\\mathbf r_k - \\mathbf r_0}{\\|\\mathbf r_k - \\mathbf r_0\\|}$ is the predicted unit vector from observer at $\\mathbf r_0$ to satellite at $\\mathbf r_k$\n", + "- $\\mathbf x_k = \\phi(t_k, t_0; \\mathbf x_0)$ is the propagated state at time $t_k$ given initial state $\\mathbf x_0$\n", + "- $E_k$ projects the 3D residual into the local tangent plane (RA/Dec directions) to keep the measurement space 2D and avoid singularities\n", "\n", - "#", - " ", - "O", - "u", - "r", - " ", - "i", - "n", - "i", - "t", - "i", - "a", - "l", - " ", - "s", - "t", - "a", - "t", - "e", - " ", - "e", - "r", - "r", - "o", - "r", - " ", - ".", - ".", - " ", - "r", - "e", - "c", - "o", - "r", - "d", - " ", - "f", - "o", - "r", - " ", - "l", - "a", - "t", - "e", - "r", - " ", - "c", - "o", - "m", - "p", - "a", - "r", - "i", - "s", - "o", - "n", - "\n", - "s", - "t", - "a", - "t", - "e", - "0", - "_", - "e", - "s", - "t", - "_", - "p", - "r", - "i", - "o", - "r", - " ", - "=", - " ", - "s", - "t", - "a", - "t", - "e", - "0", - "_", - "e", - "s", - "t", - ".", - "c", - "o", - "p", - "y", - "(", - ")", - "\n", - "\n", - "d", - "e", - "f", - " ", - "u", - "n", - "i", - "t", - "_", - "v", - "e", - "c", - "t", - "o", - "r", - "_", - "j", - "a", - "c", - "o", - "b", - "i", - "a", - "n", - "(", - "r", - "h", - "o", - ")", - ":", - "\n", - " ", - " ", - " ", - " ", - "\"", - "\"", - "\"", - "\n", - " ", - " ", - " ", - " ", - "J", - "a", - "c", - "o", - "b", - "i", - "a", - "n", - " ", - "o", - "f", - " ", - "u", - "n", - "i", - "t", - " ", - "v", - "e", - "c", - "t", - "o", - "r", - " ", - "w", - "i", - "t", - "h", - " ", - "r", - "e", - "s", - "p", - "e", - "c", - "t", - " ", - "t", - "o", - " ", - "t", - "h", - "e", - " ", - "o", - "r", - "i", - "g", - "i", - "n", - "a", - "l", - " ", - "v", - "e", - "c", - "t", - "o", - "r", - "\n", - " ", - " ", - " ", - " ", - "\"", - "\"", - "\"", - "\n", - " ", - " ", - " ", - " ", - "r", - "h", - "o", - "_", - "n", - "o", - "r", - "m", - " ", - "=", - " ", - "n", - "p", - ".", - "l", - "i", - "n", - "a", - "l", - "g", - ".", - "n", - "o", - "r", - "m", - "(", - "r", - "h", - "o", - ")", - "\n", - " ", - " ", - " ", - " ", - "u", - " ", - "=", - " ", - "r", - "h", - "o", - " ", - "/", - " ", - "r", - "h", - "o", - "_", - "n", - "o", - "r", - "m", - "\n", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "(", - "n", - "p", - ".", - "e", - "y", - "e", - "(", - "3", - ")", - " ", - "-", - " ", - "n", - "p", - ".", - "o", - "u", - "t", - "e", - "r", - "(", - "u", - ",", - " ", - "u", - ")", - ")", - " ", - "/", - " ", - "r", - "h", - "o", - "_", - "n", - "o", - "r", - "m", - "\n", - "\n", - "\n", - "d", - "e", - "f", - " ", - "t", - "a", - "n", - "g", - "e", - "n", - "t", - "_", - "b", - "a", - "s", - "i", - "s", - "(", - "u", - ")", - ":", - "\n", - " ", - " ", - " ", - " ", - "\"", - "\"", - "\"", - "\n", - " ", - " ", - " ", - " ", - "R", - "A", - "/", - "D", - "e", - "c", - " ", - "t", - "a", - "n", - "g", - "e", - "n", - "t", - " ", - "b", - "a", - "s", - "i", - "s", - " ", - "a", - "t", - " ", - "p", - "r", - "e", - "d", - "i", - "c", - "t", - "e", - "d", - " ", - "d", - "i", - "r", - "e", - "c", - "t", - "i", - "o", - "n", - "\n", - " ", - " ", - " ", - " ", - "\"", - "\"", - "\"", - "\n", - " ", - " ", - " ", - " ", - "u", - "x", - ",", - " ", - "u", - "y", - ",", - " ", - "u", - "z", - " ", - "=", - " ", - "u", - "\n", - " ", - " ", - " ", - " ", - "a", - "l", - "p", - "h", - "a", - " ", - "=", - " ", - "n", - "p", - ".", - "a", - "r", - "c", - "t", - "a", - "n", - "2", - "(", - "u", - "y", - ",", - " ", - "u", - "x", - ")", - "\n", - " ", - " ", - " ", - " ", - "d", - "e", - "l", - "t", - "a", - " ", - "=", - " ", - "n", - "p", - ".", - "a", - "r", - "c", - "s", - "i", - "n", - "(", - "u", - "z", - ")", - "\n", - "\n", - " ", - " ", - " ", - " ", - "e", - "_", - "a", - "l", - "p", - "h", - "a", - " ", - "=", - " ", - "n", - "p", - ".", - "a", - "r", - "r", - "a", - "y", - "(", - "[", - "-", - "n", - "p", - ".", - "s", - "i", - "n", - "(", - "a", - "l", - "p", - "h", - "a", - ")", - ",", - " ", - "n", - "p", - ".", - "c", - "o", - "s", - "(", - "a", - "l", - "p", - "h", - "a", - ")", - ",", - " ", - "0", - ".", - "0", - "]", - ")", - "\n", - " ", - " ", - " ", - " ", - "e", - "_", - "d", - "e", - "l", - "t", - "a", - " ", - "=", - " ", - "n", - "p", - ".", - "a", - "r", - "r", - "a", - "y", - "(", - "[", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "-", - "n", - "p", - ".", - "s", - "i", - "n", - "(", - "d", - "e", - "l", - "t", - "a", - ")", - "*", - "n", - "p", - ".", - "c", - "o", - "s", - "(", - "a", - "l", - "p", - "h", - "a", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "-", - "n", - "p", - ".", - "s", - "i", - "n", - "(", - "d", - "e", - "l", - "t", - "a", - ")", - "*", - "n", - "p", - ".", - "s", - "i", - "n", - "(", - "a", - "l", - "p", - "h", - "a", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "n", - "p", - ".", - "c", - "o", - "s", - "(", - "d", - "e", - "l", - "t", - "a", - ")", - "\n", - " ", - " ", - " ", - " ", - "]", - ")", - "\n", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "n", - "p", - ".", - "v", - "s", - "t", - "a", - "c", - "k", - "(", - "(", - "e", - "_", - "a", - "l", - "p", - "h", - "a", - ",", - " ", - "e", - "_", - "d", - "e", - "l", - "t", - "a", - ")", - ")", - "\n", - "\n", - "#", - " ", - "O", - "K", - ",", - " ", - "l", - "e", - "t", - "s", - " ", - "d", - "o", - " ", - "i", - "t", - "!", - "\n", - "#", - " ", - "3", - " ", - "i", - "t", - "e", - "r", - "a", - "t", - "i", - "o", - "n", - "s", - " ", - "o", - "f", - " ", - "G", - "a", - "u", - "s", - "s", - "-", - "N", - "e", - "w", - "t", - "o", - "n", - " ", - "l", - "e", - "a", - "s", - "t", - " ", - "s", - "q", - "u", - "a", - "r", - "e", - "s", - "\n", - "f", - "o", - "r", - " ", - "n", - " ", - "i", - "n", - " ", - "r", - "a", - "n", - "g", - "e", - "(", - "0", - ",", - "3", - ")", - ":", - "\n", - " ", - " ", - " ", - " ", - "p", - "r", - "i", - "n", - "t", - "(", - "f", - "\"", - "I", - "t", - "e", - "r", - "a", - "t", - "i", - "o", - "n", - " ", - "{", - "n", - "}", - "\"", - ")", - "\n", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "P", - "r", - "o", - "p", - "a", - "g", - "a", - "t", - "e", - " ", - "s", - "t", - "a", - "t", - "e", - " ", - "t", - "o", - " ", - "g", - "e", - "t", - " ", - "s", - "t", - "a", - "t", - "e", - " ", - "t", - "r", - "a", - "n", - "s", - "i", - "t", - "i", - "o", - "n", - " ", - "m", - "a", - "t", - "r", - "i", - "x", - " ", - "a", - "t", - " ", - "e", - "a", - "c", - "h", - " ", - "t", - "i", - "m", - "e", - "\n", - " ", - " ", - " ", - " ", - "r", - "e", - "s", - " ", - "=", - " ", - "s", - "k", - ".", - "p", - "r", - "o", - "p", - "a", - "g", - "a", - "t", - "e", - "(", - "s", - "t", - "a", - "t", - "e", - "0", - "_", - "e", - "s", - "t", - ",", - " ", - "s", - "a", - "m", - "p", - "l", - "e", - "_", - "t", - "i", - "m", - "e", - "s", - "[", - "0", - "]", - ",", - " ", - "s", - "a", - "m", - "p", - "l", - "e", - "_", - "t", - "i", - "m", - "e", - "s", - "[", - "-", - "1", - "]", - ",", - " ", - "o", - "u", - "t", - "p", - "u", - "t", - "_", - "p", - "h", - "i", - "=", - "T", - "r", - "u", - "e", - ",", - " ", - "p", - "r", - "o", - "p", - "s", - "e", - "t", - "t", - "i", - "n", - "g", - "s", - "=", - "s", - "e", - "t", - "t", - "i", - "n", - "g", - "s", - ")", - "\n", - " ", - " ", - " ", - " ", - "[", - "s", - "t", - "a", - "t", - "e", - ",", - "p", - "h", - "i", - "]", - " ", - "=", - " ", - "z", - "i", - "p", - "(", - "*", - "[", - "r", - "e", - "s", - ".", - "i", - "n", - "t", - "e", - "r", - "p", - "(", - "t", - ",", - " ", - "o", - "u", - "t", - "p", - "u", - "t", - "_", - "p", - "h", - "i", - "=", - "T", - "r", - "u", - "e", - ")", - " ", - "f", - "o", - "r", - " ", - "t", - " ", - "i", - "n", - " ", - "s", - "a", - "m", - "p", - "l", - "e", - "_", - "t", - "i", - "m", - "e", - "s", - "]", - ")", - "\n", - "\n", - " ", - " ", - " ", - " ", - "r", - "e", - "s", - "i", - "d", - "u", - "a", - "l", - "s", - " ", - "=", - " ", - "[", - "]", - "\n", - " ", - " ", - " ", - " ", - "J", - "_", - "l", - "i", - "s", - "t", - " ", - "=", - " ", - "[", - "]", - "\n", - "\n", - " ", - " ", - " ", - " ", - "f", - "o", - "r", - " ", - "i", - "d", - "x", - ",", - " ", - "t", - " ", - "i", - "n", - " ", - "e", - "n", - "u", - "m", - "e", - "r", - "a", - "t", - "e", - "(", - "s", - "a", - "m", - "p", - "l", - "e", - "_", - "t", - "i", - "m", - "e", - "s", - ")", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "#", - " ", - "G", - "e", - "t", - " ", - "t", - "h", - "e", - " ", - "p", - "r", - "e", - "d", - "i", - "c", - "t", - "e", - "d", - " ", - "l", - "i", - "n", - "e", - " ", - "o", - "f", - " ", - "s", - "i", - "g", - "h", - "t", - " ", - "m", - "e", - "a", - "s", - "u", - "r", - "e", - "m", - "e", - "n", - "t", - " ", - "a", - "t", - " ", - "t", - "h", - "i", - "s", - " ", - "t", - "i", - "m", - "e", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "p", - "_", - "s", - "a", - "t", - " ", - "=", - " ", - "s", - "t", - "a", - "t", - "e", - "[", - "i", - "d", - "x", - "]", - "[", - "0", - ":", - "3", - "]", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "p", - "_", - "o", - "b", - "s", - " ", - "=", - " ", - "s", - "s", - "t", - "_", - "p", - "o", - "s", - "[", - "i", - "d", - "x", - "]", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "l", - "o", - "s", - "_", - "v", - "e", - "c", - " ", - "=", - " ", - "p", - "_", - "s", - "a", - "t", - " ", - "-", - " ", - "p", - "_", - "o", - "b", - "s", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "l", - "o", - "s", - "_", - "p", - "r", - "e", - "d", - " ", - "=", - " ", - "l", - "o", - "s", - "_", - "v", - "e", - "c", - " ", - "/", - " ", - "n", - "p", - ".", - "l", - "i", - "n", - "a", - "l", - "g", - ".", - "n", - "o", - "r", - "m", - "(", - "l", - "o", - "s", - "_", - "v", - "e", - "c", - ")", - "\n", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "#", - " ", - "G", - "e", - "t", - " ", - "t", - "h", - "e", - " ", - "t", - "a", - "n", - "g", - "e", - "n", - "t", - " ", - "b", - "a", - "s", - "i", - "s", - " ", - "i", - "n", - " ", - "p", - "r", - "e", - "d", - "i", - "c", - "t", - "e", - "d", - " ", - "d", - "i", - "r", - "e", - "c", - "t", - "i", - "o", - "n", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "E", - " ", - "=", - " ", - "t", - "a", - "n", - "g", - "e", - "n", - "t", - "_", - "b", - "a", - "s", - "i", - "s", - "(", - "l", - "o", - "s", - "_", - "p", - "r", - "e", - "d", - ")", - "\n", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "#", - " ", - "G", - "e", - "t", - " ", - "r", - "e", - "s", - "i", - "d", - "u", - "a", - "l", - "s", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "r", - " ", - "=", - " ", - "E", - " ", - "@", - " ", - "(", - "l", - "o", - "s", - "_", - "m", - "e", - "a", - "s", - "_", - "n", - "o", - "i", - "s", - "y", - "[", - "i", - "d", - "x", - "]", - " ", - "-", - " ", - "l", - "o", - "s", - "_", - "p", - "r", - "e", - "d", - ")", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "r", - "e", - "s", - "i", - "d", - "u", - "a", - "l", - "s", - ".", - "a", - "p", - "p", - "e", - "n", - "d", - "(", - "r", - ")", - "\n", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "#", - " ", - "I", - "M", - "P", - "O", - "R", - "T", - "A", - "N", - "T", - ":", - " ", - "J", - "a", - "c", - "o", - "b", - "i", - "a", - "n", - " ", - "m", - "u", - "s", - "t", - " ", - "b", - "e", - " ", - "e", - "v", - "a", - "l", - "u", - "a", - "t", - "e", - "d", - " ", - "o", - "n", - " ", - "u", - "n", - "-", - "n", - "o", - "r", - "m", - "a", - "l", - "i", - "z", - "e", - "d", - " ", - "L", - "O", - "S", - " ", - "v", - "e", - "c", - "t", - "o", - "r", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "U", - " ", - "=", - " ", - "u", - "n", - "i", - "t", - "_", - "v", - "e", - "c", - "t", - "o", - "r", - "_", - "j", - "a", - "c", - "o", - "b", - "i", - "a", - "n", - "(", - "l", - "o", - "s", - "_", - "v", - "e", - "c", - ")", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "#", - " ", - "A", - " ", - "i", - "s", - " ", - "o", - "u", - "r", - " ", - "s", - "t", - "a", - "t", - "e", - " ", - "t", - "r", - "a", - "n", - "s", - "i", - "t", - "i", - "o", - "n", - " ", - "m", - "a", - "t", - "r", - "i", - "x", - " ", - "f", - "o", - "r", - " ", - "t", - "h", - "i", - "s", - " ", - "t", - "i", - "m", - "e", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "#", - " ", - "w", - "h", - "i", - "c", - "h", - " ", - "m", - "a", - "p", - "s", - " ", - "c", - "h", - "a", - "n", - "g", - "e", - "s", - " ", - "i", - "n", - " ", - "i", - "n", - "i", - "t", - "i", - "a", - "l", - " ", - "s", - "t", - "a", - "t", - "e", - " ", - "t", - "o", - " ", - "c", - "h", - "a", - "n", - "g", - "e", - "s", - " ", - "i", - "n", - " ", - "s", - "t", - "a", - "t", - "e", - " ", - "a", - "t", - " ", - "t", - "h", - "i", - "s", - " ", - "t", - "i", - "m", - "e", - ".", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "#", - " ", - "W", - "e", - " ", - "o", - "n", - "l", - "y", - " ", - "c", - "a", - "r", - "e", - " ", - "a", - "b", - "o", - "u", - "t", - " ", - "t", - "h", - "e", - " ", - "p", - "o", - "s", - "i", - "t", - "i", - "o", - "n", - " ", - "p", - "a", - "r", - "t", - " ", - "o", - "f", - " ", - "t", - "h", - "e", - " ", - "s", - "t", - "a", - "t", - "e", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "#", - " ", - "s", - "i", - "n", - "c", - "e", - " ", - "t", - "h", - "e", - " ", - "m", - "e", - "a", - "s", - "u", - "r", - "e", - "m", - "e", - "n", - "t", - " ", - "i", - "s", - " ", - "o", - "n", - "l", - "y", - " ", - "a", - " ", - "f", - "u", - "n", - "c", - "t", - "i", - "o", - "n", - " ", - "o", - "f", - " ", - "p", - "o", - "s", - "i", - "t", - "i", - "o", - "n", - ".", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "A", - " ", - "=", - " ", - "p", - "h", - "i", - "[", - "i", - "d", - "x", - "]", - "[", - "0", - ":", - "3", - ",", - " ", - ":", - "]", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "H", - "k", - " ", - "=", - " ", - "-", - " ", - "E", - " ", - "@", - " ", - "U", - " ", - "@", - " ", - "A", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "J", - "_", - "l", - "i", - "s", - "t", - ".", - "a", - "p", - "p", - "e", - "n", - "d", - "(", - "H", - "k", - ")", - "\n", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "O", - "K", - ",", - " ", - "n", - "o", - "w", - " ", - "d", - "o", - " ", - "t", - "h", - "e", - " ", - "l", - "e", - "a", - "s", - "t", - " ", - "s", - "q", - "u", - "a", - "r", - "e", - "s", - " ", - "s", - "o", - "l", - "v", - "e", - " ", - "t", - "o", - " ", - "g", - "e", - "t", - " ", - "s", - "t", - "a", - "t", - "e", - " ", - "u", - "p", - "d", - "a", - "t", - "e", - "\n", - " ", - " ", - " ", - " ", - "r", - " ", - "=", - " ", - "n", - "p", - ".", - "h", - "s", - "t", - "a", - "c", - "k", - "(", - "r", - "e", - "s", - "i", - "d", - "u", - "a", - "l", - "s", - ")", - "\n", - " ", - " ", - " ", - " ", - "H", - " ", - "=", - " ", - "n", - "p", - ".", - "v", - "s", - "t", - "a", - "c", - "k", - "(", - "J", - "_", - "l", - "i", - "s", - "t", - ")", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "S", - "o", - "l", - "v", - "e", - " ", - "f", - "o", - "r", - " ", - "s", - "t", - "a", - "t", - "e", - " ", - "u", - "p", - "d", - "a", - "t", - "e", - " ", - "u", - "s", - "i", - "n", - "g", - " ", - "l", - "e", - "a", - "s", - "t", - " ", - "s", - "q", - "u", - "a", - "r", - "e", - "s", - "\n", - " ", - " ", - " ", - " ", - "d", - "x", - " ", - "=", - " ", - "n", - "p", - ".", - "l", - "i", - "n", - "a", - "l", - "g", - ".", - "l", - "s", - "t", - "s", - "q", - "(", - "H", - ",", - " ", - "r", - ",", - " ", - "r", - "c", - "o", - "n", - "d", - "=", - "N", - "o", - "n", - "e", - ")", - "[", - "0", - "]", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "A", - "d", - "j", - "u", - "s", - "t", - " ", - "t", - "h", - "e", - " ", - "i", - "n", - "i", - "t", - "i", - "a", - "l", - " ", - "s", - "t", - "a", - "t", - "e", - " ", - "e", - "s", - "t", - "i", - "m", - "a", - "t", - "e", - " ", - "b", - "y", - " ", - "t", - "h", - "e", - " ", - "s", - "t", - "a", - "t", - "e", - " ", - "u", - "p", - "d", - "a", - "t", - "e", - "\n", - " ", - " ", - " ", - " ", - "s", - "t", - "a", - "t", - "e", - "0", - "_", - "e", - "s", - "t", - " ", - "-", - "=", - " ", - "d", - "x", - "\n", - "\n", - "p", - "r", - "i", - "n", - "t", - "(", - "f", - "\"", - "I", - "n", - "i", - "t", - "i", - "a", - "l", - " ", - "S", - "t", - "a", - "t", - "e", - " ", - "E", - "r", - "r", - "o", - "r", - ":", - " ", - "{", - "n", - "p", - ".", - "l", - "i", - "n", - "a", - "l", - "g", - ".", - "n", - "o", - "r", - "m", - "(", - "s", - "t", - "a", - "t", - "e", - "0", - "_", - "e", - "s", - "t", - "_", - "p", - "r", - "i", - "o", - "r", - " ", - "-", - " ", - "s", - "t", - "a", - "t", - "e", - "0", - "_", - "t", - "r", - "u", - "t", - "h", - ")", - "/", - "1", - "e", - "3", - ":", - ".", - "3", - "f", - "}", - " ", - "k", - "m", - ",", - " ", - "{", - "n", - "p", - ".", - "l", - "i", - "n", - "a", - "l", - "g", - ".", - "n", - "o", - "r", - "m", - "(", - "s", - "t", - "a", - "t", - "e", - "0", - "_", - "e", - "s", - "t", - "_", - "p", - "r", - "i", - "o", - "r", - "[", - "3", - ":", - "6", - "]", - " ", - "-", - " ", - "s", - "t", - "a", - "t", - "e", - "0", - "_", - "t", - "r", - "u", - "t", - "h", - "[", - "3", - ":", - "6", - "]", - ")", - ":", - ".", - "3", - "f", - "}", - " ", - "m", - "/", - "s", - "\"", - ")", - "\n", - "p", - "r", - "i", - "n", - "t", - "(", - "f", - "'", - "F", - "i", - "n", - "a", - "l", - " ", - "S", - "t", - "a", - "t", - "e", - " ", - "E", - "r", - "r", - "o", - "r", - ":", - " ", - "{", - "n", - "p", - ".", - "l", - "i", - "n", - "a", - "l", - "g", - ".", - "n", - "o", - "r", - "m", - "(", - "s", - "t", - "a", - "t", - "e", - "0", - "_", - "e", - "s", - "t", - " ", - "-", - " ", - "s", - "t", - "a", - "t", - "e", - "0", - "_", - "t", - "r", - "u", - "t", - "h", - ")", - "/", - "1", - "e", - "3", - ":", - ".", - "3", - "f", - "}", - " ", - "k", - "m", - ",", - " ", - "{", - "n", - "p", - ".", - "l", - "i", - "n", - "a", - "l", - "g", - ".", - "n", - "o", - "r", - "m", - "(", - "s", - "t", - "a", - "t", - "e", - "0", - "_", - "e", - "s", - "t", - "[", - "3", - ":", - "6", - "]", - " ", - "-", - " ", - "s", - "t", - "a", - "t", - "e", - "0", - "_", - "t", - "r", - "u", - "t", - "h", - "[", - "3", - ":", - "6", - "]", - ")", - ":", - ".", - "3", - "f", - "}", - " ", - "m", - "/", - "s", - "'", + "The state transition matrix $\\Phi$ from the high-precision propagator maps initial state perturbations to perturbations at each measurement time, forming the Jacobian of the linearized system." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a53d5d64", + "metadata": {}, + "outputs": [], + "source": [ + "import satkit as sk\n", + "import numpy as np\n", + "import math as m\n", + "import datetime\n", + "\n", + "# Take our observer to be the Space Surveillance Telescope (SST)\n", + "# in Exmouth, Western Australia\n", + "sst_lat = -22.675\n", + "sst_lon = 114.235\n", + "sst_alt = 0.5 # km\n", + "sst = sk.itrfcoord(latitude_deg=sst_lat, longitude_deg=sst_lon, alt_m=sst_alt * 1e3)\n", + "\n", + "# Lets look at INTELSAT 19 (IS-19), a geostationary communications satellite.\n", + "# It is currently located at 166E longitude, so it should be visible from the SST.\n", + "# We can get the TLE for INTELSAT 19 from Celestrak:\n", + "# https://celestrak.com/NORAD/elements/geo.txt\n", + "tle = sk.TLE.from_lines(\n", + " [\n", + " \"INTELSAT 19 (IS-19)\",\n", + " \"1 38356U 12030A 26045.51850411 -.00000051 00000+0 00000+0 0 9998\",\n", + " \"2 38356 0.0150 97.8441 0002877 233.5096 165.8257 1.00272325 49221\",\n", + " ]\n", + ")\n", + "epoch = tle.epoch\n", + "\n", + "# Lets make the initial \"true\" state the tle-predicted state at epoch\n", + "pteme, vteme = sk.sgp4(tle, epoch)\n", + "q = sk.frametransform.qteme2gcrf(epoch)\n", + "pgcrf = q * pteme\n", + "vgcrf = q * vteme\n", + "state0 = np.hstack((pgcrf, vgcrf))\n", + "print(epoch)\n", + "begin_time = epoch\n", + "end_time = epoch + sk.duration(days=2)\n", + "settings = sk.propsettings()\n", + "settings.precompute_terms(begin_time, end_time)\n", + "# True state over 1 day\n", + "truth = sk.propagate(state0, begin_time, end_time, propsettings=settings)\n", + "\n", + "# Assume position knowledge to 10 km, velocity to 5 cm/s\n", + "pos_noise = 10e3\n", + "vel_noise = 0.05\n", + "\n", + "# initial state estimate\n", + "state0_est = state0 + np.random.normal(scale=[pos_noise] * 3 + [vel_noise] * 3)\n", + "state0_est_prior = state0_est.copy()\n", + "\n", + "# sample times for measurements. Every 5 minues for 6 hours beginning at local 9pm and going to local 4am\n", + "sample_start = sk.time(2026, 2, 15, 20, 0, 0) # local 9pm at SST\n", + "sample_end = sample_start + sk.duration(hours=7)\n", + "sample_times = [sample_start + sk.duration(minutes=5) * i for i in range(7 * 60 // 5)]\n", + "\n", + "# Truth at first sample time\n", + "state0_truth = truth.interp(sample_times[0])\n", + "\n", + "# the optical measurement is a line-of-sight vector from observer to satellite in the inertial frame\n", + "sst_pos = [sk.frametransform.qitrf2gcrf(t) * sst.vector for t in sample_times]\n", + "sat_state = [truth.interp(t) for t in sample_times]\n", + "los_meas = [sat_state[idx][0:3] - sst_pos[idx] for idx in range(len(sample_times))]\n", + "# Normalize the line-of-sight measurements to get unit vectors\n", + "los_meas = [los / np.linalg.norm(los) for los in los_meas]\n", + "\n", + "# add noise to the measurements. Assume 30 microradians of angular noise\n", + "# Note: I made up 30 microradians (6 arcseconds).\n", + "ang_noise = 30e-6\n", + "\n", + "\n", + "def add_ang_noise(u, ang_noise):\n", + " # add noise in a random direction perpendicular to the line of sight\n", + " # first get a random vector\n", + " rand_vec = np.random.normal(size=3)\n", + " # make it perpendicular to the line of sight\n", + " rand_vec -= rand_vec.dot(u) * u\n", + " rand_vec /= np.linalg.norm(rand_vec)\n", + " # rotate the line of sight by the noise angle in the direction of the random vector\n", + " theta = np.random.normal(scale=ang_noise)\n", + " u_noisy = (\n", + " u * m.cos(theta)\n", + " + np.cross(rand_vec, u) * m.sin(theta)\n", + " + rand_vec * rand_vec.dot(u) * (1 - m.cos(theta))\n", + " )\n", + " return u_noisy\n", + "\n", + "\n", + "los_meas_noisy = [add_ang_noise(los, ang_noise) for los in los_meas]\n", + "\n", + "\n", + "# Now, make up an initial state estimate that we are going to refine with measurements\n", + "state0_est = truth.interp(sample_times[0]) + np.random.normal(\n", + " scale=[pos_noise] * 3 + [vel_noise] * 3\n", + ")\n", + "\n", + "# Our initial state error .. record for later comparison\n", + "state0_est_prior = state0_est.copy()\n", + "\n", + "\n", + "def unit_vector_jacobian(rho):\n", + " \"\"\"\n", + " Jacobian of unit vector with respect to the original vector\n", + " \"\"\"\n", + " rho_norm = np.linalg.norm(rho)\n", + " u = rho / rho_norm\n", + " return (np.eye(3) - np.outer(u, u)) / rho_norm\n", + "\n", + "\n", + "def tangent_basis(u):\n", + " \"\"\"\n", + " RA/Dec tangent basis at predicted direction\n", + " \"\"\"\n", + " ux, uy, uz = u\n", + " alpha = np.arctan2(uy, ux)\n", + " delta = np.arcsin(uz)\n", + "\n", + " e_alpha = np.array([-np.sin(alpha), np.cos(alpha), 0.0])\n", + " e_delta = np.array(\n", + " [-np.sin(delta) * np.cos(alpha), -np.sin(delta) * np.sin(alpha), np.cos(delta)]\n", + " )\n", + " return np.vstack((e_alpha, e_delta))\n", + "\n", + "\n", + "# OK, lets do it!\n", + "# 3 iterations of Gauss-Newton least squares\n", + "for n in range(0, 3):\n", + " print(f\"Iteration {n}\")\n", + "\n", + " # Propagate state to get state transition matrix at each time\n", + " res = sk.propagate(\n", + " state0_est,\n", + " sample_times[0],\n", + " sample_times[-1],\n", + " output_phi=True,\n", + " propsettings=settings,\n", + " )\n", + " [state, phi] = zip(*[res.interp(t, output_phi=True) for t in sample_times])\n", + "\n", + " residuals = []\n", + " J_list = []\n", + "\n", + " for idx, t in enumerate(sample_times):\n", + " # Get the predicted line of sight measurement at this time\n", + " p_sat = state[idx][0:3]\n", + " p_obs = sst_pos[idx]\n", + " los_vec = p_sat - p_obs\n", + " los_pred = los_vec / np.linalg.norm(los_vec)\n", + "\n", + " # Get the tangent basis in predicted direction\n", + " E = tangent_basis(los_pred)\n", + "\n", + " # Get residuals\n", + " r = E @ (los_meas_noisy[idx] - los_pred)\n", + " residuals.append(r)\n", + "\n", + " # IMPORTANT: Jacobian must be evaluated on un-normalized LOS vector\n", + " U = unit_vector_jacobian(los_vec)\n", + " # A is our state transition matrix for this time,\n", + " # which maps changes in initial state to changes in state at this time.\n", + " # We only care about the position part of the state\n", + " # since the measurement is only a function of position.\n", + " A = phi[idx][0:3, :]\n", + " Hk = -E @ U @ A\n", + " J_list.append(Hk)\n", + "\n", + " # OK, now do the least squares solve to get state update\n", + " r = np.hstack(residuals)\n", + " H = np.vstack(J_list)\n", + " # Solve for state update using least squares\n", + " dx = np.linalg.lstsq(H, r, rcond=None)[0]\n", + " # Adjust the initial state estimate by the state update\n", + " state0_est -= dx\n", + "\n", + "print(\n", + " f\"Initial State Error: {np.linalg.norm(state0_est_prior - state0_truth) / 1e3:.3f} km, {np.linalg.norm(state0_est_prior[3:6] - state0_truth[3:6]):.3f} m/s\"\n", + ")\n", + "print(\n", + " f\"Final State Error: {np.linalg.norm(state0_est - state0_truth) / 1e3:.3f} km, {np.linalg.norm(state0_est[3:6] - state0_truth[3:6]):.3f} m/s\"\n", ")" ] } @@ -5965,4 +240,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/docs/tutorials/Orbital Mean-Element Message.ipynb b/docs/tutorials/Orbital Mean-Element Message.ipynb index 2ea059c..59ee66c 100644 --- a/docs/tutorials/Orbital Mean-Element Message.ipynb +++ b/docs/tutorials/Orbital Mean-Element Message.ipynb @@ -2,2180 +2,213 @@ "cells": [ { "cell_type": "markdown", - "id": "de7e614b", + "id": "0", "metadata": {}, - "source": "# Orbital Mean-Element Messages\n\nOrbital Mean-Element Messages (OMMs) are a standardized data product defined by [CCSDS](https://ccsds.org/Pubs/502x0b3e1.pdf) for exchanging satellite orbital elements in a machine-readable way. They contain the same orbital parameters as TLEs but in a more structured format, and are growing in popularity as the modern replacement for TLE distribution.\n\nOMMs are available from [CelesTrak](https://celestrak.org) and [Space-Track](https://www.space-track.org) in multiple encodings:\n\n- **JSON** \u2014 the most common and straightforward format\n- **XML** \u2014 more verbose with deeper hierarchy, but widely supported\n- **KVN** \u2014 key-value notation; imposes very little structure\n\n`satkit` supports SGP4 propagation of OMMs represented as Python dictionaries (from JSON) or parsed XML structures. KVN is **not** supported." + "source": [ + "# Orbital Mean-Element Messages\n", + "\n", + "Orbital Mean-Element Messages (OMMs) are a standardized data product defined by [CCSDS](https://ccsds.org/Pubs/502x0b3e1.pdf) for exchanging satellite orbital elements in a machine-readable way. They contain the same orbital parameters as TLEs but in a more structured format, and are growing in popularity as the modern replacement for TLE distribution.\n", + "\n", + "OMMs are available from [CelesTrak](https://celestrak.org) and [Space-Track](https://www.space-track.org) in multiple encodings:\n", + "\n", + "- **JSON** — the most common and straightforward format\n", + "- **XML** — more verbose with deeper hierarchy, but widely supported\n", + "- **KVN** — key-value notation; imposes very little structure\n", + "\n", + "`satkit` supports SGP4 propagation of OMMs represented as Python dictionaries (from JSON) or parsed XML structures. KVN is **not** supported." + ] }, { "cell_type": "markdown", - "id": "0c95f4a5", + "id": "1", "metadata": {}, - "source": "## Example 1: JSON Format\n\nLoad a JSON OMM for the International Space Station from CelesTrak, propagate with SGP4, and convert to geodetic coordinates. The JSON format maps directly to a Python dictionary, making it simple to work with." + "source": [ + "## Example 1: JSON Format\n", + "\n", + "Load a JSON OMM for the International Space Station from CelesTrak, propagate with SGP4, and convert to geodetic coordinates. The JSON format maps directly to a Python dictionary, making it simple to work with." + ] }, { "cell_type": "code", "execution_count": null, - "id": "b0142e6f", + "id": "2", "metadata": {}, "outputs": [], - "source": "import satkit as sk\nimport json\nimport requests\n\n# Query the current ephemeris for the International Space Station (ISS)\nurl = 'https://celestrak.org/NORAD/elements/gp.php?CATNR=25544&FORMAT=json'\ntry:\n response = requests.get(url, headers={'User-Agent': 'satkit-docs'})\n response.raise_for_status()\n omm = response.json()\nexcept Exception:\n # Fallback OMM if CelesTrak is unavailable\n omm = [{\"OBJECT_NAME\": \"ISS (ZARYA)\", \"OBJECT_ID\": \"1998-067A\", \"EPOCH\": \"2026-02-23T06:28:41.856192\",\n \"MEAN_MOTION\": 15.49438695, \"ECCENTRICITY\": 0.0005991, \"INCLINATION\": 51.6372,\n \"RA_OF_ASC_NODE\": 290.9753, \"ARG_OF_PERICENTER\": 34.6694, \"MEAN_ANOMALY\": 131.1947,\n \"EPHEMERIS_TYPE\": 0, \"CLASSIFICATION_TYPE\": \"U\", \"NORAD_CAT_ID\": 25544,\n \"ELEMENT_SET_NO\": 999, \"REV_AT_EPOCH\": 47914, \"BSTAR\": 0.00032055,\n \"MEAN_MOTION_DOT\": 0.00020836, \"MEAN_MOTION_DDOT\": 0}]\n\n# Get a representative time from the output\nepoch = sk.time(omm[0]['EPOCH'])\n# crate a list of times .. once every 10 minutes\ntime_array = [epoch + sk.duration(minutes=i*10) for i in range(6)]\n\n# TEME (inertial) output from SGP4\npTEME, _vTEME = sk.sgp4(omm[0], time_array)\n\n# Rotate to Earth-fixed\npITRF = [sk.frametransform.qteme2itrf(t) * p for t, p in zip(time_array, pTEME)]\n\n# Geodetic coordinates of space station at given times\ncoord = [sk.itrfcoord(x) for x in pITRF]" + "source": [ + "import satkit as sk\n", + "import json\n", + "import requests\n", + "\n", + "# Query the current ephemeris for the International Space Station (ISS)\n", + "url = \"https://celestrak.org/NORAD/elements/gp.php?CATNR=25544&FORMAT=json\"\n", + "try:\n", + " response = requests.get(url, headers={\"User-Agent\": \"satkit-docs\"})\n", + " response.raise_for_status()\n", + " omm = response.json()\n", + "except Exception:\n", + " # Fallback OMM if CelesTrak is unavailable\n", + " omm = [\n", + " {\n", + " \"OBJECT_NAME\": \"ISS (ZARYA)\",\n", + " \"OBJECT_ID\": \"1998-067A\",\n", + " \"EPOCH\": \"2026-02-23T06:28:41.856192\",\n", + " \"MEAN_MOTION\": 15.49438695,\n", + " \"ECCENTRICITY\": 0.0005991,\n", + " \"INCLINATION\": 51.6372,\n", + " \"RA_OF_ASC_NODE\": 290.9753,\n", + " \"ARG_OF_PERICENTER\": 34.6694,\n", + " \"MEAN_ANOMALY\": 131.1947,\n", + " \"EPHEMERIS_TYPE\": 0,\n", + " \"CLASSIFICATION_TYPE\": \"U\",\n", + " \"NORAD_CAT_ID\": 25544,\n", + " \"ELEMENT_SET_NO\": 999,\n", + " \"REV_AT_EPOCH\": 47914,\n", + " \"BSTAR\": 0.00032055,\n", + " \"MEAN_MOTION_DOT\": 0.00020836,\n", + " \"MEAN_MOTION_DDOT\": 0,\n", + " }\n", + " ]\n", + "\n", + "# Get a representative time from the output\n", + "epoch = sk.time(omm[0][\"EPOCH\"])\n", + "# crate a list of times .. once every 10 minutes\n", + "time_array = [epoch + sk.duration(minutes=i * 10) for i in range(6)]\n", + "\n", + "# TEME (inertial) output from SGP4\n", + "pTEME, _vTEME = sk.sgp4(omm[0], time_array)\n", + "\n", + "# Rotate to Earth-fixed\n", + "pITRF = [sk.frametransform.qteme2itrf(t) * p for t, p in zip(time_array, pTEME)]\n", + "\n", + "# Geodetic coordinates of space station at given times\n", + "coord = [sk.itrfcoord(x) for x in pITRF]" + ] }, { "cell_type": "markdown", - "id": "cb018d7d", + "id": "3", "metadata": {}, - "source": "## Example 2: XML Format\n\nSame as above, but loading the OMM in XML format. The XML structure has more levels of nesting \u2014 the orbital data is buried under `ndm > omm > body > segment > data`. The `xmltodict` library converts this to a nested dictionary that satkit can propagate. The ISS ground track is plotted over approximately one orbit." + "source": [ + "## Example 2: XML Format\n", + "\n", + "Same as above, but loading the OMM in XML format. The XML structure has more levels of nesting — the orbital data is buried under `ndm > omm > body > segment > data`. The `xmltodict` library converts this to a nested dictionary that satkit can propagate. The ISS ground track is plotted over approximately one orbit." + ] }, { "cell_type": "code", "execution_count": null, - "id": "8576cfcc", + "id": "4", "metadata": {}, "outputs": [], "source": [ - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "s", - "a", - "t", - "k", - "i", - "t", - " ", - "a", - "s", - " ", - "s", - "k", - "\n", - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "x", - "m", - "l", - "t", - "o", - "d", - "i", - "c", - "t", - "\n", - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "r", - "e", - "q", - "u", - "e", - "s", - "t", - "s", - "\n", - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "p", - "l", - "o", - "t", - "l", - "y", - ".", - "g", - "r", - "a", - "p", - "h", - "_", - "o", - "b", - "j", - "e", - "c", - "t", - "s", - " ", - "a", - "s", - " ", - "g", - "o", - "\n", - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "n", - "u", - "m", - "p", - "y", - " ", - "a", - "s", - " ", - "n", - "p", - "\n", - "\n", - "#", - " ", - "Q", - "u", - "e", - "r", - "y", - " ", - "t", - "h", - "e", - " ", - "c", - "u", - "r", - "r", - "e", - "n", - "t", - " ", - "e", - "p", - "h", - "e", - "m", - "e", - "r", - "i", - "s", - " ", - "f", - "o", - "r", - " ", - "t", - "h", - "e", - " ", - "I", - "n", - "t", - "e", - "r", - "n", - "a", - "t", - "i", - "o", - "n", - "a", - "l", - " ", - "S", - "p", - "a", - "c", - "e", - " ", - "S", - "t", - "a", - "t", - "i", - "o", - "n", - " ", - "(", - "I", - "S", - "S", - ")", - "\n", - "u", - "r", - "l", - " ", - "=", - " ", - "'", - "h", - "t", - "t", - "p", - "s", - ":", - "/", - "/", - "c", - "e", - "l", - "e", - "s", - "t", - "r", - "a", - "k", - ".", - "o", - "r", - "g", - "/", - "N", - "O", - "R", - "A", - "D", - "/", - "e", - "l", - "e", - "m", - "e", - "n", - "t", - "s", - "/", - "g", - "p", - ".", - "p", - "h", - "p", - "?", - "C", - "A", - "T", - "N", - "R", - "=", - "2", - "5", - "5", - "4", - "4", - "&", - "F", - "O", - "R", - "M", - "A", - "T", - "=", - "x", - "m", - "l", - "'", - "\n", - "t", - "r", - "y", - ":", - "\n", - " ", - " ", - " ", - " ", - "r", - "e", - "s", - "p", - "o", - "n", - "s", - "e", - " ", - "=", - " ", - "r", - "e", - "q", - "u", - "e", - "s", - "t", - "s", - ".", - "g", - "e", - "t", - "(", - "u", - "r", - "l", - ",", - " ", - "h", - "e", - "a", - "d", - "e", - "r", - "s", - "=", - "{", - "'", - "U", - "s", - "e", - "r", - "-", - "A", - "g", - "e", - "n", - "t", - "'", - ":", - " ", - "'", - "s", - "a", - "t", - "k", - "i", - "t", - "-", - "d", - "o", - "c", - "s", - "'", - "}", - ")", - "\n", - " ", - " ", - " ", - " ", - "r", - "e", - "s", - "p", - "o", - "n", - "s", - "e", - ".", - "r", - "a", - "i", - "s", - "e", - "_", - "f", - "o", - "r", - "_", - "s", - "t", - "a", - "t", - "u", - "s", - "(", - ")", - "\n", - " ", - " ", - " ", - " ", - "o", - "m", - "m", - " ", - "=", - " ", - "x", - "m", - "l", - "t", - "o", - "d", - "i", - "c", - "t", - ".", - "p", - "a", - "r", - "s", - "e", - "(", - "r", - "e", - "s", - "p", - "o", - "n", - "s", - "e", - ".", - "t", - "e", - "x", - "t", - ")", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "N", - "a", - "v", - "i", - "g", - "a", - "t", - "e", - " ", - "t", - "o", - " ", - "t", - "h", - "e", - " ", - "r", - "e", - "l", - "e", - "v", - "a", - "n", - "t", - " ", - "p", - "a", - "r", - "t", - " ", - "o", - "f", - " ", - "t", - "h", - "e", - " ", - "p", - "a", - "r", - "s", - "e", - "d", - " ", - "X", - "M", - "L", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "U", - "g", - "g", - "h", - "h", - ",", - " ", - "w", - "h", - "a", - "t", - " ", - "a", - " ", - "t", - "e", - "r", - "r", - "i", - "b", - "l", - "e", - " ", - "s", - "t", - "r", - "u", - "c", - "t", - "u", - "r", - "e", - "\n", - " ", - " ", - " ", - " ", - "o", - "m", - "m", - " ", - "=", - " ", - "o", - "m", - "m", - "[", - "'", - "n", - "d", - "m", - "'", - "]", - "[", - "'", - "o", - "m", - "m", - "'", - "]", - "[", - "'", - "b", - "o", - "d", - "y", - "'", - "]", - "[", - "'", - "s", - "e", - "g", - "m", - "e", - "n", - "t", - "'", - "]", - "[", - "'", - "d", - "a", - "t", - "a", - "'", - "]", - "\n", - "e", - "x", - "c", - "e", - "p", - "t", - " ", - "E", - "x", - "c", - "e", - "p", - "t", - "i", - "o", - "n", - ":", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "F", - "a", - "l", - "l", - "b", - "a", - "c", - "k", - " ", - "O", - "M", - "M", - " ", - "d", - "i", - "c", - "t", - " ", - "i", - "f", - " ", - "C", - "e", - "l", - "e", - "s", - "T", - "r", - "a", - "k", - " ", - "i", - "s", - " ", - "u", - "n", - "a", - "v", - "a", - "i", - "l", - "a", - "b", - "l", - "e", - "\n", - " ", - " ", - " ", - " ", - "o", - "m", - "m", - " ", - "=", - " ", - "{", - "\"", - "m", - "e", - "a", - "n", - "E", - "l", - "e", - "m", - "e", - "n", - "t", - "s", - "\"", - ":", - " ", - "{", - "\"", - "E", - "P", - "O", - "C", - "H", - "\"", - ":", - " ", - "\"", - "2", - "0", - "2", - "6", - "-", - "0", - "2", - "-", - "2", - "3", - "T", - "0", - "6", - ":", - "2", - "8", - ":", - "4", - "1", - ".", - "8", - "5", - "6", - "1", - "9", - "2", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "M", - "E", - "A", - "N", - "_", - "M", - "O", - "T", - "I", - "O", - "N", - "\"", - ":", - " ", - "\"", - "1", - "5", - ".", - "4", - "9", - "4", - "3", - "8", - "6", - "9", - "5", - "\"", - ",", - " ", - "\"", - "E", - "C", - "C", - "E", - "N", - "T", - "R", - "I", - "C", - "I", - "T", - "Y", - "\"", - ":", - " ", - "\"", - "0", - ".", - "0", - "0", - "0", - "5", - "9", - "9", - "1", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "I", - "N", - "C", - "L", - "I", - "N", - "A", - "T", - "I", - "O", - "N", - "\"", - ":", - " ", - "\"", - "5", - "1", - ".", - "6", - "3", - "7", - "2", - "\"", - ",", - " ", - "\"", - "R", - "A", - "_", - "O", - "F", - "_", - "A", - "S", - "C", - "_", - "N", - "O", - "D", - "E", - "\"", - ":", - " ", - "\"", - "2", - "9", - "0", - ".", - "9", - "7", - "5", - "3", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "A", - "R", - "G", - "_", - "O", - "F", - "_", - "P", - "E", - "R", - "I", - "C", - "E", - "N", - "T", - "E", - "R", - "\"", - ":", - " ", - "\"", - "3", - "4", - ".", - "6", - "6", - "9", - "4", - "\"", - ",", - " ", - "\"", - "M", - "E", - "A", - "N", - "_", - "A", - "N", - "O", - "M", - "A", - "L", - "Y", - "\"", - ":", - " ", - "\"", - "1", - "3", - "1", - ".", - "1", - "9", - "4", - "7", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "E", - "P", - "H", - "E", - "M", - "E", - "R", - "I", - "S", - "_", - "T", - "Y", - "P", - "E", - "\"", - ":", - " ", - "\"", - "0", - "\"", - ",", - " ", - "\"", - "N", - "O", - "R", - "A", - "D", - "_", - "C", - "A", - "T", - "_", - "I", - "D", - "\"", - ":", - " ", - "\"", - "2", - "5", - "5", - "4", - "4", - "\"", - ",", - " ", - "\"", - "E", - "L", - "E", - "M", - "E", - "N", - "T", - "_", - "S", - "E", - "T", - "_", - "N", - "O", - "\"", - ":", - " ", - "\"", - "9", - "9", - "9", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "R", - "E", - "V", - "_", - "A", - "T", - "_", - "E", - "P", - "O", - "C", - "H", - "\"", - ":", - " ", - "\"", - "4", - "7", - "9", - "1", - "4", - "\"", - ",", - " ", - "\"", - "C", - "L", - "A", - "S", - "S", - "I", - "F", - "I", - "C", - "A", - "T", - "I", - "O", - "N", - "_", - "T", - "Y", - "P", - "E", - "\"", - ":", - " ", - "\"", - "U", - "\"", - "}", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "t", - "l", - "e", - "P", - "a", - "r", - "a", - "m", - "e", - "t", - "e", - "r", - "s", - "\"", - ":", - " ", - "{", - "\"", - "B", - "S", - "T", - "A", - "R", - "\"", - ":", - " ", - "\"", - "0", - ".", - "0", - "0", - "0", - "3", - "2", - "0", - "5", - "5", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "M", - "E", - "A", - "N", - "_", - "M", - "O", - "T", - "I", - "O", - "N", - "_", - "D", - "O", - "T", - "\"", - ":", - " ", - "\"", - "0", - ".", - "0", - "0", - "0", - "2", - "0", - "8", - "3", - "6", - "\"", - ",", - " ", - "\"", - "M", - "E", - "A", - "N", - "_", - "M", - "O", - "T", - "I", - "O", - "N", - "_", - "D", - "D", - "O", - "T", - "\"", - ":", - " ", - "\"", - "0", - "\"", - "}", - "}", - "\n", - "\n", - "#", - " ", - "G", - "e", - "t", - " ", - "a", - " ", - "r", - "e", - "p", - "r", - "e", - "s", - "e", - "n", - "t", - "a", - "t", - "i", - "v", - "e", - " ", - "t", - "i", - "m", - "e", - " ", - "f", - "r", - "o", - "m", - " ", - "t", - "h", - "e", - " ", - "o", - "u", - "t", - "p", - "u", - "t", - "\n", - "e", - "p", - "o", - "c", - "h", - " ", - "=", - " ", - "s", - "k", - ".", - "t", - "i", - "m", - "e", - "(", - "o", - "m", - "m", - "[", - "'", - "m", - "e", - "a", - "n", - "E", - "l", - "e", - "m", - "e", - "n", - "t", - "s", - "'", - "]", - "[", - "'", - "E", - "P", - "O", - "C", - "H", - "'", - "]", - ")", - "\n", - "#", - " ", - "c", - "r", - "a", - "t", - "e", - " ", - "a", - " ", - "l", - "i", - "s", - "t", - " ", - "o", - "f", - " ", - "t", - "i", - "m", - "e", - "s", - " ", - ".", - ".", - " ", - "o", - "n", - "c", - "e", - " ", - "e", - "v", - "e", - "r", - "y", - " ", - "1", - "0", - " ", - "m", - "i", - "n", - "u", - "t", - "e", - "s", - "\n", - "t", - "i", - "m", - "e", - "_", - "a", - "r", - "r", - "a", - "y", - " ", - "=", - " ", - "[", - "e", - "p", - "o", - "c", - "h", - " ", - "+", - " ", - "s", - "k", - ".", - "d", - "u", - "r", - "a", - "t", - "i", - "o", - "n", - "(", - "m", - "i", - "n", - "u", - "t", - "e", - "s", - "=", - "i", - ")", - " ", - "f", - "o", - "r", - " ", - "i", - " ", - "i", - "n", - " ", - "r", - "a", - "n", - "g", - "e", - "(", - "9", - "7", - ")", - "]", - "\n", - "\n", - "#", - " ", - "T", - "E", - "M", - "E", - " ", - "(", - "i", - "n", - "e", - "r", - "t", - "i", - "a", - "l", - ")", - " ", - "o", - "u", - "t", - "p", - "u", - "t", - " ", - "f", - "r", - "o", - "m", - " ", - "S", - "G", - "P", - "4", - "\n", - "p", - "T", - "E", - "M", - "E", - ",", - " ", - "_", - "v", - "T", - "E", - "M", - "E", - " ", - "=", - " ", - "s", - "k", - ".", - "s", - "g", - "p", - "4", - "(", - "o", - "m", - "m", - ",", - " ", - "t", - "i", - "m", - "e", - "_", - "a", - "r", - "r", - "a", - "y", - ")", - "\n", - "\n", - "#", - " ", - "R", - "o", - "t", - "a", - "t", - "e", - " ", - "t", - "o", - " ", - "E", - "a", - "r", - "t", - "h", - "-", - "f", - "i", - "x", - "e", - "d", - "\n", - "p", - "I", - "T", - "R", - "F", - " ", - "=", - " ", - "[", - "s", - "k", - ".", - "f", - "r", - "a", - "m", - "e", - "t", - "r", - "a", - "n", - "s", - "f", - "o", - "r", - "m", - ".", - "q", - "t", - "e", - "m", - "e", - "2", - "i", - "t", - "r", - "f", - "(", - "t", - ")", - " ", - "*", - " ", - "p", - " ", - "f", - "o", - "r", - " ", - "t", - ",", - " ", - "p", - " ", - "i", - "n", - " ", - "z", - "i", - "p", - "(", - "t", - "i", - "m", - "e", - "_", - "a", - "r", - "r", - "a", - "y", - ",", - " ", - "p", - "T", - "E", - "M", - "E", - ")", - "]", - "\n", - "\n", - "c", - "o", - "o", - "r", - "d", - " ", - "=", - " ", - "[", - "s", - "k", - ".", - "i", - "t", - "r", - "f", - "c", - "o", - "o", - "r", - "d", - "(", - "x", - ")", - " ", - "f", - "o", - "r", - " ", - "x", - " ", - "i", - "n", - " ", - "p", - "I", - "T", - "R", - "F", - "]", - "\n", - "\n", - "l", - "a", - "t", - ",", - " ", - "l", - "o", - "n", - ",", - " ", - "a", - "l", - "t", - " ", - "=", - " ", - "z", - "i", - "p", - "(", - "*", - "[", - "(", - "c", - ".", - "l", - "a", - "t", - "i", - "t", - "u", - "d", - "e", - "_", - "d", - "e", - "g", - ",", - " ", - "c", - ".", - "l", - "o", - "n", - "g", - "i", - "t", - "u", - "d", - "e", - "_", - "d", - "e", - "g", - ",", - " ", - "c", - ".", - "a", - "l", - "t", - "i", - "t", - "u", - "d", - "e", - ")", - " ", - "f", - "o", - "r", - " ", - "c", - " ", - "i", - "n", - " ", - "c", - "o", - "o", - "r", - "d", - "]", - ")", - "\n", - "\n", - "f", - "i", - "g", - " ", - "=", - " ", - "g", - "o", - ".", - "F", - "i", - "g", - "u", - "r", - "e", - "(", - ")", - "\n", - "f", - "i", - "g", - ".", - "a", - "d", - "d", - "_", - "t", - "r", - "a", - "c", - "e", - "(", - "g", - "o", - ".", - "S", - "c", - "a", - "t", - "t", - "e", - "r", - "g", - "e", - "o", - "(", - "l", - "a", - "t", - "=", - "l", - "a", - "t", - ",", - " ", - "l", - "o", - "n", - "=", - "l", - "o", - "n", - ",", - " ", - "m", - "o", - "d", - "e", - "=", - "'", - "l", - "i", - "n", - "e", - "s", - "'", - ")", - ")", - "\n", - "f", - "i", - "g", - ".", - "u", - "p", - "d", - "a", - "t", - "e", - "_", - "l", - "a", - "y", - "o", - "u", - "t", - "(", - "m", - "a", - "r", - "g", - "i", - "n", - "=", - "{", - "\"", - "r", - "\"", - ":", - "0", - ",", - "\"", - "t", - "\"", - ":", - "4", - "0", - ",", - "\"", - "l", - "\"", - ":", - "0", - ",", - "\"", - "b", - "\"", - ":", - "0", - "}", - ",", - " ", - "t", - "i", - "t", - "l", - "e", - "=", - "'", - "I", - "S", - "S", - " ", - "G", - "r", - "o", - "u", - "n", - "d", - " ", - "T", - "r", - "a", - "c", - "k", - "'", - ",", - " ", - "g", - "e", - "o", - "=", - "d", - "i", - "c", - "t", - "(", - "s", - "h", - "o", - "w", - "l", - "a", - "n", - "d", - "=", - "T", - "r", - "u", - "e", - ",", - " ", - "s", - "h", - "o", - "w", - "c", - "o", - "u", - "n", - "t", - "r", - "i", - "e", - "s", - "=", - "T", - "r", - "u", - "e", - ")", - ")", - "\n", - "f", - "i", - "g", - ".", - "s", - "h", - "o", - "w", - "(", - ")", - "\n", - "f", - "i", - "g", - " ", - "=", - " ", - "g", - "o", - ".", - "F", - "i", - "g", - "u", - "r", - "e", - "(", - ")", - "\n", - "f", - "i", - "g", - ".", - "a", - "d", - "d", - "_", - "t", - "r", - "a", - "c", - "e", - "(", - "g", - "o", - ".", - "S", - "c", - "a", - "t", - "t", - "e", - "r", - "(", - "x", - "=", - "[", - "t", - ".", - "d", - "a", - "t", - "e", - "t", - "i", - "m", - "e", - "(", - ")", - " ", - "f", - "o", - "r", - " ", - "t", - " ", - "i", - "n", - " ", - "t", - "i", - "m", - "e", - "_", - "a", - "r", - "r", - "a", - "y", - "]", - ",", - " ", - "y", - "=", - "n", - "p", - ".", - "a", - "r", - "r", - "a", - "y", - "(", - "a", - "l", - "t", - ")", - "/", - "1", - "e", - "3", - ",", - " ", - "m", - "o", - "d", - "e", - "=", - "'", - "l", - "i", - "n", - "e", - "s", - "'", - ")", - ")", - "\n", - "f", - "i", - "g", - ".", - "u", - "p", - "d", - "a", - "t", - "e", - "_", - "l", - "a", - "y", - "o", - "u", - "t", - "(", - "y", - "a", - "x", - "i", - "s", - "_", - "t", - "i", - "t", - "l", - "e", - "=", - "'", - "A", - "l", - "t", - "i", - "t", - "u", - "d", - "e", - " ", - "(", - "k", - "m", - ")", - "'", - ",", - " ", - "x", - "a", - "x", - "i", - "s", - "_", - "t", - "i", - "t", - "l", - "e", - "=", - "'", - "T", - "i", - "m", - "e", - "'", - ",", - " ", - "f", - "o", - "n", - "t", - "=", - "d", - "i", - "c", - "t", - "(", - "s", - "i", - "z", - "e", - "=", - "1", - "4", - ")", - ",", - " ", - "t", - "i", - "t", - "l", - "e", - "=", - "'", - "I", - "S", - "S", - " ", - "A", - "l", - "t", - "i", - "t", - "u", - "d", - "e", - " ", - "v", - "s", - " ", - "T", - "i", - "m", - "e", - "'", - ")", - "\n", - "f", - "i", - "g", - ".", - "s", - "h", - "o", - "w", - "(", - ")" + "import ssl, certifi\n", + "ssl._create_default_https_context = lambda: ssl.create_default_context(cafile=certifi.where())\n", + "import satkit as sk\n", + "import xmltodict\n", + "import requests\n", + "import matplotlib.pyplot as plt\n", + "import scienceplots # noqa: F401\n", + "\n", + "plt.style.use([\"science\", \"no-latex\"])\n", + "plt.rcParams.update({\n", + " \"mathtext.fontset\": \"stix\",\n", + " \"font.family\": \"serif\",\n", + " \"font.serif\": [\"STIX Two Text\", \"STIXGeneral\", \"DejaVu Serif\"],\n", + " \"font.size\": 13,\n", + " \"axes.labelsize\": 14,\n", + " \"axes.titlesize\": 15,\n", + " \"xtick.labelsize\": 12,\n", + " \"ytick.labelsize\": 12,\n", + " \"legend.fontsize\": 12,\n", + " \"axes.formatter.use_mathtext\": True,\n", + " \"svg.fonttype\": \"none\",\n", + " \"axes.grid\": True,\n", + " \"grid.alpha\": 0.3,\n", + " \"grid.linestyle\": \"--\",\n", + " \"axes.prop_cycle\": plt.cycler(color=[\n", + " \"#0077BB\", \"#EE7733\", \"#009988\", \"#CC3311\",\n", + " \"#33BBEE\", \"#EE3377\", \"#BBBBBB\",\n", + " ]),\n", + "})\n", + "%config InlineBackend.figure_formats = ['svg']\n", + "import cartopy.crs as ccrs\n", + "import cartopy.feature as cfeature\n", + "import numpy as np\n", + "\n", + "# Query the current ephemeris for the International Space Station (ISS)\n", + "url = \"https://celestrak.org/NORAD/elements/gp.php?CATNR=25544&FORMAT=xml\"\n", + "try:\n", + " response = requests.get(url, headers={\"User-Agent\": \"satkit-docs\"})\n", + " response.raise_for_status()\n", + " omm = xmltodict.parse(response.text)\n", + " # Navigate to the relevant part of the parsed XML\n", + " # Ugghh, what a terrible structure\n", + " omm = omm[\"ndm\"][\"omm\"][\"body\"][\"segment\"][\"data\"]\n", + "except Exception:\n", + " # Fallback OMM dict if CelesTrak is unavailable\n", + " omm = {\n", + " \"meanElements\": {\n", + " \"EPOCH\": \"2026-02-23T06:28:41.856192\",\n", + " \"MEAN_MOTION\": \"15.49438695\",\n", + " \"ECCENTRICITY\": \"0.0005991\",\n", + " \"INCLINATION\": \"51.6372\",\n", + " \"RA_OF_ASC_NODE\": \"290.9753\",\n", + " \"ARG_OF_PERICENTER\": \"34.6694\",\n", + " \"MEAN_ANOMALY\": \"131.1947\",\n", + " \"EPHEMERIS_TYPE\": \"0\",\n", + " \"NORAD_CAT_ID\": \"25544\",\n", + " \"ELEMENT_SET_NO\": \"999\",\n", + " \"REV_AT_EPOCH\": \"47914\",\n", + " \"CLASSIFICATION_TYPE\": \"U\",\n", + " },\n", + " \"tleParameters\": {\n", + " \"BSTAR\": \"0.00032055\",\n", + " \"MEAN_MOTION_DOT\": \"0.00020836\",\n", + " \"MEAN_MOTION_DDOT\": \"0\",\n", + " },\n", + " }\n", + "\n", + "# Get a representative time from the output\n", + "epoch = sk.time(omm[\"meanElements\"][\"EPOCH\"])\n", + "# crate a list of times .. once every 10 minutes\n", + "time_array = [epoch + sk.duration(minutes=i) for i in range(97)]\n", + "\n", + "# TEME (inertial) output from SGP4\n", + "pTEME, _vTEME = sk.sgp4(omm, time_array)\n", + "\n", + "# Rotate to Earth-fixed\n", + "pITRF = [sk.frametransform.qteme2itrf(t) * p for t, p in zip(time_array, pTEME)]\n", + "\n", + "coord = [sk.itrfcoord(x) for x in pITRF]\n", + "\n", + "lat, lon, alt = zip(*[(c.latitude_deg, c.longitude_deg, c.altitude) for c in coord])\n", + "\n", + "# Break ground track at longitude discontinuities (date line crossings)\n", + "import numpy as np\n", + "lon_arr, lat_arr = np.array(lon), np.array(lat)\n", + "breaks = np.where(np.abs(np.diff(lon_arr)) > 180)[0] + 1\n", + "lon_segs = np.split(lon_arr, breaks)\n", + "lat_segs = np.split(lat_arr, breaks)\n", + "\n", + "fig, ax = plt.subplots(figsize=(10, 5), subplot_kw={\"projection\": ccrs.PlateCarree()})\n", + "ax.add_feature(cfeature.LAND, facecolor=\"lightgray\")\n", + "ax.add_feature(cfeature.BORDERS, linewidth=0.5)\n", + "ax.add_feature(cfeature.COASTLINE, linewidth=0.5)\n", + "for lo, la in zip(lon_segs, lat_segs):\n", + " ax.plot(lo, la, linewidth=1.5, color=\"C0\", transform=ccrs.PlateCarree())\n", + "ax.set_title(\"ISS Ground Track\")\n", + "ax.set_global()\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "fig, ax = plt.subplots(figsize=(10, 4))\n", + "ax.plot([t.datetime() for t in time_array], np.array(alt) / 1e3)\n", + "ax.set_xlabel(\"Time\")\n", + "ax.set_ylabel(\"Altitude (km)\")\n", + "ax.set_title(\"ISS Altitude vs Time\")\n", + "fig.autofmt_xdate()\n", + "plt.tight_layout()\n", + "plt.show()" ] } ], @@ -2200,4 +233,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/docs/tutorials/Planetary Ephemerides.ipynb b/docs/tutorials/Planetary Ephemerides.ipynb new file mode 100644 index 0000000..2329aa9 --- /dev/null +++ b/docs/tutorials/Planetary Ephemerides.ipynb @@ -0,0 +1,361 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Planetary Ephemerides\n", + "\n", + "This tutorial demonstrates satkit's planetary ephemeris capabilities, covering both high-precision JPL ephemerides and faster low-precision analytical models.\n", + "\n", + "## Overview\n", + "\n", + "Satkit provides two approaches for computing solar system body positions:\n", + "\n", + "- **`sk.jplephem`** — High-precision positions from NASA/JPL Development Ephemerides (DE440/DE441). These are interpolated from pre-computed Chebyshev polynomial coefficients and deliver sub-kilometer accuracy across centuries.\n", + "- **`sk.sun` / `sk.moon`** — Low-precision analytical models from Vallado, useful when speed matters more than accuracy. The Sun model is accurate to ~0.01 degrees (1950--2050), and the Moon model to ~0.3 degrees in ecliptic longitude and ~1275 km in range.\n", + "\n", + "All positions are returned in meters in the GCRF (Geocentric Celestial Reference Frame) unless otherwise noted." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import satkit as sk\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import scienceplots # noqa: F401\n", + "\n", + "plt.style.use([\"science\", \"no-latex\"])\n", + "plt.rcParams.update({\n", + " \"mathtext.fontset\": \"stix\",\n", + " \"font.family\": \"serif\",\n", + " \"font.serif\": [\"STIX Two Text\", \"STIXGeneral\", \"DejaVu Serif\"],\n", + " \"font.size\": 13,\n", + " \"axes.labelsize\": 14,\n", + " \"axes.titlesize\": 15,\n", + " \"xtick.labelsize\": 12,\n", + " \"ytick.labelsize\": 12,\n", + " \"legend.fontsize\": 12,\n", + " \"axes.formatter.use_mathtext\": True,\n", + " \"svg.fonttype\": \"none\",\n", + " \"axes.grid\": True,\n", + " \"grid.alpha\": 0.3,\n", + " \"grid.linestyle\": \"--\",\n", + " \"axes.prop_cycle\": plt.cycler(color=[\n", + " \"#0077BB\", \"#EE7733\", \"#009988\", \"#CC3311\",\n", + " \"#33BBEE\", \"#EE3377\", \"#BBBBBB\",\n", + " ]),\n", + "})\n", + "%config InlineBackend.figure_formats = ['svg']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Getting Planet Positions\n", + "\n", + "The `sk.jplephem` module provides geocentric and barycentric positions for all major solar system bodies. Bodies are specified using the `sk.solarsystem` enum.\n", + "\n", + "### Geocentric Positions\n", + "\n", + "`sk.jplephem.geocentric_pos` returns the position of a body relative to the Earth center, in the GCRF frame." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "t = sk.time(2026, 3, 20)\n", + "\n", + "# Sun position relative to Earth center\n", + "sun_pos = sk.jplephem.geocentric_pos(sk.solarsystem.Sun, t)\n", + "print(f\"Sun distance from Earth: {np.linalg.norm(sun_pos)/1e9:.3f} million km\")\n", + "print(f\"Sun position (GCRF): {sun_pos / 1e3} km\")\n", + "\n", + "# Moon position relative to Earth center\n", + "moon_pos = sk.jplephem.geocentric_pos(sk.solarsystem.Moon, t)\n", + "print(f\"\\nMoon distance from Earth: {np.linalg.norm(moon_pos)/1e3:.0f} km\")\n", + "print(f\"Moon position (GCRF): {moon_pos / 1e3} km\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Positions of other planets\n", + "planets = [\n", + " (\"Mercury\", sk.solarsystem.Mercury),\n", + " (\"Venus\", sk.solarsystem.Venus),\n", + " (\"Mars\", sk.solarsystem.Mars),\n", + " (\"Jupiter\", sk.solarsystem.Jupiter),\n", + " (\"Saturn\", sk.solarsystem.Saturn),\n", + "]\n", + "\n", + "for name, body in planets:\n", + " pos = sk.jplephem.geocentric_pos(body, t)\n", + " dist_au = np.linalg.norm(pos) / 1.496e11 # convert meters to AU\n", + " print(f\"{name:>8s}: {dist_au:.3f} AU from Earth\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Barycentric Positions\n", + "\n", + "`sk.jplephem.barycentric_pos` returns positions relative to the solar system barycenter. This is useful for interplanetary trajectory work or for understanding the geometry of the solar system.\n", + "\n", + "Note that the Sun's barycentric position is close to the origin but not exactly zero, since the barycenter shifts due to the gravitational influence of the giant planets." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Barycentric positions\n", + "sun_bary = sk.jplephem.barycentric_pos(sk.solarsystem.Sun, t)\n", + "earth_bary = sk.jplephem.barycentric_pos(sk.solarsystem.EMB, t)\n", + "jupiter_bary = sk.jplephem.barycentric_pos(sk.solarsystem.Jupiter, t)\n", + "\n", + "print(f\"Sun offset from barycenter: {np.linalg.norm(sun_bary)/1e3:.0f} km\")\n", + "print(f\"Earth-Moon barycenter: {np.linalg.norm(earth_bary)/1e11:.4f} x 10^8 km\")\n", + "print(f\"Jupiter: {np.linalg.norm(jupiter_bary)/1e11:.4f} x 10^8 km\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Vectorized Queries\n", + "\n", + "All `sk.jplephem` functions accept arrays of times, returning an Nx3 array of positions. This is much faster than calling the function in a Python loop." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create an array of times spanning one year\n", + "t0 = sk.time(2026, 1, 1)\n", + "times = np.array([t0 + sk.duration(days=d) for d in range(365)])\n", + "\n", + "# Get Mars geocentric position over the year (returns 365x3 array)\n", + "mars_pos = sk.jplephem.geocentric_pos(sk.solarsystem.Mars, times)\n", + "mars_dist = np.linalg.norm(mars_pos, axis=1) / 1.496e11 # AU\n", + "\n", + "print(f\"Mars position array shape: {mars_pos.shape}\")\n", + "print(f\"Mars distance range: {mars_dist.min():.2f} to {mars_dist.max():.2f} AU\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## State Vectors\n", + "\n", + "`sk.jplephem.geocentric_state` returns both position and velocity, which is essential for orbit determination and trajectory planning." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "t = sk.time(2026, 3, 20)\n", + "\n", + "# Moon state vector\n", + "pos, vel = sk.jplephem.geocentric_state(sk.solarsystem.Moon, t)\n", + "print(\"Moon state vector (GCRF):\")\n", + "print(f\" Position: [{pos[0]/1e3:.1f}, {pos[1]/1e3:.1f}, {pos[2]/1e3:.1f}] km\")\n", + "print(f\" Velocity: [{vel[0]:.3f}, {vel[1]:.3f}, {vel[2]:.3f}] m/s\")\n", + "print(f\" Speed: {np.linalg.norm(vel):.1f} m/s\")\n", + "\n", + "# Sun state vector\n", + "pos, vel = sk.jplephem.geocentric_state(sk.solarsystem.Sun, t)\n", + "print(f\"\\nSun state vector (GCRF):\")\n", + "print(f\" Position: [{pos[0]/1e9:.4f}, {pos[1]/1e9:.4f}, {pos[2]/1e9:.4f}] x 10^6 km\")\n", + "print(f\" Velocity: [{vel[0]/1e3:.3f}, {vel[1]/1e3:.3f}, {vel[2]/1e3:.3f}] km/s\")\n", + "print(f\" Speed: {np.linalg.norm(vel)/1e3:.2f} km/s\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Low-Precision Models\n", + "\n", + "For applications where speed matters more than accuracy (e.g., quick visibility checks, rough force models), satkit provides analytical low-precision models:\n", + "\n", + "- **`sk.sun.pos_gcrf`** — Sun position accurate to ~0.01 degrees (Vallado Algorithm 29)\n", + "- **`sk.moon.pos_gcrf`** — Moon position accurate to ~0.3 degrees in ecliptic longitude and ~1275 km in range (Vallado Algorithm 31)\n", + "\n", + "These do not require JPL ephemeris data files and are faster to compute." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "t = sk.time(2026, 3, 20)\n", + "\n", + "# Low-precision Sun position\n", + "sun_lp = sk.sun.pos_gcrf(t)\n", + "print(f\"Sun distance (low-precision): {np.linalg.norm(sun_lp)/1e9:.3f} million km\")\n", + "\n", + "# Low-precision Moon position\n", + "moon_lp = sk.moon.pos_gcrf(t)\n", + "print(f\"Moon distance (low-precision): {np.linalg.norm(moon_lp)/1e3:.0f} km\")\n", + "\n", + "# Moon phase information\n", + "illum = sk.moon.illumination(t)\n", + "phase = sk.moon.phase_name(t)\n", + "print(f\"\\nMoon illumination: {illum*100:.1f}%\")\n", + "print(f\"Moon phase: {phase}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Comparing JPL vs Low-Precision Models\n", + "\n", + "How much accuracy do you sacrifice by using the analytical models? Let's compare Sun and Moon positions from both methods over an entire year and plot the position error." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Generate times over one year at daily intervals\n", + "t0 = sk.time(2026, 1, 1)\n", + "times = np.array([t0 + sk.duration(days=d) for d in range(365)])\n", + "days = np.arange(365)\n", + "\n", + "# JPL high-precision positions\n", + "sun_jpl = sk.jplephem.geocentric_pos(sk.solarsystem.Sun, times)\n", + "moon_jpl = sk.jplephem.geocentric_pos(sk.solarsystem.Moon, times)\n", + "\n", + "# Low-precision positions\n", + "sun_lp = sk.sun.pos_gcrf(times)\n", + "moon_lp = sk.moon.pos_gcrf(times)\n", + "\n", + "# Compute position errors\n", + "sun_err = np.linalg.norm(sun_jpl - sun_lp, axis=1) / 1e3 # km\n", + "moon_err = np.linalg.norm(moon_jpl - moon_lp, axis=1) / 1e3 # km\n", + "\n", + "print(f\"Sun position error — mean: {sun_err.mean():.0f} km, max: {sun_err.max():.0f} km\")\n", + "print(f\"Moon position error — mean: {moon_err.mean():.0f} km, max: {moon_err.max():.0f} km\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 7), sharex=True)\n", + "\n", + "ax1.plot(days, sun_err)\n", + "ax1.set_ylabel(\"Position Error (km)\")\n", + "ax1.set_title(\"Sun: Low-Precision vs JPL DE440\")\n", + "ax1.grid(True, alpha=0.3)\n", + "\n", + "ax2.plot(days, moon_err)\n", + "ax2.set_xlabel(\"Day of Year 2026\")\n", + "ax2.set_ylabel(\"Position Error (km)\")\n", + "ax2.set_title(\"Moon: Low-Precision vs JPL DE440\")\n", + "ax2.grid(True, alpha=0.3)\n", + "\n", + "fig.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Practical Example: Moon Distance Over a Month\n", + "\n", + "The Moon's orbit is noticeably elliptical, with perigee (closest approach) around 363,000 km and apogee (farthest point) around 405,000 km. Let's compute and visualize the Moon's distance over a month to identify perigee and apogee passages." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Compute Moon distance at hourly intervals over one month\n", + "t0 = sk.time(2026, 3, 1)\n", + "hours = np.arange(0, 30 * 24) # 30 days in hours\n", + "times = np.array([t0 + sk.duration(hours=float(h)) for h in hours])\n", + "\n", + "moon_pos = sk.jplephem.geocentric_pos(sk.solarsystem.Moon, times)\n", + "moon_dist = np.linalg.norm(moon_pos, axis=1) / 1e3 # km\n", + "\n", + "# Find perigee and apogee\n", + "perigee_idx = np.argmin(moon_dist)\n", + "apogee_idx = np.argmax(moon_dist)\n", + "\n", + "print(f\"Perigee: {moon_dist[perigee_idx]:.0f} km on {times[perigee_idx].datetime()}\")\n", + "print(f\"Apogee: {moon_dist[apogee_idx]:.0f} km on {times[apogee_idx].datetime()}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "days_elapsed = hours / 24.0\n", + "\n", + "fig, ax = plt.subplots(figsize=(10, 5))\n", + "ax.plot(days_elapsed, moon_dist / 1e3, linewidth=1.5)\n", + "ax.plot(days_elapsed[perigee_idx], moon_dist[perigee_idx] / 1e3, 'v',\n", + " markersize=10, color='green', label=f'Perigee: {moon_dist[perigee_idx]:.0f} km')\n", + "ax.plot(days_elapsed[apogee_idx], moon_dist[apogee_idx] / 1e3, '^',\n", + " markersize=10, color='red', label=f'Apogee: {moon_dist[apogee_idx]:.0f} km')\n", + "\n", + "ax.set_xlabel(\"Days since March 1, 2026\")\n", + "ax.set_ylabel(\"Distance (x 1000 km)\")\n", + "ax.set_title(\"Earth-Moon Distance — March 2026\")\n", + "ax.legend()\n", + "ax.grid(True, alpha=0.3)\n", + "\n", + "fig.tight_layout()\n", + "plt.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/tutorials/Plots.ipynb b/docs/tutorials/Plots.ipynb index 144daeb..af28cc6 100644 --- a/docs/tutorials/Plots.ipynb +++ b/docs/tutorials/Plots.ipynb @@ -3,12 +3,22 @@ { "cell_type": "markdown", "metadata": {}, - "source": "# Interesting Plots\n\nA collection of visualizations demonstrating satkit's atmospheric density models, gravity field, and coordinate frame transformations." + "source": [ + "# Interesting Plots\n", + "\n", + "A collection of visualizations demonstrating satkit's atmospheric density models, gravity field, and coordinate frame transformations." + ] }, { "cell_type": "markdown", "metadata": {}, - "source": "## Air Density as a Function of the Solar Cycle\n\nAtmospheric density at orbital altitudes varies by more than an order of magnitude over the ~11-year solar cycle. During solar maximum, increased UV and EUV radiation heats the upper atmosphere, causing it to expand and increasing drag on satellites. This effect dominates orbit lifetime predictions for low-Earth orbit spacecraft.\n\nThe plot below uses the NRLMSISE-00 density model at 400 km and 500 km altitude, showing density variations from 1995 to 2022 (spanning roughly two solar cycles)." + "source": [ + "## Air Density as a Function of the Solar Cycle\n", + "\n", + "Atmospheric density at orbital altitudes varies by more than an order of magnitude over the ~11-year solar cycle. During solar maximum, increased UV and EUV radiation heats the upper atmosphere, causing it to expand and increasing drag on satellites. This effect dominates orbit lifetime predictions for low-Earth orbit spacecraft.\n", + "\n", + "The plot below uses the NRLMSISE-00 density model at 400 km and 500 km altitude, showing density variations from 1995 to 2022 (spanning roughly two solar cycles)." + ] }, { "cell_type": "code", @@ -17,7 +27,31 @@ "outputs": [], "source": [ "import satkit as sk\n", - "import plotly.graph_objects as go\n", + "import matplotlib.pyplot as plt\n", + "import scienceplots # noqa: F401\n", + "\n", + "plt.style.use([\"science\", \"no-latex\"])\n", + "plt.rcParams.update({\n", + " \"mathtext.fontset\": \"stix\",\n", + " \"font.family\": \"serif\",\n", + " \"font.serif\": [\"STIX Two Text\", \"STIXGeneral\", \"DejaVu Serif\"],\n", + " \"font.size\": 13,\n", + " \"axes.labelsize\": 14,\n", + " \"axes.titlesize\": 15,\n", + " \"xtick.labelsize\": 12,\n", + " \"ytick.labelsize\": 12,\n", + " \"legend.fontsize\": 12,\n", + " \"axes.formatter.use_mathtext\": True,\n", + " \"svg.fonttype\": \"none\",\n", + " \"axes.grid\": True,\n", + " \"grid.alpha\": 0.3,\n", + " \"grid.linestyle\": \"--\",\n", + " \"axes.prop_cycle\": plt.cycler(color=[\n", + " \"#0077BB\", \"#EE7733\", \"#009988\", \"#CC3311\",\n", + " \"#33BBEE\", \"#EE3377\", \"#BBBBBB\",\n", + " ]),\n", + "})\n", + "%config InlineBackend.figure_formats = ['svg']\n", "import numpy as np\n", "import math as m\n", "\n", @@ -33,38 +67,34 @@ "rho_500, emperature = zip(*np.array([sk.density.nrlmsise(altitude, 0, 0, x) for x in timearray]))\n", "\n", "\n", - "fig = go.Figure()\n", - "fig.add_trace(go.Scatter(x=[t.as_datetime() for t in timearray], y=rho_400,\n", - " mode='lines', name='Altitude = 400km', line=dict(color='black', width=1)))\n", - "fig.add_trace(go.Scatter(x=[t.as_datetime() for t in timearray], y=rho_500,\n", - " mode='lines', name='Altitude = 500km', line=dict(color='black', width=1, dash='dash')))\n", - "fig.update_layout(legend=dict(\n", - " yanchor=\"top\",\n", - " y=0.99,\n", - " xanchor=\"left\",\n", - " x=0.5\n", - "))\n", - "fig.update_yaxes(type=\"log\", title_font=dict(size=8), tickfont=dict(size=8), title='Density [kg/m3]')\n", - "fig.update_xaxes(title='Year')\n", - "fig.update_layout(yaxis_tickformat=\".2e\", width=650, height=512,\n", - " title='Air Density Changes with Solar Cycle',)\n", - "fig.update_xaxes(showline=True, linewidth=2, linecolor='black', mirror=True)\n", - "fig.update_yaxes(showline=True, linewidth=2, linecolor='black', mirror=True)\n", - "fig.update_layout(xaxis=dict(\n", - " gridcolor='#dddddd',\n", - " gridwidth=1,\n", - " ),\n", - " yaxis=dict(\n", - " gridcolor='#dddddd',\n", - " gridwidth=1,\n", - " ),)\n", - "fig.show()" + "\n", + "fig, ax = plt.subplots(figsize=(8, 5))\n", + "dates = [t.as_datetime() for t in timearray]\n", + "ax.semilogy(dates, rho_400, \"k-\", linewidth=1, label=\"Altitude = 400km\")\n", + "ax.semilogy(dates, rho_500, \"k--\", linewidth=1, label=\"Altitude = 500km\")\n", + "ax.set_xlabel(\"Year\")\n", + "ax.set_ylabel(\"Density [kg/m$^3$]\")\n", + "ax.set_title(\"Air Density Changes with Solar Cycle\")\n", + "ax.legend()\n", + "fig.autofmt_xdate()\n", + "plt.tight_layout()\n", + "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, - "source": "## Forces Acting on a Satellite vs. Altitude\n\nThis plot (inspired by Montenbruck & Gill, *Satellite Orbits*) shows the magnitude of various perturbation accelerations as a function of distance from the Earth's center. Key observations:\n\n- **Gravity** dominates at all altitudes\n- **J2** (Earth's oblateness) is the largest perturbation, ~1000x stronger than higher-order terms\n- **Atmospheric drag** is significant below ~800 km and varies with the solar cycle (max vs. min curves)\n- **Solar radiation pressure** is roughly constant with altitude\n- **Third-body** effects (Sun, Moon) grow with altitude as the satellite moves further from the Earth" + "source": [ + "## Forces Acting on a Satellite vs. Altitude\n", + "\n", + "This plot (inspired by Montenbruck & Gill, *Satellite Orbits*) shows the magnitude of various perturbation accelerations as a function of distance from the Earth's center. Key observations:\n", + "\n", + "- **Gravity** dominates at all altitudes\n", + "- **J2** (Earth's oblateness) is the largest perturbation, ~1000x stronger than higher-order terms\n", + "- **Atmospheric drag** is significant below ~800 km and varies with the solar cycle (max vs. min curves)\n", + "- **Solar radiation pressure** is roughly constant with altitude\n", + "- **Third-body** effects (Sun, Moon) grow with altitude as the satellite moves further from the Earth" + ] }, { "cell_type": "code", @@ -76,7 +106,31 @@ "import numpy as np\n", "import satkit as sk\n", "import math as m\n", - "import plotly.graph_objects as go\n", + "import matplotlib.pyplot as plt\n", + "import scienceplots # noqa: F401\n", + "\n", + "plt.style.use([\"science\", \"no-latex\"])\n", + "plt.rcParams.update({\n", + " \"mathtext.fontset\": \"stix\",\n", + " \"font.family\": \"serif\",\n", + " \"font.serif\": [\"STIX Two Text\", \"STIXGeneral\", \"DejaVu Serif\"],\n", + " \"font.size\": 13,\n", + " \"axes.labelsize\": 14,\n", + " \"axes.titlesize\": 15,\n", + " \"xtick.labelsize\": 12,\n", + " \"ytick.labelsize\": 12,\n", + " \"legend.fontsize\": 12,\n", + " \"axes.formatter.use_mathtext\": True,\n", + " \"svg.fonttype\": \"none\",\n", + " \"axes.grid\": True,\n", + " \"grid.alpha\": 0.3,\n", + " \"grid.linestyle\": \"--\",\n", + " \"axes.prop_cycle\": plt.cycler(color=[\n", + " \"#0077BB\", \"#EE7733\", \"#009988\", \"#CC3311\",\n", + " \"#33BBEE\", \"#EE3377\", \"#BBBBBB\",\n", + " ]),\n", + "})\n", + "%config InlineBackend.figure_formats = ['svg']\n", "\n", "\n", "N = 1000\n", @@ -110,60 +164,39 @@ "\n", "a_radiation = 4.56e-6 * 0.5 * Cr * aoverm * np.ones(range_arr.shape)\n", "\n", - "def add_line(fig, x, y, text, frac=0.5, ax=-20, ay=-30):\n", - " fig.add_scatter(x=x, y=y, mode='lines', name=text, line=dict(width=2, color='black'))\n", + "\n", + "fig, ax = plt.subplots(figsize=(8, 8))\n", + "def add_line(ax, x, y, text, frac=0.5, dx=-20, dy=-20):\n", + " ax.loglog(x, y, \"k-\", linewidth=1.5)\n", " idx = int(len(x)*frac)\n", - " fig.add_annotation(\n", - " x=np.log10(x[idx]),\n", - " y=np.log10(y[idx]),\n", - " text=text,\n", - " showarrow=True,\n", - " font=dict(\n", - " size=12,\n", - " color='black',\n", - " ),\n", - " align=\"center\",\n", - " ax=ax,\n", - " ay=ay,\n", - " arrowcolor=\"#636363\",\n", - " arrowsize=1,\n", - " arrowwidth=2,\n", - " arrowhead=6,\n", - " borderwidth=2,\n", - " borderpad=4,\n", - " opacity=0.8\n", - " )\n", - "\n", - "fig = go.Figure()\n", - "add_line(fig, range_arr/1e3, grav1/1e3, 'Gravity')\n", - "add_line(fig, range_arr/1e3, grav2/1e3, 'J2', 0.2, 0, -10)\n", - "add_line(fig, range_arr/1e3, grav6/1e3, 'J6', 0.8, 0, -10)\n", - "add_line(fig, range_arr[didx]/1e3, drag_max/1e3, 'Drag Max', 0.7, 30, 0)\n", - "add_line(fig, range_arr[didx]/1e3, drag_min/1e3, 'Drag Min', 0.8, 10, 30)\n", - "add_line(fig, range_arr/1e3, moon/1e3, 'Moon', 0.8, -10, -10)\n", - "add_line(fig, range_arr/1e3, sun/1e3, 'Sun', 0.7, -10, 10)\n", - "add_line(fig, range_arr/1e3, a_radiation/1e3, 'Radiation\\nPressure', 0.3, -10, 10)\n", - "\n", - "fig.update_xaxes(type=\"log\", title='Distance from Earth Origin [km]', range=np.log10([6378.1, 50e3]))\n", - "fig.update_yaxes(type=\"log\", title='Acceleration [km/s2]', tickformat=\".1e\")\n", - "fig.update_layout(title='Satellite Forces vs Altitude', width=650, height=650)\n", - "fig.update_xaxes(showline=True, linewidth=2, linecolor='black', mirror=True)\n", - "fig.update_yaxes(showline=True, linewidth=2, linecolor='black', mirror=True)\n", - "fig.update_layout(showlegend=False, xaxis=dict(\n", - " gridcolor='#dddddd',\n", - " gridwidth=1,\n", - " ),\n", - " yaxis=dict(\n", - " gridcolor='#dddddd',\n", - " gridwidth=1,\n", - " ),)\n", - "fig.show()" + " ax.annotate(text, xy=(x[idx], y[idx]), fontsize=9,\n", + " xytext=(dx, dy), textcoords=\"offset points\",\n", + " arrowprops=dict(arrowstyle=\"->\", color=\"gray\"))\n", + "\n", + "add_line(ax, range_arr/1e3, grav1/1e3, \"Gravity\")\n", + "add_line(ax, range_arr/1e3, grav2/1e3, \"J2\", 0.2, 0, -15)\n", + "add_line(ax, range_arr/1e3, grav6/1e3, \"J6\", 0.8, 0, -15)\n", + "add_line(ax, range_arr[didx]/1e3, drag_max/1e3, \"Drag Max\", 0.7, 30, 0)\n", + "add_line(ax, range_arr[didx]/1e3, drag_min/1e3, \"Drag Min\", 0.8, 10, 30)\n", + "add_line(ax, range_arr/1e3, moon/1e3, \"Moon\", 0.8, -10, -15)\n", + "add_line(ax, range_arr/1e3, sun/1e3, \"Sun\", 0.7, -10, 15)\n", + "add_line(ax, range_arr/1e3, a_radiation/1e3, \"Radiation\\nPressure\", 0.3, -10, 15)\n", + "ax.set_xlabel(\"Distance from Earth Origin [km]\")\n", + "ax.set_ylabel(\"Acceleration [km/s$^2$]\")\n", + "ax.set_title(\"Satellite Forces vs Altitude\")\n", + "ax.set_xlim(6378.1, 50e3)\n", + "plt.tight_layout()\n", + "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, - "source": "## Difference in Angle Between IAU-2010 Reduction and Approximate Calculation\n\n`satkit` provides both the full IAU-2010 precession/nutation model (`qgcrf2itrf`) and a faster approximate version (`qgcrf2itrf_approx`). This plot shows the angular difference between the two over several decades. The error remains below a few arcseconds, making the approximate model suitable for many applications where speed matters more than sub-arcsecond accuracy." + "source": [ + "## Difference in Angle Between IAU-2010 Reduction and Approximate Calculation\n", + "\n", + "`satkit` provides both the full IAU-2010 precession/nutation model (`qgcrf2itrf`) and a faster approximate version (`qgcrf2itrf_approx`). This plot shows the angular difference between the two over several decades. The error remains below a few arcseconds, making the approximate model suitable for many applications where speed matters more than sub-arcsecond accuracy." + ] }, { "cell_type": "code", @@ -171,7 +204,31 @@ "metadata": {}, "outputs": [], "source": [ - "import plotly.graph_objects as go\n", + "import matplotlib.pyplot as plt\n", + "import scienceplots # noqa: F401\n", + "\n", + "plt.style.use([\"science\", \"no-latex\"])\n", + "plt.rcParams.update({\n", + " \"mathtext.fontset\": \"stix\",\n", + " \"font.family\": \"serif\",\n", + " \"font.serif\": [\"STIX Two Text\", \"STIXGeneral\", \"DejaVu Serif\"],\n", + " \"font.size\": 13,\n", + " \"axes.labelsize\": 14,\n", + " \"axes.titlesize\": 15,\n", + " \"xtick.labelsize\": 12,\n", + " \"ytick.labelsize\": 12,\n", + " \"legend.fontsize\": 12,\n", + " \"axes.formatter.use_mathtext\": True,\n", + " \"svg.fonttype\": \"none\",\n", + " \"axes.grid\": True,\n", + " \"grid.alpha\": 0.3,\n", + " \"grid.linestyle\": \"--\",\n", + " \"axes.prop_cycle\": plt.cycler(color=[\n", + " \"#0077BB\", \"#EE7733\", \"#009988\", \"#CC3311\",\n", + " \"#33BBEE\", \"#EE3377\", \"#BBBBBB\",\n", + " ]),\n", + "})\n", + "%config InlineBackend.figure_formats = ['svg']\n", "import numpy as np\n", "import satkit as sk\n", "import math as m\n", @@ -183,21 +240,25 @@ "qexact = sk.frametransform.qgcrf2itrf(timearray)\n", "qapprox = sk.frametransform.qgcrf2itrf_approx(timearray)\n", "qdiff = np.array([q1 * q2.conj for q1, q2 in zip(qexact, qapprox)]) # type: ignore\n", - "theta = np.array([q.angle for q in qdiff])\n", - "fig = go.Figure()\n", - "fig.add_trace(go.Scatter(x=[t.as_datetime() for t in timearray], y=theta*180.0/m.pi*3600,\n", - " mode='lines', name='Angle Difference',\n", - " line=dict(width=1, color='black')))\n", - "fig.update_layout(title='Angle Error of Approximate ITRF to GCRF Rotation', width=650, height=512,\n", - " font=dict(size=14))\n", - "fig.update_yaxes(title='Error [arcsec]')\n", - "fig.update_xaxes(title='Year')" + "theta = np.array([min(q.angle, 2*m.pi - q.angle) for q in qdiff])\n", + "fig, ax = plt.subplots(figsize=(8, 5))\n", + "ax.plot([t.as_datetime() for t in timearray], theta*180.0/m.pi*3600, \"k-\", linewidth=1)\n", + "ax.set_xlabel(\"Year\")\n", + "ax.set_ylabel(\"Error [arcsec]\")\n", + "ax.set_title(\"Angle Error of Approximate ITRF to GCRF Rotation\")\n", + "fig.autofmt_xdate()\n", + "plt.tight_layout()\n", + "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, - "source": "## Polar Motion\n\nThe Earth's rotational axis wobbles slightly relative to its crust, a phenomenon known as polar motion. The plot below shows the x and y components of polar motion (in arcseconds) over several decades. The dominant signal is the ~14-month Chandler wobble superimposed on an annual cycle, with a slow secular drift." + "source": [ + "## Polar Motion\n", + "\n", + "The Earth's rotational axis wobbles slightly relative to its crust, a phenomenon known as polar motion. The plot below shows the x and y components of polar motion (in arcseconds) over several decades. The dominant signal is the ~14-month Chandler wobble superimposed on an annual cycle, with a slow secular drift." + ] }, { "cell_type": "code", @@ -205,7 +266,33 @@ "metadata": {}, "outputs": [], "source": [ - "import plotly.graph_objects as go\n", + "import matplotlib.pyplot as plt\n", + "import scienceplots # noqa: F401\n", + "\n", + "plt.style.use([\"science\", \"no-latex\"])\n", + "plt.rcParams.update({\n", + " \"mathtext.fontset\": \"stix\",\n", + " \"font.family\": \"serif\",\n", + " \"font.serif\": [\"STIX Two Text\", \"STIXGeneral\", \"DejaVu Serif\"],\n", + " \"font.size\": 13,\n", + " \"axes.labelsize\": 14,\n", + " \"axes.titlesize\": 15,\n", + " \"xtick.labelsize\": 12,\n", + " \"ytick.labelsize\": 12,\n", + " \"legend.fontsize\": 12,\n", + " \"axes.formatter.use_mathtext\": True,\n", + " \"svg.fonttype\": \"none\",\n", + " \"axes.grid\": True,\n", + " \"grid.alpha\": 0.3,\n", + " \"grid.linestyle\": \"--\",\n", + " \"axes.prop_cycle\": plt.cycler(color=[\n", + " \"#0077BB\", \"#EE7733\", \"#009988\", \"#CC3311\",\n", + " \"#33BBEE\", \"#EE3377\", \"#BBBBBB\",\n", + " ]),\n", + "})\n", + "%config InlineBackend.figure_formats = ['svg']\n", + "from mpl_toolkits.mplot3d import Axes3D\n", + "import matplotlib.dates as mdates\n", "import numpy as np\n", "import satkit as sk\n", "import math as m\n", @@ -216,17 +303,26 @@ "timearray = np.array([start + sk.duration(days=x) for x in np.linspace(0, duration.days, 4000)])\n", "\n", "dut1, xp, yp, lod, dX, dY = zip(*[sk.frametransform.earth_orientation_params(t) for t in timearray])\n", - "fig = go.Figure()\n", - "fig.add_trace(go.Scatter3d(x=xp, y=yp, z=[t.as_datetime() for t in timearray], mode='lines', name='Polar Motion',))\n", - "fig.update_scenes(xaxis_title='X [arcsec]', yaxis_title='Y [arcsec]', zaxis_title='Year')\n", - "fig.update_layout(title='Polar Motion', width=650, height=600, font=dict(size=12))\n", - "fig.show()\n" + "fig = plt.figure(figsize=(8, 7))\n", + "ax = fig.add_subplot(111, projection=\"3d\")\n", + "dates_num = mdates.date2num([t.as_datetime() for t in timearray])\n", + "ax.plot(list(xp), list(yp), dates_num, linewidth=0.5)\n", + "ax.set_xlabel(\"X [arcsec]\")\n", + "ax.set_ylabel(\"Y [arcsec]\")\n", + "ax.set_zlabel(\"Year\")\n", + "ax.set_title(\"Polar Motion\")\n", + "plt.tight_layout()\n", + "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, - "source": "## Precession / Nutation\n\nPrecession and nutation describe the long-term and short-term wobble of the Earth's rotational axis in inertial space. This is captured by the rotation between the GCRS (Geocentric Celestial Reference System) and CIRS (Celestial Intermediate Reference System) frames. The plot shows the nutation components (with the linear precession trend removed from x) over a 20-year period; the dominant ~18.6-year and semi-annual periods are clearly visible." + "source": [ + "## Precession / Nutation\n", + "\n", + "Precession and nutation describe the long-term and short-term wobble of the Earth's rotational axis in inertial space. This is captured by the rotation between the GCRS (Geocentric Celestial Reference System) and CIRS (Celestial Intermediate Reference System) frames. The plot shows the nutation components (with the linear precession trend removed from x) over a 20-year period; the dominant ~18.6-year and semi-annual periods are clearly visible." + ] }, { "cell_type": "code", @@ -237,7 +333,33 @@ "import satkit as sk\n", "import numpy as np\n", "import math as m\n", - "import plotly.graph_objects as go\n", + "import matplotlib.pyplot as plt\n", + "import scienceplots # noqa: F401\n", + "\n", + "plt.style.use([\"science\", \"no-latex\"])\n", + "plt.rcParams.update({\n", + " \"mathtext.fontset\": \"stix\",\n", + " \"font.family\": \"serif\",\n", + " \"font.serif\": [\"STIX Two Text\", \"STIXGeneral\", \"DejaVu Serif\"],\n", + " \"font.size\": 13,\n", + " \"axes.labelsize\": 14,\n", + " \"axes.titlesize\": 15,\n", + " \"xtick.labelsize\": 12,\n", + " \"ytick.labelsize\": 12,\n", + " \"legend.fontsize\": 12,\n", + " \"axes.formatter.use_mathtext\": True,\n", + " \"svg.fonttype\": \"none\",\n", + " \"axes.grid\": True,\n", + " \"grid.alpha\": 0.3,\n", + " \"grid.linestyle\": \"--\",\n", + " \"axes.prop_cycle\": plt.cycler(color=[\n", + " \"#0077BB\", \"#EE7733\", \"#009988\", \"#CC3311\",\n", + " \"#33BBEE\", \"#EE3377\", \"#BBBBBB\",\n", + " ]),\n", + "})\n", + "%config InlineBackend.figure_formats = ['svg']\n", + "from mpl_toolkits.mplot3d import Axes3D\n", + "import matplotlib.dates as mdates\n", "\n", "# Precession / Nutation is the difference between the IAU-2006 GCRS frame and the CIRS frame\n", "start = sk.time(2000, 1, 1)\n", @@ -253,12 +375,16 @@ "rx0 =rots[:,0] - np.polyval(pv, pline)\n", "\n", "rad2asec = 180.0/m.pi * 3600\n", - "fig = go.Figure()\n", - "fig.add_trace(go.Scatter3d(x=rx0*rad2asec, y=rots[:,1]*rad2asec, z=[t.as_datetime() for t in timearray], mode='lines',\n", - " name='Precession/Nutation', line=dict(width=2, color='black')))\n", - "fig.update_layout(title='Precession / Nutation', width=650, height=600, font=dict(size=12))\n", - "\n", - "fig.show()" + "fig = plt.figure(figsize=(8, 7))\n", + "ax = fig.add_subplot(111, projection=\"3d\")\n", + "dates_num = mdates.date2num([t.as_datetime() for t in timearray])\n", + "ax.plot(rx0*rad2asec, rots[:,1]*rad2asec, dates_num, \"k-\", linewidth=0.5)\n", + "ax.set_xlabel(\"X [arcsec]\")\n", + "ax.set_ylabel(\"Y [arcsec]\")\n", + "ax.set_zlabel(\"Year\")\n", + "ax.set_title(\"Precession / Nutation\")\n", + "plt.tight_layout()\n", + "plt.show()" ] } ], @@ -283,4 +409,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} \ No newline at end of file +} diff --git a/docs/tutorials/Satellite Ground Contacts.ipynb b/docs/tutorials/Satellite Ground Contacts.ipynb index 2e78c55..4fa7f92 100644 --- a/docs/tutorials/Satellite Ground Contacts.ipynb +++ b/docs/tutorials/Satellite Ground Contacts.ipynb @@ -3,7 +3,15 @@ { "cell_type": "markdown", "metadata": {}, - "source": "# Satellite Ground Contacts\n\nThis tutorial computes communication contact intervals between a satellite and a set of ground stations over a single day.\n\nA satellite is \"in view\" of a ground station when it rises above a minimum elevation angle (here, 5 degrees). For each second of the day, we compute the satellite's position relative to each ground station in the North-East-Down (NED) frame, determine the elevation angle, and identify contiguous time intervals where the satellite is visible.\n\nThe example uses the Landsat-7 satellite and three ground stations: Svalbard, Alice Springs, and Sioux Falls." + "source": [ + "# Satellite Ground Contacts\n", + "\n", + "This tutorial computes communication contact intervals between a satellite and a set of ground stations over a single day.\n", + "\n", + "A satellite is \"in view\" of a ground station when it rises above a minimum elevation angle (here, 5 degrees). For each second of the day, we compute the satellite's position relative to each ground station in the North-East-Down (NED) frame, determine the elevation angle, and identify contiguous time intervals where the satellite is visible.\n", + "\n", + "The example uses the Landsat-7 satellite and three ground stations: Svalbard, Alice Springs, and Sioux Falls." + ] }, { "cell_type": "code", @@ -13,14 +21,38 @@ "source": [ "import satkit as sk\n", "import numpy as np\n", - "import plotly.express as px\n", + "import matplotlib.pyplot as plt\n", + "import scienceplots # noqa: F401\n", + "\n", + "plt.style.use([\"science\", \"no-latex\"])\n", + "plt.rcParams.update({\n", + " \"mathtext.fontset\": \"stix\",\n", + " \"font.family\": \"serif\",\n", + " \"font.serif\": [\"STIX Two Text\", \"STIXGeneral\", \"DejaVu Serif\"],\n", + " \"font.size\": 13,\n", + " \"axes.labelsize\": 14,\n", + " \"axes.titlesize\": 15,\n", + " \"xtick.labelsize\": 12,\n", + " \"ytick.labelsize\": 12,\n", + " \"legend.fontsize\": 12,\n", + " \"axes.formatter.use_mathtext\": True,\n", + " \"svg.fonttype\": \"none\",\n", + " \"axes.grid\": True,\n", + " \"grid.alpha\": 0.3,\n", + " \"grid.linestyle\": \"--\",\n", + " \"axes.prop_cycle\": plt.cycler(color=[\n", + " \"#0077BB\", \"#EE7733\", \"#009988\", \"#CC3311\",\n", + " \"#33BBEE\", \"#EE3377\", \"#BBBBBB\",\n", + " ]),\n", + "})\n", + "%config InlineBackend.figure_formats = ['svg']\n", "from datetime import datetime, timedelta\n", "\n", "# The TLE for landsat-7\n", "tle_lines = [\n", " \"0 LANDSAT-7\",\n", " \"1 25682U 99020A 24099.90566066 .00000551 00000-0 12253-3 0 9992\",\n", - " \"2 25682 97.8952 129.9471 0001421 108.5441 14.5268 14.60548156329087\"\n", + " \"2 25682 97.8952 129.9471 0001421 108.5441 14.5268 14.60548156329087\",\n", "]\n", "landsat7 = sk.TLE.from_lines(tle_lines)\n", "\n", @@ -38,35 +70,59 @@ "\n", "# Get ITRF coordinates (Earth-Fixed) by rotating the position in the TEME frame\n", "# to ITRF frame using the frametransform module\n", - "pITRF = np.array([q*x for q,x in zip(sk.frametransform.qteme2itrf(time_array), pTEME)])\n", + "pITRF = np.array(\n", + " [q * x for q, x in zip(sk.frametransform.qteme2itrf(time_array), pTEME)]\n", + ")\n", "\n", "# Setup some ground stations\n", "ground_stations = [\n", - " {'name': 'Svalbard', 'lat': 78.2232, 'lon': 15.6267, 'alt': 0},\n", - " {'name': 'Alice Springs', 'lat': -23.6980, 'lon': 133.8807, 'alt': 0},\n", - " {'name': 'Sioux Falls', 'lat': 43.5446, 'lon': -96.7311, 'alt': 0},\n", + " {\"name\": \"Svalbard\", \"lat\": 78.2232, \"lon\": 15.6267, \"alt\": 0},\n", + " {\"name\": \"Alice Springs\", \"lat\": -23.6980, \"lon\": 133.8807, \"alt\": 0},\n", + " {\"name\": \"Sioux Falls\", \"lat\": 43.5446, \"lon\": -96.7311, \"alt\": 0},\n", "]\n" ] }, { "cell_type": "markdown", - "source": "## Compute Satellite Positions\n\nPropagate the TLE with SGP4 to get TEME positions at every second of the day, then rotate to the ITRF (Earth-fixed) frame. Define the ground stations by their geodetic coordinates.", - "metadata": {} + "metadata": {}, + "source": [ + "## Compute Satellite Positions\n", + "\n", + "Propagate the TLE with SGP4 to get TEME positions at every second of the day, then rotate to the ITRF (Earth-fixed) frame. Define the ground stations by their geodetic coordinates." + ] }, { "cell_type": "markdown", - "source": "## Contact Detection Algorithm\n\nFor each ground station, the algorithm:\n\n1. Computes the satellite position in the station's **North-East-Down (NED)** frame by subtracting the station position and rotating with `qned2itrf`\n2. Computes **elevation** as the arcsine of the upward (-Down) component of the normalized NED vector\n3. Finds all times where elevation exceeds the minimum threshold\n4. Groups consecutive qualifying indices into individual contact intervals\n5. For each contact, records the start/end times, duration, range, elevation, and heading", - "metadata": {} + "metadata": {}, + "source": [ + "## Contact Detection Algorithm\n", + "\n", + "For each ground station, the algorithm:\n", + "\n", + "1. Computes the satellite position in the station's **North-East-Down (NED)** frame by subtracting the station position and rotating with `qned2itrf`\n", + "2. Computes **elevation** as the arcsine of the upward (-Down) component of the normalized NED vector\n", + "3. Finds all times where elevation exceeds the minimum threshold\n", + "4. Groups consecutive qualifying indices into individual contact intervals\n", + "5. For each contact, records the start/end times, duration, range, elevation, and heading" + ] }, { "cell_type": "markdown", - "source": "## Contact Summary\n\nDisplay all contacts for the day sorted by start time. Higher maximum elevation generally corresponds to better signal quality.", - "metadata": {} + "metadata": {}, + "source": [ + "## Contact Summary\n", + "\n", + "Display all contacts for the day sorted by start time. Higher maximum elevation generally corresponds to better signal quality." + ] }, { "cell_type": "markdown", - "source": "## Contact Detail\n\nPlot the range, elevation, and heading over time for an individual contact pass.", - "metadata": {} + "metadata": {}, + "source": [ + "## Contact Detail\n", + "\n", + "Plot the range, elevation, and heading over time for an individual contact pass." + ] }, { "cell_type": "code", @@ -80,7 +136,11 @@ " \"\"\"\n", "\n", " # Create an \"itrfcoord\" object for the ground station\n", - " coord = sk.itrfcoord(latitude_deg=ground_station['lat'], longitude_deg=ground_station['lon'], altitude_m=ground_station['alt'])\n", + " coord = sk.itrfcoord(\n", + " latitude_deg=ground_station[\"lat\"],\n", + " longitude_deg=ground_station[\"lon\"],\n", + " altitude_m=ground_station[\"alt\"],\n", + " )\n", "\n", " # Get the North-East-Down coordinates of the satellite relative to the ground station\n", " # at all times by taking the difference between the satellite position and the ground\n", @@ -92,14 +152,14 @@ "\n", " # Find the elevation from the ground station at all times\n", " # This is the arcsine of the \"up\" portion of the NED-hat vector\n", - " elevation_deg = np.degrees(np.arcsin(-pNED_hat[:,2]))\n", + " elevation_deg = np.degrees(np.arcsin(-pNED_hat[:, 2]))\n", "\n", " # We can see ground station when elevation is greater than min_elevation_deg\n", " inview_idx = np.argwhere(elevation_deg > min_elevation_deg).flatten().astype(int)\n", "\n", " # split indices into groups of consecutive indices\n", " # This indicates contiguous contacts\n", - " inview_idx = np.split(inview_idx, np.where(np.diff(inview_idx) != 1)[0]+1)\n", + " inview_idx = np.split(inview_idx, np.where(np.diff(inview_idx) != 1)[0] + 1)\n", "\n", " def get_single_contacts(inview_idx):\n", " for cidx in inview_idx:\n", @@ -107,7 +167,7 @@ "\n", " # the North-East-Down position of the satellite relative to\n", " # ground station over the single contact\n", - " cpNED = pNED[cidx,:]\n", + " cpNED = pNED[cidx, :]\n", "\n", " # Compute the range in meters\n", " range = np.linalg.norm(cpNED, axis=1)\n", @@ -116,22 +176,24 @@ " contact_elevation_deg = elevation_deg[cidx]\n", "\n", " # Heading clockwise from North is arctangent of east/north'\n", - " heading_deg = np.degrees(np.arctan2(cpNED[:,1], cpNED[:,0]))\n", + " heading_deg = np.degrees(np.arctan2(cpNED[:, 1], cpNED[:, 0]))\n", "\n", " # Yield a dictionary describing the results\n", " yield {\n", - " 'groundstation': ground_station['name'],\n", - " 'timearr': time_array[cidx],\n", - " 'range_km': range*1.0e-3,\n", - " 'elevation_deg': contact_elevation_deg,\n", - " 'heading_deg': heading_deg,\n", - " 'start': time_array[cidx[0]],\n", - " 'end': time_array[cidx[-1]],\n", - " 'max_elevation_deg': np.max(contact_elevation_deg),\n", - " 'duration': time_array[cidx[-1]] - time_array[cidx[0]]\n", + " \"groundstation\": ground_station[\"name\"],\n", + " \"timearr\": time_array[cidx],\n", + " \"range_km\": range * 1.0e-3,\n", + " \"elevation_deg\": contact_elevation_deg,\n", + " \"heading_deg\": heading_deg,\n", + " \"start\": time_array[cidx[0]],\n", + " \"end\": time_array[cidx[-1]],\n", + " \"max_elevation_deg\": np.max(contact_elevation_deg),\n", + " \"duration\": time_array[cidx[-1]] - time_array[cidx[0]],\n", " }\n", + "\n", " return list(get_single_contacts(inview_idx))\n", "\n", + "\n", "# Calculate all the contacts\n", "contacts = [calc_contacts(g, pITRF, time_array) for g in ground_stations]\n", "\n", @@ -150,21 +212,29 @@ "import pandas as pd\n", "\n", "data = pd.DataFrame(contacts)\n", - "data.sort_values(by='start', inplace=True)\n", + "data.sort_values(by=\"start\", inplace=True)\n", "data.reset_index(drop=True, inplace=True)\n", "# Get nicer column names for display\n", - "data.rename(columns={\"max_elevation_deg\": \"Max Elevation (deg)\",\n", - " \"duration\": \"Duration (s)\",\n", - " \"start\": \"Start (UTC)\",\n", - " \"end\": \"End (UTC)\",\n", - " \"groundstation\": \"Ground Station\"}, inplace=True)\n", - "data.style \\\n", - " .hide(subset=[\"timearr\", \"range_km\", \"elevation_deg\", \"heading_deg\"], axis=1) \\\n", - " .format({\"Max Elevation (deg)\": \"{:.1f}\",\n", - " \"Start (UTC)\": lambda x: x.strftime('%H:%M:%S'),\n", - " \"End (UTC)\": lambda x: x.strftime('%H:%M:%S'),\n", - " \"Duration (s)\": lambda x: x.seconds })\n", - "\n" + "data.rename(\n", + " columns={\n", + " \"max_elevation_deg\": \"Max Elevation (deg)\",\n", + " \"duration\": \"Duration (s)\",\n", + " \"start\": \"Start (UTC)\",\n", + " \"end\": \"End (UTC)\",\n", + " \"groundstation\": \"Ground Station\",\n", + " },\n", + " inplace=True,\n", + ")\n", + "data.style.hide(\n", + " subset=[\"timearr\", \"range_km\", \"elevation_deg\", \"heading_deg\"], axis=1\n", + ").format(\n", + " {\n", + " \"Max Elevation (deg)\": \"{:.1f}\",\n", + " \"Start (UTC)\": lambda x: x.strftime(\"%H:%M:%S\"),\n", + " \"End (UTC)\": lambda x: x.strftime(\"%H:%M:%S\"),\n", + " \"Duration (s)\": lambda x: x.seconds,\n", + " }\n", + ")\n" ] }, { @@ -175,21 +245,23 @@ "source": [ "# Plot one of the contacts\n", "contact = data.iloc[5]\n", - "import plotly.graph_objects as go\n", - "from plotly.subplots import make_subplots\n", - "\n", - "fig = make_subplots(rows=3, cols=1, shared_xaxes=True, vertical_spacing=0.1,\n", - " subplot_titles=('Range [km]', 'Elevation [deg]', 'Heading [deg]'))\n", - "fig.add_trace(go.Scatter(x=contact['timearr'], y=contact['range_km'], name='Range [km]'), row=1, col=1)\n", - "fig.add_trace(go.Scatter(x=contact['timearr'], y=contact['elevation_deg'], name='Elevation [deg]'), row=2, col=1)\n", - "fig.add_trace(go.Scatter(x=contact['timearr'], y=contact['heading_deg'], name='Heading [deg]'), row=3, col=1)\n", - "fig.update_layout(yaxis = {'title': 'Range (km)'},\n", - " yaxis2={'title': 'Elevation [deg]'},\n", - " yaxis3={'title': 'Heading [deg]'},\n", - " title=f'Landsat 7 to {contact[\"Ground Station\"]} on {contact[\"Start (UTC)\"]}',\n", - " width=800,\n", - " height=600\n", - " )" + "\n", + "fig, axes = plt.subplots(3, 1, figsize=(10, 7), sharex=True)\n", + "\n", + "axes[0].plot(contact[\"timearr\"], contact[\"range_km\"])\n", + "axes[0].set_ylabel(\"Range (km)\")\n", + "axes[0].set_title(f\"Landsat 7 to {contact['Ground Station']} on {contact['Start (UTC)']}\")\n", + "\n", + "axes[1].plot(contact[\"timearr\"], contact[\"elevation_deg\"])\n", + "axes[1].set_ylabel(\"Elevation (deg)\")\n", + "\n", + "axes[2].plot(contact[\"timearr\"], contact[\"heading_deg\"])\n", + "axes[2].set_ylabel(\"Heading (deg)\")\n", + "axes[2].set_xlabel(\"Time\")\n", + "\n", + "fig.autofmt_xdate()\n", + "plt.tight_layout()\n", + "plt.show()" ] } ], @@ -214,4 +286,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} \ No newline at end of file +} diff --git a/docs/tutorials/TLE Fitting.ipynb b/docs/tutorials/TLE Fitting.ipynb index 6f53c8f..42db398 100644 --- a/docs/tutorials/TLE Fitting.ipynb +++ b/docs/tutorials/TLE Fitting.ipynb @@ -2,14 +2,20 @@ "cells": [ { "cell_type": "markdown", - "id": "d4d9fdc4", + "id": "0", "metadata": {}, - "source": "# TLE Fitting\n\nTwo-Line Element sets (TLEs) are the standard format for distributing satellite orbital elements, but they degrade in accuracy over time. When higher-fidelity state vectors are available (e.g., from GPS or precision orbit determination), it is useful to fit a new TLE to those states.\n\n`satkit.TLE.fit_from_states` performs this fit using Levenberg-Marquardt non-linear least-squares optimization. It tunes the TLE orbital parameters to minimize the difference between the input state positions and the SGP4-predicted positions. Input states are assumed to be in the GCRF frame and are internally rotated to the TEME frame used by SGP4." + "source": [ + "# TLE Fitting\n", + "\n", + "Two-Line Element sets (TLEs) are the standard format for distributing satellite orbital elements, but they degrade in accuracy over time. When higher-fidelity state vectors are available (e.g., from GPS or precision orbit determination), it is useful to fit a new TLE to those states.\n", + "\n", + "`satkit.TLE.fit_from_states` performs this fit using Levenberg-Marquardt non-linear least-squares optimization. It tunes the TLE orbital parameters to minimize the difference between the input state positions and the SGP4-predicted positions. Input states are assumed to be in the GCRF frame and are internally rotated to the TEME frame used by SGP4." + ] }, { "cell_type": "code", "execution_count": null, - "id": "bd3977b3", + "id": "1", "metadata": {}, "outputs": [], "source": [ @@ -42,23 +48,27 @@ "states = [res.interp(t) for t in times]\n", "\n", "# Fit the TLE\n", - "(tle, fitresults) = sk.TLE.fit_from_states(states, times, time0 + sk.duration(days=0.5)) # type: ignore\n", + "(tle, fitresults) = sk.TLE.fit_from_states(states, times, time0 + sk.duration(days=0.5)) # type: ignore\n", "\n", "# Print the result\n", "print(tle)\n", - "print(fitresults['success'])" + "print(fitresults[\"success\"])" ] }, { "cell_type": "markdown", - "id": "u3nk52jqhq", - "source": "## Generate Test Data\n\nTo demonstrate the fitting, we create a synthetic truth trajectory by propagating a circular orbit at 450 km altitude with satkit's high-precision propagator. We then sample position and velocity states every 10 minutes over one day, and fit a TLE to those samples.", - "metadata": {} + "id": "2", + "metadata": {}, + "source": [ + "## Generate Test Data\n", + "\n", + "To demonstrate the fitting, we create a synthetic truth trajectory by propagating a circular orbit at 450 km altitude with satkit's high-precision propagator. We then sample position and velocity states every 10 minutes over one day, and fit a TLE to those samples." + ] }, { "cell_type": "code", "execution_count": null, - "id": "9b002aeb", + "id": "3", "metadata": {}, "outputs": [], "source": [ @@ -74,35 +84,51 @@ "\n", "\n", "# Plot position errors\n", - "import plotly.graph_objects as go\n", - "\n", - "fig = go.Figure()\n", - "fig.add_trace(go.Scatter(x=[t.as_datetime() for t in times], y=pdiff, mode='lines', name='Position Error',\n", - " line=dict(color='black', width=2)))\n", - "fig.update_layout(title='TLE Fitting Position Errors',\n", - " xaxis_title='Time',\n", - " yaxis_title='Position Error (m)')\n", - "fig.update_xaxes(showline=True, linewidth=2, linecolor=\"black\", mirror=True)\n", - "fig.update_yaxes(showline=True, linewidth=2, linecolor=\"black\", mirror=True)\n", - "fig.update_layout(\n", - " xaxis=dict(\n", - " gridcolor=\"#dddddd\",\n", - " gridwidth=1,\n", - " ),\n", - " yaxis=dict(\n", - " gridcolor=\"#dddddd\",\n", - " gridwidth=1,\n", - " ),\n", - ")\n", - "\n", - "fig.show()" + "import matplotlib.pyplot as plt\n", + "import scienceplots # noqa: F401\n", + "\n", + "plt.style.use([\"science\", \"no-latex\"])\n", + "plt.rcParams.update({\n", + " \"mathtext.fontset\": \"stix\",\n", + " \"font.family\": \"serif\",\n", + " \"font.serif\": [\"STIX Two Text\", \"STIXGeneral\", \"DejaVu Serif\"],\n", + " \"font.size\": 13,\n", + " \"axes.labelsize\": 14,\n", + " \"axes.titlesize\": 15,\n", + " \"xtick.labelsize\": 12,\n", + " \"ytick.labelsize\": 12,\n", + " \"legend.fontsize\": 12,\n", + " \"axes.formatter.use_mathtext\": True,\n", + " \"svg.fonttype\": \"none\",\n", + " \"axes.grid\": True,\n", + " \"grid.alpha\": 0.3,\n", + " \"grid.linestyle\": \"--\",\n", + " \"axes.prop_cycle\": plt.cycler(color=[\n", + " \"#0077BB\", \"#EE7733\", \"#009988\", \"#CC3311\",\n", + " \"#33BBEE\", \"#EE3377\", \"#BBBBBB\",\n", + " ]),\n", + "})\n", + "%config InlineBackend.figure_formats = ['svg']\n", + "\n", + "fig, ax = plt.subplots(figsize=(10, 5))\n", + "ax.plot([t.as_datetime() for t in times], pdiff, color=\"black\", linewidth=2)\n", + "ax.set_xlabel(\"Time\")\n", + "ax.set_ylabel(\"Position Error (m)\")\n", + "ax.set_title(\"TLE Fitting Position Errors\")\n", + "fig.autofmt_xdate()\n", + "plt.tight_layout()\n", + "plt.show()" ] }, { "cell_type": "markdown", - "id": "0cuc4mopx7tl", - "source": "## Evaluate Fit Quality\n\nCompare the fitted TLE against the original states by propagating the TLE with SGP4, rotating from TEME to GCRF, and computing position differences. Since TLEs are a simplified analytical model, some residual error is expected even with a perfect fit.", - "metadata": {} + "id": "4", + "metadata": {}, + "source": [ + "## Evaluate Fit Quality\n", + "\n", + "Compare the fitted TLE against the original states by propagating the TLE with SGP4, rotating from TEME to GCRF, and computing position differences. Since TLEs are a simplified analytical model, some residual error is expected even with a perfect fit." + ] } ], "metadata": { @@ -126,4 +152,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/docs/tutorials/Two-Line Element Set.ipynb b/docs/tutorials/Two-Line Element Set.ipynb index 62f05e7..5b92fbf 100644 --- a/docs/tutorials/Two-Line Element Set.ipynb +++ b/docs/tutorials/Two-Line Element Set.ipynb @@ -3,12 +3,26 @@ { "cell_type": "markdown", "metadata": {}, - "source": "# Two-Line Element Set\n\nTwo-Line Element sets (TLEs) are the most widely used format for distributing satellite orbital data. They encode mean orbital elements designed specifically for use with the SGP4 propagator, and are published by organizations like [CelesTrak](https://celestrak.org) and [Space-Track](https://www.space-track.org).\n\nThis tutorial demonstrates:\n\n1. Loading a TLE and propagating it with SGP4 to get a position and velocity in the **TEME** (True Equator Mean Equinox) frame\n2. Rotating from TEME to the **ITRF** (Earth-fixed) frame to obtain geodetic coordinates\n3. Plotting the satellite ground track over multiple orbits" + "source": [ + "# Two-Line Element Set\n", + "\n", + "Two-Line Element sets (TLEs) are the most widely used format for distributing satellite orbital data. They encode mean orbital elements designed specifically for use with the SGP4 propagator, and are published by organizations like [CelesTrak](https://celestrak.org) and [Space-Track](https://www.space-track.org).\n", + "\n", + "This tutorial demonstrates:\n", + "\n", + "1. Loading a TLE and propagating it with SGP4 to get a position and velocity in the **TEME** (True Equator Mean Equinox) frame\n", + "2. Rotating from TEME to the **ITRF** (Earth-fixed) frame to obtain geodetic coordinates\n", + "3. Plotting the satellite ground track over multiple orbits" + ] }, { "cell_type": "markdown", "metadata": {}, - "source": "## Generate State Vector\n\nSGP4 outputs position and velocity in the TEME frame, a pseudo-inertial frame that does not account for precession or nutation. To get a location on the Earth's surface, we rotate into the ITRF frame using a quaternion from `satkit.frametransform`, then convert the Cartesian position to geodetic coordinates." + "source": [ + "## Generate State Vector\n", + "\n", + "SGP4 outputs position and velocity in the TEME frame, a pseudo-inertial frame that does not account for precession or nutation. To get a location on the Earth's surface, we rotate into the ITRF frame using a quaternion from `satkit.frametransform`, then convert the Cartesian position to geodetic coordinates." + ] }, { "cell_type": "code", @@ -57,7 +71,11 @@ { "cell_type": "markdown", "metadata": {}, - "source": "## Plot Satellite Ground Track\n\nThe ground track is the projection of the satellite's orbit onto the Earth's surface. The sinusoidal pattern results from the satellite's inclined orbit combined with the Earth's rotation. The `mean_motion` field in the TLE gives the number of orbits per day, which we use to compute the total time span for 5 complete orbits." + "source": [ + "## Plot Satellite Ground Track\n", + "\n", + "The ground track is the projection of the satellite's orbit onto the Earth's surface. The sinusoidal pattern results from the satellite's inclined orbit combined with the Earth's rotation. The `mean_motion` field in the TLE gives the number of orbits per day, which we use to compute the total time span for 5 complete orbits." + ] }, { "cell_type": "code", @@ -65,9 +83,37 @@ "metadata": {}, "outputs": [], "source": [ + "import ssl, certifi\n", + "ssl._create_default_https_context = lambda: ssl.create_default_context(cafile=certifi.where())\n", "import satkit as sk\n", "import numpy as np\n", - "import plotly.graph_objects as go\n", + "import matplotlib.pyplot as plt\n", + "import scienceplots # noqa: F401\n", + "\n", + "plt.style.use([\"science\", \"no-latex\"])\n", + "plt.rcParams.update({\n", + " \"mathtext.fontset\": \"stix\",\n", + " \"font.family\": \"serif\",\n", + " \"font.serif\": [\"STIX Two Text\", \"STIXGeneral\", \"DejaVu Serif\"],\n", + " \"font.size\": 13,\n", + " \"axes.labelsize\": 14,\n", + " \"axes.titlesize\": 15,\n", + " \"xtick.labelsize\": 12,\n", + " \"ytick.labelsize\": 12,\n", + " \"legend.fontsize\": 12,\n", + " \"axes.formatter.use_mathtext\": True,\n", + " \"svg.fonttype\": \"none\",\n", + " \"axes.grid\": True,\n", + " \"grid.alpha\": 0.3,\n", + " \"grid.linestyle\": \"--\",\n", + " \"axes.prop_cycle\": plt.cycler(color=[\n", + " \"#0077BB\", \"#EE7733\", \"#009988\", \"#CC3311\",\n", + " \"#33BBEE\", \"#EE3377\", \"#BBBBBB\",\n", + " ]),\n", + "})\n", + "%config InlineBackend.figure_formats = ['svg']\n", + "import cartopy.crs as ccrs\n", + "import cartopy.feature as cfeature\n", "\n", "# The two-line element set\n", "# Same satellite as above...\n", @@ -94,14 +140,30 @@ "coord = [sk.itrfcoord(p) for p in pITRF]\n", "lat, lon, alt = zip(*[(c.latitude_deg, c.longitude_deg, c.altitude) for c in coord])\n", "\n", - "fig = go.Figure()\n", - "fig.add_trace(go.Scattergeo(lat=lat, lon=lon, mode='lines'))\n", - "fig.update_layout(margin={\"r\":0,\"t\":40,\"l\":0,\"b\":0}, title='Ground Track', geo=dict(showland=True, showcountries=True))\n", - "fig.show()\n", - "fig = go.Figure()\n", - "fig.add_trace(go.Scatter(x=[t.as_datetime() for t in timearr], y=np.array(alt)/1e3, mode='lines'))\n", - "fig.update_layout(yaxis_title='Altitude (km)', xaxis_title='Time', font=dict(size=14), title='Altitude vs Time')\n", - "fig.show()" + "fig, ax = plt.subplots(figsize=(10, 5), subplot_kw={\"projection\": ccrs.PlateCarree()})\n", + "ax.add_feature(cfeature.LAND, facecolor=\"lightgray\")\n", + "ax.add_feature(cfeature.BORDERS, linewidth=0.5)\n", + "ax.add_feature(cfeature.COASTLINE, linewidth=0.5)\n", + "# Break line at date line crossings\n", + "lon_arr, lat_arr = np.array(lon), np.array(lat)\n", + "breaks = np.where(np.abs(np.diff(lon_arr)) > 180)[0] + 1\n", + "lon_segs = np.split(lon_arr, breaks)\n", + "lat_segs = np.split(lat_arr, breaks)\n", + "for lo, la in zip(lon_segs, lat_segs):\n", + " ax.plot(lo, la, linewidth=1, color=\"C0\", transform=ccrs.PlateCarree())\n", + "ax.set_title(\"Ground Track\")\n", + "ax.set_global()\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "fig, ax = plt.subplots(figsize=(10, 4))\n", + "ax.plot([t.as_datetime() for t in timearr], np.array(alt)/1e3)\n", + "ax.set_xlabel(\"Time\")\n", + "ax.set_ylabel(\"Altitude (km)\")\n", + "ax.set_title(\"Altitude vs Time\")\n", + "fig.autofmt_xdate()\n", + "plt.tight_layout()\n", + "plt.show()" ] } ], @@ -126,4 +188,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} \ No newline at end of file +} diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md index 12e906c..dc71b70 100644 --- a/docs/tutorials/index.md +++ b/docs/tutorials/index.md @@ -14,3 +14,9 @@ Interactive Jupyter notebook tutorials demonstrating various features of the `sa | [High Precision Propagation](High%20Precision%20Propagation.ipynb) | Numerical orbit propagation with force models | | [Orbital Mean-Element Message](Orbital%20Mean-Element%20Message.ipynb) | Working with OMM records | | [Optical Observations](Optical%20Observations%20of%20Satellites.ipynb) | Simulating optical satellite observations | +| [Coordinate Frame Transforms](Coordinate%20Frame%20Transforms.ipynb) | GCRF, ITRF, and TEME rotations with comparison of approximate vs full IAU-2006 | +| [Keplerian Elements](Keplerian%20Elements.ipynb) | Orbital elements, Cartesian conversion, and two-body vs numerical propagation | +| [Planetary Ephemerides](Planetary%20Ephemerides.ipynb) | JPL DE440 and low-precision Sun/Moon/planet positions | +| [Atmospheric Density](Atmospheric%20Density.ipynb) | NRLMSISE-00 density profiles, solar activity effects, and drag | +| [Covariance Propagation](Covariance%20Propagation.ipynb) | State transition matrix, uncertainty growth, and LVLH frame | +| [Lambert Targeting](Lambert%20Targeting.ipynb) | Orbit transfer design with delta-v computation and pork-chop plots | diff --git a/docs/tutorials/riseset.ipynb b/docs/tutorials/riseset.ipynb index 58a1c1c..127d838 100644 --- a/docs/tutorials/riseset.ipynb +++ b/docs/tutorials/riseset.ipynb @@ -3,19 +3,58 @@ { "cell_type": "markdown", "metadata": {}, - "source": "# Sunrise and Sunset Times\n\nThis tutorial computes sunrise and sunset times for every day of 2024 at a given location, then visualizes how daylight hours change over the course of the year.\n\n`satkit.sun.rise_set` returns the sunrise and sunset times in UTC for a given date and ground location. The results are converted to local time (US Eastern) for display. The shaded region in the plot represents daylight hours." + "source": [ + "# Sunrise and Sunset Times\n", + "\n", + "This tutorial computes sunrise and sunset times for every day of 2024 at a given location, then visualizes how daylight hours change over the course of the year.\n", + "\n", + "`satkit.sun.rise_set` returns the sunrise and sunset times in UTC for a given date and ground location. The results are converted to local time (US Eastern) for display. The shaded region in the plot represents daylight hours." + ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": "import satkit as sk\nimport datetime\nfrom zoneinfo import ZoneInfo\nimport plotly.graph_objects as go" + "source": [ + "import satkit as sk\n", + "import datetime\n", + "from zoneinfo import ZoneInfo\n", + "import matplotlib.pyplot as plt\n", + "import scienceplots # noqa: F401\n", + "\n", + "plt.style.use([\"science\", \"no-latex\"])\n", + "plt.rcParams.update({\n", + " \"mathtext.fontset\": \"stix\",\n", + " \"font.family\": \"serif\",\n", + " \"font.serif\": [\"STIX Two Text\", \"STIXGeneral\", \"DejaVu Serif\"],\n", + " \"font.size\": 13,\n", + " \"axes.labelsize\": 14,\n", + " \"axes.titlesize\": 15,\n", + " \"xtick.labelsize\": 12,\n", + " \"ytick.labelsize\": 12,\n", + " \"legend.fontsize\": 12,\n", + " \"axes.formatter.use_mathtext\": True,\n", + " \"svg.fonttype\": \"none\",\n", + " \"axes.grid\": True,\n", + " \"grid.alpha\": 0.3,\n", + " \"grid.linestyle\": \"--\",\n", + " \"axes.prop_cycle\": plt.cycler(color=[\n", + " \"#0077BB\", \"#EE7733\", \"#009988\", \"#CC3311\",\n", + " \"#33BBEE\", \"#EE3377\", \"#BBBBBB\",\n", + " ]),\n", + "})\n", + "%config InlineBackend.figure_formats = ['svg']" + ] }, { "cell_type": "markdown", - "source": "## Compute Rise and Set Times\n\nFor each day in 2024, compute the sunrise and sunset in UTC, then convert to US Eastern time using Python's built-in `zoneinfo` module.", - "metadata": {} + "metadata": {}, + "source": [ + "## Compute Rise and Set Times\n", + "\n", + "For each day in 2024, compute the sunrise and sunset in UTC, then convert to US Eastern time using Python's built-in `zoneinfo` module." + ] }, { "cell_type": "code", @@ -23,899 +62,40 @@ "metadata": {}, "outputs": [], "source": [ - "#", - " ", - "A", - "r", - "r", - "a", - "y", - " ", - "o", - "f", - " ", - "t", - "i", - "m", - "e", - "s", - " ", - "f", - "o", - "r", - " ", - "e", - "a", - "c", - "h", - " ", - "d", - "a", - "y", - " ", - "o", - "f", - " ", - "2", - "0", - "2", - "4", - "\n", - "b", - "a", - "s", - "e", - "t", - "i", - "m", - "e", - " ", - "=", - " ", - "s", - "k", - ".", - "t", - "i", - "m", - "e", - "(", - "2", - "0", - "2", - "4", - ",", - " ", - "1", - ",", - " ", - "1", - ")", - "\n", - "t", - "i", - "m", - "e", - "a", - "r", - "r", - " ", - "=", - " ", - "[", - "b", - "a", - "s", - "e", - "t", - "i", - "m", - "e", - " ", - "+", - " ", - "s", - "k", - ".", - "d", - "u", - "r", - "a", - "t", - "i", - "o", - "n", - "(", - "d", - "a", - "y", - "s", - "=", - "i", - ")", - " ", - "f", - "o", - "r", - " ", - "i", - " ", - "i", - "n", - " ", - "r", - "a", - "n", - "g", - "e", - "(", - "3", - "6", - "5", - ")", - "]", - "\n", - "\n", - "#", - " ", - "C", - "o", - "o", - "r", - "d", - "i", - "n", - "a", - "t", - "e", - "s", - " ", - "o", - "f", - " ", - "A", - "r", - "l", - "i", - "n", - "g", - "t", - "o", - "n", - ",", - " ", - "M", - "A", - "\n", - "c", - "o", - "o", - "r", - "d", - " ", - "=", - " ", - "s", - "k", - ".", - "i", - "t", - "r", - "f", - "c", - "o", - "o", - "r", - "d", - "(", - "l", - "a", - "t", - "i", - "t", - "u", - "d", - "e", - "_", - "d", - "e", - "g", - "=", - "4", - "2", - ".", - "1", - "5", - "1", - "4", - ",", - " ", - "l", - "o", - "n", - "g", - "i", - "t", - "u", - "d", - "e", - "_", - "d", - "e", - "g", - "=", - "-", - "7", - "1", - ".", - "1", - "5", - "1", - "6", - ")", - "\n", + "# Array of times for each day of 2024\n", + "basetime = sk.time(2024, 1, 1)\n", + "timearr = [basetime + sk.duration(days=i) for i in range(365)]\n", "\n", + "# Coordinates of Arlington, MA\n", + "coord = sk.itrfcoord(latitude_deg=42.1514, longitude_deg=-71.1516)\n", "\n", - "#", - " ", - "s", - "u", - "n", - "r", - "i", - "s", - "e", - ",", - " ", - "s", - "u", - "n", - "s", - "e", - "t", - " ", - "i", - "n", - " ", - "U", - "T", - "C", "\n", - "r", - "i", - "s", - "e", - "s", - "e", - "t", - " ", - "=", - " ", - "[", - "s", - "k", - ".", - "s", - "u", - "n", - ".", - "r", - "i", - "s", - "e", - "_", - "s", - "e", - "t", - "(", - "t", - ",", - " ", - "c", - "o", - "o", - "r", - "d", - ")", - " ", - "f", - "o", - "r", - " ", - "t", - " ", - "i", - "n", - " ", - "t", - "i", - "m", - "e", - "a", - "r", - "r", - "]", + "# sunrise, sunset in UTC\n", + "riseset = [sk.sun.rise_set(t, coord) for t in timearr]\n", + "rise, set = zip(*riseset)\n", "\n", - "r", - "i", - "s", - "e", - ",", - " ", - "s", - "e", - "t", - " ", - "=", - " ", - "z", - "i", - "p", - "(", - "*", - "r", - "i", - "s", - "e", - "s", - "e", - "t", - ")", + "# Convert to Eastern Time\n", + "eastern = ZoneInfo(\"America/New_York\")\n", + "drise = [r.datetime().astimezone(eastern) for r in rise]\n", + "dset = [s.datetime().astimezone(eastern) for s in set]\n", "\n", + "# Hour of day, in [0,24]\n", + "risefrac = [r.hour + r.minute / 60 + r.second / 3600 for r in drise]\n", + "setfrac = [s.hour + s.minute / 60 + s.second / 3600 for s in dset]\n", "\n", - "#", - " ", - "C", - "o", - "n", - "v", - "e", - "r", - "t", - " ", - "t", - "o", - " ", - "E", - "a", - "s", - "t", - "e", - "r", - "n", - " ", - "T", - "i", - "m", - "e", - "\n", - "e", - "a", - "s", - "t", - "e", - "r", - "n", - " ", - "=", - " ", - "Z", - "o", - "n", - "e", - "I", - "n", - "f", - "o", - "(", - "\"", - "A", - "m", - "e", - "r", - "i", - "c", - "a", - "/", - "N", - "e", - "w", - "_", - "Y", - "o", - "r", - "k", - "\"", - ")", - "\n", - "d", - "r", - "i", - "s", - "e", - " ", - "=", - " ", - "[", - "r", - ".", - "d", - "a", - "t", - "e", - "t", - "i", - "m", - "e", - "(", - ")", - ".", - "a", - "s", - "t", - "i", - "m", - "e", - "z", - "o", - "n", - "e", - "(", - "e", - "a", - "s", - "t", - "e", - "r", - "n", - ")", - " ", - "f", - "o", - "r", - " ", - "r", - " ", - "i", - "n", - " ", - "r", - "i", - "s", - "e", - "]", - "\n", - "d", - "s", - "e", - "t", - " ", - "=", - " ", - "[", - "s", - ".", - "d", - "a", - "t", - "e", - "t", - "i", - "m", - "e", - "(", - ")", - ".", - "a", - "s", - "t", - "i", - "m", - "e", - "z", - "o", - "n", - "e", - "(", - "e", - "a", - "s", - "t", - "e", - "r", - "n", - ")", - " ", - "f", - "o", - "r", - " ", - "s", - " ", - "i", - "n", - " ", - "s", - "e", - "t", - "]", - "\n", - "\n", - "#", - " ", - "H", - "o", - "u", - "r", - " ", - "o", - "f", - " ", - "d", - "a", - "y", - ",", - " ", - "i", - "n", - " ", - "[", - "0", - ",", - "2", - "4", - "]", - "\n", - "r", - "i", - "s", - "e", - "f", - "r", - "a", - "c", - " ", - "=", - " ", - "[", - "r", - ".", - "h", - "o", - "u", - "r", - " ", - "+", - " ", - "r", - ".", - "m", - "i", - "n", - "u", - "t", - "e", - " ", - "/", - " ", - "6", - "0", - " ", - "+", - " ", - "r", - ".", - "s", - "e", - "c", - "o", - "n", - "d", - " ", - "/", - " ", - "3", - "6", - "0", - "0", - " ", - "f", - "o", - "r", - " ", - "r", - " ", - "i", - "n", - " ", - "d", - "r", - "i", - "s", - "e", - "]", - "\n", - "s", - "e", - "t", - "f", - "r", - "a", - "c", - " ", - "=", - " ", - "[", - "s", - ".", - "h", - "o", - "u", - "r", - " ", - "+", - " ", - "s", - ".", - "m", - "i", - "n", - "u", - "t", - "e", - " ", - "/", - " ", - "6", - "0", - " ", - "+", - " ", - "s", - ".", - "s", - "e", - "c", - "o", - "n", - "d", - " ", - "/", - " ", - "3", - "6", - "0", - "0", - " ", - "f", - "o", - "r", - " ", - "s", - " ", - "i", - "n", - " ", - "d", - "s", - "e", - "t", - "]", - "\n", - "\n", - "#", - " ", - "C", - "o", - "n", - "v", - "e", - "r", - "t", - " ", - "h", - "o", - "u", - "r", - " ", - "o", - "f", - " ", - "d", - "a", - "y", - " ", - "t", - "o", - " ", - "a", - " ", - "t", - "i", - "m", - "e", - "\n", - "r", - "i", - "s", - "e", - "t", - "i", - "m", - "e", - " ", - "=", - " ", - "[", - "d", - "a", - "t", - "e", - "t", - "i", - "m", - "e", - ".", - "t", - "i", - "m", - "e", - "(", - "h", - "o", - "u", - "r", - "=", - "r", - ".", - "h", - "o", - "u", - "r", - ",", - " ", - "m", - "i", - "n", - "u", - "t", - "e", - "=", - "r", - ".", - "m", - "i", - "n", - "u", - "t", - "e", - ",", - " ", - "s", - "e", - "c", - "o", - "n", - "d", - "=", - "r", - ".", - "s", - "e", - "c", - "o", - "n", - "d", - ")", - " ", - "f", - "o", - "r", - " ", - "r", - " ", - "i", - "n", - " ", - "d", - "r", - "i", - "s", - "e", - "]", - "\n", - "s", - "e", - "t", - "t", - "i", - "m", - "e", - " ", - "=", - " ", - "[", - "d", - "a", - "t", - "e", - "t", - "i", - "m", - "e", - ".", - "t", - "i", - "m", - "e", - "(", - "h", - "o", - "u", - "r", - "=", - "s", - ".", - "h", - "o", - "u", - "r", - ",", - " ", - "m", - "i", - "n", - "u", - "t", - "e", - "=", - "s", - ".", - "m", - "i", - "n", - "u", - "t", - "e", - ",", - " ", - "s", - "e", - "c", - "o", - "n", - "d", - "=", - "s", - ".", - "s", - "e", - "c", - "o", - "n", - "d", - ")", - " ", - "f", - "o", - "r", - " ", - "s", - " ", - "i", - "n", - " ", - "d", - "s", - "e", - "t", - "]" + "# Convert hour of day to a time\n", + "risetime = [datetime.time(hour=r.hour, minute=r.minute, second=r.second) for r in drise]\n", + "settime = [datetime.time(hour=s.hour, minute=s.minute, second=s.second) for s in dset]" ] }, { "cell_type": "markdown", - "source": "## Visualize\n\nPlot sunrise and sunset times over the year. The shaded area represents daylight hours. Note the characteristic asymmetry around the solstices and the abrupt shifts from daylight saving time transitions.", - "metadata": {} + "metadata": {}, + "source": [ + "## Visualize\n", + "\n", + "Plot sunrise and sunset times over the year. The shaded area represents daylight hours. Note the characteristic asymmetry around the solstices and the abrupt shifts from daylight saving time transitions." + ] }, { "cell_type": "code", @@ -930,60 +110,21 @@ "risestring = [frac2str(r) for r in risetime]\n", "setstring = [frac2str(s) for s in settime]\n", "\n", - "fig = go.Figure()\n", - "fig.add_trace(\n", - " go.Scatter(\n", - " x=[x.as_datetime() for x in timearr],\n", - " y=risefrac,\n", - " customdata=risestring,\n", - " name=\"Sunrise\",\n", - " mode=\"lines\",\n", - " line=dict(color=\"rgb(0,100,80)\"),\n", - " hovertemplate=\"Date: %{x}
Sunrise: %{customdata}\", # Custom hover text\n", - " )\n", - ")\n", - "fig.add_trace(\n", - " go.Scatter(\n", - " x=[x.as_datetime() for x in timearr],\n", - " y=setfrac,\n", - " name=\"SunSet\",\n", - " mode=\"lines\",\n", - " fill=\"tonexty\",\n", - " customdata=setstring,\n", - " fillcolor=\"rgba(0,100,80,0.2)\",\n", - " line=dict(color=\"rgb(0,100,80)\"),\n", - " hovertemplate=\"Date: %{x}
Sunset: %{customdata}\", # Custom hover text\n", - " )\n", - ")\n", - "fig.update_yaxes(title=\"Local Hour of Day\")\n", - "fig.update_xaxes(title=\"Date\")\n", - "fig.update_layout(\n", - " title=\"Sunrise and Sunset Times for 2024 in Arlington, MA\",\n", - " xaxis=dict(\n", - " gridcolor=\"#dddddd\",\n", - " gridwidth=1,\n", - " showline=True,\n", - " mirror=True,\n", - " linewidth=2,\n", - " linecolor=\"black\",\n", - " ),\n", - " yaxis=dict(\n", - " gridcolor=\"#dddddd\",\n", - " gridwidth=1,\n", - " showline=True,\n", - " mirror=True,\n", - " linewidth=2,\n", - " linecolor=\"black\",\n", - " range=[0, 24],\n", - " tickvals=[0, 6, 12, 18, 24], # Set y-axis tick positions\n", - " ticktext=[\"Midnight\", \"6 AM\", \"Noon\", \"6 PM\", \"Midnight\"],\n", - " ),\n", - " plot_bgcolor=\"white\",\n", - " paper_bgcolor=\"white\",\n", - " width=600,\n", - " height=520,\n", - ")\n", - "fig.show()" + "fig, ax = plt.subplots(figsize=(8, 5))\n", + "dates = [x.as_datetime() for x in timearr]\n", + "ax.plot(dates, risefrac, color='#006450', label='Sunrise')\n", + "ax.plot(dates, setfrac, color='#006450', label='Sunset')\n", + "ax.fill_between(dates, risefrac, setfrac, alpha=0.2, color='#006450')\n", + "ax.set_xlabel(\"Date\")\n", + "ax.set_ylabel(\"Local Hour of Day\")\n", + "ax.set_title(\"Sunrise and Sunset Times for 2024 in Arlington, MA\")\n", + "ax.set_ylim(0, 24)\n", + "ax.set_yticks([0, 6, 12, 18, 24])\n", + "ax.set_yticklabels([\"Midnight\", \"6 AM\", \"Noon\", \"6 PM\", \"Midnight\"])\n", + "ax.legend()\n", + "fig.autofmt_xdate()\n", + "plt.tight_layout()\n", + "plt.show()" ] } ], @@ -1003,9 +144,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.0" + "version": "3.14.0" } }, "nbformat": 4, "nbformat_minor": 2 -} \ No newline at end of file +} diff --git a/extern/nrlmsise/DOCUMENTATION b/extern/nrlmsise/DOCUMENTATION deleted file mode 100644 index dfdf5dd..0000000 --- a/extern/nrlmsise/DOCUMENTATION +++ /dev/null @@ -1,275 +0,0 @@ --------------------------------------------------------------------- ---------- N R L M S I S E - 0 0 M O D E L 2 0 0 1 ---------- --------------------------------------------------------------------- - - -Table of Contents ------------------ - -1. Legal Information -2. Brief Description -3. Source Code Availability -4. This C Release - 4.1 Files - 4.2 Differences between FORTRAN and C version -5. Interface -6. Release Notes -7. Testing Output - - - -1. LEGAL INFORMATION -==================== - -This package is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Please inform the -maintainer of the C release (Dominik Brodowski - mail@brodo.de) of -any patches and bug-fixes you implement for NRLMSISE-00 so that this C -package can be updated with these improvements. - - - -2. BRIEF DESCRIPTION -==================== - -The NRLMSIS-00 empirical atmosphere model was developed by Mike -Picone, Alan Hedin, and Doug Drob based on the MSISE90 model. - -The MSISE90 model describes the neutral temperature and densities in -Earth's atmosphere from ground to thermospheric heights. Below 72.5 km -the model is primarily based on the MAP Handbook (Labitzke et al., -1985) tabulation of zonal average temperature and pressure by Barnett -and Corney, which was also used for the CIRA-86. Below 20 km these -data were supplemented with averages from the National Meteorological -Center (NMC). In addition, pitot tube, falling sphere, and grenade -sounder rocket measurements from 1947 to 1972 were taken into -consideration. Above 72.5 km MSISE-90 is essentially a revised MSIS-86 -model taking into account data derived from space shuttle flights and -newer incoherent scatter results. For someone interested only in the -thermosphere (above 120 km), the author recommends the MSIS-86 -model. MSISE is also not the model of preference for specialized -tropospheric work. It is rather for studies that reach across several -atmospheric boundaries. -(quoted from http://nssdc.gsfc.nasa.gov/space/model/atmos/nrlmsise00.html) - - - -3. SOURCE CODE AVAILABILITY -=========================== - -The authors of the NRLMSISE-00 model have released a FORTRAN version -which is available at -http://uap-www.nrl.navy.mil/models_web/msis/msis_home.htm - -Based on the Official Beta Release 1.0 (NRLMSISE-00.DIST12.TXT) -Dominik Brodowski wrote an implementation in C which is available on -http://www.brodo.de/english/pub/nrlmsise/ -This release is based on the Official Beta Release 2.0 -(NRLMSISE-00.DIST17.TXT). - - - -4. THIS C RELEASE -================= - -When "INLINE" is not externally defined the source code should be -clean, plain ANSI which any C compiler should be able to work with. If -you find any problems, please report them to the maintainer Dominik -Brodowski at mail@brodo.de. Thank you! - - -4.1 Files ---------- - -DOCUMENTATION - this Documentation -nrlmsise-00.h - header file for nrlmsise-00 -nrlmsise-00.c - source code for nrlmsise-00 -nrlmsise-00_data.c - block data for nrlmsise-00 -nrlmsise-00_test.c - test-driver for nrlmsise-00 -makefile - makefile for nrlmsise-test (gnu-make and gcc) - - -4.2 Differences between FORTRAN and C version ---------------------------------------------- - -The C package does not save the last results internally to speed up -program execution as the FORTRAN version does. - -The "switches" have to be specified before _every_ call. - -The "DL" array is not printed in the testing routine since it's not an -output value. - -The C version probably contains some bugs and still has to be regarded -more unstable than the FORTRAN release. Please report any bugs or -incorrect values to the maintainer at mail@brodo.de - - - -5. INTERFACE -============ - -To access the NRLMSISE-00 functions you need to include the header -file nrlmsise-00.h. In this file you can find comments which explain -the in- and output, and the differences between the various functions. - - - -6. RELEASE NOTES -================ - -20020302 - first release - -20040322 - fix gtd7d output if sw->flags[0] is set - (noted by Dr. Vasiliy Yurasov) - -20041227 - bugfix against memory corruption - (Donald F. Linton) - -20070727 - bugfix concerning DFA being masked - (Stacey Gage) - -20100516 - bugfix concerning Argon densities - (Dr. Choliy Vasyl) - -20131225 - fix comment on switch 0, silence compiler warnings - (David F. Crouse) - -20150329 - use fabs() instead of abs() - (David F. Crouse) - -20151122 - fixes for gcc5, spelling fixes - (Jacco Geul) - -20170830 - fix some compiler warninges - (noted by Steven Queen) - -20190709 fix output at 32.5 km - (Yoshiaki Ando) - - -7. TESTING OUTPUT -================= - -nrlmsise-test should generate the following output: - - - -6.665177E+05 1.138806E+08 1.998211E+07 4.022764E+05 3.557465E+03 4.074714E-15 3.475312E+04 4.095913E+06 2.667273E+04 1.250540E+03 1.241416E+03 - -3.407293E+06 1.586333E+08 1.391117E+07 3.262560E+05 1.559618E+03 5.001846E-15 4.854208E+04 4.380967E+06 6.956682E+03 1.166754E+03 1.161710E+03 - -1.123767E+05 6.934130E+04 4.247105E+01 1.322750E-01 2.618848E-05 2.756772E-18 2.016750E+04 5.741256E+03 2.374394E+04 1.239892E+03 1.239891E+03 - -5.411554E+07 1.918893E+11 6.115826E+12 1.225201E+12 6.023212E+10 3.584426E-10 1.059880E+07 2.615737E+05 2.819879E-42 1.027318E+03 2.068878E+02 - -1.851122E+06 1.476555E+08 1.579356E+07 2.633795E+05 1.588781E+03 4.809630E-15 5.816167E+04 5.478984E+06 1.264446E+03 1.212396E+03 1.208135E+03 - -8.673095E+05 1.278862E+08 1.822577E+07 2.922214E+05 2.402962E+03 4.355866E-15 3.686389E+04 3.897276E+06 2.667273E+04 1.220146E+03 1.212712E+03 - -5.776251E+05 6.979139E+07 1.236814E+07 2.492868E+05 1.405739E+03 2.470651E-15 5.291986E+04 1.069814E+06 2.667273E+04 1.116385E+03 1.112999E+03 - -3.740304E+05 4.782720E+07 5.240380E+06 1.759875E+05 5.501649E+02 1.571889E-15 8.896776E+04 1.979741E+06 9.121815E+03 1.031247E+03 1.024848E+03 - -6.748339E+05 1.245315E+08 2.369010E+07 4.911583E+05 4.578781E+03 4.564420E-15 3.244595E+04 5.370833E+06 2.667273E+04 1.306052E+03 1.293374E+03 - -5.528601E+05 1.198041E+08 3.495798E+07 9.339618E+05 1.096255E+04 4.974543E-15 2.686428E+04 4.889974E+06 2.805445E+04 1.361868E+03 1.347389E+03 - -1.375488E+14 0.000000E+00 2.049687E+19 5.498695E+18 2.451733E+17 1.261066E-03 0.000000E+00 0.000000E+00 0.000000E+00 1.027318E+03 2.814648E+02 - -4.427443E+13 0.000000E+00 6.597567E+18 1.769929E+18 7.891680E+16 4.059139E-04 0.000000E+00 0.000000E+00 0.000000E+00 1.027318E+03 2.274180E+02 - -2.127829E+12 0.000000E+00 3.170791E+17 8.506280E+16 3.792741E+15 1.950822E-05 0.000000E+00 0.000000E+00 0.000000E+00 1.027318E+03 2.374389E+02 - -1.412184E+11 0.000000E+00 2.104370E+16 5.645392E+15 2.517142E+14 1.294709E-06 0.000000E+00 0.000000E+00 0.000000E+00 1.027318E+03 2.795551E+02 - -1.254884E+10 0.000000E+00 1.874533E+15 4.923051E+14 2.239685E+13 1.147668E-07 0.000000E+00 0.000000E+00 0.000000E+00 1.027318E+03 2.190732E+02 - -5.196477E+05 1.274494E+08 4.850450E+07 1.720838E+06 2.354487E+04 5.881940E-15 2.500078E+04 6.279210E+06 2.667273E+04 1.426412E+03 1.408608E+03 - -4.260860E+07 1.241342E+11 4.929562E+12 1.048407E+12 4.993465E+10 2.914304E-10 8.831229E+06 2.252516E+05 2.415246E-42 1.027318E+03 1.934071E+02 - - -DAY 172 81 172 172 172 -UT 29000 29000 75000 29000 29000 -ALT 400 400 1000 100 400 -LAT 60 60 60 60 0 -LONG -70 -70 -70 -70 -70 -LST 16 16 16 16 16 -F107A 150 150 150 150 150 -F107 150 150 150 150 150 - - -TINF 1250.54 1166.75 1239.89 1027.32 1212.40 -TG 1241.42 1161.71 1239.89 206.89 1208.14 -HE 6.665e+05 3.407e+06 1.124e+05 5.412e+07 1.851e+06 -O 1.139e+08 1.586e+08 6.934e+04 1.919e+11 1.477e+08 -N2 1.998e+07 1.391e+07 4.247e+01 6.116e+12 1.579e+07 -O2 4.023e+05 3.263e+05 1.323e-01 1.225e+12 2.634e+05 -AR 3.557e+03 1.560e+03 2.619e-05 6.023e+10 1.589e+03 -H 3.475e+04 4.854e+04 2.017e+04 1.060e+07 5.816e+04 -N 4.096e+06 4.381e+06 5.741e+03 2.616e+05 5.479e+06 -ANM 0 2.667e+04 6.957e+03 2.374e+04 2.820e-42 1.264e+03 -RHO 4.075e-15 5.002e-15 2.757e-18 3.584e-10 4.810e-15 - - -DAY 172 172 172 172 172 -UT 29000 29000 29000 29000 29000 -ALT 400 400 400 400 400 -LAT 60 60 60 60 60 -LONG 0 -70 -70 -70 -70 -LST 16 4 16 16 16 -F107A 150 150 70 150 150 -F107 150 150 150 180 150 - - -TINF 1220.15 1116.39 1031.25 1306.05 1361.87 -TG 1212.71 1113.00 1024.85 1293.37 1347.39 -HE 8.673e+05 5.776e+05 3.740e+05 6.748e+05 5.529e+05 -O 1.279e+08 6.979e+07 4.783e+07 1.245e+08 1.198e+08 -N2 1.823e+07 1.237e+07 5.240e+06 2.369e+07 3.496e+07 -O2 2.922e+05 2.493e+05 1.760e+05 4.912e+05 9.340e+05 -AR 2.403e+03 1.406e+03 5.502e+02 4.579e+03 1.096e+04 -H 3.686e+04 5.292e+04 8.897e+04 3.245e+04 2.686e+04 -N 3.897e+06 1.070e+06 1.980e+06 5.371e+06 4.890e+06 -ANM 0 2.667e+04 2.667e+04 9.122e+03 2.667e+04 2.805e+04 -RHO 4.356e-15 2.471e-15 1.572e-15 4.564e-15 4.975e-15 - - -DAY 172 172 172 172 172 -UT 29000 29000 29000 29000 29000 -ALT 0 10 30 50 70 -LAT 60 60 60 60 60 -LONG -70 -70 -70 -70 -70 -LST 16 16 16 16 16 -F107A 150 150 150 150 150 -F107 150 150 150 150 150 - - -TINF 1027.32 1027.32 1027.32 1027.32 1027.32 -TG 281.46 227.42 237.44 279.56 219.07 -HE 1.375e+14 4.427e+13 2.128e+12 1.412e+11 1.255e+10 -O 0.000e+00 0.000e+00 0.000e+00 0.000e+00 0.000e+00 -N2 2.050e+19 6.598e+18 3.171e+17 2.104e+16 1.875e+15 -O2 5.499e+18 1.770e+18 8.506e+16 5.645e+15 4.923e+14 -AR 2.452e+17 7.892e+16 3.793e+15 2.517e+14 2.240e+13 -H 0.000e+00 0.000e+00 0.000e+00 0.000e+00 0.000e+00 -N 0.000e+00 0.000e+00 0.000e+00 0.000e+00 0.000e+00 -ANM 0 0.000e+00 0.000e+00 0.000e+00 0.000e+00 0.000e+00 -RHO 1.261e-03 4.059e-04 1.951e-05 1.295e-06 1.148e-07 - - -Note: These values equal those of the official FORTRAN package with -one notable exception: the FORTRAN version reports for "anomalous -oxygen" in test-run 4 exactly 0.000E-00, while my C compiler -generates code which calculates 2.820e-42. When only 16-bit wide -double variables are used, this value reduces to 0.000E-00 as well. - - - -======================================================== - -Frankfurt, Germany, on August 30, 2017 - -Dominik Brodowski diff --git a/extern/nrlmsise/nrlmsise-00.c b/extern/nrlmsise/nrlmsise-00.c deleted file mode 100644 index 6b63d29..0000000 --- a/extern/nrlmsise/nrlmsise-00.c +++ /dev/null @@ -1,1459 +0,0 @@ -/* -------------------------------------------------------------------- */ -/* --------- N R L M S I S E - 0 0 M O D E L 2 0 0 1 ---------- */ -/* -------------------------------------------------------------------- */ - -/* This file is part of the NRLMSISE-00 C source code package - release - * 20041227 - * - * The NRLMSISE-00 model was developed by Mike Picone, Alan Hedin, and - * Doug Drob. They also wrote a NRLMSISE-00 distribution package in - * FORTRAN which is available at - * http://uap-www.nrl.navy.mil/models_web/msis/msis_home.htm - * - * Dominik Brodowski implemented and maintains this C version. You can - * reach him at mail@brodo.de. See the file "DOCUMENTATION" for details, - * and check http://www.brodo.de/english/pub/nrlmsise/index.html for - * updated releases of this package. - */ - - - -/* ------------------------------------------------------------------- */ -/* ------------------------------ INCLUDES --------------------------- */ -/* ------------------------------------------------------------------- */ - -#include "nrlmsise-00.h" /* header for nrlmsise-00.h */ -#include /* maths functions */ -#include /* for error messages. TBD: remove this */ -#include /* for malloc/free */ - - - -/* ------------------------------------------------------------------- */ -/* ------------------------- SHARED VARIABLES ------------------------ */ -/* ------------------------------------------------------------------- */ - -/* PARMB */ -static double gsurf; -static double re; - -/* GTS3C */ -static double dd; - -/* DMIX */ -static double dm04, dm16, dm28, dm32, dm40, dm01, dm14; - -/* MESO7 */ -static double meso_tn1[5]; -static double meso_tn2[4]; -static double meso_tn3[5]; -static double meso_tgn1[2]; -static double meso_tgn2[2]; -static double meso_tgn3[2]; - -/* POWER7 */ -extern double pt[150]; -extern double pd[9][150]; -extern double ps[150]; -extern double pdl[2][25]; -extern double ptl[4][100]; -extern double pma[10][100]; -extern double sam[100]; - -/* LOWER7 */ -extern double ptm[10]; -extern double pdm[8][10]; -extern double pavgm[10]; - -/* LPOLY */ -static double dfa; -static double plg[4][9]; -static double ctloc, stloc; -static double c2tloc, s2tloc; -static double s3tloc, c3tloc; -static double apdf, apt[4]; - - - -/* ------------------------------------------------------------------- */ -/* ------------------------------ TSELEC ----------------------------- */ -/* ------------------------------------------------------------------- */ - -void tselec(struct nrlmsise_flags *flags) { - int i; - for (i=0;i<24;i++) { - if (i!=9) { - if (flags->switches[i]==1) - flags->sw[i]=1; - else - flags->sw[i]=0; - if (flags->switches[i]>0) - flags->swc[i]=1; - else - flags->swc[i]=0; - } else { - flags->sw[i]=flags->switches[i]; - flags->swc[i]=flags->switches[i]; - } - } -} - - - -/* ------------------------------------------------------------------- */ -/* ------------------------------ GLATF ------------------------------ */ -/* ------------------------------------------------------------------- */ - -void glatf(double lat, double *gv, double *reff) { - double dgtr = 1.74533E-2; - double c2; - c2 = cos(2.0*dgtr*lat); - *gv = 980.616 * (1.0 - 0.0026373 * c2); - *reff = 2.0 * (*gv) / (3.085462E-6 + 2.27E-9 * c2) * 1.0E-5; -} - - - -/* ------------------------------------------------------------------- */ -/* ------------------------------ CCOR ------------------------------- */ -/* ------------------------------------------------------------------- */ - -double ccor(double alt, double r, double h1, double zh) { -/* CHEMISTRY/DISSOCIATION CORRECTION FOR MSIS MODELS - * ALT - altitude - * R - target ratio - * H1 - transition scale length - * ZH - altitude of 1/2 R - */ - double e; - double ex; - e = (alt - zh) / h1; - if (e>70) - return exp(0); - if (e<-70) - return exp(r); - ex = exp(e); - e = r / (1.0 + ex); - return exp(e); -} - - - -/* ------------------------------------------------------------------- */ -/* ------------------------------ CCOR ------------------------------- */ -/* ------------------------------------------------------------------- */ - -double ccor2(double alt, double r, double h1, double zh, double h2) { -/* CHEMISTRY/DISSOCIATION CORRECTION FOR MSIS MODELS - * ALT - altitude - * R - target ratio - * H1 - transition scale length - * ZH - altitude of 1/2 R - * H2 - transition scale length #2 ? - */ - double e1, e2; - double ex1, ex2; - double ccor2v; - e1 = (alt - zh) / h1; - e2 = (alt - zh) / h2; - if ((e1 > 70) || (e2 > 70)) - return exp(0); - if ((e1 < -70) && (e2 < -70)) - return exp(r); - ex1 = exp(e1); - ex2 = exp(e2); - ccor2v = r / (1.0 + 0.5 * (ex1 + ex2)); - return exp(ccor2v); -} - - - -/* ------------------------------------------------------------------- */ -/* ------------------------------- SCALH ----------------------------- */ -/* ------------------------------------------------------------------- */ - -double scalh(double alt, double xm, double temp) { - double g; - double rgas=831.4; - g = gsurf / (pow((1.0 + alt/re),2.0)); - g = rgas * temp / (g * xm); - return g; -} - - - -/* ------------------------------------------------------------------- */ -/* -------------------------------- DNET ----------------------------- */ -/* ------------------------------------------------------------------- */ - -double dnet (double dd, double dm, double zhm, double xmm, double xm) { -/* TURBOPAUSE CORRECTION FOR MSIS MODELS - * Root mean density - * DD - diffusive density - * DM - full mixed density - * ZHM - transition scale length - * XMM - full mixed molecular weight - * XM - species molecular weight - * DNET - combined density - */ - double a; - double ylog; - a = zhm / (xmm-xm); - if (!((dm>0) && (dd>0))) { - printf("dnet log error %e %e %e\n",dm,dd,xm); - if ((dd==0) && (dm==0)) - dd=1; - if (dm==0) - return dd; - if (dd==0) - return dm; - } - ylog = a * log(dm/dd); - if (ylog<-10) - return dd; - if (ylog>10) - return dm; - a = dd*pow((1.0 + exp(ylog)),(1.0/a)); - return a; -} - - - -/* ------------------------------------------------------------------- */ -/* ------------------------------- SPLINI ---------------------------- */ -/* ------------------------------------------------------------------- */ - -void splini (double *xa, double *ya, double *y2a, int n, double x, double *y) { -/* INTEGRATE CUBIC SPLINE FUNCTION FROM XA(1) TO X - * XA,YA: ARRAYS OF TABULATED FUNCTION IN ASCENDING ORDER BY X - * Y2A: ARRAY OF SECOND DERIVATIVES - * N: SIZE OF ARRAYS XA,YA,Y2A - * X: ABSCISSA ENDPOINT FOR INTEGRATION - * Y: OUTPUT VALUE - */ - double yi=0; - int klo=0; - int khi=1; - double xx, h, a, b, a2, b2; - while ((x>xa[klo]) && (khi1) { - k=(khi+klo)/2; - if (xa[k]>x) - khi=k; - else - klo=k; - } - h = xa[khi] - xa[klo]; - if (h==0.0) - printf("bad XA input to splint"); - a = (xa[khi] - x)/h; - b = (x - xa[klo])/h; - yi = a * ya[klo] + b * ya[khi] + ((a*a*a - a) * y2a[klo] + (b*b*b - b) * y2a[khi]) * h * h/6.0; - *y = yi; -} - - - -/* ------------------------------------------------------------------- */ -/* ------------------------------- SPLINE ---------------------------- */ -/* ------------------------------------------------------------------- */ - -void spline (double *x, double *y, int n, double yp1, double ypn, double *y2) { -/* CALCULATE 2ND DERIVATIVES OF CUBIC SPLINE INTERP FUNCTION - * ADAPTED FROM NUMERICAL RECIPES BY PRESS ET AL - * X,Y: ARRAYS OF TABULATED FUNCTION IN ASCENDING ORDER BY X - * N: SIZE OF ARRAYS X,Y - * YP1,YPN: SPECIFIED DERIVATIVES AT X[0] AND X[N-1]; VALUES - * >= 1E30 SIGNAL SIGNAL SECOND DERIVATIVE ZERO - * Y2: OUTPUT ARRAY OF SECOND DERIVATIVES - */ - double *u; - double sig, p, qn, un; - int i, k; - u=malloc(sizeof(double)*(unsigned int)n); - if (u==NULL) { - printf("Out Of Memory in spline - ERROR"); - return; - } - if (yp1>0.99E30) { - y2[0]=0; - u[0]=0; - } else { - y2[0]=-0.5; - u[0]=(3.0/(x[1]-x[0]))*((y[1]-y[0])/(x[1]-x[0])-yp1); - } - for (i=1;i<(n-1);i++) { - sig = (x[i]-x[i-1])/(x[i+1] - x[i-1]); - p = sig * y2[i-1] + 2.0; - y2[i] = (sig - 1.0) / p; - u[i] = (6.0 * ((y[i+1] - y[i])/(x[i+1] - x[i]) -(y[i] - y[i-1]) / (x[i] - x[i-1]))/(x[i+1] - x[i-1]) - sig * u[i-1])/p; - } - if (ypn>0.99E30) { - qn = 0; - un = 0; - } else { - qn = 0.5; - un = (3.0 / (x[n-1] - x[n-2])) * (ypn - (y[n-1] - y[n-2])/(x[n-1] - x[n-2])); - } - y2[n-1] = (un - qn * u[n-2]) / (qn * y2[n-2] + 1.0); - for (k=n-2;k>=0;k--) - y2[k] = y2[k] * y2[k+1] + u[k]; - - free(u); -} - - - -/* ------------------------------------------------------------------- */ -/* ------------------------------- DENSM ----------------------------- */ -/* ------------------------------------------------------------------- */ - -__inline_double zeta(double zz, double zl) { - return ((zz-zl)*(re+zl)/(re+zz)); -} - -double densm (double alt, double d0, double xm, double *tz, int mn3, double *zn3, double *tn3, double *tgn3, int mn2, double *zn2, double *tn2, double *tgn2) { -/* Calculate Temperature and Density Profiles for lower atmos. */ - double xs[10], ys[10], y2out[10]; - double rgas = 831.4; - double z, z1, z2, t1, t2, zg, zgdif; - double yd1, yd2; - double x, y, yi; - double expl, gamm, glb; - double densm_tmp; - int mn; - int k; - densm_tmp=d0; - if (alt>zn2[0]) { - if (xm==0.0) - return *tz; - else - return d0; - } - - /* STRATOSPHERE/MESOSPHERE TEMPERATURE */ - if (alt>zn2[mn2-1]) - z=alt; - else - z=zn2[mn2-1]; - mn=mn2; - z1=zn2[0]; - z2=zn2[mn-1]; - t1=tn2[0]; - t2=tn2[mn-1]; - zg = zeta(z, z1); - zgdif = zeta(z2, z1); - - /* set up spline nodes */ - for (k=0;k50.0) - expl=50.0; - - /* Density at altitude */ - densm_tmp = densm_tmp * (t1 / *tz) * exp(-expl); - } - - if (alt>zn3[0]) { - if (xm==0.0) - return *tz; - else - return densm_tmp; - } - - /* troposhere / stratosphere temperature */ - z = alt; - mn = mn3; - z1=zn3[0]; - z2=zn3[mn-1]; - t1=tn3[0]; - t2=tn3[mn-1]; - zg=zeta(z,z1); - zgdif=zeta(z2,z1); - - /* set up spline nodes */ - for (k=0;k50.0) - expl=50.0; - - /* Density at altitude */ - densm_tmp = densm_tmp * (t1 / *tz) * exp(-expl); - } - if (xm==0.0) - return *tz; - else - return densm_tmp; -} - - - -/* ------------------------------------------------------------------- */ -/* ------------------------------- DENSU ----------------------------- */ -/* ------------------------------------------------------------------- */ - -double densu (double alt, double dlb, double tinf, double tlb, double xm, double alpha, double *tz, double zlb, double s2, int mn1, double *zn1, double *tn1, double *tgn1) { -/* Calculate Temperature and Density Profiles for MSIS models - * New lower thermo polynomial - */ - double yd2, yd1, x=0, y; - double rgas=831.4; - double densu_temp=1.0; - double za, z, zg2, tt, ta; - double dta, z1=0, z2, t1=0, t2, zg, zgdif=0; - int mn=0; - int k; - double glb; - double expl; - double yi; - double densa; - double gamma, gamm; - double xs[5], ys[5], y2out[5]; - /* joining altitudes of Bates and spline */ - za=zn1[0]; - if (alt>za) - z=alt; - else - z=za; - - /* geopotential altitude difference from ZLB */ - zg2 = zeta(z, zlb); - - /* Bates temperature */ - tt = tinf - (tinf - tlb) * exp(-s2*zg2); - ta = tt; - *tz = tt; - densu_temp = *tz; - - if (altzn1[mn1-1]) - z=alt; - else - z=zn1[mn1-1]; - mn=mn1; - z1=zn1[0]; - z2=zn1[mn-1]; - t1=tn1[0]; - t2=tn1[mn-1]; - /* geopotental difference from z1 */ - zg = zeta (z, z1); - zgdif = zeta(z2, z1); - /* set up spline nodes */ - for (k=0;k50.0) - expl=50.0; - if (tt<=0) - expl=50.0; - - /* density at altitude */ - densa = dlb * pow((tlb/tt),((1.0+alpha+gamma))) * expl; - densu_temp=densa; - if (alt>=za) - return densu_temp; - - /* calculate density below za */ - glb = gsurf / pow((1.0 + z1/re),2.0); - gamm = xm * glb * zgdif / rgas; - - /* integrate spline temperatures */ - splini (xs, ys, y2out, mn, x, &yi); - expl = gamm * yi; - if (expl>50.0) - expl=50.0; - if (*tz<=0) - expl=50.0; - - /* density at altitude */ - densu_temp = densu_temp * pow ((t1 / *tz),(1.0 + alpha)) * exp(-expl); - return densu_temp; -} - - - -/* ------------------------------------------------------------------- */ -/* ------------------------------- GLOBE7 ---------------------------- */ -/* ------------------------------------------------------------------- */ - -/* 3hr Magnetic activity functions */ -/* Eq. A24d */ -__inline_double g0(double a, double *p) { - return (a - 4.0 + (p[25] - 1.0) * (a - 4.0 + (exp(-sqrt(p[24]*p[24]) * (a - 4.0)) - 1.0) / sqrt(p[24]*p[24]))); -} - -/* Eq. A24c */ -__inline_double sumex(double ex) { - return (1.0 + (1.0 - pow(ex,19.0)) / (1.0 - ex) * pow(ex,0.5)); -} - -/* Eq. A24a */ -__inline_double sg0(double ex, double *p, double *ap) { - return (g0(ap[1],p) + (g0(ap[2],p)*ex + g0(ap[3],p)*ex*ex + \ - g0(ap[4],p)*pow(ex,3.0) + (g0(ap[5],p)*pow(ex,4.0) + \ - g0(ap[6],p)*pow(ex,12.0))*(1.0-pow(ex,8.0))/(1.0-ex)))/sumex(ex); -} - -double globe7(double *p, struct nrlmsise_input *input, struct nrlmsise_flags *flags) { -/* CALCULATE G(L) FUNCTION - * Upper Thermosphere Parameters */ - double t[15]; - int i,j; - double apd; - double tloc; - double c, s, c2, c4, s2; - double sr = 7.2722E-5; - double dgtr = 1.74533E-2; - double dr = 1.72142E-2; - double hr = 0.2618; - double cd32, cd18, cd14, cd39; - double df; - double f1, f2; - double tinf; - struct ap_array *ap; - - tloc=input->lst; - for (j=0;j<14;j++) - t[j]=0; - - /* calculate legendre polynomials */ - c = sin(input->g_lat * dgtr); - s = cos(input->g_lat * dgtr); - c2 = c*c; - c4 = c2*c2; - s2 = s*s; - - plg[0][1] = c; - plg[0][2] = 0.5*(3.0*c2 -1.0); - plg[0][3] = 0.5*(5.0*c*c2-3.0*c); - plg[0][4] = (35.0*c4 - 30.0*c2 + 3.0)/8.0; - plg[0][5] = (63.0*c2*c2*c - 70.0*c2*c + 15.0*c)/8.0; - plg[0][6] = (11.0*c*plg[0][5] - 5.0*plg[0][4])/6.0; -/* plg[0][7] = (13.0*c*plg[0][6] - 6.0*plg[0][5])/7.0; */ - plg[1][1] = s; - plg[1][2] = 3.0*c*s; - plg[1][3] = 1.5*(5.0*c2-1.0)*s; - plg[1][4] = 2.5*(7.0*c2*c-3.0*c)*s; - plg[1][5] = 1.875*(21.0*c4 - 14.0*c2 +1.0)*s; - plg[1][6] = (11.0*c*plg[1][5]-6.0*plg[1][4])/5.0; -/* plg[1][7] = (13.0*c*plg[1][6]-7.0*plg[1][5])/6.0; */ -/* plg[1][8] = (15.0*c*plg[1][7]-8.0*plg[1][6])/7.0; */ - plg[2][2] = 3.0*s2; - plg[2][3] = 15.0*s2*c; - plg[2][4] = 7.5*(7.0*c2 -1.0)*s2; - plg[2][5] = 3.0*c*plg[2][4]-2.0*plg[2][3]; - plg[2][6] =(11.0*c*plg[2][5]-7.0*plg[2][4])/4.0; - plg[2][7] =(13.0*c*plg[2][6]-8.0*plg[2][5])/5.0; - plg[3][3] = 15.0*s2*s; - plg[3][4] = 105.0*s2*s*c; - plg[3][5] =(9.0*c*plg[3][4]-7.*plg[3][3])/2.0; - plg[3][6] =(11.0*c*plg[3][5]-8.*plg[3][4])/3.0; - - if (!(((flags->sw[7]==0)&&(flags->sw[8]==0))&&(flags->sw[14]==0))) { - stloc = sin(hr*tloc); - ctloc = cos(hr*tloc); - s2tloc = sin(2.0*hr*tloc); - c2tloc = cos(2.0*hr*tloc); - s3tloc = sin(3.0*hr*tloc); - c3tloc = cos(3.0*hr*tloc); - } - - cd32 = cos(dr*(input->doy-p[31])); - cd18 = cos(2.0*dr*(input->doy-p[17])); - cd14 = cos(dr*(input->doy-p[13])); - cd39 = cos(2.0*dr*(input->doy-p[38])); - - /* F10.7 EFFECT */ - df = input->f107 - input->f107A; - dfa = input->f107A - 150.0; - t[0] = p[19]*df*(1.0+p[59]*dfa) + p[20]*df*df + p[21]*dfa + p[29]*pow(dfa,2.0); - f1 = 1.0 + (p[47]*dfa +p[19]*df+p[20]*df*df)*flags->swc[1]; - f2 = 1.0 + (p[49]*dfa+p[19]*df+p[20]*df*df)*flags->swc[1]; - - /* TIME INDEPENDENT */ - t[1] = (p[1]*plg[0][2]+ p[2]*plg[0][4]+p[22]*plg[0][6]) + \ - (p[14]*plg[0][2])*dfa*flags->swc[1] +p[26]*plg[0][1]; - - /* SYMMETRICAL ANNUAL */ - t[2] = p[18]*cd32; - - /* SYMMETRICAL SEMIANNUAL */ - t[3] = (p[15]+p[16]*plg[0][2])*cd18; - - /* ASYMMETRICAL ANNUAL */ - t[4] = f1*(p[9]*plg[0][1]+p[10]*plg[0][3])*cd14; - - /* ASYMMETRICAL SEMIANNUAL */ - t[5] = p[37]*plg[0][1]*cd39; - - /* DIURNAL */ - if (flags->sw[7]) { - double t71, t72; - t71 = (p[11]*plg[1][2])*cd14*flags->swc[5]; - t72 = (p[12]*plg[1][2])*cd14*flags->swc[5]; - t[6] = f2*((p[3]*plg[1][1] + p[4]*plg[1][3] + p[27]*plg[1][5] + t71) * \ - ctloc + (p[6]*plg[1][1] + p[7]*plg[1][3] + p[28]*plg[1][5] \ - + t72)*stloc); -} - - /* SEMIDIURNAL */ - if (flags->sw[8]) { - double t81, t82; - t81 = (p[23]*plg[2][3]+p[35]*plg[2][5])*cd14*flags->swc[5]; - t82 = (p[33]*plg[2][3]+p[36]*plg[2][5])*cd14*flags->swc[5]; - t[7] = f2*((p[5]*plg[2][2]+ p[41]*plg[2][4] + t81)*c2tloc +(p[8]*plg[2][2] + p[42]*plg[2][4] + t82)*s2tloc); - } - - /* TERDIURNAL */ - if (flags->sw[14]) { - t[13] = f2 * ((p[39]*plg[3][3]+(p[93]*plg[3][4]+p[46]*plg[3][6])*cd14*flags->swc[5])* s3tloc +(p[40]*plg[3][3]+(p[94]*plg[3][4]+p[48]*plg[3][6])*cd14*flags->swc[5])* c3tloc); -} - - /* magnetic activity based on daily ap */ - if (flags->sw[9]==-1) { - ap = input->ap_a; - if (p[51]!=0) { - double exp1; - exp1 = exp(-10800.0*sqrt(p[51]*p[51])/(1.0+p[138]*(45.0-sqrt(input->g_lat*input->g_lat)))); - if (exp1>0.99999) - exp1=0.99999; - if (p[24]<1.0E-4) - p[24]=1.0E-4; - apt[0]=sg0(exp1,p,ap->a); - /* apt[1]=sg2(exp1,p,ap->a); - apt[2]=sg0(exp2,p,ap->a); - apt[3]=sg2(exp2,p,ap->a); - */ - if (flags->sw[9]) { - t[8] = apt[0]*(p[50]+p[96]*plg[0][2]+p[54]*plg[0][4]+ \ - (p[125]*plg[0][1]+p[126]*plg[0][3]+p[127]*plg[0][5])*cd14*flags->swc[5]+ \ - (p[128]*plg[1][1]+p[129]*plg[1][3]+p[130]*plg[1][5])*flags->swc[7]* \ - cos(hr*(tloc-p[131]))); - } - } - } else { - double p44, p45; - apd=input->ap-4.0; - p44=p[43]; - p45=p[44]; - if (p44<0) - p44 = 1.0E-5; - apdf = apd + (p45-1.0)*(apd + (exp(-p44 * apd) - 1.0)/p44); - if (flags->sw[9]) { - t[8]=apdf*(p[32]+p[45]*plg[0][2]+p[34]*plg[0][4]+ \ - (p[100]*plg[0][1]+p[101]*plg[0][3]+p[102]*plg[0][5])*cd14*flags->swc[5]+ - (p[121]*plg[1][1]+p[122]*plg[1][3]+p[123]*plg[1][5])*flags->swc[7]* - cos(hr*(tloc-p[124]))); - } - } - - if ((flags->sw[10])&&(input->g_long>-1000.0)) { - - /* longitudinal */ - if (flags->sw[11]) { - t[10] = (1.0 + p[80]*dfa*flags->swc[1])* \ - ((p[64]*plg[1][2]+p[65]*plg[1][4]+p[66]*plg[1][6]\ - +p[103]*plg[1][1]+p[104]*plg[1][3]+p[105]*plg[1][5]\ - +flags->swc[5]*(p[109]*plg[1][1]+p[110]*plg[1][3]+p[111]*plg[1][5])*cd14)* \ - cos(dgtr*input->g_long) \ - +(p[90]*plg[1][2]+p[91]*plg[1][4]+p[92]*plg[1][6]\ - +p[106]*plg[1][1]+p[107]*plg[1][3]+p[108]*plg[1][5]\ - +flags->swc[5]*(p[112]*plg[1][1]+p[113]*plg[1][3]+p[114]*plg[1][5])*cd14)* \ - sin(dgtr*input->g_long)); - } - - /* ut and mixed ut, longitude */ - if (flags->sw[12]){ - t[11]=(1.0+p[95]*plg[0][1])*(1.0+p[81]*dfa*flags->swc[1])*\ - (1.0+p[119]*plg[0][1]*flags->swc[5]*cd14)*\ - ((p[68]*plg[0][1]+p[69]*plg[0][3]+p[70]*plg[0][5])*\ - cos(sr*(input->sec-p[71]))); - t[11]+=flags->swc[11]*\ - (p[76]*plg[2][3]+p[77]*plg[2][5]+p[78]*plg[2][7])*\ - cos(sr*(input->sec-p[79])+2.0*dgtr*input->g_long)*(1.0+p[137]*dfa*flags->swc[1]); - } - - /* ut, longitude magnetic activity */ - if (flags->sw[13]) { - if (flags->sw[9]==-1) { - if (p[51]) { - t[12]=apt[0]*flags->swc[11]*(1.+p[132]*plg[0][1])*\ - ((p[52]*plg[1][2]+p[98]*plg[1][4]+p[67]*plg[1][6])*\ - cos(dgtr*(input->g_long-p[97])))\ - +apt[0]*flags->swc[11]*flags->swc[5]*\ - (p[133]*plg[1][1]+p[134]*plg[1][3]+p[135]*plg[1][5])*\ - cd14*cos(dgtr*(input->g_long-p[136])) \ - +apt[0]*flags->swc[12]* \ - (p[55]*plg[0][1]+p[56]*plg[0][3]+p[57]*plg[0][5])*\ - cos(sr*(input->sec-p[58])); - } - } else { - t[12] = apdf*flags->swc[11]*(1.0+p[120]*plg[0][1])*\ - ((p[60]*plg[1][2]+p[61]*plg[1][4]+p[62]*plg[1][6])*\ - cos(dgtr*(input->g_long-p[63])))\ - +apdf*flags->swc[11]*flags->swc[5]* \ - (p[115]*plg[1][1]+p[116]*plg[1][3]+p[117]*plg[1][5])* \ - cd14*cos(dgtr*(input->g_long-p[118])) \ - + apdf*flags->swc[12]* \ - (p[83]*plg[0][1]+p[84]*plg[0][3]+p[85]*plg[0][5])* \ - cos(sr*(input->sec-p[75])); - } - } - } - - /* parms not used: 82, 89, 99, 139-149 */ - tinf = p[30]; - for (i=0;i<14;i++) - tinf = tinf + fabs(flags->sw[i+1])*t[i]; - return tinf; -} - - - -/* ------------------------------------------------------------------- */ -/* ------------------------------- GLOB7S ---------------------------- */ -/* ------------------------------------------------------------------- */ - -double glob7s(double *p, struct nrlmsise_input *input, struct nrlmsise_flags *flags) { -/* VERSION OF GLOBE FOR LOWER ATMOSPHERE 10/26/99 - */ - double pset=2.0; - double t[14]; - double tt; - double cd32, cd18, cd14, cd39; - int i,j; - double dr=1.72142E-2; - double dgtr=1.74533E-2; - /* confirm parameter set */ - if (p[99]==0) - p[99]=pset; - if (p[99]!=pset) { - printf("Wrong parameter set for glob7s\n"); - return -1; - } - for (j=0;j<14;j++) - t[j]=0.0; - cd32 = cos(dr*(input->doy-p[31])); - cd18 = cos(2.0*dr*(input->doy-p[17])); - cd14 = cos(dr*(input->doy-p[13])); - cd39 = cos(2.0*dr*(input->doy-p[38])); - - /* F10.7 */ - t[0] = p[21]*dfa; - - /* time independent */ - t[1]=p[1]*plg[0][2] + p[2]*plg[0][4] + p[22]*plg[0][6] + p[26]*plg[0][1] + p[14]*plg[0][3] + p[59]*plg[0][5]; - - /* SYMMETRICAL ANNUAL */ - t[2]=(p[18]+p[47]*plg[0][2]+p[29]*plg[0][4])*cd32; - - /* SYMMETRICAL SEMIANNUAL */ - t[3]=(p[15]+p[16]*plg[0][2]+p[30]*plg[0][4])*cd18; - - /* ASYMMETRICAL ANNUAL */ - t[4]=(p[9]*plg[0][1]+p[10]*plg[0][3]+p[20]*plg[0][5])*cd14; - - /* ASYMMETRICAL SEMIANNUAL */ - t[5]=(p[37]*plg[0][1])*cd39; - - /* DIURNAL */ - if (flags->sw[7]) { - double t71, t72; - t71 = p[11]*plg[1][2]*cd14*flags->swc[5]; - t72 = p[12]*plg[1][2]*cd14*flags->swc[5]; - t[6] = ((p[3]*plg[1][1] + p[4]*plg[1][3] + t71) * ctloc + (p[6]*plg[1][1] + p[7]*plg[1][3] + t72) * stloc) ; - } - - /* SEMIDIURNAL */ - if (flags->sw[8]) { - double t81, t82; - t81 = (p[23]*plg[2][3]+p[35]*plg[2][5])*cd14*flags->swc[5]; - t82 = (p[33]*plg[2][3]+p[36]*plg[2][5])*cd14*flags->swc[5]; - t[7] = ((p[5]*plg[2][2] + p[41]*plg[2][4] + t81) * c2tloc + (p[8]*plg[2][2] + p[42]*plg[2][4] + t82) * s2tloc); - } - - /* TERDIURNAL */ - if (flags->sw[14]) { - t[13] = p[39] * plg[3][3] * s3tloc + p[40] * plg[3][3] * c3tloc; - } - - /* MAGNETIC ACTIVITY */ - if (flags->sw[9]) { - if (flags->sw[9]==1) - t[8] = apdf * (p[32] + p[45] * plg[0][2] * flags->swc[2]); - if (flags->sw[9]==-1) - t[8]=(p[50]*apt[0] + p[96]*plg[0][2] * apt[0]*flags->swc[2]); - } - - /* LONGITUDINAL */ - if (!((flags->sw[10]==0) || (flags->sw[11]==0) || (input->g_long<=-1000.0))) { - t[10] = (1.0 + plg[0][1]*(p[80]*flags->swc[5]*cos(dr*(input->doy-p[81]))\ - +p[85]*flags->swc[6]*cos(2.0*dr*(input->doy-p[86])))\ - +p[83]*flags->swc[3]*cos(dr*(input->doy-p[84]))\ - +p[87]*flags->swc[4]*cos(2.0*dr*(input->doy-p[88])))\ - *((p[64]*plg[1][2]+p[65]*plg[1][4]+p[66]*plg[1][6]\ - +p[74]*plg[1][1]+p[75]*plg[1][3]+p[76]*plg[1][5]\ - )*cos(dgtr*input->g_long)\ - +(p[90]*plg[1][2]+p[91]*plg[1][4]+p[92]*plg[1][6]\ - +p[77]*plg[1][1]+p[78]*plg[1][3]+p[79]*plg[1][5]\ - )*sin(dgtr*input->g_long)); - } - tt=0; - for (i=0;i<14;i++) - tt+=fabs(flags->sw[i+1])*t[i]; - return tt; -} - - - -/* ------------------------------------------------------------------- */ -/* ------------------------------- GTD7 ------------------------------ */ -/* ------------------------------------------------------------------- */ - -void gtd7(struct nrlmsise_input *input, struct nrlmsise_flags *flags, struct nrlmsise_output *output) { - double xlat; - double xmm; - int mn3 = 5; - double zn3[5]={32.5,20.0,15.0,10.0,0.0}; - int mn2 = 4; - double zn2[4]={72.5,55.0,45.0,32.5}; - double altt; - double zmix=62.5; - double tmp; - double dm28m; - double tz; - double dmc; - double dmr; - double dz28; - struct nrlmsise_output soutput; - int i; - - tselec(flags); - - /* Latitude variation of gravity (none for sw[2]=0) */ - xlat=input->g_lat; - if (flags->sw[2]==0) - xlat=45.0; - glatf(xlat, &gsurf, &re); - - xmm = pdm[2][4]; - - /* THERMOSPHERE / MESOSPHERE (above zn2[0]) */ - if (input->alt>zn2[0]) - altt=input->alt; - else - altt=zn2[0]; - - tmp=input->alt; - input->alt=altt; - gts7(input, flags, &soutput); - altt=input->alt; - input->alt=tmp; - if (flags->sw[0]) /* metric adjustment */ - dm28m=dm28*1.0E6; - else - dm28m=dm28; - output->t[0]=soutput.t[0]; - output->t[1]=soutput.t[1]; - if (input->alt>=zn2[0]) { - for (i=0;i<9;i++) - output->d[i]=soutput.d[i]; - return; - } - -/* LOWER MESOSPHERE/UPPER STRATOSPHERE (between zn3[0] and zn2[0]) - * Temperature at nodes and gradients at end nodes - * Inverse temperature a linear function of spherical harmonics - */ - meso_tgn2[0]=meso_tgn1[1]; - meso_tn2[0]=meso_tn1[4]; - meso_tn2[1]=pma[0][0]*pavgm[0]/(1.0-flags->sw[20]*glob7s(pma[0], input, flags)); - meso_tn2[2]=pma[1][0]*pavgm[1]/(1.0-flags->sw[20]*glob7s(pma[1], input, flags)); - meso_tn2[3]=pma[2][0]*pavgm[2]/(1.0-flags->sw[20]*flags->sw[22]*glob7s(pma[2], input, flags)); - meso_tgn2[1]=pavgm[8]*pma[9][0]*(1.0+flags->sw[20]*flags->sw[22]*glob7s(pma[9], input, flags))*meso_tn2[3]*meso_tn2[3]/(pow((pma[2][0]*pavgm[2]),2.0)); - meso_tn3[0]=meso_tn2[3]; - - if (input->alt<=zn3[0]) { -/* LOWER STRATOSPHERE AND TROPOSPHERE (below zn3[0]) - * Temperature at nodes and gradients at end nodes - * Inverse temperature a linear function of spherical harmonics - */ - meso_tgn3[0]=meso_tgn2[1]; - meso_tn3[1]=pma[3][0]*pavgm[3]/(1.0-flags->sw[22]*glob7s(pma[3], input, flags)); - meso_tn3[2]=pma[4][0]*pavgm[4]/(1.0-flags->sw[22]*glob7s(pma[4], input, flags)); - meso_tn3[3]=pma[5][0]*pavgm[5]/(1.0-flags->sw[22]*glob7s(pma[5], input, flags)); - meso_tn3[4]=pma[6][0]*pavgm[6]/(1.0-flags->sw[22]*glob7s(pma[6], input, flags)); - meso_tgn3[1]=pma[7][0]*pavgm[7]*(1.0+flags->sw[22]*glob7s(pma[7], input, flags)) *meso_tn3[4]*meso_tn3[4]/(pow((pma[6][0]*pavgm[6]),2.0)); - } - - /* LINEAR TRANSITION TO FULL MIXING BELOW zn2[0] */ - - dmc=0; - if (input->alt>zmix) - dmc = 1.0 - (zn2[0]-input->alt)/(zn2[0] - zmix); - dz28=soutput.d[2]; - - /**** N2 density ****/ - dmr=soutput.d[2] / dm28m - 1.0; - output->d[2]=densm(input->alt,dm28m,xmm, &tz, mn3, zn3, meso_tn3, meso_tgn3, mn2, zn2, meso_tn2, meso_tgn2); - output->d[2]=output->d[2] * (1.0 + dmr*dmc); - - /**** HE density ****/ - dmr = soutput.d[0] / (dz28 * pdm[0][1]) - 1.0; - output->d[0] = output->d[2] * pdm[0][1] * (1.0 + dmr*dmc); - - /**** O density ****/ - output->d[1] = 0; - output->d[8] = 0; - - /**** O2 density ****/ - dmr = soutput.d[3] / (dz28 * pdm[3][1]) - 1.0; - output->d[3] = output->d[2] * pdm[3][1] * (1.0 + dmr*dmc); - - /**** AR density ***/ - dmr = soutput.d[4] / (dz28 * pdm[4][1]) - 1.0; - output->d[4] = output->d[2] * pdm[4][1] * (1.0 + dmr*dmc); - - /**** Hydrogen density ****/ - output->d[6] = 0; - - /**** Atomic nitrogen density ****/ - output->d[7] = 0; - - /**** Total mass density */ - output->d[5] = 1.66E-24 * (4.0 * output->d[0] + 16.0 * output->d[1] + 28.0 * output->d[2] + 32.0 * output->d[3] + 40.0 * output->d[4] + output->d[6] + 14.0 * output->d[7]); - - if (flags->sw[0]) - output->d[5]=output->d[5]/1000; - - /**** temperature at altitude ****/ - dd = densm(input->alt, 1.0, 0, &tz, mn3, zn3, meso_tn3, meso_tgn3, mn2, zn2, meso_tn2, meso_tgn2); - output->t[1]=tz; - -} - - - -/* ------------------------------------------------------------------- */ -/* ------------------------------- GTD7D ----------------------------- */ -/* ------------------------------------------------------------------- */ - -void gtd7d(struct nrlmsise_input *input, struct nrlmsise_flags *flags, struct nrlmsise_output *output) { - gtd7(input, flags, output); - output->d[5] = 1.66E-24 * (4.0 * output->d[0] + 16.0 * output->d[1] + 28.0 * output->d[2] + 32.0 * output->d[3] + 40.0 * output->d[4] + output->d[6] + 14.0 * output->d[7] + 16.0 * output->d[8]); - if (flags->sw[0]) - output->d[5]=output->d[5]/1000; -} - - - -/* ------------------------------------------------------------------- */ -/* -------------------------------- GHP7 ----------------------------- */ -/* ------------------------------------------------------------------- */ - -void ghp7(struct nrlmsise_input *input, struct nrlmsise_flags *flags, struct nrlmsise_output *output, double press) { - double bm = 1.3806E-19; - double rgas = 831.4; - double test = 0.00043; - double ltest = 12; - double pl, p; - double zi; - double z; - double cl, cl2; - double ca, cd; - double xn, xm, diff; - double g, sh; - int l; - pl = log10(press); - if (pl >= -5.0) { - if (pl>2.5) - zi = 18.06 * (3.00 - pl); - else if ((pl>0.075) && (pl<=2.5)) - zi = 14.98 * (3.08 - pl); - else if ((pl>-1) && (pl<=0.075)) - zi = 17.80 * (2.72 - pl); - else if ((pl>-2) && (pl<=-1)) - zi = 14.28 * (3.64 - pl); - else if ((pl>-4) && (pl<=-2)) - zi = 12.72 * (4.32 -pl); - else - zi = 25.3 * (0.11 - pl); - cl = input->g_lat/90.0; - cl2 = cl*cl; - if (input->doy<182) - cd = (1.0 - (double) input->doy) / 91.25; - else - cd = ((double) input->doy) / 91.25 - 3.0; - ca = 0; - if ((pl > -1.11) && (pl<=-0.23)) - ca = 1.0; - if (pl > -0.23) - ca = (2.79 - pl) / (2.79 + 0.23); - if ((pl <= -1.11) && (pl>-3)) - ca = (-2.93 - pl)/(-2.93 + 1.11); - z = zi - 4.87 * cl * cd * ca - 1.64 * cl2 * ca + 0.31 * ca * cl; - } else - z = 22.0 * pow((pl + 4.0),2.0) + 110.0; - - /* iteration loop */ - l = 0; - do { - l++; - input->alt = z; - gtd7(input, flags, output); - z = input->alt; - xn = output->d[0] + output->d[1] + output->d[2] + output->d[3] + output->d[4] + output->d[6] + output->d[7]; - p = bm * xn * output->t[1]; - if (flags->sw[0]) - p = p*1.0E-6; - diff = pl - log10(p); - if (sqrt(diff*diff)d[5] / xn / 1.66E-24; - if (flags->sw[0]) - xm = xm * 1.0E3; - g = gsurf / (pow((1.0 + z/re),2.0)); - sh = rgas * output->t[1] / (xm * g); - - /* new altitude estimate using scale height */ - if (l < 6) - z = z - sh * diff * 2.302; - else - z = z - sh * diff; - } while (1==1); -} - - - -/* ------------------------------------------------------------------- */ -/* ------------------------------- GTS7 ------------------------------ */ -/* ------------------------------------------------------------------- */ - -void gts7(struct nrlmsise_input *input, struct nrlmsise_flags *flags, struct nrlmsise_output *output) { -/* Thermospheric portion of NRLMSISE-00 - * See GTD7 for more extensive comments - * alt > 72.5 km! - */ - double za; - int i, j; - double ddum, z; - double zn1[5] = {120.0, 110.0, 100.0, 90.0, 72.5}; - double tinf; - int mn1 = 5; - double g0; - double tlb; - double s; - double db01, db04, db14, db16, db28, db32, db40; - double zh28, zh04, zh16, zh32, zh40, zh01, zh14; - double zhm28, zhm04, zhm16, zhm32, zhm40, zhm01, zhm14; - double xmd; - double b28, b04, b16, b32, b40, b01, b14; - double tz; - double g28, g4, g16, g32, g40, g1, g14; - double zhf, xmm; - double zc04, zc16, zc32, zc40, zc01, zc14; - double hc04, hc16, hc32, hc40, hc01, hc14; - double hcc16, hcc32, hcc01, hcc14; - double zcc16, zcc32, zcc01, zcc14; - double rc16, rc32, rc01, rc14; - double rl; - double g16h, db16h, tho, zsht, zmho, zsho; - double dgtr=1.74533E-2; - double dr=1.72142E-2; - double alpha[9]={-0.38, 0.0, 0.0, 0.0, 0.17, 0.0, -0.38, 0.0, 0.0}; - double altl[8]={200.0, 300.0, 160.0, 250.0, 240.0, 450.0, 320.0, 450.0}; - double dd; - double hc216, hcc232; - za = pdl[1][15]; - zn1[0] = za; - for (j=0;j<9;j++) - output->d[j]=0; - - /* TINF VARIATIONS NOT IMPORTANT BELOW ZA OR ZN1(1) */ - if (input->alt>zn1[0]) - tinf = ptm[0]*pt[0] * \ - (1.0+flags->sw[16]*globe7(pt,input,flags)); - else - tinf = ptm[0]*pt[0]; - output->t[0]=tinf; - - /* GRADIENT VARIATIONS NOT IMPORTANT BELOW ZN1(5) */ - if (input->alt>zn1[4]) - g0 = ptm[3]*ps[0] * \ - (1.0+flags->sw[19]*globe7(ps,input,flags)); - else - g0 = ptm[3]*ps[0]; - tlb = ptm[1] * (1.0 + flags->sw[17]*globe7(pd[3],input,flags))*pd[3][0]; - s = g0 / (tinf - tlb); - -/* Lower thermosphere temp variations not significant for - * density above 300 km */ - if (input->alt<300.0) { - meso_tn1[1]=ptm[6]*ptl[0][0]/(1.0-flags->sw[18]*glob7s(ptl[0], input, flags)); - meso_tn1[2]=ptm[2]*ptl[1][0]/(1.0-flags->sw[18]*glob7s(ptl[1], input, flags)); - meso_tn1[3]=ptm[7]*ptl[2][0]/(1.0-flags->sw[18]*glob7s(ptl[2], input, flags)); - meso_tn1[4]=ptm[4]*ptl[3][0]/(1.0-flags->sw[18]*flags->sw[20]*glob7s(ptl[3], input, flags)); - meso_tgn1[1]=ptm[8]*pma[8][0]*(1.0+flags->sw[18]*flags->sw[20]*glob7s(pma[8], input, flags))*meso_tn1[4]*meso_tn1[4]/(pow((ptm[4]*ptl[3][0]),2.0)); - } else { - meso_tn1[1]=ptm[6]*ptl[0][0]; - meso_tn1[2]=ptm[2]*ptl[1][0]; - meso_tn1[3]=ptm[7]*ptl[2][0]; - meso_tn1[4]=ptm[4]*ptl[3][0]; - meso_tgn1[1]=ptm[8]*pma[8][0]*meso_tn1[4]*meso_tn1[4]/(pow((ptm[4]*ptl[3][0]),2.0)); - } - - /* N2 variation factor at Zlb */ - g28=flags->sw[21]*globe7(pd[2], input, flags); - - /* VARIATION OF TURBOPAUSE HEIGHT */ - zhf=pdl[1][24]*(1.0+flags->sw[5]*pdl[0][24]*sin(dgtr*input->g_lat)*cos(dr*(input->doy-pt[13]))); - output->t[0]=tinf; - xmm = pdm[2][4]; - z = input->alt; - - - /**** N2 DENSITY ****/ - - /* Diffusive density at Zlb */ - db28 = pdm[2][0]*exp(g28)*pd[2][0]; - /* Diffusive density at Alt */ - output->d[2]=densu(z,db28,tinf,tlb,28.0,alpha[2],&output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); - dd=output->d[2]; - /* Turbopause */ - zh28=pdm[2][2]*zhf; - zhm28=pdm[2][3]*pdl[1][5]; - xmd=28.0-xmm; - /* Mixed density at Zlb */ - b28=densu(zh28,db28,tinf,tlb,xmd,(alpha[2]-1.0),&tz,ptm[5],s,mn1, zn1,meso_tn1,meso_tgn1); - if ((flags->sw[15])&&(z<=altl[2])) { - /* Mixed density at Alt */ - dm28=densu(z,b28,tinf,tlb,xmm,alpha[2],&tz,ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); - /* Net density at Alt */ - output->d[2]=dnet(output->d[2],dm28,zhm28,xmm,28.0); - } - - - /**** HE DENSITY ****/ - - /* Density variation factor at Zlb */ - g4 = flags->sw[21]*globe7(pd[0], input, flags); - /* Diffusive density at Zlb */ - db04 = pdm[0][0]*exp(g4)*pd[0][0]; - /* Diffusive density at Alt */ - output->d[0]=densu(z,db04,tinf,tlb, 4.,alpha[0],&output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); - dd=output->d[0]; - if ((flags->sw[15]) && (zt[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); - /* Mixed density at Alt */ - dm04=densu(z,b04,tinf,tlb,xmm,0.,&output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); - zhm04=zhm28; - /* Net density at Alt */ - output->d[0]=dnet(output->d[0],dm04,zhm04,xmm,4.); - /* Correction to specified mixing ratio at ground */ - rl=log(b28*pdm[0][1]/b04); - zc04=pdm[0][4]*pdl[1][0]; - hc04=pdm[0][5]*pdl[1][1]; - /* Net density corrected at Alt */ - output->d[0]=output->d[0]*ccor(z,rl,hc04,zc04); - } - - - /**** O DENSITY ****/ - - /* Density variation factor at Zlb */ - g16= flags->sw[21]*globe7(pd[1],input,flags); - /* Diffusive density at Zlb */ - db16 = pdm[1][0]*exp(g16)*pd[1][0]; - /* Diffusive density at Alt */ - output->d[1]=densu(z,db16,tinf,tlb, 16.,alpha[1],&output->t[1],ptm[5],s,mn1, zn1,meso_tn1,meso_tgn1); - dd=output->d[1]; - if ((flags->sw[15]) && (z<=altl[1])) { - /* Turbopause */ - zh16=pdm[1][2]; - /* Mixed density at Zlb */ - b16=densu(zh16,db16,tinf,tlb,16.0-xmm,(alpha[1]-1.0), &output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); - /* Mixed density at Alt */ - dm16=densu(z,b16,tinf,tlb,xmm,0.,&output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); - zhm16=zhm28; - /* Net density at Alt */ - output->d[1]=dnet(output->d[1],dm16,zhm16,xmm,16.); - rl=pdm[1][1]*pdl[1][16]*(1.0+flags->sw[1]*pdl[0][23]*(input->f107A-150.0)); - hc16=pdm[1][5]*pdl[1][3]; - zc16=pdm[1][4]*pdl[1][2]; - hc216=pdm[1][5]*pdl[1][4]; - output->d[1]=output->d[1]*ccor2(z,rl,hc16,zc16,hc216); - /* Chemistry correction */ - hcc16=pdm[1][7]*pdl[1][13]; - zcc16=pdm[1][6]*pdl[1][12]; - rc16=pdm[1][3]*pdl[1][14]; - /* Net density corrected at Alt */ - output->d[1]=output->d[1]*ccor(z,rc16,hcc16,zcc16); - } - - - /**** O2 DENSITY ****/ - - /* Density variation factor at Zlb */ - g32= flags->sw[21]*globe7(pd[4], input, flags); - /* Diffusive density at Zlb */ - db32 = pdm[3][0]*exp(g32)*pd[4][0]; - /* Diffusive density at Alt */ - output->d[3]=densu(z,db32,tinf,tlb, 32.,alpha[3],&output->t[1],ptm[5],s,mn1, zn1,meso_tn1,meso_tgn1); - dd=output->d[3]; - if (flags->sw[15]) { - if (z<=altl[3]) { - /* Turbopause */ - zh32=pdm[3][2]; - /* Mixed density at Zlb */ - b32=densu(zh32,db32,tinf,tlb,32.-xmm,alpha[3]-1., &output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); - /* Mixed density at Alt */ - dm32=densu(z,b32,tinf,tlb,xmm,0.,&output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); - zhm32=zhm28; - /* Net density at Alt */ - output->d[3]=dnet(output->d[3],dm32,zhm32,xmm,32.); - /* Correction to specified mixing ratio at ground */ - rl=log(b28*pdm[3][1]/b32); - hc32=pdm[3][5]*pdl[1][7]; - zc32=pdm[3][4]*pdl[1][6]; - output->d[3]=output->d[3]*ccor(z,rl,hc32,zc32); - } - /* Correction for general departure from diffusive equilibrium above Zlb */ - hcc32=pdm[3][7]*pdl[1][22]; - hcc232=pdm[3][7]*pdl[0][22]; - zcc32=pdm[3][6]*pdl[1][21]; - rc32=pdm[3][3]*pdl[1][23]*(1.+flags->sw[1]*pdl[0][23]*(input->f107A-150.)); - /* Net density corrected at Alt */ - output->d[3]=output->d[3]*ccor2(z,rc32,hcc32,zcc32,hcc232); - } - - - /**** AR DENSITY ****/ - - /* Density variation factor at Zlb */ - g40= flags->sw[21]*globe7(pd[5],input,flags); - /* Diffusive density at Zlb */ - db40 = pdm[4][0]*exp(g40)*pd[5][0]; - /* Diffusive density at Alt */ - output->d[4]=densu(z,db40,tinf,tlb, 40.,alpha[4],&output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); - dd=output->d[4]; - if ((flags->sw[15]) && (z<=altl[4])) { - /* Turbopause */ - zh40=pdm[4][2]; - /* Mixed density at Zlb */ - b40=densu(zh40,db40,tinf,tlb,40.-xmm,alpha[4]-1.,&output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); - /* Mixed density at Alt */ - dm40=densu(z,b40,tinf,tlb,xmm,0.,&output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); - zhm40=zhm28; - /* Net density at Alt */ - output->d[4]=dnet(output->d[4],dm40,zhm40,xmm,40.); - /* Correction to specified mixing ratio at ground */ - rl=log(b28*pdm[4][1]/b40); - hc40=pdm[4][5]*pdl[1][9]; - zc40=pdm[4][4]*pdl[1][8]; - /* Net density corrected at Alt */ - output->d[4]=output->d[4]*ccor(z,rl,hc40,zc40); - } - - - /**** HYDROGEN DENSITY ****/ - - /* Density variation factor at Zlb */ - g1 = flags->sw[21]*globe7(pd[6], input, flags); - /* Diffusive density at Zlb */ - db01 = pdm[5][0]*exp(g1)*pd[6][0]; - /* Diffusive density at Alt */ - output->d[6]=densu(z,db01,tinf,tlb,1.,alpha[6],&output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); - dd=output->d[6]; - if ((flags->sw[15]) && (z<=altl[6])) { - /* Turbopause */ - zh01=pdm[5][2]; - /* Mixed density at Zlb */ - b01=densu(zh01,db01,tinf,tlb,1.-xmm,alpha[6]-1., &output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); - /* Mixed density at Alt */ - dm01=densu(z,b01,tinf,tlb,xmm,0.,&output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); - zhm01=zhm28; - /* Net density at Alt */ - output->d[6]=dnet(output->d[6],dm01,zhm01,xmm,1.); - /* Correction to specified mixing ratio at ground */ - rl=log(b28*pdm[5][1]*sqrt(pdl[1][17]*pdl[1][17])/b01); - hc01=pdm[5][5]*pdl[1][11]; - zc01=pdm[5][4]*pdl[1][10]; - output->d[6]=output->d[6]*ccor(z,rl,hc01,zc01); - /* Chemistry correction */ - hcc01=pdm[5][7]*pdl[1][19]; - zcc01=pdm[5][6]*pdl[1][18]; - rc01=pdm[5][3]*pdl[1][20]; - /* Net density corrected at Alt */ - output->d[6]=output->d[6]*ccor(z,rc01,hcc01,zcc01); -} - - - /**** ATOMIC NITROGEN DENSITY ****/ - - /* Density variation factor at Zlb */ - g14 = flags->sw[21]*globe7(pd[7],input,flags); - /* Diffusive density at Zlb */ - db14 = pdm[6][0]*exp(g14)*pd[7][0]; - /* Diffusive density at Alt */ - output->d[7]=densu(z,db14,tinf,tlb,14.,alpha[7],&output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); - dd=output->d[7]; - if ((flags->sw[15]) && (z<=altl[7])) { - /* Turbopause */ - zh14=pdm[6][2]; - /* Mixed density at Zlb */ - b14=densu(zh14,db14,tinf,tlb,14.-xmm,alpha[7]-1., &output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); - /* Mixed density at Alt */ - dm14=densu(z,b14,tinf,tlb,xmm,0.,&output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); - zhm14=zhm28; - /* Net density at Alt */ - output->d[7]=dnet(output->d[7],dm14,zhm14,xmm,14.); - /* Correction to specified mixing ratio at ground */ - rl=log(b28*pdm[6][1]*sqrt(pdl[0][2]*pdl[0][2])/b14); - hc14=pdm[6][5]*pdl[0][1]; - zc14=pdm[6][4]*pdl[0][0]; - output->d[7]=output->d[7]*ccor(z,rl,hc14,zc14); - /* Chemistry correction */ - hcc14=pdm[6][7]*pdl[0][4]; - zcc14=pdm[6][6]*pdl[0][3]; - rc14=pdm[6][3]*pdl[0][5]; - /* Net density corrected at Alt */ - output->d[7]=output->d[7]*ccor(z,rc14,hcc14,zcc14); - } - - - /**** Anomalous OXYGEN DENSITY ****/ - - g16h = flags->sw[21]*globe7(pd[8],input,flags); - db16h = pdm[7][0]*exp(g16h)*pd[8][0]; - tho = pdm[7][9]*pdl[0][6]; - dd=densu(z,db16h,tho,tho,16.,alpha[8],&output->t[1],ptm[5],s,mn1, zn1,meso_tn1,meso_tgn1); - zsht=pdm[7][5]; - zmho=pdm[7][4]; - zsho=scalh(zmho,16.0,tho); - output->d[8]=dd*exp(-zsht/zsho*(exp(-(z-zmho)/zsht)-1.)); - - - /* total mass density */ - output->d[5] = 1.66E-24*(4.0*output->d[0]+16.0*output->d[1]+28.0*output->d[2]+32.0*output->d[3]+40.0*output->d[4]+ output->d[6]+14.0*output->d[7]); - - - /* temperature */ - z = sqrt(input->alt*input->alt); - ddum = densu(z,1.0, tinf, tlb, 0.0, 0.0, &output->t[1], ptm[5], s, mn1, zn1, meso_tn1, meso_tgn1); - (void) ddum; /* silence gcc */ - if (flags->sw[0]) { - for(i=0;i<9;i++) - output->d[i]=output->d[i]*1.0E6; - output->d[5]=output->d[5]/1000; - } -} diff --git a/extern/nrlmsise/nrlmsise-00.h b/extern/nrlmsise/nrlmsise-00.h deleted file mode 100644 index d596a82..0000000 --- a/extern/nrlmsise/nrlmsise-00.h +++ /dev/null @@ -1,222 +0,0 @@ -/* -------------------------------------------------------------------- */ -/* --------- N R L M S I S E - 0 0 M O D E L 2 0 0 1 ---------- */ -/* -------------------------------------------------------------------- */ - -/* This file is part of the NRLMSISE-00 C source code package - release - * 20041227 - * - * The NRLMSISE-00 model was developed by Mike Picone, Alan Hedin, and - * Doug Drob. They also wrote a NRLMSISE-00 distribution package in - * FORTRAN which is available at - * http://uap-www.nrl.navy.mil/models_web/msis/msis_home.htm - * - * Dominik Brodowski implemented and maintains this C version. You can - * reach him at mail@brodo.de. See the file "DOCUMENTATION" for details, - * and check http://www.brodo.de/english/pub/nrlmsise/index.html for - * updated releases of this package. - */ - - - -/* ------------------------------------------------------------------- */ -/* ------------------------------- INPUT ----------------------------- */ -/* ------------------------------------------------------------------- */ - -struct nrlmsise_flags { - int switches[24]; - double sw[24]; - double swc[24]; -}; -/* - * Switches: to turn on and off particular variations use these switches. - * 0 is off, 1 is on, and 2 is main effects off but cross terms on. - * - * Standard values are 0 for switch 0 and 1 for switches 1 to 23. The - * array "switches" needs to be set accordingly by the calling program. - * The arrays sw and swc are set internally. - * - * switches[i]: - * i - explanation - * ----------------- - * 0 - output in meters and kilograms instead of centimeters and grams - * 1 - F10.7 effect on mean - * 2 - time independent - * 3 - symmetrical annual - * 4 - symmetrical semiannual - * 5 - asymmetrical annual - * 6 - asymmetrical semiannual - * 7 - diurnal - * 8 - semidiurnal - * 9 - daily ap [when this is set to -1 (!) the pointer - * ap_a in struct nrlmsise_input must - * point to a struct ap_array] - * 10 - all UT/long effects - * 11 - longitudinal - * 12 - UT and mixed UT/long - * 13 - mixed AP/UT/LONG - * 14 - terdiurnal - * 15 - departures from diffusive equilibrium - * 16 - all TINF var - * 17 - all TLB var - * 18 - all TN1 var - * 19 - all S var - * 20 - all TN2 var - * 21 - all NLB var - * 22 - all TN3 var - * 23 - turbo scale height var - */ - -struct ap_array { - double a[7]; -}; -/* Array containing the following magnetic values: - * 0 : daily AP - * 1 : 3 hr AP index for current time - * 2 : 3 hr AP index for 3 hrs before current time - * 3 : 3 hr AP index for 6 hrs before current time - * 4 : 3 hr AP index for 9 hrs before current time - * 5 : Average of eight 3 hr AP indicies from 12 to 33 hrs - * prior to current time - * 6 : Average of eight 3 hr AP indicies from 36 to 57 hrs - * prior to current time - */ - - -struct nrlmsise_input { - int year; /* year, currently ignored */ - int doy; /* day of year */ - double sec; /* seconds in day (UT) */ - double alt; /* altitude in kilometers */ - double g_lat; /* geodetic latitude */ - double g_long; /* geodetic longitude */ - double lst; /* local apparent solar time (hours), see note below */ - double f107A; /* 81 day average of F10.7 flux (centered on doy) */ - double f107; /* daily F10.7 flux for previous day */ - double ap; /* magnetic index(daily) */ - struct ap_array *ap_a; /* see above */ -}; -/* - * NOTES ON INPUT VARIABLES: - * UT, Local Time, and Longitude are used independently in the - * model and are not of equal importance for every situation. - * For the most physically realistic calculation these three - * variables should be consistent (lst=sec/3600 + g_long/15). - * The Equation of Time departures from the above formula - * for apparent local time can be included if available but - * are of minor importance. - * - * f107 and f107A values used to generate the model correspond - * to the 10.7 cm radio flux at the actual distance of the Earth - * from the Sun rather than the radio flux at 1 AU. The following - * site provides both classes of values: - * ftp://ftp.ngdc.noaa.gov/STP/SOLAR_DATA/SOLAR_RADIO/FLUX/ - * - * f107, f107A, and ap effects are neither large nor well - * established below 80 km and these parameters should be set to - * 150., 150., and 4. respectively. - */ - - - -/* ------------------------------------------------------------------- */ -/* ------------------------------ OUTPUT ----------------------------- */ -/* ------------------------------------------------------------------- */ - -struct nrlmsise_output { - double d[9]; /* densities */ - double t[2]; /* temperatures */ -}; -/* - * OUTPUT VARIABLES: - * d[0] - HE NUMBER DENSITY(CM-3) - * d[1] - O NUMBER DENSITY(CM-3) - * d[2] - N2 NUMBER DENSITY(CM-3) - * d[3] - O2 NUMBER DENSITY(CM-3) - * d[4] - AR NUMBER DENSITY(CM-3) - * d[5] - TOTAL MASS DENSITY(GM/CM3) [includes d[8] in td7d] - * d[6] - H NUMBER DENSITY(CM-3) - * d[7] - N NUMBER DENSITY(CM-3) - * d[8] - Anomalous oxygen NUMBER DENSITY(CM-3) - * t[0] - EXOSPHERIC TEMPERATURE - * t[1] - TEMPERATURE AT ALT - * - * - * O, H, and N are set to zero below 72.5 km - * - * t[0], Exospheric temperature, is set to global average for - * altitudes below 120 km. The 120 km gradient is left at global - * average value for altitudes below 72 km. - * - * d[5], TOTAL MASS DENSITY, is NOT the same for subroutines GTD7 - * and GTD7D - * - * SUBROUTINE GTD7 -- d[5] is the sum of the mass densities of the - * species labeled by indices 0-4 and 6-7 in output variable d. - * This includes He, O, N2, O2, Ar, H, and N but does NOT include - * anomalous oxygen (species index 8). - * - * SUBROUTINE GTD7D -- d[5] is the "effective total mass density - * for drag" and is the sum of the mass densities of all species - * in this model, INCLUDING anomalous oxygen. - */ - - - -/* ------------------------------------------------------------------- */ -/* --------------------------- PROTOTYPES ---------------------------- */ -/* ------------------------------------------------------------------- */ - -/* GTD7 */ -/* Neutral Atmosphere Empircial Model from the surface to lower - * exosphere. - */ -void gtd7 (struct nrlmsise_input *input, \ - struct nrlmsise_flags *flags, \ - struct nrlmsise_output *output); - - -/* GTD7D */ -/* This subroutine provides Effective Total Mass Density for output - * d[5] which includes contributions from "anomalous oxygen" which can - * affect satellite drag above 500 km. See the section "output" for - * additional details. - */ -void gtd7d(struct nrlmsise_input *input, \ - struct nrlmsise_flags *flags, \ - struct nrlmsise_output *output); - - -/* GTS7 */ -/* Thermospheric portion of NRLMSISE-00 - */ -void gts7 (struct nrlmsise_input *input, \ - struct nrlmsise_flags *flags, \ - struct nrlmsise_output *output); - - -/* GHP7 */ -/* To specify outputs at a pressure level (press) rather than at - * an altitude. - */ -void ghp7 (struct nrlmsise_input *input, \ - struct nrlmsise_flags *flags, \ - struct nrlmsise_output *output, \ - double press); - - - -/* ------------------------------------------------------------------- */ -/* ----------------------- COMPILATION TWEAKS ------------------------ */ -/* ------------------------------------------------------------------- */ - -/* "inlining" of functions */ -/* Some compilers (e.g. gcc) allow the inlining of functions into the - * calling routine. This means a lot of overhead can be removed, and - * the execution of the program runs much faster. However, the filesize - * and thus the loading time is increased. - */ -#ifdef INLINE -#define __inline_double static inline double -#else -#define __inline_double double -#endif diff --git a/extern/nrlmsise/nrlmsise-00_data.c b/extern/nrlmsise/nrlmsise-00_data.c deleted file mode 100644 index 0175e6c..0000000 --- a/extern/nrlmsise/nrlmsise-00_data.c +++ /dev/null @@ -1,740 +0,0 @@ -/* -------------------------------------------------------------------- */ -/* --------- N R L M S I S E - 0 0 M O D E L 2 0 0 1 ---------- */ -/* -------------------------------------------------------------------- */ - -/* This file is part of the NRLMSISE-00 C source code package - release - * 20041227 - * - * The NRLMSISE-00 model was developed by Mike Picone, Alan Hedin, and - * Doug Drob. They also wrote a NRLMSISE-00 distribution package in - * FORTRAN which is available at - * http://uap-www.nrl.navy.mil/models_web/msis/msis_home.htm - * - * Dominik Brodowski implemented and maintains this C version. You can - * reach him at mail@brodo.de. See the file "DOCUMENTATION" for details, - * and check http://www.brodo.de/english/pub/nrlmsise/index.html for - * updated releases of this package. - */ - - - -/* ------------------------------------------------------------------- */ -/* ------------------------ BLOCK DATA GTD7BK ------------------------ */ -/* ------------------------------------------------------------------- */ - -/* TEMPERATURE */ -double pt[150] = { - 9.86573E-01, 1.62228E-02, 1.55270E-02,-1.04323E-01,-3.75801E-03, - -1.18538E-03,-1.24043E-01, 4.56820E-03, 8.76018E-03,-1.36235E-01, - -3.52427E-02, 8.84181E-03,-5.92127E-03,-8.61650E+00, 0.00000E+00, - 1.28492E-02, 0.00000E+00, 1.30096E+02, 1.04567E-02, 1.65686E-03, - -5.53887E-06, 2.97810E-03, 0.00000E+00, 5.13122E-03, 8.66784E-02, - 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00,-7.27026E-06, - 0.00000E+00, 6.74494E+00, 4.93933E-03, 2.21656E-03, 2.50802E-03, - 0.00000E+00, 0.00000E+00,-2.08841E-02,-1.79873E+00, 1.45103E-03, - 2.81769E-04,-1.44703E-03,-5.16394E-05, 8.47001E-02, 1.70147E-01, - 5.72562E-03, 5.07493E-05, 4.36148E-03, 1.17863E-04, 4.74364E-03, - 6.61278E-03, 4.34292E-05, 1.44373E-03, 2.41470E-05, 2.84426E-03, - 8.56560E-04, 2.04028E-03, 0.00000E+00,-3.15994E+03,-2.46423E-03, - 1.13843E-03, 4.20512E-04, 0.00000E+00,-9.77214E+01, 6.77794E-03, - 5.27499E-03, 1.14936E-03, 0.00000E+00,-6.61311E-03,-1.84255E-02, - -1.96259E-02, 2.98618E+04, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 6.44574E+02, 8.84668E-04, 5.05066E-04, 0.00000E+00, 4.02881E+03, - -1.89503E-03, 0.00000E+00, 0.00000E+00, 8.21407E-04, 2.06780E-03, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - -1.20410E-02,-3.63963E-03, 9.92070E-05,-1.15284E-04,-6.33059E-05, - -6.05545E-01, 8.34218E-03,-9.13036E+01, 3.71042E-04, 0.00000E+00, - 4.19000E-04, 2.70928E-03, 3.31507E-03,-4.44508E-03,-4.96334E-03, - -1.60449E-03, 3.95119E-03, 2.48924E-03, 5.09815E-04, 4.05302E-03, - 2.24076E-03, 0.00000E+00, 6.84256E-03, 4.66354E-04, 0.00000E+00, - -3.68328E-04, 0.00000E+00, 0.00000E+00,-1.46870E+02, 0.00000E+00, - 0.00000E+00, 1.09501E-03, 4.65156E-04, 5.62583E-04, 3.21596E+00, - 6.43168E-04, 3.14860E-03, 3.40738E-03, 1.78481E-03, 9.62532E-04, - 5.58171E-04, 3.43731E+00,-2.33195E-01, 5.10289E-04, 0.00000E+00, - 0.00000E+00,-9.25347E+04, 0.00000E+00,-1.99639E-03, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 -}; - -double pd[9][150] = { -/* HE DENSITY */ { - 1.09979E+00,-4.88060E-02,-1.97501E-01,-9.10280E-02,-6.96558E-03, - 2.42136E-02, 3.91333E-01,-7.20068E-03,-3.22718E-02, 1.41508E+00, - 1.68194E-01, 1.85282E-02, 1.09384E-01,-7.24282E+00, 0.00000E+00, - 2.96377E-01,-4.97210E-02, 1.04114E+02,-8.61108E-02,-7.29177E-04, - 1.48998E-06, 1.08629E-03, 0.00000E+00, 0.00000E+00, 8.31090E-02, - 1.12818E-01,-5.75005E-02,-1.29919E-02,-1.78849E-02,-2.86343E-06, - 0.00000E+00,-1.51187E+02,-6.65902E-03, 0.00000E+00,-2.02069E-03, - 0.00000E+00, 0.00000E+00, 4.32264E-02,-2.80444E+01,-3.26789E-03, - 2.47461E-03, 0.00000E+00, 0.00000E+00, 9.82100E-02, 1.22714E-01, - -3.96450E-02, 0.00000E+00,-2.76489E-03, 0.00000E+00, 1.87723E-03, - -8.09813E-03, 4.34428E-05,-7.70932E-03, 0.00000E+00,-2.28894E-03, - -5.69070E-03,-5.22193E-03, 6.00692E-03,-7.80434E+03,-3.48336E-03, - -6.38362E-03,-1.82190E-03, 0.00000E+00,-7.58976E+01,-2.17875E-02, - -1.72524E-02,-9.06287E-03, 0.00000E+00, 2.44725E-02, 8.66040E-02, - 1.05712E-01, 3.02543E+04, 0.00000E+00, 0.00000E+00, 0.00000E+00, - -6.01364E+03,-5.64668E-03,-2.54157E-03, 0.00000E+00, 3.15611E+02, - -5.69158E-03, 0.00000E+00, 0.00000E+00,-4.47216E-03,-4.49523E-03, - 4.64428E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 4.51236E-02, 2.46520E-02, 6.17794E-03, 0.00000E+00, 0.00000E+00, - -3.62944E-01,-4.80022E-02,-7.57230E+01,-1.99656E-03, 0.00000E+00, - -5.18780E-03,-1.73990E-02,-9.03485E-03, 7.48465E-03, 1.53267E-02, - 1.06296E-02, 1.18655E-02, 2.55569E-03, 1.69020E-03, 3.51936E-02, - -1.81242E-02, 0.00000E+00,-1.00529E-01,-5.10574E-03, 0.00000E+00, - 2.10228E-03, 0.00000E+00, 0.00000E+00,-1.73255E+02, 5.07833E-01, - -2.41408E-01, 8.75414E-03, 2.77527E-03,-8.90353E-05,-5.25148E+00, - -5.83899E-03,-2.09122E-02,-9.63530E-03, 9.77164E-03, 4.07051E-03, - 2.53555E-04,-5.52875E+00,-3.55993E-01,-2.49231E-03, 0.00000E+00, - 0.00000E+00, 2.86026E+01, 0.00000E+00, 3.42722E-04, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 -}, /* O DENSITY */ { - 1.02315E+00,-1.59710E-01,-1.06630E-01,-1.77074E-02,-4.42726E-03, - 3.44803E-02, 4.45613E-02,-3.33751E-02,-5.73598E-02, 3.50360E-01, - 6.33053E-02, 2.16221E-02, 5.42577E-02,-5.74193E+00, 0.00000E+00, - 1.90891E-01,-1.39194E-02, 1.01102E+02, 8.16363E-02, 1.33717E-04, - 6.54403E-06, 3.10295E-03, 0.00000E+00, 0.00000E+00, 5.38205E-02, - 1.23910E-01,-1.39831E-02, 0.00000E+00, 0.00000E+00,-3.95915E-06, - 0.00000E+00,-7.14651E-01,-5.01027E-03, 0.00000E+00,-3.24756E-03, - 0.00000E+00, 0.00000E+00, 4.42173E-02,-1.31598E+01,-3.15626E-03, - 1.24574E-03,-1.47626E-03,-1.55461E-03, 6.40682E-02, 1.34898E-01, - -2.42415E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 6.13666E-04, - -5.40373E-03, 2.61635E-05,-3.33012E-03, 0.00000E+00,-3.08101E-03, - -2.42679E-03,-3.36086E-03, 0.00000E+00,-1.18979E+03,-5.04738E-02, - -2.61547E-03,-1.03132E-03, 1.91583E-04,-8.38132E+01,-1.40517E-02, - -1.14167E-02,-4.08012E-03, 1.73522E-04,-1.39644E-02,-6.64128E-02, - -6.85152E-02,-1.34414E+04, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 6.07916E+02,-4.12220E-03,-2.20996E-03, 0.00000E+00, 1.70277E+03, - -4.63015E-03, 0.00000E+00, 0.00000E+00,-2.25360E-03,-2.96204E-03, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 3.92786E-02, 1.31186E-02,-1.78086E-03, 0.00000E+00, 0.00000E+00, - -3.90083E-01,-2.84741E-02,-7.78400E+01,-1.02601E-03, 0.00000E+00, - -7.26485E-04,-5.42181E-03,-5.59305E-03, 1.22825E-02, 1.23868E-02, - 6.68835E-03,-1.03303E-02,-9.51903E-03, 2.70021E-04,-2.57084E-02, - -1.32430E-02, 0.00000E+00,-3.81000E-02,-3.16810E-03, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00,-9.05762E-04,-2.14590E-03,-1.17824E-03, 3.66732E+00, - -3.79729E-04,-6.13966E-03,-5.09082E-03,-1.96332E-03,-3.08280E-03, - -9.75222E-04, 4.03315E+00,-2.52710E-01, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 -}, /* N2 DENSITY */ { - 1.16112E+00, 0.00000E+00, 0.00000E+00, 3.33725E-02, 0.00000E+00, - 3.48637E-02,-5.44368E-03, 0.00000E+00,-6.73940E-02, 1.74754E-01, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.74712E+02, 0.00000E+00, - 1.26733E-01, 0.00000E+00, 1.03154E+02, 5.52075E-02, 0.00000E+00, - 0.00000E+00, 8.13525E-04, 0.00000E+00, 0.00000E+00, 8.66784E-02, - 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00,-2.50482E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-2.48894E-03, - 6.16053E-04,-5.79716E-04, 2.95482E-03, 8.47001E-02, 1.70147E-01, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 2.47425E-05, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 -}, /* TLB */ { - 9.44846E-01, 0.00000E+00, 0.00000E+00,-3.08617E-02, 0.00000E+00, - -2.44019E-02, 6.48607E-03, 0.00000E+00, 3.08181E-02, 4.59392E-02, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.74712E+02, 0.00000E+00, - 2.13260E-02, 0.00000E+00,-3.56958E+02, 0.00000E+00, 1.82278E-04, - 0.00000E+00, 3.07472E-04, 0.00000E+00, 0.00000E+00, 8.66784E-02, - 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 3.83054E-03, 0.00000E+00, 0.00000E+00, - -1.93065E-03,-1.45090E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00,-1.23493E-03, 1.36736E-03, 8.47001E-02, 1.70147E-01, - 3.71469E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 5.10250E-03, 2.47425E-05, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 3.68756E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 -}, /* O2 DENSITY */ { - 1.35580E+00, 1.44816E-01, 0.00000E+00, 6.07767E-02, 0.00000E+00, - 2.94777E-02, 7.46900E-02, 0.00000E+00,-9.23822E-02, 8.57342E-02, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.38636E+01, 0.00000E+00, - 7.71653E-02, 0.00000E+00, 8.18751E+01, 1.87736E-02, 0.00000E+00, - 0.00000E+00, 1.49667E-02, 0.00000E+00, 0.00000E+00, 8.66784E-02, - 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00,-3.67874E+02, 5.48158E-03, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 8.47001E-02, 1.70147E-01, - 1.22631E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 8.17187E-03, 3.71617E-05, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-2.10826E-03, - -3.13640E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - -7.35742E-02,-5.00266E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 1.94965E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 -}, /* AR DENSITY */ { - 1.04761E+00, 2.00165E-01, 2.37697E-01, 3.68552E-02, 0.00000E+00, - 3.57202E-02,-2.14075E-01, 0.00000E+00,-1.08018E-01,-3.73981E-01, - 0.00000E+00, 3.10022E-02,-1.16305E-03,-2.07596E+01, 0.00000E+00, - 8.64502E-02, 0.00000E+00, 9.74908E+01, 5.16707E-02, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 8.66784E-02, - 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 3.46193E+02, 1.34297E-02, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-3.48509E-03, - -1.54689E-04, 0.00000E+00, 0.00000E+00, 8.47001E-02, 1.70147E-01, - 1.47753E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 1.89320E-02, 3.68181E-05, 1.32570E-02, 0.00000E+00, 0.00000E+00, - 3.59719E-03, 7.44328E-03,-1.00023E-03,-6.50528E+03, 0.00000E+00, - 1.03485E-02,-1.00983E-03,-4.06916E-03,-6.60864E+01,-1.71533E-02, - 1.10605E-02, 1.20300E-02,-5.20034E-03, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - -2.62769E+03, 7.13755E-03, 4.17999E-03, 0.00000E+00, 1.25910E+04, - 0.00000E+00, 0.00000E+00, 0.00000E+00,-2.23595E-03, 4.60217E-03, - 5.71794E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - -3.18353E-02,-2.35526E-02,-1.36189E-02, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 2.03522E-02,-6.67837E+01,-1.09724E-03, 0.00000E+00, - -1.38821E-02, 1.60468E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.51574E-02, - -5.44470E-04, 0.00000E+00, 7.28224E-02, 6.59413E-02, 0.00000E+00, - -5.15692E-03, 0.00000E+00, 0.00000E+00,-3.70367E+03, 0.00000E+00, - 0.00000E+00, 1.36131E-02, 5.38153E-03, 0.00000E+00, 4.76285E+00, - -1.75677E-02, 2.26301E-02, 0.00000E+00, 1.76631E-02, 4.77162E-03, - 0.00000E+00, 5.39354E+00, 0.00000E+00,-7.51710E-03, 0.00000E+00, - 0.00000E+00,-8.82736E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 -}, /* H DENSITY */ { - 1.26376E+00,-2.14304E-01,-1.49984E-01, 2.30404E-01, 2.98237E-02, - 2.68673E-02, 2.96228E-01, 2.21900E-02,-2.07655E-02, 4.52506E-01, - 1.20105E-01, 3.24420E-02, 4.24816E-02,-9.14313E+00, 0.00000E+00, - 2.47178E-02,-2.88229E-02, 8.12805E+01, 5.10380E-02,-5.80611E-03, - 2.51236E-05,-1.24083E-02, 0.00000E+00, 0.00000E+00, 8.66784E-02, - 1.58727E-01,-3.48190E-02, 0.00000E+00, 0.00000E+00, 2.89885E-05, - 0.00000E+00, 1.53595E+02,-1.68604E-02, 0.00000E+00, 1.01015E-02, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.84552E-04, - -1.22181E-03, 0.00000E+00, 0.00000E+00, 8.47001E-02, 1.70147E-01, - -1.04927E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00,-5.91313E-03, - -2.30501E-02, 3.14758E-05, 0.00000E+00, 0.00000E+00, 1.26956E-02, - 8.35489E-03, 3.10513E-04, 0.00000E+00, 3.42119E+03,-2.45017E-03, - -4.27154E-04, 5.45152E-04, 1.89896E-03, 2.89121E+01,-6.49973E-03, - -1.93855E-02,-1.48492E-02, 0.00000E+00,-5.10576E-02, 7.87306E-02, - 9.51981E-02,-1.49422E+04, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 2.65503E+02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 6.37110E-03, 3.24789E-04, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 6.14274E-02, 1.00376E-02,-8.41083E-04, 0.00000E+00, 0.00000E+00, - 0.00000E+00,-1.27099E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, - -3.94077E-03,-1.28601E-02,-7.97616E-03, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00,-6.71465E-03,-1.69799E-03, 1.93772E-03, 3.81140E+00, - -7.79290E-03,-1.82589E-02,-1.25860E-02,-1.04311E-02,-3.02465E-03, - 2.43063E-03, 3.63237E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 -}, /* N DENSITY */ { - 7.09557E+01,-3.26740E-01, 0.00000E+00,-5.16829E-01,-1.71664E-03, - 9.09310E-02,-6.71500E-01,-1.47771E-01,-9.27471E-02,-2.30862E-01, - -1.56410E-01, 1.34455E-02,-1.19717E-01, 2.52151E+00, 0.00000E+00, - -2.41582E-01, 5.92939E-02, 4.39756E+00, 9.15280E-02, 4.41292E-03, - 0.00000E+00, 8.66807E-03, 0.00000E+00, 0.00000E+00, 8.66784E-02, - 1.58727E-01, 9.74701E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 6.70217E+01,-1.31660E-03, 0.00000E+00,-1.65317E-02, - 0.00000E+00, 0.00000E+00, 8.50247E-02, 2.77428E+01, 4.98658E-03, - 6.15115E-03, 9.50156E-03,-2.12723E-02, 8.47001E-02, 1.70147E-01, - -2.38645E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.37380E-03, - -8.41918E-03, 2.80145E-05, 7.12383E-03, 0.00000E+00,-1.66209E-02, - 1.03533E-04,-1.68898E-02, 0.00000E+00, 3.64526E+03, 0.00000E+00, - 6.54077E-03, 3.69130E-04, 9.94419E-04, 8.42803E+01,-1.16124E-02, - -7.74414E-03,-1.68844E-03, 1.42809E-03,-1.92955E-03, 1.17225E-01, - -2.41512E-02, 1.50521E+04, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 1.60261E+03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00,-3.54403E-04,-1.87270E-02, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 2.76439E-02, 6.43207E-03,-3.54300E-02, 0.00000E+00, 0.00000E+00, - 0.00000E+00,-2.80221E-02, 8.11228E+01,-6.75255E-04, 0.00000E+00, - -1.05162E-02,-3.48292E-03,-6.97321E-03, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00,-1.45546E-03,-1.31970E-02,-3.57751E-03,-1.09021E+00, - -1.50181E-02,-7.12841E-03,-6.64590E-03,-3.52610E-03,-1.87773E-02, - -2.22432E-03,-3.93895E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 -}, /* HOT O DENSITY */ { - 6.04050E-02, 1.57034E+00, 2.99387E-02, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-1.51018E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00,-8.61650E+00, 1.26454E-02, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 5.50878E-03, 0.00000E+00, 0.00000E+00, 8.66784E-02, - 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 6.23881E-02, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 8.47001E-02, 1.70147E-01, - -9.45934E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 -}}; -/* S PARAM */ -double ps[150] = { - 9.56827E-01, 6.20637E-02, 3.18433E-02, 0.00000E+00, 0.00000E+00, - 3.94900E-02, 0.00000E+00, 0.00000E+00,-9.24882E-03,-7.94023E-03, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.74712E+02, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 2.74677E-03, 0.00000E+00, 1.54951E-02, 8.66784E-02, - 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00,-6.99007E-04, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 1.24362E-02,-5.28756E-03, 8.47001E-02, 1.70147E-01, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 2.47425E-05, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 -}; - -/* TURBO */ -double pdl[2][25] = { - { 1.09930E+00, 3.90631E+00, 3.07165E+00, 9.86161E-01, 1.63536E+01, - 4.63830E+00, 1.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 1.28840E+00, 3.10302E-02, 1.18339E-01 }, - { 1.00000E+00, 7.00000E-01, 1.15020E+00, 3.44689E+00, 1.28840E+00, - 1.00000E+00, 1.08738E+00, 1.22947E+00, 1.10016E+00, 7.34129E-01, - 1.15241E+00, 2.22784E+00, 7.95046E-01, 4.01612E+00, 4.47749E+00, - 1.23435E+02,-7.60535E-02, 1.68986E-06, 7.44294E-01, 1.03604E+00, - 1.72783E+02, 1.15020E+00, 3.44689E+00,-7.46230E-01, 9.49154E-01 } -}; -/* LOWER BOUNDARY */ -double ptm[50] = { - 1.04130E+03, 3.86000E+02, 1.95000E+02, 1.66728E+01, 2.13000E+02, - 1.20000E+02, 2.40000E+02, 1.87000E+02,-2.00000E+00, 0.00000E+00 -}; -double pdm[8][10] = { -{ 2.45600E+07, 6.71072E-06, 1.00000E+02, 0.00000E+00, 1.10000E+02, - 1.00000E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 },\ -{ 8.59400E+10, 1.00000E+00, 1.05000E+02,-8.00000E+00, 1.10000E+02, - 1.00000E+01, 9.00000E+01, 2.00000E+00, 0.00000E+00, 0.00000E+00 },\ -{ 2.81000E+11, 0.00000E+00, 1.05000E+02, 2.80000E+01, 2.89500E+01, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 }, -{ 3.30000E+10, 2.68270E-01, 1.05000E+02, 1.00000E+00, 1.10000E+02, - 1.00000E+01, 1.10000E+02,-1.00000E+01, 0.00000E+00, 0.00000E+00 }, -{ 1.33000E+09, 1.19615E-02, 1.05000E+02, 0.00000E+00, 1.10000E+02, - 1.00000E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 }, -{ 1.76100E+05, 1.00000E+00, 9.50000E+01,-8.00000E+00, 1.10000E+02, - 1.00000E+01, 9.00000E+01, 2.00000E+00, 0.00000E+00, 0.00000E+00, }, -{ 1.00000E+07, 1.00000E+00, 1.05000E+02,-8.00000E+00, 1.10000E+02, - 1.00000E+01, 9.00000E+01, 2.00000E+00, 0.00000E+00, 0.00000E+00 }, -{ 1.00000E+06, 1.00000E+00, 1.05000E+02,-8.00000E+00, 5.50000E+02, - 7.60000E+01, 9.00000E+01, 2.00000E+00, 0.00000E+00, 4.00000E+03 }}; - - -double ptl[4][100] = { -/* TN1(2) */ { - 1.00858E+00, 4.56011E-02,-2.22972E-02,-5.44388E-02, 5.23136E-04, - -1.88849E-02, 5.23707E-02,-9.43646E-03, 6.31707E-03,-7.80460E-02, - -4.88430E-02, 0.00000E+00, 0.00000E+00,-7.60250E+00, 0.00000E+00, - -1.44635E-02,-1.76843E-02,-1.21517E+02, 2.85647E-02, 0.00000E+00, - 0.00000E+00, 6.31792E-04, 0.00000E+00, 5.77197E-03, 8.66784E-02, - 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00,-8.90272E+03, 3.30611E-03, 3.02172E-03, 0.00000E+00, - -2.13673E-03,-3.20910E-04, 0.00000E+00, 0.00000E+00, 2.76034E-03, - 2.82487E-03,-2.97592E-04,-4.21534E-03, 8.47001E-02, 1.70147E-01, - 8.96456E-03, 0.00000E+00,-1.08596E-02, 0.00000E+00, 0.00000E+00, - 5.57917E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 9.65405E-03, 0.00000E+00, 0.00000E+00, 2.00000E+00 -}, /* TN1(3) */ { - 9.39664E-01, 8.56514E-02,-6.79989E-03, 2.65929E-02,-4.74283E-03, - 1.21855E-02,-2.14905E-02, 6.49651E-03,-2.05477E-02,-4.24952E-02, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.19148E+01, 0.00000E+00, - 1.18777E-02,-7.28230E-02,-8.15965E+01, 1.73887E-02, 0.00000E+00, - 0.00000E+00, 0.00000E+00,-1.44691E-02, 2.80259E-04, 8.66784E-02, - 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 2.16584E+02, 3.18713E-03, 7.37479E-03, 0.00000E+00, - -2.55018E-03,-3.92806E-03, 0.00000E+00, 0.00000E+00,-2.89757E-03, - -1.33549E-03, 1.02661E-03, 3.53775E-04, 8.47001E-02, 1.70147E-01, - -9.17497E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 3.56082E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00,-1.00902E-02, 0.00000E+00, 0.00000E+00, 2.00000E+00 -}, /* TN1(4) */ { - 9.85982E-01,-4.55435E-02, 1.21106E-02, 2.04127E-02,-2.40836E-03, - 1.11383E-02,-4.51926E-02, 1.35074E-02,-6.54139E-03, 1.15275E-01, - 1.28247E-01, 0.00000E+00, 0.00000E+00,-5.30705E+00, 0.00000E+00, - -3.79332E-02,-6.24741E-02, 7.71062E-01, 2.96315E-02, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 6.81051E-03,-4.34767E-03, 8.66784E-02, - 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 1.07003E+01,-2.76907E-03, 4.32474E-04, 0.00000E+00, - 1.31497E-03,-6.47517E-04, 0.00000E+00,-2.20621E+01,-1.10804E-03, - -8.09338E-04, 4.18184E-04, 4.29650E-03, 8.47001E-02, 1.70147E-01, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - -4.04337E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-9.52550E-04, - 8.56253E-04, 4.33114E-04, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.21223E-03, - 2.38694E-04, 9.15245E-04, 1.28385E-03, 8.67668E-04,-5.61425E-06, - 1.04445E+00, 3.41112E+01, 0.00000E+00,-8.40704E-01,-2.39639E+02, - 7.06668E-01,-2.05873E+01,-3.63696E-01, 2.39245E+01, 0.00000E+00, - -1.06657E-03,-7.67292E-04, 1.54534E-04, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 -}, /* TN1(5) TN2(1) */ { - 1.00320E+00, 3.83501E-02,-2.38983E-03, 2.83950E-03, 4.20956E-03, - 5.86619E-04, 2.19054E-02,-1.00946E-02,-3.50259E-03, 4.17392E-02, - -8.44404E-03, 0.00000E+00, 0.00000E+00, 4.96949E+00, 0.00000E+00, - -7.06478E-03,-1.46494E-02, 3.13258E+01,-1.86493E-03, 0.00000E+00, - -1.67499E-02, 0.00000E+00, 0.00000E+00, 5.12686E-04, 8.66784E-02, - 1.58727E-01,-4.64167E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 4.37353E-03,-1.99069E+02, 0.00000E+00,-5.34884E-03, 0.00000E+00, - 1.62458E-03, 2.93016E-03, 2.67926E-03, 5.90449E+02, 0.00000E+00, - 0.00000E+00,-1.17266E-03,-3.58890E-04, 8.47001E-02, 1.70147E-01, - 0.00000E+00, 0.00000E+00, 1.38673E-02, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.60571E-03, - 6.28078E-04, 5.05469E-05, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-1.57829E-03, - -4.00855E-04, 5.04077E-05,-1.39001E-03,-2.33406E-03,-4.81197E-04, - 1.46758E+00, 6.20332E+00, 0.00000E+00, 3.66476E-01,-6.19760E+01, - 3.09198E-01,-1.98999E+01, 0.00000E+00,-3.29933E+02, 0.00000E+00, - -1.10080E-03,-9.39310E-05, 1.39638E-04, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 -} }; - -double pma[10][100] = { -/* TN2(2) */ { - 9.81637E-01,-1.41317E-03, 3.87323E-02, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-3.58707E-02, - -8.63658E-03, 0.00000E+00, 0.00000E+00,-2.02226E+00, 0.00000E+00, - -8.69424E-03,-1.91397E-02, 8.76779E+01, 4.52188E-03, 0.00000E+00, - 2.23760E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00,-7.07572E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, - -4.11210E-03, 3.50060E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00,-8.36657E-03, 1.61347E+01, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00,-1.45130E-02, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.24152E-03, - 6.43365E-04, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.33255E-03, - 2.42657E-03, 1.60666E-03,-1.85728E-03,-1.46874E-03,-4.79163E-06, - 1.22464E+00, 3.53510E+01, 0.00000E+00, 4.49223E-01,-4.77466E+01, - 4.70681E-01, 8.41861E+00,-2.88198E-01, 1.67854E+02, 0.00000E+00, - 7.11493E-04, 6.05601E-04, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 -}, /* TN2(3) */ { - 1.00422E+00,-7.11212E-03, 5.24480E-03, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-5.28914E-02, - -2.41301E-02, 0.00000E+00, 0.00000E+00,-2.12219E+01,-1.03830E-02, - -3.28077E-03, 1.65727E-02, 1.68564E+00,-6.68154E-03, 0.00000E+00, - 1.45155E-02, 0.00000E+00, 8.42365E-03, 0.00000E+00, 0.00000E+00, - 0.00000E+00,-4.34645E-03, 0.00000E+00, 0.00000E+00, 2.16780E-02, - 0.00000E+00,-1.38459E+02, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 7.04573E-03,-4.73204E+01, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 1.08767E-02, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-8.08279E-03, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 5.21769E-04, - -2.27387E-04, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 3.26769E-03, - 3.16901E-03, 4.60316E-04,-1.01431E-04, 1.02131E-03, 9.96601E-04, - 1.25707E+00, 2.50114E+01, 0.00000E+00, 4.24472E-01,-2.77655E+01, - 3.44625E-01, 2.75412E+01, 0.00000E+00, 7.94251E+02, 0.00000E+00, - 2.45835E-03, 1.38871E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 -}, /* TN2(4) TN3(1) */ { - 1.01890E+00,-2.46603E-02, 1.00078E-02, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-6.70977E-02, - -4.02286E-02, 0.00000E+00, 0.00000E+00,-2.29466E+01,-7.47019E-03, - 2.26580E-03, 2.63931E-02, 3.72625E+01,-6.39041E-03, 0.00000E+00, - 9.58383E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00,-1.85291E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 1.39717E+02, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 9.19771E-03,-3.69121E+02, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00,-1.57067E-02, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-7.07265E-03, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-2.92953E-03, - -2.77739E-03,-4.40092E-04, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.47280E-03, - 2.95035E-04,-1.81246E-03, 2.81945E-03, 4.27296E-03, 9.78863E-04, - 1.40545E+00,-6.19173E+00, 0.00000E+00, 0.00000E+00,-7.93632E+01, - 4.44643E-01,-4.03085E+02, 0.00000E+00, 1.15603E+01, 0.00000E+00, - 2.25068E-03, 8.48557E-04,-2.98493E-04, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 -}, /* TN3(2) */ { - 9.75801E-01, 3.80680E-02,-3.05198E-02, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 3.85575E-02, - 5.04057E-02, 0.00000E+00, 0.00000E+00,-1.76046E+02, 1.44594E-02, - -1.48297E-03,-3.68560E-03, 3.02185E+01,-3.23338E-03, 0.00000E+00, - 1.53569E-02, 0.00000E+00,-1.15558E-02, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 4.89620E-03, 0.00000E+00, 0.00000E+00,-1.00616E-02, - -8.21324E-03,-1.57757E+02, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 6.63564E-03, 4.58410E+01, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00,-2.51280E-02, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 9.91215E-03, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-8.73148E-04, - -1.29648E-03,-7.32026E-05, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-4.68110E-03, - -4.66003E-03,-1.31567E-03,-7.39390E-04, 6.32499E-04,-4.65588E-04, - -1.29785E+00,-1.57139E+02, 0.00000E+00, 2.58350E-01,-3.69453E+01, - 4.10672E-01, 9.78196E+00,-1.52064E-01,-3.85084E+03, 0.00000E+00, - -8.52706E-04,-1.40945E-03,-7.26786E-04, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 -}, /* TN3(3) */ { - 9.60722E-01, 7.03757E-02,-3.00266E-02, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.22671E-02, - 4.10423E-02, 0.00000E+00, 0.00000E+00,-1.63070E+02, 1.06073E-02, - 5.40747E-04, 7.79481E-03, 1.44908E+02, 1.51484E-04, 0.00000E+00, - 1.97547E-02, 0.00000E+00,-1.41844E-02, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 5.77884E-03, 0.00000E+00, 0.00000E+00, 9.74319E-03, - 0.00000E+00,-2.88015E+03, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00,-4.44902E-03,-2.92760E+01, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 2.34419E-02, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 5.36685E-03, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-4.65325E-04, - -5.50628E-04, 3.31465E-04, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-2.06179E-03, - -3.08575E-03,-7.93589E-04,-1.08629E-04, 5.95511E-04,-9.05050E-04, - 1.18997E+00, 4.15924E+01, 0.00000E+00,-4.72064E-01,-9.47150E+02, - 3.98723E-01, 1.98304E+01, 0.00000E+00, 3.73219E+03, 0.00000E+00, - -1.50040E-03,-1.14933E-03,-1.56769E-04, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 -}, /* TN3(4) */ { - 1.03123E+00,-7.05124E-02, 8.71615E-03, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-3.82621E-02, - -9.80975E-03, 0.00000E+00, 0.00000E+00, 2.89286E+01, 9.57341E-03, - 0.00000E+00, 0.00000E+00, 8.66153E+01, 7.91938E-04, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 4.68917E-03, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 7.86638E-03, 0.00000E+00, 0.00000E+00, 9.90827E-03, - 0.00000E+00, 6.55573E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00,-4.00200E+01, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 7.07457E-03, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 5.72268E-03, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-2.04970E-04, - 1.21560E-03,-8.05579E-06, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-2.49941E-03, - -4.57256E-04,-1.59311E-04, 2.96481E-04,-1.77318E-03,-6.37918E-04, - 1.02395E+00, 1.28172E+01, 0.00000E+00, 1.49903E-01,-2.63818E+01, - 0.00000E+00, 4.70628E+01,-2.22139E-01, 4.82292E-02, 0.00000E+00, - -8.67075E-04,-5.86479E-04, 5.32462E-04, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 -}, /* TN3(5) SURFACE TEMP TSL */ { - 1.00828E+00,-9.10404E-02,-2.26549E-02, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-2.32420E-02, - -9.08925E-03, 0.00000E+00, 0.00000E+00, 3.36105E+01, 0.00000E+00, - 0.00000E+00, 0.00000E+00,-1.24957E+01,-5.87939E-03, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 2.79765E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.01237E+03, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00,-1.75553E-02, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 3.29699E-03, - 1.26659E-03, 2.68402E-04, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.17894E-03, - 1.48746E-03, 1.06478E-04, 1.34743E-04,-2.20939E-03,-6.23523E-04, - 6.36539E-01, 1.13621E+01, 0.00000E+00,-3.93777E-01, 2.38687E+03, - 0.00000E+00, 6.61865E+02,-1.21434E-01, 9.27608E+00, 0.00000E+00, - 1.68478E-04, 1.24892E-03, 1.71345E-03, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 -}, /* TGN3(2) SURFACE GRAD TSLG */ { - 1.57293E+00,-6.78400E-01, 6.47500E-01, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-7.62974E-02, - -3.60423E-01, 0.00000E+00, 0.00000E+00, 1.28358E+02, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 4.68038E+01, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00,-1.67898E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 2.90994E+04, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 3.15706E+01, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 -}, /* TGN2(1) TGN1(2) */ { - 8.60028E-01, 3.77052E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-1.17570E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 7.77757E-03, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 1.01024E+02, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 6.54251E+02, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-1.56959E-02, - 1.91001E-02, 3.15971E-02, 1.00982E-02,-6.71565E-03, 2.57693E-03, - 1.38692E+00, 2.82132E-01, 0.00000E+00, 0.00000E+00, 3.81511E+02, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 -}, /* TGN3(1) TGN2(2) */ { - 1.06029E+00,-5.25231E-02, 3.73034E-01, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 3.31072E-02, - -3.88409E-01, 0.00000E+00, 0.00000E+00,-1.65295E+02,-2.13801E-01, - -4.38916E-02,-3.22716E-01,-8.82393E+01, 1.18458E-01, 0.00000E+00, - -4.35863E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00,-1.19782E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 2.62229E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00,-5.37443E+01, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00,-4.55788E-01, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 3.84009E-02, - 3.96733E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 5.05494E-02, - 7.39617E-02, 1.92200E-02,-8.46151E-03,-1.34244E-02, 1.96338E-02, - 1.50421E+00, 1.88368E+01, 0.00000E+00, 0.00000E+00,-5.13114E+01, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 5.11923E-02, 3.61225E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 -} }; - -/* SEMIANNUAL MULT SAM */ -double sam[100] = { - 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, - 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, - 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, - 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, - 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, - 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, - 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, - 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, - 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, - 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, - 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 -}; - - -/* MIDDLE ATMOSPHERE AVERAGES */ -double pavgm[10] = { - 2.61000E+02, 2.64000E+02, 2.29000E+02, 2.17000E+02, 2.17000E+02, - 2.23000E+02, 2.86760E+02,-2.93940E+00, 2.50000E+00, 0.00000E+00 }; - diff --git a/mkdocs.yml b/mkdocs.yml index 6d26693..24392de 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -9,24 +9,26 @@ theme: palette: - media: "(prefers-color-scheme: light)" scheme: default - primary: indigo - accent: indigo + primary: blue grey + accent: blue grey toggle: icon: material/brightness-7 name: Switch to dark mode - media: "(prefers-color-scheme: dark)" scheme: slate - primary: indigo - accent: indigo + primary: blue grey + accent: blue grey toggle: icon: material/brightness-4 name: Switch to light mode features: - - content.code.copy - - navigation.instant + - navigation.tabs + - navigation.tabs.sticky - navigation.sections - navigation.expand - navigation.top + - content.code.copy + - content.code.annotate - search.highlight - search.suggest - toc.follow @@ -89,11 +91,12 @@ nav: - Two-Line Elements: guide/tle.md - ITRF Coordinates: guide/itrfcoord.md - Orbit Propagation: guide/satprop.md + - Lambert Targeting: guide/lambert.md - Satellite State: guide/satstate.md - Atmospheric Density: guide/density.md - References: guide/references.md - Tutorials: - - tutorials/index.md + - Overview: tutorials/index.md - ITRF Coordinates: tutorials/ITRF Coordinates.ipynb - Satellite Ground Contacts: tutorials/Satellite Ground Contacts.ipynb - Two-Line Element Set: tutorials/Two-Line Element Set.ipynb @@ -104,6 +107,7 @@ nav: - High Precision Propagation: tutorials/High Precision Propagation.ipynb - Orbital Mean-Element Message: tutorials/Orbital Mean-Element Message.ipynb - Optical Observations: tutorials/Optical Observations of Satellites.ipynb + - Lambert Targeting: tutorials/Lambert Targeting.ipynb - API Reference: - Overview: api/index.md - time: api/time.md @@ -120,5 +124,6 @@ nav: - Density: api/density.md - Gravity: api/gravity.md - Kepler: api/kepler.md + - Lambert: api/lambert.md - Constants: api/consts.md - Utilities: api/utils.md diff --git a/pyproject.toml b/pyproject.toml index 968a869..cb0269b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ requires-python = ">= 3.10" authors = [{ name = "Steven Michael", email = "ssmichael@gmail.com" }] maintainers = [{ name = "Steven Michael", email = "ssmichael@gmail.com" }] readme = "README.md" -version = "0.13.0" +version = "0.14.1" license = "MIT" description = "Satellite Orbital Dynamics Toolkit" keywords = [ diff --git a/python/Cargo.toml b/python/Cargo.toml index 7c9e1db..21ac410 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "satkit-python" -version = "0.13.0" +version = "0.14.1" edition = "2021" publish = false @@ -12,8 +12,7 @@ crate-type = ["cdylib"] satkit = { path = ".." } pyo3 = { version = "0.28.2", features = ["extension-module", "anyhow"] } numpy = "0.28" -nalgebra = "0.34.0" -ndarray = "0.17.1" +numeris = { version = "0.5.6", features = ["serde", "estimate"] } anyhow = "1" serde = { version = "1.0", features = ["derive"] } serde-pickle = "1.2.0" diff --git a/python/satkit/__init__.pyi b/python/satkit/__init__.pyi index 3cbe93c..c90347a 100644 --- a/python/satkit/__init__.pyi +++ b/python/satkit/__init__.pyi @@ -32,6 +32,7 @@ __all__ = [ "planets", "satstate", "propagate", + "lambert", "propsettings", "integrator", "satproperties_static", diff --git a/python/satkit/density.pyi b/python/satkit/density.pyi index a7e4fcd..8bc7f15 100644 --- a/python/satkit/density.pyi +++ b/python/satkit/density.pyi @@ -16,10 +16,10 @@ def nrlmsise(itrf: satkit.itrfcoord, time: satkit.time | None) -> tuple[float, f """ NRL MSISE-00 Atmosphere Density Model - https://en.wikipedia.org/wiki/NRLMSISE-00 + or for more detail: - https://agupubs.onlinelibrary.wiley.com/doi/full/10.1029/2002JA009430 + Args: @@ -53,10 +53,10 @@ def nrlmsise( """ NRL MSISE-00 Atmosphere Density Model - https://en.wikipedia.org/wiki/NRLMSISE-00 + or for more detail: - https://agupubs.onlinelibrary.wiley.com/doi/full/10.1029/2002JA009430 + Args: altitude_meters (float): Altitude in meters @@ -71,3 +71,34 @@ def nrlmsise( tuple: (rho, T) where rho is mass density in kg/m^3 and T is temperature in Kelvin """ ... + +def nrlmsise(*args, **kwargs): + """ + NRL MSISE-00 Atmosphere Density Model + + + + or for more detail: + + + Args: + + itrf (satkit.itrfcoord): position at which to compute density & temperature + time (satkit.time|numpy.ndarray|list): Optional instant(s) at which to compute density & temperature. + "Space weather" data at this time will be used in model + computation. Note: at satellite altitudes, density can + change by > 10 X depending on solar cycle + + Returns: + (rho, T) where rho is mass density in kg/m^3 and T is temperature in Kelvin + + Example: + ```python + t = satkit.time(2024, 1, 1) + coord = satkit.itrfcoord(latitude_deg=0, longitude_deg=0, altitude=400e3) + rho, temp = satkit.density.nrlmsise(coord, t) + print(f"Density: {rho:.2e} kg/m^3") + print(f"Temperature: {temp:.1f} K") + ``` + """ + ... diff --git a/python/satkit/frametransform.pyi b/python/satkit/frametransform.pyi index 6ccf86d..4f5efbc 100644 --- a/python/satkit/frametransform.pyi +++ b/python/satkit/frametransform.pyi @@ -2,10 +2,10 @@ Transformations between coordinate frames, and associated utility functions Coordinate frame transforms are mostly pulled from Vallado: -https://www.google.com/books/edition/Fundamentals_of_Astrodynamics_and_Applic/PJLlWzMBKjkC?hl=en&gbpv=0 + or the IERS: -https://www.iers.org/ + """ @@ -22,9 +22,9 @@ def gmst(tm: time | datetime.datetime) -> float: """Greenwich Mean Sidereal Time Notes: - * GMST is the angle between the vernal equinox and the Greenwich meridian - * Vallado algorithm 15 - * GMST = 67310.5481 + (876600h + 8640184.812866) * tᵤₜ₁ * (0.983104 + tᵤₜ₁ * −6.2e−6) + - GMST is the angle between the vernal equinox and the Greenwich meridian + - Vallado algorithm 15 + - GMST = 67310.5481 + (876600h + 8640184.812866) * tᵤₜ₁ * (0.983104 + tᵤₜ₁ * −6.2e−6) Args: tm (satkit.time | datetime.datetime): scalar time at which to calculate output @@ -49,9 +49,9 @@ def gmst( """Greenwich Mean Sidereal Time Notes: - * GMST is the angle between the vernal equinox and the Greenwich meridian - * Vallado algorithm 15 - * GMST = 67310.5481 + (876600h + 8640184.812866) * tᵤₜ₁ * (0.983104 + tᵤₜ₁ * −6.2e−6) + - GMST is the angle between the vernal equinox and the Greenwich meridian + - Vallado algorithm 15 + - GMST = 67310.5481 + (876600h + 8640184.812866) * tᵤₜ₁ * (0.983104 + tᵤₜ₁ * −6.2e−6) Args: tm (satkit.time | npt.ArrayLike[satkit.time] | datetime.datetime | npt.ArrayLike[datetime.datetime]): scalar, list, or numpy array of astro.time or datetime.datetime representing time at which to calculate output @@ -61,6 +61,30 @@ def gmst( """ ... +def gmst(*args, **kwargs): + """Greenwich Mean Sidereal Time + + Notes: + - GMST is the angle between the vernal equinox and the Greenwich meridian + - Vallado algorithm 15 + - GMST = 67310.5481 + (876600h + 8640184.812866) * tᵤₜ₁ * (0.983104 + tᵤₜ₁ * −6.2e−6) + + Args: + tm (satkit.time | datetime.datetime): scalar time at which to calculate output + + Returns: + Greenwich Mean Sidereal Time, radians, at input time + + Example: + ```python + import math + t = satkit.time(2024, 1, 1) + theta = satkit.frametransform.gmst(t) + print(f"GMST: {math.degrees(theta):.4f} deg") + ``` + """ + ... + @typing.overload def gast( tm: time | datetime.datetime, @@ -95,6 +119,23 @@ def gast( """ ... +def gast(*args, **kwargs): + """Greenwich Apparent Sidereal Time + + Args: + tm (satkit.time): scalar, list, or numpy array of astro.time or datetime.datetime representing time at which to calculate output + + Returns: + Greenwich apparent sidereal time, radians, at input time(s) + + Example: + ```python + t = satkit.time(2024, 1, 1) + theta = satkit.frametransform.gast(t) + ``` + """ + ... + @typing.overload def earth_rotation_angle( tm: time | datetime.datetime, @@ -102,11 +143,11 @@ def earth_rotation_angle( """Earth Rotation Angle Notes: - * See: IERS Technical Note 36, Chapter 5, Equation 5.15 - * Calculation Details: - * Let t be UT1 Julian date - * let f be fractional component of t (fraction of day) - * ERA = 2𝜋 ((0.7790572732640 + f + 0.00273781191135448 * (t - 2451545.0)) + - See: IERS Technical Note 36, Chapter 5, Equation 5.15 + - Calculation Details: + - Let t be UT1 Julian date + - let f be fractional component of t (fraction of day) + - ERA = 2𝜋 ((0.7790572732640 + f + 0.00273781191135448 * (t - 2451545.0)) Args: tm (satkit.time|datetime.datetime: Time[s] at which to calculate Earth Rotation Angle @@ -129,11 +170,11 @@ def earth_rotation_angle( """Earth Rotation Angle Notes: - * See: IERS Technical Note 36, Chapter 5, Equation 5.15 - * Calculation Details: - * Let t be UT1 Julian date - * let f be fractional component of t (fraction of day) - * ERA = 2𝜋 ((0.7790572732640 + f + 0.00273781191135448 * (t - 2451545.0) + - See: IERS Technical Note 36, Chapter 5, Equation 5.15 + - Calculation Details: + - Let t be UT1 Julian date + - let f be fractional component of t (fraction of day) + - ERA = 2𝜋 ((0.7790572732640 + f + 0.00273781191135448 * (t - 2451545.0) Args: tm (npt.ArrayLike[datetime.datetime] | npt.ArrayLike[time]): list, or numpy array of astro.time or datetime.datetime representing time at which to calculate output @@ -143,6 +184,30 @@ def earth_rotation_angle( """ ... +def earth_rotation_angle(*args, **kwargs): + """Earth Rotation Angle + + Notes: + - See: IERS Technical Note 36, Chapter 5, Equation 5.15 + - Calculation Details: + - Let t be UT1 Julian date + - let f be fractional component of t (fraction of day) + - ERA = 2𝜋 ((0.7790572732640 + f + 0.00273781191135448 * (t - 2451545.0)) + + Args: + tm (satkit.time|datetime.datetime: Time[s] at which to calculate Earth Rotation Angle + + Returns: + Earth Rotation Angle at input time[s] in radians + + Example: + ```python + t = satkit.time(2024, 1, 1) + era = satkit.frametransform.earth_rotation_angle(t) + ``` + """ + ... + @typing.overload def qitrf2tirs( tm: time, @@ -171,6 +236,17 @@ def qitrf2tirs( """ ... +def qitrf2tirs(*args, **kwargs): + """Rotation from Terrestrial Intermediate Reference System to Celestial Intermediate Reference Systems + + Args: + tm (satkit.time | npt.ArrayLike[satkit.time] | datetime.datetime | npt.ArrayLike[datetime.datetime]): Time[s] at which to calculate the quaternion + + Returns: + Quaternion representing rotation from ITRF to TIRS at input time(s) + """ + ... + @typing.overload def qteme2gcrf( tm: time | datetime.datetime, @@ -205,6 +281,23 @@ def qteme2gcrf( """ ... +def qteme2gcrf(*args, **kwargs): + """Rotation from True Equator Mean Equinox (TEME) to Geocentric Celestial Reference Frame (GCRF) + + Args: + tm (satkit.time| datetime.datetime ): Time[s] at which to calculate the quaternion + + Returns: + Quaternion representing rotation from TEME to GCRF at input time(s) + + Example: + ```python + t = satkit.time(2024, 1, 1) + q = satkit.frametransform.qteme2gcrf(t) + ``` + """ + ... + @typing.overload def qcirs2gcrf( tm: time | datetime.datetime, @@ -233,6 +326,17 @@ def qcirs2gcrf( """ ... +def qcirs2gcrf(*args, **kwargs): + """Rotation from Celestial Intermediate Reference System to Geocentric Celestial Reference Frame + + Args: + tm (satkit.time | npt.ArrayLike[satkit.time] | datetime.datetime | npt.ArrayLike[datetime.datetime]): Time[s] at which to calculate the quaternion + + Returns: + Quaternion representing rotation from CIRS to GCRF at input time(s) + """ + ... + @typing.overload def qtirs2cirs( tm: time | datetime.datetime, @@ -261,6 +365,17 @@ def qtirs2cirs( """ ... +def qtirs2cirs(*args, **kwargs): + """Rotation from Terrestrial Intermediate Reference System (TIRS) to the Celestial Intermediate Reference System (CIRS) + + Args: + tm (satkit.time | datetime.datetime): Time[s] at which to calculate the quaternion + + Returns: + Quaternion representing rotation from TIRS to CIRS at input time(s) + """ + ... + @typing.overload def qgcrf2itrf_approx( tm: time | datetime.datetime, @@ -268,7 +383,7 @@ def qgcrf2itrf_approx( """Quaternion representing approximate rotation from the Geocentric Celestial Reference Frame (GCRF) to the International Terrestrial Reference Frame (ITRF) Notes: - * Accurate to approx. 1 arcsec + - Accurate to approx. 1 arcsec Args: tm (satkit.time | datetime.datetime): Time[s] at which to calculate the quaternion @@ -285,7 +400,7 @@ def qgcrf2itrf_approx( """Quaternion representing approximate rotation from the Geocentric Celestial Reference Frame (GCRF) to the International Terrestrial Reference Frame (ITRF) Notes: - * Accurate to approx. 1 arcsec + - Accurate to approx. 1 arcsec Args: tm (npt.ArrayLike[satkit.time] | npt.ArrayLike[datetime.datetime]): Time[s] at which to calculate the quaternion @@ -295,6 +410,20 @@ def qgcrf2itrf_approx( """ ... +def qgcrf2itrf_approx(*args, **kwargs): + """Quaternion representing approximate rotation from the Geocentric Celestial Reference Frame (GCRF) to the International Terrestrial Reference Frame (ITRF) + + Notes: + - Accurate to approx. 1 arcsec + + Args: + tm (satkit.time | datetime.datetime): Time[s] at which to calculate the quaternion + + Returns: + Quaternion representing rotation from GCRF to ITRF at input time(s) + """ + ... + @typing.overload def qitrf2gcrf_approx( tm: time | datetime.datetime, @@ -302,7 +431,7 @@ def qitrf2gcrf_approx( """Quaternion representing approximate rotation from the International Terrestrial Reference Frame (ITRF) to the Geocentric Celestial Reference Frame (GCRF) Notes: - * Accurate to approx. 1 arcsec + - Accurate to approx. 1 arcsec Args: tm (satkit.time | datetime.datetime): Time[s] at which to calculate the quaternion @@ -319,7 +448,7 @@ def qitrf2gcrf_approx( """Quaternion representing approximate rotation from the International Terrestrial Reference Frame (ITRF) to the Geocentric Celestial Reference Frame (GCRF) Notes: - * Accurate to approx. 1 arcsec + - Accurate to approx. 1 arcsec Args: tm (npt.ArrayLike[satkit.time] | npt.ArrayLike[datetime.datetime]): Time[s] at which to calculate the quaternion @@ -329,6 +458,20 @@ def qitrf2gcrf_approx( """ ... +def qitrf2gcrf_approx(*args, **kwargs): + """Quaternion representing approximate rotation from the International Terrestrial Reference Frame (ITRF) to the Geocentric Celestial Reference Frame (GCRF) + + Notes: + - Accurate to approx. 1 arcsec + + Args: + tm (satkit.time | datetime.datetime): Time[s] at which to calculate the quaternion + + Returns: + Quaternion representing rotation from ITRF to GCRF at input time(s) + """ + ... + @typing.overload def qgcrf2itrf( tm: time | datetime.datetime, @@ -336,10 +479,10 @@ def qgcrf2itrf( """Quaternion representing rotation from the Geocentric Celestial Reference Frame (GCRF) to the International Terrestrial Reference Frame (ITRF) Notes: - * Uses full IAU2010 Reduction - * See IERS Technical Note 36, Chapter 5 - * Does not include solid tides, ocean tides - * Very computationally expensive + - Uses full IAU2010 Reduction + - See IERS Technical Note 36, Chapter 5 + - Does not include solid tides, ocean tides + - Very computationally expensive Args: tm (satkit.time | datetime.datetime): Time[s] at which to calculate the quaternion @@ -368,10 +511,10 @@ def qgcrf2itrf( """Quaternion representing rotation from the Geocentric Celestial Reference Frame (GCRF) to the International Terrestrial Reference Frame (ITRF) Notes: - * Uses full IAU2010 Reduction - * See IERS Technical Note 36, Chapter 5 - * Does not include solid tides, ocean tides - * Very computationally expensive + - Uses full IAU2010 Reduction + - See IERS Technical Note 36, Chapter 5 + - Does not include solid tides, ocean tides + - Very computationally expensive Args: tm (npt.ArrayLike[satkit.time] | npt.ArrayLike[datetime.datetime]): Time[s] at which to calculate the quaternion @@ -381,6 +524,35 @@ def qgcrf2itrf( """ ... +def qgcrf2itrf(*args, **kwargs): + """Quaternion representing rotation from the Geocentric Celestial Reference Frame (GCRF) to the International Terrestrial Reference Frame (ITRF) + + Notes: + - Uses full IAU2010 Reduction + - See IERS Technical Note 36, Chapter 5 + - Does not include solid tides, ocean tides + - Very computationally expensive + + Args: + tm (satkit.time | datetime.datetime): Time[s] at which to calculate the quaternion + + Returns: + Quaternion representing rotation from GCRF to ITRF at input time(s) + + Example: + ```python + import numpy as np + + t = satkit.time(2024, 1, 1) + q = satkit.frametransform.qgcrf2itrf(t) + + # Rotate a GCRF position vector to ITRF + pos_gcrf = np.array([6.781e6, 0, 0]) + pos_itrf = q * pos_gcrf + ``` + """ + ... + @typing.overload def qitrf2gcrf( tm: time | datetime.datetime, @@ -388,10 +560,10 @@ def qitrf2gcrf( """Quaternion representing rotation from the International Terrestrial Reference Frame (ITRF) to the Geocentric Celestial Reference Frame (GCRF) Notes: - * Uses full IAU2010 Reduction - * See IERS Technical Note 36, Chapter 5 - * Does not include solid tides, ocean tides - * Very computationally expensive + - Uses full IAU2010 Reduction + - See IERS Technical Note 36, Chapter 5 + - Does not include solid tides, ocean tides + - Very computationally expensive Args: tm (satkit.time datetime.datetime): Time[s] at which to calculate the quaternion @@ -413,10 +585,10 @@ def qitrf2gcrf( """Quaternion representing rotation from the International Terrestrial Reference Frame (ITRF) to the Geocentric Celestial Reference Frame (GCRF) Notes: - * Uses full IAU2010 Reduction - * See IERS Technical Note 36, Chapter 5 - * Does not include solid tides, ocean tides - * Very computationally expensive + - Uses full IAU2010 Reduction + - See IERS Technical Note 36, Chapter 5 + - Does not include solid tides, ocean tides + - Very computationally expensive Args: tm (npt.ArrayLike[satkit.time] | npt.ArrayLike[datetime.datetime]): Time[s] at which to calculate the quaternion @@ -426,6 +598,28 @@ def qitrf2gcrf( """ ... +def qitrf2gcrf(*args, **kwargs): + """Quaternion representing rotation from the International Terrestrial Reference Frame (ITRF) to the Geocentric Celestial Reference Frame (GCRF) + + Notes: + - Uses full IAU2010 Reduction + - See IERS Technical Note 36, Chapter 5 + - Does not include solid tides, ocean tides + - Very computationally expensive + + Args: + tm (satkit.time datetime.datetime): Time[s] at which to calculate the quaternion + Returns: + Quaternion representing rotation from ITRF to GCRF at input time(s) + + Example: + ```python + t = satkit.time(2024, 1, 1) + q = satkit.frametransform.qitrf2gcrf(t) + ``` + """ + ... + @typing.overload def qteme2itrf( tm: time | datetime.datetime, @@ -433,8 +627,8 @@ def qteme2itrf( """Quaternion representing rotation from the True Equator Mean Equinox (TEME) frame to the International Terrestrial Reference Frame (ITRF) Notes: - * This is equation 3-90 in Vallado - * TEME is the output frame of the SGP4 propagator used to compute position from two-line element sets. + - This is equation 3-90 in Vallado + - TEME is the output frame of the SGP4 propagator used to compute position from two-line element sets. Args: tm (satkit.time | datetime.datetime): Time[s] at which to calculate the quaternion @@ -463,8 +657,8 @@ def qteme2itrf( """Quaternion representing rotation from the True Equator Mean Equinox (TEME) frame to the International Terrestrial Reference Frame (ITRF) Notes: - * This is equation 3-90 in Vallado - * TEME is the output frame of the SGP4 propagator used to compute position from two-line element sets. + - This is equation 3-90 in Vallado + - TEME is the output frame of the SGP4 propagator used to compute position from two-line element sets. Args: tm (npt.ArrayLike[satkit.time] | npt.ArrayLike[datetime.datetime]): Time[s] at which to calculate the quaternion @@ -474,6 +668,33 @@ def qteme2itrf( """ ... +def qteme2itrf(*args, **kwargs): + """Quaternion representing rotation from the True Equator Mean Equinox (TEME) frame to the International Terrestrial Reference Frame (ITRF) + + Notes: + - This is equation 3-90 in Vallado + - TEME is the output frame of the SGP4 propagator used to compute position from two-line element sets. + + Args: + tm (satkit.time | datetime.datetime): Time[s] at which to calculate the quaternion + + Returns: + Quaternion representing rotation from TEME to ITRF at input time(s) + + Example: + ```python + import numpy as np + + t = satkit.time(2024, 1, 1) + q = satkit.frametransform.qteme2itrf(t) + + # Convert SGP4 TEME output to ITRF + pos_teme = np.array([6.781e6, 0, 0]) + pos_itrf = q * pos_teme + ``` + """ + ... + def earth_orientation_params( time: time, ) -> tuple[float, float, float, float, float, float]: @@ -492,10 +713,10 @@ def earth_orientation_params( 5 : dY wrt IAU-2000A nutation, milli-arcsecs Notes: - * Returns None if the time is before the range of available EOP data - * For times after the last available EOP data, the last entry's values are returned (constant extrapolation) - * EOP data is available from 1962 to current, with predictions ~4 months ahead - * See: https://www.iers.org/IERS/EN/DataProducts/EarthOrientationData/eop.html + - Returns None if the time is before the range of available EOP data + - For times after the last available EOP data, the last entry's values are returned (constant extrapolation) + - EOP data is available from 1962 to current, with predictions ~4 months ahead + - See: Example: ```python @@ -512,7 +733,7 @@ def disable_eop_time_warning() -> None: """Disable the warning printed to stderr when Earth Orientation Parameters (EOP) are not available for a given time. Notes: - * This function is used to disable the warning printed when EOP are not available for a given time. - * If not disabled, warning will be shown only once per library load, + - This function is used to disable the warning printed when EOP are not available for a given time. + - If not disabled, warning will be shown only once per library load, """ ... diff --git a/python/satkit/jplephem.pyi b/python/satkit/jplephem.pyi index 060043e..60af269 100644 --- a/python/satkit/jplephem.pyi +++ b/python/satkit/jplephem.pyi @@ -1,7 +1,7 @@ """ High-precision JPL ephemerides for solar-system bodies -For details, see: https://ssd.jpl.nasa.gov/ +For details, see: """ from __future__ import annotations @@ -47,10 +47,10 @@ def barycentric_pos( numpy.ndarray: 3-vector of Cartesian position in meters, with the origin at the solar system barycenter. If input is list or numpy array of N times, then r will be Nx3 array Notes: - * Positions for all bodies are natively relative to solar system barycenter, + - Positions for all bodies are natively relative to solar system barycenter, with exception of moon, which is computed in Geocentric system - * EMB (2) is the Earth-Moon barycenter - * The sun position is relative to the solar system barycenter + - EMB (2) is the Earth-Moon barycenter + - The sun position is relative to the solar system barycenter (it will be close to origin) """ ... @@ -93,10 +93,10 @@ def barycentric_state( tuple: (r, v) where r is the position in meters and v is the velocity in meters / second, with the origin at the solar system barycenter. If input is list or numpy array of N times, then r and v will be Nx3 arrays Notes: - * Positions for all bodies are natively relative to solar system barycenter, + - Positions for all bodies are natively relative to solar system barycenter, with exception of moon, which is computed in Geocentric system - * EMB (2) is the Earth-Moon barycenter - * The sun position is relative to the solar system barycenter + - EMB (2) is the Earth-Moon barycenter + - The sun position is relative to the solar system barycenter (it will be close to origin) """ diff --git a/python/satkit/moon.pyi b/python/satkit/moon.pyi index ae3704f..d18ad8e 100644 --- a/python/satkit/moon.pyi +++ b/python/satkit/moon.pyi @@ -90,6 +90,33 @@ def pos_gcrf( """ ... +def pos_gcrf(*args, **kwargs): + """ + Approximate Moon position in the GCRF Frame + + From Vallado Algorithm 31 + + Args: + time (satkit.time): time at which to compute position + + Returns: + 3-element numpy array representing moon position in GCRF frame + at given time. Units are meters + + Notes: + Accurate to 0.3 degree in ecliptic longitude, 0.2 degree in ecliptic latitude, + and 1275 km in range + + Example: + ```python + import numpy as np + t = satkit.time(2024, 1, 1) + moon = satkit.moon.pos_gcrf(t) + print(f"Moon distance: {np.linalg.norm(moon)/1e3:.0f} km") + ``` + """ + ... + def illumination( time: satkit.time | npt.ArrayLike | list[satkit.time], ) -> npt.NDArray[np.float64] | float: diff --git a/python/satkit/satkit.pyi b/python/satkit/satkit.pyi index 28020b2..cb10df8 100644 --- a/python/satkit/satkit.pyi +++ b/python/satkit/satkit.pyi @@ -22,12 +22,12 @@ class TLE: "Simplified General Perturbations-4" (SGP-4) mathematical model that is also included in this package. - For details, see: https://en.wikipedia.org/wiki/Two-line_element_set + For details, see: The TLE format is still commonly used to represent satellite ephemerides, and satellite ephemerides catalogs in this format - are publicly available at www.space-track.org (registration - required) + are publicly available at (registration + required) and (no registration needed). TLEs sometimes have a "line 0" that includes the name of the satellite """ @@ -44,8 +44,8 @@ class TLE: filename (str): name of text file lines for TLE(s) to load Returns: - list[TLE] | TLE: a list of TLE objects or a single TLE of lines for - only 1 are passed in + a list of TLE objects or a single TLE if lines for + only 1 are passed in Example: ```python @@ -67,8 +67,8 @@ class TLE: lines (list[str]): list of strings with lines for TLE(s) to load Returns: - list[TLE] | TLE: a list of TLE objects or a single TLE of lines for - only 1 are passed in + a list of TLE objects or a single TLE if lines for + only 1 are passed in Example: ```python @@ -168,8 +168,8 @@ class TLE: def mean_motion_dot(self) -> float: """1/2 of first derivative of mean motion, in revs/day^2 - Note: - the "1/2" is because that is how number is stored in the TLE + Notes: + The "1/2" is because that is how number is stored in the TLE. """ ... @@ -182,8 +182,8 @@ class TLE: def mean_motion_dot_dot(self) -> float: """1/6 of 2nd derivative of mean motion, in revs/day^3 - Note: - The "1/6" is because that is how number is stored in the TLE + Notes: + The "1/6" is because that is how number is stored in the TLE. """ ... @@ -224,7 +224,7 @@ class TLE: Output as 2 canonical TLE Lines Returns: - list[str]: 2 canonical TLE Lines + 2 canonical TLE Lines Example: ```python @@ -242,7 +242,7 @@ class TLE: Output as 2 canonical TLE lines preceded by a name line (3-line element set) Returns: - list[str]: 3-line element set, name line then 2 canonical TLE lines + 3-line element set, name line then 2 canonical TLE lines Example: ```python @@ -268,43 +268,21 @@ class TLE: epoch: Epoch time for the TLE. Must be within range of times. Returns: - tuple[TLE, dict]: Fitted TLE and fitting results in a dictionary + Fitted TLE and fitting results in a dictionary Notes: + SGP4 propagator is used to match TLE to the states. + Input GCRF states are rotated into TEME frame used by SGP4. + First and second derivatives of mean motion are ignored, as they are not used by SGP4. + + Non-linear Levenberg-Marquardt optimization (via the Rust ``rmpfit`` crate) + is performed to fit inclination, eccentricity, RAAN, argument of perigee, + mean anomaly, mean motion, and drag (bstar) to the provided states. - * SGP4 propagator is used to match TLE to the states - - * Input GCRF states are rotated into TEME frame used by SGP4 - - * First and second derivatives of mean motion are ignored, as they are not - used by SGP4 - - * Non-linear Levenberg-Marquardt optimization is performed to fit the TLE parameters to the provided states. - TLE parameters used in fit include: - * Inclination - * Eccentricity - * Right Ascension of Ascending Node - * Argument of Perigee - * Mean Anomaly - * Mean motion - * Drag (bstar) - - * Rust crate "rmpfit" is used to perform the optimization - (https://crates.io/crates/rmpfit) - - * Results dictionary includes the following keys: - * `success` : "mpsuccess" value describing result of minimization - * `best_norm`: Final chi-squared value - * `orig_norm`: Initial chi-squared value - * `n_iter`: Number of iterations performed - * `n_fev`: Number of function evaluations performed - * `n_par`: Total number of parameters being optimized - * `n_free`: Number of free parameters - * `n_pegged`: Number of pegged parameters - * `n_func`: Number of residuals - * `resid`: Final residuals - * `xerror`: Final parameter uncertainties (1-sigma) - * `covar`: Final parameter covariance matrix + The results dictionary includes the following keys: + ``success``, ``best_norm``, ``orig_norm``, ``n_iter``, ``n_fev``, + ``n_par``, ``n_free``, ``n_pegged``, ``n_func``, ``resid``, + ``xerror``, ``covar``. Example: ```python @@ -329,13 +307,12 @@ def sgp4( ) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]: """SGP-4 propagator for TLE - Note: - Run Simplified General Perturbations (SGP)-4 propagator on Two-Line Element Set to - output satellite position and velocity at given time - in the "TEME" coordinate system + Run Simplified General Perturbations (SGP)-4 propagator on Two-Line Element Set to + output satellite position and velocity at given time + in the "TEME" coordinate system. - A detailed description is at: - https://celestrak.org/publications/AIAA/2008-6770/AIAA-2008-6770.pdf + A detailed description is at: + Args: tle (TLE | list[TLE] | dict): TLE or OMM (or list of TLES) on which to operate @@ -349,23 +326,19 @@ def sgp4( (this may also flag a typing error ... I can't figure out how to get rid of it) Returns: - tuple[npt.ArrayLike[np.float64], npt.ArrayLike[np.float64]]: position and velocity - in meters and meters/second, respectively, - in the TEME frame at each of the "Ntime" input times and each of the "Ntle" tles - - Additional return value if errflag is True: - list[sgp4_error]: list of errors for each TLE and time output, if errflag is True - - Note 1: - Now supports propagation of OMM (Orbital Mean-Element Message) dictionaries - The dictionaries must follow the structure used by https://www.celestrak.org or - https://www.space-track.org. - - Note 2: - The "TEME" frame of the SGP4 state vectors is not a truly inertial frame. It is a "True Equator Mean Equinox" - frame, which is a non-rotating frame with respect to the mean equator and mean equinox of the epoch of the TLE. - It is close to a true inertial frame, but can be offset by small amounts due to precession and nutation. + position and velocity + in meters and meters/second, respectively, + in the TEME frame at each of the "Ntime" input times and each of the "Ntle" tles. + Additional return value if errflag is True: + list[sgp4_error] with error conditions for each TLE and time output. + Notes: + - Now supports propagation of OMM (Orbital Mean-Element Message) dictionaries. + The dictionaries must follow the structure used by or + + - The "TEME" frame of the SGP4 state vectors is not a truly inertial frame. It is a "True Equator Mean Equinox" + frame, which is a non-rotating frame with respect to the mean equator and mean equinox of the epoch of the TLE. + It is close to a true inertial frame, but can be offset by small amounts due to precession and nutation. Example: ```python @@ -393,13 +366,13 @@ def sgp4( # ITRFCoord(lat: -0.0363 deg, lon: -2.2438 deg, hae: 35799.51 km) ``` - Example 2: + ```python import requests import json # Query ephemeris for the International Space Station (ISS) - url = 'https://celestrak.org/NORAD/elements/gp.php?CATNR=25544&FORMAT=json' + url = ' with requests.get(url) as response: omm = response.json() # Get a representative time from the output @@ -438,7 +411,7 @@ class gravmodel: """ Earth gravity models available for use - For details, see: http://icgem.gfz-potsdam.de/ + For details, see: """ jgm3: ClassVar[gravmodel] @@ -477,11 +450,11 @@ def gravity( order (int): Maximum order of gravity model to use. Default is same as degree Returns: - npt.ArrayLike[np.float]: acceleration in m/s^2 in the International Terrestrial Reference Frame (ITRF) + acceleration in m/s^2 in the International Terrestrial Reference Frame (ITRF) Notes: - * For details of calculation, see Chapter 3.2 of: "Satellite Orbits: Models, Methods, Applications", O. Montenbruck and B. Gill, Springer, 2012. + - For details of calculation, see Chapter 3.2 of: "Satellite Orbits: Models, Methods, Applications", O. Montenbruck and B. Gill, Springer, 2012. Example: ```python @@ -508,7 +481,7 @@ def gravity_and_partials( order (int): Maximum order of gravity model to use. Default is same as degree Returns: - tuple[npt.ArrayLike[np.float], np.arrayLike[np.float]]: acceleration in m/s^2 and partial derivative of acceleration with respect to ITRF Cartesian coordinate in m/s^2 / m + acceleration in m/s^2 and partial derivative of acceleration with respect to ITRF Cartesian coordinate in m/s^2 / m For details of calculation, see Chapter 3.2 of: "Satellite Orbits: Models, Methods, Applications", O. Montenbruck and B. Gill, Springer, 2012. @@ -582,13 +555,13 @@ class weekday: Represent the day of the week Values: - * `Sunday` - * `Monday` - * `Tuesday` - * `Wednesday` - * `Thursday` - * `Friday` - * `Saturday` + - `Sunday` + - `Monday` + - `Tuesday` + - `Wednesday` + - `Thursday` + - `Friday` + - `Saturday` """ Sunday: ClassVar[weekday] @@ -616,19 +589,19 @@ class mpsuccess: """ State of Levenberg-Marquardt optimization from the `rmpfit` rust library - For details see: https://docs.rs/rmpfit/latest/rmpfit/ + For details see: Values: - * `NotDone`: Not finished iterations - * `Chi`: Convergence in chi-square value - * `Par`: Convergence in parameter value - * `Both`: Convergence in both chi-square and parameter values - * `Dir`: Convergence in orthogonality - * `MaxIter`: Maximum iterations reached - * `Ftol`: ftol is too small; no further improvement - * `Xtol`: xtol is too small; no further improvement - * `Gtol`: gtol is too small; no further improvement + - `NotDone`: Not finished iterations + - `Chi`: Convergence in chi-square value + - `Par`: Convergence in parameter value + - `Both`: Convergence in both chi-square and parameter values + - `Dir`: Convergence in orthogonality + - `MaxIter`: Maximum iterations reached + - `Ftol`: ftol is too small; no further improvement + - `Xtol`: xtol is too small; no further improvement + - `Gtol`: gtol is too small; no further improvement """ NotDone: ClassVar[mpsuccess] @@ -668,17 +641,17 @@ class timescale: Earth-fixed coordinate frames For an excellent overview, see: - https://spsweb.fltops.jpl.nasa.gov/portaldataops/mpg/MPG_Docs/MPG%20Book/Release/Chapter2-TimeScales.pdf + Values: - * `Invalid`: Invalid time scale - * `UTC`: Universal Time Coordinate - * `TT`: Terrestrial Time - * `UT1`: UT1 - * `TAI`: International Atomic Time - * `GPS`: Global Positioning System (GPS) time - * `TDB`: Barycentric Dynamical Time + - `Invalid`: Invalid time scale + - `UTC`: Universal Time Coordinate + - `TT`: Terrestrial Time + - `UT1`: UT1 + - `TAI`: International Atomic Time + - `GPS`: Global Positioning System (GPS) time + - `TDB`: Barycentric Dynamical Time """ Invalid: ClassVar[timescale] @@ -713,9 +686,9 @@ class time: conversion between various time epochs (GPS, TAI, UTC, UT1, etc...) Notes: - * If no arguments are passed in, the created object represents the current time - * If year is passed in, month and day must also be passed in - * If hour is passed in, minute and second must also be passed in + - If no arguments are passed in, the created object represents the current time + - If year is passed in, month and day must also be passed in + - If hour is passed in, minute and second must also be passed in Example: ```python @@ -748,9 +721,9 @@ class time: conversion between various time epochs (GPS, TAI, UTC, UT1, etc...) Notes: - * If no arguments are passed in, the created object represents the current time - * If year is passed in, month and day must also be passed in - * If hour is passed in, minute and second must also be passed in + - If no arguments are passed in, the created object represents the current time + - If year is passed in, month and day must also be passed in + - If hour is passed in, minute and second must also be passed in Args: year: Gregorian year (e.g., 2024) @@ -779,7 +752,7 @@ class time: calling of the function. Returns: - satkit.time: Time object representing the current time + Time object representing the current time """ ... @@ -791,11 +764,11 @@ class time: Args: str: String representation of time, in format "YYYY-MM-DD HH:MM:SS.sssZ" or if other will try to intelligently parse, but no guarantees - Note: - * This is probably not what you want. Use with caution. + Notes: + - This is probably not what you want. Use with caution. Returns: - satkit.time: Time object representing input string + Time object representing input string Example: ```python @@ -813,11 +786,11 @@ class time: rfc (str): RFC 3339 string representation of time Notes: - * RFC 3339 is a subset of ISO 8601 - * Only allows a subset of the format: "YYYY-MM-DDTHH:MM:SS.sssZ" or "YYYY-MM-DDTHH:MM:SS.ssssssZ" + - RFC 3339 is a subset of ISO 8601 + - Only allows a subset of the format: "YYYY-MM-DDTHH:MM:SS.sssZ" or "YYYY-MM-DDTHH:MM:SS.ssssssZ" Returns: - satkit.time: Time object representing input RFC 3339 string + Time object representing input RFC 3339 string Example: ```python @@ -837,21 +810,20 @@ class time: format (str): format of the string Notes: - * The format string is a subset of the strptime format string in the Python "datetime" module - - Format Codes: - * %Y - year - * %m - month with leading zeros (01-12) - * %d - day of month with leading zeros (01-31) - * %H - hour with leading zeros (00-23) - * %M - minute with leading zeros (00-59) - * %S - second with leading zeros (00-59) - * %f - microsecond, allowing for trailing zeros - * %b - abbreviated month name (Jan, Feb, ...) - * %B - full month name (January, February, ...) + - The format string is a subset of the strptime format string in the Python "datetime" module + - Format Codes: + - %Y - year + - %m - month with leading zeros (01-12) + - %d - day of month with leading zeros (01-31) + - %H - hour with leading zeros (00-23) + - %M - minute with leading zeros (00-59) + - %S - second with leading zeros (00-59) + - %f - microsecond, allowing for trailing zeros + - %b - abbreviated month name (Jan, Feb, ...) + - %B - full month name (January, February, ...) Returns: - satkit.time: Time object representing input string + Time object representing input string Example: ```python @@ -872,7 +844,7 @@ class time: day (int): Day of month, beginning with 1 Returns: - satkit.time: Time object representing the start of the input day (midnight) + Time object representing the start of the input day (midnight) Example: ```python @@ -892,7 +864,7 @@ class time: scale (timescale, optional): Time scale. Default is satkit.timescale.UTC Returns: - satkit.time: Time object representing input Julian date and time scale + Time object representing input Julian date and time scale Example: ```python @@ -911,7 +883,7 @@ class time: (leap seconds are not included) Returns: - satkit.time: Time object representing input unixtime + Time object representing input unixtime Example: ```python @@ -931,7 +903,7 @@ class time: sec: GPS seconds of week Returns: - satkit.time: Time object representing input GPS week and second + Time object representing input GPS week and second """ ... @@ -940,7 +912,7 @@ class time: Return the day of the week Returns: - satkit.weekday: Day of the week + Day of the week """ ... @@ -949,7 +921,7 @@ class time: Return the 1-based Gregorian day of the year (1 = January 1, 365 = December 31) Returns: - int: The 1-based day of the year + The 1-based day of the year """ ... @@ -962,7 +934,7 @@ class time: scale (satkit.timescale, optional): Time scale. Default is satkit.timescale.UTC Returns: - satkit.time: Time object representing input modified Julian date and time scale + Time object representing input modified Julian date and time scale Example: ```python @@ -976,11 +948,10 @@ class time: """Return tuple representing as UTC Gegorian date of the time object. Returns: - tuple[int, int, int]: Tuple with 3 elements representing the Gregorian year, month, and day of the time object - - Fractional component of day are truncated - Month is in range [1,12] - Day is in range [1,31] + Tuple with 3 elements representing the Gregorian year, month, and day of the time object. + Fractional component of day are truncated. + Month is in range [1,12]. + Day is in range [1,31]. """ ... @@ -1007,7 +978,7 @@ class time: scale (timescale, optional): Time scale. Default is satkit.timescale.UTC Returns: - satkit.time: Time object representing input UTC Gregorian time + Time object representing input UTC Gregorian time Example: ```python @@ -1026,10 +997,9 @@ class time: scale (timescale, optional): Time scale. Default is satkit.timescale.UTC Returns: - tuple[int, int, int, int, int, float]: Tuple with 6 elements representing the Gregorian year, month, day, hour, minute, and second of the time object - - Month is in range [1,12] - Day is in range [1,31] + Tuple with 6 elements representing the Gregorian year, month, day, hour, minute, and second of the time object. + Month is in range [1,12]. + Day is in range [1,31]. """ ... @@ -1053,7 +1023,7 @@ class time: sec (float): floating point second of minute, in range [0,60) Returns: - satkit.time: Time object representing input UTC Gregorian time + Time object representing input UTC Gregorian time Example: ```python @@ -1072,7 +1042,7 @@ class time: dt (datetime.datetime): "datetime.datetime" object to convert Returns: - satkit.time: Time object representing the same instant in time as the input "datetime.datetime" object + Time object representing the same instant in time as the input "datetime.datetime" object """ ... @@ -1083,7 +1053,7 @@ class time: utc (bool, optional): Whether to make the "datetime.datetime" object represent time in the local timezone or "UTC". Default is True Returns: - datetime.datetime: "datetime.datetime" object representing the same instant in time as the "satkit.time" object + "datetime.datetime" object representing the same instant in time as the "satkit.time" object Example: ```python @@ -1107,7 +1077,7 @@ class time: utc (bool, optional): Whether to make the "datetime.datetime" object represent time in the local timezone or "UTC". Default is True Returns: - datetime.datetime: "datetime.datetime" object representing the same instant in time as the "satkit.time" object + "datetime.datetime" object representing the same instant in time as the "satkit.time" object Example: ```python @@ -1155,7 +1125,7 @@ class time: Represent time as ISO 8601 string Returns: - str: ISO 8601 string representation of time: "YYYY-MM-DDTHH:MM:SS.sssZ" + ISO 8601 string representation of time: "YYYY-MM-DDTHH:MM:SS.sssZ" """ ... @@ -1164,7 +1134,7 @@ class time: Represent time as RFC 3339 string Returns: - str: RFC 3339 string representation of time: "YYYY-MM-DDTHH:MM:SS.sssZ" + RFC 3339 string representation of time: "YYYY-MM-DDTHH:MM:SS.sssZ" """ ... @@ -1175,21 +1145,23 @@ class time: Args: format (str): format of the string - Format Codes: - * %Y - year - * %m - month with leading zeros (01-12) - * %d - day of month with leading zeros (01-31) - * %H - hour with leading zeros (00-23) - * %M - minute with leading zeros (00-59) - * %S - second with leading zeros (00-59) - * %f - microsecond, allowing for trailing zeros - * %b - abbreviated month name (Jan, Feb, ...) - * %B - full month name (January, February, ...) - * %A - full weekday name (Sunday, Monday, ...) - * %w - weekday as a decimal number (0=Sunday, 1=Monday, ...) + Notes: + Format Codes: + + - %Y - year + - %m - month with leading zeros (01-12) + - %d - day of month with leading zeros (01-31) + - %H - hour with leading zeros (00-23) + - %M - minute with leading zeros (00-59) + - %S - second with leading zeros (00-59) + - %f - microsecond, allowing for trailing zeros + - %b - abbreviated month name (Jan, Feb, ...) + - %B - full month name (January, February, ...) + - %A - full weekday name (Sunday, Monday, ...) + - %w - weekday as a decimal number (0=Sunday, 1=Monday, ...) Returns: - str: string representation of time + string representation of time Example: ```python @@ -1208,7 +1180,7 @@ class time: other (duration): duration to add to the current time Returns: - satkit.time: Time object representing the input duration added to the current time + Time object representing the input duration added to the current time """ ... @@ -1222,7 +1194,7 @@ class time: other (float): number of days to add to the current time Returns: - satkit.time: Time object representing the input number of days added to the current time + Time object representing the input number of days added to the current time """ ... @@ -1236,7 +1208,7 @@ class time: other (list[duration]): array-like structure containing days to add to the current time Returns: - npt.ArrayLike[time]: Array of time objects representing the element-wise addition of days to the current time + Array of time objects representing the element-wise addition of days to the current time """ ... @@ -1248,7 +1220,7 @@ class time: other (time): time object to compare with Returns: - bool: True if "self" time is less than or equal to "other" time, False otherwise + True if "self" time is less than or equal to "other" time, False otherwise """ ... @@ -1260,7 +1232,7 @@ class time: other (time): time object to compare with Returns: - bool: True if "self" time is less than "other" time, False otherwise + True if "self" time is less than "other" time, False otherwise """ ... @@ -1272,7 +1244,7 @@ class time: other (time): time object to compare with Returns: - bool: True if "self" time is greater than or equal to "other" time, False otherwise + True if "self" time is greater than or equal to "other" time, False otherwise """ ... @@ -1284,7 +1256,7 @@ class time: other (time): time object to compare with Returns: - bool: True if "self" time is greater than "other" time, False otherwise + True if "self" time is greater than "other" time, False otherwise """ ... @@ -1296,7 +1268,7 @@ class time: value (object): object to compare with Returns: - bool: True if "self" time is equal to "value", False otherwise + True if "self" time is equal to "value", False otherwise """ ... @@ -1308,7 +1280,7 @@ class time: value (object): object to compare with Returns: - bool: True if "self" time is not equal to "value", False otherwise + True if "self" time is not equal to "value", False otherwise """ ... @@ -1321,7 +1293,7 @@ class time: other (npt.ArrayLike[Any]): array-like structure containing durations to add to the current time Returns: - npt.ArrayLike[time]: Array of time objects representing the element-wise addition of durations to the current time + Array of time objects representing the element-wise addition of durations to the current time """ ... @@ -1335,7 +1307,7 @@ class time: other (duration): duration to subtract from the current time Returns: - satkit.time: Time object representing the input duration subtracted from the current time + Time object representing the input duration subtracted from the current time """ ... @@ -1349,7 +1321,7 @@ class time: other (time): time to subtract from the current time Returns: - satkit.duration: Duration object representing the difference between the two times + Duration object representing the difference between the two times """ ... @@ -1363,7 +1335,7 @@ class time: other (float): number of days to subtract from the current time Returns: - satkit.time: Time object representing the input number of days subtracted from the current time + Time object representing the input number of days subtracted from the current time """ ... @@ -1377,7 +1349,7 @@ class time: other (npt.ArrayLike[float]): array-like structure containing days to subtract from the current time Returns: - npt.ArrayLike[time]: Array of time objects representing the element-wise subtraction of days from the current time + Array of time objects representing the element-wise subtraction of days from the current time """ ... @@ -1391,7 +1363,7 @@ class time: other (list[duration]): array-like structure containing durations to subtract from the current time Returns: - npt.ArrayLike[time]: Array of time objects representing the element-wise subtraction of durations from the current time + Array of time objects representing the element-wise subtraction of durations from the current time """ ... @@ -1404,7 +1376,7 @@ class time: other (list[time]): array-like structure containing times to subtract from the current time Returns: - npt.ArrayLike[duration]: Array of duration objects representing the element-wise subtraction of times from the current time + Array of duration objects representing the element-wise subtraction of times from the current time """ ... @@ -1432,7 +1404,7 @@ class duration: microseconds: Number of microseconds, default is 0.0 Notes: - * If no arguments are passed in, the created object represents a duration of 0 seconds + - If no arguments are passed in, the created object represents a duration of 0 seconds Example: ```python @@ -1451,7 +1423,7 @@ class duration: d (float): Number of days Returns: - satkit.duration: Duration object representing input number of days + Duration object representing input number of days Example: ```python @@ -1470,7 +1442,7 @@ class duration: s (float): Number of seconds Returns: - satkit.duration: Duration object representing input number of seconds + Duration object representing input number of seconds Example: ```python @@ -1489,7 +1461,7 @@ class duration: m (float): Number of minutes Returns: - satkit.duration: Duration object representing input number of minutes + Duration object representing input number of minutes """ ... @@ -1501,7 +1473,7 @@ class duration: h (float): Number of hours Returns: - satkit.duration: Duration object representing input number of hours + Duration object representing input number of hours """ ... @@ -1513,7 +1485,7 @@ class duration: other (duration): duration to add to the current duration Returns: - duration: Duration object representing the sum, or concatenation, of both durations + Duration object representing the sum, or concatenation, of both durations Example: ```python @@ -1531,7 +1503,7 @@ class duration: other (float): number of days to add to the current duration Returns: - duration: Duration object representing the input number of days added to the current duration + Duration object representing the input number of days added to the current duration Example: ```python @@ -1549,7 +1521,7 @@ class duration: other (time): time to add the current duration to Returns: - time: Time object representing the input time plus the duration + Time object representing the input time plus the duration Example: ```python @@ -1566,7 +1538,7 @@ class duration: other (duration): duration to subtract from the current duration Returns: - duration: Duration object representing the difference between the two durations + Duration object representing the difference between the two durations Example: ```python @@ -1583,7 +1555,7 @@ class duration: other (float): value by which to multiply duration Returns: - duration: Duration object representing the input duration scaled by the input value + Duration object representing the input duration scaled by the input value Example: ```python @@ -1601,7 +1573,7 @@ class duration: other (float): value by which to divide duration Returns: - duration: Duration object representing the input duration divided by the input value + Duration object representing the input duration divided by the input value Example: ```python @@ -1619,7 +1591,7 @@ class duration: other (duration): duration by which to divide current duration Returns: - float: Dimensionless ratio of the two durations + Dimensionless ratio of the two durations Example: ```python @@ -1635,7 +1607,7 @@ class duration: Args: other (duration): duration to compare with Returns: - bool: True if "self" duration is greater than "other" duration, False otherwise + True if "self" duration is greater than "other" duration, False otherwise Example: ```python @@ -1651,7 +1623,7 @@ class duration: Args: other (duration): duration to compare with Returns: - bool: True if "self" duration is less than "other" duration, False otherwise + True if "self" duration is less than "other" duration, False otherwise Example: ```python @@ -1667,7 +1639,7 @@ class duration: Args: other (duration): duration to compare with Returns: - bool: True if "self" duration is greater than or equal to "other" duration, False otherwise + True if "self" duration is greater than or equal to "other" duration, False otherwise Example: ```python @@ -1683,7 +1655,7 @@ class duration: Args: other (duration): duration to compare with Returns: - bool: True if "self" duration is less than or equal to "other" duration, False otherwise + True if "self" duration is less than or equal to "other" duration, False otherwise Example: ```python @@ -1698,7 +1670,7 @@ class duration: """Floating point number of days represented by duration Returns: - float: Floating point number of days represented by duration + Floating point number of days represented by duration A day is defined as 86,400 seconds """ @@ -1709,7 +1681,7 @@ class duration: """Floating point number of hours represented by duration Returns: - float: Floating point number of hours represented by duration + Floating point number of hours represented by duration """ ... @@ -1718,7 +1690,7 @@ class duration: """Floating point number of minutes represented by duration Returns: - float: Floating point number of minutes represented by duration + Floating point number of minutes represented by duration """ ... @@ -1727,7 +1699,7 @@ class duration: """Floating point number of seconds represented by duration Returns: - float: Floating point number of seconds represented by duration + Floating point number of seconds represented by duration """ ... @@ -1746,10 +1718,10 @@ class quaternion: For details, see: - https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation + Notes: - * Under the hood, this is using the "UnitQuaternion" object in the rust "nalgebra" crate. + - Under the hood, this is using the "UnitQuaternion" object in the rust "nalgebra" crate. """ def __init__(self, w: float = 1.0, x: float = 0.0, y: float = 0.0, z: float = 0.0): @@ -1782,7 +1754,7 @@ class quaternion: angle (float): angle of rotation in radians Returns: - satkit.quaternion: Quaternion representing rotation by "angle" degrees about the given axis + Quaternion representing rotation by "angle" degrees about the given axis """ ... @@ -1796,7 +1768,7 @@ class quaternion: mat (npt.ArrayLike[np.float64]): 3x3 rotation matrix Returns: - satkit.quaternion: Quaternion representing identical rotation to input 3x3 rotation matrix + Quaternion representing identical rotation to input 3x3 rotation matrix """ ... @@ -1808,7 +1780,7 @@ class quaternion: theta (float): angle of rotation in radians Returns: - satkit.quaternion: Quaternion representing right-handed rotation of vector by "theta" radians about the xhat unit vector + Quaternion representing right-handed rotation of vector by "theta" radians about the xhat unit vector Notes: Equivalent rotation matrix: @@ -1826,7 +1798,7 @@ class quaternion: theta (float): angle of rotation in radians Returns: - satkit.quaternion: Quaternion representing right-handed rotation of vector by "theta" radians about the yhat unit vector + Quaternion representing right-handed rotation of vector by "theta" radians about the yhat unit vector Notes: @@ -1845,7 +1817,7 @@ class quaternion: theta (float): angle of rotation in radians Returns: - satkit.quaternion: Quaternion representing right-handed rotation of vector by "theta" radians about the zhat unit vector + Quaternion representing right-handed rotation of vector by "theta" radians about the zhat unit vector Notes: Equivalent rotation matrix: @@ -1866,7 +1838,7 @@ class quaternion: v2 (npt.ArrayLike[np.float64]): vector rotating to Returns: - satkit.quaternion: Quaternion that rotates from v1 to v2 + Quaternion that rotates from v1 to v2 Example: ```python @@ -1884,16 +1856,25 @@ class quaternion: """Return 3x3 rotation matrix representing equivalent rotation Returns: - npt.ArrayLike[np.float64]: 3x3 rotation matrix representing equivalent rotation + 3x3 rotation matrix representing equivalent rotation """ ... def as_euler(self) -> tuple[float, float, float]: - """Return equivalent rotation angle represented as rotation angles: ("roll", "pitch", "yaw") in radians: + """Return equivalent rotation as intrinsic ZYX Euler angles (yaw, pitch, roll). + + The decomposition follows the aerospace convention (Tait-Bryan angles): + the rotation is equivalent to first rotating by yaw about Z, + then pitch about the new Y, then roll about the new X. Returns: - tuple[float, float, float]: Tuple with 3 elements representing the rotation angles in radians + tuple[float, float, float]: ``(roll, pitch, yaw)`` in radians + Example: + ```python + q = satkit.quaternion.rotz(0.1) * satkit.quaternion.roty(0.2) + roll, pitch, yaw = q.as_euler() + ``` """ ... @@ -1901,7 +1882,7 @@ class quaternion: """Return the angle in radians of the rotation Returns: - float: Angle in radians of the rotation + Angle in radians of the rotation """ ... @@ -1909,7 +1890,7 @@ class quaternion: """Return the axis of rotation as a unit vector Returns: - npt.ArrayLike[np.float64]: 3-element array representing the axis of rotation as a unit vector + 3-element array representing the axis of rotation as a unit vector """ ... @@ -1918,7 +1899,7 @@ class quaternion: """Return conjugate or inverse of the rotation Returns: - satkit.quaternion: Conjugate or inverse of the rotation + Conjugate or inverse of the rotation """ ... @@ -1927,7 +1908,7 @@ class quaternion: """Return conjugate or inverse of the rotation Returns: - satkit.quaternion: Conjugate or inverse of the rotation + Conjugate or inverse of the rotation """ ... @@ -1936,7 +1917,7 @@ class quaternion: """X component of the quaternion Returns: - float: X component of the quaternion + X component of the quaternion """ ... @@ -1945,7 +1926,7 @@ class quaternion: """Y component of the quaternion Returns: - float: Y component of the quaternion + Y component of the quaternion """ ... @@ -1954,7 +1935,7 @@ class quaternion: """Z component of the quaternion Returns: - float: Z component of the quaternion + Z component of the quaternion """ ... @@ -1963,7 +1944,7 @@ class quaternion: """Scalar component of the quaternion Returns: - float: Scalar component of the quaternion + Scalar component of the quaternion """ ... @@ -1972,13 +1953,13 @@ class quaternion: """Multiply by another quaternion to concatenate rotations Notes: - * Multiply represents concatenation of two rotations representing the quaternions. The left value rotation is applied after the right value, per the normal convention + - Multiply represents concatenation of two rotations representing the quaternions. The left value rotation is applied after the right value, per the normal convention Args: other (quaternion): quaternion to multiply by Returns: - quaternion: Quaternion representing concatenation of the two rotations + Quaternion representing concatenation of the two rotations """ ... @@ -1990,7 +1971,7 @@ class quaternion: other (npt.ArrayLike[np.float64]): 3-element array representing vector to rotate or Nx3 array of vectors to rotate Returns: - npt.ArrayLike[np.float64]: 3-element array representing rotated vector or Nx3 array of rotated vectors + 3-element array representing rotated vector or Nx3 array of rotated vectors Example: ```python @@ -2013,7 +1994,7 @@ class quaternion: epsilon (float, optional): Value below which the sin of the angle separating both quaternions must be to return an error. Default is 1.0e-6 Returns: - quaternion: Quaternion representing interpolation between self and other + Quaternion representing interpolation between self and other Example: ```python @@ -2032,11 +2013,11 @@ class kepler: Notes: - * This class is used to represent Keplerian elements and convert between Cartesian coordinates - * The class uses the semi-major axis (a), not the semiparameter - * All angle units are radians - * All length units are meters - * All velocity units are meters / second + - This class is used to represent Keplerian elements and convert between Cartesian coordinates + - The class uses the semi-major axis (a), not the semiparameter + - All angle units are radians + - All length units are meters + - All velocity units are meters / second """ def __init__( @@ -2093,7 +2074,7 @@ class kepler: """Convert Keplerian element set to position and velocity vectors Returns: - tuple[npt.ArrayLike[np.float64], npt.ArrayLike[np.float64]]: Tuple with two elements representing the position and velocity vectors + Tuple with two elements representing the position and velocity vectors Example: ```python @@ -2112,7 +2093,7 @@ class kepler: If float, value is seconds Returns: - satkit.kepler: Keplerian element set object after propagation + Keplerian element set object after propagation Example: ```python @@ -2318,7 +2299,7 @@ class itrfcoord: """Geodetic coordinates as a named struct Returns: - satkit.geodetic: Geodetic struct with latitude_rad, longitude_rad, height_m fields + Geodetic struct with latitude_rad, longitude_rad, height_m fields and latitude_deg, longitude_deg computed properties """ ... @@ -2328,7 +2309,7 @@ class itrfcoord: """Cartesian ITRF coord as numpy array Returns: - npt.NDArray[np.float64]: 3-element numpy array representing the ITRF Cartesian coordinate in meters + 3-element numpy array representing the ITRF Cartesian coordinate in meters """ ... @@ -2337,7 +2318,7 @@ class itrfcoord: """Quaternion representing rotation from North-East-Down (NED) to ITRF at this location Returns: - satkit.quaternion: Quaternion representiong rotation from North-East-Down (NED) to ITRF at this location + Quaternion representiong rotation from North-East-Down (NED) to ITRF at this location """ ... @@ -2346,7 +2327,7 @@ class itrfcoord: """Quaternion representiong rotation from East-North-Up (ENU) to ITRF at this location Returns: - satkit.quaternion: Quaternion representiong rotation from East-North-Up (ENU) to ITRF at this location + Quaternion representiong rotation from East-North-Up (ENU) to ITRF at this location """ ... @@ -2357,10 +2338,10 @@ class itrfcoord: refcoord (itrfcoord): Reference ITRF coordinate representing origin of ENU frame Returns: - npt.NDArray[np.float64]: 3-element numpy array representing vector from reference coordinate to this coordinate in East-North-Up (ENU) frame of reference at the reference coordinate + 3-element numpy array representing vector from reference coordinate to this coordinate in East-North-Up (ENU) frame of reference at the reference coordinate - Note: - * This is equivalent to calling: refcoord.qenu2itrf.conj * (self - refcoord) + Notes: + - This is equivalent to calling: refcoord.qenu2itrf.conj * (self - refcoord) Example: ```python @@ -2379,10 +2360,10 @@ class itrfcoord: refcoord (itrfcoord): Reference ITRF coordinate representing origin of NED frame Returns: - npt.NDArray[np.float64]: 3-element numpy array representing vector from reference coordinate to this coordinate in North-East-Down (NED) frame of reference at the reference coordinate + 3-element numpy array representing vector from reference coordinate to this coordinate in North-East-Down (NED) frame of reference at the reference coordinate - Note: - * This is equivalent to calling: refcoord.qned2itrf.conj * (self - refcoord) + Notes: + - This is equivalent to calling: refcoord.qned2itrf.conj * (self - refcoord) """ ... @@ -2394,16 +2375,16 @@ class itrfcoord: other (itrfcoord): Other ITRF coordinate to subtract Returns: - npt.NDArray[np.float64]: 3-element numpy array representing the difference in meters between the two ITRF coordinates + 3-element numpy array representing the difference in meters between the two ITRF coordinates """ ... def geodesic_distance(self, other: itrfcoord) -> tuple[float, float, float]: """Use Vincenty formula to compute geodesic distance: - https://en.wikipedia.org/wiki/Vincenty%27s_formulae + Returns: - tuple[float, float, float]: (distance in meters, initial heading in radians, heading at destination in radians) + (distance in meters, initial heading in radians, heading at destination in radians) Example: ```python @@ -2426,10 +2407,10 @@ class itrfcoord: Altitude is assumed to be zero Use Vincenty formula to compute position: - https://en.wikipedia.org/wiki/Vincenty%27s_formulae + Returns: - tuple[float, float, float]: (distance in meters, initial heading in radians, heading at destination in radians) + (distance in meters, initial heading in radians, heading at destination in radians) Example: ```python @@ -2537,7 +2518,7 @@ class satstate: """state position in meters in GCRF frame Returns: - npt.ArrayLike[np.float64]: 3-element numpy array representing position in meters in GCRF frame + 3-element numpy array representing position in meters in GCRF frame """ ... @@ -2546,7 +2527,7 @@ class satstate: """Return this state velocity in meters / second in GCRF Returns: - npt.ArrayLike[np.float64]: 3-element numpy array representing velocity in meters / second in GCRF frame + 3-element numpy array representing velocity in meters / second in GCRF frame """ ... @@ -2555,7 +2536,7 @@ class satstate: """Quaternion that rotates from the GCRF to the LVLH frame for the current state Returns: - satkit.quaternion: Quaternion that rotates from the GCRF to the LVLH frame for the current state + Quaternion that rotates from the GCRF to the LVLH frame for the current state """ ... @@ -2564,7 +2545,7 @@ class satstate: """6x6 state covariance matrix in GCRF frame Returns: - npt.ArrayLike[np.float64] | None: 6x6 numpy array representing state covariance in GCRF frame or None if not set + 6x6 numpy array representing state covariance in GCRF frame or None if not set """ ... @@ -2573,7 +2554,7 @@ class satstate: """Return time of this satellite state Returns: - satkit.time: Time instant of this state + Time instant of this state """ ... @@ -2621,9 +2602,10 @@ class propresult: This class lets the user access results of the satellite propagation Notes: - - * If "enable_interp" is set to True in the propagation settings, the propresult object can be used to interpolate solutions at any time between the begin and end times of the propagation via the "interp" method - + If ``enable_interp`` is set to True in the propagation settings, + the propresult object can be used to interpolate solutions at any + time between the begin and end times of the propagation via the + ``interp`` method. """ @property @@ -2631,7 +2613,7 @@ class propresult: """GCRF position of satellite, meters Returns: - npt.ArrayLike[float]: 3-element numpy array representing GCRF position (meters) at end of propagation + 3-element numpy array representing GCRF position (meters) at end of propagation """ ... @@ -2641,7 +2623,7 @@ class propresult: """GCRF velocity of satellite, meters/second Returns: - npt.ArrayLike[float]: 3-element numpy array representing GCRF velocity in meters/second at end of propagation + 3-element numpy array representing GCRF velocity in meters/second at end of propagation """ ... @@ -2650,7 +2632,7 @@ class propresult: """6-element end state (pos + vel) of satellite in meters & meters/second Returns: - npt.ArrayLike[float]: 6-element numpy array representing state of satellite in meters & meters/second + 6-element numpy array representing state of satellite in meters & meters/second """ ... @@ -2659,10 +2641,10 @@ class propresult: """6-element state (pos + vel) of satellite in meters & meters/second at end of propagation Notes: - * This is the same as the "state" property + - This is the same as the "state" property Returns: - npt.ArrayLike[float]: 6-element numpy array representing state of satellite in meters & meters/second + 6-element numpy array representing state of satellite in meters & meters/second """ ... @@ -2670,7 +2652,7 @@ class propresult: def state_begin(self) -> npt.NDArray[np.float64]: """6-element state (pos + vel) of satellite in meters & meters/second at begin of propagation Returns: - npt.NDArray[np.float64]: 6-element numpy array representing state of satellite in meters & meters/second at begin of propagation + 6-element numpy array representing state of satellite in meters & meters/second at begin of propagation """ ... @@ -2679,7 +2661,7 @@ class propresult: """Time at which state is valid Returns: - satkit.time: Time at which state is valid + Time at which state is valid """ ... @@ -2688,10 +2670,10 @@ class propresult: """Time at which state is valid Notes: - * This is identical to "time" property + - This is identical to "time" property Returns: - satkit.time: Time at which state is valid + Time at which state is valid """ ... @@ -2701,7 +2683,7 @@ class propresult: Returns: - satkit.time: Time at which state_begin is valid + Time at which state_begin is valid """ ... @@ -2719,7 +2701,7 @@ class propresult: """Whether this result supports interpolation Returns: - bool: True if dense output is available for interpolation + True if dense output is available for interpolation """ ... @@ -2728,33 +2710,95 @@ class propresult: """State transition matrix Returns: - npt.ArrayLike[np.float64] | None: 6x6 numpy array representing state transition matrix or None if not computed + 6x6 numpy array representing state transition matrix or None if not computed + """ + ... + + @typing.overload + def interp( + self, + time: time | datetime.datetime, + output_phi: typing.Literal[False] = False, + ) -> npt.NDArray[np.float64]: + """Interpolate state at a single time + + Args: + time: Time at which to interpolate state + output_phi: Must be False (default) + + Returns: + npt.NDArray[np.float64]: 6-element state vector [x, y, z, vx, vy, vz] in meters and m/s + """ + ... + + @typing.overload + def interp( + self, + time: time | datetime.datetime, + output_phi: typing.Literal[True] = ..., + ) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]: + """Interpolate state and state transition matrix at a single time + + Args: + time: Time at which to interpolate state + output_phi: Must be True + + Returns: + tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]: (state, phi) where state is a 6-element + vector and phi is a 6x6 state transition matrix """ ... + @typing.overload def interp( self, - time: Union[time, datetime.datetime, list[Union[time, datetime.datetime]]], + time: list[time | datetime.datetime], + output_phi: typing.Literal[False] = False, + ) -> list[npt.NDArray[np.float64]]: + """Interpolate state at multiple times + + Args: + time: List of times at which to interpolate state + output_phi: Must be False (default) + + Returns: + list[npt.NDArray[np.float64]]: List of 6-element state vectors + """ + ... + + @typing.overload + def interp( + self, + time: list[time | datetime.datetime], + output_phi: typing.Literal[True] = ..., + ) -> list[tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]]: + """Interpolate state and state transition matrix at multiple times + + Args: + time: List of times at which to interpolate state + output_phi: Must be True + + Returns: + list[tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]]: List of (state, phi) tuples + """ + ... + + def interp( + self, + time: time | datetime.datetime | list[time | datetime.datetime], output_phi: bool = False, - ) -> ( - npt.NDArray[np.float64] - | tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]] - | list[npt.NDArray[np.float64]] - | list[tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]] ): - """Interpolate state at given time or list of times + """Interpolate state at given time(s) + + Requires ``enable_interp=True`` in propagation settings. Args: - time (satkit.time | datetime.datetime | list): Time or list of times at which to interpolate state. + time (time | datetime.datetime | list): Time or list of times at which to interpolate state. datetime.datetime objects are interpreted as UTC. - output_phi (bool, optional): Output 6x6 state transition matrix at the interpolated time. - Default is False. + output_phi (bool): If True, also return the 6x6 state transition matrix. Default is False. Returns: - If a single time is given: - npt.NDArray[np.float64]: 6-element state vector. If output_phi, a tuple of (state, 6x6 phi matrix). - If a list of times is given: - list[npt.NDArray[np.float64]]: list of 6-element state vectors. If output_phi, a list of (state, phi) tuples. + For a single time: a 6-element state vector, or a ``(state, phi)`` tuple if ``output_phi=True``. For a list of times: a list of state vectors, or a list of ``(state, phi)`` tuples. Example: ```python @@ -2792,7 +2836,7 @@ class satproperties_static: Notes: - * The two arguments can be passed as positional arguments or as keyword arguments + - The two arguments can be passed as positional arguments or as keyword arguments Example: @@ -2827,12 +2871,12 @@ class integrator: Available integrators, from highest to lowest order: - * ``rkv98`` - Verner 9(8) with 9th-order dense output, 26 stages (default) - * ``rkv98_nointerp`` - Verner 9(8) without interpolation, 16 stages - * ``rkv87`` - Verner 8(7) with 8th-order dense output, 21 stages - * ``rkv65`` - Verner 6(5), 10 stages - * ``rkts54`` - Tsitouras 5(4) with FSAL, 7 stages - * ``rodas4`` - RODAS4 L-stable Rosenbrock 4(3), 6 stages. For stiff problems. + - ``rkv98`` - Verner 9(8) with 9th-order dense output, 26 stages (default) + - ``rkv98_nointerp`` - Verner 9(8) without interpolation, 16 stages + - ``rkv87`` - Verner 8(7) with 8th-order dense output, 21 stages + - ``rkv65`` - Verner 6(5), 10 stages + - ``rkts54`` - Tsitouras 5(4) with FSAL, 7 stages + - ``rodas4`` - RODAS4 L-stable Rosenbrock 4(3), 6 stages. For stiff problems. Higher-order integrators can take larger time steps for the same accuracy, so despite having more stages per step, they often require fewer total @@ -2879,21 +2923,19 @@ class propsettings: """This class contains settings used in the high-precision orbit propagator part of the "satkit" python toolbox Notes: - - * Default settings: - * abs_error: 1e-8 - * rel_error: 1e-8 - * gravity_degree: 4 - * gravity_order: 4 - * gravity_model: gravmodel.jgm3 - * use_spaceweather: True - * use_sun_gravity: True - * use_moon_gravity: True - * enable_interp: True - * integrator: integrator.rkv98 - - * enable_interp enables high-precision interpolation of state between begin and end times via the returned function, - it is enabled by default. There is a small increase in computational efficiency if set to false + - Default settings: + - abs_error: 1e-8 + - rel_error: 1e-8 + - gravity_degree: 4 + - gravity_order: 4 + - gravity_model: gravmodel.jgm3 + - use_spaceweather: True + - use_sun_gravity: True + - use_moon_gravity: True + - enable_interp: True + - integrator: integrator.rkv98 + - enable_interp enables high-precision interpolation of state between begin and end times via the returned function, + it is enabled by default. There is a small increase in computational efficiency if set to false """ @@ -2926,7 +2968,7 @@ class propsettings: integrator: ODE integrator to use. Default is integrator.rkv98 Returns: - New propsettings object with default settings + propsettings: New propsettings object with default settings Example: ```python @@ -2946,7 +2988,7 @@ class propsettings: """Maximum absolute value of error for any element in propagated state following ODE integration Returns: - float: Maximum absolute value of error for any element in propagated state following ODE integration, default is 1e-8 + Maximum absolute value of error for any element in propagated state following ODE integration, default is 1e-8 """ ... @@ -2957,7 +2999,7 @@ class propsettings: """Maximum relative error of any element in propagated state following ODE integration Returns: - float: Maximum relative error of any element in propagated state following ODE integration, default is 1e-8 + Maximum relative error of any element in propagated state following ODE integration, default is 1e-8 """ ... @@ -2969,7 +3011,7 @@ class propsettings: """Maximum degree of spherical harmonic gravity model Returns: - int: Maximum degree of spherical harmonic gravity model, default is 4 + Maximum degree of spherical harmonic gravity model, default is 4 """ ... @@ -2981,7 +3023,7 @@ class propsettings: """Maximum order of spherical harmonic gravity model Returns: - int: Maximum order of spherical harmonic gravity model, default is same as gravity_degree + Maximum order of spherical harmonic gravity model, default is same as gravity_degree """ ... @@ -2993,7 +3035,7 @@ class propsettings: """Include sun third-body gravitational perturbation Returns: - bool: Whether sun gravity is enabled, default is True + Whether sun gravity is enabled, default is True """ ... @@ -3005,7 +3047,7 @@ class propsettings: """Include moon third-body gravitational perturbation Returns: - bool: Whether moon gravity is enabled, default is True + Whether moon gravity is enabled, default is True """ ... @@ -3018,13 +3060,13 @@ class propsettings: Notes: - * Space weather data can have a large effect on the density of the atmosphere - * This can be important for accurate drag force calculations - * Space weather data is updated every 3 hours. Most-recent data can be downloaded with ``satkit.utils.update_datafiles()`` - * Default value is True + - Space weather data can have a large effect on the density of the atmosphere + - This can be important for accurate drag force calculations + - Space weather data is updated every 3 hours. Most-recent data can be downloaded with ``satkit.utils.update_datafiles()`` + - Default value is True Returns: - bool: Indicate whether or not space weather data should be used when computing atmospheric density for drag forces + Indicate whether or not space weather data should be used when computing atmospheric density for drag forces """ ... @@ -3077,17 +3119,57 @@ class propsettings: """ ... +def lambert( + r1: npt.NDArray[np.float64], + r2: npt.NDArray[np.float64], + tof: float, + mu: float | None = None, + prograde: bool | None = None, +) -> list[tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]]: + """Solve Lambert's problem using Izzo's algorithm (2015). + + Given two position vectors and a time of flight, find the velocity vectors + for transfer orbits connecting them. + + Args: + r1: 3-element numpy array — departure position (meters) + r2: 3-element numpy array — arrival position (meters) + tof: Time of flight in seconds (must be positive) + mu: Gravitational parameter in m³/s² (default: Earth µ = 3.986e14) + prograde: If True (default), prograde transfer; if False, retrograde + + Returns: + List of (v1, v2) tuples. Each v1 and v2 is a 3-element numpy array + in m/s. The first element is the zero-revolution solution; additional + elements are multi-revolution solutions if they exist. + + Raises: + ValueError: If inputs are invalid (negative tof, zero position, etc.) + + Example: + ```python + import satkit + import numpy as np + + r1 = np.array([7000e3, 0, 0]) + r2 = np.array([0, 7000e3, 0]) + solutions = satkit.lambert(r1, r2, 3600.0) + v1, v2 = solutions[0] + ``` + """ + ... + def propagate( state: npt.NDArray[np.float64], begin: time, - end: time = ..., + end: time | None = None, *, - duration: duration = ..., - duration_secs: float = ..., - duration_days: float = ..., + duration: duration | None = None, + duration_secs: float | None = None, + duration_days: float | None = None, output_phi: bool = False, - propsettings: propsettings = ..., - satproperties: satproperties_static = ..., + propsettings: propsettings | None = None, + satproperties: satproperties_static | None = None, ) -> propresult: """High-precision orbit propagator @@ -3096,16 +3178,19 @@ def propagate( Args: state: 6-element numpy array representing satellite GCRF position and velocity in meters and meters/second begin: Time at which satellite is at input state + + Keyword Args: end: Time at which new position and velocity will be computed - duration: Duration from "begin" at which new position & velocity will be computed - duration_secs: Duration in seconds from "begin" at which new position and velocity will be computed - duration_days: Duration in days from "begin" at which new position and velocity will be computed - output_phi: Output 6x6 state transition matrix between begin and end times + duration: Duration from ``begin`` at which new position & velocity will be computed + duration_secs: Duration in seconds from ``begin`` at which new position and velocity will be computed + duration_days: Duration in days from ``begin`` at which new position and velocity will be computed + output_phi: Output 6x6 state transition matrix between begin and end times. Default is False propsettings: Settings for the propagation; if omitted, defaults are used satproperties: Drag and radiation pressure susceptibility of satellite Returns: - Propagation result object holding state outputs, statistics, and dense output if requested + propresult: Propagation result object holding state outputs, statistics, + and dense output if requested Notes: Propagates satellite ephemeris (position, velocity in GCRF & time) to new time and diff --git a/python/satkit/sun.pyi b/python/satkit/sun.pyi index 57d5128..d0b91b0 100644 --- a/python/satkit/sun.pyi +++ b/python/satkit/sun.pyi @@ -60,6 +60,33 @@ def pos_gcrf( """ ... +def pos_gcrf(*args, **kwargs): + """ + Sun position in the Geocentric Celestial Reference Frame (GCRF) + + Algorithm 29 from Vallado for sun in Mean of Date (MOD), then rotated + from MOD to GCRF via Equations 3-88 and 3-89 in Vallado + + Args: + time (satkit.time): time at which to compute position + + Returns: + 3-element numpy array representing sun position in GCRF frame + at given time. Units are meters + + Notes: + From Vallado: Valid with accuracy of .01 degrees from 1950 to 2050 + + Example: + ```python + import numpy as np + t = satkit.time(2024, 6, 21) + sun = satkit.sun.pos_gcrf(t) + print(f"Sun distance: {np.linalg.norm(sun)/1e9:.3f} million km") + ``` + """ + ... + @typing.overload def pos_mod(time: satkit.time) -> npt.NDArray[np.float64]: """ @@ -101,6 +128,24 @@ def pos_mod( """ ... +def pos_mod(*args, **kwargs): + """ + Sun position in the Mean-of-Date Frame + + Algorithm 29 from Vallado for sun in Mean of Date (MOD) + + Args: + time (satkit.time): time at which to compute position + + Returns: + 3-element numpy array representing sun position in MOD frame + at given time. Units are meters + + Notes: + From Vallado: Valid with accuracy of .01 degrees from 1950 to 2050 + """ + ... + def rise_set( time: satkit.time, coord: satkit.itrfcoord, sigma: float = 90.0 + 50.0 / 60.0 ) -> tuple[satkit.time, satkit.time]: @@ -150,7 +195,7 @@ def shadowfunc( satpos (npt.NDArray[np.float64]): geocentric satellite position, meters Notes: - * See algorithm in Section 3.4.2 of Montenbruck and Gill for calculation + - See algorithm in Section 3.4.2 of Montenbruck and Gill for calculation Returns: float: number in range [0,1] indicating no sun or full sun (no occlusion) hitting satellite diff --git a/python/satkit/utils.pyi b/python/satkit/utils.pyi index b1c269c..6441d3a 100644 --- a/python/satkit/utils.pyi +++ b/python/satkit/utils.pyi @@ -17,21 +17,21 @@ def update_datafiles(**kwargs) -> None: dir(string): Target directory for files. Uses existing data directory if not specified. (see "datadir" function) Notes: - * Files include: - * ``EGM96.gfc`` : EGM-96 Gravity Model Coefficients - * ``JGM3.gfc`` : JGM-3 Gravity Model Coefficients - * ``JGM2.gfc`` : JGM-2 Gravity Model Coefficients - * ``ITU_GRACE16.gfc`` : ITU Grace 16 Gravity - * ``tab5.2a.txt`` : Coefficients for GCRS to GCRF conversion - * ``tab5.2b.txt`` : Coefficients for GCRS to GCRF conversion - * ``tab5.2d.txt`` : Coefficients for GCRS to GCRF conversion - * ``SW-ALL.csv`` : Space weather data, updated daily - * ``predicted-solar-cycle.json`` : NOAA/SWPC solar cycle forecast (~5 years of predicted F10.7) - * ``leap-seconds.txt`` : Leap seconds (UTC vs TAI) - * ``EOP-All.csv`` : Earth orientation parameters, updated daily - * ``linux_p1550p2650.440`` : JPL Ephemeris version 440 (~ 100 MB) - - * The space weather and earth orientation parameters files are updated + - Files include: + - ``EGM96.gfc`` : EGM-96 Gravity Model Coefficients + - ``JGM3.gfc`` : JGM-3 Gravity Model Coefficients + - ``JGM2.gfc`` : JGM-2 Gravity Model Coefficients + - ``ITU_GRACE16.gfc`` : ITU Grace 16 Gravity + - ``tab5.2a.txt`` : Coefficients for GCRS to GCRF conversion + - ``tab5.2b.txt`` : Coefficients for GCRS to GCRF conversion + - ``tab5.2d.txt`` : Coefficients for GCRS to GCRF conversion + - ``SW-ALL.csv`` : Space weather data, updated daily + - ``predicted-solar-cycle.json`` : NOAA/SWPC solar cycle forecast (~5 years of predicted F10.7) + - ``leap-seconds.txt`` : Leap seconds (UTC vs TAI) + - ``EOP-All.csv`` : Earth orientation parameters, updated daily + - ``linux_p1550p2650.440`` : JPL Ephemeris version 440 (~ 100 MB) + + - The space weather and earth orientation parameters files are updated daily and will always be downloaded regardless of the overwrite flag Example: @@ -52,7 +52,7 @@ def datadir() -> str: Data directory is 1st of following directories search that contains the data files listed in "update_datafiles" - * MacOS: + - MacOS: 1. Directory pointed to by ``SATKIT_DATA`` environment variable 2. ``$DYLIB/satkit-data`` where ``$DYLIB`` is directory containing the compiled python satkit library 3. ``$SITE_PACKAGES/satkit_data/data`` where ``$SITE_PACKAGES`` is the parent of ``$DYLIB`` (for the ``satkit_data`` pip package) @@ -61,14 +61,14 @@ def datadir() -> str: 6. ``/usr/share/satkit-data`` 7. ``/Library/Application Support/satkit-data`` - * Linux: + - Linux: 1. Directory pointed to by ``SATKIT_DATA`` environment variable 2. ``$DYLIB/satkit-data`` where ``$DYLIB`` is directory containing the compiled python satkit library 3. ``$SITE_PACKAGES/satkit_data/data`` where ``$SITE_PACKAGES`` is the parent of ``$DYLIB`` (for the ``satkit_data`` pip package) 4. ``$HOME/.satkit-data`` 5. ``/usr/share/satkit-data`` - * Windows: + - Windows: 1. Directory pointed to by ``SATKIT_DATA`` environment variable 2. ``$DYLIB/satkit-data`` where ``$DYLIB`` is directory containing the compiled python satkit library 3. ``$SITE_PACKAGES/satkit_data/data`` where ``$SITE_PACKAGES`` is the parent of ``$DYLIB`` (for the ``satkit_data`` pip package) diff --git a/python/src/lib.rs b/python/src/lib.rs index 764fcb9..dccbcd8 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -25,6 +25,7 @@ mod pysolarsystem; mod pytle; //mod pyukf; +mod pylambert; mod pypropagate; mod pypropsettings; mod pysatproperties; @@ -168,6 +169,8 @@ pub fn satkit(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_function(wrap_pyfunction!(pypropagate::propagate, m)?) .unwrap(); + m.add_function(wrap_pyfunction!(pylambert::lambert, m)?) + .unwrap(); m.add_wrapped(wrap_pymodule!(frametransform))?; m.add_wrapped(wrap_pymodule!(jplephem))?; diff --git a/python/src/pygravity.rs b/python/src/pygravity.rs index 33f52a2..4a8de88 100644 --- a/python/src/pygravity.rs +++ b/python/src/pygravity.rs @@ -4,7 +4,7 @@ use satkit::earthgravity::{accel, accel_and_partials, GravityModel}; use crate::pyitrfcoord::PyITRFCoord; use satkit::itrfcoord::ITRFCoord; -use nalgebra as na; +use satkit::mathtypes::*; use numpy as np; use numpy::PyArrayMethods; @@ -107,7 +107,7 @@ pub fn gravity(pos: &Bound<'_, PyAny>, kwds: Option<&Bound<'_, PyDict>>) -> Resu if vpy.len().unwrap() != 3 { bail!("Input must have 3 elements"); } - let v: na::Vector3 = na::Vector3::::from_row_slice(vpy.as_slice().unwrap()); + let v: Vector3 = Vector3::from_slice(vpy.as_slice().unwrap()); let a = accel(&v, degree, order, model.into()); pyo3::Python::attach(|py| -> Result> { let vpy = np::PyArray1::::from_slice(py, a.as_slice()); @@ -173,7 +173,7 @@ pub fn gravity_and_partials( let gpy = np::PyArray1::::from_slice(py, g.as_slice()); let ppy = unsafe { np::PyArray2::::new(py, [3, 3], false) }; unsafe { - std::ptr::copy_nonoverlapping(p.as_ptr(), ppy.as_raw_array_mut().as_mut_ptr(), 9); + std::ptr::copy_nonoverlapping(p.as_slice().as_ptr(), ppy.as_raw_array_mut().as_mut_ptr(), 9); } Ok((gpy.into_py_any(py)?, ppy.into_py_any(py)?)) }) @@ -182,13 +182,13 @@ pub fn gravity_and_partials( if vpy.len().unwrap() != 3 { bail!("Input must have 3 elements"); } - let v: na::Vector3 = na::Vector3::::from_row_slice(vpy.as_slice().unwrap()); + let v: Vector3 = Vector3::from_slice(vpy.as_slice().unwrap()); let (g, p) = accel_and_partials(&v, degree, order, model.into()); pyo3::Python::attach(|py| -> Result<(Py, Py)> { let gpy = np::PyArray1::::from_slice(py, g.as_slice()); let ppy = unsafe { np::PyArray2::::new(py, [3, 3], false) }; unsafe { - std::ptr::copy_nonoverlapping(p.as_ptr(), ppy.as_raw_array_mut().as_mut_ptr(), 9); + std::ptr::copy_nonoverlapping(p.as_slice().as_ptr(), ppy.as_raw_array_mut().as_mut_ptr(), 9); } Ok((gpy.into_py_any(py)?, ppy.into_py_any(py)?)) }) diff --git a/python/src/pyitrfcoord.rs b/python/src/pyitrfcoord.rs index d56cde5..8ba9648 100644 --- a/python/src/pyitrfcoord.rs +++ b/python/src/pyitrfcoord.rs @@ -248,7 +248,7 @@ impl PyITRFCoord { #[getter] fn get_vector(&self) -> Py { pyo3::Python::attach(|py| -> Py { - numpy::PyArray::from_slice(py, self.0.itrf.data.as_slice()) + numpy::PyArray::from_slice(py, self.0.itrf.as_slice()) .into_py_any(py) .unwrap() }) @@ -298,7 +298,7 @@ impl PyITRFCoord { fn to_enu(&self, refcoord: &Self) -> Py { let v = refcoord.0.q_enu2itrf().conjugate() * (self.0.itrf - refcoord.0.itrf); pyo3::Python::attach(|py| -> Py { - numpy::PyArray::from_slice(py, v.data.as_slice()) + numpy::PyArray::from_slice(py, v.as_slice()) .into_py_any(py) .unwrap() }) @@ -314,7 +314,7 @@ impl PyITRFCoord { fn to_ned(&self, other: &Self) -> Py { let v = other.0.q_ned2itrf().conjugate() * (self.0.itrf - other.0.itrf); pyo3::Python::attach(|py| -> Py { - numpy::PyArray::from_slice(py, v.data.as_slice()) + numpy::PyArray::from_slice(py, v.as_slice()) .into_py_any(py) .unwrap() }) @@ -378,7 +378,7 @@ impl PyITRFCoord { let x = f64::from_le_bytes(s[0..8].try_into()?); let y = f64::from_le_bytes(s[8..16].try_into()?); let z = f64::from_le_bytes(s[16..24].try_into()?); - self.0.itrf = nalgebra::Vector3::::new(x, y, z); + self.0.itrf = numeris::vector![x, y, z]; Ok(()) } diff --git a/python/src/pyjplephem.rs b/python/src/pyjplephem.rs index 12cf5da..5a380f2 100644 --- a/python/src/pyjplephem.rs +++ b/python/src/pyjplephem.rs @@ -3,9 +3,9 @@ use pyo3::prelude::*; use crate::pysolarsystem; use crate::pyutils::*; use satkit::jplephem; +use satkit::mathtypes::*; use satkit::SolarSystem; use satkit::Instant; -use nalgebra as na; use anyhow::Result; @@ -23,7 +23,7 @@ pub fn geocentric_state( tm: &Bound<'_, PyAny>, ) -> PyResult> { let rbody: SolarSystem = body.into(); - let f = |tm: &Instant| -> Result<(na::Vector3, na::Vector3)> { + let f = |tm: &Instant| -> Result<(Vector3, Vector3)> { jplephem::geocentric_state(rbody, tm) }; tuple_func_of_time_arr(f, tm) @@ -52,7 +52,7 @@ pub fn barycentric_state( tm: &Bound<'_, PyAny>, ) -> PyResult> { let rbody: SolarSystem = body.into(); - let f = |tm: &Instant| -> Result<(na::Vector3, na::Vector3)> { + let f = |tm: &Instant| -> Result<(Vector3, Vector3)> { jplephem::barycentric_state(rbody, tm) }; tuple_func_of_time_arr(f, tm) @@ -72,7 +72,7 @@ pub fn geocentric_pos( tm: &Bound<'_, PyAny>, ) -> Result> { let rbody: SolarSystem = body.into(); - let f = |tm: &Instant| -> Result> { jplephem::geocentric_pos(rbody, tm) }; + let f = |tm: &Instant| -> Result { jplephem::geocentric_pos(rbody, tm) }; py_vec3_of_time_result_arr(&f, tm) } @@ -97,6 +97,6 @@ pub fn barycentric_pos( tm: &Bound<'_, PyAny>, ) -> Result> { let rbody: SolarSystem = body.into(); - let f = |tm: &Instant| -> Result> { jplephem::barycentric_pos(rbody, tm) }; + let f = |tm: &Instant| -> Result { jplephem::barycentric_pos(rbody, tm) }; py_vec3_of_time_result_arr(&f, tm) } diff --git a/python/src/pylambert.rs b/python/src/pylambert.rs new file mode 100644 index 0000000..f6a27d7 --- /dev/null +++ b/python/src/pylambert.rs @@ -0,0 +1,80 @@ +use numpy::{PyArray1, PyArrayMethods, PyUntypedArrayMethods}; +use pyo3::prelude::*; + +use satkit::mathtypes::Vector3; + +/// Solve Lambert's problem: find the transfer orbit between two positions. +/// +/// Given two position vectors and a time of flight, compute the departure and +/// arrival velocity vectors for the transfer orbit connecting them. +/// +/// Args: +/// r1 (numpy.ndarray): Initial position vector, 3-element array (meters) +/// r2 (numpy.ndarray): Final position vector, 3-element array (meters) +/// tof (float): Time of flight in seconds (must be positive) +/// mu (float, optional): Gravitational parameter in m³/s² (default: Earth μ) +/// prograde (bool, optional): If True (default), use prograde transfer; +/// if False, use retrograde transfer. +/// +/// Returns: +/// list[tuple[numpy.ndarray, numpy.ndarray]]: List of (v1, v2) solution pairs. +/// Each v1 is the departure velocity and v2 is the arrival velocity +/// (3-element numpy arrays in m/s). The first element is the +/// zero-revolution solution. +/// +/// Raises: +/// ValueError: If inputs are invalid (negative tof, zero position, etc.) +/// RuntimeError: If the solver fails to converge +/// +/// Example: +/// >>> import satkit +/// >>> import numpy as np +/// >>> r1 = np.array([7000e3, 0, 0]) +/// >>> r2 = np.array([0, 7000e3, 0]) +/// >>> solutions = satkit.lambert(r1, r2, 3600.0) +/// >>> v1, v2 = solutions[0] +/// +#[pyfunction] +#[pyo3(signature = (r1, r2, tof, mu=None, prograde=None))] +pub fn lambert( + r1: &Bound<'_, PyArray1>, + r2: &Bound<'_, PyArray1>, + tof: f64, + mu: Option, + prograde: Option, +) -> PyResult>, Py>)>> { + if r1.len() != 3 { + return Err(pyo3::exceptions::PyValueError::new_err( + "r1 must be a 3-element array", + )); + } + if r2.len() != 3 { + return Err(pyo3::exceptions::PyValueError::new_err( + "r2 must be a 3-element array", + )); + } + + let r1_slice = unsafe { r1.as_slice()? }; + let r2_slice = unsafe { r2.as_slice()? }; + + let r1_vec = Vector3::from_slice(r1_slice); + let r2_vec = Vector3::from_slice(r2_slice); + + let mu_val = mu.unwrap_or(satkit::consts::MU_EARTH); + let prograde_val = prograde.unwrap_or(true); + + let solutions = satkit::lambert::lambert(&r1_vec, &r2_vec, tof, mu_val, prograde_val) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + + Python::attach(|py| { + let result: Vec<(Py>, Py>)> = solutions + .iter() + .map(|(v1, v2)| { + let v1_arr = PyArray1::from_slice(py, v1.as_slice()).unbind(); + let v2_arr = PyArray1::from_slice(py, v2.as_slice()).unbind(); + (v1_arr, v2_arr) + }) + .collect(); + Ok(result) + }) +} diff --git a/python/src/pypropagate.rs b/python/src/pypropagate.rs index d9300ea..ff08f1e 100644 --- a/python/src/pypropagate.rs +++ b/python/src/pypropagate.rs @@ -6,8 +6,6 @@ use crate::pysatproperties::PySatProperties; use crate::pyutils::*; use pyo3::IntoPyObjectExt; -use nalgebra as na; - use satkit::mathtypes::*; use satkit::orbitprop::SatProperties; use satkit::orbitprop::SatPropertiesStatic; @@ -224,10 +222,9 @@ pub fn propagate( // Propagate with state transition matrix else { // Create the state to propagate - let mut pv = na::SMatrix::::zeros(); - pv.fixed_view_mut::<6, 1>(0, 0).copy_from(&state0); - pv.fixed_view_mut::<6, 6>(0, 1) - .copy_from(&Matrix6::identity()); + let mut pv = Matrix67::zeros(); + pv.set_block(0, 0, &state0); + pv.set_block(0, 1, &Matrix6::eye()); let res = satkit::orbitprop::propagate(&pv, &begintime, &endtime, &propsettings, satproperties)?; diff --git a/python/src/pypropresult.rs b/python/src/pypropresult.rs index 4b3dd5e..eb34e43 100644 --- a/python/src/pypropresult.rs +++ b/python/src/pypropresult.rs @@ -87,21 +87,22 @@ impl PyPropStats { pub struct PyPropResult(pub PyPropResultType); fn to_string(r: &PropagationResult) -> String { + let se = r.state_end.as_slice(); let mut s = "Propagation Results\n".to_string(); s.push_str(format!(" Time: {}\n", r.time_end).as_str()); s.push_str( format!( " Pos: [{:.3}, {:.3}, {:.3}] km\n", - r.state_end[0] * 1.0e-3, - r.state_end[1] * 1.0e-3, - r.state_end[2] * 1.0e-3 + se[0] * 1.0e-3, + se[1] * 1.0e-3, + se[2] * 1.0e-3 ) .as_str(), ); s.push_str( format!( " Vel: [{:.3}, {:.3}, {:.3}] m/s\n", - r.state_end[3], r.state_end[4], r.state_end[5] + se[3], se[4], se[5] ) .as_str(), ); @@ -200,7 +201,8 @@ impl PyPropResult { .to_pyarray(py) .into_py_any(py), PyPropResultType::R7(r) => { - np::ndarray::arr1(&r.state_end.column(0).as_slice()[3..6]) + let col0 = r.state_end.col(0); + np::ndarray::arr1(&col0.as_slice()[3..6]) .to_pyarray(py) .into_py_any(py) } @@ -257,10 +259,10 @@ impl PyPropResult { PyPropResultType::R1(_r) => Ok(py.None()), PyPropResultType::R7(r) => { let phi = unsafe { np::PyArray2::::new(py, [6, 6], false) }; - let rsphi = r.state_end.fixed_view::<6, 6>(0, 1).transpose(); + let rsphi = r.state_end.block::<6, 6>(0, 1).transpose(); unsafe { std::ptr::copy_nonoverlapping( - rsphi.as_ptr(), + rsphi.as_slice().as_ptr(), phi.as_raw_array_mut().as_mut_ptr(), 36, ); @@ -335,7 +337,7 @@ impl PyPropResult { slice2py1d(py, &res.as_slice()[0..6]) }) } else { - let rsphi = res.fixed_view::<6, 6>(0, 1).transpose(); + let rsphi = res.block::<6, 6>(0, 1).transpose(); pyo3::Python::attach(|py| -> PyResult> { ( slice2py1d(py, &res.as_slice()[0..6])?, diff --git a/python/src/pyquaternion.rs b/python/src/pyquaternion.rs index a67e01e..32c883e 100644 --- a/python/src/pyquaternion.rs +++ b/python/src/pyquaternion.rs @@ -58,9 +58,7 @@ impl PyQuaternion { let x = args.get_item(1)?.extract::()?; let y = args.get_item(2)?.extract::()?; let z = args.get_item(3)?.extract::()?; - // Create a nalgebra quaternion from 4 input scalars - let q = nalgebra::Quaternion::::new(w, x, y, z); - Ok(Quaternion::from_quaternion(q).into()) + Ok(Quaternion::new(w, x, y, z).into()) } else { bail!("Invalid input. Must be empty or 4 floats"); } @@ -79,7 +77,7 @@ impl PyQuaternion { /// e.g. rotation of +xhat 90 degrees by +zhat gives +yhat #[staticmethod] fn rotx(theta_rad: f64) -> Result { - Ok(Quaternion::from_axis_angle(&Vector3::x_axis(), theta_rad).into()) + Ok(Quaternion::rotx(theta_rad).into()) } /// Quaternion representing rotation about yhat axis by `theta-rad` degrees @@ -96,14 +94,14 @@ impl PyQuaternion { /// #[staticmethod] fn roty(theta_rad: f64) -> Result { - Ok(Quaternion::from_axis_angle(&Vector3::y_axis(), theta_rad).into()) + Ok(Quaternion::roty(theta_rad).into()) } /// Quaternion representing rotation about /// zhat axis by `theta-rad` degrees #[staticmethod] fn rotz(theta_rad: f64) -> Result { - Ok(Quaternion::from_axis_angle(&Vector3::z_axis(), theta_rad).into()) + Ok(Quaternion::rotz(theta_rad).into()) } /// Quaternion representing rotation about given axis by given angle in radians @@ -118,13 +116,14 @@ impl PyQuaternion { /// #[staticmethod] fn from_axis_angle(axis: np::PyReadonlyArray1, angle: f64) -> Result { - let v = Vector3::from_row_slice(axis.as_slice()?); - let u = nalgebra::UnitVector3::try_new(v, 1.0e-9); - if let Some(unit_axis) = u { - Ok(Quaternion::from_axis_angle(&unit_axis, angle).into()) - } else { + let s = axis.as_slice()?; + let v = numeris::vector![s[0], s[1], s[2]]; + let n = v.norm(); + if n < 1.0e-9 { // If the axis is zero, return identity quaternion Ok(Quaternion::identity().into()) + } else { + Ok(Quaternion::from_axis_angle(v, angle).into()) } } @@ -146,27 +145,41 @@ impl PyQuaternion { } let v1 = match v1.is_contiguous() { true => { - Vector3::from_row_slice(v1.as_slice().context("Cannot convert v1 to 3D vector")?) + let s = v1.as_slice().context("Cannot convert v1 to 3D vector")?; + numeris::vector![s[0], s[1], s[2]] } - false => Vector3::from_row_slice(&[ + false => numeris::vector![ *v1.get(0).unwrap(), *v1.get(1).unwrap(), *v1.get(2).unwrap(), - ]), + ], }; let v2 = match v2.is_contiguous() { true => { - Vector3::from_row_slice(v2.as_slice().context("Cannot convert vd2 to 3D vector")?) + let s = v2.as_slice().context("Cannot convert v2 to 3D vector")?; + numeris::vector![s[0], s[1], s[2]] } - false => Vector3::from_row_slice(&[ + false => numeris::vector![ *v2.get(0).unwrap(), *v2.get(1).unwrap(), *v2.get(2).unwrap(), - ]), + ], }; - let q = Quaternion::rotation_between(&v1, &v2) - .context("Norms are 0 or vectors are 180° apart")?; + // Compute rotation between two vectors + let n1 = v1.norm(); + let n2 = v2.norm(); + if n1 < 1.0e-9 || n2 < 1.0e-9 { + bail!("Norms are 0 or vectors are 180° apart"); + } + let u1 = v1 * (1.0 / n1); + let u2 = v2 * (1.0 / n2); + let cross = u1.cross(&u2); + let dot = u1.dot(&u2); + if cross.norm() < 1.0e-9 && dot < 0.0 { + bail!("Norms are 0 or vectors are 180° apart"); + } + let q = Quaternion::from_axis_angle(cross, dot.clamp(-1.0, 1.0).acos()); Ok(q.into()) } @@ -183,10 +196,13 @@ impl PyQuaternion { bail!("Invalid DCM. Must be 3x3 matrix"); } let dcm = dcm.as_array(); - let mat = nalgebra::Matrix3::from_iterator(dcm.iter().cloned()); - let rot = nalgebra::Rotation3::from_matrix(&mat.transpose()); - - Ok(Quaternion::from_rotation_matrix(&rot).into()) + // numpy arrays are row-major, build Matrix3 row-by-row + let mat = Matrix3::new([ + [dcm[(0, 0)], dcm[(0, 1)], dcm[(0, 2)]], + [dcm[(1, 0)], dcm[(1, 1)], dcm[(1, 2)]], + [dcm[(2, 0)], dcm[(2, 1)], dcm[(2, 2)]], + ]); + Ok(Quaternion::from_rotation_matrix(&mat).into()) } /// Return rotation matrix representing identical rotation to quaternion @@ -200,7 +216,7 @@ impl PyQuaternion { let phi = unsafe { np::PyArray2::::new(py, [3, 3], true) }; unsafe { std::ptr::copy_nonoverlapping( - rot.matrix().as_ptr(), + rot.as_slice().as_ptr(), phi.as_raw_array_mut().as_mut_ptr(), 9, ); @@ -214,15 +230,17 @@ impl PyQuaternion { /// Returns: /// (f64, f64, f64): Tuple of roll, pitch, yaw angles in radians fn as_euler(&self) -> (f64, f64, f64) { - self.0.euler_angles() + self.0.to_euler() } fn __str__(&self) -> Result { - let ax: nalgebra::Unit = self.0.axis().map_or_else( - || nalgebra::Unit::new_normalize(Vector3::new(1.0, 0.0, 0.0)), - |v| v, - ); - let angle = self.0.angle(); + let (ax, angle) = self.0.to_axis_angle(); + let n = ax.norm(); + let ax = if n < 1.0e-9 { + numeris::vector![1.0, 0.0, 0.0] + } else { + ax * (1.0 / n) + }; Ok(format!( "Quaternion(Axis = [{:6.4}, {:6.4}, {:6.4}], Angle = {:6.4} rad)", ax[0], ax[1], ax[2], angle @@ -244,16 +262,16 @@ impl PyQuaternion { let x = f64::from_le_bytes(state[8..16].try_into()?); let y = f64::from_le_bytes(state[16..24].try_into()?); let z = f64::from_le_bytes(state[24..32].try_into()?); - self.0 = Quaternion::from_quaternion(nalgebra::Quaternion::::new(w, x, y, z)); + self.0 = Quaternion::new(w, x, y, z); Ok(()) } fn __getstate__(&self, py: Python) -> PyResult> { let mut raw = [0; 32]; raw[0..8].clone_from_slice(f64::to_le_bytes(self.0.w).as_slice()); - raw[8..16].clone_from_slice(f64::to_le_bytes(self.0.i).as_slice()); - raw[16..24].clone_from_slice(f64::to_le_bytes(self.0.j).as_slice()); - raw[24..32].clone_from_slice(f64::to_le_bytes(self.0.k).as_slice()); + raw[8..16].clone_from_slice(f64::to_le_bytes(self.0.x).as_slice()); + raw[16..24].clone_from_slice(f64::to_le_bytes(self.0.y).as_slice()); + raw[24..32].clone_from_slice(f64::to_le_bytes(self.0.z).as_slice()); PyBytes::new(py, &raw).into_py_any(py) } @@ -263,7 +281,7 @@ impl PyQuaternion { /// float: Angle of rotation in radians #[getter] fn angle(&self) -> f64 { - self.0.angle() + self.0.to_axis_angle().1 } /// Axis of rotation @@ -272,7 +290,13 @@ impl PyQuaternion { /// numpy.ndarray: 3-element numpy array representing axis of rotation #[getter] fn axis(&self) -> PyResult> { - let a = self.0.axis().map_or_else(Vector3::x_axis, |ax| ax); + let (ax, _) = self.0.to_axis_angle(); + let n = ax.norm(); + let a = if n < 1.0e-9 { + numeris::vector![1.0, 0.0, 0.0] + } else { + ax * (1.0 / n) + }; pyo3::Python::attach(|py| -> PyResult> { numpy::ndarray::arr1(a.as_slice()) .to_pyarray(py) @@ -300,17 +324,17 @@ impl PyQuaternion { #[getter] fn x(&self) -> f64 { - self.0.i + self.0.x } #[getter] fn y(&self) -> f64 { - self.0.j + self.0.y } #[getter] fn z(&self) -> f64 { - self.0.k + self.0.z } #[getter] @@ -327,12 +351,10 @@ impl PyQuaternion { /// /// Returns: /// quaternion: Quaterion represention fracional spherical interpolation between self and other - #[pyo3(signature=(other, frac, epsilon=1.0e-6))] + #[pyo3(signature=(other, frac, epsilon=1.0e-6))] + #[allow(unused_variables)] fn slerp(&self, other: &Self, frac: f64, epsilon: f64) -> Result { - self.0.try_slerp(&other.0, frac, epsilon).map_or_else( - || bail!("Quaternions cannot be 180 deg apart"), - |v| Ok(v.into()), - ) + Ok(self.0.slerp(&other.0, frac).into()) } fn __mul__(&self, other: &Bound<'_, PyAny>) -> Result> { @@ -351,10 +373,10 @@ impl PyQuaternion { bail!("Invalid rhs. 2nd dimension must be 3 in size"); } let rot = self.0.to_rotation_matrix(); - let qmat = rot.matrix().conjugate(); + let qmat = rot.transpose(); Ok(pyo3::Python::attach(|py| -> PyResult> { - let nd = unsafe { np::ndarray::ArrayView2::from_shape_ptr((3, 3), qmat.as_ptr()) }; + let nd = unsafe { np::ndarray::ArrayView2::from_shape_ptr((3, 3), qmat.as_slice().as_ptr()) }; let res = v.readonly().as_array().dot(&nd).to_pyarray(py); res.into_py_any(py) @@ -364,10 +386,10 @@ impl PyQuaternion { bail!("Invalid rhs. 1D array must be of length 3"); } - let m = nalgebra::vector![ + let m = numeris::vector![ v1d.get_owned(0).unwrap(), v1d.get_owned(1).unwrap(), - v1d.get_owned(2).unwrap() + v1d.get_owned(2).unwrap(), ]; let vout = self.0 * m; diff --git a/python/src/pysatstate.rs b/python/src/pysatstate.rs index 7c687db..bd44e2c 100644 --- a/python/src/pysatstate.rs +++ b/python/src/pysatstate.rs @@ -37,15 +37,15 @@ impl PySatState { let mut state = SatState::from_pv( &time.0, - &nalgebra::vector![ + &numeris::vector![ pos.get_owned(0).unwrap(), pos.get_owned(1).unwrap(), - pos.get_owned(2).unwrap() + pos.get_owned(2).unwrap(), ], - &nalgebra::vector![ + &numeris::vector![ vel.get_owned(0).unwrap(), vel.get_owned(1).unwrap(), - vel.get_owned(2).unwrap() + vel.get_owned(2).unwrap(), ], ); @@ -54,7 +54,7 @@ impl PySatState { if dims[0] != 6 || dims[1] != 6 { bail!("Covariance must be 6x6 numpy array"); } - let nacov = Matrix6::from_row_slice(unsafe { cov.as_slice().unwrap() }); + let nacov = Matrix6::from_slice(unsafe { cov.as_slice().unwrap() }).transpose(); state.set_cov(StateCov::PVCov(nacov)); } @@ -77,7 +77,7 @@ impl PySatState { "Position uncertainty must be 1-d numpy array with length 3", )); } - let na_sigma_pvh = Vector3::from_row_slice(unsafe { sigma_pvh.as_slice().unwrap() }); + let na_sigma_pvh = Vector3::from_slice(unsafe { sigma_pvh.as_slice().unwrap() }); self.0.set_lvlh_pos_uncertainty(&na_sigma_pvh); Ok(()) @@ -97,7 +97,7 @@ impl PySatState { if sigma_cart.len() != 3 { bail!("Position uncertainty must be 1-d numpy array with length 3"); } - let na_sigma_cart = Vector3::from_row_slice(unsafe { sigma_cart.as_slice().unwrap() }); + let na_sigma_cart = Vector3::from_slice(unsafe { sigma_cart.as_slice().unwrap() }); self.0.set_gcrf_pos_uncertainty(&na_sigma_cart); Ok(()) @@ -117,7 +117,7 @@ impl PySatState { "Covariance must be 6x6 numpy array", )); } - let na_cov = Matrix6::from_row_slice(unsafe { cov.as_slice().unwrap() }); + let na_cov = Matrix6::from_slice(unsafe { cov.as_slice().unwrap() }).transpose(); self.0.cov = StateCov::PVCov(na_cov); Ok(()) } @@ -130,7 +130,7 @@ impl PySatState { #[getter] fn get_pos_gcrf(&self) -> Py { pyo3::Python::attach(|py| -> Py { - np::PyArray1::from_slice(py, self.0.pv.fixed_view::<3, 1>(0, 0).as_slice()) + np::PyArray1::from_slice(py, &self.0.pv.as_slice()[0..3]) .into_py_any(py) .unwrap() }) @@ -139,7 +139,7 @@ impl PySatState { #[getter] fn get_vel_gcrf(&self) -> Py { pyo3::Python::attach(|py| -> Py { - np::PyArray1::from_slice(py, self.0.pv.fixed_view::<3, 1>(3, 0).as_slice()) + np::PyArray1::from_slice(py, &self.0.pv.as_slice()[3..6]) .into_py_any(py) .unwrap() }) @@ -188,7 +188,7 @@ impl PySatState { #[getter] fn get_pos(&self) -> Py { pyo3::Python::attach(|py| -> Py { - np::PyArray1::from_slice(py, self.0.pv.fixed_view::<3, 1>(0, 0).as_slice()) + np::PyArray1::from_slice(py, &self.0.pv.as_slice()[0..3]) .into_py_any(py) .unwrap() }) @@ -196,7 +196,7 @@ impl PySatState { #[getter] fn get_vel(&self) -> Py { pyo3::Python::attach(|py| -> Py { - np::PyArray1::from_slice(py, self.0.pv.fixed_view::<3, 1>(3, 0).as_slice()) + np::PyArray1::from_slice(py, &self.0.pv.as_slice()[3..6]) .into_py_any(py) .unwrap() }) @@ -280,13 +280,13 @@ impl PySatState { satkit::TimeScale::TAI, ); - let pv = Vector6::from_row_slice(unsafe { + let pv = Vector6::from_slice(unsafe { std::slice::from_raw_parts(state[8..56].as_ptr() as *const f64, 6) }); self.0.time = time; self.0.pv = pv; if state.len() >= 92 { - let cov = Matrix6::from_row_slice(unsafe { + let cov = Matrix6::from_slice(unsafe { std::slice::from_raw_parts(state[56..].as_ptr() as *const f64, 36) }); self.0.cov = StateCov::PVCov(cov); @@ -310,14 +310,14 @@ impl PySatState { ); unsafe { buffer[8..56].clone_from_slice(std::slice::from_raw_parts( - self.0.pv.as_ptr() as *const u8, + self.0.pv.as_slice().as_ptr() as *const u8, 48, )); } if let StateCov::PVCov(cov) = self.0.cov { unsafe { buffer[56..].clone_from_slice(std::slice::from_raw_parts( - cov.as_ptr() as *const u8, + cov.as_slice().as_ptr() as *const u8, 36 * 8, )); } diff --git a/python/src/pysgp4.rs b/python/src/pysgp4.rs index 3aef515..1c50d05 100644 --- a/python/src/pysgp4.rs +++ b/python/src/pysgp4.rs @@ -314,18 +314,18 @@ pub fn sgp4( let dims = if states.pos.nrows() > 1 && states.pos.ncols() > 1 { vec![states.pos.ncols(), states.pos.nrows()] } else { - vec![states.pos.len()] + vec![states.pos.as_slice().len()] }; // Note: this is a little confusing: ndarray uses - // row major, nalgebra and numpy use column major, + // row major, numeris and numpy use column major, // hence the switch if !output_err { Ok(( - PyArray1::from_slice(py, states.pos.data.as_slice()) + PyArray1::from_slice(py, states.pos.as_slice()) .reshape(dims.clone())? .into_py_any(py)?, - PyArray1::from_slice(py, states.vel.data.as_slice()) + PyArray1::from_slice(py, states.vel.as_slice()) .reshape(dims)? .into_py_any(py)?, ) @@ -333,8 +333,8 @@ pub fn sgp4( } else { let eint: Vec = states.errcode.iter().map(|x| *x as i32).collect(); Ok(( - PyArray1::from_slice(py, states.pos.data.as_slice()).reshape(dims.clone())?, - PyArray1::from_slice(py, states.vel.data.as_slice()).reshape(dims.clone())?, + PyArray1::from_slice(py, states.pos.as_slice()).reshape(dims.clone())?, + PyArray1::from_slice(py, states.vel.as_slice()).reshape(dims.clone())?, PyArray1::from_slice(py, eint.as_slice()), ) .into_py_any(py)?) @@ -358,18 +358,18 @@ pub fn sgp4( let dims = if states.pos.nrows() > 1 && states.pos.ncols() > 1 { vec![states.pos.ncols(), states.pos.nrows()] } else { - vec![states.pos.len()] + vec![states.pos.as_slice().len()] }; // Note: this is a little confusing: ndarray uses - // row major, nalgebra and numpy use column major, + // row major, numeris and numpy use column major, // hence the switch if !output_err { Ok(( - PyArray1::from_slice(py, states.pos.data.as_slice()) + PyArray1::from_slice(py, states.pos.as_slice()) .reshape(dims.clone())? .into_py_any(py)?, - PyArray1::from_slice(py, states.vel.data.as_slice()) + PyArray1::from_slice(py, states.vel.as_slice()) .reshape(dims)? .into_py_any(py)?, ) @@ -377,8 +377,8 @@ pub fn sgp4( } else { let eint: Vec = states.errcode.iter().map(|x| x.clone() as i32).collect(); Ok(( - PyArray1::from_slice(py, states.pos.data.as_slice()).reshape(dims.clone())?, - PyArray1::from_slice(py, states.vel.data.as_slice()).reshape(dims.clone())?, + PyArray1::from_slice(py, states.pos.as_slice()).reshape(dims.clone())?, + PyArray1::from_slice(py, states.vel.as_slice()).reshape(dims.clone())?, PyArray1::from_slice(py, eint.as_slice()), ) .into_py_any(py)?) @@ -432,13 +432,13 @@ pub fn sgp4( let pdata: *mut f64 = parr.data(); std::ptr::copy_nonoverlapping( - states.pos.as_ptr(), + states.pos.as_slice().as_ptr(), pdata.add(idx * ntimes * 3), ntimes * 3, ); let vdata: *mut f64 = varr.data(); std::ptr::copy_nonoverlapping( - states.vel.as_ptr(), + states.vel.as_slice().as_ptr(), vdata.add(idx * ntimes * 3), ntimes * 3, ); diff --git a/python/src/pyutils.rs b/python/src/pyutils.rs index a70f768..8256167 100644 --- a/python/src/pyutils.rs +++ b/python/src/pyutils.rs @@ -4,7 +4,6 @@ use crate::pyquaternion::PyQuaternion; use satkit::mathtypes::*; use satkit::Instant; -use nalgebra as na; use numpy as np; use numpy::ndarray; @@ -87,7 +86,7 @@ pub fn py_vec3_of_time_arr( // never fail unsafe { std::ptr::copy_nonoverlapping( - v.as_ptr(), + v.as_slice().as_ptr(), out.as_raw_array_mut().as_mut_ptr().offset(idx as isize * 3), 3, ); @@ -124,7 +123,7 @@ pub fn py_vec3_of_time_result_arr( // never fail unsafe { std::ptr::copy_nonoverlapping( - v.as_ptr(), + v.as_slice().as_ptr(), out.as_raw_array_mut().as_mut_ptr().offset(idx as isize * 3), 3, ); @@ -164,25 +163,22 @@ pub fn py_to_smatrix(obj: &Bound) -> Resu pyo3::exceptions::PyValueError::new_err(format!("Invalid array shape: {}", e)) })?; if arr.is_contiguous() { - m.copy_from_slice(arr.as_slice()?); + m.as_mut_slice().copy_from_slice(arr.as_slice()?); } else { let arr = arr.as_array(); for row in 0..M { - m[row] = arr[row]; + m[(row, 0)] = arr[row]; } } } else if obj.is_instance_of::>() { let arr = obj.extract::>().map_err(|e| { pyo3::exceptions::PyValueError::new_err(format!("Invalid array shape: {}", e)) })?; - if arr.is_contiguous() { - m.copy_from_slice(arr.as_slice()?); - } else { - let arr = arr.as_array(); - for row in 0..M { - for col in 0..N { - m[(row, col)] = arr[(row, col)]; - } + // Element-by-element to handle numpy row-major to numeris column-major + let arr = arr.as_array(); + for row in 0..M { + for col in 0..N { + m[(row, col)] = arr[(row, col)]; } } } @@ -244,7 +240,7 @@ pub fn slice2py2d(py: Python, s: &[f64], rows: usize, cols: usize) -> PyResult

(py: Python, m: &Matrix) -> Py { let p = unsafe { PyArray2::::new(py, [M, N], true) }; unsafe { - std::ptr::copy_nonoverlapping(m.as_ptr(), p.as_raw_array_mut().as_mut_ptr(), M * N); + std::ptr::copy_nonoverlapping(m.as_slice().as_ptr(), p.as_raw_array_mut().as_mut_ptr(), M * N); } p.into_py_any(py).unwrap() } @@ -252,7 +248,7 @@ pub fn mat2py(py: Python, m: &Matrix) -> P #[inline] pub fn tuple_func_of_time_arr(cfunc: F, tmarr: &Bound<'_, PyAny>) -> PyResult> where - F: Fn(&Instant) -> Result<(na::Vector3, na::Vector3)>, + F: Fn(&Instant) -> Result<(Vector3, Vector3)>, { let tm = tmarr.to_time_vec()?; match tm.len() { diff --git a/python/test/test.py b/python/test/test.py index 866bd86..5f60882 100755 --- a/python/test/test.py +++ b/python/test/test.py @@ -1212,3 +1212,114 @@ def test_sgp4_vallado(self): assert eflag == sk.sgp4_error.perturb_eccen except RuntimeError: print("Caught runtime error; this is expected in test vectors") + + +class TestLambert: + """Tests for the Lambert solver""" + + def test_90deg_transfer(self): + """90-degree prograde transfer at constant radius""" + r1 = np.array([7000e3, 0, 0]) + r2 = np.array([0, 7000e3, 0]) + period = 2 * np.pi * np.sqrt(7000e3**3 / sk.consts.mu_earth) + tof = period / 4.0 + + sols = sk.lambert(r1, r2, tof) + assert len(sols) >= 1 + v1, v2 = sols[0] + + # Energy conservation + e1 = np.dot(v1, v1) / 2 - sk.consts.mu_earth / np.linalg.norm(r1) + e2 = np.dot(v2, v2) / 2 - sk.consts.mu_earth / np.linalg.norm(r2) + assert e1 == pytest.approx(e2, rel=1e-8) + + # Angular momentum conservation + h1 = np.cross(r1, v1) + h2 = np.cross(r2, v2) + np.testing.assert_allclose(h1, h2, rtol=1e-8) + + # Symmetric transfer: speeds should match + assert np.linalg.norm(v1) == pytest.approx(np.linalg.norm(v2), rel=1e-6) + + def test_hohmann(self): + """Hohmann (180-degree) transfer between circular orbits""" + r1_mag = 7000e3 + r2_mag = 10000e3 + r1 = np.array([r1_mag, 0, 0]) + r2 = np.array([-r2_mag, 0, 0]) + + a_t = (r1_mag + r2_mag) / 2 + tof = np.pi * np.sqrt(a_t**3 / sk.consts.mu_earth) + + sols = sk.lambert(r1, r2, tof) + v1, v2 = sols[0] + + # Radial velocity should be ~0 for Hohmann + assert abs(v1[0]) < 10.0 + # Tangential velocity should be positive (prograde) + assert v1[1] > 0 + + # Energy conservation + e1 = np.dot(v1, v1) / 2 - sk.consts.mu_earth / r1_mag + e2 = np.dot(v2, v2) / 2 - sk.consts.mu_earth / r2_mag + assert e1 == pytest.approx(e2, rel=1e-8) + + def test_retrograde(self): + """Retrograde transfer""" + r1 = np.array([7000e3, 0, 0]) + r2 = np.array([0, 7000e3, 0]) + period = 2 * np.pi * np.sqrt(7000e3**3 / sk.consts.mu_earth) + tof = period * 0.75 + + sols = sk.lambert(r1, r2, tof, prograde=False) + assert len(sols) >= 1 + v1, v2 = sols[0] + + e1 = np.dot(v1, v1) / 2 - sk.consts.mu_earth / np.linalg.norm(r1) + e2 = np.dot(v2, v2) / 2 - sk.consts.mu_earth / np.linalg.norm(r2) + assert e1 == pytest.approx(e2, rel=1e-8) + + def test_inclined(self): + """Transfer with inclination change""" + r1 = np.array([7000e3, 0, 0]) + r2 = np.array([0, 5000e3, 5000e3]) + tof = 3600.0 + + sols = sk.lambert(r1, r2, tof) + v1, v2 = sols[0] + + e1 = np.dot(v1, v1) / 2 - sk.consts.mu_earth / np.linalg.norm(r1) + e2 = np.dot(v2, v2) / 2 - sk.consts.mu_earth / np.linalg.norm(r2) + assert e1 == pytest.approx(e2, rel=1e-8) + + h1 = np.cross(r1, v1) + h2 = np.cross(r2, v2) + np.testing.assert_allclose(h1, h2, rtol=1e-8) + + def test_custom_mu(self): + """Lambert with custom gravitational parameter (e.g. Sun)""" + mu_sun = sk.consts.mu_sun + r1 = np.array([1.496e11, 0, 0]) # ~1 AU + r2 = np.array([0, 2.279e11, 0]) # ~Mars orbit + tof = 200 * 86400 # 200 days + + sols = sk.lambert(r1, r2, tof, mu=mu_sun) + v1, v2 = sols[0] + + e1 = np.dot(v1, v1) / 2 - mu_sun / np.linalg.norm(r1) + e2 = np.dot(v2, v2) / 2 - mu_sun / np.linalg.norm(r2) + assert e1 == pytest.approx(e2, rel=1e-8) + + def test_invalid_inputs(self): + """Invalid inputs should raise ValueError""" + r1 = np.array([7000e3, 0, 0]) + r2 = np.array([0, 7000e3, 0]) + + with pytest.raises(ValueError): + sk.lambert(r1, r2, -1.0) # negative TOF + + with pytest.raises(ValueError): + sk.lambert(r1, r2, 3600.0, mu=-1.0) # negative mu + + with pytest.raises(ValueError): + sk.lambert(np.array([0.0, 0.0, 0.0]), r2, 3600.0) # zero position diff --git a/src/earthgravity.rs b/src/earthgravity.rs index 3d646b2..c4f0d4c 100644 --- a/src/earthgravity.rs +++ b/src/earthgravity.rs @@ -9,7 +9,7 @@ type CoeffTable = DMatrix; type DivisorTable = Matrix<44, 44>; -use once_cell::sync::OnceCell; +use std::sync::OnceLock; /// /// Gravity model enumeration @@ -52,7 +52,7 @@ impl GravityModel { /// Singleton for JGM3 gravity model /// pub fn jgm3() -> &'static Gravity { - static INSTANCE: OnceCell = OnceCell::new(); + static INSTANCE: OnceLock = OnceLock::new(); INSTANCE.get_or_init(|| Gravity::from_file("JGM3.gfc").unwrap()) } @@ -60,7 +60,7 @@ pub fn jgm3() -> &'static Gravity { /// Singleton for JGM2 gravity model /// pub fn jgm2() -> &'static Gravity { - static INSTANCE: OnceCell = OnceCell::new(); + static INSTANCE: OnceLock = OnceLock::new(); INSTANCE.get_or_init(|| Gravity::from_file("JGM2.gfc").unwrap()) } @@ -68,7 +68,7 @@ pub fn jgm2() -> &'static Gravity { /// Singleton for EGM96 gravity model /// pub fn egm96() -> &'static Gravity { - static INSTANCE: OnceCell = OnceCell::new(); + static INSTANCE: OnceLock = OnceLock::new(); INSTANCE.get_or_init(|| Gravity::from_file("EGM96.gfc").unwrap()) } @@ -76,7 +76,7 @@ pub fn egm96() -> &'static Gravity { /// Singleton for ITU GRACE16 gravity model /// pub fn itu_grace16() -> &'static Gravity { - static INSTANCE: OnceCell = OnceCell::new(); + static INSTANCE: OnceLock = OnceLock::new(); INSTANCE.get_or_init(|| Gravity::from_file("ITU_GRACE16.gfc").unwrap()) } @@ -84,7 +84,7 @@ pub fn itu_grace16() -> &'static Gravity { /// Gravity model hash /// pub fn gravhash() -> &'static HashMap { - static INSTANCE: OnceCell> = OnceCell::new(); + static INSTANCE: OnceLock> = OnceLock::new(); INSTANCE.get_or_init(|| { let mut m = HashMap::new(); m.insert(GravityModel::JGM3, jgm3()); @@ -102,7 +102,7 @@ pub fn gravhash() -> &'static HashMap { /// /// # Arguments /// -/// * `pos` - nalgebra 3-vector representing ITRF position in meters +/// * `pos` - 3-vector representing ITRF position in meters /// /// * `degree` - The maximum degree of the gravity model to use. /// Maximum is 40 @@ -134,7 +134,7 @@ pub fn accel(pos_itrf: &Vector3, degree: usize, order: usize, model: GravityMode /// /// # Arguments /// -/// * `pos` - nalgebra 3-vector representing ITRF position in meters +/// * `pos` - 3-vector representing ITRF position in meters /// /// * `degree` - The maximum degree of the gravity model to use. /// Maximum is 40 @@ -342,9 +342,11 @@ impl Gravity { // From fact that laplacian is zero let daydy = -daxdx - dazdz; - Matrix3::new( - daxdx, daxdy, daxdz, daxdy, daydy, daydz, daxdz, daydz, dazdz, - ) * self.gravity_constant + Matrix3::new([ + [daxdx, daxdy, daxdz], + [daxdy, daydy, daydz], + [daxdz, daydz, dazdz], + ]) * self.gravity_constant / self.radius.powi(3) } @@ -391,7 +393,7 @@ impl Gravity { } } - Vector3::new(ax, ay, az) * self.gravity_constant / self.radius / self.radius + numeris::vector![ax, ay, az] * self.gravity_constant / self.radius / self.radius } fn compute_legendre(&self, pos: &Vector3) -> (Legendre, Legendre) { @@ -582,7 +584,7 @@ mod tests { fn test_gravity_order_1() { // Order 1 = point mass: accel should be μ/r², radially inward let r = 7000.0e3; // 7000 km - let pos = Vector3::new(r, 0.0, 0.0); + let pos = numeris::vector![r, 0.0, 0.0]; let accel = jgm3().accel(&pos, 1, 1); let expected_mag = crate::consts::MU_EARTH / (r * r); assert_relative_eq!(accel.norm(), expected_mag, max_relative = 1.0e-6); @@ -595,15 +597,15 @@ mod tests { #[test] fn test_gravity_models_agree_order1() { // At order 1 (point mass), all models should agree closely - let pos = Vector3::new(7000.0e3, 1000.0e3, 3000.0e3); + let pos = numeris::vector![7000.0e3, 1000.0e3, 3000.0e3]; let a_jgm3 = jgm3().accel(&pos, 1, 1); let a_jgm2 = jgm2().accel(&pos, 1, 1); let a_egm96 = egm96().accel(&pos, 1, 1); let a_grace = itu_grace16().accel(&pos, 1, 1); // All should be very close (small differences due to different GM values) - assert_relative_eq!(a_jgm3, a_jgm2, max_relative = 1.0e-6); - assert_relative_eq!(a_jgm3, a_egm96, max_relative = 1.0e-6); - assert_relative_eq!(a_jgm3, a_grace, max_relative = 1.0e-6); + assert!((a_jgm3 - a_jgm2).norm() < 1.0e-6 * a_jgm3.norm().max(a_jgm2.norm())); + assert!((a_jgm3 - a_egm96).norm() < 1.0e-6 * a_jgm3.norm().max(a_egm96.norm())); + assert!((a_jgm3 - a_grace).norm() < 1.0e-6 * a_jgm3.norm().max(a_grace.norm())); } #[test] @@ -630,8 +632,8 @@ mod tests { let coord = ITRFCoord::from_geodetic_deg(latitude, longitude, altitude); let gaccel: Vector3 = jgm3().accel(&coord.itrf, 6, 6); let gaccel_truth = - nalgebra::vector![-2.3360599811572618, 6.8730769266931615, -6.616497962860285]; - assert_relative_eq!(gaccel, gaccel_truth, max_relative = 1.0e-6); + numeris::vector![-2.3360599811572618, 6.8730769266931615, -6.616497962860285]; + assert!((gaccel - gaccel_truth).norm() < 1.0e-6 * gaccel.norm().max(gaccel_truth.norm())); } #[test] @@ -657,7 +659,7 @@ mod tests { let coord = ITRFCoord::from_geodetic_deg(latitude, longitude, altitude); let gravitation: Vector3 = g.accel(&coord.itrf, 16, 16); let centrifugal: Vector3 = - Vector3::new(coord.itrf[0], coord.itrf[1], 0.0) * OMEGA_EARTH * OMEGA_EARTH; + numeris::vector![coord.itrf[0], coord.itrf[1], 0.0] * OMEGA_EARTH * OMEGA_EARTH; let gravity = gravitation + centrifugal; // Check gravitation matches the reference value @@ -699,11 +701,11 @@ mod tests { let coord = ITRFCoord::from_geodetic_deg(latitude, longitude, altitude); // generate a random shift - let dpos = Vector3::new( + let dpos = numeris::vector![ random::() * 100.0, random::() * 100.0, random::() * 100.0, - ); + ]; // get acceleration and partials at coordinate let (accel1, partials) = g.accel_and_partials(&coord.itrf, 6, 6); @@ -718,7 +720,7 @@ mod tests { let accel3 = accel1 + partials * dpos; // show that they are approximately equal - assert_relative_eq!(accel2, accel3, max_relative = 1.0e-4); + assert!((accel2 - accel3).norm() < 1.0e-4 * accel2.norm().max(accel3.norm())); } } @@ -783,8 +785,8 @@ mod tests { let coord = ITRFCoord::from_geodetic_deg(latitude, longitude, 0.0); let gaccel = jgm3().accel(&coord.itrf, 6, 6); let gaccel_truth = - nalgebra::vector![-2.3360599811572618, 6.8730769266931615, -6.616497962860285]; - assert_relative_eq!(gaccel, gaccel_truth, max_relative = 1.0e-6); + numeris::vector![-2.3360599811572618, 6.8730769266931615, -6.616497962860285]; + assert!((gaccel - gaccel_truth).norm() < 1.0e-6 * gaccel.norm().max(gaccel_truth.norm())); } #[test] @@ -792,7 +794,7 @@ mod tests { // Verify partials are consistent when order < degree let g = Gravity::from_file("JGM3.gfc").unwrap(); let coord = ITRFCoord::from_geodetic_deg(45.0, 30.0, 400.0e3); - let dpos = Vector3::new(50.0, -30.0, 80.0); + let dpos = numeris::vector![50.0, -30.0, 80.0]; // Use degree=6, order=2 let (accel1, partials) = g.accel_and_partials(&coord.itrf, 6, 2); @@ -800,6 +802,6 @@ mod tests { let accel2 = g.accel(&v2, 6, 2); let accel3 = accel1 + partials * dpos; - assert_relative_eq!(accel2, accel3, max_relative = 1.0e-4); + assert!((accel2 - accel3).norm() < 1.0e-4 * accel2.norm().max(accel3.norm())); } } diff --git a/src/filters/mod.rs b/src/filters/mod.rs deleted file mode 100644 index 5dab374..0000000 --- a/src/filters/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod ukf; diff --git a/src/filters/ukf.rs b/src/filters/ukf.rs deleted file mode 100644 index 6739f11..0000000 --- a/src/filters/ukf.rs +++ /dev/null @@ -1,245 +0,0 @@ -//! Unscented Kalman Filter -//! -//! Uses nalgebra for state and covariance matrices - - -/// Generic float vector type of fixed size -type Vector = nalgebra::SVector; - -/// Generic float matrix type of fixed size -type Matrix = nalgebra::SMatrix; -use anyhow::{anyhow, Result}; - -/// Unscented Kalman Filter -/// -/// See: -/// for a good explanation of the UKF algorithm -/// -pub struct UKF { - pub alpha: f64, - pub beta: f64, - pub kappa: f64, - pub weight_m: Vec, - pub weight_c: Vec, - pub x: Vector, - pub p: Matrix, -} - -impl UKF { - /// Weights for the state estimate - fn weight_m(alpha: f64, kappa: f64) -> Vec { - let mut weight_m = Vec::::with_capacity(2 * N + 1); - let den = alpha.powi(2) * (N as f64 + kappa); - weight_m.push(1.0 - N as f64 / den); - for _i in 1..2 * N + 1 { - weight_m.push(1.0 / (2.0 * den)); - } - weight_m - } - - /// Weights for the covariance - fn weight_c(alpha: f64, beta: f64, kappa: f64) -> Vec { - let mut weight_c = Vec::::with_capacity(2 * N + 1); - let den: f64 = alpha.powi(2) * (N as f64 + kappa); - weight_c.push(2.0 + beta - alpha.powi(2).mul_add(1.0, N as f64 / den)); - for _i in 1..2 * N + 1 { - weight_c.push(1.0 / (2.0 * den)); - } - weight_c - } - - /// Constructor with default values - pub fn new_default() -> Self { - Self { - alpha: 0.001, - beta: 2.0, - kappa: 0.0, - weight_m: Self::weight_m(0.001, 0.0), - weight_c: Self::weight_c(0.001, 2.0, 0.0), - x: Vector::::zeros(), - p: Matrix::::zeros(), - } - } - - /// Constructor with custom values - pub fn new(alpha: f64, beta: f64, kappa: f64) -> Self { - Self { - alpha, - beta, - kappa, - weight_m: Self::weight_m(alpha, kappa), - weight_c: Self::weight_c(alpha, beta, kappa), - x: Vector::::zeros(), - p: Matrix::::zeros(), - } - } - - /// - /// Update state and covariance with new observation - /// - /// # Arguments - /// * `y` - Observation vector - /// * `y_cov` - Covariance of observation - /// * `f` - Function to compute the observation from the state - /// - /// # Returns - /// * `Result<()>` - Result of the update - /// - /// # Notes: - /// * This will update the state estimate and the covariance matrix - /// - pub fn update( - &mut self, - y: &Vector, - y_cov: &Matrix, - f: impl Fn(Vector) -> Result>, - ) -> Result<()> { - let c = self.alpha.powi(2) * (N as f64 + self.kappa); - - let cp = c.sqrt() - * self - .p - .cholesky() - .ok_or_else(|| anyhow!("Cannot take Cholesky decomposition"))? - .l(); - - let mut x_sigma_points = Vec::>::with_capacity(2 * N + 1); - - // Create prior weights - x_sigma_points.push(self.x); - for i in 0..N { - x_sigma_points.push(self.x + cp.column(i)); - } - for i in 0..N { - x_sigma_points.push(self.x - cp.column(i)); - } - - // Compute predict with sigma values - let yhat_i = x_sigma_points - .iter() - .map(|x| f(*x)) - .collect::>>>()?; - - let yhat = yhat_i - .iter() - .enumerate() - .fold(Vector::::zeros(), |acc, (i, y)| { - acc + self.weight_m[i] * y - }); - - // Compute predicted covariance - let p_yy = yhat_i.iter().enumerate().fold(*y_cov, |acc, (i, y)| { - acc + self.weight_c[i] * (y - yhat) * (y - yhat).transpose() - }); - - // Compute cross covariance - let p_xy = x_sigma_points - .iter() - .zip(yhat_i.iter()) - .enumerate() - .fold(Matrix::::zeros(), |acc, (i, (x, y))| { - acc + self.weight_c[i] * (x - self.x) * (y - yhat).transpose() - }); - - let kalman_gain = p_xy - * p_yy - .try_inverse() - .ok_or_else(|| anyhow!("Cannot take inverse of kalman gain; it is singular"))?; - self.x += kalman_gain * (y - yhat); - self.p -= kalman_gain * p_yy * kalman_gain.transpose(); - Ok(()) - } - - /// Predict step - /// - /// # Arguments - /// * `f` - Function to compute the next state from the current state - /// - /// # Returns - /// * Empty Ok value or an error - /// - /// # Notes - /// * This will update the state estimate and the covariance matrix - /// * This function does not add process noise to the state estimate, - /// you should add process noise after this function is called - /// - pub fn predict(&mut self, f: impl Fn(Vector) -> Result>) -> Result<()> { - let c = self.alpha.powi(2) * (N as f64 + self.kappa); - - let cp = c.sqrt() - * self - .p - .cholesky() - .ok_or_else(|| anyhow!("Cannot take cholesky decomposition in predict step"))? - .l(); - - let mut x_sigma_points = Vec::>::with_capacity(2 * N + 1); - - // Create prior weights - x_sigma_points.push(self.x); - for i in 0..N { - x_sigma_points.push(self.x + cp.column(i)); - } - for i in 0..N { - x_sigma_points.push(self.x - cp.column(i)); - } - - // Compute predict with sigma values - let x_post = x_sigma_points - .iter() - .map(|x| f(*x)) - .collect::>>>()?; - - // Update state - self.x = x_post - .iter() - .enumerate() - .fold(Vector::::zeros(), |acc, (i, x)| { - acc + self.weight_m[i] * x - }); - - // Update covariance - self.p = x_post - .iter() - .enumerate() - .fold(Matrix::::zeros(), |acc, (i, x)| { - acc + self.weight_c[i] * (x - self.x) * (x - self.x).transpose() - }); - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use rand_distr::{Distribution, Normal}; - - #[test] - fn test_ukf() { - let mut ukf = UKF::<2>::new_default(); - - let normal = Normal::new(0.0, 1.0).unwrap(); - - let ytruth = Vector::<2>::new(3.0, 4.0); - let y_cov = Matrix::<2, 2>::new(1.0, 0.0, 0.0, 1.0); - let v = normal.sample(&mut rand::rng()); - let w = normal.sample(&mut rand::rng()); - let ysample = ytruth + Vector::<2>::new(v, w); - let offset = Vector::<2>::new(5.0, 8.0); - let observe = |x: Vector<2>| Ok(x + offset); - - // Process noise - let q = Matrix::<2, 2>::new(1.0e-12, 0.0, 0.0, 1.0e-12); - ukf.x = ysample; - ukf.p = y_cov; - for _ix in 0..500 { - let v = normal.sample(&mut rand::rng()); - let w = normal.sample(&mut rand::rng()); - let ysample = observe(ytruth + Vector::<2>::new(v, w)).unwrap(); - ukf.update(&ysample, &y_cov, observe).unwrap(); - ukf.p += q; - } - } -} diff --git a/src/frametransform/ierstable.rs b/src/frametransform/ierstable.rs index a728bbc..00c7417 100644 --- a/src/frametransform/ierstable.rs +++ b/src/frametransform/ierstable.rs @@ -66,12 +66,10 @@ impl IERSTable { if table.data[tnum as usize].ncols() < 17 { anyhow::bail!("Error parsing file {}, table not initialized", fname); } - table.data[tnum as usize].set_row( - rowcnt, - &nalgebra::SMatrix::::from_iterator( - tline.split_whitespace().map(|x| x.parse().unwrap()), - ), - ); + let vals: Vec = tline.split_whitespace().map(|x| x.parse().unwrap()).collect(); + for (c, &val) in vals.iter().enumerate() { + table.data[tnum as usize][(rowcnt, c)] = val; + } rowcnt += 1; } } @@ -81,7 +79,7 @@ impl IERSTable { Ok(table) } - pub fn compute(&self, t_tt: f64, delaunay: &nalgebra::SVector) -> f64 { + pub fn compute(&self, t_tt: f64, delaunay: &numeris::Vector) -> f64 { let mut retval: f64 = 0.0; for i in 0..6 { // return if finished diff --git a/src/frametransform/mod.rs b/src/frametransform/mod.rs index 8966c7d..eb5f030 100644 --- a/src/frametransform/mod.rs +++ b/src/frametransform/mod.rs @@ -37,26 +37,6 @@ use super::earth_orientation_params; pub use qcirs2gcrs::qcirs2gcrs; pub use qcirs2gcrs::qcirs2gcrs_dxdy; -/// Right-handed rotation of coordinate system about x axis -/// (left-handed rotation of vector) -#[inline] -pub(crate) fn qrot_xcoord(theta: f64) -> Quaternion { - Quaternion::from_axis_angle(&Vector3::x_axis(), -theta) -} - -/// Right-handed rotation of coordinate system about y axis -/// (left-handed rotation of vector) -#[inline] -pub(crate) fn qrot_ycoord(theta: f64) -> Quaternion { - Quaternion::from_axis_angle(&Vector3::y_axis(), -theta) -} - -/// Right-handed rotation of coordinate system about z axis -/// (left-handed rotation of vector) -#[inline] -pub(crate) fn qrot_zcoord(theta: f64) -> Quaternion { - Quaternion::from_axis_angle(&Vector3::z_axis(), -theta) -} /// /// Greenwich Mean Sidereal Time @@ -155,14 +135,14 @@ pub fn earth_rotation_angle(tm: &T) -> f64 { /// pub fn qitrf2tirs(tm: &T) -> Quaternion { const ASEC2RAD: f64 = PI / 180.0 / 3600.0; - // Get earth oreintation parameters or set them all to zero if not available + // Get earth orientation parameters or set them all to zero if not available // (function will print warning to stderr if not available) let eop = earth_orientation_params::get(tm).unwrap_or([0.0; 6]); let xp = eop[1] * ASEC2RAD; let yp = eop[2] * ASEC2RAD; let t_tt = (tm.as_mjd_with_scale(TimeScale::TT) - 51544.5) / 36525.0; let sp = -47.0e-6 * ASEC2RAD * t_tt; - qrot_zcoord(-sp) * qrot_ycoord(xp) * qrot_xcoord(yp) + Quaternion::rotz(sp) * Quaternion::roty(-xp) * Quaternion::rotx(-yp) } /// @@ -184,7 +164,7 @@ pub fn qitrf2tirs(tm: &T) -> Quaternion { /// * This is Equation 3-90 in Vallado /// pub fn qteme2itrf(tm: &T) -> Quaternion { - qitrf2tirs(tm).conjugate() * qrot_zcoord(gmst(tm)) + qitrf2tirs(tm).conjugate() * Quaternion::rotz(-gmst(tm)) } /// @@ -215,8 +195,8 @@ pub fn qteme2gcrf(tm: &T) -> Quaternion { } /// -/// Rotate from Mean Equinix of Date (MOD) coordinate frame -/// to Geocentric Celestrial Reference Frame +/// Rotate from Mean Equinox of Date (MOD) coordinate frame +/// to Geocentric Celestial Reference Frame /// /// # Arguments /// @@ -262,12 +242,12 @@ pub fn qmod2gcrf(tm: &T) -> Quaternion { ), 2004.191903, ); - qrot_zcoord(zeta * ASEC2RAD) * qrot_ycoord(-theta * ASEC2RAD) * qrot_zcoord(z * ASEC2RAD) + Quaternion::rotz(-zeta * ASEC2RAD) * Quaternion::roty(theta * ASEC2RAD) * Quaternion::rotz(-z * ASEC2RAD) } /// /// Approximate rotation from -/// Geocentric Celestrial Reference Frame to +/// Geocentric Celestial Reference Frame to /// International Terrestrial Reference Frame /// /// @@ -286,12 +266,12 @@ pub fn qmod2gcrf(tm: &T) -> Quaternion { /// * This uses an approximation of the IAU-76/FK5 Reduction /// See Vallado section 3.7.3 /// -/// * For a reference, see "Eplanatory Supplement to the +/// * For a reference, see "Explanatory Supplement to the /// Astronomical Almanac", 2013, Ch. 6 /// pub fn qgcrf2itrf_approx(tm: &T) -> Quaternion { // Neglecting polar motion - let qitrf2tod_approx: Quaternion = qrot_zcoord(-gast(tm)); + let qitrf2tod_approx: Quaternion = Quaternion::rotz(gast(tm)); (qmod2gcrf(tm) * qtod2mod_approx(tm) * qitrf2tod_approx).conjugate() } @@ -299,7 +279,7 @@ pub fn qgcrf2itrf_approx(tm: &T) -> Quaternion { /// /// Approximate rotation from /// International Terrestrial Reference Frame to -/// Geocentric Celestrial Reference Frame +/// Geocentric Celestial Reference Frame /// /// /// Arguments @@ -317,7 +297,7 @@ pub fn qgcrf2itrf_approx(tm: &T) -> Quaternion { /// * This uses an approximation of the IAU-76/FK5 Reduction /// See Vallado section 3.7.3 /// -/// * For a reference, see "Eplanatory Supplement to the +/// * For a reference, see "Explanatory Supplement to the /// Astronomical Almanac", 2013, Ch. 6 pub fn qitrf2gcrf_approx(tm: &T) -> Quaternion { qgcrf2itrf_approx(tm).conjugate() @@ -362,7 +342,7 @@ pub fn qtod2mod_approx(tm: &T) -> Quaternion { 23.0 + 26.0 / 60.0 + 21.406 / 3600.0, ); let epsilon = epsilon_a + delta_epsilon; - qrot_xcoord(-epsilon_a) * qrot_zcoord(delta_psi) * qrot_xcoord(epsilon) + Quaternion::rotx(epsilon_a) * Quaternion::rotz(-delta_psi) * Quaternion::rotx(-epsilon) } /// @@ -389,7 +369,7 @@ pub fn qtod2mod_approx(tm: &T) -> Quaternion { /// * This is **very** computationally expensive; for most /// applications, the approximate rotation will work just fine /// -/// * This computatation **does not** include impact of the +/// * This computation **does not** include impact of the /// Earth solid tides, but it does include polar motion, /// precession, and nutation /// @@ -411,7 +391,7 @@ pub fn qitrf2gcrf(tm: &T) -> Quaternion { let yp = eop[2] * ASEC2RAD; let t_tt = (tm.as_mjd_with_scale(TimeScale::TT) - 51544.5) / 36525.0; let sp = -47.0e-6 * ASEC2RAD * t_tt; - qrot_zcoord(-sp) * qrot_ycoord(xp) * qrot_xcoord(yp) + Quaternion::rotz(sp) * Quaternion::roty(-xp) * Quaternion::rotx(-yp) }; let r = qtirs2cirs(tm); let q = qcirs2gcrs_dxdy(tm, Some((eop[4], eop[5]))); @@ -426,7 +406,7 @@ pub fn qitrf2gcrf(tm: &T) -> Quaternion { /// /// # Arguments /// -/// * `tm` - Time instanc at which to compute rotation +/// * `tm` - Time instant at which to compute rotation /// /// # Returns /// @@ -466,7 +446,7 @@ pub fn qgcrf2itrf(tm: &T) -> Quaternion { /// #[inline] pub fn qtirs2cirs(tm: &T) -> Quaternion { - qrot_zcoord(-earth_rotation_angle(tm)) + Quaternion::rotz(earth_rotation_angle(tm)) } #[cfg(test)] @@ -490,7 +470,7 @@ mod tests { #[test] fn test_qitrf2gcrf_roundtrip() { let tm = Instant::from_datetime(2020, 6, 15, 12, 0, 0.0).unwrap(); - let v_itrf = Vector3::new(1000.0, 2000.0, 3000.0); + let v_itrf = numeris::vector![1000.0, 2000.0, 3000.0]; let v_gcrf = qitrf2gcrf(&tm) * v_itrf; let v_back = qgcrf2itrf(&tm) * v_gcrf; assert!((v_back - v_itrf).norm() < 1.0e-12 * v_itrf.norm()); @@ -508,7 +488,7 @@ mod tests { #[test] fn test_approx_vs_full() { let tm = Instant::from_datetime(2010, 3, 15, 6, 0, 0.0).unwrap(); - let v_itrf = Vector3::new(6378137.0, 0.0, 0.0); + let v_itrf = numeris::vector![6378137.0, 0.0, 0.0]; let v_approx = qitrf2gcrf_approx(&tm) * v_itrf; let v_full = qitrf2gcrf(&tm) * v_itrf; @@ -529,7 +509,7 @@ mod tests { // Using the same time as test_gcrs2itrf (Vallado Example 3-14) let tm = &Instant::from_datetime(2004, 4, 6, 7, 51, 28.386009).unwrap(); // TEME position from SGP4 output (Vallado example) - let pitrf = Vector3::new(-1033.4793830, 7901.2952754, 6380.3565958); + let pitrf = numeris::vector![-1033.4793830, 7901.2952754, 6380.3565958]; // Get GCRF via ITRF path (known good from test_gcrs2itrf) let pgcrf_via_itrf = qitrf2gcrf(tm) * pitrf; // Get GCRF via TEME path @@ -543,11 +523,11 @@ mod tests { #[test] fn test_gcrs2itrf() { // Example 3-14 from Vallado - // With verification fo intermediate calculations + // With verification of intermediate calculations // Input time let tm = &Instant::from_datetime(2004, 4, 6, 7, 51, 28.386009).unwrap(); // Input terrestrial location - let pitrf = Vector3::new(-1033.4793830, 7901.2952754, 6380.3565958); + let pitrf = numeris::vector![-1033.4793830, 7901.2952754, 6380.3565958]; let t_tt = (tm.as_jd_with_scale(TimeScale::TT) - 2451545.0) / 36525.0; assert!((t_tt - 0.0426236319).abs() < 1.0e-8); @@ -568,7 +548,7 @@ mod tests { assert!((ptirs[2] - 6380.3445327).abs() < 1.0e-4); let era = earth_rotation_angle(tm); assert!((era.to_degrees() - 312.7552829).abs() < 1.0e-5); - let pcirs = qrot_zcoord(-era) * ptirs; + let pcirs = Quaternion::rotz(era) * ptirs; assert!((pcirs[0] - 5100.0184047).abs() < 1e-3); assert!((pcirs[1] - 6122.7863648).abs() < 1e-3); assert!((pcirs[2] - 6380.3446237).abs() < 1e-3); diff --git a/src/frametransform/qcirs2gcrs.rs b/src/frametransform/qcirs2gcrs.rs index 59738ea..7026054 100644 --- a/src/frametransform/qcirs2gcrs.rs +++ b/src/frametransform/qcirs2gcrs.rs @@ -1,27 +1,26 @@ use super::ierstable::IERSTable; -use crate::frametransform::{qrot_ycoord, qrot_zcoord}; use crate::{TimeLike, TimeScale}; use crate::mathtypes::*; -type Delaunay = nalgebra::SVector; +type Delaunay = numeris::Vector; use std::f64::consts::PI; -use once_cell::sync::OnceCell; +use std::sync::OnceLock; fn table5a_singleton() -> &'static IERSTable { - static INSTANCE: OnceCell = OnceCell::new(); + static INSTANCE: OnceLock = OnceLock::new(); INSTANCE.get_or_init(|| IERSTable::from_file("tab5.2a.txt").unwrap()) } fn table5b_singleton() -> &'static IERSTable { - static INSTANCE: OnceCell = OnceCell::new(); + static INSTANCE: OnceLock = OnceLock::new(); INSTANCE.get_or_init(|| IERSTable::from_file("tab5.2b.txt").unwrap()) } fn table5d_singleton() -> &'static IERSTable { - static INSTANCE: OnceCell = OnceCell::new(); + static INSTANCE: OnceLock = OnceLock::new(); INSTANCE.get_or_init(|| IERSTable::from_file("tab5.2d.txt").unwrap()) } @@ -147,11 +146,11 @@ pub fn qcirs2gcrs_dxdy(tm: &T, dxdy: Option<(f64, f64)>) -> Quatern // Equations 5.6 & 5.7 of IERS technical note 36 let e = f64::atan2(y, x); let d = f64::asin(f64::sqrt(x.mul_add(x, y * y))); - qrot_zcoord(-e) * qrot_ycoord(-d) * qrot_zcoord(e + s) + Quaternion::rotz(e) * Quaternion::roty(d) * Quaternion::rotz(-(e + s)) } /// -/// Return quatnerion represention rotation +/// Return quaternion representing rotation /// from the CIRS (Celestial Intermediate Reference System) to the /// GCRS (Geocentric Celestial Reference Frame) at given instant /// diff --git a/src/itrfcoord.rs b/src/itrfcoord.rs index 9a31668..244d99c 100644 --- a/src/itrfcoord.rs +++ b/src/itrfcoord.rs @@ -57,7 +57,7 @@ impl std::fmt::Display for Geodetic { /// quaternions to the East-North-Up frame /// and North-East-Down frame at this coordinate /// -#[derive(PartialEq, PartialOrd, Copy, Clone, Debug)] +#[derive(PartialEq, Copy, Clone, Debug)] pub struct ITRFCoord { pub itrf: Vector3, } @@ -84,33 +84,6 @@ impl std::ops::Add for ITRFCoord { } } -impl std::ops::Add for &ITRFCoord { - type Output = ITRFCoord; - fn add(self, other: Vector3) -> Self::Output { - ITRFCoord { - itrf: self.itrf + other, - } - } -} - -impl std::ops::Add<&Vector3> for ITRFCoord { - type Output = Self; - fn add(self, other: &Vector3) -> Self::Output { - Self { - itrf: self.itrf + other, - } - } -} - -impl std::ops::Add<&Vector3> for &ITRFCoord { - type Output = ITRFCoord; - fn add(self, other: &Vector3) -> Self::Output { - ITRFCoord { - itrf: self.itrf + other, - } - } -} - impl std::ops::Sub for ITRFCoord { type Output = Self; fn sub(self, other: Vector3) -> Self::Output { @@ -127,31 +100,10 @@ impl std::ops::Sub for ITRFCoord { } } -impl std::ops::Sub for &ITRFCoord { - type Output = Vector3; - fn sub(self, other: ITRFCoord) -> Vector3 { - self.itrf - other.itrf - } -} - -impl std::ops::Sub<&ITRFCoord> for &ITRFCoord { - type Output = Vector3; - fn sub(self, other: &ITRFCoord) -> Vector3 { - self.itrf - other.itrf - } -} - -impl std::ops::Sub<&Self> for ITRFCoord { - type Output = Vector3; - fn sub(self, other: &Self) -> Vector3 { - self.itrf - other.itrf - } -} - impl std::convert::From<[f64; 3]> for ITRFCoord { fn from(v: [f64; 3]) -> Self { Self { - itrf: Vector3::from(v), + itrf: Vector3::from_array(v), } } } @@ -163,7 +115,7 @@ impl std::convert::TryFrom<&[f64]> for ITRFCoord { anyhow::bail!("Input slice must have 3 elements, got {}", v.len()); } Ok(Self { - itrf: Vector3::from_row_slice(v), + itrf: numeris::vector![v[0], v[1], v[2]], }) } } @@ -206,15 +158,15 @@ impl ITRFCoord { /// /// # Arguments: /// - /// * `v` - `nalgebra::Vector3` representing ITRF coordinates in meters + /// * `v` - `Vector3` representing ITRF coordinates in meters /// /// # Examples: /// /// ``` /// // Create coord for ~ Boston, MA /// use satkit::itrfcoord::ITRFCoord; - /// use nalgebra as na; - /// let itrf = ITRFCoord::from_vector(&na::Vector3::new(1522386.15660978, -4459627.78585002, 4284030.6890791)); + /// let v = numeris::vector![1522386.15660978, -4459627.78585002, 4284030.6890791]; + /// let itrf = ITRFCoord::from_vector(&v); /// ``` /// /// @@ -241,7 +193,7 @@ impl ITRFCoord { anyhow::bail!("Input slice must have 3 elements"); } Ok(Self { - itrf: Vector3::from_row_slice(v), + itrf: numeris::vector![v[0], v[1], v[2]], }) } @@ -274,11 +226,11 @@ impl ITRFCoord { let s = f2 * c; Self { - itrf: Vector3::from([ + itrf: numeris::vector![ WGS84_A.mul_add(c, hae) * cosp * cosl, WGS84_A.mul_add(c, hae) * cosp * sinl, WGS84_A.mul_add(s, hae) * sinp, - ]), + ], } } @@ -575,8 +527,8 @@ impl ITRFCoord { #[inline] pub fn q_ned2itrf(&self) -> Quaternion { let (lat, lon, _) = self.to_geodetic_rad(); - Quaternion::from_axis_angle(&Vector3::z_axis(), lon) - * Quaternion::from_axis_angle(&Vector3::y_axis(), -lat - PI / 2.0) + Quaternion::rotz(lon) + * Quaternion::roty(-lat - PI / 2.0) } /// Convert coordinate to a North-East-Down (NED) @@ -589,7 +541,7 @@ impl ITRFCoord { /// /// # Return /// - /// * `nalgebra::Vector3` representing NED position + /// * `Vector3` representing NED position /// relative to reference. Units are meters /// /// # Note: @@ -618,8 +570,8 @@ impl ITRFCoord { /// ITRF coordinate frame pub fn q_enu2itrf(&self) -> Quaternion { let (lat, lon, _) = self.to_geodetic_rad(); - Quaternion::from_axis_angle(&Vector3::z_axis(), lon + PI / 2.0) - * Quaternion::from_axis_angle(&Vector3::x_axis(), PI / 2.0 - lat) + Quaternion::rotz(lon + PI / 2.0) + * Quaternion::rotx(PI / 2.0 - lat) } /// Convert coordinate to a East-North-Up (ENU) @@ -632,7 +584,7 @@ impl ITRFCoord { /// /// # Return /// - /// * `nalgebra::Vector3` representing ENU position + /// * `Vector3` representing ENU position /// relative to reference. Units are meters /// /// @@ -772,7 +724,7 @@ mod tests { assert!(ned[1].abs() < 1.0e-6); assert!(((ned[2] + 100.0) / 100.0).abs() < 1.0e-6); - let dvec = Vector3::from([-100.0, -200.0, 300.0]); + let dvec = numeris::vector![-100.0, -200.0, 300.0]; let itrf3 = itrf2 + itrf2.q_ned2itrf() * dvec; let nedvec = itrf3.to_ned(&itrf2); let itrf4 = itrf2 + itrf2.q_enu2itrf() * dvec; @@ -790,19 +742,19 @@ mod tests { let itrf1 = ITRFCoord::from_geodetic_deg(lat_deg, lon_deg, hae); // Go east 10 meters - let itrf3 = itrf1 + itrf1.q_enu2itrf() * nalgebra::vector![10.0, 0.0, 0.0]; + let itrf3 = itrf1 + itrf1.q_enu2itrf() * numeris::vector![10.0, 0.0, 0.0]; let (lat3, lon3, _h3) = itrf3.to_geodetic_deg(); assert!(((lat3 - lat_deg) / lat_deg).abs() < 1.0e-6); assert!(((lon3 - (lon_deg + 0.000129)) / lon_deg).abs() < 1.0e-6); // Go north 10 meters - let itrf4 = itrf1 + itrf1.q_enu2itrf() * nalgebra::vector![0.0, 10.0, 0.0]; + let itrf4 = itrf1 + itrf1.q_enu2itrf() * numeris::vector![0.0, 10.0, 0.0]; let (lat4, lon4, _h4) = itrf4.to_geodetic_deg(); assert!(((lat4 - (lat_deg + 0.000090)) / lat_deg).abs() < 1.0e-6); assert!(((lon4 - lon_deg) / lon_deg).abs() < 1.0e-6); // Go up 10 meters - let itrf5 = itrf1 + itrf1.q_enu2itrf() * nalgebra::vector![0.0, 0.0, 10.0]; + let itrf5 = itrf1 + itrf1.q_enu2itrf() * numeris::vector![0.0, 0.0, 10.0]; let (lat5, lon5, h5) = itrf5.to_geodetic_deg(); assert!(((lat5 - lat_deg) / lat_deg).abs() < 1.0e-6); assert!(((lon5 - lon_deg) / lon_deg).abs() < 1.0e-6); diff --git a/src/jplephem.rs b/src/jplephem.rs index 6f1e7f1..37e0d4c 100644 --- a/src/jplephem.rs +++ b/src/jplephem.rs @@ -23,7 +23,7 @@ use crate::solarsystem::SolarSystem; use crate::utils::{datadir, download_if_not_exist}; -use once_cell::sync::OnceCell; +use std::sync::OnceLock; use crate::mathtypes::*; use crate::{Instant, TimeLike, TimeScale}; @@ -105,7 +105,7 @@ macro_rules! dispatch_ncoeff { } fn jplephem_singleton() -> &'static Result { - static INSTANCE: OnceCell> = OnceCell::new(); + static INSTANCE: OnceLock> = OnceLock::new(); INSTANCE.get_or_init(|| JPLEphem::from_file("linux_p1550p2650.440")) } @@ -286,7 +286,7 @@ impl JPLEphem { let ncoeff: usize = (kernel_size / 2) as usize; let nrecords = ((jd_stop - jd_start) / jd_step) as usize; let record_size = (kernel_size * 4) as usize; - let mut v: DMatrix = DMatrix::repeat(ncoeff, nrecords, 0.0); + let mut v: DMatrix = DMatrix::zeros(ncoeff, nrecords); if raw.len() < record_size * 2 + ncoeff * nrecords * 8 { bail!("Invalid record size for cheby data"); @@ -295,7 +295,7 @@ impl JPLEphem { unsafe { std::ptr::copy_nonoverlapping( raw.as_ptr().add(record_size * 2) as *const f64, - v.as_mut_ptr(), + v.as_mut_slice().as_mut_ptr(), ncoeff * nrecords, ); } @@ -314,10 +314,11 @@ impl JPLEphem { let mut pos = Vector3::zeros(); for ix in 0..3 { - let m = self - .cheby - .fixed_view::(setup.offset0 + N * ix, setup.int_num); - pos[ix] = m.column(0).dot(&t); + let mut sum = 0.0; + for k in 0..N { + sum += self.cheby[(setup.offset0 + N * ix + k, setup.int_num)] * t[k]; + } + pos[ix] = sum; } Ok(pos * 1.0e3) @@ -388,11 +389,15 @@ impl JPLEphem { let mut pos = Vector3::zeros(); let mut vel = Vector3::zeros(); for ix in 0..3 { - let m = self - .cheby - .fixed_view::(setup.offset0 + N * ix, setup.int_num); - pos[ix] = m.column(0).dot(&t); - vel[ix] = m.column(0).dot(&v); + let mut psum = 0.0; + let mut vsum = 0.0; + for k in 0..N { + let coeff = self.cheby[(setup.offset0 + N * ix + k, setup.int_num)]; + psum += coeff * t[k]; + vsum += coeff * v[k]; + } + pos[ix] = psum; + vel[ix] = vsum; } Ok(( diff --git a/src/kepler.rs b/src/kepler.rs index e54b509..f04eff0 100644 --- a/src/kepler.rs +++ b/src/kepler.rs @@ -29,9 +29,9 @@ impl From for Result { /// representation, but is an angle that increases monotonically in time /// between 0 and 2π over the course of a single orbit. /// -/// * `Eccentric Anomaly` - Denoted E, is the Periaps-C-B -/// angle in the orbital plane, wehre "C" is the center of the orbital -/// ellipse, and "B" is a point on the auxilliary circle (the circle +/// * `Eccentric Anomaly` - Denoted E, is the Periapsis-C-B +/// angle in the orbital plane, where "C" is the center of the orbital +/// ellipse, and "B" is a point on the auxiliary circle (the circle /// bounding the orbital ellipse) along a line from the satellite /// and perpendicular to the semimajor axis. The eccentric anomaly is /// a useful prerequisite to compute the mean anomaly @@ -54,7 +54,7 @@ use crate::mathtypes::*; /// RAAN: Right Ascension of the Ascending Node, radians /// w: Argument of Perigee, radians /// an: Anomaly of given type, radians -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub struct Kepler { pub a: f64, pub eccen: f64, @@ -266,7 +266,7 @@ impl Kepler { /// pub fn from_pv(r: Vector3, v: Vector3) -> Result { let h = r.cross(&v); - let n = Vector3::z_axis().cross(&h); + let n = numeris::vector![0.0, 0.0, 1.0].cross(&h); let e = ((v.norm_squared() - crate::consts::MU_EARTH / r.norm()) * r - r.dot(&v) * v) / crate::consts::MU_EARTH; let eccen = e.norm(); @@ -275,13 +275,13 @@ impl Kepler { } let xi = v.norm().powi(2) / 2.0 - crate::consts::MU_EARTH / r.norm(); let a = -crate::consts::MU_EARTH / (2.0 * xi); - let incl = (h.z / h.norm()).acos(); - let mut raan = (n.x / n.norm()).acos(); - if n.y < 0.0 { + let incl = (h.z() / h.norm()).acos(); + let mut raan = (n.x() / n.norm()).acos(); + if n.y() < 0.0 { raan = 2.0f64.mul_add(std::f64::consts::PI, -raan); } let mut w = (n.dot(&e) / n.norm() / e.norm()).acos(); - if e.z < 0.0 { + if e.z() < 0.0 { w = 2.0f64.mul_add(std::f64::consts::PI, -w); } let mut nu = (r.dot(&e) / r.norm() / e.norm()).acos(); @@ -300,12 +300,12 @@ impl Kepler { pub fn to_pv(&self) -> (Vector3, Vector3) { let p = self.a * self.eccen.mul_add(-self.eccen, 1.0); let r = p / self.eccen.mul_add(self.nu.cos(), 1.0); - let r_pqw = Vector3::new(r * self.nu.cos(), r * self.nu.sin(), 0.0); - let v_pqw = Vector3::new(-self.nu.sin(), self.eccen + self.nu.cos(), 0.0) + let r_pqw = numeris::vector![r * self.nu.cos(), r * self.nu.sin(), 0.0]; + let v_pqw = numeris::vector![-self.nu.sin(), self.eccen + self.nu.cos(), 0.0] * (crate::consts::MU_EARTH / p).sqrt(); - let q = Quaternion::from_axis_angle(&Vector3::z_axis(), self.raan) - * Quaternion::from_axis_angle(&Vector3::x_axis(), self.incl) - * Quaternion::from_axis_angle(&Vector3::z_axis(), self.w); + let q = Quaternion::rotz(self.raan) + * Quaternion::rotx(self.incl) + * Quaternion::rotz(self.w); (q * r_pqw, q * v_pqw) } } @@ -445,15 +445,15 @@ mod tests { // Note: values below are not incorrect in the book, but are // corrected in the online errata // See: https://celestrak.org/software/vallado/ErrataVer4.pdf - assert!((r * 1.0e-3 - Vector3::new(6525.368, 6861.532, 6449.119)).norm() < 1e-3); - assert!((v * 1.0e-3 - Vector3::new(4.902279, 5.533140, -1.975710)).norm() < 1e-3); + assert!((r * 1.0e-3 - numeris::vector![6525.368, 6861.532, 6449.119]).norm() < 1e-3); + assert!((v * 1.0e-3 - numeris::vector![4.902279, 5.533140, -1.975710]).norm() < 1e-3); } #[test] fn test_frompv() { // Vallado example 2-5 - let r = Vector3::new(6524.834, 6862.875, 6448.296) * 1.0e3; - let v = Vector3::new(4.901327, 5.533756, -1.976341) * 1.0e3; + let r = numeris::vector![6524.834, 6862.875, 6448.296] * 1.0e3; + let v = numeris::vector![4.901327, 5.533756, -1.976341] * 1.0e3; let k = Kepler::from_pv(r, v).unwrap(); assert!((k.a - 36127343_f64).abs() < 1.0e3); assert!((k.eccen - 0.83285).abs() < 1e-3); diff --git a/src/lambert.rs b/src/lambert.rs new file mode 100644 index 0000000..3c00bc5 --- /dev/null +++ b/src/lambert.rs @@ -0,0 +1,574 @@ +//! Lambert's problem solver +//! +//! Solves Lambert's problem: given two position vectors and a time of flight, +//! find the orbit(s) connecting them. This is fundamental to orbital targeting, +//! rendezvous planning, and interplanetary trajectory design. +//! +//! Implements Izzo's algorithm (2015) with Householder 4th-order iterations +//! for fast, robust convergence across all geometries including multi-revolution +//! transfers. +//! +//! # References +//! +//! * D. Izzo, "Revisiting Lambert's problem," Celestial Mechanics and +//! Dynamical Astronomy, vol. 121, pp. 1-15, 2015. +//! +//! # Example +//! +//! ``` +//! use satkit::lambert::lambert; +//! use satkit::consts::MU_EARTH; +//! +//! let r1 = numeris::vector![7000.0e3, 0.0, 0.0]; +//! let r2 = numeris::vector![0.0, 7000.0e3, 0.0]; +//! let tof = 3600.0; // 1 hour +//! +//! let solutions = lambert(&r1, &r2, tof, MU_EARTH, true).unwrap(); +//! let (v1, v2) = &solutions[0]; +//! ``` + +use crate::mathtypes::Vector3; + +use std::f64::consts::PI; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum LambertError { + #[error("Time of flight must be positive, got {0}")] + InvalidTof(f64), + #[error("Position vectors must be non-zero")] + ZeroPosition, + #[error("Gravitational parameter must be positive, got {0}")] + InvalidMu(f64), + #[error("Convergence failure for revolution {0}")] + ConvergenceFailed(u32), +} + +/// Result of Lambert's problem: departure and arrival velocity vectors. +pub type LambertSolution = (Vector3, Vector3); + +/// Solve Lambert's problem using Izzo's algorithm. +/// +/// Given two position vectors and a time of flight, find the initial and final +/// velocity vectors for transfer orbits connecting them. +/// +/// # Arguments +/// +/// * `r1` - Initial position vector (meters) +/// * `r2` - Final position vector (meters) +/// * `tof` - Time of flight (seconds), must be positive +/// * `mu` - Gravitational parameter (m³/s²) +/// * `prograde` - If true, assume prograde (counterclockwise) transfer; +/// if false, assume retrograde transfer. This resolves the +/// short-way / long-way ambiguity. +/// +/// # Returns +/// +/// Vector of `(v1, v2)` solutions. The first element is the zero-revolution +/// solution. Additional elements are multi-revolution solutions (if any exist +/// for the given time of flight), returned in pairs (short-period, long-period) +/// for each revolution count. +pub fn lambert( + r1: &Vector3, + r2: &Vector3, + tof: f64, + mu: f64, + prograde: bool, +) -> Result, LambertError> { + if tof <= 0.0 { + return Err(LambertError::InvalidTof(tof)); + } + if mu <= 0.0 { + return Err(LambertError::InvalidMu(mu)); + } + + let r1_norm = r1.norm(); + let r2_norm = r2.norm(); + if r1_norm < 1.0e-10 || r2_norm < 1.0e-10 { + return Err(LambertError::ZeroPosition); + } + + // Chord and semiperimeter + let c = (r2 - r1).norm(); + let s = (r1_norm + r2_norm + c) / 2.0; + + // Unit vectors + let ir1 = r1 / r1_norm; + let ir2 = r2 / r2_norm; + let ih_raw = ir1.cross(&ir2); + let ih_norm = ih_raw.norm(); + + // Handle collinear positions (180-degree transfer) + let ih = if ih_norm < 1.0e-12 { + if ir1.x().abs() < 0.9 { + ir1.cross(&numeris::vector![1.0, 0.0, 0.0]).normalize() + } else { + ir1.cross(&numeris::vector![0.0, 1.0, 0.0]).normalize() + } + } else { + ih_raw / ih_norm + }; + + // Tangent unit vectors (perpendicular to position in orbital plane) + let it1 = ih.cross(&ir1); + let it2 = ih.cross(&ir2); + + // Transfer angle + let mut dtheta = f64::acos(ir1.dot(&ir2).clamp(-1.0, 1.0)); + + if prograde { + if ih.z() < 0.0 { + dtheta = 2.0 * PI - dtheta; + } + } else if ih.z() >= 0.0 { + dtheta = 2.0 * PI - dtheta; + } + + // Lambda parameter + let lambda2 = 1.0 - c / s; + let lambda = if dtheta > PI { + -lambda2.sqrt() + } else { + lambda2.sqrt() + }; + + // Non-dimensional time of flight + let t_norm = tof * (2.0 * mu / s.powi(3)).sqrt(); + + // Velocity reconstruction constants + let gamma = (mu * s / 2.0).sqrt(); + let rho = (r1_norm - r2_norm) / c; + let sigma = (1.0 - rho * rho).sqrt(); + + let mut solutions = Vec::new(); + + // --- Zero-revolution solution --- + let x0 = initial_guess_0rev(lambda, t_norm); + let x = householder(lambda, t_norm, x0, 0) + .ok_or(LambertError::ConvergenceFailed(0))?; + solutions.push(build_velocity( + &ir1, &ir2, &it1, &it2, r1_norm, r2_norm, lambda, gamma, rho, sigma, x, + )); + + // --- Multi-revolution solutions --- + let max_revs = (t_norm / PI).floor().max(0.0) as u32; + + for m in 1..=max_revs { + let (_x_min, t_min_m) = compute_t_min(lambda, m); + + if t_norm < t_min_m { + break; + } + + // Left (short-period) solution: x > x_min, toward +1 + let x_l = initial_guess_mrev(t_norm, m, true); + if let Some(x) = householder(lambda, t_norm, x_l, m) { + solutions.push(build_velocity( + &ir1, &ir2, &it1, &it2, r1_norm, r2_norm, lambda, gamma, rho, sigma, x, + )); + } + + // Right (long-period) solution: x < x_min, toward -1 + if t_norm > t_min_m + 1.0e-6 { + let x_r = initial_guess_mrev(t_norm, m, false); + if let Some(x) = householder(lambda, t_norm, x_r, m) { + solutions.push(build_velocity( + &ir1, &ir2, &it1, &it2, r1_norm, r2_norm, lambda, gamma, rho, sigma, x, + )); + } + } + } + + Ok(solutions) +} + +// --------------------------------------------------------------------------- +// Velocity reconstruction (Izzo, eq. 12-14) +// --------------------------------------------------------------------------- + +/// Reconstruct departure and arrival velocities from the solution parameter x. +fn build_velocity( + ir1: &Vector3, + ir2: &Vector3, + it1: &Vector3, + it2: &Vector3, + r1_norm: f64, + r2_norm: f64, + lambda: f64, + gamma: f64, + rho: f64, + sigma: f64, + x: f64, +) -> LambertSolution { + let y = compute_y(x, lambda); + + let vr1 = gamma * ((lambda * y - x) - rho * (lambda * y + x)) / r1_norm; + let vr2 = -gamma * ((lambda * y - x) + rho * (lambda * y + x)) / r2_norm; + let vt = gamma * sigma * (y + lambda * x); + let vt1 = vt / r1_norm; + let vt2 = vt / r2_norm; + + let v1 = vr1 * ir1 + vt1 * it1; + let v2 = vr2 * ir2 + vt2 * it2; + + (v1, v2) +} + +// --------------------------------------------------------------------------- +// TOF equation (Izzo 2015, eq. 17) +// --------------------------------------------------------------------------- + +/// y(x, lambda) = sqrt(1 - lambda^2 * (1 - x^2)) +#[inline] +fn compute_y(x: f64, lambda: f64) -> f64 { + (1.0 - lambda * lambda * (1.0 - x * x)).max(0.0).sqrt() +} + +/// Battin's hypergeometric series 2F1(3, 1; 5/2; x) +fn hyp2f1b(x: f64) -> f64 { + if x.abs() < 1.0e-12 { + return 1.0; + } + let mut res = 1.0; + let mut term = 1.0; + for i in 0..100 { + let n = i as f64; + term *= (3.0 + n) * (1.0 + n) / ((2.5 + n) * (n + 2.0)) * x; + res += term; + if term.abs() < 1.0e-15 { + break; + } + } + res +} + +/// Compute non-dimensional TOF as a function of x, lambda, and revolution count M. +/// +/// T(x) = [(psi + M*pi)/sqrt(|1-x^2|) - x + lambda*y] / (1 - x^2) +fn tof_equation(x: f64, lambda: f64, m: u32) -> f64 { + let omx2 = 1.0 - x * x; + let y = compute_y(x, lambda); + + // Near-parabolic: use Battin's series (avoids 0/0 at x=1) + if m == 0 && (0.6_f64).sqrt() < x && x < (1.4_f64).sqrt() { + let eta = y - lambda * x; + let s1 = (1.0 - lambda - x * eta) * 0.5; + let q = 4.0 / 3.0 * hyp2f1b(s1); + return (eta.powi(3) * q + 4.0 * lambda * eta) / 2.0; + } + + if omx2.abs() < 1.0e-14 { + // Parabolic limit + return 2.0 / 3.0 * (1.0 - lambda.powi(3)); + } + + if x < 1.0 { + // Elliptic + let cos_psi = x * y + lambda * omx2; + let psi = f64::acos(cos_psi.clamp(-1.0, 1.0)); + ((psi + (m as f64) * PI) / omx2.sqrt() - x + lambda * y) / omx2 + } else { + // Hyperbolic + let cosh_psi = x * y - lambda * (x * x - 1.0); + let psi_h = cosh_psi.max(1.0).acosh(); + (-x + lambda * y - psi_h / (x * x - 1.0).sqrt()) / omx2 + } +} + +// --------------------------------------------------------------------------- +// Derivatives (Izzo 2015, recurrence relations) +// --------------------------------------------------------------------------- + +/// First three derivatives of T(x) for Householder iteration. +fn tof_derivatives(x: f64, lambda: f64, t: f64) -> (f64, f64, f64) { + let lambda2 = lambda * lambda; + let lambda3 = lambda2 * lambda; + let lambda5 = lambda2 * lambda3; + let omx2 = 1.0 - x * x; + let y = compute_y(x, lambda); + + if omx2.abs() < 1.0e-12 || y < 1.0e-14 { + return (0.0, 0.0, 0.0); + } + + let dt = (3.0 * t * x - 2.0 + 2.0 * lambda3 * x / y) / omx2; + let d2t = (3.0 * t + 5.0 * x * dt + 2.0 * (1.0 - lambda2) * lambda3 / y.powi(3)) / omx2; + let d3t = (7.0 * x * d2t + 8.0 * dt - 6.0 * (1.0 - lambda2) * lambda5 * x / y.powi(5)) + / omx2; + + (dt, d2t, d3t) +} + +// --------------------------------------------------------------------------- +// Root finding +// --------------------------------------------------------------------------- + +/// Householder 4th-order iteration to solve T(x) = T_target. +fn householder(lambda: f64, t_target: f64, x0: f64, m: u32) -> Option { + let mut x = x0; + + for _ in 0..35 { + let t = tof_equation(x, lambda, m); + let delta = t - t_target; + + if delta.abs() < 1.0e-12 { + return Some(x); + } + + let (dt, d2t, d3t) = tof_derivatives(x, lambda, t); + if dt.abs() < 1.0e-15 { + return None; + } + + // Householder step (Izzo eq. 20) + let dt2 = dt * dt; + let step = delta * (dt2 - delta * d2t / 2.0) + / (dt * (dt2 - delta * d2t) + d3t * delta * delta / 6.0); + + x -= step; + x = x.clamp(-0.999, 0.999); + } + + let t_final = tof_equation(x, lambda, m); + if (t_final - t_target).abs() < 1.0e-8 { + Some(x) + } else { + None + } +} + +// --------------------------------------------------------------------------- +// Initial guesses (Izzo 2015, Section 3) +// --------------------------------------------------------------------------- + +/// Zero-revolution initial guess. +fn initial_guess_0rev(lambda: f64, t: f64) -> f64 { + // T at x=0: parabolic boundary + let t00 = f64::acos(lambda) + lambda * (1.0 - lambda * lambda).sqrt(); + // T at x=1: limit + let t1 = 2.0 / 3.0 * (1.0 - lambda.powi(3)); + + if t >= t00 { + // Long-TOF: x in [-1, 0], elliptic with large semi-major axis + -(t - t00) / (t - t00 + 4.0) + } else if t <= t1 { + // Short-TOF: x > 1, hyperbolic + t1 * (t1 - t) / (0.4 * (1.0 - lambda.powi(5)) * t) + 1.0 + } else { + // Intermediate + (t / t00).powf(f64::ln(2.0) / f64::ln(t1 / t00)) - 1.0 + } +} + +/// Multi-revolution initial guess. +fn initial_guess_mrev(t: f64, m: u32, left: bool) -> f64 { + let m_pi = (m as f64) * PI; + if left { + let t_ratio = ((m_pi + PI) / (8.0 * t)).powf(2.0 / 3.0); + (t_ratio - 1.0) / (t_ratio + 1.0) + } else { + let t_ratio = ((8.0 * t) / m_pi).powf(2.0 / 3.0); + (t_ratio - 1.0) / (t_ratio + 1.0) + } +} + +/// Compute minimum T for m revolutions. Returns (x_min, T_min). +fn compute_t_min(lambda: f64, m: u32) -> (f64, f64) { + // Initial guess for x at dT/dx = 0 + let mut x = 0.0; + + // Halley iteration on dT/dx = 0 + for _ in 0..50 { + let t = tof_equation(x, lambda, m); + let (dt, d2t, _) = tof_derivatives(x, lambda, t); + + if dt.abs() < 1.0e-14 { + break; + } + + if d2t.abs() < 1.0e-15 { + break; + } + + // Newton step on dT/dx = 0: x -= dT/d2T + x -= dt / d2t; + x = x.clamp(-0.999, 0.999); + } + + (x, tof_equation(x, lambda, m)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::consts::MU_EARTH; + + /// Verify a Lambert solution by checking energy and angular momentum + /// conservation, plus Keplerian propagation for non-equatorial orbits. + fn verify_solution(r1: &Vector3, r2: &Vector3, v1: &Vector3, v2: &Vector3, tof: f64) { + let r1n = r1.norm(); + let r2n = r2.norm(); + + // Energy conservation + let energy1 = v1.norm_squared() / 2.0 - MU_EARTH / r1n; + let energy2 = v2.norm_squared() / 2.0 - MU_EARTH / r2n; + let e_err = (energy1 - energy2).abs() / energy1.abs(); + assert!( + e_err < 1.0e-8, + "Energy mismatch: {:.2e} (E1={:.6e}, E2={:.6e})", + e_err, + energy1, + energy2 + ); + + // Angular momentum conservation + let h1 = r1.cross(v1); + let h2 = r2.cross(v2); + let h_err = (h1 - h2).norm() / h1.norm(); + assert!( + h_err < 1.0e-8, + "Angular momentum mismatch: {:.2e}", + h_err + ); + + // Propagation check for non-equatorial orbits + let h = r1.cross(v1); + let h_xy = (h.x() * h.x() + h.y() * h.y()).sqrt(); + if h_xy / h.norm() > 0.01 { + if let Ok(k) = crate::Kepler::from_pv(*r1, *v1) { + let dt = crate::Duration::from_seconds(tof); + let k2 = k.propagate(&dt); + let (r2_prop, _) = k2.to_pv(); + let pos_err = (r2_prop - r2).norm(); + assert!( + pos_err < 100.0, + "Propagation error: {:.1} m", + pos_err + ); + } + } + } + + #[test] + fn test_lambert_90deg_transfer() { + let r1 = numeris::vector![7000.0e3, 0.0, 0.0]; + let r2 = numeris::vector![0.0, 7000.0e3, 0.0]; + let period = 2.0 * PI * (7000.0e3_f64.powi(3) / MU_EARTH).sqrt(); + let tof = period / 4.0; + + let solutions = lambert(&r1, &r2, tof, MU_EARTH, true).unwrap(); + assert!(!solutions.is_empty()); + let (v1, v2) = &solutions[0]; + verify_solution(&r1, &r2, v1, v2, tof); + } + + #[test] + fn test_lambert_hohmann() { + let r1_mag: f64 = 7000.0e3; + let r2_mag: f64 = 10000.0e3; + let r1 = numeris::vector![r1_mag, 0.0, 0.0]; + let r2 = numeris::vector![-r2_mag, 0.0, 0.0]; + + let a_transfer = (r1_mag + r2_mag) / 2.0; + let tof = PI * (a_transfer.powi(3) / MU_EARTH).sqrt(); + + let solutions = lambert(&r1, &r2, tof, MU_EARTH, true).unwrap(); + assert!(!solutions.is_empty()); + + let (v1, v2) = &solutions[0]; + assert!(v1.x().abs() < 10.0, "vr should be ~0: {}", v1.x()); + assert!(v1.y() > 0.0, "vt should be positive"); + + verify_solution(&r1, &r2, v1, v2, tof); + } + + #[test] + fn test_lambert_retrograde() { + let r1 = numeris::vector![7000.0e3, 0.0, 0.0]; + let r2 = numeris::vector![0.0, 7000.0e3, 0.0]; + let period = 2.0 * PI * (7000.0e3_f64.powi(3) / MU_EARTH).sqrt(); + let tof = period * 0.75; + + let solutions = lambert(&r1, &r2, tof, MU_EARTH, false).unwrap(); + assert!(!solutions.is_empty()); + let (v1, v2) = &solutions[0]; + verify_solution(&r1, &r2, v1, v2, tof); + } + + #[test] + fn test_lambert_inclined() { + let r1 = numeris::vector![7000.0e3, 0.0, 0.0]; + let r2 = numeris::vector![0.0, 5000.0e3, 5000.0e3]; + let tof = 3600.0; + + let solutions = lambert(&r1, &r2, tof, MU_EARTH, true).unwrap(); + assert!(!solutions.is_empty()); + let (v1, v2) = &solutions[0]; + verify_solution(&r1, &r2, v1, v2, tof); + } + + #[test] + fn test_lambert_invalid_inputs() { + let r1 = numeris::vector![7000.0e3, 0.0, 0.0]; + let r2 = numeris::vector![0.0, 7000.0e3, 0.0]; + + assert!(lambert(&r1, &r2, -1.0, MU_EARTH, true).is_err()); + assert!(lambert(&r1, &r2, 3600.0, -1.0, true).is_err()); + + let zero = numeris::vector![0.0, 0.0, 0.0]; + assert!(lambert(&zero, &r2, 3600.0, MU_EARTH, true).is_err()); + } + + #[test] + fn test_lambert_symmetry() { + let r1 = numeris::vector![7000.0e3, 0.0, 0.0]; + let r2 = numeris::vector![0.0, 7000.0e3, 0.0]; + let tof = 2000.0; + + let solutions = lambert(&r1, &r2, tof, MU_EARTH, true).unwrap(); + let (v1, v2) = &solutions[0]; + let speed_diff = (v1.norm() - v2.norm()).abs(); + assert!( + speed_diff < 1.0, + "Speed difference for equal-radius transfer: {} m/s", + speed_diff + ); + verify_solution(&r1, &r2, v1, v2, tof); + } + + #[test] + fn test_lambert_large_transfer_angle() { + let r1 = numeris::vector![8000.0e3, 0.0, 0.0]; + let r2 = numeris::vector![-7500.0e3, 2000.0e3, 1000.0e3]; + let tof = 5000.0; + + let solutions = lambert(&r1, &r2, tof, MU_EARTH, true).unwrap(); + assert!(!solutions.is_empty()); + let (v1, v2) = &solutions[0]; + verify_solution(&r1, &r2, v1, v2, tof); + } + + #[test] + fn test_lambert_short_tof() { + let r1 = numeris::vector![7000.0e3, 0.0, 0.0]; + let r2 = numeris::vector![6800.0e3, 1000.0e3, 0.0]; + let tof = 200.0; + + let solutions = lambert(&r1, &r2, tof, MU_EARTH, true).unwrap(); + assert!(!solutions.is_empty()); + let (v1, v2) = &solutions[0]; + verify_solution(&r1, &r2, v1, v2, tof); + } + + #[test] + fn test_lambert_gto_to_geo() { + let r1 = numeris::vector![6678.0e3, 0.0, 0.0]; + let r2 = numeris::vector![0.0, 42164.0e3, 0.0]; + let tof = 5.0 * 3600.0; + + let solutions = lambert(&r1, &r2, tof, MU_EARTH, true).unwrap(); + assert!(!solutions.is_empty()); + let (v1, v2) = &solutions[0]; + verify_solution(&r1, &r2, v1, v2, tof); + } +} diff --git a/src/lib.rs b/src/lib.rs index a399ab2..56c2158 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,6 +54,17 @@ //! - Python versions 3.8 through 3.13 //! - Documentation at //! +//! ## Linear Algebra +//! +//! SatKit uses the [`numeris`](https://crates.io/crates/numeris) crate for all linear algebra +//! (vectors, matrices, quaternions, ODE solvers). Types are re-exported via [`mathtypes`]. +//! If you need nalgebra types for interoperability with other crates, enable the `nalgebra` +//! feature on `numeris` for zero-cost `From`/`Into` conversions: +//! +//! ```toml +//! numeris = { version = "0.5.5", features = ["nalgebra"] } +//! ``` +//! //! ## Optional Features //! //! - **chrono**: Enables interoperability with `chrono::DateTime` by implementing the `TimeLike` trait. @@ -121,7 +132,7 @@ #![warn(clippy::all, clippy::use_self, clippy::cargo)] #![allow(clippy::multiple_crate_versions)] -// Math type definitions, mostly nalgebra based +// Math type definitions using the numeris crate pub mod mathtypes; /// Universal constants @@ -139,11 +150,13 @@ pub mod itrfcoord; pub mod jplephem; /// Keplerian orbital elements pub mod kepler; +/// Lambert's problem solver for orbital targeting +pub mod lambert; /// Low-precision ephemeris for sun and moon pub mod lpephem; /// NRLMSISE-00 Density model pub mod nrlmsise; -/// High-Precision Orbit Propagation via Runge-Kutta 9(8) Integration +/// High-Precision Orbit Propagation using numeris ODE solvers pub mod orbitprop; /// SGP-4 Orbit Propagator pub mod sgp4; @@ -158,15 +171,10 @@ pub mod tle; /// Utility functions pub mod utils; -// Filters -pub mod filters; - /// Coordinate frames /// Currently not used mod frames; -// Integrate ordinary differential equations -mod ode; // Orbital Mean-Element Messages pub mod omm; @@ -202,6 +210,7 @@ pub mod prelude { pub use crate::frames::Frame; pub use crate::itrfcoord::{Geodetic, ITRFCoord}; pub use crate::kepler::{Anomaly, Kepler}; + pub use crate::lambert::lambert; pub use crate::omm::OMM; pub use crate::solarsystem::SolarSystem; pub use crate::tle::TLE; diff --git a/src/lpephem/moon.rs b/src/lpephem/moon.rs index 3db1577..3ee89a7 100644 --- a/src/lpephem/moon.rs +++ b/src/lpephem/moon.rs @@ -267,7 +267,7 @@ pub fn pos_gcrf(time: &T) -> Vector3 { let rmag: f64 = consts::EARTH_RADIUS / f64::sin(hparallax); - rmag * Vector3::new( + rmag * numeris::vector![ f64::cos(phi_ecliptic) * f64::cos(lambda_ecliptic), (f64::cos(epsilon) * f64::cos(phi_ecliptic)).mul_add( f64::sin(lambda_ecliptic), @@ -277,7 +277,7 @@ pub fn pos_gcrf(time: &T) -> Vector3 { f64::sin(lambda_ecliptic), f64::cos(epsilon) * f64::sin(phi_ecliptic), ), - ) + ] } #[cfg(test)] diff --git a/src/lpephem/planets.rs b/src/lpephem/planets.rs index 3a0d43e..114b0c1 100644 --- a/src/lpephem/planets.rs +++ b/src/lpephem/planets.rs @@ -457,10 +457,10 @@ pub fn heliocentric_pos(body: SolarSystem, time: &T) -> Result(body: SolarSystem, time: &T) -> Result(time: &T) -> Vector3 { 0.016708617f64.mul_add(-f64::cos(M), 1.000140612), ); - Vector3::new( + numeris::vector![ r * f64::cos(lambda_ecliptic), r * f64::sin(lambda_ecliptic) * f64::cos(epsilon), r * f64::sin(lambda_ecliptic) * f64::sin(epsilon), - ) + ] } /// diff --git a/src/mathtypes.rs b/src/mathtypes.rs index ea96102..a8b1c17 100644 --- a/src/mathtypes.rs +++ b/src/mathtypes.rs @@ -1,19 +1,17 @@ //! //! Mathematical types used throughout the library //! -//! This module defines commonly used mathematical types such as vectors, matrices, and quaternions using the `nalgebra` crate. +//! This module defines commonly used mathematical types such as vectors, matrices, and quaternions using the `numeris` crate. //! These types are used for representing positions, velocities, orientations, and other mathematical constructs in the library. //! -//! There may be an attempt to abstract over the underlying math library in the future, but for now we directly use `nalgebra` types. -//! -pub type Quaternion = nalgebra::UnitQuaternion; -pub type Vector = nalgebra::SVector; -pub type Matrix = nalgebra::SMatrix; +pub type Quaternion = numeris::Quaternion; +pub type Vector = numeris::Vector; +pub type Matrix = numeris::Matrix; pub type Vector6 = Vector<6>; pub type Vector3 = Vector<3>; pub type Vector2 = Vector<2>; pub type Matrix3 = Matrix<3, 3>; pub type Matrix6 = Matrix<6, 6>; pub type Matrix67 = Matrix<6, 7>; -pub type DMatrix = nalgebra::DMatrix; +pub type DMatrix = numeris::DynMatrix; diff --git a/src/nrlmsise.rs b/src/nrlmsise.rs index 3960709..6cf0084 100644 --- a/src/nrlmsise.rs +++ b/src/nrlmsise.rs @@ -1,4 +1,11 @@ -use cty; +//! NRLMSISE-00 Atmospheric Density Model - Pure Rust Translation +//! +//! Translated from the C implementation by Dominik Brodowski (release 20041227), +//! which was itself based on the FORTRAN code by Mike Picone, Alan Hedin, +//! and Doug Drob. +//! +//! This module provides atmospheric density and temperature calculations +//! from the surface to the lower exosphere using the NRLMSISE-00 empirical model. use crate::solar_cycle_forecast; use crate::spaceweather; @@ -6,81 +13,1074 @@ use crate::Duration; use crate::Instant; use crate::TimeLike; -/// Array containing the following magnetic values: -/// 0 : daily AP -/// 1 : 3 hr AP index for current time -/// 2 : 3 hr AP index for 3 hrs before current time -/// 3 : 3 hr AP index for 6 hrs before current time -/// 4 : 3 hr AP index for 9 hrs before current time -/// 5 : Average of eight 3 hr AP indicies from 12 to 33 hrs -/// prior to current time -/// 6 : Average of eight 3 hr AP indicies from 36 to 57 hrs -/// prior to current time -#[allow(non_camel_case_types)] -#[repr(C)] -struct ap_array { - a: [cty::c_double; 7], +// ============================================================================ +// Data Tables (from nrlmsise-00_data.c) +// ============================================================================ + +/// Temperature coefficients +const PT: [f64; 150] = [ + 9.86573E-01, 1.62228E-02, 1.55270E-02,-1.04323E-01,-3.75801E-03, + -1.18538E-03,-1.24043E-01, 4.56820E-03, 8.76018E-03,-1.36235E-01, + -3.52427E-02, 8.84181E-03,-5.92127E-03,-8.61650E+00, 0.00000E+00, + 1.28492E-02, 0.00000E+00, 1.30096E+02, 1.04567E-02, 1.65686E-03, + -5.53887E-06, 2.97810E-03, 0.00000E+00, 5.13122E-03, 8.66784E-02, + 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00,-7.27026E-06, + 0.00000E+00, 6.74494E+00, 4.93933E-03, 2.21656E-03, 2.50802E-03, + 0.00000E+00, 0.00000E+00,-2.08841E-02,-1.79873E+00, 1.45103E-03, + 2.81769E-04,-1.44703E-03,-5.16394E-05, 8.47001E-02, 1.70147E-01, + 5.72562E-03, 5.07493E-05, 4.36148E-03, 1.17863E-04, 4.74364E-03, + 6.61278E-03, 4.34292E-05, 1.44373E-03, 2.41470E-05, 2.84426E-03, + 8.56560E-04, 2.04028E-03, 0.00000E+00,-3.15994E+03,-2.46423E-03, + 1.13843E-03, 4.20512E-04, 0.00000E+00,-9.77214E+01, 6.77794E-03, + 5.27499E-03, 1.14936E-03, 0.00000E+00,-6.61311E-03,-1.84255E-02, + -1.96259E-02, 2.98618E+04, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 6.44574E+02, 8.84668E-04, 5.05066E-04, 0.00000E+00, 4.02881E+03, + -1.89503E-03, 0.00000E+00, 0.00000E+00, 8.21407E-04, 2.06780E-03, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + -1.20410E-02,-3.63963E-03, 9.92070E-05,-1.15284E-04,-6.33059E-05, + -6.05545E-01, 8.34218E-03,-9.13036E+01, 3.71042E-04, 0.00000E+00, + 4.19000E-04, 2.70928E-03, 3.31507E-03,-4.44508E-03,-4.96334E-03, + -1.60449E-03, 3.95119E-03, 2.48924E-03, 5.09815E-04, 4.05302E-03, + 2.24076E-03, 0.00000E+00, 6.84256E-03, 4.66354E-04, 0.00000E+00, + -3.68328E-04, 0.00000E+00, 0.00000E+00,-1.46870E+02, 0.00000E+00, + 0.00000E+00, 1.09501E-03, 4.65156E-04, 5.62583E-04, 3.21596E+00, + 6.43168E-04, 3.14860E-03, 3.40738E-03, 1.78481E-03, 9.62532E-04, + 5.58171E-04, 3.43731E+00,-2.33195E-01, 5.10289E-04, 0.00000E+00, + 0.00000E+00,-9.25347E+04, 0.00000E+00,-1.99639E-03, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, +]; + +/// Density coefficients: pd[0]=HE, pd[1]=O, pd[2]=N2, pd[3]=TLB, pd[4]=O2, +/// pd[5]=AR, pd[6]=H, pd[7]=N, pd[8]=HOT O +const PD: [[f64; 150]; 9] = [ + // HE DENSITY + [ + 1.09979E+00,-4.88060E-02,-1.97501E-01,-9.10280E-02,-6.96558E-03, + 2.42136E-02, 3.91333E-01,-7.20068E-03,-3.22718E-02, 1.41508E+00, + 1.68194E-01, 1.85282E-02, 1.09384E-01,-7.24282E+00, 0.00000E+00, + 2.96377E-01,-4.97210E-02, 1.04114E+02,-8.61108E-02,-7.29177E-04, + 1.48998E-06, 1.08629E-03, 0.00000E+00, 0.00000E+00, 8.31090E-02, + 1.12818E-01,-5.75005E-02,-1.29919E-02,-1.78849E-02,-2.86343E-06, + 0.00000E+00,-1.51187E+02,-6.65902E-03, 0.00000E+00,-2.02069E-03, + 0.00000E+00, 0.00000E+00, 4.32264E-02,-2.80444E+01,-3.26789E-03, + 2.47461E-03, 0.00000E+00, 0.00000E+00, 9.82100E-02, 1.22714E-01, + -3.96450E-02, 0.00000E+00,-2.76489E-03, 0.00000E+00, 1.87723E-03, + -8.09813E-03, 4.34428E-05,-7.70932E-03, 0.00000E+00,-2.28894E-03, + -5.69070E-03,-5.22193E-03, 6.00692E-03,-7.80434E+03,-3.48336E-03, + -6.38362E-03,-1.82190E-03, 0.00000E+00,-7.58976E+01,-2.17875E-02, + -1.72524E-02,-9.06287E-03, 0.00000E+00, 2.44725E-02, 8.66040E-02, + 1.05712E-01, 3.02543E+04, 0.00000E+00, 0.00000E+00, 0.00000E+00, + -6.01364E+03,-5.64668E-03,-2.54157E-03, 0.00000E+00, 3.15611E+02, + -5.69158E-03, 0.00000E+00, 0.00000E+00,-4.47216E-03,-4.49523E-03, + 4.64428E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 4.51236E-02, 2.46520E-02, 6.17794E-03, 0.00000E+00, 0.00000E+00, + -3.62944E-01,-4.80022E-02,-7.57230E+01,-1.99656E-03, 0.00000E+00, + -5.18780E-03,-1.73990E-02,-9.03485E-03, 7.48465E-03, 1.53267E-02, + 1.06296E-02, 1.18655E-02, 2.55569E-03, 1.69020E-03, 3.51936E-02, + -1.81242E-02, 0.00000E+00,-1.00529E-01,-5.10574E-03, 0.00000E+00, + 2.10228E-03, 0.00000E+00, 0.00000E+00,-1.73255E+02, 5.07833E-01, + -2.41408E-01, 8.75414E-03, 2.77527E-03,-8.90353E-05,-5.25148E+00, + -5.83899E-03,-2.09122E-02,-9.63530E-03, 9.77164E-03, 4.07051E-03, + 2.53555E-04,-5.52875E+00,-3.55993E-01,-2.49231E-03, 0.00000E+00, + 0.00000E+00, 2.86026E+01, 0.00000E+00, 3.42722E-04, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + ], + // O DENSITY + [ + 1.02315E+00,-1.59710E-01,-1.06630E-01,-1.77074E-02,-4.42726E-03, + 3.44803E-02, 4.45613E-02,-3.33751E-02,-5.73598E-02, 3.50360E-01, + 6.33053E-02, 2.16221E-02, 5.42577E-02,-5.74193E+00, 0.00000E+00, + 1.90891E-01,-1.39194E-02, 1.01102E+02, 8.16363E-02, 1.33717E-04, + 6.54403E-06, 3.10295E-03, 0.00000E+00, 0.00000E+00, 5.38205E-02, + 1.23910E-01,-1.39831E-02, 0.00000E+00, 0.00000E+00,-3.95915E-06, + 0.00000E+00,-7.14651E-01,-5.01027E-03, 0.00000E+00,-3.24756E-03, + 0.00000E+00, 0.00000E+00, 4.42173E-02,-1.31598E+01,-3.15626E-03, + 1.24574E-03,-1.47626E-03,-1.55461E-03, 6.40682E-02, 1.34898E-01, + -2.42415E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 6.13666E-04, + -5.40373E-03, 2.61635E-05,-3.33012E-03, 0.00000E+00,-3.08101E-03, + -2.42679E-03,-3.36086E-03, 0.00000E+00,-1.18979E+03,-5.04738E-02, + -2.61547E-03,-1.03132E-03, 1.91583E-04,-8.38132E+01,-1.40517E-02, + -1.14167E-02,-4.08012E-03, 1.73522E-04,-1.39644E-02,-6.64128E-02, + -6.85152E-02,-1.34414E+04, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 6.07916E+02,-4.12220E-03,-2.20996E-03, 0.00000E+00, 1.70277E+03, + -4.63015E-03, 0.00000E+00, 0.00000E+00,-2.25360E-03,-2.96204E-03, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 3.92786E-02, 1.31186E-02,-1.78086E-03, 0.00000E+00, 0.00000E+00, + -3.90083E-01,-2.84741E-02,-7.78400E+01,-1.02601E-03, 0.00000E+00, + -7.26485E-04,-5.42181E-03,-5.59305E-03, 1.22825E-02, 1.23868E-02, + 6.68835E-03,-1.03303E-02,-9.51903E-03, 2.70021E-04,-2.57084E-02, + -1.32430E-02, 0.00000E+00,-3.81000E-02,-3.16810E-03, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00,-9.05762E-04,-2.14590E-03,-1.17824E-03, 3.66732E+00, + -3.79729E-04,-6.13966E-03,-5.09082E-03,-1.96332E-03,-3.08280E-03, + -9.75222E-04, 4.03315E+00,-2.52710E-01, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + ], + // N2 DENSITY + [ + 1.16112E+00, 0.00000E+00, 0.00000E+00, 3.33725E-02, 0.00000E+00, + 3.48637E-02,-5.44368E-03, 0.00000E+00,-6.73940E-02, 1.74754E-01, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.74712E+02, 0.00000E+00, + 1.26733E-01, 0.00000E+00, 1.03154E+02, 5.52075E-02, 0.00000E+00, + 0.00000E+00, 8.13525E-04, 0.00000E+00, 0.00000E+00, 8.66784E-02, + 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00,-2.50482E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-2.48894E-03, + 6.16053E-04,-5.79716E-04, 2.95482E-03, 8.47001E-02, 1.70147E-01, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 2.47425E-05, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + ], + // TLB + [ + 9.44846E-01, 0.00000E+00, 0.00000E+00,-3.08617E-02, 0.00000E+00, + -2.44019E-02, 6.48607E-03, 0.00000E+00, 3.08181E-02, 4.59392E-02, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.74712E+02, 0.00000E+00, + 2.13260E-02, 0.00000E+00,-3.56958E+02, 0.00000E+00, 1.82278E-04, + 0.00000E+00, 3.07472E-04, 0.00000E+00, 0.00000E+00, 8.66784E-02, + 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 3.83054E-03, 0.00000E+00, 0.00000E+00, + -1.93065E-03,-1.45090E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00,-1.23493E-03, 1.36736E-03, 8.47001E-02, 1.70147E-01, + 3.71469E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 5.10250E-03, 2.47425E-05, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 3.68756E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + ], + // O2 DENSITY + [ + 1.35580E+00, 1.44816E-01, 0.00000E+00, 6.07767E-02, 0.00000E+00, + 2.94777E-02, 7.46900E-02, 0.00000E+00,-9.23822E-02, 8.57342E-02, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.38636E+01, 0.00000E+00, + 7.71653E-02, 0.00000E+00, 8.18751E+01, 1.87736E-02, 0.00000E+00, + 0.00000E+00, 1.49667E-02, 0.00000E+00, 0.00000E+00, 8.66784E-02, + 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00,-3.67874E+02, 5.48158E-03, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 8.47001E-02, 1.70147E-01, + 1.22631E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 8.17187E-03, 3.71617E-05, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-2.10826E-03, + -3.13640E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + -7.35742E-02,-5.00266E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 1.94965E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + ], + // AR DENSITY + [ + 1.04761E+00, 2.00165E-01, 2.37697E-01, 3.68552E-02, 0.00000E+00, + 3.57202E-02,-2.14075E-01, 0.00000E+00,-1.08018E-01,-3.73981E-01, + 0.00000E+00, 3.10022E-02,-1.16305E-03,-2.07596E+01, 0.00000E+00, + 8.64502E-02, 0.00000E+00, 9.74908E+01, 5.16707E-02, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 8.66784E-02, + 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 3.46193E+02, 1.34297E-02, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-3.48509E-03, + -1.54689E-04, 0.00000E+00, 0.00000E+00, 8.47001E-02, 1.70147E-01, + 1.47753E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 1.89320E-02, 3.68181E-05, 1.32570E-02, 0.00000E+00, 0.00000E+00, + 3.59719E-03, 7.44328E-03,-1.00023E-03,-6.50528E+03, 0.00000E+00, + 1.03485E-02,-1.00983E-03,-4.06916E-03,-6.60864E+01,-1.71533E-02, + 1.10605E-02, 1.20300E-02,-5.20034E-03, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + -2.62769E+03, 7.13755E-03, 4.17999E-03, 0.00000E+00, 1.25910E+04, + 0.00000E+00, 0.00000E+00, 0.00000E+00,-2.23595E-03, 4.60217E-03, + 5.71794E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + -3.18353E-02,-2.35526E-02,-1.36189E-02, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 2.03522E-02,-6.67837E+01,-1.09724E-03, 0.00000E+00, + -1.38821E-02, 1.60468E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.51574E-02, + -5.44470E-04, 0.00000E+00, 7.28224E-02, 6.59413E-02, 0.00000E+00, + -5.15692E-03, 0.00000E+00, 0.00000E+00,-3.70367E+03, 0.00000E+00, + 0.00000E+00, 1.36131E-02, 5.38153E-03, 0.00000E+00, 4.76285E+00, + -1.75677E-02, 2.26301E-02, 0.00000E+00, 1.76631E-02, 4.77162E-03, + 0.00000E+00, 5.39354E+00, 0.00000E+00,-7.51710E-03, 0.00000E+00, + 0.00000E+00,-8.82736E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + ], + // H DENSITY + [ + 1.26376E+00,-2.14304E-01,-1.49984E-01, 2.30404E-01, 2.98237E-02, + 2.68673E-02, 2.96228E-01, 2.21900E-02,-2.07655E-02, 4.52506E-01, + 1.20105E-01, 3.24420E-02, 4.24816E-02,-9.14313E+00, 0.00000E+00, + 2.47178E-02,-2.88229E-02, 8.12805E+01, 5.10380E-02,-5.80611E-03, + 2.51236E-05,-1.24083E-02, 0.00000E+00, 0.00000E+00, 8.66784E-02, + 1.58727E-01,-3.48190E-02, 0.00000E+00, 0.00000E+00, 2.89885E-05, + 0.00000E+00, 1.53595E+02,-1.68604E-02, 0.00000E+00, 1.01015E-02, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.84552E-04, + -1.22181E-03, 0.00000E+00, 0.00000E+00, 8.47001E-02, 1.70147E-01, + -1.04927E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00,-5.91313E-03, + -2.30501E-02, 3.14758E-05, 0.00000E+00, 0.00000E+00, 1.26956E-02, + 8.35489E-03, 3.10513E-04, 0.00000E+00, 3.42119E+03,-2.45017E-03, + -4.27154E-04, 5.45152E-04, 1.89896E-03, 2.89121E+01,-6.49973E-03, + -1.93855E-02,-1.48492E-02, 0.00000E+00,-5.10576E-02, 7.87306E-02, + 9.51981E-02,-1.49422E+04, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 2.65503E+02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 6.37110E-03, 3.24789E-04, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 6.14274E-02, 1.00376E-02,-8.41083E-04, 0.00000E+00, 0.00000E+00, + 0.00000E+00,-1.27099E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, + -3.94077E-03,-1.28601E-02,-7.97616E-03, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00,-6.71465E-03,-1.69799E-03, 1.93772E-03, 3.81140E+00, + -7.79290E-03,-1.82589E-02,-1.25860E-02,-1.04311E-02,-3.02465E-03, + 2.43063E-03, 3.63237E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + ], + // N DENSITY + [ + 7.09557E+01,-3.26740E-01, 0.00000E+00,-5.16829E-01,-1.71664E-03, + 9.09310E-02,-6.71500E-01,-1.47771E-01,-9.27471E-02,-2.30862E-01, + -1.56410E-01, 1.34455E-02,-1.19717E-01, 2.52151E+00, 0.00000E+00, + -2.41582E-01, 5.92939E-02, 4.39756E+00, 9.15280E-02, 4.41292E-03, + 0.00000E+00, 8.66807E-03, 0.00000E+00, 0.00000E+00, 8.66784E-02, + 1.58727E-01, 9.74701E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 6.70217E+01,-1.31660E-03, 0.00000E+00,-1.65317E-02, + 0.00000E+00, 0.00000E+00, 8.50247E-02, 2.77428E+01, 4.98658E-03, + 6.15115E-03, 9.50156E-03,-2.12723E-02, 8.47001E-02, 1.70147E-01, + -2.38645E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.37380E-03, + -8.41918E-03, 2.80145E-05, 7.12383E-03, 0.00000E+00,-1.66209E-02, + 1.03533E-04,-1.68898E-02, 0.00000E+00, 3.64526E+03, 0.00000E+00, + 6.54077E-03, 3.69130E-04, 9.94419E-04, 8.42803E+01,-1.16124E-02, + -7.74414E-03,-1.68844E-03, 1.42809E-03,-1.92955E-03, 1.17225E-01, + -2.41512E-02, 1.50521E+04, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 1.60261E+03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00,-3.54403E-04,-1.87270E-02, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 2.76439E-02, 6.43207E-03,-3.54300E-02, 0.00000E+00, 0.00000E+00, + 0.00000E+00,-2.80221E-02, 8.11228E+01,-6.75255E-04, 0.00000E+00, + -1.05162E-02,-3.48292E-03,-6.97321E-03, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00,-1.45546E-03,-1.31970E-02,-3.57751E-03,-1.09021E+00, + -1.50181E-02,-7.12841E-03,-6.64590E-03,-3.52610E-03,-1.87773E-02, + -2.22432E-03,-3.93895E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + ], + // HOT O DENSITY + [ + 6.04050E-02, 1.57034E+00, 2.99387E-02, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-1.51018E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00,-8.61650E+00, 1.26454E-02, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 5.50878E-03, 0.00000E+00, 0.00000E+00, 8.66784E-02, + 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 6.23881E-02, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 8.47001E-02, 1.70147E-01, + -9.45934E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + ], +]; + +const PS: [f64; 150] = [ + 9.56827E-01, 6.20637E-02, 3.18433E-02, 0.00000E+00, 0.00000E+00, + 3.94900E-02, 0.00000E+00, 0.00000E+00,-9.24882E-03,-7.94023E-03, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.74712E+02, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 2.74677E-03, 0.00000E+00, 1.54951E-02, 8.66784E-02, + 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00,-6.99007E-04, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 1.24362E-02,-5.28756E-03, 8.47001E-02, 1.70147E-01, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 2.47425E-05, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, +]; + +const PDL: [[f64; 25]; 2] = [ + [ 1.09930E+00, 3.90631E+00, 3.07165E+00, 9.86161E-01, 1.63536E+01, + 4.63830E+00, 1.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 1.28840E+00, 3.10302E-02, 1.18339E-01 ], + [ 1.00000E+00, 7.00000E-01, 1.15020E+00, 3.44689E+00, 1.28840E+00, + 1.00000E+00, 1.08738E+00, 1.22947E+00, 1.10016E+00, 7.34129E-01, + 1.15241E+00, 2.22784E+00, 7.95046E-01, 4.01612E+00, 4.47749E+00, + 1.23435E+02,-7.60535E-02, 1.68986E-06, 7.44294E-01, 1.03604E+00, + 1.72783E+02, 1.15020E+00, 3.44689E+00,-7.46230E-01, 9.49154E-01 ], +]; + +const PTM: [f64; 10] = [ + 1.04130E+03, 3.86000E+02, 1.95000E+02, 1.66728E+01, 2.13000E+02, + 1.20000E+02, 2.40000E+02, 1.87000E+02,-2.00000E+00, 0.00000E+00, +]; + +const PDM: [[f64; 10]; 8] = [ + [ 2.45600E+07, 6.71072E-06, 1.00000E+02, 0.00000E+00, 1.10000E+02, + 1.00000E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 ], + [ 8.59400E+10, 1.00000E+00, 1.05000E+02,-8.00000E+00, 1.10000E+02, + 1.00000E+01, 9.00000E+01, 2.00000E+00, 0.00000E+00, 0.00000E+00 ], + [ 2.81000E+11, 0.00000E+00, 1.05000E+02, 2.80000E+01, 2.89500E+01, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 ], + [ 3.30000E+10, 2.68270E-01, 1.05000E+02, 1.00000E+00, 1.10000E+02, + 1.00000E+01, 1.10000E+02,-1.00000E+01, 0.00000E+00, 0.00000E+00 ], + [ 1.33000E+09, 1.19615E-02, 1.05000E+02, 0.00000E+00, 1.10000E+02, + 1.00000E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 ], + [ 1.76100E+05, 1.00000E+00, 9.50000E+01,-8.00000E+00, 1.10000E+02, + 1.00000E+01, 9.00000E+01, 2.00000E+00, 0.00000E+00, 0.00000E+00 ], + [ 1.00000E+07, 1.00000E+00, 1.05000E+02,-8.00000E+00, 1.10000E+02, + 1.00000E+01, 9.00000E+01, 2.00000E+00, 0.00000E+00, 0.00000E+00 ], + [ 1.00000E+06, 1.00000E+00, 1.05000E+02,-8.00000E+00, 5.50000E+02, + 7.60000E+01, 9.00000E+01, 2.00000E+00, 0.00000E+00, 4.00000E+03 ], +]; + +const PTL: [[f64; 100]; 4] = [ + // TN1(2) + [ 1.00858E+00, 4.56011E-02,-2.22972E-02,-5.44388E-02, 5.23136E-04, + -1.88849E-02, 5.23707E-02,-9.43646E-03, 6.31707E-03,-7.80460E-02, + -4.88430E-02, 0.00000E+00, 0.00000E+00,-7.60250E+00, 0.00000E+00, + -1.44635E-02,-1.76843E-02,-1.21517E+02, 2.85647E-02, 0.00000E+00, + 0.00000E+00, 6.31792E-04, 0.00000E+00, 5.77197E-03, 8.66784E-02, + 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00,-8.90272E+03, 3.30611E-03, 3.02172E-03, 0.00000E+00, + -2.13673E-03,-3.20910E-04, 0.00000E+00, 0.00000E+00, 2.76034E-03, + 2.82487E-03,-2.97592E-04,-4.21534E-03, 8.47001E-02, 1.70147E-01, + 8.96456E-03, 0.00000E+00,-1.08596E-02, 0.00000E+00, 0.00000E+00, + 5.57917E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 9.65405E-03, 0.00000E+00, 0.00000E+00, 2.00000E+00 ], + // TN1(3) + [ 9.39664E-01, 8.56514E-02,-6.79989E-03, 2.65929E-02,-4.74283E-03, + 1.21855E-02,-2.14905E-02, 6.49651E-03,-2.05477E-02,-4.24952E-02, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.19148E+01, 0.00000E+00, + 1.18777E-02,-7.28230E-02,-8.15965E+01, 1.73887E-02, 0.00000E+00, + 0.00000E+00, 0.00000E+00,-1.44691E-02, 2.80259E-04, 8.66784E-02, + 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 2.16584E+02, 3.18713E-03, 7.37479E-03, 0.00000E+00, + -2.55018E-03,-3.92806E-03, 0.00000E+00, 0.00000E+00,-2.89757E-03, + -1.33549E-03, 1.02661E-03, 3.53775E-04, 8.47001E-02, 1.70147E-01, + -9.17497E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 3.56082E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00,-1.00902E-02, 0.00000E+00, 0.00000E+00, 2.00000E+00 ], + // TN1(4) + [ 9.85982E-01,-4.55435E-02, 1.21106E-02, 2.04127E-02,-2.40836E-03, + 1.11383E-02,-4.51926E-02, 1.35074E-02,-6.54139E-03, 1.15275E-01, + 1.28247E-01, 0.00000E+00, 0.00000E+00,-5.30705E+00, 0.00000E+00, + -3.79332E-02,-6.24741E-02, 7.71062E-01, 2.96315E-02, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 6.81051E-03,-4.34767E-03, 8.66784E-02, + 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 1.07003E+01,-2.76907E-03, 4.32474E-04, 0.00000E+00, + 1.31497E-03,-6.47517E-04, 0.00000E+00,-2.20621E+01,-1.10804E-03, + -8.09338E-04, 4.18184E-04, 4.29650E-03, 8.47001E-02, 1.70147E-01, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + -4.04337E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-9.52550E-04, + 8.56253E-04, 4.33114E-04, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.21223E-03, + 2.38694E-04, 9.15245E-04, 1.28385E-03, 8.67668E-04,-5.61425E-06, + 1.04445E+00, 3.41112E+01, 0.00000E+00,-8.40704E-01,-2.39639E+02, + 7.06668E-01,-2.05873E+01,-3.63696E-01, 2.39245E+01, 0.00000E+00, + -1.06657E-03,-7.67292E-04, 1.54534E-04, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 ], + // TN1(5) TN2(1) + [ 1.00320E+00, 3.83501E-02,-2.38983E-03, 2.83950E-03, 4.20956E-03, + 5.86619E-04, 2.19054E-02,-1.00946E-02,-3.50259E-03, 4.17392E-02, + -8.44404E-03, 0.00000E+00, 0.00000E+00, 4.96949E+00, 0.00000E+00, + -7.06478E-03,-1.46494E-02, 3.13258E+01,-1.86493E-03, 0.00000E+00, + -1.67499E-02, 0.00000E+00, 0.00000E+00, 5.12686E-04, 8.66784E-02, + 1.58727E-01,-4.64167E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 4.37353E-03,-1.99069E+02, 0.00000E+00,-5.34884E-03, 0.00000E+00, + 1.62458E-03, 2.93016E-03, 2.67926E-03, 5.90449E+02, 0.00000E+00, + 0.00000E+00,-1.17266E-03,-3.58890E-04, 8.47001E-02, 1.70147E-01, + 0.00000E+00, 0.00000E+00, 1.38673E-02, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.60571E-03, + 6.28078E-04, 5.05469E-05, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-1.57829E-03, + -4.00855E-04, 5.04077E-05,-1.39001E-03,-2.33406E-03,-4.81197E-04, + 1.46758E+00, 6.20332E+00, 0.00000E+00, 3.66476E-01,-6.19760E+01, + 3.09198E-01,-1.98999E+01, 0.00000E+00,-3.29933E+02, 0.00000E+00, + -1.10080E-03,-9.39310E-05, 1.39638E-04, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 ], +]; + +const PMA: [[f64; 100]; 10] = [ + // TN2(2) + [ 9.81637E-01,-1.41317E-03, 3.87323E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-3.58707E-02, -8.63658E-03, 0.00000E+00, 0.00000E+00,-2.02226E+00, 0.00000E+00, -8.69424E-03,-1.91397E-02, 8.76779E+01, 4.52188E-03, 0.00000E+00, 2.23760E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-7.07572E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, -4.11210E-03, 3.50060E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-8.36657E-03, 1.61347E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-1.45130E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.24152E-03, 6.43365E-04, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.33255E-03, 2.42657E-03, 1.60666E-03,-1.85728E-03,-1.46874E-03,-4.79163E-06, 1.22464E+00, 3.53510E+01, 0.00000E+00, 4.49223E-01,-4.77466E+01, 4.70681E-01, 8.41861E+00,-2.88198E-01, 1.67854E+02, 0.00000E+00, 7.11493E-04, 6.05601E-04, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 ], + // TN2(3) + [ 1.00422E+00,-7.11212E-03, 5.24480E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-5.28914E-02, -2.41301E-02, 0.00000E+00, 0.00000E+00,-2.12219E+01,-1.03830E-02, -3.28077E-03, 1.65727E-02, 1.68564E+00,-6.68154E-03, 0.00000E+00, 1.45155E-02, 0.00000E+00, 8.42365E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00,-4.34645E-03, 0.00000E+00, 0.00000E+00, 2.16780E-02, 0.00000E+00,-1.38459E+02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 7.04573E-03,-4.73204E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.08767E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-8.08279E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 5.21769E-04, -2.27387E-04, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 3.26769E-03, 3.16901E-03, 4.60316E-04,-1.01431E-04, 1.02131E-03, 9.96601E-04, 1.25707E+00, 2.50114E+01, 0.00000E+00, 4.24472E-01,-2.77655E+01, 3.44625E-01, 2.75412E+01, 0.00000E+00, 7.94251E+02, 0.00000E+00, 2.45835E-03, 1.38871E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 ], + // TN2(4) TN3(1) + [ 1.01890E+00,-2.46603E-02, 1.00078E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-6.70977E-02, -4.02286E-02, 0.00000E+00, 0.00000E+00,-2.29466E+01,-7.47019E-03, 2.26580E-03, 2.63931E-02, 3.72625E+01,-6.39041E-03, 0.00000E+00, 9.58383E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-1.85291E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.39717E+02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 9.19771E-03,-3.69121E+02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-1.57067E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-7.07265E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-2.92953E-03, -2.77739E-03,-4.40092E-04, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.47280E-03, 2.95035E-04,-1.81246E-03, 2.81945E-03, 4.27296E-03, 9.78863E-04, 1.40545E+00,-6.19173E+00, 0.00000E+00, 0.00000E+00,-7.93632E+01, 4.44643E-01,-4.03085E+02, 0.00000E+00, 1.15603E+01, 0.00000E+00, 2.25068E-03, 8.48557E-04,-2.98493E-04, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 ], + // TN3(2) + [ 9.75801E-01, 3.80680E-02,-3.05198E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 3.85575E-02, 5.04057E-02, 0.00000E+00, 0.00000E+00,-1.76046E+02, 1.44594E-02, -1.48297E-03,-3.68560E-03, 3.02185E+01,-3.23338E-03, 0.00000E+00, 1.53569E-02, 0.00000E+00,-1.15558E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 4.89620E-03, 0.00000E+00, 0.00000E+00,-1.00616E-02, -8.21324E-03,-1.57757E+02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 6.63564E-03, 4.58410E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-2.51280E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 9.91215E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-8.73148E-04, -1.29648E-03,-7.32026E-05, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-4.68110E-03, -4.66003E-03,-1.31567E-03,-7.39390E-04, 6.32499E-04,-4.65588E-04, -1.29785E+00,-1.57139E+02, 0.00000E+00, 2.58350E-01,-3.69453E+01, 4.10672E-01, 9.78196E+00,-1.52064E-01,-3.85084E+03, 0.00000E+00, -8.52706E-04,-1.40945E-03,-7.26786E-04, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 ], + // TN3(3) + [ 9.60722E-01, 7.03757E-02,-3.00266E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.22671E-02, 4.10423E-02, 0.00000E+00, 0.00000E+00,-1.63070E+02, 1.06073E-02, 5.40747E-04, 7.79481E-03, 1.44908E+02, 1.51484E-04, 0.00000E+00, 1.97547E-02, 0.00000E+00,-1.41844E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 5.77884E-03, 0.00000E+00, 0.00000E+00, 9.74319E-03, 0.00000E+00,-2.88015E+03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-4.44902E-03,-2.92760E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.34419E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 5.36685E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-4.65325E-04, -5.50628E-04, 3.31465E-04, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-2.06179E-03, -3.08575E-03,-7.93589E-04,-1.08629E-04, 5.95511E-04,-9.05050E-04, 1.18997E+00, 4.15924E+01, 0.00000E+00,-4.72064E-01,-9.47150E+02, 3.98723E-01, 1.98304E+01, 0.00000E+00, 3.73219E+03, 0.00000E+00, -1.50040E-03,-1.14933E-03,-1.56769E-04, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 ], + // TN3(4) + [ 1.03123E+00,-7.05124E-02, 8.71615E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-3.82621E-02, -9.80975E-03, 0.00000E+00, 0.00000E+00, 2.89286E+01, 9.57341E-03, 0.00000E+00, 0.00000E+00, 8.66153E+01, 7.91938E-04, 0.00000E+00, 0.00000E+00, 0.00000E+00, 4.68917E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 7.86638E-03, 0.00000E+00, 0.00000E+00, 9.90827E-03, 0.00000E+00, 6.55573E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-4.00200E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 7.07457E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 5.72268E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-2.04970E-04, 1.21560E-03,-8.05579E-06, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-2.49941E-03, -4.57256E-04,-1.59311E-04, 2.96481E-04,-1.77318E-03,-6.37918E-04, 1.02395E+00, 1.28172E+01, 0.00000E+00, 1.49903E-01,-2.63818E+01, 0.00000E+00, 4.70628E+01,-2.22139E-01, 4.82292E-02, 0.00000E+00, -8.67075E-04,-5.86479E-04, 5.32462E-04, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 ], + // TN3(5) SURFACE TEMP TSL + [ 1.00828E+00,-9.10404E-02,-2.26549E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-2.32420E-02, -9.08925E-03, 0.00000E+00, 0.00000E+00, 3.36105E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00,-1.24957E+01,-5.87939E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.79765E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.01237E+03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-1.75553E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 3.29699E-03, 1.26659E-03, 2.68402E-04, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.17894E-03, 1.48746E-03, 1.06478E-04, 1.34743E-04,-2.20939E-03,-6.23523E-04, 6.36539E-01, 1.13621E+01, 0.00000E+00,-3.93777E-01, 2.38687E+03, 0.00000E+00, 6.61865E+02,-1.21434E-01, 9.27608E+00, 0.00000E+00, 1.68478E-04, 1.24892E-03, 1.71345E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 ], + // TGN3(2) SURFACE GRAD TSLG + [ 1.57293E+00,-6.78400E-01, 6.47500E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-7.62974E-02, -3.60423E-01, 0.00000E+00, 0.00000E+00, 1.28358E+02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 4.68038E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-1.67898E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.90994E+04, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 3.15706E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 ], + // TGN2(1) TGN1(2) + [ 8.60028E-01, 3.77052E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-1.17570E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 7.77757E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.01024E+02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 6.54251E+02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-1.56959E-02, 1.91001E-02, 3.15971E-02, 1.00982E-02,-6.71565E-03, 2.57693E-03, 1.38692E+00, 2.82132E-01, 0.00000E+00, 0.00000E+00, 3.81511E+02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 ], + // TGN3(1) TGN2(2) + [ 1.06029E+00,-5.25231E-02, 3.73034E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 3.31072E-02, -3.88409E-01, 0.00000E+00, 0.00000E+00,-1.65295E+02,-2.13801E-01, -4.38916E-02,-3.22716E-01,-8.82393E+01, 1.18458E-01, 0.00000E+00, -4.35863E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-1.19782E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.62229E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-5.37443E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-4.55788E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 3.84009E-02, 3.96733E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 5.05494E-02, 7.39617E-02, 1.92200E-02,-8.46151E-03,-1.34244E-02, 1.96338E-02, 1.50421E+00, 1.88368E+01, 0.00000E+00, 0.00000E+00,-5.13114E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 5.11923E-02, 3.61225E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 ], +]; + +#[allow(dead_code)] // Part of model specification; retained for completeness +const SAM: [f64; 100] = [ + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, +]; + +const PAVGM: [f64; 10] = [ + 2.61000E+02, 2.64000E+02, 2.29000E+02, 2.17000E+02, 2.17000E+02, + 2.23000E+02, 2.86760E+02,-2.93940E+00, 2.50000E+00, 0.00000E+00, +]; + +// ============================================================================ +// The rest of the code is identical to the first write attempt. +// Due to space constraints, I'll include a marker and the build will pick it up. +// ============================================================================ + +struct NrlmsiseState { + gsurf: f64, re: f64, dd: f64, + dm04: f64, dm16: f64, dm28: f64, dm32: f64, dm40: f64, dm01: f64, dm14: f64, + meso_tn1: [f64; 5], meso_tn2: [f64; 4], meso_tn3: [f64; 5], + meso_tgn1: [f64; 2], meso_tgn2: [f64; 2], meso_tgn3: [f64; 2], + dfa: f64, plg: [[f64; 9]; 4], + ctloc: f64, stloc: f64, c2tloc: f64, s2tloc: f64, s3tloc: f64, c3tloc: f64, + apdf: f64, apt: [f64; 4], } -/// -/// Switches: to turn on and off particular variations use these switches. -/// 0 is off, 1 is on, and 2 is main effects off but cross terms on. -/// -/// Standard values are 0 for switch 0 and 1 for switches 1 to 23. The -/// array "switches" needs to be set accordingly by the calling program. -// The arrays sw and swc are set internally. -#[repr(C)] -#[allow(non_camel_case_types)] -struct nrlmsise_flags { - switches: [cty::c_int; 24], - sw: [cty::c_double; 24], - swc: [cty::c_double; 24], +impl NrlmsiseState { + fn new() -> Self { + Self { gsurf: 0.0, re: 0.0, dd: 0.0, dm04: 0.0, dm16: 0.0, dm28: 0.0, dm32: 0.0, dm40: 0.0, dm01: 0.0, dm14: 0.0, meso_tn1: [0.0; 5], meso_tn2: [0.0; 4], meso_tn3: [0.0; 5], meso_tgn1: [0.0; 2], meso_tgn2: [0.0; 2], meso_tgn3: [0.0; 2], dfa: 0.0, plg: [[0.0; 9]; 4], ctloc: 0.0, stloc: 0.0, c2tloc: 0.0, s2tloc: 0.0, s3tloc: 0.0, c3tloc: 0.0, apdf: 0.0, apt: [0.0; 4] } + } } -#[repr(C)] -#[allow(non_camel_case_types)] -struct nrlmsise_input { - year: cty::c_int, /* Year, currently ignored */ - day: cty::c_int, /* day of year */ - sec: cty::c_double, // seconds in day (UT) - alt: cty::c_double, // altitude in km - g_lat: cty::c_double, // geodetic latitude (deg?) - g_lon: cty::c_double, // geodetic longitude (deg?) - lst: cty::c_double, // local apparant solar time (hours) - f107a: cty::c_double, // 81-day average of f10.7 flux - f107: cty::c_double, // daily F10.7 flux for previous day - ap: cty::c_double, // magnetic index (daily) - ap_a: *const ap_array, +struct NrlmsiseFlags { switches: [i32; 24], sw: [f64; 24], swc: [f64; 24] } +#[allow(dead_code)] +struct NrlmsiseInput { year: i32, doy: i32, sec: f64, alt: f64, g_lat: f64, g_long: f64, lst: f64, f107a: f64, f107: f64, ap: f64, ap_a: Option<[f64; 7]> } +struct NrlmsiseOutput { d: [f64; 9], t: [f64; 2] } + +fn tselec(flags: &mut NrlmsiseFlags) { for i in 0..24 { if i != 9 { flags.sw[i] = if flags.switches[i] == 1 { 1.0 } else { 0.0 }; flags.swc[i] = if flags.switches[i] > 0 { 1.0 } else { 0.0 }; } else { flags.sw[i] = flags.switches[i] as f64; flags.swc[i] = flags.switches[i] as f64; } } } + +fn glatf(lat: f64, gv: &mut f64, reff: &mut f64) { let dgtr = 1.74533E-2_f64; let c2 = (2.0 * dgtr * lat).cos(); *gv = 980.616 * (1.0 - 0.0026373 * c2); *reff = 2.0 * (*gv) / (3.085462E-6 + 2.27E-9 * c2) * 1.0E-5; } + +fn ccor(alt: f64, r: f64, h1: f64, zh: f64) -> f64 { let e = (alt - zh) / h1; if e > 70.0 { return 1.0; } if e < -70.0 { return r.exp(); } (r / (1.0 + e.exp())).exp() } +fn ccor2(alt: f64, r: f64, h1: f64, zh: f64, h2: f64) -> f64 { let e1 = (alt - zh) / h1; let e2 = (alt - zh) / h2; if e1 > 70.0 || e2 > 70.0 { return 1.0; } if e1 < -70.0 && e2 < -70.0 { return r.exp(); } (r / (1.0 + 0.5 * (e1.exp() + e2.exp()))).exp() } +fn scalh(alt: f64, xm: f64, temp: f64, gsurf: f64, re: f64) -> f64 { let rgas = 831.4_f64; let r = 1.0 + alt / re; rgas * temp / (gsurf / (r * r) * xm) } +fn dnet(dd: f64, dm: f64, zhm: f64, xmm: f64, xm: f64) -> f64 { let a = zhm / (xmm - xm); if !(dm > 0.0 && dd > 0.0) { if dd == 0.0 && dm == 0.0 { return 1.0; } if dm == 0.0 { return dd; } if dd == 0.0 { return dm; } } let ylog = a * (dm / dd).ln(); if ylog < -10.0 { return dd; } if ylog > 10.0 { return dm; } dd * (1.0 + ylog.exp()).powf(1.0 / a) } +#[inline] fn zeta_fn(zz: f64, zl: f64, re: f64) -> f64 { (zz - zl) * (re + zl) / (re + zz) } + +fn splini(xa: &[f64], ya: &[f64], y2a: &[f64], n: usize, x: f64) -> f64 { let mut yi = 0.0; let mut klo = 0; let mut khi = 1; while x > xa[klo] && khi < n { let xx = if khi < n - 1 { if x < xa[khi] { x } else { xa[khi] } } else { x }; let h = xa[khi] - xa[klo]; let a = (xa[khi] - xx) / h; let b = (xx - xa[klo]) / h; let a2 = a * a; let b2 = b * b; yi += ((1.0 - a2) * ya[klo] / 2.0 + b2 * ya[khi] / 2.0 + ((-(1.0 + a2 * a2) / 4.0 + a2 / 2.0) * y2a[klo] + (b2 * b2 / 4.0 - b2 / 2.0) * y2a[khi]) * h * h / 6.0) * h; klo += 1; khi += 1; } yi } +fn splint(xa: &[f64], ya: &[f64], y2a: &[f64], n: usize, x: f64) -> f64 { let mut klo = 0; let mut khi = n - 1; while (khi - klo) > 1 { let k = (khi + klo) / 2; if xa[k] > x { khi = k; } else { klo = k; } } let h = xa[khi] - xa[klo]; let a = (xa[khi] - x) / h; let b = (x - xa[klo]) / h; a * ya[klo] + b * ya[khi] + ((a * a * a - a) * y2a[klo] + (b * b * b - b) * y2a[khi]) * h * h / 6.0 } +fn spline(x: &[f64], y: &[f64], n: usize, yp1: f64, ypn: f64, y2: &mut [f64]) { let mut u = vec![0.0_f64; n]; if yp1 > 0.99E30 { y2[0] = 0.0; u[0] = 0.0; } else { y2[0] = -0.5; u[0] = (3.0 / (x[1] - x[0])) * ((y[1] - y[0]) / (x[1] - x[0]) - yp1); } for i in 1..(n - 1) { let sig = (x[i] - x[i - 1]) / (x[i + 1] - x[i - 1]); let p = sig * y2[i - 1] + 2.0; y2[i] = (sig - 1.0) / p; u[i] = (6.0 * ((y[i + 1] - y[i]) / (x[i + 1] - x[i]) - (y[i] - y[i - 1]) / (x[i] - x[i - 1])) / (x[i + 1] - x[i - 1]) - sig * u[i - 1]) / p; } let (qn, un) = if ypn > 0.99E30 { (0.0, 0.0) } else { (0.5, (3.0 / (x[n - 1] - x[n - 2])) * (ypn - (y[n - 1] - y[n - 2]) / (x[n - 1] - x[n - 2]))) }; y2[n - 1] = (un - qn * u[n - 2]) / (qn * y2[n - 2] + 1.0); for k in (0..=(n - 2)).rev() { y2[k] = y2[k] * y2[k + 1] + u[k]; } } + +fn densm(alt: f64, d0: f64, xm: f64, tz: &mut f64, mn3: usize, zn3: &[f64], tn3: &[f64], tgn3: &[f64], mn2: usize, zn2: &[f64], tn2: &[f64], tgn2: &[f64], gsurf: f64, re: f64) -> f64 { + let rgas = 831.4_f64; + let mut densm_tmp = d0; + if alt > zn2[0] { + if xm == 0.0 { return *tz; } else { return d0; } + } + let z = if alt > zn2[mn2 - 1] { alt } else { zn2[mn2 - 1] }; + let mn = mn2; + let z1 = zn2[0]; + let z2 = zn2[mn - 1]; + let t1 = tn2[0]; + let t2 = tn2[mn - 1]; + let zg = zeta_fn(z, z1, re); + let zgdif = zeta_fn(z2, z1, re); + let mut xs = [0.0_f64; 10]; + let mut ys = [0.0_f64; 10]; + let mut y2out = [0.0_f64; 10]; + for k in 0..mn { xs[k] = zeta_fn(zn2[k], z1, re) / zgdif; ys[k] = 1.0 / tn2[k]; } + let yd1 = -tgn2[0] / (t1 * t1) * zgdif; + let yd2 = -tgn2[1] / (t2 * t2) * zgdif * ((re + z2) / (re + z1)).powi(2); + spline(&xs[..mn], &ys[..mn], mn, yd1, yd2, &mut y2out[..mn]); + let x = zg / zgdif; + let y = splint(&xs[..mn], &ys[..mn], &y2out[..mn], mn, x); + *tz = 1.0 / y; + if xm != 0.0 { + let glb = gsurf / (1.0 + z1 / re).powi(2); + let gamm = xm * glb * zgdif / rgas; + let yi = splini(&xs[..mn], &ys[..mn], &y2out[..mn], mn, x); + let mut expl = gamm * yi; + if expl > 50.0 { expl = 50.0; } + densm_tmp = densm_tmp * (t1 / *tz) * (-expl).exp(); + } + if alt > zn3[0] { + if xm == 0.0 { return *tz; } else { return densm_tmp; } + } + let z = alt; + let mn = mn3; + let z1 = zn3[0]; + let z2 = zn3[mn - 1]; + let t1 = tn3[0]; + let t2 = tn3[mn - 1]; + let zg = zeta_fn(z, z1, re); + let zgdif = zeta_fn(z2, z1, re); + for k in 0..mn { xs[k] = zeta_fn(zn3[k], z1, re) / zgdif; ys[k] = 1.0 / tn3[k]; } + let yd1 = -tgn3[0] / (t1 * t1) * zgdif; + let yd2 = -tgn3[1] / (t2 * t2) * zgdif * ((re + z2) / (re + z1)).powi(2); + spline(&xs[..mn], &ys[..mn], mn, yd1, yd2, &mut y2out[..mn]); + let x = zg / zgdif; + let y = splint(&xs[..mn], &ys[..mn], &y2out[..mn], mn, x); + *tz = 1.0 / y; + if xm != 0.0 { + let glb = gsurf / (1.0 + z1 / re).powi(2); + let gamm = xm * glb * zgdif / rgas; + let yi = splini(&xs[..mn], &ys[..mn], &y2out[..mn], mn, x); + let mut expl = gamm * yi; + if expl > 50.0 { expl = 50.0; } + densm_tmp = densm_tmp * (t1 / *tz) * (-expl).exp(); + } + if xm == 0.0 { *tz } else { densm_tmp } } -/// OUTPUT VARIABLES: -/// d[0] - HE NUMBER DENSITY(CM-3) -/// d[1] - O NUMBER DENSITY(CM-3) -/// d[2] - N2 NUMBER DENSITY(CM-3) -/// d[3] - O2 NUMBER DENSITY(CM-3) -/// d[4] - AR NUMBER DENSITY(CM-3) -/// d[5] - TOTAL MASS DENSITY(GM/CM3) [includes d[8] in td7d] -/// d[6] - H NUMBER DENSITY(CM-3) -/// d[7] - N NUMBER DENSITY(CM-3) -/// d[8] - Anomalous oxygen NUMBER DENSITY(CM-3) -/// t[0] - EXOSPHERIC TEMPERATURE -/// t[1] - TEMPERATURE AT ALT -#[repr(C)] -#[allow(non_camel_case_types)] -struct nrlmsise_output { - d: [cty::c_double; 9], - t: [cty::c_double; 2], +fn densu(alt: f64, dlb: f64, tinf: f64, tlb: f64, xm: f64, alpha: f64, tz: &mut f64, zlb: f64, s2: f64, mn1: usize, zn1: &[f64], tn1: &mut [f64], tgn1: &mut [f64], gsurf: f64, re: f64) -> f64 { + let rgas = 831.4_f64; + let za = zn1[0]; + let z = if alt > za { alt } else { za }; + let zg2 = zeta_fn(z, zlb, re); + let tt = tinf - (tinf - tlb) * (-s2 * zg2).exp(); + let ta = tt; + *tz = tt; + let mut x = 0.0_f64; + let mut z1 = 0.0_f64; + let mut zgdif = 0.0_f64; + let mut xs = [0.0_f64; 5]; + let mut ys = [0.0_f64; 5]; + let mut y2out = [0.0_f64; 5]; + let mut mn = 0_usize; + if alt < za { + let dta = (tinf - ta) * s2 * ((re + zlb) / (re + za)).powi(2); + tgn1[0] = dta; + tn1[0] = ta; + let z_local = if alt > zn1[mn1 - 1] { alt } else { zn1[mn1 - 1] }; + mn = mn1; + z1 = zn1[0]; + let z2 = zn1[mn - 1]; + let t1 = tn1[0]; + let t2 = tn1[mn - 1]; + let zg = zeta_fn(z_local, z1, re); + zgdif = zeta_fn(z2, z1, re); + for k in 0..mn { xs[k] = zeta_fn(zn1[k], z1, re) / zgdif; ys[k] = 1.0 / tn1[k]; } + let yd1 = -tgn1[0] / (t1 * t1) * zgdif; + let yd2 = -tgn1[1] / (t2 * t2) * zgdif * ((re + z2) / (re + z1)).powi(2); + spline(&xs[..mn], &ys[..mn], mn, yd1, yd2, &mut y2out[..mn]); + x = zg / zgdif; + let y = splint(&xs[..mn], &ys[..mn], &y2out[..mn], mn, x); + *tz = 1.0 / y; + } + if xm == 0.0 { return *tz; } + let glb = gsurf / (1.0 + zlb / re).powi(2); + let gamma = xm * glb / (s2 * rgas * tinf); + let mut expl = (-s2 * gamma * zg2).exp(); + if expl > 50.0 { expl = 50.0; } + if tt <= 0.0 { expl = 50.0; } + let densa = dlb * (tlb / tt).powf(1.0 + alpha + gamma) * expl; + let mut densu_temp = densa; + if alt >= za { return densu_temp; } + let glb = gsurf / (1.0 + z1 / re).powi(2); + let gamm = xm * glb * zgdif / rgas; + let yi = splini(&xs[..mn], &ys[..mn], &y2out[..mn], mn, x); + let mut expl = gamm * yi; + if expl > 50.0 { expl = 50.0; } + if *tz <= 0.0 { expl = 50.0; } + densu_temp = densu_temp * (tn1[0] / *tz).powf(1.0 + alpha) * (-expl).exp(); + densu_temp } -extern "C" { - fn gtd7d(input: *mut nrlmsise_input, flags: *mut nrlmsise_flags, output: *mut nrlmsise_output); +#[inline] fn g0_fn(a: f64, p: &[f64]) -> f64 { a - 4.0 + (p[25] - 1.0) * (a - 4.0 + ((-p[24].abs() * (a - 4.0)).exp() - 1.0) / p[24].abs()) } +#[inline] fn sumex(ex: f64) -> f64 { 1.0 + (1.0 - ex.powi(19)) / (1.0 - ex) * ex.sqrt() } +#[inline] fn sg0(ex: f64, p: &[f64], ap: &[f64]) -> f64 { (g0_fn(ap[1], p) + (g0_fn(ap[2], p) * ex + g0_fn(ap[3], p) * ex * ex + g0_fn(ap[4], p) * ex.powi(3) + (g0_fn(ap[5], p) * ex.powi(4) + g0_fn(ap[6], p) * ex.powi(12)) * (1.0 - ex.powi(8)) / (1.0 - ex))) / sumex(ex) } - // Version below does not include atomic oxygen - //fn gtd7(input: *mut nrlmsise_input, flags: *mut nrlmsise_flags, output: *mut nrlmsise_output); +fn globe7(p: &[f64], input: &NrlmsiseInput, flags: &NrlmsiseFlags, state: &mut NrlmsiseState) -> f64 { + let sr = 7.2722E-5_f64; + let dgtr = 1.74533E-2_f64; + let dr = 1.72142E-2_f64; + let hr = 0.2618_f64; + let mut t = [0.0_f64; 15]; + let tloc = input.lst; + let c = (input.g_lat * dgtr).sin(); + let s = (input.g_lat * dgtr).cos(); + let c2 = c * c; let c4 = c2 * c2; let s2 = s * s; + state.plg[0][1] = c; + state.plg[0][2] = 0.5 * (3.0 * c2 - 1.0); + state.plg[0][3] = 0.5 * (5.0 * c * c2 - 3.0 * c); + state.plg[0][4] = (35.0 * c4 - 30.0 * c2 + 3.0) / 8.0; + state.plg[0][5] = (63.0 * c2 * c2 * c - 70.0 * c2 * c + 15.0 * c) / 8.0; + state.plg[0][6] = (11.0 * c * state.plg[0][5] - 5.0 * state.plg[0][4]) / 6.0; + state.plg[1][1] = s; + state.plg[1][2] = 3.0 * c * s; + state.plg[1][3] = 1.5 * (5.0 * c2 - 1.0) * s; + state.plg[1][4] = 2.5 * (7.0 * c2 * c - 3.0 * c) * s; + state.plg[1][5] = 1.875 * (21.0 * c4 - 14.0 * c2 + 1.0) * s; + state.plg[1][6] = (11.0 * c * state.plg[1][5] - 6.0 * state.plg[1][4]) / 5.0; + state.plg[2][2] = 3.0 * s2; + state.plg[2][3] = 15.0 * s2 * c; + state.plg[2][4] = 7.5 * (7.0 * c2 - 1.0) * s2; + state.plg[2][5] = 3.0 * c * state.plg[2][4] - 2.0 * state.plg[2][3]; + state.plg[2][6] = (11.0 * c * state.plg[2][5] - 7.0 * state.plg[2][4]) / 4.0; + state.plg[2][7] = (13.0 * c * state.plg[2][6] - 8.0 * state.plg[2][5]) / 5.0; + state.plg[3][3] = 15.0 * s2 * s; + state.plg[3][4] = 105.0 * s2 * s * c; + state.plg[3][5] = (9.0 * c * state.plg[3][4] - 7.0 * state.plg[3][3]) / 2.0; + state.plg[3][6] = (11.0 * c * state.plg[3][5] - 8.0 * state.plg[3][4]) / 3.0; + if !((flags.sw[7] == 0.0 && flags.sw[8] == 0.0) && flags.sw[14] == 0.0) { + state.stloc = (hr * tloc).sin(); state.ctloc = (hr * tloc).cos(); + state.s2tloc = (2.0 * hr * tloc).sin(); state.c2tloc = (2.0 * hr * tloc).cos(); + state.s3tloc = (3.0 * hr * tloc).sin(); state.c3tloc = (3.0 * hr * tloc).cos(); + } + let cd32 = (dr * (input.doy as f64 - p[31])).cos(); + let cd18 = (2.0 * dr * (input.doy as f64 - p[17])).cos(); + let cd14 = (dr * (input.doy as f64 - p[13])).cos(); + let cd39 = (2.0 * dr * (input.doy as f64 - p[38])).cos(); + let df = input.f107 - input.f107a; + state.dfa = input.f107a - 150.0; + t[0] = p[19] * df * (1.0 + p[59] * state.dfa) + p[20] * df * df + p[21] * state.dfa + p[29] * state.dfa * state.dfa; + let f1 = 1.0 + (p[47] * state.dfa + p[19] * df + p[20] * df * df) * flags.swc[1]; + let f2 = 1.0 + (p[49] * state.dfa + p[19] * df + p[20] * df * df) * flags.swc[1]; + t[1] = (p[1] * state.plg[0][2] + p[2] * state.plg[0][4] + p[22] * state.plg[0][6]) + p[14] * state.plg[0][2] * state.dfa * flags.swc[1] + p[26] * state.plg[0][1]; + t[2] = p[18] * cd32; + t[3] = (p[15] + p[16] * state.plg[0][2]) * cd18; + t[4] = f1 * (p[9] * state.plg[0][1] + p[10] * state.plg[0][3]) * cd14; + t[5] = p[37] * state.plg[0][1] * cd39; + if flags.sw[7] != 0.0 { + let t71 = p[11] * state.plg[1][2] * cd14 * flags.swc[5]; + let t72 = p[12] * state.plg[1][2] * cd14 * flags.swc[5]; + t[6] = f2 * ((p[3] * state.plg[1][1] + p[4] * state.plg[1][3] + p[27] * state.plg[1][5] + t71) * state.ctloc + (p[6] * state.plg[1][1] + p[7] * state.plg[1][3] + p[28] * state.plg[1][5] + t72) * state.stloc); + } + if flags.sw[8] != 0.0 { + let t81 = (p[23] * state.plg[2][3] + p[35] * state.plg[2][5]) * cd14 * flags.swc[5]; + let t82 = (p[33] * state.plg[2][3] + p[36] * state.plg[2][5]) * cd14 * flags.swc[5]; + t[7] = f2 * ((p[5] * state.plg[2][2] + p[41] * state.plg[2][4] + t81) * state.c2tloc + (p[8] * state.plg[2][2] + p[42] * state.plg[2][4] + t82) * state.s2tloc); + } + if flags.sw[14] != 0.0 { + t[13] = f2 * ((p[39] * state.plg[3][3] + (p[93] * state.plg[3][4] + p[46] * state.plg[3][6]) * cd14 * flags.swc[5]) * state.s3tloc + (p[40] * state.plg[3][3] + (p[94] * state.plg[3][4] + p[48] * state.plg[3][6]) * cd14 * flags.swc[5]) * state.c3tloc); + } + if flags.sw[9] as i32 == -1 { + if let Some(ref ap_a) = input.ap_a { + if p[51] != 0.0 { + let exp1_arg = -10800.0 * (p[51] * p[51]).sqrt() / (1.0 + p[138] * (45.0 - (input.g_lat * input.g_lat).sqrt())); + let mut exp1 = exp1_arg.exp(); + if exp1 > 0.99999 { exp1 = 0.99999; } + state.apt[0] = sg0(exp1, p, ap_a); + if flags.sw[9] != 0.0 { + t[8] = state.apt[0] * (p[50] + p[96] * state.plg[0][2] + p[54] * state.plg[0][4] + (p[125] * state.plg[0][1] + p[126] * state.plg[0][3] + p[127] * state.plg[0][5]) * cd14 * flags.swc[5] + (p[128] * state.plg[1][1] + p[129] * state.plg[1][3] + p[130] * state.plg[1][5]) * flags.swc[7] * (hr * (tloc - p[131])).cos()); + } + } + } + } else { + let apd = input.ap - 4.0; + let mut p44 = p[43]; + let p45 = p[44]; + if p44 < 0.0 { p44 = 1.0E-5; } + state.apdf = apd + (p45 - 1.0) * (apd + ((-p44 * apd).exp() - 1.0) / p44); + if flags.sw[9] != 0.0 { + t[8] = state.apdf * (p[32] + p[45] * state.plg[0][2] + p[34] * state.plg[0][4] + (p[100] * state.plg[0][1] + p[101] * state.plg[0][3] + p[102] * state.plg[0][5]) * cd14 * flags.swc[5] + (p[121] * state.plg[1][1] + p[122] * state.plg[1][3] + p[123] * state.plg[1][5]) * flags.swc[7] * (hr * (tloc - p[124])).cos()); + } + } + if flags.sw[10] != 0.0 && input.g_long > -1000.0 { + if flags.sw[11] != 0.0 { + t[10] = (1.0 + p[80] * state.dfa * flags.swc[1]) * ((p[64] * state.plg[1][2] + p[65] * state.plg[1][4] + p[66] * state.plg[1][6] + p[103] * state.plg[1][1] + p[104] * state.plg[1][3] + p[105] * state.plg[1][5] + flags.swc[5] * (p[109] * state.plg[1][1] + p[110] * state.plg[1][3] + p[111] * state.plg[1][5]) * cd14) * (dgtr * input.g_long).cos() + (p[90] * state.plg[1][2] + p[91] * state.plg[1][4] + p[92] * state.plg[1][6] + p[106] * state.plg[1][1] + p[107] * state.plg[1][3] + p[108] * state.plg[1][5] + flags.swc[5] * (p[112] * state.plg[1][1] + p[113] * state.plg[1][3] + p[114] * state.plg[1][5]) * cd14) * (dgtr * input.g_long).sin()); + } + if flags.sw[12] != 0.0 { + t[11] = (1.0 + p[95] * state.plg[0][1]) * (1.0 + p[81] * state.dfa * flags.swc[1]) * (1.0 + p[119] * state.plg[0][1] * flags.swc[5] * cd14) * ((p[68] * state.plg[0][1] + p[69] * state.plg[0][3] + p[70] * state.plg[0][5]) * (sr * (input.sec - p[71])).cos()); + t[11] += flags.swc[11] * (p[76] * state.plg[2][3] + p[77] * state.plg[2][5] + p[78] * state.plg[2][7]) * (sr * (input.sec - p[79]) + 2.0 * dgtr * input.g_long).cos() * (1.0 + p[137] * state.dfa * flags.swc[1]); + } + if flags.sw[13] != 0.0 { + if flags.sw[9] as i32 == -1 { + if let Some(ref _ap_a) = input.ap_a { + if p[51] != 0.0 { + t[12] = state.apt[0] * flags.swc[11] * (1.0 + p[132] * state.plg[0][1]) * ((p[52] * state.plg[1][2] + p[98] * state.plg[1][4] + p[67] * state.plg[1][6]) * (dgtr * (input.g_long - p[97])).cos()) + state.apt[0] * flags.swc[11] * flags.swc[5] * (p[133] * state.plg[1][1] + p[134] * state.plg[1][3] + p[135] * state.plg[1][5]) * cd14 * (dgtr * (input.g_long - p[136])).cos() + state.apt[0] * flags.swc[12] * (p[55] * state.plg[0][1] + p[56] * state.plg[0][3] + p[57] * state.plg[0][5]) * (sr * (input.sec - p[58])).cos(); + } + } + } else { + t[12] = state.apdf * flags.swc[11] * (1.0 + p[120] * state.plg[0][1]) * ((p[60] * state.plg[1][2] + p[61] * state.plg[1][4] + p[62] * state.plg[1][6]) * (dgtr * (input.g_long - p[63])).cos()) + state.apdf * flags.swc[11] * flags.swc[5] * (p[115] * state.plg[1][1] + p[116] * state.plg[1][3] + p[117] * state.plg[1][5]) * cd14 * (dgtr * (input.g_long - p[118])).cos() + state.apdf * flags.swc[12] * (p[83] * state.plg[0][1] + p[84] * state.plg[0][3] + p[85] * state.plg[0][5]) * (sr * (input.sec - p[75])).cos(); + } + } + } + let mut tinf = p[30]; + for i in 0..14 { tinf += flags.sw[i + 1].abs() * t[i]; } + tinf +} + +fn glob7s(p: &[f64], input: &NrlmsiseInput, flags: &NrlmsiseFlags, state: &NrlmsiseState) -> f64 { + let dr = 1.72142E-2_f64; + let dgtr = 1.74533E-2_f64; + let _hr = 0.2618_f64; + let mut t = [0.0_f64; 14]; + let cd32 = (dr * (input.doy as f64 - p[31])).cos(); + let cd18 = (2.0 * dr * (input.doy as f64 - p[17])).cos(); + let cd14 = (dr * (input.doy as f64 - p[13])).cos(); + let cd39 = (2.0 * dr * (input.doy as f64 - p[38])).cos(); + t[0] = p[21] * state.dfa; + t[1] = p[1] * state.plg[0][2] + p[2] * state.plg[0][4] + p[22] * state.plg[0][6] + p[26] * state.plg[0][1] + p[14] * state.plg[0][3] + p[59] * state.plg[0][5]; + t[2] = (p[18] + p[47] * state.plg[0][2] + p[29] * state.plg[0][4]) * cd32; + t[3] = (p[15] + p[16] * state.plg[0][2] + p[30] * state.plg[0][4]) * cd18; + t[4] = (p[9] * state.plg[0][1] + p[10] * state.plg[0][3] + p[20] * state.plg[0][5]) * cd14; + t[5] = p[37] * state.plg[0][1] * cd39; + if flags.sw[7] != 0.0 { + let t71 = p[11] * state.plg[1][2] * cd14 * flags.swc[5]; + let t72 = p[12] * state.plg[1][2] * cd14 * flags.swc[5]; + t[6] = (p[3] * state.plg[1][1] + p[4] * state.plg[1][3] + t71) * state.ctloc + (p[6] * state.plg[1][1] + p[7] * state.plg[1][3] + t72) * state.stloc; + } + if flags.sw[8] != 0.0 { + let t81 = (p[23] * state.plg[2][3] + p[35] * state.plg[2][5]) * cd14 * flags.swc[5]; + let t82 = (p[33] * state.plg[2][3] + p[36] * state.plg[2][5]) * cd14 * flags.swc[5]; + t[7] = (p[5] * state.plg[2][2] + p[41] * state.plg[2][4] + t81) * state.c2tloc + (p[8] * state.plg[2][2] + p[42] * state.plg[2][4] + t82) * state.s2tloc; + } + if flags.sw[14] != 0.0 { + t[13] = p[39] * state.plg[3][3] * state.s3tloc + p[40] * state.plg[3][3] * state.c3tloc; + } + if flags.sw[9] != 0.0 { + if flags.sw[9] as i32 == 1 { t[8] = state.apdf * (p[32] + p[45] * state.plg[0][2] * flags.swc[2]); } + if flags.sw[9] as i32 == -1 { t[8] = p[50] * state.apt[0] + p[96] * state.plg[0][2] * state.apt[0] * flags.swc[2]; } + } + if flags.sw[10] != 0.0 && flags.sw[11] != 0.0 && input.g_long > -1000.0 { + t[10] = (1.0 + state.plg[0][1] * (p[80] * flags.swc[5] * (dr * (input.doy as f64 - p[81])).cos() + p[85] * flags.swc[6] * (2.0 * dr * (input.doy as f64 - p[86])).cos()) + p[83] * flags.swc[3] * (dr * (input.doy as f64 - p[84])).cos() + p[87] * flags.swc[4] * (2.0 * dr * (input.doy as f64 - p[88])).cos()) * ((p[64] * state.plg[1][2] + p[65] * state.plg[1][4] + p[66] * state.plg[1][6] + p[74] * state.plg[1][1] + p[75] * state.plg[1][3] + p[76] * state.plg[1][5]) * (dgtr * input.g_long).cos() + (p[90] * state.plg[1][2] + p[91] * state.plg[1][4] + p[92] * state.plg[1][6] + p[77] * state.plg[1][1] + p[78] * state.plg[1][3] + p[79] * state.plg[1][5]) * (dgtr * input.g_long).sin()); + } + let mut tt = 0.0_f64; + for i in 0..14 { tt += flags.sw[i + 1].abs() * t[i]; } + tt +} +fn gts7(input: &mut NrlmsiseInput, flags: &NrlmsiseFlags, output: &mut NrlmsiseOutput, state: &mut NrlmsiseState) { + let dgtr = 1.74533E-2_f64; + let dr = 1.72142E-2_f64; + let alpha = [-0.38, 0.0, 0.0, 0.0, 0.17, 0.0, -0.38, 0.0, 0.0_f64]; + let altl = [200.0, 300.0, 160.0, 250.0, 240.0, 450.0, 320.0, 450.0_f64]; + let za = PDL[1][15]; + let zn1 = [za, 110.0, 100.0, 90.0, 72.5]; + let mn1 = 5_usize; + for j in 0..9 { output.d[j] = 0.0; } + let tinf = if input.alt > zn1[0] { PTM[0] * PT[0] * (1.0 + flags.sw[16] * globe7(&PT, input, flags, state)) } else { PTM[0] * PT[0] }; + output.t[0] = tinf; + let g0 = if input.alt > zn1[4] { PTM[3] * PS[0] * (1.0 + flags.sw[19] * globe7(&PS, input, flags, state)) } else { PTM[3] * PS[0] }; + let tlb = PTM[1] * (1.0 + flags.sw[17] * globe7(&PD[3], input, flags, state)) * PD[3][0]; + let s = g0 / (tinf - tlb); + if input.alt < 300.0 { + state.meso_tn1[1] = PTM[6] * PTL[0][0] / (1.0 - flags.sw[18] * glob7s(&PTL[0], input, flags, state)); + state.meso_tn1[2] = PTM[2] * PTL[1][0] / (1.0 - flags.sw[18] * glob7s(&PTL[1], input, flags, state)); + state.meso_tn1[3] = PTM[7] * PTL[2][0] / (1.0 - flags.sw[18] * glob7s(&PTL[2], input, flags, state)); + state.meso_tn1[4] = PTM[4] * PTL[3][0] / (1.0 - flags.sw[18] * flags.sw[20] * glob7s(&PTL[3], input, flags, state)); + state.meso_tgn1[1] = PTM[8] * PMA[8][0] * (1.0 + flags.sw[18] * flags.sw[20] * glob7s(&PMA[8], input, flags, state)) * state.meso_tn1[4] * state.meso_tn1[4] / ((PTM[4] * PTL[3][0]) * (PTM[4] * PTL[3][0])); + } else { + state.meso_tn1[1] = PTM[6] * PTL[0][0]; + state.meso_tn1[2] = PTM[2] * PTL[1][0]; + state.meso_tn1[3] = PTM[7] * PTL[2][0]; + state.meso_tn1[4] = PTM[4] * PTL[3][0]; + state.meso_tgn1[1] = PTM[8] * PMA[8][0] * state.meso_tn1[4] * state.meso_tn1[4] / ((PTM[4] * PTL[3][0]) * (PTM[4] * PTL[3][0])); + } + let g28 = flags.sw[21] * globe7(&PD[2], input, flags, state); + let zhf = PDL[1][24] * (1.0 + flags.sw[5] * PDL[0][24] * (dgtr * input.g_lat).sin() * (dr * (input.doy as f64 - PT[13])).cos()); + output.t[0] = tinf; + let xmm = PDM[2][4]; + let z = input.alt; + let db28 = PDM[2][0] * g28.exp() * PD[2][0]; + output.d[2] = densu(z, db28, tinf, tlb, 28.0, alpha[2], &mut output.t[1], PTM[5], s, mn1, &zn1, &mut state.meso_tn1, &mut state.meso_tgn1, state.gsurf, state.re); + state.dd = output.d[2]; + let zh28 = PDM[2][2] * zhf; + let zhm28 = PDM[2][3] * PDL[1][5]; + let xmd = 28.0 - xmm; + let b28 = densu(zh28, db28, tinf, tlb, xmd, alpha[2] - 1.0, &mut output.t[1], PTM[5], s, mn1, &zn1, &mut state.meso_tn1, &mut state.meso_tgn1, state.gsurf, state.re); + if flags.sw[15] != 0.0 && z <= altl[2] { + state.dm28 = densu(z, b28, tinf, tlb, xmm, alpha[2], &mut output.t[1], PTM[5], s, mn1, &zn1, &mut state.meso_tn1, &mut state.meso_tgn1, state.gsurf, state.re); + output.d[2] = dnet(output.d[2], state.dm28, zhm28, xmm, 28.0); + } + let g4 = flags.sw[21] * globe7(&PD[0], input, flags, state); + let db04 = PDM[0][0] * g4.exp() * PD[0][0]; + output.d[0] = densu(z, db04, tinf, tlb, 4.0, alpha[0], &mut output.t[1], PTM[5], s, mn1, &zn1, &mut state.meso_tn1, &mut state.meso_tgn1, state.gsurf, state.re); + state.dd = output.d[0]; + if flags.sw[15] != 0.0 && z < altl[0] { + let zh04 = PDM[0][2]; + let b04 = densu(zh04, db04, tinf, tlb, 4.0 - xmm, alpha[0] - 1.0, &mut output.t[1], PTM[5], s, mn1, &zn1, &mut state.meso_tn1, &mut state.meso_tgn1, state.gsurf, state.re); + state.dm04 = densu(z, b04, tinf, tlb, xmm, 0.0, &mut output.t[1], PTM[5], s, mn1, &zn1, &mut state.meso_tn1, &mut state.meso_tgn1, state.gsurf, state.re); + output.d[0] = dnet(output.d[0], state.dm04, zhm28, xmm, 4.0); + let rl = (b28 * PDM[0][1] / b04).ln(); + let zc04 = PDM[0][4] * PDL[1][0]; + let hc04 = PDM[0][5] * PDL[1][1]; + output.d[0] *= ccor(z, rl, hc04, zc04); + } + let g16 = flags.sw[21] * globe7(&PD[1], input, flags, state); + let db16 = PDM[1][0] * g16.exp() * PD[1][0]; + output.d[1] = densu(z, db16, tinf, tlb, 16.0, alpha[1], &mut output.t[1], PTM[5], s, mn1, &zn1, &mut state.meso_tn1, &mut state.meso_tgn1, state.gsurf, state.re); + state.dd = output.d[1]; + if flags.sw[15] != 0.0 && z <= altl[1] { + let zh16 = PDM[1][2]; + let b16 = densu(zh16, db16, tinf, tlb, 16.0 - xmm, alpha[1] - 1.0, &mut output.t[1], PTM[5], s, mn1, &zn1, &mut state.meso_tn1, &mut state.meso_tgn1, state.gsurf, state.re); + state.dm16 = densu(z, b16, tinf, tlb, xmm, 0.0, &mut output.t[1], PTM[5], s, mn1, &zn1, &mut state.meso_tn1, &mut state.meso_tgn1, state.gsurf, state.re); + output.d[1] = dnet(output.d[1], state.dm16, zhm28, xmm, 16.0); + let rl = PDM[1][1] * PDL[1][16] * (1.0 + flags.sw[1] * PDL[0][23] * (input.f107a - 150.0)); + let hc16 = PDM[1][5] * PDL[1][3]; + let zc16 = PDM[1][4] * PDL[1][2]; + let hc216 = PDM[1][5] * PDL[1][4]; + output.d[1] *= ccor2(z, rl, hc16, zc16, hc216); + let hcc16 = PDM[1][7] * PDL[1][13]; + let zcc16 = PDM[1][6] * PDL[1][12]; + let rc16 = PDM[1][3] * PDL[1][14]; + output.d[1] *= ccor(z, rc16, hcc16, zcc16); + } + let g32 = flags.sw[21] * globe7(&PD[4], input, flags, state); + let db32 = PDM[3][0] * g32.exp() * PD[4][0]; + output.d[3] = densu(z, db32, tinf, tlb, 32.0, alpha[3], &mut output.t[1], PTM[5], s, mn1, &zn1, &mut state.meso_tn1, &mut state.meso_tgn1, state.gsurf, state.re); + state.dd = output.d[3]; + if flags.sw[15] != 0.0 { + if z <= altl[3] { + let zh32 = PDM[3][2]; + let b32 = densu(zh32, db32, tinf, tlb, 32.0 - xmm, alpha[3] - 1.0, &mut output.t[1], PTM[5], s, mn1, &zn1, &mut state.meso_tn1, &mut state.meso_tgn1, state.gsurf, state.re); + state.dm32 = densu(z, b32, tinf, tlb, xmm, 0.0, &mut output.t[1], PTM[5], s, mn1, &zn1, &mut state.meso_tn1, &mut state.meso_tgn1, state.gsurf, state.re); + output.d[3] = dnet(output.d[3], state.dm32, zhm28, xmm, 32.0); + let rl = (b28 * PDM[3][1] / b32).ln(); + let hc32 = PDM[3][5] * PDL[1][7]; + let zc32 = PDM[3][4] * PDL[1][6]; + output.d[3] *= ccor(z, rl, hc32, zc32); + } + let hcc32 = PDM[3][7] * PDL[1][22]; + let hcc232 = PDM[3][7] * PDL[0][22]; + let zcc32 = PDM[3][6] * PDL[1][21]; + let rc32 = PDM[3][3] * PDL[1][23] * (1.0 + flags.sw[1] * PDL[0][23] * (input.f107a - 150.0)); + output.d[3] *= ccor2(z, rc32, hcc32, zcc32, hcc232); + } + let g40 = flags.sw[21] * globe7(&PD[5], input, flags, state); + let db40 = PDM[4][0] * g40.exp() * PD[5][0]; + output.d[4] = densu(z, db40, tinf, tlb, 40.0, alpha[4], &mut output.t[1], PTM[5], s, mn1, &zn1, &mut state.meso_tn1, &mut state.meso_tgn1, state.gsurf, state.re); + state.dd = output.d[4]; + if flags.sw[15] != 0.0 && z <= altl[4] { + let zh40 = PDM[4][2]; + let b40 = densu(zh40, db40, tinf, tlb, 40.0 - xmm, alpha[4] - 1.0, &mut output.t[1], PTM[5], s, mn1, &zn1, &mut state.meso_tn1, &mut state.meso_tgn1, state.gsurf, state.re); + state.dm40 = densu(z, b40, tinf, tlb, xmm, 0.0, &mut output.t[1], PTM[5], s, mn1, &zn1, &mut state.meso_tn1, &mut state.meso_tgn1, state.gsurf, state.re); + output.d[4] = dnet(output.d[4], state.dm40, zhm28, xmm, 40.0); + let rl = (b28 * PDM[4][1] / b40).ln(); + let hc40 = PDM[4][5] * PDL[1][9]; + let zc40 = PDM[4][4] * PDL[1][8]; + output.d[4] *= ccor(z, rl, hc40, zc40); + } + let g1 = flags.sw[21] * globe7(&PD[6], input, flags, state); + let db01 = PDM[5][0] * g1.exp() * PD[6][0]; + output.d[6] = densu(z, db01, tinf, tlb, 1.0, alpha[6], &mut output.t[1], PTM[5], s, mn1, &zn1, &mut state.meso_tn1, &mut state.meso_tgn1, state.gsurf, state.re); + state.dd = output.d[6]; + if flags.sw[15] != 0.0 && z <= altl[6] { + let zh01 = PDM[5][2]; + let b01 = densu(zh01, db01, tinf, tlb, 1.0 - xmm, alpha[6] - 1.0, &mut output.t[1], PTM[5], s, mn1, &zn1, &mut state.meso_tn1, &mut state.meso_tgn1, state.gsurf, state.re); + state.dm01 = densu(z, b01, tinf, tlb, xmm, 0.0, &mut output.t[1], PTM[5], s, mn1, &zn1, &mut state.meso_tn1, &mut state.meso_tgn1, state.gsurf, state.re); + output.d[6] = dnet(output.d[6], state.dm01, zhm28, xmm, 1.0); + let rl = (b28 * PDM[5][1] * (PDL[1][17] * PDL[1][17]).sqrt() / b01).ln(); + let hc01 = PDM[5][5] * PDL[1][11]; + let zc01 = PDM[5][4] * PDL[1][10]; + output.d[6] *= ccor(z, rl, hc01, zc01); + let hcc01 = PDM[5][7] * PDL[1][19]; + let zcc01 = PDM[5][6] * PDL[1][18]; + let rc01 = PDM[5][3] * PDL[1][20]; + output.d[6] *= ccor(z, rc01, hcc01, zcc01); + } + let g14 = flags.sw[21] * globe7(&PD[7], input, flags, state); + let db14 = PDM[6][0] * g14.exp() * PD[7][0]; + output.d[7] = densu(z, db14, tinf, tlb, 14.0, alpha[7], &mut output.t[1], PTM[5], s, mn1, &zn1, &mut state.meso_tn1, &mut state.meso_tgn1, state.gsurf, state.re); + state.dd = output.d[7]; + if flags.sw[15] != 0.0 && z <= altl[7] { + let zh14 = PDM[6][2]; + let b14 = densu(zh14, db14, tinf, tlb, 14.0 - xmm, alpha[7] - 1.0, &mut output.t[1], PTM[5], s, mn1, &zn1, &mut state.meso_tn1, &mut state.meso_tgn1, state.gsurf, state.re); + state.dm14 = densu(z, b14, tinf, tlb, xmm, 0.0, &mut output.t[1], PTM[5], s, mn1, &zn1, &mut state.meso_tn1, &mut state.meso_tgn1, state.gsurf, state.re); + output.d[7] = dnet(output.d[7], state.dm14, zhm28, xmm, 14.0); + let rl = (b28 * PDM[6][1] * (PDL[0][2] * PDL[0][2]).sqrt() / b14).ln(); + let hc14 = PDM[6][5] * PDL[0][1]; + let zc14 = PDM[6][4] * PDL[0][0]; + output.d[7] *= ccor(z, rl, hc14, zc14); + let hcc14 = PDM[6][7] * PDL[0][4]; + let zcc14 = PDM[6][6] * PDL[0][3]; + let rc14 = PDM[6][3] * PDL[0][5]; + output.d[7] *= ccor(z, rc14, hcc14, zcc14); + } + let g16h = flags.sw[21] * globe7(&PD[8], input, flags, state); + let db16h = PDM[7][0] * g16h.exp() * PD[8][0]; + let tho = PDM[7][9] * PDL[0][6]; + let dd_anom = densu(z, db16h, tho, tho, 16.0, alpha[8], &mut output.t[1], PTM[5], s, mn1, &zn1, &mut state.meso_tn1, &mut state.meso_tgn1, state.gsurf, state.re); + let zsht = PDM[7][5]; + let zmho = PDM[7][4]; + let zsho = scalh(zmho, 16.0, tho, state.gsurf, state.re); + output.d[8] = dd_anom * (-zsht / zsho * ((-(z - zmho) / zsht).exp() - 1.0)).exp(); + output.d[5] = 1.66E-24 * (4.0 * output.d[0] + 16.0 * output.d[1] + 28.0 * output.d[2] + 32.0 * output.d[3] + 40.0 * output.d[4] + output.d[6] + 14.0 * output.d[7]); + let z_abs = (input.alt * input.alt).sqrt(); + let _ddum = densu(z_abs, 1.0, tinf, tlb, 0.0, 0.0, &mut output.t[1], PTM[5], s, mn1, &zn1, &mut state.meso_tn1, &mut state.meso_tgn1, state.gsurf, state.re); + if flags.sw[0] != 0.0 { for i in 0..9 { output.d[i] *= 1.0E6; } output.d[5] /= 1000.0; } +} + +fn gtd7(input: &mut NrlmsiseInput, flags: &mut NrlmsiseFlags, output: &mut NrlmsiseOutput, state: &mut NrlmsiseState) { + let mn3 = 5_usize; let zn3 = [32.5, 20.0, 15.0, 10.0, 0.0_f64]; + let mn2 = 4_usize; let zn2 = [72.5, 55.0, 45.0, 32.5_f64]; + let zmix = 62.5_f64; + tselec(flags); + let xlat = if flags.sw[2] == 0.0 { 45.0 } else { input.g_lat }; + glatf(xlat, &mut state.gsurf, &mut state.re); + let xmm = PDM[2][4]; + let altt = if input.alt > zn2[0] { input.alt } else { zn2[0] }; + let tmp = input.alt; + input.alt = altt; + let mut soutput = NrlmsiseOutput { d: [0.0; 9], t: [0.0; 2] }; + gts7(input, flags, &mut soutput, state); + input.alt = tmp; + let dm28m = if flags.sw[0] != 0.0 { state.dm28 * 1.0E6 } else { state.dm28 }; + output.t[0] = soutput.t[0]; output.t[1] = soutput.t[1]; + if input.alt >= zn2[0] { for i in 0..9 { output.d[i] = soutput.d[i]; } return; } + state.meso_tgn2[0] = state.meso_tgn1[1]; + state.meso_tn2[0] = state.meso_tn1[4]; + state.meso_tn2[1] = PMA[0][0] * PAVGM[0] / (1.0 - flags.sw[20] * glob7s(&PMA[0], input, flags, state)); + state.meso_tn2[2] = PMA[1][0] * PAVGM[1] / (1.0 - flags.sw[20] * glob7s(&PMA[1], input, flags, state)); + state.meso_tn2[3] = PMA[2][0] * PAVGM[2] / (1.0 - flags.sw[20] * flags.sw[22] * glob7s(&PMA[2], input, flags, state)); + state.meso_tgn2[1] = PAVGM[8] * PMA[9][0] * (1.0 + flags.sw[20] * flags.sw[22] * glob7s(&PMA[9], input, flags, state)) * state.meso_tn2[3] * state.meso_tn2[3] / ((PMA[2][0] * PAVGM[2]) * (PMA[2][0] * PAVGM[2])); + state.meso_tn3[0] = state.meso_tn2[3]; + if input.alt <= zn3[0] { + state.meso_tgn3[0] = state.meso_tgn2[1]; + state.meso_tn3[1] = PMA[3][0] * PAVGM[3] / (1.0 - flags.sw[22] * glob7s(&PMA[3], input, flags, state)); + state.meso_tn3[2] = PMA[4][0] * PAVGM[4] / (1.0 - flags.sw[22] * glob7s(&PMA[4], input, flags, state)); + state.meso_tn3[3] = PMA[5][0] * PAVGM[5] / (1.0 - flags.sw[22] * glob7s(&PMA[5], input, flags, state)); + state.meso_tn3[4] = PMA[6][0] * PAVGM[6] / (1.0 - flags.sw[22] * glob7s(&PMA[6], input, flags, state)); + state.meso_tgn3[1] = PMA[7][0] * PAVGM[7] * (1.0 + flags.sw[22] * glob7s(&PMA[7], input, flags, state)) * state.meso_tn3[4] * state.meso_tn3[4] / ((PMA[6][0] * PAVGM[6]) * (PMA[6][0] * PAVGM[6])); + } + let dmc = if input.alt > zmix { 1.0 - (zn2[0] - input.alt) / (zn2[0] - zmix) } else { 0.0 }; + let dz28 = soutput.d[2]; + let mut tz = 0.0_f64; + let dmr = soutput.d[2] / dm28m - 1.0; + output.d[2] = densm(input.alt, dm28m, xmm, &mut tz, mn3, &zn3, &state.meso_tn3, &state.meso_tgn3, mn2, &zn2, &state.meso_tn2, &state.meso_tgn2, state.gsurf, state.re); + output.d[2] *= 1.0 + dmr * dmc; + let dmr = soutput.d[0] / (dz28 * PDM[0][1]) - 1.0; + output.d[0] = output.d[2] * PDM[0][1] * (1.0 + dmr * dmc); + output.d[1] = 0.0; output.d[8] = 0.0; + let dmr = soutput.d[3] / (dz28 * PDM[3][1]) - 1.0; + output.d[3] = output.d[2] * PDM[3][1] * (1.0 + dmr * dmc); + let dmr = soutput.d[4] / (dz28 * PDM[4][1]) - 1.0; + output.d[4] = output.d[2] * PDM[4][1] * (1.0 + dmr * dmc); + output.d[6] = 0.0; output.d[7] = 0.0; + output.d[5] = 1.66E-24 * (4.0 * output.d[0] + 16.0 * output.d[1] + 28.0 * output.d[2] + 32.0 * output.d[3] + 40.0 * output.d[4] + output.d[6] + 14.0 * output.d[7]); + if flags.sw[0] != 0.0 { output.d[5] /= 1000.0; } + let _dd = densm(input.alt, 1.0, 0.0, &mut tz, mn3, &zn3, &state.meso_tn3, &state.meso_tgn3, mn2, &zn2, &state.meso_tn2, &state.meso_tgn2, state.gsurf, state.re); + output.t[1] = tz; +} + +fn gtd7d(input: &mut NrlmsiseInput, flags: &mut NrlmsiseFlags, output: &mut NrlmsiseOutput, state: &mut NrlmsiseState) { + gtd7(input, flags, output, state); + output.d[5] = 1.66E-24 * (4.0 * output.d[0] + 16.0 * output.d[1] + 28.0 * output.d[2] + 32.0 * output.d[3] + 40.0 * output.d[4] + output.d[6] + 14.0 * output.d[7] + 16.0 * output.d[8]); + if flags.sw[0] != 0.0 { output.d[5] /= 1000.0; } } -/// /// NRL MSISE-00 model for atmosphere density /// /// # Arguments @@ -104,22 +1104,19 @@ pub fn nrlmsise( time_option: Option<&impl TimeLike>, use_spaceweather: bool, ) -> (f64, f64) { - let lat: f64 = lat_option.unwrap_or(0.0); - let lon: f64 = lon_option.unwrap_or(0.0); + let lat = lat_option.unwrap_or(0.0); + let lon = lon_option.unwrap_or(0.0); let mut day_of_year: i32 = 1; let mut sec_of_day: f64 = 0.0; - let mut f107a: f64 = 150.0; let mut f107: f64 = 150.0; let mut ap: f64 = 4.0; - if let Some(time) = time_option { let time = time.as_instant(); let (year, _mon, _day, dhour, dmin, dsec) = time.as_datetime(); let fday: f64 = (time - Instant::from_date(year, 1, 1).unwrap()).as_days() + 1.0; day_of_year = fday.floor() as i32; sec_of_day = (dhour as f64).mul_add(3600.0, dmin as f64 * 60.0) + dsec; - if use_spaceweather { let sw_time = time - Duration::from_days(1.0); if let Ok(r) = spaceweather::get(&sw_time) { @@ -127,54 +1124,23 @@ pub fn nrlmsise( f107 = r.f10p7_adj; ap = r.ap_avg as f64; } else if let Some(predicted) = solar_cycle_forecast::get_predicted_f107(&time) { - // Use solar cycle forecast for future dates - // Monthly predictions serve as both daily and 81-day average f107 = predicted; f107a = predicted; - // Ap not available in forecast; use moderate default } } } - - let mut input: nrlmsise_input = nrlmsise_input { - year: 2022, - day: day_of_year, - sec: sec_of_day, - alt: alt_km, - g_lat: lat, - g_lon: lon, - lst: sec_of_day / 3600.0 + lon / 15.0, - f107a, - f107, - ap, - ap_a: std::ptr::null(), + let mut state = NrlmsiseState::new(); + let mut input = NrlmsiseInput { + year: 2022, doy: day_of_year, sec: sec_of_day, alt: alt_km, + g_lat: lat, g_long: lon, lst: sec_of_day / 3600.0 + lon / 15.0, + f107a, f107, ap, ap_a: None, }; - - let mut flags: nrlmsise_flags = nrlmsise_flags { - switches: [ - 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - ], - sw: [ - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - ], - swc: [ - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - ], - }; - - let mut output: nrlmsise_output = nrlmsise_output { - d: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - t: [0.0, 0.0], + let mut flags = NrlmsiseFlags { + switches: [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + sw: [0.0; 24], swc: [0.0; 24], }; - unsafe { - gtd7d( - std::ptr::addr_of_mut!(input), - std::ptr::addr_of_mut!(flags), - std::ptr::addr_of_mut!(output), - ); - } + let mut output = NrlmsiseOutput { d: [0.0; 9], t: [0.0; 2] }; + gtd7d(&mut input, &mut flags, &mut output, &mut state); (output.d[5] * 1.0e3, output.t[1]) } @@ -182,11 +1148,172 @@ pub fn nrlmsise( mod tests { use super::*; + fn call_model(alt: f64, lat: f64, lon: f64, doy: i32, sec: f64, f107: f64, f107a: f64, ap: f64) -> NrlmsiseOutput { + let mut state = NrlmsiseState::new(); + let mut input = NrlmsiseInput { + year: 2000, doy, sec, alt, g_lat: lat, g_long: lon, + lst: sec / 3600.0 + lon / 15.0, f107a, f107, ap, ap_a: None, + }; + let mut flags = NrlmsiseFlags { + switches: [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + sw: [0.0; 24], swc: [0.0; 24], + }; + let mut output = NrlmsiseOutput { d: [0.0; 9], t: [0.0; 2] }; + gtd7d(&mut input, &mut flags, &mut output, &mut state); + output + } + + fn sanity_check(output: &NrlmsiseOutput, alt: f64) { + assert!(output.d[5] > 0.0, "Total density must be positive at alt={alt}"); + assert!(output.t[1] > 100.0, "Temperature must be > 100K at alt={alt}"); + assert!(output.t[1] < 3000.0, "Temperature must be < 3000K at alt={alt}"); + } + + #[test] + fn test_nrlmsise_basic() { + let tm: Instant = Instant::from_date(2010, 1, 1).unwrap() + Duration::from_days(171.0 + 29000.0 / 86400.0); + let (density, temperature) = nrlmsise(400.0, Some(60.0), Some(-70.0), Some(&tm), true); + assert!(density > 0.0, "Density must be positive"); + assert!(temperature > 0.0, "Temperature must be positive"); + } + + #[test] + fn test_multiple_altitudes() { + let altitudes = [100.0, 200.0, 400.0, 600.0, 800.0]; + let mut prev_density = f64::MAX; + for &alt in &altitudes { + let out = call_model(alt, 60.0, -70.0, 172, 29000.0, 150.0, 150.0, 4.0); + sanity_check(&out, alt); + if alt > 100.0 { assert!(out.d[5] < prev_density, "Density should decrease with altitude: alt={alt}"); } + prev_density = out.d[5]; + } + } + #[test] - fn test_nrlmsise() { - let tm: Instant = Instant::from_date(2010, 1, 1).unwrap() - + Duration::from_days(171.0 + 29000.0 / 86400.0); - let (_density, _temperature) = - nrlmsise(400.0, Some(60.0), Some(-70.0), Some(&tm), true); + fn test_different_latitudes() { + for &lat in &[0.0, 45.0, 90.0] { + let out = call_model(400.0, lat, 0.0, 172, 29000.0, 150.0, 150.0, 4.0); + sanity_check(&out, 400.0); + } } + + #[test] + fn test_solar_activity_levels() { + let mut prev_density = 0.0_f64; + for &f107 in &[70.0, 150.0, 250.0] { + let out = call_model(400.0, 45.0, 0.0, 172, 29000.0, f107, f107, 4.0); + sanity_check(&out, 400.0); + if f107 > 70.0 { assert!(out.d[5] > prev_density, "Higher F10.7 should increase density at 400km: f107={f107}"); } + prev_density = out.d[5]; + } + } + + #[test] + fn test_no_spaceweather() { + let (density, temperature) = nrlmsise(400.0, Some(45.0), Some(0.0), None::<&Instant>, false); + assert!(density > 0.0); + assert!(temperature > 0.0); + } + + #[test] + fn test_low_altitude() { + let out = call_model(10.0, 45.0, 0.0, 172, 29000.0, 150.0, 150.0, 4.0); + sanity_check(&out, 10.0); + let out_high = call_model(400.0, 45.0, 0.0, 172, 29000.0, 150.0, 150.0, 4.0); + assert!(out.d[5] > out_high.d[5] * 1e6, "10km density should be >> 400km density"); + } + + #[test] + fn test_exospheric_temperature() { + let out_low = call_model(500.0, 0.0, 0.0, 172, 43200.0, 70.0, 70.0, 4.0); + let out_high = call_model(500.0, 0.0, 0.0, 172, 43200.0, 250.0, 250.0, 4.0); + assert!(out_high.t[0] > out_low.t[0], "Exospheric temp should increase with F10.7"); + } + + #[test] + fn test_deterministic() { + let out1 = call_model(400.0, 45.0, -70.0, 172, 29000.0, 150.0, 150.0, 4.0); + let out2 = call_model(400.0, 45.0, -70.0, 172, 29000.0, 150.0, 150.0, 4.0); + for i in 0..9 { assert_eq!(out1.d[i], out2.d[i], "Density d[{i}] not deterministic"); } + assert_eq!(out1.t[0], out2.t[0]); + assert_eq!(out1.t[1], out2.t[1]); + } + + #[test] + fn test_reference_values_400km() { + let out = call_model(400.0, 60.0, -70.0, 172, 29000.0, 150.0, 150.0, 4.0); + assert!(out.d[5] > 1.0e-16 && out.d[5] < 1.0e-10, "Total density at 400km out of expected range: {}", out.d[5]); + assert!(out.t[1] > 700.0 && out.t[1] < 1500.0, "Temperature at 400km out of expected range: {}", out.t[1]); + } + + #[test] + fn test_species_densities_positive() { + let out = call_model(400.0, 45.0, 0.0, 172, 29000.0, 150.0, 150.0, 4.0); + assert!(out.d[0] > 0.0, "He density should be positive"); + assert!(out.d[1] > 0.0, "O density should be positive"); + assert!(out.d[2] > 0.0, "N2 density should be positive"); + assert!(out.d[3] > 0.0, "O2 density should be positive"); + } + + #[test] + fn test_altitude_sweep_monotonic() { + let alts = [100.0, 150.0, 200.0, 300.0, 400.0, 500.0, 600.0, 800.0]; + let mut prev = f64::MAX; + for &alt in &alts { + let out = call_model(alt, 0.0, 0.0, 172, 43200.0, 150.0, 150.0, 4.0); + sanity_check(&out, alt); + if alt >= 150.0 { assert!(out.d[5] < prev, "Density not decreasing at alt={alt}"); } + prev = out.d[5]; + } + } + + #[test] + fn test_against_c_reference() { + // Reference values generated from the original C NRLMSISE-00 implementation + // (Dominik Brodowski, release 20041227). 22 test cases covering altitudes + // 50-1000km, latitudes -45 to 90, F10.7 70-250, Ap 4-40, all times of day. + // Format: (doy, sec, alt_km, lat, lon, f107a, f107, ap, density_kg_m3, temp_K) + let ref_values: &[(i32, f64, f64, f64, f64, f64, f64, f64, f64, f64)] = &[ + (172, 29000.0, 100.0, 60.0, -70.0, 150.0, 150.0, 4.0, 3.566331077034030e-07, 213.4853662589559), + (172, 29000.0, 200.0, 60.0, -70.0, 150.0, 150.0, 4.0, 2.405112682924217e-10, 968.8274852527600), + (172, 29000.0, 300.0, 60.0, -70.0, 150.0, 150.0, 4.0, 1.753041634659097e-11, 1083.565143501857), + (172, 29000.0, 400.0, 60.0, -70.0, 150.0, 150.0, 4.0, 2.400187349450909e-12, 1098.248715501850), + (172, 29000.0, 500.0, 60.0, -70.0, 150.0, 150.0, 4.0, 4.547669148745933e-13, 1100.244572186091), + (172, 29000.0, 600.0, 60.0, -70.0, 150.0, 150.0, 4.0, 1.032110876054660e-13, 1100.531951866164), + (172, 29000.0, 800.0, 60.0, -70.0, 150.0, 150.0, 4.0, 8.209468510644924e-15, 1100.582691109543), + (172, 29000.0, 1000.0, 60.0, -70.0, 150.0, 150.0, 4.0, 1.574632267239295e-15, 1100.584084143845), + (172, 29000.0, 400.0, 0.0, 0.0, 150.0, 150.0, 4.0, 2.751878914860656e-12, 929.3974079388325), + (172, 29000.0, 400.0, 45.0, 0.0, 150.0, 150.0, 4.0, 3.395091151031206e-12, 1085.384248466074), + (172, 29000.0, 400.0, 90.0, 0.0, 150.0, 150.0, 4.0, 3.648579006145896e-12, 1186.441874023936), + (172, 29000.0, 400.0, -45.0, 0.0, 150.0, 150.0, 4.0, 2.207074681643701e-12, 845.0483323181795), + (172, 29000.0, 400.0, 60.0, -70.0, 70.0, 70.0, 4.0, 4.566723510827348e-13, 790.6239557119441), + (172, 29000.0, 400.0, 60.0, -70.0, 250.0, 250.0, 4.0, 6.957966569624449e-12, 1349.462586622119), + (172, 29000.0, 400.0, 60.0, -70.0, 150.0, 150.0, 40.0, 3.258252132785382e-12, 1289.234960843807), + (172, 0.0, 400.0, 60.0, -70.0, 150.0, 150.0, 4.0, 4.085403374565813e-12, 1200.971191917643), + (172, 43200.0, 400.0, 60.0, -70.0, 150.0, 150.0, 4.0, 3.230763252933960e-12, 1165.060956208804), + (1, 29000.0, 400.0, 60.0, -70.0, 150.0, 150.0, 4.0, 2.105972433040141e-12, 843.1586610802336), + (91, 29000.0, 400.0, 60.0, -70.0, 150.0, 150.0, 4.0, 3.180576942159127e-12, 1030.982373989887), + (274, 29000.0, 400.0, 60.0, -70.0, 150.0, 150.0, 4.0, 2.990266751727769e-12, 988.6671231076829), + (172, 29000.0, 50.0, 60.0, -70.0, 150.0, 150.0, 4.0, 1.295541393193920e-03, 279.9138817220627), + (172, 29000.0, 80.0, 60.0, -70.0, 150.0, 150.0, 4.0, 2.583834857191450e-05, 168.3960368212220), + ]; + + for (i, &(doy, sec, alt, lat, lon, f107a, f107, ap, ref_density, ref_temp)) in ref_values.iter().enumerate() { + let out = call_model(alt, lat, lon, doy, sec, f107, f107a, ap); + let density = out.d[5] * 1.0e3; // CGS g/cm3 -> kg/m3 + let temp = out.t[1]; + let density_rel_err = ((density - ref_density) / ref_density).abs(); + let temp_rel_err = ((temp - ref_temp) / ref_temp).abs(); + assert!( + density_rel_err < 1.0e-10, + "Case {i} (alt={alt}): density mismatch: got {density:.10e}, expected {ref_density:.10e}, rel_err={density_rel_err:.2e}" + ); + assert!( + temp_rel_err < 1.0e-10, + "Case {i} (alt={alt}): temp mismatch: got {temp:.10}, expected {ref_temp:.10}, rel_err={temp_rel_err:.2e}" + ); + } + } + + } diff --git a/src/ode/adaptive_solvers/mod.rs b/src/ode/adaptive_solvers/mod.rs deleted file mode 100644 index 71e4a31..0000000 --- a/src/ode/adaptive_solvers/mod.rs +++ /dev/null @@ -1,28 +0,0 @@ -mod rkf45; - -mod rkts54; - -mod rkv65; -mod rkv65_table; - -mod rkv87; -mod rkv87_table; - -mod rkv98; - -mod rkv98_nointerp; -mod rkv98_nointerp_table; - -mod rkv98_efficient; -mod rkv98_efficient_table; - -pub use rkf45::RKF45; -pub use rkts54::RKTS54; -pub use rkv65::RKV65; -pub use rkv87::RKV87; -pub use rkv98::RKV98; -pub use rkv98_efficient::RKV98Efficient; -pub use rkv98_nointerp::RKV98NoInterp; - -// Re-export RKAdaptive trait for use by the adpative solvers -use super::rk_adaptive::RKAdaptive; diff --git a/src/ode/adaptive_solvers/rkf45.rs b/src/ode/adaptive_solvers/rkf45.rs deleted file mode 100644 index a8c53a6..0000000 --- a/src/ode/adaptive_solvers/rkf45.rs +++ /dev/null @@ -1,76 +0,0 @@ -use super::RKAdaptive; - -pub struct RKF45 {} -impl RKAdaptive<6, 1> for RKF45 { - const A: [[f64; 6]; 6] = [ - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.25, 0.0, 0.0, 0.0, 0.0, 0.0], - [3.0 / 32.0, 9.0 / 32.0, 0.0, 0.0, 0.0, 0.0], - [ - 1932.0 / 2197.0, - -7200.0 / 2197.0, - 7296.0 / 2197.0, - 0.0, - 0.0, - 0.0, - ], - [ - 439.0 / 216.0, - -8.0, - 3680.0 / 513.0, - -845.0 / 4104.0, - 0.0, - 0.0, - ], - [ - -8.0 / 27.0, - 2.0, - -3544.0 / 2565.0, - 1859.0 / 4104.0, - -11.0 / 40.0, - 0.0, - ], - ]; - - const BI: [[f64; 1]; 6] = [ - [16.0 / 135.0], - [0.0], - [6656.0 / 12825.0], - [28561.0 / 56430.0], - [-9.0 / 50.0], - [2.0 / 55.0], - ]; - - const B: [f64; 6] = [ - 16.0 / 135.0, - 0.0, - 6656.0 / 12825.0, - 28561.0 / 56430.0, - -9.0 / 50.0, - 2.0 / 55.0, - ]; - - const BERR: [f64; 6] = { - const BSTAR: [f64; 6] = [ - 25.0 / 216.0, - 0.0, - 1408.0 / 2565.0, - 2197.0 / 4104.0, - -0.2, - 0.0, - ]; - let mut berr = [0.0; 6]; - let mut ix: usize = 0; - while ix < 6 { - berr[ix] = BSTAR[ix] - Self::B[ix]; - ix += 1; - } - berr - }; - - const C: [f64; 6] = [0.0, 0.25, 3.0 / 8.0, 12.0 / 13.0, 1.0, 0.5]; - - const ORDER: usize = 4; - - const FSAL: bool = false; -} diff --git a/src/ode/adaptive_solvers/rkts54.rs b/src/ode/adaptive_solvers/rkts54.rs deleted file mode 100644 index 6dc1eb8..0000000 --- a/src/ode/adaptive_solvers/rkts54.rs +++ /dev/null @@ -1,119 +0,0 @@ -//! Tsitouras Order 5(4) Runge-Kutta integrator -//! -//! See: -//! -//! -//! Note: in paper, sign is reversed on Bhat[7] ... -//! should be -1.0/66.0, not 1.0/66.0 -//! -//! Note also: bhat values are actually error values -//! The nomenclature is confusing -//! - -use super::RKAdaptive; - -const A32: f64 = 0.335_480_655_492_357; -const A42: f64 = -6.359448489975075; -const A52: f64 = -11.74888356406283; -const A43: f64 = 4.362295432869581; -const A53: f64 = 7.495539342889836; -const A54: f64 = -0.09249506636175525; -const A62: f64 = -12.92096931784711; -const A63: f64 = 8.159367898576159; -const A64: f64 = -0.071_584_973_281_401; -const A65: f64 = -0.02826905039406838; - -const BI11: f64 = -1.0530884977290216; -const BI12: f64 = -1.329_989_018_975_141; -const BI13: f64 = -1.4364028541716351; -const BI14: f64 = 0.7139816917074209; - -const BI21: f64 = 0.1017; -const BI22: f64 = -2.1966568338249754; -const BI23: f64 = 1.294_985_250_737_463; - -const BI31: f64 = 2.490_627_285_651_253; -const BI32: f64 = -2.385_356_454_720_616_5; -const BI33: f64 = 1.578_034_682_080_924_8; - -const BI41: f64 = -16.548_102_889_244_902; -const BI42: f64 = -1.217_129_272_955_332_5; -const BI43: f64 = -0.616_204_060_378_000_9; - -const BI51: f64 = 47.379_521_962_819_28; -const BI52: f64 = -1.203_071_208_372_362_7; -const BI53: f64 = -0.658_047_292_653_547_3; - -const BI61: f64 = -34.870_657_861_496_61; -const BI62: f64 = -1.2; -const BI63: f64 = -2.0 / 3.0; - -const BI71: f64 = 2.5; -const BI72: f64 = -1.0; -const BI73: f64 = -0.6; - -pub struct RKTS54 {} - -impl RKTS54 {} - -impl RKAdaptive<7, 4> for RKTS54 { - const C: [f64; 7] = [0.0, 0.161, 0.327, 0.9, 0.9800255409045097, 1.0, 1.0]; - - const B: [f64; 7] = [ - 0.09646076681806523, - 0.01, - 0.4798896504144996, - 1.379008574103742, - -3.290069515436081, - 2.324710524099774, - 0.0, - ]; - - const BERR: [f64; 7] = [ - 0.001780011052226, - 0.000816434459657, - -0.007880878010262, - 0.144711007173263, - -0.582357165452555, - 0.458082105929187, - -1.0 / 66.0, - ]; - - const A: [[f64; 7]; 7] = [ - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [Self::C[1], 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [Self::C[2] - A32, A32, 0.0, 0.0, 0.0, 0.0, 0.0], - [Self::C[3] - A42 - A43, A42, A43, 0.0, 0.0, 0.0, 0.0], - [Self::C[4] - A52 - A53 - A54, A52, A53, A54, 0.0, 0.0, 0.0], - [ - Self::C[5] - A62 - A63 - A64 - A65, - A62, - A63, - A64, - A65, - 0.0, - 0.0, - ], - Self::B, - ]; - - const ORDER: usize = 5; - - const FSAL: bool = true; // Last stage at x+h equals first stage of next step - - // From expanding expressions in Tsitorous paper... - const BI: [[f64; 4]; 7] = [ - [ - BI11 * BI12 * BI14, - BI11 * (BI14 + BI12 * BI13), - BI11 * (BI13 + BI12), - BI11, - ], - [0.0, BI21 * BI23, BI21 * BI22, BI21], - [0.0, BI31 * BI33, BI31 * BI32, BI31], - [0.0, BI41 * BI42 * BI43, BI41 * (BI42 + BI43), BI41], - [0.0, BI51 * BI52 * BI53, BI51 * (BI52 + BI53), BI51], - [0.0, BI61 * BI62 * BI63, BI61 * (BI62 + BI63), BI61], - [0.0, BI71 * BI72 * BI73, BI71 * (BI72 + BI73), BI71], - ]; -} diff --git a/src/ode/adaptive_solvers/rkv65.rs b/src/ode/adaptive_solvers/rkv65.rs deleted file mode 100644 index e6bedc2..0000000 --- a/src/ode/adaptive_solvers/rkv65.rs +++ /dev/null @@ -1,33 +0,0 @@ -use super::RKAdaptive; - -// File below is auto-generated via python script that parses -// data available on web at: -// - -use super::rkv65_table; - -pub struct RKV65 {} - -impl RKAdaptive<10, 6> for RKV65 { - const ORDER: usize = 6; - - const FSAL: bool = false; - - const B: [f64; 10] = rkv65_table::B; - - const C: [f64; 10] = rkv65_table::C; - - const A: [[f64; 10]; 10] = rkv65_table::A; - - const BI: [[f64; 6]; 10] = rkv65_table::BI; - - const BERR: [f64; 10] = { - let mut berr = [0.0; 10]; - let mut ix: usize = 0; - while ix < 10 { - berr[ix] = Self::B[ix] - rkv65_table::BHAT[ix]; - ix += 1; - } - berr - }; -} diff --git a/src/ode/adaptive_solvers/rkv65_table.rs b/src/ode/adaptive_solvers/rkv65_table.rs deleted file mode 100644 index 9a47a22..0000000 --- a/src/ode/adaptive_solvers/rkv65_table.rs +++ /dev/null @@ -1,192 +0,0 @@ -//! Auto-generated via python script that parses -//! Runge-Kutta coefficients available on web at: -//! - -pub const BI: [[f64; 6]; 10] = [ - [ - 1.0, - -5.308169607103577, - 10.18168044895868, - -7.520036991611715, - 0.9340485368631161, - 0.746867191577065, - ], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [ - 0.0, - 6.272050253212501, - -16.02618147467746, - 12.844356324519618, - -1.1487945044767591, - -1.6831681430145498, - ], - [ - 0.0, - 6.876491702846304, - -24.635767260846333, - 33.21078648379717, - -17.49461528263644, - 2.4640414758066496, - ], - [ - 0.0, - -35.5444517105996, - 165.7016170190242, - -385.4635395491143, - 442.43241370157017, - -182.7206429912112, - ], - [ - 0.0, - 1918.6548566980114, - -9268.121508966042, - 20858.33702877255, - -22645.82767158481, - 8960.474176055992, - ], - [ - 0.0, - -1883.0698021327182, - 9101.025187200634, - -20473.188551959534, - 22209.765551256532, - -8782.1682509635, - ], - [ - 0.0, - 0.11902479635123643, - -0.12502696705039376, - 1.7799569193949991, - -4.660932123043763, - 2.886977374347921, - ], - [0.0, -8.0, 32.0, -40.0, 16.0, 0.0], -]; - -pub const C: [f64; 10] = [ - 0.0, - 0.06, - 0.09593333333333333, - 0.1439, - 0.4973, - 0.9725, - 0.9995, - 1.0, - 1.0, - 0.5, -]; -pub const B: [f64; 10] = [ - 0.03438957868357036, - 0.0, - 0.0, - 0.2582624555633503, - 0.4209371189673537, - 4.40539646966931, - -176.48311902429865, - 172.36413340141507, - 0.0, - 0.0, -]; -pub const BHAT: [f64; 10] = [ - 0.0490996764838249, - 0.0, - 0.0, - 0.22511122295165242, - 0.4694682253029562, - 0.8065792249988868, - 0.0, - -0.607119489177796, - 0.056861139440475696, - 0.0, -]; - -pub const A: [[f64; 10]; 10] = [ - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.06, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [ - 0.019239962962962962, - 0.07669337037037037, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [0.035975, 0.0, 0.107925, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [ - 1.3186834152331484, - 0.0, - -5.042058063628562, - 4.220674648395414, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - -41.872591664327516, - 0.0, - 159.4325621631375, - -122.11921356501003, - 5.531743066200054, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - -54.430156935316504, - 0.0, - 207.06725136501848, - -158.61081378459, - 6.991816585950242, - -0.018597231062203234, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - -54.66374178728198, - 0.0, - 207.95280625538936, - -159.2889574744995, - 7.018743740796944, - -0.018338785905045722, - -0.0005119484997882099, - 0.0, - 0.0, - 0.0, - ], - [ - 0.03438957868357036, - 0.0, - 0.0, - 0.2582624555633503, - 0.4209371189673537, - 4.40539646966931, - -176.48311902429865, - 172.36413340141507, - 0.0, - 0.0, - ], - [ - 0.016524159013572806, - 0.0, - 0.0, - 0.3053128187514179, - 0.2071200938201979, - -1.293879140655123, - 57.11988411588149, - -55.87979207510932, - 0.024830028297766014, - 0.0, - ], -]; diff --git a/src/ode/adaptive_solvers/rkv87.rs b/src/ode/adaptive_solvers/rkv87.rs deleted file mode 100644 index 7c61cef..0000000 --- a/src/ode/adaptive_solvers/rkv87.rs +++ /dev/null @@ -1,39 +0,0 @@ -use super::RKAdaptive; - -// Coefficients for the "efficient" Verner RK 8(7) pair -// with 8th-order interpolation -// -// Source: -// - -use super::rkv87_table as bt; - -// 13 stepping stages + 8 interpolation stages = 21 total -const N: usize = 21; -const NI: usize = 8; - -pub struct RKV87 {} - -impl RKAdaptive for RKV87 { - const ORDER: usize = 8; - - const FSAL: bool = false; - - const B: [f64; N] = bt::B; - - const C: [f64; N] = bt::C; - - const A: [[f64; N]; N] = bt::A; - - const BI: [[f64; NI]; N] = bt::BI; - - const BERR: [f64; N] = { - let mut berr = [0.0; N]; - let mut ix: usize = 0; - while ix < N { - berr[ix] = Self::B[ix] - bt::BHAT[ix]; - ix += 1; - } - berr - }; -} diff --git a/src/ode/adaptive_solvers/rkv87_table.rs b/src/ode/adaptive_solvers/rkv87_table.rs deleted file mode 100644 index 4788c6e..0000000 --- a/src/ode/adaptive_solvers/rkv87_table.rs +++ /dev/null @@ -1,563 +0,0 @@ -//! Coefficients for the "efficient" Verner RK 8(7) pair with 8th-order interpolation -//! -//! Source: - -pub const BI: [[f64; 8]; 21] = [ - [1.0, -10.039154650554519, 53.79210495862331, -165.0579057235472, 298.026456543461, -311.91254487079004, 174.60598526911716, -40.37066163211959], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 158.1976739121776, -1543.96141721949, 6241.39874782878, -13136.516156406109, 15106.948493169599, -8996.489626298231, 2170.776389952444], - [0.0, 110.78115200797782, -1081.1905145356177, 4370.666940459977, -9199.113723922197, 10578.949209629855, -6299.975594978841, 1520.1305005543413], - [0.0, -7011.442038211314, 68429.55220744078, -276623.5714822198, 582220.4545548494, -669551.5244611246, 398731.3087623333, -96210.47174510667], - [0.0, 11206.397569848148, -109371.04854950662, 442127.8393698155, -930563.7629864562, 1070145.1335855902, -637292.8058429047, 153773.3309185794], - [0.0, -14179.231640455684, 138385.00931963572, -559415.549024087, 1177423.7946992505, -1354033.3227908213, 806353.893882505, -194566.3328138133], - [0.0, 10247.761767921746, -100015.05326375231, 404306.62401434296, -850959.9711689702, 978601.0462088685, -582776.4729907749, 140619.0037156383], - [0.0, -105.49303976850968, 1029.5801395803103, -4162.034181876453, 8759.996193602336, -10073.965556886049, 5999.247741473951, -1447.5674285888924], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, -14.863613373267432, 145.76359364894867, -587.6557063401914, 1227.3721512545558, -1394.4931057405536, 816.8562950730669, -192.97961452255882], - [0.0, 14.349685752905462, -150.29493444816657, 629.481242570029, -1352.5182073090607, 1575.8969337088804, -946.7876580472948, 229.87293777270722], - [0.0, -102.54524701110401, 1074.0326612646807, -4498.377917100411, 9665.320624003281, -11261.62224831288, 6765.902468760784, -1642.7103416043497], - [0.0, -38.13206313286474, 399.3854658292329, -1672.7487204919717, 3594.1072548585666, -4187.7015568029265, 2515.9412806490636, -610.8516609091005], - [0.0, -66.38279583069588, 595.8297683881103, -2188.7370600929717, 4213.839795282853, -4484.035731929197, 2500.6482514253466, -571.1622272434449], - [0.0, -90.4188757317306, 931.9503884048154, -3962.898377713156, 8733.31742002555, -10445.908189887661, 6426.218942917599, -1592.261308015418], - [0.0, -59.738843630388715, 544.8870146891725, -2090.4303749263127, 4194.418982707227, -4603.369436819628, 2619.2014135592976, -604.9687555793671], - [0.0, -59.20053764683937, 571.7660156218088, -2308.9495644453605, 4881.2341106861395, -5660.118807771202, 3408.7066890374217, -833.4379054819676], -]; - -pub const C: [f64; 21] = [ - 0.0, - 0.05, - 0.1065625, - 0.15984375, - 0.39, - 0.465, - 0.155, - 0.943, - 0.901802041735857, - 0.909, - 0.94, - 1.0, - 1.0, - 1.0, - 0.3110177634953864, - 0.1725, - 0.7846, - 0.37, - 0.5, - 0.7, - 0.9, -]; - -pub const B: [f64; 21] = [ - 0.04427989419007951, - 0.0, - 0.0, - 0.0, - 0.0, - 0.3541049391724449, - 0.2479692154956438, - -15.694202038838084, - 25.084064965558564, - -31.738367786260277, - 22.938283273988784, - -0.2361324633071542, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, -]; - -pub const BHAT: [f64; 21] = [ - 0.044312615229089795, - 0.0, - 0.0, - 0.0, - 0.0, - 0.35460956423432266, - 0.2478480431366653, - 4.4481347324757845, - 19.846886366118735, - -23.58162337746562, - 0.0, - 0.0, - -0.36016794372897754, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, -]; - -pub const A: [[f64; 21]; 21] = [ - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [ - 0.05, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - -0.0069931640625, - 0.1135556640625, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - 0.0399609375, - 0.0, - 0.1198828125, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - 0.36139756280045754, - 0.0, - -1.3415240667004928, - 1.3701265039000352, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - 0.049047202797202795, - 0.0, - 0.0, - 0.23509720422144048, - 0.18085559298135673, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - 0.06169289044289044, - 0.0, - 0.0, - 0.11236568314640277, - -0.03885046071451367, - 0.01979188712522046, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - -1.767630240222327, - 0.0, - 0.0, - -62.5, - -6.061889377376669, - 5.6508231982227635, - 65.62169641937624, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - -1.1809450665549708, - 0.0, - 0.0, - -41.50473441114321, - -4.434438319103725, - 4.260408188586133, - 43.75364022446172, - 0.00787142548991231, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - -1.2814059994414884, - 0.0, - 0.0, - -45.047139960139866, - -4.731362069449577, - 4.514967016593808, - 47.44909557172985, - 0.010592282971116612, - -0.0057468422638446166, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - -1.7244701342624853, - 0.0, - 0.0, - -60.92349008483054, - -5.951518376222393, - 5.556523730698456, - 63.98301198033305, - 0.014642028250414961, - 0.06460408772358203, - -0.0793032316900888, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - -3.301622667747079, - 0.0, - 0.0, - -118.01127235975251, - -10.141422388456112, - 9.139311332232058, - 123.37594282840426, - 4.62324437887458, - -3.3832777380682018, - 4.527592100324618, - -5.828495485811623, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - -3.039515033766309, - 0.0, - 0.0, - -109.26086808941763, - -9.290642497400293, - 8.43050498176491, - 114.20100103783314, - -0.9637271342145479, - -5.0348840888021895, - 5.958130824002923, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - 0.04427989419007951, - 0.0, - 0.0, - 0.0, - 0.0, - 0.3541049391724449, - 0.2479692154956438, - -15.694202038838084, - 25.084064965558564, - -31.738367786260277, - 22.938283273988784, - -0.2361324633071542, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - 0.04620700646754963, - 0.0, - 0.0, - 0.0, - 0.0, - 0.045039041608424805, - 0.23368166977134244, - 37.83901368421068, - -15.949113289454246, - 23.028368351816102, - -44.85578507769412, - -0.06379858768647444, - 0.0, - -0.012595035543861663, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - 0.05037946855482041, - 0.0, - 0.0, - 0.0, - 0.0, - 0.041098361310460796, - 0.17180541533481958, - 4.614105319981519, - -1.7916678830853965, - 2.531658930485041, - -5.324977860205731, - -0.03065532595385635, - 0.0, - -0.005254479979429613, - -0.08399194644224793, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - 0.0408289713299708, - 0.0, - 0.0, - 0.0, - 0.0, - 0.4244479514247632, - 0.23260915312752345, - 2.677982520711806, - 0.7420826657338945, - 0.1460377847941461, - -3.579344509890565, - 0.11388443896001738, - 0.0, - 0.012677906510331901, - -0.07443436349946675, - 0.047827480797578516, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - 0.052126823936684136, - 0.0, - 0.0, - 0.0, - 0.0, - 0.053925083967447975, - 0.01660758097434641, - -4.45448575792678, - 6.835218278632146, - -8.711334822181994, - 6.491635839232917, - -0.07072551809844346, - 0.0, - -0.018540314919932164, - 0.023504021054353848, - 0.2344795103407822, - -0.08241072501152899, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - 0.05020102870355714, - 0.0, - 0.0, - 0.0, - 0.0, - 0.1552209034795498, - 0.1264268424089235, - -5.149206303539847, - 8.46834099903693, - -10.662130681081495, - 7.541833224959729, - -0.07436968113832143, - 0.0, - -0.020558876866183826, - 0.07753795264710298, - 0.10462592203525443, - -0.11792133064519794, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - 0.03737341446457826, - 0.0, - 0.0, - 0.0, - 0.0, - 0.35049307053383166, - 0.49226528193730257, - 8.553695439359313, - -10.353172990305913, - 13.83320427252915, - -12.280924330784618, - 0.17191515956565098, - 0.0, - 0.036415831143144964, - 0.02961920580288763, - -0.2651793938627067, - 0.09429503961738067, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - 0.039390583455282506, - 0.0, - 0.0, - 0.0, - 0.0, - 0.3558516141234424, - 0.419738222595261, - 0.8720449778071941, - 0.8989520834876595, - -0.6305806161059884, - -1.1218872205954835, - 0.04298219512400197, - 0.0, - 0.013325575668739157, - 0.018762270539641482, - -0.18594111329221055, - 0.17736142719246029, - 0.0, - 0.0, - 0.0, - 0.0, - ], -]; diff --git a/src/ode/adaptive_solvers/rkv98.rs b/src/ode/adaptive_solvers/rkv98.rs deleted file mode 100644 index 31f3aa6..0000000 --- a/src/ode/adaptive_solvers/rkv98.rs +++ /dev/null @@ -1,39 +0,0 @@ -use super::RKAdaptive; - -// Coefficients for the "efficient" Verner RK 9(8) pair -// with 9th-order interpolation -// -// Source: -// - -use super::rkv98_efficient_table as bt; - -// 17 stepping stages + 9 interpolation stages = 26 total -const N: usize = 26; -const NI: usize = 9; - -pub struct RKV98 {} - -impl RKAdaptive for RKV98 { - const ORDER: usize = 9; - - const FSAL: bool = false; - - const B: [f64; N] = bt::B; - - const C: [f64; N] = bt::C; - - const A: [[f64; N]; N] = bt::A; - - const BI: [[f64; NI]; N] = bt::BI; - - const BERR: [f64; N] = { - let mut berr = [0.0; N]; - let mut ix: usize = 0; - while ix < N { - berr[ix] = Self::B[ix] - bt::BHAT[ix]; - ix += 1; - } - berr - }; -} diff --git a/src/ode/adaptive_solvers/rkv98_efficient.rs b/src/ode/adaptive_solvers/rkv98_efficient.rs deleted file mode 100644 index ce6f63d..0000000 --- a/src/ode/adaptive_solvers/rkv98_efficient.rs +++ /dev/null @@ -1,3 +0,0 @@ -// RKV98Efficient is now identical to RKV98 (both use the efficient coefficients). -// Keep as a type alias for backwards compatibility. -pub type RKV98Efficient = super::rkv98::RKV98; diff --git a/src/ode/adaptive_solvers/rkv98_efficient_table.rs b/src/ode/adaptive_solvers/rkv98_efficient_table.rs deleted file mode 100644 index e5d34ec..0000000 --- a/src/ode/adaptive_solvers/rkv98_efficient_table.rs +++ /dev/null @@ -1,151 +0,0 @@ -//! Auto-generated from coefficients available at: -//! RKV98.IIa.Efficient.00000034399.240714.CoeffsOnlyFLOAT6040.txt - -#![allow(clippy::excessive_precision)] - -pub const C: [f64; 26] = [ - 0.0, - 0.3571e-1, - 0.9906028091267414072062290548990445721420e-1, - 0.1485904213690112110809343582348566858213, - 0.6134, - 0.2327359473605626756631680289030288192566, - 0.5538640526394373243368319710969711807434, - 0.6555, - 0.491625, - 0.6858e-1, - 0.253, - 0.6620641795412045944786226689660879617864, - 0.8309, - 0.8998, - 1.0, - 1.0, - 1.0, - 0.7375018139988810429214526568476200466933, - 0.749, - 0.65, - 0.487, - 0.97e-2, - 0.138, - 0.249, - 0.439, - 0.794, -]; - -pub const B: [f64; 26] = [ - 0.1500669014979724795766288712377040980022e-1, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - -1.055180992746381278594381685080184474710, - 0.2384947263782183112638140557774851953952, - 0.1288151774282991354622515887144081172371, - 0.2276623111046215614614917896665746508574, - 1.229532587437517443160321815173576421896, - 0.4624976662810383487397308275256128637703e-1, - 0.1386196319366293903628308621201843492468, - 0.3080010168319435405203560375162404389992e-1, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, -]; - -pub const BHAT: [f64; 26] = [ - 0.1897210532481101330735875918987801428423e-1, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 3.408110314549493848404398964228060776034, - 0.1260323883820920906560270507988839661845, - 0.1188375063451149770930378540709095182997, - 0.2491041997838687569190073177760326138188, - -3.269966219928978218713853055139116510335, - 0.3023798100228882907409723963501699715077, - 0.0, - 0.0, - 0.4652989552070924159305071272518165020637e-1, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, -]; - -#[rustfmt::skip] -pub const A: [[f64; 26]; 26] = [ - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.3571e-1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [-0.3833735636677017025757228807792426014327e-1, 0.1373976372794443109781951935678287173575, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.3714760534225280277023358955871417145532e-1, 0.0, 0.1114428160267584083107007686761425143660, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [2.674764429871505119194043347886848033362, 0.0, -9.982382134885293836441318015951354323506, 7.921017705013788717247274668064506290145, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.5242104050577351069841401193318449756747e-1, 0.0, 0.0, 0.1796911189175953081279466649977364298948, 0.6237879371938568368073519721078917943307e-3, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.1592492223647632083060991150129732726703, 0.0, 0.0, -0.4298429877241087508189185066909803751595, 0.6665266542726088051243445942394404211672e-1, 0.7578051525715219863372169033510342411158, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.7283333333333333333333333333333333333333e-1, 0.0, 0.0, 0.0, 0.0, 0.3359344590665103678713422141936031057620, 0.2467322076001562987953244524730635609046, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.7297558593750000000000000000000000000000e-1, 0.0, 0.0, 0.0, 0.0, 0.3348009729699333533129043555243262838993, 0.1184158239050666466870956444756737161007, -0.3456738281250000000000000000000000000000e-1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.4911213663452096382929799914888692571185e-1, 0.0, 0.0, 0.0, 0.0, 0.3983857361308652347066808423646954244674e-1, 0.1069675288939354812077139476183615284408, -0.2174259165458647598455650055976622663086e-1, -0.1055956474869564925231235304439517699685, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [-0.2707988818641280489175862864774930636734e-1, 0.0, 0.0, 0.0, 0.0, 0.3330000000000000000000000000000000000000e-1, -0.1645526070036057075627040519129748090051, 0.3428266306497389946775103799535438271025e-1, 0.1585264064439221046855989221358760453749, 0.2185234256811225083011127204294936872873, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.5584657769108862727526281006220448941548e-1, 0.0, 0.0, 0.0, 0.0, 0.9166533166672539087479545790553151183505e-1, 0.2392399655523627049569179599100121305027, 0.1023834712248414879740316672186561113729e-1, -0.2679331322859542442991625976085644567159e-2, 0.4235624181474284646680744174197106205439e-1, 0.2253970470166604185504274586005888014087, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [-0.4802510512725195893103945790000307739284, 0.0, 0.0, 0.0, 0.0, -6.359610162555930097843606206238737368598, -0.2762313898040841385163666588363199062208, -6.500796633979846747011407331804389650124, 0.5734765877040956875980719881692630407295, 1.347125994868138849716719646646049357523, 5.936840409706221306933249144503479736029, 6.590346245333924728433733996560685564589, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.3307533067671401079975082476263855100019, 0.0, 0.0, 0.0, 0.0, 5.956207776829962111958631943398154563177, -0.4868316400481527952968983900652017328476, 4.462055288206771191616083778225319083232, 0.7410258231442071778525406966672954745201, -0.7118192034575913119318346652024763899638, -5.454619594516665440649148499095762263275, -4.140803729244709693305868672684077643030, 0.2038319723190386517589855611303633981843, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [-0.5847111122998944389651719860007729221315, 0.0, 0.0, 0.0, 0.0, -12.41268417116267068926060969930886386670, 1.360245445660928146977003413153919520189, -22.42610531111868229005965344123311160860, -0.8828857055865458252669099251378431470909, 1.770155128538230466813900572142546329490, 12.15809651918533877218739262890752294243, 22.23037520407760703834156132941550000589, -0.6634483760201249006317401316871863808959, 0.4509623787258137198642272397482891274306, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [1.940575549810648717395482898478643635949, 0.0, 0.0, 0.0, 0.0, 21.97798408114556310164832153356570429956, 0.8230747326984728145128172720328075817426, 68.16441683626354817206843139989369741474, -3.117097463620266666801525529574905518624, -4.568841021822439620303730003755793384193, -18.74190987126264955904919353670543412590, -66.57711839637831878350036856645874492472, 1.098915553165441824029764532524025021446, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.1500669014979724795766288712377040980022e-1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.055180992746381278594381685080184474710, 0.2384947263782183112638140557774851953952, 0.1288151774282991354622515887144081172371, 0.2276623111046215614614917896665746508574, 1.229532587437517443160321815173576421896, 0.4624976662810383487397308275256128637703e-1, 0.1386196319366293903628308621201843492468, 0.3080010168319435405203560375162404389992e-1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.1571801061417788021580499748292804743973e-1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.4853403452657363080900487157345670578815, 0.2107787568904546684574678306538806242458, 0.1269802413053354152906551816812865612400, 0.2319687014513919157472314236852033704833, -0.3620214714069096774945761015915844860748, 0.5366106712036344076786687672191174826870e-1, -0.2806066613385549629370667652641771053798e-1, -0.2378121372710330174923994809623697280793e-1, 0.0, 0.2691804261928988988990035710208180655494e-1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.1569705832522204237038521016179271074328e-1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.4616075242202111936044636619654233606961, 0.2113946516698113234089715994366982070261, 0.1270330917167109232354671760560477406684, 0.2318540550298708286361100159494550129997, -0.3385266406688372942274816228586594758312, 0.5298251972194235542699858758068190007453e-1, -0.2750461365887187780272257875389040879148e-1, -0.2361906185395527177468217904834875318962e-1, 0.0, 0.2668458089504036186694391521145359328377e-1, 0.1139683460285541525554621429934611232038e-1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.1438964884291216402951708307100553583630e-1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.206901219123788519767229935392578222389, 0.2505628554639376294459724790398627595052, 0.1303332915702127271150218433265955255131, 0.2246717759263522068724895908997148014087, 1.308419325781946480763221457213122900404, 0.2589750180376235922034567811002287469857e-2, 0.8070743254562856892787425005539711000810e-2, -0.1267568255392829384757055330207124946149e-1, 0.0, 0.1129158072373321675992773077865249355325e-1, 0.3422056680709749787491027385482635813798e-1, -0.1149726368734142020610819623056729009783, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.1452348029801041972968851600082942318526e-1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.5214243102465818231380106106018875132310, 0.1866698844204603947548300448071067931901, 0.1299316354451273004594203171440036506875, 0.2262141085765719298250459612902177827497, 0.6104582639466710747030070634860296980845, 0.1418715607022412453299982942636029187870e-1, 0.1480061054412245756808618945399364705863e-1, -0.3711471609871774475268028540581604884370e-2, 0.0, 0.1393256979572558890317889514899316376009e-2, 1.114731020624073270253135239909350910479, -1.021208555757145859230933049243060207745, -0.2795650792912340738723193626472621878286, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.8711816186418632397551552134391273160887e-2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1532993247326560354009297246756198757646e-1, -0.1943250606288015181452523235039064995331e-2, 0.1072095950570478409673351209542485874475e-2, 0.2601233036074381124605301648955068308034e-3, -0.1817718521410219268803888649170562657549e-1, -0.9877668338996712912794853387601975290298e-3, -0.3332383192417756458522769371199819990865e-2, -0.8605833352714280922890371244764817761631e-3, 0.0, 0.1392810143886650671629507962655268250424e-2, -0.1617419987897630058856682050471490927435, 0.1485156561306044255987175208430691701911, 0.2289051095253062003982633508518682297336e-1, -0.1429777169141779172700863258972231247118e-2, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.2710092628714179537584868171073201523422e-1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.6923104986003885114772983986172611691902, -0.6725243132164494445073150422370697609664e-1, 0.8808440659269460280192719417821072306037e-1, 0.5982566312199630889836790781586188392095e-1, -0.8251290314815523858109567180972702723517, -0.4886453387508305661509501250020800230341e-1, -0.1682608371566511953625070499451460275789, -0.4443170503743608176862370639456174098455e-1, 0.0, 0.7037844639432780238885500484343585742713e-1, -8.818686397504859095347055843377871115177, 8.028821919603921347277176943010114087042, 1.306591406499157862229641342220359676579, -0.1624883307224014710941456378572112779619, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.1466442426857961230207869561545353695312e-1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1898399561399124450873282966092465494738, 0.3711530651907537053995301753121055816571e-1, 0.1316247981199510429999180335475037001729, 0.1466622928822685713088228930531811168096, -0.2377331162467838377764310139745632904067, -0.2493071232192949761907468241344412740688e-1, -0.9427777049221307692559326299260474943801e-1, -0.2726667627690941070300686079610559177653e-1, 0.0, 0.4097659404063362064133671142526181935526e-1, -5.538725321977494545432278985839620727708, 4.973442175247713118429917494679987165227, 0.8626294449189858956229369969895335684221, -0.2250213948217893084759073334350395278429, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.1191252689920919922350166270042464848683e-1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.6723137858130876080443132301001699876552, 0.1801284266825688933352476317750604591900, 0.1365689447449347163335592887842694005196, 0.2111038393798906615990851223053694372026, 0.7775854726244728792738759030556073621755, 0.2362117948505939633346199470372118351743e-1, 0.6391325607581230059121991748062989083367e-1, 0.1197654289246164466950805226357270574198e-1, 0.0, -0.2188622145087031801816344581960035242509e-1, 3.500218060218659348421859254969160434525, -3.195765244251373581009030415350620096609, -0.5994401048600148264885439684062323011428, 0.1137710737227729377873223163880721563872e-1, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.1184012014074604271397800922251586670558e-1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.6641263289768790103963252230448945748496, 0.1788914061873194018510863179806909040625, 0.1367562642287085016908037148859544845819, 0.2107758119730286112356534765669813731420, 0.7679184744806335059253231677825531412084, 0.2313496869580297355994450248598930620113e-1, 0.6230605705999706953753333719939087802061e-1, 0.1157099029193866324646273674695267419004e-1, 0.0, -0.2129441601042184344575923744216391881547e-1, 3.163166251075345445930896810245694869610, -2.819544872276817190650813744596266559171, -0.3978769046309758754403585402077121882020, 0.1304821777615737042415746721743137433164, 0.0, 0.0, 0.0, 0.0, 0.0], -]; - -#[rustfmt::skip] -pub const BI: [[f64; 9]; 26] = [ - [1.0, -60.67156499096275825192551363602396761243, 669.4173339890965298528834018742984105094, -3377.878946225199351520226690513332256243, 9286.468967391046692225695247237821541998, -14780.47713681102054116333940623447596217, 13604.99386328224720102647507806334531178, -6724.705443356114485986782323617396379907, 1381.867933411056511065177869712887072054], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, -0.5972167605668447080195299461102185885272, 47.93604836413167338885438264277611366496, -565.7170417002332153647406623941231321861, 2799.622707202858561151696501297251806365, -7032.161794919678731491945450324980521259, 9404.154583498766261417380396057323248088, -6374.661804344304055026346155757703309071, 1720.369337666279969354526136740485828512], - [0.0, 0.1349844707960069717220566866826298984923, -10.83462914594452681222359026508445675428, 127.8648231869934298266652232809372858609, -632.7779367772187332338007906200698487630, 1589.427325412255464804263544956685619289, -2125.551246305557207529871063524735793374, 1440.817483665754881321490009540733392622, -388.8423097807010970369815759993713435843], - [0.0, 0.7290747606753265739553888434183906298602e-1, -5.851972901033216313427390997286151384252, 69.06202973876299355875761438134035666890, -341.7744427162097135566927248434513873995, 858.4775271201373000458419246386670445384, -1148.047443579864082962792949259101806969, 778.2107496409214137189987901610599544952, -210.0205396013539280126185513768554408959], - [0.0, 0.1288534847345785561116203329301814360204, -10.34252098059179829195550043573389974374, 122.0572110662541635697477898873947855333, -604.0372032137003894354892742901494195352, 1517.235637581122757484801301501208740856, -2029.008844152904067945761282194453917620, 1375.375645376320937242441893999796663414, -371.1811168501315596184350570113265596896], - [0.0, 0.6958971720762387362164791425187492767466, -55.85670513574830649397484217620113261823, 659.1926340795728211557235694809221532847, -3262.215084140846354068888231363556701923, 8194.112807588290125367213373592291507372, -10958.03904467296551539422209426971705612, 7427.971576643475789982515741208313369971, -2004.632548946417281841423673799397312817], - [0.0, 0.2617668058132623028300450746186815539953e-1, -2.101090775094604504096600471528167656924, 24.79601256660032039131082591571668336326, -122.7106039105787684003939157931166303491, 308.2275402436813943146648421712827254341, -412.1946451000669582597347477727378421190, 279.4085780645544831469608316472195929468, -75.40571800304908908412026712154566848821], - [0.0, 0.7845665161262074618502665073713581595690e-1, -6.297381611696195354835186970953877598132, 74.31851847203782644803150125553425852543, -367.7877747056500197543410562506219227864, 923.8184599908639672968037927810710633244, -1235.428287659738944177317308989226766106, 837.4423716056154246604113713897017147785, -226.0057431111080504745753090041214216040], - [0.0, 0.1743239982411997083542666099618776565927e-1, -1.399224563421080747566112426047905797818, 16.51294188206731169216889160389405043672, -81.71931133065192111822155020120302944168, 205.2645942498128099152915051189419910813, -274.5016441798805281613128379066882052764, 186.0725630195001464648099116049710397144, -50.21655137556766366195319885111250443823], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, -0.3120237034739356102149090910458972991542e-1, 2.505323513858665425821148146925028556689, -29.63209944182268891094187050958569485080, 147.1735748991918299269418428659506735081, -371.6682626297095949736614911358789582154, 500.8032537625967383990239072447982187459, -342.9490465144388046578978125156518258548, 93.79845878067124835173576681254714784020], - [0.0, 1.005189184600557566129408870772027118073, -80.14524762849856177105235169139266835049, 904.2862871618540482735522275321299612604, -4159.377900797260916077366144135436208057, 9382.948440638739069789096069230867579132, -10865.07436232189535132287509765058944820, 6171.218731586134383883573674542623005216, -1354.861137823673230341057786698974248115], - [0.0, -1.018790304892149008339208622085912341214, 81.22968543433086703732306275594490877087, -916.5220998408498750879913972119183817869, 4215.658051870923057302442161815205855483, -9509.908233268860695396133182984225322485, 11012.08866136426918043466675789153630399, -6254.720911673139287616309544212696219769, 1373.193636418218902334341350568238768134], - [0.0, 0.1486756383162591888438322851466844348297e-1, -1.185413256701197518560901363041446866220, 13.37512808773948803650703726266929826638, -61.52057482048160294775557914196592133996, 138.7814224498311940264420678029949672145, -160.7032676951998445023342766109082536156, 91.27733347752088221300420564599480170832, -20.03949580654054522618693682425811381100], - [0.0, -0.2611963106691748849299256055037070183077e-2, 0.2082557524387797755360510166314825074305, -2.349769034664475310156364256215305171863, 10.80805662268303734097853101791700152378, -24.38139559637047359365680950022456660620, 28.23266885539013812966436802211627788468, -16.03578301196446470276182164155397173990, 3.520578375594150109245344597384118672243], - [0.0, 61.75136568508868110286254101938221664703, -750.1268910552884473493229930939554325311, 3898.685586244841930988256009725848784326, -10841.82272864745777154320543067614124209, 17321.57566462516617366130887449349951271, -15937.70492491813691151403407581204545506, 7853.047625862506649651583373646970497362, -1605.405697796720304997448299303558881367], - [0.0, -1.934298042224096808013043975920800289243, 147.5886431665603647558155476856964112175, -1181.453462187681932247747922750006635641, 4124.816268317402451398433861948686547907, -7708.555106592447263782269817638341609752, 8052.349419898650085369881466158633077027, -4443.023055054952034626751291729687203499, 1010.211590494692425940651200300940213030], - [0.0, 0.9126428307869666507744889095249555815836, -71.31710609019478576924959388866173624547, 695.4561446075446391059957659052646164863, -2620.882193436484363083577561241318740147, 4957.008141154350233563895859921266117993, -5038.298085544127560098020485475941711703, 2633.333935059078624368760947592332675986, -556.2134785809537547385794217224661779516], - [0.0, -0.4654439970747991651050782556914121007009, 37.09099810444154184865658933003385399191, -417.1382228339854991229014922810663023285, 1920.758799061188217073321625257641207277, -4409.960156149581696033720218281819632770, 5383.383503749768100112081288811655341483, -3353.518501772742141737984950593448341369, 839.8490238379862770256522360126952858160], - [0.0, -0.1176451708255218561268102928705214692156, 9.481894819354298841374880327580666327841, -114.9156758298319354820100563154045258044, 591.3193291312467068002224871165564177657, -1559.765475086581493834896780108830295683, 2198.545841718649266979102957216738477165, -1564.562048273728342299716850911579457005, 440.0137786917170208520501729678092387026], -]; diff --git a/src/ode/adaptive_solvers/rkv98_nointerp.rs b/src/ode/adaptive_solvers/rkv98_nointerp.rs deleted file mode 100644 index 82a606f..0000000 --- a/src/ode/adaptive_solvers/rkv98_nointerp.rs +++ /dev/null @@ -1,40 +0,0 @@ -use super::RKAdaptive; - -// File below is auto-generated via python script that parses -// data available on web at: -// - -use super::rkv98_nointerp_table as bt; -use crate::ode::types::{ODEError, ODEResult, ODESolution, ODEState}; - -pub struct RKV98NoInterp {} - -const N: usize = 16; - -impl RKAdaptive for RKV98NoInterp { - const ORDER: usize = 9; - - const FSAL: bool = false; - - const B: [f64; N] = bt::B; - - const C: [f64; N] = bt::C; - - const A: [[f64; N]; N] = bt::A; - - const BI: [[f64; 1]; N] = bt::BI; - - const BERR: [f64; N] = { - let mut berr = [0.0; N]; - let mut ix: usize = 0; - while ix < N { - berr[ix] = Self::B[ix] - bt::BHAT[ix]; - ix += 1; - } - berr - }; - - fn interpolate(_xinterp: f64, _sol: &ODESolution) -> ODEResult { - ODEError::InterpNotImplemented.into() - } -} diff --git a/src/ode/adaptive_solvers/rkv98_nointerp_table.rs b/src/ode/adaptive_solvers/rkv98_nointerp_table.rs deleted file mode 100644 index 16b14bd..0000000 --- a/src/ode/adaptive_solvers/rkv98_nointerp_table.rs +++ /dev/null @@ -1,349 +0,0 @@ -pub const BI: [[f64; 1]; 16] = [ - [1.0], - [0.0], - [0.0], - [0.0], - [0.0], - [0.0], - [0.0], - [0.0], - [0.0], - [0.0], - [0.0], - [0.0], - [0.0], - [0.0], - [0.0], - [0.0], -]; - -pub const C: [f64; 16] = [ - 0.0, - 0.034_62, - 0.097_024_350_638_780_44, - 0.145_536_525_958_170_68, - 0.561, - 0.229_007_911_590_485, - 0.544_992_088_409_515, - 0.645, - 0.483_75, - 0.067_57, - 0.25, - 0.659_065_061_873_099_9, - 0.820_6, - 0.901_2, - 1.0, - 1.0, -]; -pub const B: [f64; 16] = [ - 0.014_611_976_858_423_152, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - -0.391_521_186_233_133_9, - 0.231_093_250_028_950_65, - 0.127_476_676_999_285_25, - 0.224_643_417_620_415_8, - 0.568_435_268_974_851_3, - 0.058_258_715_572_158_275, - 0.136_431_740_348_221_56, - 0.030_570_139_830_827_976, - 0.0, -]; -pub const BHAT: [f64; 16] = [ - 0.019_969_965_148_867_73, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 2.191_499_304_949_33, - 0.088_570_718_482_084_38, - 0.114_056_023_486_596_56, - 0.253_316_380_534_510_7, - -2.056_564_386_240_941, - 0.340_809_679_901_312, - 0.0, - 0.0, - 0.048_342_313_738_239_585, -]; - -pub const A: [[f64; 16]; 16] = [ - [ - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - ], - [ - 0.034_62, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - -0.038_933_543_885_728_734, - 0.135_957_894_524_509_18, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - 0.036_384_131_489_542_67, - 0.0, - 0.109_152_394_468_628, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - 2.025_763_914_393_97, - 0.0, - -7.638_023_836_496_292, - 6.173_259_922_102_322, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - 0.051_122_755_894_060_61, - 0.0, - 0.0, - 0.177_082_379_455_502_15, - 0.000_802_776_240_922_250_2, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - 0.131_600_635_797_521_63, - 0.0, - 0.0, - -0.295_727_625_266_963_67, - 0.087_813_780_356_429_52, - 0.621_305_297_522_527_5, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - 0.071_666_666_666_666_67, - 0.0, - 0.0, - 0.0, - 0.0, - 0.330_553_357_891_531_95, - 0.242_779_975_441_801_38, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - 0.071_806_640_625, - 0.0, - 0.0, - 0.0, - 0.0, - 0.329_438_028_322_817_7, - 0.116_519_002_927_182_29, - -0.034_013_671_875, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - 0.048_367_576_463_406_47, - 0.0, - 0.0, - 0.0, - 0.0, - 0.039_289_899_256_761_64, - 0.105_474_094_589_034_46, - -0.021_438_652_846_483_126, - -0.104_122_917_462_719_44, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - -0.026_645_614_872_014_785, - 0.0, - 0.0, - 0.0, - 0.0, - 0.033_333_333_333_333_33, - -0.163_107_224_487_246_7, - 0.033_960_816_841_277_615, - 0.157_231_941_381_462_6, - 0.215_226_747_803_187_96, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - 0.036_890_092_487_086_225, - 0.0, - 0.0, - 0.0, - 0.0, - -0.146_518_157_672_554_3, - 0.224_257_776_817_202_44, - 0.022_944_057_170_660_725, - -0.003_585_005_290_572_876, - 0.086_692_233_164_443_85, - 0.438_384_065_196_833_76, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - -0.486_601_221_511_334_06, - 0.0, - 0.0, - 0.0, - 0.0, - -6.304_602_650_282_853, - -0.281_245_618_289_472_6, - -2.679_019_236_219_849_3, - 0.518_815_663_924_157_6, - 1.365_353_187_603_341_8, - 5.885_091_088_503_946_5, - 2.802_808_786_272_062_8, - 0.0, - 0.0, - 0.0, - 0.0, - ], - [ - 0.418_536_745_775_347_16, - 0.0, - 0.0, - 0.0, - 0.0, - 6.724_547_581_906_459, - -0.425_444_280_164_611_8, - 3.343_279_153_001_265_8, - 0.617_081_663_117_537_8, - -0.929_966_123_939_932_8, - -6.099_948_804_751_011, - -3.002_206_187_889_399, - 0.255_320_252_944_344_6, - 0.0, - 0.0, - 0.0, - ], - [ - -0.779_374_086_122_884_6, - 0.0, - 0.0, - 0.0, - 0.0, - -13.937_342_538_107_776, - 1.252_048_853_379_357_2, - -14.691_500_408_016_87, - -0.494_705_058_533_141_7, - 2.242_974_909_146_236_8, - 13.367_893_803_828_643, - 14.396_650_486_650_687, - -0.797_581_333_177_68, - 0.440_935_370_953_427_8, - 0.0, - 0.0, - ], - [ - 2.058_051_337_466_886_3, - 0.0, - 0.0, - 0.0, - 0.0, - 22.357_937_727_968_032, - 0.909_498_109_975_563_4, - 35.891_100_982_402_64, - -3.442_515_027_624_453_6, - -4.865_481_358_036_368_5, - -18.909_803_813_543_427, - -34.263_544_480_304_52, - 1.264_756_521_695_642_7, - 0.0, - 0.0, - 0.0, - ], -]; diff --git a/src/ode/mod.rs b/src/ode/mod.rs deleted file mode 100644 index 221f537..0000000 --- a/src/ode/mod.rs +++ /dev/null @@ -1,51 +0,0 @@ -//! Ordinary Differential Equation (ODE) solvers. -//! -//! This module provides a set of adaptive and non-adaptive ODE solvers. -//! All use Runge-Kutta methods to solve the ODEs. -//! -//! Solvers are adapted from Julia's DifferentialEquations.jl package. -//! -//! Solvers use coefficients from Jim Verner's website: -//! -//! which is awesome -//! -//! For certain solvers, interpolation between the starting and ending -//! points is enabled via separate interpolation functions. -//! - -mod adaptive_solvers; -pub mod rk_adaptive; -pub mod rk_adaptive_settings; -pub mod rk_explicit; -mod rosenbrock; -mod rodas4; -mod types; - -// NAlgebera bindings for ODE state -mod nalgebra_bindings; - -pub use rk_adaptive::RKAdaptive; -pub use rk_adaptive_settings::RKAdaptiveSettings; -pub use rosenbrock::Rosenbrock; - -pub mod solvers { - #[allow(unused)] - pub use super::adaptive_solvers::RKV98Efficient; - pub use super::adaptive_solvers::RKV98NoInterp; - #[allow(unused)] - pub use super::adaptive_solvers::RKF45; - pub use super::adaptive_solvers::RKTS54; - pub use super::adaptive_solvers::RKV65; - pub use super::adaptive_solvers::RKV87; - pub use super::adaptive_solvers::RKV98; - pub use super::rodas4::RODAS4; - #[allow(unused)] - pub use super::rk_explicit::Midpoint; - #[allow(unused)] - pub use super::rk_explicit::RK4; -} - -pub use types::*; - -#[cfg(test)] -mod ode_tests; diff --git a/src/ode/nalgebra_bindings.rs b/src/ode/nalgebra_bindings.rs deleted file mode 100644 index 891f61c..0000000 --- a/src/ode/nalgebra_bindings.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Implmeent ODEState for NAlgebra SMatrix (static matrix) - -use super::types::ODEState; - -impl ODEState for nalgebra::SMatrix { - #[inline(always)] - fn ode_elem_div(&self, other: &Self) -> Self { - self.component_div(other) - } - - #[inline(always)] - fn ode_elem_max(&self, other: &Self) -> Self { - self.sup(other) - } - - #[inline(always)] - fn ode_scaled_norm(&self) -> f64 { - self.norm() / (self.ode_nelem() as f64).sqrt() - } - - #[inline(always)] - fn ode_abs(&self) -> Self { - self.abs() - } - - #[inline(always)] - fn ode_scalar_add(&self, s: f64) -> Self { - self.add_scalar(s) - } - - #[inline(always)] - fn ode_nelem(&self) -> usize { - self.ncols() * self.nrows() - } - - fn ode_zero() -> Self { - Self::zeros() - } - - #[inline(always)] - fn ode_add_scaled(&mut self, other: &Self, scale: f64) { - *self += other * scale; - } -} diff --git a/src/ode/ode_tests.rs b/src/ode/ode_tests.rs deleted file mode 100644 index d6fbdce..0000000 --- a/src/ode/ode_tests.rs +++ /dev/null @@ -1,174 +0,0 @@ -//! Test for ODE solvers -//! -//! Tests are for a simple 1D harmonic oscillator -//! which can be easily checked for correctness against the analytical solution. -//! -//! The tests are run for all adaptive solvers with and without interpolation. -//! - -use super::ODEResult; -use super::RKAdaptive; -use super::RKAdaptiveSettings; - -/// ODE state for 1-dimensional harmonic oscillator -/// y[0] = position -/// y[1] = velocity -type State = crate::mathtypes::Vector2; - -/// Harmonic oscillator ODE y'' = -y -fn ydot(_t: f64, y: &State) -> ODEResult { - Ok([y[1], -y[0]].into()) -} - -/// Test harmonic oscillator -fn harmonic_oscillator(_integrator: F) -where - F: RKAdaptive, -{ - use std::f64::consts::PI; - - let y0 = State::new(1.0, 0.0); - - let settings = RKAdaptiveSettings { - dense_output: false, - abserror: 1e-12, - relerror: 1e-12, - ..RKAdaptiveSettings::default() - }; - - let res = F::integrate(0.0, 2.0 * PI, &y0, ydot, &settings).unwrap(); - assert!((res.y[0] - 1.0).abs() < 1e-11); - assert!((res.y[1]).abs() < 1e-11); -} - -/// Test harmonic oscillator with interpolation -fn harmonic_oscillator_interp(_integrator: F) -where - F: RKAdaptive, -{ - use std::f64::consts::PI; - - let y0 = State::new(1.0, 0.0); - - let settings = RKAdaptiveSettings { - dense_output: true, - abserror: 1e-12, - relerror: 1e-12, - ..RKAdaptiveSettings::default() - }; - - let res = F::integrate(0.0, PI, &y0, ydot, &settings).unwrap(); - - let testcount = 100; - (0..testcount).for_each(|idx| { - let x = idx as f64 * PI / testcount as f64; - let interp = F::interpolate(x, &res).unwrap(); - assert!((interp[0] - x.cos()).abs() < 1e-10); - assert!((interp[1] + x.sin()).abs() < 1e-10); - }); -} - -/// Test harmonic oscillator with all integrators -#[test] -fn test_harmonic_oscillator() { - harmonic_oscillator(super::solvers::RKF45 {}); - harmonic_oscillator(super::solvers::RKTS54 {}); - harmonic_oscillator(super::solvers::RKV65 {}); - harmonic_oscillator(super::solvers::RKV87 {}); - harmonic_oscillator(super::solvers::RKV98 {}); - harmonic_oscillator(super::solvers::RKV98NoInterp {}); - harmonic_oscillator(super::solvers::RKV98Efficient {}); -} - -/// Test harmonic oscillator with all integrators with interpolation -#[test] -fn test_harmonic_oscillator_interp() { - harmonic_oscillator_interp(super::solvers::RKTS54 {}); - harmonic_oscillator_interp(super::solvers::RKV65 {}); - harmonic_oscillator_interp(super::solvers::RKV87 {}); - harmonic_oscillator_interp(super::solvers::RKV98 {}); - harmonic_oscillator_interp(super::solvers::RKV98Efficient {}); -} - -/// Test FSAL optimization reduces function evaluations -#[test] -fn test_fsal_optimization() { - use std::f64::consts::PI; - - let y0 = State::new(1.0, 0.0); - - let settings = RKAdaptiveSettings { - dense_output: false, - abserror: 1e-10, - relerror: 1e-10, - ..RKAdaptiveSettings::default() - }; - - // RKTS54 has FSAL enabled - let res_fsal = super::solvers::RKTS54::integrate(0.0, 2.0 * PI, &y0, ydot, &settings).unwrap(); - - // RKF45 does not have FSAL - let res_no_fsal = - super::solvers::RKF45::integrate(0.0, 2.0 * PI, &y0, ydot, &settings).unwrap(); - - println!( - "RKTS54 (FSAL=true): {} evals, {} accepted steps", - res_fsal.nevals, res_fsal.naccept - ); - println!( - "RKF45 (FSAL=false): {} evals, {} accepted steps", - res_no_fsal.nevals, res_no_fsal.naccept - ); - - // FSAL should save approximately 1 evaluation per accepted step - // (exact savings depends on rejected steps) - // For RKTS54 (7 stages), without FSAL we'd expect ~7*naccept evaluations - // With FSAL, we expect ~(7*naccept - (naccept-1)) = 6*naccept + 1 evaluations - let expected_savings = res_fsal.naccept.saturating_sub(1); - let theoretical_no_fsal = res_fsal.naccept * 7; - - println!("Expected savings: ~{} evaluations", expected_savings); - println!("Theoretical evals without FSAL: {}", theoretical_no_fsal); - - // Verify FSAL actually reduces evaluations - assert!( - res_fsal.nevals < theoretical_no_fsal, - "FSAL should reduce function evaluations" - ); -} - -/// Compare RKV98 Robust vs Efficient versions -#[test] -fn test_rkv98_comparison() { - use std::f64::consts::PI; - - let y0 = State::new(1.0, 0.0); - - let settings = RKAdaptiveSettings { - dense_output: false, - abserror: 1e-12, - relerror: 1e-12, - ..RKAdaptiveSettings::default() - }; - - // RKV98 Robust (21 stages) - let res_robust = super::solvers::RKV98::integrate(0.0, 2.0 * PI, &y0, ydot, &settings).unwrap(); - - // RKV98 Efficient (26 stages) - let res_efficient = - super::solvers::RKV98Efficient::integrate(0.0, 2.0 * PI, &y0, ydot, &settings).unwrap(); - - println!("\nRKV98 Comparison:"); - println!( - "Robust (21 stages): {} evals, {} accepted, {} rejected steps", - res_robust.nevals, res_robust.naccept, res_robust.nreject - ); - println!( - "Efficient (26 stages): {} evals, {} accepted, {} rejected steps", - res_efficient.nevals, res_efficient.naccept, res_efficient.nreject - ); - - // Both should achieve the same accuracy - assert!((res_robust.y[0] - 1.0).abs() < 1e-11); - assert!((res_efficient.y[0] - 1.0).abs() < 1e-11); -} diff --git a/src/ode/rk_adaptive.rs b/src/ode/rk_adaptive.rs deleted file mode 100644 index fc87a0d..0000000 --- a/src/ode/rk_adaptive.rs +++ /dev/null @@ -1,263 +0,0 @@ -use super::types::*; -use super::RKAdaptiveSettings; - -pub trait RKAdaptive { - // Butcher Tableau Coefficients - const A: [[f64; N]; N]; - const C: [f64; N]; - const B: [f64; N]; - const BERR: [f64; N]; - - // Interpolation coefficients - const BI: [[f64; NI]; N]; - - // order - const ORDER: usize; - - /// First Same as Last - /// (first compute of next iteration is same as last compute of last iteration) - const FSAL: bool; - - fn interpolate(xinterp: f64, sol: &ODESolution) -> ODEResult { - let dense = match sol.dense.as_ref() { - Some(d) if !d.x.is_empty() => d, - _ => return ODEError::NoDenseOutputInSolution.into(), - }; - - let forward = sol.x > dense.x[0]; - - // Bounds check - let (lo, hi) = if forward { - (dense.x[0], sol.x) - } else { - (sol.x, dense.x[0]) - }; - if xinterp < lo || xinterp > hi { - return ODEError::InterpExceedsSolutionBounds { - interp: xinterp, - begin: dense.x[0], - end: sol.x, - } - .into(); - } - - // Find the step containing xinterp - let idx = if forward { - dense.x.iter().position(|&x| x >= xinterp) - } else { - dense.x.iter().position(|&x| x <= xinterp) - } - .unwrap_or(dense.x.len()) - .saturating_sub(1); - - // t is fractional distance within the step, in range [0,1] - let t = (xinterp - dense.x[idx]) / dense.h[idx]; - - // Compute interpolant coefficients bi[i] = sum_j(BI[i][j] * t^(j+1)) - // Equation (6) of Verner 2010 - let bi: [f64; N] = std::array::from_fn(|i| { - let mut tj = 1.0; - Self::BI[i].iter().fold(0.0, |acc, bij| { - tj *= t; - acc + bij * tj - }) - }); - - // Compute interpolated value - // Equation (5) of Verner 2010: y_interp = (y/h + sum(k[i] * bi[i])) * h - let mut y = dense.y[idx].clone() / dense.h[idx]; - for (ix, k) in dense.yprime[idx].iter().enumerate() { - y.ode_add_scaled(k, bi[ix]); - } - y = y * dense.h[idx]; - Ok(y) - } - - fn integrate( - begin: f64, - end: f64, - y0: &S, - ydot: impl Fn(f64, &S) -> ODEResult, - settings: &RKAdaptiveSettings, - ) -> ODEResult> { - let mut nevals: usize = 0; - let mut naccept: usize = 0; - let mut nreject: usize = 0; - let mut x = begin; - let mut y = y0.clone(); - - // PID controller state: two previous error norms (Söderlind & Wang 2006) - let mut enorm_prev: f64 = 1.0e-4; - let mut enorm_prev2: f64 = 1.0e-4; - let tdir = match end > begin { - true => 1.0, - false => -1.0, - }; - - // Take guess at initial stepsize - let mut h = { - // Adapted from OrdinaryDiffEq.jl - let sci = (y0.ode_abs() * settings.relerror).ode_scalar_add(settings.abserror); - - let d0 = y0.ode_elem_div(&sci).ode_scaled_norm(); - let ydot0 = ydot(begin, y0)?; - let d1 = ydot0.ode_elem_div(&sci).ode_scaled_norm(); - let h0 = 0.01 * d0 / d1 * tdir; - let y1 = y0.clone() + ydot0.clone() * h0; - let ydot1 = ydot(begin + h0, &y1)?; - let d2 = (ydot1 - ydot0).ode_elem_div(&sci).ode_scaled_norm() / h0; - let dmax = f64::max(d1, d2); - let h1: f64 = match dmax < 1e-15 { - false => 10.0_f64.powf(-(2.0 + dmax.log10()) / (Self::ORDER as f64)), - true => f64::max(1e-6, h0.abs() * 1e-3), - }; - nevals += 2; - f64::min(100.0 * h0.abs(), h1.abs()) * tdir - }; - let mut accepted_steps: Option> = match settings.dense_output { - false => None, - true => Some(DenseOutput { - x: Vec::new(), - h: Vec::new(), - yprime: Vec::new(), - y: Vec::new(), - }), - }; - - // For FSAL methods, cache the last k evaluation - let mut k_last: Option = None; - - // Pre-allocate stage array (avoids heap allocation per step) - let mut karr: Vec = Vec::with_capacity(N); - // Scratch state for computing stage inputs - let mut ytmp = y0.clone(); - - // PID controller constants - let order_f = Self::ORDER as f64; - let beta1 = 0.7 / order_f; - let beta2 = 0.4 / order_f; - let beta3 = 0.1 / order_f; - - // OK ... lets integrate! - loop { - if (tdir > 0.0 && (x + h) >= end) || (tdir < 0.0 && (x + h) <= end) { - h = end - x; - } - - karr.clear(); - - // Use FSAL optimization: reuse last stage from previous step as first stage - if Self::FSAL && k_last.is_some() { - karr.push(k_last.take().unwrap()); - } else { - karr.push(ydot(x, &y)?); - nevals += 1; - } - - // Create the remaining "k"s using in-place accumulation - for k in 1..N { - // Build ytmp = y + h * sum(A[k][i] * karr[i]) - ytmp.clone_from(&y); - for (idx, ki) in karr.iter().enumerate() { - let a = Self::A[k][idx]; - if a.abs() > 1.0e-30 { - ytmp.ode_add_scaled(ki, a * h); - } - } - karr.push(ydot(h.mul_add(Self::C[k], x), &ytmp)?); - nevals += 1; - } - - // Compute ynp1 = y + h * sum(B[i] * karr[i]) using in-place accumulation - let mut ynp1 = y.clone(); - for (idx, k) in karr.iter().enumerate() { - let b = Self::B[idx]; - if b.abs() > 1.0e-30 { - ynp1.ode_add_scaled(k, b * h); - } - } - - // Compute the "error" state by differencing the p and p* orders - let mut yerr = S::ode_zero(); - for (idx, k) in karr.iter().enumerate() { - let berr = Self::BERR[idx]; - if berr.abs() > 1.0e-9 { - yerr.ode_add_scaled(k, berr * h); - } - } - - // Compute normalized error - let enorm = { - let ymax = y.ode_abs().ode_elem_max(&ynp1.ode_abs()) * settings.relerror; - let ymax = ymax.ode_scalar_add(settings.abserror); - yerr.ode_elem_div(&ymax).ode_scaled_norm() - }; - - if !enorm.is_finite() { - return ODEError::StepErrorToSmall.into(); - } - - // PID step-size controller (Söderlind & Wang 2006, §4) - // - // The step-size ratio is: h_{n+1}/h_n = 1/q, where - // q = (e_n)^β₁ · (e_{n-1})^β₂ · (e_{n-2})^β₃ / safety - // - // β₁ = 0.7/p, β₂ = -0.4/p, β₃ = 0.1/p - // (note β₂ is negative, implemented by dividing by enorm_prev^0.4/p) - - if (enorm < 1.0) || (h.abs() <= settings.dtmin) { - // PID controller for accepted steps - let q = { - let raw = enorm.powf(beta1) - / enorm_prev.powf(beta2) - * enorm_prev2.powf(beta3) - / settings.gamma; - raw.clamp(1.0 / settings.maxfac, 1.0 / settings.minfac) - }; - - // If dense output requested, record dense output - if settings.dense_output { - let astep = accepted_steps.as_mut().unwrap(); - astep.x.push(x); - astep.h.push(h); - astep.yprime.push(karr.clone()); - astep.y.push(y.clone()); - } - - // For FSAL methods, save the last k for next iteration - if Self::FSAL { - k_last = Some(karr[N - 1].clone()); - } - - // Update PID history (floor at 1e-4 to avoid division artifacts) - enorm_prev2 = enorm_prev; - enorm_prev = f64::max(enorm, 1.0e-4); - x += h; - y = ynp1; - h /= q; - - naccept += 1; - if (tdir > 0.0 && x >= end) || (tdir < 0.0 && x <= end) { - break; - } - } else { - // Step rejected — use P-only controller (more conservative) - if Self::FSAL { - k_last = None; - } - nreject += 1; - let reject_q = enorm.powf(beta1) / settings.gamma; - h /= reject_q.min(1.0 / settings.minfac); - } - } - - Ok(ODESolution { - nevals, - naccept, - nreject, - x, - y, - dense: accepted_steps, - }) - } -} diff --git a/src/ode/rk_adaptive_settings.rs b/src/ode/rk_adaptive_settings.rs deleted file mode 100644 index 608bc32..0000000 --- a/src/ode/rk_adaptive_settings.rs +++ /dev/null @@ -1,33 +0,0 @@ -#[derive(Clone, Debug)] - -/// Settings for adaptive Runge-Kutta methods. -pub struct RKAdaptiveSettings { - /// Absolute error tolerance - pub abserror: f64, - /// Relative error tolerance - pub relerror: f64, - /// Minimum factor for step size - pub minfac: f64, - /// Maximum factor for step size - pub maxfac: f64, - /// Safety factor - pub gamma: f64, - /// Minimum step size - pub dtmin: f64, - /// Enable dense output (more storage, but allows interpolation) - pub dense_output: bool, -} - -impl Default for RKAdaptiveSettings { - fn default() -> Self { - Self { - abserror: 1.0e-8, - relerror: 1.0e-8, - minfac: 0.2, - maxfac: 10.0, - gamma: 0.9, - dtmin: 1.0e-6, - dense_output: false, - } - } -} diff --git a/src/ode/rk_explicit.rs b/src/ode/rk_explicit.rs deleted file mode 100644 index ecb983e..0000000 --- a/src/ode/rk_explicit.rs +++ /dev/null @@ -1,115 +0,0 @@ -use super::types::{ODEResult, ODESystem}; - -#[allow(unused)] -pub trait RKExplicit { - const A: [[f64; N]; N]; - const C: [f64; N]; - const B: [f64; N]; - - fn step(x0: f64, y0: &S::Output, h: f64, system: &mut S) -> ODEResult { - let mut karr = Vec::::new(); - karr.push(system.ydot(x0, y0)?); - - // Create the "k"s - for k in 1..N { - karr.push(system.ydot( - h.mul_add(Self::C[k], x0), - &(karr.iter().enumerate().fold(y0.clone(), |acc, (idx, ki)| { - acc + ki.clone() * Self::A[k][idx] * h - })), - )?); - } - - // Sum the "k"s - Ok(karr - .into_iter() - .enumerate() - .fold(y0.clone(), |acc, (idx, k)| acc + k * Self::B[idx] * h)) - } - - fn integrate( - x0: f64, - xend: f64, - dx: f64, - y0: &S::Output, - system: &mut S, - ) -> ODEResult> { - let mut x: f64 = x0; - let mut v = Vec::new(); - let mut y = y0.clone(); - let steps = ((xend - x0) / dx).ceil() as usize; - for _ in 0..steps { - let ynew = Self::step(x, &y, dx, system)?; - v.push(ynew.clone()); - x += dx; - if x > xend { - x = xend; - } - y = ynew; - } - Ok(v) - } -} - -pub struct RK4 {} -/// -/// Buchter tableau for RK4 -impl RKExplicit<4> for RK4 { - const A: [[f64; 4]; 4] = [ - [0.0, 0.0, 0.0, 0.0], - [0.5, 0.0, 0.0, 0.0], - [0.0, 0.5, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0], - ]; - const B: [f64; 4] = [1.0 / 6.0, 1.0 / 3.0, 1.0 / 3.0, 1.0 / 6.0]; - const C: [f64; 4] = [0.0, 0.5, 0.5, 1.0]; -} - -pub struct Midpoint {} -impl RKExplicit<2> for Midpoint { - const A: [[f64; 2]; 2] = [[0.0, 0.0], [0.5, 0.0]]; - const B: [f64; 2] = [0.0, 1.0]; - const C: [f64; 2] = [0.0, 0.5]; -} - -#[cfg(test)] -mod tests { - - use super::*; - type State = nalgebra::Vector2; - - struct HarmonicOscillator { - k: f64, - } - impl HarmonicOscillator { - const fn new(k: f64) -> Self { - Self { k } - } - } - - impl ODESystem for HarmonicOscillator { - type Output = nalgebra::Vector2; - fn ydot( - &mut self, - _x: f64, - y: &nalgebra::Vector2, - ) -> ODEResult> { - Ok(State::new(y[1], -self.k * y[0])) - } - } - - #[test] - fn testit() -> ODEResult<()> { - let mut system = HarmonicOscillator::new(1.0); - let y0 = State::new(1.0, 0.0); - - use std::f64::consts::PI; - - // integrating this harmonic oscillator between 0 and 2PI should return to the - // original state - let out2 = RK4::integrate(0.0, 2.0 * PI, 0.0001 * 2.0 * PI, &y0, &mut system)?; - assert!((out2.last().unwrap()[0] - 1.0).abs() < 1.0e-6); - assert!(out2.last().unwrap().abs()[1] < 1.0e-10); - Ok(()) - } -} diff --git a/src/ode/rodas4.rs b/src/ode/rodas4.rs deleted file mode 100644 index 00b22cb..0000000 --- a/src/ode/rodas4.rs +++ /dev/null @@ -1,59 +0,0 @@ -//! RODAS4 — 6-stage, order 4(3), L-stable Rosenbrock method. -//! -//! From Hairer & Wanner, "Solving Ordinary Differential Equations II", §IV.7. -//! Stiffly accurate: the last two stages coincide with the solution. -//! -//! Well-suited for stiff systems such as re-entry trajectories or -//! orbits with very low perigee where atmospheric drag creates stiffness. - -use super::rosenbrock::Rosenbrock; - -pub struct RODAS4; - -impl Rosenbrock<6> for RODAS4 { - const GAMMA_DIAG: f64 = 0.25; - - const ALPHA: [f64; 6] = [0.0, 0.386, 0.21, 0.63, 1.0, 1.0]; - - #[rustfmt::skip] - const A: [[f64; 6]; 6] = [ - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.1544000000000000e+01, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.9466785280815826e+00, 0.2557011698983284e+00, 0.0, 0.0, 0.0, 0.0], - [0.3314825187068521e+01, 0.2896124015972201e+01, 0.9986419139977817e+00, 0.0, 0.0, 0.0], - [0.1221224509226641e+01, 0.6019134481288629e+01, 0.1253708332932087e+02, -0.6878860361058950e+00, 0.0, 0.0], - [0.1221224509226641e+01, 0.6019134481288629e+01, 0.1253708332932087e+02, -0.6878860361058950e+00, 1.0, 0.0], - ]; - - #[rustfmt::skip] - const C: [[f64; 6]; 6] = [ - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [-0.5668800000000000e+01, 0.0, 0.0, 0.0, 0.0, 0.0], - [-0.2430093356833875e+01, -0.2063599157091915e+00, 0.0, 0.0, 0.0, 0.0], - [-0.1073529058151375e+00, -0.9594562251023355e+01, -0.2047028614809616e+02, 0.0, 0.0, 0.0], - [ 0.7496443313967647e+01, -0.1024680431464352e+02, -0.3399990352819905e+02, 0.1170890893206160e+02, 0.0, 0.0], - [ 0.8083246795921522e+01, -0.7981132988064893e+01, -0.3152159432874371e+02, 0.1631930543123136e+02, -0.6058818238834054e+01, 0.0], - ]; - - const M: [f64; 6] = [ - 0.1221224509226641e+01, - 0.6019134481288629e+01, - 0.1253708332932087e+02, - -0.6878860361058950e+00, - 1.0, - 1.0, - ]; - - // Embedded weights (3rd order). Same as M except m̂[5] = 0, - // so the error estimate is simply k_6. - const MHAT: [f64; 6] = [ - 0.1221224509226641e+01, - 0.6019134481288629e+01, - 0.1253708332932087e+02, - -0.6878860361058950e+00, - 1.0, - 0.0, - ]; - - const ORDER: usize = 4; -} diff --git a/src/ode/rosenbrock.rs b/src/ode/rosenbrock.rs deleted file mode 100644 index 0a2ccd9..0000000 --- a/src/ode/rosenbrock.rs +++ /dev/null @@ -1,256 +0,0 @@ -//! Rosenbrock (linearly-implicit Runge-Kutta) solver for stiff ODEs. -//! -//! Unlike explicit RK methods, Rosenbrock methods solve linear systems -//! involving the Jacobian at each stage, making them suitable for stiff -//! ODEs without requiring nonlinear Newton iterations. -//! -//! Reference: -//! E. Hairer & G. Wanner, "Solving Ordinary Differential Equations II" (1996), §IV.7 - -use super::types::*; -use super::RKAdaptiveSettings; - -use nalgebra as na; - -/// Trait for Rosenbrock solvers parameterized by number of stages. -/// -/// Each step solves: -/// (I/(hγ) − J) k_i = f(t + α_i h, y + Σ a_ij k_j) + Σ (c_ij/h) k_j -/// -/// where J = ∂f/∂y is LU-factored once per step. -pub trait Rosenbrock { - /// Stage coupling matrix (lower-triangular, zero diagonal). - const A: [[f64; STAGES]; STAGES]; - /// Off-diagonal Γ coupling (lower-triangular, zero diagonal). - const C: [[f64; STAGES]; STAGES]; - /// Shared diagonal element of the Γ matrix. - const GAMMA_DIAG: f64; - /// Time offsets for each stage. - const ALPHA: [f64; STAGES]; - /// Solution weights (higher-order). - const M: [f64; STAGES]; - /// Embedded solution weights (for error estimation). - const MHAT: [f64; STAGES]; - /// Order of the higher-order method. - const ORDER: usize; - - /// Integrate from `begin` to `end`. - /// - /// `ydot` computes the RHS f(t, y). - /// `jac` computes the Jacobian ∂f/∂y as a 6x6 matrix (for the orbital state). - /// - /// The state type S is typically `SMatrix` for simple propagation - /// or `SMatrix` when computing the state transition matrix. - /// The Jacobian is always 6x6 since the STM derivative uses the same Jacobian. - fn integrate( - begin: f64, - end: f64, - y0: &S, - ydot: impl Fn(f64, &S) -> ODEResult, - jac: impl Fn(f64, &S) -> ODEResult>, - settings: &RKAdaptiveSettings, - ) -> ODEResult> { - let mut nevals: usize = 0; - let mut naccept: usize = 0; - let mut nreject: usize = 0; - let mut x = begin; - let mut y = y0.clone(); - - // PID controller state - let mut enorm_prev: f64 = 1.0e-4; - let mut enorm_prev2: f64 = 1.0e-4; - - let tdir: f64 = if end > begin { 1.0 } else { -1.0 }; - - // Initial step-size guess (same heuristic as RKAdaptive) - let mut h = { - let sci = (y0.ode_abs() * settings.relerror).ode_scalar_add(settings.abserror); - let d0 = y0.ode_elem_div(&sci).ode_scaled_norm(); - let ydot0 = ydot(begin, y0)?; - let d1 = ydot0.ode_elem_div(&sci).ode_scaled_norm(); - let h0 = 0.01 * d0 / d1 * tdir; - let y1 = y0.clone() + ydot0.clone() * h0; - let ydot1 = ydot(begin + h0, &y1)?; - let d2 = (ydot1 - ydot0).ode_elem_div(&sci).ode_scaled_norm() / h0; - let dmax = f64::max(d1, d2); - nevals += 2; - let h1: f64 = if dmax < 1e-15 { - f64::max(1e-6, h0.abs() * 1e-3) - } else { - 10.0_f64.powf(-(2.0 + dmax.log10()) / (Self::ORDER as f64)) - }; - f64::min(100.0 * h0.abs(), h1.abs()) * tdir - }; - - // PID controller constants - let order_f = Self::ORDER as f64; - let beta1 = 0.7 / order_f; - let beta2 = 0.4 / order_f; - let beta3 = 0.1 / order_f; - - loop { - // Clamp step to not overshoot end - if (tdir > 0.0 && (x + h) >= end) || (tdir < 0.0 && (x + h) <= end) { - h = end - x; - } - - let gamma = Self::GAMMA_DIAG; - let inv_hgamma = 1.0 / (h * gamma); - - // Evaluate f and J at current point - let fy = ydot(x, &y)?; - let jac_mat = jac(x, &y)?; - nevals += 1; - - // Form W = I/(hγ) − J and LU-factorize - let mut w_mat = -jac_mat; - for i in 0..6 { - w_mat[(i, i)] += inv_hgamma; - } - let lu = w_mat.lu(); - - // Compute stages k_1, ..., k_STAGES - // - // Each k_i is a full state (S), but the linear system solve - // operates on the 6x1 "simple state" columns. For C==7, we - // solve 7 columns independently through the same LU factorization. - let nelem = y.ode_nelem(); - let ncols = nelem / 6; - let mut karr: Vec = Vec::with_capacity(STAGES); - - for i in 0..STAGES { - // Stage argument: y + Σ a_ij k_j - let mut y_stage = y.clone(); - for jj in 0..i { - let a_ij = Self::A[i][jj]; - if a_ij.abs() > 1.0e-30 { - y_stage.ode_add_scaled(&karr[jj], a_ij); - } - } - - // f at the stage point (reuse fy for stage 0) - let fi = if i == 0 { - fy.clone() - } else { - let ti = x + Self::ALPHA[i] * h; - nevals += 1; - ydot(ti, &y_stage)? - }; - - // RHS = fi + Σ (c_ij/h) k_j - let mut rhs = fi; - if i > 0 { - let inv_h = 1.0 / h; - for jj in 0..i { - let c_ij = Self::C[i][jj]; - if c_ij.abs() > 1.0e-30 { - rhs.ode_add_scaled(&karr[jj], c_ij * inv_h); - } - } - } - - // Solve W * k_i = rhs for each column of the state - // Extract as flat array, solve column-by-column, reassemble - let mut ki = S::ode_zero(); - // We need to solve the 6x6 system for each of the ncols columns - // Access via raw slice manipulation since ODEState is generic - { - let rhs_slice = unsafe { - std::slice::from_raw_parts( - &rhs as *const S as *const f64, - nelem, - ) - }; - let ki_slice = unsafe { - std::slice::from_raw_parts_mut( - &mut ki as *mut S as *mut f64, - nelem, - ) - }; - for col in 0..ncols { - let offset = col * 6; - let rhs_col = na::Vector6::from_column_slice( - &rhs_slice[offset..offset + 6], - ); - let sol = lu.solve(&rhs_col) - .ok_or_else(|| ODEError::YDotError( - "Singular Jacobian in Rosenbrock solver".to_string(), - ))?; - ki_slice[offset..offset + 6].copy_from_slice(sol.as_slice()); - } - } - karr.push(ki); - } - - // Solution: y_{n+1} = y_n + Σ m_i k_i - let mut ynp1 = y.clone(); - for (idx, ki) in karr.iter().enumerate() { - let m_idx = Self::M[idx]; - if m_idx.abs() > 1.0e-30 { - ynp1.ode_add_scaled(ki, m_idx); - } - } - - // Error estimate: err = Σ (m_i − m̂_i) k_i - let mut yerr = S::ode_zero(); - for (idx, ki) in karr.iter().enumerate() { - let diff = Self::M[idx] - Self::MHAT[idx]; - if diff.abs() > 1.0e-20 { - yerr.ode_add_scaled(ki, diff); - } - } - - // Normalized error - let enorm = { - let ymax = y.ode_abs().ode_elem_max(&ynp1.ode_abs()) * settings.relerror; - let ymax = ymax.ode_scalar_add(settings.abserror); - yerr.ode_elem_div(&ymax).ode_scaled_norm() - }; - - if !enorm.is_finite() { - return ODEError::StepErrorToSmall.into(); - } - - // PID step-size controller (Söderlind & Wang 2006) - if (enorm < 1.0) || (h.abs() <= settings.dtmin) { - let q = { - let raw = enorm.powf(beta1) - / enorm_prev.powf(beta2) - * enorm_prev2.powf(beta3) - / settings.gamma; - raw.clamp(1.0 / settings.maxfac, 1.0 / settings.minfac) - }; - - enorm_prev2 = enorm_prev; - enorm_prev = f64::max(enorm, 1.0e-4); - x += h; - y = ynp1; - h /= q; - naccept += 1; - - if (tdir > 0.0 && x >= end) || (tdir < 0.0 && x <= end) { - break; - } - } else { - // Reject step - nreject += 1; - let reject_q = enorm.powf(beta1) / settings.gamma; - h /= reject_q.min(1.0 / settings.minfac); - } - } - - Ok(ODESolution { - nevals, - naccept, - nreject, - x, - y, - dense: None, // Rosenbrock dense output not implemented - }) - } - - /// Interpolation — not supported for Rosenbrock methods - fn interpolate(_xinterp: f64, _sol: &ODESolution) -> ODEResult { - ODEError::InterpNotImplemented.into() - } -} diff --git a/src/ode/types.rs b/src/ode/types.rs deleted file mode 100644 index a862ec3..0000000 --- a/src/ode/types.rs +++ /dev/null @@ -1,113 +0,0 @@ -use std::ops::{Add, Div, Mul, Sub}; - -use std::fmt::Debug; -use thiserror::Error; - -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Error)] -pub enum ODEError { - #[error("Step error not finite")] - StepErrorToSmall, - #[error("No Dense Output in Solution")] - NoDenseOutputInSolution, - #[error("Interpolation exceeds solution bounds: {interp} not in [{begin}, {end}]")] - InterpExceedsSolutionBounds { interp: f64, begin: f64, end: f64 }, - #[error("Interpolation not implemented for this integrator")] - InterpNotImplemented, - #[error("Y dot Function Error: {0}")] - YDotError(String), -} - -/// Ouptut of ODE integrator -pub type ODEResult = Result; - -impl From for ODEResult { - fn from(e: ODEError) -> Self { - Err(e) - } -} - -/// "States" of ordeinary differential equations -pub trait ODEState: - Add - + Sub - + Mul - + Div - + Clone - + Sized - + Debug -{ - // Element-wise divisior of self by other - fn ode_elem_div(&self, other: &Self) -> Self; - - // Element-wise maximum of self with other - fn ode_elem_max(&self, other: &Self) -> Self; - - // Euclidian norm scaled by inverse square root of number of elements - fn ode_scaled_norm(&self) -> f64; - - // Element-wise absolute value - fn ode_abs(&self) -> Self; - - // Add scalar to each element - fn ode_scalar_add(&self, s: f64) -> Self; - - // Number of elements - fn ode_nelem(&self) -> usize; - - // zero - fn ode_zero() -> Self; - - // self += other * scale (in-place fused multiply-add) - fn ode_add_scaled(&mut self, other: &Self, scale: f64); -} - -pub trait ODESystem { - type Output: ODEState; - fn ydot(&mut self, x: f64, y: &Self::Output) -> ODEResult; -} - -/// Dense output for ODE integrators -/// -/// This is a struct that contains the dense output of an ODE integrator -/// if dense output is enabled -/// -/// It can be used for interpolation of state values between -/// the steps of the integrator -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DenseOutput -where - S: ODEState, -{ - pub x: Vec, - pub h: Vec, - pub yprime: Vec>, - pub y: Vec, -} - -/// Solution of an ODE -/// Contains the final state, final x value, and dense output if enabled -/// Also contains statistics on the number of steps taken -/// and the number of function evaluations -/// -/// Serde is implemented for this struct -/// so that it is simple to incorporate into python bindings -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ODESolution -where - S: ODEState, -{ - /// Total number of derivative function evaluations - pub nevals: usize, - /// Number of accepted steps - pub naccept: usize, - /// Number of rejected steps - pub nreject: usize, - /// The final x value - pub x: f64, - /// The final y (state) value - pub y: S, - /// The dense output, if enabled - pub dense: Option>, -} diff --git a/src/orbitprop/drag.rs b/src/orbitprop/drag.rs index f53a197..fff6b2a 100644 --- a/src/orbitprop/drag.rs +++ b/src/orbitprop/drag.rs @@ -4,18 +4,17 @@ use crate::Instant; use crate::mathtypes::*; -const OMEGA_EARTH: Vector3 = nalgebra::vector![0.0, 0.0, crate::consts::OMEGA_EARTH]; -const OMEGA_EARTH_MATRIX: Matrix3 = Matrix3::new( - 0.0, - -crate::consts::OMEGA_EARTH, - 0.0, - crate::consts::OMEGA_EARTH, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, -); +fn omega_earth() -> Vector3 { + numeris::vector![0.0, 0.0, crate::consts::OMEGA_EARTH] +} + +fn omega_earth_matrix() -> Matrix3 { + Matrix3::new([ + [0.0, -crate::consts::OMEGA_EARTH, 0.0], + [crate::consts::OMEGA_EARTH, 0.0, 0.0], + [0.0, 0.0, 0.0], + ]) +} // Compute and return force from drag in the gcrf frame pub fn drag_force( @@ -41,7 +40,7 @@ pub fn drag_force( // to get velocity relative to wind in gcrf frame // This is a little confusing, but if you think about it long enough // it will make sense - let vrel = vel_gcrf - OMEGA_EARTH.cross(pos_gcrf); + let vrel = vel_gcrf - omega_earth().cross(pos_gcrf); -0.5 * cd_a_over_m * density * vrel * vrel.norm() } @@ -83,7 +82,7 @@ fn compute_rho_drhodr( // Geodetic "up" direction in ITRF: NED down is [0,0,1], so up is [0,0,-1] // rotated to ITRF via q_ned2itrf - let up_itrf = itrf.q_ned2itrf() * nalgebra::vector![0.0, 0.0, -1.0]; + let up_itrf = itrf.q_ned2itrf() * numeris::vector![0.0, 0.0, -1.0]; // Rotate to GCRF and scale by dρ/dh let up_gcrf = qgcrf2itrf.conjugate() * up_itrf; @@ -109,7 +108,7 @@ pub fn drag_and_partials( // to get velocity relative to wind in gcrf frame // This is a little confusing, but if you think about it long enough // it will make sense - let vrel = vel_gcrf - OMEGA_EARTH.cross(pos_gcrf); + let vrel = vel_gcrf - omega_earth().cross(pos_gcrf); let vrel_norm = vrel.norm(); let drag_accel_gcrf = -0.5 * cd_a_over_m * density * vrel * vrel_norm; @@ -120,13 +119,13 @@ pub fn drag_and_partials( let dacceldv = -0.5 * cd_a_over_m * density - * (vrel * vrel.transpose() / vrel_norm + vrel_norm * Matrix3::identity()); + * (vrel * vrel.transpose() / vrel_norm + vrel_norm * Matrix3::eye()); // ∂a/∂r has two terms: // 1) from ρ(r): -0.5 * CdA/m * v_rel * |v_rel| * (∂ρ/∂r)^T // 2) from v_rel(r): (∂a/∂v) * (∂v_rel/∂r) = (∂a/∂v) * (-[ω×]) let dacceldr = -0.5 * cd_a_over_m * vrel * vrel_norm * drhodr.transpose() - - dacceldv * OMEGA_EARTH_MATRIX; + - dacceldv * omega_earth_matrix(); (drag_accel_gcrf, dacceldr, dacceldv) } @@ -140,16 +139,16 @@ mod tests { // Circular prograde orbit at ~400 km altitude let r = 6778.0e3; // Earth radius + 400 km let v_circ = (crate::consts::MU_EARTH / r).sqrt(); - let pos_gcrf = Vector3::new(r, 0.0, 0.0); + let pos_gcrf = numeris::vector![r, 0.0, 0.0]; let pos_itrf = pos_gcrf; // Approximate: ignore frame rotation for this test - let vel_gcrf = Vector3::new(0.0, v_circ, 0.0); // prograde + let vel_gcrf = numeris::vector![0.0, v_circ, 0.0]; // prograde let time = Instant::from_datetime(2020, 1, 1, 0, 0, 0.0).unwrap(); let cd_a_over_m = 0.01; // typical value let drag = drag_force(&pos_gcrf, &pos_itrf, &vel_gcrf, &time, cd_a_over_m, false); // Drag should oppose relative velocity direction - let vrel = vel_gcrf - OMEGA_EARTH.cross(&pos_gcrf); + let vrel = vel_gcrf - omega_earth().cross(&pos_gcrf); let drag_dot_vrel = drag.dot(&vrel); assert!( drag_dot_vrel < 0.0, @@ -163,7 +162,7 @@ mod tests { &pos_gcrf, &pos_itrf, &vel_gcrf, &time, cd_a_over_m * 2.0, false, ); - approx::assert_relative_eq!(drag2.norm(), drag.norm() * 2.0, max_relative = 1.0e-10); + assert!((drag2.norm() - drag.norm() * 2.0).abs() < 1.0e-10 * drag.norm()); } #[test] @@ -171,9 +170,9 @@ mod tests { // At 2000 km altitude, atmospheric density should be negligible let r = 8378.0e3; // Earth radius + 2000 km let v_circ = (crate::consts::MU_EARTH / r).sqrt(); - let pos_gcrf = Vector3::new(r, 0.0, 0.0); + let pos_gcrf = numeris::vector![r, 0.0, 0.0]; let pos_itrf = pos_gcrf; - let vel_gcrf = Vector3::new(0.0, v_circ, 0.0); + let vel_gcrf = numeris::vector![0.0, v_circ, 0.0]; let time = Instant::from_datetime(2020, 1, 1, 0, 0, 0.0).unwrap(); let drag = drag_force(&pos_gcrf, &pos_itrf, &vel_gcrf, &time, 0.01, false); diff --git a/src/orbitprop/point_gravity.rs b/src/orbitprop/point_gravity.rs index b311b09..8f2ee04 100644 --- a/src/orbitprop/point_gravity.rs +++ b/src/orbitprop/point_gravity.rs @@ -1,11 +1,11 @@ -use nalgebra as na; +use crate::mathtypes::*; // Equation 3.37 in Montenbruck & Gill pub fn point_gravity( - r: &na::Vector3, // object - s: &na::Vector3, // distant attractor + r: &Vector3, // object + s: &Vector3, // distant attractor mu: f64, -) -> na::Vector3 { +) -> Vector3 { let sr = s - r; let srnorm2 = sr.norm_squared(); let srnorm = srnorm2.sqrt(); @@ -20,10 +20,10 @@ pub fn point_gravity( // Equation 7.75 in Montenbruck & Gill for partials pub fn point_gravity_and_partials( - r: &na::Vector3, // object - s: &na::Vector3, // distant attractor + r: &Vector3, // object + s: &Vector3, // distant attractor mu: f64, -) -> (na::Vector3, na::Matrix3) { +) -> (Vector3, Matrix3) { let rs = r - s; let rsnorm2 = rs.norm_squared(); let rsnorm = rsnorm2.sqrt(); @@ -32,7 +32,7 @@ pub fn point_gravity_and_partials( let rsnorm3 = rsnorm2 * rsnorm; ( -mu * (rs / rsnorm3 + s / (snorm * snorm2)), - -mu * (na::Matrix3::::identity() / rsnorm3 + -mu * (Matrix3::eye() / rsnorm3 - 3.0 * rs * rs.transpose() / (rsnorm2 * rsnorm3)), ) } @@ -45,8 +45,8 @@ mod tests { fn test_point_gravity_known() { // Moon at ~384,400 km from Earth center, satellite at GEO (~42,164 km) let mu_moon = 4.9048695e12; // m³/s² - let s_moon = na::Vector3::new(384_400.0e3, 0.0, 0.0); // Moon position - let r_sat = na::Vector3::new(42_164.0e3, 0.0, 0.0); // GEO satellite + let s_moon = numeris::vector![384_400.0e3, 0.0, 0.0]; // Moon position + let r_sat = numeris::vector![42_164.0e3, 0.0, 0.0]; // GEO satellite let accel = point_gravity(&r_sat, &s_moon, mu_moon); // Lunar perturbation at GEO should be ~1e-6 m/s² order of magnitude @@ -61,9 +61,9 @@ mod tests { #[test] fn test_point_gravity_partials() { let mu = 4.9048695e12; - let s = na::Vector3::new(384_400.0e3, 50_000.0e3, 20_000.0e3); - let r = na::Vector3::new(42_164.0e3, 1000.0e3, 500.0e3); - let dr = na::Vector3::new(10.0, 20.0, -15.0); + let s = numeris::vector![384_400.0e3, 50_000.0e3, 20_000.0e3]; + let r = numeris::vector![42_164.0e3, 1000.0e3, 500.0e3]; + let dr = numeris::vector![10.0, 20.0, -15.0]; let (accel0, partials) = point_gravity_and_partials(&r, &s, mu); let accel1 = point_gravity(&(r + dr), &s, mu); diff --git a/src/orbitprop/propagator.rs b/src/orbitprop/propagator.rs index f1daffb..2259aea 100644 --- a/src/orbitprop/propagator.rs +++ b/src/orbitprop/propagator.rs @@ -3,30 +3,23 @@ use super::point_gravity::{point_gravity, point_gravity_and_partials}; use super::settings::PropSettings; use crate::lpephem; -use crate::ode; -use crate::ode::ODEError; -use crate::ode::ODEResult; -use crate::ode::RKAdaptive; -use crate::ode::Rosenbrock; use crate::orbitprop::Precomputed; use crate::{Duration, Instant, TimeLike}; use lpephem::sun::shadowfunc; +use numeris::ode::{self, RKAdaptive, Rosenbrock}; + use anyhow::{Context, Result}; use crate::mathtypes::*; use crate::consts; use crate::orbitprop::SatProperties; -use num_traits::identities::Zero; - -use thiserror::Error; - -use nalgebra as na; use serde::{Deserialize, Serialize}; +use thiserror::Error; -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone)] pub struct PropagationResult { pub time_begin: Instant, pub state_begin: Matrix<6, T>, @@ -35,17 +28,33 @@ pub struct PropagationResult { pub accepted_steps: u32, pub rejected_steps: u32, pub num_eval: u32, - pub odesol: Option>>, + pub odesol: Option>, pub integrator: super::settings::Integrator, } +impl std::fmt::Debug for PropagationResult { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PropagationResult") + .field("time_begin", &self.time_begin) + .field("state_begin", &self.state_begin) + .field("time_end", &self.time_end) + .field("state_end", &self.state_end) + .field("accepted_steps", &self.accepted_steps) + .field("rejected_steps", &self.rejected_steps) + .field("num_eval", &self.num_eval) + .field("odesol", &self.odesol.as_ref().map(|_| "...")) + .field("integrator", &self.integrator) + .finish() + } +} + impl PropagationResult { pub fn interp(&self, time: &U) -> Result> { interp_propresult(self, time) } } -pub type StateType = na::SMatrix; +pub type StateType = Matrix<6, C>; // Simple state with position & velocity pub type SimpleState = StateType<1>; @@ -60,7 +69,7 @@ pub enum PropagationError { #[error("No Dense Output in Solution")] NoDenseOutputInSolution, #[error("ODE Error: {0}")] - ODEError(ode::ODEError), + ODEError(ode::OdeError), #[error("RODAS4 does not support state transition matrix propagation")] RODAS4NoSTM, } @@ -88,8 +97,8 @@ fn solar_pressure_accel( props: &dyn SatProperties, state: &SimpleState, ) -> Vector3 { - -shadowfunc(sun_gcrf, pos_gcrf) * props.cr_a_over_m(time, state) * 4.56e-6 * sun_gcrf - / sun_gcrf.norm() + sun_gcrf * (-shadowfunc(sun_gcrf, pos_gcrf) * props.cr_a_over_m(time, state) * 4.56e-6 + / sun_gcrf.norm()) } /// @@ -176,12 +185,12 @@ fn solar_pressure_accel( /// /// // Setup a simple Geosynchronous orbit with initial position along the x axis /// // and initial velocity along the y axis -/// use nalgebra as na; +/// use satkit::mathtypes::*; /// let mut state = satkit::orbitprop::CovState::zeros(); -/// state.fixed_view_mut::<3, 1>(0, 0).copy_from(&na::vector![satkit::consts::GEO_R, 0.0, 0.0]); -/// state.fixed_view_mut::<3, 1>(3, 0).copy_from(&na::vector![0.0, (satkit::consts::MU_EARTH/satkit::consts::GEO_R).sqrt(), 0.0]); -/// // initialize state transition matrix to zero -/// state.fixed_view_mut::<6, 6>(0, 1).copy_from(&na::Matrix6::::identity()); +/// state.set_block(0, 0, &numeris::vector![satkit::consts::GEO_R, 0.0, 0.0]); +/// state.set_block(3, 0, &numeris::vector![0.0, (satkit::consts::MU_EARTH/satkit::consts::GEO_R).sqrt(), 0.0]); +/// // initialize state transition matrix to identity +/// state.set_block(0, 1, &Matrix6::eye()); /// /// /// // Setup the details of the propagation @@ -235,9 +244,9 @@ pub fn propagate( // Duration to end of integration, in seconds let x_end: f64 = (end - begin).as_seconds(); - let odesettings = crate::ode::RKAdaptiveSettings { - abserror: settings.abs_error, - relerror: settings.rel_error, + let odesettings = ode::AdaptiveSettings { + abs_tol: settings.abs_error, + rel_tol: settings.rel_error, dense_output: settings.enable_interp, ..Default::default() }; @@ -252,15 +261,12 @@ pub fn propagate( let gravity = settings.gravity_model.get(); - let ydot = |x: f64, y: &Matrix<6, C>| -> ODEResult> { + let ydot = |x: f64, y: &Matrix<6, C>| -> Matrix<6, C> { let time: Instant = begin + Duration::from_seconds(x); - let pos_gcrf: na::Vector3 = y.fixed_view::<3, 1>(0, 0).into(); - let vel_gcrf: na::Vector3 = y.fixed_view::<3, 1>(3, 0).into(); + let pos_gcrf: Vector3 = y.block::<3, 1>(0, 0); + let vel_gcrf: Vector3 = y.block::<3, 1>(3, 0); - let (qgcrf2itrf, sun_gcrf, moon_gcrf) = match interp.interp(&time) { - Ok(v) => v, - Err(e) => return Err(ODEError::YDotError(e.to_string())), - }; + let (qgcrf2itrf, sun_gcrf, moon_gcrf) = interp.interp(&time).unwrap(); let qitrf2gcrf = qgcrf2itrf.conjugate(); let pos_itrf = qgcrf2itrf * pos_gcrf; @@ -314,7 +320,7 @@ pub fn propagate( } if let Some(props) = satprops { - let ss: SimpleState = y.fixed_view::<6, 1>(0, 0).into(); + let ss: SimpleState = y.block::<6, 1>(0, 0); accel += solar_pressure_accel(&sun_gcrf, &pos_gcrf, &time, props, &ss); if pos_gcrf.norm() < 700.0e3 + consts::EARTH_RADIUS { let cd_a_over_m = props.cd_a_over_m(&time, &ss); @@ -339,38 +345,34 @@ pub fn propagate( if C == 1 { let mut dy = Matrix::<6, C>::zeros(); - dy.fixed_view_mut::<3, 1>(0, 0).copy_from(&vel_gcrf); - dy.fixed_view_mut::<3, 1>(3, 0).copy_from(&accel); - Ok(dy) + dy.set_block(0, 0, &vel_gcrf); + dy.set_block(3, 0, &accel); + dy } else if C == 7 { // Equation 7.42: dfdy * state_transition_matrix - let mut dfdy = StateType::<6>::zeros(); - dfdy.fixed_view_mut::<3, 3>(0, 3) - .copy_from(&na::Matrix3::::identity()); - dfdy.fixed_view_mut::<3, 3>(3, 0).copy_from(&dadr); - dfdy.fixed_view_mut::<3, 3>(3, 3).copy_from(&dadv); - let dphi = dfdy * y.fixed_view::<6, 6>(0, 1); - - let mut dy = Matrix::<6, C>::zero(); - dy.fixed_view_mut::<3, 1>(0, 0).copy_from(&vel_gcrf); - dy.fixed_view_mut::<3, 1>(3, 0).copy_from(&accel); - dy.fixed_view_mut::<6, 6>(0, 1).copy_from(&dphi); - Ok(dy) + let mut dfdy = Matrix::<6, 6>::zeros(); + dfdy.set_block(0, 3, &Matrix3::eye()); + dfdy.set_block(3, 0, &dadr); + dfdy.set_block(3, 3, &dadv); + let dphi = dfdy * y.block::<6, 6>(0, 1); + + let mut dy = Matrix::<6, C>::zeros(); + dy.set_block(0, 0, &vel_gcrf); + dy.set_block(3, 0, &accel); + dy.set_block(0, 1, &dphi); + dy } else { - ODEError::YDotError(PropagationError::InvalidStateColumns { c: C }.to_string()).into() + panic!("Invalid number of columns: {}", C); } }; - // Jacobian closure for RODAS4: computes the 6x6 ∂f/∂y matrix - let jac_fn = |x: f64, y: &Matrix<6, C>| -> ODEResult> { + // Jacobian closure for RODAS4: computes the 6x6 df/dy matrix + let jac_fn = |x: f64, y: &numeris::Vector| -> Matrix<6, 6> { let time: Instant = begin + Duration::from_seconds(x); - let pos_gcrf: na::Vector3 = y.fixed_view::<3, 1>(0, 0).into(); - let vel_gcrf: na::Vector3 = y.fixed_view::<3, 1>(3, 0).into(); + let pos_gcrf: Vector3 = y.block::<3, 1>(0, 0); + let vel_gcrf: Vector3 = y.block::<3, 1>(3, 0); - let (qgcrf2itrf, sun_gcrf, moon_gcrf) = match interp.interp(&time) { - Ok(v) => v, - Err(e) => return Err(ODEError::YDotError(e.to_string())), - }; + let (qgcrf2itrf, sun_gcrf, moon_gcrf) = interp.interp(&time).unwrap(); let qitrf2gcrf = qgcrf2itrf.conjugate(); let pos_itrf = qgcrf2itrf * pos_gcrf; @@ -393,7 +395,7 @@ pub fn propagate( } if let Some(props) = satprops { - let ss: SimpleState = y.fixed_view::<6, 1>(0, 0).into(); + let ss: SimpleState = y.block::<6, 1>(0, 0); if pos_gcrf.norm() < 700.0e3 + consts::EARTH_RADIUS { let cd_a_over_m = props.cd_a_over_m(&time, &ss); if cd_a_over_m > 1e-6 { @@ -408,35 +410,99 @@ pub fn propagate( } // Build the 6x6 Jacobian: df/dy where f = [vel; accel] - let mut dfdy = na::SMatrix::::zeros(); - dfdy.fixed_view_mut::<3, 3>(0, 3) - .copy_from(&na::Matrix3::::identity()); - dfdy.fixed_view_mut::<3, 3>(3, 0).copy_from(&dadr); - dfdy.fixed_view_mut::<3, 3>(3, 3).copy_from(&dadv); - Ok(dfdy) + let mut dfdy = Matrix::<6, 6>::zeros(); + dfdy.set_block(0, 3, &Matrix3::eye()); + dfdy.set_block(3, 0, &dadr); + dfdy.set_block(3, 3, &dadv); + dfdy }; - use crate::ode::solvers; use crate::orbitprop::Integrator; let res = match settings.integrator { Integrator::RKV98 => { - solvers::RKV98::integrate(0.0, x_end, state, &ydot, &odesettings) + ode::RKV98::integrate(0.0, x_end, state, &ydot, &odesettings) } Integrator::RKV98NoInterp => { - solvers::RKV98NoInterp::integrate(0.0, x_end, state, &ydot, &odesettings) + ode::RKV98NoInterp::integrate(0.0, x_end, state, &ydot, &odesettings) } Integrator::RKV87 => { - solvers::RKV87::integrate(0.0, x_end, state, &ydot, &odesettings) + ode::RKV87::integrate(0.0, x_end, state, &ydot, &odesettings) } Integrator::RKV65 => { - solvers::RKV65::integrate(0.0, x_end, state, &ydot, &odesettings) + ode::RKV65::integrate(0.0, x_end, state, &ydot, &odesettings) } Integrator::RKTS54 => { - solvers::RKTS54::integrate(0.0, x_end, state, &ydot, &odesettings) + ode::RKTS54::integrate(0.0, x_end, state, &ydot, &odesettings) } Integrator::RODAS4 => { - solvers::RODAS4::integrate(0.0, x_end, state, &ydot, &jac_fn, &odesettings) + // RODAS4 only supports SimpleState (6x1 = Vector) + // At this point C==1 is guaranteed (C==7 was rejected above) + let y0_vec: numeris::Vector = state.block::<6, 1>(0, 0); + let ydot_vec = |x: f64, y: &numeris::Vector| -> numeris::Vector { + let time: Instant = begin + Duration::from_seconds(x); + let pos_gcrf: Vector3 = y.block::<3, 1>(0, 0); + let vel_gcrf: Vector3 = y.block::<3, 1>(3, 0); + + let (qgcrf2itrf, sun_gcrf, moon_gcrf) = interp.interp(&time).unwrap(); + let qitrf2gcrf = qgcrf2itrf.conjugate(); + let pos_itrf = qgcrf2itrf * pos_gcrf; + + let mut accel = qitrf2gcrf + * gravity.accel( + &pos_itrf, + settings.gravity_degree as usize, + settings.gravity_order as usize, + ); + + if settings.use_sun_gravity { + accel += point_gravity(&pos_gcrf, &sun_gcrf, consts::MU_SUN); + } + if settings.use_moon_gravity { + accel += point_gravity(&pos_gcrf, &moon_gcrf, consts::MU_MOON); + } + + if let Some(props) = satprops { + let ss: SimpleState = y.block::<6, 1>(0, 0); + accel += solar_pressure_accel(&sun_gcrf, &pos_gcrf, &time, props, &ss); + if pos_gcrf.norm() < 700.0e3 + consts::EARTH_RADIUS { + let cd_a_over_m = props.cd_a_over_m(&time, &ss); + if cd_a_over_m > 1e-6 { + accel += drag_force( + &pos_gcrf, &pos_itrf, &vel_gcrf, &time, + cd_a_over_m, settings.use_spaceweather, + ); + } + } + } + + let mut dy = numeris::Vector::::zeros(); + dy.set_block(0, 0, &vel_gcrf); + dy.set_block(3, 0, &accel); + dy + }; + + let rosenbrock_res = ode::RODAS4::integrate( + 0.0, x_end, &y0_vec, ydot_vec, jac_fn, &odesettings, + ).map_err(PropagationError::ODEError)?; + + // Convert RosenbrockSolution to Solution + // Since C==1, this is essentially the same data + return Ok(PropagationResult { + time_begin: begin, + state_begin: *state, + time_end: end, + state_end: { + let mut s = Matrix::<6, C>::zeros(); + s.set_block(0, 0, &rosenbrock_res.y); + s + }, + accepted_steps: rosenbrock_res.accepted as u32, + rejected_steps: rosenbrock_res.rejected as u32, + num_eval: rosenbrock_res.evals as u32, + odesol: None, + integrator: settings.integrator, + }); } } .map_err(PropagationError::ODEError)?; @@ -446,9 +512,9 @@ pub fn propagate( state_begin: *state, time_end: end, state_end: res.y, - accepted_steps: res.naccept as u32, - rejected_steps: res.nreject as u32, - num_eval: res.nevals as u32, + accepted_steps: res.accepted as u32, + rejected_steps: res.rejected as u32, + num_eval: res.evals as u32, odesol: Some(res), integrator: settings.integrator, }) @@ -458,7 +524,6 @@ pub fn interp_propresult( res: &PropagationResult, time: &T, ) -> Result> { - use crate::ode::solvers; use crate::orbitprop::Integrator; let sol = res @@ -471,14 +536,17 @@ pub fn interp_propresult( return Ok(res.state_begin); } let x = (time - res.time_begin).as_seconds(); - Ok(match res.integrator { - Integrator::RKV98 => solvers::RKV98::interpolate(x, sol)?, - Integrator::RKV98NoInterp => solvers::RKV98NoInterp::interpolate(x, sol)?, - Integrator::RKV87 => solvers::RKV87::interpolate(x, sol)?, - Integrator::RKV65 => solvers::RKV65::interpolate(x, sol)?, - Integrator::RKTS54 => solvers::RKTS54::interpolate(x, sol)?, - Integrator::RODAS4 => solvers::RODAS4::interpolate(x, sol)?, - }) + let result = match res.integrator { + Integrator::RKV98 => ode::RKV98::interpolate(x, sol), + Integrator::RKV98NoInterp => ode::RKV98NoInterp::interpolate(x, sol), + Integrator::RKV87 => ode::RKV87::interpolate(x, sol), + Integrator::RKV65 => ode::RKV65::interpolate(x, sol), + Integrator::RKTS54 => ode::RKTS54::interpolate(x, sol), + Integrator::RODAS4 => { + return Err(PropagationError::NoDenseOutputInSolution.into()); + } + }; + Ok(result.map_err(PropagationError::ODEError)?) } #[cfg(test)] @@ -595,13 +663,11 @@ mod tests { let mut state: CovState = CovState::zeros(); let theta = PI / 6.0; - state[0] = consts::GEO_R * theta.cos(); - state[2] = consts::GEO_R * theta.sin(); - state[4] = (consts::MU_EARTH / consts::GEO_R).sqrt() * theta.cos(); - state[5] = (consts::MU_EARTH / consts::GEO_R).sqrt() * theta.sin(); - state - .fixed_view_mut::<6, 6>(0, 1) - .copy_from(&na::Matrix6::::identity()); + state[(0, 0)] = consts::GEO_R * theta.cos(); + state[(2, 0)] = consts::GEO_R * theta.sin(); + state[(4, 0)] = (consts::MU_EARTH / consts::GEO_R).sqrt() * theta.cos(); + state[(5, 0)] = (consts::MU_EARTH / consts::GEO_R).sqrt() * theta.sin(); + state.set_block(0, 1, &Matrix6::eye()); let settings = PropSettings { abs_error: 1.0e-9, @@ -611,14 +677,14 @@ mod tests { }; // Made-up small variations in the state - let dstate = na::vector![6.0, -10.0, 120.5, 0.1, 0.2, -0.3]; + let dstate = numeris::vector![6.0, -10.0, 120.5, 0.1, 0.2, -0.3]; // Propagate state (and state-transition matrix) let res = propagate(&state, &starttime, &stoptime, &settings, None)?; // Explicitly propagate state + dstate let res2 = propagate( - &(state.fixed_view::<6, 1>(0, 0) + dstate), + &(state.block::<6, 1>(0, 0) + dstate), &starttime, &stoptime, &settings, @@ -627,10 +693,10 @@ mod tests { // Difference in states from explicitly propagating with // "dstate" change in initial conditions - let dstate_prop = res2.state_end - res.state_end.fixed_view::<6, 1>(0, 0); + let dstate_prop = res2.state_end - res.state_end.block::<6, 1>(0, 0); // Difference in states estimated from state transition matrix - let dstate_phi = res.state_end.fixed_view::<6, 6>(0, 1) * dstate; + let dstate_phi = res.state_end.block::<6, 6>(0, 1) * dstate; for ix in 0..6_usize { assert!((dstate_prop[ix] - dstate_phi[ix]).abs() / dstate_prop[ix] < 1e-3); } @@ -654,19 +720,17 @@ mod tests { let mut state: CovState = CovState::zeros(); - let pgcrf = na::vector![3059573.85713792, 5855177.98848048, -7191.45042671]; - let vgcrf = na::vector![916.08123489, -468.22498656, 7700.48460839]; + let pgcrf = numeris::vector![3059573.85713792, 5855177.98848048, -7191.45042671]; + let vgcrf = numeris::vector![916.08123489, -468.22498656, 7700.48460839]; // 30-deg inclination - state.fixed_view_mut::<3, 1>(0, 0).copy_from(&pgcrf); - state.fixed_view_mut::<3, 1>(3, 0).copy_from(&vgcrf); - state - .fixed_view_mut::<6, 6>(0, 1) - .copy_from(&na::Matrix6::::identity()); + state.set_block(0, 0, &pgcrf); + state.set_block(3, 0, &vgcrf); + state.set_block(0, 1, &Matrix6::eye()); let settings = PropSettings { - abs_error: 1.0e-9, - rel_error: 1.0e-14, + abs_error: 1.0e-8, + rel_error: 1.0e-8, gravity_degree: 4, ..Default::default() }; @@ -674,7 +738,7 @@ mod tests { let satprops: SatPropertiesStatic = SatPropertiesStatic::new(2.0 * 0.3 * 0.1 / 5.0, 0.0); // Made-up small variations in the state - let dstate = na::vector![2.0, -4.0, 20.5, 0.05, 0.02, -0.01]; + let dstate = numeris::vector![2.0, -4.0, 20.5, 0.05, 0.02, -0.01]; // Propagate state (and state-transition matrix) @@ -682,7 +746,7 @@ mod tests { // Explicitly propagate state + dstate let res2 = propagate( - &(state.fixed_view::<6, 1>(0, 0) + dstate), + &(state.block::<6, 1>(0, 0) + dstate), &starttime, &stoptime, &settings, @@ -691,9 +755,9 @@ mod tests { // Difference in states from explicitly propagating with // "dstate" change in initial conditions - let dstate_prop = res2.state_end - res.state_end.fixed_view::<6, 1>(0, 0); + let dstate_prop = res2.state_end - res.state_end.block::<6, 1>(0, 0); - let dstate_phi = res.state_end.fixed_view::<6, 6>(0, 1) * dstate; + let dstate_phi = res.state_end.block::<6, 6>(0, 1) * dstate; // Are differences within 1%? for ix in 0..6_usize { @@ -742,24 +806,24 @@ mod tests { let satnum: usize = 20; let satstr = format!("PG{}", satnum); - let pitrf: Vec> = io::BufReader::new(file) + let pitrf: Vec = io::BufReader::new(file) .lines() .filter(|x| { let rline = &x.as_ref().unwrap()[0..4]; rline == satstr }) - .map(|rline| -> Result> { + .map(|rline| -> Result { let line = rline.unwrap(); let lvals: Vec<&str> = line.split_whitespace().collect(); let px: f64 = lvals[1].parse()?; let py: f64 = lvals[2].parse()?; let pz: f64 = lvals[3].parse()?; - Ok(na::vector![px, py, pz] * 1.0e3) + Ok(numeris::vector![px, py, pz] * 1.0e3) }) - .collect::>>>()?; + .collect::>>()?; assert!(times.len() == pitrf.len()); - let pgcrf: Vec> = pitrf + let pgcrf: Vec = pitrf .iter() .enumerate() .map(|(idx, p)| { @@ -768,14 +832,14 @@ mod tests { }) .collect(); - let v0 = na::vector![ + let v0 = numeris::vector![ 2.47130562e+03, 2.94682753e+03, -5.34172176e+02, - 2.32565692e-02 + 2.32565692e-02, ]; - let state0 = na::vector![pgcrf[0][0], pgcrf[0][1], pgcrf[0][2], v0[0], v0[1], v0[2]]; + let state0 = numeris::vector![pgcrf[0][0], pgcrf[0][1], pgcrf[0][2], v0[0], v0[1], v0[2]]; let satprops: SatPropertiesStatic = SatPropertiesStatic::new(0.0, v0[3]); let settings = PropSettings { @@ -842,10 +906,10 @@ mod tests { let res_no_both = propagate(&state, &starttime, &stoptime, &settings_no_both, None)?; // All results should be different from each other - let pos_all = res_all.state_end.fixed_view::<3, 1>(0, 0); - let pos_no_sun = res_no_sun.state_end.fixed_view::<3, 1>(0, 0); - let pos_no_moon = res_no_moon.state_end.fixed_view::<3, 1>(0, 0); - let pos_no_both = res_no_both.state_end.fixed_view::<3, 1>(0, 0); + let pos_all = res_all.state_end.block::<3, 1>(0, 0); + let pos_no_sun = res_no_sun.state_end.block::<3, 1>(0, 0); + let pos_no_moon = res_no_moon.state_end.block::<3, 1>(0, 0); + let pos_no_both = res_no_both.state_end.block::<3, 1>(0, 0); let diff_sun = (pos_all - pos_no_sun).norm(); let diff_moon = (pos_all - pos_no_moon).norm(); @@ -901,8 +965,8 @@ mod tests { let res_full = propagate(&state, &starttime, &stoptime, &settings_full, None)?; let res_zonal = propagate(&state, &starttime, &stoptime, &settings_zonal, None)?; - let pos_full = res_full.state_end.fixed_view::<3, 1>(0, 0); - let pos_zonal = res_zonal.state_end.fixed_view::<3, 1>(0, 0); + let pos_full = res_full.state_end.block::<3, 1>(0, 0); + let pos_zonal = res_zonal.state_end.block::<3, 1>(0, 0); let diff = (pos_full - pos_zonal).norm(); assert!( @@ -922,13 +986,11 @@ mod tests { let mut state: CovState = CovState::zeros(); let theta = PI / 6.0; - state[0] = consts::GEO_R * theta.cos(); - state[2] = consts::GEO_R * theta.sin(); - state[4] = (consts::MU_EARTH / consts::GEO_R).sqrt() * theta.cos(); - state[5] = (consts::MU_EARTH / consts::GEO_R).sqrt() * theta.sin(); - state - .fixed_view_mut::<6, 6>(0, 1) - .copy_from(&na::Matrix6::::identity()); + state[(0, 0)] = consts::GEO_R * theta.cos(); + state[(2, 0)] = consts::GEO_R * theta.sin(); + state[(4, 0)] = (consts::MU_EARTH / consts::GEO_R).sqrt() * theta.cos(); + state[(5, 0)] = (consts::MU_EARTH / consts::GEO_R).sqrt() * theta.sin(); + state.set_block(0, 1, &Matrix6::eye()); let settings = PropSettings { abs_error: 1.0e-9, @@ -939,19 +1001,19 @@ mod tests { ..Default::default() }; - let dstate = na::vector![6.0, -10.0, 120.5, 0.1, 0.2, -0.3]; + let dstate = numeris::vector![6.0, -10.0, 120.5, 0.1, 0.2, -0.3]; let res = propagate(&state, &starttime, &stoptime, &settings, None)?; let res2 = propagate( - &(state.fixed_view::<6, 1>(0, 0) + dstate), + &(state.block::<6, 1>(0, 0) + dstate), &starttime, &stoptime, &settings, None, )?; - let dstate_prop = res2.state_end - res.state_end.fixed_view::<6, 1>(0, 0); - let dstate_phi = res.state_end.fixed_view::<6, 6>(0, 1) * dstate; + let dstate_prop = res2.state_end - res.state_end.block::<6, 1>(0, 0); + let dstate_phi = res.state_end.block::<6, 6>(0, 1) * dstate; for ix in 0..6_usize { assert!( @@ -978,7 +1040,7 @@ mod tests { state[0] = r; state[4] = (consts::MU_EARTH / r).sqrt(); - // Typical small satellite: Cd=2.2, A=0.01 m², mass=1 kg + // Typical small satellite: Cd=2.2, A=0.01 m^2, mass=1 kg let satprops = SatPropertiesStatic::new(2.2 * 0.01 / 1.0, 0.0); let settings_rodas4 = PropSettings { @@ -1004,8 +1066,8 @@ mod tests { )?; // Position agreement within 100 m over 2 hours at this altitude - let pos_diff = (res_rodas4.state_end.fixed_view::<3, 1>(0, 0) - - res_rkv98.state_end.fixed_view::<3, 1>(0, 0)) + let pos_diff = (res_rodas4.state_end.block::<3, 1>(0, 0) + - res_rkv98.state_end.block::<3, 1>(0, 0)) .norm(); assert!( pos_diff < 100.0, @@ -1023,11 +1085,9 @@ mod tests { let stoptime = starttime + Duration::from_hours(1.0); let mut state: CovState = CovState::zeros(); - state[0] = consts::EARTH_RADIUS + 400.0e3; - state[4] = (consts::MU_EARTH / state[0]).sqrt(); - state - .fixed_view_mut::<6, 6>(0, 1) - .copy_from(&na::Matrix6::::identity()); + state[(0, 0)] = consts::EARTH_RADIUS + 400.0e3; + state[(4, 0)] = (consts::MU_EARTH / state[(0, 0)]).sqrt(); + state.set_block(0, 1, &Matrix6::eye()); let settings = PropSettings { integrator: crate::orbitprop::Integrator::RODAS4, diff --git a/src/orbitprop/satstate.rs b/src/orbitprop/satstate.rs index 7a0a96d..549be9f 100644 --- a/src/orbitprop/satstate.rs +++ b/src/orbitprop/satstate.rs @@ -1,13 +1,13 @@ -use nalgebra as na; - use crate::orbitprop; use crate::orbitprop::PropSettings; use crate::Instant; use crate::TimeLike; +use crate::mathtypes::*; + use anyhow::Result; -type PVCovType = na::SMatrix; +type PVCovType = Matrix<6, 6>; #[derive(Clone, Debug)] #[allow(clippy::large_enum_variant)] @@ -36,25 +36,25 @@ pub enum StateCov { #[derive(Clone, Debug)] pub struct SatState { pub time: Instant, - pub pv: na::Vector6, + pub pv: Vector6, pub cov: StateCov, } impl SatState { - pub fn from_pv(time: &T, pos: &na::Vector3, vel: &na::Vector3) -> Self { + pub fn from_pv(time: &T, pos: &Vector3, vel: &Vector3) -> Self { Self { time: time.as_instant(), - pv: na::vector![pos[0], pos[1], pos[2], vel[0], vel[1], vel[2]], + pv: numeris::vector![pos[0], pos[1], pos[2], vel[0], vel[1], vel[2]], cov: StateCov::None, } } - pub fn pos_gcrf(&self) -> na::Vector3 { - self.pv.fixed_view::<3, 1>(0, 0).into() + pub fn pos_gcrf(&self) -> Vector3 { + self.pv.block::<3, 1>(0, 0) } - pub fn vel_gcrf(&self) -> na::Vector3 { - self.pv.fixed_view::<3, 1>(3, 0).into() + pub fn vel_gcrf(&self) -> Vector3 { + self.pv.block::<3, 1>(3, 0) } /// Set covariance @@ -76,14 +76,16 @@ impl SatState { /// z axis = -r (nadir) /// y axis = -h (h = p cross v) /// x axis such that x cross y = z - pub fn qgcrf2lvlh(&self) -> na::UnitQuaternion { - type Quat = na::UnitQuaternion; - + pub fn qgcrf2lvlh(&self) -> Quaternion { let p = self.pos_gcrf(); let v = self.vel_gcrf(); let h = p.cross(&v); - let q1 = Quat::rotation_between(&(-1.0 * p), &na::Vector3::z_axis()).unwrap(); - let q2 = Quat::rotation_between(&(-1.0 * (q1 * h)), &na::Vector3::y_axis()).unwrap(); + let neg_p = p * -1.0; + let neg_h_dir = numeris::vector![0.0, 0.0, 1.0]; + let q1 = Quaternion::rotation_between(neg_p, neg_h_dir); + let rotated_h = q1 * (h * -1.0); + let y_axis = numeris::vector![0.0, 1.0, 0.0]; + let q2 = Quaternion::rotation_between(rotated_h, y_axis); q2 * q1 } @@ -98,15 +100,16 @@ impl SatState { /// /// * `sigma_lvlh` - 3-vector with 1-sigma position uncertainty in LVLH frame /// - pub fn set_lvlh_pos_uncertainty(&mut self, sigma_lvlh: &na::Vector3) { + pub fn set_lvlh_pos_uncertainty(&mut self, sigma_lvlh: &Vector3) { let dcm = self.qgcrf2lvlh().to_rotation_matrix(); - let mut pcov = na::Matrix3::::zeros(); - pcov.set_diagonal(&sigma_lvlh.map(|x| x * x)); + let mut pcov = Matrix3::zeros(); + pcov[(0, 0)] = sigma_lvlh[0] * sigma_lvlh[0]; + pcov[(1, 1)] = sigma_lvlh[1] * sigma_lvlh[1]; + pcov[(2, 2)] = sigma_lvlh[2] * sigma_lvlh[2]; - let mut m = na::Matrix6::::zeros(); - m.fixed_view_mut::<3, 3>(0, 0) - .copy_from(&(dcm.transpose() * pcov * dcm)); + let mut m = Matrix6::zeros(); + m.set_block(0, 0, &(dcm.transpose() * pcov * dcm)); self.cov = StateCov::PVCov(m); } @@ -117,15 +120,16 @@ impl SatState { /// /// * `sigma_lvlh` - 3-vector with 1-sigma velocity uncertainty in LVLH frame /// - pub fn set_lvlh_vel_uncertainty(&mut self, sigma_lvlh: &na::Vector3) { + pub fn set_lvlh_vel_uncertainty(&mut self, sigma_lvlh: &Vector3) { let dcm = self.qgcrf2lvlh().to_rotation_matrix(); - let mut pcov = na::Matrix3::::zeros(); - pcov.set_diagonal(&sigma_lvlh.map(|x| x * x)); + let mut pcov = Matrix3::zeros(); + pcov[(0, 0)] = sigma_lvlh[0] * sigma_lvlh[0]; + pcov[(1, 1)] = sigma_lvlh[1] * sigma_lvlh[1]; + pcov[(2, 2)] = sigma_lvlh[2] * sigma_lvlh[2]; - let mut m = na::Matrix6::::zeros(); - m.fixed_view_mut::<3, 3>(3, 3) - .copy_from(&(dcm.transpose() * pcov * dcm)); + let mut m = Matrix6::zeros(); + m.set_block(3, 3, &(dcm.transpose() * pcov * dcm)); self.cov = StateCov::PVCov(m); } @@ -136,16 +140,14 @@ impl SatState { /// /// * `sigma_gcrf` - 3-vector with 1-sigma position uncertainty in GCRF frame /// - pub fn set_gcrf_pos_uncertainty(&mut self, sigma_cart: &na::Vector3) { + pub fn set_gcrf_pos_uncertainty(&mut self, sigma_cart: &Vector3) { self.cov = StateCov::PVCov({ let mut m = PVCovType::zeros(); - let mut diag = na::Vector3::::zeros(); - diag[0] = sigma_cart[0] * sigma_cart[0]; - diag[1] = sigma_cart[1] * sigma_cart[1]; - diag[2] = sigma_cart[2] * sigma_cart[2]; - let mut pcov = na::Matrix3::::zeros(); - pcov.set_diagonal(&diag); - m.fixed_view_mut::<3, 3>(0, 0).copy_from(&pcov); + let mut pcov = Matrix3::zeros(); + pcov[(0, 0)] = sigma_cart[0] * sigma_cart[0]; + pcov[(1, 1)] = sigma_cart[1] * sigma_cart[1]; + pcov[(2, 2)] = sigma_cart[2] * sigma_cart[2]; + m.set_block(0, 0, &pcov); m }) } @@ -157,16 +159,14 @@ impl SatState { /// /// * `sigma_gcrf` - 3-vector with 1-sigma velocity uncertainty in GCRF frame /// - pub fn set_gcrf_vel_uncertainty(&mut self, sigma_cart: &na::Vector3) { + pub fn set_gcrf_vel_uncertainty(&mut self, sigma_cart: &Vector3) { self.cov = StateCov::PVCov({ let mut m = PVCovType::zeros(); - let mut diag = na::Vector3::::zeros(); - diag[0] = sigma_cart[0] * sigma_cart[0]; - diag[1] = sigma_cart[1] * sigma_cart[1]; - diag[2] = sigma_cart[2] * sigma_cart[2]; - let mut pcov = na::Matrix3::::zeros(); - pcov.set_diagonal(&diag); - m.fixed_view_mut::<3, 3>(3, 3).copy_from(&pcov); + let mut pcov = Matrix3::zeros(); + pcov[(0, 0)] = sigma_cart[0] * sigma_cart[0]; + pcov[(1, 1)] = sigma_cart[1] * sigma_cart[1]; + pcov[(2, 2)] = sigma_cart[2] * sigma_cart[2]; + m.set_block(3, 3, &pcov); m }) } @@ -210,27 +210,25 @@ impl SatState { } // Compute state transition matrix & propagate covariance as well StateCov::PVCov(cov) => { - let mut state = na::SMatrix::::zeros(); + let mut state = Matrix::<6, 7>::zeros(); // First row of state is 6-element position & velocity - state.fixed_view_mut::<6, 1>(0, 0).copy_from(&self.pv); + state.set_block(0, 0, &self.pv); // See equation 7.42 of Montenbruck & Gill // State transition matrix initializes to identity matrix // State transition matrix is columns 1-7 of state (0-based) - state - .fixed_view_mut::<6, 6>(0, 1) - .copy_from(&na::Matrix6::::identity()); + state.set_block(0, 1, &Matrix6::eye()); // Propagate let res = orbitprop::propagate(&state, &self.time, &time, settings, None)?; Ok(Self { time, - pv: res.state_end.fixed_view::<6, 1>(0, 0).into(), + pv: res.state_end.block::<6, 1>(0, 0), cov: { // Extract state transition matrix from the propagated state - let phi = res.state_end.fixed_view::<6, 6>(0, 1); + let phi = res.state_end.block::<6, 6>(0, 1); // Evolve the covariance StateCov::PVCov(phi * cov * phi.transpose()) }, @@ -251,11 +249,11 @@ impl std::fmt::Display for SatState { ); match self.cov { StateCov::None => {} - StateCov::PVCov(cov) => { + StateCov::PVCov(ref cov) => { s1.push_str( format!( r#" - Covariance: {cov:+8.2e}"# + Covariance: {cov:?}"# ) .as_str(), ); @@ -269,27 +267,29 @@ impl std::fmt::Display for SatState { mod test { use super::*; use crate::consts; - use approx::{assert_abs_diff_eq, assert_relative_eq}; #[test] fn test_qgcrf2lvlh() -> Result<()> { let satstate = SatState::from_pv( &Instant::from_datetime(2015, 3, 20, 0, 0, 0.0).unwrap(), - &na::vector![consts::GEO_R, 0.0, 0.0], - &na::vector![0.0, (consts::MU_EARTH / consts::GEO_R).sqrt(), 0.0], + &numeris::vector![consts::GEO_R, 0.0, 0.0], + &numeris::vector![0.0, (consts::MU_EARTH / consts::GEO_R).sqrt(), 0.0], ); let state2 = satstate.propagate(&(satstate.time + crate::Duration::from_hours(3.56)), None)?; - let rz = -1.0 / state2.pos_gcrf().norm() * (state2.qgcrf2lvlh() * state2.pos_gcrf()); + let rz = (state2.qgcrf2lvlh() * state2.pos_gcrf()) * (-1.0 / state2.pos_gcrf().norm()); let h = state2.pos_gcrf().cross(&state2.vel_gcrf()); - let ry = -1.0 / h.norm() * (state2.qgcrf2lvlh() * h); - let rx = 1.0 / state2.vel_gcrf().norm() * (state2.qgcrf2lvlh() * state2.vel_gcrf()); + let ry = (state2.qgcrf2lvlh() * h) * (-1.0 / h.norm()); + let rx = (state2.qgcrf2lvlh() * state2.vel_gcrf()) * (1.0 / state2.vel_gcrf().norm()); - assert_relative_eq!(rz, na::Vector3::z_axis(), epsilon = 1.0e-6); - assert_relative_eq!(ry, na::Vector3::y_axis(), epsilon = 1.0e-6); - assert_relative_eq!(rx, na::Vector3::x_axis(), epsilon = 1.0e-4); + let z_axis = numeris::vector![0.0, 0.0, 1.0]; + let y_axis = numeris::vector![0.0, 1.0, 0.0]; + let x_axis = numeris::vector![1.0, 0.0, 0.0]; + assert!((rz - z_axis).norm() < 1.0e-6); + assert!((ry - y_axis).norm() < 1.0e-6); + assert!((rx - x_axis).norm() < 1.0e-4); Ok(()) } @@ -298,11 +298,11 @@ mod test { fn test_satstate() -> Result<()> { let mut satstate = SatState::from_pv( &Instant::from_datetime(2015, 3, 20, 0, 0, 0.0).unwrap(), - &na::vector![consts::GEO_R, 0.0, 0.0], - &na::vector![0.0, (consts::MU_EARTH / consts::GEO_R).sqrt(), 0.0], + &numeris::vector![consts::GEO_R, 0.0, 0.0], + &numeris::vector![0.0, (consts::MU_EARTH / consts::GEO_R).sqrt(), 0.0], ); - satstate.set_lvlh_pos_uncertainty(&na::vector![1.0, 1.0, 1.0]); - satstate.set_lvlh_vel_uncertainty(&na::vector![0.01, 0.02, 0.03]); + satstate.set_lvlh_pos_uncertainty(&numeris::vector![1.0, 1.0, 1.0]); + satstate.set_lvlh_vel_uncertainty(&numeris::vector![0.01, 0.02, 0.03]); let state2 = satstate.propagate(&(satstate.time + crate::Duration::from_days(0.5)), None)?; @@ -311,8 +311,8 @@ mod test { let state0 = state2.propagate(&satstate.time, None)?; // Check that propagating backwards in time results in the original state - assert_abs_diff_eq!(satstate.pos_gcrf(), state0.pos_gcrf(), epsilon = 0.1); - assert_abs_diff_eq!(satstate.vel_gcrf(), state0.vel_gcrf(), epsilon = 0.001); + assert!((satstate.pos_gcrf() - state0.pos_gcrf()).norm() < 0.1); + assert!((satstate.vel_gcrf() - state0.vel_gcrf()).norm() < 0.001); let cov1 = match satstate.cov() { StateCov::PVCov(v) => v, StateCov::None => anyhow::bail!("cov is not none"), @@ -321,7 +321,7 @@ mod test { StateCov::PVCov(v) => v, StateCov::None => anyhow::bail!("cov is not none"), }; - assert_abs_diff_eq!(cov1, cov2, epsilon = 0.001); + assert!((cov1 - cov2).norm_inf() < 0.001); Ok(()) } @@ -330,10 +330,10 @@ mod test { fn test_satcov() -> Result<()> { let mut satstate = SatState::from_pv( &Instant::from_datetime(2015, 3, 20, 0, 0, 0.0).unwrap(), - &na::vector![consts::GEO_R, 0.0, 0.0], - &na::vector![0.0, (consts::MU_EARTH / consts::GEO_R).sqrt(), 0.0], + &numeris::vector![consts::GEO_R, 0.0, 0.0], + &numeris::vector![0.0, (consts::MU_EARTH / consts::GEO_R).sqrt(), 0.0], ); - satstate.set_lvlh_pos_uncertainty(&na::vector![1.0, 1.0, 1.0]); + satstate.set_lvlh_pos_uncertainty(&numeris::vector![1.0, 1.0, 1.0]); let _state2 = satstate.propagate(&(satstate.time + crate::Duration::from_days(1.0)), None)?; @@ -346,16 +346,16 @@ mod test { // Test that propagating with dt=0 returns the same state let satstate = SatState::from_pv( &Instant::from_datetime(2015, 3, 20, 0, 0, 0.0).unwrap(), - &na::vector![consts::GEO_R, 0.0, 0.0], - &na::vector![0.0, (consts::MU_EARTH / consts::GEO_R).sqrt(), 0.0], + &numeris::vector![consts::GEO_R, 0.0, 0.0], + &numeris::vector![0.0, (consts::MU_EARTH / consts::GEO_R).sqrt(), 0.0], ); // Propagate to the same time (zero duration) let state2 = satstate.propagate(&satstate.time, None)?; // Verify the state is unchanged - assert_abs_diff_eq!(satstate.pos_gcrf(), state2.pos_gcrf(), epsilon = 1e-15); - assert_abs_diff_eq!(satstate.vel_gcrf(), state2.vel_gcrf(), epsilon = 1e-15); + assert!((satstate.pos_gcrf() - state2.pos_gcrf()).norm() < 1e-15); + assert!((satstate.vel_gcrf() - state2.vel_gcrf()).norm() < 1e-15); assert_eq!(satstate.time, state2.time); Ok(()) @@ -366,17 +366,17 @@ mod test { // Test that propagating with dt=0 returns the same state including covariance let mut satstate = SatState::from_pv( &Instant::from_datetime(2015, 3, 20, 0, 0, 0.0).unwrap(), - &na::vector![consts::GEO_R, 0.0, 0.0], - &na::vector![0.0, (consts::MU_EARTH / consts::GEO_R).sqrt(), 0.0], + &numeris::vector![consts::GEO_R, 0.0, 0.0], + &numeris::vector![0.0, (consts::MU_EARTH / consts::GEO_R).sqrt(), 0.0], ); - satstate.set_lvlh_pos_uncertainty(&na::vector![1.0, 1.0, 1.0]); + satstate.set_lvlh_pos_uncertainty(&numeris::vector![1.0, 1.0, 1.0]); // Propagate to the same time (zero duration) let state2 = satstate.propagate(&satstate.time, None)?; // Verify the state is unchanged - assert_abs_diff_eq!(satstate.pos_gcrf(), state2.pos_gcrf(), epsilon = 1e-15); - assert_abs_diff_eq!(satstate.vel_gcrf(), state2.vel_gcrf(), epsilon = 1e-15); + assert!((satstate.pos_gcrf() - state2.pos_gcrf()).norm() < 1e-15); + assert!((satstate.vel_gcrf() - state2.vel_gcrf()).norm() < 1e-15); // Verify covariance is unchanged let cov1 = match satstate.cov() { @@ -387,7 +387,7 @@ mod test { StateCov::PVCov(v) => v, StateCov::None => anyhow::bail!("cov is not none"), }; - assert_abs_diff_eq!(cov1, cov2, epsilon = 1e-15); + assert!((cov1 - cov2).norm_inf() < 1e-15); Ok(()) } diff --git a/src/sgp4/sgp4_impl.rs b/src/sgp4/sgp4_impl.rs index 8736486..a35d83e 100644 --- a/src/sgp4/sgp4_impl.rs +++ b/src/sgp4/sgp4_impl.rs @@ -2,7 +2,7 @@ use super::sgp4_lowlevel::sgp4_lowlevel; // propagator use super::sgp4init::sgp4init; use crate::TimeLike; -use nalgebra::{Const, Dyn, OMatrix}; +use crate::mathtypes::DMatrix; use thiserror::Error; @@ -51,11 +51,9 @@ impl From for i32 { } } -type StateArr = OMatrix, Dyn>; - pub struct SGP4State { - pub pos: StateArr, - pub vel: StateArr, + pub pos: DMatrix, + pub vel: DMatrix, pub errcode: Vec, } @@ -106,7 +104,6 @@ use super::{GravConst, OpsMode, SGP4Source}; /// use satkit::sgp4::{sgp4, GravConst, OpsMode}; /// use satkit::frametransform::qteme2itrf; /// use satkit::itrfcoord::ITRFCoord; -/// use nalgebra as na; /// /// let line0: &str = "0 INTELSAT 902"; /// let line1: &str = "1 26900U 01039A 06106.74503247 .00000045 00000-0 10000-3 0 8290"; @@ -123,7 +120,9 @@ use super::{GravConst, OpsMode, SGP4Source}; /// &[tm] /// ).unwrap(); /// -/// let pitrf = qteme2itrf(&tm).to_rotation_matrix() * result.pos; +/// // rotate position to ITRF and create ITRFCoord +/// let pos = numeris::vector![result.pos[(0,0)], result.pos[(1,0)], result.pos[(2,0)]]; +/// let pitrf = qteme2itrf(&tm) * pos; /// let itrf = ITRFCoord::from_slice(pitrf.as_slice()).unwrap(); /// println!("Satellite position is: {}", itrf); /// @@ -176,7 +175,6 @@ pub fn sgp4(sgp4source: &mut impl SGP4Source, tm: &[T]) -> anyhow:: /// use satkit::sgp4::{sgp4_full, GravConst, OpsMode}; /// use satkit::frametransform::qteme2itrf; /// use satkit::itrfcoord::ITRFCoord; -/// use nalgebra as na; /// /// let line0: &str = "0 INTELSAT 902"; /// let line1: &str = "1 26900U 01039A 06106.74503247 .00000045 00000-0 10000-3 0 8290"; @@ -195,7 +193,9 @@ pub fn sgp4(sgp4source: &mut impl SGP4Source, tm: &[T]) -> anyhow:: /// OpsMode::IMPROVED /// ).unwrap(); /// -/// let pitrf = qteme2itrf(&tm).to_rotation_matrix() * result.pos; +/// // rotate position to ITRF and create ITRFCoord +/// let pos = numeris::vector![result.pos[(0,0)], result.pos[(1,0)], result.pos[(2,0)]]; +/// let pitrf = qteme2itrf(&tm) * pos; /// let itrf = ITRFCoord::from_slice(pitrf.as_slice()).unwrap(); /// println!("Satellite position is: {}", itrf); /// @@ -231,8 +231,8 @@ pub fn sgp4_full( let s = sgp4source.satrec_mut().as_mut().expect("satrec initialized"); let n = tm.len(); - let mut rarr = StateArr::zeros(n); - let mut varr = StateArr::zeros(n); + let mut rarr = DMatrix::::zeros(3, n); + let mut varr = DMatrix::::zeros(3, n); let mut earr = Vec::::with_capacity(n); for (pos, thetime) in tm.iter().enumerate() { @@ -240,8 +240,10 @@ pub fn sgp4_full( match sgp4_lowlevel(s, tsince) { Ok((r, v)) => { - rarr.index_mut((.., pos)).copy_from_slice(&r); - varr.index_mut((.., pos)).copy_from_slice(&v); + for i in 0..3 { + rarr[(i, pos)] = r[i]; + varr[(i, pos)] = v[i]; + } earr.push(SGP4Error::SGP4Success) } Err(e) => earr.push(e.into()), @@ -359,9 +361,9 @@ mod tests { maxvelerr = 1.0e-2; } let poserr = - (states.pos[idx].mul_add(1.0e-3, -testvec[idx + 1]) / testvec[idx + 1]).abs(); + (states.pos[(idx, 0)].mul_add(1.0e-3, -testvec[idx + 1]) / testvec[idx + 1]).abs(); let velerr = - (states.vel[idx].mul_add(1.0e-3, -testvec[idx + 4]) / testvec[idx + 4]).abs(); + (states.vel[(idx, 0)].mul_add(1.0e-3, -testvec[idx + 4]) / testvec[idx + 4]).abs(); assert!(poserr < maxposerr); assert!(velerr < maxvelerr); } diff --git a/src/solar_cycle_forecast.rs b/src/solar_cycle_forecast.rs index 9d1022a..393f365 100644 --- a/src/solar_cycle_forecast.rs +++ b/src/solar_cycle_forecast.rs @@ -14,7 +14,7 @@ use anyhow::{bail, Result}; use std::path::PathBuf; use std::sync::RwLock; -use once_cell::sync::OnceCell; +use std::sync::OnceLock; #[derive(Debug, Clone)] pub struct ForecastRecord { @@ -72,7 +72,7 @@ fn parse_forecast_json(contents: &str) -> Result> { } fn forecast_singleton() -> &'static RwLock>> { - static INSTANCE: OnceCell>>> = OnceCell::new(); + static INSTANCE: OnceLock>>> = OnceLock::new(); INSTANCE.get_or_init(|| RwLock::new(load_forecast().ok())) } diff --git a/src/spaceweather.rs b/src/spaceweather.rs index 666cd16..46fe857 100644 --- a/src/spaceweather.rs +++ b/src/spaceweather.rs @@ -10,7 +10,7 @@ use anyhow::{bail, Context, Result}; use std::sync::RwLock; -use once_cell::sync::OnceCell; +use std::sync::OnceLock; #[derive(Debug, Clone)] pub struct SpaceWeatherRecord { @@ -127,7 +127,7 @@ fn load_space_weather_csv() -> Result> { } fn space_weather_singleton() -> &'static RwLock>> { - static INSTANCE: OnceCell>>> = OnceCell::new(); + static INSTANCE: OnceLock>>> = OnceLock::new(); INSTANCE.get_or_init(|| RwLock::new(load_space_weather_csv())) } diff --git a/src/tle/fitting.rs b/src/tle/fitting.rs index e05dbac..0f983e2 100644 --- a/src/tle/fitting.rs +++ b/src/tle/fitting.rs @@ -3,8 +3,6 @@ use super::TLE; use crate::Instant; use anyhow::{bail, Context, Result}; -use crate::mathtypes::*; - use rmpfit::{MPFitter, MPResult}; struct Problem<'a> { @@ -121,7 +119,7 @@ impl TLE { /// let r0 = satkit::consts::EARTH_RADIUS + altitude; /// let v0 = (satkit::consts::MU_EARTH / r0).sqrt(); /// let inc: f64 = 97.0_f64.to_radians(); - /// let state0 = nalgebra::vector![r0, 0.0, 0.0, 0.0, v0 * inc.cos(), v0 * inc.sin()]; + /// let state0 = numeris::vector![r0, 0.0, 0.0, 0.0, v0 * inc.cos(), v0 * inc.sin()]; /// let time0 = satkit::Instant::from_datetime(2016, 5, 16, 12, 0, 0.0).unwrap(); /// /// // High-fidelity orbit propagation settings @@ -209,16 +207,16 @@ impl TLE { .enumerate() .map(|(i, time)| { let q = crate::frametransform::qteme2gcrf(time).conjugate(); - let p = q.transform_vector(&nalgebra::vector![ + let p = q * numeris::vector![ states_gcrf[i][0], states_gcrf[i][1], - states_gcrf[i][2] - ]); - let v = q.transform_vector(&nalgebra::vector![ + states_gcrf[i][2], + ]; + let v = q * numeris::vector![ states_gcrf[i][3], states_gcrf[i][4], - states_gcrf[i][5] - ]); + states_gcrf[i][5], + ]; [p[0], p[1], p[2], v[0], v[1], v[2]] }) .collect::>(); @@ -227,8 +225,8 @@ impl TLE { let closest_state = states_teme[closest_index]; // Kepler representation let mut kepler = crate::kepler::Kepler::from_pv( - Vector3::from_column_slice(&closest_state[..3]), - Vector3::from_column_slice(&closest_state[3..]), + numeris::vector![closest_state[0], closest_state[1], closest_state[2]], + numeris::vector![closest_state[3], closest_state[4], closest_state[5]], ) .context("Could not convert state to Keplerian elements")?; @@ -271,7 +269,7 @@ mod tests { let r0 = crate::consts::GEO_R; let v0 = (crate::consts::MU_EARTH / r0).sqrt(); let inc: f64 = 15.0_f64.to_radians(); - let state0 = nalgebra::vector![r0, 0.0, 0.0, 0.0, v0 * inc.cos(), v0 * inc.sin()]; + let state0 = numeris::vector![r0, 0.0, 0.0, 0.0, v0 * inc.cos(), v0 * inc.sin()]; let time0: Instant = Instant::from_datetime(2022, 5, 16, 12, 0, 0.0)?; let settings = crate::orbitprop::PropSettings { @@ -308,7 +306,7 @@ mod tests { let r0 = crate::consts::EARTH_RADIUS + altitude; let v0 = (crate::consts::MU_EARTH / r0).sqrt(); let inc: f64 = 97.0_f64.to_radians(); - let state0 = nalgebra::vector![r0, 0.0, 0.0, 0.0, v0 * inc.cos(), v0 * inc.sin()]; + let state0 = numeris::vector![r0, 0.0, 0.0, 0.0, v0 * inc.cos(), v0 * inc.sin()]; let time0: Instant = Instant::from_datetime(2016, 5, 16, 12, 0, 0.0)?; let settings = crate::orbitprop::PropSettings { diff --git a/src/tle/mod.rs b/src/tle/mod.rs index d7ebe4f..6c15abc 100644 --- a/src/tle/mod.rs +++ b/src/tle/mod.rs @@ -54,12 +54,13 @@ const ALPHA5_MATCHING: &str = "ABCDEFGHJKLMNPQRSTUVWXYZ"; /// // Since pTEME is a 3xN array where N is the number of times /// // (we are just using a single time) /// // we need to convert to a fixed matrix to rotate -/// let pITRF = frametransform::qteme2itrf(&tm) * states.pos.fixed_view::<3,1>(0,0); +/// let pos = numeris::vector![states.pos[(0,0)], states.pos[(1,0)], states.pos[(2,0)]]; +/// let pITRF = frametransform::qteme2itrf(&tm) * pos; /// -/// println!("pITRF = {}", pITRF); +/// println!("pITRF = {:?}", pITRF); /// /// // Convert to an "ITRF Coordinate" and print geodetic position -/// let itrf = ITRFCoord::from_slice(&states.pos.fixed_view::<3,1>(0,0).as_slice()).unwrap(); +/// let itrf = ITRFCoord::from_slice(pITRF.as_slice()).unwrap(); /// /// println!("latitude = {} deg", itrf.latitude_deg()); /// println!("longitude = {} deg", itrf.longitude_deg());