cmdstan 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +3 -0
- data/LICENSE.txt +29 -0
- data/README.md +84 -0
- data/ext/cmdstan/Makefile +5 -0
- data/ext/cmdstan/extconf.rb +60 -0
- data/lib/cmdstan.rb +20 -0
- data/lib/cmdstan/mcmc.rb +60 -0
- data/lib/cmdstan/mle.rb +21 -0
- data/lib/cmdstan/model.rb +107 -0
- data/lib/cmdstan/utils.rb +16 -0
- data/lib/cmdstan/version.rb +3 -0
- metadata +96 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f52650c597cfa86c4e1bd0f095a2b321e906285df981f9f345bde5054fb8cc80
|
4
|
+
data.tar.gz: 3c98b6ff5c800ad9ecd28e325ed6f97d576306567a4ac8530bb117f948c6c321
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ddab8b6b73175afcbee65f5653deee681b746fe2083015ec06b65866bc467ca2bb7ec6cc475a65bc83976ff4f13e8911ac08d999703c52e96d529f8b68408554
|
7
|
+
data.tar.gz: c8316343f62159e1963f7835bb231114275b15ab7fd982f3ae58f50c7168aad08e6448c6c2fd6110910f061595f586f3882a7a38a9e377c332b8b5a381bc4acb
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
BSD 3-Clause License
|
2
|
+
|
3
|
+
Copyright (c) 2020, Andrew Kane
|
4
|
+
All rights reserved.
|
5
|
+
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
8
|
+
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
10
|
+
list of conditions and the following disclaimer.
|
11
|
+
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
14
|
+
and/or other materials provided with the distribution.
|
15
|
+
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
17
|
+
contributors may be used to endorse or promote products derived from
|
18
|
+
this software without specific prior written permission.
|
19
|
+
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# CmdStan.rb
|
2
|
+
|
3
|
+
Bayesian inference for Ruby, powered by [CmdStan](https://github.com/stan-dev/cmdstan)
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application’s Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'cmdstan'
|
11
|
+
```
|
12
|
+
|
13
|
+
Installation can take a few minutes as CmdStan downloads and builds.
|
14
|
+
|
15
|
+
## Getting Started
|
16
|
+
|
17
|
+
Create a Stan file, like `bernoulli.stan`
|
18
|
+
|
19
|
+
```stan
|
20
|
+
data {
|
21
|
+
int<lower=0> N;
|
22
|
+
int<lower=0,upper=1> y[N];
|
23
|
+
}
|
24
|
+
parameters {
|
25
|
+
real<lower=0,upper=1> theta;
|
26
|
+
}
|
27
|
+
model {
|
28
|
+
theta ~ beta(1,1);
|
29
|
+
y ~ bernoulli(theta);
|
30
|
+
}
|
31
|
+
```
|
32
|
+
|
33
|
+
Compile the model
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
model = CmdStan::Model.new(stan_file: "bernoulli.stan")
|
37
|
+
```
|
38
|
+
|
39
|
+
Fit the model
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
data = {"N" => 10, "y" => [0, 1, 0, 0, 0, 0, 0, 0, 0, 1]}
|
43
|
+
fit = model.sample(data: data, chains: 5)
|
44
|
+
```
|
45
|
+
|
46
|
+
Summarize the results
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
fit.summary
|
50
|
+
```
|
51
|
+
|
52
|
+
## Maximum Likelihood Estimation
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
mle = model.optimize(data: data)
|
56
|
+
mle.optimized_params
|
57
|
+
```
|
58
|
+
|
59
|
+
## Credits
|
60
|
+
|
61
|
+
This library is modeled after the [CmdStanPy API](https://github.com/stan-dev/cmdstanpy).
|
62
|
+
|
63
|
+
## History
|
64
|
+
|
65
|
+
View the [changelog](https://github.com/ankane/cmdstan/blob/master/CHANGELOG.md)
|
66
|
+
|
67
|
+
## Contributing
|
68
|
+
|
69
|
+
Everyone is encouraged to help improve this project. Here are a few ways you can help:
|
70
|
+
|
71
|
+
- [Report bugs](https://github.com/ankane/cmdstan/issues)
|
72
|
+
- Fix bugs and [submit pull requests](https://github.com/ankane/cmdstan/pulls)
|
73
|
+
- Write, clarify, or fix documentation
|
74
|
+
- Suggest or add new features
|
75
|
+
|
76
|
+
To get started with development:
|
77
|
+
|
78
|
+
```sh
|
79
|
+
git clone https://github.com/ankane/cmdstan.git
|
80
|
+
cd cmdstan
|
81
|
+
bundle install
|
82
|
+
ruby ext/cmdstan/extconf.rb
|
83
|
+
bundle exec rake test
|
84
|
+
```
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require "digest"
|
2
|
+
require "fileutils"
|
3
|
+
require "net/http"
|
4
|
+
require "tmpdir"
|
5
|
+
|
6
|
+
version = "2.22.1"
|
7
|
+
checksum = "d12e46bda4bd3db9e8abe0554712b56e41f8e7843900338446d9a3b1acc2d0ce"
|
8
|
+
url = "https://github.com/stan-dev/cmdstan/releases/download/v#{version}/cmdstan-#{version}.tar.gz"
|
9
|
+
|
10
|
+
def download_file(url, download_path, checksum)
|
11
|
+
uri = URI(url)
|
12
|
+
location = nil
|
13
|
+
|
14
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
15
|
+
request = Net::HTTP::Get.new(uri)
|
16
|
+
http.request(request) do |response|
|
17
|
+
case response
|
18
|
+
when Net::HTTPRedirection
|
19
|
+
location = response["location"]
|
20
|
+
when Net::HTTPSuccess
|
21
|
+
digest = Digest::SHA2.new
|
22
|
+
|
23
|
+
i = 0
|
24
|
+
File.open(download_path, "wb") do |f|
|
25
|
+
response.read_body do |chunk|
|
26
|
+
f.write(chunk)
|
27
|
+
digest.update(chunk)
|
28
|
+
|
29
|
+
# print progress
|
30
|
+
putc "." if i % 50 == 0
|
31
|
+
i += 1
|
32
|
+
end
|
33
|
+
end
|
34
|
+
puts # newline
|
35
|
+
|
36
|
+
abort "Bad checksum" if digest.hexdigest != checksum
|
37
|
+
else
|
38
|
+
abort "Bad response"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# outside of Net::HTTP block to close previous connection
|
44
|
+
download_file(location, download_path, checksum) if location
|
45
|
+
end
|
46
|
+
|
47
|
+
# download
|
48
|
+
puts "Downloading #{url}..."
|
49
|
+
download_path = "#{Dir.tmpdir}/cmdstan-#{version}.tar.gz"
|
50
|
+
download_file(url, download_path, checksum)
|
51
|
+
|
52
|
+
# extract
|
53
|
+
path = ENV["CMDSTAN"] || File.expand_path("../../tmp/cmdstan", __dir__)
|
54
|
+
FileUtils.mkdir_p(path)
|
55
|
+
Dir.chdir(path)
|
56
|
+
# TODO use Gem::Package::TarReader from Rubygems
|
57
|
+
system "tar", "zxvf", download_path, "-C", path, "--strip-components=1"
|
58
|
+
|
59
|
+
# build
|
60
|
+
system "make", "build", "-j"
|
data/lib/cmdstan.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# stdlib
|
2
|
+
require "csv"
|
3
|
+
require "json"
|
4
|
+
require "open3"
|
5
|
+
|
6
|
+
# modules
|
7
|
+
require "cmdstan/utils"
|
8
|
+
require "cmdstan/mcmc"
|
9
|
+
require "cmdstan/mle"
|
10
|
+
require "cmdstan/model"
|
11
|
+
require "cmdstan/version"
|
12
|
+
|
13
|
+
module CmdStan
|
14
|
+
class Error < StandardError; end
|
15
|
+
|
16
|
+
class << self
|
17
|
+
attr_accessor :path
|
18
|
+
end
|
19
|
+
self.path = ENV["CMDSTAN"] || File.expand_path("../tmp/cmdstan", __dir__)
|
20
|
+
end
|
data/lib/cmdstan/mcmc.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
module CmdStan
|
2
|
+
class MCMC
|
3
|
+
include Utils
|
4
|
+
|
5
|
+
attr_reader :column_names, :draws
|
6
|
+
|
7
|
+
# private
|
8
|
+
# TODO use runset for args
|
9
|
+
def initialize(output_files)
|
10
|
+
@output_files = output_files
|
11
|
+
validate_csv_files
|
12
|
+
end
|
13
|
+
|
14
|
+
def sample
|
15
|
+
sample = []
|
16
|
+
@output_files.each_with_index do |output_file, chain|
|
17
|
+
i = 0
|
18
|
+
CSV.foreach(output_file.path, skip_lines: /^#/, headers: true) do |row|
|
19
|
+
(sample[i] ||= [])[chain] = row.to_h.values.map(&:to_f)
|
20
|
+
i += 1
|
21
|
+
end
|
22
|
+
raise "Bug detected" if i != draws
|
23
|
+
end
|
24
|
+
sample
|
25
|
+
end
|
26
|
+
|
27
|
+
def summary
|
28
|
+
csv_file = Tempfile.new
|
29
|
+
run_command "#{CmdStan.path}/bin/stansummary", "--csv_file=#{csv_file.path}", *@output_files.map(&:path)
|
30
|
+
|
31
|
+
result = {}
|
32
|
+
CSV.foreach(csv_file.path, headers: true, converters: :numeric) do |row|
|
33
|
+
value = row.to_h
|
34
|
+
name = value.delete("name")
|
35
|
+
result[name] = value if name == "lp__" || !name.end_with?("__")
|
36
|
+
end
|
37
|
+
result
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def validate_csv_files
|
43
|
+
# TODO ensure consistent files
|
44
|
+
output_file = @output_files.first
|
45
|
+
|
46
|
+
File.foreach(output_file.path) do |line|
|
47
|
+
matches = /num_samples = (\d+)/.match(line)
|
48
|
+
if matches
|
49
|
+
@draws = matches[1].to_i
|
50
|
+
break
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
CSV.foreach(output_file.path, skip_lines: /^#/, headers: true) do |row|
|
55
|
+
@column_names = row.to_h.keys
|
56
|
+
break
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/lib/cmdstan/mle.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module CmdStan
|
2
|
+
class MLE
|
3
|
+
include Utils
|
4
|
+
|
5
|
+
# private
|
6
|
+
# TODO use runset for args
|
7
|
+
def initialize(output_file)
|
8
|
+
@output_file = output_file
|
9
|
+
end
|
10
|
+
|
11
|
+
def optimized_params
|
12
|
+
CSV.foreach(@output_file.path, skip_lines: /^#/, headers: true, converters: :numeric) do |row|
|
13
|
+
return row.to_h
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def column_names
|
18
|
+
optimized_params.keys
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module CmdStan
|
2
|
+
class Model
|
3
|
+
include Utils
|
4
|
+
|
5
|
+
attr_reader :exe_file, :name, :stan_file
|
6
|
+
|
7
|
+
def initialize(stan_file: nil, exe_file: nil, compile: true)
|
8
|
+
# convert to absolute path
|
9
|
+
stan_file = File.expand_path(stan_file) if stan_file
|
10
|
+
|
11
|
+
@stan_file = stan_file
|
12
|
+
@exe_file = exe_file || stan_file.sub(/.stan\z/, "")
|
13
|
+
@name = File.basename(@exe_file)
|
14
|
+
|
15
|
+
if compile && !exe_file
|
16
|
+
self.compile
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def compile
|
21
|
+
Dir.chdir(CmdStan.path) do
|
22
|
+
run_command "make", @exe_file
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def code
|
27
|
+
File.read(stan_file)
|
28
|
+
end
|
29
|
+
|
30
|
+
def sample(data:, chains: nil, seed: nil, inits: nil, warmup_iters: nil, sampling_iters: nil)
|
31
|
+
data_file = Tempfile.new(["cmdstan", ".json"])
|
32
|
+
data_file.write(data.to_json)
|
33
|
+
data_file.close
|
34
|
+
|
35
|
+
chain ||= 4
|
36
|
+
|
37
|
+
output_files = []
|
38
|
+
chains.times do |chain|
|
39
|
+
output_file = Tempfile.new(["cmdstan", ".csv"])
|
40
|
+
|
41
|
+
args = [@exe_file, "id=#{chain + 1}"]
|
42
|
+
|
43
|
+
# random
|
44
|
+
args += ["random", "seed=#{seed.to_i}"] if seed
|
45
|
+
|
46
|
+
# data
|
47
|
+
args += ["data", "file=#{data_file.path}"]
|
48
|
+
if inits
|
49
|
+
init_file = Tempfile.new(["cmdstan", ".json"])
|
50
|
+
init_file.write(inits.to_json)
|
51
|
+
init_file.close
|
52
|
+
args << "init=#{init_file.path}"
|
53
|
+
end
|
54
|
+
|
55
|
+
# output
|
56
|
+
args += ["output", "file=#{output_file.path}"]
|
57
|
+
|
58
|
+
# method
|
59
|
+
args += ["method=sample"]
|
60
|
+
args << "num_warmup=#{warmup_iters.to_i}" if warmup_iters
|
61
|
+
args << "num_samples=#{sampling_iters.to_i}" if sampling_iters
|
62
|
+
args += ["algorithm=hmc", "adapt", "engaged=1"]
|
63
|
+
|
64
|
+
run_command *args
|
65
|
+
|
66
|
+
output_files << output_file
|
67
|
+
end
|
68
|
+
|
69
|
+
MCMC.new(output_files)
|
70
|
+
end
|
71
|
+
|
72
|
+
def optimize(data:, seed: nil, inits: nil, algorithm: nil, iter: nil)
|
73
|
+
data_file = Tempfile.new(["cmdstan", ".json"])
|
74
|
+
data_file.write(data.to_json)
|
75
|
+
data_file.close
|
76
|
+
|
77
|
+
output_file = Tempfile.new(["cmdstan", ".csv"])
|
78
|
+
diagnostic_file = Tempfile.new(["cmdstan", ".csv"])
|
79
|
+
|
80
|
+
args = [@exe_file]
|
81
|
+
|
82
|
+
# random
|
83
|
+
args += ["random", "seed=#{seed.to_i}"] if seed
|
84
|
+
|
85
|
+
# data
|
86
|
+
args += ["data", "file=#{data_file.path}"]
|
87
|
+
if inits
|
88
|
+
init_file = Tempfile.new(["cmdstan", ".json"])
|
89
|
+
init_file.write(inits.to_json)
|
90
|
+
init_file.close
|
91
|
+
args << "init=#{init_file.path}"
|
92
|
+
end
|
93
|
+
|
94
|
+
# output
|
95
|
+
args += ["output", "file=#{output_file.path}", "diagnostic_file=#{diagnostic_file.path}"]
|
96
|
+
|
97
|
+
# method
|
98
|
+
args << "method=optimize"
|
99
|
+
args << "algorithm=#{algorithm.to_s.downcase}" if algorithm
|
100
|
+
args << "iter=#{iter.to_i}" if iter
|
101
|
+
|
102
|
+
run_command *args
|
103
|
+
|
104
|
+
MLE.new(output_file)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module CmdStan
|
2
|
+
module Utils
|
3
|
+
private
|
4
|
+
|
5
|
+
def run_command(*args)
|
6
|
+
# use popen3 since it does escaping (like system)
|
7
|
+
Open3.popen3(*args) do |i, o, e, t|
|
8
|
+
if t.value.exitstatus != 0
|
9
|
+
$stderr.puts o.read
|
10
|
+
$stderr.puts e.read
|
11
|
+
raise Error, "Command failed"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cmdstan
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Andrew Kane
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-04-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '5'
|
55
|
+
description:
|
56
|
+
email: andrew@chartkick.com
|
57
|
+
executables: []
|
58
|
+
extensions:
|
59
|
+
- ext/cmdstan/extconf.rb
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- CHANGELOG.md
|
63
|
+
- LICENSE.txt
|
64
|
+
- README.md
|
65
|
+
- ext/cmdstan/Makefile
|
66
|
+
- ext/cmdstan/extconf.rb
|
67
|
+
- lib/cmdstan.rb
|
68
|
+
- lib/cmdstan/mcmc.rb
|
69
|
+
- lib/cmdstan/mle.rb
|
70
|
+
- lib/cmdstan/model.rb
|
71
|
+
- lib/cmdstan/utils.rb
|
72
|
+
- lib/cmdstan/version.rb
|
73
|
+
homepage: https://github.com/ankane/cmdstan
|
74
|
+
licenses:
|
75
|
+
- BSD-3-Clause
|
76
|
+
metadata: {}
|
77
|
+
post_install_message:
|
78
|
+
rdoc_options: []
|
79
|
+
require_paths:
|
80
|
+
- lib
|
81
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '2.4'
|
86
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
requirements: []
|
92
|
+
rubygems_version: 3.1.2
|
93
|
+
signing_key:
|
94
|
+
specification_version: 4
|
95
|
+
summary: Bayesian inference for Ruby, powered by CmdStan
|
96
|
+
test_files: []
|