# `Monty.Frameworks.Utils.LoggingUtils`
[🔗](https://github.com/d-led/ex.tbp.monty/blob/v0.1.0/packages/ex_tbp_monty_core/lib/monty/frameworks/utils/logging_utils.ex#L1)

`tbp/monty/frameworks/utils/logging_utils.py`: what the loggers write with.
The Python file is the largest in Monty's framework, and most of it is pandas
shape-shifting and plotting (`lm_stats_to_dataframe`, `get_rgba_frames_single_sm`,
the buffer-size recipe `total_size`) that exists to feed a notebook. What is
here is the part a logger cannot do without, ported as it is needed rather than
as it appears:

  * `maybe_rename_existing_file/1` and `maybe_rename_existing_dir/1` — a run's
    files belong to that run, so what a previous run left is moved aside, once,
    with a `_old` suffix. Every handler that writes a file needs this, which is
    why it is here and not copied into each of them.
  * `target_data_to_dict/1` — the primary target as the columns a CSV carries:
    object, position, rotation, scale.
  * `compute_pose_error/3` — how far a recognition's pose was from the pose the
    object was placed with, which is the `rotation_error` a CSV column holds.

`total_size/1` is deliberately absent: Monty keeps it for the size-based logging
it removed (`maybe_log` is dead code with a "remove if not used" note), and a
memory-footprint recipe has nothing to do in a language that does not flatten
objects into dicts.

## Differences from Monty, on purpose

  * The renames return `:ok` or `{:error, reason}` instead of raising (Python
    raises on a directory that is not one). A logger that cannot move a file
    aside can still report the reason and stop, which the fan-out already
    handles; a raise from inside a callback loses the rest of the episode.
  * A target key that is absent is `nil` rather than a `KeyError`: it becomes an
    empty cell in the CSV, which is the truth — the run did not have a position
    to record — where a crash would take the whole episode with it.

# `compute_pose_error`

```elixir
@spec compute_pose_error(
  Monty.Geometry.Rotation.t() | [Monty.Geometry.Rotation.t()],
  Monty.Geometry.Rotation.t(),
  keyword()
) :: float()
```

The angular error between a predicted rotation and the target's.

Monty's `compute_pose_error`, which is the smallest of `compute_pose_errors`: the
geodesic distance on SO(3), the angle of the relative rotation. It is what a
recognition's `rotation_error` is, so a run's pose accuracy is this number and
not a difference of Euler angles — two rotations can differ by nought in every
Euler angle and still be different rotations.

`predicted` may be a list, in which case the closest one wins, which is Monty's
`np.min` over the errors: a symmetric object seen at any of its equivalent poses
is not wrong.

# `compute_pose_errors`

```elixir
@spec compute_pose_errors(
  Monty.Geometry.Rotation.t() | [Monty.Geometry.Rotation.t()],
  Monty.Geometry.Rotation.t()
) :: [float()]
```

The angular error of every predicted rotation against the target's.

Monty's `compute_pose_errors`, which `compute_pose_error/3` is the smallest of:
a hypothesis space's pose errors are the whole distribution, not its best
member, and that is what the pyview dashboard's telemetry draws.

# `format_columns_for_wandb`

```elixir
@spec format_columns_for_wandb(map()) :: map()
```

`format_columns_for_wandb`: the one column a table cannot take as it is.

Monty's words: "various columns break wandb because we are playing fast and loose
with types". A result is a list of object names, and a list in a cell makes a
table column whose type depends on the row; joined with `^` it stays a string in
every row. Everything else is passed through.

Monty deep-copies first, because Python mutates a dict in place. A map here is
already a value, so there is nothing to copy and nothing to be careful about —
which is the only difference between the two.

# `get_rgba_frames_single_sm`

```elixir
@spec get_rgba_frames_single_sm([map()]) :: [[[[number()]]]]
```

`get_rgba_frames_single_sm`: a run of frames as wandb.Video wants them.

A frame is rows of pixels — `(height, width, channels)` — and wandb takes
`(channels, height, width)` per timestep, which is Monty's `np.moveaxis`. A
frame with no pixels has no channels to move, and is passed through as the empty
frame it is.

# `graph_lm_episode_stats`

```elixir
@spec graph_lm_episode_stats(struct()) :: map()
```

`get_graph_lm_episode_stats`: one episode's stats for one learning module.

The performance is a judgement about an object, so it needs three things the
module has to have been told or to have worked out: the object it was asked
about (`primary_target`), the object it concluded (`detected_object`), and which
objects the graph it matched was built from (`graph_id_to_target`). Without the
last one there is no way to tell a recognition from a coincidence, which is why
a training episode records it.

A module that was never on an object reports `patch_off_object` and no steps: it
was never in the episode, and calling that a failure would put a wrong number in
a file someone averages.

# `lm_stats_to_dataframe`

```elixir
@spec lm_stats_to_dataframe(%{optional(term()) =&gt; map()}, keyword()) :: %{
  columns: [String.t()],
  data: [[term()]]
}
```

`lm_stats_to_dataframe`: an episode's learning modules as one table.

Monty builds a pandas frame: a row per learning module, a column per statistic,
and `lm_id` last saying which module the row is. A table is what wandb takes, so
that is what this answers — `%{columns: [...], data: [[...], ...]}` — with
`lm_id` appended as pandas appends it. The columns are in name order: pandas
keeps dict insertion order, and an Elixir map has none to keep, so a table whose
columns depended on how a map happened to be built would be a table that changed
for reasons a reader cannot see.

Sensor modules and the motor system are in the same dictionary and are not rows:
a table a reader compares is made of the things that are comparable. An episode
with no learning module in it is no rows rather than an error.

Options: `:format_for_wandb`, which runs each module's statistics through
`format_columns_for_wandb/1` first.

# `maybe_rename_existing_dir`

```elixir
@spec maybe_rename_existing_dir(Path.t()) :: :ok | {:error, term()}
```

Moves `path` to `<name>_old` if it is a directory, replacing an older `_old`.

A path that exists and is not a directory is `{:error, {:not_a_directory, path}}`
— the caller asked for a directory to be moved aside and got a file, which is a
run set up wrong rather than a run with nothing to do.

# `maybe_rename_existing_file`

```elixir
@spec maybe_rename_existing_file(Path.t()) :: :ok | {:error, term()}
```

Moves `path` to `<name>_old<ext>` if it exists, replacing an older `_old`.

One `_old`, not a pile of them: a run that has been restarted three times is
still one previous run.

# `stats_per_lm`

```elixir
@spec stats_per_lm([struct()], map() | nil, keyword()) :: %{
  optional(String.t()) =&gt; map()
}
```

`get_stats_per_lm`: every learning module's stats for one episode.

Keyed `LM_0`, `LM_1` … by the module's position, which is the key Monty's CSV
rows and its `lm_id` column are written from, and each module's stats carry the
run's step counts, the episode's seed and the target — because a row that cannot
say which object it was about cannot be compared with one that can.

Options: `:step` and `:matching_steps`, the run's counters (Monty's
`model.episode_steps` and `model.matching_steps`), and `:episode_seed`.

# `target_data_to_dict`

```elixir
@spec target_data_to_dict(map()) :: %{optional(String.t()) =&gt; term()}
```

The primary target as the columns a CSV carries.

Monty reads `object`, `position`, `euler_rotation`, `rotation` and `scale` off
the environment's target and names them `primary_target_*`, because those are
the names its readers filter on. Scale is uniform along every dimension, so only
its first entry is recorded.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
