dendrotweaks 0.3.1__py3-none-any.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.
- dendrotweaks/__init__.py +10 -0
- dendrotweaks/analysis/__init__.py +11 -0
- dendrotweaks/analysis/ephys_analysis.py +482 -0
- dendrotweaks/analysis/morphometric_analysis.py +106 -0
- dendrotweaks/membrane/__init__.py +6 -0
- dendrotweaks/membrane/default_mod/AMPA.mod +65 -0
- dendrotweaks/membrane/default_mod/AMPA_NMDA.mod +100 -0
- dendrotweaks/membrane/default_mod/CaDyn.mod +54 -0
- dendrotweaks/membrane/default_mod/GABAa.mod +65 -0
- dendrotweaks/membrane/default_mod/Leak.mod +27 -0
- dendrotweaks/membrane/default_mod/NMDA.mod +72 -0
- dendrotweaks/membrane/default_mod/vecstim.mod +76 -0
- dendrotweaks/membrane/default_templates/NEURON_template.py +354 -0
- dendrotweaks/membrane/default_templates/default.py +73 -0
- dendrotweaks/membrane/default_templates/standard_channel.mod +87 -0
- dendrotweaks/membrane/default_templates/template_jaxley.py +108 -0
- dendrotweaks/membrane/default_templates/template_jaxley_new.py +108 -0
- dendrotweaks/membrane/distributions.py +324 -0
- dendrotweaks/membrane/groups.py +103 -0
- dendrotweaks/membrane/io/__init__.py +11 -0
- dendrotweaks/membrane/io/ast.py +201 -0
- dendrotweaks/membrane/io/code_generators.py +312 -0
- dendrotweaks/membrane/io/converter.py +108 -0
- dendrotweaks/membrane/io/factories.py +144 -0
- dendrotweaks/membrane/io/grammar.py +417 -0
- dendrotweaks/membrane/io/loader.py +90 -0
- dendrotweaks/membrane/io/parser.py +499 -0
- dendrotweaks/membrane/io/reader.py +212 -0
- dendrotweaks/membrane/mechanisms.py +574 -0
- dendrotweaks/model.py +1916 -0
- dendrotweaks/model_io.py +75 -0
- dendrotweaks/morphology/__init__.py +5 -0
- dendrotweaks/morphology/domains.py +100 -0
- dendrotweaks/morphology/io/__init__.py +5 -0
- dendrotweaks/morphology/io/factories.py +212 -0
- dendrotweaks/morphology/io/reader.py +66 -0
- dendrotweaks/morphology/io/validation.py +212 -0
- dendrotweaks/morphology/point_trees.py +681 -0
- dendrotweaks/morphology/reduce/__init__.py +16 -0
- dendrotweaks/morphology/reduce/reduce.py +155 -0
- dendrotweaks/morphology/reduce/reduced_cylinder.py +129 -0
- dendrotweaks/morphology/sec_trees.py +1112 -0
- dendrotweaks/morphology/seg_trees.py +157 -0
- dendrotweaks/morphology/trees.py +567 -0
- dendrotweaks/path_manager.py +261 -0
- dendrotweaks/simulators.py +235 -0
- dendrotweaks/stimuli/__init__.py +3 -0
- dendrotweaks/stimuli/iclamps.py +73 -0
- dendrotweaks/stimuli/populations.py +265 -0
- dendrotweaks/stimuli/synapses.py +203 -0
- dendrotweaks/utils.py +239 -0
- dendrotweaks-0.3.1.dist-info/METADATA +70 -0
- dendrotweaks-0.3.1.dist-info/RECORD +56 -0
- dendrotweaks-0.3.1.dist-info/WHEEL +5 -0
- dendrotweaks-0.3.1.dist-info/licenses/LICENSE +674 -0
- dendrotweaks-0.3.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,499 @@
|
|
1
|
+
import re
|
2
|
+
import pprint
|
3
|
+
from typing import List, Dict, Union, Any
|
4
|
+
|
5
|
+
from dendrotweaks.membrane.io.grammar import title, comment_block
|
6
|
+
from dendrotweaks.membrane.io.grammar import neuron_block
|
7
|
+
from dendrotweaks.membrane.io.grammar import units_block, parameter_block, assigned_block
|
8
|
+
from dendrotweaks.membrane.io.grammar import state_block
|
9
|
+
from dendrotweaks.membrane.io.grammar import breakpoint_block, derivative_block, initial_block
|
10
|
+
from dendrotweaks.membrane.io.grammar import function_block, procedure_block
|
11
|
+
|
12
|
+
from dendrotweaks.membrane.io.ast import AbstracSyntaxTree
|
13
|
+
|
14
|
+
|
15
|
+
class MODFileParser():
|
16
|
+
"""
|
17
|
+
A parser for MOD files that uses a Pyparsing grammar
|
18
|
+
to parse the content of the file.
|
19
|
+
"""
|
20
|
+
|
21
|
+
BLOCKS = {"TITLE": title,
|
22
|
+
"COMMENT": comment_block,
|
23
|
+
"NEURON": neuron_block,
|
24
|
+
"UNITS": units_block,
|
25
|
+
"PARAMETER": parameter_block,
|
26
|
+
"ASSIGNED": assigned_block,
|
27
|
+
"STATE": state_block,
|
28
|
+
"BREAKPOINT": breakpoint_block,
|
29
|
+
"DERIVATIVE": derivative_block,
|
30
|
+
"INITIAL": initial_block,
|
31
|
+
"FUNCTION": function_block,
|
32
|
+
"PROCEDURE": procedure_block,
|
33
|
+
}
|
34
|
+
|
35
|
+
def __init__(self):
|
36
|
+
self._result = {}
|
37
|
+
self._ast = {}
|
38
|
+
|
39
|
+
def info(self):
|
40
|
+
"""
|
41
|
+
Print information about the parser.
|
42
|
+
"""
|
43
|
+
print(f"\n{'='*20}\nPARSER\n")
|
44
|
+
print(f"File parsed: {bool(self._ast)}")
|
45
|
+
for block_name, parsed_content in self._ast.items():
|
46
|
+
print(f"{bool(parsed_content):1} - {block_name}")
|
47
|
+
|
48
|
+
# PARSING
|
49
|
+
|
50
|
+
|
51
|
+
def get_ast(self) -> Dict:
|
52
|
+
"""
|
53
|
+
Get the abstract syntax tree of the parsed content.
|
54
|
+
Available after parsing the content of the file.
|
55
|
+
"""
|
56
|
+
return AbstracSyntaxTree(self._ast)
|
57
|
+
|
58
|
+
def parse_block(self, block_name: str, block_content: List[str]) -> List[Dict]:
|
59
|
+
"""
|
60
|
+
Parse a block of the MOD file.
|
61
|
+
Ensures that parsing is independent for each block.
|
62
|
+
"""
|
63
|
+
grammar = self.BLOCKS.get(block_name)
|
64
|
+
if grammar is None:
|
65
|
+
return [] # Or handle the error appropriately
|
66
|
+
|
67
|
+
parsed_blocks = [grammar.parseString(block) for block in block_content]
|
68
|
+
self._result[block_name] = parsed_blocks
|
69
|
+
return [block.asDict()['block'] for block in parsed_blocks]
|
70
|
+
|
71
|
+
def parse(self, blocks: Dict[str, List[str]], verbose: bool = True) -> None:
|
72
|
+
"""
|
73
|
+
Parse the entire content of the file.
|
74
|
+
|
75
|
+
Parameters
|
76
|
+
----------
|
77
|
+
blocks : Dict[str, List[str]]
|
78
|
+
A dictionary with the blocks of the MOD file.
|
79
|
+
"""
|
80
|
+
for block_name, block_content in blocks.items():
|
81
|
+
self.parse_block(block_name, block_content)
|
82
|
+
if verbose: print(f"Parsed {block_name} block")
|
83
|
+
self._ast = {block_name: [r.asDict()['block'] for r in result]
|
84
|
+
for block_name, result in self._result.items()}
|
85
|
+
self._ast = {k: v[0] if len(v) == 1 and k not in ['FUNCTION', 'PROCEDURE'] else v
|
86
|
+
for k, v in self._ast.items()}
|
87
|
+
|
88
|
+
# POST PROCESSING
|
89
|
+
|
90
|
+
def postprocess(self, restore_expressions=True):
|
91
|
+
"""
|
92
|
+
Postprocess the parsed AST.
|
93
|
+
|
94
|
+
Parameters
|
95
|
+
----------
|
96
|
+
restore_expressions : bool
|
97
|
+
Whether to restore the expressions in the AST to their original form
|
98
|
+
after parsing.
|
99
|
+
"""
|
100
|
+
# self.split_comment_block()
|
101
|
+
self.standardize_state_var_names()
|
102
|
+
self.update_state_vars_with_power()
|
103
|
+
self.restore_expressions()
|
104
|
+
|
105
|
+
def split_comment_block(self):
|
106
|
+
|
107
|
+
comment_block = [line for line in self._ast['COMMENT'].split('\n') if line]
|
108
|
+
self._ast['COMMENT'] = comment_block
|
109
|
+
|
110
|
+
def restore_expressions(self):
|
111
|
+
"""
|
112
|
+
Restore the expressions in the AST to their original form.
|
113
|
+
"""
|
114
|
+
for block_name, block_asts in self._ast.items():
|
115
|
+
if block_name in ['FUNCTION', 'PROCEDURE']:
|
116
|
+
for i, block_ast in enumerate(block_asts):
|
117
|
+
for j, statement in enumerate(block_ast['statements']):
|
118
|
+
if 'condition' in statement:
|
119
|
+
condition = restore_expression(statement['condition'])
|
120
|
+
self._ast[block_name][i]['statements'][j]['condition'] = condition
|
121
|
+
if_statements = statement['if_statements']
|
122
|
+
for k, if_statement in enumerate(if_statements):
|
123
|
+
expression = restore_expression(if_statement['expression'])
|
124
|
+
self._ast[block_name][i]['statements'][j]['if_statements'][k]['expression'] = expression
|
125
|
+
if 'else_statements' in statement:
|
126
|
+
else_statements = statement['else_statements']
|
127
|
+
for k, else_statement in enumerate(else_statements):
|
128
|
+
expression = restore_expression(else_statement['expression'])
|
129
|
+
self._ast[block_name][i]['statements'][j]['else_statements'][k]['expression'] = expression
|
130
|
+
else:
|
131
|
+
expression = restore_expression(statement['expression'])
|
132
|
+
self._ast[block_name][i]['statements'][j]['expression'] = expression
|
133
|
+
elif block_name in ['BREAKPOINT', 'DERIVATIVE', ]:
|
134
|
+
for j, statement in enumerate(block_asts['statements']):
|
135
|
+
expression = restore_expression(statement['expression'])
|
136
|
+
self._ast[block_name]['statements'][j]['expression'] = expression
|
137
|
+
|
138
|
+
# General methods
|
139
|
+
|
140
|
+
def find_in_blocks(self, pattern, block_types = ['ASSIGNED', 'PROCEDURE', 'PARAMETER']):
|
141
|
+
"""
|
142
|
+
Find a pattern in the specified block types.
|
143
|
+
|
144
|
+
Parameters
|
145
|
+
----------
|
146
|
+
pattern : str
|
147
|
+
The regex pattern to search for in the blocks.
|
148
|
+
block_types : list
|
149
|
+
A list of block types to search for the pattern.
|
150
|
+
|
151
|
+
Returns
|
152
|
+
-------
|
153
|
+
str or list or None
|
154
|
+
A list of matching strings if found, a single matching string if there's only one,
|
155
|
+
or None if no matches are found.
|
156
|
+
|
157
|
+
Examples
|
158
|
+
--------
|
159
|
+
Find the name of the variable representing time constant of a state variable:
|
160
|
+
>>> pattern = re.compile('tau', re.IGNORECASE)
|
161
|
+
>>> parser.find_in_blocks('tau')
|
162
|
+
['m_tau', 'hTau', 'ntau']
|
163
|
+
"""
|
164
|
+
for block_type in block_types:
|
165
|
+
matches = find_in_nested_dict(self._ast[block_type], pattern)
|
166
|
+
if matches:
|
167
|
+
return matches[0] if len(matches) == 1 else matches
|
168
|
+
|
169
|
+
def replace_in_blocks(self, replacements, block_types = ['FUNCTION', 'PROCEDURE']):
|
170
|
+
"""
|
171
|
+
Replace the variable names with their values or another variable name in
|
172
|
+
the specified block types.
|
173
|
+
|
174
|
+
Parameters
|
175
|
+
----------
|
176
|
+
replacements : dict
|
177
|
+
A dictionary with the constants as keys and their replacement values.
|
178
|
+
block_types : list
|
179
|
+
A list of block types to apply the replacements to.
|
180
|
+
|
181
|
+
Examples
|
182
|
+
--------
|
183
|
+
Replace the Faraday constant with its value in every FUNCTION block:
|
184
|
+
>>> parser.replace({'FARADAY': 96485.309}, block_types=['FUNCTION'])
|
185
|
+
"""
|
186
|
+
for block_type in block_types:
|
187
|
+
for old, new in replacements.items():
|
188
|
+
self._ast[block_type] = [
|
189
|
+
replace_in_nested_dict(block, old, new)
|
190
|
+
for block in self._ast[block_type]
|
191
|
+
]
|
192
|
+
|
193
|
+
# Specific methods
|
194
|
+
|
195
|
+
def standardize_state_var_names(self):
|
196
|
+
"""
|
197
|
+
Standardize the names of the variables representing the inf and tau of
|
198
|
+
the state variables in the MOD file.
|
199
|
+
"""
|
200
|
+
|
201
|
+
for state_var in self._ast['STATE']:
|
202
|
+
|
203
|
+
# print(f"Standardizing names for state variable: {state_var}")
|
204
|
+
|
205
|
+
inf_pattern = f'({state_var}.*inf|inf.*{state_var})'
|
206
|
+
inf_pattern = re.compile(inf_pattern, re.IGNORECASE)
|
207
|
+
|
208
|
+
tau_pattern = f'({state_var}.*tau|tau.*{state_var})'
|
209
|
+
tau_pattern = re.compile(tau_pattern, re.IGNORECASE)
|
210
|
+
|
211
|
+
inf_var_name = self.find_in_blocks(pattern=inf_pattern)
|
212
|
+
tau_var_name = self.find_in_blocks(pattern=tau_pattern)
|
213
|
+
|
214
|
+
# print(f"Found inf variable: {inf_var_name}")
|
215
|
+
# print(f"Found tau variable: {tau_var_name}")
|
216
|
+
|
217
|
+
replacements = {
|
218
|
+
inf_var_name: f'{state_var}Inf',
|
219
|
+
tau_var_name: f'{state_var}Tau'
|
220
|
+
}
|
221
|
+
|
222
|
+
self.replace_in_blocks(replacements)
|
223
|
+
|
224
|
+
def _find_power(self, expression: Dict or List, state_var: str, power=0):
|
225
|
+
"""
|
226
|
+
Finds the power of a given state variable in an expression by
|
227
|
+
recursively searching the expression in a nested dictionary.
|
228
|
+
|
229
|
+
Parameters
|
230
|
+
----------
|
231
|
+
expression : dict or list
|
232
|
+
The expression to search for the state variable.
|
233
|
+
state_var : str
|
234
|
+
The state variable name to search for.
|
235
|
+
power : int
|
236
|
+
The current power of the state variable. Used to keep track of the power.
|
237
|
+
|
238
|
+
Returns
|
239
|
+
-------
|
240
|
+
int
|
241
|
+
The power of the state variable in the expression.
|
242
|
+
|
243
|
+
Examples
|
244
|
+
--------
|
245
|
+
Consider a statement in the BREAKPOINT block for a Na channel:
|
246
|
+
>>> g = tadj * gbar * pow(m, 3) * h
|
247
|
+
The expression for this statement could be represented as a nested dictionary:
|
248
|
+
>>> expr = {'*': ['tadj', {'*': ['gbar', {'*': [{'pow': ['m', 3]}, 'h']}]}]}
|
249
|
+
The corresponding tree representation would be:
|
250
|
+
*
|
251
|
+
└── tadj
|
252
|
+
└── *
|
253
|
+
├── gbar
|
254
|
+
└── *
|
255
|
+
├── pow
|
256
|
+
│ ├── m
|
257
|
+
│ └── 3
|
258
|
+
└── h
|
259
|
+
To find the power of 'm' and 'h' in the expression:
|
260
|
+
>>> parser._find_power(expr, 'm')
|
261
|
+
3
|
262
|
+
>>> parser._find_power(expr, 'h')
|
263
|
+
1
|
264
|
+
"""
|
265
|
+
# If expression is a dictionary (e.g. {'pow': ['m', 3]})
|
266
|
+
if isinstance(expression, dict):
|
267
|
+
for operator, operands in expression.items():
|
268
|
+
if operator == 'pow' and operands[0] == state_var:
|
269
|
+
# Found a power operation on the target variable
|
270
|
+
power = int(operands[1])
|
271
|
+
else:
|
272
|
+
# Continue traversing the dictionary
|
273
|
+
power = self._find_power(operands, state_var, power)
|
274
|
+
|
275
|
+
# If expression is a list (e.g. [{'pow': ['m', 3]}, 'h'])
|
276
|
+
elif isinstance(expression, list):
|
277
|
+
for operand in expression:
|
278
|
+
power = self._find_power(operand, state_var, power)
|
279
|
+
|
280
|
+
# If expression directly matches the variable (e.g., 'h')
|
281
|
+
elif expression == state_var:
|
282
|
+
# A standalone variable has an implicit power of 1
|
283
|
+
power += 1 # In case the variable is found multiple times in the expression
|
284
|
+
|
285
|
+
# If none of the above, just return current power
|
286
|
+
return power
|
287
|
+
|
288
|
+
def update_state_vars_with_power(self):
|
289
|
+
"""
|
290
|
+
Update the state variables in the AST with the corresponding power
|
291
|
+
from the equation in the BREAKPOINT block.
|
292
|
+
"""
|
293
|
+
if self._ast['BREAKPOINT'].get('statements'):
|
294
|
+
expr = self._ast['BREAKPOINT']['statements'][0]['expression']
|
295
|
+
state_vars = {
|
296
|
+
state_var: {'power': self._find_power(expr, state_var)}
|
297
|
+
for state_var in self._ast['STATE']
|
298
|
+
}
|
299
|
+
self._ast['STATE'] = state_vars
|
300
|
+
else:
|
301
|
+
print(
|
302
|
+
f"The breakpoint block for {self._ast.suffix} does not have any statements.")
|
303
|
+
|
304
|
+
|
305
|
+
|
306
|
+
# HELPER FUNCTIONS
|
307
|
+
|
308
|
+
def replace_in_nested_dict(data: Dict, target: Any, replacement: Any) -> Dict:
|
309
|
+
"""
|
310
|
+
A recursive helper function to replace a target value
|
311
|
+
with a replacement value in a nested dictionary.
|
312
|
+
|
313
|
+
Notes
|
314
|
+
-----
|
315
|
+
The structure of the AST dictionary assumes that dictionaries can have:
|
316
|
+
- as a key a single value (string) that represents an operator (e.g. '+' or '*')
|
317
|
+
or a function name (e.g. 'pow').
|
318
|
+
- as a value either a list or a single value (string or number). A list can contain
|
319
|
+
only dictionaries or single values (string or number).
|
320
|
+
Lists represent the operands of an operator or arguments of a function.
|
321
|
+
Dictionaries represent the operator or function and its operands or arguments.
|
322
|
+
Single values represent variables or numbers.
|
323
|
+
|
324
|
+
Parameters
|
325
|
+
----------
|
326
|
+
data : dict
|
327
|
+
The original dictionary to search for the target value.
|
328
|
+
target : any
|
329
|
+
The value to search for in the dictionary.
|
330
|
+
replacement : any
|
331
|
+
The value to replace the target value with.
|
332
|
+
|
333
|
+
Examples
|
334
|
+
--------
|
335
|
+
Rename a variable:
|
336
|
+
>>> d = {'+': {'a': 'b'}} # Represents a + b
|
337
|
+
>>> replace_in_nested_dict(d, 'a', 'c')
|
338
|
+
{'+': {'c': 'b'}}
|
339
|
+
|
340
|
+
Replace a constant name with its value:
|
341
|
+
>>> d = {'*': {'a': 'FARADAY'}} # Represents a * FARADAY
|
342
|
+
>>> replace_in_nested_dict(d, 'FARADAY', 96485.309)
|
343
|
+
{'*': {'a': 96485.309}}
|
344
|
+
|
345
|
+
Returns
|
346
|
+
-------
|
347
|
+
dict
|
348
|
+
The original dictionary with the target value replaced
|
349
|
+
by the replacement value.
|
350
|
+
"""
|
351
|
+
if isinstance(data, dict):
|
352
|
+
return {key: replace_in_nested_dict(value, target, replacement)
|
353
|
+
for key, value in data.items()}
|
354
|
+
elif isinstance(data, list):
|
355
|
+
return [replace_in_nested_dict(item, target, replacement)
|
356
|
+
for item in data]
|
357
|
+
elif data == target:
|
358
|
+
return replacement
|
359
|
+
else:
|
360
|
+
return data
|
361
|
+
|
362
|
+
def find_in_nested_dict(data: Dict, pattern: str) -> Union[str, List[str], None]:
|
363
|
+
"""
|
364
|
+
A recursive function to find strings in a nested dictionary
|
365
|
+
that match a given regex pattern.
|
366
|
+
|
367
|
+
Parameters
|
368
|
+
----------
|
369
|
+
data : dict
|
370
|
+
The dictionary to search within.
|
371
|
+
pattern : str
|
372
|
+
The regex pattern to search for within string values in the dictionary.
|
373
|
+
|
374
|
+
Returns
|
375
|
+
-------
|
376
|
+
Union[str, List[str], None]
|
377
|
+
A list of matching strings if found, a single matching string if there's only one,
|
378
|
+
or None if no matches are found.
|
379
|
+
"""
|
380
|
+
matches = []
|
381
|
+
|
382
|
+
def _recursive_search(data: Any):
|
383
|
+
if isinstance(data, dict):
|
384
|
+
for key, value in data.items():
|
385
|
+
if isinstance(key, str) and re.search(pattern, key):
|
386
|
+
matches.append(key)
|
387
|
+
_recursive_search(value)
|
388
|
+
elif isinstance(data, list):
|
389
|
+
for item in data:
|
390
|
+
_recursive_search(item)
|
391
|
+
elif isinstance(data, str) and re.search(pattern, data):
|
392
|
+
matches.append(data)
|
393
|
+
|
394
|
+
_recursive_search(data)
|
395
|
+
if not matches:
|
396
|
+
return None
|
397
|
+
matches = list(set(matches))
|
398
|
+
return matches if len(matches) > 1 else matches[0]
|
399
|
+
|
400
|
+
|
401
|
+
def restore_expression(d):
|
402
|
+
"""
|
403
|
+
Recursively restore the expression from the AST dictionary
|
404
|
+
and remove the outermost parentheses if they exist.
|
405
|
+
|
406
|
+
Parameters
|
407
|
+
----------
|
408
|
+
d : dict
|
409
|
+
The AST dictionary representing the expression
|
410
|
+
|
411
|
+
Returns
|
412
|
+
-------
|
413
|
+
str
|
414
|
+
The restored expression
|
415
|
+
|
416
|
+
Examples
|
417
|
+
--------
|
418
|
+
>>> d = {'exp': {'/': [{'-':['v', 'vhalf']}, 'q']}}
|
419
|
+
>>> restore_expression(d)
|
420
|
+
'exp((v - vhalf) / q)'
|
421
|
+
"""
|
422
|
+
|
423
|
+
def remove_parentheses(s):
|
424
|
+
if s.startswith('(') and s.endswith(')'):
|
425
|
+
return s[1:-1]
|
426
|
+
return s
|
427
|
+
|
428
|
+
NMODL_TO_PY = {
|
429
|
+
'exp': 'np.exp',
|
430
|
+
'log': 'np.log',
|
431
|
+
'log10': 'np.log10',
|
432
|
+
'sin': 'np.sin',
|
433
|
+
'cos': 'np.cos',
|
434
|
+
'tan': 'np.tan',
|
435
|
+
'sqrt': 'np.sqrt',
|
436
|
+
'fabs': 'np.abs',
|
437
|
+
'pow': 'np.power',
|
438
|
+
}
|
439
|
+
|
440
|
+
OPERATORS = ['+', '-', '*', '/', '^', '>', '<', '==']
|
441
|
+
|
442
|
+
def handle_operator_expression(key, value):
|
443
|
+
"""
|
444
|
+
Handles operator expressions (e.g., +, -, ^) within the expression.
|
445
|
+
"""
|
446
|
+
operator = '**' if key == '^' else key
|
447
|
+
joined = f" {operator} ".join(
|
448
|
+
recursively_restore_expression(v) for v in value
|
449
|
+
)
|
450
|
+
return f"({joined})"
|
451
|
+
|
452
|
+
def handle_function_call(key, value):
|
453
|
+
"""
|
454
|
+
Handles function calls with arguments.
|
455
|
+
"""
|
456
|
+
args = ", ".join(recursively_restore_expression(v) for v in value)
|
457
|
+
return f"{key}({args})"
|
458
|
+
|
459
|
+
def handle_single_value(key, value):
|
460
|
+
"""
|
461
|
+
Handles cases where the value list has a single element.
|
462
|
+
"""
|
463
|
+
inner = recursively_restore_expression(value[0])
|
464
|
+
if key == '-':
|
465
|
+
return f"-{inner}"
|
466
|
+
return f"{key}({inner})"
|
467
|
+
|
468
|
+
def map_key(key):
|
469
|
+
"""
|
470
|
+
Maps the key using the NMODL_TO_PY mapping if it exists, or returns the original key.
|
471
|
+
"""
|
472
|
+
return NMODL_TO_PY.get(key, key)
|
473
|
+
|
474
|
+
def recursively_restore_expression(expr):
|
475
|
+
"""
|
476
|
+
Recursively restores the given nested expression into a string representation.
|
477
|
+
"""
|
478
|
+
if isinstance(expr, dict):
|
479
|
+
for key, value in expr.items():
|
480
|
+
# Map the key using the helper function
|
481
|
+
key = map_key(key)
|
482
|
+
|
483
|
+
if isinstance(value, list):
|
484
|
+
if len(value) == 1:
|
485
|
+
return handle_single_value(key, value)
|
486
|
+
|
487
|
+
if key in OPERATORS:
|
488
|
+
return handle_operator_expression(key, value)
|
489
|
+
|
490
|
+
return handle_function_call(key, value)
|
491
|
+
|
492
|
+
# Handle unexpected single non-list value
|
493
|
+
raise ValueError(f"Unexpected value: {value}")
|
494
|
+
|
495
|
+
# Base case: leaf node (not a dict or list)
|
496
|
+
return str(expr)
|
497
|
+
|
498
|
+
|
499
|
+
return remove_parentheses(recursively_restore_expression(d))
|