rnba 0.1.0__py3-none-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rnba/__init__.py +23 -0
- rnba/_bin/rnba-sim.exe +0 -0
- rnba/_sim.py +57 -0
- rnba/architecture.py +491 -0
- rnba/blackboard.py +1125 -0
- rnba/circuit.py +207 -0
- rnba/circuits/__init__.py +66 -0
- rnba/circuits/core.py +355 -0
- rnba/cli.py +15 -0
- rnba/constants.py +88 -0
- rnba/converters/__init__.py +27 -0
- rnba/converters/architecture.py +307 -0
- rnba/inputs.py +48 -0
- rnba/network.py +555 -0
- rnba/nodes.py +31 -0
- rnba/schematic_visualization.py +1341 -0
- rnba/visualize.py +693 -0
- rnba/visualize_activity_flow.py +1742 -0
- rnba-0.1.0.dist-info/METADATA +352 -0
- rnba-0.1.0.dist-info/RECORD +23 -0
- rnba-0.1.0.dist-info/WHEEL +5 -0
- rnba-0.1.0.dist-info/entry_points.txt +5 -0
- rnba-0.1.0.dist-info/licenses/LICENSE +21 -0
rnba/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""RNBA package
|
|
2
|
+
|
|
3
|
+
Simple package with a greet() helper used in tests and CLI.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def greet(name: str = "RNBA") -> str:
|
|
10
|
+
"""Return a greeting for `name`."""
|
|
11
|
+
return f"Hello, {name}!"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
from .architecture import load_architecture, apply_instruction_schedule, apply_bindings
|
|
15
|
+
|
|
16
|
+
# Try importing visualization module if graphviz is available
|
|
17
|
+
try:
|
|
18
|
+
from .visualize import visualize_network, visualize_circuit_template
|
|
19
|
+
__all__ = ['greet', 'load_architecture', 'apply_instruction_schedule', 'apply_bindings',
|
|
20
|
+
'visualize_network', 'visualize_circuit_template']
|
|
21
|
+
except ImportError:
|
|
22
|
+
# graphviz not installed, visualization not available
|
|
23
|
+
__all__ = ['greet', 'load_architecture', 'apply_instruction_schedule', 'apply_bindings']
|
rnba/_bin/rnba-sim.exe
ADDED
|
Binary file
|
rnba/_sim.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Locate and run the bundled ``rnba-sim`` C++ simulator executable.
|
|
2
|
+
|
|
3
|
+
Wheel installs ship the compiled binary in ``rnba/_bin/``. Development
|
|
4
|
+
checkouts fall back to the CMake build tree (``cpp-sim/build/``) and
|
|
5
|
+
finally to ``PATH``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
from importlib.resources import files
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional, Sequence
|
|
17
|
+
|
|
18
|
+
_EXE_NAME = "rnba-sim.exe" if os.name == "nt" else "rnba-sim"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def sim_executable() -> str:
|
|
22
|
+
"""Return the path to the ``rnba-sim`` executable.
|
|
23
|
+
|
|
24
|
+
Raises FileNotFoundError if no executable can be located.
|
|
25
|
+
"""
|
|
26
|
+
bundled = Path(str(files("rnba"))) / "_bin" / _EXE_NAME
|
|
27
|
+
if bundled.is_file():
|
|
28
|
+
return str(bundled)
|
|
29
|
+
|
|
30
|
+
dev_build = Path(__file__).resolve().parent.parent / "cpp-sim" / "build" / _EXE_NAME
|
|
31
|
+
if dev_build.is_file():
|
|
32
|
+
return str(dev_build)
|
|
33
|
+
|
|
34
|
+
on_path = shutil.which(_EXE_NAME)
|
|
35
|
+
if on_path:
|
|
36
|
+
return on_path
|
|
37
|
+
|
|
38
|
+
raise FileNotFoundError(
|
|
39
|
+
"rnba-sim executable not found. Install the rnba wheel (which bundles "
|
|
40
|
+
"it), or build it from a source checkout with: "
|
|
41
|
+
"cmake -B cpp-sim/build cpp-sim && cmake --build cpp-sim/build"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def run_sim(args: Sequence[str], **subprocess_kwargs) -> subprocess.CompletedProcess:
|
|
46
|
+
"""Run ``rnba-sim`` with ``args`` and return the completed process."""
|
|
47
|
+
return subprocess.run([sim_executable(), *args], **subprocess_kwargs)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
51
|
+
"""Console entry point forwarding to the bundled executable."""
|
|
52
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
53
|
+
return subprocess.call([sim_executable(), *args])
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
raise SystemExit(main())
|
rnba/architecture.py
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
"""Load an RNBA architecture text file into a Network.
|
|
2
|
+
|
|
3
|
+
Parses the row-based architecture format produced by the NBA build tools
|
|
4
|
+
and returns a fully wired Network where:
|
|
5
|
+
|
|
6
|
+
- Normal populations become Node objects.
|
|
7
|
+
- External populations become Input objects with dormant activation
|
|
8
|
+
(``lambda t: 0.0``). Use :func:`apply_instruction_schedule` to set
|
|
9
|
+
activation schedules, and :func:`apply_bindings` to register WM
|
|
10
|
+
clamps.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections import defaultdict
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Dict, List, Tuple, Union
|
|
18
|
+
|
|
19
|
+
from rnba.network import Network
|
|
20
|
+
from rnba.nodes import Node
|
|
21
|
+
from rnba.inputs import Input
|
|
22
|
+
from rnba.constants import (
|
|
23
|
+
EXCITATORY_CON_TYPES as _EXCITATORY_CON_TYPES,
|
|
24
|
+
SIGMOID_PARAMS,
|
|
25
|
+
TAU_E,
|
|
26
|
+
TAU_I,
|
|
27
|
+
W_ALL,
|
|
28
|
+
WEIGHT_SCALE as _WEIGHT_SCALE,
|
|
29
|
+
WC_COUPLING_PARAMS,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_architecture(
|
|
34
|
+
filepath: Union[str, Path],
|
|
35
|
+
weight: float = W_ALL,
|
|
36
|
+
tau_E: float = TAU_E,
|
|
37
|
+
tau_I: float = TAU_I,
|
|
38
|
+
) -> Network:
|
|
39
|
+
"""Parse an architecture file and build a ready-to-schedule Network.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
filepath : str or Path
|
|
44
|
+
Path to an ``RNBA_arch_SGall_SNB*.txt`` file.
|
|
45
|
+
weight : float, optional
|
|
46
|
+
Base connection weight (default 0.2, the ``w_all`` used in the
|
|
47
|
+
original simulation script).
|
|
48
|
+
tau_E : float, optional
|
|
49
|
+
Time constant for excitatory populations (default 2.5 ms,
|
|
50
|
+
matching ``1/0.4`` from ``pop_module.c``).
|
|
51
|
+
tau_I : float, optional
|
|
52
|
+
Time constant for inhibitory populations (default 1.25 ms,
|
|
53
|
+
matching ``1/0.8`` from ``pop_module.c``).
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
Network
|
|
58
|
+
A Network with all internal wiring complete. External inputs
|
|
59
|
+
have ``activation = lambda t: 0.0`` — set their activation
|
|
60
|
+
functions before calling ``simulate()``.
|
|
61
|
+
|
|
62
|
+
Raises
|
|
63
|
+
------
|
|
64
|
+
FileNotFoundError
|
|
65
|
+
If *filepath* does not exist.
|
|
66
|
+
ValueError
|
|
67
|
+
If the file cannot be parsed or has inconsistent row counts.
|
|
68
|
+
"""
|
|
69
|
+
filepath = Path(filepath)
|
|
70
|
+
if not filepath.exists():
|
|
71
|
+
raise FileNotFoundError(f"Architecture file not found: {filepath}")
|
|
72
|
+
|
|
73
|
+
# ------------------------------------------------------------------
|
|
74
|
+
# 1. Parse
|
|
75
|
+
# ------------------------------------------------------------------
|
|
76
|
+
rows = [] # one dict per population row
|
|
77
|
+
expected_len = None
|
|
78
|
+
|
|
79
|
+
with open(filepath) as fh:
|
|
80
|
+
for raw_line in fh:
|
|
81
|
+
line = raw_line.strip()
|
|
82
|
+
if not line:
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
# Header line: "lenlist= 16298"
|
|
86
|
+
if line.startswith("lenlist="):
|
|
87
|
+
expected_len = int(line.split("=")[1])
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
# Data rows start with a digit (the p_indx column)
|
|
91
|
+
if not line[0].isdigit():
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
parts = line.split()
|
|
95
|
+
if len(parts) < 8:
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
rows.append({
|
|
99
|
+
"index": int(parts[0]),
|
|
100
|
+
"con_typ": parts[1],
|
|
101
|
+
"pop_typ": parts[2],
|
|
102
|
+
"pop_id": parts[3],
|
|
103
|
+
"circ_id": parts[4],
|
|
104
|
+
"h_row": parts[5],
|
|
105
|
+
"d_row": parts[6],
|
|
106
|
+
"mat_id": parts[7],
|
|
107
|
+
"connection_input": [int(tok) for tok in parts[8:] if tok.isdigit()],
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
if expected_len is not None and len(rows) != expected_len:
|
|
111
|
+
raise ValueError(
|
|
112
|
+
f"Expected {expected_len} rows (from lenlist header) but parsed {len(rows)}"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# ------------------------------------------------------------------
|
|
116
|
+
# 2. Create population objects
|
|
117
|
+
# ------------------------------------------------------------------
|
|
118
|
+
# Index-ordered lists so row indices stay valid for connection wiring.
|
|
119
|
+
#
|
|
120
|
+
# Normal pop_ids are NOT unique (e.g. "GC_start_G1" appears 2745×
|
|
121
|
+
# across different circuit instances). External pop_ids ARE unique.
|
|
122
|
+
# We therefore use the row index as the Node name for normals, and
|
|
123
|
+
# the pop_id as the Input name for externals.
|
|
124
|
+
pop_objects = [] # Node | Input, ordered by architecture index
|
|
125
|
+
pop_is_node = [] # True if Node, False if Input
|
|
126
|
+
metadata: Dict[str, dict] = {}
|
|
127
|
+
|
|
128
|
+
_zero = lambda t: 0.0 # noqa: E731 — shared dormant activation
|
|
129
|
+
|
|
130
|
+
for row in rows:
|
|
131
|
+
idx = row["index"]
|
|
132
|
+
pid = row["pop_id"]
|
|
133
|
+
|
|
134
|
+
if row["pop_typ"] == "normal":
|
|
135
|
+
# Name by row index — guarantees uniqueness.
|
|
136
|
+
# Assign tau based on con_typ: excitatory vs inhibitory.
|
|
137
|
+
is_excit = row["con_typ"] in _EXCITATORY_CON_TYPES
|
|
138
|
+
tau = tau_E if is_excit else tau_I
|
|
139
|
+
obj = Node(str(idx), tau=tau, is_excitatory=is_excit)
|
|
140
|
+
pop_is_node.append(True)
|
|
141
|
+
else:
|
|
142
|
+
# External pop_ids are unique; use them directly so callers
|
|
143
|
+
# can look them up by descriptive name.
|
|
144
|
+
obj = Input(pid, _zero)
|
|
145
|
+
pop_is_node.append(False)
|
|
146
|
+
|
|
147
|
+
pop_objects.append(obj)
|
|
148
|
+
metadata[str(idx)] = {
|
|
149
|
+
"index": idx,
|
|
150
|
+
"pop_id": pid,
|
|
151
|
+
"con_typ": row["con_typ"],
|
|
152
|
+
"pop_typ": row["pop_typ"],
|
|
153
|
+
"circ_id": row["circ_id"],
|
|
154
|
+
"h_row": row["h_row"],
|
|
155
|
+
"d_row": row["d_row"],
|
|
156
|
+
"mat_id": row["mat_id"],
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# ------------------------------------------------------------------
|
|
160
|
+
# 3. Register in Network (build phase)
|
|
161
|
+
# ------------------------------------------------------------------
|
|
162
|
+
net = Network(sigmoid_params=SIGMOID_PARAMS, wc_coupling=WC_COUPLING_PARAMS)
|
|
163
|
+
|
|
164
|
+
for obj, is_node in zip(pop_objects, pop_is_node):
|
|
165
|
+
if is_node:
|
|
166
|
+
net.add_node(obj)
|
|
167
|
+
else:
|
|
168
|
+
net.add_input(obj)
|
|
169
|
+
|
|
170
|
+
# ------------------------------------------------------------------
|
|
171
|
+
# 4. Wire connections (connect phase — triggers __MapNodes on first call)
|
|
172
|
+
# ------------------------------------------------------------------
|
|
173
|
+
for row in rows:
|
|
174
|
+
if row["pop_typ"] != "normal":
|
|
175
|
+
continue # external pops are sources only, never targets
|
|
176
|
+
|
|
177
|
+
target_name = str(row["index"])
|
|
178
|
+
|
|
179
|
+
for src_idx in row["connection_input"]:
|
|
180
|
+
src_row = rows[src_idx]
|
|
181
|
+
src_con_typ = src_row["con_typ"]
|
|
182
|
+
|
|
183
|
+
scale = _WEIGHT_SCALE.get(src_con_typ)
|
|
184
|
+
if scale is None:
|
|
185
|
+
raise ValueError(
|
|
186
|
+
f"Unknown connection type '{src_con_typ}' on population "
|
|
187
|
+
f"{src_idx} ({src_row['pop_id']})"
|
|
188
|
+
)
|
|
189
|
+
w = weight * scale
|
|
190
|
+
|
|
191
|
+
if src_row["pop_typ"] == "normal":
|
|
192
|
+
net.ConnectNodeNode(str(src_idx), target_name, w)
|
|
193
|
+
else:
|
|
194
|
+
net.ConnectInputNode(src_row["pop_id"], target_name, w)
|
|
195
|
+
|
|
196
|
+
# Attach metadata for downstream use (visualisation, debugging).
|
|
197
|
+
net.population_metadata = metadata
|
|
198
|
+
|
|
199
|
+
return net
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# ======================================================================
|
|
203
|
+
# Instruction-file command → external pop_id mapping
|
|
204
|
+
# ======================================================================
|
|
205
|
+
|
|
206
|
+
_LMA_SHORT = ['Adj', 'Adv', 'C', 'Co', 'Det', 'N', 'P', 'Pr', 'V']
|
|
207
|
+
_LMA_FULL = [
|
|
208
|
+
'Adjective', 'Adverb', 'Clause', 'Coordinator', 'Determiner',
|
|
209
|
+
'Noun', 'Preposition', 'Pronoun', 'Verb',
|
|
210
|
+
]
|
|
211
|
+
_SG_NAMES = [
|
|
212
|
+
'SG1Verb', 'SG2Subject', 'SG3Object', 'SG4Det', 'SG5Compl',
|
|
213
|
+
'SG6Mod', 'SG7Adjunct', 'SG8PC', 'SG9Ex_Mod', 'SG10Coord', 'SG11GAP',
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _build_command_map() -> Dict[str, str]:
|
|
218
|
+
"""Return a mapping from instruction-file command → external pop_id."""
|
|
219
|
+
m: Dict[str, str] = {}
|
|
220
|
+
m['Word_inhib'] = 'EX0a_inhib_W_all'
|
|
221
|
+
|
|
222
|
+
for short, full in zip(_LMA_SHORT, _LMA_FULL):
|
|
223
|
+
m[full] = f'EX2_Bgate_W_{short}'
|
|
224
|
+
m[f'{full}_H'] = f'EX3H_Gate_{short}_H'
|
|
225
|
+
m[f'{full}_D'] = f'EX3D_Gate_{short}_D'
|
|
226
|
+
m[f'CMtoW_{full}_H'] = f'EX8H_LMA_out_{short}_H'
|
|
227
|
+
m[f'CMtoW_{full}_D'] = f'EX8D_LMA_out_{short}_D'
|
|
228
|
+
m[f'{full}_inhib'] = f'EX0_inhib_{short}'
|
|
229
|
+
|
|
230
|
+
for sg in _SG_NAMES:
|
|
231
|
+
m[f'binding_{sg}_H'] = f'EX4H_Bgate_LMA_{sg}_H'
|
|
232
|
+
m[f'binding_{sg}_D'] = f'EX4D_Bgate_LMA_{sg}_D'
|
|
233
|
+
m[f'{sg}_H'] = f'EX6H_Gate_{sg}_CM_H'
|
|
234
|
+
m[f'{sg}_D'] = f'EX6D_Gate_{sg}_CM_D'
|
|
235
|
+
m[f'{sg}_W_H'] = f'EX5H_Gate_{sg}_W_H'
|
|
236
|
+
m[f'{sg}_W_D'] = f'EX5D_Gate_{sg}_W_D'
|
|
237
|
+
m[f'inhibcomp_{sg}_H'] = f'EX7H_SG_comp_inhib_{sg}_H'
|
|
238
|
+
m[f'inhibcomp_{sg}_D'] = f'EX7D_SG_comp_inhib_{sg}_D'
|
|
239
|
+
|
|
240
|
+
return m
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
_COMMAND_MAP = _build_command_map()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# ======================================================================
|
|
247
|
+
# Instruction-file parser
|
|
248
|
+
# ======================================================================
|
|
249
|
+
|
|
250
|
+
def _parse_instruction_file(
|
|
251
|
+
filepath: Path,
|
|
252
|
+
) -> List[Tuple[int, int, str]]:
|
|
253
|
+
"""Parse a sentence/question instruction file.
|
|
254
|
+
|
|
255
|
+
Returns a list of ``(start_time, end_time, external_pop_id)``
|
|
256
|
+
triples, reproducing the logic of
|
|
257
|
+
``NBA24_QA_sentence_input.Sentence_file``.
|
|
258
|
+
"""
|
|
259
|
+
schedule: List[Tuple[int, int, str]] = []
|
|
260
|
+
word_list: List[str] = []
|
|
261
|
+
step_size = 0
|
|
262
|
+
|
|
263
|
+
with open(filepath) as fh:
|
|
264
|
+
for raw_line in fh:
|
|
265
|
+
parts = raw_line.split()
|
|
266
|
+
if not parts or raw_line.startswith('#'):
|
|
267
|
+
continue
|
|
268
|
+
|
|
269
|
+
cmd = parts[0]
|
|
270
|
+
|
|
271
|
+
if cmd == 'Num_of_words':
|
|
272
|
+
n_words = int(parts[1])
|
|
273
|
+
word_list = [f'EX1_input_W{k}' for k in range(n_words)]
|
|
274
|
+
continue
|
|
275
|
+
|
|
276
|
+
if cmd == 'Word':
|
|
277
|
+
start = step_size + int(parts[1])
|
|
278
|
+
step_size = start
|
|
279
|
+
end = step_size + int(parts[2])
|
|
280
|
+
word_idx = int(parts[3])
|
|
281
|
+
schedule.append((start, end, word_list[word_idx]))
|
|
282
|
+
continue
|
|
283
|
+
|
|
284
|
+
if cmd in _COMMAND_MAP:
|
|
285
|
+
start = step_size + int(parts[1])
|
|
286
|
+
end = step_size + int(parts[2])
|
|
287
|
+
schedule.append((start, end, _COMMAND_MAP[cmd]))
|
|
288
|
+
|
|
289
|
+
return schedule
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _make_piecewise_activation(
|
|
293
|
+
windows: List[Tuple[int, int]],
|
|
294
|
+
amplitude: float,
|
|
295
|
+
):
|
|
296
|
+
"""Return an activation function that is *amplitude* during any of
|
|
297
|
+
the given ``[start, end]`` windows and 0.0 otherwise."""
|
|
298
|
+
sorted_windows = sorted(windows)
|
|
299
|
+
|
|
300
|
+
def activation(t):
|
|
301
|
+
for start, end in sorted_windows:
|
|
302
|
+
if start <= t <= end:
|
|
303
|
+
return amplitude
|
|
304
|
+
return 0.0
|
|
305
|
+
|
|
306
|
+
return activation
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ======================================================================
|
|
310
|
+
# Public API: apply instruction schedule
|
|
311
|
+
# ======================================================================
|
|
312
|
+
|
|
313
|
+
def apply_instruction_schedule(
|
|
314
|
+
net: Network,
|
|
315
|
+
instruction_file: Union[str, Path],
|
|
316
|
+
amplitude: float = 100.0,
|
|
317
|
+
h: float = 0.1,
|
|
318
|
+
) -> None:
|
|
319
|
+
"""Set activation functions on external Inputs from an instruction file.
|
|
320
|
+
|
|
321
|
+
Parameters
|
|
322
|
+
----------
|
|
323
|
+
net : Network
|
|
324
|
+
A network previously created by :func:`load_architecture`.
|
|
325
|
+
instruction_file : str or Path
|
|
326
|
+
Path to a sentence/question instruction file (e.g.
|
|
327
|
+
``QA_3NVN_O_success.txt``).
|
|
328
|
+
amplitude : float, optional
|
|
329
|
+
Activation level during scheduled ON windows (default 100.0).
|
|
330
|
+
h : float, optional
|
|
331
|
+
Integration step size in ms (default 0.1, from ``pop_module.c``).
|
|
332
|
+
Instruction-file times are in timestep indices; multiplying by
|
|
333
|
+
*h* converts them to milliseconds for ``odeint``.
|
|
334
|
+
"""
|
|
335
|
+
instruction_file = Path(instruction_file)
|
|
336
|
+
if not instruction_file.exists():
|
|
337
|
+
raise FileNotFoundError(
|
|
338
|
+
f"Instruction file not found: {instruction_file}"
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
schedule = _parse_instruction_file(instruction_file)
|
|
342
|
+
|
|
343
|
+
# Group windows by pop_id (a single pop may appear in multiple triples).
|
|
344
|
+
# Scale timestep indices → ms by multiplying by h.
|
|
345
|
+
windows: Dict[str, List[Tuple[float, float]]] = defaultdict(list)
|
|
346
|
+
for start, end, pop_id in schedule:
|
|
347
|
+
windows[pop_id].append((start * h, end * h))
|
|
348
|
+
|
|
349
|
+
for pop_id, wins in windows.items():
|
|
350
|
+
inp = net.get_input(pop_id)
|
|
351
|
+
inp.activation_func = _make_piecewise_activation(wins, amplitude)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
# ======================================================================
|
|
355
|
+
# SGall instruction-file parser (fixed_time format)
|
|
356
|
+
# ======================================================================
|
|
357
|
+
|
|
358
|
+
def _parse_sgall_instruction_file(
|
|
359
|
+
filepath: Path,
|
|
360
|
+
) -> List[Tuple[int, int, str]]:
|
|
361
|
+
"""Parse an SGall-format sentence instruction file.
|
|
362
|
+
|
|
363
|
+
SGall files use a ``fixed_time`` directive and two-argument ``Word``
|
|
364
|
+
commands (``Word <offset> <word_idx>``), in contrast to QA files
|
|
365
|
+
which have explicit start/end times.
|
|
366
|
+
|
|
367
|
+
Returns a list of ``(start_time, end_time, external_pop_id)`` triples
|
|
368
|
+
in the same form as :func:`_parse_instruction_file`.
|
|
369
|
+
"""
|
|
370
|
+
schedule: List[Tuple[int, int, str]] = []
|
|
371
|
+
word_list: List[str] = []
|
|
372
|
+
step_size = 0
|
|
373
|
+
fixed_time = 0
|
|
374
|
+
|
|
375
|
+
with open(filepath) as fh:
|
|
376
|
+
for raw_line in fh:
|
|
377
|
+
parts = raw_line.split()
|
|
378
|
+
if not parts or raw_line.startswith('#'):
|
|
379
|
+
continue
|
|
380
|
+
|
|
381
|
+
cmd = parts[0]
|
|
382
|
+
|
|
383
|
+
if cmd == 'fixed_time':
|
|
384
|
+
fixed_time = int(parts[1])
|
|
385
|
+
elif cmd == 'Num_of_words':
|
|
386
|
+
n_words = int(parts[1])
|
|
387
|
+
word_list = [f'EX1_input_W{k}' for k in range(n_words)]
|
|
388
|
+
elif cmd == 'Word':
|
|
389
|
+
start = step_size + int(parts[1])
|
|
390
|
+
step_size = start
|
|
391
|
+
end = start + fixed_time
|
|
392
|
+
word_idx = int(parts[2])
|
|
393
|
+
schedule.append((start, end, word_list[word_idx]))
|
|
394
|
+
elif cmd in _COMMAND_MAP:
|
|
395
|
+
start = step_size + int(parts[1])
|
|
396
|
+
end = start + fixed_time
|
|
397
|
+
schedule.append((start, end, _COMMAND_MAP[cmd]))
|
|
398
|
+
|
|
399
|
+
return schedule
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def apply_sgall_instruction_schedule(
|
|
403
|
+
net: Network,
|
|
404
|
+
instruction_file: Union[str, Path],
|
|
405
|
+
amplitude: float = 100.0,
|
|
406
|
+
h: float = 0.1,
|
|
407
|
+
) -> None:
|
|
408
|
+
"""Set activation functions on external Inputs from an SGall sentence file.
|
|
409
|
+
|
|
410
|
+
Equivalent to :func:`apply_instruction_schedule` but handles the
|
|
411
|
+
SGall fixed-time instruction format (``fixed_time`` directive,
|
|
412
|
+
two-argument ``Word`` command).
|
|
413
|
+
|
|
414
|
+
Parameters
|
|
415
|
+
----------
|
|
416
|
+
net : Network
|
|
417
|
+
A network previously created by :func:`load_architecture`.
|
|
418
|
+
instruction_file : str or Path
|
|
419
|
+
Path to an SGall sentence instruction file (e.g.
|
|
420
|
+
``Sentence_2CV.txt``).
|
|
421
|
+
amplitude : float, optional
|
|
422
|
+
Activation level during scheduled ON windows (default 100.0).
|
|
423
|
+
h : float, optional
|
|
424
|
+
Integration step size in ms (default 0.1). Timestep indices in
|
|
425
|
+
the file are multiplied by *h* to convert to milliseconds.
|
|
426
|
+
"""
|
|
427
|
+
instruction_file = Path(instruction_file)
|
|
428
|
+
if not instruction_file.exists():
|
|
429
|
+
raise FileNotFoundError(
|
|
430
|
+
f"Instruction file not found: {instruction_file}"
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
schedule = _parse_sgall_instruction_file(instruction_file)
|
|
434
|
+
|
|
435
|
+
windows: Dict[str, List[Tuple[float, float]]] = defaultdict(list)
|
|
436
|
+
for start, end, pop_id in schedule:
|
|
437
|
+
windows[pop_id].append((start * h, end * h))
|
|
438
|
+
|
|
439
|
+
for pop_id, wins in windows.items():
|
|
440
|
+
inp = net.get_input(pop_id)
|
|
441
|
+
inp.activation_func = _make_piecewise_activation(wins, amplitude)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
# ======================================================================
|
|
445
|
+
# Public API: apply bindings
|
|
446
|
+
# ======================================================================
|
|
447
|
+
|
|
448
|
+
def _parse_bindings_file(filepath: Path) -> List[int]:
|
|
449
|
+
"""Parse a WM bindings file, returning the list of p_index values."""
|
|
450
|
+
indices: List[int] = []
|
|
451
|
+
with open(filepath) as fh:
|
|
452
|
+
for raw_line in fh:
|
|
453
|
+
parts = raw_line.split()
|
|
454
|
+
if parts and parts[0].isdigit():
|
|
455
|
+
indices.append(int(parts[0]))
|
|
456
|
+
return indices
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def apply_bindings(
|
|
460
|
+
net: Network,
|
|
461
|
+
bindings_file: Union[str, Path],
|
|
462
|
+
t_start: float,
|
|
463
|
+
t_end: float,
|
|
464
|
+
value: float,
|
|
465
|
+
) -> None:
|
|
466
|
+
"""Register clamps on WM binding populations from a bindings file.
|
|
467
|
+
|
|
468
|
+
Parameters
|
|
469
|
+
----------
|
|
470
|
+
net : Network
|
|
471
|
+
A network previously created by :func:`load_architecture`.
|
|
472
|
+
bindings_file : str or Path
|
|
473
|
+
Path to a WM bindings file (e.g.
|
|
474
|
+
``RNBA_SGall_SNB15_Bindings_Sentence_3NVN.txt``).
|
|
475
|
+
t_start : float
|
|
476
|
+
Start time of the clamp window.
|
|
477
|
+
t_end : float
|
|
478
|
+
End time of the clamp window.
|
|
479
|
+
value : float
|
|
480
|
+
Activation value to hold during the clamp window.
|
|
481
|
+
"""
|
|
482
|
+
bindings_file = Path(bindings_file)
|
|
483
|
+
if not bindings_file.exists():
|
|
484
|
+
raise FileNotFoundError(
|
|
485
|
+
f"Bindings file not found: {bindings_file}"
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
indices = _parse_bindings_file(bindings_file)
|
|
489
|
+
for idx in indices:
|
|
490
|
+
# Node names in load_architecture are the stringified row index.
|
|
491
|
+
net.add_clamp(str(idx), t_start, t_end, value)
|