Skip to content

Commit cfcb130

Browse files
committed
FIX: add explicit args for probe/probe positions/metadata (for pixel size) data files; auto-detect DP size to avoid cropping; fix probe loading
1 parent 457d3ee commit cfcb130

1 file changed

Lines changed: 140 additions & 84 deletions

File tree

scripts/scripted_format_conversion/convert_format.py

Lines changed: 140 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,32 @@
33
r"""Convert ptychography data format using Ptychodus.
44
55
Example 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
"""
1418
from __future__ import annotations
1519

1620
import argparse
1721
from pathlib import Path
1822
from typing import Literal, Optional
1923

24+
import h5py
25+
2026
from 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",
@@ -42,9 +51,9 @@
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",
@@ -66,35 +75,8 @@
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",
@@ -119,6 +101,12 @@
119101
]
120102
ProductWriterName = 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

123111
def _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+
145160
def 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

Comments
 (0)