Skip to content

Add heuristic JIT execution for JAX delta_x - #144

Open
steven-murray wants to merge 8 commits into
v1from
jax-phase2-execution-quality
Open

Add heuristic JIT execution for JAX delta_x#144
steven-murray wants to merge 8 commits into
v1from
jax-phase2-execution-quality

Conversation

@steven-murray

@steven-murray steven-murray commented May 25, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add a heuristic usejit policy for powerbox.jax.PowerBox / LogNormalPowerBox.
  • Make delta_x() use a cached JIT kernel by default, with a private eager path for comparison and benchmarks.
  • Expand JAX tests and benchmark docs/artifacts, including cold vs warm timings and eager vs JIT comparisons.
  • Update the JAX tutorial with get_power usage and usejit guidance.

Target

  • Base branch: v1

Summary by Sourcery

Introduce a heuristic-controlled JIT execution path for JAX PowerBox real-space field generation and extend benchmarks to compare eager vs JIT performance with cold and warm timings.

New Features:

  • Add a configurable usejit policy for JAX PowerBox and LogNormalPowerBox that selects JIT or eager execution based on box size or explicit user choice.
  • Introduce a cached JIT-compiled delta_x kernel for JAX PowerBox to reuse compilation across calls.
  • Extend backend benchmarks to record separate cold and warm runtimes for NumPy, FFTW, and JAX backends, including eager vs JIT variants on CPU and GPU.

Enhancements:

  • Require an explicit PRNG key for JAX PowerBox delta_x calls when no key has been provided, improving error reporting for misconfigured randomness.
  • Emit a one-time warning when repeated delta_x calls run in default eager mode for large boxes without an explicit JIT choice.
  • Refine JAX backend plotting and metadata to handle additional eager and JIT backend variants and their timing fields.

Tests:

  • Add JAX tests to assert that JIT-based delta_x matches the eager implementation for Gaussian and lognormal fields, and that PRNG key handling behaves as expected.
  • Add tests covering the default usejit heuristic, including threshold-based switching and warning behavior for repeated eager execution.

Steven Murray and others added 4 commits May 25, 2026 12:40
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sourcery-ai

sourcery-ai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Implements a heuristic JIT execution policy for JAX PowerBox/LogNormalPowerBox delta_x, adds a cached JIT kernel with optional eager path, wires it into benchmarks to report cold vs warm timings for eager vs JIT across CPU/GPU, and expands tests to validate behavior and warnings.

Sequence diagram for JAX PowerBox delta_x heuristic JIT execution

sequenceDiagram
    actor User
    participant PowerBox
    participant _delta_x_kernel
    participant _delta_x_eager
    participant warnings

    User->>PowerBox: delta_x(key)
    PowerBox->>PowerBox: _delta_x_calls += 1
    PowerBox->>PowerBox: _resolve_key(key)

    alt [not usejit and not _usejit_requested and _delta_x_calls > 1 and not _delta_x_warning_emitted]
        PowerBox->>warnings: warn
        PowerBox->>PowerBox: _delta_x_warning_emitted = True
    end

    alt [usejit]
        PowerBox->>_delta_x_kernel: _delta_x_kernel
        _delta_x_kernel-->>PowerBox: field
    else [not usejit]
        PowerBox->>_delta_x_eager: _delta_x_eager
        _delta_x_eager-->>PowerBox: field
    end

    PowerBox-->>User: field
Loading

File-Level Changes

Change Details Files
Add heuristic usejit policy and cached JIT kernel for JAX PowerBox.delta_x and LogNormalPowerBox.delta_x.
  • Introduce DEFAULT_JIT_NTOT_THRESHOLD and a _resolve_usejit helper to choose JIT by default for sufficiently large grids.
  • Track delta_x call count and emit a performance warning only when eager execution is implicitly chosen and called repeatedly.
  • Implement a private _delta_x_eager implementation and a cached_property _delta_x_kernel JIT-compiled wrapper used when usejit is enabled.
  • Update PowerBox.init to accept an optional usejit flag, store whether it was explicitly requested, and expose the resolved usejit on instances.
  • Refactor LogNormalPowerBox.delta_x to delegate to the base-class policy while keeping its own eager implementation for the underlying lognormal field.
src/powerbox/jax/powerbox.py
Expand JAX tests to cover JIT vs eager correctness, key requirements, and the heuristic/warning behavior.
  • Import the powerbox.jax.powerbox module directly for access to DEFAULT_JIT_NTOT_THRESHOLD during tests.
  • Add tests confirming JIT and eager delta_x results match for PowerBox and LogNormalPowerBox when usejit=True.
  • Add a test ensuring delta_x raises a ValueError when no PRNG key is available from either the instance or the call.
  • Add tests verifying that the default heuristic can be forced into eager or JIT via monkeypatching DEFAULT_JIT_NTOT_THRESHOLD and checking usejit.
  • Add tests that default eager mode issues a warning only on repeated delta_x calls, while explicit usejit=False suppresses that warning.
tests/test_jax.py
Enhance backend benchmarks to report cold vs warm timings and compare JAX eager vs JIT on CPU and GPU.
  • Change _time_numpy and _time_jax to run an explicit cold call followed by warmups and repeats, returning a dict with cold_seconds and warm_seconds instead of a single scalar.
  • Propagate the new dict timing API through all benchmark helpers for generation and power estimation across NumPy, FFTW, and JAX backends.
  • Extend JAX generation benchmarks to accept a use_jit flag, allocate sufficient random keys, and pass usejit through to jpb.PowerBox.
  • Split JAX benchmark backends into eager and JIT variants for both CPU and GPU, skipping power benchmarks for JIT-only entries and updating backend naming and colors accordingly.
  • Record both cold_seconds and warm_seconds in the collected benchmark results, and add timing_fields metadata to the benchmark summary for downstream consumers.
docs/demos/benchmark_backends.py
docs/_static/backend_benchmark_results.json
docs/demos/backend_benchmarks.rst

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@codecov

codecov Bot commented May 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.35%. Comparing base (a326fee) to head (277a982).

Additional details and impacted files
@@            Coverage Diff             @@
##               v1     #144      +/-   ##
==========================================
+ Coverage   95.02%   95.35%   +0.32%     
==========================================
  Files          12       12              
  Lines        1206     1248      +42     
==========================================
+ Hits         1146     1190      +44     
+ Misses         60       58       -2     
Flag Coverage Δ
unittests 95.35% <100.00%> (+0.32%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sourcery-ai sourcery-ai 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.

Hey - I've found 4 issues, and left some high level feedback:

  • The new warning logic in PowerBox.delta_x uses warnings.warn but warnings is not imported in powerbox.jax.powerbox, which will raise a NameError at runtime; add the import at the top of the module.
  • The _delta_x_kernel cached JIT uses a closure over self; if any instance attributes that affect the computation (e.g., pk, ensure_physical, geometry) are mutated after construction, the compiled kernel will not see those changes—consider documenting this expectation or guarding against post-construction mutation if that’s not intended.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new warning logic in `PowerBox.delta_x` uses `warnings.warn` but `warnings` is not imported in `powerbox.jax.powerbox`, which will raise a `NameError` at runtime; add the import at the top of the module.
- The `_delta_x_kernel` cached JIT uses a closure over `self`; if any instance attributes that affect the computation (e.g., `pk`, `ensure_physical`, geometry) are mutated after construction, the compiled kernel will not see those changes—consider documenting this expectation or guarding against post-construction mutation if that’s not intended.

## Individual Comments

### Comment 1
<location path="src/powerbox/jax/powerbox.py" line_range="248-252" />
<code_context>
+    def delta_x(self, key: jax.Array | None = None) -> jax.Array:
+        """Return the realized real-space field using the configured execution policy."""
+        self._delta_x_calls += 1
+        if (
+            not self.usejit
+            and not self._usejit_requested
+            and self._delta_x_calls > 1
+            and not self._delta_x_warning_emitted
+        ):
+            warnings.warn(
+                "delta_x() is using eager execution by default for this box size. "
+                "Repeated calls may be much slower than usejit=True.",
+                stacklevel=2,
+            )
+            self._delta_x_warning_emitted = True
+
+        run_key = self._resolve_key(key)
</code_context>
<issue_to_address>
**suggestion (performance):** The eager-execution warning message can be misleading given the JIT heuristic.

Because the default policy prefers eager for small `Ntot` (where JIT overhead can make JIT slower), this warning can push users with small boxes and repeated calls toward `usejit=True` even when that hurts performance. Consider either:
- Emitting the warning only when `Ntot` exceeds the JIT threshold, or
- Rewording to something like "Consider usejit=True for large boxes" so it doesn’t imply JIT is generally faster in all cases.

```suggestion
            warnings.warn(
                "delta_x() is using eager execution by default for this box size. "
                "For large boxes with repeated calls, consider usejit=True for better performance.",
                stacklevel=2,
            )
```
</issue_to_address>

### Comment 2
<location path="tests/test_jax.py" line_range="147-163" />
<code_context>
+    assert jit_pb.usejit is True
+
+
+def test_jax_default_eager_mode_warns_only_on_repeated_calls(monkeypatch) -> None:
+    monkeypatch.setattr(jpb_powerbox, "DEFAULT_JIT_NTOT_THRESHOLD", 10_000)
+    pb = jpb.PowerBox(
+        (8, 8),
+        dim=2,
+        pk=lambda k: (1 + k) ** -2.0,
+        boxlength=4.0,
+        key=jax.random.key(27),
+    )
+
+    with warnings.catch_warnings(record=True) as record:
+        warnings.simplefilter("always")
+        pb.delta_x()
+        pb.delta_x()
+
+    assert len(record) == 1
+    assert "Repeated calls may be much slower" in str(record[0].message)
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** Add a complementary test that default JIT mode (Ntot above threshold) does not emit warnings on repeated calls.

To cover the JIT path as well, please add a counterpart test where `DEFAULT_JIT_NTOT_THRESHOLD` is set below `Ntot` so that `usejit` is `True` by default, and then assert that repeated `delta_x()` calls do not emit warnings. This will help catch regressions where warnings might appear even when JIT is enabled.

```suggestion
def test_jax_default_eager_mode_warns_only_on_repeated_calls(monkeypatch) -> None:
    monkeypatch.setattr(jpb_powerbox, "DEFAULT_JIT_NTOT_THRESHOLD", 10_000)
    pb = jpb.PowerBox(
        (8, 8),
        dim=2,
        pk=lambda k: (1 + k) ** -2.0,
        boxlength=4.0,
        key=jax.random.key(27),
    )

    with warnings.catch_warnings(record=True) as record:
        warnings.simplefilter("always")
        pb.delta_x()
        pb.delta_x()

    assert len(record) == 1
    assert "Repeated calls may be much slower" in str(record[0].message)


def test_jax_default_jit_mode_does_not_warn_on_repeated_calls(monkeypatch) -> None:
    # Set the threshold low enough that Ntot for the box is above it,
    # so that JIT is enabled by default.
    monkeypatch.setattr(jpb_powerbox, "DEFAULT_JIT_NTOT_THRESHOLD", 1)
    pb = jpb.PowerBox(
        (8, 8),
        dim=2,
        pk=lambda k: (1 + k) ** -2.0,
        boxlength=4.0,
        key=jax.random.key(28),
    )

    assert pb.usejit is True

    with warnings.catch_warnings(record=True) as record:
        warnings.simplefilter("always")
        pb.delta_x()
        pb.delta_x()

    # When JIT is enabled by default, repeated calls should not emit
    # the "Repeated calls may be much slower" warning.
    assert not any(
        "Repeated calls may be much slower" in str(warning.message)
        for warning in record
    )
```
</issue_to_address>

### Comment 3
<location path="docs/demos/backend_benchmarks.rst" line_range="15" />
<code_context>
-shapes ``(N,)*dim`` and ``boxlength=100``. The current size ladders are
+The benchmark script is ``docs/demos/benchmark_backends.py``. It records both a
+``cold`` timing (first run, including any one-time setup such as JAX compilation) and a
+``warm`` timing (median of repeated post-warmup runs) for each configuration, using
+cubic boxes with shapes ``(N,)*dim`` and ``boxlength=100``. The current size ladders are
 ``[4096, 16384, 65536, 262144]`` in 1D, ``[64, 128, 256, 512]`` in 2D, and
</code_context>
<issue_to_address>
**nitpick (typo):** Consider making "post-warmup" consistent with the earlier "warm-up" spelling.

Earlier in this paragraph you use "warm-up run", but here you have "post-warmup runs". Consider changing this to "post–warm-up runs" (or "post-warm-up runs") for consistent hyphenation.

```suggestion
``warm`` timing (median of repeated post-warm-up runs) for each configuration, using
```
</issue_to_address>

### Comment 4
<location path="src/powerbox/jax/powerbox.py" line_range="221" />
<code_context>

-    def delta_x(self, key: jax.Array | None = None) -> jax.Array:
-        """Return the realized real-space field from the input power spectrum."""
+    def _delta_x_eager(self, key: jax.Array | None = None) -> jax.Array:
+        """Return the realized real-space field without JIT compilation."""
         dk = jnp.sqrt(self._power_array_rfft()) * self._gaussian_modes_rfft(key=key)
</code_context>
<issue_to_address>
**issue (complexity):** Consider unifying eager and JIT execution behind a single `_delta_x_impl` chosen once in `__init__`, so `delta_x` becomes a simple dispatcher and subclasses only override `_delta_x_impl` rather than redefining `delta_x` or using a cached kernel.

You can keep all current behavior while flattening the execution path and clarifying the subclass contract by:

1. **Unifying eager/JIT around a single `_delta_x_impl`**  
2. **Binding the chosen implementation once in `__init__`**  
3. **Letting subclasses override `_delta_x_impl` only (no `delta_x` override)**  
4. **Dropping the `cached_property` kernel that closes over `self`**

### 1. Base class: single impl + bound dispatcher

Replace `_delta_x_eager` + `_delta_x_kernel` with a single implementation method and a bound callable chosen in `__init__`:

```python
class PowerBox:
    def __init__(..., usejit: bool | None = None):
        ...
        self._usejit_requested = usejit is not None
        self.usejit = self._resolve_usejit(usejit)
        self._delta_x_calls = 0
        self._delta_x_warning_emitted = False

        # bind the execution function once
        impl = self._delta_x_impl  # instance method

        if self.usejit:
            # jitted wrapper, but delta_x() only sees a simple callable
            self._delta_x = jax.jit(lambda run_key: impl(run_key))
        else:
            self._delta_x = impl
        ...

    def _delta_x_impl(self, key: jax.Array | None = None) -> jax.Array:
        """Core delta_x implementation (eager semantics)."""
        dk = jnp.sqrt(self._power_array_rfft()) * self._gaussian_modes_rfft(key=key)
        field = self._irfft_to_field(dk, scale=self.V)
        if self.ensure_physical:
            field = jnp.clip(field, -1, jnp.inf)
        return field

    def delta_x(self, key: jax.Array | None = None) -> jax.Array:
        """Return the realized real-space field using the configured execution policy."""
        self._delta_x_calls += 1
        if (
            not self.usejit
            and not self._usejit_requested
            and self._delta_x_calls > 1
            and not self._delta_x_warning_emitted
        ):
            warnings.warn(
                "delta_x() is using eager execution by default for this box size. "
                "Repeated calls may be much slower than usejit=True.",
                stacklevel=2,
            )
            self._delta_x_warning_emitted = True

        run_key = self._resolve_key(key)
        return self._delta_x(run_key)
```

This keeps:

- The heuristic and explicit `usejit` override.
- The call counter and one-time warning.
- A private eager implementation (`_delta_x_impl`) still available for benchmarking.

But `delta_x` is now a simple, stateless dispatcher after the warning check, and there’s no extra indirection via a `cached_property`.

### 2. Subclass: override `_delta_x_impl` only

Have `LogNormalPowerBox` override only the implementation, not `delta_x`, and reuse the base policy logic:

```python
class LogNormalPowerBox(PowerBox):
    def _delta_x_impl(self, key: jax.Array | None = None) -> jax.Array:
        """Return the realized lognormal over-density field (eager semantics)."""
        dk = jnp.sqrt(self.gaussian_power_array())
        dk = dk * self._gaussian_modes_rfft(key=key)
        field = self._irfft_to_field(dk, scale=jnp.sqrt(self.V))
        sigma_g = jnp.var(field)
        return jnp.exp(field - sigma_g / 2) - 1
```

You can then drop:

- `PowerBox._delta_x_eager`
- `PowerBox._delta_x_kernel` (`@cached_property`)
- `LogNormalPowerBox._delta_x_eager`
- `LogNormalPowerBox.delta_x` override

This removes the subtle base-class contract “`delta_x` calls `_delta_x_eager`” and the closure-based cached kernel, while preserving JIT control, eager benchmarking, and the warning behavior.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/powerbox/jax/powerbox.py
Comment thread tests/test_jax.py
Comment on lines +147 to +163
def test_jax_default_eager_mode_warns_only_on_repeated_calls(monkeypatch) -> None:
monkeypatch.setattr(jpb_powerbox, "DEFAULT_JIT_NTOT_THRESHOLD", 10_000)
pb = jpb.PowerBox(
(8, 8),
dim=2,
pk=lambda k: (1 + k) ** -2.0,
boxlength=4.0,
key=jax.random.key(27),
)

with warnings.catch_warnings(record=True) as record:
warnings.simplefilter("always")
pb.delta_x()
pb.delta_x()

assert len(record) == 1
assert "Repeated calls may be much slower" in str(record[0].message)

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.

suggestion (testing): Add a complementary test that default JIT mode (Ntot above threshold) does not emit warnings on repeated calls.

To cover the JIT path as well, please add a counterpart test where DEFAULT_JIT_NTOT_THRESHOLD is set below Ntot so that usejit is True by default, and then assert that repeated delta_x() calls do not emit warnings. This will help catch regressions where warnings might appear even when JIT is enabled.

Suggested change
def test_jax_default_eager_mode_warns_only_on_repeated_calls(monkeypatch) -> None:
monkeypatch.setattr(jpb_powerbox, "DEFAULT_JIT_NTOT_THRESHOLD", 10_000)
pb = jpb.PowerBox(
(8, 8),
dim=2,
pk=lambda k: (1 + k) ** -2.0,
boxlength=4.0,
key=jax.random.key(27),
)
with warnings.catch_warnings(record=True) as record:
warnings.simplefilter("always")
pb.delta_x()
pb.delta_x()
assert len(record) == 1
assert "Repeated calls may be much slower" in str(record[0].message)
def test_jax_default_eager_mode_warns_only_on_repeated_calls(monkeypatch) -> None:
monkeypatch.setattr(jpb_powerbox, "DEFAULT_JIT_NTOT_THRESHOLD", 10_000)
pb = jpb.PowerBox(
(8, 8),
dim=2,
pk=lambda k: (1 + k) ** -2.0,
boxlength=4.0,
key=jax.random.key(27),
)
with warnings.catch_warnings(record=True) as record:
warnings.simplefilter("always")
pb.delta_x()
pb.delta_x()
assert len(record) == 1
assert "Repeated calls may be much slower" in str(record[0].message)
def test_jax_default_jit_mode_does_not_warn_on_repeated_calls(monkeypatch) -> None:
# Set the threshold low enough that Ntot for the box is above it,
# so that JIT is enabled by default.
monkeypatch.setattr(jpb_powerbox, "DEFAULT_JIT_NTOT_THRESHOLD", 1)
pb = jpb.PowerBox(
(8, 8),
dim=2,
pk=lambda k: (1 + k) ** -2.0,
boxlength=4.0,
key=jax.random.key(28),
)
assert pb.usejit is True
with warnings.catch_warnings(record=True) as record:
warnings.simplefilter("always")
pb.delta_x()
pb.delta_x()
# When JIT is enabled by default, repeated calls should not emit
# the "Repeated calls may be much slower" warning.
assert not any(
"Repeated calls may be much slower" in str(warning.message)
for warning in record
)

Comment thread docs/demos/backend_benchmarks.rst

def delta_x(self, key: jax.Array | None = None) -> jax.Array:
"""Return the realized real-space field from the input power spectrum."""
def _delta_x_eager(self, key: jax.Array | None = None) -> jax.Array:

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.

issue (complexity): Consider unifying eager and JIT execution behind a single _delta_x_impl chosen once in __init__, so delta_x becomes a simple dispatcher and subclasses only override _delta_x_impl rather than redefining delta_x or using a cached kernel.

You can keep all current behavior while flattening the execution path and clarifying the subclass contract by:

  1. Unifying eager/JIT around a single _delta_x_impl
  2. Binding the chosen implementation once in __init__
  3. Letting subclasses override _delta_x_impl only (no delta_x override)
  4. Dropping the cached_property kernel that closes over self

1. Base class: single impl + bound dispatcher

Replace _delta_x_eager + _delta_x_kernel with a single implementation method and a bound callable chosen in __init__:

class PowerBox:
    def __init__(..., usejit: bool | None = None):
        ...
        self._usejit_requested = usejit is not None
        self.usejit = self._resolve_usejit(usejit)
        self._delta_x_calls = 0
        self._delta_x_warning_emitted = False

        # bind the execution function once
        impl = self._delta_x_impl  # instance method

        if self.usejit:
            # jitted wrapper, but delta_x() only sees a simple callable
            self._delta_x = jax.jit(lambda run_key: impl(run_key))
        else:
            self._delta_x = impl
        ...

    def _delta_x_impl(self, key: jax.Array | None = None) -> jax.Array:
        """Core delta_x implementation (eager semantics)."""
        dk = jnp.sqrt(self._power_array_rfft()) * self._gaussian_modes_rfft(key=key)
        field = self._irfft_to_field(dk, scale=self.V)
        if self.ensure_physical:
            field = jnp.clip(field, -1, jnp.inf)
        return field

    def delta_x(self, key: jax.Array | None = None) -> jax.Array:
        """Return the realized real-space field using the configured execution policy."""
        self._delta_x_calls += 1
        if (
            not self.usejit
            and not self._usejit_requested
            and self._delta_x_calls > 1
            and not self._delta_x_warning_emitted
        ):
            warnings.warn(
                "delta_x() is using eager execution by default for this box size. "
                "Repeated calls may be much slower than usejit=True.",
                stacklevel=2,
            )
            self._delta_x_warning_emitted = True

        run_key = self._resolve_key(key)
        return self._delta_x(run_key)

This keeps:

  • The heuristic and explicit usejit override.
  • The call counter and one-time warning.
  • A private eager implementation (_delta_x_impl) still available for benchmarking.

But delta_x is now a simple, stateless dispatcher after the warning check, and there’s no extra indirection via a cached_property.

2. Subclass: override _delta_x_impl only

Have LogNormalPowerBox override only the implementation, not delta_x, and reuse the base policy logic:

class LogNormalPowerBox(PowerBox):
    def _delta_x_impl(self, key: jax.Array | None = None) -> jax.Array:
        """Return the realized lognormal over-density field (eager semantics)."""
        dk = jnp.sqrt(self.gaussian_power_array())
        dk = dk * self._gaussian_modes_rfft(key=key)
        field = self._irfft_to_field(dk, scale=jnp.sqrt(self.V))
        sigma_g = jnp.var(field)
        return jnp.exp(field - sigma_g / 2) - 1

You can then drop:

  • PowerBox._delta_x_eager
  • PowerBox._delta_x_kernel (@cached_property)
  • LogNormalPowerBox._delta_x_eager
  • LogNormalPowerBox.delta_x override

This removes the subtle base-class contract “delta_x calls _delta_x_eager” and the closure-based cached kernel, while preserving JIT control, eager benchmarking, and the warning behavior.

Steven Murray and others added 4 commits May 25, 2026 13:58
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant