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.
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 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.
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 |
As a latex balloon rises, internal pressure must balance external pressure plus elastic stress:
For a thin-walled sphere under internal pressure:
where the hoop stress $\sigma$ depends on strain:
Combining all terms, the pressure inside the balloon is:
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 (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).
The net vertical force on a ZP balloon is:
For neutral buoyancy (floating condition):
The float altitude is where the balloon achieves neutral buoyancy:
When the balloon reaches full inflation, excess gas must be vented:
where vent velocity depends on buoyancy:
Altitude control through ballast drops:
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}
The open bottom of ZP balloons causes passive gas exchange through convection and diffusion:
This results in 2-4% daily gas loss through the nadir hole, contributing to the 16-24 hour flight duration.
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 |
Our implementation uses Reynolds number-dependent drag without artificial limits:
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
}
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 |
Accurate gas modeling is crucial for both balloon types. We use both ideal and real gas equations depending on conditions.
For most conditions, the ideal gas law suffices:
For high pressures or low temperatures, we use the Peng-Robinson equation of state:
where:
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 |
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)
For latex balloons, we model the nonlinear stress-strain behavior:
For moderate strains (up to 100%):
where $G = E/3$ is the shear modulus for incompressible materials.
For large strains (>100%):
Material degradation affects burst altitude:
Gas leakage determines flight duration, especially for ZP balloons designed for multi-day flights.
Gas molecules diffuse through the balloon material:
Relative permeation rates for different gases:
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.
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) |
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
Proper initialization of launch altitude is critical for accurate simulation results.
# 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
Accurate burst prediction is crucial for latex balloon trajectory planning.
For a thin-walled sphere under internal pressure:
For a balloon (biaxial stress state):
Therefore:
Due to manufacturing variations, burst diameter follows a normal distribution:
Typical values:
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 |
As balloons expand, their shape transitions from spherical to more complex geometries due to material stress and pressure differentials.
The balloon's aspect ratio changes with internal pressure:
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
At high altitudes, ice can accumulate on balloon surfaces, affecting mass and thermal properties.
Ice accumulation depends on atmospheric conditions and balloon temperature:
The fraction of water droplets/crystals that stick to the balloon:
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
Ultraviolet radiation degrades balloon materials, reducing burst altitude over time.
Cumulative UV exposure depends on altitude and time:
Ultimate tensile strength decreases with UV dose:
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 is the net upward force measured at the balloon's neck during filling, critical for achieving desired ascent rates.
The relationship between neck lift and ascent rate:
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
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
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