Cannonball SRP #10

Closed
opened 2026-09-30 11:08:03 -04:00 by connor · 4 comments
Owner

Solar radiation pressure, cannonball model.

M1.

Solar radiation pressure, cannonball model. M1.
connor added this to the M1 milestone 2026-09-30 11:24:56 -04:00
connor added the size/S label 2026-09-30 11:24:56 -04:00
Author
Owner

Ready for review, scoped wider than the title: one Dynamics::Sunshine covering radiation pressure, photovoltaic power and the eclipse, since all three answer to the same geometry and splitting them would mean two ephemeris lookups and two eclipse calculations per evaluation to produce two halves of one answer.

Pressure works for both surface models, not only the cannonball. A faceted surface is asked which way the light comes from in its own frame, so the force follows the attitude; a cannonball ignores the direction and needs no attitude at all. The force is along the Sun line scaled by effective area — a real faceted surface also reflects specularly, which tilts the force off that line, and that needs a per-face force rather than a per-face area. Box-wing in M2.

Power comes from a new SolarArray on Spacecraft: area, efficiency, body normal, and whether it tracks the Sun. Contributed only when the layout carries stored energy, since a propagation that is not integrating energy is not asking the question.

Eclipse is src/eclipse.rs: the fraction of the solar disk left visible, as the overlap of two circles on the sky. Umbra, penumbra and the annular case past the cone's tip all come out of the one calculation, where a cone model approximates it and a cylindrical one drops the penumbra and misstates entry and exit by a couple of minutes at low altitude.

Validation. Eight tests on the eclipse geometry, ten on the model.

  • The lens area against counting: equal-area samples scattered over the solar disk, asking how many the occulting disk covers. Shares no algebra with the formula. Agrees to 2e-3 across a dozen partial geometries found by sweeping rather than guessed at.
  • The umbra's length against R_body d / (R_sun - R_body), found by bisecting for where light first appears on the axis. Agrees to 1e-6, and past the tip the fraction matches the annular ratio of disk areas.
  • Radiation pressure at one astronomical unit against the published 4.54e-6 N/m^2, inverse square, and linearity in the visible fraction.
  • A cannonball pushed along the anti-Sun line to 1e-12 in direction; a faceted one changing with attitude; a fixed array matching an articulated one when pointed at the Sun and never generating negative power.
  • Setups refused before integrating: sunlight with no spacecraft, and a faceted surface or a fixed array on a layout with no attitude to point it.

In examples/full.rs the day comes out 63.5% sunlit, which is right for a 51.6 degree low orbit and is an independent check on the eclipse geometry that nothing else provides.

Ready for review, scoped wider than the title: one `Dynamics::Sunshine` covering radiation pressure, photovoltaic power and the eclipse, since all three answer to the same geometry and splitting them would mean two ephemeris lookups and two eclipse calculations per evaluation to produce two halves of one answer. **Pressure** works for both surface models, not only the cannonball. A faceted surface is asked which way the light comes from in its own frame, so the force follows the attitude; a cannonball ignores the direction and needs no attitude at all. The force is along the Sun line scaled by effective area — a real faceted surface also reflects specularly, which tilts the force off that line, and that needs a per-face force rather than a per-face area. Box-wing in M2. **Power** comes from a new `SolarArray` on `Spacecraft`: area, efficiency, body normal, and whether it tracks the Sun. Contributed only when the layout carries stored energy, since a propagation that is not integrating energy is not asking the question. **Eclipse** is `src/eclipse.rs`: the fraction of the solar disk left visible, as the overlap of two circles on the sky. Umbra, penumbra and the annular case past the cone's tip all come out of the one calculation, where a cone model approximates it and a cylindrical one drops the penumbra and misstates entry and exit by a couple of minutes at low altitude. **Validation.** Eight tests on the eclipse geometry, ten on the model. - The lens area against counting: equal-area samples scattered over the solar disk, asking how many the occulting disk covers. Shares no algebra with the formula. Agrees to 2e-3 across a dozen partial geometries found by sweeping rather than guessed at. - The umbra's length against `R_body d / (R_sun - R_body)`, found by bisecting for where light first appears on the axis. Agrees to 1e-6, and past the tip the fraction matches the annular ratio of disk areas. - Radiation pressure at one astronomical unit against the published 4.54e-6 N/m^2, inverse square, and linearity in the visible fraction. - A cannonball pushed along the anti-Sun line to 1e-12 in direction; a faceted one changing with attitude; a fixed array matching an articulated one when pointed at the Sun and never generating negative power. - Setups refused before integrating: sunlight with no spacecraft, and a faceted surface or a fixed array on a layout with no attitude to point it. In `examples/full.rs` the day comes out **63.5% sunlit**, which is right for a 51.6 degree low orbit and is an independent check on the eclipse geometry that nothing else provides.
Author
Owner

Two corrections to the comment above, and one addition.

The sunlit fraction is 61.4%, not 63.5%. I had computed the reference generation rate at one astronomical unit, and the Earth is near perihelion in early January where the irradiance is three percent higher — so the figure read as three percent more daylight than there was. The example now takes the Sun's actual distance from the ephemeris. 61.4% is still the right answer for a 51.6 degree low orbit, so the check stands; the number was just inflated.

Accuracy now sets a per-component absolute tolerance rather than one scalar, which removes what the step count surprise was really about. An absolute tolerance is a floor for when a component is near zero, and a state mixing meters with joules has no single sensible floor: position never approaches zero in an orbit so any small floor does, while stored energy starts at zero with a derivative of kilowatts and a floor meant for meters held it to 1e-10 J. That cost a one-day arc eight times the steps for a bit-identical answer.

Each component now gets a floor in its own units, scaled by the preset — one for everything but energy, which gets a joule at Medium. The example dropped its manual override and still runs in 1602 steps against the 10758 it took before. All 174 tests pass unchanged, since only energy's floor moved.

Two corrections to the comment above, and one addition. **The sunlit fraction is 61.4%, not 63.5%.** I had computed the reference generation rate at one astronomical unit, and the Earth is near perihelion in early January where the irradiance is three percent higher — so the figure read as three percent more daylight than there was. The example now takes the Sun's actual distance from the ephemeris. 61.4% is still the right answer for a 51.6 degree low orbit, so the check stands; the number was just inflated. **`Accuracy` now sets a per-component absolute tolerance** rather than one scalar, which removes what the step count surprise was really about. An absolute tolerance is a floor for when a component is near zero, and a state mixing meters with joules has no single sensible floor: position never approaches zero in an orbit so any small floor does, while stored energy starts at zero with a derivative of kilowatts and a floor meant for meters held it to 1e-10 J. That cost a one-day arc eight times the steps for a bit-identical answer. Each component now gets a floor in its own units, scaled by the preset — one for everything but energy, which gets a joule at `Medium`. The example dropped its manual override and still runs in 1602 steps against the 10758 it took before. All 174 tests pass unchanged, since only energy's floor moved.
Author
Owner

Follow-on commit, addressing the review of the first.

Mounting is an enum now, and the single-axis case it was missing is the one most spacecraft actually have. Mounting::Mounted { normal } is the old fixed panel, Articulated { axis } is a wing on one drive, and FullyArticulated is the previous articulated: true.

The physics the old boolean could not express: a single drive turns the wing about its axis, so the normal can go anywhere in the plane perpendicular to it and nowhere out of that plane. What survives is the sine of the angle between the axis and the Sun, so an array whose axis points at the Sun generates nothing however it is driven. That is the beta-angle loss that sizes real arrays, and the previous model gave such an array its full output. Tested against sin at five angles, against zero along the axis from both ends, and ordered against the other two mountings — locking a drive never helps, adding a second never hurts.

Battery { capacity } on the spacecraft, with the cap applied in assembly rather than in the model that generated the power, so a second source would be capped too and the cap is stated once. events::BatteryFull is attached automatically when the layout carries stored energy and the vehicle has a battery.

One finding worth recording: the cap turns out to be self-limiting, so the event fires less than expected. A step's later stages are evaluated at states already at capacity, their power is zero, and the step cannot carry the store past it — the energy converges onto capacity from below and the event function never changes sign. Measured over a day of a low orbit: zero firings, energy landing on capacity exactly, never a joule over. Over a short arc with steps long enough to span the fill, it does fire. So the event bounds the overshoot when a step is long against the headroom left, rather than enforcing the cap. The docs said more than that and have been corrected.

Per-face radiation pressure force is #30, in M2, as agreed — including that it is what would give a radiation-pressure torque, which this model cannot produce at all.

Follow-on commit, addressing the review of the first. **Mounting is an enum now**, and the single-axis case it was missing is the one most spacecraft actually have. `Mounting::Mounted { normal }` is the old fixed panel, `Articulated { axis }` is a wing on one drive, and `FullyArticulated` is the previous `articulated: true`. The physics the old boolean could not express: a single drive turns the wing about its axis, so the normal can go anywhere in the plane perpendicular to it and nowhere out of that plane. What survives is the **sine** of the angle between the axis and the Sun, so an array whose axis points at the Sun generates nothing however it is driven. That is the beta-angle loss that sizes real arrays, and the previous model gave such an array its full output. Tested against `sin` at five angles, against zero along the axis from both ends, and ordered against the other two mountings — locking a drive never helps, adding a second never hurts. **`Battery { capacity }`** on the spacecraft, with the cap applied in assembly rather than in the model that generated the power, so a second source would be capped too and the cap is stated once. `events::BatteryFull` is attached automatically when the layout carries stored energy and the vehicle has a battery. One finding worth recording: **the cap turns out to be self-limiting**, so the event fires less than expected. A step's later stages are evaluated at states already at capacity, their power is zero, and the step cannot carry the store past it — the energy converges onto capacity from below and the event function never changes sign. Measured over a day of a low orbit: zero firings, energy landing on capacity exactly, never a joule over. Over a short arc with steps long enough to span the fill, it does fire. So the event bounds the overshoot when a step is long against the headroom left, rather than enforcing the cap. The docs said more than that and have been corrected. Per-face radiation pressure force is **#30**, in M2, as agreed — including that it is what would give a radiation-pressure torque, which this model cannot produce at all.
Author
Owner

Done, in 0bb728e and 36e96b0.

Scope widened from the title: rather than a cannonball SRP force on its own, this is one
Dynamics::Sunshine covering radiation pressure, power and eclipse together, since all
three need the same geometry.

What landed:

  • src/eclipse.rs — the eclipse as the overlap of two circular disks on the sky, which
    gives umbra, penumbra and the annular case past the cone's tip from one calculation.
    Validated against counting equal-area samples over the solar disk, which shares no
    algebra with the lens-area formula.
  • models::sunshine, and SolarArray on Spacecraft.
  • Mounting as an enum — Mounted { normal }, Articulated { axis }, FullyArticulated.
    The middle one is what a boolean could not express and is what most spacecraft have: a
    wing on one drive keeps the sine of the angle between its axis and the Sun, so an array
    whose axis points at the Sun generates nothing however it is driven.
  • Battery { capacity }, capped in state_derivative rather than in the model that
    generated the power, so a second source would be capped too.

Measured: a 51.6 degree low orbit comes out 61.6% sunlit over a day, by two independent
routes — integrating eclipse::illumination along the trajectory, and inferring it from
the energy before the battery cap existed.

Two things worth recording because they were wrong first:

  • I first reported 63.5% sunlit, having computed the reference generation rate at one
    astronomical unit. The Earth is near perihelion in early January and the irradiance there
    is 3% higher, so the figure read as 3% more daylight than there was. Take the Sun's
    distance from the ephemeris whenever a number depends on irradiance.
  • BatteryFull bounds the overshoot rather than enforcing the cap. A step's later stages
    sit at states already at capacity, so their power is zero and the energy converges onto
    capacity from below without the event function ever changing sign — zero firings over a
    day of a low orbit, never a joule over. It does fire over a short arc with long steps.
    I had documented it as the mechanism; corrected.

One finding that generalizes beyond this issue. Adding sunlight took the example from 1399
steps a day to 10758. The obvious suspect was the penumbra, where the illuminated fraction
has corners as the disks go tangent. Measured instead: with the eclipse but no solar array
it was 1395 steps, so the eclipse costs nothing. It was the energy component — one scalar
a_tol was holding a 1.3e8 J quantity to 1e-10 J. Accuracy now sets absolute tolerances
per component, which brought it to 1602 steps with a bit-identical answer.

Per-face radiation pressure force, including that it is the only route to a
radiation-pressure torque, is deferred to #30.

Done, in `0bb728e` and `36e96b0`. Scope widened from the title: rather than a cannonball SRP force on its own, this is one `Dynamics::Sunshine` covering radiation pressure, power and eclipse together, since all three need the same geometry. What landed: - `src/eclipse.rs` — the eclipse as the overlap of two circular disks on the sky, which gives umbra, penumbra and the annular case past the cone's tip from one calculation. Validated against counting equal-area samples over the solar disk, which shares no algebra with the lens-area formula. - `models::sunshine`, and `SolarArray` on `Spacecraft`. - `Mounting` as an enum — `Mounted { normal }`, `Articulated { axis }`, `FullyArticulated`. The middle one is what a boolean could not express and is what most spacecraft have: a wing on one drive keeps the sine of the angle between its axis and the Sun, so an array whose axis points at the Sun generates nothing however it is driven. - `Battery { capacity }`, capped in `state_derivative` rather than in the model that generated the power, so a second source would be capped too. Measured: a 51.6 degree low orbit comes out 61.6% sunlit over a day, by two independent routes — integrating `eclipse::illumination` along the trajectory, and inferring it from the energy before the battery cap existed. Two things worth recording because they were wrong first: - I first reported 63.5% sunlit, having computed the reference generation rate at one astronomical unit. The Earth is near perihelion in early January and the irradiance there is 3% higher, so the figure read as 3% more daylight than there was. Take the Sun's distance from the ephemeris whenever a number depends on irradiance. - `BatteryFull` bounds the overshoot rather than enforcing the cap. A step's later stages sit at states already at capacity, so their power is zero and the energy converges onto capacity from below without the event function ever changing sign — zero firings over a day of a low orbit, never a joule over. It does fire over a short arc with long steps. I had documented it as the mechanism; corrected. One finding that generalizes beyond this issue. Adding sunlight took the example from 1399 steps a day to 10758. The obvious suspect was the penumbra, where the illuminated fraction has corners as the disks go tangent. Measured instead: with the eclipse but no solar array it was 1395 steps, so the eclipse costs nothing. It was the energy component — one scalar `a_tol` was holding a 1.3e8 J quantity to 1e-10 J. `Accuracy` now sets absolute tolerances per component, which brought it to 1602 steps with a bit-identical answer. Per-face radiation pressure force, including that it is the only route to a radiation-pressure torque, is deferred to #30.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: connor/astrokit#10