Skip to content

Commit d292a9f

Browse files
committed
clean up scope a bit
1 parent 7250295 commit d292a9f

9 files changed

Lines changed: 83 additions & 64 deletions

File tree

sparsediffpy/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from sparsediffpy import _sparsediffengine # noqa: F401
1919

2020
# Core classes
21-
from sparsediffpy._core._scope import Scope, Variable, Parameter # noqa: F401
21+
from sparsediffpy._core._scope import Scope, Variable, Parameter, DimensionError # noqa: F401
2222
from sparsediffpy._core._expression import Expression # noqa: F401
2323

2424
# Compile

sparsediffpy/_core/_compile.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,9 +163,12 @@ def _convert_node(node, n_vars, cache, param_caps, param_objs):
163163
)
164164

165165
if isinstance(node, Parameter):
166+
# Use current values if set, otherwise zeros as placeholder.
167+
# Real values are synced via problem_update_params before evaluation.
168+
size = node.shape[0] * node.shape[1]
169+
values = node._value_flat if node._value_flat is not None else np.zeros(size)
166170
cap = _C.make_parameter(
167-
node.shape[0], node.shape[1], node._param_id, n_vars,
168-
node._value_flat,
171+
node.shape[0], node.shape[1], node._param_id, n_vars, values,
169172
)
170173
param_caps.append(cap)
171174
param_objs.append(node)
@@ -308,6 +311,12 @@ def _sync_params(self):
308311
"""Push current parameter values to the C problem."""
309312
if not self._param_objects:
310313
return
314+
for p in self._param_objects:
315+
if p._value_flat is None:
316+
raise ValueError(
317+
f"Parameter with shape {p.shape} has no value set. "
318+
f"Assign a value via parameter.value = ... before evaluating."
319+
)
311320
theta_parts = [p._value_flat for p in self._param_objects]
312321
theta = np.concatenate(theta_parts)
313322
_C.problem_update_params(self._problem, theta)

sparsediffpy/_core/_registry.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,14 @@ def make_dense_right_matmul(param_node, child_cap, A_flat, m, n):
6262

6363

6464
def _to_dense_row_major(matrix):
65-
"""Convert a Constant or Parameter to row-major flat data for dense matmul."""
65+
"""Convert a Constant or Parameter to row-major flat data for dense matmul.
66+
67+
For Parameters with no value set yet, returns zeros as a placeholder —
68+
the real values are pushed via problem_update_params before evaluation.
69+
"""
6670
m, n = matrix.shape
71+
if matrix._value_flat is None:
72+
return np.zeros(m * n, dtype=np.float64)
6773
return matrix._value_flat.reshape((m, n), order="F").flatten(order="C")
6874

6975

sparsediffpy/_core/_scope.py

Lines changed: 30 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
from sparsediffpy._core._shapes import validate_shape
77

88

9+
class DimensionError(ValueError):
10+
"""Raised when a value has the wrong number of elements."""
11+
pass
12+
13+
914
class Variable(Expression):
1015
"""A decision variable in the expression tree.
1116
@@ -20,61 +25,41 @@ def __init__(self, scope, var_id, shape):
2025

2126
@property
2227
def value(self):
23-
size = self.shape[0] * self.shape[1]
24-
return self._scope._flat_values[self._var_id:self._var_id + size].copy()
28+
return self._scope._flat_values[self._var_id:self._var_id + self.size].copy()
2529

2630
@value.setter
2731
def value(self, val):
2832
val = np.asarray(val, dtype=np.float64).ravel()
29-
size = self.shape[0] * self.shape[1]
30-
if val.size != size:
31-
raise ValueError(
32-
f"Expected {size} elements for Variable with shape {self.shape}, "
33-
f"got {val.size}"
34-
)
35-
self._scope._flat_values[self._var_id:self._var_id + size] = val
33+
if val.size != self.size:
34+
raise DimensionError(f"expected {self.size} elements, got {val.size}")
35+
self._scope._flat_values[self._var_id:self._var_id + self.size] = val
3636

3737

3838
class Parameter(Expression):
3939
"""An updatable parameter in the expression tree.
4040
41-
Created by Scope.Parameter(). Values are stored on the parameter itself
42-
(not in the scope's flat buffer). Updated via .value property.
41+
Created by Scope.Parameter(). Values must be set via .value before
42+
evaluating any expression that uses this parameter.
4343
"""
4444

45-
def __init__(self, scope, param_id, shape, value=None):
45+
def __init__(self, scope, param_id, shape):
4646
self._scope = scope
4747
self._param_id = param_id
4848
self.shape = shape
49-
size = shape[0] * shape[1]
50-
if value is not None:
51-
self._value_flat = np.asarray(value, dtype=np.float64).ravel(order="F")
52-
if self._value_flat.size != size:
53-
raise ValueError(
54-
f"Parameter value has {self._value_flat.size} elements, "
55-
f"expected {size} for shape {shape}"
56-
)
57-
else:
58-
self._value_flat = np.zeros(size, dtype=np.float64)
49+
self._value_flat = None
5950

6051
@property
6152
def value(self):
53+
if self._value_flat is None:
54+
return None
6255
return self._value_flat.copy()
6356

6457
@value.setter
6558
def value(self, val):
6659
val = np.asarray(val, dtype=np.float64).ravel(order="F")
67-
size = self.shape[0] * self.shape[1]
68-
if val.size != size:
69-
raise ValueError(
70-
f"Expected {size} elements for Parameter with shape {self.shape}, "
71-
f"got {val.size}"
72-
)
73-
self._value_flat[:] = val
74-
75-
76-
# Patch _is_param_like to recognize Parameter
77-
# (already handled via lazy import in _expressions.py)
60+
if val.size != self.size:
61+
raise DimensionError(f"expected {self.size} elements, got {val.size}")
62+
self._value_flat = val.copy()
7863

7964

8065
class Scope:
@@ -103,26 +88,27 @@ def Variable(self, d1, d2):
10388
self._variables.append(var)
10489
return var
10590

106-
def Parameter(self, d1, d2, value=None):
107-
"""Create a new updatable parameter in this scope."""
91+
def Parameter(self, d1, d2):
92+
"""Create a new updatable parameter in this scope.
93+
94+
Set its value via .value = ... before evaluating.
95+
"""
10896
validate_shape(d1, d2)
10997
size = d1 * d2
11098
param_id = self._next_param_offset
11199
self._next_param_offset += size
112100

113-
param = Parameter(self, param_id, (d1, d2), value)
101+
param = Parameter(self, param_id, (d1, d2))
114102
self._parameters.append(param)
115103
return param
116104

117-
def set_values(self, flat_array):
105+
def set_values(self, array):
118106
"""Set all variable values at once from a flat array."""
119-
flat_array = np.asarray(flat_array, dtype=np.float64)
120-
if flat_array.size != self._flat_values.size:
121-
raise ValueError(
122-
f"Expected flat array of size {self._flat_values.size}, "
123-
f"got {flat_array.size}"
124-
)
125-
self._flat_values[:] = flat_array
107+
array = np.asarray(array, dtype=np.float64)
108+
in_size = self._flat_values.size
109+
if array.size != in_size:
110+
raise DimensionError(f"expected {in_size} elements, got {array.size}")
111+
self._flat_values[:] = array
126112

127113
def get_values(self):
128114
"""Return a copy of the flat value buffer."""

tests/affine/test_left_matmul.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ def test_left_matmul_sparse_forward(scope, rng):
4242

4343
def test_left_matmul_parameter_jacobian(scope, rng):
4444
x = scope.Variable(3, 1)
45-
A = scope.Parameter(4, 3, value=rng.standard_normal((4, 3)))
45+
A = scope.Parameter(4, 3)
46+
A.value = rng.standard_normal((4, 3))
4647
f = A @ x
4748
fn = sp.compile(f)
4849
checker = NumericalDerivativeChecker(fn, scope)
@@ -51,7 +52,8 @@ def test_left_matmul_parameter_jacobian(scope, rng):
5152

5253
def test_left_matmul_parameter_update(scope, rng):
5354
x = scope.Variable(3, 1)
54-
A = scope.Parameter(3, 3, value=np.eye(3))
55+
A = scope.Parameter(3, 3)
56+
A.value = np.eye(3)
5557
f = A @ x
5658
fn = sp.compile(f)
5759
x0 = random_point(scope, rng)

tests/affine/test_scalar_mult.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ def test_scalar_mult_constant_forward(scope, rng):
2121

2222
def test_scalar_mult_parameter_jacobian(scope, rng):
2323
x = scope.Variable(4, 1)
24-
a = scope.Parameter(1, 1, value=np.array([[3.0]]))
24+
a = scope.Parameter(1, 1)
25+
a.value = np.array([[3.0]])
2526
f = a * x
2627
fn = sp.compile(f)
2728
checker = NumericalDerivativeChecker(fn, scope)
@@ -30,7 +31,8 @@ def test_scalar_mult_parameter_jacobian(scope, rng):
3031

3132
def test_scalar_mult_parameter_update(scope, rng):
3233
x = scope.Variable(3, 1)
33-
a = scope.Parameter(1, 1, value=np.array([[2.0]]))
34+
a = scope.Parameter(1, 1)
35+
a.value = np.array([[2.0]])
3436
f = a * x
3537
fn = sp.compile(f)
3638
x0 = random_point(scope, rng)

tests/affine/test_vector_mult.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ def test_vector_mult_constant_forward(scope, rng):
2323

2424
def test_vector_mult_parameter_jacobian(scope, rng):
2525
x = scope.Variable(3, 1)
26-
a = scope.Parameter(3, 1, value=np.array([1.0, 2.0, 3.0]))
26+
a = scope.Parameter(3, 1)
27+
a.value = np.array([1.0, 2.0, 3.0])
2728
f = a * x
2829
fn = sp.compile(f)
2930
checker = NumericalDerivativeChecker(fn, scope)
@@ -32,7 +33,8 @@ def test_vector_mult_parameter_jacobian(scope, rng):
3233

3334
def test_vector_mult_parameter_update(scope, rng):
3435
x = scope.Variable(3, 1)
35-
a = scope.Parameter(3, 1, value=np.array([1.0, 1.0, 1.0]))
36+
a = scope.Parameter(3, 1)
37+
a.value = np.array([1.0, 1.0, 1.0])
3638
f = a * x
3739
fn = sp.compile(f)
3840
x0 = random_point(scope, rng)

tests/test_misc.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,8 @@ def test_negative_fancy(self, scope, rng):
149149
class TestParameterJacobianAfterUpdate:
150150
def test_left_matmul_jacobian_after_update(self, scope, rng):
151151
x = scope.Variable(3, 1)
152-
A = scope.Parameter(3, 3, value=np.eye(3))
152+
A = scope.Parameter(3, 3)
153+
A.value = np.eye(3)
153154
f = A @ x
154155
fn = sp.compile(f)
155156

@@ -163,7 +164,8 @@ def test_left_matmul_jacobian_after_update(self, scope, rng):
163164

164165
def test_scalar_mult_jacobian_after_update(self, scope, rng):
165166
x = scope.Variable(3, 1)
166-
a = scope.Parameter(1, 1, value=np.array([[3.0]]))
167+
a = scope.Parameter(1, 1)
168+
a.value = np.array([[3.0]])
167169
f = a * x
168170
fn = sp.compile(f)
169171

tests/test_validation.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -176,27 +176,37 @@ def test_prod_axis_must_be_variable(self, scope):
176176
class TestValueAssignment:
177177
def test_variable_wrong_size(self, scope):
178178
x = scope.Variable(3, 1)
179-
with pytest.raises(ValueError, match="Expected 3"):
179+
with pytest.raises(ValueError, match="expected 3 elements"):
180180
x.value = np.array([1.0, 2.0])
181181

182182
def test_variable_too_many(self, scope):
183183
x = scope.Variable(3, 1)
184-
with pytest.raises(ValueError, match="Expected 3"):
184+
with pytest.raises(ValueError, match="expected 3 elements"):
185185
x.value = np.array([1.0, 2.0, 3.0, 4.0])
186186

187187
def test_parameter_wrong_size(self, scope):
188-
p = scope.Parameter(2, 2, value=np.eye(2))
189-
with pytest.raises(ValueError, match="Expected 4"):
188+
p = scope.Parameter(2, 2)
189+
p.value = np.eye(2)
190+
with pytest.raises(ValueError, match="expected 4 elements"):
190191
p.value = np.array([1.0, 2.0])
191192

192193
def test_scope_set_values_wrong_size(self, scope):
193194
x = scope.Variable(3, 1)
194-
with pytest.raises(ValueError, match="Expected flat array of size 3"):
195+
with pytest.raises(ValueError, match="expected 3 elements"):
195196
scope.set_values(np.array([1.0, 2.0]))
196197

197-
def test_parameter_init_wrong_size(self, scope):
198-
with pytest.raises(ValueError, match="elements"):
199-
scope.Parameter(2, 2, value=np.array([1.0, 2.0]))
198+
def test_parameter_unset_value_is_none(self, scope):
199+
p = scope.Parameter(2, 2)
200+
assert p.value is None
201+
202+
def test_parameter_unset_raises_on_eval(self, scope):
203+
x = scope.Variable(3, 1)
204+
A = scope.Parameter(3, 3)
205+
f = A @ x
206+
fn = sp.compile(f)
207+
x.value = np.array([1.0, 2.0, 3.0])
208+
with pytest.raises(ValueError, match="has no value set"):
209+
fn.forward()
200210

201211

202212
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)