5. Collections#

A dependency field can hold a list of keys, or a dict of them keyed by string. Use a list when order is what matters — a pipeline of stages, a set of loss terms — and a dict when the members need names.

python examples/tutorial/05_collections.py
class ModelConfig(Configuration):
    losses: list[RegistrationKey] = Param(
        [key("cross_entropy"), key("sparsity")],
        description="Applied in order",
    )
    metrics: dict[str, RegistrationKey] = Param(
        {"accuracy": key("cross_entropy")},
        description="Reported under these names",
    )

The component receives the container of keys, in the shape it was declared with, and Registry.from_keys builds the whole thing at once while keeping that shape.

class Model:
    def __init__(
        self,
        losses: list[RegistrationKey],
        metrics: dict[str, RegistrationKey],
    ):
        # `from_keys` builds a whole dependency at once and keeps its shape: a
        # list stays a list in the same order, a dict keeps its labels. It is
        # the same thing as
        #
        #     [Registry.from_key(key) for key in losses]
        #     {name: Registry.from_key(key) for name, key in metrics.items()}
        #
        # without a comprehension per field saying "build these" twice.
        self.losses = Registry.from_keys(losses)
        self.metrics = Registry.from_keys(metrics)

What to notice#

  • One level only. list[list[RegistrationKey]] is refused, with an error that says so. Nesting would mean inventing a path language to address a key inside a container, and the graph would stop being a graph over keys.

  • Optional[list[RegistrationKey]] and the parameterized RegistrationKey[Tokenizer] are both dependencies too.

  • A container varies through variants declared on the whole field — Param([a, b], variants=[[a], [a, b, c]]) — not by its members varying individually. Registration Dependencies explains why, and what the derived tag looks like.

The whole file#

  1"""
  25. Depending on many registrations at once.
  3
  4    python examples/tutorial/05_collections.py
  5
  6A dependency field can hold a ``list`` of keys, or a ``dict`` of them keyed by
  7string. Use a list when order is what matters (a pipeline of stages, a set of
  8loss terms) and a dict when the members need names (metrics you will report
  9individually).
 10
 11One level only: ``list[list[RegistrationKey]]`` is refused, with an error that
 12says so. Nesting would mean inventing a path language to address a key inside a
 13container, and the graph would stop being a graph over keys.
 14"""
 15
 16from cinnamon.configuration import Configuration, Param
 17from cinnamon.registry import RegistrationKey, Registry
 18
 19NAMESPACE = "tutorial"
 20
 21
 22def key(name: str) -> RegistrationKey:
 23    return RegistrationKey(name=name, namespace=NAMESPACE)
 24
 25
 26class Metric:
 27    def __init__(self, power: int):
 28        self.power = power
 29
 30    def score(self, value: float) -> float:
 31        return round(value**self.power, 3)
 32
 33
 34class Model:
 35    def __init__(
 36        self,
 37        losses: list[RegistrationKey],
 38        metrics: dict[str, RegistrationKey],
 39    ):
 40        # `from_keys` builds a whole dependency at once and keeps its shape: a
 41        # list stays a list in the same order, a dict keeps its labels. It is
 42        # the same thing as
 43        #
 44        #     [Registry.from_key(key) for key in losses]
 45        #     {name: Registry.from_key(key) for name, key in metrics.items()}
 46        #
 47        # without a comprehension per field saying "build these" twice.
 48        self.losses = Registry.from_keys(losses)
 49        self.metrics = Registry.from_keys(metrics)
 50
 51
 52class MetricConfig(Configuration):
 53    power: int = Param(1)
 54
 55
 56class ModelConfig(Configuration):
 57    losses: list[RegistrationKey] = Param(
 58        [key("cross_entropy"), key("sparsity")],
 59        description="Applied in order",
 60    )
 61    metrics: dict[str, RegistrationKey] = Param(
 62        {"accuracy": key("cross_entropy")},
 63        description="Reported under these names",
 64    )
 65
 66
 67def main() -> None:
 68    Registry.initialize()
 69    for name, power in (("cross_entropy", 1), ("sparsity", 2)):
 70        Registry.register_configuration(
 71            MetricConfig(power=power),
 72            name=name,
 73            namespace=NAMESPACE,
 74            component=f"{__name__}.Metric",
 75        )
 76    Registry.register_configuration(
 77        ModelConfig(),
 78        name="model",
 79        namespace=NAMESPACE,
 80        component=f"{__name__}.Model",
 81    )
 82    Registry.dag_resolution()
 83
 84    model = Registry.from_key(key("model"))
 85    print("losses (ordered):", [loss.score(3.0) for loss in model.losses])
 86    print("metrics (named):  ", {n: m.score(3.0) for n, m in model.metrics.items()})
 87
 88    # `from_keys` takes a single key too, so a field typed
 89    # `RegistrationKey | list[RegistrationKey]` needs no branch, and `None`
 90    # comes back as `None` for an optional dependency that was left unset.
 91    # It builds eagerly: keep the comprehension when a child should only be
 92    # built under some condition. That laziness is why components are handed
 93    # keys rather than instances in the first place.
 94
 95    # Every member is a real edge in the graph, so a typo in any one of them is
 96    # caught by `cmn-check` rather than at the moment you try to build.
 97    config = Registry.retrieve_configuration(registration_key=key("model"))
 98    print("\ndependency fields:", list(config.dependencies))
 99
100    print(
101        "\nNote what a container does NOT do: a member's own variants are not"
102        "\nmultiplied into the parent, the way a scalar dependency's are. Three"
103        "\nlosses with three variants each would be 27 parent keys from one"
104        "\nfield. To vary a container, vary the whole thing:"
105        "\n    Param([A], variants=[[A, B], [A, B, C]])"
106    )
107
108    try:
109
110        class Nested(Configuration):
111            groups: list[list[RegistrationKey]] = []
112
113        Nested().dependencies
114    except TypeError as error:
115        message = str(error)
116        print("\nnesting is refused:")
117        print("  " + message[message.index("Nested containers") :])
118
119
120if __name__ == "__main__":
121    main()

Next: 6. Conditions — rejecting the combinations that make no sense.