Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
dfb3150
small_cell: apply beamtime enhancements for subsampling and clustering
dwpaley Mar 21, 2026
dd96fdd
small_cell: remove superseded cake_plot_prep script
dwpaley Mar 26, 2026
7ac5fbc
Move cluster2 and index2 to smx_findexer branch
dwpaley Mar 26, 2026
68860c4
Add n_max parameter to powder_from_spots for limiting plotted experim…
dwpaley Mar 26, 2026
ba640e6
powder_util: remove angle_histogram feature and fix n_max off-by-one
dwpaley Mar 27, 2026
5c78246
powder_refine_geometry: use plain str for space_group in print
dwpaley Apr 2, 2026
a558f11
geometry_refiner: hierarchy access, flex.double fix, remove average_u…
dwpaley Apr 2, 2026
5e81ce8
powder_util: add search_step_z, mean_squared_error_from_target, net_d…
dwpaley Apr 2, 2026
84feb91
powder_from_spots: add q-parameters and Z-axis refinement
dwpaley Apr 2, 2026
2986238
geometry_refiner: vectorized residuals, experiment subset, progress c…
dwpaley Apr 2, 2026
b154a07
Add missing comments for q-parameter conversion and Z-refinement blocks
dwpaley Apr 2, 2026
4ea4dfe
Merge branch 'geometry-refiner-perf-1775158067' into smx_small_cell_p…
dwpaley Apr 2, 2026
0143ae0
Merge branch 'feature/powder-util-z-1775157969' into smx_small_cell_p…
dwpaley Apr 2, 2026
6984dd8
Merge branch 'smx_small_cell_processing_qparams' into smx_small_cell_…
dwpaley Apr 2, 2026
93da2c2
Merge branch 'smx_small_cell_processing_powder_fix_1775157961' into s…
dwpaley Apr 2, 2026
107d3c6
powder_util: restore matplotlib backend assertion for interactive pea…
dwpaley Apr 2, 2026
32cf267
powder_util: revert anomalous_flag to False for powder peak overlay
dwpaley Apr 2, 2026
96e3db0
Add xy_file_units phil param, default to d-spacing
dwpaley Apr 6, 2026
fe2a70c
clean clutter
dwpaley Apr 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions xfel/small_cell/command_line/powder_from_spots.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@
.type = space_group
.help = Show positions of miller indices from this unit_cell and space \
group. Not implemented.
n_max = None
.type = int
.help = Stop plotting after this many experiments.
filter {
enable = False
.type = bool
Expand All @@ -121,6 +124,10 @@
.type = str
xy_file = None
.type = str
xy_file_units = *d q
.type = choice
.help = X-axis units for xy_file output: d-spacing in Angstroms (default) \
or inverse-d-spacing (q) in inverse Angstroms.
peak_file = None
.type = str
.help = Optionally, specify an output file for interactive peak picking in \
Expand All @@ -139,9 +146,21 @@
.type = float
d_max = None
.type = float
q_min = None
.type = float
.help = Minimum q (inverse angstroms). Converted to d_max if provided.
q_max = None
.type = float
.help = Maximum q (inverse angstroms). Converted to d_min if provided.
step_px = None
.type = float
.multiple = True
d_target = None
.type = float
.help = If set, enables detector distance refinement along z-axis
q_target = None
.type = float
.help = Target q value (inverse angstroms). Converted to d_target if provided.
}
plot {
interactive = True
Expand Down Expand Up @@ -185,11 +204,33 @@ def run(self):
experiments = params.input.experiments[0].data
reflections = params.input.reflections[0].data

# Convert q parameters to d parameters if needed (q = 1/d)
if params.center_scan.q_min is not None:
params.center_scan.d_max = 1.0 / params.center_scan.q_min
if params.center_scan.q_max is not None:
params.center_scan.d_min = 1.0 / params.center_scan.q_max
if params.center_scan.q_target is not None:
params.center_scan.d_target = 1.0 / params.center_scan.q_target

if params.center_scan.d_min:
assert params.center_scan.d_max
cscan = Center_scan(experiments, reflections, params)

# First XY refinement sequence
for step in params.center_scan.step_px:
cscan.search_step(step)

# Z-axis refinement if d_target is set
if params.center_scan.d_target is not None:
# Default Z steps in microns
z_steps_um = [4000, 2000, 1000, 500, 250, 125, 80, 40, 20, 10, 5]
for step_um in z_steps_um:
cscan.search_step_z(step_um, params.center_scan.d_target)

# Second XY refinement sequence
for step in params.center_scan.step_px:
cscan.search_step(step)

if params.output.geom_file is not None:
experiments.as_file(params.output.geom_file)

Expand Down
158 changes: 158 additions & 0 deletions xfel/small_cell/command_line/powder_refine_geometry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# LIBTBX_SET_DISPATCHER_NAME cctbx.xfel.powder_refine_geometry
from __future__ import division
import logging

from iotbx.phil import parse
from dials.util import log
from dials.util import show_mail_on_error
from dials.util.options import ArgumentParser
from xfel.small_cell.geometry_refiner import PowderGeometryRefiner


logger = logging.getLogger("dials.command_line.powder_refine_geometry")

help_message = """
Refine detector geometry using powder diffraction d-spacings.

Examples of usage:

# Basic usage with defaults (LaB6)
$ cctbx.xfel.powder_refine_geometry spots.expt spots.refl

# Custom d-spacings (e.g., for Si standard)
$ cctbx.xfel.powder_refine_geometry spots.expt spots.refl \\
reference_d_spacings=3.135,1.920,1.637,1.357

# Or use unit cell and space group instead
$ cctbx.xfel.powder_refine_geometry spots.expt spots.refl \\
unit_cell=4.156,4.156,4.156,90,90,90 space_group=Pm-3m

# Refine only XY shift (fix distance and tilt)
$ cctbx.xfel.powder_refine_geometry spots.expt spots.refl \\
refine.distance=False refine.tilt=False

# Specify output file
$ cctbx.xfel.powder_refine_geometry spots.expt spots.refl \\
output.experiments=calibrated.expt

This tool uses spotfinding output from a powder standard with known d-spacings
to refine detector geometry. It optimizes a 5-parameter detector model:
- distance: shift along detector normal
- shift1/shift2: XY shifts along fast/slow axes
- tau2/tau3: tilts around fast/slow axes

The refinement minimizes the sum of squared differences between observed
d-spacings and the nearest reference d-spacing.

Default reference d-spacings are for LaB6 (SRM 660):
4.156 A (100), 2.939 A (110), 2.399 A (111), 2.078 A (200), 1.858 A (210)
"""

phil_scope = parse(
"""
reference_d_spacings = 4.156 2.939 2.399 2.078 1.858
.type = floats
.help = Reference d-spacings in Angstroms. Default: first 5 LaB6 peaks. \
Either use this OR specify unit_cell and space_group.

unit_cell = None
.type = unit_cell
.help = Unit cell to generate reference d-spacings (use with space_group). \
If specified, this overrides reference_d_spacings.

space_group = None
.type = space_group
.help = Space group to generate reference d-spacings (use with unit_cell). \
If specified, this overrides reference_d_spacings.

d_min = 1.5
.type = float
.help = Minimum d-spacing to include in refinement

d_max = 20
.type = float
.help = Maximum d-spacing to include in refinement

max_distance_inv_ang = 0.002
.type = float
.help = Maximum distance from reference d-spacing in inverse Angstroms. \
Reflections further than this from any reference will be excluded.

refine {
distance = True
.type = bool
.help = Refine detector distance along normal
shift = True
.type = bool
.help = Refine XY shift (shift1 and shift2)
tilt = True
.type = bool
.help = Refine detector tilts (tau2 and tau3)
}

output {
experiments = refined.expt
.type = path
.help = Output filename for refined experiments
log = powder_refine_geometry.log
.type = path
.help = Output log file
}
"""
)


class Script(object):
def __init__(self):
usage = "$ cctbx.xfel.powder_refine_geometry EXPERIMENTS REFLECTIONS [options]"
self.parser = ArgumentParser(
usage=usage,
phil=phil_scope,
epilog=help_message,
check_format=False,
read_reflections=True,
read_experiments=True,
)

def run(self):
params, options = self.parser.parse_args(show_diff_phil=True)

# Validate input
if len(params.input.experiments) != 1 or len(params.input.reflections) != 1:
raise ValueError("Please provide exactly one experiments file and "
"one reflections file")

experiments = params.input.experiments[0].data
reflections = params.input.reflections[0].data

print(f"\nLoaded {len(experiments)} experiments with "
f"{len(reflections)} reflections")

if params.unit_cell is not None and params.space_group is not None:
print(f"Using unit_cell: {params.unit_cell}")
print(f"Using space_group: {params.space_group}")
else:
print(f"Reference d-spacings: {params.reference_d_spacings}")

# Create and run refiner
refiner = PowderGeometryRefiner(experiments, reflections, params)
result = refiner.run()

if result is not None:
# Report final geometry
refiner.report_geometry_changes()

# Save refined experiments
refined_experiments = refiner.get_refined_experiments()
print(f"\nSaving refined experiments to {params.output.experiments}")
refined_experiments.as_file(params.output.experiments)

print("\nRefinement complete.")
else:
print("\nNo refinement performed.")


if __name__ == "__main__":
with show_mail_on_error():
script = Script()
script.run()
Loading
Loading