basic transpile

main
henine 2 years ago
parent c54b7e122f
commit 2c985945de

@ -1,18 +1,91 @@
require Logger
defmodule ElixirStan do
@moduledoc """
Documentation for `ElixirStan`.
"""
@doc """
Hello world.
Get the version of the stanc3 executable bundled with the library.
"""
@spec stanc_version :: String.t() | {:error, String.t()}
def stanc_version() do
case System.cmd(Application.app_dir(:elixir_stan, "/priv/bin/stanc"), ["--version"]) do
{version_string, 0} -> String.trim(version_string)
{error, _} -> {:error, error}
end
end
@doc """
Computes the hash of a file used for caching.
Computes SHA256 of the file and truncates it to 8 bytes, then encodes it in a hex string.
"""
@spec hash_file(binary()) :: String.t()
def hash_file(file) when is_binary(file) do
<<hash::bitstring-size(64), _::binary>> = :crypto.hash(:sha256, file)
Base.encode16 hash
end
@spec model_tmp_dir(Path.t()) :: binary | {:error, atom}
def model_tmp_dir(path) do
with {:ok, model_code} <- File.read(path) do
model_name = Path.basename(path, ".stan")
model_hash = hash_file(model_code)
Path.join([
System.tmp_dir(),
"elixir_stan",
model_name <> model_hash <> String.replace(stanc_version(), " ", "_")
])
end
end
@doc """
Transpiles the model into C++ and stores it in a temporary directory.
Temporary directory is based on `System.tmp_dir()`. An `elixir_stan` directory is created in the system temporary
directory and the model is stored in a subdirectory of that. The model directory is based on the model name, the hash
of the model code and the version of stanc used to compile it.
"""
@spec transpile_model(Path.t()) :: :ok | {:error, any()}
def transpile_model(path) do
with {:ok, model_code} <- File.read(path) do
model_tmp_path = model_tmp_dir(path)
## Examples
# Create model dir
:ok = File.mkdir_p(model_tmp_path)
iex> ElixirStan.hello()
:world
model_tmp_file = Path.join(model_tmp_path, Path.basename(path))
# Write model code
:ok = File.write(model_tmp_file, model_code)
case System.cmd(Application.app_dir(:elixir_stan, "/priv/bin/stanc"), [model_tmp_file]) do
{_, 0} -> :ok
end
end
end
@doc """
Cleans the model tmp directory.
"""
def hello do
:world
@spec clean_model(Path.t()) :: :ok | {:error, String.t()}
def clean_model(path) do
model_tmp_path = model_tmp_dir(path)
# Remove model dir
case File.rm_rf(model_tmp_path) do
{:ok, _} ->
:ok
{:error, error, file} ->
Logger.error(
"Could not clean model: #{path}\n\t Error: #{:file.format_error(error)}\n\t File: #{file}"
)
{:error, :file.format_error(error)}
end
end
end

@ -14,7 +14,7 @@ defmodule ElixirStan.MixProject do
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
extra_applications: [:logger, :crypto]
]
end

Binary file not shown.

@ -0,0 +1,13 @@
data {
int<lower=0> N;
vector[N] y;
}
parameters {
real mu;
real<lower=0> sigma;
}
model {
y ~ normal(mu, sigma);
}

@ -2,7 +2,33 @@ defmodule ElixirStanTest do
use ExUnit.Case
doctest ElixirStan
test "greets the world" do
assert ElixirStan.hello() == :world
test "check stanc version" do
assert ElixirStan.stanc_version() == "stanc3 v2.31.0 (Unix)"
end
test "hash test" do
assert ElixirStan.hash_file("a") == "CA978112CA1BBDCA"
end
test "check basic temp path" do
# assert Application.app_dir(:elixir_stan, "test/data/normal.stan") == "a"
assert ElixirStan.model_tmp_dir("test/data/normal.stan") ==
"/tmp/elixir_stan/normalA32F3F2E4143ABA4stanc3_v2.31.0_(Unix)"
end
test "check basic transpile and clean" do
assert ElixirStan.transpile_model("test/data/normal.stan") == :ok
assert File.exists?(
Path.join(ElixirStan.model_tmp_dir("test/data/normal.stan"), "normal.stan")
)
assert File.exists?(
Path.join(ElixirStan.model_tmp_dir("test/data/normal.stan"), "normal.hpp")
)
assert ElixirStan.clean_model("test/data/normal.stan") == :ok
assert !File.exists?(ElixirStan.model_tmp_dir("test/data/normal.stan"))
end
end

Loading…
Cancel
Save