Skip to content

State

State and StateDict classes for Tölvera.

Every Tölvera instance has a StateDict, which is a dictionary of State instances. The StateDict is accessible via the 's' attribute of a Tölvera instance, and can be used to create and access states.

Each State instance has a Taichi struct field and a corresponding NpNdarrayDict, which handles OSC accessors and endpoints.

CONSTS

Dict of CONSTS that can be used in Taichi scope

Source code in src/tolvera/utils.py
class CONSTS:
    """
    Dict of CONSTS that can be used in Taichi scope
    """

    def __init__(self, dict: dict[str, (DataType, Any)]):
        self.struct = ti.types.struct(**{k: v[0] for k, v in dict.items()})
        self.consts = self.struct(**{k: v[1] for k, v in dict.items()})

    def __getattr__(self, name):
        try:
            return self.consts[name]
        except:
            raise AttributeError(f"CONSTS has no attribute {name}")

    def __getitem__(self, name):
        try:
            return self.consts[name]
        except:
            raise AttributeError(f"CONSTS has no attribute {name}")

State

State class for Tölvera.

This class takes a name, dictionary of state attributes, and a shape, and creates a Taichi struct field and a corresponding dictionary of NumPy arrays (NpNdarrayDict) for a state.

The Taichi struct field can be used in Taichi scope, and the NpNdarrayDict can be used in Python scope, and the two are kept in sync by the from_nddict() and to_nddict() methods.

The State class also handles OSC accessors for the state, which use the NpNdarrayDict to get and set data. A Tölvera instance is therefore required to initialise a State instance.

State attributes are defined as a dictionary of attribute names and tuples of (Taichi type, min value, max value). The domain of the attribute is used when randomising the data in the state, and by OSCMap endpoints and client patches.

The state is n-dimensional based on the shape argument, and the NpNdarrayDict provides methods for accessing the data in the state in n-dimensional slices.

Example
tv.s.flock_p = {
    "state": {
        "separate": (ti.math.vec2, 0.0, 1.0),
        "align": (ti.math.vec2, 0.0, 1.0),
        "cohere": (ti.math.vec2, 0.0, 1.0),
        "nearby": (ti.i32, 0, self.tv.p.n - 1),
    },
    "shape": self.tv.pn, # particle count
    "osc": ("get"),
    "randomise": False,
}
Source code in src/tolvera/state.py
@ti.data_oriented
class State:
    """State class for Tölvera.

    This class takes a name, dictionary of state attributes, and a shape, and
    creates a Taichi struct field and a corresponding dictionary of NumPy arrays 
    (NpNdarrayDict) for a state.

    The Taichi struct field can be used in Taichi scope, and the NpNdarrayDict
    can be used in Python scope, and the two are kept in sync by the from_nddict()
    and to_nddict() methods.

    The State class also handles OSC accessors for the state, which use the
    NpNdarrayDict to get and set data. A Tölvera instance is therefore required
    to initialise a State instance.

    State attributes are defined as a dictionary of attribute names and tuples of
    (Taichi type, min value, max value). The domain of the attribute is used when
    randomising the data in the state, and by OSCMap endpoints and client patches.

    The state is n-dimensional based on the shape argument, and the NpNdarrayDict
    provides methods for accessing the data in the state in n-dimensional slices.

    Example:
        ```py
        tv.s.flock_p = {
            "state": {
                "separate": (ti.math.vec2, 0.0, 1.0),
                "align": (ti.math.vec2, 0.0, 1.0),
                "cohere": (ti.math.vec2, 0.0, 1.0),
                "nearby": (ti.i32, 0, self.tv.p.n - 1),
            },
            "shape": self.tv.pn, # particle count
            "osc": ("get"),
            "randomise": False,
        }
        ```
    """
    def __init__(
        self,
        tolvera,
        name: str,
        state: dict[str, tuple[DataType, Any, Any]],
        shape: int | tuple[int] = None,
        osc: str | tuple = None,  # ('get', 'set', 'stream')
        randomise: bool = True,
        methods: dict[str, Any] = None,
    ):
        """Initialise a state for Tölvera.

        Args:
            tolvera (Tolvera): Tolvera instance to which this state belongs.
            name (str): Name of this state.
            state (dict[str, tuple[DataType, Any, Any]]): Dict of state attributes.
            shape (int | tuple[int], optional): Shape of the state. Defaults to 1.
            methods (dict[str, Any], optional): Flag for OSC via iipyper. Defaults to False.
        """
        self.tv = tolvera
        assert name is not None, "State must have a name."
        self.name = name
        shape = 1 if shape is None else shape
        self.setup_data(state, shape, randomise, methods)
        self.setup_osc(osc)

    def setup_data(
        self,
        dict: dict[str, tuple[DataType, Any, Any]],
        shape: int | tuple[int],
        randomise: bool = True,
        methods: dict[str, Any] = None,
    ):
        """Setup data structures and data for this state.

        Args:
            dict (dict[str, tuple[DataType, Any, Any]]): Dict of state attributes.
            shape (int | tuple[int]): Shape of the state.
            randomise (bool, optional): Flag to randomise the data on creation. Defaults to True.
            methods (dict[str, Any], optional): Dict of Taichi field struct methods. Defaults to None.
        """
        self.create_struct_field(dict, shape, methods)
        self.create_npndarray_dict()
        if randomise:
            self.randomise()

    def create_struct_field(
        self,
        dict: dict[str, tuple[DataType, Any, Any]],
        shape: int | tuple[int],
        methods: dict[str, Any] = None,
    ):
        """Create a Taichi struct field for this state.

        Args:
            dict (dict[str, tuple[DataType, Any, Any]]): Dict of state attributes.
            shape (int | tuple[int]): Shape of the state.
            methods (dict[str, Any], optional): Dict of Taichi field struct methods. Defaults to None.
        """
        self.dict = dict
        self.shape = (shape,) if isinstance(shape, int) else shape
        if methods is None:
            self.struct = ti.types.struct(**{k: v[0] for k, v in self.dict.items()})
        else:
            self.methods = methods if methods is not None else {}
            self.struct = ti.types.struct(
                **{k: v[0] for k, v in self.dict.items()}, methods=self.methods
            )
        self.field = self.struct.field(shape=self.shape)

    def create_npndarray_dict(self):
        """Create a NpNdarrayDict for this state.

        Raises:
            NotImplementedError: If no Numpy type is found for a Taichi type.
        """
        nddict = {}
        for k, v in self.dict.items():
            titype, min_val, max_val = v
            nptype = TiNpTypeMap.get(titype)
            if nptype is None:
                raise NotImplementedError(f"no nptype for {titype}")
            nddict[k] = (nptype, min_val, max_val)
        self.nddict = NpNdarrayDict(nddict, self.shape)
        self.size = self.nddict.size

    def randomise(self):
        """Randomise the data in this state."""
        self.nddict.randomise()
        self.from_nddict()

    def randomise_attr(self, attr: str):
        """Randomise an attribute in this state.

        Args:
            attr (str): Attribute name.
        """
        self.nddict.randomise_attr(attr)
        self.from_nddict()

    def setup_osc(self, osc: tuple|str = None):
        """Setup OSC for this state.

        Args:
            osc (tuple | str, optional): ("get", "set", "stream"). Defaults to None.
        """
        self.osc = osc is not None
        if not self.osc: return
        if isinstance(osc, str): osc = (osc,)
        self.osc_set = "set" in osc if self.osc else False
        self.osc_get = "get" in osc if self.osc else False
        self.osc_stream = "stream" in osc if self.osc else False
        self.setter_name = f"{self.tv.name_clean}_set_{self.name}"
        self.getter_name = f"{self.tv.name_clean}_get_{self.name}"
        self.stream_name = f"{self.tv.name_clean}_stream_{self.name}"
        if self.tv.osc is not False and self.osc:
            self.osc = self.tv.osc
            if self.osc_set: self.add_osc_setters()
            # if self.osc_get: self.add_osc_getters()
            # if self.osc_stream: self.add_osc_streams()

    def add_osc_setters(self):
        name = self.setter_name
        self.osc.map.receive_args_inline(name + "_randomise", self.randomise)

    def add_osc_getters(self):
        name = self.getter_name
        for k, v in self.dict.items():
            ranges = (int(v[0]), int(v[0]), int(v[1]))
            kwargs = {"i": ranges, "j": ranges, "attr": (k, k, k)}
            self.osc.map.receive_args_inline(f"{name}", self.osc_getter, **kwargs)

    # def osc_getter(self, i: int, j: int, attribute: str):
    #     ret = self.get((i, j), attribute)
    #     if ret is not None:
    #         route = self.osc.map.pascal_to_path(self.getter_name)  # +'/'+attribute
    #         self.osc.host.return_to_sender_by_name(
    #             (route, attribute, ret), self.osc.client_name
    #         )
    #     return ret

    # def add_osc_streams(self):
    #     # add send in broadcast mode
    #     raise NotImplementedError("add_osc_streams not implemented")

    def serialize(self) -> str:
        return ti_serialize(self.field)

    def deserialize(self, json_str: str):
        ti_deserialize(self.field, json_str)

    def save(self, path: str):
        # TODO: path validation, save to path, etc.
        json_str = self.serialize()
        raise NotImplementedError("save not implemented")

    def load(self, path: str):
        # TODO: path validation, file ext., etc.
        # TODO: data validation (pydantic?)
        json_str = jsons.load(path)
        self.deserialize(json_str)
        raise NotImplementedError("load not implemented")

    def from_nddict(self):
        """Copy data from NpNdarrayDict to Taichi field.

        Raises:
            Exception: If data cannot be copied.
        """
        try:
            data = self.nddict.get_data()
            self.field.from_numpy(data)
        except Exception as e:
            raise Exception(f"[tolvera.state.from_nddict] {e}") from e

    def to_nddict(self):
        """Copy data from Taichi field to NpNdarrayDict.

        Raises:
            Exception: If data cannot be copied.
        """
        try:
            data = self.field.to_numpy()
            self.nddict.set_data(data)
        except Exception as e:
            raise Exception(f"[tolvera.state.to_nddict] {e}") from e

    def set_from_nddict(self, data: dict):
        """Copy data from NumPy array dict to Taichi field.

        Args:
            data (dict): NumPy array dict to copy.

        Raises:
            Exception: If data cannot be copied.
        """
        try:
            self.field.from_numpy(data)
        except Exception as e:
            raise Exception(f"[tolvera.state.from_numpy] {e}") from e

    """
    npndarray_dict wrappers
    """

    def from_vec(self, vec: list):
        """Wrapper for NpNdarrayDict.from_vec()."""
        self.to_nddict()
        self.nddict.from_vec(vec)
        self.from_nddict()

    def to_vec(self) -> list:
        """Wrapper for NpNdarrayDict.to_vec()."""
        self.to_nddict()
        return self.nddict.to_vec()

    def attr_from_vec(self, attr: str, vec: list):
        """Wrapper for NpNdarrayDict.attr_from_vec()."""
        self.to_nddict()
        self.nddict.attr_from_vec(attr, vec)
        self.from_nddict()

    def attr_to_vec(self, attr: str) -> list:
        """Wrapper for NpNdarrayDict.attr_to_vec()."""
        self.to_nddict()
        return self.nddict.attr_to_vec(attr)

    def slice_from_vec(self, slice_args: list, slice_vec: list):
        """Wrapper for NpNdarrayDict.slice_from_vec()."""
        self.to_nddict()
        self.nddict.slice_from_vec(slice_args, slice_vec)
        self.from_nddict()

    def slice_to_vec(self, slice_args: list) -> list:
        """Wrapper for NpNdarrayDict.slice_to_vec()."""
        self.to_nddict()
        return self.nddict.slice_to_vec(slice_args)

    def attr_slice_from_vec(self, attr: str, slice_args: list, slice_vec: list):
        """Wrapper for NpNdarrayDict.attr_slice_from_vec()."""
        self.to_nddict()
        self.nddict.attr_slice_from_vec(attr, slice_args, slice_vec)
        self.from_nddict()

    def attr_slice_to_vec(self, attr: str, slice_args: list) -> list:
        """Wrapper for NpNdarrayDict.attr_slice_to_vec()."""
        self.to_nddict()
        return self.nddict.attr_slice_to_vec(attr, slice_args)

    def attr_size(self, attr: str) -> int:
        """Return the size of the attribute."""
        return self.nddict.data[attr].size

    """
    misc
    """

    def fill(self, value: ti.f32):
        """Fill the Taichi field with a value."""
        self.field.fill(value)

    @ti.func
    def __getitem__(self, index: ti.i32):
        """Return the Taichi field attribute.

        Args:
            index (ti.i32): Attribute index.
        """
        return self.field[index]

    def __call__(self, *args: Any, **kwds: Any) -> Any:
        """Return the Taichi field."""
        return self.field

__call__(*args, **kwds)

Return the Taichi field.

Source code in src/tolvera/state.py
def __call__(self, *args: Any, **kwds: Any) -> Any:
    """Return the Taichi field."""
    return self.field

__getitem__(index)

Return the Taichi field attribute.

Parameters:

Name Type Description Default
index i32

Attribute index.

required
Source code in src/tolvera/state.py
@ti.func
def __getitem__(self, index: ti.i32):
    """Return the Taichi field attribute.

    Args:
        index (ti.i32): Attribute index.
    """
    return self.field[index]

__init__(tolvera, name, state, shape=None, osc=None, randomise=True, methods=None)

Initialise a state for Tölvera.

Parameters:

Name Type Description Default
tolvera Tolvera

Tolvera instance to which this state belongs.

required
name str

Name of this state.

required
state dict[str, tuple[DataType, Any, Any]]

Dict of state attributes.

required
shape int | tuple[int]

Shape of the state. Defaults to 1.

None
methods dict[str, Any]

Flag for OSC via iipyper. Defaults to False.

None
Source code in src/tolvera/state.py
def __init__(
    self,
    tolvera,
    name: str,
    state: dict[str, tuple[DataType, Any, Any]],
    shape: int | tuple[int] = None,
    osc: str | tuple = None,  # ('get', 'set', 'stream')
    randomise: bool = True,
    methods: dict[str, Any] = None,
):
    """Initialise a state for Tölvera.

    Args:
        tolvera (Tolvera): Tolvera instance to which this state belongs.
        name (str): Name of this state.
        state (dict[str, tuple[DataType, Any, Any]]): Dict of state attributes.
        shape (int | tuple[int], optional): Shape of the state. Defaults to 1.
        methods (dict[str, Any], optional): Flag for OSC via iipyper. Defaults to False.
    """
    self.tv = tolvera
    assert name is not None, "State must have a name."
    self.name = name
    shape = 1 if shape is None else shape
    self.setup_data(state, shape, randomise, methods)
    self.setup_osc(osc)

attr_from_vec(attr, vec)

Wrapper for NpNdarrayDict.attr_from_vec().

Source code in src/tolvera/state.py
def attr_from_vec(self, attr: str, vec: list):
    """Wrapper for NpNdarrayDict.attr_from_vec()."""
    self.to_nddict()
    self.nddict.attr_from_vec(attr, vec)
    self.from_nddict()

attr_size(attr)

Return the size of the attribute.

Source code in src/tolvera/state.py
def attr_size(self, attr: str) -> int:
    """Return the size of the attribute."""
    return self.nddict.data[attr].size

attr_slice_from_vec(attr, slice_args, slice_vec)

Wrapper for NpNdarrayDict.attr_slice_from_vec().

Source code in src/tolvera/state.py
def attr_slice_from_vec(self, attr: str, slice_args: list, slice_vec: list):
    """Wrapper for NpNdarrayDict.attr_slice_from_vec()."""
    self.to_nddict()
    self.nddict.attr_slice_from_vec(attr, slice_args, slice_vec)
    self.from_nddict()

attr_slice_to_vec(attr, slice_args)

Wrapper for NpNdarrayDict.attr_slice_to_vec().

Source code in src/tolvera/state.py
def attr_slice_to_vec(self, attr: str, slice_args: list) -> list:
    """Wrapper for NpNdarrayDict.attr_slice_to_vec()."""
    self.to_nddict()
    return self.nddict.attr_slice_to_vec(attr, slice_args)

attr_to_vec(attr)

Wrapper for NpNdarrayDict.attr_to_vec().

Source code in src/tolvera/state.py
def attr_to_vec(self, attr: str) -> list:
    """Wrapper for NpNdarrayDict.attr_to_vec()."""
    self.to_nddict()
    return self.nddict.attr_to_vec(attr)

create_npndarray_dict()

Create a NpNdarrayDict for this state.

Raises:

Type Description
NotImplementedError

If no Numpy type is found for a Taichi type.

Source code in src/tolvera/state.py
def create_npndarray_dict(self):
    """Create a NpNdarrayDict for this state.

    Raises:
        NotImplementedError: If no Numpy type is found for a Taichi type.
    """
    nddict = {}
    for k, v in self.dict.items():
        titype, min_val, max_val = v
        nptype = TiNpTypeMap.get(titype)
        if nptype is None:
            raise NotImplementedError(f"no nptype for {titype}")
        nddict[k] = (nptype, min_val, max_val)
    self.nddict = NpNdarrayDict(nddict, self.shape)
    self.size = self.nddict.size

create_struct_field(dict, shape, methods=None)

Create a Taichi struct field for this state.

Parameters:

Name Type Description Default
dict dict[str, tuple[DataType, Any, Any]]

Dict of state attributes.

required
shape int | tuple[int]

Shape of the state.

required
methods dict[str, Any]

Dict of Taichi field struct methods. Defaults to None.

None
Source code in src/tolvera/state.py
def create_struct_field(
    self,
    dict: dict[str, tuple[DataType, Any, Any]],
    shape: int | tuple[int],
    methods: dict[str, Any] = None,
):
    """Create a Taichi struct field for this state.

    Args:
        dict (dict[str, tuple[DataType, Any, Any]]): Dict of state attributes.
        shape (int | tuple[int]): Shape of the state.
        methods (dict[str, Any], optional): Dict of Taichi field struct methods. Defaults to None.
    """
    self.dict = dict
    self.shape = (shape,) if isinstance(shape, int) else shape
    if methods is None:
        self.struct = ti.types.struct(**{k: v[0] for k, v in self.dict.items()})
    else:
        self.methods = methods if methods is not None else {}
        self.struct = ti.types.struct(
            **{k: v[0] for k, v in self.dict.items()}, methods=self.methods
        )
    self.field = self.struct.field(shape=self.shape)

fill(value)

Fill the Taichi field with a value.

Source code in src/tolvera/state.py
def fill(self, value: ti.f32):
    """Fill the Taichi field with a value."""
    self.field.fill(value)

from_nddict()

Copy data from NpNdarrayDict to Taichi field.

Raises:

Type Description
Exception

If data cannot be copied.

Source code in src/tolvera/state.py
def from_nddict(self):
    """Copy data from NpNdarrayDict to Taichi field.

    Raises:
        Exception: If data cannot be copied.
    """
    try:
        data = self.nddict.get_data()
        self.field.from_numpy(data)
    except Exception as e:
        raise Exception(f"[tolvera.state.from_nddict] {e}") from e

from_vec(vec)

Wrapper for NpNdarrayDict.from_vec().

Source code in src/tolvera/state.py
def from_vec(self, vec: list):
    """Wrapper for NpNdarrayDict.from_vec()."""
    self.to_nddict()
    self.nddict.from_vec(vec)
    self.from_nddict()

randomise()

Randomise the data in this state.

Source code in src/tolvera/state.py
def randomise(self):
    """Randomise the data in this state."""
    self.nddict.randomise()
    self.from_nddict()

randomise_attr(attr)

Randomise an attribute in this state.

Parameters:

Name Type Description Default
attr str

Attribute name.

required
Source code in src/tolvera/state.py
def randomise_attr(self, attr: str):
    """Randomise an attribute in this state.

    Args:
        attr (str): Attribute name.
    """
    self.nddict.randomise_attr(attr)
    self.from_nddict()

set_from_nddict(data)

Copy data from NumPy array dict to Taichi field.

Parameters:

Name Type Description Default
data dict

NumPy array dict to copy.

required

Raises:

Type Description
Exception

If data cannot be copied.

Source code in src/tolvera/state.py
def set_from_nddict(self, data: dict):
    """Copy data from NumPy array dict to Taichi field.

    Args:
        data (dict): NumPy array dict to copy.

    Raises:
        Exception: If data cannot be copied.
    """
    try:
        self.field.from_numpy(data)
    except Exception as e:
        raise Exception(f"[tolvera.state.from_numpy] {e}") from e

setup_data(dict, shape, randomise=True, methods=None)

Setup data structures and data for this state.

Parameters:

Name Type Description Default
dict dict[str, tuple[DataType, Any, Any]]

Dict of state attributes.

required
shape int | tuple[int]

Shape of the state.

required
randomise bool

Flag to randomise the data on creation. Defaults to True.

True
methods dict[str, Any]

Dict of Taichi field struct methods. Defaults to None.

None
Source code in src/tolvera/state.py
def setup_data(
    self,
    dict: dict[str, tuple[DataType, Any, Any]],
    shape: int | tuple[int],
    randomise: bool = True,
    methods: dict[str, Any] = None,
):
    """Setup data structures and data for this state.

    Args:
        dict (dict[str, tuple[DataType, Any, Any]]): Dict of state attributes.
        shape (int | tuple[int]): Shape of the state.
        randomise (bool, optional): Flag to randomise the data on creation. Defaults to True.
        methods (dict[str, Any], optional): Dict of Taichi field struct methods. Defaults to None.
    """
    self.create_struct_field(dict, shape, methods)
    self.create_npndarray_dict()
    if randomise:
        self.randomise()

setup_osc(osc=None)

Setup OSC for this state.

Parameters:

Name Type Description Default
osc tuple | str

("get", "set", "stream"). Defaults to None.

None
Source code in src/tolvera/state.py
def setup_osc(self, osc: tuple|str = None):
    """Setup OSC for this state.

    Args:
        osc (tuple | str, optional): ("get", "set", "stream"). Defaults to None.
    """
    self.osc = osc is not None
    if not self.osc: return
    if isinstance(osc, str): osc = (osc,)
    self.osc_set = "set" in osc if self.osc else False
    self.osc_get = "get" in osc if self.osc else False
    self.osc_stream = "stream" in osc if self.osc else False
    self.setter_name = f"{self.tv.name_clean}_set_{self.name}"
    self.getter_name = f"{self.tv.name_clean}_get_{self.name}"
    self.stream_name = f"{self.tv.name_clean}_stream_{self.name}"
    if self.tv.osc is not False and self.osc:
        self.osc = self.tv.osc
        if self.osc_set: self.add_osc_setters()

slice_from_vec(slice_args, slice_vec)

Wrapper for NpNdarrayDict.slice_from_vec().

Source code in src/tolvera/state.py
def slice_from_vec(self, slice_args: list, slice_vec: list):
    """Wrapper for NpNdarrayDict.slice_from_vec()."""
    self.to_nddict()
    self.nddict.slice_from_vec(slice_args, slice_vec)
    self.from_nddict()

slice_to_vec(slice_args)

Wrapper for NpNdarrayDict.slice_to_vec().

Source code in src/tolvera/state.py
def slice_to_vec(self, slice_args: list) -> list:
    """Wrapper for NpNdarrayDict.slice_to_vec()."""
    self.to_nddict()
    return self.nddict.slice_to_vec(slice_args)

to_nddict()

Copy data from Taichi field to NpNdarrayDict.

Raises:

Type Description
Exception

If data cannot be copied.

Source code in src/tolvera/state.py
def to_nddict(self):
    """Copy data from Taichi field to NpNdarrayDict.

    Raises:
        Exception: If data cannot be copied.
    """
    try:
        data = self.field.to_numpy()
        self.nddict.set_data(data)
    except Exception as e:
        raise Exception(f"[tolvera.state.to_nddict] {e}") from e

to_vec()

Wrapper for NpNdarrayDict.to_vec().

Source code in src/tolvera/state.py
def to_vec(self) -> list:
    """Wrapper for NpNdarrayDict.to_vec()."""
    self.to_nddict()
    return self.nddict.to_vec()

StateDict

Bases: dotdict

StateDict class for Tölvera.

This class is a dictionary of State instances, and is accessible via the 's' attribute of a Tölvera instance.

States can be created by assigning a dictionary or a tuple to a StateDict key. and can be used in Taichi scope and Python scope respectively.

Example

tv = Tolvera(**kwargs)

tv.s.mystate = { "state": { "id": (ti.i32, 0, tv.pn - 1), "pos": (ti.math.vec2, -1.0, 1.0), "vel": (ti.math.vec2, -1.0, 1.0), }, "shape": (tv.pn, 1), "osc": "get", "randomise": True }

tv.s.mystate.field.pos[0] = 0.5

Source code in src/tolvera/state.py
class StateDict(dotdict):
    """StateDict class for Tölvera.

    This class is a dictionary of State instances, and is accessible via the 's'
    attribute of a Tölvera instance.

    States can be created by assigning a dictionary or a tuple to a StateDict key.
    and can be used in Taichi scope and Python scope respectively.

    Example:
        tv = Tolvera(**kwargs)

        tv.s.mystate = {
            "state": {
                "id":  (ti.i32, 0, tv.pn - 1),
                "pos": (ti.math.vec2, -1.0, 1.0),
                "vel": (ti.math.vec2, -1.0, 1.0),
            }, 
            "shape": (tv.pn, 1), 
            "osc": "get", 
            "randomise": True
        }

        tv.s.mystate.field.pos[0] = 0.5
    """
    def __init__(self, tolvera) -> None:
        """Initialise a StateDict for Tölvera.

        Args:
            tolvera (Tolvera): Tolvera instance to which this StateDict belongs.
        """
        self.tv = tolvera
        self.size = 0

    def set(self, name, kwargs: Any) -> None:
        """Set a state in the StateDict.

        Args:
            name (str): Name of the state.
            kwargs (Any): State attributes.

        Raises:
            ValueError: If the state is already in the StateDict.
            Exception: If the state cannot be added.
        """
        if name in self and name != "size":
            raise ValueError(f"[tolvera.state.StateDict] '{name}' already in dict.")
        try:
            self.add(name, kwargs)
        except Exception as e:
            raise type(e)(f"[tolvera.state.StateDict] {e}") from e

    def add(self, name, kwargs: Any):
        """Add a state to the StateDict.

        Args:
            name (str): Name of the state.
            kwargs (Any): State attributes.

        Raises:
            TypeError: If kwargs is not a dict or tuple.
        """
        if name == "tv" and type(kwargs) is not dict and type(kwargs) is not tuple:
            self[name] = kwargs
        elif name == "size" and type(kwargs) is int:
            self[name] = kwargs
        elif type(kwargs) is dict:
            self[name] = State(self.tv, name=name, **kwargs)
            self.size += self[name].size
        elif type(kwargs) is tuple:
            self[name] = State(self.tv, name, *kwargs)
            self.size += self[name].size
        else:
            raise TypeError(
                f"[tolvera.state.StateDict] set() requires dict|tuple, not {type(kwargs)}"
            )

    def from_vec(self, states: list[str], vector: list[float]):
        """Copy data from a vector to states in the StateDict.

        Args:
            states (list[str]): List of state names.
            vector (list[float]): Vector of data to copy.

        Raises:
            Exception: If the vector is not the correct size.
        """
        sizes_sum = self.get_size(states)
        assert sizes_sum == len(
            vector
        ), f"sizes_sum={sizes_sum} != len(vector)={len(vector)}"
        vec_start = 0
        for state in states:
            s = self.tv.s[state]
            vec = vector[vec_start : vec_start + s.size]
            s.from_vec(vec)
            vec_start += s.size

    def get_size(self, states: str | list[str]) -> int:
        """Return the size of the states in the StateDict.

        Args:
            states (str | list[str]): State name or list of state names.

        Returns:
            int: Size of the states.
        """
        if isinstance(states, str):
            states = [states]
        return sum([self.tv.s[state].size for state in states])

    def __setattr__(self, __name: str, __value: Any) -> None:
        """Set a state in the StateDict.

        Args:
            __name (str): Name of the state.
            __value (Any): State attributes.
        """
        self.set(__name, __value)

__init__(tolvera)

Initialise a StateDict for Tölvera.

Parameters:

Name Type Description Default
tolvera Tolvera

Tolvera instance to which this StateDict belongs.

required
Source code in src/tolvera/state.py
def __init__(self, tolvera) -> None:
    """Initialise a StateDict for Tölvera.

    Args:
        tolvera (Tolvera): Tolvera instance to which this StateDict belongs.
    """
    self.tv = tolvera
    self.size = 0

__setattr__(__name, __value)

Set a state in the StateDict.

Parameters:

Name Type Description Default
__name str

Name of the state.

required
__value Any

State attributes.

required
Source code in src/tolvera/state.py
def __setattr__(self, __name: str, __value: Any) -> None:
    """Set a state in the StateDict.

    Args:
        __name (str): Name of the state.
        __value (Any): State attributes.
    """
    self.set(__name, __value)

add(name, kwargs)

Add a state to the StateDict.

Parameters:

Name Type Description Default
name str

Name of the state.

required
kwargs Any

State attributes.

required

Raises:

Type Description
TypeError

If kwargs is not a dict or tuple.

Source code in src/tolvera/state.py
def add(self, name, kwargs: Any):
    """Add a state to the StateDict.

    Args:
        name (str): Name of the state.
        kwargs (Any): State attributes.

    Raises:
        TypeError: If kwargs is not a dict or tuple.
    """
    if name == "tv" and type(kwargs) is not dict and type(kwargs) is not tuple:
        self[name] = kwargs
    elif name == "size" and type(kwargs) is int:
        self[name] = kwargs
    elif type(kwargs) is dict:
        self[name] = State(self.tv, name=name, **kwargs)
        self.size += self[name].size
    elif type(kwargs) is tuple:
        self[name] = State(self.tv, name, *kwargs)
        self.size += self[name].size
    else:
        raise TypeError(
            f"[tolvera.state.StateDict] set() requires dict|tuple, not {type(kwargs)}"
        )

from_vec(states, vector)

Copy data from a vector to states in the StateDict.

Parameters:

Name Type Description Default
states list[str]

List of state names.

required
vector list[float]

Vector of data to copy.

required

Raises:

Type Description
Exception

If the vector is not the correct size.

Source code in src/tolvera/state.py
def from_vec(self, states: list[str], vector: list[float]):
    """Copy data from a vector to states in the StateDict.

    Args:
        states (list[str]): List of state names.
        vector (list[float]): Vector of data to copy.

    Raises:
        Exception: If the vector is not the correct size.
    """
    sizes_sum = self.get_size(states)
    assert sizes_sum == len(
        vector
    ), f"sizes_sum={sizes_sum} != len(vector)={len(vector)}"
    vec_start = 0
    for state in states:
        s = self.tv.s[state]
        vec = vector[vec_start : vec_start + s.size]
        s.from_vec(vec)
        vec_start += s.size

get_size(states)

Return the size of the states in the StateDict.

Parameters:

Name Type Description Default
states str | list[str]

State name or list of state names.

required

Returns:

Name Type Description
int int

Size of the states.

Source code in src/tolvera/state.py
def get_size(self, states: str | list[str]) -> int:
    """Return the size of the states in the StateDict.

    Args:
        states (str | list[str]): State name or list of state names.

    Returns:
        int: Size of the states.
    """
    if isinstance(states, str):
        states = [states]
    return sum([self.tv.s[state].size for state in states])

set(name, kwargs)

Set a state in the StateDict.

Parameters:

Name Type Description Default
name str

Name of the state.

required
kwargs Any

State attributes.

required

Raises:

Type Description
ValueError

If the state is already in the StateDict.

Exception

If the state cannot be added.

Source code in src/tolvera/state.py
def set(self, name, kwargs: Any) -> None:
    """Set a state in the StateDict.

    Args:
        name (str): Name of the state.
        kwargs (Any): State attributes.

    Raises:
        ValueError: If the state is already in the StateDict.
        Exception: If the state cannot be added.
    """
    if name in self and name != "size":
        raise ValueError(f"[tolvera.state.StateDict] '{name}' already in dict.")
    try:
        self.add(name, kwargs)
    except Exception as e:
        raise type(e)(f"[tolvera.state.StateDict] {e}") from e

dotdict

Bases: dict

dot.notation access to dictionary attributes

Source code in src/tolvera/utils.py
class dotdict(dict):
    """dot.notation access to dictionary attributes"""
    __getattr__ = dict.get
    __setattr__ = dict.__setitem__
    __delattr__ = dict.__delitem__

create_and_validate_slice(arg, target_array)

Creates and validates a slice object based on the target array.

Source code in src/tolvera/utils.py
def create_and_validate_slice(
    arg: Union[int, tuple[int, ...], slice], target_array: np.ndarray
) -> slice:
    """
    Creates and validates a slice object based on the target array.
    """
    try:
        slice_obj = create_safe_slice(arg)
        if not validate_slice(slice_obj, target_array):
            raise ValueError(f"Invalid slice: {slice_obj}")
        return slice_obj
    except Exception as e:
        raise type(e)(f"Error creating slice: {e}")

create_ndslices(dims)

Create a multi-dimensional slice from a list of tuples.

Parameters:

Name Type Description Default
dims list[tuple]

A list of tuples containing the slice parameters for each dimension.

required

Returns:

Type Description
s_

np.s_: A multi-dimensional slice object.

Source code in src/tolvera/utils.py
def create_ndslices(dims: list[tuple]) -> np.s_:
    """
    Create a multi-dimensional slice from a list of tuples.

    Args:
        dims (list[tuple]): A list of tuples containing the slice parameters for each dimension.

    Returns:
        np.s_: A multi-dimensional slice object.
    """
    return np.s_[tuple(slice(*dim) if isinstance(dim, tuple) else dim for dim in dims)]

create_safe_slice(arg)

Creates a slice object based on the input argument.

Parameters:

Name Type Description Default
arg (int, tuple, slice)

The argument for creating the slice. It can be an integer, a tuple with slice parameters, or a slice object itself.

required

Returns:

Name Type Description
slice slice

A slice object created based on the provided argument.

Source code in src/tolvera/utils.py
def create_safe_slice(arg: Union[int, tuple[int, ...], slice]) -> slice:
    """
    Creates a slice object based on the input argument.

    Args:
        arg (int, tuple, slice): The argument for creating the slice. It can be an integer,
                                 a tuple with slice parameters, or a slice object itself.

    Returns:
        slice: A slice object created based on the provided argument.
    """
    try:
        if isinstance(arg, slice):
            return arg
        elif isinstance(arg, tuple):
            return slice(*arg)
        elif isinstance(arg, int):
            return slice(arg, arg + 1)
        else:
            raise TypeError(f"Invalid slice type: {type(arg)} {arg}")
    except Exception as e:
        raise type(e)(f"[create_safe_slice] Error creating slice: {e}")

flatten(lst)

Flatten a nested list or return a non-nested list as is.

Source code in src/tolvera/utils.py
def flatten(lst):
    """Flatten a nested list or return a non-nested list as is."""
    if all(isinstance(el, list) for el in lst):
        return [item for sublist in lst for item in sublist]
    return lst

generic_slice(array, slice_params)

Slices a NumPy array based on a tuple of slice parameters for each dimension.

Parameters:

Name Type Description Default
array ndarray

The array to be sliced.

required
slice_params tuple

A tuple where each item is either an integer, a tuple with slice parameters, or a slice object.

required

Returns:

Name Type Description
ndarray ndarray

The sliced array.

Source code in src/tolvera/utils.py
def generic_slice(
    array: np.ndarray,
    slice_params: Union[
        tuple[Union[int, tuple[int, ...], slice], ...],
        Union[int, tuple[int, ...], slice],
    ],
) -> np.ndarray:
    """
    Slices a NumPy array based on a tuple of slice parameters for each dimension.

    Args:
        array (np.ndarray): The array to be sliced.
        slice_params (tuple): A tuple where each item is either an integer, a tuple with
                             slice parameters, or a slice object.

    Returns:
        ndarray: The sliced array.
    """
    if not isinstance(slice_params, tuple):
        slice_params = (slice_params,)
    slices = tuple(create_safe_slice(param) for param in slice_params)
    return array.__getitem__(slices)

time_function(func, *args, **kwargs)

Time how long it takes to run a function and print the result

Source code in src/tolvera/utils.py
def time_function(func, *args, **kwargs):
    """Time how long it takes to run a function and print the result"""
    start = time.time()
    ret = func(*args, **kwargs)
    end = time.time()
    print(f"[Tolvera.utils] {func.__name__}() ran in {end-start:.4f}s")
    if ret is not None:
        return (ret, end - start)
    return end - start

validate_json_path(path)

Validate a JSON file path. It uses validate_path for initial validation.

Parameters:

Name Type Description Default
path str

The JSON file path to be validated.

required

Returns:

Name Type Description
bool bool

True if the path is a valid JSON file path, raises an exception otherwise.

Raises:

Type Description
ValueError

If the path does not end with '.json'.

Source code in src/tolvera/utils.py
def validate_json_path(path: str) -> bool:
    """
    Validate a JSON file path. It uses validate_path for initial validation.

    Args:
        path (str): The JSON file path to be validated.

    Returns:
        bool: True if the path is a valid JSON file path, raises an exception otherwise.

    Raises:
        ValueError: If the path does not end with '.json'.
    """
    # Using validate_path for basic path validation
    validate_path(path)

    if not path.endswith(".json"):
        raise ValueError("Path should end with '.json'")

    return True

validate_path(path)

Validate a path using os.path and pathlib.

Parameters:

Name Type Description Default
path str

The path to be validated.

required

Returns:

Name Type Description
bool bool

True if the path is valid, raises an exception otherwise.

Raises:

Type Description
TypeError

If the input is not a string.

FileNotFoundError

If the path does not exist.

PermissionError

If the path is not accessible.

Source code in src/tolvera/utils.py
def validate_path(path: str) -> bool:
    """
    Validate a path using os.path and pathlib.

    Args:
        path (str): The path to be validated.

    Returns:
        bool: True if the path is valid, raises an exception otherwise.

    Raises:
        TypeError: If the input is not a string.
        FileNotFoundError: If the path does not exist.
        PermissionError: If the path is not accessible.
    """
    if not isinstance(path, str):
        raise TypeError(f"Expected a string for path, but received {type(path)}")

    path_obj = Path(path)
    if not path_obj.is_file():
        raise FileNotFoundError(f"The path {path} does not exist or is not a file")

    if not os.access(path, os.R_OK):
        raise PermissionError(f"The path {path} is not accessible")

    return True

validate_slice(slice_obj, target_array)

Validates if the given slice object is applicable to the target ndarray.

Parameters:

Name Type Description Default
slice_obj tuple[slice]

A tuple containing slice objects for each dimension.

required
target_array ndarray

The array to be sliced.

required

Returns:

Name Type Description
bool bool

True if the slice is valid for the given array, False otherwise.

Source code in src/tolvera/utils.py
def validate_slice(slice_obj: tuple[slice], target_array: np.ndarray) -> bool:
    """
    Validates if the given slice object is applicable to the target ndarray.

    Args:
        slice_obj (tuple[slice]): A tuple containing slice objects for each dimension.
        target_array (np.ndarray): The array to be sliced.

    Returns:
        bool: True if the slice is valid for the given array, False otherwise.
    """
    if len(slice_obj) != target_array.ndim:
        return False

    for sl, size in zip(slice_obj, target_array.shape):
        # Check if slice start and stop are within the dimension size
        start, stop, _ = sl.indices(size)
        if not (0 <= start < size and (0 <= stop <= size or stop == -1)):
            return False
    return True