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,53 @@
1
+ from imas.backends.netcdf.db_entry_nc import NCDBEntryImpl
2
+ from imas.backends.netcdf.nc2ids import NC2IDS
3
+ from imas.db_entry import DBEntry
4
+ from imas.exception import InvalidNetCDFEntry
5
+
6
+
7
+ def validate_netcdf_file(filename: str) -> None:
8
+ """Validate if the provided netCDF file adheres to the IMAS conventions."""
9
+ if not filename.endswith(".nc"):
10
+ raise InvalidNetCDFEntry(
11
+ f"Invalid filename `{filename}` provided: "
12
+ "an IMAS netCDF file should end with `.nc`"
13
+ )
14
+
15
+ with DBEntry(filename, "r") as entry:
16
+ entry_impl: NCDBEntryImpl = entry._dbe_impl
17
+ dataset = entry_impl._dataset
18
+ factory = entry_impl._ds_factory
19
+
20
+ ids_names = factory.ids_names()
21
+
22
+ # Check that groups in the dataset correspond to an IDS/occurrence and no
23
+ # additional variables are smuggled inside:
24
+ groups = [dataset] + [dataset[group] for group in dataset.groups]
25
+ for group in groups:
26
+ if group.variables or group.dimensions:
27
+ raise InvalidNetCDFEntry(
28
+ "NetCDF file should not have variables or dimensions in the "
29
+ f"{group.name} group."
30
+ )
31
+ if group is dataset:
32
+ continue
33
+ if group.name not in ids_names:
34
+ raise InvalidNetCDFEntry(
35
+ f"Invalid group name {group.name}: there is no IDS with this name."
36
+ )
37
+ for subgroup in group.groups:
38
+ try:
39
+ int(subgroup)
40
+ except ValueError:
41
+ raise InvalidNetCDFEntry(
42
+ f"Invalid group name {group.name}/{subgroup}: "
43
+ f"{subgroup} is not a valid occurrence number."
44
+ )
45
+
46
+ for ids_name in ids_names:
47
+ for occurrence in entry.list_all_occurrences(ids_name):
48
+ group = dataset[f"{ids_name}/{occurrence}"]
49
+ try:
50
+ NC2IDS(group, factory.new(ids_name)).validate_variables()
51
+ except InvalidNetCDFEntry as exc:
52
+ occ = f":{occurrence}" if occurrence else ""
53
+ raise InvalidNetCDFEntry(f"Invalid IDS {ids_name}{occ}: {exc}")
imas/command/cli.py ADDED
@@ -0,0 +1,239 @@
1
+ # This file is part of imas-python.
2
+ # You should have received the imas-python LICENSE file with this project.
3
+ """ Main CLI entry point """
4
+
5
+ import logging
6
+ import sys
7
+ from contextlib import ExitStack
8
+ from pathlib import Path
9
+
10
+ import click
11
+ from packaging.version import Version
12
+ from rich import box, console, traceback
13
+ from rich.progress import (
14
+ BarColumn,
15
+ Progress,
16
+ SpinnerColumn,
17
+ TaskProgressColumn,
18
+ TextColumn,
19
+ TimeElapsedColumn,
20
+ )
21
+ from rich.table import Table
22
+
23
+ import imas
24
+ import imas.backends.imas_core.imas_interface
25
+ from imas import DBEntry, dd_zip
26
+ from imas.backends.imas_core.imas_interface import ll_interface
27
+ from imas.command.db_analysis import analyze_db, process_db_analysis
28
+ from imas.command.helpers import min_version_guard, setup_rich_log_handler
29
+ from imas.command.timer import Timer
30
+ from imas.exception import UnknownDDVersion
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ def _excepthook(type_, value, tb):
36
+ logger.debug("Suppressed traceback:", exc_info=(type_, value, tb))
37
+ # Only display the last traceback frame:
38
+ if tb is not None:
39
+ while tb.tb_next:
40
+ tb = tb.tb_next
41
+ rich_tb = traceback.Traceback.from_exception(type_, value, tb, extra_lines=0)
42
+ console.Console(stderr=True).print(rich_tb)
43
+
44
+
45
+ @click.group("imas", invoke_without_command=True, no_args_is_help=True)
46
+ def cli():
47
+ """imas-python command line interface.
48
+
49
+ Please use one of the available commands listed below. You can get help for each
50
+ command by executing:
51
+
52
+ imas <command> --help
53
+ """
54
+ # Limit the traceback to 1 item: avoid scaring CLI users with long traceback prints
55
+ # and let them focus on the actual error message
56
+ sys.excepthook = _excepthook
57
+
58
+
59
+ cli.add_command(analyze_db)
60
+ cli.add_command(process_db_analysis)
61
+
62
+
63
+ @cli.command("version")
64
+ def print_version():
65
+ """Print version information of imas-python."""
66
+ cons = console.Console()
67
+ grid = Table(
68
+ title="imas-python version info", show_header=False, title_style="bold"
69
+ )
70
+ grid.box = box.HORIZONTALS
71
+ if cons.size.width > 120:
72
+ grid.width = 120
73
+ grid.add_row("imas-python version:", imas.__version__)
74
+ grid.add_section()
75
+ grid.add_row("Default data dictionary version:", imas.IDSFactory().dd_version)
76
+ dd_versions = ", ".join(imas.dd_zip.dd_xml_versions())
77
+ grid.add_row("Available data dictionary versions:", dd_versions)
78
+ grid.add_section()
79
+ grid.add_row("Access Layer core version:", ll_interface.get_al_version() or "N/A")
80
+ console.Console().print(grid)
81
+
82
+
83
+ @cli.command("print", no_args_is_help=True)
84
+ @click.argument("uri")
85
+ @click.argument("ids")
86
+ @click.argument("occurrence", default=0)
87
+ @click.option(
88
+ "--all",
89
+ "-a",
90
+ "print_all",
91
+ is_flag=True,
92
+ help="Also show nodes with empty/default values",
93
+ )
94
+ def print_ids(uri, ids, occurrence, print_all):
95
+ """Pretty print the contents of an IDS.
96
+
97
+ \b
98
+ uri URI of the Data Entry (e.g. "imas:mdsplus?path=testdb").
99
+ ids Name of the IDS to print (e.g. "core_profiles").
100
+ occurrence Which occurrence to print (defaults to 0).
101
+ """
102
+ min_version_guard(Version("5.0"))
103
+ setup_rich_log_handler(False)
104
+
105
+ with DBEntry(uri, "r") as dbentry:
106
+ ids_obj = dbentry.get(ids, occurrence, autoconvert=False)
107
+ imas.util.print_tree(ids_obj, not print_all)
108
+
109
+
110
+ @cli.command("convert", no_args_is_help=True)
111
+ @click.argument("uri_in")
112
+ @click.argument("dd_version")
113
+ @click.argument("uri_out")
114
+ @click.option(
115
+ "--ids",
116
+ default="*",
117
+ help="Specify which IDS to convert. \
118
+ If not provided, all IDSs in the data entry are converted.",
119
+ )
120
+ @click.option("--occurrence", default=-1, help="Specify which occurrence to convert.")
121
+ @click.option("--quiet", "-q", is_flag=True, help="Suppress progress output.")
122
+ @click.option("--timeit", is_flag=True, help="Show timing information.")
123
+ @click.option(
124
+ "--no-provenance",
125
+ is_flag=True,
126
+ help="Don't add provenance metadata to the converted IDS.",
127
+ )
128
+ def convert_ids(
129
+ uri_in, dd_version, uri_out, ids, occurrence, quiet, timeit, no_provenance
130
+ ):
131
+ """Convert a Data Entry (or a single IDS) to the target DD version.
132
+
133
+ Provide a different backend to URI_OUT than URI_IN to convert between backends.
134
+ For example:
135
+
136
+ imas convert imas:mdsplus?path=db-in 3.41.0 imas:hdf5?path=db-out
137
+
138
+ \b
139
+ uri_in URI of the input Data Entry.
140
+ dd_version Data dictionary version to convert to. Can also be the path to an
141
+ IDSDef.xml to convert to a custom/unreleased DD version.
142
+ uri_out URI of the output Data Entry.
143
+ """
144
+ min_version_guard(Version("5.1"))
145
+ setup_rich_log_handler(quiet)
146
+
147
+ # Check if we can load the requested version
148
+ if dd_version in dd_zip.dd_xml_versions():
149
+ version_params = dict(dd_version=dd_version)
150
+ elif Path(dd_version).exists():
151
+ version_params = dict(xml_path=dd_version)
152
+ else:
153
+ raise UnknownDDVersion(dd_version, dd_zip.dd_xml_versions())
154
+
155
+ provenance_origin_uri = ""
156
+ if not no_provenance:
157
+ provenance_origin_uri = uri_in
158
+
159
+ # Use an ExitStack to avoid three nested with-statements
160
+ with ExitStack() as stack:
161
+ entry_in = stack.enter_context(DBEntry(uri_in, "r"))
162
+ entry_out = stack.enter_context(DBEntry(uri_out, "x", **version_params))
163
+
164
+ # First build IDS/occurrence list so we can show a decent progress bar
165
+ ids_list = [ids] if ids != "*" else entry_out.factory.ids_names()
166
+ idss_with_occurrences = []
167
+ for ids_name in ids_list:
168
+ if occurrence == -1:
169
+ idss_with_occurrences.extend(
170
+ (ids_name, occ) for occ in entry_in.list_all_occurrences(ids_name)
171
+ )
172
+ else:
173
+ idss_with_occurrences.append((ids_name, occurrence))
174
+
175
+ # Create progress bar and task
176
+ columns = (
177
+ TimeElapsedColumn(),
178
+ BarColumn(),
179
+ TaskProgressColumn(),
180
+ TextColumn("[progress.description]{task.description}"),
181
+ SpinnerColumn("simpleDots", style="[white]"),
182
+ )
183
+ progress = stack.enter_context(Progress(*columns, disable=quiet))
184
+ task = progress.add_task("Converting", total=len(idss_with_occurrences) * 3)
185
+ # Create timer for timing get/convert/put
186
+ timer = Timer("Operation", "IDS/occurrence")
187
+
188
+ # Convert all IDSs
189
+ for ids_name, occurrence in idss_with_occurrences:
190
+ name = f"[bold green]{ids_name}[/][green]/{occurrence}[/]"
191
+
192
+ progress.update(task, description=f"Reading {name}")
193
+ with timer("Get", name):
194
+ ids = entry_in.get(ids_name, occurrence, autoconvert=False)
195
+
196
+ progress.update(task, description=f"Converting {name}", advance=1)
197
+ # Explicitly convert instead of auto-converting during put. This is a bit
198
+ # slower, but gives better diagnostics:
199
+ if ids._dd_version == entry_out.dd_version:
200
+ ids2 = ids
201
+ else:
202
+ with timer("Convert", name):
203
+ ids2 = imas.convert_ids(
204
+ ids,
205
+ None,
206
+ factory=entry_out.factory,
207
+ provenance_origin_uri=provenance_origin_uri,
208
+ )
209
+
210
+ # Store in output entry:
211
+ progress.update(task, description=f"Storing {name}", advance=1)
212
+ with timer("Put", name):
213
+ entry_out.put(ids2, occurrence)
214
+
215
+ # Update progress bar
216
+ progress.update(task, advance=1)
217
+
218
+ # Display timing information
219
+ if timeit:
220
+ console.Console().print(timer.get_table("Time required per IDS"))
221
+
222
+
223
+ @cli.command("validate_nc", no_args_is_help=True)
224
+ @click.argument("filename", type=click.Path(exists=True, dir_okay=False))
225
+ def validate_nc(filename):
226
+ """Validate if the provided netCDF file adheres to the IMAS conventions."""
227
+ from imas.backends.netcdf.nc_validate import validate_netcdf_file
228
+
229
+ try:
230
+ validate_netcdf_file(filename)
231
+ except Exception as exc:
232
+ click.echo(f"File `{filename}` does not adhere to the IMAS conventions:")
233
+ click.echo(exc)
234
+ sys.exit(1)
235
+ click.echo(f"File `{filename}` is a valid IMAS netCDF file.")
236
+
237
+
238
+ if __name__ == "__main__":
239
+ cli()
@@ -0,0 +1,290 @@
1
+ """imas-python-based command line tool for analysing fields in a database."""
2
+
3
+ import gzip
4
+ import json
5
+ import logging
6
+ import platform
7
+ import re
8
+ import readline
9
+ import sys
10
+ from dataclasses import dataclass, field
11
+ from pathlib import Path
12
+ from typing import Dict, Iterable, List, Optional
13
+
14
+ import click
15
+ import rich
16
+ import rich.panel
17
+ import rich.progress
18
+ import rich.table
19
+ import rich.text
20
+ import rich.tree
21
+
22
+ import imas
23
+ from imas.command.helpers import setup_rich_log_handler
24
+ from imas.ids_metadata import IDSMetadata
25
+
26
+ directory_path = click.Path(exists=True, file_okay=False, path_type=Path)
27
+ outfile_path = click.Path(dir_okay=False, writable=True, path_type=Path)
28
+ infile_path = click.Path(exists=True, dir_okay=False, readable=True, path_type=Path)
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ @click.command("analyze-db")
34
+ @click.argument("dbentry", nargs=-1, type=directory_path)
35
+ @click.option(
36
+ "--output",
37
+ "-o",
38
+ type=outfile_path,
39
+ default="imas-db-analysis.json.gz",
40
+ help="Output file",
41
+ )
42
+ def analyze_db(dbentry: Iterable[Path], output: Path) -> None:
43
+ """Analyze one or more Data Entries stored in the HDF5 backend format.
44
+
45
+ This analysis will collect the following data:
46
+
47
+ \b
48
+ - The (host)name of the machine
49
+ - The path to the Data Entry
50
+ - Which IDSs are stored in the Data Entry
51
+ - Which fields in the IDSs are filled
52
+
53
+ \b
54
+ Arguments:
55
+ DBENTRY Folder(s) containing IMAS data stored in the HDF5 backend format.
56
+
57
+ \b
58
+ Notes:
59
+ 1. This tool does not accept IMAS URIs, only folder names. To avoid loading all
60
+ data, the IDSs are inspected by looking at the HDF5 files directly.
61
+ 2. This tool uses the optional `h5py` dependency. An error is raised when this
62
+ package is not available.
63
+ 3. If your data is stored in another format than HDF5, you may use `imas convert`
64
+ to convert the data into the HDF5 backend format first.
65
+ """
66
+ # Test if h5py is available
67
+ try:
68
+ import h5py # noqa
69
+ except ModuleNotFoundError:
70
+ rich.print(
71
+ "[red]Module [bold]h5py[/bold] is not available.[/]\n"
72
+ "Please load the [bold]h5py[/] HPC module, or install "
73
+ "[bold]h5py[/] with `[bold]pip install h5py[/bold]`.",
74
+ file=sys.stderr,
75
+ )
76
+ sys.exit(1)
77
+ setup_rich_log_handler(False)
78
+
79
+ # Load existing data?
80
+ if output.exists():
81
+ logger.info("Existing data found. New data will be appended to it.")
82
+ try:
83
+ with gzip.open(output, "rt", encoding="utf-8") as file:
84
+ data = json.load(file)
85
+ except Exception:
86
+ logger.error("Could not read data from '%s'.", output, exc_info=True)
87
+ logger.error("Please remove the file '%s' and try again.", output)
88
+ sys.exit(1)
89
+ else:
90
+ data = []
91
+
92
+ # Analyze provided paths
93
+ for entry in rich.progress.track(dbentry, transient=True):
94
+ # Check if a master.h5 is present
95
+ if not (entry / "master.h5").exists:
96
+ logger.error("Could not find '%s/master.h5', skipping this folder.", entry)
97
+ continue
98
+ result = analyze_folder(entry)
99
+ data.append(result)
100
+
101
+ if data:
102
+ with gzip.open(output, "wt", encoding="utf-8") as file:
103
+ json.dump(data, file)
104
+ logger.info(f"Output data is stored in {output}")
105
+
106
+
107
+ def analyze_folder(entry: Path):
108
+ """Analyze a folder with HDF5 files, returning a structure containing IDS usage."""
109
+ return {
110
+ "machine_name": platform.node(),
111
+ "dbentry_path": str(entry),
112
+ "ids_info": [
113
+ ids_info(idsfile)
114
+ for idsfile in entry.glob("*.h5")
115
+ if idsfile.stem != "master"
116
+ ],
117
+ }
118
+
119
+
120
+ def ids_info(idsfile: Path):
121
+ """Open HDF5 ids file and return structure containing the filled data paths."""
122
+ import h5py
123
+
124
+ name_and_occurrence = idsfile.stem
125
+ name, occurrence = re.split("_(?=[0-9]+)|$", name_and_occurrence, 1)
126
+ filled_data = []
127
+ with h5py.File(idsfile, "r") as f:
128
+ group = f[name_and_occurrence]
129
+ for dataset in group.keys():
130
+ if dataset.endswith("SHAPE"):
131
+ continue
132
+ path = dataset.replace("[", "").replace("]", "").replace("&", "/")
133
+ filled_data.append(path)
134
+
135
+ return {
136
+ "name": name,
137
+ "occurrence": occurrence,
138
+ "filled_data": filled_data,
139
+ }
140
+
141
+
142
+ @click.command("process-db-analysis")
143
+ @click.argument(
144
+ "infiles", metavar="INPUT_FILES...", nargs=-1, type=infile_path, required=True
145
+ )
146
+ @click.option("--show-empty-ids", is_flag=True, help="Show empty IDSs in the overview.")
147
+ def process_db_analysis(infiles, show_empty_ids):
148
+ """Process supplied Data Entry analyses, and display statistics.
149
+
150
+ \b
151
+ Arguments:
152
+ INPUT_FILES File(s) produced by `imas analyze-db` to process.
153
+ """
154
+ setup_rich_log_handler(False)
155
+
156
+ factory = imas.IDSFactory()
157
+ filled_per_ids = {ids_name: set() for ids_name in factory.ids_names()}
158
+ logger.info("Using Data Dictionary version %s.", factory.dd_version)
159
+ logger.info("Reading %d input files...", len(infiles))
160
+
161
+ # Read input data and collate usage info per IDS
162
+ for fname in infiles:
163
+ with gzip.open(fname, "rt", encoding="utf-8") as file:
164
+ data = json.load(file)
165
+
166
+ for entry in data:
167
+ for ids_info in entry["ids_info"]:
168
+ fill_info = filled_per_ids[ids_info["name"]]
169
+ fill_info.update(ids_info["filled_data"])
170
+
171
+ logger.info("Done reading input files.")
172
+ logger.info("Analyzing filled data...")
173
+
174
+ # Construct AnalysisNodes per IDS
175
+ analysis_nodes: Dict[str, _AnalysisNode] = {}
176
+ for ids_name, filled in filled_per_ids.items():
177
+ metadata = factory.new(ids_name).metadata
178
+ ids_analysis_node = _AnalysisNode("")
179
+
180
+ def walk_metadata_tree(metadata: IDSMetadata, node: _AnalysisNode):
181
+ """Recursively walk the IDSMetadata tree to check which nodes are filled."""
182
+ for meta_child in metadata._children.values():
183
+ path = meta_child.path_string
184
+ childnode = _AnalysisNode(path, path in filled)
185
+ node.children.append(childnode)
186
+
187
+ if meta_child._children: # Childnode is a structure/AoS itself
188
+ # Recurse
189
+ walk_metadata_tree(meta_child, childnode)
190
+ # Update node statistics
191
+ node.num_desc_nodes += childnode.num_desc_nodes
192
+ node.num_desc_nodes_filled += childnode.num_desc_nodes_filled
193
+ else: # Childnode is a data node
194
+ # Update node statistics
195
+ node.num_desc_nodes += 1
196
+ if childnode.used:
197
+ node.num_desc_nodes_filled += 1
198
+ node.used = node.num_desc_nodes_filled > 0
199
+
200
+ walk_metadata_tree(metadata, ids_analysis_node)
201
+ analysis_nodes[ids_name] = ids_analysis_node
202
+
203
+ # Display summary results per IDS
204
+ logger.info("Analysis done. Printing results:")
205
+ sorted_ids_names = sorted(
206
+ analysis_nodes,
207
+ key=lambda ids_name: -analysis_nodes[ids_name].fill_fraction,
208
+ )
209
+ table = rich.table.Table("IDS", "Filled nodes", caption="Usage summary")
210
+ for ids_name in sorted_ids_names:
211
+ node = analysis_nodes[ids_name]
212
+ if node.num_desc_nodes_filled == 0 and not show_empty_ids:
213
+ continue # hide IDSs without data
214
+ dim = "" if node.num_desc_nodes_filled > 0 else "[dim]"
215
+ table.add_row(
216
+ f"{dim}[bold]{ids_name}",
217
+ f"{dim}{node.num_desc_nodes_filled: >4} / {node.num_desc_nodes: >4} "
218
+ f"({node.fill_fraction: >6.2%})",
219
+ )
220
+ rich.print(table)
221
+
222
+ # Use readline for autocompleting IDS names when pressing <tab>
223
+ def rlcompleter(text, state) -> Optional[str]:
224
+ matching_idss = [name for name in sorted_ids_names if name.startswith(text)]
225
+ if state < len(matching_idss):
226
+ return matching_idss[state]
227
+ return None
228
+
229
+ readline.set_completer(rlcompleter)
230
+ readline.parse_and_bind("tab: complete")
231
+
232
+ # Display input prompt for detailed results per IDS
233
+ while True:
234
+ # click.prompt doesn't work nicely with readline, so use builtin input():
235
+ ids_name = input("Enter IDS name to show detailed usage [exit]: ").strip()
236
+
237
+ if ids_name == "exit" or not ids_name:
238
+ break
239
+ elif ids_name not in analysis_nodes:
240
+ rich.print(f"[red]Unknown IDS name: [bold]{ids_name}")
241
+ continue
242
+
243
+ node = analysis_nodes[ids_name]
244
+ tree = rich.tree.Tree(
245
+ f"[bold red]{ids_name}[/]: [bold cyan]{node.num_desc_nodes_filled}[/]/"
246
+ f"[bold cyan]{node.num_desc_nodes}[/] data nodes filled"
247
+ )
248
+ for childnode in node.children:
249
+ childnode.fill_tree(tree)
250
+ rich.print(rich.panel.Panel(tree, expand=False))
251
+
252
+
253
+ @dataclass
254
+ class _AnalysisNode:
255
+ """Data class to store analysis results for a node in an IDS."""
256
+
257
+ path: str
258
+ """Path of this node, e.g. "ids_properties/comment"."""
259
+ used: bool = False
260
+ """True when any analysed Data Entry had this node populated."""
261
+ children: List["_AnalysisNode"] = field(repr=False, default_factory=list)
262
+ """List of child nodes (when this is a structure or AoS node)."""
263
+ num_desc_nodes = 0
264
+ """Total descendent data (i.e. no structure/AoS) nodes."""
265
+ num_desc_nodes_filled = 0
266
+ """Total number of descendent data nodes that are used."""
267
+
268
+ @property
269
+ def fill_fraction(self):
270
+ """The fill fraction of all descendent nodes."""
271
+ return self.num_desc_nodes_filled / self.num_desc_nodes
272
+
273
+ def fill_tree(self, tree: rich.tree.Tree):
274
+ """Fill provided rich.tree.Tree with analysis data."""
275
+ if not self.children: # leaf node
276
+ if not self.used:
277
+ return # skip empty leaf nodes
278
+ label = f"[green]{self.path}[/] is used"
279
+ else:
280
+ label = (
281
+ f"[blue]{self.path}[/]: [bold cyan]{self.num_desc_nodes_filled}[/]/"
282
+ f"[bold cyan]{self.num_desc_nodes}[/] descendent nodes filled"
283
+ )
284
+ if self.num_desc_nodes_filled == 0:
285
+ label = rich.text.Text.from_markup(label, style="dim")
286
+
287
+ subtree = tree.add(label)
288
+ if self.used:
289
+ for child in self.children:
290
+ child.fill_tree(subtree)
@@ -0,0 +1,45 @@
1
+ import logging
2
+ import sys
3
+
4
+ import click
5
+ from packaging.version import Version
6
+ from rich.logging import RichHandler
7
+
8
+ from imas.backends.imas_core.imas_interface import ll_interface
9
+
10
+
11
+ def setup_rich_log_handler(quiet: bool):
12
+ """Setup rich.logging.RichHandler on the root logger.
13
+
14
+ Args:
15
+ quiet: When True: set log level of the `imas` logger to WARNING or higher.
16
+ """
17
+ # Disable default imas log handler
18
+ imas_logger = logging.getLogger("imas")
19
+ for handler in imas_logger.handlers:
20
+ imas_logger.removeHandler(handler)
21
+ # Disable any root log handlers
22
+ root_logger = logging.getLogger()
23
+ for handler in root_logger.handlers:
24
+ root_logger.removeHandler(handler)
25
+ # Install rich handler on the root logger:
26
+ root_logger.addHandler(RichHandler())
27
+ if quiet: # Silence imas-python INFO messages
28
+ # If loglevel is less than WARNING, set it to WARNING:
29
+ imas_logger.setLevel(max(logging.WARNING, imas_logger.getEffectiveLevel()))
30
+
31
+
32
+ def min_version_guard(al_version: Version):
33
+ """Print an error message if the loaded AL version is too old.
34
+
35
+ Args:
36
+ al_version: Minimum imas_core version required for this command.
37
+ """
38
+ used_version = ll_interface._al_version
39
+ if used_version >= al_version:
40
+ return
41
+ click.echo(
42
+ f"This command requires at least version {al_version} of the Access Layer."
43
+ )
44
+ click.echo(f"The current loaded version is {used_version}, which is too old.")
45
+ sys.exit(1)