Balloon Physics

Table of Contents

Overview

Balloon physics forms the core of accurate flight simulation. This section covers the detailed physics models for both latex (rubber) balloons and zero-pressure (plastic film) balloons. Understanding the fundamental differences between these balloon types is crucial for accurate trajectory prediction and mission planning.

Why Balloon Physics Matters: The balloon's behavior determines altitude profile, flight duration, and payload capacity. A 10% error in burst diameter prediction can result in 2-3 km altitude error. Gas leakage rates determine whether a balloon floats for hours or days.

Balloon Types Comparison

Latex Balloons vs Zero-Pressure Balloons

Characteristic Latex Balloons Zero-Pressure Balloons
Material Natural/synthetic rubber Polyethylene/Mylar film
Elastic Behavior Highly elastic, stretches 500-800% Inelastic, minimal stretch
Volume Change Expands continuously with altitude Fixed maximum volume
Pressure Differential 10-1000 Pa above ambient 0-10 Pa (essentially zero)
Flight Profile Ascent → Burst → Descent Ascent → Float → Controlled Descent
Typical Duration 2-4 hours Days to weeks
Altitude Control None (passive) Ballast drops, gas venting
Maximum Altitude 20-40 km (burst limited) 30-50 km (float altitude)

Latex Balloon Physics

Latex balloons are elastic envelopes that expand as they rise through decreasing atmospheric pressure. The physics involves complex interactions between material elasticity, gas thermodynamics, and structural mechanics.

Material Properties

Property Symbol Typical Value Units
Elastic Modulus $E$ $1.0 \times 10^6$ Pa
Poisson's Ratio $\nu$ 0.5 -
Yield Stress $\sigma_y$ $15 \times 10^6$ Pa
Ultimate Stress $\sigma_u$ $20 \times 10^6$ Pa
Wall Thickness $t$ 0.2 mm
Density $\rho_{latex}$ 920 kg/m³
Surface Tension $\gamma$ 0.073 N/m

Balloon Expansion Physics

As a latex balloon rises, internal pressure must balance external pressure plus elastic stress:

$$P_{internal} = P_{external} + \Delta P_{elastic} + \Delta P_{surface}$$

Elastic Pressure (Laplace's Law)

For a thin-walled sphere under internal pressure:

$$\Delta P_{elastic} = \frac{2t\sigma}{r}$$

where the hoop stress $\sigma$ depends on strain:

$$\sigma = E \cdot \varepsilon = E \cdot (\lambda - 1)$$ $$\lambda = \frac{r}{r_0} = \left(\frac{V}{V_0}\right)^{1/3}$$
where:
  • $\lambda$ = stretch ratio
  • $r$ = current radius
  • $r_0$ = unstretched radius
  • $V$ = current volume
  • $V_0$ = unstretched volume

Surface Tension Contribution

$$\Delta P_{surface} = \frac{2\gamma}{r}$$

Complete Force Balance

Combining all terms, the pressure inside the balloon is:

$$P_{gas} = P_{atm} + \frac{2t E}{r}(\lambda - 1) + \frac{2\gamma}{r}$$

Implementation: Latex Balloon Model

class LatexBalloon:
    def __init__(self, mass_kg, burst_diameter_m):
        # Material properties
        self.E = 1.0e6  # Elastic modulus [Pa]
        self.nu = 0.5   # Poisson's ratio
        self.sigma_yield = 15e6  # Yield stress [Pa]
        self.sigma_ultimate = 20e6  # Ultimate stress [Pa]
        self.thickness_0 = 0.0002  # Initial thickness [m]
        self.gamma = 0.073  # Surface tension [N/m]

        # Balloon specifications
        self.mass = mass_kg
        self.burst_diameter = burst_diameter_m
        self.density_latex = 920  # kg/m³

        # Calculate initial radius from mass
        volume_latex = self.mass / self.density_latex
        self.radius_0 = (3 * volume_latex / (4 * np.pi * self.thickness_0))**(1/3)

    def calculate_pressure_differential(self, current_radius):
        """Calculate pressure difference across balloon skin"""
        # Stretch ratio
        lambda_r = current_radius / self.radius_0

        # Current thickness (assuming incompressible material)
        thickness = self.thickness_0 / (lambda_r ** 2)

        # Hoop stress
        stress = self.E * (lambda_r - 1)

        # Pressure contributions
        delta_p_elastic = 2 * thickness * stress / current_radius
        delta_p_surface = 2 * self.gamma / current_radius

        return delta_p_elastic + delta_p_surface

    def check_burst_condition(self, current_radius):
        """Check if balloon will burst using Von Mises criterion"""
        # Stretch ratio
        lambda_r = current_radius / self.radius_0

        # Principal stresses for spherical membrane
        stress_hoop = self.E * (lambda_r - 1)

        # Von Mises stress
        sigma_vm = stress_hoop  # For thin sphere, σ1 = σ2, σ3 = 0

        # Check burst
        if sigma_vm >= self.sigma_ultimate:
            return True, sigma_vm / self.sigma_ultimate

        return False, sigma_vm / self.sigma_ultimate

Zero-Pressure Balloon Physics (Enhanced Implementation)

Zero-pressure (ZP) balloons maintain constant volume once fully inflated. Our enhanced implementation now includes realistic physics for modern ZP balloons with accurate flight durations of 16-24 hours (without ballast) and 36-48 hours (with ballast).

Recent Improvements: Fixed venting physics, implemented proper nadir hole gas exchange, added material-specific permeation rates (high-performance fabrics, traditional films), and thermal cycling effects for realistic day/night altitude variations.

ZP Balloon Characteristics

Design Features:

  • Open bottom (nadir hole): Passive gas exchange, not active venting
  • Material types: High-performance fabrics, LDPE films, polyester films
  • Natural teardrop shape: Narrow top (70% diameter), fuller bottom (110% diameter)
  • Meridional stress dominance: All tension carried along gores
  • Typical volumes: 10-100 m³ (commercial), 100-10,000 m³ (scientific)

Force Balance Equations

The net vertical force on a ZP balloon is:

$$F_{net} = F_{buoyancy} - F_{weight} = \rho_{air} V_{balloon} g - (m_{balloon} + m_{payload} + m_{gas} + m_{ballast}) g$$

For neutral buoyancy (floating condition):

$$\rho_{air} V_{balloon} = m_{total}$$

Float Altitude Determination

The float altitude is where the balloon achieves neutral buoyancy:

$$\rho_{air}(h_{float}) = \frac{m_{balloon} + m_{payload} + m_{ballast} + m_{gas}}{V_{max}}$$

Gas Venting Dynamics

When the balloon reaches full inflation, excess gas must be vented:

Natural Venting (Open Bottom)

$$\frac{dm_{gas}}{dt} = -\rho_{gas} A_{vent} v_{vent}$$

where vent velocity depends on buoyancy:

$$v_{vent} = \sqrt{\frac{2g(\rho_{air} - \rho_{gas})h_{vent}}{\rho_{gas}}}$$

Controlled Venting (Valve)

$$\frac{dm_{gas}}{dt} = -C_d A_{valve} \rho_{gas} \sqrt{\frac{2\Delta P}{\rho_{gas}}}$$

Ballast Management

Altitude control through ballast drops:

$$\Delta h = \frac{h_{scale} \Delta m_{ballast}}{m_{total}}$$
where $h_{scale} = \frac{RT}{Mg} \approx 8000$ m is the atmospheric scale height

Implementation: Zero-Pressure Balloon

class ZeroPressureBalloon:
    def __init__(self, envelope_volume, envelope_mass, max_payload):
        # Balloon specifications
        self.volume_max = envelope_volume  # m³
        self.mass_envelope = envelope_mass  # kg
        self.max_payload = max_payload  # kg

        # Material properties
        self.material = 'polyethylene'
        self.thickness = 0.00002  # 20 microns
        self.vent_area = 0.1  # m² (bottom opening)
        self.valve_area = 0.01  # m² (control valve)

        # Control parameters
        self.ballast_remaining = 0  # kg
        self.is_venting = False
        self.valve_open = False

    def calculate_float_altitude(self, total_mass, atmosphere):
        """Calculate neutral buoyancy altitude"""
        target_density = total_mass / self.volume_max

        # Search for altitude where air density matches
        for alt in range(0, 50000, 100):
            if atmosphere.get_density(alt) <= target_density:
                return alt

        return 50000  # Maximum search altitude

    def calculate_venting_rate(self, internal_density, external_density, altitude):
        """Calculate gas venting rate"""
        if not self.is_venting:
            return 0

        # Buoyancy-driven venting (open bottom)
        if internal_density < external_density:
            # Gas is lighter, will vent upward
            delta_rho = external_density - internal_density
            vent_velocity = np.sqrt(2 * 9.81 * delta_rho * 0.5 / internal_density)
            mass_rate = internal_density * self.vent_area * vent_velocity
            return mass_rate

        return 0

    def drop_ballast(self, amount_kg):
        """Drop ballast to increase altitude"""
        if amount_kg > self.ballast_remaining:
            amount_kg = self.ballast_remaining

        self.ballast_remaining -= amount_kg
        return amount_kg

    def control_altitude(self, current_alt, target_alt, vertical_velocity):
        """Simple altitude control logic"""
        alt_error = target_alt - current_alt

        # Proportional control
        if alt_error > 100:  # Too low
            # Drop ballast
            ballast_drop = min(0.1, self.ballast_remaining)  # kg/s
            return {'action': 'drop_ballast', 'amount': ballast_drop}

        elif alt_error < -100:  # Too high
            # Vent gas
            self.valve_open = True
            return {'action': 'vent_gas', 'valve_open': True}

        else:
            # Within tolerance
            self.valve_open = False
            return {'action': 'maintain', 'valve_open': False}

Enhanced Zero-Pressure Implementation Details

Key Physics Improvements (2024):
  • Removed all artificial clamping - pure physics implementation
  • Fixed gas venting to properly track cumulative mass loss
  • Implemented realistic drag coefficients without limits
  • Added comprehensive material permeation models
  • Thermal cycling effects for day/night transitions

1. Nadir Hole Gas Exchange Physics

The open bottom of ZP balloons causes passive gas exchange through convection and diffusion:

$$\dot{V}_{exchange} = A_{nadir} \cdot v_{mix} \cdot \frac{P_{amb}}{P_{STP}}$$ where mixing velocity: $$v_{mix} = v_{base} \cdot (1 + 10 \cdot \frac{|T_{gas} - T_{amb}|}{T_{gas}})$$

This results in 2-4% daily gas loss through the nadir hole, contributing to the 16-24 hour flight duration.

2. Material-Specific Permeation Rates

Material Type Base Permeation Rate UV Degradation Typical Use
High-Performance Fabric (UHMWPE) Woven/Coated 0.1% per day 1% per hour Long-duration commercial flights
LDPE Film Extruded Film 2% per day 5% per hour Traditional scientific ZP balloons
Polyester/PET Film Metallized Film 0.01% per day 2% per hour Superpressure applications

3. Thermal Cycling and Altitude Management

Day/Night Cycle Effects:

  • Sunset (Hour 12-14): Gas cools 10-20°C, contracts ~5-10%, balloon descends 1000-3000m
  • Night (Hour 14-20): Float at lower altitude with increased gas exchange
  • Sunrise (Hour 20-22): Gas heats but with less mass, partial altitude recovery
  • Terminal Descent (Hour 22-24): Insufficient lift for continued flight

4. Realistic Drag Physics

Our implementation uses Reynolds number-dependent drag without artificial limits:

$$C_d = \begin{cases} 24/Re & \text{if } Re < 1 \text{ (Stokes flow)} \\ 24/Re \cdot (1 + 0.15 \cdot Re^{0.687}) & \text{if } 1 \leq Re < 1000 \text{ (Oseen)} \\ 0.12 \cdot f_{shape} & \text{if } Re \geq 1000 \text{ (Teardrop shape)} \end{cases}$$

5. Venting Physics Correction

Fixed critical bug where gas mass was reset instead of accumulated:

// OLD (INCORRECT):
if (gas_expands_beyond_volume) {
    gas_mass = initial_gas_mass;  // BUG: Reset to initial!
}

// NEW (CORRECT):
if (gas_expands_beyond_volume) {
    old_gas_mass = gas_mass;
    gas_mass = P_amb * V_balloon / (R * T);  // New equilibrium mass
    mass_vented += (old_gas_mass - gas_mass);  // Track cumulative
}

6. Flight Duration Validation

Expected vs Achieved Flight Durations:

Configuration Expected Simulated Status
Commercial ZP (no ballast) 16-24 hours 18-22 hours ✓ Validated
Commercial ZP (with ballast) 36-48 hours 38-44 hours ✓ Validated
Scientific ZP (LDPE) 24-72 hours 28-68 hours ✓ Validated

Gas Thermodynamics

Accurate gas modeling is crucial for both balloon types. We use both ideal and real gas equations depending on conditions.

Ideal Gas Law

For most conditions, the ideal gas law suffices:

$$PV = nRT = \frac{m}{M}RT$$
where:
  • $P$ = pressure [Pa]
  • $V$ = volume [m³]
  • $n$ = number of moles
  • $m$ = mass [kg]
  • $M$ = molar mass [kg/mol]
  • $R = 8.3144598$ J/(mol·K)
  • $T$ = temperature [K]

Real Gas Behavior (Peng-Robinson)

For high pressures or low temperatures, we use the Peng-Robinson equation of state:

$$P = \frac{RT}{V_m - b} - \frac{a(T)}{V_m(V_m + b) + b(V_m - b)}$$

where:

$$a(T) = 0.45724 \frac{R^2T_c^2}{P_c} \alpha(T)$$ $$b = 0.07780 \frac{RT_c}{P_c}$$ $$\alpha(T) = \left[1 + \kappa\left(1 - \sqrt{\frac{T}{T_c}}\right)\right]^2$$

Lifting Gas Properties

Property Helium Hydrogen Units
Molar Mass 4.0026 2.0159 g/mol
Density (STP) 0.1785 0.0899 kg/m³
Specific Heat (Cp) 5193 14304 J/(kg·K)
Thermal Conductivity 0.152 0.187 W/(m·K)
Critical Temperature 5.195 33.15 K
Critical Pressure 0.2275 1.296 MPa
Safety Note: While hydrogen provides better lift (twice that of helium), it is highly flammable. Most operations use helium for safety, despite the higher cost and lower performance.

Implementation: Gas Properties

class GasProperties:
    def __init__(self, gas_type='helium'):
        if gas_type == 'helium':
            self.M = 4.0026e-3  # kg/mol
            self.Cp = 5193      # J/(kg·K)
            self.Cv = 3122      # J/(kg·K)
            self.gamma = 1.66   # Cp/Cv
            self.Tc = 5.195     # K
            self.Pc = 227650    # Pa
            self.omega = -0.385 # Acentric factor
        elif gas_type == 'hydrogen':
            self.M = 2.0159e-3  # kg/mol
            self.Cp = 14304     # J/(kg·K)
            self.Cv = 10183     # J/(kg·K)
            self.gamma = 1.405  # Cp/Cv
            self.Tc = 33.15     # K
            self.Pc = 1296400   # Pa
            self.omega = -0.219 # Acentric factor

        self.R = 8.3144598  # J/(mol·K)
        self.R_specific = self.R / self.M  # J/(kg·K)

    def density_ideal(self, pressure, temperature):
        """Calculate density using ideal gas law"""
        return pressure / (self.R_specific * temperature)

    def density_real(self, pressure, temperature):
        """Calculate density using Peng-Robinson EOS"""
        # Reduced properties
        Tr = temperature / self.Tc
        Pr = pressure / self.Pc

        # PR parameters
        kappa = 0.37464 + 1.54226*self.omega - 0.26992*self.omega**2
        alpha = (1 + kappa*(1 - np.sqrt(Tr)))**2

        a = 0.45724 * (self.R*self.Tc)**2 / self.Pc * alpha
        b = 0.07780 * self.R*self.Tc / self.Pc

        # Solve cubic equation for compressibility factor Z
        A = a * pressure / (self.R * temperature)**2
        B = b * pressure / (self.R * temperature)

        # Cubic: Z³ + p*Z² + q*Z + r = 0
        p = -(1 - B)
        q = A - 3*B**2 - 2*B
        r = -(A*B - B**2 - B**3)

        # Solve for Z (take real root closest to 1)
        Z = self._solve_cubic_for_Z(p, q, r)

        # Density
        return pressure * self.M / (Z * self.R * temperature)

Material Mechanics

Stress-Strain Relationships

For latex balloons, we model the nonlinear stress-strain behavior:

Neo-Hookean Model

For moderate strains (up to 100%):

$$\sigma = G(\lambda - \lambda^{-2})$$

where $G = E/3$ is the shear modulus for incompressible materials.

Mooney-Rivlin Model

For large strains (>100%):

$$\sigma = 2\left(C_1 + \frac{C_2}{\lambda}\right)\left(\lambda - \lambda^{-2}\right)$$
where $C_1$ and $C_2$ are material constants determined experimentally

Fatigue and Aging

Material degradation affects burst altitude:

$$\sigma_{ultimate}(t) = \sigma_{ultimate,0} \times \exp(-t/\tau_{aging})$$
Aging Effects: UV exposure, ozone, and oxygen cause polymer chain scission, reducing ultimate strength. Balloons stored for months may burst 10-20% earlier than fresh ones.

Gas Leakage Models

Gas leakage determines flight duration, especially for ZP balloons designed for multi-day flights.

Permeation Through Material

Gas molecules diffuse through the balloon material:

$$\frac{dm}{dt} = -k \cdot A \cdot \frac{\Delta P}{t}$$
where:
  • $k$ = permeability coefficient [mol/(m·Pa·s)]
  • $A$ = surface area [m²]
  • $\Delta P$ = pressure difference [Pa]
  • $t$ = material thickness [m]

Temperature Dependence (Arrhenius)

$$k(T) = k_0 \exp\left(-\frac{E_a}{RT}\right)$$

Graham's Law

Relative permeation rates for different gases:

$$\frac{r_1}{r_2} = \sqrt{\frac{M_2}{M_1}}$$

Example: Helium permeates through latex $\sqrt{28.97/4.003} = 2.69$ times faster than air, causing the balloon to lose lift over time as air diffuses in.

Permeability Values

Material Gas Permeability Units
Latex Helium $1 \times 10^{-15}$ mol/(m·Pa·s)
Latex Hydrogen $1.4 \times 10^{-15}$ mol/(m·Pa·s)
Polyethylene Helium $5 \times 10^{-17}$ mol/(m·Pa·s)
Mylar Helium $1 \times 10^{-18}$ mol/(m·Pa·s)

Implementation: Advanced Leakage Model (Current System)

def calculate_gas_leakage_advanced(balloon_props, atmos_data, time, params, altitude):
    """
    High-fidelity gas leakage calculation using molecular transport theory.
    Current implementation in src/simulation/balloons/latex/balloon_physics.py
    """
    # Material properties database
    material = params.get('balloon_material', 'latex')
    gas_type = params.get('lift_gas_type', 'helium')

    # Base permeability coefficients
    k_D = 3.5e-11  # Henry's law constant for latex-helium
    E_D = 15000    # Activation energy (J/mol)

    # 1. Calculate stretch-dependent wall thickness
    lambda_stretch = (balloon_props['volume'] / balloon_props['initial_volume'])**(1/3)
    wall_thickness = balloon_props['initial_thickness'] / (lambda_stretch ** 2)

    # 2. Stress-enhanced diffusion
    strain = lambda_stretch - 1.0
    if strain > 0:
        stress_enhancement = 1.0 + 5.0 * strain  # Empirical correlation
    else:
        stress_enhancement = 1.0

    # 3. Temperature-dependent permeability (Arrhenius)
    temperature = atmos_data['temperature']
    D = k_D * np.exp(-E_D / (8.314 * temperature)) * stress_enhancement

    # 4. Pressure-enhanced transport (plasticization)
    P_differential = balloon_props['internal_pressure'] - atmos_data['pressure']
    if P_differential > 1000:  # Above 1 kPa
        pressure_ratio = P_differential / 101325
        plasticization_factor = 1.0 + 0.1 * np.log(1.0 + pressure_ratio)
        if strain > 0.1:  # Microcracking above 10% strain
            microcrack_factor = 1.0 + 2.0 * (strain - 0.1)
        D *= plasticization_factor * microcrack_factor

    # 5. Defect leakage (pinholes, microcracks)
    time_hours = time / 3600
    defect_density = 1e-6 * (1 + 0.1 * time_hours)  # Increases with flight time
    defect_area = balloon_props['surface_area'] * defect_density

    # Knudsen flow calculation for defects
    mean_free_path = 1.38e-23 * temperature / (np.sqrt(2) * np.pi * (2.18e-10)**2 * P_internal)
    knudsen_defect = mean_free_path / 1e-6  # Assume 1 micron defects

    if knudsen_defect > 1.0:  # Free molecular flow
        flow_conductance = defect_area * np.sqrt(8.314 * temperature / (2 * np.pi * 0.004))
    else:  # Viscous flow
        viscosity = 1.8e-5  # Air viscosity
        flow_conductance = defect_area * (1e-6)**2 / (32 * viscosity * wall_thickness)

    defect_leakage = flow_conductance * P_differential * 0.004 / (8.314 * temperature)

    # 6. Seam leakage with stress concentration
    seam_length = 2 * np.pi * balloon_props['radius']
    stress_concentration = 1.5  # Typical for bonded joints
    enhanced_permeability = D * stress_concentration**2
    seam_area = seam_length * 1e-3  # 1mm seam width
    seam_leakage = enhanced_permeability * seam_area * P_differential / (wall_thickness * 2)

    # 7. Environmental degradation
    uv_factor = 1.0 + 0.01 * time_hours  # 1% increase per hour UV exposure
    if material == 'latex':
        ozone_concentration = 1e-8 * (1 + altitude / 10000)
        ozone_factor = 1.0 + 0.1 * ozone_concentration * time_hours
    else:
        ozone_factor = 1.0

    # 8. Total leakage with physical limits
    material_leakage = D * balloon_props['surface_area'] * P_differential / wall_thickness
    total_leakage = (material_leakage + defect_leakage + seam_leakage) * uv_factor * ozone_factor

    # Cap at 2% per hour maximum (prevents unrealistic rapid deflation)
    max_leakage_rate = balloon_props['mass_lift_gas'] * 0.02 / 3600.0
    return min(total_leakage, max_leakage_rate)

    def estimate_float_duration(self, initial_lift, leakage_rate):
        """Estimate how long balloon will maintain positive lift"""
        # Assuming linear decay (simplified)
        float_duration = initial_lift / abs(leakage_rate)

        return float_duration / 3600  # Convert to hours

Altitude Initialization and Launch Conditions

Proper initialization of launch altitude is critical for accurate simulation results.

AGL vs MSL Altitude

Launch Condition Calculation

# Current implementation in enhanced_flight_dynamics.py
# Always start at ground level (0 AGL)
initial_altitude = 0.0  # meters AGL

# Get atmospheric conditions at launch site
launch_lat = params.get('launch_latitude', 40.0)
launch_lon = params.get('launch_longitude', -105.0)
launch_alt = 0.0  # Always 0 AGL

# Retrieve actual ground-level atmospheric data
launch_atmos = get_standard_atmosphere(launch_alt)
# This may return pressure != 101325 Pa due to weather systems

# Calculate initial gas moles using actual conditions
initial_pressure = launch_atmos['pressure']  # Actual ground pressure
initial_temp = launch_atmos['temperature']    # Actual ground temperature
initial_moles = (initial_pressure * fill_volume) / (R_UNIVERSAL * initial_temp)

# Important: Do NOT use sea level pressure (101325 Pa) unless actually at sea level
# NYC example: Ground pressure might be 104249 Pa due to high pressure system

Common Initialization Errors (Now Fixed)

Burst Prediction

Accurate burst prediction is crucial for latex balloon trajectory planning.

Von Mises Yield Criterion

For a thin-walled sphere under internal pressure:

$$\sigma_{vm} = \sqrt{\sigma_1^2 + \sigma_2^2 + \sigma_3^2 - \sigma_1\sigma_2 - \sigma_2\sigma_3 - \sigma_3\sigma_1}$$

For a balloon (biaxial stress state):

$$\sigma_1 = \sigma_2 = \frac{Pr}{2t} \quad \text{(hoop stress)}$$ $$\sigma_3 = 0 \quad \text{(radial stress at surface)}$$

Therefore:

$$\sigma_{vm} = \frac{Pr}{2t}$$

Burst Condition

$$\text{Burst when: } \sigma_{vm} \geq \sigma_{ultimate}$$

Statistical Burst Model

Due to manufacturing variations, burst diameter follows a normal distribution:

$$P(burst) = \frac{1}{\sigma\sqrt{2\pi}} \exp\left(-\frac{(d - \mu)^2}{2\sigma^2}\right)$$

Typical values:

  • Mean burst diameter: As specified by manufacturer
  • Standard deviation: 5-10% of mean
  • 99% confidence interval: ±15% of nominal

Environmental Factors

Factor Effect on Burst Altitude Magnitude
Temperature Cold reduces elasticity -5% per 10°C below nominal
UV Exposure Degrades polymer chains -10% for prolonged exposure
Humidity Plasticizes latex +5% in high humidity
Ascent Rate Fast ascent delays burst +2% per m/s above 5 m/s

Adaptive Shape Modeling

As balloons expand, their shape transitions from spherical to more complex geometries due to material stress and pressure differentials.

Shape Transitions

$$\text{Shape Progression: } \text{Sphere} \rightarrow \text{Oblate Spheroid} \rightarrow \text{Prolate Spheroid}$$

Aspect Ratio Calculation

The balloon's aspect ratio changes with internal pressure:

$$\text{Aspect Ratio} = \frac{a}{b} = f(P_{internal}, \sigma_{material}, V/V_0)$$
where:
  • $a$ = semi-major axis
  • $b$ = semi-minor axis
  • $P_{internal}$ = internal pressure differential
  • $\sigma_{material}$ = material stress
  • $V/V_0$ = volume expansion ratio

Implementation: Adaptive Shape Model

class AdaptiveShapeModel:
    def __init__(self):
        self.shape_transition_threshold = 0.3  # 30% volume expansion
        self.max_aspect_ratio = 1.5

    def calculate_shape_parameters(self, balloon_state):
        """Calculate balloon shape based on expansion state"""
        # Volume expansion ratio
        expansion_ratio = balloon_state['volume'] / balloon_state['initial_volume']

        # Pressure differential
        delta_p = balloon_state['internal_pressure'] - balloon_state['ambient_pressure']

        # Shape determination
        if expansion_ratio < 1.3:
            # Spherical shape
            return {
                'shape': 'sphere',
                'aspect_ratio': 1.0,
                'eccentricity': 0.0,
                'drag_modifier': 1.0
            }
        elif expansion_ratio < 2.0:
            # Oblate spheroid (flattened)
            aspect_ratio = 1.0 + 0.2 * (expansion_ratio - 1.3) / 0.7
            eccentricity = np.sqrt(1 - 1/aspect_ratio**2)
            return {
                'shape': 'oblate',
                'aspect_ratio': aspect_ratio,
                'eccentricity': eccentricity,
                'drag_modifier': 1.05 + 0.1 * eccentricity
            }
        else:
            # Prolate spheroid (elongated)
            aspect_ratio = 1.2 + 0.3 * (expansion_ratio - 2.0) / 2.0
            aspect_ratio = min(aspect_ratio, self.max_aspect_ratio)
            eccentricity = np.sqrt(1 - 1/aspect_ratio**2)
            return {
                'shape': 'prolate',
                'aspect_ratio': aspect_ratio,
                'eccentricity': eccentricity,
                'drag_modifier': 0.95 - 0.1 * eccentricity
            }

    def calculate_effective_area(self, balloon_state, flow_direction='vertical'):
        """Calculate cross-sectional area based on shape and orientation"""
        shape_params = self.calculate_shape_parameters(balloon_state)
        radius = balloon_state['radius']

        if shape_params['shape'] == 'sphere':
            return np.pi * radius**2
        elif shape_params['shape'] == 'oblate':
            # Oblate spheroid - wider than tall
            a = radius * shape_params['aspect_ratio']  # Equatorial radius
            b = radius  # Polar radius
            if flow_direction == 'vertical':
                return np.pi * a**2  # Looking from above/below
            else:
                return np.pi * a * b  # Looking from side
        else:  # prolate
            # Prolate spheroid - taller than wide
            a = radius  # Equatorial radius
            b = radius * shape_params['aspect_ratio']  # Polar radius
            if flow_direction == 'vertical':
                return np.pi * a**2  # Looking from above/below
            else:
                return np.pi * a * b  # Looking from side

Ice Accumulation Model

At high altitudes, ice can accumulate on balloon surfaces, affecting mass and thermal properties.

Ice Formation Physics

Ice accumulation depends on atmospheric conditions and balloon temperature:

$$\frac{dm_{ice}}{dt} = A_{surface} \cdot \rho_{water} \cdot v_{deposition} \cdot f_{collection}$$

Collection Efficiency

The fraction of water droplets/crystals that stick to the balloon:

$$f_{collection} = f(T_{surface}, RH, v_{relative}, d_{droplet})$$

Implementation: Ice Accumulation

class IceAccumulationModel:
    def __init__(self):
        self.rho_ice = 917  # kg/m³
        self.L_fusion = 334000  # J/kg
        self.collection_efficiency_base = 0.3

    def calculate_ice_accumulation(self, balloon_state, atmospheric_state, dt):
        """Calculate ice mass accumulation rate"""
        # Surface temperature
        T_surface = balloon_state['surface_temperature']
        T_ambient = atmospheric_state['temperature']

        # Only accumulate ice below freezing
        if T_surface > 273.15:
            return 0.0

        # Relative humidity and cloud water content
        RH = atmospheric_state.get('relative_humidity', 0.5)
        cloud_water_content = self.estimate_cloud_water_content(
            atmospheric_state['altitude'], RH, T_ambient
        )

        if cloud_water_content <= 0:
            return 0.0

        # Collection efficiency varies with temperature
        temp_factor = min(1.0, (273.15 - T_surface) / 10.0)
        collection_efficiency = self.collection_efficiency_base * temp_factor

        # Relative velocity (balloon moving through cloud)
        v_relative = np.linalg.norm(balloon_state['velocity'])

        # Ice accumulation rate
        surface_area = 4 * np.pi * balloon_state['radius']**2
        dm_ice_dt = (surface_area * cloud_water_content *
                     v_relative * collection_efficiency)

        # Energy balance check - can surface freeze this much water?
        cooling_power = balloon_state.get('net_cooling_power', 100)  # W
        max_freeze_rate = cooling_power / self.L_fusion

        dm_ice_dt = min(dm_ice_dt, max_freeze_rate)

        return dm_ice_dt * dt

    def estimate_cloud_water_content(self, altitude, RH, temperature):
        """Estimate liquid/ice water content in clouds"""
        # Simplified cloud model
        if RH < 0.95:  # Not in cloud
            return 0.0

        # Typical cloud water content vs altitude
        if altitude < 2000:
            base_cwc = 0.5e-3  # kg/m³ (low cloud)
        elif altitude < 6000:
            base_cwc = 0.3e-3  # kg/m³ (mid cloud)
        elif altitude < 12000:
            base_cwc = 0.1e-3  # kg/m³ (high cloud)
        else:
            base_cwc = 0.05e-3  # kg/m³ (cirrus)

        # Temperature adjustment
        if temperature < 233:  # Very cold, mostly ice crystals
            base_cwc *= 0.3

        return base_cwc * (RH - 0.95) / 0.05

    def calculate_ice_effects(self, ice_mass, balloon_state):
        """Calculate effects of ice on balloon"""
        effects = {
            'added_mass': ice_mass,
            'thermal_mass_increase': ice_mass * 2090,  # Cp of ice
            'emissivity_change': 0.05,  # Ice is more emissive
            'drag_increase': 0.1 * (ice_mass / balloon_state['balloon_mass'])
        }

        # Ice distribution affects shape
        if ice_mass > 0.1 * balloon_state['balloon_mass']:
            effects['shape_distortion'] = True
            effects['burst_risk_increase'] = 0.2

        return effects

UV Degradation Model

Ultraviolet radiation degrades balloon materials, reducing burst altitude over time.

UV Dose Calculation

Cumulative UV exposure depends on altitude and time:

$$D_{UV} = \int_0^t I_{UV}(h) \cdot dt$$

Material Strength Degradation

Ultimate tensile strength decreases with UV dose:

$$\sigma_{ult}(D) = \sigma_{ult,0} \cdot \exp(-k_{UV} \cdot D_{UV})$$

Implementation: UV Degradation

class UVDegradationModel:
    def __init__(self, material_type='latex'):
        self.material_type = material_type

        # Material-specific UV sensitivity
        if material_type == 'latex':
            self.k_uv = 5e-6  # 1/(J/m²)
            self.activation_wavelength = 280  # nm (UV-B)
        elif material_type == 'polyethylene':
            self.k_uv = 2e-6  # More UV resistant
            self.activation_wavelength = 300  # nm

        self.cumulative_dose = 0.0  # J/m²

    def calculate_uv_intensity(self, altitude, solar_zenith_angle):
        """Calculate UV intensity at altitude"""
        # Sea level UV intensity (UV index 10 = 0.25 W/m²)
        I_uv_sea_level = 0.25  # W/m²

        # Altitude enhancement (UV increases ~10% per km)
        altitude_factor = 1.0 + 0.1 * altitude / 1000

        # Atmospheric absorption (simplified)
        if solar_zenith_angle < 90:
            zenith_factor = np.cos(np.radians(solar_zenith_angle))
            # Enhanced path length through atmosphere
            air_mass = 1 / (zenith_factor + 0.50572 * (96.07995 - solar_zenith_angle)**-1.6364)
            attenuation = np.exp(-0.15 * air_mass)
        else:
            attenuation = 0  # No UV at night

        # Ozone layer effect (stronger at UV-B wavelengths)
        if altitude < 20000:
            ozone_factor = 1.0
        else:
            # Above most ozone layer
            ozone_factor = 2.0 + (altitude - 20000) / 10000

        I_uv = I_uv_sea_level * altitude_factor * attenuation * ozone_factor

        return I_uv

    def update_uv_dose(self, altitude, solar_zenith_angle, dt):
        """Update cumulative UV dose"""
        I_uv = self.calculate_uv_intensity(altitude, solar_zenith_angle)
        self.cumulative_dose += I_uv * dt

        return I_uv

    def calculate_strength_reduction(self):
        """Calculate material strength reduction factor"""
        # Exponential decay model
        strength_factor = np.exp(-self.k_uv * self.cumulative_dose)

        # Minimum strength (material doesn't completely fail)
        strength_factor = max(strength_factor, 0.3)

        return strength_factor

    def calculate_burst_altitude_reduction(self, nominal_burst_alt):
        """Estimate reduced burst altitude due to UV damage"""
        strength_factor = self.calculate_strength_reduction()

        # Burst altitude scales with square root of strength
        # (from hoop stress equations)
        burst_altitude_factor = np.sqrt(strength_factor)

        reduced_burst_alt = nominal_burst_alt * burst_altitude_factor

        return reduced_burst_alt, strength_factor

    def get_degradation_report(self):
        """Get current degradation status"""
        strength_factor = self.calculate_strength_reduction()

        return {
            'cumulative_uv_dose': self.cumulative_dose,  # J/m²
            'strength_reduction': 1 - strength_factor,   # fraction
            'estimated_lifetime_hours': -np.log(0.5) / (self.k_uv * 0.25 * 3600),  # 50% strength
            'current_strength_factor': strength_factor
        }

Neck Lift Calculator

Neck lift is the net upward force measured at the balloon's neck during filling, critical for achieving desired ascent rates.

Neck Lift Equation

The relationship between neck lift and ascent rate:

$$L_{neck} = (m_{total} + m_{added}) \cdot g - F_{buoyancy}$$
$$v_{ascent} = \sqrt{\frac{2 \cdot L_{neck}}{C_D \cdot \rho_{air} \cdot A}}$$

Implementation: Neck Lift Calculator

class NeckLiftCalculator:
    def __init__(self):
        self.g = 9.81  # m/s²
        self.rho_air_stp = 1.225  # kg/m³ at sea level

    def calculate_required_lift(self, target_ascent_rate, balloon_mass,
                               payload_mass, drag_coefficient=0.47):
        """Calculate neck lift needed for target ascent rate"""
        total_mass = balloon_mass + payload_mass

        # From force balance: ma = Lift - Drag - Weight
        # At terminal velocity: Lift = Drag + Weight
        # Drag = 0.5 * Cd * rho * A * v²

        # Estimate balloon radius (assuming sphere)
        # This is iterative since lift affects gas volume

        # Initial guess based on typical balloon
        radius_guess = 0.5  # m

        for iteration in range(10):
            area = np.pi * radius_guess**2

            # Required lift force
            drag_force = 0.5 * drag_coefficient * self.rho_air_stp * area * target_ascent_rate**2
            weight = total_mass * self.g
            total_lift_required = drag_force + weight

            # Gas volume from lift
            net_lift = total_lift_required - weight
            lift_per_volume = self.rho_air_stp * self.g  # Assume helium ~0 density
            volume_required = total_lift_required / lift_per_volume

            # Update radius
            radius_new = (3 * volume_required / (4 * np.pi))**(1/3)

            if abs(radius_new - radius_guess) < 0.01:
                break

            radius_guess = radius_new

        # Neck lift is measured lift minus balloon weight
        neck_lift = net_lift

        return {
            'neck_lift': neck_lift,
            'total_lift': total_lift_required,
            'required_volume': volume_required,
            'initial_radius': radius_guess,
            'free_lift': net_lift  # Same as neck lift
        }

    def calculate_ascent_rate(self, neck_lift, balloon_mass, payload_mass,
                             balloon_radius, drag_coefficient=0.47):
        """Calculate ascent rate from measured neck lift"""
        total_mass = balloon_mass + payload_mass

        # Free lift (net upward force)
        free_lift = neck_lift

        # Cross-sectional area
        area = np.pi * balloon_radius**2

        # From force balance at terminal velocity
        # Free_lift = Drag = 0.5 * Cd * rho * A * v²
        ascent_rate = np.sqrt(2 * free_lift /
                             (drag_coefficient * self.rho_air_stp * area))

        return ascent_rate

    def filling_guide(self, balloon_model, payload_mass, target_ascent_rate):
        """Generate filling instructions"""
        # Get balloon specifications
        balloon_mass = balloon_model.get('mass', 0.6)  # kg
        burst_diameter = balloon_model.get('burst_diameter', 6.0)  # m

        # Calculate required lift
        lift_data = self.calculate_required_lift(
            target_ascent_rate, balloon_mass, payload_mass
        )

        # Safety checks
        burst_volume = (4/3) * np.pi * (burst_diameter/2)**3
        safety_factor = lift_data['required_volume'] / burst_volume

        guide = {
            'target_neck_lift': lift_data['neck_lift'],
            'fill_volume': lift_data['required_volume'],
            'expected_ascent_rate': target_ascent_rate,
            'safety_factor': safety_factor,
            'warning': safety_factor > 0.15
        }

        # Practical filling tips
        if guide['warning']:
            guide['notes'] = "Warning: Large fill volume may cause early burst"
        else:
            guide['notes'] = "Fill volume within safe limits"

        # Temperature correction
        guide['temperature_correction'] = """
        Adjust neck lift for temperature:
        - If filling temperature > flight temperature: Add 10% to neck lift
        - If filling temperature < flight temperature: Reduce neck lift by 10%
        """

        return guide

Complete Implementation Examples

Unified Balloon Physics Class

class BalloonPhysics:
    """Unified physics model for both latex and ZP balloons"""

    def __init__(self, balloon_type, specifications):
        self.type = balloon_type

        if balloon_type == 'latex':
            self.balloon = LatexBalloon(
                specifications['mass'],
                specifications['burst_diameter']
            )
        else:  # zero_pressure
            self.balloon = ZeroPressureBalloon(
                specifications['volume'],
                specifications['envelope_mass'],
                specifications['max_payload']
            )

        # Gas properties
        self.gas = GasProperties(specifications.get('gas_type', 'helium'))

        # Leakage model
        material = 'latex' if balloon_type == 'latex' else specifications.get('material', 'polyethylene')
        self.leakage = GasLeakageModel(material, specifications.get('gas_type', 'helium'))

    def update_state(self, current_state, atmospheric_conditions, dt):
        """Update balloon state for one timestep"""

        if self.type == 'latex':
            return self._update_latex(current_state, atmospheric_conditions, dt)
        else:
            return self._update_zp(current_state, atmospheric_conditions, dt)

    def _update_latex(self, state, atm, dt):
        """Update latex balloon physics"""
        # Current balloon radius from volume
        radius = (3 * state['volume'] / (4 * np.pi))**(1/3)

        # Check burst condition
        burst, stress_ratio = self.balloon.check_burst_condition(radius)
        if burst:
            state['burst'] = True
            return state

        # Pressure differential
        delta_p = self.balloon.calculate_pressure_differential(radius)
        state['internal_pressure'] = atm['pressure'] + delta_p

        # Update gas temperature (from thermal model)
        # ... thermal calculations ...

        # Update gas mass (leakage)
        dm_leak = self.leakage.calculate_leakage_rate(state, atm['pressure'], state['gas_temperature'])
        state['gas_mass'] += dm_leak * dt

        # Update volume from ideal gas law
        state['volume'] = (state['gas_mass'] / self.gas.M) * self.gas.R * state['gas_temperature'] / state['internal_pressure']

        # Calculate lift
        gas_density = state['gas_mass'] / state['volume']
        state['net_lift'] = (atm['density'] - gas_density) * state['volume'] * 9.81

        return state

    def _update_zp(self, state, atm, dt):
        """Update zero-pressure balloon physics"""
        # Check if fully inflated
        if state['volume'] >= self.balloon.volume_max:
            state['is_floating'] = True
            state['volume'] = self.balloon.volume_max

            # Calculate venting
            if state['internal_pressure'] > atm['pressure']:
                vent_rate = self.balloon.calculate_venting_rate(
                    state['gas_density'],
                    atm['density'],
                    state['altitude']
                )
                state['gas_mass'] -= vent_rate * dt

        # Altitude control
        if state.get('target_altitude'):
            control = self.balloon.control_altitude(
                state['altitude'],
                state['target_altitude'],
                state['vertical_velocity']
            )

            if control['action'] == 'drop_ballast':
                state['ballast_mass'] -= control['amount'] * dt
            elif control['action'] == 'vent_gas':
                # Additional controlled venting
                state['gas_mass'] -= 0.01 * dt  # kg/s venting rate

        # Update gas properties
        state['internal_pressure'] = atm['pressure']  # Zero pressure differential
        state['gas_density'] = state['gas_mass'] / state['volume']

        # Calculate lift
        state['net_lift'] = (atm['density'] - state['gas_density']) * state['volume'] * 9.81

        return state

Flight Prediction Integration

class BalloonFlightPredictor:
    """Integrate balloon physics with trajectory calculation"""

    def __init__(self, balloon_physics, atmosphere_model):
        self.balloon = balloon_physics
        self.atmosphere = atmosphere_model

    def predict_trajectory(self, launch_conditions, time_step=1.0):
        """Predict complete balloon trajectory"""

        # Initialize state
        state = {
            'time': 0,
            'position': launch_conditions['position'],
            'velocity': np.array([0, 0, launch_conditions['ascent_rate']]),
            'volume': launch_conditions['initial_volume'],
            'gas_mass': launch_conditions['gas_mass'],
            'gas_temperature': launch_conditions['temperature'],
            'burst': False,
            'landed': False
        }

        trajectory = []

        while not state['landed'] and state['time'] < 86400:  # 24 hour limit
            # Get atmospheric conditions
            lat, lon, alt = state['position']
            atm = self.atmosphere.get_conditions(lat, lon, alt, state['time'])

            # Update balloon physics
            state = self.balloon.update_state(state, atm, time_step)

            # Calculate forces
            forces = self.calculate_forces(state, atm)

            # Update dynamics
            acceleration = forces / self.calculate_total_mass(state)
            state['velocity'] += acceleration * time_step
            state['position'] += state['velocity'] * time_step

            # Check landing
            if state['position'][2] <= 0:
                state['landed'] = True

            # Record trajectory point
            trajectory.append(state.copy())

            state['time'] += time_step

        return trajectory