Skip to content

Add array heatmap logging to ViewerGL#2433

Open
han-xudong wants to merge 4 commits intonewton-physics:mainfrom
han-xudong:feat/viewergl-log-array
Open

Add array heatmap logging to ViewerGL#2433
han-xudong wants to merge 4 commits intonewton-physics:mainfrom
han-xudong:feat/viewergl-log-array

Conversation

@han-xudong
Copy link
Copy Markdown

@han-xudong han-xudong commented Apr 14, 2026

Description

Add support for visualizing logged numeric arrays in ViewerGL.

This implements ViewerGL.log_array() for scalar, 1-D, and 2-D numeric arrays.
2-D arrays are rendered as heatmaps in the existing ImGui diagnostics overlay,
alongside the current scalar plots.

This makes it easier to inspect grid-based or field-like debug data directly in
the GL viewer.

Checklist

  • New or existing tests cover these changes
  • The documentation is up to date with these changes
  • CHANGELOG.md has been updated (if user-facing change)

Test plan

Manual verification in a local ViewerGL session.

Verified that:

  • ViewerGL.log_array() accepts scalar inputs and displays them as a 1x1 array
  • ViewerGL.log_array() accepts 1-D arrays and displays them as a single-row heatmap
  • ViewerGL.log_array() accepts 2-D arrays and displays them as a heatmap
  • array metadata (shape, min, max) is shown in the diagnostics overlay
  • scalar plots continue to render correctly alongside logged arrays
  • passing None removes a previously logged array

New feature / API change

import numpy as np

viewer.log_array("Scalar", np.array(1.0))
viewer.log_array("Series", np.array([1.0, 2.0, 3.0]))
viewer.log_array("Heatmap", np.array([[0.0, 1.0], [2.0, 3.0]]))
viewer.log_array("Heatmap", None)

Summary by CodeRabbit

  • New Features

    • Added heatmap visualization for logged 2D numeric arrays in the Plots window.
    • Supports adding, updating, and removing arrays; heatmaps refresh when data or display shape change.
    • Large arrays are downsampled for efficient display; NaN/non-finite values use a distinct color and an Inferno-like palette.
    • Shows shape/min/max overlays and provides collapsible per-array panels with dynamic sizing.
  • Bug Fixes

    • Improved GPU/resource cleanup when arrays are removed or replaced.

@linux-foundation-easycla
Copy link
Copy Markdown

linux-foundation-easycla Bot commented Apr 14, 2026

CLA Signed

The committers listed above are authorized under a signed CLA.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 14, 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

Add heatmap rendering for 2D numeric arrays to ViewerGL: implement log_array(...), per-name array buffers and dirty tracking, LUT colorization and NaN handling, OpenGL texture lifecycle, downsampling/normalization/colorization pipeline, and ImGui "Plots" integration to display heatmaps alongside scalar plots.

Changes

Cohort / File(s) Summary
Changelog
CHANGELOG.md
Added unreleased entry: "Add heatmap rendering for scalar arrays logged through `ViewerGL.log_array()`".
ViewerGL heatmap feature
newton/_src/viewer/viewer_gl.py
Implemented log_array(...) (accepts wp.array or np.ndarray or None, converts to float32, normalizes shapes, rejects non‑2D, handles removals), added per-name buffers (_array_buffers), dirty flags (_array_dirty), cached GL textures (_array_textures), NaN color and LUT (_heatmap_color_lut), texture lifecycle helpers (delete/clear + integrate into clear_model()/close()), heatmap pipeline (downsampling with block averaging preserving NaNs, normalization, LUT colorization), GL texture allocation/update via glTexSubImage2D, and ImGui "Plots" window rendering changes to show collapsible per-array heatmaps with border and shape/min/max overlays. Review attention: GL resource management, NaN/downsampling correctness, dirty-flag synchronization, and ImGui layout sizing.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant ViewerGL
    participant CPU as CPU (downsample/LUT)
    participant GPU as OpenGL
    participant ImGui

    Client->>ViewerGL: log_array(name, array)
    ViewerGL->>ViewerGL: validate/normalize array, store in _array_buffers[name], set _array_dirty[name]=true
    loop per frame
        ViewerGL->>ViewerGL: check for scalar/array data
        ViewerGL->>ViewerGL: if _array_dirty or shape/display changed
        ViewerGL->>CPU: compute downsampled grid, normalize values, apply LUT (colorize), preserve NaNs
        CPU->>GPU: allocate/update texture (glTexImage2D/glTexSubImage2D)
        GPU->>ViewerGL: texture ready
        ViewerGL->>ImGui: render Plots window with texture and overlays (border, shape/min/max)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.89% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add array heatmap logging to ViewerGL' directly and accurately describes the main change: implementing heatmap rendering support for logged arrays in ViewerGL.log_array().

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

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

Copy link
Copy Markdown
Contributor

@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: 1

🧹 Nitpick comments (1)
newton/_src/viewer/viewer_gl.py (1)

2386-2400: Consider downsampling large arrays for performance.

The nested loop issues one add_rect_filled call per cell. For large arrays (e.g., 256×256 = 65,536 cells), this could cause noticeable frame rate drops in the diagnostics overlay.

Consider adding downsampling for arrays exceeding a threshold (e.g., 64×64) to maintain interactive frame rates:

Sketch of downsampling approach
# At the start of _render_array_heatmap, before the loop:
MAX_CELLS = 64
if rows > MAX_CELLS or cols > MAX_CELLS:
    # Downsample using block averaging
    row_factor = max(1, rows // MAX_CELLS)
    col_factor = max(1, cols // MAX_CELLS)
    new_rows = rows // row_factor
    new_cols = cols // col_factor
    array = array[:new_rows * row_factor, :new_cols * col_factor]
    array = array.reshape(new_rows, row_factor, new_cols, col_factor).mean(axis=(1, 3))
    rows, cols = new_rows, new_cols
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@newton/_src/viewer/viewer_gl.py` around lines 2386 - 2400, The nested
per-cell draw in _render_array_heatmap currently calls draw_list.add_rect_filled
for every element (using variables rows, cols and cell drawing logic), which can
be very slow for large arrays; implement downsampling before the nested loop
when rows or cols exceed a threshold (e.g., MAX_CELLS = 64): compute integer
row_factor and col_factor, trim the array to a multiple of those factors,
perform block averaging to produce a smaller array and update rows and cols to
the downsampled dimensions, then continue using the existing loop and
add_rect_filled calls on the downsampled array (keep the original origin/cell
sizing logic but base cell counts on the new rows/cols so the overlay scales
correctly).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@newton/_src/viewer/viewer_gl.py`:
- Around line 1225-1248: The log_array method's signature and docstring don't
reflect that None can be passed to remove a previously logged array and the
Warp-array detection is brittle; update the type annotation of
ViewerGL.log_array to accept Optional (None) for the array parameter, update the
docstring to document that passing None removes the named array from
_array_buffers, and change the Warp-array detection from isinstance(array,
wp.array) to a hasattr(array, "numpy") check (consistent with
ViewerRerun._to_numpy()) before converting to numpy; ensure you import
typing.Optional if needed and keep the rest of the conversion/contiguity logic
the same.

---

Nitpick comments:
In `@newton/_src/viewer/viewer_gl.py`:
- Around line 2386-2400: The nested per-cell draw in _render_array_heatmap
currently calls draw_list.add_rect_filled for every element (using variables
rows, cols and cell drawing logic), which can be very slow for large arrays;
implement downsampling before the nested loop when rows or cols exceed a
threshold (e.g., MAX_CELLS = 64): compute integer row_factor and col_factor,
trim the array to a multiple of those factors, perform block averaging to
produce a smaller array and update rows and cols to the downsampled dimensions,
then continue using the existing loop and add_rect_filled calls on the
downsampled array (keep the original origin/cell sizing logic but base cell
counts on the new rows/cols so the overlay scales correctly).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 7eab504e-c449-4ed7-a131-ab06ca1cd602

📥 Commits

Reviewing files that changed from the base of the PR and between adac37b and cc137e8.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • newton/_src/viewer/viewer_gl.py

Comment thread newton/_src/viewer/viewer_gl.py Outdated
Comment on lines +1225 to +1248
def log_array(self, name: str, array: wp.array[Any] | np.ndarray):
"""
Log a generic array for visualization (not implemented).
Log a numeric array for visualization.

Args:
name: Unique path/name for the array signal.
array: Array data to visualize.
"""
pass
if array is None:
self._array_buffers.pop(name, None)
return

array_np = array.numpy() if isinstance(array, wp.array) else np.asarray(array)
array_np = np.asarray(array_np, dtype=np.float32)
array_np = np.squeeze(array_np)

if array_np.ndim == 0:
array_np = array_np.reshape(1, 1)
elif array_np.ndim == 1:
array_np = array_np.reshape(1, -1)
elif array_np.ndim != 2:
raise ValueError("ViewerGL.log_array only supports scalar, 1-D, or 2-D arrays.")

self._array_buffers[name] = np.ascontiguousarray(array_np)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Type annotation and docstring should reflect None behavior.

The method accepts None to remove a previously logged array (per PR description), but the type annotation doesn't include None and the docstring doesn't document this behavior.

Additionally, consider using the hasattr(x, "numpy") pattern for Warp array detection, which is more flexible and consistent with the pattern in ViewerRerun._to_numpy().

Proposed fix
 `@override`
-def log_array(self, name: str, array: wp.array[Any] | np.ndarray):
+def log_array(self, name: str, array: wp.array[Any] | np.ndarray | None):
     """
     Log a numeric array for visualization.

     Args:
         name: Unique path/name for the array signal.
-        array: Array data to visualize.
+        array: Array data to visualize, or ``None`` to remove a
+            previously logged array.
     """
     if array is None:
         self._array_buffers.pop(name, None)
         return

-    array_np = array.numpy() if isinstance(array, wp.array) else np.asarray(array)
+    array_np = array.numpy() if hasattr(array, "numpy") else np.asarray(array)
     array_np = np.asarray(array_np, dtype=np.float32)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@newton/_src/viewer/viewer_gl.py` around lines 1225 - 1248, The log_array
method's signature and docstring don't reflect that None can be passed to remove
a previously logged array and the Warp-array detection is brittle; update the
type annotation of ViewerGL.log_array to accept Optional (None) for the array
parameter, update the docstring to document that passing None removes the named
array from _array_buffers, and change the Warp-array detection from
isinstance(array, wp.array) to a hasattr(array, "numpy") check (consistent with
ViewerRerun._to_numpy()) before converting to numpy; ensure you import
typing.Optional if needed and keep the rest of the conversion/contiguity logic
the same.

@eric-heiden
Copy link
Copy Markdown
Member

Nice addition! Having inline heatmap visualization for logged arrays is a great debugging tool for ViewerGL.

The main concern is that the per-cell rendering loop runs entirely in Python every frame, which will bottleneck the viewer at moderate array sizes.

Comment thread newton/_src/viewer/viewer_gl.py Outdated
for row in range(rows):
for col in range(cols):
value = float(array[row, col])
if np.isfinite(value):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🟡 This nested loop runs rows × cols Python-level draw calls every frame. For a 64×64 array that is 4,096 iterations of float(), np.isfinite(), _heatmap_color(), two ImVec2 constructions, and an add_rect_filled() call -- enough to drop below interactive frame rates.

_heatmap_color makes it worse by calling np.clip on a single scalar each iteration instead of plain min/max.

Suggested fix (two levels):

Level Change Effort
Quick win Replace np.clip(value, ...) in _heatmap_color with max(0.0, min(1.0, value)), and add a size cap that down-samples arrays larger than 128×128 before rendering Small
Proper fix Vectorize normalization and color mapping across the whole array in NumPy, rasterize into an RGBA texture, and draw it with a single imgui.image() call Medium

The proper fix moves all O(rows×cols) work out of Python and into one GPU texture upload per frame, which keeps the viewer responsive regardless of array size.

Quick-win example for _heatmap_color
# before
normalized = np.clip(value, 0.0, 1.0)

# after -- avoids numpy overhead on a single float
normalized = max(0.0, min(1.0, value))

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

Inline comments:
In `@newton/_src/viewer/viewer_gl.py`:
- Line 2398: The expression int(round(...)) is redundant in the rgb list
comprehension in viewer_gl.py; replace int(round(255.0 * ((1.0 - alpha) *
c0[channel] + alpha * c1[channel]))) with round(255.0 * ((1.0 - alpha) *
c0[channel] + alpha * c1[channel])) (and the same change for the other
occurrence around line 2403) so the list comprehension producing rgb uses
round(...) directly (round returns an int when no ndigits is given).
- Around line 1270-1279: The code in ViewerGL.log_array currently calls
np.squeeze which collapses singleton dimensions and can silently transpose
column vectors and accept higher-rank arrays with singleton dims; change the
logic to inspect the original array.ndim (or isinstance(array, wp.array)
converted shape) before calling np.squeeze: if original ndim == 0 reshape to
(1,1), if ndim == 1 reshape to (1, -1), if ndim == 2 leave as-is (do not squeeze
away singleton dims that would change orientation), otherwise raise the
ValueError; update the branch around the existing array_np handling (referencing
ViewerGL.log_array and the variables array and array_np) so that squeezing does
not alter the authoritative ndim-based behavior described by the error message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 3ccb3bdf-c68b-4225-9380-af382c49bd33

📥 Commits

Reviewing files that changed from the base of the PR and between cc137e8 and e33d05e.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • newton/_src/viewer/viewer_gl.py
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md

Comment thread newton/_src/viewer/viewer_gl.py
Comment thread newton/_src/viewer/viewer_gl.py Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

Inline comments:
In `@newton/_src/viewer/viewer_gl.py`:
- Around line 2432-2452: In _colorize_heatmap, when the input has no finite
values (finite_mask all False) currently you return (rgba, 0.0, 0.0); change it
to return NaN for the min/max so callers can detect "no finite values" (e.g.,
return np.nan or float('nan') for value_min and value_max) while still returning
the same rgba (filled with self._heatmap_nan_rgba and
ascontiguousarray(rgba[::-1])); update the early-return branch in the
_colorize_heatmap function to set value_min/value_max to NaN instead of 0.0 so
the overlay can render "—" or similar.
- Around line 2462-2465: The glGenTextures call in viewer_gl.py uses a scalar
gl.GLuint() which isn't a ctypes-compatible pointer; change to allocate an array
like the PBO code: create texture_id = (gl.GLuint * 1)() and call
gl.glGenTextures(1, texture_id), then use texture_id[0] (not texture_id.value)
for subsequent calls such as gl.glBindTexture and anywhere the texture ID is
read; update any references to texture_id.value to texture_id[0].
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: bd9661b5-6d1e-4486-b596-a28fdfe1441c

📥 Commits

Reviewing files that changed from the base of the PR and between e33d05e and 7f4315f.

📒 Files selected for processing (1)
  • newton/_src/viewer/viewer_gl.py

Comment thread newton/_src/viewer/viewer_gl.py Outdated
Comment thread newton/_src/viewer/viewer_gl.py Outdated
Implement texture-backed array heatmap rendering in ViewerGL.\n\nSupport scalar, 1-D, and 2-D array logging, including removal via\nNone, array downsampling, NaN handling, and texture lifecycle\ncleanup. Fold the follow-up fixes for column-vector handling, all-NaN\nmetadata display, and correct OpenGL texture allocation into the same\nchange so the PR history stays compact.
@han-xudong han-xudong force-pushed the feat/viewergl-log-array branch from 49fae6d to e126a2d Compare April 17, 2026 06:57
Copy link
Copy Markdown
Contributor

@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: 1

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

Inline comments:
In `@newton/_src/viewer/viewer_gl.py`:
- Around line 1256-1282: The ViewerGL.log_array change accepts None to remove a
logged array but the abstract and peer backends (ViewerBase.log_array,
ViewerFile.log_array, ViewerNull.log_array, ViewerRerun.log_array) still use the
old non-optional signature and lack removal semantics; update the abstract
signature in ViewerBase.log_array to accept Optional[wp.array[Any] | np.ndarray]
(or None) and update the docstring to document removal behavior, then modify
each peer override (ViewerFile.log_array, ViewerNull.log_array,
ViewerRerun.log_array) to accept None and implement the same removal/no-op
semantics (e.g., treat None by removing any stored buffer/metadata or returning
early) so the contract is consistent across all viewers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 8827e4b6-d866-47b1-b1ac-65909a32dfe1

📥 Commits

Reviewing files that changed from the base of the PR and between 49fae6d and e126a2d.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • newton/_src/viewer/viewer_gl.py
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md

Comment on lines +1256 to +1282
def log_array(self, name: str, array: wp.array[Any] | np.ndarray | None):
"""
Log a generic array for visualization (not implemented).
Log a numeric array for visualization.

Args:
name: Unique path/name for the array signal.
array: Array data to visualize.
array: Array data to visualize, or ``None`` to remove a previously
logged array.
"""
pass
if array is None:
self._array_buffers.pop(name, None)
self._array_dirty.discard(name)
self._delete_array_texture(name)
return

array_np = array.numpy() if isinstance(array, wp.array) else np.asarray(array)
array_np = np.asarray(array_np, dtype=np.float32)

if array_np.ndim == 0:
array_np = array_np.reshape(1, 1)
elif array_np.ndim == 1:
array_np = array_np.reshape(1, -1)
elif array_np.ndim != 2:
raise ValueError("ViewerGL.log_array only supports scalar, 1-D, or 2-D arrays.")

self._array_buffers[name] = np.ascontiguousarray(array_np)
self._array_dirty.add(name)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Abstract base and peer backends still don't accept None — contract divergence.

The override now accepts None to remove a logged array, but ViewerBase.log_array (newton/_src/viewer/viewer.py:1205), ViewerFile.log_array, ViewerNull.log_array, and ViewerRerun.log_array all keep the old array: wp.array[Any] | np.ndarray signature and have no removal semantics. Code written against the abstract contract either can't type-safely pass None, or will silently no-op / raise on the other backends. Please align the abstract signature/docstring (and at minimum make the peer overrides treat None as a no-op removal) so removal behavior is portable across viewers.

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

In `@newton/_src/viewer/viewer_gl.py` around lines 1256 - 1282, The
ViewerGL.log_array change accepts None to remove a logged array but the abstract
and peer backends (ViewerBase.log_array, ViewerFile.log_array,
ViewerNull.log_array, ViewerRerun.log_array) still use the old non-optional
signature and lack removal semantics; update the abstract signature in
ViewerBase.log_array to accept Optional[wp.array[Any] | np.ndarray] (or None)
and update the docstring to document removal behavior, then modify each peer
override (ViewerFile.log_array, ViewerNull.log_array, ViewerRerun.log_array) to
accept None and implement the same removal/no-op semantics (e.g., treat None by
removing any stored buffer/metadata or returning early) so the contract is
consistent across all viewers.

@han-xudong
Copy link
Copy Markdown
Author

@eric-heiden Thanks for the thoughtful review and for calling out the Python-side rendering cost here. You were absolutely right that the per-cell draw loop would become a bottleneck at moderate array sizes.

I reworked the heatmap path toward the “proper fix” you suggested: the data is now normalized and color-mapped in NumPy, downsampled to the displayed resolution, uploaded as an RGBA texture, and rendered with a single imgui.image() call instead of one Python draw call per cell. That also let me add texture cleanup when arrays are replaced or removed.

I also folded in a few follow-up fixes that came up during review, including some CodeRabbit suggestions:

  • preserving 2-D column vectors as column heatmaps instead of collapsing them through squeeze()
  • reporting min -- / max -- for all-non-finite inputs rather than showing misleading 0 / 0
  • fixing the OpenGL texture allocation call to use the correct pyglet ctypes pattern
  • cleaning up a small redundant round() cast nit

Thanks again for the push here. The performance concern was the right one to fix properly.

Comment thread newton/_src/viewer/viewer_gl.py Outdated
if not np.any(finite_mask):
rgba = np.empty((*array.shape, 4), dtype=np.uint8)
rgba[...] = self._heatmap_nan_rgba
return np.ascontiguousarray(rgba[::-1]), float("nan"), float("nan")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🟡 The rgba[::-1] here flips the heatmap vertically before upload, so the top row of the source array is rendered at the bottom of the image. imgui.image() with the default UVs already maps texture row 0 to the top of the rect, so this inverts the array relative to the usual NumPy/Matplotlib orientation. The same reversal is present in the non-NaN return below. Unless origin='lower' is intentional, return np.ascontiguousarray(rgba) in both places, or do the flip through uv0/uv1 and document why.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@eric-heiden Thanks, good catch. You were right that the extra rgba[::-1] was flipping the heatmap vertically. I removed the reversal in both return paths so the texture upload now preserves the usual array orientation.

@han-xudong han-xudong requested a deployment to external-pr-approval April 22, 2026 02:49 — with GitHub Actions Waiting
@han-xudong han-xudong requested a deployment to external-pr-approval April 22, 2026 02:49 — with GitHub Actions Waiting
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