xbtm 0.1.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.
- xbtm/__init__.py +1 -0
- xbtm/main.py +219 -0
- xbtm/validation.py +277 -0
- xbtm-0.1.0.dist-info/METADATA +119 -0
- xbtm-0.1.0.dist-info/RECORD +7 -0
- xbtm-0.1.0.dist-info/WHEEL +4 -0
- xbtm-0.1.0.dist-info/licenses/LICENSE +21 -0
xbtm/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .main import Program, State, OperatorType, generate
|
xbtm/main.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Self
|
|
4
|
+
import re
|
|
5
|
+
import numpy as np
|
|
6
|
+
from .validation import val_natural, val_parse, val_post_init
|
|
7
|
+
|
|
8
|
+
def _uniquify(lst: list) -> tuple:
|
|
9
|
+
return tuple(dict.fromkeys(lst))
|
|
10
|
+
|
|
11
|
+
class OperatorType(Enum):
|
|
12
|
+
EXPAND = '>'
|
|
13
|
+
SPLIT = '='
|
|
14
|
+
SPLIT_TO = '|'
|
|
15
|
+
SPLIT_MUL = 'x'
|
|
16
|
+
END = '!'
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def text(self):
|
|
20
|
+
return self.value
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class State:
|
|
24
|
+
number: int
|
|
25
|
+
is_ref: bool = False
|
|
26
|
+
is_node: bool = True
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def text(self) -> str:
|
|
30
|
+
if self.is_ref:
|
|
31
|
+
return f'({self.number})'
|
|
32
|
+
return str(self.number)
|
|
33
|
+
|
|
34
|
+
def __post_init__(self):
|
|
35
|
+
val_natural(self.number)
|
|
36
|
+
if not (isinstance(self.is_ref, bool)): raise TypeError(f"{self.is_ref} should be bool.")
|
|
37
|
+
if not (isinstance(self.is_node, bool)): raise TypeError(f"{self.is_node} should be bool.")
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class Program:
|
|
41
|
+
base: int
|
|
42
|
+
target: int
|
|
43
|
+
tokens: tuple[State|OperatorType, ...] = ()
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def text(self) -> str:
|
|
47
|
+
return f'X{self.base}T{self.target}:' + ''.join([token.text for token in self.tokens])
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def nodes(self) -> tuple[int, ...]:
|
|
51
|
+
return _uniquify([
|
|
52
|
+
token.number
|
|
53
|
+
for token in self.tokens
|
|
54
|
+
if isinstance(token, State) and token.is_node
|
|
55
|
+
])
|
|
56
|
+
|
|
57
|
+
def _state_at(self, i: int) -> State:
|
|
58
|
+
token = self.tokens[i]
|
|
59
|
+
assert isinstance(token, State), f"Type Mismatch, {token} should be State."
|
|
60
|
+
return token
|
|
61
|
+
|
|
62
|
+
def to_matrix(self) -> tuple[np.ndarray, np.ndarray]:
|
|
63
|
+
index = self.nodes
|
|
64
|
+
inv_index = {value: idx for idx, value in enumerate(index)}
|
|
65
|
+
node_count = len(index)
|
|
66
|
+
|
|
67
|
+
A = np.zeros((node_count, node_count))
|
|
68
|
+
b = np.zeros((node_count,))
|
|
69
|
+
|
|
70
|
+
for i, token in enumerate(self.tokens):
|
|
71
|
+
match token:
|
|
72
|
+
case OperatorType.EXPAND:
|
|
73
|
+
A[inv_index[self._state_at(i-1).number], inv_index[self._state_at(i-1).number]] = 1
|
|
74
|
+
A[inv_index[self._state_at(i-1).number], inv_index[self._state_at(i+1).number]] = -1
|
|
75
|
+
b[inv_index[self._state_at(i-1).number] ] = 1
|
|
76
|
+
|
|
77
|
+
case OperatorType.SPLIT:
|
|
78
|
+
A[inv_index[self._state_at(i-1).number], inv_index[self._state_at(i-1).number]] = 1
|
|
79
|
+
|
|
80
|
+
if self.tokens[i+2] == OperatorType.SPLIT_MUL and self.tokens[i+5] != OperatorType.END:
|
|
81
|
+
A[inv_index[self._state_at(i-1).number], inv_index[self._state_at(i+5).number]] = -self._state_at(i+5).number / self._state_at(i-1).number
|
|
82
|
+
elif self.tokens[i+2] == OperatorType.SPLIT_TO:
|
|
83
|
+
A[inv_index[self._state_at(i-1).number], inv_index[self._state_at(i+3).number]] = -self._state_at(i+3).number / self._state_at(i-1).number
|
|
84
|
+
|
|
85
|
+
case OperatorType.END:
|
|
86
|
+
if self.tokens[i-1] != OperatorType.SPLIT_TO:
|
|
87
|
+
A[inv_index[self._state_at(i-1).number], inv_index[self._state_at(i-1).number]] = 1
|
|
88
|
+
|
|
89
|
+
return A,b
|
|
90
|
+
|
|
91
|
+
def get_trials(self) -> float:
|
|
92
|
+
return np.linalg.solve(*self.to_matrix())[0]
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def parse(cls, dsl:str) -> Self:
|
|
96
|
+
|
|
97
|
+
val_parse(dsl)
|
|
98
|
+
|
|
99
|
+
header, body = dsl.split(':')
|
|
100
|
+
|
|
101
|
+
base, target = map(
|
|
102
|
+
int,
|
|
103
|
+
re.fullmatch(r"X(\d+)T(\d+)", header).groups() # type: ignore
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
tokens = []
|
|
107
|
+
buffer = None
|
|
108
|
+
for ch in body:
|
|
109
|
+
match ch:
|
|
110
|
+
case '>':
|
|
111
|
+
if buffer is not None: tokens.append(State(buffer))
|
|
112
|
+
|
|
113
|
+
tokens.append(OperatorType.EXPAND)
|
|
114
|
+
|
|
115
|
+
case '=':
|
|
116
|
+
if buffer is not None: tokens.append(State(buffer))
|
|
117
|
+
|
|
118
|
+
tokens.append(OperatorType.SPLIT)
|
|
119
|
+
|
|
120
|
+
case 'x':
|
|
121
|
+
if buffer is not None: tokens.append(State(buffer, is_node=False))
|
|
122
|
+
|
|
123
|
+
tokens.append(OperatorType.SPLIT_MUL)
|
|
124
|
+
|
|
125
|
+
case '|':
|
|
126
|
+
if buffer is not None: tokens.append(State(buffer, is_node=False))
|
|
127
|
+
|
|
128
|
+
tokens.append(OperatorType.SPLIT_TO)
|
|
129
|
+
|
|
130
|
+
case '!':
|
|
131
|
+
if buffer is not None: tokens.append(State(buffer))
|
|
132
|
+
|
|
133
|
+
tokens.append(OperatorType.END)
|
|
134
|
+
break;
|
|
135
|
+
|
|
136
|
+
case '(':
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
case ')':
|
|
140
|
+
if buffer is not None: tokens.append(State(buffer, is_ref=True))
|
|
141
|
+
break;
|
|
142
|
+
|
|
143
|
+
if ch.isdigit():
|
|
144
|
+
buffer = (buffer or 0) * 10 + int(ch)
|
|
145
|
+
|
|
146
|
+
else:
|
|
147
|
+
buffer = None
|
|
148
|
+
|
|
149
|
+
return cls(
|
|
150
|
+
base = base,
|
|
151
|
+
target = target,
|
|
152
|
+
tokens = tuple(tokens)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def __post_init__(self):
|
|
156
|
+
|
|
157
|
+
val_post_init(
|
|
158
|
+
base=self.base,
|
|
159
|
+
target=self.target,
|
|
160
|
+
tokens=self.tokens
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def generate(base: int, target: int) -> Program:
|
|
164
|
+
val_natural(base)
|
|
165
|
+
val_natural(target)
|
|
166
|
+
|
|
167
|
+
if target>=2 and base==1: raise ValueError("When base({base}) is 1, target should be 1.")
|
|
168
|
+
|
|
169
|
+
current: int = 1
|
|
170
|
+
visited: set = {1}
|
|
171
|
+
tokens: list[State|OperatorType] = [State(1)]
|
|
172
|
+
|
|
173
|
+
while True:
|
|
174
|
+
if current < target:
|
|
175
|
+
current *= base
|
|
176
|
+
|
|
177
|
+
tokens.append(OperatorType.EXPAND)
|
|
178
|
+
|
|
179
|
+
if current in visited:
|
|
180
|
+
tokens.append(State(current, is_ref=True))
|
|
181
|
+
break
|
|
182
|
+
|
|
183
|
+
tokens.append(State(current))
|
|
184
|
+
visited.add(current)
|
|
185
|
+
|
|
186
|
+
elif current == target:
|
|
187
|
+
tokens.append(OperatorType.END)
|
|
188
|
+
break
|
|
189
|
+
|
|
190
|
+
else:
|
|
191
|
+
q:int = current // target
|
|
192
|
+
current %= target
|
|
193
|
+
|
|
194
|
+
tokens.append(OperatorType.SPLIT)
|
|
195
|
+
tokens.append(State(target, is_node=False))
|
|
196
|
+
|
|
197
|
+
if q != 1:
|
|
198
|
+
tokens.append(OperatorType.SPLIT_MUL)
|
|
199
|
+
tokens.append(State(q, is_node=False))
|
|
200
|
+
|
|
201
|
+
tokens.append(OperatorType.SPLIT_TO)
|
|
202
|
+
|
|
203
|
+
if current in visited:
|
|
204
|
+
tokens.append(State(current, is_ref=True))
|
|
205
|
+
break
|
|
206
|
+
|
|
207
|
+
elif current == 0:
|
|
208
|
+
tokens.append(OperatorType.END)
|
|
209
|
+
break
|
|
210
|
+
|
|
211
|
+
tokens.append(State(current))
|
|
212
|
+
visited.add(current)
|
|
213
|
+
|
|
214
|
+
return Program(
|
|
215
|
+
base=base,
|
|
216
|
+
target=target,
|
|
217
|
+
tokens=tuple(tokens)
|
|
218
|
+
)
|
|
219
|
+
|
xbtm/validation.py
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# GENERATED USING CODEX
|
|
2
|
+
# ...because manual labor for this was way beyond my limits.
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .main import OperatorType, State
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
NAT = r"[1-9][0-9]*"
|
|
14
|
+
REF = rf"\({NAT}\)"
|
|
15
|
+
|
|
16
|
+
HEADER_RE = re.compile(rf"X{NAT}T{NAT}")
|
|
17
|
+
|
|
18
|
+
EXPAND_RE = rf">{NAT}"
|
|
19
|
+
SPLIT_RE = rf"={NAT}(?:x{NAT})?\|{NAT}"
|
|
20
|
+
TERMINAL_RE = rf"(?:!|>{REF}|={NAT}(?:x{NAT})?\|{REF}|={NAT}x{NAT}\|!)"
|
|
21
|
+
BODY_RE = re.compile(rf"{NAT}(?:{EXPAND_RE}|{SPLIT_RE})*{TERMINAL_RE}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def val_natural(num: int):
|
|
25
|
+
if not isinstance(num, int):
|
|
26
|
+
raise TypeError(f"{num} should be a natural number.")
|
|
27
|
+
if not num >= 1:
|
|
28
|
+
raise ValueError(f"{num} should be a natural number.")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def val_parse(dsl: str):
|
|
32
|
+
if not isinstance(dsl, str):
|
|
33
|
+
raise TypeError(f"{dsl} should be a str.")
|
|
34
|
+
|
|
35
|
+
if dsl.count(":") != 1:
|
|
36
|
+
raise ValueError(f"{dsl} should contain exactly one ':'.")
|
|
37
|
+
|
|
38
|
+
header, body = dsl.split(":")
|
|
39
|
+
|
|
40
|
+
if HEADER_RE.fullmatch(header) is None:
|
|
41
|
+
raise ValueError(f"{header} is not a valid header.")
|
|
42
|
+
|
|
43
|
+
if BODY_RE.fullmatch(body) is None:
|
|
44
|
+
raise ValueError(f"{body} is not a valid body.")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def val_post_init(base: int, target: int, tokens: tuple[State | OperatorType, ...]):
|
|
48
|
+
from .main import OperatorType, State
|
|
49
|
+
|
|
50
|
+
val_natural(base)
|
|
51
|
+
val_natural(target)
|
|
52
|
+
|
|
53
|
+
if target >= 2 and base == 1:
|
|
54
|
+
raise ValueError(f"When base({base}) is 1, target should be 1.")
|
|
55
|
+
|
|
56
|
+
if len(tokens) == 0:
|
|
57
|
+
raise ValueError("Program should have at least one token.")
|
|
58
|
+
|
|
59
|
+
first = tokens[0]
|
|
60
|
+
if not isinstance(first, State):
|
|
61
|
+
raise ValueError("Program should start with a state.")
|
|
62
|
+
if first.number != 1:
|
|
63
|
+
raise ValueError("Program should start at state 1.")
|
|
64
|
+
if first.is_ref:
|
|
65
|
+
raise ValueError("Program should not start with a reference.")
|
|
66
|
+
if not first.is_node:
|
|
67
|
+
raise ValueError("Program should start with a node.")
|
|
68
|
+
|
|
69
|
+
current = first.number
|
|
70
|
+
visited = {current}
|
|
71
|
+
i = 1
|
|
72
|
+
|
|
73
|
+
while i < len(tokens):
|
|
74
|
+
token = tokens[i]
|
|
75
|
+
|
|
76
|
+
if token == OperatorType.EXPAND:
|
|
77
|
+
i, current, terminated = _val_expand(
|
|
78
|
+
tokens=tokens,
|
|
79
|
+
i=i,
|
|
80
|
+
current=current,
|
|
81
|
+
visited=visited,
|
|
82
|
+
base=base,
|
|
83
|
+
State=State,
|
|
84
|
+
)
|
|
85
|
+
if terminated:
|
|
86
|
+
return
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
if token == OperatorType.SPLIT:
|
|
90
|
+
i, current, terminated = _val_split(
|
|
91
|
+
tokens=tokens,
|
|
92
|
+
i=i,
|
|
93
|
+
current=current,
|
|
94
|
+
visited=visited,
|
|
95
|
+
target=target,
|
|
96
|
+
State=State,
|
|
97
|
+
OperatorType=OperatorType,
|
|
98
|
+
)
|
|
99
|
+
if terminated:
|
|
100
|
+
return
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
if token == OperatorType.END:
|
|
104
|
+
if current != target:
|
|
105
|
+
raise ValueError("Standalone end should only appear at the target state.")
|
|
106
|
+
if i != len(tokens) - 1:
|
|
107
|
+
raise ValueError("No token should appear after end.")
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
raise ValueError(f"Unexpected token: {token}.")
|
|
111
|
+
|
|
112
|
+
raise ValueError("Program should end with end or reference.")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _val_expand(*, tokens, i, current, visited, base, State):
|
|
116
|
+
if i + 1 >= len(tokens):
|
|
117
|
+
raise ValueError("Expand should be followed by a state.")
|
|
118
|
+
|
|
119
|
+
next_state = tokens[i + 1]
|
|
120
|
+
if not isinstance(next_state, State):
|
|
121
|
+
raise ValueError("Expand should be followed by a state.")
|
|
122
|
+
|
|
123
|
+
expected = current * base
|
|
124
|
+
if next_state.number != expected:
|
|
125
|
+
raise ValueError(f"Expand should produce {expected}, not {next_state.number}.")
|
|
126
|
+
|
|
127
|
+
if next_state.is_ref:
|
|
128
|
+
if next_state.number not in visited:
|
|
129
|
+
raise ValueError(f"Reference state {next_state.number} has not been visited.")
|
|
130
|
+
if i + 2 != len(tokens):
|
|
131
|
+
raise ValueError("Reference should terminate the program.")
|
|
132
|
+
return len(tokens), current, True
|
|
133
|
+
|
|
134
|
+
if not next_state.is_node:
|
|
135
|
+
raise ValueError("Expand successor should be a node.")
|
|
136
|
+
if next_state.number in visited:
|
|
137
|
+
raise ValueError(f"State {next_state.number} should be written as a reference.")
|
|
138
|
+
|
|
139
|
+
visited.add(next_state.number)
|
|
140
|
+
return i + 2, next_state.number, False
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _val_split(*, tokens, i, current, visited, target, State, OperatorType):
|
|
144
|
+
if current <= target:
|
|
145
|
+
raise ValueError("Split should only appear when current state is greater than target.")
|
|
146
|
+
|
|
147
|
+
if i + 2 >= len(tokens):
|
|
148
|
+
raise ValueError("Split expression is incomplete.")
|
|
149
|
+
|
|
150
|
+
split_target = tokens[i + 1]
|
|
151
|
+
if not isinstance(split_target, State):
|
|
152
|
+
raise ValueError("Split should be followed by a target term.")
|
|
153
|
+
if split_target.is_ref or split_target.is_node:
|
|
154
|
+
raise ValueError("Split target term should be a non-node state.")
|
|
155
|
+
if split_target.number != target:
|
|
156
|
+
raise ValueError(f"Split target should be {target}, not {split_target.number}.")
|
|
157
|
+
|
|
158
|
+
q = current // target
|
|
159
|
+
r = current % target
|
|
160
|
+
|
|
161
|
+
if tokens[i + 2] == OperatorType.SPLIT_MUL:
|
|
162
|
+
return _val_split_with_mul(
|
|
163
|
+
tokens=tokens,
|
|
164
|
+
i=i,
|
|
165
|
+
current=current,
|
|
166
|
+
visited=visited,
|
|
167
|
+
target=target,
|
|
168
|
+
q=q,
|
|
169
|
+
r=r,
|
|
170
|
+
State=State,
|
|
171
|
+
OperatorType=OperatorType,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
if tokens[i + 2] == OperatorType.SPLIT_TO:
|
|
175
|
+
return _val_split_without_mul(
|
|
176
|
+
tokens=tokens,
|
|
177
|
+
i=i,
|
|
178
|
+
visited=visited,
|
|
179
|
+
q=q,
|
|
180
|
+
r=r,
|
|
181
|
+
State=State,
|
|
182
|
+
OperatorType=OperatorType,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
raise ValueError("Split should contain '|' or 'x'.")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _val_split_with_mul(*, tokens, i, current, visited, target, q, r, State, OperatorType):
|
|
189
|
+
if i + 4 >= len(tokens):
|
|
190
|
+
raise ValueError("Split expression with multiplier is incomplete.")
|
|
191
|
+
|
|
192
|
+
multiplier = tokens[i + 3]
|
|
193
|
+
split_to = tokens[i + 4]
|
|
194
|
+
|
|
195
|
+
if not isinstance(multiplier, State):
|
|
196
|
+
raise ValueError("Split multiplier should be a state.")
|
|
197
|
+
if multiplier.is_ref or multiplier.is_node:
|
|
198
|
+
raise ValueError("Split multiplier should be a non-node state.")
|
|
199
|
+
if multiplier.number != q:
|
|
200
|
+
raise ValueError(f"Split multiplier should be {q}, not {multiplier.number}.")
|
|
201
|
+
if q == 1:
|
|
202
|
+
raise ValueError("Split multiplier should be omitted when q is 1.")
|
|
203
|
+
if split_to != OperatorType.SPLIT_TO:
|
|
204
|
+
raise ValueError("Split multiplier should be followed by '|'.")
|
|
205
|
+
|
|
206
|
+
if i + 5 >= len(tokens):
|
|
207
|
+
raise ValueError("Split remainder is missing.")
|
|
208
|
+
|
|
209
|
+
successor = tokens[i + 5]
|
|
210
|
+
|
|
211
|
+
if successor == OperatorType.END:
|
|
212
|
+
if r != 0:
|
|
213
|
+
raise ValueError("Exact split termination requires zero remainder.")
|
|
214
|
+
if q < 2:
|
|
215
|
+
raise ValueError("Exact split termination requires q >= 2.")
|
|
216
|
+
if current != target * q:
|
|
217
|
+
raise ValueError("Exact split arithmetic is invalid.")
|
|
218
|
+
if i + 6 != len(tokens):
|
|
219
|
+
raise ValueError("No token should appear after exact split termination.")
|
|
220
|
+
return len(tokens), current, True
|
|
221
|
+
|
|
222
|
+
return _val_split_remainder(
|
|
223
|
+
tokens=tokens,
|
|
224
|
+
i=i,
|
|
225
|
+
successor_i=i + 5,
|
|
226
|
+
successor=successor,
|
|
227
|
+
visited=visited,
|
|
228
|
+
r=r,
|
|
229
|
+
State=State,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _val_split_without_mul(*, tokens, i, visited, q, r, State, OperatorType):
|
|
234
|
+
if q != 1:
|
|
235
|
+
raise ValueError("Split multiplier should be written when q is greater than 1.")
|
|
236
|
+
|
|
237
|
+
if i + 3 >= len(tokens):
|
|
238
|
+
raise ValueError("Split remainder is missing.")
|
|
239
|
+
|
|
240
|
+
successor = tokens[i + 3]
|
|
241
|
+
if successor == OperatorType.END:
|
|
242
|
+
raise ValueError("Exact split termination should include an explicit multiplier.")
|
|
243
|
+
|
|
244
|
+
return _val_split_remainder(
|
|
245
|
+
tokens=tokens,
|
|
246
|
+
i=i,
|
|
247
|
+
successor_i=i + 3,
|
|
248
|
+
successor=successor,
|
|
249
|
+
visited=visited,
|
|
250
|
+
r=r,
|
|
251
|
+
State=State,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _val_split_remainder(*, tokens, i, successor_i, successor, visited, r, State):
|
|
256
|
+
if r == 0:
|
|
257
|
+
raise ValueError("Zero remainder should be written as exact split termination.")
|
|
258
|
+
|
|
259
|
+
if not isinstance(successor, State):
|
|
260
|
+
raise ValueError("Split remainder should be a state.")
|
|
261
|
+
if successor.number != r:
|
|
262
|
+
raise ValueError(f"Split remainder should be {r}, not {successor.number}.")
|
|
263
|
+
|
|
264
|
+
if successor.is_ref:
|
|
265
|
+
if successor.number not in visited:
|
|
266
|
+
raise ValueError(f"Reference state {successor.number} has not been visited.")
|
|
267
|
+
if successor_i + 1 != len(tokens):
|
|
268
|
+
raise ValueError("Reference should terminate the program.")
|
|
269
|
+
return len(tokens), successor.number, True
|
|
270
|
+
|
|
271
|
+
if not successor.is_node:
|
|
272
|
+
raise ValueError("Split remainder should be a node.")
|
|
273
|
+
if successor.number in visited:
|
|
274
|
+
raise ValueError(f"State {successor.number} should be written as a reference.")
|
|
275
|
+
|
|
276
|
+
visited.add(successor.number)
|
|
277
|
+
return successor_i + 1, successor.number, False
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xbtm
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: XbTm (X-base Target Mapping) is a tool and DSL for generating canonical, entropy-efficient mappings from an x-way random source to an m-way target choice.
|
|
5
|
+
Project-URL: Repository, https://github.com/Imagination12357/xbtm
|
|
6
|
+
Project-URL: Issues, https://github.com/Imagination12357/xbtm/issues
|
|
7
|
+
Author: Imagination12357
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: dsl,entropy,probability,random,random-generation,rejection-sampling
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: numpy>=2.0.0
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
XbTm (X-base Target Mapping) is a tool and DSL for generating canonical, entropy-efficient mappings from an x-way random source to an m-way target choice.
|
|
24
|
+
|
|
25
|
+
`xbtm` is a DSL for deterministically exploring and visualizing ways to emulate a uniform random generator with `target` outcomes using one with `base` outcomes.
|
|
26
|
+
|
|
27
|
+
## DSL format
|
|
28
|
+
|
|
29
|
+
A complete program combines a header and a body with a colon.
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
X{base}T{target}:{body}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`base` and `target` are natural numbers. When `target` is at least 2, `base` must also be at least 2. When `target` is 1, `base` may be 1 because there is no outcome to choose between.
|
|
36
|
+
|
|
37
|
+
Every body starts at the ordinary state `1`.
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
X2T5:1>2>4>8=5|3>6=5|(1)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## States and termination
|
|
44
|
+
|
|
45
|
+
- `n`: An ordinary, newly visited state `n`, written as a natural number.
|
|
46
|
+
- `(n)`: A reference state that returns to the previously visited state `n`. A reference state terminates the body.
|
|
47
|
+
- `!`: A termination marker indicating that the current ordinary state exactly equals `target`. The implementation represents it as an operator token for convenience.
|
|
48
|
+
|
|
49
|
+
Thus, a body starts at an ordinary state, repeats transitions, and ends with either a reference state or `!`.
|
|
50
|
+
|
|
51
|
+
## Transitions
|
|
52
|
+
|
|
53
|
+
There are two kinds of transition.
|
|
54
|
+
|
|
55
|
+
### Expand: `>`
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
old_state>next_state
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`expand` multiplies the current state by `base`.
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
next_state = old_state * base
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Split: `=`
|
|
68
|
+
|
|
69
|
+
```text
|
|
70
|
+
old_state=pxq|r
|
|
71
|
+
old_state=p|r # when q == 1
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`split` divides the current state using `target`.
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
p = target
|
|
78
|
+
q = old_state // target
|
|
79
|
+
r = old_state % target
|
|
80
|
+
old_state = p * q + r
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Here, `p` and `q` describe the split, and `r` is the next state. When `q` is 1, `x1` is omitted. If the next state `r` was already visited, it is written as `(r)` and the body ends.
|
|
84
|
+
|
|
85
|
+
When the split has no remainder, it ends with `|!` instead of creating state `0`.
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
old_state=pxq|!
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
For the full DSL rules, see the [DSL specification](https://github.com/Imagination12357/xbtm/blob/main/docs/dsl-spec.md).
|
|
92
|
+
|
|
93
|
+
## Installation
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pip install xbtm
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Python usage
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from xbtm import Program, generate
|
|
103
|
+
|
|
104
|
+
program = generate(2, 5)
|
|
105
|
+
|
|
106
|
+
print(program.text)
|
|
107
|
+
print(program.get_trials())
|
|
108
|
+
|
|
109
|
+
same_program = Program.parse("X2T5:1>2>4>8=5|3>6=5|(1)")
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Examples
|
|
113
|
+
|
|
114
|
+
```text
|
|
115
|
+
X2T8:1>2>4>8!
|
|
116
|
+
X7T8:1>7>49=8x6|(1)
|
|
117
|
+
X1T1:1!
|
|
118
|
+
X4T2:1>4=2x2|!
|
|
119
|
+
```
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
xbtm/__init__.py,sha256=gsc-c20HzxEpTxL1FwEAY13xuFGJvmtwLqxUxdqiFxg,56
|
|
2
|
+
xbtm/main.py,sha256=_eWjcgRC-c2-yKZ5S3xqU-HdRG6pcLuXy5UhvByrpkM,6847
|
|
3
|
+
xbtm/validation.py,sha256=Nt2friYDdFbL_UZLLRRmze4ARuWU7GX-R6rAPMtFzKA,8946
|
|
4
|
+
xbtm-0.1.0.dist-info/METADATA,sha256=p_I6VDooal9SSGPdhFLKDpgiyYFXduBJNRvBs2DvnpM,3384
|
|
5
|
+
xbtm-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
6
|
+
xbtm-0.1.0.dist-info/licenses/LICENSE,sha256=hmxe-_8PyFXkmhDqeol7jo9z566NxqgxP5nUfpCZbWk,1094
|
|
7
|
+
xbtm-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Imagination12357
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|