Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# SIMULATOR

## Description

This project is a physical simulator of robotic systems in Python. The simulator implements the calculation of forward kinematics and dynamics (including the Articulated-Body algorithm), integration of equations of motion using SciPy (Runge-Kutta method) and step-by-step visualization of the operation of mechanisms using Matplotlib. Currently, various types of models are supported: two-link mechanisms, a reverse pendulum on a cart (CartPole) and complex multi-link trees (RobotTree).

## Run guide

To run project, use the following command
```
uv run python src/simulator/main.py
```

To run tests, use the following command
```
uv run pytest
```
7 changes: 7 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"numpy>=2.4.4",
"scipy",
"matplotlib",
"pre-commit>=4.6.0",
"pytest>=9.0.3",
"ruff>=0.15.12",
Expand Down Expand Up @@ -35,3 +37,8 @@ line-length = 100
quote-style = "double"
indent-style = "space"
docstring-code-format = true

[tool.pytest.ini_options]
pythonpath = ["src/simulator"]
testpaths = ["tests"]
python_files = "test_*.py"
2 changes: 1 addition & 1 deletion src/simulator/dynamics/ab_algorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def forward_dynamics(self, obj, q, qd, tau, f_ext=None):
a[i] = Xup[i] @ (-a_grav) + c[i]
else:
a[i] = Xup[i] @ a[parent] + c[i]
qdd[i] = (u[i] - U[i].T @ a[i]) / d[i]
qdd[i] = float(np.squeeze((u[i] - U[i].T @ a[i]) / d[i]))
a[i] = a[i] + S[i].dot(qdd[i])

return qdd
Expand Down
104 changes: 0 additions & 104 deletions src/simulator/renderer.py

This file was deleted.

101 changes: 101 additions & 0 deletions src/simulator/renderer/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import numpy as np
from .camera import Camera


class Renderer:
"""
Main Renderer interface wrapping a Camera and a specific graphics Backend.
"""

def __init__(self, backend_type="matplotlib", camera=None, **kwargs):
if camera is None:
self.camera = Camera()
else:
self.camera = camera

if backend_type == "matplotlib":
from .backends.matplotlib_backend import MatplotlibBackend
self.backend = MatplotlibBackend()
elif backend_type == "headless":
from .backends.headless_backend import HeadlessBackend
self.backend = HeadlessBackend()
else:
raise ValueError(f"Unknown backend type: {backend_type}")

self.backend.init(**kwargs)

def update(self, objects, dt=0.0001):
"""
Draws a list of objects and refreshes the frame.
Usually called periodically by the simulation loop.
"""
self.backend.clear()

for obj in objects:
self._draw_object(obj)

self.backend.render()

def _draw_object(self, obj):
# Cleanly support user objects implementing their own custom draw logic based on abstract primitives
if hasattr(obj, "draw"):
obj.draw(self)
# Fallback to standard generic tree structure logic if it matches legacy spec
elif hasattr(obj, "model") and "parent" in obj.model:
self._draw_robot_tree(obj)

def _draw_robot_tree(self, obj):
parents = obj.model["parent"]
nodes = []
angles = [0.0] * len(parents)
length = 1.0
q = obj.q

for i in range(len(parents)):
parent = parents[i]

if parent == -1:
x_p, y_p = 0.0, 0.0
angles[i] = q[i]
else:
x_p, y_p = nodes[parent]
angles[i] = angles[parent] + q[i]

x_child = x_p + length * np.cos(angles[i])
y_child = y_p + length * np.sin(angles[i])
nodes.append((x_child, y_child))

start = (x_p, y_p)
end = (x_child, y_child)

# Dispatch to generic primitive drawers
self.draw_line(start, end, color="blue", width=3)
# Draw joint
if parent != -1:
self.draw_circle(start, radius=0.15, color="lightblue")

# Draw end effectors/leafs
for pt in nodes:
self.draw_circle(pt, radius=0.15, color="lightblue")

# --- Public Abstract Shapes Drawing API ---
# Objects like Terrains, Walls, chained robots can use these primitives.

def draw_line(self, start, end, color="black", width=1):
start_cm = self.camera.apply(start)
end_cm = self.camera.apply(end)
self.backend.draw_line(start_cm, end_cm, color, width)

def draw_circle(self, center, radius, color="blue"):
r_cm = radius * self.camera.scale
self.backend.draw_circle(self.camera.apply(center), r_cm, color)

def draw_box(self, center, width, height, angle=0.0, color="green"):
w_cm = width * self.camera.scale
h_cm = height * self.camera.scale
self.backend.draw_box(self.camera.apply(center),
w_cm, h_cm, angle, color)

def close(self):
"""Clean up rendering resources."""
self.backend.close()
5 changes: 5 additions & 0 deletions src/simulator/renderer/backends/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .base import RendererBackend
from .matplotlib_backend import MatplotlibBackend
from .headless_backend import HeadlessBackend

__all__ = ["RendererBackend", "MatplotlibBackend", "HeadlessBackend"]
67 changes: 67 additions & 0 deletions src/simulator/renderer/backends/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from abc import ABC, abstractmethod


class RendererBackend(ABC):
"""
Clear abstract Renderer interface / base class.
Defines the contract for pluggable drawing backends (Matplotlib, Pygame, OpenGL, Headless, etc.).
"""

@abstractmethod
def init(self, x_limits=(-10.0, 10.0), y_limits=(-10.0, 10.0), **kwargs):
"""Initialize the rendering backend, window, limits, etc."""
pass

@abstractmethod
def clear(self):
"""Prepare for drawing a new frame by clearing the previous artifacts."""
pass

@abstractmethod
def draw_line(self, start, end, color="black", width=1):
"""
Draw a line segment.

Args:
start (tuple): (x, y) start point.
end (tuple): (x, y) end point.
color (str or tuple): Color specification.
width (float): Line width.
"""
pass

@abstractmethod
def draw_circle(self, center, radius, color="blue"):
"""
Draw a circle (for joints, floating bases, etc.).

Args:
center (tuple): (x, y) center position.
radius (float): Radius of the circle.
color (str or tuple): Color specification.
"""
pass

@abstractmethod
def draw_box(self, center, width, height, angle=0.0, color="green"):
"""
Draw a rectangle or box (for terrains, walls-obstacles).

Args:
center (tuple): (x, y) geometric center of the box.
width (float): Total width.
height (float): Total height.
angle (float): Rotation angle in radians from horizontal axis.
color (str or tuple): Color specification.
"""
pass

@abstractmethod
def render(self):
"""Update the screen/buffer with the queued shapes."""
pass

@abstractmethod
def close(self):
"""Cleanup, close windows, or destroy contexts."""
pass
29 changes: 29 additions & 0 deletions src/simulator/renderer/backends/headless_backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from .base import RendererBackend


class HeadlessBackend(RendererBackend):
"""
Headless renderer backend for fast simulation
without any UI/graphical overhead (e.g. for RL training/CI).
"""

def init(self, x_limits=(-10.0, 10.0), y_limits=(-10.0, 10.0), **kwargs):
pass

def clear(self):
pass

def draw_line(self, start, end, color="black", width=1):
pass

def draw_circle(self, center, radius, color="blue"):
pass

def draw_box(self, center, width, height, angle=0.0, color="green"):
pass

def render(self):
pass

def close(self):
pass
Loading
Loading