diff --git a/README.md b/README.md
index e14c11ad..607de288 100755
--- a/README.md
+++ b/README.md
@@ -137,15 +137,15 @@ 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
|
@@ -155,17 +155,17 @@ python tutorials/multi_robot_3d_reachavoid.py

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

@@ -179,21 +179,21 @@ python tutorials/multi_robot_3d_reachavoid.py
|

- 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/examples/double_integrator/__init__.py b/examples/double_integrator/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/double_integrator/mpc_tracking.py b/examples/double_integrator/mpc_tracking.py
new file mode 100644
index 00000000..53eda273
--- /dev/null
+++ b/examples/double_integrator/mpc_tracking.py
@@ -0,0 +1,99 @@
+"""Receding-horizon MPC tracking a goal with an LTI double integrator."""
+import os
+import sys
+
+# Add the project root to the path so we can import cbfkit + examples.
+root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
+if root_path not in sys.path:
+ sys.path.insert(0, root_path)
+
+import jax.numpy as jnp
+import numpy as np
+
+from cbfkit.optimization.mpc.quadratic_cost_linear_dynamics import (
+ generate_mpc_solver_quadratic_cost_linear_dynamics,
+)
+
+# In test mode we shorten the horizon loop and skip the (slow) GIF render.
+TEST_MODE = bool(os.getenv("CBFKIT_TEST_MODE"))
+
+
+def main() -> float:
+ """Run the receding-horizon MPC loop and (optionally) save the animation.
+
+ Returns the final Euclidean position error to the goal.
+ """
+ dt = 0.1
+ # Discrete-time double integrator: state [px, py, vx, vy], control [ax, ay].
+ A = jnp.array(
+ [[1.0, 0.0, dt, 0.0], [0.0, 1.0, 0.0, dt], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]]
+ )
+ B = jnp.array([[0.0, 0.0], [0.0, 0.0], [dt, 0.0], [0.0, dt]])
+ Q = jnp.diag(jnp.array([10.0, 10.0, 1.0, 1.0]))
+ R = 0.1 * jnp.eye(2)
+ Qn = 50.0 * Q
+ N = 20
+ solve = generate_mpc_solver_quadratic_cost_linear_dynamics(A, B, Q, R, Qn, N)
+
+ goal = jnp.array([4.0, 4.0, 0.0, 0.0])
+ ref_horizon = jnp.tile(goal, (N, 1)) # (N, 4) constant reference over the horizon
+ x = jnp.array([0.0, 0.0, 0.0, 0.0])
+ n_steps = 40 if not TEST_MODE else 5
+
+ xs = [np.asarray(x)]
+ preds = []
+ for _ in range(n_steps):
+ concatenated_x_xr = jnp.vstack([x.reshape(1, -1), ref_horizon]) # (N+1, 4)
+ x_opt, u_opt = solve(concatenated_x_xr) # x_opt (4, N+1), u_opt (2, N)
+ u = u_opt[:, 0]
+ x = A @ x + B @ u
+ xs.append(np.asarray(x))
+ preds.append(np.asarray(x_opt.T)) # (N+1, 4) predicted state horizon
+ xs = np.array(xs)
+
+ final_err = float(np.linalg.norm(xs[-1, :2] - np.asarray(goal)[:2]))
+ print(f"Final position error to goal: {final_err:.4f}")
+
+ if TEST_MODE:
+ return final_err
+
+ import matplotlib.pyplot as plt
+ from matplotlib.animation import FuncAnimation, PillowWriter
+
+ fig, ax = plt.subplots(figsize=(6, 6))
+ ax.plot(float(goal[0]), float(goal[1]), "g*", markersize=18, label="Goal")
+ (realized,) = ax.plot([], [], "b-", lw=2, label="Realized")
+ (pred,) = ax.plot(
+ [], [], color="orange", ls="--", lw=1.5, alpha=0.85, label="Predicted horizon"
+ )
+ dot = ax.scatter([], [], s=80, color="blue", zorder=5)
+ ax.set_xlim(-0.5, 4.5)
+ ax.set_ylim(-0.5, 4.5)
+ ax.set_aspect("equal")
+ ax.set_xlabel("x")
+ ax.set_ylabel("y")
+ ax.set_title("Model Predictive Control — receding-horizon LTI tracking", fontsize=10)
+ ax.legend(loc="lower right", fontsize=9)
+ ax.grid(True, alpha=0.3)
+
+ def update(i):
+ realized.set_data(xs[: i + 1, 0], xs[: i + 1, 1])
+ dot.set_offsets([[xs[i, 0], xs[i, 1]]])
+ p = preds[min(i, len(preds) - 1)]
+ pred.set_data(p[:, 0], p[:, 1])
+ return realized, pred, dot
+
+ anim = FuncAnimation(fig, update, frames=len(xs), interval=100, blit=True)
+
+ results_dir = os.path.join(os.path.dirname(__file__), "results")
+ os.makedirs(results_dir, exist_ok=True)
+ out = os.path.join(results_dir, "mpc_tracking.gif")
+ anim.save(out, writer=PillowWriter(fps=10))
+ plt.close(fig)
+ print(f"Saved animation to {out}")
+
+ return final_err
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/quadrotor_6dof/__init__.py b/examples/quadrotor_6dof/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/quadrotor_6dof/geometric_tracking.py b/examples/quadrotor_6dof/geometric_tracking.py
new file mode 100644
index 00000000..1c913ef6
--- /dev/null
+++ b/examples/quadrotor_6dof/geometric_tracking.py
@@ -0,0 +1,150 @@
+"""6-DOF quadrotor geometric SE(3) tracking with a live altitude-CBF barrier value."""
+import os
+import sys
+
+# Add the project root to the path so cbfkit + examples imports resolve.
+root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
+if root_path not in sys.path:
+ sys.path.insert(0, root_path)
+
+import jax.numpy as jnp
+import numpy as np
+
+import cbfkit.simulation.simulator as sim
+from cbfkit.estimators import naive as estimator
+from cbfkit.integration import runge_kutta_4 as integrator
+from cbfkit.sensors import perfect as sensor
+from cbfkit.systems.quadrotor_6dof.certificates.barrier_functions import h_alt
+from cbfkit.systems.quadrotor_6dof.controllers.geometric import geometric_controller
+from cbfkit.systems.quadrotor_6dof.models.quadrotor_6dof_dynamics import (
+ quadrotor_6dof_dynamics,
+)
+
+TEST_MODE = bool(os.getenv("CBFKIT_TEST_MODE"))
+
+
+def main() -> None:
+ # Mass/inertia must be consistent between plant and controller: geometric_controller's
+ # default gains are tuned for m≈4.34 kg, while quadrotor_6dof_dynamics defaults to
+ # m=0.25 kg. Mismatch -> instant integration NaN. Use the heavier plant.
+ m, jx, jy, jz = 4.34, 0.0820, 0.0845, 0.1377
+ three_tuple = quadrotor_6dof_dynamics(m=m, jx=jx, jy=jy, jz=jz)
+
+ def dyn(x):
+ f, g, _s = three_tuple(x)
+ return f, g
+
+ desired = jnp.array([2.0, 1.5, 3.0]) # target (pn, pe, h)
+ dt = 0.01
+ tf = 6.0 if not TEST_MODE else 0.5
+ n = int(tf / dt)
+
+ # state layout: [pn, pe, h, u, v, w, phi, theta, psi, p, q, r]
+ x0 = jnp.array([0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
+
+ nominal = geometric_controller(
+ dynamics=dyn, desired_state=desired, dt=dt, m=m, jx=jx, jy=jy, jz=jz
+ )
+
+ res = sim.execute(
+ x0=x0,
+ dt=dt,
+ num_steps=n,
+ dynamics=dyn,
+ integrator=integrator,
+ nominal_controller=nominal,
+ sensor=sensor,
+ estimator=estimator,
+ use_jit=True,
+ )
+ states = np.asarray(res["states"]) # (n+1, 12)
+
+ # Altitude-CBF barrier value h_alt(z, alt_limit). z = hstack([x, t]).
+ # alt_limit must comfortably exceed our setpoint altitude (3 m) — pick 5 m.
+ alt_limit = 5.0
+ n_states_full = states.shape[0]
+ ts = np.linspace(0.0, tf, n_states_full)
+ h_vals = np.array(
+ [
+ float(h_alt(jnp.hstack([jnp.asarray(states[i]), jnp.asarray(ts[i])]), alt_limit))
+ for i in range(n_states_full)
+ ]
+ )
+
+ final_pos = states[-1, :3]
+ dist = float(np.linalg.norm(final_pos - np.asarray(desired)))
+ print(f"Final distance to goal: {dist:.4f} m")
+ print(f"Minimum altitude-CBF value h_alt: {float(h_vals.min()):.4f} (positive => safe)")
+
+ if TEST_MODE:
+ return
+
+ import matplotlib.pyplot as plt
+ from matplotlib.animation import FuncAnimation, PillowWriter
+ from mpl_toolkits.mplot3d import Axes3D # noqa: F401 — registers 3D projection
+
+ # Subsample frames for a compact GIF.
+ stride = max(1, n_states_full // 80)
+ idx = np.arange(0, n_states_full, stride)
+ pn, pe, h_alt_traj = states[idx, 0], states[idx, 1], states[idx, 2]
+
+ fig = plt.figure(figsize=(10, 5))
+ ax3d = fig.add_subplot(1, 2, 1, projection="3d")
+ ax_h = fig.add_subplot(1, 2, 2)
+
+ ax3d.scatter(
+ [float(desired[0])],
+ [float(desired[1])],
+ [float(desired[2])],
+ color="green",
+ s=120,
+ marker="*",
+ label="Goal",
+ zorder=10,
+ )
+ (line3d,) = ax3d.plot([], [], [], "b-", lw=2, label="Quadrotor")
+ dot3d = ax3d.scatter([], [], [], s=60, color="blue", zorder=11)
+ pad = 0.5
+ ax3d.set_xlim(min(pn.min(), float(desired[0])) - pad, max(pn.max(), float(desired[0])) + pad)
+ ax3d.set_ylim(min(pe.min(), float(desired[1])) - pad, max(pe.max(), float(desired[1])) + pad)
+ ax3d.set_zlim(0, alt_limit + 0.5)
+ ax3d.set_xlabel("pn [m]")
+ ax3d.set_ylabel("pe [m]")
+ ax3d.set_zlabel("h [m]")
+ ax3d.set_title("Quadrotor 6-DOF — geometric SE(3) tracking", fontsize=10)
+ ax3d.legend(loc="upper right", fontsize=8)
+ ax3d.view_init(elev=22, azim=-60)
+
+ # h(z) trace: stays >0 ⇒ altitude envelope satisfied.
+ ax_h.plot(ts, h_vals, color="purple", lw=1.5)
+ (h_dot,) = ax_h.plot([], [], "o", color="purple", markersize=7)
+ ax_h.axhline(0.0, color="red", ls="--", lw=1, alpha=0.7, label="Safety boundary h=0")
+ ax_h.set_xlim(0, tf)
+ ax_h.set_ylim(min(0.0, float(h_vals.min())) - 0.1, max(1.0, float(h_vals.max())) + 0.1)
+ ax_h.set_xlabel("t [s]")
+ ax_h.set_ylabel("$h_{\\rm alt}(z)$")
+ ax_h.set_title("Altitude-CBF barrier value (positive ⇒ safe)", fontsize=10)
+ ax_h.legend(loc="lower right", fontsize=8)
+ ax_h.grid(True, alpha=0.3)
+
+ def update(i):
+ line3d.set_data(pn[: i + 1], pe[: i + 1])
+ line3d.set_3d_properties(h_alt_traj[: i + 1])
+ dot3d._offsets3d = ([pn[i]], [pe[i]], [h_alt_traj[i]])
+ # Map subsampled index back to full-resolution h_vals index for the dot.
+ full_i = idx[i]
+ h_dot.set_data([ts[full_i]], [h_vals[full_i]])
+ return line3d, dot3d, h_dot
+
+ anim = FuncAnimation(fig, update, frames=len(idx), interval=100, blit=False)
+
+ results_dir = os.path.join(os.path.dirname(__file__), "results")
+ os.makedirs(results_dir, exist_ok=True)
+ out = os.path.join(results_dir, "geometric_tracking.gif")
+ anim.save(out, writer=PillowWriter(fps=10))
+ plt.close(fig)
+ print(f"Saved animation to {out}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/single_integrator/monte_carlo_safety.py b/examples/single_integrator/monte_carlo_safety.py
new file mode 100644
index 00000000..c2ee5bc7
--- /dev/null
+++ b/examples/single_integrator/monte_carlo_safety.py
@@ -0,0 +1,238 @@
+"""GPU/vmap Monte Carlo safety verification: 200 stochastic single-integrator CBF-QP rollouts."""
+import os
+import sys
+
+# Add the project root to the path so we can import cbfkit + examples.
+root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
+if root_path not in sys.path:
+ sys.path.insert(0, root_path)
+
+import contextlib
+
+import jax.numpy as jnp
+import matplotlib
+
+matplotlib.use("Agg")
+import matplotlib.pyplot as plt
+import numpy as np
+from jax import random
+from matplotlib.animation import FuncAnimation, PillowWriter
+from matplotlib.collections import LineCollection
+from matplotlib.lines import Line2D
+
+from cbfkit.controllers.cbf_clf.cbf_clf_qp_generator import cbf_clf_qp_generator
+from cbfkit.controllers.cbf_clf.generate_constraints import (
+ generate_compute_vanilla_clf_constraints,
+ generate_compute_zeroing_cbf_constraints,
+)
+from cbfkit.integration import forward_euler
+from cbfkit.modeling.additive_disturbances import generate_stochastic_perturbation
+from cbfkit.simulation.monte_carlo_gpu import MonteCarloSetup, conduct_monte_carlo_gpu
+from cbfkit.utils.user_types import CertificateCollection, ControllerData, PlannerData
+
+# --- Scenario (verified-clean: low alpha keeps the jaxopt QP stable under vmap) ---
+GOAL = jnp.array([4.0, 4.0])
+OBS = jnp.array([2.0, 2.0])
+R = 0.6
+ALPHA = 1.0
+NOISE = 0.4
+DT = 0.05
+# CBFKIT_TEST_MODE: short horizon, few trials, and skip the GIF render entirely.
+TEST_MODE = bool(os.getenv("CBFKIT_TEST_MODE"))
+NSTEPS = 20 if TEST_MODE else 100
+N_TRIALS = 20 if TEST_MODE else 200
+
+RESULTS_DIR = os.path.join(os.path.dirname(__file__), "results")
+GIF_PATH = os.path.join(RESULTS_DIR, "monte_carlo_safety.gif")
+
+
+def dynamics(x):
+ return jnp.zeros(2), jnp.eye(2)
+
+
+# h(x) = ||x - c||^2 - r^2, relative-degree-1 zeroing barrier (single integrator).
+f_h = lambda _t, x: jnp.sum((x - OBS) ** 2) - R**2 # noqa: E731
+j_h = lambda _t, x: 2.0 * (x - OBS) # noqa: E731
+h_h = lambda _t, _x: 2.0 * jnp.eye(2) # noqa: E731
+p_h = lambda _t, _x: 0.0 # noqa: E731
+a_h = lambda h: ALPHA * h # noqa: E731
+barriers = CertificateCollection([f_h], [j_h], [h_h], [p_h], [a_h])
+
+controller = cbf_clf_qp_generator(
+ generate_compute_zeroing_cbf_constraints,
+ generate_compute_vanilla_clf_constraints,
+)(
+ control_limits=jnp.array([8.0, 8.0]),
+ dynamics_func=dynamics,
+ barriers=barriers,
+ relaxable_cbf=False,
+ relaxable_clf=True,
+)
+
+
+def nominal_controller(t, x, _key, _ref):
+ return 2.0 * (GOAL - x), None
+
+
+def initial_state_sampler(key):
+ return jnp.array([0.0, 0.0]) + 0.18 * random.normal(key, (2,))
+
+
+def _sensor(t, x, *, sigma=None, key=None):
+ return x
+
+
+def _estimator(t, y, z, u, c):
+ return y, (c if c is not None else jnp.zeros((len(y), len(y))))
+
+
+# The CBF-QP controller emits batched jax.debug.print spam under vmap (every branch of
+# its status lax.switch fires); silence it at the fd level around the kernel run.
+@contextlib.contextmanager
+def _silence_fds():
+ saved = os.dup(1), os.dup(2)
+ devnull = os.open(os.devnull, os.O_WRONLY)
+ os.dup2(devnull, 1)
+ os.dup2(devnull, 2)
+ try:
+ yield
+ finally:
+ os.dup2(saved[0], 1)
+ os.dup2(saved[1], 2)
+ os.close(devnull)
+ os.close(saved[0])
+ os.close(saved[1])
+
+
+def main():
+ # Pass the perturbation UNWRAPPED so its `.is_increment` flag survives (Euler-Maruyama).
+ perturbation = generate_stochastic_perturbation(sigma=lambda x: NOISE * jnp.eye(2), dt=DT)
+
+ _, c_data = controller(0.0, jnp.zeros(2), jnp.zeros(2), random.PRNGKey(0), ControllerData())
+ setup = MonteCarloSetup(
+ dt=DT,
+ num_steps=NSTEPS,
+ dynamics=dynamics,
+ integrator=forward_euler,
+ initial_state_sampler=initial_state_sampler,
+ nominal_controller=nominal_controller,
+ controller=controller,
+ sensor=_sensor,
+ estimator=_estimator,
+ perturbation=perturbation,
+ sigma=jnp.zeros(0),
+ controller_data=c_data,
+ planner=None,
+ planner_data=PlannerData(),
+ )
+
+ print(f"[monte_carlo_safety] running {N_TRIALS} vmap'd stochastic rollouts...")
+ with _silence_fds():
+ results = conduct_monte_carlo_gpu(setup, n_trials=N_TRIALS, seed=0)
+ states = np.asarray(results.states) # (N_TRIALS, NSTEPS, 2)
+ print(f"[monte_carlo_safety] kernel wall time: {results.wall_time_s:.2f}s")
+
+ # Geometric safety check (independent of the controller's internal barrier bookkeeping).
+ dist = np.linalg.norm(states - np.asarray(OBS), axis=-1) # (N, NSTEPS)
+ inside = dist < R # (N, NSTEPS)
+ ever_inside = inside.any(axis=1) # (N,)
+ # Cumulative empirical violation rate up to each step.
+ cum_viol_rate = np.array([float(inside[:, : k + 1].any(axis=1).mean()) for k in range(NSTEPS)])
+ overall_rate = float(ever_inside.mean())
+ print(
+ f"[monte_carlo_safety] overall empirical violation rate: {overall_rate:.3f} "
+ f"(min dist to obstacle center {dist.min():.3f}, R={R})"
+ )
+
+ if TEST_MODE:
+ # Fast path: skip the GIF render, just report the safety metric.
+ print(f"[monte_carlo_safety] CBFKIT_TEST_MODE: skipping GIF render.")
+ return overall_rate
+
+ # Draw a representative subset to keep the GIF small (the full 200-line translucent tangle
+ # bloats the palette), but ALWAYS include every breaching trial so the red paths shown stay
+ # consistent with the empirical-risk counter, which is computed over ALL N_TRIALS.
+ N_DRAW = 60
+ rng = np.random.default_rng(0)
+ viol_idx = np.flatnonzero(ever_inside)
+ safe_idx = np.flatnonzero(~ever_inside)
+ n_safe_draw = min(len(safe_idx), max(0, N_DRAW - len(viol_idx)))
+ safe_draw = rng.choice(safe_idx, size=n_safe_draw, replace=False)
+ draw_idx = np.concatenate([safe_draw, viol_idx]).astype(int)
+ draw_states = states[draw_idx] # (N_DRAW, NSTEPS, 2)
+ draw_colors = ["tab:red" if ever_inside[i] else "tab:blue" for i in draw_idx]
+
+ fig, ax = plt.subplots(figsize=(5.0, 5.0))
+ ax.add_patch(plt.Circle((float(OBS[0]), float(OBS[1])), R, color="red", alpha=0.3, zorder=1))
+ ax.add_patch(
+ plt.Circle((float(OBS[0]), float(OBS[1])), R, fill=False, color="red", lw=1.5, zorder=2)
+ )
+ ax.plot(float(GOAL[0]), float(GOAL[1]), "g*", markersize=18, zorder=6)
+ ax.plot(0.0, 0.0, "ks", markersize=6, zorder=6)
+
+ lc = LineCollection([], colors=draw_colors, linewidths=0.5, alpha=0.3, zorder=3)
+ ax.add_collection(lc)
+ dots = ax.scatter(
+ draw_states[:, 0, 0], draw_states[:, 0, 1], s=6, c=draw_colors, alpha=0.7, zorder=4
+ )
+ txt = ax.text(
+ 0.03,
+ 0.97,
+ "",
+ transform=ax.transAxes,
+ va="top",
+ ha="left",
+ fontsize=9,
+ family="monospace",
+ bbox=dict(boxstyle="round", facecolor="white", alpha=0.85),
+ )
+
+ legend_handles = [
+ Line2D([0], [0], color="tab:blue", lw=1.5, label="safe rollout"),
+ Line2D(
+ [0], [0], marker="*", color="w", markerfacecolor="g", markersize=12, lw=0, label="Goal"
+ ),
+ Line2D(
+ [0], [0], marker="s", color="w", markerfacecolor="k", markersize=7, lw=0, label="Start"
+ ),
+ ]
+ if len(viol_idx) > 0:
+ legend_handles.insert(1, Line2D([0], [0], color="tab:red", lw=1.5, label="breached"))
+
+ ax.set_xlim(-1.0, 5.0)
+ ax.set_ylim(-1.0, 5.0)
+ ax.set_aspect("equal")
+ ax.set_xlabel("x")
+ ax.set_ylabel("y")
+ ax.set_title(
+ f"Monte Carlo safety verification — {N_TRIALS} stochastic CBF rollouts", fontsize=9
+ )
+ ax.legend(handles=legend_handles, loc="lower right", fontsize=8)
+ ax.grid(True, alpha=0.3)
+
+ stride = max(1, NSTEPS // 30)
+ frame_idx = list(range(0, NSTEPS, stride))
+
+ def update(k):
+ lc.set_segments([draw_states[i, : k + 1, :] for i in range(len(draw_idx))])
+ dots.set_offsets(draw_states[:, k, :])
+ rate = cum_viol_rate[k]
+ n_viol = int(round(rate * N_TRIALS))
+ txt.set_text(
+ f"step {k + 1:3d}/{NSTEPS}\n"
+ f"trials {N_TRIALS}\n"
+ f"violations {n_viol}\n"
+ f"empirical risk {rate * 100:4.1f}%"
+ )
+ return lc, dots, txt
+
+ os.makedirs(RESULTS_DIR, exist_ok=True)
+ anim = FuncAnimation(fig, update, frames=frame_idx, interval=100, blit=False)
+ anim.save(GIF_PATH, writer=PillowWriter(fps=10), dpi=80)
+ plt.close(fig)
+ print(f"[monte_carlo_safety] saved GIF -> {GIF_PATH}")
+ return overall_rate
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/single_integrator/mppi_reach_avoid.py b/examples/single_integrator/mppi_reach_avoid.py
new file mode 100644
index 00000000..7d94f9dc
--- /dev/null
+++ b/examples/single_integrator/mppi_reach_avoid.py
@@ -0,0 +1,146 @@
+"""MPPI sampling-based reach-avoid planning for a 2D single integrator."""
+import os
+import sys
+
+# Add the project root to the path so we can import cbfkit + examples.
+root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
+if root_path not in sys.path:
+ sys.path.insert(0, root_path)
+
+import jax.numpy as jnp
+import matplotlib
+
+matplotlib.use("Agg")
+import numpy as np
+from jax import Array, jit
+from matplotlib.animation import FuncAnimation, PillowWriter
+
+import cbfkit.controllers.mppi as mppi_planner
+import cbfkit.simulation.simulator as sim
+from cbfkit.estimators import naive as estimator
+from cbfkit.integration import runge_kutta_4 as integrator
+from cbfkit.sensors import perfect as sensor
+
+# CBFKIT_TEST_MODE: short horizon and skip the GIF render entirely.
+TEST_MODE = bool(os.getenv("CBFKIT_TEST_MODE"))
+
+DT = 0.1
+TF = 1.0 if TEST_MODE else 8.0
+N_STEPS = int(TF / DT) + 1
+x0 = jnp.array([0.0, 0.0])
+goal = jnp.array([4.0, 4.0])
+obstacle = jnp.array([3.0, 3.0])
+obstacle_radius = 0.6
+
+RESULTS_DIR = os.path.join(os.path.dirname(__file__), "results")
+GIF_PATH = os.path.join(RESULTS_DIR, "mppi_reach_avoid.gif")
+
+
+def plant():
+ def dynamics(x):
+ return jnp.zeros(2), jnp.eye(2)
+
+ return dynamics
+
+
+dynamics = plant()
+
+
+@jit
+def stage_cost(state_and_time: Array, action: Array) -> Array:
+ x = state_and_time
+ dist_goal_sq = (x[0] - goal[0]) ** 2 + (x[1] - goal[1]) ** 2
+ margin = jnp.maximum(jnp.linalg.norm(x[0:2] - obstacle[0:2]) - obstacle_radius, 0.01)
+ return 5.0 * dist_goal_sq + 8.0 / margin + 0.1 * (action[0] ** 2 + action[1] ** 2)
+
+
+@jit
+def terminal_cost(state_and_time: Array, action: Array) -> Array:
+ x = state_and_time
+ return 50.0 * ((x[0] - goal[0]) ** 2 + (x[1] - goal[1]) ** 2)
+
+
+def main():
+ mppi_args = {
+ "robot_state_dim": 2,
+ "robot_control_dim": 2,
+ "prediction_horizon": 25,
+ "num_samples": 2000,
+ "plot_samples": 30,
+ "time_step": DT,
+ "use_GPU": False,
+ "costs_lambda": 0.03,
+ "cost_perturbation": 0.1,
+ }
+ planner = mppi_planner.vanilla_mppi(
+ control_limits=jnp.array([5.0, 5.0]),
+ dynamics_func=dynamics,
+ trajectory_cost=None,
+ stage_cost=stage_cost,
+ terminal_cost=terminal_cost,
+ mppi_args=mppi_args,
+ )
+
+ res = sim.execute(
+ x0=x0,
+ dt=DT,
+ num_steps=N_STEPS,
+ dynamics=dynamics,
+ integrator=integrator,
+ planner=planner,
+ nominal_controller=None,
+ controller=None,
+ sensor=sensor,
+ estimator=estimator,
+ planner_data={
+ "u_traj": jnp.ones((mppi_args["prediction_horizon"], mppi_args["robot_control_dim"])),
+ },
+ controller_data={},
+ )
+ states = np.asarray(res["states"])
+
+ final_dist = float(np.linalg.norm(states[-1] - np.asarray(goal)))
+ print(f"[mppi_reach_avoid] final distance to goal: {final_dist:.3f}")
+
+ if TEST_MODE:
+ # Fast path: skip the GIF render, just report the reach metric.
+ print("[mppi_reach_avoid] CBFKIT_TEST_MODE: skipping GIF render.")
+ return final_dist
+
+ import matplotlib.pyplot as plt
+
+ fig, ax = plt.subplots(figsize=(6, 6))
+ ax.add_patch(
+ plt.Circle(
+ (float(obstacle[0]), float(obstacle[1])),
+ obstacle_radius,
+ color="red",
+ alpha=0.35,
+ )
+ )
+ ax.plot(float(goal[0]), float(goal[1]), "g*", markersize=18, label="Goal")
+ (line,) = ax.plot([], [], "b-", lw=2)
+ dot = ax.scatter([], [], s=80, color="blue", zorder=5)
+ ax.set_xlim(-1, 7)
+ ax.set_ylim(-1, 7)
+ ax.set_aspect("equal")
+ ax.set_title("MPPI — sampling-based reach-avoid planning", fontsize=10)
+ ax.legend(loc="lower right", fontsize=9)
+ ax.grid(True, alpha=0.3)
+
+ def update(i):
+ line.set_data(states[: i + 1, 0], states[: i + 1, 1])
+ dot.set_offsets([[states[i, 0], states[i, 1]]])
+ return line, dot
+
+ stride = max(1, len(states) // 60)
+ os.makedirs(RESULTS_DIR, exist_ok=True)
+ anim = FuncAnimation(fig, update, frames=range(0, len(states), stride), interval=100, blit=True)
+ anim.save(GIF_PATH, writer=PillowWriter(fps=10))
+ plt.close(fig)
+ print(f"[mppi_reach_avoid] saved GIF -> {GIF_PATH}")
+ return final_dist
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/single_integrator/multi_robot_coordination.py b/examples/single_integrator/multi_robot_coordination.py
new file mode 100644
index 00000000..92d0c338
--- /dev/null
+++ b/examples/single_integrator/multi_robot_coordination.py
@@ -0,0 +1,185 @@
+"""Multi-robot 2D coordination: 6 single integrators on a ring swapping to opposite positions via pairwise distance CBFs."""
+import os
+import sys
+
+# Add the project root to the path so we can import cbfkit.
+root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
+if root_path not in sys.path:
+ sys.path.insert(0, root_path)
+
+import jax.numpy as jnp
+import numpy as np
+from jax import jacfwd, jacrev
+
+import cbfkit.simulation.simulator as sim
+from cbfkit.certificates.conditions.barrier_conditions import zeroing_barriers
+from cbfkit.controllers.cbf_clf import vanilla_cbf_clf_qp_controller
+from cbfkit.estimators import naive as estimator
+from cbfkit.integration import runge_kutta_4 as integrator
+from cbfkit.sensors import perfect as sensor
+from cbfkit.utils.user_types import CertificateCollection
+
+# CBFKIT_TEST_MODE: short horizon + skip the GIF render/save entirely.
+TEST_MODE = bool(os.getenv("CBFKIT_TEST_MODE"))
+
+NUM = 6
+DIM = 2 * NUM
+RADIUS = 2.0
+SAFE_DIST = 0.55
+DT = 0.05
+TF = 0.5 if TEST_MODE else 4.0
+
+RESULTS_DIR = os.path.join(os.path.dirname(__file__), "results")
+GIF_PATH = os.path.join(RESULTS_DIR, "multi_robot_coordination.gif")
+
+INITIAL = np.zeros(DIM)
+GOALS = np.zeros(DIM)
+rng = np.random.default_rng(7)
+for i in range(NUM):
+ ang = 2 * np.pi * i / NUM + rng.normal(0, 0.03)
+ INITIAL[2 * i] = RADIUS * np.cos(ang)
+ INITIAL[2 * i + 1] = RADIUS * np.sin(ang)
+ # Goal is opposite side of the ring.
+ GOALS[2 * i] = -RADIUS * np.cos(2 * np.pi * i / NUM)
+ GOALS[2 * i + 1] = -RADIUS * np.sin(2 * np.pi * i / NUM)
+goal_arr = jnp.asarray(GOALS)
+
+
+def dynamics(x):
+ return jnp.zeros(DIM), jnp.eye(DIM)
+
+
+def nominal(t, x, *args, **kwargs):
+ u = -1.5 * (x - goal_arr)
+ return u, {}
+
+
+# Build pairwise distance barriers: h = dx^2 + dy^2 - SAFE_DIST^2.
+def make_h(i, j):
+ def h(t, x):
+ dx = x[2 * i] - x[2 * j]
+ dy = x[2 * i + 1] - x[2 * j + 1]
+ return dx * dx + dy * dy - SAFE_DIST**2
+
+ return h
+
+
+funcs = []
+jacs = []
+hess = []
+partials = []
+conds = []
+
+cond_factory = zeroing_barriers.linear_class_k(alpha=2.0)
+
+for i in range(NUM):
+ for j in range(i + 1, NUM):
+ h = make_h(i, j)
+ grad = jacfwd(lambda x, _h=h: _h(0.0, x))
+ hess_fn = jacfwd(jacrev(lambda x, _h=h: _h(0.0, x)))
+
+ def partial_t(t, x, _h=h):
+ return 0.0
+
+ funcs.append(h)
+ jacs.append(lambda t, x, _g=grad: _g(x))
+ hess.append(lambda t, x, _H=hess_fn: _H(x))
+ partials.append(partial_t)
+ conds.append(cond_factory)
+
+barriers = CertificateCollection(
+ functions=funcs,
+ jacobians=jacs,
+ hessians=hess,
+ partials=partials,
+ conditions=conds,
+)
+
+controller = vanilla_cbf_clf_qp_controller(
+ control_limits=100.0 * jnp.ones(DIM),
+ nominal_input=nominal,
+ dynamics_func=dynamics,
+ barriers=barriers,
+)
+
+
+def main():
+ N = int(TF / DT)
+ res = sim.execute(
+ x0=jnp.asarray(INITIAL),
+ dt=DT,
+ num_steps=N,
+ dynamics=dynamics,
+ integrator=integrator,
+ nominal_controller=nominal,
+ controller=controller,
+ sensor=sensor,
+ estimator=estimator,
+ )
+ states = np.asarray(res["states"])
+
+ # Minimum pairwise distance over the whole run (safety metric, SAFE_DIST is the bound).
+ min_pair_dist = np.inf
+ for i in range(NUM):
+ for j in range(i + 1, NUM):
+ dx = states[:, 2 * i] - states[:, 2 * j]
+ dy = states[:, 2 * i + 1] - states[:, 2 * j + 1]
+ min_pair_dist = min(min_pair_dist, float(np.sqrt(dx * dx + dy * dy).min()))
+ print(
+ f"[multi_robot_coordination] min pairwise distance over run: {min_pair_dist:.3f} "
+ f"(safety bound SAFE_DIST={SAFE_DIST})"
+ )
+
+ if TEST_MODE:
+ # Fast path: skip the GIF render, just report the safety metric.
+ print("[multi_robot_coordination] CBFKIT_TEST_MODE: skipping GIF render.")
+ return min_pair_dist
+
+ import matplotlib
+
+ matplotlib.use("Agg")
+ import matplotlib.pyplot as plt
+ from matplotlib.animation import FuncAnimation, PillowWriter
+
+ fig, ax = plt.subplots(figsize=(6, 6))
+ cmap = plt.get_cmap("tab10")
+ colors = [cmap(i) for i in range(NUM)]
+ dots = []
+ lines = []
+ for i in range(NUM):
+ (ln,) = ax.plot([], [], "-", color=colors[i], lw=1.5, alpha=0.7)
+ dot = ax.scatter([], [], s=80, color=colors[i], zorder=5)
+ lines.append(ln)
+ dots.append(dot)
+ ax.plot(
+ float(GOALS[2 * i]),
+ float(GOALS[2 * i + 1]),
+ "*",
+ color=colors[i],
+ markersize=14,
+ markeredgecolor="black",
+ alpha=0.5,
+ )
+ ax.set_xlim(-RADIUS - 1, RADIUS + 1)
+ ax.set_ylim(-RADIUS - 1, RADIUS + 1)
+ ax.set_aspect("equal")
+ ax.set_title(f"Multi-robot 2D coordination ({NUM} agents, pairwise CBF)", fontsize=10)
+ ax.grid(True, alpha=0.3)
+
+ def update(k):
+ for i in range(NUM):
+ lines[i].set_data(states[: k + 1, 2 * i], states[: k + 1, 2 * i + 1])
+ dots[i].set_offsets([[states[k, 2 * i], states[k, 2 * i + 1]]])
+ return tuple(lines) + tuple(dots)
+
+ os.makedirs(RESULTS_DIR, exist_ok=True)
+ stride = max(1, len(states) // 70)
+ anim = FuncAnimation(fig, update, frames=range(0, len(states), stride), interval=100, blit=True)
+ anim.save(GIF_PATH, writer=PillowWriter(fps=10))
+ plt.close(fig)
+ print(f"[multi_robot_coordination] saved GIF -> {GIF_PATH}")
+ return min_pair_dist
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/unicycle/reach_goal/ellipsoidal_obstacle_cbf.py b/examples/unicycle/reach_goal/ellipsoidal_obstacle_cbf.py
new file mode 100644
index 00000000..34ea5e1e
--- /dev/null
+++ b/examples/unicycle/reach_goal/ellipsoidal_obstacle_cbf.py
@@ -0,0 +1,122 @@
+"""Unicycle reach-goal with a vanilla CBF-CLF QP filter avoiding one ellipsoidal obstacle."""
+import os
+import sys
+
+# Add the project root to the path so we can import examples
+root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../.."))
+if root_path not in sys.path:
+ sys.path.insert(0, root_path)
+
+import jax.numpy as jnp
+import numpy as np
+
+import cbfkit.simulation.simulator as sim
+import cbfkit.systems.unicycle.models.olfatisaber2002approximate as unicycle
+from cbfkit.certificates import concatenate_certificates, rectify_relative_degree
+from cbfkit.certificates.conditions.barrier_conditions import zeroing_barriers
+from cbfkit.controllers.cbf_clf import vanilla_cbf_clf_qp_controller
+from cbfkit.estimators import naive as estimator
+from cbfkit.integration import runge_kutta_4 as integrator
+from cbfkit.sensors import perfect as sensor
+from cbfkit.systems.unicycle import proportional_controller
+from cbfkit.utils.user_types import PlannerData
+from examples.unicycle.common.ellipsoidal_obstacle import cbf as ellipsoid_cbf
+
+# Test mode: short horizon, skip the GIF render so tests stay fast.
+TEST_MODE = bool(os.getenv("CBFKIT_TEST_MODE"))
+
+RESULTS_DIR = os.path.join(os.path.dirname(__file__), "results")
+GIF_PATH = os.path.join(RESULTS_DIR, "ellipsoidal_obstacle_cbf.gif")
+
+
+def main() -> str:
+ dyn = unicycle.plant(lam=1.0)
+ x0 = jnp.array([0.0, 0.0, jnp.pi / 2])
+ xg = jnp.array([4.0, 4.0, 0.0])
+ obs = jnp.array([2.0, 2.0, 0.0])
+ ell = jnp.array([0.6, 0.6])
+ barriers = concatenate_certificates(
+ rectify_relative_degree(
+ function=ellipsoid_cbf(obs, ell),
+ system_dynamics=dyn,
+ state_dim=3,
+ form="exponential",
+ roots=jnp.array([-1.0]),
+ )(certificate_conditions=zeroing_barriers.linear_class_k(alpha=2.0))
+ )
+ nominal = proportional_controller(dynamics=dyn, Kp_pos=1, Kp_theta=0.01)
+ controller = vanilla_cbf_clf_qp_controller(
+ control_limits=jnp.array([5.0, 5.0]),
+ nominal_input=nominal,
+ dynamics_func=dyn,
+ barriers=barriers,
+ )
+ tf = 8.0 if not TEST_MODE else 1.0
+ dt = 0.02
+ n = int(tf / dt)
+ res = sim.execute(
+ x0=x0,
+ dt=dt,
+ num_steps=n,
+ dynamics=dyn,
+ integrator=integrator,
+ nominal_controller=nominal,
+ controller=controller,
+ sensor=sensor,
+ estimator=estimator,
+ planner_data=PlannerData(
+ u_traj=None,
+ x_traj=jnp.tile(xg.reshape(-1, 1), (1, n + 1)),
+ prev_robustness=None,
+ ),
+ use_jit=True,
+ )
+ states = np.asarray(res["states"])
+
+ final_dist = float(np.linalg.norm(states[-1, :2] - np.asarray(xg[:2])))
+ print(f"Final distance to goal: {final_dist:.4f}")
+
+ if TEST_MODE:
+ return GIF_PATH
+
+ import matplotlib.pyplot as plt
+ from matplotlib.animation import FuncAnimation, PillowWriter
+
+ fig, ax = plt.subplots(figsize=(6, 6))
+ ax.add_patch(
+ plt.matplotlib.patches.Ellipse(
+ (float(obs[0]), float(obs[1])),
+ float(ell[0]) * 2,
+ float(ell[1]) * 2,
+ facecolor="red",
+ alpha=0.35,
+ edgecolor="red",
+ lw=1.5,
+ )
+ )
+ ax.plot(float(xg[0]), float(xg[1]), "g*", markersize=18, label="Goal")
+ (line,) = ax.plot([], [], "b-", lw=2)
+ dot = ax.scatter([], [], s=80, color="blue", zorder=5)
+ ax.set_xlim(-1, 5)
+ ax.set_ylim(-1, 5)
+ ax.set_aspect("equal")
+ ax.set_title("Ellipsoidal-obstacle CBF — unicycle reach-goal", fontsize=10)
+ ax.legend(loc="lower right", fontsize=9)
+ ax.grid(True, alpha=0.3)
+
+ def update(i):
+ line.set_data(states[: i + 1, 0], states[: i + 1, 1])
+ dot.set_offsets([[states[i, 0], states[i, 1]]])
+ return line, dot
+
+ stride = max(1, len(states) // 70)
+ anim = FuncAnimation(fig, update, frames=range(0, len(states), stride), interval=100, blit=True)
+ os.makedirs(RESULTS_DIR, exist_ok=True)
+ anim.save(GIF_PATH, writer=PillowWriter(fps=10))
+ plt.close(fig)
+ print(f"Saved animation to {GIF_PATH}")
+ return GIF_PATH
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/unicycle/reach_goal/robust_cbf.py b/examples/unicycle/reach_goal/robust_cbf.py
new file mode 100644
index 00000000..7ce7a0d3
--- /dev/null
+++ b/examples/unicycle/reach_goal/robust_cbf.py
@@ -0,0 +1,130 @@
+"""Unicycle reach-goal with robust CBF-CLF QP controller under worst-case bounded disturbance."""
+import os
+import sys
+
+# Add the project root to the path so we can import examples
+root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../.."))
+if root_path not in sys.path:
+ sys.path.insert(0, root_path)
+
+import jax.numpy as jnp
+import matplotlib.pyplot as plt
+import numpy as np
+from matplotlib.animation import FuncAnimation, PillowWriter
+from matplotlib.patches import Ellipse
+
+import cbfkit.simulation.simulator as sim
+import cbfkit.systems.unicycle.models.olfatisaber2002approximate as unicycle
+from cbfkit.certificates import concatenate_certificates, rectify_relative_degree
+from cbfkit.certificates.conditions.barrier_conditions import zeroing_barriers
+from cbfkit.controllers.cbf_clf import robust_cbf_clf_qp_controller
+from cbfkit.estimators import naive as estimator
+from cbfkit.integration import runge_kutta_4 as integrator
+from cbfkit.sensors import perfect as sensor
+from cbfkit.systems.unicycle import proportional_controller
+from cbfkit.utils.user_types import PlannerData
+from examples.unicycle.common.ellipsoidal_obstacle import cbf as ellipsoid_cbf
+
+TEST_MODE = bool(os.getenv("CBFKIT_TEST_MODE"))
+
+
+def main() -> float:
+ dyn = unicycle.plant(lam=1.0)
+ x0 = jnp.array([0.0, 0.0, jnp.pi / 2])
+ xg = jnp.array([4.0, 4.0, 0.0])
+
+ # Two obstacles between start and goal
+ obs_list = [
+ (jnp.array([1.5, 1.5, 0.0]), jnp.array([0.5, 0.5])),
+ (jnp.array([3.0, 3.0, 0.0]), jnp.array([0.5, 0.5])),
+ ]
+ barriers = concatenate_certificates(
+ *[
+ rectify_relative_degree(
+ function=ellipsoid_cbf(o, e),
+ system_dynamics=dyn,
+ state_dim=3,
+ form="exponential",
+ roots=jnp.array([-1.0]),
+ )(certificate_conditions=zeroing_barriers.linear_class_k(alpha=2.0))
+ for o, e in obs_list
+ ]
+ )
+ nominal = proportional_controller(dynamics=dyn, Kp_pos=1.0, Kp_theta=0.01)
+ controller = robust_cbf_clf_qp_controller(
+ control_limits=jnp.array([5.0, 5.0]),
+ nominal_input=nominal,
+ dynamics_func=dyn,
+ barriers=barriers,
+ disturbance_norm=2,
+ disturbance_norm_bound=0.25,
+ )
+ tf, dt = (8.0, 0.02) if not TEST_MODE else (0.5, 0.02)
+ n = int(tf / dt)
+ res = sim.execute(
+ x0=x0,
+ dt=dt,
+ num_steps=n,
+ dynamics=dyn,
+ integrator=integrator,
+ nominal_controller=nominal,
+ controller=controller,
+ sensor=sensor,
+ estimator=estimator,
+ planner_data=PlannerData(
+ u_traj=None,
+ x_traj=jnp.tile(xg.reshape(-1, 1), (1, n + 1)),
+ prev_robustness=None,
+ ),
+ )
+ states = np.asarray(res["states"])
+
+ final_dist = float(np.linalg.norm(states[-1, :2] - np.asarray(xg)[:2]))
+ print(f"Final distance to goal: {final_dist:.4f}")
+
+ if TEST_MODE:
+ return final_dist
+
+ fig, ax = plt.subplots(figsize=(6, 6))
+
+ for o, e in obs_list:
+ ax.add_patch(
+ Ellipse(
+ (float(o[0]), float(o[1])),
+ float(e[0]) * 2,
+ float(e[1]) * 2,
+ facecolor="red",
+ alpha=0.35,
+ edgecolor="red",
+ lw=1.5,
+ )
+ )
+ ax.plot(float(xg[0]), float(xg[1]), "g*", markersize=18, label="Goal")
+ (line,) = ax.plot([], [], "b-", lw=2)
+ dot = ax.scatter([], [], s=80, color="blue", zorder=5)
+ ax.set_xlim(-1, 5)
+ ax.set_ylim(-1, 5)
+ ax.set_aspect("equal")
+ ax.set_title("Robust CBF — safety under worst-case bounded disturbance", fontsize=10)
+ ax.legend(loc="lower right", fontsize=9)
+ ax.grid(True, alpha=0.3)
+
+ def update(i):
+ line.set_data(states[: i + 1, 0], states[: i + 1, 1])
+ dot.set_offsets([[states[i, 0], states[i, 1]]])
+ return line, dot
+
+ stride = max(1, len(states) // 70)
+ anim = FuncAnimation(fig, update, frames=range(0, len(states), stride), interval=100, blit=True)
+
+ results_dir = os.path.join(os.path.dirname(__file__), "results")
+ os.makedirs(results_dir, exist_ok=True)
+ out = os.path.join(results_dir, "robust_cbf.gif")
+ anim.save(out, writer=PillowWriter(fps=10))
+ plt.close(fig)
+ print(f"Saved animation to {out}")
+ return final_dist
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/unicycle/reach_goal/stochastic_cbf.py b/examples/unicycle/reach_goal/stochastic_cbf.py
index a1844efa..b27b377c 100644
--- a/examples/unicycle/reach_goal/stochastic_cbf.py
+++ b/examples/unicycle/reach_goal/stochastic_cbf.py
@@ -23,7 +23,7 @@
plot = 1 if not os.getenv("CBFKIT_TEST_MODE") else 0
should_animate = 1 if not os.getenv("CBFKIT_TEST_MODE") else 0
-save = 0
+save = 1 if not os.getenv("CBFKIT_TEST_MODE") else 0
# Simulation parameters
tf = 10.0 if not os.getenv("CBFKIT_TEST_MODE") else 0.5
@@ -129,5 +129,6 @@ def sigma(x):
obstacles=obstacles,
ellipsoids=ellipsoids,
save_animation=save,
- animation_filename=file_path + "stochastic_cbf_control.mp4",
+ animation_filename=file_path + "stochastic_cbf_control.gif",
+ backend="matplotlib",
)
diff --git a/examples/van_der_pol/regulation/perfect_sensing.py b/examples/van_der_pol/regulation/perfect_sensing.py
index 364e8dcd..e69357d9 100644
--- a/examples/van_der_pol/regulation/perfect_sensing.py
+++ b/examples/van_der_pol/regulation/perfect_sensing.py
@@ -28,7 +28,7 @@
tf = 5.0 if not os.getenv("CBFKIT_TEST_MODE") else 0.5
n_steps = int(tf / setup.dt)
plot = 1 if not os.getenv("CBFKIT_TEST_MODE") else 0
-save = 0
+save = 1
# Controlled reverse-time Van der Pol dynamics
dynamics = van_der_pol.reverse_van_der_pol_oscillator(epsilon=setup.epsilon, sigma=setup.Q)
diff --git a/examples/van_der_pol/visualizations/path.py b/examples/van_der_pol/visualizations/path.py
index 0d837997..ccdc3b6e 100644
--- a/examples/van_der_pol/visualizations/path.py
+++ b/examples/van_der_pol/visualizations/path.py
@@ -67,15 +67,22 @@ def animate(
):
from cbfkit.utils.animator import CBFAnimator
- animator = CBFAnimator(states, dt=dt, x_lim=x_lim, y_lim=y_lim, title=title)
+ animator = CBFAnimator(
+ states, dt=dt, x_lim=x_lim, y_lim=y_lim, title=title, backend="matplotlib"
+ )
animator.add_goal(desired_state[:2], radius=desired_state_radius)
animator.add_trajectory(
- x_idx=0, y_idx=1, data=estimates,
- color="tab:orange", label="Estimated Trajectory",
+ x_idx=0,
+ y_idx=1,
+ data=estimates,
+ color="tab:orange",
+ label="Estimated Trajectory",
)
animator.add_trajectory(
- x_idx=0, y_idx=1,
- color="tab:blue", label="Trajectory",
+ x_idx=0,
+ y_idx=1,
+ color="tab:blue",
+ label="Trajectory",
)
if save_animation:
diff --git a/media/showcase/mpc_double_integrator.gif b/media/showcase/mpc_double_integrator.gif
index 5e9f6768..8badd47d 100644
Binary files a/media/showcase/mpc_double_integrator.gif and b/media/showcase/mpc_double_integrator.gif differ
diff --git a/media/showcase/mppi_stl.gif b/media/showcase/mppi_stl.gif
index 0e8270f2..ad0eeccf 100644
Binary files a/media/showcase/mppi_stl.gif and b/media/showcase/mppi_stl.gif differ
diff --git a/media/showcase/multi_robot_2d.gif b/media/showcase/multi_robot_2d.gif
index 753b5238..86eab9cb 100644
Binary files a/media/showcase/multi_robot_2d.gif and b/media/showcase/multi_robot_2d.gif differ
diff --git a/media/showcase/quadrotor_6dof.gif b/media/showcase/quadrotor_6dof.gif
index bcea4043..fa8546df 100644
Binary files a/media/showcase/quadrotor_6dof.gif and b/media/showcase/quadrotor_6dof.gif differ
diff --git a/media/showcase/risk_aware_cvar.gif b/media/showcase/risk_aware_cvar.gif
index a2ca76ab..bd1f240e 100644
Binary files a/media/showcase/risk_aware_cvar.gif and b/media/showcase/risk_aware_cvar.gif differ
diff --git a/media/showcase/robust_cbf.gif b/media/showcase/robust_cbf.gif
index d0c694d2..45c0a64f 100644
Binary files a/media/showcase/robust_cbf.gif and b/media/showcase/robust_cbf.gif differ
diff --git a/media/showcase/stochastic_cbf.gif b/media/showcase/stochastic_cbf.gif
index adf81e48..38ebd73c 100644
Binary files a/media/showcase/stochastic_cbf.gif and b/media/showcase/stochastic_cbf.gif differ
diff --git a/media/showcase/van_der_pol_clf.gif b/media/showcase/van_der_pol_clf.gif
index f8740511..174a1fcc 100644
Binary files a/media/showcase/van_der_pol_clf.gif and b/media/showcase/van_der_pol_clf.gif differ