Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
25a98c7
Basic conversion operators for humidity, temperature and pressure
daflack Jan 2, 2026
7a144bc
Add mixing ratio to specific humidity operator
daflack Jan 2, 2026
c9bc564
Add standard atmospheric constants
daflack Jan 2, 2026
e5c1315
Add vapour pressure conversion
daflack Jan 2, 2026
fd2e047
Add vapour pressure if dewpoint unknown
daflack Jan 2, 2026
ce4ba9c
Update naming convections for vapour pressures
daflack Jan 2, 2026
5fa319d
Calculate saturation mixing ratio and specific humidity
daflack Jan 2, 2026
0b77391
Rename vapour_pressure_if_dewpoint_unknown to vapour_pressure_from_RH
daflack Jan 2, 2026
81eaa65
Add mixing ratio from relative humidity conversion
daflack Jan 2, 2026
84a6bc8
Add specific humidity from RH conversion
daflack Jan 2, 2026
5980f83
Add relative humidity conversions from mixing ratio and specific
daflack Jan 2, 2026
90a3b56
Add dewpoint temperature calculation
daflack Jan 2, 2026
82c2e88
Add virtual temperature calculation
daflack Jan 2, 2026
58a3bee
Update atmospheric constants with kappa
daflack Jan 2, 2026
66d5d15
Add potential temperature and exner pressure convertors
daflack Jan 5, 2026
a2e034a
Adds virtual potential temperature convertor
daflack Jan 5, 2026
c9cb801
Adds equivalent potential temperature conversion and fixes unit assig…
daflack Jan 5, 2026
e498366
Remove if loop for RH and switch to convert units
daflack Jan 5, 2026
bfe0993
Adds wet-bulb temperature convertor
daflack Jan 5, 2026
c3a036e
Adds relevant references for temperature calculations where required
daflack Jan 5, 2026
8dad22b
Adds wet-bulb potential temperature convertor
daflack Jan 5, 2026
e917bd6
Adds saturation equivalent potential temperature convertor
daflack Jan 5, 2026
13c9d93
Adds to init file and updates name for atmospheric constants
daflack Jan 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/CSET/operators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,15 @@
convection,
ensembles,
filters,
humidity,
imageprocessing,
mesoscale,
misc,
plot,
pressure,
read,
regrid,
temperature,
transect,
wind,
write,
Expand All @@ -56,13 +59,16 @@
"ensembles",
"execute_recipe",
"filters",
"humidity",
"get_operator",
"imageprocessing",
"mesoscale",
"misc",
"plot",
"pressure",
"read",
"regrid",
"temperature",
"transect",
"wind",
"write",
Expand Down
41 changes: 41 additions & 0 deletions src/CSET/operators/_atmospheric_constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# © Crown copyright, Met Office (2022-2026) and CSET contributors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Constants for the atmosphere."""

# Reference pressure.
P0 = 1000.0 # hPa

# Specific gas constant for dry air.
RD = 287.0

# Specific gas constant for water vapour.
RV = 461.0

# Specific heat capacity for dry air.
CPD = 1005.7

# Latent heat of vaporization.
LV = 2.501e6

# Reference vapour pressure.
E0 = 6.1078 # hPa

# Reference temperature.
T0 = 273.15 # K

# Ratio between mixing ratio of dry and moist air.
EPSILON = 0.622

# Ratio between specific gas constant and specific heat capacity.
KAPPA = RD / CPD
176 changes: 176 additions & 0 deletions src/CSET/operators/humidity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# © Crown copyright, Met Office (2022-2026) and CSET contributors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Operators for humidity conversions."""

import iris.cube

from CSET._common import iter_maybe
from CSET.operators._atmospheric_constants import EPSILON
from CSET.operators.misc import convert_units
from CSET.operators.pressure import vapour_pressure


def specific_humidity_to_mixing_ratio(
cubes: iris.cube.Cube | iris.cube.CubeList,
) -> iris.cube.Cube | iris.cube.CubeList:
"""Convert specific humidity to mixing ratio."""
w = iris.cube.CubeList([])
for cube in iter_maybe(cubes):
mr = cube.copy()
mr = cube / (1 - cube)
mr.rename("mixing_ratio")
w.append(mr)
if len(w) == 1:
return w[0]
else:
return w


def mixing_ratio_to_specific_humidity(
cubes: iris.cube.Cube | iris.cube.CubeList,
) -> iris.cube.Cube | iris.cube.CubeList:
"""Convert mixing ratio to specific humidity."""
q = iris.cube.CubeList([])
for cube in iter_maybe(cubes):
sh = cube.copy()
sh = cube / (1 + cube)
sh.rename("specific_humidity")
q.append(sh)
if len(q) == 1:
return q[0]
else:
return q


def saturation_mixing_ratio(
temperature: iris.cube.Cube | iris.cube.CubeList,
pressure: iris.cube.Cube | iris.cube.CubeList,
) -> iris.cube.Cube | iris.cube.CubeList:
"""Calculate saturation mixing ratio."""
w = iris.cube.CubeList([])
for T, P in zip(iter_maybe(temperature), iter_maybe(pressure), strict=True):
mr = (EPSILON * vapour_pressure(T)) / ((P / 100.0) - vapour_pressure(T))
mr.units = "kg/kg"
mr.rename("mixing_ratio")
w.append(mr)
if len(w) == 1:
return w[0]
else:
return w


def saturation_specific_humidity(
temperature: iris.cube.Cube | iris.cube.CubeList,
pressure: iris.cube.Cube | iris.cube.CubeList,
) -> iris.cube.Cube | iris.cube.CubeList:
"""Calculate saturation specific humidity."""
q = iris.cube.CubeList([])
for T, P in zip(iter_maybe(temperature), iter_maybe(pressure), strict=True):
sh = (EPSILON * vapour_pressure(T)) / (P / 100.0)
sh.units = "kg/kg"
sh.rename("specific_humidity")
q.append(sh)
if len(q) == 1:
return q[0]
else:
return q


def mixing_ratio_from_RH(
temperature: iris.cube.Cube | iris.cube.CubeList,
pressure: iris.cube.Cube | iris.cube.CubeList,
relative_humidity: iris.cube.Cube | iris.cube.CubeList,
) -> iris.cube.Cube | iris.cube.CubeList:
"""Calculate the mixing ratio from RH."""
w = iris.cube.CubeList([])
for T, P, RH in zip(
iter_maybe(temperature),
iter_maybe(pressure),
iter_maybe(relative_humidity),
strict=True,
):
RH = convert_units(RH, "1")
mr = saturation_mixing_ratio(T, P) * RH
w.append(mr)
if len(w) == 1:
return w[0]
else:
return w


def specific_humidity_from_RH(
temperature: iris.cube.Cube | iris.cube.CubeList,
pressure: iris.cube.Cube | iris.cube.CubeList,
relative_humidity: iris.cube.Cube | iris.cube.CubeList,
) -> iris.cube.Cube | iris.cube.CubeList:
"""Calculate the mixing ratio from RH."""
q = iris.cube.CubeList([])
for T, P, RH in zip(
iter_maybe(temperature),
iter_maybe(pressure),
iter_maybe(relative_humidity),
strict=True,
):
RH = convert_units(RH, "1")
sh = saturation_specific_humidity(T, P) * RH
q.append(sh)
if len(q) == 1:
return q[0]
else:
return q


def relative_humidity_from_mixing_ratio(
mixing_ratio: iris.cube.Cube | iris.cube.CubeList,
temperature: iris.cube.Cube | iris.cube.CubeList,
pressure: iris.cube.Cube | iris.cube.CubeList,
) -> iris.cube.Cube | iris.cube.CubeList:
"""Convert mixing ratio to relative humidity."""
RH = iris.cube.CubeList([])
for W, T, P in zip(
iter_maybe(mixing_ratio),
iter_maybe(temperature),
iter_maybe(pressure),
strict=True,
):
rel_h = W / saturation_mixing_ratio(T, P)
rel_h.rename("relative_humidity")
RH.append(rel_h)
if len(RH) == 1:
return RH[0]
else:
return RH


def relative_humidity_from_specific_humidity(
specific_humidity: iris.cube.Cube | iris.cube.CubeList,
temperature: iris.cube.Cube | iris.cube.CubeList,
pressure: iris.cube.Cube | iris.cube.CubeList,
) -> iris.cube.Cube | iris.cube.CubeList:
"""Convert specific humidity to relative humidity."""
RH = iris.cube.CubeList([])
for Q, T, P in zip(
iter_maybe(specific_humidity),
iter_maybe(temperature),
iter_maybe(pressure),
strict=True,
):
rel_h = Q / saturation_specific_humidity(T, P)
rel_h.rename("relative_humidity")
RH.append(rel_h)
if len(RH) == 1:
return RH[0]
else:
return RH
75 changes: 75 additions & 0 deletions src/CSET/operators/pressure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# © Crown copyright, Met Office (2022-2026) and CSET contributors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Operators for pressure conversions."""

import iris.cube
import numpy as np

from CSET._common import iter_maybe
from CSET.operators._atmospheric_constants import E0, KAPPA, P0
from CSET.operators.misc import convert_units


def vapour_pressure(
temperature: iris.cube.Cube | iris.cube.CubeList,
) -> iris.cube.Cube | iris.cube.CubeList:
"""Calculate the vapour pressure of the atmosphere."""
v_pressure = iris.cube.CubeList([])
for T in iter_maybe(temperature):
es = T.copy()
exponent = 17.27 * (T - 273.16) / (T - 35.86)
es.data[:] = E0 * np.exp(exponent.core_data())
es.units = "hPa"
es.rename("vapour_pressure")
v_pressure.append(es)
if len(v_pressure) == 1:
return v_pressure[0]
else:
return v_pressure


def vapour_pressure_from_RH(
temperature: iris.cube.Cube | iris.cube.CubeList,
relative_humidity: iris.cube.Cube | iris.cube.CubeList,
) -> iris.cube.Cube | iris.cube.CubeList:
"""Calculate the vapour pressure using RH."""
v_pressure = iris.cube.CubeList([])
for T, RH in zip(
iter_maybe(temperature), iter_maybe(relative_humidity), strict=True
):
RH = convert_units(RH, "1")
vp = vapour_pressure(T) * RH
v_pressure.append(vp)
if len(v_pressure) == 1:
return v_pressure[0]
else:
return v_pressure


def exner_pressure(
pressure: iris.cube.Cube | iris.cube.CubeList,
) -> iris.cube.Cube | iris.cube.CubeList:
"""Calculate the exner pressure."""
pi = iris.cube.CubeList([])
for P in iter_maybe(pressure):
PI = P.copy()
PI.data[:] = (P.core_data() / P0) ** KAPPA
PI.rename("exner_pressure")
PI.units = "1"
pi.append(PI)
if len(pi) == 1:
return pi[0]
else:
return pi
Loading
Loading