Skip to content

The plants — dynamics and power

Reference documentation for sim/ — the simulated universe and the simulated hardware that the flight software flies against. See adcs.md and eps.md for the subsystems that consume it, and index.md for the platform overview.


1. What this is, and what it is not

It is not flight software. Everything under sim/ is plant, and in status it’s a sibling of hub/ and egse/ — support code, not something that would ever fly.

There are two plants, in two different binaries, and one shared universe underneath them:

PlantBinaryInterfaceModels
External Dynamics Engineadcs_nodeaocs_icd.hRigid body, sensor heads, actuators
Power Planteps_nodeeps_icd.hSolar array, battery, thermal, transducers
(shared universe)bothOrbit, Sun, Earth’s shadow, and the time base

Each is linked into its own node’s binary, which makes them the only non-flight code inside a node. The reason is physical in both cases: the interface each drives is that board’s own sensor harness — the ADCS’s gyro triad, magnetometer, sun cells, star trackers and tachometers on that PCB, the EPS’s shunts, bus ADC and thermistors inside that box. No raw measurement crosses CAN, and a harness is not something you reach across a process boundary, so each plant sits where its harness sits. Three rules keep that from becoming an excuse:

  • A flight application never includes a plant header. nodes/adcs_node.c’s entire view is aocs_icd.h; nodes/eps_node.c’s is eps_icd.h. The single exception is main(), which constructs the plant before the scheduler starts — boot code, not control code.
  • A plant never calls into its application. It reads the RW segment and writes the RO and TRUTH segments. That’s the whole conversation.
  • Sharing between the two plants is fine; sharing between a plant and its application is not. Both sit on dyn_world.h because one spacecraft cannot be in two places. The duplication rule is about a flight application sharing arithmetic with the truth model measuring it, which makes the estimate return its own input; here both consumers are the truth model.

They run inside FreeRTOS nodes, so they inherit the platform’s Posix-port hazards even though they aren’t flight software.

Files

FileContents
sim/dyn_world.h / .cThe shared universe: the time base, the three model vtables, orbit/sun sampling and the Earth’s shadow. Depends on nothing.
sim/dyn_rand.h / .cThe deterministic noise source, shared by every sensor head in either plant.
sim/dyn_orbit.cSimplified SGP4: TLE parsing and J2-secular Kepler propagation (§4).
sim/dyn_env.cTilted-dipole IGRF (§5) and the solar ephemeris. It gives the EPS the sun model it wants and the field model it doesn’t; one unused static isn’t worth splitting the file for.
sim/dyn_engine.h / .cThe ADCS’s plant: configuration (dyn_cfg_t), orchestration, quaternion helpers, the interface guard.
sim/dyn_sensors.cThe ADCS’s sensor heads, their noise and their validity (§8).
sim/dyn_rigid.cEuler’s equations, reaction wheels, magnetorquers, RK4 (§7).
sim/eps_plant.h / .cThe EPS’s plant: array illumination, the battery, the two thermal lags, its transducers and its guard (§11).

Built into adcs_node and eps_node through the ${ARGN} extra-sources argument of add_freertos_node in CMakeLists.txt — the first four rows into both, the rest into one each. Putting them in common/ would compile them into all five nodes.


2. The tick

dynStep(dt_s) advances the ADCS’s plant by one control period. The order is fixed and each step depends on the one before:

1. universe        dynWorldStep(dt)                 -> r, v, s in ECI, eclipse, jd
2. field           dynWorldField(w)                 -> B in ECI
3. rigid body      READS THE RW SEGMENT, integrates -> q_true, w_true, wheels
4. sensors         degrade the truth                -> writes the RO segment
5. publish         r, v, wheels, eclipse, truth     -> stamps the guard

Step 1 is dyn_world.c’s and carries the time base; the EPS’s plant opens its own step the same way, in its own process. Step 2 is kept separate deliberately — only the ADCS wants the geomagnetic field, and evaluating a tilted-dipole IGRF twice a second in the EPS for a vector nothing reads is the kind of cost that accumulates unnoticed. Step 3 is the closed loop: the only read of the RW segment and the only point at which the flight software’s output affects the world.

The caller is prvControlTask in ../nodes/adcs_node.c, which runs dynStep() and then the application, in that order, in a single task. That ordering is the synchronisation design: one thread of control means no lock between the two modules and no ambiguity about which setpoints the plant integrated. The engine has no task of its own. The EPS does the same, and had to be changed to be able to (§11).

Time: integer milliseconds, never truncated seconds

Elapsed time accumulates in integer milliseconds in a uint64_t and is converted to Julian Date only at the point of use. This isn’t fussiness, and it’s the platform’s rule rather than this file’s: every model step here is sub-second, so counter += (uint32_t) dt_s adds zero forever and the model freezes at its phase-0 state without erroring — no eclipse, array current pinned, and a run that looks plausible until you notice nothing moves. The EPS’s own orbit counter stood still that way long enough to be baked into sample telemetry; the payload’s GNSS clock carried the same bug against gnss_period_ms (payload.md §4). A uint64_t of milliseconds covers 584 million years, so it cannot wrap.

truth.orbit_ms is the low 32 bits of that counter, so it does wrap — after 49 days of simulated time. It’s a diagnostic, not a clock.

dyn_world.c’s statics are per process, so the two plants are two instances, not one universe: each holds its own installed models, its own parsed element set and its own elapsed-time counter started at its own boot instant, and the shipped configuration compounds that with different time scales ([adcs_dyn] 1 against [eps_dyn] 60). ADCS_ECLIPSE and EPS_ECLIPSE disagreeing side by side in the DHS’s data pool is expected, not a fault. The honest cure — driving elapsed time from on-board time once obtIsSynced() — is a deliberate non-goal: it would couple two plants that are meant to be independent.


3. The model seam

The three environment models are reached through vtables declared in dyn_world.h — one level below either plant, so both get the same universe and neither owns it:

typedef struct {
    const char *name;
    int  (*init)(const dyn_world_cfg_t *cfg, double *epoch_jd);
    void (*propagate)(double jd, float pos_eci_km[3], float vel_eci_kms[3]);
} dyn_orbit_model_t;

Only the orbit has an init, because only the orbit has an element set to parse and an epoch to report. dyn_field_model_t is { name, field } and dyn_sun_model_t is { name, sun }.

The vectors are typed float[3] rather than float[AOCS_AXES] precisely so this header depends on nothing: AOCS_AXES is 3, but it belongs to the ADCS’s interface and the EPS’s plant has no business including that.

dynWorldInit installs them (pxOrbit = &xDynOrbitSgp4; and the two like it), and swapping in a high-fidelity model is those assignments and nothing else. To replace the dipole with a full IGRF-13: write sim/dyn_env_igrf13.c exporting a const dyn_field_model_t xDynFieldIgrf13, add it to the add_freertos_node(adcs_node ...) source list, and change one line. No caller changes, no interface change, and — the point of the exercise — no flight-software change.

All three take Julian Date rather than seconds-since-boot, because both the field and the Sun are functions of absolute time. A model taking an elapsed count would bake the epoch into itself and couldn’t be swapped for one that didn’t.


4. Orbit — dyn_orbit.c

What it parses

A real two-line element set, per the standard — this is the published ISS pair for the shipped epoch, which is not quite what config/minscs.conf ships (see §6):

1 25544U 98067A   24001.50000000  .00016717  00000-0  10270-3 0  9009
2 25544  51.6400 208.9163 0006317  69.9862 290.1993 15.49309239 10003
  • Fixed columns, extracted into a scratch buffer before strtod. sscanf with %lf would run past a field boundary into the next one whenever a value is short.
  • Implied decimal point on eccentricity: 0006317 means 0.0006317. Read as a plain number it gives an orbit six thousand times too eccentric — a spectacular trajectory and no diagnostic.
  • Checksum: modulo-10 sum of digits with - counting one.
  • Two-digit year windowed at 57; day-of-year counts from 1.0 = January 1, 00:00 UTC.

A set that fails any of this, or the sanity limits (eccentricity below 0.9, perigee above the surface), is rejected, logged as an error, and replaced by a 500 km circular orbit at 51.64° — so a typo in the config degrades the run’s fidelity instead of ending it.

What it propagates

Kepler plus the secular J2 terms that dominate SGP4’s own initialisation:

n0'' = n0 (1 + 3/2 J2 (Re/p)^2 sqrt(1-e^2) (1 - 3/2 sin^2 i))    mean-motion correction
dΩ/dt = -3/2 J2 n (Re/p)^2 cos i                                 nodal regression
dω/dt =  3/4 J2 n (Re/p)^2 (5 cos^2 i - 1)                       apsidal rotation

then Kepler’s equation M = E − e sin E by Newton–Raphson, bounded at eight iterations: for e below 0.1 it converges to double precision in three or four, and the bound matters more than the last digit because an unbounded loop in a periodic task is a hazard rather than a refinement. Perifocal position and velocity are rotated into ECI by Rz(−Ω) Rx(−i) Rz(−ω), of which only the first two columns are needed, the perifocal z being zero by definition.

Fidelity envelope

This is not SGP4. It omits:

OmittedConsequence
Atmospheric dragBSTAR is parsed and ignored. Semi-major axis does not decay.
Short- and long-period termsPosition error of hundreds of metres to a few km over a day.
Deep-space extensionOrbits above a 225-minute period are not handled correctly.

That error only matters relative to what consumes it: a kilometre of along-track error feeds a dipole field model that’s itself 10–20 % wrong and a shadow test accurate to a few seconds of orbit. When that stops being true, §3 is where a real SGP4 goes.


5. Geomagnetic field — dyn_env.c

The full IGRF is a spherical-harmonic expansion to degree 13. Keeping only the three degree-one terms leaves a centred tilted dipole:

B(r) = B0 (Re/r)^3 [ 3 (m̂ · r̂) r̂ − m̂ ]

with m̂ = (g11, h11, g10)/B0 and B0 = |(g10, g11, h11)|. Coefficients are IGRF-13 epoch 2020.0 — g10 = −29404.8, g11 = −1450.9, h11 = 4652.5 nT — and are not secularly advanced, the drift being about 0.05 % a year, far inside the model’s own error.

With g10 negative the moment points broadly south, which is why a compass needle’s north end points north: the geomagnetic pole in the northern hemisphere is a magnetic south pole.

Two checks any retune must still pass, both falling out of the coefficients above: ≈ 60 µT at the magnetic pole and ≈ 30 µT horizontal at the magnetic equator, at the surface. If a change breaks either, it’s wrong.

GMST is not optional bookkeeping

The field is defined in an Earth-fixed frame and consumed in an inertial one, so the model rotates the position into ECEF by −GMST, evaluates, and rotates the result back. Get this wrong and the field is plausible in magnitude and useless in direction — and B-dot, which depends entirely on direction, quietly stops working.

Accuracy, and what depends on it

A dipole is good to roughly 10–20 % in magnitude and rather better in direction away from the South Atlantic Anomaly — entirely adequate for B-dot, which needs only the direction of dB/dt to be approximately right and is famously tolerant of gain error because it’s a damping law. It’s not adequate for magnetic attitude determination to any real accuracy, which is why the ADCS ranks its star trackers above TRIAD.


6. Sun and eclipse

Sun — the low-precision Astronomical Almanac series: mean longitude, mean anomaly, equation of centre, obliquity. Four lines, good to about 0.01° over 1950–2050; the Earth’s orbital eccentricity is carried (the sin g term), planetary perturbations are not. 0.01° is two orders of magnitude finer than the sun sensors that consume it, whose cosine response and 1 % noise give a few degrees at best.

Eclipse — a cylindrical shadow, in dyn_world.c because both plants need it: the spacecraft is eclipsed when it’s on the anti-sun side of the Earth (r · ŝ < 0) and within one Earth radius of the shadow axis. Ignoring the penumbra and the Sun’s finite angular size smears the terminator by a few seconds of orbit — irrelevant to a sun sensor reporting a boolean validity or to an array that is either lit or not, and a natural place for a conical model later.

The beta angle, and why the shipped element set’s RAAN is not the published one

The solar beta angle decides whether there is an eclipse at all. β is the Sun’s elevation out of the orbit plane — the angle between ŝ and the plane whose normal is r × v. Eclipse becomes geometrically impossible once |β| exceeds asin(R⊕/r), about 69.7° at this altitude: the shadow cylinder simply doesn’t reach the orbit, and the spacecraft is in permanent sunlight from entirely correct physics.

That’s not hypothetical, and it’s the one trap in this file most likely to be “fixed” back in. The real ISS element set for the shipped epoch (2024 day 1.5) sits at β = −68.6°, one degree inside the limit. Flown unmodified it produces no eclipse at all after about eight simulated hours — which at [eps_dyn] orbit_time_scale = 60 is roughly eight minutes of wall clock, and at [adcs_dyn]’s shipped scale of 1 is longer than any demonstration run. What an operator sees is an orbit counter advancing, an array current pinned high and an eclipse flag that never sets: the exact signature of a frozen orbit counter (§2), from a model that’s working perfectly.

So the shipped tle_line2 in both [adcs_dyn] and [eps_dyn] carries RAAN 90.0000 where the published set has 208.9163. Nothing else is touched — the checksum digit is unchanged, so the line is still format-valid — and at RAAN 90 the beta angle is about −5°, the eclipse fraction 0.39, and it stays there for ten simulated days. This is not a transcription error and must not be “corrected” back to the published value. Both plants fly the same element set, because a spacecraft has one orbit; the ADCS doesn’t care about eclipse and the EPS very much does.

epsPlantInit logs β and the cutoff at boot so the condition announces itself either way:

[EPS] plant: beta=-5.0deg (eclipse cutoff 69.7deg) -- eclipse expected
[EPS] WARN  plant: beta=-68.6deg exceeds the 69.7deg eclipse cutoff -- this element set is in
            permanent sunlight and NO eclipse will occur

7. Rigid body — dyn_rigid.c

Euler’s equations

I ω̇ = τ_mtq + τ_react − ω × (I ω + h_wheel)
τ_mtq   = m × B          dipole against the local field
τ_react = −τ_wheel       every N·m into a rotor comes back out on the body

The gyroscopic term carries the total angular momentum, body plus wheels. Dropping the wheel term is the classic error: it makes a momentum-biased spacecraft behave like an unbiased one, so the coupling a real 3-axis controller spends most of its effort fighting simply isn’t there, and the controller looks better than it is.

Conventions

q is ECI→BODY, scalar first, and the kinematics are q̇ = ½ ω_body ⊗ q, expanded to exactly the four lines the node used before the refactor — deliberately, so that a sign error shows up as a difference from known-good arithmetic. dynQuatRotate is the matching direction-cosine matrix.

Actuators

Wheels — a first-order speed loop toward the commanded rotor speed with time constant wheel_tau_s, torque clamped at wheel_hw_max_torque_nm, speed clamped at wheel_hw_max_rpm. At the speed limit the rotor cannot take more momentum outward, so the torque available to the body in that direction is gone. The plant does not report saturation anywhere: the flight software detects it from the tachometers in the RO segment, which is both how a real controller learns it and the only way consistent with the ICD — an accessor on the plant would be a second channel between the two modules, and there is deliberately only one.

Magnetorquers — the coil delivers what was commanded, clamped to mtq_hw_max_dipole.

Integration

RK4 over the ten-state vector (quaternion, body rate, three rotor speeds), sub-stepped at substep_ms within each control period — five sub-steps per 100 ms period by default.

The field is re-rotated into the body frame at every RK4 stage, from that stage’s own attitude. Passing a body-frame field computed once per control period would hold the torque direction fixed across the step, which is exactly wrong for the case that matters: a spacecraft tumbling fast enough to need detumbling turns several degrees inside one control period.

The quaternion is renormalised once per sub-step rather than per stage. RK4 takes it off the unit sphere by O(h⁵), far below the point where renormalising inside the stages would buy anything.


8. Sensor heads — dyn_sensors.c

Each head takes the truth, degrades it the way the real unit would, and publishes into the RO segment with a validity flag. The degradation is the point: a control law tested against perfect measurements is not tested.

HeadModelValidity
Gyro triadconstant bias + random-walk bias (∝√dt) + white noisealways 1
Magnetometerwhite noisealways 1
Sun cells (6)max(0, ŝ_body · n̂_face) per face, dark current in eclipse; the head reconstructs the vector from opposing pairs0 in eclipse, and when no cell exceeds sun_threshold
Star trackers (2)truth perturbed by a small rotation about a random axis0 inside st_sun_excl_deg of the Sun, or within the Earth’s limb plus st_earth_excl_deg
Tachometersexact rotor speedn/a

Two flags are constants. gyro_valid and mag_valid are hard-wired to 1 and never cleared, because neither head has a failure model. Three application paths are consequently unreachable today: the !mag_valid early return in prvDetumble, the B-dot filter invalidation, and the magnetometer validity bit in telemetry. They’re written and correct, but unexercised — worth knowing before trusting them.

The star-tracker perturbation is a rotation, not independent noise on four components — the latter would leave the quaternion off the unit sphere in a way no real tracker’s output ever is. A blinded tracker keeps its last quaternion rather than zeroing it, because a real unit holds its last solution and zeroing would hand the flight software a non-unit quaternion to trip over if it ignored the flag.

All noise comes from one xorshift32 + Box–Muller seeded from noise_seed, deliberately not rand(): a run has to be reproducible so a misbehaving detumble can be re-run and watched, and so two builds of the same configuration produce the same telemetry.

A deliberate omission. A real magnetorquer saturates the magnetometer that shares its spacecraft, which is why flight B-dot implementations duty-cycle the coils and sample the field only while they’re off. It’s left out so the detumble demonstration exercises the control law rather than the sampling schedule.


9. The interface guard

Both plants carry the same mechanism, on the same terms; this section is the account of it, and adcs.md and eps.md point here.

PUS service 6 isn’t implemented on this platform, and obdb.md §4 argues against uplinkable raw byte ranges into on-board memory. The useful half of the service is the other half: [6,9]/[6,10], check a memory region against an expected checksum. The ICD’s RO+TRUTH segments are that region — [0, AOCS_GUARDED_LEN), which offsetof(aocs_icd_t, rw) defines as ending exactly where the RW segment begins, so the application’s own outputs are outside it and writing a wheel command never trips the check.

Once per control cycle:

  1. the plant writes ro and truth, then pusCrc16s the guarded span — dynGuardStamp;
  2. the application runs, and should write only rw;
  3. the application re-checksums the same span as its last act — dynGuardCheck.

Step 3’s placement is load-bearing, and the obvious alternative is silently useless. Checking at the start of the next cycle asks the question after dynStep has already rewritten essentially every field of the RO segment, so a violation is overwritten before it can be seen and the guard passes forever — including while the application scribbles over the segment on every tick. That was a real defect during development, and it’s exactly what the check exists to catch.

The whole argument holds only if the plant and the application run in one task, in sequence, which is what makes the RW segment single-writer (§11).

A mismatch sets AOCS_FDIR_ICD_VIOLATION, logs an error, and rides bit 4 of the ADCS_RPT_CTRL frame’s FDIR octet to the DHS, which reports it in the same bit of the ADCS housekeeping packet — ICD_VIOL on the ground. The EPS’s guard is the twin of this one over eps_icd.h.

Padding is covered by the checksum, so it must be deterministic. xAocsIcd is static (zero-initialised) and dynReboot memsets the whole image, so padding is zero and stays zero.


10. Configuration — [adcs_dyn]

Read by the node in main() and deliberately not on-board database content: these describe the spacecraft’s physical construction and the world it flies in — the truth model — and a flight computer has no business holding that. Nothing here is reachable from the ground. Contrast [adcs], which holds the control gains and is distributed by the DHS (adcs.md).

KeyUnitDefaultNotes
tle_line1, tle_line2ISS, 2024 day 1.5, RAAN adjusted (§6)69 characters each; CFG_VALUE_LEN is 80 to fit them
orbit_time_scale×1See the warning below
inertia_xx/_yy/_zzkg·m²0.045 / 0.048 / 0.030principal moments, 12U-class
gyro_bias_x/_y/_zdeg/s0.02 / −0.015 / 0.008constant bias
gyro_noise_dpsdeg/s0.011σ white
gyro_walk_dpsdeg/s/√s0.0005why attitude cannot be integrated forever
mtm_noise_utµT0.15
sun_noise0.011σ on a normalised cell
sun_threshold0.05brightest cell below this ⇒ head reports invalid
st_noise_degdeg0.021σ attitude error
st_sun_excl_degdeg30.0boresight-to-Sun keep-out half-angle
st_earth_excl_degdeg20.0margin beyond the limb (~70° in LEO)
wheel_inertia_kgm2kg·m²4.5e-5rotor
wheel_tau_ss0.25speed-loop time constant
wheel_hw_max_rpmrpm6500hardware limit
wheel_hw_max_torque_nmN·m0.004hardware limit
mtq_hw_max_dipoleA·m²3.2hardware limit
substep_msms20plant integration step
noise_seed20260802any fixed value makes a run reproducible

Hardware limits versus commanded limits

*_hw_* here is what the unit can physically deliver. mtq_max_dipole and wheel_max_rpm in [adcs] are what the control law is permitted to ask for. The two are deliberately separate: a controller’s clamp is policy the ground may retune; a rotor’s maximum speed is not. Setting the commanded limit above the hardware limit is legal and interesting — the plant saturates and the flight software raises AOCS_FDIR_WHEEL_SAT, exactly as it would in orbit.

The magnetorquer is sized so a detumble finishes inside a demonstration run; a flight-representative rod of a few tenths of an A·m² would take tens of minutes, which is correct and dull.

orbit_time_scale — the one knob that silently invalidates the control loop

It compresses the orbit and environment only, never the rigid-body integration, so eclipse entry and the field’s rotation are observable in a short run. Leave it at 1 whenever the control law is under test: compressing the orbit speeds up how fast the field sweeps past the spacecraft without touching how fast the spacecraft rotates, and B-dot cannot tell those apart. Raise it only to watch eclipse, the sun angle or the field geometry, none of which care. The full argument, with the measured numbers, is in the [adcs_dyn] comment block of config/minscs.conf and is kept in one place deliberately.


11. The EPS Power Plant

sim/eps_plant.c is the second plant, and it’s built on the same three rules as the first: one interface (eps_icd.h), no calls into the application, and the shared universe underneath. The application it serves is eps.md.

The step

1. universe        dynWorldStep(dt)                  -> r, v, s in ECI, eclipse
2. geometry        beta from r x v and s             -> illumination cosine
3. array           wing.cos(beta) + body.max(0,r.s)  -> generated current
4. battery         READS THE RW SEGMENT               -> stored energy, terminal voltage
5. thermal         two first-order lags               -> battery and radiator truth
6. transducers     degrade the truth                  -> writes the RO segment
7. publish         orbit state, TRUTH                 -> stamps the guard

Step 4 is the closed loop: the rail currents the flight software left in the RW segment are the discharge, and they’re summed here rather than read pre-summed so the plant cannot be shown a load that doesn’t match the rails the application actually switched.

What the array model can and cannot know

The EPS has no attitude sensor and no access to the ADCS’s estimate — that lives in another process, and only its heavily reduced echo ever reaches the bus. So the array model uses only what r, v and ŝ give it: a single-axis sun-tracked wing whose drive turns about the orbit normal (incidence cos β, no attitude needed) plus a zenith-facing body panel on a platform assumed nadir-pointing (incidence max(0, r̂·ŝ)). If the platform tumbles, this model is wrong and doesn’t know it — a true statement about a real EPS rather than a shortcut: without an array-current-to-attitude cross-check it doesn’t have, it cannot tell “shadowed by a bad attitude” from “degraded”.

The one thing this plant does that the ADCS’s does not

It models a battery whose properties the flight software gets wrong on purpose. The cell’s true capacity, open-circuit-voltage curve and charge acceptance live in [eps_dyn]; the fuel gauge’s beliefs about the first two are on-board database content and are deliberately different numbers. truth.soc_pct against the gauge’s rw.soc_est_pct is the EPS’s answer to q_true against q_est, and exactly as with the ADCS’s reference models, making the two sets equal destroys the measurement (eps.md §4).

The single-writer precondition

§9’s argument — stamp after the plant writes, check as the application’s last act — is only valid if the plant and the application run in one task, in sequence. The ADCS always did. The EPS did not: its rails were latched from the dispatch task while the model task integrated. Making the model task the sole writer of the RW segment was a prerequisite for this plant, not a tidy-up, and it retired a live race on the battery state as a side effect.

There is no reboot entry point. dynReboot exists because the ADCS is one of the cold-redundant pairs and its board can be power cycled from orbit. The EPS is the box that does the switching, so eps_plant.h has no counterpart and shouldn’t grow one.


12. Known simplifications

An index, so none of them is a surprise; each is argued where it’s made.

AreaSimplification§
Orbitno drag, no periodic terms, no deep space — a few km/day against real SGP44
Fielddegree-1 only, no secular drift, no South Atlantic Anomaly5
Sunno planetary perturbations; ~0.01°6
Eclipsecylindrical, no penumbra, no finite solar disc6
Torquesmagnetic only — no flexibility, slosh, gravity gradient or SRP7
Sensorsgyro and magnetometer never fail, so two validity flags are constants and the application’s dropout paths are unexercised8
Sensorssampling is instantaneous and simultaneous — no read latency or skew between heads; no magnetorquer/magnetometer interaction, so no coil duty-cycling8
Timetruth.orbit_ms wraps after 49 days of simulated time, and the two plants share physics but not a clock2
Powerarray output is attitude-independent, absent a wire the EPS does not have; a tumbling spacecraft keeps producing full power11
Powerno bus impedance, no switching transient, no discharge-side loss — charge acceptance is modelled, being what a coulomb counter cannot see; the cell does not age over a run11
Last updated on