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.
@@ -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
+
@@ -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
+
@@ -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
+ ]