auto-sync 2026-07-02T19:07:26Z workspace (part 7)
Browse files
workspace/scripts/export_positive_tangent_targets.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import argparse
|
| 5 |
+
import json
|
| 6 |
+
import sys
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 10 |
+
if str(PROJECT_ROOT) not in sys.path:
|
| 11 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 12 |
+
|
| 13 |
+
from dovla_cil.data.datasets import CILDataset # noqa: E402
|
| 14 |
+
from dovla_cil.generation.tangent_targets import ( # noqa: E402
|
| 15 |
+
DEFAULT_BASE_CANDIDATE_TYPES,
|
| 16 |
+
DEFAULT_LABELS,
|
| 17 |
+
export_positive_tangent_targets,
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def main(argv: list[str] | None = None) -> int:
|
| 22 |
+
parser = argparse.ArgumentParser(
|
| 23 |
+
description=(
|
| 24 |
+
"Export positive/negative same-state residual tangents for the learned "
|
| 25 |
+
"CIL-Atlas Generator V2/V3 training path."
|
| 26 |
+
)
|
| 27 |
+
)
|
| 28 |
+
parser.add_argument("--dataset", type=Path, required=True)
|
| 29 |
+
parser.add_argument("--out", type=Path, required=True)
|
| 30 |
+
parser.add_argument("--epsilon", type=float, default=0.0)
|
| 31 |
+
parser.add_argument(
|
| 32 |
+
"--base-candidate-types",
|
| 33 |
+
default=",".join(DEFAULT_BASE_CANDIDATE_TYPES),
|
| 34 |
+
help="Comma-separated candidate_type priority used as the base action.",
|
| 35 |
+
)
|
| 36 |
+
parser.add_argument(
|
| 37 |
+
"--labels",
|
| 38 |
+
default=",".join(DEFAULT_LABELS),
|
| 39 |
+
help="Comma-separated tangent labels to export: positive, negative, neutral.",
|
| 40 |
+
)
|
| 41 |
+
parser.add_argument("--max-groups", type=int, default=None)
|
| 42 |
+
parser.add_argument(
|
| 43 |
+
"--summary-out",
|
| 44 |
+
type=Path,
|
| 45 |
+
default=None,
|
| 46 |
+
help="Optional summary JSON path. Defaults to OUT with _summary suffix.",
|
| 47 |
+
)
|
| 48 |
+
parser.add_argument("--no-summary", action="store_true")
|
| 49 |
+
args = parser.parse_args(argv)
|
| 50 |
+
|
| 51 |
+
if args.max_groups is not None and args.max_groups <= 0:
|
| 52 |
+
parser.error("--max-groups must be positive when provided")
|
| 53 |
+
if args.epsilon < 0:
|
| 54 |
+
parser.error("--epsilon must be non-negative")
|
| 55 |
+
|
| 56 |
+
dataset = CILDataset(args.dataset)
|
| 57 |
+
payload = export_positive_tangent_targets(
|
| 58 |
+
dataset.iter_groups(),
|
| 59 |
+
epsilon=args.epsilon,
|
| 60 |
+
base_candidate_types=_parse_csv(args.base_candidate_types),
|
| 61 |
+
labels=_parse_csv(args.labels),
|
| 62 |
+
max_groups=args.max_groups,
|
| 63 |
+
)
|
| 64 |
+
payload["dataset"] = str(args.dataset)
|
| 65 |
+
|
| 66 |
+
args.out.parent.mkdir(parents=True, exist_ok=True)
|
| 67 |
+
args.out.write_text(json.dumps(payload, indent=2) + "\n")
|
| 68 |
+
summary = {key: value for key, value in payload.items() if key != "targets"}
|
| 69 |
+
if not args.no_summary:
|
| 70 |
+
summary_out = args.summary_out or args.out.with_name(f"{args.out.stem}_summary.json")
|
| 71 |
+
summary_out.parent.mkdir(parents=True, exist_ok=True)
|
| 72 |
+
summary_out.write_text(json.dumps(summary, indent=2) + "\n")
|
| 73 |
+
print(json.dumps(summary, indent=2))
|
| 74 |
+
print(f"Wrote {args.out}")
|
| 75 |
+
return 0
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def _parse_csv(value: str) -> tuple[str, ...]:
|
| 79 |
+
return tuple(item.strip() for item in value.split(",") if item.strip())
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
if __name__ == "__main__":
|
| 83 |
+
raise SystemExit(main())
|
workspace/scripts/slurm/advance_v1_generator.sbatch
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
#SBATCH --job-name=advance_v1
|
| 3 |
+
#SBATCH --account=def-yalda
|
| 4 |
+
#SBATCH --time=00:20:00
|
| 5 |
+
#SBATCH --cpus-per-task=1
|
| 6 |
+
#SBATCH --mem=2G
|
| 7 |
+
#SBATCH --output=outputs/hpc/logs/%x_%j.out
|
| 8 |
+
#SBATCH --error=outputs/hpc/logs/%x_%j.err
|
| 9 |
+
|
| 10 |
+
set -euo pipefail
|
| 11 |
+
|
| 12 |
+
PROJECT_DIR="${PROJECT_DIR:-$SLURM_SUBMIT_DIR}"
|
| 13 |
+
RUN_ROOT="${RUN_ROOT:-/scratch/$USER/dovla/experiments/dovla_h16_policy_ckpt_runs}"
|
| 14 |
+
DATASET="${DATASET:-/scratch/$USER/dovla/experiments/six_task_h16_collection}"
|
| 15 |
+
OBJECTIVE="${OBJECTIVE:-transport_field_reground_fieldonly_k6clean_dropnoopwg_b12_v1}"
|
| 16 |
+
SUBMIT_NEXT="${SUBMIT_NEXT:-0}"
|
| 17 |
+
ADVANCE_ROUND="${ADVANCE_ROUND:-0}"
|
| 18 |
+
PYTHON="${PYTHON:-python3}"
|
| 19 |
+
|
| 20 |
+
cd "$PROJECT_DIR"
|
| 21 |
+
mkdir -p results outputs/hpc/logs
|
| 22 |
+
|
| 23 |
+
ARGS=(
|
| 24 |
+
--project-dir "$PROJECT_DIR"
|
| 25 |
+
--run-root "$RUN_ROOT"
|
| 26 |
+
--dataset "$DATASET"
|
| 27 |
+
--objective "$OBJECTIVE"
|
| 28 |
+
--round "$ADVANCE_ROUND"
|
| 29 |
+
)
|
| 30 |
+
if [[ "$SUBMIT_NEXT" == "1" ]]; then
|
| 31 |
+
ARGS+=(--submit-next)
|
| 32 |
+
fi
|
| 33 |
+
|
| 34 |
+
"$PYTHON" scripts/advance_v1_generator.py "${ARGS[@]}"
|
workspace/scripts/slurm/export_positive_tangent_targets.sbatch
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
#SBATCH --job-name=export_pos_tangents
|
| 3 |
+
#SBATCH --account=def-yalda
|
| 4 |
+
#SBATCH --time=00:20:00
|
| 5 |
+
#SBATCH --cpus-per-task=1
|
| 6 |
+
#SBATCH --mem=4G
|
| 7 |
+
#SBATCH --output=outputs/hpc/logs/%x_%j.out
|
| 8 |
+
#SBATCH --error=outputs/hpc/logs/%x_%j.err
|
| 9 |
+
|
| 10 |
+
set -euo pipefail
|
| 11 |
+
|
| 12 |
+
PROJECT_DIR="${PROJECT_DIR:-$SLURM_SUBMIT_DIR}"
|
| 13 |
+
SCRATCH_ROOT="/scratch/$USER/dovla"
|
| 14 |
+
PYTHON="${PYTHON:-python3}"
|
| 15 |
+
DATASET="${DATASET:-$SCRATCH_ROOT/experiments/six_task_h16_collection}"
|
| 16 |
+
OUT="${OUT:-$PROJECT_DIR/results/generator_v2_positive_tangent_targets.json}"
|
| 17 |
+
EPSILON="${EPSILON:-0.05}"
|
| 18 |
+
BASE_CANDIDATE_TYPES="${BASE_CANDIDATE_TYPES:-policy,policy_residual,anchor,base,expert}"
|
| 19 |
+
LABELS="${LABELS:-positive,negative,neutral}"
|
| 20 |
+
MAX_GROUPS="${MAX_GROUPS:-}"
|
| 21 |
+
|
| 22 |
+
cd "$PROJECT_DIR"
|
| 23 |
+
mkdir -p outputs/hpc/logs "$(dirname "$OUT")"
|
| 24 |
+
|
| 25 |
+
ARGS=(
|
| 26 |
+
--dataset "$DATASET"
|
| 27 |
+
--out "$OUT"
|
| 28 |
+
--epsilon "$EPSILON"
|
| 29 |
+
--base-candidate-types "$BASE_CANDIDATE_TYPES"
|
| 30 |
+
--labels "$LABELS"
|
| 31 |
+
)
|
| 32 |
+
if [[ -n "$MAX_GROUPS" ]]; then
|
| 33 |
+
ARGS+=(--max-groups "$MAX_GROUPS")
|
| 34 |
+
fi
|
| 35 |
+
|
| 36 |
+
"$PYTHON" scripts/export_positive_tangent_targets.py "${ARGS[@]}"
|
workspace/tests/test_positive_tangent_targets.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import math
|
| 4 |
+
|
| 5 |
+
from dovla_cil.data.schema import (
|
| 6 |
+
CIL_VERSION,
|
| 7 |
+
ActionChunk,
|
| 8 |
+
CILRecord,
|
| 9 |
+
FailureInfo,
|
| 10 |
+
RewardInfo,
|
| 11 |
+
StructuredEffect,
|
| 12 |
+
make_record_id,
|
| 13 |
+
)
|
| 14 |
+
from dovla_cil.generation.tangent_targets import (
|
| 15 |
+
choose_base_record,
|
| 16 |
+
export_positive_tangent_targets,
|
| 17 |
+
spline_tangent_summary,
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _record(
|
| 22 |
+
group_id: str,
|
| 23 |
+
action_id: str,
|
| 24 |
+
candidate_type: str,
|
| 25 |
+
action: list[list[float]],
|
| 26 |
+
progress: float,
|
| 27 |
+
*,
|
| 28 |
+
success: bool = False,
|
| 29 |
+
failure: FailureInfo | None = None,
|
| 30 |
+
) -> CILRecord:
|
| 31 |
+
return CILRecord(
|
| 32 |
+
version=CIL_VERSION,
|
| 33 |
+
record_id=make_record_id(group_id, action_id, seed=3),
|
| 34 |
+
group_id=group_id,
|
| 35 |
+
state_hash="same-state",
|
| 36 |
+
task_id="PickCube-v1",
|
| 37 |
+
scene_id=None,
|
| 38 |
+
instruction="pick the cube",
|
| 39 |
+
instruction_family={"family": "pick"},
|
| 40 |
+
observation_ref=None,
|
| 41 |
+
observation_inline={"state": [0.0, 1.0]},
|
| 42 |
+
action_chunk=ActionChunk(action_id=action_id, horizon=len(action), values=action),
|
| 43 |
+
next_observation_ref=None,
|
| 44 |
+
next_observation_inline={"state": [progress]},
|
| 45 |
+
structured_effect=StructuredEffect(),
|
| 46 |
+
reward=RewardInfo(
|
| 47 |
+
progress=progress,
|
| 48 |
+
success=success,
|
| 49 |
+
terminal_success=success,
|
| 50 |
+
dense_components={"progress": progress},
|
| 51 |
+
),
|
| 52 |
+
regret=None,
|
| 53 |
+
rank_within_group=None,
|
| 54 |
+
candidate_type=candidate_type,
|
| 55 |
+
failure=failure,
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def test_export_positive_tangent_targets_labels_same_state_utility_contrasts() -> None:
|
| 60 |
+
base = _record("g", "base", "policy", [[0.0, 0.0], [0.0, 0.0]], 0.4)
|
| 61 |
+
positive = _record("g", "repair", "near_miss", [[0.1, 0.0], [0.2, 0.0]], 0.8)
|
| 62 |
+
negative = _record(
|
| 63 |
+
"g",
|
| 64 |
+
"collide",
|
| 65 |
+
"random_negative",
|
| 66 |
+
[[-0.1, 0.0], [-0.2, 0.0]],
|
| 67 |
+
0.2,
|
| 68 |
+
failure=FailureInfo(type="collision"),
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
payload = export_positive_tangent_targets([[base, positive, negative]], epsilon=0.05)
|
| 72 |
+
|
| 73 |
+
assert payload["num_usable_groups"] == 1
|
| 74 |
+
assert payload["label_counts"] == {"negative": 1, "positive": 1}
|
| 75 |
+
by_type = {item["candidate_type"]: item for item in payload["targets"]}
|
| 76 |
+
assert by_type["near_miss"]["tangent_label"] == "positive"
|
| 77 |
+
assert by_type["random_negative"]["tangent_label"] == "negative"
|
| 78 |
+
assert by_type["near_miss"]["delta_action"] == [[0.1, 0.0], [0.2, 0.0]]
|
| 79 |
+
assert by_type["near_miss"]["tangent_summary"]["endpoint_delta"] == [0.2, 0.0]
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def test_choose_base_record_uses_priority_order() -> None:
|
| 83 |
+
expert = _record("g", "expert", "expert", [[1.0]], 1.0)
|
| 84 |
+
policy = _record("g", "policy", "policy", [[0.0]], 0.3)
|
| 85 |
+
|
| 86 |
+
assert choose_base_record([expert, policy]).record_id == policy.record_id
|
| 87 |
+
assert choose_base_record([expert], base_candidate_types=("expert",)).record_id == expert.record_id
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def test_spline_tangent_summary_exposes_low_dimensional_code() -> None:
|
| 91 |
+
summary = spline_tangent_summary(
|
| 92 |
+
[
|
| 93 |
+
[0.0, 0.0, 0.0, 0.1],
|
| 94 |
+
[0.1, 0.0, 0.2, 0.3],
|
| 95 |
+
[0.2, 0.1, 0.4, 0.7],
|
| 96 |
+
]
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
assert summary["spline_code"]["endpoint_delta_position"] == [0.2, 0.1, 0.4]
|
| 100 |
+
assert summary["spline_code"]["gripper_gate_shift"] == 0.7
|
| 101 |
+
assert math.isclose(summary["spline_code"]["lift_bias"], 0.4)
|
| 102 |
+
assert summary["temporal_total_variation"] > 0.0
|