Skip to content

Emuv3 - #87

Open
DanielaBreitman wants to merge 7 commits into
masterfrom
emuv3
Open

Emuv3#87
DanielaBreitman wants to merge 7 commits into
masterfrom
emuv3

Conversation

@DanielaBreitman

@DanielaBreitman DanielaBreitman commented Aug 18, 2026

Copy link
Copy Markdown
Contributor
  • 21cmEMUv3 support
  • pspec-likelihood support
  • nautilus sampler added
  • Tests incoming, missing for now.

Summary by Sourcery

Expand emulator, likelihood, and sampling capabilities for 21cmEMU v3, radio-background analyses, and advanced HERA power-spectrum inference.

New Features:

  • Add support for the 21cmEMU v3 and RadioEMU emulator backends, including optional two-dimensional power-spectrum emulation.
  • Add Arcade radio-temperature and two-sided neutral-fraction likelihoods.
  • Add pspec-likelihood-backed HERA power-spectrum likelihoods for one- and two-dimensional data.
  • Add Nautilus as an optional nested sampler with configurable priors, resumption, vectorization, and posterior handling.
  • Add JWST luminosity-function datasets alongside the existing HST likelihood data.

Bug Fixes:

  • Improve likelihood handling for vectorized and scalar model inputs by consistently returning squeezed results.
  • Support both fractional and integer redshift naming conventions in power-spectrum datasets.

Enhancements:

  • Extend existing power-spectrum, Planck, neutral-fraction, and luminosity-function likelihoods to work with radio emulator outputs and emulator uncertainties.
  • Improve power-spectrum data reduction for matching redshift grids and two-dimensional spectra.

Build:

  • Add nautilus-sampler to the optional sampler dependencies.

@sourcery-ai

sourcery-ai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds Emu v3 and RadioEMU support, extends likelihoods to work with new emulator outputs and pspec_likelihood, and introduces Nautilus and richer UltraNest/MultiNest options in the MCMC driver along with assorted logging and data-handling tweaks.

Sequence diagram for Nautilus sampler integration in run_mcmc

sequenceDiagram
    participant User
    participant run_mcmc
    participant NautilusSampler as nautilus.Sampler
    participant Chain as LikelihoodComputationChain
    participant Core as CoreModules
    participant Lk as LikelihoodModules

    User->>run_mcmc: run_mcmc(core_modules, likelihood_modules, params, use_nautilus=True)
    run_mcmc->>Chain: build_computation_chain(core_modules, likelihood_modules, params)
    run_mcmc->>Chain: chain.setup()

    run_mcmc->>NautilusSampler: Sampler(prior, likelihood, n_dim, n_live, ...)
    Note over run_mcmc,NautilusSampler: likelihood is a wrapper calling chain

    NautilusSampler->>Chain: likelihood(p)
    Chain->>Core: build_model_data(Params)
    Core-->>Chain: context with emulator outputs
    Chain->>Lk: computeLikelihoods(context)
    Lk-->>Chain: total loglike
    Chain-->>NautilusSampler: loglike

    alt post_only is False
        User->>NautilusSampler: run(verbose, discard_exploration)
        NautilusSampler-->>User: posterior(equal_weight, equal_weight_boost)
    else post_only is True
        NautilusSampler-->>User: posterior(equal_weight, equal_weight_boost)
    end
Loading

File-Level Changes

Change Details Files
Extend 1D power and HERA likelihoods to support CoreRadioEMU, flexible band naming, and safer array handling.
  • Allow paired_core and required_cores to accept CoreRadioEMU alongside Core21cmEMU across several likelihood classes.
  • Relax k/kwfband key conventions using np.round(z,1), int(np.round(z)), and fallbacks for kperp/kpar grids.
  • Simplify Likelihood1DPowerCoeval.computeLikelihood vectorization and always return squeezed arrays.
  • Add band-level try/except handling in HERA upper-limit likelihood for multiple band key formats and compute error terms with optional emulator error arrays.
src/py21cmmc/likelihood.py
Introduce LikelihoodArcade and extended neutral-fraction and luminosity-function likelihood capabilities.
  • Add LikelihoodArcade using a parametric Arcade radio temperature model, compatible with CoreRadioEMU and vectorized over Tr.
  • Adjust NeutralFraction likelihood to handle per-chain xHI_err via at least-2d arrays and spline on error; add a two-sided chi^2 variant.
  • Update UV luminosity function likelihood to support HST/JWST datasets, dynamic redshift bin selection from emulator outputs, and telescope-specific data/noise files.
  • Standardize logger.debug messages to .format and squeeze outputs in multiple likelihoods.
src/py21cmmc/likelihood.py
Add a pspec_likelihood-based Pspec likelihood that can consume 1D or 2D emulator power spectra.
  • Introduce LikelihoodPspec subclass wiring pspec_likelihood DataModelInterface objects to emulator outputs, with choice of Gaussian or marginalized positive-systematics likelihood.
  • Use RegularGridInterpolator / interp1d / RectBivariateSpline depending on dimensionality and redshift sampling to build the theory_model callback for pspec_likelihood.
  • Handle both spherical and cylindrical k-space representations, building kperp/kpar flat arrays and window-function / covariance matrices from stored HERA-like data.
  • Ensure Pspec and HERA upper-limit likelihoods pair with CoreRadioEMU as well as Core21cmEMU via updated paired_core implementations.
src/py21cmmc/likelihood.py
Replace previous Core21cmEMU implementation with a generalized py21cmemu-backed emulator supporting v1/v2/v3 and optional 2D PS.
  • Rework Core21cmEMU.init to accept emulator_name, emulate_ps_2d, ps_2d_redshifts, mu_min, k1d_inp, and related options plus ctx_variables and caching.
  • Use py21cmemu.Emulator and get_emulator_properties to derive astro_param_keys, cosmo/flag/user params, and define io_options/store behavior.
  • Implement build_model_data to construct vectorized astro_params arrays, call emulator.predict, optionally write outputs to cache_dir, and push PS/PS_redshifts and other summaries (with errors) into the context.
  • Support three modes for PS: pure 2D PS, 2D -> 1D conversion via cylindrical_to_spherical, or native 1D PS; expose appropriate k/kperp/kpar and redshift arrays for each mode.
src/py21cmmc/core.py
Introduce CoreRadioEMU to interface with a radio background emulator and feed Radio-specific summaries into the context.
  • Define CoreRadioEMU class mirroring CoreBase patterns, with astro_param_keys specific to radio background and ctx_variables including Tb/Tr/xHI and PS-related outputs.
  • Instantiate py21cmemu.Emulator(emulator='radio_background') and implement build_model_data to support both dict and AstroParams vectorized inputs with default-filling for missing parameters.
  • Write emulator outputs and error arrays into the context, using .value attributes when present, and consistent cache_dir naming for stored outputs.
src/py21cmmc/core.py
Extend the MCMC driver to support Nautilus and enhance UltraNest and MultiNest configuration, including user-provided priors and warm-starting.
  • Add prior parameter to run_mcmc, allowing caller-supplied prior transforms for MultiNest and UltraNest; keep existing uniform-prior behavior as default.
  • Introduce use_nautilus and post_only flags, and extract extensive Nautilus configuration from mcmc_options (live points, networks, bounds, vectorization, etc.).
  • Wire MultiNest run() and UltraNest ReactiveNestedSampler with new options: verbose flag, warmstart_from_similar_file, optional SliceSampler stepsampler, and vectorized likelihood/prior handling.
  • Implement Nautilus likelihood wrapper around the computation chain and construction of a nautilus.Prior from params when none provided, returning both sampler and posterior samples with equal-weight options.
  • Minor robustness tweaks: use explicit 'r' for YAML read, adjust ultranest import of stepsampler, avoid writing LCC YAML, and squeeze likelihood outputs before returning.
src/py21cmmc/mcmc.py
Update project optional dependencies to include Nautilus.
  • Add 'nautilus-sampler' to the samplers extra in pyproject.toml to declare optional dependency for the new Nautilus integration.
pyproject.toml

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

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

  • The new LikelihoodNeutralFractionTwoSided class is defined twice with different implementations, which will cause one definition to overwrite the other; consolidate into a single class or clearly differentiate the names.
  • In LikelihoodArcade.init you call get_T_arcade(redshift) as a free function, but it is defined as an instance method (self.get_T_arcade); this will raise an error and should be changed to use the bound method.
  • Core21cmEMU/CoreRadioEMU parameter handling includes branches that reference variables like ap and astro_param_defaults that are not always initialized (e.g., ap in the dict branch and astro_param_defaults in CoreRadioEMU), which can lead to runtime errors; consider simplifying and centralizing the construction of the astro_params dict to avoid these cases.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new LikelihoodNeutralFractionTwoSided class is defined twice with different implementations, which will cause one definition to overwrite the other; consolidate into a single class or clearly differentiate the names.
- In LikelihoodArcade.__init__ you call get_T_arcade(redshift) as a free function, but it is defined as an instance method (self.get_T_arcade); this will raise an error and should be changed to use the bound method.
- Core21cmEMU/CoreRadioEMU parameter handling includes branches that reference variables like ap and astro_param_defaults that are not always initialized (e.g., ap in the dict branch and astro_param_defaults in CoreRadioEMU), which can lead to runtime errors; consider simplifying and centralizing the construction of the astro_params dict to avoid these cases.

## Individual Comments

### Comment 1
<location path="src/py21cmmc/likelihood.py" line_range="923-924" />
<code_context>
+        self.redshift = redshift
+        # If redshift is provided, evaluate the arcade model now
+        # Otherwise will be evaluated every time on emulator zs
+        if redshift is not None:
+            self.arcade = get_T_arcade(redshift)
+            
+    def get_T_arcade(self,z):
</code_context>
<issue_to_address>
**issue (bug_risk):** Calling `get_T_arcade` without `self` will raise a NameError.

Here `get_T_arcade` is used as a free function, but it’s defined as an instance method. Consider calling `self.get_T_arcade(redshift)` instead, or refactor `get_T_arcade` into a `@staticmethod` or module-level function and update all call sites.
</issue_to_address>

### Comment 2
<location path="src/py21cmmc/likelihood.py" line_range="233" />
<code_context>
         try:
             from classy import Class

</code_context>
<issue_to_address>
**issue (bug_risk):** Exception handling can leave `model_spline`/`err_spline` undefined and prints directly to stdout.

In `LikelihoodNeutralFraction.computeLikelihood`, if `InterpolatedUnivariateSpline` raises, the `except` block only prints `(xHI.shape, n)` and continues. This can leave `model_spline`/`err_spline` undefined and cause an `UnboundLocalError` later, and direct `print` calls inside a library API bypass proper logging. Please either re‑raise with a clearer error or implement a safe fallback path, and use the project logger (or remove the print) instead of `print`.
</issue_to_address>

### Comment 3
<location path="src/py21cmmc/likelihood.py" line_range="1567-1576" />
<code_context>
+class LikelihoodNeutralFractionTwoSided(LikelihoodNeutralFraction):
</code_context>
<issue_to_address>
**issue (bug_risk):** This new `LikelihoodNeutralFractionTwoSided` class duplicates the existing one below and silently gets overridden.

This makes the new implementation dead code and can mislead future maintainers. Please either remove the old definition, rename one of the classes, or consolidate the intended behaviour into a single class.
</issue_to_address>

### Comment 4
<location path="src/py21cmmc/core.py" line_range="1340-1341" />
<code_context>
+                    try:
+                        values.append(astro_params[k])
+                    except KeyError:
+                        if k == 'L_X_MINI':
+                            values.append(ap['L_X'])
+                        else:
+                            values.append(self.astro_param_defaults[k])
</code_context>
<issue_to_address>
**issue (bug_risk):** Use of `ap` before it is defined when filling missing astro parameters.

In `Core21cmEMU.build_model_data`, inside the `isinstance(astro_params, dict)` branch, the fallback for missing `L_X_MINI` references `ap['L_X']`, but `ap` is only defined later in the non‑dict path. This will raise a `NameError` when `L_X_MINI` is absent. Consider either constructing `ap` from `astro_params` earlier in the function or using `astro_params['L_X']` directly for this fallback.
</issue_to_address>

### Comment 5
<location path="src/py21cmmc/core.py" line_range="1343" />
<code_context>
+                        if k == 'L_X_MINI':
+                            values.append(ap['L_X'])
+                        else:
+                            values.append(self.astro_param_defaults[k])
+        elif isinstance(astro_params, p21.AstroParams):
+            values = astro_params.defining_dict.values
</code_context>
<issue_to_address>
**issue (bug_risk):** `CoreRadioEMU` references `self.astro_param_defaults` which is never defined in the class.

`build_model_data` falls back to `self.astro_param_defaults[k]`, but `CoreRadioEMU.__init__` never sets `self.astro_param_defaults` (unlike `Core21cmEMU`). This will raise an `AttributeError` when a missing key is encountered. Please either define `astro_param_defaults` in `CoreRadioEMU.__init__` (e.g., via an argument) or inline the required defaults at the call site.
</issue_to_address>

### Comment 6
<location path="src/py21cmmc/mcmc.py" line_range="288-289" />
<code_context>
         multimodal = mcmc_options.get("multimodal", True)
         write_output = mcmc_options.get("write_output", True)
         datadir = datadir + "/MultiNest/"
+        verbose = mcmc_options.get("vectorize", True)
         try:
             from pymultinest import run
</code_context>
<issue_to_address>
**issue (bug_risk):** MultiNest `verbose` flag is incorrectly tied to the `vectorize` option.

`verbose` is currently derived from `mcmc_options.get("vectorize", True)`, which makes verbosity depend on an unrelated option and is almost certainly a typo (likely intended to be `mcmc_options.get("verbose", True)`). This can cause confusing logging behaviour. Please introduce/use a dedicated `verbose` option instead.
</issue_to_address>

### Comment 7
<location path="src/py21cmmc/mcmc.py" line_range="549-558" />
<code_context>
-
+        
         def likelihood(p):
             if vectorized:
                 return chain.computeLikelihoods(
                     chain.build_model_data(
</code_context>
<issue_to_address>
**issue (bug_risk):** Nautilus likelihood assumes `p` is a dict, but Nautilus passes arrays; vectorized path will likely break.

In the Nautilus vectorized path, `likelihood` assumes `p` is a mapping (`zip(p.keys(), p.values())`), but Nautilus passes NumPy arrays by default. This will raise at runtime. You likely need a fixed parameter ordering (e.g., from `params.keys`) and to map array entries to `Params`, as done in the MultiNest/UltraNest paths. If you instead rely on `pass_dict`, the vectorized handling should be conditioned on that mode and clearly separated/documented.
</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 on lines +923 to +924
if redshift is not None:
self.arcade = get_T_arcade(redshift)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Calling get_T_arcade without self will raise a NameError.

Here get_T_arcade is used as a free function, but it’s defined as an instance method. Consider calling self.get_T_arcade(redshift) instead, or refactor get_T_arcade into a @staticmethod or module-level function and update all call sites.

)

else:
try:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Exception handling can leave model_spline/err_spline undefined and prints directly to stdout.

In LikelihoodNeutralFraction.computeLikelihood, if InterpolatedUnivariateSpline raises, the except block only prints (xHI.shape, n) and continues. This can leave model_spline/err_spline undefined and cause an UnboundLocalError later, and direct print calls inside a library API bypass proper logging. Please either re‑raise with a clearer error or implement a safe fallback path, and use the project logger (or remove the print) instead of print.

Comment on lines +1567 to +1576
class LikelihoodNeutralFractionTwoSided(LikelihoodNeutralFraction):
"""
A likelihood based on the measured neutral fraction at a range of redshifts.

The log-likelihood statistic is a simple chi^2.
"""

required_cores = (
(
core.CoreLightConeModule,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): This new LikelihoodNeutralFractionTwoSided class duplicates the existing one below and silently gets overridden.

This makes the new implementation dead code and can mislead future maintainers. Please either remove the old definition, rename one of the classes, or consolidate the intended behaviour into a single class.

Comment thread src/py21cmmc/core.py Outdated
Comment on lines +1340 to +1341
if k == 'L_X_MINI':
values.append(ap['L_X'])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Use of ap before it is defined when filling missing astro parameters.

In Core21cmEMU.build_model_data, inside the isinstance(astro_params, dict) branch, the fallback for missing L_X_MINI references ap['L_X'], but ap is only defined later in the non‑dict path. This will raise a NameError when L_X_MINI is absent. Consider either constructing ap from astro_params earlier in the function or using astro_params['L_X'] directly for this fallback.

Comment thread src/py21cmmc/core.py
if k == 'L_X_MINI':
values.append(ap['L_X'])
else:
values.append(self.astro_param_defaults[k])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): CoreRadioEMU references self.astro_param_defaults which is never defined in the class.

build_model_data falls back to self.astro_param_defaults[k], but CoreRadioEMU.__init__ never sets self.astro_param_defaults (unlike Core21cmEMU). This will raise an AttributeError when a missing key is encountered. Please either define astro_param_defaults in CoreRadioEMU.__init__ (e.g., via an argument) or inline the required defaults at the call site.

Comment thread src/py21cmmc/mcmc.py
Comment on lines 288 to +289
datadir = datadir + "/MultiNest/"
verbose = mcmc_options.get("vectorize", True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): MultiNest verbose flag is incorrectly tied to the vectorize option.

verbose is currently derived from mcmc_options.get("vectorize", True), which makes verbosity depend on an unrelated option and is almost certainly a typo (likely intended to be mcmc_options.get("verbose", True)). This can cause confusing logging behaviour. Please introduce/use a dedicated verbose option instead.

Comment thread src/py21cmmc/mcmc.py
Comment on lines 549 to 558
if vectorized:
return chain.computeLikelihoods(
chain.build_model_data(
Params(*[(k, v) for k, v in zip(params.keys, p.T)])
)
)
).squeeze()
else:
try:
return chain.computeLikelihoods(
chain.build_model_data(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Nautilus likelihood assumes p is a dict, but Nautilus passes arrays; vectorized path will likely break.

In the Nautilus vectorized path, likelihood assumes p is a mapping (zip(p.keys(), p.values())), but Nautilus passes NumPy arrays by default. This will raise at runtime. You likely need a fixed parameter ordering (e.g., from params.keys) and to map array entries to Params, as done in the MultiNest/UltraNest paths. If you instead rely on pass_dict, the vectorized handling should be conditioned on that mode and clearly separated/documented.

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