Skip to main content
Glama
atmosphere.py8.57 kB
""" Atmosphere Models and Wind Profiles Provides ISA/COESA atmosphere models and wind profile calculations. Falls back to manual ISA calculations when optional dependencies unavailable. """ import math from pydantic import BaseModel, Field from . import update_availability # Constants R_SPECIFIC = 287.0528 # J/(kg·K) - specific gas constant for dry air GAMMA = 1.4 # ratio of specific heats G0 = 9.80665 # m/s² - standard gravity # ISA Standard atmosphere layers (altitude_m, temp_K, lapse_rate_K_per_m) ISA_LAYERS = [ (0, 288.15, -0.0065), # Troposphere (11000, 216.65, 0.0), # Tropopause (20000, 216.65, 0.001), # Stratosphere 1 (32000, 228.65, 0.0028), # Stratosphere 2 (47000, 270.65, 0.0), # Stratopause (51000, 270.65, -0.0028), # Mesosphere 1 (71000, 214.65, -0.002), # Mesosphere 2 (84852, 186.946, 0.0), # Mesopause ] # Optional library imports AMBIANCE_AVAILABLE = False try: import ambiance AMBIANCE_AVAILABLE = True # Try to get version, but don't fail if not available try: version = ambiance.__version__ except AttributeError: version = "unknown" update_availability("atmosphere", True, {"ambiance": version}) except ImportError: update_availability("atmosphere", True, {}) # Still available with manual ISA # Data models class AtmospherePoint(BaseModel): """Single atmosphere condition point.""" altitude_m: float = Field(..., description="Geometric altitude in meters") pressure_pa: float = Field(..., description="Static pressure in Pascals") temperature_k: float = Field(..., description="Temperature in Kelvin") density_kg_m3: float = Field(..., description="Air density in kg/m³") speed_of_sound_mps: float = Field(..., description="Speed of sound in m/s") viscosity_pa_s: float | None = Field(None, description="Dynamic viscosity in Pa·s") class WindPoint(BaseModel): """Single wind profile point.""" altitude_m: float = Field(..., description="Altitude in meters") wind_speed_mps: float = Field(..., description="Wind speed in m/s") wind_direction_deg: float | None = Field( None, description="Wind direction in degrees" ) def _isa_manual(altitude_m: float) -> tuple[float, float, float]: """ Manual ISA calculation for fallback when ambiance unavailable. Returns: (pressure_pa, temperature_k, density_kg_m3) """ h = altitude_m # Find appropriate layer for i, (h_base, T_base, lapse_rate) in enumerate(ISA_LAYERS): if i == len(ISA_LAYERS) - 1 or h < ISA_LAYERS[i + 1][0]: break dh = h - h_base if abs(lapse_rate) < 1e-10: # Isothermal layer T = T_base p_ratio = math.exp(-G0 * dh / (R_SPECIFIC * T_base)) else: # Temperature gradient layer T = T_base + lapse_rate * dh p_ratio = (T / T_base) ** (-G0 / (R_SPECIFIC * lapse_rate)) # Get base pressure from previous layer p_base = 101325.0 # Sea level pressure for j in range(i): h_layer_base, T_layer_base, lr = ISA_LAYERS[j] h_layer_top = ISA_LAYERS[j + 1][0] dh_layer = h_layer_top - h_layer_base if abs(lr) < 1e-10: p_base *= math.exp(-G0 * dh_layer / (R_SPECIFIC * T_layer_base)) else: T_layer_top = T_layer_base + lr * dh_layer p_base *= (T_layer_top / T_layer_base) ** (-G0 / (R_SPECIFIC * lr)) pressure = p_base * p_ratio density = pressure / (R_SPECIFIC * T) return pressure, T, density def get_atmosphere_profile( altitudes_m: list[float], model_type: str = "ISA" ) -> list[AtmospherePoint]: """ Get atmospheric properties at specified altitudes. Args: altitudes_m: List of geometric altitudes in meters (0-81020m when using ambiance, 0-86000m for manual ISA) model_type: Atmosphere model ("ISA", "COESA") - currently only ISA supported Returns: List of AtmospherePoint objects with pressure, temperature, density, etc. """ if model_type not in ["ISA", "COESA"]: raise ValueError(f"Unknown model type: {model_type}. Use 'ISA' or 'COESA'") results = [] for altitude in altitudes_m: # Use appropriate limits based on availability of ambiance library max_altitude = 81020 if AMBIANCE_AVAILABLE else 86000 if altitude < 0 or altitude > max_altitude: range_str = f"0-{max_altitude}m" raise ValueError(f"Altitude {altitude}m out of ISA range ({range_str})") if AMBIANCE_AVAILABLE and model_type == "ISA": # Use ambiance library if available atm = ambiance.Atmosphere(altitude) point = AtmospherePoint( altitude_m=altitude, pressure_pa=float(atm.pressure), temperature_k=float(atm.temperature), density_kg_m3=float(atm.density), speed_of_sound_mps=float(atm.speed_of_sound), viscosity_pa_s=( float(atm.dynamic_viscosity) if hasattr(atm, "dynamic_viscosity") else None ), ) else: # Fall back to manual calculation pressure, temperature, density = _isa_manual(altitude) speed_of_sound = math.sqrt(GAMMA * R_SPECIFIC * temperature) point = AtmospherePoint( altitude_m=altitude, pressure_pa=pressure, temperature_k=temperature, density_kg_m3=density, speed_of_sound_mps=speed_of_sound, viscosity_pa_s=None, # Not calculated in manual mode ) results.append(point) return results def wind_model_simple( altitudes_m: list[float], surface_wind_mps: float, surface_altitude_m: float = 0.0, model: str = "logarithmic", roughness_length_m: float = 0.1, reference_height_m: float = 10.0, ) -> list[WindPoint]: """ Simple wind profile models for low-altitude studies. Args: altitudes_m: Altitude points for wind calculation surface_wind_mps: Wind speed at reference height surface_altitude_m: Surface elevation model: "logarithmic" or "power" law roughness_length_m: Surface roughness length (for logarithmic) reference_height_m: Height of surface wind measurement Returns: List of WindPoint objects with wind speeds """ if model not in ["logarithmic", "power"]: raise ValueError(f"Unknown wind model: {model}. Use 'logarithmic' or 'power'") results = [] for altitude in altitudes_m: height_agl = altitude - surface_altitude_m if height_agl < 0: wind_speed = 0.0 # Below ground elif height_agl < reference_height_m: # Linear interpolation below reference height wind_speed = surface_wind_mps * (height_agl / reference_height_m) else: if model == "logarithmic": # Logarithmic wind profile if roughness_length_m <= 0: raise ValueError("Roughness length must be positive") wind_speed = surface_wind_mps * ( math.log(height_agl / roughness_length_m) / math.log(reference_height_m / roughness_length_m) ) else: # power law # Power law with typical exponent alpha = 0.143 # Typical for open terrain wind_speed = ( surface_wind_mps * (height_agl / reference_height_m) ** alpha ) results.append( WindPoint( altitude_m=altitude, wind_speed_mps=max(0.0, wind_speed), # Ensure non-negative ) ) return results def get_atmospheric_properties(altitude_m: float) -> dict[str, float]: """ Get atmospheric properties at a single altitude (convenience function). Args: altitude_m: Altitude in meters Returns: Dictionary with atmospheric properties """ profile = get_atmosphere_profile([altitude_m]) point = profile[0] return { "altitude_m": point.altitude_m, "pressure_pa": point.pressure_pa, "temperature_k": point.temperature_k, "temperature_c": point.temperature_k - 273.15, "density_kg_m3": point.density_kg_m3, "speed_of_sound_mps": point.speed_of_sound_mps, "viscosity_pa_s": point.viscosity_pa_s, }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cheesejaguar/aerospace-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server