3
3
from typing import Iterator , List , Optional , Tuple
4
4
5
5
import netCDF4
6
+ import numpy as np
6
7
7
8
from imas .backends .netcdf import ids2nc
8
9
from imas .backends .netcdf .nc_metadata import NCMetadata
9
10
from imas .exception import InvalidNetCDFEntry
10
11
from imas .ids_base import IDSBase
12
+ from imas .ids_convert import NBCPathMap
11
13
from imas .ids_data_type import IDSDataType
12
14
from imas .ids_defs import IDS_TIME_MODE_HOMOGENEOUS
13
15
from imas .ids_metadata import IDSMetadata
@@ -70,22 +72,37 @@ def _tree_iter(
70
72
class NC2IDS :
71
73
"""Class responsible for reading an IDS from a NetCDF group."""
72
74
73
- def __init__ (self , group : netCDF4 .Group , ids : IDSToplevel ) -> None :
75
+ def __init__ (
76
+ self ,
77
+ group : netCDF4 .Group ,
78
+ ids : IDSToplevel ,
79
+ ids_metadata : IDSMetadata ,
80
+ nbc_map : Optional [NBCPathMap ],
81
+ ) -> None :
74
82
"""Initialize NC2IDS converter.
75
83
76
84
Args:
77
85
group: NetCDF group that stores the IDS data.
78
86
ids: Corresponding IDS toplevel to store the data in.
87
+ ids_metadata: Metadata corresponding to the DD version that the data is
88
+ stored in.
89
+ nbc_map: Path map for implicit DD conversions.
79
90
"""
80
91
self .group = group
81
92
"""NetCDF Group that the IDS is stored in."""
82
93
self .ids = ids
83
94
"""IDS to store the data in."""
95
+ self .ids_metadata = ids_metadata
96
+ """Metadata of the IDS in the DD version that the data is stored in"""
97
+ self .nbc_map = nbc_map
98
+ """Path map for implicit DD conversions."""
84
99
85
- self .ncmeta = NCMetadata (ids . metadata )
100
+ self .ncmeta = NCMetadata (ids_metadata )
86
101
"""NetCDF related metadata."""
87
102
self .variables = list (group .variables )
88
103
"""List of variable names stored in the netCDF group."""
104
+
105
+ self ._lazy_map = {}
89
106
# Don't use masked arrays: they're slow and we'll handle most of the unset
90
107
# values through the `:shape` arrays
91
108
self .group .set_auto_mask (False )
@@ -99,31 +116,60 @@ def __init__(self, group: netCDF4.Group, ids: IDSToplevel) -> None:
99
116
"Mandatory variable `ids_properties.homogeneous_time` does not exist."
100
117
)
101
118
var = group ["ids_properties.homogeneous_time" ]
102
- self ._validate_variable (var , ids .ids_properties . homogeneous_time . metadata )
119
+ self ._validate_variable (var , ids .metadata [ " ids_properties/ homogeneous_time" ] )
103
120
if var [()] not in [0 , 1 , 2 ]:
104
121
raise InvalidNetCDFEntry (
105
122
f"Invalid value for ids_properties.homogeneous_time: { var [()]} . "
106
123
"Was expecting: 0, 1 or 2."
107
124
)
108
125
self .homogeneous_time = var [()] == IDS_TIME_MODE_HOMOGENEOUS
109
126
110
- def run (self ) -> None :
127
+ def run (self , lazy : bool ) -> None :
111
128
"""Load the data from the netCDF group into the IDS."""
112
129
self .variables .sort ()
113
130
self .validate_variables ()
131
+ if lazy :
132
+ self .ids ._set_lazy_context (LazyContext (self ))
114
133
for var_name in self .variables :
115
134
if var_name .endswith (":shape" ):
116
135
continue
117
- metadata = self .ids . metadata [var_name ]
136
+ metadata = self .ids_metadata [var_name ]
118
137
119
138
if metadata .data_type is IDSDataType .STRUCTURE :
120
139
continue # This only contains DD metadata we already know
121
140
141
+ # Handle implicit DD version conversion
142
+ if self .nbc_map is None :
143
+ target_metadata = metadata # no conversion
144
+ elif metadata .path_string in self .nbc_map :
145
+ new_path = self .nbc_map .path [metadata .path_string ]
146
+ if new_path is None :
147
+ logging .info (
148
+ "Not loading data for %s: no equivalent data structure exists "
149
+ "in the target Data Dictionary version." ,
150
+ metadata .path_string ,
151
+ )
152
+ continue
153
+ target_metadata = self .ids .metadata [new_path ]
154
+ elif metadata .path_string in self .nbc_map .type_change :
155
+ logging .info (
156
+ "Not loading data for %s: cannot hanlde type changes when "
157
+ "implicitly converting data to the target Data Dictionary version." ,
158
+ metadata .path_string ,
159
+ )
160
+ continue
161
+ else :
162
+ target_metadata = metadata # no conversion required
163
+
122
164
var = self .group [var_name ]
165
+ if lazy :
166
+ self ._lazy_map [target_metadata .path_string ] = var
167
+ continue
168
+
123
169
if metadata .data_type is IDSDataType .STRUCT_ARRAY :
124
170
if "sparse" in var .ncattrs ():
125
171
shapes = self .group [var_name + ":shape" ][()]
126
- for index , node in tree_iter (self .ids , metadata ):
172
+ for index , node in tree_iter (self .ids , target_metadata ):
127
173
node .resize (shapes [index ][0 ])
128
174
129
175
else :
@@ -132,7 +178,7 @@ def run(self) -> None:
132
178
metadata .path_string , self .homogeneous_time
133
179
)[- 1 ]
134
180
size = self .group .dimensions [dim ].size
135
- for _ , node in tree_iter (self .ids , metadata ):
181
+ for _ , node in tree_iter (self .ids , target_metadata ):
136
182
node .resize (size )
137
183
138
184
continue
@@ -144,23 +190,30 @@ def run(self) -> None:
144
190
if "sparse" in var .ncattrs ():
145
191
if metadata .ndim :
146
192
shapes = self .group [var_name + ":shape" ][()]
147
- for index , node in tree_iter (self .ids , metadata ):
193
+ for index , node in tree_iter (self .ids , target_metadata ):
148
194
shape = shapes [index ]
149
195
if shape .all ():
150
- node .value = data [index + tuple (map (slice , shapes [index ]))]
196
+ # NOTE: bypassing IDSPrimitive.value.setter logic
197
+ node ._IDSPrimitive__value = data [
198
+ index + tuple (map (slice , shape ))
199
+ ]
151
200
else :
152
- for index , node in tree_iter (self .ids , metadata ):
201
+ for index , node in tree_iter (self .ids , target_metadata ):
153
202
value = data [index ]
154
203
if value != getattr (var , "_FillValue" , None ):
155
- node .value = data [index ]
204
+ # NOTE: bypassing IDSPrimitive.value.setter logic
205
+ node ._IDSPrimitive__value = value
156
206
157
207
elif metadata .path_string not in self .ncmeta .aos :
158
208
# Shortcut for assigning untensorized data
159
- self .ids [metadata .path ] = data
209
+ # Note: var[()] can return 0D numpy arrays. Instead of handling this
210
+ # here, we'll let IDSPrimitive.value.setter take care of it:
211
+ self .ids [target_metadata .path ].value = data
160
212
161
213
else :
162
- for index , node in tree_iter (self .ids , metadata ):
163
- node .value = data [index ]
214
+ for index , node in tree_iter (self .ids , target_metadata ):
215
+ # NOTE: bypassing IDSPrimitive.value.setter logic
216
+ node ._IDSPrimitive__value = data [index ]
164
217
165
218
def validate_variables (self ) -> None :
166
219
"""Validate that all variables in the netCDF Group exist and match the DD."""
@@ -194,7 +247,7 @@ def validate_variables(self) -> None:
194
247
# Check that the DD defines this variable, and validate its metadata
195
248
var = self .group [var_name ]
196
249
try :
197
- metadata = self .ids . metadata [var_name ]
250
+ metadata = self .ids_metadata [var_name ]
198
251
except KeyError :
199
252
raise InvalidNetCDFEntry (
200
253
f"Invalid variable { var_name } : no such variable exists in the "
@@ -300,3 +353,69 @@ def _validate_sparsity(
300
353
raise variable_error (
301
354
shape_var , "dtype" , shape_var .dtype , "any integer type"
302
355
)
356
+
357
+
358
+ class LazyContext :
359
+ def __init__ (self , nc2ids , index = ()):
360
+ self .nc2ids = nc2ids
361
+ self .index = index
362
+
363
+ def get_child (self , child ):
364
+ metadata = child .metadata
365
+ path = metadata .path_string
366
+ data_type = metadata .data_type
367
+ nc2ids = self .nc2ids
368
+ var = nc2ids ._lazy_map .get (path )
369
+
370
+ if data_type is IDSDataType .STRUCT_ARRAY :
371
+ # Determine size of the aos
372
+ if var is None :
373
+ size = 0
374
+ elif "sparse" in var .ncattrs ():
375
+ size = nc2ids .group [var .name + ":shape" ][self .index ][0 ]
376
+ else :
377
+ # FIXME: extract dimension name from nc file?
378
+ dim = nc2ids .ncmeta .get_dimensions (
379
+ metadata .path_string , nc2ids .homogeneous_time
380
+ )[- 1 ]
381
+ size = nc2ids .group .dimensions [dim ].size
382
+
383
+ child ._set_lazy_context (LazyArrayStructContext (nc2ids , self .index , size ))
384
+
385
+ elif data_type is IDSDataType .STRUCTURE :
386
+ child ._set_lazy_context (self )
387
+
388
+ elif var is not None : # Data elements
389
+ value = None
390
+ if "sparse" in var .ncattrs ():
391
+ if metadata .ndim :
392
+ shape_var = nc2ids .group [var .name + ":shape" ]
393
+ shape = shape_var [self .index ]
394
+ if shape .all ():
395
+ value = var [self .index + tuple (map (slice , shape ))]
396
+ else :
397
+ value = var [self .index ]
398
+ if value == getattr (var , "_FillValue" , None ):
399
+ value = None # Skip setting
400
+ else :
401
+ value = var [self .index ]
402
+
403
+ if value is not None :
404
+ if isinstance (value , np .ndarray ):
405
+ # Convert the numpy array to a read-only view
406
+ value = value .view ()
407
+ value .flags .writeable = False
408
+ # NOTE: bypassing IDSPrimitive.value.setter logic
409
+ child ._IDSPrimitive__value = value
410
+
411
+
412
+ class LazyArrayStructContext (LazyContext ):
413
+ def __init__ (self , nc2ids , index , size ):
414
+ super ().__init__ (nc2ids , index )
415
+ self .size = size
416
+
417
+ def get_context (self ):
418
+ return self # IDSStructArray expects to get something with a size attribute
419
+
420
+ def iterate_to_index (self , index : int ) -> LazyContext :
421
+ return LazyContext (self .nc2ids , self .index + (index ,))
0 commit comments