From 129a22cfcc7651daa4b11ed52e7870249f6373a5 Mon Sep 17 00:00:00 2001
From: Patrick Simianer
Date: Tue, 16 Sep 2014 10:23:14 +0100
Subject: spring cleaning
---
prototype/grammar.rb | 137 ++++++++++++++++++++++++++++++
prototype/hypergraph.rb | 219 ++++++++++++++++++++++++++++++++++++++++++++++++
prototype/parse.rb | 207 +++++++++++++++++++++++++++++++++++++++++++++
prototype/test_hg.rb | 32 +++++++
prototype/test_parse.rb | 49 +++++++++++
prototype/weaver.rb | 70 ++++++++++++++++
6 files changed, 714 insertions(+)
create mode 100644 prototype/grammar.rb
create mode 100644 prototype/hypergraph.rb
create mode 100644 prototype/parse.rb
create mode 100755 prototype/test_hg.rb
create mode 100755 prototype/test_parse.rb
create mode 100755 prototype/weaver.rb
(limited to 'prototype')
diff --git a/prototype/grammar.rb b/prototype/grammar.rb
new file mode 100644
index 0000000..42ffbc0
--- /dev/null
+++ b/prototype/grammar.rb
@@ -0,0 +1,137 @@
+module Grammar
+
+class T
+ attr_accessor :word
+
+ def initialize word
+ @word = word
+ end
+
+ def to_s
+ @word
+ end
+end
+
+class NT
+ attr_accessor :symbol, :index
+
+ def initialize symbol=nil, index=-1
+ @symbol = symbol
+ @index = index
+ end
+
+ #FIXME? symbol should not contain [, ] or ",
+ # else we're in trouble
+ def from_s s
+ @symbol, @index = s.delete('[]').split ','
+ @symbol.strip!
+ @index = @index.to_i-1
+ end
+
+ def self.from_s s
+ new = NT.new
+ new.from_s s
+ return new
+ end
+
+ #FIXME? indexed by 1
+ def to_s
+ return "[#{@symbol},#{@index+1}]" if @index>=0
+ return "[#{@symbol}]"
+ end
+end
+
+class Rule
+ attr_accessor :lhs, :rhs, :target, :map, :f
+
+ def initialize lhs=NT.new, rhs=[], target=[], map=[], f=SparseVector.new, arity=0
+ @lhs = lhs
+ @rhs = rhs
+ @target = target
+ @map = map
+ @f = f
+ @arity = arity
+ end
+
+ def read_rhs_ s, make_meta=false
+ a = []
+ s.split.map { |x|
+ x.strip!
+ if x[0] == '[' && x[x.size-1] == ']'
+ a << NT.from_s(x)
+ if make_meta
+ @map << a.last.index
+ @arity += 1
+ end
+ else
+ a << T.new(x)
+ end
+ }
+ return a
+ end
+
+ def from_s s
+ lhs, rhs, target, f = splitpipe s, 3
+ @lhs = NT.from_s lhs
+ @rhs = read_rhs_ rhs, true
+ @target = read_rhs_ target
+ @f = (f ? SparseVector.from_kv(f, '=', ' ') : SparseVector.new)
+ end
+
+ def self.from_s s
+ r = Rule.new
+ r.from_s s
+ return r
+ end
+
+ def to_s
+ "#{@lhs.to_s} ||| #{@rhs.map { |x| x.to_s }.join ' '} ||| #{@target.map { |x| x.to_s }.join ' '}"
+ end
+end
+
+class Grammar
+ attr_accessor :rules, :start_nt, :start_t, :flat
+
+ def initialize fn
+ @rules = []; @start_nt = []; @start_t = []; @flat = []
+ n = 0
+ ReadFile.readlines_strip(fn).each_with_index { |s,i|
+ STDERR.write '.'; STDERR.write " #{i+1}\n" if (i+1)%40==0
+ n += 1
+ @rules << Rule.from_s(s)
+ if @rules.last.rhs.first.class == NT
+ @start_nt << @rules.last
+ else
+ if rules.last.arity == 0
+ @flat << @rules.last
+ else
+ @start_t << @rules.last
+ end
+ end
+ }
+ STDERR.write " #{n}\n"
+ end
+
+ def to_s
+ @rules.map { |r| r.to_s }.join "\n"
+ end
+
+ def add_glue
+ @rules.map { |r| r.lhs.symbol }.select { |s| s != 'S' }.uniq.each { |symbol|
+ @rules << Rule.new(NT.new('S'), [NT.new(symbol, 0)], [NT.new(symbol, 0)], [0])
+ @start_nt << @rules.last
+ @rules << Rule.new(NT.new('S'), [NT.new('S', 0), NT.new('X', 1)], [NT.new('S', 0), NT.new('X', 1)], [0, 1])
+ @start_nt << @rules.last
+ }
+ end
+
+ def add_pass_through a
+ a.each { |word|
+ @rules << Rule.new(NT.new('X'), [T.new(word)], [T.new(word)])
+ @flat << @rules.last
+ }
+ end
+end
+
+end #module
+
diff --git a/prototype/hypergraph.rb b/prototype/hypergraph.rb
new file mode 100644
index 0000000..d43d5ee
--- /dev/null
+++ b/prototype/hypergraph.rb
@@ -0,0 +1,219 @@
+require 'zipf'
+require_relative 'grammar'
+
+module HG
+
+class HG::Node
+ attr_accessor :id, :symbol, :left, :right, :outgoing, :incoming, :score
+
+ def initialize id=nil, symbol='', span=[-1,-1], outgoing=[], incoming=[], score=nil
+ @id = id
+ @symbol = symbol
+ @left = span[0]
+ @right = span[1]
+ @outgoing = outgoing
+ @incoming = incoming
+ @score = score
+ end
+
+ def to_s
+ "Node"
+ end
+end
+
+class HG::Hypergraph
+ attr_accessor :nodes, :edges, :nodes_by_id
+
+ def initialize nodes=[], edges=[], nodes_by_id={}
+ @nodes = nodes
+ @edges = edges
+ @nodes_by_id = nodes_by_id
+ @arity_ = nil
+ end
+
+ def arity
+ @arity_ = @edges.map { |e| e.arity }.max if !@arity_
+ return @arity_
+ end
+
+ def reset
+ @edges.each { |e| e.mark = 0 }
+ end
+
+ def to_s
+ "Hypergraph"
+ end
+
+ def to_json weights=nil
+ json_s = "{\n"
+ json_s += "\"weights\":#{(weights ? weights.to_json : '{}')},\n"
+ json_s += "\"nodes\":\n"
+ json_s += "[\n"
+ json_s += @nodes.map { |n|
+ "{ \"id\":#{n.id}, \"cat\":\"#{n.symbol.to_json.slice(1..-1).chomp('"')}\", \"span\":[#{n.left},#{n.right}] }"
+ }.join ",\n"
+ json_s += "\n],\n"
+ json_s += "\"edges\":\n"
+ json_s += "[\n"
+ json_s += @edges.map { |e|
+ "{ \"head\":#{e.head.id}, \"rule\":#{e.rule.to_json}, \"tails\":#{e.tails.map{ |n| n.id }}, \"f\":#{(e.f ? e.f.to_json : '{}')} }"
+ }.join ",\n"
+ json_s += "\n]\n"
+ json_s += "}\n"
+
+ return json_s
+
+ end
+end
+
+class HG::Hyperedge
+ attr_accessor :head, :tails, :score, :f, :mark, :rule
+
+ def initialize head=Node.new, tails=[], score=0.0, f=SparseVector.new, rule=nil
+ @head = head
+ @tails = tails
+ @score = score
+ @f = f
+ @mark = 0
+ @rule = (rule.class==String ? Grammar::Rule.from_s(rule) : rule)
+ end
+
+ def arity
+ return @tails.size
+ end
+
+ def marked?
+ arity == @mark
+ end
+
+ def to_s
+ "Hyperedge"
+ end
+end
+
+def HG::topological_sort nodes
+ sorted = []
+ s = nodes.reject { |n| !n.incoming.empty? }
+ while !s.empty?
+ sorted << s.shift
+ sorted.last.outgoing.each { |e|
+ next if e.marked?
+ e.mark += 1
+ s << e.head if e.head.incoming.reject{ |f| f.marked? }.empty?
+ }
+ end
+ return sorted
+end
+
+def HG::init nodes, semiring, root
+ nodes.each { |n| n.score=semiring.null }
+ root.score = semiring.one
+end
+
+def HG::viterbi hypergraph, root, semiring=ViterbiSemiring.new
+ toposorted = topological_sort hypergraph.nodes
+ init toposorted, semiring, root
+ toposorted.each { |n|
+ n.incoming.each { |e|
+ s = semiring.one
+ e.tails.each { |m|
+ s = semiring.multiply.call(s, m.score)
+ }
+ n.score = semiring.add.call(n.score, semiring.multiply.call(s, e.score))
+ }
+ }
+end
+
+def HG::viterbi_path hypergraph, root, semiring=ViterbiSemiring.new
+ toposorted = topological_sort hypergraph.nodes
+ init toposorted, semiring, root
+ best_path = []
+ toposorted.each { |n|
+ best_edge = nil
+ n.incoming.each { |e|
+ s = semiring.one
+ e.tails.each { |m|
+ s = semiring.multiply.call(s, m.score)
+ }
+ if n.score < semiring.multiply.call(s, e.score) # ViterbiSemiring add
+ best_edge = e
+ end
+ n.score = semiring.add.call(n.score, semiring.multiply.call(s, e.score))
+ }
+ best_path << best_edge if best_edge
+ }
+ return best_path, toposorted.last.score
+end
+
+def HG::k_best hypergraph, root, semiring=nil
+ #TODO
+end
+
+def HG::all_paths hypergraph, root
+ toposorted = topological_sort hypergraph.nodes
+ paths = [[]]
+ toposorted.each { |n|
+ next if n.incoming.empty?
+ new_paths = []
+ while !paths.empty?
+ p = paths.pop
+ n.incoming.each { |e|
+ new_paths << p+[e]
+ }
+ end
+ paths = new_paths
+ }
+ return paths
+end
+
+def HG::derive path, cur, carry
+ edge = path.select { |e| e.head.symbol==cur.symbol \
+ && e.head.left==cur.left \
+ && e.head.right==cur.right }.first
+ j = 0
+ edge.rule.target.each { |i|
+ if i.class == Grammar::NT
+ derive path, edge.tails[edge.rule.map[j]], carry
+ j += 1
+ else
+ carry << i
+ end
+ }
+ return carry
+end
+
+def HG::read_hypergraph_from_json fn, semiring=RealSemiring.new, log_weights=false
+ nodes = []
+ edges = []
+ nodes_by_id = {}
+ h = JSON.parse File.new(fn).read
+ w = SparseVector.from_h h['weights']
+ h['nodes'].each { |x|
+ n = Node.new x['id'], x['symbol'], x['span']
+ nodes << n
+ nodes_by_id[n.id] = n
+ }
+ h['edges'].each { |x|
+ e = Hyperedge.new(nodes_by_id[x['head']], \
+ x['tails'].map { |j| nodes_by_id[j] }.to_a, \
+ (x['score'] ? semiring.convert.call(x['score'].to_f) : nil), \
+ (x['f'] ? SparseVector.from_h(x['f']) : SparseVector.new), \
+ x['rule'])
+ if x['f']
+ if log_weights
+ e.score = Math.exp(w.dot(e.f))
+ else
+ e.score = w.dot(e.f)
+ end
+ end
+ e.tails.each { |m|
+ m.outgoing << e
+ }
+ e.head.incoming << e
+ edges << e
+ }
+ return Hypergraph.new(nodes, edges), nodes_by_id
+end
+
+end #module
+
diff --git a/prototype/parse.rb b/prototype/parse.rb
new file mode 100644
index 0000000..adf2b91
--- /dev/null
+++ b/prototype/parse.rb
@@ -0,0 +1,207 @@
+require 'zipf'
+require_relative 'grammar'
+require_relative 'hypergraph'
+
+module Parse
+
+def Parse::visit i, l, r, x=0
+ i.upto(r-x) { |span|
+ l.upto(r-span) { |k|
+ yield k, k+span
+ }
+ }
+end
+
+class Chart
+
+ def initialize n
+ @n = n
+ @m = []
+ (n+1).times {
+ a = []
+ (n+1).times { a << [] }
+ @m << a
+ }
+ @b = {}
+ end
+
+ def at i, j
+ @m[i][j]
+ end
+
+ def add item, i, j
+ at(i,j) << item
+ @b["#{i},#{j},#{item.lhs.symbol}"] = true
+ end
+
+ def has symbol, i, j
+ return @b["#{i},#{j},#{symbol}"]
+ end
+
+ def to_hg weights=SparseVector.new
+ nodes = []
+ edges = []
+ nodes_by_id = {}
+ nodes << HG::Node.new(-1, "root", [-1,-1])
+ nodes_by_id[-1] = nodes.last
+ id = 0
+ seen = {}
+ Parse::visit(1, 0, @n) { |i,j|
+ self.at(i,j).each { |item|
+ _ = "#{item.lhs.symbol},#{i},#{j}"
+ if !seen[_]
+ nodes << HG::Node.new(id, item.lhs.symbol, [i,j])
+ nodes_by_id[id] = nodes.last
+ seen[_] = id
+ id += 1
+ end
+ }
+ }
+
+ Parse::visit(1, 0, @n) { |i,j|
+ self.at(i,j).each { |item|
+ edges << HG::Hyperedge.new(nodes_by_id[seen[item.lhs.symbol+','+i.to_s+','+j.to_s]], \
+ (item.tail_spans.empty? ? [nodes_by_id[-1]] : item.rhs.zip((0..item.rhs.size-1).map{|q| item.tail_spans[q] }).select{|x| x[0].class==Grammar::NT }.map{|x| nodes_by_id[seen["#{x[0].symbol},#{x[1].left},#{x[1].right}"]]}), \
+ Math.exp(weights.dot(item.f)),
+ item.f,
+ Grammar::Rule.new(item.lhs, item.rhs, item.target, item.map, item.f), \
+ )
+ edges.last.head.incoming << edges.last
+ edges.last.tails.each { |n| n.outgoing << edges.last }
+ }
+ }
+ return HG::Hypergraph.new(nodes, edges, nodes_by_id)
+ end
+end
+
+Span = Struct.new(:left, :right)
+
+class Item < Grammar::Rule
+ attr_accessor :left, :right, :tail_spans, :dot, :f
+
+ def initialize rule_or_item, left, right, dot
+ @lhs = Grammar::NT.new rule_or_item.lhs.symbol, rule_or_item.lhs.index
+ @left = left
+ @right = right
+ @rhs = []
+ @tail_spans = {} # refers to source side, use @map
+ @f = rule_or_item.f
+ @map = (rule_or_item.map ? rule_or_item.map.dup : [])
+ rule_or_item.rhs.each_with_index { |x,i| # duplicate rhs partially
+ @rhs << x
+ if x.class == Grammar::NT
+ begin
+ if i >= dot
+ @tail_spans[i] = Span.new(-1, -1)
+ else
+ @tail_spans[i] = rule_or_item.tail_spans[i].dup
+ end
+ rescue
+ @tail_spans[i] = Span.new(-1, -1)
+ end
+ end
+ }
+ @dot = dot
+ @target = rule_or_item.target
+ end
+
+ def to_s
+ "(#{@left}, #{@right}) [#{tail_spans.map{|k,v| k.to_s+'('+v.left.to_s+','+v.right.to_s+')'}.join ' '}] {#{@map.to_s.delete('[]')}} #{lhs} -> #{rhs.map{|i|i.to_s}.insert(@dot,'*').join ' '} [dot@#{@dot}] ||| #{@target.map{|x|x.to_s}.join ' '}"
+ end
+end
+
+def Parse::init input, n, active_chart, passive_chart, grammar
+ grammar.flat.each { |r|
+ input.each_index { |i|
+ if input[i, r.rhs.size] == r.rhs.map { |x| x.word }
+ passive_chart.add Item.new(r, i, i+r.rhs.size, r.rhs.size), i, i+r.rhs.size
+ end
+ }
+ }
+end
+
+def Parse::scan item, input, limit, passive_chart
+ while item.rhs[item.dot].class == Grammar::T
+ return false if item.right==limit
+ if item.rhs[item.dot].word == input[item.right]
+ item.dot += 1
+ item.right += 1
+ else
+ return false
+ end
+ end
+ return true
+end
+
+def Parse::parse input, n, active_chart, passive_chart, grammar
+ visit(1, 0, n) { |i,j|
+
+ STDERR.write " span(#{i},#{j})\n"
+
+ # try to apply rules starting with T
+ grammar.start_t.select { |r| r.rhs.first.word == input[i] }.each { |r|
+ new_item = Item.new r, i, i, 0
+ active_chart.at(i,j) << new_item if scan new_item, input, j, passive_chart
+ }
+
+ # seed active chart
+ grammar.start_nt.each { |r|
+ next if r.rhs.size > j-i
+ active_chart.at(i,j) << Item.new(r, i, i, 0)
+ }
+
+ # parse
+ new_symbols = []
+ remaining_items = []
+ while !active_chart.at(i,j).empty?
+ active_item = active_chart.at(i,j).pop
+ advanced = false
+ visit(1, i, j, 1) { |k,l|
+ if passive_chart.has active_item.rhs[active_item.dot].symbol, k, l
+ if k == active_item.right
+ new_item = Item.new active_item, active_item.left, l, active_item.dot+1
+ new_item.tail_spans[new_item.dot-1] = Span.new(k,l)
+ if scan new_item, input, j, passive_chart
+ if new_item.dot == new_item.rhs.size
+ if new_item.left == i && new_item.right == j
+ new_symbols << new_item.lhs.symbol if !new_symbols.include? new_item.lhs.symbol
+ passive_chart.add new_item, i, j
+ advanced = true
+ end
+ else
+ if new_item.right+(new_item.rhs.size-(new_item.dot)) <= j
+ active_chart.at(i,j) << new_item
+ advanced = true
+ end
+ end
+ end
+ end
+ end
+ }
+ if !advanced
+ remaining_items << active_item
+ end
+ end
+
+
+ # 'self-filling' step
+ new_symbols.each { |s|
+ remaining_items.each { |item|
+ next if item.dot!=0
+ next if item.rhs[item.dot].class!=Grammar::NT
+ if item.rhs[item.dot].symbol == s
+ new_item = Item.new item, i, j, item.dot+1
+ new_item.tail_spans[new_item.dot-1] = Span.new(i,j)
+ if new_item.dot==new_item.rhs.size
+ new_symbols << new_item.lhs.symbol if !new_symbols.include? new_item.lhs.symbol
+ passive_chart.add new_item, i, j
+ end
+ end
+ }
+ }
+
+ }
+end
+
+end #module
+
diff --git a/prototype/test_hg.rb b/prototype/test_hg.rb
new file mode 100755
index 0000000..4be72bd
--- /dev/null
+++ b/prototype/test_hg.rb
@@ -0,0 +1,32 @@
+#!/usr/bin/env ruby
+
+require_relative 'hypergraph'
+
+
+def main
+ # viterbi
+ semiring = ViterbiSemiring.new
+ hypergraph, nodes_by_id = HG::read_hypergraph_from_json('../example/toy/toy.json', semiring, true)
+ #hypergraph, nodes_by_id = HG::read_hypergraph_from_json('../example/toy/toy-test.json', semiring, true)
+ #hypergraph, nodes_by_id = HG::read_hypergraph_from_json('../example/glue/glue.json', semiring, true)
+ #hypergraph, nodes_by_id = HG::read_hypergraph_from_json('../example/3/3.json', semiring, true)
+ path, score = HG::viterbi_path hypergraph, nodes_by_id[-1], semiring
+ s = HG::derive path, path.last.head, []
+ path.each { |e| puts "#{e.rule}" }
+ puts "---"
+ puts "#{s.map { |i| i.word }.join ' '}"
+ puts Math.log score
+ puts
+
+ # all paths
+ hypergraph.reset
+ paths = HG::all_paths hypergraph, nodes_by_id[-1]
+ paths.each_with_index { |p,i|
+ s = HG::derive p, p.last.head, []
+ puts "#{i+1}. #{s.map { |x| x.word }.join ' '}"
+ }
+end
+
+
+main
+
diff --git a/prototype/test_parse.rb b/prototype/test_parse.rb
new file mode 100755
index 0000000..918101b
--- /dev/null
+++ b/prototype/test_parse.rb
@@ -0,0 +1,49 @@
+#!/usr/bin/env ruby
+
+require_relative 'parse'
+
+
+def main
+ STDERR.write "> reading input from TODO\n"
+ input = 'ich sah ein kleines haus'.split
+ #input = 'lebensmittel schuld an europäischer inflation'.split
+ #input = 'offizielle prognosen sind von nur 3 prozent ausgegangen , meldete bloomberg .'.split
+ n = input.size
+
+ STDERR.write "> reading grammar\n"
+ grammar = Grammar::Grammar.new '../example/toy/grammar'
+ #grammar = Grammar::Grammar.new '../example/toy/grammar-test'
+ #grammar = Grammar::Grammar.new '../example/glue/grammar'
+ #grammar = Grammar::Grammar.new '../example/3/grammar.3.gz'
+
+ STDERR.write ">> adding glue grammar\n"
+ #grammar.add_glue_rules
+
+ STDERR.write ">> adding pass-through grammar\n"
+ #grammar.add_pass_through_rules input
+
+ STDERR.write "> initializing charts\n"
+ passive_chart = Parse::Chart.new n
+ active_chart = Parse::Chart.new n
+ Parse::init input, n, active_chart, passive_chart, grammar
+
+ STDERR.write "> parsing\n"
+ Parse::parse input, n, active_chart, passive_chart, grammar
+
+ puts "\n---\npassive chart"
+ Parse::visit(1, 0, 5) { |i,j| puts "#{i},#{j}"; passive_chart.at(i,j).each { |item| puts " #{j} #{item.to_s}" }; puts }
+
+ weights_file = '../example/toy/weights'
+ #weights_file = '../example/glue/weights'
+ #weights_file = '../example/3/weights.init'
+ weights = SparseVector.from_kv(ReadFile.read(weights_file), ' ', "\n")
+ if !weights
+ weights = SparseVector.new
+ end
+
+ puts passive_chart.to_hg.to_json weights
+end
+
+
+main
+
diff --git a/prototype/weaver.rb b/prototype/weaver.rb
new file mode 100755
index 0000000..966d4c8
--- /dev/null
+++ b/prototype/weaver.rb
@@ -0,0 +1,70 @@
+#!/usr/bin/env ruby
+
+require 'trollop'
+require 'xmlsimple'
+require_relative 'parse'
+
+def read_grammar fn, add_glue, add_pass_through, input=nil
+ STDERR.write "> reading grammar '#{fn}'\n"
+ grammar = Grammar::Grammar.new fn
+ if add_glue
+ STDERR.write ">> adding glue rules\n"
+ grammar.add_glue
+ end
+ if add_pass_through
+ STDERR.write ">> adding pass-through rules\n"
+ grammar.add_pass_through input
+ end
+ return grammar
+end
+
+def main
+ cfg = Trollop::options do
+ opt :input, "", :type => :string, :default => '-', :short => '-i'
+ opt :grammar, "", :type => :string, :default => nil, :short => '-g'
+ opt :weights, "", :type => :string, :default => nil, :short => '-w'
+ opt :add_glue, "", :type => :bool, :default => false, :short => '-l'
+ opt :add_pass_through, "", :type => :bool, :default => false, :short => '-p'
+ end
+
+ grammar = nil
+ if cfg[:grammar]
+ grammar = read_grammar cfg[:grammar], cfg[:add_glue], cfg[:add_pass_through]
+ end
+
+ STDERR.write "> reading input from '#{cfg[:input]}'\n"
+ ReadFile.readlines_strip(cfg[:input]).each { |input|
+
+ x = XmlSimple.xml_in(input)
+ input = x['content'].split
+ n = input.size
+
+ if x['grammar']
+ grammar = read_grammar x['grammar'], cfg[:add_glue], cfg[:add_pass_through], input
+ end
+
+ STDERR.write "> initializing charts\n"
+ passive_chart = Parse::Chart.new n
+ active_chart = Parse::Chart.new n
+ Parse::init input, n, active_chart, passive_chart, grammar
+
+ STDERR.write "> parsing\n"
+ Parse::parse input, n, active_chart, passive_chart, grammar
+
+ weights = SparseVector.from_kv(ReadFile.read(cfg[:weights]), ' ', "\n")
+ if !weights
+ weights = SparseVector.new
+ end
+
+ hypergraph = passive_chart.to_hg weights
+
+ STDERR.write "> viterbi\n"
+ semiring = ViterbiSemiring.new
+ path, score = HG::viterbi_path hypergraph, hypergraph.nodes_by_id[-1], semiring
+ s = HG::derive path, path.last.head, []
+ STDERR.write " #{s.map { |i| i.word }.join ' '} ||| #{Math.log score}\n"
+ }
+end
+
+main
+
--
cgit v1.2.3