Skip to content

Commit 2806320

Browse files
committed
Surface mismatched features in state-reset reconstruction warning
The 'Could not reconstruct state exactly in reset' warning at pybullet_env.py:506 previously logged nothing about what diverged, making it useless for diagnosing kinematic drift during option-model rollouts. Add _reconstruction_diff: a static helper that compares requested vs reconstructed States feature-by-feature, sorts the mismatches by absolute delta, and prints the top entries (object, feature, requested value, reconstructed value, signed delta). The warning now includes this listing; the ValueError raised by envs that override _get_state inherits the same diff.
1 parent 62ff922 commit 2806320

1 file changed

Lines changed: 56 additions & 2 deletions

File tree

predicators/envs/pybullet_env.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -507,10 +507,64 @@ def _set_state(self, state: State) -> None:
507507
if wrote_anything:
508508
reconstructed = self._get_state()
509509
if not reconstructed.allclose(state):
510+
diff = self._reconstruction_diff(state, reconstructed)
510511
if type(self)._get_state is not PyBulletEnv._get_state:
511-
raise ValueError("Could not reconstruct state.")
512+
raise ValueError(
513+
f"Could not reconstruct state. Mismatched "
514+
f"features:\n{diff}")
512515
logging.warning(
513-
"Could not reconstruct state exactly in reset.")
516+
"Could not reconstruct state exactly in reset. "
517+
"Mismatched features:\n%s", diff)
518+
519+
@staticmethod
520+
def _reconstruction_diff(requested: State,
521+
reconstructed: State,
522+
atol: float = 1e-3,
523+
max_lines: int = 10) -> str:
524+
"""Format per-feature mismatches between two States for debugging.
525+
526+
Returns a human-readable summary of which (object, feature)
527+
pairs differ by more than ``atol``, sorted by largest absolute
528+
delta. Truncates to ``max_lines`` rows so the warning stays
529+
scannable.
530+
"""
531+
req_objs = set(requested.data)
532+
rec_objs = set(reconstructed.data)
533+
rows = []
534+
only_in_req = req_objs - rec_objs
535+
only_in_rec = rec_objs - req_objs
536+
if only_in_req:
537+
rows.append(f" objects only in requested: "
538+
f"{sorted(o.name for o in only_in_req)}")
539+
if only_in_rec:
540+
rows.append(f" objects only in reconstructed: "
541+
f"{sorted(o.name for o in only_in_rec)}")
542+
feature_diffs: List[Tuple[float, str, str, float, float]] = []
543+
for obj in req_objs & rec_objs:
544+
req_vals = requested.data[obj]
545+
rec_vals = reconstructed.data[obj]
546+
if len(req_vals) != len(rec_vals):
547+
rows.append(f" {obj.name}: feature-count mismatch "
548+
f"requested={len(req_vals)} "
549+
f"reconstructed={len(rec_vals)}")
550+
continue
551+
for i, feat in enumerate(obj.type.feature_names):
552+
delta = float(rec_vals[i] - req_vals[i])
553+
if abs(delta) > atol:
554+
feature_diffs.append((abs(delta), obj.name, feat,
555+
float(req_vals[i]),
556+
float(rec_vals[i])))
557+
feature_diffs.sort(reverse=True)
558+
for _absdelta, name, feat, req, rec in feature_diffs[:max_lines]:
559+
rows.append(f" {name}.{feat}: requested={req:.6f} "
560+
f"reconstructed={rec:.6f} (Δ={rec - req:+.6f})")
561+
if len(feature_diffs) > max_lines:
562+
rows.append(f" ... and {len(feature_diffs) - max_lines} "
563+
f"more features over the {atol:g} tolerance")
564+
if not rows:
565+
rows.append(" (no per-feature delta exceeded "
566+
f"{atol:g}; check simulator_state)")
567+
return "\n".join(rows)
514568

515569
def _robot_matches_state(self, state: State, atol: float = 1e-3) -> bool:
516570
"""True if PyBullet's live robot pose already equals state's.

0 commit comments

Comments
 (0)