Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
f47c864
Basic conversion operators for humidity, temperature and pressure
daflack Jan 2, 2026
c3e4c3f
Add mixing ratio to specific humidity operator
daflack Jan 2, 2026
3c8d3c8
Add standard atmospheric constants
daflack Jan 2, 2026
efa2e98
Add vapour pressure conversion
daflack Jan 2, 2026
10ad1c0
Add vapour pressure if dewpoint unknown
daflack Jan 2, 2026
f6a299b
Update naming convections for vapour pressures
daflack Jan 2, 2026
c592e5c
Calculate saturation mixing ratio and specific humidity
daflack Jan 2, 2026
a194686
Rename vapour_pressure_if_dewpoint_unknown to vapour_pressure_from_RH
daflack Jan 2, 2026
722a5b9
Add mixing ratio from relative humidity conversion
daflack Jan 2, 2026
1498892
Add specific humidity from RH conversion
daflack Jan 2, 2026
6d2d64d
Add relative humidity conversions from mixing ratio and specific
daflack Jan 2, 2026
15c5c36
Add dewpoint temperature calculation
daflack Jan 2, 2026
a800592
Add virtual temperature calculation
daflack Jan 2, 2026
207ed09
Update atmospheric constants with kappa
daflack Jan 2, 2026
0951fcc
Add potential temperature and exner pressure convertors
daflack Jan 5, 2026
b7450e8
Adds virtual potential temperature convertor
daflack Jan 5, 2026
5528601
Adds equivalent potential temperature conversion and fixes unit assig…
daflack Jan 5, 2026
b2c2d78
Remove if loop for RH and switch to convert units
daflack Jan 5, 2026
249d7a8
Adds wet-bulb temperature convertor
daflack Jan 5, 2026
b7a9e84
Adds relevant references for temperature calculations where required
daflack Jan 5, 2026
fa8a734
Adds wet-bulb potential temperature convertor
daflack Jan 5, 2026
1654a59
Adds saturation equivalent potential temperature convertor
daflack Jan 5, 2026
07383f6
Adds to init file and updates name for atmospheric constants
daflack Jan 5, 2026
023f115
Update argument names in specific humidity to mixing ratio and vice
daflack Jan 6, 2026
73c473c
Uses convert_units for pressure consistency
daflack Jan 6, 2026
335e7b9
Adds names to humidity cubes where missing
daflack Jan 7, 2026
aa3819d
Correct units
daflack Jan 7, 2026
64c9831
Adds tests data and fixtures
daflack Jan 7, 2026
b3d5343
Update copyright year
daflack Jan 7, 2026
5782370
Add tests for vapour pressure
daflack Jan 7, 2026
577a48c
Update to assignment in pressure convertors
daflack Jan 7, 2026
20f10d1
Rename vapour_pressure_from_RH to relative_humidity_to_vapour_pressure
daflack Jan 7, 2026
9a0e633
Adds tests for relative_humidity_to_vapour_pressure
daflack Jan 7, 2026
4b3d1c7
Adds tests for exner pressure
daflack Jan 7, 2026
82516ba
Uses atmospheric constants in tests
daflack Jan 7, 2026
e399389
Change to vapour pressure from relative humidity as more intuitive
daflack Jan 7, 2026
1a820d2
Update aming convention to be x_from_y rather than y_from_x
daflack Jan 7, 2026
48519be
Update operators in temperature for correct conversion name
daflack Jan 7, 2026
ddaa113
Adds test data and fixtures for humidity convertors
daflack Jan 7, 2026
37dffcb
Adds tests for specific humidity and mixing ratio conversions
daflack Jan 7, 2026
b7b141d
Adds tests for saturation mixing ratio
daflack Jan 8, 2026
40fc813
Adds tests for saturation specific humidity
daflack Jan 8, 2026
2c1779e
Adds tests for mixing ratio from relative humidity
daflack Jan 8, 2026
15512e7
Adds tests for specific humidity from relative humidity
daflack Jan 8, 2026
18e63f3
Adds tests for relative humidity from mixing ratio
daflack Jan 8, 2026
51f0fa5
Update tests for relative humidity from mixing ratio and correct RH
daflack Jan 8, 2026
2232c1c
Adds tests for relative humidity from specific humidity
daflack Jan 8, 2026
b022fdd
Adds tests and fixes calculation for dewpoint temperature
daflack Jan 8, 2026
0293f0f
Adds tests for virtual temperature
daflack Jan 8, 2026
6824afe
Adds tests for wet-bulb temperature conversions
daflack Jan 13, 2026
df57c5e
Adds tests for potenital temperature
daflack Jan 14, 2026
9e15a6f
Adds tests for virtual potential temperature
daflack Jan 14, 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
184 changes: 184 additions & 0 deletions src/CSET/operators/humidity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# © 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 mixing_ratio_from_specific_humidity(
specific_humidity: iris.cube.Cube | iris.cube.CubeList,
) -> iris.cube.Cube | iris.cube.CubeList:
"""Convert specific humidity to mixing ratio."""
w = iris.cube.CubeList([])
for q in iter_maybe(specific_humidity):
mr = q.copy()
mr = q / (1 - q)
mr.rename("mixing_ratio")
w.append(mr)
if len(w) == 1:
return w[0]
else:
return w


def specific_humidity_from_mixing_ratio(
mixing_ratio: iris.cube.Cube | iris.cube.CubeList,
) -> iris.cube.Cube | iris.cube.CubeList:
"""Convert mixing ratio to specific humidity."""
q = iris.cube.CubeList([])
for w in iter_maybe(mixing_ratio):
sh = w.copy()
sh = w / (1 + w)
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):
P = convert_units(P, "hPa")
mr = (EPSILON * vapour_pressure(T)) / (P - vapour_pressure(T))
mr.units = "kg/kg"
mr.rename("saturation_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):
P = convert_units(P, "hPa")
sh = (EPSILON * vapour_pressure(T)) / P
sh.units = "kg/kg"
sh.rename("saturation_specific_humidity")
q.append(sh)
if len(q) == 1:
return q[0]
else:
return q


def mixing_ratio_from_relative_humidity(
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
mr.rename("mixing_ratio")
mr.units = "kg/kg"
w.append(mr)
if len(w) == 1:
return w[0]
else:
return w


def specific_humidity_from_relative_humidity(
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
sh.rename("specific_humidity")
sh.units = "kg/kg"
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")
rel_h = convert_units(rel_h, "%")
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")
rel_h = convert_units(rel_h, "%")
RH.append(rel_h)
if len(RH) == 1:
return RH[0]
else:
return RH
78 changes: 78 additions & 0 deletions src/CSET/operators/pressure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# © 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_relative_humidity(
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
vp.units = "hPa"
vp.rename("vapour_pressure")
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()
P = convert_units(P, "hPa")
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