Skip to content

Adding generalized 2D envs #117

Merged
merlerm merged 35 commits into
mainfrom
generalized-envs-2d
Jul 15, 2026
Merged

Adding generalized 2D envs #117
merlerm merged 35 commits into
mainfrom
generalized-envs-2d

Conversation

@merlerm

@merlerm merlerm commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

I am working on this, but it's shaping up to be a pretty big PR and this week I will also have rebuttal for ARR, so I wanted to try something different and open a draft PR for now in case anyone else wants to have a look or try it out.

This is currently just claude's first try and I have to test it properly and review everything, starting to do that now

merlerm added 8 commits July 7, 2026 20:29
A robocode env that varies the object count per reset, driving kinder's object-centric layer directly so one program spans instances of different sizes. reset() yields an ObjectCentricState at a chosen count; a per-count ConstantObjectKinDEREnv backend cache gives the planner its Box view. Count is inferred by object-name prefix, not type. build_sesame_models gains per-count observation_space/model_kwargs overrides and fails loud on a variable-count env.
Sweep object counts across the eval set so the program and the planner face the same (seed, count) instances, and scale the step budget with the count. Report a by_count breakdown (solve rate per count over all scheduled episodes, crashes and unattempted counted as failures) plus largest_count_all_solved / largest_count_any_solved, and add plot_scaling.py for the program-vs-planner curve.
Rebuild and cache SeSamE models per object count, vectorizing the object-centric state to that count's Box for the planner; thread the pinned count through the per-instance approaches; teach check_action_collision the variable-count env.
For a variable-count env the generated program receives an ObjectCentricState, so the interface note documents that API (get_objects / get) and forbids shape, devectorize, and fixed-count assumptions.
A self-contained ObjectCentricState<->JSON codec (embedding the type hierarchy, preserving is_instance ancestors), a serialize_space branch for the object-centric space, and a stdlib+numpy local mirror the sandbox reconstructs, so a blackboxed program gets the same state object it sees at eval.
Obstruction2D, ClutteredRetrieval2D, ClutteredStorage2D (odd block counts), Motion2D, and StickButton2D, each with feasible design and held-out eval counts.
For a variable-count env the observation is an ObjectCentricState, not a flat vector, so render_state's arbitrary-state mode (a list of floats) and the devectorize/vectorize guidance do not apply. Thread object_centric through build_system_prompt / build_mcp_tool_lines / mcp_tool_descriptions and swap in a seed-mode + render_policy variant.
Replace the side-effect `import kinder_geom2d_env` in VariableObjectCountEnv with
an explicit configure_gl_backend() call. That wrapper drives the kinder env
classes directly (no kinder.make), so it needs only the GL-backend selection, not
register_all_environments() and the gym registry.

Factor the GL preamble that the 2D and 3D wrappers each duplicated into the new
environments/mujoco_gl.py. This drops the wrong-import-position, unused-import,
and broad-except pylint disables from both wrappers and restores normal import
order.
@merlerm merlerm force-pushed the generalized-envs-2d branch from e3e7bba to e1e2ca9 Compare July 8, 2026 10:29
@merlerm

merlerm commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator Author

Some cool very preliminary stuff with this

StickButton2D — three-way comparison (identical seed=42 instances, 50 evals each)

buttons program +prims program no-prims planner
1 0.90 0.50 1.00
2 1.00 0.30 0.60
3 0.90 0.20 0.10
5 (held-out) 0.80 0.10 0.10
10 (held-out) 0.70 0.00 0.00
overall 0.86 0.22 0.36
cost $4.96 $4.93 $0

merlerm added 3 commits July 8, 2026 14:07
Import pyplot in normal order and call plt.switch_backend("Agg") after, instead
of matplotlib.use("Agg") wedged between imports. Drops the E402 / wrong-import-position
noqa and pylint disables it forced on every following import, plus a redundant
`import matplotlib`.
Two runs of one approach (e.g. agentic with and without primitives) previously
pooled into a single blended line, since collect() keyed only by approach. Track
the source dir each run was found under and split a series only when one approach
spans several sources, so a genuine three-way comparison shows three lines while
seeds of a single run still pool.

Make the reusable helpers (collect, series, by_count, plot_environment) public and
add tests/experiments/test_plot_scaling.py covering source tracking, label
disambiguation, per-count aggregation, and PNG output.
_kinder_check and _variable_count_check differed only in which attribute holds the
active kinder backend, so fold them into _kinder_reference_check(env, backend_attr,
...). This also collapses the six per-line type-ignore/disable annotations into one
block-scope disable (the type-ignores were unnecessary once the inner env is Any).

Add variable-count collision tests (collision, free move, state preservation) so
both dispatch paths through the shared routine are covered.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces support for variable-object-count (generalized) 2D environments by switching observations from fixed-length vectors to object-centric states, and extends the blackbox client/server protocol and experiment tooling to evaluate and plot performance vs. object count.

Changes:

  • Add VariableObjectCountEnv wrapper to expose ObjectCentricState observations while still supporting per-count Box views for bilevel planning.
  • Extend blackbox env client/server to serialize/transport object-centric states (including type hierarchies) and update collision checking + bilevel model building for variable-count instances.
  • Add evaluation/reporting utilities for solve-rate-vs-count curves (summarize_by_count, plotting + result flattening) plus comprehensive tests.

Reviewed changes

Copilot reviewed 31 out of 31 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/utils/test_object_centric_codec.py New tests covering JSON-safe round-trips and blackbox loop for object-centric state transport.
tests/utils/test_episode.py Adds unit tests for per-count aggregation (summarize_by_count) and threads count into scripted approach calls.
tests/utils/test_bilevel.py Skips env-id inference for variable-count env configs that don’t have env_id.
tests/experiments/test_plot_scaling.py New tests for experiments/plot_scaling.py helpers and PNG output.
tests/environments/test_variable_object_count_env.py New tests validating count pinning, determinism, state restore, and description content.
tests/environments/test_collision_checking.py Adds collision-check coverage for VariableObjectCountEnv dispatch.
src/robocode/utils/object_centric_codec.py New host-side JSON codec and space serializer for ObjectCentricState.
src/robocode/utils/episode.py Adds count pinning to episodes, stores object_count in metrics, and implements summarize_by_count.
src/robocode/utils/env_server.py Registers builtin codecs and adds ObjectCentricStateSpace serialization support.
src/robocode/utils/env_server_runtime.py Allows rendering custom states coming from object-centric payloads.
src/robocode/utils/env_client.py Adds a local object-centric mirror in the sandbox and object-centric-aware state handling.
src/robocode/utils/bilevel.py Updates build_sesame_models to support per-count overrides and reject variable-count primitive misuse.
src/robocode/prompts.py Adds object-centric-specific prompt note and MCP prompt suffix selection.
src/robocode/primitives/check_action_collision.py Extends collision checking dispatch to variable-count envs via backend reference checks.
src/robocode/mcp/init.py Adds object-centric variant of MCP tool descriptions and system suffix.
src/robocode/environments/variable_object_count_env.py New variable-count wrapper env with count inference, per-count model building, and object-centric descriptions.
src/robocode/environments/mujoco_gl.py New helper to configure and lock MuJoCo/PyOpenGL backend once per process.
src/robocode/environments/kinder_geom3d_env.py Refactors GL backend locking to use configure_gl_backend().
src/robocode/environments/kinder_geom2d_env.py Refactors GL backend locking to use configure_gl_backend().
src/robocode/approaches/bilevel_planning_approach.py Rebuilds/caches planning models per count and vectorizes object-centric observations for the planner.
src/robocode/approaches/base_approach.py Extends solve_instance contract to accept an optional count pin.
src/robocode/approaches/agentic_per_instance_approach.py Pins eval count per episode and scales horizons for variable-count envs.
src/robocode/approaches/agentic_base.py Detects object-centric observation spaces and switches prompt/tooling accordingly.
experiments/run_experiment.py Sweeps eval_counts for variable-count envs, pins counts per episode, and stores by-count summaries.
experiments/plot_scaling.py New script to plot solve rate and planner degradation vs. object count across runs.
experiments/conf/environment/stickbutton2d_generalized.yaml New variable-count env config for StickButton2D.
experiments/conf/environment/obstruction2d_generalized.yaml New variable-count env config for Obstruction2D.
experiments/conf/environment/motion2d_generalized.yaml New variable-count env config for Motion2D.
experiments/conf/environment/clutteredstorage2d_generalized.yaml New variable-count env config for ClutteredStorage2D.
experiments/conf/environment/clutteredretrieval2d_generalized.yaml New variable-count env config for ClutteredRetrieval2D.
experiments/analyze_results.py Flattens nested by_count solve rates into numeric solve_rate@<count> columns for aggregation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/robocode/utils/object_centric_codec.py Outdated
Comment thread src/robocode/utils/episode.py
Comment thread tests/environments/test_collision_checking.py Outdated
Comment thread tests/environments/test_collision_checking.py Outdated
Comment thread tests/environments/test_collision_checking.py Outdated
merlerm added 14 commits July 9, 2026 00:06
…t envs

- object_centric_codec: sort objects by name explicitly instead of relying on
  relational_structs.Object rich comparison, keeping the payload order stable and
  independent of a dependency's comparison semantics.
- summarize_by_count: raise on a scheduled_counts/per_episode length mismatch
  rather than letting zip silently drop episodes and skew the scheduled denominator.
- collision tests: close the variable-count envs, matching the kinder tests.
A variable-object-count env has no single SesameModels bundle (the models bake
in the count), so build_sesame_models raised -- leaving the coding agent unable
to use the bilevel_models primitive there, a core axis of the generalized
head-to-head experiment. Return a VariableCountBilevelModels accessor instead:
the program calls models_for_state(state) to get the SesameModels for the
current instance's count, built once per count and cached. The primitive
description now covers both the fixed-count bundle and the variable-count
accessor.
On a variable-count env the observation is an ObjectCentricState, not a flat
vector. LLMGenPlanApproach now detects an ObjectCentricStateSpace and its
GeneratedApproach interface spec describes the object-centric API (and appends
the shared object-centric usage note) instead of calling the state a numpy
observation. best_of_k inherits this through the shared prompt path.
A variable-count blackbox observation is a local ObjectCentricState, not a
remote handle, so a program passing it to a remote-module primitive (e.g.
crv_motion_planning) sends it by value as an {__ocs__} leaf. The handle-aware
decode_ref resolved only handle/ndarray/set tags and recursed the codec tag
into a raw dict, so the planner received a dict instead of a state. It now
honors registered codecs the way decode does, covering the deferred non-Box
types as well.
CDL is a prompt strategy (decompose into precondition/subgoal/policy-body
behaviors); its Behavior base class takes an opaque state, so nothing structural
ties it to flat vectors. Detect an ObjectCentricStateSpace and thread the
object_centric flag through the prompt builders, and give the behavior-impl
obs guidance object-centric variants (read typed objects via get_objects /
get_object_from_name / get, no vector indices) selected for variable-count envs.
The fixed-count (vector) prompt is byte-for-byte unchanged.
Adds an end-to-end test that pins object counts through run_episode (including a
held-out count) and aggregates by_count over real rollouts, complementing the
synthetic-dict summarize_by_count unit tests.
One parametrized test covers Obstruction2D, ClutteredRetrieval2D,
ClutteredStorage2D, StickButton2D, and Motion2D: a pinned count is reported and
set_state infers it back from the object-name prefix. A one_to_one flag captures
that Motion2D makes ~2 obstacle objects per passage (so its prefixed-object
count is not the count parameter). A separate test asserts ClutteredStorage2D
rejects even block counts.
… wire)

Clearbox: crv_motion_planning plans on a multi-object ObjectCentricState pulled
from VariableObjectCountEnv, and crv_motion_planning_grasp reaches a geometric
outcome over the same state. Blackbox: a local ObjectCentricState passed to the
crv remote-module primitive round-trips over the wire and runs the host planner,
guarding the decode_ref codec-tag fix. The wire setup is factored into a
_blackbox_client helper shared with the existing object-centric loop test.
…unt reporting

run_per_instance_eval attached object_count only to solved attempts, so crashed
and budget-exhausted entries lacked it. Any consumer that groups per_episode by
object_count (e.g. plot_scaling) then dropped those failures, inflating the
per-count solve rate once the budget ran out. Every entry now carries its
scheduled count, matching the full-scheduled denominator summarize_by_count uses.
A per-instance agentic run was told to solve env.reset(seed=S) but scored on
env.reset(seed=S, options={"object_count": K}) for a variable-count env, so the
agent developed against an unpinned design-count instance rather than the held-out
count it was evaluated on. The prompt now names the exact scored instance, threading
per_instance_count through build_agentic_prompt and the per-instance directive.
The black-box env_client spec always described a numpy observation vector
(shape/low/high/dtype, devectorize/vectorize). For a variable-count env the
client's observation space is object-centric (types/type_features/get_type) and
get_state returns a local ObjectCentricState, so the vector spec pointed the agent
at attributes that do not exist. build_agentic_prompt / build_cdl_prompt now select
an object-centric interaction spec (read typed objects via get_objects / get /
get_object_from_name) when the observation is object-centric; the fixed-count
(vector) spec is byte-for-byte unchanged.
The black-box CRV notes told agents to build the planner state with
observation_space.devectorize(obs), which does not exist on a variable-count
env's object-centric client space (only types/type_features/get_type). Any agent
following the note would crash. format_primitives_description is now
object-centric-aware and emits notes that pass the ObjectCentricState straight to
the host-side CRV planners; agentic, CDL, and genplan thread the flag through.
render_state (reset mode) and render_policy reset with env.reset(seed=seed) and
no count, so a held-out per-instance (seed, count) evaluation was visualized as a
different design-count instance -- undermining debugging exactly where count
pinning matters. Both tools now take an optional object_count, threaded through
the local and black-box implementations (env_client -> env server) into the reset.
The object-centric render_state description and system-prompt suffix still listed
render_state(seed, label) / "render_state(seed=...), render_policy" without the
object_count argument the tools now accept, so an agent in a per-instance
variable-count run had no way to learn it could pin the scored held-out (seed,
count) instance. Both the render_state description and the object-centric system
suffix now advertise object_count for render_state and render_policy.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 47 out of 47 changed files in this pull request and generated 5 comments.

Comment thread src/robocode/environments/variable_object_count_env.py
Comment thread src/robocode/utils/episode.py
Comment thread tests/utils/test_object_centric_codec.py
Comment thread tests/utils/test_episode_variable_count.py Outdated
Comment thread tests/utils/test_episode_variable_count.py Outdated
merlerm added 3 commits July 9, 2026 17:23
…rity)

The black-box env_client space mirror and the object-centric prompts advertise
observation_space.get_type(name), but the real ObjectCentricStateSpace exposes
only .types. A program developed black-box (which naturally uses get_type) then
crashed at eval with AttributeError: 'ObjectCentricStateSpace' object has no
attribute 'get_type'. Attach get_type to the eval-time space (KeyError on an
unknown name, matching the client) so test-time and eval-time APIs are identical.
Found by an end-to-end object-centric black-box run.
@merlerm

merlerm commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator Author

I think this is ready for merge. I have checked this as best I can going through all files and testing it end to end. Some points I want to discuss:

  • 3D envs are not included for now to try to keep the PR scope a bit smaller (didn't really work, sigh), but they should be straightforward to include in a future PR.
  • We have a combination that is not yet implemented: blackbox env does not support the bilevel planning primitive module. I was a bit conflicted about this, we can support it, but it also seems a bit counterintuitive. Blackbox is meant to test having no understanding of the environment besides interaction, but the bilevel modules give a lot of info about the env structure, so they kinda defeat the point. We can also take care of this in a future PR, but I'd like to hear your thoughts.
  • Prompts are not checked carefully yet. This is my next TODO and I already started to check everything manually and clean up, but it will have to wait a bit since now I'm in the middle of EMNLP rebuttal, so probably next week (or when I find some spare time). I didn't focus on cleaning these new ones up too much since I am making a full pass anyways.

Supports all five 2D families and every approach except black-box bilevel_models (deferred by design) and 3D envs (follow-up). All paths are unit-tested. End-to-end (live-agent) validation covers Obstruction2D across the key approaches — generalized agentic (clearbox + black-box), the SeSamE planner head-to-head, and genplan — demonstrating the program-vs-planner scaling claim; the remaining approaches and families are unit-tested but not yet swept end-to-end.

@merlerm merlerm marked this pull request as ready for review July 9, 2026 16:08
@merlerm merlerm requested a review from tomsilver July 9, 2026 16:08

@tomsilver tomsilver left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not checked carefully but don't want to block!

@merlerm

merlerm commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator Author

@tomsilver thanks! If you want/have time you can still check with no rush, I am not planning to do much between tonight and tomorrow unfortunately, so it's not super urgent to unblock

@tomsilver tomsilver left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a few thoughts after a slightly close look :)

Comment thread experiments/run_experiment.py Outdated
Comment thread src/robocode/environments/mujoco_gl.py
Comment thread src/robocode/environments/variable_object_count_env.py
merlerm added 7 commits July 10, 2026 19:34
Lower per-frame resolution keeps long, high-object-count rollouts within
memory when capturing gifs; unset, each family keeps its default DPI.
Probing for methods with hasattr/getattr hides renames from mypy and
degrades to a silent wrong branch when a probe misses. These six sites all
ask the same question -- is this a VariableObjectCountEnv -- so ask it
directly; the isinstance check narrows the type, so its methods are
verified at type-check time.
to_box() is the single entry point the planner needs, so fold the
current_box_space property and the current_box_obs helper (a copy of
to_box over the current state) into it. Make _count_for_seed private, since
only reset() calls it.
The family description, action-space, and reward prose are built by
ConstantObjectKinDEREnv onto its own metadata; the inner object-centric env
carries only render metadata. Reading the latter left the Action Space,
Reward, and family-description sections of the agent's env card empty. Point
the card at the wrapper's metadata (matching KinderGeom2DEnv) and assert the
sections are populated so it cannot silently regress.
The reference exemplar drives the observation table; taking it from the
smallest design count dropped the count-defining type entirely when that
count was 0 (obstruction2d, motion2d), so the agent never saw that type or
its features. Use the largest design count, which always contains every
type, and drop the unused reference_count knob. Assert the table lists the
type so it cannot regress.
Extend the per-family test to check every card section is populated,
including the count-defining type row in the observation table. Guards the
two card gaps (empty action/reward prose, missing object type) beyond the
single obstruction2d case.
@merlerm merlerm merged commit 02d6307 into main Jul 15, 2026
4 checks passed
@merlerm merlerm deleted the generalized-envs-2d branch July 15, 2026 12:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants