Skip to content

Commit 9b07803

Browse files
Updates to dials.scale error modelling to handle stills data (dials#2654)
1 parent a8945b6 commit 9b07803

File tree

8 files changed

+135
-16
lines changed

8 files changed

+135
-16
lines changed

newsfragments/2654.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
``dials.scale``: Add filtering options to default basic error model to allow error modelling of stills data

src/dials/algorithms/scaling/algorithm.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -641,16 +641,21 @@ def targeted_scaling_algorithm(scaler):
641641
scaler.make_ready_for_scaling()
642642
scaler.perform_scaling()
643643

644+
expand_and_do_outlier_rejection(scaler, calc_cov=True)
645+
do_error_analysis(scaler, reselect=True)
646+
644647
if scaler.params.scaling_options.full_matrix and (
645648
scaler.params.scaling_refinery.engine == "SimpleLBFGS"
646649
):
647650
scaler.perform_scaling(
648651
engine=scaler.params.scaling_refinery.full_matrix_engine,
649652
max_iterations=scaler.params.scaling_refinery.full_matrix_max_iterations,
650653
)
654+
else:
655+
scaler.perform_scaling()
651656

652657
expand_and_do_outlier_rejection(scaler, calc_cov=True)
653-
# do_error_analysis(scaler, reselect=False)
658+
do_error_analysis(scaler, reselect=False)
654659

655660
scaler.prepare_reflection_tables_for_output()
656661
return scaler

src/dials/algorithms/scaling/error_model/engine.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
logger = logging.getLogger("dials")
2222

2323

24-
def run_error_model_refinement(model, Ih_table):
24+
def run_error_model_refinement(
25+
model, Ih_table, min_partiality=0.4, use_stills_filtering=False
26+
):
2527
"""
2628
Refine an error model for the input data, returning the model.
2729
@@ -30,7 +32,9 @@ def run_error_model_refinement(model, Ih_table):
3032
RuntimeError: can be raised in LBFGS minimiser.
3133
"""
3234
assert Ih_table.n_work_blocks == 1
33-
model.configure_for_refinement(Ih_table.blocked_data_list[0])
35+
model.configure_for_refinement(
36+
Ih_table.blocked_data_list[0], min_partiality, use_stills_filtering
37+
)
3438
if not model.active_parameters:
3539
logger.info("All error model parameters fixed, skipping refinement")
3640
else:
@@ -170,7 +174,6 @@ def test_value_convergence(self):
170174
r2 = self.avals[-2]
171175
except IndexError:
172176
return False
173-
174177
if r2 > 0:
175178
return abs((r2 - r1) / r2) < self._avals_tolerance
176179
else:
@@ -201,7 +204,7 @@ def _refine_component(self, model, target, parameterisation):
201204
def run(self):
202205
"""Refine the model."""
203206
if self.parameters_to_refine == ["a", "b"]:
204-
for n in range(20): # usually converges in around 5 cycles
207+
for n in range(50): # usually converges in around 5 cycles
205208
self._refine_a()
206209
# now update in model
207210
self.avals.append(self.model.components["a"].parameters[0])

src/dials/algorithms/scaling/error_model/error_model.py

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@
4242
"determine both parameters concurrently. If minimisation=None,"
4343
"the model parameters are fixed to their initial or given values."
4444
.expert_level = 3
45+
stills {
46+
min_Isigma = 2.0
47+
.type=float
48+
.help = "Minimum uncorrected I/sigma for individual reflections used in error model optimisation"
49+
min_multiplicity = 4
50+
.type = int
51+
.help = "Only reflections with at least this multiplicity (after Isigma filtering) are"
52+
"used in error model optimisation."
53+
}
4554
min_Ih = 25.0
4655
.type = float
4756
.help = "Reflections with expected intensity above this value are to."
@@ -248,7 +257,10 @@ def _create_summation_matrix(self):
248257
n = self.Ih_table.size
249258
self.binning_info["n_reflections"] = n
250259
summation_matrix = sparse.matrix(n, self.n_bins)
260+
# calculate expected intensity value in pixels on scale of each image
251261
Ih = self.Ih_table.Ih_values * self.Ih_table.inverse_scale_factors
262+
if "partiality" in self.Ih_table.Ih_table:
263+
Ih *= self.Ih_table.Ih_table["partiality"].to_numpy()
252264
size_order = flex.sort_permutation(flumpy.from_numpy(Ih), reverse=True)
253265
Imax = Ih.max()
254266
min_Ih = Ih.min()
@@ -383,7 +395,6 @@ def __init__(self, a=None, b=None, basic_params=None):
383395
see if a user specified fixed value is set. If no fixed values are given
384396
then the model starts with the default parameters a=1.0 b=0.02
385397
"""
386-
387398
self.free_components = []
388399
self.sortedy = None
389400
self.sortedx = None
@@ -408,14 +419,19 @@ def __init__(self, a=None, b=None, basic_params=None):
408419
if not basic_params.b:
409420
self._active_parameters.append("b")
410421

411-
def configure_for_refinement(self, Ih_table, min_partiality=0.4):
422+
def configure_for_refinement(
423+
self, Ih_table, min_partiality=0.4, use_stills_filtering=False
424+
):
412425
"""
413426
Add data to allow error model refinement.
414427
415428
Raises: ValueError if insufficient reflections left after filtering.
416429
"""
417430
self.filtered_Ih_table = self.filter_unsuitable_reflections(
418-
Ih_table, self.params, min_partiality
431+
Ih_table,
432+
self.params,
433+
min_partiality,
434+
use_stills_filtering,
419435
)
420436
# always want binning info so that can calc for output.
421437
self.binner = ErrorModelBinner(
@@ -455,8 +471,19 @@ def n_refl(self):
455471
return self.filtered_Ih_table.size
456472

457473
@classmethod
458-
def filter_unsuitable_reflections(cls, Ih_table, error_params, min_partiality):
474+
def filter_unsuitable_reflections(
475+
cls, Ih_table, error_params, min_partiality, use_stills_filtering
476+
):
459477
"""Filter suitable reflections for minimisation."""
478+
if use_stills_filtering:
479+
return filter_unsuitable_reflections_stills(
480+
Ih_table,
481+
error_params.stills.min_multiplicity,
482+
error_params.stills.min_Isigma,
483+
min_partiality=min_partiality,
484+
min_reflections_required=cls.min_reflections_required,
485+
min_Ih=error_params.min_Ih,
486+
)
460487
return filter_unsuitable_reflections(
461488
Ih_table,
462489
min_Ih=error_params.min_Ih,
@@ -570,6 +597,49 @@ def binned_variances_summary(self):
570597
)
571598

572599

600+
def filter_unsuitable_reflections_stills(
601+
Ih_table,
602+
min_multiplicity,
603+
min_Isigma,
604+
min_partiality,
605+
min_reflections_required,
606+
min_Ih,
607+
):
608+
"""Filter suitable reflections for minimisation."""
609+
610+
if "partiality" in Ih_table.Ih_table:
611+
sel = Ih_table.Ih_table["partiality"].to_numpy() > min_partiality
612+
Ih_table = Ih_table.select(sel)
613+
614+
sel = (Ih_table.intensities / (Ih_table.variances**0.5)) >= min_Isigma
615+
Ih_table = Ih_table.select(sel)
616+
617+
Ih = Ih_table.Ih_values * Ih_table.inverse_scale_factors
618+
if "partiality" in Ih_table.Ih_table:
619+
Ih *= Ih_table.Ih_table["partiality"].to_numpy()
620+
sel = Ih > min_Ih
621+
Ih_table = Ih_table.select(sel)
622+
623+
n_h = Ih_table.calc_nh()
624+
sigmaprime = calc_sigmaprime([1.0, 0.0], Ih_table)
625+
delta_hl = calc_deltahl(Ih_table, n_h, sigmaprime)
626+
# Optimise on the central bulk distribution of the data - avoid the few
627+
# reflections in the long tails.
628+
sel = np.abs(delta_hl) < 6.0
629+
Ih_table = Ih_table.select(sel)
630+
631+
sel = Ih_table.calc_nh() >= min_multiplicity
632+
Ih_table = Ih_table.select(sel)
633+
n = Ih_table.size
634+
635+
if n < min_reflections_required:
636+
raise ValueError(
637+
"Insufficient reflections (%s < %s) to perform error modelling."
638+
% (n, min_reflections_required)
639+
)
640+
return Ih_table
641+
642+
573643
def filter_unsuitable_reflections(
574644
Ih_table, min_Ih, min_partiality, min_reflections_required
575645
):

src/dials/algorithms/scaling/scaler.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,9 @@ def __init__(self, params, experiment, reflection_table, for_multi=False):
350350
self.free_set_selection = flex.bool(self.n_suitable_refl, False)
351351
self._free_Ih_table = None # An array of len n_suitable_refl
352352
self._configure_model_and_datastructures(for_multi=for_multi)
353+
self.is_still = True
354+
if self._experiment.scan and self._experiment.scan.get_oscillation()[1] != 0.0:
355+
self.is_still = False
353356
if self.params.weighting.error_model.error_model:
354357
# reload current error model parameters, or create new null
355358
self.experiment.scaling_model.load_error_model(
@@ -380,7 +383,10 @@ def perform_error_optimisation(self, update_Ih=True):
380383
Ih_table, _ = self._create_global_Ih_table(anomalous=True, remove_outliers=True)
381384
try:
382385
model = run_error_model_refinement(
383-
self._experiment.scaling_model.error_model, Ih_table
386+
self._experiment.scaling_model.error_model,
387+
Ih_table,
388+
self.params.reflection_selection.min_partiality,
389+
use_stills_filtering=self.is_still,
384390
)
385391
except (ValueError, RuntimeError) as e:
386392
logger.info(e)
@@ -1500,7 +1506,12 @@ def perform_error_optimisation(self, update_Ih=True):
15001506
continue
15011507
tables = [s.get_valid_reflections().select(~s.outliers) for s in scalers]
15021508
space_group = scalers[0].experiment.crystal.get_space_group()
1503-
Ih_table = IhTable(tables, space_group, anomalous=True)
1509+
Ih_table = IhTable(
1510+
tables,
1511+
space_group,
1512+
anomalous=True,
1513+
additional_cols=["partiality"],
1514+
)
15041515
if len(minimisation_groups) == 1:
15051516
logger.info("Determining a combined error model for all datasets")
15061517
else:
@@ -1509,7 +1520,10 @@ def perform_error_optimisation(self, update_Ih=True):
15091520
)
15101521
try:
15111522
model = run_error_model_refinement(
1512-
scalers[0]._experiment.scaling_model.error_model, Ih_table
1523+
scalers[0]._experiment.scaling_model.error_model,
1524+
Ih_table,
1525+
min_partiality=self.params.reflection_selection.min_partiality,
1526+
use_stills_filtering=scalers[0].is_still,
15131527
)
15141528
except (ValueError, RuntimeError) as e:
15151529
logger.info(e)

src/dials/command_line/refine_error_model.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@
5555
phil_scope = phil.parse(
5656
"""
5757
include scope dials.algorithms.scaling.error_model.error_model.phil_scope
58+
min_partiality = 0.4
59+
.type = float
60+
.help = "Use reflections with at least this partiality in error model optimisation."
5861
intensity_choice = *profile sum combine
5962
.type = choice
6063
.help = "Use profile or summation intensities"
@@ -101,13 +104,23 @@ def refine_error_model(params, experiments, reflection_tables):
101104
reflection_tables[i] = table
102105
space_group = experiments[0].crystal.get_space_group()
103106
Ih_table = IhTable(
104-
reflection_tables, space_group, additional_cols=["partiality"], anomalous=True
107+
reflection_tables,
108+
space_group,
109+
additional_cols=["partiality"],
110+
anomalous=True,
105111
)
106112

113+
use_stills_filtering = True
114+
for expt in experiments:
115+
if expt.scan and expt.scan.get_oscillation()[1] != 0.0:
116+
use_stills_filtering = False
117+
break
107118
# now do the error model refinement
108119
model = BasicErrorModel(basic_params=params.basic)
109120
try:
110-
model = run_error_model_refinement(model, Ih_table)
121+
model = run_error_model_refinement(
122+
model, Ih_table, params.min_partiality, use_stills_filtering
123+
)
111124
except (ValueError, RuntimeError) as e:
112125
logger.info(e)
113126
else:

tests/algorithms/scaling/test_error_model.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,19 +183,24 @@ def test_error_model_on_simulated_data(
183183
)
184184

185185

186-
def test_errormodel(large_reflection_table, test_sg):
186+
@pytest.mark.parametrize("use_stills_filtering", [True, False])
187+
def test_errormodel(large_reflection_table, test_sg, use_stills_filtering):
187188
"""Test the initialisation and methods of the error model."""
188189

189190
Ih_table = IhTable([large_reflection_table], test_sg, nblocks=1)
190191
block = Ih_table.blocked_data_list[0]
191192
params = generated_param()
193+
params.weighting.error_model.basic.stills.min_multiplicity = 2
194+
params.weighting.error_model.basic.stills.min_Isigma = 0.0
192195
params.weighting.error_model.basic.n_bins = 2
193196
params.weighting.error_model.basic.min_Ih = 1.0
194197
em = BasicErrorModel
195198
em.min_reflections_required = 1
196199
error_model = em(basic_params=params.weighting.error_model.basic)
197200
error_model.min_reflections_required = 1
198-
error_model.configure_for_refinement(block)
201+
error_model.configure_for_refinement(
202+
block, use_stills_filtering=use_stills_filtering
203+
)
199204
assert error_model.binner.summation_matrix[0, 1] == 1
200205
assert error_model.binner.summation_matrix[1, 1] == 1
201206
assert error_model.binner.summation_matrix[2, 0] == 1

tests/command_line/test_ssx_reduction.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,11 @@ def test_ssx_reduction(dials_data, tmp_path):
9191
)
9292
assert not result.returncode and not result.stderr
9393
assert (tmp_path / "compute_delta_cchalf.html").is_file()
94+
95+
# will not be able to refine error model due to lack of data, but should rather exit cleanly.
96+
result = subprocess.run(
97+
[shutil.which("dials.refine_error_model"), scale_expts, scale_refls],
98+
cwd=tmp_path,
99+
capture_output=True,
100+
)
101+
assert not result.returncode and not result.stderr

0 commit comments

Comments
 (0)