adopt-plot 0.0.1__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.
adopt_plot/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """Adopt Plot package"""
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+ # Copyright (c) 2026 VartRusData. All rights reserved.
4
+ class TooManyVariablesError(ValueError):
5
+ """Исключение, возникающее при попытке построить график для более чем двух переменных."""
6
+ pass
7
+ from .plot import AdoptPlot
8
+ __all__ = ["AdoptPlot", "TooManyVariablesError"]
adopt_plot/plot.py ADDED
@@ -0,0 +1,401 @@
1
+
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+ # Copyright (c) 2026 VartRusData. All rights reserved.
4
+ from sympy import *
5
+ import numpy as np
6
+ import matplotlib.pyplot as plt
7
+ import logging
8
+ import re
9
+ import sympy
10
+
11
+
12
+ from . import TooManyVariablesError
13
+
14
+ def get_all_sympy_function_names():
15
+ """Собирает имена всех встроенных функций и констант SymPy."""
16
+ names = set(sympy.__all__)
17
+ #print(names)
18
+ return names
19
+
20
+
21
+ def insert_multiplication_signs(expr: str, extra_functions=None) -> str:
22
+ """
23
+ Вставляет знаки умножения с учётом неявного умножения.
24
+ Поддерживает латиницу и кириллицу в именах переменных.
25
+ Защищает все известные SymPy-функции и константы от разбиения.
26
+ """
27
+ func_set = get_all_sympy_function_names() # теперь там и функции, и константы
28
+ if extra_functions:
29
+ func_set.update(extra_functions)
30
+
31
+ sorted_funcs = sorted(func_set, key=len, reverse=True)
32
+ func_pattern = '|'.join(re.escape(f) for f in sorted_funcs)
33
+
34
+ LETTER = r'[A-Za-zА-Яа-яёЁ_]'
35
+ LETTER_DIGIT = r'[\dA-Za-zА-Яа-яёЁ_]'
36
+
37
+ # === ШАГ 1: Защита известных имён (функций и констант) ===
38
+ protected = {}
39
+
40
+ def protect_known(match):
41
+ name = match.group(0)
42
+ if name not in func_set:
43
+ return name
44
+ placeholder = f'\ue000{len(protected)}\ue001'
45
+ protected[placeholder] = name
46
+ return placeholder
47
+
48
+ #print(expr)
49
+ expr = re.sub(r'[A-Za-z_]\w*', protect_known, expr)
50
+ #print(expr)
51
+ # === ШАГ 2: Правила вставки умножения ===
52
+ expr = re.sub(rf'(\d+)(\ue000\d+\ue001)(?=\()', r'\1*\2', expr)
53
+ # Явно вставляем * между цифрой/буквой и известным именем перед '('
54
+ expr = re.sub(rf'(\d)({func_pattern})(?=\()', r'\1*\2', expr)
55
+ expr = re.sub(rf'({LETTER})({func_pattern})(?=\()', r'\1*\2', expr)
56
+
57
+ # Основные правила с кириллицей
58
+ expr = re.sub(rf'(\d)({LETTER})', r'\1*\2', expr) # 2x, 2я
59
+ # РАЗРЫВАЕМ ВСЕ ЦЕПОЧКИ БУКВ (кроме защищённых имён)
60
+ expr = re.sub(rf'({LETTER})(?={LETTER})', r'\1*', expr) # a*b*c, а*я
61
+ expr = re.sub(rf'({LETTER_DIGIT})(\()', r'\1*\2', expr) # a(, 3(, я(
62
+ expr = re.sub(rf'(\))({LETTER_DIGIT}\()', r'\1*\2', expr) # )a, )(, )я
63
+
64
+ # === ШАГ 3: Возвращаем защищённые имена на место ===
65
+ for placeholder, name in protected.items():
66
+ expr = expr.replace(placeholder, name)
67
+
68
+ return expr
69
+
70
+
71
+
72
+
73
+
74
+
75
+
76
+ from typing import Literal, Optional, Tuple
77
+
78
+ class AdoptPlot():
79
+ def __init__(self,
80
+ expr: str,
81
+ lib: Literal['contour', 'implicit', None] = None,
82
+ xlims: Tuple[float, float] = (-20, 20),
83
+ ylims: Tuple[float, float] = (-20, 20),
84
+ n: int = 10000,
85
+
86
+ depth: int = 9,
87
+ linewidth: float = 2.0,
88
+ limit: float | Tuple[float, float, float, float] = 100,
89
+ color: str = 'blue',
90
+ legend: bool = True,
91
+ points: bool = True,
92
+ grid: bool = True,
93
+ interact: bool = True,
94
+ show: bool = True,
95
+ text_legend: Optional[str] = None
96
+ ):
97
+ """
98
+ Plots an implicit function with adaptive engine selection.
99
+
100
+ Parameters:
101
+ expr: String containing the equation (e.g., "y = 1/x").
102
+ lib: Forced plotting engine ('contour', 'implicit', or None for auto-selection).
103
+ xlims :Initial visible range for the plot.
104
+ ylims: Initial visible range for the plot.
105
+ n: Grid density for the contour engine.
106
+ depth: Adaptive refinement depth for plot_implicit.
107
+ linewidth: Thickness of the plot line.
108
+ limit: Defines the computational domain for the plot. Accepts a single number (for a square area) or a tuple of 4 floats (x_min, x_max, y_min, y_max) for a rectangular area.
109
+ color: Line color.
110
+ legend: Whether to display the legend.
111
+ points: Whether to display axis intersection points.
112
+ grid: Whether to display the grid.
113
+ interact: Allow zooming/panning (does not fix limits rigidly).
114
+ show: If False, returns a PlotHandle object instead of displaying the plot.
115
+ text_legend: Additional text for the legend (supports "\\n" and a list of strings).
116
+ """
117
+ try:
118
+ self.lib = lib
119
+ self.xlims = xlims
120
+ self.ylims = ylims
121
+ self.n = n
122
+ self.depth = depth
123
+ self.linewidth = linewidth
124
+ self.color = color
125
+ self.legend = legend
126
+ self.points = points
127
+ self.grid = grid
128
+ self.interact = interact
129
+ self.show = show
130
+ self.limit = limit
131
+ self.text_legend = text_legend
132
+ # Получаем уравнения из поля ввода
133
+ equations_str = expr
134
+ logging.info(f"Полученная строка уравнений: {equations_str}")
135
+ if equations_str == "":
136
+ raise ValueError("Empty string is not allowed.")
137
+ # Проверяем наличие запятых в строке
138
+ equation = equations_str
139
+ # Разбиение строки на отдельные уравнения
140
+
141
+
142
+
143
+ # Преобразование уравнений в объекты Sympy
144
+ expressions = []
145
+ used_variables = set() # Множество переменных, используемых в уравнениях
146
+
147
+ logging.info(f"Преобразование уравнения: {equation}")
148
+
149
+ equation = insert_multiplication_signs(equation)
150
+
151
+ lhs, rhs = equation.split('=')
152
+ logging.info(str(lhs))
153
+ logging.info(str(rhs))
154
+ expressions.append(Eq(sympify(lhs), sympify(rhs)))
155
+ logging.info(f"Добавлено уравнение: {expressions[-1]}")
156
+
157
+ # Определяем переменные, участвующие в текущем уравнении
158
+ used_variables.update(list(expressions[-1].free_symbols))
159
+
160
+ logging.info(f"Переменные, задействованные в уравнениях: {used_variables}")
161
+
162
+ # Проверка на недоопределённость системы
163
+ # Строим график для первого уравнения
164
+ self.eq = expressions[0]
165
+ self.vars_list = list(self.eq.free_symbols)
166
+
167
+ if len(self.vars_list) == 2:
168
+ self.var1, self.var2 = self.vars_list[0], self.vars_list[1]
169
+ self.plot_equation()
170
+ return
171
+ else:
172
+ raise TooManyVariablesError(
173
+ f"adopt_plot поддерживает только 2D-графики. Получено {len(self.vars_list)} переменных: {self.vars_list}. "
174
+ f"Если вам нужен 3D-график, подождите следующей версии!"
175
+ )
176
+
177
+
178
+
179
+
180
+
181
+ # Обновляем историю
182
+
183
+ except Exception as e:
184
+ raise Exception(e)
185
+
186
+ def plot_equation(self):
187
+ try:
188
+ def has_odz(expr, var):
189
+ """
190
+ Проверяет, есть ли у выражения ограничения на область допустимых значений (ОДЗ).
191
+ Если есть - лучше рисовать через plot_implicit.
192
+ """
193
+ # 1. Проверяем наличие нелинейных функций с ограничениями (log, sqrt, asin, acos)
194
+ # В SymPy log и ln — одно и то же.
195
+ if expr.has(log) or expr.has(sqrt) or expr.has(asin) or expr.has(acos):
196
+ return True
197
+
198
+ # 2. Проверяем, есть ли переменная в знаменателе (деление на ноль)
199
+ numer, denom = expr.as_numer_denom()
200
+ if var in denom.free_symbols:
201
+ return True
202
+
203
+ return False
204
+
205
+
206
+ expr = self.eq.lhs - self.eq.rhs
207
+ vars_list = sorted(self.eq.free_symbols, key=lambda s: str(s))
208
+ var1, var2 = vars_list[0], vars_list[1]
209
+ # === 1. Безопасный поиск пересечений (ловит ошибки abs) ===
210
+ x_intercepts = []
211
+ y_intercepts = []
212
+ try:
213
+ x_sol = solve(expr.subs(var2, 0), var1)
214
+ x_intercepts = [float(s) for s in x_sol if s.is_real]
215
+ except Exception:
216
+ pass # Если sympy не может решить (abs), пропускаем
217
+
218
+ try:
219
+ y_sol = solve(expr.subs(var1, 0), var2)
220
+ y_intercepts = [float(s) for s in y_sol if s.is_real]
221
+ except Exception:
222
+ pass
223
+
224
+
225
+ def zoom_key_handler(event):
226
+ # Проверяем, что нажатие произошло на оси и это сочетание с Ctrl
227
+ if event.inaxes != self.ax:
228
+ return
229
+
230
+ # Коэффициент масштабирования (1.2 = 120%, 0.8 = 80%)
231
+ scale_factor = 1.2
232
+
233
+ # Получаем текущие границы
234
+ cur_xlim = self.ax.get_xlim()
235
+ cur_ylim = self.ax.get_ylim()
236
+
237
+ # Вычисляем центр текущего обзора
238
+ x_center = (cur_xlim[0] + cur_xlim[1]) / 2
239
+ y_center = (cur_ylim[0] + cur_ylim[1]) / 2
240
+
241
+ # Длина текущего диапазона
242
+ cur_xrange = cur_xlim[1] - cur_xlim[0]
243
+ cur_yrange = cur_ylim[1] - cur_ylim[0]
244
+ # print(event.key)
245
+ # Если нажато Ctrl + (или Ctrl + =) — приближаем
246
+ if event.key == 'ctrl+plus' or event.key == 'ctrl+equal' or event.key == 'ctrl+=' or event.key == 'ctrl++':
247
+ new_xrange = cur_xrange / scale_factor
248
+ new_yrange = cur_yrange / scale_factor
249
+ # Если нажато Ctrl - — отдаляем
250
+ elif event.key == 'ctrl+minus' or event.key == 'ctrl+-':
251
+ new_xrange = cur_xrange * scale_factor
252
+ new_yrange = cur_yrange * scale_factor
253
+ else:
254
+ return # Если нажата другая клавиша, ничего не делаем
255
+
256
+ # Устанавливаем новые границы, центрируя обзор
257
+ self.ax.set_xlim([x_center - new_xrange / 2, x_center + new_xrange / 2])
258
+ self.ax.set_ylim([y_center - new_yrange / 2, y_center + new_yrange / 2])
259
+
260
+ # Принудительно перерисовываем график
261
+ event.canvas.draw_idle()
262
+ # === 2. Увеличиваем разрешение для гладкости (было 500, стало 1000) ===
263
+ if self.lib == 'implicit' or (has_odz and self.lib is None):
264
+ from sympy.plotting import plot_implicit
265
+ if isinstance(self.limit, (int, float)):
266
+ self.p = plot_implicit(self.eq, (var1, -self.limit, self.limit), (var2, -self.limit, self.limit), show=False, n=self.n, depth=self.depth)
267
+ elif isinstance(self.limit, tuple):
268
+ if len(self.limit) == 4:
269
+ self.p = plot_implicit(self.eq, (var1, *self.limit[:2]), (var2, *self.limit[2:]), show=False, n=self.n, depth=self.depth)
270
+ else:
271
+ raise ValueError(f"Length mismatch (expected 4), got {len(self.limit)}.")
272
+ else:
273
+ raise TypeError(f"Type mismatch.\nExpected float | tuple[float, float, float, float], got {type(self.limit)}.")
274
+ self.p.process_series() # Отрисовываем линию
275
+
276
+ self.ax = self.p.ax
277
+ logging.info(f"{self.ax}")
278
+ self.fig = self.p.fig
279
+ if self.interact:
280
+ self.fig.canvas.mpl_connect('key_press_event', zoom_key_handler)
281
+ x_coords_str = ", ".join([f"({x:.2f}, 0)" for x in x_intercepts]) if x_intercepts else "Нет"
282
+ y_coords_str = ", ".join([f"(0, {y:.2f})" for y in y_intercepts]) if y_intercepts else "Нет"
283
+
284
+ # Три пустые линии для легенды (они не видны на графике, но создают текст в легенде)
285
+ self.ax.plot([], [], ' ', label=f"${latex(self.eq)}$") # Красивое уравнение через LaTeX
286
+ self.ax.plot([], [], ' ', label=f"Пересечения с X: {x_coords_str}") # Текст про X
287
+ self.ax.plot([], [], ' ', label=f"Пересечения с Y: {y_coords_str}") # Текст про Y
288
+
289
+
290
+ patches = self.ax.patches
291
+ logging.info(f"{patches}")
292
+ # === УНИВЕРСАЛЬНОЕ ИЗМЕНЕНИЕ ТОЛЬКО ГРАФИКА (БЕЗ РАМОК) ===
293
+
294
+ # 1. Работаем с контурами и коллекциями (contour/plot_implicit)
295
+ for collection in self.ax.collections:
296
+ collection.set_linewidth(self.linewidth)
297
+ collection.set_edgecolor(self.color)
298
+
299
+ # 2. Работаем с патчами (заливки в implicit), но пропускаем оси (Spines)
300
+ from matplotlib.spines import Spine
301
+ for patch in self.ax.patches:
302
+ # Если это рамка — пропускаем
303
+ if isinstance(patch, Spine):
304
+ continue
305
+ patch.set_linewidth(self.linewidth)
306
+ patch.set_edgecolor(self.color)
307
+
308
+ # 3. Работаем с обычными линиями (например, ax.plot)
309
+ for line in self.ax.lines:
310
+ line.set_linewidth(self.linewidth)
311
+ line.set_color(self.color)
312
+
313
+
314
+
315
+ # Дальше твой код с точками и оформлением...
316
+ try:
317
+
318
+ try:
319
+
320
+ for x in x_intercepts: self.ax.scatter(x, 0, s=60, color='red', marker='o', zorder=5)
321
+ except:
322
+ pass
323
+ try:
324
+
325
+ for y in y_intercepts: self.ax.scatter(0, y, s=60, color='blue', marker='o', zorder=5)
326
+ except:
327
+ pass
328
+
329
+ self.ax.axhline(0, color='black', linewidth=1)
330
+ self.ax.axvline(0, color='black', linewidth=1)
331
+ self.ax.set_xlim(self.xlims)
332
+ self.ax.set_ylim(self.ylims)
333
+ self.ax.set_xlabel(str(var1))
334
+ self.ax.set_ylabel(str(var2))
335
+ if self.text_legend:
336
+ self.ax.plot([], [], ' ', label=f"{self.text_legend}")
337
+ if self.legend:
338
+ self.ax.legend()
339
+ self.ax.grid(True, linestyle='--', alpha=0.7)
340
+ except Exception as e:
341
+ logging.warning(f"Ошибка при добавлении оформления на p.ax: {e}")
342
+
343
+ self.p.fig.tight_layout()
344
+ if self.show:
345
+ self.p.plt.show()
346
+ return
347
+ else:
348
+ if isinstance(self.limit, (float, int)):
349
+
350
+ x_vals = np.linspace(-self.limit, self.limit, self.n)
351
+ y_vals = np.linspace(-self.limit, self.limit, self.n)
352
+ elif isinstance(self.limit, tuple):
353
+ if len(self.limit) == 4:
354
+ x_vals = np.linspace(self.limit[0], self.limit[1], self.n)
355
+ y_vals = np.linspace(self.limit[2], self.limit[3], self.n)
356
+ else:
357
+ raise ValueError(f"Length mismatch (expected 4), got {len(self.limit)}.")
358
+ else:
359
+ raise TypeError(f"Type mismatch.\nExpected float | tuple[float, float, float, float], got {type(self.limit)}.")
360
+ y_vals[~np.isfinite(y_vals)] = np.nan
361
+ X, Y = np.meshgrid(x_vals, y_vals)
362
+ f = lambdify((var1, var2), expr, modules='numpy')
363
+ # print(x_vals, y_vals, X, Y, f, sep='\n<------------------------------->\n')
364
+ Z = f(X, Y)
365
+ # print(Z)
366
+ self.fig, self.ax = plt.subplots()
367
+ x_coords_str = ", ".join([f"({x:.2f}, 0)" for x in x_intercepts]) if x_intercepts else "Нет"
368
+ y_coords_str = ", ".join([f"(0, {y:.2f})" for y in y_intercepts]) if y_intercepts else "Нет"
369
+ self.ax.contour(X, Y, Z, levels=[0], colors='blue', linewidths=self.linewidth)
370
+
371
+ self.ax.plot([], [], ' ', label=f"Уравнение: ${latex(self.eq)}$")
372
+
373
+ # Оси X и Y
374
+ self.ax.axhline(0, color='black', linewidth=1)
375
+ self.ax.axvline(0, color='black', linewidth=1)
376
+
377
+ # Точки пересечения (теперь они не вызовут ошибку, если не найдутся)
378
+ self.ax.scatter([x for x in x_intercepts], [0] * len(x_intercepts), s=60, color='red', marker='o', zorder=5,
379
+ label=f"Пересечения с X: {x_coords_str}")
380
+
381
+ self.ax.scatter([0] * len(y_intercepts), [y for y in y_intercepts], s=60, color='blue', marker='o', zorder=5,
382
+ label=f"Пересечения с Y: {y_coords_str}")
383
+
384
+ # Подписи осей строго по переменным
385
+ self.ax.set_xlabel(str(var1))
386
+ self.ax.set_ylabel(str(var2))
387
+ self.ax.grid(True, linestyle='--', alpha=0.7)
388
+ self.ax.set_xlim(self.xlims)
389
+ self.ax.set_ylim(self.ylims)
390
+ if self.text_legend:
391
+ self.ax.plot([], [], ' ', label=f"{self.text_legend}")
392
+ if self.legend:
393
+ self.ax.legend()
394
+ plt.show()
395
+
396
+ except Exception as e1:
397
+ logging.warning(f"Matplotlib contour не сработал: {e1}")
398
+ # Если контур упал (сингулярности), используем sympy.plot_implicit
399
+ from sympy.plotting import plot_implicit
400
+ p = plot_implicit(self.eq, (var1, -10, 10), (var2, -10, 10), show=False)
401
+ p.show()
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.4
2
+ Name: adopt-plot
3
+ Version: 0.0.1
4
+ Summary: A universal plotter for implicit functions using adaptive engine selection.
5
+ Author: VartRusData
6
+ License: BSD 3-Clause
7
+ Classifier: License :: OSI Approved :: BSD License
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: sympy
12
+ Requires-Dist: matplotlib
13
+ Requires-Dist: numpy
14
+ Dynamic: license-file
15
+
16
+ # adopt-plot
17
+
18
+ A simple Python library for plotting implicit functions with adaptive engine selection.
19
+ Just pass an equation and get a beautiful, mathematically correct plot.
@@ -0,0 +1,7 @@
1
+ adopt_plot/__init__.py,sha256=lxUd6MgfND2muGCURby4s4R3SmAaW8qWGCNTC_0nT_E,418
2
+ adopt_plot/plot.py,sha256=sUmjMOMVPYbv6gYGnQbkMPvnjrsKmqO5CzxJBbiLE8Q,19714
3
+ adopt_plot-0.0.1.dist-info/licenses/LICENSE,sha256=WZte-rQIp6QmVj6-5DLhGEK2By1GnzykObQqbcazK-Y,1478
4
+ adopt_plot-0.0.1.dist-info/METADATA,sha256=7_uWqtIYzU-f8TmtuCv45R30yC6Pc2bb37V2fpdRNws,602
5
+ adopt_plot-0.0.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
6
+ adopt_plot-0.0.1.dist-info/top_level.txt,sha256=pFg71VDYed8NjYV3j_hv7GPLBiqJjDd-MkN6yJPOmHM,11
7
+ adopt_plot-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2026 VartRusData.
2
+
3
+ Redistribution and use in source and binary forms, with or without modification,
4
+ are permitted provided that the following conditions are met:
5
+
6
+ 1. Redistributions of source code must retain the above copyright notice,
7
+ this list of conditions and the following disclaimer.
8
+ 2. Redistributions in binary form must reproduce the above copyright notice,
9
+ this list of conditions and the following disclaimer in the documentation and/or other materials provided
10
+ with the distribution.
11
+ 3. Neither the name of the copyright holder nor the names of its contributors
12
+ may be used to endorse or promote products derived from this software
13
+ without specific prior written permission.
14
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
16
+ THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
17
+ IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
18
+ INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
19
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
20
+ OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
21
+ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
22
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
1
+ adopt_plot