Source code for prophys.nn.tokens

"""Token sets for field surrogates.

Field transformers take a set of rows. These helpers turn prophys structures
into such rows. A raster becomes one row per cell with its world coordinates
and values, a point list one row per point, geometry one row per segment or
edge, and `sample_set` turns the Monte Carlo sample of any upstream quantity
into a set, so a surrogate can map a whole distribution to an output.
"""

from __future__ import annotations

from typing import Any

import jax
import jax.numpy as jnp

from ..diagnostics import raise_error
from ..symbolic.expr import Expr, _collect, op, wrap

__all__ = ["field_tokens", "point_tokens", "geometry_tokens", "sample_set", "SampleSet"]


[docs] def field_tokens(field, channels: int = 1, coords: bool = True) -> Expr: """Token rows of a raster field: cell centre coordinates followed by the cell values. Args: field: A raster structure with ``values`` ``[..., ny, nx]`` (or ``[..., ny, nx, channels]``), ``origin`` and ``cell`` size. channels: Number of value channels per cell. coords: Whether to prepend the world coordinates ``x, y`` of each cell centre. Returns: An expression of shape ``[..., ny * nx, 2 + channels]`` (or ``[..., ny * nx, channels]`` without coordinates). Leading batch axes of the values are kept. Raises: ~prophys.diagnostics.ProphysError: ``NN123`` when the field's channel count differs from `channels`. """ def _tok(v): v = jnp.asarray(v) if channels == 1: ny, nx = v.shape[-2:] vals = v.reshape(v.shape[:-2] + (ny * nx, 1)) else: ny, nx, c = v.shape[-3:] if c != channels: raise_error("NN123", f"field has {c} channels, {channels} declared") vals = v.reshape(v.shape[:-3] + (ny * nx, c)) if not coords: return vals yy, xx = jnp.meshgrid(jnp.arange(ny), jnp.arange(nx), indexing="ij") x = field.origin[0] + (xx.reshape(-1) + 0.5) * field.cell[1] y = field.origin[1] + (yy.reshape(-1) + 0.5) * field.cell[0] xy = jnp.stack([x, y], axis=-1).astype(vals.dtype) xy = jnp.broadcast_to(xy, vals.shape[:-1] + (2,)) return jnp.concatenate([xy, vals], axis=-1) return op("field_tokens", _tok, field.values)
[docs] def point_tokens(points, features: Any = None) -> Expr: """Token rows of a point set: coordinates followed by optional per point features. Args: points: A point structure with ``coords`` ``[n, 2]``, or an expression or array of coordinates. features: Optional features ``[n, f]`` or ``[n]``. Returns: An expression of shape ``[n, 2 + f]`` (the coordinates alone when no features are given). """ coords = points.coords if hasattr(points, "coords") else wrap(points) if features is None: return coords def _tok(c, f): f = jnp.asarray(f) if f.ndim == c.ndim - 1: f = f[..., None] return jnp.concatenate([c, jnp.broadcast_to(f, c.shape[:-1] + f.shape[-1:]).astype(c.dtype)], axis=-1) return op("point_tokens", _tok, coords, features)
[docs] def geometry_tokens(geometry) -> Expr: """Token rows of a geometry, one row ``[x0, y0, x1, y1]`` per segment or edge. Works for a line list, a polyline, a polygon (edges including the closing one) or a polygon list. Returns: An expression of shape ``[n_segments, 4]``. Raises: ~prophys.diagnostics.ProphysError: ``NN123`` for any other structure. """ if hasattr(geometry, "segments"): src, closed = geometry.segments, None elif hasattr(geometry, "vertices"): src = geometry.vertices closed = type(geometry).__name__.startswith("Polygon") else: raise_error( "NN123", f"cannot make tokens from a {type(geometry).__name__}", hint="pass a LineList, Polyline, Polygon or PolygonList", ) def _tok(v): v = jnp.asarray(v) if closed is None: return v.reshape(v.shape[:-2] + (4,)) if v.shape[-2:] == (2, 2) else v nxt = jnp.roll(v, -1, axis=-2) if closed else v[..., 1:, :] cur = v if closed else v[..., :-1, :] seg = jnp.concatenate([cur, nxt], axis=-1) return seg.reshape((-1, 4)) if seg.ndim == 3 else seg return op("geometry_tokens", _tok, src)
[docs] class SampleSet(Expr): """Expression node holding a fresh Monte Carlo sample ``[n, k]`` of an expression. The sample is drawn with fixed random numbers (common random numbers, set by `seed`), so it is smooth in the model parameters. Every random variable `expr` depends on is drawn anew, independently of the outer Monte Carlo draw of the model. Create it with :func:`sample_set`. Args: expr: The expression to sample. n: Sample size (rows). seed: Seed of the fixed random numbers. """ def __init__(self, expr: Any, n: int, seed: int = 0): from ..domain.random_variable import RandomVariable self.expr = wrap(expr) self.n = int(n) self.seed = int(seed) found: dict = {} _collect(self.expr, RandomVariable, found) self.rvs = found def _children(self): # only the parameters are visible, the random variables are drawn here # and must not join the outer Monte Carlo draw of the model return tuple(self.expr.params().values()) def _evaluate(self, env): key = jax.random.PRNGKey(self.seed) keys = jax.random.split(key, max(len(self.rvs), 1)) env2 = {k: v for k, v in env.items() if k not in self.rvs} draws = { name: rv.distribution.sample(k, (self.n,)).evaluate(env) for k, (name, rv) in zip(keys, self.rvs.items()) } env2["__rv_samples__"] = {**dict(env.get("__rv_samples__", {})), **draws} v = jnp.asarray(self.expr._evaluate(env2)) v = jnp.broadcast_to(v, (self.n,) + v.shape[1:]) if v.ndim else jnp.broadcast_to(v, (self.n,)) return v.reshape(self.n, -1) def __repr__(self): return f"SampleSet({self.n})"
[docs] def sample_set(expr: Any, n: int, seed: int = 0) -> SampleSet: """Turn the Monte Carlo sample of an upstream quantity into a token set. Returns a :class:`SampleSet` whose value is ``[n, k]``: `n` draws of `expr` (flattened to `k` columns), drawn independently of the model's own Monte Carlo draw with fixed random numbers so the set is smooth in the parameters. Feeding it to a :class:`~prophys.nn.FieldSurrogate` makes the surrogate a map from a distribution to an output. Args: expr: The expression to sample, typically depending on random variables. n: Sample size. seed: Seed of the fixed random numbers. Example: >>> tokens = nn.sample_set(load, 256) # [256, 1] >>> damage = nn.FieldSurrogate("damage", rows=256, cols=1)(tokens) """ return SampleSet(expr, n, seed)