Skip to content

About

Procedural asteroid geometry generator using python. Implements the polyhedral gravity model and uses the photo-gravitational CR3BP to approximate the AEP.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

PyAsteroid

A Python framework for simulating spacecraft dynamics near small bodies in the photogravitational circular restricted three-body problem (PGCR3BP), built to test station-keeping controllers at SRP-balanced artificial equilibrium points.

PyAsteroid generates randomized asteroid populations, computes their irregular gravity fields from polyhedral meshes, models solar radiation pressure including self-shadowing, locates artificial equilibrium points, and provides an interactive 3D visualization layer for inspecting the result.

Artificial equilibrium point geometry


Table of Contents


Motivation

Close-proximity operations at small bodies are hard for a specific reason: the asteroid's gravity is weak enough that non-gravitational forces are not perturbations, they are comparable to the primary attraction. Solar radiation pressure on a spacecraft near a kilometer-scale asteroid can rival the body's own gravitational pull.

That flips from a nuisance into a design opportunity. If SRP is deliberately balanced against asteroid gravity, a spacecraft can be placed at an artificial equilibrium point — a location where, in the rotating frame, the net force vanishes. The spacecraft co-orbits the Sun alongside the asteroid rather than orbiting the asteroid itself, holding proximity for science return without expending propellant on orbital maintenance.

The catch is that these equilibria are saddles. They are unstable, and the environment that creates them is not steady: the gravity field is irregular and time-varying as the body rotates, and the solar flux driving SRP fluctuates with solar activity. Holding position requires active disturbance rejection.

PyAsteroid exists to build that environment so controllers can be tested against it.


The Physics

Equations of Motion

The framework uses the nondimensional circular restricted three-body problem with the Sun as primary and the asteroid as secondary, modified so that solar radiation pressure reduces the effective solar attraction.

Working in the rotating frame with the origin at the Sun–asteroid barycenter, distances normalized by the heliocentric separation $R$, and time normalized so the mean motion is unity:

$$\ddot{x} - 2\dot{y} = \frac{\partial \Omega}{\partial x}, \qquad \ddot{y} + 2\dot{x} = \frac{\partial \Omega}{\partial y}, \qquad \ddot{z} = \frac{\partial \Omega}{\partial z}$$

where the effective potential is

$$\Omega(x,y,z) = \frac{1}{2}\left(x^2 + y^2\right) + \frac{(1-\beta)(1-\mu)}{r_1} + \frac{\mu}{r_2}$$

with

$$r_1 = \sqrt{(x+\mu)^2 + y^2 + z^2}, \qquad r_2 = \sqrt{(x-1+\mu)^2 + y^2 + z^2}$$

Here $\mu = m_a / (m_\odot + m_a)$ is the mass ratio, $r_1$ and $r_2$ are the distances to the Sun and asteroid, and the $-2\dot{y}$ and $+2\dot{x}$ terms are Coriolis acceleration from the rotating frame.

The single modification relative to the classical CR3BP is the factor $(1-\beta)$ on the solar term. SRP acts radially outward from the Sun, exactly opposing solar gravity, so for a spacecraft with fixed attitude and area-to-mass ratio the two combine into a single reduced attraction.

Coriolis is not a disturbance. It depends only on measured velocity and the known frame rotation rate, making it exactly computable. In the controller comparison it belongs in the feedforward term, not in the disturbance to be rejected.

The Lightness Number $\beta$

$\beta$ is the ratio of SRP force to solar gravitational force on the spacecraft:

$$\beta = \frac{F_{\text{SRP}}}{F_{\text{grav},\odot}} = \frac{L_\odot}{4\pi c , G M_\odot} \cdot \frac{A_{\text{eff}}}{m}$$

Both forces fall off as $1/R^2$, so $\beta$ is independent of heliocentric distance and depends only on the spacecraft's effective area-to-mass ratio $A_{\text{eff}}/m$ — where "effective" absorbs the surface reflectivity and the cosine of the incidence angle.

This makes $\beta$ the primary design knob. It does not merely perturb the trajectory; it relocates the equilibrium. A spacecraft that changes its effective area — by slewing a solar array, deploying a sail, or simply experiencing a change in solar flux — moves the point it is trying to hold. Uncertainty in $\beta$ is therefore uncertainty in the setpoint itself, which is a materially different problem from uncertainty in a force acting on a fixed setpoint.

Locating the Artificial Equilibrium Point

An equilibrium requires $\nabla \Omega = 0$ with zero velocity. Solving this globally yields the collinear and triangular points of the classical problem, displaced by $\beta$. But for close-proximity operations the useful solution is local, so PyAsteroid works in asteroid-centered coordinates.

Let $\rho$ be position relative to the asteroid, with $d = |\rho|$ and $d \ll R$. Along the Sun–asteroid line, anti-sunward of the body, three terms compete:

$$\underbrace{\beta \frac{GM_\odot}{R^2}}_{\text{SRP, outward}} ;+; \underbrace{3n^2 d}_{\text{tidal, outward}} ;-; \underbrace{\frac{GM_a}{d^2}}_{\text{asteroid gravity, inward}} ;=; 0$$

where $n^2 = GM_\odot / R^3$ is the mean motion.

The relative size of the first two terms decides the regime. Their ratio is

$$\frac{\text{tidal}}{\text{SRP}} = \frac{3d}{\beta R}$$

For kilometer-scale bodies at ~1 AU this is of order $10^{-5}$ — the tidal term is negligible, and the balance is SRP-dominated. Dropping it gives a closed-form estimate:

$$\boxed{;d ;=; R \sqrt{\frac{GM_a}{\beta , GM_\odot}};}$$

PyAsteroid uses this as the initial guess, then refines it by Newton iteration against the full potential — including the polyhedral gravity field rather than the point-mass approximation, which shifts the solution because a real asteroid is not spherically symmetric.

Worked example. A 1.88 km body at $\beta = 2.4 \times 10^{-3}$ and $R = 1$ AU yields an AEP 5.64 km anti-sunward, confirming the SRP-dominated regime.

The umbra problem

The closed-form solution places the AEP directly anti-sunward — which is inside the asteroid's shadow. A spacecraft there receives no sunlight, meaning no power and, self-defeatingly, no SRP, so the equilibrium it was placed at does not exist.

Umbra computed by ray casting, with the collinear AEP inside it

Illumination state on the equatorial plane, from vectorized Möller–Trumbore ray casting. The collinear AEP falls squarely inside the umbra.

Real mission designs resolve this by displacing the equilibrium toward the terminator plane, where the spacecraft stays illuminated at grazing incidence. The tradeoff is that the SRP force is no longer antiparallel to gravity, so the balance must be closed by the component along the Sun line while a transverse residual is absorbed by control authority.

PyAsteroid supports both configurations. Which one is appropriate depends on the mission profile, and it changes the control problem — see Known Limitations.

Why the AEP Requires Active Control

Linearizing about the equilibrium yields a Jacobian with a real positive eigenvalue: the point is a saddle. Any displacement grows exponentially with time constant $\tau = 1/\lambda$.

For representative kilometer-scale cases $\tau \approx 4$ hours — the same order as a typical asteroid rotation period.

That coincidence is not incidental. In most control problems the plant dynamics and the disturbance occupy separable timescales, so stabilization and disturbance rejection can be designed independently. Here they do not separate: the gravity field varies as the body rotates, on the same timescale over which the instability grows. The controller cannot treat the rotating gravity field as quasi-static, and it cannot treat the instability as fast relative to the disturbance. Both must be handled at once.

Force Budget

Acceleration magnitude versus distance for each modeled force

Every modeled force for a 1.88 km body. Gravity and SRP cross at the AEP; the tidal, albedo, and thermal terms sit three or more orders of magnitude below.

Force Role
Asteroid gravity Primary inward attraction; irregular and time-varying with body rotation
Solar radiation pressure Primary outward force; balances gravity at the AEP
Coriolis Exactly known from velocity and frame rate — feedforward, not disturbance
Solar tidal Negligible in the SRP-dominated regime, retained for completeness
Albedo Reflected sunlight from the body surface; small correction
Thermal IR Re-radiated heat from the body surface; small correction

Installation

git clone https://github.com/JCarroll-OU/PyAsteroid.git
cd PyAsteroid
python -m pip install -r requirements.txt

Dependencies

Package Purpose
numpy Array operations throughout
scipy Integration, root finding, eigenvalue analysis
polyhedral_gravity Exact polyhedral gravity via Tsoulis' method
matplotlib 3D visualization and interactive plotting

Quick Start

from pyasteroid import AsteroidGenerator, GravityField, PGCR3BP, Plotter

# 1. Generate a randomized asteroid
gen = AsteroidGenerator(seed=42)
asteroid = gen.generate()

# 2. Build its gravity field from the mesh
field = GravityField(asteroid, density=2000.0)   # kg/m^3

# 3. Set up the dynamics model
system = PGCR3BP(
    asteroid=asteroid,
    gravity=field,
    beta=2.4e-3,           # spacecraft lightness number
    heliocentric_distance=1.0,   # AU
)

# 4. Locate the artificial equilibrium point
aep = system.find_aep()
print(f"AEP at {aep} m, |d| = {np.linalg.norm(aep)/1e3:.2f} km")

# 5. Check its stability
eigenvalues = system.linearize(aep)
tau = 1.0 / max(eigenvalues.real)
print(f"Instability time constant: {tau/3600:.2f} hours")

# 6. Visualize
plot = Plotter(asteroid)
plot.add_marker(aep, label="AEP")
plot.show()

Module Reference

Asteroid

Represents a single small body: its shape, its mesh, and its rotation state.

The mesh is built by subdividing an icosphere, which gives near-uniform triangle areas across the surface — important because the polyhedral gravity computation sums per-face contributions, and wildly uneven faces degrade accuracy in ways that are hard to diagnose. The base icosphere is then scaled into a tri-axial ellipsoid and perturbed with surface features.

The same body at two icosphere subdivision levels

The same body at two subdivision levels. Face count grows as $20 \cdot 4^n$, so cost rises steeply with resolution.

Parameters

Parameter Type Description
semi_axes (a, b, c) Tri-axial ellipsoid semi-axis lengths in meters. Setting all three equal gives a sphere, useful for validation.
subdivisions int Icosphere subdivision level. Face count grows as $20 \cdot 4^n$, so cost rises steeply — level 4 gives 5,120 faces, level 6 gives 81,920.
n_features int Number of surface features (craters and hills) to place.
feature_params — Controls the Gaussian shape of each feature: amplitude (positive for hills, negative for craters), width, and surface position.
spin_period float Rotation period in seconds.
spin_axis (x, y, z) Rotation axis direction; need not align with a principal axis.

Surface features are Gaussian-parameterized: each is a radial displacement of the mesh vertices falling off as $\exp(-s^2/2\sigma^2)$ in surface distance $s$ from the feature center. Negative amplitude produces a crater, positive a hill. This keeps the surface smooth and differentiable — a mesh with discontinuous features produces gravity field artifacts that can be mistaken for real dynamics.

Rotation uses quaternions rather than Euler angles, avoiding gimbal lock and drift over long propagations where a rotation matrix accumulates orthogonality error.

Key methods

Method Description
rotate(t) Advance the body's attitude to time t
vertices() / faces() Return the mesh arrays for gravity computation and rendering
is_illuminated(point, sun_dir) Ray-cast shadow test — see GravityField

AsteroidGenerator and Range

Produces randomized asteroid populations for Monte Carlo campaigns. Every geometric parameter is drawn from a Range, so a whole population is specified declaratively and reproduced from a single seed.

Eight asteroids sampled from one generator configuration

Eight bodies from a single generator configuration, rendered at common scale.

Range

Samples from a truncated Gaussian. The truncation matters: a plain Gaussian on a physical quantity will eventually produce a negative semi-axis or an implausibly extreme aspect ratio, and a single invalid body can silently poison a long campaign.

Range(mean=1000.0, std=300.0, min=400.0, max=2500.0)
Parameter Description
mean Distribution center
std Standard deviation before truncation
min, max Hard bounds; samples outside are rejected and redrawn

AsteroidGenerator

gen = AsteroidGenerator(
    size=Range(mean=1000.0, std=300.0, min=400.0, max=2500.0),
    elongation=Range(mean=1.3, std=0.2, min=1.0, max=2.0),
    flattening=Range(mean=1.2, std=0.15, min=1.0, max=1.8),
    n_features=Range(mean=12, std=4, min=0, max=30),
    spin_period=Range(mean=6*3600, std=2*3600, min=2*3600, max=20*3600),
    seed=42,
)

Shape is specified through ratios rather than three independent axes. Drawing $a$, $b$, $c$ separately would produce a population whose size and shape are correlated in ways nobody intended; drawing a characteristic size and then aspect ratios keeps the two independent, so a campaign can attribute controller performance to one or the other.


GravityField

Computes gravitational acceleration from the asteroid's polyhedral mesh using Tsoulis' method, wrapping the polyhedral_gravity library.

This is an exact evaluation for a constant-density polyhedron — not a series truncation. Spherical harmonics diverge inside the circumscribing sphere (the Brillouin sphere), which is precisely the region of interest for close proximity operations. For a strongly elongated body, the Brillouin sphere may enclose the entire operational volume, making the harmonic expansion unusable exactly where accuracy is needed. The polyhedral formulation has no such restriction and remains valid arbitrarily close to the surface.

Acceleration magnitude on an equatorial slice

Acceleration magnitude on the equatorial plane, with the body outline overlaid and direction shown as unit vectors. The departure from spherical symmetry is what the controller has to reject.

Parameters

Parameter Type Description
asteroid Asteroid Source of the mesh geometry
density float Bulk density in kg/m³, assumed constant. Typical asteroid values run 1200–3000.

Key methods

Method Description
acceleration(points) Gravitational acceleration at one or many field points, in the body-fixed frame
potential(points) Gravitational potential
gm() Total $GM$ from mesh volume and density

Shadow geometry

SRP requires knowing whether a point is illuminated, which means testing whether the ray from the point toward the Sun intersects the asteroid mesh. PyAsteroid uses vectorized Möller–Trumbore ray–triangle intersection, testing all faces simultaneously with NumPy rather than looping. This matters at scale: a Monte Carlo campaign performs this test at every integration step for every run, and a Python-level loop over tens of thousands of faces would dominate total runtime.

The result of that test is shown in the umbra figure above.


PGCR3BP

The dynamics model. Assembles the gravity field, SRP, and rotating-frame terms into equations of motion, locates equilibria, and analyzes stability.

Parameters

Parameter Type Description
asteroid Asteroid The secondary body
gravity GravityField Gravity model for that body
beta float Spacecraft lightness number. Sets the AEP location — see above.
heliocentric_distance float Sun–asteroid separation in AU. Sets the mean motion and the solar flux.
include_tidal bool Include the solar tidal term. Negligible in the SRP-dominated regime; useful for verifying that claim.
include_albedo bool Include reflected sunlight from the body surface.
include_thermal bool Include thermal IR re-radiation from the body surface.
aep_mode str "collinear" for the pure anti-sunward point, "terminator" for the illuminated displaced solution.

Key methods

Method Description
dynamics(t, state) State derivative for a 6-element $[\rho, \dot{\rho}]$ state; pass directly to scipy.integrate.solve_ivp
find_aep() Closed-form initial guess refined by Newton iteration against the full field
linearize(point) Jacobian eigenvalues at a point; the largest real part gives $1/\tau$
srp_acceleration(point, t) SRP acceleration including the shadow test

Newton residual versus iteration for five different bodies

Newton refinement from the closed-form guess, across bodies from 1.0 to 2.6 km and aspect ratios up to 1.84. Convergence is quadratic and reaches the floating-point floor in three to four iterations — the analytic estimate is a good enough starting point that refinement is cheap.


Plotter

Interactive 3D visualization for asteroid geometry, gravity fields, trajectories, and equilibrium points.

Plotter is deliberately import-free of the domain classes — it never imports Asteroid, GravityField, or PGCR3BP. Interaction is wired through a factory-pattern callback contract: the caller supplies functions producing the data to display, and Plotter invokes them without knowing what produced them.

The payoff is that visualization can be developed, tested, and modified without touching physics code, and the physics classes carry no rendering dependencies. It also means Plotter can display anything conforming to the contract — including data the framework does not currently produce.

The interactive plotting window

The interactive window, with toggles wired through the callback contract.

Key methods

Method Description
add_marker(point, label) Place a labeled point, e.g. the AEP
add_trajectory(states) Draw a propagated path
add_toggle(label, callback) Register an interactive button against a data-producing callback
show() Open the interactive window

Monte Carlo Campaigns

The framework's purpose is generating many randomized environments and evaluating a controller across all of them.

gen = AsteroidGenerator(seed=42, **population_config)

results = []
for i in range(N_RUNS):
    asteroid = gen.generate()
    field = GravityField(asteroid, density=2000.0)
    system = PGCR3BP(asteroid=asteroid, gravity=field, beta=beta)

    aep = system.find_aep()
    results.append(run_controller(system, aep, controller))

Because every asteroid derives from a single seeded generator, an entire campaign is reproducible from that seed and the population configuration — and an individual anomalous run can be regenerated in isolation for debugging without re-running the campaign.

SRP disturbance. Beyond geometric randomization, the framework injects heavy noise into the solar flux to represent solar flares and coronal mass ejections. This is the physically motivated worst case for this problem: a flux transient does not merely push the spacecraft, it shifts the equilibrium the spacecraft is trying to hold, because the AEP location depends on the SRP magnitude. The controller must reject a disturbance whose effect is to move the setpoint.


Validation

Analytic sphere check. With all semi-axes equal and no surface features, the polyhedral gravity field must converge to the closed-form solution for a uniform sphere, $g = GM/r^2$ externally. Residual error versus subdivision level quantifies mesh discretization error and establishes the resolution needed for a target accuracy.

Log-log convergence of the polyhedral field against the analytic sphere

Mean relative error in $|\mathbf{g}|$ on a shell at $r = 3R$, against the analytic sphere. Convergence is second order in mesh edge length — the expected rate for a linear triangulation of a smooth surface, which indicates the wrapper is behaving correctly rather than merely producing plausible numbers.

Tidal term check. Running with include_tidal=True and False and comparing AEP locations verifies the SRP-dominated regime claim numerically rather than relying on the order-of-magnitude argument alone.


Regenerating the Figures

Every figure in this README except Plotter_UI.png is produced by generate_figures.py:

python generate_figures.py                # all figures -> Images/
python generate_figures.py --list         # available figure names
python generate_figures.py force_budget   # regenerate just one

Plotter_UI.png is a screenshot of the live interactive window and has no scripted equivalent.


Known Limitations

  • Constant density. The polyhedral formulation assumes uniform bulk density. Real asteroids, particularly rubble piles, are heterogeneous. This is a modeling choice, not an implementation gap — heterogeneity would require a mass-concentration or layered model.

  • AEP configuration is unresolved. Whether the target should be the displaced-collinear point or a terminator-plane AEP remains an open design question. The collinear point is analytically cleaner but sits inside the umbra; the terminator solution stays illuminated but does not admit the simple closed-form balance. This choice changes the control problem, not just the setpoint.

  • Timescale coupling. With $\tau$ comparable to the spin period, the usual separation between stabilization and disturbance rejection does not hold. Controllers designed assuming quasi-static gravity should not be expected to transfer.

  • Attitude is not modeled. The spacecraft is treated as a point mass with a fixed effective area-to-mass ratio. Real attitude motion modulates $\beta$, and therefore modulates the equilibrium location.

  • SRP incidence angle. Work on solar-sail sliding mode control reports failure beyond roughly 30° solar incidence, arising from geometric constraints on the achievable force direction rather than from controller tuning. Since SRP direction is constrained by illumination geometry, this bounds the control authority available in the transverse direction — directly relevant to any terminator-displaced configuration.


References

  1. Tsoulis, D. — Analytical computation of the gravity field of a polyhedral source with linearly varying density.
  2. Möller, T. and Trumbore, B. — Fast, minimum storage ray/triangle intersection.
  3. McInnes, C. R. — Solar Sailing: Technology, Dynamics and Mission Applications (artificial equilibrium points and the lightness number).
  4. Scheeres, D. J. — Orbital Motion in Strongly Perturbed Environments (small-body dynamics).

About

Procedural asteroid geometry generator using python. Implements the polyhedral gravity model and uses the photo-gravitational CR3BP to approximate the AEP.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages