Monty.Frameworks.Utils.LoggingUtils (ExTbpMonty.Core v0.1.0)

Copy Markdown View Source

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.

Summary

Functions

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

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

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

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

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

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

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

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

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

The primary target as the columns a CSV carries.

Functions

compute_pose_error(predicted, target, options \\ [])

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(predicted, target)

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(lm_stats)

@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(observations)

@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(lm)

@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(stats, opts \\ [])

@spec lm_stats_to_dataframe(%{optional(term()) => 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(path)

@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(path)

@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(learning_modules, primary_target, options \\ [])

@spec stats_per_lm([struct()], map() | nil, keyword()) :: %{
  optional(String.t()) => 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(target)

@spec target_data_to_dict(map()) :: %{optional(String.t()) => 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.