• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

simonsobs / so3g / 9787369276

03 Jul 2024 10:06PM UTC coverage: 49.186% (-0.8%) from 50.0%
9787369276

Pull #180

github

skhrg
fix: several fixes from Matthew's comments

* fix typos
* change scale_jumps to find_quantized_jumps
* fix indexing issues
* better formatting
Pull Request #180: Porting over jump finding stuff

1299 of 2641 relevant lines covered (49.19%)

0.49 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.57
/python/proj/wcs.py
1
import so3g
1✔
2
from . import quat
1✔
3

4
import numpy as np
1✔
5

6
from .ranges import Ranges, RangesMatrix
1✔
7
from . import mapthreads
1✔
8

9
# For coordinate systems we use the following abbreviations:
10
#
11
# - DC: Detector coordinates
12
# - FP: Focal plane coordinates
13
# - CS: Celestial spherical coordinates (likely equatorial)
14
# - NS: Native spherical coordinates of the map projection
15
#
16
# Saying that a quaternion rotation q_B<A "takes A to B", where A and
17
# B are coordinate systems, means that if a vector has coordinates v_A
18
# in system A then its coordinates in system B are:
19
#
20
#      v_B = q_B<A v_A q_B<A*
21
#
22
# The rotation taking detector coordinates to native spherical
23
# coordinates is the product
24
#
25
#     q_NS<DC = q_NS<CS q_CS<FP q_FP<DC
26
#
27
# In this module, quaternion rotations are written as:
28
# - Q_CS<FP: Assembly.Q (vector in samples)
29
# - Q_FP<DC: Assembly.dets (vector in dets)
30
# - Q_NS<CS: Projectionst.q_celestial_to_native
31
#
32
# The Projectionist caches a vector of Q_NS<FP, which is then simply
33
# called "q_native".  This is usually the thing to pass to C++ level.
34

35

36
#: Valid settings for "interpol".  First entry is the default.
37
INTERPOLS = ['nearest', 'bilinear']
1✔
38

39

40
class Projectionist:
1✔
41
    """This class assists with analyzing WCS information to populate data
42
    structures needed for accelerated pointing routines.
43

44
    On instantiation, it carries information about the relation
45
    between celestial spherical coordinates and the Native Spherical
46
    coordinates of the projection, and also the pixelization scheme
47
    for the projection.
48

49
    As in pixell, the code and discussion here uses the term
50
    "geometry" to refer to the combination of an astropy.WCS object
51
    and a 2-d array shape.
52

53
    The following symbols are used in docstrings to represent shapes:
54

55
    * ``n_dets`` - Number of detectors in the focal plane.
56
    * ``n_samps`` - Number of samples in the signal space of each detector.
57
    * ``n_threads`` - Number of threads to use in parallelized computation.
58
    * ``n_mapidx`` - Number of indices required to specify a pixel in
59
      a map.  For simple maps this is probably 2, with the first index
60
      specifying row and the second index specifying column.  But for
61
      tiled maps n_mapidx is 3, with the first axis specifying the
62
      tile number.
63
    * ``n_comp`` - Number of map components, such as Stokes components
64
      or other spin components.  This takes the value 3, for example,
65
      if projection into TQU space has been requested.
66

67
    When coordinate computation or projection routines are called, a
68
    ProjEng (a wrapped C++ object) is instantiated to perform the
69
    accelerated computations.  The spin-composition (i.e. is this a
70
    'T' map or a 'TQU' map) must be specified at this time, to
71
    instantiate the correct accelerated object.
72

73
    Some method parameters are common to many functions and are documented
74
    here for consistency:
75

76
    * ``assembly`` - an Assembly, providing a focal plane (quaternion
77
      offsets for each detector) as well as a boresight vector
78
      (quaternion for each time sample).
79
    * ``comps`` - a string specifying the Stokes components of the
80
      map, for example 'T' or 'TQU'.  When absent, this will be
81
      guessed from the map shape; with 1|2|3 mapping to 'T'|'QU'|'TQU'
82
      respectively.
83
    * ``proj_name`` - a string specifying a projection.  The
84
      nomenclature is mostly the same as the FITS CTYPE identifiers.
85
      Accepted values: ARC, CAR, CEA, TAN, ZEA, Flat, Quat.
86
    * ``shape`` - the shape describing the celestial axes of the map.
87
      (For tiled representation this specifies the parent footprint.)
88
    * ``wcs`` - the WCS describing the celestial axes of the map.
89
      Together with ``shape`` this is a geometry; see pixell.enmap
90
      documentation.
91
    * ``threads`` - the thread assignment, consisting of a list of
92
      RangesMatrix objects.  Each RangesMatrix object must have shape
93
      (n_threads, n_dets, n_samps).  The n_threads does not need to
94
      be the same for every entry in the list.  In TOD-to-map
95
      operations, each entry of this list is processed fully before
96
      proceeding to the next one.  Each entry "ranges" is processed
97
      using (up to) the specified number of threads, such that thread
98
      i performs operations only on the samples included in
99
      ranges[i,:,:].  Most thread assignment routines in this module
100
      will return a list of two RangesMatrix objects,
101
      [ranges_parallel, ranges_serial].  The first item represents the
102
      part of the computation that can be done in parallel, and has
103
      shape (n_threads, n_dets, n_samps).  The ranges_serial object
104
      has shape (1, n_dets, n_samps) and represents any samples that
105
      need to be treated in a single thread.  The ranges_serial is
106
      only non-trivial when interpolation is active.
107
    * ``interpol``: How positions that fall between pixel centers will
108
      be handled. Options are "nearest" (default): Use Nearest
109
      Neighbor interpolation, so a sample takes the value of
110
      whatever pixel is closest; or "bilinear": linearly
111
      interpolate between the four closest pixels. bilinear is
112
      slower (around 60%) but avoids problems caused by a
113
      discontinuous model.
114

115
    Attributes:
116
        naxis: 2-element integer array specifying the map shape (for
117
            the 2 celestial axes).
118
        cdelt: 2-element float array specifying the pixel pitch.
119
        crpix: 2-element float array specifying the pixel coordinates
120
            of the reference point.
121
        proj_name: string, name of the projection.
122
        q_celestial_to_native: quaternion rotation taking celestial
123
            coordinates to the native spherical coordinates of the
124
            projection.
125
        tile_shape: 2-element integer array specifying the tile shape
126
            (this is the shape of one full-size sub-tile, not the
127
            decimation factor on each axis).
128
        tiling: a _Tiling object if the map is tiled, None otherwise.
129

130
    """
131
    @staticmethod
1✔
132
    def get_q(wcs):
1✔
133
        """Analyze a wcs object to compute the quaternion rotation from
134
        celestial to native spherical coordinates.
135

136
        """
137
        alpha0, delta0 = wcs.wcs.crval  # In degrees.
1✔
138
        if (wcs.wcs.phi0 == 0. and wcs.wcs.theta0 == 0.):
1✔
139
            # This is typical for cylindrical projections.
140
            assert((delta0 >= 0 and wcs.wcs.lonpole == 180.0) or
×
141
                   (delta0 <= 0 and wcs.wcs.lonpole ==   0.0))
142
            Q = (quat.euler(1,  delta0 * quat.DEG) *
×
143
                 quat.euler(2, -alpha0 * quat.DEG))
144
        elif (wcs.wcs.phi0 == 0. and wcs.wcs.theta0 == 90.):
1✔
145
            # This is typical for zenithal projections.
146
            assert(wcs.wcs.lonpole == 180.0)
1✔
147
            Q = (quat.euler(2, np.pi) *
1✔
148
                 quat.euler(1, (delta0 - 90)*quat.DEG) *
149
                 quat.euler(2, -alpha0 * quat.DEG))
150
        else:
151
            raise ValueError(f'Unimplemented NSC reference (phi0,theta0)='
×
152
                             '({wcs.wcs.phi0:.2f},{wcs.wcs.theta0:.2f})')
153
        return Q
1✔
154

155
    def __init__(self):
1✔
156
        self._q_fp_to_native = None
1✔
157
        self._q_fp_to_celestial = None
1✔
158
        self.tile_shape = None
1✔
159
        self.active_tiles = None
1✔
160
        self.wcs = None
1✔
161
        self.proj_name = None
1✔
162
        self.q_celestial_to_native = None
1✔
163
        self.interpol = INTERPOLS[0]
1✔
164

165
        self.naxis = np.array([0, 0])
1✔
166
        self.cdelt = np.array([0., 0.])
1✔
167
        self.crpix = np.array([0., 0.])
1✔
168

169
    @property
1✔
170
    def tiling(self):
1✔
171
        if self.tile_shape is None:
1✔
172
            return None
1✔
173
        return _Tiling(self.naxis[::-1], self.tile_shape)
1✔
174

175
    @classmethod
1✔
176
    def for_geom(cls, shape, wcs, interpol=None):
1✔
177
        """Construct a Projectionist for use with the specified "geometry".
178

179
        The shape and wcs are the core information required to prepare
180
        a Projectionist, so this method is likely to be called by
181
        other constructors.
182
        """
183
        self = cls()
1✔
184
        ax1, ax2 = wcs.wcs.lng, wcs.wcs.lat
1✔
185
        ndim = len(shape)
1✔
186
        # The axes are numbered from outside in...
187
        self.naxis = np.array([shape[ndim - ax1 - 1],
1✔
188
                               shape[ndim - ax2 - 1]], dtype=int)
189
        # Get just the celestial part.
190
        wcs = wcs.celestial
1✔
191
        self.wcs = wcs
1✔
192

193
        # Extract the projection name (e.g. CAR)
194
        proj = [c[-3:] for c in wcs.wcs.ctype]
1✔
195
        assert(proj[0] == proj[1])
1✔
196
        proj_name = proj[0]  # Projection name
1✔
197
        self.proj_name = proj_name
1✔
198

199
        # Store the rotation to native spherical coordinates.
200
        self.q_celestial_to_native = self.get_q(wcs)
1✔
201

202
        # Store the grid info.
203
        self.cdelt = np.array(wcs.wcs.cdelt) * quat.DEG
1✔
204
        self.crpix = np.array(wcs.wcs.crpix)
1✔
205

206
        # Pixel interpolation mode
207
        if interpol is None:
1✔
208
            interpol = INTERPOLS[0]
1✔
209
        self.interpol = interpol
1✔
210

211
        return self
1✔
212

213
    @classmethod
1✔
214
    def for_map(cls, emap, wcs=None, interpol=None):
1✔
215
        """Construct a Projectionist for use with maps having the same
216
        geometry as the provided enmap.
217

218
        Args:
219
          emap: enmap from which to extract shape and wcs information.
220
            It is acceptable to pass a bare ndarray here (or anything
221
            with shape attribute), provided that wcs is provided
222
            separately.
223
          wcs: optional WCS object to use instead of emap.wcs.
224
        """
225
        if wcs is None:
×
226
            wcs = emap.wcs
×
227
        return cls.for_geom(emap.shape, wcs, interpol=interpol)
×
228

229
    @classmethod
1✔
230
    def for_source_at(cls, alpha0, delta0, gamma0=0.,
1✔
231
                      proj_name='TAN'):
232
        """Return a pointing-only Projectionist where some particular
233
        equatorial position will be put at the North Pole of the
234
        Native spherical coordinates.
235

236
        """
237
        self = cls()
×
238
        self.proj_name = proj_name
×
239
        assert(gamma0 == 0.)
×
240
        self.q_celestial_to_native = (
×
241
            quat.euler(2, np.pi)
242
            * quat.euler(1, (delta0 - 90)*quat.DEG)
243
            * quat.euler(2, -alpha0 * quat.DEG))
244
        return self
×
245

246
    @classmethod
1✔
247
    def for_tiled(cls, shape, wcs, tile_shape, active_tiles=True, interpol=None):
1✔
248
        """Construct a Projectionist for use with the specified geometry
249
        (shape, wcs), cut into tiles of shape tile_shape.
250

251
        See class documentation for description of standard arguments.
252

253
        Args:
254
            tile_shape: tuple of ints, giving the shape of each tile.
255
            active_tiles: bool or list of ints.  Specifies which tiles
256
                should be considered active.  If True, all tiles are
257
                populated.  If None or False, this will remain
258
                uninitialized and attempts to generate blank maps
259
                (such as calls to zeros or to projection functions
260
                without a target map set) will fail.  Otherwise this
261
                must be a list of integers specifying which tiles to
262
                populate on such calls.
263

264
        """
265
        self = cls.for_geom(shape, wcs, interpol=interpol)
1✔
266
        self.tile_shape = np.array(tile_shape, 'int')
1✔
267
        if active_tiles is True:
1✔
268
            self.active_tiles = list(range(self.tiling.tile_count))
1✔
269
        elif active_tiles in [False, None]:
1✔
270
            self.active_tiles = None
1✔
271
        else:
272
            # Presumably a list of tile numbers.
273
            self.active_tiles = active_tiles
1✔
274
        return self
1✔
275

276
    def _get_pixelizor_args(self):
1✔
277
        """Returns a tuple of arguments that may be passed to the ProjEng
278
        constructor to define the pixelization.
279

280
        """
281
        # All these casts are required because boost-python doesn't
282
        # like numpy scalars.
283
        args = (int(self.naxis[1]), int(self.naxis[0]),
1✔
284
                float(self.cdelt[1]), float(self.cdelt[0]),
285
                float(self.crpix[1]), float(self.crpix[0]))
286
        if self.tiling:
1✔
287
            args += tuple(map(int, self.tile_shape))
1✔
288
            if self.active_tiles is not None:
1✔
289
                args += (self.active_tiles,)
1✔
290
        return args
1✔
291

292
    def get_ProjEng(self, comps='TQU', proj_name=None, get=True,
1✔
293
                    instance=True, interpol=None):
294
        """Returns an so3g.ProjEng object appropriate for use with the
295
        configured geometry.
296

297
        """
298
        if proj_name is None: proj_name = self.proj_name
1✔
299
        tile_suffix = 'Tiled' if self.tiling else 'NonTiled'
1✔
300

301
        # Interpolation mode
302
        if interpol is None:
1✔
303
            interpol = self.interpol
1✔
304
        if interpol in ["nn", "nearest"]:
1✔
305
            interpol_suffix = ""
1✔
306
        elif interpol in ["lin", "bilinear"]:
1✔
307
            interpol_suffix = "_Bilinear"
1✔
308
        else:
309
            raise ValueError("ProjEng interpol '%s' not recognized" % str(interpol))
×
310

311
        projeng_name = f'ProjEng_{proj_name}_{comps}_{tile_suffix}{interpol_suffix}'
1✔
312
        if not get:
1✔
313
            return projeng_name
×
314
        try:
1✔
315
            projeng_cls = getattr(so3g, projeng_name)
1✔
316
        except AttributeError:
×
317
            raise ValueError(f'There is no projector implemented for '
×
318
                             f'pixelization "{proj_name}", components '
319
                             f'"{comps}" (tried "{projeng_name}").')
320
        if not instance:
1✔
321
            return projeng_cls
×
322
        return projeng_cls(self._get_pixelizor_args())
1✔
323

324
    def _cache_q_fp_to_native(self, q_fp_to_celestial):
1✔
325
        """Get q_fp_to_native for argument q_fp_to_celestial, and cache the
326
        result, and return that result later if called with the same
327
        argument.
328

329
        """
330
        if q_fp_to_celestial is not self._q_fp_to_celestial:
1✔
331
            self._q_fp_to_native = self.q_celestial_to_native * q_fp_to_celestial
1✔
332
            self._q_fp_to_celestial = q_fp_to_celestial
1✔
333
        return self._q_fp_to_native
1✔
334

335
    def _guess_comps(self, map_shape):
1✔
336
        if len(map_shape) != 3:
×
337
            raise ValueError('Cannot guess components based on '
×
338
                             'map with %i!=3 dimensions!' % len(map_shape))
339
        if map_shape[0] == 1:
×
340
            return 'T'
×
341
        elif map_shape[0] == 2:
×
342
            return 'QU'
×
343
        elif map_shape[0] == 3:
×
344
            return 'TQU'
×
345
        raise ValueError('No default components for ncomp=%i; '
×
346
                         'set comps=... explicitly.' % map_shape[0])
347

348
    def get_pixels(self, assembly):
1✔
349
        """Get the pixel indices for the provided pointing Assembly.  For each
350
        detector, an int32 array of shape (n_samps, n_mapidx) is
351
        returned.  The value of the first slot of the second axis will
352
        be -1 if and only if the sample's projected position is
353
        outside the map footprint.
354

355
        See class documentation for description of standard arguments.
356

357
        """
358
        projeng = self.get_ProjEng('TQU')
×
359
        q_native = self._cache_q_fp_to_native(assembly.Q)
×
360
        return projeng.pixels(q_native, assembly.dets, None)
×
361

362
    def get_pointing_matrix(self, assembly):
1✔
363
        """Get the pointing matrix information, which is to say both the pixel
364
        indices and the "spin projection factors" for the provided
365
        pointing Assembly.  Returns (pixel_indices, spin_proj) where
366
        pixel_indices is as returned by get_pixels() and spin_proj is
367
        a list of float arrays of shape [n_samps, n_comp].  As an
368
        alternative to on-the-fly computation, this information can be
369
        cached and used for very fast projection operations.
370

371
        See class documentation for description of standard arguments.
372

373
        """
374
        projeng = self.get_ProjEng('TQU')
×
375
        q_native = self._cache_q_fp_to_native(assembly.Q)
×
376
        return projeng.pointing_matrix(q_native, assembly.dets, None, None)
×
377

378
    def get_coords(self, assembly, use_native=False, output=None):
1✔
379
        """Get the spherical coordinates for the provided pointing Assembly.
380
        For each detector, a float64 array of shape [n_samps,4] is
381
        returned.  In the right-most index, the first two components
382
        are the longitude and latitude in radians.  The next two
383
        components are the cosine and sine of the parallactic angle.
384

385
        This routine uses a trivial CAR projection to return results
386
        from the celestial coordinate system (i.e. (lon,lat) =
387
        (RA,dec)), and the parallactic angle will be relative to that
388
        projection.  If you are interested in the parallactic angle of
389
        the native spherical coordinate system (e.g. if you're doing a
390
        tangent plane projection), make a second call specifying
391
        use_native=True.  In this case you might also take a look at
392
        the get_planar() routine.
393

394
        See class documentation for description of standard arguments.
395

396
        """
397
        projeng = self.get_ProjEng('TQU', 'CAR')
×
398
        if use_native:
×
399
            q_native = self._cache_q_fp_to_native(assembly.Q)
×
400
        else:
401
            q_native = assembly.Q
×
402
        return projeng.coords(q_native, assembly.dets, output)
×
403

404
    def get_planar(self, assembly, output=None):
1✔
405
        """Get projection plane coordinates for all detectors at all times.
406
        For each detector, a float64 array of shape [n_samps,4] is
407
        returned.  The first two elements are the x and y projection
408
        plane coordiantes, similar to the "intermediate world
409
        coordinates", in FITS language.  Insofar as FITS ICW has units
410
        of degrees, these coordinates have units of radians.  Indices
411
        2 and 3 carry the cosine and sine of the detector parallactic
412
        angle.
413

414
        """
415
        q_native = self._cache_q_fp_to_native(assembly.Q)
×
416
        projeng = self.get_ProjEng('TQU')
×
417
        return projeng.coords(q_native, assembly.dets, output)
×
418

419
    def zeros(self, super_shape):
1✔
420
        """Return a map, filled with zeros, with shape (super_shape,) +
421
        self.shape.  For tiled maps, this will be a list of map tiles
422
        (with shape (super_shape, ) + tile_shape.  If any tiles are
423
        not active, None is returned in the corresponding slots.
424

425
        """
426
        projeng = self.get_ProjEng('T')
1✔
427
        return projeng.zeros(super_shape)
1✔
428

429
    def to_map(self, signal, assembly, output=None, det_weights=None,
1✔
430
               threads=None, comps=None):
431
        """Project signal into a map.
432

433
        Arguments:
434
          signal (Signal-like): The signal to project.
435
          output (Map-like): The map into which to accumulate the
436
            projected signal.  If None, a map will be initialized internally.
437
          det_weights (shape n_det): Weights to use for each detector.
438

439
        See class documentation for description of standard arguments.
440

441
        """
442
        if output is None and comps is None:
1✔
443
            raise ValueError("Provide an output map or specify component of "
×
444
                             "interest (e.g. comps='TQU').")
445
        if comps is None:
1✔
446
            comps = self._guess_comps(output.shape)
×
447
        projeng = self.get_ProjEng(comps)
1✔
448
        q_native = self._cache_q_fp_to_native(assembly.Q)
1✔
449
        map_out = projeng.to_map(
1✔
450
            output, q_native, assembly.dets, signal, det_weights, threads)
451
        return map_out
1✔
452

453
    def to_weights(self, assembly, output=None, det_weights=None,
1✔
454
                   threads=None, comps=None):
455
        """Project pointing into a weights map.
456

457
        Arguments:
458
          output (Map-like): The weights map into which to accumulate
459
            the projected signal.  If None, a map will be initialized
460
            internally.
461
          det_weights (shape n_det): Weights to use for each detector.
462

463
        See class documentation for description of standard arguments.
464

465
        """
466
        if output is None and comps is None:
1✔
467
            raise ValueError("Provide an output map or specify component of "
×
468
                             "interest (e.g. comps='TQU').")
469
        if comps is None:
1✔
470
            assert(output.shape[0] == output.shape[1])
×
471
            comps = self._guess_comps(output.shape[1:])
×
472
        projeng = self.get_ProjEng(comps)
1✔
473
        q_native = self._cache_q_fp_to_native(assembly.Q)
1✔
474
        map_out = projeng.to_weight_map(
1✔
475
            output, q_native, assembly.dets, det_weights, threads)
476
        return map_out
1✔
477

478
    def from_map(self, src_map, assembly, signal=None, comps=None):
1✔
479
        """De-project from a map, returning a Signal-like object.
480

481
        Arguments:
482
          src_map (Map-like): The map from which to sample.
483
          signal (Signal-like): The object into which to accumulate
484
            the signal.  If not provided, a suitable object will be
485
            created and initialized to zero.
486

487
        See class documentation for description of standard arguments.
488

489
        """
490
        if src_map.ndim == 2:
×
491
            # Promote to (1,...)
492
            src_map = src_map[None]
×
493
        if comps is None:
×
494
            comps = self._guess_comps(src_map.shape)
×
495
        projeng = self.get_ProjEng(comps)
×
496
        q_native = self._cache_q_fp_to_native(assembly.Q)
×
497
        signal_out = projeng.from_map(
×
498
            src_map, q_native, assembly.dets, signal)
499
        return signal_out
×
500

501
    def assign_threads(self, assembly, method='domdir', n_threads=None):
1✔
502
        """Get a thread assignment RangesMatrix.  Different algorithms can be
503
        chosen, though this method does not provide fine-grained
504
        control of algorithm parameters and instead seeks to apply
505
        reasonable defaults.
506

507
        The methods exposed here are:
508

509
        - ``'simple'``: Divides the map into n_threads horizontal
510
          bands, and assigns the pixels in each band to a single
511
          thread.  This tends to be bad for scans that aren't
512
          horizontally correlated, or that don't have equal weight
513
          throughout the map.  The 'domdir' algorithm addresses both
514
          of those problems.
515
        - ``'domdir'``: For constant elevation scans, determines the
516
          dominant scan direction in the map and divides it into bands
517
          parallel to that scan direction.  The thickness of bands is
518
          adjusted so roughly equal numbers of samples fall into each
519
          band.
520
        - ``'tiles'``: In Tiled projections, assign each tile to a
521
          thread.  Some effort is made to balance the total number of
522
          samples over the threads.
523

524
        The returned object can be passed to the ``threads`` argument
525
        of the projection methods in this class.
526

527
        See class documentation for description of standard arguments.
528

529
        Args:
530
          method (str): Algorithm to use.
531

532
        """
533
        if method not in THREAD_ASSIGNMENT_METHODS:
1✔
534
            raise ValueError(f'No thread assignment method "{method}"; '
×
535
                             f'expected one of {THREAD_ASSIGNMENT_METHODS}')
536
        n_threads = mapthreads.get_num_threads(n_threads)
1✔
537

538
        if method == 'simple':
1✔
539
            projeng = self.get_ProjEng('T')
1✔
540
            q_native = self._cache_q_fp_to_native(assembly.Q)
1✔
541
            omp_ivals = projeng.pixel_ranges(q_native, assembly.dets, None, n_threads)
1✔
542
            return wrap_ivals(omp_ivals)
1✔
543

544
        elif method == 'domdir':
1✔
545
            offs_rep = assembly.dets[::100]
1✔
546
            if (self.tiling is not None) and (self.active_tiles is None):
1✔
547
                tile_info = self.get_active_tiles(assembly)
1✔
548
                active_tiles = tile_info['active_tiles']
1✔
549
                self.active_tiles = active_tiles
1✔
550
            else:
551
                active_tiles = None
1✔
552
            return mapthreads.get_threads_domdir(
1✔
553
                assembly.Q, assembly.dets, shape=self.naxis[::-1], wcs=self.wcs,
554
                tile_shape=self.tile_shape, active_tiles=active_tiles,
555
                n_threads=n_threads, offs_rep=offs_rep)
556

557
        elif method == 'tiles':
1✔
558
            tile_info = self.get_active_tiles(assembly, assign=n_threads)
1✔
559
            self.active_tiles = tile_info['active_tiles']
1✔
560
            return tile_info['group_ranges']
1✔
561

562
        raise RuntimeError(f"Unimplemented method: {method}.")
×
563

564
    def assign_threads_from_map(self, assembly, tmap, n_threads=None):
1✔
565
        """Assign threads based on a map.
566

567
        The returned object can be passed to the ``threads`` argument
568
        of the projection methods in this object.
569

570
        See class documentation for description of standard arguments.
571

572
        Args:
573
          tmap: Map structure with shape (1,m,n) where each pixel,
574
            when converted to integer, species the index of the thread
575
            to which that pixel should be assigned.
576

577
        """
578
        projeng = self.get_ProjEng('T')
1✔
579
        q_native = self._cache_q_fp_to_native(assembly.Q)
1✔
580
        n_threads = mapthreads.get_num_threads(n_threads)
1✔
581
        omp_ivals = projeng.pixel_ranges(q_native, assembly.dets, tmap, n_threads)
1✔
582
        return wrap_ivals(omp_ivals)
1✔
583

584
    def get_active_tiles(self, assembly, assign=False):
1✔
585
        """For a tiled Projection, figure out what tiles are hit by an
586
        assembly.
587

588
        See class documentation for description of standard arguments.
589

590
        Args:
591
          assign (bool or int): If True or an int, then the function
592
            will also compute a thread assignment, based on assigning
593
            each tile to a particular thread.  If this is an int, it
594
            sets the thread count; if it is simply True then
595
            OMP_NUM_THREADS is used.
596

597
        Returns:
598
          dict : The basic analysis results are:
599

600
            - ``'active_tiles'`` (list of int): sorted, non-repeating
601
              list of tiles that are hit by the assembly.
602
            - ``'hit_counts'`` (list of int): the number of hits in
603
              each tile returned in 'active_tiles', respectively.
604

605
          If the thread assignment took place then the dict will also
606
          contain:
607

608
            - ``'group_tiles'`` (list of lists of ints): There is one
609
              entry per thread, and the entry lists the tiles that
610
              have been assigned to that thread.
611
            - ``'group_ranges'`` (RangesMatrix): The thread
612
              assignments; shape (n_thread, n_det, n_samp).
613

614
        """
615
        if self.tiling is None:
1✔
616
            raise RuntimeError("This Projectionist not set up for Tiled maps.")
1✔
617
        projeng = self.get_ProjEng('T')
1✔
618
        q_native = self._cache_q_fp_to_native(assembly.Q)
1✔
619
        # This returns a G3VectorInt of length n_tiles giving count of hits per tile.
620
        hits = np.array(projeng.tile_hits(q_native, assembly.dets))
1✔
621
        tiles = np.nonzero(hits)[0]
1✔
622
        hits = hits[tiles]
1✔
623
        if assign is True:
1✔
624
            assign = so3g.useful_info()['omp_num_threads']
×
625
        if assign > 0:
1✔
626
            group_n = np.array([0 for g in range(assign)])
1✔
627
            group_tiles = [[] for _ in group_n]
1✔
628
            cands = sorted(list(zip(hits, tiles)), reverse=True) # [(1000, 12), (900, 4), ...]
1✔
629
            for _n, _t in cands:
1✔
630
                idx = group_n.argmin()
1✔
631
                group_n[idx] += _n
1✔
632
                group_tiles[idx].append(_t)
1✔
633
            # Now paint them into Ranges.
634
            R = projeng.tile_ranges(q_native, assembly.dets, group_tiles)
1✔
635
            R = wrap_ivals(R)
1✔
636
            return {
1✔
637
                'active_tiles': list(tiles),
638
                'hit_counts': list(hits),
639
                'group_ranges': R,
640
                'group_tiles': group_tiles,
641
            }
642
        return {
1✔
643
            'active_tiles': list(tiles),
644
            'hit_counts': list(hits),
645
        }
646

647
    _ivals_format = 2
1✔
648

649

650
class _Tiling:
1✔
651
    """Utility class for computations involving tiled maps.
652

653
    """
654
    def __init__(self, shape, tile_shape):
1✔
655
        self.shape = shape[-2:]
1✔
656
        self.tile_shape = tile_shape
1✔
657
        nt0 = int(np.ceil(self.shape[0] / self.tile_shape[0]))
1✔
658
        nt1 = int(np.ceil(self.shape[1] / self.tile_shape[1]))
1✔
659
        self.super_shape = (nt0, nt1)
1✔
660
        self.tile_count = nt0 * nt1
1✔
661
    def tile_dims(self, tile):
1✔
662
        rowcol = self.tile_rowcol(tile)
1✔
663
        return (min(self.shape[0] - rowcol[0] * self.tile_shape[0], self.tile_shape[0]),
1✔
664
                min(self.shape[1] - rowcol[1] * self.tile_shape[1], self.tile_shape[1]))
665
    def tile_rowcol(self, tile):
1✔
666
        if tile >= self.tile_count:
1✔
667
            raise ValueError(f"Request for tile {tile} is out of bounds for "
×
668
                             f"super_shape={self.super_shape}")
669
        return (tile // self.super_shape[1], tile % self.super_shape[1])
1✔
670
    def tile_offset(self, tile):
1✔
671
        row, col = self.tile_rowcol(tile)
1✔
672
        return row * self.tile_shape[0], col * self.tile_shape[1]
1✔
673

674
def wrap_ivals(ivals):
1✔
675
    """Thread computation routines at C++ level return nested lists of
676
    Ranges objects; i.e. something like this::
677

678
      ivals = [
679
               [                             # thread assignments for first "bunch"
680
                 [Ranges, Ranges, ... ],     # for thread 0
681
                 [Ranges, Ranges, ... ],
682
                 ...
683
                 [Ranges, Ranges, ... ],     # for thread n-1.
684
               ],
685
               [                             # thread assignments for second "bunch"
686
                 [Ranges, Ranges, ... ],     # for thread 0
687
               ],
688
              ]
689

690
    This function wraps and returns each highest level entry into a
691
    RangesMatrix, i.e.::
692

693
       wrapped = [
694
               RangesMatrix(n_threads1, n_det, n_samp),
695
               RangesMatrix(n_threads2, n_det, n_samp),
696
       ]
697

698
    Currently all use cases have len(ivals) == 2 and n_threads2 = 1
699
    but the scheme is more general than that.
700

701
    """
702
    return [RangesMatrix([RangesMatrix(y) for y in x]) for x in ivals]
1✔
703

704
THREAD_ASSIGNMENT_METHODS = [
1✔
705
    'simple',
706
    'domdir',
707
    'tiles',
708
]
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc