Skip to content

Commit 842cde1

Browse files
committed
Merge branch 'develop' into renaming_3to4_name_identifier
2 parents 14afe29 + e2195e4 commit 842cde1

File tree

6 files changed

+177
-37
lines changed

6 files changed

+177
-37
lines changed

docs/source/cli.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,8 @@ process these files with ``imas process-db-analysis``. This will:
121121
variable).
122122
2. These results are summarized in a table, showing per IDS:
123123

124-
- The number of data fields [#data_fields]_ that were filled in *any* of the
125-
analyzed data entries.
124+
- The number of data fields [#data_fields]_ that were filled in *any* occurrence of
125+
the IDS in *any* of the analyzed data entries.
126126
- The total number of data fields [#data_fields]_ that the Data Dictionary
127127
defines for this IDS.
128128
- The percentage of fields filled.

imas/db_entry.py

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22
# You should have received the IMAS-Python LICENSE file with this project.
33
"""Logic for interacting with IMAS Data Entries."""
44

5+
from __future__ import annotations
6+
57
import logging
68
import os
7-
from typing import Any, List, Optional, Tuple, Type, Union, overload
9+
import pathlib
10+
from typing import Any, Type, overload
811

912
import numpy
1013

@@ -33,14 +36,14 @@
3336
logger = logging.getLogger(__name__)
3437

3538

36-
def _get_uri_mode(uri, mode) -> Tuple[str, str]:
39+
def _get_uri_mode(uri, mode) -> tuple[str, str]:
3740
"""Helper method to parse arguments of DBEntry.__init__."""
3841
return uri, mode
3942

4043

4144
def _get_legacy_params(
4245
backend_id, db_name, pulse, run, user_name=None, data_version=None
43-
) -> Tuple[int, str, int, int, Optional[str], Optional[str]]:
46+
) -> tuple[int, str, int, int, str | None, str | None]:
4447
"""Helper method to parse arguments of DBEntry.__init__."""
4548
return backend_id, db_name, pulse, run, user_name, data_version
4649

@@ -74,8 +77,8 @@ def __init__(
7477
uri: str,
7578
mode: str,
7679
*,
77-
dd_version: Optional[str] = None,
78-
xml_path: Optional[str] = None,
80+
dd_version: str | None = None,
81+
xml_path: str | pathlib.Path | None = None,
7982
) -> None: ...
8083

8184
@overload
@@ -85,19 +88,19 @@ def __init__(
8588
db_name: str,
8689
pulse: int,
8790
run: int,
88-
user_name: Optional[str] = None,
89-
data_version: Optional[str] = None,
91+
user_name: str | None = None,
92+
data_version: str | None = None,
9093
*,
91-
shot: Optional[int] = None,
92-
dd_version: Optional[str] = None,
93-
xml_path: Optional[str] = None,
94+
shot: int | None = None,
95+
dd_version: str | None = None,
96+
xml_path: str | pathlib.Path | None = None,
9497
) -> None: ...
9598

9699
def __init__(
97100
self,
98101
*args,
99-
dd_version: Optional[str] = None,
100-
xml_path: Optional[str] = None,
102+
dd_version: str | None = None,
103+
xml_path: str | pathlib.Path | None = None,
101104
**kwargs,
102105
):
103106
"""Open or create a Data Entry based on the provided URI and mode, or prepare a
@@ -162,7 +165,7 @@ def __init__(
162165
) from None
163166

164167
# Actual intializiation
165-
self._dbe_impl: Optional[DBEntryImpl] = None
168+
self._dbe_impl: DBEntryImpl | None = None
166169
self._dd_version = dd_version
167170
self._xml_path = xml_path
168171
self._ids_factory = IDSFactory(dd_version, xml_path)
@@ -186,7 +189,7 @@ def __init__(
186189
self._dbe_impl = cls.from_uri(self.uri, mode, self._ids_factory)
187190

188191
@staticmethod
189-
def _select_implementation(uri: Optional[str]) -> Type[DBEntryImpl]:
192+
def _select_implementation(uri: str | None) -> Type[DBEntryImpl]:
190193
"""Select which DBEntry implementation to use based on the URI."""
191194
if uri and uri.endswith(".nc") and not uri.startswith("imas:"):
192195
from imas.backends.netcdf.db_entry_nc import NCDBEntryImpl as impl
@@ -307,7 +310,7 @@ def get(
307310
lazy: bool = False,
308311
autoconvert: bool = True,
309312
ignore_unknown_dd_version: bool = False,
310-
destination: Optional[IDSToplevel] = None,
313+
destination: IDSToplevel | None = None,
311314
) -> IDSToplevel:
312315
"""Read the contents of an IDS into memory.
313316
@@ -370,7 +373,7 @@ def get_slice(
370373
lazy: bool = False,
371374
autoconvert: bool = True,
372375
ignore_unknown_dd_version: bool = False,
373-
destination: Optional[IDSToplevel] = None,
376+
destination: IDSToplevel | None = None,
374377
) -> IDSToplevel:
375378
"""Read a single time slice from an IDS in this Database Entry.
376379
@@ -434,14 +437,14 @@ def get_sample(
434437
ids_name: str,
435438
tmin: float,
436439
tmax: float,
437-
dtime: Optional[Union[float, numpy.ndarray]] = None,
438-
interpolation_method: Optional[int] = None,
440+
dtime: float | numpy.ndarray | None = None,
441+
interpolation_method: int | None = None,
439442
occurrence: int = 0,
440443
*,
441444
lazy: bool = False,
442445
autoconvert: bool = True,
443446
ignore_unknown_dd_version: bool = False,
444-
destination: Optional[IDSToplevel] = None,
447+
destination: IDSToplevel | None = None,
445448
) -> IDSToplevel:
446449
"""Read a range of time slices from an IDS in this Database Entry.
447450
@@ -547,8 +550,8 @@ def _get(
547550
self,
548551
ids_name: str,
549552
occurrence: int,
550-
parameters: Union[None, GetSliceParameters, GetSampleParameters],
551-
destination: Optional[IDSToplevel],
553+
parameters: None | GetSliceParameters | GetSampleParameters,
554+
destination: IDSToplevel | None,
552555
lazy: bool,
553556
autoconvert: bool,
554557
ignore_unknown_dd_version: bool,
@@ -751,12 +754,12 @@ def delete_data(self, ids_name: str, occurrence: int = 0) -> None:
751754
@overload
752755
def list_all_occurrences(
753756
self, ids_name: str, node_path: None = None
754-
) -> List[int]: ...
757+
) -> list[int]: ...
755758

756759
@overload
757760
def list_all_occurrences(
758761
self, ids_name: str, node_path: str
759-
) -> Tuple[List[int], List[IDSBase]]: ...
762+
) -> tuple[list[int], list[IDSBase]]: ...
760763

761764
def list_all_occurrences(self, ids_name, node_path=None):
762765
"""List all non-empty occurrences of an IDS

imas/ids_convert.py

Lines changed: 116 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -349,13 +349,18 @@ def _add_rename(self, old_path: str, new_path: str) -> None:
349349

350350
def _apply_3to4_conversion(self, old: Element, new: Element) -> None:
351351
# Postprocessing for COCOS definition change:
352+
cocos_paths = []
352353
for psi_like in ["psi_like", "dodpsi_like"]:
353354
xpath_query = f".//field[@cocos_label_transformation='{psi_like}']"
354355
for old_item in old.iterfind(xpath_query):
355-
old_path = old_item.get("path")
356-
new_path = self.old_to_new.path.get(old_path, old_path)
357-
self.new_to_old.post_process[new_path] = _cocos_change
358-
self.old_to_new.post_process[old_path] = _cocos_change
356+
cocos_paths.append(old_item.get("path"))
357+
# Sign flips not covered by the generic rule:
358+
cocos_paths.extend(_3to4_sign_flip_paths.get(self.ids_name, []))
359+
for old_path in cocos_paths:
360+
new_path = self.old_to_new.path.get(old_path, old_path)
361+
self.new_to_old.post_process[new_path] = _cocos_change
362+
self.old_to_new.post_process[old_path] = _cocos_change
363+
359364
# Definition change for pf_active circuit/connections
360365
if self.ids_name == "pf_active":
361366
path = "circuit/connections"
@@ -729,6 +734,113 @@ def _copy_structure(
729734
callback(item, target_item)
730735

731736

737+
_3to4_sign_flip_paths = {
738+
"core_instant_changes": [
739+
"change/profiles_1d/grid/psi_magnetic_axis",
740+
"change/profiles_1d/grid/psi_boundary",
741+
],
742+
"core_profiles": [
743+
"profiles_1d/grid/psi_magnetic_axis",
744+
"profiles_1d/grid/psi_boundary",
745+
],
746+
"core_sources": [
747+
"source/profiles_1d/grid/psi_magnetic_axis",
748+
"source/profiles_1d/grid/psi_boundary",
749+
],
750+
"core_transport": [
751+
"model/profiles_1d/grid_d/psi_magnetic_axis",
752+
"model/profiles_1d/grid_d/psi_boundary",
753+
"model/profiles_1d/grid_v/psi_magnetic_axis",
754+
"model/profiles_1d/grid_v/psi_boundary",
755+
"model/profiles_1d/grid_flux/psi_magnetic_axis",
756+
"model/profiles_1d/grid_flux/psi_boundary",
757+
],
758+
"disruption": [
759+
"global_quantities/psi_halo_boundary",
760+
"profiles_1d/grid/psi_magnetic_axis",
761+
"profiles_1d/grid/psi_boundary",
762+
],
763+
"ece": [
764+
"channel/beam_tracing/beam/position/psi",
765+
"psi_normalization/psi_magnetic_axis",
766+
"psi_normalization/psi_boundary",
767+
],
768+
"edge_profiles": [
769+
"profiles_1d/grid/psi",
770+
"profiles_1d/grid/psi_magnetic_axis",
771+
"profiles_1d/grid/psi_boundary",
772+
],
773+
"equilibrium": [
774+
"time_slice/boundary/psi",
775+
"time_slice/global_quantities/q_min/psi",
776+
"time_slice/ggd/psi/values",
777+
],
778+
"mhd": ["ggd/psi/values"],
779+
"pellets": ["time_slice/pellet/path_profiles/psi"],
780+
"plasma_profiles": [
781+
"profiles_1d/grid/psi",
782+
"profiles_1d/grid/psi_magnetic_axis",
783+
"profiles_1d/grid/psi_boundary",
784+
"ggd/psi/values",
785+
],
786+
"plasma_sources": [
787+
"source/profiles_1d/grid/psi",
788+
"source/profiles_1d/grid/psi_magnetic_axis",
789+
"source/profiles_1d/grid/psi_boundary",
790+
],
791+
"plasma_transport": [
792+
"model/profiles_1d/grid_d/psi",
793+
"model/profiles_1d/grid_d/psi_magnetic_axis",
794+
"model/profiles_1d/grid_d/psi_boundary",
795+
"model/profiles_1d/grid_v/psi",
796+
"model/profiles_1d/grid_v/psi_magnetic_axis",
797+
"model/profiles_1d/grid_v/psi_boundary",
798+
"model/profiles_1d/grid_flux/psi",
799+
"model/profiles_1d/grid_flux/psi_magnetic_axis",
800+
"model/profiles_1d/grid_flux/psi_boundary",
801+
],
802+
"radiation": [
803+
"process/profiles_1d/grid/psi_magnetic_axis",
804+
"process/profiles_1d/grid/psi_boundary",
805+
],
806+
"reflectometer_profile": [
807+
"psi_normalization/psi_magnetic_axis",
808+
"psi_normalization/psi_boundary",
809+
],
810+
"reflectometer_fluctuation": [
811+
"psi_normalization/psi_magnetic_axis",
812+
"psi_normalization/psi_boundary",
813+
],
814+
"runaway_electrons": [
815+
"profiles_1d/grid/psi_magnetic_axis",
816+
"profiles_1d/grid/psi_boundary",
817+
],
818+
"sawteeth": [
819+
"profiles_1d/grid/psi_magnetic_axis",
820+
"profiles_1d/grid/psi_boundary",
821+
],
822+
"summary": [
823+
"global_quantities/psi_external_average/value",
824+
"local/magnetic_axis/position/psi",
825+
],
826+
"transport_solver_numerics": [
827+
"solver_1d/grid/psi_magnetic_axis",
828+
"solver_1d/grid/psi_boundary",
829+
"derivatives_1d/grid/psi_magnetic_axis",
830+
"derivatives_1d/grid/psi_boundary",
831+
],
832+
"wall": ["description_ggd/ggd/psi/values"],
833+
"waves": [
834+
"coherent_wave/profiles_1d/grid/psi_magnetic_axis",
835+
"coherent_wave/profiles_1d/grid/psi_boundary",
836+
"coherent_wave/profiles_2d/grid/psi",
837+
"coherent_wave/beam_tracing/beam/position/psi",
838+
],
839+
}
840+
"""List of paths per IDS that require a COCOS sign change, but aren't covered by the
841+
generic rule."""
842+
843+
732844
########################################################################################
733845
# Type changed handlers and post-processing functions #
734846
########################################################################################

imas/ids_factory.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
# This file is part of IMAS-Python.
22
# You should have received the IMAS-Python LICENSE file with this project.
3-
"""Tools for generating IDSs from a Data Dictionary version.
4-
"""
3+
"""Tools for generating IDSs from a Data Dictionary version."""
4+
5+
from __future__ import annotations
56

67
import logging
8+
import pathlib
9+
from collections.abc import Iterable, Iterator
710
from functools import partial
8-
from typing import Any, Iterable, Iterator, List, Optional
11+
from typing import Any
912

1013
from imas import dd_zip
1114
from imas.exception import IDSNameError
@@ -27,7 +30,7 @@ class IDSFactory:
2730
"""
2831

2932
def __init__(
30-
self, version: Optional[str] = None, xml_path: Optional[str] = None
33+
self, version: str | None = None, xml_path: str | pathlib.Path | None = None
3134
) -> None:
3235
"""Create a new IDS Factory
3336
@@ -77,7 +80,7 @@ def __iter__(self) -> Iterator[str]:
7780
"""Iterate over the IDS names defined by the loaded Data Dictionary"""
7881
return iter(self._ids_elements)
7982

80-
def ids_names(self) -> List[str]:
83+
def ids_names(self) -> list[str]:
8184
"""Get a list of all known IDS names in the loaded Data Dictionary"""
8285
return list(self._ids_elements)
8386

imas/ids_structure.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# This file is part of IMAS-Python.
22
# You should have received the IMAS-Python LICENSE file with this project.
3-
"""A structure in an IDS
4-
"""
3+
"""A structure in an IDS"""
54

65
import logging
76
from copy import deepcopy
@@ -151,7 +150,9 @@ def __deepcopy__(self, memo):
151150
for child in self._children:
152151
if child in self.__dict__:
153152
child_copy = deepcopy(getattr(self, child), memo)
154-
setattr(copy, child, child_copy)
153+
# bypass __setattr__:
154+
copy.__dict__[child] = child_copy
155+
child_copy._parent = copy
155156
return copy
156157

157158
def __dir__(self) -> List[str]:

imas/test/test_ids_convert.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212

1313
from imas import identifiers
1414
from imas.ids_convert import (
15+
_3to4_sign_flip_paths,
1516
_get_ctxpath,
1617
_get_tbp,
1718
convert_ids,
1819
dd_version_map_from_factories,
1920
iter_parents,
2021
)
22+
from imas.ids_data_type import IDSDataType
2123
from imas.ids_defs import (
2224
ASCII_BACKEND,
2325
IDS_TIME_MODE_HETEROGENEOUS,
@@ -571,3 +573,22 @@ def test_4to3_name_identifier_mapping_magnetics():
571573

572574
# DD4 name -> DD3 identifier
573575
assert dst.b_field_pol_probe[0].identifier == "TEST_NAME"
576+
577+
578+
def test_3to4_cocos_hardcoded_paths():
579+
# Check for existence in 3.42.0
580+
factory = IDSFactory("3.42.0")
581+
for ids_name, paths in _3to4_sign_flip_paths.items():
582+
ids = factory.new(ids_name)
583+
for path in paths:
584+
# Check path exists and is not a FLT
585+
metadata = ids.metadata[path]
586+
assert metadata.data_type is IDSDataType.FLT
587+
588+
# Test a conversion
589+
eq = factory.equilibrium()
590+
eq.time_slice.resize(1)
591+
eq.time_slice[0].boundary.psi = 3.141
592+
593+
eq4 = convert_ids(eq, "4.0.0")
594+
assert eq4.time_slice[0].boundary.psi == -3.141

0 commit comments

Comments
 (0)