myokit 1.37.5__py3-none-any.whl → 1.39.0__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.
- myokit/__init__.py +5 -0
- myokit/_datablock.py +6 -5
- myokit/_expressions.py +6 -1
- myokit/_model_api.py +44 -18
- myokit/_myokit_version.py +1 -1
- myokit/_parsing.py +8 -2
- myokit/_sim/cvodessim.py +26 -0
- myokit/formats/__init__.py +37 -0
- myokit/formats/ansic/_ewriter.py +1 -1
- myokit/formats/axon/_abf.py +43 -9
- myokit/formats/cellml/v1/__init__.py +5 -5
- myokit/formats/cellml/v1/_api.py +220 -122
- myokit/formats/cellml/v1/_parser.py +91 -87
- myokit/formats/cellml/v1/_writer.py +13 -6
- myokit/formats/cellml/v2/__init__.py +5 -8
- myokit/formats/cellml/v2/_api.py +182 -106
- myokit/formats/cellml/v2/_parser.py +68 -64
- myokit/formats/cellml/v2/_writer.py +7 -3
- myokit/formats/heka/_patchmaster.py +71 -14
- myokit/formats/mathml/_parser.py +106 -67
- myokit/gui/source.py +18 -12
- myokit/lib/hh.py +21 -37
- myokit/tests/test_cellml_v1_api.py +227 -33
- myokit/tests/test_cellml_v1_parser.py +48 -17
- myokit/tests/test_cellml_v1_writer.py +14 -4
- myokit/tests/test_cellml_v2_api.py +132 -114
- myokit/tests/test_cellml_v2_parser.py +31 -1
- myokit/tests/test_cellml_v2_writer.py +8 -1
- myokit/tests/test_datalog.py +17 -0
- myokit/tests/test_expressions.py +61 -0
- myokit/tests/test_formats.py +99 -0
- myokit/tests/test_formats_mathml_content.py +97 -37
- myokit/tests/test_formats_python.py +1 -1
- myokit/tests/test_model_building.py +2 -0
- myokit/tests/test_parsing.py +32 -0
- myokit/tests/test_simulation_cvodes.py +10 -4
- myokit/tests/test_variable.py +10 -7
- {myokit-1.37.5.dist-info → myokit-1.39.0.dist-info}/METADATA +22 -7
- {myokit-1.37.5.dist-info → myokit-1.39.0.dist-info}/RECORD +43 -43
- {myokit-1.37.5.dist-info → myokit-1.39.0.dist-info}/WHEEL +1 -1
- {myokit-1.37.5.dist-info → myokit-1.39.0.dist-info}/entry_points.txt +0 -0
- {myokit-1.37.5.dist-info → myokit-1.39.0.dist-info/licenses}/LICENSE.txt +0 -0
- {myokit-1.37.5.dist-info → myokit-1.39.0.dist-info}/top_level.txt +0 -0
myokit/formats/mathml/_parser.py
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
#
|
|
7
7
|
import myokit
|
|
8
8
|
|
|
9
|
+
from myokit.formats import is_integer_string, is_real_number_string
|
|
9
10
|
from myokit.formats.xml import split
|
|
10
11
|
|
|
11
12
|
|
|
@@ -145,6 +146,13 @@ class MathMLParser:
|
|
|
145
146
|
``<rem>``
|
|
146
147
|
Becomes a :class:`myokit.Remainder`.
|
|
147
148
|
|
|
149
|
+
Minimum and maximum
|
|
150
|
+
|
|
151
|
+
``<max>`` and ``<min>``
|
|
152
|
+
Are converted to if-statements. Only binary comparisons are supported,
|
|
153
|
+
and the resulting ``if`` will evaluate its operands twice, which may be
|
|
154
|
+
inefficient if either operand is a complex expression.
|
|
155
|
+
|
|
148
156
|
Trigonometry
|
|
149
157
|
|
|
150
158
|
``<sin>``, ``<cos>`` and ``<tan>``
|
|
@@ -415,6 +423,12 @@ class MathMLParser:
|
|
|
415
423
|
elif name == 'leq':
|
|
416
424
|
return myokit.LessEqual(*self._eat(element, iterator, 2))
|
|
417
425
|
|
|
426
|
+
# Min and max
|
|
427
|
+
elif name == 'min':
|
|
428
|
+
return self._parse_minmax(element, iterator, myokit.Less)
|
|
429
|
+
elif name == 'max':
|
|
430
|
+
return self._parse_minmax(element, iterator, myokit.More)
|
|
431
|
+
|
|
418
432
|
# Trigonometry
|
|
419
433
|
elif name == 'sin':
|
|
420
434
|
return myokit.Sin(*self._eat(element, iterator))
|
|
@@ -655,6 +669,41 @@ class MathMLParser:
|
|
|
655
669
|
else:
|
|
656
670
|
return myokit.Log(op, base)
|
|
657
671
|
|
|
672
|
+
def _parse_minmax(self, element, iterator, operator):
|
|
673
|
+
"""
|
|
674
|
+
Parses a ``min`` or ``max`` operation, replacing it with one or more
|
|
675
|
+
:class:`myokit.If` functions.
|
|
676
|
+
|
|
677
|
+
Caveats: Only binary min & max are supported, and if either operand is
|
|
678
|
+
a complex expression if will evaluated twice.
|
|
679
|
+
|
|
680
|
+
Arguments:
|
|
681
|
+
|
|
682
|
+
``element``
|
|
683
|
+
The element that determined the operator type.
|
|
684
|
+
``iterator``
|
|
685
|
+
An iterator pointing at the first element after ``element``.
|
|
686
|
+
``operator``
|
|
687
|
+
Either :class:`myokit.Less` or :class:`myokit.More`, depending on
|
|
688
|
+
the desired operation.
|
|
689
|
+
|
|
690
|
+
"""
|
|
691
|
+
# Get all operands
|
|
692
|
+
ops = [self._parse_atomic(x) for x in iterator]
|
|
693
|
+
|
|
694
|
+
# Check the number of operands
|
|
695
|
+
n = len(ops)
|
|
696
|
+
if n != 2:
|
|
697
|
+
raise MathMLError(
|
|
698
|
+
'Only binary min() and max() operations are supported.',
|
|
699
|
+
element)
|
|
700
|
+
|
|
701
|
+
# Return chain of ifs
|
|
702
|
+
e = ops[0]
|
|
703
|
+
for i in range(1, n):
|
|
704
|
+
e = myokit.If(operator(e, ops[i]), e, ops[i])
|
|
705
|
+
return e
|
|
706
|
+
|
|
658
707
|
def _parse_nary(self, element, iterator, binary, unary=None):
|
|
659
708
|
"""
|
|
660
709
|
Parses operands for unary, binary, or n-ary operators (for example
|
|
@@ -706,62 +755,51 @@ class MathMLParser:
|
|
|
706
755
|
|
|
707
756
|
# Get value
|
|
708
757
|
if kind == 'real':
|
|
709
|
-
# Float, specified as 123.123 (no exponent!)
|
|
710
|
-
#
|
|
711
|
-
base = element.attrib.get('base', '10')
|
|
712
|
-
|
|
713
|
-
base
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
'Invalid base specified on <ci> element.', element)
|
|
758
|
+
# Float, specified as 123.123 (no exponent!). May be in a different
|
|
759
|
+
# base than 10.
|
|
760
|
+
base = element.attrib.get('base', '10')
|
|
761
|
+
if not is_integer_string(base, True):
|
|
762
|
+
raise MathMLError('Invalid base specified on <ci> element:'
|
|
763
|
+
f' {base}.', element)
|
|
764
|
+
base = int(base)
|
|
717
765
|
if base != 10:
|
|
718
|
-
raise MathMLError(
|
|
719
|
-
|
|
720
|
-
element)
|
|
766
|
+
raise MathMLError('Real numbers in bases other than 10 are not'
|
|
767
|
+
' supported.', element)
|
|
721
768
|
|
|
722
769
|
# Get value
|
|
723
|
-
# Note: We are being tolerant here and allowing e-notation
|
|
724
|
-
# is not consistent with the spec
|
|
770
|
+
# Note: We are being tolerant here and allowing e-notation - which
|
|
771
|
+
# is not consistent with the spec.
|
|
725
772
|
if element.text is None:
|
|
726
773
|
raise MathMLError('Empty <cn> element', element)
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
'Unable to convert contents of <cn> to a real number: "'
|
|
732
|
-
+ str(element.text) + '"', element)
|
|
774
|
+
if not is_real_number_string(element.text, True):
|
|
775
|
+
raise MathMLError('Unable to convert contents of <cn> to a'
|
|
776
|
+
f' real number: "{element.text}".', element)
|
|
777
|
+
value = float(element.text)
|
|
733
778
|
|
|
734
779
|
elif kind == 'integer':
|
|
735
780
|
# Integer in any given base
|
|
736
781
|
base = element.attrib.get('base', '10').strip()
|
|
737
|
-
|
|
738
|
-
base
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
'Unable to parse base of <cn> element: "' + base + '"',
|
|
742
|
-
element)
|
|
782
|
+
if not is_integer_string(base, True):
|
|
783
|
+
raise MathMLError('Invalid base specified on <ci> element:'
|
|
784
|
+
f' {base}.', element)
|
|
785
|
+
base = int(base)
|
|
743
786
|
|
|
744
787
|
# Get value
|
|
745
788
|
if element.text is None:
|
|
746
789
|
raise MathMLError('Empty <cn> element', element)
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
'Unable to convert contents of <cn> to an integer: "'
|
|
752
|
-
+ str(element.text) + '"', element)
|
|
790
|
+
if not is_integer_string(element.text, True):
|
|
791
|
+
raise MathMLError('Unable to convert contents of <cn> to an'
|
|
792
|
+
f' integer: "{element.text}".', element)
|
|
793
|
+
value = int(element.text, base)
|
|
753
794
|
|
|
754
795
|
elif kind == 'double':
|
|
755
796
|
# Floating point (positive, negative, exponents, etc)
|
|
756
|
-
|
|
757
797
|
if element.text is None:
|
|
758
798
|
raise MathMLError('Empty <cn> element', element)
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
'Unable to convert contents of <cn> to a real number: "'
|
|
764
|
-
+ str(element.text) + '"', element)
|
|
799
|
+
if not is_real_number_string(element.text, True):
|
|
800
|
+
raise MathMLError('Unable to convert contents of <cn> to a'
|
|
801
|
+
f' real number: "{element.text}".', element)
|
|
802
|
+
value = float(element.text)
|
|
765
803
|
|
|
766
804
|
elif kind == 'e-notation':
|
|
767
805
|
# 1<sep />3 = 1e3
|
|
@@ -769,61 +807,62 @@ class MathMLParser:
|
|
|
769
807
|
# Check contents
|
|
770
808
|
parts = [x for x in element]
|
|
771
809
|
if len(parts) != 1 or split(parts[0].tag)[1] != 'sep':
|
|
772
|
-
raise MathMLError(
|
|
773
|
-
|
|
774
|
-
' number<sep />number.', element)
|
|
810
|
+
raise MathMLError('Number in e-notation should have the format'
|
|
811
|
+
' number<sep />number.', element)
|
|
775
812
|
|
|
776
813
|
# Get parts of number
|
|
777
814
|
sig = element.text
|
|
778
815
|
exp = parts[0].tail
|
|
779
|
-
if sig is None
|
|
816
|
+
if sig is None:
|
|
780
817
|
raise MathMLError(
|
|
781
818
|
'Unable to parse number in e-notation: missing part before'
|
|
782
819
|
' the separator.', element)
|
|
783
|
-
if exp is None
|
|
820
|
+
if exp is None:
|
|
784
821
|
raise MathMLError(
|
|
785
822
|
'Unable to parse number in e-notation: missing part after'
|
|
786
823
|
' the separator.', element)
|
|
787
824
|
|
|
788
|
-
|
|
825
|
+
sig, exp = sig.strip(), exp.strip()
|
|
826
|
+
if not is_integer_string(exp):
|
|
827
|
+
raise MathMLError(
|
|
828
|
+
'Unable to parse number in e-notation: part after the'
|
|
829
|
+
' separator should be an integer.', element)
|
|
830
|
+
# For sig, we can't allow e.g. 1e3, so different strategy
|
|
789
831
|
try:
|
|
790
|
-
value = float(sig
|
|
832
|
+
value = float(f'{sig}e{exp}')
|
|
791
833
|
except ValueError:
|
|
792
834
|
raise MathMLError(
|
|
793
|
-
'Unable to parse number in e-notation
|
|
794
|
-
|
|
835
|
+
'Unable to parse number in e-notation: part before the'
|
|
836
|
+
' separator should be a basic real number.', element)
|
|
795
837
|
|
|
796
838
|
elif kind == 'rational':
|
|
797
839
|
# 1<sep />3 = 1 / 3
|
|
798
840
|
# Check contents
|
|
799
841
|
parts = [x for x in element]
|
|
800
842
|
if len(parts) != 1 or split(parts[0].tag)[1] != 'sep':
|
|
801
|
-
raise MathMLError(
|
|
802
|
-
|
|
803
|
-
' number<sep />number.', element)
|
|
843
|
+
raise MathMLError('Rational number should have the format'
|
|
844
|
+
' number<sep />number.', element)
|
|
804
845
|
|
|
805
846
|
# Get parts of number
|
|
806
847
|
numer = element.text
|
|
807
848
|
denom = parts[0].tail
|
|
808
|
-
if numer is None
|
|
809
|
-
raise MathMLError(
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
'Unable to parse rational number "' + numer + ' / ' + denom
|
|
823
|
-
+ '".', element)
|
|
849
|
+
if numer is None:
|
|
850
|
+
raise MathMLError('Unable to parse rational number: missing'
|
|
851
|
+
' part before the separator.', element)
|
|
852
|
+
if denom is None:
|
|
853
|
+
raise MathMLError('Unable to parse rational number: missing'
|
|
854
|
+
' part after the separator.', element)
|
|
855
|
+
|
|
856
|
+
if not is_integer_string(numer, True):
|
|
857
|
+
raise MathMLError('Unable to parse rational : part before the'
|
|
858
|
+
' separator should be an integer.', element)
|
|
859
|
+
if not is_integer_string(denom, True):
|
|
860
|
+
raise MathMLError('Unable to parse rational : part after the'
|
|
861
|
+
' separator should be an integer.', element)
|
|
862
|
+
value = int(numer) / int(denom)
|
|
824
863
|
|
|
825
864
|
else:
|
|
826
|
-
raise MathMLError('Unsupported <cn> type: '
|
|
865
|
+
raise MathMLError(f'Unsupported <cn> type: {kind}', element)
|
|
827
866
|
|
|
828
867
|
# Create number and return
|
|
829
868
|
return self._nfac(value, element)
|
myokit/gui/source.py
CHANGED
|
@@ -51,6 +51,14 @@ COLOR_BRACKET = QtGui.QColor(240, 100, 0)
|
|
|
51
51
|
# Selected line is highlighted
|
|
52
52
|
COLOR_SELECTED_LINE = QtGui.QColor(238, 238, 238)
|
|
53
53
|
|
|
54
|
+
# Real number regex
|
|
55
|
+
# Note 1: The first part is a "lookbehind", that stops it matching just after a
|
|
56
|
+
# word character, so in "a1" it won't match, and in "a.1" it won't start
|
|
57
|
+
# matching until the "1" because "." is not a word character.
|
|
58
|
+
# Note 2: The remainder is the same as in myokit.float
|
|
59
|
+
# Note 3: Deliberately unsigned
|
|
60
|
+
LITERAL = rf'(?<!\w){myokit._RE_UNSIGNED_REAL}'
|
|
61
|
+
|
|
54
62
|
|
|
55
63
|
def _adapt_for_dark_mode(palette):
|
|
56
64
|
"""
|
|
@@ -991,24 +999,24 @@ class ModelHighlighter(QtGui.QSyntaxHighlighter):
|
|
|
991
999
|
self._rules = []
|
|
992
1000
|
|
|
993
1001
|
# Numbers
|
|
994
|
-
|
|
995
|
-
self._rules.append((pattern, STYLE_LITERAL))
|
|
1002
|
+
self._rules.append((R(LITERAL), STYLE_LITERAL))
|
|
996
1003
|
unit = r'\[([a-zA-Z0-9/^-]|\*)+\]'
|
|
997
1004
|
self._rules.append((R(unit), STYLE_INLINE_UNIT))
|
|
998
1005
|
|
|
999
1006
|
# Keywords
|
|
1007
|
+
# Note: \b is a "word boundary" match
|
|
1000
1008
|
for keyword in self.KEYWORD_1:
|
|
1001
|
-
self._rules.append((R(
|
|
1009
|
+
self._rules.append((R(rf'\b{keyword}\b'), STYLE_KEYWORD_1))
|
|
1002
1010
|
for keyword in self.KEYWORD_2:
|
|
1003
|
-
self._rules.append((R(
|
|
1011
|
+
self._rules.append((R(rf'\b{keyword}\b'), STYLE_KEYWORD_2))
|
|
1004
1012
|
|
|
1005
1013
|
# Meta-data coloring
|
|
1006
1014
|
self._rules_labels = [
|
|
1007
|
-
R(
|
|
1008
|
-
R(
|
|
1015
|
+
R(rf'(\s*)(bind)\s+({name})'),
|
|
1016
|
+
R(rf'(\s*)(label)\s+({name})'),
|
|
1009
1017
|
]
|
|
1010
|
-
self._rule_meta = R(
|
|
1011
|
-
self._rule_var_unit = R(
|
|
1018
|
+
self._rule_meta = R(rf'^\s*({name}:)(\s*)(.+)')
|
|
1019
|
+
self._rule_var_unit = R(rf'^(\s*)(in)(\s*)({unit})')
|
|
1012
1020
|
|
|
1013
1021
|
# Comment
|
|
1014
1022
|
self._comment = R(r'#')
|
|
@@ -1141,8 +1149,7 @@ class ProtocolHighlighter(QtGui.QSyntaxHighlighter):
|
|
|
1141
1149
|
self._rules = []
|
|
1142
1150
|
|
|
1143
1151
|
# Numbers
|
|
1144
|
-
self._rules.append(
|
|
1145
|
-
(R(r'\b[+-]?[0-9]*\.?[0-9]+([eE][+-]?[0-9]+)?\b'), STYLE_LITERAL))
|
|
1152
|
+
self._rules.append((R(LITERAL), STYLE_LITERAL))
|
|
1146
1153
|
|
|
1147
1154
|
# Keyword "next"
|
|
1148
1155
|
self._rules.append((R(r'\bnext\b'), STYLE_KEYWORD_1))
|
|
@@ -1192,8 +1199,7 @@ class ScriptHighlighter(QtGui.QSyntaxHighlighter):
|
|
|
1192
1199
|
|
|
1193
1200
|
# Literals: numbers, True, False, None
|
|
1194
1201
|
# Override some keywords
|
|
1195
|
-
self._rules.append((R(
|
|
1196
|
-
STYLE_LITERAL))
|
|
1202
|
+
self._rules.append((R(LITERAL), STYLE_LITERAL))
|
|
1197
1203
|
self._rules.append((R(r'\bTrue\b'), STYLE_LITERAL))
|
|
1198
1204
|
self._rules.append((R(r'\bFalse\b'), STYLE_LITERAL))
|
|
1199
1205
|
self._rules.append((R(r'\bNone\b'), STYLE_LITERAL))
|
myokit/lib/hh.py
CHANGED
|
@@ -123,13 +123,12 @@ class HHModel:
|
|
|
123
123
|
try:
|
|
124
124
|
state = self._model.get(str(state), myokit.Variable)
|
|
125
125
|
except KeyError:
|
|
126
|
-
raise HHModelError('Unknown state: <
|
|
126
|
+
raise HHModelError(f'Unknown state: <{state}>.')
|
|
127
127
|
if not state.is_state():
|
|
128
128
|
raise HHModelError(
|
|
129
|
-
'Variable <
|
|
129
|
+
f'Variable <{state.qname()}> is not a state.')
|
|
130
130
|
if state in self._states:
|
|
131
|
-
raise HHModelError(
|
|
132
|
-
'State <' + state.qname() + '> was added twice.')
|
|
131
|
+
raise HHModelError(f'State <{state.qname()}> was added twice.')
|
|
133
132
|
self._states.append(state)
|
|
134
133
|
del states
|
|
135
134
|
|
|
@@ -144,14 +143,11 @@ class HHModel:
|
|
|
144
143
|
try:
|
|
145
144
|
parameter = self._model.get(parameter, myokit.Variable)
|
|
146
145
|
except KeyError:
|
|
147
|
-
raise HHModelError(
|
|
148
|
-
'Unknown parameter: <' + str(parameter) + '>.')
|
|
146
|
+
raise HHModelError(f'Unknown parameter: <{parameter}>.')
|
|
149
147
|
if not parameter.is_literal():
|
|
150
|
-
raise HHModelError(
|
|
151
|
-
'Unsuitable parameter: <' + str(parameter) + '>.')
|
|
148
|
+
raise HHModelError(f'Unsuitable parameter: <{parameter}>.')
|
|
152
149
|
if parameter in unique:
|
|
153
|
-
raise HHModelError(
|
|
154
|
-
'Parameter listed twice: <' + str(parameter) + '>.')
|
|
150
|
+
raise HHModelError(f'Parameter listed twice: <{parameter}>.')
|
|
155
151
|
unique.add(parameter)
|
|
156
152
|
self._parameters.append(parameter)
|
|
157
153
|
del unique
|
|
@@ -217,7 +213,7 @@ class HHModel:
|
|
|
217
213
|
for state in self._states:
|
|
218
214
|
if not has_inf_tau_form(state, self._membrane_potential):
|
|
219
215
|
raise HHModelError(
|
|
220
|
-
'State `
|
|
216
|
+
f'State `{state.qname()}` must have "inf-tau form" or'
|
|
221
217
|
' "alpha-beta form". See'
|
|
222
218
|
' `myokit.lib.hh.has_inf_tau_form()` and'
|
|
223
219
|
' `myokit.lib.hh.has_alpha_beta_form()`.'
|
|
@@ -294,13 +290,9 @@ class HHModel:
|
|
|
294
290
|
inf = w.ex(myokit.Name(inf))
|
|
295
291
|
tau = w.ex(myokit.Name(tau))
|
|
296
292
|
k = str(k)
|
|
297
|
-
f.append(
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
+ ') * numpy.exp(-_t / ' + tau + ')')
|
|
301
|
-
f.append(
|
|
302
|
-
'_y[' + k + '][_t == 0] = ' + state.uname() + '[_t == 0] = '
|
|
303
|
-
+ '_y0[' + k + ']')
|
|
293
|
+
f.append(f'_y[{k}] = {state.uname()} ='
|
|
294
|
+
f' {inf} + (_y0[{k}] - {inf}) * numpy.exp(-_t / {tau})')
|
|
295
|
+
f.append(f'_y[{k}][_t == 0] = {state.uname()}[_t == 0] = _y0[{k}]')
|
|
304
296
|
|
|
305
297
|
# Add current calculation
|
|
306
298
|
if self._current is not None:
|
|
@@ -313,7 +305,6 @@ class HHModel:
|
|
|
313
305
|
for i in range(1, len(f)):
|
|
314
306
|
f[i] = ' ' + f[i]
|
|
315
307
|
f = '\n'.join(f)
|
|
316
|
-
#print(f)
|
|
317
308
|
local = {}
|
|
318
309
|
exec(f, {'numpy': np}, local)
|
|
319
310
|
self._function = local['_f']
|
|
@@ -331,7 +322,7 @@ class HHModel:
|
|
|
331
322
|
k = str(k)
|
|
332
323
|
inf, tau = get_inf_and_tau(state, self._membrane_potential)
|
|
333
324
|
inf = inf.rhs().clone(expand=True, retain=self._inputs)
|
|
334
|
-
g.append('_y[
|
|
325
|
+
g.append(f'_y[{k}] = {w.ex(inf)}')
|
|
335
326
|
|
|
336
327
|
# Create python function from g
|
|
337
328
|
g.append('return _y')
|
|
@@ -431,8 +422,7 @@ class HHModel:
|
|
|
431
422
|
raise HHModelError(
|
|
432
423
|
'The given component has more than one variable that could'
|
|
433
424
|
' be a current: '
|
|
434
|
-
+ ', '.join(['<
|
|
435
|
-
+ '.')
|
|
425
|
+
+ ', '.join([f'<{x.qname()}>' for x in currents]) + '.')
|
|
436
426
|
try:
|
|
437
427
|
current = currents[0]
|
|
438
428
|
except IndexError:
|
|
@@ -494,9 +484,8 @@ class HHModel:
|
|
|
494
484
|
if parameters is not None:
|
|
495
485
|
if len(parameters) != len(self._parameters):
|
|
496
486
|
raise ValueError(
|
|
497
|
-
'Illegal parameter vector size: '
|
|
498
|
-
|
|
499
|
-
+ str(len(parameters)) + ' provided.')
|
|
487
|
+
f'Illegal parameter vector size: f{len(self._parameters)}'
|
|
488
|
+
f' required, {len(parameters)} provided.')
|
|
500
489
|
inputs[1:] = [float(x) for x in parameters]
|
|
501
490
|
return inputs
|
|
502
491
|
|
|
@@ -764,8 +753,7 @@ class AnalyticalSimulation:
|
|
|
764
753
|
log[key] = np.concatenate((
|
|
765
754
|
log[key], np.zeros(log_times.shape)))
|
|
766
755
|
except KeyError:
|
|
767
|
-
raise ValueError(
|
|
768
|
-
'Invalid log: missing entry for <' + str(key) + '>.')
|
|
756
|
+
raise ValueError(f'Invalid log: missing entry for <{key}>.')
|
|
769
757
|
|
|
770
758
|
# Run simulation
|
|
771
759
|
if self._protocol is None:
|
|
@@ -855,8 +843,7 @@ class AnalyticalSimulation:
|
|
|
855
843
|
'Wrong size state vector, expecing (' + str(len(self._state))
|
|
856
844
|
+ ') values.')
|
|
857
845
|
if np.any(state < 0) or np.any(state > 1):
|
|
858
|
-
raise ValueError(
|
|
859
|
-
'All states must be in the range [0, 1].')
|
|
846
|
+
raise ValueError('All states must be in the range [0, 1].')
|
|
860
847
|
self._default_state = state
|
|
861
848
|
|
|
862
849
|
def set_membrane_potential(self, v):
|
|
@@ -873,9 +860,8 @@ class AnalyticalSimulation:
|
|
|
873
860
|
Changes the parameter values used in this simulation.
|
|
874
861
|
"""
|
|
875
862
|
if len(parameters) != len(self._parameters):
|
|
876
|
-
raise ValueError(
|
|
877
|
-
|
|
878
|
-
+ str(len(self._parameters)) + ') values.')
|
|
863
|
+
raise ValueError('Wrong size parameter vector, expecting'
|
|
864
|
+
f' ({len(self._parameters)}) values.')
|
|
879
865
|
self._parameters = np.array(parameters, copy=True, dtype=float)
|
|
880
866
|
|
|
881
867
|
def set_state(self, state):
|
|
@@ -884,12 +870,10 @@ class AnalyticalSimulation:
|
|
|
884
870
|
"""
|
|
885
871
|
state = np.array(state, copy=True, dtype=float)
|
|
886
872
|
if len(state) != len(self._state):
|
|
887
|
-
raise ValueError(
|
|
888
|
-
|
|
889
|
-
+ ') values.')
|
|
873
|
+
raise ValueError('Wrong size state vector, expecting'
|
|
874
|
+
f' ({len(self._state)}) values.')
|
|
890
875
|
if np.any(state < 0) or np.any(state > 1):
|
|
891
|
-
raise ValueError(
|
|
892
|
-
'All states must be in the range [0, 1].')
|
|
876
|
+
raise ValueError('All states must be in the range [0, 1].')
|
|
893
877
|
self._state = state
|
|
894
878
|
|
|
895
879
|
def solve(self, times):
|