示例#1
0
def load_clsim_table_minimal(fpath, mmap=False, include_overflow=False):
    """Load a CLSim table from disk (optionally compressed with zstd).

    Similar to the `load_clsim_table` function but the full table, including
    under/overflow bins, is kept and no normalization or further processing is
    performed on the table data besides populating the ouptput OrderedDict.

    Parameters
    ----------
    fpath : string
        Path to file to be loaded. If the file has extension 'zst', 'zstd', or
        'zstandard', the file will be decompressed using the `python-zstandard`
        Python library before passing to `fits` for interpreting.

    mmap : bool, optional
        Whether to memory map the table

    include_overflow : bool, optional
        By default, overflow bins (if present) are removed

    Returns
    -------
    table : OrderedDict

    """
    t0 = time()

    table = OrderedDict()
    fpath = expand(fpath)

    if DEBUG:
        wstderr('Loading table from {} ...\n'.format(fpath))

    if isdir(fpath):
        indir = fpath
        if mmap:
            mmap_mode = 'r'
        else:
            mmap_mode = None

        for rel_fpath in listdir(indir):
            key, ext = splitext(rel_fpath)
            abs_fpath = join(indir, rel_fpath)

            if not (isfile(abs_fpath) and ext == '.npy'):
                continue

            if DEBUG:
                wstderr('    loading {} from "{}" ...'.format(key, abs_fpath))

            t1 = time()
            val = np.load(abs_fpath, mmap_mode=mmap_mode)

            # Pull "small" things (less than 10 MiB) into memory so we don't
            # have too many file handles open due to memory mapping
            if mmap and val.nbytes < 10 * 1024**2:
                val = np.copy(val)

            table[key] = val

            if DEBUG:
                wstderr(' ({} ms)\n'.format(np.round((time() - t1) * 1e3, 3)))

    elif isfile(fpath):
        from astropy.io import fits
        fobj = get_decompressd_fobj(fpath)
        pf_table = None
        try:
            pf_table = fits.open(fobj, mode='readonly', memmap=mmap)

            header = pf_table[0].header  # pylint: disable=no-member
            table['table_shape'] = np.array(pf_table[0].data.shape, dtype=int)  # pylint: disable=no-member
            table['group_refractive_index'] = set_explicit_dtype(
                force_little_endian(header['_i3_n_group']))
            table['phase_refractive_index'] = set_explicit_dtype(
                force_little_endian(header['_i3_n_phase']))

            n_dims = len(table['table_shape'])

            new_style = False
            axnames = [None] * n_dims
            binning = [None] * n_dims
            for key in header.keys():
                if not key.startswith('_i3_ax_'):
                    continue
                new_style = True
                axnum = header[key]
                axname = key[len('_i3_ax_'):]
                be0 = header['_i3_{}_min'.format(axname)]
                be1 = header['_i3_{}_max'.format(axname)]
                n_bins = header['_i3_{}_n_bins'.format(axname)]
                power = header.get('_i3_{}_power'.format(axname), 1)
                bin_edges = force_little_endian(pf_table[axnum + 1].data)  # pylint: disable=no-member
                assert np.isclose(bin_edges[0],
                                  be0), '%f .. %f' % (be0, bin_edges[0])
                assert np.isclose(bin_edges[-1],
                                  be1), '%f .. %f' % (be1, bin_edges[-1])
                assert len(bin_edges) == n_bins + 1, '%d vs. %d' % (
                    len(bin_edges), n_bins + 1)
                assert np.allclose(
                    bin_edges,
                    powerspace(start=be0,
                               stop=be1,
                               num=n_bins + 1,
                               power=power),
                )
                axnames[axnum] = axname
                binning[axnum] = bin_edges

            if not new_style:
                if n_dims == 5:
                    axnames = [
                        'r', 'costheta', 't', 'costhetadir', 'deltaphidir'
                    ]
                elif n_dims == 6:
                    axnames = [
                        'r', 'costheta', 'phi', 't', 'costhetadir',
                        'deltaphidir'
                    ]
                else:
                    raise NotImplementedError(
                        '{}-dimensional table not handled for old-style CLSim'
                        ' tables'.format(n_dims))
                binning = [
                    force_little_endian(pf_table[i + 1].data).flat
                    for i in range(len(axnames))
                ]  # pylint: disable=no-member

            for axnum, (axname, bin_edges) in enumerate(zip(axnames, binning)):
                assert axname is not None, 'missing axis %d name' % axnum
                assert bin_edges is not None, 'missing axis %d binning' % axnum

            dtype = np.dtype([(axname, np.float64, dim.size)
                              for axname, dim in zip(axnames, binning)])
            table['binning'] = np.array(tuple(binning), dtype=dtype)

            for keyroot in GENERIC_KEYS:
                keyname = '_i3_' + keyroot
                if keyname in header:
                    val = force_little_endian(header[keyname])
                    if keyroot in (
                            't_is_residual_time',
                            'disable_tilt',
                            'disable_anisotropy',
                    ):
                        val = np.bool8(val)
                    else:
                        val = set_explicit_dtype(val)
                    table[keyroot] = val

            # Get string values from keys that have a prefix preceded by the
            # value all in the key (I3 software had issues saving strings as
            # values in the header "dict" so the workaround was to store the
            # string value in this way)
            for infix in INFIX_KEYS:
                keyroot = '_i3_' + infix + '_'
                for keyname in header.keys():
                    if not keyname.startswith(keyroot):
                        continue
                    val = keyname[len(keyroot):]
                    table[infix] = np.string0(val)

            if include_overflow:
                slicer = (slice(None), ) * n_dims
            else:
                slicer = (slice(1, -1), ) * n_dims
            table['table'] = force_little_endian(pf_table[0].data[slicer])  # pylint: disable=no-member

            wstderr('    (load took {} s)\n'.format(np.round(time() - t0, 3)))

        except:
            wstderr('ERROR: Failed to load "{}"\n'.format(fpath))
            raise

        finally:
            del pf_table
            if hasattr(fobj, 'close'):
                fobj.close()
            del fobj

    else:  # fpath is neither dir nor file
        raise ValueError('Table does not exist at path "{}"'.format(fpath))

    if 'step_length' not in table:
        table['step_length'] = 1

    if 't_is_residual_time' not in table:
        table['t_is_residual_time'] = True

    if DEBUG:
        wstderr('  Total time to load: {} s\n'.format(np.round(time() - t0,
                                                               3)))

    return table
示例#2
0
def load_t_r_theta_table(fpath,
                         depth_idx,
                         scale=1,
                         exponent=1,
                         photon_info=None):
    """Extract info from a file containing a (t, r, theta)-binned Retro table.

    Parameters
    ----------
    fpath : string
        Path to FITS file corresponding to the passed ``depth_idx``.

    depth_idx : int
        Depth index (e.g. from 0 to 59)

    scale : float
        Scaling factor to apply to the photon survival probability from the
        table, e.g. for quantum efficiency. This is applied _before_
        `exponent`. See `Notes` for more info.

    exponent : float >= 0, optional
        Modify probabilties in the table by ``prob = 1 - (1 - prob)**exponent``
        to allow for up- and down-scaling the efficiency of the DOMs. This is
        applied to each DOM's table _after_ `scale`. See `Notes` for more
        info.

    photon_info : None or RetroPhotonInfo namedtuple of dicts
        If None, creates a new RetroPhotonInfo namedtuple with empty dicts to
        fill. If one is provided, the existing component dictionaries are
        updated.

    Returns
    -------
    photon_info : RetroPhotonInfo namedtuple of dicts
        Tuple fields are 'survival_prob', 'theta', 'phi', and 'length'. Each
        dict is keyed by `depth_idx` and values are the arrays loaded
        from the FITS file.

    bin_edges : TimeSphCoord namedtuple
        Each element of the tuple is an array of bin edges.

    Notes
    -----
    The parameters `scale` and `exponent` modify a table's probability `P` by::

        P = 1 - (1 - P*scale)**exponent

    This allows for `scale` (which must be from 0 to 1) to be used for e.g.
    quantum efficiency--which always reduces the detection probability--and
    `exponent` (which must be 0 or greater) to be used as a systematic that
    modifies the post-`scale` probabilities up and down while keeping them
    valid (i.e., between 0 and 1). Larger values of `scale` (i.e., closer to 1)
    indicate a more efficient DOM. Likewise, values of `exponent` greater than
    one scale up the DOM efficiency, while values of `exponent` between 0 and 1
    scale the efficiency down.

    """
    # pylint: disable=no-member
    from astropy.io import fits

    assert 0 <= scale <= 1
    assert exponent >= 0

    if photon_info is None:
        empty_dicts = []
        for _ in RetroPhotonInfo._fields:
            empty_dicts.append({})
        photon_info = RetroPhotonInfo(*empty_dicts)

    with fits.open(expand(fpath)) as table:
        data = force_little_endian(table[0].data)

        if scale == exponent == 1:
            photon_info.survival_prob[depth_idx] = data
        else:
            photon_info.survival_prob[depth_idx] = (
                1 - (1 - data * scale)**exponent)

        photon_info.theta[depth_idx] = force_little_endian(table[1].data)

        photon_info.deltaphi[depth_idx] = force_little_endian(table[2].data)

        photon_info.length[depth_idx] = force_little_endian(table[3].data)

        # Note that we invert (reverse and multiply by -1) time edges; also,
        # no phi edges are defined in these tables.
        data = force_little_endian(table[4].data)
        t = -data[::-1]

        r = force_little_endian(table[5].data)

        # Previously used the following to get "agreement" w/ raw photon sim
        #r_volumes = np.square(0.5 * (r[1:] + r[:-1]))
        #r_volumes = (0.5 * (r[1:] + r[:-1]))**2 * (r[1:] - r[:-1])
        r_volumes = 0.25 * (r[1:]**3 - r[:-1]**3)

        photon_info.survival_prob[depth_idx] /= r_volumes[np.newaxis, :,
                                                          np.newaxis]

        photon_info.time_indep_survival_prob[depth_idx] = np.sum(
            photon_info.survival_prob[depth_idx], axis=0)

        theta = force_little_endian(table[6].data)

        bin_edges = TimeSphCoord(t=t,
                                 r=r,
                                 theta=theta,
                                 phi=np.array([], dtype=t.dtype))

    return photon_info, bin_edges
示例#3
0
    def load_tables(self, force_reload=False):
        """Load all tables that match the `proto_tile_hash`; if multiple tables
        match, then stitch these together into one large TDI table."""
        if self.tables_loaded and not force_reload:
            return
        from astropy.io import fits

        t0 = time()

        x_ref = self.proto_meta['x_min']
        y_ref = self.proto_meta['y_min']
        z_ref = self.proto_meta['z_min']

        must_match = [
            'binmap_hash',
            'geom_hash',
            'dom_tables_hash',
            'time_indices',
            'binwidth',
            'anisotropy',
            # For simplicity, assume all tiles have equal widths. If there's a
            # compelling reason to use something more complicated, we could
            # implement it... but I see no reason to do so now
            'x_width',
            'y_width',
            'z_width'
        ]

        # Work with "survival_prob" table filepaths, which generalizes to all
        # table filepaths (so long as they exist)
        fpaths = glob(
            join(expand(self.tables_dir),
                 'retro_tdi_table_*survival_prob.fits'))

        lowermost_corner = np.array([np.inf] * 3)
        uppermost_corner = np.array([-np.inf] * 3)
        to_load_meta = {}
        for fpath in fpaths:
            meta = self.get_table_metadata(fpath)
            if meta is None:
                continue

            is_match = True
            for key in must_match:
                if meta[key] != self.proto_meta[key]:
                    is_match = False

            if not is_match:
                continue

            # Make sure that the corner falls on the reference grid (within
            # micrometer precision)
            x_float_idx = (meta['x_min'] - x_ref) / self.x_tile_width
            y_float_idx = (meta['y_min'] - y_ref) / self.y_tile_width
            z_float_idx = (meta['z_min'] - z_ref) / self.x_tile_width
            indices_widths = ([x_float_idx, y_float_idx, z_float_idx], [
                self.x_tile_width, self.y_tile_width, self.z_tile_width
            ])
            for float_idx, tile_width in zip(indices_widths):
                if abs(np.round(float_idx) - float_idx) * tile_width >= 1e-6:
                    continue

            # Extend the limits of the tiled volume to include this tile
            lower_corner = [meta['x_min'], meta['y_min'], meta['z_min']]
            upper_corner = [meta['x_max'], meta['y_max'], meta['z_max']]
            lowermost_corner = np.min([lowermost_corner, lower_corner], axis=0)
            uppermost_corner = np.max([uppermost_corner, upper_corner], axis=0)

            # Store the metadata by relative tile index
            rel_idx = tuple(
                int(np.round(i))
                for i in (x_float_idx, y_float_idx, z_float_idx))
            to_load_meta[rel_idx] = meta

        x_min, y_min, z_min = lowermost_corner
        x_max, y_max, z_max = uppermost_corner

        # Figure out how many tiles we _should_ have
        nx_tiles = int(np.round((x_max - x_min) / self.x_tile_width))
        ny_tiles = int(np.round((y_max - y_min) / self.y_tile_width))
        nz_tiles = int(np.round((z_max - z_min) / self.z_tile_width))
        n_tiles = nx_tiles * ny_tiles * nz_tiles
        if len(to_load_meta) < n_tiles:
            raise ValueError('Not enough tiles found! Cannot fill the extents'
                             ' of the outermost extents of the volume defined'
                             ' by the tiles found.')
        elif len(to_load_meta) > n_tiles:
            print(self.proto_meta['tdi_hash'])
            print('x:', self.proto_meta['x_min'], self.proto_meta['x_max'],
                  self.proto_meta['x_width'])
            print('y:', self.proto_meta['y_min'], self.proto_meta['y_max'],
                  self.proto_meta['y_width'])
            print('z:', self.proto_meta['z_min'], self.proto_meta['z_max'],
                  self.proto_meta['z_width'])
            print('')
            for v in to_load_meta.values():
                print(v['tdi_hash'])
                print('x:', v['x_min'], v['x_max'], v['x_width'])
                print('y:', v['y_min'], v['y_max'], v['y_width'])
                print('z:', v['z_min'], v['z_max'], v['z_width'])
                print('')
            raise ValueError(
                'WTF? How did we get here? to_load_meta = %d, n_tiles = %d' %
                (len(to_load_meta), n_tiles))

        # Figure out how many bins in each dimension fill the volume
        nx = int(np.round(nx_tiles * self.x_tile_width / self.binwidth))
        ny = int(np.round(ny_tiles * self.y_tile_width / self.binwidth))
        nz = int(np.round(nz_tiles * self.z_tile_width / self.binwidth))

        # Number of bins per dimension in the tile
        nx_per_tile = int(np.round(self.x_tile_width / self.binwidth))
        ny_per_tile = int(np.round(self.y_tile_width / self.binwidth))
        nz_per_tile = int(np.round(self.z_tile_width / self.binwidth))

        # Create empty arrays to fill
        survival_prob = np.empty((nx, ny, nz), dtype=np.float32)
        if self.use_directionality:
            avg_photon_x = np.empty((nx, ny, nz), dtype=np.float32)
            avg_photon_y = np.empty((nx, ny, nz), dtype=np.float32)
            avg_photon_z = np.empty((nx, ny, nz), dtype=np.float32)
        else:
            avg_photon_x, avg_photon_y, avg_photon_z = None, None, None

        anisotropy_str = generate_anisotropy_str(self.anisotropy)

        tables_meta = {}  #[[[None]*nz_tiles]*ny_tiles]*nx_tiles
        for meta in to_load_meta.values():
            tile_x_idx = int(
                np.round((meta['x_min'] - x_min) / self.x_tile_width))
            tile_y_idx = int(
                np.round((meta['y_min'] - y_min) / self.y_tile_width))
            tile_z_idx = int(
                np.round((meta['z_min'] - z_min) / self.z_tile_width))

            x0_idx = int(np.round((meta['x_min'] - x_min) / self.binwidth))
            y0_idx = int(np.round((meta['y_min'] - y_min) / self.binwidth))
            z0_idx = int(np.round((meta['z_min'] - z_min) / self.binwidth))

            bin_idx_range = (slice(x0_idx, x0_idx + nx_per_tile),
                             slice(y0_idx, y0_idx + ny_per_tile),
                             slice(z0_idx, z0_idx + nz_per_tile))

            kwargs = deepcopy(meta)
            kwargs.pop('table_name')

            to_fill = [('survival_prob', survival_prob)]
            if self.use_directionality:
                to_fill.extend([('avg_photon_x', avg_photon_x),
                                ('avg_photon_y', avg_photon_y),
                                ('avg_photon_z', avg_photon_z)])

            for table_name, table in to_fill:
                fpath = join(
                    self.tables_dir, TDI_TABLE_FNAME_PROTO[-1].format(
                        table_name=table_name,
                        anisotropy_str=anisotropy_str,
                        **kwargs).lower())

                with fits.open(fpath) as fits_table:
                    data = force_little_endian(fits_table[0].data)  # pylint: disable=no-member

                if self.scale != 1 and table_name == 'survival_prob':
                    data = 1 - (1 - data)**self.scale

                table[bin_idx_range] = data

            tables_meta[(tile_x_idx, tile_y_idx, tile_z_idx)] = meta

        # Since we have made it to the end successfully, it is now safe to
        # store the above-computed info to the object for later use
        self.nx, self.ny, self.nz = nx, ny, nz
        self.nx_tiles = nx_tiles
        self.ny_tiles = ny_tiles
        self.nz_tiles = nz_tiles
        self.n_bins = self.nx * self.ny * self.nz
        self.n_tiles = self.nx_tiles * self.ny_tiles * self.nz_tiles
        self.x_min, self.y_min, self.z_min = x_min, y_min, z_min
        self.x_max, self.y_max, self.z_max = x_max, y_max, z_max

        self.survival_prob = survival_prob
        self.avg_photon_x = avg_photon_x
        self.avg_photon_y = avg_photon_y
        self.avg_photon_z = avg_photon_z

        self.tables_meta = tables_meta
        self.tables_loaded = True

        if self.n_tiles == 1:
            tstr = 'tile'
        else:
            tstr = 'tiles'
        print('Loaded %d %s spanning'
              ' x ∈ [%.2f, %.2f) m,'
              ' y ∈ [%.2f, %.2f) m,'
              ' z ∈ [%.2f, %.2f) m;'
              ' bins are (%.3f m)³' %
              (self.n_tiles, tstr, self.x_min, self.x_max, self.y_min,
               self.y_max, self.z_min, self.z_max, self.binwidth))
        print('Time to load: {} s'.format(np.round(time() - t0, 3)))
示例#4
0
        '_{hash}_tile_{tile}_string_{string}_dom_{dom}'
        '_seed_{seed}_n_{n_events}.fits'
        .format(hash=TILESET_HASH, **spec)
    )
    tile_fits = fits.open(
        join(tdi_tile_dir, tile_fname),
        memmap=True,
    )
    header = tile_fits[0].header
    tile = tile_fits[0].data

    # Get rid of underflow and overflow bins
    tile = tile[(slice(1, -1),) * tile.ndim]

    # Convert to little-endian for numba codes (plus native format on Intel)
    tile = force_little_endian(tile)

    if np.product(tile.shape) == 0:
        wstdout(' tile failed!.\n')
        continue

    n_photons = header['_i3_n_photons']
    n_phase = header['_i3_n_phase']
    cos_ckv = 1 / (n_phase * BETA)
    sin_ckv = np.sin(np.arccos(cos_ckv))

    if avg_angsens is None:
        angsens_model = [
            k.replace('_i3_angsens_', '')
            for k in header.keys() if k.startswith('_i3_angsens_')
        ][0]
示例#5
0
def load_clsim_table_minimal(fpath, step_length=None, mmap=False):
    """Load a CLSim table from disk (optionally compressed with zstd).

    Similar to the `load_clsim_table` function but the full table, including
    under/overflow bins, is kept and no normalization or further processing is
    performed on the table data besides populating the ouptput OrderedDict.

    Parameters
    ----------
    fpath : string
        Path to file to be loaded. If the file has extension 'zst', 'zstd', or
        'zstandard', the file will be decompressed using the `python-zstandard`
        Python library before passing to `pyfits` for interpreting.

    mmap : bool, optional
        Whether to memory map the table (if it's stored in a directory
        containing .npy files).

    Returns
    -------
    table : OrderedDict
        Items include
        - 'table_shape' : tuple of int
        - 'table' : np.ndarray
        - 't_indep_table' : np.ndarray (if available)
        - 'n_photons' :
        - 'phase_refractive_index' :
        - 'r_bin_edges' :
        - 'costheta_bin_edges' :
        - 't_bin_edges' :
        - 'costhetadir_bin_edges' :
        - 'deltaphidir_bin_edges' :

    """
    table = OrderedDict()

    fpath = expand(fpath)

    if DEBUG:
        wstderr('Loading table from {} ...\n'.format(fpath))

    if isdir(fpath):
        t0 = time()
        indir = fpath
        if mmap:
            mmap_mode = 'r'
        else:
            mmap_mode = None
        for key in MY_CLSIM_TABLE_KEYS + ['t_indep_table']:
            fpath = join(indir, key + '.npy')
            if DEBUG:
                wstderr('    loading {} from "{}" ...'.format(key, fpath))
            t1 = time()
            if isfile(fpath):
                table[key] = np.load(fpath, mmap_mode=mmap_mode)
            elif key != 't_indep_table':
                raise ValueError(
                    'Could not find file "{}" for loading table key "{}"'
                    .format(fpath, key)
                )
            if DEBUG:
                wstderr(' ({} ms)\n'.format(np.round((time() - t1)*1e3, 3)))
        if step_length is not None and 'step_length' in table:
            assert step_length == table['step_length']
        if DEBUG:
            wstderr('  Total time to load: {} s\n'.format(np.round(time() - t0, 3)))
        return table

    if not isfile(fpath):
        raise ValueError('Table does not exist at path "{}"'.format(fpath))

    if mmap:
        print('WARNING: Cannot memory map a fits or compressed fits file;'
              ' ignoring `mmap=True`.')

    import pyfits
    t0 = time()
    fobj = get_decompressd_fobj(fpath)
    try:
        pf_table = pyfits.open(fobj)

        table['table_shape'] = pf_table[0].data.shape # pylint: disable=no-member
        table['n_photons'] = force_little_endian(
            pf_table[0].header['_i3_n_photons'] # pylint: disable=no-member
        )
        table['group_refractive_index'] = force_little_endian(
            pf_table[0].header['_i3_n_group'] # pylint: disable=no-member
        )
        table['phase_refractive_index'] = force_little_endian(
            pf_table[0].header['_i3_n_phase'] # pylint: disable=no-member
        )
        if step_length is not None:
            table['step_length'] = step_length

        n_dims = len(table['table_shape'])
        if n_dims == 5:
            # Space-time dimensions
            table['r_bin_edges'] = force_little_endian(
                pf_table[1].data # meters # pylint: disable=no-member
            )
            table['costheta_bin_edges'] = force_little_endian(
                pf_table[2].data # pylint: disable=no-member
            )
            table['t_bin_edges'] = force_little_endian(
                pf_table[3].data # nanoseconds # pylint: disable=no-member
            )

            # Photon directionality
            table['costhetadir_bin_edges'] = force_little_endian(
                pf_table[4].data # pylint: disable=no-member
            )
            table['deltaphidir_bin_edges'] = force_little_endian(
                pf_table[5].data # pylint: disable=no-member
            )

        else:
            raise NotImplementedError(
                '{}-dimensional table not handled'.format(n_dims)
            )

        table['table'] = force_little_endian(pf_table[0].data) # pylint: disable=no-member

        wstderr('    (load took {} s)\n'.format(np.round(time() - t0, 3)))

    finally:
        del pf_table
        if hasattr(fobj, 'close'):
            fobj.close()
        del fobj

    return table