5959import fnmatch
6060import json
6161import os
62- import posixpath
6362import shutil
6463import subprocess
6564import sys
7877import gymnasium as gym
7978
8079from isaaclab .envs import DirectMARLEnvCfg
81- from isaaclab .utils import Checkpoint
80+ from isaaclab .utils . assets import NUCLEUS_ASSET_ROOT_DIR
8281
8382from isaaclab_rl .utils .pretrained_checkpoint import (
8483 WORKFLOW_EXPERIMENT_NAME_VARIABLE ,
8584 WORKFLOWS ,
86- get_auxiliary_checkpoint_path ,
87- get_auxiliary_checkpoints ,
88- get_latest_job_run_path ,
89- get_pretrained_checkpoint_backend_names ,
90- get_pretrained_checkpoint_filename ,
91- get_pretrained_checkpoint_path ,
92- get_pretrained_checkpoint_publish_path ,
93- get_pretrained_checkpoint_review ,
94- get_pretrained_checkpoint_review_path ,
95- has_pretrained_checkpoint_job_finished ,
96- has_pretrained_checkpoint_job_run ,
97- has_pretrained_checkpoints_asset_root_dir ,
85+ CheckpointBundle ,
9886)
9987
10088import isaaclab_tasks # noqa: F401
10896
10997
11098@dataclass (frozen = True )
111- class CheckpointJob :
99+ class CheckpointJob ( CheckpointBundle ) :
112100 """One workflow, task, physics, and renderer training combination."""
113101
114- workflow : str
115- task_name : str
116- physics_backend : str | None = None
117- render_backend : str | None = None
118102 physics_selector : str | None = None
119103 render_selector : str | None = None
120104 agent : str | None = None
121105 algorithm : str | None = None
122- auxiliary_checkpoints : tuple [Checkpoint , ...] = ()
123- """Run artifacts the task publishes beside its policy."""
124106
125107 @property
126108 def job_id (self ) -> str :
@@ -129,20 +111,6 @@ def job_id(self) -> str:
129111 return f"{ self .workflow } :{ self .task_name } "
130112 return f"{ self .workflow } :{ self .task_name } :{ self .physics_backend } :{ self .render_backend } "
131113
132- @property
133- def experiment_name (self ) -> str :
134- """Return the experiment directory name used by the RL workflow."""
135- if self .physics_backend is None and self .render_backend is None :
136- return self .task_name
137- filename = get_pretrained_checkpoint_filename (
138- self .workflow ,
139- self .task_name ,
140- self .physics_backend ,
141- self .render_backend ,
142- )
143- extension = os .path .splitext (filename )[1 ]
144- return filename .removesuffix (extension )
145-
146114 @property
147115 def preset_args (self ) -> list [str ]:
148116 """Return typed preset selectors for this job."""
@@ -293,7 +261,7 @@ def _resolve_physics_backend(task_name: str, physics_selector: str | None, defau
293261 if physics_selector is None :
294262 return default_backend
295263 env_cfg , _ = resolve_task_config (task_name , None , overrides = (f"physics={ physics_selector } " ,))
296- physics_backend , _ = get_pretrained_checkpoint_backend_names (env_cfg )
264+ physics_backend , _ = CheckpointBundle . backend_names (env_cfg )
297265 return physics_backend
298266
299267
@@ -343,7 +311,7 @@ def _build_core_jobs(args: argparse.Namespace) -> list[CheckpointJob]:
343311 workflow , agent , algorithm = _select_workflow (task_spec , env_cfg )
344312 default_physics = None
345313 if not physics_variants :
346- default_physics , _ = get_pretrained_checkpoint_backend_names (env_cfg )
314+ default_physics , _ = CheckpointBundle . backend_names (env_cfg )
347315
348316 physics_selections = _select_physics_variants (
349317 task_spec .id ,
@@ -356,16 +324,16 @@ def _build_core_jobs(args: argparse.Namespace) -> list[CheckpointJob]:
356324 physics_backend = _resolve_physics_backend (task_spec .id , physics_selector , default_physics )
357325 for render_backend , render_selector in render_selections :
358326 jobs .append (
359- CheckpointJob (
360- workflow = workflow ,
361- task_name = task_spec .id ,
362- physics_backend = physics_backend ,
363- render_backend = render_backend ,
327+ CheckpointJob .from_env_cfg (
328+ workflow ,
329+ task_spec .id ,
330+ env_cfg ,
331+ physics_backend ,
332+ render_backend ,
364333 physics_selector = physics_selector ,
365334 render_selector = render_selector ,
366335 agent = agent ,
367336 algorithm = algorithm ,
368- auxiliary_checkpoints = tuple (get_auxiliary_checkpoints (env_cfg )),
369337 )
370338 )
371339 return jobs
@@ -411,7 +379,7 @@ def _training_command(job: CheckpointJob, args: argparse.Namespace, smoke: bool)
411379 if job .algorithm is not None :
412380 command .extend (["--algorithm" , job .algorithm ])
413381
414- experiment_name = f"{ job .experiment_name } _smoke" if smoke else job .experiment_name
382+ experiment_name = f"{ job .stem } _smoke" if smoke else job .stem
415383 experiment_variable = WORKFLOW_EXPERIMENT_NAME_VARIABLE [job .workflow ]
416384 if experiment_variable is not None :
417385 command .append (f"{ experiment_variable } ={ experiment_name } " )
@@ -471,30 +439,15 @@ def _run_command(command: list[str], dry_run: bool) -> int:
471439
472440def _has_training_job_completed (job : CheckpointJob ) -> bool :
473441 """Return whether the latest run exited successfully with a checkpoint."""
474- run_path = get_latest_job_run_path (
475- job .workflow ,
476- job .task_name ,
477- job .physics_backend ,
478- job .render_backend ,
479- )
442+ run_path = job .latest_run
480443 if run_path is None or not os .path .isfile (os .path .join (run_path , _TRAINING_COMPLETE_FILENAME )):
481444 return False
482- return has_pretrained_checkpoint_job_finished (
483- job .workflow ,
484- job .task_name ,
485- job .physics_backend ,
486- job .render_backend ,
487- )
445+ return job .has_finished
488446
489447
490448def _mark_training_job_completed (job : CheckpointJob ) -> None :
491449 """Record that the latest training subprocess exited successfully."""
492- run_path = get_latest_job_run_path (
493- job .workflow ,
494- job .task_name ,
495- job .physics_backend ,
496- job .render_backend ,
497- )
450+ run_path = job .latest_run
498451 if run_path is None :
499452 raise RuntimeError (f"Unable to determine the latest run for { job .job_id } " )
500453 marker_path = os .path .join (run_path , _TRAINING_COMPLETE_FILENAME )
@@ -514,12 +467,7 @@ def train_job(job: CheckpointJob, args: argparse.Namespace, smoke: bool = False)
514467 return False
515468 if smoke or args .dry_run :
516469 return True
517- if not has_pretrained_checkpoint_job_finished (
518- job .workflow ,
519- job .task_name ,
520- job .physics_backend ,
521- job .render_backend ,
522- ):
470+ if not job .has_finished :
523471 print (f"Training did not produce a checkpoint for { job .job_id } " , file = sys .stderr )
524472 return False
525473 _mark_training_job_completed (job )
@@ -528,71 +476,45 @@ def train_job(job: CheckpointJob, args: argparse.Namespace, smoke: bool = False)
528476
529477def collect_pretrained_checkpoint (job : CheckpointJob , output_dir : str , dry_run : bool = False ) -> str | None :
530478 """Copy the last or best checkpoint into the structured output directory."""
531- destination = _get_collected_checkpoint_path ( job , output_dir )
479+ destination = job . collected_path ( output_dir )
532480 if dry_run :
533481 print (f"Would collect the completed checkpoint -> { destination } " )
534482 return destination
535483
536- source_path = get_pretrained_checkpoint_path (
537- job .workflow ,
538- job .task_name ,
539- job .physics_backend ,
540- job .render_backend ,
541- )
484+ source_path = job .trained_path ()
542485 if source_path is None or not os .path .isfile (source_path ):
543486 print (f"No completed checkpoint to collect for { job .job_id } " )
544487 return None
545488
546489 print (f"Collecting { source_path } -> { destination } " )
547490 os .makedirs (os .path .dirname (destination ), exist_ok = True )
548491 shutil .copy2 (source_path , destination )
549- run_path = get_latest_job_run_path (job .workflow , job .task_name , job .physics_backend , job .render_backend )
550- for checkpoint in job .auxiliary_checkpoints :
551- aux_source = checkpoint .find_in (run_path )
492+ for checkpoint in job .checkpoints :
493+ aux_source = job .trained_path (checkpoint )
552494 if aux_source is None :
553495 print (f"No { checkpoint .name } checkpoint matched { checkpoint .run_glob !r} for { job .job_id } " )
554496 continue
555- aux_destination = get_auxiliary_checkpoint_path ( destination , job .workflow , checkpoint )
497+ aux_destination = job .collected_path ( output_dir , checkpoint )
556498 print (f"Collecting { aux_source } -> { aux_destination } " )
557499 shutil .copy2 (aux_source , aux_destination )
558500 return destination
559501
560502
561503def review_pretrained_checkpoint (job : CheckpointJob , args : argparse .Namespace ) -> bool :
562504 """Play and interactively review one checkpoint."""
563- if not has_pretrained_checkpoint_job_run (
564- job .workflow ,
565- job .task_name ,
566- job .physics_backend ,
567- job .render_backend ,
568- ):
505+ if not job .has_run :
569506 print (f"Skipping review of { job .job_id } ; it has not been trained" )
570507 return False
571- if not has_pretrained_checkpoint_job_finished (
572- job .workflow ,
573- job .task_name ,
574- job .physics_backend ,
575- job .render_backend ,
576- ):
508+ if not job .has_finished :
577509 print (f"Skipping review of { job .job_id } ; training is incomplete" )
578510 return False
579511
580- review = get_pretrained_checkpoint_review (
581- job .workflow ,
582- job .task_name ,
583- job .physics_backend ,
584- job .render_backend ,
585- )
512+ review = job .review
586513 if not args .force_review and review and review .get ("reviewed" ):
587514 print (f"Review already complete for { job .job_id } " )
588515 return True
589516
590- checkpoint_path = get_pretrained_checkpoint_path (
591- job .workflow ,
592- job .task_name ,
593- job .physics_backend ,
594- job .render_backend ,
595- )
517+ checkpoint_path = job .trained_path ()
596518 if checkpoint_path is None :
597519 print (f"Skipping review of { job .job_id } ; no checkpoint was found" )
598520 return False
@@ -610,12 +532,7 @@ def review_pretrained_checkpoint(job: CheckpointJob, args: argparse.Namespace) -
610532 if notes :
611533 review_data ["notes" ] = notes
612534
613- review_path = get_pretrained_checkpoint_review_path (
614- job .workflow ,
615- job .task_name ,
616- job .physics_backend ,
617- job .render_backend ,
618- )
535+ review_path = job .review_path
619536 if review_path is None :
620537 raise RuntimeError (f"Unable to determine review path for { job .job_id } " )
621538 with open (review_path , "w" , encoding = "utf-8" ) as review_file :
@@ -625,46 +542,27 @@ def review_pretrained_checkpoint(job: CheckpointJob, args: argparse.Namespace) -
625542
626543def publish_pretrained_checkpoint (job : CheckpointJob , args : argparse .Namespace ) -> bool :
627544 """Publish an accepted checkpoint to the configured Nucleus asset root."""
628- if args .publish_root is None and not has_pretrained_checkpoints_asset_root_dir () :
545+ if args .publish_root is None and not NUCLEUS_ASSET_ROOT_DIR :
629546 raise RuntimeError ("A pretrained-checkpoint Nucleus asset root is not configured" )
630- local_path = _get_collected_checkpoint_path ( job , args .output_dir )
547+ local_path = job . collected_path ( args .output_dir )
631548 if not os .path .isfile (local_path ):
632549 print (f"Skipping publish of { job .job_id } ; no collected checkpoint was found" )
633550 return False
634551
635552 if not args .force_publish :
636- review = get_pretrained_checkpoint_review (
637- job .workflow ,
638- job .task_name ,
639- job .physics_backend ,
640- job .render_backend ,
641- )
553+ review = job .review
642554 if not review or review .get ("result" ) != "accepted" :
643555 print (f"Skipping publish of { job .job_id } ; it does not have an accepted review" )
644556 return False
645557
646- if args .publish_root is None :
647- publish_path = get_pretrained_checkpoint_publish_path (
648- job .workflow ,
649- job .task_name ,
650- job .physics_backend ,
651- job .render_backend ,
652- )
653- else :
654- filename = get_pretrained_checkpoint_filename (
655- job .workflow ,
656- job .task_name ,
657- job .physics_backend ,
658- job .render_backend ,
659- )
660- publish_path = posixpath .join (args .publish_root .rstrip ("/" ), job .workflow , filename )
558+ publish_path = job .published_path (root = args .publish_root )
661559 uploads = [(local_path , publish_path )]
662- for checkpoint in job .auxiliary_checkpoints :
663- local_auxiliary = get_auxiliary_checkpoint_path ( local_path , job .workflow , checkpoint )
664- if not os .path .isfile (local_auxiliary ):
560+ for checkpoint in job .checkpoints :
561+ local_declared = job .collected_path ( args . output_dir , checkpoint )
562+ if not os .path .isfile (local_declared ):
665563 print (f"Skipping the { checkpoint .name } checkpoint for { job .job_id } ; it was not collected" )
666564 continue
667- uploads .append ((local_auxiliary , get_auxiliary_checkpoint_path ( publish_path , job .workflow , checkpoint )))
565+ uploads .append ((local_declared , job .published_path ( checkpoint , root = args . publish_root )))
668566 for source , destination in uploads :
669567 print (f"Publishing { source } -> { destination } " )
670568 if args .dry_run :
@@ -683,24 +581,10 @@ def publish_pretrained_checkpoint(job: CheckpointJob, args: argparse.Namespace)
683581
684582def _summary_row (job : CheckpointJob , output_dir : str ) -> list [str | bool ]:
685583 """Return one CSV summary row."""
686- has_run = has_pretrained_checkpoint_job_run (
687- job .workflow ,
688- job .task_name ,
689- job .physics_backend ,
690- job .render_backend ,
691- )
692- has_finished = (
693- _has_training_job_completed (job )
694- if job .physics_backend is not None
695- else has_pretrained_checkpoint_job_finished (job .workflow , job .task_name )
696- )
697- collected_path = _get_collected_checkpoint_path (job , output_dir )
698- review = get_pretrained_checkpoint_review (
699- job .workflow ,
700- job .task_name ,
701- job .physics_backend ,
702- job .render_backend ,
703- )
584+ has_run = job .has_run
585+ has_finished = _has_training_job_completed (job ) if job .physics_backend is not None else job .has_finished
586+ collected_path = job .collected_path (output_dir )
587+ review = job .review
704588 return [
705589 job .workflow ,
706590 job .task_name ,
@@ -715,20 +599,6 @@ def _summary_row(job: CheckpointJob, output_dir: str) -> list[str | bool]:
715599 ]
716600
717601
718- def _get_collected_checkpoint_path (job : CheckpointJob , output_dir : str ) -> str :
719- """Return the absolute path of a checkpoint in the collection directory."""
720- filename = get_pretrained_checkpoint_filename (
721- job .workflow ,
722- job .task_name ,
723- job .physics_backend ,
724- job .render_backend ,
725- )
726- path_parts = [output_dir , job .workflow ]
727- if job .physics_backend is None :
728- path_parts .append (job .task_name )
729- return os .path .abspath (os .path .join (* path_parts , filename ))
730-
731-
732602def main (argv : list [str ] | None = None ) -> int :
733603 """Run checkpoint management actions."""
734604 parser = _create_parser ()
0 commit comments