Metadata-Version: 2.4
Name: bashar
Version: 2.0.0
Summary: Hardware-agnostic Kinematics, Dynamics, and Control Middleware for Articulated Robots
Home-page: https://github.com/ziad-ashraf-abdu/bashar
Author: Zeyad Ashraf
Author-email: ziad.mohamed04@eng-st.cu.edu.eg
License: MIT
Keywords: robotics,kinematics,dynamics,URDF,PoE,Product of Exponentials,Newton-Euler,collision avoidance,trajectory,ROS,manipulator,serial chain
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Science/Research
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Physics
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: numpy>=1.20.0
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: keywords
Dynamic: license
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# BASHAR

### Bimodal Autonomous System for Handling and Articulated Robotics

[![PyPI version](https://img.shields.io/pypi/v/bashar.svg)](https://pypi.org/project/bashar/)
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

> **The Spinal Cord for your robot.** Drop in a URDF, get stress-tested kinematics, dynamics, and whole-arm collision avoidance — hardware-agnostic, zero boilerplate.

---

## What is BASHAR?

BASHAR is a pure-Python robotics middleware library that separates **physics** from **decisions**. You give it a URDF or XACRO file. It handles:

- **Product of Exponentials (PoE) forward kinematics** — correctly ordered, topology-aware
- **Analytical & numerical Jacobians** (space and body frames)
- **Damped Least Squares IK** — stable at singularities and workspace boundaries
- **Full Newton-Euler dynamics** — mass matrix, Coriolis, gravity, computed-torque control
- **Whole-arm collision avoidance** — APF repulsion checked at every joint along the chain
- **Trajectory generation** — cubic, quintic, and trapezoidal time-scaling profiles
- **URDF/XACRO compiler** — validates topology, decomposes planar/floating joints, detects loops and branches

The design metaphor we use is simple: **BASHAR is the Spinal Cord.** It does the physics. Whatever makes the high-level decisions — an ML model, a vision script, a keyboard operator — is the **Brain.** BASHAR doesn't know what a camera is and doesn't need to.

Swapping robots means dropping in a new profile file. The math, the control logic, and your Brain code stay exactly the same.

---

## Quick Start

```bash
pip install bashar
```

```python
from bashar.api import compile_profile, BasharSystem

# Step 1 — compile your URDF once (creates config/profiles/my_robot.json)
compile_profile("my_robot.urdf", "my_robot", verbose=True)

# Step 2 — boot the system
robot = BasharSystem("config/profiles/my_robot.json")

# Step 3 — run your control loop
while True:
    robot.update_state(encoder_readings)          # sync to hardware
    tip = robot.get_tip_position()                # [x, y, z] in base frame

    # Manual mode: filter velocity commands through the collision guard
    safe_joints = robot.manual_step(
        desired_dtheta=[0.01, -0.02, 0.0],
        obstacles=[[0.3, 0.1, 0.4]]              # base-frame coordinates
    )

    # Autonomous mode: drive to a target while dodging obstacles
    new_joints, reached = robot.auto_step(
        target_xyz=[0.4, 0.0, 0.3],
        obstacles=[[0.2, 0.2, 0.2]]
    )

    # Computed-torque control: exact motor torques (N·m)
    torques = robot.calculate_motor_torques(
        current_dtheta, desired_theta, desired_dtheta
    )
```

Requires Python >= 3.10 and NumPy. XACRO support requires ROS 2 to be sourced (uses the `xacro` command). Plain `.urdf` files work standalone.

---

## Architecture

```
BASHAR/
├── src/bashar/
│   ├── api.py                  ← The only file you import
│   ├── core/
│   │   ├── kinematics.py       ← PoE forward/inverse kinematics, Jacobians
│   │   ├── dynamics.py         ← RNEA: mass matrix, Coriolis, gravity, torque
│   │   ├── controller.py       ← RobotState, CollisionGuard, AutoPilot, CTC
│   │   └── trajectory.py       ← Cubic / quintic / trapezoidal time-scaling
│   └── utils/
│       ├── model_compiler.py   ← URDF/XACRO → validated JSON profile
│       ├── config_parser.py    ← Profile loader and schema validator
│       └── logger.py
├── tests/
│   ├── test_fixes.py           ← Bug-fix regression suite (23 tests)
│   ├── test_core.py            ← Core math unit tests
│   └── urdfs/                  ← Test URDFs for each bug scenario
├── examples/
│   ├── basic_usage.py
│   ├── ros2_node.py
│   └── hardware_loop.py
└── config/profiles/            ← Compiled robot JSON profiles (gitignored)
```

---

## Module Reference

### `compile_profile` — URDF/XACRO Compiler

```python
from bashar.api import compile_profile

compile_profile(
    model_path="my_robot.urdf",   # or .xacro
    output_name="my_robot",
    output_dir="config/profiles", # default
    verbose=True                  # prints ASCII joint tree
)
```

The compiler walks the URDF, validates it, and writes a clean JSON profile that all downstream components read. What it does specifically:

- **Topological ordering** — builds the actual parent→child link graph and walks it from `base_frame` via BFS, so joints are always emitted in physical traversal order regardless of how they are declared in the file. This is the single fix point that keeps FK, IK, Jacobians, and RNEA consistent. See [CHANGELOG](CHANGELOG.md) for the v1.x bug this addresses.
- **Branch detection** — if two or more active (moving) joints share the same parent link, the compiler raises a `RuntimeError` at compile time rather than producing a silently broken profile.
- **Continuous joint limits** — `type="continuous"` joints without explicit `<lower>`/`<upper>` default to `(-inf, +inf)`. Wheels and turrets are no longer frozen at zero.
- **Virtual DOF decomposition** — `planar` joints become 3 virtual 1-DOF joints (Tx, Ty, Rz); `floating` joints become 6 (Tx, Ty, Tz, Rx, Ry, Rz). The downstream PoE math only ever sees one degree of freedom per entry.
- **Validation** — axis normalization, limit sanity (lower < upper, non-negative velocity/effort), and closed-loop detection.
- **Inertial pipeline** — every `<link>` tag's mass, origin, and 6-element inertia tensor is extracted and stored in the profile. The dynamics engine reads these directly to build spatial inertia matrices.

---

### `BasharSystem` — The Main API

```python
from bashar.api import BasharSystem

robot = BasharSystem("config/profiles/my_robot.json", initial_positions=[0.0, 0.0, 0.0])
```

#### State Management

| Method | Description |
|---|---|
| `update_state(positions)` | Sync internal state from hardware encoders. Validates DOF count, clips to limits. |
| `get_state()` → `list[float]` | Current joint positions after clipping. |
| `get_tip_position()` → `[x, y, z]` | End-effector position in the robot's base frame. |

#### Navigation & Control

| Method | Description |
|---|---|
| `manual_step(dtheta, obstacles)` → `list[float]` | Filter a velocity command through the whole-arm collision guard. |
| `auto_step(target_xyz, obstacles)` → `(list[float], bool)` | Autonomous step toward a target. Returns (positions, reached). |
| `calculate_motor_torques(dtheta, theta_des, dtheta_des)` → `list[float]` | Computed-torque control. Returns torques in N·m. |

---

### `BasharKinematics` — The Math Engine

```python
from bashar.core.kinematics import BasharKinematics

kin = BasharKinematics(profile_dict)

T = kin.forward_kinematics_space([0.1, -0.3, 0.5])   # 4x4 SE(3) transform
Js = kin.jacobian_space(theta)                          # 6xn Space Jacobian
theta_sol, converged = kin.ik_body(T_target, theta_guess)
```

| Method | Description |
|---|---|
| `forward_kinematics_space(theta)` | PoE FK: `T = prod(exp([Si]ti)) * M` |
| `jacobian_space(theta)` | Space Jacobian: `Js` in R^(6xn) |
| `jacobian_body(Js, T)` | Body Jacobian derived from the space Jacobian |
| `ik_body(T_sd, theta_guess)` | Newton-Raphson IK with DLS. Returns `(theta, converged)`. |
| `dls_pinv(J, damping)` | Damped Least Squares pseudo-inverse: `J+ = J'(JJ' + λ²I)⁻¹` |
| `ellipsoid_analysis(J)` | Manipulability metrics (condition number, volume measure) |

All Jacobian inversions use Damped Least Squares by default (`λ = 0.05`). Near singularities where plain pseudo-inverse diverges, DLS stays bounded by trading a small amount of accuracy for guaranteed numerical stability. The damping factor is a tunable constructor parameter on both `CollisionGuard` and `AutoPilot`.

---

### `CollisionGuard` — Whole-Arm Safety

```python
from bashar.core.controller import CollisionGuard

guard = CollisionGuard(
    kin,
    influence_radius=0.20,  # obstacle influence sphere (m)
    repulse_gain=1.5,        # APF gain
    body_radius=0.08,        # physical arm thickness (m)
    damping=0.05             # DLS damping for Jacobian inversion
)

safe_state = guard.filter_command(state, d_theta, obstacles)
```

> **Frame requirement:** All obstacle coordinates must be in the **robot's base frame**. Frame transformation is the Brain's responsibility.

As of v2.0, the guard checks every active joint's workspace position along the chain, not just the end-effector tip. For each checkpoint `k`, it uses a Jacobian truncated to only the joints upstream of that point (columns 0 through k) to map the repulsion wrench into joint-space corrections. An obstacle on the elbow produces a real response. Obstacles far from all checkpoints produce no correction.

The `body_radius` parameter inflates the influence sphere around each checkpoint to account for the physical thickness of the arm.

---

### `Trajectory` — Motion Planning

```python
from bashar.core.trajectory import Trajectory

# Joint-space trajectory (returns list of N joint configs)
path = Trajectory.joint_trajectory(
    start=[0.0, 0.0, 0.0],
    end=[1.0, -0.5, 0.8],
    Tf=3.0,           # total time (s)
    N=100,            # number of waypoints
    method='quintic'  # 'cubic' | 'quintic' | 'trapezoidal'
)

# With velocities (for computed-torque control)
path_with_vel = Trajectory.joint_trajectory_velocities(start, end, Tf=3.0, N=100)

# Via-point path through multiple waypoints
path = Trajectory.via_point_trajectory(points, times, N_per_segment=50)

# Task-space screw trajectory (list of N SE(3) transforms)
path = Trajectory.screw_trajectory(T_start, T_end, Tf=3.0, N=100)
```

| Profile | Boundary conditions | When to use |
|---|---|---|
| Cubic | Zero velocity at start and end | General point-to-point motion |
| Quintic | Zero velocity and acceleration at start and end | Payload handling, torque-sensitive moves |
| Trapezoidal | Bang-coast-bang velocity profile | Maximum throughput, cycle time matters |

---

## Safety Notes for Hardware

A few things worth knowing before connecting to a real robot:

1. **Verify FK against a known pose** before trusting any autonomous motion. Measure a physical configuration, call `update_state`, call `get_tip_position`, and compare against what you measured.
2. **Obstacle coordinates are base-frame only.** Everything passed to `manual_step`, `auto_step`, or `CollisionGuard` must already be in the robot's base frame. Camera-frame or world-frame coordinates need to be transformed by your Brain code before the call.
3. **`manual_step` applies delta-position, not velocity.** The docstring says "velocity command" but the implementation adds `d_theta` directly to joint positions with no `dt` multiplication. If your loop passes genuine rad/s, pre-multiply by your tick period.
4. **No tunneling protection.** The guard checks distance at the start of each tick, not along a swept path. Fast motion at a low control rate can pass through a thin obstacle in one step. Keep a physical e-stop within reach.
5. **Branching robots fail at compile time.** Grippers, dual-arm configurations, and any robot where two active joints share a parent link will now raise a `RuntimeError` during `compile_profile`. The fix is to split them into separate serial chains and compile each independently.

---

## Running the Tests

```bash
git clone https://github.com/ziad-ashraf-abdu/bashar
cd bashar
python3 -m venv .venv && source .venv/bin/activate
pip install -e .

python -m pytest tests/ -v
```

23 tests, 0 failures — covering all three v2.0 bug fixes and regression checks for FK, IK, Jacobian finite-difference, and RNEA.

---

## Changelog

See [CHANGELOG.md](CHANGELOG.md) for the full bug report, root-cause analysis, and fix descriptions.

**v2.0.0** — Major release. Four bugs identified and fixed:

- **Critical:** Topological joint ordering (FK was silent-wrong for out-of-order URDF declarations)
- **Critical:** `continuous` joints frozen at 0 (wheels, turrets permanently locked)
- **Critical:** Whole-arm collision guard (only tip was checked; elbow/shoulder obstacles were invisible)
- **High:** Branching robot detection (now raises `RuntimeError` at compile time instead of silent garbage output)

---

## Contributing

Issues and pull requests welcome at [github.com/ziad-ashraf-abdu/bashar](https://github.com/ziad-ashraf-abdu/bashar).

The test suite in `tests/test_fixes.py` uses purpose-built URDFs in `tests/urdfs/` that directly reproduce each bug scenario — a useful reference when adding new fixes.

---

## License

MIT — see [LICENSE](LICENSE) for details.


---

# BASHAR — Changelog

All notable changes to this project are documented here.  
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).  
Versioning follows [Semantic Versioning](https://semver.org/).

---

## [2.0.0] — 2026-08-14

### Bug Report & Root-Cause Analysis

A thorough audit of the installed v1.0.0 library — reading every module and running it against purpose-built test URDFs — revealed four bugs in the layer that bridges URDF description to the rigid-body math. The math itself (PoE kinematics, Jacobians, RNEA dynamics, trajectory time-scaling) was verified numerically correct throughout. The bugs were all in the compiler and controller, not in the core algorithms.

---

#### Bug #1 — Topological joint ordering ignored

**File:** `src/bashar/utils/model_compiler.py` → `_extract_kinematics()`  
**File:** `src/bashar/core/kinematics.py` → `_build_poe_from_profile()`

**Root cause:**  
`_extract_kinematics()` appended joints to the output list in the exact order they appear in the URDF XML file. `_build_poe_from_profile()` then consumed that list sequentially, accumulating `T_accum = T_accum @ T_local` for each joint — treating file order as physical parent→child order.

URDF does not guarantee that joints are declared in tree traversal order. Many authoring tools, XACRO macros, and hand-edited files declare joints in arbitrary order (e.g. end-effector first, base last). When this happens, the PoE product accumulates transforms in the wrong sequence and the resulting screw axes are computed from wrong global positions.

**Reproduction:**

```
URDF declares: j_elbow (parent=link1) THEN j_shoulder (parent=base_link)
Physical order: j_shoulder → j_elbow

Expected tip at theta=[0,0]: [0, 0.5, 0.1]
v1.0.0 actual:               [0.3, 0.0, 0.2]   ← silent wrong answer
```

**Why it was invisible:**  
The ASCII tree printer (`_print_tree`) reads `parent`/`child` attributes correctly and displayed the right hierarchy. `verbose=True` showed a tree that looked correct while the math underneath used a different, wrong order. The system gave no indication that anything was wrong.

**Fix:**  
After collecting all joints, build a `parent → [joints]` adjacency map and perform a BFS walk from `base_frame` to emit joints in strict parent→child traversal order, regardless of XML declaration order. This is the single fix point: every downstream consumer (kinematics, `RobotState`, dynamics) reads the list in order, so fixing it once at the source keeps everything consistent.

```python
# model_compiler.py — _extract_kinematics()
parent_to_joints = {}
for j in joints:
    parent_to_joints.setdefault(j['parent'], []).append(j)

ordered_joints = []
queue = [base_frame]
while queue:
    current_link = queue.pop(0)
    for j in parent_to_joints.get(current_link, []):
        ordered_joints.append(j)
        queue.append(j['child'])
```

**Verified:** FK on the out-of-order URDF now returns `[0, 0.5, 0.1]`.

---

#### Bug #2a — `continuous` joints frozen at zero

**File:** `src/bashar/utils/model_compiler.py` → `_extract_kinematics()`  
**File:** `src/bashar/core/controller.py` → `RobotState.clip()`

**Root cause:**  
The compiler defaulted `limits = {"lower": 0.0, "upper": 0.0, ...}` for all joints. For `type="continuous"` joints (wheels, turrets, continuously-rotating joints) the URDF standard explicitly says `<lower>`/`<upper>` are omitted because the joint is unbounded. The compiler's `0.0` defaults were then written to the JSON profile. `RobotState.clip()` then applied `np.clip(position, 0.0, 0.0)` on every tick, permanently clamping the joint to exactly 0.

**Reproduction:**

```python
# Wheel joint with type="continuous", no lower/upper in <limit>
robot.update_state([3.14])
print(robot.get_state())   # v1.0.0: [0.0]  ← commanded 3.14 rad, got 0
```

**Fix:**  
`continuous` joints now default to `(-inf, +inf)`. Explicit `<lower>`/`<upper>` attributes in `<limit>` are respected if present; otherwise the bounds stay infinite.

```python
_is_continuous = (j_type == 'continuous')
limits = {
    "lower": float('-inf') if _is_continuous else 0.0,
    "upper": float('+inf') if _is_continuous else 0.0,
    ...
}
```

`RobotState.clip()` now short-circuits the `np.clip` call for joints whose limits are `(-inf, +inf)`:

```python
def clip(self):
    for i in range(self.num_joints):
        lo, hi = self.limits[i]['lower'], self.limits[i]['upper']
        if lo == float('-inf') and hi == float('inf'):
            continue   # unbounded joint — never clamp
        self.positions[i] = np.clip(self.positions[i], lo, hi)
```

**Verified:** Wheel joint accepts 3.14 rad and -100 rad (multiple full turns) with no clamping.

---

#### Bug #2b — Branching robots compiled silently to garbage

**File:** `src/bashar/utils/model_compiler.py` → `_extract_kinematics()`

**Root cause:**  
The PoE / Jacobian / RNEA math is defined for open serial chains only — a single path from base to tip. A gripper with two fingers, a dual-arm, or any robot where two active joints share a parent link has no single tip. In v1.0.0, such robots compiled without error. The kinematics engine then chained the finger joints sequentially as if `right_finger` were physically downstream of `left_finger_link`, and `get_tip_position()` returned a meaningless point for a topology that has no unique tip.

**Fix:**  
The compiler now detects branching among active joints and raises `RuntimeError` at compile time with a descriptive message.

```python
for link, children in parent_to_joints.items():
    active_children = [j for j in children if j['type'] in _active_types]
    if len(active_children) > 1:
        raise RuntimeError(
            f"[BASHAR] Branching robot detected at link '{link}': "
            f"active joints {[j['name'] for j in active_children]} share the same parent. "
            "Split into separate serial chains and compile each independently."
        )
```

Supporting tree topologies correctly in PoE/Jacobian/RNEA would require rearchitecting the math layer, which is out of scope for this release. Converting the failure mode from silent garbage to a loud RuntimeError is the right call for now.

**Verified:** Branching gripper URDF raises `RuntimeError` during `compile_profile()`.

---

#### Bug #3 — Collision guard only checks end-effector tip

**File:** `src/bashar/core/controller.py` → `CollisionGuard.filter_command()`

**Root cause:**  
`filter_command()` computed the end-effector tip position, checked it against obstacles, and mapped any repulsion wrench to joint space. The `body_radius` parameter inflated the influence sphere around that single tip check. An obstacle placed anywhere along the arm — elbow, forearm, shoulder — produced zero response as long as the tip itself was outside the influence sphere.

**Reproduction:**

```python
# 3-DOF arm: tip at [0,0,1.2], elbow at [0,0,0.9]
# Obstacle placed at elbow: [0, 0.05, 0.9]
guard.filter_command(state, [0,0,0], obstacles=[[0, 0.05, 0.9]])
# v1.0.0: correction = [0.0, 0.0, 0.0]  ← no reaction at all
```

**Fix:**  
Complete rewrite of `filter_command()`. A new `_checkpoint_positions(theta)` method computes the world-frame position of each active joint's axis point under the current configuration:

```python
def _checkpoint_positions(self, theta):
    positions = []
    T_pre = np.eye(4)
    for k in range(len(theta)):
        S_k = self.kin.S_list[k]
        omega_k, v_k = S_k[:3], S_k[3:]
        omega_norm_sq = float(omega_k @ omega_k)
        if omega_norm_sq > 1e-10:     # revolute / continuous
            q_at_zero = np.cross(omega_k, v_k) / omega_norm_sq
            p_world = T_pre[:3, :3] @ q_at_zero + T_pre[:3, 3]
        else:                          # prismatic
            p_world = T_pre[:3, 3]
        positions.append(p_world)
        T_pre = T_pre @ self.kin.matrix_exp_6(S_k, theta[k])
    positions.append((T_pre @ self.kin.M)[:3, 3])  # true tip
    return positions
```

For each checkpoint `k`, only columns 0 through k of the full Space Jacobian are used. Joints downstream of checkpoint k cannot physically move it, so including them in the inversion is wrong:

```python
Js_full = self.kin.jacobian_space(theta)
for cp_idx, cp_xyz in enumerate(checkpoints):
    n_effective = min(cp_idx + 1, state.num_joints)
    Js_cp = Js_full[:, :n_effective]          # truncated Jacobian
    # ... compute V_repulse_cp for this checkpoint ...
    dq_cp = self.kin.dls_pinv(Js_cp, self.damping) @ V_repulse_cp
    d_theta_correction[:n_effective] += dq_cp
```

The `actual_distance == 0` edge case (obstacle exactly at a checkpoint) now uses a fixed diagonal fallback direction instead of skipping repulsion entirely.

**Verified:**

- Obstacle at elbow region: non-zero joint correction
- Obstacle far away (5 m): zero correction, no false positive
- Obstacle exactly at tip: non-zero response, no ZeroDivisionError
- Mid-chain obstacle: only proximal joints respond

---

### Regression — Verified Working Math

All three fixes were regression-tested against a clean 3-DOF serial arm. The following were confirmed unaffected:

| Test | Result |
|---|---|
| FK identity at home configuration | pass |
| Space Jacobian shape (6xn) | pass |
| Space Jacobian via finite-difference cross-check | pass |
| IK round-trip (theta → FK → IK converges to 1e-4 m) | pass |
| Mass matrix: symmetric, positive semi-definite | pass |
| Gravity torques: ~0 for collinear (non-moment-arm) configs | pass |
| `RobotState` limit clipping for bounded revolute joints | pass |

Full suite: 23 tests, 0 failures.

---

### Added

- `tests/test_fixes.py` — regression suite covering all three fixes (18 tests)
- `tests/urdfs/out_of_order.urdf` — Bug #1 reproduction URDF
- `tests/urdfs/continuous_wheel.urdf` — Bug #2a reproduction URDF
- `tests/urdfs/branching_gripper.urdf` — Bug #2b reproduction URDF
- `tests/urdfs/serial_3dof.urdf` — clean 3-DOF arm for regression tests
- `CollisionGuard._checkpoint_positions()` — whole-arm checkpoint computation
- `CollisionGuard._sanitize_obstacles()` — obstacle normalization (was previously inline)

### Changed

- `ModelCompiler._extract_kinematics()` — BFS topological joint ordering
- `ModelCompiler._extract_kinematics()` — continuous joint limits default to `(-inf, +inf)`
- `CollisionGuard.filter_command()` — complete rewrite for whole-arm checking
- `RobotState.clip()` — short-circuits for unbounded joints
- `setup.py` — version bumped to `2.0.0`
- PyPI `Development Status` promoted to `5 - Production/Stable`

---

## [1.0.0] — 2026-06-24

### Added

- Initial release
- Product of Exponentials forward kinematics (`BasharKinematics`)
- Space and body Jacobians
- Damped Least Squares IK (`ik_body`)
- Manipulability ellipsoid analysis
- Recursive Newton-Euler dynamics (`BasharDynamics`)
  - Mass matrix, Coriolis/centripetal, gravity, forward dynamics
- Computed-Torque Controller (`ComputedTorqueController`)
- Artificial Potential Field collision guard (`CollisionGuard`)
- Autonomous navigation (`AutoPilot`)
- Trajectory generation — cubic, quintic, trapezoidal time-scaling (`Trajectory`)
- URDF/XACRO model compiler with validation (`ModelCompiler`)
- Full inertial pipeline: mass, COM, and 6-element inertia tensor from URDF to dynamics
- `BasharSystem` high-level API
- ROS 2 node example, hardware loop example, basic usage example
