Skip to content

update_node_attrs signal emission crashes on pl.Array columns with GraphArrayView attached #294

@cmalinmayor

Description

@cmalinmayor

Human generated update: This is related to the API change in 9bfbf8f change single node|edge api - passing in the lists is now actually wrong and leads to this specific error. So, might just be that we need to update our callers, but I didn't want to go down the rabbit hole far enough to verify.

Warning: Claude generated summary, I was in the middle of something else and didn't have time to verify, but wanted to make the issue so I didn't forget. Hopefully this points us in the right direction.

Summary

After #287 (Fix graphview signals, 14b92ec), calling update_node_attrs on a GraphView that has a GraphArrayView connected crashes with polars.exceptions.InvalidOperationError: cannot cast Array to a different width when the updated attributes include a pl.Array-typed column (e.g. a position column of type Array(Float64, 2)).

This did not happen before #287 because update_node_attrs did not read back attribute values for the signal payload.

Root cause

GraphView.update_node_attrs (line 703–717 in _graph_view.py) builds signal_keys from DEFAULT_ATTR_KEYS plus attrs.keys(). It then calls node_attrs(attr_keys=signal_keys) to capture old/new values for the node_updated signal.

The problem is in _list_to_pl_series (_rustworkx_graph.py:71–82). When a pl.Array(Float64, 2) column stores a value like [50.0, 50.0], rustworkx returns it as [[50.0, 50.0]] for a single-node query. np.asarray([[50.0, 50.0]]) produces shape (1, 2), and pl.Series(values=..., dtype=Array(Float64, 2)) handles this correctly.

However, when the value was stored via __setitem__ as graph.nodes[node][attr] = [[50.0, 50.0]] (double-wrapped — the caller wraps the value in a list as the tracksdata API expects), rustworkx returns [[[50.0, 50.0]]] for a single node. np.asarray([[[50.0, 50.0]]]) produces shape (1, 1, 2), and polars infers this as Array(Float64, (1, 2)), then fails to cast it to the schema's Array(Float64, 2).

Reproduction

import numpy as np
import polars as pl
from tracksdata.graph import RustWorkXGraph

# Create a graph with an Array-typed position column
graph = RustWorkXGraph()
graph.add_node_attr_key("t", dtype=pl.Int64, default_value=0)
graph.add_node_attr_key("pos", dtype=pl.Array(pl.Float64, 2), default_value=[0.0, 0.0])
graph.add_node_attr_key("mask", dtype=pl.Object)
graph.add_node_attr_key("bbox", dtype=pl.Array(pl.Int64, 4), default_value=[0, 0, 0, 0])

# Add a node
from tracksdata.nodes import Mask
mask = Mask(np.ones((3, 3), dtype=bool), bbox=np.array([0, 0, 3, 3]))
graph.add_node(attrs={"t": 0, "pos": [50.0, 50.0], "mask": mask, "bbox": [0, 0, 3, 3]})

# Create a GraphView + attach a GraphArrayView (this connects node_updated signal)
from tracksdata.graph import GraphView
from tracksdata.array import GraphArrayView
view = GraphView(graph)
gav = GraphArrayView(graph=view, shape=(1, 100, 100), attr_key="node_id", offset=0)

# This crashes: update an attribute on the node
# The signal emission path tries to read back "pos" and hits the polars Array width error
node_id = list(view.node_ids())[0]
view.update_node_attrs(attrs={"pos": [[60.0, 60.0]]}, node_ids=[node_id])

Error:

polars.exceptions.InvalidOperationError: cannot cast Array to a different width

Where it matters

This breaks any funtracks workflow where RegionpropsAnnotator updates node attributes (pos, area, etc.) after a GraphArrayView is created — for example, AddNode followed by annotator recomputation, or enable_features(["area"]).

Environment

  • tracksdata: main at or after 14b92ec
  • polars: 1.40.1
  • Python: 3.12

Possible fixes

  1. In _list_to_pl_series: Handle the case where np.asarray(values) produces more dimensions than expected for the schema's pl.Array type. For example, squeeze or reshape to (n_rows, width).

  2. In GraphView.update_node_attrs: Only include attrs.keys() in signal_keys — don't also include the default spatial keys. The signal receivers can fetch any additional attrs they need from the graph directly. (This would also avoid the separate issue where custom bbox_attr_key like nuc_bbox is missing from the signal payload.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions