Boundaries

Discretize and Embed, which move between bounded continuous spaces and finite sets.

boundaries

Boundary morphisms between discrete and continuous spaces.

These morphisms bridge the gap between the finite tensor world (FinSet, discrete Morphism) and the continuous sampling world (ContinuousSpace, ContinuousMorphism).

Morphisms provided

Discretize — continuous space -> finite set (binning) Embed — finite set -> continuous space (kernel density)

Discretize

Discretize(domain: Euclidean, n_bins: int, soft: bool = True, temperature: float = 0.1)

Bases: ContinuousMorphism

Map a continuous space to a finite set by binning.

Divides a bounded continuous space into n_bins equal-width bins and assigns each continuous input to the bin containing it. The resulting distribution is deterministic (one-hot on the correct bin).

This is useful for converting continuous outputs into discrete categories for downstream processing in the finite tensor world.

For log_prob: returns log(1) = 0 if y equals the correct bin, log(0) otherwise. In practice uses a soft assignment based on distance to bin centers for gradient flow.

PARAMETER DESCRIPTION
domain

Source continuous space (must be bounded, 1-dimensional).

TYPE: Euclidean

n_bins

Number of discrete bins.

TYPE: int

soft

If True (default), use soft binning via softmax over negative squared distances. If False, use hard (argmax) assignment.

TYPE: bool DEFAULT: True

temperature

Temperature for soft binning. Lower = sharper.

TYPE: float DEFAULT: 0.1

Source code in src/quivers/continuous/boundaries.py
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def __init__(
    self,
    domain: Euclidean,
    n_bins: int,
    soft: bool = True,
    temperature: float = 0.1,
) -> None:
    if not isinstance(domain, Euclidean) or not domain.is_bounded:
        raise ValueError("Discretize requires a bounded Euclidean domain")

    if domain.dim != 1:
        raise ValueError(
            f"Discretize currently supports 1-d spaces only, got dim={domain.dim}"
        )

    codomain = FinSet(name="bins", cardinality=n_bins)
    super().__init__(domain, codomain)

    self._n_bins = n_bins
    self._soft = soft
    self._temperature = temperature

    # compute bin centers and register as buffer
    assert domain.low is not None and domain.high is not None
    centers = torch.linspace(domain.low, domain.high, n_bins)
    self.register_buffer("centers", centers)

log_prob

log_prob(x: Tensor, y: Tensor) -> Tensor

Log-probability of bin assignment y given input x.

PARAMETER DESCRIPTION
x

Continuous inputs. Shape (batch, 1).

TYPE: Tensor

y

Bin indices. Shape (batch,).

TYPE: Tensor

RETURNS DESCRIPTION
Tensor

Log-probabilities. Shape (batch,).

Source code in src/quivers/continuous/boundaries.py
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def log_prob(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
    """Log-probability of bin assignment y given input x.

    Parameters
    ----------
    x : torch.Tensor
        Continuous inputs. Shape (batch, 1).
    y : torch.Tensor
        Bin indices. Shape (batch,).

    Returns
    -------
    torch.Tensor
        Log-probabilities. Shape (batch,).
    """
    # soft assignment probabilities
    probs = self._soft_assign(x)  # (batch, n_bins)
    y_long = y.long()
    selected = probs[torch.arange(len(y_long)), y_long]
    return torch.log(selected.clamp(min=EPS))

rsample

rsample(x: Tensor, sample_shape: Size = Size()) -> Tensor

Assign continuous inputs to discrete bins.

PARAMETER DESCRIPTION
x

Continuous inputs. Shape (batch, 1).

TYPE: Tensor

sample_shape

Ignored (assignment is deterministic).

TYPE: Size DEFAULT: Size()

RETURNS DESCRIPTION
Tensor

Bin indices. Shape (batch,) or (*sample_shape, batch).

Source code in src/quivers/continuous/boundaries.py
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
def rsample(
    self,
    x: torch.Tensor,
    sample_shape: torch.Size = torch.Size(),
) -> torch.Tensor:
    """Assign continuous inputs to discrete bins.

    Parameters
    ----------
    x : torch.Tensor
        Continuous inputs. Shape (batch, 1).
    sample_shape : torch.Size
        Ignored (assignment is deterministic).

    Returns
    -------
    torch.Tensor
        Bin indices. Shape (batch,) or (*sample_shape, batch).
    """
    if self._soft:
        probs = self._soft_assign(x)
        bins = torch.multinomial(probs, 1).squeeze(-1)

    else:
        # hard assignment: closest bin center
        dists = (x - cast(torch.Tensor, self.centers).unsqueeze(0)).abs()
        bins = dists.argmin(dim=-1)

    if len(sample_shape) > 0:
        return bins.unsqueeze(0).expand(*sample_shape, -1)

    return bins

Embed

Embed(domain: FinSet, codomain: Euclidean)

Bases: ContinuousMorphism

Map a finite set to a continuous space via kernel density placement.

Each element i of the domain FinSet is associated with a point in the continuous codomain. Sampling from Embed(x=i) produces a value near that point, with learnable spread.

Concretely, Embed places a Gaussian kernel at each bin center:

p(y | i) = Normal(center_i, sigma_i)

The centers and log-sigmas are learnable parameters.

PARAMETER DESCRIPTION
domain

Source discrete set.

TYPE: FinSet

codomain

Target continuous space (should be bounded for initialization).

TYPE: Euclidean

Source code in src/quivers/continuous/boundaries.py
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
def __init__(
    self,
    domain: FinSet,
    codomain: Euclidean,
) -> None:
    super().__init__(domain, codomain)
    n = domain.size
    d = codomain.dim

    # initialize centers evenly spaced across codomain
    if codomain.is_bounded:
        assert codomain.low is not None and codomain.high is not None
        if d == 1:
            init_centers = torch.linspace(
                codomain.low,
                codomain.high,
                n,
            ).unsqueeze(-1)

        else:
            init_centers = (
                torch.rand(n, d) * (codomain.high - codomain.low) + codomain.low
            )

    else:
        init_centers = torch.randn(n, d) * 0.5

    self.centers = nn.Parameter(init_centers)
    self.log_sigma = nn.Parameter(torch.zeros(n, d))

base_dimension

base_dimension(x: Tensor) -> int

One standard-normal coordinate per embedded output axis.

A (batch, seq) index matrix places one kernel per position, so the budget grows with the sequence length as well as the codomain dimension.

Source code in src/quivers/continuous/boundaries.py
233
234
235
236
237
238
239
240
241
def base_dimension(self, x: torch.Tensor) -> int:
    """One standard-normal coordinate per embedded output axis.

    A ``(batch, seq)`` index matrix places one kernel per
    position, so the budget grows with the sequence length as well
    as the codomain dimension.
    """
    mu, _ = self._kernel_params(dimension_probe(x))
    return _event_size(mu.shape)

push_base

push_base(x: Tensor, base: Tensor) -> Tensor

The location-scale map around the embedding of x.

Source code in src/quivers/continuous/boundaries.py
243
244
245
246
def push_base(self, x: torch.Tensor, base: torch.Tensor) -> torch.Tensor:
    """The location-scale map around the embedding of ``x``."""
    mu, sigma = self._kernel_params(x)
    return mu + sigma * base.reshape(mu.shape)

log_prob

log_prob(x: Tensor, y: Tensor) -> Tensor

Log-density of y under the kernel centered at x's embedding.

PARAMETER DESCRIPTION
x

Domain indices. Shape (batch,).

TYPE: Tensor

y

Continuous outputs. Shape (batch, d).

TYPE: Tensor

RETURNS DESCRIPTION
Tensor

Log-densities. Shape (batch,).

Source code in src/quivers/continuous/boundaries.py
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def log_prob(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
    """Log-density of y under the kernel centered at x's embedding.

    Parameters
    ----------
    x : torch.Tensor
        Domain indices. Shape (batch,).
    y : torch.Tensor
        Continuous outputs. Shape (batch, d).

    Returns
    -------
    torch.Tensor
        Log-densities. Shape (batch,).
    """
    mu, sigma = self._kernel_params(x)

    log_p = (
        -0.5 * ((y - mu) / sigma) ** 2 - sigma.log() - 0.5 * math.log(2 * math.pi)
    )

    return log_p.sum(dim=-1)

rsample

rsample(x: Tensor, sample_shape: Size = Size()) -> Tensor

Sample from the Gaussian kernel at x's embedding point.

PARAMETER DESCRIPTION
x

Domain indices. Shape (batch,).

TYPE: Tensor

sample_shape

Additional sample dimensions.

TYPE: Size DEFAULT: Size()

RETURNS DESCRIPTION
Tensor

Continuous samples. Shape (*sample_shape, batch, d).

Source code in src/quivers/continuous/boundaries.py
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
def rsample(
    self,
    x: torch.Tensor,
    sample_shape: torch.Size = torch.Size(),
) -> torch.Tensor:
    """Sample from the Gaussian kernel at x's embedding point.

    Parameters
    ----------
    x : torch.Tensor
        Domain indices. Shape (batch,).
    sample_shape : torch.Size
        Additional sample dimensions.

    Returns
    -------
    torch.Tensor
        Continuous samples. Shape (*sample_shape, batch, d).
    """
    mu, sigma = self._kernel_params(x)

    eps = torch.randn(
        *sample_shape,
        *mu.shape,
        device=mu.device,
        dtype=mu.dtype,
    )

    return mu + sigma * eps