module YPetri::Simulation::Timed

A mixin for timed simulations, used by an #extend call during init.

Constants

DEFAULT_SETTINGS

Attributes

default_sampling[R]
ending_time[R]
initial_time[R]
starting_time[R]
step[RW]
target_time[RW]
time[R]
time_unit[R]

Public Instance Methods

Fluxes( array ) click to toggle source

Expects a single array of TS transitions or transition ids and returns an array of their fluxes under current marking.

# File lib/y_petri/simulation/timed.rb, line 69
def Fluxes( array )
  tt = TS_transitions()
  TS_Transitions( array )
    .map { |t| flux_vector.column_to_a.fetch tt.index( t ) }
end
T_fluxes( array ) click to toggle source

Fluxes of the indicated TS transitions. Expects a single array argument, and returns a hash with transition names as keys.

# File lib/y_petri/simulation/timed.rb, line 88
def T_fluxes( array )
  TS_Transitions( array ).names( true ) >> Fluxes( array )
end
Also aliased as: t_Fluxes
at(*args) click to toggle source

Alias for #dup for timed simulations.

# File lib/y_petri/simulation/timed.rb, line 199
def at *args
  dup *args
end
dup(time: time(), **named_args) click to toggle source

Customized dup method that allows to modify the attributes of the duplicate upon creation.

Calls superclass method
# File lib/y_petri/simulation/timed.rb, line 193
def dup time: time(), **named_args
  super( **named_args ).tap { |instance| instance.reset_time! time }
end
flux( *transitions )
Alias for: fluxes
fluxes( *transitions ) click to toggle source

Expects an arbitrary number of arguments identifying TS transitions, and retuns an array of their fluxes. Returns fluxes of all the TS transitions if no argument is given.

# File lib/y_petri/simulation/timed.rb, line 79
def fluxes( *transitions )
  return Fluxes TS_transitions() if transitions.empty?
  Fluxes( transitions )
end
Also aliased as: flux
increment_time!(Δt=step) click to toggle source

Increments the simulation's time and alerts the recorder.

# File lib/y_petri/simulation/timed.rb, line 178
def increment_time! Δt=step
  @time += Δt
  recorder.alert!
end
pflux( *transitions, gap: 0, precision: 4 ) click to toggle source

Pretty prints flux of the indicated TS transitions as a hash with transition names as keys. Takes optional list of transition ids (first ordered arg.), and optional 2 named arguments (:gap and :precision), as in #pretty_print_numeric_values.

# File lib/y_petri/simulation/timed.rb, line 107
def pflux( *transitions, gap: 0, precision: 4 )
  t_flux( *transitions )
    .pretty_print_numeric_values( gap: gap, precision: precision )
end
Also aliased as: pfluxes
pfluxes( *transitions, gap: 0, precision: 4 )
Alias for: pflux
reset!(**nn) click to toggle source

Resets the timed simulation.

Calls superclass method
# File lib/y_petri/simulation/timed.rb, line 185
def reset! **nn
  @time = initial_time || time_unit * 0
  super
end
run( upto: target_time, final_step: :exact ) click to toggle source

Same as #run!, but guards against run upto infinity.

# File lib/y_petri/simulation/timed.rb, line 130
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
run!( upto: target_time, final_step: :exact ) click to toggle source

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.

# File lib/y_petri/simulation/timed.rb, line 139
def run!( upto: target_time, final_step: :exact )
  run_upto( upto, final_step: final_step )
end
run_upto( target_time, final_step: :exact ) click to toggle source

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.

stop_before: last step has normal size, simulation stops before or just

on the target time

stop_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
# File lib/y_petri/simulation/timed.rb, line 154
def run_upto( target_time, final_step: :exact )
  case final_step
  when :stop_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 :stop_after then
    step! while time < target_time
  else
    fail ArgumentError, "Unrecognized :final_step option: #{final_step}"
  end
end
set_sampling(sampling) click to toggle source

Sets sampling of the simulation's data recorder.

# File lib/y_petri/simulation/timed.rb, line 48
def set_sampling sampling
  recorder.sampling = sampling
end
set_simulation_method() click to toggle source

Changing the simulation method on the fly not supported.

# File lib/y_petri/simulation/timed.rb, line 54
def set_simulation_method
  fail NotImplementedError,
       "Changing simulation method on the fly not supported!"
end
set_step(n) click to toggle source

Explicit alias for #step= method. Deprecated, use #step= instead.

# File lib/y_petri/simulation/timed.rb, line 30
def set_step n
  step=( n )
end
Also aliased as: set_step_size
set_step_size(n)
Alias for: set_step
set_target_time(target_time)
Alias for: set_time
set_time(target_time) click to toggle source

Explicit alias for #target_time= method. Deprecated, use #target_time= instead.

# File lib/y_petri/simulation/timed.rb, line 38
def set_time target_time
  target_time=( target_time )
end
Also aliased as: set_target_time
settings(all=false) click to toggle source

Returns the settings pertaining to the Timed aspect of the simulation, that is, :step, :sampling and :time.

Calls superclass method
# File lib/y_petri/simulation/timed.rb, line 122
def settings all=false
  super.update( step: step,
                sampling: sampling,
                time: time_range )
end
t_Fluxes( array )
Alias for: T_fluxes
t_flux( *transitions )
Alias for: t_fluxes
t_fluxes( *transitions ) click to toggle source

Fluxes of the indicated TS transitions. Expects an arbitrary number of TS transitions or their ids, returns a hash with transition names as keys.

# File lib/y_petri/simulation/timed.rb, line 96
def t_fluxes( *transitions )
  return T_fluxes TS_transitions() if transitions.empty?
  T_fluxes( transitions )
end
Also aliased as: t_flux
time_range() click to toggle source

Reads the time range (initial_time .. target_time) of the simulation.

# File lib/y_petri/simulation/timed.rb, line 115
def time_range
  initial_time .. target_time
end
timed?() click to toggle source

True for timed simulations.

# File lib/y_petri/simulation/timed.rb, line 12
def timed?
  true
end
to_s() click to toggle source

String representation of this timed simulation.

# File lib/y_petri/simulation/timed.rb, line 171
def to_s
  "#<Simulation: time: %s, pp: %s, tt: %s, oid: %s>" %
    [ time, pp.size, tt.size, object_id ]
end
zero_gradient(places: nil) click to toggle source

Returns the zero gradient. Optionally, places can be specified, for which the zero vector is returned.

# File lib/y_petri/simulation/timed.rb, line 206
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
Also aliased as: zero_∇
zero_∇(places: nil)
Alias for: zero_gradient

Protected Instance Methods

reset_time!(time=nil) click to toggle source

Resets the time to initial time, or to the argument (if provided).

# File lib/y_petri/simulation/timed.rb, line 219
def reset_time! time=nil
  @time = time.nil? ? initial_time : time
end

Private Instance Methods

init(**settings) click to toggle source

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.

# File lib/y_petri/simulation/timed.rb, line 235
def init **settings
  method = settings[:method] # the simulation method
  features_to_record = settings[:record]
  if settings.has? :time, syn!: :time_range then # time range given
    case settings[:time]
    when Range then
      time_range = settings[:time]
      @initial_time, @target_time = time_range.begin, time_range.end
      @time_unit = case initial_time
                   when Float then 1.0
                   when Integer then 1
                   else
                     initial_time.class.one
                   end
    else
      # TODO: When using simulation after some time, I found this behavior
      # surprising. I wanted to call simulation time: 100, expecting it
      # to run until 100 (in the range 0..100). Instead, I see that it wants
      # to run from 100 to infinity. While I understand how important it
      # is to have a simple way to set the time of a newly constructed
      # simulation to some value (for the purposes such as cloning of
      # simulation, interpolation of simulations etc. -- actually, there
      # is no really stateful net in YPetri at the moment, so Simulation
      # class behaves somewhat as a stateful net...), not just me, but
      # other users might expect :time argument to set final time with
      # initial time being 0. I'm not gonna change it quite yet.
      #
      # The way to refactor it would be to first introduce "initial_time"
      # parameter and make "time" parameter raise an error, and refactor
      # the code until the tests pass. Then, to reintroduce "time"
      # parameter with the new, more intuitive meaning. Interactive
      # users can always modify time later (simulation.time = something).
      #
      @initial_time = settings[:time]
      @time_unit = case initial_time
                   when Float then 1.0
                   when Integer then 1
                   else
                     initial_time.class.one
                   end
      @target_time = time_unit * Float::INFINITY
    end
  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 = case anything
                 when Float then 1.0
                 when Integer then 1
                 else
                   anything.class.one
                 end
    @initial_time, @target_time = time_unit * 0, time_unit * Float::INFINITY
  end
  # Set up a parametrized subclas of the sampler for timed simulation.
  param_class( { Recorder: Recorder }, with: { simulation: self } )
  reset_time!
  @step = settings[:step] || time_unit
  @default_sampling = settings[:sampling] || step
  if method == :runge_kutta then
    # This is a bit irregular for now, but since the core has to behave
    # differently (that is, more like a real simulation core), at least
    # for the more advanced runge_kutta method, a core under a different
    # instance variable will be constructed.
    @rk_core = if @guarded then
                 YPetri::Core::Timed.new( simulation: self, method: method, guarded: true )
               else
                 YPetri::Core::Timed.new( simulation: self, method: method, guarded: false )
               end
    singleton_class.class_exec do
      attr_reader :rk_core
      delegate :simulation_method,
               :firing_vector_tS,
               to: :rk_core
      
      # This method steps the simulation forward by the prescribed step. Simulation uses the core to perform the #step! method.
      #
      def step! Δt=step()
        # Pseudocode would be like this:
        
        # 1. set_state_and_time_of_core_to_the_current_simulation's_state_and_time
        
        # 2. explicitly tell the core the code by which to alert the sampler when necessary
        # ie. when the state vector of the core progresses sufficiently for it to be
        # interesting to the sampler

        # 3. explicitly tell the core the code by which to update the simulation's state,
        # and when should it be updated.
        #
        # (Note: This can be done in several ways. For example, one possibility is to
        # update the simulation only after the core is finished computing. Another
        # possibility is to have some other criterion to update the simulation more
        # often in the course of the core's work. Since this is some sort of sampling
        # job again, there is an option of actually delegating it to the sampler,
        # which would thus get closer to its role of the interface.)
        #
        # (Note 2: It is actually more clear what the role of the core should be rather
        # than what the simulation's role should be. The core should receive the initial
        # instructions, a relatively simple method of what to do, and it should be
        # specialized in doing its job fast. Secondly, it should receive the method for
        # alerting the superiors: simulation and/or sampler. Thirdly, it should be told
        # when to stop.)

        # This should set the state of the rk_core to the marking vector of free
        # places (#marking_vector method).
        rk_core.marking_of_free_places.reset!( marking_vector )
        sim, rec = self, recorder
        rk_core.set_user_alert_closure do |mv_free| # marking vect. of free places
          # TODO: This can be done differently. For example, the simulation can hand
          # the core the function which, when handed a hash, will update the marking
          # vector with the hash. This is actually quite similar, but there is some
          # ugliness in it...
          sim.m_vector.reset! mv_free.to_hash
          sim.increment_time! Δt
          Kernel.print '.'
          rec.alert!
        end

        rk_core.step! Δt

        # TODO: In the above lines, setting rec = recorder and then calling
        # rec!.alert in the block is a bit weird. It would be nicer to use
        # recorder.alert!, but maybe
        # wise Ruby closure mechanism does not allow it...
      end # def step!
    end # singleton_class.class_exec
  else # the method is not :runge_kutta
    @core = if @guarded then
              YPetri::Core::Timed.new( simulation: self,
                                       method: method, guarded: true )
            else
              YPetri::Core::Timed.new( simulation: self,
                                       method: method, guarded: false )
            end
    # TODO: But why am I doing it like this? Did I want to
    # emphasize the standalone nature of Core class? Maybe... And
    # maybe I did it so that the runge-kutta method with its
    # @rk_core instance variable instead of @core does not have
    # @core and #core.  In this manner, I'm forcing myself to
    # rethink Simulation class.
    singleton_class.class_exec do
      attr_reader :core
      delegate :simulation_method, # this looks quite redundant with simulation.rb
               :step!,
               :firing_vector_tS,
               to: :core
    end # singleton_class.class_exec
  end 
  @recorder = if features_to_record then
                # we'll have to figure out features
                ff = case features_to_record
                     when Array then
                       net.State.Features
                         .infer_from_nodes( features_to_record )
                     when Hash then
                       net.State.features( features_to_record )
                     end
                Recorder().new( sampling: settings[:sampling], features: ff )
              else
                Recorder().new( sampling: settings[:sampling] )
              end
end
step!(Δt=step()) click to toggle source

This method steps the simulation forward by the prescribed step. Simulation uses the core to perform the step! method.

# File lib/y_petri/simulation/timed.rb, line 313
def step! Δt=step()
  # Pseudocode would be like this:
  
  # 1. set_state_and_time_of_core_to_the_current_simulation's_state_and_time
  
  # 2. explicitly tell the core the code by which to alert the sampler when necessary
  # ie. when the state vector of the core progresses sufficiently for it to be
  # interesting to the sampler

  # 3. explicitly tell the core the code by which to update the simulation's state,
  # and when should it be updated.
  #
  # (Note: This can be done in several ways. For example, one possibility is to
  # update the simulation only after the core is finished computing. Another
  # possibility is to have some other criterion to update the simulation more
  # often in the course of the core's work. Since this is some sort of sampling
  # job again, there is an option of actually delegating it to the sampler,
  # which would thus get closer to its role of the interface.)
  #
  # (Note 2: It is actually more clear what the role of the core should be rather
  # than what the simulation's role should be. The core should receive the initial
  # instructions, a relatively simple method of what to do, and it should be
  # specialized in doing its job fast. Secondly, it should receive the method for
  # alerting the superiors: simulation and/or sampler. Thirdly, it should be told
  # when to stop.)

  # This should set the state of the rk_core to the marking vector of free
  # places (#marking_vector method).
  rk_core.marking_of_free_places.reset!( marking_vector )
  sim, rec = self, recorder
  rk_core.set_user_alert_closure do |mv_free| # marking vect. of free places
    # TODO: This can be done differently. For example, the simulation can hand
    # the core the function which, when handed a hash, will update the marking
    # vector with the hash. This is actually quite similar, but there is some
    # ugliness in it...
    sim.m_vector.reset! mv_free.to_hash
    sim.increment_time! Δt
    Kernel.print '.'
    rec.alert!
  end

  rk_core.step! Δt

  # TODO: In the above lines, setting rec = recorder and then calling
  # rec!.alert in the block is a bit weird. It would be nicer to use
  # recorder.alert!, but maybe
  # wise Ruby closure mechanism does not allow it...
end