Skip to content

Optimize and harden clusterless position decoding - #3

Open
CommanderPho wants to merge 1 commit into
developfrom
optimize-clusterless-position-decoding-10468796758105246808
Open

Optimize and harden clusterless position decoding#3
CommanderPho wants to merge 1 commit into
developfrom
optimize-clusterless-position-decoding-10468796758105246808

Conversation

@CommanderPho

@CommanderPho CommanderPho commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Optimize performance and robustness of _perform_clusterless_position_decoding_computation


PR created automatically by Jules for task 10468796758105246808 started by @CommanderPho

Summary by Sourcery

Optimize clusterless position decoding by caching multiunit computations and hardening decoder construction against missing or invalid positional data.

Bug Fixes:

  • Skip clusterless decoder construction when positional data or time columns are missing or empty to avoid runtime failures.

Enhancements:

  • Cache multiunit data derived from the session over shared time ranges to avoid repeated expensive computations when building decoders.
  • Add logging around clusterless decoder construction to surface issues and safely handle exceptions by returning no decoder instead of failing the computation.
  • Guard 1D and 2D clusterless decoder construction when corresponding place fields are absent, ensuring computed_data entries are explicitly set to None.

- Introduced a cache in `_perform_clusterless_position_decoding_computation` to prevent computing multiunits multiple times for the same time bounds, significantly speeding up the pipeline when running both 1D and 2D decoders.
- Removed an unnecessary dataframe copy operation before creating multiunits.
- Added safety checks to verify validity of `pos_df` and time data before decoding.
- Added defensive `try-except` blocks around decoder initialization to ensure errors won't crash the entire computation pipeline.

Co-authored-by: CommanderPho <962210+CommanderPho@users.noreply.github.com>
@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@sourcery-ai

sourcery-ai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds robustness checks, logging, and multiunit caching to clusterless position decoding, ensuring decoders are only built when valid positional data exist and reusing expensive multiunit computations across 1D/2D placefield decoders.

Sequence diagram for clusterless position decoding with validation and multiunit caching

sequenceDiagram
    participant ComputationResult
    participant perform_clusterless_position_decoding as _perform_clusterless_position_decoding_computation
    participant build_decoder_for_pf as _build_decoder_for_pf
    participant MultiunitCache as _multiunits_cache
    participant Session as sess
    participant Decoder as ClusterlessRTCPositionDecoder
    participant Logging as logging

    ComputationResult->>perform_clusterless_position_decoding: call
    perform_clusterless_position_decoding->>build_decoder_for_pf: _build_decoder_for_pf(pf1D)
    perform_clusterless_position_decoding->>build_decoder_for_pf: _build_decoder_for_pf(pf2D)

    rect rgb(230,230,250)
        build_decoder_for_pf-->>build_decoder_for_pf: [pf is None or pos_df invalid]
        build_decoder_for_pf->>Logging: logging.warning
        build_decoder_for_pf-->>perform_clusterless_position_decoding: return None
    end

    rect rgb(220,255,220)
        build_decoder_for_pf-->>build_decoder_for_pf: [pos_df has valid time column]
        build_decoder_for_pf-->>build_decoder_for_pf: compute t_start, t_end
        alt multiunits is not None
            build_decoder_for_pf->>Session: build_multiunits_from_array(multiunits, rtc_time)
        else multiunits is None
            alt cache hit
                build_decoder_for_pf->>MultiunitCache: get (t_start, t_end)
            else cache miss
                build_decoder_for_pf->>Session: build_multiunits_from_session(sess, sampling_frequency_hz, t_start, t_end, spikes_df)
                build_decoder_for_pf->>MultiunitCache: set (t_start, t_end)
            end
        end
        build_decoder_for_pf->>Decoder: ClusterlessRTCPositionDecoder(...)
        build_decoder_for_pf->>Decoder: compute_all()
        build_decoder_for_pf-->>perform_clusterless_position_decoding: return Decoder
    end

    rect rgb(255,230,230)
        build_decoder_for_pf-->>build_decoder_for_pf: [exception]
        build_decoder_for_pf->>Logging: logging.error
        build_decoder_for_pf-->>perform_clusterless_position_decoding: return None
    end
Loading

File-Level Changes

Change Details Files
Harden the clusterless decoder construction with validation and error handling so decoders are only built when valid positional data and time vectors are available.
  • Return early from decoder construction when the placefield or its filtered position DataFrame is missing or empty.
  • Select the time source from either t or t_seconds column and skip decoding if neither exists or if the resulting time array is empty.
  • Wrap decoder construction in a try/except block and log errors instead of raising, returning None on failure.
  • Set computed_data entries for 1D/2D clusterless decoders to None when corresponding placefields are absent.
src/pyphoplacecellanalysis/General/Pipeline/Stages/ComputationFunctions/DefaultComputationFunctions.py
Optimize performance by caching multiunit builds for identical time ranges and removing unnecessary DataFrame copies.
  • Introduce an in-function _multiunits_cache keyed by (t_start, t_end) to reuse build_multiunits_from_session results between decoders.
  • Avoid copying pf.filtered_spikes_df when passing it into build_multiunits_from_session.
  • Maintain support for externally supplied multiunits/rtc_time while using cache only for session-derived multiunits.
src/pyphoplacecellanalysis/General/Pipeline/Stages/ComputationFunctions/DefaultComputationFunctions.py
Add logging to improve observability of decoder construction and failure modes.
  • Import the logging module within the function for local logging usage.
  • Log warnings when positional data or time information are missing or empty, and when decoders are skipped.
  • Log errors on exceptions during decoder construction with the exception message.
src/pyphoplacecellanalysis/General/Pipeline/Stages/ComputationFunctions/DefaultComputationFunctions.py

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 2 issues, and left some high level feedback:

  • The _multiunits_cache currently keys on raw t_start/t_end floats, which may differ due to floating point precision; consider normalizing or discretizing these values (or keying by a more stable identifier like the underlying time index range) to avoid ineffective caching.
  • The broad except Exception as e in _build_decoder_for_pf silently converts all decoder build failures into None; consider either narrowing the exception types or re-raising after logging to avoid masking unexpected errors.
  • Instead of importing logging inside _perform_clusterless_position_decoding_computation, consider using a module-level logger (e.g., logger = logging.getLogger(__name__)) so that log messages can be more easily controlled and are consistent with typical logging patterns.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `_multiunits_cache` currently keys on raw `t_start`/`t_end` floats, which may differ due to floating point precision; consider normalizing or discretizing these values (or keying by a more stable identifier like the underlying time index range) to avoid ineffective caching.
- The broad `except Exception as e` in `_build_decoder_for_pf` silently converts all decoder build failures into `None`; consider either narrowing the exception types or re-raising after logging to avoid masking unexpected errors.
- Instead of importing `logging` inside `_perform_clusterless_position_decoding_computation`, consider using a module-level logger (e.g., `logger = logging.getLogger(__name__)`) so that log messages can be more easily controlled and are consistent with typical logging patterns.

## Individual Comments

### Comment 1
<location path="src/pyphoplacecellanalysis/General/Pipeline/Stages/ComputationFunctions/DefaultComputationFunctions.py" line_range="128" />
<code_context>
+                    if cache_key in _multiunits_cache:
+                        pf_multiunits, pf_rtc_time = _multiunits_cache[cache_key]
+                    else:
+                        pf_multiunits, pf_rtc_time = build_multiunits_from_session(sess, clusterless_params.clusterless_sampling_frequency_hz, t_start, t_end, spikes_df=pf.filtered_spikes_df)
+                        _multiunits_cache[cache_key] = (pf_multiunits, pf_rtc_time)
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Dropping the `.copy()` on `pf.filtered_spikes_df` may introduce unexpected mutation of shared data.

This used to pass `pf.filtered_spikes_df.copy()`, but now passes the original DataFrame. If `build_multiunits_from_session` mutates `spikes_df`, that will also mutate `filtered_spikes_df` and any other code using it. Please either restore the `.copy()` or confirm that `build_multiunits_from_session` treats `spikes_df` as read-only.
</issue_to_address>

### Comment 2
<location path="src/pyphoplacecellanalysis/General/Pipeline/Stages/ComputationFunctions/DefaultComputationFunctions.py" line_range="134-136" />
<code_context>
+                decoder = ClusterlessRTCPositionDecoder(pf=pf, sampling_frequency_hz=clusterless_params.clusterless_sampling_frequency_hz, multiunits=pf_multiunits, rtc_time=pf_rtc_time, clusterless_params=clusterless_params, setup_on_init=True, post_load_on_init=False, debug_print=False)
+                decoder.compute_all()
+                return decoder
+            except Exception as e:
+                logging.error(f"Error building clusterless decoder: {e}")
+                return None
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Catching all exceptions and only logging the message may make debugging decoder failures harder.

The broad `except Exception` likely keeps the pipeline running, but the current `logging.error(f"...{e}")` drops the traceback and context needed to debug failures. Prefer `logging.exception("Error building clusterless decoder")` so the full stack trace is logged, and consider selectively re-raising or handling specific exception types for critical failures.

```suggestion
            except Exception:
                logging.exception("Error building clusterless decoder")
                return None
```
</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.

if cache_key in _multiunits_cache:
pf_multiunits, pf_rtc_time = _multiunits_cache[cache_key]
else:
pf_multiunits, pf_rtc_time = build_multiunits_from_session(sess, clusterless_params.clusterless_sampling_frequency_hz, t_start, t_end, spikes_df=pf.filtered_spikes_df)

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): Dropping the .copy() on pf.filtered_spikes_df may introduce unexpected mutation of shared data.

This used to pass pf.filtered_spikes_df.copy(), but now passes the original DataFrame. If build_multiunits_from_session mutates spikes_df, that will also mutate filtered_spikes_df and any other code using it. Please either restore the .copy() or confirm that build_multiunits_from_session treats spikes_df as read-only.

Comment on lines +134 to +136
except Exception as e:
logging.error(f"Error building clusterless decoder: {e}")
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): Catching all exceptions and only logging the message may make debugging decoder failures harder.

The broad except Exception likely keeps the pipeline running, but the current logging.error(f"...{e}") drops the traceback and context needed to debug failures. Prefer logging.exception("Error building clusterless decoder") so the full stack trace is logged, and consider selectively re-raising or handling specific exception types for critical failures.

Suggested change
except Exception as e:
logging.error(f"Error building clusterless decoder: {e}")
return None
except Exception:
logging.exception("Error building clusterless decoder")
return None

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