diff --git a/calculators/__init__.py b/calculators/__init__.py new file mode 100644 index 0000000..f3a308d --- /dev/null +++ b/calculators/__init__.py @@ -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", +] \ No newline at end of file diff --git a/calculators/atmosphere.py b/calculators/atmosphere.py new file mode 100644 index 0000000..663e0d5 --- /dev/null +++ b/calculators/atmosphere.py @@ -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 \ No newline at end of file diff --git a/calculators/g_force.py b/calculators/g_force.py new file mode 100644 index 0000000..38da488 --- /dev/null +++ b/calculators/g_force.py @@ -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) \ No newline at end of file diff --git a/calculators/radiation.py b/calculators/radiation.py new file mode 100644 index 0000000..afeccd1 --- /dev/null +++ b/calculators/radiation.py @@ -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 \ No newline at end of file diff --git a/calculators/tuc.py b/calculators/tuc.py new file mode 100644 index 0000000..4a7ce11 --- /dev/null +++ b/calculators/tuc.py @@ -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)) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b5f2046 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +streamlit>=1.32 +numpy>=1.26 \ No newline at end of file diff --git a/streamlit_app.py b/streamlit_app.py new file mode 100644 index 0000000..30d3d7c --- /dev/null +++ b/streamlit_app.py @@ -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.") \ No newline at end of file