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 parameterizedRegistrationKey[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.