Skip to content

Commit 3cf6ced

Browse files
Merge pull request #3663 from jessica-mitchell/ticket686_positiveparams
Port regression test 686 positive params to Pytest
2 parents d1e0a5f + 4028784 commit 3cf6ced

File tree

2 files changed

+187
-197
lines changed

2 files changed

+187
-197
lines changed
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# test_ticket_686_positive_parameters.py
4+
#
5+
# This file is part of NEST.
6+
#
7+
# Copyright (C) 2004 The NEST Initiative
8+
#
9+
# NEST is free software: you can redistribute it and/or modify
10+
# it under the terms of the GNU General Public License as published by
11+
# the Free Software Foundation, either version 2 of the License, or
12+
# (at your option) any later version.
13+
#
14+
# NEST is distributed in the hope that it will be useful,
15+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
# GNU General Public License for more details.
18+
#
19+
# You should have received a copy of the GNU General Public License
20+
# along with NEST. If not, see <http://www.gnu.org/licenses/>.
21+
22+
import math
23+
24+
import nest
25+
import numpy as np
26+
27+
"""
28+
Regression test for Ticket #686.
29+
30+
Test ported from SLI regression test.
31+
Ensure models with positive-only parameters reject non-positive values and accept larger positive ones.
32+
33+
Author: Hans Ekkehard Plesser, 2013-04-18
34+
"""
35+
36+
37+
SKIPPED_MODELS = {
38+
"correlation_detector",
39+
"correlomatrix_detector",
40+
"correlospinmatrix_detector",
41+
"siegert_neuron",
42+
}
43+
44+
POSITIVE_KEYS_CACHE: dict[str, tuple[str, ...]] = {}
45+
46+
# Parameters that must be updated together to maintain matching dimensions
47+
DIMENSION_PAIRS = {
48+
"tau_sfa": ("q_sfa",),
49+
"tau_stc": ("q_stc",),
50+
}
51+
52+
53+
def _models_to_check():
54+
return [model for model in nest.node_models if model not in SKIPPED_MODELS]
55+
56+
57+
def _positive_keys(model: str) -> tuple[str, ...]:
58+
if model not in POSITIVE_KEYS_CACHE:
59+
defaults = nest.GetDefaults(model)
60+
keys = tuple(key for key in defaults if key == "C_m" or key.startswith("tau_"))
61+
POSITIVE_KEYS_CACHE[model] = keys
62+
return POSITIVE_KEYS_CACHE[model]
63+
64+
65+
def _as_numeric_tuple(value) -> tuple[float, ...]:
66+
"""Convert value to tuple of floats, handling scalars, lists, and arrays."""
67+
if isinstance(value, np.ndarray):
68+
return tuple(float(v) for v in value.tolist())
69+
if isinstance(value, (list, tuple)):
70+
return tuple(float(v) for v in value)
71+
return (float(value),)
72+
73+
74+
def _cast_like(reference, numeric_values: tuple[float, ...]):
75+
"""Cast numeric values to match the type of reference value."""
76+
if isinstance(reference, np.ndarray):
77+
return np.array(numeric_values, dtype=reference.dtype)
78+
if isinstance(reference, tuple):
79+
return tuple(numeric_values)
80+
if isinstance(reference, list):
81+
return list(numeric_values)
82+
return numeric_values[0] if len(numeric_values) == 1 else numeric_values
83+
84+
85+
def _allclose(values_a: tuple[float, ...], values_b: tuple[float, ...], *, abs_tol: float = 1e-12) -> bool:
86+
return len(values_a) == len(values_b) and all(
87+
math.isclose(a, b, rel_tol=0.0, abs_tol=abs_tol) for a, b in zip(values_a, values_b)
88+
)
89+
90+
91+
def _get_dimension_pairs(neuron, key):
92+
"""Get dimension-paired parameter values to include in updates."""
93+
return {pair: neuron.get(pair) for pair in DIMENSION_PAIRS.get(key, ())}
94+
95+
96+
def _test_parameter_update(neuron, key, raw_value, new_values):
97+
"""Test updating a parameter with new values, including dimension-paired parameters."""
98+
dimension_pairs = _get_dimension_pairs(neuron, key)
99+
try:
100+
update = {key: _cast_like(raw_value, new_values)}
101+
update.update(dimension_pairs)
102+
nest.SetStatus(neuron, update)
103+
return True, None
104+
except nest.kernel.NESTError as err:
105+
return False, str(err)
106+
107+
108+
def test_ticket_686_rejects_non_positive_values():
109+
"""
110+
Ensure parameters that must remain positive reject zero and negative assignments.
111+
"""
112+
113+
models = _models_to_check()
114+
assert models, "No models available to test (all models may be skipped)"
115+
failing_cases = []
116+
117+
for model in models:
118+
positive_keys = _positive_keys(model)
119+
if not positive_keys:
120+
continue
121+
122+
nest.ResetKernel()
123+
nest.SetKernelStatus({"dict_miss_is_error": False})
124+
125+
for key in positive_keys:
126+
for candidate in (0.0, -1.0):
127+
if model == "iaf_tum_2000" and candidate == 0.0:
128+
continue
129+
neuron = nest.Create(model)
130+
raw_value = neuron.get(key)
131+
original_values = _as_numeric_tuple(raw_value)
132+
if not original_values:
133+
continue
134+
invalid_values = tuple(float(candidate) for _ in original_values)
135+
136+
success, _ = _test_parameter_update(neuron, key, raw_value, invalid_values)
137+
if success:
138+
current_values = _as_numeric_tuple(neuron.get(key))
139+
if not _allclose(current_values, original_values):
140+
failing_cases.append((model, key, f"accepted_non_positive_{candidate}"))
141+
else:
142+
failing_cases.append((model, key, f"silently_ignored_non_positive_{candidate}"))
143+
else:
144+
current_values = _as_numeric_tuple(neuron.get(key))
145+
if not _allclose(current_values, original_values):
146+
failing_cases.append((model, key, "value_changed_after_exception"))
147+
148+
assert not failing_cases, f"Models not rejecting non-positive values: {failing_cases}"
149+
150+
151+
def test_ticket_686_accepts_positive_assignments():
152+
"""
153+
Ensure the same parameters accept updated positive values.
154+
"""
155+
156+
models = _models_to_check()
157+
assert models, "No models available to test (all models may be skipped)"
158+
failing_cases = []
159+
160+
for model in models:
161+
if model == "iaf_psc_exp_ps_lossless":
162+
continue
163+
164+
positive_keys = _positive_keys(model)
165+
if not positive_keys:
166+
continue
167+
168+
nest.ResetKernel()
169+
nest.SetKernelStatus({"dict_miss_is_error": False})
170+
171+
for key in positive_keys:
172+
neuron = nest.Create(model)
173+
raw_value = neuron.get(key)
174+
original_values = _as_numeric_tuple(raw_value)
175+
if not original_values:
176+
continue
177+
new_values = tuple(v + 1.0 for v in original_values)
178+
179+
success, error_msg = _test_parameter_update(neuron, key, raw_value, new_values)
180+
if success:
181+
updated_values = _as_numeric_tuple(neuron.get(key))
182+
if not _allclose(updated_values, new_values):
183+
failing_cases.append((model, key, "value_not_updated"))
184+
else:
185+
failing_cases.append((model, key, f"raised_exception:{error_msg}"))
186+
187+
assert not failing_cases, f"Models not accepting positive updates: {failing_cases}"

testsuite/regressiontests/ticket-686-positive-parameters.sli

Lines changed: 0 additions & 197 deletions
This file was deleted.

0 commit comments

Comments
 (0)