Source code for bamengine.core.role
"""
Role (Component) base class definition.
This module defines the Role base class, the fundamental building block
of the BAM-ECS architecture. Roles are dataclasses containing NumPy arrays
that represent specific aspects of agent behavior.
Design Notes
------------
- All Roles auto-register via __init_subclass__ hook
- Each Role has a name (ClassVar) set automatically
- Roles should be immutable containers; use system functions for mutations
- NumPy array fields enable vectorized operations across all agents
Auto-Registration
-----------------
When a class inherits from Role, __init_subclass__ automatically:
1. Sets the role name (cls.name) to class name if not provided
2. Registers the role class in the global _ROLE_REGISTRY
3. Makes the role retrievable via get_role(name)
This eliminates manual registration boilerplate and ensures all
roles are discoverable at runtime.
See Also
--------
:class:`~bamengine.core.event.Event` : Base class for events (systems) in BAM-ECS
:mod:`~bamengine.core.registry` : Global role and event lookup system
:func:`~bamengine.core.decorators.role` : Simplified decorator for defining roles
"""
from __future__ import annotations
from abc import ABC
from dataclasses import dataclass
from typing import Any, ClassVar
[docs]
@dataclass(slots=True)
class Role(ABC):
"""
Base class for all roles (components) in the BAM-ECS architecture.
A Role is a dataclass containing NumPy arrays representing state variables
for a specific aspect of agent behavior (e.g., Producer, Worker, Lender).
Each array index corresponds to an agent ID.
Class Attributes
----------------
name : str
Role name, automatically set to class name if not provided.
Design Guidelines
-----------------
- All state variables should be NumPy arrays (Float, Int, Bool types)
- Scratch buffers (optional fields) can be added for performance
- Avoid methods that mutate state; use system functions instead
- Use @role decorator for simplified role definition
Examples
--------
Define a role using traditional syntax:
>>> from dataclasses import dataclass
>>> from bamengine.core import Role
>>> from bamengine import Float
>>> import numpy as np
>>>
>>> @dataclass(slots=True)
... class Producer(Role):
... price: Float
... production: Float
>>> # Producer is now auto-registered!
Define a role using the @role decorator (simplified):
>>> from bamengine import role, Float
>>>
>>> @role
... class MyRole:
... value: Float
>>> # MyRole is now auto-registered!
Access registered roles:
>>> from bamengine.core.registry import get_role
>>> prod_cls = get_role("Producer")
>>> instance = prod_cls(
... price=np.array([1.0, 1.2]), production=np.array([100.0, 120.0])
... )
Notes
-----
The __init_subclass__ hook automatically registers roles in the global
registry and sets the role name. This eliminates manual registration
boilerplate.
For example, if there are 100 firms, `Producer.price` would be a 1D NumPy
array of length 100, where index i corresponds to firm i.
See Also
--------
:class:`~bamengine.core.event.Event` : Base class for events (systems) in BAM-ECS
:func:`~bamengine.core.decorators.role` : Simplified @role decorator
:func:`~bamengine.core.registry.get_role` : Retrieve role by name
"""
# Class variable to store role name (set automatically by __init_subclass__)
name: ClassVar[str | None] = None
[docs]
def __init_subclass__(cls, name: str | None = None, **kwargs: Any) -> None:
"""
Auto-register Role subclasses in the global registry.
This hook is called automatically when a class inherits from Role.
It handles role registration and name assignment.
Parameters
----------
name : str, optional
Custom name for the role. If not provided, uses the class name.
**kwargs : Any
Additional keyword arguments passed to parent __init_subclass__.
Notes
-----
This method is called twice when using @dataclass(slots=True):
once during class definition and once when dataclass creates the
final class. The name preservation logic handles this correctly.
Examples
--------
Normal usage (automatic):
>>> from bamengine.typing import Float
>>> from dataclasses import dataclass
>>> @dataclass(slots=True)
... class MyRole(Role):
... value: Float
Custom name:
>>> @dataclass(slots=True)
... class MyRole(Role, name="CustomName"):
... value: Float
"""
super(Role, cls).__init_subclass__(**kwargs)
# Use custom name if provided, otherwise preserve existing name or use cls name
# This handles the case where @dataclass(slots=True) creates a new class
# and triggers __init_subclass__ a second time without the custom name
if name is not None:
cls.name = name
elif cls.name is None:
cls.name = cls.__name__
# Auto-register in global registry
from bamengine.core.registry import _ROLE_REGISTRY
_ROLE_REGISTRY[cls.name] = cls
[docs]
def __repr__(self) -> str:
"""
Provide informative repr showing role name and field count.
Returns
-------
str
String representation in format "RoleName(fields=N)".
Examples
--------
>>> from bamengine.roles import Producer
>>> import numpy as np
>>> prod = Producer(
... price=np.array([1.0]),
... production=np.array([100.0]),
... inventory=np.array([0.0]),
... labor_productivity=np.array([2.0]),
... )
>>> repr(prod)
'Producer(fields=4)'
"""
fields = getattr(self, "__dataclass_fields__", {})
role_name = self.name or self.__class__.__name__
return f"{role_name}(fields={len(fields)})"