Skip to content

Commit a149238

Browse files
authored
Merge pull request #68 from dee-mew/renaming_3to4_name_identifier
[GH#59] Convert name/identifier(DD3) to description/name(DD4)
2 parents e2195e4 + 2d4f1de commit a149238

File tree

3 files changed

+131
-16
lines changed

3 files changed

+131
-16
lines changed

docs/source/multi-dd.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,25 @@ Explicit conversion
117117
versions, the corresponding data is not copied. IMAS-Python provides logging to indicate
118118
when this happens.
119119

120+
.. rubric:: DD3 -> DD4 special rule: name + identifier -> description + name (GH#59)
121+
122+
IMAS‑Python implements an additional explicit conversion rule (see GH#59) to improve
123+
migration of Machine Description parts of IDSs when moving from major version 3 to 4.
124+
The rule targets simple sibling pairs on the same parent that provide both a "name"
125+
and an "identifier" field and that are NOT part of an "identifier structure" (the
126+
parent must not also have an "index" sibling). When applicable the rule performs the
127+
following renames during explicit DD3->DD4 conversion:
128+
129+
- DD3: parent/name -> DD4: parent/description
130+
- DD3: parent/identifier -> DD4: parent/name
131+
132+
The conversion is applied only when the corresponding target fields exist in the
133+
DD4 definition and when no earlier mapping already covers the same paths. This
134+
is performed by the explicit conversion machinery (for example via
135+
imas.convert_ids or DBEntry explicit conversion) and is not guaranteed to be
136+
applied by automatic conversion when reading/writing from a backend.
137+
138+
In some cases like the one above, reverse conversion is also allowed(DD 4.0.0 -> 3.41.1)
120139

121140
.. _`Supported conversions`:
122141

imas/ids_convert.py

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,10 @@ def _build_map(self, old: Element, new: Element) -> None:
201201
old_path_set = set(old_paths)
202202
new_path_set = set(new_paths)
203203

204+
# expose the path->Element maps as members so other methods can reuse them
205+
self.old_paths = old_paths
206+
self.new_paths = new_paths
207+
204208
def process_parent_renames(path: str) -> str:
205209
# Apply any parent AoS/structure rename
206210
# Loop in reverse order to find the closest parent which was renamed:
@@ -222,20 +226,6 @@ def get_old_path(path: str, previous_name: str) -> str:
222226
old_path = previous_name
223227
return process_parent_renames(old_path)
224228

225-
def add_rename(old_path: str, new_path: str):
226-
old_item = old_paths[old_path]
227-
new_item = new_paths[new_path]
228-
self.new_to_old[new_path] = (
229-
old_path,
230-
_get_tbp(old_item, old_paths),
231-
_get_ctxpath(old_path, old_paths),
232-
)
233-
self.old_to_new[old_path] = (
234-
new_path,
235-
_get_tbp(new_item, new_paths),
236-
_get_ctxpath(new_path, new_paths),
237-
)
238-
239229
# Iterate through all NBC metadata and add entries
240230
for new_item in new.iterfind(".//field[@change_nbc_description]"):
241231
new_path = new_item.get("path")
@@ -275,14 +265,16 @@ def add_rename(old_path: str, new_path: str):
275265
self.version_old,
276266
)
277267
elif self._check_data_type(old_item, new_item):
278-
add_rename(old_path, new_path)
268+
# use class helper to register simple renames and
269+
# reciprocal mappings
270+
self._add_rename(old_path, new_path)
279271
if old_item.get("data_type") in DDVersionMap.STRUCTURE_TYPES:
280272
# Add entries for common sub-elements
281273
for path in old_paths:
282274
if path.startswith(old_path):
283275
npath = path.replace(old_path, new_path, 1)
284276
if npath in new_path_set:
285-
add_rename(path, npath)
277+
self._add_rename(path, npath)
286278
elif nbc_description == "type_changed":
287279
pass # We will handle this (if possible) in self._check_data_type
288280
elif nbc_description == "repeat_children_first_point":
@@ -334,6 +326,28 @@ def add_rename(old_path: str, new_path: str):
334326
if self.version_old.major == 3 and new_version and new_version.major == 4:
335327
self._apply_3to4_conversion(old, new)
336328

329+
def _add_rename(self, old_path: str, new_path: str) -> None:
330+
"""Register a simple rename from old_path -> new_path using the
331+
path->Element maps stored on the instance (self.old_paths/self.new_paths).
332+
This will also add the reciprocal mapping when possible.
333+
"""
334+
old_item = self.old_paths[old_path]
335+
new_item = self.new_paths[new_path]
336+
337+
# forward mapping
338+
self.old_to_new[old_path] = (
339+
new_path,
340+
_get_tbp(new_item, self.new_paths),
341+
_get_ctxpath(new_path, self.new_paths),
342+
)
343+
344+
# reciprocal mapping
345+
self.new_to_old[new_path] = (
346+
old_path,
347+
_get_tbp(old_item, self.old_paths),
348+
_get_ctxpath(old_path, self.old_paths),
349+
)
350+
337351
def _apply_3to4_conversion(self, old: Element, new: Element) -> None:
338352
# Postprocessing for COCOS definition change:
339353
cocos_paths = []
@@ -391,6 +405,46 @@ def _apply_3to4_conversion(self, old: Element, new: Element) -> None:
391405
to_update[p] = v
392406
self.old_to_new.path.update(to_update)
393407

408+
# GH#59: To improve further the conversion of DD3 to DD4, especially the
409+
# Machine Description part of the IDSs, we would like to add a 3to4 specific
410+
# rule to convert any siblings name + identifier (that are not part of an
411+
# identifier structure, meaning that there is no index sibling) into
412+
# description + name. Meaning:
413+
# parent/name (DD3) -> parent/description (DD4)
414+
# parent/identifier (DD3) -> parent/name (DD4)
415+
# Only perform the mapping if the corresponding target fields exist in the
416+
# new DD and if we don't already have a mapping for the involved paths.
417+
# use self.old_paths and self.new_paths set in _build_map
418+
for p in self.old_paths:
419+
# look for name children
420+
if not p.endswith("/name"):
421+
continue
422+
parent = p.rsplit("/", 1)[0]
423+
name_path = f"{parent}/name"
424+
id_path = f"{parent}/identifier"
425+
index_path = f"{parent}/index"
426+
desc_path = f"{parent}/description"
427+
new_name_path = name_path
428+
429+
# If neither 'name' nor 'identifier' existed in the old DD, skip this parent
430+
if name_path not in self.old_paths or id_path not in self.old_paths:
431+
continue
432+
# exclude identifier-structure (has index sibling)
433+
if index_path in self.old_paths:
434+
continue
435+
436+
# Ensure the candidate target fields exist in the new DD
437+
if desc_path not in self.new_paths or new_name_path not in self.new_paths:
438+
continue
439+
440+
# Map DD3 name -> DD4 description
441+
if name_path not in self.old_to_new.path:
442+
self._add_rename(name_path, desc_path)
443+
444+
# Map DD3 identifier -> DD4 name
445+
if id_path in self.old_to_new.path:
446+
self._add_rename(id_path, new_name_path)
447+
394448
def _map_missing(self, is_new: bool, missing_paths: Set[str]):
395449
rename_map = self.new_to_old if is_new else self.old_to_new
396450
# Find all structures which have a renamed sub-item

imas/test/test_ids_convert.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,48 @@ def test_3to4_migrate_deprecated_fields(): # GH#55
533533
assert cp4.profiles_1d[0].ion[0].name == "y"
534534

535535

536+
def test_3to4_name_identifier_mapping_magnetics():
537+
# Create source IDS using DD 3.40.1
538+
factory = IDSFactory("3.40.1")
539+
540+
src = factory.magnetics()
541+
src.ids_properties.homogeneous_time = IDS_TIME_MODE_HOMOGENEOUS
542+
# Populate a parent that has name + identifier (no 'index' sibling)
543+
src.b_field_pol_probe.resize(1)
544+
src.b_field_pol_probe[0].name = "TEST_NAME"
545+
src.b_field_pol_probe[0].identifier = "TEST_IDENTIFIER"
546+
547+
# Convert to DD 4.0.0
548+
dst = convert_ids(src, "4.0.0")
549+
550+
# DD3 name -> DD4 description
551+
assert dst.b_field_pol_probe[0].description == "TEST_NAME"
552+
553+
# DD3 identifier -> DD4 name
554+
assert dst.b_field_pol_probe[0].name == "TEST_IDENTIFIER"
555+
556+
557+
def test_4to3_name_identifier_mapping_magnetics():
558+
# Create source IDS using DD 4.0.0
559+
factory = IDSFactory("4.0.0")
560+
561+
src = factory.magnetics()
562+
src.ids_properties.homogeneous_time = IDS_TIME_MODE_HOMOGENEOUS
563+
# Populate a parent that has description + name (no 'index' sibling)
564+
src.b_field_pol_probe.resize(1)
565+
src.b_field_pol_probe[0].description = "TEST_DESCRIPTION"
566+
src.b_field_pol_probe[0].name = "TEST_NAME"
567+
568+
# Convert to DD 3.40.1
569+
dst = convert_ids(src, "3.40.1")
570+
571+
# DD4 description -> DD3 name
572+
assert dst.b_field_pol_probe[0].name == "TEST_DESCRIPTION"
573+
574+
# DD4 name -> DD3 identifier
575+
assert dst.b_field_pol_probe[0].identifier == "TEST_NAME"
576+
577+
536578
def test_3to4_cocos_hardcoded_paths():
537579
# Check for existence in 3.42.0
538580
factory = IDSFactory("3.42.0")

0 commit comments

Comments
 (0)