コード例 #1
0
def tc():
    test = _Test()

    # Predict the reflections in place and put in a reflection manager
    ref_predictor = StillsExperimentsPredictor(test.stills_experiments)
    ref_predictor(test.reflections)
    test.refman = ReflectionManagerFactory.from_parameters_reflections_experiments(
        refman_phil_scope.extract(),
        test.reflections,
        test.stills_experiments,
        do_stills=True,
    )
    test.refman.finalise()

    # Build a prediction parameterisation for the stills experiment
    test.pred_param = StillsPredictionParameterisation(
        test.stills_experiments,
        detector_parameterisations=[test.det_param],
        beam_parameterisations=[test.s0_param],
        xl_orientation_parameterisations=[test.xlo_param],
        xl_unit_cell_parameterisations=[test.xluc_param],
    )
    return test
コード例 #2
0
def test_stills_pred_param(tc):
    print("Testing derivatives for StillsPredictionParameterisation")
    print("========================================================")

    # Build a prediction parameterisation for the stills experiment
    pred_param = StillsPredictionParameterisation(
        tc.stills_experiments,
        detector_parameterisations=[tc.det_param],
        beam_parameterisations=[tc.s0_param],
        xl_orientation_parameterisations=[tc.xlo_param],
        xl_unit_cell_parameterisations=[tc.xluc_param],
    )

    # Predict the reflections in place. Must do this ahead of calculating
    # the analytical gradients so quantities like s1 are correct

    ref_predictor = StillsExperimentsPredictor(tc.stills_experiments)
    ref_predictor(tc.reflections)

    # get analytical gradients
    an_grads = pred_param.get_gradients(tc.reflections)

    fd_grads = tc.get_fd_gradients(pred_param, ref_predictor)

    for i, (an_grad, fd_grad) in enumerate(zip(an_grads, fd_grads)):

        # compare FD with analytical calculations
        print(f"\nParameter {i}: {fd_grad['name']}")

        for name in ["dX_dp", "dY_dp", "dDeltaPsi_dp"]:
            print(name)
            a = fd_grad[name]
            b = an_grad[name]

            abs_error = a - b

            fns = five_number_summary(abs_error)
            print(("  summary of absolute errors: %9.6f %9.6f %9.6f " +
                   "%9.6f %9.6f") % fns)
            assert flex.max(flex.abs(abs_error)) < 0.0003
            # largest absolute error found to be about 0.00025 for dY/dp of
            # Crystal0g_param_3. Reject outlying absolute errors and test again.
            iqr = fns[3] - fns[1]

            # skip further stats on errors with an iqr of near zero, e.g. dDeltaPsi_dp
            # for detector parameters, which are all equal to zero
            if iqr < 1.0e-10:
                continue

            sel1 = abs_error < fns[3] + 1.5 * iqr
            sel2 = abs_error > fns[1] - 1.5 * iqr
            sel = sel1 & sel2
            tst = flex.max_index(flex.abs(abs_error.select(sel)))
            tst_val = abs_error.select(sel)[tst]
            n_outliers = sel.count(False)
            print(("  {0} outliers rejected, leaving greatest " +
                   "absolute error: {1:9.6f}").format(n_outliers, tst_val))
            # largest absolute error now 0.000086 for dX/dp of Beam0Mu2
            assert abs(tst_val) < 0.00009

            # Completely skip parameters with FD gradients all zero (e.g. gradients of
            # DeltaPsi for detector parameters)
            sel1 = flex.abs(a) < 1.0e-10
            if sel1.all_eq(True):
                continue

            # otherwise calculate normalised errors, by dividing absolute errors by
            # the IQR (more stable than relative error calculation)
            norm_error = abs_error / iqr
            fns = five_number_summary(norm_error)
            print(("  summary of normalised errors: %9.6f %9.6f %9.6f " +
                   "%9.6f %9.6f") % fns)
            # largest normalised error found to be about 25.7 for dY/dp of
            # Crystal0g_param_3.
            try:
                assert flex.max(flex.abs(norm_error)) < 30
            except AssertionError as e:
                e.args += (
                    f"extreme normalised error value: {flex.max(flex.abs(norm_error))}",
                )
                raise e

            # Reject outlying normalised errors and test again
            iqr = fns[3] - fns[1]
            if iqr > 0.0:
                sel1 = norm_error < fns[3] + 1.5 * iqr
                sel2 = norm_error > fns[1] - 1.5 * iqr
                sel = sel1 & sel2
                tst = flex.max_index(flex.abs(norm_error.select(sel)))
                tst_val = norm_error.select(sel)[tst]
                n_outliers = sel.count(False)

                # most outliers found for for dY/dp of Crystal0g_param_3 (which had
                # largest errors, so no surprise there).
                try:
                    assert n_outliers < 250
                except AssertionError as e:
                    e.args += (f"too many outliers rejected: {n_outliers}", )
                    raise e

                print(
                    ("  {0} outliers rejected, leaving greatest " +
                     "normalised error: {1:9.6f}").format(n_outliers, tst_val))
                # largest normalied error now about -4. for dX/dp of Detector0Tau1
                assert abs(tst_val) < 6, f"should be < 6, not {tst_val}"
コード例 #3
0
def test():
    # Build models, with a larger crystal than default in order to get plenty of
    # reflections on the 'still' image
    overrides = """
  geometry.parameters.crystal.a.length.range=40 50;
  geometry.parameters.crystal.b.length.range=40 50;
  geometry.parameters.crystal.c.length.range=40 50;
  geometry.parameters.random_seed = 42"""

    master_phil = parse(
        """
      include scope dials.test.algorithms.refinement.geometry_phil
      """,
        process_includes=True,
    )

    models = Extract(master_phil, overrides)

    mydetector = models.detector
    mygonio = models.goniometer
    mycrystal = models.crystal
    mybeam = models.beam

    # Build a mock scan for a 3 degree sweep
    from dxtbx.model import ScanFactory

    sf = ScanFactory()
    myscan = sf.make_scan(
        image_range=(1, 1),
        exposure_times=0.1,
        oscillation=(0, 3.0),
        epochs=list(range(1)),
        deg=True,
    )
    sweep_range = myscan.get_oscillation_range(deg=False)

    # Create parameterisations of these models
    det_param = DetectorParameterisationSinglePanel(mydetector)
    s0_param = BeamParameterisation(mybeam, mygonio)
    xlo_param = CrystalOrientationParameterisation(mycrystal)
    xluc_param = CrystalUnitCellParameterisation(mycrystal)

    # Create a scans ExperimentList, only for generating reflections
    experiments = ExperimentList()
    experiments.append(
        Experiment(
            beam=mybeam,
            detector=mydetector,
            goniometer=mygonio,
            scan=myscan,
            crystal=mycrystal,
            imageset=None,
        ))

    # Create a stills ExperimentList
    stills_experiments = ExperimentList()
    stills_experiments.append(
        Experiment(beam=mybeam,
                   detector=mydetector,
                   crystal=mycrystal,
                   imageset=None))

    # Generate rays - only to work out which hkls are predicted
    ray_predictor = ScansRayPredictor(experiments, sweep_range)
    resolution = 2.0
    index_generator = IndexGenerator(
        mycrystal.get_unit_cell(),
        space_group(space_group_symbols(1).hall()).type(),
        resolution,
    )
    indices = index_generator.to_array()
    rays = ray_predictor(indices)

    # Make a standard reflection_table and copy in the ray data
    reflections = flex.reflection_table.empty_standard(len(rays))
    reflections.update(rays)

    # Build a standard prediction parameterisation for the stills experiment to do
    # FD calculation (not used for its analytical gradients)
    pred_param = StillsPredictionParameterisation(
        stills_experiments,
        detector_parameterisations=[det_param],
        beam_parameterisations=[s0_param],
        xl_orientation_parameterisations=[xlo_param],
        xl_unit_cell_parameterisations=[xluc_param],
    )

    # Make a managed SphericalRelpStillsReflectionPredictor reflection predictor
    # for the first (only) experiment
    ref_predictor = Predictor(stills_experiments)

    # Predict these reflections in place. Must do this ahead of calculating
    # the analytical gradients so quantities like s1 are correct
    ref_predictor.update()
    ref_predictor.predict(reflections)

    # calculate analytical gradients
    ag = AnalyticalGradients(
        stills_experiments,
        detector_parameterisation=det_param,
        beam_parameterisation=s0_param,
        xl_orientation_parameterisation=xlo_param,
        xl_unit_cell_parameterisation=xluc_param,
    )
    an_grads = ag.get_beam_gradients(reflections)
    an_grads.update(ag.get_crystal_orientation_gradients(reflections))
    an_grads.update(ag.get_crystal_unit_cell_gradients(reflections))

    # get finite difference gradients
    p_vals = pred_param.get_param_vals()
    deltas = [1.0e-7] * len(p_vals)

    fd_grads = []
    p_names = pred_param.get_param_names()
    for i, delta in enumerate(deltas):

        # save parameter value
        val = p_vals[i]

        # calc reverse state
        p_vals[i] -= delta / 2.0
        pred_param.set_param_vals(p_vals)

        ref_predictor.update()
        ref_predictor.predict(reflections)

        x, y, _ = reflections["xyzcal.mm"].deep_copy().parts()
        s1 = reflections["s1"].deep_copy()
        rev_state = s1

        # calc forward state
        p_vals[i] += delta
        pred_param.set_param_vals(p_vals)

        ref_predictor.update()
        ref_predictor.predict(reflections)

        x, y, _ = reflections["xyzcal.mm"].deep_copy().parts()
        s1 = reflections["s1"].deep_copy()
        fwd_state = s1

        # reset parameter to saved value
        p_vals[i] = val

        # finite difference - currently for s1 only
        fd = fwd_state - rev_state
        inv_delta = 1.0 / delta
        s1_grads = fd * inv_delta

        # store gradients
        fd_grads.append({"name": p_names[i], "ds1": s1_grads})

    # return to the initial state
    pred_param.set_param_vals(p_vals)

    for i, fd_grad in enumerate(fd_grads):

        ## compare FD with analytical calculations
        print("\n\nParameter {0}: {1}".format(i, fd_grad["name"]))

        print("d[s1]/dp for the first reflection")
        print("finite diff", fd_grad["ds1"][0])
        try:
            an_grad = an_grads[fd_grad["name"]]
        except KeyError:
            continue

        print("checking analytical vs finite difference gradients for s1")
        for a, b in zip(fd_grad["ds1"], an_grad["ds1"]):
            assert a == pytest.approx(b, abs=1e-7)
コード例 #4
0
def test_check_and_remove():

    test = _Test()

    # Override the single panel model and parameterisation. This test function
    # exercises the code for non-hierarchical multi-panel detectors. The
    # hierarchical detector version is tested via test_cspad_refinement.py
    multi_panel_detector = Detector()
    for x in range(3):
        for y in range(3):
            new_panel = make_panel_in_array((x, y), test.detector[0])
            multi_panel_detector.add_panel(new_panel)
    test.detector = multi_panel_detector
    test.stills_experiments[0].detector = multi_panel_detector
    test.det_param = DetectorParameterisationMultiPanel(multi_panel_detector, test.beam)

    # update the generated reflections
    test.generate_reflections()

    # Predict the reflections in place and put in a reflection manager
    ref_predictor = StillsExperimentsPredictor(test.stills_experiments)
    ref_predictor(test.reflections)
    test.refman = ReflectionManagerFactory.from_parameters_reflections_experiments(
        refman_phil_scope.extract(),
        test.reflections,
        test.stills_experiments,
        do_stills=True,
    )
    test.refman.finalise()

    # Build a prediction parameterisation for the stills experiment
    test.pred_param = StillsPredictionParameterisation(
        test.stills_experiments,
        detector_parameterisations=[test.det_param],
        beam_parameterisations=[test.s0_param],
        xl_orientation_parameterisations=[test.xlo_param],
        xl_unit_cell_parameterisations=[test.xluc_param],
    )

    # A non-hierarchical detector does not have panel groups, thus panels are
    # not treated independently wrt which reflections affect their parameters.
    # As before, setting 792 reflections as the minimum should leave all
    # parameters free, and should not remove any reflections
    options = ar_phil_scope.extract()
    options.min_nref_per_parameter = 792
    ar = AutoReduce(options, pred_param=test.pred_param, reflection_manager=test.refman)
    ar.check_and_remove()

    det_params = test.pred_param.get_detector_parameterisations()
    beam_params = test.pred_param.get_beam_parameterisations()
    xl_ori_params = test.pred_param.get_crystal_orientation_parameterisations()
    xl_uc_params = test.pred_param.get_crystal_unit_cell_parameterisations()
    assert det_params[0].num_free() == 6
    assert beam_params[0].num_free() == 3
    assert xl_ori_params[0].num_free() == 3
    assert xl_uc_params[0].num_free() == 6
    assert len(test.refman.get_obs()) == 823

    # Setting 793 reflections as the minimum fixes 3 unit cell parameters,
    # and removes all those reflections. There are then too few reflections
    # for any parameterisation and all will be fixed, leaving no free
    # parameters for refinement. This fails within PredictionParameterisation,
    # during update so the final 31 reflections are not removed.
    options = ar_phil_scope.extract()
    options.min_nref_per_parameter = 793
    ar = AutoReduce(options, pred_param=test.pred_param, reflection_manager=test.refman)
    with pytest.raises(
        DialsRefineConfigError, match="There are no free parameters for refinement"
    ):
        ar.check_and_remove()

    det_params = test.pred_param.get_detector_parameterisations()
    beam_params = test.pred_param.get_beam_parameterisations()
    xl_ori_params = test.pred_param.get_crystal_orientation_parameterisations()
    xl_uc_params = test.pred_param.get_crystal_unit_cell_parameterisations()
    assert det_params[0].num_free() == 0
    assert beam_params[0].num_free() == 0
    assert xl_ori_params[0].num_free() == 0
    assert xl_uc_params[0].num_free() == 0
    assert len(test.refman.get_obs()) == 823 - 792
コード例 #5
0
def run(verbose = False):

  # Build models, with a larger crystal than default in order to get plenty of
  # reflections on the 'still' image
  overrides = """
  geometry.parameters.crystal.a.length.range=40 50;
  geometry.parameters.crystal.b.length.range=40 50;
  geometry.parameters.crystal.c.length.range=40 50;
  geometry.parameters.random_seed = 42"""

  master_phil = parse("""
      include scope dials.test.algorithms.refinement.geometry_phil
      """, process_includes=True)

  models = Extract(master_phil, overrides)

  mydetector = models.detector
  mygonio = models.goniometer
  mycrystal = models.crystal
  mybeam = models.beam

  # Build a mock scan for a 3 degree sweep
  from dxtbx.model.scan import scan_factory
  sf = scan_factory()
  myscan = sf.make_scan(image_range = (1,1),
                        exposure_times = 0.1,
                        oscillation = (0, 3.0),
                        epochs = range(1),
                        deg = True)
  sweep_range = myscan.get_oscillation_range(deg=False)

  # Create parameterisations of these models
  det_param = DetectorParameterisationSinglePanel(mydetector)
  s0_param = BeamParameterisation(mybeam, mygonio)
  xlo_param = CrystalOrientationParameterisation(mycrystal)
  xluc_param = CrystalUnitCellParameterisation(mycrystal)

  # Create a scans ExperimentList, only for generating reflections
  experiments = ExperimentList()
  experiments.append(Experiment(
        beam=mybeam, detector=mydetector, goniometer=mygonio, scan=myscan,
        crystal=mycrystal, imageset=None))

  # Create a stills ExperimentList
  stills_experiments = ExperimentList()
  stills_experiments.append(Experiment(
        beam=mybeam, detector=mydetector, crystal=mycrystal, imageset=None))

  # Generate rays - only to work out which hkls are predicted
  ray_predictor = ScansRayPredictor(experiments, sweep_range)
  resolution = 2.0
  index_generator = IndexGenerator(mycrystal.get_unit_cell(),
                        space_group(space_group_symbols(1).hall()).type(),
                        resolution)
  indices = index_generator.to_array()
  rays = ray_predictor.predict(indices)

  # Make a standard reflection_table and copy in the ray data
  reflections = flex.reflection_table.empty_standard(len(rays))
  reflections.update(rays)

  # Build a standard prediction parameterisation for the stills experiment to do
  # FD calculation (not used for its analytical gradients)
  pred_param = StillsPredictionParameterisation(stills_experiments,
                 detector_parameterisations = [det_param],
                 beam_parameterisations = [s0_param],
                 xl_orientation_parameterisations = [xlo_param],
                 xl_unit_cell_parameterisations = [xluc_param])

  # Make a managed SphericalRelpStillsReflectionPredictor reflection predictor
  # for the first (only) experiment
  ref_predictor = Predictor(stills_experiments)

  # Predict these reflections in place. Must do this ahead of calculating
  # the analytical gradients so quantities like s1 are correct
  ref_predictor.update()
  ref_predictor.predict(reflections)

  # calculate analytical gradients
  ag = AnalyticalGradients(stills_experiments,
                 detector_parameterisation=det_param,
                 beam_parameterisation=s0_param,
                 xl_orientation_parameterisation=xlo_param,
                 xl_unit_cell_parameterisation=xluc_param)
  an_grads = ag.get_beam_gradients(reflections)
  an_grads.update(ag.get_crystal_orientation_gradients(reflections))
  an_grads.update(ag.get_crystal_unit_cell_gradients(reflections))

  # get finite difference gradients
  p_vals = pred_param.get_param_vals()
  deltas = [1.e-7] * len(p_vals)

  fd_grads = []
  p_names = pred_param.get_param_names()
  for i in range(len(deltas)):

    # save parameter value
    val = p_vals[i]

    # calc reverse state
    p_vals[i] -= deltas[i] / 2.
    pred_param.set_param_vals(p_vals)

    ref_predictor.update()
    ref_predictor.predict(reflections)

    x, y, _ = reflections['xyzcal.mm'].deep_copy().parts()
    delpsi = reflections['delpsical.rad'].deep_copy()
    s1 = reflections['s1'].deep_copy()
    rev_state = s1

    # calc forward state
    p_vals[i] += deltas[i]
    pred_param.set_param_vals(p_vals)

    ref_predictor.update()
    ref_predictor.predict(reflections)

    x, y, _ = reflections['xyzcal.mm'].deep_copy().parts()
    delpsi = reflections['delpsical.rad'].deep_copy()
    s1 = reflections['s1'].deep_copy()
    fwd_state = s1

    # reset parameter to saved value
    p_vals[i] = val

    # finite difference - currently for s1 only
    fd = (fwd_state - rev_state)
    inv_delta = 1. / deltas[i]
    s1_grads = fd * inv_delta

    # store gradients
    fd_grads.append({'name':p_names[i], 'ds1':s1_grads})

  # return to the initial state
  pred_param.set_param_vals(p_vals)

  for i, fd_grad in enumerate(fd_grads):

    ## compare FD with analytical calculations
    if verbose: print "\n\nParameter {0}: {1}". format(i,  fd_grad['name'])

    print "d[s1]/dp for the first reflection"
    print 'finite diff', fd_grad['ds1'][0]
    try:
      an_grad = an_grads[fd_grad['name']]
    except KeyError:
      continue

    print 'checking analytical vs finite difference gradients for s1'
    for a, b in zip(fd_grad['ds1'], an_grad['ds1']):
      assert approx_equal(a, b)
    print 'OK'
コード例 #6
0
  def run_stills_pred_param(self, verbose = False):

    if verbose:
      print 'Testing derivatives for StillsPredictionParameterisation'
      print '========================================================'

    # Build a prediction parameterisation for the stills experiment
    pred_param = StillsPredictionParameterisation(self.stills_experiments,
                   detector_parameterisations = [self.det_param],
                   beam_parameterisations = [self.s0_param],
                   xl_orientation_parameterisations = [self.xlo_param],
                   xl_unit_cell_parameterisations = [self.xluc_param])

    # Predict the reflections in place. Must do this ahead of calculating
    # the analytical gradients so quantities like s1 are correct
    from dials.algorithms.refinement.prediction import ExperimentsPredictor
    ref_predictor = ExperimentsPredictor(self.stills_experiments)
    ref_predictor.update()
    ref_predictor.predict(self.reflections)

    # get analytical gradients
    an_grads = pred_param.get_gradients(self.reflections)

    fd_grads = self.get_fd_gradients(pred_param, ref_predictor)

    for i, (an_grad, fd_grad) in enumerate(zip(an_grads, fd_grads)):

      # compare FD with analytical calculations
      if verbose: print "\nParameter {0}: {1}". format(i,  fd_grad['name'])

      for idx, name in enumerate(["dX_dp", "dY_dp", "dDeltaPsi_dp"]):
        if verbose: print name
        a = fd_grad[name]
        b = an_grad[name]

        abs_error = a - b
        denom = a + b

        fns = five_number_summary(abs_error)
        if verbose: print ("  summary of absolute errors: %9.6f %9.6f %9.6f " + \
          "%9.6f %9.6f") % fns
        assert flex.max(flex.abs(abs_error)) < 0.0003
        # largest absolute error found to be about 0.00025 for dY/dp of
        # Crystal0g_param_3. Reject outlying absolute errors and test again.
        iqr = fns[3] - fns[1]

        # skip further stats on errors with an iqr of near zero, e.g. dDeltaPsi_dp
        # for detector parameters, which are all equal to zero
        if iqr < 1.e-10:
          continue

        sel1 = abs_error < fns[3] + 1.5 * iqr
        sel2 = abs_error > fns[1] - 1.5 * iqr
        sel = sel1 & sel2
        tst = flex.max_index(flex.abs(abs_error.select(sel)))
        tst_val = abs_error.select(sel)[tst]
        n_outliers = sel.count(False)
        if verbose: print ("  {0} outliers rejected, leaving greatest " + \
          "absolute error: {1:9.6f}").format(n_outliers, tst_val)
        # largest absolute error now 0.000086 for dX/dp of Beam0Mu2
        assert abs(tst_val) < 0.00009

        # Completely skip parameters with FD gradients all zero (e.g. gradients of
        # DeltaPsi for detector parameters)
        sel1 = flex.abs(a) < 1.e-10
        if sel1.all_eq(True):
          continue

        # otherwise calculate normalised errors, by dividing absolute errors by
        # the IQR (more stable than relative error calculation)
        norm_error = abs_error / iqr
        fns = five_number_summary(norm_error)
        if verbose: print ("  summary of normalised errors: %9.6f %9.6f %9.6f " + \
          "%9.6f %9.6f") % fns
        # largest normalised error found to be about 25.7 for dY/dp of
        # Crystal0g_param_3.
        try:
          assert flex.max(flex.abs(norm_error)) < 30
        except AssertionError as e:
          e.args += ("extreme normalised error value: {0}".format(
                     flex.max(flex.abs(norm_error))),)
          raise e

        # Reject outlying normalised errors and test again
        iqr = fns[3] - fns[1]
        if iqr > 0.:
          sel1 = norm_error < fns[3] + 1.5 * iqr
          sel2 = norm_error > fns[1] - 1.5 * iqr
          sel = sel1 & sel2
          tst = flex.max_index(flex.abs(norm_error.select(sel)))
          tst_val = norm_error.select(sel)[tst]
          n_outliers = sel.count(False)

          # most outliers found for for dY/dp of Crystal0g_param_3 (which had
          # largest errors, so no surprise there).
          try:
            assert n_outliers < 250
          except AssertionError as e:
            e.args += ("too many outliers rejected: {0}".format(n_outliers),)
            raise e

          if verbose: print ("  {0} outliers rejected, leaving greatest " + \
            "normalised error: {1:9.6f}").format(n_outliers, tst_val)
          # largest normalied error now about -4. for dX/dp of Detector0Tau1
          assert abs(tst_val) < 4.5
    if verbose: print

    return
コード例 #7
0
    def run_stills_pred_param(self, verbose=False):

        if verbose:
            print 'Testing derivatives for StillsPredictionParameterisation'
            print '========================================================'

        # Build a prediction parameterisation for the stills experiment
        pred_param = StillsPredictionParameterisation(
            self.stills_experiments,
            detector_parameterisations=[self.det_param],
            beam_parameterisations=[self.s0_param],
            xl_orientation_parameterisations=[self.xlo_param],
            xl_unit_cell_parameterisations=[self.xluc_param])

        # Predict the reflections in place. Must do this ahead of calculating
        # the analytical gradients so quantities like s1 are correct
        from dials.algorithms.refinement.prediction import ExperimentsPredictor
        ref_predictor = ExperimentsPredictor(self.stills_experiments)
        ref_predictor(self.reflections)

        # get analytical gradients
        an_grads = pred_param.get_gradients(self.reflections)

        fd_grads = self.get_fd_gradients(pred_param, ref_predictor)

        for i, (an_grad, fd_grad) in enumerate(zip(an_grads, fd_grads)):

            # compare FD with analytical calculations
            if verbose: print "\nParameter {0}: {1}".format(i, fd_grad['name'])

            for idx, name in enumerate(["dX_dp", "dY_dp", "dDeltaPsi_dp"]):
                if verbose: print name
                a = fd_grad[name]
                b = an_grad[name]

                abs_error = a - b
                denom = a + b

                fns = five_number_summary(abs_error)
                if verbose:                    print ("  summary of absolute errors: %9.6f %9.6f %9.6f " + \
          "%9.6f %9.6f") % fns
                assert flex.max(flex.abs(abs_error)) < 0.0003
                # largest absolute error found to be about 0.00025 for dY/dp of
                # Crystal0g_param_3. Reject outlying absolute errors and test again.
                iqr = fns[3] - fns[1]

                # skip further stats on errors with an iqr of near zero, e.g. dDeltaPsi_dp
                # for detector parameters, which are all equal to zero
                if iqr < 1.e-10:
                    continue

                sel1 = abs_error < fns[3] + 1.5 * iqr
                sel2 = abs_error > fns[1] - 1.5 * iqr
                sel = sel1 & sel2
                tst = flex.max_index(flex.abs(abs_error.select(sel)))
                tst_val = abs_error.select(sel)[tst]
                n_outliers = sel.count(False)
                if verbose:                    print ("  {0} outliers rejected, leaving greatest " + \
          "absolute error: {1:9.6f}").format(n_outliers, tst_val)
                # largest absolute error now 0.000086 for dX/dp of Beam0Mu2
                assert abs(tst_val) < 0.00009

                # Completely skip parameters with FD gradients all zero (e.g. gradients of
                # DeltaPsi for detector parameters)
                sel1 = flex.abs(a) < 1.e-10
                if sel1.all_eq(True):
                    continue

                # otherwise calculate normalised errors, by dividing absolute errors by
                # the IQR (more stable than relative error calculation)
                norm_error = abs_error / iqr
                fns = five_number_summary(norm_error)
                if verbose:                    print ("  summary of normalised errors: %9.6f %9.6f %9.6f " + \
          "%9.6f %9.6f") % fns
                # largest normalised error found to be about 25.7 for dY/dp of
                # Crystal0g_param_3.
                try:
                    assert flex.max(flex.abs(norm_error)) < 30
                except AssertionError as e:
                    e.args += ("extreme normalised error value: {0}".format(
                        flex.max(flex.abs(norm_error))), )
                    raise e

                # Reject outlying normalised errors and test again
                iqr = fns[3] - fns[1]
                if iqr > 0.:
                    sel1 = norm_error < fns[3] + 1.5 * iqr
                    sel2 = norm_error > fns[1] - 1.5 * iqr
                    sel = sel1 & sel2
                    tst = flex.max_index(flex.abs(norm_error.select(sel)))
                    tst_val = norm_error.select(sel)[tst]
                    n_outliers = sel.count(False)

                    # most outliers found for for dY/dp of Crystal0g_param_3 (which had
                    # largest errors, so no surprise there).
                    try:
                        assert n_outliers < 250
                    except AssertionError as e:
                        e.args += ("too many outliers rejected: {0}".format(
                            n_outliers), )
                        raise e

                    if verbose:                        print ("  {0} outliers rejected, leaving greatest " + \
              "normalised error: {1:9.6f}").format(n_outliers, tst_val)
                    # largest normalied error now about -4. for dX/dp of Detector0Tau1
                    assert abs(
                        tst_val) < 4.5, 'should be about 4 not %s' % tst_val
        if verbose: print

        return