Skip to content

Commit 347546f

Browse files
katdueckerclaudeasoplata
authored
[MRG] Validate param files and network configs against model_variant (#14)
* Validate param files and network configs against model_variant Introduce a 'model_variant' entry in the param .json files so that each network model can check that it was given parameters written for it. - preserve 'model_variant' in Params, which otherwise drops any key that is not part of the legacy defaults. This also fixes default values being silently substituted for the parameters of default_duecker_ET.json, whose 'sim_prefix' no longer marked it as a duecker_ET_model param file - store the variant on the Network and write it to the network config - accept abbreviated variant names, e.g. 'neymotin' or 'duecker_ET' - check that the params define the cell types of the network being built: pyramidal and basket cells for neymotin_2020_model (and the law_2021_model and calcium_model networks built on it), pyramidal cells and interneurons for duecker_ET_model - check the cell types of a network config against its variant when reading it back in, pointing out when a neymotin_2020_model network is being read as a duecker_ET_model Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test pass * add model variant check method * remove sim_prefix * update model_variant bsl_cor check * [MRG] MAINT cap ruff version due to breaking changes (#1333) * MAINT cap ruff version due to breaking changes * FIX forgot to update standalone ruff install * removed model variant bsl_cor checks because tests failing * remove model validation cheks * remove stale import * remove sim_prefix * trivial change to re-run test * ruff * remove final sim_prefix * update test and validation function * DOC some docstring and comment changes * FIX remove str type check on params_fname is none passed --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Austin E. Soplata <asoplata@users.noreply.github.com> Co-authored-by: Austin E. Soplata <me@asoplata.com>
1 parent 04889e3 commit 347546f

12 files changed

Lines changed: 338 additions & 145693 deletions

hnn_core/dipole.py

Lines changed: 4 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ def simulate_dipole(
116116

117117
if bsl_cor is None:
118118
bsl_cor = "jones"
119-
elif bsl_cor not in {"jones", "duecker", "none"}:
120-
raise ValueError("'bsl_cor' must be 'jones', 'duecker' or 'none'")
119+
elif bsl_cor not in {"jones", "neymotin", "duecker", "none"}:
120+
raise ValueError("'bsl_cor' must be 'jones', 'neymotin', 'duecker' or 'none'")
121121

122122
net._instantiate_drives(n_trials=n_trials, tstop=tstop)
123123
net._reset_rec_arrays()
@@ -363,72 +363,8 @@ def _rmse(dpl, exp_dpl, tstart=0.0, tstop=0.0, weights=None):
363363
return np.sqrt((weights * ((dpl1 - dpl2) ** 2)).sum() / weights.sum())
364364

365365

366-
# # KDTODO very different _rmse, copied here
367-
# def _rmse(dpl, exp_dpl, tstart=0.0, tstop=0.0, weights=None):
368-
# """Calculates RMSE between data in dpl and exp_dpl
369-
# Parameters
370-
# ----------
371-
# dpl : instance of Dipole
372-
# A dipole object with simulated data
373-
# exp_dpl : instance of Dipole
374-
# A dipole object with experimental data
375-
# tstart : None | float
376-
# Time at beginning of range over which to calculate RMSE
377-
# tstop : None | float
378-
# Time at end of range over which to calculate RMSE
379-
# weights : None | array
380-
# An array of weights to be applied to each point in
381-
# simulated dpl. Must have length >= dpl.data
382-
# If None, weights will be replaced with 1's for typical RMSE
383-
# calculation.
384-
385-
# Returns
386-
# -------
387-
# err : float
388-
# Weighted RMSE between data in dpl and exp_dpl
389-
# """
390-
# from scipy import signal
391-
392-
# exp_times = exp_dpl.times
393-
# sim_times = dpl.times
394-
395-
# # do tstart and tstop fall within both datasets?
396-
# # if not, use the closest data point as the new tstop/tstart
397-
# for tseries in [exp_times, sim_times]:
398-
# if tstart < tseries[0]:
399-
# tstart = tseries[0]
400-
# if tstop > tseries[-1]:
401-
# tstop = tseries[-1]
402-
403-
# # make sure start and end times are valid for both dipoles
404-
# exp_start_index = (np.abs(exp_times - tstart)).argmin()
405-
# exp_end_index = (np.abs(exp_times - tstop)).argmin()
406-
# exp_length = exp_end_index - exp_start_index
407-
408-
# sim_start_index = (np.abs(sim_times - tstart)).argmin()
409-
# sim_end_index = (np.abs(sim_times - tstop)).argmin()
410-
# sim_length = sim_end_index - sim_start_index
411-
412-
# if weights is None:
413-
# # weighted RMSE with weights of all 1's is equivalent to
414-
# # normal RMSE
415-
# weights = np.ones(len(sim_times[0:sim_end_index]))
416-
# weights = weights[sim_start_index:sim_end_index]
417-
418-
# dpl1 = dpl.data["agg"][sim_start_index:sim_end_index]
419-
# dpl2 = exp_dpl.data["agg"][exp_start_index:exp_end_index]
420-
# if sim_length > exp_length:
421-
# # downsample simulation timeseries to match exp data
422-
# dpl1 = signal.resample(dpl1, exp_length)
423-
# weights = signal.resample(weights, exp_length)
424-
# indices = np.where(weights < 1e-4)
425-
# weights[indices] = 0
426-
# elif sim_length < exp_length:
427-
# # downsample exp timeseries to match simulation data
428-
# dpl2 = signal.resample(dpl2, sim_length)
429-
430-
# return np.sqrt((weights * ((dpl1 - dpl2) ** 2)).sum() / weights.sum())
431-
# # end of KDTODO
366+
def exp_decay(t, A, C, b):
367+
return ((C - A) * np.exp(-b * (t))) + A
432368

433369

434370
def _anticorr(dpl, exp_dpl, tstart=0.0, tstop=0.0, weights=None):

hnn_core/hnn_io.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,7 @@ def network_to_dict(net, write_output=False):
381381

382382
net_data = {
383383
"object_type": "Network",
384+
"model_variant": net._model_variant,
384385
"legacy_mode": net._legacy_mode,
385386
"N_pyr_x": net._N_pyr_x,
386387
"N_pyr_y": net._N_pyr_y,
@@ -505,7 +506,7 @@ def dict_to_network(net_data, read_drives=True, read_external_biases=True):
505506
params = dict()
506507
params["celsius"] = net_data["celsius"]
507508
params["threshold"] = net_data["threshold"]
508-
509+
params["model_variant"] = net_data.get("model_variant", None)
509510
mesh_shape = (net_data["N_pyr_x"], net_data["N_pyr_y"])
510511

511512
# Instantiating network
@@ -581,6 +582,36 @@ def read_network_configuration(fname, read_drives=True, read_external_biases=Tru
581582
"type %s" % (net_data.get("object_type"))
582583
)
583584

585+
# ensure the cell types match the model variant
586+
check_var = net_data.get("model_variant", None)
587+
if check_var is not None and "duecker_ET_model".startswith(check_var):
588+
missing_cells = [
589+
cell_name
590+
for cell_name in [
591+
"L2_pyramidal",
592+
"L5_pyramidal",
593+
"L2_inhibitory",
594+
"L5_inhibitory",
595+
]
596+
if cell_name not in net_data["cell_types"]
597+
]
598+
if missing_cells:
599+
hint = ""
600+
if all(
601+
cell_name in net_data["cell_types"]
602+
for cell_name in ["L2_basket", "L5_basket"]
603+
):
604+
hint = (
605+
" The network has basket cells instead, so you are likely"
606+
" trying to create a duecker_ET_model with"
607+
" neymotin_2020_model cell types."
608+
)
609+
raise ValueError(
610+
f"The cell types of the network do not match "
611+
f"model_variant duecker_ET_model: no "
612+
f"{', '.join(missing_cells)} found.{hint}"
613+
)
614+
584615
net = dict_to_network(net_data, read_drives, read_external_biases)
585616
_check_global_synaptic_gains_uniformity(net)
586617

hnn_core/network.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,7 @@ def __init__(
450450
# Save the parameters used to create the Network
451451
_validate_type(params, dict, "params")
452452
self._params = params
453+
self._model_variant = params.get("model_variant", None)
453454
# Initialise a dictionary of cell ID's, which get used when the
454455
# network is constructed ('built') in NetworkBuilder
455456
# We want it to remain in each Network object, so that the user can

hnn_core/network_models.py

Lines changed: 124 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,92 @@
7575
}
7676

7777

78+
def _validate_params_for_model(
79+
net,
80+
params,
81+
model_variant,
82+
alt_variants=[],
83+
require_variant=False,
84+
excluded_cells=[],
85+
):
86+
"""Check that a param file matches the network model it is used for.
87+
88+
Parameters
89+
----------
90+
net : Instance of Network object
91+
The network the parameters are used for.
92+
params : dict
93+
The parameters the network was built from.
94+
model_variant : str
95+
Name of the network model, e.g. 'duecker_ET_model'. The
96+
'model_variant' entry of `params` must be this name (or of one of
97+
`alt_variants`).
98+
alt_variants : list of str, default=[]
99+
Further model names that are accepted in the 'model_variant' entry of `params`,
100+
e.g. the deprecated name of a model. If `params` defines a local 'model_variant'
101+
that is in 'alt_variants', the returned value will be the value of
102+
'model_variant' that is passed to this function instead.
103+
require_variant : bool, default=False
104+
If True, raise if `params` does not define 'model_variant'. Used for
105+
models that share no parameters with the default model, and would
106+
otherwise silently fall back to default values.
107+
excluded_cells : list of str, default=[]
108+
Short names of cells that are *not* part of this network, e.g.
109+
('L2Basket', 'L5Basket') for a model in which basket cells are
110+
replaced. Parameters for these cells are rejected.
111+
112+
Returns
113+
-------
114+
model_variant : str
115+
The official model variant name.
116+
"""
117+
check_var = params.get("model_variant", None)
118+
if check_var is None:
119+
if require_variant:
120+
raise ValueError(
121+
f"'model_variant' is required for simulations with "
122+
f"{model_variant}. If you are sure that you are using the "
123+
f"correct parameters, add 'model_variant': '{model_variant}', "
124+
"to the first line of the param .json file."
125+
)
126+
elif check_var not in [model_variant] + alt_variants:
127+
raise ValueError(
128+
f"Parameters for {check_var} used for {model_variant}."
129+
" Ensure that your param .json file matches the network "
130+
f"and that your model variant is one of {[model_variant] + alt_variants}. "
131+
)
132+
133+
# check that the params define the cell types of this network
134+
missing_cells = [
135+
cell_name
136+
for cell_name in net.cell_types
137+
if not any(_short_name(cell_name) in key for key in params)
138+
]
139+
if missing_cells:
140+
raise ValueError(
141+
f"No parameters found for {', '.join(missing_cells)}."
142+
" Ensure that your param .json file matches the network. "
143+
" Reach out to us if this doesn't solve your problem. "
144+
" https://github.com/jonescompneurolab/hnn-core/discussions"
145+
)
146+
147+
# check that the params don't define cell types this network replaced
148+
unexpected_cells = [
149+
cell_name
150+
for cell_name in excluded_cells
151+
if any(cell_name in key for key in params)
152+
]
153+
if unexpected_cells:
154+
raise ValueError(
155+
f"Parameters found for {', '.join(unexpected_cells)}, which"
156+
f" are not part of {model_variant}. Ensure that your"
157+
" param .json file matches the network."
158+
" Reach out to us if this doesn't solve your problem. "
159+
" https://github.com/jonescompneurolab/hnn-core/discussions"
160+
)
161+
return model_variant
162+
163+
78164
def neymotin_2020_model(
79165
params=None,
80166
add_drives_from_params=False,
@@ -131,9 +217,8 @@ def neymotin_2020_model(
131217
"""
132218
hnn_core_root = Path(hnn_core.__file__).parent
133219
if params is None:
134-
params = hnn_core_root / "param" / "default.json"
135-
if isinstance(params, (str, Path)):
136-
params = read_params(params)
220+
params_fname = hnn_core_root / "param" / "default.json"
221+
params = read_params(params_fname)
137222

138223
# Define cell types for Jones 2009 model
139224
# data is here in metaData format
@@ -185,6 +270,9 @@ def neymotin_2020_model(
185270

186271
delay = net.delay
187272

273+
# Ensure model_variant and params' cell types match current model
274+
net._model_variant = _validate_params_for_model(net, params, "neymotin_2020_model")
275+
188276
# source of synapse is always at soma
189277

190278
# layer2 Pyr -> layer2 Pyr
@@ -412,12 +500,20 @@ def law_2021_model(
412500
Perception." Cerebral Cortex, 32, 668–688 (2022).
413501
"""
414502

415-
net = jones_2009_model(
503+
hnn_core_root = Path(hnn_core.__file__).parent
504+
if params is None:
505+
params_fname = hnn_core_root / "param" / "default.json"
506+
params = read_params(params_fname)
507+
508+
net = neymotin_2020_model(
416509
params,
417510
add_drives_from_params,
418511
legacy_mode,
419512
mesh_shape=mesh_shape,
420513
)
514+
# Ensure model_variant and params' cell types match current model (same cell types
515+
# as 'neymotin_2020_model')
516+
net._model_variant = _validate_params_for_model(net, params, "law_2021_model")
421517

422518
# Update biophysics (increase gabab duration of inhibition)
423519
net.cell_types["L2_pyramidal"]["cell_object"].synapses["gabab"]["tau1"] = 45.0
@@ -500,8 +596,8 @@ def calcium_model(
500596
Brain Topography, 35, 19–35 (2022).
501597
"""
502598
hnn_core_root = Path(hnn_core.__file__).parent
503-
params_fname = hnn_core_root / "param" / "default.json"
504599
if params is None:
600+
params_fname = hnn_core_root / "param" / "default.json"
505601
params = read_params(params_fname)
506602

507603
net = jones_2009_model(
@@ -511,6 +607,10 @@ def calcium_model(
511607
mesh_shape=mesh_shape,
512608
)
513609

610+
# Ensure model_variant and params' cell types match current model (same cell types
611+
# as 'neymotin_2020_model')
612+
net._model_variant = _validate_params_for_model(net, params, "calcium_model")
613+
514614
# Replace L5 pyramidal cell template with updated calcium
515615
cell_name = "L5_pyramidal"
516616
pos = net.cell_types[cell_name]["cell_object"].pos
@@ -527,13 +627,15 @@ def duecker_ET_model(
527627
""" "Initiate like old calcium model and then replace with new cells"""
528628

529629
hnn_core_root = Path(hnn_core.__file__).parent
530-
params_fname = hnn_core_root / "param" / "default_duecker_ET.json"
531630
if params is None:
631+
params_fname = hnn_core_root / "param" / "default_duecker_ET.json"
532632
params = read_params(params_fname)
533633

534634
cell_types = {
535635
"L2_inhibitory": {
536-
"cell_object": human_gen_interneuron(cell_name="L2Inh", layer=2),
636+
"cell_object": human_gen_interneuron(
637+
cell_name=_short_name("L2_inhibitory"), layer=2
638+
),
537639
"cell_metadata": {
538640
"morpho_type": "interneuron",
539641
"electro_type": "inhibitory",
@@ -546,7 +648,7 @@ def duecker_ET_model(
546648
},
547649
},
548650
"L2_pyramidal": {
549-
"cell_object": pyramidal_humanL23(cell_name="L2Pyr"),
651+
"cell_object": pyramidal_humanL23(cell_name=_short_name("L2_pyramidal")),
550652
"cell_metadata": {
551653
"morpho_type": "pyramidal",
552654
"electro_type": "excitatory",
@@ -559,7 +661,9 @@ def duecker_ET_model(
559661
},
560662
},
561663
"L5_inhibitory": {
562-
"cell_object": human_gen_interneuron(cell_name="L5Inh", layer=5),
664+
"cell_object": human_gen_interneuron(
665+
cell_name=_short_name("L5_inhibitory"), layer=5
666+
),
563667
"cell_metadata": {
564668
"morpho_type": "interneuron",
565669
"electro_type": "inhibitory",
@@ -572,7 +676,7 @@ def duecker_ET_model(
572676
},
573677
},
574678
"L5_pyramidal": {
575-
"cell_object": pyramidal_humanL5ET(cell_name="L5Pyr"),
679+
"cell_object": pyramidal_humanL5ET(cell_name=_short_name("L5_pyramidal")),
576680
"cell_metadata": {
577681
"morpho_type": "pyramidal",
578682
"electro_type": "excitatory",
@@ -613,6 +717,16 @@ def duecker_ET_model(
613717
cell_types=cell_types,
614718
)
615719

720+
# check variant and cell types. Basket cells are replaced by
721+
# interneurons in duecker_ET_model, so their parameters are rejected
722+
net._model_variant = _validate_params_for_model(
723+
net,
724+
params,
725+
"duecker_ET_model",
726+
require_variant=True,
727+
excluded_cells=("L2Basket", "L5Basket"),
728+
)
729+
616730
delay = net.delay
617731

618732
# layer2 Pyr -> layer2 Pyr

hnn_core/parallel_backends.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ def _gather_trial_data(sim_data, net, n_trials, postproc, bsl_cor="jones"):
8080

8181
N_pyr_x = net._N_pyr_x
8282
N_pyr_y = net._N_pyr_y
83-
if bsl_cor == "jones":
83+
if bsl_cor == "jones" or bsl_cor == "neymotin":
8484
if net._verbose:
85-
print("Applying Jones baseline correction", flush=True)
85+
print("Applying Neymotin, 2020 baseline correction", flush=True)
8686
dpl._baseline_renormalize(N_pyr_x, N_pyr_y) # XXX cf. #270
8787

8888
dpl._convert_fAm_to_nAm() # always applied, cf. #264

hnn_core/param/default_duecker_ET.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
{ "sim_prefix": "duecker_ET_model",
1+
{
2+
"model_variant": "duecker_ET_model",
23
"tstop": 170,
34
"dt": 0.025,
45
"celsius": 37.0,

hnn_core/param/duecker_ET_gamma_50Hz_bursty.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{ "sim_prefix": "duecker_ET_model",
1+
{ "model_variant": "duecker_ET_model",
22
"tstop": 170,
33
"dt": 0.025,
44
"celsius": 37.0,

0 commit comments

Comments
 (0)