Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8f0deaf
Implement cost evaluation functionality; tests and examples still needed
ebianchi Feb 23, 2026
1daabf9
Initial attempt at python bindings (may be incomplete)
ebianchi Feb 23, 2026
b4743ae
Draft moving cost computation utilities outside C3 class
ebianchi Feb 25, 2026
df22ca5
Restructure to use namespace and static class functions
ebianchi Feb 26, 2026
16bc945
Add unit tests for trajectory evaluation functions
ebianchi Mar 2, 2026
c50db1a
Clean outdated comments; match function comment formatting to rest of…
ebianchi Mar 3, 2026
21f98bb
Switch quadratic trajectory cost implementation to more flexible vari…
ebianchi Mar 3, 2026
277ad7c
Incorporate reviewer feedback: more trajectory cost input argument o…
ebianchi Mar 19, 2026
25f0d4a
Add cost calculation where trajectory from C3 object is paired with d…
ebianchi Mar 24, 2026
93362dd
Merge branch 'main' into bibit/cost-evaluation
ebianchi Mar 24, 2026
0752816
Add python bindings for trajectory evaluation functions
ebianchi Mar 26, 2026
abf88ab
Tighten variadic templating for more precise input argument checking
ebianchi Mar 27, 2026
19e7db2
Consolidate function overloads with optional input arguments; expose …
ebianchi Mar 31, 2026
1aa4928
Take in LCS simulation configuration for LCS-based PD control
ebianchi Apr 8, 2026
9cacea5
Add input to turn on/off feedforward term in PD simulation
ebianchi Apr 8, 2026
ddd70cb
Merge branch 'main' into bibit/cost-evaluation
ebianchi Apr 8, 2026
ae1afa2
Add new PD simulation option without provided feedforward terms; incl…
ebianchi Apr 22, 2026
ef873a4
Merge branch 'main' into bibit/cost-evaluation
ebianchi Apr 22, 2026
f96553a
Evaluate quadratic trajectory cost over a subset of state vector
ebianchi Apr 23, 2026
d95bdf8
Remove unnecessary private implementation and move functionality into…
ebianchi Apr 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 204 additions & 5 deletions bindings/pyc3/test/test_traj_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ def setUp(self):
self.Q_matrices = [
np.eye(self.n_x, dtype=np.float64) for _ in range(self.N + 1)
]
self.R_matrices = [np.eye(self.n_u, dtype=np.float64) for _ in range(self.N)]
self.R_matrices = [
np.eye(self.n_u, dtype=np.float64) for _ in range(self.N)
]

def test_compute_quadratic_trajectory_cost_basic(self):
# Test with single matrices
Expand Down Expand Up @@ -88,6 +90,110 @@ def test_compute_quadratic_trajectory_cost_zero_desired(self):
)
self.assertGreater(cost, 0)

def test_compute_quadratic_trajectory_cost_with_start_end_indices(self):
data = [
np.array([1.0, 2.0, 3.0, 4.0]),
np.array([2.0, 3.0, 4.0, 5.0]),
]
data_des = [
np.array([0.0, 1.0, 1.0, 1.0]),
np.array([0.0, 1.0, 1.0, 1.0]),
]

# Use diagonal weights so expected subset cost is easy to verify.
Q0 = np.diag([1.0, 2.0, 3.0, 4.0])
Q1 = np.diag([5.0, 6.0, 7.0, 8.0])
Q_list = [Q0, Q1]

# Compute cost only on indices [1, 3).
start_index, end_index = 1, 3
expected_subset = (
2.0 * (2.0 - 1.0) ** 2
+ 3.0 * (3.0 - 1.0) ** 2
+ 6.0 * (3.0 - 1.0) ** 2
+ 7.0 * (4.0 - 1.0) ** 2
)

cost_list = (
traj_eval.TrajectoryEvaluator.ComputeQuadraticTrajectoryCost(
start_index,
end_index,
data,
data_des,
Q_list,
)
)
self.assertAlmostEqual(cost_list, expected_subset)

# Single-matrix overload should match manually computed value.
Q_single = np.diag([10.0, 20.0, 30.0, 40.0])
expected_single = (
20.0 * (2.0 - 1.0) ** 2
+ 30.0 * (3.0 - 1.0) ** 2
+ 20.0 * (3.0 - 1.0) ** 2
+ 30.0 * (4.0 - 1.0) ** 2
)
cost_single = (
traj_eval.TrajectoryEvaluator.ComputeQuadraticTrajectoryCost(
start_index,
end_index,
data,
data_des,
Q_single,
)
)
self.assertAlmostEqual(cost_single, expected_single)

# Zero-desired overload with indices.
expected_zero_des = (
20.0 * (2.0) ** 2
+ 30.0 * (3.0) ** 2
+ 20.0 * (3.0) ** 2
+ 30.0 * (4.0) ** 2
)
cost_zero_des = (
traj_eval.TrajectoryEvaluator.ComputeQuadraticTrajectoryCost(
start_index,
end_index,
data,
Q_single,
)
)
self.assertAlmostEqual(cost_zero_des, expected_zero_des)

def test_compute_quadratic_trajectory_cost_with_start_end_index_errors(
self,
):
data = [np.ones(self.n_x) for _ in range(self.N + 1)]
data_des = [np.zeros(self.n_x) for _ in range(self.N + 1)]

with self.assertRaises(Exception):
traj_eval.TrajectoryEvaluator.ComputeQuadraticTrajectoryCost(
-1,
2,
data,
data_des,
self.Q_matrix,
)

with self.assertRaises(Exception):
traj_eval.TrajectoryEvaluator.ComputeQuadraticTrajectoryCost(
3,
2,
data,
data_des,
self.Q_matrix,
)

with self.assertRaises(Exception):
traj_eval.TrajectoryEvaluator.ComputeQuadraticTrajectoryCost(
0,
self.n_x + 1,
data,
data_des,
self.Q_matrix,
)

def test_compute_quadratic_trajectory_cost_with_c3(self):
# Create a C3 solver similar to core_test.cc but with penalize_input_change = False
opts = c3.C3Options()
Expand All @@ -103,14 +209,18 @@ def test_compute_quadratic_trajectory_cost_with_c3(self):
opts.M = 100.0
opts.gamma = 1.0
opts.rho_scale = 1.0
opts.penalize_input_change = False # This should prevent the constraint failure
opts.penalize_input_change = (
False # This should prevent the constraint failure
)

costs = c3.C3.CreateCostMatricesFromC3Options(opts, self.N)
solver = c3.C3QP(self.lcs, costs, self.x_des, opts)
solver.Solve(np.zeros(self.n_x))

# Test cost computation from C3 solver
cost = traj_eval.TrajectoryEvaluator.ComputeQuadraticTrajectoryCost(solver)
cost = traj_eval.TrajectoryEvaluator.ComputeQuadraticTrajectoryCost(
solver
)
self.assertGreaterEqual(cost, 0)

# Test with custom matrices
Expand Down Expand Up @@ -141,11 +251,49 @@ def test_simulate_pd_control_with_lcs(self):
for u in u_sim:
self.assertEqual(len(u), self.n_u)

def test_simulate_pd_control_with_lcs_no_feedforward_overload(self):
# Test the overload without u_plan.
Kp = np.zeros(self.n_x)
Kd = np.zeros(self.n_x)
Kp[0] = 10.0
Kp[1] = 10.0
Kd[2] = 1.0
Kd[3] = 1.0

x_sim, u_sim = traj_eval.TrajectoryEvaluator.SimulatePDControlWithLCS(
self.x_traj, Kp, Kd, self.lcs
)

self.assertEqual(len(x_sim), self.N + 1)
self.assertEqual(len(u_sim), self.N)
for x in x_sim:
self.assertEqual(len(x), self.n_x)
for u in u_sim:
self.assertEqual(len(u), self.n_u)

# This overload should match explicit zero feedforward behavior.
zero_u_plan = [np.zeros(self.n_u) for _ in range(self.N)]
x_sim_zero, u_sim_zero = (
traj_eval.TrajectoryEvaluator.SimulatePDControlWithLCS(
self.x_traj,
zero_u_plan,
Kp,
Kd,
self.lcs,
False,
)
)
for x_a, x_b in zip(x_sim, x_sim_zero):
np.testing.assert_allclose(x_a, x_b)
for u_a, u_b in zip(u_sim, u_sim_zero):
np.testing.assert_allclose(u_a, u_b)

def test_simulate_pd_control_with_coarse_fine_lcs(self):
# Create fine LCS with smaller dt, following core_test.cc pattern
fine_lcs = c3.LCS(
np.eye(self.n_x),
np.ones((self.n_x, self.n_u)) / 2.0, # Scale B matrix for finer time step
np.ones((self.n_x, self.n_u))
/ 2.0, # Scale B matrix for finer time step
np.ones((self.n_x, self.n_lambda)),
np.zeros(self.n_x),
np.ones((self.n_lambda, self.n_x)),
Expand All @@ -171,6 +319,55 @@ def test_simulate_pd_control_with_coarse_fine_lcs(self):
self.assertEqual(len(x_sim), self.N + 1)
self.assertEqual(len(u_sim), self.N)

def test_simulate_pd_control_with_coarse_fine_lcs_no_feedforward_overload(
self,
):
# Test the coarse/fine overload without u_plan.
fine_lcs = c3.LCS(
np.eye(self.n_x),
np.ones((self.n_x, self.n_u)) / 2.0,
np.ones((self.n_x, self.n_lambda)),
np.zeros(self.n_x),
np.ones((self.n_lambda, self.n_x)),
np.eye(self.n_lambda),
np.ones((self.n_lambda, self.n_u)),
np.ones(self.n_lambda),
self.N * 2,
self.dt / 2,
)

Kp = np.zeros(self.n_x)
Kd = np.zeros(self.n_x)
Kp[0] = 10.0
Kp[1] = 10.0
Kd[2] = 1.0
Kd[3] = 1.0

x_sim, u_sim = traj_eval.TrajectoryEvaluator.SimulatePDControlWithLCS(
self.x_traj, Kp, Kd, self.lcs, fine_lcs
)

self.assertEqual(len(x_sim), self.N + 1)
self.assertEqual(len(u_sim), self.N)

# This overload should match explicit zero feedforward behavior.
zero_u_plan = [np.zeros(self.n_u) for _ in range(self.N)]
x_sim_zero, u_sim_zero = (
traj_eval.TrajectoryEvaluator.SimulatePDControlWithLCS(
self.x_traj,
zero_u_plan,
Kp,
Kd,
self.lcs,
fine_lcs,
False,
)
)
for x_a, x_b in zip(x_sim, x_sim_zero):
np.testing.assert_allclose(x_a, x_b)
for u_a, u_b in zip(u_sim, u_sim_zero):
np.testing.assert_allclose(u_a, u_b)

def test_simulate_lcs_over_trajectory(self):
config = c3.LCSSimulateConfig()

Expand Down Expand Up @@ -320,7 +517,9 @@ def test_trajectory_compatibility_errors(self):
)

# Test with wrong length
short_x = [np.ones(self.n_x) for _ in range(self.N)] # Missing one state
short_x = [
np.ones(self.n_x) for _ in range(self.N)
] # Missing one state

with self.assertRaises(Exception):
traj_eval.TrajectoryEvaluator.CheckLCSAndTrajectoryCompatibility(
Expand Down
Loading
Loading