ramp-coreoperator 0.0.2__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.
- coreoperator/__init__.py +14 -0
- coreoperator/example.py +11 -0
- coreoperator/featherstate.py +89 -0
- coreoperator/history.py +228 -0
- coreoperator/mobilization/__init__.py +8 -0
- coreoperator/mobilization/apply.py +47 -0
- coreoperator/mobilization/cyclic_shuffle.py +47 -0
- coreoperator/mobilization/grid_action.py +201 -0
- coreoperator/mobilization/load.py +120 -0
- coreoperator/mobilization/remove.py +39 -0
- coreoperator/mobilization/scheme.py +48 -0
- coreoperator/mobilization/transform_inplace.py +48 -0
- coreoperator/operational_state.py +416 -0
- ramp_coreoperator-0.0.2.dist-info/METADATA +53 -0
- ramp_coreoperator-0.0.2.dist-info/RECORD +18 -0
- ramp_coreoperator-0.0.2.dist-info/WHEEL +5 -0
- ramp_coreoperator-0.0.2.dist-info/licenses/LICENSE +21 -0
- ramp_coreoperator-0.0.2.dist-info/top_level.txt +1 -0
coreoperator/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Package for defining operations on core states and for maintaining an
|
|
3
|
+
operational history for it.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .featherstate import FeatherState
|
|
7
|
+
from .history import History, OperationalPeriod, StateParams
|
|
8
|
+
from .mobilization import jsonable as mob_jsonable
|
|
9
|
+
from .operational_state import OperationalState
|
|
10
|
+
|
|
11
|
+
jsonable = mob_jsonable + [History, OperationalState, FeatherState, OperationalPeriod, StateParams]
|
|
12
|
+
|
|
13
|
+
__all__ = ["jsonable", "OperationalState", "FeatherState", "History", "StateParams"]
|
|
14
|
+
|
coreoperator/example.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from coremaker.example import example_core
|
|
2
|
+
|
|
3
|
+
from coreoperator.history import History, StateParams
|
|
4
|
+
from coreoperator.operational_state import OperationalState
|
|
5
|
+
|
|
6
|
+
blank_hist = History()
|
|
7
|
+
example_state = OperationalState(history=blank_hist,
|
|
8
|
+
params=StateParams(power=0),
|
|
9
|
+
tags={"blank"},
|
|
10
|
+
core=example_core,
|
|
11
|
+
)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import pickle
|
|
2
|
+
import zlib
|
|
3
|
+
from typing import Any, Literal, Type, TypeVar
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from typing import Self
|
|
7
|
+
except ImportError:
|
|
8
|
+
Self = TypeVar("Self")
|
|
9
|
+
|
|
10
|
+
from coremaker.core import Core
|
|
11
|
+
|
|
12
|
+
from .operational_state import OperationalState
|
|
13
|
+
|
|
14
|
+
Zlib_Compression = Literal[-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
|
15
|
+
DEFAULT_COMPRESSION = 2 # A sensible default, fast and still has an effect for us
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FeatherState(OperationalState):
|
|
19
|
+
"""An OperationalState that takes less space in memory and is much easier to
|
|
20
|
+
transmit over the wire.
|
|
21
|
+
|
|
22
|
+
This comes at the cost of slightly less comfortable ergonomics, because one
|
|
23
|
+
cannot directly edit `state.core` and has to explicitly reset the core with
|
|
24
|
+
a setter method to make changes stick.
|
|
25
|
+
To make the ergonomics similar, we made OperationalState similarly complex,
|
|
26
|
+
but deem the tradeoff to be worth the trouble.
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
ser_identifier = "FeatherState"
|
|
31
|
+
__zcore: bytes
|
|
32
|
+
|
|
33
|
+
def __init__(self, *, compression_level: Zlib_Compression = DEFAULT_COMPRESSION, **kw):
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
compression_level: Zlib_Compression
|
|
39
|
+
The compression level to use. See the `zlib` standard library documentation for details.
|
|
40
|
+
Defaults to a fast compression that still has a significant impact. Subject to change.
|
|
41
|
+
kw:
|
|
42
|
+
Keywords used to make an OperationalState. See that class for details.
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
self.compression_level = compression_level
|
|
46
|
+
super().__init__(**kw)
|
|
47
|
+
|
|
48
|
+
def serialize(self) -> tuple[str, dict[str, Any]]:
|
|
49
|
+
parent_serialized_data = super().serialize()[1]
|
|
50
|
+
parent_serialized_data["compression"] = self.compression_level
|
|
51
|
+
return self.ser_identifier, parent_serialized_data
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def deserialize(cls: Type[Self], d: dict[str, Any], *args, **kwargs) -> Self:
|
|
55
|
+
compression = d.pop("compression")
|
|
56
|
+
return cls.from_state(super().deserialize(d=d, *args, **kwargs), compression_level=compression)
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_state(cls: Type[Self],
|
|
60
|
+
state: OperationalState,
|
|
61
|
+
compression_level: Zlib_Compression = DEFAULT_COMPRESSION,
|
|
62
|
+
) -> Self:
|
|
63
|
+
"""Creates a FeatherState from an OperationalState
|
|
64
|
+
|
|
65
|
+
Parameters
|
|
66
|
+
----------
|
|
67
|
+
state: OperationalState
|
|
68
|
+
The OperationalState to compress
|
|
69
|
+
compression_level: Zlib_Compression
|
|
70
|
+
The compression level to use. See the `zlib` standard library documentation for details.
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
if isinstance(state, FeatherState):
|
|
74
|
+
state.compression_level = compression_level
|
|
75
|
+
return state
|
|
76
|
+
return cls(**state.as_dict(), compression_level=compression_level)
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def core(self) -> Core:
|
|
80
|
+
return pickle.loads(zlib.decompress(self.__zcore))
|
|
81
|
+
|
|
82
|
+
@core.setter
|
|
83
|
+
def core(self, core):
|
|
84
|
+
self.__zcore = zlib.compress(pickle.dumps(core), self.compression_level)
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def _core(self) -> Core:
|
|
88
|
+
return self.core
|
|
89
|
+
|
coreoperator/history.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
from dataclasses import dataclass, field, replace
|
|
2
|
+
from datetime import timedelta
|
|
3
|
+
from itertools import takewhile
|
|
4
|
+
from typing import Any, ClassVar, Generator, Hashable, Type, TypeVar
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from typing import Self
|
|
8
|
+
except ImportError:
|
|
9
|
+
Self = TypeVar("Self")
|
|
10
|
+
|
|
11
|
+
from ramp_core.serializable import Serializable, deserialize_default
|
|
12
|
+
from scipy.constants import day
|
|
13
|
+
|
|
14
|
+
from coreoperator.mobilization import Scheme
|
|
15
|
+
|
|
16
|
+
MW = MWD = float
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class StateParams(Serializable):
|
|
20
|
+
"""A container of operational parameters. Some are required, many are allowed.
|
|
21
|
+
|
|
22
|
+
Please use hashable values
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
ser_identifier = "StateParams"
|
|
27
|
+
|
|
28
|
+
def __init__(self, power: MW, **kwargs: dict[str, Hashable]):
|
|
29
|
+
self.power = power
|
|
30
|
+
self._attrs = kwargs
|
|
31
|
+
|
|
32
|
+
def serialize(self) -> tuple[str, dict[str, Any]]:
|
|
33
|
+
return self.ser_identifier, self.todict()
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def deserialize(cls: Type[Self], d: dict[str, Any], *_, **__) -> Self:
|
|
37
|
+
return cls(**d)
|
|
38
|
+
|
|
39
|
+
def copy(self: Self, **kwargs: dict[str, Hashable]) -> Self:
|
|
40
|
+
"""Creates a copy of these parameters, but allows changes.
|
|
41
|
+
Kind of like a dataclass' replace, but with arbitrary keywords.
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
kw = self.todict() | kwargs
|
|
45
|
+
return type(self)(**kw)
|
|
46
|
+
|
|
47
|
+
def __getitem__(self, item: str):
|
|
48
|
+
return self.power if item == "power" else self._attrs[item]
|
|
49
|
+
|
|
50
|
+
def __delitem__(self, key: str):
|
|
51
|
+
if key == "power":
|
|
52
|
+
raise KeyError("Not allowed to delete the power parameter from StateParams")
|
|
53
|
+
del self._attrs[key]
|
|
54
|
+
|
|
55
|
+
def __setitem__(self, key: str, value: Hashable):
|
|
56
|
+
if key == "power":
|
|
57
|
+
self.power = value
|
|
58
|
+
else:
|
|
59
|
+
self._attrs[key] = value
|
|
60
|
+
|
|
61
|
+
def __iter__(self) -> Generator[tuple[str, Hashable], None, None]:
|
|
62
|
+
yield "power", self.power
|
|
63
|
+
yield from self._attrs.items()
|
|
64
|
+
|
|
65
|
+
def __hash__(self):
|
|
66
|
+
return hash(frozenset((key, value) for key, value in self))
|
|
67
|
+
|
|
68
|
+
def __eq__(self, other):
|
|
69
|
+
if not isinstance(other, type(self)):
|
|
70
|
+
return NotImplemented
|
|
71
|
+
return self.todict() == other.todict()
|
|
72
|
+
|
|
73
|
+
def __repr__(self): return str(self.todict())
|
|
74
|
+
|
|
75
|
+
def todict(self) -> dict[str, Hashable]:
|
|
76
|
+
return dict(iter(self))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class OperationalPeriod(Serializable):
|
|
81
|
+
"""A segment of time with given operational parameters
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
params: StateParams
|
|
86
|
+
The operational parameters during this period.
|
|
87
|
+
time: timedelta
|
|
88
|
+
Period length
|
|
89
|
+
|
|
90
|
+
"""
|
|
91
|
+
params: StateParams
|
|
92
|
+
time: timedelta
|
|
93
|
+
|
|
94
|
+
ser_identifier: ClassVar[str] = "OpPeriod"
|
|
95
|
+
|
|
96
|
+
def serialize(self) -> tuple[str, dict[str, Any]]:
|
|
97
|
+
return self.ser_identifier, {"params": self.params.serialize(), "time": self.time.total_seconds()}
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def deserialize(cls: Type[Self], d: dict[str, Any], *, supported: dict[str, Type[Serializable]]) -> Self:
|
|
101
|
+
params: StateParams = deserialize_default(d["params"], supported=supported, default=StateParams)
|
|
102
|
+
time = timedelta(seconds=d["time"])
|
|
103
|
+
return cls(params, time)
|
|
104
|
+
|
|
105
|
+
def copy(self: Self,
|
|
106
|
+
params: StateParams | None = None,
|
|
107
|
+
time: timedelta | None = None
|
|
108
|
+
) -> Self:
|
|
109
|
+
"""Creates a copy of this period with some changes.
|
|
110
|
+
|
|
111
|
+
Parameters
|
|
112
|
+
----------
|
|
113
|
+
params: StateParams
|
|
114
|
+
Parameters to change from the original
|
|
115
|
+
time: timedelta
|
|
116
|
+
Period of time, if period changes.
|
|
117
|
+
"""
|
|
118
|
+
pars = self.params.copy(**params.todict()) if params is not None else self.params
|
|
119
|
+
time = time if time is not None else self.time
|
|
120
|
+
return replace(self, params=pars, time=time)
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def burnup(self) -> MWD:
|
|
124
|
+
return self.params.power * self.time.total_seconds() / day
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass(frozen=True)
|
|
128
|
+
class History(Serializable):
|
|
129
|
+
"""Historical information about the core.
|
|
130
|
+
|
|
131
|
+
Basically a sequentially growing list of steps at piecewise constant parameters.
|
|
132
|
+
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
steps: list[OperationalPeriod | Scheme] = field(default_factory=list)
|
|
136
|
+
|
|
137
|
+
ser_identifier: ClassVar[str] = "OpHistory"
|
|
138
|
+
|
|
139
|
+
def serialize(self) -> tuple[str, dict[str, Any]]:
|
|
140
|
+
return self.ser_identifier, {"steps": [step.serialize() for step in self.steps]}
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
def deserialize(cls: Type[Self], d: dict[str, Any], *, supported: dict[str, Type[Serializable]]) -> Self:
|
|
144
|
+
steps = [deserialize_default(step, supported=supported) for step in d["steps"]]
|
|
145
|
+
return cls(steps)
|
|
146
|
+
|
|
147
|
+
def new_cycle(self: Self, scheme: Scheme) -> Self:
|
|
148
|
+
"""Adds a new scheme to the history, marking the beginning of a new cycle.
|
|
149
|
+
|
|
150
|
+
Parameters
|
|
151
|
+
----------
|
|
152
|
+
scheme: Scheme
|
|
153
|
+
The scheme to perform on the core between cycles.
|
|
154
|
+
|
|
155
|
+
Returns
|
|
156
|
+
-------
|
|
157
|
+
History
|
|
158
|
+
The new History created by adding the scheme to this history.
|
|
159
|
+
Steps are joined if the last step was a scheme, so we combine them into one bigger scheme.
|
|
160
|
+
|
|
161
|
+
"""
|
|
162
|
+
cls = type(self)
|
|
163
|
+
if len(self) == 0:
|
|
164
|
+
return cls([scheme])
|
|
165
|
+
last = self.steps[-1]
|
|
166
|
+
if isinstance(last, Scheme):
|
|
167
|
+
return cls(self.steps[:-1] + [last @ scheme])
|
|
168
|
+
return cls(self.steps + [scheme])
|
|
169
|
+
|
|
170
|
+
def __len__(self) -> int: return len(self.steps)
|
|
171
|
+
|
|
172
|
+
def timestep(self: Self, params: StateParams, time: timedelta) -> Self:
|
|
173
|
+
"""Adds a time step to the history, where the core worked with some parameters for some time.
|
|
174
|
+
|
|
175
|
+
Parameters
|
|
176
|
+
----------
|
|
177
|
+
params: StateParams
|
|
178
|
+
The parameters the core worked under during this step
|
|
179
|
+
time: timedelta
|
|
180
|
+
Period of time the core worked under these parameters.
|
|
181
|
+
|
|
182
|
+
Returns
|
|
183
|
+
-------
|
|
184
|
+
History
|
|
185
|
+
The new history with the added step. Steps are joined if the last step matched this one's parameters.
|
|
186
|
+
|
|
187
|
+
"""
|
|
188
|
+
cls = type(self)
|
|
189
|
+
if len(self) == 0:
|
|
190
|
+
return cls([OperationalPeriod(params, time)])
|
|
191
|
+
last = self.steps[-1]
|
|
192
|
+
if isinstance(last, OperationalPeriod) and last.params == params:
|
|
193
|
+
return cls(self.steps[:-1] + [last.copy(time=last.time + time)])
|
|
194
|
+
return cls(self.steps + [OperationalPeriod(params, time)])
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def current_params(self) -> StateParams | None:
|
|
198
|
+
"""Returns the last known parameters, or None if there are none.
|
|
199
|
+
|
|
200
|
+
"""
|
|
201
|
+
for step in self.steps[::-1]:
|
|
202
|
+
if isinstance(step, OperationalPeriod):
|
|
203
|
+
return step.params
|
|
204
|
+
else:
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def cycles(self) -> int:
|
|
209
|
+
"""The number of cycles in the history"""
|
|
210
|
+
return sum((1 for step in self.steps if isinstance(step, Scheme)))
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def cycle_burnup(self) -> MWD:
|
|
214
|
+
"""The amount of burnup since the start of this cycle"""
|
|
215
|
+
cycle = takewhile(lambda x: isinstance(x, OperationalPeriod), self.steps[::-1])
|
|
216
|
+
return sum((self.burnup for step in cycle))
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def cycle_time(self) -> timedelta:
|
|
220
|
+
cycle = takewhile(lambda x: isinstance(x, OperationalPeriod), self.steps[::-1])
|
|
221
|
+
return sum((step.time for step in cycle), timedelta(0))
|
|
222
|
+
|
|
223
|
+
def __repr__(self) -> str:
|
|
224
|
+
return f"Cycles: {self.cycles}, Burnup: {self.cycle_burnup:.3f} MWD"
|
|
225
|
+
|
|
226
|
+
def __hash__(self) -> int:
|
|
227
|
+
return hash(tuple(self.steps))
|
|
228
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from .cyclic_shuffle import CyclicShuffle
|
|
2
|
+
from .grid_action import GridAction as GridAction
|
|
3
|
+
from .load import LoadSite, LoadChain
|
|
4
|
+
from .remove import Remove
|
|
5
|
+
from .scheme import Scheme
|
|
6
|
+
from .transform_inplace import TransformInPlace
|
|
7
|
+
|
|
8
|
+
jsonable = [CyclicShuffle, LoadSite, LoadChain, Remove, Scheme, TransformInPlace]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from typing import Hashable, MutableMapping, Sequence
|
|
2
|
+
|
|
3
|
+
from coremaker.protocols.core import Site
|
|
4
|
+
from coremaker.protocols.element import Element
|
|
5
|
+
from coremaker.transform import Transform
|
|
6
|
+
|
|
7
|
+
Alias = Hashable
|
|
8
|
+
SCRAM = Element
|
|
9
|
+
|
|
10
|
+
MaybeRod = Element | None
|
|
11
|
+
SiteDict = MutableMapping[Site, Element]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _set_rods(coresites: SiteDict, new_sites: dict[Site, MaybeRod]):
|
|
15
|
+
for site, rod in new_sites.items():
|
|
16
|
+
if rod:
|
|
17
|
+
coresites[site] = rod
|
|
18
|
+
elif site in coresites:
|
|
19
|
+
del coresites[site]
|
|
20
|
+
elif site not in coresites:
|
|
21
|
+
raise ValueError(f"can't remove the contents of the unoccupied "
|
|
22
|
+
f"site:{site}")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_transformed_rods(sites: Sequence[tuple[Site, Transform]],
|
|
26
|
+
rods_at_sites: SiteDict) -> Sequence[Element]:
|
|
27
|
+
"""
|
|
28
|
+
Function that returns the transformed rods at the given sites under the
|
|
29
|
+
given transformation.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
sites: Sequence[Tuple[Site, Transform]]
|
|
34
|
+
Sequence of sites and the transforms at each site
|
|
35
|
+
rods_at_sites: SiteDict
|
|
36
|
+
Dict of sites and the rods at those sites.
|
|
37
|
+
|
|
38
|
+
Returns
|
|
39
|
+
-------
|
|
40
|
+
Sequence[Element]
|
|
41
|
+
Sequence of the rods at the given sites after the transforms.
|
|
42
|
+
"""
|
|
43
|
+
rods = [rods_at_sites[site] for (site, _) in sites]
|
|
44
|
+
for (_, transform), rod in zip(sites, rods):
|
|
45
|
+
rod.transform(None, transform)
|
|
46
|
+
return rods
|
|
47
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from operator import itemgetter
|
|
2
|
+
from typing import Sequence
|
|
3
|
+
|
|
4
|
+
from coremaker.protocols.core import Site
|
|
5
|
+
from coremaker.transform import identity
|
|
6
|
+
|
|
7
|
+
from coreoperator.mobilization.grid_action import (
|
|
8
|
+
DefinitePosition,
|
|
9
|
+
GridAction,
|
|
10
|
+
Position,
|
|
11
|
+
SiteDict,
|
|
12
|
+
_ensure_unique,
|
|
13
|
+
get_transformed_rods,
|
|
14
|
+
rotate_left,
|
|
15
|
+
rotate_right,
|
|
16
|
+
set_rods,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CyclicShuffle(GridAction):
|
|
21
|
+
"""represents a single connected right permutation of rods"""
|
|
22
|
+
|
|
23
|
+
ser_identifier = "CycShuffle"
|
|
24
|
+
|
|
25
|
+
def __init__(self, sites: Sequence[Position]):
|
|
26
|
+
_ensure_unique(sites)
|
|
27
|
+
self.sites: list[DefinitePosition] = [
|
|
28
|
+
(site, identity) if isinstance(site, Site) else site
|
|
29
|
+
for site in sites]
|
|
30
|
+
self._movement = frozenset(zip(rotate_left(self.sites), self.sites))
|
|
31
|
+
|
|
32
|
+
def apply(self, d: SiteDict) -> None:
|
|
33
|
+
sites = rotate_right(self.sites)
|
|
34
|
+
rods = get_transformed_rods(sites, d)
|
|
35
|
+
news = dict(zip(map(itemgetter(0), self.sites), rods))
|
|
36
|
+
set_rods(d, news)
|
|
37
|
+
|
|
38
|
+
def __eq__(self, other):
|
|
39
|
+
if not isinstance(other, type(self)):
|
|
40
|
+
return NotImplemented
|
|
41
|
+
return self._movement == other._movement
|
|
42
|
+
|
|
43
|
+
def __hash__(self):
|
|
44
|
+
return hash(self._movement)
|
|
45
|
+
|
|
46
|
+
def __repr__(self):
|
|
47
|
+
return 'Shuffle: ->' + '->'.join(map(repr, self.sites)) + '->'
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
from collections import Counter
|
|
2
|
+
from itertools import cycle, islice
|
|
3
|
+
from typing import Any, Hashable, Iterable, Mapping, Protocol, Sequence, Type, TypeVar
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from typing import Self
|
|
7
|
+
except ImportError:
|
|
8
|
+
Self = TypeVar("Self")
|
|
9
|
+
|
|
10
|
+
from coremaker.protocols.core import Site
|
|
11
|
+
from coremaker.protocols.element import Element
|
|
12
|
+
from coremaker.transform import Transform
|
|
13
|
+
from ramp_core.serializable import Serializable
|
|
14
|
+
|
|
15
|
+
DefinitePosition = tuple[Site, Transform]
|
|
16
|
+
Position = Site | DefinitePosition
|
|
17
|
+
T = TypeVar("T")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SiteDict(Protocol):
|
|
21
|
+
"""Partial requirements from a mutable mapping of Site -> Element"""
|
|
22
|
+
|
|
23
|
+
def __getitem__(self, item: Site) -> Element: ...
|
|
24
|
+
|
|
25
|
+
def __setitem__(self, key: Site, value: Element) -> None: ...
|
|
26
|
+
|
|
27
|
+
def __delitem__(self, key: Site) -> None: ...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class IllegalActionError(ValueError):
|
|
31
|
+
"""Error class for illegal grid actions"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _ensure_unique(positions: Sequence[Position]):
|
|
35
|
+
if not positions:
|
|
36
|
+
raise IllegalActionError("Cannot create an action with no sites")
|
|
37
|
+
sites = [pos if isinstance(pos, Site) else pos[0] for pos in positions]
|
|
38
|
+
counter = Counter(sites)
|
|
39
|
+
if set(counter.values()) != {1}:
|
|
40
|
+
repeats = {site for site, v in counter.items() if v > 1}
|
|
41
|
+
raise IllegalActionError(
|
|
42
|
+
f"Some sites appear more than once in an action: {repeats}"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class GridAction(Hashable, Serializable, Protocol):
|
|
47
|
+
"""
|
|
48
|
+
Protocol for an action preformed on sites in a Grid
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
sites: Sequence[Position]
|
|
52
|
+
|
|
53
|
+
def apply(self, d: SiteDict) -> None:
|
|
54
|
+
"""Applies the action to the mapping.
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
d: SiteDict
|
|
59
|
+
Mapping of sites to contents that will be edited
|
|
60
|
+
|
|
61
|
+
"""
|
|
62
|
+
...
|
|
63
|
+
|
|
64
|
+
def serialize(self) -> tuple[str, dict[str, Any]]:
|
|
65
|
+
return self.ser_identifier, {"sites": ser_sites(self.sites)}
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def deserialize(cls: Type[Self], d: dict[str, Any], *_, **__) -> Self:
|
|
69
|
+
return cls(deser_sites(d["sites"]))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def rotate_left(it: Iterable[T], n: int = 1) -> tuple[T, ...]:
|
|
73
|
+
"""Return a tuple sequence that is a left-rotated version of a finite iterable.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
it: Iterable
|
|
78
|
+
Iterable of finite size to rotate.
|
|
79
|
+
|
|
80
|
+
n: int
|
|
81
|
+
The number of left rotations to perform.
|
|
82
|
+
|
|
83
|
+
Returns
|
|
84
|
+
-------
|
|
85
|
+
Tuple of the same items in the iterable but with a different order.
|
|
86
|
+
|
|
87
|
+
"""
|
|
88
|
+
seq = tuple(it)
|
|
89
|
+
return tuple(islice(cycle(seq), n, n + len(seq)))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def rotate_right(it: Iterable[T], n: int = 1) -> tuple[T, ...]:
|
|
93
|
+
"""Return a tuple sequence that is the right-rotated version of a finite iterable.
|
|
94
|
+
|
|
95
|
+
Parameters
|
|
96
|
+
----------
|
|
97
|
+
it: Iterable
|
|
98
|
+
Iterable of finite size to rotate
|
|
99
|
+
n: int
|
|
100
|
+
The number of right rotations to perform.
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
Tuple of the same items in the iterable but with a different order.
|
|
105
|
+
|
|
106
|
+
"""
|
|
107
|
+
seq = tuple(it)
|
|
108
|
+
return rotate_left(it, n=(-n) % len(seq))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def set_rods(coresites: SiteDict, new_sites: Mapping[Site, Element | None]) -> None:
|
|
112
|
+
"""Update rods in the mapping according to the new mapping
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
coresites: SiteDict
|
|
117
|
+
Original mapping
|
|
118
|
+
new_sites: Mapping[Site, Element | None]
|
|
119
|
+
New information mapping.
|
|
120
|
+
Where a site points to an element, we set that in the new mapping.
|
|
121
|
+
Where it points to None, we eject the rod from that site.
|
|
122
|
+
|
|
123
|
+
Raises
|
|
124
|
+
------
|
|
125
|
+
IllegalActionError
|
|
126
|
+
Raises if a site in new_sites points to None but that site isn't occupied
|
|
127
|
+
in the original mapping.
|
|
128
|
+
|
|
129
|
+
"""
|
|
130
|
+
for site, rod in new_sites.items():
|
|
131
|
+
if rod:
|
|
132
|
+
coresites[site] = rod
|
|
133
|
+
elif site in coresites:
|
|
134
|
+
del coresites[site]
|
|
135
|
+
else:
|
|
136
|
+
raise IllegalActionError(
|
|
137
|
+
f"Can't remove the contents of the unoccupied site: {site}"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def get_transformed_rods(
|
|
142
|
+
sites: Sequence[tuple[Site, Transform]], rods_at_sites: SiteDict
|
|
143
|
+
) -> Sequence[Element]:
|
|
144
|
+
"""Function that returns the transformed rods at the given sites under the given transformation.
|
|
145
|
+
|
|
146
|
+
Parameters
|
|
147
|
+
----------
|
|
148
|
+
sites: Sequence[tuple[Site, Transform]]
|
|
149
|
+
Sequence of sites and the transforms at each site.
|
|
150
|
+
rods_at_sites: SiteDict
|
|
151
|
+
Mapping of sites and the rods therein.
|
|
152
|
+
|
|
153
|
+
Returns
|
|
154
|
+
-------
|
|
155
|
+
Sequence[Element]
|
|
156
|
+
Sequence of the rods at the given sites after the transforms.
|
|
157
|
+
|
|
158
|
+
"""
|
|
159
|
+
rods = [rods_at_sites[site] for site, _ in sites]
|
|
160
|
+
for (_, transform), rod in zip(sites, rods):
|
|
161
|
+
rod.transform(None, transform)
|
|
162
|
+
return rods
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def ser_sites(sites: Iterable[Position]) -> list:
|
|
166
|
+
"""Serialize sites as they are written in grid actions
|
|
167
|
+
|
|
168
|
+
Parameters
|
|
169
|
+
----------
|
|
170
|
+
sites: Iterable[Position]
|
|
171
|
+
Sites to serialize
|
|
172
|
+
|
|
173
|
+
Returns
|
|
174
|
+
-------
|
|
175
|
+
Serialized form of the sites
|
|
176
|
+
|
|
177
|
+
"""
|
|
178
|
+
return [
|
|
179
|
+
[ptup[0], ptup[1].serialize()] if isinstance(ptup, tuple) else ptup
|
|
180
|
+
for ptup in sites
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def deser_sites(slist: list[tuple[str, dict]]) -> list[Position]:
|
|
185
|
+
"""Deserialize sites from the common format made by ser_sites
|
|
186
|
+
|
|
187
|
+
Parameters
|
|
188
|
+
----------
|
|
189
|
+
slist: list[tuple[str, dict]]
|
|
190
|
+
Actually a list of 2-lists of this form.
|
|
191
|
+
|
|
192
|
+
Returns
|
|
193
|
+
-------
|
|
194
|
+
list[Position]
|
|
195
|
+
The list of positions we started with.
|
|
196
|
+
|
|
197
|
+
"""
|
|
198
|
+
return [
|
|
199
|
+
(ptup[0], Transform.deserialize(ptup[1])) if isinstance(ptup, list) else ptup
|
|
200
|
+
for ptup in slist
|
|
201
|
+
]
|