Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions calculators/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from .atmosphere import standard_atmosphere, alveolar_PO2 # type: ignore
from .tuc import estimate_tuc # type: ignore
from .g_force import g_loc_time # type: ignore
from .radiation import dose_rate # type: ignore

__all__ = [
"standard_atmosphere",
"alveolar_PO2",
"estimate_tuc",
"g_loc_time",
"dose_rate",
]
72 changes: 72 additions & 0 deletions calculators/atmosphere.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import math

# Physical constants
T0 = 288.15 # Sea-level standard temperature (K)
P0 = 101_325 # Sea-level standard pressure (Pa)
L = 0.0065 # Temperature lapse rate in the troposphere (K/m)
R = 8.31447 # Universal gas constant (J/(mol·K))
g = 9.80665 # Acceleration due to gravity (m/s²)
M = 0.0289644 # Molar mass of dry air (kg/mol)
FIO2_STANDARD = 0.2095 # Fraction of oxygen in dry air


def _to_meters(altitude_ft: float) -> float:
"""Convert feet to metres."""
return altitude_ft * 0.3048


def standard_atmosphere(altitude_m: float | int) -> dict:
"""Return ISA properties up to ~32 km altitude.

Parameters
----------
altitude_m : float | int
Geometric height above sea level in metres.

Returns
-------
dict
temperature_C : Ambient temperature in °C
pressure_Pa : Ambient pressure in pascals
density_kg_m3 : Air density in kg/m³
pO2_Pa : Partial pressure of oxygen in pascals
"""
altitude_m = max(0.0, float(altitude_m))

# Troposphere (0–11 km): linear temperature lapse rate
if altitude_m <= 11_000:
T = T0 - L * altitude_m
P = P0 * (T / T0) ** (g * M / (R * L))
else:
# Lower stratosphere isothermal layer (11–20 km)
T11 = T0 - L * 11_000
P11 = P0 * (T11 / T0) ** (g * M / (R * L))
P = P11 * math.exp(-g * M * (altitude_m - 11_000) / (R * T11))
T = T11 # remains constant in this layer

rho = P * M / (R * T)
pO2 = FIO2_STANDARD * P

return {
"temperature_C": T - 273.15,
"pressure_Pa": P,
"density_kg_m3": rho,
"pO2_Pa": pO2,
}


def alveolar_PO2(
altitude_m: float | int,
FiO2: float = 0.21,
PaCO2: float = 40.0,
RQ: float = 0.8,
) -> float:
"""Compute alveolar \(\text{PAO₂}\) via the alveolar gas equation.

All pressures in mmHg here for pedagogical familiarity.
"""
# Barometric pressure from ISA
Pb = standard_atmosphere(altitude_m)["pressure_Pa"] / 133.322 # Pa → mmHg
PH2O = 47.0 # Water-vapour pressure at 37 °C (mmHg)
PAO2 = FiO2 * (Pb - PH2O) - PaCO2 / RQ
return PAO2
30 changes: 30 additions & 0 deletions calculators/g_force.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
def g_loc_time(Gz: float | int) -> float:
"""Return estimated maximum exposure time in seconds before G-LOC.

Based on a simplified interpretation of the Stoll curve (USAF, 1956).
Works for +G\_z accelerations in the seated position, no anti-G suit.
"""
Gz = float(Gz)

if Gz < 5.0:
return float("inf") # Below ~5 g most subjects tolerate indefinitely

_TABLE = {
6.0: 60.0,
7.0: 30.0,
8.0: 15.0,
9.0: 5.0,
}

if Gz >= 9.0:
return _TABLE[9.0]

# Linear interpolation between available points
lower_key = max(k for k in _TABLE if k <= Gz)
upper_key = min(k for k in _TABLE if k >= Gz)
if lower_key == upper_key:
return _TABLE[lower_key]

t_low = _TABLE[lower_key]
t_high = _TABLE[upper_key]
return t_low + (t_high - t_low) * (Gz - lower_key) / (upper_key - lower_key)
15 changes: 15 additions & 0 deletions calculators/radiation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
def dose_rate(altitude_ft: float | int, polar: bool = False) -> float:
"""Return approximate cosmic-radiation dose rate at cruise altitude.

The result is in micro-Sieverts per hour (µSv/h). A very coarse linear
model is used for educational demonstration only:

dose = 0.1 + 0.14 × (altitude\_ft / 10 000)

with a 20 % increase applied on polar routes (>60° latitude).
"""
altitude_ft = float(altitude_ft)
dose = 0.1 + 0.14 * (altitude_ft / 10_000)
if polar:
dose *= 1.2
return dose
29 changes: 29 additions & 0 deletions calculators/tuc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import numpy as np # type: ignore

# Altitude (ft) → median Time of Useful Consciousness (s)
# Data compiled from USAF Flight Surgeon Handbook & other sources.
_TIME_TABLE = [
(18_000, 1_500), # 25 min
(22_000, 600), # 10 min
(25_000, 240), # 4 min
(28_000, 150), # 2.5 min
(30_000, 90), # 1.5 min
(35_000, 45),
(40_000, 18),
(43_000, 10),
(50_000, 10),
(63_000, 0), # Armstrong line – instantaneous
]


def estimate_tuc(altitude_ft: float | int) -> float:
"""Estimate Time of Useful Consciousness (seconds) for given altitude."""
altitude_ft = float(altitude_ft)

alts, times = zip(*_TIME_TABLE)
if altitude_ft <= alts[0]:
return times[0]
if altitude_ft >= alts[-1]:
return times[-1]

return float(np.interp(altitude_ft, alts, times))
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
streamlit>=1.32
numpy>=1.26
108 changes: 108 additions & 0 deletions streamlit_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import streamlit as st # type: ignore

from calculators import (
standard_atmosphere,
alveolar_PO2,
estimate_tuc,
g_loc_time,
dose_rate,
)

st.set_page_config(
page_title="Aerospace Physiology Calculators",
page_icon="✈️",
layout="centered",
)

st.title("✈️ Aerospace Physiology Calculators")

st.sidebar.header("Choose a calculator")
calculator = st.sidebar.selectbox(
"Calculator", (
"Standard Atmosphere",
"Alveolar Oxygen Pressure",
"Time of Useful Consciousness",
"G-Force Tolerance",
"Cosmic Radiation Dose",
),
)

# ----------------------------------------------------------------------------
# 1. STANDARD ATMOSPHERE
# ----------------------------------------------------------------------------
if calculator == "Standard Atmosphere":
st.subheader("International Standard Atmosphere (ISA)")
alt_ft = st.slider("Altitude (ft)", 0, 60_000, 0, step=1_000)
alt_m = alt_ft * 0.3048
result = standard_atmosphere(alt_m)

st.write(f"**Temperature:** {result['temperature_C']:.2f} °C")
st.write(f"**Pressure:** {result['pressure_Pa'] / 100:.2f} hPa")
st.write(f"**Density:** {result['density_kg_m3']:.3f} kg/m³")
st.write(f"**O₂ Partial Pressure:** {result['pO2_Pa'] / 100:.2f} hPa")

st.caption("Calculations up to 11 km assume a linear temperature lapse rate; above this an isothermal layer is assumed.")

# ----------------------------------------------------------------------------
# 2. ALVEOLAR GAS EQUATION
# ----------------------------------------------------------------------------
elif calculator == "Alveolar Oxygen Pressure":
st.subheader("Alveolar Oxygen Pressure (PAO₂)")
alt_ft = st.slider("Altitude (ft)", 0, 40_000, 0, step=1_000)
alt_m = alt_ft * 0.3048

FiO2 = st.number_input("Inspired O₂ fraction (FiO₂)", 0.0, 1.0, 0.21, step=0.01)
PaCO2 = st.number_input("Arterial CO₂ (PaCO₂) [mmHg]", 20.0, 60.0, 40.0, step=1.0)
RQ = st.number_input("Respiratory Quotient (R)", 0.5, 1.2, 0.8, step=0.05)

p_ao2 = alveolar_PO2(alt_m, FiO2, PaCO2, RQ)
st.write(f"**PAO₂:** {p_ao2:.1f} mmHg")

st.caption("Alveolar gas equation: PAO₂ = FiO₂·(Pb − PH₂O) − PaCO₂/R.")

# ----------------------------------------------------------------------------
# 3. TUC
# ----------------------------------------------------------------------------
elif calculator == "Time of Useful Consciousness":
st.subheader("Time of Useful Consciousness (TUC)")
alt_ft = st.slider("Altitude (ft)", 10_000, 50_000, 25_000, step=1_000)
tuc_sec = estimate_tuc(alt_ft)

if tuc_sec == 0:
st.error("Instantaneous loss of consciousness expected at this altitude without pressure suit.")
else:
minutes = tuc_sec / 60
st.write(f"**Estimated TUC:** {tuc_sec:.0f} s (~{minutes:.1f} min)")

st.caption("Interpolated from published USAF reference values; individual tolerance varies.")

# ----------------------------------------------------------------------------
# 4. G-FORCE
# ----------------------------------------------------------------------------
elif calculator == "G-Force Tolerance":
st.subheader("G-Force Tolerance")
Gz = st.slider("Sustained +Gz (g)", 1.0, 9.0, 6.0, step=0.1)
tol = g_loc_time(Gz)

if tol == float("inf"):
st.success("Below ~5 g most healthy subjects tolerate indefinitely (without anti-G suit).")
else:
st.write(f"**Estimated tolerance time:** {tol:.0f} s")

st.caption("Simplified Stoll curve approximation; assumes seated posture and no countermeasures.")

# ----------------------------------------------------------------------------
# 5. RADIATION DOSE
# ----------------------------------------------------------------------------
else: # Cosmic Radiation Dose
st.subheader("Cosmic Radiation Dose at Cruise")
alt_ft = st.slider("Flight altitude (ft)", 0, 45_000, 35_000, step=1_000)
polar = st.checkbox("Polar route (>60° latitude)")
dose = dose_rate(alt_ft, polar)

st.write(f"**Dose rate:** {dose:.2f} µSv/h")

st.caption("Highly simplified linear model for educational purposes only; real-world exposure depends on solar activity, latitude, and flight duration.")

st.sidebar.markdown("---")
st.sidebar.write("Developed for educational demonstration of aerospace physiology.")
Loading