nrn-patch 4.0.0__py2.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Robin De Schepper
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,107 @@
1
+ Metadata-Version: 2.1
2
+ Name: nrn-patch
3
+ Version: 4.0.0
4
+ Summary: Quality of life patch for the NEURON simulator.
5
+ Author-email: Robin De Schepper <robingilbert.deschepper@unipv.it>
6
+ Description-Content-Type: text/markdown
7
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Dist: nmodl-glia[neuron]~=4.0
14
+ Requires-Dist: errr~=1.2
15
+ Requires-Dist: numpy~=1.21
16
+ Requires-Dist: NEURON>=8.0,<10.0
17
+ Requires-Dist: nrn-patch[test, docs] ; extra == "dev"
18
+ Requires-Dist: pre-commit~=3.0 ; extra == "dev"
19
+ Requires-Dist: black~=24.0 ; extra == "dev"
20
+ Requires-Dist: sphinx~=7.2 ; extra == "docs"
21
+ Requires-Dist: helveg--sphinx-code-tabs~=0.2 ; extra == "docs"
22
+ Requires-Dist: sphinx-rtd-theme~=2.0 ; extra == "docs"
23
+ Requires-Dist: mpi4py~=3.0 ; extra == "parallel"
24
+ Requires-Dist: coverage~=7.0 ; extra == "test"
25
+ Project-URL: Home, https://github.com/dbbs-lab/patch
26
+ Provides-Extra: dev
27
+ Provides-Extra: docs
28
+ Provides-Extra: parallel
29
+ Provides-Extra: test
30
+
31
+ ![build](https://github.com/Helveg/patch/actions/workflows/main.yml/badge.svg)
32
+ [![codecov](https://codecov.io/gh/Helveg/patch/branch/master/graph/badge.svg)](https://codecov.io/gh/Helveg/patch)
33
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
34
+ [![Documentation Status](https://readthedocs.org/projects/patch/badge/?version=latest)](https://patch.readthedocs.io/en/latest/?badge=latest)
35
+
36
+
37
+ ![Drop in replacement](patch.gif)
38
+
39
+ _*No ducks were punched during the construction of this monkey patch._
40
+
41
+ # Installation
42
+
43
+ ```
44
+ pip install nrn-patch
45
+ ```
46
+
47
+ ## Minimal requirements
48
+
49
+ * Python 3.8+
50
+ * NEURON 8.0+
51
+
52
+ # Philosophy
53
+
54
+ Pythonic reinvention of the NEURON Python interface:
55
+
56
+ - **Drop-in replacement:** No breaking changes between Patch and NEURON. All
57
+ your code works as is. Patch automagically fixes bugs and reduces
58
+ complexity in NEURON by having an opinion, and adds new convenient methods.
59
+ - **Loud errors:** silent failures and gotchas are caught and raise
60
+ loud errors; so that when it runs, it runs.
61
+ - **Strong referencing:** Objects connected to each other will never disappear
62
+ until you disconnect them.
63
+ - **Demystified:** The simplest form of each instruction does the most obvious thing.
64
+ Patch frequently replaces 5 or more undocumented mystical voodoo steps by 1 clearly named function.
65
+ - **Just add water:** Serial or parallel, doesn't matter, `p.run()` will run your
66
+ simulation with no extra steps. No `load_file`, `finitialize`, `fadvance`, `run`,
67
+ `continuerun`, `tstop`, `set_maxstep`, `setup_transfer`, `setgid2node`, `gid_connect`,
68
+ `cell`, `outputcell`, `psolve` required.
69
+
70
+ # Basic usage
71
+
72
+ Use it like you would use NEURON. The wrapper doesn't make any changes to the interface,
73
+ it just patches up some of the more frequent and ridiculous gotchas.
74
+
75
+ Patch supplies a new interpreter `p`, the `PythonHocInterpreter` which wraps the
76
+ standard HOC interpreter `h` provided by NEURON. Any objects returned will either be
77
+ `PythonHocObject`'s wrapping their corresponding NEURON object, or whatever NEURON
78
+ returns.
79
+
80
+ When using just Patch the difference between NEURON and Patch objects is handled
81
+ transparently, but if you wish to mix interpreters you can transform all Patch objects
82
+ back to NEURON objects with `obj.__neuron__()`, or the `transform` function.
83
+
84
+ ## Example
85
+
86
+ ``` python
87
+ from patch import p
88
+ import glia as g
89
+
90
+ section = p.Section()
91
+ point_process = g.insert(section, "AMPA")
92
+ point_process.stimulate(start=10, number=5, interval=10, weight=0.04)
93
+ ```
94
+
95
+ `.stimulate` creates both the `NetCon` and `NetStim`, and sets their properties, and
96
+ stores references to eachother so that Python doesn't garbage collect anything
97
+ until we're done with it.
98
+
99
+ Even when you forget to set values, the Patch defaults have a visible effect.
100
+ Here we forget to set the duration of a voltage clamp, but by default it will
101
+ be active the first 100ms of the simulation, so that you can see that the voltage
102
+ clamp is in fact inserted and working, but you forgot to set the duration:
103
+
104
+ ```
105
+ section.vclamp(holding=-70, voltage=20)
106
+ ```
107
+
@@ -0,0 +1,13 @@
1
+ patch/__init__.py,sha256=RP-z1eF-iRlUZmTSQo3HgIU8jOQJ-Bq8KwTqB0yUy5c,1862
2
+ patch/core.py,sha256=Ik-oX8yknVsaah-ojQSThcEEIKUnSo9t5XvdYTBKDWY,3780
3
+ patch/error_handler.py,sha256=8YK7z4aDj417pF3TyIwf9fRX2ldWdoER04EwD9-ZS7U,6453
4
+ patch/exceptions.py,sha256=21y1rV6FPu7X8KfSPY691zh9nOXbpP-u-3QmbkVzfDs,1099
5
+ patch/interpreter.py,sha256=qprHpqiOqC7imqeIceBL3uzAWm70fGs33HWCNe7vr5k,20298
6
+ patch/objects.py,sha256=RNqWwY3pnBruWE3JQJnLOuT1Us1fWeoMWEIOh5INbZ8,24542
7
+ patch/extensions/__init__.py,sha256=_wRFsm-XntEtqgDUnuOoro-GfXeuALdPlwQhAT7II2Y,232
8
+ patch/extensions/mod/VecStim.mod,sha256=0NSBaol7vnQBTARDpyVK5orL_iPdypxWUIqloSYFUn8,2741
9
+ nrn_patch-4.0.0.dist-info/entry_points.txt,sha256=1EWv69vzvdN9wt4upC_zWSiB2TSa-RP4Kc9rXslzzo8,52
10
+ nrn_patch-4.0.0.dist-info/LICENSE,sha256=tJ_zaKC2tNV_HCEYSG7iwXiJjC_IYQpDiJYsgWkCMdI,1095
11
+ nrn_patch-4.0.0.dist-info/WHEEL,sha256=Sgu64hAMa6g5FdzHxXv9Xdse9yxpGGMeagVtPMWpJQY,99
12
+ nrn_patch-4.0.0.dist-info/METADATA,sha256=gUtgnxMATlLhAC3g2twj_2Gx9zwmRpAlAadKRhbnAPo,4339
13
+ nrn_patch-4.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.9.0
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [glia.package]
2
+ extensions=patch.extensions:package
3
+
patch/__init__.py ADDED
@@ -0,0 +1,65 @@
1
+ """
2
+ Quality of life patch for the NEURON simulator.
3
+ """
4
+
5
+ try:
6
+ from functools import cache, cached_property
7
+ except ImportError: # pragma: nocover
8
+ import functools
9
+
10
+ def cached_property(f):
11
+ return property(functools.lru_cache()(f))
12
+
13
+ functools.cache = cache = functools.lru_cache()
14
+ functools.cached_property = cached_property
15
+
16
+ from .core import (
17
+ is_density_mechanism,
18
+ is_nrn_scalar,
19
+ is_point_process,
20
+ is_section,
21
+ is_segment,
22
+ transform,
23
+ transform_arc,
24
+ transform_netcon,
25
+ transform_record,
26
+ )
27
+ from .exceptions import NotConnectableError, NotConnectedError
28
+ from .interpreter import PythonHocInterpreter
29
+
30
+ __version__ = "4.0.0"
31
+ p: "PythonHocInterpreter"
32
+ h: "PythonHocInterpreter"
33
+
34
+
35
+ def __getattr__(attr):
36
+ if attr == "p" or attr == "h":
37
+ return _get_interpreter()
38
+ else:
39
+ raise AttributeError(f"module {__name__} has no attribute {attr}.")
40
+
41
+
42
+ @cache
43
+ def _get_interpreter():
44
+ p = PythonHocInterpreter()
45
+ PythonHocInterpreter._process_registration_queue()
46
+ return p
47
+
48
+
49
+ def connection(source, target, strict=True):
50
+ if not hasattr(source, "_connections"):
51
+ raise NotConnectableError(
52
+ f"Source {source} is not connectable. It lacks attribute _connections "
53
+ "required to form NetCons."
54
+ )
55
+ if not hasattr(target, "_connections"):
56
+ raise NotConnectableError(
57
+ f"Target {target} is not connectable. It lacks attribute _connections "
58
+ "required to form NetCons."
59
+ )
60
+ reverse = source in target._connections
61
+ if target not in source._connections:
62
+ if reverse and not strict:
63
+ return target._connections[source]
64
+ raise NotConnectedError("Source is not connected to target.")
65
+ return source._connections[target]
patch/core.py ADDED
@@ -0,0 +1,136 @@
1
+ import typing
2
+ from typing import Union
3
+
4
+ from .exceptions import NotConnectableError
5
+
6
+ if typing.TYPE_CHECKING:
7
+ from neuron.hoc import HocObject
8
+
9
+
10
+ def transform(obj):
11
+ """
12
+ Transform an object to its NEURON representation, if the ``__neuron__`` magic
13
+ method is present.
14
+ """
15
+ if hasattr(obj, "__neuron__"):
16
+ return obj.__neuron__()
17
+ return obj
18
+
19
+
20
+ def transform_netcon(obj):
21
+ """
22
+ Transform an object into the pointer that should be connected, if the ``__netcon__``
23
+ magic method is present.
24
+ """
25
+ if hasattr(obj, "__netcon__"):
26
+ return obj.__netcon__()
27
+ return transform(obj)
28
+
29
+
30
+ def transform_record(obj):
31
+ """
32
+ Transform an object into the pointer that should be recorded, if the ``__record__``
33
+ magic method is present.
34
+ """
35
+ if hasattr(obj, "__record__"):
36
+ return obj.__record__()
37
+ return transform(obj)
38
+
39
+
40
+ def transform_arc(obj, *args, **kwargs):
41
+ """
42
+ Get an arclength object on a NEURON object. Calls the ``__arc__`` magic method
43
+ on the callable object if present, otherwise returns the transformed object.
44
+ """
45
+ if hasattr(obj, "__arc__"):
46
+ return transform(obj(obj.__arc__(*args, **kwargs)))
47
+ else:
48
+ return transform(obj)
49
+
50
+
51
+ def _is_sequence(obj):
52
+ t = type(obj)
53
+ return hasattr(t, "__len__") and hasattr(t, "__getitem__")
54
+
55
+
56
+ def assert_connectable(obj, label=None):
57
+ """
58
+ Assert whether an object could be used as a :class:`~.objects.Connectable`.
59
+
60
+ :param label: Optional label to display to describe the object if the assertion fails.
61
+ :type label: str
62
+ """
63
+ if not hasattr(obj, "_connections"):
64
+ raise NotConnectableError(
65
+ (label if label is not None else str(obj))
66
+ + " is not connectable."
67
+ + "It lacks attribute `_connections` required to form NetCons."
68
+ )
69
+
70
+
71
+ def is_section(obj):
72
+ """
73
+ Check if an object is a section.
74
+ """
75
+ cls = type(transform(obj))
76
+ return cls.__module__ == "nrn" and cls.__name__ == "Section"
77
+
78
+
79
+ def is_segment(obj):
80
+ """
81
+ Check if an object is a segment.
82
+ """
83
+ cls = type(transform(obj))
84
+ return cls.__module__ == "nrn" and cls.__name__ == "Segment"
85
+
86
+
87
+ def is_nrn_scalar(obj):
88
+ cls = type(transform(obj))
89
+ if cls.__module__ != "hoc" or cls.__name__ != "HocObject":
90
+ return False
91
+ try:
92
+ return "pointer to hoc scalar" in str(obj)
93
+ except Exception:
94
+ return False
95
+
96
+
97
+ def is_point_process(obj: Union[str, "HocObject"]):
98
+ """
99
+ Check if obj is a (name of a) PointProcess on the ``HocInterpreter``.
100
+
101
+ :param obj: HocObject or name of the PointProcess to look for. Needs to be a known
102
+ attribute of ``neuron.h``.
103
+ :rtype: bool
104
+ """
105
+ from neuron import h, hoc
106
+
107
+ try:
108
+ if not isinstance(obj, hoc.HocObject):
109
+ obj = getattr(h, obj)
110
+ except Exception:
111
+ return False
112
+ return all(k in dir(obj) for k in ["get_loc", "has_loc", "loc", "get_segment"])
113
+
114
+
115
+ def is_density_mechanism(obj: Union[str, "HocObject"]):
116
+ """
117
+ Check if obj is a (name of a) DensityMechanism on the ``HocInterpreter``.
118
+
119
+ :param obj: HocObject or name of the DensityMechanism to look for. Needs to be a known
120
+ attribute of ``neuron.h``.
121
+ :rtype: bool
122
+ """
123
+ import nrn
124
+ from neuron import h, hoc
125
+
126
+ if isinstance(obj, nrn.Mechanism):
127
+ return True
128
+ try:
129
+ if not isinstance(obj, hoc.HocObject):
130
+ obj = getattr(h, obj)
131
+ hname = str(obj)
132
+ return "neuron.DensityMechanism" in hname
133
+ except TypeError as e:
134
+ return "mechanism" in str(e)
135
+ except Exception as e:
136
+ return False
patch/error_handler.py ADDED
@@ -0,0 +1,188 @@
1
+ import io
2
+ import os
3
+ import sys
4
+ from contextlib import contextmanager
5
+
6
+ from patch.exceptions import (
7
+ ErrorHandlingError,
8
+ HocConnectError,
9
+ HocError,
10
+ HocRecordError,
11
+ HocSectionAccessError,
12
+ PatchError,
13
+ )
14
+
15
+
16
+ @contextmanager
17
+ def _suppress_nrn(stream=None, close=False):
18
+ """
19
+ Makes NEURON (mostly) shut the fuck up.
20
+ """
21
+ if stream is None:
22
+ stream = open(os.devnull, "w")
23
+ close = True
24
+ old_stdout = sys.stdout
25
+ old_stderr = sys.stderr
26
+ sys.stdout = stream
27
+ sys.stderr = stream
28
+ try:
29
+ yield
30
+ finally:
31
+ if close:
32
+ stream.close()
33
+ sys.stdout = old_stdout
34
+ sys.stderr = old_stderr
35
+
36
+
37
+ @contextmanager
38
+ def catch_hoc_error(*args, **context):
39
+ with io.StringIO() as error_stream:
40
+ try:
41
+ with _suppress_nrn(error_stream):
42
+ yield
43
+ except RuntimeError as e:
44
+ error = error_stream.getvalue()
45
+ try:
46
+ for handler in args:
47
+ handler(error, context)
48
+ except Exception as e:
49
+ raise e from None
50
+ # Uncaught HocError
51
+ if "hoc error" in str(e) or "hocobj_call error" in str(e):
52
+ raise HocError(error) from None
53
+ # Actual RuntimeError
54
+ raise e from None # pragma: nocover
55
+
56
+
57
+ class ErrorHandler:
58
+ """
59
+ Base error handler class.
60
+
61
+ The class object is "callable" and takes the arguments that an error handle callable
62
+ should take. This means that when the class is passed in the argument list of
63
+ ``catch_hoc_error`` that we'll construct an object;
64
+
65
+ The constructor checks whether the context contains enough information for us to
66
+ handle the error and calls its ``catch`` method that you need to override. Inside of
67
+ the catch method you can analyze the error message and context and raise a polished
68
+ version of the error. All raised errors must inherit from ``patch.object.PatchError``
69
+ or they'll be treated as a failure of the error handler and an ``ErrorHandlingError``
70
+ will be raised on top of it.
71
+
72
+ To specify the required items of the context create a class attribute list
73
+ ``required``::
74
+
75
+ class A(ErrorHandler):
76
+ required = ["info_i_need_to_operate"]
77
+
78
+ Whenever the error handler class ``A`` is used, the ``catch_hoc_error`` call will
79
+ have the specify the keyword argument ``info_i_need_to_operate``.
80
+ """
81
+
82
+ def __init__(self, error, context):
83
+ if not hasattr(self.__class__, "required"):
84
+ raise ErrorHandlingError(
85
+ "Error context requirements missing for {}".format(
86
+ self.__class__.__name__
87
+ )
88
+ )
89
+ for required_item in self.__class__.required:
90
+ if required_item not in context:
91
+ raise ErrorHandlingError(
92
+ "Required error context item '{}' for {} is missing".format(
93
+ required_item, self.__class__.__name__
94
+ )
95
+ )
96
+ self.__dict__.update(context)
97
+ try:
98
+ self.catch(error, context)
99
+ except PatchError:
100
+ raise
101
+ except Exception as e:
102
+ raise ErrorHandlingError(
103
+ f"{self.__class__.__name__} errored out during error handling."
104
+ )
105
+
106
+ def catch(self, error, context):
107
+ raise ErrorHandlingError(
108
+ "Catch method not implemented for {}".format(self.__class__.__name__)
109
+ )
110
+
111
+
112
+ def detector(error):
113
+ """
114
+ Pass this the error message and it returns a lambda function that you can pass a
115
+ string. Returns ``True`` if the string occurs in the error message, ``False``
116
+ otherwise.
117
+ """
118
+ return lambda trigger: error.lower().find(trigger) != -1
119
+
120
+
121
+ class CatchNetCon(ErrorHandler):
122
+ """
123
+ Catches a variety of errors that can occur when using ``h.NetCon`` and raises
124
+ ``HocConnectError``.
125
+ """
126
+
127
+ required = ["nrn_source", "nrn_target"]
128
+
129
+ def catch(self, error, context):
130
+ e = detector(error)
131
+ if e("must be a point process or nullobject"):
132
+ if e("arg 1"):
133
+ raise HocConnectError(
134
+ "Source is not a point process. Transformed type: '{}'".format(
135
+ type(self.nrn_source)
136
+ )
137
+ )
138
+ if e("arg 2"):
139
+ raise HocConnectError(
140
+ "Target is not a point process. Transformed type: '{}'".format(
141
+ type(self.nrn_target)
142
+ )
143
+ )
144
+ if e("interpreter stack type error"):
145
+ raise HocConnectError(
146
+ "Incorrect types passed to NetCon. Source: {}, target: {}".format(
147
+ type(self.nrn_source), type(self.nrn_target)
148
+ )
149
+ )
150
+
151
+
152
+ # Section access errors can't be triggered without NEURON exiting:
153
+ # https://github.com/neuronsimulator/nrn/issues/769
154
+ class CatchSectionAccess(ErrorHandler): # pragma: nocover
155
+ """
156
+ Catches errors that occur when the Section stack is empty and accessed, raises
157
+ ``HocSectionAccessError``.
158
+ """
159
+
160
+ required = []
161
+
162
+ def catch(self, error, context):
163
+ e = detector(error)
164
+ if e("Section access unspecified"):
165
+ raise HocSectionAccessError(
166
+ "This operation requires a Section on the stack or perhaps a `sec` keyword argument."
167
+ )
168
+
169
+
170
+ class CatchRecord(ErrorHandler):
171
+ """
172
+ Catches a variety of errors that occur when using ``h.Vector().record``, raises
173
+ ``HocRecordError``.
174
+ """
175
+
176
+ required = ["target"]
177
+
178
+ def catch(self, error, context):
179
+ e = detector(error)
180
+ if e("first arg is not a point_process") or e("interpreter stack type error"):
181
+ raise HocRecordError(
182
+ f"Can't record {self.target}, its record pointer is not a point process."
183
+ )
184
+ # Encountered this error locally, most likely on NEURON 7.7, don't cover
185
+ if e("number was provided instead of a pointer"): # pragma: nocover
186
+ raise HocRecordError(
187
+ f"Can't record {self.target}, its record pointer is a value. Make sure that you're recording `y._ref_x` rather than `y.x`."
188
+ )
patch/exceptions.py ADDED
@@ -0,0 +1,38 @@
1
+ from typing import Type
2
+
3
+ from errr.tree import exception as _e
4
+ from errr.tree import make_tree as _make_tree
5
+
6
+ _make_tree(
7
+ globals(),
8
+ PatchError=_e(
9
+ NotConnectableError=_e(),
10
+ NotConnectedError=_e(),
11
+ TransformError=_e(),
12
+ HocError=_e(
13
+ HocConnectError=_e(), HocRecordError=_e(), HocSectionAccessError=_e()
14
+ ),
15
+ SimulationError=_e(),
16
+ UninitializedError=_e(),
17
+ ErrorHandlingError=_e(),
18
+ ParallelError=_e(
19
+ ParallelConnectError=_e(),
20
+ BroadcastError=_e(),
21
+ ),
22
+ ),
23
+ )
24
+
25
+ PatchError: Type[Exception]
26
+ NotConnectableError: Type[PatchError]
27
+ NotConnectedError: Type[PatchError]
28
+ TransformError: Type[PatchError]
29
+ HocError: Type[PatchError]
30
+ HocConnectError: Type[HocError]
31
+ HocRecordError: Type[HocError]
32
+ HocSectionAccessError: Type[HocError]
33
+ SimulationError: Type[PatchError]
34
+ UninitializedError: Type[PatchError]
35
+ ErrorHandlingError: Type[PatchError]
36
+ ParallelError: Type[PatchError]
37
+ ParallelConnectError: Type[ParallelError]
38
+ BroadcastError: Type[ParallelError]
@@ -0,0 +1,9 @@
1
+ from pathlib import Path
2
+
3
+ from glia import Mod, Package
4
+
5
+ package = Package(
6
+ "patch_extensions",
7
+ Path(__file__).resolve().parent,
8
+ mods=[Mod("mod/VecStim.mod", "VecStim", variant="0", is_artificial_cell=True)],
9
+ )