@@ -109,4 +109,219 @@ end
109109# end
110110# Xsamp /= Nsims
111111# @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