Skip to content

Commit d97e77e

Browse files
asoplatakatduecker
andauthored
[MRG]: feat: KD add remaining extracted non-model code (#1188)
* feat: KD add remaining extracted non-model code I believe this adds the last of the code that is extracted from #1168 that is *not* the actual Duecker model itself, but instead code changes that are used by the default model. These functions are not currently tested at all, and I have not added tests for them for similar reasons to #1185 (comment) After this, we should be able to make a PR that adds the actual Duecker model itself. However, there will be additional work for that step: we need to update `hnn_io.read_network_configuration` and the equivalent `...write...` such that they support the Duecker model. Continuing the pattern, this is based on top of #1187. * maint: apply suggested renames * ref: rename zero_val * test: conductance-distance func tests These tests were written by Claude-AI. Perfect use case for AI to fill in boring boilerplate. --------- Co-authored-by: katduecker <katharina.duecker@gmail.com>
1 parent 63647fe commit d97e77e

File tree

3 files changed

+178
-12
lines changed

3 files changed

+178
-12
lines changed

hnn_core/cells_default.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ def _cell_L5Pyr(override_params, pos=(0.0, 0.0, 0), gid=0.0):
192192

193193
if sec_name != "soma":
194194
sections[sec_name].mechs["ar"]["gbar_ar"] = partial(
195-
_exp_g_at_dist, zero_val=1e-6, exp_term=3e-3, offset=0.0
195+
_exp_g_at_dist, gbar_at_zero=1e-6, exp_term=3e-3, offset=0.0
196196
)
197197

198198
cell_tree = {
@@ -303,23 +303,24 @@ def _get_mechanisms(p_all, cell_type, section_names, mechanisms):
303303
return mech_props
304304

305305

306-
def _exp_g_at_dist(x, zero_val, exp_term, offset):
306+
def _exp_g_at_dist(x, gbar_at_zero, exp_term, offset, slope=1):
307307
"""Compute exponential distance-dependent ionic conductance.
308308
309309
Parameters
310310
----------
311311
x : float | int
312312
Distance from soma
313-
zero_val : float | int
313+
gbar_at_zero : float | int
314314
Value of function when x = 0
315315
exp_term : float | int
316316
Multiplier of x in the exponent
317-
offset: float |int
317+
offset : float | int
318318
Offset value added to output
319-
319+
slope : int | float, default=1
320+
Slope of the exponential component
320321
"""
321-
322-
return zero_val * np.exp(exp_term * x) + offset
322+
gbar = gbar_at_zero * (slope * np.exp(exp_term * x) + offset)
323+
return gbar
323324

324325

325326
def basket(cell_name, pos=(0, 0, 0), gid=None):
@@ -390,7 +391,9 @@ def pyramidal(cell_name, pos=(0, 0, 0), override_params=None, gid=None):
390391
raise ValueError(f"Unknown pyramidal cell type: {cell_name}")
391392

392393

393-
def _linear_g_at_dist(x, gsoma, gdend, xkink):
394+
def _linear_g_at_dist(
395+
x, gsoma, gdend, xkink, hotzone_factor=1, hotzone_boundaries=[0, 0]
396+
):
394397
"""Compute linear distance-dependent ionic conductance.
395398
396399
Parameters
@@ -403,13 +406,22 @@ def _linear_g_at_dist(x, gsoma, gdend, xkink):
403406
Dendritic conductance
404407
xkink : float | int
405408
Plateau value where conductance is fixed at gdend.
409+
hotzone_factor: int | float, default=1
410+
Increase in conducivity that creates a hotzone.
411+
hotzone_boundaries : [float, float]
412+
Start and end of hotzone if hotzone_factor > 1. Units are the same as that of
413+
`x`.
406414
407415
Notes
408416
-----
409417
Linearly scales conductance along dendrite.
410418
Returns gdend when x > xkink.
411419
"""
412-
return gsoma + np.min([xkink, x]) * (gdend - gsoma) / xkink
420+
gbar = gsoma + np.min([xkink, x]) * (gdend - gsoma) / xkink
421+
if hotzone_boundaries[0] < x < hotzone_boundaries[1]:
422+
gbar *= hotzone_factor
423+
424+
return gbar
413425

414426

415427
def pyramidal_ca(cell_name, pos, override_params=None, gid=None):
@@ -430,7 +442,7 @@ def pyramidal_ca(cell_name, pos, override_params=None, gid=None):
430442
)
431443
gbar_k = partial(
432444
_exp_g_at_dist,
433-
zero_val=override_params["L5Pyr_soma_gkbar_hh2"],
445+
gbar_at_zero=override_params["L5Pyr_soma_gkbar_hh2"],
434446
exp_term=-0.006,
435447
offset=1e-4,
436448
)

hnn_core/gui/gui.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2915,7 +2915,7 @@ def _update_L5_biophysics_cell_params(net, cell_param_key, param_list):
29152915
mechs_params["cat"] = {"gbar_cat": param_list[18].value}
29162916
mechs_params["ar"] = {
29172917
"gbar_ar": partial(
2918-
_exp_g_at_dist, zero_val=param_list[19].value, exp_term=3e-3, offset=0.0
2918+
_exp_g_at_dist, gbar_at_zero=param_list[19].value, exp_term=3e-3, offset=0.0
29192919
)
29202920
}
29212921

hnn_core/tests/test_cells_default.py

Lines changed: 155 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from neuron import h
44
import numpy as np
55

6-
from hnn_core.cells_default import pyramidal, basket
6+
from hnn_core.cells_default import pyramidal, basket, _exp_g_at_dist, _linear_g_at_dist
77
from hnn_core.network_builder import load_custom_mechanisms
88

99

@@ -84,3 +84,157 @@ def test_cells_default():
8484

8585
with pytest.raises(ValueError, match="Unknown basket cell type"):
8686
basket(cell_name="blah")
87+
88+
89+
def test_exp_g_at_dist():
90+
"""Test exponential distance-dependent conductance function."""
91+
# Test at x=0 with default slope
92+
gbar = _exp_g_at_dist(x=0, gbar_at_zero=1e-6, exp_term=3e-3, offset=0.0)
93+
expected = 1e-6 * (1 * np.exp(0) + 0.0) # 1e-6 * 1 = 1e-6
94+
assert np.isclose(gbar, expected)
95+
96+
# Test at x=0 with non-default slope and offset
97+
gbar = _exp_g_at_dist(x=0, gbar_at_zero=2e-6, exp_term=3e-3, offset=0.5, slope=2)
98+
expected = 2e-6 * (2 * np.exp(0) + 0.5) # 2e-6 * 2.5 = 5e-6
99+
assert np.isclose(gbar, expected)
100+
101+
# Test at positive distance
102+
x = 100
103+
gbar = _exp_g_at_dist(x=x, gbar_at_zero=1e-6, exp_term=3e-3, offset=0.0)
104+
expected = 1e-6 * (1 * np.exp(3e-3 * x) + 0.0)
105+
assert np.isclose(gbar, expected)
106+
107+
# Test with negative exp_term (decay)
108+
x = 100
109+
gbar = _exp_g_at_dist(x=x, gbar_at_zero=0.06, exp_term=-0.006, offset=1e-4, slope=1)
110+
expected = 0.06 * (1 * np.exp(-0.006 * x) + 1e-4)
111+
assert np.isclose(gbar, expected)
112+
113+
# Test with array input
114+
x_array = np.array([0, 50, 100, 200])
115+
gbar_array = _exp_g_at_dist(
116+
x=x_array, gbar_at_zero=1e-6, exp_term=3e-3, offset=0.0, slope=1
117+
)
118+
expected_array = 1e-6 * (np.exp(3e-3 * x_array) + 0.0)
119+
assert np.allclose(gbar_array, expected_array)
120+
121+
# Test that function is monotonically increasing with positive exp_term
122+
x_values = np.linspace(0, 100, 10)
123+
gbar_values = _exp_g_at_dist(
124+
x=x_values, gbar_at_zero=1e-6, exp_term=3e-3, offset=0.0, slope=1
125+
)
126+
assert np.all(np.diff(gbar_values) > 0)
127+
128+
# Test that function is monotonically decreasing with negative exp_term
129+
x_values = np.linspace(0, 100, 10)
130+
gbar_values = _exp_g_at_dist(
131+
x=x_values, gbar_at_zero=1.0, exp_term=-0.01, offset=0.0, slope=1
132+
)
133+
assert np.all(np.diff(gbar_values) < 0)
134+
135+
136+
def test_linear_g_at_dist():
137+
"""Test linear distance-dependent conductance function."""
138+
# Test at x=0 (should return gsoma)
139+
gbar = _linear_g_at_dist(x=0, gsoma=10.0, gdend=40.0, xkink=1501)
140+
assert np.isclose(gbar, 10.0)
141+
142+
# Test at x < xkink (linear interpolation)
143+
x = 750 # halfway to xkink
144+
gbar = _linear_g_at_dist(x=x, gsoma=10.0, gdend=40.0, xkink=1501)
145+
expected = 10.0 + 750 * (40.0 - 10.0) / 1501
146+
assert np.isclose(gbar, expected)
147+
148+
# Test at x = xkink (should return gdend)
149+
gbar = _linear_g_at_dist(x=1501, gsoma=10.0, gdend=40.0, xkink=1501)
150+
assert np.isclose(gbar, 40.0)
151+
152+
# Test at x > xkink (should still return gdend)
153+
gbar = _linear_g_at_dist(x=2000, gsoma=10.0, gdend=40.0, xkink=1501)
154+
assert np.isclose(gbar, 40.0)
155+
156+
# Test with hotzone (x inside hotzone boundaries)
157+
x = 500
158+
gbar = _linear_g_at_dist(
159+
x=x,
160+
gsoma=10.0,
161+
gdend=40.0,
162+
xkink=1501,
163+
hotzone_factor=2.0,
164+
hotzone_boundaries=[400, 600],
165+
)
166+
expected_base = 10.0 + 500 * (40.0 - 10.0) / 1501
167+
expected = expected_base * 2.0
168+
assert np.isclose(gbar, expected)
169+
170+
# Test outside hotzone (x before hotzone)
171+
x = 300
172+
gbar = _linear_g_at_dist(
173+
x=x,
174+
gsoma=10.0,
175+
gdend=40.0,
176+
xkink=1501,
177+
hotzone_factor=2.0,
178+
hotzone_boundaries=[400, 600],
179+
)
180+
expected = 10.0 + 300 * (40.0 - 10.0) / 1501
181+
assert np.isclose(gbar, expected)
182+
183+
# Test outside hotzone (x after hotzone)
184+
x = 700
185+
gbar = _linear_g_at_dist(
186+
x=x,
187+
gsoma=10.0,
188+
gdend=40.0,
189+
xkink=1501,
190+
hotzone_factor=2.0,
191+
hotzone_boundaries=[400, 600],
192+
)
193+
expected = 10.0 + 700 * (40.0 - 10.0) / 1501
194+
assert np.isclose(gbar, expected)
195+
196+
# Test at hotzone boundary (exactly at start, should not apply factor)
197+
x = 400
198+
gbar = _linear_g_at_dist(
199+
x=x,
200+
gsoma=10.0,
201+
gdend=40.0,
202+
xkink=1501,
203+
hotzone_factor=2.0,
204+
hotzone_boundaries=[400, 600],
205+
)
206+
expected = 10.0 + 400 * (40.0 - 10.0) / 1501
207+
assert np.isclose(gbar, expected)
208+
209+
# Test at hotzone boundary (exactly at end, should not apply factor)
210+
x = 600
211+
gbar = _linear_g_at_dist(
212+
x=x,
213+
gsoma=10.0,
214+
gdend=40.0,
215+
xkink=1501,
216+
hotzone_factor=2.0,
217+
hotzone_boundaries=[400, 600],
218+
)
219+
expected = 10.0 + 600 * (40.0 - 10.0) / 1501
220+
assert np.isclose(gbar, expected)
221+
222+
# Test with hotzone_factor = 1 (no effect)
223+
x = 500
224+
gbar = _linear_g_at_dist(
225+
x=x,
226+
gsoma=10.0,
227+
gdend=40.0,
228+
xkink=1501,
229+
hotzone_factor=1.0,
230+
hotzone_boundaries=[400, 600],
231+
)
232+
expected = 10.0 + 500 * (40.0 - 10.0) / 1501
233+
assert np.isclose(gbar, expected)
234+
235+
# Test decreasing conductance (gdend < gsoma)
236+
x = 500
237+
gbar = _linear_g_at_dist(x=x, gsoma=40.0, gdend=10.0, xkink=1000)
238+
expected = 40.0 + 500 * (10.0 - 40.0) / 1000
239+
assert np.isclose(gbar, expected)
240+
assert gbar < 40.0 # Should be less than gsoma

0 commit comments

Comments
 (0)