lib/y_petri/transition.rb in y_petri-2.0.3 vs lib/y_petri/transition.rb in y_petri-2.0.7

- old
+ new

@@ -1,893 +1,453 @@ -#encoding: utf-8 +# -*- coding: utf-8 -*- -# Now is a good time to talk about transition classification: +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: # -# STOICHIOMETRIC / NON-STOICHIOMETRIC +# * <b>ts</b> – timeless nonstoichiometric +# * <b>tS</b> – timeless stoichiometric +# * <b>Tsr</b> – timed rateless nonstoichiometric +# * <b>TSr</b> – timed rateless stoichiometric +# * <b>sR</b> – nonstoichiometric with rate +# * <b>SR</b> – 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*. -# -# HAVING / NOT HAVING RATE +# # I. For transitions with rate, the closure result has to be -# multiplied by the time step duration (delta_t) to get action. +# 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. -# -# TIMED / TIMELESS +# 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 <em>action</em>: +# 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 <em>action vector</em>. 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 <b>need to multiply the +# rate/action closure result by stoichiometry</b>. +# +# ==== 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 <b>the need to multiply the closure +# result by delta time</b> - 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. -# II. For timeless transitions, action is time-independent. Timeless +# * 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. +# transitions, timedness distinguishes <b>the need to supply time step +# duration as the first argument to the action closure</b>. # -# ASSIGNMENT TRANSITIONS +# === 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. For numeric types, same -# effect can be achieved by subtracting the old number from the place -# and subsequently adding the new value to it. +# 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. # -module YPetri +# ==== 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 - # Represents a Petri net transition. YPetri transitions come in 6 - # basic types - # - # === Basic transition types + 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. # - # * <b>ts</b> – timeless nonstoichiometric - # * <b>tS</b> – timeless stoichiometric - # * <b>Tsr</b> – timed rateless nonstoichiometric - # * <b>TSr</b> – timed rateless stoichiometric - # * <b>sR</b> – nonstoichiometric with rate - # * <b>SR</b> – stoichiometric with rate - # - # These 6 kinds of YPetri transitions correspond to the vertices - # of a cube with 3 dimensions: + 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. # - # - stoichiometric (S) / nonstoichiometric (s) - # - timed (T) / timeless (t) - # - having rate (R) / not having rate (r) + 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? # - # Since transitions with rate are always timed, and vice-versa, timeless - # transitions cannot have rate, there are only 6 permissible combinations, - # mentioned 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 <em>action</em>: - # 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 <em>action vector</em>. 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. + def stoichiometric?; @stoichiometric end + alias :s? :stoichiometric? + + # Is the transition nonstoichiometric? (Opposite of #stoichiometric?) # - # Conclusion: stoichiometricity distinguishes <b>need to multiply the - # rate/action closure result by stoichiometry</b>. - # - # ==== 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 <b>the need to multiply the closure - # result by delta time</b> - 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. + def nonstoichiometric? + not stoichiometric? + end + + # Stoichiometry (implies that the transition is stoichiometric). # - # Conclusion: Transitions with rate are always timed. In rateless - # transitions, timedness distinguishes <b>the need to supply time step - # duration as the first argument to the action closure</b>. - # - # === 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. + attr_reader :stoichiometry + + # Stoichiometry as a hash of pairs: + # { codomain_place_instance => stoichiometric_coefficient } # - class Transition - include NameMagic + def stoichio + Hash[ codomain.zip( @stoichiometry ) ] + end - 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" - } + # Stoichiometry as a hash of pairs: + # { codomain_place_name_symbol => stoichiometric_coefficient } + # + def s + stoichio.with_keys { |k| k.name || k.object_id } + end - # 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 + # Does the transition have rate? + # + def has_rate? + @has_rate + end - # Names of upstream places. - # - def domain_pp; domain.map &:name end - alias :upstream_pp :domain_pp + # Is the transition rateless? + # + def rateless? + not has_rate? + end - # 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 + # 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 - # Names of downstream places. - # - def codomain_pp; codomain.map &:name end - alias :downstream_pp :codomain_pp + # 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 - # Union of action arcs and test arcs. - # - def arcs; domain | codomain end + # Does the transition's action depend on delta time? + # + def timed? + @timed + end - # Returns names of the (places connected to) the transition's arcs. - # - def aa; arcs.map &:name end + # Is the transition timeless? (Opposite of #timed?) + # + def timeless? + not timed? + end - # Is the transition stoichiometric? - # - def stoichiometric?; @stoichiometric end - alias :s? :stoichiometric? + # 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 - # Is the transition nonstoichiometric? (Opposite of #stoichiometric?) - # - def nonstoichiometric?; not stoichiometric? end + # Opposite of #functional? + # + def functionless? + not functional? + end - # Stoichiometry (implies that the transition is stoichiometric). - # - attr_reader :stoichiometry + # 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 - # Stoichiometry as a hash of pairs: - # { codomain_place_instance => stoichiometric_coefficient } - # - def stoichio; Hash[ codomain.zip( @stoichiometry ) ] end + # Reports transition's type (basic type + whether it's an assignment + # transition). + # + def type + assignment_action? ? "A(ts)" : basic_type + end - # Stoichiometry as a hash of pairs: - # { codomain_place_name_symbol => stoichiometric_coefficient } - # - def s; stoichio.with_keys { |k| k.name.to_sym } 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? - # 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 transition membership in one of 6 basic types of YPetri transitions: - # 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? - - # Is the transition cocked? - # - # The transition has to be cocked before #fire method can be called - # successfully. (Can be overriden using #fire! method.) - # - def cocked?; @cocked end - - # Opposite of #cocked? - # - def uncocked?; not cocked? end - - # As you could have noted in the introduction, Transition class encompasses - # all different kinds of Petri net transitions. This is considered a good - # design pattern for cases like this, but it makes the transition class and - # its constructor look a bit complicated. Should you feel that way, please - # remember that you only learn one constructor, but can create many kinds - # of transition – the computer is doing a lot of work behind the scenes for - # you. The type of a transition created depends on the qualities of supplied - # arguments. However, you can also explicitly specify what kind of - # transition do you want, to exclude any ambiguity. - # - # Whatever arguments you supply, the constructor will always need a way to - # determine domain (upstream arcs) and codomain (downstream arcs) of your - # transitions, implicitly or explicitly. Secondly, the constructor must - # have a way to determine the transition's action, although there is more - # than one way of doing so. So enough talking and onto the examples. We - # will imagine having 3 places A, B, C, for which we will create various - # transitions: - # - # ==== Timeless nonstoichiometric (ts) transitions - # Action closure has to be supplied, whose return arity correspons to - # the codomain size. - # <tt> - # Transition.new upstream_arcs: [A, C], downstream_arcs: [A, B], - # action_closure: proc { |m, x| - # if x > 0 then [-(m / 2), (m / 2)] - # else [1, 0] end } - # </tt> - # (This represents a transition connected by arcs to places A, B, C, whose - # operation depends on C in such way, that if C.marking is positive, - # then half of the marking of A is shifted to B, while if C.marking is - # nonpositive, 1 is added to A.) - # - # ==== Timeless stoichiometric (tS) transitions - # Stochiometry has to be supplied, with optional action closure. - # Action closure return arity should be 1 (its result will be multiplied - # by the stoichiometry vector). - # - # If no action closure is given, a <em>functionless</em> transition will - # be created, whose action closure will be by default 1 * stoichiometry - # vector. - # - # ==== Timed rateless nonstoichiometric (Tsr) transitions - # Action closure has to be supplied, whose first argument is Δt, and the - # remaining arguments correspond to the domain size. Return arity of this - # closure should correspond to the codomain size. - # - # ==== Timed rateless stoichiometric (TSr) transitions - # Action closure has to be supplied, whose first argument is Δt, and the - # remaining arguments correspond to the domain size. Return arity of this - # closure should be 1 (to be multiplied by the stoichiometry vector). - # - # ==== Nonstoichiometric transitions with rate (sR) - # Rate closure has to be supplied, whose arity should correspond to the - # domain size (Δt argument is not needed). Return arity of this closure - # should correspond to the codomain size and represents rate of change - # contribution for marking of the codomain places. - # - # ==== Stoichiometric transitions with rate (SR) - # - # Rate closure and stoichiometry has to be supplied, whose arity should - # correspond to the domain size. Return arity of this closure should be 1 - # (to be multiplied by the stoichiometry vector, as in all stoichiometric - # transitions). - # - # <tt>Transition( stoichiometry: { A: -1, B: 1 }, - # rate: λ { |a| a * 0.5 } ) - # - def initialize *args - check_in_arguments *args # the big work of checking in args - inform_upstream_places # that they have been connected - inform_downstream_places # that they have been connected - @cocked = false # transitions initialize uncocked - end - - # Marking of the domain places. - # - def domain_marking; domain.map &:marking end - - # Marking of the codomain places. - # - def codomain_marking; codomain.map &:marking end - - # 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 + # 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 - 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 } + rslt = action_closure.( Δt, *domain_marking ) + stoichiometry.map { |coeff| rslt * coeff } + else + action_closure.( Δt, *domain_marking ) # caveat result arity! 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 + 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 # action - - # Zero action - # - def zero_action - codomain.map { 0 } end + end # action - # 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 + # Zero action + # + def zero_action + codomain.map { 0 } + end - # Allows #fire method to succeed. (#fire! disregards cocking.) - # - def cock; @cocked = true end - alias :cock! :cock - - # Uncocks a cocked transition without firing it. - # - def uncock; @cocked = false end - alias :uncock! :uncock - - # If #fire method of a transition applies its action (token adding/taking) - # on its domain, depending on codomain marking. Time step is expected as - # argument if the transition is timed. Only works if the transition has - # been cocked and causes the transition to uncock. - # - def fire( Δt=nil ) - raise AErr, "Δtime argument required for timed transitions!" if - timed? and Δt.nil? - return false unless cocked? - uncock - fire! Δt - return true + # 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 - # Fires the transition regardless of cocked/uncocked status. - # - def fire!( Δt=nil ) - raise AErr, "Δtime required for timed transitions!" if timed? && Δt.nil? + # 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 - act = Array action( Δt ) + note has: "assignment action" + act = note "action", is: Array( action( Δt ) ) codomain.each_with_index do |place, i| - place.marking = act[i] + "place #{place}".try "to assign marking #{i}" do + place.marking = act[i] + end end else - act = action_after_feasibility_check( Δt ) + act = note "action", is: action_after_feasibility_check( Δt ) codomain.each_with_index do |place, i| - place.add act[i] - 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 ) - raise AErr, "Δtime argument required for timed transitions!" if - timed? and Δt.nil? - codomain - .zip( action Δt ) - .all? { |place, change| place.marking.to_f >= -change.to_f } - end - - # Recursive firing of the upstream net portion (honors #cocked?). - # - def fire_upstream_recursively - return false unless cocked? - uncock - upstream_places.each &:fire_upstream_recursively - fire! - return true - end - - # Recursive firing of the downstream net portion (honors #cocked?). - # - def fire_downstream_recursively - return false unless cocked? - uncock - fire! - downstream_places.each &:fire_downstream_recursively - return true - 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 - "#<Transition: %s >" % - "#{name.nil? ? '' : '%s ' % name }(#{basic_type}%s)%s" % - [ "#{assignment_action? ? ' Assign.' : ''}", - "#{name.nil? ? ' id:%s' % object_id : ''}" ] - end - - private - - # ********************************************************************** - # ARGUMENT CHECK-IN UPON INITIALIZATION - # ********************************************************************** - - # Checking in the arguments supplied to #initialize looks like a big job. - # I won't contest to that, but let us not, that it is basically nothing - # else then defining the duck type of the input argument collection. - # TypeError is therefore raised if invalid collection has been supplied. - # - def check_in_arguments *args - oo = args.extract_options! - oo.may_have :stoichiometry, syn!: [ :stoichio, :s ] - oo.may_have :codomain, syn!: [ :codomain_arcs, :codomain_places, - :downstream, - :downstream_arcs, :downstream_places, - :action_arcs ] - oo.may_have :domain, syn!: [ :domain_arcs, :domain_places, - :upstream, :upstream_arcs, :upstream_places ] - oo.may_have :rate, syn!: [ :rate_closure, - :propensity, :propensity_closure ] - oo.may_have :action, syn!: :action_closure - oo.may_have :timed - - @has_rate = oo.has? :rate # was the rate was given? - - # is the transition stoichiometric (S) or nonstoichiometric (s)? - @stoichiometric = oo.has? :stoichiometry - - # downstream description arguments: codomain, stoichiometry (if S) - if stoichiometric? then - @codomain, @stoichiometry = check_in_downstream_description_for_S( oo ) - else # s transitions have no stoichiometry - @codomain = check_in_downstream_description_for_s( oo ) - end - - # check in domain first, :missing symbol may appear - @domain = check_in_domain( oo ) - - # upstream description arguments; also takes care of :missing domain - if has_rate? then - @domain, @rate_closure, @timed, @functional = - check_in_upstream_description_for_R( oo ) - else - @domain, @action_closure, @timed, @functional = - check_in_upstream_description_for_r( oo ) - end - - # optional assignment action: - @assignment_action = check_in_assignment_action( oo ) - end # def check_in_arguments - - # Makes sure that supplied collection consists only of appropriate places. - # Second optional argument customizes the error message. - # - def sanitize_place_collection place_collection, what_is_collection=nil - c = what_is_collection ? what_is_collection.capitalize : "Collection" - Array( place_collection ).map do |pl_id| - begin - place( pl_id ) - rescue NameError - raise TypeError, "#{c} member #{pl_id} does not specify a valid place!" - end - end.aT what_is_collection, "not contain duplicate places" do |collection| - collection == collection.uniq - end - end - - # Private method, part of #initialize argument checking-in. - # - def check_in_domain( oo ) - if oo.has? :domain then - sanitize_place_collection( oo[:domain], "supplied domain" ) - else - if stoichiometric? then - # take arcs with non-positive stoichiometry coefficients - Hash[ [ @codomain, @stoichiometry ].transpose ] - .delete_if{ |place, coeff| coeff > 0 }.keys - else - :missing - # Barring the caller's error, missing domain can mean: - # 1. empty domain - # 2. domain == codomain - # This will be figured later by rate/action closure arity - end - end - end - - def check_in_upstream_description_for_R( oo ) - _domain = domain # this method may modify domain - # check against colliding :action argument - raise TErr, "Rate & action are mutually exclusive!" if oo.has? :action - # lets figure the rate closure - rate_λ = case rate_arg = oo[:rate] - when Proc then # We received the closure directly, - # but we've to be concerned about missing domain. - if domain == :missing then # we've to figure user's intent - _domain = if rate_arg.arity == 0 then - [] # user meant empty domain - else - codomain # user meant domain same as codomain - end - else # domain not missing - raise TErr, "Rate closure arity (#{rate_arg.arity}) " + - "greater than domain size (#{domain.size})!" unless - rate_arg.arity.abs <= domain.size - end - rate_arg - else # We received something else, - # we must make assumption user's intent. - if stoichiometric? then # user's intent was mass action - raise TErr, "When a number is supplied as rate, domain " + - "must not be given!" if oo.has? :domain - construct_standard_mass_action( rate_arg ) - else # user's intent was constant closure - raise TErr, "When rate is a number and no stoichiometry " + - "is supplied, codomain size must be 1!" unless - codomain.size == 1 - # Missing domain is OK here, - _domain = [] if domain == :missing - # but if it was supplied explicitly, it must be empty. - raise TErr, "Rate is a number, but non-empty domain was " + - "supplied!" unless domain.empty? if oo.has?( :domain ) - lambda { rate_arg } - end - end - # R transitions are implicitly timed - _timed = true - # check against colliding :timed argument - oo[:timed].tE :timed, "not be false if rate given" if oo.has? :timed - # R transitions are implicitly functional - _functional = true - return _domain, rate_λ, _timed, _functional - end - - def check_in_upstream_description_for_r( oo ) - _domain = domain # this method may modify domain - _functional = true - # was action closure was given explicitly? - if oo.has? :action then - action_λ = oo[:action].aT_is_a Proc, "supplied action named argument" - if oo.has? :timed then - _timed = oo[:timed] - # Time to worry about the domain_missing - if domain == :missing then # figure user's intent from closure arity - _domain = if action_λ.arity == ( _timed ? 1 : 0 ) then - [] # user meant empty domain - else - codomain # user meant domain same as codomain - end - else # domain not missing - raise TErr, "Rate closure arity (#{rate_arg.arity}) > domain " + - "size (#{domain.size})!" if action_λ.arity.abs > domain.size + "place #{place}".try "to assign marking #{i}" do + place.add act[i] end - else # :timed argument not supplied - if domain == :missing then - # If no domain was supplied, there is no way to reasonably figure - # out the user's intent, except when arity is 0: - _domain = case action_λ.arity - when 0 then - _timed = false - [] # empty domain is implied - else # no deduction of user intent possible - raise AErr, "Too much ambiguity: Neither domain nor " + - "timedness of the rateless transition was specified." - end - else # domain not missing - # Even if the user did not bother to inform us explicitly about - # timedness, we can use closure arity as a clue. If it equals the - # domain size, leaving no room for Δtime argument, the user intent - # was to create timeless transition. If it equals domain size + 1, - # theu user intended to create a timed transition. - _timed = case action_λ.arity - when domain.size then false - when domain.size + 1 then true - else # no deduction of user intent possible - raise AErr, "Timedness was not specified, and the " + - "arity of the action supplied action closure " + - "(#{action_λ.arity}) does not give clear hint on it." - end - end end - else # rateless cases with no action closure specified - # Assumption must be made on transition's action. In particular, - # lambda { 1 } action closure will be assumed, - action_λ = lambda { 1 } - # and it will be required that the transition be stoichiometric and - # timeless. Domain will thus be required empty. - raise AErr, "Stoichiometry is compulsory, if rate/action was " + - "not supplied." unless stoichiometric? - # With this, we can drop worries about missing domain. - raise AErr, "When no rate/action is supplied, the transition can't " + - "be declared timed." if oo[:timed] if oo.has? :timed - _timed = false - _domain = [] - _functional = false # the transition is considered functionless end - return _domain, action_λ, _timed, _functional end + return nil + end - def construct_standard_mass_action( num ) - # assume standard mass-action law - nonpositive_coeffs = stoichiometry.select { |coeff| coeff <= 0 } - # the closure takes markings of the domain as its arguments - lambda { |*markings| - nonpositive_coeffs.size.times.reduce num do |acc, i| - marking, coeff = markings[ i ], nonpositive_coeffs[ i ] - # Stoichiometry coefficients equal to zero are taken to indicate - # plain factors, assuming that if these places were not involved - # in the transition at all, the user would not be mentioning them. - case coeff - when 0, -1 then marking * acc - else marking ** -coeff end - end - } - end - - # Private method, checking in downstream specification from the argument - # field for stoichiometric transition. - # - def check_in_downstream_description_for_S( oo ) - codomain, stoichio = - case oo[:stoichiometry] - when Hash then - # contains pairs { codomain place => stoichiometry coefficient } - raise AErr, "With hash-type stoichiometry, :codomain named " + - "argument must not be supplied." if oo.has? :codomain - oo[:stoichiometry].each_with_object [[], []] do |pair, memo| - codomain_place, stoichio_coeff = pair - memo[0] << codomain_place - memo[1] << stoichio_coeff - end - else - # array of stoichiometry coefficients - raise AErr, "With array-type stoichiometry, :codomain named " + - "argument must be supplied." unless oo.has? :codomain - [ oo[:codomain], Array( oo[:stoichiometry] ) ] - end - # enforce that stoichiometry is a collection of numbers - return sanitize_place_collection( codomain, "supplied codomain" ), - stoichio.aT_all_numeric( "supplied stoichiometry" ) - end - - # Private method, checking in downstream specification from the argument - # field for nonstoichiometric transition. - # - def check_in_downstream_description_for_s( oo ) - # codomain must be explicitly given - no way around it: - raise AErr, "For non-stoichiometric transitions, :codomain named " + - "argument is compulsory." unless oo.has? :codomain - return sanitize_place_collection( oo[:codomain], "supplied codomain" ) - end - - # Private method, part of #initialize argument checking-in. - # - def check_in_assignment_action( oo ) - if oo.has? :assignment_action, syn!: [ :assignment, :assign, :A ] then - if timed? then - msg = "Timed transitions may not have assignment action!" - raise TypeError, msg if oo[:assignment_action] - false - else # timeless transitions are eligible for assignment action - oo[:assignment_action] - end - else # if assignment action is not specified, false is + # 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 - # Informs upstream places that they are connected to this transition. - # - def inform_upstream_places - upstream_places.each { |p| p.send :register_downstream_transition, self } - end + # def lock + # # LATER + # end + # alias :disable! :force_disabled - # Informs downstream places that they are connected to this transition. - # - def inform_downstream_places - downstream_places.each { |p| p.send :register_upstream_transition, self } - end + # def unlock + # # LATER + # end + # alias :undisable! :remove_force_disabled - # Place class pertinent herein. Provided for the purpose of parametrized - # subclassing; expected to be overriden in the subclasses. - # - def Place - ::YPetri::Place - end + # def force_enabled!( boolean ) + # # true - the transition is always regarded as enabled + # # false - the status is removed + # # LATER + # end - # Transition class pertinent herein. Provided for the purpose of - # parametrized subclassing; expected to be overriden in the subclasses. - # - def Transition - ::YPetri::Transition - end + # def clamp + # # LATER + # end - # Net class pertinent herein. Provided for the purpose of parametrized - # subclassing; expected to be overriden in the subclasses. - # - def Net - ::YPetri::Net - end + # def remove_clamp + # # LATER + # end - # Presents Place instance specified by the argument. - # - def place instance_identifier - Place().instance( instance_identifier ) - end + # def reset! + # uncock + # remove_force_disabled + # remove_force_enabled + # remove_clamp + # return self + # end - # Presents Transition instance specified by the argument. - # - def transition instance_identifier - Transition().instance( instance_identifier ) - end + # Inspect string for a transition. + # + def inspect + to_s + end - # Presents Net instance specified by the argument. - # - def net instance_identifier - Net().instance( instance_identifier ) - end - end # class Transition -end # module YPetri + # Conversion to a string. + # + def to_s + "#<Transition: %s >" % + "#{name.nil? ? '' : '%s ' % name }(#{basic_type}%s)%s" % + [ "#{assignment_action? ? ' Assign.' : ''}", + "#{name.nil? ? ' id:%s' % object_id : ''}" ] + end +end # class YPetri::Transition