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.
Files changed (56) hide show
  1. dendrotweaks/__init__.py +10 -0
  2. dendrotweaks/analysis/__init__.py +11 -0
  3. dendrotweaks/analysis/ephys_analysis.py +482 -0
  4. dendrotweaks/analysis/morphometric_analysis.py +106 -0
  5. dendrotweaks/membrane/__init__.py +6 -0
  6. dendrotweaks/membrane/default_mod/AMPA.mod +65 -0
  7. dendrotweaks/membrane/default_mod/AMPA_NMDA.mod +100 -0
  8. dendrotweaks/membrane/default_mod/CaDyn.mod +54 -0
  9. dendrotweaks/membrane/default_mod/GABAa.mod +65 -0
  10. dendrotweaks/membrane/default_mod/Leak.mod +27 -0
  11. dendrotweaks/membrane/default_mod/NMDA.mod +72 -0
  12. dendrotweaks/membrane/default_mod/vecstim.mod +76 -0
  13. dendrotweaks/membrane/default_templates/NEURON_template.py +354 -0
  14. dendrotweaks/membrane/default_templates/default.py +73 -0
  15. dendrotweaks/membrane/default_templates/standard_channel.mod +87 -0
  16. dendrotweaks/membrane/default_templates/template_jaxley.py +108 -0
  17. dendrotweaks/membrane/default_templates/template_jaxley_new.py +108 -0
  18. dendrotweaks/membrane/distributions.py +324 -0
  19. dendrotweaks/membrane/groups.py +103 -0
  20. dendrotweaks/membrane/io/__init__.py +11 -0
  21. dendrotweaks/membrane/io/ast.py +201 -0
  22. dendrotweaks/membrane/io/code_generators.py +312 -0
  23. dendrotweaks/membrane/io/converter.py +108 -0
  24. dendrotweaks/membrane/io/factories.py +144 -0
  25. dendrotweaks/membrane/io/grammar.py +417 -0
  26. dendrotweaks/membrane/io/loader.py +90 -0
  27. dendrotweaks/membrane/io/parser.py +499 -0
  28. dendrotweaks/membrane/io/reader.py +212 -0
  29. dendrotweaks/membrane/mechanisms.py +574 -0
  30. dendrotweaks/model.py +1916 -0
  31. dendrotweaks/model_io.py +75 -0
  32. dendrotweaks/morphology/__init__.py +5 -0
  33. dendrotweaks/morphology/domains.py +100 -0
  34. dendrotweaks/morphology/io/__init__.py +5 -0
  35. dendrotweaks/morphology/io/factories.py +212 -0
  36. dendrotweaks/morphology/io/reader.py +66 -0
  37. dendrotweaks/morphology/io/validation.py +212 -0
  38. dendrotweaks/morphology/point_trees.py +681 -0
  39. dendrotweaks/morphology/reduce/__init__.py +16 -0
  40. dendrotweaks/morphology/reduce/reduce.py +155 -0
  41. dendrotweaks/morphology/reduce/reduced_cylinder.py +129 -0
  42. dendrotweaks/morphology/sec_trees.py +1112 -0
  43. dendrotweaks/morphology/seg_trees.py +157 -0
  44. dendrotweaks/morphology/trees.py +567 -0
  45. dendrotweaks/path_manager.py +261 -0
  46. dendrotweaks/simulators.py +235 -0
  47. dendrotweaks/stimuli/__init__.py +3 -0
  48. dendrotweaks/stimuli/iclamps.py +73 -0
  49. dendrotweaks/stimuli/populations.py +265 -0
  50. dendrotweaks/stimuli/synapses.py +203 -0
  51. dendrotweaks/utils.py +239 -0
  52. dendrotweaks-0.3.1.dist-info/METADATA +70 -0
  53. dendrotweaks-0.3.1.dist-info/RECORD +56 -0
  54. dendrotweaks-0.3.1.dist-info/WHEEL +5 -0
  55. dendrotweaks-0.3.1.dist-info/licenses/LICENSE +674 -0
  56. 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))