pathsim 0.2.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.
Files changed (109) hide show
  1. pathsim/__init__.py +3 -0
  2. pathsim/blocks/__init__.py +14 -0
  3. pathsim/blocks/_block.py +209 -0
  4. pathsim/blocks/adder.py +30 -0
  5. pathsim/blocks/amplifier.py +34 -0
  6. pathsim/blocks/delay.py +70 -0
  7. pathsim/blocks/differentiator.py +70 -0
  8. pathsim/blocks/function.py +82 -0
  9. pathsim/blocks/integrator.py +66 -0
  10. pathsim/blocks/lti.py +155 -0
  11. pathsim/blocks/multiplier.py +30 -0
  12. pathsim/blocks/ode.py +86 -0
  13. pathsim/blocks/rf/__init__.py +4 -0
  14. pathsim/blocks/rf/filters.py +169 -0
  15. pathsim/blocks/rf/noise.py +218 -0
  16. pathsim/blocks/rf/sources.py +163 -0
  17. pathsim/blocks/rf/wienerhammerstein.py +338 -0
  18. pathsim/blocks/rng.py +57 -0
  19. pathsim/blocks/scope.py +224 -0
  20. pathsim/blocks/sources.py +71 -0
  21. pathsim/blocks/spectrum.py +316 -0
  22. pathsim/connection.py +112 -0
  23. pathsim/simulation.py +652 -0
  24. pathsim/solvers/__init__.py +25 -0
  25. pathsim/solvers/_solver.py +403 -0
  26. pathsim/solvers/bdf.py +240 -0
  27. pathsim/solvers/dirk2.py +101 -0
  28. pathsim/solvers/dirk3.py +86 -0
  29. pathsim/solvers/esdirk32.py +131 -0
  30. pathsim/solvers/esdirk4.py +99 -0
  31. pathsim/solvers/esdirk43.py +139 -0
  32. pathsim/solvers/esdirk54.py +141 -0
  33. pathsim/solvers/esdirk85.py +200 -0
  34. pathsim/solvers/euler.py +81 -0
  35. pathsim/solvers/rk4.py +61 -0
  36. pathsim/solvers/rkbs32.py +101 -0
  37. pathsim/solvers/rkck54.py +108 -0
  38. pathsim/solvers/rkdp54.py +111 -0
  39. pathsim/solvers/rkdp87.py +116 -0
  40. pathsim/solvers/rkf45.py +102 -0
  41. pathsim/solvers/rkf78.py +111 -0
  42. pathsim/solvers/rkv65.py +103 -0
  43. pathsim/solvers/ssprk22.py +62 -0
  44. pathsim/solvers/ssprk33.py +65 -0
  45. pathsim/solvers/ssprk34.py +74 -0
  46. pathsim/subsystem.py +267 -0
  47. pathsim/utils/__init__.py +0 -0
  48. pathsim/utils/adaptivebuffer.py +87 -0
  49. pathsim/utils/anderson.py +180 -0
  50. pathsim/utils/funcs.py +205 -0
  51. pathsim/utils/gilbert.py +110 -0
  52. pathsim/utils/progresstracker.py +90 -0
  53. pathsim/utils/realtimeplotter.py +230 -0
  54. pathsim/utils/statespacerealizations.py +116 -0
  55. pathsim/utils/waveforms.py +36 -0
  56. pathsim-0.2.0.dist-info/LICENSE.txt +21 -0
  57. pathsim-0.2.0.dist-info/METADATA +149 -0
  58. pathsim-0.2.0.dist-info/RECORD +109 -0
  59. pathsim-0.2.0.dist-info/WHEEL +5 -0
  60. pathsim-0.2.0.dist-info/top_level.txt +2 -0
  61. tests/__init__.py +0 -0
  62. tests/blocks/__init__.py +0 -0
  63. tests/blocks/test_adder.py +85 -0
  64. tests/blocks/test_amplifier.py +66 -0
  65. tests/blocks/test_block.py +138 -0
  66. tests/blocks/test_delay.py +122 -0
  67. tests/blocks/test_differentiator.py +102 -0
  68. tests/blocks/test_function.py +165 -0
  69. tests/blocks/test_integrator.py +92 -0
  70. tests/blocks/test_lti.py +162 -0
  71. tests/blocks/test_multiplier.py +87 -0
  72. tests/blocks/test_ode.py +125 -0
  73. tests/blocks/test_rng.py +109 -0
  74. tests/blocks/test_scope.py +196 -0
  75. tests/blocks/test_sources.py +119 -0
  76. tests/blocks/test_spectrum.py +119 -0
  77. tests/solvers/__init__.py +0 -0
  78. tests/solvers/test_bdf.py +364 -0
  79. tests/solvers/test_dirk2.py +138 -0
  80. tests/solvers/test_dirk3.py +137 -0
  81. tests/solvers/test_esdirk32.py +158 -0
  82. tests/solvers/test_esdirk4.py +138 -0
  83. tests/solvers/test_esdirk43.py +158 -0
  84. tests/solvers/test_esdirk54.py +160 -0
  85. tests/solvers/test_esdirk85.py +157 -0
  86. tests/solvers/test_euler.py +223 -0
  87. tests/solvers/test_rk4.py +138 -0
  88. tests/solvers/test_rkbs32.py +159 -0
  89. tests/solvers/test_rkck54.py +157 -0
  90. tests/solvers/test_rkdp54.py +159 -0
  91. tests/solvers/test_rkdp87.py +157 -0
  92. tests/solvers/test_rkf45.py +159 -0
  93. tests/solvers/test_rkf78.py +160 -0
  94. tests/solvers/test_rkv65.py +160 -0
  95. tests/solvers/test_solver.py +119 -0
  96. tests/solvers/test_ssprk22.py +136 -0
  97. tests/solvers/test_ssprk33.py +136 -0
  98. tests/solvers/test_ssprk34.py +136 -0
  99. tests/test_connection.py +176 -0
  100. tests/test_simulation.py +271 -0
  101. tests/test_subsystem.py +182 -0
  102. tests/utils/__init__.py +0 -0
  103. tests/utils/test_adaptivebuffer.py +111 -0
  104. tests/utils/test_anderson.py +142 -0
  105. tests/utils/test_funcs.py +143 -0
  106. tests/utils/test_gilbert.py +108 -0
  107. tests/utils/test_progresstracker.py +144 -0
  108. tests/utils/test_realtimeplotter.py +122 -0
  109. tests/utils/test_statespacerealizations.py +107 -0
pathsim/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .simulation import Simulation
2
+ from .connection import Connection
3
+ from .subsystem import Subsystem, Interface
@@ -0,0 +1,14 @@
1
+ from .differentiator import *
2
+ from .integrator import *
3
+ from .amplifier import *
4
+ from .function import *
5
+ from .spectrum import *
6
+ from .sources import *
7
+ from .multiplier import *
8
+ from .adder import*
9
+ from .scope import *
10
+ from .delay import *
11
+ from .lti import *
12
+ from .ode import *
13
+ from .rng import *
14
+
@@ -0,0 +1,209 @@
1
+ #########################################################################################
2
+ ##
3
+ ## BASE BLOCK
4
+ ## (blocks/_block.py)
5
+ ##
6
+ ## This module defines the base 'Block' class that is the parent
7
+ ## to all other blocks and can serve as a base for new or custom blocks
8
+ ##
9
+ ##
10
+ ## Milan Rother 2024
11
+ ##
12
+ #########################################################################################
13
+
14
+ # IMPORTS ===============================================================================
15
+
16
+ #for unique identifiers of blocks
17
+ import uuid
18
+
19
+
20
+ # BASE BLOCK CLASS ======================================================================
21
+
22
+ class Block:
23
+ """
24
+ Base 'Block' object that defines the inputs, outputs and the connect method.
25
+
26
+ Block interconnections are handeled via the io interface of the blocks.
27
+ It is realized by dicts for the 'inputs' and for the 'outputs', where
28
+ the key of the dict is the input/output channel and the corresponding
29
+ value is the input/output value.
30
+
31
+ NOTE :
32
+ This block is not intended to be used directly and serves as a base
33
+ class definition for other blocks to be inherited.
34
+ """
35
+
36
+ def __init__(self):
37
+
38
+ #dicts to hold input and output values
39
+ self.inputs = {0:0.0}
40
+ self.outputs = {0:0.0}
41
+
42
+ #initialize integration engine as 'None' by default
43
+ self.engine = None
44
+
45
+ #unique block identifier
46
+ self.id = uuid.uuid4()
47
+
48
+
49
+ def __str__(self):
50
+ return self.__class__.__name__
51
+
52
+
53
+ def __len__(self):
54
+ """
55
+ The '__len__' method of the block is used to compute the length of the
56
+ pass-throuh path of the block. For instant time blocks or blocks with
57
+ pass-through components (adders, amplifiers, etc.) it returns 1,
58
+ otherwise (integrator, buffer, etc.) it returns 0.
59
+ """
60
+ return 1
61
+
62
+
63
+ def __getitem__(self, key):
64
+ """
65
+ This method is intended to make connection creation more convenient
66
+ and therefore just returns the block itself and the key directly after
67
+ doing some basic checks.
68
+ """
69
+ if not isinstance(key, int):
70
+ raise ValueError(f"Port has to be of type 'int' but is '{type(key)}'!")
71
+ return (self, key)
72
+
73
+
74
+ def reset(self):
75
+ """
76
+ Reset the blocks inputs and outputs and also its internal solver, if the
77
+ block has a solver instance.
78
+ """
79
+ #reset inputs and outputs while maintaining ports
80
+ self.inputs = {k:0.0 for k in sorted(self.inputs.keys())}
81
+ self.outputs = {k:0.0 for k in sorted(self.outputs.keys())}
82
+
83
+ #reset engine if block has solver
84
+ if self.engine is not None:
85
+ self.engine.reset()
86
+
87
+
88
+ # methods for blocks with integration engines ---------------------------------------
89
+
90
+ def set_solver(self, Solver, tolerance_lte=1e-6):
91
+ """
92
+ Initialize the numerical integration engine with local truncation error
93
+ tolerance if required.
94
+ If the block already has an integration engine, it is changed,
95
+ if it does not require an integration engine, this method just passes.
96
+ """
97
+ pass
98
+
99
+
100
+ def revert(self):
101
+ """
102
+ Revert the block to the state of the previous timestep, if the
103
+ block has a solver instance indicated by the 'has_engine' flag.
104
+ This is required for adaptive solvers to revert the state to the
105
+ previous timestep.
106
+ """
107
+ if self.engine is not None:
108
+ self.engine.revert()
109
+
110
+
111
+ def buffer(self):
112
+ """
113
+ Buffer current internal state of the block, if the block has
114
+ a solver instance (is stateful).
115
+ This is required for multistage and implicit solvers.
116
+ """
117
+ if self.engine is not None:
118
+ self.engine.buffer()
119
+
120
+
121
+ # methods for sampling data ---------------------------------------------------------
122
+
123
+ def sample(self, t):
124
+ """
125
+ Samples the data of the blocks inputs or internal state when called.
126
+ This can record block parameters after a succesful timestep such as
127
+ for the 'Scope' and 'Delay' blocks but also for sampling from a random
128
+ distribution in the 'RNG' and the noise blocks.
129
+ """
130
+ pass
131
+
132
+
133
+ # methods for inter-block data transfer ---------------------------------------------
134
+
135
+ def set(self, port, value):
136
+ """
137
+ Set the value of an input port of the block.
138
+ """
139
+ self.inputs[port] = value
140
+
141
+
142
+ def get(self, port):
143
+ """
144
+ Get the value of an output port of the block.
145
+ Uses the 'get' method of 'outputs' dict with default value '0.0'.
146
+ """
147
+ return self.outputs.get(port, 0.0)
148
+
149
+
150
+ # methods for block output and state updates ----------------------------------------
151
+
152
+ def update(self, t):
153
+ """
154
+ The 'update' method is called iteratively for all blocks BEFORE the timestep
155
+ to resolve algebraic loops (fixed-point iteraion).
156
+
157
+ It is meant for instant time blocks (blocks that dont have a delay due to the
158
+ timestep, such as Amplifier, etc.) and updates the 'outputs' of the block
159
+ directly based on the 'inputs' and possibly internal states.
160
+
161
+ It computes and returns the relative difference between the new output and
162
+ the previous output (before the step) to track convergence of the fixed-point
163
+ iteration.
164
+
165
+ RETURNS :
166
+ error : (float) relative error to previous iteration for convergence control
167
+ """
168
+ return 0.0
169
+
170
+
171
+ def solve(self, t, dt):
172
+ """
173
+ The 'solve' method performes one iterative solution step that is required
174
+ to solve the implicit update equation of the solver if an implicit solver
175
+ (numerical integrator) is used.
176
+
177
+ It returns the relative difference between the new updated solution
178
+ and the previous iteration of the solution to track convergence within
179
+ an outer loop.
180
+
181
+ This only has to be implemented by blocks that have an internal
182
+ integration engine with an implicit solver.
183
+
184
+ RETURNS :
185
+ error : (float) solver residual norm
186
+ """
187
+ return 0.0
188
+
189
+
190
+ def step(self, t, dt):
191
+ """
192
+ The 'step' method is used in transient simulations and performs an action
193
+ (numeric integration timestep, recording data, etc.) based on the current
194
+ inputs and the current internal state.
195
+
196
+ It performes one timestep for the internal states. For instant time blocks,
197
+ the 'step' method does not has to be implemented specifically.
198
+
199
+ The method handles timestepping for dynamic blocks with internal states
200
+ such as 'Integrator', 'StateSpace', etc.
201
+
202
+ RETURNS :
203
+ success : (bool) step was successful
204
+ error : (float) local truncation error from adaptive integrators
205
+ scale : (float) timestep rescale from adaptive integrators
206
+ """
207
+
208
+ #by default no error estimate
209
+ return True, 0.0, 1.0
@@ -0,0 +1,30 @@
1
+ #########################################################################################
2
+ ##
3
+ ## REDUCTION BLOCKS (blocks/adder.py)
4
+ ##
5
+ ## This module defines static 'Adder' block
6
+ ##
7
+ ## Milan Rother 2024
8
+ ##
9
+ #########################################################################################
10
+
11
+ # IMPORTS ===============================================================================
12
+
13
+ import numpy as np
14
+
15
+ from ._block import Block
16
+
17
+ from ..utils.funcs import dict_to_array
18
+
19
+
20
+ # MISO BLOCKS ===========================================================================
21
+
22
+ class Adder(Block):
23
+ """
24
+ summs / adds all input signals (MISO)
25
+ """
26
+
27
+ def update(self, t):
28
+ prev_output = self.outputs[0]
29
+ self.outputs[0] = np.sum(dict_to_array(self.inputs), axis=0)
30
+ return abs(prev_output - self.outputs[0])
@@ -0,0 +1,34 @@
1
+ #########################################################################################
2
+ ##
3
+ ## IDEAL AMPLIFIER BLOCK
4
+ ## (blocks/amplifier.py)
5
+ ##
6
+ ## Milan Rother 2024
7
+ ##
8
+ #########################################################################################
9
+
10
+ # IMPORTS ===============================================================================
11
+
12
+ from ._block import Block
13
+
14
+
15
+ # SISO BLOCKS ===========================================================================
16
+
17
+ class Amplifier(Block):
18
+ """
19
+ amplifies the input signal by
20
+ multiplication with a constant gain term
21
+
22
+ INPUTS :
23
+ gain : (float) amplifier gain
24
+ """
25
+
26
+ def __init__(self, gain=1.0):
27
+ super().__init__()
28
+ self.gain = gain
29
+
30
+
31
+ def update(self, t):
32
+ prev_output = self.outputs[0]
33
+ self.outputs[0] = self.gain * self.inputs[0]
34
+ return abs(prev_output - self.outputs[0])
@@ -0,0 +1,70 @@
1
+ #########################################################################################
2
+ ##
3
+ ## TIME DOMAIN DELAY BLOCK (blocks/delay.py)
4
+ ##
5
+ ## Milan Rother 2024
6
+ ##
7
+ #########################################################################################
8
+
9
+ # IMPORTS ===============================================================================
10
+
11
+ import numpy as np
12
+
13
+ from ._block import Block
14
+ from ..utils.adaptivebuffer import AdaptiveBuffer
15
+
16
+
17
+ # BLOCKS ================================================================================
18
+
19
+ class Delay(Block):
20
+ """
21
+ delays the input signal by a time constant 'tau' in seconds
22
+ using an adaptive rolling buffer
23
+
24
+ INPUTS :
25
+ tau : (float) delay time constant for
26
+ """
27
+
28
+ def __init__(self, tau=1e-3):
29
+ super().__init__()
30
+
31
+ #time delay in seconds
32
+ self.tau = tau
33
+
34
+ #create adaptive buffer
35
+ self._buffer = AdaptiveBuffer(self.tau)
36
+
37
+
38
+ def __len__(self):
39
+ #no passthrough by definition
40
+ return 0
41
+
42
+
43
+ def reset(self):
44
+ #reset inputs and outputs
45
+ self.inputs = {0:0.0}
46
+ self.outputs = {0:0.0}
47
+
48
+ #clear the buffer
49
+ self._buffer.clear()
50
+
51
+
52
+ def update(self, t):
53
+ """
54
+ Evaluation of the buffer at different times.
55
+ """
56
+
57
+ #retrieve value from buffer
58
+ self.outputs[0] = self._buffer.get(t)
59
+
60
+ return 0.0
61
+
62
+
63
+ def sample(self, t):
64
+ """
65
+ Sample input values and time of sampling
66
+ and add them to the buffer.
67
+ """
68
+
69
+ #add new value to buffer
70
+ self._buffer.add(t, self.inputs[0])
@@ -0,0 +1,70 @@
1
+ #########################################################################################
2
+ ##
3
+ ## DIFFERENTIATOR BLOCK (blocks/differentiator.py)
4
+ ##
5
+ ## Milan Rother 2024
6
+ ##
7
+ #########################################################################################
8
+
9
+ # IMPORTS ===============================================================================
10
+
11
+ import numpy as np
12
+
13
+ from ._block import Block
14
+
15
+
16
+ # BLOCKS ================================================================================
17
+
18
+ class Differentiator(Block):
19
+ """
20
+ Differentiates the input signal (SISO) using a first order transfer function
21
+ with a pole at the origin which implements a high pass filter.
22
+
23
+ H_diff(s) = s / (1 + s/f_max)
24
+
25
+ The approximation holds for signals up to a frequency of approximately f_max.
26
+
27
+ NOTE :
28
+ Depending on 'f_max', the resulting system might become stiff or ill conditioned!
29
+ As a practical choice set f_max to 3x the highest expected signal frequency.
30
+
31
+ INPUTS :
32
+ f_max : (float) highest expected signal frequency
33
+ """
34
+
35
+ def __init__(self, f_max=1e2):
36
+ super().__init__()
37
+
38
+ #maximum frequency for differentiator approximation
39
+ self.f_max = f_max
40
+
41
+ def __len__(self):
42
+ return 1
43
+
44
+
45
+ def set_solver(self, Solver, tolerance_lte=1e-6):
46
+ #change solver if already initialized
47
+ if self.engine is not None:
48
+ self.engine = self.engine.change(Solver, tolerance_lte)
49
+ return #quit early
50
+ #initialize the numerical integration engine with kernel
51
+ def _f(x, u, t): return - self.f_max * (x - u)
52
+ def _jac(x, u, t): return - self.f_max
53
+ self.engine = Solver(0.0, _f, _jac, tolerance_lte)
54
+
55
+
56
+ def update(self, t):
57
+ #compute implicit balancing update
58
+ prev_output = self.outputs[0]
59
+ self.outputs[0] = -self.f_max * (self.engine.get() - self.inputs[0])
60
+ return abs(prev_output - self.outputs[0])
61
+
62
+
63
+ def solve(self, t, dt):
64
+ #advance solution of implicit update equation
65
+ return self.engine.solve(self.inputs[0], t, dt)
66
+
67
+
68
+ def step(self, t, dt):
69
+ #compute update step with integration engine
70
+ return self.engine.step(self.inputs[0], t, dt)
@@ -0,0 +1,82 @@
1
+ #########################################################################################
2
+ ##
3
+ ## GENERIC MIMO FUNCTION BLOCK (blocks/function.py)
4
+ ##
5
+ ## Milan Rother 2024
6
+ ##
7
+ #########################################################################################
8
+
9
+ # IMPORTS ===============================================================================
10
+
11
+ import numpy as np
12
+
13
+ from ._block import Block
14
+
15
+ from ..utils.funcs import (
16
+ max_error_dicts,
17
+ array_to_dict,
18
+ dict_to_array
19
+ )
20
+
21
+
22
+ # MIMO BLOCKS ===========================================================================
23
+
24
+ class Function(Block):
25
+ """
26
+ Arbitrary MIMO function block, defined by a callable object,
27
+ i.e. function or lambda expression.
28
+
29
+ The function can have multiple arguments that are then provided
30
+ by the input channels of the function block.
31
+
32
+ Form multi input, the function has to specify multiple arguments
33
+ and for multi output, the aoutputs have to be provided as a
34
+ tuple or list.
35
+
36
+ INPUTS :
37
+ func : (callable) MIMO function that defines block IO behaviour
38
+
39
+ NOTE :
40
+ If the outputs are provided as a single numpy array,
41
+ they are considered a single output
42
+
43
+ EXAMPLE :
44
+ consider the function:
45
+ func = lambda a, b, c : (a**2, a*b, b/c)
46
+ then the input channels of the block are assigned
47
+ to the function arguments following this scheme:
48
+ inputs[0] -> a
49
+ inputs[1] -> b
50
+ inputs[2] -> c
51
+ and the function outputs are assigned to the
52
+ output channels of the block in the same way:
53
+ a**2 -> outputs[0]
54
+ a*b -> outputs[1]
55
+ b/c -> outputs[2]
56
+ """
57
+
58
+ def __init__(self, func=lambda x: x):
59
+ super().__init__()
60
+
61
+ #some checks to ensure that function works correctly
62
+ if not callable(func):
63
+ raise ValueError(f"'{func}' is not callable")
64
+
65
+ #function defining the block update
66
+ self.func = func
67
+
68
+
69
+ def update(self, t):
70
+
71
+ #compute function output
72
+ output = self.func(*dict_to_array(self.inputs))
73
+
74
+ #check if the output is scalar
75
+ if np.isscalar(output):
76
+ prev_output = self.outputs[0]
77
+ self.outputs[0] = output
78
+ return abs(prev_output - self.outputs[0])
79
+ else:
80
+ prev_outputs = self.outputs.copy()
81
+ self.outputs = array_to_dict(output)
82
+ return max_error_dicts(prev_outputs, self.outputs)
@@ -0,0 +1,66 @@
1
+ #########################################################################################
2
+ ##
3
+ ## STANDARD INTEGRATOR BLOCK
4
+ ## (blocks/integrator.py)
5
+ ##
6
+ ## Milan Rother 2024
7
+ ##
8
+ #########################################################################################
9
+
10
+ # IMPORTS ===============================================================================
11
+
12
+ import numpy as np
13
+
14
+ from ._block import Block
15
+
16
+ from ..utils.funcs import (
17
+ dict_to_array,
18
+ array_to_dict
19
+ )
20
+
21
+
22
+ # BLOCKS ================================================================================
23
+
24
+ class Integrator(Block):
25
+ """
26
+ Integrates the input signal using a numerical integration engine.
27
+ The block is inherently MIMO capable.
28
+
29
+ INPUTS :
30
+ initial_value : (float or array) initial value of integrator
31
+ """
32
+
33
+ def __init__(self, initial_value=0.0):
34
+ super().__init__()
35
+
36
+ #save initial value
37
+ self.initial_value = initial_value
38
+
39
+
40
+ def __len__(self):
41
+ return 0
42
+
43
+
44
+ def set_solver(self, Solver, tolerance_lte=1e-6):
45
+ #change solver if already initialized
46
+ if self.engine is not None:
47
+ self.engine = self.engine.change(Solver, tolerance_lte)
48
+ return #quit early
49
+ #initialize the integration engine
50
+ def _f(x, u, t): return u
51
+ self.engine = Solver(self.initial_value, _f, None, tolerance_lte)
52
+
53
+
54
+ def update(self, t):
55
+ self.outputs = array_to_dict(self.engine.get())
56
+ return 0.0
57
+
58
+
59
+ def solve(self, t, dt):
60
+ #advance solution of implicit update equation and update block outputs
61
+ return self.engine.solve(dict_to_array(self.inputs), t, dt)
62
+
63
+
64
+ def step(self, t, dt):
65
+ #compute update step with integration engine and update block outputs
66
+ return self.engine.step(dict_to_array(self.inputs), t, dt)