Skip to content

Module streamgen.parameter.store

🗃️ parameter stores are dictionary-like collections of parameters and schedules.

View Source
"""🗃️ parameter stores are dictionary-like collections of parameters and schedules."""

from typing import Any, Self

import pandas as pd

from beartype import beartype

from loguru import logger

from rich.pretty import pretty_repr

from streamgen.parameter import Parameter, ScopedParameterDict

@beartype()

class ParameterStore:

    """🗃️ a dictionary-like container of `streamgen.parameter.Parameter` with their names as keys.

    The dictionary can be nested one level to create parameter scopes.

    All top-level parameters are considered as having `scope=None`.

    Args:

        parameters (list[Parameter] | ScopedParameterDict): parameters to store

    Raises:

        ValueError: if `parameters` are of type `ScopedParameterDict` and are nested more than two levels.

    Examples:

        >>> store = ParameterStore(

                {

                    "var1": {

                        "value": 1,

                        "schedule": [2,3],

                        "strategy": "cycle",

                    },

                    "var2": {

                        "name": "var2", # can be present, but is not needed

                        "schedule": [0.1, 0.2, 0.3],

                    },

                    "scope1": {

                        "var1": { # var1 can be used again since its inside a scope

                            "value": 1,

                            "schedule": [2,3],

                            "strategy": "cycle",

                        },

                    },

                }

            )

        >>> store = ParameterStore([

                    Parameter("var1", 1, [2]),

                    Parameter("var2", schedule=[0.0, 1.0]),

                ]

            })

    """

    def __init__(self, parameters: list[Parameter] | ScopedParameterDict) -> None:  # noqa: D107

        self.scopes: set[str] = set()

        match parameters:

            case list(parameters):

                self.parameters: dict[str, Parameter] = {p.name: p for p in parameters}

                self.parameter_names: set[str] = {p.name for p in parameters}

            case dict(parameters):

                self.parameters = {}

                self.parameter_names: set[str] = set()

                for key, dictionary in parameters.items():

                    if self._dict_depth(dictionary) == 2:  # then key is a scope name  # noqa: PLR2004

                        scope = key

                        self.scopes.add(scope)

                        self.parameters[scope] = {}

                        # remove `name` entries since they are redundant

                        for name, parameter_kwargs in parameters[scope].items():

                            parameter_kwargs.pop("name", None)

                            self.parameters[scope][name] = Parameter(name=name, **parameter_kwargs)

                            self.parameter_names.add(f"{scope}.{self.parameters[scope][name].name}")

                    elif self._dict_depth(dictionary) == 1:  # otherwise its a top-level parameter

                        parameters[key].pop("name", None)

                        self.parameters[key] = Parameter(name=key, **parameters[key])

                        self.parameter_names.add(self.parameters[key].name)

                    else:

                        logger.warning("📚 parameters of type `ScopedParameterDict` should not be nested more than two levels.")

                        raise ValueError

    @staticmethod

    def _dict_depth(dictionary: Any) -> int:  # noqa: ANN401

        """📚 get the nesting-level/depth of a dictionary.

        Code adapted from https://www.geeksforgeeks.org/python-find-depth-of-a-dictionary/

        Args:

            dictionary (Any): a dictionary or any of its values

        Returns:

            int: depth of dictionary

        Examples:

            >>> store = ParameterStore._dict_depth({1: "a", 2: {3: {4: {}}}})

            4

        """

        if isinstance(dictionary, dict):

            return 1 + (max(map(ParameterStore._dict_depth, dictionary.values())) if dictionary else 0)

        return 0

    def __getitem__(self, name: str) -> Parameter:

        """🫱 gets a parameter by its name using `store[name]` syntax.

        Scoped parameters are fetched using `{scope}.{parameter.name}`.

        Args:

            name (str): name of the parameter

        Returns:

            Parameter: the parameter

        """

        if "." in name:

            scope, name = name.split(".")

            return self.parameters[scope][name]

        return self.parameters[name]

    def set_update_step(self, idx: int) -> None:

        """🕐 updates every parameter of `self` to a certain update step using `param[idx]`.

        Args:

            idx (int): parameter update step

        Returns:

            None: this function mutates `self`

        """

        for param in self.parameters.values():

            # indexing of parameters mutates their internal state, so we do not have to set anything

            param[idx]

    def get_scope(self, scope: str) -> Self | None:

        """🔭 get all parameters in a scope as a new `ParameterStore`.

        Args:

            scope (str): scope name

        Returns:

            ParameterStore | None: parameter store of all parameters inside the scope. None if scope is not present.

        """

        if scope not in self.scopes:

            return None

        return ParameterStore(list(self.parameters[scope].values()))

    def get_params(self) -> dict[str, Any]:

        """📖 get the current parameters as one dictionary.

        Scoped parameters are represented by "{scope}.{parameter.name}".

        Returns:

            dict[str, Any]: dictionary with scoped parameter names as keys and `parameter.value` as values.

        """

        return {name: self[name].value for name in sorted(self.parameter_names)}

    def update(self) -> dict[str, Any]:

        """🆙 updates every parameter and returns them using `ParameterStore.get_params`.

        Returns:

            dict[str, Any]: dictionary with `parameter.name` as keys and `parameter.value` as values

        """

        for name in self.parameter_names:

            self[name].update()

        return self.get_params()

    def to_dataframe(self, number_of_update_steps: int = 0) -> pd.DataFrame:

        """📅🐼 rolls out `number_of_update_steps` and returns the result as a `pd.DataFrame`.

        Args:

            number_of_update_steps (int, optional): number of rows in the dataframe. Defaults to 0.

        Returns:

            pd.DataFrame: parameter store as dataframe

        """

        return pd.DataFrame(

            [self.get_params()] + ([self.update() for _ in range(number_of_update_steps)] if number_of_update_steps > 0 else []),

        )

    @staticmethod

    def from_dataframe(df: pd.DataFrame) -> Self:

        """📅🐼 constructs a `ParameterStore` from a dataframe.

        Each columns represents a parameter.

        columns can be namespaced following the same "{scope}.{parameter.name}" rules as `ParameterStore.__getitem__`.

        Args:

            df (pd.DataFrame): dataframe

        Returns:

            ParameterStore: parameter store

        """

        scopes: set[str] = {name.split(".")[0] for name in df.columns if "." in name}

        params = {}

        for scope in scopes:

            params[scope] = {}

            for name in df.columns:

                if not name.startswith(f"{scope}."):

                    continue

                parameter_name = name.split(".")[1]

                params[scope][parameter_name] = {"schedule": df[name]}

        for name in df.columns:

            if "." in name:

                continue

            params[name] = {"schedule": df[name]}

        return ParameterStore(params)

    def __or__(self, params: Parameter | Self) -> Self:

        """➕ combines self with another `Parameter` or `ParameterStore` using `|`.

        This function takes care of merging the scopes properly.

        Args:

            params (Parameter | ParameterStore): another parameter or store

        Returns:

            ParameterStore: combined parameter store

        """  # noqa: RUF002

        if isinstance(params, Parameter):

            params = ParameterStore([params])

        for scope in params.scopes:

            if scope not in self.scopes:

                self.parameters[scope] = {}

                self.scopes.add(scope)

        for param in params.parameter_names:

            if "." in param:

                scope, name = param.split(".")

                self.parameters[scope][name] = params[param]

            else:

                self.parameters[param] = params[param]

            self.parameter_names.add(param)

        return self

    def __repr__(self) -> str:

        """🏷️ Returns the debug string representation of self.

        Returns:

            str: string representation of self

        """

        s = "("

        for name in sorted(self.parameter_names):

            s += str(self[name]) + ", "

        return s[:-2] + ")"

    def __str__(self) -> str:

        """🏷️ Returns the string representation `str(self)`.

        Returns:

            str: string representation of self

        """

        return pretty_repr(self.get_params())

Classes

ParameterStore

class ParameterStore(
    parameters: list[streamgen.parameter.Parameter] | dict[str, streamgen.parameter.ParameterDict | dict[str, streamgen.parameter.ParameterDict]]
)

🗃️ a dictionary-like container of streamgen.parameter.Parameter with their names as keys.

The dictionary can be nested one level to create parameter scopes. All top-level parameters are considered as having scope=None.

Attributes

Name Type Description Default
parameters list[Parameter] ScopedParameterDict parameters to store
View Source
@beartype()

class ParameterStore:

    """🗃️ a dictionary-like container of `streamgen.parameter.Parameter` with their names as keys.

    The dictionary can be nested one level to create parameter scopes.

    All top-level parameters are considered as having `scope=None`.

    Args:

        parameters (list[Parameter] | ScopedParameterDict): parameters to store

    Raises:

        ValueError: if `parameters` are of type `ScopedParameterDict` and are nested more than two levels.

    Examples:

        >>> store = ParameterStore(

                {

                    "var1": {

                        "value": 1,

                        "schedule": [2,3],

                        "strategy": "cycle",

                    },

                    "var2": {

                        "name": "var2", # can be present, but is not needed

                        "schedule": [0.1, 0.2, 0.3],

                    },

                    "scope1": {

                        "var1": { # var1 can be used again since its inside a scope

                            "value": 1,

                            "schedule": [2,3],

                            "strategy": "cycle",

                        },

                    },

                }

            )

        >>> store = ParameterStore([

                    Parameter("var1", 1, [2]),

                    Parameter("var2", schedule=[0.0, 1.0]),

                ]

            })

    """

    def __init__(self, parameters: list[Parameter] | ScopedParameterDict) -> None:  # noqa: D107

        self.scopes: set[str] = set()

        match parameters:

            case list(parameters):

                self.parameters: dict[str, Parameter] = {p.name: p for p in parameters}

                self.parameter_names: set[str] = {p.name for p in parameters}

            case dict(parameters):

                self.parameters = {}

                self.parameter_names: set[str] = set()

                for key, dictionary in parameters.items():

                    if self._dict_depth(dictionary) == 2:  # then key is a scope name  # noqa: PLR2004

                        scope = key

                        self.scopes.add(scope)

                        self.parameters[scope] = {}

                        # remove `name` entries since they are redundant

                        for name, parameter_kwargs in parameters[scope].items():

                            parameter_kwargs.pop("name", None)

                            self.parameters[scope][name] = Parameter(name=name, **parameter_kwargs)

                            self.parameter_names.add(f"{scope}.{self.parameters[scope][name].name}")

                    elif self._dict_depth(dictionary) == 1:  # otherwise its a top-level parameter

                        parameters[key].pop("name", None)

                        self.parameters[key] = Parameter(name=key, **parameters[key])

                        self.parameter_names.add(self.parameters[key].name)

                    else:

                        logger.warning("📚 parameters of type `ScopedParameterDict` should not be nested more than two levels.")

                        raise ValueError

    @staticmethod

    def _dict_depth(dictionary: Any) -> int:  # noqa: ANN401

        """📚 get the nesting-level/depth of a dictionary.

        Code adapted from https://www.geeksforgeeks.org/python-find-depth-of-a-dictionary/

        Args:

            dictionary (Any): a dictionary or any of its values

        Returns:

            int: depth of dictionary

        Examples:

            >>> store = ParameterStore._dict_depth({1: "a", 2: {3: {4: {}}}})

            4

        """

        if isinstance(dictionary, dict):

            return 1 + (max(map(ParameterStore._dict_depth, dictionary.values())) if dictionary else 0)

        return 0

    def __getitem__(self, name: str) -> Parameter:

        """🫱 gets a parameter by its name using `store[name]` syntax.

        Scoped parameters are fetched using `{scope}.{parameter.name}`.

        Args:

            name (str): name of the parameter

        Returns:

            Parameter: the parameter

        """

        if "." in name:

            scope, name = name.split(".")

            return self.parameters[scope][name]

        return self.parameters[name]

    def set_update_step(self, idx: int) -> None:

        """🕐 updates every parameter of `self` to a certain update step using `param[idx]`.

        Args:

            idx (int): parameter update step

        Returns:

            None: this function mutates `self`

        """

        for param in self.parameters.values():

            # indexing of parameters mutates their internal state, so we do not have to set anything

            param[idx]

    def get_scope(self, scope: str) -> Self | None:

        """🔭 get all parameters in a scope as a new `ParameterStore`.

        Args:

            scope (str): scope name

        Returns:

            ParameterStore | None: parameter store of all parameters inside the scope. None if scope is not present.

        """

        if scope not in self.scopes:

            return None

        return ParameterStore(list(self.parameters[scope].values()))

    def get_params(self) -> dict[str, Any]:

        """📖 get the current parameters as one dictionary.

        Scoped parameters are represented by "{scope}.{parameter.name}".

        Returns:

            dict[str, Any]: dictionary with scoped parameter names as keys and `parameter.value` as values.

        """

        return {name: self[name].value for name in sorted(self.parameter_names)}

    def update(self) -> dict[str, Any]:

        """🆙 updates every parameter and returns them using `ParameterStore.get_params`.

        Returns:

            dict[str, Any]: dictionary with `parameter.name` as keys and `parameter.value` as values

        """

        for name in self.parameter_names:

            self[name].update()

        return self.get_params()

    def to_dataframe(self, number_of_update_steps: int = 0) -> pd.DataFrame:

        """📅🐼 rolls out `number_of_update_steps` and returns the result as a `pd.DataFrame`.

        Args:

            number_of_update_steps (int, optional): number of rows in the dataframe. Defaults to 0.

        Returns:

            pd.DataFrame: parameter store as dataframe

        """

        return pd.DataFrame(

            [self.get_params()] + ([self.update() for _ in range(number_of_update_steps)] if number_of_update_steps > 0 else []),

        )

    @staticmethod

    def from_dataframe(df: pd.DataFrame) -> Self:

        """📅🐼 constructs a `ParameterStore` from a dataframe.

        Each columns represents a parameter.

        columns can be namespaced following the same "{scope}.{parameter.name}" rules as `ParameterStore.__getitem__`.

        Args:

            df (pd.DataFrame): dataframe

        Returns:

            ParameterStore: parameter store

        """

        scopes: set[str] = {name.split(".")[0] for name in df.columns if "." in name}

        params = {}

        for scope in scopes:

            params[scope] = {}

            for name in df.columns:

                if not name.startswith(f"{scope}."):

                    continue

                parameter_name = name.split(".")[1]

                params[scope][parameter_name] = {"schedule": df[name]}

        for name in df.columns:

            if "." in name:

                continue

            params[name] = {"schedule": df[name]}

        return ParameterStore(params)

    def __or__(self, params: Parameter | Self) -> Self:

        """➕ combines self with another `Parameter` or `ParameterStore` using `|`.

        This function takes care of merging the scopes properly.

        Args:

            params (Parameter | ParameterStore): another parameter or store

        Returns:

            ParameterStore: combined parameter store

        """  # noqa: RUF002

        if isinstance(params, Parameter):

            params = ParameterStore([params])

        for scope in params.scopes:

            if scope not in self.scopes:

                self.parameters[scope] = {}

                self.scopes.add(scope)

        for param in params.parameter_names:

            if "." in param:

                scope, name = param.split(".")

                self.parameters[scope][name] = params[param]

            else:

                self.parameters[param] = params[param]

            self.parameter_names.add(param)

        return self

    def __repr__(self) -> str:

        """🏷️ Returns the debug string representation of self.

        Returns:

            str: string representation of self

        """

        s = "("

        for name in sorted(self.parameter_names):

            s += str(self[name]) + ", "

        return s[:-2] + ")"

    def __str__(self) -> str:

        """🏷️ Returns the string representation `str(self)`.

        Returns:

            str: string representation of self

        """

        return pretty_repr(self.get_params())

Static methods

from_dataframe

def from_dataframe(
    df: pandas.core.frame.DataFrame
) -> Self

📅🐼 constructs a ParameterStore from a dataframe.

Each columns represents a parameter. columns can be namespaced following the same "{scope}.{parameter.name}" rules as ParameterStore.__getitem__.

Parameters:

Name Type Description Default
df pd.DataFrame dataframe None

Returns:

Type Description
ParameterStore parameter store
View Source
    @staticmethod

    def from_dataframe(df: pd.DataFrame) -> Self:

        """📅🐼 constructs a `ParameterStore` from a dataframe.

        Each columns represents a parameter.

        columns can be namespaced following the same "{scope}.{parameter.name}" rules as `ParameterStore.__getitem__`.

        Args:

            df (pd.DataFrame): dataframe

        Returns:

            ParameterStore: parameter store

        """

        scopes: set[str] = {name.split(".")[0] for name in df.columns if "." in name}

        params = {}

        for scope in scopes:

            params[scope] = {}

            for name in df.columns:

                if not name.startswith(f"{scope}."):

                    continue

                parameter_name = name.split(".")[1]

                params[scope][parameter_name] = {"schedule": df[name]}

        for name in df.columns:

            if "." in name:

                continue

            params[name] = {"schedule": df[name]}

        return ParameterStore(params)

Methods

get_params

def get_params(
    self
) -> dict[str, typing.Any]

📖 get the current parameters as one dictionary.

Scoped parameters are represented by "{scope}.{parameter.name}".

Returns:

Type Description
dict[str, Any] dictionary with scoped parameter names as keys and parameter.value as values.
View Source
    def get_params(self) -> dict[str, Any]:

        """📖 get the current parameters as one dictionary.

        Scoped parameters are represented by "{scope}.{parameter.name}".

        Returns:

            dict[str, Any]: dictionary with scoped parameter names as keys and `parameter.value` as values.

        """

        return {name: self[name].value for name in sorted(self.parameter_names)}

get_scope

def get_scope(
    self,
    scope: str
) -> Optional[Self]

🔭 get all parameters in a scope as a new ParameterStore.

Parameters:

Name Type Description Default
scope str scope name None

Returns:

Type Description
None ParameterStore
View Source
    def get_scope(self, scope: str) -> Self | None:

        """🔭 get all parameters in a scope as a new `ParameterStore`.

        Args:

            scope (str): scope name

        Returns:

            ParameterStore | None: parameter store of all parameters inside the scope. None if scope is not present.

        """

        if scope not in self.scopes:

            return None

        return ParameterStore(list(self.parameters[scope].values()))

set_update_step

def set_update_step(
    self,
    idx: int
) -> None

🕐 updates every parameter of self to a certain update step using param[idx].

Parameters:

Name Type Description Default
idx int parameter update step None

Returns:

Type Description
None this function mutates self
View Source
    def set_update_step(self, idx: int) -> None:

        """🕐 updates every parameter of `self` to a certain update step using `param[idx]`.

        Args:

            idx (int): parameter update step

        Returns:

            None: this function mutates `self`

        """

        for param in self.parameters.values():

            # indexing of parameters mutates their internal state, so we do not have to set anything

            param[idx]

to_dataframe

def to_dataframe(
    self,
    number_of_update_steps: int = 0
) -> pandas.core.frame.DataFrame

📅🐼 rolls out number_of_update_steps and returns the result as a pd.DataFrame.

Parameters:

Name Type Description Default
number_of_update_steps int number of rows in the dataframe. Defaults to 0. 0

Returns:

Type Description
pd.DataFrame parameter store as dataframe
View Source
    def to_dataframe(self, number_of_update_steps: int = 0) -> pd.DataFrame:

        """📅🐼 rolls out `number_of_update_steps` and returns the result as a `pd.DataFrame`.

        Args:

            number_of_update_steps (int, optional): number of rows in the dataframe. Defaults to 0.

        Returns:

            pd.DataFrame: parameter store as dataframe

        """

        return pd.DataFrame(

            [self.get_params()] + ([self.update() for _ in range(number_of_update_steps)] if number_of_update_steps > 0 else []),

        )

update

def update(
    self
) -> dict[str, typing.Any]

🆙 updates every parameter and returns them using ParameterStore.get_params.

Returns:

Type Description
dict[str, Any] dictionary with parameter.name as keys and parameter.value as values
View Source
    def update(self) -> dict[str, Any]:

        """🆙 updates every parameter and returns them using `ParameterStore.get_params`.

        Returns:

            dict[str, Any]: dictionary with `parameter.name` as keys and `parameter.value` as values

        """

        for name in self.parameter_names:

            self[name].update()

        return self.get_params()