Repository: wcxve/elisa
Pull request #234: feat: storage of `emcee` sampler state
Author: login: wcxve
Created: 2025-05-17T16:56:25Z | Merged: 2025-05-17T17:16:07Z

## Summary by Sourcery

Enable storing, returning, and reusing emcee sampler states to support seamless warm-up resumption and refitting

New Features:
- Add support for passing initial emcee sampler states to resume sampling
- Return final sampler State objects from the emcee runner
- Introduce a `post_warmup_state` argument in `fit.emcee` to resume sampling from a previous state

Enhancements:
- Refactor `run` to handle `State` objects, validate provided states, and disable warmup when reusing states
- Adjust worker function to accept and return `State` instances instead of separate initialization and RNG
- Update progress bar management to initialize after warmup computation

Tests:
- Add test to verify refitting with the returned `sampler_state`


Diff:
diff --git a/src/elisa/infer/fit.py b/src/elisa/infer/fit.py
index 472ed4ff..9a82c59e 100644
--- a/src/elisa/infer/fit.py
+++ b/src/elisa/infer/fit.py
@@ -1359,6 +1359,7 @@ def emcee(
         init: dict[str, float] | None = None,
         n_parallel: int | None = None,
         progress: bool = True,
+        post_warmup_state: Sequence | None = None,
         ignore_nan: bool = False,
         **kwargs: dict,
     ) -> PosteriorResult:
@@ -1393,6 +1394,9 @@ def emcee(
             The default is ``jax.local_device_count()``.
         progress : bool, optional
             Whether to show progress bars during sampling. The default is True.
+        post_warmup_state : sequence, optional
+            The state before the sampling phase. The sampling will start from
+            the given state if provided.
         ignore_nan : bool, optional
             Whether to transform a NaN log probability to a large negative
             number (-1e300). The default is False.
@@ -1425,13 +1429,14 @@ def emcee(
             ignore_nan=ignore_nan,
             seed=self._helper.seed['mcmc'],
         )
-        samples = sampler.run(
+        states, samples = sampler.run(
             warmup=warmup,
             steps=steps,
             chains=chains,
             thinning=thinning,
             n_parallel=n_parallel,
             progress=progress,
+            states=post_warmup_state,
             **kwargs,
         )
         ess, reff = self._get_ess(samples, n_parallel)
@@ -1439,6 +1444,7 @@ def emcee(
             samples=samples,
             ess=ess,
             reff=reff,
+            sampler_state=states,
             inference_library='emcee',
         )
 
diff --git a/src/elisa/infer/samplers/ensemble/emcee.py b/src/elisa/infer/samplers/ensemble/emcee.py
index d6abd5bd..351b0405 100644
--- a/src/elisa/infer/samplers/ensemble/emcee.py
+++ b/src/elisa/infer/samplers/ensemble/emcee.py
@@ -15,7 +15,7 @@
 from elisa.infer.samplers.util import get_model_info
 
 if TYPE_CHECKING:
-    from collections.abc import Callable
+    from collections.abc import Callable, Sequence
 
     from numpy.typing import NDArray
 
@@ -90,34 +90,43 @@ def run(
         n_parallel: int = 4,
         tune: bool = False,
         progress: bool = True,
+        states: Sequence[State] | None = None,
         **kwargs: dict,
-    ) -> dict[str, NDArray[float]]:
-        pbars = {
-            i: tqdm(
-                total=warmup + steps,
-                desc='Initializing... ',
-                position=i,
-                disable=not progress,
-            )
-            for i in range(1, n_parallel + 1)
-        }
-
+    ) -> tuple[tuple[State, ...], dict[str, NDArray[float]]]:
         ndim = self._ndim
         if chains is None:
             chains = 4 * ndim
 
-        ss = np.random.SeedSequence(self._seed)
-        seeds = ss.spawn(n_parallel)
-        mcmc_rngs = list(map(np.random.default_rng, seeds))
-
-        init = np.full((chains, ndim), self._init)
+        if states is None:
+            seeds = np.random.SeedSequence(self._seed).spawn(n_parallel + 1)
+            rng = np.random.default_rng(seeds[0])
+            init = np.full((chains, ndim), self._init)
+            jitter = rng.uniform(0.99, 1.01, size=(n_parallel, chains, ndim))
+            init = init * jitter
+            rngs = list(map(np.random.default_rng, seeds[1:]))
+            states = [
+                State(i, random_state=j)
+                for i, j in zip(init, rngs, strict=True)
+            ]
+        else:
+            states = list(states)
+            if len(states) != n_parallel:
+                raise ValueError('states number should match n_parallel')
+            if not all(isinstance(s, State) for s in states):
+                raise ValueError('states must be sequence of emcee State')
+            for s in states:
+                if s.coords.shape != (chains, ndim):
+                    raise ValueError(
+                        f"states' coords must have shape ({chains}, {ndim})"
+                    )
+                s.log_prob = None
+                s.blobs = None
+            warmup = 0
 
         log_prob_fn = self._log_prob_with_blobs
         blobs_dtype = self._blobs_dtype
 
-        def run_sampler(sampler_id, init, rng, queue):
-            jitter = rng.uniform(0.99, 1.01, size=(chains, ndim))
-            init = init * jitter
+        def run_sampler(sampler_id, state, queue):
             sampler = EnsembleSampler(
                 chains,
                 ndim,
@@ -126,7 +135,6 @@ def run_sampler(sampler_id, init, rng, queue):
                 blobs_dtype=blobs_dtype,
                 **kwargs,
             )
-            state = State(init, random_state=rng)
             queue.put((sampler_id, 'warmup'))
             for s in sampler.sample(
                 state,
@@ -138,7 +146,7 @@ def run_sampler(sampler_id, init, rng, queue):
                 state = s
                 queue.put((sampler_id, 'update'))
             queue.put((sampler_id, 'sample'))
-            for _ in sampler.sample(
+            for s in sampler.sample(
                 state,
                 iterations=steps,
                 tune=tune,
@@ -146,9 +154,10 @@ def run_sampler(sampler_id, init, rng, queue):
                 store=True,
                 progress=False,
             ):
+                state = s
                 queue.put((sampler_id, 'update'))
             queue.put((sampler_id, 'finish'))
-            return sampler.get_blobs()
+            return sampler.get_blobs(), state
 
         def progress_listener(queue, pbars, num_samplers):
             finished = 0
@@ -158,7 +167,7 @@ def progress_listener(queue, pbars, num_samplers):
                     pbars[sampler_id].update(1)
                 elif msg == 'warmup':
                     pbars[sampler_id].set_description(
-                        f'Warming up sampler {sampler_id}'
+                        f'Warm-up sampler {sampler_id}'
                     )
                 elif msg == 'sample':
                     pbars[sampler_id].set_description(
@@ -177,6 +186,16 @@ def progress_listener(queue, pbars, num_samplers):
         else:
             old_method = ''
 
+        pbars = {
+            i: tqdm(
+                total=warmup + steps,
+                desc='Initializing... ',
+                position=i,
+                disable=not progress,
+            )
+            for i in range(1, n_parallel + 1)
+        }
+
         queue = mp.Manager().Queue()
         listener_thread = threading.Thread(
             target=progress_listener, args=(queue, pbars, n_parallel)
@@ -188,7 +207,7 @@ def progress_listener(queue, pbars, num_samplers):
             for i in range(n_parallel):
                 r = pool.apply_async(
                     run_sampler,
-                    args=(i + 1, init, mcmc_rngs[i], queue),
+                    args=(i + 1, states[i], queue),
                 )
                 results.append(r)
             results = [r.get() for r in results]
@@ -198,10 +217,16 @@ def progress_listener(queue, pbars, num_samplers):
         if old_method:
             mp.set_start_method(old_method, force=True)
 
-        # reshape results from (n_step, n_walker) to (n_walker, n_step)
-        results = list(map(np.transpose, results))
+        samples = [r[0] for r in results]
+        states = tuple(r[1] for r in results)
+        for s in states:
+            s.log_prob = None
+            s.blobs = None
+
+        # reshape samples from (n_step, n_walker) to (n_walker, n_step)
+        samples = list(map(np.transpose, samples))
         # merge chains from the same sampler into a single one
-        results = list(map(np.concatenate, results))
-        # stack results of different samplers
-        results = np.vstack(results)
-        return {i: results[i] for i in results.dtype.names}
+        samples = list(map(np.concatenate, samples))
+        # stack samples of different samplers
+        samples = np.vstack(samples)
+        return states, {i: samples[i] for i in samples.dtype.names}
diff --git a/tests/test_fit.py b/tests/test_fit.py
index da4825e8..d43cceed 100644
--- a/tests/test_fit.py
+++ b/tests/test_fit.py
@@ -81,8 +81,14 @@ def test_trivial_bayes_fit(simulation, method, options):
         model['PowerLaw']['alpha'].default = 0.0
         model['PowerLaw']['K'].default = 10.0
 
-    # Get Bayesian fit result, i.e. posterior
-    result = getattr(BayesFit(data, model, seed=100), method)(**options)
+    fit_fn = getattr(BayesFit(data, model, seed=100), method)
+
+    # Bayesian fit result, i.e. posterior
+    result = fit_fn(**options)
+
+    # test refit with given post_warmup_state
+    if result.sampler_state is not None:
+        fit_fn(steps=100, post_warmup_state=result.sampler_state, **options)
 
     # check convergence
     assert all(i < 1.01 for i in result.rhat.values() if not np.isnan(i))


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR adds support for storing and reusing the emcee sampler’s state after warmup, updates the sampler API accordingly, and adds a test for refitting with a saved state.

- Introduce a `states` parameter in `EmceeSampler.run` to accept pre-warmup states and return both samples and post-warmup states.
- Extend the top‐level `fit.emcee` and `BayesFit.emcee` methods to accept a `post_warmup_state` argument and pass it through.
- Update `tests/test_fit.py` to call the fit function twice and exercise the new `post_warmup_state` logic.

### Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

| File                                            | Description                                                                         |
| ----------------------------------------------- | ----------------------------------------------------------------------------------- |
| tests/test_fit.py                               | Refactor test to extract `fit_fn`, run with and without `post_warmup_state`.        |
| src/elisa/infer/samplers/ensemble/emcee.py      | Add `states` parameter handling, validate shapes/types, return new sampler states.  |
| src/elisa/infer/fit.py                          | Add `post_warmup_state` parameter to `emcee` signature and propagate to sampler.    |


<details>
<summary>Comments suppressed due to low confidence (3)</summary>

**tests/test_fit.py:90**
* The test exercises `post_warmup_state` by calling `fit_fn` again, but does not assert that the state was actually applied or that subsequent results differ as expected. Consider adding assertions to verify correct reuse of the provided sampler state.
```
    if result.sampler_state is not None:
```
**src/elisa/infer/samplers/ensemble/emcee.py:113**
* [nitpick] The error message could be clearer and more grammatically correct. For example: `'Number of states must match n_parallel'`.
```
                raise ValueError('states number should match n_parallel')
```
**src/elisa/infer/samplers/ensemble/emcee.py:93**
* The annotation `Sequence` is only imported under `TYPE_CHECKING` and won’t be available at runtime, which can trigger a NameError when Python evaluates annotations. Consider importing `Sequence` unconditionally or enabling postponed annotations with `from __future__ import annotations`.
```
        states: Sequence[State] | None = None,
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-17T16:57:35Z
user: login: sourcery-ai[bot]
body: Hey @wcxve - I've reviewed your changes and they look great!

<details>
<summary>Here's what I looked at during the review</summary>

- 🟡 **General issues**: 2 issues found
- 🟡 **Testing**: 1 issue found
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-17T17:01:35Z

Review comments:
diff_hunk: @@ -1359,6 +1359,7 @@ def emcee(
         init: dict[str, float] | None = None,
         n_parallel: int | None = None,
         progress: bool = True,
path: src/elisa/infer/fit.py
user: login: Copilot
body: Inserting `post_warmup_state` before `ignore_nan` shifts the positional argument order and may break backward compatibility for callers using positional invocation. Consider making it keyword-only (e.g., place it after a `*`) to avoid argument position changes.
```suggestion
        progress: bool = True,
        *,
```
created_at: 2025-05-17T16:57:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -90,34 +90,42 @@
         n_parallel: int = 4,
         tune: bool = False,
         progress: bool = True,
+        states: Sequence[State] | None = None,
         **kwargs: dict,
-    ) -> dict[str, NDArray[float]]:
-        pbars = {
-            i: tqdm(
-                total=warmup + steps,
-                desc='Initializing... ',
-                position=i,
-                disable=not progress,
-            )
-            for i in range(1, n_parallel + 1)
-        }
-
+    ) -> tuple[tuple[State, ...], dict[str, NDArray[float]]]:
         ndim = self._ndim
         if chains is None:
             chains = 4 * ndim
 
-        ss = np.random.SeedSequence(self._seed)
-        seeds = ss.spawn(n_parallel)
-        mcmc_rngs = list(map(np.random.default_rng, seeds))
-
-        init = np.full((chains, ndim), self._init)
+        if states is None:
+            rng = np.random.default_rng(self._seed)
+            init = np.full((chains, ndim), self._init)
+            jitter = rng.uniform(0.99, 1.01, size=(n_parallel, chains, ndim))
+            init = init * jitter
+            rngs = rng.spawn(n_parallel)
+            states = [
+                State(i, random_state=j)
+                for i, j in zip(init, rngs, strict=True)
+            ]
+        else:
+            states = list(states)
+            if len(states) != n_parallel:
+                raise ValueError('states number should match n_parallel')
+            if not all(isinstance(s, State) for s in states):
+                raise ValueError('states must be sequence of emcee State')
+            for s in states:
+                if s.coords.shape != (chains, ndim):
+                    raise ValueError(
+                        f"states' coords must have shape ({chains}, {ndim})"
+                    )
+                s.log_prob = None
+                s.blobs = None
+            warmup = 0
path: src/elisa/infer/samplers/ensemble/emcee.py
user: login: sourcery-ai[bot]
body: **suggestion (bug_risk):** Providing states silently disables warmup

Document that providing `states` forces `warmup=0`, or update the logic to honor the supplied `warmup` when resuming from a state.

Suggested implementation:

```python
            # honor supplied warmup when resuming from states (do not force warmup=0)

```

1. Edit the function’s docstring (near its top) to clarify that if `states` is provided, the user’s `warmup` argument is still applied rather than being reset to zero.  
2. Ensure any examples or parameter descriptions under “Parameters” for `states` and `warmup` reflect this updated behavior.
created_at: 2025-05-17T17:01:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1359,6 +1359,7 @@ def emcee(
         init: dict[str, float] | None = None,
         n_parallel: int | None = None,
         progress: bool = True,
+        post_warmup_state: Sequence | None = None,
path: src/elisa/infer/fit.py
user: login: sourcery-ai[bot]
body: **suggestion:** Specify the element type in Sequence for clarity

Use Sequence[State] | None instead of Sequence | None for better type safety, and add the Sequence import.

Suggested implementation:

```python
        post_warmup_state: Sequence[State] | None = None,

```

1. At the top of `src/elisa/infer/fit.py`, among your other imports, add:

   ```python
   from typing import Sequence
   ```

2. Make sure `State` is imported or defined in this module’s namespace so that `Sequence[State]` resolves correctly.
created_at: 2025-05-17T17:01:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,8 +81,14 @@ def test_trivial_bayes_fit(simulation, method, options):
         model['PowerLaw']['alpha'].default = 0.0
         model['PowerLaw']['K'].default = 10.0
 
-    # Get Bayesian fit result, i.e. posterior
-    result = getattr(BayesFit(data, model, seed=100), method)(**options)
+    fit_fn = getattr(BayesFit(data, model, seed=100), method)
+
+    # Bayesian fit result, i.e. posterior
+    result = fit_fn(**options)
+
+    # test refit with given post_warmup_state
path: tests/test_fit.py
user: login: sourcery-ai[bot]
body: **suggestion (testing):** Add unit tests for `EmceeSampler.run` input validation regarding `states`.

Add unit tests triggering ValueErrors in EmceeSampler.run for mismatched states length, incorrect State types, and invalid coords shapes to ensure robust validation and faster feedback.
created_at: 2025-05-17T17:01:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,8 +81,14 @@
         model['PowerLaw']['alpha'].default = 0.0
         model['PowerLaw']['K'].default = 10.0
 
-    # Get Bayesian fit result, i.e. posterior
-    result = getattr(BayesFit(data, model, seed=100), method)(**options)
+    fit_fn = getattr(BayesFit(data, model, seed=100), method)
+
+    # Bayesian fit result, i.e. posterior
+    result = fit_fn(**options)
+
+    # test refit with given post_warmup_state
+    if result.sampler_state is not None:
+        fit_fn(steps=100, post_warmup_state=result.sampler_state, **options)
path: tests/test_fit.py
user: login: sourcery-ai[bot]
body: **issue (code-quality):** Avoid conditionals in tests. ([`no-conditionals-in-tests`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/no-conditionals-in-tests))

<details><summary>Explanation</summary>Avoid complex code, like conditionals, in test functions.

Google's software engineering guidelines says:
"Clear tests are trivially correct upon inspection"
To reach that avoid complex code in tests:
* loops
* conditionals

Some ways to fix this:

* Use parametrized tests to get rid of the loop.
* Move the complex logic into helpers.
* Move the complex part into pytest fixtures.

> Complexity is most often introduced in the form of logic. Logic is defined via the imperative parts of programming languages such as operators, loops, and conditionals. When a piece of code contains logic, you need to do a bit of mental computation to determine its result instead of just reading it off of the screen. It doesn't take much logic to make a test more difficult to reason about.

Software Engineering at Google / [Don't Put Logic in Tests](https://abseil.io/resources/swe-book/html/ch12.html#donapostrophet_put_logic_in_tests)
</details>
created_at: 2025-05-17T17:01:35Z
author_association: CONTRIBUTOR

PR comments:
user: login: sourcery-ai[bot]
created_at: 2025-05-17T16:56:29Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

## Reviewer's Guide

This PR overhauls the emcee sampler to allow capturing and reusing sampler states by extending the run pipeline with State objects, refactoring internal execution and progress tracking, and exposing a resume interface in the fit API along with corresponding tests.

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Enhanced run() method to accept and return emcee State objects | <ul><li>Added optional states parameter and changed return type to (states, samples)</li><li>Generated initial states with jitter when none provided using SeedSequence.spawn</li><li>Validated provided states count, type, and coords shape, resetting log_prob and blobs</li><li>Reshaped and stacked samples after run and cleared state log_prob/blobs before return</li></ul> | `src/elisa/infer/samplers/ensemble/emcee.py` |
| Refactored sampler execution and progress handling | <ul><li>Modified run_sampler to take a State object, remove local RNG, and return both blobs and final State</li><li>Adjusted pool.apply_async calls to pass State instances instead of init and RNG</li><li>Relocated and simplified progress bar setup and fixed description messages</li></ul> | `src/elisa/infer/samplers/ensemble/emcee.py` |
| Integrated state resume in fit API and tests | <ul><li>Added post_warmup_state argument to fit.emcee and forwarded it to sampler.run</li><li>Unpacked returned states, stored them in PosteriorResult.sampler_state</li><li>Extended test to perform a refit using the returned sampler_state</li></ul> | `src/elisa/infer/fit.py`<br/>`tests/test_fit.py` |

---

<details>
<summary>Tips and commands</summary>

#### 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](https://app.sourcery.ai) 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

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-05-17T17:01:25Z
body: ## [Codecov](https://app.codecov.io/gh/wcxve/elisa/pull/234?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=W.C.+Xue) Report
:x: Patch coverage is `82.35294%` with `6 lines` in your changes missing coverage. Please review.
:white_check_mark: Project coverage is 76.92%. Comparing base ([`e9f7c05`](https://app.codecov.io/gh/wcxve/elisa/commit/e9f7c050478a0dbf605808babe206e2bff87451c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=W.C.+Xue)) to head ([`4bf0e3c`](https://app.codecov.io/gh/wcxve/elisa/commit/4bf0e3c698e61edc28a42f7ebc11c0c813840590?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=W.C.+Xue)).
:warning: Report is 34 commits behind head on main.
:white_check_mark: All tests successful. No failed tests found.

| [Files with missing lines](https://app.codecov.io/gh/wcxve/elisa/pull/234?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=W.C.+Xue) | Patch % | Lines |
|---|---|---|
| [src/elisa/infer/samplers/ensemble/emcee.py](https://app.codecov.io/gh/wcxve/elisa/pull/234?src=pr&el=tree&filepath=src%2Felisa%2Finfer%2Fsamplers%2Fensemble%2Femcee.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=W.C.+Xue#diff-c3JjL2VsaXNhL2luZmVyL3NhbXBsZXJzL2Vuc2VtYmxlL2VtY2VlLnB5) | 81.81% | [6 Missing :warning: ](https://app.codecov.io/gh/wcxve/elisa/pull/234?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=W.C.+Xue) |

<details><summary>Additional details and impacted files</summary>



```diff
@@            Coverage Diff             @@
##             main     #234      +/-   ##
==========================================
+ Coverage   76.72%   76.92%   +0.20%     
==========================================
  Files          38       38              
  Lines        8495     8513      +18     
==========================================
+ Hits         6518     6549      +31     
+ Misses       1977     1964      -13     
```
</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/wcxve/elisa/pull/234?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=W.C.+Xue).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=W.C.+Xue).
author_association: NONE