From bb215000e7356f99591d15f88ca1b8f4383d6fe5 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarutin Date: Sun, 25 Oct 2026 15:01:21 -0400 Subject: [PATCH 1/2] Fix diagnose: send error= argument and remove output directory The error argument was passed to CmdStan as epsilon=. The output directory was created with mkdtemp and never removed; it is now a TemporaryDirectory. Closes #871 Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_013imjkuZ1miKJ7HzHWg6Ku2 --- cmdstanpy/model.py | 13 +++++++++---- test/test_model.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/cmdstanpy/model.py b/cmdstanpy/model.py index d1d96f48..d9f4e9fc 100644 --- a/cmdstanpy/model.py +++ b/cmdstanpy/model.py @@ -2027,7 +2027,14 @@ def diagnose( Gradients are evaluated in the unconstrained space. """ - with temp_single_json(data) as _data, temp_single_json(inits) as _inits: + with ( + temp_single_json(data) as _data, + temp_single_json(inits) as _inits, + # the output is read below and is not needed after this call. + tempfile.TemporaryDirectory( + prefix=self.name, dir=_TMPDIR + ) as output_dir, + ): cmd = [ str(self.exe_file), "diagnose", @@ -2036,14 +2043,12 @@ def diagnose( if epsilon is not None: cmd.append(f"epsilon={epsilon}") if error is not None: - cmd.append(f"epsilon={error}") + cmd.append(f"error={error}") if _data is not None: cmd += ["data", f"file={_data}"] if _inits is not None: cmd.append(f"init={_inits}") - output_dir = tempfile.mkdtemp(prefix=self.name, dir=_TMPDIR) - output = os.path.join(output_dir, "output.csv") cmd += ["output", f"file={output}"] if sig_figs is not None: diff --git a/test/test_model.py b/test/test_model.py index d725eed0..f8eef35b 100644 --- a/test/test_model.py +++ b/test/test_model.py @@ -11,6 +11,7 @@ import numpy as np import pytest +from cmdstanpy import _TMPDIR from cmdstanpy.model import CmdStanModel from cmdstanpy.utils import EXTENSION @@ -472,3 +473,37 @@ def test_diagnose() -> None: require_gradients_ok=False, ) assert np.abs(gradients["error"]).max() > 1e-3 + + +def test_diagnose_error_argument(caplog: pytest.LogCaptureFixture) -> None: + model = CmdStanModel(stan_file=BERN_STAN) + with caplog.at_level(logging.DEBUG, logger="cmdstanpy"): + model.diagnose(data=BERN_DATA, epsilon=1e-5, error=1e-3) + cmds = [r.getMessage() for r in caplog.records if "Cmd:" in r.message] + assert len(cmds) == 1 + assert "'epsilon=1e-05'" in cmds[0] + assert "'error=0.001'" in cmds[0] + assert cmds[0].count("epsilon=") == 1 + + +@pytest.mark.parametrize("fails", [False, True]) +def test_diagnose_removes_output_dir( + fails: bool, caplog: pytest.LogCaptureFixture +) -> None: + model = CmdStanModel(stan_file=BERN_STAN) + cmdstanpy_tmp = set(os.listdir(_TMPDIR)) + + with caplog.at_level(logging.DEBUG, logger="cmdstanpy"): + for _ in range(3): + if fails: + with pytest.raises(RuntimeError, match="may exceed"): + model.diagnose(data=BERN_DATA, epsilon=3) + else: + model.diagnose(data=BERN_DATA) + + assert set(os.listdir(_TMPDIR)) == cmdstanpy_tmp + # the command is logged as a list repr, which escapes Windows separators + output_arg = "file=" + repr(str(_TMPDIR))[1:-1] + cmds = [r.getMessage() for r in caplog.records if "Cmd:" in r.message] + assert len(cmds) == 3 + assert all(output_arg in cmd for cmd in cmds) From eede26efaaac638088c9abf647615b26b5d251b4 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarutin Date: Mon, 26 Oct 2026 12:42:03 -0400 Subject: [PATCH 2/2] test: call diagnose once in the output-directory test One call is enough to show its output directory is removed. The repeated calls were copied from the log_prob test, where directories accumulated across calls. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_013imjkuZ1miKJ7HzHWg6Ku2 --- test/test_model.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/test/test_model.py b/test/test_model.py index f8eef35b..a6af29b5 100644 --- a/test/test_model.py +++ b/test/test_model.py @@ -494,16 +494,15 @@ def test_diagnose_removes_output_dir( cmdstanpy_tmp = set(os.listdir(_TMPDIR)) with caplog.at_level(logging.DEBUG, logger="cmdstanpy"): - for _ in range(3): - if fails: - with pytest.raises(RuntimeError, match="may exceed"): - model.diagnose(data=BERN_DATA, epsilon=3) - else: - model.diagnose(data=BERN_DATA) + if fails: + with pytest.raises(RuntimeError, match="may exceed"): + model.diagnose(data=BERN_DATA, epsilon=3) + else: + model.diagnose(data=BERN_DATA) assert set(os.listdir(_TMPDIR)) == cmdstanpy_tmp # the command is logged as a list repr, which escapes Windows separators output_arg = "file=" + repr(str(_TMPDIR))[1:-1] cmds = [r.getMessage() for r in caplog.records if "Cmd:" in r.message] - assert len(cmds) == 3 - assert all(output_arg in cmd for cmd in cmds) + assert len(cmds) == 1 + assert output_arg in cmds[0]