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.
- imas/__init__.py +34 -0
- imas/__main__.py +17 -0
- imas/_util.py +260 -0
- imas/_version.py +16 -0
- imas/assets/IDSDef2MDSpreTree.xsl +161 -0
- imas/assets/IDS_fake_toplevel.xml +318 -0
- imas/assets/IDS_minimal.xml +37 -0
- imas/assets/IDS_minimal_2.xml +37 -0
- imas/assets/IDS_minimal_struct_array.xml +43 -0
- imas/assets/IDS_minimal_types.xml +169 -0
- imas/assets/ITER_134173_106_core_profiles.ids +2438 -0
- imas/assets/ITER_134173_106_equilibrium.ids +3172 -0
- imas/assets/README.md +5 -0
- imas/assets/core_profiles.ids +2438 -0
- imas/assets/equilibrium.ids +3172 -0
- imas/backends/__init__.py +12 -0
- imas/backends/db_entry_impl.py +122 -0
- imas/backends/imas_core/__init__.py +4 -0
- imas/backends/imas_core/al_context.py +391 -0
- imas/backends/imas_core/db_entry_al.py +405 -0
- imas/backends/imas_core/db_entry_helpers.py +200 -0
- imas/backends/imas_core/imas_interface.py +216 -0
- imas/backends/imas_core/mdsplus_model.py +367 -0
- imas/backends/imas_core/uda_support.py +56 -0
- imas/backends/netcdf/__init__.py +4 -0
- imas/backends/netcdf/db_entry_nc.py +176 -0
- imas/backends/netcdf/ids2nc.py +306 -0
- imas/backends/netcdf/nc2ids.py +302 -0
- imas/backends/netcdf/nc_metadata.py +357 -0
- imas/backends/netcdf/nc_validate.py +53 -0
- imas/command/cli.py +239 -0
- imas/command/db_analysis.py +290 -0
- imas/command/helpers.py +45 -0
- imas/command/timer.py +77 -0
- imas/db_entry.py +802 -0
- imas/dd_helpers.py +300 -0
- imas/dd_zip.py +297 -0
- imas/exception.py +107 -0
- imas/ids_base.py +149 -0
- imas/ids_convert.py +920 -0
- imas/ids_coordinates.py +400 -0
- imas/ids_data_type.py +115 -0
- imas/ids_defs.py +171 -0
- imas/ids_factory.py +108 -0
- imas/ids_identifiers.py +125 -0
- imas/ids_metadata.py +307 -0
- imas/ids_path.py +254 -0
- imas/ids_primitive.py +510 -0
- imas/ids_struct_array.py +233 -0
- imas/ids_structure.py +331 -0
- imas/ids_toplevel.py +381 -0
- imas/setup_logging.py +64 -0
- imas/test/test_all_dd_versions.py +28 -0
- imas/test/test_cli.py +44 -0
- imas/test/test_dbentry.py +104 -0
- imas/test/test_dd_helpers.py +134 -0
- imas/test/test_dd_zip.py +29 -0
- imas/test/test_deepcopy.py +25 -0
- imas/test/test_exception.py +12 -0
- imas/test/test_get_sample.py +388 -0
- imas/test/test_hash.py +188 -0
- imas/test/test_helpers.py +354 -0
- imas/test/test_identifiers.py +105 -0
- imas/test/test_ids2nc.py +166 -0
- imas/test/test_ids_ascii_data.py +39 -0
- imas/test/test_ids_convert.py +415 -0
- imas/test/test_ids_coordinates.py +165 -0
- imas/test/test_ids_data_type.py +40 -0
- imas/test/test_ids_factory.py +35 -0
- imas/test/test_ids_metadata.py +66 -0
- imas/test/test_ids_mixin.py +20 -0
- imas/test/test_ids_path.py +192 -0
- imas/test/test_ids_primitive.py +109 -0
- imas/test/test_ids_struct_array.py +89 -0
- imas/test/test_ids_structure.py +70 -0
- imas/test/test_ids_toplevel.py +55 -0
- imas/test/test_ids_validate.py +343 -0
- imas/test/test_latest_dd_autofill.py +74 -0
- imas/test/test_latest_dd_manual.py +38 -0
- imas/test/test_latest_dd_resample.py +102 -0
- imas/test/test_lazy_loading.py +201 -0
- imas/test/test_list_occurrences.py +82 -0
- imas/test/test_minimal.py +37 -0
- imas/test/test_minimal_conversion.py +44 -0
- imas/test/test_minimal_io.py +21 -0
- imas/test/test_minimal_struct_array_io.py +60 -0
- imas/test/test_minimal_types.py +436 -0
- imas/test/test_minimal_types_io.py +70 -0
- imas/test/test_minimal_types_io_automatic.py +66 -0
- imas/test/test_nbc_change.py +408 -0
- imas/test/test_nc_autofill.py +13 -0
- imas/test/test_nc_entry.py +28 -0
- imas/test/test_nc_metadata.py +153 -0
- imas/test/test_nc_validation.py +197 -0
- imas/test/test_snippets.py +25 -0
- imas/test/test_static_ids.py +39 -0
- imas/test/test_str_1d.py +99 -0
- imas/test/test_time_slicing.py +182 -0
- imas/test/test_uda_support.py +12 -0
- imas/test/test_util.py +209 -0
- imas/training.py +45 -0
- imas/util.py +526 -0
- imas_python-0.1.dev1618.dist-info/LICENSE.txt +165 -0
- imas_python-0.1.dev1618.dist-info/METADATA +288 -0
- imas_python-0.1.dev1618.dist-info/RECORD +108 -0
- imas_python-0.1.dev1618.dist-info/WHEEL +5 -0
- imas_python-0.1.dev1618.dist-info/entry_points.txt +3 -0
- 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)
|
imas/command/helpers.py
ADDED
|
@@ -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)
|