simulator 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,40 @@
1
+ require 'simulator'
2
+ include Simulator
3
+
4
+ Given /^a savings model$/ do
5
+ @model = Model.new
6
+ end
7
+
8
+ Given /^variable "([_\w]+)"$/ do |var_name|
9
+ @model.add_variable Variable.new var_name.to_sym
10
+ end
11
+
12
+ Given /^equation "(.*?)" bound to ([\w_]+)$/ do |eqtn_str, var_name|
13
+ var = @model.get_variable var_name.to_sym
14
+ eqtn = Equation.new var do
15
+ eval eqtn_str
16
+ end
17
+ @model.add_equation eqtn
18
+ end
19
+
20
+ Given /^the value (-?[\d\.]+) bound to ([\w_]+)$/ do |value, name|
21
+ @run.set name.to_sym => value.to_f
22
+ end
23
+
24
+ Given /^a new run of the model$/ do
25
+ @run = Run.new @model
26
+ end
27
+
28
+ When /^I step the run for (\d+) periods$/ do |period_count|
29
+ period_count.to_i.times do
30
+ @run.step
31
+ end
32
+ end
33
+
34
+ Then /^the value (-?[\d\.]+) should be bound to ([\w_]+)$/ do |expected, var_name|
35
+ actual = @run.variable_value var_name.to_sym
36
+ actual.should be_within(0.001).of expected.to_f
37
+ end
38
+
39
+
40
+
data/lib/simulator.rb ADDED
@@ -0,0 +1,14 @@
1
+ require "simulator/version"
2
+ require 'simulator/equation'
3
+ require 'simulator/variable'
4
+ require 'simulator/bound_variable'
5
+ require 'simulator/variable_context'
6
+ require 'simulator/sandbox'
7
+ require 'simulator/model'
8
+ require 'simulator/run'
9
+ require 'simulator/period'
10
+ require 'simulator/data'
11
+
12
+ module Simulator
13
+ end
14
+
@@ -0,0 +1,18 @@
1
+ module Simulator
2
+ # represents a variable bound to a context
3
+ class BoundVariable
4
+ attr_accessor :value
5
+ attr_reader :context, :variable
6
+
7
+ def initialize(variable, context)
8
+ @variable = variable
9
+ @value = @variable.default_value
10
+ @context = context
11
+ end
12
+
13
+ def name
14
+ @variable.name
15
+ end
16
+ end
17
+ end
18
+
@@ -0,0 +1,23 @@
1
+ module Simulator
2
+ class Data
3
+ attr_accessor :periods
4
+ def series(*var_names)
5
+ data = var_names.collect do |var_name|
6
+ @periods.collect do |period|
7
+ var = period.context.get var_name
8
+ var.value unless var.nil?
9
+ end
10
+ end
11
+
12
+ # If we just hae one series, then just return the first
13
+ var_names.length == 1 ? data.first : data
14
+ end
15
+ def series_with_headers(*var_names)
16
+ data = series *var_names
17
+ table = var_names.zip data
18
+ table.collect do |row|
19
+ row.flatten
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,16 @@
1
+ module Simulator
2
+ class Equation
3
+ attr_reader :variable
4
+
5
+ def initialize(var, &equation)
6
+ @equation_block = equation
7
+ @variable = var
8
+ end
9
+
10
+ # evaluate the equation in the passed context
11
+ def evaluate_in(context, periods = [])
12
+ sandbox = Sandbox.new context, periods
13
+ sandbox.instance_eval &@equation_block
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,43 @@
1
+ module Simulator
2
+ #
3
+ class Model
4
+ attr_accessor :name
5
+ attr_reader :equations
6
+
7
+ def initialize(&block)
8
+ @equations = []
9
+ @variables = {}
10
+ instance_eval &block unless block.nil?
11
+ end
12
+ def var(name, default_value = nil)
13
+ v = get_variable(name)
14
+ if v.nil?
15
+ v = Variable.new name, default_value
16
+ add_variable v
17
+ end
18
+ v
19
+ end
20
+ def eqtn(var_name, default_value = nil, &block)
21
+ v = var(var_name, default_value)
22
+ e = Equation.new(v, &block)
23
+ add_equation e
24
+ e
25
+ end
26
+ def add_equation(equation)
27
+ @equations << equation
28
+ end
29
+ def add_variable(variable)
30
+ @variables[variable.name] = variable
31
+ end
32
+ def get_variable(name)
33
+ @variables[name]
34
+ end
35
+ def variables
36
+ @variables.values
37
+ end
38
+
39
+ def new_run
40
+ Run.new self
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,28 @@
1
+ module Simulator
2
+ class Period
3
+ attr_reader :context
4
+ attr_reader :run
5
+ attr_reader :previous_periods
6
+
7
+ def initialize(run, previous_periods = [])
8
+ @run = run
9
+ @previous_periods = previous_periods
10
+ @context = @previous_periods.empty? ? VariableContext.new : previous.context.clone
11
+ end
12
+
13
+ # the first period
14
+ def first
15
+ @previous_periods.first
16
+ end
17
+
18
+ # the period before this one
19
+ def previous
20
+ @previous_periods.last
21
+ end
22
+
23
+ # returns a period count ago
24
+ def ago(count)
25
+ count == 0 ? self : @previous_periods[-count]
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,95 @@
1
+ module Simulator
2
+ # A run of a model. It has its own context
3
+ class Run
4
+ attr_reader :model
5
+ attr_reader :periods
6
+
7
+ def initialize(model)
8
+ @model = model
9
+ @periods = [Period.new(self)]
10
+ current_context.add_variables *@model.variables
11
+ end
12
+
13
+ def evaluate
14
+ # evaluate the equations in the current context
15
+ @model.equations.each do | eqtn |
16
+ result = eqtn.evaluate_in current_context, @periods
17
+ var = eqtn.variable
18
+ bound_var = current_context.get(var.name)
19
+ bound_var.value = result
20
+ end
21
+ end
22
+
23
+ def increment_period
24
+ p = Period.new self, @periods.dup
25
+ @periods << p
26
+ p
27
+ end
28
+
29
+ def step
30
+ evaluate
31
+ increment_period
32
+ end
33
+
34
+ # The number of periods so far. All runs have at least 1 period.
35
+ def period_count
36
+ @periods.length
37
+ end
38
+
39
+ # Returns the current_context, that is the variable context of the last
40
+ # period.
41
+ def current_context
42
+ @periods.last.context
43
+ end
44
+
45
+ def variables
46
+ current_context.variables
47
+ end
48
+
49
+ def set(var_hash)
50
+ current_context.set var_hash
51
+ end
52
+
53
+ def variable_value(var_name)
54
+ current_context.get(var_name).value
55
+ end
56
+
57
+ def summary
58
+ var_list = variables.sort_by(&:name).collect do |v|
59
+ "\t#{v.name} = #{v.value}"
60
+ end.join("\n")
61
+ model_summary = %Q{Model "#{@model.name}"}
62
+ period_summary = "Period #{period_count}"
63
+
64
+ "#{model_summary}\n#{period_summary}\nVariables:\n#{var_list}"
65
+ end
66
+
67
+ def variables_csv
68
+ vars = variables.sort_by(&:name)
69
+ var_names = vars.collect(&:name)
70
+ header = "'Period',#{var_names.collect{|n| "'#{n}'"}.join(',')}"
71
+
72
+ rows = []
73
+ @periods.each_index do |index|
74
+ period = @periods[index]
75
+
76
+ context = period.context
77
+ var_values = var_names.collect do | var_name |
78
+ var = context.get var_name
79
+ var.value
80
+ end.join(',')
81
+
82
+ rows << "#{index},#{var_values}"
83
+ end
84
+
85
+ "#{header}\n#{rows.join("\n")}"
86
+ end
87
+
88
+ def data
89
+ data = Data.new
90
+ data.periods = @periods
91
+ data
92
+ end
93
+ end
94
+ end
95
+
@@ -0,0 +1,31 @@
1
+ module Simulator
2
+ class Sandbox
3
+ def initialize(context, periods = [])
4
+ @context = context
5
+ @periods = periods
6
+ end
7
+
8
+ private
9
+
10
+ def delay(var_name, var_periods)
11
+ throw 'First parameter must be a variable symbol' unless var_name.class == Symbol
12
+
13
+ # Get the period n steps back
14
+ period = @periods[-var_periods] # We might have to add 1 period back (- (var_periods+1))
15
+ context = period.context
16
+ var = context.get var_name
17
+ var.value unless var.nil?
18
+ end
19
+
20
+ def respond_to?(sym)
21
+ puts "Respond to: #{sym}"
22
+ not @context.get(sym.to_sym).nil?
23
+ end
24
+
25
+ def method_missing(method_name, *args, &block)
26
+ var = @context.get method_name.to_sym
27
+ return var.value unless var.nil?
28
+ super(method_name, *args, &block)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,11 @@
1
+ module Simulator
2
+ class Variable
3
+ attr_reader :name, :default_value
4
+
5
+ def initialize(name, default_value = nil)
6
+ @name = name
7
+ @default_value = default_value
8
+ end
9
+ end
10
+ end
11
+
@@ -0,0 +1,66 @@
1
+ module Simulator
2
+ class VariableContext
3
+ def initialize
4
+ @variables_hash = {}
5
+ end
6
+
7
+ def add(var_hash)
8
+ vars = var_hash.collect do |k, v|
9
+ Variable.new k
10
+ end
11
+ add_variables *vars
12
+ set var_hash
13
+ end
14
+
15
+ # add variables. doesn't check for uniqueness, does not overwrite existing
16
+ def add_variables(*vars)
17
+ # create bound variables for each variable
18
+ bound_vars = vars.collect do |v|
19
+ BoundVariable.new v, self
20
+ end
21
+
22
+ # add all the bound variables to the variables hash
23
+ keys = vars.collect(&:name)
24
+ append_hash = Hash[ keys.zip(bound_vars) ]
25
+ @variables_hash.merge!(append_hash) do |key, oldval, newval|
26
+ oldval # the first bound variable
27
+ end
28
+ end
29
+
30
+ # Use to set the value for a variety of variables
31
+ def set(var_hash)
32
+ var_hash.each do |variable_name, value|
33
+ throw :MissingVariable unless @variables_hash.has_key? variable_name
34
+
35
+ bv = @variables_hash[variable_name]
36
+ bv.value = value
37
+ end
38
+ end
39
+
40
+ def get(var_name)
41
+ @variables_hash[var_name]
42
+ end
43
+
44
+ def variables
45
+ @variables_hash.values
46
+ end
47
+
48
+ def unbound_variables
49
+ variables.collect(&:variable)
50
+ end
51
+
52
+ def clone
53
+ copy = VariableContext.new
54
+ copy.add_variables *unbound_variables
55
+
56
+ # create a value hash of variable names and values
57
+ value_hash = variables.inject({}) do |memo, v|
58
+ memo[v.name] = v.value
59
+ memo
60
+ end
61
+ copy.set value_hash
62
+ copy
63
+ end
64
+ end
65
+ end
66
+
@@ -0,0 +1,3 @@
1
+ module Simulator
2
+ VERSION = "0.1.2"
3
+ end
data/simulator.gemspec ADDED
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/simulator/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Jamie Ly"]
6
+ gem.email = ["me@jamie.ly"]
7
+ gem.description = %q{Use to simulate discrete time models. See the examples directory for examples.}
8
+ gem.summary = %q{Use to simulate discrete time models.}
9
+ gem.homepage = "http://github.com/jamiely/simulator"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "simulator"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Simulator::VERSION
17
+
18
+ gem.add_development_dependency 'rspec'
19
+ gem.add_development_dependency 'cucumber'
20
+ end
data/spec/beer_spec.rb ADDED
@@ -0,0 +1,91 @@
1
+ require 'simulator'
2
+ include Simulator
3
+
4
+ describe "a beer distribution simulation" do
5
+ before :each do
6
+ @model = Model.new do
7
+ name = "Beer Distribution"
8
+
9
+ var :time_to_ship, 1
10
+ var :vendor_orders, 10
11
+ eqtn :available_goods do
12
+ delay :vendor_orders, time_to_ship
13
+ end
14
+
15
+ eqtn :ordered_from_vendors do
16
+ if ordered_from_vendors.nil?
17
+ vendor_orders
18
+ else
19
+ delta = vendor_orders - available_goods
20
+ ordered_from_vendors + delta
21
+ end
22
+ end
23
+
24
+ var :time_to_build_product, 1
25
+ eqtn :wip_completion do
26
+ delay :available_goods, time_to_build_product
27
+ end
28
+ eqtn :work_in_progress do
29
+ if work_in_progress.nil?
30
+ available_goods
31
+ else
32
+ delta = available_goods - wip_completion
33
+ work_in_progress + delta
34
+ end
35
+ end
36
+ eqtn :finished_goods_inventory do
37
+ if finished_goods_inventory.nil?
38
+ wip_completion
39
+ else
40
+ delta = wip_completion - shipments
41
+ finished_goods_inventory + delta
42
+ end
43
+ end
44
+
45
+ var :new_customer_orders, 10 # this should be an equation later
46
+ eqtn :unfilled_orders do
47
+ [0, new_customer_orders - shipments].max
48
+ end
49
+ eqtn :total_customer_orders do
50
+ cumulative_unfilled_orders + new_customer_orders
51
+ end
52
+ eqtn :shipments, 0 do
53
+ [total_customer_orders, finished_goods_inventory].min
54
+ end
55
+
56
+ eqtn :inventory_coverage do
57
+ begin
58
+ finished_goods_inventory/total_customer_orders
59
+ rescue
60
+ 0
61
+ end
62
+ end
63
+
64
+ var :fraction_of_unfilled_orders_that_are_cancelled, 0.25
65
+ eqtn :cancelled_unfilled_orders do
66
+ tmp = cumulative_unfilled_orders - filled_orders
67
+ tmp2 = tmp * fraction_of_unfilled_orders_that_are_cancelled
68
+ tmp2.round
69
+ end
70
+
71
+ eqtn :cumulative_unfilled_orders, 0 do
72
+ delta = unfilled_orders - filled_orders - cancelled_unfilled_orders
73
+ cumulative_unfilled_orders + delta
74
+ end
75
+
76
+ eqtn :filled_orders, 0 do
77
+ [0, shipments - new_customer_orders].max
78
+ end
79
+ eqtn :lost_orders, 0 do
80
+ lost_orders + cancelled_unfilled_orders
81
+ end
82
+ end
83
+
84
+ @run = Run.new @model
85
+ end
86
+
87
+ it "can step a run" do
88
+ @run.period_count.should eq 1
89
+ end
90
+ end
91
+