Skip to main content

BASHAR

Bimodal Autonomous System for Handling and Articulated Robotics

PyPI version Python 3.10+ License: 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

pip install bashar
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

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 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 limitstype="continuous" joints without explicit <lower>/<upper> default to (-inf, +inf). Wheels and turrets are no longer frozen at zero.
  • Virtual DOF decompositionplanar 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

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

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

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

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

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 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.

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 for details.


BASHAR — Changelog

All notable changes to this project are documented here.
Format follows Keep a Changelog.
Versioning follows Semantic Versioning.


[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.

# 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.pyRobotState.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:

# 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.

_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):

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.

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.pyCollisionGuard.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:

# 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:

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:

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

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

bashar-2.0.0.tar.gz (47.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bashar-2.0.0-py3-none-any.whl (34.8 kB view details)

Uploaded Python 3

File details

Details for the file bashar-2.0.0.tar.gz.

File metadata

  • Download URL: bashar-2.0.0.tar.gz
  • Upload date:
  • Size: 47.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for bashar-2.0.0.tar.gz
Algorithm Hash digest
SHA256 c33eb62f00b4d60f6dcbb834e5538a08796668889457b426a81fd26c92f513ac
MD5 cea1a059e4b1b6edd2b0df8fcdc381d5
BLAKE2b-256 b113451bcc9d4f28b62f4bfb07e5004f0ed25e20a62eeed7fd1ab6b5a01d4c91

See more details on using hashes here.

Provenance

The following attestation bundles were made for bashar-2.0.0.tar.gz:

Publisher: publish.yml on Ziad-Ashraf-Abdu/BASHAR

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file bashar-2.0.0-py3-none-any.whl.

File metadata

  • Download URL: bashar-2.0.0-py3-none-any.whl
  • Upload date:
  • Size: 34.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for bashar-2.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 526318a6db85584ae6ebeefa3e7b12a327649e39513c7b1449bd7032e0e744d1
MD5 21bea13b0a7c7e6d0e981a02333cc813
BLAKE2b-256 cb16fe3610eb70cdfaa6b86761c21e8eea41eb99d8db42990571cc77c9c45b0c

See more details on using hashes here.

Provenance

The following attestation bundles were made for bashar-2.0.0-py3-none-any.whl:

Publisher: publish.yml on Ziad-Ashraf-Abdu/BASHAR

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

2.0.0 This release

2 files

1.0.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page