Skip to content

Conversation

adamamer20
Copy link
Member

@adamamer20 adamamer20 commented Aug 30, 2025

This PR simplifies the mental model and usage of AgentSet / AgentSetRegistry by:

  1. giving each AgentSet a name and enabling flexible lookup (by name, type, or index), and
  2. enforcing separation of concerns: the registry manages sets, while each set manages its own pl.DataFrame.

This yields a friendlier, clearer and safer API for everyday tasks (query one set, broadcast across sets).


What changed

1) Registry scope

  • Before: AgentSetRegistry.df returned a mapping {AgentSet → pl.DataFrame} and high-level helpers like get often operated directly on the sets’ underlying DataFrames, which was easy to misread as “registry-wide” operations.
  • After: the registry does not expose a df interface anymore, nor any “agents” iterator. You always work per set (via model.sets[...]) and then use that set’s API (.df, .get, .set, …). The registry orchestrates and routes, but never acts like a DataFrame holder.

Before → After: accessing DataFrames

Get one set’s frame

# BEFORE (The syntax is not very clear here, what are we obtaining at each step?)
frames = model.sets.df                   # {AgentSet -> pl.DataFrame} 
citizens_df = frames[model.sets[Citizens]]

# AFTER (clearly the df of the set)
citizens_df = model.sets["Citizens"].df

Iterate multiple sets’ frames

# BEFORE
frames = model.sets.df  # {AgentSet -> pl.DataFrame}

# AFTER (More flexibility in obtaining dfs)
frames = {s: s.df for s in model.sets}          # key by set object
# or, if you prefer labels:
frames_by_name = {s.name: s.df for s in model.sets}

2) Named sets & flexible lookup

model.sets["Citizens"]   # by name (string)
model.sets[Citizens]     # by type (class)
model.sets[0]            # by index (int)

Summary by CodeRabbit

  • New Features

    • Agent sets are now registry-centric: access via model.sets["Name"], batch actions via sets.do(...).
    • AgentSet supports names and renaming; enhanced get/set, add/remove, membership, and operator behaviors.
    • Space APIs accept AgentSet and collections for move/place/swap/neighbors/distance/direction operations.
    • DataCollector agent_reporters support strings, collections of strings, and callables at registry or set level, with per-set column suffixing.
    • Models track steps via m.steps; step dispatch uses registry; current_id exposed.
  • Documentation

    • User guides and tutorials updated to the registry-based workflow and new DataCollector patterns.
  • Refactor

    • Consolidated APIs toward AgentSet/registry abstractions for consistency.

…ement and enhance rename method for better delegation to AgentsDF.
…gent sets as a list for multiple matches and improve error messaging for better clarity.
…o 146-enhancement-consider-using-a-key-based-structure-for-agentsets-instead-of-list-in-agentsdf
…ency and clarity in the abstract class naming.
…proved consistency and clarity; enhance error messaging in __getitem__ and rename methods for better readability.
Copy link

codecov bot commented Aug 30, 2025

Codecov Report

❌ Patch coverage is 82.23443% with 97 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.85%. Comparing base (51c54cd) to head (dccc834).

Files with missing lines Patch % Lines
mesa_frames/concrete/agentsetregistry.py 86.08% 38 Missing ⚠️
mesa_frames/concrete/datacollector.py 48.64% 38 Missing ⚠️
mesa_frames/concrete/agentset.py 86.66% 8 Missing ⚠️
mesa_frames/abstract/agentsetregistry.py 88.52% 7 Missing ⚠️
mesa_frames/abstract/agentset.py 90.19% 5 Missing ⚠️
mesa_frames/abstract/space.py 93.75% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #172      +/-   ##
==========================================
- Coverage   92.54%   88.85%   -3.69%     
==========================================
  Files          14       14              
  Lines        1717     1983     +266     
==========================================
+ Hits         1589     1762     +173     
- Misses        128      221      +93     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…nt set types, improving performance and clarity in KeyError messages.
…arity in AGENTS.md, agents.py, agentset.py, and test_sets_accessor.py; improve formatting in test cases.
…r-agentsets-instead-of-list-in-agentsdf' of https://github.com/projectmesa/mesa-frames into 146-enhancement-consider-using-a-key-based-structure-for-agentsets-instead-of-list-in-agentsdf
…cessor; update parameter descriptions for improved understanding.
…update conflict resolution and mode descriptions for improved understanding.
…cessor and AgentSetsAccessor; update default values and descriptions for parameters.
pre-commit-ci bot and others added 18 commits September 15, 2025 08:44
…r-agentsets-instead-of-list-in-agentsdf' of https://github.com/projectmesa/mesa-frames into 146-enhancement-consider-using-a-key-based-structure-for-agentsets-instead-of-list-in-agentsdf
…o 146-enhancement-consider-using-a-key-based-structure-for-agentsets-instead-of-list-in-agentsdf
…files

These files contained outdated implementations of the AgentSetsAccessor and AgentsDF classes, which are no longer in use. Their removal helps to clean up the codebase and reduce confusion regarding the current architecture of the mesa-frames library.
@adamamer20 adamamer20 marked this pull request as ready for review September 19, 2025 14:43
@adamamer20
Copy link
Member Author

@coderabbitai review

Copy link
Contributor

coderabbitai bot commented Sep 19, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

coderabbitai bot commented Sep 19, 2025

Walkthrough

Refactors agent-set APIs toward a registry-centric model: abstract and concrete AgentSet/Registry interfaces redesigned; Space accepts AbstractAgentSet inputs; DataCollector reporter semantics updated; Model step delegates via registry and adds step counter; docs and examples updated; tests restructured to new APIs, including extensive new registry tests and removal of legacy suite.

Changes

Cohort / File(s) Summary
Docs: registry-centric usage
docs/general/user-guide/1_classes.md, docs/general/user-guide/2_introductory-tutorial.ipynb, docs/general/user-guide/4_datacollector.ipynb
Update examples to use model.sets registry, set-scoped DataFrames, registry-based stepping and reporters; switch triggers to m.steps; clarify agent reporters.
Examples: Sugarscape (polars)
examples/sugarscape_ig/ss_polars/agents.py, examples/sugarscape_ig/ss_polars/model.py
Constrain sugar updates to in-set agents; introduce named main_set and termination when main set empty.
Abstract APIs: AgentSet/Registry/DataCollector/Space/Types
mesa_frames/abstract/agentset.py, mesa_frames/abstract/agentsetregistry.py, mesa_frames/abstract/datacollector.py, mesa_frames/abstract/space.py, mesa_frames/types_.py
AgentSet now CopyMixin-based with name/model/random/space/index, item ops, rename/set/remove/do; Registry shifts to selector-based set operations, keys/items/values, ids view, rename/replace; Space methods accept AbstractAgentSet inputs; DataCollector docstring clarifies agent_reporters; new type aliases (AgentSetSelector, KeyBy, coordinates).
Concrete implementations: AgentSet/Registry/DataCollector/Model
mesa_frames/concrete/agentset.py, mesa_frames/concrete/agentsetregistry.py, mesa_frames/concrete/datacollector.py, mesa_frames/concrete/model.py
AgentSet adds naming, rename, masked do/get/remove, inplace controls; Registry gains selector-based add/remove/rename/replace/do, ids cache, name generation; DataCollector expands agent_reporters handling and Postgres column validation; Model adds step wrapper with steps counter, removes set-type queries, step delegates via registry.do.
Tests: setup and suites
tests/conftest.py, tests/test_agents.py (deleted), tests/test_agentset.py, tests/test_agentsetregistry.py, tests/test_datacollector.py, tests/test_grid.py
Enable runtime type checking; remove legacy agents tests; add rename test for AgentSet; add comprehensive AgentSetRegistry tests; update agent_reporters signatures to accept sets; adjust ID aggregation to iterate registry.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Model
  participant Registry as AgentSetRegistry
  participant SetA as AgentSet A
  participant SetB as AgentSet B

  User->>Model: step()
  note over Model: _wrapped_step increments m.steps
  Model->>Registry: do("step")
  Registry->>SetA: step()
  SetA-->>Registry: None
  Registry->>SetB: step()
  SetB-->>Registry: None
  Registry-->>Model: None
  Model-->>User: return
Loading
sequenceDiagram
  autonumber
  participant Caller
  participant Space
  participant Reg as AbstractAgentSetRegistry
  participant ASet as AbstractAgentSet

  Caller->>Space: move_agents(agents=ASet, pos=...)
  Space->>Space: _get_ids_srs(ASet)
  note over Space: Validate ids ∈ Reg.ids
  Space->>Space: _place_or_move_agents_to_cells(...)
  Space-->>Caller: Self
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

Possibly related PRs

Suggested labels

feature

Suggested reviewers

  • EwoutH
  • Ben-geo

Poem

I hop through sets by name, not guess—
A tidy registry, I must confess.
With steps that tick and IDs aligned,
I nibble data, well-defined.
Rename, replace—so sleek, so neat!
Carrot-shaped reports? A perfect treat. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.79% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title concisely describes the core changes—introducing named AgentSets, enabling flexible lookup, and emphasizing separation of concerns—so it is directly related to the changeset and easy for reviewers to scan. It focuses on the main user-facing design change without extraneous detail.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 146-enhancement-consider-using-a-key-based-structure-for-agentsets-instead-of-list-in-agentsdf

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (11)
mesa_frames/concrete/datacollector.py (1)

458-462: Fix “postgress” typos and include batch as a required column.

Typos reduce discoverability; missing batch validation can lead to runtime INSERT errors.

Apply this diff:

-                self._validate_postgress_table_exists(conn)
-                self._validate_postgress_columns_exists(conn)
+                self._validate_postgres_table_exists(conn)
+                self._validate_postgres_columns_exists(conn)
@@
-    def _validate_postgress_table_exists(self, conn: connection):
+    def _validate_postgres_table_exists(self, conn: connection):
@@
-    def _validate_postgress_columns_exists(self, conn: connection):
+    def _validate_postgres_columns_exists(self, conn: connection):

And update required columns:

-        required_columns = {
-            "step": "Integer",
-            "seed": "Varchar",
-        }
+        required_columns = {
+            "step": "Integer",
+            "seed": "Varchar",
+            "batch": "Integer",
+        }

Also applies to: 465-478, 479-496

mesa_frames/types_.py (1)

45-53: Runtime NameError when pandas is missing (pd.DataFrame referenced).

PolarsDataFrameInput refers to pd.DataFrame directly. If pandas is unavailable, pd is undefined.

Use the alias defined above to avoid runtime failures:

 PolarsDataFrameInput = Union[
     Mapping[
         str, Union[Sequence[object], Mapping[str, Sequence[object]], pl.Series, Any]
     ],
     Sequence[Any],
     np.ndarray,
     ArrowTable,
-    pd.DataFrame,
+    PandasDataFrame,
 ]
mesa_frames/abstract/space.py (9)

232-258: swap_agents can mis-pair agents due to ordering; reindex masked DFs before reassignment.

_df_get_masked_df may not preserve the mask order. Without explicit reindexing, agent IDs can be swapped against the wrong counterparts.

Apply:

         agents0_df = obj._df_get_masked_df(
             obj._agents, index_cols="agent_id", mask=ids0
         )
+        agents0_df = obj._df_reindex(agents0_df, ids0, "agent_id")
         agents1_df = obj._df_get_masked_df(
             obj._agents, index_cols="agent_id", mask=ids1
         )
+        agents1_df = obj._df_reindex(agents1_df, ids1, "agent_id")
         agents0_df = obj._df_set_index(agents0_df, "agent_id", ids1)
         agents1_df = obj._df_set_index(agents1_df, "agent_id", ids0)

491-527: Indexing a generic Collection breaks for sets/generators; normalize to list and guard mixed types.

agents[0] on Collection raises for non-sequences; also allows mixed container types silently.

-        elif isinstance(agents, Collection) and (
-            isinstance(agents[0], AbstractAgentSet)
-            or isinstance(agents[0], AbstractAgentSetRegistry)
-        ):
-            ids = []
-            for a in agents:
+        elif isinstance(agents, Collection) and not isinstance(agents, (str, bytes)):
+            seq = list(agents)
+            if not seq:
+                return self._srs_constructor([], name="agent_id", dtype="uint64")
+            ids: list[Series] = []
+            for a in seq:
                 if isinstance(a, AbstractAgentSet):
                     ids.append(
                         self._srs_constructor(
                             self._df_index(a.df, "unique_id"),
                             name="agent_id",
                             dtype="uint64",
                         )
                     )
-                elif isinstance(a, AbstractAgentSetRegistry):
+                elif isinstance(a, AbstractAgentSetRegistry):
                     ids.append(
                         self._srs_constructor(a.ids, name="agent_id", dtype="uint64")
                     )
+                else:
+                    raise TypeError("Mixed collection of agent containers is not supported")
             return self._df_concat(ids, ignore_index=True)

1345-1349: Broaden get_neighbors(agents=...) to accept AbstractAgentSet, matching base API.

Current signature narrows accepted types vs Space.get_neighbors and _get_df_coords.

-        agents: IdsLike
-        | AbstractAgentSetRegistry
-        | Collection[AbstractAgentSetRegistry]
+        agents: IdsLike
+        | AbstractAgentSet
+        | AbstractAgentSetRegistry
+        | Collection[AbstractAgentSet]
+        | Collection[AbstractAgentSetRegistry]
         | None = None,

1362-1369: Broaden get_neighborhood(agents=...) to accept AbstractAgentSet.

Keep subclass surface consistent with base and helpers.

-        agents: IdsLike
-        | AbstractAgentSetRegistry
-        | Collection[AbstractAgentSetRegistry]
-        | None = None,
+        agents: IdsLike
+        | AbstractAgentSet
+        | AbstractAgentSetRegistry
+        | Collection[AbstractAgentSet]
+        | Collection[AbstractAgentSetRegistry]
+        | None = None,

1372-1392: Avoid isinstance(..., ArrayLike): use Sequence checks to prevent runtime TypeError.

ArrayLike is a typing alias and not safe for isinstance. Use Sequence and exclude str/bytes.

-        if __debug__:
-            if isinstance(radius, ArrayLike):
+        if __debug__:
+            if isinstance(radius, Sequence) and not isinstance(radius, (str, bytes)):
                 if len(radius) != len(pos_df):
                     raise ValueError(
                         "The length of the radius sequence must be equal to the number of positions/agents"
                     )
@@
-        if isinstance(radius, ArrayLike):
+        if isinstance(radius, Sequence) and not isinstance(radius, (str, bytes)):
             radius_srs = self._srs_constructor(radius, name="radius")
             radius_df = self._srs_to_df(radius_srs)
             max_radius = radius_srs.max()
         else:
             max_radius = radius
@@
-        if isinstance(radius, ArrayLike):
+        if isinstance(radius, Sequence) and not isinstance(radius, (str, bytes)):
             radius_df = self._df_rename_columns(
                 self._df_concat([pos_df, radius_df], how="horizontal"),
                 self._pos_col_names + ["radius"],
                 self._center_col_names + ["max_radius"],
             )

Also, this line likely needs stop= (see next comment):

-        range_df = self._srs_to_df(
-            self._srs_range(name="radius", start=1, end=max_radius + 1)
-        )
+        range_df = self._srs_to_df(
+            self._srs_range(name="radius", start=1, stop=max_radius + 1)
+        )

Also applies to: 1486-1502


1831-1843: Don’t mutate tuple/sequence in-place; convert to list first. Also prefer Sequence over ArrayLike in isinstance.

In-place assignment on tuples raises; using Sequence keeps runtime-safe checks.

-        elif isinstance(pos, ArrayLike) and len(pos) == len(self._dimensions):
+        elif isinstance(pos, Sequence) and not isinstance(pos, (str, bytes)) and len(pos) == len(self._dimensions):
             # This means that the sequence is already a sequence where each element is the
             # sequence of coordinates for dimension i
-            for i, c in enumerate(pos):
+            pos_list = list(pos)
+            for i, c in enumerate(pos_list):
                 if isinstance(c, slice):
                     start = c.start if c.start is not None else 0
                     step = c.step if c.step is not None else 1
                     stop = c.stop if c.stop is not None else self._dimensions[i]
-                    pos[i] = self._srs_range(start=start, stop=stop, step=step)
+                    pos_list[i] = self._srs_range(start=start, stop=stop, step=step)
             return self._df_constructor(
-                data=[pos],
+                data=[pos_list],
                 columns=self._pos_col_names,
                 dtypes={col: int for col in self._pos_col_names},
             )

1372-1392: ArrayLike is a PEP 604 union — replace isinstance(..., ArrayLike) checks

types_.py (line 85) defines ArrayLike = ndarray | Series | Sequence — this is a runtime UnionType and cannot be passed to isinstance; those checks will raise TypeError.
Affected call sites: mesa_frames/abstract/space.py:1373, 1382, 1486, 1830–1831.
Fix: replace isinstance(x, ArrayLike) with isinstance(x, (ndarray, Series, Sequence)) or add an explicit is_arraylike(obj) helper that checks the concrete types (and import them).


1298-1339: LGTM — distance/direction normalization good; fix inconsistent signatures.

Abstract declarations (mesa_frames/abstract/space.py:350, 858) accept radius: int | float | Sequence[...] | ArrayLike, while later definitions (mesa_frames/abstract/space.py:1341, 1360) narrow radius to int | Sequence[int]; align the signatures or document/justify the narrowing to avoid API/typing breakage.


1390-1392: Fix _srs_range keyword mismatch (end vs stop)

abstract/mixin.py defines _srs_range(..., end: int, ...); space.py has calls using both end= (line ~1390) and stop= (line ~1838). Update the call at mesa_frames/abstract/space.py:1838 to use end= (or change the method signature to accept stop=) so all call sites and the implementation use the same keyword to avoid a TypeError at runtime.

🧹 Nitpick comments (37)
tests/conftest.py (1)

5-7: Determinism: set the env var unconditionally (docstring vs behavior).

Docstring says we ensure type checking is enabled; setdefault won’t override an existing “0”. Set it explicitly to avoid nondeterminism across dev machines/CI.

-os.environ.setdefault("MESA_FRAMES_RUNTIME_TYPECHECKING", "1")
+os.environ["MESA_FRAMES_RUNTIME_TYPECHECKING"] = "1"

Also applies to: 11-11

tests/test_grid.py (1)

15-17: Handle empty registries in get_unique_ids.

pl.concat([]) raises; make helper robust for models with no sets.

-    series_list = [aset["unique_id"].cast(pl.UInt64) for aset in model.sets]
-    return pl.concat(series_list)
+    series_list = [aset["unique_id"].cast(pl.UInt64) for aset in model.sets]
+    return pl.concat(series_list) if series_list else pl.Series("unique_id", [], dtype=pl.UInt64)
examples/sugarscape_ig/ss_polars/agents.py (1)

38-46: Scope fix LGTM; add dtype guard and micro‑simplify.

Cast ids to UInt64 to match unique_id dtype and combine filters to avoid a temporary mask.

-        # Only consider cells currently occupied by agents of this set
-        cells = self.space.cells.filter(pl.col("agent_id").is_not_null())
-        mask_in_set = cells["agent_id"].is_in(self.index)
-        if mask_in_set.any():
-            cells = cells.filter(mask_in_set)
-            ids = cells["agent_id"]
+        # Only consider cells occupied by agents of this set
+        cells = self.space.cells.filter(
+            pl.col("agent_id").is_not_null() & pl.col("agent_id").is_in(self.index)
+        )
+        if len(cells) > 0:
+            ids = cells["agent_id"].cast(pl.UInt64)
             self[ids, "sugar"] = (
                 self[ids, "sugar"] + cells["sugar"] - self[ids, "metabolism"]
             )
examples/sugarscape_ig/ss_polars/model.py (1)

36-40: Main-set lookup can break after rename; keep a direct reference.

Storing only the name is brittle. Persist the object for termination checks.

         main_set = agent_type(self, n_agents, initial_sugar, metabolism, vision)
         self.sets += main_set
         self._main_set_name = main_set.name
+        self._main_set = main_set  # robust to renames
docs/general/user-guide/2_introductory-tutorial.ipynb (1)

77-79: Docs: named-set key may vary with canonicalization.

If the set name collides, it becomes MoneyAgents_1. Consider noting this or deriving the name programmatically in examples to avoid KeyError in reused kernels.

- "total_wealth": lambda m: m.sets["MoneyAgents"].df["wealth"].sum()
+ "total_wealth": lambda m: m.sets["MoneyAgents"].df["wealth"].sum()  # if renamed, use next(iter(m.sets)).df
mesa_frames/abstract/datacollector.py (1)

94-99: Doc update aligns with new reporter semantics.

Clear and helpful. Consider qualifying types in text (e.g., pl.Series/pl.DataFrame) for readers.

docs/general/user-guide/4_datacollector.ipynb (1)

123-125: Docs: consistent MoneyAgents lookups; add note for renamed sets.

Using m.sets["MoneyAgents"] is fine; add a brief note that in multi-set demos names may canonicalize (MoneyAgents_1), or show an alternative using the first set to keep examples copy-paste proof.

Also applies to: 201-203, 252-254, 293-295

docs/general/user-guide/1_classes.md (2)

30-33: Clarify naming/defaults when adding sets.

Suggest adding a sentence that +=/add() registers the set under its default name (class name) unless explicitly overridden, and show retrieval by that name for consistency with later examples.


80-92: LGTM: DataCollector example matches new registry/set-centric reporters.

Minor suggestion: mirror the trigger example from tests/docs to use m.steps (avoid private _steps).

mesa_frames/concrete/model.py (1)

97-101: Duplicate steps property; keep a single definition.

Two identical @property steps blocks exist. Retain one to prevent confusion and doc tool noise.

Apply this diff to remove the earlier duplicate:

@@
-    @property
-    def steps(self) -> int:
-        """Get the current step count."""
-        return self._steps

Also applies to: 132-142

tests/test_datacollector.py (2)

11-13: Prefer public steps over private _steps in trigger.

Use model.steps % 2 == 0 to avoid reaching into internals.

Apply this diff:

-def custom_trigger(model):
-    return model._steps % 2 == 0
+def custom_trigger(model):
+    return model.steps % 2 == 0

733-734: Fix assertion message to match expected count.

Message says 4 files but the assertion expects 8.

Apply this diff:

-            assert len(created_files) == 8, (
-                f"Expected 4 files, found {len(created_files)}: {created_files}"
+            assert len(created_files) == 8, (
+                f"Expected 8 files, found {len(created_files)}: {created_files}"
             )
mesa_frames/concrete/datacollector.py (3)

181-213: Deduplicate _is_str_collection and make it exception-free.

Define it once (e.g., as a @staticmethod) and avoid broad try/except.

Apply this minimal change in-place:

-        def _is_str_collection(x: Any) -> bool:
-            try:
-                from collections.abc import Collection
-
-                if isinstance(x, str):
-                    return False
-                return isinstance(x, Collection) and all(isinstance(i, str) for i in x)
-            except Exception:
-                return False
+        from collections.abc import Collection
+        def _is_str_collection(x: Any) -> bool:
+            return (
+                not isinstance(x, str)
+                and isinstance(x, Collection)
+                and all(isinstance(i, str) for i in x)
+            )

452-456: Tighten None check and fix message grammar.

Apply this diff:

-        if self._storage != "memory" and self._storage_uri == None:
+        if self._storage != "memory" and self._storage_uri is None:
             raise ValueError(
-                "Please define a storage_uri to if to be stored not in memory"
+                "Please define storage_uri to store data outside memory"
             )

497-523: Prefer parametrized identifiers or safe quoting for identifiers.

While schema/table come from controlled values, consider psycopg2 sql.Identifier to guard against edge cases in names.

tests/test_agentsetregistry.py (3)

271-275: Replace tautological assertion with a meaningful check.

assert reg[1].name == reg[1].name is a no-op. Verify mapping coherence or the replacement instead.

Apply this diff:

-        assert reg[1].name == reg[1].name
+        assert reg.get(reg[1].name) is reg[1]

313-313: Adopt Ruff hint for first value retrieval.

Use an iterator instead of list materialization.

Apply this diff:

-        assert list(reg.values())[0] is reg[0]
+        assert next(iter(reg.values())) is reg[0]

362-374: Strengthen deep-copy assertion in __add__ test.

Optionally assert identity inequality to ensure copies, e.g., reg_new[0] is not a1.

mesa_frames/types_.py (3)

19-26: Define PandasSeries when pandas is available.

When pandas is installed, PandasSeries isn’t defined, leading to asymmetric aliases vs. the ImportError path.

Apply this diff:

 try:
     import pandas as pd

     PandasDataFrame = pd.DataFrame
+    PandasSeries = pd.Series
 except ImportError:
     # just give us a class so annotations don’t break
     PandasDataFrame = type("PandasDataFrame", (), {})
     PandasSeries = type("PandasSeries", (), {})

112-126: Sort all (RUF022) and keep it stable.

Minor style nit, improves diffs and tooling friendliness.

Apply this diff:

 __all__ = [
-    # common
-    "DataFrame",
-    "Series",
-    "Index",
-    "BoolSeries",
-    "Mask",
-    "AgentMask",
-    "IdsLike",
-    "ArrayLike",
-    "KeyBy",
-    # selectors
-    "AbstractAgentSetSelector",
-    "AgentSetSelector",
+    # common
+    "AgentMask",
+    "ArrayLike",
+    "BoolSeries",
+    "DataFrame",
+    "IdysLike",  # kept spelling per codebase, but see next comment about 'Continuous'
+    "Index",
+    "KeyBy",
+    "Mask",
+    "Series",
+    # selectors
+    "AbstractAgentSetSelector",
+    "AgentSetSelector",
 ]

Note: If you prefer to keep a specific grouping, at least alphabetize within groups.


138-153: Spelling: “Continous” → “Continuous” for coordinate aliases.**

Typo appears across type aliases (Coordinate/Coordinates). Consider adding new “Continuous*” aliases and keeping the old names as deprecated aliases to avoid churn.

Example:

-ContinousCoordinate = float | Sequence[float] | DataFrame
+ContinuousCoordinate = float | Sequence[float] | DataFrame
+ContinousCoordinate = ContinuousCoordinate  # backward-compat alias

-ContinousCoordinates = (
-    ContinousCoordinate
+ContinuousCoordinates = (
+    ContinuousCoordinate
     | Sequence[float | Sequence[float]]
-    | Collection[ContinousCoordinate]
+    | Collection[ContinuousCoordinate]
 )
+ContinousCoordinates = ContinuousCoordinates  # backward-compat alias
mesa_frames/concrete/agentset.py (5)

94-97: Docstring claims snake_case conversion, but code doesn’t implement it.

Either implement the normalization or remove the statement to avoid misleading users.

Option A (implement):

-            Will be converted to snake_case if in camelCase.
+            Will be converted to snake_case if in camelCase.

Add simple normalizer:

@@
-        self._name = name if name is not None else self.__class__.__name__
+        proposed = name if name is not None else self.__class__.__name__
+        self._name = self._to_snake(proposed)

and helper:

@@
 class AgentSet(AbstractAgentSet, PolarsMixin):
@@
+    @staticmethod
+    def _to_snake(s: str) -> str:
+        return (
+            pl.Series([s])
+            .str.to_lowercase()
+            .str.replace_all(r"(?<!^)([A-Z])", r"_$1")
+            .str.replace_all(r"[^a-z0-9_]+", "_")
+            .str.replace_all(r"_+", "_")
+            .str.strip_chars("_")
+            .item()
+        )

Option B (remove the claim): delete the sentence in the docstring.


131-149: Avoid bare excepts; scope failures precisely in rename().

Catching Exception obscures real bugs. Limit to expected errors and preserve tracebacks where appropriate.

Apply this diff:

-        try:
+        try:
             if self in self.model.sets:
@@
-        except Exception:
+        except (AttributeError, KeyError, ValueError):
             # Fall back to local rename if delegation fails
             obj._name = new_name
             return obj

447-448: Use boolean ‘and’ instead of bitwise ‘&’.

Bitwise ops on bools work but are non-idiomatic and can surprise readers.

-        if duplicates_allowed & keep_first_only:
+        if duplicates_allowed and keep_first_only:

681-689: Route name assignment through rename() when attached to a registry.

Directly setting _name can violate registry-level uniqueness guarantees. Delegate when possible.

 @name.setter
 def name(self, value: str) -> None:
-    """Set the name of the AgentSet."""
-    self._name = value
+    """Set the name of the AgentSet, delegating to the registry if present."""
+    try:
+        if self in self.model.sets:
+            self.rename(value, inplace=True)
+            return
+    except Exception:
+        pass
+    self._name = value

595-600: Super getattr call is unused.

You call super().getattr(key) but discard its result. If the intention is only to trigger the debug guard, add a brief comment. Otherwise, remove the call.

-        super().__getattr__(key)
+        # Trigger AbstractAgentSet debug guard for '_df' access; ignore return.
+        super().__getattr__(key)
         return self._df[key]
mesa_frames/concrete/agentsetregistry.py (3)

149-160: Drop unused variable ‘single’.

Minor cleanup; keeps lints quiet.

-            pairs_idx: list[tuple[int, str]] = [(_resolve_one(target), new_name)]
-            single = True
+            pairs_idx: list[tuple[int, str]] = [(_resolve_one(target), new_name)]
@@
-            pairs_idx = [(_resolve_one(k), v) for k, v in target.items()]
-            single = False
+            pairs_idx = [(_resolve_one(k), v) for k, v in target.items()]
@@
-            pairs_idx = [(_resolve_one(k), v) for k, v in target]
-            single = False
+            pairs_idx = [(_resolve_one(k), v) for k, v in target]

191-199: Use zip(..., strict=True) to avoid silent truncation.

Ensures pairs_idx and target_sets lengths match.

-        for aset, (_idx, desired) in zip(target_sets, pairs_idx):
+        for aset, (_idx, desired) in zip(target_sets, pairs_idx, strict=True):

246-256: Prefer raising from None for not-found lookups.

Produces cleaner tracebacks and satisfies B904.

-            except KeyError:
-                raise KeyError(f"Agent set '{name}' not found")
+            except KeyError:
+                raise KeyError(f"Agent set '{name}' not found") from None

Apply similarly to other not-found cases in this file.

mesa_frames/abstract/agentset.py (2)

49-52: ClassVar for mutable class attributes (RUF012).

mark _copy_only_reference as ClassVar to avoid accidental instance mutation.

-from typing import Any, Literal, Self, overload
+from typing import Any, Literal, Self, overload, ClassVar
@@
-    _copy_only_reference: list[str] = ["_model"]
+    _copy_only_reference: ClassVar[list[str]] = ["_model"]

59-82: Return docstrings for add()/operators should reflect inplace semantics.

Text still says “A new AbstractAgentSet” even when inplace=True. Tweak wording to avoid confusion.

Proposed wording: “The updated AgentSet (or a new copy when inplace=False).”

mesa_frames/abstract/agentsetregistry.py (2)

528-559: Avoid bare except; restrict setitem fallback path.

Catching Exception hides real issues. Limit to expected rename failures.

-            try:
-                value.rename(key, inplace=True)
-            except Exception:
+            try:
+                value.rename(key, inplace=True)
+            except (ValueError, KeyError, TypeError):
                 if hasattr(value, "_name"):
                     value._name = key  # type: ignore[attr-defined]

Also consider raising from None in explicit KeyErrors elsewhere to satisfy B904.


590-606: keys()/items(): validate key_by via Literal and fail early.

You already check at runtime; consider narrowing annotation or centralizing the check to reduce duplication. Non-blocking.

mesa_frames/abstract/space.py (4)

55-55: Remove unused import.

cast is imported but not used.

-from typing import Any, Literal, Self, cast
+from typing import Any, Literal, Self

993-1001: Sample without replacement when targeting “empty/available” cells.

With replacement may pick the same cell multiple times, violating intent.

-        cells = self.sample_cells(len(agents), cell_type=cell_type)
+        cells = self.sample_cells(
+            len(agents), cell_type=cell_type, with_replacement=False
+        )

1780-1811: Small clean-up: drop redundant Series recast.

agents is already a Series from _get_ids_srs.

-                agents = pl.Series(agents)
                 if agents.n_unique() != len(agents):
                     raise ValueError("Some agents are present multiple times")

1362-1370: Parameter doc/validation: radius type check mentions ArrayLike but code paths assume integer radii.

Consider tightening docs/types to int | Sequence[int] for grids, or explicitly handle floats.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 51c54cd and 4ef1a25.

📒 Files selected for processing (20)
  • docs/general/user-guide/1_classes.md (3 hunks)
  • docs/general/user-guide/2_introductory-tutorial.ipynb (1 hunks)
  • docs/general/user-guide/4_datacollector.ipynb (17 hunks)
  • examples/sugarscape_ig/ss_polars/agents.py (1 hunks)
  • examples/sugarscape_ig/ss_polars/model.py (1 hunks)
  • mesa_frames/abstract/agentset.py (10 hunks)
  • mesa_frames/abstract/agentsetregistry.py (11 hunks)
  • mesa_frames/abstract/datacollector.py (1 hunks)
  • mesa_frames/abstract/space.py (44 hunks)
  • mesa_frames/concrete/agentset.py (6 hunks)
  • mesa_frames/concrete/agentsetregistry.py (6 hunks)
  • mesa_frames/concrete/datacollector.py (3 hunks)
  • mesa_frames/concrete/model.py (2 hunks)
  • mesa_frames/types_.py (2 hunks)
  • tests/conftest.py (1 hunks)
  • tests/test_agents.py (0 hunks)
  • tests/test_agentset.py (1 hunks)
  • tests/test_agentsetregistry.py (1 hunks)
  • tests/test_datacollector.py (8 hunks)
  • tests/test_grid.py (1 hunks)
💤 Files with no reviewable changes (1)
  • tests/test_agents.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-04-29T09:25:34.183Z
Learnt from: adamamer20
PR: projectmesa/mesa-frames#143
File: mesa_frames/abstract/space.py:50-63
Timestamp: 2025-04-29T09:25:34.183Z
Learning: The project mesa-frames has been upgraded to Python 3.11, which provides native support for `Self` type in the standard typing module, eliminating the need for imports from typing_extensions.

Applied to files:

  • mesa_frames/types_.py
🪛 Ruff (0.12.2)
tests/test_agentsetregistry.py

313-313: Prefer next(iter(reg.values())) over single element slice

Replace with next(iter(reg.values()))

(RUF015)

mesa_frames/concrete/datacollector.py

188-188: Do not catch blind exception: Exception

(BLE001)


239-241: Abstract raise to an inner function

(TRY301)


239-241: Avoid specifying long messages outside the exception class

(TRY003)


243-243: Do not catch blind exception: Exception

(BLE001)


258-260: Avoid specifying long messages outside the exception class

(TRY003)


264-266: Avoid specifying long messages outside the exception class

(TRY003)


555-555: Do not catch blind exception: Exception

(BLE001)

mesa_frames/concrete/agentsetregistry.py

88-90: Avoid specifying long messages outside the exception class

(TRY003)


142-142: Avoid specifying long messages outside the exception class

(TRY003)


147-147: Avoid specifying long messages outside the exception class

(TRY003)


151-151: Avoid specifying long messages outside the exception class

(TRY003)


159-159: Local variable single is assigned to but never used

Remove assignment to unused variable single

(F841)


191-191: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)


248-248: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


248-248: Avoid specifying long messages outside the exception class

(TRY003)


256-256: Avoid specifying long messages outside the exception class

(TRY003)


265-265: Avoid specifying long messages outside the exception class

(TRY003)


267-269: Avoid specifying long messages outside the exception class

(TRY003)


275-277: Avoid specifying long messages outside the exception class

(TRY003)


281-283: Avoid specifying long messages outside the exception class

(TRY003)


291-293: Avoid specifying long messages outside the exception class

(TRY003)


304-306: Avoid specifying long messages outside the exception class

(TRY003)


309-309: Avoid specifying long messages outside the exception class

(TRY003)


385-385: Avoid specifying long messages outside the exception class

(TRY003)


612-612: Avoid specifying long messages outside the exception class

(TRY003)


654-656: Avoid specifying long messages outside the exception class

(TRY003)


697-697: Avoid specifying long messages outside the exception class

(TRY003)


700-700: Avoid specifying long messages outside the exception class

(TRY003)

mesa_frames/abstract/agentsetregistry.py

102-102: Undefined name mesa_frames

(F821)


104-104: Undefined name mesa_frames

(F821)


105-105: Undefined name mesa_frames

(F821)


146-146: Undefined name mesa_frames

(F821)


147-147: Undefined name mesa_frames

(F821)


172-172: Undefined name mesa_frames

(F821)


173-173: Undefined name mesa_frames

(F821)


183-183: Undefined name mesa_frames

(F821)


184-184: Undefined name mesa_frames

(F821)


233-233: Undefined name mesa_frames

(F821)


251-251: Undefined name mesa_frames

(F821)


287-287: Undefined name mesa_frames

(F821)


293-293: Undefined name mesa_frames

(F821)


299-299: Undefined name mesa_frames

(F821)


301-301: Undefined name mesa_frames

(F821)


307-307: Undefined name mesa_frames

(F821)


308-308: Undefined name mesa_frames

(F821)


309-309: Undefined name mesa_frames

(F821)


312-312: Undefined name mesa_frames

(F821)


313-313: Undefined name mesa_frames

(F821)


320-320: Undefined name mesa_frames

(F821)


321-321: Undefined name mesa_frames

(F821)


322-322: Undefined name mesa_frames

(F821)


325-325: Undefined name mesa_frames

(F821)


326-326: Undefined name mesa_frames

(F821)


359-359: Undefined name mesa_frames

(F821)


360-360: Undefined name mesa_frames

(F821)


430-430: Undefined name mesa_frames

(F821)


431-431: Undefined name mesa_frames

(F821)


437-437: Undefined name mesa_frames

(F821)


445-445: Undefined name mesa_frames

(F821)


450-450: Undefined name mesa_frames

(F821)


454-454: Undefined name mesa_frames

(F821)


455-455: Undefined name mesa_frames

(F821)


458-458: Undefined name mesa_frames

(F821)


460-460: Undefined name mesa_frames

(F821)


461-461: Undefined name mesa_frames

(F821)


468-468: Undefined name mesa_frames

(F821)


469-469: Undefined name mesa_frames

(F821)


489-489: Undefined name mesa_frames

(F821)


490-490: Undefined name mesa_frames

(F821)


510-510: Undefined name mesa_frames

(F821)


511-511: Undefined name mesa_frames

(F821)


531-531: Undefined name mesa_frames

(F821)


542-542: Avoid specifying long messages outside the exception class

(TRY003)


554-554: Do not catch blind exception: Exception

(BLE001)


559-559: Avoid specifying long messages outside the exception class

(TRY003)


566-566: Undefined name mesa_frames

(F821)


592-592: Undefined name mesa_frames

(F821)


602-602: Avoid specifying long messages outside the exception class

(TRY003)


611-611: Undefined name mesa_frames

(F821)


612-612: Undefined name mesa_frames

(F821)


625-625: Avoid specifying long messages outside the exception class

(TRY003)


630-630: Undefined name mesa_frames

(F821)

mesa_frames/types_.py

112-126: __all__ is not sorted

Apply an isort-style sorting to __all__

(RUF022)

mesa_frames/abstract/space.py

237-237: Avoid specifying long messages outside the exception class

(TRY003)


239-239: Avoid specifying long messages outside the exception class

(TRY003)


241-241: Avoid specifying long messages outside the exception class

(TRY003)


243-243: Avoid specifying long messages outside the exception class

(TRY003)

mesa_frames/abstract/agentset.py

49-49: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


460-460: Undefined name mesa_frames

(F821)


468-468: Undefined name mesa_frames

(F821)

mesa_frames/concrete/agentset.py

86-86: Undefined name mesa_frames

(F821)


138-138: Do not catch blind exception: Exception

(BLE001)


146-146: Do not catch blind exception: Exception

(BLE001)


326-326: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (7)
tests/test_agentset.py (1)

263-292: Rename workflow test looks solid.

Covers inplace, collision/canonicalization, and copy semantics; exercises registry mapping updates.

If non-inplace rename should or should not auto-register the copy, consider asserting the intended behavior to lock it down.

docs/general/user-guide/1_classes.md (2)

46-48: LGTM: registry-based lookup example is clear.


95-97: LGTM: step delegation via registry.

mesa_frames/concrete/model.py (2)

129-131: LGTM: step() dispatches via registry .do("step").


89-91: Confirm step-wrapping captures subclass overrides.

self._user_step = self.step must bind the subclass’s override when super().__init__() runs. If any subclass rebinds self.step after super().__init__(), wrapping will be bypassed. Worth a quick audit in the codebase.

tests/test_datacollector.py (1)

167-167: LGTM: agent reporter callables now receive the registry (sets).

This aligns with the documented semantics and exercises registry-level access cleanly.

Also applies to: 226-226, 282-282, 364-365, 440-441, 516-517, 565-566, 710-711

mesa_frames/abstract/agentsetregistry.py (1)

664-675: ids property docstring OK; ensure concrete returns pl.Series consistently.

Looks good; verify concrete implementation dtype is pl.UInt64 (it is).

If you want me to add a quick assertion-based test for ids dtype and emptiness semantics, I can draft it.

@adamamer20 adamamer20 changed the title Ehancement: Add names to AgentSetDF and AgentSetsAccessor for name/type/index lookup and container-aware keying Ehancement: Named 'AgentSet's + flexible lookup & stricter separation of concerns Sep 19, 2025
@adamamer20 adamamer20 changed the title Ehancement: Named 'AgentSet's + flexible lookup & stricter separation of concerns Ehancement: Named AgentSets + flexible lookup & stricter separation of concerns Sep 19, 2025
@adamamer20 adamamer20 changed the title Ehancement: Named AgentSets + flexible lookup & stricter separation of concerns Ehancement: Named AgentSet + flexible lookup & stricter separation of concerns Sep 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking Changes that break backwards compatibility or require major updates. enhancement Improvements to existing features or performance.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Enhancement]: Consider using a key-based structure for agentsets instead of list in AgentsDf
2 participants