API reference#

Quick reference#

Geometry#

Polygon

One spherical polygon outer boundary and any holes, in Cartesian XYZ.

MultiPolygon

A union of zero or more Polygon components.

Covering regions#

cover_polygon

Find the cells selected by each polygonal region.

cover_cap

Find the cells selected by each spherical cap.

cover_sweep

Cover the quadrilaterals between two sampled edges of a swath.

Cells#

cell_at

Return the HEALPix cell that each direction falls in.

cell_centers

Return the center direction of each HEALPix cell.

cell_corners

Return the four corner directions of each HEALPix cell.

cell_count

Return the number of HEALPix cells at one resolution.

cell_neighbors

Return the topological neighbors of each HEALPix cell.

Reductions#

Count

Count how many segments cover each cell.

Sum

Add one value per segment into every cell that segment covers.

revisit

Summarize how often each cell is covered across ordered bins.

Results#

Coverage

Segmented HEALPix RING coverage, validated and read-only.

RevisitStats

Per-cell revisit statistics on one thresholded axis.

Geometry#

class polypix.Polygon(outer, *holes)#

One spherical polygon outer boundary and any holes, in Cartesian XYZ.

Coordinates are copied into read-only float64 arrays and the geometry is validated immediately. Ring orientation does not matter.

Parameters:
  • outer (array_like) – The (vertices, 3) outer boundary.

  • *holes (array_like) – Zero or more (vertices, 3) hole boundaries.

Notes

Coverage uses the geometry copied and validated at construction. The coordinate arrays are read-only to catch accidental writes; deliberately changing their NumPy flags and contents is unsupported and does not alter the prepared geometry.

class polypix.MultiPolygon(*polygons)#

A union of zero or more Polygon components.

The union is one input region and therefore one coverage segment. An empty MultiPolygon is a valid empty region.

Covering regions#

polypix.cover_polygon(geometry, *, resolution, mode='center', candidate_cells=None, threads=None, reduce=None)#

Find the cells selected by each polygonal region.

This is the shape of an imaging scene, a detector frame projected on the ground, or an area of interest. Simple array inputs may be convex or concave. Use Polygon for holes and MultiPolygon for a multipart region. Adjacent vertices use the shorter great-circle arc.

Parameters:
  • geometry (array_like, Polygon, MultiPolygon, mapping, or sequence) – One (vertices, 3) polygon, a dense (polygons, vertices, 3) batch, or a sequence of (vertices, 3) arrays when the polygons have different vertex counts. A Polygon or MultiPolygon is one result segment; a sequence of those objects is a batch. An object implementing __geo_interface__, or its mapping directly, may hold a Polygon, MultiPolygon, or one polygonal Feature in longitude and latitude degrees. Pass list(series) for a GeoPandas GeoSeries; its own geo-interface mapping is a FeatureCollection rather than one polygonal region. Vectors are Cartesian directions in one frame of your choosing, finite and nonzero; we normalize the magnitudes internally. The accepted geometry is described under Geometry contract.

  • resolution (int) – HEALPix resolution, 0 through 29. Returned cells satisfy 0 <= cell < cell_count(resolution).

  • mode ({"center", "overlap"}, optional) – "center" selects a cell when its center is inside the region. "overlap" selects it when any part of its area intersects the region, including boundary tangency.

  • candidate_cells (array_like of int, optional) – RING indices at resolution limiting which cells are tested. Without reduce, duplicates and order are ignored. With a reducer, the result has one value per requested cell in the original order, including duplicates. An empty selection returns empty segments or an empty reduced array without dropping any input item.

  • threads (int, optional) – None picks the automatic policy, 1 runs sequentially, and a larger value sets the maximum size of the reusable worker pool, capped by the host.

  • reduce (Count or Sum, optional) – Leave this out to get the segmented Coverage. Pass a reducer to get its accumulated array instead, which also lets us skip building the cell lists where we can. See Ask for the result you need.

Returns:

Coverage or ndarray – Without reduce, one segment per input region. With a reducer, the array described under Count and Sum.

Raises:
  • TypeError – If the inputs have incompatible numeric types.

  • ValueError – If a shape, resolution, vector, candidate index, or the polygon geometry itself is invalid.

  • MemoryError – If the segmented result does not fit in memory. Consider batching the input, or a reducer if counts are what you are after.

Notes

The default mode="center" selects a cell when its center lies inside the polygon or exactly on its boundary. mode="overlap" instead uses the true curved HEALPix cell boundary and returns every cell the region touches. Adjacent regions can therefore share boundary cells.

Geo-interface coordinates must be longitude and latitude in decimal degrees, interpreted directly as angles on a unit sphere, with optional altitude ignored. The datum and frame belong to the caller. Longitude is limited to [-180, 180] and latitude to [-90, 90]. Polypix does not inspect or transform a CRS. Mapping properties, IDs, bounding boxes, and foreign members are ignored; non-polygonal geometries and collections are rejected. Supplied vertices retain Polypix’s shorter great-circle edges rather than GeoJSON’s planar longitude/latitude interpolation.

An empty sequence, a one-dimensional empty array, and a dense (0, vertices, 3) array all describe a batch of zero polygons. A (0, 3) array is unambiguously one polygon with no vertices, and is rejected.

An aligned, C-contiguous float64 dense batch is the cheapest thing to hand over. Other real numeric arrays are converted once, and a ragged sequence is concatenated before the native call, which costs about what concatenating it yourself would. Strictly increasing candidate arrays are borrowed as they are; anything else is sorted and deduplicated internally.

Contiguous arrays are borrowed for the duration of the call. Since the native kernel releases the GIL, do not mutate an input or candidate array from another thread before the call returns. Threading never changes membership, segment order, or cell order on one build and platform.

Validation compares vertex pairs and tests every edge against every vertex, so its cost grows with the square of the vertex count. A densely sampled boundary is better handed to cover_sweep() in short segments than passed as one polygon with hundreds of vertices.

Examples

>>> import numpy as np
>>> import polypix as px
>>> scene = np.array(
...     [
...         [0.99, -0.10, -0.10],
...         [0.99, 0.10, -0.10],
...         [0.99, 0.10, 0.10],
...         [0.99, -0.10, 0.10],
...     ]
... )
>>> coverage = px.cover_polygon(scene, resolution=6)
>>> len(coverage)
1
polypix.cover_cap(centers_xyz, radii_rad, *, resolution, mode='center', candidate_cells=None, threads=None, reduce=None)#

Find the cells selected by each spherical cap.

A cap is a center direction plus an angular radius. It is the shape of a ground station’s view of anything above an elevation mask, of a satellite’s own service circle, and of any circular instantaneous field of view. Use this rather than approximating a circle with a many-sided polygon: the region here is the exact cap.

Parameters:
  • centers_xyz (array_like) – One (3,) Cartesian direction or a (caps, 3) batch, in one frame of your choosing. Vectors are finite and nonzero, and we normalize the magnitudes internally.

  • radii_rad (float or array_like) – One finite angular radius in radians shared by every center, or exactly (caps,) radii, one per center. A length-one array is not broadcast. Radii lie in the closed interval [0, pi], where 0 is a point cap and pi is the whole sphere.

  • resolution (int) – HEALPix resolution, 0 through 29.

  • mode ({"center", "overlap"}, optional) – "center" selects a cell when its center is inside the cap. "overlap" selects it when any part of its area intersects the cap, including boundary tangency.

  • candidate_cells (array_like of int, optional) – RING indices at resolution limiting which cells are tested. Without reduce, duplicates and order are ignored. With a reducer, the result has one value per requested cell in the original order, including duplicates.

  • threads (int, optional) – None picks the automatic policy, 1 runs sequentially, and a larger value sets the maximum size of the reusable worker pool.

  • reduce (Count or Sum, optional) – Leave this out to get the segmented Coverage. Pass a reducer to get its accumulated array instead.

Returns:

Coverage or ndarray – Without reduce, one segment per cap; a single (3,) center still comes back as one segment. With a reducer, its accumulated array.

Raises:
  • TypeError – If the inputs have incompatible numeric types.

  • ValueError – If a shape, vector, radius, resolution, or candidate index is invalid.

  • MemoryError – If the segmented result does not fit in memory. Use reduce=Count() if counts are the intended result.

Notes

The default mode="center" selects a cell when its center lies inside the cap or on its boundary. mode="overlap" instead returns every cell whose true curved area touches the cap, so even a point cap returns the cell containing that point.

Counting center-selected caps is the one place where fusing the reduction into the geometry kernel currently wins by a wide margin, because the cap kernel accumulates private RING spans and never allocates the cap-cell pairs at all. It does this for a dense grid and for a selection alike, and falls back to covering once and counting when it judges that cheaper. Either way the answer is the same. Overlap mode uses the ordinary coverage-then-reduce path.

An empty (0, 3) batch of centers accepts a scalar or empty radius array and returns a coverage with offsets [0], which keeps empty chunks composable. As with the other borrowed inputs, do not mutate a contiguous center, radius, or candidate array from another thread before the call returns.

Examples

>>> import numpy as np
>>> import polypix as px
>>> centers = np.array([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]])
>>> coverage = px.cover_cap(centers, np.radians([6.0, 4.0]), resolution=4)
>>> np.diff(coverage.offsets)
array([8, 4])
polypix.cover_sweep(left_edge_xyz, right_edge_xyz, *, resolution, mode='center', candidate_cells=None, threads=None, reduce=None)#

Cover the quadrilaterals between two sampled edges of a swath.

A moving sensor sweeps out a swath. Sample its left and right edges at whatever cadence your propagator gives you, and every consecutive pair of samples becomes one quadrilateral, [left[i], right[i], right[i + 1], left[i + 1]], covered on its own. This paired-edge sweep is not the constant-colatitude operation traditionally called a HEALPix “strip”.

Parameters:
  • left_edge_xyz (array_like) – (samples, 3) arrays of Cartesian directions of equal length, in one frame of your choosing.

  • right_edge_xyz (array_like) – (samples, 3) arrays of Cartesian directions of equal length, in one frame of your choosing.

  • resolution (int) – HEALPix resolution, 0 through 29.

  • mode ({"center", "overlap"}, optional) – "center" selects a cell when its center is inside a segment. "overlap" selects it when any part of its area intersects the segment, including boundary tangency.

  • candidate_cells (array_like of int, optional) – RING indices at resolution limiting which cells are tested. Without reduce, duplicates and order are ignored. With a reducer, the result has one value per requested cell in the original order, including duplicates.

  • threads (int, optional) – None picks the automatic policy, 1 runs sequentially, and a larger value sets the maximum size of the reusable worker pool.

  • reduce (Count or Sum, optional) – Leave this out to get the segmented Coverage. Pass a reducer to get its accumulated array instead.

Returns:

Coverage or ndarray – Without reduce, max(samples - 1, 0) segments. With a reducer, its accumulated array.

Raises:
  • TypeError – If the inputs have incompatible numeric types.

  • ValueError – If the two edges have different lengths, or a segment is invalid or encloses no area.

  • MemoryError – If the segmented result does not fit in memory.

Notes

Consecutive samples are joined by the shorter great-circle arc, which makes sampling density part of the input contract. Steps approaching 180 degrees bow noticeably, steps past 180 degrees give you the opposite arc, and exactly ambiguous steps are rejected. We cannot tell a deliberate minor arc from an undersampled trajectory, so sample densely enough that each arc is the boundary you meant.

Segments are independent, and we neither merge nor deduplicate them. A global np.unique(coverage.cells) forms the sorted union, at the cost of the segmentation and some sorting time and memory.

The default mode="center" samples one point per HEALPix cell. mode="overlap" uses the true curved cell boundary and includes every cell a segment touches. A tiny boundary touch still contributes a whole hit to Count and a whole value to Sum.

Zero or one paired sample describes no intervals and returns empty coverage with offsets [0]. Repeating both edge samples at the same step encloses no area and is rejected; represent a stationary interval upstream instead, since deleting a sample shifts your time bins. Repeating a sample on one edge only is accepted, and gives a triangle pinched at that edge.

Examples

>>> import numpy as np
>>> import polypix as px
>>> track = np.radians(np.linspace(-13.0, 13.0, 7))
>>> def edge(offset):
...     lat = np.radians(offset)
...     return np.stack(
...         [
...             np.cos(lat) * np.cos(track),
...             np.cos(lat) * np.sin(track),
...             np.full_like(track, np.sin(lat)),
...         ],
...         axis=-1,
...     )
>>> coverage = px.cover_sweep(edge(3.2), edge(-3.2), resolution=4)
>>> len(coverage)
6

Working with cells#

polypix.cell_at(vectors_xyz, *, resolution)#

Return the HEALPix cell that each direction falls in.

Use this when your input is points rather than regions: a ground track, a set of targets, individual pointings.

Parameters:
  • vectors_xyz (array_like) – One (3,) Cartesian direction or a (vectors, 3) batch. Vectors are finite and nonzero; magnitudes are ignored.

  • resolution (int) – HEALPix resolution, 0 through 29.

Returns:

ndarray – Shape (vectors,), dtype int64. A single (3,) vector returns shape (1,) and an empty (0, 3) batch returns shape (0,).

Raises:
  • TypeError – If the input is not a real numeric array.

  • ValueError – If the shape, a vector, or the resolution is invalid.

Notes

Every finite nonzero direction lands in exactly one cell, so this quantizes rather than approximating: it does not turn center-sampled region coverage into a conservative spatial index.

A direction sitting numerically on a cell edge or vertex is a floating-point tie. The answer is repeatable for the same input, build, and platform, but we do not promise which of the adjacent cells owns an exact boundary direction across platforms. Resolve the tie upstream if your application needs a portable policy.

Large batches are parallelized inside the native kernel, and contiguous inputs are borrowed while the GIL is released, so do not mutate them concurrently.

Examples

>>> import numpy as np
>>> import polypix as px
>>> cells = px.cell_at(np.array([[1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]), resolution=4)
>>> px.cell_at(px.cell_centers(cells, resolution=4), resolution=4)
array([1504,    0])
polypix.cell_centers(cells, *, resolution)#

Return the center direction of each HEALPix cell.

Parameters:
  • cells (int or array_like of int) – RING indices at resolution.

  • resolution (int) – HEALPix resolution, 0 through 29.

Returns:

ndarray – Shape (cells, 3), dtype float64, unit vectors. A scalar cell returns (1, 3) and an empty input returns (0, 3).

Raises:
  • TypeError – If cells is not an integer array.

  • ValueError – If the resolution is invalid, or an index is negative or off the grid.

Notes

These are the cells’ own centers, not the directions you started from. Only a cell center round-trips exactly through cell_at(). Large arrays are parallelized inside the native kernel, which is why there is no threading argument here.

Examples

>>> import polypix as px
>>> px.cell_centers(0, resolution=0).round(3)
array([[0.527, 0.527, 0.667]])
polypix.cell_corners(cells, *, resolution)#

Return the four corner directions of each HEALPix cell.

Parameters:
  • cells (int or array_like of int) – RING indices at resolution.

  • resolution (int) – HEALPix resolution, 0 through 29.

Returns:

ndarray – Shape (cells, 4, 3), dtype float64, unit vectors in boundary traversal order. A scalar cell keeps the leading axis and returns (1, 4, 3). The first corner is not repeated at the end.

Raises:
  • TypeError – If cells is not an integer array.

  • ValueError – If the resolution is invalid, or an index is negative or off the grid.

Notes

HEALPix cell edges are curved, and we do not sample them between the corners. Four corners are therefore not a boundary: do not round-trip them as an exact great-circle polygon for the cell.

polypix.cell_count(resolution)#

Return the number of HEALPix cells at one resolution.

The count is 12 * 4 ** resolution, since the grid starts from 12 cells and splits each one into four at every step up. Use this when allocating or checking the length of a dense map.

Parameters:

resolution (int) – HEALPix resolution, 0 through 29.

Returns:

int – The number of cells in the grid.

Raises:
  • TypeError – If resolution is not an integer.

  • ValueError – If resolution falls outside 0 through 29.

Examples

>>> import polypix as px
>>> px.cell_count(4)
3072
polypix.cell_neighbors(cells, *, resolution)#

Return the topological neighbors of each HEALPix cell.

Cells touching at an edge or corner are neighbors. The input cell itself is excluded. Most cells have eight neighbors. The 24 cells meeting at eight exceptional grid vertices have seven, and cells at resolution 0 have six.

Parameters:
  • cells (int or array_like of int) – RING indices at resolution.

  • resolution (int) – HEALPix resolution, 0 through 29.

Returns:

Coverage – One unordered segment per input cell, preserving input alignment. A scalar input produces one segment.

Raises:
  • TypeError – If cells is not an integer array.

  • ValueError – If the resolution is invalid, or an index is negative or off the grid.

Examples

>>> import polypix as px
>>> neighbors = px.cell_neighbors(4, resolution=0)
>>> sorted(neighbors[0].tolist())
[0, 3, 5, 7, 8, 11]

Reducing and summarizing#

class polypix.Count#

Count how many segments cover each cell.

Pass an instance as reduce=Count() to a covering function, or to Coverage.reduce(). The reducer names the accumulation and nothing else: which cells are reported is decided by the operation being reduced, through candidate_cells on the covering call or on Coverage.reduce().

Examples

>>> import numpy as np
>>> import polypix as px
>>> counts = px.cover_cap(
...     np.array([[0.0, 0.0, 1.0]]), 0.2, resolution=4, reduce=px.Count()
... )
>>> int(counts.sum())
24
class polypix.Sum(values)#

Add one value per segment into every cell that segment covers.

Reach for this when a segment contributes an exposure, a duration, a probability, or a capacity instead of a single count.

Parameters:

values (float or array_like of float) – One finite value shared by every segment, or one value per segment. The reducer keeps a read-only float64 copy, so later edits to the array you passed do not change it.

Raises:
  • TypeError – If values is not real-valued.

  • ValueError – If values has more than one dimension or a non-finite value. A length that does not match the segments is reported by the call that uses the reducer, before any covering work.

Notes

Floating-point addition runs in segment and hit order, so a repeated call on the same input returns the same sums. Cells are selected as described for Count.

Examples

>>> import numpy as np
>>> import polypix as px
>>> weighted = px.cover_cap(
...     np.array([[0.0, 0.0, 1.0]]),
...     0.4,
...     resolution=2,
...     reduce=px.Sum([2.5]),
... )
>>> float(weighted.sum())
10.0
polypix.revisit(timelines, *, minimum_sources=1)#

Summarize how often each cell is covered across ordered bins.

Read the segments of a coverage as a timeline instead of a batch. A cell is covered in a bin when at least minimum_sources of the timelines contain it there, and consecutive covered bins form one visit. What comes back is the per-cell visit count, the gaps between visits, and the bounds of the observed window.

Parameters:
  • timelines (Coverage or sequence of Coverage) – One coverage per source, each of whose segments are consecutive, temporally adjacent bins in ascending order. Every entry must share a resolution and a segment count.

  • minimum_sources (int, default 1) – How many sources must cover a cell in the same bin for it to count. A threshold above the number of sources returns an empty result.

Returns:

RevisitStats – Ascending qualifying cells, with one statistic per cell.

Raises:
  • TypeError – If timelines holds something other than coverages.

  • ValueError – If the sequence is empty, the threshold is not positive, or the sources disagree on resolution or segment count.

Notes

Two things we cannot check for you, because a Coverage carries no clock. Matching segment indices must describe identical bin boundaries, and consecutive bins must really be adjacent in time. Split the analysis at a discontinuity, or insert a deliberately empty separator bin, so that a visit cannot bridge one.

Bins are ordinal, so map first_bin and stop_bin through your own array of time edges afterwards, and choose there whether revisit means end-to-start, start-to-start, or something cyclic.

Counts and gaps accumulate in a single pass, and the result is allocated by cell rather than by visit. That matters because the visits themselves are not guaranteed to be smaller than the input: a cell hit in alternating bins produces one visit per hit. The result is sparse and sorted by cell ID, so a high resolution does not force a dense global allocation.

Sequence positions are counted independently, so source uniqueness is yours to guarantee, and thresholding several sources deliberately drops source identity. Call this once per source when you need to know which observer saw what. For a sampled sweep, these are occupied bins rather than exact access events, and the boundary times are uncertain at the sampling cadence.

Examples

Four bins, in which one cell is covered, missed, then covered twice:

>>> import polypix as px
>>> timeline = px.Coverage.from_arrays(
...     cells=[5, 5, 5], offsets=[0, 1, 1, 2, 3], resolution=0
... )
>>> stats = px.revisit(timeline)
>>> stats.cells, stats.visit_counts, stats.gap_steps_max
(array([5]), array([2]), array([1]))
>>> stats.first_bin, stats.stop_bin
(array([0]), array([4]))

Results#

class polypix.Coverage(*_args, **_kwargs)#

Segmented HEALPix RING coverage, validated and read-only.

One segment is one input item: one polygon, cap, sweep interval, or cell. Every cell is stored in the flat cells array, and offsets records where each segment begins and ends. So len(coverage) counts input items and not cells, and coverage[i] is a zero-copy view of the cells found for item i.

The covering functions and cell_neighbors() return this type. Calling Coverage(...) directly raises TypeError; use from_arrays() to bring segmented arrays back in from storage.

cells#

Flat int64 array of standard HEALPix RING indices, in input order.

Type:

ndarray

offsets#

int64 segment boundaries, of length len(coverage) + 1.

Type:

ndarray

resolution#

The HEALPix resolution that every cell in the result belongs to.

Type:

int

Notes

Holding a Coverage is proof that its invariants hold: every cell is in range for the resolution, the offsets are nondecreasing and closed, and no cell repeats within a segment. That is why revisit() does not rescan what it is handed. The same cell may of course turn up in many different segments.

Two derived arrays are easy to want, and we deliberately do not return them, since NumPy computes them no more slowly than we could:

sizes = np.diff(coverage.offsets)
segment_of_each_hit = np.repeat(np.arange(len(coverage)), sizes)

Cell order within a segment is deterministic for a given build and platform, but it is not part of the interface, and we never sort a result for presentation alone. Imported segments keep the order they arrived in.

Read-only means the arrays come back with WRITEABLE=False. That catches accidental writes; it is not a promise of deep immutability against deliberate flag manipulation. Imported arrays are copied, so later edits to your own input cannot reach into a Coverage.

Pickling and copy.deepcopy() rebuild through from_arrays(), so a restored coverage is read-only and revalidated. That costs one pass over the cells on the way back in, which is the price of a stored coverage still being proof of its own invariants.

Equality is identity-based, so comparing two large coverages never starts an implicit linear scan. Compare cells, offsets, and resolution yourself when you want value equality.

classmethod from_arrays(cells, offsets, resolution)#

Copy and validate segmented RING-cell arrays from elsewhere.

This is the way back in from storage. Coverage is usually the expensive product of a campaign, so the realistic shape of the work is to compute it once and query it many times afterwards. A reloaded coverage is an ordinary coverage: revisit() and reduce() apply unchanged.

Parameters:
  • cells (array_like of int) – RING indices at resolution, concatenated in segment order.

  • offsets (array_like of int) – Segment boundaries. The sequence starts at 0, never decreases, and ends at the number of cells.

  • resolution (int) – HEALPix resolution from 0 through 29.

Returns:

Coverage – A coverage owning validated copies of both arrays.

Raises:
  • TypeError – If either input is not an integer array.

  • ValueError – If a cell falls outside the grid, the offsets are not a closed nondecreasing sequence, or a cell repeats inside one segment.

  • OverflowError – If a value cannot be represented by the signed int64 result arrays.

Notes

Validation costs one pass over the cells and segments, and an unsorted segment may need temporary storage. It happens once, here at the boundary, so nothing downstream has to re-check a hit.

Examples

>>> import polypix as px
>>> coverage = px.Coverage.from_arrays(
...     cells=[2, 7, 9], offsets=[0, 2, 3], resolution=1
... )
>>> len(coverage)
2
>>> coverage[1]
array([9])
__len__()#

Return the number of input items (segments).

__getitem__(index)#

Return a read-only view of one segment’s cell IDs.

reduce(reducer, *, candidate_cells=None)#

Accumulate this coverage with a Count or Sum.

Use this when several reductions share one expensive covering pass, or when the coverage came back from storage. The answer is the one a fused reduce= call on the covering function would have given.

Parameters:
  • reducer (Count or Sum) – The accumulation to perform.

  • candidate_cells (int or array_like of int, optional) – RING indices to report, as on a covering call: leave it out for a dense array indexed by cell ID, or name cells to get one value per requested ID.

Returns:

ndarray – With candidate_cells=None, a dense array of length cell_count(resolution) indexed by RING cell ID. Otherwise one value per requested ID, in query order and including duplicates. Count returns int64 and Sum returns float64.

Raises:
  • TypeError – If reducer is not a Count or Sum, or candidate_cells is not an integer array.

  • ValueError – If a requested cell falls outside the grid, or a Sum carries the wrong number of values.

Notes

Naming cells is the reason to reach for this method: it answers a question about a few thousand cells without ever allocating the grid. Small grids still accumulate through a dense scratch array internally, and large ones through a hash table, so memory follows what you asked for instead of the resolution.

Without candidate_cells, the array-level equivalent is a one-liner:

np.bincount(coverage.cells, minlength=px.cell_count(resolution))

The same call with weights=np.repeat(values, sizes) covers Sum. That equivalence stops holding as soon as you name cells, because numpy.bincount() has to build the whole grid before it can index a few cells out of it. At resolution 13 that is six gibibytes to answer a question about one city.

class polypix.RevisitStats(*_args, **_kwargs)#

Per-cell revisit statistics on one thresholded axis.

revisit() produces these; direct construction raises TypeError. Every field describes the same thresholded, source-unioned coverage of a cell, so they can be combined freely. Gaps and window bounds are measured in segments, not seconds, because a Coverage carries no clock.

cells#

Ascending int64 RING indices of the cells that were covered at least once.

Type:

ndarray

visit_counts#

int64 number of separate visits per cell, at least one.

Type:

ndarray

gap_steps_total#

int64 total length of the gaps between visits, in segments. Zero for a cell visited once.

Type:

ndarray

gap_steps_max#

int64 longest single gap between two visits, in segments. Zero for a cell visited once.

Type:

ndarray

first_bin#

int64 segment index at which the cell was first covered.

Type:

ndarray

stop_bin#

int64 segment index just after the cell was last covered.

Type:

ndarray

Notes

All six arrays need the same single pass over the segment axis, and none of them can be recovered from the others afterwards. gap_steps_max is the clearest case: an individual gap exists only in the moment one visit closes and the next opens, so computing it later would mean materializing every visit.

Only complete gaps between two visits are counted. What happens at the ends of the timeline is a policy choice, so we report the window instead of choosing for you: the trailing gap is segment_count - stop_bin against the segment count you passed in, and the number of internal gaps per cell is visit_counts - 1.

Anything this result does not carry is either one NumPy expression away or already in your hands, since the resolution, segment count, threshold, and source count all came from the arguments you supplied. The arrays follow the same read-only and identity-equality rules as Coverage, and len(stats) is the number of cells reported.

__len__()#

Return the number of cells having at least one qualifying run.

Geometry contract#

A polygon is given as vertices in boundary order. It may be convex or concave, but each component must fit inside an open hemisphere. Adjacent vertices are joined by the shorter of the two great-circle arcs between them, so longitudes -179° and 179° are two degrees apart, and nothing as large as a hemisphere can be described this way.

Accepted:

  • either vertex orientation;

  • a repeated closing vertex;

  • redundant vertices on an existing edge, within floating-point precision;

  • explicit holes and multipart unions through Polygon and MultiPolygon;

  • a cell center lying exactly on an edge in center mode, which counts as covered;

  • a region tangent to a true curved cell edge in overlap mode, which also counts as covered.

Rejected:

  • fewer than three distinct vertices;

  • duplicate, antipodal, or non-finite vertices;

  • degenerate edges and self-intersections;

  • holes outside, touching, or crossing their outer ring or another hole;

  • exact-hemisphere boundaries, and anything else detectably ambiguous.

Detectably is doing real work in that last item. Vertices that are individually valid but were meant to describe the other side of the sphere look like any other polygon from here.

Geographic interface inputs#

cover_polygon() accepts a mapping directly or an object whose __geo_interface__ property returns one. The mapping may describe a Polygon, MultiPolygon, or one Feature containing either. The first polygon ring is the outer boundary and the rest are holes; each multipolygon component becomes one Polygon in a single union region.

Positions are (longitude, latitude) or (longitude, latitude, altitude) in decimal degrees, interpreted directly as angles on a unit sphere. The datum and frame belong to the caller, and altitude is ignored. Longitudes must lie in [-180, 180] and latitudes in [-90, 90]; Polypix neither reads nor transforms a CRS. Polygonal geometry with no rings and a Feature with null geometry produce one empty segment. Feature properties, IDs, bounding boxes, and foreign members are ignored.

Points, lines, geometry collections, and feature collections are rejected. Homogeneous sequences form batches, but geographic mappings, Cartesian arrays, and Polypix geometry objects cannot be mixed in one batch. These inputs import vertices and ring topology, not GeoJSON edge interpolation: the shorter great-circle rule above remains authoritative. Reproject, repair, simplify, or densify with a GIS library before the call when needed.

A whole-world longitude/latitude bounding box is not a spherical polygon: -180 and 180 are the same meridian, and every longitude meets at each pole.

Numerical limits#

Footprints below roughly 1e-8 radians across may be rejected. Where that starts depends on how the vertices are laid out, and at the same scale concavity becomes indistinguishable from a collinear edge.

Coverage resolves floating-point boundary ties toward inclusion in both modes. Full scans, candidate_cells=, and fused counts agree on every cell, except that a cell center within about 1e-15 radians of the tolerance edge itself can be classified differently by paths that place it with different rounding.

Validation compares vertex pairs and tests every edge against every vertex, so its cost grows with the square of the vertex count. Hand a densely sampled boundary to cover_sweep() in short segments instead of passing one polygon with hundreds of vertices. That advice is firmer under mode="overlap", where each visited cell is also tested against every edge, making coverage itself linear in the vertex count; see Performance and memory for measured figures.