pyformula 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.
pyformula/__init__.py ADDED
@@ -0,0 +1,26 @@
1
+ from .compiler.compiler import FormulaCompiler
2
+ from .compiler.models import Expression
3
+ from .exceptions import (
4
+ FormulaAlreadyRegisteredError,
5
+ FormulaNotFoundError,
6
+ FormulaNotRegisteredError,
7
+ )
8
+ from .formula import Formula
9
+ from .models import Number, Operator
10
+ from .operator import OperatorFn
11
+ from .registry import VariablesRegistry
12
+ from .variable import variable
13
+
14
+ __all__ = [
15
+ "Expression",
16
+ "Formula",
17
+ "FormulaAlreadyRegisteredError",
18
+ "FormulaCompiler",
19
+ "FormulaNotFoundError",
20
+ "FormulaNotRegisteredError",
21
+ "Number",
22
+ "Operator",
23
+ "OperatorFn",
24
+ "VariablesRegistry",
25
+ "variable",
26
+ ]
File without changes
@@ -0,0 +1,160 @@
1
+ from collections.abc import Callable
2
+ from decimal import Decimal
3
+ from typing import Any, cast
4
+
5
+ from pyformula import math
6
+ from pyformula.exceptions import FormulaNotFoundError, InvalidExpressionError
7
+ from pyformula.formula import Formula
8
+ from pyformula.operator import OPERATORS
9
+
10
+ from .models import Expression, FormulaDict, is_formula_dict, is_round_wrapper_dict
11
+
12
+ WRAPPER_FNS: dict[str, Callable[[Formula[Any]], Formula[Any]]] = {
13
+ "positive": lambda fm: +fm,
14
+ "negative": lambda fm: -fm,
15
+ "absolute": abs,
16
+ "ceil": math.ceil,
17
+ "floor": math.floor,
18
+ "trunc": math.trunc,
19
+ "sqrt": math.sqrt,
20
+ "cbrt": math.cbrt,
21
+ "exp": math.exp,
22
+ "exp2": math.exp2,
23
+ "expm1": math.expm1,
24
+ "log10": math.log10,
25
+ "log1p": math.log1p,
26
+ "log2": math.log2,
27
+ "sin": math.sin,
28
+ "sinh": math.sinh,
29
+ "asin": math.asin,
30
+ "asinh": math.asinh,
31
+ "cos": math.cos,
32
+ "cosh": math.cosh,
33
+ "acos": math.acos,
34
+ "acosh": math.acosh,
35
+ "tan": math.tan,
36
+ "tanh": math.tanh,
37
+ "atan": math.atan,
38
+ "atanh": math.atanh,
39
+ "degrees": math.degrees,
40
+ "radians": math.radians,
41
+ "erf": math.erf,
42
+ "erfc": math.erfc,
43
+ "gamma": math.gamma,
44
+ "lgamma": math.lgamma,
45
+ "fabs": math.fabs,
46
+ "ulp": math.ulp,
47
+ }
48
+
49
+
50
+ class FormulaCompiler[T]:
51
+ """Compile a structured expression tree into a runtime formula.
52
+
53
+ Examples:
54
+ ```python
55
+ from dataclasses import dataclass
56
+ from pyformula import Formula
57
+
58
+
59
+ @dataclass
60
+ class Rectangular:
61
+ width: float
62
+ height: float
63
+
64
+
65
+ def main() -> None:
66
+ fms: dict[str, Formula[Rectangular]] = {
67
+ "width": Formula[Rectangular](lambda rectangle: rectangle.width),
68
+ "height": Formula[Rectangular](lambda rectangle: rectangle.height),
69
+ }
70
+
71
+ compiler = FormulaCompiler[Rectangular](fms)
72
+
73
+ perimeter = compiler.compile(
74
+ {
75
+ "operator": "multiply",
76
+ "expressions":[
77
+ "operator": "add",
78
+ "expressions": [
79
+ "width",
80
+ "height",
81
+ ], 2,
82
+ ]
83
+ }
84
+ )
85
+
86
+ area = compiler.compile(
87
+ {
88
+ "operator": "multiply",
89
+ "expressions": [
90
+ "width",
91
+ "height",
92
+ ],
93
+ }
94
+ )
95
+
96
+ print(perimeter(Rectangular(width=10, height=20))) # 60.0
97
+ print(area(Rectangular(width=10, height=20))) # 200.0
98
+
99
+ print(perimeter(Rectangular(width=5, height=10))) # 30.0
100
+ print(area(Rectangular(width=5, height=10))) # 50.0
101
+
102
+
103
+ if __name__ == "__main__":
104
+ main()
105
+ ```
106
+
107
+
108
+ """
109
+
110
+ def __init__(self, fns: dict[str, Formula[T]], /) -> None:
111
+ self._fns = fns
112
+
113
+ def compile(self, expression: Expression) -> Formula[T]:
114
+ """Compile an expression object into a formula callable for a given model type."""
115
+ if isinstance(expression, (int, float, Decimal)):
116
+ return Formula(lambda _: expression)
117
+
118
+ if isinstance(expression, str) and expression not in self._fns:
119
+ raise FormulaNotFoundError(expression)
120
+
121
+ if isinstance(expression, str):
122
+ return self._fns[expression]
123
+
124
+ if is_formula_dict(expression):
125
+ return self._compile_formula(expression)
126
+
127
+ if is_round_wrapper_dict(expression) and not isinstance(expression["ndigits"], int):
128
+ raise InvalidExpressionError(expression)
129
+
130
+ if is_round_wrapper_dict(expression):
131
+ return round(self.compile(expression["round"]), ndigits=expression["ndigits"])
132
+
133
+ if not isinstance(expression, dict):
134
+ raise InvalidExpressionError(expression)
135
+
136
+ for fn_name, wrap in WRAPPER_FNS.items():
137
+ if fn_name in expression and len(expression) == 1:
138
+ return wrap(self.compile(expression[fn_name]))
139
+
140
+ raise InvalidExpressionError(expression)
141
+
142
+ def _compile_formula(self, fm_dict: FormulaDict) -> Formula[T]:
143
+ expressions = fm_dict["expressions"]
144
+ operator = fm_dict["operator"]
145
+
146
+ if (
147
+ not isinstance(expressions, list)
148
+ or not expressions
149
+ or not isinstance(operator, str)
150
+ or operator not in OPERATORS
151
+ ):
152
+ raise InvalidExpressionError(fm_dict)
153
+
154
+ formula = self.compile(expressions[0])
155
+ applier, _ = OPERATORS[operator]
156
+
157
+ for expression in expressions[1:]:
158
+ formula = applier(formula, self.compile(expression))
159
+
160
+ return cast("Formula[T]", formula)
@@ -0,0 +1,330 @@
1
+ from typing import TypeAlias, TypedDict, TypeGuard
2
+
3
+ from pyformula.models import Number, Operator
4
+
5
+ # -----------------------
6
+ # basic operators
7
+ # -----------------------
8
+
9
+
10
+ class PositiveWrapperDict(TypedDict):
11
+ """Typed dict for a unary positive wrapper expression."""
12
+
13
+ positive: "Expression"
14
+
15
+
16
+ class NegativeWrapperDict(TypedDict):
17
+ """Typed dict for a unary negative wrapper expression."""
18
+
19
+ negative: "Expression"
20
+
21
+
22
+ class AbsoluteWrapperDict(TypedDict):
23
+ """Typed dict for an absolute-value wrapper expression."""
24
+
25
+ absolute: "Expression"
26
+
27
+
28
+ class RoundWrapperDict(TypedDict):
29
+ """Typed dict for a round wrapper that carries an integer precision."""
30
+
31
+ round: "Expression"
32
+ ndigits: int
33
+
34
+
35
+ def is_round_wrapper_dict(obj: object) -> TypeGuard[RoundWrapperDict]:
36
+ """Return whether the object matches the round-wrapper schema."""
37
+ return _is_typed_dict(obj, "round", "ndigits")
38
+
39
+
40
+ # -----------------------
41
+ # rounding
42
+ # -----------------------
43
+
44
+
45
+ class FloorWrapperDict(TypedDict):
46
+ """Typed dict for a floor wrapper expression."""
47
+
48
+ floor: "Expression"
49
+
50
+
51
+ class CeilWrapperDict(TypedDict):
52
+ """Typed dict for a ceil wrapper expression."""
53
+
54
+ ceil: "Expression"
55
+
56
+
57
+ class TruncWrapperDict(TypedDict):
58
+ """Typed dict for a trunc wrapper expression."""
59
+
60
+ trunc: "Expression"
61
+
62
+
63
+ # -----------------------
64
+ # roots and powers
65
+ # -----------------------
66
+
67
+
68
+ class SqrtWrapperDict(TypedDict):
69
+ """Typed dict for a square-root wrapper expression."""
70
+
71
+ sqrt: "Expression"
72
+
73
+
74
+ class CbrtWrapperDict(TypedDict):
75
+ """Typed dict for a cube-root wrapper expression."""
76
+
77
+ cbrt: "Expression"
78
+
79
+
80
+ # ----------------------------------------------
81
+ # exponential and logarithmic
82
+ # ----------------------------------------------
83
+
84
+
85
+ class ExpWrapperDict(TypedDict):
86
+ """Typed dict for an exp wrapper expression."""
87
+
88
+ exp: "Expression"
89
+
90
+
91
+ class Exp2WrapperDict(TypedDict):
92
+ """Typed dict for an exp2 wrapper expression."""
93
+
94
+ exp2: "Expression"
95
+
96
+
97
+ class Expm1WrapperDict(TypedDict):
98
+ """Typed dict for an expm1 wrapper expression."""
99
+
100
+ expm1: "Expression"
101
+
102
+
103
+ class Log10WrapperDict(TypedDict):
104
+ """Typed dict for a log10 wrapper expression."""
105
+
106
+ log10: "Expression"
107
+
108
+
109
+ class Log1pWrapperDict(TypedDict):
110
+ """Typed dict for a log1p wrapper expression."""
111
+
112
+ log1p: "Expression"
113
+
114
+
115
+ class Log2WrapperDict(TypedDict):
116
+ """Typed dict for a log2 wrapper expression."""
117
+
118
+ log2: "Expression"
119
+
120
+
121
+ # ----------------------------------------------
122
+ # trigonometry
123
+ # ----------------------------------------------
124
+
125
+
126
+ class SinWrapperDict(TypedDict):
127
+ """Typed dict for a sine wrapper expression."""
128
+
129
+ sin: "Expression"
130
+
131
+
132
+ class SinhWrapperDict(TypedDict):
133
+ """Typed dict for a hyperbolic sine wrapper expression."""
134
+
135
+ sinh: "Expression"
136
+
137
+
138
+ class AsinWrapperDict(TypedDict):
139
+ """Typed dict for an arc-sine wrapper expression."""
140
+
141
+ asin: "Expression"
142
+
143
+
144
+ class AsinhWrapperDict(TypedDict):
145
+ """Typed dict for an inverse hyperbolic sine wrapper expression."""
146
+
147
+ asinh: "Expression"
148
+
149
+
150
+ class CosWrapperDict(TypedDict):
151
+ """Typed dict for a cosine wrapper expression."""
152
+
153
+ cos: "Expression"
154
+
155
+
156
+ class CoshWrapperDict(TypedDict):
157
+ """Typed dict for a hyperbolic cosine wrapper expression."""
158
+
159
+ cosh: "Expression"
160
+
161
+
162
+ class AcosWrapperDict(TypedDict):
163
+ """Typed dict for an arc-cosine wrapper expression."""
164
+
165
+ acos: "Expression"
166
+
167
+
168
+ class AcoshWrapperDict(TypedDict):
169
+ """Typed dict for an inverse hyperbolic cosine wrapper expression."""
170
+
171
+ acosh: "Expression"
172
+
173
+
174
+ class TanWrapperDict(TypedDict):
175
+ """Typed dict for a tangent wrapper expression."""
176
+
177
+ tan: "Expression"
178
+
179
+
180
+ class TanhWrapperDict(TypedDict):
181
+ """Typed dict for a hyperbolic tangent wrapper expression."""
182
+
183
+ tanh: "Expression"
184
+
185
+
186
+ class AtanWrapperDict(TypedDict):
187
+ """Typed dict for an arc-tangent wrapper expression."""
188
+
189
+ atan: "Expression"
190
+
191
+
192
+ class AtanhWrapperDict(TypedDict):
193
+ """Typed dict for an inverse hyperbolic tangent wrapper expression."""
194
+
195
+ atanh: "Expression"
196
+
197
+
198
+ # ----------------------------------------------
199
+ # angular conversion
200
+ # ----------------------------------------------
201
+
202
+
203
+ class DegreesWrapperDict(TypedDict):
204
+ """Typed dict for a degrees-conversion wrapper expression."""
205
+
206
+ degrees: "Expression"
207
+
208
+
209
+ class RadiansWrapperDict(TypedDict):
210
+ """Typed dict for a radians-conversion wrapper expression."""
211
+
212
+ radians: "Expression"
213
+
214
+
215
+ # ----------------------------------------------
216
+ # special functions
217
+ # ----------------------------------------------
218
+
219
+
220
+ class ErfWrapperDict(TypedDict):
221
+ """Typed dict for an error-function wrapper expression."""
222
+
223
+ erf: "Expression"
224
+
225
+
226
+ class ErfcWrapperDict(TypedDict):
227
+ """Typed dict for a complementary error-function wrapper expression."""
228
+
229
+ erfc: "Expression"
230
+
231
+
232
+ class GammaWrapperDict(TypedDict):
233
+ """Typed dict for a gamma-function wrapper expression."""
234
+
235
+ gamma: "Expression"
236
+
237
+
238
+ class LgammaWrapperDict(TypedDict):
239
+ """Typed dict for a logarithmic gamma wrapper expression."""
240
+
241
+ lgamma: "Expression"
242
+
243
+
244
+ class FabsWrapperDict(TypedDict):
245
+ """Typed dict for a floating-point absolute-value wrapper expression."""
246
+
247
+ fabs: "Expression"
248
+
249
+
250
+ class UlpWrapperDict(TypedDict):
251
+ """Typed dict for an ulp wrapper expression."""
252
+
253
+ ulp: "Expression"
254
+
255
+
256
+ # -----------------------
257
+ # formula
258
+ # -----------------------
259
+
260
+
261
+ Expression: TypeAlias = "FormulaDict | WrapperDict | str | Number"
262
+
263
+
264
+ WrapperDict: TypeAlias = (
265
+ # basic operators
266
+ PositiveWrapperDict
267
+ | NegativeWrapperDict
268
+ | AbsoluteWrapperDict
269
+ | RoundWrapperDict
270
+ # rounding
271
+ | FloorWrapperDict
272
+ | CeilWrapperDict
273
+ | TruncWrapperDict
274
+ # roots and powers
275
+ | SqrtWrapperDict
276
+ | CbrtWrapperDict
277
+ # exponential and logarithmic
278
+ | ExpWrapperDict
279
+ | Exp2WrapperDict
280
+ | Expm1WrapperDict
281
+ | Log10WrapperDict
282
+ | Log1pWrapperDict
283
+ | Log2WrapperDict
284
+ # trigonometry
285
+ | SinWrapperDict
286
+ | SinhWrapperDict
287
+ | AsinWrapperDict
288
+ | AsinhWrapperDict
289
+ | CosWrapperDict
290
+ | CoshWrapperDict
291
+ | AcosWrapperDict
292
+ | AcoshWrapperDict
293
+ | TanWrapperDict
294
+ | TanhWrapperDict
295
+ | AtanWrapperDict
296
+ | AtanhWrapperDict
297
+ # angular conversion
298
+ | DegreesWrapperDict
299
+ | RadiansWrapperDict
300
+ # special functions
301
+ | ErfWrapperDict
302
+ | ErfcWrapperDict
303
+ | GammaWrapperDict
304
+ | LgammaWrapperDict
305
+ | FabsWrapperDict
306
+ | UlpWrapperDict
307
+ )
308
+
309
+
310
+ class FormulaDict(TypedDict):
311
+ """Typed dict describing a binary operation expression tree."""
312
+
313
+ operator: Operator
314
+ expressions: list["Expression"]
315
+
316
+
317
+ def is_formula_dict(obj: object) -> TypeGuard[FormulaDict]:
318
+ """Return whether the object matches the binary-formula schema."""
319
+ return _is_typed_dict(obj, "operator", "expressions")
320
+
321
+
322
+ # -----------------------
323
+ # helper
324
+ # -----------------------
325
+
326
+
327
+ def _is_typed_dict(obj: object, *keys: str) -> bool:
328
+ return (
329
+ isinstance(obj, dict) and all(key in obj for key in keys) and len(obj.keys()) == len(keys)
330
+ )
@@ -0,0 +1,29 @@
1
+ from typing import Any
2
+
3
+
4
+ class FormulaAlreadyRegisteredError(Exception):
5
+ """Raised when attempting to register a formula name that already exists."""
6
+
7
+ def __init__(self, name: str) -> None:
8
+ super().__init__(f"Formula already registered: {name}")
9
+
10
+
11
+ class FormulaNotRegisteredError(Exception):
12
+ """Raised when a requested formula name has not been registered."""
13
+
14
+ def __init__(self, name: str) -> None:
15
+ super().__init__(f"Formula not registered: {name}")
16
+
17
+
18
+ class FormulaNotFoundError(Exception):
19
+ """Raised when a compiled expression references a formula that is not in scope."""
20
+
21
+ def __init__(self, name: str) -> None:
22
+ super().__init__(f"Formula not found: {name}")
23
+
24
+
25
+ class InvalidExpressionError(Exception):
26
+ """Raised when an expression structure does not match the compiler schema."""
27
+
28
+ def __init__(self, expression: Any, /) -> None:
29
+ super().__init__(f"Invalid expression: {expression}")