File: extending_dials.rst

package info (click to toggle)
dials 3.25.0%2Bdfsg3-3
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 20,112 kB
  • sloc: python: 134,740; cpp: 34,526; makefile: 160; sh: 142
file content (374 lines) | stat: -rw-r--r-- 13,685 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
+++++++++++++++
Extending DIALS
+++++++++++++++

.. contents::
   :depth: 2

Entry points
============

DIALS uses `entry points <https://packaging.python.org/specifications/entry-points/>`_
to define points in the code that can be extended by external developers. These entry
points are defined in the ``dials`` and ``dxtbx`` ``libtbx_refresh.py`` files:

.. literalinclude:: ../../../../libtbx_refresh.py
   :language: python
   :lines: 4-25

Developers implementing their own extensions can register their extensions either in
their package ``libtbx.refresh.py`` if writing a cctbx-style package, or in their
package ``setup.py`` if using the
`setuptools <https://setuptools.readthedocs.io/en/latest/>`_ framework.

The list of installed plugins can be obtained by running the ``dials.plugins`` command::

  $ dials.plugins
  dials.index.basis_vector_search_strategy  Basis vector search strategies
   fft1d (dials.algorithms.indexing.basis_vector_search.strategies via libtbx.dials 0.0.0)
   fft3d (dials.algorithms.indexing.basis_vector_search.strategies via libtbx.dials 0.0.0)
   real_space_grid_search (dials.algorithms.indexing.basis_vector_search.strategies via libtbx.dials 0.0.0)

  dxtbx.scaling_model_ext  scaling models
   KB (dials.algorithms.scaling.model.scaling_model_ext via libtbx.dials 0.0.0)
   array (dials.algorithms.scaling.model.scaling_model_ext via libtbx.dials 0.0.0)
   physical (dials.algorithms.scaling.model.scaling_model_ext via libtbx.dials 0.0.0)

  dxtbx.profile_model  profile models
   gaussian_rs (dials.extensions.gaussian_rs_profile_model_ext via libtbx.dials 0.0.0)

  dials.index.lattice_search_strategy  Lattice search strategies
   low_res_spot_match (dials.algorithms.indexing.lattice_search_strategies via libtbx.dials 0.0.0)


Adding new format classes
=========================

``dxtbx`` now discovers format classes during configuration time instead of
at runtime. New format classes can either be added into the dxtbx/format
directory, registered by other python packages using the
'dxtbx.format' entry point, or installed by the user via the
'dxtbx.install_format' command.

To add a new format class to be distributed with ``dials``, please submit a pull request
to the `dxtbx repository <https://github.com/cctbx/dxtbx>`_.

To register format classes stored in ~/.dxtbx you need to run
'dxtbx.install_format -u' whenever you add or remove format classes.

Writing a new format class
--------------------------

The `dxtbx` format class framework enables beamline staff and users to easily add
support for new detector types and beamlines. In essence all that is needed is to
implement a Python class which extends the Format class to add some specific details
about this detector and the associated beamline/experimental environment.

In particular there are two groups of things which need to be
implemented - a static method named `understand` which will take a
look at the image and return True if it understands it, and a number of
class methods which need to override the construction of the `dxtbx` models.

`understand` Static Method
--------------------------

This method is the key to how the whole framework operates - you write code
which looks at the image to decide whether it is right for this class. If it is
not you must return False - i.e. if you are making a custom class for a given
detector serial number and it is given an image from a different detector.

Ideally your implementation will inherit from a similar Format class
and just apply further customizations.  Your implementation will be
chosen to read the image if it is the most customized, i.e. it derives
from the longest chain of ancestors, all of which claim to understand
the image.

Class Methods
-------------

The class methods need to use the built in factories to construct descriptions
of the experimental apparatus from the image, namely the goniometer, detector,
beam and scan. In many cases the "simple" model will be the best which is
often trivial. In other cases it may be more complex but will hopefully
correspond to an already existing factory method.


As an example, let's pretend your beamline has a "reversed" rotation axis. We can create
a new format class that correctly understands images from your beamline and instantiates
a goniometer model with a reversed direction goniometer:

.. code-block:: python

  from __future__ import absolute_import, division, print_function

  from dxtbx.format.FormatCBFMiniPilatus import FormatCBFMiniPilatus

  class FormatCBFMiniPilatusMyBeamline(FormatCBFMiniPilatus):
      """A class for reading mini CBF format Pilatus images for MyBeamline."""

      @staticmethod
      def understand(image_file):
          """Check the detector serial number to check it is from MyBeamline."""

          header = FormatCBFMiniPilatus.get_cbf_header(image_file)
          for record in header.split("\n"):
              if (
                  "# Detector" in record
                  and "PILATUS" in record
                  and "S/N 42-4242" in header
              ):
                  return True
          return False

      def _goniometer(self):
          """Return a model for a simple single-axis reversed direction goniometer."""

          return self._goniometer_factory.single_axis_reverse()


We can then register this format class in the ``libtbx_refresh.py`` file of our local
``myproject`` ``cctbx`` package:

.. code-block:: python

  import libtbx.pkg_utils
  libtbx.pkg_utils.define_entry_points(
      {
          "dxtbx.format": [
              "FormatCBFMiniPilatusMyBeamline:FormatCBFMiniPilatus = myproject.my_format_module:FormatCBFMiniPilatusMyBeamline",
          ],
      }
  )

More generally, the format of an entry point for dxtbx.format is::

    "FormatMyClass:FormatBaseClass1,FormatBaseClass2 = myproject.myformat:FormatMyClass"

Format classes must be named 'Format*', and must inherit either from
other format classes or from the top-level format class, 'Format'.
Base classes must be given as their original name and must therefore not
contain '.'s.

To view the full hierarchy of registered format classes, run the command
``dxtbx.show_registry``::

  $ dxtbx.show_registry
  Showing hierarchy of classes in the dxtbx registry. The root classes are shown with depth of 1, and subclasses are shown indented and with a higher depth number.

  Depth  Class name
      0  Format
      1    FormatBruker
      2      FormatBrukerFixedChi
      2      FormatBrukerPhotonII
      ...


Extending dials.index
=====================

``dials.index`` defines two possible entry points,
``dials.index.basis_vector_search_strategy`` and
``dials.index.lattice_search_strategy``.


Basis vector search strategies
------------------------------
The ``dials.index.basis_vector_search_strategy`` entry point can be used to extend the
list of possible basis vector search strategies available in DIALS, by delegating the
search for a list of possible real space basis vectors to a strategy. DIALS currently
includes the `fft1d`, `fft3d` and `real_space_grid_search` strategies. A basis vector
search strategy should inherit from the class
:class:`dials.algorithms.indexing.basis_vector_search.strategies.Strategy` and provide
an implementation of the ``find_basis_vectors`` method.

.. code-block:: python

  from libtbx import phil
  from dials.algorithms.indexing.basis_vector_search.strategies import Strategy

  mystrategy_phil_str = """\
  magic_parameter = 42
      .help = "This is a magic parameter."
      .type = float
  """

  phil_scope = phil.parse(mystrategy_phil_str)

  class MyStrategy(Strategy):
      """Basis vector search using my magic algorithm."""

      def find_basis_vectors(self, reciprocal_lattice_vectors):
          """Find a list of likely basis vectors.

          Args:
              reciprocal_lattice_vectors (scitbx.array_family.flex.vec3_double):
                  The list of reciprocal lattice vectors to search for periodicity.

          Returns:
              A tuple containing the list of basis vectors and a flex.bool array
              identifying which reflections were used in indexing.

          """
          used_in_indexing = flex.bool(reciprocal_lattice_vectors.size(), True)
          # determine the list of candidate_basis_vectors
          ...
      return candidate_basis_vectors, used_in_indexing


We can now register this new basis vector search strategy in the ``libtbx_refresh.py``
file of our local ``myproject`` package:

.. code-block:: python

  import libtbx.pkg_utils
  libtbx.pkg_utils.define_entry_points(
      {
          "dials.index.basis_vector_search_strategy": [
              "mystrategy = myproject.mystrategy:MyStrategy",
          ],
      }
  )

Lattice search strategies
-------------------------

An alternative entry point into dials.index is
``dials.index.lattice_search_strategy``, where the entire crystal model search is
delegated to the strategy.

.. code-block:: python

  from libtbx import phil
  from dials.algorithms.indexing.lattice_search_strategies import Strategy

  mystrategy_phil_str = """\
  magic_parameter = 42
      .help = "This is a magic parameter."
      .type = float
  """

  class MyLatticeSearch(Strategy):
      """My magic lattice search strategy."""

      phil_scope = phil.parse(mystrategy_phil_str)

      def find_crystal_models(self, reflections, experiments):
          """Find a list of candidate crystal models.

          Args:
              reflections (dials.array_family.flex.reflection_table):
                  The found spots centroids and associated data

              experiments (dxtbx.model.experiment_list.ExperimentList):
                  The experimental geometry models

          Returns:
              A list of candidate crystal models.

          """
          # determine the list of candidate_crystal_models
          return candidate_crystal_models


As above, register this new lattice search strategy in the ``libtbx_refresh.py``
file of our local ``myproject`` package:

.. code-block:: python

  import libtbx.pkg_utils
  libtbx.pkg_utils.define_entry_points(
      {
          "dials.index.lattice_search_strategy": [
              "mylatticesearch = myproject.mylatticesearch:MyLatticeSearch",
          ],
      }
  )


Extending profile models
========================


Extending dials.scale
=====================

`dials.scale` can be extended by defining new scaling models using the
entry point ``dxtbx.scaling_model_ext``.


Defining a scaling model
------------------------
A new scaling model can be defined, which should inherit from the class
:class:`dials.algorithms.scaling.model.model.ScalingModelBase`. A new
scaling model must define the `from_dict`, `from_data` and
`configure_components` methods, and should also define an `__init__` method. The model
must also define `consecutive_refinement_order` to indicate which order the components
should be refined for the consecutive scaling mode.
The scaling model must be composed of multiplicative components, which must
inherit from
:class:`dials.algorithms.scaling.model.components.scale_components.ScaleComponentBase`.

.. code-block:: python

  from libtbx import phil
  from scitbx.array_family import flex
  from dials.algorithms.scaling.model.model import ScalingModelBase
  from mypath.components import SpecialComponent

  mymodel_phil_str = """\
  special_correction = True
      .help = "Option to toggle the special correction."
      .type = bool
  """

  class MyScalingModel(ScalingModelBase):
      """My scaling model."""

      id_ = "modelname"

      phil_scope = phil.parse(mymodel_phil_str)

      def __init__(self, parameters_dict, configdict, is_scaled=False):
          super(MyScalingModel, self).__init__(configdict, is_scaled)
          if "special" in configdict["corrections"]:
              self._components["special"] = SpecialComponent(
                  parameters_dict["special"]["parameters"],
                  parameters_dict["special"]["parameter_esds"],
              )

      @classmethod
      def from_dict(cls, obj):
          """Create a MyScalingModel from a dictionary."""
          configdict = obj["configuration_parameters"]
          is_scaled = obj["is_scaled"]
          if "special" in configdict["corrections"]:
              parameters = flex.double(obj["special"]["parameters"])
              if "est_standard_devs" in obj["special"]:
                  parameter_esds = flex.double(obj["special"]["est_standard_devs"])
          parameters_dict = {"special : {"parameters" : parameters, "parameter_esds" : parameter_esds}}
          return cls(parameters_dict, configdict, is_scaled)

      @classmethod
      def from_data(cls, params, experiment, reflection_table):
          """Create the MycalingModel from data."""
          configdict = {"corrections": []}
          parameters_dict = {}

          if params.modelname.special_correction:
              configdict["corrections"].append("special")
              parameters_dict["special"] = {
                  "parameters": flex.double([1.0, 1.0, 1.0]),
                  "parameter_esds": None,
              }
          configdict["important_number"] = len(reflection_table)

          return cls(parameters_dict, configdict)

      def configure_components(self, reflection_table, experiment, params):
          """Add the required reflection table data to the model components."""
          if "special" in self.components:
              self.components["special"].data = {"d": reflection_table["d"]}

      def consecutive_refinement_order(self):
          "A nested list of the refinement order".
          return [["special"]]