Skip to content

Commit 3ef241a

Browse files
authored
Merge pull request #231 from robotpy/numerical-integration
Add frc/system/NumericalIntegration.h
2 parents c3e4931 + 206fa0d commit 3ef241a

File tree

5 files changed

+294
-7
lines changed

5 files changed

+294
-7
lines changed

subprojects/robotpy-wpimath/pyproject.toml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,6 @@ scan_headers_ignore = [
7272
"frc/estimator/UnscentedTransform.h",
7373

7474
"frc/system/Discretization.h",
75-
"frc/system/NumericalIntegration.h",
76-
"frc/system/NumericalJacobian.h",
7775

7876
"frc/proto/*",
7977
"frc/*/proto/*",
@@ -1610,8 +1608,8 @@ TravelingSalesman = "frc/path/TravelingSalesman.h"
16101608
# Discretization = "frc/system/Discretization.h"
16111609
LinearSystem = "frc/system/LinearSystem.h"
16121610
LinearSystemLoop = "frc/system/LinearSystemLoop.h"
1613-
# NumericalIntegration = "frc/system/NumericalIntegration.h"
1614-
# NumericalJacobian = "frc/system/NumericalJacobian.h"
1611+
NumericalIntegration = "frc/system/NumericalIntegration.h"
1612+
NumericalJacobian = "frc/system/NumericalJacobian.h"
16151613

16161614
# frc/system/plant
16171615
DCMotor = "frc/system/plant/DCMotor.h"
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
defaults:
2+
subpackage: system
3+
4+
extra_includes:
5+
- frc_eigen.h
6+
- frc/EigenCore.h
7+
- pybind11/functional.h
8+
9+
functions:
10+
RK4:
11+
overloads:
12+
F&&, T, units::second_t:
13+
template_impls:
14+
- [std::function<Eigen::MatrixXd(Eigen::MatrixXd)>, Eigen::MatrixXd]
15+
F&&, T, U, units::second_t:
16+
template_impls:
17+
- ["std::function<Eigen::MatrixXd(Eigen::MatrixXd, Eigen::MatrixXd)>", Eigen::MatrixXd, Eigen::MatrixXd]
18+
F&&, units::second_t, T, units::second_t:
19+
template_impls:
20+
- ["std::function<Eigen::MatrixXd(units::second_t, Eigen::MatrixXd)>", Eigen::MatrixXd]
21+
RKDP:
22+
overloads:
23+
F&&, T, U, units::second_t, double:
24+
template_impls:
25+
- ["std::function<Eigen::MatrixXd(Eigen::MatrixXd, Eigen::MatrixXd)>", Eigen::MatrixXd, Eigen::MatrixXd]
26+
F&&, units::second_t, T, units::second_t, double:
27+
template_impls:
28+
- ["std::function<Eigen::MatrixXd(units::second_t, Eigen::MatrixXd)>", Eigen::MatrixXd]
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
defaults:
2+
subpackage: system
3+
4+
extra_includes:
5+
- frc_eigen.h
6+
- frc/EigenCore.h
7+
- pybind11/functional.h
8+
- pybind11/typing.h
9+
10+
functions:
11+
NumericalJacobian:
12+
overloads:
13+
F&&, const Vectord<Cols>&:
14+
ignore: true
15+
F&&, const Eigen::VectorXd&:
16+
template_impls:
17+
- [std::function<Eigen::VectorXd(Eigen::VectorXd)>]
18+
NumericalJacobianX:
19+
overloads:
20+
F&&, const Vectord<States>&, const Vectord<Inputs>&, Args&&...:
21+
ignore: true
22+
F&&, const Eigen::VectorXd&, const Eigen::VectorXd&, Args&&...:
23+
# template_impls:
24+
# - ["std::function<Eigen::VectorXd(Eigen::VectorXd, Eigen::VectorXd, py::args)>", py::args, Eigen::MatrixXd]
25+
param_override:
26+
args:
27+
ignore: true
28+
no_release_gil: true
29+
cpp_code: |
30+
[](py::typing::Callable<Eigen::VectorXd(Eigen::VectorXd, Eigen::VectorXd, py::args)> fn,
31+
const Eigen::VectorXd& x, const Eigen::VectorXd& u, py::args args) {
32+
return frc::NumericalJacobianX([=](const Eigen::VectorXd &ix, const Eigen::VectorXd &iu) {
33+
py::object r = fn(ix, iu, *args);
34+
return r.cast<Eigen::VectorXd>();
35+
}, x, u);
36+
}
37+
NumericalJacobianU:
38+
overloads:
39+
F&&, const Vectord<States>&, const Vectord<Inputs>&, Args&&...:
40+
ignore: true
41+
F&&, const Eigen::VectorXd&, const Eigen::VectorXd&, Args&&...:
42+
# template_impls:
43+
# - ["std::function<Eigen::VectorXd(Eigen::VectorXd, Eigen::VectorXd, py::args)>", py::args, Eigen::MatrixXd]
44+
param_override:
45+
args:
46+
ignore: true
47+
no_release_gil: true
48+
cpp_code: |
49+
[](py::typing::Callable<Eigen::VectorXd(Eigen::VectorXd, Eigen::VectorXd, py::args)> fn,
50+
const Eigen::VectorXd& x, const Eigen::VectorXd& u, py::args args) {
51+
return frc::NumericalJacobianU([=](const Eigen::VectorXd &ix, const Eigen::VectorXd &iu) {
52+
py::object r = fn(ix, iu, *args);
53+
return r.cast<Eigen::VectorXd>();
54+
}, x, u);
55+
}
Lines changed: 199 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,202 @@
1+
import math
2+
13
import wpimath.system
2-
import wpimath.system.plant
34

5+
import pytest
6+
import numpy as np
7+
8+
9+
def test_rk4_exponential():
10+
"""Test that integrating dx/dt = eˣ works"""
11+
y0 = np.array([[0.0]])
12+
13+
y1 = wpimath.system.RK4(lambda x: np.array([[math.exp(x[0, 0])]]), y0, 0.1)
14+
15+
assert math.isclose(y1[0, 0], math.exp(0.1) - math.exp(0.0), abs_tol=1e-3)
16+
17+
18+
def test_rk4_exponential_with_u():
19+
"""Test that integrating dx/dt = eˣ works when we provide a u"""
20+
y0 = np.array([[0.0]])
21+
22+
y1 = wpimath.system.RK4(
23+
lambda x, u: np.array([[math.exp(u[0, 0] * x[0, 0])]]),
24+
y0,
25+
np.array([[1.0]]),
26+
0.1,
27+
)
28+
29+
assert math.isclose(y1[0, 0], math.exp(0.1) - math.exp(0.0), abs_tol=1e-3)
30+
31+
32+
def test_rk4_time_varying():
33+
"""
34+
Tests RK4 with a time varying solution. From
35+
http://www2.hawaii.edu/~jmcfatri/math407/RungeKuttaTest.html:
36+
37+
dx/dt = x (2 / (eᵗ + 1) - 1)
38+
39+
The true (analytical) solution is:
40+
41+
x(t) = 12eᵗ/(eᵗ + 1)²
42+
"""
43+
y0 = np.array([[12.0 * math.exp(5.0) / math.pow(math.exp(5.0) + 1.0, 2.0)]])
44+
45+
y1 = wpimath.system.RK4(
46+
lambda t, x: np.array([[x[0, 0] * (2.0 / (math.exp(t) + 1.0) - 1.0)]]),
47+
5.0,
48+
y0,
49+
1.0,
50+
)
51+
52+
expected = 12.0 * math.exp(6.0) / math.pow(math.exp(6.0) + 1.0, 2.0)
53+
assert math.isclose(y1[0, 0], expected, abs_tol=1e-3)
54+
55+
56+
def test_rkdp_zero():
57+
"""Tests that integrating dx/dt = 0 works with RKDP"""
58+
y1 = wpimath.system.RKDP(
59+
lambda x, u: np.zeros((1, 1)),
60+
np.array([[0.0]]),
61+
np.array([[0.0]]),
62+
0.1,
63+
)
64+
65+
assert math.isclose(y1[0, 0], 0.0, abs_tol=1e-3)
66+
67+
68+
def test_rkdp_exponential():
69+
"""Tests that integrating dx/dt = eˣ works with RKDP"""
70+
y0 = np.array([[0.0]])
71+
72+
y1 = wpimath.system.RKDP(
73+
lambda x, u: np.array([[math.exp(x[0, 0])]]),
74+
y0,
75+
np.array([[0.0]]),
76+
0.1,
77+
)
78+
79+
assert math.isclose(y1[0, 0], math.exp(0.1) - math.exp(0.0), abs_tol=1e-3)
80+
81+
82+
def test_rkdp_time_varying():
83+
"""
84+
Tests RKDP with a time varying solution. From
85+
http://www2.hawaii.edu/~jmcfatri/math407/RungeKuttaTest.html:
86+
87+
dx/dt = x(2/(eᵗ + 1) - 1)
88+
89+
The true (analytical) solution is:
90+
91+
x(t) = 12eᵗ/(eᵗ + 1)²
92+
"""
93+
y0 = np.array([[12.0 * math.exp(5.0) / math.pow(math.exp(5.0) + 1.0, 2.0)]])
94+
95+
y1 = wpimath.system.RKDP(
96+
lambda t, x: np.array([[x[0, 0] * (2.0 / (math.exp(t) + 1.0) - 1.0)]]),
97+
5.0,
98+
y0,
99+
1.0,
100+
1e-12,
101+
)
102+
103+
expected = 12.0 * math.exp(6.0) / math.pow(math.exp(6.0) + 1.0, 2.0)
104+
assert math.isclose(y1[0, 0], expected, abs_tol=1e-3)
105+
106+
107+
def test_numerical_jacobian():
108+
"""Test that we can recover A from ax_fn() pretty accurately"""
109+
a = np.array(
110+
[
111+
[1.0, 2.0, 4.0, 1.0],
112+
[5.0, 2.0, 3.0, 4.0],
113+
[5.0, 1.0, 3.0, 2.0],
114+
[1.0, 1.0, 3.0, 7.0],
115+
]
116+
)
117+
118+
def ax_fn(x):
119+
return a @ x
120+
121+
new_a = wpimath.system.numericalJacobian(ax_fn, np.zeros((4, 1)))
122+
np.testing.assert_allclose(new_a, a, rtol=1e-6, atol=1e-5)
123+
124+
125+
def test_numerical_jacobian_x_u_square():
126+
"""Test that we can recover B from axbu_fn() pretty accurately"""
127+
a = np.array(
128+
[
129+
[1.0, 2.0, 4.0, 1.0],
130+
[5.0, 2.0, 3.0, 4.0],
131+
[5.0, 1.0, 3.0, 2.0],
132+
[1.0, 1.0, 3.0, 7.0],
133+
]
134+
)
135+
b = np.array([[1.0, 1.0], [2.0, 1.0], [3.0, 2.0], [3.0, 7.0]])
136+
137+
def axbu_fn(x, u):
138+
return a @ x + b @ u
139+
140+
x0 = np.zeros((4, 1))
141+
u0 = np.zeros((2, 1))
142+
new_a = wpimath.system.numericalJacobianX(axbu_fn, x0, u0)
143+
new_b = wpimath.system.numericalJacobianU(axbu_fn, x0, u0)
144+
np.testing.assert_allclose(new_a, a, rtol=1e-6, atol=1e-5)
145+
np.testing.assert_allclose(new_b, b, rtol=1e-6, atol=1e-5)
146+
147+
148+
def test_numerical_jacobian_x_u_rectangular():
149+
c = np.array(
150+
[
151+
[1.0, 2.0, 4.0, 1.0],
152+
[5.0, 2.0, 3.0, 4.0],
153+
[5.0, 1.0, 3.0, 2.0],
154+
]
155+
)
156+
d = np.array([[1.0, 1.0], [2.0, 1.0], [3.0, 2.0]])
157+
158+
def cxdu_fn(x, u):
159+
return c @ x + d @ u
160+
161+
x0 = np.zeros((4, 1))
162+
u0 = np.zeros((2, 1))
163+
new_c = wpimath.system.numericalJacobianX(cxdu_fn, x0, u0)
164+
new_d = wpimath.system.numericalJacobianU(cxdu_fn, x0, u0)
165+
np.testing.assert_allclose(new_c, c, rtol=1e-6, atol=1e-5)
166+
np.testing.assert_allclose(new_d, d, rtol=1e-6, atol=1e-5)
167+
168+
169+
def test_numerical_jacobian_x_passes_extra_args():
170+
a = np.array([[2.0, -1.0], [0.5, 3.0]])
171+
b = np.array([[1.0], [4.0]])
172+
x0 = np.zeros((2, 1))
173+
u0 = np.zeros((1, 1))
174+
175+
seen = {}
176+
177+
def axbu_fn(x, u, scale, bias):
178+
seen["args"] = (scale, bias)
179+
return scale * (a @ x) + bias * (b @ u)
180+
181+
new_a = wpimath.system.numericalJacobianX(axbu_fn, x0, u0, 2.5, -3.0)
182+
183+
assert seen["args"] == (2.5, -3.0)
184+
np.testing.assert_allclose(new_a, 2.5 * a, rtol=1e-6, atol=1e-5)
185+
186+
187+
def test_numerical_jacobian_u_passes_extra_args():
188+
a = np.array([[1.0, 0.0], [0.0, -2.0]])
189+
b = np.array([[1.5], [-0.5]])
190+
x0 = np.zeros((2, 1))
191+
u0 = np.zeros((1, 1))
192+
193+
seen = {}
194+
195+
def axbu_fn(x, u, scale, bias):
196+
seen["args"] = (scale, bias)
197+
return scale * (a @ x) + bias * (b @ u)
198+
199+
new_b = wpimath.system.numericalJacobianU(axbu_fn, x0, u0, 4.0, 0.25)
4200

5-
def test_todo():
6-
pass
201+
assert seen["args"] == (4.0, 0.25)
202+
np.testing.assert_allclose(new_b, 0.25 * b, rtol=1e-6, atol=1e-5)

subprojects/robotpy-wpimath/wpimath/system/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717
LinearSystem_3_2_1,
1818
LinearSystem_3_2_2,
1919
LinearSystem_3_2_3,
20+
RK4,
21+
RKDP,
22+
numericalJacobian,
23+
numericalJacobianU,
24+
numericalJacobianX,
2025
)
2126

2227
__all__ = [
@@ -37,4 +42,9 @@
3742
"LinearSystem_3_2_1",
3843
"LinearSystem_3_2_2",
3944
"LinearSystem_3_2_3",
45+
"RK4",
46+
"RKDP",
47+
"numericalJacobian",
48+
"numericalJacobianU",
49+
"numericalJacobianX",
4050
]

0 commit comments

Comments
 (0)