diff --git a/examples/__init__.py b/examples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/rocket_drone_lqr/README.md b/examples/rocket_drone_lqr/README.md new file mode 100644 index 0000000..205d4e9 --- /dev/null +++ b/examples/rocket_drone_lqr/README.md @@ -0,0 +1,214 @@ +# Rocket Drone LQR + +Реализация GitHub Issue #34: + +**[control] 2d rocket dron** + +Дисциплина: **Программирование роботов (ИТМО)** + +Алгоритм управления: **LQR (Linear Quadratic Regulator)** + +Протокол обмена: **ZeroMQ** + +# Цель проекта + +Разработать систему управления двухмерной ракетой, способной отслеживать заданную траекторию в плоскости X–Z. + +Управление осуществляется через реальные силы двигателей: + +- **F1** — основной двигатель +- **F2** — левый боковой двигатель +- **F3** — правый боковой двигатель + +Ракета компенсирует действие силы тяжести и следует заданной траектории. + +# Математическая модель + +### Состояние ракеты + +```text +x положение по оси X +z положение по оси Z + +theta угол корпуса + +vx скорость по X +vz скорость по Z + +omega угловая скорость +``` + +### Управление + +```text +F1 основная тяга +F2 левый боковой двигатель +F3 правый боковой двигатель +``` + +В динамическую модель передаются реальные силы двигателей. + +# Архитектура системы + +```text +Trajectory Generator + │ + ▼ + LQR Controller + │ + ▼ + Control Allocation + │ + ▼ + F1 F2 F3 + │ + ▼ + Rocket Dynamics + │ + ▼ + State + │ + ▼ + ZeroMQ + │ + ▼ + LQR Controller +``` + +# Структура проекта + +```text +rocket_drone_lqr/ +│ +├── communication/ +│ ├── messages.py +│ └── zmq_protocol.py +│ +├── control/ +│ └── lqr_controller.py +│ +├── sim/ +│ ├── dynamics.py +│ ├── limits.py +│ └── trajectory.py +│ +├── tests/ +│ ├── test_dynamics.py +│ ├── test_lqr_controller.py +│ ├── test_messages.py +│ └── test_zmq_protocol.py +│ +├── demo_closed_loop.py +├── run_controller.py +└── run_simulator.py +``` + +# Локальный запуск + +Запуск симуляции без разделения процессов: + +```bash +python -m examples.rocket_drone_lqr.demo_closed_loop +``` + +# Запуск через ZeroMQ + +Окно №1: + +```bash +python -m examples.rocket_drone_lqr.run_controller +``` + +Окно №2: + +```bash +python -m examples.rocket_drone_lqr.run_simulator +``` + +Контроллер и симулятор работают как независимые процессы и обмениваются сообщениями через ZeroMQ. + +# Траектория + +Используется траектория типа **«восьмёрка» (Figure Eight)**. + +Контроллер отслеживает: + +```text +x_ref +z_ref +vx_ref +vz_ref +``` + +# Тестирование + +Запуск тестов: + +```bash +uv run pytest examples/rocket_drone_lqr/tests +``` + +Результат: + +```text +6 passed +``` + +Покрыты проверки: + +- сериализации сообщений; +- динамической модели ракеты; +- LQR-контроллера; +- ZeroMQ-протокола. + +# Реализованные возможности + +- физическая модель ракеты; +- учёт силы тяжести; +- генератор траектории «восьмёрка»; +- LQR-регулятор; +- преобразование управляющих воздействий в силы **F1**, **F2**, **F3**; +- обмен сообщениями через ZeroMQ; +- запуск контроллера отдельным процессом; +- запуск симулятора отдельным процессом; +- визуализация движения; +- автоматические тесты. + +# Алгоритм управления + +В проекте используется **Linear Quadratic Regulator (LQR)**. + +PID- и PD-регуляторы не используются, так как условия Issue #34 требуют применения более продвинутого алгоритма управления. + +LQR вычисляет оптимальные управляющие воздействия на основе: + +- текущего состояния ракеты; +- требуемого состояния на траектории. + +После этого управляющие воздействия преобразуются в реальные силы двигателей **F1**, **F2** и **F3**. + +# Протокол обмена + +Используется схема **ZeroMQ REQ/REP**. + +Формат сообщений: + +```text +ControlRequest + time + state + reference + +ControlResponse + F1 + F2 + F3 +``` + +# Статус проекта + +- **GitHub Issue:** #34 +- **Алгоритм управления:** LQR +- **Протокол обмена:** ZeroMQ +- **Автоматические тесты:** пройдены (6 passed) +- **Статус:** реализовано diff --git a/examples/rocket_drone_lqr/communication/__init__.py b/examples/rocket_drone_lqr/communication/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/rocket_drone_lqr/communication/messages.py b/examples/rocket_drone_lqr/communication/messages.py new file mode 100644 index 0000000..36b907d --- /dev/null +++ b/examples/rocket_drone_lqr/communication/messages.py @@ -0,0 +1,41 @@ +from dataclasses import asdict, dataclass +from typing import Any + +from examples.rocket_drone_lqr.sim.dynamics import RocketForces, RocketState +from examples.rocket_drone_lqr.sim.trajectory import TrajectoryPoint + + +@dataclass(frozen=True) +class ControlRequest: + time: float + state: RocketState + reference: TrajectoryPoint + + def to_dict(self) -> dict[str, Any]: + return { + "time": self.time, + "state": asdict(self.state), + "reference": asdict(self.reference), + } + + @staticmethod + def from_dict(data: dict[str, Any]) -> "ControlRequest": + return ControlRequest( + time=float(data["time"]), + state=RocketState(**data["state"]), + reference=TrajectoryPoint(**data["reference"]), + ) + + +@dataclass(frozen=True) +class ControlResponse: + forces: RocketForces + + def to_dict(self) -> dict[str, Any]: + return {"forces": asdict(self.forces)} + + @staticmethod + def from_dict(data: dict[str, Any]) -> "ControlResponse": + return ControlResponse( + forces=RocketForces(**data["forces"]), + ) \ No newline at end of file diff --git a/examples/rocket_drone_lqr/communication/zmq_protocol.py b/examples/rocket_drone_lqr/communication/zmq_protocol.py new file mode 100644 index 0000000..5acc1a9 --- /dev/null +++ b/examples/rocket_drone_lqr/communication/zmq_protocol.py @@ -0,0 +1,32 @@ +import json +from typing import Any + +import zmq + +DEFAULT_ENDPOINT = "tcp://127.0.0.1:5557" + + +class ZmqServer: + def __init__(self, endpoint: str = DEFAULT_ENDPOINT): + self.context = zmq.Context.instance() + self.socket = self.context.socket(zmq.REP) + self.socket.bind(endpoint) + + def receive(self) -> dict[str, Any]: + raw = self.socket.recv_string() + return json.loads(raw) + + def send(self, message: dict[str, Any]) -> None: + self.socket.send_string(json.dumps(message)) + + +class ZmqClient: + def __init__(self, endpoint: str = DEFAULT_ENDPOINT): + self.context = zmq.Context.instance() + self.socket = self.context.socket(zmq.REQ) + self.socket.connect(endpoint) + + def request(self, message: dict[str, Any]) -> dict[str, Any]: + self.socket.send_string(json.dumps(message)) + raw = self.socket.recv_string() + return json.loads(raw) \ No newline at end of file diff --git a/examples/rocket_drone_lqr/control/__init__.py b/examples/rocket_drone_lqr/control/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/rocket_drone_lqr/control/limits.py b/examples/rocket_drone_lqr/control/limits.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/rocket_drone_lqr/control/lqr_controller.py b/examples/rocket_drone_lqr/control/lqr_controller.py new file mode 100644 index 0000000..0d60d12 --- /dev/null +++ b/examples/rocket_drone_lqr/control/lqr_controller.py @@ -0,0 +1,74 @@ +import numpy as np +from scipy.linalg import solve_continuous_are + +from examples.rocket_drone_lqr.sim.dynamics import RocketForces, RocketState +from examples.rocket_drone_lqr.sim.limits import RocketLimits +from examples.rocket_drone_lqr.sim.trajectory import TrajectoryPoint + + +class LQRController: + def __init__(self, limits: RocketLimits | None = None): + self.limits = limits or RocketLimits() + + self.k_x = self._lqr(3.0, 2.0, 5.0) + self.k_z = self._lqr(10.0, 7.0, 2.0) + + self.max_side_force = 0.55 + self.max_tilt = 0.28 + self.tilt_kp = 3.0 + self.tilt_kd = 1.2 + + def _lqr(self, q_pos: float, q_vel: float, r_val: float) -> np.ndarray: + a = np.array([[0.0, 1.0], [0.0, 0.0]], dtype=float) + b = np.array([[0.0], [1.0]], dtype=float) + q = np.diag([q_pos, q_vel]) + r = np.array([[r_val]], dtype=float) + p = solve_continuous_are(a, b, q, r) + return np.linalg.inv(r) @ b.T @ p + + def control(self, state: RocketState, reference: TrajectoryPoint) -> RocketForces: + x_error = np.array( + [state.x - reference.x, state.vx - reference.vx], + dtype=float, + ) + z_error = np.array( + [state.z - reference.z, state.vz - reference.vz], + dtype=float, + ) + + ax_cmd = float(reference.ax - (self.k_x @ x_error)[0]) + az_cmd = float(reference.az - (self.k_z @ z_error)[0]) + + ax_cmd = float(np.clip(ax_cmd, -1.4, 1.4)) + az_cmd = float(np.clip(az_cmd, -2.0, 2.0)) + + f1 = self.limits.mass * (self.limits.gravity + az_cmd) + f1 = float(np.clip(f1, self.limits.f1_min, self.limits.f1_max)) + + side_cmd = 0.7 * reference.vx + 0.25 * (reference.x - state.x) + + if state.theta > self.max_tilt: + side_cmd -= self.tilt_kp * (state.theta - self.max_tilt) + + if state.theta < -self.max_tilt: + side_cmd -= self.tilt_kp * (state.theta + self.max_tilt) + + side_cmd -= self.tilt_kd * state.omega + + side_cmd = float(np.clip( + side_cmd, + -self.max_side_force, + self.max_side_force, + )) + if abs(state.theta) > 0.45: + side_cmd = 0.0 + + # F2 — вправо, F3 — влево. + if side_cmd > 0.0: + f2 = side_cmd + f3 = 0.0 + else: + f2 = 0.0 + f3 = -side_cmd + + return RocketForces(f1=f1, f2=f2, f3=f3).clipped(self.limits) \ No newline at end of file diff --git a/examples/rocket_drone_lqr/control/trajectory.py b/examples/rocket_drone_lqr/control/trajectory.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/rocket_drone_lqr/demo_closed_loop.py b/examples/rocket_drone_lqr/demo_closed_loop.py new file mode 100644 index 0000000..4ebf7a7 --- /dev/null +++ b/examples/rocket_drone_lqr/demo_closed_loop.py @@ -0,0 +1,141 @@ +import math + +import matplotlib.animation as animation +import matplotlib.pyplot as plt + +from examples.rocket_drone_lqr.control.lqr_controller import LQRController +from examples.rocket_drone_lqr.sim.dynamics import RocketDynamics, RocketState +from examples.rocket_drone_lqr.sim.limits import RocketLimits +from examples.rocket_drone_lqr.sim.trajectory import FigureEightTrajectory + + +def rocket_shape(x: float, z: float, theta: float) -> tuple[list[float], list[float]]: + body = [ + (0.0, 0.45), + (-0.18, -0.25), + (0.18, -0.25), + (0.0, 0.45), + ] + + xs = [] + zs = [] + + for bx, bz in body: + rx = bx * math.cos(theta) - bz * math.sin(theta) + rz = bx * math.sin(theta) + bz * math.cos(theta) + xs.append(x + rx) + zs.append(z + rz) + + return xs, zs + + +def main() -> None: + limits = RocketLimits() + dynamics = RocketDynamics(limits) + controller = LQRController(limits) + trajectory = FigureEightTrajectory() + + start_reference = trajectory.get(0.0) + + state = RocketState( + x=start_reference.x, + z=start_reference.z, + theta=0.0, + vx=start_reference.vx, + vz=start_reference.vz, + omega=0.0, + ) + simulation_time = 70.0 + steps = int(simulation_time / limits.dt) + + ref_x = [] + ref_z = [] + real_x = [] + real_z = [] + states = [] + forces_log = [] + + for step in range(steps): + t = step * limits.dt + reference = trajectory.get(t) + forces = controller.control(state, reference) + state = dynamics.step(state, forces) + + ref_x.append(reference.x) + ref_z.append(reference.z) + real_x.append(state.x) + real_z.append(state.z) + states.append(state) + forces_log.append(forces) + + fig, ax = plt.subplots(figsize=(10, 7)) + + ax.set_title("2D Rocket Drone LQR trajectory tracking") + ax.set_xlabel("x") + ax.set_ylabel("z") + ax.set_xlim(limits.x_min, limits.x_max) + ax.set_ylim(limits.z_min, limits.z_max) + ax.grid(True) + ax.set_aspect("equal", adjustable="box") + + ax.plot(ref_x, ref_z, "--", label="reference trajectory") + real_line, = ax.plot([], [], label="rocket trajectory") + rocket_line, = ax.plot([], [], linewidth=2, label="rocket body") + thrust_line, = ax.plot([], [], linewidth=2, label="main thrust") + text = ax.text(0.02, 0.95, "", transform=ax.transAxes) + + ax.legend(loc="upper right") + + frame_stride = 5 + frame_count = len(states) // frame_stride + + def update(frame: int): + index = frame * frame_stride + current_state = states[index] + current_forces = forces_log[index] + current_time = index * limits.dt + + real_line.set_data(real_x[:index], real_z[:index]) + + xs, zs = rocket_shape( + current_state.x, + current_state.z, + current_state.theta, + ) + rocket_line.set_data(xs, zs) + + thrust_scale = 0.035 * current_forces.f1 + thrust_x = [ + current_state.x, + current_state.x + thrust_scale * math.sin(current_state.theta), + ] + thrust_z = [ + current_state.z - 0.3, + current_state.z - 0.3 - thrust_scale * math.cos(current_state.theta), + ] + thrust_line.set_data(thrust_x, thrust_z) + + text.set_text( + f"t = {current_time:.1f} s\n" + f"x = {current_state.x:.2f}, z = {current_state.z:.2f}\n" + f"theta = {current_state.theta:.2f} rad\n" + f"F1 = {current_forces.f1:.2f}, " + f"F2 = {current_forces.f2:.2f}, " + f"F3 = {current_forces.f3:.2f}" + ) + + return real_line, rocket_line, thrust_line, text + + ani = animation.FuncAnimation( + fig, + update, + frames=frame_count, + interval=20, + blit=True, + ) + + plt.show() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/rocket_drone_lqr/run_controller.py b/examples/rocket_drone_lqr/run_controller.py new file mode 100644 index 0000000..78d7ddb --- /dev/null +++ b/examples/rocket_drone_lqr/run_controller.py @@ -0,0 +1,33 @@ +from examples.rocket_drone_lqr.communication.messages import ( + ControlRequest, + ControlResponse, +) +from examples.rocket_drone_lqr.communication.zmq_protocol import ZmqServer +from examples.rocket_drone_lqr.control.lqr_controller import LQRController + + +def main() -> None: + controller = LQRController() + server = ZmqServer() + + print("Rocket controller started") + + while True: + message = server.receive() + + request = ControlRequest.from_dict(message) + + forces = controller.control( + request.state, + request.reference, + ) + + response = ControlResponse( + forces=forces, + ) + + server.send(response.to_dict()) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/rocket_drone_lqr/run_simulator.py b/examples/rocket_drone_lqr/run_simulator.py new file mode 100644 index 0000000..f3aef84 --- /dev/null +++ b/examples/rocket_drone_lqr/run_simulator.py @@ -0,0 +1,147 @@ +import math + +import matplotlib.animation as animation +import matplotlib.pyplot as plt + +from examples.rocket_drone_lqr.communication.messages import ( + ControlRequest, + ControlResponse, +) +from examples.rocket_drone_lqr.communication.zmq_protocol import ZmqClient +from examples.rocket_drone_lqr.sim.dynamics import RocketDynamics, RocketState +from examples.rocket_drone_lqr.sim.limits import RocketLimits +from examples.rocket_drone_lqr.sim.trajectory import FigureEightTrajectory + + +def rocket_shape(x: float, z: float, theta: float) -> tuple[list[float], list[float]]: + body = [(0.0, 0.45), (-0.18, -0.25), (0.18, -0.25), (0.0, 0.45)] + xs, zs = [], [] + + for bx, bz in body: + rx = bx * math.cos(theta) - bz * math.sin(theta) + rz = bx * math.sin(theta) + bz * math.cos(theta) + xs.append(x + rx) + zs.append(z + rz) + + return xs, zs + + +def main() -> None: + limits = RocketLimits() + dynamics = RocketDynamics(limits) + trajectory = FigureEightTrajectory() + client = ZmqClient() + + start_reference = trajectory.get(0.0) + + state = RocketState( + x=start_reference.x, + z=start_reference.z, + theta=0.0, + vx=start_reference.vx, + vz=start_reference.vz, + omega=0.0, + ) + + simulation_time = 70.0 + steps = int(simulation_time / limits.dt) + + ref_x, ref_z = [], [] + real_x, real_z = [], [] + states = [] + forces_log = [] + + for step in range(steps): + t = step * limits.dt + reference = trajectory.get(t) + + request = ControlRequest( + time=t, + state=state, + reference=reference, + ) + + response_data = client.request(request.to_dict()) + response = ControlResponse.from_dict(response_data) + + forces = response.forces + state = dynamics.step(state, forces) + + ref_x.append(reference.x) + ref_z.append(reference.z) + real_x.append(state.x) + real_z.append(state.z) + states.append(state) + forces_log.append(forces) + + fig, ax = plt.subplots(figsize=(10, 7)) + + ax.set_title("2D Rocket Drone LQR trajectory tracking via ZeroMQ") + ax.set_xlabel("x") + ax.set_ylabel("z") + ax.set_xlim(limits.x_min, limits.x_max) + ax.set_ylim(limits.z_min, limits.z_max) + ax.grid(True) + ax.set_aspect("equal", adjustable="box") + + ax.plot(ref_x, ref_z, "--", label="reference trajectory") + real_line, = ax.plot([], [], label="rocket trajectory") + rocket_line, = ax.plot([], [], linewidth=2, label="rocket body") + thrust_line, = ax.plot([], [], linewidth=2, label="main thrust") + text = ax.text(0.02, 0.95, "", transform=ax.transAxes) + + ax.legend(loc="upper right") + + frame_stride = 5 + frame_count = len(states) // frame_stride + + def update(frame: int): + index = frame * frame_stride + current_state = states[index] + current_forces = forces_log[index] + current_time = index * limits.dt + + real_line.set_data(real_x[:index], real_z[:index]) + + xs, zs = rocket_shape( + current_state.x, + current_state.z, + current_state.theta, + ) + rocket_line.set_data(xs, zs) + + thrust_scale = 0.035 * current_forces.f1 + thrust_x = [ + current_state.x, + current_state.x + thrust_scale * math.sin(current_state.theta), + ] + thrust_z = [ + current_state.z - 0.3, + current_state.z - 0.3 - thrust_scale * math.cos(current_state.theta), + ] + thrust_line.set_data(thrust_x, thrust_z) + + text.set_text( + f"t = {current_time:.1f} s\n" + f"x = {current_state.x:.2f}, z = {current_state.z:.2f}\n" + f"theta = {current_state.theta:.2f} rad\n" + f"F1 = {current_forces.f1:.2f}, " + f"F2 = {current_forces.f2:.2f}, " + f"F3 = {current_forces.f3:.2f}" + ) + + return real_line, rocket_line, thrust_line, text + + ani = animation.FuncAnimation( + fig, + update, + frames=frame_count, + interval=20, + blit=True, + ) + + plt.show() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/rocket_drone_lqr/sim/__init__.py b/examples/rocket_drone_lqr/sim/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/rocket_drone_lqr/sim/dynamics.py b/examples/rocket_drone_lqr/sim/dynamics.py new file mode 100644 index 0000000..9e35430 --- /dev/null +++ b/examples/rocket_drone_lqr/sim/dynamics.py @@ -0,0 +1,114 @@ +import math +from dataclasses import dataclass + +import numpy as np + +from examples.rocket_drone_lqr.sim.limits import RocketLimits + + +@dataclass +class RocketState: + x: float + z: float + theta: float + vx: float + vz: float + omega: float + + def as_vector(self) -> np.ndarray: + return np.array( + [self.x, self.z, self.theta, self.vx, self.vz, self.omega], + dtype=float, + ) + + @staticmethod + def from_vector(vector: np.ndarray) -> "RocketState": + return RocketState( + x=float(vector[0]), + z=float(vector[1]), + theta=float(vector[2]), + vx=float(vector[3]), + vz=float(vector[4]), + omega=float(vector[5]), + ) + + +@dataclass +class RocketForces: + f1: float + f2: float + f3: float + + def clipped(self, limits: RocketLimits) -> "RocketForces": + return RocketForces( + f1=float(np.clip(self.f1, limits.f1_min, limits.f1_max)), + f2=float(np.clip(self.f2, limits.f2_min, limits.f2_max)), + f3=float(np.clip(self.f3, limits.f3_min, limits.f3_max)), + ) + + +class RocketDynamics: + def __init__(self, limits: RocketLimits | None = None): + self.limits = limits or RocketLimits() + self.angular_damping = 0.7 + self.side_torque_gain = 0.01 + + def derivatives(self, state: RocketState, forces: RocketForces) -> np.ndarray: + limits = self.limits + forces = forces.clipped(limits) + + sin_theta = math.sin(state.theta) + cos_theta = math.cos(state.theta) + + nose_x = -sin_theta + nose_z = cos_theta + + side_x = cos_theta + side_z = sin_theta + + main_force = forces.f1 + + # F2 толкает влево, F3 толкает вправо. + side_force = forces.f2 - forces.f3 + + torque = ( + self.side_torque_gain * limits.body_half_width * side_force + - self.angular_damping * state.omega + ) + + fx = main_force * nose_x + side_force * side_x + fz = main_force * nose_z + side_force * side_z + + ax = fx / limits.mass + az = fz / limits.mass - limits.gravity + alpha = torque / limits.inertia + + return np.array( + [ + state.vx, + state.vz, + state.omega, + ax, + az, + alpha, + ], + dtype=float, + ) + + def step(self, state: RocketState, forces: RocketForces) -> RocketState: + dt = self.limits.dt + + y = state.as_vector() + k1 = self.derivatives(RocketState.from_vector(y), forces) + k2 = self.derivatives(RocketState.from_vector(y + 0.5 * dt * k1), forces) + k3 = self.derivatives(RocketState.from_vector(y + 0.5 * dt * k2), forces) + k4 = self.derivatives(RocketState.from_vector(y + dt * k3), forces) + + new_y = y + (dt / 6.0) * (k1 + 2 * k2 + 2 * k3 + k4) + + new_y[0] = np.clip(new_y[0], self.limits.x_min, self.limits.x_max) + new_y[1] = np.clip(new_y[1], self.limits.z_min, self.limits.z_max) + new_y[2] = (new_y[2] + math.pi) % (2.0 * math.pi) - math.pi + new_y[5] = np.clip(new_y[5], -self.limits.omega_max, self.limits.omega_max) + + return RocketState.from_vector(new_y) \ No newline at end of file diff --git a/examples/rocket_drone_lqr/sim/limits.py b/examples/rocket_drone_lqr/sim/limits.py new file mode 100644 index 0000000..55d0fcd --- /dev/null +++ b/examples/rocket_drone_lqr/sim/limits.py @@ -0,0 +1,28 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True) +class RocketLimits: + dt: float = 0.02 + + mass: float = 1.0 + inertia: float = 0.08 + gravity: float = 9.81 + + body_half_width: float = 0.25 + + f1_min: float = 0.0 + f1_max: float = 18.0 + + f2_min: float = 0.0 + f2_max: float = 10.0 + + f3_min: float = 0.0 + f3_max: float = 10.0 + + x_min: float = -8.0 + x_max: float = 8.0 + z_min: float = 0.0 + z_max: float = 8.0 + + omega_max: float = 12.0 \ No newline at end of file diff --git a/examples/rocket_drone_lqr/sim/trajectory.py b/examples/rocket_drone_lqr/sim/trajectory.py new file mode 100644 index 0000000..91b8cf2 --- /dev/null +++ b/examples/rocket_drone_lqr/sim/trajectory.py @@ -0,0 +1,45 @@ +import math +from dataclasses import dataclass + + +@dataclass +class TrajectoryPoint: + x: float + z: float + vx: float + vz: float + ax: float + az: float + theta: float + + +class FigureEightTrajectory: + def __init__( + self, + center_x: float = 0.0, + center_z: float = 4.0, + amplitude_x: float = 3.0, + amplitude_z: float = 1.4, + period: float = 70.0, + ): + self.center_x = center_x + self.center_z = center_z + self.amplitude_x = amplitude_x + self.amplitude_z = amplitude_z + self.omega = 2.0 * math.pi / period + + def get(self, t: float) -> TrajectoryPoint: + s = self.omega * t + + x = self.center_x + self.amplitude_x * math.sin(s) + z = self.center_z + self.amplitude_z * math.sin(2.0 * s) + + vx = self.amplitude_x * self.omega * math.cos(s) + vz = 2.0 * self.amplitude_z * self.omega * math.cos(2.0 * s) + + ax = -self.amplitude_x * self.omega**2 * math.sin(s) + az = -4.0 * self.amplitude_z * self.omega**2 * math.sin(2.0 * s) + + theta = 0.0 + + return TrajectoryPoint(x=x, z=z, vx=vx, vz=vz, ax=ax, az=az, theta=theta) \ No newline at end of file diff --git a/examples/rocket_drone_lqr/tests/__init__.py b/examples/rocket_drone_lqr/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/rocket_drone_lqr/tests/conftest.py b/examples/rocket_drone_lqr/tests/conftest.py new file mode 100644 index 0000000..b9ef53c --- /dev/null +++ b/examples/rocket_drone_lqr/tests/conftest.py @@ -0,0 +1,8 @@ +import sys +from pathlib import Path + + +PROJECT_ROOT = Path(__file__).resolve().parents[3] + +if str(PROJECT_ROOT) not in sys.path: + sys.path.insert(0, str(PROJECT_ROOT)) \ No newline at end of file diff --git a/examples/rocket_drone_lqr/tests/test_dynamics.py b/examples/rocket_drone_lqr/tests/test_dynamics.py new file mode 100644 index 0000000..3838638 --- /dev/null +++ b/examples/rocket_drone_lqr/tests/test_dynamics.py @@ -0,0 +1,31 @@ +from examples.rocket_drone_lqr.sim.dynamics import ( + RocketDynamics, + RocketForces, + RocketState, +) +from examples.rocket_drone_lqr.sim.limits import RocketLimits + + +def test_rocket_hover_with_gravity_compensation(): + limits = RocketLimits() + dynamics = RocketDynamics(limits) + + state = RocketState( + x=0.0, + z=4.0, + theta=0.0, + vx=0.0, + vz=0.0, + omega=0.0, + ) + + forces = RocketForces( + f1=limits.mass * limits.gravity, + f2=0.0, + f3=0.0, + ) + + next_state = dynamics.step(state, forces) + + assert abs(next_state.z - state.z) < 1e-3 + assert abs(next_state.vz) < 1e-3 \ No newline at end of file diff --git a/examples/rocket_drone_lqr/tests/test_lqr_controller.py b/examples/rocket_drone_lqr/tests/test_lqr_controller.py new file mode 100644 index 0000000..961b794 --- /dev/null +++ b/examples/rocket_drone_lqr/tests/test_lqr_controller.py @@ -0,0 +1,62 @@ +from examples.rocket_drone_lqr.control.lqr_controller import LQRController +from examples.rocket_drone_lqr.sim.dynamics import RocketState +from examples.rocket_drone_lqr.sim.limits import RocketLimits +from examples.rocket_drone_lqr.sim.trajectory import TrajectoryPoint + + +def test_lqr_controller_returns_limited_forces(): + limits = RocketLimits() + controller = LQRController(limits) + + state = RocketState( + x=0.0, + z=4.0, + theta=0.0, + vx=0.0, + vz=0.0, + omega=0.0, + ) + + reference = TrajectoryPoint( + x=1.0, + z=4.2, + vx=0.2, + vz=0.0, + ax=0.0, + az=0.0, + theta=0.0, + ) + + forces = controller.control(state, reference) + + assert limits.f1_min <= forces.f1 <= limits.f1_max + assert limits.f2_min <= forces.f2 <= limits.f2_max + assert limits.f3_min <= forces.f3 <= limits.f3_max + + +def test_lqr_controller_uses_f2_for_right_motion(): + controller = LQRController() + + state = RocketState( + x=0.0, + z=4.0, + theta=0.0, + vx=0.0, + vz=0.0, + omega=0.0, + ) + + reference = TrajectoryPoint( + x=1.0, + z=4.0, + vx=0.5, + vz=0.0, + ax=0.0, + az=0.0, + theta=0.0, + ) + + forces = controller.control(state, reference) + + assert forces.f2 > 0.0 + assert forces.f3 == 0.0 \ No newline at end of file diff --git a/examples/rocket_drone_lqr/tests/test_messages.py b/examples/rocket_drone_lqr/tests/test_messages.py new file mode 100644 index 0000000..f72492b --- /dev/null +++ b/examples/rocket_drone_lqr/tests/test_messages.py @@ -0,0 +1,36 @@ +from examples.rocket_drone_lqr.communication.messages import ( + ControlRequest, + ControlResponse, +) +from examples.rocket_drone_lqr.sim.dynamics import RocketForces, RocketState +from examples.rocket_drone_lqr.sim.trajectory import TrajectoryPoint + + +def test_control_request_roundtrip(): + request = ControlRequest( + time=1.5, + state=RocketState(x=1.0, z=2.0, theta=0.1, vx=0.2, vz=0.3, omega=0.4), + reference=TrajectoryPoint( + x=1.1, + z=2.1, + vx=0.2, + vz=0.3, + ax=0.0, + az=0.0, + theta=0.0, + ), + ) + + restored = ControlRequest.from_dict(request.to_dict()) + + assert restored == request + + +def test_control_response_roundtrip(): + response = ControlResponse( + forces=RocketForces(f1=9.8, f2=0.5, f3=0.1), + ) + + restored = ControlResponse.from_dict(response.to_dict()) + + assert restored == response \ No newline at end of file diff --git a/examples/rocket_drone_lqr/tests/test_zmq_protocol.py b/examples/rocket_drone_lqr/tests/test_zmq_protocol.py new file mode 100644 index 0000000..9970efb --- /dev/null +++ b/examples/rocket_drone_lqr/tests/test_zmq_protocol.py @@ -0,0 +1,26 @@ +import threading +import time + +from examples.rocket_drone_lqr.communication.zmq_protocol import ZmqClient, ZmqServer + + +def test_zmq_request_response_roundtrip(): + endpoint = "tcp://127.0.0.1:5567" + received = {} + + def server_task(): + server = ZmqServer(endpoint) + message = server.receive() + received.update(message) + server.send({"ok": True, "value": message["value"]}) + + thread = threading.Thread(target=server_task, daemon=True) + thread.start() + + time.sleep(0.1) + + client = ZmqClient(endpoint) + response = client.request({"value": 42}) + + assert response == {"ok": True, "value": 42} + assert received == {"value": 42} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 370ab92..f1e29a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,9 @@ dependencies = [ "numpy>=2.4.4", "pre-commit>=4.6.0", "pytest>=9.0.3", + "pyzmq>=27.1.0", "ruff>=0.15.12", + "scipy>=1.18.0", ] [tool.ruff.lint] diff --git a/uv.lock b/uv.lock index 2eff7b6..8c7783f 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,63 @@ version = 1 revision = 3 requires-python = ">=3.12" +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + [[package]] name = "cfgv" version = "3.5.0" @@ -169,6 +226,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, ] +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -253,6 +319,49 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, +] + [[package]] name = "ruff" version = "0.15.12" @@ -278,6 +387,57 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" }, ] +[[package]] +name = "scipy" +version = "1.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/19/ca10ead60b0acc80b2b833c2c4a4f2ff753d0f58b811f70d911c7e94a25c/scipy-1.18.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd21faaf5a1a3b2eff922d02db5f191b99a6518db9078a8fb23169f6d22259a", size = 31056519, upload-time = "2026-06-19T14:59:45.203Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1e6442a00cd2924d361aa1b642ab6373ec35c6fabf311a760be9f76e0f13/scipy-1.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:265915e79107de9f946b855e50d7470d5893ec3f54b342e1aa6201cbdcd8bb6b", size = 28681889, upload-time = "2026-06-19T14:59:48.103Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2d/11dd93d21e147a73ba22bd75c0b9208d3a2e0ec76d53170ce7d9029b1015/scipy-1.18.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9ab7b758be6940954a713ee466e2043e9f6e2ed965c1fce5c91039f4be3d90a9", size = 20423580, upload-time = "2026-06-19T14:59:50.665Z" }, + { url = "https://files.pythonhosted.org/packages/9c/01/93552f75e0d2a7dd115a45e59209c51e8d514daff02fc887d2623be06fe1/scipy-1.18.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:97b6cddaaee0a779ef6b5ca83c9604b27cc16b2b8fc22c142652df8793319fb8", size = 23054441, upload-time = "2026-06-19T14:59:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/3c/23/21f5e703643d66f21faa6b4c73195bfcad70c55efcb4f1ab327cd7c4101a/scipy-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52a96e21517c7292375c0e27dd796a811f03fcea5fd4d108fdfea8145dcf17ab", size = 33968720, upload-time = "2026-06-19T14:59:56.415Z" }, + { url = "https://files.pythonhosted.org/packages/dd/aa/1b939f6c67ed68635bb538e6752d3dacc02f66535182e939a89581a44e9c/scipy-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55797419e16e7f30cf88ffb3113ce0467f00cfe3f70d5c281730b21769bfc2", size = 35287115, upload-time = "2026-06-19T14:59:59.411Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ff/eec46be7e9234208f801062b53e1983085eddebd693f6c9bfb03b459830d/scipy-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ad033410e2e0672ffdc1042110cef20e1c46f8fd0616cee1d44d8d58fad8fc11", size = 35577989, upload-time = "2026-06-19T15:00:02.235Z" }, + { url = "https://files.pythonhosted.org/packages/84/ca/210d4759c7210bb7d269437421959b39a33434e2776b60c5cb8a763bb30a/scipy-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a55985d54c769c872e64b7f4c8a81cc30ef700cc04296abbbf3705439c126de", size = 37421717, upload-time = "2026-06-19T15:00:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/9a9edb45345bd6744da5ddfb6628e5d5185920494c6a67ec45b6381004cb/scipy-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:71ccc8faa2dd16ac310233203474a8b5cb67f10dedd54a3116d34943f4b19132", size = 36597428, upload-time = "2026-06-19T15:00:08.112Z" }, + { url = "https://files.pythonhosted.org/packages/99/0e/33f32a2a58987e26aec0f7df252cbbad1e90ae77bdbc76f40dd4ed0cf0ea/scipy-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76", size = 24351481, upload-time = "2026-06-19T15:00:11.103Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9c0136c2de7ae0779b7b366447766cec6d9f0702c56bb8ffeb04c8fd3af4/scipy-1.18.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:09143f676d157d9f546d663504ef9c1becb819824f1afc018814176411942446", size = 31036107, upload-time = "2026-06-19T15:00:14.03Z" }, + { url = "https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5efe260f69417b97ddae455bfb5a95e8359f7f66ad7fa9522a60feb66f169520", size = 28663303, upload-time = "2026-06-19T15:00:16.819Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0f/10ffa0b697a572f4e0d48b92a88895d366422f019f723e7e14a84c050dac/scipy-1.18.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:68363b7eaacd8b5dd426df56d782cc156468ac79a127a1b87ca597d6e2e82197", size = 20404960, upload-time = "2026-06-19T15:00:19.635Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d2/e896cea21ba8edd6c81d4c55b1ffcc717e79698dcbebf9641b4cfb4c6622/scipy-1.18.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:c5557d8be5da8e41353fcd4d21491fdbab83b062fc579e94dc09a7c8ab4f669b", size = 23034074, upload-time = "2026-06-19T15:00:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b2/e83ea34279a52c03374477c74006256ec78df65fc877baa4617d6de1d202/scipy-1.18.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d13bca67c096d89fb95ced0d8921807300fce0275643aef9533cc63a0773468", size = 33942038, upload-time = "2026-06-19T15:00:24.964Z" }, + { url = "https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a46f9273dbd0eb1cefba61c9b8648b4dfe3cbc14a080176f9a73e44b8336dc7f", size = 35266390, upload-time = "2026-06-19T15:00:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/3a/49/2c5cbb907b56695fc67517811d1db234dfd83381a84814ec220aded2794d/scipy-1.18.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5aba46108853ddfc77906b6557aac839d2b52e900c1d72a1180adaaab58d265f", size = 35551324, upload-time = "2026-06-19T15:00:31.014Z" }, + { url = "https://files.pythonhosted.org/packages/bb/73/eda39f7a2d306ff0ffc574afd13c0bbb6d10a603d9a413998ee269487a80/scipy-1.18.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6f758e35f12757b5d95c00bc6de2438e229c2664b7a92e96f205959d9f2dfa4", size = 37404785, upload-time = "2026-06-19T15:00:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d2/ae881ee28d014f38e0ccbfd974a06a919ba9af34f1f74bf42b5301891d63/scipy-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:1afac4a847207c7ff8efd321734a50b06d0280b3b2a2c0fc2f413101747ad7c7", size = 36554943, upload-time = "2026-06-19T15:00:36.903Z" }, + { url = "https://files.pythonhosted.org/packages/70/3a/21154e2d54eb3639c6bf4dbae2e531c68356bfe95990daa30df33b30d556/scipy-1.18.0-cp313-cp313-win_arm64.whl", hash = "sha256:c5dbddf60e58c2312316d097271a8e73d40eaf2eabfa4d95ed7d3695bbf2ce7b", size = 24350911, upload-time = "2026-06-19T15:00:40.062Z" }, + { url = "https://files.pythonhosted.org/packages/78/b5/915a19b3de2f7430062b509653563db1633ddbb6f021b06731521115d4e2/scipy-1.18.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4c256ee70c0d1a8a2ace807e199ccd4e3f57037433842abb3fb36bc17eaa9578", size = 31036253, upload-time = "2026-06-19T15:00:43.216Z" }, + { url = "https://files.pythonhosted.org/packages/d7/88/b72def7262e150d16be13fca37a96481138d624e700340bc3362a7588929/scipy-1.18.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:2ef3abc54a4ffc53765374b0d5728532dfdd2585ed23f6b11c206a1f0b1b9af8", size = 28673758, upload-time = "2026-06-19T15:00:46.663Z" }, + { url = "https://files.pythonhosted.org/packages/91/02/2e636a61a525632c373cf6a9c24442a3ffb79e364d38e98b32042964ac32/scipy-1.18.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2a6af57bd9e4a75d70e4117e78a1bbee84f79ae3fbb6d0111005d6ebcc4cb8d", size = 20415514, upload-time = "2026-06-19T15:00:49.399Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b6/2135974442f6aba159d9d39d774a1c8cb19947016725d69fecc685df45bf/scipy-1.18.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:3f1ac564d3bf6c03d861d2cd87a1bea0da2887136f7fb1bf519c05a8971452d6", size = 23034398, upload-time = "2026-06-19T15:00:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e6/ba89ec5abf6ee9257c0d1ec985573f3ae32742c24bc03e016388a40b1b15/scipy-1.18.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40395a5fcd1abee49a5c7aaa98c29db393eedc835138560a588c47ec16156690", size = 33998032, upload-time = "2026-06-19T15:00:54.838Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c4/bc41eb19b0fd0db868f4132920879019318d80cc522ad8f2bca4611af808/scipy-1.18.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ca01e8ae69f1b18e9a58d91afead31be3cef0dd905a10249dac559ee15460a0", size = 35283333, upload-time = "2026-06-19T15:00:58.152Z" }, + { url = "https://files.pythonhosted.org/packages/53/a4/cbdeef6eb3830a8462a9d4ada814de5fc984345cc9ecf17cbec51a036f1e/scipy-1.18.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7a7f3b01647384dbc3a711e8c6778e0aabbe93959249fef5c7393396bcac0867", size = 35610216, upload-time = "2026-06-19T15:01:01.155Z" }, + { url = "https://files.pythonhosted.org/packages/80/4d/b2b82502b65f661d1b789c1665dcdf315d5f12194e06fc0b37946294ebae/scipy-1.18.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6aa94e78ec192a30063a5e72e561c28af769dc311190b24fe91774eff1969709", size = 37418960, upload-time = "2026-06-19T15:01:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/902d836831474b0ab5a37d16404f7bc5fafd9efba632890e271ba952635f/scipy-1.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:2d8bbdc6c817f5b4006a54d799d4f5bab6f910193cbb9a1ff310833d4d270f61", size = 37288845, upload-time = "2026-06-19T15:01:07.822Z" }, + { url = "https://files.pythonhosted.org/packages/b6/43/8d73b337a3bdb14daa0314f0434210747c02d79d729ce1777574a817dcf6/scipy-1.18.0-cp314-cp314-win_arm64.whl", hash = "sha256:18e9575f1569b2c54174e6159d32942e03731177f63dce7975f0a0c88d102f5b", size = 24988971, upload-time = "2026-06-19T15:01:11.076Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b4/f11918b0508a2787031a0499a03fbe3546f3bb5ca05d01038c45b278c09a/scipy-1.18.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f351e0dd702687d12a402b867a1b4146a256923e1c38317cbc472f6372b94707", size = 31399325, upload-time = "2026-06-19T15:01:13.723Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d1/1f287b57c0ff0ee5185dff3946d92c8017d39b0e431f0ae79a3ff1859512/scipy-1.18.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7c7a51b33ce387193c97f228320cf8e87361daa1bba750638677729598b3e677", size = 29092110, upload-time = "2026-06-19T15:01:16.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1a/7b74eb6c392fdcb27d414c0e7558a6d0231eb3b6d73571f479bb81ea8794/scipy-1.18.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:84031d7b052a54fae2f8632e0ec802073d385476eb9a63079bce6e23ef9283d4", size = 20833811, upload-time = "2026-06-19T15:01:20.488Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ad/f3941716320a7b9cb4d68734a903b45fe16eff5fb7da7e16f2e619304979/scipy-1.18.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:56abf29a7c067dde59be8b9a22d606a4ea1b2f2a4b756d9d903c62818f5dacce", size = 23396644, upload-time = "2026-06-19T15:01:23.364Z" }, + { url = "https://files.pythonhosted.org/packages/22/22/1446b62ffe07f9719b7d9b1b6a4e05a772833ae8f441fe4c22c34c9b250f/scipy-1.18.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ad44305cfa24b1ba5803cbbebf033590ccbac1aa5d612d727b785325ab408b0", size = 34079318, upload-time = "2026-06-19T15:01:26.002Z" }, + { url = "https://files.pythonhosted.org/packages/56/3b/b87da667098bb470fa30c7011b0ba351ee976dd395c78798c66e941665a3/scipy-1.18.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:945c1761b93f38d7f99ae81ae80c63e621471608c7eeead563f6df025585cd58", size = 35324320, upload-time = "2026-06-19T15:01:28.881Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a1/c7932f91909759b0267f75fdea34e91309f96b895757534b76a90b6b4344/scipy-1.18.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a4441f15d620578772a49e5ab48c0ee1f7a0220e387110283062729136b2553", size = 35699541, upload-time = "2026-06-19T15:01:31.968Z" }, + { url = "https://files.pythonhosted.org/packages/f7/86/5185061a1fcc41d18c5dc2463969b3a3964b31d9ac67b2fb05d4c7ff7670/scipy-1.18.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9aac6192fac56bf2ca534389d24623f07b39ff83317d58287285e7fbd622ff76", size = 37472480, upload-time = "2026-06-19T15:01:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/31/8e/f04c68e39919a010d34f2ee1367fd705b0a25a02f609d755f0bfbc0a15fc/scipy-1.18.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e40baea28ae7f5475c779741e2d90b1247c78531207b49c7030e698ff81cee3f", size = 37365390, upload-time = "2026-06-19T15:01:38.091Z" }, + { url = "https://files.pythonhosted.org/packages/d5/19/969dc072906c84dd0a3b05dcf57ea750936087d7873549e408b35cfc3f97/scipy-1.18.0-cp314-cp314t-win_arm64.whl", hash = "sha256:368e0a705903c466aa5f08eefb39e6b1b6b2d659e7352a31fd9e2438365be0f8", size = 25279661, upload-time = "2026-06-19T15:01:40.817Z" }, +] + [[package]] name = "simulator" version = "0.1.0" @@ -286,7 +446,9 @@ dependencies = [ { name = "numpy" }, { name = "pre-commit" }, { name = "pytest" }, + { name = "pyzmq" }, { name = "ruff" }, + { name = "scipy" }, ] [package.metadata] @@ -294,7 +456,9 @@ requires-dist = [ { name = "numpy", specifier = ">=2.4.4" }, { name = "pre-commit", specifier = ">=4.6.0" }, { name = "pytest", specifier = ">=9.0.3" }, + { name = "pyzmq", specifier = ">=27.1.0" }, { name = "ruff", specifier = ">=0.15.12" }, + { name = "scipy", specifier = ">=1.18.0" }, ] [[package]]