Source code for improver.psychrometric_calculations.air_density
# (C) Crown Copyright, Met Office. All rights reserved.
#
# This file is part of 'IMPROVER' and is released under the BSD 3-Clause license.
# See LICENSE in the root of the repository for full licensing details.
"""Plugin to calculate air density from virtual temperature."""
from typing import Union
import iris
from iris.cube import Cube, CubeList
from improver import BasePlugin
from improver.constants import R_DRY_AIR
[docs]
class AirDensity(BasePlugin):
"""Calculate air density from virtual temperature."""
[docs]
def _get_pressure_cube(self, cubes: CubeList) -> Cube:
"""Extract pressure cube if present."""
for cube in cubes:
if cube.standard_name == "air_pressure":
return cube
return None
[docs]
def _get_temperature_cube(self, cubes: CubeList) -> Cube:
"""Extract virtual temperature cube."""
for cube in cubes:
if cube.standard_name == "virtual_temperature":
return cube
raise ValueError("No virtual_temperature cube provided.")
[docs]
def process(self, inputs: Union[Cube, CubeList]) -> Cube:
"""
Calculate air density from virtual temperature.
If an explicit pressure cube is provided the pressure is taken from here.
Otherwise the virtual temperature cube needs to be on pressure levels.
The cube arguments are not checked explicitly for conformant dimensions
but must have the same shape.
Args:
inputs:
Either:
- A single Cube (virtual temperature), or
- A CubeList containing virtual temperature and optionally pressure.
Returns:
Cube containing air density (kg m-3) on the same grid/levels as virtual temmperature
"""
# --- Normalize input to CubeList ---
if isinstance(inputs, Cube):
cubes = CubeList([inputs])
else:
cubes = inputs
# --- Get temperature cube ---
Tv_cube = self._get_temperature_cube(cubes)
# --- Try to get explicit pressure cube ---
pressure_cube = self._get_pressure_cube(cubes)
# --- Determine pressure source ---
if pressure_cube is not None:
# Use explicit pressure cube
pressure = pressure_cube.copy()
pressure.convert_units("Pa")
pressure_data = pressure.data
else:
# Try to infer from temperature cube (pressure levels case)
try:
pressure_coord = Tv_cube.coord("air_pressure")
except iris.exceptions.CoordinateNotFoundError:
raise ValueError(
"No pressure information supplied: "
"provide an air_pressure cube or use a temperature cube "
"on pressure levels."
)
pressure = pressure_coord.copy()
pressure.convert_units("Pa")
pressure_data = iris.util.broadcast_to_shape(
pressure.points,
Tv_cube.shape,
Tv_cube.coord_dims("air_pressure"),
)
# --- Ensure temperature is in Kelvin ---
Tv = Tv_cube.copy()
Tv.convert_units("K")
# --- Compute density ---
density_data = pressure_data / (R_DRY_AIR * Tv.data)
# --- Create output cube ---
density = Tv_cube.copy(data=density_data)
density.rename("air_density")
density.units = "kg m-3"
return density