Skip to content

Atmo

Atmo

Atmo(
    altitude: Optional[Union[float, Distance]] = None,
    pressure: Optional[Union[float, Pressure]] = None,
    temperature: Optional[Union[float, Temperature]] = None,
    humidity: float = 0.0,
    powder_t: Optional[Union[float, Temperature]] = None,
)

Atmospheric conditions and density calculations.

This class encapsulates atmospheric conditions (altitude, pressure, temperature, relative humidity) and provides helpers to derive air density ratio, actual densities, and local speed of sound (Mach 1). The instance stores an internal "base" altitude/pressure/temperature snapshot (_a0, _p0, _t0) used to interpolate conditions at other altitudes using lapse-rate models.

Attributes:

Name Type Description
altitude Distance

Altitude relative to sea level.

pressure Pressure

Unadjusted barometric (station) pressure.

temperature Temperature

Ambient air temperature.

humidity float

Relative humidity expressed either as fraction [0..1] or percent [0..100].

powder_temp Temperature

Powder temperature (may differ from ambient when powder sensitivity enabled).

density_ratio float

Ratio of local air density to standard density.

mach Velocity

Local speed of sound (Mach 1).

density_metric float

Air density in kg/m^3.

density_imperial float

Air density in lb/ft^3.

Parameters:

Name Type Description Default
altitude Optional[Union[float, Distance]]

Altitude relative to sea level. Defaults to 0.

None
pressure Optional[Union[float, Pressure]]

Station pressure (unadjusted). Defaults to standard pressure for altitude.

None
temperature Optional[Union[float, Temperature]]

Ambient temperature. Defaults to standard temperature for altitude.

None
humidity float

Relative humidity (fraction or percent). Defaults to 0.

0.0
powder_t Optional[Union[float, Temperature]]

Powder (propellant) temperature. Defaults to ambient temperature.

None
Example
from py_ballisticcalc import Atmo, Unit
atmo = Atmo(
    altitude=Unit.Meter(100),
    pressure=Unit.hPa(1000),
    temperature=Unit.Celsius(20),
    humidity=50,
    powder_t=Unit.Celsius(15)
)
Notes

The constructor caches base conditions (_t0 in °C, _p0 in hPa, _a0 in feet) and computes associated _mach and _density_ratio. Subsequent changes to humidity trigger an automatic density recomputation.

Methods:

Name Description
update_density_ratio

Recompute density ratio for changed humidity.

temperature_at_altitude

Interpolate temperature (°C) at altitude using lapse rate.

pressure_at_altitude

Interpolate pressure (hPa) at altitude using barometric formula.

get_density_and_mach_for_altitude

Compute density ratio and Mach (fps) for the specified altitude.

standard_temperature

ICAO standard temperature for altitude (valid to ~36,000 ft).

standard_pressure

ICAO standard pressure for altitude (valid to ~36,000 ft).

icao

Create a standard ICAO atmosphere at altitude.

machF

Mach 1 (fps) for given Fahrenheit temperature.

machC

Mach 1 (m/s) for given Celsius temperature.

machK

Mach 1 (m/s) for given Kelvin temperature.

calculate_air_density

Air density from temperature (°C), pressure (hPa), and humidity.

Source code in py_ballisticcalc/conditions.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
def __init__(
    self,
    altitude: Optional[Union[float, Distance]] = None,
    pressure: Optional[Union[float, Pressure]] = None,
    temperature: Optional[Union[float, Temperature]] = None,
    humidity: float = 0.0,
    powder_t: Optional[Union[float, Temperature]] = None,
):
    """Initialize an `Atmo` instance.

    Args:
        altitude: Altitude relative to sea level. Defaults to 0.
        pressure: Station pressure (unadjusted). Defaults to standard pressure for altitude.
        temperature: Ambient temperature. Defaults to standard temperature for altitude.
        humidity: Relative humidity (fraction or percent). Defaults to 0.
        powder_t: Powder (propellant) temperature. Defaults to ambient temperature.

    Example:
        ```python
        from py_ballisticcalc import Atmo, Unit
        atmo = Atmo(
            altitude=Unit.Meter(100),
            pressure=Unit.hPa(1000),
            temperature=Unit.Celsius(20),
            humidity=50,
            powder_t=Unit.Celsius(15)
        )
        ```

    Notes:
        The constructor caches base conditions (`_t0` in °C, `_p0` in hPa, `_a0` in feet) and computes associated
        `_mach` and `_density_ratio`. Subsequent changes to humidity trigger an automatic density recomputation.
    """
    self._initializing = True
    self._altitude = PreferredUnits.distance(altitude or 0)
    self._pressure = PreferredUnits.pressure(pressure or Atmo.standard_pressure(self._altitude))
    self._temperature = PreferredUnits.temperature(temperature or Atmo.standard_temperature(self._altitude))
    # If powder_temperature not provided we use atmospheric temperature:
    self._powder_temp = PreferredUnits.temperature(powder_t or self._temperature)
    self._t0 = self._temperature >> Temperature.Celsius
    self._p0 = self._pressure >> Pressure.hPa
    self._a0 = self._altitude >> Distance.Foot
    self._mach = Atmo.machF(self._temperature >> Temperature.Fahrenheit)
    self.humidity = humidity
    self._initializing = False
    self.update_density_ratio()

Attributes

altitude property
altitude: Distance

Altitude relative to sea level.

pressure property
pressure: Pressure

Station barometric pressure (not altitude adjusted).

temperature property
temperature: Temperature

Air temperature.

powder_temp property
powder_temp: Temperature

Powder temperature (falls back to ambient when unspecified).

mach property
mach: Velocity

Local speed of sound (Mach 1).

density_ratio property
density_ratio: float

Ratio of local density to standard density (dimensionless).

humidity property writable
humidity: float

Relative humidity as fraction [0..1].

density_metric property
density_metric: float

Air density in metric units (kg/m^3).

density_imperial property
density_imperial: float

Air density in imperial units (lb/ft^3).

Functions

update_density_ratio
update_density_ratio() -> None

Recompute density ratio for changed humidity.

Source code in py_ballisticcalc/conditions.py
220
221
222
def update_density_ratio(self) -> None:
    """Recompute density ratio for changed humidity."""
    self._density_ratio = Atmo.calculate_air_density(self._t0, self._p0, self.humidity) / cStandardDensityMetric
temperature_at_altitude
temperature_at_altitude(altitude: float) -> float

Interpolate temperature (°C) at altitude using lapse rate.

Parameters:

Name Type Description Default
altitude float

Altitude above mean sea level (ft).

required

Returns:

Type Description
float

Temperature in degrees Celsius (bounded by model lower limit).

Source code in py_ballisticcalc/conditions.py
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
def temperature_at_altitude(self, altitude: float) -> float:
    """Interpolate temperature (°C) at altitude using lapse rate.

    Args:
        altitude: Altitude above mean sea level (ft).

    Returns:
        Temperature in degrees Celsius (bounded by model lower limit).
    """
    t = (altitude - self._a0) * cLapseRateKperFoot + self._t0
    if t < Atmo.cLowestTempC:
        t = Atmo.cLowestTempC
        warnings.warn(
            f"Temperature interpolated from altitude fell below minimum model limit. Bounded at {cLowestTempF}°F.",
            RuntimeWarning,
        )
    return t
pressure_at_altitude
pressure_at_altitude(altitude: float) -> float

Interpolate pressure (hPa) at altitude using barometric formula.

Parameters:

Name Type Description Default
altitude float

Altitude above mean sea level (ft).

required

Returns:

Type Description
float

Pressure in hPa.

Source code in py_ballisticcalc/conditions.py
242
243
244
245
246
247
248
249
250
251
252
253
def pressure_at_altitude(self, altitude: float) -> float:
    """Interpolate pressure (hPa) at altitude using barometric formula.

    Args:
        altitude: Altitude above mean sea level (ft).

    Returns:
        Pressure in hPa.
    """
    return self._p0 * math.pow(
        1 + cLapseRateKperFoot * (altitude - self._a0) / (self._t0 + cDegreesCtoK), cPressureExponent
    )
get_density_and_mach_for_altitude
get_density_and_mach_for_altitude(
    altitude: float,
) -> Tuple[float, float]

Compute density ratio and Mach (fps) for the specified altitude.

Uses lapse-rate interpolation unless altitude is within 30 ft of the base altitude, in which case the initial cached values are used for performance.

Parameters:

Name Type Description Default
altitude float

Altitude above mean sea level (ft).

required

Returns:

Type Description
Tuple[float, float]

Tuple (density_ratio, mach_fps).

Source code in py_ballisticcalc/conditions.py
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
def get_density_and_mach_for_altitude(self, altitude: float) -> Tuple[float, float]:
    """Compute density ratio and Mach (fps) for the specified altitude.

    Uses lapse-rate interpolation unless altitude is within 30 ft of the base altitude,
        in which case the initial cached values are used for performance.

    Args:
        altitude: Altitude above mean sea level (ft).

    Returns:
        Tuple (density_ratio, mach_fps).
    """
    if math.fabs(self._a0 - altitude) < 30:  # fast path near base altitude
        return self._density_ratio, self._mach

    if altitude > 36089:  # troposphere limit ~36k ft
        warnings.warn(
            "Density request for altitude above modeled troposphere. Atmospheric model not valid here.",
            RuntimeWarning,
        )

    t_k = self.temperature_at_altitude(altitude) + cDegreesCtoK
    mach = Velocity.MPS(Atmo.machK(t_k)) >> Velocity.FPS
    p = self.pressure_at_altitude(altitude)
    density_delta = ((self._t0 + cDegreesCtoK) * p) / (self._p0 * t_k)
    density_ratio = self._density_ratio * density_delta
    # Alternative simplified exponential model (retained for reference):
    # density_ratio = self._density_ratio * math.exp(-(altitude - self._a0) / 34122)
    return density_ratio, mach
standard_temperature staticmethod
standard_temperature(altitude: Distance) -> Temperature

ICAO standard temperature for altitude (valid to ~36,000 ft).

Source code in py_ballisticcalc/conditions.py
288
289
290
291
@staticmethod
def standard_temperature(altitude: Distance) -> Temperature:
    """ICAO standard temperature for altitude (valid to ~36,000 ft)."""
    return Temperature.Fahrenheit(cStandardTemperatureF + (altitude >> Distance.Foot) * cLapseRateImperial)
standard_pressure staticmethod
standard_pressure(altitude: Distance) -> Pressure

ICAO standard pressure for altitude (valid to ~36,000 ft).

Source code in py_ballisticcalc/conditions.py
293
294
295
296
297
298
299
300
301
302
@staticmethod
def standard_pressure(altitude: Distance) -> Pressure:
    """ICAO standard pressure for altitude (valid to ~36,000 ft)."""
    return Pressure.hPa(
        cStandardPressureMetric
        * math.pow(
            1 + cLapseRateMetric * (altitude >> Distance.Meter) / (cStandardTemperatureC + cDegreesCtoK),
            cPressureExponent,
        )
    )
icao staticmethod
icao(
    altitude: Union[float, Distance] = 0,
    temperature: Optional[Temperature] = None,
    humidity: float = cStandardHumidity,
) -> Atmo

Create a standard ICAO atmosphere at altitude.

Parameters:

Name Type Description Default
altitude Union[float, Distance]

Altitude (defaults to sea level).

0
temperature Optional[Temperature]

Optional override temperature (defaults to standard at altitude).

None
humidity float

Relative humidity (fraction or percent). Defaults to standard humidity.

cStandardHumidity

Returns:

Type Description
Atmo

Atmo instance representing standard atmosphere at altitude.

Source code in py_ballisticcalc/conditions.py
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
@staticmethod
def icao(
    altitude: Union[float, Distance] = 0,
    temperature: Optional[Temperature] = None,
    humidity: float = cStandardHumidity,
) -> Atmo:
    """Create a standard ICAO atmosphere at altitude.

    Args:
        altitude: Altitude (defaults to sea level).
        temperature: Optional override temperature (defaults to standard at altitude).
        humidity: Relative humidity (fraction or percent). Defaults to standard humidity.

    Returns:
        Atmo instance representing standard atmosphere at altitude.
    """
    altitude = PreferredUnits.distance(altitude)
    temperature = temperature or Atmo.standard_temperature(altitude)
    pressure = Atmo.standard_pressure(altitude)
    return Atmo(altitude, pressure, temperature, humidity)
machF staticmethod
machF(fahrenheit: float) -> float

Mach 1 (fps) for given Fahrenheit temperature.

Source code in py_ballisticcalc/conditions.py
328
329
330
331
332
333
334
335
@staticmethod
def machF(fahrenheit: float) -> float:
    """Mach 1 (fps) for given Fahrenheit temperature."""
    if fahrenheit < -cDegreesFtoR:
        bad_temp = fahrenheit
        fahrenheit = cLowestTempF
        warnings.warn(f"Invalid temperature: {bad_temp}°F. Adjusted to ({cLowestTempF}°F).", RuntimeWarning)
    return math.sqrt(fahrenheit + cDegreesFtoR) * cSpeedOfSoundImperial
machC staticmethod
machC(celsius: float) -> float

Mach 1 (m/s) for given Celsius temperature.

Source code in py_ballisticcalc/conditions.py
337
338
339
340
341
342
343
344
@staticmethod
def machC(celsius: float) -> float:
    """Mach 1 (m/s) for given Celsius temperature."""
    if celsius < -cDegreesCtoK:
        bad_temp = celsius
        celsius = Atmo.cLowestTempC
        warnings.warn(f"Invalid temperature: {bad_temp}°C. Adjusted to ({celsius}°C).", RuntimeWarning)
    return Atmo.machK(celsius + cDegreesCtoK)
machK staticmethod
machK(kelvin: float) -> float

Mach 1 (m/s) for given Kelvin temperature.

Source code in py_ballisticcalc/conditions.py
346
347
348
349
350
351
352
353
@staticmethod
def machK(kelvin: float) -> float:
    """Mach 1 (m/s) for given Kelvin temperature."""
    if kelvin < 0:
        bad_temp = kelvin
        kelvin = Atmo.cLowestTempC + cDegreesCtoK
        warnings.warn(f"Invalid temperature: {bad_temp}K. Adjusted to ({kelvin}K).", RuntimeWarning)
    return math.sqrt(kelvin) * cSpeedOfSoundMetric
calculate_air_density staticmethod
calculate_air_density(
    t: float, p_hpa: float, humidity: float
) -> float

Air density from temperature (°C), pressure (hPa), and humidity.

Parameters:

Name Type Description Default
t float

Temperature in degrees Celsius.

required
p_hpa float

Pressure in hPa (hectopascals). Internally converted to Pa.

required
humidity float

Relative humidity (fraction or percent).

required

Returns:

Type Description
float

Air density in kg/m³.

Notes
  • Divide result by cDensityImperialToMetric to get density in lb/ft³.
  • Source: CIPM-2007 (https://www.nist.gov/system/files/documents/calibrations/CIPM-2007.pdf)
Source code in py_ballisticcalc/conditions.py
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
@staticmethod
def calculate_air_density(t: float, p_hpa: float, humidity: float) -> float:
    """Air density from temperature (°C), pressure (hPa), and humidity.

    Args:
        t: Temperature in degrees Celsius.
        p_hpa: Pressure in hPa (hectopascals). Internally converted to Pa.
        humidity: Relative humidity (fraction or percent).

    Returns:
        Air density in kg/m³.

    Notes:
        - Divide result by `cDensityImperialToMetric` to get density in lb/ft³.
        - Source: CIPM-2007 (https://www.nist.gov/system/files/documents/calibrations/CIPM-2007.pdf)
    """
    R = 8.314472  # J/(mol·K), universal gas constant
    M_a = 28.96546e-3  # kg/mol, molar mass of dry air
    M_v = 18.01528e-3  # kg/mol, molar mass of water vapor

    def saturation_vapor_pressure(T):  # noqa: N802 (retain formula variable naming)
        A = [1.2378847e-5, -1.9121316e-2, 33.93711047, -6.3431645e3]
        return math.exp(A[0] * T**2 + A[1] * T + A[2] + A[3] / T)

    def enhancement_factor(p, T):  # noqa: N802
        alpha = 1.00062
        beta = 3.14e-8
        gamma = 5.6e-7
        return alpha + beta * p + gamma * T**2

    def compressibility_factor(p, T, x_v):  # noqa: N802
        a0 = 1.58123e-6
        a1 = -2.9331e-8
        a2 = 1.1043e-10
        b0 = 5.707e-6
        b1 = -2.051e-8
        c0 = 1.9898e-4
        c1 = -2.376e-6
        d = 1.83e-11
        e = -0.765e-8
        t_l = T - cDegreesCtoK
        Z = (
            1
            - (p / T) * (a0 + a1 * t_l + a2 * t_l**2 + (b0 + b1 * t_l) * x_v + (c0 + c1 * t_l) * x_v**2)
            + (p / T) ** 2 * (d + e * x_v**2)
        )
        return Z

    # Normalize humidity to fraction [0..1]
    rh = float(humidity)
    rh_frac = rh / 100.0 if rh > 1.0 else rh
    rh_frac = max(0.0, min(1.0, rh_frac))

    # Convert inputs for CIPM equations
    T_K = t + cDegreesCtoK  # Kelvin
    p = float(p_hpa) * 100.0  # hPa -> Pa

    # Calculation of saturated vapor pressure and enhancement factor
    p_sv = saturation_vapor_pressure(T_K)  # Pa (saturated vapor pressure)
    f = enhancement_factor(p, t)  # Enhancement factor (p in Pa, t in °C)

    # Partial pressure of water vapor and mole fraction
    p_v = rh_frac * f * p_sv  # Pa
    x_v = p_v / p  # Mole fraction of water vapor

    # Calculation of compressibility factor
    Z = compressibility_factor(p, T_K, x_v)
    return (p * M_a) / (Z * R * T_K) * (1.0 - x_v * (1.0 - M_v / M_a))

Vacuum

Vacuum(
    altitude: Optional[Union[float, Distance]] = None,
    temperature: Optional[Union[float, Temperature]] = None,
)

Bases: Atmo

Vacuum atmosphere (zero density => zero drag).

Source code in py_ballisticcalc/conditions.py
428
429
430
431
432
433
def __init__(
    self, altitude: Optional[Union[float, Distance]] = None, temperature: Optional[Union[float, Temperature]] = None
):
    super().__init__(altitude, 0, temperature, 0)
    self._pressure = PreferredUnits.pressure(0)
    self._density_ratio = 0