@@ -109,4 +109,219 @@ end
109
109
# end
110
110
# Xsamp /= Nsims
111
111
# @test abs(Xsamp - Xf(0.2, p) < 0.05 * Xf(0.2, p))
112
- # end
112
+ # end
113
+
114
+ # Checks that a disjoint hybrid model (i.e. where the Jump and ODE parts do not interact) gives the
115
+ # same results as simulating the two parts separately.
116
+ let
117
+ # Creates the disjoint ODE/Jump models, and the hybrid model combining both.
118
+ rn_ode = @reaction_network begin
119
+ A, ∅ → X
120
+ 1 , 2 X + Y → 3 X
121
+ B, X → Y
122
+ 1 , X → ∅
123
+ end
124
+ rn_jump = @reaction_network begin
125
+ (p,d), 0 <--> V
126
+ (k1,k2), V + W <--> VW
127
+ end
128
+ rn_hybrid = @reaction_network begin
129
+ A, ∅ → X, [physical_scale = PhysicalScale. ODE]
130
+ 1 , 2 X + Y → 3 X, [physical_scale = PhysicalScale. ODE]
131
+ B, X → Y, [physical_scale = PhysicalScale. ODE]
132
+ 1 , X → ∅, [physical_scale = PhysicalScale. ODE]
133
+ (p,d), 0 <--> V
134
+ (k1,k2), V + W <--> VW
135
+ end
136
+
137
+ # Sets simulation conditions and creates problems corresponding to the different models.
138
+ u0_ode = [:X => 1.0 , :Y => 0.0 ]
139
+ u0_jump = [:V => 1 , :W => 20 , :VW => 0.0 ]
140
+ u0_hybrid = (u0_ode... , u0_jump... )
141
+ ps_ode = [:A => 1.0 , :B => 4.0 ]
142
+ ps_jump = [:p => 2.0 , :d => 0.1 , :k1 => 0.2 , :k2 => 1.0 ]
143
+ ps_hybrid = [ps_ode; ps_jump]
144
+ tspan = (0.0 , 1000.0 )
145
+ ode_prob = ODEProblem (rn_ode, u0_ode, tspan, ps_ode)
146
+ jump_prob = JumpProblem (JumpInputs (rn_jump, u0_jump, tspan, ps_jump); save_positions = (false ,false ))
147
+ hybrid_prob = JumpProblem (JumpInputs (rn_hybrid, u0_hybrid, tspan, ps_hybrid); save_positions = (false ,false ))
148
+
149
+ # Performs simulations. Checks that ODE parts are identical. Check that jump parts have similar statistics.
150
+ ode_sol = solve (ode_prob, Rosenbrock23 (); saveat = 1.0 , abstol = 1e-10 , reltol = 1e-10 )
151
+ jump_sol = solve (jump_prob, SSAStepper (); saveat = 1.0 )
152
+ hybrid_sol = solve (hybrid_prob, Rosenbrock23 (); saveat = 1.0 , abstol = 1e-10 , reltol = 1e-10 )
153
+ @test ode_sol[:Y ] ≈ hybrid_sol[:Y ] atol = 1e-4 rtol = 1e-4
154
+ @test mean (jump_sol[:V ]) ≈ mean (hybrid_sol[:V ]) atol = 1e-1 rtol = 1e-1
155
+ @test mean (jump_sol[:W ]) ≈ mean (hybrid_sol[:W ]) atol = 1e-1 rtol = 1e-1
156
+ @test mean (jump_sol[:VW ]) ≈ mean (hybrid_sol[:VW ]) atol = 1e-1 rtol = 1e-1
157
+ end
158
+
159
+ # ## Other Tests ###
160
+
161
+ # Checks that the full pipeline of symbolic accessing/updating problems/integrators/solutions
162
+ # of hybrid models work.
163
+ # CURRENTLY NOT WORKING DUE TO `remake` ISSUE WITH HYBRID MODELS.
164
+ let
165
+ # Creates model and initial problem (with defaults for one species and one parameter).
166
+ rn_hybrid = @reaction_network begin
167
+ @species X (t) = 1.0
168
+ @parameters p = 1.0
169
+ p, 0 --> X, [physical_scale = PhysicalScale. ODE]
170
+ (kB,kD), 2 X <--> X2
171
+ k, X2 --> Y2, [physical_scale = PhysicalScale. ODE]
172
+ (kB,kD), 2 Y <--> Y2
173
+ d, Y --> 0 , [physical_scale = PhysicalScale. ODE]
174
+ end
175
+ u0 = [:X2 => 0.0 , :Y => 0.0 , :Y2 => 0.0 ]
176
+ ps = [:kB => 2.0 , :kD => 0.4 , :k => 1.0 , :d => 0.5 ]
177
+ prob = JumpProblem (JumpInputs (rn_hybrid, u0, (0.0 , 5.0 ), ps))
178
+
179
+ # Check problem content, updates problem with remake, checks again.
180
+ @test prob[:X ] == 1.0
181
+ @test prob[:X2 ] == 0.0
182
+ @test prob[:Y ] == 0.0
183
+ @test prob[:Y2 ] == 0.0
184
+ @test prob. ps[:p ] == 1.0
185
+ @test prob. ps[:kB ] == 2.0
186
+ @test prob. ps[:kD ] == 0.4
187
+ @test prob. ps[:k ] == 1.0
188
+ @test prob. ps[:d ] == 0.5
189
+ prob = remake (prob; u0 = [:X => 3.0 , :X2 => 2.0 , :Y => 1.0 ], p = [:p => 5.0 , :dD => 0.8 , :k => 2.0 ])
190
+ @test prob[:X ] == 3.0
191
+ @test prob[:X2 ] == 2.0
192
+ @test prob[:Y ] == 1.0
193
+ @test prob[:Y2 ] == 0.0
194
+ @test prob. ps[:p ] == 5.0
195
+ @test prob. ps[:kB ] == 2.0
196
+ @test prob. ps[:kD ] == 0.8
197
+ @test prob. ps[:k ] == 2.0
198
+ @test prob. ps[:d ] == 0.5
199
+
200
+ # Creates, checks, updates, and checks an integrator.
201
+ int = init (prob, Tsit5 ())
202
+ @test int[:X ] == 3.0
203
+ @test int[:X2 ] == 2.0
204
+ @test int[:Y ] == 1.0
205
+ @test int[:Y2 ] == 0.0
206
+ @test int. ps[:p ] == 5.0
207
+ @test int. ps[:kB ] == 2.0
208
+ @test int. ps[:kD ] == 0.8
209
+ @test int. ps[:k ] == 2.0
210
+ @test int. ps[:d ] == 0.5
211
+ @test int[:X ] = 4.0
212
+ @test int[:X2 ] = 3.0
213
+ @test int[:Y ] = 2.0
214
+ @test int. ps[:p ] = 6.0
215
+ @test int. ps[:kB ] = 3.0
216
+ @test int. ps[:kD ] = 0.6
217
+ @test int. ps[:k ] = 3.0
218
+ @test int[:X ] == 4.0
219
+ @test int[:X2 ] == 3.0
220
+ @test int[:Y ] == 2.0
221
+ @test int[:Y2 ] == 0.0
222
+ @test int. ps[:p ] == 6.0
223
+ @test int. ps[:kB ] == 3.0
224
+ @test int. ps[:kD ] == 0.5
225
+ @test int. ps[:k ] == 3.0
226
+ @test int. ps[:d ] == 0.5
227
+
228
+ # Solves and checks values of solution.
229
+ sol = solve (prob, Tsit5 (); maxiters = 1 , verbose = false )
230
+ @test sol[:X ][1 ] == 3.0
231
+ @test sol[:X2 ][1 ] == 2.0
232
+ @test sol[:Y ][1 ] == 1.0
233
+ @test sol[:Y2 ][1 ] == 0.0
234
+ @test sol. ps[:p ] == 5.0
235
+ @test sol. ps[:kB ] == 2.0
236
+ @test sol. ps[:kD ] == 0.8
237
+ @test sol. ps[:k ] == 2.0
238
+ @test sol. ps[:d ] == 0.5
239
+ end
240
+
241
+ # Checks that various model options (observables, events, defaults and metadata, differential equations,
242
+ # non-default_iv) works for hybrid models.
243
+ let
244
+ # Creates the model (X species is pure jump, Y is pure ODE, and Z1,Z2 are mixed).
245
+ # Hybrid species have non-constant rates containing the two other species.
246
+ rn = @reaction_network begin
247
+ @ivs τ
248
+ @differentials Δ = Differential (τ)
249
+ @parameters X0 Y0
250
+ @species X (τ) = X0 [description = " A jump only species" ] Y (τ) = Y0 [description = " An ODE only species" ]
251
+ @observables Ztot ~ Z1 + Z2
252
+ @discrete_events [1.0 ] => [Z1 ~ Z1 + 1.0 ]
253
+ @continuous_events [Y ~ 1.0 ] => [Y ~ 5.0 ]
254
+ @equations Δ (V) ~ Z1 + X^ 2 - V
255
+ (p,d), 0 <--> X
256
+ d, Y --> 0 , [physical_scale = PhysicalScale. ODE]
257
+ (k* X, k* Y), Z1 <--> Z2, ([physical_scale = PhysicalScale. ODE], [physical_scale = PhysicalScale. Jump])
258
+ end
259
+
260
+ # Simulates the model.
261
+ u0 = [:Z1 => 1.0 , :Z2 => 2.0 , :V => 1.5 ]
262
+ ps = [:p => 2.0 , :d => 0.5 , :k => 1.0 , :X0 => 0.1 , :Y0 => 4.0 ]
263
+ prob = JumpProblem (JumpInputs (rn, u0, (0.0 , 10.0 ), ps))
264
+ sol = solve (prob, Tsit5 (); tstops = [1.0 - eps (), 1.0 + eps ()])
265
+
266
+ # Tests that the model contain the correct stuff.
267
+ @test ModelingToolkit. getdescription (rn. X) == " A jump only species"
268
+ @test ModelingToolkit. getdescription (rn. Y) == " An ODE only species"
269
+ @test sol[:X ][1 ] == sol. ps[:X0 ] == 0.1
270
+ @test sol[:Y ][1 ] == sol. ps[:Y0 ] == 4.0
271
+ @test sol[:V ][1 ] == 1.5
272
+ @test sol[:Ztot ] ≈ sol[rn. Z1 + rn. Z2]
273
+ @test minimum (sol[:Y ]) ≈ 1.0
274
+ @test maximum (sol[:Y ]) ≈ 5.0 atol = 1e-1 rtol = 1e-1
275
+ @test all (isequal ([rn. τ], Symbolics. arguments (Symbolics. value (u))) for u in unknowns (rn))
276
+ @test sol (1.0 - eps (); idxs = :Z1 ) + 1 ≈ sol (1.0 + eps (); idxs = :Z1 )
277
+ end
278
+
279
+ # Checks the types of species when various combinations of default/non-default types are used.
280
+ let
281
+ # Creates model and parameter set. Model have one pure ODE and one pure Jump species.
282
+ rn = @reaction_network begin
283
+ d, X --> 0
284
+ d, Y --> 0 , [physical_scale = PhysicalScale. ODE]
285
+ end
286
+ ps = [:d => 0.5 ]
287
+
288
+ # Checks case: X Int64 (default), Y Float64 (default). Everything is converted to Float64.
289
+ u0 = (:X => 1 , :Y => 1.0 )
290
+ prob = JumpProblem (JumpInputs (rn, u0, (0.0 , 1.0 ), ps))
291
+ int = init (prob, Tsit5 ())
292
+ sol = solve (prob, Tsit5 ())
293
+ @test typeof (prob[:X ]) == typeof (int[:X ]) == typeof (sol[:X ][end ]) == Float64
294
+ @test typeof (prob[:Y ]) == typeof (int[:Y ]) == typeof (sol[:Y ][end ]) == Float64
295
+
296
+ # Checks case: X Int32 (non-default), Y Float64 (default). Everything is converted to Float64.
297
+ u0 = (:X => Int32 (1 ), :Y => 1.0 )
298
+ prob = JumpProblem (JumpInputs (rn, u0, (0.0 , 1.0 ), ps))
299
+ int = init (prob, Tsit5 ())
300
+ sol = solve (prob, Tsit5 ())
301
+ @test typeof (prob[:X ]) == typeof (int[:X ]) == typeof (sol[:X ][end ]) == Float64
302
+ @test typeof (prob[:Y ]) == typeof (int[:Y ]) == typeof (sol[:Y ][end ]) == Float64
303
+
304
+ # Checks case: X Int64 (default), Y Float32 (non-default). Everything is converted to Float64.
305
+ u0 = (:X => 1 , :Y => 1.0f0 )
306
+ prob = JumpProblem (JumpInputs (rn, u0, (0.0 , 1.0 ), ps))
307
+ int = init (prob, Tsit5 ())
308
+ sol = solve (prob, Tsit5 ())
309
+ @test typeof (prob[:X ]) == typeof (int[:X ]) == typeof (sol[:X ][end ]) == Float64
310
+ @test typeof (prob[:Y ]) == typeof (int[:Y ]) == typeof (sol[:Y ][end ]) == Float64
311
+
312
+ # Checks case: X Int32 (non-default), Y Float32 (non-default). Everything is converted to Float64.
313
+ u0 = (:X => Int32 (1 ), :Y => 1.0f0 )
314
+ prob = JumpProblem (JumpInputs (rn, u0, (0.0 , 1.0 ), ps))
315
+ int = init (prob, Tsit5 ())
316
+ sol = solve (prob, Tsit5 ())
317
+ @test typeof (prob[:X ]) == typeof (int[:X ]) == typeof (sol[:X ][end ]) == Float64
318
+ @test typeof (prob[:Y ]) == typeof (int[:Y ]) == typeof (sol[:Y ][end ]) == Float64
319
+
320
+ # Checks case: X Float32 (non-default float), Y Float32 (non-default). Everything is converted to Float32.
321
+ u0 = (:X => 1.0f0 , :Y => 1.0f0 )
322
+ prob = JumpProblem (JumpInputs (rn, u0, (0.0 , 1.0 ), ps))
323
+ int = init (prob, Tsit5 ())
324
+ sol = solve (prob, Tsit5 ())
325
+ @test typeof (prob[:X ]) == typeof (int[:X ]) == typeof (sol[:X ][end ]) == Float32
326
+ @test typeof (prob[:Y ]) == typeof (int[:Y ]) == typeof (sol[:Y ][end ]) == Float32
327
+ end
0 commit comments