Некорректная работа модуля multiprocessing в exe файле
Я решил использовать multiprocessing для параллельного отображения графиков (они анимированные). При запуске из скрипта именно из среды разработки, всё работает, я получаю нужный мне результат графики отображаются параллельно друг другу. Но когда я создаю exe, то вместо создания параллельных анимационных графиков, создаются окна exe приложения, а не графики. Как это можно исправить? Если у кого есть возможность, то посмотрите два файла, или может быть соберёте exe файл, чтобы приложение работало корректно и создавала параллельные анимированные графики. (Создавал exe файл через py-auto-to-exe, работаю с библиотекой matplotlib.) Вот код:
from tkinter import *
from tkinter import ttk
import re
import webbrowser
from dynamics import init_graf, run_animation
import multiprocessing as mu
import sys, os
def rp(relative):
if hasattr(sys, "_MEIPASS"):
return os.path.join(sys._MEIPASS, relative)
return os.path.join(relative)
class user_0():
def __init__(self):
self.run_rpg()
def run_rpg(self):
self.root = Tk()
self.root.title('Школьный проект 06.11.23')
img = PhotoImage(file = rp('favicon-0.png'))
self.root.iconphoto(0, img)
self.notebook = ttk.Notebook()
self.notebook.pack(expand=1)
frame0 = ttk.Frame(self.notebook)
frame1 = ttk.Frame(self.notebook)
frame2 = ttk.Frame(self.notebook)
frame0.pack(expand=1)
frame1.pack(expand=1)
frame2.pack(expand=1)
self.notebook.add(frame0, text="График Максвелла")
self.notebook.add(frame1, text="Мои графики")
self.notebook.add(frame2, text="Доплнительные настройки графиков")
font0 = ('Comic Sans MS', 14, 'bold')
Label(frame0, text='2d график Максвелла', font=font0).pack()
self.root.geometry("450x300")
self.box = Listbox(frame0, selectmode=EXTENDED)
self.box.pack(side=LEFT)
scroll = Scrollbar(command=self.box.yview)
scroll.pack(side=LEFT, fill=Y)# fill=Y
self.box.config(yscrollcommand=scroll.set)
f = LabelFrame(frame0, text="Молярная масса (г/моль) | Температура (K)", width=200, height=100)
f.pack(side=LEFT, padx=10)
self.entry0 = Entry(f)
self.entry0.pack(anchor=N)
self.entry1 = Entry(f)
self.entry1.pack(anchor=N)
Button(f, text="Добавить", command=self.add_item).pack(fill=X)
Button(f, text="Удалить", command=self.del_list).pack(fill=X)
Button(f, text="Запустить график!", command=self.load_2d_Maxwell).pack(fill=X)
Button(f, text='Сравнить данные графика с интернетом', command=self.open_website)\
.pack(fill=X)
font1 = ('Comic Sans MS', 12)
f1 = LabelFrame(frame1, text="Общие сведения (парметры газа)", width=600, height=800)
f1.pack(anchor="nw", padx=10, ipady=10)
Label(f1, text='Число молекул 9 < n < 2001', font=font1).pack(anchor="center")
self.entry2 = Entry(f1)
self.entry2.pack(anchor="center")
Label(f1, text='Температура (K)', font=font1).pack(anchor="center")
self.entry3 = Entry(f1)
self.entry3.pack(anchor="center")
Label(f1, text='Молярная масса (г/моль)', font=font1).pack(anchor="center")
self.entry4 = Entry(f1)
self.entry4.pack(anchor="center")
Label(f1, text='Радиус (пикометры)', font=font1).pack(anchor="center")
self.entry5 = Entry(f1)
self.entry5.pack(anchor="center")
Label(f1, text='Колличество шагов моделирования', font=font1).pack(anchor="center")
self.entry6 = Entry(f1)
self.entry6.pack(anchor="center")
Label(f1, text='Единичный шаг', font=font1).pack(anchor="center")
self.entry7 = Entry(f1)
self.entry7.pack(anchor="center")
Label(f1, text='Во сколько раз увеличить объём занимаемый молекулами?', font=font1).pack(anchor="center")
self.entry8 = Entry(f1)
self.entry8.pack(anchor="center")
f2 = LabelFrame(frame1, text="Выбор графиков")
f2.pack(side=LEFT, padx=10, ipady = 2)
self.var0 = IntVar()
self.var1 = IntVar()
self.var2 = IntVar()
self.var0.set(0); self.var1.set(0); self.var2.set(0)
a = Checkbutton(f2, text="Гистограмма",\
variable=self.var0, onvalue=1, offvalue=0).pack(side=LEFT)
b = Checkbutton(f2, text="Куб",\
variable=self.var1, onvalue=2, offvalue=0).pack(side=LEFT)
c = Checkbutton(f2, text="Сфера",\
variable=self.var2, onvalue=3, offvalue=0).pack(side=LEFT)
f3 = LabelFrame(frame1, text="Запуск график(а-ов)")
f3.pack(side=LEFT, padx=0, ipadx = 8.5, ipady = 2) # 27.5
Button(f3, text='Запуск', command = self.creat_my_graf).pack(anchor="center")
f4 = LabelFrame(frame2, text="Дополнения графика (сфера)")
f4.pack(ipady = 7)
Label(f4, text='Диапазон (по умолчанию наиболее вероятная скорость (пустое поле))', font=font1).pack(anchor="center")
Label(f4, text='Укажите диапазон скоростей через пробел в м/c', font=font1).pack(anchor="center")
Label(f4, text='Пример: 100 750 (от 100 м/с до 751 м/с)', font=font1).pack(anchor="center")
self.entry9 = Text(f4, height=1, width=16)
self.entry9.pack(anchor="center")
Label(f4, text='Возможное отклонение от диапазона (по умолчанию ± 150 м/с)', font=font1).pack(anchor="center")
self.entry10 = Entry(f4)
self.entry10.pack(anchor="center")
Label(f4, text='Размер точек (x > 5)', font=font1).pack(anchor="center")
self.entry11 = Entry(f4)
self.entry11.pack(anchor="center")
f5 = LabelFrame(frame2, text="Дополнения графика (куб)")
f5.pack(ipadx = 195, ipady = 7)
Label(f5, text='Размер точек (x > 5)', font=font1).pack(anchor="center")
self.entry12 = Entry(f5)
self.entry12.pack(anchor="center")
self.root.bind("<<NotebookTabChanged>>", self.tab_change)
self.root.mainloop()
def add_item(self):
if not (re.findall(r'[^0-9.,]',self.entry0.get()) or re.findall(r'[^0-9.,]',self.entry1.get())):
st0, st1 = re.sub(r',', '.', self.entry0.get()), re.sub(r',', '.', self.entry1.get())
self.box.insert(END, st0 + ' г/моль ' + '| ' + st1 + ' К ')
self.entry0.delete(0, END); self.entry1.delete(0, END)
def del_list(self):
select = list(self.box.curselection())
select.reverse()
for i in select:
self.box.delete(i)
def open_website(self):
url = "https://www.calculatoratoz.com/ru/most-probable-speed-calculator/Calc-1405"
webbrowser.open(url)
def load_2d_Maxwell(self):
if self.box.get(0, END):
st = "*".join(self.box.get(0, END))
st = list(map(lambda x: re.sub(r'[гКмоль|/]', '', x), st.split('*')))
f = list(map(lambda x: [(t:=list(map(float, x.split())))[0]*10**(-3), t[1]], st))
init_graf(f)
def tab_change(self, event):
if self.notebook.index(self.notebook.select()) == 1:
self.root.resizable(1, 1); self.root.geometry("500x450"); self.root.resizable(0, 0) #500x450
elif self.notebook.index(self.notebook.select()) == 2:
self.root.resizable(1, 1); self.root.geometry("570x355"); self.root.resizable(0, 0) #500x450
else:
self.root.resizable(1, 1); self.root.geometry("450x250"); self.root.resizable(0, 0)
def slova(self):
params = {'natoms': (n:=int(self.entry2.get())), # Число атомов
'temp': float(self.entry3.get()), # 300 Кельвинов
'mass': float(self.entry4.get())*10**(-3), # 1 кг на моль
'radius': (r:=float(self.entry5.get()) * 10**(-12)) , # 120 Пикометров в метрах
'relax': 1e-13, # 100 центро-секунд
'dt': 1e-15, # временной шаг равный фенто секунде определена в секнудах 1e-15
'steps': int(self.entry6.get()), # Количество шагов моделирования
'freq': int(self.entry7.get()), # 1 шаг ---> steps = 10000/100 = 100
'R': (R:=float(self.entry8.get())), # Во сколько раз увеличить занимаемый объём молекул, ведь начальный объём равен объёму занимаемый всеми молекулами.
'box': ((0, ((R * 5)**(1/3) * ((2*r)**3) * n)**(1/3)), (0, ((R * 5)**(1/3) * ((2*r)**3) * n)**(1/3)), (0, ((R * 5)**(1/3) * ((2*r)**3) * n)**(1/3))), # Пространство куб значения сторон в нанометрах 100*(152 * 10**(-12))
'size_o_sfer': float(t) if (t:=self.entry11.get()) else 30,
'size_o_cube': float(t) if (t:=self.entry12.get()) else 10,
}
return params
def coll_bennet(self, x):
if x == 1:
return [self.slova()]
if x == 2:
return [self.slova(), 44]
else:
return [self.slova(), 1, list(map(float, t.split())) if (t:=self.entry9.get('1.0', 'end-1c')) else [],\
float(self.entry10.get() if self.entry10.get() else 150)]
def creat_my_graf(self):
proverka = lambda X: [(t:=[re.sub(',', '', x) for x in X if not re.findall(r'[^0-9.,]',x)]), len(X) == len(t)]
s = list(map(lambda x: x.get(), [self.entry2, self.entry3, self.entry4, self.entry5, self.entry6, self.entry7, self.entry8, self.entry10]))
if proverka(s)[-1]:
arg = [self.coll_bennet(i) for i in [self.var0.get(), self.var1.get(), self.var2.get()] if i]
l, r = len(arg) * [run_animation], [*arg]
[mu.Process(target=l).start() if not r else mu.Process(target=l, args=(r,)).start() for l, r in zip(l, r)]
if __name__ == '__main__':
App = user_0()
Код второго скрипта для создания графиков:
import numpy as np
import matplotlib.pylab as plt
from pylab import get_current_fig_manager
import tkinter
import matplotlib
matplotlib.use('TkAgg')
# 2D графики!
def maxwell_speed_distribution(v, M, T):
return 4 * np.pi * (M / (2 * np.pi * T * 8.31))**0.5 * v**2 *\
np.exp(1)**(-(M * v**2) / (2 * T * 8.31))
def creat_graf(fig0, ax0, mass, temp):
v = np.linspace(0, 5000, 10**(4))
a1 = ax0.plot(v, (f:=maxwell_speed_distribution(v, mass,\
(T:=temp))), label=f"T = {T}K")
color = a1[0].get_color()
ax0.scatter((k:=np.sqrt((2*T*8.31)/mass)), np.max(f), s=50,
color=color)
ax0.plot((k , k), (0, np.max(f)), '--', color=color,
label=f"$Vн.в = {round(k,1)}м/с$")
ax0.axhline(0, color='red')
ax0.axvline(0, color='red')
ax0.set_xlabel("Скорость (м/с)"); ax0.set_ylabel("$f$ $(x)$")
ax0.set_title("Распределение скоростей Максвелла")
ax0.legend();
def on_scroll(event):
scale_factor = 1.2 if event.button == "up" else 1 / 1.2
axis = event.inaxes
if axis:
axis.set_xlim(axis.get_xlim()[0] * scale_factor,
axis.get_xlim()[1] * scale_factor)
axis.set_ylim(axis.get_ylim()[0] * scale_factor,
axis.get_ylim()[1] * scale_factor)
plt.draw()
def init_graf(spi):
fig0, ax0 = plt.subplots(); ax0.grid()
get_current_fig_manager().set_window_title('График функции
распределения скоростей Максвелла')
[creat_graf(fig0, ax0, *i) for i in spi]
fig0.canvas.mpl_connect('scroll_event', on_scroll)
plt.show()
class molecular_motion():
def __init__(self, args, visual='', your_spread='', otcl = 150):
self.visual, self.PIG = visual, None
self.otcl, self.your_spread = otcl, your_spread
self.natoms, self.box, self.dt, self.temp = args['natoms'], args['box'], args['dt'], args['temp']
self.mass, self.relax, self.nsteps = args['mass'], args['relax'], args['steps']
self.freq, self.radius = args['freq'], args['radius']
self.size_o_sfer, self.size_o_cube = args['size_o_sfer'], args['size_o_cube']
ok = self._init_3d() if visual else self._init_2d()
self.f = maxwell_speed_distribution(np.linspace(0, 10**(4), 10**(4)), self.mass, self.temp)
# Определите глобальные физические константы
self.Avogadro = 6.02214086e23
self.Boltzmann = 1.38064852e-23
self.dim = len(self.box) # Размерность т.е какой тип моделирования проводим (мы в 3-х мерном пространстве)
self.pos = np.random.rand(self.natoms,self.dim) # Генерация случайных позиций 2D - массив координаты по осям x y z
for i in range(self.dim): # проводим цикл по всем измерениям
self.pos[:,i] = self.box[i][0] + (self.box[i][1] - self.box[i][0]) * self.pos[:,i] # Задание координат в поле моделирования
# Относително размеров куба, ведь координаты на могут лежать от 0 до 1
self.vels = np.random.rand(self.natoms,self.dim) # создание 2D массива со случайными скоростями по осям x y z
self.mass = np.ones(self.natoms) * self.mass / self.Avogadro # Т.к дана молярная масса, в г/моль нужно разделить на
# число авогодро, тем самым получим массу одной молекулы m0 = M/Na
self.radius = np.ones(self.natoms) * self.radius
self.step = 0
self.output = [] # список для динамических данных
self.Pause = False
self.run() # ЗАПУСК ГРАФИКА !!!
def _init_3d(self):
if self.visual == 1:
self.fig3, self.ax3 = plt.subplots(subplot_kw={'projection': '3d'})
self.fig3.set_size_inches(900 / self.fig3.dpi, 700 / self.fig3.dpi); self.PIG = self.fig3
get_current_fig_manager().set_window_title('3D график распределения скоростей')
self.fig3.canvas.mpl_connect('scroll_event', on_scroll)
self.fig3.canvas.mpl_connect('pick_event', self._pressed_molecul)
self.fig3.canvas.mpl_connect('key_press_event', self.press_batton)
self.fig3.canvas.mpl_connect('resize_event', self._on_resize)
else:
self.fig2, self.ax2 = plt.subplots(subplot_kw={'projection': '3d'})
self.fig2.set_size_inches(900 / self.fig2.dpi, 700 / self.fig2.dpi); self.PIG = self.fig2
get_current_fig_manager().set_window_title('Движение молекул')
self.fig2.canvas.mpl_connect('key_press_event', self.press_batton)
def press_batton(self, event):
self.Pause = not self.Pause # if evemt.key == 'space' else self.Pause
def _init_2d(self):
if self.freq < self.nsteps//15:
self.fig4, self.ax4 = plt.subplots()
self.fig4.set_size_inches(900 / self.fig4.dpi, 700 / self.fig4.dpi); self.PIG = self.fig4
get_current_fig_manager().set_window_title('2D график распределения скоростей')
self.fig4.canvas.mpl_connect('scroll_event', on_scroll)
else:
self.fig1, self.ax1 = plt.subplots(2,1)
self.fig1.set_size_inches(900 / self.fig1.dpi, 700 / self.fig1.dpi); self.PIG = self.fig1
get_current_fig_manager().set_window_title('2D график распределения скоростей')
self.fig1.canvas.mpl_connect('scroll_event', on_scroll)
self.fig1.canvas.mpl_connect('key_press_event', self.press_batton)
def run(self):
# Основной цикл эволюции
while True: # self.step <= self.nsteps
if self.step <= self.nsteps and not self.Pause:
self._runner_png()
else:
plt.pause(0.1)
def _runner_png(self):
#print(self.Pause)
if self.visual:
self._update_vels() if self.visual == 1 else self._update_cube()
if not self.visual and self.step <= self.nsteps:
self.output.extend(self.vels.tolist()) # [[1,2,3], [], .... []] # 2D
if self.step == self.nsteps:
self.output = np.round(np.sqrt(np.sum(np.array(self.output)**2, 1)), 1)
sl = {i: count for i, count in zip(*np.unique(self.output, return_counts=True))}
if self.freq < self.nsteps//15:
self._plt_if([list(sl.keys())[list(sl.values()).index(max(sl.values()))], sl])
else:
self._update_plot([list(sl.keys())[list(sl.values()).index(max(sl.values()))], sl])
self.step += 1
# Вычислить все силы
forces = self._computeForce()
# Переместите систему во времени (отвечает за скорость и положение частиц по времени)
self._integrate(forces)
# Проверьте, не столкнулась ли какая-либо частица со стеной
self._wallHitCheck()
if not self.step%self.freq and not self.visual and not (self.freq < self.nsteps//15): # 2D
self._update_plot() # 2D
def _wallHitCheck(self):
self.ndims = len(self.box)
# столкновение считаем абсолютно упругим при взаимедействии со стенкой скорость меняется на противоположную
for i in range(self.ndims):
self.vels[((self.pos[:,i] <= self.box[i][0]) | (self.pos[:,i] >= self.box[i][1])),i] *= -1
def _integrate(self, forces):
self.pos += self.vels * self.dt if not self.visual else self.vels * self.dt * 25 # 100 константа увеличения dt
self.vels += forces * self.dt / self.mass[np.newaxis].T
def _computeForce(self):
self.natoms, self.ndims = self.vels.shape # ndlims число измерений 3D
sigma = np.sqrt(2.0 * self.mass * self.temp * self.Boltzmann / (self.relax * self.dt)) # стандартное отклонение для случайной силы
noise = np.random.randn(self.natoms, self.ndims) * sigma[np.newaxis].T # Вычисление случайной силы
# noise = np.random.randn(natoms, ndims) * sigma Можно так
force = - (self.vels * self.mass[np.newaxis].T) / self.relax + noise
# force = - (vels * mass[0] / relax + noise Можно так
return force
def _update_cube(self):
self.ax2.clear()
prx, pry, prz = self.box
self.ax2.set_xlim(prx); self.ax2.set_ylim(pry); self.ax2.set_zlim(prz)
self.ax2.set_axis_off()
t = [[prx[0],pry[0],0], [prx[0],pry[1],0], [prx[1], pry[1], 0], [prx[1], pry[0], 0]]
XY = np.concatenate(((t:=np.array([[v, v1] for v, v1 in zip(t, t[1:])]+[[[prx[0],pry[0],0],[prx[1], pry[0], 0]]])), (y:=t+[0,0,pry[1]]),
np.array([[v, v1] for v, v1 in zip(t[:-1].reshape(6,3),y[:-1].reshape(6,3))])))
x, y, z = self.pos[:,0],self.pos[:,1], self.pos[:,2]
[self.ax2.plot([v[0,0],v[1,0]],[v[0,1],v[1,1]],[v[0,2],v[1,2]], 'red') for v in XY]
self.ax2.scatter(x, y, z, s=self.size_o_cube, color='blue')
self.ax2.set_title('Модель движения молекул в пространстве')
plt.pause(0.1)
def _update_vels(self):
self.ax3.clear(); maxf = np.max(self.f)
sp = np.round(np.sqrt(self.vels[:,0]*self.vels[:,0] + self.vels[:,1]*self.vels[:,1] + self.vels[:,2]*self.vels[:,2]))
sl = {i: count for i, count in zip(*np.unique(sp, return_counts=True))}
most_frequent = list(sl.keys())[list((v:=sl.values())).index(max(v))]
spread = np.logical_and(most_frequent-self.otcl <= sp, most_frequent+self.otcl >= sp) if not self.your_spread else \
np.logical_and(min(self.your_spread) <= sp, max(self.your_spread) >= sp) # разброс рыжих скоростей
self.colors_palate = np.where(spread, '#d77d31', '#700961')
self.ax3.set_xlim(j:=([-maxf*1.2, maxf*1.2])); self.ax3.set_ylim(j); self.ax3.set_zlim(j)
self.ax3.set_axis_off()
self.ax3.plot(j, [0,0], [0,0], color='red', alpha=0.7, label=f'$Vx м/с$')
self.ax3.plot([0,0], j, [0,0], color='green', alpha=0.7, label=f'$Vy м/с$')
self.ax3.plot([0,0], [0,0], j, color='blue', alpha=0.7, label=f'$Vz м/с$')
self.ax3.scatter(0,0,0, color='grey')
self.ax3.set_title('График проекций скоростей')
self.scatter = self.ax3.scatter(*self.vels.T, color=self.colors_palate, s=self.size_o_sfer, picker=1)
#print(self.colors_palate)
self.ax3.set_title('График проекций скоростей' + \
f'\nВсего точек: {self.colors_palate.size} ' + \
f'Рыжих точек: {np.count_nonzero(self.colors_palate == "#d77d31")} ' + \
f'Фиолетовых точек: {np.count_nonzero(self.colors_palate == "#700961")}')
self.ax3.legend()
plt.pause(0.1)
def _pressed_molecul(self, event):
ind = event.ind[0]; x, y, z = self.vels[ind]
self.ax3.set_title('График проекций скоростей' + \
f'\nВсего точек: {self.colors_palate.size} ' + \
f'Рыжих точек: {np.count_nonzero(self.colors_palate == "#d77d31")} ' + \
f'Фиолетовых точек: {np.count_nonzero(self.colors_palate == "#700961")}' + \
f'\n(Скорость вашей точки: $V$ = {round((x*x + y*y + z*z)**0.5, 1)} $м/с$)')
self.scatter.set_color(['r' if i == ind else c for i, c in zip(range(len(self.vels)), self.colors_palate)])
def _update_plot(self, end=''):
if not end:
self.ax1[0].clear()
sp = np.round(np.sqrt(self.vels[:,0]*self.vels[:,0] + self.vels[:,1]*self.vels[:,1] + self.vels[:,2]*self.vels[:,2]), 1)
sl = {i: count for i, count in zip(*np.unique(sp, return_counts=True))}
self.ax1[0].bar(sl.keys(), sl.values())
self.ax1[0].plot((0, (n:=list(sl.keys())[list(sl.values()).index((j:=max(sl.values())))])),(j,j), '--r',
label=f'$Vн.в = {n}м/с$')
self.ax1[0].axhline(0, color='red')
self.ax1[0].axvline(0, color='red')
self.ax1[0].legend(); self.ax1[0].grid()
self.ax1[0].set_xlabel("Скорость $(м/с)$")
self.ax1[0].set_ylabel("Количество молекул")
if end:
endi, sl = end
#self.ax1[1].bar(sl.keys(), sl.values())
#print(len(list(sl.keys())), len(list(sl.values())))
self.ax1[1].stairs(sl.values(), list(sl.keys())+[1], fill=1)
self.ax1[1].plot((0, endi),(sl[endi],sl[endi]), '--r', label=f'$Vн.в = {endi}м/с$')
self.ax1[1].axhline(0, color='red'); self.ax1[1].axvline(0, color='red')
self.ax1[1].legend(); self.ax1[1].grid()
self.ax1[1].set_xlabel("Скорость $(м/с)$")
self.ax1[1].set_ylabel("Количество молекул")
plt.pause(0.1)
def _plt_if(self, end):
endi, sl = end
self.ax4.stairs(sl.values(), list(sl.keys())+[1], fill=1)
self.ax4.plot((0, endi),(sl[endi],sl[endi]), '--r', label=f'$Vн.в = {endi}м/с$')
self.ax4.axhline(0, color='red'); self.ax4.axvline(0, color='red')
self.ax4.legend(); self.ax4.grid()
self.ax4.set_xlabel("Скорость $(м/с)$")
self.ax4.set_ylabel("Количество молекул")
def _on_resize(self, event):
# Минимальные размеры окна (в данном случае, 400x300)
min_width = 900
min_height = 700
# Получаем текущие размеры окна
current_width = self.PIG.get_size_inches()[0] * self.PIG.dpi
current_height = self.PIG.get_size_inches()[1] * self.PIG.dpi
# Если текущие размеры меньше минимальных, устанавливаем минимальные размеры
if current_width < min_width or current_height < min_height:
self.PIG.set_size_inches(min_width / self.PIG.dpi, min_height / self.PIG.dpi)
self.PIG.canvas.draw()
def run_animation(a):
molecular_motion(*a)
plt.show()
На всякий случай вставлю новые ссылки на эти коды: https://dpaste.org/ayj1M https://dpaste.org/yEgag
Ответы (1 шт):
Совет insolor был полезным, с помощью freeze_support я заморозил часть кода, тем самым предотвратил запускание окон, вместо окон как и предполагалось в программе показываются графики параллельно друг другу. Вот ссылки на коды в них добавлена эта функция (с помощью этих кодов можно смоделировать молекулярное движение, кому-то это может быть полезно): https://dpaste.org/LPQQX https://dpaste.org/Gs1P9