Summary
Mixing an object-dtype ndarray of expressions with a scalar Expression
raises an opaque NumPy-internals error instead of either working elementwise or
refusing clearly:
import numpy as np
from discopt import Model
m = Model()
x = m.continuous("x", lb=0.1, ub=2.0)
y = m.continuous("y", lb=0.1, ub=2.0)
arr = np.array([x, y], dtype=object)
arr * x # ValueError: setting an array element with a sequence.
x * arr # ValueError: setting an array element with a sequence.
arr + x # ValueError: setting an array element with a sequence.
ValueError: setting an array element with a sequence gives no indication of
what the user did wrong or what to do instead.
Why it is inconsistent rather than simply unsupported
The neighbouring cases all work, which is what makes the failure surprising:
| expression |
result |
x * np.array([1.0, 2.0]) (float ndarray ⊗ scalar expr) |
✅ BinaryOp |
np.array([1.0, 2.0]) * x |
✅ BinaryOp |
arr * arr (object ndarray ⊗ object ndarray) |
✅ ndarray of exprs |
A @ arr / np.dot(A, arr) |
✅ ndarray of exprs |
np.sum(arr) |
✅ BinaryOp |
arr + 1.0 |
✅ ndarray of exprs |
arr * x (object ndarray ⊗ scalar expr) |
❌ ValueError |
So object arrays of expressions are supported for array⊗array and for
matrix products, and scalar-expression⊗ndarray is supported for numeric
ndarrays. Only the object-ndarray ⊗ scalar-expression corner falls through.
Mechanism
Expression.__array_ufunc__ = None (python/discopt/modeling/core.py:619) is
the documented NEP-13 opt-out: it makes NumPy return NotImplemented for
ndarray * Expression so the operation reflects to Expression.__rmul__, which
then consumes the whole array into a single expression node rather than letting
NumPy loop elementwise. That mechanism works and should be kept — it is why
x * np.array([1.0, 2.0]) yields one BinaryOp instead of an object array of
two.
The bug is that __mul__ / __rmul__ / __add__ assume any ndarray operand is
numeric data. With dtype=object the wrap fails deep inside NumPy while trying
to build the constant operand, surfacing as ValueError: setting an array element with a sequence. Confirmed by calling the dunder directly:
x.__rmul__(arr) # ValueError: setting an array element with a sequence.
so the error originates inside discopt's own operator, not in NumPy dispatch.
Scope (deliberately narrow)
This does not affect the normal modeling path. m.continuous(name, shape=(n,)) returns a native Variable carrying .shape, not a NumPy object
array, and the shaped-variable operations are fine:
xs = m.continuous("xs", shape=(3,), lb=0, ub=1)
t = m.continuous("t", lb=0, ub=1)
xs * t, t * xs, xs + t, xs * 2.0 # all OK -> BinaryOp
The failure requires a user to assemble np.array([...], dtype=object) by hand,
which is a plausible thing to do when collecting expressions from a
comprehension:
terms = np.array([some_expr(i) for i in range(n)], dtype=object)
scaled = terms * t # ValueError
Suggested fix
In the arithmetic dunders, branch on operand.dtype == object for ndarray
operands. Either:
- Elementwise — return an object ndarray whose element
i is
arr[i] <op> scalar. This is what NumPy would have produced had
__array_ufunc__ not been set to None, and matches the already-working
arr * arr behaviour; or
- Refuse clearly — raise
TypeError naming the object-dtype array and
pointing at shaped variables (m.continuous(..., shape=...)) as the
supported vectorized form.
(1) is more consistent with the table above. (2) is acceptable if object arrays
are considered out of contract, but the current opaque ValueError is not.
A regression test should pin whichever behaviour is chosen for all of
* / + - in both operand orders.
Verification
All results above were executed on branch fix/lp-std-form-consolidation
(worktree wt-stdform); the two probes reported PROBES_EXECUTED 7 and
PROBES_EXECUTED 5 respectively, with no skipped comparisons.
Related: #1233 (intrinsic functions missing from the top-level namespace).
Summary
Mixing an object-dtype ndarray of expressions with a scalar
Expressionraises an opaque NumPy-internals error instead of either working elementwise or
refusing clearly:
ValueError: setting an array element with a sequencegives no indication ofwhat the user did wrong or what to do instead.
Why it is inconsistent rather than simply unsupported
The neighbouring cases all work, which is what makes the failure surprising:
x * np.array([1.0, 2.0])(float ndarray ⊗ scalar expr)BinaryOpnp.array([1.0, 2.0]) * xBinaryOparr * arr(object ndarray ⊗ object ndarray)A @ arr/np.dot(A, arr)np.sum(arr)BinaryOparr + 1.0arr * x(object ndarray ⊗ scalar expr)ValueErrorSo object arrays of expressions are supported for array⊗array and for
matrix products, and scalar-expression⊗ndarray is supported for numeric
ndarrays. Only the object-ndarray ⊗ scalar-expression corner falls through.
Mechanism
Expression.__array_ufunc__ = None(python/discopt/modeling/core.py:619) isthe documented NEP-13 opt-out: it makes NumPy return
NotImplementedforndarray * Expressionso the operation reflects toExpression.__rmul__, whichthen consumes the whole array into a single expression node rather than letting
NumPy loop elementwise. That mechanism works and should be kept — it is why
x * np.array([1.0, 2.0])yields oneBinaryOpinstead of an object array oftwo.
The bug is that
__mul__/__rmul__/__add__assume any ndarray operand isnumeric data. With
dtype=objectthe wrap fails deep inside NumPy while tryingto build the constant operand, surfacing as
ValueError: setting an array element with a sequence. Confirmed by calling the dunder directly:so the error originates inside discopt's own operator, not in NumPy dispatch.
Scope (deliberately narrow)
This does not affect the normal modeling path.
m.continuous(name, shape=(n,))returns a nativeVariablecarrying.shape, not a NumPy objectarray, and the shaped-variable operations are fine:
The failure requires a user to assemble
np.array([...], dtype=object)by hand,which is a plausible thing to do when collecting expressions from a
comprehension:
Suggested fix
In the arithmetic dunders, branch on
operand.dtype == objectfor ndarrayoperands. Either:
iisarr[i] <op> scalar. This is what NumPy would have produced had__array_ufunc__not been set toNone, and matches the already-workingarr * arrbehaviour; orTypeErrornaming the object-dtype array andpointing at shaped variables (
m.continuous(..., shape=...)) as thesupported vectorized form.
(1) is more consistent with the table above. (2) is acceptable if object arrays
are considered out of contract, but the current opaque
ValueErroris not.A regression test should pin whichever behaviour is chosen for all of
* / + -in both operand orders.Verification
All results above were executed on branch
fix/lp-std-form-consolidation(worktree
wt-stdform); the two probes reportedPROBES_EXECUTED 7andPROBES_EXECUTED 5respectively, with no skipped comparisons.Related: #1233 (intrinsic functions missing from the top-level namespace).