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 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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.