Packages

A configurable spherical geodesic grid data model designed for simulations GEOF Planet offers an ideal data model for simulations of 2½D spherical phenomena like oceanography and climate. It is ported from the JavaScript project g-e-o-f/peels.

Current section

Files

Jump to
geof_planet lib servers panel_server.ex
Raw

lib/servers/panel_server.ex

defmodule GEOF.Planet.PanelServer do
@moduledoc """
Hosts per-Field compute frames for a set of Fields in a Panel, a contiguous subset of a Sphere's Fields.
"""
use GenServer
alias GEOF.Planet.{
Field,
Registry,
SphereServer
}
###
#
# API
#
###
@spec start_link(SphereServer.sphere(), SphereServer.panel_index()) :: GenServer.on_start()
def start_link(sphere, panel_index) do
GenServer.start_link(__MODULE__, [sphere, panel_index],
name: Registry.panel_via_reg(sphere.id, panel_index)
)
end
# `get_state` is for testing purposes only
def get_state(sphere_id, panel_index) do
GenServer.call(Registry.panel_via_reg(sphere_id, panel_index), :get_state)
end
@doc "Gets the data in each Field managed by this Panel"
@spec get_all_field_data(SphereServer.sphere_id(), SphereServer.panel_index()) ::
SphereServer.sphere_data()
def get_all_field_data(sphere_id, panel_index) do
GenServer.call(Registry.panel_via_reg(sphere_id, panel_index), :get_all_field_data)
end
@doc "Starts a computation frame on this Panel. The PanelServer will send `__ready_to_commit_frame__` to its parent SphereServer when the frame is finished."
@spec start_frame(SphereServer.sphere_id(), SphereServer.panel_index(), SphereServer.fn_ref()) ::
:ok
def start_frame(sphere_id, panel_index, per_field) do
GenServer.cast(
Registry.panel_via_reg(sphere_id, panel_index),
{:start_frame, per_field}
)
end
###
#
# Server
#
###
###
# Utility
###
@impl true
def init([sphere, panel_index]) do
fields = sphere.fields_at_panels[panel_index]
adjacent_fields = init_adjacent_fields(sphere, panel_index)
{:ok,
%{
id: {sphere.id, panel_index},
divisions: sphere.divisions,
fields: fields,
field_data: init_field_data(fields),
adjacent_fields: adjacent_fields,
n_adjacent_fields: n_adjacent_fields(adjacent_fields),
frame_fun: nil
}}
end
@spec init_field_data(MapSet.t(Field.index())) :: SphereServer.sphere_data()
defp init_field_data(field_set) do
Enum.reduce(field_set, %{}, &Map.put(&2, &1, nil))
end
@impl true
def handle_call(:get_all_field_data, _from, state) do
{:reply, state.field_data, state}
end
@impl true
def handle_call(:get_state, _from, state) do
{:reply, state, state}
end
@impl true
def handle_call(:__commit_frame__, _from, state) do
state =
Map.merge(state, %{
frame_fun: nil,
field_data: state.__current_frame__
})
state = Map.delete(state, :__current_frame__)
{:reply, :ok, state}
end
###
# Adjacent field computation
###
@doc """
Creates a `fields_at_panels` mapping panel indexes to the fields belonging to those
panels which are adjacent to _this_ panel. Used to share relevant field data between processes.
"""
@spec init_adjacent_fields(SphereServer.sphere(), SphereServer.panel_index()) ::
SphereServer.fields_at_panels()
defp init_adjacent_fields(sphere, panel_index) do
panel_field_set = sphere.fields_at_panels[panel_index]
Enum.reduce(
panel_field_set,
init_adjacent_fields(Map.keys(sphere.fields_at_panels)),
fn field_index, adjacent_fields_at_panels ->
Stream.reject(
Field.adjacents(field_index, sphere.divisions),
fn {_direction, adjacent_field_index} ->
adjacent_field_index == nil or MapSet.member?(panel_field_set, adjacent_field_index)
end
)
|> Enum.reduce(
adjacent_fields_at_panels,
fn {_direction, foreign_adjacent_field_index}, adjacent_fields_at_panels ->
update_in(
adjacent_fields_at_panels[
panel_for_field(sphere.fields_at_panels, foreign_adjacent_field_index)
],
&MapSet.put(&1, foreign_adjacent_field_index)
)
end
)
end
)
end
defp panel_for_field(fields_at_panels, field_index) do
{panel_index, _field_set} =
Enum.find(fields_at_panels, fn {_panel_index, field_set} ->
MapSet.member?(field_set, field_index)
end)
panel_index
end
defp init_adjacent_fields(panel_indexes) do
Enum.reduce(panel_indexes, %{}, fn panel_index, adjacent_fields_at_panels ->
Map.put(adjacent_fields_at_panels, panel_index, MapSet.new())
end)
end
defp n_adjacent_fields(adjacent_fields) do
Enum.reduce(adjacent_fields, 0, fn {_panel_index, fields_at_panel}, sum ->
sum + MapSet.size(fields_at_panel)
end)
end
###
# Frames
###
@impl true
def handle_cast({:start_frame, per_field}, state) do
{sphere_id, panel_index} = state.id
state = Map.put(state, :frame_fun, per_field)
state = Map.put(state, :adjacent_field_data, Map.new())
Enum.each(state.adjacent_fields, fn {adjacent_panel_index, fields} ->
if adjacent_panel_index != panel_index and MapSet.size(fields) > 0 do
GenServer.cast(
Registry.panel_via_reg(sphere_id, adjacent_panel_index),
{:__send_field_data__, panel_index, fields}
)
end
end)
{:noreply, state}
end
@impl true
def handle_cast({:__send_field_data__, to_panel_index, fields}, state) do
{sphere_id, _panel_index} = state.id
Enum.each(fields, fn field_index ->
if Map.has_key?(state.field_data, field_index) do
GenServer.cast(
Registry.panel_via_reg(sphere_id, to_panel_index),
{:__receive_field_data__, field_index, state.field_data[field_index]}
)
end
end)
{:noreply, state}
end
@impl true
def handle_cast({:__receive_field_data__, field_index, data_for_field}, state) do
# store field_data in a cache of data for adjacent fields
state =
update_in(
state.adjacent_field_data,
&Map.put(&1, field_index, data_for_field)
)
# evaluate if we're ready to begin iterating
if ready_to_populate_frame?(state) do
GenServer.cast(self(), :__populate_frame__)
end
{:noreply, state}
end
@impl true
def handle_cast(:__populate_frame__, state) do
{sphere_id, panel_index} = state.id
{module, function_name} = state.frame_fun
state =
Map.put(
state,
:__current_frame__,
Enum.reduce(state.fields, %{}, fn field_index, current_frame ->
Map.put(
current_frame,
field_index,
apply(
module,
function_name,
[
{field_index, Map.get(state.field_data, field_index)},
get_adjacents_for_apply(state, field_index)
]
)
)
end)
)
GenServer.cast(Registry.sphere_via_reg(sphere_id), {:__ready_to_commit_frame__, panel_index})
{:noreply, state}
end
defp ready_to_populate_frame?(state) do
map_size(state.adjacent_field_data) >= state.n_adjacent_fields
end
defp get_adjacents_for_apply(state, field_index) do
{_sphere_id, panel_index} = state.id
Map.new(
Field.adjacents(field_index, state.divisions),
fn {dir, adjacent_field_index} ->
cond do
Map.has_key?(state.field_data, adjacent_field_index) ->
{dir, {adjacent_field_index, state.field_data[adjacent_field_index]}}
Map.has_key?(state.adjacent_field_data, adjacent_field_index) ->
{dir, {adjacent_field_index, state.adjacent_field_data[adjacent_field_index]}}
true ->
exit(
"Field data logistical error: data for Field #{
Field.index_to_string(adjacent_field_index)
} not available for panel {ref, #{panel_index}}}"
)
end
end
)
end
end