Monty.Frameworks.Models.GoalGeneration.EvidenceGoalGenerator (ExTbpMonty.Core v0.1.0)

Copy Markdown View Source

Look where the candidates disagree most, then look again.

Mirrors tbp/monty/frameworks/models/goal_generation.py::EvidenceGoalGenerator, the goal-state generator that turns "I cannot tell these objects apart" into one concrete place to look. It compares the graph of the most likely object with the graph of the second most likely one, and asks for the point on the first that is farthest from anything on the second: a mug's handle, when the alternatives are handleless cups.

Monty subclasses GraphGoalGenerator; Elixir has no inheritance, so this holds one instead and delegates the parts Monty does not override — the parent module, the goal itself, and the crude "did the sensory input change" test for whether the last goal was reached. What the subclass does override is all here: when to look somewhere new, where, and the goal that points there.

The schedule

A hypothesis jump is a gamble: move to where the two candidates disagree, and the observation there should favour one of them. Gambling every step makes the agent walk back and forth over the object and learn nothing, so check_conditions_for_hypothesis_test/2 spends most steps matching and jumps when the odds are good:

  • never, until min_post_goal_success_steps steps have passed since the last goal — Monty's default is :infinity, so only an experiment that asks for it jumps at all;
  • when one object is left but several poses, to separate the poses;
  • when one object is clearly ahead but not certain, on half the occasions;
  • when the top two objects change, or the most likely pose does;
  • otherwise, every wait_factor * elapsed_steps_factor steps, with wait_factor doubling after each jump so a test that taught nothing is not repeated on a schedule that keeps spending.

A quirk kept on purpose

Monty writes both the top-two check and the pose check as np.all(...), so they fire only when every compared value differs: both object ids must change, and all three Euler angles must change. That is stricter than the comment above it ("a change or shuffle in top-two MLH IDs") describes, and it is reproduced rather than corrected, because correcting it would change when the agent moves and make our numbers incomparable with Monty's.

Summary

Types

The most likely hypothesis of an object, or of everything: what EvidenceGraphLM.get_mlh_for_object/2 answers.

t()

Where to look: which hypothesis would be tested, and the point on its graph.

Functions

Whether there is a good chance of separating the competing objects or poses.

A goal that puts the sensor above the target point, facing its surface.

The node of the most likely object that the alternative explains worst.

The generator a config entry describes.

The statistics this generator keeps on its parent's buffer.

A generator that has not looked anywhere yet.

The goal this generator wants others to achieve, or none.

The learning module this generator reads, or an error when there is none.

The same generator, now reading parent_lm as it stands.

Points this generator at its learning module and resets it.

Forgets the goal in hand, the schedule, and the log.

Receives the high-level goal to drive towards; unused, as in Monty.

One step: log whether the last goal was reached, then decide what to do.

Where the point to test is, on the most likely object's graph.

Types

mlh()

The most likely hypothesis of an object, or of everything: what EvidenceGraphLM.get_mlh_for_object/2 answers.

t()

@type t() :: %Monty.Frameworks.Models.GoalGeneration.EvidenceGoalGenerator{
  base: Monty.Frameworks.Models.GoalGeneration.GraphGoalGenerator.t(),
  desired_object_distance: number(),
  elapsed_steps_factor: pos_integer(),
  focus_on_pose: boolean(),
  min_post_goal_success_steps: non_neg_integer() | :infinity,
  prev_top_mlhs: [mlh()] | nil,
  wait_factor: pos_integer(),
  wait_growth_multiplier: number(),
  x_percent_scale_factor: number()
}

target_info()

@type target_info() :: %{
  hypothesis_to_test: mlh(),
  target_loc: [float()],
  target_surface_normal: [float()]
}

Where to look: which hypothesis would be tested, and the point on its graph.

target_surface_normal is the learned normal of that node, in the object's own frame — the direction a sensor has to face to observe it.

Functions

check_conditions_for_hypothesis_test(generator, ctx)

@spec check_conditions_for_hypothesis_test(t(), Monty.Context.t()) :: {boolean(), t()}

Whether there is a good chance of separating the competing objects or poses.

Returns the answer together with the generator, because deciding changes it: focus_on_pose is cleared at the start of every call, and the elapsed-steps branch grows wait_factor. Monty mutates both in place; a struct cannot.

compute_goal_for_target_loc(generator, observations, target_info)

@spec compute_goal_for_target_loc(t(), [Monty.Cmp.Message.t()], target_info()) ::
  {:ok, Monty.Cmp.Goal.t()} | {:error, term()}

A goal that puts the sensor above the target point, facing its surface.

Monty's _compute_goal_for_target_loc. The displacement from the hypothesis's own location to the target is rotated out of the object's frame by the object pose, added to where the sensor is now, and then pushed off the surface along the (equally rotated) normal so that the sensor starts outside the object and looks down at it.

The roll is left undefined, as in Monty: which way is up does not matter for telling two objects apart, and the policy that acts on this goal decides it.

compute_graph_mismatch(generator)

@spec compute_graph_mismatch(t()) :: {:ok, non_neg_integer(), t()} | {:error, term()}

The node of the most likely object that the alternative explains worst.

Both graphs are brought into the same frame first: the MLH's rotation is the rotation that would turn a displacement into the object's pose, so its inverse is the object's pose, and applying it puts the graph in environmental coordinates. The second object's stored rotation then does the opposite, so both graphs are expressed as they were learned and the comparison is between shapes rather than between placements.

Returns the index into the most likely object's positions — the point to look at — and the generator, whose prev_top_mlhs now records what was compared, so the next step can notice if either changed.

from_config(config)

@spec from_config(map()) :: t()

The generator a config entry describes.

The entry is Monty's gsg: under a learning module — goal_tolerances by feature, then the schedule: how many steps without a goal before one is worth trying, the x percent scale factor, how close to the object a goal counts as reached, and how fast the wait between attempts grows.

goal_stat_keys()

@spec goal_stat_keys() :: [String.t()]

The statistics this generator keeps on its parent's buffer.

new(options \\ [])

@spec new(keyword()) :: t()

A generator that has not looked anywhere yet.

Options are :parent_lm (the learning module to read), :goal_tolerances and :parent_output (handed to the base generator), and Monty's five schedule parameters: :elapsed_steps_factor, :min_post_goal_success_steps, :x_percent_scale_factor, :desired_object_distance and :wait_growth_multiplier.

output_goals(evidence_goal_generator)

@spec output_goals(t()) :: [Monty.Cmp.Goal.t()]

The goal this generator wants others to achieve, or none.

parent_lm(evidence_goal_generator)

@spec parent_lm(t()) :: struct()

The learning module this generator reads, or an error when there is none.

point_at(generator, parent_lm)

@spec point_at(t(), map()) :: t()

The same generator, now reading parent_lm as it stands.

Someone driving this generator step by step has the module's current value in hand and the generator has a copy; this swaps the copy without throwing away the schedule. See GraphGoalGenerator.point_at/2.

put_parent_lm(generator, parent_lm)

@spec put_parent_lm(t(), struct()) :: t()

Points this generator at its learning module and resets it.

The schedule is part of the reset: wait_factor and prev_top_mlhs describe what this module has already tried, and another module's tries say nothing about this one.

reset(generator)

@spec reset(t()) :: t()

Forgets the goal in hand, the schedule, and the log.

set_driving_goal(generator, goal)

@spec set_driving_goal(t(), Monty.Cmp.Goal.t() | nil) :: t()

Receives the high-level goal to drive towards; unused, as in Monty.

step(generator, ctx, observations)

@spec step(t(), Monty.Context.t(), [Monty.Cmp.Message.t()]) :: t()

One step: log whether the last goal was reached, then decide what to do.

Reaching the goal is not a reason to want a new one — it means the agent arrived, and arriving is where the judging happens — so an achieved goal is dropped. A goal that was not achieved is dropped too: hypothesis tests are one-off attempts, because an unreachable goal repeated forever is a loop.

target_loc_info(generator, target_loc_id)

@spec target_loc_info(t(), non_neg_integer()) ::
  {:ok, target_info()} | {:error, term()}

Where the point to test is, on the most likely object's graph.

Monty's _get_target_loc_info. The normal is the first three numbers of the node's pose_vectors, which is the surface normal the sensor has to face.