From e5d7bc6a6d544b73f6d4ac975d8a469efe9301a9 Mon Sep 17 00:00:00 2001 From: Bernard Lambeau Date: Mon, 23 Jul 2012 12:03:04 +0200 Subject: [PATCH] Refactor Utils::Decorate to accept an output object. --- CHANGELOG.md | 3 +- .../stamina/automaton/equivalence.rb | 19 ++- lib/stamina-core/stamina/automaton/metrics.rb | 20 +-- lib/stamina-core/stamina/utils/decorate.rb | 145 +++++++++++------- .../stamina/reg_lang/canonical_info.rb | 18 ++- test/unit/utils/decorate_test.rb | 30 ++-- 6 files changed, 138 insertions(+), 97 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3350400..1a74ef0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # 0.5.5 / FIX ME -* Generalized the decoration algorithm to work backwards as well as forwards. +* Generalized the decoration algorithm to work backwards as well as forwards as well as + accepting an output Hash-like object, defaulting to true state decorations. # O.5.4 / 2012-03-06 diff --git a/lib/stamina-core/stamina/automaton/equivalence.rb b/lib/stamina-core/stamina/automaton/equivalence.rb index 9e7f5b9..2eea444 100644 --- a/lib/stamina-core/stamina/automaton/equivalence.rb +++ b/lib/stamina-core/stamina/automaton/equivalence.rb @@ -26,30 +26,29 @@ def equivalent?(other, equiv = nil, key = :equiv_state) # * suppremum is identity and fails when the equivalent state is not unique # * propagation checks transition function delta # - algo = Stamina::Utils::Decorate.new(key) + algo = Stamina::Utils::Decorate.new algo.set_suppremum do |d0, d1| if (d0.nil? or d1.nil?) (d0 || d1) - elsif d0==d1 - d0 else - raise Stamina::Abord + throw :non_equivalent unless d0==d1 + d0 end end + algo.set_initiator{|s| s.initial? ? other.initial_state.index : nil} + algo.set_start_predicate{|s| s.initial? } algo.set_propagate do |d,e| reached = other.ith_state(d).dfa_step(e.symbol) - raise Stamina::Abord if reached.nil? - raise Stamina::Abord unless equiv[e.target, reached] + throw :non_equivalent unless reached && equiv[e.target, reached] reached.index end # Run the algorithm now - begin - algo.execute(self, nil, other.initial_state.index) + catch(:non_equivalent) do + algo.call(self, key) return true - rescue Stamina::Abord - return false end + return false end alias :<=> :equivalent? diff --git a/lib/stamina-core/stamina/automaton/metrics.rb b/lib/stamina-core/stamina/automaton/metrics.rb index 236e76b..ba7a1a7 100644 --- a/lib/stamina-core/stamina/automaton/metrics.rb +++ b/lib/stamina-core/stamina/automaton/metrics.rb @@ -49,23 +49,17 @@ def error_ratio # each state as a mark under _key_, which defaults to :depth. # def depth(key = :depth) - algo = Stamina::Utils::Decorate.new(key) + algo = Stamina::Utils::Decorate.new algo.set_suppremum do |d0,d1| - # Here, unreached state has the max value (i.e. nil is +INF) - # and we look at the minimum depth for each state - if d0.nil? - d1 - elsif d1.nil? - d0 - else - (d0 <= d1 ? d0 : d1) - end + # Unreached state is MAX (i.e. nil is +INF); we look at the min depth for each state + (d0.nil? or d1.nil?) ? (d0 || d1) : (d0 <= d1 ? d0 : d1) end algo.set_propagate{|d,e| d+1 } - algo.execute(self, nil, 0) + algo.set_initiator{|s| s.initial? ? 0 : nil } + algo.set_start_predicate{|s| s.initial? } + algo.call(self, key) deepest = states.max do |s0,s1| - # Here, we do not take unreachable states into account - # so that -1 is taken when nil is encountered + # do not take unreachable states into account: -1 is taken if nil is encountered (s0[:depth] || -1) <=> (s1[:depth] || -1) end deepest[:depth] diff --git a/lib/stamina-core/stamina/utils/decorate.rb b/lib/stamina-core/stamina/utils/decorate.rb index a29822a..88ed61c 100644 --- a/lib/stamina-core/stamina/utils/decorate.rb +++ b/lib/stamina-core/stamina/utils/decorate.rb @@ -1,25 +1,35 @@ module Stamina module Utils # - # Decorates states of an automaton by applying a propagation rule - # until a fix point is reached. + # Decorates states of an automaton by applying a propagation rule until a fix point is + # reached. # class Decorate - # The key to use to maintain the decoration on states (:invariant - # is used by default) - attr_writer :decoration_key - # Creates a decoration algorithm instance - def initialize(decoration_key = :invariant) - @decoration_key = decoration_key + def initialize(output = :invariant) + @output = Decorate.state_output(output) if output @suppremum = nil @propagate = nil @initiator = nil + @backward = false @start_predicate = nil - @backward = false end + # Builds an output hash that keeps decoration in states + def self.state_output(decoration_key) + Object.new.extend Module.new{ + define_method :[] do |state| + state[decoration_key] + end + define_method :[]= do |state,deco| + state[decoration_key] = deco + end + } + end + + ### CONFIGURATION ################################################################## + # Installs a suppremum function through a block. def set_suppremum(&block) raise ArgumentError, 'Suppremum expected through a block' if block.nil? @@ -32,16 +42,6 @@ def suppremum=(proc) set_suppremum(&proc) end - # Computes the suppremum between two decorations. By default, this method - # looks for a suppremum function installed with set_suppremum. If not found, - # it tries calling a suppremum method on d0. If not found it raises an error. - # This method may be overriden. - def suppremum(d0, d1) - return @suppremum.call(d0, d1) if @suppremum - return d0.suppremum(d1) if d0.respond_to?(:suppremum) - raise "No suppremum function installed or implemented by decorations" - end - # Installs a propagate function through a block. def set_propagate(&block) raise ArgumentError, 'Propagate expected through a block' if block.nil? @@ -54,16 +54,6 @@ def propagate=(proc) set_propagate(&proc) end - # Computes the propagation rule. By default, this method looks for a propagate - # function installed with set_propagate. If not found, it tries calling a + - # method on deco. If not found it raises an error. - # This method may be overriden. - def propagate(deco, edge) - return @propagate.call(deco, edge) if @propagate - return deco.+(edge) if deco.respond_to?(:+) - raise "No propagate function installed or implemented by decorations" - end - # Set an initiator methods, responsible of computing the initial decoration of # each state def set_initiator(&block) @@ -77,12 +67,6 @@ def initiator=(proc) set_initiator(&proc) end - # Returns the initial decoration of state `s` - def init_deco(s) - return @initiator.call(s) if @initiator - raise "No initiator function installed" - end - # Sets the start predicate to use def set_start_predicate(&block) raise ArgumentError, 'Start predicate expected through a block' if block.nil? @@ -95,50 +79,101 @@ def start_predicate=(proc) set_start_predicate(&proc) end + # Sets if the algorithms works backward + def backward=(val) + @backward = val + end + + ### SUBCLASS HOOKS ################################################################# + + # Computes the suppremum between two decorations. By default, this method + # looks for a suppremum function installed with set_suppremum. If not found, + # it tries calling a suppremum method on d0. If not found it raises an error. + # This method may be overriden. + def suppremum(d0, d1) + return @suppremum.call(d0, d1) if @suppremum + return d0.suppremum(d1) if d0.respond_to?(:suppremum) + raise "No suppremum function installed or implemented by decorations" + end + + # Computes the propagation rule. By default, this method looks for a propagate + # function installed with set_propagate. If not found, it tries calling a + + # method on deco. If not found it raises an error. + # This method may be overriden. + def propagate(deco, edge) + return @propagate.call(deco, edge) if @propagate + return deco.+(edge) if deco.respond_to?(:+) + raise "No propagate function installed or implemented by decorations" + end + + # Returns the initial decoration of state `s` + def init_deco(s) + return @initiator.call(s) if @initiator + raise "No initiator function installed" + end + # Returns the start predicate def take_at_start?(s) return @start_predicate.call(s) if @start_predicate raise "No start predicate function installed" end - # Sets if the algorithms works backward - def backward=(val) - @backward = val - end - # Work backward? def backward? @backward end + ### MAIN ########################################################################### + # Executes the propagation algorithm on a given automaton. - def call(fa) - fa.states.each do |s| - s[@decoration_key] = init_deco(s) - end - to_explore = fa.states.select{|s| take_at_start?(s)} - until to_explore.empty? - source = to_explore.pop - (backward? ? source.in_edges : source.out_edges).each do |edge| - target = backward? ? edge.source : edge.target - p_decor = propagate(source[@decoration_key], edge) - p_decor = suppremum(target[@decoration_key], p_decor) - unless p_decor == target[@decoration_key] - target[@decoration_key] = p_decor - to_explore << target unless to_explore.include?(target) + def call(fa, out = nil) + with_output(out) do |output| + fa.states.each{|s| output[s] = init_deco(s) } # Init decoration on each state + to_explore = fa.states.select{|s| take_at_start?(s)} # Init to_explore (start predicate) + until to_explore.empty? # empty to_explore now + source = to_explore.pop + each_edge_and_target(source) do |edge, target| + p_decor = propagate(output[source], edge) + p_decor = suppremum(output[target], p_decor) + unless p_decor == output[target] + output[target] = p_decor + to_explore << target unless to_explore.include?(target) + end end end + fa end - fa end # Executes the propagation algorithm on a given automaton. def execute(fa, bottom, d0) + warn "Decorate#execute is deprecated, use Decorate#call (#{caller[0]})" self.initiator = lambda{|s| (s.initial? ? d0 : bottom)} self.start_predicate = lambda{|s| s.initial? } call(fa) end + private + + def with_output(out) + if out + yield out.is_a?(Symbol) ? Decorate.state_output(out) : out + elsif @output + warn "Decorate.new(:decokey) is deprecated, use Decorate#call(fa, :decokey) (#{caller[1]})" + yield @output + else + raise ArgumentError, "Output may not be nil", caller + end + end + + def each_edge_and_target(source) + edges = backward? ? source.in_edges : source.out_edges + edges.each do |edge| + target = target = backward? ? edge.source : edge.target + yield edge, target + end + end + end # class Decorate end # module Utils end # module Stamina \ No newline at end of file diff --git a/lib/stamina-induction/stamina/reg_lang/canonical_info.rb b/lib/stamina-induction/stamina/reg_lang/canonical_info.rb index 3a65bb3..95c35e4 100644 --- a/lib/stamina-induction/stamina/reg_lang/canonical_info.rb +++ b/lib/stamina-induction/stamina/reg_lang/canonical_info.rb @@ -2,19 +2,23 @@ module Stamina class RegLang class CanonicalInfo - SHORT_PREFIXES = begin - algo = Stamina::Utils::Decorate.new(:short_prefix) - algo.set_suppremum do |d0,d1| + class ShortPrefixes < Utils::Decorate + def suppremum(d0, d1) if (d0.nil? || d1.nil?) (d0 || d1) else - d0.size <= d1.size ? d0 : d1 + (d0.size <= d1.size) ? d0 : d1 end end - algo.set_propagate do |deco, edge| + def propagate(deco, edge) deco.dup << edge.symbol end - algo + def init_deco(s) + s.initial? ? [] : nil + end + def take_at_start?(s) + s.initial? + end end attr_reader :cdfa @@ -105,7 +109,7 @@ def characteristic_sample # Ensures that short prefixes of states are recognized def prefixes! unless defined?(@prefixes) - SHORT_PREFIXES.execute(cdfa, nil, []) + ShortPrefixes.new.call(cdfa, :short_prefix) @prefixes = true end end diff --git a/test/unit/utils/decorate_test.rb b/test/unit/utils/decorate_test.rb index 194b7cc..162ca58 100644 --- a/test/unit/utils/decorate_test.rb +++ b/test/unit/utils/decorate_test.rb @@ -4,16 +4,22 @@ module Utils class DecorateTest < ::Stamina::StaminaTest module Reachability + def init_deco(s) s.initial?; end + def take_at_start?(s) s.initial?; end def suppremum(d0, d1) d0 || d1; end def propagate(deco, edge) deco; end end module Depth + def init_deco(s) s.initial? ? 0 : 1000000; end + def take_at_start?(s) s.initial?; end def suppremum(d0, d1) (d0 < d1 ? d0 : d1) end def propagate(deco, edge) deco+1; end end module ShortPrefix + def init_deco(s) s.initial? ? [] : nil; end + def take_at_start?(s) s.initial?; end def suppremum(d0, d1) return d0 if d1.nil? return d1 if d0.nil? @@ -25,32 +31,34 @@ def propagate(deco, edge) end def test_reachability_on_small_dfa - algo = Stamina::Utils::Decorate.new(:reachable) + algo = Stamina::Utils::Decorate.new algo.set_suppremum {|d0,d1| d0 || d1 } algo.set_propagate {|deco,edge| deco } - algo.execute(@small_dfa, false, true) - assert_equal @small_dfa.states.select {|s| s[:reachable]==true}, @small_dfa.states + algo.set_initiator {|s| s.initial?} + algo.set_start_predicate{|s| s.initial?} + algo.call(@small_dfa, :reachable) + assert_equal @small_dfa.states.select{|s| s[:reachable]==true}, @small_dfa.states - algo = Stamina::Utils::Decorate.new(:reachable) + algo = Stamina::Utils::Decorate.new algo.extend(Reachability) - algo.execute(@small_dfa, false, true) - assert_equal @small_dfa.states.select {|s| s[:reachable]==true}, @small_dfa.states + algo.call(@small_dfa, :reachable) + assert_equal @small_dfa.states.select{|s| s[:reachable]==true}, @small_dfa.states end def test_depth_on_small_dfa - algo = Stamina::Utils::Decorate.new(:depth) + algo = Stamina::Utils::Decorate.new algo.extend(Depth) - algo.execute(@small_dfa, 1000000, 0) + algo.call(@small_dfa, :depth) assert_equal 0, @small_dfa.ith_state(3)[:depth] assert_equal 1, @small_dfa.ith_state(2)[:depth] assert_equal 2, @small_dfa.ith_state(0)[:depth] assert_equal 3, @small_dfa.ith_state(1)[:depth] end - def test_depth_on_small_dfa - algo = Stamina::Utils::Decorate.new(:short_prefix) + def test_short_prefix_on_small_dfa + algo = Stamina::Utils::Decorate.new algo.extend(ShortPrefix) - algo.execute(@small_dfa, nil, []) + algo.call(@small_dfa, :short_prefix) assert_equal [], @small_dfa.ith_state(3)[:short_prefix] assert_equal ['b'], @small_dfa.ith_state(2)[:short_prefix] assert_equal ['b', 'c'], @small_dfa.ith_state(0)[:short_prefix]