33from typing import Iterator , List , Optional , Tuple
44
55import netCDF4
6+ import numpy as np
67
78from imas .backends .netcdf import ids2nc
89from imas .backends .netcdf .nc_metadata import NCMetadata
910from imas .exception import InvalidNetCDFEntry
1011from imas .ids_base import IDSBase
12+ from imas .ids_convert import NBCPathMap
1113from imas .ids_data_type import IDSDataType
1214from imas .ids_defs import IDS_TIME_MODE_HOMOGENEOUS
1315from imas .ids_metadata import IDSMetadata
@@ -70,22 +72,37 @@ def _tree_iter(
7072class NC2IDS :
7173 """Class responsible for reading an IDS from a NetCDF group."""
7274
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 :
7482 """Initialize NC2IDS converter.
7583
7684 Args:
7785 group: NetCDF group that stores the IDS data.
7886 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.
7990 """
8091 self .group = group
8192 """NetCDF Group that the IDS is stored in."""
8293 self .ids = ids
8394 """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."""
8499
85- self .ncmeta = NCMetadata (ids . metadata )
100+ self .ncmeta = NCMetadata (ids_metadata )
86101 """NetCDF related metadata."""
87102 self .variables = list (group .variables )
88103 """List of variable names stored in the netCDF group."""
104+
105+ self ._lazy_map = {}
89106 # Don't use masked arrays: they're slow and we'll handle most of the unset
90107 # values through the `:shape` arrays
91108 self .group .set_auto_mask (False )
@@ -99,31 +116,60 @@ def __init__(self, group: netCDF4.Group, ids: IDSToplevel) -> None:
99116 "Mandatory variable `ids_properties.homogeneous_time` does not exist."
100117 )
101118 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" ] )
103120 if var [()] not in [0 , 1 , 2 ]:
104121 raise InvalidNetCDFEntry (
105122 f"Invalid value for ids_properties.homogeneous_time: { var [()]} . "
106123 "Was expecting: 0, 1 or 2."
107124 )
108125 self .homogeneous_time = var [()] == IDS_TIME_MODE_HOMOGENEOUS
109126
110- def run (self ) -> None :
127+ def run (self , lazy : bool ) -> None :
111128 """Load the data from the netCDF group into the IDS."""
112129 self .variables .sort ()
113130 self .validate_variables ()
131+ if lazy :
132+ self .ids ._set_lazy_context (LazyContext (self ))
114133 for var_name in self .variables :
115134 if var_name .endswith (":shape" ):
116135 continue
117- metadata = self .ids . metadata [var_name ]
136+ metadata = self .ids_metadata [var_name ]
118137
119138 if metadata .data_type is IDSDataType .STRUCTURE :
120139 continue # This only contains DD metadata we already know
121140
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+
122164 var = self .group [var_name ]
165+ if lazy :
166+ self ._lazy_map [target_metadata .path_string ] = var
167+ continue
168+
123169 if metadata .data_type is IDSDataType .STRUCT_ARRAY :
124170 if "sparse" in var .ncattrs ():
125171 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 ):
127173 node .resize (shapes [index ][0 ])
128174
129175 else :
@@ -132,7 +178,7 @@ def run(self) -> None:
132178 metadata .path_string , self .homogeneous_time
133179 )[- 1 ]
134180 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 ):
136182 node .resize (size )
137183
138184 continue
@@ -144,23 +190,30 @@ def run(self) -> None:
144190 if "sparse" in var .ncattrs ():
145191 if metadata .ndim :
146192 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 ):
148194 shape = shapes [index ]
149195 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+ ]
151200 else :
152- for index , node in tree_iter (self .ids , metadata ):
201+ for index , node in tree_iter (self .ids , target_metadata ):
153202 value = data [index ]
154203 if value != getattr (var , "_FillValue" , None ):
155- node .value = data [index ]
204+ # NOTE: bypassing IDSPrimitive.value.setter logic
205+ node ._IDSPrimitive__value = value
156206
157207 elif metadata .path_string not in self .ncmeta .aos :
158208 # 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
160212
161213 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 ]
164217
165218 def validate_variables (self ) -> None :
166219 """Validate that all variables in the netCDF Group exist and match the DD."""
@@ -194,7 +247,7 @@ def validate_variables(self) -> None:
194247 # Check that the DD defines this variable, and validate its metadata
195248 var = self .group [var_name ]
196249 try :
197- metadata = self .ids . metadata [var_name ]
250+ metadata = self .ids_metadata [var_name ]
198251 except KeyError :
199252 raise InvalidNetCDFEntry (
200253 f"Invalid variable { var_name } : no such variable exists in the "
@@ -300,3 +353,69 @@ def _validate_sparsity(
300353 raise variable_error (
301354 shape_var , "dtype" , shape_var .dtype , "any integer type"
302355 )
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