Skip to content

Commit 7a9458c

Browse files
Fix/srp atm (#274)
* Refactor spacecraft parameters and add shadow fraction calculations for celestial items * Update spacecraft documentation and versioning for Astrodynamics framework * Refactor GeopotentialModelReader and GravitationalField initialization to use FileStream for improved file access
1 parent 5e851ac commit 7a9458c

15 files changed

Lines changed: 277 additions & 41 deletions

File tree

DEVELOPER_GUIDE.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1226,15 +1226,18 @@ Represents a spacecraft with components.
12261226

12271227
| Constructor | Description |
12281228
|------------|-------------|
1229-
| `Spacecraft(int naifId, string name, double dryMass, double maximumThrustPower, Clock clock, OrbitalParameters orbit)` | Create spacecraft |
1229+
| `Spacecraft(int naifId, string name, double dryMass, double maxMass, Clock clock, OrbitalParameters orbit, double sectionalArea = 1.0, double dragCoeff = 2.2, string cosparId = null, double solarRadiationCoeff = 1.0)` | Create spacecraft |
12301230

12311231
| Property | Description |
12321232
|----------|-------------|
12331233
| `NaifId` | NAIF ID (negative) |
12341234
| `Name` | Spacecraft name |
1235-
| `DryMass` | Mass without fuel (kg) |
1236-
| `MaximumThrustPower` | Maximum thrust power (W) |
1235+
| `DryOperatingMass` | Mass without fuel (kg) |
1236+
| `MaximumOperatingMass` | Maximum operating mass (kg) |
12371237
| `Clock` | Onboard clock |
1238+
| `SectionalArea` | Mean cross-sectional area for drag and SRP (m²) |
1239+
| `DragCoefficient` | Drag coefficient Cd (default 2.2) |
1240+
| `SolarRadiationCoeff` | Solar radiation pressure coefficient Cr (default 1.0, range 1.0–2.0) |
12381241
| `InitialOrbitalParameters` | Initial orbit |
12391242
| `StateVectorsRelativeToICRF` | Propagated states |
12401243

@@ -1467,8 +1470,8 @@ Numerical orbit propagator using a Velocity-Verlet (symplectic) integrator with
14671470

14681471
**Force Models:**
14691472
- **Gravitational acceleration** from each body in the `bodies` list. If a body has a `GeopotentialModelParameters`, the full spherical harmonic model (EGM2008) is used; otherwise point-mass gravity is applied.
1470-
- **Atmospheric drag** (when `drag` is true): uses the body's atmospheric model
1471-
- **Solar radiation pressure** (when `srp` is true): cannonball model with eclipse detection
1473+
- **Atmospheric drag** (when `drag` is true): uses the body's atmospheric model with atmosphere-relative velocity (accounts for body co-rotation). Default Cd = 2.2 (free-molecular flow). Mass ratio is dynamic (tracks fuel consumption via `GetTotalMass()`).
1474+
- **Solar radiation pressure** (when `srp` is true): cannonball model with reflectivity coefficient Cr (`Spacecraft.SolarRadiationCoeff`, default 1.0). Uses continuous shadow fraction for partial/annular eclipse geometry instead of binary eclipse detection. Mass ratio is dynamic.
14721475

14731476
**Geopotential usage:**
14741477

IO.Astrodynamics.Net/CLAUDE.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,45 @@ var propagator = new SpacecraftPropagator(window, spacecraft,
432432

433433
**Thread Safety:** `GeopotentialGravitationalField` is NOT thread-safe (pre-allocated buffers). Create separate `CelestialBody` instances for concurrent propagation.
434434

435+
### Force Models (Propagator Perturbations)
436+
437+
The `IO.Astrodynamics.Propagator.Forces` namespace provides configurable perturbation models used by the `SpacecraftPropagator`.
438+
439+
**Atmospheric Drag (`AtmosphericDrag`)**
440+
- Uses **atmosphere-relative velocity**: body-centered velocity minus co-rotation (`v_rel = v_body - omega x r_body`)
441+
- The body's angular velocity is obtained via `CelestialBody.GetOrientation(Frame.ICRF, epoch).AngularVelocity`
442+
- **Dynamic mass**: area/mass ratio is computed per step using `Spacecraft.GetTotalMass()` (dry + fuel + payload)
443+
- Drag coefficient default is **2.2** (appropriate for satellites in free-molecular flow)
444+
445+
```csharp
446+
// Atmospheric drag requires a CelestialBody with an atmospheric model
447+
var earth = new CelestialBody(PlanetsAndMoons.EARTH, Frames.Frame.ICRF, epoch,
448+
geopotentialParams, new EarthStandardAtmosphere());
449+
var drag = new AtmosphericDrag(spacecraft, earth);
450+
```
451+
452+
**Solar Radiation Pressure (`SolarRadiationPressure`)**
453+
- Cannonball model: `F = (L_sun / 4*pi*c) * Cr * (A/m) * r_hat / r^2`
454+
- Includes **reflectivity coefficient Cr** from `Spacecraft.SolarRadiationCoeff` (range 1.0–2.0, default 1.0)
455+
- **Continuous shadow model**: uses `ShadowFraction` (two-circle overlap area) instead of binary eclipse check
456+
- Handles none, partial, annular, and total eclipse geometries
457+
- SRP is scaled by `(1 - maxShadowFraction)` across all occluding bodies
458+
- **Dynamic mass**: area/mass ratio recomputed each step via `GetTotalMass()`
459+
- Occluding bodies are materialized to `CelestialBody[]` (avoids IEnumerable re-evaluation)
460+
461+
```csharp
462+
// SRP with Cr = 1.5
463+
var spacecraft = new Spacecraft(-1001, "Sat", 100.0, 10000.0, clock, orbit,
464+
sectionalArea: 10.0, solarRadiationCoeff: 1.5);
465+
var srp = new SolarRadiationPressure(spacecraft, [earth]);
466+
```
467+
468+
**Shadow Fraction (`CelestialItem.ShadowFraction`)**
469+
- Static method: `ShadowFraction(double angularSeparation, double backSize, double bySize)``[0, 1]`
470+
- Instance method: `ShadowFraction(CelestialItem by, OrbitalParameters from, Aberration aberration)``[0, 1]`
471+
- Returns 0.0 for full illumination, 1.0 for total eclipse
472+
- Uses the standard two-circle intersection area formula for partial eclipses
473+
435474
### Attitude Maneuvers
436475

437476
The framework provides a family of attitude maneuvers for spacecraft orientation control. All inherit from the abstract `Attitude` base class.
@@ -574,6 +613,13 @@ Test data files are in `Data/SolarSystem/` and copied to output directory.
574613
- `GeopotentialGravitationalField` is NOT thread-safe — one instance per thread
575614
- Legendre functions use geodesy normalization without Condon-Shortley phase
576615
- The model file starts at degree 2; degrees 0 and 1 are handled as zeros internally
616+
11. **Force Models (Drag & SRP)**: When working with atmospheric drag or solar radiation pressure:
617+
- Default drag coefficient (Cd) is **2.2** (free-molecular flow for satellites), not 0.3
618+
- Atmospheric drag uses **atmosphere-relative velocity** (body-centered velocity minus co-rotation)
619+
- SRP includes **Cr coefficient** from `Spacecraft.SolarRadiationCoeff` (default 1.0, range 1.0–2.0)
620+
- SRP uses **continuous shadow fraction** (partial/annular eclipses reduce SRP proportionally)
621+
- Both forces use **dynamic mass** via `GetTotalMass()` (accounts for fuel consumption during propagation)
622+
- Use `CelestialItem.ShadowFraction()` for eclipse geometry calculations
577623

578624
## Code Quality Standards
579625

IO.Astrodynamics.Net/IO.Astrodynamics.CLI/IO.Astrodynamics.CLI.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<FileVersion>0.0.1</FileVersion>
1010
<PackAsTool>true</PackAsTool>
1111
<ToolCommandName>astro</ToolCommandName>
12-
<Version>0.8.3.1</Version>
12+
<Version>0.8.3.2</Version>
1313
<Title>Astrodynamics command line interface</Title>
1414
<Authors>Sylvain Guillet</Authors>
1515
<Description>This CLI allows end user to exploit IO.Astrodynamics framework </Description>

IO.Astrodynamics.Net/IO.Astrodynamics.Tests/Body/CelestialBodyTests.cs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ public void PhaseHelioSynchronousOrbit()
333333
public void GeopotentialModelReader()
334334
{
335335
GeopotentialModelReader geopotentialModelReader =
336-
new GeopotentialModelReader(new StreamReader(Path.Combine(Constants.SolarSystemKernelPath.ToString(), "EGM2008_to70_TideFree")));
336+
new GeopotentialModelReader(new StreamReader(new FileStream(Path.Combine(Constants.SolarSystemKernelPath.ToString(), "EGM2008_to70_TideFree"), FileMode.Open, FileAccess.Read, FileShare.Read)));
337337
Assert.Equal(new GeopotentialCoefficient(4, 1, -0.536157389388867E-06, -0.473567346518086E-06, 0.4568074333E-11, 0.4684043490E-11),
338338
geopotentialModelReader.ReadCoefficient(4, 1));
339339
}
@@ -342,7 +342,7 @@ public void GeopotentialModelReader()
342342
public void GeopotentialModelReaderException()
343343
{
344344
GeopotentialModelReader geopotentialModelReader =
345-
new GeopotentialModelReader(new StreamReader(Path.Combine(Constants.SolarSystemKernelPath.ToString(), "EGM2008_to70_TideFree")));
345+
new GeopotentialModelReader(new StreamReader(new FileStream(Path.Combine(Constants.SolarSystemKernelPath.ToString(), "EGM2008_to70_TideFree"), FileMode.Open, FileAccess.Read, FileShare.Read)));
346346
Assert.Throws<ArgumentException>(() => geopotentialModelReader.ReadCoefficient(4, 5));
347347
}
348348

@@ -372,6 +372,46 @@ public void IsOccultedAnnular()
372372
Assert.Equal(OccultationType.Annular, CelestialItem.IsOcculted(1.0, 4.0, 2.0));
373373
}
374374

375+
[Fact]
376+
public void ShadowFractionNone()
377+
{
378+
// No occultation: angular separation larger than sum of radii
379+
Assert.Equal(0.0, CelestialItem.ShadowFraction(3.0, 2.0, 4.0));
380+
Assert.Equal(0.0, CelestialItem.ShadowFraction(3.0, 4.0, 2.0));
381+
}
382+
383+
[Fact]
384+
public void ShadowFractionFull()
385+
{
386+
// Total eclipse: occluding body completely covers the light source
387+
Assert.Equal(1.0, CelestialItem.ShadowFraction(0.5, 2.0, 4.0));
388+
}
389+
390+
[Fact]
391+
public void ShadowFractionAnnular()
392+
{
393+
// Annular eclipse: occluding body fully inside the sun disc
394+
// rOcc = 1.0, rSun = 2.0, fraction = (1.0^2)/(2.0^2) = 0.25
395+
Assert.Equal(0.25, CelestialItem.ShadowFraction(0.5, 4.0, 2.0));
396+
}
397+
398+
[Fact]
399+
public void ShadowFractionPartial()
400+
{
401+
// Partial eclipse: between no occultation and full/annular
402+
var fraction = CelestialItem.ShadowFraction(2.0, 2.0, 4.0);
403+
Assert.True(fraction > 0.0);
404+
Assert.True(fraction < 1.0);
405+
}
406+
407+
[Fact]
408+
public void ShadowFractionEdge()
409+
{
410+
// Edge case: exactly touching (d = rSun + rOcc)
411+
// rSun = 1.0, rOcc = 2.0, d = 3.0 -> no occultation
412+
Assert.Equal(0.0, CelestialItem.ShadowFraction(3.0, 2.0, 4.0));
413+
}
414+
375415
[Fact]
376416
public void EarthAirTemperature()
377417
{

IO.Astrodynamics.Net/IO.Astrodynamics.Tests/Body/GravitationalAccelerationTest.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public GravitationalAccelerationTest()
1818
public void ComputeGeopotentialGravityAcceleration()
1919
{
2020
GeopotentialGravitationalField gravity =
21-
new GeopotentialGravitationalField(new StreamReader(Path.Combine(Constants.SolarSystemKernelPath.ToString(), "EGM2008_to70_TideFree")));
21+
new GeopotentialGravitationalField(new StreamReader(new FileStream(Path.Combine(Constants.SolarSystemKernelPath.ToString(), "EGM2008_to70_TideFree"), FileMode.Open, FileAccess.Read, FileShare.Read)));
2222
StateVector parkingOrbit = new StateVector(new Vector3(6800000.0, 0.0, 0.0), new Vector3(0.0, 7656.2204182967143, 0.0), TestHelpers.EarthWithAtmAndGeoAtJ2000,
2323
TimeSystem.Time.J2000TDB,
2424
Frames.Frame.ICRF);
@@ -47,7 +47,7 @@ public void ComputeGeopotentialAt45DegreesLatitude()
4747
// Position at ~45° latitude exercises tesseral harmonic terms
4848
GeopotentialGravitationalField gravity =
4949
new GeopotentialGravitationalField(
50-
new StreamReader(Path.Combine(Constants.SolarSystemKernelPath.ToString(), "EGM2008_to70_TideFree")));
50+
new StreamReader(new FileStream(Path.Combine(Constants.SolarSystemKernelPath.ToString(), "EGM2008_to70_TideFree"), FileMode.Open, FileAccess.Read, FileShare.Read)));
5151

5252
// r=6800km, 45° latitude: x=r*cos(45°), y=0, z=r*sin(45°)
5353
double r = 6800000.0;
@@ -77,7 +77,7 @@ public void ComputeGeopotentialSouthernHemisphere()
7777
// Southern hemisphere test: odd-degree zonal harmonics (J3, J5) should cause asymmetry
7878
GeopotentialGravitationalField gravity =
7979
new GeopotentialGravitationalField(
80-
new StreamReader(Path.Combine(Constants.SolarSystemKernelPath.ToString(), "EGM2008_to70_TideFree")));
80+
new StreamReader(new FileStream(Path.Combine(Constants.SolarSystemKernelPath.ToString(), "EGM2008_to70_TideFree"), FileMode.Open, FileAccess.Read, FileShare.Read)));
8181

8282
double r = 6800000.0;
8383
double component = r / System.Math.Sqrt(2.0);
@@ -117,7 +117,7 @@ public void ComputeGeopotentialWithLongitude()
117117
// Position with longitude exercises S_nm sine terms
118118
GeopotentialGravitationalField gravity =
119119
new GeopotentialGravitationalField(
120-
new StreamReader(Path.Combine(Constants.SolarSystemKernelPath.ToString(), "EGM2008_to70_TideFree")));
120+
new StreamReader(new FileStream(Path.Combine(Constants.SolarSystemKernelPath.ToString(), "EGM2008_to70_TideFree"), FileMode.Open, FileAccess.Read, FileShare.Read)));
121121

122122
double r = 6800000.0;
123123
double component = r / System.Math.Sqrt(2.0);
@@ -147,7 +147,7 @@ public void ComputeJ2OnlyAnalytical()
147147
// Use degree 2 only, which is dominated by J2 (C20 term)
148148
GeopotentialGravitationalField gravity =
149149
new GeopotentialGravitationalField(
150-
new StreamReader(Path.Combine(Constants.SolarSystemKernelPath.ToString(), "EGM2008_to70_TideFree")),
150+
new StreamReader(new FileStream(Path.Combine(Constants.SolarSystemKernelPath.ToString(), "EGM2008_to70_TideFree"), FileMode.Open, FileAccess.Read, FileShare.Read)),
151151
maxDegrees: 2);
152152

153153
// Position in body-fixed frame at equator (φ=0)

IO.Astrodynamics.Net/IO.Astrodynamics.Tests/CCSDS/OPM/OpmTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -787,7 +787,7 @@ public void ToSpacecraft_WithMinimalOpm_CreatesSpacecraftWithDefaults()
787787
Assert.Equal(1.0, spacecraft.Mass); // Default mass when not specified
788788
Assert.Equal(500000.0, spacecraft.MaximumOperatingMass);
789789
Assert.Equal(1.0, spacecraft.SectionalArea); // Default
790-
Assert.Equal(0.3, spacecraft.DragCoefficient); // Default
790+
Assert.Equal(2.2, spacecraft.DragCoefficient); // Default
791791
Assert.Equal(1.0, spacecraft.SolarRadiationCoeff); // Default
792792
Assert.Same(clock, spacecraft.Clock);
793793
}

IO.Astrodynamics.Net/IO.Astrodynamics.Tests/Propagators/Integrators/Forces/AtmosphericDragTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@ public void ComputeAcceleration()
3434
StateVector parkingOrbit = new StateVector(new Vector3(7380000.0, 0.0, 0.0), new Vector3(0.0, 9700.0, 0.0), earth, TimeSystem.Time.J2000TDB,
3535
Frames.Frame.ICRF);
3636
var res = atmosphericDrag.Apply(parkingOrbit);
37-
Assert.Equal(new Vector3(0.0, -1.4911628830275622E-09, 0.0), res);
37+
Assert.Equal(new Vector3(-0.0, -1.3302926867698408E-09, 2.184861621946448E-15), res, TestHelpers.VelocityVectorComparer);
3838
}
3939
}

IO.Astrodynamics.Net/IO.Astrodynamics.Tests/Propagators/Integrators/Forces/SolarRadiationPressureTests.cs

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
using System.IO;
1+
using System;
2+
using System.IO;
23
using IO.Astrodynamics.Body;
34
using IO.Astrodynamics.Body.Spacecraft;
45
using IO.Astrodynamics.OrbitalParameters;
56
using IO.Astrodynamics.Propagator.Forces;
7+
using IO.Astrodynamics.SolarSystemObjects;
68
using IO.Astrodynamics.TimeSystem;
79
using Xunit;
810
using CelestialBody = IO.Astrodynamics.Body.CelestialBody;
@@ -24,11 +26,73 @@ public void ComputeAcceleration()
2426
Clock clk = new Clock("My clock", 256);
2527
Spacecraft spc = new Spacecraft(-1001, "MySpacecraft", 100.0, 10000.0, clk,
2628
new StateVector(new Vector3(6800000.0, 0.0, 0.0), new Vector3(0.0, 7656.2204182967143, 0.0), earth, TimeSystem.Time.J2000TDB, Frames.Frame.ICRF));
27-
SolarRadiationPressure solarRadiationPressure = new SolarRadiationPressure(spc,[earth]);
29+
SolarRadiationPressure solarRadiationPressure = new SolarRadiationPressure(spc, [earth]);
2830

2931
StateVector parkingOrbit = new StateVector(new Vector3(6800000.0, 0.0, 0.0), new Vector3(0.0, 7656.2204182967143, 0.0), earth, TimeSystem.Time.J2000TDB,
3032
Frames.Frame.ICRF);
3133
var res = solarRadiationPressure.Apply(parkingOrbit);
32-
Assert.Equal(new Vector3(-8.456677063948622E-09, 4.237795744348693E-08, 1.8372880484798083E-08), res, TestHelpers.VectorComparer);
34+
Assert.Equal(new Vector3(-8.456677063948622E-09, 4.237795744348693E-08, 1.8372880484798083E-08), res, TestHelpers.VectorComparer);
3335
}
34-
}
36+
37+
[Fact]
38+
public void ComputeAccelerationWithCr()
39+
{
40+
// Cr = 1.8 should scale the SRP by 1.8x compared to Cr = 1.0
41+
var earth = new CelestialBody(399, new GeopotentialModelParameters(Path.Combine(Constants.SolarSystemKernelPath.ToString(), "EGM2008_to70_TideFree")));
42+
Clock clk = new Clock("My clock", 256);
43+
Spacecraft spc = new Spacecraft(-1001, "MySpacecraft", 100.0, 10000.0, clk,
44+
new StateVector(new Vector3(6800000.0, 0.0, 0.0), new Vector3(0.0, 7656.2204182967143, 0.0), earth, TimeSystem.Time.J2000TDB, Frames.Frame.ICRF),
45+
solarRadiationCoeff: 1.8);
46+
SolarRadiationPressure solarRadiationPressure = new SolarRadiationPressure(spc, [earth]);
47+
48+
StateVector parkingOrbit = new StateVector(new Vector3(6800000.0, 0.0, 0.0), new Vector3(0.0, 7656.2204182967143, 0.0), earth, TimeSystem.Time.J2000TDB,
49+
Frames.Frame.ICRF);
50+
var res = solarRadiationPressure.Apply(parkingOrbit);
51+
52+
// Expected = base result * 1.8
53+
var expected = new Vector3(-8.456677063948622E-09 * 1.8, 4.237795744348693E-08 * 1.8, 1.8372880484798083E-08 * 1.8);
54+
Assert.Equal(expected, res, TestHelpers.VectorComparer);
55+
}
56+
57+
[Fact]
58+
public void ComputeAccelerationWithDynamicMass()
59+
{
60+
// Spacecraft with fuel tank — GetTotalMass() = dry mass + fuel = 100 + 50 = 150 kg
61+
// areaMassRatio = 1.0 / 150 instead of 1.0 / 100
62+
// So result should be 100/150 = 2/3 of the base result
63+
var earth = new CelestialBody(399, new GeopotentialModelParameters(Path.Combine(Constants.SolarSystemKernelPath.ToString(), "EGM2008_to70_TideFree")));
64+
Clock clk = new Clock("My clock", 256);
65+
Spacecraft spc = new Spacecraft(-1001, "MySpacecraft", 100.0, 10000.0, clk,
66+
new StateVector(new Vector3(6800000.0, 0.0, 0.0), new Vector3(0.0, 7656.2204182967143, 0.0), earth, TimeSystem.Time.J2000TDB, Frames.Frame.ICRF));
67+
var fuelTank = new FuelTank("tank1", "model1", "sn001", 100.0, 50.0);
68+
spc.AddFuelTank(fuelTank);
69+
70+
SolarRadiationPressure solarRadiationPressure = new SolarRadiationPressure(spc, [earth]);
71+
72+
StateVector parkingOrbit = new StateVector(new Vector3(6800000.0, 0.0, 0.0), new Vector3(0.0, 7656.2204182967143, 0.0), earth, TimeSystem.Time.J2000TDB,
73+
Frames.Frame.ICRF);
74+
var res = solarRadiationPressure.Apply(parkingOrbit);
75+
76+
// Expected = base result * (100/150)
77+
double ratio = 100.0 / 150.0;
78+
var expected = new Vector3(-8.456677063948622E-09 * ratio, 4.237795744348693E-08 * ratio, 1.8372880484798083E-08 * ratio);
79+
Assert.Equal(expected, res, TestHelpers.VectorComparer);
80+
}
81+
82+
[Fact]
83+
public void ConstructorThrowsOnNullSpacecraft()
84+
{
85+
var earth = new CelestialBody(399);
86+
Assert.Throws<ArgumentNullException>(() => new SolarRadiationPressure(null, [earth]));
87+
}
88+
89+
[Fact]
90+
public void ConstructorThrowsOnNullOccultingBodies()
91+
{
92+
var earth = new CelestialBody(399);
93+
Clock clk = new Clock("My clock", 256);
94+
Spacecraft spc = new Spacecraft(-1001, "MySpacecraft", 100.0, 10000.0, clk,
95+
new StateVector(new Vector3(6800000.0, 0.0, 0.0), new Vector3(0.0, 7656.2204182967143, 0.0), earth, TimeSystem.Time.J2000TDB, Frames.Frame.ICRF));
96+
Assert.Throws<ArgumentNullException>(() => new SolarRadiationPressure(spc, null));
97+
}
98+
}

0 commit comments

Comments
 (0)