pcalc.presence_matrix

  1# -*- coding: utf-8 -*-
  2# Copyright (c) 2025 Krishna Kumar
  3# SPDX-License-Identifier: MIT
  4
  5from __future__ import annotations
  6
  7from typing import List, Optional
  8
  9import numpy as np
 10from numpy import typing as npt
 11
 12from .presence import PresenceAssertion
 13from .presence_map import PresenceMap
 14from .time_scale import Timescale
 15
 16
 17class PresenceMatrix:
 18    """
 19    A scale-invariant, optionally lazy matrix representation of element presences over discrete time bins.
 20
 21    Each row corresponds to a single `Presence`, and each column represents a discrete time bin defined by a `Timescale`.
 22    The matrix records the degree of presence of each element within a given boundary over time, where each value is a real number in [0.0, 1.0]:
 23    - 1.0 = full presence for the duration of the time bin
 24    - 0.0 = no presence
 25    - values in between represent partial presence due to overlap
 26
 27    The matrix can be computed eagerly via `materialize()` or accessed lazily through NumPy-style slicing.
 28
 29    ### Structure
 30
 31    - **Rows:** Each row corresponds to one `Presence` instance.
 32    - **Columns:** Each column represents a discrete bin of time defined by the `Timescale` (from `t0` to `t1` using `bin_width`).
 33    - The matrix shape is therefore `(len(presences), timescale.num_bins)`.
 34
 35    ### Usage
 36
 37    ```python
 38    from pcalc import PresenceMatrix
 39    from metamodel.timescale import Timescale
 40
 41    # Define a time window with bin width of 1.0
 42    ts = Timescale(t0=0.0, t1=5.0, bin_width=1.0)
 43
 44    # Construct lazily
 45    matrix = PresenceMatrix(presences, time_scale=ts)
 46
 47    # Access entire row
 48    row = matrix[0]  # row 0 as 1D array
 49
 50    # Access single value
 51    value = matrix[2, 3]  # presence at row 2, column 3
 52
 53    # Access slice of row
 54    window = matrix[1, 1:4]  # row 1, columns 1–3
 55
 56    # Access slice of rows
 57    rows = matrix[0:2]  # rows 0 and 1
 58
 59    # Access single column
 60    col = matrix[:, 2]  # all rows at column 2
 61
 62    # Access column block
 63    col_block = matrix[:, 1:4]  # all rows, columns 1–3
 64
 65    # Materialize full matrix (optional)
 66    dense = matrix.materialize()
 67    ```
 68
 69    Features
 70    --------
 71    - Lazy evaluation by default; full matrix computed only if needed
 72    - NumPy-style indexing: supports full rows, row/column slicing, and scalar access
 73    - Precision-preserving: partial bin overlaps are retained in the matrix
 74    - Compatible with matrix operations and flow metric calculations
 75    - Backed by a sparse `PresenceMap`, which stores slice indices and fractional overlaps
 76
 77    Notes
 78    -----
 79    - Use `materialize()` if you want to extract or operate on the full dense matrix
 80    - Use `matrix[i, j]`, `matrix[i, j:k]`, or `matrix[:, j]` for lightweight slicing without allocating the full matrix
 81    - Stepped slicing (e.g., `matrix[::2]`) is not currently supported
 82
 83"""
 84
 85    def __init__(self, presences: List[PresenceAssertion], time_scale: Timescale, materialize=False):
 86        """
 87        Construct a presence matrix from a list of Presences and time window configuration.
 88
 89        Args:
 90            presences: A list of `Presence` instances, one per element of interest.
 91            time_scale: The discrete `Timescale` that the matrix will be normalized to.
 92            materialize: If True, immediately constructs the full backing matrix.
 93                     Otherwise, matrix values will be computed on demand using the sparse presence map.
 94        """
 95
 96        self.presence_matrix: Optional[npt.NDArray[np.float64]] = None
 97        """
 98        A real valued matrix with shape (num_Presences, num_bins).
 99        When every presence.start and presence.end are whole numbers every value
100        in the matrix is either 0.0 or 1.0. If they are not whole numbers, the presence
101        is mapped to a number between 0.0 and 1.0 at the start and end (or both), with any
102        intermediate value being 1.0. 
103        """
104
105        self.time_scale = time_scale
106        """
107        The time scale of the presence matrix.
108        """
109
110        self.presence_map: List[PresenceMap] = []
111        self.shape = None
112        self.init_presence_map(presences)
113
114        if materialize:
115            self.materialize()
116
117    def init_presence_map(self, presences: List[PresenceAssertion]) -> None:
118        """
119        Initialize the internal presence matrix based on the Presence intervals and binning scheme.
120        Only presences that overlap the timescale endpoints [t0, t1) are mapped.
121        """
122        ts = self.time_scale  # Timescale object: includes t0, t1, bin_width
123
124        for row, presence in enumerate(presences):
125            presence_map = PresenceMap(presence, ts)
126            if presence_map.is_mapped:
127                self.presence_map.append(presence_map)
128
129        num_bins = ts.num_bins
130        num_rows = len(self.presence_map)
131        self.shape = (num_rows, num_bins)
132
133    def is_materialized(self) -> bool:
134        """Return True if the backing matrix has been materialized."""
135        return self.presence_matrix is not None
136
137    def materialize(self) -> npt.NDArray[np.float64]:
138        """
139        Materialize and return the full presence matrix from presence maps.
140        If already materialized, this is a no-op and returns the cached matrix.
141
142        Use this only if want the full matrix to operate on. For most cases, you should
143        be able to work with the sparse representation with the presence map and using
144        array slicing on the matrix object. So think twice about why you are materializing
145        a matrix.
146
147        Returns:
148            The dense presence matrix of shape (num_presences, num_bins).
149        """
150        if self.is_materialized():
151            return self.presence_matrix
152
153        num_rows, num_cols = self.shape
154        matrix = np.zeros((num_rows, num_cols), dtype=float)
155
156        for row, pm in enumerate(self.presence_map):
157            if not pm.is_mapped:
158                continue
159            start = pm.start_bin
160            end = pm.end_bin
161            matrix[row, start] = pm.start_value
162            if end - 1 > start:
163                matrix[row, end - 1] = pm.end_value
164            if end - start > 2:
165                matrix[row, start + 1: end - 1] = 1.0
166
167        self.presence_matrix = matrix
168        return self.presence_matrix
169
170    def drop_materialization(self) -> None:
171        """Discard the cached matrix, returning to sparse-only mode."""
172        self.presence_matrix = None
173
174    def _compute_row_slice(self, row: int, start: int, stop: int) -> np.ndarray:
175        """
176        On-demand computation of a row slice from presence map.
177        If the presence is not mapped or out of bounds, returns zeroes.
178        """
179        pm = self.presence_map[row]
180        width = stop - start
181        output = np.zeros(width, dtype=float)
182
183        if not pm.is_mapped:
184            return output
185
186        # Slice and overlap window
187        for col in range(start, stop):
188            if col < pm.start_bin or col >= pm.end_bin:
189                continue
190            elif col == pm.start_bin:
191                output[col - start] = pm.start_value
192            elif col == pm.end_bin - 1:
193                output[col - start] = pm.end_value
194            else:
195                output[col - start] = 1.0
196
197        return output
198
199    @property
200    def presences(self) -> List[PresenceAssertion]:
201        return [pm.presence for pm in self.presence_map]
202
203    def __getitem__(self, index):
204        """
205        Support NumPy-style indexing:
206        - matrix[i]           → row i
207        - matrix[i:j]         → row slice (returns 2D array)
208        - matrix[i, j]        → scalar
209        - matrix[i, j:k]      → row[i], column slice
210        - matrix[:, j]        → column j (all rows)
211        - matrix[:, j:k]      → column block j:k (all rows)
212        """
213        # matrix[i:j] → top-level row slicing
214        if isinstance(index, slice):
215            if index.step is not None:
216                raise ValueError("PresenceMatrix does not support slicing with a step.")
217            start = index.start if index.start is not None else 0
218            stop = index.stop if index.stop is not None else self.shape[0]
219            if self.presence_matrix is not None:
220                return self.presence_matrix[start:stop]
221            return np.array([self[row] for row in range(start, stop)])
222
223        # matrix[i, j], matrix[i, j:k], matrix[:, j]
224        if isinstance(index, tuple):
225            row_idx, col_idx = index
226
227            # matrix[:, j] or matrix[:, j:k]
228            if isinstance(row_idx, slice):
229                if row_idx.step is not None:
230                    raise ValueError("PresenceMatrix does not support stepped row slicing.")
231                row_start = row_idx.start if row_idx.start is not None else 0
232                row_stop = row_idx.stop if row_idx.stop is not None else self.shape[0]
233
234                if isinstance(col_idx, int):
235                    return np.array([self[row, col_idx] for row in range(row_start, row_stop)])
236
237                elif isinstance(col_idx, slice):
238                    if col_idx.step is not None:
239                        raise ValueError("PresenceMatrix does not support stepped column slicing.")
240                    col_start = col_idx.start if col_idx.start is not None else 0
241                    col_stop = col_idx.stop if col_idx.stop is not None else self.shape[1]
242                    return np.array([self[row, col_start:col_stop] for row in range(row_start, row_stop)])
243
244            # matrix[i, j] or matrix[i, j:k]
245            elif isinstance(row_idx, int):
246                if isinstance(col_idx, int):
247                    return self.presence_matrix[row_idx, col_idx] if self.presence_matrix is not None \
248                        else self._compute_row_slice(row_idx, col_idx, col_idx + 1)[0]
249
250                elif isinstance(col_idx, slice):
251                    if col_idx.step is not None:
252                        raise ValueError("PresenceMatrix does not support stepped column slicing.")
253                    start = col_idx.start if col_idx.start is not None else 0
254                    stop = col_idx.stop if col_idx.stop is not None else self.shape[1]
255                    return self.presence_matrix[row_idx, start:stop] if self.presence_matrix is not None \
256                        else self._compute_row_slice(row_idx, start, stop)
257
258        # matrix[i] → single row
259        if isinstance(index, int):
260            return self.presence_matrix[index] if self.presence_matrix is not None \
261                else self._compute_row_slice(index, 0, self.shape[1])
262
263        raise TypeError(f"Invalid index for PresenceMatrix: {index}")
class PresenceMatrix:
 18class PresenceMatrix:
 19    """
 20    A scale-invariant, optionally lazy matrix representation of element presences over discrete time bins.
 21
 22    Each row corresponds to a single `Presence`, and each column represents a discrete time bin defined by a `Timescale`.
 23    The matrix records the degree of presence of each element within a given boundary over time, where each value is a real number in [0.0, 1.0]:
 24    - 1.0 = full presence for the duration of the time bin
 25    - 0.0 = no presence
 26    - values in between represent partial presence due to overlap
 27
 28    The matrix can be computed eagerly via `materialize()` or accessed lazily through NumPy-style slicing.
 29
 30    ### Structure
 31
 32    - **Rows:** Each row corresponds to one `Presence` instance.
 33    - **Columns:** Each column represents a discrete bin of time defined by the `Timescale` (from `t0` to `t1` using `bin_width`).
 34    - The matrix shape is therefore `(len(presences), timescale.num_bins)`.
 35
 36    ### Usage
 37
 38    ```python
 39    from pcalc import PresenceMatrix
 40    from metamodel.timescale import Timescale
 41
 42    # Define a time window with bin width of 1.0
 43    ts = Timescale(t0=0.0, t1=5.0, bin_width=1.0)
 44
 45    # Construct lazily
 46    matrix = PresenceMatrix(presences, time_scale=ts)
 47
 48    # Access entire row
 49    row = matrix[0]  # row 0 as 1D array
 50
 51    # Access single value
 52    value = matrix[2, 3]  # presence at row 2, column 3
 53
 54    # Access slice of row
 55    window = matrix[1, 1:4]  # row 1, columns 1–3
 56
 57    # Access slice of rows
 58    rows = matrix[0:2]  # rows 0 and 1
 59
 60    # Access single column
 61    col = matrix[:, 2]  # all rows at column 2
 62
 63    # Access column block
 64    col_block = matrix[:, 1:4]  # all rows, columns 1–3
 65
 66    # Materialize full matrix (optional)
 67    dense = matrix.materialize()
 68    ```
 69
 70    Features
 71    --------
 72    - Lazy evaluation by default; full matrix computed only if needed
 73    - NumPy-style indexing: supports full rows, row/column slicing, and scalar access
 74    - Precision-preserving: partial bin overlaps are retained in the matrix
 75    - Compatible with matrix operations and flow metric calculations
 76    - Backed by a sparse `PresenceMap`, which stores slice indices and fractional overlaps
 77
 78    Notes
 79    -----
 80    - Use `materialize()` if you want to extract or operate on the full dense matrix
 81    - Use `matrix[i, j]`, `matrix[i, j:k]`, or `matrix[:, j]` for lightweight slicing without allocating the full matrix
 82    - Stepped slicing (e.g., `matrix[::2]`) is not currently supported
 83
 84"""
 85
 86    def __init__(self, presences: List[PresenceAssertion], time_scale: Timescale, materialize=False):
 87        """
 88        Construct a presence matrix from a list of Presences and time window configuration.
 89
 90        Args:
 91            presences: A list of `Presence` instances, one per element of interest.
 92            time_scale: The discrete `Timescale` that the matrix will be normalized to.
 93            materialize: If True, immediately constructs the full backing matrix.
 94                     Otherwise, matrix values will be computed on demand using the sparse presence map.
 95        """
 96
 97        self.presence_matrix: Optional[npt.NDArray[np.float64]] = None
 98        """
 99        A real valued matrix with shape (num_Presences, num_bins).
100        When every presence.start and presence.end are whole numbers every value
101        in the matrix is either 0.0 or 1.0. If they are not whole numbers, the presence
102        is mapped to a number between 0.0 and 1.0 at the start and end (or both), with any
103        intermediate value being 1.0. 
104        """
105
106        self.time_scale = time_scale
107        """
108        The time scale of the presence matrix.
109        """
110
111        self.presence_map: List[PresenceMap] = []
112        self.shape = None
113        self.init_presence_map(presences)
114
115        if materialize:
116            self.materialize()
117
118    def init_presence_map(self, presences: List[PresenceAssertion]) -> None:
119        """
120        Initialize the internal presence matrix based on the Presence intervals and binning scheme.
121        Only presences that overlap the timescale endpoints [t0, t1) are mapped.
122        """
123        ts = self.time_scale  # Timescale object: includes t0, t1, bin_width
124
125        for row, presence in enumerate(presences):
126            presence_map = PresenceMap(presence, ts)
127            if presence_map.is_mapped:
128                self.presence_map.append(presence_map)
129
130        num_bins = ts.num_bins
131        num_rows = len(self.presence_map)
132        self.shape = (num_rows, num_bins)
133
134    def is_materialized(self) -> bool:
135        """Return True if the backing matrix has been materialized."""
136        return self.presence_matrix is not None
137
138    def materialize(self) -> npt.NDArray[np.float64]:
139        """
140        Materialize and return the full presence matrix from presence maps.
141        If already materialized, this is a no-op and returns the cached matrix.
142
143        Use this only if want the full matrix to operate on. For most cases, you should
144        be able to work with the sparse representation with the presence map and using
145        array slicing on the matrix object. So think twice about why you are materializing
146        a matrix.
147
148        Returns:
149            The dense presence matrix of shape (num_presences, num_bins).
150        """
151        if self.is_materialized():
152            return self.presence_matrix
153
154        num_rows, num_cols = self.shape
155        matrix = np.zeros((num_rows, num_cols), dtype=float)
156
157        for row, pm in enumerate(self.presence_map):
158            if not pm.is_mapped:
159                continue
160            start = pm.start_bin
161            end = pm.end_bin
162            matrix[row, start] = pm.start_value
163            if end - 1 > start:
164                matrix[row, end - 1] = pm.end_value
165            if end - start > 2:
166                matrix[row, start + 1: end - 1] = 1.0
167
168        self.presence_matrix = matrix
169        return self.presence_matrix
170
171    def drop_materialization(self) -> None:
172        """Discard the cached matrix, returning to sparse-only mode."""
173        self.presence_matrix = None
174
175    def _compute_row_slice(self, row: int, start: int, stop: int) -> np.ndarray:
176        """
177        On-demand computation of a row slice from presence map.
178        If the presence is not mapped or out of bounds, returns zeroes.
179        """
180        pm = self.presence_map[row]
181        width = stop - start
182        output = np.zeros(width, dtype=float)
183
184        if not pm.is_mapped:
185            return output
186
187        # Slice and overlap window
188        for col in range(start, stop):
189            if col < pm.start_bin or col >= pm.end_bin:
190                continue
191            elif col == pm.start_bin:
192                output[col - start] = pm.start_value
193            elif col == pm.end_bin - 1:
194                output[col - start] = pm.end_value
195            else:
196                output[col - start] = 1.0
197
198        return output
199
200    @property
201    def presences(self) -> List[PresenceAssertion]:
202        return [pm.presence for pm in self.presence_map]
203
204    def __getitem__(self, index):
205        """
206        Support NumPy-style indexing:
207        - matrix[i]           → row i
208        - matrix[i:j]         → row slice (returns 2D array)
209        - matrix[i, j]        → scalar
210        - matrix[i, j:k]      → row[i], column slice
211        - matrix[:, j]        → column j (all rows)
212        - matrix[:, j:k]      → column block j:k (all rows)
213        """
214        # matrix[i:j] → top-level row slicing
215        if isinstance(index, slice):
216            if index.step is not None:
217                raise ValueError("PresenceMatrix does not support slicing with a step.")
218            start = index.start if index.start is not None else 0
219            stop = index.stop if index.stop is not None else self.shape[0]
220            if self.presence_matrix is not None:
221                return self.presence_matrix[start:stop]
222            return np.array([self[row] for row in range(start, stop)])
223
224        # matrix[i, j], matrix[i, j:k], matrix[:, j]
225        if isinstance(index, tuple):
226            row_idx, col_idx = index
227
228            # matrix[:, j] or matrix[:, j:k]
229            if isinstance(row_idx, slice):
230                if row_idx.step is not None:
231                    raise ValueError("PresenceMatrix does not support stepped row slicing.")
232                row_start = row_idx.start if row_idx.start is not None else 0
233                row_stop = row_idx.stop if row_idx.stop is not None else self.shape[0]
234
235                if isinstance(col_idx, int):
236                    return np.array([self[row, col_idx] for row in range(row_start, row_stop)])
237
238                elif isinstance(col_idx, slice):
239                    if col_idx.step is not None:
240                        raise ValueError("PresenceMatrix does not support stepped column slicing.")
241                    col_start = col_idx.start if col_idx.start is not None else 0
242                    col_stop = col_idx.stop if col_idx.stop is not None else self.shape[1]
243                    return np.array([self[row, col_start:col_stop] for row in range(row_start, row_stop)])
244
245            # matrix[i, j] or matrix[i, j:k]
246            elif isinstance(row_idx, int):
247                if isinstance(col_idx, int):
248                    return self.presence_matrix[row_idx, col_idx] if self.presence_matrix is not None \
249                        else self._compute_row_slice(row_idx, col_idx, col_idx + 1)[0]
250
251                elif isinstance(col_idx, slice):
252                    if col_idx.step is not None:
253                        raise ValueError("PresenceMatrix does not support stepped column slicing.")
254                    start = col_idx.start if col_idx.start is not None else 0
255                    stop = col_idx.stop if col_idx.stop is not None else self.shape[1]
256                    return self.presence_matrix[row_idx, start:stop] if self.presence_matrix is not None \
257                        else self._compute_row_slice(row_idx, start, stop)
258
259        # matrix[i] → single row
260        if isinstance(index, int):
261            return self.presence_matrix[index] if self.presence_matrix is not None \
262                else self._compute_row_slice(index, 0, self.shape[1])
263
264        raise TypeError(f"Invalid index for PresenceMatrix: {index}")

A scale-invariant, optionally lazy matrix representation of element presences over discrete time bins.

Each row corresponds to a single Presence, and each column represents a discrete time bin defined by a Timescale. The matrix records the degree of presence of each element within a given boundary over time, where each value is a real number in [0.0, 1.0]:

  • 1.0 = full presence for the duration of the time bin
  • 0.0 = no presence
  • values in between represent partial presence due to overlap

The matrix can be computed eagerly via materialize() or accessed lazily through NumPy-style slicing.

Structure

  • Rows: Each row corresponds to one Presence instance.
  • Columns: Each column represents a discrete bin of time defined by the Timescale (from t0 to t1 using bin_width).
  • The matrix shape is therefore (len(presences), timescale.num_bins).

Usage

from pcalc import PresenceMatrix
from metamodel.timescale import Timescale

# Define a time window with bin width of 1.0
ts = Timescale(t0=0.0, t1=5.0, bin_width=1.0)

# Construct lazily
matrix = PresenceMatrix(presences, time_scale=ts)

# Access entire row
row = matrix[0]  # row 0 as 1D array

# Access single value
value = matrix[2, 3]  # presence at row 2, column 3

# Access slice of row
window = matrix[1, 1:4]  # row 1, columns 1–3

# Access slice of rows
rows = matrix[0:2]  # rows 0 and 1

# Access single column
col = matrix[:, 2]  # all rows at column 2

# Access column block
col_block = matrix[:, 1:4]  # all rows, columns 1–3

# Materialize full matrix (optional)
dense = matrix.materialize()

Features

  • Lazy evaluation by default; full matrix computed only if needed
  • NumPy-style indexing: supports full rows, row/column slicing, and scalar access
  • Precision-preserving: partial bin overlaps are retained in the matrix
  • Compatible with matrix operations and flow metric calculations
  • Backed by a sparse PresenceMap, which stores slice indices and fractional overlaps

Notes

  • Use materialize() if you want to extract or operate on the full dense matrix
  • Use matrix[i, j], matrix[i, j:k], or matrix[:, j] for lightweight slicing without allocating the full matrix
  • Stepped slicing (e.g., matrix[::2]) is not currently supported
PresenceMatrix( presences: List[pcalc.PresenceAssertion], time_scale: pcalc.Timescale, materialize=False)
 86    def __init__(self, presences: List[PresenceAssertion], time_scale: Timescale, materialize=False):
 87        """
 88        Construct a presence matrix from a list of Presences and time window configuration.
 89
 90        Args:
 91            presences: A list of `Presence` instances, one per element of interest.
 92            time_scale: The discrete `Timescale` that the matrix will be normalized to.
 93            materialize: If True, immediately constructs the full backing matrix.
 94                     Otherwise, matrix values will be computed on demand using the sparse presence map.
 95        """
 96
 97        self.presence_matrix: Optional[npt.NDArray[np.float64]] = None
 98        """
 99        A real valued matrix with shape (num_Presences, num_bins).
100        When every presence.start and presence.end are whole numbers every value
101        in the matrix is either 0.0 or 1.0. If they are not whole numbers, the presence
102        is mapped to a number between 0.0 and 1.0 at the start and end (or both), with any
103        intermediate value being 1.0. 
104        """
105
106        self.time_scale = time_scale
107        """
108        The time scale of the presence matrix.
109        """
110
111        self.presence_map: List[PresenceMap] = []
112        self.shape = None
113        self.init_presence_map(presences)
114
115        if materialize:
116            self.materialize()

Construct a presence matrix from a list of Presences and time window configuration.

Arguments:
  • presences: A list of Presence instances, one per element of interest.
  • time_scale: The discrete Timescale that the matrix will be normalized to.
  • materialize: If True, immediately constructs the full backing matrix. Otherwise, matrix values will be computed on demand using the sparse presence map.
presence_matrix: Optional[numpy.ndarray[Any, numpy.dtype[numpy.float64]]]

A real valued matrix with shape (num_Presences, num_bins). When every presence.start and presence.end are whole numbers every value in the matrix is either 0.0 or 1.0. If they are not whole numbers, the presence is mapped to a number between 0.0 and 1.0 at the start and end (or both), with any intermediate value being 1.0.

time_scale

The time scale of the presence matrix.

presence_map: List[pcalc.PresenceMap]
shape
def init_presence_map(self, presences: List[pcalc.PresenceAssertion]) -> None:
118    def init_presence_map(self, presences: List[PresenceAssertion]) -> None:
119        """
120        Initialize the internal presence matrix based on the Presence intervals and binning scheme.
121        Only presences that overlap the timescale endpoints [t0, t1) are mapped.
122        """
123        ts = self.time_scale  # Timescale object: includes t0, t1, bin_width
124
125        for row, presence in enumerate(presences):
126            presence_map = PresenceMap(presence, ts)
127            if presence_map.is_mapped:
128                self.presence_map.append(presence_map)
129
130        num_bins = ts.num_bins
131        num_rows = len(self.presence_map)
132        self.shape = (num_rows, num_bins)

Initialize the internal presence matrix based on the Presence intervals and binning scheme. Only presences that overlap the timescale endpoints [t0, t1) are mapped.

def is_materialized(self) -> bool:
134    def is_materialized(self) -> bool:
135        """Return True if the backing matrix has been materialized."""
136        return self.presence_matrix is not None

Return True if the backing matrix has been materialized.

def materialize(self) -> numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]:
138    def materialize(self) -> npt.NDArray[np.float64]:
139        """
140        Materialize and return the full presence matrix from presence maps.
141        If already materialized, this is a no-op and returns the cached matrix.
142
143        Use this only if want the full matrix to operate on. For most cases, you should
144        be able to work with the sparse representation with the presence map and using
145        array slicing on the matrix object. So think twice about why you are materializing
146        a matrix.
147
148        Returns:
149            The dense presence matrix of shape (num_presences, num_bins).
150        """
151        if self.is_materialized():
152            return self.presence_matrix
153
154        num_rows, num_cols = self.shape
155        matrix = np.zeros((num_rows, num_cols), dtype=float)
156
157        for row, pm in enumerate(self.presence_map):
158            if not pm.is_mapped:
159                continue
160            start = pm.start_bin
161            end = pm.end_bin
162            matrix[row, start] = pm.start_value
163            if end - 1 > start:
164                matrix[row, end - 1] = pm.end_value
165            if end - start > 2:
166                matrix[row, start + 1: end - 1] = 1.0
167
168        self.presence_matrix = matrix
169        return self.presence_matrix

Materialize and return the full presence matrix from presence maps. If already materialized, this is a no-op and returns the cached matrix.

Use this only if want the full matrix to operate on. For most cases, you should be able to work with the sparse representation with the presence map and using array slicing on the matrix object. So think twice about why you are materializing a matrix.

Returns:

The dense presence matrix of shape (num_presences, num_bins).

def drop_materialization(self) -> None:
171    def drop_materialization(self) -> None:
172        """Discard the cached matrix, returning to sparse-only mode."""
173        self.presence_matrix = None

Discard the cached matrix, returning to sparse-only mode.

presences: List[pcalc.PresenceAssertion]
200    @property
201    def presences(self) -> List[PresenceAssertion]:
202        return [pm.presence for pm in self.presence_map]