diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index eb33fcdd..f9dc6678 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -68,3 +68,31 @@ jobs:
CBFKIT_TEST_MODE: "1"
run: |
pytest -m "slow" tests
+
+ core-import:
+ name: Slim-core import (no extras)
+ runs-on: ubuntu-latest
+ env:
+ JAX_PLATFORM_NAME: cpu
+ steps:
+ - uses: actions/checkout@v6
+ - name: Set up Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: "3.11"
+ - name: Install core only (no extras)
+ run: pip install .
+ - name: Assert the default import surface stays lean
+ run: |
+ python - <<'PY'
+ import sys
+ import cbfkit
+ from cbfkit.controllers.cbf_clf import vanilla_cbf_clf_qp_controller
+ from cbfkit.optimization.quadratic_program import get_solver
+ get_solver("fast")
+ get_solver("jaxopt")
+ heavy = {"casadi", "cvxopt", "kvxopt", "plotly"}
+ leaked = sorted(m for m in sys.modules if m.split(".")[0] in heavy)
+ assert not leaked, f"Heavy optional deps leaked into the default import: {leaked}"
+ print("OK: default import surface is lean (no casadi/cvxopt/plotly).")
+ PY
diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
new file mode 100644
index 00000000..01b3f3e7
--- /dev/null
+++ b/.github/workflows/pages.yml
@@ -0,0 +1,41 @@
+name: Deploy blog to GitHub Pages
+
+# Serves blog/ (the "CBFKit Reborn" landing page) at the project's Pages URL.
+# One-time setup: repo Settings -> Pages -> Build and deployment -> Source: "GitHub Actions".
+# All media in blog/index.html is referenced by absolute raw.githubusercontent URLs,
+# so the deployed folder stays free of large binaries.
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - "blog/**"
+ - ".github/workflows/pages.yml"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: pages
+ cancel-in-progress: false
+
+jobs:
+ deploy:
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+ - name: Configure Pages
+ uses: actions/configure-pages@v5
+ - name: Upload blog as Pages artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: blog
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.gitignore b/.gitignore
index 6f6cbd97..51082cac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -228,3 +228,6 @@ jax_cache/
# Generated results
results/
+
+# Local-only launch notes (not published to the repo)
+docs/launch/
diff --git a/README.md b/README.md
index 607de288..3e3c229a 100755
--- a/README.md
+++ b/README.md
@@ -1,14 +1,20 @@
# CBFKit: A Control Barrier Function Toolbox for Robotics Applications
+[](https://github.com/bardhh/cbfkit)
+[](https://github.com/bardhh/cbfkit/actions/workflows/ci.yml)
+[](https://github.com/bardhh/cbfkit/blob/main/LICENSE)
+[](https://arxiv.org/abs/2404.07158)
+[](https://colab.research.google.com/github/bardhh/cbfkit/blob/main/examples/gymnasium/safe_single_integrator.ipynb)
+
CBFKit is a Python/ROS2 toolbox for safe planning and control using Control Barrier Functions (CBFs). Built on JAX for automatic differentiation and JIT compilation, it provides formal safety guarantees for robotic systems operating in deterministic, disturbed, and stochastic environments. It also includes an efficient JAX implementation of Model Predictive Path Integral (MPPI) control with reach-avoid specifications.
-
-
+
+
-
-
+
+
MPPI among pedestrians | CBF head-on safety
@@ -19,11 +25,19 @@ Supported dynamics: $\dot{x} = f(x) + g(x)u$, $\dot{x} = f(x) + g(x)u + Mw$, $dx
## Quick Start
-Requires **Python 3.10--3.12**.
+Requires **Python 3.10--3.12**. Install directly from GitHub:
+
+```bash
+pip install "cbfkit @ git+https://github.com/bardhh/cbfkit.git"
+```
+
+Need optional features? Add extras, e.g. `pip install "cbfkit[gymnasium] @ git+https://github.com/bardhh/cbfkit.git"` (also available: `neural`, `casadi`, `cvxopt`, `plotly`, `manim`, or `all`).
+
+Or clone for development (editable install with all dev tools + extras):
```bash
git clone https://github.com/bardhh/cbfkit.git && cd cbfkit
-pip install -e .
+pip install -e ".[dev]"
python examples/unicycle/reach_goal/unicycle_reach_avoid_cbf.py
```
@@ -87,7 +101,7 @@ print(f"Final position: ({results.states[-1, 0]:.2f}, {results.states[-1, 1]:.2f
Drop-in CBF safety filter for any continuous Gymnasium environment. Wraps the env so every action from your RL policy gets safety-projected by a CBF-QP before reaching the simulator — works with PPO, SAC, or any off-the-shelf algorithm, no policy retraining required.
-
+
```bash
python examples/gymnasium/safe_single_integrator.py
@@ -97,7 +111,7 @@ python examples/gymnasium/safe_single_integrator.py
Skip the math: learn the barrier function from samples. A small neural network learns h(x) from labeled safe/unsafe states, then plugs straight into CBFKit's CBF-QP controller. Useful when obstacles are hard to describe analytically — point clouds, learned occupancy maps, scanned environments.
-
+
```bash
python examples/neural_cbf/neural_cbf_obstacle_avoidance.py
@@ -115,7 +129,7 @@ Measured on random PD QPs (50 reps after warmup) on the sizes typical of CBF-CLF
| 4×10 | **785×** | 73× |
| 8×20 | **696×** | 64× |
-
+
```bash
python benchmarks/qp_solver_comparison.py
@@ -125,7 +139,7 @@ python benchmarks/qp_solver_comparison.py
Cinematic 3D simulation rendering with Manim. Multi-robot reach-avoid in 3D, rendered via CBFKit's Manim backend. Shows the visualization stack scales from quick matplotlib plots to publication-quality 3D animations.
-
+
```bash
python tutorials/multi_robot_3d_reachavoid.py
@@ -136,64 +150,64 @@ python tutorials/multi_robot_3d_reachavoid.py
-
- Ellipsoidal-obstacle CBF 🔗 Unicycle reach-goal with linear class-K
+
+ Ellipsoidal-obstacle CBF 🔗 Unicycle reach-goal with linear class-K
-
- Stochastic CBF (SDE) 🔗 Safety under Brownian disturbance
+
+ Stochastic CBF (SDE) 🔗 Safety under Brownian disturbance
-
- Robust CBF 🔗 Worst-case bounded disturbance
+
+ Robust CBF 🔗 Worst-case bounded disturbance
-
- MPPI rollout sampling 🔗 Sampling-based planning
+
+ MPPI rollout sampling 🔗 Sampling-based planning
-
- MPPI reach-avoid 🔗 Sampling-based planning with goal + obstacle cost
+
+ MPPI reach-avoid 🔗 Sampling-based planning with goal + obstacle cost
-
- Multi-robot 2D 🔗 Coordination via shared CBFs
+
+ Multi-robot 2D 🔗 Coordination via shared CBFs
-
- Fixed-wing aerial 3D 🔗 UAV reach-drop-point in 3D
+
+ Fixed-wing aerial 3D 🔗 UAV reach-drop-point in 3D
-
- Pedestrian head-on 🔗 Dynamic-agent avoidance
+
+ Pedestrian head-on 🔗 Dynamic-agent avoidance
-
- EKF state estimation 🔗 Unicycle reach-goal under measurement noise
+
+ EKF state estimation 🔗 Unicycle reach-goal under measurement noise
-
- Van der Pol (CLF) 🔗 Nonlinear regulation to the origin
+
+ Van der Pol (CLF) 🔗 Nonlinear regulation to the origin
-
- Model Predictive Control 🔗 Receding-horizon LTI tracking
+
+ Model Predictive Control 🔗 Receding-horizon LTI tracking
-
- Quadrotor 6-DOF 🔗 Geometric SE(3) tracking + altitude CBF
+
+ Quadrotor 6-DOF 🔗 Geometric SE(3) tracking + altitude CBF
-
- Monte Carlo safety verification 🔗 200 stochastic CBF rollouts (jax.vmap), live empirical risk
+
+ Monte Carlo safety verification 🔗 200 stochastic CBF rollouts (jax.vmap), live empirical risk
diff --git a/blog/blog-carousel.js b/blog/blog-carousel.js
new file mode 100644
index 00000000..5920cbd7
--- /dev/null
+++ b/blog/blog-carousel.js
@@ -0,0 +1,458 @@
+/**
+ * — Lightweight scroll-snap carousel Web Component.
+ *
+ * No Shadow DOM — uses scoped `bc-` class prefixes so page CSS still
+ * applies to slide content (benchmark charts, videos, images, etc.).
+ *
+ * The component fully owns video play/pause — do NOT put `autoplay`
+ * on tags. Instead add `muted loop playsinline preload="auto"`.
+ *
+ * Usage:
+ *
+ *
+ * …
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ */
+class BlogCarousel extends HTMLElement {
+ static get observedAttributes() {
+ return ["loop"];
+ }
+
+ constructor() {
+ super();
+ this._index = 0;
+ this._count = 0;
+ this._scrollTimer = null;
+ this._isScrolling = false; // guards against scroll-handler race
+ }
+
+ /* ── Lifecycle ───────────────────────────────────────── */
+
+ connectedCallback() {
+ BlogCarousel._injectStyles();
+
+ this._ul = this.querySelector("ul");
+ this._slides = Array.from(this.querySelectorAll("ul > li"));
+ this._count = this._slides.length;
+ if (!this._count) return;
+
+ /* Mark up existing DOM */
+ this.classList.add("bc-root");
+ this._ul.classList.add("bc-track");
+ this._slides.forEach((li) => li.classList.add("bc-slide"));
+
+ /* Strip any `autoplay` the author left on videos — we manage play/pause */
+ this._slides.forEach((li) => {
+ const v = li.querySelector("video");
+ if (v) {
+ v.removeAttribute("autoplay");
+ v.pause();
+ }
+ });
+
+ /* Build controls (appended after the ) */
+ this._buildNav();
+ this._buildDots();
+ this._buildCaption();
+ this._buildLiveRegion();
+ this._initVideoToggles();
+
+ this._bind();
+ this._syncState();
+ }
+
+ disconnectedCallback() {
+ this._ul?.removeEventListener("scroll", this._onScroll);
+ if (this._onResize) window.removeEventListener("resize", this._onResize);
+ }
+
+ attributeChangedCallback() {
+ this._syncState();
+ }
+
+ get loop() {
+ return this.getAttribute("loop") === "true";
+ }
+
+ /* ── One-time global stylesheet injection ────────────── */
+
+ static _injected = false;
+
+ static _injectStyles() {
+ if (BlogCarousel._injected) return;
+ BlogCarousel._injected = true;
+
+ const style = document.createElement("style");
+ style.id = "bc-carousel-styles";
+ style.textContent = /* css */ `
+ /* ── Root ──────────────────────────────────── */
+ .bc-root {
+ display: block;
+ position: relative;
+ }
+
+ /* ── Scroll track (the ) ───────────────── */
+ .bc-track {
+ display: flex;
+ gap: 20px;
+ overflow-x: auto;
+ scroll-snap-type: x mandatory;
+ -webkit-overflow-scrolling: touch;
+ scrollbar-width: none;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ }
+ .bc-track::-webkit-scrollbar { display: none; }
+
+ /* ── Each slide (the ) ─────────────────── */
+ .bc-slide {
+ flex: 0 0 100%;
+ scroll-snap-align: center;
+ min-width: 0;
+ }
+
+ /* ── Prev / Next buttons ───────────────────── */
+ .bc-btn {
+ position: absolute;
+ top: 0;
+ z-index: 4;
+ width: 44px;
+ height: 44px;
+ border-radius: 50%;
+ border: 2px solid #1e293b;
+ background: rgba(255,255,255,0.92);
+ backdrop-filter: blur(6px);
+ -webkit-backdrop-filter: blur(6px);
+ color: #1e293b;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: opacity 0.2s, background 0.2s, box-shadow 0.2s;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.08);
+ padding: 0;
+ }
+ .bc-btn:hover { background: #fff; box-shadow: 0 4px 16px rgba(0,0,0,0.12); }
+ /* Scale only — a translate here would move the button out from under
+ the pointer mid-press and swallow the click (buttons are positioned
+ via an absolute px \`top\`, not a translateY(-50%) baseline). */
+ .bc-btn:active { transform: scale(0.95); }
+ .bc-btn[aria-disabled="true"] {
+ opacity: 0.25;
+ cursor: default;
+ pointer-events: none;
+ }
+ .bc-prev { left: 12px; }
+ .bc-next { right: 12px; }
+
+ /* ── Dots ──────────────────────────────────── */
+ .bc-dots {
+ display: flex;
+ justify-content: center;
+ gap: 8px;
+ margin-top: 16px;
+ }
+ .bc-dot {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ border: 2px solid #1e293b;
+ background: transparent;
+ cursor: pointer;
+ padding: 0;
+ transition: background 0.25s, transform 0.25s;
+ }
+ .bc-dot:hover { background: #475569; }
+ .bc-dot[aria-current="true"] {
+ background: #1e293b;
+ transform: scale(1.2);
+ }
+
+ /* ── Caption ───────────────────────────────── */
+ .bc-caption {
+ text-align: center;
+ font-style: italic;
+ color: #475569;
+ font-size: 0.9rem;
+ line-height: 1.5;
+ margin-top: 12px;
+ min-height: 1.4em;
+ transition: opacity 0.3s;
+ }
+
+ /* ── Video toggle ──────────────────────────── */
+ .bc-video-toggle {
+ position: absolute;
+ bottom: 12px;
+ right: 12px;
+ z-index: 3;
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ border: 2px solid rgba(255,255,255,0.6);
+ background: rgba(0,0,0,0.45);
+ backdrop-filter: blur(4px);
+ -webkit-backdrop-filter: blur(4px);
+ color: #fff;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 0.2s;
+ padding: 0;
+ }
+ .bc-video-toggle:hover { background: rgba(0,0,0,0.65); }
+
+ /* ── Screen-reader live region ─────────────── */
+ .bc-sr-live {
+ position: absolute;
+ width: 1px; height: 1px;
+ overflow: hidden;
+ clip: rect(0,0,0,0);
+ white-space: nowrap;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+
+ /* ── Build controls ──────────────────────────────────── */
+
+ _buildNav() {
+ const makeSVG = (points) =>
+ ` `;
+
+ this._prevBtn = document.createElement("button");
+ this._prevBtn.type = "button";
+ this._prevBtn.className = "bc-btn bc-prev";
+ this._prevBtn.setAttribute("aria-label", "Previous slide");
+ this._prevBtn.innerHTML = makeSVG("13 4 7 10 13 16");
+
+ this._nextBtn = document.createElement("button");
+ this._nextBtn.type = "button";
+ this._nextBtn.className = "bc-btn bc-next";
+ this._nextBtn.setAttribute("aria-label", "Next slide");
+ this._nextBtn.innerHTML = makeSVG("7 4 13 10 7 16");
+
+ this.appendChild(this._prevBtn);
+ this.appendChild(this._nextBtn);
+ }
+
+ _buildDots() {
+ this._dotsWrap = document.createElement("div");
+ this._dotsWrap.className = "bc-dots";
+ this._dotsWrap.setAttribute("role", "tablist");
+ this._dotsWrap.setAttribute("aria-label", "Slide navigation");
+
+ this._dots = [];
+ for (let i = 0; i < this._count; i++) {
+ const dot = document.createElement("button");
+ dot.className = "bc-dot";
+ dot.setAttribute("role", "tab");
+ dot.setAttribute("aria-label", `Go to slide ${i + 1}`);
+ dot.addEventListener("click", () => this._goTo(i));
+ this._dotsWrap.appendChild(dot);
+ this._dots.push(dot);
+ }
+ this.appendChild(this._dotsWrap);
+ }
+
+ _buildCaption() {
+ this._caption = document.createElement("figcaption");
+ this._caption.className = "bc-caption";
+ this.appendChild(this._caption);
+ }
+
+ _buildLiveRegion() {
+ this._live = document.createElement("div");
+ this._live.className = "bc-sr-live";
+ this._live.setAttribute("aria-live", "polite");
+ this._live.setAttribute("aria-atomic", "true");
+ this.appendChild(this._live);
+ }
+
+ /* ── Events ──────────────────────────────────────────── */
+
+ _bind() {
+ this._prevBtn.addEventListener("click", () => this._prev());
+ this._nextBtn.addEventListener("click", () => this._next());
+
+ this._onScroll = () => {
+ /* While a programmatic _goTo scroll is in flight, ignore events */
+ if (this._isScrolling) return;
+ clearTimeout(this._scrollTimer);
+ this._scrollTimer = setTimeout(() => this._onScrollEnd(), 100);
+ };
+ this._ul.addEventListener("scroll", this._onScroll, { passive: true });
+
+ /* Button vertical centering depends on track height, which changes on
+ viewport resize and as slide images finish loading. */
+ this._onResize = () => this._syncState();
+ window.addEventListener("resize", this._onResize);
+ this.querySelectorAll("img").forEach((img) => {
+ if (!img.complete) {
+ img.addEventListener("load", this._onResize, { once: true });
+ }
+ });
+ }
+
+ _onScrollEnd() {
+ const scrollLeft = this._ul.scrollLeft;
+ const slideW = this._slides[0].offsetWidth + 20; /* gap */
+ const idx = Math.round(scrollLeft / slideW);
+ const clamped = Math.max(0, Math.min(idx, this._count - 1));
+ if (clamped !== this._index) {
+ this._index = clamped;
+ this._syncState();
+ }
+ }
+
+ /* ── Navigation ──────────────────────────────────────── */
+
+ _prev() {
+ if (this._index > 0) {
+ this._goTo(this._index - 1);
+ } else if (this.loop) {
+ this._goTo(this._count - 1);
+ }
+ }
+
+ _next() {
+ if (this._index < this._count - 1) {
+ this._goTo(this._index + 1);
+ } else if (this.loop) {
+ this._goTo(0);
+ }
+ }
+
+ _goTo(i) {
+ this._index = i;
+ const slideW = this._slides[0].offsetWidth + 20;
+
+ /* Block the scroll handler while the programmatic scroll is animating */
+ this._isScrolling = true;
+ this._ul.scrollTo({ left: slideW * i, behavior: "smooth" });
+
+ /* Release the guard after animation settles (smooth scroll ~400-500ms) */
+ clearTimeout(this._scrollGuard);
+ this._scrollGuard = setTimeout(() => {
+ this._isScrolling = false;
+ }, 600);
+
+ this._syncState();
+ }
+
+ /* ── Sync UI state ───────────────────────────────────── */
+
+ _syncState() {
+ if (this._count === 0) return;
+ const i = this._index;
+
+ /* Prev / Next */
+ if (!this.loop) {
+ this._prevBtn.setAttribute("aria-disabled", i === 0 ? "true" : "false");
+ this._nextBtn.setAttribute("aria-disabled", i === this._count - 1 ? "true" : "false");
+ } else {
+ this._prevBtn.removeAttribute("aria-disabled");
+ this._nextBtn.removeAttribute("aria-disabled");
+ }
+
+ /* Position buttons vertically centered over the track */
+ const trackH = this._ul.offsetHeight;
+ const btnY = trackH / 2 - 22; /* half button height */
+ this._prevBtn.style.top = btnY + "px";
+ this._nextBtn.style.top = btnY + "px";
+
+ /* Dots */
+ this._dots?.forEach((d, j) => {
+ d.setAttribute("aria-current", j === i ? "true" : "false");
+ });
+
+ /* Caption */
+ const cap = this._slides[i]?.dataset.caption || "";
+ this._caption.textContent = cap;
+
+ /* Live region */
+ this._live.textContent = `Slide ${i + 1} of ${this._count}${cap ? ": " + cap : ""}`;
+
+ /* Video play/pause — active slide plays, all others pause */
+ this._slides.forEach((li, j) => {
+ const video = li.querySelector("video");
+ if (!video) return;
+ if (j === i) {
+ video.currentTime = 0;
+ const p = video.play();
+ if (p) p.catch(() => {}); // swallow AbortError on rapid navigation
+ } else {
+ video.pause();
+ }
+ });
+
+ /* Sync toggle button icons */
+ this._slides.forEach((li, j) => {
+ const video = li.querySelector("video");
+ const toggle = li.querySelector(".bc-video-toggle");
+ if (!video || !toggle) return;
+ if (j === i) {
+ toggle.innerHTML = BlogCarousel._pauseIcon();
+ toggle.setAttribute("aria-label", "Pause video");
+ } else {
+ toggle.innerHTML = BlogCarousel._playIcon();
+ toggle.setAttribute("aria-label", "Play video");
+ }
+ });
+ }
+
+ /* ── Video pause/play toggles ────────────────────────── */
+
+ _initVideoToggles() {
+ this._slides.forEach((li) => {
+ const video = li.querySelector("video");
+ if (!video) return;
+
+ /* The slide's inner wrapper needs position:relative for the toggle */
+ const wrapper = li.firstElementChild;
+ if (wrapper && getComputedStyle(wrapper).position === "static") {
+ wrapper.style.position = "relative";
+ }
+
+ const btn = document.createElement("button");
+ btn.className = "bc-video-toggle";
+ btn.setAttribute("aria-label", "Pause video");
+ btn.innerHTML = BlogCarousel._pauseIcon();
+
+ btn.addEventListener("click", () => {
+ if (video.paused) {
+ video.play().catch(() => {});
+ btn.innerHTML = BlogCarousel._pauseIcon();
+ btn.setAttribute("aria-label", "Pause video");
+ } else {
+ video.pause();
+ btn.innerHTML = BlogCarousel._playIcon();
+ btn.setAttribute("aria-label", "Play video");
+ }
+ });
+
+ /* Append into the wrapper div so it overlays the video */
+ (wrapper || li).appendChild(btn);
+ });
+ }
+
+ static _pauseIcon() {
+ return ` `;
+ }
+
+ static _playIcon() {
+ return ` `;
+ }
+}
+
+customElements.define("blog-carousel", BlogCarousel);
diff --git a/blog/index.html b/blog/index.html
new file mode 100644
index 00000000..3f0cd418
--- /dev/null
+++ b/blog/index.html
@@ -0,0 +1,1842 @@
+
+
+
+
+
+ CBFKit — Control Barrier Functions for Robotics, in JAX
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ As robots move from controlled lab environments into the real world — navigating
+ city streets, sharing factory floors with humans, and flying through cluttered airspace —
+ the question is no longer can they work, but can we guarantee they are safe?
+
+
+
+ Classical control gives us stability. Machine learning gives us adaptability.
+ But neither alone provides formal safety guarantees at runtime — the
+ mathematical promise that a robot will never collide, never exceed a boundary,
+ never enter a dangerous state, regardless of what its nominal controller wants to do.
+
+
+ This is the domain of Control Barrier Functions (CBFs) : a framework that
+ acts as a safety filter on top of any controller, solving a constrained optimization
+ problem at each timestep to find the closest safe action to the desired one. The theory
+ is elegant. The implementations have historically been painful — slow, inflexible,
+ and difficult to extend.
+
+
+ CBFKit addresses this. It is built on
+ JAX , so the safety filter is
+ automatically differentiable, JIT-compiled, and vectorizable — the same code
+ runs a single controller or thousands of Monte Carlo rollouts in parallel.
+
+
+
+
+
+
+
+
+
Architecture
+
Composable Safety by Design
+
+ Every component in CBFKit is a pure function. No class hierarchies, no inheritance chains —
+ just composable building blocks that you snap together into a simulation pipeline.
+
+
+
+
+
+
Planner
+
MPPI / RRT*
+
+
→
+
+
Nominal Ctrl
+
Goal reaching
+
+
→
+
+
Safety Filter
+
CBF-CLF-QP
+
+
→
+
+
Dynamics
+
f(x) + g(x)u
+
+
→
+
+
Integrator
+
RK4 / Euler
+
+
→
+
+
Sensor
+
Noisy / perfect
+
+
→
+
+
Estimator
+
EKF / UKF
+
+
+
+
+
+
+ The safety filter sits at the heart of the pipeline. It receives the nominal control
+ signal — whatever your planner or tracking controller wants to do —
+ and solves a Quadratic Program to find the minimally invasive correction that
+ keeps the system within its safe set. If the nominal command is already safe, it passes through
+ unchanged. If not, the CBF bends it just enough.
+
+
+
+
+
+ minu ‖u − unom ‖²
+ subject to
+ ḣ(x, u) + α(h(x)) ≥ 0
+
+
CBF-QP: find the closest safe action to the desired one
+
+
+
+
+
+
+
+
+
+
+
Showcase
+
Every Tile Is a Runnable Example
+
+ Each animation below is rendered by a script in the repository —
+ open a tile to read the source it came from.
+
+
+
+
+
+
+
+
+
+
Developer Experience
+
Express Safety in a Few Lines
+
+ CBFKit's functional API lets you define dynamics, barriers, and controllers as
+ composable building blocks. A complete safe simulation in under 40 lines.
+
+
+
+
+
import jax.numpy as jnp
+import cbfkit.simulation.simulator as sim
+import cbfkit.systems.unicycle.models.accel_unicycle as unicycle
+from cbfkit.controllers.cbf_clf import vanilla_cbf_clf_qp_controller
+from cbfkit.certificates import concatenate_certificates, rectify_relative_degree
+from cbfkit.certificates.conditions.barrier_conditions import zeroing_barriers
+
+
+dynamics = unicycle.plant (lam=1.0 )
+
+
+nom_controller = unicycle.controllers.proportional_controller (
+ dynamics=dynamics, Kp_pos=1.0 , Kp_theta=1.0
+)
+
+
+barriers = [
+ rectify_relative_degree (
+ function=ellipsoid_cbf (center, radii),
+ system_dynamics=dynamics,
+ state_dim=4 , form="high-order" ,
+ )(certificate_conditions=zeroing_barriers.linear_class_k (5.0 ))
+ for center, radii in obstacles
+]
+
+
+controller = vanilla_cbf_clf_qp_controller (
+ control_limits=jnp.array([100.0 , 100.0 ]),
+ dynamics_func=dynamics,
+ barriers=concatenate_certificates (*barriers),
+)
+
+
+x, u, z, p, *_ = sim.execute (
+ x0=jnp.array([0.0 , 0.0 , 0.0 , 0.785 ]),
+ dt=0.01 , num_steps=1000 ,
+ dynamics=dynamics, controller=controller,
+ nominal_controller=nom_controller,
+ use_jit=True ,
+)
+
+
+
+
+
+
+
+
Safety Guarantees
+
Beyond Vanilla CBFs
+
+ Real-world robots face noise, disturbances, and model uncertainty. CBFKit provides a
+ spectrum of CBF formulations that match the right safety guarantee to the right problem.
+
+
+
+
+
+
+ All formulations share the same compositional API. You can
+ concatenate multiple barriers (obstacle avoidance + workspace bounds +
+ inter-robot separation) into a single QP, swap in a different solver backend
+ (jaxopt, cvxopt, or casadi), and switch between deterministic and stochastic
+ dynamics — all without changing the rest of your pipeline.
+
+
+
+
+
+
+
+
+
Planning
+
MPPI Meets Safety Filters
+
+ Sampling-based planning for complex environments, with CBF safety
+ filtering on the executed trajectory. The best of both worlds.
+
+
+
+ Model Predictive Path Integral (MPPI) control generates
+ candidate trajectories by sampling thousands of control sequences in parallel,
+ weighting them by a user-defined cost function. CBFKit's MPPI implementation
+ runs entirely in JAX — meaning the sampling, rollout, and cost evaluation
+ are all JIT-compiled and GPU-acceleratable.
+
+
+ The MPPI planner outputs a reference trajectory. At each timestep, the CBF
+ safety filter intervenes only if needed , ensuring that the executed
+ control never violates the barrier constraints. This decouples planning quality
+ from safety — you get aggressive, cost-optimal plans with ironclad safety
+ guarantees.
+
+
+
+
+
1000+ parallel samples
+
+ Roll out thousands of candidate trajectories simultaneously using JAX's
+ vectorization primitives. GPU acceleration makes this practical for real-time use.
+
+
+
+
JIT-compiled cost functions
+
+ Define stage and terminal costs as standard Python functions. JAX traces and
+ compiles them once, then executes at native speed for every sample.
+
+
+
+
STL specifications
+
+ Express complex temporal objectives with Signal Temporal Logic. Reach region A
+ within 5 seconds, then stay in region B for 3 seconds — all encoded as
+ MPPI cost terms.
+
+
+
+
+
+
+
+
+
+
Verification
+
GPU-Accelerated Monte Carlo Safety Verification
+
+ Don't just simulate once. Simulate a thousand times in parallel and
+ statistically verify that your controller is safe.
+
+
+
+ CBFKit's Monte Carlo engine uses jax.vmap to run hundreds to thousands
+ of simulations simultaneously — each with different initial conditions, noise
+ realizations, or disturbance profiles. The result is a statistical safety certificate:
+ violation rates, minimum barrier values, and success probabilities
+ across your entire operating envelope.
+
+
+ Combined with the Optuna-powered parameter sweep engine , you can
+ automatically search for the CBF and class-K function parameters that maximize
+ performance while maintaining zero safety violations. The sweep engine tracks
+ per-trial metrics and produces structured JSON results for downstream analysis.
+
+
+
+
+
+
+
+
+
Pre-Built Systems
+
From Single Integrators to 6-DOF Quadrotors
+
+ Eight ready-to-use robotic systems with dynamics, controllers, and barrier
+ functions. Start running experiments in minutes, not days.
+
+
+
+
+
Unicycle
+
Mobile robot with acceleration control. Reach-goal, reach-avoid, and obstacle avoidance examples.
+
+ CBF
+ MPPI
+ Stochastic
+ Risk-Aware
+
+
+
+
Single Integrator
+
First-order dynamics in 2D, 3D, or N-D. The simplest testbed for barrier function research.
+
+ 2D/3D/ND
+ EKF
+ UKF
+
+
+
+
Quadrotor 6-DOF
+
Full six-degree-of-freedom dynamics with geometric and proportional controllers. Multi-robot coordination.
+
+ 3D
+ Multi-Robot
+ Geometric Ctrl
+
+
+
+
Fixed-Wing UAV
+
Beard 2014 kinematic model. Reach-and-drop-point missions with 3D obstacle avoidance.
+
+ 3D Flight
+ Missions
+
+
+
+
Pedestrian
+
Multi-agent pedestrian dynamics with crowd navigation scenarios: crossing, overtaking, head-on, and starburst.
+
+ Multi-Agent
+ Crowd Nav
+
+
+
+
Van der Pol Oscillator
+
Nonlinear limit-cycle dynamics. Lyapunov regulation with fixed-time, stochastic, and risk-aware variants.
+
+ Lyapunov
+ Stochastic
+
+
+
+
Differential Drive
+
Obstacle avoidance and human-aware navigation with barrier-activated and augmented CBF modes.
+
+ Human-Aware
+ Dynamic Obs
+
+
+
+
Double Integrator
+
Position + velocity state space. Used in adaptive CVaR-CBF controller development and testing.
+
+ CVaR
+ Adaptive
+
+
+
+
+
+
+
+
+
+
Visualization
+
Three Backends, One API
+
+ From interactive exploration to publication-quality renders.
+ Choose the visualization backend that fits your workflow.
+
+
+
+
+
+
+
Plotly
+
Interactive 3D HTML visualizations. Rotate, zoom, and inspect trajectories in the browser. Default backend.
+
+
+
+
+
+
Matplotlib
+
High-quality MP4 and GIF export. Publication-ready trajectory plots with obstacle rendering.
+
+
+
+
+
+
Manim
+
3D mathematical animations with distance-metric panels, safety bubbles, and camera motion.
+
+
+
+
+
+
+ The unified CBFAnimator class provides a single interface across all three
+ backends. Visualize 2D trajectories, 3D multi-robot coordination with distance-to-goal
+ panels, minimum inter-robot distance tracking, and per-robot safety metrics —
+ all generated from the same simulation results.
+
+
+
+
+
+
+
+
+
Extensibility
+
Generate New Systems Automatically
+
+ Define dynamics as symbolic expressions. CBFKit's code generator builds a
+ complete system module with controllers, barriers, and ROS 2 nodes.
+
+
+
+ The code generation engine uses Jinja2 templates to transform symbolic
+ dynamics definitions into fully typed Python modules. Specify your drift dynamics as
+ string expressions, provide a control matrix, and list your barrier function formulas.
+ CBFKit generates a complete package: plant model, barrier functions with automatic
+ differentiation, Lyapunov functions, and optionally ROS 2 nodes for deployment.
+
+
+ This is how multi-robot coordination scales. Instead of hand-coding N(N-1)/2 inter-robot
+ collision avoidance barriers, you define the pattern once and generate for any number of
+ agents. The 3D multi-robot tutorial demonstrates this with 4 robots, 6 pairwise collision
+ barriers, and 4 obstacle barriers — all auto-generated and composed into a single QP.
+
+
+
+
+
+
+
+
+
Getting Started
+
Tutorials for Every Level
+
+ From first barrier function to multi-robot 3D reach-avoid with MPPI.
+ Each tutorial builds on the last.
+
+
+
+
01
+
Code Generation Tutorial
+
+ Generate a Van der Pol oscillator system from scratch —
+ dynamics, controllers, barriers, and ROS 2 nodes.
+
+
+
+
02
+
Multi-Robot Coordination
+
+ N-robot collision avoidance with inter-robot CBF constraints.
+ Composable barrier packaging for scalable multi-agent safety.
+
+
+
+
03
+
MPPI + CBF Reach-Avoid
+
+ Sampling-based planning with safety filtering.
+ Navigate complex obstacle fields with formal guarantees.
+
+
+
+
04
+
MPPI + STL Specifications
+
+ Signal Temporal Logic objectives encoded as MPPI cost terms.
+ Express complex time-bounded reach-avoid missions.
+
+
+
+
05
+
Dynamic Obstacles
+
+ Single integrator with moving obstacles. Time-varying barrier
+ functions that track obstacle state.
+
+
+
+
06
+
3D Multi-Robot Reach-Avoid
+
+ The full stack: 4 robots in 3D, MPPI planning, CBF safety,
+ code generation, and Manim visualization.
+
+
+
+
+
+
+
+
+
+
+
Get started
+
+ CBFKit is open source and ready for your research.
+ Install it in one line, run an example in five minutes.
+
+
+
+ pip install cbfkit
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/gymnasium/safe_single_integrator.ipynb b/examples/gymnasium/safe_single_integrator.ipynb
new file mode 100644
index 00000000..e05878d9
--- /dev/null
+++ b/examples/gymnasium/safe_single_integrator.ipynb
@@ -0,0 +1,176 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# CBFKit \u2014 Safe Reinforcement Learning Quickstart\n",
+ "\n",
+ "[](https://colab.research.google.com/github/bardhh/cbfkit/blob/main/examples/gymnasium/safe_single_integrator.ipynb)\n",
+ "\n",
+ "**CBFKit** turns any continuous-control Gymnasium environment into a *provably safe* one: every action from your policy is projected by a Control Barrier Function (CBF) quadratic program before it reaches the simulator. No retraining, no reward shaping \u2014 the safety filter is a drop-in wrapper.\n",
+ "\n",
+ "This notebook runs the **same** naive \"drive straight at the goal\" policy twice \u2014 once raw, once wrapped in a CBF safety filter \u2014 and plots the difference."
+ ],
+ "id": "cell-00"
+ },
+ {
+ "cell_type": "code",
+ "metadata": {},
+ "execution_count": null,
+ "outputs": [],
+ "source": [
+ "# Install CBFKit (with the Gymnasium extra) straight from GitHub.\n",
+ "%pip install \"cbfkit[gymnasium] @ git+https://github.com/bardhh/cbfkit.git\""
+ ],
+ "id": "cell-01"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 1. Set up the environment and a (deliberately naive) policy\n",
+ "\n",
+ "`CBFKit/SafeSingleIntegratorObstacles-v0` is a 2-D point robot that must reach a goal without hitting circular obstacles. Our policy just drives straight at the goal \u2014 it knows nothing about the obstacles."
+ ],
+ "id": "cell-02"
+ },
+ {
+ "cell_type": "code",
+ "metadata": {},
+ "execution_count": null,
+ "outputs": [],
+ "source": [
+ "import numpy as np\n",
+ "import jax.numpy as jnp\n",
+ "import matplotlib.pyplot as plt\n",
+ "import gymnasium\n",
+ "\n",
+ "from cbfkit.envs.gymnasium import circular_obstacle_barriers, register_envs\n",
+ "from cbfkit.systems.single_integrator.dynamics import two_dimensional_single_integrator\n",
+ "from cbfkit.wrappers.gymnasium import SafetyFilterWrapper\n",
+ "\n",
+ "register_envs()\n",
+ "SEED, MAX_STEPS = 42, 200\n",
+ "\n",
+ "def naive_policy(obs):\n",
+ " \"\"\"Drive straight toward the goal (ignores obstacles).\"\"\"\n",
+ " pos, goal = obs[:2], obs[2:4]\n",
+ " direction = goal - pos\n",
+ " dist = np.linalg.norm(direction)\n",
+ " return (direction / dist).astype(np.float32) if dist > 1e-6 else np.zeros(2, np.float32)\n",
+ "\n",
+ "def run_episode(env, seed=SEED, max_steps=MAX_STEPS):\n",
+ " obs, _ = env.reset(seed=seed)\n",
+ " traj, collision, goal_reached, interventions = [obs[:2].copy()], False, False, 0\n",
+ " for _ in range(max_steps):\n",
+ " obs, _, terminated, truncated, info = env.step(naive_policy(obs))\n",
+ " traj.append(obs[:2].copy())\n",
+ " if info.get(\"safety_filter\", {}).get(\"intervened\"):\n",
+ " interventions += 1\n",
+ " if terminated:\n",
+ " collision = info.get(\"collision\", False)\n",
+ " goal_reached = info.get(\"goal_reached\", False)\n",
+ " break\n",
+ " if truncated:\n",
+ " break\n",
+ " return np.array(traj), collision, goal_reached, interventions"
+ ],
+ "id": "cell-03"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 2. Run it twice: raw policy vs. CBF-filtered policy\n",
+ "\n",
+ "The only difference is `SafetyFilterWrapper.from_cbf_qp(...)`, which wraps the env so every action is projected onto the safe set by a CBF quadratic program. The policy itself is byte-for-byte identical."
+ ],
+ "id": "cell-04"
+ },
+ {
+ "cell_type": "code",
+ "metadata": {},
+ "execution_count": null,
+ "outputs": [],
+ "source": [
+ "# --- Raw policy (no safety filter) ---\n",
+ "env_unsafe = gymnasium.make(\"CBFKit/SafeSingleIntegratorObstacles-v0\")\n",
+ "traj_u, coll_u, goal_u, _ = run_episode(env_unsafe)\n",
+ "\n",
+ "# --- Same policy, wrapped in a CBF safety filter ---\n",
+ "env_safe = gymnasium.make(\"CBFKit/SafeSingleIntegratorObstacles-v0\")\n",
+ "barriers = circular_obstacle_barriers(env_safe.unwrapped.obstacles, alpha=1.0)\n",
+ "safe_env = SafetyFilterWrapper.from_cbf_qp(\n",
+ " env_safe,\n",
+ " dynamics=two_dimensional_single_integrator(),\n",
+ " barriers=barriers,\n",
+ " control_limits=jnp.array([1.0, 1.0]),\n",
+ " obs_to_state=lambda o: o[:2],\n",
+ ")\n",
+ "traj_s, coll_s, goal_s, interventions = run_episode(safe_env)\n",
+ "\n",
+ "print(f\"Naive policy : collision={coll_u!s:<5} goal_reached={goal_u}\")\n",
+ "print(f\"CBF-filtered : collision={coll_s!s:<5} goal_reached={goal_s} interventions={interventions}\")"
+ ],
+ "id": "cell-05"
+ },
+ {
+ "cell_type": "code",
+ "metadata": {},
+ "execution_count": null,
+ "outputs": [],
+ "source": [
+ "fig, axes = plt.subplots(1, 2, figsize=(12, 5))\n",
+ "obstacles = env_unsafe.unwrapped.obstacles\n",
+ "goal = env_unsafe.unwrapped._default_goal\n",
+ "for ax, traj, title in [(axes[0], traj_u, \"Naive policy (no CBF)\"),\n",
+ " (axes[1], traj_s, \"Same policy + CBF safety filter\")]:\n",
+ " for cx, cy, rad in obstacles:\n",
+ " ax.add_patch(plt.Circle((cx, cy), rad, color=\"red\", alpha=0.3))\n",
+ " ax.plot(*goal, \"g*\", ms=15, label=\"Goal\")\n",
+ " ax.plot(traj[:, 0], traj[:, 1], \"b-\", lw=2, label=\"Trajectory\")\n",
+ " ax.plot(traj[0, 0], traj[0, 1], \"bs\", ms=8, label=\"Start\")\n",
+ " ax.plot(traj[-1, 0], traj[-1, 1], \"bo\", ms=8)\n",
+ " ax.set_xlim(-0.5, 5); ax.set_ylim(-1.5, 1.5); ax.set_aspect(\"equal\")\n",
+ " ax.set_title(title); ax.legend(loc=\"upper left\", fontsize=8); ax.grid(alpha=0.3)\n",
+ "plt.tight_layout(); plt.show()"
+ ],
+ "id": "cell-06"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## What you're seeing\n",
+ "\n",
+ "- **Left:** the naive policy drives straight through the obstacle and collides.\n",
+ "- **Right:** the *same* policy, wrapped in the CBF safety filter, is minimally deflected around the obstacle and still reaches the goal. `interventions` counts the steps where the filter had to modify the action.\n",
+ "\n",
+ "At each step the filter solves a small quadratic program \u2014 it finds the action closest to the policy's action that still satisfies the barrier condition $\\dot h(x) \\ge -\\alpha\\, h(x)$.\n",
+ "\n",
+ "### Where to go next\n",
+ "- Swap the naive policy for a trained **PPO/SAC** agent \u2014 the wrapper is unchanged.\n",
+ "- Try the other CBF variants (robust, stochastic, risk-aware) under [`examples/`](https://github.com/bardhh/cbfkit/tree/main/examples).\n",
+ "- Read the paper: https://arxiv.org/abs/2404.07158\n",
+ "- Star the repo: https://github.com/bardhh/cbfkit"
+ ],
+ "id": "cell-07"
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "name": "python"
+ },
+ "colab": {
+ "provenance": []
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/pyproject.toml b/pyproject.toml
index 4738684c..9756d433 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,11 +10,26 @@ authors = [
{name = "Hideki Okamoto"}
]
license = "BSD-3-Clause"
+keywords = [
+ "control-barrier-functions",
+ "safe-control",
+ "safe-reinforcement-learning",
+ "robotics",
+ "jax",
+ "optimal-control",
+ "model-predictive-control",
+ "autonomous-systems",
+]
classifiers = [
+ "Development Status :: 4 - Beta",
+ "Intended Audience :: Science/Research",
+ "Topic :: Scientific/Engineering",
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
+ "Operating System :: OS Independent",
]
readme = "README.md"
requires-python = ">=3.10,<3.13"
@@ -25,12 +40,7 @@ dependencies = [
"tqdm>=4.60",
"pyyaml>=6.0",
"matplotlib>=3.8.2,<4.0",
- "plotly>=5.18,<7.0",
- "black>=23.12.1,<27.0",
"jinja2>=3.0.0",
- "casadi>=3.6.4,<4.0",
- "cvxopt>=1.3.2,<2.0; platform_machine != 'aarch64' and platform_machine != 'arm64'",
- "kvxopt>=1.3.2.0,<2.0; platform_machine == 'aarch64' or platform_machine == 'arm64'",
"scipy>=1.9,<2.0",
]
@@ -38,32 +48,56 @@ dependencies = [
[project.scripts]
cbfkit-bench = "cbfkit.cli.bench:main"
[project.urls]
+Homepage = "https://github.com/bardhh/cbfkit"
Repository = "https://github.com/bardhh/cbfkit"
Documentation = "https://github.com/bardhh/cbfkit#readme"
Issues = "https://github.com/bardhh/cbfkit/issues"
+Changelog = "https://github.com/bardhh/cbfkit/releases"
+"Paper (arXiv)" = "https://arxiv.org/abs/2404.07158"
[project.optional-dependencies]
-manim = [
- "manim>=0.18,<1.0",
-]
-optuna = [
- "optuna>=3.0",
-]
-neural = [
- "flax>=0.8",
- "optax>=0.2",
+# --- Single-purpose extras ---
+manim = ["manim>=0.18,<1.0"]
+optuna = ["optuna>=3.0"]
+neural = ["flax>=0.8", "optax>=0.2"]
+gymnasium = ["gymnasium>=1.0"]
+plotly = ["plotly>=5.18,<7.0"]
+casadi = ["casadi>=3.6.4,<4.0"]
+cvxopt = [
+ "cvxopt>=1.3.2,<2.0; platform_machine != 'aarch64' and platform_machine != 'arm64'",
+ "kvxopt>=1.3.2.0,<2.0; platform_machine == 'aarch64' or platform_machine == 'arm64'",
]
-gymnasium = [
- "gymnasium>=1.0",
+# Black formats the code emitted by the codegen pipeline (run_black in
+# codegen/create_new_system/generate_model.py). jinja2 itself is a core dep,
+# so code generation works out of the box; this extra only adds the formatter.
+codegen = ["black>=23.12.1,<27.0"]
+
+# --- Convenience umbrella extras ---
+solvers = ["cbfkit[casadi]", "cbfkit[cvxopt]"]
+vis = ["cbfkit[plotly]"]
+# Everything installable without system-level dependencies. Excludes `manim`,
+# which additionally requires ffmpeg and a LaTeX toolchain.
+all = [
+ "cbfkit[optuna]",
+ "cbfkit[neural]",
+ "cbfkit[gymnasium]",
+ "cbfkit[plotly]",
+ "cbfkit[casadi]",
+ "cbfkit[cvxopt]",
+ "cbfkit[codegen]",
]
+
dev = [
+ "cbfkit[all]",
"pytest>=8.0.2",
"pytest-cov",
"python-dotenv>=1.2.1",
"mypy>=1.8.0,<2.0",
"jupyter>=1.0.0,<2.0",
"ruff>=0.3.4",
- "gymnasium>=1.0",
+ "black>=23.12.1,<27.0",
+ "build>=1.0",
+ "twine>=5.0",
]
[build-system]
diff --git a/src/cbfkit/VERSION b/src/cbfkit/VERSION
index 17e51c38..0d91a54c 100644
--- a/src/cbfkit/VERSION
+++ b/src/cbfkit/VERSION
@@ -1 +1 @@
-0.1.1
+0.3.0
diff --git a/src/cbfkit/benchmarks/unicycle_sweep.py b/src/cbfkit/benchmarks/unicycle_sweep.py
index 1096dbb7..26db9f9b 100644
--- a/src/cbfkit/benchmarks/unicycle_sweep.py
+++ b/src/cbfkit/benchmarks/unicycle_sweep.py
@@ -380,7 +380,6 @@ def _unicycle_batch_runner(seeds: list[int], params: dict) -> list[dict]:
# Generate keys and initial states for all seeds × trials
n_seeds = len(seeds)
- total = n_seeds * N_TRIALS
all_keys = []
all_sampler_keys = []
for seed in seeds:
diff --git a/src/cbfkit/codegen/create_new_system/generate_model.py b/src/cbfkit/codegen/create_new_system/generate_model.py
index 5e766617..7ac92ec5 100644
--- a/src/cbfkit/codegen/create_new_system/generate_model.py
+++ b/src/cbfkit/codegen/create_new_system/generate_model.py
@@ -32,7 +32,8 @@
from jinja2 import Environment, FileSystemLoader
except ImportError as exc:
raise ImportError(
- "Code generation requires 'jinja2'. Please install it with: pip install cbfkit[codegen]"
+ "Code generation requires 'jinja2', which normally ships with cbfkit. "
+ "Reinstall the package with: pip install cbfkit"
) from exc
op_sys = platform.system()
@@ -76,7 +77,12 @@ def create_python_file(directory: str, file_name: str, file_contents: str):
def run_black(file_path: str) -> None:
- """Format the given file with Black."""
+ """Format the given file with Black, if Black is available.
+
+ Black ships in the optional ``codegen`` extra (``pip install cbfkit[codegen]``).
+ When it is not installed the file is left unformatted (still valid Python) and
+ a warning is logged, so code generation works on a slim ``pip install cbfkit``.
+ """
try:
subprocess.run(
["black", file_path],
@@ -84,12 +90,12 @@ def run_black(file_path: str) -> None:
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
- except FileNotFoundError as exc:
- LOGGER.error("Black executable not found while formatting %s", file_path)
- raise RuntimeError(
- "Black executable not found. Please ensure Black is installed and available. "
- "You can install it with: pip install cbfkit[codegen]"
- ) from exc
+ except FileNotFoundError:
+ LOGGER.warning(
+ "Black is not installed; leaving %s unformatted. Install the codegen "
+ "extra for auto-formatted output: pip install cbfkit[codegen]",
+ file_path,
+ )
except subprocess.CalledProcessError as exc:
stderr = exc.stderr.decode().strip() if exc.stderr else str(exc)
LOGGER.error("Black failed to format %s: %s", file_path, stderr)
diff --git a/src/cbfkit/controllers/adaptive_cvar_cbf.py b/src/cbfkit/controllers/adaptive_cvar_cbf.py
index 006d52d0..fef83770 100644
--- a/src/cbfkit/controllers/adaptive_cvar_cbf.py
+++ b/src/cbfkit/controllers/adaptive_cvar_cbf.py
@@ -153,6 +153,11 @@ def solve(
return u_opt, log_data
def _solve_one_iter(self, x, u_nom, robot_pmf, robot_wu, robot_wx, obs_pmfs, obs_wu, obs_wx):
+ if ca is None:
+ raise ImportError(
+ "The adaptive CVaR-CBF controller requires CasADi. "
+ "Install it with `pip install cbfkit[casadi]`."
+ )
u = ca.MX.sym("u", self.m, 1)
n_zeta = len(self.obstacles) # + len(all_robots) - 1 (assuming single robot for now)
n_eta = n_zeta * self.S
diff --git a/src/cbfkit/ros/_experiment.py b/src/cbfkit/ros/_experiment.py
index 3cf00ed5..f6e4adf8 100755
--- a/src/cbfkit/ros/_experiment.py
+++ b/src/cbfkit/ros/_experiment.py
@@ -9,6 +9,7 @@
Covariance,
Estimate,
EstimatorCallable,
+ Time,
)
diff --git a/src/cbfkit/simulation/monte_carlo_gpu.py b/src/cbfkit/simulation/monte_carlo_gpu.py
index 4bd439b3..0ceaf3d9 100644
--- a/src/cbfkit/simulation/monte_carlo_gpu.py
+++ b/src/cbfkit/simulation/monte_carlo_gpu.py
@@ -186,7 +186,6 @@ def conduct_monte_carlo_gpu_multiseed(
A list of ``MonteCarloGPUResults``, one per seed.
"""
n_seeds = len(seeds)
- total = n_seeds * n_trials
# Generate keys and initial states for all seeds × trials
all_keys_list = []
diff --git a/src/cbfkit/utils/animators/animator.py b/src/cbfkit/utils/animators/animator.py
index 80a7364f..33001b91 100644
--- a/src/cbfkit/utils/animators/animator.py
+++ b/src/cbfkit/utils/animators/animator.py
@@ -57,9 +57,12 @@ def __init__(
if backend == "matplotlib":
_require_matplotlib()
elif backend.startswith("manim"):
- _require_manim()
- raise NotImplementedError("Manim 2D backend not yet implemented. "
- "Use visualize_3d_multi_robot(backend='manim') for 3D scenes.")
+ # The 2D Manim backend is unimplemented whether or not Manim is
+ # installed, so surface that before requiring the optional dependency.
+ raise NotImplementedError(
+ "Manim 2D backend not yet implemented. "
+ "Use visualize_3d_multi_robot(backend='manim') for 3D scenes."
+ )
else:
_require_plotly()
@@ -81,9 +84,9 @@ def __init__(
self._frame_callbacks: List[Callable] = []
# Built objects (created by build / animate)
- self._fig = None # matplotlib Figure or Plotly Figure
- self._ax = None # matplotlib Axes (None for Plotly)
- self._anim = None # matplotlib FuncAnimation (None for Plotly)
+ self._fig = None # matplotlib Figure or Plotly Figure
+ self._ax = None # matplotlib Axes (None for Plotly)
+ self._anim = None # matplotlib FuncAnimation (None for Plotly)
self._traj_artists: List = []
self._agent_artists: List = []
self._prediction_artists: List = []
@@ -99,9 +102,7 @@ def add_goal(
label: str = "Goal",
) -> "CBFAnimator":
"""Add a goal marker (filled dot + dashed circle)."""
- self._goals.append(
- {"position": position, "radius": radius, "color": color, "label": label}
- )
+ self._goals.append({"position": position, "radius": radius, "color": color, "label": label})
return self
def add_obstacle(
@@ -381,7 +382,10 @@ def _compute_prediction(self, spec: dict, frame: int):
if traj_data is not None and frame < len(traj_data):
traj = np.asarray(traj_data[frame])
if traj.ndim >= 2 and traj.shape[0] > max(spec["traj_x_row"], spec["traj_y_row"]):
- return traj[spec["traj_x_row"], :].tolist(), traj[spec["traj_y_row"], :].tolist()
+ return (
+ traj[spec["traj_x_row"], :].tolist(),
+ traj[spec["traj_y_row"], :].tolist(),
+ )
return [], []
return [], []
diff --git a/src/cbfkit/utils/visualizations/manim_3d_multi_robot.py b/src/cbfkit/utils/visualizations/manim_3d_multi_robot.py
index 24a91c7f..1c6a7700 100644
--- a/src/cbfkit/utils/visualizations/manim_3d_multi_robot.py
+++ b/src/cbfkit/utils/visualizations/manim_3d_multi_robot.py
@@ -43,6 +43,9 @@
_MANIM_AVAILABLE = True
except ImportError:
_MANIM_AVAILABLE = False
+ # Fallback base class so this module still imports when Manim is absent;
+ # actual rendering is gated by _require_manim() in render_multi_robot_3d().
+ ThreeDScene = object # type: ignore[assignment,misc]
# ---------------------------------------------------------------------------
@@ -245,9 +248,7 @@ def s(p: np.ndarray) -> np.ndarray:
goal_pos = s(goals[idx : idx + 3])
color = _robot_color(i)
goal_dot = Dot3D(point=goal_pos, radius=0.15, color=color).set_opacity(0.9)
- goal_sphere = _goal_sphere(
- goal_pos, self.desired_state_radius * self._scale, color
- )
+ goal_sphere = _goal_sphere(goal_pos, self.desired_state_radius * self._scale, color)
self.add(goal_dot, goal_sphere)
# --- Animated robot dots + safety bubbles + traced paths -----------
diff --git a/src/cbfkit/utils/visualizations/plot_mppi_ffmpeg.py b/src/cbfkit/utils/visualizations/plot_mppi_ffmpeg.py
index 6e2bd560..695411ef 100644
--- a/src/cbfkit/utils/visualizations/plot_mppi_ffmpeg.py
+++ b/src/cbfkit/utils/visualizations/plot_mppi_ffmpeg.py
@@ -72,7 +72,6 @@ def animate(
from cbfkit.utils.animator import AnimationConfig, CBFAnimator
states_np = np.asarray(states)
- estimates_np = np.asarray(estimates)
animator = CBFAnimator(
states_np,
@@ -108,9 +107,7 @@ def animate(
(selected_line,) = ax.plot([], [], "b", linewidth=2)
sampled_key = (
- "sampled_x_traj"
- if "sampled_x_traj" in controller_data_keys
- else "robot_sampled_states"
+ "sampled_x_traj" if "sampled_x_traj" in controller_data_keys else "robot_sampled_states"
)
state_dim = mppi_args["robot_state_dim"]