Skip to content

feat(math): add finite stochastic processes domain with sigma algebras, conditional expectation, filtrations, and Doob martingales (#1806) - #2036

Open
morluto wants to merge 2 commits into
mainfrom
agent/finite-stochastic-processes-1806
Open

feat(math): add finite stochastic processes domain with sigma algebras, conditional expectation, filtrations, and Doob martingales (#1806)#2036
morluto wants to merge 2 commits into
mainfrom
agent/finite-stochastic-processes-1806

Conversation

@morluto

@morluto morluto commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Closes #1806.

Summary

Add the finite_stochastic_processes domain implementing exact, bounded, deterministic finite stochastic process operations over immutable finite probability spaces. This is the missing layer above Jacobian's exact finite distributions and below its Markov-chain operations.

Design and library choices

The domain uses an exact rational kernel over immutable FiniteProbabilitySpace values. No martingale verifier, optimal-stopping solver, trading simulator, or general measure-theory framework is introduced. Per the issue, sample identity survives random-variable pushforwards, and conditional expectation preserves sample identity, combines atoms into measurable blocks, and returns a random variable constant on those blocks.

Representation (values.py). FiniteProbabilitySpace binds unique sample labels with positive rational masses summing to exactly 1. FiniteRandomVariable binds a complete value per sample. FiniteSigmaAlgebra represents a sigma algebra by its atom partition (blocks of sample labels). The values parse only well-formed inputs: unique labels, positive masses, valid partitions (disjoint, nonempty, union = entire space).

Operations (operations.py). All functions are deterministic and complete for accepted values.

  • from_observation.compute — sigma algebra generated by an observation map Y: Omega -> labels, with equal-value fibers as blocks.
  • join.compute — least sigma algebra containing both inputs, as the finest common refinement.
  • conditional_expectation.finite.computeE[X | G] as a block-constant random variable. On each block, the value is the probability-weighted average of X over the samples in that block.
  • filtration.natural.compute — natural filtration F_t = sigma(Y_0, ..., Y_t) with monotone refinement.
  • process.doob_martingale.compute — Doob martingale M_t = E[payoff | F_t] with rational-valued conditional expectations.

Invariances verified by tests

  • The trivial sigma algebra (one block = whole space) gives E[X | trivial] = E[X] (e.g., 1/2 for a fair coin).
  • The discrete sigma algebra (each sample is its own block) gives E[X | {H}] = X(H) and E[X | {T}] = X(T).
  • The Doob martingale of a fair coin payoff gives 1 on H and 0 on T under the full-information filtration.
  • Nonpositive masses, masses not summing to 1, and duplicate samples each produce named obstructions.

Validation

  • make check lint + typecheck clean; 12 focused domain tests; all 1134 tests/math, tests/catalog, tests/dispatch tests pass.
  • Frozen admission baselines updated and the finite_stochastic_processes schema-snapshot fragment added (5 operations).

Continue this on Linzumi


Open in Devin Review

…s, conditional expectation, filtrations, and Doob martingales

Add the finite_stochastic_processes domain implementing exact, bounded,
deterministic finite stochastic process operations over immutable finite
probability spaces:

- probability.finite_sigma_algebra.from_observation.compute: sigma algebra
  generated by an observation map, with equal-value fibers as blocks
- probability.finite_sigma_algebra.join.compute: least sigma algebra
  containing both inputs, as the finest common refinement
- probability.conditional_expectation.finite.compute: E[X | G] as a
  block-constant random variable with probability-weighted block averages
- probability.filtration.natural.compute: natural filtration
  F_t = sigma(Y_0, ..., Y_t) with monotone refinement
- probability.process.doob_martingale.compute: Doob martingale
  M_t = E[payoff | F_t] with rational-valued conditional expectations

The FiniteProbabilitySpace value parses only well-formed spaces with unique
labels, positive rational masses summing to exactly 1. The FiniteSigmaAlgebra
value parses only valid partitions (disjoint, nonempty, union = entire space).

Closes #1806
…es, tests

- Import TOOLS and ADMISSIONS in catalog/builtins.py.
- Update frozen admission baselines and add the finite_stochastic_processes
  schema-snapshot fragment (5 operations).
- Add 12 focused tests (sigma algebra, join, conditional expectation,
  filtration, Doob martingale, validation).
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 5 potential issues.

Open in Devin Review

Comment on lines +15 to +48
class FromObservationRequest(StrictModel):
"""Construct a sigma algebra from an observation map."""

space: FiniteProbabilitySpace
observation: tuple[str, ...] = Field(min_length=1)


class JoinRequest(StrictModel):
"""Compute the join of two sigma algebras."""

sigma1: FiniteSigmaAlgebra
sigma2: FiniteSigmaAlgebra


class ConditionalExpectationRequest(StrictModel):
"""Compute E[X | G]."""

rv: FiniteRandomVariable
sigma: FiniteSigmaAlgebra


class FiltrationRequest(StrictModel):
"""Compute the natural filtration of observations."""

space: FiniteProbabilitySpace
observations: tuple[tuple[str, ...], ...] = Field(default=())


class DoobMartingaleRequest(StrictModel):
"""Compute the Doob martingale of a payoff process."""

space: FiniteProbabilitySpace
observations: tuple[tuple[str, ...], ...] = Field(default=())
payoff: tuple[str, ...] = Field(min_length=1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Valid-looking probability requests crash the server instead of returning a result

Requests that pass validation but describe mismatched data are rejected deep inside the computation by raising a plain error (raise ValueError("observation must have one entry per sample") at src/jacobian/math/finite_stochastic_processes/operations.py:35) instead of being refused up front, so the call surfaces as a host crash rather than a typed answer.
Impact: A caller sending an observation list, payoff list, or pair of sigma algebras that do not line up with the sample space gets an unhandled server error instead of a clear rejection.

Missing cross-field validation on the request models lets kernel ValueErrors escape through dispatch

Verified by running the adapters: FromObservationRequest(space={'samples':['H','T'],...}, observation=('a',)) parses fine (the model only enforces min_length=1 at src/jacobian/math/finite_stochastic_processes/_models.py:19) and then compute_sigma_from_observation raises ValueError: observation must have one entry per sample. Likewise JoinRequest with two sigma algebras over different spaces raises ValueError: sigma algebras must share the same probability space (operations.py:68), ConditionalExpectationRequest with mismatched spaces raises at operations.py:90, and DoobMartingaleRequest with a mis-sized payoff or mis-sized observation entries raises at operations.py:143 / operations.py:35.

invoke_operation in src/jacobian/dispatch.py:47 calls operation.run(parsed) with no exception handling, so these become host exceptions. AGENTS.md requires: "The request must encode the advertised mathematical domain—bounds, positivity, completeness, non-degeneracy—not only the JSON shape. A request the model accepts must return a typed domain result; mathematical inapplicability belongs in the request validator or the result, not in a raised backend exception."

Fix by adding model_validator(mode="after") cross-field checks to FromObservationRequest, JoinRequest, ConditionalExpectationRequest, FiltrationRequest, and DoobMartingaleRequest.

Prompt for agents
The finite stochastic process request models only validate JSON shape, not the mathematical domain. Requests that parse successfully can still hit raise ValueError inside operations.py (observation length vs samples, payoff length vs samples, sigma algebras sharing a space, random variable and sigma algebra sharing a space), and jacobian/dispatch.py invoke_operation does not catch those, so an accepted request becomes a host exception. Add after-model validators on FromObservationRequest, JoinRequest, ConditionalExpectationRequest, FiltrationRequest (each observation entry) and DoobMartingaleRequest so every accepted request is guaranteed to produce a typed domain result, and add tests covering each rejection path.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +62 to +77
def sigma_algebra_join_correct(
sigma1: FiniteSigmaAlgebra,
sigma2: FiniteSigmaAlgebra,
) -> FiniteSigmaAlgebra:
"""Return the least sigma algebra containing both sigma algebras."""
if sigma1.space != sigma2.space:
raise ValueError("sigma algebras must share the same probability space")
blocks: list[tuple[str, ...]] = []
for b1 in sigma1.blocks:
for b2 in sigma2.blocks:
intersection = frozenset(b1) & frozenset(b2)
if intersection:
blocks.append(tuple(sorted(intersection)))
return FiniteSigmaAlgebra(
space=sigma1.space, blocks=tuple(b for b in blocks)
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Two identical copies of the sigma-algebra join exist, and the published one is not the one actually used

A second, byte-identical copy of the join computation is defined and used by the tools (sigma_algebra_join_correct at src/jacobian/math/finite_stochastic_processes/operations.py:62) while the copy advertised in the module's public list is never called, so the documented public function and the executed function can silently drift apart.
Impact: Maintainers fixing the published join function would not change the behaviour agents actually get.

Duplicate kernel plus __all__ / public API mismatch

operations.py:43-59 defines sigma_algebra_join, which is listed in operations.__all__ (operations.py:9-15) and re-exported from the package (src/jacobian/math/finite_stochastic_processes/__init__.py:8). operations.py:62-77 defines sigma_algebra_join_correct with an identical body; it is absent from __all__ yet is imported (via a function-local import) by the catalog adapter compute_join in src/jacobian/math/finite_stochastic_processes/_operations.py:38-42 and by the test at tests/math/finite_stochastic_processes/test_finite_stochastic_processes.py:81.

AGENTS.md: "Native public functions belong under jacobian.math, have explicit __all__" — the function backing the public probability.finite_sigma_algebra.join.compute operation is not part of any declared surface. The duplicate should be deleted and the adapter should call sigma_algebra_join with a normal module-level import.

Prompt for agents
operations.py contains two identical join implementations: sigma_algebra_join (exported in __all__ and from the package __init__) and sigma_algebra_join_correct (not exported, but imported lazily inside compute_join in _operations.py and by the domain test). Remove the duplicate, have compute_join import sigma_algebra_join at module level like the other adapter functions, and update the test accordingly.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +51 to +54
class FiltrationResult(StrictModel):
"""The natural filtration as a tuple of sigma algebra dicts."""

sigmas: tuple[dict[str, object], ...] = Field(default=())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Filtration results are returned as untyped dictionaries that drop the probability space

Each step of the filtration result is emitted as a loose key/value bag containing only the blocks ({"blocks": s.blocks} at src/jacobian/math/finite_stochastic_processes/_operations.py:55) rather than the typed sigma-algebra value, so the published output schema places no constraints on the data and the sample space with its probabilities is lost.
Impact: Callers receive an unconstrained, weakly described filtration output that cannot be fed straight back into the other sigma-algebra operations.

FiltrationResult uses tuple[dict[str, object], ...]

FiltrationResult.sigmas is declared as tuple[dict[str, object], ...] (src/jacobian/math/finite_stochastic_processes/_models.py:54), so the generated JSON schema for probability.filtration.natural.compute allows arbitrary objects. The kernel already returns tuple[FiniteSigmaAlgebra, ...] (src/jacobian/math/finite_stochastic_processes/operations.py:113-129), which is a StrictModel usable directly as the element type; the projection to a dict discards space and prevents round-tripping the output into JoinRequest / ConditionalExpectationRequest.

AGENTS.md "Types and transport": request/result models are authoritative at operation and wire boundaries and operations should compose through their typed mathematical values.

Suggested change
class FiltrationResult(StrictModel):
"""The natural filtration as a tuple of sigma algebra dicts."""
sigmas: tuple[dict[str, object], ...] = Field(default=())
class FiltrationResult(StrictModel):
"""The natural filtration as a tuple of sigma algebras."""
sigmas: tuple[FiniteSigmaAlgebra, ...] = Field(default=())
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +89 to +94
from jacobian.math.finite_stochastic_processes._admission import (
ADMISSIONS as FINITE_STOCHASTIC_PROCESSES_ADMISSIONS,
)
from jacobian.math.finite_stochastic_processes._tools import (
TOOLS as FINITE_STOCHASTIC_PROCESSES_TOOLS,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Import block ordering breaks ruff check despite the claimed clean lint

The new imports for finite_stochastic_processes are inserted before finite_state_transducers, but isort ordering puts finite_state_... first (a < o). Running uv run ruff check src/jacobian/catalog/builtins.py on this branch reports I001 Import block is un-sorted or un-formatted (1 error, fixable with --fix). The PR description claims make check lint is clean, so this would fail CI as-is.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +118 to +119
if not observations:
return ()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Doob martingale with no observations silently returns an empty result

observations defaults to () on both FiltrationRequest and DoobMartingaleRequest (src/jacobian/math/finite_stochastic_processes/_models.py:40,47), and filtration_natural short-circuits to () for empty input (operations.py:118-119). I confirmed by execution that compute_doob_martingale(DoobMartingaleRequest(space=..., payoff=("1","0"))) returns martingale=(). That is defensible (zero time steps), but an agent supplying a payoff and no observations gets an empty answer with no signal; the conventional convention would be to include the trivial F_0 (giving E[payoff]) or to require at least one observation. Worth deciding explicitly and testing.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@morluto morluto left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review verdict: no mathematical correctness blocker found; tighten the finite contract

I checked the core constructions independently:

  • equal-observation fibers are exactly the atoms of sigma(Y);
  • nonempty intersections of two atom partitions give the atom partition of their sigma-algebra join;
  • the blockwise probability-weighted average is the exact finite conditional expectation because every atom has positive mass;
  • repeated joins produce the natural filtration F_t = sigma(Y_0,...,Y_t);
  • M_t = E[X | F_t] satisfies the expected tower/martingale identities. I exhaustively checked the partition/join/tower identity over every set partition through five atoms with nonuniform rational masses.

I did not find a formula or logic bug in those kernels. The main issues are at the boundary:

  1. observations has no maximum number of time steps, so filtration and Doob outputs are unbounded despite the domain's otherwise bounded value model. Add a time-horizon bound and cross-field validation that every observation and payoff has exactly one entry per sample.
  2. FromObservationRequest likewise accepts the wrong observation length and fails only inside the kernel. Make this a request invariant.
  3. FiltrationResult erases the typed FiniteSigmaAlgebra values into dict[str, object] records containing only blocks. Returning tuple[FiniteSigmaAlgebra, ...] would preserve the exact space/partition invariant and make monotone refinement replayable.
  4. sigma_algebra_join_correct() is byte-for-byte the same construction as the public sigma_algebra_join(), while the catalog adapter imports the private-sounding duplicate. Keep one primitive and test it directly.

Useful property tests would assert E[E[X|H]|G] = E[X|G] whenever G ⊆ H, preservation of expectation, block constancy, and that every successive filtration partition refines the previous one. The current mathematics appears sound.

morluto commented Aug 18, 2026

Copy link
Copy Markdown
Owner Author

Deep review summary

Verdict: REQUEST CHANGES — the probability identities are correct, but accepted requests can escape as host exceptions and the executed join kernel is not the published one.

I independently checked the core constructions:

  • equal-observation fibers are the atoms of σ(Y);
  • nonempty intersections of atom partitions give the atom partition of the join;
  • blockwise probability-weighted averages are the exact finite conditional expectation;
  • repeated joins form F_t = σ(Y_0,…,Y_t);
  • M_t = E[X | F_t] satisfies the tower and martingale identities. I exhaustively checked the partition/join/tower identity through five atoms with nonuniform rational masses.

The blocker is the request boundary. Models accept mismatched observation lengths, payoff lengths, probability spaces, and observation rows, then kernels raise plain ValueError. Dispatch does not convert those into typed results, so validly parsed requests become server errors. Add cross-field validators to every request so accepted inputs are guaranteed to produce domain results.

There is also an avoidable split-brain implementation: sigma_algebra_join is the documented/exported function, while an identical unexported sigma_algebra_join_correct is what the catalog adapter and tests call. Delete the duplicate and route the operation through the public kernel; otherwise fixes can silently apply to the wrong function.

Finally, canonicalize the partition/block ordering and rational serialization so equivalent probability spaces and sigma algebras have deterministic wire identity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant