Source code for ugants.io.load
# (C) Crown Copyright, Met Office. All rights reserved.
#
# This file is part of UG-ANTS and is released under the BSD 3-Clause license.
# See LICENSE.txt in the root of the repository for full licensing details.
"""Load functions for UG-ANTS."""
from collections.abc import Iterable
from itertools import chain
from pathlib import Path
import iris
import iris.cube
import iris.exceptions
from iris.experimental.ugrid import PARSE_UGRID_ON_LOAD, Mesh, load_meshes
from ugants.utils.cube import is_ugrid
[docs]
def ugrid(uris, constraints=None) -> iris.cube.CubeList:
"""Load a UGrid file. Will not load if the file contains regular data.
Parameters
----------
uris : Any
Location of file to load. Must be a NetCDF file.
constraints: Any | None
One or more iris constraints. Each constraint can be either a string,
or an instance of :class:`iris.Constraint`. If the constraint is a string
it will be used to match against cube.name().
Returns
-------
iris.cube.CubeList
An iris :class:`iris.cube.CubeList` containing the loaded data.
Raises
------
iris.exceptions.InvalidCubeError
If the specified file does not contain UGrid data.
iris.exceptions.InvalidCubeError
If no data can be found which matches the provided constraint(s).
iris.exceptions.InvalidCubeError
If a mesh has been removed from a cube during constrained load.
This may be caused by constraining on an unstructured dimension.
"""
with PARSE_UGRID_ON_LOAD.context():
ugrid_cubelist = iris.load(
uris, constraints=constraints, callback=_check_for_non_ugrid
)
if len(ugrid_cubelist) == 0:
raise iris.exceptions.InvalidCubeError(
f"No data found in file(s) '{uris}' matching the "
f"provided constraint(s) '{constraints}'."
)
# By constraining on a horizontal (unstructured) dimension, iris attempts to
# subset a MeshCoord. This causes the MeshCoords to be converted to AuxCoords,
# so the resulting cube has no mesh. This will result in non-ugrid data.
if not all(is_ugrid(cube) for cube in ugrid_cubelist):
raise iris.exceptions.InvalidCubeError(
f"Attempting to load UGrid data from '{uris}' with constraint(s) "
f"'{constraints}' has resulted in non-UGrid data being loaded. "
"This may be caused by constraining on an unstructured dimension."
)
return ugrid_cubelist
def _check_for_non_ugrid(cube: iris.cube.Cube, field, filename):
"""Check if the loaded data is UGrid or not.
A function to be passed to callback in iris.load.
Parameters
----------
cube : iris.cube.Cube
Loaded :class:`iris.cube.CubeList` to be checked
field : Any
Only present to satisfy the function signature used by callback.
filename : Any
Original file from which the cube data was loaded
Raises
------
ValueError
If cube does not contain UGrid data
"""
if not is_ugrid(cube):
raise iris.exceptions.InvalidCubeError(
f"Specified file '{filename}' does not contain UGrid data."
)
[docs]
def cf(uris, **kwargs):
"""Load one or more regular data input files.
Fails if files are identified as being UGRID.
Parameters
----------
uris : str or :class:`pathlib.Path`
A string or Path object to a file containing regular data.
Can also be a list of files.
Returns
-------
cubes : iris.cube.CubeList
An iris :class:`iris.cube.CubeList`.
Raises
------
ValueError
Raised if any input uris are not netCDF files.
"""
is_netcdf(uris)
cubes = iris.load(uris, **kwargs)
for cube in cubes:
error_if_mesh(cube, uris)
return cubes
[docs]
def mesh(
uri: str | Path, mesh_name: str | None = None
) -> iris.experimental.ugrid.mesh.Mesh:
"""Load a single mesh from a uri.
If no mesh_name is given, the mesh_name is inferred from the file itself,
provided there is only one mesh in the file.
Parameters
----------
uri : str | :class:`pathlib.Path`
A string or Path to a netCDF file containing the mesh.
mesh_name : str | None
The name of the mesh to load.
Returns
-------
iris.experimental.ugrid.mesh.Mesh
An iris Mesh as provided by :func:iris.experimental.ugrid.load_mesh
Raises
------
ValueError
If multiple meshes are present.
If no meshes are present.
"""
mesh_list = meshes(str(uri), mesh_name)
if len(mesh_list) > 1:
mesh_names = [mesh.var_name for mesh in mesh_list]
raise ValueError(f"Expected one mesh, found {len(mesh_list)}. {mesh_names}")
return mesh_list[0]
[docs]
def meshes(uris: str | Iterable[str], mesh_name: str | None = None) -> list[Mesh]:
"""Load multiple meshes from netCDF file(s).
Parameters
----------
uris
Filepath(s) from which to load meshes.
mesh_name
The ``var_name`` of the meshes to load.
If None (the default), all meshes are loaded.
Returns
-------
list[~iris.experimental.ugrid.Mesh]
A list of meshes loaded from the ``uris`` with ``mesh_name``, if provided.
Raises
------
ValueError
If no meshes are present at the given ``uris`` matching the
``mesh_name`` (if provided).
"""
is_netcdf(uris)
with PARSE_UGRID_ON_LOAD.context():
mesh_dict = load_meshes(uris, mesh_name)
error_message = f"No meshes were found at {uris}"
if mesh_name:
error_message = f"{error_message} with var_name '{mesh_name}'"
if not mesh_dict:
raise ValueError(error_message)
# Iris' load_meshes returns a dict mapping uri to a list of loaded meshes
# We will extract just the lists
return list(chain(*mesh_dict.values()))
[docs]
def is_netcdf(uris):
"""Check uris point to netCDF files.
Parameters
----------
uris : str or :class:`pathlib.Path`
A string or Path object to a file containing regular data.
Can also be a list of files.
Raises
------
ValueError
Raised if any input uris are not netCDF files.
"""
if isinstance(uris, str | Path):
uris = [uris]
uris = [Path(uri) for uri in uris]
for uri in uris:
if not uri.suffix == ".nc":
message = f"Input file '{uri}' must be netCDF."
raise ValueError(message)
[docs]
def error_if_mesh(cube, filename):
"""Perform basic sanity checks to prevent accidently loading UGRID data.
A callback function for use with iris.load.
Parameters
----------
cube : :class:`iris.cube.Cube`
The iris cube to perform checking on.
filename : Any
Original file used to generate the cube.
Raises
------
ValueError
If cf_role is defined and is 'mesh_topology'
"""
if hasattr(cube, "attributes"):
cf_role = cube.attributes.get("cf_role", None)
if cf_role == "mesh_topology":
message = (
f"Cannot load input file '{filename!s}' as 'cf_role' is '{cf_role}'"
)
raise ValueError(message)