Skip to content

Commit

Permalink
CmdStan now installs when the first model is compiled rather than on …
Browse files Browse the repository at this point in the history
…gem installation
  • Loading branch information
ankane committed Apr 23, 2022
1 parent 1d0b1e1 commit f1d7327
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 90 deletions.
11 changes: 8 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest] # TODO add windows-latest
include:
- ruby: 3.1
os: ubuntu-20.04
- ruby: "3.0"
os: ubuntu-18.04
- ruby: 2.7
os: macos-latest
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- run: bundle exec ruby ext/cmdstan/extconf.rb
- run: bundle exec rake test
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## 0.2.0 (unreleased)

- CmdStan now installs when the first model is compiled rather than on gem installation
- Added `install_cmdstan` method
- Updated CmdStan to 2.29.2
- Fixed issue with `summary` method
- Dropped support for Ruby < 2.7

Expand Down
25 changes: 21 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ Add this line to your application’s Gemfile:
gem "cmdstan"
```

Installation can take a few minutes as CmdStan downloads and builds.

## Getting Started

Create a Stan file, like `bernoulli.stan`
Expand All @@ -32,7 +30,7 @@ model {
}
```

Compile the model
Compile the model (this can take a few minutes the first time as CmdStan downloads and builds)

```ruby
model = CmdStan::Model.new(stan_file: "bernoulli.stan")
Expand All @@ -51,13 +49,33 @@ Summarize the results
fit.summary
```

Load a compiled model

```ruby
model = CmdStan::Model.new(exe_file: "bernoulli")
```

## Maximum Likelihood Estimation

```ruby
mle = model.optimize(data: data)
mle.optimized_params
```

## Reference

Check if CmdStan is installed

```ruby
CmdStan.cmdstan_installed?
```

Install CmdStan manually

```ruby
CmdStan.install_cmdstan
```

## Credits

This library is modeled after the [CmdStanPy API](https://github.com/stan-dev/cmdstanpy).
Expand All @@ -81,6 +99,5 @@ To get started with development:
git clone https://github.com/ankane/cmdstan-ruby.git
cd cmdstan-ruby
bundle install
bundle exec ruby ext/cmdstan/extconf.rb
bundle exec rake test
```
3 changes: 1 addition & 2 deletions cmdstan.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ Gem::Specification.new do |spec|
spec.author = "Andrew Kane"
spec.email = "andrew@ankane.org"

spec.files = Dir["*.{md,txt}", "{ext,lib}/**/*"]
spec.files = Dir["*.{md,txt}", "{lib}/**/*"]
spec.require_path = "lib"
spec.extensions = ["ext/cmdstan/extconf.rb"]

spec.required_ruby_version = ">= 2.7"
end
5 changes: 0 additions & 5 deletions ext/cmdstan/Makefile

This file was deleted.

75 changes: 0 additions & 75 deletions ext/cmdstan/extconf.rb

This file was deleted.

6 changes: 6 additions & 0 deletions lib/cmdstan.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
# stdlib
require "digest"
require "csv"
require "fileutils"
require "json"
require "net/http"
require "open3"
require "tempfile"

# modules
require "cmdstan/install"
require "cmdstan/utils"
require "cmdstan/mcmc"
require "cmdstan/mle"
Expand All @@ -14,6 +18,8 @@
module CmdStan
class Error < StandardError; end

extend Install

class << self
attr_accessor :path
end
Expand Down
100 changes: 100 additions & 0 deletions lib/cmdstan/install.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
module CmdStan
module Install
def cmdstan_version
"2.29.2"
end

def cmdstan_installed?
Dir.exist?(CmdStan.path)
end

def install_cmdstan
version = cmdstan_version
dir = CmdStan.path

# TODO figure out Mac ARM
if RbConfig::CONFIG["host_os"] !~ /darwin/i && RbConfig::CONFIG["host_cpu"] =~ /arm|aarch64/i
checksum = "9b7eec78e217cab39d3d794e817a1ca08f36b1e5cb434c4cd8263bb2650ba125"
url = "https://github.com/stan-dev/cmdstan/releases/download/v#{version}/cmdstan-#{version}-linux-arm64.tar.gz"
else
checksum = "567b531fa73ffdf706caa17eb3344e1dfb41e86993caf8ba40875ff910335153"
url = "https://github.com/stan-dev/cmdstan/releases/download/v#{version}/cmdstan-#{version}.tar.gz"
end

puts "Installing CmdStan version: #{version}"
puts "Install directory: #{dir}"

# only needed if default path
FileUtils.mkdir_p(File.expand_path("../../tmp", __dir__)) unless ENV["CMDSTAN"]

if Dir.exist?(dir)
puts "Already installed"
return true
end

Dir.mktmpdir do |tmpdir|
puts "Downloading..."
download_path = File.join(tmpdir, "cmdstan-#{version}.tar.gz")
download_file(url, download_path, checksum)

puts "Unpacking..."
path = File.join(tmpdir, "cmdstan-#{version}")
FileUtils.mkdir_p(path)
tar_args = Gem.win_platform? ? ["--force-local"] : []
system "tar", "xzf", download_path, "-C", path, "--strip-components=1", *tar_args

puts "Building..."
make_command = Gem.win_platform? ? "mingw32-make" : "make"
Dir.chdir(path) do
# disable precompiled header to save space
output, status = Open3.capture2e(make_command, "build", "PRECOMPILED_HEADERS=false")
if status.exitstatus != 0
puts output
raise Error, "Build failed"
end
end

FileUtils.mv(path, dir)
end

puts "Installed"

true
end

private

def download_file(url, download_path, checksum, redirects = 0)
raise Error, "Too many redirects" if redirects > 10

uri = URI(url)
location = nil

Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
request = Net::HTTP::Get.new(uri)
http.request(request) do |response|
case response
when Net::HTTPRedirection
location = response["location"]
when Net::HTTPSuccess
digest = Digest::SHA2.new

File.open(download_path, "wb") do |f|
response.read_body do |chunk|
f.write(chunk)
digest.update(chunk)
end
end

raise Error, "Bad checksum: #{digest.hexdigest}" if digest.hexdigest != checksum
else
raise Error, "Bad response"
end
end
end

# outside of Net::HTTP block to close previous connection
download_file(location, download_path, checksum, redirects + 1) if location
end
end
end
6 changes: 5 additions & 1 deletion lib/cmdstan/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ def initialize(stan_file: nil, exe_file: nil, compile: true)
end

def compile
unless ENV["CMDSTAN"] || CmdStan.cmdstan_installed?
CmdStan.install_cmdstan
end

Dir.chdir(CmdStan.path) do
run_command make_command, @exe_file
run_command make_command, @exe_file, "PRECOMPILED_HEADERS=false"
end
end

Expand Down
5 changes: 5 additions & 0 deletions test/model_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ def test_works
assert_equal ["lp__", "theta"], mle.column_names
assert_in_delta(-5.00402, mle.optimized_params["lp__"])
assert_in_delta(0.2, mle.optimized_params["theta"])

# load model
model = CmdStan::Model.new(exe_file: model.exe_file)
fit = model.sample(chains: 5, data: data, seed: 123)
assert_equal 1000, fit.draws
end

private
Expand Down

0 comments on commit f1d7327

Please sign in to comment.