Skip to content

feature: add core data structures and search algorithms#3

Closed
dhalmazna wants to merge 18 commits intomasterfrom
feature/ciao-core-algorithm
Closed

feature: add core data structures and search algorithms#3
dhalmazna wants to merge 18 commits intomasterfrom
feature/ciao-core-algorithm

Conversation

@dhalmazna
Copy link
Collaborator

@dhalmazna dhalmazna commented Feb 24, 2026

**DEPENDS ON PR #2 **
Please review and merge PR #2 first. This PR is stacked on top of it, so it temporarily shows commits from PR #2. Once PR #2 is merged into master, those commits will automatically disappear from this diff.

What was changed:

  • Migrated foundational data structures (structures/ - nodes, bitmask graphs).
  • Added core helper functions (utils/ - segmentation, surrogate calculations).
  • Implemented the main search logic (algorithm/ - MCTS, MCGS, lookahead). The logic is described here XAI-A-21.
  • Resolved initial ruff and mypy linting issues for the newly added modules.

Why:
To establish the underlying mathematical and algorithmic backbone of the CIAO package. Moving these isolated modules first allows us to keep the codebase clean before wrapping them into the final user-facing CIAOExplainer interface in the next PR.

Related Task:
XAI-29

Summary by CodeRabbit

  • New Features

    • Added multiple hyperpixel search algorithms (MCTS/MCGS/potential/lookahead), bitmask graph utilities, segmentation pipelines (hex/square), surrogate-dataset tooling, masking/replacement options, a ModelPredictor utility, and ImageNet class labels.
  • Documentation

    • Replaced README with a full CIAO overview, Quick Start, tutorials, method details, search/segmentation options, and project structure.
  • Chores

    • Updated project metadata, dependencies, authorship, and .gitignore.

Copilot AI review requested due to automatic review settings February 24, 2026 08:54
@coderabbitai
Copy link

coderabbitai bot commented Feb 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds CIAO: an XAI library for image classification. Introduces four hyperpixel search algorithms (lookahead, MCTS, MCGS, potential), segmentation and bitmask utilities, node structures, ModelPredictor and batching utilities, a 1000-entry ImageNet labels resource, and README + project metadata updates.

Changes

Cohort / File(s) Summary
Documentation & Project Config
README.md, pyproject.toml, .gitignore
Replaces template README with detailed CIAO guide; updates project name/description/authors and dependencies (torch, torchvision, numpy, scikit-image, networkx, hydra-core, mlflow, etc.); adds .pre-commit-config.yaml to .gitignore.
Package Inits / API surface
ciao/__init__.py, ciao/algorithm/__init__.py, ciao/structures/__init__.py, ciao/utils/__init__.py
Adds module docstrings, defines/updates __all__, and re-exports key symbols (ModelPredictor, create_segmentation, MCGSNode, MCTSNode).
Search Algorithms
ciao/algorithm/...
ciao/algorithm/lookahead_bitset.py, ciao/algorithm/mcts.py, ciao/algorithm/mcgs.py, ciao/algorithm/potential.py
Adds four hyperpixel construction strategies: greedy lookahead (bitset), MCTS (standard & RAVE), MCGS (DAG-aware with RAVE), and potential-guided SMC; each provides single- and multi-hyperpixel builders and search primitives.
Model & Search Utilities
ciao/utils/calculations.py, ciao/utils/search_utils.py
Introduces ModelPredictor, batched prediction/logit helpers, replacement-image strategies (mean_color, blur, interlace, solid_color), hyperpixel delta computations, surrogate dataset builder, and evaluate_masks/is_terminal helpers.
Segmentation
ciao/utils/segmentation.py
Adds hexagonal and square segmentation pipelines, pixel-to-hex utilities, adjacency graph builders, fast adjacency lists, adjacency bitmask builder, and create_segmentation dispatcher.
Bitmask Graph & Node Structures
ciao/structures/bitmask_graph.py, ciao/structures/nodes.py
Adds bitmask utilities (mask iteration, frontier, sampling connected supersets, weighted frontier selection) and node classes (MCTSNode, MCGSNode) with visit/value/RAVE/edge stats and helpers.
Static Resources
ciao/imagenet_classes.txt
Adds ImageNet class labels (1000 entries) as a static resource.
Misc (small edits)
ciao/__init__.py (docstring)
Adds module-level CIAO docstring and a commented potential export line for CIAOExplainer.

Sequence Diagram

sequenceDiagram
    participant User
    participant Segmentation as "Segmentation\n(create_segmentation)"
    participant Bitmask as "BitmaskGraph\n(get_frontier / sample)"
    participant Search as "SearchAlgo\n(MCTS/MCGS/lookahead/potential)"
    participant Predictor as "ModelPredictor"
    participant Results

    User->>Segmentation: create_segmentation(image)
    Segmentation-->>User: segments, adjacency (adj_masks)
    User->>Search: build_all_hyperpixels(segments, model, target_class)
    loop per seed
        Search->>Bitmask: compute frontier / propose candidate masks
        Bitmask-->>Search: candidate masks
        Search->>Predictor: evaluate_masks(batch of masks)
        Predictor-->>Search: delta scores / logits
        Search->>Search: select/commit best prefix (update used_mask)
    end
    Search-->>Results: hyperpixels list (masks, scores, stats)
    Results-->>User: explanations / visualizations
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 I nudge the hexes, tally every dot,

I chase the masks where tiny highlights trot,
Four curious searches bend the light to show,
I count the scores and stitch the answers slow,
CIAO blooms — a soft, explainable glow.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding core data structures (nodes, bitmask graphs) and search algorithms (MCTS, MCGS, lookahead) to the CIAO package.
Docstring Coverage ✅ Passed Docstring coverage is 86.11% which is sufficient. The required threshold is 80.00%.

✏️ 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 feature/ciao-core-algorithm

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.

@gemini-code-assist
Copy link

Summary of Changes

Hello @dhalmazna, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request establishes the fundamental mathematical and algorithmic infrastructure for the CIAO explainable AI package. It introduces robust data structures for efficient graph manipulation and node management, alongside a suite of advanced search algorithms designed to identify influential image regions. These core components are integrated with comprehensive utility functions for model interaction, image processing, and segmentation, laying the groundwork for the user-facing CIAOExplainer interface.

Highlights

  • Core Data Structures: Introduced foundational data structures for graph representation and node management, including bitmask-based graph operations and dedicated MCTS/MCGS node classes.
  • Search Algorithms: Implemented several core search algorithms for hyperpixel building: greedy lookahead, Monte Carlo Tree Search (MCTS) with RAVE, Monte Carlo Graph Search (MCGS) with RAVE, and a Sequential Monte Carlo method using potential-based selection.
  • Utility Functions: Added utility functions for model prediction handling, various image replacement strategies (mean color, interlacing, blur, solid color), surrogate dataset creation, and image segmentation (square and hexagonal grids).
  • Linting and Setup: Resolved initial ruff and mypy linting issues for the newly integrated modules and established initial package structure.
Changelog
  • ciao/init.py
    • Initialized the ciao package and imported CIAOExplainer.
  • ciao/algorithm/init.py
    • Initialized the ciao.algorithm package.
  • ciao/algorithm/lookahead_bitset.py
    • Implemented a greedy lookahead algorithm for hyperpixel construction using bitmask operations.
  • ciao/algorithm/mcgs.py
    • Implemented a unified Monte Carlo Graph Search (MCGS) algorithm with standard and RAVE modes, featuring eager expansion and virtual loss.
  • ciao/algorithm/mcts.py
    • Implemented a unified Monte Carlo Tree Search (MCTS) algorithm with standard and RAVE modes, including batch evaluation and terminal caching.
  • ciao/algorithm/potential.py
    • Implemented a Sequential Monte Carlo algorithm for hyperpixel building using potential-based selection.
  • ciao/imagenet_classes.txt
    • Added a comprehensive list of ImageNet class names.
  • ciao/structures/init.py
    • Initialized the ciao.structures package and exported MCGSNode and MCTSNode.
  • ciao/structures/bitmask_graph.py
    • Provided bitmask-based utilities for efficient graph operations like node manipulation and frontier calculation.
  • ciao/structures/nodes.py
    • Defined MCTSNode and MCGSNode classes to manage search tree/graph state and statistics.
  • ciao/utils/init.py
    • Initialized the ciao.utils package and exported ModelPredictor and create_segmentation.
  • ciao/utils/calculations.py
    • Developed a ModelPredictor class for model interaction, implemented various image replacement strategies, and added functions for surrogate dataset creation and hyperpixel delta calculation.
  • ciao/utils/search_utils.py
    • Provided shared utility functions is_terminal and evaluate_masks for search algorithms.
  • ciao/utils/segmentation.py
    • Implemented functions for creating square and hexagonal image segmentations and building their adjacency graphs.
Activity
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@dhalmazna dhalmazna changed the base branch from chore/project-setup to master February 24, 2026 08:56
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a significant amount of core functionality, including various search algorithms (Greedy Lookahead, MCTS, MCGS, Potential-based) and the underlying data structures and utilities to support them. The code is well-structured, leveraging bitmasks for efficiency. My review focuses on improving robustness, maintainability, and consistency. Key suggestions include replacing print statements with a proper logging framework, handling potential None returns more gracefully to prevent crashes, removing unused parameters, and ensuring all documentation is in English for consistency.

Comment on lines +428 to +451
if mode == "rave":
uct_result = select_uct_child_rave(
node, exploration_c, virtual_loss, rave_k
)
assert uct_result is not None
action, child = uct_result

# Apply virtual loss to edge
if action not in node.pending_edges:
node.pending_edges[action] = 0
node.pending_edges[action] += 1

actions_taken.append(action)
else:
uct_result = select_uct_child(node, exploration_c, virtual_loss)
assert uct_result is not None
action, child = uct_result

# Apply virtual loss to edge
if action not in node.pending_edges:
node.pending_edges[action] = 0
node.pending_edges[action] += 1

actions_taken.append(action)

Choose a reason for hiding this comment

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

high

Using assert uct_result is not None could lead to a crash with an AssertionError if select_uct_child* returns None (e.g., if a node has no children). While the surrounding logic might prevent this, it's safer to handle this case gracefully by checking for None and breaking the loop. You can also refactor the common logic out of the if/else block.

                if mode == "rave":
                    uct_result = select_uct_child_rave(
                        node, exploration_c, virtual_loss, rave_k
                    )
                else:
                    uct_result = select_uct_child(node, exploration_c, virtual_loss)

                if uct_result is None:
                    break  # No valid child to select

                action, child = uct_result

                # Apply virtual loss to edge
                if action not in node.pending_edges:
                    node.pending_edges[action] = 0
                node.pending_edges[action] += 1

                actions_taken.append(action)

Comment on lines +370 to +380
if mode == "rave":
child = select_uct_child_rave(
node, exploration_c, virtual_loss, rave_k
)
else:
child = select_uct_child(node, exploration_c, virtual_loss)

assert child is not None
child.pending += 1
node = child
path.append(node)

Choose a reason for hiding this comment

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

high

Using assert child is not None can lead to a crash if select_uct_child returns None. It's safer to handle this case gracefully, for example by checking for None and breaking the loop.

Suggested change
if mode == "rave":
child = select_uct_child_rave(
node, exploration_c, virtual_loss, rave_k
)
else:
child = select_uct_child(node, exploration_c, virtual_loss)
assert child is not None
child.pending += 1
node = child
path.append(node)
if mode == "rave":
child = select_uct_child_rave(
node, exploration_c, virtual_loss, rave_k
)
else:
child = select_uct_child(node, exploration_c, virtual_loss)
if child is None:
break # No valid child to select
child.pending += 1
node = child
path.append(node)

total_gpu_evaluations = 0

# --- MAIN MCTS LOOP ---
for _ in tqdm(range(num_iterations), desc=f"MCTS-{mode.upper()}", unit="iter"):

Choose a reason for hiding this comment

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

medium

Using tqdm directly prints to the console, which might not be desirable when this code is used as a library. Consider making the progress bar optional, for example by adding a show_progress: bool = True parameter and wrapping tqdm in an if block.


# --- MAIN MCGS LOOP ---
mode_label = f"MCGS-{mode.upper()}"
for iteration in tqdm(range(num_iterations), desc=f" {mode_label}", ncols=80):

Choose a reason for hiding this comment

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

medium

Using tqdm directly prints to the console, which might not be desirable when this code is used as a library. Consider making the progress bar optional, for example by adding a show_progress: bool = True parameter and wrapping tqdm in an if block.

total_evaluations = 0
total_samples = 0

print("\n=== Sequential Monte Carlo Set Extension ===")

Choose a reason for hiding this comment

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

medium

Using print() for logging throughout this file is not ideal for a library. It's better to use the logging module. This allows consumers of your library to control log levels and output destinations.

].item()

# Get replacement image using the specified strategy
assert predictor.replacement_image is not None

Choose a reason for hiding this comment

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

medium

The function relies on predictor.replacement_image being set before this function is called, which is enforced by an assert. This creates a temporal dependency that can make the code harder to reason about and test in isolation. Consider if this dependency can be made more explicit, for example by passing the replacement image as a parameter.

Comment on lines +373 to +374
if torch.cuda.is_available():
torch.cuda.empty_cache()

Choose a reason for hiding this comment

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

medium

Calling torch.cuda.empty_cache() can be expensive and may not be necessary. Deleting tensors (del batch_inputs, masked_logits) should be sufficient for PyTorch's garbage collector to free up memory. You might want to profile this to see if empty_cache() is providing a real benefit or just adding overhead.

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: 10

🧹 Nitpick comments (18)
ciao/utils/search_utils.py (1)

14-18: adj_masks typed as bare tuple; should be tuple[int, ...] to match get_frontier's signature.

✏️ Proposed fix
-def is_terminal(mask: int, adj_masks: tuple, used_mask: int, max_depth: int) -> bool:
+def is_terminal(mask: int, adj_masks: tuple[int, ...], used_mask: int, max_depth: int) -> bool:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ciao/utils/search_utils.py` around lines 14 - 18, The parameter adj_masks in
is_terminal is typed as a bare tuple but get_frontier expects tuple[int, ...];
update is_terminal's signature to use adj_masks: tuple[int, ...] and ensure any
callers pass the same typed structure so the types align (refer to functions
is_terminal and get_frontier to locate and update the annotation).
ciao/__init__.py (1)

7-7: Missing __all__CIAOExplainer won't be part of the package's public surface contract.

✏️ Proposed fix
 from ciao.explainer.ciao_explainer import CIAOExplainer
+
+__all__ = ["CIAOExplainer"]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ciao/__init__.py` at line 7, The package __init__ currently imports
CIAOExplainer but does not declare a public API, so add an explicit __all__ that
includes "CIAOExplainer" to make it part of the package's public surface; update
ciao/__init__.py to declare __all__ = ["CIAOExplainer"] (keeping the existing
import from ciao.explainer.ciao_explainer import CIAOExplainer) so consumers
using from ciao import * or introspection will expose CIAOExplainer.
README.md (1)

84-116: Project structure lists files that don't exist in this PR or PR #2.

Modules such as ciao/data/, ciao/explainer/, ciao/visualization/, ciao/__main__.py, and configs/ are shown in the directory tree but are not introduced until future PRs. Contributors cloning this branch will find a mismatched structure. Consider either trimming the tree to only what's present on this branch, or adding a note that the tree shows the planned final layout.

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

In `@README.md` around lines 84 - 116, The README's project tree currently lists
modules that are not present in this branch (e.g., ciao/data/, ciao/explainer/,
ciao/visualization/, ciao/__main__.py, configs/); update README.md so the
directory tree either (a) only shows files and directories actually in this PR,
or (b) keeps the full planned tree but clearly marks missing items as "planned"
or "coming in future PRs" (add a short note and/or annotate entries like
ciao/data/ and configs/ as planned). Ensure references to files such as
ciao/explainer/ciao_explainer.py, ciao/visualization/visualization.py, and
configs/ciao.yaml are consistent with the branch contents.
ciao/algorithm/mcts.py (2)

436-442: Unused indices variable from unpacking.

indices from the zip(*masks_to_evaluate) unpacking is never used. Prefix with _ to signal intent.

Proposed fix
-            indices, masks = zip(*masks_to_evaluate, strict=True)
+            _indices, masks = zip(*masks_to_evaluate, strict=True)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ciao/algorithm/mcts.py` around lines 436 - 442, The unpacking in the masks
evaluation branch creates an unused variable `indices` (in the line that
currently does `indices, masks = zip(*masks_to_evaluate, strict=True)`); update
this to `_indices, masks = zip(*masks_to_evaluate, strict=True)` (or simply `_`
instead of `_indices`) so the intent of an unused value is clear in the
`evaluate_masks` logic inside the gpu evaluation block of `mcts.py`.

559-593: Simplify segment extraction and dict iteration.

Two minor cleanups:

  1. Line 561: Use seg_id in scores instead of seg_id in scores.keys().
  2. Lines 591-593: Use the existing mask_to_ids utility instead of manually iterating range(next_id).
Proposed fix
         available_segments = [
-            seg_id for seg_id in scores.keys() if seg_id not in processed_segments
+            seg_id for seg_id in scores if seg_id not in processed_segments
         ]
-        hyperpixel_segments = [
-            seg_id for seg_id in range(next_id) if hyperpixel_mask & (1 << seg_id)
-        ]
+        hyperpixel_segments = mask_to_ids(hyperpixel_mask)

Note: mask_to_ids is already imported from ciao.structures.bitmask_graph at line 28.

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

In `@ciao/algorithm/mcts.py` around lines 559 - 593, Replace the two manual
iterations with the simpler constructs: for available_segments, iterate directly
over scores (use "seg_id for seg_id in scores if seg_id not in
processed_segments" instead of "scores.keys()"); for hyperpixel_segments, call
the existing utility mask_to_ids(hyperpixel_mask, next_id) (or
mask_to_ids(hyperpixel_mask) if that is the function signature) instead of the
manual range/bit-check loop. These changes touch the block that calls
build_hyperpixel_mcts and the variables scores, processed_segments,
hyperpixel_mask and next_id.
ciao/algorithm/potential.py (3)

500-503: Use seg_id in scores instead of seg_id in scores.keys().

Per Ruff SIM118, iterating over .keys() is redundant.

Proposed fix
         available_segments = [
-            seg_id for seg_id in scores.keys() if seg_id not in processed_segments
+            seg_id for seg_id in scores if seg_id not in processed_segments
         ]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ciao/algorithm/potential.py` around lines 500 - 503, The list comprehension
building available_segments uses the redundant .keys() call; update it to check
membership directly against the mapping by changing the comprehension to use "if
seg_id not in processed_segments and seg_id in scores" (or simply iterate over
scores and filter out processed_segments) so that scores is used directly
instead of scores.keys(); this touches the variable available_segments and the
variables scores and processed_segments in the potential computation code.

379-411: scores list may contain None if a prefix mask is somehow both in cache and in missing_indices.

scores is initialized as [None] * len(prefix_masks) and filled conditionally. While the current logic correctly partitions indices between cache hits and missing_indices, the type of scores is list[None] which later gets compared with > max_score (line 409). If any entry remained None due to a future code change, this would raise a TypeError at runtime.

Consider initializing with -float("inf") or adding a type annotation to make the intent explicit:

Proposed fix
-    scores = [None] * len(prefix_masks)
+    scores: list[float] = [-float("inf")] * len(prefix_masks)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ciao/algorithm/potential.py` around lines 379 - 411, The scores list is
initialized with None which can cause a TypeError when compared to max_score;
change initialization of scores in the selection logic to a numeric sentinel
(e.g., scores = [-float("inf")] * len(prefix_masks)) and keep setting cache hits
via scores[i] = cache[mask] and computed values from calculate_hyperpixel_deltas
(used with missing_indices) as you already do; ensure every branch that assigns
to scores (the cache hit loop and the for i, score in zip(missing_indices,
computed_scores) block) writes numeric values so no None remains before the
final max search using best_idx and max_score.

112-113: Replace print() calls with logging module.

Same pattern as lookahead_bitset.py — extensive debug print() statements throughout build_hyperpixel_using_potential, sampling_phase, and select_best_prefix. Use logging.debug()/logging.info() for controllable verbosity.

Also applies to: 121-123, 128-130, 133-133, 155-155, 167-170, 186-188, 190-190, 315-315, 415-421

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

In `@ciao/algorithm/potential.py` around lines 112 - 113, Replace the numerous
print() debug/info statements in build_hyperpixel_using_potential,
sampling_phase, and select_best_prefix with logging calls following the same
pattern used in lookahead_bitset.py: import the logging module (if not already),
replace user-facing prints with logging.info(...) and verbose/debug prints with
logging.debug(...), and keep the message text identical or more descriptive to
preserve context (e.g., the seed/target/sim counts and progress messages
referenced around Seed: {seed_idx}, Target: {desired_length}, Sims:
{num_simulations}). Make these replacements for all occurrences noted (around
lines with prints at 112-113, 121-123, 128-130, 133, 155, 167-170, 186-188, 190,
315, 415-421) so verbosity can be controlled via logger configuration. Ensure
imports and any logger = logging.getLogger(__name__) usage match
lookahead_bitset.py.
ciao/structures/nodes.py (1)

32-64: Consider typed structures for edge/RAVE stats instead of dict[str, float].

The string-keyed dicts for edge_stats and rave_stats (e.g., {"N": 0, "W": 0.0, "Q": 0.0, "max_reward": -inf}) are easy to misuse — typos in keys silently create new entries and "N" (an integer count) is typed as float. A TypedDict or dataclass would catch key errors statically and clarify types.

This is a low-risk pattern for now since init_edge centralizes creation, but worth considering as the codebase grows.

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

In `@ciao/structures/nodes.py` around lines 32 - 64, Replace the loose dicts used
for per-edge stats with a typed structure to prevent key typos and clarify
types: define a TypedDict or dataclass (e.g., EdgeStats or EdgeStatsDict)
containing fields N: int, W: float, Q: float, max_reward: float and update the
MCGSNode.edge_stats and MCGSNode.rave_stats annotations to use that type; then
refactor MCGSNode.init_edge to create and assign that typed object instead of a
string-keyed dict and adjust any code accessing keys (e.g., places that
read/write edge_stats[action]["N"] etc.) to use the typed fields or keys
consistently.
ciao/algorithm/mcgs.py (2)

322-331: RAVE backup recomputes frontier for every node in every path — potentially expensive.

backup_paths_rave calls get_frontier(node.mask, adj_masks, used_mask) for every node in every path during backpropagation. With batch_size paths of depth up to desired_length, this is O(batch_size × depth × frontier_cost) per iteration.

For moderate parameters this is likely acceptable, but consider caching the frontier on the node (e.g., node.frontier_cache) if profiling reveals this as a bottleneck.

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

In `@ciao/algorithm/mcgs.py` around lines 322 - 331, backup_paths_rave currently
recomputes the frontier for every node via get_frontier(node.mask, adj_masks,
used_mask) during backpropagation, causing repeated expensive work; to fix, add
a cached frontier on the node (e.g., node.frontier_cache) and change
backup_paths_rave to check and reuse node.frontier_cache before calling
get_frontier, updating node.frontier_cache when absent or invalid, then use that
cached bitmask for the membership check and call update_rave_stats(node, seg_id,
reward) as before; locate get_frontier, backup_paths_rave, and update_rave_stats
to implement the cache and ensure any cache invalidation logic is applied where
node.mask or adj_masks/used_mask change.

636-670: Same minor cleanup opportunities as in mcts.py.

  1. Line 638: seg_id in scores.keys()seg_id in scores
  2. Lines 668-670: Replace manual bit-checking with mask_to_ids(hyperpixel_mask) (already imported via iter_bits; mask_to_ids would need to be added to the import).
Proposed fix
         available_segments = [
-            seg_id for seg_id in scores.keys() if seg_id not in processed_segments
+            seg_id for seg_id in scores if seg_id not in processed_segments
         ]

Add mask_to_ids to the import from ciao.structures.bitmask_graph:

 from ciao.structures.bitmask_graph import (
     add_node,
     get_frontier,
     iter_bits,
+    mask_to_ids,
     sample_connected_superset,
 )

Then:

-        hyperpixel_segments = [
-            seg_id for seg_id in range(next_id) if hyperpixel_mask & (1 << seg_id)
-        ]
+        hyperpixel_segments = mask_to_ids(hyperpixel_mask)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ciao/algorithm/mcgs.py` around lines 636 - 670, Replace the small
inefficiencies in the loop: change the comprehension that builds
available_segments to use "seg_id in scores" instead of "seg_id in
scores.keys()", and replace the manual bit-checking that builds
hyperpixel_segments with a call to mask_to_ids(hyperpixel_mask); also add
mask_to_ids to the existing import from ciao.structures.bitmask_graph so
mask_to_ids is available for use in build_hyperpixel_mcgs result handling.
ciao/structures/bitmask_graph.py (2)

97-148: base_frontier parameter is unused — document deprecation or remove.

The base_frontier parameter is accepted but never used (the docstring says "unused, kept for compatibility"). If this is a deliberate compatibility shim, consider marking it as deprecated or removing it to avoid confusion for new callers.

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

In `@ciao/structures/bitmask_graph.py` around lines 97 - 148, The parameter
base_frontier in sample_connected_superset is unused; either remove it from the
function signature and update all call sites (and tests) to stop passing it, or
retain it but mark it explicitly deprecated: add a short note to the docstring
and emit a runtime warnings.warn("base_frontier is deprecated and ignored",
DeprecationWarning) at the top of sample_connected_superset so callers are
informed, and keep behavior unchanged; update any internal references to call
_pick_weighted_frontier_segment / pick_random_set_bit using the computed
frontier only and remove mentions of base_frontier from comments and docs.

151-207: Mixed RNG sources may complicate reproducibility.

_pick_weighted_frontier_segment uses np.random.choice (line 206) while other sampling in this module uses Python's random module (e.g., pick_random_set_bit at line 58). If reproducible runs are needed (e.g., for debugging or benchmarking), both RNGs need to be seeded independently. Consider standardizing on one RNG, or at least documenting the dual-source requirement for seed management.

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

In `@ciao/structures/bitmask_graph.py` around lines 151 - 207, The function
_pick_weighted_frontier_segment currently uses np.random.choice while other
samplers like pick_random_set_bit use Python's random module, which breaks
reproducibility unless both RNGs are seeded separately; fix by standardizing RNG
usage—either switch _pick_weighted_frontier_segment to accept and use a single
RNG (e.g., pass in a random.Random or a numpy.random.Generator) or replace
np.random.choice with the same RNG used by pick_random_set_bit, and update
callers to pass the chosen RNG; ensure you reference and use the same RNG
instance across pick_random_set_bit and _pick_weighted_frontier_segment (or
document dual-RNG seeding requirements if you intentionally keep both).
ciao/utils/segmentation.py (1)

88-120: build_adjacency_graph iterates pixel-by-pixel — consider using NumPy vectorized adjacency detection.

For large images, the triple nested Python loop (horizontal, vertical, diagonal) over every pixel is slow. NumPy operations like comparing shifted arrays (segments[:-1, :] != segments[1:, :]) would be significantly faster for building the adjacency set.

This is fine if segmentation is a one-time init cost, but worth noting for scalability.

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

In `@ciao/utils/segmentation.py` around lines 88 - 120, build_adjacency_graph
currently loops pixel-by-pixel; replace the triple nested loops with vectorized
NumPy adjacency detection by comparing shifted arrays (e.g., segments[:-1,:] vs
segments[1:,:], segments[:,:-1] vs segments[:,1:], and the two diagonals when
neighborhood==8) to produce pairs of differing segment ids, use np.where and
flatten/zip or np.stack+unique to get edge pairs, then call
adj_graph.add_edges_from on those pairs instead of adding edges one-at-a-time;
keep function signature build_adjacency_graph(segments, neighborhood=8) and
ensure you still add all segment_ids to adj_graph before adding edges.
ciao/algorithm/lookahead_bitset.py (1)

56-56: Replace print() calls with logging for production readiness.

There are numerous print() statements throughout this module for debug tracing. These will produce noisy output in production and cannot be easily toggled. Use Python's logging module at DEBUG or INFO level instead, so callers can control verbosity.

This applies across all functions in this file (build_hyperpixel_greedy_lookahead, build_all_hyperpixels_greedy_lookahead).

Also applies to: 73-76, 78-80, 104-106, 113-113, 128-130, 287-288, 318-320

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

In `@ciao/algorithm/lookahead_bitset.py` at line 56, Replace all raw print(...)
calls in this module with Python logging calls: import logging at the top,
create logger = logging.getLogger(__name__), and use logger.debug(...) or
logger.info(...) instead of print for the debug/tracing messages found in
build_hyperpixel_greedy_lookahead and build_all_hyperpixels_greedy_lookahead
(e.g., the "Starting greedy lookahead from seed {seed_idx}" message and the
other prints referenced around lines 73-76, 78-80, 104-106, 113, 128-130,
287-288, 318-320). Preserve the original message text and formatting but pass it
to logger.debug or logger.info so callers can control verbosity via the logging
configuration.
ciao/utils/calculations.py (3)

7-14: replacement_image is initialized as None and relied upon via assert — consider a clearer lifecycle.

ModelPredictor.replacement_image is set to None in __init__ (line 14) and must be set externally before calculate_hyperpixel_deltas is called (which asserts it's not None at line 331). This implicit contract is fragile — callers have no compile-time signal that setup is incomplete.

Consider either requiring it in the constructor, providing a setter method that validates, or raising a descriptive error instead of using assert (assertions are stripped in optimized mode with -O).

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

In `@ciao/utils/calculations.py` around lines 7 - 14, ModelPredictor currently
sets replacement_image = None in __init__ but calculate_hyperpixel_deltas
asserts it's set, which is fragile; add an explicit setter and replace the
assert with a clear runtime check. Implement a method
set_replacement_image(self, image) on ModelPredictor that validates the image
(type/shape/device) and assigns self.replacement_image, update __init__
docstring to mention the lifecycle, and change the assertion in
calculate_hyperpixel_deltas to raise a descriptive ValueError (e.g.,
"replacement_image must be set via set_replacement_image before calling
calculate_hyperpixel_deltas").

160-167: Remove commented-out experiment code.

Lines 163-165 contain a commented-out alternative implementation. This is dead code that adds confusion. Remove it or document the intent in a proper comment/ADR if it's planned work.

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

In `@ciao/utils/calculations.py` around lines 160 - 167, Remove the dead
experimental code inside the with torch.no_grad() block that calls
get_predictions and computes log-odds (the commented lines referencing
"probabilities = self.get_predictions(input_batch)" and "result =
torch.log(probabilities) - torch.log(1 - probabilities)"); simply return the
logits slice as currently done (outputs[:, target_class_idx]) or, if you intend
to preserve the experiment, move that logic and explanation into a dedicated ADR
or a clearly named helper (e.g., a new method compute_log_odds) instead of
leaving commented-out code in the body of the method.

203-211: Replace print() calls with logging in utility functions.

Same pattern as the algorithm modules — create_surrogate_dataset and calculate_scores_from_surrogate use print() for diagnostics. Use logging for consistent, controllable output.

Also applies to: 253-254, 283-284

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

In `@ciao/utils/calculations.py` around lines 203 - 211, The utility functions in
ciao/utils/calculations.py currently use print() for diagnostics (e.g., the
prints showing original_logit and class probability around the
predictor.get_class_logit_batch and predictor.get_predictions calls) — replace
these print() calls with a module logger: add "import logging" and "logger =
logging.getLogger(__name__)" at top if missing, then change prints to
logger.debug() or logger.info() as appropriate (use logger.debug for verbose
diagnostics); make the same replacements in the other occurrences mentioned
(around the blocks at the create_surrogate_dataset and
calculate_scores_from_surrogate areas, i.e., the lines you noted at 253-254 and
283-284) so all diagnostic output uses the logger consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ciao/__init__.py`:
- Around line 1-5: The module docstring's first line in ciao/__init__.py lacks a
trailing period; update the top-level docstring so its first line ends with a
period (e.g., change the first line of the triple-quoted string to "CIAO:
Contextual Importance Assessment via Obfuscation.") to satisfy Ruff D415 and
keep the rest of the docstring unchanged.
- Line 7: The top-level import of CIAOExplainer from
ciao.explainer.ciao_explainer is causing ImportError when that module isn't
present; remove or guard the direct import in __init__ and instead either
perform a lazy import where CIAOExplainer is actually used or wrap the import in
a try/except that sets CIAOExplainer = None (or a safe fallback) and optionally
logs the absence; update any code that expects CIAOExplainer to handle the
None/fallback case so package import no longer fails if
ciao.explainer.ciao_explainer is missing.

In `@ciao/algorithm/lookahead_bitset.py`:
- Around line 186-198: The fallback in lookahead_bitset.py where mask not in
candidates and first_step = _find_first_step(current_mask, new_mask) can
silently pick an arbitrary segment; update the BFS to log a warning when this
branch is hit so unexpected reachability is visible: inside the block that
computes first_step for depth > 1, call your logger (or warnings.warn) to emit a
warning that includes the current_mask, new_mask, and computed first_step and
note that this is a fallback path; keep behavior unchanged (still set
candidates[new_mask]=first_step) but ensure the warning message gives enough
context to debug (_find_first_step, current_mask, new_mask, first_step).

In `@ciao/imagenet_classes.txt`:
- Line 419: Update the two inconsistent labels by replacing the exact string
"Band Aid" with the hyphenated trademark form "Band-Aid" and replace "web site"
with the single-word modern form "website" (locate these entries by their
current label text "Band Aid" and "web site" in the imagenet_classes list and
edit them in place).

In `@ciao/utils/calculations.py`:
- Around line 78-96: The get_replacement_image function uses hardcoded 224 for
spatial dims causing shape mismatches for non-ImageNet inputs; instead read
height and width from the input tensor (e.g., h = input_tensor.size(1), w =
input_tensor.size(2)) and use those when expanding mean_color in
calculate_image_mean_color/replacement_image, when building interlacing indices
(torch.arange(0, h, 2) and torch.arange(0, w, 2) for even row/column indices
used with replacement_image and even_indices), and when creating solid_color
replacements so all branches (mean_color, interlacing, solid_color) produce
replacement_image with matching [3, h, w] shape.
- Around line 338-345: The gpu_segments tensor is being re-created on the GPU
each iteration; move the conversion
torch.from_numpy(segments).to(predictor.device) out of the for batch_start in
range(...) loop so it is computed once and reused. Specifically, create
gpu_segments = torch.from_numpy(segments).to(predictor.device) before the loop
that iterates over batch_start (the loop where batch_inputs =
input_batch.repeat(...) is used) and remove the in-loop conversion so subsequent
iterations reuse the same gpu_segments tensor.

In `@ciao/utils/search_utils.py`:
- Around line 21-38: In evaluate_masks, detect any zero bitmask in the masks
list before calling iter_bits/calculate_hyperpixel_deltas: iterate over masks
and if any mask == 0 raise a ValueError (or convert to a non-empty default) with
a clear message identifying the offending index and that a zero bitmask produced
an empty segment list; reference the masks parameter and the all_segment_ids
construction that uses iter_bits so the error points to the real root cause
rather than the downstream calculate_hyperpixel_deltas "Empty segment list"
error.

In `@ciao/utils/segmentation.py`:
- Around line 123-154: Translate all Czech docstrings and inline comments in
build_fast_adjacency_list (and similarly in build_adjacency_bitmasks) into clear
English: update the function docstring to describe args and return value in
English, convert inline comments such as the initialization comment, the
neighbor offsets comment, the "if neighbor exists" check, and the final
conversion/sorting comment to English, and keep the original variable names
(e.g., temp_adj, hex_neighbors, final_adj) and logic unchanged so only textual
content is modified for readability by an international team.

In `@pyproject.toml`:
- Line 29: The numpy dependency in pyproject.toml is too permissive
("numpy>=1.21.0"); update that requirement to a tighter range to avoid pulling
in incompatible NumPy 2.x versions—for example replace "numpy>=1.21.0" with
"numpy>=1.24.0,<3" (or at minimum "numpy>=1.24.0") in pyproject.toml, then
regenerate/lock dependencies and run the test matrix / dependency resolver to
verify compatibility with torch and scikit-image.

In `@README.md`:
- Line 83: The fenced code block containing the snippet "ciao/" lacks a language
specifier; update the opening triple-backticks to include a language (e.g.,
change ``` to ```text) so the block is annotated (identify the block by its
content "ciao/") and commit the README.md change.

---

Nitpick comments:
In `@ciao/__init__.py`:
- Line 7: The package __init__ currently imports CIAOExplainer but does not
declare a public API, so add an explicit __all__ that includes "CIAOExplainer"
to make it part of the package's public surface; update ciao/__init__.py to
declare __all__ = ["CIAOExplainer"] (keeping the existing import from
ciao.explainer.ciao_explainer import CIAOExplainer) so consumers using from ciao
import * or introspection will expose CIAOExplainer.

In `@ciao/algorithm/lookahead_bitset.py`:
- Line 56: Replace all raw print(...) calls in this module with Python logging
calls: import logging at the top, create logger = logging.getLogger(__name__),
and use logger.debug(...) or logger.info(...) instead of print for the
debug/tracing messages found in build_hyperpixel_greedy_lookahead and
build_all_hyperpixels_greedy_lookahead (e.g., the "Starting greedy lookahead
from seed {seed_idx}" message and the other prints referenced around lines
73-76, 78-80, 104-106, 113, 128-130, 287-288, 318-320). Preserve the original
message text and formatting but pass it to logger.debug or logger.info so
callers can control verbosity via the logging configuration.

In `@ciao/algorithm/mcgs.py`:
- Around line 322-331: backup_paths_rave currently recomputes the frontier for
every node via get_frontier(node.mask, adj_masks, used_mask) during
backpropagation, causing repeated expensive work; to fix, add a cached frontier
on the node (e.g., node.frontier_cache) and change backup_paths_rave to check
and reuse node.frontier_cache before calling get_frontier, updating
node.frontier_cache when absent or invalid, then use that cached bitmask for the
membership check and call update_rave_stats(node, seg_id, reward) as before;
locate get_frontier, backup_paths_rave, and update_rave_stats to implement the
cache and ensure any cache invalidation logic is applied where node.mask or
adj_masks/used_mask change.
- Around line 636-670: Replace the small inefficiencies in the loop: change the
comprehension that builds available_segments to use "seg_id in scores" instead
of "seg_id in scores.keys()", and replace the manual bit-checking that builds
hyperpixel_segments with a call to mask_to_ids(hyperpixel_mask); also add
mask_to_ids to the existing import from ciao.structures.bitmask_graph so
mask_to_ids is available for use in build_hyperpixel_mcgs result handling.

In `@ciao/algorithm/mcts.py`:
- Around line 436-442: The unpacking in the masks evaluation branch creates an
unused variable `indices` (in the line that currently does `indices, masks =
zip(*masks_to_evaluate, strict=True)`); update this to `_indices, masks =
zip(*masks_to_evaluate, strict=True)` (or simply `_` instead of `_indices`) so
the intent of an unused value is clear in the `evaluate_masks` logic inside the
gpu evaluation block of `mcts.py`.
- Around line 559-593: Replace the two manual iterations with the simpler
constructs: for available_segments, iterate directly over scores (use "seg_id
for seg_id in scores if seg_id not in processed_segments" instead of
"scores.keys()"); for hyperpixel_segments, call the existing utility
mask_to_ids(hyperpixel_mask, next_id) (or mask_to_ids(hyperpixel_mask) if that
is the function signature) instead of the manual range/bit-check loop. These
changes touch the block that calls build_hyperpixel_mcts and the variables
scores, processed_segments, hyperpixel_mask and next_id.

In `@ciao/algorithm/potential.py`:
- Around line 500-503: The list comprehension building available_segments uses
the redundant .keys() call; update it to check membership directly against the
mapping by changing the comprehension to use "if seg_id not in
processed_segments and seg_id in scores" (or simply iterate over scores and
filter out processed_segments) so that scores is used directly instead of
scores.keys(); this touches the variable available_segments and the variables
scores and processed_segments in the potential computation code.
- Around line 379-411: The scores list is initialized with None which can cause
a TypeError when compared to max_score; change initialization of scores in the
selection logic to a numeric sentinel (e.g., scores = [-float("inf")] *
len(prefix_masks)) and keep setting cache hits via scores[i] = cache[mask] and
computed values from calculate_hyperpixel_deltas (used with missing_indices) as
you already do; ensure every branch that assigns to scores (the cache hit loop
and the for i, score in zip(missing_indices, computed_scores) block) writes
numeric values so no None remains before the final max search using best_idx and
max_score.
- Around line 112-113: Replace the numerous print() debug/info statements in
build_hyperpixel_using_potential, sampling_phase, and select_best_prefix with
logging calls following the same pattern used in lookahead_bitset.py: import the
logging module (if not already), replace user-facing prints with
logging.info(...) and verbose/debug prints with logging.debug(...), and keep the
message text identical or more descriptive to preserve context (e.g., the
seed/target/sim counts and progress messages referenced around Seed: {seed_idx},
Target: {desired_length}, Sims: {num_simulations}). Make these replacements for
all occurrences noted (around lines with prints at 112-113, 121-123, 128-130,
133, 155, 167-170, 186-188, 190, 315, 415-421) so verbosity can be controlled
via logger configuration. Ensure imports and any logger =
logging.getLogger(__name__) usage match lookahead_bitset.py.

In `@ciao/structures/bitmask_graph.py`:
- Around line 97-148: The parameter base_frontier in sample_connected_superset
is unused; either remove it from the function signature and update all call
sites (and tests) to stop passing it, or retain it but mark it explicitly
deprecated: add a short note to the docstring and emit a runtime
warnings.warn("base_frontier is deprecated and ignored", DeprecationWarning) at
the top of sample_connected_superset so callers are informed, and keep behavior
unchanged; update any internal references to call
_pick_weighted_frontier_segment / pick_random_set_bit using the computed
frontier only and remove mentions of base_frontier from comments and docs.
- Around line 151-207: The function _pick_weighted_frontier_segment currently
uses np.random.choice while other samplers like pick_random_set_bit use Python's
random module, which breaks reproducibility unless both RNGs are seeded
separately; fix by standardizing RNG usage—either switch
_pick_weighted_frontier_segment to accept and use a single RNG (e.g., pass in a
random.Random or a numpy.random.Generator) or replace np.random.choice with the
same RNG used by pick_random_set_bit, and update callers to pass the chosen RNG;
ensure you reference and use the same RNG instance across pick_random_set_bit
and _pick_weighted_frontier_segment (or document dual-RNG seeding requirements
if you intentionally keep both).

In `@ciao/structures/nodes.py`:
- Around line 32-64: Replace the loose dicts used for per-edge stats with a
typed structure to prevent key typos and clarify types: define a TypedDict or
dataclass (e.g., EdgeStats or EdgeStatsDict) containing fields N: int, W: float,
Q: float, max_reward: float and update the MCGSNode.edge_stats and
MCGSNode.rave_stats annotations to use that type; then refactor
MCGSNode.init_edge to create and assign that typed object instead of a
string-keyed dict and adjust any code accessing keys (e.g., places that
read/write edge_stats[action]["N"] etc.) to use the typed fields or keys
consistently.

In `@ciao/utils/calculations.py`:
- Around line 7-14: ModelPredictor currently sets replacement_image = None in
__init__ but calculate_hyperpixel_deltas asserts it's set, which is fragile; add
an explicit setter and replace the assert with a clear runtime check. Implement
a method set_replacement_image(self, image) on ModelPredictor that validates the
image (type/shape/device) and assigns self.replacement_image, update __init__
docstring to mention the lifecycle, and change the assertion in
calculate_hyperpixel_deltas to raise a descriptive ValueError (e.g.,
"replacement_image must be set via set_replacement_image before calling
calculate_hyperpixel_deltas").
- Around line 160-167: Remove the dead experimental code inside the with
torch.no_grad() block that calls get_predictions and computes log-odds (the
commented lines referencing "probabilities = self.get_predictions(input_batch)"
and "result = torch.log(probabilities) - torch.log(1 - probabilities)"); simply
return the logits slice as currently done (outputs[:, target_class_idx]) or, if
you intend to preserve the experiment, move that logic and explanation into a
dedicated ADR or a clearly named helper (e.g., a new method compute_log_odds)
instead of leaving commented-out code in the body of the method.
- Around line 203-211: The utility functions in ciao/utils/calculations.py
currently use print() for diagnostics (e.g., the prints showing original_logit
and class probability around the predictor.get_class_logit_batch and
predictor.get_predictions calls) — replace these print() calls with a module
logger: add "import logging" and "logger = logging.getLogger(__name__)" at top
if missing, then change prints to logger.debug() or logger.info() as appropriate
(use logger.debug for verbose diagnostics); make the same replacements in the
other occurrences mentioned (around the blocks at the create_surrogate_dataset
and calculate_scores_from_surrogate areas, i.e., the lines you noted at 253-254
and 283-284) so all diagnostic output uses the logger consistently.

In `@ciao/utils/search_utils.py`:
- Around line 14-18: The parameter adj_masks in is_terminal is typed as a bare
tuple but get_frontier expects tuple[int, ...]; update is_terminal's signature
to use adj_masks: tuple[int, ...] and ensure any callers pass the same typed
structure so the types align (refer to functions is_terminal and get_frontier to
locate and update the annotation).

In `@ciao/utils/segmentation.py`:
- Around line 88-120: build_adjacency_graph currently loops pixel-by-pixel;
replace the triple nested loops with vectorized NumPy adjacency detection by
comparing shifted arrays (e.g., segments[:-1,:] vs segments[1:,:],
segments[:,:-1] vs segments[:,1:], and the two diagonals when neighborhood==8)
to produce pairs of differing segment ids, use np.where and flatten/zip or
np.stack+unique to get edge pairs, then call adj_graph.add_edges_from on those
pairs instead of adding edges one-at-a-time; keep function signature
build_adjacency_graph(segments, neighborhood=8) and ensure you still add all
segment_ids to adj_graph before adding edges.

In `@README.md`:
- Around line 84-116: The README's project tree currently lists modules that are
not present in this branch (e.g., ciao/data/, ciao/explainer/,
ciao/visualization/, ciao/__main__.py, configs/); update README.md so the
directory tree either (a) only shows files and directories actually in this PR,
or (b) keeps the full planned tree but clearly marks missing items as "planned"
or "coming in future PRs" (add a short note and/or annotate entries like
ciao/data/ and configs/ as planned). Ensure references to files such as
ciao/explainer/ciao_explainer.py, ciao/visualization/visualization.py, and
configs/ciao.yaml are consistent with the branch contents.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 123ebec and 85dbb4d.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • README.md
  • ciao/__init__.py
  • ciao/algorithm/__init__.py
  • ciao/algorithm/lookahead_bitset.py
  • ciao/algorithm/mcgs.py
  • ciao/algorithm/mcts.py
  • ciao/algorithm/potential.py
  • ciao/imagenet_classes.txt
  • ciao/structures/__init__.py
  • ciao/structures/bitmask_graph.py
  • ciao/structures/nodes.py
  • ciao/utils/__init__.py
  • ciao/utils/calculations.py
  • ciao/utils/search_utils.py
  • ciao/utils/segmentation.py
  • pyproject.toml

bakery
balance beam
balloon
ballpoint
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

Minor label spelling inconsistencies.

Two labels deviate from their canonical spellings:

  • Line 419: Band AidBand-Aid (registered trademark, always hyphenated).
  • Line 917: web sitewebsite (single-word form is now standard).
✏️ Proposed fix
-Band Aid
+Band-Aid
-web site
+website

Also applies to: 917-917

🧰 Tools
🪛 LanguageTool

[uncategorized] ~419-~419: The sticky tape used to cover wounds is normally capitalized (trademark) but also spelled with a hyphen.
Context: ...k bakery balance beam balloon ballpoint Band Aid banjo bannister barbell barber chair ba...

(BAND_AID)

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

In `@ciao/imagenet_classes.txt` at line 419, Update the two inconsistent labels by
replacing the exact string "Band Aid" with the hyphenated trademark form
"Band-Aid" and replace "web site" with the single-word modern form "website"
(locate these entries by their current label text "Band Aid" and "web site" in
the imagenet_classes list and edit them in place).

Comment on lines +78 to +96
if replacement == "mean_color":
# Fill entire image with mean color
mean_color = self.calculate_image_mean_color(input_tensor) # [3, 1, 1]
replacement_image = mean_color.expand(-1, 224, 224) # [3, 224, 224]

elif replacement == "interlacing":
# Create interlaced pattern: even columns flipped vertically, then even indices flipped horizontally
replacement_image = input_tensor.clone()
even_indices = torch.arange(0, 224, 2) # [0, 2, 4, ..., 222]

# Step 1: Flip even columns vertically (upside down)
replacement_image[:, :, even_indices] = torch.flip(
replacement_image[:, :, even_indices], dims=[1]
)

# Step 2: Flip even indices horizontally (left-right)
replacement_image[:, even_indices, :] = torch.flip(
replacement_image[:, even_indices, :], dims=[2]
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Hardcoded 224 in replacement image generation will break for non-ImageNet input sizes.

get_replacement_image hardcodes 224 in the mean_color (line 81), interlacing (line 86), and solid_color (line 144) branches. If the input tensor has a different spatial resolution, the replacement image dimensions won't match, causing shape mismatches downstream.

Derive dimensions from the input tensor:

Proposed fix (showing mean_color branch as example)
+        _, h, w = input_tensor.shape
+
         if replacement == "mean_color":
             mean_color = self.calculate_image_mean_color(input_tensor)  # [3, 1, 1]
-            replacement_image = mean_color.expand(-1, 224, 224)  # [3, 224, 224]
+            replacement_image = mean_color.expand(-1, h, w)  # [3, H, W]

Apply the same pattern for the interlacing (torch.arange(0, h, 2) / torch.arange(0, w, 2)) and solid_color branches.

Also applies to: 128-144

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

In `@ciao/utils/calculations.py` around lines 78 - 96, The get_replacement_image
function uses hardcoded 224 for spatial dims causing shape mismatches for
non-ImageNet inputs; instead read height and width from the input tensor (e.g.,
h = input_tensor.size(1), w = input_tensor.size(2)) and use those when expanding
mean_color in calculate_image_mean_color/replacement_image, when building
interlacing indices (torch.arange(0, h, 2) and torch.arange(0, w, 2) for even
row/column indices used with replacement_image and even_indices), and when
creating solid_color replacements so all branches (mean_color, interlacing,
solid_color) produce replacement_image with matching [3, h, w] shape.

"pillow>=9.0.0",

# Scientific computing
"numpy>=1.21.0",
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

numpy>=1.21.0 lower bound is very permissive — NumPy 2.x breaking changes may surface.

NumPy 2.0 (June 2024) removed many deprecated APIs and introduced C-extension ABI changes. Depending on which transitive packages are resolved, this open floor could pull in a numpy version that conflicts with pre-2.x-compatible builds of torch>=2.0.0 and scikit-image>=0.19.0. Consider tightening to >=1.24.0,<3 or at least >=1.24.0 to avoid the earliest affected builds, or explicitly verifying that the full dependency graph resolves cleanly against numpy 2.x.

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

In `@pyproject.toml` at line 29, The numpy dependency in pyproject.toml is too
permissive ("numpy>=1.21.0"); update that requirement to a tighter range to
avoid pulling in incompatible NumPy 2.x versions—for example replace
"numpy>=1.21.0" with "numpy>=1.24.0,<3" (or at minimum "numpy>=1.24.0") in
pyproject.toml, then regenerate/lock dependencies and run the test matrix /
dependency resolver to verify compatibility with torch and scikit-image.


## Project Structure

```
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

Add a language specifier to the fenced code block (flagged by markdownlint MD040).

✏️ Proposed fix
-```
+```text
 ciao/
📝 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
```
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 83-83: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

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

In `@README.md` at line 83, The fenced code block containing the snippet "ciao/"
lacks a language specifier; update the opening triple-backticks to include a
language (e.g., change ``` to ```text) so the block is annotated (identify the
block by its content "ciao/") and commit the README.md change.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces the foundational building blocks for CIAO’s segmentation and hyperpixel search, adding core data structures (bitmask graph primitives + search nodes) and multiple search algorithm implementations (MCTS/MCGS/lookahead/potential), along with shared evaluation utilities.

Changes:

  • Added segmentation utilities for square/hex grids, including adjacency graph construction.
  • Added bitmask-based graph primitives and node data structures for MCTS/MCGS.
  • Implemented core search algorithms (MCTS, MCGS, greedy lookahead, potential-based search) plus shared search utilities and surrogate/score calculations.

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 20 comments.

Show a summary per file
File Description
ciao/utils/segmentation.py Grid segmentation + adjacency graph/list utilities.
ciao/utils/search_utils.py Shared helpers for terminal detection and batched evaluation of masks.
ciao/utils/calculations.py Model predictor utilities, masking delta computation, surrogate dataset helpers.
ciao/utils/__init__.py Exposes common utils (ModelPredictor, create_segmentation).
ciao/structures/nodes.py Node structures for MCTS and MCGS.
ciao/structures/bitmask_graph.py Bitmask graph primitives: frontier computation, sampling, weighted selection.
ciao/structures/__init__.py Exposes node structures.
ciao/algorithm/mcts.py Unified MCTS implementation (standard + RAVE).
ciao/algorithm/mcgs.py Unified MCGS implementation (standard + RAVE) with transpositions.
ciao/algorithm/lookahead_bitset.py Greedy rolling-horizon lookahead search using bitmasks.
ciao/algorithm/potential.py Potential-based sequential Monte Carlo region growing.
ciao/algorithm/__init__.py Algorithm package init.
ciao/__init__.py Package init (currently imports CIAOExplainer).
ciao/imagenet_classes.txt ImageNet class label list resource.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +338 to +346
for batch_start in range(0, num_masks, batch_size):
batch_end = min(batch_start + batch_size, num_masks)
current_batch_size = batch_end - batch_start

batch_inputs = input_batch.repeat(current_batch_size, 1, 1, 1)

# Convert segments numpy array to GPU tensor once
gpu_segments = torch.from_numpy(segments).to(predictor.device)

Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

segments is converted to a GPU tensor inside the batching loop, so it is re-created every batch. This adds avoidable overhead; move gpu_segments = torch.from_numpy(segments).to(predictor.device) outside the for batch_start ... loop and reuse it for all batches.

Suggested change
for batch_start in range(0, num_masks, batch_size):
batch_end = min(batch_start + batch_size, num_masks)
current_batch_size = batch_end - batch_start
batch_inputs = input_batch.repeat(current_batch_size, 1, 1, 1)
# Convert segments numpy array to GPU tensor once
gpu_segments = torch.from_numpy(segments).to(predictor.device)
# Convert segments numpy array to GPU tensor once outside the batch loop
gpu_segments = torch.from_numpy(segments).to(predictor.device)
for batch_start in range(0, num_masks, batch_size):
batch_end = min(batch_start + batch_size, num_masks)
current_batch_size = batch_end - batch_start
batch_inputs = input_batch.repeat(current_batch_size, 1, 1, 1)

Copilot uses AI. Check for mistakes.
"""
for path, reward in zip(batch_paths, rewards, strict=True):
for node in path:
node.pending -= 1 # Release virtual loss
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

Virtual loss bookkeeping is inconsistent: selection/expansion increments child.pending, but backup decrements pending for every node in path (including the root, whose pending is never incremented). This will drive root.pending negative over time. Decrement only for nodes whose pending was incremented (e.g., path[1:]), or increment pending for the root at the start of each simulation.

Suggested change
node.pending -= 1 # Release virtual loss
if node.pending > 0:
node.pending -= 1 # Release virtual loss

Copilot uses AI. Check for mistakes.
Comment on lines +199 to +202
def create_square_grid(input_tensor, square_size=14, neighborhood=8):
"""Create a grid of squares with graph structure representing spatial relationships"""
channels, height, width = input_tensor.shape
segments = np.zeros((height, width), dtype=np.int32)
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

Unused local channels will be flagged by Ruff (F841). If the channel count isn't needed, unpack as _, height, width = ... (or name it _channels) to satisfy linting.

Copilot uses AI. Check for mistakes.
Comment on lines +238 to +240
channels, height, width = input_tensor.shape
segments = np.zeros((height, width), dtype=np.int32)

Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

Unused local channels will be flagged by Ruff (F841). If the channel count isn't needed, unpack as _, height, width = ... (or name it _channels) to satisfy linting.

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +17


def mask_to_ids(mask: int) -> list[int]:
"""Convert integer bitmask to list of segment indices."""
return [i for i in range(mask.bit_length()) if (mask >> i) & 1]


def iter_bits(mask: int):
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

iter_bits is missing a return type annotation. Under mypy strict mode (strict = True in .mypy.ini), untyped function defs are errors. Annotate it as returning an iterator/generator of int to keep mypy passing.

Suggested change
def mask_to_ids(mask: int) -> list[int]:
"""Convert integer bitmask to list of segment indices."""
return [i for i in range(mask.bit_length()) if (mask >> i) & 1]
def iter_bits(mask: int):
from collections.abc import Iterator
def mask_to_ids(mask: int) -> list[int]:
"""Convert integer bitmask to list of segment indices."""
return [i for i in range(mask.bit_length()) if (mask >> i) & 1]
def iter_bits(mask: int) -> Iterator[int]:

Copilot uses AI. Check for mistakes.

return replacement_image

def plot_image_mean_color(self, input_tensor):
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

plot_image_mean_color is missing type annotations for its parameter/return value. With mypy strict = True, this will fail uv run mypy .. Please annotate input_tensor (and the return type, likely None) or mark the method to be ignored by mypy.

Suggested change
def plot_image_mean_color(self, input_tensor):
def plot_image_mean_color(self, input_tensor: torch.Tensor) -> None:

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +52
def build_hyperpixel_using_potential(
predictor,
input_batch,
segments,
adj_list: tuple[tuple[int, ...], ...],
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

This function has multiple untyped parameters (predictor, input_batch, segments, etc.). With .mypy.ini set to strict = True, this will be a mypy error when running uv run mypy .. Add type annotations (or configure mypy to ignore this module) to keep strict type checking passing.

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +29
def mean_value(self):
return self.value_sum / self.visits if self.visits > 0 else 0.0

def rave_mean(self):
return self.rave_value_sum / self.rave_visits if self.rave_visits > 0 else 0.0
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

mean_value / rave_mean are missing return type annotations. Under mypy strict = True, unannotated defs are errors. Add explicit -> float return types (and consider typing children with forward references via from __future__ import annotations for consistency).

Copilot uses AI. Check for mistakes.
Comment on lines +233 to +237
# Calculate deltas for all local groups
deltas = calculate_hyperpixel_deltas(
predictor,
input_batch,
segments,
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

create_surrogate_dataset() calls calculate_hyperpixel_deltas(...), but calculate_hyperpixel_deltas requires predictor.replacement_image to be pre-initialized (it asserts non-None). Since create_surrogate_dataset doesn't set it, this will raise at runtime unless every caller remembers to set it beforehand. Consider initializing predictor.replacement_image here (or accept a replacement strategy param and build it internally) to make the function self-contained.

Copilot uses AI. Check for mistakes.
Comment on lines +260 to +262
for node in path:
# --- STANDARD BACKUP ---
node.pending -= 1 # Release virtual loss
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

Same virtual-loss issue as backup_paths(): backup_paths_rave() decrements pending for every node in path even though pending is only incremented on selected/expanded children. This will make the root's pending negative and makes virtual loss unreliable. Decrement only for nodes whose pending was incremented (typically path[1:]) or increment root pending at rollout start.

Suggested change
for node in path:
# --- STANDARD BACKUP ---
node.pending -= 1 # Release virtual loss
for depth, node in enumerate(path):
# --- STANDARD BACKUP ---
if depth > 0:
# Release virtual loss only for nodes whose pending was incremented
node.pending -= 1

Copilot uses AI. Check for mistakes.
dhalmazna and others added 7 commits February 25, 2026 06:52
Co-authored-by: Adam Kukučka <adam.kukucka4@gmail.com>
Add bitmask graph operations, MCTS/MCGS node structures, segmentation utilities, model predictor, and supporting ImageNet class names.
Add MCTS, MCGS, lookahead, and potential search algorithms for hyperpixel construction.
Improve error handling and remove legacy parameters in algorithm functions.
@dhalmazna dhalmazna force-pushed the feature/ciao-core-algorithm branch from a29b524 to 96eb26c Compare February 25, 2026 07:02
@dhalmazna dhalmazna self-assigned this Feb 25, 2026
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: 7

♻️ Duplicate comments (5)
README.md (1)

83-83: ⚠️ Potential issue | 🟡 Minor

Add a language specifier to the fenced code block.

Line 83 still violates markdownlint MD040 (``` without language). Please change it to something like ```text.

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

In `@README.md` at line 83, Update the fenced code block that currently uses only
``` to include a language specifier (e.g., change ``` to ```text or another
appropriate language) so it satisfies markdownlint MD040; locate the plain
triple-backtick fenced block in README.md (the one without a language tag) and
add the language identifier to the opening fence.
ciao/imagenet_classes.txt (1)

420-420: Minor label spelling inconsistencies remain unfixed.

Band Aid (line 420) → Band-Aid and web site (line 917) → website — as flagged in a prior review.

Also applies to: 917-917

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

In `@ciao/imagenet_classes.txt` at line 420, Update the two inconsistent labels in
imagenet_classes.txt: change the entry "Band Aid" to "Band-Aid" and change the
entry "web site" to "website" so spelling/hyphenation matches the prior review;
locate and edit the literal strings "Band Aid" and "web site" in the file
(entries around the previously flagged lines) to apply the corrections.
pyproject.toml (1)

29-29: numpy>=1.21.0 lower bound is still too permissive — previously flagged.

NumPy 2.x introduced breaking changes. Consider tightening to >=1.24.0,<3 as suggested in the prior review.

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

In `@pyproject.toml` at line 29, Update the NumPy version specifier in
pyproject.toml: replace the current requirement string "numpy>=1.21.0" with a
stricter range "numpy>=1.24.0,<3" to avoid NumPy 2.x breaking changes; edit the
dependency entry where "numpy>=1.21.0" appears to use the new specifier.
ciao/utils/search_utils.py (1)

32-36: ⚠️ Potential issue | 🟡 Minor

Report the offending zero-mask index in the validation error.

Line 32 still raises a generic batch-level error. Include the failing index so batched debugging is actionable.

Proposed fix
-    if any(mask == 0 for mask in masks):
-        raise ValueError(
-            "Cannot evaluate zero mask: A mask with value 0 contains no segments. "
-            "Ensure all masks have at least one bit set."
-        )
+    for idx, mask in enumerate(masks):
+        if mask == 0:
+            raise ValueError(
+                f"Cannot evaluate zero mask at index {idx}: mask value 0 contains no segments. "
+                "Ensure all masks have at least one bit set."
+            )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ciao/utils/search_utils.py` around lines 32 - 36, Change the generic
batch-level ValueError raised when a zero mask is found to include the index of
the offending mask: instead of using the any(mask == 0 for mask in masks) check,
iterate with enumerate over masks (e.g., for i, mask in enumerate(masks)) and
when mask == 0 raise a ValueError that mentions the index i (and optionally the
mask value) so callers of the function in search_utils.py can identify which
item in the batch failed validation.
ciao/algorithm/lookahead_bitset.py (1)

194-198: ⚠️ Potential issue | 🟡 Minor

Emit a warning when the BFS fallback path is hit.

Line 194 silently falls back to _find_first_step(...). If this invariant breaks, debugging becomes difficult without visibility.

Proposed fix
                 else:
                     # This shouldn't happen in proper BFS, but handle edge case
                     # This mask came from current_mask, find the connection
                     first_step = _find_first_step(current_mask, new_mask)
+                    import warnings
+                    warnings.warn(
+                        "Fallback in _generate_lookahead_candidates: "
+                        f"mask={mask}, new_mask={new_mask}, first_step={first_step}",
+                        RuntimeWarning,
+                        stacklevel=2,
+                    )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ciao/algorithm/lookahead_bitset.py` around lines 194 - 198, The else branch
that silently falls back to calling _find_first_step(current_mask, new_mask)
needs to emit a warning so this invariant break is visible; update the else
block to log/warn (using the module logger or warnings.warn consistent with the
file) with a clear message that includes the function/context, the values of
current_mask and new_mask, and that the BFS invariant was violated before
calling _find_first_step; keep the existing call to _find_first_step but ensure
the warning is emitted immediately prior to that call.
🧹 Nitpick comments (5)
ciao/algorithm/__init__.py (1)

1-3: Consider populating __all__ with public symbols from sibling modules.

The empty __all__ = [] is valid scaffolding, but since lookahead_bitset.py, mcgs.py, mcts.py, and potential.py are all introduced in this same PR, consumers currently have no ergonomic top-level import path from the ciao.algorithm package (e.g., from ciao.algorithm import MCTS won't work). Deferring this until integration into CIAOExplainer is reasonable, but it's worth a deliberate follow-up once the public API of each module stabilises.

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

In `@ciao/algorithm/__init__.py` around lines 1 - 3, Populate the package export
list so top-level imports work: update __all__ in ciao.algorithm to include the
public symbols exported by the sibling modules (e.g., add the module names
'lookahead_bitset', 'mcgs', 'mcts', 'potential' or the concrete public
classes/functions such as 'MCTS', 'MCGS', 'LookaheadBitset', 'Potential' after
importing them). Modify the file to import the selected public symbols from
lookahead_bitset, mcgs, mcts, and potential and list those symbol names in
__all__ so consumers can do from ciao.algorithm import MCTS (or other public
names).
ciao/utils/calculations.py (1)

375-378: Calling torch.cuda.empty_cache() on every batch iteration hurts throughput.

empty_cache() forces CUDA to return freed memory to the allocator, which is expensive. It's generally only useful before a known peak-memory operation or in OOM recovery. Inside a tight batch loop it can degrade performance significantly. Consider removing it or calling it only when an OOM is caught.

♻️ Proposed change
             # Memory cleanup
             del batch_inputs, masked_logits
-            if torch.cuda.is_available():
-                torch.cuda.empty_cache()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ciao/utils/calculations.py` around lines 375 - 378, Remove the unconditional
torch.cuda.empty_cache() call inside the batch loop (where batch_inputs and
masked_logits are deleted) because it hurts throughput; either delete the
empty_cache() invocation entirely or guard it to only run in OOM handling code
paths (e.g., inside an except RuntimeError catching "out of memory"), keeping
the memory cleanup of del batch_inputs, masked_logits as-is and referencing the
existing torch.cuda.empty_cache() call to locate and modify it.
ciao/utils/segmentation.py (1)

165-190: Duplicated pixel-to-hex mapping loop across create_hexagonal_grid_with_list and create_hexagonal_grid.

Lines 176-185 and 258-267 contain identical logic for mapping pixels to hex IDs. Consider extracting a shared helper (e.g., _assign_hex_segments) that returns (segments, hex_to_id, num_segments), and have both functions call it.

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

In `@ciao/utils/segmentation.py` around lines 165 - 190, The pixel-to-hex mapping
loop in create_hexagonal_grid_with_list and create_hexagonal_grid is duplicated;
extract it into a shared helper (e.g., _assign_hex_segments(input_tensor:
torch.Tensor, hex_radius: int) -> tuple[np.ndarray, dict, int]) that performs
the nested x/y loop, calls pixel_to_hex, populates segments and hex_to_id and
returns (segments, hex_to_id, next_id); then replace the duplicated loops in
create_hexagonal_grid_with_list and create_hexagonal_grid to call
_assign_hex_segments(input_tensor, hex_radius) and use its returned segments,
hex_to_id and num_segments (pass num_segments to build_fast_adjacency_list and
keep variable names consistent like adjacency_list =
build_fast_adjacency_list(hex_to_id, num_segments)).
ciao/structures/nodes.py (2)

1-23: Inconsistent Optional vs | union syntax.

Line 6 uses Optional["MCTSNode"] while line 23 uses int | None. Since the project targets Python ≥3.11, consider standardizing on X | None throughout and dropping the from typing import Optional import. For the forward reference, adding from __future__ import annotations at the top would allow MCTSNode | None without quotes.

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

In `@ciao/structures/nodes.py` around lines 1 - 23, The file mixes typing styles:
change the forward-reference and optional types to consistent PEP 604 unions by
adding "from __future__ import annotations" at the top, replace the __init__
parent parameter type Optional["MCTSNode"] with "MCTSNode | None" (and any other
Optional[...] usages), and remove the now-unused "from typing import Optional"
import; confirm the annotations for attributes like frontier_cache and children
remain valid (e.g., frontier_cache: int | None, children: dict[int, MCTSNode])
and update the MCTSNode.__init__ signature and related attribute annotations
accordingly.

37-62: String-keyed edge stats dicts are fragile.

edge_stats and rave_stats use magic string keys ("N", "W", "Q", "max_reward"). A typo in any consumer will silently produce a KeyError at runtime. Consider defining a TypedDict or a small dataclass for these stat bundles to get static type checking.

♻️ Example with TypedDict
from typing import TypedDict

class EdgeStat(TypedDict):
    N: float
    W: float
    Q: float
    max_reward: float

Then type the dicts as dict[int, EdgeStat].

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

In `@ciao/structures/nodes.py` around lines 37 - 62, Replace fragile string-keyed
stat dicts with a typed structure: define a TypedDict (e.g., EdgeStat with keys
N, W, Q, max_reward) or a small dataclass, change the types of edge_stats and
rave_stats to dict[int, EdgeStat] (or dict[int, EdgeStatClass]) and update
init_edge to initialize instances of that typed structure instead of raw dicts;
then update all consumers that access edge_stats and rave_stats (references to
edge_stats, rave_stats, init_edge, and any code reading
"N"/"W"/"Q"/"max_reward") to use the typed fields (or attributes) so you get
static checking and avoid silent KeyError at runtime.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ciao/algorithm/mcts.py`:
- Around line 220-223: The backup loop that iterates over for path, reward in
zip(batch_paths, rewards, strict=True) decrements node.pending for every node
(including the root), causing underflow because the root never receives a
virtual-loss increment; change the decrement to only release virtual loss when
appropriate by guarding the decrement with either a check that node.pending > 0
(e.g., if node.pending: node.pending -= 1) or by skipping the root (e.g., if
node.parent is not None: node.pending -= 1), and apply the same guard in the
corresponding backup at lines ~261-264 so pending cannot go negative.

In `@ciao/structures/bitmask_graph.py`:
- Around line 194-197: The softmax computation using effective_scores divides by
temperature without validation (in code around effective_scores -> exp_scores ->
softmax_probs); add a guard that ensures temperature is > 0 before using it
(either raise a clear ValueError mentioning temperature and function/class name,
or clamp temperature to a small positive epsilon like 1e-8), then proceed to
compute exp_scores = np.exp((effective_scores - max_score) / temperature) and
softmax_probs as before so np.random.choice receives valid probabilities.

In `@ciao/utils/calculations.py`:
- Around line 206-214: Replace the direct print(...) diagnostic calls in this
module with the logging module: create or use a module logger (e.g., logger =
logging.getLogger(__name__)) and change the prints that report original_logit
and prediction probability (calls around predictor.get_class_logit_batch and
predictor.get_predictions) to logger.debug or logger.info as appropriate; also
find and replace the other print usages mentioned (the prints near the other
predictor-related diagnostics) so all diagnostic output is emitted via the
logger allowing configurable verbosity.
- Around line 154-157: plot_image_mean_color currently sends ImageNet-normalized
values from calculate_image_mean_color straight to plt.imshow, which shows
non-human-readable colors; update plot_image_mean_color to unnormalize the
tensor before display by undoing ImageNet normalization (multiply by std
[0.229,0.224,0.225], add mean [0.485,0.456,0.406]), clamp to [0,1], move to
CPU/detach, permute to HWC and convert to numpy for plt.imshow; alternatively,
if you prefer to keep normalized display, add a clear note in
plot_image_mean_color's docstring explaining the image is normalized. Ensure you
reference the methods calculate_image_mean_color and plot_image_mean_color when
making the change.
- Around line 335-336: Replace the runtime assert with an explicit check and
raise a clear exception when the replacement image is missing: in the block that
accesses predictor.replacement_image (the lines currently doing "assert
predictor.replacement_image is not None" and "replacement_image =
predictor.replacement_image"), change to an if-statement that tests "if
predictor.replacement_image is None" and raise a ValueError or RuntimeError with
a descriptive message (e.g., "replacement_image is required but missing on
predictor"); then assign replacement_image = predictor.replacement_image after
the check.
- Around line 279-284: When computing per-segment means in the loop (variables
X, y, scores, segment_id, segment_scores), guard against empty segment_scores:
check if segment_scores.size == 0 (or len(segment_scores) == 0) and assign a
safe default (e.g., 0.0 or another chosen fallback) to scores[segment_id];
otherwise assign float(segment_scores.mean()). This prevents np.ndarray.mean()
from returning NaN when no samples mask a given segment.

In `@ciao/utils/segmentation.py`:
- Around line 112-122: The 8-neighborhood diagonal adjacency misses
anti-diagonals at the rightmost column because the loop uses for x in
range(width - 1) so center x==width-1 is never processed; update the logic in
the neighborhood==8 block (where segments, adj_graph, width, height are used) to
either iterate x over range(width) and guard diagonals by bounds checks or keep
range(width - 1) for the main diagonal and add a separate check/loop that
examines the anti-diagonal neighbor (segments[y + 1, x - 1]) for x == width - 1
(or generally x in 1..width-1) so the edge between (y, width-1) and (y+1,
width-2) is added to adj_graph.

---

Duplicate comments:
In `@ciao/algorithm/lookahead_bitset.py`:
- Around line 194-198: The else branch that silently falls back to calling
_find_first_step(current_mask, new_mask) needs to emit a warning so this
invariant break is visible; update the else block to log/warn (using the module
logger or warnings.warn consistent with the file) with a clear message that
includes the function/context, the values of current_mask and new_mask, and that
the BFS invariant was violated before calling _find_first_step; keep the
existing call to _find_first_step but ensure the warning is emitted immediately
prior to that call.

In `@ciao/imagenet_classes.txt`:
- Line 420: Update the two inconsistent labels in imagenet_classes.txt: change
the entry "Band Aid" to "Band-Aid" and change the entry "web site" to "website"
so spelling/hyphenation matches the prior review; locate and edit the literal
strings "Band Aid" and "web site" in the file (entries around the previously
flagged lines) to apply the corrections.

In `@ciao/utils/search_utils.py`:
- Around line 32-36: Change the generic batch-level ValueError raised when a
zero mask is found to include the index of the offending mask: instead of using
the any(mask == 0 for mask in masks) check, iterate with enumerate over masks
(e.g., for i, mask in enumerate(masks)) and when mask == 0 raise a ValueError
that mentions the index i (and optionally the mask value) so callers of the
function in search_utils.py can identify which item in the batch failed
validation.

In `@pyproject.toml`:
- Line 29: Update the NumPy version specifier in pyproject.toml: replace the
current requirement string "numpy>=1.21.0" with a stricter range
"numpy>=1.24.0,<3" to avoid NumPy 2.x breaking changes; edit the dependency
entry where "numpy>=1.21.0" appears to use the new specifier.

In `@README.md`:
- Line 83: Update the fenced code block that currently uses only ``` to include
a language specifier (e.g., change ``` to ```text or another appropriate
language) so it satisfies markdownlint MD040; locate the plain triple-backtick
fenced block in README.md (the one without a language tag) and add the language
identifier to the opening fence.

---

Nitpick comments:
In `@ciao/algorithm/__init__.py`:
- Around line 1-3: Populate the package export list so top-level imports work:
update __all__ in ciao.algorithm to include the public symbols exported by the
sibling modules (e.g., add the module names 'lookahead_bitset', 'mcgs', 'mcts',
'potential' or the concrete public classes/functions such as 'MCTS', 'MCGS',
'LookaheadBitset', 'Potential' after importing them). Modify the file to import
the selected public symbols from lookahead_bitset, mcgs, mcts, and potential and
list those symbol names in __all__ so consumers can do from ciao.algorithm
import MCTS (or other public names).

In `@ciao/structures/nodes.py`:
- Around line 1-23: The file mixes typing styles: change the forward-reference
and optional types to consistent PEP 604 unions by adding "from __future__
import annotations" at the top, replace the __init__ parent parameter type
Optional["MCTSNode"] with "MCTSNode | None" (and any other Optional[...]
usages), and remove the now-unused "from typing import Optional" import; confirm
the annotations for attributes like frontier_cache and children remain valid
(e.g., frontier_cache: int | None, children: dict[int, MCTSNode]) and update the
MCTSNode.__init__ signature and related attribute annotations accordingly.
- Around line 37-62: Replace fragile string-keyed stat dicts with a typed
structure: define a TypedDict (e.g., EdgeStat with keys N, W, Q, max_reward) or
a small dataclass, change the types of edge_stats and rave_stats to dict[int,
EdgeStat] (or dict[int, EdgeStatClass]) and update init_edge to initialize
instances of that typed structure instead of raw dicts; then update all
consumers that access edge_stats and rave_stats (references to edge_stats,
rave_stats, init_edge, and any code reading "N"/"W"/"Q"/"max_reward") to use the
typed fields (or attributes) so you get static checking and avoid silent
KeyError at runtime.

In `@ciao/utils/calculations.py`:
- Around line 375-378: Remove the unconditional torch.cuda.empty_cache() call
inside the batch loop (where batch_inputs and masked_logits are deleted) because
it hurts throughput; either delete the empty_cache() invocation entirely or
guard it to only run in OOM handling code paths (e.g., inside an except
RuntimeError catching "out of memory"), keeping the memory cleanup of del
batch_inputs, masked_logits as-is and referencing the existing
torch.cuda.empty_cache() call to locate and modify it.

In `@ciao/utils/segmentation.py`:
- Around line 165-190: The pixel-to-hex mapping loop in
create_hexagonal_grid_with_list and create_hexagonal_grid is duplicated; extract
it into a shared helper (e.g., _assign_hex_segments(input_tensor: torch.Tensor,
hex_radius: int) -> tuple[np.ndarray, dict, int]) that performs the nested x/y
loop, calls pixel_to_hex, populates segments and hex_to_id and returns
(segments, hex_to_id, next_id); then replace the duplicated loops in
create_hexagonal_grid_with_list and create_hexagonal_grid to call
_assign_hex_segments(input_tensor, hex_radius) and use its returned segments,
hex_to_id and num_segments (pass num_segments to build_fast_adjacency_list and
keep variable names consistent like adjacency_list =
build_fast_adjacency_list(hex_to_id, num_segments)).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 123ebec and a29b524.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • README.md
  • ciao/__init__.py
  • ciao/algorithm/__init__.py
  • ciao/algorithm/lookahead_bitset.py
  • ciao/algorithm/mcgs.py
  • ciao/algorithm/mcts.py
  • ciao/algorithm/potential.py
  • ciao/imagenet_classes.txt
  • ciao/structures/__init__.py
  • ciao/structures/bitmask_graph.py
  • ciao/structures/nodes.py
  • ciao/utils/__init__.py
  • ciao/utils/calculations.py
  • ciao/utils/search_utils.py
  • ciao/utils/segmentation.py
  • pyproject.toml

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

♻️ Duplicate comments (8)
pyproject.toml (1)

29-29: Line 29 NumPy floor likely needs tightening for the declared Python baseline.

requires-python = ">=3.11" plus numpy>=1.21.0 is a weak compatibility signal; consider bumping NumPy minimum to a Python-3.11-compatible floor.

What is the earliest NumPy release that supports Python 3.11, and what minimum NumPy version is recommended for Python 3.11+ projects in 2026?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pyproject.toml` at line 29, The dependency floor "numpy>=1.21.0" in
pyproject.toml is too low for the declared requires-python=">=3.11"; bump the
NumPy minimum to a Python-3.11-compatible release (earliest official NumPy
release that added Python 3.11 support is 1.24.0), and for 2026 projects I
recommend setting a safer minimum like "numpy>=1.26.0" (or whichever current
stable minor you standardize on); update the "numpy>=1.21.0" line in
pyproject.toml accordingly and run the test matrix/CI to verify compatibility.
ciao/algorithm/lookahead_bitset.py (1)

195-197: Add visibility on unexpected BFS fallback path.

This branch is marked as “shouldn’t happen”; emitting a warning here would make debugging easier when invariants break.

🔎 Proposed tweak
+                        import logging
+                        logging.getLogger(__name__).warning(
+                            "Unexpected lookahead fallback: current_mask=%s new_mask=%s",
+                            current_mask,
+                            new_mask,
+                        )
                         first_step = _find_first_step(current_mask, new_mask)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ciao/algorithm/lookahead_bitset.py` around lines 195 - 197, This branch in
lookahead_bitset.py where we handle the “shouldn't happen” BFS fallback (calling
_find_first_step(current_mask, new_mask)) needs visibility: add a warning log
that includes contextual details (e.g., current_mask, new_mask, maybe node or
step identifiers available in scope) so when the invariant is violated you can
trace the inputs that triggered the fallback; use the module/logger used
elsewhere in this file (or the standard logging module) and keep the message
concise but descriptive.
ciao/utils/calculations.py (3)

335-336: ⚠️ Potential issue | 🟠 Major

Use an explicit exception instead of assert for required runtime state.

This precondition is part of normal runtime correctness; assert can be disabled and should not guard required state.

🛡️ Proposed fix
-        assert predictor.replacement_image is not None
-        replacement_image = predictor.replacement_image
+        if predictor.replacement_image is None:
+            raise RuntimeError(
+                "predictor.replacement_image must be set before "
+                "calculate_hyperpixel_deltas is called."
+            )
+        replacement_image = predictor.replacement_image
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ciao/utils/calculations.py` around lines 335 - 336, Replace the
runtime-guarding assert with an explicit exception: instead of "assert
predictor.replacement_image is not None", check "if predictor.replacement_image
is None: raise RuntimeError('predictor.replacement_image is required')" (or
ValueError) and then set replacement_image = predictor.replacement_image; update
the block around predictor.replacement_image to perform this explicit None check
and error raise so the required state cannot be skipped when Python assertions
are disabled.

283-285: ⚠️ Potential issue | 🟠 Major

Guard empty segment buckets before mean().

When a segment never appears in X, segment_scores is empty and mean() returns NaN, which silently contaminates ranking.

🛡️ Proposed fix
         segment_scores = y[mask]
-        scores[segment_id] = float(segment_scores.mean())
+        scores[segment_id] = (
+            float(segment_scores.mean()) if segment_scores.size > 0 else 0.0
+        )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ciao/utils/calculations.py` around lines 283 - 285, When computing
per-segment means, guard against empty buckets so mean() doesn't return NaN:
check if segment_scores (derived from y[mask]) has any elements (e.g.,
segment_scores.size or len(segment_scores)) and if empty assign a default
numeric score (e.g., 0.0 or another sentinel) to scores[segment_id] instead of
calling segment_scores.mean(); otherwise compute float(segment_scores.mean()).
Update the block that sets segment_scores = y[mask] and scores[segment_id] to
include this conditional.

92-101: ⚠️ Potential issue | 🟠 Major

Fix interlacing indices for non-square images.

The same index vector is used for both axes. On non-square inputs this can index the width axis with height-based indices and fail at runtime.

🐛 Proposed fix
-            even_indices = torch.arange(0, height, 2)  # Even row indices
+            even_rows = torch.arange(0, height, 2, device=self.device)
+            even_cols = torch.arange(0, width, 2, device=self.device)

             # Step 1: Flip even columns vertically (upside down)
-            replacement_image[:, :, even_indices] = torch.flip(
-                replacement_image[:, :, even_indices], dims=[1]
+            replacement_image[:, :, even_cols] = torch.flip(
+                replacement_image[:, :, even_cols], dims=[1]
             )

             # Step 2: Flip even indices horizontally (left-right)
-            replacement_image[:, even_indices, :] = torch.flip(
-                replacement_image[:, even_indices, :], dims=[2]
+            replacement_image[:, even_rows, :] = torch.flip(
+                replacement_image[:, even_rows, :], dims=[2]
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ciao/utils/calculations.py` around lines 92 - 101, The code incorrectly
reuses even_indices (built from height) for both axes, causing out-of-bounds on
non-square images; create two index vectors—even_row_indices = torch.arange(0,
height, 2) and even_col_indices = torch.arange(0, width, 2)—then apply the
vertical flip to replacement_image[:, even_row_indices, :] with dims=[1] and the
horizontal flip to replacement_image[:, :, even_col_indices] with dims=[2],
updating uses of replacement_image and the flip calls accordingly.
ciao/algorithm/mcts.py (1)

220-223: ⚠️ Potential issue | 🟡 Minor

Prevent pending underflow during backup.

pending is decremented on every node in the path, including root, which is never incremented. This drives root pending negative.

🛡️ Proposed fix
-            node.pending -= 1  # Release virtual loss
+            if node.pending > 0:
+                node.pending -= 1  # Release virtual loss
@@
-            node.pending -= 1  # Release virtual loss
+            if node.pending > 0:
+                node.pending -= 1  # Release virtual loss

Also applies to: 261-264

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

In `@ciao/algorithm/mcts.py` around lines 220 - 223, The backup loop over
batch_paths (the for path, reward in zip(batch_paths, rewards, strict=True)
loop) decrements node.pending for every node including the root which was never
incremented; update the logic to avoid underflow by only decrementing pending
when it is positive or by skipping the root (e.g., check node.pending > 0 or
node.parent is not None before doing node.pending -= 1) and leave node.visits
increment as-is; apply the same guard to the other identical backup section that
updates node.pending/node.visits.
ciao/utils/segmentation.py (1)

112-122: ⚠️ Potential issue | 🟠 Major

8-neighborhood diagonal pass misses right-edge anti-diagonal adjacency.

The current x loop excludes the last column as a center pixel, so anti-diagonal edges from that column are not evaluated.

🐛 Proposed fix
     if neighborhood == 8:
         # Add diagonal adjacency for 8-neighborhood
         for y in range(height - 1):
-            for x in range(width - 1):
+            for x in range(width):
                 center_seg = segments[y, x]
                 # Check diagonal neighbors
-                if segments[y + 1, x + 1] != center_seg:
+                if x < width - 1 and segments[y + 1, x + 1] != center_seg:
                     adj_graph.add_edge(center_seg, segments[y + 1, x + 1])
                 if x > 0 and segments[y + 1, x - 1] != center_seg:
                     adj_graph.add_edge(center_seg, segments[y + 1, x - 1])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ciao/utils/segmentation.py` around lines 112 - 122, The diagonal adjacency
pass currently loops x in range(width - 1) so the last column is never a center
pixel and its anti-diagonal (down-left) adjacency is missed; modify the loop to
iterate x in range(width) and guard neighbor access with bounds checks: inside
the for y in range(height - 1): for x in range(width): get center_seg =
segments[y, x], then if x < width - 1 and segments[y + 1, x + 1] != center_seg:
adj_graph.add_edge(...), and if x > 0 and segments[y + 1, x - 1] != center_seg:
adj_graph.add_edge(...), keeping the same variables (neighborhood, segments,
adj_graph) and y range to avoid index errors.
ciao/imagenet_classes.txt (1)

420-420: ⚠️ Potential issue | 🟡 Minor

Normalize two label spellings for consistency.

Band Aid and web site are inconsistent with the preferred forms used elsewhere in label sets.

✏️ Proposed fix
-Band Aid
+Band-Aid
...
-web site
+website

Also applies to: 917-917

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

In `@ciao/imagenet_classes.txt` at line 420, Replace inconsistent label spellings
by normalizing the strings "Band Aid" and "web site" to the preferred forms used
across the label set: change "Band Aid" to "Band-Aid" and "web site" to
"website" wherever they occur (the diff shows "Band Aid" and also an occurrence
of "web site"); update both occurrences so all labels match the canonical
spelling.
🧹 Nitpick comments (1)
pyproject.toml (1)

9-34: Move visualization and experiment-tracking packages to optional extras.

mlflow, plotly, and ipywidgets are unused in the codebase. matplotlib is only used in the plot_image_mean_color() visualization method, which is not called by any core algorithm code. These packages can be safely moved to optional dependencies to reduce bloat for algorithm-only users.

Proposed refactor
 [project]
 dependencies = [
     # Core ML/DL frameworks
     "torch>=2.0.0",
     "torchvision>=0.15.0",
 
     # Configuration and experiment tracking
     "hydra-core>=1.3.0",
-    "mlflow>=3.0",
     "omegaconf>=2.3.0",
 
-    # XAI and visualization
-    "matplotlib>=3.5.0",
-    "plotly>=5.0.0",
-    "ipywidgets>=7.0.0",
-
     # Image processing and segmentation
     "scikit-image>=0.19.0",
     "pillow>=9.0.0",
@@
     "tqdm>=4.0.0",
 ]
+
+[project.optional-dependencies]
+tracking = ["mlflow>=3.0"]
+viz = [
+    "matplotlib>=3.5.0",
+    "plotly>=5.0.0",
+    "ipywidgets>=7.0.0",
+]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pyproject.toml` around lines 9 - 34, Update pyproject.toml to move non-core
visualization and experiment-tracking packages into optional extras: remove
"mlflow", "plotly", "ipywidgets", and "matplotlib" from the main dependencies
list and add them under an [project.optional-dependencies] (or extras) group
such as extras = {viz =
["matplotlib>=3.5.0","plotly>=5.0.0","ipywidgets>=7.0.0"], tracking =
["mlflow>=3.0"] } so core installs remain lightweight; ensure package names and
version specifiers match the originals and update any CI/dev docs that reference
installing these extras.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ciao/utils/search_utils.py`:
- Around line 31-36: The validation currently only rejects zero masks but allows
negative integers which break iter_bits() (Python's negative ints have infinite
set bits); update the guard in search_utils.py to reject any mask <= 0 (or
explicitly mask == 0 or mask < 0) and raise a clear ValueError mentioning
negative masks, and optionally validate masks are ints before calling
iter_bits() (reference: masks and iter_bits()) so invalid negative bitmasks fail
fast with an informative message.

In `@ciao/utils/segmentation.py`:
- Around line 275-304: Validate segment_size at the start of create_segmentation
to prevent zero/negative values from reaching create_square_grid or
create_hexagonal_grid: check that segment_size is an integer (or castable to
int) and > 0, and if not raise a clear ValueError (e.g., "segment_size must be a
positive integer"), before dispatching to create_square_grid or
create_hexagonal_grid so you avoid step=0 or division-by-zero errors in those
functions.

---

Duplicate comments:
In `@ciao/algorithm/lookahead_bitset.py`:
- Around line 195-197: This branch in lookahead_bitset.py where we handle the
“shouldn't happen” BFS fallback (calling _find_first_step(current_mask,
new_mask)) needs visibility: add a warning log that includes contextual details
(e.g., current_mask, new_mask, maybe node or step identifiers available in
scope) so when the invariant is violated you can trace the inputs that triggered
the fallback; use the module/logger used elsewhere in this file (or the standard
logging module) and keep the message concise but descriptive.

In `@ciao/algorithm/mcts.py`:
- Around line 220-223: The backup loop over batch_paths (the for path, reward in
zip(batch_paths, rewards, strict=True) loop) decrements node.pending for every
node including the root which was never incremented; update the logic to avoid
underflow by only decrementing pending when it is positive or by skipping the
root (e.g., check node.pending > 0 or node.parent is not None before doing
node.pending -= 1) and leave node.visits increment as-is; apply the same guard
to the other identical backup section that updates node.pending/node.visits.

In `@ciao/imagenet_classes.txt`:
- Line 420: Replace inconsistent label spellings by normalizing the strings
"Band Aid" and "web site" to the preferred forms used across the label set:
change "Band Aid" to "Band-Aid" and "web site" to "website" wherever they occur
(the diff shows "Band Aid" and also an occurrence of "web site"); update both
occurrences so all labels match the canonical spelling.

In `@ciao/utils/calculations.py`:
- Around line 335-336: Replace the runtime-guarding assert with an explicit
exception: instead of "assert predictor.replacement_image is not None", check
"if predictor.replacement_image is None: raise
RuntimeError('predictor.replacement_image is required')" (or ValueError) and
then set replacement_image = predictor.replacement_image; update the block
around predictor.replacement_image to perform this explicit None check and error
raise so the required state cannot be skipped when Python assertions are
disabled.
- Around line 283-285: When computing per-segment means, guard against empty
buckets so mean() doesn't return NaN: check if segment_scores (derived from
y[mask]) has any elements (e.g., segment_scores.size or len(segment_scores)) and
if empty assign a default numeric score (e.g., 0.0 or another sentinel) to
scores[segment_id] instead of calling segment_scores.mean(); otherwise compute
float(segment_scores.mean()). Update the block that sets segment_scores =
y[mask] and scores[segment_id] to include this conditional.
- Around line 92-101: The code incorrectly reuses even_indices (built from
height) for both axes, causing out-of-bounds on non-square images; create two
index vectors—even_row_indices = torch.arange(0, height, 2) and even_col_indices
= torch.arange(0, width, 2)—then apply the vertical flip to replacement_image[:,
even_row_indices, :] with dims=[1] and the horizontal flip to
replacement_image[:, :, even_col_indices] with dims=[2], updating uses of
replacement_image and the flip calls accordingly.

In `@ciao/utils/segmentation.py`:
- Around line 112-122: The diagonal adjacency pass currently loops x in
range(width - 1) so the last column is never a center pixel and its
anti-diagonal (down-left) adjacency is missed; modify the loop to iterate x in
range(width) and guard neighbor access with bounds checks: inside the for y in
range(height - 1): for x in range(width): get center_seg = segments[y, x], then
if x < width - 1 and segments[y + 1, x + 1] != center_seg:
adj_graph.add_edge(...), and if x > 0 and segments[y + 1, x - 1] != center_seg:
adj_graph.add_edge(...), keeping the same variables (neighborhood, segments,
adj_graph) and y range to avoid index errors.

In `@pyproject.toml`:
- Line 29: The dependency floor "numpy>=1.21.0" in pyproject.toml is too low for
the declared requires-python=">=3.11"; bump the NumPy minimum to a
Python-3.11-compatible release (earliest official NumPy release that added
Python 3.11 support is 1.24.0), and for 2026 projects I recommend setting a
safer minimum like "numpy>=1.26.0" (or whichever current stable minor you
standardize on); update the "numpy>=1.21.0" line in pyproject.toml accordingly
and run the test matrix/CI to verify compatibility.

---

Nitpick comments:
In `@pyproject.toml`:
- Around line 9-34: Update pyproject.toml to move non-core visualization and
experiment-tracking packages into optional extras: remove "mlflow", "plotly",
"ipywidgets", and "matplotlib" from the main dependencies list and add them
under an [project.optional-dependencies] (or extras) group such as extras = {viz
= ["matplotlib>=3.5.0","plotly>=5.0.0","ipywidgets>=7.0.0"], tracking =
["mlflow>=3.0"] } so core installs remain lightweight; ensure package names and
version specifiers match the originals and update any CI/dev docs that reference
installing these extras.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a29b524 and 96eb26c.

📒 Files selected for processing (15)
  • ciao/__init__.py
  • ciao/algorithm/__init__.py
  • ciao/algorithm/lookahead_bitset.py
  • ciao/algorithm/mcgs.py
  • ciao/algorithm/mcts.py
  • ciao/algorithm/potential.py
  • ciao/imagenet_classes.txt
  • ciao/structures/__init__.py
  • ciao/structures/bitmask_graph.py
  • ciao/structures/nodes.py
  • ciao/utils/__init__.py
  • ciao/utils/calculations.py
  • ciao/utils/search_utils.py
  • ciao/utils/segmentation.py
  • pyproject.toml
✅ Files skipped from review due to trivial changes (1)
  • ciao/init.py
🚧 Files skipped from review as they are similar to previous changes (6)
  • ciao/structures/init.py
  • ciao/utils/init.py
  • ciao/algorithm/potential.py
  • ciao/structures/bitmask_graph.py
  • ciao/algorithm/init.py
  • ciao/algorithm/mcgs.py

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: 8

♻️ Duplicate comments (3)
ciao/structures/bitmask_graph.py (1)

153-215: Temperature validation is in place — good. Past concern resolved.

The temperature <= 0 guard at Line 177 properly addresses the prior review feedback. The softmax + epsilon-greedy mixing logic is numerically sound (max-subtraction for stability, renormalization for floating-point drift).

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

In `@ciao/structures/bitmask_graph.py` around lines 153 - 215, The
_pick_weighted_frontier_segment function already validates temperature (>0),
handles -inf weights, applies optimization_sign, uses numerically-stable softmax
(subtracting max_score) and renormalizes after epsilon-greedy mixing, so no code
change is required; leave _pick_weighted_frontier_segment as implemented.
ciao/algorithm/lookahead_bitset.py (1)

156-214: BFS candidate generation looks correct.

The depth-tracking logic properly inherits first_step from parent masks and the RuntimeError at Line 200 replaces the old silent fallback. The "first path wins" deduplication is sound BFS behavior.

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

In `@ciao/algorithm/lookahead_bitset.py` around lines 156 - 214, No change
required: the BFS in _generate_lookahead_candidates correctly tracks depth,
assigns first_step (using seg_id at depth==1 and inheriting from candidates for
deeper depths), enforces "first path wins" deduplication, and replaces the old
silent fallback with a RuntimeError for inconsistency; leave
_generate_lookahead_candidates as-is.
ciao/utils/calculations.py (1)

335-396: Previous review concerns addressed — LGTM.

The replacement_image is now guarded with a proper RuntimeError (Line 349-353), and gpu_segments is correctly moved outside the batch loop (Line 357). The batched delta computation logic is sound.

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

In `@ciao/utils/calculations.py` around lines 335 - 396, The previous issues have
been resolved: replacement_image is now validated with a RuntimeError (see
replacement_image and create_replacement_image()) and gpu_segments is moved
outside the batch loop in calculate_hyperpixel_deltas; no further changes are
required in this section—approve the PR as-is.
🧹 Nitpick comments (5)
ciao/utils/calculations.py (1)

174-179: Minor: commented-out experimental code.

Lines 175-177 contain a commented-out alternative logit computation. Consider removing it or converting to a documented configuration option to keep the codebase clean.

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

In `@ciao/utils/calculations.py` around lines 174 - 179, Remove or formalize the
commented-out experimental logit computation in calculations.py: eliminate the
commented lines referencing get_predictions, probabilities and the torch.log
expression, or move them behind a clear, documented configuration flag (e.g., an
optional mode in the method that currently returns outputs[:, target_class_idx])
so the alternative computation can be toggled explicitly; update the
function/method docstring to mention the available modes and ensure the code
only contains the active implementation using outputs and target_class_idx.
ciao/structures/bitmask_graph.py (1)

99-150: Dead parameter base_frontier in sample_connected_superset.

The docstring at Line 124 says base_frontier is "unused, kept for compatibility", and indeed the function body never references it — frontier is recomputed via get_frontier on every loop iteration. Carrying a dead parameter in a new module creates unnecessary API surface and confusion for callers.

Consider removing it now while the API is fresh, or at minimum mark it as deprecated so callers don't construct it needlessly.

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

In `@ciao/structures/bitmask_graph.py` around lines 99 - 150,
sample_connected_superset currently accepts a dead parameter base_frontier that
is never used; remove this parameter from sample_connected_superset's signature
and all its callers (and update the docstring and any tests) to eliminate the
unnecessary API surface, or if you must preserve compatibility, mark
base_frontier as deprecated by keeping it in the signature but adding a warning
(e.g., via warnings.warn) in sample_connected_superset and documenting that it
is ignored; update references to the function name sample_connected_superset and
the docstring accordingly so callers no longer construct or pass base_frontier.
ciao/algorithm/potential.py (2)

349-437: select_best_prefix always receives an empty cache — the caching mechanism is unused.

build_hyperpixel_using_potential calls select_best_prefix with cache={} (Line 206), so the cache lookup at Lines 394-398 will never hit, and the writes at Line 415 are discarded. Either wire through a persistent cache across calls, or simplify the function by removing the caching logic.

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

In `@ciao/algorithm/potential.py` around lines 349 - 437, select_best_prefix
currently contains dead caching because build_hyperpixel_using_potential calls
it with cache={} so lookups never hit and stored values are thrown away; either
remove the cache logic from select_best_prefix or (preferred) thread a
persistent cache through the caller: create a single dict cache in
build_hyperpixel_using_potential and pass that same cache into each call to
select_best_prefix (keep the mask keys as used, i.e., prefix_masks and
cache[prefix_masks[i]] assignments), or if removing caching, delete the cache
parameter and all cache lookups/assignments (lines around scores population and
cache[...] = signed_score) and simplify score handling accordingly.

289-310: base_frontier is computed but unused by sample_connected_superset.

Line 298-300 computes valid_n_neighbors and base_frontier, but sample_connected_superset never uses its base_frontier parameter (it recomputes the frontier internally on each step). This work is wasted on every simulation.

This is the downstream effect of the dead base_frontier parameter flagged in bitmask_graph.py.

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

In `@ciao/algorithm/potential.py` around lines 289 - 310, The loop builds a
precomputed exploration boundary (valid_n_neighbors and base_frontier) but
sample_connected_superset currently ignores its base_frontier argument, wasting
work; update sample_connected_superset to accept and initialize its internal
frontier from the passed base_frontier (instead of recomputing from
base_mask/current state), and ensure its signature and callers (here in
potential.py) use that frontier; alternatively, if you prefer to keep
sample_connected_superset unchanged, remove the base_frontier computation and
argument here (symbols to inspect: sample_connected_superset, base_frontier,
valid_n_neighbors, current_frontier_mask, add_node, remove_node, adj_masks) and
either pass only base_mask/target_length/used_mask or refactor so frontier logic
is centralized in one place.
ciao/algorithm/lookahead_bitset.py (1)

217-223: Remove the unused _find_first_step function.

After replacing the BFS fallback with a RuntimeError (lines 200–203), this function at lines 217–223 is no longer called anywhere in the module and can be safely removed.

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

In `@ciao/algorithm/lookahead_bitset.py` around lines 217 - 223, Remove the
now-unused helper function _find_first_step from the module: delete the entire
def _find_first_step(base_mask: int, target_mask: int) block (the docstring,
computation of diff, loop over iter_bits, and the final ValueError) since the
BFS fallback was replaced by raising a RuntimeError and nothing references
_find_first_step any more.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.gitignore:
- Around line 173-174: Remove the ".pre-commit-config.yaml" entry from
.gitignore and commit the actual ".pre-commit-config.yaml" file into the
repository so the shared pre-commit configuration is tracked; if you need
local-only ignores, move them to .git/info/exclude or a separate local file, but
do not keep ".pre-commit-config.yaml" listed in .gitignore.

In `@ciao/algorithm/lookahead_bitset.py`:
- Around line 23-36: The function build_hyperpixel_greedy_lookahead has an
unused parameter scores; remove scores from the signature and any type hints,
update the docstring to stop mentioning it, and adjust all call sites to stop
passing scores (they should continue using optimization_sign and the internally
computed scores_list from calculate_hyperpixel_deltas()); ensure any tests or
usages that relied on the old parameter are updated to the new signature and run
type checking to catch mismatches.

In `@ciao/algorithm/mcts.py`:
- Around line 220-225: The backup loop over batch_paths currently skips the root
(path[1:]) causing root.visits to remain zero and collapsing exploration in
select_uct_child; change the loop to iterate over the entire path and handle the
root specially: for the root node (path[0]) increment visits, value_sum, and
max_value but do not decrement pending (avoid releasing virtual loss), while for
non-root nodes continue to decrement pending, increment visits, and update
value_sum and max_value; reference the loop over batch_paths/rewards and the
attributes pending, visits, value_sum, max_value and the select_uct_child
behavior to locate where to apply this conditional logic.
- Around line 261-266: The RAVE backup loop currently skips the root via for
node in path[1:], causing root visit-starvation; apply the same fix used in
backup_paths by including the root in the RAVE backup (or explicitly updating
the root before the loop): ensure path[0].visits (and its value_sum/max_value as
appropriate) is incremented and that node.pending is only decremented for
non-root nodes (or only when pending>0) so you can change the loop to iterate
over path (for node in path:) and guard pending decrement, or add an explicit
increment/update for path[0] before the existing loop; reference the loop
variables node, path, pending, visits, value_sum, and max_value when making the
change.
- Around line 520-536: The function signature for build_all_hyperpixels_mcts
declares an unused parameter next_id; remove next_id from the parameter list of
build_all_hyperpixels_mcts and update any call sites that pass next_id to it
(search for build_all_hyperpixels_mcts(...) to find callers) so they no longer
provide that argument, and run tests/linting to ensure no other references
remain; leave all other parameters and internal logic unchanged.

In `@ciao/utils/calculations.py`:
- Around line 16-28: The __init__ uses next(model.parameters()).device which
will raise StopIteration for parameter-free models; update the __init__ in
calculations.Calculations (or the class containing __init__) to defensively
obtain device: try to get device from next(model.parameters()), and if
StopIteration occurs fall back to a safe default (e.g., torch.device('cpu')) or
by checking for a model-level attribute (like model.device) before defaulting;
ensure self.device is always set and include a clear error or warning message
when falling back so debugging is easier.
- Around line 94-107: The code uses even_indices = torch.arange(0, height, 2)
for both row and column operations, which can index out of bounds on non-square
images; change to compute separate index ranges (e.g., even_row_indices =
torch.arange(0, height, 2) and even_col_indices = torch.arange(0, width, 2)) and
use even_col_indices when indexing the width/channel slice replacement_image[:,
:, ...] (flip vertically with torch.flip dims=[1]) and use even_row_indices when
indexing the height slice replacement_image[:, even_row_indices, :] (flip
horizontally with torch.flip dims=[2]); keep the same variables
replacement_image and input_tensor and the existing flip dims.
- Around line 255-263: The code assumes node IDs are 0..num_segments-1 which can
cause IndexError; instead build an explicit mapping from graph node IDs to
column indices (e.g., id_to_col = {node: idx for idx,node in
enumerate(segment_ids)}) and use that mapping when filling the surrogate matrix
X (replace uses of segment_id as a column index with id_to_col[segment_id]);
also update calculate_scores_from_surrogate (and any loop using
range(num_segments)) to iterate using the same mapping (or map columns back to
original node IDs) so all indexing is consistent for arbitrary graph node IDs
and add a validation/assertion that all masked segment IDs exist in id_to_col to
surface mismatches early.

---

Duplicate comments:
In `@ciao/algorithm/lookahead_bitset.py`:
- Around line 156-214: No change required: the BFS in
_generate_lookahead_candidates correctly tracks depth, assigns first_step (using
seg_id at depth==1 and inheriting from candidates for deeper depths), enforces
"first path wins" deduplication, and replaces the old silent fallback with a
RuntimeError for inconsistency; leave _generate_lookahead_candidates as-is.

In `@ciao/structures/bitmask_graph.py`:
- Around line 153-215: The _pick_weighted_frontier_segment function already
validates temperature (>0), handles -inf weights, applies optimization_sign,
uses numerically-stable softmax (subtracting max_score) and renormalizes after
epsilon-greedy mixing, so no code change is required; leave
_pick_weighted_frontier_segment as implemented.

In `@ciao/utils/calculations.py`:
- Around line 335-396: The previous issues have been resolved: replacement_image
is now validated with a RuntimeError (see replacement_image and
create_replacement_image()) and gpu_segments is moved outside the batch loop in
calculate_hyperpixel_deltas; no further changes are required in this
section—approve the PR as-is.

---

Nitpick comments:
In `@ciao/algorithm/lookahead_bitset.py`:
- Around line 217-223: Remove the now-unused helper function _find_first_step
from the module: delete the entire def _find_first_step(base_mask: int,
target_mask: int) block (the docstring, computation of diff, loop over
iter_bits, and the final ValueError) since the BFS fallback was replaced by
raising a RuntimeError and nothing references _find_first_step any more.

In `@ciao/algorithm/potential.py`:
- Around line 349-437: select_best_prefix currently contains dead caching
because build_hyperpixel_using_potential calls it with cache={} so lookups never
hit and stored values are thrown away; either remove the cache logic from
select_best_prefix or (preferred) thread a persistent cache through the caller:
create a single dict cache in build_hyperpixel_using_potential and pass that
same cache into each call to select_best_prefix (keep the mask keys as used,
i.e., prefix_masks and cache[prefix_masks[i]] assignments), or if removing
caching, delete the cache parameter and all cache lookups/assignments (lines
around scores population and cache[...] = signed_score) and simplify score
handling accordingly.
- Around line 289-310: The loop builds a precomputed exploration boundary
(valid_n_neighbors and base_frontier) but sample_connected_superset currently
ignores its base_frontier argument, wasting work; update
sample_connected_superset to accept and initialize its internal frontier from
the passed base_frontier (instead of recomputing from base_mask/current state),
and ensure its signature and callers (here in potential.py) use that frontier;
alternatively, if you prefer to keep sample_connected_superset unchanged, remove
the base_frontier computation and argument here (symbols to inspect:
sample_connected_superset, base_frontier, valid_n_neighbors,
current_frontier_mask, add_node, remove_node, adj_masks) and either pass only
base_mask/target_length/used_mask or refactor so frontier logic is centralized
in one place.

In `@ciao/structures/bitmask_graph.py`:
- Around line 99-150: sample_connected_superset currently accepts a dead
parameter base_frontier that is never used; remove this parameter from
sample_connected_superset's signature and all its callers (and update the
docstring and any tests) to eliminate the unnecessary API surface, or if you
must preserve compatibility, mark base_frontier as deprecated by keeping it in
the signature but adding a warning (e.g., via warnings.warn) in
sample_connected_superset and documenting that it is ignored; update references
to the function name sample_connected_superset and the docstring accordingly so
callers no longer construct or pass base_frontier.

In `@ciao/utils/calculations.py`:
- Around line 174-179: Remove or formalize the commented-out experimental logit
computation in calculations.py: eliminate the commented lines referencing
get_predictions, probabilities and the torch.log expression, or move them behind
a clear, documented configuration flag (e.g., an optional mode in the method
that currently returns outputs[:, target_class_idx]) so the alternative
computation can be toggled explicitly; update the function/method docstring to
mention the available modes and ensure the code only contains the active
implementation using outputs and target_class_idx.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 96eb26c and 07877d2.

📒 Files selected for processing (9)
  • .gitignore
  • ciao/algorithm/__init__.py
  • ciao/algorithm/lookahead_bitset.py
  • ciao/algorithm/mcts.py
  • ciao/algorithm/potential.py
  • ciao/structures/bitmask_graph.py
  • ciao/utils/calculations.py
  • ciao/utils/search_utils.py
  • ciao/utils/segmentation.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • ciao/algorithm/init.py
  • ciao/utils/search_utils.py
  • ciao/utils/segmentation.py

Comment on lines +173 to +174
# pre-commit
.pre-commit-config.yaml No newline at end of file
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

pre-commit .pre-commit-config.yaml should be committed to version control

💡 Result:

Yes — .pre-commit-config.yaml should be committed to version control so everyone on the team (and CI) runs the same hooks and versions. The config file is meant to “live in version control,” while each developer still needs to run pre-commit install locally because Git hooks under .git/hooks/ are not versioned. [1], [2]

This is also how services like pre-commit.ci work: they rely on the repository’s committed .pre-commit-config.yaml to know what to run. [3]

Sources:
[1] Stefanie Molin pre-commit workshop (config lives in version control / commit it) (stefaniemolin.com)
[2] DevSecOps guide (config can be versioned; hooks install is local) (devsecops.puziol.com.br)
[3] pre-commit.ci (configured via .pre-commit-config.yaml) (pre-commit.ci)

Citations:


Remove .pre-commit-config.yaml from .gitignore.

.pre-commit-config.yaml is a shared project configuration file that must be committed to version control. It ensures all team members and CI/CD services run the same hooks and versions. Ignoring it breaks team consistency and prevents services like pre-commit.ci from functioning correctly—each developer would need to independently set up their own configuration, defeating the purpose of a shared tool.

If the intent is to keep a personal/local variation out of VCS, use .git/info/exclude (local-only) instead and commit the shared .pre-commit-config.yaml to the repo.

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

In @.gitignore around lines 173 - 174, Remove the ".pre-commit-config.yaml"
entry from .gitignore and commit the actual ".pre-commit-config.yaml" file into
the repository so the shared pre-commit configuration is tracked; if you need
local-only ignores, move them to .git/info/exclude or a separate local file, but
do not keep ".pre-commit-config.yaml" listed in .gitignore.

Comment on lines +23 to +36
def build_hyperpixel_greedy_lookahead(
predictor: ModelPredictor,
input_batch: torch.Tensor,
segments: np.ndarray,
adj_masks: tuple[int, ...],
target_class_idx: int,
scores: dict[int, float],
seed_idx: int,
desired_length: int,
lookahead_distance: int,
optimization_sign: int,
used_mask: int,
batch_size: int = 64,
) -> dict[str, object]:
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

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and read the function to understand the context
cat -n ciao/algorithm/lookahead_bitset.py | head -60

Repository: RationAI/ciao

Length of output: 2277


🏁 Script executed:

# Now read the function body from lines 59-153 to verify scores usage
sed -n '59,153p' ciao/algorithm/lookahead_bitset.py | cat -n

Repository: RationAI/ciao

Length of output: 4000


🏁 Script executed:

# Search for any reference to 'scores' in the entire function
rg -n 'scores' ciao/algorithm/lookahead_bitset.py -A 2 -B 2

Repository: RationAI/ciao

Length of output: 2294


Remove unused scores parameter from function signature.

The scores parameter (line 29) is never referenced in the function body. The docstring incorrectly suggests it's used "for determining sign," but the function actually receives optimization_sign as a separate parameter and computes scores_list internally via calculate_hyperpixel_deltas(). Remove this parameter to avoid misleading callers.

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

In `@ciao/algorithm/lookahead_bitset.py` around lines 23 - 36, The function
build_hyperpixel_greedy_lookahead has an unused parameter scores; remove scores
from the signature and any type hints, update the docstring to stop mentioning
it, and adjust all call sites to stop passing scores (they should continue using
optimization_sign and the internally computed scores_list from
calculate_hyperpixel_deltas()); ensure any tests or usages that relied on the
old parameter are updated to the new signature and run type checking to catch
mismatches.

Comment on lines +220 to +225
for path, reward in zip(batch_paths, rewards, strict=True):
for node in path[1:]: # Skip root (never incremented, so shouldn't decrement)
node.pending -= 1 # Release virtual loss
node.visits += 1
node.value_sum += reward # Mean tracking
node.max_value = max(node.max_value, reward) # MAX backup
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Skipping root in backup fixes pending underflow but starves root of visit counts, killing UCT exploration at depth 1.

path[1:] means root.visits stays 0 forever. In select_uct_child (Line 87), parent_visits = node.visits + 1 = 1, so log(1) = 0, and the exploration term for all of root's children is zero. Selection at depth 1 degrades to pure exploitation — once one child receives a positive max_value, it is always selected over unvisited siblings.

The fix should increment root visits without decrementing root's pending:

Proposed fix
 def backup_paths(batch_paths: list[list[MCTSNode]], rewards: list[float]) -> None:
     for path, reward in zip(batch_paths, rewards, strict=True):
-        for node in path[1:]:  # Skip root (never incremented, so shouldn't decrement)
+        # Update root visits (but not pending, since root is never given virtual loss)
+        path[0].visits += 1
+        path[0].value_sum += reward
+        path[0].max_value = max(path[0].max_value, reward)
+
+        for node in path[1:]:
             node.pending -= 1  # Release virtual loss
             node.visits += 1
             node.value_sum += reward  # Mean tracking
             node.max_value = max(node.max_value, reward)  # MAX backup
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ciao/algorithm/mcts.py` around lines 220 - 225, The backup loop over
batch_paths currently skips the root (path[1:]) causing root.visits to remain
zero and collapsing exploration in select_uct_child; change the loop to iterate
over the entire path and handle the root specially: for the root node (path[0])
increment visits, value_sum, and max_value but do not decrement pending (avoid
releasing virtual loss), while for non-root nodes continue to decrement pending,
increment visits, and update value_sum and max_value; reference the loop over
batch_paths/rewards and the attributes pending, visits, value_sum, max_value and
the select_uct_child behavior to locate where to apply this conditional logic.

Comment on lines +16 to +28
def __init__(self, model: torch.nn.Module, class_names: list[str]) -> None:
self.model = model
self.class_names = class_names
self.device = next(model.parameters()).device
self.replacement_image = None

# Pre-compute normalization constants
self.imagenet_mean = (
torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1).to(self.device)
)
self.imagenet_std = (
torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1).to(self.device)
)
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

next(model.parameters()).device will raise StopIteration on a parameter-free model.

If a model with no parameters is ever passed (e.g., a wrapper or identity model during testing), this will crash without a clear message. A defensive guard would improve debuggability.

Proposed fix
     def __init__(self, model: torch.nn.Module, class_names: list[str]) -> None:
         self.model = model
         self.class_names = class_names
-        self.device = next(model.parameters()).device
+        try:
+            self.device = next(model.parameters()).device
+        except StopIteration:
+            self.device = torch.device("cpu")
         self.replacement_image = None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ciao/utils/calculations.py` around lines 16 - 28, The __init__ uses
next(model.parameters()).device which will raise StopIteration for
parameter-free models; update the __init__ in calculations.Calculations (or the
class containing __init__) to defensively obtain device: try to get device from
next(model.parameters()), and if StopIteration occurs fall back to a safe
default (e.g., torch.device('cpu')) or by checking for a model-level attribute
(like model.device) before defaulting; ensure self.device is always set and
include a clear error or warning message when falling back so debugging is
easier.

Comment on lines +255 to +263
# Create surrogate dataset
num_samples = len(local_groups)
X = np.zeros((num_samples, num_segments), dtype=np.float32)
y = np.array(deltas, dtype=np.float32)

# Fill indicator matrix
for i, masked_segments in enumerate(local_groups):
for segment_id in masked_segments:
X[i, segment_id] = 1.0
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, locate the file and check its size
find . -name "calculations.py" -path "*/ciao/utils/*" | head -5

Repository: RationAI/ciao

Length of output: 85


🏁 Script executed:

#!/bin/bash
# Read the specific lines and surrounding context to understand the code
if [ -f "ciao/utils/calculations.py" ]; then
  wc -l ciao/utils/calculations.py
fi

Repository: RationAI/ciao

Length of output: 87


🏁 Script executed:

#!/bin/bash
# Get the function context around lines 255-263
sed -n '240,280p' ciao/utils/calculations.py

Repository: RationAI/ciao

Length of output: 1405


🏁 Script executed:

#!/bin/bash
# Search for where num_segments is defined in the function
rg -n "num_segments\s*=" ciao/utils/calculations.py -B5 -A5

Repository: RationAI/ciao

Length of output: 850


🏁 Script executed:

#!/bin/bash
# Search for segment_ids definition and how local_groups is constructed
rg -n "segment_ids|local_groups" ciao/utils/calculations.py -B3 -A3

Repository: RationAI/ciao

Length of output: 3188


🏁 Script executed:

#!/bin/bash
# Find the function signature and understand graph parameter
sed -n '200,230p' ciao/utils/calculations.py

Repository: RationAI/ciao

Length of output: 1133


🏁 Script executed:

#!/bin/bash
# Search for where this function is called to see the graph source
rg -n "def.*graph.*:" ciao/utils/calculations.py -B2 -A10

Repository: RationAI/ciao

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Search for graph imports and creation
rg -n "graph\s*=" ciao/utils/calculations.py -B5 -A5

Repository: RationAI/ciao

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Search for where the segments are created/obtained
rg -n "segments\s*=" ciao/utils/calculations.py -B3 -A3

Repository: RationAI/ciao

Length of output: 760


🏁 Script executed:

#!/bin/bash
# Look for imports to understand the graph library being used
head -30 ciao/utils/calculations.py

Repository: RationAI/ciao

Length of output: 916


🏁 Script executed:

#!/bin/bash
# Find the function definition that uses graph parameter
rg -n "def.*graph.*:" ciao/utils/calculations.py -B2 -A15

Repository: RationAI/ciao

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Search for where this function is called
rg -n "create_surrogate_dataset" --type=py -B2 -A2

Repository: RationAI/ciao

Length of output: 305


🏁 Script executed:

#!/bin/bash
# Look for segmentation library usage and graph creation
rg -n "Graph|nx\.Graph|segment" ciao/utils/calculations.py | head -20

Repository: RationAI/ciao

Length of output: 1115


🏁 Script executed:

#!/bin/bash
# Check if there's segment ID validation anywhere
rg -n "validate|check|assert.*segment" ciao/utils/calculations.py

Repository: RationAI/ciao

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Search for calls to create_surrogate_dataset in all Python files
rg "create_surrogate_dataset" --type=py -B3 -A3

Repository: RationAI/ciao

Length of output: 413


🏁 Script executed:

#!/bin/bash
# Search for graph creation patterns - NetworkX graph instantiation
rg "nx\.Graph|from_nx|DiGraph" --type=py -B2 -A2

Repository: RationAI/ciao

Length of output: 2318


🏁 Script executed:

#!/bin/bash
# Look for segmentation imports and segment ID generation
rg "skimage|watershed|felzenszwalb|slic|segment" --type=py -B2 -A2 | head -40

Repository: RationAI/ciao

Length of output: 2315


🏁 Script executed:

#!/bin/bash
# Check main files and entry points
find . -name "main.py" -o -name "__main__.py" -o -name "demo.py" | head -5

Repository: RationAI/ciao

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Check create_square_grid implementation
sed -n '/def create_square_grid/,/^def /p' ciao/utils/segmentation.py | head -50

Repository: RationAI/ciao

Length of output: 1002


🏁 Script executed:

#!/bin/bash
# Check create_hexagonal_grid implementation
sed -n '/def create_hexagonal_grid/,/^def /p' ciao/utils/segmentation.py | head -60

Repository: RationAI/ciao

Length of output: 2132


🏁 Script executed:

#!/bin/bash
# Check the complete segmentation.py to understand segment ID generation
wc -l ciao/utils/segmentation.py

Repository: RationAI/ciao

Length of output: 87


🏁 Script executed:

#!/bin/bash
# Get the complete create_hexagonal_grid function
sed -n '/^def create_hexagonal_grid\(/,/^def /p' ciao/utils/segmentation.py | head -70

Repository: RationAI/ciao

Length of output: 106


🏁 Script executed:

#!/bin/bash
# Verify segment ID generation is always contiguous by checking build_adjacency_graph
sed -n '/^def build_adjacency_graph/,/^def /p' ciao/utils/segmentation.py

Repository: RationAI/ciao

Length of output: 1375


🏁 Script executed:

#!/bin/bash
# Escape the parenthesis properly and get the complete create_hexagonal_grid function
sed -n '/^def create_hexagonal_grid/,/^def /p' ciao/utils/segmentation.py | head -80

Repository: RationAI/ciao

Length of output: 2413


🏁 Script executed:

#!/bin/bash
# Let's verify: are segment_ids always contiguous in both grid creation functions?
# Check if there are any other segmentation methods that might not use contiguous IDs
rg "def create_" ciao/utils/segmentation.py | grep -v "^Binary"

Repository: RationAI/ciao

Length of output: 169


🏁 Script executed:

#!/bin/bash
# Check if there's any external segmentation library used that might have non-contiguous IDs
rg "felzenszwalb|watershed|slic|mark_boundaries" --type=py

Repository: RationAI/ciao

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Verify the complete flow - trace from segment creation to graph creation
cat ciao/utils/segmentation.py | grep -A5 "segment_id = \|next_id = "

Repository: RationAI/ciao

Length of output: 621


🏁 Script executed:

#!/bin/bash
# Check if there's any validation of segment IDs in the function
sed -n '180,270p' ciao/utils/calculations.py

Repository: RationAI/ciao

Length of output: 2999


🏁 Script executed:

#!/bin/bash
# Search for any tests that verify the behavior with different graph structures
find . -name "*test*.py" -o -name "test_*.py" | head -10

Repository: RationAI/ciao

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Check if there are any docstrings or comments about segment ID requirements
rg "segment.*id|contiguous|range" ciao/utils/calculations.py -B2 -A2

Repository: RationAI/ciao

Length of output: 4415


X[i, segment_id] = 1.0 assumes segment IDs are contiguous 0..N-1.

The function accepts a generic nx.Graph parameter but doesn't validate that graph.nodes() returns IDs in the range [0, num_segments). When segment_ids = list(graph.nodes()) is unpacked, if any segment ID exceeds the column count, X[i, segment_id] = 1.0 will raise an IndexError. This assumption is further embedded in calculate_scores_from_surrogate() (line 288), which iterates with range(num_segments), expecting indices 0..N-1.

Add validation to ensure segment IDs are contiguous, or build an ID-to-column mapping to handle arbitrary node IDs.

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

In `@ciao/utils/calculations.py` around lines 255 - 263, The code assumes node IDs
are 0..num_segments-1 which can cause IndexError; instead build an explicit
mapping from graph node IDs to column indices (e.g., id_to_col = {node: idx for
idx,node in enumerate(segment_ids)}) and use that mapping when filling the
surrogate matrix X (replace uses of segment_id as a column index with
id_to_col[segment_id]); also update calculate_scores_from_surrogate (and any
loop using range(num_segments)) to iterate using the same mapping (or map
columns back to original node IDs) so all indexing is consistent for arbitrary
graph node IDs and add a validation/assertion that all masked segment IDs exist
in id_to_col to surface mismatches early.

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

♻️ Duplicate comments (2)
ciao/utils/calculations.py (2)

16-20: ⚠️ Potential issue | 🟡 Minor

Guard device inference for parameter-free models.

Line 19 can raise StopIteration for modules without parameters. Please keep a safe fallback device.

In Python, what exception does next(iterator) raise when the iterator is empty?
Suggested fix
     def __init__(self, model: torch.nn.Module, class_names: list[str]) -> None:
         self.model = model
         self.class_names = class_names
-        self.device = next(model.parameters()).device
+        try:
+            self.device = next(model.parameters()).device
+        except StopIteration:
+            self.device = torch.device("cpu")
         self.replacement_image = None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ciao/utils/calculations.py` around lines 16 - 20, The constructor currently
uses next(model.parameters()).device which raises StopIteration for
parameter-free models; change to use a safe lookup like p =
next(model.parameters(), None) and set self.device = p.device if p is not None
else torch.device("cpu") (or another desired fallback), leaving other attributes
(self.model, self.class_names, self.replacement_image) unchanged; update the
__init__ in the same class/method to perform this guarded device inference.

226-228: ⚠️ Potential issue | 🟠 Major

Validate or remap graph node IDs before filling X.

Line 264 uses segment_id as a direct column index, which breaks when graph node IDs are not contiguous 0..N-1 (and then also misaligns Line 289’s scoring loop). This is still the same unresolved issue from earlier review.

Suggested fix
     segment_ids = list(graph.nodes())
     num_segments = len(segment_ids)
+    expected_ids = list(range(num_segments))
+    if sorted(segment_ids) != expected_ids:
+        raise ValueError(
+            "Graph node IDs must be contiguous integers in [0, num_segments). "
+            "Provide remapped IDs or add an explicit id->column mapping."
+        )
@@
     for i, masked_segments in enumerate(local_groups):
         for segment_id in masked_segments:
             X[i, segment_id] = 1.0

Also applies to: 262-264

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

In `@ciao/utils/calculations.py` around lines 226 - 228, The code assumes graph
node IDs are contiguous and uses segment_id as a direct column index when
filling X and later when scoring; instead create an explicit mapping from graph
node IDs to contiguous column indices (e.g., segment_id_to_idx) derived from
list(graph.nodes()) (or a sorted list if stable order is required), use that
mapping whenever you index into X or iterate to compute scores, and replace any
direct uses of segment_id as a column index with segment_id_to_idx[segment_id]
so X and the scoring loop remain aligned for non-contiguous or arbitrary node
IDs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ciao/utils/calculations.py`:
- Around line 351-354: The RuntimeError message raised when replacement_image is
uninitialized references the wrong helper name; update the message in the raise
inside calculate_hyperpixel_deltas (the block that checks replacement_image) to
instruct callers to call get_replacement_image() instead of
create_replacement_image(), keeping the same explanatory text otherwise so the
error points to the actual public API function.
- Around line 160-167: plot_image_mean_color currently passes a potentially CUDA
tensor to plt.imshow, which will fail on GPU; modify plot_image_mean_color (and
use calculate_image_mean_color output) to .detach(), .cpu(), convert to a NumPy
array, reorder to HWC (permute as needed) and then pass that NumPy array to
plt.imshow (optionally clip or squeeze if shape differs) before plt.show().

---

Duplicate comments:
In `@ciao/utils/calculations.py`:
- Around line 16-20: The constructor currently uses
next(model.parameters()).device which raises StopIteration for parameter-free
models; change to use a safe lookup like p = next(model.parameters(), None) and
set self.device = p.device if p is not None else torch.device("cpu") (or another
desired fallback), leaving other attributes (self.model, self.class_names,
self.replacement_image) unchanged; update the __init__ in the same class/method
to perform this guarded device inference.
- Around line 226-228: The code assumes graph node IDs are contiguous and uses
segment_id as a direct column index when filling X and later when scoring;
instead create an explicit mapping from graph node IDs to contiguous column
indices (e.g., segment_id_to_idx) derived from list(graph.nodes()) (or a sorted
list if stable order is required), use that mapping whenever you index into X or
iterate to compute scores, and replace any direct uses of segment_id as a column
index with segment_id_to_idx[segment_id] so X and the scoring loop remain
aligned for non-contiguous or arbitrary node IDs.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b4ef5f4 and 6a68c1c.

📒 Files selected for processing (1)
  • ciao/utils/calculations.py

Comment on lines +160 to +167
def plot_image_mean_color(self, input_tensor: torch.Tensor) -> None:
"""Display the mean color of the image.

Note: The visualization shows the normalized tensor (ImageNet normalization).
"""
normalized_mean = self.calculate_image_mean_color(input_tensor).unsqueeze(0)
plt.imshow(normalized_mean[0].permute(1, 2, 0))
plt.show()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -name "calculations.py" -path "*/utils/*" | head -5

Repository: RationAI/ciao

Length of output: 85


🌐 Web query:

matplotlib.pyplot.imshow PyTorch tensor CUDA GPU support direct input

💡 Result:

matplotlib.pyplot.imshow does not accept a CUDA torch.Tensor “directly”. Matplotlib’s image APIs expect CPU array-like inputs (effectively NumPy/PIL), and a CUDA tensor can’t be converted to NumPy without copying it back to host memory. [1], [2], [3]

Use:

plt.imshow(t.detach().cpu().numpy())
# (optional for CHW tensors)
# plt.imshow(t.detach().permute(1,2,0).cpu().numpy())

If you need true “display from GPU without a CPU copy”, Matplotlib isn’t the right tool—you’d need a dedicated GPU visualization path (e.g., OpenGL/Vulkan interop tooling), not imshow. [2]


🏁 Script executed:

cat -n ./ciao/utils/calculations.py | sed -n '155,170p'

Repository: RationAI/ciao

Length of output: 769


🏁 Script executed:

# Check broader context and class definition
head -20 ./ciao/utils/calculations.py

Repository: RationAI/ciao

Length of output: 551


🏁 Script executed:

# Look for calculate_image_mean_color method
rg -n "def calculate_image_mean_color" ./ciao/utils/calculations.py -A 5

Repository: RationAI/ciao

Length of output: 361


🏁 Script executed:

# Check the full calculate_image_mean_color method
rg -n "def calculate_image_mean_color" ./ciao/utils/calculations.py -A 15

Repository: RationAI/ciao

Length of output: 855


🏁 Script executed:

# Look for other plotting methods in the file to see if they handle tensor conversion
rg -n "plt\." ./ciao/utils/calculations.py

Repository: RationAI/ciao

Length of output: 139


🏁 Script executed:

# Check if there are other similar tensor visualization patterns in the codebase
rg -n "\.detach\(\)\.cpu\(\)\.numpy\(\)" . --type py | head -10

Repository: RationAI/ciao

Length of output: 39


🏁 Script executed:

# Check if the class stores/tracks device for the model
rg -n "self.device" ./ciao/utils/calculations.py -B 2 -A 2

Repository: RationAI/ciao

Length of output: 1293


🏁 Script executed:

# Verify if input_tensor device can vary (GPU or CPU)
rg -n "plot_image_mean_color" ./ciao/utils/calculations.py -B 5 -A 10

Repository: RationAI/ciao

Length of output: 721


Convert tensor to CPU/NumPy before imshow.

plot_image_mean_color passes a tensor directly to matplotlib. When the model is on GPU, normalized_mean will be a CUDA tensor, and matplotlib.pyplot.imshow() cannot accept CUDA tensors directly—it requires CPU-resident NumPy arrays. This will fail in any GPU workflow.

Suggested fix
     def plot_image_mean_color(self, input_tensor: torch.Tensor) -> None:
         """Display the mean color of the image.
 
         Note: The visualization shows the normalized tensor (ImageNet normalization).
         """
         normalized_mean = self.calculate_image_mean_color(input_tensor).unsqueeze(0)
-        plt.imshow(normalized_mean[0].permute(1, 2, 0))
+        img = (
+            normalized_mean[0]
+            .detach()
+            .cpu()
+            .permute(1, 2, 0)
+            .clamp(0, 1)
+            .numpy()
+        )
+        plt.imshow(img)
         plt.show()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ciao/utils/calculations.py` around lines 160 - 167, plot_image_mean_color
currently passes a potentially CUDA tensor to plt.imshow, which will fail on
GPU; modify plot_image_mean_color (and use calculate_image_mean_color output) to
.detach(), .cpu(), convert to a NumPy array, reorder to HWC (permute as needed)
and then pass that NumPy array to plt.imshow (optionally clip or squeeze if
shape differs) before plt.show().

Comment on lines +351 to +354
raise RuntimeError(
"replacement_image is not initialized. "
"Call create_replacement_image() before using calculate_hyperpixel_deltas."
)
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

Fix stale method name in error message.

The message says create_replacement_image(), but this module exposes get_replacement_image(). This makes troubleshooting harder.

Suggested fix
             raise RuntimeError(
                 "replacement_image is not initialized. "
-                "Call create_replacement_image() before using calculate_hyperpixel_deltas."
+                "Call get_replacement_image() before using calculate_hyperpixel_deltas."
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ciao/utils/calculations.py` around lines 351 - 354, The RuntimeError message
raised when replacement_image is uninitialized references the wrong helper name;
update the message in the raise inside calculate_hyperpixel_deltas (the block
that checks replacement_image) to instruct callers to call
get_replacement_image() instead of create_replacement_image(), keeping the same
explanatory text otherwise so the error points to the actual public API
function.

@dhalmazna dhalmazna requested a review from a team February 26, 2026 08:24
@dhalmazna dhalmazna closed this Mar 4, 2026
@dhalmazna dhalmazna deleted the feature/ciao-core-algorithm branch March 4, 2026 16:37
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.

2 participants