Concepts ========= Symbolic graphs, not eager computation ---------------------------------------- Every quantity you build in prophys — a distance, a transformed value, a distribution parameter — is an :class:`~prophys.symbolic.Expr` node, not a concrete number. Operators (``+ - * / ** @``, comparisons, indexing) are overloaded so ordinary Python arithmetic builds a graph: .. code-block:: python import prophys as prp a = prp.Param("a", init=2.0) b = prp.Const(3.0) expr = a * b + prp.exp(a) # still symbolic value = expr.evaluate({"a": 5.0}) # now a concrete jax.Array ``evaluate(env)`` substitutes concrete values for every :class:`Param `/:class:`Input ` leaf and recursively computes with ``jax.numpy`` — so any expression is automatically ``jit``/``grad``/``vmap``-able, with no special handling required. Leaves: Const, Input, Param, RandomVariable ---------------------------------------------- - **Const** — a fixed value (baked into the graph; folds away under ``jit``). - **Input** — a named placeholder bound at evaluation time (e.g. observations passed into ``log_prob``). - **Param** — a named, learnable/optimizable leaf. ``bounds=(lo, hi)`` or ``transform="softplus"`` constrain it through a smooth bijector, so unconstrained gradient descent (Adam, L-BFGS) never needs to know the parameter is bounded. - **RandomVariable** — a named *stochastic* leaf backed by a :class:`~prophys.distributions.Distribution`. Unlike the other leaves, uncertainty can enter a model at any point in the pipeline — not just at the very end. See :ref:`marginalization` below. Frames: a shared coordinate system -------------------------------------- Every spatial structure (:class:`~prophys.structures.PointList`, :class:`~prophys.structures.Polygon`, :class:`~prophys.structures.Field`, ...) is annotated with a :class:`~prophys.frames.Frame`. Structures declared in the same frame can be combined freely; combining structures from different, *unregistered* frames raises a :class:`~prophys.frames.FrameError` immediately at model-build time (a plain Python check, not a runtime cost inside the JAX trace). .. code-block:: python site = prp.Frame("site") houses = prp.PointList(house_coords, site) turbines = prp.PointList(turbine_coords, site) # same frame -> fine If two frames are related by a known affine transform (e.g. a local survey grid vs. a site-wide frame), register it once and structures in either frame interoperate transparently: .. code-block:: python from prophys import FrameRegistry, FrameTransform registry = FrameRegistry() registry.register(FrameTransform(local_frame, site, offset=jnp.array([1000.0, 500.0]))) Eval mode vs. opt mode ------------------------- Several primitives — the minimum over a set of distances, containment inside a polygon, a piecewise-linear kink — are not smooth at their decision boundary. Every such primitive exposes a ``mode`` argument: - ``mode="eval"`` (default): the exact, hard computation — a true minimum, a hard 0/1 containment indicator. Use this to *read off* a model's current numbers. - ``mode="opt"``, with a temperature ``tau``: a smooth approximation (``softmin``/``softmax``, a sigmoid-smoothed indicator, a soft segment blend for :class:`~prophys.transformations.PiecewiseLinear`) so gradients remain well-behaved when that quantity feeds a calibration or design optimization loop. :meth:`ProbabilityModel.compile(mode=...) ` sets this consistently for an entire model. Differentiable discrete states ---------------------------------- The same eval/opt split extends to discrete *design* decisions, not just geometry: :class:`~prophys.symbolic.BinaryState` (a learnable on/off, e.g. a valve or a reinforcement decision) and :class:`~prophys.symbolic.CategoricalState` (a learnable choice among ``k`` discrete options, e.g. a machine operating mode) evaluate to the exact hard state (0/1, one-hot) in ``eval`` mode, and to a temperature-relaxed soft assignment — the deterministic limit of a Gumbel-Softmax relaxation — in ``opt`` mode: .. code-block:: python reinforce = prp.BinaryState("reinforce_line_3", init=0.3) flow_capacity = base_capacity * (0.2 + 0.8 * reinforce) # differentiable gate pump_mode = prp.CategoricalState("pump_mode", k=3) setting = prp.dot(pump_mode, jnp.array([0.0, 0.5, 1.0])) Multiply or dot a state into any expression to gate that term; the same gradient pass used for continuous ``Param``\ s (via ``prp.optimize``) then also searches over the relaxed discrete decision, with the mode/``tau`` schedule following the model's ``compile(mode=, tau=)`` setting exactly like the geometry primitives above. .. _marginalization: Marginalizing over random inputs ------------------------------------ Because a :class:`~prophys.domain.RandomVariable` can sit *anywhere* in the graph (e.g. wind speed and direction feeding a dispersion model, not just the final noise term), evaluating an attribute's ``log_prob`` or ``expectation`` in general requires integrating out that upstream randomness: .. math:: p(y) = \int p(y \mid v)\, p(v)\, dv, \qquad \mathbb{E}[Y] = \int \mathbb{E}[Y \mid v]\, p(v)\, dv :class:`~prophys.engine.CompiledModel` does this via reparameterized Monte Carlo, entirely inside one JAX trace: draw ``n_samples`` samples of every upstream ``RandomVariable``, evaluate the attribute at each sample, and combine with ``logsumexp`` (for ``log_prob``) or a plain mean (for ``expectation``). Because sampling uses each distribution's reparameterized form where available (Gaussian, Weibull, Exponential, Gumbel, LogNormal), gradients flow through the Monte-Carlo estimate back to every upstream ``Param`` — including parameters of the wind distribution itself. A practical shape note: when an attribute's distribution parameters have their own batch shape (e.g. one concentration value per residence), that shape must broadcast against the ``(n_samples,)`` shape of the marginalized samples. A per-receptor batch of size 1 broadcasts against any ``n_samples``; a batch of, say, 4 residences generally will not unless ``n_samples`` happens to equal 4. See the :doc:`gas-dispersion example ` for the standard pattern (build one scalar receptor per attribute that needs marginalizing). The standardized model object --------------------------------- :class:`~prophys.domain.ProbabilityModel` bundles a model's :class:`~prophys.domain.UncertainAttribute`\ s (plus any :class:`~prophys.domain.AttributeInteractions`) into one object with a single entry point: ``.compile(mode, tau, n_samples, seed)`` returns a :class:`~prophys.engine.CompiledModel` exposing ``log_prob``, ``sample``, ``expectation``, ``prob``, ``quantile``, ``cvar``, and ``export`` — a stable interface regardless of how the underlying graph was built.