Designing Solar Arrays That Survive Space

Risk-Optimized Degradation Planning with Python

A satellite lives or dies by its power budget. The solar array is the only source of electricity for the entire mission, yet it is under constant attack from trapped radiation, solar particle events, thermal cycling, and ultraviolet exposure. Every year in orbit, the array delivers a little less power than the year before.

The naive engineering answer is to size the array for the average degradation. That design misses its end-of-life requirement roughly half of the time. The opposite answer is to add a huge safety margin, which costs launch mass, and mass is money. In this article we treat the problem as a chance-constrained optimization and solve it with Monte Carlo simulation in Python.

1. The Problem

We design a solar array for a satellite in a harsh radiation environment (think of a medium-altitude orbit). The mission requirements are:

  • Mission life: $T = 15$ years
  • Required end-of-life (EOL) power: $P_{\mathrm{req}} = 6000\ \mathrm{W}$
  • The probability of falling short of $P_{\mathrm{req}}$ at EOL must not exceed $\alpha = 5%$

We have two design variables:

  • $t$: the equivalent shield (coverglass) thickness in mm. A thicker shield blocks more radiation, but it is heavy.
  • $A$: the array area in $\mathrm{m}^2$. A larger array produces more power, but it is also heavy.

The objective is to minimize the array mass:

$$
\min_{t,,A}; M(t,A) = A,\bigl(m_0 + \rho,t\bigr)
$$

$$
\text{subject to}\quad \Pr\bigl[,P_{\mathrm{EOL}}(t,A,\omega) \ge P_{\mathrm{req}},\bigr] \ge 1-\alpha
$$

Here $m_0 = 4.2\ \mathrm{kg/m^2}$ is the areal mass of the bare array, $\rho = 2.6\ \mathrm{kg/m^2/mm}$ is the areal mass added per millimeter of shield, and $\omega$ denotes one random scenario.

2. The Degradation Model

The power at end of life is

$$
P_{\mathrm{EOL}} = A, p_{\mathrm{BOL}},\bigl(1 - L_{\mathrm{rad}}\bigr)\bigl(1 - k,T\bigr)
$$

where $p_{\mathrm{BOL}} = S_0,\eta,f_{\mathrm{pack}},f_{\cos},f_{\mathrm{temp}} \approx 306.6\ \mathrm{W/m^2}$ is the beginning-of-life power density.

The radiation loss follows the well-known semi-empirical logarithmic law:

$$
L_{\mathrm{rad}} = C,\log_{10}!\left(1 + \frac{\Phi_{\mathrm{eff}}}{\Phi_x}\right)
$$

The effective fluence behind the shield is attenuated exponentially with thickness. It has two components, a continuous background and the sum of discrete solar particle events (SPEs):

$$
\Phi_{\mathrm{eff}} = \Phi_{\mathrm{bg}},e^{-t/\lambda_{\mathrm{bg}}} + \Phi_{\mathrm{spe}},e^{-t/\lambda_{\mathrm{spe}}}
$$

$$
\Phi_{\mathrm{bg}} = r_{\mathrm{bg}},T, \qquad \Phi_{\mathrm{spe}} = \sum_{j=1}^{N} \phi_j, \qquad N \sim \mathrm{Poisson}(\lambda_{\mathrm{ev}} T)
$$

The random ingredients are:

Quantity Distribution
Background fluence rate $r_{\mathrm{bg}}$ Log-normal, median $4\times10^{14}$, $\sigma = 0.30$
SPE fluence per event $\phi_j$ Log-normal, median $8\times10^{13}$, $\sigma = 1.5$
Number of SPEs $N$ Poisson, 1 event per year
Damage coefficient $C$ Normal, mean 0.18, std 0.02
Thermal/UV loss rate $k$ Normal, mean 0.4 %/year, std 0.12 %/year

3. A Fast Solution Strategy

A brute-force approach would simulate every design pair $(t, A)$ separately. The chance constraint has a much better structure. For a fixed thickness $t$, define the EOL power per unit area $u(t,\omega) = P_{\mathrm{EOL}}/A$. The shortfall event is $A,u < P_{\mathrm{req}}$, and therefore

$$
\Pr\bigl[A,u(t,\omega) \ge P_{\mathrm{req}}\bigr] \ge 1-\alpha
;\Longleftrightarrow;
A \ge A_{\mathrm{req}}(t) = \frac{P_{\mathrm{req}}}{Q_{\alpha}\bigl[u(t,\cdot)\bigr]}
$$

where $Q_\alpha$ is the $\alpha$-quantile of the scenario distribution. The two-dimensional stochastic problem collapses into a one-dimensional deterministic problem:

$$
\min_{t}; M(t) = A_{\mathrm{req}}(t),\bigl(m_0 + \rho,t\bigr)
$$

The full risk map over $(t, A)$ is also cheap. After sorting the scenarios once per thickness, the shortfall probability for any area is a single binary search. All 20,000 scenarios share the same random numbers across every design (common random numbers), which keeps the risk surface smooth and the comparison between designs fair. Everything is vectorized with NumPy, with no Python loop over scenarios.

4. The Complete Source Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
import time
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import Normalize
from mpl_toolkits.mplot3d import Axes3D # noqa: F401

plt.style.use("dark_background")

# ------------------------------------------------------------
# 1. Problem parameters
# ------------------------------------------------------------
SEED = 2026
N_MC = 20000
T_YEARS = 15.0
P_REQ = 6000.0
CONFIDENCE = 0.95
ALPHA = 1.0 - CONFIDENCE

S0 = 1361.0
ETA_BOL = 0.30
PACKING = 0.85
COS_LOSS = 0.95
TEMP_FACTOR = 0.93
UNIT_POWER_BOL = S0 * ETA_BOL * PACKING * COS_LOSS * TEMP_FACTOR

M_BASE = 4.2
RHO_GLASS = 2.6

PHI_X = 5.0e13
LAM_BG = 0.10
LAM_SPE = 0.12
BG_RATE_MEDIAN = 4.0e14
BG_SIGMA = 0.30
SPE_RATE = 1.0
SPE_MEDIAN = 8.0e13
SPE_SIGMA = 1.5
C_MEAN, C_STD = 0.18, 0.02
K_MEAN, K_STD = 0.004, 0.0012

# ------------------------------------------------------------
# 2. Monte Carlo scenarios (shared by every design candidate)
# ------------------------------------------------------------
rng = np.random.default_rng(SEED)
BG_RATE = rng.lognormal(np.log(BG_RATE_MEDIAN), BG_SIGMA, N_MC)
C_SAMPLE = np.clip(rng.normal(C_MEAN, C_STD, N_MC), 0.10, 0.26)
K_SAMPLE = np.clip(rng.normal(K_MEAN, K_STD, N_MC), 0.0, None)
N_EVENTS = rng.poisson(SPE_RATE * T_YEARS, N_MC)
MAX_EV = max(int(N_EVENTS.max()), 1)
EV_MAG = rng.lognormal(np.log(SPE_MEDIAN), SPE_SIGMA, (N_MC, MAX_EV))
EV_TIME = rng.uniform(0.0, T_YEARS, (N_MC, MAX_EV))
EV_MASK = np.arange(MAX_EV)[None, :] < N_EVENTS[:, None]
EV_MAG = EV_MAG * EV_MASK
PHI_BG_EOL = BG_RATE * T_YEARS
PHI_SPE_EOL = EV_MAG.sum(axis=1)


def eol_unit_power(t_mm):
"""End-of-life power per square metre, shape (n_thickness, N_MC)."""
t = np.atleast_1d(np.asarray(t_mm, dtype=float))[:, None]
phi = PHI_BG_EOL[None, :] * np.exp(-t / LAM_BG) + PHI_SPE_EOL[None, :] * np.exp(-t / LAM_SPE)
rad_loss = C_SAMPLE[None, :] * np.log10(1.0 + phi / PHI_X)
other_loss = K_SAMPLE[None, :] * T_YEARS
return UNIT_POWER_BOL * (1.0 - rad_loss) * (1.0 - other_loss)


def power_timeseries(t_mm, area, n_paths=4000, n_steps=181):
"""Power history of one design for the first n_paths scenarios."""
times = np.linspace(0.0, T_YEARS, n_steps)
att_bg = np.exp(-t_mm / LAM_BG)
att_spe = np.exp(-t_mm / LAM_SPE)
ev_m = EV_MAG[:n_paths]
ev_t = EV_TIME[:n_paths]
out = np.empty((n_steps, n_paths))
for i, tau in enumerate(times):
spe = (ev_m * (ev_t <= tau)).sum(axis=1)
phi = BG_RATE[:n_paths] * tau * att_bg + spe * att_spe
rad = C_SAMPLE[:n_paths] * np.log10(1.0 + phi / PHI_X)
oth = K_SAMPLE[:n_paths] * tau
out[i] = area * UNIT_POWER_BOL * (1.0 - rad) * (1.0 - oth)
return times, out


# ------------------------------------------------------------
# 3. Optimisation (quantile reduction + vectorised risk map)
# ------------------------------------------------------------
T_GRID = np.linspace(0.05, 0.80, 121)
A_GRID = np.linspace(18.0, 44.0, 61)

t_start = time.perf_counter()
U = eol_unit_power(T_GRID)
U_SORTED = np.sort(U, axis=1)
U_Q = np.quantile(U, ALPHA, axis=1)
A_REQ = P_REQ / U_Q
MASS_REQ = A_REQ * (M_BASE + RHO_GLASS * T_GRID)
i_opt = int(np.argmin(MASS_REQ))
T_OPT = float(T_GRID[i_opt])
A_OPT = float(A_REQ[i_opt])
M_OPT = float(MASS_REQ[i_opt])

thresholds = P_REQ / A_GRID
RISK = np.empty((T_GRID.size, A_GRID.size))
for i in range(T_GRID.size):
RISK[i] = np.searchsorted(U_SORTED[i], thresholds, side="left") / N_MC
RISK_PCT = 100.0 * RISK
MASS_MAP = (M_BASE + RHO_GLASS * T_GRID)[:, None] * A_GRID[None, :]

CONF_GRID = np.linspace(0.50, 0.99, 50)
Q = np.quantile(U, 1.0 - CONF_GRID, axis=1)
M_CONF = (P_REQ / Q) * (M_BASE + RHO_GLASS * T_GRID)[None, :]
best_idx = M_CONF.argmin(axis=1)
BEST_MASS = M_CONF[np.arange(CONF_GRID.size), best_idx]
BEST_T = T_GRID[best_idx]
elapsed = time.perf_counter() - t_start

u_opt = U[i_opt]
p_eol = A_OPT * u_opt
risk_opt = float(np.mean(p_eol < P_REQ))
A_NOM = P_REQ / float(np.median(u_opt))
risk_nom = float(np.mean(A_NOM * u_opt < P_REQ))
mass_nom = A_NOM * (M_BASE + RHO_GLASS * T_OPT)

# ------------------------------------------------------------
# 4. Console report
# ------------------------------------------------------------
print("=" * 72)
print(" Solar array degradation risk optimisation (chance-constrained design)")
print("=" * 72)
print(f" Mission life : {T_YEARS:.0f} years")
print(f" Required EOL power : {P_REQ:.0f} W")
print(f" Required confidence : {100 * CONFIDENCE:.1f} %")
print(f" Monte Carlo scenarios : {N_MC:,}")
print(f" Design points evaluated : {T_GRID.size} x {A_GRID.size} = {T_GRID.size * A_GRID.size:,}")
print(f" Computation time : {elapsed:.2f} s")
print("-" * 72)
print(" Optimal design")
print(f" Shield thickness : {T_OPT:.3f} mm")
print(f" Array area : {A_OPT:.2f} m^2")
print(f" Array mass : {M_OPT:.1f} kg")
print(f" Shortfall probability : {100 * risk_opt:.2f} %")
print("-" * 72)
print(" Nominal (median-sized) design at the same thickness")
print(f" Array area : {A_NOM:.2f} m^2")
print(f" Array mass : {mass_nom:.1f} kg")
print(f" Shortfall probability : {100 * risk_nom:.2f} %")
print("-" * 72)
print(f" {'t [mm]':>8} {'A_req [m^2]':>13} {'Mass [kg]':>11} {'vs optimum [%]':>16}")
for tv in [0.10, 0.20, 0.30, 0.40, T_OPT, 0.60, 0.70]:
k = int(np.argmin(np.abs(T_GRID - tv)))
print(f" {T_GRID[k]:8.3f} {A_REQ[k]:13.2f} {MASS_REQ[k]:11.1f} {100 * (MASS_REQ[k] / M_OPT - 1.0):16.2f}")
print("=" * 72)

# ------------------------------------------------------------
# 5. Visualisation (single combined figure)
# ------------------------------------------------------------
times, PS = power_timeseries(T_OPT, A_OPT)
q05, q25, q50, q75, q95 = np.percentile(PS, [5, 25, 50, 75, 95], axis=1)

Xg, Yg = np.meshgrid(T_GRID, A_GRID)

fig = plt.figure(figsize=(21, 12))
gs = fig.add_gridspec(2, 3)

# (1) 3D risk surface
ax1 = fig.add_subplot(gs[0, 0], projection="3d")
ax1.plot_surface(Xg, Yg, RISK_PCT.T, cmap="plasma", edgecolor="none", rstride=2, cstride=2, alpha=0.95)
ax1.contour(Xg, Yg, RISK_PCT.T, levels=[100.0 * ALPHA], colors="cyan", linewidths=2.5)
ax1.scatter([T_OPT], [A_OPT], [100.0 * ALPHA], s=180, c="white", marker="*", depthshade=False)
ax1.set_xlabel("Shield thickness t [mm]")
ax1.set_ylabel("Array area A [m$^2$]")
ax1.set_zlabel("Shortfall risk [%]")
ax1.set_title("3D risk surface (cyan = 5 % chance constraint)")
ax1.view_init(elev=26, azim=-125)

# (2) 3D mass surface with feasibility
ax2 = fig.add_subplot(gs[0, 1], projection="3d")
norm = Normalize(vmin=float(MASS_MAP.min()), vmax=float(MASS_MAP.max()))
face = plt.get_cmap("viridis")(norm(MASS_MAP.T))
face[RISK.T > ALPHA] = (0.25, 0.25, 0.28, 0.35)
ax2.plot_surface(Xg, Yg, MASS_MAP.T, facecolors=face, shade=False, rstride=2, cstride=2, edgecolor="none")
ax2.scatter([T_OPT], [A_OPT], [M_OPT], s=200, c="red", marker="*", depthshade=False)
ax2.set_xlabel("Shield thickness t [mm]")
ax2.set_ylabel("Array area A [m$^2$]")
ax2.set_zlabel("Array mass [kg]")
ax2.set_title("3D mass surface (gray = infeasible, red star = optimum)")
ax2.view_init(elev=26, azim=-45)

# (3) Required area and mass versus thickness
ax3 = fig.add_subplot(gs[0, 2])
ax3b = ax3.twinx()
l1, = ax3.plot(T_GRID, A_REQ, color="#00e5ff", lw=2.4)
l2, = ax3b.plot(T_GRID, MASS_REQ, color="#ff9100", lw=2.4)
ax3.axvline(T_OPT, color="white", ls="--", lw=1.2)
ax3b.scatter([T_OPT], [M_OPT], s=140, c="red", zorder=5)
ax3.set_xlabel("Shield thickness t [mm]")
ax3.set_ylabel("Required area A [m$^2$]", color="#00e5ff")
ax3b.set_ylabel("Array mass [kg]", color="#ff9100")
ax3.set_title("Area-mass trade-off at 95 % confidence")
ax3.legend(handles=[l1, l2], labels=["Required area", "Array mass"], loc="upper center")
ax3.grid(alpha=0.2)

# (4) Power history fan chart
ax4 = fig.add_subplot(gs[1, 0])
for k in range(25):
ax4.plot(times, PS[:, k], color="white", lw=0.5, alpha=0.3)
ax4.fill_between(times, q05, q95, color="#00e5ff", alpha=0.22, label="5-95 % band")
ax4.fill_between(times, q25, q75, color="#00e5ff", alpha=0.35, label="25-75 % band")
ax4.plot(times, q50, color="#ffea00", lw=2.4, label="Median")
ax4.axhline(P_REQ, color="#ff1744", ls="--", lw=2.0, label="Requirement")
ax4.set_xlabel("Mission time [years]")
ax4.set_ylabel("Array power [W]")
ax4.set_title("Power history of the optimal design")
ax4.legend(loc="lower left")
ax4.grid(alpha=0.2)

# (5) EOL power distribution
ax5 = fig.add_subplot(gs[1, 1])
ax5.hist(p_eol, bins=80, color="#7c4dff", alpha=0.9)
ax5.axvspan(float(p_eol.min()), P_REQ, color="#ff1744", alpha=0.25)
ax5.axvline(P_REQ, color="#ff1744", ls="--", lw=2.0, label=f"Requirement (risk {100 * risk_opt:.1f} %)")
ax5.axvline(float(np.median(p_eol)), color="#ffea00", lw=2.0, label="Median")
ax5.set_xlabel("End-of-life power [W]")
ax5.set_ylabel("Number of scenarios")
ax5.set_title("End-of-life power distribution")
ax5.legend(loc="upper right")
ax5.grid(alpha=0.2)

# (6) Price of confidence
ax6 = fig.add_subplot(gs[1, 2])
ax6b = ax6.twinx()
m1, = ax6.plot(100.0 * CONF_GRID, BEST_MASS, color="#ff9100", lw=2.4)
m2, = ax6b.plot(100.0 * CONF_GRID, BEST_T, color="#69f0ae", lw=2.4)
ax6.axvline(100.0 * CONFIDENCE, color="white", ls="--", lw=1.2)
ax6.set_xlabel("Required confidence [%]")
ax6.set_ylabel("Minimum array mass [kg]", color="#ff9100")
ax6b.set_ylabel("Optimal thickness t [mm]", color="#69f0ae")
ax6.set_title("The price of confidence")
ax6.legend(handles=[m1, m2], labels=["Minimum mass", "Optimal thickness"], loc="upper left")
ax6.grid(alpha=0.2)

fig.suptitle("Solar Array Degradation Risk Optimization", fontsize=20, y=0.97)
fig.subplots_adjust(left=0.04, right=0.97, top=0.91, bottom=0.07, wspace=0.28, hspace=0.30)
plt.show()

5. Code Walkthrough

5.1 Parameters

The first block collects every physical and economic assumption in one place. UNIT_POWER_BOL multiplies the solar constant by the cell efficiency, the packing factor, the average cosine loss, and a temperature derating factor, giving about 306.6 W per square meter at beginning of life. M_BASE and RHO_GLASS define the mass model $M = A(m_0 + \rho t)$. The remaining constants describe the radiation environment and the uncertainty of each ingredient.

5.2 Scenario Generation

Each of the 20,000 scenarios carries its own background fluence rate, damage coefficient $C$, and thermal/UV loss rate $k$. Solar particle events need more care because the number of events varies from scenario to scenario. We draw a Poisson count per scenario, then generate a padded matrix of event magnitudes and event times with MAX_EV columns. A boolean mask, EV_MASK, zeroes out the unused columns. Summing along the event axis gives the total SPE fluence per scenario, PHI_SPE_EOL. The event times are kept because we need them later for the power history.

The clipping of $C$ and $k$ keeps the sampled values physically meaningful, since a negative degradation rate has no meaning.

5.3 The Vectorized Core

eol_unit_power is the heart of the model. Given a whole vector of shield thicknesses, it returns a matrix of shape (thickness, scenario). Broadcasting handles the exponential attenuation, the logarithmic radiation law, and the linear thermal/UV loss in a single expression. No Python loop touches the scenarios, so evaluating all 121 thicknesses against 20,000 scenarios takes a fraction of a second.

5.4 Quantile Reduction

The line U_Q = np.quantile(U, ALPHA, axis=1) returns, for every thickness, the power density that 95% of the scenarios exceed. Dividing $P_{\mathrm{req}}$ by it gives the smallest area that satisfies the chance constraint, exactly as derived in Section 3. Multiplying by the areal mass gives the mass curve, and argmin picks the optimal thickness.

5.5 The Risk Map

To draw the full 3D risk surface we still need the shortfall probability for arbitrary $(t, A)$ pairs. Each row of U is sorted once, after which np.searchsorted counts how many scenarios fall below the threshold $P_{\mathrm{req}}/A$ for all 61 areas simultaneously. The loop runs only over the 121 thicknesses, never over scenarios or areas.

5.6 The Price of Confidence

The same quantile trick is repeated for 50 different confidence levels between 50% and 99% with a single call to np.quantile using a vector of probabilities. This yields the minimum achievable mass as a function of the required confidence, which is the practical trade-off a program manager cares about.

5.7 Time History

power_timeseries replays the first 4,000 scenarios through time. At every time step, only the solar particle events that have already occurred contribute to the fluence, while the background fluence grows linearly. The final time step reproduces the EOL values used in the optimization, so the fan chart is consistent with the optimizer.

5.8 Visualization

All six panels are drawn into one figure. The two 3D panels show the risk surface and the mass surface, the latter with infeasible designs painted gray so that the feasible region and the optimum stand out at a glance.

6. Execution Results

========================================================================
 Solar array degradation risk optimisation (chance-constrained design)
========================================================================
 Mission life               : 15 years
 Required EOL power         : 6000 W
 Required confidence        : 95.0 %
 Monte Carlo scenarios      : 20,000
 Design points evaluated    : 121 x 61 = 7,381
 Computation time           : 0.29 s
------------------------------------------------------------------------
 Optimal design
   Shield thickness         : 0.575 mm
   Array area               : 23.01 m^2
   Array mass               : 131.0 kg
   Shortfall probability    : 5.00 %
------------------------------------------------------------------------
 Nominal (median-sized) design at the same thickness
   Array area               : 21.96 m^2
   Array mass               : 125.0 kg
   Shortfall probability    : 50.00 %
------------------------------------------------------------------------
   t [mm]   A_req [m^2]   Mass [kg]   vs optimum [%]
    0.100         35.70       159.2            21.50
    0.200         31.41       148.3            13.14
    0.300         28.18       140.3             7.10
    0.400         25.75       134.9             2.96
    0.575         23.01       131.0             0.00
    0.600         22.75       131.1             0.02
    0.700         22.07       132.9             1.39
========================================================================

7. Interpreting the Results

The optimal design. With the random seed used here, the optimizer selects a shield thickness of about 0.575 mm and an array area of about 23.0 $\mathrm{m}^2$, for a total array mass of about 131 kg. The Monte Carlo estimate of the shortfall probability is 5.00%, matching the chance constraint with no wasted margin.

The nominal design is a trap. Sizing the array with the median degradation at the same thickness needs only about 22.0 $\mathrm{m}^2$ and 125 kg. That is 6 kg lighter, but it fails to deliver 6000 W in half of all scenarios. The console report shows this clearly: a shortfall probability of 50%.

The 3D risk surface (top left). The surface is close to a cliff. For a given thickness there is a narrow band of areas over which the shortfall probability climbs from nearly 0% to nearly 100%, because the scenario spread is small compared with the design range. The cyan curve marks the 5% level, and every design on or beyond it is acceptable. The optimum lies on that curve, which is exactly what a chance-constrained optimum should do.

The 3D mass surface (top center). The mass grows almost linearly with area, while the effect of thickness is more subtle. The gray region marks infeasible designs. The red star sits on the boundary of the feasible region, at the lowest mass point on it.

The trade-off curve (top right). The required area decreases steadily as the shield gets thicker, because radiation damage shrinks. The mass, however, has a minimum. Thin shields force a large array; thick shields add glass mass faster than they save array mass. At 0.10 mm the array is about 21.5% heavier than the optimum, and at 0.30 mm it is still about 7.1% heavier. The curve is quite flat near the optimum, so a thickness between 0.5 and 0.7 mm costs at most about 1.4% extra mass. Engineers can pick a practical thickness without losing much.

The fan chart (bottom left). The median power falls from about 7050 W at launch to roughly 6300 W after 15 years. The lower edge of the 5-95% band touches the 6000 W requirement line at the end of the mission, which is precisely what a 95% confidence design should look like. The thin white lines are individual scenarios; sudden downward steps are solar particle events.

The EOL distribution (bottom center). The histogram has a longer tail toward low power. Heavy-tailed solar particle events produce rare but severe losses, and the red-shaded region to the left of the requirement contains exactly 5% of the scenarios.

The price of confidence (bottom right). Raising the required confidence from 50% to 99% raises the minimum mass from about 125 kg to about 134 kg, and the curve steepens sharply near the high end. The last few percentage points of reliability are the most expensive ones. Meanwhile, the optimal thickness creeps upward with the required confidence, because a thicker shield suppresses the heavy tail of the fluence distribution.

8. Conclusion

We turned a vague engineering worry into a precise optimization problem: minimize array mass subject to a probabilistic end-of-life power requirement. The key mathematical step was the quantile reduction

$$
A_{\mathrm{req}}(t) = \frac{P_{\mathrm{req}}}{Q_{\alpha}\bigl[u(t,\cdot)\bigr]}
$$

which converts a stochastic two-variable problem into a fast one-dimensional search, and vectorized NumPy evaluates the whole design space in a fraction of a second.

The results deliver three lessons. First, designing for the average is a coin flip. Second, the mass-optimal shield is neither the thinnest nor the thickest, and the optimum is broad enough to leave room for practical engineering judgment. Third, the last few percent of confidence are expensive, so the confidence level itself should be a deliberate management decision.

The same framework extends naturally to other risk drivers: eclipse thermal cycling in different orbits, cell-technology selection, in-orbit annealing, or a multi-objective formulation with launch cost. Only the scenario generator and the mass model need to change.