imas-python 0.1.dev1618__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of imas-python might be problematic. Click here for more details.

Files changed (108) hide show
  1. imas/__init__.py +34 -0
  2. imas/__main__.py +17 -0
  3. imas/_util.py +260 -0
  4. imas/_version.py +16 -0
  5. imas/assets/IDSDef2MDSpreTree.xsl +161 -0
  6. imas/assets/IDS_fake_toplevel.xml +318 -0
  7. imas/assets/IDS_minimal.xml +37 -0
  8. imas/assets/IDS_minimal_2.xml +37 -0
  9. imas/assets/IDS_minimal_struct_array.xml +43 -0
  10. imas/assets/IDS_minimal_types.xml +169 -0
  11. imas/assets/ITER_134173_106_core_profiles.ids +2438 -0
  12. imas/assets/ITER_134173_106_equilibrium.ids +3172 -0
  13. imas/assets/README.md +5 -0
  14. imas/assets/core_profiles.ids +2438 -0
  15. imas/assets/equilibrium.ids +3172 -0
  16. imas/backends/__init__.py +12 -0
  17. imas/backends/db_entry_impl.py +122 -0
  18. imas/backends/imas_core/__init__.py +4 -0
  19. imas/backends/imas_core/al_context.py +391 -0
  20. imas/backends/imas_core/db_entry_al.py +405 -0
  21. imas/backends/imas_core/db_entry_helpers.py +200 -0
  22. imas/backends/imas_core/imas_interface.py +216 -0
  23. imas/backends/imas_core/mdsplus_model.py +367 -0
  24. imas/backends/imas_core/uda_support.py +56 -0
  25. imas/backends/netcdf/__init__.py +4 -0
  26. imas/backends/netcdf/db_entry_nc.py +176 -0
  27. imas/backends/netcdf/ids2nc.py +306 -0
  28. imas/backends/netcdf/nc2ids.py +302 -0
  29. imas/backends/netcdf/nc_metadata.py +357 -0
  30. imas/backends/netcdf/nc_validate.py +53 -0
  31. imas/command/cli.py +239 -0
  32. imas/command/db_analysis.py +290 -0
  33. imas/command/helpers.py +45 -0
  34. imas/command/timer.py +77 -0
  35. imas/db_entry.py +802 -0
  36. imas/dd_helpers.py +300 -0
  37. imas/dd_zip.py +297 -0
  38. imas/exception.py +107 -0
  39. imas/ids_base.py +149 -0
  40. imas/ids_convert.py +920 -0
  41. imas/ids_coordinates.py +400 -0
  42. imas/ids_data_type.py +115 -0
  43. imas/ids_defs.py +171 -0
  44. imas/ids_factory.py +108 -0
  45. imas/ids_identifiers.py +125 -0
  46. imas/ids_metadata.py +307 -0
  47. imas/ids_path.py +254 -0
  48. imas/ids_primitive.py +510 -0
  49. imas/ids_struct_array.py +233 -0
  50. imas/ids_structure.py +331 -0
  51. imas/ids_toplevel.py +381 -0
  52. imas/setup_logging.py +64 -0
  53. imas/test/test_all_dd_versions.py +28 -0
  54. imas/test/test_cli.py +44 -0
  55. imas/test/test_dbentry.py +104 -0
  56. imas/test/test_dd_helpers.py +134 -0
  57. imas/test/test_dd_zip.py +29 -0
  58. imas/test/test_deepcopy.py +25 -0
  59. imas/test/test_exception.py +12 -0
  60. imas/test/test_get_sample.py +388 -0
  61. imas/test/test_hash.py +188 -0
  62. imas/test/test_helpers.py +354 -0
  63. imas/test/test_identifiers.py +105 -0
  64. imas/test/test_ids2nc.py +166 -0
  65. imas/test/test_ids_ascii_data.py +39 -0
  66. imas/test/test_ids_convert.py +415 -0
  67. imas/test/test_ids_coordinates.py +165 -0
  68. imas/test/test_ids_data_type.py +40 -0
  69. imas/test/test_ids_factory.py +35 -0
  70. imas/test/test_ids_metadata.py +66 -0
  71. imas/test/test_ids_mixin.py +20 -0
  72. imas/test/test_ids_path.py +192 -0
  73. imas/test/test_ids_primitive.py +109 -0
  74. imas/test/test_ids_struct_array.py +89 -0
  75. imas/test/test_ids_structure.py +70 -0
  76. imas/test/test_ids_toplevel.py +55 -0
  77. imas/test/test_ids_validate.py +343 -0
  78. imas/test/test_latest_dd_autofill.py +74 -0
  79. imas/test/test_latest_dd_manual.py +38 -0
  80. imas/test/test_latest_dd_resample.py +102 -0
  81. imas/test/test_lazy_loading.py +201 -0
  82. imas/test/test_list_occurrences.py +82 -0
  83. imas/test/test_minimal.py +37 -0
  84. imas/test/test_minimal_conversion.py +44 -0
  85. imas/test/test_minimal_io.py +21 -0
  86. imas/test/test_minimal_struct_array_io.py +60 -0
  87. imas/test/test_minimal_types.py +436 -0
  88. imas/test/test_minimal_types_io.py +70 -0
  89. imas/test/test_minimal_types_io_automatic.py +66 -0
  90. imas/test/test_nbc_change.py +408 -0
  91. imas/test/test_nc_autofill.py +13 -0
  92. imas/test/test_nc_entry.py +28 -0
  93. imas/test/test_nc_metadata.py +153 -0
  94. imas/test/test_nc_validation.py +197 -0
  95. imas/test/test_snippets.py +25 -0
  96. imas/test/test_static_ids.py +39 -0
  97. imas/test/test_str_1d.py +99 -0
  98. imas/test/test_time_slicing.py +182 -0
  99. imas/test/test_uda_support.py +12 -0
  100. imas/test/test_util.py +209 -0
  101. imas/training.py +45 -0
  102. imas/util.py +526 -0
  103. imas_python-0.1.dev1618.dist-info/LICENSE.txt +165 -0
  104. imas_python-0.1.dev1618.dist-info/METADATA +288 -0
  105. imas_python-0.1.dev1618.dist-info/RECORD +108 -0
  106. imas_python-0.1.dev1618.dist-info/WHEEL +5 -0
  107. imas_python-0.1.dev1618.dist-info/entry_points.txt +3 -0
  108. imas_python-0.1.dev1618.dist-info/top_level.txt +1 -0
@@ -0,0 +1,166 @@
1
+ import netCDF4
2
+ import numpy
3
+ import pytest
4
+
5
+ from imas.backends.netcdf.ids2nc import IDS2NC, default_fillvals
6
+ from imas.ids_data_type import IDSDataType
7
+ from imas.ids_defs import IDS_TIME_MODE_HOMOGENEOUS
8
+ from imas.ids_factory import IDSFactory
9
+
10
+
11
+ @pytest.fixture
12
+ def group(tmp_path):
13
+ with netCDF4.Dataset(tmp_path / "test.nc", "w") as group:
14
+ yield group
15
+
16
+
17
+ def test_tensorization(group):
18
+ ids = IDSFactory("3.39.0").core_profiles()
19
+
20
+ ids.ids_properties.homogeneous_time = IDS_TIME_MODE_HOMOGENEOUS
21
+ ids.time = [1.0, 2.0, 3.0]
22
+ ids.profiles_1d.resize(3)
23
+ for p1d in ids.profiles_1d:
24
+ p1d.ion.resize(2)
25
+ p1d.ion[0].label = "D"
26
+ p1d.ion[0].z_ion = 1.0
27
+ p1d.ion[0].element.resize(1)
28
+ p1d.ion[0].element[0].a = 2.0
29
+ p1d.ion[0].element[0].z_n = 1.0
30
+ p1d.ion[0].element[0].atoms_n = 1
31
+
32
+ p1d.ion[1].label = "OH-"
33
+ p1d.ion[1].z_ion = -1.0
34
+ p1d.ion[1].element.resize(2)
35
+ p1d.ion[1].element[0].a = 1.0
36
+ p1d.ion[1].element[0].z_n = 1.0
37
+ p1d.ion[1].element[0].atoms_n = 1
38
+ p1d.ion[1].element[1].a = 16.0
39
+ p1d.ion[1].element[1].z_n = 8.0
40
+ p1d.ion[1].element[1].atoms_n = 1
41
+
42
+ IDS2NC(ids, group).run()
43
+ # Test tensorized values
44
+ expected = [["D", "OH-"]] * 3
45
+ assert numpy.array_equal(group["profiles_1d.ion.label"], expected)
46
+ assert not hasattr(group["profiles_1d.ion.label"], "sparse")
47
+
48
+ expected = [[1.0, -1.0]] * 3
49
+ assert numpy.array_equal(group["profiles_1d.ion.z_ion"], expected)
50
+ assert not hasattr(group["profiles_1d.ion.z_ion"], "sparse")
51
+
52
+ expected = [[[2.0, netCDF4.default_fillvals["f8"]], [1.0, 16.0]]] * 3
53
+ assert numpy.array_equal(group["profiles_1d.ion.element.a"], expected)
54
+ assert hasattr(group["profiles_1d.ion.element.a"], "sparse")
55
+
56
+ expected = [[[1.0, netCDF4.default_fillvals["f8"]], [1.0, 8.0]]] * 3
57
+ assert numpy.array_equal(group["profiles_1d.ion.element.z_n"], expected)
58
+ assert hasattr(group["profiles_1d.ion.element.z_n"], "sparse")
59
+
60
+ expected = [[[1, netCDF4.default_fillvals["i4"]], [1, 1]]] * 3
61
+ assert numpy.array_equal(group["profiles_1d.ion.element.atoms_n"], expected)
62
+ assert hasattr(group["profiles_1d.ion.element.atoms_n"], "sparse")
63
+
64
+ # Test :shape arrays
65
+ assert "profiles_1d:shape" not in group.variables
66
+ assert not hasattr(group["profiles_1d"], "sparse")
67
+ assert "profiles_1d.ion:shape" not in group.variables
68
+ assert not hasattr(group["profiles_1d.ion"], "sparse")
69
+ assert "profiles_1d.ion.element:shape" in group.variables
70
+ # The shape array should be mentioned in the sparse attribute:
71
+ assert "profiles_1d.ion.element:shape" in group["profiles_1d.ion.element"].sparse
72
+ expected = [[[1], [2]]] * 3
73
+ assert numpy.array_equal(group["profiles_1d.ion.element:shape"], expected)
74
+ assert group["profiles_1d.ion.element:shape"].documentation != ""
75
+
76
+
77
+ def test_metadata(group):
78
+ ids = IDSFactory("3.39.0").core_profiles()
79
+
80
+ ids.ids_properties.homogeneous_time = IDS_TIME_MODE_HOMOGENEOUS
81
+ ids.ids_properties.comment = "Test string variable"
82
+ ids.time = [1.0, 2.0, 3.0]
83
+ ids.profiles_1d.resize(3)
84
+ for p1d in ids.profiles_1d:
85
+ p1d.grid.rho_tor_norm = [0.0, 0.5, 1.0]
86
+ p1d.j_tor = [1e3, 1e4, 1e5]
87
+
88
+ IDS2NC(ids, group).run()
89
+
90
+ for var in [
91
+ "ids_properties",
92
+ "ids_properties.homogeneous_time",
93
+ "ids_properties.comment",
94
+ "time",
95
+ "profiles_1d",
96
+ "profiles_1d.grid.rho_tor_norm",
97
+ "profiles_1d.j_tor",
98
+ ]:
99
+ assert group[var].documentation == ids.metadata[var].documentation
100
+ units = ids.metadata[var].units
101
+ if units:
102
+ assert group[var].units == units
103
+ fillvalue = default_fillvals.get(ids.metadata[var].data_type)
104
+ if fillvalue is None:
105
+ assert ids.metadata[var].data_type in [
106
+ IDSDataType.STRUCTURE,
107
+ IDSDataType.STRUCT_ARRAY,
108
+ ]
109
+ else:
110
+ assert group[var]._FillValue == fillvalue
111
+
112
+ assert (
113
+ group["profiles_1d.j_tor"].coordinates == "time profiles_1d.grid.rho_tor_norm"
114
+ )
115
+
116
+
117
+ def test_filter_coordinates(group):
118
+ ids = IDSFactory("3.39.0").pf_active()
119
+
120
+ ids.ids_properties.homogeneous_time = IDS_TIME_MODE_HOMOGENEOUS
121
+ ids.time = [1.0]
122
+ ids.coil.resize(2)
123
+ ids.coil[0].resistance = 1.0
124
+ ids.coil[1].resistance = 2.0
125
+
126
+ # Using sub-groups: IDS2NC expects an empty group to store the IDS in
127
+ IDS2NC(ids, group.createGroup("1")).run()
128
+ # coil.name or coil.identifier could be a coordinate, but they're not filled
129
+ assert not hasattr(group["1/coil.resistance"], "coordinates")
130
+
131
+ ids.coil[0].name = "coil 0"
132
+ ids.coil[1].name = "coil 1"
133
+ IDS2NC(ids, group.createGroup("2")).run()
134
+ assert group["2/coil.resistance"].coordinates == "coil.name"
135
+
136
+ ids.coil[0].identifier = "aab7b42a-5646-4f0a-8173-03dd5e4ad386"
137
+ ids.coil[1].identifier = "aab7b42a-5646-4f0a-8173-283a57b3a801"
138
+ IDS2NC(ids, group.createGroup("3")).run()
139
+ assert group["3/coil.resistance"].coordinates == "coil.name coil.identifier"
140
+
141
+
142
+ def test_ancillary_variables(group):
143
+ ids = IDSFactory("3.39.0").core_profiles()
144
+
145
+ ids.ids_properties.homogeneous_time = IDS_TIME_MODE_HOMOGENEOUS
146
+ ids.time = [1.0]
147
+ ids.profiles_1d.resize(1)
148
+
149
+ ids.profiles_1d[0].grid.rho_tor_norm = [0.0, 0.5, 1.0]
150
+ ids.profiles_1d[0].j_tor = [1e2, 1e3, 1e4]
151
+ ids.profiles_1d[0].j_tor_error_upper = [1e-1, 1e0, 1e1]
152
+ ids.profiles_1d[0].j_total = [1e2, 1e3, 1e4]
153
+ ids.profiles_1d[0].j_total_error_upper = [2e-1, 2e0, 2e1]
154
+ ids.profiles_1d[0].j_total_error_lower = [1e-1, 1e0, 1e1]
155
+
156
+ IDS2NC(ids, group).run()
157
+
158
+ assert not hasattr(group["profiles_1d.grid.rho_tor_norm"], "ancillary_variables")
159
+ assert (
160
+ group["profiles_1d.j_tor"].ancillary_variables
161
+ == "profiles_1d.j_tor_error_upper"
162
+ )
163
+ assert (
164
+ group["profiles_1d.j_total"].ancillary_variables
165
+ == "profiles_1d.j_total_error_upper profiles_1d.j_total_error_lower"
166
+ )
@@ -0,0 +1,39 @@
1
+ # This file is part of imas-python.
2
+ # You should have received the imas-python LICENSE file with this project.
3
+ from pathlib import PosixPath
4
+
5
+ import pytest
6
+
7
+ try:
8
+ from importlib.resources import files
9
+ except ImportError: # Python 3.8 support
10
+ from importlib_resources import files
11
+
12
+ import imas.training
13
+
14
+
15
+ def test_data_exists():
16
+ data_file: PosixPath = files(imas) / "assets/ITER_134173_106_equilibrium.ids"
17
+ assert data_file.exists()
18
+
19
+
20
+ @pytest.fixture
21
+ def test_data(requires_imas):
22
+ db_entry = imas.training.get_training_db_entry()
23
+ yield db_entry
24
+ db_entry.close()
25
+
26
+
27
+ def test_data_is_sane(test_data):
28
+ assert isinstance(test_data, imas.DBEntry)
29
+ eq = test_data.get("equilibrium")
30
+ assert len(eq.time_slice) == 3
31
+ ts = eq.time_slice[0]
32
+ r = ts.boundary.outline.r
33
+ z = ts.boundary.outline.z
34
+
35
+ # Test a few relevant numbers to check if data loading went okay
36
+ assert r.value[0] == pytest.approx(7.2896011)
37
+ assert r.value[-1] == pytest.approx(7.29120937)
38
+ assert z.value[0] == pytest.approx(-1.00816660e-01)
39
+ assert z.value[-1] == pytest.approx(-9.60027185e-14)
@@ -0,0 +1,415 @@
1
+ # Unit tests for ids_convert.py.
2
+ # See also integration tests for conversions in test_nbc_change.py
3
+
4
+ import logging
5
+ import re
6
+ from datetime import datetime, timedelta, timezone
7
+ from unittest.mock import MagicMock
8
+
9
+ import numpy
10
+ import pytest
11
+
12
+ from imas import identifiers
13
+ from imas.ids_convert import (
14
+ _get_ctxpath,
15
+ _get_tbp,
16
+ convert_ids,
17
+ dd_version_map_from_factories,
18
+ iter_parents,
19
+ )
20
+ from imas.ids_defs import (
21
+ ASCII_BACKEND,
22
+ IDS_TIME_MODE_HETEROGENEOUS,
23
+ IDS_TIME_MODE_HOMOGENEOUS,
24
+ IDS_TIME_MODE_INDEPENDENT,
25
+ MEMORY_BACKEND,
26
+ )
27
+ from imas.ids_factory import IDSFactory
28
+ from imas.ids_struct_array import IDSStructArray
29
+ from imas.ids_structure import IDSStructure
30
+ from imas.test.test_helpers import compare_children, open_dbentry
31
+
32
+ UTC = timezone.utc
33
+
34
+
35
+ def test_iter_parents():
36
+ assert list(iter_parents("a/b/c/d/e")) == ["a", "a/b", "a/b/c", "a/b/c/d"]
37
+ assert list(iter_parents("abc/def/g")) == ["abc", "abc/def"]
38
+
39
+
40
+ def test_dd_version_map_from_factories_invalid_version():
41
+ factory1 = IDSFactory(version="3.39.0")
42
+ factory2 = MagicMock()
43
+ factory2._version = "3.30.0-123-12345678"
44
+ factory2._etree = factory1._etree
45
+
46
+ version_map, factory1_is_oldest = dd_version_map_from_factories(
47
+ "core_profiles", factory1, factory2
48
+ )
49
+ assert not factory1_is_oldest
50
+ # maps should be empty, since we set the same etree on factory2
51
+ assert not version_map.new_to_old.path
52
+ assert not version_map.old_to_new.path
53
+
54
+
55
+ @pytest.fixture()
56
+ def factory():
57
+ return IDSFactory(version="3.38.0")
58
+
59
+
60
+ @pytest.fixture()
61
+ def core_profiles_paths(factory):
62
+ etree = factory._etree
63
+ cp = etree.find("IDS[@name='core_profiles']")
64
+ return {field.get("path", ""): field for field in cp.iterfind(".//field")}
65
+
66
+
67
+ def test_aos_and_ctxpath(core_profiles_paths):
68
+ paths = core_profiles_paths
69
+ f = _get_ctxpath
70
+ assert f("time", paths) == "time"
71
+ assert f("profiles_1d", paths) == "profiles_1d"
72
+ assert f("profiles_1d/time", paths) == "time"
73
+ assert f("profiles_1d/grid/rho_tor_norm", paths) == "grid/rho_tor_norm"
74
+ assert f("profiles_1d/ion", paths) == "ion"
75
+ assert f("profiles_1d/ion/element", paths) == "element"
76
+ assert f("profiles_1d/ion/element/z_n", paths) == "z_n"
77
+
78
+
79
+ def test_timebasepath(core_profiles_paths):
80
+ paths = core_profiles_paths
81
+ f = _get_tbp
82
+ assert f(paths["time"], paths) == "time"
83
+ assert f(paths["profiles_1d"], paths) == "profiles_1d/time"
84
+ assert f(paths["profiles_1d/grid"], paths) == ""
85
+
86
+
87
+ def test_compare_timebasepath_functions(ids_name):
88
+ # Ensure that the two timebasepath implementations are consistent
89
+ ids = IDSFactory().new(ids_name)
90
+ ids_element = ids.metadata._structure_xml
91
+ paths = {field.get("path", ""): field for field in ids_element.iterfind(".//field")}
92
+
93
+ def recurse(structure: IDSStructure, ctx_path: str):
94
+ for item in structure:
95
+ name = item.metadata.name
96
+ new_path = f"{ctx_path}/{name}" if ctx_path else name
97
+
98
+ tbp1 = _get_tbp(item.metadata._structure_xml, paths)
99
+ tbp2 = item.metadata.timebasepath
100
+ assert tbp1 == tbp2
101
+
102
+ if isinstance(item, IDSStructure):
103
+ recurse(item, new_path)
104
+ else:
105
+ if isinstance(item, IDSStructArray):
106
+ item.resize(1)
107
+ recurse(item[0], "")
108
+
109
+ recurse(ids, "")
110
+
111
+
112
+ def test_dbentry_autoconvert1(backend, worker_id, tmp_path):
113
+ entry_331 = open_dbentry(backend, "w", worker_id, tmp_path, dd_version="3.31.0")
114
+ old_factory = entry_331.factory
115
+ old_ids = old_factory.new("core_profiles")
116
+ old_ids.ids_properties.homogeneous_time = IDS_TIME_MODE_HETEROGENEOUS
117
+
118
+ # Put without conversion:
119
+ entry_331.put(old_ids)
120
+ assert old_ids.ids_properties.version_put.data_dictionary == "3.31.0"
121
+ if backend != MEMORY_BACKEND:
122
+ entry_331.close()
123
+
124
+ entry_342 = open_dbentry(backend, "r", worker_id, tmp_path, dd_version="3.42.0")
125
+
126
+ # Get without conversion
127
+ old_ids_get = entry_342.get("core_profiles", autoconvert=False)
128
+ assert old_ids_get.ids_properties.version_put.data_dictionary == "3.31.0"
129
+ assert old_ids_get._dd_version == "3.31.0"
130
+
131
+ # Work around ASCII backend bug...
132
+ if backend == ASCII_BACKEND:
133
+ entry_342.close()
134
+ entry_342 = open_dbentry(backend, "r", worker_id, tmp_path, dd_version="3.42.0")
135
+
136
+ # Get with conversion
137
+ new_ids_get = entry_342.get("core_profiles")
138
+ assert new_ids_get.ids_properties.version_put.data_dictionary == "3.31.0"
139
+ assert new_ids_get._dd_version == "3.42.0"
140
+
141
+ entry_342.close()
142
+
143
+
144
+ def test_dbentry_autoconvert2(backend, worker_id, tmp_path):
145
+ entry_342 = open_dbentry(backend, "w", worker_id, tmp_path, dd_version="3.42.0")
146
+ new_ids = entry_342.factory.new("core_profiles")
147
+ new_ids.ids_properties.homogeneous_time = IDS_TIME_MODE_HETEROGENEOUS
148
+
149
+ # Put without conversion:
150
+ entry_342.put(new_ids)
151
+ assert new_ids.ids_properties.version_put.data_dictionary == "3.42.0"
152
+ if backend != MEMORY_BACKEND:
153
+ entry_342.close()
154
+
155
+ entry_331 = open_dbentry(backend, "r", worker_id, tmp_path, dd_version="3.31.0")
156
+
157
+ # Get without conversion
158
+ new_ids_get = entry_331.get("core_profiles", autoconvert=False)
159
+ assert new_ids_get.ids_properties.version_put.data_dictionary == "3.42.0"
160
+ assert new_ids_get._dd_version == "3.42.0"
161
+
162
+ # Work around ASCII backend bug...
163
+ if backend == ASCII_BACKEND:
164
+ entry_331.close()
165
+ entry_331 = open_dbentry(backend, "r", worker_id, tmp_path, dd_version="3.31.0")
166
+
167
+ # Get with conversion
168
+ old_ids_get = entry_331.get("core_profiles")
169
+ assert old_ids_get.ids_properties.version_put.data_dictionary == "3.42.0"
170
+ assert old_ids_get._dd_version == "3.31.0"
171
+
172
+ entry_331.close()
173
+
174
+
175
+ def test_provenance_entry(factory):
176
+ cp = factory.core_profiles()
177
+ # Note: DD 3.31.0 doesn't have the provenance data structure, test that it doesn't
178
+ # report an error:
179
+ cp2 = convert_ids(cp, "3.31.0", provenance_origin_uri="<testdata>")
180
+ # Convert back to 3.38.0
181
+ cp3 = convert_ids(cp2, "3.38.0", provenance_origin_uri="<testdata>")
182
+ assert len(cp3.ids_properties.provenance.node) == 1
183
+ assert cp3.ids_properties.provenance.node[0].path == ""
184
+ assert len(cp3.ids_properties.provenance.node[0].sources) == 1
185
+ provenance_txt = cp3.ids_properties.provenance.node[0].sources[0]
186
+ # Check that the provided origin URI is in the text
187
+ assert "<testdata>" in provenance_txt
188
+ # Check that origin and destination DD versions are included
189
+ assert "3.31.0" in provenance_txt
190
+ assert "3.38.0" in provenance_txt
191
+ # Check that imas-python is mentioned
192
+ assert "imas-python" in provenance_txt
193
+
194
+ # Test logic branch for node.reference implemented with IMAS-5304
195
+ cp4 = convert_ids(cp2, "3.42.0", provenance_origin_uri="<testdata>")
196
+ assert len(cp4.ids_properties.provenance.node) == 1
197
+ assert cp4.ids_properties.provenance.node[0].path == ""
198
+ assert len(cp4.ids_properties.provenance.node[0].reference) == 1
199
+ assert "<testdata>" in cp4.ids_properties.provenance.node[0].reference[0].name
200
+ timestamp = str(cp4.ids_properties.provenance.node[0].reference[0].timestamp)
201
+ # Check that timestamp adheres to the format YYYY-MM-DDTHH:MM:SSZ
202
+ assert re.match(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z", timestamp)
203
+ dtime = datetime.now(UTC) - datetime.fromisoformat(timestamp)
204
+ assert timedelta(seconds=0) <= dtime < timedelta(seconds=2)
205
+
206
+
207
+ @pytest.fixture
208
+ def dd4factory():
209
+ return IDSFactory("4.0.0")
210
+
211
+
212
+ def test_3to4_ggd_space_identifier(dd4factory):
213
+ ep = IDSFactory("3.39.0").edge_profiles()
214
+ ep.ids_properties.homogeneous_time = IDS_TIME_MODE_HETEROGENEOUS
215
+ ep.grid_ggd.resize(1)
216
+ ep.grid_ggd[0].time = 0.0
217
+ ep.grid_ggd[0].space.resize(1)
218
+ ep.grid_ggd[0].space[0].coordinates_type = numpy.array([1, 2], dtype=numpy.int32)
219
+
220
+ ep4 = convert_ids(ep, None, factory=dd4factory)
221
+ cid = identifiers.coordinate_identifier
222
+ assert ep4.grid_ggd[0].time == 0.0
223
+ coordinates_type = ep4.grid_ggd[0].space[0].coordinates_type
224
+ assert len(coordinates_type) == 2
225
+ for i in range(2):
226
+ # Test that the full identifier structure is filled:
227
+ identifier = cid(i + 1)
228
+ assert coordinates_type[i].index == identifier.index
229
+ assert coordinates_type[i].name == identifier.name
230
+ assert coordinates_type[i].description == identifier.description
231
+
232
+ ep3 = convert_ids(ep4, "3.39.0")
233
+ compare_children(ep, ep3)
234
+
235
+
236
+ def test_3to4_repeat_children_first_point_conditional(dd4factory):
237
+ # The wall IDS contains all (four!) cases with conditional repeats
238
+ wall = IDSFactory("3.39.0").wall()
239
+ wall.ids_properties.homogeneous_time = IDS_TIME_MODE_HETEROGENEOUS
240
+ wall.description_2d.resize(2)
241
+
242
+ # Case 1: repeat_children_first_point_conditional
243
+ wall.description_2d[0].vessel.unit.resize(2)
244
+ for i in range(2):
245
+ outline_inner = wall.description_2d[0].vessel.unit[i].annular.outline_inner
246
+ outline_inner.closed = i # first is open, second is closed
247
+ outline_inner.r = [1.0, 2.0, 3.0]
248
+ outline_inner.z = [-1.0, -2.0, -3.0]
249
+
250
+ # Case 2: repeat_children_first_point_conditional_sibling
251
+ wall.description_2d[0].limiter.unit.resize(2)
252
+ for i in range(2):
253
+ unit = wall.description_2d[0].limiter.unit[i]
254
+ unit.closed = i # first is open, second is closed
255
+ unit.outline.r = [1.0, 2.0, 3.0]
256
+ unit.outline.z = [-1.0, -2.0, -3.0]
257
+
258
+ # Case 3: repeat_children_first_point_conditional_sibling_dynamic
259
+ wall.description_2d[0].mobile.unit.resize(2)
260
+ for i in range(2):
261
+ unit = wall.description_2d[0].mobile.unit[i]
262
+ unit.closed = i # first is open, second is closed
263
+ unit.outline.resize(3)
264
+ for j in range(3):
265
+ unit.outline[j].r = [1.0, 2.0, 3.0]
266
+ unit.outline[j].z = [-1.0, -2.0, -3.0]
267
+ unit.outline[j].time = j / 5
268
+
269
+ # Case 4: repeat_children_first_point_conditional_centreline
270
+ # (see https://jira.iter.org/browse/IMAS-5541)
271
+ wall.description_2d[1].vessel.unit.resize(2)
272
+ for i in range(2):
273
+ centreline = wall.description_2d[1].vessel.unit[i].annular.centreline
274
+ centreline.closed = i # first is open, second is closed
275
+ centreline.r = [1.0, 2.0, 3.0]
276
+ centreline.z = [-1.0, -2.0, -3.0]
277
+ # if it was open there were too many thickness values!
278
+ # The last one will be dropped and repeated
279
+ wall.description_2d[1].vessel.unit[i].annular.thickness = [1, 0.9, 0.9]
280
+
281
+ wall4 = convert_ids(wall, None, factory=dd4factory)
282
+ assert len(wall4.description_2d) == 2
283
+
284
+ # Test conversion for case 1:
285
+ assert len(wall4.description_2d[0].vessel.unit) == 2
286
+ for i in range(2):
287
+ outline_inner = wall4.description_2d[0].vessel.unit[i].annular.outline_inner
288
+ if i == 0: # open outline, first point not repeated:
289
+ assert numpy.array_equal(outline_inner.r, [1.0, 2.0, 3.0])
290
+ assert numpy.array_equal(outline_inner.z, [-1.0, -2.0, -3.0])
291
+ else: # closed outline, first point repeated:
292
+ assert numpy.array_equal(outline_inner.r, [1.0, 2.0, 3.0, 1.0])
293
+ assert numpy.array_equal(outline_inner.z, [-1.0, -2.0, -3.0, -1.0])
294
+
295
+ # Test conversion for case 2:
296
+ assert len(wall4.description_2d[0].limiter.unit) == 2
297
+ for i in range(2):
298
+ unit = wall4.description_2d[0].limiter.unit[i]
299
+ if i == 0: # open outline, first point not repeated:
300
+ assert numpy.array_equal(unit.outline.r, [1.0, 2.0, 3.0])
301
+ assert numpy.array_equal(unit.outline.z, [-1.0, -2.0, -3.0])
302
+ else: # closed outline, first point repeated:
303
+ assert numpy.array_equal(unit.outline.r, [1.0, 2.0, 3.0, 1.0])
304
+ assert numpy.array_equal(unit.outline.z, [-1.0, -2.0, -3.0, -1.0])
305
+
306
+ # Test conversion for case 3:
307
+ assert len(wall4.description_2d[0].mobile.unit) == 2
308
+ for i in range(2):
309
+ unit = wall4.description_2d[0].mobile.unit[i]
310
+ for j in range(3):
311
+ if i == 0: # open outline, first point not repeated:
312
+ assert numpy.array_equal(unit.outline[j].r, [1.0, 2.0, 3.0])
313
+ assert numpy.array_equal(unit.outline[j].z, [-1.0, -2.0, -3.0])
314
+ else: # closed outline, first point repeated:
315
+ assert numpy.array_equal(unit.outline[j].r, [1.0, 2.0, 3.0, 1.0])
316
+ assert numpy.array_equal(unit.outline[j].z, [-1.0, -2.0, -3.0, -1.0])
317
+ assert unit.outline[j].time == pytest.approx(j / 5)
318
+
319
+ # Test conversion for case 4:
320
+ assert len(wall4.description_2d[1].vessel.unit) == 2
321
+ for i in range(2):
322
+ thickness = wall4.description_2d[1].vessel.unit[i].annular.thickness
323
+ if i == 0: # open outline, there was one value too many, drop the last one
324
+ assert numpy.array_equal(thickness, [1, 0.9])
325
+ else: # closed outline, thickness values kept
326
+ assert numpy.array_equal(thickness, [1, 0.9, 0.9])
327
+
328
+ # Test conversion back
329
+ wall3 = convert_ids(wall4, "3.39.0")
330
+ compare_children(wall, wall3)
331
+
332
+
333
+ def test_3to4_repeat_children_first_point(dd4factory):
334
+ iron_core = IDSFactory("3.39.0").iron_core()
335
+ iron_core.ids_properties.homogeneous_time = IDS_TIME_MODE_HETEROGENEOUS
336
+ iron_core.segment.resize(1)
337
+ iron_core.segment[0].geometry.outline.r = [1.0, 2.0, 3.0]
338
+ iron_core.segment[0].geometry.outline.z = [-1.0, -2.0, -3.0]
339
+
340
+ iron_core4 = convert_ids(iron_core, None, factory=dd4factory)
341
+ geometry = iron_core4.segment[0].geometry
342
+ assert numpy.array_equal(geometry.outline.r, [1.0, 2.0, 3.0, 1.0])
343
+ assert numpy.array_equal(geometry.outline.z, [-1.0, -2.0, -3.0, -1.0])
344
+
345
+ iron_core3 = convert_ids(iron_core4, "3.39.0")
346
+ compare_children(iron_core, iron_core3)
347
+
348
+
349
+ def test_3to4_cocos_change(dd4factory):
350
+ cp = IDSFactory("3.39.0").core_profiles()
351
+ cp.ids_properties.homogeneous_time = IDS_TIME_MODE_HOMOGENEOUS
352
+ cp.time = [1.0]
353
+ cp.profiles_1d.resize(1)
354
+ cp.profiles_1d[0].grid.rho_tor_norm = numpy.linspace(0, 1, 11)
355
+ cp.profiles_1d[0].grid.psi = numpy.linspace(10, 20, 11)
356
+
357
+ cp4 = convert_ids(cp, None, factory=dd4factory)
358
+ assert numpy.array_equal(
359
+ cp4.profiles_1d[0].grid.rho_tor_norm,
360
+ cp.profiles_1d[0].grid.rho_tor_norm,
361
+ )
362
+ assert numpy.array_equal(
363
+ cp4.profiles_1d[0].grid.psi,
364
+ -cp.profiles_1d[0].grid.psi,
365
+ )
366
+
367
+ cp3 = convert_ids(cp4, "3.39.0")
368
+ compare_children(cp, cp3)
369
+
370
+
371
+ def test_3to4_circuit_connections(dd4factory, caplog):
372
+ pfa = IDSFactory("3.39.0").pf_active()
373
+ pfa.ids_properties.homogeneous_time = IDS_TIME_MODE_HETEROGENEOUS
374
+ pfa.circuit.resize(1)
375
+ pfa.circuit[0].connections = [
376
+ [0, 1, 0, 0, 1, 0],
377
+ [0, 0, 1, 0, 0, 1],
378
+ [1, 0, 0, 1, 0, 0],
379
+ ]
380
+
381
+ pfa4 = convert_ids(pfa, None, factory=dd4factory)
382
+ assert numpy.array_equal(
383
+ pfa4.circuit[0].connections, [[-1, 0, 1], [0, 1, -1], [1, -1, 0]]
384
+ )
385
+
386
+ pfa3 = convert_ids(pfa4, "3.39.0")
387
+ compare_children(pfa, pfa3)
388
+
389
+ # Test invalid connections shape
390
+ pfa.circuit[0].connections = [
391
+ [0, 1, 0, 0, 1, 0, 1],
392
+ [0, 0, 1, 0, 0, 1, 1],
393
+ [1, 0, 0, 1, 0, 0, 1],
394
+ ]
395
+ caplog.clear()
396
+ with caplog.at_level(logging.ERROR):
397
+ pfa4 = convert_ids(pfa, None, factory=dd4factory)
398
+ # Incorrect shape, data is not converted:
399
+ assert numpy.array_equal(pfa.circuit[0].connections, pfa4.circuit[0].connections)
400
+ # Check that a message with ERROR severity was logged
401
+ assert len(caplog.record_tuples) == 1
402
+ assert caplog.record_tuples[0][1] == logging.ERROR
403
+
404
+
405
+ def test_3to4_cocos_magnetics_workaround(dd4factory):
406
+ mag = IDSFactory("3.39.0").magnetics()
407
+ mag.ids_properties.homogeneous_time = IDS_TIME_MODE_INDEPENDENT
408
+ mag.flux_loop.resize(1)
409
+ mag.flux_loop[0].flux.data = [1.0, 2.0]
410
+
411
+ mag4 = convert_ids(mag, None, factory=dd4factory)
412
+ assert numpy.array_equal(mag4.flux_loop[0].flux.data, [-1.0, -2.0])
413
+
414
+ mag3 = convert_ids(mag4, "3.39.0")
415
+ compare_children(mag, mag3)