# -*- coding: utf-8 -*-
require_relative 'dependency_injection'
require_relative 'transition/arcs'
require_relative 'transition/cocking'
require_relative 'transition/constructor_syntax'
# A Petri net transition. There are 6 basic types of YPetri transitions:
#
# * ts – timeless nonstoichiometric
# * tS – timeless stoichiometric
# * Tsr – timed rateless nonstoichiometric
# * TSr – timed rateless stoichiometric
# * sR – nonstoichiometric with rate
# * SR – stoichiometric with rate
#
# These 6 kinds of YPetri transitions correspond to the vertices of a cube,
# whose 3 dimensions are:
#
# - stoichiometric (S) / nonstoichiometric (s)
# - timed (T) / timeless (t)
# - having rate (R) / not having rate (r)
#
# I. For stoichiometric transitions:
# 1. Rate vector is computed as rate * stoichiometry vector, or
# 2. Δ vector is computed a action * stoichiometry vector.
# II. For non-stoichiometric transitions:
# 1. Rate vector is obtained as the rate closure result, or
# 2. action vector is obtained as the action closure result.
#
# Conclusion: stoichiometricity distinguishes *need to multiply the
# rate/action closure result by stoichiometry*.
#
# I. For transitions with rate, the closure result has to be
# multiplied by the time step duration (delta_t) to get action.
# II. For rateless transitions, the closure result is used as is.
#
# Conclusion: has_rate? distinguishes *need to multiply the closure
# result by delta time* -- differentiability of action by time.
#
# I. For timed transitions, action is time-dependent. Transitions with
# rate are thus always timed. In rateless transitions, timedness means
# that the action closure expects time step length (delta_t) as its first
# argument - its arity is thus codomain size + 1.
# II. For timeless transitions, action is time-independent. Timeless
# transitions are necessarily also rateless. Arity of the action closure
# is expected to match the domain size.
#
# Conclusion: Transitions with rate are always timed. In rateless
# transitions, timedness distinguishes the need to supply time step
# duration as the first argument to the action closure.
#
# Since transitions with rate are always timed, and vice-versa, timeless
# transitions cannot have rate, there are not 8, but only 6 permissible
# combinations -- 6 basic transition types listed above.
#
# === Domain and codomin
#
# Each transition has a domain, or 'upstream places': A collection of places
# whose marking directly affects the transition's operation. Also, each
# transition has a codomain, or 'downstream places': A collection of places,
# whose marking is directly affected by the transition's operation.
#
# === Action and action vector
#
# Regardless of the type, every transition has action:
# A prescription of how the transition changes the marking of its codomain
# when it fires. With respect to the transition's codomain, we can also
# talk about action vector. For non-stoichiometric transitions,
# the action vector is directly the output of the action closure or rate
# closure multiplied by Δtime, while for stoichiometric transitions, this
# needs to be additionaly multiplied by the transitions stoichiometric
# vector. Now we are finally equipped to talk about the exact meaning of
# 3 basic transition properties.
#
# === Meaning of the 3 basic transition properties
#
# ==== Stoichiometric / non-stoichiometric
# * For stoichiometric transitions:
# [Rate vector] is computed as rate * stoichiometry vector, or
# [Δ vector] is computed a action * stoichiometry vector
# * For non-stoichiometric transitions:
# [Rate vector] is obtained as the rate closure result, or
# [action vector] is obtained as the action closure result.
#
# Conclusion: stoichiometricity distinguishes need to multiply the
# rate/action closure result by stoichiometry.
#
# ==== Having / not having rate
# * For transitions with rate, the closure result has to be
# multiplied by the time step duration (Δt) to get the action.
# * For rateless transitions, the closure result is used as is.
#
# Conclusion: has_rate? distinguishes the need to multiply the closure
# result by delta time - differentiability of action by time.
#
# ==== Timed / Timeless
# * For timed transitions, action is time-dependent. Transitions with
# rate are thus always timed. In rateless transitions, timedness means
# that the action closure expects time step length (delta_t) as its first
# argument - its arity is thus codomain size + 1.
# * For timeless transitions, action is time-independent. Timeless
# transitions are necessarily also rateless. Arity of the action closure
# is expected to match the domain size.
#
# Conclusion: Transitions with rate are always timed. In rateless
# transitions, timedness distinguishes the need to supply time step
# duration as the first argument to the action closure.
#
# === Other transition types
#
# ==== Assignment transitions
# Named argument :assignment_action set to true indicates that the
# transitions acts by replacing the object stored as place marking by
# the object supplied by the transition. (Same as in with spreadsheet
# functions.) For numeric types, same effect can be achieved by subtracting
# the old number from the place and subsequently adding the new value to it.
#
# ==== Functional / Functionless transitions
# Original Petri net definition does not speak about transition "functions",
# but it more or less assumes timeless action according to the stoichiometry.
# So in YPetri, stoichiometric transitions with no action / rate closure
# specified become functionless transitions as meant by Carl Adam Petri.
#
class YPetri::Transition
include NameMagic
include YPetri::DependencyInjection
BASIC_TRANSITION_TYPES = {
ts: "timeless nonstoichiometric transition",
tS: "timeless stoichiometric transition",
Tsr: "timed rateless nonstoichiometric transition",
TSr: "timed rateless stoichiometric transition",
sR: "nonstoichiometric transition with rate",
SR: "stoichiometric transition with rate"
}
# Domain, or 'upstream arcs', is a collection of places, whose marking
# directly affects the transition's action.
#
attr_reader :domain
alias :domain_arcs :domain
alias :domain_places :domain
alias :upstream :domain
alias :upstream_arcs :domain
alias :upstream_places :domain
# Codomain, 'downstream arcs', or 'action arcs', is a collection of places,
# whose marking is directly changed by this transition's firing.
#
attr_reader :codomain
alias :codomain_arcs :codomain
alias :codomain_places :codomain
alias :downstream :codomain
alias :downstream_arcs :codomain
alias :downstream_places :codomain
alias :action_arcs :codomain
# Is the transition stoichiometric?
#
def stoichiometric?; @stoichiometric end
alias :s? :stoichiometric?
# Is the transition nonstoichiometric? (Opposite of #stoichiometric?)
#
def nonstoichiometric?
not stoichiometric?
end
# Stoichiometry (implies that the transition is stoichiometric).
#
attr_reader :stoichiometry
# Stoichiometry as a hash of pairs:
# { codomain_place_instance => stoichiometric_coefficient }
#
def stoichio
Hash[ codomain.zip( @stoichiometry ) ]
end
# Stoichiometry as a hash of pairs:
# { codomain_place_name_symbol => stoichiometric_coefficient }
#
def s
stoichio.with_keys { |k| k.name || k.object_id }
end
# Does the transition have rate?
#
def has_rate?
@has_rate
end
# Is the transition rateless?
#
def rateless?
not has_rate?
end
# The term 'flux' (meaning flow) is associated with continuous transitions,
# while term 'propensity' is used with discrete stochastic transitions.
# By the design of YPetri, distinguishing between discrete and continuous
# computation is the responsibility of the simulation method, considering
# current marking of the transition's connectivity and quanta of its
# codomain. To emphasize unity of 'flux' and 'propensity', term 'rate' is
# used to represent both of them. Rate closure input arguments must
# correspond to the domain places.
#
attr_reader :rate_closure
alias :rate :rate_closure
alias :flux_closure :rate_closure
alias :flux :rate_closure
alias :propensity_closure :rate_closure
alias :propensity :rate_closure
# For rateless transition, action closure must be present. Action closure
# input arguments must correspond to the domain places, and for timed
# transitions, the first argument of the action closure must be Δtime.
#
attr_reader :action_closure
alias :action :action_closure
# Does the transition's action depend on delta time?
#
def timed?
@timed
end
# Is the transition timeless? (Opposite of #timed?)
#
def timeless?
not timed?
end
# Is the transition functional?
# Explanation: If rate or action closure is supplied, a transition is always
# considered 'functional'. Otherwise, it is considered not 'functional'.
# Note that even transitions that are not functional still have standard
# action acc. to Petri's definition. Also note that a timed transition is
# necessarily functional.
#
def functional?
@functional
end
# Opposite of #functional?
#
def functionless?
not functional?
end
# Reports the transition's membership in one of 6 basic types :
# 1. ts ..... timeless nonstoichiometric
# 2. tS ..... timeless stoichiometric
# 3. Tsr .... timed rateless nonstoichiometric
# 4. TSr .... timed rateless stoichiometric
# 5. sR ..... nonstoichiometric with rate
# 6. SR ..... stoichiometric with rate
#
def basic_type
if has_rate? then stoichiometric? ? :SR : :sR
elsif timed? then stoichiometric? ? :TSr : :Tsr
else stoichiometric? ? :tS : :ts end
end
# Reports transition's type (basic type + whether it's an assignment
# transition).
#
def type
assignment_action? ? "A(ts)" : basic_type
end
# Is it an assignment transition?
#
# A transition can be specified to have 'assignment action', in which case
# it completely replaces codomain marking with the objects resulting from
# the transition's action. Note that for numeric marking, specifying
# assignment action is a matter of convenience, not necessity, as it can
# be emulated by fully subtracting the present codomain values and adding
# the numbers computed by the transition to them. Assignment action flag
# is a matter of necessity only when codomain marking involves objects
# not supporting subtraction/addition (which is out of the scope of Petri's
# original specification anyway.)
#
def assignment_action?; @assignment_action end
alias :assignment? :assignment_action?
# Result of the transition's "function", regardless of the #enabled? status.
#
def action Δt=nil
raise ArgumentError, "Δtime argument required for timed transitions!" if
timed? and Δt.nil?
# the code here looks awkward, because I was trying to speed it up
if has_rate? then
if stoichiometric? then
rate = rate_closure.( *domain_marking )
stoichiometry.map { |coeff| rate * coeff * Δt }
else # assuming correct return value arity from the rate closure:
rate_closure.( *domain_marking ).map { |e| component * Δt }
end
else # rateless
if timed? then
if stoichiometric? then
rslt = action_closure.( Δt, *domain_marking )
stoichiometry.map { |coeff| rslt * coeff }
else
action_closure.( Δt, *domain_marking ) # caveat result arity!
end
else # timeless
if stoichiometric? then
rslt = action_closure.( *domain_marking )
stoichiometry.map { |coeff| rslt * coeff }
else
action_closure.( *domain_marking ) # caveat result arity!
end
end
end
end # action
# Zero action
#
def zero_action
codomain.map { 0 }
end
# Changes to the marking of codomain, as they would happen if #fire! was
# called right now (ie. honoring #enabled?, but not #cocked? status.
#
def action_after_feasibility_check( Δt=nil )
raise AErr, "Δtime argument required for timed transitions!" if
timed? and Δt.nil?
act = Array( action Δt )
# Assignment actions are always feasible - no need to check:
return act if assignment?
# check if the marking after the action would still be positive
enabled = codomain
.zip( act )
.all? { |place, change| place.marking.to_f >= -change.to_f }
if enabled then act else
raise "firing of #{self}#{ Δt ? ' with Δtime %s' % Δt : '' } " +
"would result in negative marking"
zero_action
end
# LATER: This use of #zip here should be avoided for speed
end
# Applies transition's action (adding/taking tokens) on its downstream
# places (aka. domain places). If the transition is timed, delta time has
# to be supplied as argument. In order for this method to work, the
# transition has to be cocked (#cock method), and firing uncocks the
# transition, so it has to be cocked again before it can be fired for
# the second time. If the transition is not cocked, this method has no
# effect.
#
def fire( Δt=nil )
raise ArgumentError, "Δtime argument required for timed transitions!" if
timed? and Δt.nil?
return false unless cocked?
uncock
fire! Δt
return true
end
# Fires the transition just like #fire method, but disregards the cocked /
# uncocked state of the transition.
#
def fire!( Δt=nil )
raise ArgumentError, "Δt required for timed transitions!" if
Δt.nil? if timed?
try "to fire" do
if assignment_action? then
note has: "assignment action"
act = note "action", is: Array( action( Δt ) )
codomain.each_with_index do |place, i|
"place #{place}".try "to assign marking #{i}" do
place.marking = act[i]
end
end
else
act = note "action", is: action_after_feasibility_check( Δt )
codomain.each_with_index do |place, i|
"place #{place}".try "to assign marking #{i}" do
place.add act[i]
end
end
end
end
return nil
end
# Sanity of execution is ensured by Petri's notion of transitions being
# "enabled" if and only if the intended action can immediately take
# place without getting places into forbidden state (negative marking).
#
def enabled?( Δt=nil )
fail ArgumentError, "Δtime argument compulsory for timed transitions!" if
timed? && Δt.nil?
codomain.zip( action Δt ).all? do |place, change|
begin
place.guard.( place.marking + change )
rescue YPetri::GuardError
false
end
end
end
# def lock
# # LATER
# end
# alias :disable! :force_disabled
# def unlock
# # LATER
# end
# alias :undisable! :remove_force_disabled
# def force_enabled!( boolean )
# # true - the transition is always regarded as enabled
# # false - the status is removed
# # LATER
# end
# def clamp
# # LATER
# end
# def remove_clamp
# # LATER
# end
# def reset!
# uncock
# remove_force_disabled
# remove_force_enabled
# remove_clamp
# return self
# end
# Inspect string for a transition.
#
def inspect
to_s
end
# Conversion to a string.
#
def to_s
"#" %
"#{name.nil? ? '' : '%s ' % name }(#{basic_type}%s)%s" %
[ "#{assignment_action? ? ' Assign.' : ''}",
"#{name.nil? ? ' id:%s' % object_id : ''}" ]
end
end # class YPetri::Transition