MATE-libraries 1.0.0.dev0__tar.gz → 1.0.0rc1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of MATE-libraries might be problematic. Click here for more details.
- {mate_libraries-1.0.0.dev0 → mate_libraries-1.0.0rc1}/.gitignore +2 -0
- {mate_libraries-1.0.0.dev0 → mate_libraries-1.0.0rc1}/PKG-INFO +7 -4
- {mate_libraries-1.0.0.dev0 → mate_libraries-1.0.0rc1}/README.md +5 -2
- {mate_libraries-1.0.0.dev0 → mate_libraries-1.0.0rc1}/pyproject.toml +2 -2
- mate_libraries-1.0.0rc1/src/MATE_libraries/networks/__init__.py +67 -0
- mate_libraries-1.0.0rc1/src/MATE_libraries/networks/_catalogue.py +258 -0
- mate_libraries-1.0.0rc1/src/MATE_libraries/networks/_compiled.py +463 -0
- mate_libraries-1.0.0rc1/src/MATE_libraries/networks/_graph.py +139 -0
- mate_libraries-1.0.0rc1/src/MATE_libraries/networks/_networkx.py +249 -0
- mate_libraries-1.0.0rc1/src/MATE_libraries/networks/_organ.py +100 -0
- mate_libraries-1.0.0rc1/src/MATE_libraries/networks/_prepare.py +492 -0
- mate_libraries-1.0.0rc1/src/MATE_libraries/networks/_propagate.py +173 -0
- mate_libraries-1.0.0rc1/src/MATE_libraries/networks/_state.py +332 -0
- mate_libraries-1.0.0rc1/src/MATE_libraries/networks/_techniques.py +430 -0
- mate_libraries-1.0.0rc1/src/MATE_libraries/operators/__init__.py +54 -0
- mate_libraries-1.0.0rc1/src/MATE_libraries/operators/_draws.py +121 -0
- mate_libraries-1.0.0rc1/src/MATE_libraries/operators/_selection.py +607 -0
- mate_libraries-1.0.0rc1/src/MATE_libraries/operators/_variation.py +121 -0
- {mate_libraries-1.0.0.dev0 → mate_libraries-1.0.0rc1}/COPYING +0 -0
- {mate_libraries-1.0.0.dev0 → mate_libraries-1.0.0rc1}/COPYING.LESSER +0 -0
- {mate_libraries-1.0.0.dev0 → mate_libraries-1.0.0rc1}/src/MATE_libraries/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: MATE-libraries
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0rc1
|
|
4
4
|
Summary: Libraries of MATE, the Modular Algorithmic Toolkit for Evolution: network library and standard operators library for MATE modules
|
|
5
5
|
Project-URL: Homepage, https://gitlab.com/artificial_evolution/mate/mate
|
|
6
6
|
Project-URL: Issues, https://gitlab.com/artificial_evolution/mate/mate/-/issues
|
|
@@ -15,7 +15,7 @@ Classifier: Intended Audience :: Science/Research
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
16
|
Classifier: Topic :: Scientific/Engineering
|
|
17
17
|
Requires-Python: >=3.14
|
|
18
|
-
Requires-Dist: mate-contract<2,>=1.0.
|
|
18
|
+
Requires-Dist: mate-contract<2,>=1.0.0rc1
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
|
|
21
21
|
# MATE-libraries
|
|
@@ -25,8 +25,11 @@ Modular Algorithmic Toolkit for Evolution: the network library and the standard
|
|
|
25
25
|
operators library, which method and system modules may use. It depends on
|
|
26
26
|
`MATE-contract` only.
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
The standard operators library (`MATE_libraries.operators`) gives a variation
|
|
29
|
+
operator per domain and the standard selections. The network library
|
|
30
|
+
(`MATE_libraries.networks`) compiles batches of `Network` organs and propagates
|
|
31
|
+
them on the CPU, with results that do not depend on the batch. Each library has
|
|
32
|
+
its own `version`, recorded in every run whose modules import it.
|
|
30
33
|
|
|
31
34
|
## Installation
|
|
32
35
|
|
|
@@ -5,8 +5,11 @@ Modular Algorithmic Toolkit for Evolution: the network library and the standard
|
|
|
5
5
|
operators library, which method and system modules may use. It depends on
|
|
6
6
|
`MATE-contract` only.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
The standard operators library (`MATE_libraries.operators`) gives a variation
|
|
9
|
+
operator per domain and the standard selections. The network library
|
|
10
|
+
(`MATE_libraries.networks`) compiles batches of `Network` organs and propagates
|
|
11
|
+
them on the CPU, with results that do not depend on the batch. Each library has
|
|
12
|
+
its own `version`, recorded in every run whose modules import it.
|
|
10
13
|
|
|
11
14
|
## Installation
|
|
12
15
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "MATE-libraries"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.0rc1"
|
|
8
8
|
description = "Libraries of MATE, the Modular Algorithmic Toolkit for Evolution: network library and standard operators library for MATE modules"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.14"
|
|
@@ -21,7 +21,7 @@ classifiers = [
|
|
|
21
21
|
"Programming Language :: Python :: 3",
|
|
22
22
|
"Topic :: Scientific/Engineering",
|
|
23
23
|
]
|
|
24
|
-
dependencies = ["MATE-contract>=1.0.
|
|
24
|
+
dependencies = ["MATE-contract>=1.0.0rc1,<2"]
|
|
25
25
|
|
|
26
26
|
[project.urls]
|
|
27
27
|
Homepage = "https://gitlab.com/artificial_evolution/mate/mate"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""The network library: compile and propagate batches of ``Network`` organs.
|
|
2
|
+
|
|
3
|
+
Three objects are never fused: the organ (frozen arrays, one per individual), the compiled
|
|
4
|
+
network (immutable, batched, shared between threads, never serialised) and the activation
|
|
5
|
+
state (the node values of individual x episode rows, mutable and serialised by copy). The
|
|
6
|
+
caller chooses what changes the result (propagation mode, refresh rate, activation functions,
|
|
7
|
+
aggregation, fate of unfilled cells, compute dtype); the library chooses only what does not
|
|
8
|
+
(batching technique, shortcuts derived from the graph). Each individual's results are
|
|
9
|
+
bit-identical whether it is compiled alone or in any batch, under any technique.
|
|
10
|
+
``from_networkx`` builds an organ from a networkx graph, for hand-designed networks.
|
|
11
|
+
|
|
12
|
+
Attributes
|
|
13
|
+
----------
|
|
14
|
+
version : int
|
|
15
|
+
What the library computes; changes whenever a definition, a default, a convergence rule
|
|
16
|
+
or the set of accepted inputs changes (rules V1, V4 of the libraries specification). A
|
|
17
|
+
protocol may hold it fixed under the key ``networks.version``.
|
|
18
|
+
TECHNIQUES : tuple of str
|
|
19
|
+
The batching techniques: ``"dense"``, ``"padded"``, ``"block_diagonal"``.
|
|
20
|
+
ACTIVATIONS : Mapping[str, Activation]
|
|
21
|
+
Read-only catalogue of activation functions: ``id``, ``tanh``, ``relu``, ``sigmoid``,
|
|
22
|
+
``neat_sigmoid``, ``sin``, ``gauss``, ``abs``, ``step``.
|
|
23
|
+
AGGREGATIONS : Mapping[str, Aggregation]
|
|
24
|
+
Read-only catalogue of aggregations: ``sum`` (the default), ``product``, ``max``,
|
|
25
|
+
``min``, ``mean``.
|
|
26
|
+
|
|
27
|
+
Notes
|
|
28
|
+
-----
|
|
29
|
+
Section 4 of the libraries specification. The device path is deferred in MATE-libraries
|
|
30
|
+
1.0.0: ``Device("opencl")`` is refused, any other non-CPU device runs on the CPU, and
|
|
31
|
+
``CompiledNetwork.kernels()`` raises.
|
|
32
|
+
|
|
33
|
+
Examples
|
|
34
|
+
--------
|
|
35
|
+
>>> from MATE_libraries import networks as nets
|
|
36
|
+
>>> compiled = nets.compile(organs, spec, unfilled={"activation": "id"},
|
|
37
|
+
... device=ctx.device) # doctest: +SKIP
|
|
38
|
+
>>> act = compiled.state(10) # doctest: +SKIP
|
|
39
|
+
>>> y = compiled.step(act, x[t], refresh_rate=1) # doctest: +SKIP
|
|
40
|
+
>>> res = compiled.settle(u, max_steps=50, tol=1e-6) # doctest: +SKIP
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from typing import Final
|
|
44
|
+
|
|
45
|
+
from ._catalogue import ACTIVATIONS, AGGREGATIONS, VERSION, Activation, Aggregation
|
|
46
|
+
from ._compiled import TECHNIQUES, CompileDiagnostics, CompiledNetwork, compile, validate
|
|
47
|
+
from ._networkx import from_networkx
|
|
48
|
+
from ._state import ActivationState, IndividualActivation, SettleResult
|
|
49
|
+
|
|
50
|
+
version: Final[int] = VERSION
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
"version",
|
|
54
|
+
"TECHNIQUES",
|
|
55
|
+
"Activation",
|
|
56
|
+
"Aggregation",
|
|
57
|
+
"ACTIVATIONS",
|
|
58
|
+
"AGGREGATIONS",
|
|
59
|
+
"compile",
|
|
60
|
+
"validate",
|
|
61
|
+
"from_networkx",
|
|
62
|
+
"CompileDiagnostics",
|
|
63
|
+
"CompiledNetwork",
|
|
64
|
+
"ActivationState",
|
|
65
|
+
"IndividualActivation",
|
|
66
|
+
"SettleResult",
|
|
67
|
+
]
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Catalogues of activation functions and aggregations, and name resolution ([L] §4.5)."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Mapping
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from types import MappingProxyType
|
|
6
|
+
from typing import Any, Final
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
VERSION: Final[int] = 1
|
|
11
|
+
"""What the network library computes (rules V1, V4); exposed as ``networks.version``."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _kernels(value: Mapping[str, str], where: str) -> Mapping[str, str]:
|
|
15
|
+
"""Return a read-only copy of a backend-to-kernel-source mapping."""
|
|
16
|
+
if not isinstance(value, Mapping):
|
|
17
|
+
raise TypeError(f"{where}.kernels: expected a mapping, got {type(value).__name__}")
|
|
18
|
+
for backend, source in value.items():
|
|
19
|
+
if not isinstance(backend, str) or not isinstance(source, str):
|
|
20
|
+
raise TypeError(f"{where}.kernels: keys and values are str (backend -> source)")
|
|
21
|
+
return MappingProxyType(dict(value))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class Activation:
|
|
26
|
+
"""An activation function of the network library.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
function : callable
|
|
31
|
+
Elementwise, pure and dtype-preserving map of an array; its result for an element
|
|
32
|
+
must not depend on the element's position, the array length, alignment or strides.
|
|
33
|
+
kernels : mapping of str to str, default {}
|
|
34
|
+
Device kernel source per backend name; without one, the device path is lost.
|
|
35
|
+
|
|
36
|
+
Raises
|
|
37
|
+
------
|
|
38
|
+
TypeError
|
|
39
|
+
If ``function`` is not callable or ``kernels`` is not a mapping of str to str.
|
|
40
|
+
|
|
41
|
+
Notes
|
|
42
|
+
-----
|
|
43
|
+
Rules K2, K5 of the libraries specification. Pickles through its constructor, so that a
|
|
44
|
+
module keeping it can be sent to a worker process.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
function: Callable[[np.ndarray], np.ndarray]
|
|
48
|
+
kernels: Mapping[str, str] = field(default_factory=dict)
|
|
49
|
+
|
|
50
|
+
def __post_init__(self) -> None:
|
|
51
|
+
if not callable(self.function):
|
|
52
|
+
raise TypeError(
|
|
53
|
+
f"Activation.function: expected a callable, got {type(self.function).__name__}"
|
|
54
|
+
)
|
|
55
|
+
object.__setattr__(self, "kernels", _kernels(self.kernels, "Activation"))
|
|
56
|
+
|
|
57
|
+
def __reduce__(self) -> tuple[Any, ...]:
|
|
58
|
+
return (Activation, (self.function, dict(self.kernels)))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class Aggregation:
|
|
63
|
+
"""How a node combines the weighted terms of its present incoming edges.
|
|
64
|
+
|
|
65
|
+
Parameters
|
|
66
|
+
----------
|
|
67
|
+
combine : callable
|
|
68
|
+
``(accumulator, term) -> accumulator``, elementwise; the fold starts from the first
|
|
69
|
+
term and follows the canonical edge order.
|
|
70
|
+
empty : float
|
|
71
|
+
Value of a node without any present incoming edge.
|
|
72
|
+
finalize : callable, optional
|
|
73
|
+
``(accumulator, count) -> value``, applied after the fold.
|
|
74
|
+
kernels : mapping of str to str, default {}
|
|
75
|
+
Device kernel source per backend name.
|
|
76
|
+
|
|
77
|
+
Raises
|
|
78
|
+
------
|
|
79
|
+
TypeError
|
|
80
|
+
If ``combine`` or ``finalize`` is not callable, ``empty`` is not a real number, or
|
|
81
|
+
``kernels`` is not a mapping of str to str.
|
|
82
|
+
|
|
83
|
+
Notes
|
|
84
|
+
-----
|
|
85
|
+
Rules K2, K4, K5 and S9a of the libraries specification. Pickles through its constructor,
|
|
86
|
+
so that a module keeping it can be sent to a worker process.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
combine: Callable[[np.ndarray, np.ndarray], np.ndarray]
|
|
90
|
+
empty: float
|
|
91
|
+
finalize: Callable[[np.ndarray, np.ndarray], np.ndarray] | None = None
|
|
92
|
+
kernels: Mapping[str, str] = field(default_factory=dict)
|
|
93
|
+
|
|
94
|
+
def __post_init__(self) -> None:
|
|
95
|
+
if not callable(self.combine):
|
|
96
|
+
raise TypeError(
|
|
97
|
+
f"Aggregation.combine: expected a callable, got {type(self.combine).__name__}"
|
|
98
|
+
)
|
|
99
|
+
if self.finalize is not None and not callable(self.finalize):
|
|
100
|
+
raise TypeError(
|
|
101
|
+
"Aggregation.finalize: expected a callable or None, got "
|
|
102
|
+
f"{type(self.finalize).__name__}"
|
|
103
|
+
)
|
|
104
|
+
if isinstance(self.empty, bool) or not isinstance(self.empty, (int, float, np.number)):
|
|
105
|
+
raise TypeError(
|
|
106
|
+
f"Aggregation.empty: expected a real number, got {type(self.empty).__name__}"
|
|
107
|
+
)
|
|
108
|
+
object.__setattr__(self, "empty", float(self.empty))
|
|
109
|
+
object.__setattr__(self, "kernels", _kernels(self.kernels, "Aggregation"))
|
|
110
|
+
|
|
111
|
+
def __reduce__(self) -> tuple[Any, ...]:
|
|
112
|
+
return (Aggregation, (self.combine, self.empty, self.finalize, dict(self.kernels)))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# Catalogue functions: built from numpy ufuncs evaluated in the array's own dtype, so that
|
|
116
|
+
# each element goes through one code path whatever its position (K5).
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _identity(x: np.ndarray) -> np.ndarray:
|
|
120
|
+
"""x (a new array, so that callers never alias their input)."""
|
|
121
|
+
return np.positive(x)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _tanh(x: np.ndarray) -> np.ndarray:
|
|
125
|
+
"""tanh(x)."""
|
|
126
|
+
return np.tanh(x)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _relu(x: np.ndarray) -> np.ndarray:
|
|
130
|
+
"""max(x, 0)."""
|
|
131
|
+
return np.maximum(x, x.dtype.type(0))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _sigmoid(x: np.ndarray) -> np.ndarray:
|
|
135
|
+
"""1 / (1 + exp(-x))."""
|
|
136
|
+
one = x.dtype.type(1)
|
|
137
|
+
return one / (one + np.exp(-x))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _neat_sigmoid(x: np.ndarray) -> np.ndarray:
|
|
141
|
+
"""1 / (1 + exp(-4.9 x)), the steepened sigmoid of the original NEAT paper."""
|
|
142
|
+
one = x.dtype.type(1)
|
|
143
|
+
return one / (one + np.exp(x.dtype.type(-4.9) * x))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _sin(x: np.ndarray) -> np.ndarray:
|
|
147
|
+
"""sin(x)."""
|
|
148
|
+
return np.sin(x)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _gauss(x: np.ndarray) -> np.ndarray:
|
|
152
|
+
"""exp(-x²)."""
|
|
153
|
+
return np.exp(-(x * x))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _abs(x: np.ndarray) -> np.ndarray:
|
|
157
|
+
"""|x|."""
|
|
158
|
+
return np.abs(x)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _step(x: np.ndarray) -> np.ndarray:
|
|
162
|
+
"""1 if x > 0 else 0."""
|
|
163
|
+
return (x > 0).astype(x.dtype)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _mean(acc: np.ndarray, count: np.ndarray) -> np.ndarray:
|
|
167
|
+
"""Sum divided by the term count (0 where there is no term)."""
|
|
168
|
+
count = np.asarray(count).astype(acc.dtype)
|
|
169
|
+
return np.divide(acc, count, out=np.zeros_like(acc), where=count > 0)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
ACTIVATIONS: Final[Mapping[str, Activation]] = MappingProxyType(
|
|
173
|
+
{
|
|
174
|
+
"id": Activation(_identity),
|
|
175
|
+
"tanh": Activation(_tanh),
|
|
176
|
+
"relu": Activation(_relu),
|
|
177
|
+
"sigmoid": Activation(_sigmoid),
|
|
178
|
+
"neat_sigmoid": Activation(_neat_sigmoid),
|
|
179
|
+
"sin": Activation(_sin),
|
|
180
|
+
"gauss": Activation(_gauss),
|
|
181
|
+
"abs": Activation(_abs),
|
|
182
|
+
"step": Activation(_step),
|
|
183
|
+
}
|
|
184
|
+
)
|
|
185
|
+
"""Read-only catalogue of activation functions, dated by ``VERSION`` (rule K3)."""
|
|
186
|
+
|
|
187
|
+
AGGREGATIONS: Final[Mapping[str, Aggregation]] = MappingProxyType(
|
|
188
|
+
{
|
|
189
|
+
"sum": Aggregation(np.add, 0.0),
|
|
190
|
+
"product": Aggregation(np.multiply, 1.0),
|
|
191
|
+
"max": Aggregation(np.maximum, 0.0),
|
|
192
|
+
"min": Aggregation(np.minimum, 0.0),
|
|
193
|
+
"mean": Aggregation(np.add, 0.0, finalize=_mean),
|
|
194
|
+
}
|
|
195
|
+
)
|
|
196
|
+
"""Read-only catalogue of aggregations, dated by ``VERSION`` (rule K4)."""
|
|
197
|
+
|
|
198
|
+
_KINDS: Final[Mapping[str, tuple[Mapping[str, object], type, str]]] = MappingProxyType(
|
|
199
|
+
{
|
|
200
|
+
"activation": (ACTIVATIONS, Activation, "activations"),
|
|
201
|
+
"aggregation": (AGGREGATIONS, Aggregation, "aggregations"),
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def caller_definitions[T](
|
|
207
|
+
given: Mapping[str, T] | None, kind: str
|
|
208
|
+
) -> Mapping[str, T]:
|
|
209
|
+
"""Check the definitions a caller supplies for ``kind`` and return them (R2, K2, K6)."""
|
|
210
|
+
catalogue, cls, keyword = _KINDS[kind]
|
|
211
|
+
if given is None:
|
|
212
|
+
return MappingProxyType({})
|
|
213
|
+
if not isinstance(given, Mapping):
|
|
214
|
+
raise TypeError(f"{keyword}=: expected a mapping of str to {cls.__name__}, "
|
|
215
|
+
f"got {type(given).__name__}")
|
|
216
|
+
for name, definition in given.items():
|
|
217
|
+
if not isinstance(name, str):
|
|
218
|
+
raise TypeError(f"{keyword}=: names are str, got {type(name).__name__}")
|
|
219
|
+
if not isinstance(definition, cls):
|
|
220
|
+
raise TypeError(
|
|
221
|
+
f"{keyword}['{name}']: expected an {cls.__name__}, got "
|
|
222
|
+
f"{type(definition).__name__}"
|
|
223
|
+
)
|
|
224
|
+
if name in catalogue:
|
|
225
|
+
raise ValueError(
|
|
226
|
+
f"'{name}' is a catalogue name of the network library (version {VERSION}); "
|
|
227
|
+
"give your variant another name"
|
|
228
|
+
)
|
|
229
|
+
return MappingProxyType(dict(given))
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def missing_names_message(kind: str, missing: Mapping[str, list[str]]) -> str:
|
|
233
|
+
"""Build the K6 message for names of ``kind`` found nowhere.
|
|
234
|
+
|
|
235
|
+
``missing`` maps each unknown name to the places it was read from, in order of appearance.
|
|
236
|
+
"""
|
|
237
|
+
catalogue, cls, keyword = _KINDS[kind]
|
|
238
|
+
names = list(missing)
|
|
239
|
+
places: list[str] = []
|
|
240
|
+
for where in missing.values():
|
|
241
|
+
places.extend(w for w in where if w not in places)
|
|
242
|
+
quoted = ", ".join(f"'{n}'" for n in names)
|
|
243
|
+
supply = ", ".join(f"'{n}': {cls.__name__}(...)" for n in names)
|
|
244
|
+
noun = kind if len(names) == 1 else f"{kind}s"
|
|
245
|
+
pronoun = "it" if len(names) == 1 else "them"
|
|
246
|
+
return (
|
|
247
|
+
f"network library (version {VERSION}): unknown {noun} {quoted} in "
|
|
248
|
+
f"{' and '.join(places)}; the catalogue offers {list(catalogue)}; "
|
|
249
|
+
f"pass {keyword}={{{supply}}} to supply {pronoun}"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def lookup[T](name: str, kind: str, given: Mapping[str, T]) -> T | None:
|
|
254
|
+
"""Resolve ``name``: the catalogue first, then the caller's definitions (K6)."""
|
|
255
|
+
catalogue = _KINDS[kind][0]
|
|
256
|
+
if name in catalogue:
|
|
257
|
+
return catalogue[name] # type: ignore[return-value]
|
|
258
|
+
return given.get(name)
|