На клиенте есть маршрут до сервера, где локальный (российский) конец туннеля.
import sys
from pathlib import Path
# __file__ и sys.argv[0] могут не совпадать для скриптов из нескольких файлов.
script_path = Path(sys.argv[0]).resolve()
dotenv_path = script_path.parent / '.env'
script_name = script_path.stem
log_dir = script_path.parent
log_file = log_dir / (script_name + '.log')
rects_to_send = []
for rect in rectangles:
rects_to_send.append( [rect.x, rect.y, rect.w, rect.h] )
Проще всего будет, если ты завернёшь решение в функцию, которая принимает простую структуру данных и возвращает простую структуру данных. Скажем, принимает строку из файла - возвращает кортеж чисел. Тогда через multiprocessing.Pool.map() можно будет достаточно просто распихать вызовы этой функции по процессам, а потом собрать обратно их результаты.
Насчёт солвера: numpy из коробки умеет только систему линейных уравнений решать, а у тебя, кажется, квадратичная, пусть и с двумя переменными (уравнение для z у тебя тривиальное, так что его значение ты вроде как знаешь). Свести к линейной системе через замену t = x^2 твои уравнения вроде не получится (хотя я могу быть и не прав).
А раз так, смотри в сторону пакета scipy, в частности, scipy.optimize.fsolve(). Проблема в том, что численным методам нужна какая-то "точка отсчёта" - начальное приближение для корней системы. Как его выбрать - смотри сам. Может, есть какое-то осмысленное значение, может, банальный 0;0 сойдёт.
Ещё один вариант - заменить на numpy расчёты с матрицами, уж с этим-то он справится. А вот поиск решения оставить на sympy. Но тут надо замерять, сколько времени уходит на эти шаги, и есть ли смысл в такой экономии.