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
## Examples
@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)
iex> ElixirStan.hello()
:world
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.
"""
def hello do
:world
@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)
# Create model dir
:ok = File.mkdir_p(model_tmp_path)
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.
"""
@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