Skip to content

Commit bdad6fb

Browse files
committed
Verify and test higher-order grad
Nested calls such as grad(grad(f, x), x) already yield higher-order derivatives: the plan rewrite walks expressions bottom-up (transform_up), so the inner grad is differentiated to a plain expression first and the outer grad differentiates that result. No code change was needed; this adds tests and documents the behavior. - Rust: a unit test that differentiation composes (d2/dx2 sin = -sin). - Python: second derivatives of sin (-sin) and x^3 (6x) and the third derivative of sin (-cos), against numpy. - Doc: note higher-order support in the module overview. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017mDoFJgsm9kS7SicGoCVF6
1 parent c49003a commit bdad6fb

2 files changed

Lines changed: 37 additions & 0 deletions

File tree

src/autograd.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@
3636
//! full gradient or Jacobian is expressed as several scalar columns (e.g.
3737
//! `grad(f, x) AS dfdx, grad(f, y) AS dfdy`) rather than a nested array, which
3838
//! would break the one-value-per-coordinate model.
39+
//!
40+
//! Calls nest, giving higher-order derivatives for free: the rewrite walks
41+
//! bottom-up, so the inner call in `grad(grad(f, x), x)` is differentiated
42+
//! first and the outer call differentiates that result.
3943
4044
#![allow(dead_code)]
4145

@@ -596,6 +600,14 @@ mod tests {
596600
assert!(differentiate(&e, "x").is_err());
597601
}
598602

603+
#[test]
604+
fn higher_order_derivative() {
605+
// Differentiation composes: d2/dx2 sin(x) = -sin(x).
606+
let d1 = differentiate(&expr_fn::sin(col("x")), "x").unwrap();
607+
let d2 = differentiate(&d1, "x").unwrap();
608+
assert_eq!(d2, neg(expr_fn::sin(col("x"))));
609+
}
610+
599611
#[test]
600612
fn jvp_seeds_a_tangent_on_one_input() {
601613
// jvp(x*y, {x: dx}) = product rule with tangent(x)=dx, tangent(y)=0

tests/test_autograd.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,31 @@ def test_grad_quotient_and_power(ctx):
8080
np.testing.assert_allclose(res["dcube"], 3.0 * val**2)
8181

8282

83+
def test_higher_order_grad(ctx):
84+
# Nested grad() differentiates repeatedly: the inner call is rewritten
85+
# first, then the outer differentiates its result.
86+
val = np.linspace(0.1, 3.0, 16)
87+
res = _ordered(
88+
ctx.sql(
89+
"SELECT i, "
90+
"grad(grad(sin(val), val), val) AS d2_sin, "
91+
"grad(grad(power(val, 3), val), val) AS d2_cube FROM t"
92+
)
93+
)
94+
np.testing.assert_allclose(res["d2_sin"], -np.sin(val)) # -sin
95+
np.testing.assert_allclose(res["d2_cube"], 6.0 * val) # d2/dx2 x^3 = 6x
96+
97+
98+
def test_third_order_grad(ctx):
99+
val = np.linspace(0.1, 3.0, 16)
100+
res = _ordered(
101+
ctx.sql(
102+
"SELECT i, grad(grad(grad(sin(val), val), val), val) AS d3 FROM t"
103+
)
104+
)
105+
np.testing.assert_allclose(res["d3"], -np.cos(val)) # d3/dx3 sin = -cos
106+
107+
83108
def test_non_grad_query_is_unaffected(ctx):
84109
# Queries without grad() bypass the rewrite and behave normally.
85110
res = _ordered(ctx.sql("SELECT i, val FROM t"))

0 commit comments

Comments
 (0)