Skip to content

Commit 4570e13

Browse files
merged develop branch
2 parents 42c3f91 + 68c225d commit 4570e13

16 files changed

+140
-28
lines changed

ci/build_dd_zip.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,9 @@ echo "Done loading modules"
1717
set -x
1818

1919
# Build the DD zip
20+
rm -rf venv # Environment should be clean, but remove directory to be sure
21+
python -m venv venv
22+
source venv/bin/activate
23+
pip install gitpython saxonche packaging
2024
python imas/dd_helpers.py
25+
deactivate

conftest.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,15 @@ def pytest_addoption(parser):
7272
}
7373

7474

75+
try:
76+
import pytest_xdist
77+
except ImportError:
78+
# If pytest-xdist is not available we provide a dummy worker_id fixture.
79+
@pytest.fixture()
80+
def worker_id():
81+
return "master"
82+
83+
7584
@pytest.fixture(params=_BACKENDS)
7685
def backend(pytestconfig: pytest.Config, request: pytest.FixtureRequest):
7786
backends_provided = any(map(pytestconfig.getoption, _BACKENDS))

imas/backends/netcdf/db_entry_nc.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,20 @@ def __init__(self, fname: str, mode: str, factory: IDSFactory) -> None:
3333
"The `netCDF4` python module is not available. Please install this "
3434
"module to read/write IMAS netCDF files with imas-python."
3535
)
36+
# To support netcdf v1.4 (which has no mode "x") we map it to "w" with
37+
# `clobber=True`.
38+
if mode == "x":
39+
mode = "w"
40+
clobber = False
41+
else:
42+
clobber = True
3643

3744
self._dataset = netCDF4.Dataset(
3845
fname,
3946
mode,
4047
format="NETCDF4",
4148
auto_complex=True,
49+
clobber=clobber,
4250
)
4351
"""NetCDF4 dataset."""
4452
self._factory = factory

imas/backends/netcdf/ids2nc.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import netCDF4
99
import numpy
10+
from packaging import version
1011

1112
from imas.backends.netcdf.nc_metadata import NCMetadata
1213
from imas.ids_base import IDSBase
@@ -185,9 +186,20 @@ def create_variables(self) -> None:
185186

186187
else:
187188
dtype = dtypes[metadata.data_type]
189+
if (
190+
version.parse(netCDF4.__version__) < version.parse("1.7.0")
191+
and dtype is dtypes[IDSDataType.CPX]
192+
):
193+
raise InvalidNetCDFEntry(
194+
f"Found complex data in {var_name}, NetCDF 1.7.0 or"
195+
f" later is required for complex data types"
196+
)
188197
kwargs = {}
189198
if dtype is not str: # Enable compression:
190-
kwargs.update(compression="zlib", complevel=1)
199+
if version.parse(netCDF4.__version__) > version.parse("1.4.1"):
200+
kwargs.update(compression="zlib", complevel=1)
201+
else:
202+
kwargs.update(zlib=True, complevel=1)
191203
if dtype is not dtypes[IDSDataType.CPX]: # Set fillvalue
192204
kwargs.update(fill_value=default_fillvals[metadata.data_type])
193205
# Create variable

imas/backends/netcdf/nc_validate.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,24 @@ def validate_netcdf_file(filename: str) -> None:
2323
# additional variables are smuggled inside:
2424
groups = [dataset] + [dataset[group] for group in dataset.groups]
2525
for group in groups:
26+
group_name = group.path.split("/")[-1]
2627
if group.variables or group.dimensions:
2728
raise InvalidNetCDFEntry(
2829
"NetCDF file should not have variables or dimensions in the "
29-
f"{group.name} group."
30+
f"{group_name} group."
3031
)
3132
if group is dataset:
3233
continue
33-
if group.name not in ids_names:
34+
if group_name not in ids_names:
3435
raise InvalidNetCDFEntry(
35-
f"Invalid group name {group.name}: there is no IDS with this name."
36+
f"Invalid group name {group_name}: there is no IDS with this name."
3637
)
3738
for subgroup in group.groups:
3839
try:
3940
int(subgroup)
4041
except ValueError:
4142
raise InvalidNetCDFEntry(
42-
f"Invalid group name {group.name}/{subgroup}: "
43+
f"Invalid group name {group_name}/{subgroup}: "
4344
f"{subgroup} is not a valid occurrence number."
4445
)
4546

imas/ids_primitive.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,10 @@ def _cast_value(self, value):
481481
value = np.asanyarray(value)
482482
if value.dtype != dtype:
483483
logger.info(_CONVERT_MSG, value.dtype, self)
484-
value = np.array(value, dtype=dtype, copy=False)
484+
value = np.asarray(
485+
value,
486+
dtype=dtype,
487+
)
485488
if value.ndim != self.metadata.ndim:
486489
raise ValueError(f"Trying to assign a {value.ndim}D value to {self!r}.")
487490
return value

imas/test/test_cli.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from click.testing import CliRunner
55
from packaging.version import Version
66

7+
from imas.backends.imas_core.imas_interface import has_imas
78
from imas.backends.imas_core.imas_interface import ll_interface
89
from imas.command.cli import print_version
910
from imas.command.db_analysis import analyze_db, process_db_analysis
@@ -19,8 +20,13 @@ def test_imas_version():
1920

2021

2122
@pytest.mark.cli
22-
@pytest.mark.skipif(ll_interface._al_version < Version("5.0"), reason="Needs AL >= 5")
23-
def test_db_analysis(tmp_path):
23+
@pytest.mark.skipif(
24+
not has_imas or ll_interface._al_version < Version("5.0"),
25+
reason="Needs AL >= 5 AND Requires IMAS Core.",
26+
)
27+
def test_db_analysis(
28+
tmp_path,
29+
):
2430
# This only tests the happy flow, error handling is not tested
2531
db_path = tmp_path / "test_db_analysis"
2632
with DBEntry(f"imas:hdf5?path={db_path}", "w") as entry:

imas/test/test_dbentry.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def test_dbentry_constructor():
8282
assert get_entry_attrs(entry) == (1, 2, 3, 4, None, 6)
8383

8484

85-
def test_ignore_unknown_dd_version(monkeypatch, worker_id, tmp_path):
85+
def test_ignore_unknown_dd_version(monkeypatch, worker_id, tmp_path, requires_imas):
8686
entry = open_dbentry(imas.ids_defs.MEMORY_BACKEND, "w", worker_id, tmp_path)
8787
ids = entry.factory.core_profiles()
8888
ids.ids_properties.homogeneous_time = 0

imas/test/test_helpers.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,9 @@ def fill_with_random_data(structure, max_children=3):
9393
child.value = random_data(child.metadata.data_type, child.metadata.ndim)
9494

9595

96-
def maybe_set_random_value(primitive: IDSPrimitive, leave_empty: float) -> None:
96+
def maybe_set_random_value(
97+
primitive: IDSPrimitive, leave_empty: float, skip_complex: bool
98+
) -> None:
9799
"""Set the value of an IDS primitive with a certain chance.
98100
99101
If the IDSPrimitive has coordinates, then the size of the coordinates is taken into
@@ -153,7 +155,7 @@ def maybe_set_random_value(primitive: IDSPrimitive, leave_empty: float) -> None:
153155
# Scale chance of not setting a coordinate by our number of dimensions,
154156
# such that overall there is roughly a 50% chance that any coordinate
155157
# remains empty
156-
maybe_set_random_value(coordinate_element, 0.5**ndim)
158+
maybe_set_random_value(coordinate_element, 0.5**ndim, skip_complex)
157159
size = coordinate_element.shape[0 if coordinate.references else dim]
158160

159161
if coordinate.size: # coordinateX = <path> OR 1...1
@@ -176,13 +178,18 @@ def maybe_set_random_value(primitive: IDSPrimitive, leave_empty: float) -> None:
176178
elif primitive.metadata.data_type is IDSDataType.FLT:
177179
primitive.value = np.random.random_sample(size=shape)
178180
elif primitive.metadata.data_type is IDSDataType.CPX:
181+
if skip_complex:
182+
# If we are skipping complex numbers then leave the value empty.
183+
return
179184
val = np.random.random_sample(shape) + 1j * np.random.random_sample(shape)
180185
primitive.value = val
181186
else:
182187
raise ValueError(f"Invalid IDS data type: {primitive.metadata.data_type}")
183188

184189

185-
def fill_consistent(structure: IDSStructure, leave_empty: float = 0.2):
190+
def fill_consistent(
191+
structure: IDSStructure, leave_empty: float = 0.2, skip_complex: bool = False
192+
):
186193
"""Fill a structure with random data, such that coordinate sizes are consistent.
187194
188195
Sets homogeneous_time to heterogeneous (always).
@@ -196,6 +203,9 @@ def fill_consistent(structure: IDSStructure, leave_empty: float = 0.2):
196203
exclusive_coordinates: list of IDSPrimitives that have exclusive alternative
197204
coordinates. These are initially not filled, and only at the very end of
198205
filling an IDSToplevel, a choice is made between the exclusive coordinates.
206+
skip_complex: Whether to skip over populating complex numbers. This is
207+
useful for maintaining compatibility with older versions of netCDF4
208+
(<1.7.0) where complex numbers are not supported.
199209
"""
200210
if isinstance(structure, IDSToplevel):
201211
unsupported_ids_name = (
@@ -218,7 +228,9 @@ def fill_consistent(structure: IDSStructure, leave_empty: float = 0.2):
218228

219229
for child in structure:
220230
if isinstance(child, IDSStructure):
221-
exclusive_coordinates.extend(fill_consistent(child, leave_empty))
231+
exclusive_coordinates.extend(
232+
fill_consistent(child, leave_empty, skip_complex)
233+
)
222234

223235
elif isinstance(child, IDSStructArray):
224236
if child.metadata.coordinates[0].references:
@@ -230,7 +242,7 @@ def fill_consistent(structure: IDSStructure, leave_empty: float = 0.2):
230242
if isinstance(coor, IDSPrimitive):
231243
# maybe fill with random data:
232244
try:
233-
maybe_set_random_value(coor, leave_empty)
245+
maybe_set_random_value(coor, leave_empty, skip_complex)
234246
except (RuntimeError, ValueError):
235247
pass
236248
child.resize(len(coor))
@@ -244,7 +256,9 @@ def fill_consistent(structure: IDSStructure, leave_empty: float = 0.2):
244256
else:
245257
child.resize(child.metadata.coordinates[0].size or 1)
246258
for ele in child:
247-
exclusive_coordinates.extend(fill_consistent(ele, leave_empty))
259+
exclusive_coordinates.extend(
260+
fill_consistent(ele, leave_empty, skip_complex)
261+
)
248262

249263
else: # IDSPrimitive
250264
coordinates = child.metadata.coordinates
@@ -256,7 +270,7 @@ def fill_consistent(structure: IDSStructure, leave_empty: float = 0.2):
256270
exclusive_coordinates.append(child)
257271
else:
258272
try:
259-
maybe_set_random_value(child, leave_empty)
273+
maybe_set_random_value(child, leave_empty, skip_complex)
260274
except (RuntimeError, ValueError):
261275
pass
262276

@@ -278,7 +292,7 @@ def fill_consistent(structure: IDSStructure, leave_empty: float = 0.2):
278292
coor = filled_refs.pop()
279293
unset_coordinate(coor)
280294

281-
maybe_set_random_value(element, leave_empty)
295+
maybe_set_random_value(element, leave_empty, skip_complex)
282296
else:
283297
return exclusive_coordinates
284298

imas/test/test_ids_toplevel.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def test_pretty_print(ids):
4646
assert pprint.pformat(ids) == "<IDSToplevel (IDS:gyrokinetics)>"
4747

4848

49-
def test_serialize_nondefault_dd_version():
49+
def test_serialize_nondefault_dd_version(requires_imas):
5050
ids = IDSFactory("3.31.0").core_profiles()
5151
fill_with_random_data(ids)
5252
data = ids.serialize()

0 commit comments

Comments
 (0)