latex2code 0.2.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.
- latex2code/__init__.py +38 -0
- latex2code/__main__.py +4 -0
- latex2code/api.py +461 -0
- latex2code/backends.py +88 -0
- latex2code/cli.py +230 -0
- latex2code/core.py +35 -0
- latex2code/errors.py +210 -0
- latex2code/normalize.py +207 -0
- latex2code/parsing.py +597 -0
- latex2code/printers.py +205 -0
- latex2code/py.typed +0 -0
- latex2code-0.2.0.dist-info/METADATA +481 -0
- latex2code-0.2.0.dist-info/RECORD +17 -0
- latex2code-0.2.0.dist-info/WHEEL +5 -0
- latex2code-0.2.0.dist-info/entry_points.txt +2 -0
- latex2code-0.2.0.dist-info/licenses/LICENSE +19 -0
- latex2code-0.2.0.dist-info/top_level.txt +1 -0
latex2code/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
2
|
+
|
|
3
|
+
from latex2code.core import (
|
|
4
|
+
Backend,
|
|
5
|
+
CodeGenerationError,
|
|
6
|
+
CompiledFormula,
|
|
7
|
+
InvalidLaTeXSyntaxError,
|
|
8
|
+
InvalidPythonIdentifierError,
|
|
9
|
+
LaTeXTranspilerError,
|
|
10
|
+
PiecewiseEvaluationWarning,
|
|
11
|
+
TranspilationInfo,
|
|
12
|
+
UnsupportedLaTeXFeatureError,
|
|
13
|
+
compile_latex,
|
|
14
|
+
inspect_latex,
|
|
15
|
+
latex_to_code,
|
|
16
|
+
transpile_latex,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
__version__ = version("latex2code")
|
|
21
|
+
except PackageNotFoundError:
|
|
22
|
+
__version__ = "0+unknown"
|
|
23
|
+
__all__ = [
|
|
24
|
+
"__version__",
|
|
25
|
+
"Backend",
|
|
26
|
+
"transpile_latex",
|
|
27
|
+
"compile_latex",
|
|
28
|
+
"inspect_latex",
|
|
29
|
+
"latex_to_code",
|
|
30
|
+
"CompiledFormula",
|
|
31
|
+
"TranspilationInfo",
|
|
32
|
+
"LaTeXTranspilerError",
|
|
33
|
+
"CodeGenerationError",
|
|
34
|
+
"InvalidLaTeXSyntaxError",
|
|
35
|
+
"UnsupportedLaTeXFeatureError",
|
|
36
|
+
"InvalidPythonIdentifierError",
|
|
37
|
+
"PiecewiseEvaluationWarning",
|
|
38
|
+
]
|
latex2code/__main__.py
ADDED
latex2code/api.py
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
import re
|
|
3
|
+
import warnings
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Callable, Mapping, Sequence
|
|
6
|
+
|
|
7
|
+
import sympy as sp
|
|
8
|
+
|
|
9
|
+
from .backends import BACKENDS, Backend
|
|
10
|
+
from .errors import (
|
|
11
|
+
CodeGenerationError,
|
|
12
|
+
InvalidLaTeXSyntaxError,
|
|
13
|
+
LaTeXTranspilerError,
|
|
14
|
+
PiecewiseEvaluationWarning,
|
|
15
|
+
_validate_python_identifier,
|
|
16
|
+
)
|
|
17
|
+
from .normalize import _normalize_latex_string
|
|
18
|
+
from .parsing import (
|
|
19
|
+
_LATEX_PARSE_ERRORS,
|
|
20
|
+
_check_for_undefined_commands,
|
|
21
|
+
_ordered_free_symbols,
|
|
22
|
+
_parse_cases_environment,
|
|
23
|
+
_parse_expression,
|
|
24
|
+
_parse_matrix_environment,
|
|
25
|
+
_parse_operatorname_expression,
|
|
26
|
+
)
|
|
27
|
+
from .printers import _expression_tree, _FrameworkPrinter, _PythonPrinter
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class TranspilationInfo:
|
|
32
|
+
"""Parsed expression and generated source returned by :func:`inspect_latex`."""
|
|
33
|
+
|
|
34
|
+
input_expression: str
|
|
35
|
+
backend: str
|
|
36
|
+
parsed_expression: str
|
|
37
|
+
expression_tree: dict[str, object]
|
|
38
|
+
variables: tuple[str, ...]
|
|
39
|
+
generated_code: str
|
|
40
|
+
warnings: tuple[str, ...]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class CompiledFormula:
|
|
45
|
+
"""Callable generated formula together with its source and inspection details."""
|
|
46
|
+
|
|
47
|
+
info: TranspilationInfo
|
|
48
|
+
function: Callable[..., object]
|
|
49
|
+
|
|
50
|
+
def __call__(self, *args: object, **kwargs: object) -> object:
|
|
51
|
+
return self.function(*args, **kwargs)
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def source(self) -> str:
|
|
55
|
+
return self.info.generated_code
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _transpile_latex(
|
|
59
|
+
latex_str: str,
|
|
60
|
+
*,
|
|
61
|
+
function_name: str,
|
|
62
|
+
type_hints: bool,
|
|
63
|
+
use_numpy: bool | None,
|
|
64
|
+
backend: Backend | None,
|
|
65
|
+
variable_map: Mapping[str, str] | None,
|
|
66
|
+
args: Sequence[str] | None,
|
|
67
|
+
constants: Mapping[str, object] | None,
|
|
68
|
+
warning_stacklevel: int,
|
|
69
|
+
) -> TranspilationInfo:
|
|
70
|
+
"""Build generated source and inspection details from a LaTeX expression."""
|
|
71
|
+
if not latex_str or not latex_str.strip():
|
|
72
|
+
raise LaTeXTranspilerError("LaTeX expression string cannot be empty.")
|
|
73
|
+
_validate_python_identifier(function_name, "Function name")
|
|
74
|
+
if use_numpy is not None:
|
|
75
|
+
warnings.warn(
|
|
76
|
+
"use_numpy is deprecated; use backend='numpy' instead.",
|
|
77
|
+
DeprecationWarning,
|
|
78
|
+
stacklevel=warning_stacklevel,
|
|
79
|
+
)
|
|
80
|
+
if backend is None:
|
|
81
|
+
backend = "numpy" if use_numpy else "python"
|
|
82
|
+
elif not isinstance(backend, str) or backend not in {
|
|
83
|
+
"python",
|
|
84
|
+
"numpy",
|
|
85
|
+
"torch",
|
|
86
|
+
"jax",
|
|
87
|
+
}:
|
|
88
|
+
raise LaTeXTranspilerError(
|
|
89
|
+
f"Unknown backend {backend!r}; choose 'python', 'numpy', 'torch', or 'jax'."
|
|
90
|
+
)
|
|
91
|
+
if use_numpy and backend != "numpy":
|
|
92
|
+
raise LaTeXTranspilerError(
|
|
93
|
+
"use_numpy=True conflicts with an explicit backend; use backend='numpy' instead."
|
|
94
|
+
)
|
|
95
|
+
backend_spec = BACKENDS[backend]
|
|
96
|
+
normalized_latex = _normalize_latex_string(latex_str)
|
|
97
|
+
|
|
98
|
+
# 1. Parse matrix, piecewise, or general expression
|
|
99
|
+
try:
|
|
100
|
+
expr = _parse_matrix_environment(normalized_latex)
|
|
101
|
+
is_matrix = expr is not None
|
|
102
|
+
|
|
103
|
+
if not is_matrix:
|
|
104
|
+
expr = _parse_cases_environment(normalized_latex)
|
|
105
|
+
if expr is not None:
|
|
106
|
+
is_matrix = False
|
|
107
|
+
|
|
108
|
+
if expr is None:
|
|
109
|
+
_check_for_undefined_commands(sp.Symbol("temp"), normalized_latex)
|
|
110
|
+
if r"\operatorname" in normalized_latex:
|
|
111
|
+
expr = _parse_operatorname_expression(normalized_latex)
|
|
112
|
+
else:
|
|
113
|
+
expr = _parse_expression(normalized_latex)
|
|
114
|
+
_check_for_undefined_commands(expr, normalized_latex)
|
|
115
|
+
|
|
116
|
+
# Evaluate derivatives, limits, or integrals symbolically if present
|
|
117
|
+
if (
|
|
118
|
+
expr.has(sp.Derivative)
|
|
119
|
+
or expr.has(sp.Integral)
|
|
120
|
+
or expr.has(sp.Limit)
|
|
121
|
+
or expr.has(sp.Product)
|
|
122
|
+
):
|
|
123
|
+
expr = expr.doit()
|
|
124
|
+
except _LATEX_PARSE_ERRORS as exc:
|
|
125
|
+
raise InvalidLaTeXSyntaxError(latex_str, original_error=exc) from exc
|
|
126
|
+
|
|
127
|
+
constant_values: Mapping[str, object]
|
|
128
|
+
if constants is None:
|
|
129
|
+
constant_values = {"pi": sp.pi, "e": sp.E}
|
|
130
|
+
else:
|
|
131
|
+
if not isinstance(constants, Mapping):
|
|
132
|
+
raise LaTeXTranspilerError(
|
|
133
|
+
"constants must map symbol names to numeric values."
|
|
134
|
+
)
|
|
135
|
+
if not all(
|
|
136
|
+
isinstance(name, str) and isinstance(value, (int, float, complex, sp.Expr))
|
|
137
|
+
for name, value in constants.items()
|
|
138
|
+
):
|
|
139
|
+
raise LaTeXTranspilerError(
|
|
140
|
+
"constant names must be strings and values must be numbers or SymPy expressions."
|
|
141
|
+
)
|
|
142
|
+
unused_constants = (
|
|
143
|
+
set(constants) - {str(symbol) for symbol in expr.free_symbols} - {"pi"}
|
|
144
|
+
)
|
|
145
|
+
if unused_constants:
|
|
146
|
+
raise LaTeXTranspilerError(
|
|
147
|
+
"constants contains symbols not present in the expression: "
|
|
148
|
+
f"{', '.join(sorted(unused_constants))}."
|
|
149
|
+
)
|
|
150
|
+
constant_values = constants
|
|
151
|
+
expr = expr.subs(
|
|
152
|
+
{sp.Symbol(name): sp.sympify(value) for name, value in constant_values.items()}
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# 2. Extract free variables
|
|
156
|
+
variables = _ordered_free_symbols(expr, normalized_latex)
|
|
157
|
+
if args is not None:
|
|
158
|
+
if (
|
|
159
|
+
isinstance(args, (str, bytes))
|
|
160
|
+
or not isinstance(args, Sequence)
|
|
161
|
+
or not all(isinstance(argument, str) for argument in args)
|
|
162
|
+
):
|
|
163
|
+
raise LaTeXTranspilerError("args must be a sequence of symbol names.")
|
|
164
|
+
if len(set(args)) != len(args) or set(args) != set(variables):
|
|
165
|
+
raise LaTeXTranspilerError(
|
|
166
|
+
"args must contain every expression variable exactly once."
|
|
167
|
+
)
|
|
168
|
+
variables = list(args)
|
|
169
|
+
if variable_map is not None:
|
|
170
|
+
if not isinstance(variable_map, Mapping):
|
|
171
|
+
raise LaTeXTranspilerError(
|
|
172
|
+
"variable_map must map symbol names to Python identifiers."
|
|
173
|
+
)
|
|
174
|
+
if not all(
|
|
175
|
+
isinstance(source, str) and isinstance(target, str)
|
|
176
|
+
for source, target in variable_map.items()
|
|
177
|
+
):
|
|
178
|
+
raise LaTeXTranspilerError("variable_map keys and values must be strings.")
|
|
179
|
+
unknown_symbols = set(variable_map) - set(variables)
|
|
180
|
+
if unknown_symbols:
|
|
181
|
+
raise LaTeXTranspilerError(
|
|
182
|
+
"variable_map contains symbols not present in the expression: "
|
|
183
|
+
f"{', '.join(sorted(unknown_symbols))}."
|
|
184
|
+
)
|
|
185
|
+
mapped_variables = [
|
|
186
|
+
variable_map.get(variable, variable) for variable in variables
|
|
187
|
+
]
|
|
188
|
+
for variable in mapped_variables:
|
|
189
|
+
_validate_python_identifier(variable, "Mapped argument name")
|
|
190
|
+
if len(set(mapped_variables)) != len(mapped_variables):
|
|
191
|
+
raise LaTeXTranspilerError(
|
|
192
|
+
"variable_map must not map multiple symbols to the same Python identifier."
|
|
193
|
+
)
|
|
194
|
+
expr = expr.xreplace(
|
|
195
|
+
{
|
|
196
|
+
sp.Symbol(source): sp.Symbol(target)
|
|
197
|
+
for source, target in variable_map.items()
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
variables = mapped_variables
|
|
201
|
+
transpilation_warnings = []
|
|
202
|
+
for variable in variables:
|
|
203
|
+
_validate_python_identifier(variable, "Generated argument name")
|
|
204
|
+
if backend in {"numpy", "torch", "jax"} and expr.has(sp.Piecewise):
|
|
205
|
+
warning_message = (
|
|
206
|
+
"Array-backend piecewise expressions use eager elementwise selection; "
|
|
207
|
+
"all branch expressions may be evaluated, including branches not selected "
|
|
208
|
+
"for an element. Keep every branch valid over the full input domain."
|
|
209
|
+
)
|
|
210
|
+
transpilation_warnings.append(warning_message)
|
|
211
|
+
warnings.warn(
|
|
212
|
+
warning_message,
|
|
213
|
+
PiecewiseEvaluationWarning,
|
|
214
|
+
stacklevel=warning_stacklevel,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# 3. Build argument signature
|
|
218
|
+
argument_type = backend_spec.argument_type
|
|
219
|
+
return_type = (
|
|
220
|
+
backend_spec.matrix_return_type
|
|
221
|
+
if is_matrix
|
|
222
|
+
else backend_spec.scalar_return_type
|
|
223
|
+
)
|
|
224
|
+
if type_hints:
|
|
225
|
+
args_str = ", ".join([f"{var}: {argument_type}" for var in variables])
|
|
226
|
+
return_hint = f" -> {return_type}"
|
|
227
|
+
else:
|
|
228
|
+
args_str = ", ".join(variables)
|
|
229
|
+
return_hint = ""
|
|
230
|
+
|
|
231
|
+
# 4. Generate Python code representation
|
|
232
|
+
printer: _PythonPrinter | _FrameworkPrinter | None = None
|
|
233
|
+
try:
|
|
234
|
+
if is_matrix and backend in {"torch", "jax"}:
|
|
235
|
+
printer = _FrameworkPrinter(backend)
|
|
236
|
+
matrix_rows = [
|
|
237
|
+
"[" + ", ".join(printer.doprint(value) for value in row) + "]"
|
|
238
|
+
for row in expr.tolist()
|
|
239
|
+
]
|
|
240
|
+
if backend == "torch" and variables:
|
|
241
|
+
like = variables[0]
|
|
242
|
+
rows_with_tensors = [
|
|
243
|
+
"["
|
|
244
|
+
+ ", ".join(
|
|
245
|
+
f"({value} if torch.is_tensor({value}) else "
|
|
246
|
+
f"torch.as_tensor({value}, dtype={like}.dtype, device={like}.device))"
|
|
247
|
+
for value in row
|
|
248
|
+
)
|
|
249
|
+
+ "]"
|
|
250
|
+
for row in expr.tolist()
|
|
251
|
+
]
|
|
252
|
+
python_expr_code = (
|
|
253
|
+
"torch.stack([torch.stack(row) for row in ["
|
|
254
|
+
+ ", ".join(rows_with_tensors)
|
|
255
|
+
+ "]])"
|
|
256
|
+
)
|
|
257
|
+
elif backend == "torch":
|
|
258
|
+
python_expr_code = f"torch.tensor([{', '.join(matrix_rows)}])"
|
|
259
|
+
else:
|
|
260
|
+
python_expr_code = f"jnp.array([{', '.join(matrix_rows)}])"
|
|
261
|
+
elif is_matrix:
|
|
262
|
+
matrix_list = expr.tolist()
|
|
263
|
+
module = backend_spec.module_alias or "np"
|
|
264
|
+
python_expr_code = f"{module}.array({matrix_list})"
|
|
265
|
+
elif backend == "numpy":
|
|
266
|
+
printer = _FrameworkPrinter(backend)
|
|
267
|
+
python_expr_code = printer.doprint(expr)
|
|
268
|
+
elif backend in {"torch", "jax"}:
|
|
269
|
+
printer = _FrameworkPrinter(backend)
|
|
270
|
+
python_expr_code = printer.doprint(expr)
|
|
271
|
+
else:
|
|
272
|
+
printer = _PythonPrinter()
|
|
273
|
+
python_expr_code = printer.doprint(expr)
|
|
274
|
+
printer_modules = set(printer.module_imports) if printer is not None else set()
|
|
275
|
+
if backend in {"torch", "jax"} and "math" in printer_modules:
|
|
276
|
+
raise CodeGenerationError(
|
|
277
|
+
f"The {backend} backend has no differentiable native implementation for "
|
|
278
|
+
f"part of this expression: {expr}"
|
|
279
|
+
)
|
|
280
|
+
except Exception as exc:
|
|
281
|
+
if isinstance(exc, CodeGenerationError):
|
|
282
|
+
raise
|
|
283
|
+
raise CodeGenerationError(
|
|
284
|
+
f"Unable to generate Python code for parsed expression: {expr}"
|
|
285
|
+
) from exc
|
|
286
|
+
|
|
287
|
+
# 5. Assemble required imports
|
|
288
|
+
imports_list = list(backend_spec.imports)
|
|
289
|
+
if backend == "python":
|
|
290
|
+
if is_matrix:
|
|
291
|
+
imports_list.append("import numpy as np")
|
|
292
|
+
for module in ("builtins", "math", "functools"):
|
|
293
|
+
if module in printer_modules:
|
|
294
|
+
imports_list.append(f"import {module}")
|
|
295
|
+
|
|
296
|
+
imports = "\n".join(imports_list)
|
|
297
|
+
if imports:
|
|
298
|
+
imports += "\n\n"
|
|
299
|
+
|
|
300
|
+
# 6. Construct and return complete source code
|
|
301
|
+
code = f"{imports}def {function_name}({args_str}){return_hint}:\n"
|
|
302
|
+
code += f" return {python_expr_code}\n"
|
|
303
|
+
|
|
304
|
+
return TranspilationInfo(
|
|
305
|
+
input_expression=latex_str,
|
|
306
|
+
backend=backend,
|
|
307
|
+
parsed_expression=str(expr),
|
|
308
|
+
expression_tree=_expression_tree(expr),
|
|
309
|
+
variables=tuple(variables),
|
|
310
|
+
generated_code=code,
|
|
311
|
+
warnings=tuple(transpilation_warnings),
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def transpile_latex(
|
|
316
|
+
latex_str: str,
|
|
317
|
+
*,
|
|
318
|
+
function_name: str = "formula",
|
|
319
|
+
type_hints: bool = True,
|
|
320
|
+
use_numpy: bool | None = None,
|
|
321
|
+
backend: Backend | None = None,
|
|
322
|
+
variable_map: Mapping[str, str] | None = None,
|
|
323
|
+
args: Sequence[str] | None = None,
|
|
324
|
+
constants: Mapping[str, object] | None = None,
|
|
325
|
+
_warning_stacklevel: int = 3,
|
|
326
|
+
) -> str:
|
|
327
|
+
"""Convert LaTeX into Python source targeting Python math, NumPy, PyTorch, or JAX."""
|
|
328
|
+
return _transpile_latex(
|
|
329
|
+
latex_str,
|
|
330
|
+
function_name=function_name,
|
|
331
|
+
type_hints=type_hints,
|
|
332
|
+
use_numpy=use_numpy,
|
|
333
|
+
backend=backend,
|
|
334
|
+
variable_map=variable_map,
|
|
335
|
+
args=args,
|
|
336
|
+
constants=constants,
|
|
337
|
+
warning_stacklevel=_warning_stacklevel,
|
|
338
|
+
).generated_code
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def inspect_latex(
|
|
342
|
+
latex_str: str,
|
|
343
|
+
*,
|
|
344
|
+
function_name: str = "formula",
|
|
345
|
+
type_hints: bool = True,
|
|
346
|
+
use_numpy: bool | None = None,
|
|
347
|
+
backend: Backend | None = None,
|
|
348
|
+
variable_map: Mapping[str, str] | None = None,
|
|
349
|
+
args: Sequence[str] | None = None,
|
|
350
|
+
constants: Mapping[str, object] | None = None,
|
|
351
|
+
_warning_stacklevel: int = 3,
|
|
352
|
+
) -> TranspilationInfo:
|
|
353
|
+
"""Return the parsed expression, variables, warnings, and generated source."""
|
|
354
|
+
return _transpile_latex(
|
|
355
|
+
latex_str,
|
|
356
|
+
function_name=function_name,
|
|
357
|
+
type_hints=type_hints,
|
|
358
|
+
use_numpy=use_numpy,
|
|
359
|
+
backend=backend,
|
|
360
|
+
variable_map=variable_map,
|
|
361
|
+
args=args,
|
|
362
|
+
constants=constants,
|
|
363
|
+
warning_stacklevel=_warning_stacklevel,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def compile_latex(
|
|
368
|
+
latex_str: str,
|
|
369
|
+
*,
|
|
370
|
+
function_name: str = "formula",
|
|
371
|
+
type_hints: bool = True,
|
|
372
|
+
use_numpy: bool | None = None,
|
|
373
|
+
backend: Backend | None = None,
|
|
374
|
+
variable_map: Mapping[str, str] | None = None,
|
|
375
|
+
args: Sequence[str] | None = None,
|
|
376
|
+
constants: Mapping[str, object] | None = None,
|
|
377
|
+
) -> CompiledFormula:
|
|
378
|
+
"""Compile LaTeX into a callable and retain its generated source and metadata."""
|
|
379
|
+
selected_backend = backend or ("numpy" if use_numpy else "python")
|
|
380
|
+
if selected_backend == "torch" and importlib.util.find_spec("torch") is None:
|
|
381
|
+
raise LaTeXTranspilerError(
|
|
382
|
+
"The 'torch' backend requires the optional dependency 'torch'. "
|
|
383
|
+
"Install it with `pip install latex2code[torch]`."
|
|
384
|
+
)
|
|
385
|
+
if selected_backend == "jax" and importlib.util.find_spec("jax") is None:
|
|
386
|
+
raise LaTeXTranspilerError(
|
|
387
|
+
"The 'jax' backend requires the optional dependency 'jax'. "
|
|
388
|
+
"Install it with `pip install latex2code[jax]`."
|
|
389
|
+
)
|
|
390
|
+
info = inspect_latex(
|
|
391
|
+
latex_str,
|
|
392
|
+
function_name=function_name,
|
|
393
|
+
type_hints=type_hints,
|
|
394
|
+
use_numpy=use_numpy,
|
|
395
|
+
backend=backend,
|
|
396
|
+
variable_map=variable_map,
|
|
397
|
+
args=args,
|
|
398
|
+
constants=constants,
|
|
399
|
+
_warning_stacklevel=4,
|
|
400
|
+
)
|
|
401
|
+
namespace: dict[str, object] = {}
|
|
402
|
+
exec(info.generated_code, namespace)
|
|
403
|
+
function = namespace[function_name]
|
|
404
|
+
if not callable(function):
|
|
405
|
+
raise CodeGenerationError(
|
|
406
|
+
f"Generated object {function_name!r} is not callable."
|
|
407
|
+
)
|
|
408
|
+
return CompiledFormula(info=info, function=function)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def latex_to_code(
|
|
412
|
+
latex_str: str,
|
|
413
|
+
*,
|
|
414
|
+
custom_symbol_map: dict[str, str] | None = None,
|
|
415
|
+
function_name: str = "formula",
|
|
416
|
+
type_hints: bool = True,
|
|
417
|
+
use_numpy: bool | None = None,
|
|
418
|
+
backend: Backend | None = None,
|
|
419
|
+
variable_map: Mapping[str, str] | None = None,
|
|
420
|
+
args: Sequence[str] | None = None,
|
|
421
|
+
constants: Mapping[str, object] | None = None,
|
|
422
|
+
) -> str:
|
|
423
|
+
"""Convert LaTeX into executable Python code using the main transpile pipeline.
|
|
424
|
+
|
|
425
|
+
This helper preserves the historical `custom_symbol_map` behavior for macro
|
|
426
|
+
overrides while also accepting the same generation options as
|
|
427
|
+
:func:`transpile_latex`. Values are remapped via the standard variable map
|
|
428
|
+
after the LaTeX parser has interpreted the expression.
|
|
429
|
+
"""
|
|
430
|
+
merged_variable_map = dict(variable_map) if variable_map is not None else {}
|
|
431
|
+
|
|
432
|
+
if custom_symbol_map is not None:
|
|
433
|
+
warnings.warn(
|
|
434
|
+
"custom_symbol_map is deprecated; use variable_map instead.",
|
|
435
|
+
DeprecationWarning,
|
|
436
|
+
stacklevel=2,
|
|
437
|
+
)
|
|
438
|
+
for macro, python_var in custom_symbol_map.items():
|
|
439
|
+
if not isinstance(macro, str) or not macro.startswith("\\"):
|
|
440
|
+
raise LaTeXTranspilerError(
|
|
441
|
+
"custom_symbol_map keys must be LaTeX command strings like '\\lambda'."
|
|
442
|
+
)
|
|
443
|
+
symbol_name = macro.lstrip("\\")
|
|
444
|
+
if not re.fullmatch(r"[A-Za-z]+", symbol_name):
|
|
445
|
+
raise LaTeXTranspilerError(
|
|
446
|
+
"custom_symbol_map supports plain LaTeX symbol commands such as '\\lambda'; "
|
|
447
|
+
f"received {macro!r}."
|
|
448
|
+
)
|
|
449
|
+
merged_variable_map[symbol_name] = python_var
|
|
450
|
+
|
|
451
|
+
return transpile_latex(
|
|
452
|
+
latex_str,
|
|
453
|
+
function_name=function_name,
|
|
454
|
+
type_hints=type_hints,
|
|
455
|
+
use_numpy=use_numpy,
|
|
456
|
+
backend=backend,
|
|
457
|
+
variable_map=merged_variable_map or None,
|
|
458
|
+
args=args,
|
|
459
|
+
constants=constants,
|
|
460
|
+
_warning_stacklevel=4,
|
|
461
|
+
)
|
latex2code/backends.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Literal, Mapping
|
|
3
|
+
|
|
4
|
+
Backend = Literal["python", "numpy", "torch", "jax"]
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class BackendSpec:
|
|
9
|
+
argument_type: str
|
|
10
|
+
scalar_return_type: str
|
|
11
|
+
matrix_return_type: str
|
|
12
|
+
module_alias: str | None
|
|
13
|
+
imports: tuple[str, ...]
|
|
14
|
+
function_aliases: Mapping[str, str]
|
|
15
|
+
special_functions: Mapping[str, str]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_FUNCTION_ALIASES = {
|
|
19
|
+
"sin": "sin",
|
|
20
|
+
"cos": "cos",
|
|
21
|
+
"tan": "tan",
|
|
22
|
+
"cot": "cot",
|
|
23
|
+
"sec": "sec",
|
|
24
|
+
"csc": "csc",
|
|
25
|
+
"sinh": "sinh",
|
|
26
|
+
"cosh": "cosh",
|
|
27
|
+
"tanh": "tanh",
|
|
28
|
+
"coth": "coth",
|
|
29
|
+
"sech": "sech",
|
|
30
|
+
"csch": "csch",
|
|
31
|
+
"acos": "arccos",
|
|
32
|
+
"acosh": "arccosh",
|
|
33
|
+
"asin": "arcsin",
|
|
34
|
+
"asinh": "arcsinh",
|
|
35
|
+
"atan": "arctan",
|
|
36
|
+
"atanh": "arctanh",
|
|
37
|
+
"exp": "exp",
|
|
38
|
+
"log": "log",
|
|
39
|
+
"ceiling": "ceil",
|
|
40
|
+
"floor": "floor",
|
|
41
|
+
"sqrt": "sqrt",
|
|
42
|
+
"ln": "log",
|
|
43
|
+
"Sqrt": "sqrt",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
BACKENDS: dict[Backend, BackendSpec] = {
|
|
48
|
+
"python": BackendSpec("float", "float", "np.ndarray", None, (), {}, {}),
|
|
49
|
+
"numpy": BackendSpec(
|
|
50
|
+
"float | np.ndarray",
|
|
51
|
+
"float | np.ndarray",
|
|
52
|
+
"np.ndarray",
|
|
53
|
+
"np",
|
|
54
|
+
("import numpy as np", "import math"),
|
|
55
|
+
_FUNCTION_ALIASES,
|
|
56
|
+
{
|
|
57
|
+
"erf": "np.vectorize(math.erf)",
|
|
58
|
+
"erfc": "np.vectorize(math.erfc)",
|
|
59
|
+
"gamma": "np.vectorize(math.gamma)",
|
|
60
|
+
},
|
|
61
|
+
),
|
|
62
|
+
"torch": BackendSpec(
|
|
63
|
+
"torch.Tensor",
|
|
64
|
+
"torch.Tensor",
|
|
65
|
+
"torch.Tensor",
|
|
66
|
+
"torch",
|
|
67
|
+
("import torch",),
|
|
68
|
+
_FUNCTION_ALIASES,
|
|
69
|
+
{
|
|
70
|
+
"erf": "torch.erf",
|
|
71
|
+
"erfc": "torch.special.erfc",
|
|
72
|
+
"gamma": "torch.special.gamma",
|
|
73
|
+
},
|
|
74
|
+
),
|
|
75
|
+
"jax": BackendSpec(
|
|
76
|
+
"jax.Array",
|
|
77
|
+
"jax.Array",
|
|
78
|
+
"jax.Array",
|
|
79
|
+
"jnp",
|
|
80
|
+
("import jax", "import jax.numpy as jnp", "import jax.scipy.special"),
|
|
81
|
+
_FUNCTION_ALIASES,
|
|
82
|
+
{
|
|
83
|
+
"erf": "jax.scipy.special.erf",
|
|
84
|
+
"erfc": "jax.scipy.special.erfc",
|
|
85
|
+
"gamma": "jax.scipy.special.gamma",
|
|
86
|
+
},
|
|
87
|
+
),
|
|
88
|
+
}
|