Skip to content

Add phase+distance marginalized likelihood#87

Open
mrosep wants to merge 1 commit intojim-devfrom
dist_marg
Open

Add phase+distance marginalized likelihood#87
mrosep wants to merge 1 commit intojim-devfrom
dist_marg

Conversation

@mrosep
Copy link

@mrosep mrosep commented Mar 2, 2026

Summary

  • Adds PhaseDistanceMarginalizedLikelihoodFD class implementing combined phase + distance marginalization following Thrane & Talbot (2019), Section C.5 (Eq. 79)
  • Phase is marginalized first analytically via Bessel function I_0, then distance is marginalized numerically over a pre-computed grid via logsumexp
  • Inherits all distance grid setup, validation, and prior handling from DistanceMarginalizedLikelihoodFD (merged in PR Add distance-marginalized likelihood #84)
  • Neither phase_c nor d_L are sampling parameters — reduces dimensionality by 2

Implementation

The integrand at each distance grid point i is:

log I_0(|kappa²_C_ref| * s_i) - 0.5 * rho²_opt_ref * s_i² + log_w_i

where kappa²_C_ref is the complex matched-filter inner product at the reference distance (computed via complex_inner_product), s_i = D_0/D_i is the scaling factor, and log_w_i are the normalized log prior weights.

Verification

Tested against bilby's GravitationalWaveTransient with phase_marginalization=True, distance_marginalization=True using shared 4s injection data (IMRPhenomD, M_c=35, q=0.9, 3-detector H1/L1/V1):

Test Result
Full logL (Jim vs bilby) delta = 0.0000 (exact match)
Phase-only marg logL (Jim vs bilby) delta = 0.0000 (exact match)
Phase+distance marg logL (Jim vs bilby) delta = 0.0026
Eq. 79 formula vs scipy.quad delta < 1e-04
Zero signal (kappa²=rho²=0) logL = 0.0
Phase+dist formula differs from distance-only Confirmed
Phase+dist marg vs brute-force integration delta < 0.5

The 0.0026 difference is due to bilby's 2D lookup table with bicubic spline interpolation vs Jim's direct quadrature (same as distance-only marginalization in PR #84).

Test plan

  • Pure math tests (Eq. 79 vs scipy quadrature, zero signal, comparison with distance-only)
  • Jim self-consistency (brute-force integration of phase-marg likelihood over distance vs combined class)
  • Cross-code comparison (Jim vs bilby, all marginalization modes)
  • Nested sampling runs on GW150914 with both AW and NSS kernels (13D)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • A new frequency-domain likelihood option is now available, enabling joint analytic marginalisation over phase and numerical marginalisation over distance parameters for gravitational wave analysis.

… marginalization

Implements Thrane & Talbot (2019) Eq. 79: phase is marginalized first
analytically via Bessel function, then distance is marginalized numerically.
Inherits all distance grid setup from DistanceMarginalizedLikelihoodFD.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Mar 2, 2026

📝 Walkthrough

Walkthrough

A new frequency-domain likelihood class PhaseDistanceMarginalizedLikelihoodFD is introduced to perform joint analytic phase marginalisation and numerical distance marginalisation. The likelihood registry and base class frequency-consistency check are updated to accommodate this new class type.

Changes

Cohort / File(s) Summary
Frequency-domain likelihood implementation
src/jimgw/core/single_event/likelihood.py
Added PhaseDistanceMarginalizedLikelihoodFD class implementing joint analytic phase and numerical distance marginalisation using complex inner products, log I0 magnitude terms, and logsumexp integration. Updated BaseTransientLikelihoodFD frequency-consistency check and likelihood_presets registry to include the new class.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • thomasckng
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add phase+distance marginalized likelihood' clearly and concisely describes the main change: introducing a new likelihood class that implements combined phase and distance marginalization.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dist_marg

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/jimgw/core/single_event/likelihood.py`:
- Line 537: The method _likelihood currently has an unused parameter named data
which triggers Ruff ARG002; rename the parameter to _data in the _likelihood
method signature and update any internal references (if any) to use _data so the
linter recognizes the parameter as intentionally unused — adjust the definition
of def _likelihood(self, params: dict[str, Float], data: dict) -> Float to use
_data instead and run tests/lint to confirm the warning is resolved.
- Around line 529-535: The evaluate method currently applies
self.fixed_parameters then unconditionally sets params["phase_c"] = 0.0 which
silently ignores any user-supplied phase_c; modify evaluate (method evaluate,
symbol fixed_parameters, and call to _likelihood) to detect if "phase_c" is
present in self.fixed_parameters and raise a clear ValueError (fail fast)
explaining that phase_c cannot be fixed when phase is marginalised, before
overwriting params["phase_c"]; keep the remaining assignments (d_L,
trigger_time, gmst) and then call self._likelihood(params, data).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae74fe2 and 1e184ef.

📒 Files selected for processing (1)
  • src/jimgw/core/single_event/likelihood.py

Comment on lines +529 to +535
def evaluate(self, params: dict[str, Float], data: dict) -> Float:
params.update(self.fixed_parameters)
params["d_L"] = self.ref_dist
params["phase_c"] = 0.0
params["trigger_time"] = self.trigger_time
params["gmst"] = self.gmst
return self._likelihood(params, data)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Reject fixed phase_c when phase is marginalised.

Line 530 applies fixed_parameters, but Line 532 always overrides phase_c = 0.0. If phase_c is supplied in fixed parameters, it is silently ignored. Please fail fast to avoid hidden misconfiguration.

🔧 Proposed fix
 def evaluate(self, params: dict[str, Float], data: dict) -> Float:
+    if "phase_c" in self.fixed_parameters:
+        raise ValueError("Cannot have phase_c fixed while marginalising over phase_c")
     params.update(self.fixed_parameters)
     params["d_L"] = self.ref_dist
     params["phase_c"] = 0.0
     params["trigger_time"] = self.trigger_time
     params["gmst"] = self.gmst
     return self._likelihood(params, data)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def evaluate(self, params: dict[str, Float], data: dict) -> Float:
params.update(self.fixed_parameters)
params["d_L"] = self.ref_dist
params["phase_c"] = 0.0
params["trigger_time"] = self.trigger_time
params["gmst"] = self.gmst
return self._likelihood(params, data)
def evaluate(self, params: dict[str, Float], data: dict) -> Float:
if "phase_c" in self.fixed_parameters:
raise ValueError("Cannot have phase_c fixed while marginalising over phase_c")
params.update(self.fixed_parameters)
params["d_L"] = self.ref_dist
params["phase_c"] = 0.0
params["trigger_time"] = self.trigger_time
params["gmst"] = self.gmst
return self._likelihood(params, data)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/jimgw/core/single_event/likelihood.py` around lines 529 - 535, The
evaluate method currently applies self.fixed_parameters then unconditionally
sets params["phase_c"] = 0.0 which silently ignores any user-supplied phase_c;
modify evaluate (method evaluate, symbol fixed_parameters, and call to
_likelihood) to detect if "phase_c" is present in self.fixed_parameters and
raise a clear ValueError (fail fast) explaining that phase_c cannot be fixed
when phase is marginalised, before overwriting params["phase_c"]; keep the
remaining assignments (d_L, trigger_time, gmst) and then call
self._likelihood(params, data).

params["gmst"] = self.gmst
return self._likelihood(params, data)

def _likelihood(self, params: dict[str, Float], data: dict) -> Float:
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Address Ruff ARG002 for unused _likelihood argument.

data is unused in this override; rename it to _data to make intent explicit and clear the warning.

🧹 Proposed fix
-    def _likelihood(self, params: dict[str, Float], data: dict) -> Float:
+    def _likelihood(self, params: dict[str, Float], _data: dict) -> Float:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _likelihood(self, params: dict[str, Float], data: dict) -> Float:
def _likelihood(self, params: dict[str, Float], _data: dict) -> Float:
🧰 Tools
🪛 Ruff (0.15.2)

[warning] 537-537: Unused method argument: data

(ARG002)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/jimgw/core/single_event/likelihood.py` at line 537, The method
_likelihood currently has an unused parameter named data which triggers Ruff
ARG002; rename the parameter to _data in the _likelihood method signature and
update any internal references (if any) to use _data so the linter recognizes
the parameter as intentionally unused — adjust the definition of def
_likelihood(self, params: dict[str, Float], data: dict) -> Float to use _data
instead and run tests/lint to confirm the warning is resolved.

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