radphen-iadpython 0.7.0__py3-none-any.whl
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.
- iadpython/__init__.py +59 -0
- iadpython/ad.py +1377 -0
- iadpython/agrid.py +306 -0
- iadpython/cache.py +133 -0
- iadpython/combine.py +620 -0
- iadpython/constants.py +3 -0
- iadpython/data/M38597.csv +759 -0
- iadpython/double.py +376 -0
- iadpython/fresnel.py +642 -0
- iadpython/grid.py +125 -0
- iadpython/iad.py +615 -0
- iadpython/iadc.py +1003 -0
- iadpython/iadcommand.py +712 -0
- iadpython/layer.py +241 -0
- iadpython/nist.py +114 -0
- iadpython/port.py +242 -0
- iadpython/quadrature.py +172 -0
- iadpython/redistribution.py +318 -0
- iadpython/rxt.py +201 -0
- iadpython/sphere.py +582 -0
- iadpython/start.py +377 -0
- iadpython/txt.py +147 -0
- radphen_iadpython-0.7.0.dist-info/METADATA +264 -0
- radphen_iadpython-0.7.0.dist-info/RECORD +28 -0
- radphen_iadpython-0.7.0.dist-info/WHEEL +5 -0
- radphen_iadpython-0.7.0.dist-info/entry_points.txt +2 -0
- radphen_iadpython-0.7.0.dist-info/licenses/LICENSE.txt +18 -0
- radphen_iadpython-0.7.0.dist-info/top_level.txt +1 -0
iadpython/agrid.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""Adaptive grid helper for inverse adding-doubling calculations.
|
|
2
|
+
|
|
3
|
+
`AGrid` builds an adaptive 2D grid for the two-parameter search modes:
|
|
4
|
+
`find_ab`, `find_ag`, and `find_bg`. Corner values are recursively sampled until
|
|
5
|
+
the local variation in `(ur1 + ut1)` falls below a tolerance, which typically
|
|
6
|
+
requires fewer RT evaluations than a dense uniform grid.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import matplotlib.pyplot as plt
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
from .cache import Cache
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AGrid:
|
|
18
|
+
"""Adaptive cache-backed grid for 2-parameter inversion warm starts."""
|
|
19
|
+
|
|
20
|
+
# for each mode, which axis is held fixed (and which two vary)
|
|
21
|
+
_modes = {
|
|
22
|
+
"find_ab": ("g", ("a", "b")),
|
|
23
|
+
"find_ag": ("b", ("a", "g")),
|
|
24
|
+
"find_bg": ("a", ("b", "g")),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# overall ranges for each axis
|
|
28
|
+
_ranges = {
|
|
29
|
+
"a": (0.0, 1.0),
|
|
30
|
+
"b": (0.0, 10.0),
|
|
31
|
+
"g": (-0.99, 0.99),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
exp=None,
|
|
37
|
+
search: str | None = None,
|
|
38
|
+
tol: float = 0.03,
|
|
39
|
+
max_depth: int = 6,
|
|
40
|
+
min_depth: int = 2,
|
|
41
|
+
default: float | None = None,
|
|
42
|
+
):
|
|
43
|
+
"""Initialize adaptive grid.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
exp: Optional experiment. If provided with `search`, the grid is built.
|
|
47
|
+
search: Search mode (`find_ab`, `find_ag`, or `find_bg`).
|
|
48
|
+
tol: Subdivision threshold for center interpolation error of `(ur1 + ut1)`.
|
|
49
|
+
max_depth: Maximum recursion depth for quadtree subdivision.
|
|
50
|
+
min_depth: Minimum enforced subdivision depth.
|
|
51
|
+
default: Fixed-axis value for the selected search mode.
|
|
52
|
+
"""
|
|
53
|
+
self.cache = Cache()
|
|
54
|
+
self.exp = exp
|
|
55
|
+
self.search = search
|
|
56
|
+
self.default = default
|
|
57
|
+
self.tol = tol
|
|
58
|
+
self.max_depth = max_depth
|
|
59
|
+
self.min_depth = min_depth
|
|
60
|
+
self.fixed_axis = None
|
|
61
|
+
self.vary_axes = None
|
|
62
|
+
|
|
63
|
+
if search is not None:
|
|
64
|
+
self._set_mode(search)
|
|
65
|
+
|
|
66
|
+
if exp is not None and search is not None:
|
|
67
|
+
self.calc(exp, default=default, search=search)
|
|
68
|
+
|
|
69
|
+
def _set_mode(self, search: str) -> None:
|
|
70
|
+
"""Set fixed and varying axes based on search mode."""
|
|
71
|
+
try:
|
|
72
|
+
self.fixed_axis, self.vary_axes = self._modes[search]
|
|
73
|
+
except KeyError as exc:
|
|
74
|
+
raise ValueError(f"Unknown search mode: {search!r}") from exc
|
|
75
|
+
self.search = search
|
|
76
|
+
|
|
77
|
+
def _default_for_search(self) -> float:
|
|
78
|
+
"""Return fixed-axis value for current search mode."""
|
|
79
|
+
if self.exp is None:
|
|
80
|
+
raise ValueError("AGrid.calc requires an experiment")
|
|
81
|
+
if self.search == "find_ab":
|
|
82
|
+
return self.exp.sample.g
|
|
83
|
+
if self.search == "find_ag":
|
|
84
|
+
return self.exp.sample.b
|
|
85
|
+
if self.search == "find_bg":
|
|
86
|
+
return self.exp.sample.a
|
|
87
|
+
raise ValueError(f"Unsupported search mode: {self.search!r}")
|
|
88
|
+
|
|
89
|
+
def calc(self, exp, default: float | None = None, search: str | None = None):
|
|
90
|
+
"""Build (or rebuild) adaptive grid for an experiment.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
exp: Experiment instance used to evaluate RT.
|
|
94
|
+
default: Fixed-axis value for the selected search mode.
|
|
95
|
+
search: Optional search mode override.
|
|
96
|
+
"""
|
|
97
|
+
self.exp = exp
|
|
98
|
+
if search is None:
|
|
99
|
+
search = self.search or exp.search
|
|
100
|
+
self._set_mode(search)
|
|
101
|
+
self.default = self._default_for_search() if default is None else default
|
|
102
|
+
self.cache = Cache()
|
|
103
|
+
self.build()
|
|
104
|
+
|
|
105
|
+
def __str__(self):
|
|
106
|
+
"""Return basic details as a string for printing."""
|
|
107
|
+
s = "---------------- Grid ---------------\n"
|
|
108
|
+
if self.search is None:
|
|
109
|
+
s += "search = None\n"
|
|
110
|
+
else:
|
|
111
|
+
s += "search = %s\n" % self.search
|
|
112
|
+
|
|
113
|
+
if self.default is None:
|
|
114
|
+
s += "default = None\n"
|
|
115
|
+
else:
|
|
116
|
+
s += "default = %.5f\n" % self.default
|
|
117
|
+
|
|
118
|
+
s += "entries = %d" % len(self.cache)
|
|
119
|
+
return s
|
|
120
|
+
|
|
121
|
+
def eval_f(self, a: float, b: float, g: float) -> None:
|
|
122
|
+
"""Memoised evaluation that fills the cache if missing."""
|
|
123
|
+
if self.cache.get(a, b, g) is None:
|
|
124
|
+
self.exp.sample.a = a
|
|
125
|
+
self.exp.sample.b = b
|
|
126
|
+
self.exp.sample.g = g
|
|
127
|
+
ur1, ut1, uru, utu = self.exp.sample.rt()
|
|
128
|
+
self.cache.put(a, b, g, ur1, ut1, uru, utu)
|
|
129
|
+
|
|
130
|
+
def _sum_val(self, a: float, b: float, g: float) -> float:
|
|
131
|
+
"""Ensure cached, then return ur1+ut1."""
|
|
132
|
+
self.eval_f(a, b, g)
|
|
133
|
+
ur1, ut1, _, _ = self.cache.get(a, b, g)
|
|
134
|
+
return ur1 + ut1
|
|
135
|
+
|
|
136
|
+
def _subdivide(self, x0, x1, y0, y1, depth, collect):
|
|
137
|
+
"""Recursively subdivide the rectangle [x0,x1]×[y0,y1]."""
|
|
138
|
+
xm = 0.5 * (x0 + x1)
|
|
139
|
+
ym = 0.5 * (y0 + y1)
|
|
140
|
+
|
|
141
|
+
# corners
|
|
142
|
+
v00 = self._sum_val(*self._make_args(x0, y0))
|
|
143
|
+
v10 = self._sum_val(*self._make_args(x1, y0))
|
|
144
|
+
v01 = self._sum_val(*self._make_args(x0, y1))
|
|
145
|
+
v11 = self._sum_val(*self._make_args(x1, y1))
|
|
146
|
+
vcc = self._sum_val(*self._make_args(xm, ym))
|
|
147
|
+
|
|
148
|
+
interp = 0.25 * (v00 + v10 + v01 + v11)
|
|
149
|
+
err = np.abs(vcc - interp)
|
|
150
|
+
need_split = (depth < self.min_depth) or (err > self.tol and depth < self.max_depth)
|
|
151
|
+
|
|
152
|
+
if need_split:
|
|
153
|
+
# recurse into four sub-quads
|
|
154
|
+
for nx0, nx1, ny0, ny1 in [
|
|
155
|
+
(x0, xm, y0, ym),
|
|
156
|
+
(xm, x1, y0, ym),
|
|
157
|
+
(x0, xm, ym, y1),
|
|
158
|
+
(xm, x1, ym, y1),
|
|
159
|
+
]:
|
|
160
|
+
self._subdivide(nx0, nx1, ny0, ny1, depth + 1, collect)
|
|
161
|
+
else:
|
|
162
|
+
# accept corners and centre for the leaf cell
|
|
163
|
+
collect.update(
|
|
164
|
+
{
|
|
165
|
+
(x0, y0),
|
|
166
|
+
(x1, y0),
|
|
167
|
+
(x0, y1),
|
|
168
|
+
(x1, y1),
|
|
169
|
+
(xm, ym),
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def _make_args(self, u, v):
|
|
174
|
+
"""Build (a,b,g) from the 2D coords (u,v) and the fixed axis."""
|
|
175
|
+
if self.fixed_axis == "g":
|
|
176
|
+
return (u, v, self.default)
|
|
177
|
+
if self.fixed_axis == "b":
|
|
178
|
+
return (u, self.default, v)
|
|
179
|
+
if self.fixed_axis == "a":
|
|
180
|
+
return (self.default, u, v)
|
|
181
|
+
raise ValueError(f"Bad fixed axis: {self.fixed_axis!r}")
|
|
182
|
+
|
|
183
|
+
def build(self) -> None:
|
|
184
|
+
"""
|
|
185
|
+
Build an adaptive quadtree.
|
|
186
|
+
|
|
187
|
+
The quadtree is created over the two free axes determined by `search`,
|
|
188
|
+
subdividing until center interpolation error in `(ur1 + ut1)` is below
|
|
189
|
+
`tol` (after `min_depth`) or `max_depth` is reached.
|
|
190
|
+
"""
|
|
191
|
+
if self.search is None or self.fixed_axis is None:
|
|
192
|
+
raise ValueError("Search mode must be set before build()")
|
|
193
|
+
if self.default is None:
|
|
194
|
+
raise ValueError("Fixed-axis default must be set before build()")
|
|
195
|
+
if self.exp is None:
|
|
196
|
+
raise ValueError("Experiment must be set before build()")
|
|
197
|
+
|
|
198
|
+
axis0, axis1 = self.vary_axes
|
|
199
|
+
|
|
200
|
+
# grab the ranges for the two free axes
|
|
201
|
+
(u0, u1), (v0, v1) = self._ranges[axis0], self._ranges[axis1]
|
|
202
|
+
|
|
203
|
+
# collect the (u,v) corner points that meet the tolerance
|
|
204
|
+
uv_points: set[tuple[float, float]] = set()
|
|
205
|
+
self._subdivide(u0, u1, v0, v1, depth=0, collect=uv_points)
|
|
206
|
+
|
|
207
|
+
for u, v in uv_points:
|
|
208
|
+
a, b, g = self._make_args(u, v)
|
|
209
|
+
self.eval_f(a, b, g)
|
|
210
|
+
|
|
211
|
+
def min_abg(self, mr, mt):
|
|
212
|
+
"""Find closest a, b, g closest to mr and mt."""
|
|
213
|
+
minimum = float("inf")
|
|
214
|
+
a_min = 0
|
|
215
|
+
b_min = 0
|
|
216
|
+
g_min = 0
|
|
217
|
+
|
|
218
|
+
for a, b, g, ur1, ut1, _uru, _utu in self.cache:
|
|
219
|
+
delta = np.abs(mr - ur1) + np.abs(mt - ut1)
|
|
220
|
+
if delta < minimum:
|
|
221
|
+
minimum = delta
|
|
222
|
+
a_min, b_min, g_min = a, b, g
|
|
223
|
+
|
|
224
|
+
if minimum == float("inf"):
|
|
225
|
+
raise RuntimeError("AGrid cache is empty. Call calc() first.")
|
|
226
|
+
return a_min, b_min, g_min
|
|
227
|
+
|
|
228
|
+
def is_stale(self, default_value, search: str | None = None):
|
|
229
|
+
"""Decide if current grid is still useful."""
|
|
230
|
+
if self.default is None:
|
|
231
|
+
return True
|
|
232
|
+
if search is not None and self.search != search:
|
|
233
|
+
return True
|
|
234
|
+
if self.default != default_value:
|
|
235
|
+
return True
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
def square_grid(self, N=21):
|
|
239
|
+
"""Return a square grid."""
|
|
240
|
+
aa = np.linspace(self._ranges["a"][0], self._ranges["a"][1], N)
|
|
241
|
+
bb = np.linspace(self._ranges["b"][0], self._ranges["b"][1], N)
|
|
242
|
+
gg = np.linspace(self._ranges["g"][0], self._ranges["g"][1], N)
|
|
243
|
+
|
|
244
|
+
if self.search == "find_ab":
|
|
245
|
+
ggg = np.full((N, N), self.default)
|
|
246
|
+
aaa, bbb = np.meshgrid(aa, bb)
|
|
247
|
+
elif self.search == "find_bg":
|
|
248
|
+
aaa = np.full((N, N), self.default)
|
|
249
|
+
ggg, bbb = np.meshgrid(gg, bb)
|
|
250
|
+
elif self.search == "find_ag":
|
|
251
|
+
bbb = np.full((N, N), self.default)
|
|
252
|
+
aaa, ggg = np.meshgrid(aa, gg)
|
|
253
|
+
else:
|
|
254
|
+
raise ValueError(f"Unknown search mode: {self.search!r}")
|
|
255
|
+
|
|
256
|
+
ur1 = np.zeros((N, N))
|
|
257
|
+
ut1 = np.zeros((N, N))
|
|
258
|
+
for i in range(N):
|
|
259
|
+
for j in range(N):
|
|
260
|
+
self.exp.sample.a = aaa[i, j]
|
|
261
|
+
self.exp.sample.b = bbb[i, j]
|
|
262
|
+
self.exp.sample.g = ggg[i, j]
|
|
263
|
+
ur1[i, j], ut1[i, j], _, _ = self.exp.sample.rt()
|
|
264
|
+
|
|
265
|
+
return aaa, bbb, ggg, ur1, ut1
|
|
266
|
+
|
|
267
|
+
def plot(self):
|
|
268
|
+
"""Plot the grid."""
|
|
269
|
+
# turn the cache into a (N×7) array
|
|
270
|
+
data = np.array(list(self.cache))
|
|
271
|
+
if data.size == 0:
|
|
272
|
+
raise RuntimeError("Cache is empty – run calc() first")
|
|
273
|
+
|
|
274
|
+
a, b, g, _ur1, _ut1, _uru, _utu = data.T
|
|
275
|
+
|
|
276
|
+
A, B, G, R, T = self.square_grid()
|
|
277
|
+
Z = R + T
|
|
278
|
+
# for i in range(21):
|
|
279
|
+
# for j in range(21):
|
|
280
|
+
# print("%8.4f %8.4f %8.4f %8.4f" % (A[i,j], B[i,j], G[i,j], Z[i,j]))
|
|
281
|
+
|
|
282
|
+
if self.search == "find_ab":
|
|
283
|
+
plt.plot(b, a, "sb", markersize=1)
|
|
284
|
+
plt.ylabel("albedo [-]")
|
|
285
|
+
plt.xlabel("optical thickness [-]")
|
|
286
|
+
plt.pcolormesh(B, A, Z, cmap="gray")
|
|
287
|
+
|
|
288
|
+
if self.search == "find_bg":
|
|
289
|
+
plt.plot(b, g, "sb", markersize=1)
|
|
290
|
+
plt.ylabel("anisotropy [-]")
|
|
291
|
+
plt.xlabel("optical thickness [-]")
|
|
292
|
+
plt.pcolormesh(B, G, Z, cmap="gray")
|
|
293
|
+
|
|
294
|
+
if self.search == "find_ag":
|
|
295
|
+
plt.plot(a, g, "sb", markersize=1)
|
|
296
|
+
plt.xlabel("albedo [-]")
|
|
297
|
+
plt.ylabel("anisotropy [-]")
|
|
298
|
+
plt.pcolormesh(A, G, Z, cmap="gray")
|
|
299
|
+
|
|
300
|
+
def __len__(self):
|
|
301
|
+
"""Return number of sampled points in the adaptive cache."""
|
|
302
|
+
return len(self.cache)
|
|
303
|
+
|
|
304
|
+
def __iter__(self):
|
|
305
|
+
"""Iterate over cached adaptive grid entries."""
|
|
306
|
+
return iter(self.cache)
|
iadpython/cache.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Light-weight (a, b, g) → (ur1, ut1, uru, utu) cache.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
import struct
|
|
8
|
+
from typing import Dict, Iterator, Tuple, Optional
|
|
9
|
+
|
|
10
|
+
_FNV_OFFSET = 0xCBF29CE484222325
|
|
11
|
+
_FNV_PRIME = 0x100000001B3
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _bits(f: float) -> int:
|
|
15
|
+
"""Return the IEEE-754 bit pattern of *f* as an unsigned 64-bit int."""
|
|
16
|
+
return struct.unpack("<Q", struct.pack("<d", f))[0]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def fnv1a_hash(a: float, b: float, g: float) -> int:
|
|
20
|
+
"""64-bit FNV-1a hash of the 3 doubles (identical to the C code)."""
|
|
21
|
+
h = _FNV_OFFSET
|
|
22
|
+
for x in (a, b, g):
|
|
23
|
+
h ^= _bits(x)
|
|
24
|
+
h = (h * _FNV_PRIME) & 0xFFFFFFFFFFFFFFFF # stay in 64-bit range
|
|
25
|
+
return h
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True, slots=True)
|
|
29
|
+
class Key:
|
|
30
|
+
"""Cache key for the `(a, b, g)` optical parameter triple.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
a: Single-scattering albedo.
|
|
34
|
+
b: Optical thickness.
|
|
35
|
+
g: Scattering anisotropy.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
a: float
|
|
39
|
+
b: float
|
|
40
|
+
g: float
|
|
41
|
+
|
|
42
|
+
def __hash__(self) -> int: # Python calls this automatically
|
|
43
|
+
"""Return a stable hash compatible with the C cache implementation.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
A 64-bit FNV-1a hash for `(a, b, g)`.
|
|
47
|
+
"""
|
|
48
|
+
return fnv1a_hash(self.a, self.b, self.g)
|
|
49
|
+
|
|
50
|
+
def __iter__(self):
|
|
51
|
+
"""Iterate over key components in `(a, b, g)` order.
|
|
52
|
+
|
|
53
|
+
Yields:
|
|
54
|
+
The `a`, `b`, and `g` values.
|
|
55
|
+
"""
|
|
56
|
+
yield from (self.a, self.b, self.g)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Cache:
|
|
60
|
+
"""Dictionary-backed cache mapping `(a, b, g)` to reflectance/transmittance.
|
|
61
|
+
|
|
62
|
+
Cached values are stored as `(ur1, ut1, uru, utu)` tuples and keyed by
|
|
63
|
+
:class:`Key` so hash behavior matches the corresponding C implementation.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self):
|
|
67
|
+
"""Initialize an empty cache."""
|
|
68
|
+
self._data: Dict[Key, Tuple[float, float, float, float]] = {}
|
|
69
|
+
|
|
70
|
+
def put(
|
|
71
|
+
self,
|
|
72
|
+
a: float,
|
|
73
|
+
b: float,
|
|
74
|
+
g: float,
|
|
75
|
+
ur1: float,
|
|
76
|
+
ut1: float,
|
|
77
|
+
uru: float,
|
|
78
|
+
utu: float,
|
|
79
|
+
) -> None:
|
|
80
|
+
"""Insert or overwrite the cache entry for a parameter triple.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
a: Single-scattering albedo.
|
|
84
|
+
b: Optical thickness.
|
|
85
|
+
g: Scattering anisotropy.
|
|
86
|
+
ur1: Total reflection for collimated incidence.
|
|
87
|
+
ut1: Total transmission for collimated incidence.
|
|
88
|
+
uru: Total reflection for diffuse incidence.
|
|
89
|
+
utu: Total transmission for diffuse incidence.
|
|
90
|
+
"""
|
|
91
|
+
self._data[Key(a, b, g)] = (ur1, ut1, uru, utu)
|
|
92
|
+
|
|
93
|
+
def get(self, a: float, b: float, g: float) -> Optional[Tuple[float, float, float, float]]:
|
|
94
|
+
"""Fetch a cached result for a parameter triple.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
a: Single-scattering albedo.
|
|
98
|
+
b: Optical thickness.
|
|
99
|
+
g: Scattering anisotropy.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
The tuple `(ur1, ut1, uru, utu)` if present; otherwise `None`.
|
|
103
|
+
"""
|
|
104
|
+
return self._data.get(Key(a, b, g))
|
|
105
|
+
|
|
106
|
+
# helpers mirroring the C extras
|
|
107
|
+
def __len__(self):
|
|
108
|
+
"""Return the number of cache entries.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
The current cache size.
|
|
112
|
+
"""
|
|
113
|
+
return len(self._data) # cache_size()
|
|
114
|
+
|
|
115
|
+
def __iter__(
|
|
116
|
+
self,
|
|
117
|
+
) -> Iterator[Tuple[float, float, float, float, float, float, float]]:
|
|
118
|
+
"""Iterate over cache records.
|
|
119
|
+
|
|
120
|
+
Yields:
|
|
121
|
+
Tuples of `(a, b, g, ur1, ut1, uru, utu)` for each entry.
|
|
122
|
+
"""
|
|
123
|
+
for k, v in self._data.items():
|
|
124
|
+
yield (*k, *v)
|
|
125
|
+
|
|
126
|
+
# convenience for testing / demo
|
|
127
|
+
def __repr__(self) -> str:
|
|
128
|
+
"""Return a concise debug representation of the cache.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
A string containing the cache size.
|
|
132
|
+
"""
|
|
133
|
+
return f"Cache(n={len(self)})"
|