diff --git a/newsfragments/2552.bugfix b/newsfragments/2552.bugfix new file mode 100644 index 0000000000..4d2f832d38 --- /dev/null +++ b/newsfragments/2552.bugfix @@ -0,0 +1 @@ +``dials.symmetry/dials.cosym``: Avoid crashes when unable to find consistent symmetry during cell reduction routine. diff --git a/src/dials/command_line/symmetry.py b/src/dials/command_line/symmetry.py index 6003f855a3..939a09aeee 100644 --- a/src/dials/command_line/symmetry.py +++ b/src/dials/command_line/symmetry.py @@ -163,7 +163,7 @@ def change_of_basis_ops_to_minimum_cell( Returns: The experiments and reflections mapped to the minimum cell """ - + logger.info("Mapping all input cells to a common minimum cell") median_cell = median_unit_cell(experiments) unit_cells_are_similar = unit_cells_are_similar_to( experiments, median_cell, relative_length_tolerance, absolute_angle_tolerance @@ -184,6 +184,19 @@ def change_of_basis_ops_to_minimum_cell( cb_op_best_to_min = group["best_subsym"].change_of_basis_op_to_minimum_cell() cb_ops = [cb_op_best_to_min * group["cb_op_inp_best"]] * len(experiments) else: + if len(set(centring_symbols)) > 1: + logger.info( + f"Multiple lattice centerings in input cells: {', '.join(list(set(centring_symbols)))}.\n" + + "Attempting to map to a common minimum cell through the most common lattice group." + ) + else: + median_params = ", ".join(f"{i:.2f}" for i in median_cell.parameters()) + logger.info( + f"Some input cells are not sufficiently similar to the median cell:\n {median_params}\n" + + f" within a relative_length_tolerance of {relative_length_tolerance}\n" + + f" and an absolute_angle_tolerance of {absolute_angle_tolerance}.\n" + + "Attempting to map to a common minimum cell through the most common lattice group." + ) groups = [ metric_subgroups( expt.crystal.get_crystal_symmetry(), @@ -198,6 +211,7 @@ def change_of_basis_ops_to_minimum_cell( ) target_group = counter.most_common()[0][0] cb_ops = [] + best_cell = None for expt in experiments: groups = metric_subgroups( expt.crystal.get_crystal_symmetry(), @@ -211,6 +225,28 @@ def change_of_basis_ops_to_minimum_cell( group = g if group: cb_ops.append(group["cb_op_inp_best"]) + if not best_cell: + best_cell = group["best_subsym"].unit_cell() + else: + this_best_cell = group["best_subsym"].unit_cell() + if not best_cell.is_similar_to( + this_best_cell, + relative_length_tolerance, + absolute_angle_tolerance, + ): + best_params = ", ".join( + f"{i:.2f}" for i in best_cell.parameters() + ) + this_params = ", ".join( + f"{i:.2f}" for i in this_best_cell.parameters() + ) + raise ValueError( + "Exiting symmetry analysis: Unable to map input cells to a minimum cell through\n" + + f"a consistent best cell in space group {target_group.info()}.\n" + + f"Incompatible best cells:\n {best_params},\n {this_params},\n" + + f" within a relative_length_tolerance of {relative_length_tolerance}\n" + + f" and an absolute_angle_tolerance of {absolute_angle_tolerance}" + ) else: cb_ops.append(None) logger.info( @@ -329,6 +365,14 @@ def symmetry(experiments, reflection_tables, params=None): params.relative_length_tolerance, params.absolute_angle_tolerance, ) + if None in cb_ops: + indices = [str(i + 1) for i, v in enumerate(cb_ops) if v is None] + raise ValueError( + "Exiting symmetry analysis: Unable to match all cells to target symmetry.\n" + + "This may be avoidable by increasing the absolute_angle_tolerance or relative_length_tolerance,\n" + + "if the cells are similar enough.\n" + + f"Alternatively, remove dataset number{'s' if len(indices) > 1 else ''} {', '.join(indices)} from the input" + ) reflection_tables = eliminate_sys_absent(experiments, reflection_tables) experiments, reflection_tables = apply_change_of_basis_ops( experiments, reflection_tables, cb_ops diff --git a/tests/command_line/test_cosym.py b/tests/command_line/test_cosym.py index af69cd926d..7d4a467874 100644 --- a/tests/command_line/test_cosym.py +++ b/tests/command_line/test_cosym.py @@ -123,6 +123,62 @@ def test_cosym_with_reference(dials_data, run_in_tmp_path): assert len(experiments) == 5 # check we have the right number of expts +def test_synthetic_map_cell_issue(run_in_tmp_path): + # Test that the program cleanly exits if a set of unit cells cannot be mapped to a consistent + # minimum cell in the change_of_basis_ops_to_minimum_cell function. + unit_cell = uctbx.unit_cell((5.46, 9.82, 29.58, 95.24, 94.54, 105.21)) + space_group = sgtbx.space_group_info("P1").group() + experiments, reflections, _ = generate_experiments_reflections( + space_group=space_group, + unit_cell=unit_cell, + unit_cell_volume=10000, + sample_size=7, + map_to_p1=True, + d_min=2.0, + ) + # Set a distribution of cells that will cause the set of cells to fail the cell + # similarity test, and also which don't all find the same best C2 cell. + cells = [ + (5.4, 9.9, 29.9, 95.6, 94.6, 105.1), + (5.4, 9.6, 29.6, 97.4, 92.9, 103.1), + (5.4, 9.9, 29.5, 95.1, 96.6, 106.0), + (5.5, 9.6, 29.4, 94.1, 94.7, 105.7), + (5.5, 9.8, 29.6, 96.4, 93.0, 105.2), + (5.5, 10.1, 29.6, 95.2, 94.5, 105.0), + (5.5, 9.8, 29.7, 95.8, 94.0, 103.9), + ] + for expt, cell in zip(experiments, cells): + expt.crystal.set_unit_cell(uctbx.unit_cell(cell)) + + experiments.as_json("tmp.expt") + expt_file = "tmp.expt" + joint_table = flex.reflection_table.concat(reflections) + joint_table.as_file("tmp.refl") + refl_file = "tmp.refl" + + args = [ + expt_file, + refl_file, + "output.experiments=symmetrized.expt", + "output.reflections=symmetrized.refl", + "output.html=cosym.html", + "output.json=cosym.json", + ] + + with pytest.raises(Sorry) as e: + dials_cosym.run(args=args) + assert str(e).startswith("Sorry: Exiting symmetry analysis") + + # Increase the angle tolerance so that the cells are determined as similar + # and can therefore be correctly mapped to the same cell setting. + args.append("absolute_angle_tolerance=3.0") + dials_cosym.run(args=args) + assert pathlib.Path("symmetrized.refl").is_file() + assert pathlib.Path("symmetrized.expt").is_file() + assert pathlib.Path("cosym.html").is_file() + assert pathlib.Path("cosym.json").is_file() + + @pytest.mark.parametrize( ( "space_group",