Skip to content

metrology

getModulePosition(metrologyFile, moduleId)

Position (in mm) of a module relative to the top left corner of its quadrant. In case of tile-level positions, the the position refers to the center of the top left pixel.

Args

str

Fully qualified path and filename of the metrology file

str

Identifier of the module in question (e.g. 'Q1M2T03')

Returns

ndarray

(x, y)-Position of the module in its quadrant

Raises

In case the moduleId contains invalid module

identifieres

Source code in /usr/src/app/checkouts/readthedocs.org/user_builds/european-xfel-offline-calibration/envs/latest/lib/python3.8/site-packages/cal_tools/metrology.py
def getModulePosition(metrologyFile, moduleId):
    """Position (in mm) of a module relative to the top left
    corner of its quadrant. In case of tile-level positions,
    the the position refers to the center of the top left
    pixel.

    Args
    ----

    metrologyFile : str
        Fully qualified path and filename of the metrology file
    moduleId : str
        Identifier of the module in question (e.g. 'Q1M2T03')

    Returns
    -------

    ndarray:
        (x, y)-Position of the module in its quadrant

    Raises
    ------

    ValueError: In case the moduleId contains invalid module
        identifieres
    """
    # The moduleId does not directly appear as a dataset in the
    # metrology file. Instread, it follows the nomenclature of
    # the detector. LPD-style module identifier have the format
    #
    #   QXMYTZZ
    #
    # where X, Y, and Z are digits. Q denotes the quadrant
    # (X = 1, ..., 4), M the supermodule (Y = 1, ..., 4) and T
    # the tile (Z = 1, ..., 16; with leading zeros).
    modulePattern = re.compile(r'[QMT]\d+')
    # Give the module identifier Q1M1T01, the moduleList splits this
    # into the associated quadrant, supermodule, and tile identifiers:
    # >>> print(moduleList)
    # ['Q1', 'M1', 'T01']
    moduleList = modulePattern.findall(moduleId)
    # The metrology file is stored in hdf5 format. It stores positions
    # hierarchally, starting on the supermodule level. the h5Keys list
    # contains all path that will be accessed in the hdf5 file
    # >>> print(h5Keys)
    # ['Q1', 'Q1/M1', 'Q1/M1/T01']
    h5Keys = ['/'.join(moduleList[:idx+1]) for idx in range(len(moduleList))]

    # Every module of the detector gives its position relative to
    # the top left corner of its parent structure. Every position
    # is stored in the positions array
    positions = []
    # Access file
    with h5py.File(metrologyFile, "r") as fh:
        # Check if the keys actually appear in the metrology file
        for key in h5Keys:
            if key not in fh:
                raise ValueError("Invalid key '{}'".format(key))
        # Extract the positions from the hdf5 groups corresponding
        # to a module, if the module has dataset 'Position'.
        positions = [
            np.asarray(fh[key]['Position']) for key in h5Keys if 'Position' in fh[key]
        ]
    if not positions:
        # This is the case when requesting a quadrant; e.g.
        # getModulePosition('Q1'). Key is valid, but quadrant
        # has no location (yet).
        positions = [[0.0, 0.0]]
    # Convert to numpy array
    positions = np.asarray(positions)
    # Return the sum of all positions retrieved
    return positions.sum(axis=0)

plotSupermoduleData(tileData, metrologyPositions, zoom=1.0, vmin=100.0, vmax=6000.0)

Plots data of a supermodule with tile positions determined by the metrology data.

Parameters

ndarray

Supermodule image data separated in individual tiles. Must have shape (16, 32, 128).

ndarray

Tile positions as retrieved from the metrology file. Must have shape (16, 2)

float, optional

Can enlarge or decrease the size of the plot. Default = 1.

vmin, vmax : float, optional Value range. Default vmin=100., vmax=6000.

Returns

matplotlib.Figure Figure object containing the supermodule image

Source code in /usr/src/app/checkouts/readthedocs.org/user_builds/european-xfel-offline-calibration/envs/latest/lib/python3.8/site-packages/cal_tools/metrology.py
def plotSupermoduleData(tileData, metrologyPositions, zoom=1., vmin=100., vmax=6000.):
    """Plots data of a supermodule with tile positions
    determined by the metrology data.

    Parameters
    ----------

    tileData : ndarray
        Supermodule image data separated in individual tiles.
        Must have shape (16, 32, 128).

    metrologyPositions : ndarray
        Tile positions as retrieved from the metrology file.
        Must have shape (16, 2)

    zoom : float, optional
        Can enlarge or decrease the size of the plot. Default = 1.

    vmin, vmax : float, optional
        Value range. Default vmin=100., vmax=6000.

    Returns
    -------
    matplotlib.Figure
        Figure object containing the supermodule image
    """
    # Data needs to have 16 tiles, each with
    # 32x128 pixels
    assert tileData.shape == (16, 32, 128)

    # Conversion coefficient, required since
    # matplotlib does its business in inches
    mmToInch = 1./25.4 # inch/mm

    # Some constants
    numberOfTiles = 16
    numberOfRows = 8
    numberOfCols = 2
    tileWidth = 65.7 # in mm
    tileHeight = 17.7 # in mm

    # Base width and height are given by spatial
    # extend of the modules. The constants 3.4 and 1
    # are estimated as a best guess for gaps between
    # modules.
    figureWidth = zoom * numberOfCols*(tileWidth + 3.4)*mmToInch
    figureHeight = zoom * numberOfRows*(tileHeight + 1.)*mmToInch
    fig = plt.figure(figsize=(figureWidth, figureHeight))

    # The metrology file references module positions
    bottomRightCornerCoordinates = translateToModuleBL(metrologyPositions)

    # The offset here accounts for the fact that there
    # might be negative x,y values
    offset = np.asarray(
        [min(bottomRightCornerCoordinates[:, 0]),
         min(bottomRightCornerCoordinates[:, 1])]
    )

    # Account for blank borders in the plot
    borderLeft = 0.5 * mmToInch
    borderBottom = 0.5 * mmToInch

    # The height and width of the plot remain
    # constant for a given supermodule
    width = zoom * 65.7 * mmToInch / (figureWidth - 2.*borderLeft)
    height = zoom * 17.7 * mmToInch / (figureHeight - 2.*borderBottom)

    for i in range(numberOfTiles):
        # This is the top left corner of the tile with
        # respect to the top left corner of the supermodule
        x0, y0 = bottomRightCornerCoordinates[i] - offset
        # Transform to figure coordinates
        ax0 = borderLeft + zoom * x0 * mmToInch / (figureWidth - 2.*borderLeft)
        ay0 = borderBottom + zoom * y0 * mmToInch / (figureHeight - 2.*borderBottom)
        # Place the plot in the figure
        ax = fig.add_axes((ax0, ay0, width, height), frameon=False)
        # Do not display axes, tick markers or labels
        ax.tick_params(
            axis='both', left='off', top='off', right='off', bottom='off',
            labelleft='off', labeltop='off', labelright='off', labelbottom='off'
        )
        # Plot the image. If one wanted to have a colorbar
        # the img object would be needed to produce one
        img = ax.imshow(
            tileData[i],
            interpolation='nearest',
            vmin=vmin, vmax=vmax
        )

    return fig

splitChannelDataIntoTiles(channelData, clockwiseOrder=False)

Splits the raw channel data into indiviual tiles

Args

ndarray

Raw channel data. Must have shape (256, 256)

bool, optional

If set to True, the sequence of tiles is given in the clockwise order starting with the top right tile (LPD standard). If set to false, tile data is returned in reading order

Returns

ndarray Same data, but reshaped into (12, 32, 128)

Source code in /usr/src/app/checkouts/readthedocs.org/user_builds/european-xfel-offline-calibration/envs/latest/lib/python3.8/site-packages/cal_tools/metrology.py
def splitChannelDataIntoTiles(channelData, clockwiseOrder=False):
    """Splits the raw channel data into indiviual tiles

    Args
    ----

    channelData : ndarray
        Raw channel data. Must have shape (256, 256)

    clockwiseOrder : bool, optional
        If set to True, the sequence of tiles is given
        in the clockwise order starting with the top
        right tile (LPD standard). If set to false, tile
        data is returned in reading order

    Returns
    -------

    ndarray
        Same data, but reshaped into (12, 32, 128)
    """
    tiles = np.asarray(np.split(channelData, 2, axis=1))
    tiles = np.asarray(np.split(tiles, 8, axis=1))
    orderedTiles = tiles.reshape(16, 32, 128)
    if clockwiseOrder:
        # Naturally, the tile data after splitting is in reading
        # order (i.e. top left tile is first, top right tile is second,
        # etc.). The official LPD tile order however is clockwise,
        # starting with the top right tile. The following array
        # contains indices of tiles in reading order as they would
        # be iterated in clockwise order (starting from the top right)
        readingOrderToClockwise = [1,3,5,7,9,11,13,15,14,12,10,8,6,4,2,0]
        # Return tiles in reading order
        orderedTiles = orderedTiles[readingOrderToClockwise]
    return orderedTiles

splitChannelDataIntoTiles2(channelData, clockwiseOrder=False)

Splits the raw channel data into indiviual tiles

Args

ndarray

Raw channel data. Must have shape (256, 256)

bool, optional

If set to True, the sequence of tiles is given in the clockwise order starting with the top right tile (LPD standard). If set to false, tile data is returned in reading order

Returns

ndarray Same data, but reshaped into (12, 32, 128)

Source code in /usr/src/app/checkouts/readthedocs.org/user_builds/european-xfel-offline-calibration/envs/latest/lib/python3.8/site-packages/cal_tools/metrology.py
def splitChannelDataIntoTiles2(channelData, clockwiseOrder=False):
    """Splits the raw channel data into indiviual tiles

    Args
    ----

    channelData : ndarray
        Raw channel data. Must have shape (256, 256)

    clockwiseOrder : bool, optional
        If set to True, the sequence of tiles is given
        in the clockwise order starting with the top
        right tile (LPD standard). If set to false, tile
        data is returned in reading order

    Returns
    -------

    ndarray
        Same data, but reshaped into (12, 32, 128)
    """
    tiles = np.asarray(np.split(channelData, 8, axis=1))
    tiles = np.asarray(np.split(tiles, 2, axis=1))
    orderedTiles = np.moveaxis(tiles.reshape(16, 128, 32, channelData.shape[2]), 2, 1)
    if clockwiseOrder:
        # Naturally, the tile data after splitting is in reading
        # order (i.e. top left tile is first, top right tile is second,
        # etc.). The official LPD tile order however is clockwise,
        # starting with the top right tile. The following array
        # contains indices of tiles in reading order as they would
        # be iterated in clockwise order (starting from the top right)
        readingOrderToClockwise = list(range(8,16))+list(range(7,-1,-1))
        # Return tiles in reading order
        orderedTiles = orderedTiles[readingOrderToClockwise]
    return orderedTiles

translateToModuleBL(tilePositions)

Tile coordinates within a supermodule with the origin in the bottom left corner.

Parameters

ndarray

Tile positions as retrieved from the LPD metrology file. Must have shape (16, 2)

Returns

ndarray Tile positions relative to the bottom left corner.

Source code in /usr/src/app/checkouts/readthedocs.org/user_builds/european-xfel-offline-calibration/envs/latest/lib/python3.8/site-packages/cal_tools/metrology.py
def translateToModuleBL(tilePositions):
    """Tile coordinates within a supermodule with the
    origin in the bottom left corner.

    Parameters
    ----------

    tilePositions : ndarray
        Tile positions as retrieved from the LPD metrology
        file. Must have shape (16, 2)

    Returns
    -------

    ndarray
        Tile positions relative to the bottom left corner.
    """
    assert tilePositions.shape == (16, 2)
    tileHeight = 17.7 # mm
    # The module origin is the top left corner of the
    # top left tile. So in the clockwise order of LPD
    # tiles, it is the last tile in the list
    moduleOrigin = tilePositions[-1]
    # np.asarray([1., -1.]): inverts the y-axis, since
    # matplotlib.figure coordinate system is on the
    # bottom left pointing up and to the right
    moduleCoords = np.asarray([1., -1.])*(
        # np.asarray([0., tileHeight]): Tile positions
        # are translated from the top left corner to the
        # bottom left corner, i.e. 0 in x, tileHeight in y
        tilePositions + np.asarray([0., tileHeight]) - moduleOrigin
    )
    # In the clockwise order of LPD tiles, the 8th
    # tile in the list is the bottom left tile
    bottomLeft8th = np.asarray([0., moduleCoords[8][1]])
    return moduleCoords - bottomLeft8th