Skip to content

Commit 0f2473f

Browse files
committed
more versions of relative entropy
1 parent 61f59d0 commit 0f2473f

3 files changed

Lines changed: 102 additions & 8 deletions

File tree

‎sparsediffpy/_core/_nodes_bivariate.py‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,24 @@ def __init__(self, x, z):
3232

3333

3434
class RelEntr(Expression):
35-
"""x * log(x / y) elementwise."""
35+
"""x * log(x / y) elementwise.
36+
37+
Supports three variants (auto-dispatched by the C layer):
38+
- Both same shape: elementwise
39+
- Scalar x, vector y: x * log(x / y_i) for each i
40+
- Vector x, scalar y: x_i * log(x_i / y) for each i
41+
"""
3642
def __init__(self, x, y):
37-
if x.shape != y.shape:
38-
raise ValueError(f"rel_entr: shape mismatch {x.shape} vs {y.shape}")
43+
if x.shape == y.shape:
44+
self.shape = x.shape
45+
elif is_scalar(x.shape):
46+
self.shape = y.shape
47+
elif is_scalar(y.shape):
48+
self.shape = x.shape
49+
else:
50+
raise ValueError(
51+
f"rel_entr: shapes must match or one must be scalar, "
52+
f"got {x.shape} and {y.shape}"
53+
)
3954
self.x = x
4055
self.y = y
41-
self.shape = x.shape

‎tests/other/test_rel_entr.py‎

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import numpy as np
2+
import pytest
23
import sparsediffpy as sp
34
from tests.utils import NumericalDerivativeChecker, random_positive_point
45

56

6-
def test_rel_entr_jacobian(scope, rng):
7+
# --- Vector-vector (both same shape) ---
8+
9+
def test_rel_entr_vector_jacobian(scope, rng):
710
x = scope.Variable(3, 1)
811
y = scope.Variable(3, 1)
912
f = sp.rel_entr(x, y)
@@ -12,7 +15,7 @@ def test_rel_entr_jacobian(scope, rng):
1215
checker.check_jacobian(random_positive_point(scope, rng))
1316

1417

15-
def test_rel_entr_hessian(scope, rng):
18+
def test_rel_entr_vector_hessian(scope, rng):
1619
x = scope.Variable(3, 1)
1720
y = scope.Variable(3, 1)
1821
f = sp.rel_entr(x, y)
@@ -22,7 +25,7 @@ def test_rel_entr_hessian(scope, rng):
2225
checker.check_hessian(x0, rng.standard_normal(3))
2326

2427

25-
def test_rel_entr_forward(scope, rng):
28+
def test_rel_entr_vector_forward(scope, rng):
2629
x = scope.Variable(3, 1)
2730
y = scope.Variable(3, 1)
2831
f = sp.rel_entr(x, y)
@@ -32,3 +35,80 @@ def test_rel_entr_forward(scope, rng):
3235
y_val = y.value
3336
expected = x_val * np.log(x_val / y_val)
3437
np.testing.assert_allclose(fn.forward(), expected)
38+
39+
40+
# --- Scalar x, vector y ---
41+
42+
def test_rel_entr_scalar_vector_jacobian(scope, rng):
43+
x = scope.Variable(1, 1)
44+
y = scope.Variable(3, 1)
45+
f = sp.rel_entr(x, y)
46+
assert f.shape == (3, 1)
47+
fn = sp.compile(f)
48+
checker = NumericalDerivativeChecker(fn, scope)
49+
checker.check_jacobian(random_positive_point(scope, rng))
50+
51+
52+
def test_rel_entr_scalar_vector_hessian(scope, rng):
53+
x = scope.Variable(1, 1)
54+
y = scope.Variable(3, 1)
55+
f = sp.rel_entr(x, y)
56+
fn = sp.compile(f)
57+
checker = NumericalDerivativeChecker(fn, scope)
58+
x0 = random_positive_point(scope, rng)
59+
checker.check_hessian(x0, rng.standard_normal(3))
60+
61+
62+
def test_rel_entr_scalar_vector_forward(scope, rng):
63+
x = scope.Variable(1, 1)
64+
y = scope.Variable(3, 1)
65+
f = sp.rel_entr(x, y)
66+
fn = sp.compile(f)
67+
x0 = random_positive_point(scope, rng)
68+
x_val = x.value[0]
69+
y_val = y.value
70+
expected = x_val * np.log(x_val / y_val)
71+
np.testing.assert_allclose(fn.forward(), expected)
72+
73+
74+
# --- Vector x, scalar y ---
75+
76+
def test_rel_entr_vector_scalar_jacobian(scope, rng):
77+
x = scope.Variable(3, 1)
78+
y = scope.Variable(1, 1)
79+
f = sp.rel_entr(x, y)
80+
assert f.shape == (3, 1)
81+
fn = sp.compile(f)
82+
checker = NumericalDerivativeChecker(fn, scope)
83+
checker.check_jacobian(random_positive_point(scope, rng))
84+
85+
86+
def test_rel_entr_vector_scalar_hessian(scope, rng):
87+
x = scope.Variable(3, 1)
88+
y = scope.Variable(1, 1)
89+
f = sp.rel_entr(x, y)
90+
fn = sp.compile(f)
91+
checker = NumericalDerivativeChecker(fn, scope)
92+
x0 = random_positive_point(scope, rng)
93+
checker.check_hessian(x0, rng.standard_normal(3))
94+
95+
96+
def test_rel_entr_vector_scalar_forward(scope, rng):
97+
x = scope.Variable(3, 1)
98+
y = scope.Variable(1, 1)
99+
f = sp.rel_entr(x, y)
100+
fn = sp.compile(f)
101+
x0 = random_positive_point(scope, rng)
102+
x_val = x.value
103+
y_val = y.value[0]
104+
expected = x_val * np.log(x_val / y_val)
105+
np.testing.assert_allclose(fn.forward(), expected)
106+
107+
108+
# --- Shape mismatch ---
109+
110+
def test_rel_entr_incompatible_shapes(scope):
111+
x = scope.Variable(3, 1)
112+
y = scope.Variable(2, 1)
113+
with pytest.raises(ValueError, match="shapes must match or one must be scalar"):
114+
sp.rel_entr(x, y)

‎tests/test_validation.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def test_matmul_inner_dim_mismatch(self, scope):
4444
def test_rel_entr_shape_mismatch(self, scope):
4545
x = scope.Variable(3, 1)
4646
y = scope.Variable(2, 1)
47-
with pytest.raises(ValueError, match="shape mismatch"):
47+
with pytest.raises(ValueError, match="shapes must match or one must be scalar"):
4848
sp.rel_entr(x, y)
4949

5050
def test_quad_over_lin_non_scalar_z(self, scope):

0 commit comments

Comments
 (0)