lib/retex.ex

defmodule Retex do
  @moduledoc """
  The algorithm utilizes symbols to create an internal representation of the world.
  Each element in the real world is converted into a triple known as a "Working Memory Element" (`Retex.Wme.t()`),
  represented as {Entity, attribute, attribute_value}.

  The world is represented through facts (WMEs) and Rules.
  A Rule consists of two essential parts: the "given" (right side) and the "then" (left side).

  To perform inference, the rule generates a directed graph starting from a common and generic Root node,
  which branches out to form leaf nodes. The branches from the Root node correspond to the initial part
  of the WME, representing the working memory elements or "Entity". For instance, if we want to
  represent a customer's account status as "silver", we would encode it as "{Customer, account_status, silver}".
  Alternatively, with the use of a struct, we can achieve the same representation as `Retex.Wme.new("Customer", "account status", "silver")`



  ## The struct
  This module also defines a Struct with the following fields:

  1. `graph: Graph.new()`: This field is initialized with the value returned by the `Graph.new()` function. It is a reference to a directed graph data structure (using `libgraph`)
  that represents a network of interconnected nodes and vertices.

  2. `wmes: %{}`: This field is initialized as an empty map. It is expected to store "working memory elements" (WMEs), which are pieces of information or facts used in the system.

  3. `agenda: []`: This field is initialized as an empty list. It is intended to store a collection of tasks or items that need to be processed or executed. The agenda typically represents a prioritized queue of pending actions.

  4. `activations: %{}`: This field is initialized as an empty map. It is used to store information related to the activations of nodes in the network.

  5. `wme_activations: %{}`: This field is initialized as an empty map. It is similar to the `activations` field but specifically focuses on the activations of working memory elements (WMEs) and can serve as reverse lookup of which facts have activated which no.

  6. `tokens: MapSet.new()`: This field is initialized with the value returned by the `MapSet.new()` function. Will be deprecated soon because it has no use but was a porting from the original paper.

  7. `bindings: %{}`: This field is initialized as an empty map. It is used to store variable bindings or associations between variables and their corresponding values. This can be useful for tracking and manipulating data within the system.

  8. `pending_activation: []`: This field is initialized as an empty list. It is likely used to keep track of activations that are pending or awaiting further processing. The exact meaning and usage of these pending activations would depend on the system's design.
  """

  @type t() :: %Retex{}
  alias Retex.{Fact, Node, Protocol, Protocol.AlphaNetwork, Token}

  alias Node.{
    BetaMemory,
    PNode,
    Select,
    Test,
    Type
  }

  @type action :: %{given: list(Retex.Wme.t()), then: list(Retex.Wme.t())}
  @type network_node :: Type.t() | Test.t() | Select.t() | PNode.t() | BetaMemory.t()

  # the Retex network is made of the following elements:
  defstruct graph: Graph.new(),
            wmes: %{},
            agenda: [],
            activations: %{},
            wme_activations: %{},
            tokens: MapSet.new(),
            bindings: %{},
            pending_activation: []

  @doc """
  Generate a new Retext.Root.t() struct that represents the first node of the network.
  An anonymous node that functions just as connector for the type nodes (`Retex.Node.Type.t()`)
  """
  @spec root_vertex :: Retex.Root.t()
  def root_vertex, do: Retex.Root.new()

  @doc @moduledoc
  @spec new :: Retex.t()
  def new do
    %{graph: graph} = %Retex{}
    graph = Graph.add_vertex(graph, Retex.Root.new())
    %Retex{graph: graph}
  end

  @doc """
  Takes the network itself and a WME struct and tries to activate as many nodes as possible traversing the graph
  from the Root until each reachable branch executing a series of "tests" (pattern matching) at each node level.

  Each node is tested implementing the activation protocol, so to know if how the test for the node against the WME
  works check their protocol implementation.
  """
  @spec add_wme(Retex.t(), Retex.Wme.t()) :: Retex.t()
  def add_wme(%Retex{} = network, %Retex.Wme{} = wme) do
    # just a timestamp might be useful for the future
    wme = Map.put(wme, :timestamp, :os.system_time(:seconds))
    # store the new fact in memory with their ID
    network = %{network | wmes: Map.put(network.wmes, wme.id, wme)}
    # traverse the network for the Root node branching out and testing each node for possible
    # activations
    {network, bindings} = propagate_activations(network, root_vertex(), wme, network.bindings)

    # if there is some activation for variables store such mapping (variable name => value) in memory
    %{network | bindings: Map.merge(network.bindings, bindings)}
  end

  @doc """
  A production is what is called a Rule in the original Rete paper from C. Forgy

  A production is a map of given and then and each of those fields contains a list of
  `Retex.Fact.t()` which can be tested against a `Retex.Wme.t()`
  """
  @spec add_production(Retex.t(), %{given: list(Retex.Wme.t()), then: action()}) :: t()
  def add_production(%{graph: graph} = network, %{given: given, then: action}) do
    # Filters are extra tests that ca be added to the given part of the rule
    # it is an extra feature present only in Retex and a personal choice
    {filters, given} = Enum.split_with(given, &is_filter?/1)
    # Take each fact in the given part of the rule and find or create the corresponding node
    # this part of what you see in the graph at https://github.com/lorenzosinisi/retex#how-does-it-work
    {graph, alphas} = build_alpha_network(graph, given)
    # this second part is just the representation of each "and" in a rule.
    {beta_memory, graph} = build_beta_network(graph, alphas)

    # finally add the "then" part of the rule on the last leaf of that subgraph (generated by the Rule)
    graph = add_p_node(graph, beta_memory, action, filters)

    %{network | graph: graph}
  end

  defp is_filter?(%Fact.Filter{}), do: true
  defp is_filter?(_), do: false

  @doc """
  Take each fact of the "given" part of the rule and construct the alpha part of the network destructuring the facts
  into "Root" -> "Entity" -> "Attribute" -> "Value" (if a node with the same value already exists it will be a noop)
  """
  @spec build_alpha_network(Graph.t(), list()) :: {Graph.t(), list()}
  def build_alpha_network(graph, given) do
    Enum.reduce(given, {graph, []}, &AlphaNetwork.append(&1, &2))
  end

  @doc """
  After building the alpha network we will have a list of nodes which are the bottom of the new subnetwork,
  not connect those two by two. Take the firs two, join them with a new node, that that one node
  connect it with the next orphan node, keep doing it until all the facts of a rule are connected together and
  we have one last "Join" node.

  And example of a graph with only one "and/join" node:

  ```mermaid
  flowchart
    2102090852["==100"]
    2332826675["==silver"]
    2833714732["[{:Discount, :code, 50}]"]
    3108351631["Root"]
    3726656564["Join"]
    3801762854["miles"]
    3860425667["Customer"]
    3895425755["account_status"]
    4112061991["Flight"]
    2102090852 --> 3726656564
    2332826675 --> 3726656564
    3108351631 --> 3860425667
    3108351631 --> 4112061991
    3726656564 --> 2833714732
    3801762854 --> 2102090852
    3860425667 --> 3895425755
    3895425755 --> 2332826675
    4112061991 --> 3801762854
  ```
  """
  @spec build_beta_network(Graph.t(), list(network_node())) :: {list(network_node()), Graph.t()}
  def build_beta_network(graph, [first, second | list]) do
    id = Retex.hash(Enum.sort([first, second]))
    beta_memory = Node.BetaMemory.new(id)

    graph
    |> Graph.add_vertex(beta_memory)
    |> Graph.add_edge(first, beta_memory)
    |> Graph.add_edge(second, beta_memory)
    |> build_beta_network([beta_memory | list])
  end

  def build_beta_network(graph, [beta_memory]) do
    {beta_memory, graph}
  end

  @doc """
  The P node is the production node, just another name of a rule
  """
  @spec add_p_node(Graph.t(), BetaMemory.t(), action(), list(Fact.Filter.t())) :: Graph.t()
  def add_p_node(graph, beta_memory, action, filters) do
    pnode = Node.PNode.new(action, filters)
    graph |> Graph.add_vertex(pnode) |> Graph.add_edge(beta_memory, pnode)
  end

  @max_phash 4_294_967_296
  @spec hash(any) :: String.t()
  def hash(:uuid4), do: UUIDTools.uuid4()

  def hash(data) do
    :erlang.phash2(data, @max_phash)
  end

  @spec replace_bindings(PNode.t(), map) :: PNode.t()
  def replace_bindings(%_{action: action_fun} = pnode, bindings)
      when is_function(action_fun) do
    %{pnode | action: action_fun, bindings: bindings}
  end

  def replace_bindings(%_{action: actions} = pnode, bindings)
      when is_list(actions) and is_map(bindings) do
    new_actions = Enum.map(actions, fn action -> replace_bindings(action, bindings) end)
    %{pnode | action: new_actions, bindings: bindings}
  end

  def replace_bindings(%Retex.Wme{} = action, bindings) when is_map(bindings) do
    populated =
      for {key, val} <- Map.from_struct(action), into: %{} do
        val = Map.get(bindings, val, val)
        {key, val}
      end

    struct(Retex.Wme, populated)
  end

  def replace_bindings(tuple, bindings) when is_map(bindings) and is_tuple(tuple) do
    List.to_tuple(
      for element <- Tuple.to_list(tuple) do
        if is_binary(element), do: Map.get(bindings, element, element), else: element
      end
    )
  end

  def replace_bindings(anything, _bindings) do
    anything
  end

  @spec add_token(Retex.t(), network_node(), Retex.Wme.t(), map, list(Retex.Token.t())) ::
          Retex.t()
  def add_token(
        %Retex{tokens: rete_tokens} = rete,
        current_node,
        _wme,
        _bindings,
        [_ | _] = tokens
      ) do
    node_tokens =
      rete_tokens
      |> Map.get(current_node.id, [])
      |> MapSet.new()

    all_tokens =
      node_tokens
      |> MapSet.new()
      |> MapSet.union(MapSet.new(tokens))

    new_tokens = Map.put(rete_tokens, current_node.id, all_tokens)

    %{rete | tokens: new_tokens}
  end

  def add_token(%Retex{tokens: rete_tokens} = rete, current_node, wme, bindings, tokens) do
    node_tokens =
      rete_tokens
      |> Map.get(current_node.id, [])
      |> MapSet.new()

    token = Token.new()

    token = %{
      token
      | wmem: wme,
        node: current_node.id,
        bindings: bindings
    }

    all_tokens =
      [token]
      |> MapSet.new()
      |> MapSet.union(node_tokens)
      |> MapSet.union(MapSet.new(tokens))

    new_tokens = Map.put(rete_tokens, current_node.id, all_tokens)
    %{rete | tokens: new_tokens}
  end

  @spec create_activation(Retex.t(), network_node(), Retex.Wme.t()) :: Retex.t()
  def create_activation(
        %__MODULE__{activations: activations, wme_activations: wme_activations} = rete,
        current_node,
        wme
      ) do
    node_activations = Map.get(activations, current_node.id, [])
    new_activations = [wme.id | node_activations]
    new_rete = %{rete | activations: Map.put(activations, current_node.id, new_activations)}
    previous_wme_activations = Map.get(wme_activations, wme.id, [])

    new_wme_activations =
      Map.put(wme_activations, wme.id, [current_node.id | previous_wme_activations])

    %{new_rete | wme_activations: new_wme_activations}
  end

  @spec propagate_activations(
          Retex.t(),
          network_node(),
          Retex.Wme.t(),
          map,
          list(Retex.Token.t())
        ) :: {Retex.t(), map}
  def propagate_activations(
        %Retex{graph: graph} = rete,
        %{} = current_node,
        %Retex.Wme{} = wme,
        bindings,
        new_tokens
      ) do
    graph
    |> Graph.out_neighbors(current_node)
    |> Enum.reduce({rete, bindings}, fn vertex, {network, bindings} ->
      propagate_activation(vertex, network, wme, bindings, new_tokens)
    end)
  end

  @spec propagate_activations(Retex.t(), network_node(), Retex.Wme.t(), map) :: {Retex.t(), map}
  def propagate_activations(
        %Retex{graph: graph} = rete,
        %{} = current_node,
        %Retex.Wme{} = wme,
        bindings
      ) do
    graph
    |> Graph.out_neighbors(current_node)
    |> Enum.reduce({rete, bindings}, fn vertex, {network, bindings} ->
      propagate_activation(vertex, network, wme, bindings)
    end)
  end

  defp propagate_activation(neighbor, rete, wme, bindings, tokens \\ []) do
    Protocol.Activation.activate(neighbor, rete, wme, bindings, tokens)
  end

  @spec deactivate_descendants(Retex.t(), network_node()) :: Retex.t()
  def deactivate_descendants(%Retex{activations: activations} = rete, %{} = current_node) do
    %{graph: graph} = rete
    children = Graph.out_neighbors(graph, current_node)

    Enum.reduce(children, rete, fn %type{} = vertex, network ->
      if type == Retex.Node.PNode do
        %{
          network
          | agenda: Enum.reject(network.agenda, fn pnode -> pnode.id == vertex.id end)
        }
      else
        new_network = %{network | activations: Map.put(activations, vertex.id, [])}
        deactivate_descendants(new_network, vertex)
      end
    end)
  end

  @spec continue_traversal(Retex.t(), map, network_node(), Retex.Wme.t(), list(Retex.Token.t())) ::
          {Retex.t(), map}
  def continue_traversal(
        %Retex{} = new_rete,
        %{} = new_bindings,
        %_{} = current_node,
        %Retex.Wme{} = wme,
        tokens
      ) do
    propagate_activations(new_rete, current_node, wme, new_bindings, tokens)
  end

  @spec continue_traversal(Retex.t(), map, network_node(), Retex.Wme.t()) :: {Retex.t(), map}
  def continue_traversal(
        %Retex{} = new_rete,
        %{} = new_bindings,
        %_{} = current_node,
        %Retex.Wme{} = wme
      ) do
    propagate_activations(new_rete, current_node, wme, new_bindings)
  end

  @spec stop_traversal(Retex.t(), map) :: {Retex.t(), map}
  def stop_traversal(%Retex{} = rete, %{} = bindings) do
    {rete, bindings}
  end
end