8U CubeSat Power Generation

Demonstration of the missiontools solar power tools.

Scenario

  • 8U CubeSat (10 × 20 × 40 cm, 1×2×4U configuration)

  • 500 km sun-synchronous orbit, nadir-pointed

  • Solar cells on all faces except nadir, 80% fill factor, 30% efficiency

  • Two cases: LTAN 12:00 (noon) and LTAN 06:00 (dawn-dusk)

[1]:
import numpy as np
import matplotlib.pyplot as plt

from missiontools import Spacecraft, NormalVectorSolarConfig
from missiontools.attitude import AttitudeLaw

1. CubeSat geometry

Body frame convention (nadir pointing): body-z = nadir, body-x = along-track. The 40 cm long axis is along body-z (nadir), so the nadir face (+z) is 10 × 20 cm.

[2]:
FILL_FACTOR = 0.80
EFFICIENCY  = 0.30

# Panel normals (body frame) and gross areas (m²)
faces = {
    'Zenith (+Z anti-nadir)': ([0, 0, -1], 0.10 * 0.20),
    '+X side':                ([1, 0,  0], 0.20 * 0.40),
    '-X side':               ([-1, 0,  0], 0.20 * 0.40),
    '+Y side':                ([0, 1,  0], 0.10 * 0.40),
    '-Y side':                ([0, -1, 0], 0.10 * 0.40),
}

normals = np.array([v[0] for v in faces.values()], dtype=float)
areas   = np.array([v[1] * FILL_FACTOR for v in faces.values()])

print(f"{'Face':<30} {'Normal':>14} {'Cell area (cm²)':>16}")
print('-' * 62)
for name, (n, a_gross) in faces.items():
    print(f"{name:<30} {str(n):>14} {a_gross * FILL_FACTOR * 1e4:>13.1f}")
print(f"{'Total':<30} {'':>14} {areas.sum() * 1e4:>13.1f}")
Face                                   Normal  Cell area (cm²)
--------------------------------------------------------------
Zenith (+Z anti-nadir)             [0, 0, -1]         160.0
+X side                             [1, 0, 0]         640.0
-X side                            [-1, 0, 0]         640.0
+Y side                             [0, 1, 0]         320.0
-Y side                            [0, -1, 0]         320.0
Total                                                2080.0

2. LTAN 12:00 (noon orbit)

[3]:
EPOCH = np.datetime64('2025-03-20T12:00:00', 'us')  # spring equinox

sc_noon = Spacecraft.sunsync(
    altitude_km=500.0,
    node_solar_time='12:00',
    epoch=EPOCH,
)
sc_noon.attitude_law = AttitudeLaw.nadir(0.0) # no rotation around local zenith

cfg_noon = NormalVectorSolarConfig(normals, areas, efficiency=EFFICIENCY)
sc_noon.add_solar_config(cfg_noon)

print(f"Altitude        : 500 km")
print(f"Inclination     : {np.degrees(sc_noon.i):.2f}°")
print(f"LTAN            : 12:00")
print(f"Propagator      : {sc_noon.propagator_type}")
Altitude        : 500 km
Inclination     : 97.40°
LTAN            : 12:00
Propagator      : j2
[4]:
# Orbital period
mu = sc_noon.central_body_mu
period_s = 2 * np.pi * np.sqrt(sc_noon.a**3 / mu)
period   = np.timedelta64(int(period_s * 1e6), 'us')

# Power generation over one orbit
result_noon = cfg_noon.generation(EPOCH, EPOCH + period, np.timedelta64(10, 's'))
oap_noon    = cfg_noon.oap()

print(f"Orbital period  : {period_s / 60:.1f} min")
print(f"Orbit avg power : {oap_noon:.2f} W")
Orbital period  : 94.6 min
Orbit avg power : 13.56 W
[5]:
elapsed_noon = (result_noon['t'] - result_noon['t'][0]) / np.timedelta64(1, 'm')

fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(elapsed_noon, result_noon['power'], linewidth=0.8, label='Instantaneous')
ax.axhline(oap_noon, color='tab:red', linestyle='--', linewidth=1, label=f'OAP = {oap_noon:.2f} W')
ax.set_xlabel('Time (minutes)')
ax.set_ylabel('Power (W)')
ax.set_title('Solar power generation \u2014 LTAN 12:00')
ax.legend()
ax.set_xlim(0, elapsed_noon[-1])
ax.set_ylim(bottom=0)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
../_images/examples_cubesat_power_7_0.png

3. LTAN 06:00 (dawn-dusk orbit)

[6]:
sc_dd = Spacecraft.sunsync(
    altitude_km=500.0,
    node_solar_time='06:00',
    epoch=EPOCH,
)
sc_dd.attitude_law = AttitudeLaw.nadir(0.5*np.pi) # rotate 90 degrees about local zenith for peak generation

cfg_dd = NormalVectorSolarConfig(normals, areas, efficiency=EFFICIENCY)
sc_dd.add_solar_config(cfg_dd)

result_dd = cfg_dd.generation(EPOCH, EPOCH + period, np.timedelta64(10, 's'))
oap_dd    = cfg_dd.oap()

print(f"LTAN            : 06:00")
print(f"Orbit avg power : {oap_dd:.2f} W")
LTAN            : 06:00
Orbit avg power : 27.35 W
[7]:
elapsed_dd = (result_dd['t'] - result_dd['t'][0]) / np.timedelta64(1, 'm')

fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(elapsed_dd, result_dd['power'], linewidth=0.8, label='Instantaneous')
ax.axhline(oap_dd, color='tab:red', linestyle='--', linewidth=1, label=f'OAP = {oap_dd:.2f} W')
ax.set_xlabel('Time (minutes)')
ax.set_ylabel('Power (W)')
ax.set_title('Solar power generation \u2014 LTAN 06:00')
ax.legend()
ax.set_xlim(0, elapsed_dd[-1])
ax.set_ylim(bottom=0)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
../_images/examples_cubesat_power_10_0.png

4. Comparison

[8]:
fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(elapsed_noon, result_noon['power'], linewidth=0.8, label='LTAN 12:00', color='tab:blue')
ax.plot(elapsed_dd,   result_dd['power'],   linewidth=0.8, label='LTAN 06:00', color='tab:orange')
ax.axhline(oap_noon, color='tab:blue',   linestyle='--', linewidth=1, alpha=0.7, label=f'OAP 12:00 = {oap_noon:.2f} W')
ax.axhline(oap_dd,   color='tab:orange', linestyle='--', linewidth=1, alpha=0.7, label=f'OAP 06:00 = {oap_dd:.2f} W')
ax.set_xlabel('Time (minutes)')
ax.set_ylabel('Power (W)')
ax.set_title('Solar power comparison \u2014 noon vs dawn-dusk orbit')
ax.legend()
ax.set_xlim(0, max(elapsed_noon[-1], elapsed_dd[-1]))
ax.set_ylim(bottom=0)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
../_images/examples_cubesat_power_12_0.png
[9]:
print('=' * 45)
print('Power summary \u2014 8U CubeSat, 500 km SSO')
print('=' * 45)
print(f"{'':20} {'LTAN 12:00':>12} {'LTAN 06:00':>12}")
print('-' * 45)
print(f"{'OAP (W)':20} {oap_noon:>12.2f} {oap_dd:>12.2f}")
print(f"{'Peak power (W)':20} {result_noon['power'].max():>12.2f} {result_dd['power'].max():>12.2f}")
eclipse_noon = (result_noon['power'] == 0).sum() / len(result_noon['power']) * 100
eclipse_dd   = (result_dd['power'] == 0).sum()   / len(result_dd['power'])   * 100
print(f"{'Eclipse fraction':20} {eclipse_noon:>11.1f}% {eclipse_dd:>11.1f}%")
=============================================
Power summary — 8U CubeSat, 500 km SSO
=============================================
                       LTAN 12:00   LTAN 06:00
---------------------------------------------
OAP (W)                     13.56        27.35
Peak power (W)              27.11        27.89
Eclipse fraction            37.6%         0.0%