예제 #1
0
  def __init__(self, crystal, experiment_ids=[0]):

    # The state of the unit cell parameterisation is the reciprocal space
    # orthogonalisation matrix 'B'. The initial state is irrelevant for
    # this model, as composition of a new B matrix and derivatives can be
    # done with just the values of 6 unit cell parameters, without
    # defining axial directions (which are selected by choice of the PDB
    # convention). For this reason also, the axes of the
    # parameters are irrelevant and are set here to None.

    ### Set up the initial state
    istate = None

    ### Set up symmetrizing object
    self._S = symmetrize_reduce_enlarge(crystal.get_space_group())
    self._S.set_orientation(orientation=crystal.get_B())
    X = self._S.forward_independent_parameters()
    dB_dp = self._S.forward_gradients()
    B = self._S.backward_orientation(independent=X).reciprocal_matrix()

    ### Set up the independent parameters, with a change of scale
    p_list = [Parameter(e * 1.e5, name = "g_param_%d" % i) \
              for i, e in enumerate(X)]

    # set up the base class
    ModelParameterisation.__init__(self, crystal, istate, p_list,
                                   experiment_ids=experiment_ids)

    # call compose to calculate all the derivatives
    self.compose()

    return
예제 #2
0
    def __init__(self, beam, goniometer=None, experiment_ids=None):

        # The state of the beam model consists of the s0 vector that it is
        # modelling. The initial state is the direction of this vector at the point
        # of initialisation. Future states are composed by rotations around axes
        # perpendicular to that direction and normalisation specified by the
        # wavenumber (inverse wavelength).

        # Set up the initial state
        if experiment_ids is None:
            experiment_ids = [0]
        s0 = matrix.col(beam.get_s0())
        s0dir = matrix.col(beam.get_unit_s0())
        istate = s0dir

        # build the parameter list
        p_list = self._build_p_list(s0, goniometer)

        # set up the base class
        ModelParameterisation.__init__(self,
                                       beam,
                                       istate,
                                       p_list,
                                       experiment_ids=experiment_ids)

        # call compose to calculate all the derivatives
        self.compose()

        return
예제 #3
0
  def __init__(self, crystal, experiment_ids=None):

    # The state of the unit cell parameterisation is the reciprocal space
    # orthogonalisation matrix 'B'. The initial state is irrelevant for
    # this model, as composition of a new B matrix and derivatives can be
    # done with just the values of 6 unit cell parameters, without
    # defining axial directions (which are selected by choice of the PDB
    # convention). For this reason also, the axes of the
    # parameters are irrelevant and are set here to None.

    ### Set up the initial state
    if experiment_ids is None:
      experiment_ids = [0]
    istate = None

    # build the parameter list
    p_list = self._build_p_list(crystal)

    # set up the base class
    ModelParameterisation.__init__(self, crystal, istate, p_list,
                                   experiment_ids=experiment_ids)

    # call compose to calculate all the derivatives
    self.compose()

    return
예제 #4
0
  def __init__(self, crystal, experiment_ids=[0]):

    # The state of a crystal orientation parameterisation is an orientation
    # matrix '[U]'. The initial state is a snapshot of the crystal
    # orientation at the time of initialisation '[U0]'. Future states are
    # composed by rotations around axes of the phi-axis frame by Tait-Bryan
    # angles.
    #
    # [U] = [Phi3][Phi2][Phi1][U0]

    ### Set up the initial state
    istate = crystal.get_U()

    ### Set up the parameters
    phi1 = Parameter(.0, matrix.col((1, 0, 0)), 'angle (mrad)', 'Phi1')
    phi2 = Parameter(.0, matrix.col((0, 1, 0)), 'angle (mrad)', 'Phi2')
    phi3 = Parameter(.0, matrix.col((0, 0, 1)), 'angle (mrad)', 'Phi3')

    # build the parameter list in a specific,  maintained order
    p_list = [phi1, phi2, phi3]

    # set up the base class
    ModelParameterisation.__init__(self, crystal, istate, p_list,
                                   experiment_ids=experiment_ids)

    # call compose to calculate all the derivatives
    self.compose()

    return
예제 #5
0
  def __init__(self, crystal, experiment_ids=None):

    # The state of a crystal orientation parameterisation is an orientation
    # matrix '[U]'. The initial state is a snapshot of the crystal
    # orientation at the time of initialisation '[U0]'. Future states are
    # composed by rotations around axes of the phi-axis frame by Tait-Bryan
    # angles.
    #
    # [U] = [Phi3][Phi2][Phi1][U0]

    # set up the initial state
    if experiment_ids is None:
      experiment_ids = [0]
    istate = crystal.get_U()

    # build the parameter list
    p_list = self._build_p_list()

    # set up the base class
    ModelParameterisation.__init__(self, crystal, istate, p_list,
                                   experiment_ids=experiment_ids)

    # call compose to calculate all the derivatives
    self.compose()

    return
예제 #6
0
  def __init__(self, beam, goniometer=None, experiment_ids=None):

    # The state of the beam model consists of the s0 vector that it is
    # modelling. The initial state is the direction of this vector at the point
    # of initialisation. Future states are composed by rotations around axes
    # perpendicular to that direction and normalisation specified by the
    # wavenumber (inverse wavelength).

    # Set up the initial state
    if experiment_ids is None:
      experiment_ids = [0]
    s0 = matrix.col(beam.get_s0())
    s0dir = matrix.col(beam.get_unit_s0())
    istate = s0dir

    # build the parameter list
    p_list = self._build_p_list(s0, goniometer)

    # set up the base class
    ModelParameterisation.__init__(self, beam, istate, p_list,
                                   experiment_ids=experiment_ids)

    # call compose to calculate all the derivatives
    self.compose()

    return
예제 #7
0
  def __init__(self, detector, experiment_ids=None):

    # The state of a single Panel is its detector matrix d = (d1|d2|d0).
    # However, for the purposes of parameterisation we choose a different
    # vector than d0 to locate the Panel. That's because we want to perform
    # rotations around a point on the detector surface, and d0 points to the
    # corner of the Panel. To avoid excess correlations between 'tilt' and
    # 'twist' angles with the detector distance, we prefer to perform
    # rotations around a point located at the centre of the Panel. This is
    # usually close to point of intersection with the plane normal drawn
    # from the origin of the laboratory frame.
    #
    # Therefore we define:
    #
    # * a vector 'dorg' locating the centre of the single Panel
    # * a pair of orthogonal unit directions 'd1' and 'd2' forming a plane
    #   with its origin at the end of the vector dorg
    # * a third unit direction 'dn', orthogonal to both 'd1' & 'd2'.
    # * offsets to locate the origin d0 of the Panel frame from the
    #   tip of the dorg vector, in terms of the coordinate system
    #   formed by d1, d2 and dn.
    #
    # Held separately in attribute 'models' are:
    # * references to the detector objects contained in this model
    #
    # For this simplified class there is only a single Panel frame
    # and the vector dn is not actually required, because the plane formed
    # by d1 and d2 is coplanar with the sensor plane. Therefore the
    # offset is fully in terms of d1 and d2

    # set up the initial state of the detector parameterisation from the
    # orientation of the single Panel it contains, in terms of the vectors
    # dorg, d1 and d2.

    if experiment_ids is None:
      experiment_ids = [0]

    dat = self._init_core(detector)

    # set up the base class
    ModelParameterisation.__init__(self, detector, dat['istate'], dat['p_list'],
                                   experiment_ids=experiment_ids)

    # call compose to calculate all the derivatives
    self.compose()

    return
예제 #8
0
  def __init__(self, beam, goniometer=None, experiment_ids=[0]):

    # The state of the beam model consists of the s0 vector that it is
    # modelling. The initial state is the direction of this vector at the point
    # of initialisation. Future states are composed by rotations around axes
    # perpendicular to that direction and normalisation specified by the
    # wavenumber (inverse wavelength).
    #
    # The 'models' attribute refers to the beam vector contained by this
    # model.

    ### Set up the initial state
    s0 = matrix.col(beam.get_s0())
    s0dir = matrix.col(beam.get_unit_s0())
    istate = s0dir

    ### Set up the parameters
    if goniometer:
      spindle = matrix.col(goniometer.get_rotation_axis())
      s0_plane_dir2 = s0.cross(spindle).normalize()
      s0_plane_dir1 = s0_plane_dir2.cross(s0).normalize()
    else:
      s0_plane_dir1 = s0.ortho().normalize()
      s0_plane_dir2 = s0.cross(s0_plane_dir1).normalize()

    # rotation around s0_plane_dir1
    mu1 = Parameter(.0, s0_plane_dir1, 'angle (mrad)', 'Mu1')
    # rotation around s0_plane_dir2
    mu2 = Parameter(.0, s0_plane_dir2, 'angle (mrad)', 'Mu2')
    # length of s0
    nu = Parameter(s0.length(), ptype='wavenumber (Angstroem^-1)', name='nu')

    # build the parameter list in a specific,  maintained order
    p_list = [mu1, mu2, nu]

    # set up the base class
    ModelParameterisation.__init__(self, beam, istate, p_list,
                                   experiment_ids=experiment_ids)

    # call compose to calculate all the derivatives
    self.compose()

    return
예제 #9
0
    def __init__(self, detector):

        # the state of the detector model is comprised of:
        #
        # * a vector 'dorg' locating the origin of the detector model
        # * two orthogonal unit directions 'd1' and 'd2' forming a plane
        #   with its origin at the end of the vector dorg
        # * a third unit direction 'dn', orthogonal to both 'd1' & 'd2'.
        # * offsets to locate the origin of each sensor frame from the
        #   tip of the dorg vector, in terms of the coordinate system
        #   formed by d1, d2 and dn.
        #
        # Held separately in attribute 'models' are:
        # * references to the sensor objects contained in this model
        #
        # For this simplified class there is only a single sensor frame
        # and the vector dn is not required, because the plane formed by
        # d1 and d2 is coplanar with the sensor plane. Therefore the
        # offset is fully in terms of d1 and d2

        # set up the initial state of the detector model from the
        # orientation of the single sensor it contains

        # get some vectors we need from the Panel
        panel = detector[0]
        so = matrix.col(panel.get_origin())
        d1 = matrix.col(panel.get_fast_axis())
        d2 = matrix.col(panel.get_slow_axis())
        dn = matrix.col(panel.get_normal())

        # we choose the dorg vector to terminate in the centre of the
        # sensor, and the offset between the end of the dorg vector and
        # the sensor origin is a coordinate matrix with elements in the
        # basis d1, d2, dn
        panel_lim = panel.get_image_size_mm()
        offset = matrix.col(
            (-1.0 * panel_lim[0] / 2.0, -1.0 * panel_lim[1] / 2.0, 0.0))

        dorg = so - offset[0] * d1 - offset[1] * d2

        # Set up the initial state. There are multiple items of interest, so
        # use a dictionary here (note for a single sensor model we can do
        # without the first 3 of these, but will need them for multiple sensors)
        istate = {"d1": d1, "d2": d2, "dn": dn, "offset": offset}

        # set up the parameters.
        # distance from lab origin to detector model plane along its
        # normal, in initial orientation
        distance = panel.get_directed_distance()
        dist = Parameter(distance, dn, "length")

        # shift in the detector model plane to locate dorg, in initial
        # orientation
        shift = dorg - dn * distance
        shift1 = Parameter(shift.dot(d1), d1, "length")
        shift2 = Parameter(shift.dot(d2), d2, "length")

        # rotations of the plane through its origin about:
        # 1) axis normal to initial orientation
        # 2) d1 axis of initial orientation
        # 3) d2 axis of initial orientation
        tau1 = Parameter(0, dn, "angle")
        tau2 = Parameter(0, d1, "angle")
        tau3 = Parameter(0, d2, "angle")

        # build the parameter list in a specific,  maintained order
        p_list = [dist, shift1, shift2, tau1, tau2, tau3]

        # set up the base class
        ModelParameterisation.__init__(self, detector, istate, p_list)

        # call compose to calculate all the derivatives
        self.compose()
예제 #10
0
  def __init__(self, detector):

    # the state of the detector model is comprised of:
    #
    # * a vector 'dorg' locating the origin of the detector model
    # * two orthogonal unit directions 'd1' and 'd2' forming a plane
    #   with its origin at the end of the vector dorg
    # * a third unit direction 'dn', orthogonal to both 'd1' & 'd2'.
    # * offsets to locate the origin of each sensor frame from the
    #   tip of the dorg vector, in terms of the coordinate system
    #   formed by d1, d2 and dn.
    #
    # Held separately in attribute 'models' are:
    # * references to the sensor objects contained in this model
    #
    # For this simplified class there is only a single sensor frame
    # and the vector dn is not required, because the plane formed by
    # d1 and d2 is coplanar with the sensor plane. Therefore the
    # offset is fully in terms of d1 and d2

    # set up the initial state of the detector model from the
    # orientation of the single sensor it contains

    # get some vectors we need from the Panel
    panel = detector[0]
    so = matrix.col(panel.get_origin())
    d1 = matrix.col(panel.get_fast_axis())
    d2 = matrix.col(panel.get_slow_axis())
    dn = matrix.col(panel.get_normal())

    # we choose the dorg vector to terminate in the centre of the
    # sensor, and the offset between the end of the dorg vector and
    # the sensor origin is a coordinate matrix with elements in the
    # basis d1, d2, dn
    panel_lim = panel.get_image_size_mm()
    offset = matrix.col((-1. * panel_lim[0] / 2.,
                         -1. * panel_lim[1] / 2.,
                          0.))

    dorg = so - offset[0] * d1 - offset[1] * d2

    # Set up the initial state. There are multiple items of interest, so
    # use a dictionary here (note for a single sensor model we can do
    # without the first 3 of these, but will need them for multiple sensors)
    istate = {'d1':d1,
              'd2':d2,
              'dn':dn,
              'offset':offset}

    # set up the parameters.
    # distance from lab origin to detector model plane along its
    # normal, in initial orientation
    distance = panel.get_directed_distance()
    dist = Parameter(distance, dn, 'length')

    # shift in the detector model plane to locate dorg, in initial
    # orientation
    shift = dorg - dn * distance
    shift1 = Parameter(shift.dot(d1), d1, 'length')
    shift2 = Parameter(shift.dot(d2), d2, 'length')

    # rotations of the plane through its origin about:
    # 1) axis normal to initial orientation
    # 2) d1 axis of initial orientation
    # 3) d2 axis of initial orientation
    tau1 = Parameter(0, dn, 'angle')
    tau2 = Parameter(0, d1, 'angle')
    tau3 = Parameter(0, d2, 'angle')

    # build the parameter list in a specific,  maintained order
    p_list = [dist, shift1, shift2, tau1, tau2, tau3]

    # set up the base class
    ModelParameterisation.__init__(self, detector, istate, p_list)

    # call compose to calculate all the derivatives
    self.compose()
예제 #11
0
  def __init__(self, detector, experiment_ids=None, level=0):
    """The additional 'level' argument selects which level of the detector
    hierarchy is chosen to determine panel groupings that are treated as
    separate rigid blocks."""
    if experiment_ids is None:
      experiment_ids = [0]

    try:
      h = detector.hierarchy()
    except AttributeError:
      print "This detector does not have a hierarchy"
      raise

    # list the panel groups at the chosen level
    try:
      self._groups = get_panel_groups_at_depth(h, level)
    except AttributeError:
      print "Cannot access the hierarchy at the depth level={0}".format(level)
      raise

    # collect the panel ids for each Panel within the groups
    panels = [p for p in detector]
    self._panel_ids_by_group = [get_panel_ids_at_root(panels, g) for g in self._groups]

    p_list = []
    self._group_ids_by_parameter = []
    istate = []
    self._offsets = []
    self._dir1s = []
    self._dir2s = []

    # loop over the groups, collecting initial parameters and states
    for igp, pnl_ids in enumerate(self._panel_ids_by_group):

      panel_centres_in_lab_frame = []
      for i in pnl_ids:
        pnl = detector[i]
        im_size = pnl.get_image_size_mm()
        cntr = matrix.col(pnl.get_origin()) + \
            0.5 * matrix.col(pnl.get_fast_axis()) * im_size[0] + \
            0.5 * matrix.col(pnl.get_slow_axis()) * im_size[1]
        panel_centres_in_lab_frame.append(cntr)

      # get some vectors we need from the group
      go = matrix.col(self._groups[igp].get_origin())
      d1 = matrix.col(self._groups[igp].get_fast_axis())
      d2 = matrix.col(self._groups[igp].get_slow_axis())
      dn = matrix.col(self._groups[igp].get_normal())

      # we choose the dorg vector for this group to terminate on the group's
      # frame, at a point that we consider close to the centre of the group of
      # panels. This point is defined by taking the 3D centroid of the panel
      # centres then projecting that point onto the group frame.
      centroid = reduce(lambda a,b: a+b, panel_centres_in_lab_frame) / len(
        panel_centres_in_lab_frame)
      try:
        gp_centroid = matrix.col(self._groups[igp].get_ray_intersection(centroid))
        dorg = go + gp_centroid[0] * d1 + gp_centroid[1] * d2
      except RuntimeError: # workaround for a group frame that passes through
        # the origin
        dorg = matrix.col((0., 0., 0.))

      # The offset between the end of the dorg vector and
      # each Panel origin is a coordinate matrix with elements in the basis d1,
      # d2, dn. We need also each Panel's plane directions dir1 and dir2 in
      # terms of d1, d2 and dn.
      offsets, dir1s, dir2s = [], [], []
      #FIXME these dot products would be more efficiently done using a change of
      # basis matrix instead
      for p in [detector[i] for i in pnl_ids]:
        offset = matrix.col(p.get_origin()) - dorg
        offsets.append(matrix.col((offset.dot(d1),
                                   offset.dot(d2),
                                   offset.dot(dn))))
        dir1 = matrix.col(p.get_fast_axis())
        dir1_new_basis = matrix.col((dir1.dot(d1),
                                     dir1.dot(d2),
                                     dir1.dot(dn)))
        dir1s.append(dir1_new_basis)
        dir2 = matrix.col(p.get_slow_axis())
        dir2_new_basis = matrix.col((dir2.dot(d1),
                                     dir2.dot(d2),
                                     dir2.dot(dn)))
        dir2s.append(dir2_new_basis)

      # The offsets and directions in the d1, d2, dn basis are fixed
      # quantities, not dependent on parameter values. Keep these as separate
      # sub-lists for each group
      self._offsets.append(offsets)
      self._dir1s.append(dir1s)
      self._dir2s.append(dir2s)

      # Set up the initial state for this group. This is the basis d1, d2, dn,
      # plus the offset locating the origin of the initial group frame
      gp_offset = go - dorg # lab frame basis
      #FIXME another set of dot products better done by a matrix multiplication
      gp_offset = matrix.col((gp_offset.dot(d1),
                              gp_offset.dot(d2),
                              gp_offset.dot(dn))) # d1,d2,dn basis
      istate.append({'d1':d1, 'd2':d2, 'dn':dn, 'gp_offset':gp_offset})

      # set up the parameters.
      # distance from lab origin to ref_panel plane along its normal,
      # in initial orientation
      distance = self._groups[igp].get_directed_distance()
      dist = Parameter(distance,
        dn, 'length (mm)', 'Group{0}Dist'.format(igp + 1))

      # shift in the detector model plane to locate dorg, in initial
      # orientation
      shift = dorg - dn * distance
      shift1 = Parameter(shift.dot(d1), d1,
        'length (mm)', 'Group{0}Shift1'.format(igp + 1))
      shift2 = Parameter(shift.dot(d2), d2,
        'length (mm)', 'Group{0}Shift2'.format(igp + 1))

      # rotations of the plane through its origin about:
      # 1) axis normal to initial orientation
      # 2) d1 axis of initial orientation
      # 3) d2 axis of initial orientation
      tau1 = Parameter(0, dn, 'angle (mrad)', 'Group{0}Tau1'.format(igp + 1))
      tau2 = Parameter(0, d1, 'angle (mrad)', 'Group{0}Tau2'.format(igp + 1))
      tau3 = Parameter(0, d2, 'angle (mrad)', 'Group{0}Tau3'.format(igp + 1))

      # extend the parameter list with those pertaining to this group
      p_list.extend([dist, shift1, shift2, tau1, tau2, tau3])
      self._group_ids_by_parameter.extend([igp] * 6)

    # set up the base class
    ModelParameterisation.__init__(self, detector, istate, p_list,
                                   experiment_ids=experiment_ids,
                                   is_multi_state=True)

    # call compose to calculate all the derivatives
    self.compose()

    return
예제 #12
0
  def __init__(self, detector, beam, experiment_ids=None):

    # The state of each Panel in the detector model is its matrix
    # d = (d1|d2|d0). We need to define a new coordinate system rigidly
    # attached to the detector model in which to express the
    # parameterisation and compose each of the Panel states.
    #
    # We define:
    #
    # * a vector 'dorg' locating a point in laboratory space that moves with
    #   the rigid body of the detector and thus is fixed wrt each of the
    #   Panels.
    # * A pair of orthogonal unit directions 'd1' and 'd2' forming a plane
    #   with its origin at the end of the vector dorg.
    # * a third unit direction 'dn', orthogonal to both 'd1' & 'd2'.
    # * offsets to locate the origin of each panel frame from the
    #   tip of the dorg vector, in terms of the coordinate system
    #   formed by d1, d2 and dn.
    #
    # Held separately in attribute 'models' are:
    # * references to detector objects contained in this model

    # set up the initial state of the detector model from the
    # orientation of whichever Panel has its centre most closely
    # located to the direct beam intersection. Call this 'mid_panel'

    if experiment_ids is None:
      experiment_ids = [0]
    beam_centres = [matrix.col(p.get_beam_centre(beam.get_unit_s0())) \
                    for p in detector]
    panel_centres = [0.5 * matrix.col(p.get_image_size_mm())
                     for p in detector]
    beam_to_centres = [(a - b).length() for a, b in \
                      zip(beam_centres, panel_centres)]
    mid_panel_id = beam_to_centres.index(min(beam_to_centres))
    mid_panel = detector[mid_panel_id]

    # get some vectors we need from the mid_panel
    so = matrix.col(mid_panel.get_origin())
    d1 = matrix.col(mid_panel.get_fast_axis())
    d2 = matrix.col(mid_panel.get_slow_axis())
    dn = matrix.col(mid_panel.get_normal())

    # we choose the dorg vector to terminate in the centre of the mid_panel,
    # and the offset between the end of the dorg vector and each Panel
    # origin is a coordinate matrix with elements in the basis d1, d2, dn.
    # We need also each Panel's plane directions dir1 and dir2 in terms of
    # d1, d2 and dn.
    mid_panel_centre = panel_centres[mid_panel_id]
    dorg = so + mid_panel_centre[0] * d1 + mid_panel_centre[1] * d2

    offsets, dir1s, dir2s = [], [], []
    for p in detector:
      offset = matrix.col(p.get_origin()) - dorg
      offsets.append(matrix.col((offset.dot(d1),
                                 offset.dot(d2),
                                 offset.dot(dn))))
      dir1 = matrix.col(p.get_fast_axis())
      dir1_new_basis = matrix.col((dir1.dot(d1),
                                   dir1.dot(d2),
                                   dir1.dot(dn)))
      dir1s.append(dir1_new_basis)
      dir2 = matrix.col(p.get_slow_axis())
      dir2_new_basis = matrix.col((dir2.dot(d1),
                                   dir2.dot(d2),
                                   dir2.dot(dn)))
      dir2s.append(dir2_new_basis)

    # The offsets and directions in the d1, d2, dn basis are fixed
    # quantities, not dependent on parameter values.
    self._offsets = offsets
    self._dir1s = dir1s
    self._dir2s = dir2s

    # Set up the initial state. This is the basis d1, d2, dn.
    istate = {'d1':d1, 'd2':d2, 'dn':dn}

    # set up the parameters.
    # distance from lab origin to mid_panel plane along its normal,
    # in initial orientation
    distance = mid_panel.get_directed_distance()
    dist = Parameter(distance, dn, 'length (mm)', 'Dist')

    # shift in the detector model plane to locate dorg, in initial
    # orientation
    shift = dorg - dn * distance
    shift1 = Parameter(shift.dot(d1), d1, 'length (mm)', 'Shift1')
    shift2 = Parameter(shift.dot(d2), d2, 'length (mm)', 'Shift2')

    # rotations of the plane through its origin about:
    # 1) axis normal to initial orientation
    # 2) d1 axis of initial orientation
    # 3) d2 axis of initial orientation
    tau1 = Parameter(0, dn, 'angle (mrad)', 'Tau1')
    tau2 = Parameter(0, d1, 'angle (mrad)', 'Tau2')
    tau3 = Parameter(0, d2, 'angle (mrad)', 'Tau3')

    # build the parameter list in a specific,  maintained order
    p_list = [dist, shift1, shift2, tau1, tau2, tau3]

    # set up the base class
    ModelParameterisation.__init__(self, detector, istate, p_list,
                                   experiment_ids=experiment_ids,
                                   is_multi_state=True)

    # call compose to calculate all the derivatives
    self.compose()

    return