1515
1616from activitysim .core import skim_dictionary , util
1717from activitysim .core .exceptions import TableTypeError
18+ from activitysim .core .skim_parquet import ParquetSkimFile , is_parquet_file
1819
1920logger = logging .getLogger (__name__ )
2021
@@ -60,18 +61,24 @@ def __init__(self, state, skim_tag, network_los):
6061
6162 skim_tag: str (e.g. 'TAZ')
6263 dtype_name: str (e.g. 'float32')
63- omx_manifest: dict dict mapping { omx_key: omx_file_name }
64- omx_shape: 2D tuple shape of omx matrix: (<number_of_zones>, <number_of_zones>)
65- num_skims: int total number of individual skim matrices in omx files
64+ omx_manifest: dict dict mapping { omx_key: skim_file_name }, whether the skim
65+ file is an omx file or a parquet file
66+ omx_shape: 2D tuple shape of skim matrix: (<number_of_zones>, <number_of_zones>)
67+ num_skims: int total number of individual skim matrices in omx/parquet files
6668 skim_data_shape: 3D tuple (num_skims, omx_shape[0], omx_shape[1]) if ROW_MAJOR_LAYOUT
67- offset_map: dict or None 1D ndarray as returned by omx_file.mapentries, if omx file has mappings
69+ offset_map: dict or None 1D ndarray as returned by omx_file.mapentries, if omx file has
70+ mappings, or the (sorted) zone ids found in a parquet skim file
6871 offset_map_name: str name of offset_map in omx_filecorresponding to offset_map, if there was one
6972 omx_keys: dict dict mapping skim key (str or tuple) to skim key in omx file
7073 {DISTWALK: DISTWALK,
7174 ('DRV_COM_WLK_BOARDS', 'AM'): DRV_COM_WLK_BOARDS__AM, ...}
7275 base_keys: list of str e.g. 'BIKEDIST' or 'SOVTOLL_VTOLL' (base key of 3d skim)
7376 block_offsets: dict dict mapping skim key tuple to offset
7477
78+ Skim files can be in either OMX or Parquet format; the format is auto-detected
79+ from each file's extension (``.omx`` vs ``.parquet``/``.pq``), and OMX and Parquet
80+ files can be freely mixed within the list of files for a single skim_tag.
81+
7582 Parameters
7683 ----------
7784 skim_tag
@@ -92,6 +99,10 @@ def __init__(self, state, skim_tag, network_los):
9299 self .base_keys = None
93100 self .block_offsets = None
94101
102+ # cache of ParquetSkimFile instances, keyed by file path, so files
103+ # opened during load_skim_info are not re-parsed when reading data
104+ self .parquet_files = {}
105+
95106 if skim_tag :
96107 self .load_skim_info (state , skim_tag )
97108
@@ -119,6 +130,44 @@ def load_skim_info(self, state, skim_tag):
119130 for omx_file_path in self .omx_file_paths :
120131 logger .debug (f"load_skim_info { skim_tag } reading { omx_file_path } " )
121132
133+ if is_parquet_file (omx_file_path ):
134+ # Skim data provided as a parquet file (auto-detected by extension)
135+ # instead of an omx file. The file is inspected here (and cached)
136+ # to determine its zone list, shape, and dense/sparse layout.
137+ parquet_skim_file = ParquetSkimFile (omx_file_path )
138+ self .parquet_files [omx_file_path ] = parquet_skim_file
139+
140+ # Check the shape of the skims, same as is done for omx files below.
141+ if self .omx_shape is None :
142+ self .omx_shape = parquet_skim_file .shape
143+ else :
144+ assert (
145+ self .omx_shape == parquet_skim_file .shape
146+ ), f"Mismatch shape { self .omx_shape } != { parquet_skim_file .shape } "
147+
148+ for skim_name in parquet_skim_file .data_cols :
149+ if skim_name in self .omx_manifest :
150+ warnings .warn (
151+ f"duplicate skim '{ skim_name } ' found in { self .omx_manifest [skim_name ]} and { omx_file_path } "
152+ )
153+ self .omx_manifest [skim_name ] = omx_file_path
154+
155+ # The origin/destination (zone id) values found in the parquet file
156+ # serve the same purpose as an omx file's offset mapping. Each parquet
157+ # file is checked independently (it need not have zones in the same
158+ # order as other files) but the set of zone ids found must match.
159+ if self .offset_map is None :
160+ self .offset_map_name = f"{ omx_file_path } zone ids"
161+ self .offset_map = parquet_skim_file .zone_ids
162+ assert len (self .offset_map ) == self .omx_shape [0 ]
163+ else :
164+ if not np .array_equal (self .offset_map , parquet_skim_file .zone_ids ):
165+ raise RuntimeError (
166+ f"Mismatched zone ids in parquet skim file { omx_file_path } : "
167+ f"expected zone ids consistent with { self .offset_map_name } "
168+ )
169+ continue
170+
122171 with omx .open_file (omx_file_path , mode = "r" ) as omx_file :
123172
124173 # Check the shape of the skims. All skim files loaded within this
@@ -322,7 +371,7 @@ def load_skim_info(self, state, skim_tag):
322371
323372 def _read_skims_from_omx (self , skim_info , skim_data ):
324373 """
325- read skims from omx file into skim_data
374+ read skims from omx and/or parquet files into skim_data
326375 """
327376
328377 skim_tag = skim_info .skim_tag
@@ -334,6 +383,34 @@ def _read_skims_from_omx(self, skim_info, skim_data):
334383
335384 logger .info (f"_read_skims_from_omx { omx_file_path } " )
336385
386+ if is_parquet_file (omx_file_path ):
387+ parquet_skim_file = skim_info .parquet_files .get (omx_file_path )
388+ if parquet_skim_file is None :
389+ parquet_skim_file = ParquetSkimFile (omx_file_path )
390+ for skim_key , omx_key in omx_keys .items ():
391+ if omx_manifest [omx_key ] == omx_file_path :
392+ offset = skim_info .block_offsets [skim_key ]
393+ logger .debug (
394+ f"_read_skims_from_omx (parquet) file { omx_file_path } "
395+ f"omx_key { omx_key } skim_key { skim_key } to offset { offset } "
396+ )
397+
398+ if skim_dictionary .ROW_MAJOR_LAYOUT :
399+ a = skim_data [offset , :, :]
400+ else :
401+ a = skim_data [:, :, offset ]
402+
403+ a [:] = parquet_skim_file .read_matrix (
404+ omx_key , dtype = skim_info .dtype_name
405+ )
406+
407+ num_skims_loaded += 1
408+
409+ logger .info (
410+ f"_read_skims_from_omx loaded { num_skims_loaded } skims from { omx_file_path } "
411+ )
412+ continue
413+
337414 # read skims into skim_data
338415 with omx .open_file (omx_file_path , mode = "r" ) as omx_file :
339416 for skim_key , omx_key in omx_keys .items ():
0 commit comments