lib/y_petri/simulation/timed.rb in y_petri-2.0.15 vs lib/y_petri/simulation/timed.rb in y_petri-2.1.3

- old
+ new

@@ -1,284 +1,184 @@ -# -*- coding: utf-8 -*- +# encoding: utf-8 -# A mixin for timed simulations. -# -module YPetri::Simulation::Timed - SAMPLING_TIME_DECIMAL_PLACES = 5 - SIMULATION_METHODS = - [ - [ :Euler ], - [ :Euler_with_timeless_transitions_firing_after_each_time_tick, :quasi_Euler ], - [ :Euler_with_timeless_transitions_firing_after_each_step, :pseudo_Euler ] - ] - DEFAULT_SIMULATION_METHOD = :Euler - - # ==== Exposing time-related global simulation settings - - # Simulation parameter: :initial_time. +class YPetri::Simulation + # A mixin for timed simulations, used by an +#extend+ call during init. # - attr_reader :initial_time - - # Simulation parameter: :step_size - # - attr_accessor :step_size - - # Simulation parameter: :sampling_period - # - attr_accessor :sampling_period - - # Simulation parameter: :target_time - # - attr_accessor :target_time - - # Reads the sampling rate. - # - def sampling_rate; 1 / sampling_period end - - # Reads the time range (initial_time..target_time) of the simulation. - # - def time_range; initial_time..target_time end - - # Reads simulation settings - # (:step_size, :sampling_period and :time_range). - # - def settings - { step: step_size, - sampling: sampling_period, - time: time_range } - end - alias simulation_settings settings + module Timed + require_relative 'timed/recorder' - # Exposing time. - # - attr_reader :time - alias ᴛ time + DEFAULT_SETTINGS = -> do { step: 0.1, sampling: 5, time: 0..60 } end - # def stop; end # LATER - # def continue; end # LATER + def self.included receiver + receiver.Recording.class_exec { prepend Recording } + end - # # Makes one Gillespie step - # def gillespie_step - # t, dt = gillespie_select( @net.transitions ) - # @marking_vector += t.project( @marking_vector, @step_size ) - # @time += dt - # note_state_change - # end + # True for timed simulations. + # + def timed? + true + end - # # Return projection of Δᴍ by mysode-ing the interior. - # def project_mysode_interior( Δt ) - # # So far, no interior - # # the internals of this method were already heavily obsolete - # # they can be seen in previous versions, if needed - # # so now, I just take use of the Δ_Euler_free - # Δ_Euler_free - # end + attr_reader :time, + :time_unit, + :initial_time, + :target_time, + :step, + :default_sampling - # Allows to explore the system at different state / time. Creates a double, - # which is set to the required state / time. In addition to the parent class, - # this version alseo sets time. - # - def at( time: ᴛ, **oo ) - super( **oo ).tap { |duplicate| duplicate.send :set_time, time } - end + alias starting_time initial_time + alias ending_time target_time - # Near alias for #run!, checks against infinite run. - # - def run( until_time=target_time, final_step: :exact ) - fail "Target time equals infinity!" if target_time = Float::INFINITY - run! until_time, final_step: final_step - end + delegate :flux_vector_TS, + :gradient_TS, + :gradient_Ts, + :gradient, + :flux_vector, + to: :core - # Near alias for #run_until, uses @target_time as :until_time by default. - # - def run!( until_time=target_time, final_step: :exact ) - run_until until_time, final_step: final_step - end + delegate :sampling, to: :recorder - # Runs the simulation until the target time, using step! method. The second - # optional parameter tunes the behavior towards the end of the run, with - # alternatives :just_before, :just_after and :exact (default). - # - # just_before: all steps have normal size, simulation stops - # before or just on the target time - # just_after: all steps have normal size, simulation stops - # after or just on the target time_step - # exact: simulation stops exactly on the prescribed time, - # to make this possible last step is shortened if necessary - # - def run_until( target_time, final_step: :exact ) - case final_step - when :before then # step until on or just before the target - step! while @time + @step_size <= target_time - when :exact then # simulate to exact time - step! while @time + @step_size < target_time - step!( target_time - @time ) # make a short last step as required - @time = target_time # to get exactly on the prescribed time - when :after then # step until on or after target - step! while @time < target_time - else - fail ArgumentError, "Unrecognized :final_step option: #{final_step}" + # Reads the time range (initial_time..target_time) of the simulation. + # + def time_range + initial_time..target_time end - end - # Scalar field gradient for free places. - # - def gradient_for_free_places - g_sR = gradient_for_sR - if g_sR then - S_SR() * flux_vector_for_SR + g_sR - else - S_SR() * flux_vector_for_SR + # Returnst the settings pertaining to the Timed aspect of the simulation, + # that is, +:step+, +:sampling+ and +:time+. + # + def settings all=false + super.update( step: step, + sampling: sampling, + time: time_range ) end - end - # Gradient for free places as a hash { place_name: ∂ / ∂ᴛ }. - # - def ∂ - free_places :gradient_for_free_places - end + # Same as +#run!+, but guards against run upto infinity. + # + def run( upto: target_time, final_step: :exact ) + fail "Upto time equals infinity!" if upto == Float::INFINITY + run!( upto: upto, final_step: final_step ) + end - # Scalar field gradient for all places. - # - def gradient_for_all_places - F2A() * gradient_for_free_places - end - alias gradient gradient_for_all_places + # Near alias for +#run_upto+. Accepts +:upto+ named argument, using + # @target_time attribute as a default. The second optional argument, + # +:final_step+, has the same options as in +#run_upto+ method. + # + def run!( upto: target_time, final_step: :exact ) + run_upto( upto, final_step: final_step ) + end - # Δ state of free places that would happen by a single Euler step Δt. - # - def Δ_Euler_for_free_places( Δt=step_size ) - # Here, ∂ represents all R transitions, to which TSr and Tsr are added: - delta_free = gradient_for_free_places * Δt - delta_free + Δ_TSr( Δt ) + Δ_Tsr( Δt ) - end - alias Δ_euler_for_free_places Δ_Euler_for_free_places - alias ΔE Δ_Euler_for_free_places + # Runs the simulation until the target time. Named argument :final_step has + # options :just_before, :just_after and :exact, and tunes the simulation + # behavior towards the end of the run. + # + # just_before: last step has normal size, simulation stops before or just + # on the target time + # just_after: last step has normal size, simulation stops after or just + # on the target time_step + # exact: simulation stops exactly on the prescribed time, last step + # is shortened if necessary + # + def run_upto( target_time, final_step: :exact ) + case final_step + when :before then + step! while time + step <= target_time + when :exact then + step! while time + step < target_time + step!( target_time - time ) + @time = target_time + when :after then + step! while time < target_time + else + fail ArgumentError, "Unrecognized :final_step option: #{final_step}" + end + end - # Δ state of all places that would happen by a single Euler step Δt. - # - def Δ_Euler_for_all_places( Δt=step_size ) - F2A() * ΔE( Δt ) - end - alias Δ_euler_for_all_places Δ_Euler_for_all_places - alias Δ_Euler Δ_Euler_for_all_places + # String representation of this timed simulation. + # + def to_s + "#<Simulation: time: %s, pp: %s, tt: %s, oid: %s>" % + [ time, pp.size, tt.size, object_id ] + end - # Makes one Euler step with T transitions. Timeless transitions are not - # affected. - # - def Euler_step!( Δt=@step_size ) # implicit Euler method - delta = Δ_Euler_for_free_places( Δt ) - if guarded? then - guard_Δ! delta - update_marking! delta - else - update_marking! delta + # Increments the simulation's time and alerts the recorder. + # + def increment_time! Δt=step + @time += Δt + recorder.alert end - update_time! Δt - end - alias euler_step! Euler_step! - # Fires timeless transitions once. Time and timed transitions are not - # affected. - # - def timeless_transitions_all_fire! - try "to update marking" do - update_marking!( note( "Δ state if tS transitions fire once", - is: Δ_if_tS_fire_once ) + - note( "Δ state if tsa transitions fire once", - is: Δ_if_tsa_fire_once ) ) + # Resets the timed simulation. + # + def reset! + @time = initial_time || time_unit * 0 + super end - try "to fire the assignment transitions" do - assignment_transitions_all_fire! + + # Customized dup method that allows to modify the attributes of + # the duplicate upon creation. + # + def dup time: time, **nn + super( **nn ).tap { |i| i.reset_time! time } end - end - alias t_all_fire! timeless_transitions_all_fire! + alias at dup - # At the moment, near alias of #euler_step! - # - def step! Δt=step_size - case @method - when :Euler then - Euler_step! Δt - note_state_change! - when :Euler_with_timeless_transitions_firing_after_each_step, - :pseudo_Euler then - Euler_step! Δt - timeless_transitions_all_fire! - note_state_change! - when :Euler_with_timeless_transitions_firing_after_each_time_tick, - :quasi_Euler then - raise # FIXME: quasi_Euler doesn't work yet - Euler_step! Δt - # if time tick has elapsed, call #timeless_transitions_all_fire! - note_state_change! - else - raise "Unrecognized simulation method: #@method !!!" + # Returns the zero gradient. Optionally, places can be specified, for which + # the zero vector is returned. + # + def zero_gradient places: nil + return zero_gradient places: places() if places.nil? + places.map { |id| + p = place( id ) + ( p.free? ? p.initial_marking : p.clamp ) * 0 / time_unit + }.to_column_vector end - return self - end + alias zero_∇ zero_gradient - # Produces the inspect string for this timed simulation. - # - def inspect - "#<Simulation: Time: #{time}, #{pp.size} places, #{tt.size} " + - "transitions, object id: #{object_id}>" - end + private - # Produces a string brief - def to_s # :nodoc: - "Simulation[T: #{time}, pp: #{pp.size}, tt: #{tt.size}]" - end + # Initialization subroutine for timed simulations. Expects named arguments + # +:time+ (alias +:time_range+), meaning the simulation time range (a Range + # of initial_time..target_time), +:step+, meaning time step of the + # simulation, and +:sampling+, meaning sampling period of the simulation. + # + # Initializes the time-related attributes @initial_time, @target_time, + # @time_unit and @time (via +#reset_time!+ call). Also sets up the + # parametrized subclasses +@Core+ and +@Recorder+, and initializes the + # +@recorder+ attribute. + # + def init **settings + if settings.has? :time, syn!: :time_range then # time range given + time_range = settings[:time] + @initial_time, @target_time = time_range.begin, time_range.end + @time_unit = target_time / target_time.to_f + else + anything = settings[:step] || settings[:sampling] + msg = "The simulation is timed, but the constructor lacks any of the " + + "time-related arguments: :time, :step, or :sampling!" + fail ArgumentError, msg unless anything + @time_unit = anything / anything.to_f + @initial_time, @target_time = time_unit * 0, time_unit * Float::INFINITY + end + init_core_and_recorder_subclasses + reset_time! + @step = settings[:step] || time_unit + @default_sampling = settings[:sampling] || step + @recorder = Recorder().new sampling: settings[:sampling] + end - private + # Sets up subclasses of +Core+ (the simulator) and +Recorder+ (the sampler) + # for timed simulations. + # + def init_core_and_recorder_subclasses + param_class( { Core: YPetri::Core::Timed, + Recorder: Recorder }, + with: { simulation: self } ) + end - def reset! - @time = initial_time || 0 - @next_sampling_time = @time - super # otherwise same as for timeless cases - end - - # Records a sample, now. - def sample! - print '.' - super time.round( SAMPLING_TIME_DECIMAL_PLACES ) - end - - # Hook to allow Simulation to react to its state changes. - def note_state_change! - return nil unless @time.round( 9 ) >= @next_sampling_time.round( 9 ) - sample! - @next_sampling_time += @sampling_period - end - - def update_time! Δt=step_size - @time += Δt - end - - def set_time t - @time = t - end - - # Duplicate creation. - # - def dup - instance = super - instance.send :set_time, time - return instance - end + # Resets the time to initial time, or to the argument (if provided). + # + def reset_time! time=nil + @time = time.nil? ? initial_time : time + end + end # module Timed end # module YPetri::Simulation::Timed - -# In general, it is not required that all net elements are simulated with the -# same method. Practically, ODE systems have many good simulation methods -# available. -# -# (1) ᴍ(t) = ϝ f(ᴍ, t).dt, where f(ᴍ, t) is a known function. -# -# Many of these methods depend on the Jacobian, but that may not be available -# for some places. Therefore, the places, whose marking defines the system -# state, are divided into two categories: "A" (accelerated), for which as -# common Jacobian can be found, and "E" places, where "E" can stand either for -# "External" or "Euler". -# -# If we apply the definition of "causal orientation" on A and E places, then it -# can be said, that only the transitions causally oriented towards "A" places -# are allowed for compliance with the equation (1).