Current section

Files

Jump to
sidereon lib sidereon space_weather.ex
Raw

lib/sidereon/space_weather.ex

defmodule Sidereon.SpaceWeather do
@moduledoc """
Space-weather tables for drag and decay inputs.
The parser and lookup policy are implemented in the core NIF. This module
reads optional files, owns the Elixir structs, and forwards table operations to
the parsed native resource.
"""
alias Sidereon.Drag
alias Sidereon.NIF
@enforce_keys [:handle]
defstruct [:handle]
@type t :: %__MODULE__{handle: reference()}
defmodule Sample do
@moduledoc """
Space-weather values plus source metadata for one lookup.
"""
@enforce_keys [:space_weather, :class, :ap_defaulted]
defstruct [:space_weather, :class, :ap_defaulted]
@type t :: %__MODULE__{
space_weather: Drag.SpaceWeather.t(),
class: :observed | :interpolated | :daily_predicted | :monthly_predicted,
ap_defaulted: boolean()
}
end
defmodule Policy do
@moduledoc """
Lookup policy for rejecting lower-trust rows or monthly Ap defaults.
"""
defstruct allow_interpolated: true,
allow_daily_predicted: true,
allow_monthly_predicted: true,
require_geomagnetic: false
@type t :: %__MODULE__{
allow_interpolated: boolean(),
allow_daily_predicted: boolean(),
allow_monthly_predicted: boolean(),
require_geomagnetic: boolean()
}
end
defmodule Coverage do
@moduledoc """
J2000-second coverage bounds for a parsed space-weather table.
"""
@enforce_keys [:first_j2000_s, :end_j2000_s]
defstruct [:first_j2000_s, :last_observed_j2000_s, :last_daily_predicted_j2000_s, :end_j2000_s]
@type t :: %__MODULE__{
first_j2000_s: float(),
last_observed_j2000_s: float() | nil,
last_daily_predicted_j2000_s: float() | nil,
end_j2000_s: float()
}
end
@type error_reason ::
:unrecognized_format
| :not_text
| {:malformed, non_neg_integer(), String.t()}
| {:before_coverage, float(), float()}
| {:after_coverage, float(), float()}
| {:missing_data, integer(), integer(), integer(), String.t()}
| {:rejected_by_policy, atom(), integer(), integer(), integer()}
| {:invalid_epoch, float()}
| term()
@spec parse(binary()) :: {:ok, t()} | {:error, error_reason()}
def parse(bytes) when is_binary(bytes) do
case NIF.space_weather_parse(bytes) do
{:ok, handle} when is_reference(handle) -> {:ok, %__MODULE__{handle: handle}}
{:error, _} = err -> err
other -> {:error, other}
end
rescue
e in ErlangError -> {:error, e.original}
end
@spec load(String.t()) :: {:ok, t()} | {:error, term()}
def load(path) when is_binary(path) do
with {:ok, bytes} <- File.read(path), do: parse(bytes)
end
@spec space_weather_at(t(), number()) :: {:ok, Drag.SpaceWeather.t()} | {:error, error_reason()}
def space_weather_at(%__MODULE__{handle: handle}, epoch_j2000_s) when is_number(epoch_j2000_s) do
case NIF.space_weather_space_weather_at(handle, epoch_j2000_s / 1.0) do
{:ok, fields} -> {:ok, to_space_weather(fields)}
{:error, _} = err -> err
end
rescue
e in ErlangError -> {:error, e.original}
end
@spec sample_at(t(), number()) :: {:ok, Sample.t()} | {:error, error_reason()}
def sample_at(%__MODULE__{handle: handle}, epoch_j2000_s) when is_number(epoch_j2000_s) do
case NIF.space_weather_sample_at(handle, epoch_j2000_s / 1.0) do
{:ok, fields} -> {:ok, to_sample(fields)}
{:error, _} = err -> err
end
rescue
e in ErlangError -> {:error, e.original}
end
@spec sample_at_with_policy(t(), number(), Policy.t() | keyword()) :: {:ok, Sample.t()} | {:error, error_reason()}
def sample_at_with_policy(%__MODULE__{handle: handle}, epoch_j2000_s, policy) when is_number(epoch_j2000_s) do
case NIF.space_weather_sample_at_with_policy(handle, epoch_j2000_s / 1.0, policy_map(policy)) do
{:ok, fields} -> {:ok, to_sample(fields)}
{:error, _} = err -> err
end
rescue
e in ErlangError -> {:error, e.original}
end
@spec ap_array_at(t(), number()) :: {:ok, [float()]} | {:error, error_reason()}
def ap_array_at(%__MODULE__{handle: handle}, epoch_j2000_s) when is_number(epoch_j2000_s) do
NIF.space_weather_ap_array_at(handle, epoch_j2000_s / 1.0)
rescue
e in ErlangError -> {:error, e.original}
end
@spec coverage(t()) :: {:ok, Coverage.t()} | {:error, term()}
def coverage(%__MODULE__{handle: handle}) do
case NIF.space_weather_coverage(handle) do
{:ok, fields} -> {:ok, struct!(Coverage, fields)}
{:error, _} = err -> err
end
rescue
e in ErlangError -> {:error, e.original}
end
@spec to_csv_text(t()) :: {:ok, String.t()} | {:error, term()}
def to_csv_text(%__MODULE__{handle: handle}), do: {:ok, NIF.space_weather_to_csv_text(handle)}
@spec to_txt_text(t()) :: {:ok, String.t()} | {:error, term()}
def to_txt_text(%__MODULE__{handle: handle}), do: {:ok, NIF.space_weather_to_txt_text(handle)}
def parse!(bytes), do: bang(parse(bytes))
def load!(path), do: bang(load(path))
def space_weather_at!(table, epoch_j2000_s), do: bang(space_weather_at(table, epoch_j2000_s))
def sample_at!(table, epoch_j2000_s), do: bang(sample_at(table, epoch_j2000_s))
def sample_at_with_policy!(table, epoch_j2000_s, policy),
do: bang(sample_at_with_policy(table, epoch_j2000_s, policy))
def ap_array_at!(table, epoch_j2000_s), do: bang(ap_array_at(table, epoch_j2000_s))
def coverage!(table), do: bang(coverage(table))
defp to_sample(fields) do
%Sample{
space_weather: to_space_weather(fields.space_weather),
class: fields.class,
ap_defaulted: fields.ap_defaulted
}
end
defp to_space_weather(fields) do
%Drag.SpaceWeather{f107: fields.f107, f107a: fields.f107a, ap: fields.ap}
end
defp policy_map(%Policy{} = policy), do: Map.from_struct(policy)
defp policy_map(opts) when is_list(opts) do
defaults = %Policy{}
struct!(Policy, Keyword.take(opts, Map.keys(Map.from_struct(defaults)))) |> Map.from_struct()
end
defp bang({:ok, value}), do: value
defp bang({:error, reason}), do: raise(ArgumentError, "space-weather operation failed: #{inspect(reason)}")
end