Skip to content

Commit d8f7935

Browse files
vosesoftclaude
andcommitted
Release v0.3.0-alpha.26: get_correlation_matrix 0-d crash
Bug #28: when only one variable resolves (single-name correlation request, or several names but only one in the .vmrs), the 1xN input to numpy.corrcoef returned a 0-d scalar instead of a (1,1) matrix. Downstream _matrix_to_optional_list then died with "iteration over a 0-d array". Fix: _corrcoef now promotes a 0-d numpy result to a (1,1) array before returning. Single-variable correlation correctly serialises as [[1.0]] in the JSON envelope. 404 tests pass (+2 covering single-row and multi-row cases as regression sentinels). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 915697d commit d8f7935

8 files changed

Lines changed: 72 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ All notable changes to ModelRisk MCP. Follows [Keep a Changelog](https://keepach
44

55
## [Unreleased]
66

7+
## [0.3.0-alpha.26] — 2026-05-22
8+
9+
### Fixed
10+
11+
- **Bug #28`get_correlation_matrix` crashed when only one variable resolved.** For a 1×N input matrix (one variable), `numpy.corrcoef` returns a 0-d scalar of value 1.0 (the variable's self-correlation) instead of a 2-d (1, 1) matrix. The downstream `_matrix_to_optional_list` then died with `TypeError: iteration over a 0-d array`. Surfaces in real use when `get_correlation_matrix` is called with a single name (or where several names are requested but only one resolves — the failure mode in the autonomous test pass). Fix: `_corrcoef` now promotes a 0-d numpy result to a (1, 1) array before returning. The downstream JSON envelope correctly serialises the trivial `[[1.0]]` matrix.
12+
13+
### Tests
14+
15+
404 unit tests pass (+2 in `test_mrservice.py::TestCorrcoefHelper` covering single-row and multi-row cases — sentinel against the 0-d regression).
16+
717
## [0.3.0-alpha.25] — 2026-05-22
818

919
Two bugs surfaced by the autonomous end-to-end test pass against a model with extensive text labels.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "modelrisk-mcp"
3-
version = "0.3.0a25"
3+
version = "0.3.0a26"
44
description = "Open MCP server bridging Anthropic Claude (and any MCP-compatible client) with the ModelRisk Excel add-in."
55
readme = "README.md"
66
requires-python = ">=3.11"

server.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
"name": "io.github.vosesoftware/modelrisk-mcp",
44
"title": "ModelRisk",
55
"description": "Read, build, fit, and run Monte Carlo risk models in Excel through Vose Software's ModelRisk.",
6-
"version": "0.3.0a25",
6+
"version": "0.3.0a26",
77
"packages": [
88
{
99
"registryType": "pypi",
1010
"identifier": "modelrisk-mcp",
11-
"version": "0.3.0a25",
11+
"version": "0.3.0a26",
1212
"transport": {
1313
"type": "stdio"
1414
}

src/modelrisk_mcp/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.3.0a25"
1+
__version__ = "0.3.0a26"

src/modelrisk_mcp/bridge/results.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,9 +335,27 @@ def _lookup_var_id(self, handle: VmrsHandle, name: str) -> int | None:
335335

336336

337337
def _corrcoef(matrix: np.ndarray) -> np.ndarray:
338+
"""Wrap numpy.corrcoef so the result is always a 2-d array.
339+
340+
Bug #28 (alpha.26): for a single-row input (1xN matrix —
341+
one variable), `np.corrcoef` returns a 0-d scalar of value 1.0
342+
(the correlation of one variable with itself). Downstream
343+
`_matrix_to_optional_list` then crashed with "iteration over
344+
a 0-d array" because it iterates over rows.
345+
346+
Surfaces in real use: `get_correlation_matrix` called with a
347+
single name (or where only one of several names resolves) was
348+
crashing instead of returning a trivial [[1.0]] matrix."""
338349
if matrix.size == 0 or matrix.shape[1] < 2:
339350
return np.full((matrix.shape[0], matrix.shape[0]), np.nan)
340-
return np.asarray(np.corrcoef(matrix))
351+
result = np.asarray(np.corrcoef(matrix))
352+
if result.ndim == 0:
353+
# Single-variable case — promote to a 1x1 matrix containing
354+
# the scalar (which numpy.corrcoef computed as 1.0 by
355+
# construction, but defer to the actual returned value rather
356+
# than hardcoding).
357+
return result.reshape(1, 1)
358+
return result
341359

342360

343361
def _rank_matrix(matrix: np.ndarray) -> np.ndarray:

tests/unit/test_mrservice.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,43 @@ def MRLIB_GetModelVarID( # noqa: N802
305305
handle.lookup_var_id("Slow")
306306

307307

308+
class TestCorrcoefHelper:
309+
"""Regression for bug #28 — `_corrcoef` returned a 0-d array
310+
when handed a 1×N matrix (single variable), and the downstream
311+
`_matrix_to_optional_list` then crashed with "iteration over a
312+
0-d array". Surfaces in real use when `get_correlation_matrix`
313+
is called with one name (or only one resolves)."""
314+
315+
def test_single_row_returns_1x1_matrix(self) -> None:
316+
import numpy as np
317+
318+
from modelrisk_mcp.bridge.results import _corrcoef
319+
320+
# One variable with 1000 samples.
321+
matrix = np.arange(1000, dtype=float).reshape(1, 1000)
322+
result = _corrcoef(matrix)
323+
# Must be 2-d, shape (1, 1), value approximately 1.0.
324+
assert result.ndim == 2
325+
assert result.shape == (1, 1)
326+
assert abs(float(result[0, 0]) - 1.0) < 1e-9
327+
328+
def test_two_row_returns_2x2_matrix(self) -> None:
329+
"""Sanity check: normal 2+ variable case still works."""
330+
import numpy as np
331+
332+
from modelrisk_mcp.bridge.results import _corrcoef
333+
334+
a = np.arange(100, dtype=float)
335+
b = a * 2.0 # perfectly correlated
336+
matrix = np.stack([a, b])
337+
result = _corrcoef(matrix)
338+
assert result.shape == (2, 2)
339+
# Self-correlation = 1, cross-correlation = 1 (perfect linear).
340+
assert abs(result[0, 0] - 1.0) < 1e-9
341+
assert abs(result[1, 1] - 1.0) < 1e-9
342+
assert abs(result[0, 1] - 1.0) < 1e-9
343+
344+
308345
class TestVmrsDiscovery:
309346
def test_returns_sibling_vmrs(self, tmp_path: Path) -> None:
310347
workbook = tmp_path / "model.xlsx"

tests/unit/test_server_boot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66

77
def test_version_is_set() -> None:
8-
assert __version__ == "0.3.0a25"
8+
assert __version__ == "0.3.0a26"
99

1010

1111
def test_server_name() -> None:

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)