33r"""Convert ptychography data format using Ptychodus.
44
55Example usage:
6- python convert_ptychoshelves_to_ptychodus.py \
7- --patterns data/ptychoshelves/data_roi0_dp.hdf5 \
8- --scan data/ptychoshelves/data_roi0_para.hdf5 \
9- --product-name ptychodus \
10- --output-dir outputs \
11- --diffraction-reader PtychoShelves \
12- --scan-reader PtychoShelves
6+ python convert_format.py \
7+ --patterns "data/ptychoshelves/velo_19c2_Jun_IC_fly145/data_roi0_dp.hdf5" \
8+ --probe "data/ptychoshelves/velo_19c2_Jun_IC_fly145/Niter100.mat" \
9+ --probe-positions "data/ptychoshelves/velo_19c2_Jun_IC_fly145/data_roi0_para.hdf5" \
10+ --metadata "data/ptychoshelves/velo_19c2_Jun_IC_fly145/data_roi0_para.hdf5" \
11+ --product-name "ptychodus" \
12+ --output-dir "outputs" \
13+ --diffraction-reader "PtychoShelves" \
14+ --probe-reader "PtychoShelves" \
15+ --probe-position-reader "PtychoShelves"
16+
1317"""
1418from __future__ import annotations
1519
1620import argparse
1721from pathlib import Path
1822from typing import Literal , Optional
1923
24+ import h5py
25+
2026from ptychodus .model import ModelCore
27+ from ptychodus .api .diffraction import CropCenter
28+ from ptychodus .api .geometry import ImageExtent
29+
2130
22- DiffractionReaderChoices = (
31+ DiffractionReaderName = Literal [
2332 "APS_CSSI" ,
2433 "APS_HXN" ,
2534 "PtychoShelves" ,
4251 "NSLS_II_2" ,
4352 "NSLS_II_MATLAB" ,
4453 "APS_Polar" ,
45- )
46- DiffractionWriterChoices = ( "PtychoShelves" , "NPZ" , "NPY" )
47- ScanReaderChoices = (
54+ ]
55+ DiffractionWriterName = Literal [ "PtychoShelves" , "NPZ" , "NPY" ]
56+ ProbeReaderName = Literal [
4857 "MDA" ,
4958 "APS_2IDD" ,
5059 "APS_2IDE" ,
6675 "TXT" ,
6776 "CSV" ,
6877 "CXI" ,
69- )
70- ProductWriterChoices = ("HDF5" , "NPZ" )
71-
72- DiffractionReaderName = Literal [
73- "APS_CSSI" ,
74- "APS_HXN" ,
75- "PtychoShelves" ,
76- "CXI" ,
77- "TIFF" ,
78- "LCLS_XPP" ,
79- "APS_2IDD" ,
80- "APS_2IDE" ,
81- "APS_BNP" ,
82- "APS_LYNX" ,
83- "NPZ" ,
84- "SLAC_NPZ" ,
85- "SLS_cSAXS" ,
86- "APS_ISN" ,
87- "APS_Velociprobe" ,
88- "NPY" ,
89- "MAX_IV_NanoMax" ,
90- "APS_PtychoSAXS" ,
91- "NSLS_II_1" ,
92- "NSLS_II_2" ,
93- "NSLS_II_MATLAB" ,
94- "APS_Polar" ,
9578]
96- DiffractionWriterName = Literal ["PtychoShelves" , "NPZ" , "NPY" ]
97- ScanReaderName = Literal [
79+ ProbePositionReaderName = Literal [
9880 "MDA" ,
9981 "APS_2IDD" ,
10082 "APS_2IDE" ,
119101]
120102ProductWriterName = Literal ["HDF5" , "NPZ" ]
121103
104+ DiffractionReaderChoices = list (DiffractionReaderName .__args__ )
105+ DiffractionWriterChoices = list (DiffractionWriterName .__args__ )
106+ ProbeReaderChoices = list (ProbeReaderName .__args__ )
107+ ProbePositionReaderChoices = list (ProbePositionReaderName .__args__ )
108+ ProductWriterChoices = list (ProductWriterName .__args__ )
109+
122110
123111def _metadata_kwargs (metadata ) -> dict [str , float ]:
124112 """Collect optional metadata fields that match create_product() parameters."""
@@ -142,20 +130,49 @@ def _metadata_kwargs(metadata) -> dict[str, float]:
142130 return values
143131
144132
133+ def _get_pixel_size (
134+ metadata_file_path : Path | None = None ,
135+ metadata_type : str | None = None ,
136+ asserted_pixel_size : float | None = None ,
137+ ) -> float | None :
138+ if asserted_pixel_size is not None :
139+ return asserted_pixel_size
140+ if metadata_file_path is not None :
141+ if metadata_type == "PtychoShelves" :
142+ with h5py .File (metadata_file_path , "r" ) as f :
143+ pixel_size_m = float (f ["dx" ][0 ])
144+ else :
145+ raise ValueError (f"Unsupported metadata type: { metadata_type } " )
146+ return pixel_size_m
147+
148+
149+ def _get_diffraction_pattern_size (
150+ diffraction_pattern_path : Path ,
151+ diffraction_reader : DiffractionReaderName ,
152+ ) -> tuple [int , int ]:
153+ if diffraction_reader == "PtychoShelves" :
154+ with h5py .File (diffraction_pattern_path , "r" ) as f :
155+ return f ["dp" ].shape [- 2 :]
156+ else :
157+ raise ValueError (f"Unsupported diffraction reader: { diffraction_reader } " )
158+
159+
145160def convert_data (
146- patterns_path : Path ,
161+ diffraction_pattern_path : Path ,
147162 * ,
148- scan_path : Path | None ,
163+ probe_path : Path | None ,
164+ probe_position_path : Path | None ,
165+ metadata_path : Path | None ,
149166 diffraction_output : Path ,
150167 product_output : Path ,
151168 product_name : str | None ,
152169 diffraction_reader : DiffractionReaderName ,
153- scan_reader : ScanReaderName | None ,
170+ probe_reader : ProbeReaderName | None ,
171+ probe_position_reader : ProbePositionReaderName | None ,
154172 diffraction_writer : DiffractionWriterName = "PtychoShelves" ,
155173 product_writer : ProductWriterName = "HDF5" ,
156174 settings_file : Optional [Path ] = None ,
157- build_probe : Optional [bool ] = True ,
158- build_object : Optional [bool ] = True ,
175+ asserted_pixel_size : Optional [float ] = None ,
159176) -> tuple [Path , Path ]:
160177 """Convert an external diffraction dataset into Ptychodus outputs.
161178
@@ -164,9 +181,15 @@ def convert_data(
164181 patterns_path : Path
165182 Path to the diffraction file that should be opened via the specified
166183 `diffraction_reader` plugin.
167- scan_path : Path or None
184+ probe_path : Path or None
185+ Optional path to a probe file. When provided, it is opened
186+ with `probe_reader` before the probe/object builders are invoked.
187+ probe_positions_path : Path or None
168188 Optional path to a scan or position file. When provided, it is opened
169- with `scan_reader` before the probe/object builders are invoked.
189+ with `probe_position_reader` before the probe/object builders are invoked.
190+ metadata_path : Path or None
191+ Optional path to a metadata file. When provided, it is used to
192+ override the pixel size from data files.
170193 diffraction_output : Path
171194 Destination filename for the converted diffraction data.
172195 product_output : Path
@@ -188,10 +211,10 @@ def convert_data(
188211 settings_file : Path or None
189212 Optional ``settings.ini`` loaded into :class:`ptychodus.model.ModelCore`
190213 before performing the conversion.
191- build_probe : bool
192- Whether to execute :meth:`build_probe` on the workflow product.
193- build_object : bool
194- Whether to execute :meth:`build_object` on the workflow product .
214+ asserted_pixel_size : float or None
215+ Optional pixel size in meters. If provided, it is used to override
216+ the pixel size from data files. This parameter needs to be provided
217+ if pixel size is not available in the data files .
195218
196219 Returns
197220 -------
@@ -203,19 +226,26 @@ def convert_data(
203226 FileNotFoundError
204227 Raised when either the diffraction file or the scan file is missing.
205228 """
206- if not patterns_path .is_file ():
207- raise FileNotFoundError (f"Diffraction patterns file not found: { patterns_path } " )
229+ if not diffraction_pattern_path .is_file ():
230+ raise FileNotFoundError (f"Diffraction patterns file not found: { diffraction_pattern_path } " )
208231
209- if scan_path is not None and not scan_path .exists ():
210- raise FileNotFoundError (f"Scan file not found: { scan_path } " )
232+ if probe_position_path is not None and not probe_position_path .exists ():
233+ raise FileNotFoundError (f"Scan file not found: { probe_position_path } " )
211234
212235 diffraction_output .parent .mkdir (parents = True , exist_ok = True )
213236 product_output .parent .mkdir (parents = True , exist_ok = True )
214237
215- product_basename = product_name or patterns_path .stem
238+ product_basename = product_name or diffraction_pattern_path .stem
216239
217240 with ModelCore (settings_file ) as model :
218- model .workflow_api .open_patterns (patterns_path , file_type = diffraction_reader )
241+ dp_size = _get_diffraction_pattern_size (diffraction_pattern_path , diffraction_reader )
242+
243+ model .workflow_api .open_patterns (
244+ diffraction_pattern_path ,
245+ file_type = diffraction_reader ,
246+ crop_center = CropCenter (position_x_px = dp_size [1 ] // 2 , position_y_px = dp_size [0 ] // 2 ),
247+ crop_extent = ImageExtent (width_px = dp_size [1 ], height_px = dp_size [0 ]),
248+ )
219249 model .diffraction .diffraction_api .start_assembling_diffraction_patterns ()
220250 model .diffraction .diffraction_api .finish_assembling_diffraction_patterns (block = True )
221251
@@ -224,17 +254,26 @@ def convert_data(
224254
225255 workflow_product = model .workflow_api .create_product (product_basename , ** product_kwargs )
226256
227- if scan_path is not None :
228- workflow_product .open_scan (scan_path , file_type = scan_reader or None )
229-
230- if build_probe :
231- workflow_product .build_probe ()
257+ if probe_position_path is not None :
258+ workflow_product .open_scan (probe_position_path , file_type = probe_position_reader or None )
232259
233- if build_object :
234- workflow_product .build_object ( )
260+ if probe_path is not None :
261+ workflow_product .open_probe ( probe_path , file_type = probe_reader or None )
235262
236263 model .diffraction .diffraction_api .save_patterns (diffraction_output , diffraction_writer )
237264 workflow_product .save_product (product_output , file_type = product_writer )
265+
266+ # Supplement pixel size
267+ if metadata_path is not None :
268+ pixel_size_m = _get_pixel_size (
269+ metadata_path ,
270+ metadata_type = diffraction_reader ,
271+ asserted_pixel_size = asserted_pixel_size
272+ )
273+ if pixel_size_m is not None :
274+ with h5py .File (product_output , "r+" ) as f :
275+ f ["object" ].attrs ["pixel_height_m" ] = pixel_size_m
276+ f ["object" ].attrs ["pixel_width_m" ] = pixel_size_m
238277
239278 return diffraction_output , product_output
240279
@@ -289,10 +328,22 @@ def main(argv: list[str] | None = None) -> int:
289328 ),
290329 )
291330 parser .add_argument (
292- "--scan " ,
331+ "--probe " ,
293332 type = Path ,
294333 default = None ,
295- help = "Optional scan/position file path required by many plugins." ,
334+ help = "Optional probe file path required by many plugins." ,
335+ )
336+ parser .add_argument (
337+ "--probe-positions" ,
338+ type = Path ,
339+ default = None ,
340+ help = "Optional probe positions file path required by many plugins." ,
341+ )
342+ parser .add_argument (
343+ "--metadata" ,
344+ type = Path ,
345+ default = None ,
346+ help = "Optional metadata file path required by many plugins." ,
296347 )
297348 parser .add_argument (
298349 "--output-dir" ,
@@ -335,10 +386,16 @@ def main(argv: list[str] | None = None) -> int:
335386 help = "Diffraction file writer plugin to use when saving the converted dataset." ,
336387 )
337388 parser .add_argument (
338- "--scan -reader" ,
389+ "--probe -reader" ,
339390 default = "PtychoShelves" ,
340- choices = sorted (ScanReaderChoices ),
341- help = "Scan reader plugin to use when a scan file is provided." ,
391+ choices = sorted (ProbeReaderChoices ),
392+ help = "Probe reader plugin to use when a probe file is provided." ,
393+ )
394+ parser .add_argument (
395+ "--probe-position-reader" ,
396+ default = "PtychoShelves" ,
397+ choices = sorted (ProbePositionReaderChoices ),
398+ help = "Probe position reader plugin to use when a probe position file is provided." ,
342399 )
343400 parser .add_argument (
344401 "--product-writer" ,
@@ -347,14 +404,10 @@ def main(argv: list[str] | None = None) -> int:
347404 help = "Product file writer plugin to use for the converted parameter file." ,
348405 )
349406 parser .add_argument (
350- "--skip-probe" ,
351- action = "store_true" ,
352- help = "Skip building the probe after loading the scan." ,
353- )
354- parser .add_argument (
355- "--skip-object" ,
356- action = "store_true" ,
357- help = "Skip building the object after loading the scan." ,
407+ "--asserted-pixel-size" ,
408+ type = float ,
409+ default = None ,
410+ help = "Optional pixel size in meters. If provided, it is used to override the pixel size from data files." ,
358411 )
359412 parser .add_argument (
360413 "--list-plugins" ,
@@ -365,10 +418,11 @@ def main(argv: list[str] | None = None) -> int:
365418 args = parser .parse_args (argv )
366419
367420 if args .list_plugins :
368- print ("Diffraction readers :" , ", " .join (sorted (DiffractionReaderChoices )))
369- print ("Diffraction writers :" , ", " .join (sorted (DiffractionWriterChoices )))
370- print ("Scan readers :" , ", " .join (sorted (ScanReaderChoices )))
371- print ("Product writers :" , ", " .join (sorted (ProductWriterChoices )))
421+ print ("Diffraction readers : " , ", " .join (sorted (DiffractionReaderChoices )))
422+ print ("Diffraction writers : " , ", " .join (sorted (DiffractionWriterChoices )))
423+ print ("Probe readers : " , ", " .join (sorted (ProbeReaderChoices )))
424+ print ("Probe position readers: " , ", " .join (sorted (ProbePositionReaderChoices )))
425+ print ("Product writers : " , ", " .join (sorted (ProductWriterChoices )))
372426 return 0
373427
374428 if args .diffraction_writer is not None and args .diffraction_writer not in DiffractionWriterChoices :
@@ -394,17 +448,19 @@ def main(argv: list[str] | None = None) -> int:
394448
395449 diffraction_path , product_path = convert_data (
396450 args .patterns ,
397- scan_path = args .scan ,
451+ probe_path = args .probe ,
452+ probe_position_path = args .probe_positions ,
453+ metadata_path = args .metadata ,
398454 diffraction_output = diffraction_output ,
399455 product_output = product_output ,
400456 product_name = args .product_name ,
401457 diffraction_reader = args .diffraction_reader ,
402458 diffraction_writer = args .diffraction_writer ,
403- scan_reader = args .scan_reader ,
459+ probe_reader = args .probe_reader ,
460+ probe_position_reader = args .probe_position_reader ,
404461 product_writer = args .product_writer ,
405462 settings_file = args .settings ,
406- build_probe = not args .skip_probe ,
407- build_object = not args .skip_object ,
463+ asserted_pixel_size = args .asserted_pixel_size ,
408464 )
409465
410466 print (f"Wrote diffraction data to { diffraction_path } " )
0 commit comments