keecas 0.1.1__tar.gz
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.
- keecas-0.1.1/PKG-INFO +20 -0
- keecas-0.1.1/README.md +0 -0
- keecas-0.1.1/keecas/__init__.py +52 -0
- keecas-0.1.1/keecas/dataframe.py +271 -0
- keecas-0.1.1/keecas/display.py +435 -0
- keecas-0.1.1/keecas/pint_sympy.py +95 -0
- keecas-0.1.1/keecas/pipe_command.py +196 -0
- keecas-0.1.1/keecas/utils.py +96 -0
- keecas-0.1.1/pyproject.toml +22 -0
keecas-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: keecas
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A set of tools for symbolic math computation done in a jupyter notebook, aimed specifically for Quarto. It is based mostly on sympy.
|
|
5
|
+
Author: kompre
|
|
6
|
+
Author-email: s.follador@gmail.com
|
|
7
|
+
Requires-Python: >=3.12,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Requires-Dist: flatten-dict (>=0.4.2,<0.5.0)
|
|
11
|
+
Requires-Dist: ipython (>=8.26.0,<9.0.0)
|
|
12
|
+
Requires-Dist: pint (>=0.24.3,<0.25.0)
|
|
13
|
+
Requires-Dist: pipe (>=2.2,<3.0)
|
|
14
|
+
Requires-Dist: pyyaml (>=6.0.1,<7.0.0)
|
|
15
|
+
Requires-Dist: regex (>=2024.7.24,<2025.0.0)
|
|
16
|
+
Requires-Dist: ruamel-yaml (>=0.18.6,<0.19.0)
|
|
17
|
+
Requires-Dist: sympy (>=1.13.1,<2.0.0)
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
|
keecas-0.1.1/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# dataframe
|
|
2
|
+
from .dataframe import Dataframe
|
|
3
|
+
|
|
4
|
+
# display
|
|
5
|
+
from .display import (
|
|
6
|
+
options,
|
|
7
|
+
show_eqn,
|
|
8
|
+
verifica,
|
|
9
|
+
dict_to_eq,
|
|
10
|
+
eq_to_dict,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
# pipe_command
|
|
14
|
+
from . import pipe_command as pc
|
|
15
|
+
|
|
16
|
+
# initialize pint
|
|
17
|
+
from .pint_sympy import unitregistry as u
|
|
18
|
+
|
|
19
|
+
u.formatter.default_format = ".2f~P"
|
|
20
|
+
|
|
21
|
+
# initialize sympy
|
|
22
|
+
import sympy as sp
|
|
23
|
+
|
|
24
|
+
from sympy import latex, Eq, Le, symbols, Basic, Dict, S, ImmutableDenseMatrix as Matrix
|
|
25
|
+
|
|
26
|
+
## latex printing settings
|
|
27
|
+
sp.init_printing(mul_symbol=options.default_mul_symbol, order="none")
|
|
28
|
+
platex = lambda x: latex(x, mode="inline", mul_symbol=options.default_mul_symbol)
|
|
29
|
+
|
|
30
|
+
## common sympy functions
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"Dataframe",
|
|
35
|
+
"show_eqn",
|
|
36
|
+
"options",
|
|
37
|
+
"verifica",
|
|
38
|
+
"dict_to_eq",
|
|
39
|
+
"eq_to_dict",
|
|
40
|
+
"pc",
|
|
41
|
+
"u",
|
|
42
|
+
"sp",
|
|
43
|
+
"latex",
|
|
44
|
+
"Eq",
|
|
45
|
+
"Le",
|
|
46
|
+
"symbols",
|
|
47
|
+
"Basic",
|
|
48
|
+
"Dict",
|
|
49
|
+
"S",
|
|
50
|
+
"Matrix",
|
|
51
|
+
"platex",
|
|
52
|
+
]
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
|
|
3
|
+
from itertools import chain
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Dataframe(dict):
|
|
7
|
+
def __init__(self, *args, filler=None, **kwargs):
|
|
8
|
+
super().__init__()
|
|
9
|
+
self._width = 0
|
|
10
|
+
self._filler = filler
|
|
11
|
+
|
|
12
|
+
if (
|
|
13
|
+
args
|
|
14
|
+
and isinstance(args[0], list)
|
|
15
|
+
and all(isinstance(item, dict) for item in args[0])
|
|
16
|
+
):
|
|
17
|
+
self._init_from_list_of_dicts(args[0])
|
|
18
|
+
else:
|
|
19
|
+
self._update_initial(*args, **kwargs)
|
|
20
|
+
|
|
21
|
+
def _init_from_list_of_dicts(self, list_of_dicts):
|
|
22
|
+
if not list_of_dicts:
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
keys = list(
|
|
26
|
+
list_of_dicts[0].keys()
|
|
27
|
+
) # the keys are determined by the first dict (order is important!)
|
|
28
|
+
for key in keys:
|
|
29
|
+
self[key] = [d.get(key, self._filler) for d in list_of_dicts]
|
|
30
|
+
|
|
31
|
+
self._width = len(list_of_dicts)
|
|
32
|
+
|
|
33
|
+
def _update_initial(self, *args, **kwargs):
|
|
34
|
+
if args:
|
|
35
|
+
if len(args) > 1:
|
|
36
|
+
raise TypeError(
|
|
37
|
+
"update expected at most 1 arguments, got %d" % len(args)
|
|
38
|
+
)
|
|
39
|
+
other = dict(args[0])
|
|
40
|
+
other.update(kwargs)
|
|
41
|
+
else:
|
|
42
|
+
other = kwargs
|
|
43
|
+
|
|
44
|
+
for key, value in other.items():
|
|
45
|
+
if isinstance(value, list):
|
|
46
|
+
self[key] = value
|
|
47
|
+
else:
|
|
48
|
+
self[key] = [value]
|
|
49
|
+
|
|
50
|
+
self._width = max(len(value) for value in self.values()) if self else 0
|
|
51
|
+
self._validate_and_fill_data()
|
|
52
|
+
|
|
53
|
+
def _validate_and_fill_data(self):
|
|
54
|
+
if not self:
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
for key, value in self.items():
|
|
58
|
+
if len(value) < self._width:
|
|
59
|
+
self[key] = value + [self._filler] * (self._width - len(value))
|
|
60
|
+
|
|
61
|
+
def update(self, *args, **kwargs):
|
|
62
|
+
if args:
|
|
63
|
+
if len(args) > 1:
|
|
64
|
+
raise TypeError(
|
|
65
|
+
"update expected at most 1 arguments, got %d" % len(args)
|
|
66
|
+
)
|
|
67
|
+
other = dict(args[0])
|
|
68
|
+
other.update(kwargs)
|
|
69
|
+
else:
|
|
70
|
+
other = kwargs
|
|
71
|
+
|
|
72
|
+
# Convert all values to lists if they aren't already
|
|
73
|
+
for key, value in other.items():
|
|
74
|
+
if not isinstance(value, list):
|
|
75
|
+
other[key] = [value]
|
|
76
|
+
|
|
77
|
+
# Find the maximum length of any value in both self and other
|
|
78
|
+
max_length = max(
|
|
79
|
+
[len(value) for value in self.values()]
|
|
80
|
+
+ [len(value) for value in other.values()]
|
|
81
|
+
+ [self._width]
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Update existing keys and add new ones
|
|
85
|
+
for key, value in other.items():
|
|
86
|
+
self[key] = value + [self._filler] * (max_length - len(value))
|
|
87
|
+
|
|
88
|
+
# Adjust existing keys that weren't in the update data
|
|
89
|
+
for key in self:
|
|
90
|
+
if key not in other:
|
|
91
|
+
if len(self[key]) < max_length:
|
|
92
|
+
self[key] = self[key] + [self._filler] * (
|
|
93
|
+
max_length - len(self[key])
|
|
94
|
+
)
|
|
95
|
+
else:
|
|
96
|
+
self[key] = self[key][:max_length]
|
|
97
|
+
|
|
98
|
+
# Update width
|
|
99
|
+
self._width = max_length
|
|
100
|
+
|
|
101
|
+
def append(self, other, strict=True):
|
|
102
|
+
if isinstance(other, Dataframe):
|
|
103
|
+
if strict:
|
|
104
|
+
other = {key: other[key] for key in self.keys() if key in other}
|
|
105
|
+
|
|
106
|
+
for key in self.keys():
|
|
107
|
+
self[key].append(
|
|
108
|
+
other[key][0]
|
|
109
|
+
if key in other and len(other[key]) > 0
|
|
110
|
+
else self._filler
|
|
111
|
+
)
|
|
112
|
+
elif isinstance(other, dict):
|
|
113
|
+
if strict:
|
|
114
|
+
other = {key: other[key] for key in self.keys() if key in other}
|
|
115
|
+
|
|
116
|
+
for key in self.keys():
|
|
117
|
+
self[key].append(other[key] if key in other else self._filler)
|
|
118
|
+
else:
|
|
119
|
+
for key in self.keys():
|
|
120
|
+
self[key].append(other)
|
|
121
|
+
|
|
122
|
+
self._width += 1
|
|
123
|
+
|
|
124
|
+
def extend(self, other, strict=True):
|
|
125
|
+
if isinstance(other, Dataframe):
|
|
126
|
+
# filter keys
|
|
127
|
+
if strict:
|
|
128
|
+
other = Dataframe(
|
|
129
|
+
{key: other[key] for key in self.keys() if key in other}
|
|
130
|
+
)
|
|
131
|
+
if not other:
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
other_width = other.width
|
|
135
|
+
|
|
136
|
+
extra_keys = [k for k in other.keys() if k not in self.keys()]
|
|
137
|
+
|
|
138
|
+
for key in chain(self.keys(), extra_keys):
|
|
139
|
+
match (key in self, key in other):
|
|
140
|
+
case (True, True):
|
|
141
|
+
self[key].extend(
|
|
142
|
+
other[key]
|
|
143
|
+
+ [self._filler] * (other_width - len(other[key]))
|
|
144
|
+
)
|
|
145
|
+
case (True, False):
|
|
146
|
+
self[key].extend([self._filler] * other_width)
|
|
147
|
+
case (False, True):
|
|
148
|
+
self[key] = (
|
|
149
|
+
[self._filler] * self._width
|
|
150
|
+
+ other[key]
|
|
151
|
+
+ [self._filler] * (other_width - len(other[key]))
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
self._width += other_width
|
|
155
|
+
elif isinstance(other, dict):
|
|
156
|
+
# filter keys
|
|
157
|
+
if strict:
|
|
158
|
+
other = {key: other[key] for key in self.keys() if key in other}
|
|
159
|
+
if not other:
|
|
160
|
+
return
|
|
161
|
+
self.extend(Dataframe(other), strict=strict)
|
|
162
|
+
|
|
163
|
+
# max_len = max(len(v) if isinstance(v, list) else 1 for v in other.values())
|
|
164
|
+
|
|
165
|
+
# for key in self.keys():
|
|
166
|
+
# if key in other:
|
|
167
|
+
# v = other[key]
|
|
168
|
+
# if isinstance(v, list):
|
|
169
|
+
# self[key].extend(v + [self._filler] * (max_len - len(v)))
|
|
170
|
+
# else:
|
|
171
|
+
# self[key].extend([v] * max_len)
|
|
172
|
+
# else:
|
|
173
|
+
# self[key].extend([self._filler] * max_len)
|
|
174
|
+
|
|
175
|
+
# self._width += max_len
|
|
176
|
+
elif isinstance(other, list):
|
|
177
|
+
for key in self.keys():
|
|
178
|
+
self[key].extend(other)
|
|
179
|
+
|
|
180
|
+
self._width += len(other)
|
|
181
|
+
else:
|
|
182
|
+
raise ValueError(
|
|
183
|
+
"Cannot extend Dataframe with this type. Use 'append' for single values."
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def __add__(self, other):
|
|
187
|
+
# if not isinstance(other, Dataframe):
|
|
188
|
+
# raise ValueError("Can only add Dataframe to Dataframe")
|
|
189
|
+
result = copy.deepcopy(Dataframe(self))
|
|
190
|
+
result.extend(other, strict=False)
|
|
191
|
+
return result
|
|
192
|
+
|
|
193
|
+
def __or__(self, other):
|
|
194
|
+
# if not isinstance(other, Dataframe):
|
|
195
|
+
# raise ValueError("Can only perform '|' operation with Dataframe")
|
|
196
|
+
result = copy.deepcopy(Dataframe(self))
|
|
197
|
+
result.update(other)
|
|
198
|
+
return result
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def width(self):
|
|
202
|
+
return self._width
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def length(self):
|
|
206
|
+
return len(self)
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def shape(self):
|
|
210
|
+
return (self.length, self.width)
|
|
211
|
+
|
|
212
|
+
def __repr__(self):
|
|
213
|
+
return f"Dataframe({self.dict_repr()}, shape={self.shape})"
|
|
214
|
+
|
|
215
|
+
def dict_repr(self):
|
|
216
|
+
return super().__repr__()
|
|
217
|
+
|
|
218
|
+
def print_dict(self):
|
|
219
|
+
print(self.dict_repr())
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
from typing import List, Union, Dict
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def create_dataframe(
|
|
226
|
+
keys: List[str],
|
|
227
|
+
width: int,
|
|
228
|
+
seed: Union[any, List, Dict, "Dataframe"] = None,
|
|
229
|
+
default_value: any = None,
|
|
230
|
+
) -> Dataframe:
|
|
231
|
+
|
|
232
|
+
df = Dataframe()
|
|
233
|
+
|
|
234
|
+
if not isinstance(seed, (list, dict, Dataframe)):
|
|
235
|
+
# Single value seed (can be of any type)
|
|
236
|
+
for key in keys:
|
|
237
|
+
df[key] = [seed] * width
|
|
238
|
+
|
|
239
|
+
elif isinstance(seed, list):
|
|
240
|
+
# List seed (applies to all rows)
|
|
241
|
+
seed_list = seed[:width] + [default_value] * (width - len(seed))
|
|
242
|
+
for key in keys:
|
|
243
|
+
df[key] = seed_list.copy()
|
|
244
|
+
|
|
245
|
+
elif isinstance(seed, Dataframe):
|
|
246
|
+
for key in keys:
|
|
247
|
+
if key in seed:
|
|
248
|
+
df[key] = seed[key][:width] + [default_value] * (width - len(seed[key]))
|
|
249
|
+
else:
|
|
250
|
+
df[key] = [default_value] * width
|
|
251
|
+
|
|
252
|
+
elif isinstance(seed, dict):
|
|
253
|
+
for key in keys:
|
|
254
|
+
if key in seed:
|
|
255
|
+
if isinstance(seed[key], list):
|
|
256
|
+
# List value for this row
|
|
257
|
+
df[key] = seed[key][:width] + [default_value] * (
|
|
258
|
+
width - len(seed[key])
|
|
259
|
+
)
|
|
260
|
+
else:
|
|
261
|
+
# Single value for this row
|
|
262
|
+
df[key] = [seed[key]] * width
|
|
263
|
+
else:
|
|
264
|
+
df[key] = [default_value] * width
|
|
265
|
+
|
|
266
|
+
# Fill any missing rows with default_value
|
|
267
|
+
for key in keys:
|
|
268
|
+
if key not in df:
|
|
269
|
+
df[key] = [default_value] * width
|
|
270
|
+
|
|
271
|
+
return df
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
# %%
|
|
2
|
+
from warnings import warn
|
|
3
|
+
from sympy import (
|
|
4
|
+
latex,
|
|
5
|
+
Eq,
|
|
6
|
+
Le,
|
|
7
|
+
symbols,
|
|
8
|
+
Basic,
|
|
9
|
+
FunctionClass,
|
|
10
|
+
Dict,
|
|
11
|
+
S,
|
|
12
|
+
)
|
|
13
|
+
from IPython.display import Markdown, display
|
|
14
|
+
import re
|
|
15
|
+
|
|
16
|
+
from typing import Union, List, Dict
|
|
17
|
+
|
|
18
|
+
from .dataframe import *
|
|
19
|
+
|
|
20
|
+
# DEFINITION OF DEFAULT VALUES
|
|
21
|
+
|
|
22
|
+
# default values for labels
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class options:
|
|
28
|
+
EQ_PREFIX: str = "eq-"
|
|
29
|
+
EQ_SUFFIX: str = ""
|
|
30
|
+
VERTICAL_SKIP: str = "8pt"
|
|
31
|
+
PRINT_LABEL: bool = False
|
|
32
|
+
DEBUG = False
|
|
33
|
+
katex = False
|
|
34
|
+
default_mul_symbol = r"\,"
|
|
35
|
+
default_environment = "align"
|
|
36
|
+
default_label_command = r"\label"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
from itertools import chain, zip_longest
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# determina esito verifica
|
|
43
|
+
def verifica(lhs, rhs, test=Le) -> Markdown:
|
|
44
|
+
"""Determines if the left-hand side (lhs) is less than or equal to
|
|
45
|
+
the right-hand side (rhs) based on the provided test function.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
lhs (sympy.Expr): The left-hand side expression.
|
|
49
|
+
rhs (sympy.Expr): The right-hand side expression.
|
|
50
|
+
test (sympy.GreaterThan, sympy.LessThan, sympy.GreaterThanEqual, sympy.LessThanEqual, optional):
|
|
51
|
+
The test function to apply. Defaults to Le (less than or equal to).
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Markdown: A Markdown object containing the formatted string indicating the verification result (green for success, red for failure).
|
|
55
|
+
"""
|
|
56
|
+
match test.__name__:
|
|
57
|
+
case "LessThan":
|
|
58
|
+
symbol_if_true = r"\le"
|
|
59
|
+
symbol_if_false = r">"
|
|
60
|
+
case "StrictLessThan":
|
|
61
|
+
symbol_if_true = r"<"
|
|
62
|
+
symbol_if_false = r"\ge"
|
|
63
|
+
case "GreaterThan":
|
|
64
|
+
symbol_if_true = r"\ge"
|
|
65
|
+
symbol_if_false = r"<"
|
|
66
|
+
case "StrictGreaterThan":
|
|
67
|
+
symbol_if_true = r">"
|
|
68
|
+
symbol_if_false = r"\le"
|
|
69
|
+
|
|
70
|
+
if test(lhs, rhs):
|
|
71
|
+
return Markdown(
|
|
72
|
+
rf"\textcolor{{green}}{{\left[{symbol_if_true}{rhs}\quad \textbf{{VERIFICATO}}\right]}}"
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
return Markdown(
|
|
76
|
+
rf"\textcolor{{red}}{{\left[{symbol_if_false}{rhs}\quad \textbf{{NON VERIFICATO}}\right]}}"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def show_eqn(
|
|
81
|
+
eqns: dict | list[dict] | Dataframe,
|
|
82
|
+
environment: str = None,
|
|
83
|
+
sep: str | list[str] = "&",
|
|
84
|
+
label: str | dict = None,
|
|
85
|
+
label_command: str = None,
|
|
86
|
+
col_wrap: list[None | tuple] = None,
|
|
87
|
+
float_format: str = None,
|
|
88
|
+
debug: bool = None,
|
|
89
|
+
**kwargs,
|
|
90
|
+
) -> Markdown:
|
|
91
|
+
"""
|
|
92
|
+
Generates a LaTeX equation or equation array based on the provided equations.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
eqns (dict | list[dict] | Dataframe): The equations to be displayed. It can be a dictionary, a list of dictionaries, or a Dataframe object.
|
|
96
|
+
environment (str, optional): The LaTeX environment to use for displaying the equations. Defaults to options.default_environment.
|
|
97
|
+
sep (str | list[str], optional): The separator to use between the key and value in each equation. It can be a string or a list of strings. Defaults to "&" or "" for specific environments (e.g. equation, gather).
|
|
98
|
+
label (str | dict, optional): The label to attach to the equation. It can be a string or a dictionary. Defaults to None.
|
|
99
|
+
label_command (str, optional): The LaTeX command to use for attaching the label. Defaults to options.default_label_command.
|
|
100
|
+
col_wrap (list[None | tuple], optional): The column wrapping specification for the Dataframe. Defaults to [None, ('=', '')].
|
|
101
|
+
float_format (str, optional): The float format specification for the Dataframe. Defaults to None.
|
|
102
|
+
debug (bool, optional): Whether to enable debug mode. Defaults to options.DEBUG.
|
|
103
|
+
**kwargs: Additional keyword arguments to be passed to the `myprint_latex` function.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Markdown: The LaTeX equation or equation array displayed as a Markdown object.
|
|
107
|
+
|
|
108
|
+
Notes:
|
|
109
|
+
- If `debug` is True, the generated LaTeX code will be printed.
|
|
110
|
+
- If `environment` is not provided, the default environment specified in `options.default_environment` will be used.
|
|
111
|
+
- If `col_wrap` is not provided, the default column wrapping specification will be used.
|
|
112
|
+
- If `float_format` is not provided, the default float format specification will be used.
|
|
113
|
+
- If `label` is not provided, a label will not be attached to the equation.
|
|
114
|
+
- If `label_command` is not provided, the default label command specified in `options.default_label_command` will be used.
|
|
115
|
+
- The `eqns` argument can be a dictionary, a list of dictionaries, or a Dataframe object.
|
|
116
|
+
- The `sep` argument can be a string or a list of strings.
|
|
117
|
+
- The `label` argument can be a string or a dictionary.
|
|
118
|
+
- The `label_command` argument can be a string.
|
|
119
|
+
- The `col_wrap` argument can be a list of None or tuples.
|
|
120
|
+
- The `float_format` argument can be a string.
|
|
121
|
+
- The `debug` argument can be a boolean.
|
|
122
|
+
- The `**kwargs` argument can be any additional keyword arguments to be passed to the `myprint_latex` function.
|
|
123
|
+
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
# set defualt values
|
|
127
|
+
if not debug:
|
|
128
|
+
debug = options.DEBUG
|
|
129
|
+
|
|
130
|
+
if not "mul_symbol" in kwargs:
|
|
131
|
+
kwargs["mul_symbol"] = options.default_mul_symbol
|
|
132
|
+
|
|
133
|
+
if not environment:
|
|
134
|
+
environment = options.default_environment
|
|
135
|
+
|
|
136
|
+
if not col_wrap:
|
|
137
|
+
col_wrap = [
|
|
138
|
+
None,
|
|
139
|
+
("=", ""),
|
|
140
|
+
] # no wrapping for the key element (first columns), then '=' sign for the second column
|
|
141
|
+
|
|
142
|
+
# warning message in case of too many labels provided
|
|
143
|
+
single_label_env = ["equation", "cases", "split"]
|
|
144
|
+
if environment.replace("*", "") in single_label_env and isinstance(label, dict):
|
|
145
|
+
warn(
|
|
146
|
+
f"ATTENTION! label is a dict, while the {environment} does not support multiple labels"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# handle edge case for 'equation' and 'gather' environment
|
|
150
|
+
if environment.replace("*", "") in ["equation", "gather"]:
|
|
151
|
+
sep = "" # no separator in environment
|
|
152
|
+
|
|
153
|
+
# convert sep to a list: str-> list[str]
|
|
154
|
+
if not isinstance(sep, list):
|
|
155
|
+
sep = [sep]
|
|
156
|
+
|
|
157
|
+
# convert eqns to a Dataframe
|
|
158
|
+
if not isinstance(eqns, Dataframe):
|
|
159
|
+
if isinstance(eqns, list):
|
|
160
|
+
eqns = Dataframe(eqns)
|
|
161
|
+
else:
|
|
162
|
+
eqns = Dataframe([eqns])
|
|
163
|
+
|
|
164
|
+
# adjust sep to the size of the list of eqns(e.g. 'key & val0 & val1' ); assume last value of sep as filler
|
|
165
|
+
sep += [sep[-1]] * (eqns.width - len(sep))
|
|
166
|
+
|
|
167
|
+
# extract keys from first dict
|
|
168
|
+
keys = eqns.keys()
|
|
169
|
+
# determine the number of columns (keys & value0 & value1 ...)
|
|
170
|
+
num_cols = eqns.width + 1
|
|
171
|
+
|
|
172
|
+
# generate the matrix (list[list]]) of keys, many values (first element is the key)
|
|
173
|
+
matrix = {k: [k] + [vv for vv in v] for k, v in eqns.items()}
|
|
174
|
+
# print(f'{matrix=}')
|
|
175
|
+
|
|
176
|
+
# create float_format (dict)
|
|
177
|
+
if isinstance(float_format, tuple):
|
|
178
|
+
float_format = create_dataframe(seed=float_format[0], default_value=float_format[1], keys=keys, width=num_cols)
|
|
179
|
+
else:
|
|
180
|
+
float_format = create_dataframe(seed=float_format, keys=keys, width=num_cols)
|
|
181
|
+
# print(f'{float_format=}')
|
|
182
|
+
|
|
183
|
+
### col_wrap
|
|
184
|
+
# adjust size of the col_wrap; assume None as default (for compatibility with earlier versions)
|
|
185
|
+
col_wrap = create_dataframe(seed=col_wrap, keys=keys, width=num_cols)
|
|
186
|
+
|
|
187
|
+
for k, v in col_wrap.items():
|
|
188
|
+
# clean the none value in wrapper with tuple
|
|
189
|
+
col_wrap[k] = [cw if cw is not None else ("", "") for cw in v]
|
|
190
|
+
# substitute single value with tuple, assuming last item is ''
|
|
191
|
+
col_wrap[k] = [cw if isinstance(cw, tuple) else (cw, "") for cw in col_wrap[k]]
|
|
192
|
+
|
|
193
|
+
# generate label dict if none is passed
|
|
194
|
+
if not label:
|
|
195
|
+
label = {k: None for k in keys}
|
|
196
|
+
|
|
197
|
+
# define label command
|
|
198
|
+
if not label_command:
|
|
199
|
+
label_command = options.default_label_command
|
|
200
|
+
|
|
201
|
+
def attach_label(key):
|
|
202
|
+
"""
|
|
203
|
+
Attaches a label to a given key.
|
|
204
|
+
|
|
205
|
+
Parameters:
|
|
206
|
+
key (str): The key to attach the label to.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
str: The label attached to the key. If the label is empty or the katex engine is being used for rendering (i.e. in a Jupyter notebook), an empty string is returned.
|
|
210
|
+
|
|
211
|
+
Notes:
|
|
212
|
+
- The label is constructed using the `options.EQ_PREFIX`, the value of `label[key]`, and `options.EQ_SUFFIX`.
|
|
213
|
+
- If `options.PRINT_LABEL` is True, the key and label are printed.
|
|
214
|
+
- The label is wrapped in a LaTeX command specified by `label_command` if it is not empty and the katex engine is not being used for rendering.
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
if isinstance(label, dict):
|
|
218
|
+
text_label = (
|
|
219
|
+
rf"{options.EQ_PREFIX}{label[key]}{options.EQ_SUFFIX}"
|
|
220
|
+
if label.get(key)
|
|
221
|
+
else ""
|
|
222
|
+
)
|
|
223
|
+
if options.PRINT_LABEL:
|
|
224
|
+
print(f"{key}: {text_label}") if text_label else None
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
rf" {label_command}{{{text_label}}} "
|
|
228
|
+
if label.get(key)
|
|
229
|
+
and not options.katex # don't add the label if there is no label to add, and if katex engine is used for rendering (i.e. jupyter notebook)
|
|
230
|
+
else ""
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
if isinstance(label, str) and not key:
|
|
234
|
+
|
|
235
|
+
text_label = rf"{options.EQ_PREFIX}{label}{options.EQ_SUFFIX}"
|
|
236
|
+
|
|
237
|
+
if options.PRINT_LABEL:
|
|
238
|
+
print(f"label: {text_label}" if text_label else None)
|
|
239
|
+
|
|
240
|
+
return (
|
|
241
|
+
rf" {label_command}{{{text_label}}} "
|
|
242
|
+
if not options.katex # don't add the label if there is no label to add, and if katex engine is used for rendering (i.e. jupyter notebook)
|
|
243
|
+
else ""
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
return ""
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# check if environment is a special (starred "cases*" and "split*" are not valid latex environment, but they need to pass the "*" operator to the "equation" outer environment)
|
|
251
|
+
if environment.replace("*", "") in ["cases", "split"]:
|
|
252
|
+
|
|
253
|
+
# determine if outer env is starred
|
|
254
|
+
env = "align*" if "*" in environment else "align"
|
|
255
|
+
|
|
256
|
+
# clear the cases|split environment from the star
|
|
257
|
+
environment = "aligned"
|
|
258
|
+
|
|
259
|
+
# wrap inner cases|split in outer "align"
|
|
260
|
+
wrap = (
|
|
261
|
+
f"\\begin{{{env}}}{attach_label(None)}\n", # for an equation environment, only one label is allowed
|
|
262
|
+
f"\t\\left\\{{\\begin{{{environment}}}",
|
|
263
|
+
f"\t\n\\end{{{environment}}}\\right.",
|
|
264
|
+
f"\n\\end{{{env}}}",
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
else:
|
|
268
|
+
# do nothing
|
|
269
|
+
wrap = (
|
|
270
|
+
"",
|
|
271
|
+
f"\\begin{{{environment}}}{attach_label(list(keys)[0]) if environment.replace('*', '') in single_label_env else ''}",
|
|
272
|
+
f"\n\\end{{{environment}}}",
|
|
273
|
+
"",
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# definition of the main template
|
|
277
|
+
template = f"{wrap[0]}{wrap[1]}\n___body___{wrap[2]}{wrap[3]}"
|
|
278
|
+
|
|
279
|
+
# generate the rows
|
|
280
|
+
body_lines = {}
|
|
281
|
+
for key, list_values in matrix.items():
|
|
282
|
+
body_lines[key] = " ".join(
|
|
283
|
+
[
|
|
284
|
+
format_decimal_numbers(
|
|
285
|
+
f'{ f"{cw[0]}{myprint_latex(v, **kwargs)}{cw[-1]}" if v is not None else " " } {s}',
|
|
286
|
+
ff,
|
|
287
|
+
)
|
|
288
|
+
for v, s, cw, ff in zip_longest(
|
|
289
|
+
list_values,
|
|
290
|
+
sep,
|
|
291
|
+
col_wrap[key],
|
|
292
|
+
float_format[key],
|
|
293
|
+
fillvalue="",
|
|
294
|
+
)
|
|
295
|
+
]
|
|
296
|
+
) + attach_label(key)
|
|
297
|
+
|
|
298
|
+
# how to join the lines of the body
|
|
299
|
+
join_token = "" if "equation" in environment else " \\\\\n "
|
|
300
|
+
|
|
301
|
+
# generate the body
|
|
302
|
+
body = join_token.join(body_lines.values())
|
|
303
|
+
|
|
304
|
+
# clean the body
|
|
305
|
+
body = replace_all(body, replacement)
|
|
306
|
+
|
|
307
|
+
template = template.replace("___body___", body)
|
|
308
|
+
|
|
309
|
+
if debug:
|
|
310
|
+
print(template)
|
|
311
|
+
|
|
312
|
+
return Markdown(template)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def myprint_latex(expr: Basic | str | Markdown, **kwargs) -> str:
|
|
316
|
+
"""Converts a mathematical expression to a LaTeX string.
|
|
317
|
+
|
|
318
|
+
This function handles different input types and allows for customization of the output format.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
expr (Basic | str | Markdown): The mathematical expression to convert.
|
|
322
|
+
* Basic (SymPy): A SymPy expression object.
|
|
323
|
+
* str: A string representation of a mathematical expression.
|
|
324
|
+
* Markdown: A Markdown object that likely contains LaTeX code (data attribute is extracted).
|
|
325
|
+
**kwargs: Additional keyword arguments passed to the SymPy `latex` function for formatting the output.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
str: The LaTeX string representation of the mathematical expression.
|
|
329
|
+
"""
|
|
330
|
+
if isinstance(expr, Markdown):
|
|
331
|
+
return expr.data
|
|
332
|
+
|
|
333
|
+
return latex(expr, **kwargs)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
import re
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def wrap_floats(text, wrapper=("", "")):
|
|
340
|
+
# Define a regular expression pattern to match decimal numbers
|
|
341
|
+
float_pattern = re.compile(r"-?\d+\.\d+")
|
|
342
|
+
|
|
343
|
+
# Define a function to use as replacement
|
|
344
|
+
def wrap_match(match):
|
|
345
|
+
return f"{wrapper[0]}{match.group(0)}{wrapper[1]}"
|
|
346
|
+
|
|
347
|
+
# Use re.sub to replace all matches with the wrapped version
|
|
348
|
+
wrapped_text = float_pattern.sub(wrap_match, text)
|
|
349
|
+
|
|
350
|
+
return wrapped_text
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def format_decimal_numbers(text, format_string="{:.2f}"):
|
|
354
|
+
"""
|
|
355
|
+
Finds all decimal numbers in a string, applies a specified format,
|
|
356
|
+
and substitutes them back into the string.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
text: The string to search for decimal numbers.
|
|
360
|
+
format_string: The format string to apply to the decimal numbers.
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
The formatted string.
|
|
364
|
+
"""
|
|
365
|
+
if text is None or format_string is None:
|
|
366
|
+
return text
|
|
367
|
+
|
|
368
|
+
def format_match(match):
|
|
369
|
+
value = float(match.group())
|
|
370
|
+
return format_string.format(value)
|
|
371
|
+
|
|
372
|
+
return re.sub(r"-?\d+\.\d+", format_match, text)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def dict_to_eq(result: dict):
|
|
376
|
+
eq = [Eq(k, v) for k, v in result.items()]
|
|
377
|
+
return eq if len(eq) > 1 else eq[0]
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def eq_to_dict(result: Eq | list | tuple):
|
|
381
|
+
if hasattr(result, "__iter__"):
|
|
382
|
+
return {x.lhs: x.rhs for x in result}
|
|
383
|
+
else:
|
|
384
|
+
return {result.lhs: result.rhs}
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
import regex
|
|
388
|
+
|
|
389
|
+
# replacements for the regex function
|
|
390
|
+
replacement = {
|
|
391
|
+
r"\\frac": r"\\dfrac", # fisrt replace all frac with dfrac
|
|
392
|
+
r"\^\{((?:[^{}]|(?:\{(?1)\}))*)}": lambda m: regex.sub(
|
|
393
|
+
"dfrac", "frac", m.group(0)
|
|
394
|
+
), # then replace all dfrac inside ^{} with frac (small exponent)
|
|
395
|
+
r"\b1 \\cdot": r"",
|
|
396
|
+
r"\\\\": rf"\\\\[{options.VERTICAL_SKIP}]",
|
|
397
|
+
r"\bfor\b": "per",
|
|
398
|
+
r"\botherwise\b": "altrimenti",
|
|
399
|
+
r"\\,": r"{\,}",
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
# %% replace all the key, value pair
|
|
404
|
+
def replace_all(body, reps=replacement):
|
|
405
|
+
for pattern, repl in reps.items():
|
|
406
|
+
body = regex.sub(pattern, repl, body)
|
|
407
|
+
return body
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def latex_inline_dict(var, mapping: dict, **kwargs):
|
|
411
|
+
if not "mul_symbol" in kwargs:
|
|
412
|
+
kwargs["mul_symbol"] = r"\,"
|
|
413
|
+
match (mode := kwargs.get("mode")):
|
|
414
|
+
case "plain" | None:
|
|
415
|
+
wrap = ("", "")
|
|
416
|
+
case "inline":
|
|
417
|
+
wrap = ("$", "$")
|
|
418
|
+
case _:
|
|
419
|
+
wrap = (rf"\begin{{{mode}}}", rf"\end{{{mode}}}")
|
|
420
|
+
|
|
421
|
+
kwargs["mode"] = "plain"
|
|
422
|
+
|
|
423
|
+
_latex = lambda x: replace_all(latex(x, **kwargs))
|
|
424
|
+
return f"{wrap[0]}{_latex(var)} = {_latex(mapping[var])}{wrap[1]}"
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
if __name__ == "__main__":
|
|
428
|
+
x, y = symbols("x y")
|
|
429
|
+
show_eqn(
|
|
430
|
+
{
|
|
431
|
+
x: 1,
|
|
432
|
+
},
|
|
433
|
+
label={x: "banana"},
|
|
434
|
+
debug=True,
|
|
435
|
+
)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import pint
|
|
2
|
+
import sympy.physics.units as sympy_units
|
|
3
|
+
from sympy.physics.units.util import convert_to
|
|
4
|
+
|
|
5
|
+
from sympy import nsimplify, sympify
|
|
6
|
+
|
|
7
|
+
unitregistry = pint.UnitRegistry()
|
|
8
|
+
unitregistry.formatter.default_format = ".2f~P"
|
|
9
|
+
|
|
10
|
+
def pint_to_sympy(quantity: unitregistry.Quantity):
|
|
11
|
+
"""convert pint quantity to sympy quantity
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
quantity (UnitRegistry.Quantity): a quantity defined with the pint module
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
# divide and extract the magnitude from the units: it will generate a two elements tuple, where the first item will be the magnitude and the second ona a tuple of tuples; each nested tuple is composed by two elements, the unit proper and the exponent to which is elevated; the tuples are supposed to be multiplied together.
|
|
18
|
+
|
|
19
|
+
# quantity is multiplied by 1 so that it is converted to pint.Quantity if pint.Unit is passed instead
|
|
20
|
+
magnitude, units = (1 * quantity).to_tuple()
|
|
21
|
+
|
|
22
|
+
# for each unit (i.e. tuple), check if it exist in the sympy.physics.units module
|
|
23
|
+
for u in units:
|
|
24
|
+
fullname = u[0]
|
|
25
|
+
shortname = f"{pint.Unit(fullname):~}"
|
|
26
|
+
exponent = sympify(u[1])
|
|
27
|
+
|
|
28
|
+
# add a new unit if it doesn't exist
|
|
29
|
+
if not hasattr(sympy_units, fullname):
|
|
30
|
+
if [True for x in unitregistry.parse_unit_name(fullname) if not x[0] == ""]:
|
|
31
|
+
is_prefixed = True
|
|
32
|
+
else:
|
|
33
|
+
is_prefixed = False
|
|
34
|
+
|
|
35
|
+
setattr(
|
|
36
|
+
sympy_units,
|
|
37
|
+
fullname,
|
|
38
|
+
sympy_units.Quantity(
|
|
39
|
+
fullname, abbrev=shortname, is_prefixed=is_prefixed
|
|
40
|
+
),
|
|
41
|
+
) # create thwith full namee new sympy unit
|
|
42
|
+
# create the alias with the shortname
|
|
43
|
+
setattr(sympy_units, shortname, getattr(sympy_units, fullname))
|
|
44
|
+
|
|
45
|
+
# set the global scale factor relative to base units (base units are assumed to be in sympy)
|
|
46
|
+
# _magnitude, _units = (1 * pint.Unit(fullname)).to_base_units().to_tuple()
|
|
47
|
+
# _reference = sympify(1)
|
|
48
|
+
# for _u in _units:
|
|
49
|
+
# _reference *= getattr(sympy_units, _u[0])**nsimplify(_u[1])
|
|
50
|
+
|
|
51
|
+
# getattr(sympy_units, fullname).set_global_relative_scale_factor(_magnitude, _reference)
|
|
52
|
+
|
|
53
|
+
# multiply magnitude for the sympy units (create a sympy.core.Mul object)
|
|
54
|
+
magnitude *= (
|
|
55
|
+
getattr(sympy_units, fullname) ** (exponent)
|
|
56
|
+
if exponent != 1
|
|
57
|
+
else getattr(sympy_units, fullname)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return sympify(magnitude)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# UnitRegistry = pint.UnitRegistry()
|
|
64
|
+
# UnitRegistry.default_format = '.2f~P'
|
|
65
|
+
# Q = UnitRegistry.Quantity
|
|
66
|
+
# Q._sympy_ = lambda s: sympify(f'{s.m}*{s.u}')
|
|
67
|
+
|
|
68
|
+
pint.Quantity._sympy_ = lambda x: pint_to_sympy(x)
|
|
69
|
+
pint.Unit._sympy_ = lambda x: pint_to_sympy(1 * x)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
u = unitregistry
|
|
74
|
+
|
|
75
|
+
F = 5000 * u.daN # this unit is not present in sympy.core.physics
|
|
76
|
+
A = 2 * u.m
|
|
77
|
+
B = 300 * u.cm
|
|
78
|
+
|
|
79
|
+
# pint unit get converted to sympy units
|
|
80
|
+
print(sympify(F))
|
|
81
|
+
print(sympify(A))
|
|
82
|
+
print(sympify(B))
|
|
83
|
+
|
|
84
|
+
# sympy will not automatically simplify different units
|
|
85
|
+
print(sympify(F / (A * B))) # this is a pressure
|
|
86
|
+
|
|
87
|
+
# you need to use convert to
|
|
88
|
+
|
|
89
|
+
print(convert_to(sympify(F), u.kN))
|
|
90
|
+
|
|
91
|
+
print(convert_to(sympify(F / (A * B)), u.MPa))
|
|
92
|
+
|
|
93
|
+
print(convert_to(sympify(F / (A * B)), u.kPa))
|
|
94
|
+
|
|
95
|
+
print(f"{F:.4f~P}")
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# %% pipe command
|
|
2
|
+
from pipe import Pipe
|
|
3
|
+
from sympy.parsing.sympy_parser import parse_expr as sympy_parse_expr
|
|
4
|
+
from sympy import Basic, sympify, S, Mul, MatrixBase, UnevaluatedExpr
|
|
5
|
+
from sympy.core.function import UndefinedFunction
|
|
6
|
+
from sympy.physics.units.util import convert_to as sympy_convert_to
|
|
7
|
+
from sympy.physics.units.util import quantity_simplify as sympy_quantity_simplify
|
|
8
|
+
from sympy import topological_sort, default_sort_key
|
|
9
|
+
from itertools import permutations
|
|
10
|
+
from inspect import currentframe
|
|
11
|
+
from keecas.display import wrap_floats
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def order_subs(subs: dict) -> list[tuple]:
|
|
15
|
+
"""Reorders the substitutions using topological order, ensuring that
|
|
16
|
+
the order of elements passed to the subs function is exhaustive.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
subs (dict): Dictionary of substitutions to perform (VERTICES).
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
list: Ordered list of substitutions.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# Generate edges between each vertex
|
|
26
|
+
edges = [
|
|
27
|
+
(i, j) for i, j in permutations(subs.items(), 2) if sympify(i[1]).has(j[0])
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
# Reorder the dict with topological_sort
|
|
31
|
+
return topological_sort((subs.items(), edges), default_sort_key)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@Pipe
|
|
35
|
+
def subs(
|
|
36
|
+
expression: Basic,
|
|
37
|
+
substitution: dict,
|
|
38
|
+
sorted=True,
|
|
39
|
+
# simplify_quantity=True, **kwargs
|
|
40
|
+
) -> Basic:
|
|
41
|
+
|
|
42
|
+
# filter out None expressions from the expression
|
|
43
|
+
if expression is None:
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
# filter out non Basic expressions from the substitution dict
|
|
47
|
+
substitution = {
|
|
48
|
+
lhs: rhs
|
|
49
|
+
for lhs, rhs in substitution.items()
|
|
50
|
+
if isinstance(lhs, Basic | UndefinedFunction | str) and rhs is not None
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if sorted:
|
|
54
|
+
substitution = order_subs(substitution)
|
|
55
|
+
|
|
56
|
+
expression = S(expression).subs(substitution)
|
|
57
|
+
|
|
58
|
+
# if simplify_quantity:
|
|
59
|
+
# expression = expression | quantity_simplify(**kwargs)
|
|
60
|
+
|
|
61
|
+
return expression
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@Pipe
|
|
65
|
+
def N(expression: Basic, precision: int = 15) -> Basic:
|
|
66
|
+
return expression.evalf(precision)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@Pipe
|
|
70
|
+
def convert_to(expression: Basic, units=1) -> Basic:
|
|
71
|
+
return sympy_convert_to(expression, target_units=units)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@Pipe
|
|
75
|
+
def doit(expression: Basic) -> Basic:
|
|
76
|
+
return expression.doit()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
from sympy.parsing.sympy_parser import T
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@Pipe
|
|
83
|
+
def parse_expr(
|
|
84
|
+
expression: Basic, local_dict: dict = None, evaluate=False, **kwargs
|
|
85
|
+
) -> Basic:
|
|
86
|
+
"""
|
|
87
|
+
Parses a mathematical expression into a SymPy expression object.
|
|
88
|
+
|
|
89
|
+
Parameters:
|
|
90
|
+
expression (Basic): The mathematical expression to parse.
|
|
91
|
+
local_dict (dict, optional): A dictionary of local variables to use during parsing. If None is passed, then the current frame's local variables will be used.
|
|
92
|
+
evaluate (bool, optional): Whether to evaluate the expression during parsing. Defaults to False.
|
|
93
|
+
**kwargs: Additional keyword arguments to pass to the SymPy parser.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Basic: The parsed SymPy expression object.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
if not local_dict:
|
|
100
|
+
local_dict = currentframe().f_back.f_back.f_back.f_locals
|
|
101
|
+
|
|
102
|
+
if "transformations" not in kwargs:
|
|
103
|
+
kwargs["transformations"] = T[:11]
|
|
104
|
+
|
|
105
|
+
parsed_expr = sympy_parse_expr(
|
|
106
|
+
expression, evaluate=evaluate, local_dict=local_dict, **kwargs
|
|
107
|
+
)
|
|
108
|
+
return parsed_expr
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@Pipe
|
|
112
|
+
def quantity_simplify(
|
|
113
|
+
expression: Basic, across_dimensions=True, unit_system="SI", **kwargs
|
|
114
|
+
) -> Basic:
|
|
115
|
+
"""
|
|
116
|
+
Simplifies a given expression by applying quantity simplification.
|
|
117
|
+
|
|
118
|
+
Parameters:
|
|
119
|
+
expression (Basic): The expression to simplify.
|
|
120
|
+
across_dimensions (bool): Whether to simplify across dimensions. Defaults to True.
|
|
121
|
+
unit_system (str): The unit system to use for simplification. Defaults to "SI".
|
|
122
|
+
**kwargs: Additional keyword arguments to pass to the underlying sympy_quantity_simplify function.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Basic: The simplified expression.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
return sympy_quantity_simplify(
|
|
129
|
+
expression, across_dimensions=across_dimensions, unit_system=unit_system
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@Pipe
|
|
134
|
+
def as_two_terms(
|
|
135
|
+
expression: Basic,
|
|
136
|
+
as_mul=False,
|
|
137
|
+
) -> Basic:
|
|
138
|
+
"""
|
|
139
|
+
This function takes in a `Basic` expression and an optional boolean flag `as_mul`.
|
|
140
|
+
It checks if the expression is an instance of `Mul`. If it is, it calls the `as_two_terms()` method on the expression.
|
|
141
|
+
If the expression is not an instance of `Mul`, it checks if it is an instance of `MatrixBase`.
|
|
142
|
+
If it is, it creates a set of units by iterating over the values of the matrix and getting the coefficients dictionary of each element.
|
|
143
|
+
If the set of units has a length of 1, it assigns the only unit to `u`, divides the matrix by `u`, and assigns the result to `att`.
|
|
144
|
+
If the set of units has a length greater than 1, it returns the original expression.
|
|
145
|
+
If the expression is neither a `Mul` nor a `MatrixBase`, it returns the original expression.
|
|
146
|
+
Finally, it returns `att` if `as_mul` is `False`, otherwise it returns `att` combined with `as_Mul`.
|
|
147
|
+
"""
|
|
148
|
+
if isinstance(expression, Mul):
|
|
149
|
+
att = expression.as_two_terms()
|
|
150
|
+
elif isinstance(expression, MatrixBase):
|
|
151
|
+
units = {u for e in expression.values() for u in e.as_coefficients_dict()}
|
|
152
|
+
if len(units) == 1:
|
|
153
|
+
u = units.pop()
|
|
154
|
+
att = (expression / u, u)
|
|
155
|
+
else:
|
|
156
|
+
return expression
|
|
157
|
+
else:
|
|
158
|
+
return expression
|
|
159
|
+
|
|
160
|
+
return att | as_Mul if as_mul else att
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@Pipe
|
|
164
|
+
def as_Mul(expression: tuple[Basic]) -> Basic:
|
|
165
|
+
"""
|
|
166
|
+
Multiplies two expressions together and returns the result as an unevaluated expression. (Ideally to nicely separate the magnitude from the units)
|
|
167
|
+
|
|
168
|
+
Parameters:
|
|
169
|
+
expression (tuple[Basic]): A tuple containing two Basic expressions to be multiplied together.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Basic: The result of multiplying the two expressions together as an unevaluated expression.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
return UnevaluatedExpr(expression[0]) * UnevaluatedExpr(expression[1])
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# print(currentframe().f_back.f_locals)
|
|
179
|
+
# %% debug
|
|
180
|
+
|
|
181
|
+
if __name__ == "__main__":
|
|
182
|
+
import sympy as sp
|
|
183
|
+
|
|
184
|
+
x, y = sp.symbols("x y")
|
|
185
|
+
|
|
186
|
+
_d = {
|
|
187
|
+
x: 3,
|
|
188
|
+
y: x * 4,
|
|
189
|
+
}
|
|
190
|
+
print((None) | subs(_d))
|
|
191
|
+
|
|
192
|
+
e = sp.symbols("e", cls=sp.Function)
|
|
193
|
+
_e = {e: "Lambda(j, j+1)" | parse_expr}
|
|
194
|
+
print("e(x)" | parse_expr | subs(_e))
|
|
195
|
+
|
|
196
|
+
# %%
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# %% inserimento immagini in documento come link markdown
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from IPython.display import Markdown
|
|
5
|
+
|
|
6
|
+
# %% flatten dict across yaml
|
|
7
|
+
import flatten_dict as fd
|
|
8
|
+
from ruamel.yaml import YAML
|
|
9
|
+
|
|
10
|
+
yaml = YAML()
|
|
11
|
+
yaml.preserve_quotes = True
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def load_data(main: str, updated_value: str) -> dict:
|
|
15
|
+
# Check if main file exists
|
|
16
|
+
if not os.path.exists(main):
|
|
17
|
+
# Create an empty file and return an empty dict
|
|
18
|
+
with open(main, "w") as f:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
# caricamento dati esistenti (generati automaticamente)
|
|
22
|
+
with open(main, "r") as m:
|
|
23
|
+
try:
|
|
24
|
+
_main = fd.flatten(yaml.load(m))
|
|
25
|
+
except ValueError:
|
|
26
|
+
_main = {}
|
|
27
|
+
|
|
28
|
+
# caricamento dei metadata (inseriti manualmente)
|
|
29
|
+
with open(updated_value, "r") as m:
|
|
30
|
+
_updated_value = fd.flatten(yaml.load(m))
|
|
31
|
+
|
|
32
|
+
return fd.unflatten(_main | _updated_value)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# %% SYMPY
|
|
36
|
+
def escape_name(symbol_name, dict_of_subs={}):
|
|
37
|
+
name = str(symbol_name)
|
|
38
|
+
for old, new in dict_of_subs.items():
|
|
39
|
+
name = name.replace(old, new)
|
|
40
|
+
return name
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def escape_var(names, dict_of_subs=None, **args):
|
|
44
|
+
"""estensione di sympy:var() con l'introduzione di una lista di sostituzioni per escapare i nomi dei simboli
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
names (_type_): _description_
|
|
48
|
+
dict_of_subs (_type_, optional): _description_. Defaults to None.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def traverse(symbols, frame):
|
|
52
|
+
"""Recursively inject symbols to the global namespace."""
|
|
53
|
+
for symbol in symbols:
|
|
54
|
+
if isinstance(symbol, Basic):
|
|
55
|
+
frame.f_globals[escape_name(symbol.name, dict_of_subs)] = symbol
|
|
56
|
+
elif isinstance(symbol, FunctionClass):
|
|
57
|
+
frame.f_globals[escape_name(symbol.__name__, dict_of_subs)] = symbol
|
|
58
|
+
else:
|
|
59
|
+
traverse(symbol, frame)
|
|
60
|
+
|
|
61
|
+
from inspect import currentframe
|
|
62
|
+
|
|
63
|
+
frame = currentframe().f_back
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
if isinstance(names, str):
|
|
67
|
+
syms = symbols(names, **args)
|
|
68
|
+
else:
|
|
69
|
+
syms = names
|
|
70
|
+
|
|
71
|
+
if syms is not None:
|
|
72
|
+
if isinstance(syms, Basic):
|
|
73
|
+
frame.f_globals[escape_name(syms.name, dict_of_subs)] = syms
|
|
74
|
+
elif isinstance(syms, FunctionClass):
|
|
75
|
+
frame.f_globals[escape_name(syms.__name__, dict_of_subs)] = syms
|
|
76
|
+
else:
|
|
77
|
+
traverse(syms, frame)
|
|
78
|
+
finally:
|
|
79
|
+
del frame # break cyclic dependencies as stated in inspect docs
|
|
80
|
+
|
|
81
|
+
return syms
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def insert_images(source_path, dest_path=".", fig_opt=""):
|
|
85
|
+
images = []
|
|
86
|
+
|
|
87
|
+
# filtra lista di immagini -> path object
|
|
88
|
+
for root, _, files in os.walk(source_path):
|
|
89
|
+
for f in files:
|
|
90
|
+
if f.lower().endswith((".jpg", ".jpeg", ".png")):
|
|
91
|
+
image = Path(root) / f
|
|
92
|
+
display(
|
|
93
|
+
Markdown(
|
|
94
|
+
f"}>){{{fig_opt}}}"
|
|
95
|
+
)
|
|
96
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "keecas"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "A set of tools for symbolic math computation done in a jupyter notebook, aimed specifically for Quarto. It is based mostly on sympy."
|
|
5
|
+
authors = ["kompre <s.follador@gmail.com>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
|
|
8
|
+
[tool.poetry.dependencies]
|
|
9
|
+
python = "^3.12"
|
|
10
|
+
pint = "^0.24.3"
|
|
11
|
+
pipe = "^2.2"
|
|
12
|
+
sympy = "^1.13.1"
|
|
13
|
+
regex = "^2024.7.24"
|
|
14
|
+
ipython = "^8.26.0"
|
|
15
|
+
pyyaml = "^6.0.1"
|
|
16
|
+
flatten-dict = "^0.4.2"
|
|
17
|
+
ruamel-yaml = "^0.18.6"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
[tool.poetry.group.dev.dependencies]
|
|
21
|
+
pytest = "^8.3.2"
|
|
22
|
+
ipykernel = "^6.29.5"
|