Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NetCDF thread safety for v3.4.1 patch release #5169

Merged
merged 8 commits into from
Feb 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 5 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ minimum_pre_commit_version: 1.21.0

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
rev: v4.4.0
hooks:
# Prevent giant files from being committed.
- id: check-added-large-files
Expand All @@ -29,32 +29,31 @@ repos:
- id: no-commit-to-branch

- repo: https://github.com/psf/black
rev: 22.10.0
rev: 22.12.0
hooks:
- id: black
pass_filenames: false
args: [--config=./pyproject.toml, .]

- repo: https://github.com/PyCQA/flake8
rev: 5.0.4
rev: 6.0.0
hooks:
- id: flake8
types: [file, python]
args: [--config=./setup.cfg]

- repo: https://github.com/pycqa/isort
rev: 5.10.1
rev: 5.12.0
hooks:
- id: isort
types: [file, python]
args: [--filter-files]

- repo: https://github.com/asottile/blacken-docs
rev: v1.12.1
rev: 1.13.0
hooks:
- id: blacken-docs
types: [file, rst]
additional_dependencies: [black==21.6b0]

- repo: https://github.com/aio-libs/sort-all
rev: v1.2.0
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/benchmarks/experimental/ugrid/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def time_create(self, *params):

class Connectivity(UGridCommon):
def setup(self, n_faces):
self.array = np.zeros([n_faces, 3], dtype=np.int)
self.array = np.zeros([n_faces, 3], dtype=int)
super().setup(n_faces)

def create(self):
Expand Down
2 changes: 1 addition & 1 deletion docs/gallery_code/oceanography/plot_atlantic_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def main():
# the southern portion of the domain, and limit the depth of the profile
# to 1000m.
lon_cons = iris.Constraint(longitude=330.5)
lat_cons = iris.Constraint(latitude=lambda l: -10 < l < -9)
lat_cons = iris.Constraint(latitude=lambda lat: -10 < lat < -9)
depth_cons = iris.Constraint(depth=lambda d: d <= 1000)
theta_1000m = theta.extract(depth_cons & lon_cons & lat_cons)
salinity_1000m = salinity.extract(depth_cons & lon_cons & lat_cons)
Expand Down
1 change: 1 addition & 0 deletions docs/src/common_links.inc
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
.. _CF-UGRID: https://ugrid-conventions.github.io/ugrid-conventions/
.. _issues on GitHub: https://github.com/SciTools/iris/issues?q=is%3Aopen+is%3Aissue+sort%3Areactions-%2B1-desc
.. _python-stratify: https://github.com/SciTools/python-stratify
.. _netCDF4: https://github.com/Unidata/netcdf4-python


.. comment
Expand Down
6 changes: 3 additions & 3 deletions docs/src/further_topics/metadata.rst
Original file line number Diff line number Diff line change
Expand Up @@ -389,10 +389,10 @@ instances. Normally, this would cause issues. For example,

.. doctest:: richer-metadata

>>> simply = {"one": np.int(1), "two": np.array([1.0, 2.0])}
>>> simply = {"one": np.int32(1), "two": np.array([1.0, 2.0])}
>>> simply
{'one': 1, 'two': array([1., 2.])}
>>> fruity = {"one": np.int(1), "two": np.array([1.0, 2.0])}
>>> fruity = {"one": np.int32(1), "two": np.array([1.0, 2.0])}
>>> fruity
{'one': 1, 'two': array([1., 2.])}
>>> simply == fruity
Expand All @@ -419,7 +419,7 @@ However, metadata class equality is rich enough to handle this eventuality,

>>> metadata1
CubeMetadata(standard_name='air_temperature', long_name=None, var_name='air_temperature', units=Unit('K'), attributes={'one': 1, 'two': array([1., 2.])}, cell_methods=(CellMethod(method='mean', coord_names=('time',), intervals=('6 hour',), comments=()),))
>>> metadata2 = cube.metadata._replace(attributes={"one": np.int(1), "two": np.array([1000.0, 2000.0])})
>>> metadata2 = cube.metadata._replace(attributes={"one": np.int32(1), "two": np.array([1000.0, 2000.0])})
>>> metadata2
CubeMetadata(standard_name='air_temperature', long_name=None, var_name='air_temperature', units=Unit('K'), attributes={'one': 1, 'two': array([1000., 2000.])}, cell_methods=(CellMethod(method='mean', coord_names=('time',), intervals=('6 hour',), comments=()),))
>>> metadata1 == metadata2
Expand Down
32 changes: 16 additions & 16 deletions docs/src/userguide/cube_maths.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ Cube Maths
==========


The section :doc:`navigating_a_cube` highlighted that
every cube has a data attribute;
The section :doc:`navigating_a_cube` highlighted that
every cube has a data attribute;
this attribute can then be manipulated directly::

cube.data -= 273.15
Expand Down Expand Up @@ -37,7 +37,7 @@ Let's load some air temperature which runs from 1860 to 2100::
filename = iris.sample_data_path('E1_north_america.nc')
air_temp = iris.load_cube(filename, 'air_temperature')

We can now get the first and last time slices using indexing
We can now get the first and last time slices using indexing
(see :ref:`cube_indexing` for a reminder)::

t_first = air_temp[0, :, :]
Expand All @@ -50,8 +50,8 @@ We can now get the first and last time slices using indexing
t_first = air_temp[0, :, :]
t_last = air_temp[-1, :, :]

And finally we can subtract the two.
The result is a cube of the same size as the original two time slices,
And finally we can subtract the two.
The result is a cube of the same size as the original two time slices,
but with the data representing their difference:

>>> print(t_last - t_first)
Expand All @@ -70,8 +70,8 @@ but with the data representing their difference:

.. note::

Notice that the coordinates "time" and "forecast_period" have been removed
from the resultant cube;
Notice that the coordinates "time" and "forecast_period" have been removed
from the resultant cube;
this is because these coordinates differed between the two input cubes.


Expand Down Expand Up @@ -174,15 +174,15 @@ broadcasting behaviour::
Combining Multiple Phenomena to Form a New One
----------------------------------------------

Combining cubes of potential-temperature and pressure we can calculate
Combining cubes of potential-temperature and pressure we can calculate
the associated temperature using the equation:

.. math::

T = \theta (\frac{p}{p_0}) ^ {(287.05 / 1005)}

Where :math:`p` is pressure, :math:`\theta` is potential temperature,
:math:`p_0` is the potential temperature reference pressure
Where :math:`p` is pressure, :math:`\theta` is potential temperature,
:math:`p_0` is the potential temperature reference pressure
and :math:`T` is temperature.

First, let's load pressure and potential temperature cubes::
Expand All @@ -191,7 +191,7 @@ First, let's load pressure and potential temperature cubes::
phenomenon_names = ['air_potential_temperature', 'air_pressure']
pot_temperature, pressure = iris.load_cubes(filename, phenomenon_names)

In order to calculate :math:`\frac{p}{p_0}` we can define a coordinate which
In order to calculate :math:`\frac{p}{p_0}` we can define a coordinate which
represents the standard reference pressure of 1000 hPa::

import iris.coords
Expand All @@ -205,7 +205,7 @@ the :meth:`iris.coords.Coord.convert_units` method::

p0.convert_units(pressure.units)

Now we can combine all of this information to calculate the air temperature
Now we can combine all of this information to calculate the air temperature
using the equation above::

temperature = pot_temperature * ( (pressure / p0) ** (287.05 / 1005) )
Expand All @@ -219,12 +219,12 @@ The result could now be plotted using the guidance provided in the

.. only:: html

A very similar example to this can be found in
A very similar example to this can be found in
:ref:`sphx_glr_generated_gallery_meteorology_plot_deriving_phenomena.py`.

.. only:: latex

A very similar example to this can be found in the examples section,
A very similar example to this can be found in the examples section,
with the title "Deriving Exner Pressure and Air Temperature".

.. _cube_maths_combining_units:
Expand All @@ -249,7 +249,7 @@ unit (if ``a`` had units ``'m2'`` then ``a ** 0.5`` would result in a cube
with units ``'m'``).

Iris inherits units from `cf_units <https://scitools.org.uk/cf-units/docs/latest/>`_
which in turn inherits from `UDUNITS <https://www.unidata.ucar.edu/software/udunits/udunits-current/udunits2.html>`_.
which in turn inherits from `UDUNITS <https://docs.unidata.ucar.edu/udunits/current/>`_.
As well as the units UDUNITS provides, cf units also provides the units
``'no-unit'`` and ``'unknown'``. A unit of ``'no-unit'`` means that the
associated data is not suitable for describing with a unit, cf units
Expand Down
8 changes: 6 additions & 2 deletions docs/src/userguide/glossary.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.. include:: ../common_links.inc

.. _glossary:

Glossary
Expand Down Expand Up @@ -125,7 +127,7 @@ Glossary
of formats.

| **Related:** :term:`CartoPy` **|** :term:`NumPy`
| **More information:** `Matplotlib <https://scitools.org.uk/cartopy/docs/latest/>`_
| **More information:** `matplotlib`_
|

Metadata
Expand All @@ -143,9 +145,11 @@ Glossary
When Iris loads this format, it also especially recognises and interprets data
encoded according to the :term:`CF Conventions`.

__ `NetCDF4`_

| **Related:** :term:`Fields File (FF) Format`
**|** :term:`GRIB Format` **|** :term:`Post Processing (PP) Format`
| **More information:** `NetCDF-4 Python Git <https://github.com/Unidata/netcdf4-python>`_
| **More information:** `NetCDF-4 Python Git`__
|

NumPy
Expand Down
8 changes: 6 additions & 2 deletions docs/src/whatsnew/2.1.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.. include:: ../common_links.inc

v2.1 (06 Jun 2018)
******************

Expand Down Expand Up @@ -67,7 +69,7 @@ Incompatible Changes
as an alternative.

* This release of Iris contains a number of updated metadata translations.
See this
See this
`changelist <https://github.com/SciTools/iris/commit/69597eb3d8501ff16ee3d56aef1f7b8f1c2bb316#diff-1680206bdc5cfaa83e14428f5ba0f848>`_
for further information.

Expand All @@ -84,14 +86,16 @@ Internal
calendar.

* Iris updated its time-handling functionality from the
`netcdf4-python <http://unidata.github.io/netcdf4-python/>`_
`netcdf4-python`__
``netcdftime`` implementation to the standalone module
`cftime <https://github.com/Unidata/cftime>`_.
cftime is entirely compatible with netcdftime, but some issues may
occur where users are constructing their own datetime objects.
In this situation, simply replacing ``netcdftime.datetime`` with
``cftime.datetime`` should be sufficient.

__ `netCDF4`_

* Iris now requires version 2 of Matplotlib, and ``>=1.14`` of NumPy.
Full requirements can be seen in the `requirements <https://github.com/SciTools/iris/>`_
directory of the Iris' the source.
22 changes: 18 additions & 4 deletions docs/src/whatsnew/3.4.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,29 @@ This document explains the changes made to Iris for this release
* We have **begun refactoring Iris' regridding**, which has already improved
performance and functionality, with more potential in future!
* We have made several other significant `🚀 Performance Enhancements`_.
* Please note that **Iris cannot currently work with the latest NetCDF4
releases**. The pin is set to ``<v1.6.1``, due to incompatibility with
Iris' lack of thread safety. We're working hard to make Iris NetCDF
loading thread safe as soon as possible.

And finally, get in touch with us on :issue:`GitHub<new/choose>` if you have
any issues or feature requests for improving Iris. Enjoy!


v3.4.1 (21 Feb 2023)
====================

.. dropdown:: :opticon:`alert` v3.4.1 Patches
:container: + shadow
:title: text-primary text-center font-weight-bold
:body: bg-light
:animate: fade-in

The patches in this release of Iris include:

#. `@trexfeathers`_ and `@pp-mo`_ made Iris' use of the `netCDF4`_ library
thread-safe. (:pull:`5095`)

#. `@trexfeathers`_ and `@pp-mo`_ removed the netCDF4 pin mentioned in
`🔗 Dependencies`_ point 3. (:pull:`5095`)


📢 Announcements
================

Expand Down
3 changes: 2 additions & 1 deletion lib/iris/experimental/ugrid/load.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,8 @@ def load_meshes(uris, var_name=None):

result = {}
for source in valid_sources:
meshes_dict = _meshes_from_cf(CFUGridReader(source))
with CFUGridReader(source) as cf_reader:
meshes_dict = _meshes_from_cf(cf_reader)
meshes = list(meshes_dict.values())
if var_name is not None:
meshes = list(filter(lambda m: m.var_name == var_name, meshes))
Expand Down
26 changes: 23 additions & 3 deletions lib/iris/fileformats/cf.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
import re
import warnings

import netCDF4
import numpy as np
import numpy.ma as ma

from iris.fileformats.netcdf import _thread_safe_nc
import iris.util

#
Expand Down Expand Up @@ -1050,7 +1050,9 @@ def __init__(self, filename, warn=False, monotonic=False):
#: Collection of CF-netCDF variables associated with this netCDF file
self.cf_group = self.CFGroup()

self._dataset = netCDF4.Dataset(self._filename, mode="r")
self._dataset = _thread_safe_nc.DatasetWrapper(
self._filename, mode="r"
)

# Issue load optimisation warning.
if warn and self._dataset.file_format in [
Expand All @@ -1068,6 +1070,19 @@ def __init__(self, filename, warn=False, monotonic=False):
self._build_cf_groups()
self._reset()

def __enter__(self):
# Enable use as a context manager
# N.B. this **guarantees* closure of the file, when the context is exited.
# Note: ideally, the class would not do so much work in the __init__ call, and
# would do all that here, after acquiring necessary permissions/locks.
# But for legacy reasons, we can't do that. So **effectively**, the context
# (in terms of access control) alreday started, when we created the object.
return self

def __exit__(self, exc_type, exc_value, traceback):
# When used as a context-manager, **always** close the file on exit.
self._close()

@property
def filename(self):
"""The file that the CFReader is reading."""
Expand Down Expand Up @@ -1294,10 +1309,15 @@ def _reset(self):
for nc_var_name in self._dataset.variables.keys():
self.cf_group[nc_var_name].cf_attrs_reset()

def __del__(self):
def _close(self):
# Explicitly close dataset to prevent file remaining open.
if self._dataset is not None:
self._dataset.close()
self._dataset = None

def __del__(self):
# Be sure to close dataset when CFReader is destroyed / garbage-collected.
self._close()


def _getncattr(dataset, attr, default=None):
Expand Down
Loading