From 95fdf8e446b2977983c9a7ffd8f0abe865287037 Mon Sep 17 00:00:00 2001 From: Aidan Date: Thu, 22 Oct 2026 16:42:59 -0400 Subject: [PATCH] tests: keep the suites that protect codec, data, graph, training, eval and rollout functions; drop launcher, cluster, contract and diagnostic pins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aidan (2026-10-22): "remove all of the unnecessary tests except for the ones that maintain all essential functions and proper training/eval/rollouts". 82 of 247 test files go (56.3k -> 37.7k lines); 165 stay. The line: a test stays when it checks a function that real or sim training, evaluation or rollouts run through (tokenizers and codecs, data loading, model graphs and stages, Lightning training / checkpoint / resume / requeue, evaluators and metrics, robot runtime, teleop, IK and dashboards, inference contracts, composition of the maintained experiment menu). A test goes when its subject is a launcher or cluster script, a scheduler / profile / receipt contract, a historical-parity or retired-path guard, a doc or migration pin, a diagnostic or visualization tool, or an opt-in benchmark harness. Removed, by family: - ICE / Slurm / OSMO tooling: ice_checkpoint_archive_inventory, ice_checkpoint_mirror, ice_checkpoint_mirror_pool, ice_dp_campaign_contract, ice_gpu_probe, ice_planar_bc_launcher, ice_requeue_runner, ice_wandb_resume, yam_gpu_uuid_contract, capture_runtime_lock, sync_runtime_script, batch_signal_relay, validate_slurm_job_contract, osmo_gate, unite_ice_contract, unite_fixed_step_requeue_boundary, check_checkpoint_storage, local_cpu_storage_guard, local_cpu_validation_guard, installed_package, pi_source_patch, wandb_health. - LIBERO native-launch contracts and cluster tooling: libero_cluster, libero_performance (+ the libero_ddp_resume_worker helper), libero_multi_embodiment_native, libero_native_binding_parser, libero_native_half_factory_identity, libero_native_launch_contract, libero_native_scheduler_contract, libero_native_smoke_contract, libero_native_suite_profiles, libero_native_velocity_ablation, libero_native_whole_launcher, libero_saved_hydra_context, libero_action_flow_artifacts, libero_active_recipes, libero_arc_sweep, libero_basis_scaling_audit, current_family_profiles, native_final_validation_lifecycle. - Action-flow launch / verifier / candidate gates: action_flow_launcher, action_flow_smoke_schedule, verify_action_flow_training_smoke, validate_action_flow_config, action_flow_candidate_{codec,configs,gates,stages}, action_flow_affine_configs, action_flow_manual4919_config, original_size_action_flow_cotrain_config, multiplier_training_launch, standard_dp_launch_args, oat_step_budget. - Historical parity / retired-path guards: c12_compatibility, alignment_parity, legacy_data_metric_parity, legacy_hpt_trunk_parity, likelihood_training_behavior_parity, imported_recipe_framework_parity, measured_speed_removed, pipeline_only_surface, pusht_parent_cleanup, offline_constructor_inputs, offline_static_buffer_audit. - Diagnostics / visualization: action_flow_diagnostics, action_flow_jacobian_checkpoint_performance, token_diagnostics, unite_diagnostics, speed_retiming_diagnostic, arc_tsne, config_graph, rank_synthetic_trajectory_nn. - Synthetic manifold toy: synthetic_{action_adapter_flow,multi_action_adapter_flow, shared_latent_flow,trainer_resume,trainer_signals,trajectory_eval}, gaussian_paraboloid_dataset, gaussian_torus_dataset, validate_planar_dataset. - Fixtures only those tests read: legacy_pipeline/, legacy_hpt_trunk/, legacy_alignment_reference, legacy_language_schedule, libero_native_scontrol_408787, libero_native_suite_corpus_authority, pusht_before_real_sim_cleanup, pusht_config_before_parent_cleanup, stattempo_contracts (unused). Kept in full: every test_pipeline*, test_hpt*, test_arc*, test_robot*, test_rollout*, test_yam_*, test_gello*, test_oat_*, test_unite_* (functional), test_libero_{arc,arc_global,arc_timed,benchmark, evaluate,replay,decoded_replay,training_checks,tuned_configs,native_checkpoint_format, saved_native_state,action_flow_boundary}, test_planar_*, test_pi05_* / test_pi_*, the data-module and resolver tests, the Slurm requeue / signal tests that trainHydra depends on (slurm_save_only_checkpoint, train_hydra_slurm_environment, trainer_pid_signal, standard_dp_pid_transport), and the config-menu tests (real_task_recipes, abc_arc_experiment_layout, recursive_config_audit, consolidated_core_configs, abc_*_configs). The remaining suite collects 2128 tests; the four collection errors (diffusers, av, mujoco not installed locally) are identical on the untouched tree. Stale mentions of removed tests remain in docs/integration/parity-manifest.json and a few docs; the code those tests covered is untouched. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5.1 --- tests/fixtures/legacy_alignment_reference.py | 75 - tests/fixtures/legacy_hpt_trunk/metadata.json | 46 - tests/fixtures/legacy_hpt_trunk/reference.pt | Bin 41018 -> 0 bytes tests/fixtures/legacy_hpt_trunk/trunk.pth | Bin 36175 -> 0 bytes tests/fixtures/legacy_language_schedule.json | 2011 ----------------- .../aria-cartesian-camframe.json | 26 - .../aria-cartesian-camframe.npz | Bin 38565 -> 0 bytes .../aria-cartesian-eef_frame.json | 26 - .../aria-cartesian-eef_frame.npz | Bin 37688 -> 0 bytes .../aria-keypoints-camframe.json | 26 - .../aria-keypoints-camframe.npz | Bin 71866 -> 0 bytes .../aria-keypoints-eef_frame.json | 26 - .../aria-keypoints-eef_frame.npz | Bin 71997 -> 0 bytes .../eva-cartesian-camframe.json | 27 - .../eva-cartesian-camframe.npz | Bin 93192 -> 0 bytes .../eva-cartesian-eef_frame.json | 27 - .../eva-cartesian-eef_frame.npz | Bin 93216 -> 0 bytes .../mecka-cartesian-camframe.json | 26 - .../mecka-cartesian-camframe.npz | Bin 38565 -> 0 bytes .../mecka-cartesian-eef_frame.json | 26 - .../mecka-cartesian-eef_frame.npz | Bin 37688 -> 0 bytes .../scale-cartesian-camframe.json | 26 - .../scale-cartesian-camframe.npz | Bin 38565 -> 0 bytes .../scale-cartesian-eef_frame.json | 26 - .../scale-cartesian-eef_frame.npz | Bin 37688 -> 0 bytes .../libero_native_scontrol_408787.txt | 1 - .../libero_native_suite_corpus_authority.json | 36 - .../pusht_before_real_sim_cleanup.json | 40 - .../pusht_config_before_parent_cleanup.json | 88 - tests/fixtures/stattempo_contracts.json | 76 - tests/libero_ddp_resume_worker.py | 64 - tests/test_action_flow_affine_configs.py | 138 -- tests/test_action_flow_candidate_codec.py | 151 -- tests/test_action_flow_candidate_configs.py | 181 -- tests/test_action_flow_candidate_gates.py | 295 --- tests/test_action_flow_candidate_stages.py | 246 -- tests/test_action_flow_diagnostics.py | 929 -------- ...on_flow_jacobian_checkpoint_performance.py | 555 ----- tests/test_action_flow_launcher.py | 315 --- tests/test_action_flow_manual4919_config.py | 134 -- tests/test_action_flow_smoke_schedule.py | 336 --- tests/test_alignment_parity.py | 189 -- tests/test_arc_tsne.py | 295 --- tests/test_batch_signal_relay.py | 161 -- tests/test_c12_compatibility.py | 224 -- tests/test_capture_runtime_lock.py | 38 - tests/test_check_checkpoint_storage.py | 150 -- tests/test_config_graph.py | 249 -- tests/test_current_family_profiles.py | 224 -- tests/test_gaussian_paraboloid_dataset.py | 40 - tests/test_gaussian_torus_dataset.py | 63 - .../test_ice_checkpoint_archive_inventory.py | 216 -- tests/test_ice_checkpoint_mirror.py | 420 ---- tests/test_ice_checkpoint_mirror_pool.py | 310 --- tests/test_ice_dp_campaign_contract.py | 28 - tests/test_ice_gpu_probe.py | 50 - tests/test_ice_planar_bc_launcher.py | 110 - tests/test_ice_requeue_runner.py | 1476 ------------ tests/test_ice_wandb_resume.py | 118 - .../test_imported_recipe_framework_parity.py | 68 - tests/test_installed_package.py | 102 - tests/test_legacy_data_metric_parity.py | 86 - tests/test_legacy_hpt_trunk_parity.py | 93 - tests/test_libero_action_flow_artifacts.py | 153 -- tests/test_libero_active_recipes.py | 78 - tests/test_libero_arc_sweep.py | 329 --- tests/test_libero_basis_scaling_audit.py | 113 - tests/test_libero_cluster.py | 750 ------ tests/test_libero_multi_embodiment_native.py | 1286 ----------- tests/test_libero_native_binding_parser.py | 75 - ...est_libero_native_half_factory_identity.py | 26 - tests/test_libero_native_launch_contract.py | 102 - .../test_libero_native_scheduler_contract.py | 101 - tests/test_libero_native_smoke_contract.py | 54 - tests/test_libero_native_suite_profiles.py | 857 ------- tests/test_libero_native_velocity_ablation.py | 276 --- tests/test_libero_native_whole_launcher.py | 417 ---- tests/test_libero_performance.py | 313 --- tests/test_libero_saved_hydra_context.py | 341 --- ...est_likelihood_training_behavior_parity.py | 171 -- tests/test_local_cpu_storage_guard.py | 25 - tests/test_local_cpu_validation_guard.py | 40 - tests/test_measured_speed_removed.py | 76 - tests/test_multiplier_training_launch.py | 93 - .../test_native_final_validation_lifecycle.py | 135 -- tests/test_oat_step_budget.py | 214 -- tests/test_offline_constructor_inputs.py | 119 - tests/test_offline_static_buffer_audit.py | 51 - ...riginal_size_action_flow_cotrain_config.py | 72 - tests/test_osmo_gate.py | 122 - tests/test_pi_source_patch.py | 50 - tests/test_pipeline_only_surface.py | 51 - tests/test_pusht_parent_cleanup.py | 86 - tests/test_rank_synthetic_trajectory_nn.py | 207 -- tests/test_speed_retiming_diagnostic.py | 123 - tests/test_standard_dp_launch_args.py | 16 - tests/test_sync_runtime_script.py | 29 - tests/test_synthetic_action_adapter_flow.py | 234 -- ...est_synthetic_multi_action_adapter_flow.py | 125 - tests/test_synthetic_shared_latent_flow.py | 55 - tests/test_synthetic_trainer_resume.py | 188 -- tests/test_synthetic_trainer_signals.py | 181 -- tests/test_synthetic_trajectory_eval.py | 120 - tests/test_token_diagnostics.py | 297 --- tests/test_unite_diagnostics.py | 117 - .../test_unite_fixed_step_requeue_boundary.py | 121 - tests/test_unite_ice_contract.py | 106 - tests/test_validate_action_flow_config.py | 421 ---- tests/test_validate_planar_dataset.py | 241 -- tests/test_validate_slurm_job_contract.py | 240 -- .../test_verify_action_flow_training_smoke.py | 1028 --------- tests/test_wandb_health.py | 120 - tests/test_yam_gpu_uuid_contract.py | 67 - 113 files changed, 21301 deletions(-) delete mode 100644 tests/fixtures/legacy_alignment_reference.py delete mode 100644 tests/fixtures/legacy_hpt_trunk/metadata.json delete mode 100644 tests/fixtures/legacy_hpt_trunk/reference.pt delete mode 100644 tests/fixtures/legacy_hpt_trunk/trunk.pth delete mode 100644 tests/fixtures/legacy_language_schedule.json delete mode 100644 tests/fixtures/legacy_pipeline/aria-cartesian-camframe.json delete mode 100644 tests/fixtures/legacy_pipeline/aria-cartesian-camframe.npz delete mode 100644 tests/fixtures/legacy_pipeline/aria-cartesian-eef_frame.json delete mode 100644 tests/fixtures/legacy_pipeline/aria-cartesian-eef_frame.npz delete mode 100644 tests/fixtures/legacy_pipeline/aria-keypoints-camframe.json delete mode 100644 tests/fixtures/legacy_pipeline/aria-keypoints-camframe.npz delete mode 100644 tests/fixtures/legacy_pipeline/aria-keypoints-eef_frame.json delete mode 100644 tests/fixtures/legacy_pipeline/aria-keypoints-eef_frame.npz delete mode 100644 tests/fixtures/legacy_pipeline/eva-cartesian-camframe.json delete mode 100644 tests/fixtures/legacy_pipeline/eva-cartesian-camframe.npz delete mode 100644 tests/fixtures/legacy_pipeline/eva-cartesian-eef_frame.json delete mode 100644 tests/fixtures/legacy_pipeline/eva-cartesian-eef_frame.npz delete mode 100644 tests/fixtures/legacy_pipeline/mecka-cartesian-camframe.json delete mode 100644 tests/fixtures/legacy_pipeline/mecka-cartesian-camframe.npz delete mode 100644 tests/fixtures/legacy_pipeline/mecka-cartesian-eef_frame.json delete mode 100644 tests/fixtures/legacy_pipeline/mecka-cartesian-eef_frame.npz delete mode 100644 tests/fixtures/legacy_pipeline/scale-cartesian-camframe.json delete mode 100644 tests/fixtures/legacy_pipeline/scale-cartesian-camframe.npz delete mode 100644 tests/fixtures/legacy_pipeline/scale-cartesian-eef_frame.json delete mode 100644 tests/fixtures/legacy_pipeline/scale-cartesian-eef_frame.npz delete mode 100644 tests/fixtures/libero_native_scontrol_408787.txt delete mode 100644 tests/fixtures/libero_native_suite_corpus_authority.json delete mode 100644 tests/fixtures/pusht_before_real_sim_cleanup.json delete mode 100644 tests/fixtures/pusht_config_before_parent_cleanup.json delete mode 100644 tests/fixtures/stattempo_contracts.json delete mode 100644 tests/libero_ddp_resume_worker.py delete mode 100644 tests/test_action_flow_affine_configs.py delete mode 100644 tests/test_action_flow_candidate_codec.py delete mode 100644 tests/test_action_flow_candidate_configs.py delete mode 100644 tests/test_action_flow_candidate_gates.py delete mode 100644 tests/test_action_flow_candidate_stages.py delete mode 100644 tests/test_action_flow_diagnostics.py delete mode 100644 tests/test_action_flow_jacobian_checkpoint_performance.py delete mode 100644 tests/test_action_flow_launcher.py delete mode 100644 tests/test_action_flow_manual4919_config.py delete mode 100644 tests/test_action_flow_smoke_schedule.py delete mode 100644 tests/test_alignment_parity.py delete mode 100644 tests/test_arc_tsne.py delete mode 100644 tests/test_batch_signal_relay.py delete mode 100644 tests/test_c12_compatibility.py delete mode 100644 tests/test_capture_runtime_lock.py delete mode 100644 tests/test_check_checkpoint_storage.py delete mode 100644 tests/test_config_graph.py delete mode 100644 tests/test_current_family_profiles.py delete mode 100644 tests/test_gaussian_paraboloid_dataset.py delete mode 100644 tests/test_gaussian_torus_dataset.py delete mode 100644 tests/test_ice_checkpoint_archive_inventory.py delete mode 100644 tests/test_ice_checkpoint_mirror.py delete mode 100644 tests/test_ice_checkpoint_mirror_pool.py delete mode 100644 tests/test_ice_dp_campaign_contract.py delete mode 100644 tests/test_ice_gpu_probe.py delete mode 100644 tests/test_ice_planar_bc_launcher.py delete mode 100644 tests/test_ice_requeue_runner.py delete mode 100644 tests/test_ice_wandb_resume.py delete mode 100644 tests/test_imported_recipe_framework_parity.py delete mode 100644 tests/test_installed_package.py delete mode 100644 tests/test_legacy_data_metric_parity.py delete mode 100644 tests/test_legacy_hpt_trunk_parity.py delete mode 100644 tests/test_libero_action_flow_artifacts.py delete mode 100644 tests/test_libero_active_recipes.py delete mode 100644 tests/test_libero_arc_sweep.py delete mode 100644 tests/test_libero_basis_scaling_audit.py delete mode 100644 tests/test_libero_cluster.py delete mode 100644 tests/test_libero_multi_embodiment_native.py delete mode 100644 tests/test_libero_native_binding_parser.py delete mode 100644 tests/test_libero_native_half_factory_identity.py delete mode 100644 tests/test_libero_native_launch_contract.py delete mode 100644 tests/test_libero_native_scheduler_contract.py delete mode 100644 tests/test_libero_native_smoke_contract.py delete mode 100644 tests/test_libero_native_suite_profiles.py delete mode 100644 tests/test_libero_native_velocity_ablation.py delete mode 100644 tests/test_libero_native_whole_launcher.py delete mode 100644 tests/test_libero_performance.py delete mode 100644 tests/test_libero_saved_hydra_context.py delete mode 100644 tests/test_likelihood_training_behavior_parity.py delete mode 100644 tests/test_local_cpu_storage_guard.py delete mode 100644 tests/test_local_cpu_validation_guard.py delete mode 100644 tests/test_measured_speed_removed.py delete mode 100644 tests/test_multiplier_training_launch.py delete mode 100644 tests/test_native_final_validation_lifecycle.py delete mode 100644 tests/test_oat_step_budget.py delete mode 100644 tests/test_offline_constructor_inputs.py delete mode 100644 tests/test_offline_static_buffer_audit.py delete mode 100644 tests/test_original_size_action_flow_cotrain_config.py delete mode 100644 tests/test_osmo_gate.py delete mode 100644 tests/test_pi_source_patch.py delete mode 100644 tests/test_pipeline_only_surface.py delete mode 100644 tests/test_pusht_parent_cleanup.py delete mode 100644 tests/test_rank_synthetic_trajectory_nn.py delete mode 100644 tests/test_speed_retiming_diagnostic.py delete mode 100644 tests/test_standard_dp_launch_args.py delete mode 100644 tests/test_sync_runtime_script.py delete mode 100644 tests/test_synthetic_action_adapter_flow.py delete mode 100644 tests/test_synthetic_multi_action_adapter_flow.py delete mode 100644 tests/test_synthetic_shared_latent_flow.py delete mode 100644 tests/test_synthetic_trainer_resume.py delete mode 100644 tests/test_synthetic_trainer_signals.py delete mode 100644 tests/test_synthetic_trajectory_eval.py delete mode 100644 tests/test_token_diagnostics.py delete mode 100644 tests/test_unite_diagnostics.py delete mode 100644 tests/test_unite_fixed_step_requeue_boundary.py delete mode 100644 tests/test_unite_ice_contract.py delete mode 100644 tests/test_validate_action_flow_config.py delete mode 100644 tests/test_validate_planar_dataset.py delete mode 100644 tests/test_validate_slurm_job_contract.py delete mode 100644 tests/test_verify_action_flow_training_smoke.py delete mode 100644 tests/test_wandb_health.py delete mode 100644 tests/test_yam_gpu_uuid_contract.py diff --git a/tests/fixtures/legacy_alignment_reference.py b/tests/fixtures/legacy_alignment_reference.py deleted file mode 100644 index 1358433a33..0000000000 --- a/tests/fixtures/legacy_alignment_reference.py +++ /dev/null @@ -1,75 +0,0 @@ -"""Verbatim methods from EgoVerse ec5c903c HPTModel, for parity only.""" - -import torch -from geomloss import SamplesLoss - - -class LegacyAlignment: - def resume_from_depth(self, block_outputs, depth): - """ - Detach at trunk depth and resume trunk forward pass. - Gradients will only flow from depth upward. - """ - cut_tokens = block_outputs[depth - 1].detach() - - blocks = self.trunk["trunk"].blocks - for blk in list(blocks)[depth:]: - cut_tokens = blk(cut_tokens, attn_mask=None) - - if self.trunk["trunk"].post_transformer_layer is not None: - cut_tokens = self.trunk["trunk"].post_transformer_layer(cut_tokens) - - return self.postprocess_tokens(cut_tokens) - - def make_custom_cost(self, scaling_mask): - def custom_cost(x, y): - cost = 0.5 * (((x.unsqueeze(1) - y.unsqueeze(0)) ** 2).sum(dim=-1)) - return cost * scaling_mask - - return custom_cost - - def compute_ot( - self, tokens1, tokens2, emb1_actions, emb2_actions, supervised, lambd - ): - tokens1 = tokens1.reshape(tokens1.shape[0], -1) - tokens2 = tokens2.reshape(tokens1.shape[0], -1) - - if not supervised: - ot_loss_fn = SamplesLoss("sinkhorn", p=2, blur=0.05, truncate=18) - ot_loss = ot_loss_fn(tokens2, tokens1) - avg_feature_dist = torch.norm(tokens2 - tokens1, dim=-1).mean() - return ot_loss, avg_feature_dist - else: - B = tokens1.shape[0] - if not self.ot_6dof: - emb1_actions = emb1_actions[..., :3] - emb2_actions = emb2_actions[..., :3] - if self.use_dtw: - emb2_delta = emb2_actions - emb1_delta = emb1_actions - emb2_expand = emb2_delta.unsqueeze(1).expand(B, B, -1, -1) - emb1_expand = emb1_delta.unsqueeze(0).expand(B, B, -1, -1) - pairwise_dist = self.dtw( - emb2_expand.reshape(B * B, *emb2_actions.shape[1:]), - emb1_expand.reshape(B * B, *emb1_actions.shape[1:]), - ).view(B, B) - else: - emb2_expand = emb2_actions.unsqueeze(1) # (B, 1, T, D) - emb1_expand = emb1_actions.unsqueeze(0) # (1, B, T, D) - pairwise_dist = ((emb2_expand - emb1_expand) ** 2).mean( - dim=(2, 3) - ) # (B, B) #changed - - labels = torch.argmin(pairwise_dist, dim=1) - W = torch.ones(B, B).to(self.device) - W[torch.arange(B), labels] = lambd - - custom_cost_fn = self.make_custom_cost(W) - - ot_loss_fn = SamplesLoss( - loss="sinkhorn", p=2, blur=0.05, cost=custom_cost_fn, truncate=18 - ) - - ot_loss = ot_loss_fn(tokens2, tokens1) - avg_feature_dist = torch.norm(tokens2 - tokens1, dim=-1).mean() - return ot_loss, avg_feature_dist diff --git a/tests/fixtures/legacy_hpt_trunk/metadata.json b/tests/fixtures/legacy_hpt_trunk/metadata.json deleted file mode 100644 index bb81e01d3e..0000000000 --- a/tests/fixtures/legacy_hpt_trunk/metadata.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "source_commit": "ec5c903c067bf1b29bbff781bc414c6df2bcf1f2", - "constructor": { - "embed_dim": 16, - "num_blocks": 2, - "num_heads": 2, - "observation_horizon": 1, - "action_horizon": 3 - }, - "seed": 9226, - "torch_version": "2.7.1", - "source_namespace": "trunk.", - "weights_sha256": "a2b5a6e44028d34195be7de5de62caa1a2bb8a3f6eceb34eec5e9201234e1786", - "reference_sha256": "bb550af3d1ce85a546dd5597c17acf8633663ea0707bd71f662e280829dd8bbd", - "keys": [ - "trunk.blocks.0.attn.in_proj_weight", - "trunk.blocks.0.attn.in_proj_bias", - "trunk.blocks.0.attn.bias_k", - "trunk.blocks.0.attn.bias_v", - "trunk.blocks.0.attn.out_proj.weight", - "trunk.blocks.0.attn.out_proj.bias", - "trunk.blocks.0.norm_1.weight", - "trunk.blocks.0.norm_1.bias", - "trunk.blocks.0.mlp.fc1.weight", - "trunk.blocks.0.mlp.fc1.bias", - "trunk.blocks.0.mlp.fc2.weight", - "trunk.blocks.0.mlp.fc2.bias", - "trunk.blocks.0.norm_2.weight", - "trunk.blocks.0.norm_2.bias", - "trunk.blocks.1.attn.in_proj_weight", - "trunk.blocks.1.attn.in_proj_bias", - "trunk.blocks.1.attn.bias_k", - "trunk.blocks.1.attn.bias_v", - "trunk.blocks.1.attn.out_proj.weight", - "trunk.blocks.1.attn.out_proj.bias", - "trunk.blocks.1.norm_1.weight", - "trunk.blocks.1.norm_1.bias", - "trunk.blocks.1.mlp.fc1.weight", - "trunk.blocks.1.mlp.fc1.bias", - "trunk.blocks.1.mlp.fc2.weight", - "trunk.blocks.1.mlp.fc2.bias", - "trunk.blocks.1.norm_2.weight", - "trunk.blocks.1.norm_2.bias" - ], - "scope": "actual legacy HPTModel trunk factory/loader, masked forward, loss and gradients; generated CPU parameters, not pretrained weights or a policy score" -} diff --git a/tests/fixtures/legacy_hpt_trunk/reference.pt b/tests/fixtures/legacy_hpt_trunk/reference.pt deleted file mode 100644 index 5da2a0afac75d5ab573484146d7c0ddd8e898a15..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41018 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 diff --git a/tests/fixtures/legacy_language_schedule.json b/tests/fixtures/legacy_language_schedule.json deleted file mode 100644 index 6b0a695279..0000000000 --- a/tests/fixtures/legacy_language_schedule.json +++ /dev/null @@ -1,2011 +0,0 @@ -{ - "source_commit": "ec5c903c067bf1b29bbff781bc414c6df2bcf1f2", - "source_path": "egomimic/utils/scheduler_utils.py", - "warmup_epochs": 100, - "total_epochs": 2000, - "eta_min": 1e-05, - "peak_lr": 0.0002, - "values": [ - 2.0000000000000002e-07, - 2.1980000000000004e-06, - 4.196e-06, - 6.194e-06, - 8.192e-06, - 1.019e-05, - 1.2188000000000001e-05, - 1.4186e-05, - 1.6184e-05, - 1.8181999999999996e-05, - 2.0179999999999997e-05, - 2.2177999999999997e-05, - 2.4175999999999994e-05, - 2.617399999999999e-05, - 2.8171999999999987e-05, - 3.0169999999999987e-05, - 3.216799999999999e-05, - 3.4165999999999984e-05, - 3.616399999999998e-05, - 3.8161999999999985e-05, - 4.015999999999998e-05, - 4.2157999999999985e-05, - 4.415599999999998e-05, - 4.615399999999998e-05, - 4.815199999999998e-05, - 5.014999999999998e-05, - 5.214799999999998e-05, - 5.4145999999999986e-05, - 5.614399999999999e-05, - 5.814199999999999e-05, - 6.014e-05, - 6.213800000000001e-05, - 6.413600000000001e-05, - 6.613400000000001e-05, - 6.813200000000002e-05, - 7.013000000000002e-05, - 7.212800000000002e-05, - 7.412600000000003e-05, - 7.612400000000002e-05, - 7.812200000000002e-05, - 8.012000000000003e-05, - 8.211800000000003e-05, - 8.411600000000003e-05, - 8.611400000000004e-05, - 8.811200000000004e-05, - 9.011000000000003e-05, - 9.210800000000003e-05, - 9.410600000000004e-05, - 9.610400000000004e-05, - 9.810200000000003e-05, - 0.00010010000000000003, - 0.00010209800000000004, - 0.00010409600000000004, - 0.00010609400000000005, - 0.00010809200000000004, - 0.00011009000000000004, - 0.00011208800000000003, - 0.00011408600000000002, - 0.00011608400000000004, - 0.00011808200000000005, - 0.00012008000000000006, - 0.00012207800000000005, - 0.00012407600000000004, - 0.00012607400000000003, - 0.00012807200000000002, - 0.00013007000000000003, - 0.00013206800000000002, - 0.00013406600000000001, - 0.000136064, - 0.00013806200000000002, - 0.00014006000000000004, - 0.00014205800000000003, - 0.00014405600000000005, - 0.00014605400000000004, - 0.00014805200000000003, - 0.00015005000000000004, - 0.00015204800000000006, - 0.00015404600000000005, - 0.00015604400000000007, - 0.00015804200000000008, - 0.00016004000000000007, - 0.0001620380000000001, - 0.00016403600000000008, - 0.00016603400000000007, - 0.0001680320000000001, - 0.00017003000000000008, - 0.0001720280000000001, - 0.0001740260000000001, - 0.00017602400000000013, - 0.00017802200000000012, - 0.00018002000000000014, - 0.00018201800000000013, - 0.00018401600000000014, - 0.00018601400000000013, - 0.00018801200000000012, - 0.00019001000000000014, - 0.00019200800000000013, - 0.00019400600000000015, - 0.00019600400000000017, - 0.00019800200000000018, - 0.0002, - 0.00019999987013681378, - 0.0001999994805476102, - 0.00019999883123345428, - 0.00019999792219612135, - 0.00019999675343809658, - 0.00019999532496257536, - 0.0001999936367734631, - 0.0001999916888753752, - 0.0001999894812736372, - 0.00019998701397428453, - 0.00019998428698406274, - 0.0001999813003104273, - 0.00019997805396154366, - 0.00019997454794628723, - 0.0001999707822742433, - 0.0001999667569557071, - 0.00019996247200168362, - 0.00019995792742388786, - 0.00019995312323474449, - 0.00019994805944738796, - 0.0001999427360756625, - 0.00019993715313412194, - 0.0001999313106380299, - 0.00019992520860335957, - 0.00019991884704679357, - 0.00019991222598572423, - 0.00019990534543825328, - 0.00019989820542319183, - 0.00019989080596006042, - 0.0001998831470690889, - 0.00019987522877121643, - 0.00019986705108809128, - 0.00019985861404207096, - 0.000199849917656222, - 0.00019984096195432, - 0.00019983174696084954, - 0.00019982227270100403, - 0.00019981253920068573, - 0.00019980254648650566, - 0.00019979229458578352, - 0.00019978178352654763, - 0.00019977101333753478, - 0.0001997599840481903, - 0.00019974869568866783, - 0.00019973714828982935, - 0.00019972534188324493, - 0.0001997132765011929, - 0.00019970095217665952, - 0.00019968836894333904, - 0.00019967552683563356, - 0.00019966242588865288, - 0.0001996490661382145, - 0.00019963544762084345, - 0.00019962157037377225, - 0.00019960743443494074, - 0.00019959303984299612, - 0.00019957838663729255, - 0.00019956347485789138, - 0.00019954830454556083, - 0.000199532875741776, - 0.00019951718848871863, - 0.00019950124282927707, - 0.0001994850388070461, - 0.00019946857646632694, - 0.00019945185585212703, - 0.00019943487701015982, - 0.00019941763998684484, - 0.0001994001448293075, - 0.00019938239158537885, - 0.0001993643803035956, - 0.00019934611103319987, - 0.0001993275838241392, - 0.00019930879872706621, - 0.0001992897557933387, - 0.00019927045507501926, - 0.0001992508966248754, - 0.00019923108049637908, - 0.0001992110067437069, - 0.00019919067542173975, - 0.00019917008658606263, - 0.00019914924029296467, - 0.00019912813659943884, - 0.00019910677556318182, - 0.00019908515724259386, - 0.00019906328169677862, - 0.00019904114898554304, - 0.00019901875916939708, - 0.00019899611230955361, - 0.00019897320846792835, - 0.0001989500477071395, - 0.0001989266300905077, - 0.00019890295568205578, - 0.0001988790245465087, - 0.00019885483674929332, - 0.00019883039235653807, - 0.00019880569143507302, - 0.00019878073405242956, - 0.0001987555202768402, - 0.00019873005017723844, - 0.00019870432382325859, - 0.00019867834128523546, - 0.0001986521026342044, - 0.00019862560794190083, - 0.0001985988572807602, - 0.00019857185072391786, - 0.00019854458834520867, - 0.00019851707021916693, - 0.00019848929642102617, - 0.00019846126702671883, - 0.00019843298211287628, - 0.00019840444175682834, - 0.0001983756460366033, - 0.0001983465950309275, - 0.00019831728881922536, - 0.0001982877274816189, - 0.0001982579110989277, - 0.00019822783975266854, - 0.00019819751352505543, - 0.00019816693249899908, - 0.00019813609675810686, - 0.00019810500638668248, - 0.00019807366146972583, - 0.00019804206209293276, - 0.00019801020834269472, - 0.00019797810030609865, - 0.00019794573807092669, - 0.00019791312172565598, - 0.00019788025135945836, - 0.0001978471270622001, - 0.00019781374892444188, - 0.00019778011703743817, - 0.00019774623149313724, - 0.00019771209238418093, - 0.00019767769980390425, - 0.0001976430538463352, - 0.0001976081546061945, - 0.00019757300217889532, - 0.00019753759666054312, - 0.00019750193814793517, - 0.00019746602673856049, - 0.0001974298625305995, - 0.0001973934456229238, - 0.00019735677611509576, - 0.0001973198541073685, - 0.00019728267970068532, - 0.00019724525299667968, - 0.0001972075740976747, - 0.00019716964310668306, - 0.00019713146012740666, - 0.00019709302526423628, - 0.00019705433862225142, - 0.00019701540030721986, - 0.00019697621042559746, - 0.00019693676908452785, - 0.00019689707639184215, - 0.00019685713245605867, - 0.00019681693738638265, - 0.0001967764912927058, - 0.00019673579428560627, - 0.00019669484647634805, - 0.00019665364797688092, - 0.00019661219889984002, - 0.00019657049935854553, - 0.00019652854946700243, - 0.00019648634933990005, - 0.00019644389909261197, - 0.0001964011988411956, - 0.00019635824870239168, - 0.00019631504879362435, - 0.00019627159923300046, - 0.00019622790013930946, - 0.000196183951632023, - 0.00019613975383129466, - 0.0001960953068579595, - 0.00019605061083353387, - 0.00019600566588021494, - 0.00019596047212088057, - 0.00019591502967908873, - 0.00019586933867907734, - 0.00019582339924576383, - 0.00019577721150474488, - 0.00019573077558229598, - 0.0001956840916053712, - 0.0001956371597016027, - 0.00019558997999930054, - 0.00019554255262745226, - 0.00019549487771572245, - 0.0001954469553944525, - 0.0001953987857946602, - 0.00019535036904803942, - 0.00019530170528695969, - 0.00019525279464446585, - 0.00019520363725427777, - 0.00019515423325078984, - 0.00019510458276907072, - 0.00019505468594486298, - 0.00019500454291458257, - 0.00019495415381531864, - 0.00019490351878483309, - 0.0001948526379615601, - 0.00019480151148460598, - 0.00019475013949374852, - 0.00019469852212943678, - 0.00019464665953279066, - 0.00019459455184560052, - 0.0001945421992103268, - 0.00019448960177009963, - 0.00019443675966871835, - 0.00019438367305065128, - 0.00019433034206103517, - 0.00019427676684567494, - 0.0001942229475510432, - 0.0001941688843242798, - 0.0001941145773131916, - 0.0001940600266662518, - 0.00019400523253259986, - 0.00019395019506204087, - 0.00019389491440504513, - 0.0001938393907127478, - 0.00019378362413694864, - 0.00019372761483011123, - 0.00019367136294536297, - 0.00019361486863649432, - 0.00019355813205795857, - 0.00019350115336487137, - 0.00019344393271301027, - 0.00019338647025881435, - 0.00019332876615938376, - 0.00019327082057247932, - 0.00019321263365652203, - 0.0001931542055705927, - 0.00019309553647443154, - 0.00019303662652843756, - 0.00019297747589366832, - 0.0001929180847318394, - 0.00019285845320532398, - 0.00019279858147715236, - 0.00019273846971101156, - 0.00019267811807124487, - 0.00019261752672285136, - 0.00019255669583148545, - 0.0001924956255634565, - 0.0001924343160857282, - 0.00019237276756591847, - 0.00019231098017229848, - 0.0001922489540737927, - 0.00019218668943997801, - 0.00019212418644108366, - 0.0001920614452479904, - 0.00019199846603223025, - 0.00019193524896598602, - 0.0001918717942220907, - 0.0001918081019740272, - 0.00019174417239592765, - 0.0001916800056625731, - 0.00019161560194939293, - 0.00019155096143246445, - 0.00019148608428851233, - 0.00019142097069490822, - 0.00019135562082967024, - 0.00019129003487146237, - 0.00019122421299959416, - 0.00019115815539402003, - 0.00019109186223533904, - 0.0001910253337047941, - 0.0001909585699842717, - 0.00019089157125630124, - 0.00019082433770405474, - 0.00019075686951134613, - 0.00019068916686263096, - 0.00019062122994300556, - 0.00019055305893820693, - 0.00019048465403461202, - 0.0001904160154192372, - 0.00019034714327973782, - 0.00019027803780440766, - 0.00019020869918217852, - 0.00019013912760261947, - 0.0001900693232559366, - 0.0001899992863329723, - 0.00018992901702520487, - 0.00018985851552474791, - 0.00018978778202434984, - 0.00018971681671739327, - 0.00018964561979789473, - 0.00018957419146050383, - 0.00018950253190050295, - 0.0001894306413138066, - 0.00018935851989696086, - 0.0001892861678471429, - 0.00018921358536216055, - 0.00018914077264045148, - 0.00018906772988108294, - 0.00018899445728375104, - 0.00018892095504878024, - 0.00018884722337712283, - 0.0001887732624703584, - 0.00018869907253069327, - 0.00018862465376095987, - 0.0001885500063646163, - 0.00018847513054574567, - 0.0001884000265090556, - 0.00018832469445987773, - 0.0001882491346041669, - 0.00018817334714850095, - 0.00018809733230007984, - 0.0001880210902667253, - 0.00018794462125688012, - 0.00018786792547960767, - 0.00018779100314459123, - 0.00018771385446213359, - 0.00018763647964315625, - 0.00018755887889919905, - 0.00018748105244241941, - 0.00018740300048559195, - 0.0001873247232421077, - 0.00018724622092597366, - 0.0001871674937518122, - 0.00018708854193486037, - 0.00018700936569096942, - 0.0001869299652366042, - 0.00018685034078884247, - 0.00018677049256537453, - 0.00018669042078450228, - 0.00018661012566513897, - 0.00018652960742680837, - 0.00018644886628964424, - 0.00018636790247438981, - 0.00018628671620239703, - 0.00018620530769562608, - 0.0001861236771766447, - 0.00018604182486862763, - 0.00018595975099535596, - 0.0001858774557812165, - 0.0001857949394512012, - 0.00018571220223090662, - 0.0001856292443465331, - 0.00018554606602488437, - 0.0001854626674933668, - 0.0001853790489799887, - 0.00018529521071336002, - 0.0001852111529226913, - 0.00018512687583779332, - 0.00018504237968907644, - 0.0001849576647075499, - 0.00018487273112482124, - 0.0001847875791730956, - 0.00018470220908517522, - 0.00018461662109445857, - 0.00018453081543494005, - 0.000184444792341209, - 0.00018435855204844927, - 0.00018427209479243852, - 0.00018418542080954762, - 0.0001840985303367399, - 0.0001840114236115706, - 0.00018392410087218622, - 0.00018383656235732376, - 0.0001837488083063102, - 0.00018366083895906171, - 0.00018357265455608317, - 0.00018348425533846736, - 0.0001833956415478944, - 0.00018330681342663095, - 0.00018321777121752973, - 0.00018312851516402874, - 0.00018303904551015063, - 0.00018294936250050201, - 0.00018285946638027279, - 0.00018276935739523558, - 0.0001826790357917448, - 0.0001825885018167364, - 0.0001824977557177267, - 0.00018240679774281213, - 0.0001823156281406683, - 0.00018222424716054941, - 0.00018213265505228758, - 0.00018204085206629216, - 0.00018194883845354897, - 0.00018185661446561976, - 0.00018176418035464135, - 0.00018167153637332507, - 0.00018157868277495607, - 0.0001814856198133925, - 0.00018139234774306495, - 0.00018129886681897569, - 0.00018120517729669803, - 0.0001811112794323755, - 0.00018101717348272136, - 0.00018092285970501757, - 0.00018082833835711447, - 0.00018073360969742978, - 0.0001806386739849481, - 0.00018054353147921998, - 0.00018044818244036145, - 0.00018035262712905309, - 0.00018025686580653948, - 0.00018016089873462842, - 0.0001800647261756903, - 0.0001799683483926571, - 0.00017987176564902206, - 0.00017977497820883872, - 0.00017967798633672022, - 0.00017958079029783863, - 0.00017948339035792424, - 0.00017938578678326474, - 0.0001792879798407046, - 0.00017918996979764427, - 0.00017909175692203943, - 0.0001789933414824004, - 0.00017889472374779113, - 0.00017879590398782886, - 0.0001786968824726829, - 0.00017859765947307441, - 0.00017849823526027523, - 0.00017839861010610737, - 0.0001782987842829422, - 0.0001781987580636997, - 0.00017809853172184777, - 0.00017799810553140137, - 0.00017789747976692182, - 0.0001777966547035162, - 0.00017769563061683635, - 0.0001775944077830783, - 0.00017749298647898136, - 0.00017739136698182752, - 0.00017728954956944062, - 0.00017718753452018553, - 0.00017708532211296757, - 0.00017698291262723152, - 0.00017688030634296098, - 0.0001767775035406777, - 0.0001766745045014405, - 0.00017657130950684492, - 0.00017646791883902207, - 0.00017636433278063812, - 0.00017626055161489338, - 0.00017615657562552163, - 0.00017605240509678918, - 0.00017594804031349434, - 0.0001758434815609664, - 0.00017573872912506508, - 0.00017563378329217948, - 0.00017552864434922748, - 0.00017542331258365497, - 0.00017531778828343492, - 0.00017521207173706684, - 0.00017510616323357565, - 0.00017500006306251116, - 0.00017489377151394723, - 0.00017478728887848082, - 0.0001746806154472315, - 0.00017457375151184028, - 0.00017446669736446906, - 0.00017435945329779987, - 0.0001742520196050338, - 0.00017414439657989055, - 0.00017403658451660732, - 0.00017392858370993816, - 0.00017382039445515318, - 0.00017371201704803764, - 0.00017360345178489127, - 0.00017349469896252732, - 0.00017338575887827188, - 0.00017327663182996298, - 0.0001731673181159498, - 0.00017305781803509185, - 0.00017294813188675813, - 0.00017283825997082647, - 0.00017272820258768245, - 0.00017261796003821875, - 0.00017250753262383427, - 0.0001723969206464334, - 0.00017228612440842498, - 0.00017217514421272176, - 0.00017206398036273934, - 0.00017195263316239543, - 0.00017184110291610904, - 0.00017172938992879958, - 0.00017161749450588616, - 0.0001715054169532865, - 0.00017139315757741638, - 0.00017128071668518868, - 0.0001711680945840125, - 0.00017105529158179236, - 0.00017094230798692732, - 0.00017082914410831025, - 0.00017071580025532687, - 0.00017060227673785497, - 0.00017048857386626345, - 0.0001703746919514117, - 0.00017026063130464844, - 0.00017014639223781117, - 0.00017003197506322513, - 0.0001699173800937025, - 0.0001698026076425416, - 0.00016968765802352585, - 0.00016957253155092317, - 0.0001694572285394849, - 0.00016934174930444514, - 0.00016922609416151961, - 0.00016911026342690514, - 0.0001689942574172785, - 0.0001688780764497957, - 0.0001687617208420911, - 0.0001686451909122764, - 0.00016852848697894004, - 0.0001684116093611461, - 0.00016829455837843354, - 0.00016817733435081528, - 0.00016805993759877734, - 0.00016794236844327792, - 0.00016782462720574663, - 0.0001677067142080835, - 0.00016758862977265819, - 0.00016747037422230903, - 0.00016735194788034216, - 0.00016723335107053067, - 0.00016711458411711373, - 0.00016699564734479566, - 0.00016687654107874506, - 0.00016675726564459388, - 0.00016663782136843664, - 0.0001665182085768294, - 0.00016639842759678903, - 0.00016627847875579205, - 0.0001661583623817741, - 0.0001660380788031287, - 0.00016591762834870658, - 0.0001657970113478147, - 0.00016567622813021534, - 0.00016555527902612516, - 0.00016543416436621446, - 0.00016531288448160602, - 0.00016519143970387446, - 0.00016506983036504514, - 0.00016494805679759337, - 0.00016482611933444345, - 0.00016470401830896768, - 0.00016458175405498568, - 0.00016445932690676318, - 0.00016433673719901142, - 0.00016421398526688592, - 0.00016409107144598574, - 0.00016396799607235263, - 0.00016384475948246995, - 0.00016372136201326184, - 0.00016359780400209225, - 0.00016347408578676407, - 0.00016335020770551816, - 0.00016322617009703247, - 0.0001631019733004211, - 0.00016297761765523335, - 0.00016285310350145282, - 0.00016272843117949644, - 0.00016260360103021354, - 0.00016247861339488501, - 0.00016235346861522226, - 0.00016222816703336638, - 0.00016210270899188708, - 0.00016197709483378186, - 0.00016185132490247503, - 0.00016172539954181677, - 0.00016159931909608225, - 0.00016147308390997053, - 0.00016134669432860382, - 0.0001612201506975264, - 0.00016109345336270375, - 0.00016096660267052155, - 0.00016083959896778468, - 0.00016071244260171648, - 0.0001605851339199576, - 0.00016045767327056508, - 0.00016033006100201147, - 0.00016020229746318388, - 0.00016007438300338293, - 0.00015994631797232184, - 0.00015981810272012553, - 0.00015968973759732962, - 0.0001595612229548794, - 0.00015943255914412907, - 0.00015930374651684055, - 0.00015917478542518262, - 0.00015904567622173, - 0.00015891641925946232, - 0.0001587870148917632, - 0.0001586574634724192, - 0.00015852776535561894, - 0.0001583979208959522, - 0.0001582679304484087, - 0.00015813779436837737, - 0.00015800751301164534, - 0.0001578770867343968, - 0.00015774651589321221, - 0.00015761580084506732, - 0.00015748494194733204, - 0.00015735393955776958, - 0.00015722279403453554, - 0.00015709150573617672, - 0.00015696007502163033, - 0.00015682850225022293, - 0.00015669678778166944, - 0.00015656493197607218, - 0.0001564329351939199, - 0.0001563007977960868, - 0.0001561685201438314, - 0.0001560361025987958, - 0.0001559035455230045, - 0.00015577084927886347, - 0.0001556380142291592, - 0.00015550504073705764, - 0.00015537192916610326, - 0.000155238679880218, - 0.00015510529324370024, - 0.00015497176962122404, - 0.00015483810937783787, - 0.00015470431287896376, - 0.00015457038049039614, - 0.00015443631257830112, - 0.00015430210950921524, - 0.00015416777165004454, - 0.00015403329936806366, - 0.0001538986930309146, - 0.000153763953006606, - 0.00015362907966351191, - 0.00015349407337037095, - 0.0001533589344962851, - 0.00015322366341071893, - 0.00015308826048349841, - 0.00015295272608481, - 0.0001528170605851995, - 0.00015268126435557128, - 0.000152545337767187, - 0.00015240928119166478, - 0.0001522730950009781, - 0.00015213677956745481, - 0.00015200033526377611, - 0.00015186376246297548, - 0.00015172706153843775, - 0.00015159023286389806, - 0.00015145327681344076, - 0.0001513161937614985, - 0.00015117898408285104, - 0.00015104164815262443, - 0.0001509041863462899, - 0.00015076659903966272, - 0.00015062888660890138, - 0.00015049104943050637, - 0.0001503530878813193, - 0.00015021500233852178, - 0.00015007679317963438, - 0.00014993846078251567, - 0.00014980000552536116, - 0.0001496614277867022, - 0.00014952272794540512, - 0.00014938390638066988, - 0.0001492449634720294, - 0.00014910589959934832, - 0.00014896671514282188, - 0.00014882741048297512, - 0.00014868798600066162, - 0.00014854844207706262, - 0.00014840877909368588, - 0.00014826899743236463, - 0.00014812909747525667, - 0.00014798907960484307, - 0.00014784894420392738, - 0.0001477086916556344, - 0.0001475683223434093, - 0.0001474278366510164, - 0.00014728723496253816, - 0.00014714651766237422, - 0.00014700568513524042, - 0.00014686473776616738, - 0.0001467236759404999, - 0.00014658250004389567, - 0.0001464412104623241, - 0.00014629980758206558, - 0.00014615829178971014, - 0.00014601666347215656, - 0.0001458749230166113, - 0.0001457330708105873, - 0.00014559110724190306, - 0.00014544903269868162, - 0.00014530684756934933, - 0.00014516455224263488, - 0.0001450221471075683, - 0.00014487963255347978, - 0.00014473700896999863, - 0.0001445942767470523, - 0.00014445143627486528, - 0.00014430848794395792, - 0.00014416543214514551, - 0.00014402226926953714, - 0.00014387899970853466, - 0.00014373562385383156, - 0.00014359214209741194, - 0.00014344855483154948, - 0.00014330486244880627, - 0.0001431610653420318, - 0.00014301716390436185, - 0.0001428731585292175, - 0.0001427290496103039, - 0.00014258483754160935, - 0.0001424405227174041, - 0.00014229610553223942, - 0.00014215158638094638, - 0.00014200696565863478, - 0.0001418622437606922, - 0.0001417174210827827, - 0.0001415724980208461, - 0.00014142747497109638, - 0.00014128235233002107, - 0.00014113713049437992, - 0.0001409918098612039, - 0.00014084639082779406, - 0.00014070087379172052, - 0.00014055525915082124, - 0.0001404095473032011, - 0.00014026373864723076, - 0.00014011783358154552, - 0.00013997183250504422, - 0.0001398257358168882, - 0.00013967954391650033, - 0.00013953325720356363, - 0.00013938687607802035, - 0.00013924040094007094, - 0.00013909383219017283, - 0.0001389471702290394, - 0.00013880041545763882, - 0.00013865356827719305, - 0.00013850662908917668, - 0.0001383595982953158, - 0.00013821247629758703, - 0.00013806526349821626, - 0.00013791796029967774, - 0.00013777056710469266, - 0.0001376230843162285, - 0.00013747551233749748, - 0.00013732785157195585, - 0.0001371801024233024, - 0.0001370322652954778, - 0.000136884340592663, - 0.0001367363287192785, - 0.00013658823007998313, - 0.00013644004507967292, - 0.00013629177412348, - 0.0001361434176167715, - 0.00013599497596514845, - 0.00013584644957444466, - 0.0001356978388507256, - 0.0001355491442002873, - 0.00013540036602965527, - 0.0001352515047455834, - 0.00013510256075505264, - 0.0001349535344652703, - 0.00013480442628366844, - 0.00013465523661790324, - 0.0001345059658758535, - 0.00013435661446561974, - 0.000134207182795523, - 0.0001340576712741038, - 0.00013390808031012085, - 0.00013375841031255026, - 0.00013360866169058398, - 0.00013345883485362905, - 0.0001333089302113063, - 0.00013315894817344934, - 0.00013300888915010334, - 0.0001328587535515239, - 0.00013270854178817603, - 0.000132558254270733, - 0.00013240789141007506, - 0.00013225745361728858, - 0.00013210694130366473, - 0.00013195635488069844, - 0.00013180569476008725, - 0.00013165496135373014, - 0.00013150415507372654, - 0.00013135327633237502, - 0.00013120232554217226, - 0.00013105130311581205, - 0.00013090020946618386, - 0.00013074904500637202, - 0.00013059781014965432, - 0.00013044650530950116, - 0.0001302951308995741, - 0.0001301436873337251, - 0.00012999217502599502, - 0.00012984059439061276, - 0.00012968894584199396, - 0.00012953722979473998, - 0.00012938544666363667, - 0.00012923359686365334, - 0.00012908168080994156, - 0.000128929698917834, - 0.00012877765160284333, - 0.00012862553928066117, - 0.0001284733623671567, - 0.00012832112127837586, - 0.00012816881643053996, - 0.00012801644824004467, - 0.00012786401712345873, - 0.00012771152349752308, - 0.0001275589677791494, - 0.00012740635038541924, - 0.00012725367173358268, - 0.00012710093224105738, - 0.00012694813232542725, - 0.00012679527240444145, - 0.00012664235289601308, - 0.00012648937421821837, - 0.0001263363367892951, - 0.00012618324102764176, - 0.00012603008735181636, - 0.00012587687618053516, - 0.00012572360793267165, - 0.00012557028302725537, - 0.00012541690188347078, - 0.00012526346492065606, - 0.000125109972558302, - 0.00012495642521605086, - 0.00012480282331369522, - 0.00012464916727117681, - 0.0001244954575085854, - 0.0001243416944461576, - 0.00012418787850427572, - 0.0001240340101034667, - 0.00012388008966440086, - 0.00012372611760789082, - 0.00012357209435489032, - 0.00012341802032649296, - 0.00012326389594393138, - 0.00012310972162857568, - 0.00012295549780193255, - 0.00012280122488564406, - 0.00012264690330148647, - 0.0001224925334713691, - 0.00012233811581733323, - 0.00012218365076155068, - 0.00012202913872632315, - 0.00012187458013408058, - 0.00012171997540738028, - 0.00012156532496890566, - 0.00012141062924146514, - 0.00012125588864799089, - 0.00012110110361153782, - 0.00012094627455528228, - 0.00012079140190252103, - 0.00012063648607666995, - 0.00012048152750126302, - 0.00012032652659995108, - 0.00012017148379650068, - 0.00012001639951479283, - 0.00011986127417882218, - 0.00011970610821269536, - 0.00011955090204063027, - 0.00011939565608695457, - 0.00011924037077610488, - 0.00011908504653262522, - 0.00011892968378116621, - 0.0001187742829464836, - 0.00011861884445343741, - 0.00011846336872699054, - 0.00011830785619220763, - 0.00011815230727425404, - 0.00011799672239839464, - 0.00011784110198999245, - 0.00011768544647450775, - 0.00011752975627749677, - 0.00011737403182461062, - 0.00011721827354159392, - 0.00011706248185428392, - 0.00011690665718860912, - 0.00011675079997058826, - 0.00011659491062632893, - 0.00011643898958202677, - 0.00011628303726396383, - 0.0001161270540985079, - 0.0001159710405121109, - 0.0001158149969313081, - 0.00011565892378271665, - 0.0001155028214930346, - 0.0001153466904890396, - 0.00011519053119758795, - 0.00011503434404561308, - 0.00011487812946012477, - 0.00011472188786820771, - 0.00011456561969702045, - 0.0001144093253737942, - 0.00011425300532583165, - 0.00011409665998050589, - 0.0001139402897652591, - 0.00011378389510760146, - 0.00011362747643511001, - 0.00011347103417542745, - 0.00011331456875626091, - 0.00011315808060538084, - 0.00011300157015061996, - 0.00011284503781987182, - 0.00011268848404108985, - 0.00011253190924228605, - 0.00011237531385153003, - 0.00011221869829694753, - 0.00011206206300671958, - 0.00011190540840908098, - 0.0001117487349323195, - 0.00011159204300477441, - 0.00011143533305483544, - 0.00011127860551094167, - 0.00011112186080158016, - 0.000110965099355285, - 0.00011080832160063595, - 0.00011065152796625746, - 0.0001104947188808173, - 0.00011033789477302554, - 0.00011018105607163328, - 0.0001100242032054316, - 0.00010986733660325015, - 0.00010971045669395633, - 0.00010955356390645376, - 0.00010939665866968136, - 0.00010923974141261204, - 0.00010908281256425159, - 0.00010892587255363746, - 0.00010876892180983773, - 0.00010861196076194966, - 0.00010845498983909879, - 0.00010829800947043762, - 0.0001081410200851445, - 0.00010798402211242238, - 0.0001078270159814978, - 0.00010767000212161947, - 0.00010751298096205728, - 0.00010735595293210107, - 0.00010719891846105954, - 0.00010704187797825888, - 0.00010688483191304183, - 0.00010672778069476626, - 0.00010657072475280428, - 0.00010641366451654079, - 0.00010625660041537248, - 0.00010609953287870663, - 0.00010594246233595989, - 0.00010578538921655709, - 0.00010562831394993017, - 0.0001054712369655169, - 0.00010531415869275978, - 0.00010515707956110476, - 0.00010500000000000023, - 0.0001048429204388957, - 0.00010468584130724072, - 0.00010452876303448355, - 0.0001043716860500703, - 0.00010421461078344338, - 0.0001040575376640406, - 0.00010390046712129383, - 0.00010374339958462799, - 0.00010358633548345969, - 0.00010342927524719621, - 0.00010327221930523419, - 0.00010311516808695866, - 0.00010295812202174159, - 0.00010280108153894093, - 0.00010264404706789938, - 0.0001024870190379432, - 0.000102329997878381, - 0.00010217298401850264, - 0.00010201597788757803, - 0.00010185897991485595, - 0.00010170199052956281, - 0.00010154501016090167, - 0.0001013880392380508, - 0.00010123107819016275, - 0.00010107412744636295, - 0.00010091718743574887, - 0.0001007602585873884, - 0.0001006033413303191, - 0.00010044643609354666, - 0.0001002895433060441, - 0.00010013266339675026, - 9.997579679456885e-05, - 9.981894392836712e-05, - 9.96621052269749e-05, - 9.950528111918314e-05, - 9.934847203374297e-05, - 9.919167839936447e-05, - 9.903490064471545e-05, - 9.887813919842027e-05, - 9.872139448905876e-05, - 9.856466694516498e-05, - 9.840795699522605e-05, - 9.825126506768094e-05, - 9.809459159091948e-05, - 9.79379369932809e-05, - 9.778130170305294e-05, - 9.762468614847044e-05, - 9.74680907577144e-05, - 9.731151595891065e-05, - 9.715496218012867e-05, - 9.699842984938049e-05, - 9.684191939461959e-05, - 9.668543124373957e-05, - 9.6528965824573e-05, - 9.637252356489044e-05, - 9.6216104892399e-05, - 9.605971023474136e-05, - 9.590334001949454e-05, - 9.57469946741688e-05, - 9.559067462620626e-05, - 9.543438030298e-05, - 9.527811213179273e-05, - 9.512187053987569e-05, - 9.496565595438737e-05, - 9.480946880241252e-05, - 9.465330951096081e-05, - 9.449717850696585e-05, - 9.434107621728378e-05, - 9.418500306869233e-05, - 9.402895948788953e-05, - 9.387294590149257e-05, - 9.371696273603661e-05, - 9.356101041797369e-05, - 9.340508937367149e-05, - 9.324920002941221e-05, - 9.30933428113913e-05, - 9.293751814571654e-05, - 9.278172645840654e-05, - 9.262596817538988e-05, - 9.247024372250367e-05, - 9.231455352549273e-05, - 9.215889801000804e-05, - 9.200327760160585e-05, - 9.184769272574638e-05, - 9.169214380779284e-05, - 9.153663127300994e-05, - 9.138115554656304e-05, - 9.122571705351685e-05, - 9.107031621883429e-05, - 9.091495346737525e-05, - 9.075962922389559e-05, - 9.060434391304587e-05, - 9.044909795937023e-05, - 9.029389178730512e-05, - 9.01387258211783e-05, - 8.998360048520761e-05, - 8.982851620349982e-05, - 8.967347340004938e-05, - 8.951847249873743e-05, - 8.936351392333052e-05, - 8.920859809747946e-05, - 8.905372544471818e-05, - 8.889889638846267e-05, - 8.874411135200959e-05, - 8.858937075853538e-05, - 8.843467503109482e-05, - 8.828002459262023e-05, - 8.812541986591992e-05, - 8.797086127367735e-05, - 8.781634923844979e-05, - 8.766188418266728e-05, - 8.750746652863135e-05, - 8.735309669851397e-05, - 8.719877511435638e-05, - 8.70445021980679e-05, - 8.689027837142476e-05, - 8.673610405606905e-05, - 8.658197967350744e-05, - 8.642790564511014e-05, - 8.627388239210958e-05, - 8.611991033559955e-05, - 8.59659898965337e-05, - 8.581212149572469e-05, - 8.565830555384278e-05, - 8.550454249141496e-05, - 8.535083272882353e-05, - 8.519717668630514e-05, - 8.504357478394947e-05, - 8.489002744169834e-05, - 8.473653507934427e-05, - 8.458309811652954e-05, - 8.442971697274494e-05, - 8.42763920673287e-05, - 8.412312381946516e-05, - 8.396991264818392e-05, - 8.38167589723585e-05, - 8.366366321070519e-05, - 8.351062578178188e-05, - 8.335764710398713e-05, - 8.32047275955588e-05, - 8.305186767457298e-05, - 8.289906775894282e-05, - 8.274632826641749e-05, - 8.259364961458095e-05, - 8.244103222085076e-05, - 8.228847650247707e-05, - 8.213598287654138e-05, - 8.198355175995546e-05, - 8.183118356946013e-05, - 8.167887872162425e-05, - 8.15266376328434e-05, - 8.137446071933897e-05, - 8.122234839715674e-05, - 8.10703010821661e-05, - 8.091831919005853e-05, - 8.076640313634674e-05, - 8.061455333636338e-05, - 8.04627702052601e-05, - 8.03110541580061e-05, - 8.015940560938728e-05, - 8.0007824974005e-05, - 7.985631266627493e-05, - 7.970486910042588e-05, - 7.955349469049884e-05, - 7.940218985034565e-05, - 7.925095499362798e-05, - 7.90997905338161e-05, - 7.89486968841879e-05, - 7.879767445782768e-05, - 7.864672366762496e-05, - 7.849584492627338e-05, - 7.834503864626977e-05, - 7.819430523991267e-05, - 7.804364511930147e-05, - 7.789305869633517e-05, - 7.774254638271133e-05, - 7.759210858992487e-05, - 7.744174572926695e-05, - 7.729145821182386e-05, - 7.714124644847602e-05, - 7.699111084989659e-05, - 7.684105182655057e-05, - 7.669106978869361e-05, - 7.65411651463709e-05, - 7.639133830941596e-05, - 7.624158968744966e-05, - 7.609191968987902e-05, - 7.594232872589613e-05, - 7.57928172044769e-05, - 7.564338553438016e-05, - 7.54940341241464e-05, - 7.534476338209666e-05, - 7.519557371633143e-05, - 7.504646553472962e-05, - 7.489743924494724e-05, - 7.474849525441653e-05, - 7.45996339703446e-05, - 7.44508557997126e-05, - 7.43021611492743e-05, - 7.415355042555527e-05, - 7.400502403485144e-05, - 7.38565823832284e-05, - 7.37082258765199e-05, - 7.355995492032698e-05, - 7.341176992001677e-05, - 7.326367128072145e-05, - 7.311565940733694e-05, - 7.296773470452212e-05, - 7.281989757669748e-05, - 7.267214842804409e-05, - 7.252448766250243e-05, - 7.237691568377144e-05, - 7.222943289530726e-05, - 7.208203970032223e-05, - 7.193473650178363e-05, - 7.178752370241289e-05, - 7.164040170468411e-05, - 7.149337091082326e-05, - 7.134643172280685e-05, - 7.119958454236108e-05, - 7.10528297709605e-05, - 7.090616780982707e-05, - 7.075959905992896e-05, - 7.061312392197957e-05, - 7.046674279643631e-05, - 7.032045608349958e-05, - 7.017426418311172e-05, - 7.002816749495576e-05, - 6.988216641845445e-05, - 6.973626135276916e-05, - 6.959045269679885e-05, - 6.944474084917872e-05, - 6.929912620827944e-05, - 6.915360917220585e-05, - 6.900819013879603e-05, - 6.886286950562e-05, - 6.871764766997887e-05, - 6.857252502890357e-05, - 6.842750197915387e-05, - 6.828257891721722e-05, - 6.813775623930776e-05, - 6.799303434136516e-05, - 6.784841361905358e-05, - 6.770389446776053e-05, - 6.755947728259585e-05, - 6.741516245839062e-05, - 6.727095038969606e-05, - 6.712684147078242e-05, - 6.698283609563805e-05, - 6.683893465796811e-05, - 6.669513755119365e-05, - 6.655144516845041e-05, - 6.640785790258798e-05, - 6.626437614616837e-05, - 6.612100029146528e-05, - 6.597773073046279e-05, - 6.583456785485445e-05, - 6.569151205604204e-05, - 6.554856372513468e-05, - 6.540572325294766e-05, - 6.526299103000132e-05, - 6.51203674465202e-05, - 6.497785289243165e-05, - 6.483544775736506e-05, - 6.469315243065065e-05, - 6.455096730131837e-05, - 6.440889275809691e-05, - 6.426692918941272e-05, - 6.41250769833887e-05, - 6.398333652784345e-05, - 6.384170821028987e-05, - 6.370019241793445e-05, - 6.355878953767594e-05, - 6.341749995610435e-05, - 6.327632405950011e-05, - 6.313526223383267e-05, - 6.299431486475965e-05, - 6.285348233762582e-05, - 6.271276503746191e-05, - 6.257216334898368e-05, - 6.243167765659073e-05, - 6.229130834436562e-05, - 6.215105579607266e-05, - 6.201092039515698e-05, - 6.187090252474336e-05, - 6.17310025676354e-05, - 6.159122090631417e-05, - 6.145155792293747e-05, - 6.131201399933846e-05, - 6.117258951702499e-05, - 6.1033284857178234e-05, - 6.089410040065178e-05, - 6.0755036527970675e-05, - 6.061609361933023e-05, - 6.047727205459498e-05, - 6.033857221329789e-05, - 6.0199994474638934e-05, - 6.0061539217484445e-05, - 5.992320682036572e-05, - 5.978499766147833e-05, - 5.96469121186808e-05, - 5.9508950569493744e-05, - 5.9371113391098714e-05, - 5.923340096033739e-05, - 5.909581365371019e-05, - 5.8958351847375664e-05, - 5.8821015917149046e-05, - 5.868380623850161e-05, - 5.854672318655934e-05, - 5.8409767136102034e-05, - 5.827293846156237e-05, - 5.813623753702468e-05, - 5.799966473622403e-05, - 5.786322043254533e-05, - 5.772690499902202e-05, - 5.759071880833536e-05, - 5.745466223281315e-05, - 5.731873564442885e-05, - 5.718293941480063e-05, - 5.704727391519016e-05, - 5.691173951650171e-05, - 5.677633658928117e-05, - 5.6641065503715017e-05, - 5.650592662962919e-05, - 5.637092033648818e-05, - 5.6236046993394116e-05, - 5.610130696908551e-05, - 5.596670063193644e-05, - 5.583222834995554e-05, - 5.569789049078483e-05, - 5.556368742169895e-05, - 5.542961950960391e-05, - 5.5295687121036317e-05, - 5.5161890622162184e-05, - 5.5028230378776036e-05, - 5.4894706756299826e-05, - 5.4761320119782116e-05, - 5.46280708338968e-05, - 5.449495926294241e-05, - 5.436198577084084e-05, - 5.4229150721136586e-05, - 5.40964544769956e-05, - 5.396389740120427e-05, - 5.383147985616869e-05, - 5.3699202203913304e-05, - 5.356706480608015e-05, - 5.343506802392789e-05, - 5.330321221833061e-05, - 5.317149774977712e-05, - 5.303992497836973e-05, - 5.290849426382332e-05, - 5.277720596546451e-05, - 5.264606044223047e-05, - 5.2515058052668014e-05, - 5.2384199154932744e-05, - 5.225348410678782e-05, - 5.212291326560327e-05, - 5.199248698835471e-05, - 5.1862205631622704e-05, - 5.1732069551591366e-05, - 5.160207910404785e-05, - 5.14722346443811e-05, - 5.134253652758087e-05, - 5.121298510823684e-05, - 5.1083580740537716e-05, - 5.0954323778270003e-05, - 5.08252145748174e-05, - 5.069625348315948e-05, - 5.056744085587093e-05, - 5.0438777045120604e-05, - 5.0310262402670425e-05, - 5.0181897279874485e-05, - 5.005368202767819e-05, - 4.9925616996617074e-05, - 4.9797702536816114e-05, - 4.966993899798853e-05, - 4.954232672943491e-05, - 4.941486608004237e-05, - 4.928755739828352e-05, - 4.9160401032215306e-05, - 4.903339732947847e-05, - 4.890654663729622e-05, - 4.877984930247357e-05, - 4.865330567139617e-05, - 4.852691609002944e-05, - 4.8400680903917706e-05, - 4.82746004581832e-05, - 4.814867509752493e-05, - 4.802290516621807e-05, - 4.789729100811286e-05, - 4.7771832966633576e-05, - 4.764653138477771e-05, - 4.752138660511496e-05, - 4.739639896978642e-05, - 4.727156882050357e-05, - 4.714689649854715e-05, - 4.702238234476658e-05, - 4.689802669957885e-05, - 4.677382990296751e-05, - 4.664979229448181e-05, - 4.652591421323592e-05, - 4.640219599790771e-05, - 4.627863798673815e-05, - 4.6155240517530006e-05, - 4.603200392764729e-05, - 4.59089285540142e-05, - 4.578601473311404e-05, - 4.566326280098849e-05, - 4.554067309323671e-05, - 4.541824594501422e-05, - 4.529598169103218e-05, - 4.517388066555648e-05, - 4.505194320240652e-05, - 4.4930169634954756e-05, - 4.4808560296125436e-05, - 4.468711551839388e-05, - 4.456583563378545e-05, - 4.444472097387471e-05, - 4.432377186978452e-05, - 4.4202988652185185e-05, - 4.4082371651293287e-05, - 4.3961921196871166e-05, - 4.384163761822578e-05, - 4.372152124420782e-05, - 4.3601572403210874e-05, - 4.3481791423170454e-05, - 4.3362178631563205e-05, - 4.324273435540601e-05, - 4.312345892125483e-05, - 4.300435265520421e-05, - 4.288541588288617e-05, - 4.276664892946925e-05, - 4.264805211965776e-05, - 4.252962577769091e-05, - 4.241137022734172e-05, - 4.229328579191645e-05, - 4.217537279425332e-05, - 4.2057631556722013e-05, - 4.1940062401222605e-05, - 4.182266564918466e-05, - 4.1705441621566384e-05, - 4.158839063885384e-05, - 4.147151302105991e-05, - 4.135480908772357e-05, - 4.12382791579089e-05, - 4.1121923550204244e-05, - 4.100574258272146e-05, - 4.088973657309483e-05, - 4.077390583848033e-05, - 4.065825069555484e-05, - 4.054277146051504e-05, - 4.042746844907679e-05, - 4.031234197647412e-05, - 4.019739235745835e-05, - 4.008261990629741e-05, - 3.996802493677484e-05, - 3.985360776218878e-05, - 3.973936869535153e-05, - 3.962530804858827e-05, - 3.95114261337365e-05, - 3.9397723262145015e-05, - 3.928419974467307e-05, - 3.917085589168968e-05, - 3.9057692013072654e-05, - 3.894470841820761e-05, - 3.883190541598744e-05, - 3.871928331481127e-05, - 3.860684242258358e-05, - 3.8494583046713477e-05, - 3.838250549411384e-05, - 3.8270610071200376e-05, - 3.815889708389098e-05, - 3.8047366837604574e-05, - 3.793601963726066e-05, - 3.782485578727825e-05, - 3.771387559157504e-05, - 3.7603079353566624e-05, - 3.7492467376165736e-05, - 3.7382039961781235e-05, - 3.727179741231754e-05, - 3.716174002917352e-05, - 3.7051868113241836e-05, - 3.6942181964908154e-05, - 3.68326818840502e-05, - 3.672336817003699e-05, - 3.66142411217281e-05, - 3.650530103747265e-05, - 3.639654821510872e-05, - 3.628798295196235e-05, - 3.617960554484681e-05, - 3.60714162900618e-05, - 3.5963415483392694e-05, - 3.585560342010944e-05, - 3.574798039496619e-05, - 3.5640546702200116e-05, - 3.55333026355309e-05, - 3.542624848815973e-05, - 3.5319384552768486e-05, - 3.521271112151912e-05, - 3.510622848605278e-05, - 3.4999936937488824e-05, - 3.489383676642433e-05, - 3.4787928262933145e-05, - 3.468221171656503e-05, - 3.4576687416345054e-05, - 3.447135565077253e-05, - 3.43662167078205e-05, - 3.426127087493493e-05, - 3.415651843903357e-05, - 3.405195968650564e-05, - 3.3947594903210826e-05, - 3.3843424374478376e-05, - 3.373944838510661e-05, - 3.363566721936188e-05, - 3.353208116097791e-05, - 3.342869049315507e-05, - 3.332549549855949e-05, - 3.322249645932229e-05, - 3.311969365703899e-05, - 3.301708737276847e-05, - 3.291467788703243e-05, - 3.281246547981447e-05, - 3.27104504305594e-05, - 3.2608633018172484e-05, - 3.2507013521018675e-05, - 3.240559221692172e-05, - 3.230436938316366e-05, - 3.22033452964838e-05, - 3.210252023307819e-05, - 3.200189446859869e-05, - 3.190146827815225e-05, - 3.180124193630029e-05, - 3.170121571705783e-05, - 3.1601389893892635e-05, - 3.150176473972476e-05, - 3.1402340526925595e-05, - 3.1303117527317104e-05, - 3.120409601217118e-05, - 3.110527625220884e-05, - 3.1006658517599575e-05, - 3.090824307796054e-05, - 3.081003020235569e-05, - 3.0712020159295335e-05, - 3.0614213216735204e-05, - 3.051660964207573e-05, - 3.0419209702161324e-05, - 3.0322013663279762e-05, - 3.022502179116125e-05, - 3.0128234350977934e-05, - 3.0031651607342883e-05, - 2.9935273824309673e-05, - 2.9839101265371512e-05, - 2.9743134193460485e-05, - 2.964737287094687e-05, - 2.955181755963852e-05, - 2.9456468520779957e-05, - 2.936132601505186e-05, - 2.9266390302570178e-05, - 2.917166164288551e-05, - 2.907714029498243e-05, - 2.8982826517278678e-05, - 2.8888720567624494e-05, - 2.879482270330201e-05, - 2.8701133181024336e-05, - 2.86076522569351e-05, - 2.8514380186607563e-05, - 2.8421317225043974e-05, - 2.8328463626674938e-05, - 2.823581964535871e-05, - 2.8143385534380284e-05, - 2.805116154645106e-05, - 2.795914793370788e-05, - 2.786734494771246e-05, - 2.7775752839450655e-05, - 2.7684371859331767e-05, - 2.7593202257187918e-05, - 2.750224428227338e-05, - 2.7411498183263677e-05, - 2.732096420825524e-05, - 2.7230642604764514e-05, - 2.7140533619727284e-05, - 2.7050637499498083e-05, - 2.696095448984945e-05, - 2.687148483597132e-05, - 2.6782228782470374e-05, - 2.669318657336915e-05, - 2.660435845210569e-05, - 2.651574466153272e-05, - 2.642734544391693e-05, - 2.633916104093839e-05, - 2.625119169368993e-05, - 2.616343764267633e-05, - 2.6075899127813907e-05, - 2.59885763884295e-05, - 2.590146966326021e-05, - 2.5814579190452517e-05, - 2.5727905207561597e-05, - 2.5641447951550874e-05, - 2.555520765879116e-05, - 2.5469184565060083e-05, - 2.5383378905541523e-05, - 2.529779091482493e-05, - 2.521242082690451e-05, - 2.5127268875178898e-05, - 2.5042335292450212e-05, - 2.4957620310923695e-05, - 2.4873124162206826e-05, - 2.4788847077308847e-05, - 2.4704789286640097e-05, - 2.4620951020011424e-05, - 2.453733250663335e-05, - 2.445393397511575e-05, - 2.437075565346703e-05, - 2.428779776909353e-05, - 2.4205060548798956e-05, - 2.4122544218783662e-05, - 2.404024900464418e-05, - 2.3958175131372522e-05, - 2.3876322823355436e-05, - 2.3794692304374045e-05, - 2.371328379760311e-05, - 2.363209752561035e-05, - 2.3551133710355896e-05, - 2.347039257319179e-05, - 2.3389874334861157e-05, - 2.330957921549787e-05, - 2.322950743462562e-05, - 2.314965921115764e-05, - 2.307003476339596e-05, - 2.299063430903075e-05, - 2.2911458065139792e-05, - 2.2832506248187967e-05, - 2.2753779074026473e-05, - 2.2675276757892457e-05, - 2.2596999514408216e-05, - 2.2518947557580736e-05, - 2.2441121100801117e-05, - 2.236352035684391e-05, - 2.2286145537866558e-05, - 2.2208996855408906e-05, - 2.213207452039247e-05, - 2.205537874312e-05, - 2.1978909733274825e-05, - 2.190266769992026e-05, - 2.1826652851499143e-05, - 2.1750865395833207e-05, - 2.1675305540122366e-05, - 2.1599973490944466e-05, - 2.1524869454254394e-05, - 2.144999363538377e-05, - 2.1375346239040193e-05, - 2.130092746930678e-05, - 2.1226737529641624e-05, - 2.1152776622877234e-05, - 2.1079044951219805e-05, - 2.1005542716248975e-05, - 2.0932270118917064e-05, - 2.085922735954852e-05, - 2.0786414637839468e-05, - 2.0713832152857093e-05, - 2.0641480103039153e-05, - 2.0569358686193427e-05, - 2.049746809949704e-05, - 2.0425808539496133e-05, - 2.035438020210526e-05, - 2.028318328260672e-05, - 2.0212217975650166e-05, - 2.0141484475252074e-05, - 2.0070982974795096e-05, - 2.0000713667027687e-05, - 1.993067674406339e-05, - 1.9860872397380515e-05, - 1.9791300817821486e-05, - 1.9721962195592336e-05, - 1.9652856720262194e-05, - 1.9583984580762825e-05, - 1.951534596538799e-05, - 1.9446941061793063e-05, - 1.9378770056994478e-05, - 1.9310833137369088e-05, - 1.9243130488653877e-05, - 1.917566229594529e-05, - 1.9108428743698775e-05, - 1.904143001572834e-05, - 1.8974666295205897e-05, - 1.8908137764660935e-05, - 1.884184460597994e-05, - 1.8775787000405825e-05, - 1.8709965128537588e-05, - 1.864437917032973e-05, - 1.8579029305091746e-05, - 1.8513915711487652e-05, - 1.8449038567535537e-05, - 1.838439805060702e-05, - 1.8319994337426875e-05, - 1.8255827604072303e-05, - 1.8191898025972746e-05, - 1.812820577790924e-05, - 1.8064751034013946e-05, - 1.800153396776969e-05, - 1.793855475200957e-05, - 1.7875813558916298e-05, - 1.7813310560021967e-05, - 1.7751045926207302e-05, - 1.76890198277015e-05, - 1.7627232434081526e-05, - 1.756568391427177e-05, - 1.75043744365435e-05, - 1.7443304168514532e-05, - 1.73824732771486e-05, - 1.7321881928755075e-05, - 1.7261530288988383e-05, - 1.720141852284757e-05, - 1.7141546794675948e-05, - 1.7081915268160542e-05, - 1.7022524106331624e-05, - 1.6963373471562405e-05, - 1.6904463525568425e-05, - 1.684579442940726e-05, - 1.6787366343477974e-05, - 1.6729179427520695e-05, - 1.6671233840616247e-05, - 1.6613529741185683e-05, - 1.6556067286989747e-05, - 1.649884663512863e-05, - 1.644186794204141e-05, - 1.638513136350566e-05, - 1.6328637054637013e-05, - 1.6272385169888727e-05, - 1.6216375863051337e-05, - 1.6160609287252176e-05, - 1.6105085594954873e-05, - 1.60498049379591e-05, - 1.5994767467400096e-05, - 1.593997333374817e-05, - 1.5885422686808397e-05, - 1.5831115675720174e-05, - 1.5777052448956777e-05, - 1.572323315432503e-05, - 1.5669657938964807e-05, - 1.56163269493487e-05, - 1.5563240331281616e-05, - 1.551039822990034e-05, - 1.545780078967313e-05, - 1.5405448154399413e-05, - 1.535334046720928e-05, - 1.5301477870563166e-05, - 1.5249860506251423e-05, - 1.5198488515393948e-05, - 1.5147362038439817e-05, - 1.5096481215166858e-05, - 1.5045846184681296e-05, - 1.4995457085417375e-05, - 1.4945314055136963e-05, - 1.489541723092921e-05, - 1.4845766749210105e-05, - 1.4796362745722175e-05, - 1.4747205355534067e-05, - 1.4698294713040261e-05, - 1.4649630951960514e-05, - 1.4601214205339733e-05, - 1.4553044605547445e-05, - 1.4505122284277506e-05, - 1.445744737254769e-05, - 1.4410020000699399e-05, - 1.4362840298397263e-05, - 1.431590839462879e-05, - 1.4269224417703989e-05, - 1.4222788495255078e-05, - 1.4176600754236111e-05, - 1.4130661320922593e-05, - 1.4084970320911206e-05, - 1.4039527879119367e-05, - 1.3994334119784972e-05, - 1.3949389166466085e-05, - 1.3904693142040434e-05, - 1.3860246168705272e-05, - 1.3816048367976926e-05, - 1.3772099860690487e-05, - 1.3728400766999503e-05, - 1.3684951206375628e-05, - 1.3641751297608287e-05, - 1.3598801158804419e-05, - 1.3556100907388011e-05, - 1.3513650660099939e-05, - 1.3471450532997587e-05, - 1.3429500641454454e-05, - 1.338780110015997e-05, - 1.3346352023119073e-05, - 1.3305153523651944e-05, - 1.3264205714393723e-05, - 1.3223508707294186e-05, - 1.3183062613617335e-05, - 1.3142867543941295e-05, - 1.3102923608157822e-05, - 1.3063230915472133e-05, - 1.302378957440252e-05, - 1.2984599692780111e-05, - 1.2945661377748526e-05, - 1.2906974735763676e-05, - 1.2868539872593322e-05, - 1.283035689331692e-05, - 1.2792425902325284e-05, - 1.2754747003320302e-05, - 1.2717320299314639e-05, - 1.2680145892631462e-05, - 1.2643223884904177e-05, - 1.2606554377076191e-05, - 1.2570137469400482e-05, - 1.2533973261439498e-05, - 1.249806185206482e-05, - 1.2462403339456867e-05, - 1.242699782110464e-05, - 1.239184539380548e-05, - 1.2356946153664776e-05, - 1.232230019609573e-05, - 1.2287907615819052e-05, - 1.2253768506862737e-05, - 1.2219882962561836e-05, - 1.2186251075558115e-05, - 1.215287293779987e-05, - 1.2119748640541646e-05, - 1.208687827434402e-05, - 1.2054261929073317e-05, - 1.2021899693901367e-05, - 1.19897916573053e-05, - 1.1957937907067259e-05, - 1.1926338530274176e-05, - 1.1894993613317536e-05, - 1.1863903241893152e-05, - 1.1833067501000918e-05, - 1.1802486474944566e-05, - 1.1772160247331445e-05, - 1.1742088901072316e-05, - 1.1712272518381085e-05, - 1.168271118077462e-05, - 1.1653404969072462e-05, - 1.1624353963396685e-05, - 1.159555824317164e-05, - 1.1567017887123709e-05, - 1.1538732973281154e-05, - 1.1510703578973834e-05, - 1.1482929780833054e-05, - 1.1455411654791319e-05, - 1.1428149276082123e-05, - 1.1401142719239768e-05, - 1.1374392058099152e-05, - 1.1347897365795578e-05, - 1.1321658714764514e-05, - 1.1295676176741409e-05, - 1.126994982276155e-05, - 1.1244479723159811e-05, - 1.1219265947570456e-05, - 1.1194308564926982e-05, - 1.116960764346195e-05, - 1.1145163250706704e-05, - 1.1120975453491292e-05, - 1.1097044317944236e-05, - 1.1073369909492333e-05, - 1.104995229286053e-05, - 1.102679153207167e-05, - 1.1003887690446406e-05, - 1.0981240830602968e-05, - 1.0958851014457006e-05, - 1.0936718303221404e-05, - 1.0914842757406183e-05, - 1.089322443681821e-05, - 1.0871863400561175e-05, - 1.085075970703534e-05, - 1.082991341393738e-05, - 1.0809324578260273e-05, - 1.0788993256293097e-05, - 1.0768919503620928e-05, - 1.0749103375124634e-05, - 1.0729544924980744e-05, - 1.0710244206661316e-05, - 1.0691201272933795e-05, - 1.0672416175860808e-05, - 1.0653888966800111e-05, - 1.0635619696404393e-05, - 1.0617608414621115e-05, - 1.0599855170692465e-05, - 1.0582360013155102e-05, - 1.0565122989840136e-05, - 1.0548144147872937e-05, - 1.0531423533672998e-05, - 1.0514961192953843e-05, - 1.0498757170722896e-05, - 1.0482811511281315e-05, - 1.0467124258223927e-05, - 1.0451695454439093e-05, - 1.0436525142108568e-05, - 1.0421613362707412e-05, - 1.0406960157003853e-05, - 1.0392565565059195e-05, - 1.037842962622771e-05, - 1.036455237915651e-05, - 1.0350933861785459e-05, - 1.0337574111347066e-05, - 1.0324473164366373e-05, - 1.0311631056660878e-05, - 1.02990478233404e-05, - 1.0286723498807036e-05, - 1.0274658116755e-05, - 1.0262851710170606e-05, - 1.02513043113321e-05, - 1.0240015951809633e-05, - 1.0228986662465152e-05, - 1.021821647345233e-05, - 1.0207705414216424e-05, - 1.0197453513494281e-05, - 1.0187460799314207e-05, - 1.0177727298995905e-05, - 1.016825303915038e-05, - 1.015903804567991e-05, - 1.0150082343777922e-05, - 1.0141385957928984e-05, - 1.0132948911908665e-05, - 1.0124771228783522e-05, - 1.0116852930911042e-05, - 1.0109194039939557e-05, - 1.0101794576808155e-05, - 1.0094654561746707e-05, - 1.0087774014275745e-05, - 1.0081152953206404e-05, - 1.0074791396640419e-05, - 1.0068689361970052e-05, - 1.006284686587803e-05, - 1.0057263924337509e-05, - 1.0051940552612027e-05, - 1.0046876765255499e-05, - 1.0042072576112127e-05, - 1.0037527998316364e-05, - 1.0033243044292918e-05, - 1.0029217725756705e-05, - 1.0025452053712766e-05, - 1.0021946038456325e-05, - 1.0018699689572687e-05, - 1.0015713015937242e-05, - 1.0012986025715443e-05, - 1.0010518726362785e-05, - 1.0008311124624773e-05, - 1.0006363226536888e-05, - 1.0004675037424615e-05, - 1.0003246561903414e-05, - 1.0002077803878662e-05, - 1.0001168766545706e-05, - 1.0000519452389823e-05, - 1.0000129863186222e-05, - 1e-05 - ] -} diff --git a/tests/fixtures/legacy_pipeline/aria-cartesian-camframe.json b/tests/fixtures/legacy_pipeline/aria-cartesian-camframe.json deleted file mode 100644 index 75990205f8..0000000000 --- a/tests/fixtures/legacy_pipeline/aria-cartesian-camframe.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "source_commit": "ec5c903c067bf1b29bbff781bc414c6df2bcf1f2", - "vendor": "aria", - "representation": "cartesian", - "frame": "camframe", - "identity": 3, - "indices": [ - 0, - 7, - 29, - 47, - 50 - ], - "model_keys": [ - "actions_cartesian", - "observations.images.front_img_1", - "observations.state.ee_pose" - ], - "api": "legacy", - "transform_arguments": { - "mode": "cartesian", - "stride": 3 - }, - "normalization_frames": 54, - "archive_sha256": "7aaf6dc3c82a95a2c844e76918334ad2762dd0e0ced038fe117baf928b5cfde6" -} \ No newline at end of file diff --git a/tests/fixtures/legacy_pipeline/aria-cartesian-camframe.npz b/tests/fixtures/legacy_pipeline/aria-cartesian-camframe.npz deleted file mode 100644 index 0529be4357c39392b696a4afe21014aa9a38b41b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38565 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 diff --git a/tests/fixtures/legacy_pipeline/aria-cartesian-eef_frame.json b/tests/fixtures/legacy_pipeline/aria-cartesian-eef_frame.json deleted file mode 100644 index 6cac58d2a1..0000000000 --- a/tests/fixtures/legacy_pipeline/aria-cartesian-eef_frame.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "source_commit": "ec5c903c067bf1b29bbff781bc414c6df2bcf1f2", - "vendor": "aria", - "representation": "cartesian", - "frame": "eef_frame", - "identity": 3, - "indices": [ - 0, - 7, - 29, - 47, - 50 - ], - "model_keys": [ - "actions_cartesian", - "observations.images.front_img_1", - "observations.state.ee_pose" - ], - "api": "legacy", - "transform_arguments": { - "mode": "cartesian_wristframe_ypr", - "stride": 3 - }, - "normalization_frames": 54, - "archive_sha256": "d456f93c3dec0d8eac559f46a5f56cabc0c42f813d2e4feac78fbf34d92fe65c" -} \ No newline at end of file diff --git a/tests/fixtures/legacy_pipeline/aria-cartesian-eef_frame.npz b/tests/fixtures/legacy_pipeline/aria-cartesian-eef_frame.npz deleted file mode 100644 index 0e34574bf1b4628029cc34392176a93591c43884..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37688 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 I!18bZ2RXEHumAu6 diff --git a/tests/fixtures/legacy_pipeline/aria-keypoints-camframe.json b/tests/fixtures/legacy_pipeline/aria-keypoints-camframe.json deleted file mode 100644 index 5f43a2dfa3..0000000000 --- a/tests/fixtures/legacy_pipeline/aria-keypoints-camframe.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "source_commit": "ec5c903c067bf1b29bbff781bc414c6df2bcf1f2", - "vendor": "aria", - "representation": "keypoints", - "frame": "camframe", - "identity": 3, - "indices": [ - 0, - 7, - 29, - 47, - 50 - ], - "model_keys": [ - "actions_keypoints", - "observations.images.front_img_1", - "observations.state.keypoints" - ], - "api": "legacy", - "transform_arguments": { - "mode": "keypoints_headframe_ypr", - "stride": 3 - }, - "normalization_frames": 54, - "archive_sha256": "4df87e74e7b8e88aa69fa4f41ca3325ca65c0c2a4a0e8b6dc0ee987c2160088f" -} \ No newline at end of file diff --git a/tests/fixtures/legacy_pipeline/aria-keypoints-camframe.npz b/tests/fixtures/legacy_pipeline/aria-keypoints-camframe.npz deleted file mode 100644 index 08607bb708171e57502eae00d929c14406e6f649..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 71866 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%0DXTC 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 K*&koK_J080bC)>) diff --git a/tests/fixtures/legacy_pipeline/aria-keypoints-eef_frame.json b/tests/fixtures/legacy_pipeline/aria-keypoints-eef_frame.json deleted file mode 100644 index 5ce0ccb599..0000000000 --- a/tests/fixtures/legacy_pipeline/aria-keypoints-eef_frame.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "source_commit": "ec5c903c067bf1b29bbff781bc414c6df2bcf1f2", - "vendor": "aria", - "representation": "keypoints", - "frame": "eef_frame", - "identity": 3, - "indices": [ - 0, - 7, - 29, - 47, - 50 - ], - "model_keys": [ - "actions_keypoints", - "observations.images.front_img_1", - "observations.state.keypoints" - ], - "api": "legacy", - "transform_arguments": { - "mode": "keypoints_wristframe_ypr", - "stride": 3 - }, - "normalization_frames": 54, - "archive_sha256": "fac7145706aa87b6e20b008833488d659c82b0ba70c9d9eed9aee29ff91f2894" -} \ No newline at end of file diff --git a/tests/fixtures/legacy_pipeline/aria-keypoints-eef_frame.npz b/tests/fixtures/legacy_pipeline/aria-keypoints-eef_frame.npz deleted file mode 100644 index b834ed1c8762dac3e27bcc49313d0e6a16512b7c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 71997 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 diff --git a/tests/fixtures/legacy_pipeline/eva-cartesian-camframe.json b/tests/fixtures/legacy_pipeline/eva-cartesian-camframe.json deleted file mode 100644 index 3b11468aa6..0000000000 --- a/tests/fixtures/legacy_pipeline/eva-cartesian-camframe.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "source_commit": "ec5c903c067bf1b29bbff781bc414c6df2bcf1f2", - "vendor": "eva", - "representation": "cartesian", - "frame": "camframe", - "identity": 6, - "indices": [ - 0, - 7, - 29, - 47, - 50 - ], - "model_keys": [ - "actions_cartesian", - "observations.images.front_img_1", - "observations.images.left_wrist_img", - "observations.images.right_wrist_img", - "observations.state.ee_pose" - ], - "api": "legacy", - "transform_arguments": { - "mode": "cartesian" - }, - "normalization_frames": 54, - "archive_sha256": "a4908f0de8cae703a2b5281f9da4e8ae61f30a0817c874000c8c7c056899ecac" -} \ No newline at end of file diff --git a/tests/fixtures/legacy_pipeline/eva-cartesian-camframe.npz b/tests/fixtures/legacy_pipeline/eva-cartesian-camframe.npz deleted file mode 100644 index a1e4d5b0799a177593742082426d5a233bc16a0b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 93192 zcmd41S5y;N^e(QVqS8iD0V&ZzVHBheoh0L+GD=fXij+`|N~oceq9Pz&M0$yiBZzd6 zUP22+AW=a=Xn_Pm3xp&P(jeFM|K0oWf4h(Otaa8tFXyav_W91<-`e}TVVX3X zj{TY!4rm*loD==;p>W{zfe_CKZBOseCxHQQh_`1*s4x79XMk2fP}IQ#za98%`q!Rc z!{pHa*p1N+tG5Ra9NiiNz5e}=j8lI{y0rc-jysew|MT@?*Y)Ec&RLJKO^>CH9ZDXo z?75ox=Gf@JD`O!^@$R29k0mMDh<*Hc=ww0C-y{1+h$zBV7h4m%-`X#>V;SNF^;)L* z4^>TAxBvc%;jf)8C(h*Hts_c4Qk|eBS9uIFIiyabMHo@^*d)t!&Wv+f{Ntrd!woUe z;2@CGwF%_(x2D?YKNPdZOB&T1XHKkJnz^7qGAzNgvwooMw3nlHoKeY9nFJFCO^)@V zbETU(4)YJuO7Xp7DECO#)7kEI)zfI@qxugbN;0W;1BCa!k1r?Rtw2y7{adP5*nX#V z<8g1tW5Ye+?$!YB^#GL_X|9s~|C-TYA3c#zG#^3BK2RCi&-05sY2b)G=lW0T%O;7c zfUlQZ6&(yKH{A1ERBRBld`^E>noi8h6s!WK=i1>%q5mbS|D$?gcqy)o`6;6H&){g* zU8knEHt4U)Qr=}b-rnoty*)F+2gwZa52aukGneX&i@aCkN4w;AnNwza=Kc6Q_KzqstRSIHBY8L8NRuq;H_{UAegL@bZ>a*D_vd?dh9e5izwEuA4us6HRMCNgbw*5gRprk$ zRrD65QN5E%fMIucsv#)GLXIXjWOIe%59Y}4Dao&^pGGUDL*_OG6-SLfC5gS171UK8_=uvwsJ9pDcX6hL^1 z(`3}jjcKy5D+h*Nj|I4*){6bYk?k-6Ij}Q|2ZpA@Iv4sC<@`JLV?dQt+pgY%76GOM z!C1%CA$PU+3f9U|nY|GmoLk8t^yo04ey@lK z*6uE>>De}Ua#P3J4_qX9s{`#V$Y*fNr|ZFNLL)=8gKpa^k(CQX?LQYki^i^s@8dUH z0uB0Scd9>!M zV$oz?zFk>obUX$UA>`7~WP!Euq;O_$S36>V<-w(1NDQOlC%M|x=_lLsl{sF>4Hu*u z0Z|d7JhwKe5q7(3S!%|OkT2JB=7py#(&r4)F( z`nyOx#{$a+G_`>OBambJoKo_>U89-VWY=LO)Ve?BK8F<>7;QhqdyWyz@Fk_utj*Fu z?0&prxc_YZg&Y8Oy{hP{Tl-JE5tfy;1LK}27R~I<^sL07mXc2p+dAK(w$Tw}*3~9+ z&-I5KFM3VsRlX+}95RsgQw}NMiGu=n=>wav?U2dIu9q=jzNIKEWgRSeg3Ww~~rIXhPoBKz^=Mw{4ma=cS=)l+7FzuHt{F}3_dlZ%pv0Qw_ z+3K3OB3nZXqZs*n%;Y!`f>_305xh`9K2s$8Ov8H40Qt{VT^Vpa?vx|q@1eAr72XDT zCp}pXE$W3-r7JdeRc)n1Hx`@c#as_f`%t6X!+ME(LlnhU+-08uci?UZmXC{*G9Ab> zE6{A&gneqx(VRb>@IxaluA7bB`{-K^-s3doz=AKS@VbqVe9jzZzjOyiWlN%Zf&BMi zo!e@Wg{U1|ZbYj~TXuR7STH-=6fT7y7a5_*A<0y2%ZSdkfv`L3`>0^CHvT5DBS2TJ@esSlS84H>U1%SQ^p-O? z24#vk`kcJs`AVm5E#Q zB{*^_*qb@E$`DO2LASaBQS)LF1!|V^!C-~1bJANziau7TWxp*+u!IpkrE=cHzf}g4 z(Xrn$u*+YLMjU7&@2@#BI>9$FE=nC#9L1B`Yq{CMD-gNv_33Z#JtuP`K0*uMOEt1- z0RG5Tt)+63D)>NKXj!E)T5EB=vuK3*G*|=KE#sPKHlI4;8He?*--cEQr9}0;Hauw} z1&NAQw=l#O3YN4r&2TXk85XN$eX>S(MLVFYeDUCit;4ePW8!P77+dvTeb88ZOdptE zBIBc0!e_Q+`+Jp^PUMozU6XDN-hr$<1LG=nfV!tPJt00;B6>I513C4LKo)H!Sf#;I zl=rp@Fb;;vc1UT*(M&~uR+CP*t$Bo;_FK$F3j}UAnLPL!jMJ~d(}F%O*P881fPy#x zAnPBJ_+H(6H_1E9vzU)Ekt)#nt4c|1Qj+0L6fwY!pvC=TW8S*<%>psQJ?gF1b@ITU+ z)GI6_hu~^?m1=HLQM4iX;D=C+aBntU@@DXPWRJF)cU#TSqg2PXnndzY0{!Ul!s}R; z8NW00AW}~BS_(3>OhHA$!On?^$n;p;g`l#ls3T}Ud-{B7b1{7(wqnHIBSR`vQTg1u zZLuHuQh;tmOU=o^HAVOTEU4#Qh>r{WYtJa1$3I%%wxjW-#Uf?0am~#SXO>Z%V}6Mr z@Q&u%RnMPb5^pe7HD8U9E@|~JngPW-81q>R)UT*bd@I^$xjl61W>6AiFdbd0=wx`- z^#%BgYT6Q*Wb9&y_CWKKRdMQzeMeUmN-3xIclB z9!h{bQD}RsZh-D5f@iv|O6t$R$`{8+ueu=Y@Xo1<^$4M`<+yWVR$qMQa3r|JOM~6tw`2Dk=Kjn}EZ4D9 z3>4JZI9lzIBt(>;gVYSW4#NJy3`#f*VN!cF=6ZH&tN}tflq#H~HgTr+TN`1aH+Qes zncrVRl8o_!`&d**Itvg+uDvT|``We=p z(40?KF;-3QBS4M#{rjYg(Y`>??1xc|&~}dg zOuLI+tU=pW%8fh1JR$Ie!ZNgi4!DE>-PRHayuXlC?a89$o-Fg{&C12Xm&e0j%6+ku z)+wou{0jDd0o3!g0=fJ|i!aG|o-|6T|KceuPBvP3jcm;!e3WS>LYmJu)tKoG7V>>@ zITVT95)0jvZmT(yT|k(%2=_PrgbGk`E}j^4CXW?mEau*Ko^lT{IwNYxDRf(M}F^>e#3jlb~0cl9l}fw=blGp zrg6297gU?iXeh`i)EwDYOy?C7xJwb(HscjHEVp_XHUAA{e%n9BYisw#m>Y z$C8lBuYoOphPqwzcGm^d@1mk-*ljpzxRTdKBR8p6)W zTZ%e=gSmfUW~+-vWTmTm61c-*l*=>Ppr3&7XI8xHN}lviqSoFMF=%~Ms~*bxtVX8U z4EtJtZ0z76^h&vtGKYBp`?a(UBRT)?@)JeFUZcd1ec8qEgpVkq(!+Oa zgvQ%>IK=q;<_`K48CMjeg-Xy3UsU0oK-0w;y{9l~gcSwQP`Y{MSVyy$eWMkjKc3C= z_wN!Hvg@}ga8QyPRo8Sv-07VnW~|!D(mbn~_0N(-&8Q%Qk=X(=in_&Cd2fTNhqty@&4v1xtb{CcWuL? znN^-*kw1__2k*D}V)wgcU|VI8e`ALdWquY%K4EFnf|?JJDQ@!K)k4FzfBgs-EHG5V zT1Q%jqvOcI;jI8zO^=%AbJvyE)!VSWQvDEhi`{UQsH+YO5~Tmaen#5qfX>81Df;Hn zLhe5R?mxT9mvD+gYdFI5uK6bbH`jVKh;?r3Ie8}|RUI82hU-8mpx&wTf1nM!43`vB z<7}eU+)Dsto}|_)Mr(zvlN|nn&Ku;)zKxz3Ujd)TUON zv^d+_N>kas9KR@9$+xpHk6{|Zzsotox|dfa=GUNNS|kM zdAir($UlkC?sncyAy`tSy_pFA`jdMe!?2sMQe>@S3iV#dmI87?*%WHGrR;(Yd?KKD(+9$<2)S*JwS76fgc7n_^U@$s_1T}=cmge1dTD0?cR*aAq z3%$6n?=Jr*tsUE3g}^k6YwcK~7Y<~TDhO)i^ZUKGw#u{-CpO2_F-$VWv3qP$TW!ZJ4`wsY=J8m*eym~mk z&wyssZvmlQ;+xhzyAU#<56iBU*RP88MtzIu6{F|5ZYwg9oVkxp)f78pHO)ZoY|K=C zROWu&Rid?YwhGReKLS~hn9M5u-I97qo2iKOYa>hDM}2kLs%+$)4AIfjD~$+Ga{00x z9d6U_gJYbNAs-H;{Z2V!?yt$cpYoErnt?qg=l)4P8?pNL%t1!T@fv*7g54gc^Dlfi z6h@FAv@4kMj?jq{m}6}0%>N0`jMaY>950m8TE?y%E9OPB`s`}_D>?_{xDQe`cvWmA zDwHtgP3Cv+UW9)693@%Nooakbb2$){UOZp9+%eoP)^p1X+WBhq`(?L&@?b|*Bir^Z z4ELn@K}9S(b<9a?tRf}#DNC)50nUytX(~nb4y*AxnBcR>cS)`?i^WuqYm<98{5Xv1Y12? zDHxmH)3Y@fkQ0#b-__!sH+XY1<4Tq>L8@pmnf@%qfW5|}LX#@vbNgQmOd^=%Eop03 zIV=hICeHW>XXnN=LcL>6G!Sygx!Vkpq@Oa-yb=_mS$tfA5)kqN5Z(jkKhdOF&}w9% z=d`2bpL2$?s{03Ehs49ovHe3eUjx|@2x(YvJ6cFvyh=kJRC<%i>^TI{$mu>isc)Vb z7<93PTLAHD>u`8vBLlN;^SYM%-LSph4D}*lHKjeXpn69cri0==c71qJYriz6!bDG!;Yxi{!5 z?oOws^JN$QG(RtWt8>-)l9fNZf&Wsl8fTt*!`#|%>OZh|Z`o9aQT*kZ6!D!>_23T5um-tk8OZ)Elo4Ze&*nQ>lcseQL#S(WO$QU4;etY0#;s)XwMG{H)N_F^(O zWuBpYX85(f$_qvEZcGD9kpg6vN5SUc#aDs+A<;_aE!1nRxQ9Kx!YWsM7RSF(L0kB^t(AMec^ zJ8dy95sha@&Lsbm;C3`qL$Oz)F<%KSvmKW@ObVY zA>JDOczm71YS5BK-*5aI+VohJc=T$vf4#QS%duIr^_;OW#j_KCMPeZMF87-jtKG%o^QorGpE~| zS>iV;&<>|G#14Go zb{*2>2q>vK3?uMNs2&jW(cat$bGEAf_{U7Ll}Fd^_9$mWeM@EYxCnHIpWqzrQGj^A zPnbHR1ZLREg4>&5DgH@pXwmx^-04j@qxl<79vF%hAs!fhJ5YGIg9X*eGN!%ry3f{j zIyp9UkUH4?$EI$;Zqkzh5epcVPg#4G#`9VUHOrq%LVz}3#>xuktn<&=fsvT<#wPkbjTKLgJL`L zTzo4}Xe{|dqJsKYq0fl%Aw1~}>`h|x0n7@ZfFiySxE)X!`HNp2?CjljZbLeg8bRMG zq13pF`qmOkNb4Cv(gmOe>=1NYp8=NDAhtCmt@bHSsVYYQgoabkVX4uq{`LNs#J7^_3)g$59USW}23HLnzbd&KIGW?;`ZRfw56M_C%jC6C0WXN^XKY+w3CZy3OF zZj_s@K%UtyRg;S6uUQyC`ma$mqPNY=uQ|f!yAkNEQ zu4rKl#u3ufVakO9ThXK01{4*zFGD=nBrmBl1X49$M*m}=B&}f@Kr&75tbkfTQk8%ysNwcI1*anze#k+sw9yBZ69Jvei z{|wr)qyN@8G-te2Is|YJYU8P)@(i${B{I#&Iw!AStw1zm5VdsWGCPMj(SB<>vUgYb zM;g=2gr`zGe@yF?Ow|>2@tDY5tg5*8X=tJ^=}*85`z?v-7iQ*mT2T|L`RWU<-V#Km z%cLKwE(v${z6G-XVo%X8ML*@8W_d+WPPE2ZAiDwE$Ix4ADDFZN>6~+>9I)2FiQ=;Q z!-+(%=vZ`F1!()&?C0hCjbV-Bl(&eXg!mNYsy+$+1!+Uemk~}cE)>2R+a?PmiY=Qc z@_T(xV6zDEfJfEOu%wZSp)PRfbI9l!(n;PMtdhR)TNCt=6y>`wYkd1(21NxmRhU~j z6dOtV;HO&b9=Tx>2*ecQ-VI#_wKczV-H8Qxx`6O|I=Hi6%ov=Y(%^!au0xO#a2~bu zW52G+pOHlA*^E5D$wbiilXJA)8Sg^<CQ;yxqvEqE!&4^9I|0u>!LQM$dGONBeuij9(X?y7GU>w1V5&NA~ABUba zoQr=jxdjWMN(^B-+T4X7(No00Jsl7x=4N$M)sE)YfVz)Xa7D9>=-UNyUSaQdN>uw_ zkBW!w;a zGdQ$TwLqfD-4Xs~D|pW2+d<8Gf9mP6#Po%f^z#uLnc5I32!BwH;=vzifkkn$~@5P}hb@R&;^x{AjvT2}RwV@AFn{c^<7%R)*0>~}}hy%{~fa;`l% z_zPz1g{6n%bcl>IIs^w&d zLVMK{mHQm~uei%fD9BXgW0!zzW^~SXhShD6j&(MFKy9nRkaw%rpyTWAEAZf%PJK*U zCoB~+zuvnfZ%3(d1nW?s{aL&b#$GDSWZ_1uxduclYx&&k*6 z^wV{t>{g{3fsU{b06Ki)0}I~&rXx4~Dj+8elU}KGO4zH|xoE^g?nWII^r?pSC;Yh4 z9NfS{aF^0kFSM}RhhOUcswBTtj1I->zEWgL(-?0hBg0I$+&DN3nEf;g5 zP~crUj}mXECKH}Db!2&B`^A7qm(rPHT*3m$#fW6t{IK_G=e!2*z+P~rQy6D%D;8$m zZfU6lt-@n6y_Y97q?Z!vviGb<^M+2_HCuv7q`oxzRPOv6@C2CfK|A-73@Y80dvI5t zU2#_xP`9J7_gD>L8YRdLKl;@vPn&>C^B;ez4}G&sBj5E zzkbUDVMfH~Ce0{IPT84ZKMTyNSoQZ7?g>CWxNw-WpeK-lID?et9tECP?7qw&1pf-i zmgfC-(X{zwBoho%;4EBb=o0P+T#Xu*T_ST85q6JsVu>rim3--i52oAgH$1WG;~iH;spR7*_-8wNP( zK_`q@xBWBd@PrGI-yEjdjl{R72V%5r6Vy3FogPk-eR7FWsRf9UgB~`35sQA&w-(UC z-!tluR&r}Yvm-{-pMnknDX;GUWuo+_4-PXEJL3#RSPZ{8DpGOs)E9wo_B@pc6CDST;D`I$T=&wpfCKG z-aKf~k^W&V``g%f)KKSWvJo}ML+0lEnMZYmdHPZ5Ra?kV-hQwq=K3Srqk63*bZ8~0 zTfg8PoR2?8_<7EDK4G(dpAvgUrrXC@FsQ~Pw^HIss@L#~gIW!V%UsL4AC96))Ao74 z+IN1;)ZEJ^ozohSzZ|x2Nwc+1bnac0ams7hjk1J4j!jQVw&~I+eyQKM=}zbx;9E_w zi(t@&#(;l53`c72+VI0$U${Px`XoLvs6n`@K(p3hqAjKMCx))V^@G zJr45-C~jh1<$w;V`<2jRf7PuG9qRwH&8D#@0WA)31~F zv~I?V!_68m+gW3IyPU7$89?(_o+;gIXy=7T8RVd9ULIQL8^X(K`vdLc{z`H6PWvdH zy?=>O^Tjf~->Q+T47owx)vBM!LHeZ6eZ-Nr(wq7^7Oi*dI~_3|+0uF#V`sltL2eTc z9sg1Qu6Z5JcApw-`LD*z2PT6(CZ<%Tt*egO)gNia04%8fmXj&Ly*NA>!X<4Q9d3vH930YmBfmA4Ec-TVND#ii?Z*lVGQd5%UR&a=3PN-Wf ze8<*@^;D-g;wz+6S#(fbsO8=zqMh%Zo%%O0QeV1|V98Ra+$_8Xs<({fC{vXRbEfRd zHx?CF#Fk&@aXf55J&3=Hj}=E(`UKZ8NS}XkOMR_Iiz@mx5!bJE@ZmSdO`__Xp$eN= z$Yl3&|4Lrar*sfhY^p!Fc%45_oBb$Ld)W2OYDInTUYR0i|1114K}ei@q-;>}Xm6>7 z0zgvOlLjvF5c4;}kusZ!Rcu-4pA8phhK*LznR)5wEay&w?nj-5Uqk+I-19Nw7Q3ri z+KGHD#aX8oCw4NDw4sdb&h=!JU@x+U(%dpT^Zu(#16WI*i87 zr4>`&H^SwSs5B78x$y0dhxjEi(}YXpg|xJTC$wh=rsU$#^MA%57_)HWy^Mo}f?GMw zan+JHZ69dG&6rO7Avz+*|0+K6(7Jt~vrmhj?$(O&_e~X$Y$kw1_P{E-d{s&Yo%=T` zYwi&y^r=%nA?b+P03M%W6o#tMZEI(pV*PS!68c?El#C<*@h2L@I6%4?f`)!BNj7;8 zwN1?@=^yrX^Q)lW(6p9|yy2R|rTPHvXO4ZK`+0eBy#dYl3op_J#QFW5L5MjqK%T(^ z-zJ3NZ0Q`$!T?~jkB^!VJlr6hYpODG+|5)){}inGp)NG$^KaCgFaBK?^>ocD4B1(B z(4OH&5v#X-{Pfzqd#!#Hf5co$Yg=q?&=vhg&i#Q(Cga)nA+s9gPDRU}EKsff%wGUH zKy9Th_#rEHU?d0sz&!yI4wy;0tA>vMFh5Xu+I>YTZSs@utAzTa!VxvbVNh8}ZEHTi6l z_9yn360K7v6q(?2n%hbUZ2yVQ$x{_W>s)s;uPel%M?xtbNpjC{=UG^`KW~ zTV*qPz}4Q0AprVvNwsb#eD(j$b^7+qDU<>KSL zon+*R;uv<>5LGlqYFax+DJ*^d=o~3!y*`{{k`-vP_vzlic1J!5zaRL{8>_K(UM@EP=qO4pHJQ9HN6(pft&byX^B`9JX9dq@8odwH)|pC`5> z%L+!IOBU?w)ZfvKFI)y}Nv9D}`0?5Q#N^{nwj2c&icUx5LZ}kC&t+RoHt?uA_klZ| ze>ULf8VMiyZDjHBc7pLE?6(Symk-z|CyoiwP71((4{d&NhH6B838UdcModt3>r<~;y zWfrebnwsFkrN0_ZGRNh89BY>N63Lf^(ZIy8d+}0SM0jswJqOzG zuazDQ?GAq!jcI7+!)&eGb*>fHLy`&?TH#u88hXC$5X)xs288W)+dh2Mvka)Y>0uda z5c6k@oBz-s2Dr3r_l8RorwFfPgG7tlTS`1Ob2~Txk)?)F&Ymy}Ud|ki4)I^hiLsXu zF1`tC*VZIMtE`EkX^heO6kk@*_6Z2<5X{zvd*tk7tZ}0G7x2#g!6L{SF!zjhlKpm0 z;BC<6{1x~q?l;c3hJ4#IJ3@Mz^wYGyvp{`=^8t`LFk^~^wI8^%|7vu3Z+5}mFZfo6 zRYJr@O-!AhVeN5{x}VZwAn2IG4493^u9tIK-Jwe-Mq5@6tJ_t*N3zuXya_+u?U{<2 z-LrJCBYFlr!XL8i_b7B8Mt+*Bw6?zR=#m`2 zG|*UGLO#rf=v>i$W?IjPQxv7Dz|PvPQr3OA_M!8gmwBrXwBK?f@;{>6c=V_{p|fb` zPoeunt9whRbO~`0Tx$GW`L#)#9%ye%S#(1Sad&xG1Mr_6Ol*z^^iJ?(9>M$-SoNJa zCx-O_Y=~!%zv!%+@?56BRvy~xzG~i#=YEmEB98O63bz0C2U*M<7zq95=iT_LF9Q}F z$Nf3gm%;6Ydlt}x`skJMwRG>>NGDS(sil}<%xJWtt9Ga1*L64hYxqaqW@=~B7B*&o?@ z^l2sJo3x;t*Af2_am=g>Dtyj-a~!Yc!mM0ondlScmcPnAEs$I_S|5Ql`Y;4#Nq2Yr zIQO?>1}PWWKcdh!fbHBcc={|c)vhoIGUT`RZeGcFC}wYDVaEo*OBm;ZTCF#r``Qff zJ#m#Z?yc}{m7(FVDX(SNNypICtTlCz4py#rB*uwj5XQz?(^`i*fK|nbf}LLYsZtf! zB@?fsk+}vMq0+-%7ZH64iB?EkRH_tvD`_KGaWA=urw(#ys}D!5IWs#*!*Kgi9#_lX z*N!?AouD6f+vA(OY>djd`R{_m7(MD#=+y>F4t=+^!s}w|Q2zGMhXwsrfyEMx=Gak+ ztZpkOrjiAMte7^=FpQp7y#_)^)wvDi@-rP>d6P|K3kUR|-ufrU2lZ0bdBf>*JAO|o zQjB_gy@6Q%LBh6Q0IjrLm24s6s^Axn4n!K_?W9B_u`4g;XhnY!8z+~q;VAU4`pjVO zW<+^mvEPa&qS(ZC;+^aC4l6)w^Y?Z7!G~8IXuB&p*!-RA6b+Muy1&_`@0A>t=7mbu zjyTJ4zlPN(NO0-_F%56b9z~RyY&BC7s(w~A{)GhjIk*lOty0^Ko{PEaH@HKqf%_5@ z?&C2g_poB6PXILLk3K1y@6>5AY6bl#;>lK)= zcyG@v=}D79U#ao#PTSn(6M?Ha%U{c1&$btQUmm~o&+N@$6}MG@dA-@>E!P<8DGan7IjLr1c-+fU3zY%o4pV(DJ(R-v-nEFBXPfkR=AFX_5Y{s^R8Mb?n(fa&$Blv z+jI+aNQ~d05~?pbi1w?b2?4a!gHht>B;>vNPx$;v&bi&s9X(p}t?+R_kdio6oC6AT zoFUknOW^Mr(X4;ksmqv#R5Jy$24&u&wQ~YGu4p%cBI8f7CiKp3*_JjRd;|q#>QWJ? zBc;6ANe{@hmtM1H#ME{K5nJ}F=@op=EwRZSPT{w@Lk2aLWjorpGxROfeyZGJZKqW+ zu6EigufYAd&#{SO4F>BY+hh8DI$qD5tAFluK1W$vI3z9=4h08P%Gj5t0|+Xcg6t?6 zQzdZ#ON{RSw3Pi|b^Yg=;pFcl%7P9APn>fxjioTxCA%0<{8ZEHS9Z|<qUB8&%`k zxk;g}*{0ZVO&VH9civzTu;3r#mJ^+bjm6~2*>(#Y75 zEmj69f>rJHyh^!YoFM9^$#9C&Y=Qg&o&q#HM4oHocJ5MtnpV@Jgh_k(1QKy1-1Cqj zLORCh4{%;s%dQq2<@&VOye#BSq;q?^*ks3YZJdezF)ZF4Y>wIKOvoA6HSHWyfG6Ar z+Y|4!m7$}*P-GjQ#{{21W*(1PF~h#cQrvlA+>)Ge<47y!d1rQPXTfrWP20|RN}s)# zbZ5-mSl4?)oUYHUKx*|8aK3{$G+}{Mc@$+aWljrr_bK zPma<~FYYd^OP(9B@f}Uv)&LoZ`t2!^ozK!K>7xx>U`&}z*%?vQrIwFn^yN}d&f#Ydev%@+lpK}4K}pi)G^VHxBgA^HT658UE+f*%fowH z6ScZS#Xayb3iI4LH~meW*VSHa{9<_&x$jBxro~rlX|k8;39bzZ z>O)1a#fABIL|nFX%BYY8CndF%b0*uD>lJEGz}BH&d2O_m%CCZXuu>d(@QvG6TW0J+ zy?~y6PD~JM$1k2@(fw#ul=;3#IoXo9e&I#IcE^4MsC4_L1NSV3Ry5gN7+5n}+_#=R zZt|%?OA1yqY)qPFoLqJsLH+Ml@+ zC;`1uYMK_Ca>Q^eCHK(LFRJc^yI&$P`$CXkFZUSeYFMjd7=v!&!DdNs5gaLOOFt!d zY|2JyIQ=u;g(|yXp}2j6SaVfwAqEuw)cl8dXDpz$u9MLEbpI&E+T1qY-X+aEC2tOw zPk)B}Y7$&#V%oCU4mbN!RDY2t6D-wvRPJ(DmAl-N2G6QDN;-jUZ2DrH( zF6Stf?E*^#G@W`-cz!4 zCqennJJL?zRP2%y2)SmzVrKhW&MRM1V>k_#UT6^#%q)lQ@T_)RgMkHw?Yfda>7(Kr zou0A$V)$Dvjl9u^NtWv;d_?D6GmM0h4xH1UJqr!AjsKz#?k_}4DEA%`ewTIm4Zfui zc^j6S-;nSb!-Y~ZofEFBWkw9<$JuQvS#E?);}tF-Z{FlN$uEI}6bmrs_c02M#r1|? z^7MzapG-UNPM&fEy(lL>nvnL>`_ zIq=`Hqag9>ec1Y(%%bkU@vquL%mntND#D62c;%!a(}>r50bmf;=xRsiUlX^-By0a^ z2p*bg9G$PH+Oc7pN6kShOuG7Y)?^BLDDQc~6{8Xc{hYQf@LHpu!5&MR zmtR+EB2k10$>x8Ar^Sq3;Kt~A$dnBP?dl0~cz>g^B|YB}?o>0WQ;S>%r0y)=X(4md zzM8g`Up)AO%R9Aq8q+;WAJE+-#XQ;E%-dE=TZ$O0rar49R#98i$9lE3ooUL)Y(2Ey z2tHcnT{Es^qTjx#1>|yKYVgdh2uyUiq@x3E_ zMwOq7c`@5EP41B^K(zhO(le%^sDEJoD3X~e96IATWWU^TZ|zH0=+k+itBJ0uRnb5a zItOo{IkZvfK>3}p%A9bOCZvO3JVCxx79Id^KS3?-lL~r788MtcA&&cy!nktvYLMW< zsT|j_hA>98c}L5!%>t)n2n#sVY=wOpEDf-Lbl`aRIN~paRl`#&-mWDWPTcY-9>J&?d0+4J4jet0e z4&1~Udd7EbD|w)AirYSm^6pOF8l5l)RJE$Pjh5pAr@EK=|85d)j(7aiF}&+gT($oD zgETwI3jz6s7x%p&v@GLqT)|#+NBS+PiZ;KF^8EsKtK7|?;@k%@a0m5*1Oumk2_VIH z6C-vA1}LQ4Bq*nqTfWoR%dQH2THmYbhIhXU5|8Me=$kA18PQBuTq;U3vBlT%+9}ce z-L&Tfj}OQao}Qn3w5TWE7Km}>RTcUb>U*qSL$`?pk=}SUTGPlSy;5WM27WXawUpuU zVCKCYs6hXuCjq*cmp@@LP_^FWaMjN6_x*QcRo5IEnmvz_sTcW$`q!Axkt_ z?&8F<^&RelD(?^Z{@4)lSa^FErnjC!vL~|gfq*Q=SRnTlafBzu)fRlMC&pb31vW-E zjRu(a8jtKPV)@pql)OS-l+Eb$5Iz?uQLQMe%+js_hdtYw<&u)I%QG5$S=kZi&qF(y ziUxBNqLx7+u3zVGkQXe(cis44i3fSFzVSr%sEGUfo?*_n>c!pi2`8Rz@LB6N=}ww5 z6pQ{GrHIUx3d=)I9Os=^OBv`u#voc|BBVR2a&Hhy6EbG=^YW<;YjC1zE6L;ME zY>59Sb z?>k<_w93)?ZkCm|)7kIf?(*h%k{9hV#V7bJ13`0V1?Ii&;KEQ))u8Ww7rqh~j_c|b z=d}Y`Xu^1Lt9V!+xgMj+D5}b^omsY42;#{~}YDMNk`utaJ zo5NQAqC9 z2L>B!6MiI9Cm&9Bz~P*=UDuO1e!a_xgZoeL7>wyRk{Ghl>vdJN<9>3AtSb3#R9&G$ z*d5)=0r?GQV40XKhVAT>#c&~Rn&D^#hO0=}df0g#2FV)MgGEk1e^95lUUeqh^zHb} zo4_XbD5p&OyOC<^?&`2TcSu8BaR@p>+iy6|=7A2xdnXWo$ zW9YALR}Va@DPZ5!Z**2HP`O~onYMessvEj3CDu`j$Yj4vBbr?&zP?;!ora#UvX&ML zT#Mlfvo9?xL27SlhWqk#O??C=nPviD9ocmGU0tt9^-HPm`Xp%j(<1a8_&~)CD_~|- zf_->=!ODQ0|0wTvanZvX$3S(AfnR<-bZ`C&;y(i75a_H`hQdsZjXzDPJhqGoiRr@~ z>;DJB)*5VA1d(uD`cPKy>-FGpMn(ZY!*{oTO+}CDJ?L2RkDRfybe}vBqcm@=;=_5I zR5VKR@aIse>igfCN5798&t$h%`Q+@L+0Cw8FJJG&bzqx>S0RRQ)Sj=Z4v@ieC7{2h z`;GVoNzHU!0pvY=Ctgo{PqXYFQV*Y9__~mMBQtYk{dM-C9PRbn^o$`0uJom{U zJaSF{sga48j$c%1$rab+t2Nk(Z-f;W@@ZOshiHmW@Y^0<5*_ZfjnBSj6wbHfy5_tN!|R&Z;On z;HhWmo&>Kj`y7I(_E=JfdYh`($s}|-Z)I9H^*hFPUj~*_Bie({=->zU$P_I00;*u< zl#VA3_E5mHSQ2E55EXXX;hKUNtYKe!9y49sAXs`GB~_ z0jmBi9kCe&Ha;e8itj%)iB>! zbWF7?P~@>{J%ay)g%t>PpSRE7<|aZT)CzLzu`r93kX+~gHf31PDx9ZW2VIGsp#2QZ8ze1-qO+ux%9X8n9|suANRU9Vn}V^yLGg~u%AtJ zhPV1OesM)@EL)i0bFGFGA0%m^|6(P%Ekjy&(qu%=&x-mbL4`+(PP#4{WI|b#g6QZ5^ZvDp@Ov@x!bymRsZCzX>(+_WUs4 z%*m`8V`5#i>d^q#g1d8@2ESl-%y)eUM&S6a*^Cse754Lyz;(5Ym>&m4wDzAbWQol#o z-BIx+OK9h+C2*9VOgl=6zgqqD0=WY~ILQj!j3h#&k5S@-^f4woWpO}}p_PHyI#{?O zWz~EL<_9BzB;u*&@tgD%rk0+~P#zRe7Nb^&al_plTl~BmW8Qz)4cVeOR6%*`yUq~P z11>TzK|N_Erh~Tp^gHmEsQ;K3;WjaHP_ZA=<)WfVwIzC+4lThuOgLcAZyXPYVq)eA zT*TYgaxP~u91Dpq6yS~Dxl_EoXJu^uDE&>isilrRTY-2_Il4Nn>e|iv^8#e5o+DU1CKHS-fc$;k47MQ+yS|Gb#<~ThVvatILUxw?1bIA(KgWc|3c@( z$HuV6xqG+!uPZT5dt*LNWDI`Hm21WsepBUBzp_4Bc|h-{keBk3%I_XB-YcS=a$qz( zz74Mp zY(nlX3|;C-84P%y){7AoiClja7!m^-H_l+1{8bB8KOt^zZb}52u><*9bZnLL?YPtO z7&OutL>^op0i;z^5i0|oI0j>z=ZGNVm&!_F0)1U9528`5xbfhd{d83`wq0} z*d^P<$BwPZ=-+1A^W?q8clQLy7i;>lzlJlB)PStLZ5zs)79(b`cMAGkju5CTFJ%}o z$GwR*`vK~k;48kbV6i>~*J`_$ z)4h3@>=2m9Unn}+%Q0HLj~KTZ$z@L?-s8Rqvu_nRMR!_%N#!9vsI)WRF$O4cmX>(JyvMDB8oaZNSdcP$H&G3T9 z|6+0ii8vSLryTKz9t;m9mJnboaUVns?5@1>XF#EClu%2SbP#ZerYWYwyk!_sG-N)U%htAfV*I0{a5HGja47(0R-b#W#R7*m-H@C z{t9i>sektCZJsUOwL-5bP`;>BHu6&Qws}Zb)6@-OpLJtVJBW3r{v*yi7{jnNnDTOsE);?HResn0Vl zikXWj()hF`TGj#zdW(q|>aQy$Pf`KEUG4F%9&EUb!v=|u?6-hz{BKq4^Pjan4!Fy|;U)jHA^m9J0LX1{Hc4_i=7O3or*o{-K@A!NcU_xu^% zfk!-%e?GAPG5;>V;GXZS*mbX-rbvG%hoT15mup#b4Ae?nzlh)GJ9Hg3T28o9AAUs7 zW$rxNcf!(0XueHbt|&k_R^A#@oR5bwZAidvXYXb%hkv9TcLEpix_pgqe=RB$!zI8y98vG*FTAo$rT=U-;a2u~D4ChIQu+<9dN-HXd=1mhq6#p|f7mVUvF>S<8Q?XicLZTxR zSuPpLJMJ+FC&v;EN#uq-L)U5?7Ur^>m}OHOY;iB_!H7RBG8GLaFxTr<{;aw!O39$k z+w~ot>|+S9m4dGl&BpQD;MN`W|FZ0{0?MLX0We_W)-ZPPN6Z@ySG|->W*dL8i!>>c zb)cpKi;Xr<`rfg%4(9GV%2@D)-Xw3mcdSIT;ON3?HC(b>O7>-g7;B5j_p;GrKW93wFvN^^aw^ba4EVF&PamM zNOIEMFC<+uZe{~fv$9ydQeX_Rdk?A3J*db){t0tv$xBGdTwCWH${F0;5ez0+`r8IF zfL02!WcO{j@$}?AsOW#0Gu?((2K+);%rm97&z@GtFfU^W@|EsCVY)s{rTI)9FjyuC z2M@%0=@?Dm$OTChYR=I#^WZ;R z!b|2)fu4+&rR17UwD1SXTI1SJ+^gP9-HSBns)z9L)J#4bm$GEAp%>YEf8VL{}R{z^cK%>J5jKzgCh3RcIgL3vUh6(3GUP2 z>ed?EJZ=~F^VO}xD5~-L2zkpp6FSE{kd<)_zwOx2URCkgqW-QebX9pIevVez$o@bV6ty<3wmY$Nf1(;QLj71zYE0ZGd$nx2i%^rK8#)j9A;cQq3X6vUl z@6G$TMAhJbRgD|hf7F%8UMOIAjwJSMf8K2FdszHh)y~W0-aJRE%KR1H(g^mxD12AM zlb}LxVnJ(%^_XC0Fx+|hb@-;^`T23xgtL~W=6c1d2}LMl4l#wY-eQc#_Z-->Z&bV-9oHR3*Wlw$67OQ)16G7@qaA}Y~4CQM;_GLqWJ%VSGN2uTlDwf zqq#l&#*3M|_~5N<$g^7}?vnz~5|!%UC=hAsX={8EXcN5EMzB*Y(t|_skBzr56RqIV zHD#akt){nOuH14$nO>Z~RQp-@n%&DEviSZipxI8~=w-65u)`AFb$;$uTJs0@NWOV+YxKht4lc2~_t7q#j=x6Cn zyR($eJw)|ooPQ4p(7gRKaDOZQB=vcbtvvIC(cHTi7p7#=zk{RTfVMqQ`jGDyI51=1 zyW3`euV4|-3T()OE?jV@x_+@&xwMdg&YKlrSyg4Rf16z6zV^L=1@9I@4HXb?l_m#0 zJFJ=))Pb{Bz^=zzbITuYSfzfX z0k{IC`AlcU3z?r#1+^eUT$vYsoPue7B=rp`lI4uv%7oV5o$>=mU*Xbv$^EbF&c=Ws zyBXSZnRl)wO_JsDcZ4?3-XeXXHsvxApX4fDf{`CKoV(pU<0=EOiTDr>Qvvwo?!Ff+ z!;ued?DW|CCOlWVKfx-J%OAiz4`o{y4&LImO{%E+k`jU&NnKR5!ChOb1?}{n)Zi3|p5V5Iq?rft594h!k|Ix!{GJiM?XvyX zsPFDm>Dp{qS}{q_>*ny*Q}cavRjj;CQ7?XXnCUtbu*yt!uBB{l+ zw5nY0HIO{FU&%zSbkz;)>xfTM25z!+ubQNLKz<9;tu8Ys8PoL}Ok$J<6u|09Lh6AW z>Z|8R?0mg?V_Wf#y-(TIdJnWxggvXS{q_L9oYnd?&2iGWtXT=wN7ODuPGHp)5!>(T zaAj+VFg?gK!1Bx#|2c>RB~(3fJ|$oAZLH$*lexirG#PC2-fSjzy4hT~s7gf>WrkY_ zDGjd0K1Zv)ryz#Iki0$pT`?;Zt^hEsDzuvsQ0mt%YA3yVPA_#y#nb#+ieL`ZGkDra z0=JHu=iy+*H;fB1>njpC`+nbqDn6*BV*l+A8;G9xY1_`*t;}EB;{4SU@ZU>b1k_*F zi#{KAJfQoaQJ(cvtG_h*ey>jzXf}mblmCOAWqR#aJ-rdRH=a~z+pxJ1VHG`b>9sz> zvC5qJby-Gw9o}>sn*!}gcg-;lz*h(Aq5GWC(8)!40PSo7N&7XYUI|IumprMCfVy+K z^}q3}J&L!NtuexBS6pc@;)mc?>i0nA6`uOtCOG zae(i-JvWBscb9nhUW&k|^ng_Kl6SHGL6IxlWA!n%!ftN9(8(@oTp1I-TTikuE+{aw z+EqZaJ6P%r`3RR)J*fE7(VhGWT?9pOb?z10sqLC9Z^lj_rvV&WN>5-Gv{Yqm3W9G@ z-qNn4%+kv=Hk&`2GxKRPY$*^f{JSz>sd$9~;Xc+AAhUdQB0fyiJA12a_gs!@WLWth zPFOJ8xm@K>JPg%vmF9Y@foEJ$sXxx)`O6J^omm+ld`-6sbBibu0E1>lV{UWAFl*r` z4qP0Z=Hs=AoRZO=#Lu3cqow7wp3)d;Q7x@8jDNyloYHUp309vh8^3J+2G@ zb6hGDgLds<^L;RVh<#X}i{R2l}hI=8~!f&=aa(jd-M9*6|QD{r{33>Y|PcS z{$9PJ6;gWjjOXNSE}G&I@5?i@T&?fB2WH7Lwyv#Kw^hzJ{+kE^2p@s*c}`UQO{I-8 zYpSwpw=O6%hz5Sy@-e-mT%DjXKORcvl@Zd#y)7l%_V!L(C21RUlD6EXKkC~Ye|h!+gQW5(l@ciZFsej{ z9l>2MtUzI-U&EiuDcfWz3Q)a4MA|U$b9vRj314=4;R^f8ok!qdJ@3SK+nLcOYjynX z#pT+4n4<5f_QZEx01(=9hy__F_@Z7$Y(hM389EB5Vzpm1WXyfbf=zaNsVrT+KjFq z&)=c2o3e=7i>D`FDR@vN`Bki<7;s1$@!X@2iJ3KgEv|49&QR(mC zBLj~WI$zr6q2n8Pzj5f}xsNogiV~jgn5|XS8ZGVi%DWaHLzA^OR1#kRBHfvIVd3|a z!{ZeToGB}G->bf#^~f01-(Cmo1-kp12E5j?iHhjbg}(up^pBsxxR5_J`C$KN)K^?R zy_31&Ew8~-oR0|I%+&YIF)M(?dc`!*V!0jFHTI?|ds~@R!#3mAd$D7|SvdpLTUj$Y z8uU^MQ6nofpOdMpoiFOblCB1MNcnHJfuc{%g@_SCYTi2O&Qa0_CF`WIs^JB(|4KoI zpG-*Nyk5{dou)HxAfjdr(P9(%2l#a_uJ+LTw00%SuO^J)UNFj zdSM(zHB4}lR(F21un1NnHO_ zjMj_c0oe3$x0i$cjGO$Tvi4jU)405?7Q-Xe-Tg&&&}uEBPIXZ9S&ri^40dswMPl1s1jSoXmpdn=$o3 zL5Ql4HkxOiEh8fufhw ze|E!oj^WKO%g@;5KQ8Dzv+)~_r5x`%`qWxwCa{p+T-!L}=rY+-G;e;;8m8`#+vX#o#(S1G(ei@>5Ko6v1$CP$~W>`Zq4pQ=N?bRQFW4L=C5pZ&g8 zeUd-u6N#L-j;-EH*cnJ?*xsXmmD5Z{@S?013z$xZxl}-j#Z*Q6iAL&mZWYE~MfA`< zB0o4PC_A57ZOEb;Smf`XNV;gWV}N(Sh{ue9Uv{LsC-pn7%{zFxvXi0rGcuUM!3xrS zBg$(#ZKwrIQPVervtt6y(khD`NDo_4Cna|eS_BWdnNvKC~Z*bz*)MSFiM<9sINQkX@T%fcWkiYmI; zoP0yzwNV=!`3}g&IzMaO`ZH?LxLm?fZt8_tm_Rd}!p<{e0|^JiF3Iv(&C^q~ga;Gb z7ecbu9wiF?+fpzwW2IiAFFG!!vkZwN<=Re!X7DM7(|XQ0;sz=Jf~p!j z2TjzzBhakw8^3(F2svsuV9nGix=^reCxp2m`5FPZ_?JSPq()z5XP-dg#9|-pDkY$? z-R1pcyKT9@Cd$mWz^n^usOHi68y>p{;~4$VlS8~!Fnt|O4eUK9f)k%41Vy?|`p<1D z6~P!0>WIf0B!9=MZ$ShZ?;s`b)d!Rt!M^)ZRD#4c!S@PL{*0=rMfUhT>9`LB9YlOJ z%WNyr&H_)|?@@NEoVC zXQ0mBH>?X=wyKhDnS9|Lba?pcy+8T8H3b0|K>3^y{1jHTFcd9htEj6Z% za$uluIjU9EBNnFN=WQzaYvY`6V0zK1pGDeY{!tKFyg0@Nr1u1q%cVzVur{!kt-P3n zNsh#=%Q@t+{DfEg(18a;b92qDkj?%I^96K&?=;JrbyDej{B~o~DtgLab?Z_+Vj18w zy0#_P+=@%DyIr-)P&LjgEpeg4R#8u}KRMB^%yx0TEJfw*WF6BQP23>*oaKmbpiexW zrj1Xh6x&#Ik(wM@>n!Cm))1Q>##$--yBQ(8`%NqGtGh%`jR^aUGMjD5XwN-Yy*r|; z_FE*wqn&^HGdcA^2Qx-IV08?QY~BIb+0e%D%Gk&~mJKth)Fmr8ZBi^MfZNc{YTDLY zmOitVKJ?_^m$>t#q8-|)sigX{etR%W8`7!f)%AP)i`}_mrRcwys0r7>mr6^@c8M&q zT!D_w_7M3eQaR9@SUs2!+V(cQDD%!}e=rG%nKOqw`g~5=3j$S!!z^x2HhvQ5RTV>O z-dXI9+1_Ylp=*yTciLA!@`j+CBsXms_92gy0_?4(#6?g^mbGcMzQzicf>*4Hgl@$B z$8cmND%|(Pv}j+B{Ia)I2bT4fr9jH2a*Yx)=3m3)6~nE$W!OWCu^H%<@?5zI(T;7b zwZtr}JT2Fdo8JVic08Jd8F-{!Ql){SUuWm2rMs-0Ui(<>SrVJfCD{aA(H{6D3xxW&lk!9RcB7XHU0Rt?(u^b zL}0njW@wQI=-wr%NieYLxP9q7oiA>ls_Jx(Qj6#^>Uvf07G(qU3Rt_-+RoUF?oIrB zS&4Raie(1~77%S7B34n+5AJGK?_RvrPe~4_Ygo+7svEkqop~}b8S}-NXyrS@3W{PB zx*3k}wH#gw+X5~Hi9$kXzasnkzuFYm!rk-Qg;q|fPg52K?D`ZeU7-oY%5gxA3NqH` zuOk0Ecywjsi+}6j(zG9X2?c_Y=_DIMD~LHidn0&pCl5v2n#Z_$Jz1+C=JM5Nfx-{$ ze+Y=Vz^q53 zS1@4c>T5Y>&*N*zC-JSxdv)uc$QmjJx zW-Ni~u}td!cTv@YeM7WjrhjhN(&oD{at`|GABE!yT|!s;B!yAy_QCKx7_GYM(ako8 z@Eyad6HIm_D8Lt3O3$ZhdJ!#Y5B6%Zs_xZP9|0rErWE6$n2wL;4o>TM<^=v%im)JT z>NxhL7(41B3v=mVL0Ii4d)Z}Yu%awP{UK$bD&sxP1?@j^WmF(<`mqt%`oxZ=fgY_x zS=OK6gmJg-U{!~z%zGg>kc=_Wwspt}><*{4R)C$neYcCHM4^!@GCU6RoZQcK-es1s( zxN_%TBNJ$^nwdvlieAHCAWfrXoHTduQq?YtRd4N$3LL9@X26X^^n%-om5XLVMQVCG zJBxK5`UK=vV?utNLlT5+f0yyxo{D$Z%l?cXzNFFMC zA+P(-1O67y+||?k2YM%`{UeA}IE-ECy=suUZe~c_c{% zFm{=%=6iz~;S#14{%`PVONtxs;b3pq%h=wR0wOxEb?oQ1KEX3|| zz!tXR8+z4rQv+If2Jxfu`I2jrC4Nv%G~mPpHVPWljx7MGt3y<%8=WLG8?vE<*fjESvk6e;Z2rDESUxu z3?Ns}B@3ezM?dX!Bn@86Jfr>8F?6L>Lc|Mdl;RR)CcAUnjsZK)0{&t|^gV;6FEl%I zr#>jA#;)8a)+}~2P$sR#VohE-5`P7@fT7t~DB(5l z;f5<^ZMq8xzGUvfsJI2n2_bj4>3q$0Hx$m}>I0|8_u6lHqPfF$ zuqc1~kT_xn$-^V*;#E^0U>u1JoN*E7CBEp~%ET}1dz^x{;Uui@qeQl%WIs#xYLT>? zgM}K4B7WYun=#1r`G>39cLH8>B=Y&%;Qgq_$QSVe)mN(GJ6TGXHrR02OkHN$Zw|k& ziTNkJvg@jqvdL?GzQ1K#d2^JxcEaO>`9?Pbmj0}Sux7hj)>0;??*u$?RV%c<&M+4# z0{7-$I}-```+1yaBLN~87oZvKX4-~!Ky#BC=2S*7_L0QHU z%>TQ4WRhuI{C~SgF8=>?kA&O{iuQ#^{R{hl3P}FH^&|5E|F?bwGrI}*0B~EiV5ugt zjND=Dnu~hon|t0@FAP&9XnbjEZR(AL)$_%v---{#_NfI8Xcmw6SKdrb&OQ4**`(;^ zi({^-Y9?oC7xo{;y*O5Uwq*aVS52qn*vbZTZ5JQ59nKjLH*IpnXwW8La<|22a#s@5 zPT+W8#MM}_24 z`~VXoUClwyrKE$UEgWCeXiB;i#rJKa?f66MlGX|0-Hcf& zr**?x)UZ$W7iQVuTBX^O=xb>7649m+!<*=5N#|oqxfD*eF-1Dniu*#?+r!67A>ge? z>v9x!`Z^55ld`~EM+{2p2%oJH4>G}o_6<+zI&#lu4-1JWh|5$rrM*y4otPKexSl>d zg%;CElrIZSb~8eBR3kf>J0~I114{yIOMOn@_ zp(tsXAgNY(TsL=a(w8BAgAozAm6IS_F^>us4+{}!AqE1ZB&^rZ?6oZ!7Fbh6-qHoh zcVp3yH)U8Q(GG?%N!d&oE6$jS4cqH(oUKJ|D)`n&`z?KIv^1rA1vt?nS9vp@#2wqj zNK0_4my(j1x)jCy+vRq~dVt5IqY259$?@ zgxp$rMc=G^z?cEWAZQVswew8cqtaDec+lLU}h#H$up^XDn^1iGq}DOPId zkN`Q6!-_?IP5=x)fJDv|lTgd*!bWko6pL(F!_z4<(z(gDAR8mNjS=V9ssTyqd}HU` zA@GK9ql&^S5JDuJYN9ZjSJz0v|EnjuKM8J%xa%Mt;*CPkqdw|^D`G&?N=cfXCjBnN zqES4BS!*ubFKn@qCW^!7#9cd_#tl4T-+p;vMk()GcO<}Ko8MQQICT^_K2Hq!Oby=# zO{{MyZDOv3B&LcKAS@=2+)nu{n!s#9_a@Q9gp)RQ6Qdj5;63n1<7iU$%8Ht!jT%7= zl_c#5-Suop-PR~=x}N()HfHy!aR`_{->qDr3s5VyiLGMM@qbnoaf%u~d)6ivSW4P4 z;9PK+z15;SsRm%G6X5>jhyWZmxd8`@S9aWd7eq~730`XP;=WivfxDzNTv7to5(fE7 z6fm2TZ-l0#xd3s=1`fH2Xe3Eer256dt=7$ zaQ3rX<%DfZf6tI6hy?}Gt6e4@i0tKT{whiBc!e||4J3-{Zu3w zOK_gfX2MTItG>0Fu1Bnc@R#c~sg0|Py9+1UF7Ne=rwDs`MYV2V1XvQ*-5kAvi0PVY zY~$81AQPTB#Pvq9aBX~m@~spIb$ZChguA{t!it9UpNNVy?UkKW5>uU|%Lk z;|A-CKP!jo5|$wTI;_J4?^jpoSF0Foxxc__v+jf|^)Tbf_beRm_dd%FR zAb2m@+99&^#`N7dFX_F0R0|MW;=Cx6Ijq#*|M{SG;xOGbeHhpdEk~C_?4tKz0Kwm) z)E1t3LWe^&pMgn4J)>u!0z))>>JvlpYe~|AHz~{&GAFP1mE&n=a%nazTTOGV`vVYktQ0PxOE0cJ)E5LW zF%cA0=f%`D9Rx?4_#P4`NEG^L7f|W!cY?bwQJt-z75zEwMMAMChy&gs!uxLLkPa)+ z^{2X$aQr!`tJfFyyU+0ziCTHfnVW-`2|V@P=lYT@iLe{RN+AEa(*sDRQy+(Ro5fQf zg9Wq9_YmtobBf5B@`e29 z4>aUf!0}X9!HSjCYv?S;JewFU8kXF3t$5hDvhOuMX2H9I9Z_8IuIN`;8G$wlCc%757;}cfIS5bR<|g*zinhPVs{QN1sm)q>6Y)mUyF-SC*1v@E7g8 zsHK!cQwBoeD|owpfL+^2c8}f8;gpDXKDmB2wD72V1^)IDSz`L|Xp?Nxw?k6hqO+KU zQkYebB~pSSNHKcc_j1_l=@BHtNZdbKw6{?`-hJ;Dv?D)(UOr{69B$x;2b>XgsD5om zZ?#81)E{t~+c{FOAM)b=f`2Zi$(nuyo5#?2URZgqJNTE#5wi0mlGOQ#xn=FMsMFiLf8;UUP80L~-2PR@_KiIck>G_tTcdAB z4U2u`>!)hGrh|%C!jlhpt?JF6#@u^liFzPE|LA1wY;zS!@Ah5JJY9Lpe6~ieArapD z*Xm=SfSsq7GTWmweNCH{SpBz!&oiKPRcQM~x^``6fIv<)P|ghB-6}HLFBmg@Y#y7s z*#qF+Brjf7;gRB=7Je#N7x+|MQAM8ju9e^J+ucdwOufQ)C&niC?sy0XU%K>un6}Th zN*aAWIX-GcbuEncQ@kb&LZ*Uu2HWz`=Y2K%_u>E(Z#u(zfPwNF{s!V(Mu0)ceSqRa z2hE<_Z^rn!a*xe0bk)u8N2FKfhKltqD%Mw(13HUM$$&YUo7WR?nvyH-~#v6LCP&bl2o7 zLoCA0Ku(EfNy4Uvh9@#FAA3PI!OWdmR~8agkU1ZiE!zPH^H>;2ozo z0;}(S)l8J(e#%lFx=EdW4$7|HzoW z3=+Hrf7YoQR{;lJ;ux>q26%LtNCF-E?)zU5yn{uZHbbTaXWKatwGs<3C}*T5|A8@U zS@|gaDCqd+g>ta!&1|5lNh0W9<+j&Asd=z--%mx4+)OpXI=op!t^TKXJFGpDi3P*ImR<7-6z_K#w3t2-XF>$ClP-I=vpZ%lgb^dANB5R_LKR8PANw%7N9 z{B3JYZ1|-rANm#cjY*>_7Xz28G;{6|%OOA4=tB^mD+22C+E`^K89Jo`JT**H^h$)S zyd3$_sZBl${%aF?1V#VZLQ#nP*|*MLFw*_)qomB2z?Y+&s+r+~9Jlm}x;jNeBjkTo z5o5P&yZ;51+M6iMxXSsy6^Fd3esm~w7u0%|LtCVOtF*2#E`SB-@DAo&%qdzxRW_Cs z3c#>sE^%F8*Yw|*OdQ-rXZVQ7d8hOf8uFIci= zKcaondb#GaJa+!E<=wOQvA6}RM&3;%$Aa{C%CM1x}e6c0e|L;G<#H272_ zo~;zfv@vO#N_Bp&LsmATYDCx;^jA(l9izS=Oem5P+z{lp1V1GbN5PEb6tvR~y6b|8 zOfij+Y5hlThB`Su7~c7usZnQVAn;H5e7#TL2x@Z4tD8Ss5RFIoH$EU(r_GF%eKXgh zX>RL65*~T9eOec#PGuNwb<%#a7|>pMHv1z9$9qzB!Oj%h8QVK4_zA~*130NnbtbJ^ zdevPJQ-}TjY~BB`XHY%f{;~~Mo|(Dz#I9V^@^)+fHKg_!{earvrExE+?n1yj0Ex&` zE7~&Pf<#X4y9aZE^aO$I(`IY5cHJ6!hY@rz19kgoNxR{U$tiu?uX>22fP1v?^+#sz zeT{LK2|c>_0Jn(CvO~q-sb6P00`@YI7w^>^+ug$WTbn$sdDrx}O;%d#)~_?qGuF=@VAdPPKX{1IG@F7hYetqUq&tIUt$lZ% z^qgj#A0Tofs%;xeym=aE{(0+1c>+dpg*nZP3w1gGm?m(Idn~u~7vC%SLt_JtjvMp7 zg>SFtPu}RuZTjY(t?2j2V5*d@p6+gojfceDDrr89f0^35))ynn;=WYax3kyf{Dqj9 zcJK!@Z7c4}H7jH9J$atOyNYSL1F4%&55@Vy^7D-8;GOk%Yi-E(T-5rU)dvUkjBPGZ zUhSKnm`bpAi1X%VM9*)AFjSUL!KomSOwt;F(k0=^uhTRJ{z2-bv(>deL%hyy^gQRu zaDOmSvA^^$LaU03dJl-s5xec8i^WCs#L*jclh^^!mq{z{f|WN0gED$AwO9%0iE#Eg zN&gM+;TB8IGr{|IGfNq8(V6}HPN{lza@tNtJNrqr5+*zI@Id5hYrbJ9_fZ zguV-IU|Je2lrFf=>`*v7jjL>xS)P}N;F=plg?-jz`$%0wNm}8sqgRgmBLy17Vnj|Y zMP2!&8H#-)ki=;5!T5^yM?M+hSq+mHExpfJk2k1Y9IL;i?1PHzW(zx>v6Rp=9CD1b zMc#QL9WSlssmcaryfjUP4!YY6g-)Z<6Up6}d7Md-`yrz#fAQLqXFqT=HGIn!O_vih zTFFMDvF?#9x?3%@G*H38->~gd)y5fXW&SoM?pKvaS%dF3ie8B0@TKQf1q&LIP@4QE z-w6PWyM#q8Q;w+u#HKCvy7%{-@AakAgYXbh9mO#9D&%avx{9qlgS)l6*639_(45&zqd^ z?Be$!{4a~&dZPa^s0yjwwzU0rP_wH2Qy!rc!Ao+E-r(>(0DXkdU>I;`v~SQ$7*SFq zTTx>_)q%5m5i^JV$ZO8{^ z`b`A$&7X@06hJG!z@b31>247DE6QfZPE|LiSx9D}Q+Q^jY1p z9fE(e@q7xXb&t(AS-%5twC!-K)mwToxs_gHJjYAJ3}$%koNVkgV^z5uVc#oUdb`gq z4F6UK-_KF~Bjz~gS#oEDTgi^rIE=%k0~pq0-)`ZYMpVSZ;>kOcS4mdRUTxeDdfENW z$cJoMX{lWu>Da_zZdfXPt&|vQ<||XscVn7_TjsDFP9)uz^V%vN>h$LQuRWOZht*YK z#36o{eBP+F0V2QIC-_F7qeWmwIN1{OULm?>;@0gE6v+AvI6)OmQ>&IqSG?7N#SBU z6R>bXDd1-96K9LB@tMqXj4gxax&YIzI+2mxgF5~}ZZF`oUL>$6Ni}AS9bqGNye$Yf za*CA7&L19^ncS9@dG-Cl9AN~GhQ7yeb!AI*t(&*`?;qSC078|GS(YNg8GGMt@ zS8N|4kQt<9?Prlm?s4nQgC4T6fm(kU{9dDv>CA~Ha+}pX3+#0ekw)z_In%|)Dc$Bw z{enFM%?>qWnSK1u<;m#IXW}$vYjmq^LedemN)p_M9&$;>o_7Ska3J6SZXvs#6d7zw z7T*c6Z@ue+Q&qi}{zqP*EFGWpFZ;CDbA_qQsNF)!H%p$5nc!~5)(_yf)>?Q5s?IF# zoW>^o#YsA=6&?S@6Y*Q4`8hM=i2n?%HY5v{dR3*CoTHz6jciDJtSME`cfs2qj71lW zIs!zGt$Ueq`O=%4J^71z=riD^)4?#@v{LIL$@jH+GyQk=Pwm~>8&3*iuHGwkX-mWm zUY7E6i6=oO_&+>*->tH&Nh>2Mt2XhUy-`#Exjnx9d6G>S+NG}}`r^4n1yrYkcet(9 zrQ2KK38oEQx=r5*lY#)`RH|}|F>||q1APKCcH8}N!1gkhb;JK0cLe$}4_yO6nsWY* z`x6&o1-?q0CRIf=)%F&w&Hk~I3_GPCh`G&pX{{P{3pq9&9RF8CM5$OX#4R1I1Gu$O z&1Q?kVirP8E>AAI+-oaN6I26`2eHUV>CkQ-w4i;+N?wJ0NSq05OCFw5^X__HlMDUw zO4VTF~h|?KnqJi&-v|K0DUpQN!RD3(kUgnHvS8&Bg;&q--K1*J*t@t+VTwq*I4O@|hoB`!DIkit;bqMUXh>*#cof2(xQmOeAg zT`*q$Ma~@EVjKHJX6IGLsz);-3h&Uo^8`HgwQCT)4!eeYRQ$#=AcQmZ`D5r#A|{An zC$;wxxB#LD8tEop_KY+zrJ&~7-ryh7)~oTs{tgy04x&T*%HAlT3>tBSE?Dx2`j80> zKA0KZEA2Sw@w#m4)zOt~%65Cb{Qe?&vMnd~e=|Xq2PidH+CKgthR(&G>HUx6byVsI zmE3Y!C3ZSQ$ery}r&Po#MRLEc+FXZW7nLISO762&ex-~1BEyEVacJ|CkBuY%`wTu$L_TZKen*-%*RgSKOXb2-+EgTh1@EW}y;A$6T6i?@X5rq&%0i8K-kXM3Do?LObj`TT(vIw5ZDci z)VvO-j(x1J4PZ`TKay~t+GdF!96Q!^>U~h76pWrcO)nt4;_L|(|6zW)GnQ@&zpP@o zd{Z={wbYO-@E0z(nfpBHqRSP$JlsqW?!GJwOXguE3M+=0M!(%nd=XWZ3f4**u@!aW z5+{@`Qeo;SHBKd~eA8u6al-NPswLZO{-Wob0ZUbU8!fff1bnwyINa}5C1+BAMn}QM zx?=Yv3)3$OdxGsQTUE8&uNY6qO-NAn>6>beQ?4kwAj~WKTnv8GqUs#mm2Tnw%w^t9 z9ftK@%|K5TTCvjr=p<*!O)@qkPLVf-5mL3qw&H1|*v|7JHGGuh3~>Qy2eM5qeh@n? zg5~#2v>jo#DNDZZmbQ!*evbPS8{Z`|%}kgh8wREMg`cuJtufr8LP-A8USGy39LxWe z9<9+_Ebr8qwKW6{S-CzH_Pf=wE7E>P-27NkVE+^#*2(6w$tRYM_y|M`Ey%#huUiY- zBLFSN^gDNbU5h&%r_ahF#jhb+hT1XZyeNO|n397LavtARP>$Ll)5`(&oyXSqr+ns3p(bwr&B)xup2@e|!LT>a66XbJ@0ybyG8pfRZwP+J- zwZyR~+gOjN3!&lv3klpK3wvhJt(?^4Ml9}Wc)0b$jM(xADs^xA-Y%y#KMLTb5%|Hu zzI`1HQ62lfXs}=IJ{aeUZ`Yl+io6d)zKUN}&iav9(_wR#KoK$UIbv7*mm9WYrn#j7 z7sHc`+6!PQ*m)UTcix0)@;g%OV|29~R)73fDbL0kDZvZ}N*rxlx~7uU`=+9;&Gs#* zPY*p+xQo}u8Z>OeRX{baib&xZ@Da$_ROs#9myAcJS{igI?msHkVq?u)Sf zvMd!Fh3$K+YlVq3e=WTQovKjHLd*kw!1vgt$>+sW z+3uq!G{|o>5r;5G66Sg77GJWK)(hwnO9NT>HuHQoD?bEQ{zj`uvb**4>=}kC2c`1m;wi=&+of{h9G<4YUXE22(<#88|T`j19C0p z`8}lsQ_{CjD4PG|4=ER@1|q9^OP3e@6R0)wjG;0kP7)xggfcaeZTV1h`S+}6f#*M* zy%TM3tZM-kMP2hr?G%H*$p^+Fy5$!ERa2UaefG;&Riop$9VC?^G>L z%vx>;RNV1d?dLH;vLVWZ>+!9vilr`99Ca8v`bqY z7+gNXx+}gK1l5?R7V1EJFjP|@`+_j&g^Q>L&xqfe4jRm;&zNXA?C)y=`3lpbwwwgN z@tM9EvCHmz92J9?hdeux-b$Cj-cte`be}TtH?!@msZ*jPdmo60PaI+)X;A6LFeiKi)bwY1J!=wCfm2rWj11ugCSN%T8R-iMGx&xZ(S*h_=Xj*U3Rc= zn#-cVih&GvSTFsh&wDllIikkqm`ldRBCECmP;DjKqVgG!=8Pv?Y#KE!3hOUCF}7Yr zJ%!u}8d@n{bw2yFjbs72vLI7&1@)#}`fP>n(ADXD-BFGZ)t!`~+z?j3C=q9lm03+T zExNC}Pcu0qvQn0%PKVn?m8y&-)Ca>K1qfzoi+8Pa*BK=o9BjNpJei%MSi(mW{dQ+ zsUy9zbT0cTaL1?g%U+Bv!ZawfP@7KHm4}?Yk|2I!>*vmLrJmtW1zuI()Kjikz1fVN zFeC#alvgPIcCgVrxvW{yUG+-;$4%6!=8SCmIop;5t5W!nS|TE23eyYOg5v^LPyiyZvUt{>8~d^h;;g6j=`;JB(}5yDU9n=<^3K)L3?9>Z0aGA7!*r&wvC zT+{P_&n$DZ*2CXK`km=Z3qtQZE#nW<;U0JBIhVT;}99(we%ZmfeJId`% zu%jdQAMsmY$ip!4Doh|}J9CO{LTs@Y1vdp1nb`+8VmtQbT%QQ1Dn3u7W*qJ*-v}LV zEH}$4gFe&k-my^Ni}QyXr~6X&t|^u-KZQL6HIN5QD+0lBgWYMc4lM%Upi`k}Ve+B# ze_Or4=W91-WVb5gtSRlXP(%oCX{s@^89=uNqupUI!UA<*=#^jP@Pg_`1xB1Ljx)^E z^=xTkSNpY9y1>4)Z7=6B;3sIMRjyXx;4YKzNfRG2`iKwp^#WuVk&HUo7UILq-!?0y zG}hrp-H0zS)0bn?;6)xA_Xj^hUS0Gi<7*EIuZ;U2&M%a7<1StGti{`o`Uh59kg`8- z@l~f?crR4Prrq=X6MKIkX4GT;mzB-Cp}{D!JP#X>CLnf-00Xu%?0tI)oKG_^<@DJt zq`~{tox#X%AxZ|gZVq2rPE_?xMom6({MokU`^P@%dn)$~F`uu4e54ihoxzc785^lt zISg5H*#6?-L4O&0v*~qH&uFLp;9i@XdXCQ%XdrEs9UT6d27j6CiBZ$~!eYM8?@#bS zMfCxUOGFM`{W9O!dqI08F_ER;U@LB81nwcgI>Zf&i`4#gEt|uYR1nOBR?+iOo`4a= z*~t!CUAMj=JCSPf^D0hyq06;$GF6=moN8NJ@&ZU=ED%{ugN~`(wuEMc+?4f7dDL^;XZ}(`Z5uM`ugj$g^jq54 zUlX266NwIN>=E@E3lM3eeS(BGuDmz=G_fa|qTlpa3^enNY#mO^HlKN$FP}XWv+}6* zx=6;0IbgMzGS4Weu``VbqkZ(L;o+VZ(NI5sxjPJDW}JcqmFxZnCGxRFS+`ZZ+Md_{ zCn1Qk$#!j9ZWQv|MK7F-(+n8H`^#z{{j5P1Jiyr44s$}^y7ff~p8tqb1~D7Y6<;sT zwt%mgRVy1Cy5}TR9K9j7rS~vI?ORv zZd^VL^X1-n7}U^y5wxf|A@TJzxZ*wXmdw3P)`y=#Wvw*ncP?x_9j(zj$Nr;dmvcFG zfFUzs>OGK?+_mf%@VsOakDDu6O6{P35?|A-iuB<^07~fW+Ug9;7R^%g>DHV+yNwIB zHj8!y6=?rcmvCjzs<@R|fh4?nYPXXf_%D2)>Y%cRyVadK-1+8!B@N62&OfND!=1&Q z|IJov!6#&FAkHI9>07|S`X5_u#vG-DmY*XN%FPNFSLI5UyHtwwdAB+fmLG~bB1e|c zu?47K@VZFhk2D8Y?erYLd;zdp&2vx@+?5njPad$s#NOVTmB*9(31Kfn2+He2ZpSpeUo!Uy>yxzbcZ(oC&3{;(6#O9Dk<_|P}x2tJf=(BB#~24p#Rl)F6MC~MYrj= zFjEljE@<;I-gq*swbvQMpt3g@&SchoQ^d6jefz5@V-G*s+4lFf_Hl59LQg+TXZIuU|NqZjwM`9hz-O@hnHKfR>F2H?mL}a|le3|{CnPzBv zUOT)>2-DTRi{zS8PRh`C3^mjoQao8lwWhN;YUeb{^hwgGIl?|I2VMrl9u^M*Hdae) z)x0IoPPTQHi2k+k>F0gNN_Jc*J0mk9Vx-qSG7{%y>x=R$OW6)S^W1JoIrNcI&CUe= zJJufDqeZI<(>&=qS@}~A$k|sxmvStAC18%?s{_PO7JC-YE}Fnz;ft}(17P^`h@sio zeB%Cj*mT~hS8e_QO$bo ztGi_Czvt(IpN2#~U+NWU;y);MoF1j2g>-2M#u43KgyBC?XuA!#s23B@Lunc zXz(DzN>S5bh_Et$Cw;*^=B%KPv#X1LW}%SaldXaEW-vxHR8cK|;d^QK*i2(HqW%2Z z8#pAYYisq~E~5-nybd+A>T3=^VksM$c<6sFrOQR*WFRf~lgE?74!n_B_=1?A(!!W!cUffh@b3 zYfCN=!Nc(vmTx3sPSmaW)f9t>o>}FGWLpJo;m6bKh%hX zh}Qdn_e9IMd>5j>iJH#}2#YRQ2i7g2FNZPXgQtCIn{xvayN&2CqyQWjU*xG1n^-;- z3kvyt&(2EK3@$UKn4njkWj;k=`Jo}pUnq3Wf_|8zthy? zr6%*1upnMlZW!ew>fP_{Um+=_Hnbg(F)`j?OZiz zM@uVsdl(I<=Fz`5ES{Y)S_cTdm3gP6YD3{F(|KX;#3~59KS8$hKy;vh>NN|Nf9*8{9wPs z&q81d$VEzzm&g*IwVil)E!s9bY&tFFjUKyj1eiOQ4!y^;^0L^4K0GD%G=Vt@j^uwpedN$sAqdVj= zQ1)c_m_{Ks43;{;7x^C-JT~3;IND?f_LQxev-dMf*U9&D;DDPs{wnw4^|EV*RisV> zn}iuxv0kE;B8gJCH&H(eGW?z-lQv7Okhs@`IdymbC00_Qk%y#eSszLfvQ3ugFsBqj z>q)CY8Ca_#CD7Sl6&*l@0v@a+@C8vois#1PGooWz*gjA|z)6!spMNR#6>lxfbx4Pp zV5Mmx=_tOW(aNpkzZ4@=w_UmnNGN(Q|Q-KB`07rDehRv zqIU+x&pGtKox8|?5T;?@oDp7vnjM0L58NmY;6qve2kp*7ebe!>zhb!BeU*WNbWm6iT(mVE3=#!->CTe0;@ox7;;=D{=JCd z+*v_?WE~-k9I{`T1kZWwXlB{<`2&=A6`wX&j>iw9I^C_KvaRWuW5Aau@O_5E=vpA# zAJX*pApLeFyEX?#m!aH^=>J;hAuc74_AZD-VZ z$b({qp!*xa9%dZ}s|`aeO9;_I3xV9~Y4ZPy7_z<`YvPt(1wGk*Kq+_ z5J!G6At5BD>$d+h{maTDv4oz@mgr-mI#h&|S&EyqgCZU?BU8ttjsM8>rQ)XWQ_o3R z89%g$Ljdv<4Q!1tAvkMI{O^pz`n;weRjFFX`ZmBZTgMjR7}-}Rbl2tmz2POu>Tvl8&aumm7n~dx%zW;4ZVr+#Fv6! z1!>>4Ljvum|H?lu8aJbDQO0y|Y_=N~Q7`iO^)B z1*F__thc2#$}H}4N0OyeLT_TJa4dP{{SJ5O8@VuBc%gF{x;=5#Fqx=yM5QoU@>iXW zNTbDWGsZLF?})oplv;_mGNq`gi6FXA96nCCEDBlN;hQCR~;Yi`omGJ-7I< z-gjUS$Tbe`jI&Ei*yz!6B_TPr?jD7Lvr4AVUEX551(rj;>7XV$lsyDC?`Wwbnf4R$N@JrqFx!)Xt4%k{aj79jW}bw${ilp zN-<2- zutJjB{Y`USKuz&Z2QM2*&$FEsc4|31il9sD;^WozYRz%7+Qr6c;v%n9w|Ex+FLLqD z1-|`}gu5+l(1(E`Rpe(&F8UU~JZ4H(OW2BB=75*ysx{=VsWRxTyr8TC5nsN{+1uap z7dVEBZyTC@inpniT#aa3rm51P3JU2{19B;&?FTKB4So$0pX3E|+ii;RAnStWhefPx zq@Ta9v52|5id%A>ID5Iy)U7PTu2ERguUjcL7JW`B;5V@6A;Ohq{XnXyOp^x3*epKK z_c<@zo1%}}m*rne9!0YJJ`v$gVQ`D1wuJ49H=3QQaSA^92}|f_vaQc8V>z9uZ*wtR zTynP6_W$gri+Yg%2p96z9GFSe?ywcl=NkBWuF@#0;%vb6m^9UnzflI;HprSrAMSX% zph#6p4jG1Ih`tmthqv;}w8Rz%ofL{QjTICm(4av9M=s2%13QokdOx8LjAKPiB^q>G zUY<9lKe@PBsJWkW8TQmhuX2&;(@}v0JmCGqMo7|oM$aK^7ZOAc=R`-m`C+J?fX{EF zgifSIME?5#tko)-{l1Dbegtaqq<#mg{8{OQO|;~;zMgTUZg*I#Y2Nv&sK+qZ0SzzpXs8=0s6fltx5?sE13|_f3&5aCs6;uK|97= z^JeO02e*-#dGZ&bRwN527Y!^F#kOi5WR!WrpiL1= zq1>hX#uw}3gvUWEEpJTmS%|ry--j~roUOcZzzuQ*zB6F?U>|C$%=tyRunG{gu5I7) zr+RfxkH@)yt#O~t@$}s+L;v_G2eCCOtlt=^)o}^R`-S1I-7wO)gB;OuFu~iLK$$Q7 zs#A6j1nU(~`E8aq_PBvXn3)%>!UR}gcV!K-#v)f)b-s@Y{tSv$GTlH$+DgWVgo^U( zTz!E(C&!tOppJS!&$5Qs+c1wzLY zMUW@z?L^hz3M_bo{YD)Va3K@!>dQ@39Zh8y`P9LSw#u(}W-n$w|4B2ObJ?gJZVt=r zQw;MelCJbio~6|~Ps}BQzVwI|9igjlyap0u^gKPto2Kd-B2cc2JKlI)M|S2v6K~P_ zZw2#!KSFd>x5ztiAe{)xn{&zSttZfgOVSToBRxiH&XYav4v7;zUt2-+GZF-9xr$So zZ*u^t*y-TRo&HfuOosnH?BEDZm&vXgrn8vC()MJ&+sJ1QtCp;Q060qY1{mngQ80;&6! z|D9Qc)=+o3&1DlAEh2^JK2cs#Z zK-d$7xpslH_)fU6@tKc!ui8BE+j!53Tvd3@kQpGRrgD#~Zyh_#C@^G%`JFSr?!#=xQ_|i_}lukf! z4L>a6n53-HS+<0cw?|{PB&1WLuaqU-Gd?@xMs6s(x+t+p=I?4e-k1{9mFGOJ zxx_6#QpprKH5N^SyR|9_GM;ZHdIJUZdGbg~b5A;_8(v&Q1<7vZq%XT}-V)f=r1V52 z*)vGytJ+T%377XrZ{O`2UE@rz0j*xRlEvBB zH2>iIM{-QU%CmCKDAn|G(#`3@ zqqr)B*^TjgBO-`0oz|$E!wXL!=FXY1H0+bOnqPHj2EGZ=NOVb&iupa+6xpdmWR+&VmLlnM_`nRz%LmnV z6?2Vze?>nkYrw1efbl1yQ7o-<+&%(f8PaiS5%b=n9Ij0(kM13+)~QdDug02$D{0@X zlUUS1bQTYeV3yw)kxN9abz2nNLP1HQ{f?1z_p(J}5%$0OyHe7Mm9BY#odF2CMJj!7 z7qz6-P%5`#4uO{WRy^c}D`?$QvisJ8HF_&&a?*oW<0cI;$Q)&K^`=y56fY=xzI=1% z12MA^AV0CgRCL)9ZjnuUiU`8yaOLy?luq+m0UQ;~>us0{bo|JP2HuQ(&9@>IZLr#D zv5?5hkI0pk0rbnLy+z{imB-XL`IvKTdi*vA?-OY=BC4pf4<)q`;Yp~r*%eV!+@Xk_ zfPC?W1Ck~Q5P9)XQPNGO$MqPP(l|Ra1f`paL%r~~jRk0wz>U4S@~OQj5mV9yifEMF zL8T;i2T0G0E_mmXqYco8WLJY(JN~&H0Nh;anP(b)BsQ0qsx_dg;!{O&eX_wg?4>q3 zWlk4n-f0UH?#r$k(zL;Q!v|z#*RPW^%0cFAqDfcK!97s_Hx!_)x!`1MnLt3nHUW)y zc;2mwTc>2nu(1QRJk3=}lZ?lhYV6vuX#%m-l>ZYOz>&;Ag39l&yqpAq1K(G0qvQRf z8o9xWAeQ!V^LEwfiaGK!vp;f>CdUxc1g|7N4_NGyn2Ghl_g$N15yo;}v_GOzYlAxn>Q| zZS}IjN5|(5Uk)x0;#U4ohkVlq0&W1ct}^+yGxt|o^(Pn!b*Mifn9&KiEj*Ue~@ zY}4T)3BuYPS}}#*QcR|3v1U7s%7|oi7iS>h4XN$ps%@+ZIoOE6tBH#=wCO27nK^GK zo0Uq1;Gsql)3QyMf~(AlSr8*RiGH@B+g3&Y>4A86qNoSB_=P|lLcs0+5rs^=l-k3$ z(`wrNvS*M3Zlwv9{wz+R4lhc2u-OzbCq9v|`KEEadZQ`)i!fFtH$$VSYr&tA*0RaT zyGKrg#^@V4uon5<$J&zNzEI)gp=QW@43~*BJ6uPFJojU3k`3a2C&zP_qB@&q78xMs z3u930M^t^YBtFCgV+ySqK$7;3_I_7E|K}uF$7bR!f{ibq0+?UpMtus+#|h39(}?C) zBN=92UNGASOox{ZHrB#;-?eOZ`rdkqobw4^If2~!|7F(Y(_RB&EJW_tx9V0L;b&4l zcFS1Q4CN&Mje4UUa_TdYrU`;>0Q#(d%|7>JcO|+t{q1_(NJ_Lk3qLij+VDg*<{#E7 zm?%syPA~XEMJ&HdkH$-fn=;)>4VYO8KsSx_xIF3Hu7rZo#N?D;9ci$zF-TQWlYO8K zWbM~LSEcR0KtaX&{?XO9T2M{IGq=S`!>3Dt_-Sq!5;YJ;* z&*cqfoEL$zXQ?ckuZ+g`;;6`Z?@@NU z&(Lk&gUx@3%_lY4EoKwfN>RBn6(r)+C+<_59EnGbj!eJ-^GbQ?uU>?1DC5jFL6}(V zupz-j_hV4v1yH@)d&m4?w;7Fwn?@T2`SL2(l|$$|fp5kJcq_#4L+;r;?CGGePKXz{ z!Ol1-;yzXES%A){GGmq+=_A%FP|+L1Hm~EUr-`SP6c=R5QtvY=OpxPWM0jE5D|q!& ze-`7#8}VDjg=nU%+u3312N6}P&|UHO&Rse2jXfyeNRw@7_Y>bVAaEmlF&S1vA4vlp z1x)7UW@!RI0B4b^r0rOotW*)feLOQ2s}`JH0rpvivBZ~a4qCX*{#-{_$Z(% z+E{Jtp?bUrG2Zn3Ne5w2#lLZu1r!OXN8S-cf{HprD29VZTs_msV2k?fW{(Z=$*<$d zsMEyf^KJ#fL&Gq+c36zpHotR;4sVy?AwjBeB?O!BBZ?0 zII*%P>txcy8=I|tvEM|iew&XoZNL$0WGmUqew0mmfs&u|n9{PCVXvXSlC^Gw;MGK1 z2NsPO>8bmS(^ptub`L|gzFMVuXG?4wPI?F--X9lhe2x_pH1Z2b7yP}M=ZNtUyFI)S z+Cc{G<~^~L!Z{tOz)iZFGhH?gjt+l0DqqL;1bVE7#TIRtLB?;O&tCpIUL3urIp=f6 zdS~a{uJLyFb+esKK$Y-l$F|73dR)sA(L+dN*nH6DpQkAqIeNcNFX+Kj`5K+!CZnNb zIKKwIz+XPa^@gn{kI%HmgfxK&)&&_ zuFGw6o;16HN&MZh1f;EVxfugHc30#(R>+Px#+bB-yyw(P19D3t(q3=r81fBOR{^)V z2y;sx;SUjiGHe1AVxS;M`6A%SC_T$kvGkr|W?M?sh2dr64U^?vQi|7u@u#PG^Anu% z)q|Sf?3#NhJ&D)Rm_8Cv%4cG4U@;|g>g{L zZl8}xt{&f0bOmf`o}o0?l}}$^F3X4}Z}U^m4|{nI_B8nTj%{zg6d#RGMB2*t)w>p~ zi@$FS%s(Vms+|KhL%Xu}f30z__7WY^4PG1@aPAh(?ntDk#^Mz_`KL9CH@~fqf;_M6 zB+!#A0fvp+3;NTg3~xat&PjSDl!RC^8!a21`fvT0kLFpVKrd`(zfi2)Sql*+@6;na zBlx)(laBvN#A5pFy3JN`OXoJs=Xsd zPH>APn#tu12Ifuw7k|8~r>8Yoj8~or1oSa?TO!^rY%{}t3JCaXo_h#V3-@KWDu+7D z!%2R@+&4b#4aK#dW<~?MNQ3ob7?}$IXy@C8aSm@Gvjl#_ANVY+uF}NH;<^p|`sy<| zHn%t$7_efSmbb7CZZ0Ru6?EY?Iy#&dZhs&PE;yWLKU zRhEd!)cIjv^ASA`C>XG-8(|_Hck16NmCeJ=J!7cS#A=7wc2f358UjB2tbE4N+7*~C zC|`Xhszq?KfX-?n$z9QPFpydkkp^-E9kfl<_>#j6&deC6e=i)B$M;K+v5V-R$$Z3Y zpW)~TI8xyhnHWO9RSBBDfL4gUEX+= z7*^&t>5fS^u%X2mgNnzdWLFB%N9$kUf8}y}Z18nPO_!4h&BY4wnX}N7ZIo)&=_-fLS&z=HIB=S2}%QJ+O3jj|FT`!gu-R z`_e3zN#*3BOSHXNLV`AEW?#sXdJpULUC){|P)ZtK(QZ_U7}$SaW%m69NLAy0W%VEF zZLe#(Jw`hU7am1xns4ms*heaw@)|8miz>Hc=f=Pj4tj~pHf%B<-!>j|_4_GymFvScXk_O#^{C^Shi#_SwsJVHGHm91HFtaU7u>4(Hc;V9%EOZY7UQ;C&R&I&5XQ; zc88s?={yCpo@Imnx>D5zoc4{58TcYPb}a3FGgRJ`R zC(Zv?l0!Sb0Rvt>3p@y2@FAW*vTczE5&duaj`%yCaj}oG!^|E3Ue-G320!4H>fCu| zd9NI+FnGeK7z!1w*v60DPY9AmSWUY~_>Q7L?~&^YAT*C$YPy(N*_N)<;X4ib&A+&X zL%)yer@R906MBhyHJ@rUU=sM@ONln8zSh!CZcMNY^f4_q*@#DWEllS9$x^GA8XMJ0 zHEqyr;2mbLXEkLE23p5Ysh206n@MUQ@}yee@D;W26)TFIEz zgM5h?CZ60522{tydc*dVpzdvYMbdd}N-X(R@r$LqJaBu@P6wuv-Dg3auQ&F6I(GvM z|4=qQ%4e0czkmuh^NSPjQ}%+=sIL?9f6#D4HhIL)3u_LKNi5y?$IWna!a7w7BG^ z;4&-X`*0&^#YX*1a(7|#5~DG#ampJ-F`v)Zs;G|a{5aA_&QM-U1Nf&|Vx1(KmThq( z8MJoP`fX6NQ7mVS^p(?q;OBbrMlc@!y~ONrR&oPxaqcwSJe6mWgpUpr?25HN_;1W~ zgl8w~X86 z4kyHEcQ)#w7tMEm#K+msV*?j|uE|;d2#&4Iof}D*ml&7W$-9oEWgt=yH*6^FE-fx z?fDNzoMf=NH;n4NP6U+4Y;N@@mf7?i37FbdgF_`xA4boxt$8cMAuD|7XhVsCal_Vi z!wJUca$GdPHC(}TWN_ng*y#PNt?LFvZnVOCb|~V4kvcziY|HHZ zZ4oD87JvprZU=`Q!|1r=^GoC{aZ*XN`3cwWA}bwQV-MqxPKa1#y08_u zX-~AC5lGf;`YI;R=$q1r8KP!^_j34!uSUI#A55&az##>wEe{LpBi>u zy#7h0Z+20Cx}Z|=nUGA}$@himCnM=HdJs?(HP!`Nf`?ZgPGG`!g{57mGTN)Rts19I z>)xT_e+%Lj2j<0OGwo;10uMX?Kes+_Oociz-;)T>DwCw&pvp0Jv=Bv|1qcW6`^@2j~6; z{B*7xD36D5^>L;Z(l$=CCuGAU?BHj3XNa(+<^blSU`aJbOXL~1qHXhEDEt%7>WSMO ztWDOWXZn6sOV0!bc=Wf3j_UHmKihr)n&@gzNHq!$>q|%L$-bdCB67Xc&!qdzjp>#i zX`WIu@2ZR%uVtUgkP-i}%_{L@FwGo=xCP|2PJ7DgK5$@z@JcDH;SiYW=_aVKwdN-a zvsIh@4X^b7K@H3Tq&F5Knh-P3i3o9ka1ru*_d%a3wD5tav#bB9m86+@4?)SQf))Q5 zqmt< z?9unb6D>YZK)1on7q);2Ei zE0|#&Xs8tbwFSufl%_cq|NCWMY*^&(d(>G+w>tCGG#168=Alx|6Pu|*fO;mai**!) zua~Eq?Ks=c(=9R!kVNrwRx2sQg8hK+#ygA&fbAY%RS&Mc>chg;-nrT2aJhG`VNU5t zSHaMunS#V0JAA{WpXc4Gx)iztf4lXK*lx6cm7Y{z#~S!PL`8&A-;DvZgBqduTMOTv z$z=hVz`b1q8Qbp)^1kz;bAvsQq`F$jN#is4zRreZF>W^h@qy@LKF=d~>gzw~yA$@w z!1Ab%An`@Szdd_Et07|{1pEdrT#bvZK88v1FgF*y;RCbXu5RQcdMvl~dK|+AAgcmF zG9bE6LJ--Lv1nM^pSR&nRJ0S=XW4aqeRthoIUt$)*`}CiWqn+6Nku808W1+{V%W-= z{=_Qep7)wd_278(ueby?m(rN;1=OF$5NY^c?qp&&j9d9&$2ak$WbOXS=H!jvI)!{3ExwD$4Wi_u*K8#=aMy?4@!*)QVc5U0ey$%?{FZ&(TV=G> zW%`ijo6wNcF>na7sw|y2ns=skpW__Rz;S$|0*Ffm`>^K|?=Rg}8Czl3r+V{uYkR=A zr)qmbHowOX-tisS*3%1;t@ouoY_Z$^6h<1|`b5_sr2(W55#7PXj1*gFUzi*PRjCl7 zjI>~L?HX|f3+Nz!iIqqJzVI+YZKiLoUs|6Zwqo2m$w|H|U^TKP$>Ek(ox@&Ht@*~b z6W?vx$)Kk8?Q)y0?6f1UMFQ!2VxOLHuxUv(a$hOpLve4@ajaA56~W0c~ymBFYJ{2N>|3&i{@fN6SAvkb6#p)^uCm+f1$s!tO7qySd3){w907wY9*tQR@SpvG*T9g!-6S?J~RT|OklXdEiDi!@qUq9=T>|FI5i(DUfp^rfuU&2|4SQ> zPOqYw?oteN!u)=64f`a3!D<)^v@4>!ON3p?;4KV1nUExd7WHht_H%_{T zdf(i87rwYc(2Ai*?gOoAx}W(xK}YlDW^E99gJzOe4pVgb@`xQ28Wl`V z_^|0?8@AG&zITq^%+4hDTuLd4SYx`pO;3~}j(V=a7L-%`nvAy8M_YUXdz~Pm>cCcc zs}y4i!OB@*#bulccLdMw)rZq$#}CtnOr3|;)pOF5<0po^8d+vnFdtXdjiNF(cdj4RWvn#D|^- z(9I}sWxzWRrw-rxl#00v9(1K`nTrd@X+&8JWp^dgX}V1uCb*0GnclV2cDB)K3jaS} z?-kW#`iAeypfE~_iV8@Hih_;;B2q(=aV(63z|1fTNC{1%h8`f46cquHCZcr6IE?ah zq@#2QHGq_eD4hgIBw#`+g!JtE*V_AF?}L4sqhzf&-?!f9x$o<8H)laT3H>Boo z$zj+5hg#92YHKa+sAj!D*5ttE{4QODWeBl4-vuu1V6P*2xOI&R&o|xA!8bU^LmtB4 z)CB#^5B_+URW}okJaTt1<{r?f7l3_$!G3Viz`tiZ{>6WKUmd~#Pura&Znv=0b)sgN z3!6qKUHArq?JP54m9_+T8tacNmO_g5^sKN;^XT2Grg_Y0TURhAy_p zYLMZi%*01?J{ctL+t~xA13t{%p5{j3jP9K*JB~3pBf3O)i#L|BZ0xIXb}JPr^qBo- zIebui{fPVWnZ*podXN1Qz^4=moPVd)lylnlux``!=zkzjUnjw&{r#%h)a3M$r>TN* zD%L%rXmMm98GKgsbUF9N$`$?Cci6^98{T%?k(lDHt08Or4Ak(Rh!tOO{fw`XCBT1V ziEn>%p@bp(g*S!(82BaIH>y23JHEXg#=JxfYrUeG6B!Vs95!S7^91hXt1&#|QXP}K zNGlG9qpj~|{G07RIAr0A_&&>}Mue3mWeG&F7(HsqR9>+=TH5MvYBsV;CcG+0`q1-M z8r8LEXnGfnwloUIgS`pC8usw=#B2ajJ@%Sftj!J*1>vKKg|RhGaus#z;0M5|K^J(nlG3|#0cHA?|oZ-P?3p=U4wJAHLIPBSEkz# zx#1I#P}zxh4)2ziM!FmjAD5b@bcc`q~utw zwtMe+pHhbl7b3~DYM)aDW+sl?ZSvl`)b&Es=2ZRGbuhAU@W{rHba$;+x9}w1Dw)e% z)EcYndDwmWornf&8uvTTuqYAgbw8;4dSn&`-%B+!gWEBiQrEuR*S;<;Y03+7jZfEg zN}mc)zb0EN^{gGMY3r=mpIDs?qZ$O>G8nuV^$%E>2Rwmme{dX9sLg39A|*?I2s^4? z1G=VUPJ!k=OWKpqA$si9BDR~@{!akjs2bDmH1DD}I`W^LCYy#m2j!v*0$XlNTd%(E zig%f{gWsCd#X0`f@8zA%*?WO6xj%v%-)3yCT)_uqv+wjrjnMVYY-adp58Cx*3s6fO zEuR@KK2MnK7J!cV^Z6-!uibc;+1BZ65B7FHh-i0Sv(_J~>lo=^5&z)#o&8084>3N@P6(pSsWs=j>-M)Xh*1gYk#I>#d0gzixG6S4ES1qa<0hc7| z2!1w>%6b=Qpi8}z@Vho_lfrR8sUUO{dv8-lZ_rooAQ%eTH|~RNA}=DE^J5tj!5QOetJD zE%WLwwSGDO5cbt_nrIaJQ^7sAQeD=J_xq?U;Zylc@ko9&aBf3aByd|MzkQokVU!fL zlp1rth4q^>eQKI1bVvvN!G?hg`S4 zUN<@+Xd=6%oj+^J=xQZBHSd`$YdaC?l%p%0T60Zg(Y8k%PTJb+d|UpH7zUu2l4rWt zCc46)h}c*w`ssGvl`!eah)rLyi*-SunYbUeHBrV|7#On8V)q1x<&~8vrA^K5VO<~| zzu3hT=SZt`bQXP&^Z#ab#+fphfbBmzm!Te@_a=IeL7CSd;Q5*4P%`qu&_eWzU)xal z+P=XNB(xyD$E3PA`JsA}EqhQ6GhLQ9&L*jU$`fW<4BwsJwf(T$)3}-0L?r}#!M^t{ zwVP3eb-}k%G8R7Q8pDk}W%*{==F~s|eW%R)B%Yj?7+3!Wha|>tHi3&PO$;ddrDJox zRtWwAbiaf)x;!g^%p6&Kh@2g!9VK=l>rVI=f&VujzSNvNp5%<&IkW^+sE!78jifst z$uHE`rjuPZ`<<SZPuGtz+8_-NBkq?*QkG~tSasxKCWcr&rpM_QXb&(|G^(3W*c4gYyvdX&l zlfN$wlyFq6QjaSlFjlS{u-=1oXGH1QxZdi-|P`0f1*%Zem8Jrf>|D+R~e7h0A{ z_kXnVeCiSP2=!tR7hk)R=)#^EvOJx_axznU2$mNDr)G)~O^`{9r}~QVg?qI{i{&rk z4(SaXA0Ll7PW{E}(arX6??T-nrEfs(%8uu1A{WJH0}W3{mJemiljx)HCW~g)$@v;k zg&WH~)Ou@CV$OE&TJlJ6#0)I0k{v7`=|uH2qdhGXbF)8PVmC{~W5}^$jrd(>WB*5r znnx?UiKcb=(R$^}H_&HqZP)7Qd+uP@bt}q7L(IOLroV)NoE|5K*53IRo$DTujBLKz z38D*Tf9L;1Mn6Xwvoqcf_=R02T#L8H|IJ!^-m!WTcZ+{xr90~(i1Ok8dBi?Q^>czD*Zrj6%<9X z)PTUJEa4Uygaif2g3_s&+W{k$a*yq|n-%!Z2wYUudxg5Ldt?i4h=7#xS6A!^YYmbX zX}Kaw;SS;cBO@fx@-~H%1t?m%=ZDH`QQ=Tt4k`?<=qEHQhIS|fi3k9JD@K8dEID4$ z3k)M9P%s^A5KhjS0VV__({tsz5*ThdRle1#jIGNCHGv|KJFoDH zObJZPNpE3y$}$$X5?i)R;DfJMqyjM8@)e4NNbSZf$x%>#u`9)&Z>M-E`#(~G1xgpA zb_s+PLKhU@hamBbRimX4=|BgiOSYC`$Hei#q*(;L1tJ>R)WS(9WCdrTjttpeP)OvF zT`aDIfLS;!p|r-vD|Facv7C$(Qp913hqx`uokeKUj*WzXTAAP6#xW?fidhi8T`}!r z+@dJn!G4toD}{I4sG=7Tfs!qIoN$qWG#U;3%iGlW?I zR&*&Q%uHBV1&Hl=59uI0K?{165HDp7U8yAqxVe#RNwFeSVW|*CUMT>IUMub?dX&F;gA-R(qBafK4ZbnAIrIKW6^shrqmG~|L z!R(2gz)A}Lja{m94kcgyxk+A+A0k?smL~m{iK@h>TLWh@;idDrp z2)knq1GY^LMng})1pN_UehP|QEYBZoxN!kuFHWH-2$daFc^Gq_oUCZVL4qN2c^oBa z)V$$ca2Ly1T%!nAOtU5V$?OmkP0QK-24RHb2Nl4BQBmSxHX6>M8HFTQqdL19t|y!X z;v_3A1WbSO7AJ!uoL}FszWjTmu#!Z$&-cen2_k*l->>lOa3oy)(DnMuZjc>g9-JV| z#LjkE^)<<#5q3nmHDqzP!qjJJNoJxD83Plw0gabtgHX&OU=WD;&WXDc-c3z3jFnc% zl9hko!^~%3>4qidzH07rvh+0x8b=CLT0XY2YpA`!z{6$h24y{Zat9V)8`@)E-e5_Z z$`=eDEu2z%ZkAOI{PKp(aQwlQZM^)gUkKIj{3SrhTfz`#0W)Mku3xckgC za6cQ!cZyNtNUCk;L;9><&L_1Fj)fF)qrTS`Jpp_6 z62hAb8^Cdb%ET#|%8q^tZul@UN~}CVFMG|wib*;Y2o+G-NhLe*ENPYPdYYzmqHx(> zoNj}WP@!T*vhQCo-lMPb$>U3X2C}S3ZUqsra}^$|U;!1#q1L1e7}mzV+7a#CdYkxK zE#AXiqJu7~72updWQn7hthiA59eNv8tTwawYJ98wD`m^Vz8~5fx0q2bObZ!>}l0pf@q4m!#dpG{$Q)FT- z7=_=1DWbAkCtHsY*aH{0(%nVBcc&>{SxT=!V^dAGQlR2>OU3Ku=i{qCb5CRXjU7I} zVn53z7$KcQdare6j<5JMN0f=LK*LJ1pPJ9==WvbK2NArT*eC4h7bqbJz4Ci_<`kMF z99E}r{FpgR@L5)>2YTFg3)pOCQF)g8y8D@>B;8M*vSf_>op~K3Jw`rgkX&mu_{Y{| zfbdD}ii*IjApJVe+Lrg=0Q-1w{7<2eVUQQDyuD0g?YVbc1K=3HYipeKVNcU2+^k@V zO>Lg_6#4)+GBn?xHSI+O-R52_e{vZ|GlJQ51mj!h>G>KNO(PFnS5He_JkfRw)DGHTQUAFX{X4X}rYU~{ohcAN zrJ0IgUTTJAmy1MvYQoAb$r>APAHA=5*~zQ_QmZcMd5hP8{cus&XETFz*~qByir|dx zHQ1|+@Geco9h-97f%PDG6ewu%>opz9>l zIIt`g7d&I;JLJBuQ@p`m?m?~3g0`mYb2m>lbfS{g?CLdIr_|+{#vG+mHa*r;2K%7v z&M;W42e1REL8{@Id${kgPoWzt9c1CZ13yUZuz8-J48VN5^8t(X&K9j-I=D~qVMYF& z6t7!lmFD)Qzzg(rkwLqo;OmM?>;B;N{8`l) zQ~p2E2S#`wXtlf_AWI$6D%l8d?wC(+qHst!vUh0Y)j&)L4N0w3O&F>R=_RkLg>u9B zjfI0|{bwy`g!g+>n742PPO;a?H0WipMe{o#r|xXFkqNdq*)EbLX?Ab%7Jfpr7qUAuVeLmZMq^I|mpM`B*&B|UFL`SJd zcAIrSy59N#8oWa7)b;<6JIb+iOH~#0koxw*RCA@3_CCy4uq`(T@iEX5hO`hFLsCvP z0DXl<(}52*rz8W{#b*Qqx>9S4RZB*S#fZ(pg=dmRZvT9bkv^PmUt!phredo?WtQT5 zXI25>8D`vENPe+&kJ(B<>nXxvU4_7DA@*Kl18zbM3=Kc0vrwX!qrFn> zPJD-w_u!tx(dylEdwfC{TAz*$SJXVyi13N-i6A=TnWK8MgCAcv7N|gJSB8kk>B~)9 zhp|HQ3;ll(E#IVCCLA}5Rz9_nYaa&wIG-z7SA__xC%EKiqyZzLYVF)EOvc>EGo0Nm z?Bk_8bBirw^#Aa!PvqZL6Y#j+Gxeuh`X4GgEnon=JU7X86}*`Q1f4`Rt2uA|MyY^% z5K~gdx|SDKAXcV}Pv1rvVfcPgqeBgsxjJq~3O}iri)%?EWm4Hzb$3Q;RvvjIt7df{ z#1lGO>9tRtXUBYG7+AcSakaD1&7cq)J^#rVlTn_fj~BiLCz;s^GD-ukE-*D9ftu{R zbZ+lkh)}ybrOUgDV|?LXk0dUH`*Aq0Cdo{prYhCM@~d|yk0+KO|E3p1Qx)8)grVg1 zPszeP;iBBlm6!`D$qu{bJ{ol4$9oRD1$3Psew@#1)bkO^fb?ZiL0t&`?;E;7>QQ%I#GBux#nDP?tr@$lE9&zmaPr^F? zh9TUdd>9Vg!>uV}+%tqr#`DxEVG`z5@MaUBVQ=TNuG!r!z4>06xE0mJ17S5bX=BCD zfzVO%*wUnq(zm{Gc2Q_ypxe@`pgo1a-NnJ3L6Pl!m(Zq^&E#=@Bx$i|c1beZD9F-y zvwp?+gxe?OCNW`TH=b`5auR#dSZ0GQT4j$EbaO$z41glTZtz&hjiC@r)+7+E3L*wr zJaBryC%v4qF0}<2P@YhQZUU;=jqe?8`;riH?mX=-@tFLzx+3lJrd|hb^j^RoSkFbx z26)ka)m5*>tMsr-J|TNt&}prMi+d9rZ1j2xV~OfF%KCKqr3qJHQCiqnh*l0B^;~w_ z6Et9ea68RCNKQM&hl$}3l_ZeAC;@GcDnp1TG)assCFF)1foHlBF%Qa$N z#eRsX5AblZM|?xbPuZp^q~ntJm$bv~P9GXRXnH@l9L050#tX*id0KcPsmHqGAdY&Y zxmVdQ1czxk^K&Z>EF|l)ZX;ZaaVP8=&5(|EmMXBdv$L=C9!6SQboS_G=#0&+TT)CZ zrq&Q+wN6#zz@?7u=+$dUe_w9avg29%(lD_()*to+6EC^@sV2Dv{e?NKYUAGx;>Vod zpoCnok}=c&_LM&6hi)D&7`U-DEPrnnPqSRAI(&gf&F2yb04SY z%4he<&C*v@vy9$s^a!I>MDY=PH1BA_aZCH#a$Bwr>z}DPACvUs9=wNhABK)G{=?2h z`rfc@-E}Q`|6H$=+ZRBqb%9Oya4Kfi3>|?M{|F?{m036)1btGIOv4=S8NXnntM#vQCgM+ZQJtuE2j%;F26xoui+6*i}XrKb-+-DK#J zLn33S#Q;CX&YW|w>@xbkMH(#laz$W@+#>WVbn(hsnEKF19fScLC3h=CyYUMvX*$B) zp&wi=mrS;3uj!IU8^szcu-U_98H#`&wwUFB?sLo5iqvaq{UCW+s|kp8rCF_9L?5&5 zk!}67#?wyxvH1BZ=KIc5-!StAeJTRm($wib}t)q52GOHP<-_W+dnBkW1O(5RE;VA~UUKeKiu zVA_dm84-e@opuvoXRWj`g8=)KaG`c_<_xBaD;14lV$GM|7E|6JV%FdRDry`^MZfu) z>iTtGhD_||TmBTlQ36YL5aU+98LCX5hJya|Px?j^NpAdCmTneTnpdl8?FivM{PP-Ge~kVMV=?O8I*Bar|5fX+!_kvP;FDjvyV(K}ydL-tk&;-=N!=kp6NPhmW_@ z4XgQc_}dKwlh+sL?PaVetI~22rYHa!>+UWLS*u-Fw?mn4=p+KM<+4AesfPP!CXb(c zatcF1fh?na*6t19!hZH$LvALhm-b$o@7t6X?lkUk)b(&b=f40QmL9_nAXae~F34-4 z#+#BImpSL_?$VzK!JXuOi|O|G?o6{`pnE+6eaHWi|JhJ9PeGPqABJGX3`B^*hgR%?mA4)^H{E#1nezDJGYv-m%DSY@Jm3cMl;({c_>Q-KWS-$V z5KO&1m|H*QHXx1#0A7w34_Jlt7%MNe%hzg;M+7l~_Tk`_h^3#R&&m5T=2LX^21dn7 zu+6o>l;Ba$d4@79kRX6i3|P!ENygnkKMf{ z$#wrWyp5lH?@+#V?RP0n{gFrTL6=G3x!ZNO7mrfTkxqKVaKL&TKU>Q7qq@;WBmIwi z0{zC$sZCZyKL_LtB340;y$CT0xNeC*eyz$w&m^+D1ZZZ>-5iUad}$atoj%I&48dmP zelpD|1E9-q?o<^6A9!E}LE@)S&#)_ijKINX^P!iw25zlf12M0SRT(i3b^|hr zdkUF*BR`qVp|ZE8ym61Bg@-)Wp~dkPGU5Pn^^`(X8o%tE6Dg%-(6`FNtK)ivF41T3 z*RHjukwwS2?{}u~v&uVBVr!tTP|bA92>0nu@QT@MupBAa-=E^8FPJ+FYz`_IGk8+B z(qF`_Mxnsp<-*^~R4K7f`lD(Zj)Dl+iA*2#dZI)8%zEU~(e$PEUQ_G?YtgBkBM1~l zf-&emgfJIf!AZ3i4(MhW41WZ1U*V=yWFipr2>{ex87|njVGZiB^K)-ML>aEiJibn( zpYM&VnFY`KBqs(_&i|~&>zf-=#kCf6X5Zl*VgIX6B4GpDAPeak6Gp}Z(7Y~%qb0v& z059%y+qzo)Gke}o&Wc)#iQ1o7Us>yk4l9+FobQilXIC*o!z$cH$X)?c*34a@8$_gE zm5c8Y!=oU>Ne8CLfH}#JI?!S5&pm_7Y8f-v2Wi9WLdMNrc)}VYnYg?g*6KvNh69aT z9j$wu8MVx#qHFRBlvH@Y~DLNiRdk~ddI#A@AZp2^zuR zbf;K0>~?CnERXm$!cAuytZ_8E8Un3$mCm4q#PTS(Z>%Z#Hsi&L9s4uf*VR+1oBS`Z zI}>mc$Hoh)G* zcJ~kIJZ`tM<*Iv3-}aVwm%roVM7{;Hpyo;9`m=EJM4AMB!tEGNR?^j4?&I{tO*-Dn zQIR%W3g>gzoU$KA3^c92@qTy2Y)qt)cv8}nIhwBCv1-))(SFb>=7iw`{7PDPbhg() z`G9y!!>;~+fYuw5uv@T>s#RWup!*4Td9ZV@q!KAiOLQdO9EBRzc3u<*f$Dz&u3q-4 zwBG!zVjMIh1~7=1bwmC!qQH^jx32AvD!uFA>hqt~78xlN(J|iONSN~TtuzEmAt zx4p{X0Op#49M{Jx4=egU@fi}i`cN^Ijb`&M_x58^5Tb9WR#r@jtD9+QU$J_gaw zam`I9vkAyINq8yasmE61R;}YN(C<5eq^E69Ugk~e+CONqiOd(m1^m(~u2GvdT8s4Y zuyCUo$8)HKqoTr{h&S;uwqj*W#YFs4!l~Q3=DaqP2(HfA%HfsV8~=k{n=E5c99G|M06^q86C)lX`iAzvlnk#ZTQw{Cqg6x>-gF zhI(zFjxbAe?n2wMmu!7iLJD-)LlXO)-Gq8XnzZ$Kkj{Du`Yr3uVn`))+GMU8!bld0 zdGnD8jt)&lzm`IR;bDH&yM10xrjC7MMDE1yblR3a3dDFxfYv*$6M{QTvAN}b9d47b zOe>Buaf#<%)-e6Nc`m(cf;rIg)Mi-&GIzQ>Lw7rxN$S{soyaVu`V6MaCjfVIr5k&i zqYsr+k9PmUyc7|8tnf=D@1S~IHcgO4>EN1?LrTxPL72r^vnbMD=yUt1i4QCFaki%+ zdnq65<~|VeUvV4WiN>S>^g9(Vx{9qW2Pj!}Y55b@enyGU86}zEFS@)Ggt#F`6DPcF z*g9cD^xwdQ3|nxI`8xfjUS-8NBIM_WxC z2Na#uvF~|@eipw~Zh6e3=w3JSy@}=Gm>N2V#Chi6++GuTkt8V?7*Ei%?D=u7?^V$Q zh@&&+W>Aa+g0vfA$JkC#i3l(zo7U+dVkyHPdfSD7x;%9ZcSYVmRUcN?qmq79AD^lbUx^QLNjSpq z*D@SCIyqliBKlIxM@2qHA$&C7S%xj>4K$>@^zNq)t)Wj9_hkq~`7A}UN7w;C!nGTV zqPeD#Gg5N3PP zzC7N{{Y|vFXMuKt6R90Mor`FKUr=sE!L7p`qDQO)1T5p|@E<`>CNg5(t&D`Vg5?L8 zQGIDC3qw@zGxt{x2h67BUk2i&z_xx>BhF=|qJxPZkyx8XKW<(=Sl65BbWoeS$Kcu7 zWmWN!%BD+>`IZg$yn-%shHGav9E3)cgd?FFH}OeCiTMauc&LGXJC|Jd;bnOFh|?G6 z?#zhU{iPf??nS~QcTT{}PmusfmZ!9FH6`FN&i0M>RAe=;N>GmIsE=cRg#8M6Z7xeQ zxzKATJ>9wL$)*7UiX!oRcj9BvH&((Gxw=l+O#|`u=CEJOcEjYsy65l7^TW0$B^zlu z8%BVh>pS5vJUBh!D?K4_x@DuW9sE-CSK~4Yo#xq<8L_OyoH)jX2Q)a`olqap%`A*o z(Ql*~UqwPeo+VR=l@nVGD`rPBX*-+ygN~z3MoZ3=g(d+=S=lAds z@`ItEsw5>vVSR}(_YdU8#6AtNwnoC(>_!=y^SJxdD<`~GQX!IhueYqi{#Mdz%sw3UGGtY_BUI&>Ss$@FsWt_>D)MA0mCjju4gs(tiAtp{>1dlvtyW?i`@_B7eEnz(VTNA+|#(!it!K z_t)Cta=6${KzrLYXFz>`rrQg?yVw|gN! zsl9f#U&Sl!F&j!XpHl|AVGnO=#)08tAxvB1z7+R0uivk^bjSTg;vH@hbN~MuRzez&F)A3YeuV*%K@D&J2y|LKYddN& z2ZF#2VlP4-yM70Fuj9)CzV9irepoXBhq;|Q)y=f1RjL!8NnnBFg+tA^mqHLVNbIcgI+1t>0XRQ-mcuweu@0W}jh?I7? zeCZ#MOv3Q#T-5L{UY=Ht>SQWQEhW+g+JETBWEbq-=%$XZAq_j)u^)K9KpymBI7~`jO z+Al}QnN@?w$zQ?iH6E9S2Ja$tdy9(xBCL;sW(kJZ&Sg_(kKh~8{y}>Um-t{j>1bXz z-!;ft`X41^%DU7uHCJdHAD~(PplLfr(PM}HhyKPbPhMwJFt8VGSl*~B+%h!S%Cc&> z+W4JZHS`hLWU6Vfgv1mgwbR87g8SDB?B7TnCr-H+Bfl>3u~%1d*oQ_OE1{lZIaYot zIhKN^mV~bnr=^IXU-kA=mXA}bG`RUi{^-96x+QQwu!q#@) z;;-y*u8`Nfj~4n(VEu!)BlVP{yl}?XMZ+=b1Q#*SE5>FHMs3aK=j)|_pE!ul#{?&f zPpsA8G2W{Pv6Tg-*Qhl&C&0(;B-Tn>-k-c(N?9-duaXuV_NjHSk=Q24J>{R8$TeQH zo9K!wMZQ~gZp?EQVg$!BVC}g7Z2!Ysr`vpR;5@O)(+5A%j}~ymW%y}3!%!#AjohWm zuU)Z1ed9shm@_=SgVE~6&fV#d$fbcHCcUiEs%bA~=nwSy;>@Owx^J(B_n}sk8sRWz zw8*GvT%Igf@i$9LIjd)C2GPnLPMrHOIiN*8G2Nm24K?e%aTlK4yAYe#z?R^q`ww zceAzv)*b|8)unFszCh0rz`ACG69~w_COg#uPWkecL&bFaMSL zW|28deT#AR+Q5W<-*Xdp0GTQOs4Ld^*`r19nz)H486))Se<|Pi^$zs+w7<3n)SuDk zfqAme|3{fLND%2$%bPlbJmc@7>r?sdw?5lM#xEeVtK6r5Uj8G@29D*bI59d<%K?Ta z{{1!R$9gH7W4B*eam<8X+GEY{RZKz{9SD{c{|ND-haxCDG_Bm7f! zld)e*%-VqdJGaP~-^C>u(L0M9VY}R$bg1n1FXb8Ow?QJFt)yA4GOBT+;v{=jy(<@L zL;p!_Ix^bW!p>6FgLxfC(>kx}jBtar>2)h@P@%qC1E%1Pwe{!xIKoGr#aH)v1}7iYzz)Gw_hH zdR0nKT5|QWmI2gq>mtr2R_su24x0^`ocVT6Ov|mOKDaInxIcVhUWAkOo}cRCL1|Aw@a*52yYM>idM!HwW~4{3 zlkW0XKMx|Co{k8mZu z?u{;+FMInT!z4?EnBA51Q9hK+2;Tm}; z?oJ4o5j$~GwaV^tb;f(LNpnm4kDZ$zqNPDzBTUm z+!%lSIZOc=7jA?{$aGdY`e#u`4TsT3G=;i$t?kMpG`W7MwFIA%47`YCCw{J96}Q#>v!T0 zY~oiNg*zOgsh}Q8+Nh|kP#sbYdk!meRLl>eLe8lJHTBHqIJsdNzy~n;{;FrlGe9HS8bZA+~f>)R!-$(xypk&jvxMG4FM&2Wvog`FsNql;UlBjZ9Ok zLWce_F$(Cl11BGWpi!$wc{`E;^_pZ62NDf2j-3`#wvDjYPb(Vcaw0G=m(J?gYmEw00VT+A#ws{O6M{t_j~PdpGE zVs`Bz=E93y1c@$g71*4N7hvpJ6`z_AP~xSSKPiLA*gY-v8FrkdFCXDE?^w5KTDd`g zpP~#b5y3XD!iI zi`q<0j^pXzsxHe0+hW7dZS}d$YAh`K3|Hk5gY zR1X@OW_39v9tx{Hx;YnoO*2+_tQG8=qdVUJ$*OZoR+d)_CWJEd>-ilY=rPJKNFOup z%%~gdpMd<5kgs0r@L>SVVM**iNwQMHhRbt=7wxG~+HRexdsWGLb^dWm_`Ku@o%WYhIMu~LoqF=EM(&nn*E^CWF!t_lvGv0;f+>iRKOunB4l!AM=oayf%m#2Ta zCI~Ce{t!CWTT73nO89MF`WDGVu%hQuk?!mT?sdM$BpM;;; zbGdC3JmcCDvA2sdw)n;Jw+eQLlB!0c+9vdXo9tZAtz(wbw>JicAZ8@)OSx8|2fi?; z9k%x8>Tc;JU|6=aIz1y;NPc@fXK1jdUS+DO_6dDFPn^{mRS(~)v61+f%QIGPzgr)R z*d&e3gXPbuSpY`K%F@Wo<)rW9nRub4Z|7RdL0DqCf^%qT)hSw^aLK5_b_%m8r@%Gdpz#`aIX0jHvN+0 zv^G=)&j`Cu8^!ky&R2DmDmPo16no+Gw;kpcy$3vC_;Veob?I@*!$9#PyXY(QlVqK+ z)mBz7Sphi5<>4vw)OU42xj^@TMXPg^+h{ntLhZ7!}H@3aaHVx$|`4Pu29BI1m z(s|jE8UMYt;c!wQWxLIj1^3$(5heZdpvtGw&MlaCP65~c1wD22fwP%>mtq@R?>Pi) zh_?Dw)>}9T{LE*1mN_;?*?u@M+)Ei7F({1BN}+@Ry8F#0s~SF5jgKztwwk)Phm6)J z;(~(?VIwCzo&coT;O9CyM=E*I^5GfXqW&)+_QBv+3D5pe5Qd-m)#7ul>An8t*v2H zql)Z^HL-I%X?@|uXl*_rwA9h8qBmH21p$SX7rlm4a{Km#I2XjYpzY$$5dl4h6J;3D zntZTgYZ)vy6L7Pl`vPa&p+|8a2|!dk9{}Ay-%|C4X$YHu10#AIh?#PF7u|5 zESn?x$2Y1(I9}gU2;C@XZ{vYc@dESRLwJ1ieFd?qn9Y?k?|CsaeHtXJ*#mQ=;`!;{ zTjAf;(~>z)$|ljND4r$KE`>WG`Zz7>wA33|pq)hZ)Mtn3fQTe-`>-Fk+euFNRJwP)LWNB!ravIElZ%M= z7-L}qrfXNYypeI+^0#xJI_A4iw^_?RHi>q{UxV8xjk67(<|^CD%%)@#D+l^=s?US1 zemY+09?V{hVDB|*#(l8iC#{?$sMlXhixu@5!NbNoXa1AY0#CM zH_WgbhC3#2t|6}Ck%hwrUZ{&Az5#&HabSnG(HTK5RmwmS-zX76)v3dM2 zAw?jEVZ2OVHEax=ah|3h-DUrT$@w3u!N<{?9%yaGG&4L}$6fxbwf}AGkS(_h+hpzA zANlm{`&_dm({F-mEz(jTgzg#I6~h8Pdc}GjzPb3e<|8Z1QxSdAU=_#7YmNSvumX}% zGfme@KBU;-i_kx?o3z*!O}fPOrniKinI@06w1y2;$$`vF*LM2W%K96=b{8Xd{488H z={2`kBm@yo-a>>gnnjEkHwj+M{)+3Y{LsiQwOL($5ov-De>x)VpD&{XLCL4c>s`ie zL+5R*hrL1bhjFXB+D}l5>jrDq=4PKN=NJp6NX@9dqD0l)UX|oRRL@c7 zc=_wqBhm4QFc^VF0?m#)+zh`zM1W&+VFy90e>Iqw9`b2AV>=AV)@lw+BM*eXJMw$| zQsUsdzH$%ehfLuCp50J?ZcXR8L?h5C9qQK!&V;b7TgjCa$e!zzae`a%&DyFq?#4j_yb%GF2kUK4jmfXIw zQvswVJC6tzjd{3F%}~N|xZ$@8K{GcOr1jebV}qr9^>YTm&YfC(;dtU;xFE0h6_+z< z|J=Nv|E1&05&zPoty>xW;}YsktX}P$ak9-3dBNgJnRZrQ?v{lhS9g7{Tf601#Geg_ zP2NpTwq)dAR0N*1DEZI&Mdy#Xt}-2~^NUn7D$|hR*HK~AX@X5)ZQV%;@)uSHBf(uw z-`@pUZp|f^vOYQ;S2nIN?(lA%+U+Uy?&_7{wi&cwuE#N}!S2Gpx)g-g1VFv%Yri!w zt2I}f_K$Qf;IW&h(fexaCptm{UMV_5x}6c;EN}_FM{2mq_TH%rg*=`3Xu{S2Hke!8 zt>-H<1CS4lUf-i8P0S)N-dkK=!$Ip3ncE%83`GOhGak0=H$ft zC}^Jp5yv|%Y`;tVoLP`lGJIkh>tu=s{5kcLNDSA`>!8WHhe%kQq0&*c zd{=owy|;^UV%Bai{$f#cO2MLiZDL(h3n8U9S^AEVo@bOzS)=tO&q!|LFW=hzW0{cA z-8q$z1Z7ZH?Ko=QqA-C(oaMH0+jqXCBR3OYl9&a&8!O{5zj#` zOy4y4X~!|ovUA)4D0}qbEmy?=X{J-VS-$9uv~&(y9Jv>WD|c{NR73Jkwicd!Qn)p@ zs0=S#AvD9FT&5T(q5)>1S_8*;jL~+n3dyqR&}c{tolu z3r-`uSAFfb@XbE>Z?c6k{;0}Pdf7e;=QlLfp^&ql2YfPMD{qn#&&QNvievti6>9WC za@aEUovO7)xu~MMuJa6kK>DhzA|gAOHBKws(m#@9wseHB&{fivvdfdY&%qd&3VXw< zM7g;>n4DaHff>UZVTv3Nksp!Q^C$A0L!sr)UGMw+LWla9k|T#j{P~m5eBT3rzYtt|jhSC` zE$|iKLET!RB0E~e!{0|z`@{N5tw z<6ZMQ9;J0kBK{mAsLQ02r*MUjx%tuH~bpl4=fOkAk1`bs_8#^U;y><+A7%+*lj9}r@_C0*AgRw9p<>LE+Blq>DEwdH|%+ zK$8f7b@WA5RKFVnkj2IIOqM8C4(*yV+FyeX!^&z+`h*Qq*(n51*a=w0RMa5sULFiE zpH~#rG`dU?;5BRz8YFdRp<%CmH#Dedt zk^wCt=@NQk_+Iu#;LWrYE%E_{8c)c*f&Iwg!R|!$rx4S+#Bxi10-Av6%_P>O(>R|y zfbhZD(B|`|?$hkf-6{|NNzL%zTWY3~BjpspI+D(8Cn ze)^1c$*7%Ocm;PFwrzEg>B7A3Dphn|P4FF<`mDl&^Mwi&C!wF$pG|llGR-(Pkpu{GmCy`0kd93`h;o@N!#k$E zS{5vELaba7zaRGI2(jjhLo-j`DeVz-f1Z>YqU2RDK(7_QxSV*l( z{&;mg`izdX*WvYcdZ+ps!RH%=AChch9VC6p$6#>{@^aO9b!T;FR}IYMD;yX9+b?pV zhEwk369J11^>|>&KFE}8qJADv5m!E$HCAf`gLEIVPu$N3p5!qM>!FA24LADQlE)h16Vl8`W*Hy@J{5e-xUyvtrMy+I34XL?~QBrB;e=T+Mc!}F*O5iPklp} zlNyRbZp-USsxqE-+G)y8DQCc!uGh_==l3~u2Jwg>iOI{i+wKO$NXe#`@~YyY63nz* za71nIs}+gj_xVL1S}9EluvRbQ86QHqawz6xV9g+Thdo49BzJ^DoOq~F5KfV(W{S&_ zpHWNP)=ONxCFur5C?DRMO6fE7dSo6f?`T-}Qn~iRfu`d5G>bOLJ8`Vt6a0sQackes z>=_h>cWS(pY8Kz$<`+$VFk0>=I`Qa8;sdT_?)()I(h(`|J%R6LuU}ZxQ?u=QL%K%AupfXY9$wW@)g|dSV_Q zW2j|tUkGCz+Zn8kteCd5DzZS(G^B|Aml~BPJQ~bL@98WDQs3>*nSX@7u9EHrAPkMIu+!+0D%zW}9D_4;&zou2p!i;@MhmGf<_+}ARv=hO z_2&b)Jms|y*!--$iWy&eDd^)6>D98{au+jC9=vo1Q4jQrP0l)|g0fx)#|im`tk>3S zu4OPEM@{hKrM+yLhHaMo#-ho`&uF_hpw#+0~Tfz(X#m( z)}Dsqr_w|v<|J`i;ofQ$qEhUKa$c7JX4b)eIfOC__L1M%VY$ z{%JH3LCCb`S|J--s=D^~(=<9qOlMn69r8V=gU-e?Z;U?++Gr~2wz`JDq}8FmE{E(U z1^PJL=c_Cjeo!Um>Ehbm2SYMPK5VFGX;0Knekb0$M$2Tj8n>QwWu6g`D#-C)=2?$gY z;f@QqY@uj5ub#mkMNk4l6;~nTIKA{igq2cq5t0>5GigmZ=k;^Jx(Z|G%?r7O^oaca z5<@-8tf!r7Sx?Cm1^#2;g0j#&ma9EjDCYyIvo-Nh1DZP>nuNzFg{BO8NrEOg47pe*cR+X0O13jsOYE9isM0%=x7 zzIV8THcWej*p|>BPY6lH{9ysdOCi?00;MgXSs)Sl`Fy`;heJ|XZ(#YuWut0dMYv9| z^?m+EOz&a#FapOWp_9=yYUiq*47}+IkNJ`x@^TKxrHwS|8yW{-l~m$#Zf{dqh`+X< zaEq_cm-eI5Jn{Vr=o|-m9y6$61~B-NKjpUjP&IXy=@zixY4Rt$?IL6y56h%K=xyum zRZnaKCQ-bmB}PuKzw#544=0(VA}EeSBSTScZ(7o#?CG9GM<6ga37yBh>}Z8#^t*DR z<{S??Am3F~Dn_p*wcs#7w5~ucWotSO zz5CF_Se&I7LHS8a9DluGBLTvW_qRe|WXkTxIspa!mmLXM$y0NhIVKnE%89%PfKB*# zkr$Vg0Vu$kI(N9l*haeOP#<=cYkd?srLL`Ma!w7^D+fHbIYFQZSVb~5(y<@Yc-KF%2iGDw>+ zFkGEU3#eE=MthgEvwDC{77;|JL!HpWqoBiUmoYQM_6q^W1pRB|d+E6)s-^+`-Pc(n zSM6KD0T0=4A+*S94Kuoy*4cZZ?molH3dt*n)>Igm^C}P*Dqp#Via3#VY$myndF@R5 z8k)y>kx6745GWXBFdIm!we_l@dPxZk>(t9zd9Kc-ocEucuK)T2nnC4s2$$6*f=8;IlI^fe5UG+4$B=5sB_Y{3B+C2^n$ z&{d_xU`tYnO%%4t1?Bn#AA_#hk(^qpU&RAcy90L$V_vZ4EKWu_niw;;A$28>*<8FV z-3}ItvqE9;<72Lr#t9?n0R&dUbbO}Dvuh7_ElxBsjU0v;@w9aq?|>nOT8jnav{Qi( z(cyFbaUlXm8&##DL=g0ej1B??Cx?t5n~6j`6b+b7*Bj(V28gV$L>qf5swhDd=hcl& zzY?zOAuk;Py~;7qSr$jt!Xbh;XWEwpAnQGg3(hLZdGRpgg{z8}@dGwE5G24}I=BqvpUTeJFE?`mPXK-$+&aM7Tk*Li z$5-;rc2&1o6@q9$2ejPR@p%bq!R5G<_jnt0&bXgCBOu*aOzmy0T6jF$Qt@*IbO6wJ zmpjBJ_^AJweeZ+bw{WH>J);=t(}?xC)nAh*%W7Z(igra6Ytpq#uTW5C%T_6S1*MAe z)TK^(Jntz24qh-|lCHmm)-?|@YR8)eb@bfvjSof2x+`Ww1-v^u;g8aXgsNe}BrL%b zEwW5e@q^xo(DR;tjVz+eOt`IFO0@{RLmTz&N*ZID)3yB|lX0o@T+AfF znis3a+9SByl1I=PS)gj*rJm!0LNm4O{*uH%o{Al7lRB=ogNQ2F_-u2ye#ZvWZ2lF( zVOo=!g6UoDFF=HlMiugfG!21pwfYQ)7|wed8*j&*s`DAFK8*y`v_Y5($RL+@#49Vh z+e=%6qS3Ke7G`TI327Es`M!*2qu*@ewgL5FhEe5$lupmoDN9&uIj;6FL z+VSG0j9R3LJI!?caa%+4!4RoXi-NI7Z3iQ#vUt>D%vfDW_3+M$ry_0WqmhOlL~N1- zXkw;k@NzI$>LRc(TC6XhR#2|a&f+aQh3G)~nKpXR=W4PM(uOL%Fjt5yu6xx5TxaE) zrINluq9tZ<999yPofythEds55g8S1D3f%=XVF{qdn%; zNZbG*gVi}CF!4AxH1CiX${f~6Ta#ra!mdRlmB$f@!GersYvzoQece}4suzLD%zXl~ z6T``Kk-fWAb2Xim!gqp~YUt64gqpklS!pAkD#;~G>}}7K>xBdF?6cOdQt$~Y=~V8} z*6zTjup44L(_#gB6aYR#)dWbROx(^;kH^9FPb&r&;%4u~&Nq1tiwrXs$&;mYageGc z2+>Bi!(A;XInvIe^w=nag-oFWuNJ>u16sXlY(m-5j6C=7w1jzfWCkj7W=nC6qvHUn z92XEsw<`~j**0x*y(5};tY6jPCvudz=Z_OMrXu+rtO`0##wT!r{v15_+Il3oH8 z2KDg=Cy7j9=!+4$ONselY^>qeFwkm@Q_xV|(o+L;4)lX$YRZWOKe@7_l651ihEBv8 zPqZAh^EIE3hT<7^8?l@ixX{>k!8i0YL@jZnabIJj%{%U(ShE?K65BG6pT<%oDVz^q z^rAE-(Yrr!F3Znj+?_fgQWLLaY0Y2#|D zIztmCvnCMd<~DqnI71(0_XETzWL~-kRQjBexXR# zJaWeSaj-3&Vu&RYMZCS`!lhCvMfM_;M{Dz9+7CLMXi=pG3;VO~lcR+^;{e2(0k^xj z=CE0@0>*pp0XslrA(h;G(5l>=1^s{tPOPR&S}IT6sQ+noH{ZH_k1sW3?vh7vM%hwi zOlGLI0mbT7&o^mP65km1Gbr8mJ$_n28{piJHL*9~&0?snuFQc<}HX70z4Bb2#AcGrRGP zDz27=s*6|E;8b>cWMBFzNh?PBJ*n&VROcd#prqA;ah;VHL=oteSKj#y6;RudQNS6T zaSza5^I?*=rW8`_kaMLaDrchJj`Kdnx2_}rTMqIJKFf;SgtYNf?A zd?uL6Zp172hORtoHc1{=T+7lA-L)Ya#t;>Y{snw2d*P(K3!-96Rt zweQhP^9gnj!C0qf1=sm-DwLVq;0~OD*$lt;dv-do$npv4_Jt}gq+736MDHVoP96Jc z%xmUwX9(5mPyxRGZom`yhH(9TrfAfVY5g#=6Ns`HF|`a!gL^TC8yv^usZT!iAxk|P+gdsp19)j zwYmHL^JPreY0`X2BY}0!BY4RwwZ%OsR(A~V`L3FYG)Gz}URp5Qpc_~YvJ@;Xyq*ch zCutl>TpaR5!|3RnM%8^^A(}DKj&`1*+^X*nwgqvTcfe`1_Cy7_WksSoAEnRIC z)^&RNZ^T0k4+5upDq+VVy{kBA+(GpHjfUrG0HWRM{G%SqQ~Qy%r;7J?&B;j(!Nvuc z_p%QHAii^_R67N5jraiT2*zpDiR!I!hGGP z`d%<*fzgLNk^jn+ru59XT1Ii50=QFQ%zI2+FDAH4Lpy<8o%d*CYV<&%^u__9{6QeQ zmaX>MF9?5ZBEj;2z_geL9;$(8XWe(wzPz%GncKN>4o6s&vXQc;_jN9e^Y+Fu?Wpld z==;OKbzDNTQ06$u55Eesu5|=iFr1I^o>cMR!%y-8C&Bo3%#nZ;m`RI^3t&mIvfu|j z1CujDoMWUjkb=ge24>5-CVN+j8NkJ|rSAn@t(6vQ?A-CVgCl+TupSuFkRD!NJ&bWQ zErPGQ^CzOux6h<^$M2W+j~;C?=N>0z^2o-#^i;XSI~k|#a(W{Y)z>7AOa@#U_X7d~ z<@nooX)8`#>3a}x&P`(l#fWQ4=}Ho9WA=SzA(j$p!&f5UjQ}0 z)kZc~qK=0uI#qqR?l$2+6H^*h09&;dLQxJ;5}7$f)zx>s*CFxU_d9jm4=F^|9hGv! z+(N&doTEI*;@mBco%dQ?0y|-zQFAu<%+ot?gLf~gu3|Ul4q!dC-;6I=CsYN}zZn*` z->P2+)2Y-p*AKGrmaKf#B_ZAit(TtO zV8wWPUjm(JVE^_Jv2QWH6rz;I8XLQmYR~qv)A7z{h$(3I4o(F=x`Yn}y}_c71XTOI z@!>hT9^N*mN>mhPzdZtIx`a^UMmQrU_E*y0LwVy|quGUvu}JXgbEFBqYQ>^2%gS+M zjn+MA+j*Fg&3=_ADNj1JD87jvEpe=ux4MGb5G~%INf26kFliozo(7M>g5znWY3+ph zbfPjifjLqEZ)0Y4?`s=*&?nsyB5afDU3ZfGfiQSbt*BD`1|gesQ5|+L3oA>0l$}Qi zUe9X>P*Vbo<+Z95{jBGCQfsBFZ`kae`pa9j_@h)L;m2q13I%_($<1SdxEk|M>@I(QX!Fd7xo#t4Iue0S+bM<}Wx$8YitJ;CL*7%)5GE=i@oLA2Z7NN1JNh?ND z(+)BHtPrFtQ^Wx0RH!QIocBI5eC-q_%+)MWT__YG5PdhVZnw=(Ld_z`Nh8-CXl?Ju zG#C4Bo)&xT6lZpnmR~X*5jmGPUkBpn%ti8NdOI(&sJ!y=Z(<>Kkp8SnO7Z`>Qk0X>gV=t#pi1Zawe4!V!%30F-x$Tk|(!ZY$FfKigjMd zwQW_2zwXw0-l|k~Qw`_7W}C;daT2)MiWdc~p>hki4A=LDq9pxYl9MF7`cZ7_QUv7u z!h!kd(Ba)hU$YG)L{z4s5H99pK}XfbXsP*~=DF9a=OILb=Jm94T4+uyiab!uO7}_$ zxLuuGT1VYip>E%Fu(2lorckE$6UFzBM3X(IR=RQA{)DBWuf~Hs>-dC9a>b~YAc^g>VJg>E!-^g%@($W>8|Hy(R1Kf8|~vGjuq#T%zq zD+IhLCenP#JC_j=t)pP4<0EXOzn7@qq&IQKb+9}vs(ba?@a?{lvrxB@wzRmJqyjZB zEV!{LVkT={4O6e*cu_L16{b;@Ww(!t$pL}L8kcvY3jk`5mro|R4@b}Hfu7O>jf;1N;IhQWm_CWGOrq8@T zAk_i8EYEB2;3&R2#HO?`Dh-*Bax1V^$856MeNrVqQnl|R9^Cu)iK*PjrQUJ!Kk0I|Hs)4VD$if_5C zzp)TCW4V@%I~rOufM%;>1jTneer|k_9VOcmamqH5f3g3mu^W7C2C&> z=PFsR9ERP2uk1;Lg?kTwGm?C0KT%h}PNeAWp!r56gM#xqin__ao=_i1F*)@TzEqPhI2VFKkV74E3@5GU&`RYOL#NzM_*7#aC#zj*!ARUlC``WndO(awG{JsrveQ zvz}`&p}4Bp^;|YIf-fi@+roxLh(!rfZDS;coiKOX$Prd8i$l8B=Xh7`+bGVs`}yd# zffP;?3a!ITZ)sv4@To@+-^Az)DnmnRH1lUAQqU8kB17(M=jg_;?2%HedAT|4t?sDW z%*iDnOr%#Su$Cv^4dd%8VZBdmnMl&Lpp^s|*gY$fR$3B(hgJ9)I#5e)@CFkHqmRI! z3Ypxa4P1|0eH!6D9=zXeQbfB}wk=ejmcd(`!gg--sB*h6@z#%#PPcy09lai~a9QPt zCgwe@`ulmi)+2f)*6TwWzaTmMWaI)60l>Ky8wXfMl9#JwjybT_L^WMJLiGde^Fm~W zCtT-+(w-paa{8B`MQWJ$W|T(y*tk}HTuhWK{nEJp>S_{&mE z!G&eXsyp^K8hU#q)aL!L)j`C&dF=;cq@mijKN(D=3ZSZ&O30%AM+^=NGE-5;^#?*} zLBV)>Pyy`_ueK6vXKza-^aRAw_bg1hUUjZ~`d~v08214>5o4tlhNuMjUKk%Wwb~e6 zZQQ|^7Pi;N9a}fC>jFUbRdk=x9l<2&6s%<`-Zmy}6o*cpKJ1;h{y=+0*q=`@SV>~8 zboIFP+PCsSJ>7%LRKh}bZHB0mi!@%cr12}8Do^xe*yzQ8A5R_az5(qIJ1rh-I8|9n zwRvWC!_H(DuYOz+Su`0A8glf8sd!BY)WR-Ny%m1+M3AZ(%1_7G^-J%6n+S# zNyy@^bLC4R%uY{oHjV~bepO~o!x%_nl2!eVUG;K_A33N&NgGR+hJbiv>6W8+3Ar;fu%0!7 zU%Q4ECER9-pFJl-eBrKibi&-iH58@+3zdz$J!#?o9py%u~x68Tve znV2wOXyobn6a5(q)r`;@qUo*l`W9Dh7o1zi-il>VC(Eb_OkMbioi-ADesqE+q%fyz zcQ_b@1AY^0x%_;;eRJ!L*U+)l2n3y#>-{KV<+S}&)lkJKqW@_Q`Hive;wvJSVsZD% zI@+(N;5tl>770%a;pQ}<-MGqvpkq0PF87N$ksj*Etc96`u(B89f`3{sz2qOvMHsde zZ5Rty(8~?6w&-b+MB2UMMfw$VwGB8Cp}4cdCqlbO*wngEyuD2?cJ)hAd*D&<5p6_K z$CrKBltcWhX_jBWgA^QD8;o0!NkTyFko_A>^-{pGf*Wy-XO{@qa#f&1dA$KWs@=aygdT-VQ7y$=Y2N}%zz1hNv*zuFou(0erQ3^mnh>LNa~kWE`+YYV>1;?^#wH#83me!b$kH` z+Km#3sqQk=Wo9KxWVYvEL;c0r{OGo{p)%H6YabtKIZG3!svepSppAxWsIqIiLvXQz zJWSW}WObmTp#1Fm%Xl4?VYAAI3JYI)^}rGeF6YPAf#LKN0OM3z0xEqu1Nx1m_)W3+ zRZz?kMup+GEFxZ@cMnV$<-0-1Xx?Sr`!=OX2c9v?} z_oA%MCrQdh+1RpiQ$rtKxU}m04ObP(ysDXJ?qj_3Y+tS!Oe~@~WW1&Ul-IXToy?sv z^+Z%2hG=7Ld%mtr`z{kwEux3DmMzn-hA3#jXjQeXVO_-b;4T&Adob;>_%sUkp7OkQ z=zi6>)HR-hWqvlyVEkDo&()c)IKu+4j1h1!+(qC$U)uximLMGHf%+1G;e`yaUf&^w zhA&S##7NL;iW5)}Jk$~W15$l4IbGU-{rOyMzumw*-S zT&%eoeH2yFZRt_{RS3VreW`gaDxgV;B^)HVLS$Y{knQQN3_9#8n&y}lT)jU0#b5(Z zbiw=tYCq(mG`gUF&AjIbJh;IV$L1*#unb@kb!H13%x-$*%lYol8TrZ=7dZVQNv-is*FEyE*OAUIExoxErw>*73b zV!pWI3VRTC(ZeV`V7jv4x|M4zTNas}cT%NYRn&>K%B>kJY@QG~YxOm+Y^h>2c48Fd zx?>?|gDN=`S&`ESi}p?p9AH@g)D$i@=|upisXfjQ#!Q2bbr173KMSpwT5aOFkvEec zUqX~NI2vm#?P&px%`C`!+O3YtHtc}q;vKO!)Z(KTJRRxS!&PtZu78yFH#kqs_|-IxU8$7Cy#Ww z?2+^A>Uc-JgTwcYUef*xah^xQO6futle7s4=bK1xQfD$k^LDq!ya%1jfS^CZ&^q`C z7&;fz{i;ZCe8$JbAXu&RQNz`lHWwFva)qDHqAe{Ccsaj)kxO0bQPp)=m~nadg%kkm zRQ(g*J|L%DqaI(Z((2%EC>+deJ^Nv zf9l?%w*+w!H2bzbC2LBn&pq&2)#yow=7LICkM+y~Z;Jzw;ZY%wz;Dv|!{;|$I8_fp! zXP`Iggi&vG96B^7uJb`tTP|s#aHd(^ne+z!fV8|8Ld60rf$?8`O%mNV^z7uF9>l-Fswg;&);=eOSHjV4$9~6gh?hfwT3-a@43h7ZRm8n zfgI*kr_C1|jypK#TH%9eNr(W8;LgGGEhgkEgJ&7nx?{`obF}UfoJ~*F$Bg3+_#BkF zZMYT#tIT|;K5MKqWK=}Tp1B!wo&*-WQ}q5eyfv)%RLtGhQk?JAjxKm`8%5`6Nh$64 zN;n8>b>E>279c4CvSjo+zz?n0%&w(Ww_dtFu;in*1Nyk7Q)o=KdnLR#!z>DqV29et zj3l7mXyWG@M=$uw$|L%-0f@+n9?3h+2{t{#8=M!f!VL0eh!&7z_EVe>Ds4)IIE`HR zcv4=ehs}P}OWm?ueatQ}`7tqiWsUth;-T6DfhDj@^LwemLT;sQJh9q$c zDDUWK4V4yegv3^~=v>ipaFsS1rvyHvX8G_V{{=A=gOf>jXn2{C28y_-7~r~pF45P z0(lyNsU|;KV#f|U(gm`Xrwwo^C~f!Ix~C;ivSty_Pp0-1vwA}tLU}+|i83tH^6wQo zW-vENg&x>k6m*f{S#IAI)M}r|e)MyJQpaj+AKxfx;m3{Sl8sw;npIvrJV7_~J=M)K zZBSfLzLe)b(z-}br(fv-@s8yhDInU)z>cFDf#u83*>?cEKtj0_1za4uF@tTNVjXyV z&1u6N<+G6keH%!a8km*VT>;Sy0&VegGFMNOm*O$UF1B2`Ic*IKr$Mmk+@`&VIN>yQ zB+9_40g>Nk4h45dTaPW9l>cIi9k+k|`A0T+`LAqp@0i9f7U9mHEw+XAiBR8I75P8) zW4=m<#dwek85E3-iYs1efL6NP2dJoM@Hc3X7-);aqKviuDcOCnU@#G};{SX?#};Co5`2q16ivK&qBI7!wKI` zqtTC(6bbS3KiYaaJH)2$XzfIs?y|`9^i%Jvr1%c9%nQ5~Vp}tS9T#GHcYdPUzIdto ze@!*pfKP39>um<0KW5NQR187=-I|Y zLa3a136&1vQ^mFeT0P^9c-p7lIA$}EvZdMP>q?&yX%5c6jkUi~xqSCB7-JAta})fv z?+wFy>lmQnVhBU2P`-F2+pC2-i>f4;4o0~dYO~#3mz6>oJ-IuRiMbIfK+N)|%6;sS z3ZJ8#x1X(tud|z@jpC=)`x_UNf0tvP`AZtAtADEE-)KYmzt2HGwxQBr((sJUng18f z2>44%s;k@lzhX&;2;iHorRMNIl(e<~ul?M{-qy$05yDwow(aFNgnwBzOa6}+Pad9s zW>{SL_^K*a{_LIsv8P21m5v38?t?g7>Z2%6{|2wnVxf~xL^1R3DjjUKGi|KWfb-#L zOt%Vhi9vinR4CP@`r7jv>1WK9ko47y%hA(ffASaHIf9?;Bz*7pgl>@_^(Q2JwCVqw zIFP4>+qgwBO!{7%9q;YV8!0M+2lZcsaz$cGx*{n!ciZBw6GJ6Nj4BtHp<%zuBEck? zD8;$ph-zW?7Z^xignwodTO<_!%_Qj7F2jIO@7}V0z5yRO;r?|6Tcni@!96wqsT=&qTKof6+(W|a zI_I*ihLdht4QYjek3u~Jb=(u#pBDwcvSAkTi67LJj74ryuvv2bl6(B#g79(5vBkpQ z>3|__yKl54ZR@;n^bNZ;q1xC7yBe^E~6D)M$&3|GzlL4=a`?TWjZLTI}KJ<8JHb z9OUSru;mYb(l6Oc{kPgz5`Sy^Dy^#^-9sVB4Kdi-T2nJ;TiwPt#*wv}q~KQy7<)%z z&<(GW9^9_K(4bOudy>Eh582t0qnLOS=L>frIaC-7*EZ_1RM%j|l*8I&q=YbiXVuLya|cj@GL;fJtd zC(8tsV6#F`w=nNK$w#a>Y|^ob<%rqYz_qUHQoE;q-0STZtNhcuRMD@${SG1^ zd+p5^SKoA+i0%?hx#;-e^uE0s;xa$j-aGnzSkK}bUp^XBH-Vkli=gEMS2C_K`u1j% zJENa}^-TUiSiUdkD@j0eW*Fx7@Q;clxb1VZ-Er$?>n?UW)G6d4BNn-oIzA=oPCQ# zY}3emfR^O{wjbRSR;L8feb*9Wm82@yzu9o44!Zx`$S!$4!Wa>07a6NsxU|0cu6Z>^=m30) zUWecJIT|yxVH=;Ma%i`U7q?t*Gky8(s_-uzPyHW`ht_?D3{~<_?g3J4fswSMeafBT|Rm?@}*(xi9%pwbet17_-_bBAP;)BA)twWey|8=YydZj#X#xBmEpu#n{;y89Zh^JyjjzyN#6pgIjwSvFy!6?`vWRkC zkQwvXYtTa$AUSxSau?=Mf}vnIGBV$zS*}9NM9PX#sult~v{AV-8!b0$OC2gr?8QZR zjm6BDutV@K?~KN`F2>nk!Cwm;UaC(ov{j5>d7{0)@q>bnPLIG(4J&u$go%RR8Nwx3 z*UP;he~yx@0(ABEed^;xVRN7QXffo}*Y36f|49@6Z>0(HA1?@hnf(7G3BPy5__rhp zn=AEik}Vm*0dR>#Y1PD!-r&+&TM*q$bk7vQ0`^8Ey?Ls#fV)UA1mZr898_x ztZ*Zwqwx+TSz!QwecJVn6X61j(D*@@+7kLE-kK~oXD}2O(!rHhqwrE;Hn#E`Ga3Bq zO;6Js*OsocM%JrMfw6fX;P!Btl}jT_L_?POxIduNaP3XCDOK-7SWWDwb1=lWJGi%O zvn_l0%a-{!We@*)KO7S0Q?4DmyiUK^ zugCl4wbuhbBG3I$d-N>R^5p(K(r1Gn-e+9@5mb5cuxk9nn@Zdh;Mym#~iIvPi3exOHISgi*s8*3#s-8 z!n?V$WbLrTwb%6K3g0>K{J{IUfRm>Z2nldSSB@;kOgaj3k&3%V$$*e-zEPv;0f0dN}YtJgRl_jFklBW-yhD!>x#d=mRh8%I5 zB_g2H9kI_BX6Iv4235%u4zDM#>T6V$Eqdn;j}>^GOy?e%%Q`-GV7rN7_m=!_y{e=4Y3>TKBE0i-oE|hYW5f)cd3uFLBKZ`{Rjff%Z>m4F zBx=&X=FO0iX)T3;y}dwY=et7&-Xn{Gj*C6*%q){F8{9LaqiF%ppy_Cj3%i1rVt&FJ z(y{zl!EkljxX8jC%w+9+r<=Qve3au!6%i*PTXVyQ`7S{cJOE>8X57dXkF~4qofDB) za)XDRUS=Ijsrs>U_GTUWrV4wFG1s*6@N-J;|A7XX+#=7Oq#ms{xR9}A+j^pFz&>Tu z>O+=4ME`{3%lCM9qEgI6C2!U`sX|gen6OH3d>V5GHrel%>!xIhb{(di+bB6+KyDqo zn?3(CwDPQX(83inqys{d%l@@&ELA`@e=Zzvhw$ z#pua>(x00SJ*2TE(ZP=-a&q>tb^CVhvVCvi{kYG z|1R&&FGLM)ri)xVPjjE<_`+_^pCdaqcYw`BeDf6J;IAIC`Qz4qCFVZ1P=(*kOFo6U zdCp8u+JW=W=639m-3Id$2YeG|+bp)TyDiueBJbqm>EUbR><<2AeDYJ!o(Y9?qWv$R zC%1v_0RJ(lo1>Gj4b;aO@?XfCY<`MM`(O#&^Q*9O+i*2n{}|WD8GQ3!0B?R0u$eb+ zo{}Ej2K+*S>$fayn@ImXK{uz`Z*y=q2h>k&!;@F!`kcwzU*>K<)qiuKd~+at6ZZHv z*gZ<00sHHu*{7(RbC+AfZbu#Y9MoS|hfh&A2dQuUuB;fqXUNKFSN|Tdn?uZ7sM|dt z;q2$2I&RGueJbw9Bq(g7)=&8}fVWQKe?J~R=HkpYa8s4P0{+Vc%%6#_v<+Qd^{=3B z4V8R~|FQIdZTOOEe+B;^1FWC^2b<%fn`57wlN*P({RiBfp#0-mvEL7p&AH1hG^uT9 zyE#pZKSKM1*w~!R*&M9d6m2^gzvdr<{Xs}<4gzjYrTi9bpVps%{eC2Dj?`^QwtdO^ z8u%w*e;5ECYkF)OZQ`Im#`|->|5)Meo6B+S&%peBcmG(>?Q1LG+-HFPk(Ylg>-IhC z+w-3R`-jfGc`CO#bhEj&Y>&<#zvcS)EMkkw?HlgD+-f$bQ?|CQ?Q2W>17iU#I z4b9B~i_OE2%`9U(*>2zezVx4!b!;A4Y%pX+Zo0i|DNQZMaRb}_;!XJ&3-HJ zXF+b((VJEAW=+G%N&oTd7T5B1u0N@&Kjm_>`rX3WULAy6{1fLlweY7fn>FiZskbSP z#6N`DEHXqbKZ5za3ik7%a{mzO5BAYNmg=07M-ktmMgaLHUR8c)vdPmq<@r@Z@=7y@rJHiGUZsy+~w0LN?6$p2BmWD}VIu$ETBB zq4fm3+pBW=X0)BQ=kxB3XQFhBxZQ%>W7)ZEP*;4$SU?0TwBWyz3M zwB_lI@-~xKm(?=QP674^BSnFA?B zMde=+t*{M(741Sci2EqSS)=k(XP!lD#IFTF%Z``#^#OFr30)vFFRhCL6B#JXUC+%Z zZ7&rUNi_uSFyWfP2_oEnmUBY(yIq2d6Xb@qqBvraGn**)e`A0-^v2$izyE(Y{O1oB zt^b2V&Hs(VU>~@DSfuy=2S%Mof!Kr8G8Nt^)&UHD*E5&D6yor1qyx0lCnD+ zM*zju>{LfpbrR+R(41w&CKSLIWx65xYg!n@1_#SXg@_i3=G%)qQYC`LVL2fG2t+{5 z?ck+)ZYTVl1z?yQDB_zGmcef)Vi|3^=3LP%B^OCwN&@-ripzw%k8%NmnxuWKIF6xf z$(s+Oi1!&A4n<}StzxJc7uje&i6WK{=SZUWAwlAmtXv&RIJKWHr{g{;>R#2#5$`lL zU!}m;#y6J@n<%@9CfXD+-b6>d`ySx2zoiav-JkE>8ICHl{Ji&WIv6s?R!0X5*O_rW z)m1;%)X`gzCe1Du5r*B}sez!F3%T0Zu+0^2Aeb-L+>9EAOrpff;t6gsc8foaK@K!D zwG&NhM5W;NJ+3pF&3#7^$VD(SXItkQcgHto#T)>m3*5}lve=xo6tzQ|B4&Z&bwD6c zSP0=KPg77UH>N3~?pzppJs#kWS}O^NM0UW0l;ExyJ{Xz_>sr7oD+G4#$APM*w%vV& ztwKyEg1L^VNABwG6|PmFviqVsxwle5=+R+7!(K6GV9>u)ne>p9xiqlZ0XHE?4ga)U z#~voQGgi13EF7MuM2m$clu-5}-jFG%8|CJR_m|WC`31RH#D%dMBu*>m=cz2t)?b9| zinj_Www`Y6>DhKgN^|GhcU&}Os}t=jEMW2~rW?Q z?h`g!gN^XBJ2jsoqmbY5Q_(2lyQH&w`?GS$e`0<#?|;~+>D=u!iVQ3gak2H63WGaV zOP5r|%a~y70)F7?;!P}ecdPG5Mkt@b1&Gp$wIMB{#n~ubFr0!McE&RNhe@i~fA|yA zLF!{`@n{Oaz@fY=HW34f67lG0iqO_{QZ%!-s~feya{p2vB#znmgHmJe@`L03!jd53 zMGDhRfT*ZZzDGOM1iM|mth30BY2M4kw#<6N;bSJE)nXl8@({nRcdEK-$(h3Gn2jQm zQClYBPE-!VD?k&QFcKsZfNx-TX1AFn%5K*yp}1uy*-bo8t&o+?VWMWsf_B~nq35?( zWt8~4hPy}t*9yx4G`E9-qmX0!+%n3(Lz9KYWcOhe)OsN19+w>-9P2p5e~J;#2&84v z?9H-Z?0%whWZ-PWgC>36}k42gW;3DxTS!>0OCKEv23ywRgQnZKIw!=FN4bBDjTvw;0Rm|v+HC(}S&Kb1-9ha_9`tK zV|j$Avo*B|#rDP)DmkLT;;u=tRu?@pw;Gr#YY8|SNozmRgyzpe{?6;M)}w2-&jH(E zDXNUxe*31HJp%Soy=4Pw73saovGZ^UA>hF-B@gymvBEc?MI94i0C8j4N;ZbaF_iCJb}BNSOG3U z#(W^(qENea6ZWwsS9|_+(s!+lgdPrd?}L8@c#qqh3k$uZ#_usf3b=Ea{jwbxjU$cf z0}9@O^=@lK7om1=c~Na{?KzntVBzd+bEFJmTx^1(P-e~qie(w%3lt)pW} zbA-fT|9G5R3DGHZcC+CIge+IREhj$L4#Mte?xR8_+69}W&LDk_rbC=sf0e~!4&nVM z@@wwkAe?s-)e8*2Uec0~vFGc&*%OaASjaYU-}~M^cb}4_1*8GCm*B{$P+!*6DpNeY1l{TmM$Jn|6>3;3hJuy4&&h6?DErx;m7!xT$o~a8z$vpY>)ZzfkPH*KfGJ_mskm`T#9@ zC)32C0|X;ib(Si~>fi(I;pJ7TXr0CNuHq5a<4`SRkDPn5#eDjRcLLV8VH;X0k`Xub z+3{sbR3s`^)5;iIBwW(fw!p@A}dbYEjGS|M<|sg%K&V4PtsfgbWXPL8A~^doBz zD%5t3-B16~xcTF#tAMb>$n!|~@L63$-Z2H9=3I0ggD+K$Ozq=c10^9_)SP&J<~_QM zoMNh{IWCofgX0W+t|4q98P^y+&aKv%l_B2jdX#&g^_V2O+a3qw#WNOO*!C-L_7wX4 zM);n=qFrH|IEB_Is@3p{i(`!`2j7QlMf!3W(l9;Q3D*Ctbjk{Cya z7hcA*Ed*WJ2ayWmmoku{WhyEf4t7mWMrOw2E`*d{MIAv0I5OtTT1prT@s%TvURg5P z%Bttq?MnhEmxA=8T5C@RuPJ-}XGJ^jM*62PuZ3ntQRw7z12iMZ_U}hP` zJrDRKhd&ZE!?KGma0@TT#XFw`egRUQyV4l~uP8CFuW9dt!Mz<30J9%KTO1 z3RTl_g|>LrA}zIxh8;CX-|re7vCy2GsLO%y6~e4de00)FGzn&YPn{6ohdrrnRNvHF9czcUmKw^xJyGtU-Ye~ zIuBZ<{Rifq(XEdVBzd=xG&~y2qQ~R{X-9}ft;b!H-}EPT4M&4peY80E{vC&3F!yGj zV|mV95}=T#rqLR&6cM5n9im~}eGv8+W>Cs$2$SBYHP^dSYYPx5pw!`9jfpdTU)zZb zeR+F*t^#~1Svt-K?q^jK?J7i=xc9B_T-ZA?2=bd9+oJv?o(?=eJHWUu)#6KT{TY^o z+spQ^WhH&D?UF;&dK#(rzNFnrQqt`v-Vemhw4*kg3c6Iex;i4x9QJYq_b1Gg@t%VC zO*LgNG~TpFkyq*A7BKT9AWXbr1VZ*)!~eYUN(f>k3C#q*0plrpqfsd_A(t&q1+xO8 z!`VQ+U`qi*%~UN*daNZ8`F|j3x|7Ary>Bd^wy2hfo*$2V zuJGAGRB-Lc)C9*A-_(86P1Zg?jTx(%4 zSS0YrzMLD47l`?oSMQgFNubM>saX;K{uj1Yd!^U^a?Nq=_CWMt5 z$vcnA&fw`FFQ~Vi(NdCAsy(u=oXIaC@|L2o?WQXpSYFLAYW^#@?hQ8dut&WGa;zyj z$1Y2k5>H`O5!3HLb_%iYt=(^1Q?*PlC@ap>CND`ZD56}Xt&#+{<^(-Dyd$Own(hO{ zWHVO%`s$$|*5Q#hg6M{_-!r+D*V<$Nze`jhcHsvUjVA77ViGJ|p5O1VHDM%eZmHY* zXof=RW9oQ_DfkiMtc6=O%a8QZ|P2YS%(&eo(W~|1=+VV{c`>!RbkY}J*Qp#V0P1Az_I@qT) zZ|Ow75^|ZyI~#cwFiMz-w?gdznLV=!zAAm2$2&QaaDQ_4skuQFW?s8V9o-z0xGp`W(4!zhj(P$kUE(H)Aw%JZt9?W5$%?$vDE}Ah7 zo5Km7TZZ{x4bxsVP*}PaXM)>gbI50ud(9S!XSeTflHsE5*^v5x!c-Ge;>;cF(ickz zc=d2%zY*O8Zv~-W5}4ONxezvB2+OHbG^~#IMSYFxlVIe#Z!0rXTzUVPYbbZcYg>Rk zIhd(}nC$)ft0Y_595tM&U<9%tHTkCOS8LiOU6wL3pq(Of5B0@mtE!2AGE7g$pe!mf z#qINQY@{9D563(wM>!l!|CM^iGEkd$FYP&PH4A%8!SkbH4r2AsnS;!*N>Zw8YpsSpF569dGzBG*Kj@vy5FiR>F^E_dC=ER(1_2@b0H= z@T)m0G$?V(mm=udy$JpMDMq@oC*Aax_Hr;Nvt+(%xpTNfqW6{$wCly_x62-Q%3x=8 z6UY8F4ELzzeq}r-eauBG+BaDG;CLoEAm6FMImgP1$epPiupfw zsV6uCiwJ-t92V*|_pW#~>OiCCdukL&-k|}?y`?G2-me9!bYoJKcaAK-(gb;POzq?7 z!mVDNG>l!}>Dk%~$O%Z~uNsN28~nMMaTV*h5OuVK9R3Yrz)|aAky*9rx&6;ZW>GB4 zmaHwi0+s@Nm0)^=yK`e2q1m}69tb<++GBx8F-#k1SqTZ#E;%km4GMb(i0lOm9%<99 z=(Y0DbGk8#Pq{;HYVZTFLy{4eSo~1!mtam5LKfE7ffmshuhNkRRbFMYdJjRga(m8B z8d@d?hg@vs6+(R4JDncd$-!*feXiwwGwx`xKs^guP3y=mtl3e;Dd$sW8&-{^Y?kd! zqGg*Vu}#R5R*w2531c$6FU87Tvvj;^eWg%bW>r5x4)@8ZL~m*7Kmmf|l8DNE>H}6; z-VMfzr^~770{MmCEzir|>RNTZWE03~6g(HMCRnE5u(UOv`VZ{eS3Z?xl6ZM0O>(CU ze$QWsNT!6gL0NnBw}KJCE4jU7qtByIR)Wu2TK_HBn<~xSn12yEw$GV5)gsM(x^SgX zcQKWhHqTT&GyKv}?U^!VH?EPbOa-zkVqkOdlB+<$ka#6~^84P_ClB*~<%ouWEkyLV z0(+?7y)ZW>s*30r)U{;T!9%2^?fq-CE}d6e2US=;)X`%6PtMS$|NOM&wlG0Pe4niGWBH@Wvz z2n615VZK^;0-@e%HDpPv{};gxT}HfGB6>9^ut8Vl`Pi(*dhTE<`kSnqubf^_*;dTd zYRsQ}+b~wPn6A)o5$(o!r6MT&l7Z_iaj03Onj2hzGV_3Alam)SqJSN}M}!4NPqz}V z+0z{@>?a^-MaV^2hdFxVC1~_pIk+nP7-?9YLvFD~n6>J1w7DOErmVdVPOsTic&AGS zVh28PyB=wF1e8(}ff0Hq)eK1Z>27XBxZ2cw_-!WD#;bdGdz3q(xuv#wTnzeKkmMTc zRfu@EPnB(%+6zjuHT@sE5%3&^2qCO$=pewo~wVE#P9U`F}e(3}1WdjW46#`VeXanXMGq0@A z#&jwh8P~suvU}uxjuKB>txBr-L~Z%PM8CNA-kNK`*0-F~;p5X-*U6cYya*5kyl|}} zCl3fpQQNsCEiFkGR}Tbk^=9lOTMoXXt$a_?=Yaz523I2t$W;E2YjW%73J1^_I&2HJ zLA9TGD!G*}GL`-=RZ07!$Zy2-5P^IK_A0sM0A>YHNR?a&-VQ2?{>iTncJ=MLwj*81 zO`xw;P+CG|Lt7~|tnG|20s2`{x0% z8%A)P2lb{qkZ-X|)20ywYgR@O{57gp?6!r4f_xT)!LMh>H#oi2S&$;|A++-Yix4%) z{?Y2apdy4kps%dfg8LR5eHZqZwdkdpEb3xr5JkXAVNNRKw?z3p-+k3!H?s~``9p5X z*b|f?*3@UDl*a#HB~{gmTgN6rpXcBfu~Es?A4k(Uf(v zBviHiojUtH9MKKf3SbO?ceC?D^M$VrJww4oz10ER`JStXbM4b^M)}>SaWx4?<8J$$ zU^TGTbD=aPP?Ez*Kx{_W7AM?<{Y8)~wG>RQSf4D2S?U1)f}sYv+d-+XN>vp~?Q_-vQ4Yx1^?@Sy(#g#7wLfXfC+> zN|979lfSFGCEeNk8qE2FGsU4?Gr^k(UxF^>;Y^aLvO92cni(sbFSG6z&axr zs@v*!7c!%=bJ1-TpzCM1pI;C#hBZx4-6Dk(6Vp_y`=taIWR0z#N4Y$^Q1oJKn<9!T zv2La+?)5){%_1a&9@ad;l1D0sy20U3A){x=C;6|iDu$x3&CrK3)NlUm@$G+^R5jF8 zQC`(hd^G)ifO?5%^oCh55L1GCJ9HV;-tydiCm!VO1|sa~;m&@xU~)ssLJQ-%4?#-7 z`LwPNczv_qqe-%}S@{8z$)Im1=jgjLzD0)1@s1=KvWXVMdqV7A@KI76oGQ|)K>M6P z$jVVQ+H66q8Fusw4%*^^Cfps=u_X-cY+~f9ULB0?~&V|Bc*` zfSxp-OT0h11q-7|4PiRlJw+eTQ>5TMJrE}DW=(VTj`r4ork_q|Ws98n>jg@FQQtRe zOvfJ&ONJd3HH8WHi>BK?QurZFR->R^F462`0)Va^q*H!#2!?o7qO7ahnT6~7nlxEzrjKv&Hxc(F{^Z>Wj& ziKizta;lgcz8`7HEGL_|ebt=F+ZlzXPA^E#ZA+pZP@Xz!Ns48tp^8--z0mr(-IrQ( z(00KSIJ8Q=P^#I}8UAW3bk6MSLG6Y>+Uc?6%!Rbf^HCewx)2$NU{HbTB^YRh#cVe8kzuN=5VRH)qt{8H0cd zo+CK)GiK|VwU_gBn4Bv*3wg>fANj9q!&S)?3EtKEgb$2(%gL8#I@k%KL=;i*PfT)t zT)m^gNm`$Fr+|QPb$^ddHkA9c7gXcp<#Iut{nV=H1RP(D1-*~jcqEk>+0_+xL|}ik zNg}{IdH3Jb0$d z5YyfTOUKNw_btge5prhxEMkZSzbVFB6HhrW6>rH5S1yc~t`D9gXbvzfK*NedzpjHZ zE-2^(jCP3ghd4IfJw1xc^bcGgwlS{85vk#Veh#b$%F94yuUAKXG;+qtBjhZpHi%~m z+X3u>Eoqb5ZjHTh6Ugb{;z2-)jO#t_cRS#yfs_i=svkYjKH#LCEQE) z%xgZfl4*Pk(wM%=3#$-Aap@%RXIRmPKbb&m?O zJkq{+sRXA~y(8bXl81igYHk#XJT0fZTiHC1X#i1YKUmfGzJXmbN8TUx-jN}!W>yi- zDc0-Z8Tv5}t1?YMXV`lH13vMd4adLg%*(tA$c?~cR;ip4^(l8Pn(&dkF-L{{>f!jL z?>AaP8`%inQfB&vR!+z8bN!#0Yuv?=tw*L zcNIC6chmv(J4$>1NT3(~BaAu|?5kgNF5GIO&5bCjRd!Gr67n{7>B*JF7Ve-IKgk^p zE+xbpw%!+IMSW`4jgc&HaoTKT2)b04HYfp#p?zl6+T zQH@z@sE5{MI6R7cHlM{YJ=x`TQDRBwJBL8=eULSl6E9a8b$y&uJ;Nj4gH? zIwF}uW0?_p%gOO9MggS-(&4Q_Rrlqlxmw)TT^zt>U4v~h9!?Oc1q>uQc1T$xJsoTu z-z7`;z{$4 z`GC5&0j%`A%Vu5ET97}Swr|ODbWU{bU6galZ`_TshW`_vnU-qTtyS{euxZni*gYVy znc$SDl#(sPCn>u_4JDQEDM^1x(Za~pdWD7FMD5V9i8;*WPWH*@lPF;Brk8EkuR>Q$ z=LqU`Ws=1mVfD}F=OJm$Qi?hgRp-XCos;r(nmImPqMk$8&D2tR$0Y1I5eg~2fj^T= zi?-k)L)GzEYj!uOmAg;9Dfp3Ox$Kc}r0HfgtY8Bw=2iKdgb$a-fx|m#-KnrF+rd{G zN`*v?+QCnwegRN5&7V{)DieN8HC8SRBT^jlTi&>n zvCsly9fbWugAfYbf9?pnx-PgYVWa&7uzerP%5b@kS;!a(`Mah{8(`3Soc0g4;US!H zowBEMGhPyI(RA6t7R%q|ev!-qTE6g3>)}AVES8240`)!LqBCOeRkHHBJ8r81_MJ%poXDn#%XCvPq_r&w==h8g6B zd&I+c?ETn}^-7|?K)O`L2PH*1p50>l`M%kye}kh9Ws8W`Y)$ITqHCZA>u9bjO{FMz z%AsOoQF%pT`DH%W%MR3w__O3#NoJ+W?U2V0@?;2e0AO=slOVk zw2OyK_AKL9@}M^>KD9Wpla-4kD%aj-6<=kMjfE6zU)K>)>fa$Gy7@XLuO zm;Z^Hi%GV_Asm;l8YPx^vZQ0yHEkQDn!YrDe`D&yHr|3FI3=MIKL~Rg?%8?Vsrs0} zm-@53%hd%B6lKuEcJT2J)0!CB61rhM zMr-HNiaGx)@$yJa28ilf^m@lj@{)vk(k045M#jMty0ZgQ3JK`>-{TO>S-9z5*1;m- zt=yJ`8tI$%_jM9x%qM;mA5jo|ksNto+cD79ufxdnXu|~hrwhq;6Tl%yU^PRrIxUOA z`xEtM?ja`pu?wDff0{J~{Wdp7P8xvt9Sve0Am0o_LqCMXhR=vUTqpP@Un- z9{>hGW2HUx0Xu$RB$sgCGYJz3m`S;#flhosKTveqb44a&@}vHXq=uuS5e?>HP|4|8zfIDq?Gft z1st=EM~;|Mol|C1xzKak+bRew{zTX0smh^so~MP+71B^@t%C6gR4IG1ZN1}a=7eo5 z1b5vu73(L|GYt51C69Sg%J+h=@#~%0GK*%%ID4(L!{+s=C*plN3866dKI(pqx~24b z$P0_D@)-l*YG36L0DZZ%$H>py5^>zU_j9N%@GsEC#=ilVl@}F4hA(CtMCfIK85KSLqzgB8Jzt2IraLs@YG62ClXv?!RG!x3Ls*%)G&O#Q^ z+Xg=v^|cYp@~$bX+^3;P(co?|d;Mo0%iy`hhaP$2!85wu()qNrqCutFPB=mQNc@GVtewS+`g=BJ8)@_u-@7hnC$AQ21UyLsA%`SKb zgx>13Ns8L2jjJ~>t~(CW3{Y7N1|4&n0dvsU^$KpACv@q=XzR*hO^51tNVZ0RFY$+` zBTHGkXO^M+voUSUVm+h3z2%hWqF=f{m)#3i-n9IC={nDccH$=UW0c4CW!X#1lgw-U z8^}dR?~INRH8Sf%*pZR|7}WrD!emto)VE5;M)?+>*UF12@=Ghu9TS{64mzh3QeQDNi0NUG972nW7+*uyh0{rIylbGWJeUrRdM=*Z` zSAQeTNnrf|8CiPWnPxQkL79c*cB|dn z#7DOseOv|kDl6>acP4&79JA<#ik`Aw9VckGv8tBYW`-n%1{)u!0M7@;Z7g? zRGFIll9|uZ=sY8>aM@v>i-`WDWE-SCDqV)Nm9i14yq8+c*95t>H$k(ui~(&b{AwdLm$BPc>2tAds9^iYgF<|DaETPN zC4Q7DuiwUvt73y7E9OlzOq0jeFM$wpOR5ceuGR+{&42p zPQYWT470(}U?5&_khmQXL@(=5r&x)3YJ`QO1JT9=2O055{K~UAdhs8mrpe`NI4a|d zAuH6k1yNB{60o9;C^562cL1lje;`2tPVNIHtF#W2rxNaljh@gN z;J(y^=XjjiU93deBLJQCy}B*cr=r#L(W zH$=avUJW70-(q}zkUYIClH&i$_3et6A3B@a%PEq4QiH%uwGLb{IZoRyr!bBU)a}IN z8Z)y)K7i<*9Ou_ey{JSx-(Te035p|^W0BWJzs)8EYzY~FgednvG+RKe>+gRy^xvha*t(?p#V3(O*`N6W3_0bX|C`7_*YIn8s*T@N!G z#BUhsg`oQ8jX@2g#(;f~x*PcL2>vDbpV5V_aDrNlb!hd>scM{-Bf!S?(?;cSTTkQ! z{vRt*qObQG*-5h^f0^-~F8jQe6Tz#w%U>#9&UO@jTOPmk*X+$uHIG$*WrM}!E%!Lu zDGaG>>t3tRjpeccgr)ci*aCmO9===IX*r{g>cOpRm*aMNV8M+Do@uZ z6);mq&&JiRDE0nQc(Ux}8SU$6>s+$)k`lt7$t-W%;SGw;Vn^FN^^@y%^3UUx>E(JE z9NuB~Wz1yr@+qNW62!}fAv?Fy!&yj_78RNQz4$`PD|x?^UbK#b;s4Y1ds`!tbSHu; z<~f_xZHAR4Braf31=XJ#LjRf4gaBF_z$i&{3i58lM?%3Q_uTHM&R(7QHu!h|NJWw+ z$pu9?&k*e`r3iOT=(ax`H08|0YFNTqqjF#I+BqQuSG*fVl?$ZW68mPi?8{mXK7<0Y z^=Syykuv`5q!(n`$DqYKYHB-*gf0JB^a?%ak=*PErwZCUA%j}W@|_*qS%%gbKh$oq zw==4lSG(+0SKtA>r`TkPMx*tS?J>iCJ)bA8H9z*bpJJ@7oRXJ{hC+j?xnMJ#$xK^Y=;#N3_Co~ZH~RZ4|}acmM$qm z=D^i#xm(y$y?>cz7TS$LqLKDb@x5xZ-Q{N5n72UQU!xWA1p~1fs?jCOU(@q$W7*Y# zjlH_^Rx1OQq3VtXK4rWJZU}ADY&cD2woq|_Km{5fqRh4Px^`(l%xf4iqLjS?BAGN2 z>3zr;AsgrS8#q6rbyo+Da(~=sSswOxv};GE#AN4kU4oh6F)YCoY>C-qIDNlc!L*ts;Cv@(Xu|SF*CXmsjIMn%OtRSy=~8#x^&f|s9;eW# z+QJ8`ez__;eYiWYZbe?u#y2!+TMJ|)#ye7@yPjlHGe;Y@z?gEm@-yPQ3 z($m^L?Z&);c9*R$4UwUQu!^?BA2cmr8s#=S?H>we--&)!^|h~G#m9(qaS~pWr@nK@ z(h2h;Mptr;%q}PEvjkE=<*kAYE=kryx=yHG5i>nA+<0#`Pup?a^IWmOHZuP(dH~O zlH41UG>3{|iwpB_Nw^%@v{4ZmPEKj9;7)cdHz?JefUQG)^4sZYRbPbjV3h>Q;46=< z_U!nD1|cK!oP;pmK~OTqW(3fysq_60b91C|c+o}ScISQ+sBHVD6YngBUOd@T6kI!6 z(!ZWFZuYSudLpDdDlcF@PIN2R73~tyy}mb9^z=LYzgH)v{Zto=KcHi-A`@^7Wn!E1 z3=HIj`nD6I6)X*yI=jmQk^mL0A6?#H^58uh#J7~KgiMbC2x})iB?=fkVbS2mqjK&F z8JpBGvX#Ej{2|zHO|p#J3LX3TiKiUtN|lK{g`Gb^EZM!cFqRacri5w@NY*v|OhmY{Xi+8~>%tsNekq zX&d;q$opX$^@RB_=+*A{a_yJWRr_g#jBUM*0q#!uH-vXQ*y~SWUqVB!!qM$qRRTE~ zpp*%%M6+TO|~~%=Oj>S|x@N^_U-xf)VGG^%d#; z8Iv7dvd+jTD5NF629y-Z+$Ec-5tW?CHh&#$kh_8T3l3jR4c?=7P(JbwFOR5gn>ous z+4_!S=F7P%6}!ph9Ax0?sfVA3bd#^9#gA?&_#h)cVBq*mL8B#mR`9d%4gDWET?Ewq zuDdGMo@A)Nbw}0(oQ_>`0U_5MS1jy*$$jBZZi=MCGK;LjLRl5i9lp(udnmB5s6$`c zFLP8VR%q#wrkRLjqIqwf&_<673+hV^9BJ>j#J_7;w5dfN6H1J3zes*U}S~u^2xve-z2e77d+o9&%jnyu0?fJN)rH(A`Ym z+@^RS1)WPU(jMBVa-#l9TxCtT%Mvre&mJM4tBMYQw;!Pv_sNC5;;cCC?-1vGNKrzC zW(`Pq;Z&~sSYrgU#GdJZB3npskJgJ2?V^9v@-2CCP)ovu0<|GNKntooW$V~h7u3hknx$nYBLi6SWA zl#b3cp}nKBB9B==U;sVr0E`B?PX z*Qs1y-Cdkmw*8y8pw9nI5g#8W84vI1#`HBX$&Ms;0TA$pITp-2MH=DD@N|V=8b}FO z!+}k)&7(nb85Q$>-80VpTC=!YG2z134?Sz! zF55*{g<{d4Vl>fO)jhzynJ>Zxl-V`+fh1ecPwS% z1k#RYza8nX8m2I9?SbPUsNLB6hkmJ5{Vut3$5!d|n413aysK+0)u$=!No%D?z7mnl3V~C8{$R#&25c}n< zL2gU^`aS2Xm^KA^|IPA>4hH8f+*8q#K=z?uruv1xW+LdG?BM*@oje!{svhz!;KCQu z!f}0rlKc)pD_xW*X_E-+r!-*HnZ-3avKO>?`9C+vz+EyD7a2aT&lQfpg69(>=T`qI zR(?5EKnfHh3hI+w%WxBXd}R{ae&L0Pr@wz~#LQ@boS`3Sm$EUQ@Z1K~N}7xGNUzLZ z$ejPeYj^r$Ak0|-C~W1r8n!>k0%(7mt&~4m_M$UR>^2%4I~%xk-4YYTmoSzd7CbBC zJ>Hke@xox^?IMq4>lMH$PB@&auKRilH=u7Bad7_;0fRCBN|r!2`Mj*Iao$f&lUJwQ ziK#DAiuhaqa!^6z8CW*v4by&h%4)a>H_ddm0mId#?7bX(4uj;48^B_hAKz)yTQ9m& z?fQ58=FMP}d(>0r_#ULjx~C>=T97fAu5!u@4LskpR@zr)wU*m>!bj8VN za<;o3+7$Y;+SLn>X%5;q4;Y=52v#lJai#CxtL}kr%Sd#VA+kBoGe{QKNiQ!K+h(9A zY;0vEg4g1BqMS?1Dv-Kc+L8WzeRDsdS+<1`SWhuuep}yXR`Xoun;{vR`M4PUH+-P- zh7B;gI>|9Iv2bO;A#jv`yQKI*t#hy@#wehm0lGJT1@RvdaR_wQCQE6i)-I5)QW0NH zg2eUXj^Y1;aC8PcltE-1k1_P7@8x=EBr~f}kmbLNUsE%n`3^c){-t2*D%-CJ#Hh^M zs`+vMNhuyBdj)c7G|m05Eu-H?j%Rb)tNn6!&+O(@tyiq~<2td;qN@;NIBL&dT@T1) zyA#o0GXq8fLS$yTuK@BNyp?Pqy`x*>hcqK+7rrc{9trscV6^NUK5~znS1{VFpfP!y z?x_%5TKdYE|FgC9wc(8tm%78C|8~uWQwiHy8{I{qxQU{Gw?N+^HH<<|5xLg%GIYUT z)oSg6Wd&6})TD#;K3t}+d>vU!aFqo>BA;u9srk+UA0>^1nke_wggjN4lqBa3j7|l* zY@YjQ6dAo{_}IiuLN6eutn`X|>hZw{!uiDeb*Ejwynne=nF$BCrXUCHV88%^r2pg>- z${+AwT~^RVU=^iEiOM1>O<*aZBtMr`Rw-F!6_6?*aitR=K$?mQN>h;@h>F4@O+Y#% z^iHCJKxhdGB$QA>Af)Hd+?oH2dtcl)XXc!l^XAN%Z+o8S^YC4Hj7BKhCCop;YKTv7 zyv+*K{$CQ(`+<7f+orgvgUHKKuJ9b=Af{CzYstqr-owqWroBADNyDtOFsS)kLXLRs`dt?e0 zdjVB2b4te(hw=}c5W4(QEP}6+w0!6)AYz;{a8EmX6qr0vjKbHpTa!nnlXt)hS6*s*Gr{^PJ>#b;S{zhQU)!Bz>(ql*Hb5AXlMl zqYYA`M>?k36)5spwI0EL!oms!yU*L_Z*vo&5o!gw^;nq2%1G$$-3N-bugC>|PFNY% z|3qn zkq=dzPmDDGq%z@}Ug3%=u;YS9rUL#R6q&(u2R$Y|>@J&0o?HY{(+g{_G`zNEZk9tw z?*u1wJ49dP=9&5OqD{vj+sb?N=N^a`!95<)FRlMKsdC%z!B5>+(eVWK|H#Gc7rI_Jb5O=+-QlY8+e zMtfe+z{qZ?nx1!!oZ8wc*TFi8o#)rHkK{S@3~gPi4T&r(0{R#+?FA&J83c^=VwLzlAyvPMJHVs4KkrD zN?7t5`2@ZW?Q zd3%1CZ{}oHjWMyVS@mduYr);QO@m)BJLbE-19CM0|H9LSi;mh`CAN1Rk=pm+l_$?BlIHc-X=Vv;%gL_Sf|?9> zBRAb^!Mq3@suoNslQn4|t|H3e-L+7xjj(HRDuLs+^fAKtFvvuXt8I@PT z45{BE?Cz-ek|nfr)e<<$Po^EE#9ys`dV$;lAe>|cZblLz(#I(ALHZbzow7Kf$k56_ zY#l6Ik+Nz&1oMNDKoap(^Y~4A3R6qZW+)E|D2q|6!?@vYjxB!PjWO@P>xOL69IBwa z^<8I(=>Zp+m!O_B6VpLke)=8wOVoeNi*TD5IjGo=>2gugq}md_O^24?9VQ&G=Qoar zLoqS)1TNz3YdM!Q7>-d3a(m*5Up0J(@44`H`^)N}( ztN_ljZu$GtT#>yL4q%bI3+JwpQ*+uFntc^ARyB*WY(#}?b$s30!1pP#UYjlf<=P%l z>RHRwk1E-!gE~LJImCZq^px$z!pOe?9Mc!&>*~^N_qW*P0s;%MeF0XI&RqBvMXj!* zY)%CsbJunXH<=E^nW48N)Jsb5(itvsOjQ^-qsN#%DB8SfR* zPB}0d9$>s~z$(ffDRNvP2T3K2ji(8u-?%IvcQ?tKEq9!?19>>(5g=u9LX;E2 z)O1exT{ajT>h$P5!Eds-F-yH#a2$&Deo_EjqT! z`F7lCc?=yK+NlPl$JKV#A{OLhoM2+vwAtHA<#&K;P?2FE;727f2HRGeN5`^)}BI?^>Z(6ewTRDI0mIdD}dst7+;6vCq1(s2#*QQ~wd?9gJbv znoSZxwn#hXOma@Ds-(n1tMHp6De1w80Kyd;5kyuaJNfzFO$tYli7sCj;;oSL8u8~e zhScYo7RAg(6lr|g5-n>11-->Y4E5KQk|(JE;I8&~R}VH^#$kiRNA_F5HvYFN_W94+ zo^gKA(5@lszrgEmamnioFq4i zX+QQBK^)^Ij$)EiYB7tP9%#=7N54q)>$B>O*|?Xutp-%xH&A8jXywZ-qNY*z5*Ox* z3s`RPj_u{WGi5MTdRmw*Ma+A@7npODnQ9$ri^|uhYO~)q$%ic{CM9Q)FHcD4rw}sX zm3#h-?!Y6S$Uh(0|CoQ5UvSTNR_wZ0PgA5nltWPi>dUpPIR<*LF^j9*(H@^A}!OrqX}5nsBTAZbbQN z{=WvGFT=aO=%Lc;G~68UOx5HH7l!ksL)dDCb)^-RL-VEyA&UQ*n+rzqu9!CBr>R)2 zI3dxIi7c0l4z{=#_F%*x7MY5M5}51tDt}hp z7NulR=k5BAPWCYb*h;}yiDu*YZE)+3`hQvWSOH~Gt^gRYacdYm_#@_xhO1smCbNyd z*hQKY$vRL|fyG9fCw=eOS_gCY9c3)|LT{3{-aA$zT5xn>wHr(CmqcZc%&dh|nBFU$ zl3DwvGvy$i=J|g+;;IXl;WbJ>kZI|BlhEz7y4c@>S6qzi@c#e=qDQaWdB>G#ztDT< zHgGWcQX{jl#ImZ%n>CvJ%()&a(3mxUZ*?A0XuyHwaO8p{nGuKC0MG^+tr079N_O6!%rWwC-RjKO>G;Kdh6 zntAXaF5xBfr$A4}%2IO8ytS)x<aA9m4>D1@@Vc$H z~hLhTnE~T!uMm<}1Acdiw2G29gv{tR>21`##+XBq0JH{donw3crcx3r(%Vv)~P-DaE$8a{R z1GDwhn)l{?T%v05zpBQK>p$vBWG@sjJVz3Hwm)w+_dP6rt!n3Ga&MlaRb~DPZ)pU3 zUlhJ8;z>}UH?g3#!+K0GGZ^l?{5pKo@%;R_YQkAdQ**sy)r2CHF^8B!S#L4Ss>@IC zE9&2$p%syRZew=yD|OWtu5KY#r-g6b^kc1=x9LtM&-lL<1-5P-pd$}zZBhLH!7E$- zmM!{w@X_2Je&fZ=U3~CXHsskY6Zc7hXNgL6a1@BN^t3fT3A72`Y9rXG7U{vE_{YXu zn2A6)_7`Bu~0`*65&LYZEizf}8K_?q3zAF}xVETGv=;OJ$tuCT)r-F1HMRa)~O z^WC6On-~dO=_p-BJZjCvUXan_Jy`wcUz|7f9Z=?6y+!F5$k881$@oH9n{uX;N8`U6s>&nso=UUX1BBtn|*itn% z!(E|d@CUCKkCX+!gh9f*5=I}w4wsKs0b2)8<#P&H>L{ntc)yl_X@Mm_m6g|oBtSDs z9T)Q8?WK&?zCZRNN_~-t)fVtO&?c($&7L>HX*G>%%MJl>mSAi|R>KEx@kFTXV}F zZdj#$qye}BrTI)}#S59APzALhLtL2`ew>17ekAn`DU#)k-pYj5-ktITM_=L6dddB- z?9Rr3AiEjbbD4LpB~6m$@ppta(B2|_qBi9+5ufBLUV@PyHk`ZNJ>x0^v5EK)4pRa6 z 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 KwB<*|rvC+^jDVy7 diff --git a/tests/fixtures/legacy_pipeline/mecka-cartesian-camframe.json b/tests/fixtures/legacy_pipeline/mecka-cartesian-camframe.json deleted file mode 100644 index ce6a59f921..0000000000 --- a/tests/fixtures/legacy_pipeline/mecka-cartesian-camframe.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "source_commit": "ec5c903c067bf1b29bbff781bc414c6df2bcf1f2", - "vendor": "mecka", - "representation": "cartesian", - "frame": "camframe", - "identity": 3, - "indices": [ - 0, - 7, - 29, - 47, - 50 - ], - "model_keys": [ - "actions_cartesian", - "observations.images.front_img_1", - "observations.state.ee_pose" - ], - "api": "legacy", - "transform_arguments": { - "mode": "cartesian", - "stride": 1 - }, - "normalization_frames": 54, - "archive_sha256": "7aaf6dc3c82a95a2c844e76918334ad2762dd0e0ced038fe117baf928b5cfde6" -} \ No newline at end of file diff --git a/tests/fixtures/legacy_pipeline/mecka-cartesian-camframe.npz b/tests/fixtures/legacy_pipeline/mecka-cartesian-camframe.npz deleted file mode 100644 index 0529be4357c39392b696a4afe21014aa9a38b41b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38565 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 diff --git a/tests/fixtures/legacy_pipeline/mecka-cartesian-eef_frame.json b/tests/fixtures/legacy_pipeline/mecka-cartesian-eef_frame.json deleted file mode 100644 index 07a13e6bda..0000000000 --- a/tests/fixtures/legacy_pipeline/mecka-cartesian-eef_frame.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "source_commit": "ec5c903c067bf1b29bbff781bc414c6df2bcf1f2", - "vendor": "mecka", - "representation": "cartesian", - "frame": "eef_frame", - "identity": 3, - "indices": [ - 0, - 7, - 29, - 47, - 50 - ], - "model_keys": [ - "actions_cartesian", - "observations.images.front_img_1", - "observations.state.ee_pose" - ], - "api": "legacy", - "transform_arguments": { - "mode": "cartesian_wristframe_ypr", - "stride": 1 - }, - "normalization_frames": 54, - "archive_sha256": "d456f93c3dec0d8eac559f46a5f56cabc0c42f813d2e4feac78fbf34d92fe65c" -} \ No newline at end of file diff --git a/tests/fixtures/legacy_pipeline/mecka-cartesian-eef_frame.npz b/tests/fixtures/legacy_pipeline/mecka-cartesian-eef_frame.npz deleted file mode 100644 index 0e34574bf1b4628029cc34392176a93591c43884..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37688 zcmbTccTf{p^fs)bqS97T0V&Z%VHKoGC&{{~tkPCdij>f!5^CtAs0c_GQ94A|6+}8n zFQJ7ZkfKUo+=^gesC=d?u_6!a4g+KNT)CvraK6v1_1An^z+Vg8{ z{f|4a{#Eym*xl1dzJ1qw?$5o5bIkqy4(!JC(|E5BukIxH{FVIbxW1R{^CHDyvDZCw z_@1w4nh(o)dak5CfZeRXU!7ZW!|#!BD?AU?mfL4T-Z=K^sU$>Q+rVAx3%To_Lt=dX z!!o*_Q~&raG1g#+7QM$*`p>g^x{|OTcrJTE?(rpmljymPYsw%$nKi+D0Q~3ntRNJ} zHYfbQ73_ybX)s7V`taj{EN$8efg{;n{v9$=It);MxSoKvDdk=m&U(S zUcOYll`2pDTQx1W|8l?l$&ed2ZltQ-Qquh~M~)%3E|6(M=48%nEFKhFy;Bug*@2() zN3-dO4Jd#o%y2>SRy8n)bvBlf0ue3}Ot%+zB#QZq!!khL5eT1>)6PwC-;V#a^2adQ zP{el$EQ8lZz%p93O?QQ}coAlD3+mZ&Yce>i}o4oc7-PO zEh4B07tvrki6WE@XN#lvA%UXh%p5Ip7^ROUqvbj&>{`*t7VR`PT_wX;$2aKujbu)O zktSKxZ=@ySd;qxZZ>j>E_vd?dh9e8jzwEuA4uZ_FRMA0#HD+vgRprlBRrDsLQN44E z0K;;2sv#)mLXIXjbYpop0L+tVYC;V|CQ%|q(Zp^Mc9S=aK@QY6wh@e~g(cv&yPMN9}B9S+IIC8v>De}U za#P3Z4_p*^vjgod$Y*lPr|ZFNLL*bOgKpa^k(CQV?LQMgi^i^q@8dUGf(-g+cd9=} zL?XZUPeq{w?-S4N?a#^}A4UIc+W)v--NETFhzKYY?qcgM6$G`flq@NW=$IhvLjS$4P zCIJJ96mn^3vcTGSQaH26(T?0-esHN563cA(Nv<|^`pNcuX^t0iBLrziKvd)?&#et= zgx#*9Yb`RPoAxrW&9k0x_?VG!rASMgG{kG|nX0N>awN0cXCnzD)TWWB1C`D2@K?vi zj|2+&;G5W;*=;6~%;|hB5H;^4xrpW|<?;Lh7X^!)aUlmd^V&q3mMEwF4r zQyVBK5;?ZNTT0%yYcw;P>^iK3S_{D3-(|%G#n=z=o?!$td`W3EYojy>yPu#K5inbS zAqRk6t17zc*8UT3gk`uQs^=i0+vFGfx2 zRlX+}96Ip!ryNqi69)xx7y}!y?a;}|t{1UjzNIKUc0finTiLk|d(8XZ)b8u)$Y*^B z;#DXIy1PbgO14fAa*#g<0#+WE4ip74-8_2hQ|UX zqZsvj?BqBRf}mrs2%ak-pDGf5rei&4fc$5wu1vTdcghj**HHS*GH)HclaV5a7WG1^ zG87xTsx~vA>x<3vVy=g#eW)?*;l0GYA&O!v?y}E-JCM_X<>TU|Ob7DJ3N%|bV4s?E zH0Mtz{?JH|?`C87KKhn}_ja3dU?G=Oc-=-w{_Y%RzjOyiWlN%Zf&BMiojYn#g{U1| zZe*)VTXse;STH-=6d{El7a5_*U05HA^k#Q(5YD}Y>H&t` zC~l5V-}83d=#E1iEMOVA?)_++dqB?A08)XuFZB0fXCwD^dO&E!DigPuOK{{=i1*gi z3R5(_1l{ZkLd}aw6sTFshkzBj&Pi_?Df(EU=>D6MU1i1 z6?~vAtgKQQt+lw;Sv0csBt!$*E#sPGHlH@)8ISd@--cEQr9}0;Hauw}1&NALw=l#O z3YN4r&2X_485XN$ZL&spSv#<+eDUCi&BL;cW8$l-7+dvTeb87!Y#*3kBIBc0!ryAk z4)7{1oyaAbyC&Zr{0Fl96pX9X0qUOG@PznSi5T5%59HK00$H?~Xq662Ro>eyz&IGD z*de7IN3#?ISWP+{yNCr6Uy`j9mT>`T0DK3S9gX8qQ zt|P1>7}puyjxCm$&Bcs`xQY>bk4&j7MdfpAw#EMBOM$wP zEj1^DRu$dcv&d^c2$NZB%;2q7itDZf^ zB;DLn)qFWdx}?>^Yz7qXV9aMNP`{!!@r`J`<<8KlTfxc9!3=b%qLblS*XQ6bs_9E$ zlCg^++5^o`QN^h*_8nbTD5ad*PcZLO39~Axp(qp&S0RHsKiL?j53k6idnf_&L}Bf% zx`Dc%2%Z_XDrrB1DqGZxmgV+dXVxx6^7}s4oS<)~e;^%Gn!jpTu53Im*BYl>sG)LE zzr7mi{X?yt8ZP_*4zM`0ws0_@-Yla37Wev=!R(JCqDl7CR!5iS#L+!+#(q=X8NyUr zZP-yco#*-?7bL$YHea{C!4w|v$UK7n7lJU$s|i;cJdU=%W>zVw|xnn!%^TCFAa8o-;UjHnENv?uw2JZF;H-0<7l-< zvJg>%4puYlItcq4GbrIOgh}hwnCsc8u?7g`P^$1})`fkETrGHBmcLNStxEJh#Hl z#ee3hf2e5P0EFzm-v8^#D0lGfYu43@>Mer^rEH;)vj}`p}MFU7WZKkBeb1kJk{=E7i-YA zm2%^cY@HBzLSdO&!3SJIfo`jb1m2%Ws`g|Nz2~j@vu5RD;fv!DFXX=1N$ZqUM|}l* zKL_ghT7g`CqQ#eFJWm=W*MIR87N;03ze2X=5I)K@6CurKn`+GT1`GMVxEzW^Zi$8N zNw<}p$u1zwT7>(HaY6;CIF~>SK9k3aHWqX5J5RZX7@gQz=MwjmVzk`P2gUe#)9%4S zDN2Zq7?d4)ULlDxVZfvUBdT9tdCvPk2`~~Lz ziJ7e~8j+Q*>Ph4dhf^-kXoG$NBA#0DZYX&&I*D3)kHw(%(XDzY@3R_NW;5*T{c&-F zhtSLAPRhGm2e4mD+c1*z|I!~T8ul6`eeBCFhA(hL@P#gYF0Atm%}Ed6tr3=B=iw0R z^P4;9Q&fCWtQIO!H)2s`_XL_D&g?yfNhd5TfQB;6v&K4_z3dyU2>l6cUO+&XxR71H zO@V`w-Ke^z6XH(q6ftAfPL}3xn^}J^Nd!L!y_Qh?7G#_j2++blqk2jv@Dz}A0{3jh zRlq2ICe8x217vp3#(OLEZjj>T6*>oRW3rg0pG?A7O^)|BPRZ3oBfo1K9?h!q6pQ+S z96ETv%@@1hEd$#ui~0*YlqB=BIO;J=lNQ{3h)i*l_pTNiw*Bi*xL|>y8rC|}G94X9 z4i0Yy!fJZdJfFEPzpCDb?Um|>s#|a(RHCmsEJ%?4i};oJ!fs)sj>z?7$=_(BJRf0J zRh!7SY%yU|QS)+DlgYd!)VzM}OQqkG@WS8RM<67Lyjo}ZX8?2-7D~}KhZb`G25|r8 zq+G%&3a#M?&%5TI0Nh;bm0;Gn&1dAD%rtd$OgOFsp@4d)&i{cn>@r+ZOpCXPQFAW= zka?0?D;TY1vQmm`T8hY9gCCdTk)^cwU*EsnBWfObGm9rC!HlkB=u(?nY0~0sZ!1k@ z`*OmfXgS}`#yob*5dK}x5!Ov#F5SwM1D&!`6NTIcX29lUN>j=`A3*v%i_0^-4o6)h zKE2y{H;mY|vwoz3{owwp_xp z81c0XVnJM#n%O=n)}am+vc3Y7r?(Sjo&rPAp(Lmw?3Fa{j?f^_#-*?wk5~-}F;o z*ORxj&CUe1N@tVKDE62v63%Wv*dW1$+q1!S{sk#U#)O%>*rl)L5b(<3ggyhBQNIO* zc8PCV_w+*OfIcj{QeM9*&KvbDvR90e=en)POm^lzGF4OTjMFp&xwA1-`O#VXbytbj z(%C9FWBv$aL1OZ4>F<`*OWIqCNdGpn)P2-fr_IVn-pNoMExppnh-8;9^q2^nejgn3 zoDBJJ5bbx$8S?;5?)}sk)Rj!^F*)~7^4W-$zh(|HLyy_#?Jijh^#n$zmNo>lolPke5{xk!|JoE38?5CkmEi`UFTJ?m8ej{lsB2* z&AABu@;O?vqC3s_wkACYlu-My_Y%-XL3Ds^F!6?cx%U#o*W*~Yf9G{I(%Rw~A(_w;Pd z1>^)I;&-(K=XKuP%(#+eY_KX?Os4-WV!&R*uh68*_}u;%1Cz)t@}{&ks~naLd>wCm zWOwK0G(x>&RWuNK$hq4Lk*uFO(7YTRs#$zof)W_|91zh1=0Db?Sh9Uh(=EL*-3r#q@du7E!+Z# zS6hdJpN$O6y3Ol)?svoXdNb7Xz?IbYtb*ztWt?IjdA5GVK*EY{YZN8jIEigU7Pqif zFNqnF;JwKfuIeS@jcdyVqEd^x0aBP(dIfq@LkkMv9~Vbd>{A|YmFC`LEW10Mn$DM9 zxMqG{`gZ4v^ChbQb_4%~U?tu>?WVc4;naU%@7}VhOrwO$GpXV`rSSW{0z?uyq!qfg zM|;N~0lb#kOEUN}3f+qLI!o=l4SQRu&WZjPp=JGosZ}LZ-=_(d3$zzgxT*6@?1m5XlEwNW9j7xZ|ibfVEY@JmpKbD z6U8mXAV&ZjV3Kq)*v!lmU`#fiG%W6(73L6D#4KxEkQ~YO^*laahJU;_YwWbeyhIGn zZBfPoaU?bCq|)P*&dHjVKU`nwJIezUpI-`?PmiXjx2aFyW8UW6SHk1De}sB#^yBe$ z4lBV+8hyX;#r4oWr=8V zeP&TEjMqy1g0Jbgj$*s&MT)831t>EYI5s(XF+CF4-g88dZ}4n09-B4Y-pqOmf|dtg zl(w6rHC%#5y_10}!;cY%RoSFwON2>_Hd}M|BhYwjubtgvG8NY0l#bYePu!_Pnj8To zSBGN+o{7~1Vm{g%>*3B;)gS+uNwM{Y#bMX{^2J&$9NPV-tQBp&M1MI zwzA;%CRl1fG814&-g(_;Ydf7B8#+iG z?EYgzH;|M3ctFGgM(4P+J5ToC-t^o!gGDp-OrrM9*l9nLrF@He(k{j7^;sGp1w=J& zy(9+f2Chb)Gl;yP*y>e)XyRUSQzz-Umfi1}kj3D^}&;oV{x~J_{Xdpz;SMrTdqK! z8HcJ##q(D!3?Tj2DH<``W@d7-nGgoAjult$@Jeez0?&g`&krm@R3Q6DtM&p55wd{Z z(i*efci5}l2F*vKW`Ipf}-8ZaB=^fa!! z)Z}#RdvDhP!06PQyo{v8kPrLG$32dm9VG8m$r{zccWJ0rFPTN-t&_ze%5CpeSs&nt zF2JTgV*t#_$_vR8yw-OQ0T=dE`ETdBuN=;?O}!QAbFuwVGv|V9h<`R5IqQ(ZlprL4OgJAPijWw3h9M`%`h; zy^TB{bx?2KlowfOF~L=;*b1JQo4FtD8mg0SNfodSFt>|2f8ic9E8QBo3k~=T+O%W* z);Kh0yi_^_a1Uy%8@6?O60s9db7xc6yjk}v5R;JN*##PoABb33i*iIsfy1y^qgqS9s34^@}M zyL;b)*nhI87?)z6@J_S5A}J?Y<1LWgfbC=G%~ce4p^0?PIZF;$Yv4q2S^42aVpMc2 zx~u@SeQfsg^8LrK#_`IV#4tias&ZAI1pk7xq2-H6r{@<6Uyf~)g^|UUO%(aPzQ?dx zgm|D|^;0Zqq++NG9QF({dWLk8_Zq9DFZ|X7^^>A}_hpT5|I4JPpr#6QD~IBuXdnDl zi`}EvO@e@!V%)o-%b>RA7p^;TAWs(%eoqH?_KO*FH@GyUAhzoeqy(Ht?flrUYjQ1$ zC_S5*=RcVQ`hIeb#+mUh)ThVU6RF5XYBcvLp>M%UL4I(mP@^2}bpjzRLs4tB2Cbyq z&@R|%it;1vEU$>}K>J5A{t{|JAa|?kYxBxYm0Pxl&JM;CyqIy{N%isQNyE8>2a}ty zP^!cbrlZYW_z^ut4BFEHVPbDpH&yLuZVssXXoXZX%ZR>RAmDjq?LrP@A2ja#3&IsyZS^10y$ z5$4P?l99_d^_kqAQE1Bag81CFINA>7uBDPFUy2$kU$N2&shi`x(wKv`@t?w>m8u02 zP415H*P9`8Cf^Qf)(22ek0oU+q-LCtT+h;mNJ036aug5#KnpB-_pQ8)E1~#2LCvaN zLnQ%@dIdr-BM}~RNkmui*hR~#esb){*P~yKIC@zqsh|Dsh`KkU=U>jX2ZwyYY(BU2 zaGVa6aYkn%Px<5_|8=gvDxNIHy;>Llkr8J;`RYtND_)p@BJdwYC*{S~+3TI8_G)(U z@d#(v57;Dqna_KH)m|P>7gSl#EDBG+`>U{^50UGSB{Cv9J427~ZI2eemjcPzaZJDe zFwyGH)bVyTzL|cce-yASr@mOUaDAa@^tG7F`Ph)G*ch$)q7KNSIn&N_#2~C!JxRIG zvHyy@tb~G0MLu>3$li*{`OdVuBhs15*BW$u<-7zBp6S%bv~|MLF!O7@ zOEM1l?AczkXhQxU@^O}gQ;thTn^MCS3*#kggXi$-157i}u>8>PYha8M3OWI!9%BC~ zib-=#i`-@U2CNNR8CKy4lrVlD8`cfwrlYdfsvDr>nyBOS!sCdc6<0H@&Dfg|bdlZ=Yk+#K4#W;nk z9of$1T=a8iQ-cWPX&FUMMbkW{9z>b_Xi?Ym7Iw)L`C!y@M+(1^QHeh%U#HX0(2cfR zk!l1w!ae{P@QDvBc>n8;+>EP$oN!D=rP3*3uVUw-5f90UJ}T%_4eL+*akDw3fra2M zWu#qbVYd&z(EU|Oey11{hShzkxb=ZK_%>+1JXp&1g8h2t!N9(NX!XtF{INQ5DsH|1 zg8Neo-K$z2)m{$GJIWZ3!hT@*xxQ`6(V4Ef2CM##4Ja0TwU%tQhSYDlm=lcxa~M2I zf}NU7#M`MO^oi}~10G#UXNqx&3nUjKl4bM5-m9JS8oUF0A(c+yyK|dyFza?pOC4wx z9+TxwpVW|EO03J?vmVVGI&If%2_}*H(iu~^^RK}ZV8RFO+)Fa33|sC&jy${Kt}38z zM`7=g7<%D9{HP<|w(4ca!tF-t+=zloMLUHdCTnGrmQ+z><_dcGi`-G+5`upHmIuPj z$j?oh(U!YqXNLVPFe_r!-&?pR0QKO);oSv2fegeMq%`LjbY8LhGJg>KDh=(OD`l}j(RM@x0uAXr$< zr;HT0#mFZ&L)tx+D5~DOsm^A1v=;}knb%<(48I8iC7*#r#|$Z|rKN!l1D*7s6Gp5% z0htVV;)SSh4%6&L;+xY0v0Ao?>bpao9!`>da!Ju?1&EP@9yWjxi+<6!7SO_9GwOcJ zxwT>0kt6C)K!=RH9wjMcg;dg z`S~3$#nak)eI-biVUM>|3oaNLW1Li~f+L=scZG1eI-z#1uOlDioRwS97kjMNmHE{)*N-C5;Q|XC+0AhJ6I>9PNIN08y?o3zYCl(9V4hW6^Ukd z1y#SkpPRVlmO|v2$Xb^z>p2N`hneHU#j4qOPKJicdnSI*0bfAw3HVi9TC@fa=_`-N zShBi^ExY@aTl}Ai=5)9CBaOGJVEOA%5x3IUC~UYS797?=?Mi`VS`WTnS12H;(YvZn z*{Q&w$k&t4QOlQHvka^d2O$;M`hV9%xYg|qGP7(bx6 ziFK6&I=JpvW`k>r4o>&~YXd)x`VBx)H+@>UsEGeL)lji8j7YZ2YkuoW!b0;2wGh@% zHGBwg|Ajs1>Kgx^n3d*J!1e>|R=U#-%tHD|@ITd+ngG3)u9$7)L_ehIc7dE8R9W9r7~?@b=0o@NGk?t;rv0E`!zDDVdW5XREa=_Ay!}IP4C3e z!U-HB#(gEaO6#+XBs&taxbEYtq<#o zPI2T{NT;&sptw-Wy-P$p-#a_?Z%~xJbRof#rB1n3cpX%48MUiSRVvJxvMXO-R9qIL zzslS7umSZT{wh9J98>8NQpY5H{>3fzwHhs|7}rHy|JK2W-yAoHs;`GCY~mo3-SqzD zyx>n6AgI_>e@O8ff1Wn`QKt5=>+6+@`rf@V#ohg{@J9q8aneuOpu%r&sf7YSQrMFQ zE(s9x*TRu9n~4={S=hCPi!;MU%NbjF8RsnLPJ-@7pN3yY{&3v$G2#}xt6AEKd@RLT zrxqu6GLyBT%m-~2P&KJCAX(i+1rDB1xkF_+@(U5tn zeUCs_evA1{Hd8!3p@6}Tx-}~EItB3~Vj`2`4bLaR7lQ(xCuqDi$f-U;^)*{vvPN*>Y@W)>CSR5SLHvH zuX&Mz6c^)PrrqhNG<%|T5CCGklI_;bIwkRwQcASh!6!eBtD~ihY5H{-jh#!&ro3;2 z%Olb0Ac}M0n;j4FOJb&pm&gn0=?70}&kjt<#iQr1#UhxqaO1tqgN1_IInD9alDBLh zXvNQ%PW&M{BFFzKKJw7IeW0^Xi;>~hiV5&d6Oe2sfJ64cDu#SjY9@pG7wYYtA13UH zQ$Hd3h}r-ipK272s?cp~XPsjGf;AcaE+<+>5`egd1~CtiZiS+upG#6q-a~EE@=5xK zz1{pP7&kSo<)Uu7=5VP#K>L|v9~k~#UR-ZL^Zmk$v;lE`e`hdaP7IJ|@W8hTVK`el zN3$>h813VuCj<}I3Fn%sj2t;xs_37BRe#il#(e(Gn)AiK%c7pFT7@G!%MRKz-6&%9 zwvV1%pLeg-kLHhVU7Ft`J5j5yfkTq6n zze^7iUMK6M4Y*cAge4fSE_eI33EH^)e;PA(5u?wRz&Po8@l$fiWn4`@+ob)mJ*GtK zlnF&9aVVum&Ts^(kTu!5)_yf(!a4?myJ4Jy^%3ak z`~S6^%e*MzeZkxC&CYD8S(AONt;X45)4G%sab6wx5EyG8^&nc+Tyib=rP*fLj2>{M zw_*r@zFg96;NxkIIPTi>CBz!|H|S!+KY+`Mi*g|{=1|)h%cYp@Bklv}t`tyRK&^P7 zuDjDajS$j^%#h05kiF2B<0kjMm=3}$DC!xYTs^gnc3yad?l#6$6>7QocyA{gd7^d= zyKIOmnj$r=9ivp1K7VwMl)6?Qv1{@+$Y$@;&pV<~i@~GJk(nIjZWVA!lNr`Ac9MI> z@HVJ^_hR_pDyzV!&?_okM}9@^+y+Z$?Vz+3skik1zK}~!)_~pe ztS+tCr=o~a;4Tqs?N=Ym;JE}pw_MTS8SO5~JnC8Eyk=`8c*b@XxVtgsESDs+cw^Gk z1Q#Lw)o^laT;9jAW{F>cMZb$uU8~+Gn@p6Rw`C9wObojhFU3bj^fuP-LL2_I(u1Mh z;SXal4b6O*t(CjZ_2PO+a^XTNTq|Bf&zBu)*=*i`u-$IkhmU%e0W~)~EW-?9uf@6r z4EU~3vvuJfIXf9=oMiq5ymNoB2(k*yJ)@m$znv3w2edJN z1%8VAZFgKlzU`?UAtPP-NqXN|puWNRKu8^!ImN=-5B#(La+JO|yWs90a=XJSF><{o zwocEm_Bcr0UuiK2bj)D}%tm9^%6D7cp-U%5Tb2*2+f}_svef*&2|wNKw-hzIXBpbR z8q+q-*3$dhnoqee`lR{pvUONfi$QsZaJuT0wXKzp0YqMKTXyYyiVz<+iyu{j>lJJECN2`$YnaIL_NF*#0*FWHEDKAncpJcjGTz1}r#^yEfIA z$?b)E7BGVQ7?laN4DUNgCsQk_rPyK2XpEw(cBkRl5P9Unfm&59qBvrF?a`CU1A@`E ztm!MIhhyD7M9%u#YiQwQgBMYTIeR18S4T3_psQdK)nM|Y;udnZOXdDXe^l$yCzX(I z(t>VYN5V(MF|#hH@Y&Yu<9Icftx7t}M4u=}|0?^WKyt-sZ3NQj!xWSy-`(-wy}unh zNV&-V5skJ1Z0Cl@>L8c4`Uupj^Hv9G7;Znx<7x%?+EIsM z67|FHczlzWja3;p|6OnxqeqVQ^Y2ye_s5bCC2 zR3 zHxS1^NZ9rdq?NX-k}X7B75u`{fha?~os?)KZu$8ft>{l;<0Sn$j>7n=zZK%$j3_TG z_FvXS6r0#iymOu2VFhY!{JzFG`0$DYjkBDC&EL5}(J(ov`StBspGc6ugX@6N3boznnV747gFCbuxGyo`J|1gw z4=Yys7(mtzi5_JRXfFbuosZrC}Dg%5DqJ2%-;xlkNP#^-(XYR)X=e zw;5j^CQZ|Y61?Acy*uM%ht6j7unWbXRwFP|Edy7Kj#IbG$c$qHwL8%{hRm$sk04qH z+wl!kCo;jt`#0G(y!^=J807WQ@3V>in*s(PKGOA1^=44Z+J|2aIpQ&=Enjg8&GZdE z?o<+C4^+IKkb6QvX6_NI>Zp?8B#TKstTuJ53sWSyk2qK+8sI3|Hwj^;O+TV zdeWrOS8BYw(>AyHM9@kO{cHKF+4h3(^zlo7&)y1Aaa#eH*PBh=c8#T;!ob@@yEY2o zhx!5MWD8S(!8HOP9N+OB#7JV6WJ@AOrXGQuV{0U=_|~Yb55Eyd+H?u3Vuqt8FiISqjJ#L>37N2LG)mwsDgEDW?>Nx=eSHy{=$OKTV3B9wMwx!Jn{h)v>T`B@~q?9*1 z=>eJc(rfmNoZ5~gV#|JsULoh)lA7${6n?8aWKe@H+tI$Asc)J7Q{^^mJH3i|wbNF4 z8Sc-0hD{P{FjyPe9@FpB@p|f9{d0f!bF`&}L()>=P)J~IvlQmKWETUApJ;mh$_^S|hz)K2qiQ@mH!0M0 z+f*B_Nki-C&TA|J7V=}Dx>-A!zX z&c93(GtGuT;Yi!(xE__+t}+u%%sU|W@6qzO{DBxX<)~u%x3t_lSXNa)Lyxws#qvN! zh^oDwS1C7qH<-F%GMuV3TOhxHrvMEPk>}dDogC^<(`rVvFnKSZKq8JrcpfrDNXPp8 z0nQ6=;b_58u1|W+%R>K&a&FHMo9v+1#+&FL!{Xh+=9rz%#GG+m)6O9Uc;X$fJ@KEm zGIY!rifrSv*pL&*tmDzkX4v;wiaRfyTaq(w9A&k2-kBZOSwN4pY1w0g9)AhL>M6F%|&UX-pCd_YkKBf#sYuh%##G7o8PPNCKAKA@xJA_2m6g*t<$x+(r z#odK<$#VnOzoUuU8XyBvzda?Y^JxYpW3*utj46{TJ0q&P)bg>-QN+gdj(q&dA}Qag_@bIsspkegbhT~4u0HbY^KHw?m- zPfR=-c1n%2X?35rcdYbhRIMbtEz7mjU_)z79TV+%>)%9Q)4mhhB|gZqJiNCxQ7b!C z+yft@aL>(i)8Ev2UG3F20cdn-U>A?_kPs2D`xP=t^eg7{JHtCZPhHuz1}%D2EyE`h zwDk&=UhUg-9NXv11#EV>EyFNr3dAc8_V+e&tJP5ce0&cuTXmewg&aeYon!Beih7vmEy^RuiZA=vf>u%1&oYy zVuCn3e(@BG;ZLig%=h`_WJ}`ug%<_e9s7}>((PLg+_M;3(PVdFP|av@-&*##$*20L ziQultT>tr4;q4q}v{QK3+TK**vmfyPUZ0foQC=+ih>pIBjK?t)39X7VFpvl8`%bV% zkR)L0EQbpu0?J!HIlaZ?!n@T7@5r0+8EylRt(~ysNMO)}S-lIF!oDY9Y*5BX7PY+(SpdsJa(&zC>a6g&_Z4?lI8S@K(ogCd0&o&63_EI8xY_{z~rH)b-K` z#%H_>Rd&Hbar-8*=BnI6EGXiM`492VI6!S(C!zJp{!xszxov{IOS*Y#-W)EU@f7>j zB&5v5v}Lg!ZuX_9{vuB%M5^6sOZX3D`U$3~lq|w^HIt8^yXV^mBB>+(u1l@!NthuY<-`b2i&Ywu@fOmzSeyNlb zro*7uoN;>1*OC?6X@rz@os}N$ZrOK)XB^n$FF|j7eU9AG?Hpx1DG8xk^&j$X8WAfUv8Asd1y5a$d-Yj~ICWrNGhRJq!3*_`2@T>`pxDLFYXsOLr2K@4O@J z1Wv;)If0O?_RD6rzvaC2B{fFSU>SuLp&?u4&>f!Dj%x_8ps-z6(kEk7T%*%7j$aIa zqot8I`Y_pY?Szl$ylbYBFv?-~^k>gP18w6!8H4)^krT?jhlJl{U4DaaDn#9Z<>oge ze#UU2lq~1O8){jRgZc4x8%mbz;nR493&>lycuw+5;9$i9jQM?xLSu2g;g>x9A??T0 z&O7;o^mTSVtvl>MFHBNB1~lsnSjYqsxBjYhD`wv*DULTc0{LXYo=c{Zqj(PdckF0L zf_fjeJ}0ZF`!D><_E0l{J*kSYYz%9Ol2yb+?BlE9|+hbC+uNi`eW}1gA zD?Hr2=3e-Jd7OT1nC4M)kP4Hoe)V=T6+M*qEb)p_36pV7+ZK4eQO{tHCC$sPD>acQ zLWE}XKf=>vM=x+=^*m(C27)pwwYf% z_=C$kwRalRJ<1r+-5|w2-q^_7R!d)s9IU23ts_=ZTQkObwY8mT%ExRywA~0kW`b3- zQmmgMUrHjWqdq%HvsC3vZA#nHFLH9$H8s)myuo1LG;{QHP`A$hwWg=uC|nfpF2P3v z(YPC96NWp{k>0XcI7xjpR7R8xqDlV8or|mUregN6^>cB6|0Gzs(GmFG5k9lZ-^IL` zZJ93T=L!&Q|Go5d%TUxmFn<)el_eZH<2Ynb@3^=6r7P^oJkZrd*VL+LAQ_#5H_#kf zuXLdNPFUHRaFr%xfS*4`zEBn(0B=7=E$)*FdPJGAyVoF&`;fx;a`kGE;KHdK*Rh6h zX0>@o%dw3DrxXYaIMd^WpG7tk;chJ#A;^Ed$_iH^p}c#3hhs}a9k(q}`!scmr9%Nowt_}L97YFi;0!$z zI<}QO(6_{GpGJFkr)-W+m;)!atQaY0kvOZ|T}2{*<&{_Ys&I22c{{r(``PVz!v ze&NM^F9_|e@i(quFQy~owp2x%e@FR#0lQW1R&a6dgIKtOdO@Or)4v3e;=73vI|LIH z+HDe?)5FZ@zg*~b7)pWzV-vxv-*y7(OTc8NuTN zvV^DS?;a!SNw5WCTzOT6{)PG;E7#F&B0-clUX9i?a!Ie$*u8-tgGDW6dOVnUZwD&S zKj}$;F6QM=m<&{{bvazMGyHx3-B{IihlXa)qh#tuexd&Lt*7!J|5I98)A+Xbit=1$ z-GCnSupKZ83nVUI0-7}(mr5rFllVgW(SPL9?eF58% zMQ`da4uKXV>}sV7SVtDjo-K@51!fGfDPDRz48$mN^#0Jw+VhNpZCWU+anSSHpmfF-@a^=Do%v zdy81U^$I1gkQZ$;Iz5EX1xi#a$||$8Yrx@8cV@Yy6fAv4gD)#P;{17N2UF2tZbH;D zD8%*a`~~uYh5D`;A1v`8@6|V+$Q~7Of8R6A`BuHiDW7oS>4u!OZjp-Hldk8BF<~H)x(r@;s;x1jc{!;4?ZDX9X51zQ=-e*JnyP7FT zU43Xj2x>F*{Ha@FQO6-w>=?==*%><w^bF@%qd89=PfH1lLr;FS;aQ zZkf1fjbTli5OkOywukHt_2_y3(TLw!v0=E~c?^LO6S3q%0%E_O)yrwFTf6Ug71Jt5 z>$_D}-p*jZgS*R{<4In$%M_oGH%tW0ofVY#rh^MZK~;ml`(OA9|V#s;e+b+Bma&`BwkC+$?kkWM{Y?9Z<<6l^zT8MKIZfO-+3mNlYxor+#^#s|= z0J+T^XZ^N^nE=i2vlX%@OJ8=xid;s6VrB!DZkS^Nd18i=!~Ex^+$Z}|*&Y~doK3`$ zES-Ef*#U=h)^=S>-u3ULBM$CA#$zz1-$-J}Mz2>@)sFiqsj{l%yU}%p3gQ3gUJlG} zI0MVVyk*+XPFV~W;-;C7R$#b_gsq32*I|&XVLe#n^z#RGdh=yxicQ~+&%6n2a*uM# zw7(mvw&t!5o93s_H94?V2?Mgtl?0Y1`jEu}J+8_o@>}eXmbNGfP0DiBK^sGVsa-wr z=%&DZQ~%Lfu^{Dw9cLQnepNSgTS}~>6p_V#kxn$bL40+&$T}T8VP!2X7PK166=q+e zD?w^+Yex9;bWME(CRt_zU>(_%{;saqr22)_cYP8x<4FsPI1x08pj}YjDdfCJ#=sW3gSNk;t=SpRi?sBjZFYesXVTX2#M{(9qa!a!qysW zR|Ju8T*lDb-dAfO5zNd2ex@&{e^o_~>OJUK@wc3@vvi+45Ti72t>Ux$D7k2q<1hd?#K{d{49NA5xE)UHH0?awPaSfC1exY~=oKZvJSKoZ93an!8+3Ny%$N-mliu zH~Ke=ooWw*{^OVor{K3U*SiWqu@i;8J^Ky!8h*A62 zhcKz$vNdE4-dP#|iFlzNs^UEde4IEEVx-tz9sEpgQk;}KFgg|Bv~li}K}6K5{u3h; zF&+Qt(vmB#DaQxH@#hmB)ShkD+Drw zn0LHO0B65bY9`4pw%U0~0qUlwP%jqKZL^tW+B{$t(R1 zb8tH6-#O3uzt8`D-of>|xGtZ$Klk>%zxVx{-`BOGzNzFz;K_bSia*(+CX@Re*@SLCu=&zR zh~ODU_h*va9rC0Q&nSoNkWxu2EZVDRuyb4q0H3^!6@VeC4}X>nfrTs2ULxh=Sb)L! ziCKO3!MdP&@S4Kio?7b&s))vT#kd8V^gJ?eG{LrZc-H49K)0nH_i~*+ zGj-qSYDU#b%q{4QH2ziPX%wt%^hZ+CXt20hqB1a?nX@@FE#^i`=8J{F*oLZLiZ-Hh zQBcx6i%XEcm(x*A1=X>c+o$95NNJW~!y(09)duiX!05bNXnXfwUBaHtsb{9&ewyWY zoY`5#53Lk{KNnK8@z3Z@h2@naO>Lmh<@pk$V0X*R#WLeNhR~tyvtzA&$B@z-d1rY(%ke5A~yT;Ynj+2!3Rcum`nSsm7a?rEARu%bH((-c>jr znb}A~gQ9X#qM&%IPNwu$zX|0@%Q`MuH(Q%j%9ZSlvU5IJ)8jYZUi}s*r<^j5TNH^Y|cPB2=$jhx@u+ejM?>8s6CKa<1@jeV~ty_9RMOb|u;-ldbDSPoI= zdu@)=98x%2gsMSqfiYmF7W&{n&D9%1*|G+tQVY&({0g3A0FQ63ik8r zDr3Q)L~t1%@EccI5C@Bz<0h^ojj`3_ly8MSa#d6n(;3}Pu;cT@hGw3*kb8K>oH&+6 z6Quk&6kJGU@Fd>T?SCNMu-G4F;l@)RoSWUc7#9aM&?j6X$&}cJ(GeL<3AJa&fe$V4 zjx1Kio4gC{U#M}FuQgPtP~5w5QalqZe26C&{h?`tC$TPn+-^3v8(`BNb=OoObYD84 zlVNOaNdY0M$>XAZq)8}?PP!jmLfu!B&Y_mo*D(rD2Vr43^tw-NB*_x-G9;+fCgo~2 zP0AyABnNEZkf;lLL#;|>dqYTvMD@b@xN~yCG?%v6X)|RE_}CFhau}^=2eKQRLuR(w zBVy^vkr^%l=K$Y>)mnL2#emw1T9Ff*yXIc&uwF^jGkh}cdg%GCqNtu1rWPPBan zzQ;IyV zv!C0qSKGN<5YX$BU6F&3(4gi|ak;DMzipcW_q)sY^7SDQ>f-N%jmPm@ehL!`AG}@^ zjiQ=CVOte;A>a}&*ZAB{NJ4O6LUI{oz~}iK6+Ow#_gXYTGh+m!#O^An-mK#&f=`LS5rtDNv&~~0sqn?i`1u_ogzDP88^rLDj zwGx1u2rXKBQ1pF)g=7`sgpYc}K!^GnBi6iZ*_>MaL|oh_g@k&9zu}u6m>Qx>2qq!) zZci6YC5hIDBsP6kMRBTtflvJ0-qq%$kXpM%0h1z46;)_g4&7nmG@@1oGW_Gk93BJo*(Xn^G%)%iQ^sYHsp zM{Maw3|7v({h75*$49z@gGh&?XR!aZ8ntS#VLSgZL4d&04OEe0!f{-E$j?YS|DMjI zc!mi~uQaW?Jr#ls3w6;X&rdyL+eUAr-B)+lM8-*Vi;O-g;CAWZ2@HxN9dB9eYJ+pQ zX0WZPm9|^1B2GH!G7Vj5&_|L8=(sI};p& z7vg%+i4KkEYQ+ijjjkNRshla_*-Ic-RTjWda;! zD+;ZfF0?4qO6-g*RokvoeO3*#t!kJO5bMtp39@6Is`PhH7Gb4DH`$^Lss}KkkZhiX zyj0Utve4O9P>wp_QCdEPr7@e5)C5t_$IreTkD@2~LQVCM&$^C0RK4H3Nv*{s`;#pB zgYu7g086TQF6Jih;Ag0^Xn=siDHbI0YOm3#U_wqxdd9mhMPDbLm&(dB4LFcr3E@?n zx3JGFL63$=Td0rSY1#@Uc@t!aq&&6^2%Ct87*6^}X(TzS*ts~=TSMIMqrhdfp2iOc7zenSny&egHBts%; zCDVNs#MU`Z-R}EOgbJrwo78%(kAvN_21Sg2&_fcDgH> zWrCZn#J3U2o0N7a2^ZI@jm1@U?`V^yC^y{cPpZFY9}vc zYj1Vua3X{|MOMkhKQ*)ULe{?N$j+*6fE=UCZm`QJL1pR%pUiz^jZq zh1+KLqql`BJDxCfP43zYywjK(5~E};k{QDlmh4?NeG|N0AX1#fwkTF-Zs)vjT+FaQ z_%HTElvF3bj+`_~VZrQ>XPt8zqg)SyAox_V46=`t2f~_$kA95>NY<7lxKwR$!)Vo+ z3RSZY(MCU|ikgGT{bfGJi$$DWc{Y8y?jI%Lw#A+*aF_lYgrRGaeFs?ONJ!?!xzW3& zi_fV+=EZqLs6`&gq}Z{rNy}%`>5d)(ujE^xRbUaD*Oy?OfRoAGySznu9YTu zkgp!rNy(_v3uJi&4CxYGQ2O}8v{d0i^-g@s-o+9dCG6Wqw~b1vi*Otwt)qxpr2`Ne zHfw(W81mL{a@yp#}SRjz8JyKGpC4iBH zf^oOyM*LcewNvPP7Ke)!FF`=8w6au5AL~Vt09U%!gJw=#Z4^Lh!nWZ!NqqLLDyrge z=flv#6J3=+XAQ4~QQmF69Pw-z6DgleZS<%F8rr^}szPt28Oq#EFC)UZNFk{6)xDo> zd1tH_q=>it2ZE)oqB`2di^cTJ8sQP|^6FmDy<1^|YS*k)*j~d}U~lojw%U8b-60s- z+{SrT0q^k3+mrFlQ`I;Gr}hJgk&Bsc!%fKMw1!MTF@A5{csyedQojLqt;pu{jOz4# zMr;A13>-;(dn6WGnnEqEIl*>Iu|&WJi?p8tnaJ|p%n?-zwTa<-JmbY~qlT`FJ}Fn* z6ztto)B=VKQ2}8zt%KxpHXM6A(YgvKS~#`|u(TDwbN?PjlbiNjw)1K5kv5L7Pud<( z(GoT=?49L^wr%**y}KnkcJY0p#elq6aLRYwK9(sNJ(zG@)gxgJme7n_5Re5hwSgk~Eu4_7sSM)^fTrRO& zSaj0#iLz?537WSSA+4Q`+!r7=V1|hE%>=8Ej6k#_&3hsHUJz^E@sPM=pKv&5TeV%& z_B_R+RFi5SY8~_f8cl|abVgn|6WB`P#$B1!09RwA2qNU;1Y96!{tJY3Gf(1DWjlz& zNW<;9D}-OvB4+a0Og|KHQ$tH~bm=BMP_K2FnmcQ{&hMV{sCok4c&en8U%-;+>zJ49 zTzI(X0G-4|NXhx3;M#j@Q}l2w0NOYF?&*6zL9yu`4iyHwPf^qo!s&6wxy9(En(g3N z7DQdlwL=94_5K8x4dx!9HZiHu{f|P&!ux`9zUXHQ3_cU5y%fou&O%jwNoNyHJ>@L3 zvBTMMyra?9-UtG9G90axuk|vfy(Ao1=SC=Q0L}`r=wGsd6VaO|fB%gMWo^bVKMB%-9*h4&+Om6sk}<;|^usm4vdr-9mS25CVC z*<|LhodY>y3(y)oqm=7a%zwOEdf}eVKqO+IBByR?j(8`e?gB&nv_hDv(IFRcY^pL* zu2(8g!Hc(UyY`;I+3pR|-4E|=(XuqF_AbWhRo@-ON;(LK%vKG8YIQ`XxzvU}Q}kNp zQVV&JH3X4VL<@Qk4}9!tPGTw@?jmUvAll#FHhm9B1eayrGT6(V{;@Ir_?=F?9fboO z#O{bhe=by>?Gxxm5jfr#m4)Pik}S4i&D)G*-W$cTr6%VY&d}|mGMNcl?K9+D`caX< zhxd9@4DaJ8*NQL@-R=W8O1&>Dr#seES8ZCx$6l!hGh<-+i3#R$$mmAjsCB9HZ24|X z%iJW@T14gW>0Quc$-y?c$0a=vyX>^7+$bBXepl@$p_*g4_hr&vi1Fs3I31ucS7)9X0xvv4_g-W;iGbo|oS?5!WIPC-V$;J{;9AIm0>C zt~J9Y1Zeyc=1Yq&$_!`DI2x0mP|hNi!cv8pg|C%qag_GaMF4d6T!zh;qye%K>$co+XY_1K)J}2?wJ>8M+Big7aUBOcDWS2&dxh$Y1X8kOV0Wn0 zy;6m*wSf=HZi0H_a?euFO;iEvJO``5WtRpQKQy3qRp~(&j7BJwr|6}R1TE-`X8RvE zyTd&xIog=R_WGQXEWY?sh8)Rc2;UAzYh&VueN51Dk1@!jrHOAmM3ESxeb=JFkfK_Z zAFS4=WDE=7n9PnyT^Q4of0mdk3eo$LIuD%GfppbCq>OZ2Y7A9h+n2!P;HNSSMl_}; zNt<@hR@!w*iKK&+ruC~MeW2epFrjDE$ z#Fk`q<=^l(5}m!Jjf>UqmSXY_r>b}J2~>*X?768No|_$=ZZ0Ys5{Yv&A0kQ%0XlM% zM$y5#RlZTh;6vWoNMG;cE)t%u1$se@ma;Mkb5G6p9n$cSzHzG^tf?+0i_4*D^@YWrNM z;QJ|f=X)H#kw-I^GFP;uzKa^b$t-T>l_`g|qvV{es*kDV^hvc_Y?4XD=P-J(ImF_c z!_wn__Kb~NbB}J24ovglPX_mHMkb4my0L~5(vfJ-2kIxX{t z@0*Eg==P<_ovH0+Npt_FW09T;^qlFjzW(f`hbdY%h_rkX7X^T;dJc}oKPpCL3IaUqrOAYrhw%wFc5ST%X{QwIeo1o~&`~VTuaC*&E zMuhXA+Tmy-_=@r_cq1c5>mtX-kngQ zXJ*TOnXKjpCH;UR2@mu82aCF2rr~B2KJ9+rl-@JmriQ9{ShGozsLPlZ5>|5jkwmEi}c`<>t(cl(LLgiZ}8$^U9-m zv)*E}bQJm7BaxZ!HJa4f-i?k?(RJO%QSD+vy(wgZP?U4rc%-;U+*}2kc-r1{ar)61 zVD4360=&NLe(VtO$~NM2UiHAn%=bO>)&ue3*EpQ~yCm(OiWM9%0TT9k5mcCn6Qu9W zZNq7NdeE8&%A27kz<1-w$G*EITan2o)lbIq`yQ7b4Ls}WRL!!Kv126BZBtB;i;@|ap&sU1me*)s%e&o&J zI$?1;JnpktqlsV>7b|hE42&zvH$7#rG+e=_w#`d8_Q&P1%3KFM8lLXqsGhb-zI}p&1aL+JDE2~*=(Bv>F zb%93C_@>a!?+BXdpVF1@-7~QTD^FOsw!3CtmT)$KZFEGdS9_^4Ewfb}En4W)%+yNqQdM69dG*1Rh1XdYhuga2oyLY6hlK&vhH;ILdL)qFZNE_`zOc6|AP;X{cG zJ{YLst~!6aZ9;Ehc7DB{VQUelOXmU*ZO|?1v0L{;dlNsEZSRWC8-1)2YU3b2NS?A) zvBiARa2}&Yj*Ati9(WvgQsA9suZ04)&^04K?dY@YJ+0Gt3wK1zSu~dIJ5d)#)wJvV zz_l+3KINg-7f;idiRl(iR%uHw-Id>cmX+ye%|CZj$1Tp$!~Ss`4J*UwmR5o?yh2Y55FIJsNu?U^oJGtg)d_@j=M~A^kxo^i@qG{uJ z6aEl4jtfZI!lU2~(M3kb^P9Zon(y6*2#o9&U=J=m<4SnG8?1f@-myO~F6TQde_05S zdeyN@pm=@)14?(;E~OO#8{5;$blgkTABt{*pGVplAu}5#!2TjvX7*ugnvPpx5Di*& z>NGLTk@$YCS%@KGTF535_Fz^cFyE5Tz`~Z$(pLjHO}5D|HGI}us+nfJ52<7suT%k+ z5i~77ghR6-pac25qvg^^QOg!UzLuv5+y?{{RUS9lEjC#gq3j6{J7^`zB2mC@g}mEjtLjpx zliDKzxY0li5$A>mPI!5p-YfD+)aI_9ph#)Y?yLi4h6_{ywF6)S^YiKnMI)Jk#tLty zHtIh3h~{_*c}Ps3MP8de>wK>`iLSSmiIjM+;&?awvE1NZY2%Qa#Ke-k1tXbAK)V9H zGx4$`Uy=9m>eH!~l+a01BG*s_N!JDE!KS=3kNKbvq4{aES06#_xWOi>+QFJ}5 zkugng8vm%>i`lzrPGo+18;~^L*ElK~R^>=aTinu))_@d^#CmNE)FX@>k0v$7`955F z=zs%3S$W@x6SFp5xd&BS{h6W8*v|B=k_~$ygUGw+@AN6BIx1LZs&i?jBr(OTeTjIKglfH|tpyB65y6F#lQdK+hy}XE zIjUUYbU4#gPB>)RD%@%6dcgqn^d#Y`keg0;s>awtLzNv!^2%IPsWDMzTlqz+=aMI`n8+&)wT?u9U#Buu&*F6m+a8$V3Q@Vxn*VA>GBe32>#53QqvrV%1}b|e zU%3wUmbnp9f8l}#RmbTTuZ@mc+dKxm5T&k&l@c02lW{zm?%-jcmsCB5|Mu%w{fb16 zyT4(NdG>SV z#l#f=uzK49{pxOsBxedCd(VzW3x*jE?5DrLUQPB6qK%X16tquc5j^`w^GEvC)t~CQ zGZ>*gSEt^Wz>BUAcnL+xRh)@c!X;BKj#0$!+c*Hbd7N3mP-Vh3EW){UGq!Exr8e&A z5DhaLVCm6d>~XH8=jP7n3e3$ksa1cE6uH*32kW`LVW`Pu7n1-#iaySnq~w##XQrU6 z2NqTX*87G*q5}KP5MN%nX_kISin+OHOGD-{jTv00>$XDaQZRaD(?%5nAWe+vj4=h; z$uBB60y1~%q;&O8&lk33mK%5ox>d+lJj^xm&;Xd((61DiQkkCB{;!YoQckxqv_u_e z2pTuxgGf&Yt9{tXqw=-nAe)@LzSyXocUQ(Dcl!tVy)huDS++CmJ*WvLGWFBKx({Yc zY~=0doSY~-5H+I=DDvo{CO1Ghe>rEh2VUI~UZdL?`&w#qB$ND`r7Df!iC@OYyF;5DGT))`b#X*9n=-U9?em zpp7LUr8kbU&y1i-abRafW!hz7w`jvt+jwALzqc=xb@hjEv=-bB@@%|I+fMX@d31vz zC&aLdVZ5pO-cP`Nvi*sRF>LwFNd_ZV-5hD#m=hq)a)(!Iml87ilxBIl!%9b^7>B`k zfM!_=q=;zZjMLaB4zqI3_K^aNI%*)0qgMIiBFkAExydq^`rpcaY%FlL+s68xt zZnoV@a^FSpmFUtJKw`ps^g#7#rRa3C^X~YjNsQJOwf&HsR}f3VD)K#dBEM+kah9Ov zd77J{3$N=9$HAfylc}5d>WT;6)emO$3Dz< zSr49uR}*go@n<3rh}_k49w=TQ;AXOTef&9ED`QH20K7dY`V-ZyCIAxFQ)s|yd(Ly; zd*C`DtFMxZR47hL1JE8M{uJ8qD(M_k7gAGcFzMuuR(PmK*a0Zg20uoF%Oa+34ui<_ z&lLb|d1X^-x-l@?iBwr4_Tlkmnc1$y+yr$6rvzzyIKu7N{V=~lcB%1I6zhEs#KT(0 z$%hb5G&K!8dRox};6oe*v>VRH`UjVfMB;ju?s2jogA&n}xBN$W7M4Cs_OOpQ8I17^Km|w*6dDvoa_agFDEmM7#$Tw^pMuiS8VQp1QXQHQkCa;0PL;^ z140?@dhB>Wz0I7VAwY?8Hz&ol9TpbHH)(3$tm@k>G?qyXc;R@kxnwhsFLFAM_&E(a zBQNE;^t?|^n;t29asn-ct3Qh_-}L!;-}B5P@NT~Zql1oKiGHeE?DJ)ET)66yRyGzd_fH?4&o0y$YfoXfpDgvYA7W~$p zJtDEiWqjrAk%M15dj#TX;juyznSSAkp$s#MYfqz4{}D-vNocUyrF|O_+l^X321&g*n)rPCek@E7HFJ3vHiDcsfLLbhafP7-@ zJ;`R6Bm0OV8`kPTUEqt;iVlt}>0p^ML~hLqENiqN=WO@eSvD&w@cHkmBH*<8&x+uX zjlOE3ve=NumJP+>pw2l9U17W>Z~zqD4)HZfnnWbD!w(`|8N0(!&N2y995}5WtF?}j zRl$yd6-tM{g%ZTmcEz|WjG+G)z=BNV?o3HX-?Ip6s8xSEi9E1!R(iDbE0jd@Y*&Y_CN?L!jUi?(!6g-Gn6$?ms9 znpT+o=lY-gUm^wo{73uYYKtlUC1UcH^1q&}^`CDvrN6`t0I>WIw_70rdwC^)?EjV9 z?+%e$v-Y!7C)`#o!~Z^N4-~^qx17W>9$q}b z9}d{XN%fl(N2Grza}pj} z;kI1(|6KHbE!tKu(kt}-`FK0`I`HbAlf_tI=?+uPf)3I8)&+M%tA814%faaD`-tmi<>K{>S_1>eTYv#_ab#`YXrP3-}7lf8JI9 zeTF>xZH8R6lCQA*kGG{~6;pR5-z-;UF0LNVR!%p3?QG>%%=af-?v;XbrSTB>-%YP; z8V^s%Us$47L`hpu7BbVOEnO2S61Jirh8)=e2dP1H2KRLuD^J$Kfw3<38};tdjEBR@9zx$FUd8okb9@#@ZIuJ$Y;))jAl)51-(2J zdO2jr!5h-)wn1Y2l)C$Eo6fv)dMjMO0j2CT3r>pidgtacD#G^Q>BZ01P8=bLTe@RR z673j)YL`o@j#ZsH@ABSBOX?jA)gDNC^^RV8#Xi1dIHUELr$)le#OE7rf!fa>YW6u# zG?vUWDV~Uq6dDb_Sjq_hd&eKI`>(}j%XO9T>O8O<9IISmsX}zN^7-GG1!Pxefxk@i ze_sw*4wBy+{$B>k{~H-V{42)(m(>2hiT_i{&H^jBa$WrULTr|n$-fZ)|7>8qbu?q4 z8>8EHU($cwFwo&d);d$YaIa%au?AX_oiP8_+0WN8ZT_xM{E)wkb55~iN50?dRP7!K zYd^p<mCrs3-boM?lyhqG@@{tIDP@`B zh4g~7_jy=awGOFk+G^{p|M6lTZ;;QHvX~(!=Png(R#69O@WteUgQi2#oYUj&#PM~@ z90u2v8PYi#F~EJOu;UN2Wd7*VllG2~8{M zN;|L8K=*^FZf`HOKziqWlIJe_Ny-%e(XqJQa|*4QF)YBOk0*-)?I9Q%?C*RpN@<%r zNzWrk$87vGe1?zBdjW?>{#=h&oTSE9^tg8^tvlO!dfc#q$hleFSgPkOogsFXR^H&1 zLUm<%I{l0C{&vm&Uvt1+8)Z|I_erfw7EgndKCMW!XO%?uH(aco{@ns)G7GOrEJ3BWrY zuwuK_&g5a|ZRfG%+m23_21@@n?|$*z$a~3f8^4p`kXiDD%}YNAHjpPWTb7g0@}_Uk z(sku^^S|ym@UVi)eKq;7hq?aaWhpV+4I6HbY}g>Vg0XbHxlwZoXc=bBc>jj871$0U zYwzLe;%Rxq8T|9?ZawJoy%NhM$8rktY2`*zES%X(% zzv0{OLRb&GJo{gs-7mu)UIQBr{s!1zMiA>!m#1ng!mdTVcKsVre+jnrsLMm}ugj`< z<6BUF4wv<)%aiEkG4iqoq}FJF!{J*{f1OLO2VSiNxi!F~ZvkFubbZ~uR_j834S2TW zH%R>}_|J{rKNDSX4SI;vUqN4Kv98BoExpnje6aIh!T)tmx&957$Aing+~tnv{xxr) zIQd$NS)TH&NVYa*Wx4+e*dGSKYNirv)E4gX$9TWG=6_As*PCKB;kCKi9r6vB zUw8M_g09W2#-85*`bS>ATGpd$_^W$;1MD9<_i|S(a0zdDX;~Yc2fR0|Zk4R4a_#!{ zFPEC-mh^HqT*hCUTedBw-@n?RUT?wW_U>}Ivb<=oRkp~|lCi4M^Q-i~3)HVU?{ZtU z@fX;&jxoOk`+K3k33Rol*J|R3?;2UZsZF*mIt@KqxJE$r1=UaO9S zh_6ZhP3l;!WAPPfm##NAE-r!os^eb-{wB!f4$s$p?Fed(oZr;nSg+ybZpjMzS`BOX ztrGZeVJ~+c2A9Y$`|(;0>)rgCcHJUg!=j_y1Y;Nx}I`ZLH%ymbWZPk&SiB-E`4=u+pytu I!18bZ2RXEHumAu6 diff --git a/tests/fixtures/legacy_pipeline/scale-cartesian-camframe.json b/tests/fixtures/legacy_pipeline/scale-cartesian-camframe.json deleted file mode 100644 index b10df9e19c..0000000000 --- a/tests/fixtures/legacy_pipeline/scale-cartesian-camframe.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "source_commit": "ec5c903c067bf1b29bbff781bc414c6df2bcf1f2", - "vendor": "scale", - "representation": "cartesian", - "frame": "camframe", - "identity": 3, - "indices": [ - 0, - 7, - 29, - 47, - 50 - ], - "model_keys": [ - "actions_cartesian", - "observations.images.front_img_1", - "observations.state.ee_pose" - ], - "api": "legacy", - "transform_arguments": { - "mode": "cartesian", - "stride": 1 - }, - "normalization_frames": 54, - "archive_sha256": "7aaf6dc3c82a95a2c844e76918334ad2762dd0e0ced038fe117baf928b5cfde6" -} \ No newline at end of file diff --git a/tests/fixtures/legacy_pipeline/scale-cartesian-camframe.npz b/tests/fixtures/legacy_pipeline/scale-cartesian-camframe.npz deleted file mode 100644 index 0529be4357c39392b696a4afe21014aa9a38b41b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38565 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 diff --git a/tests/fixtures/legacy_pipeline/scale-cartesian-eef_frame.json b/tests/fixtures/legacy_pipeline/scale-cartesian-eef_frame.json deleted file mode 100644 index 5f33ed50f7..0000000000 --- a/tests/fixtures/legacy_pipeline/scale-cartesian-eef_frame.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "source_commit": "ec5c903c067bf1b29bbff781bc414c6df2bcf1f2", - "vendor": "scale", - "representation": "cartesian", - "frame": "eef_frame", - "identity": 3, - "indices": [ - 0, - 7, - 29, - 47, - 50 - ], - "model_keys": [ - "actions_cartesian", - "observations.images.front_img_1", - "observations.state.ee_pose" - ], - "api": "legacy", - "transform_arguments": { - "mode": "cartesian_wristframe_ypr", - "stride": 1 - }, - "normalization_frames": 54, - "archive_sha256": "d456f93c3dec0d8eac559f46a5f56cabc0c42f813d2e4feac78fbf34d92fe65c" -} \ No newline at end of file diff --git a/tests/fixtures/legacy_pipeline/scale-cartesian-eef_frame.npz b/tests/fixtures/legacy_pipeline/scale-cartesian-eef_frame.npz deleted file mode 100644 index 0e34574bf1b4628029cc34392176a93591c43884..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37688 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 I!18bZ2RXEHumAu6 diff --git a/tests/fixtures/libero_native_scontrol_408787.txt b/tests/fixtures/libero_native_scontrol_408787.txt deleted file mode 100644 index 9b80c1cc7d..0000000000 --- a/tests/fixtures/libero_native_scontrol_408787.txt +++ /dev/null @@ -1 +0,0 @@ -JobId=408787 JobName=af-noaug-exact-gpu-v2 UserId=paphiwetsa3(41003) GroupId=loaner(41000) MCS_label=N/A Priority=10000 Nice=0 Account=loaner QOS=loaner-normal JobState=RUNNING Reason=None Dependency=(null) Requeue=0 Restarts=0 BatchFlag=1 Reboot=0 ExitCode=0:0 DerivedExitCode=0:0 RunTime=00:39:27 TimeLimit=02:00:00 TimeMin=N/A SubmitTime=2026-10-07T23:17:33 EligibleTime=2026-10-07T23:17:33 AccrueTime=2026-10-07T23:17:33 StartTime=2026-10-07T23:17:33 EndTime=2026-10-08T01:17:33 Deadline=N/A PreemptEligibleTime=2026-10-07T23:22:33 PreemptTime=None SuspendTime=None SecsPreSuspend=0 LastSchedEval=2026-10-07T23:17:33 Scheduler=Main Partition=rl2 AllocNode:Sid=loaner-gateway:61717 ReqNodeList=gpu19 ExcNodeList=(null) NodeList=gpu19 BatchHost=gpu19 NumNodes=1 NumCPUs=8 NumTasks=1 CPUs/Task=8 ReqB:S:C:T=0:0:*:* ReqTRES=cpu=8,mem=128G,node=1,billing=8 AllocTRES=cpu=8,mem=128G,node=1,billing=8 Socks/Node=* NtasksPerN:B:S:C=0:0:*:* CoreSpec=* JOB_GRES=gpu:h100:1 Nodes=gpu19 CPU_IDs=32-35,144-147 Mem=131072 GRES=gpu:h100:1(IDX:3) MinCPUsNode=8 MinMemoryNode=128G MinTmpDiskNode=0 Features=(null) DelayBoot=00:00:00 OverSubscribe=OK Contiguous=0 Licenses=(null) Network=(null) Command=/workspace/users/paphiwetsa3/tasks/cotrain-velocity-first-wave-20261007/rows/usocket-noaug/noaug_exact_gpu_v2.sbatch WorkDir=/workspace/users/paphiwetsa3/tasks/cotrain-velocity-first-wave-20261007/rows/usocket-noaug StdErr=/workspace/users/paphiwetsa3/tasks/cotrain-velocity-first-wave-20261007/rows/usocket-noaug/logs/noaug-exact-gpu-v2-408787.err StdIn=/dev/null StdOut=/workspace/users/paphiwetsa3/tasks/cotrain-velocity-first-wave-20261007/rows/usocket-noaug/logs/noaug-exact-gpu-v2-408787.out Power= MemPerTres=gpu:256000 TresPerNode=gres/gpu:h100:1 TresPerTask=cpu:8 diff --git a/tests/fixtures/libero_native_suite_corpus_authority.json b/tests/fixtures/libero_native_suite_corpus_authority.json deleted file mode 100644 index 814ee77b94..0000000000 --- a/tests/fixtures/libero_native_suite_corpus_authority.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "recorded_authority_sha256": "559b6b3884eed7e39bdbdef12af67f4b033030af355a408db7c62354de23c31f", - "schema": "libero-native-suite-corpus-authority/v1", - "suites": { - "libero10": { - "dataset_logical_sha256": "3ed2b86cf97eaa27045af81e6d6c4834c8659d15eda69a2394d835e602014e1d", - "historical_split_sha256": "c588c831ef0deab61a59a21840ea9e11dafe92a170d9023a87e0acee831df236", - "training_seeds": [ - 42 - ] - }, - "libero_goal": { - "dataset_logical_sha256": "c190385dfc8a7d05b966a4b5f94651826b8a906b1e63d8b348edc70df52c1f60", - "historical_split_sha256": "207c24115104bf0545e7f165abb91cf052eb99f6568446f11e835f9a3e4c6af8", - "training_seeds": [ - 42, - 43 - ] - }, - "libero_object": { - "dataset_logical_sha256": "40b5e3ccf4febcbae470690755adc8aa6a39471a4fcb7fa585a9bdb8e59c325d", - "historical_split_sha256": "5ade2a86fad7552457e093944fd60efec8b51fbe9dfd20831f627d1c56e9b5d7", - "training_seeds": [ - 42, - 43 - ] - }, - "libero_spatial": { - "dataset_logical_sha256": "ee28d02e8c6cd28fb0a235fab2ccc649b662e6f3e10eb096d89be082d2871860", - "historical_split_sha256": "15640eada8102c7071dcfcd153ef438cb98c61835474a2fb2c3c9415cf6d714e", - "training_seeds": [ - 42 - ] - } - } -} diff --git a/tests/fixtures/pusht_before_real_sim_cleanup.json b/tests/fixtures/pusht_before_real_sim_cleanup.json deleted file mode 100644 index 2cfe82172c..0000000000 --- a/tests/fixtures/pusht_before_real_sim_cleanup.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "contract": "SHA256 sorted compact JSON of unresolved composed config, excluding Hydra metadata; verified identical before/after directory move.", - "recipes": { - "action_flow_bc_usocket_bridge_likelihood_s42": "93a9a46c5c50dd063b4dc10ae457c6c503dc3f68f924b2bea94cc45ad6c6c4c5", - "action_flow_bc_usocket_graph_section_s42": "f413c4d5adfbc84c9c2fb6145156b72171ac923fdd4e369c22fa5c96083a481a", - "action_flow_bc_usocket_latent_fm_sg_recon1_200m_adamw_lr1e5_s42": "9f8b97c9ab2a95a4be95d8bb125c774e1137e0c6d2ad9dfe0fd47db008249b87", - "action_flow_bc_usocket_latent_fm_sg_recon1_200m_muon_lr1e5_s42": "6223637fc6f6d47e180bcaf3fe7689e4e3e100ebb3bedb400a044dbe55ebcabf", - "action_flow_bc_usocket_latent_fm_sg_recon1_s42": "9f0eea8acca4715cdcc174c4916edb54f4efcb024141bd2d78f28edc934d6f95", - "action_flow_bc_usocket_recon100_s42": "33d779a7c1105d49f14807b7e9514f2cd8f9c3b6a1f4f01718eba898675756b4", - "action_flow_bc_usocket_recon10_s42": "baf0e2745493a2c4c0f3dbe1b28375716fbb7dd1349245a3efd6e970cd6d4f1b", - "action_flow_bc_usocket_recon10_warmup10k_s42": "b74cda0fa33c73b45eb2b3f29f0503d9bd12a48dae3f2e663bd5a6c0f63dda3f", - "action_flow_bc_usocket_recon1_s42": "03fd25ca55fb0328904e41e1d00bd178d37b47555db1ff388130a54e7f432e69", - "action_flow_chain_latent_fm_sg_unite_h384_sum14_cfg4_val8_s42": "e6401b0b233a11b7f577e536800b3b686979257f5f809a4f5e72a7f49e09ea8d", - "action_flow_cotrain_uc_multiplier_interpolation": "1484a8fe2bafa7c297e58c5277254298c3331b48f1f88ed688ae265b75831270", - "action_flow_usocket_candidate_common": "156337979165efd521a5aa752532b91c527e4a6cac487cda99345132f0c6dbd6", - "action_flow_usocket_latent_fm_sg_unite_denoiser90m_sum14_cfg4_finalval1_s42": "9e35db31e65a8ffb4691c005ae8c955dda788add1f0d8aac1415f85b4d865708", - "action_latent_vfm_usocket_val01_h16": "a7d96acf9a9afcc8b21a63dbbea43443075287315a145a4c7252d2f89c1f5182", - "planar_uc_manual4919_dp_261m_af_obs_multiplier": "10799eebb8f927c1f265f4c50bacddec890525b270cdf25d2a35d0f98137a186", - "planar_usocket_chain_manual4919_standard_dp_retimed_masked": "f9f7072e36d1a3f05b4ee669a6aa6644fb6fd69ba6263d8332975bfd675eb5de", - "planar_v2_base": "2b81ed6636b484dba91f8f6c9924d65e25406bc564d392539a8e23f28ca066af", - "planar_v2_chain_arc_bc": "4781bc8155faafecde4ce4f67d4bfa935db29ec6eee0aa04cb78c1fb2e8c3560", - "planar_v2_chain_base": "58fe6f3c85eb69c6adf0e80b5567da445117ae9f033eba882da7721862fd0b78", - "planar_v2_chain_direct_bc": "1c2ca35042bfecb658ea58eec0c1e58e2e9e9a778e302ce9dc0449b5461c870e", - "planar_v2_chain_dp_paper": "4618c22f2295831805762865c2b08fc79461152824c1166ae6fd724a002efb21", - "planar_v2_chain_dp_standard": "e52dd80834a0371d11d5614be73ba7b9329ec970376f638c21f14fbf34b1aaab", - "planar_v2_cotrain_clean_dp_standard": "fafe963772ca0fb243b0d552d7c8f7f9e499d99f97f26c589214eb0f7180dfe5", - "planar_v2_usocket_arc_bc": "9c0c7d575365f007220806fb9c226eb07a264641162f8aab7ee6d9860d351a97", - "planar_v2_usocket_arc_graph_tok": "e52d3e962a3d4246e6637b2358477fdb35c7e64247aafa221b38bb1c04abfcab", - "planar_v2_usocket_arc_hybrid_D40_M16_R24deg_bc": "88127379eaa9098b72dccda0bceb82fe145ddd633d809226186f8c9853e3ac34", - "planar_v2_usocket_arc_paper_curvature_D40_M16_R24deg": "924e9d5aab3a586eb6c9b7170f80c7e49796240e23ab519ef5133140b22c0bb8", - "planar_v2_usocket_arc_paper_uniform_D40_M16_R24deg": "609852fa193f2bc54655074c9ab90055117f3d698dad02d29de374e670ed1ae4", - "planar_v2_usocket_base": "4eebb72701c219b503b96ddf8765630cc0a7160ce848620a26c77ab83c2d4abb", - "planar_v2_usocket_direct_bc": "c4a46e4074481e2d82f03eb15078f884f3f10d22ffd0f3ed35bc2fb20a11f1b6", - "planar_v2_usocket_dp_paper": "84cad67cd83cdb4324588dcbd2010e94245b5a29bf1cac7ebad1d1544d5bb38f", - "planar_v2_usocket_dp_standard": "7e958a8973f08fd0b6afde306d3fc539a77f381215d5ded773a739368246d98f", - "unite_cotrain_usocket_chain_val01_h16": "060912f7b91c8c9a2b1bfe4c432ffdcc03e1bb554498806fe8db4bc36b99f8fa", - "unite_usocket_register_sweep_val01_h16": "2232b805b702d255cf97739d3cd26e5b5c45ea8679b2286fefe4aa4df740c34a" - }, - "source_commit": "b6686ec9ad4fc8bbc89c97263fb63ec1891e1ae2" -} diff --git a/tests/fixtures/pusht_config_before_parent_cleanup.json b/tests/fixtures/pusht_config_before_parent_cleanup.json deleted file mode 100644 index ff2c1db81e..0000000000 --- a/tests/fixtures/pusht_config_before_parent_cleanup.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "hash_contract": "SHA256 sorted compact JSON of unresolved composed config excluding hydra metadata; integer keys normalize through JSON", - "recipes": { - "action_flow_bc_usocket_bridge_likelihood_s42": "ff73c1ecfe60c29af17c6ae6806fb797984f22064af6656b7c40f745915caf56", - "action_flow_bc_usocket_graph_section_s42": "4c7284b1f6cf021e6baccc8b3846b1155d73587c2162fe5606e798abe6851d74", - "action_flow_bc_usocket_latent_fm_sg_recon1_200m_adamw_lr1e5_s42": "4f586952aa5862aadd5fc5fc55387e1e33aeca0255b4cbd7082634fc6e2fdfd5", - "action_flow_bc_usocket_latent_fm_sg_recon1_200m_muon_lr1e5_s42": "3e2c27eb94568c5adb00c02b05e2a13ee700a598c53ed4224deecfdf1c370a09", - "action_flow_bc_usocket_latent_fm_sg_recon1_s42": "bb976a267641d6db9d6d1bfcff5341dbec9e140463987e6517e53f73d040a493", - "action_flow_bc_usocket_recon100_s42": "12617b5c6a477a08b1e35f5c0e079fe6dcca9c17b032439afc0f800dde4a420b", - "action_flow_bc_usocket_recon10_s42": "d31d062bb28d81066265b958eb8da7a97a48fbffaf3797a29e7157f4cd2e72ab", - "action_flow_bc_usocket_recon10_warmup10k_s42": "99e22eb4a39538e0fc05a4fab9fd123d2cf8fcb2ecc1dd57b18586baa01f2e59", - "action_flow_bc_usocket_recon1_s42": "dd4f13fc095b59ed10fa7f29cd3b2e3e25cbd4e8ef4048c3c564438cd2f248fb", - "action_flow_chain_latent_fm_sg_unite_h384_sum14_cfg4_val8_s42": "cc754f3f742641463584355992a5ab9a436da9bde736f961802a0ffd2c6176b2", - "action_flow_cotrain_uc_multiplier_interpolation": "9755ee842621fc3e5aa483d373ff24348159045453d5ac2b258e82c2f0707af7", - "action_flow_usocket_candidate_common": "5e4faf536d9e6d33d6c2e82b0a80c37bf4beb08954e4cdaf637afd7df7318f9d", - "action_flow_usocket_latent_fm_sg_unite_denoiser90m_sum14_cfg4_finalval1_s42": "62150360dbb2e8daa02af64cf1405835e03e012c3ab30c45fe759a3fe66f0a7c", - "action_latent_vfm_usocket_val01_h16": "c47bd51273b01fa5b516ec781ebe119e5d6e9774697990b6d3172705aca0b839", - "planar_uc_manual4919_dp_261m_af_obs_multiplier": "e25f5f9f69b205cb10206e41a896922d4eeb5700a04597d6b086a4d3b44914a2", - "planar_usocket_chain_manual4919_standard_dp_retimed_masked": "699dc867a101146e0fc6c6162b3c30514037d802dcf39f195bb9d4c29b0263ba", - "planar_v2_base": "c9e7771e7f023a9742fff0e662693be6877372c736b3c1e8da75f32a3e2ee060", - "planar_v2_chain_arc_bc": "da9b94f023d8bd098fcee001c8011a3af099c19982de203c62b6388a160fd068", - "planar_v2_chain_base": "3ce62f0b980eabc2ce0b3228f9a11b2fbb73ca8ee1fd12eaac777b9ec3bc18da", - "planar_v2_chain_direct_bc": "4d49cdd26c3066cd04f4a4632008369e7229dcf4562862a1356616fe966c21f1", - "planar_v2_chain_dp_paper": "811fb66e1e7c25138c693251de0c698d437f579f26393e87a4fb89f86c942fd1", - "planar_v2_chain_dp_standard": "bdf14917b32f5d1440417ffaca4321ff5531c284d60e46db29081236c8c8ba49", - "planar_v2_cotrain_clean_dp_standard": "868969c07806c29e50846bbf8de0022437d54e4882ef6b0c3228f90b94737a2d", - "planar_v2_usocket_arc_bc": "6085104c0e08534e8540282a5d33c5d01b247c2596f5f4281b10418e44a306a5", - "planar_v2_usocket_arc_graph_tok": "8f019000f4a449dcbc890519682fb7156add1be19396a1d85857c9a81d4bfdda", - "planar_v2_usocket_arc_hybrid_D40_M16_R24deg_bc": "752d6dda74daaebf2932ae57d544a7ed3c788fb83f1501993a8be51147f665db", - "planar_v2_usocket_arc_paper_curvature_D40_M16_R24deg": "d464be794e7c2e700362aa3c9dbd448bdf53127057d37187d2874c99a77e7d53", - "planar_v2_usocket_arc_paper_uniform_D40_M16_R24deg": "1874d622ce05170c9cc9456a3742a849bea7b86b24886d230d3455ae07af93ea", - "planar_v2_usocket_base": "72885fa5bff3ef2b48c9a0a1a2fdee2fc75b7f87af3ba6fe834b4c191deb7f44", - "planar_v2_usocket_direct_bc": "64b297d8bd5103de726f69c3c666f75fa6ce014b715448db092cb4edfd887923", - "planar_v2_usocket_dp_paper": "79f87ad8c8fa6cc0835c82ef297222b4310b00d705bd895d350b41174523cc42", - "planar_v2_usocket_dp_standard": "c7f54de8e746a66027e92bf9263e839fa1ce2d5db2a3825485f8b161c6a3d990", - "unite_cotrain_usocket_chain_val01_h16": "56b9e9e6fdc1c8ad38d81148d7f7ba7713b2c817e54ba1dd14d4a15707a52d8b", - "unite_usocket_register_sweep_val01_h16": "9823d8dd3107639ebe9a4755bba27b5f38757baa2c8b10e6e9395ef05082c84f" - }, - "retired_data": [ - "action_flow_chain_points6_frozen4920_val01_h16", - "action_flow_chain_points6_manual3000_val01_h16", - "action_flow_chain_points6_manual4919_val01_h16", - "action_flow_cotrain_usocket_chain_manual3000_val01_h16", - "action_flow_cotrain_usocket_chain_manual4919_val01_h16", - "action_flow_cotrain_usocket_chain_points6_frozen4920_val01_h16" - ], - "retired_recipes": { - "action_flow_bc_usocket_latent_fm_sg_recon1_codec98k_s42": "7e1bba2ed089e54b2d74c2ddb34cb3472b77c8a2e79c9cdf9f560402edc8792d", - "action_flow_bc_usocket_recon10_warmup10k_flow001_s42": "4f5bf734d0d8d2d72282fef6da53ffac86bbb76bb76a233793ca8923a2dca21e", - "action_flow_chain_manual3000_avln_80k_s42": "07c2daf65519e44e0ae96d84343bcc68b16b384eb600035d68d8bfc8fb4eae34", - "action_flow_chain_manual4919_avln_80k_s42": "3332315e2d95057076f5df303bfa5b8252c9f3fc5d185200ba12fb07cc128c42", - "action_flow_chain_points6_latent_fm_sg_unite_h512d14h16_sum14_cfg4_val10k_s42": "e31546f683f40d60ffc719fe80dda8dee375826d4295a23ce488c648d57d97dd", - "action_flow_cotrain_uc_latent_fm_sg_unite_h384d12h12_sum14_cfg4_val10k_s42": "79cbdbdd4c0221940f0fbd3574aab33ce5efc9f22b937fdca31126b1cf1a68f9", - "action_flow_cotrain_uc_latent_fm_sg_unite_h512d14h16_sum14_cfg4_val10k_s42": "88040dd95a75a085058fd48adbc081ea6f00ec8485d6496b25b83ddc181f43b8", - "action_flow_cotrain_uc_manual3000_avln_80k_s42": "ea67ff06292e4d080af6cd514ce642dc08830865f0ed25837a9979f534247dce", - "action_flow_cotrain_uc_manual4919_avln_80k_s42": "978b5d06e0db777a8e3367cee1bd8b744ed2812f0749c35187f393ce4e018916", - "action_flow_usocket_latent_fm_sg_unite_h384_s42": "3bbbb4f247acb88e2e74e96e20ce1617a18c2ee076d55e714f0d93a938a756d2", - "action_flow_usocket_latent_fm_sg_unite_h384_sum14_cfg4_val8_deterministic_s42": "5f23aed86062a0827250927bba4b7433c450135688d58eeabf1da8b3acb819df", - "action_flow_usocket_latent_fm_sg_unite_h384_sum14_cfg4_val8_s42": "51d39070058c361b37a0235eb72d7dea0f27163d30b16ea254a44c6c861a293e", - "action_flow_usocket_latent_fm_sg_unite_h384_sum14_cfg4_val8_scale1_s42": "cd4eef7c8927a649e6c5fde911836275b3f1c76c64951265ced3df566d0d4796", - "action_flow_usocket_latent_fm_sg_unite_h512d14h16_sum14_cfg4_val10k_s42": "afd8576ab5448604096f961b8a284d4a21f6fd8b05a83cbf59b355d5befe777a", - "action_flow_usocket_refactored_c12_compat_s42": "a62734de32fedf721e3d1eda16e67e3deba5c9d03ec6451a164720ac2b7dfa22", - "planar_chain_manual4919_standard_dp_retimed": "eb2ae443c790984c01a5159ca1f880169982a30e6bfe1c97b761449a44a4ab7e", - "action_flow_cotrain_uc_speed_interpolation": "643f68448e85aff1d57273a2d2211689a3f2bb580f75c3d4c987bce7e981c045" - }, - "retired_data": [ - "action_flow_chain_points6_frozen4920_val01_h16", - "action_flow_chain_points6_manual3000_val01_h16", - "action_flow_chain_points6_manual4919_val01_h16", - "action_flow_cotrain_usocket_chain_manual3000_val01_h16", - "action_flow_cotrain_usocket_chain_manual4919_val01_h16", - "action_flow_cotrain_usocket_chain_points6_frozen4920_val01_h16" - ], - "parent_identity_metadata": { - "parents/action_flow_usocket_h384_sum14": { - "name": "action_flow_usocket_latent_fm_sg_unite_h384_sum14_cfg4_val8_s42", - "description": "Action Flow H384 with released-UNITE flow aggregation, CFG, and validation coverage" - }, - "parents/action_flow_usocket_h384": { - "name": "action_flow_usocket_latent_fm_sg_unite_h384_s42", - "description": "Action Flow stopgrad/JVP objective on separate released-UNITE H384 modules" - }, - "parents/action_flow_cotrain_uc_manual4919": { - "name": "action_flow_cotrain_uc_manual4919_avln_80k_s42", - "description": "Manual U-Socket 2999 + ChainGripper 3000 standard/1919 obstacle recollections Action Flow AV-LN" - } - } -} diff --git a/tests/fixtures/stattempo_contracts.json b/tests/fixtures/stattempo_contracts.json deleted file mode 100644 index 952c0035f8..0000000000 --- a/tests/fixtures/stattempo_contracts.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "source": "8e80f090c2463cdcc8007ad6be6856a178870187", - "main": "7043c0251a350578c6da5a316a79c5ed7d21033d", - "configs": { - "bc_rl2_stattempo_slowpace366_arcdur_m200_d033": "a1eb967a2452aaa6d14ab601013ce42195a4eeb92f38211902c99a0b48f81095", - "bc_rl2_stattempo_slowpace366_arcvel_m200_d033": "6b406ba86dfbdc90bed17aa4547773f3478abedfaba3aafb0cd523143c14854a", - "bc_rl2_stattempo_slowpace366_arcdur_abstime_m200_d033": "b64f61df2c800bb109ac20ed2d969bb116aacb21d77c406d6e0020b4c26a1539", - "bc_rl2_stattempo_slowpace366_arcvel_nosmooth_m200_d033": "50169b927f70cda897bc81f1750bf7b5bde73c3786d07eaecd940ba71fe196d2", - "bc_rl2_stattempo_slowpace366_arc_projection_dur_m200_d033": "27db2736ea99844b2e237ab707f69e30f4bef775f06734d3506e612ff078ec4f", - "bc_rl2_stattempo_slowpace366_arc_projection_vel_m200_d033": "207ea662c66645a669595f8d4a1276f0fa434b92bbaebb22832766291c2df3d8" - }, - "codecs": { - "motion/mean": "294796c37e92e78aa5bc3f5adc01e19bfd2fec768d4d4cb1e2a35f39019ebb67", - "motion/profile": "55278af8ebf0c6af7c8d1dafe547ce440f5e4d98092abe57a0d440dafa2d4a84", - "motion/logdur": "b551c891443c3adb741ee09c019edfec11112ed33ef4eb6bdf95a14cf0efd7f7", - "motion/dur": "a24ab8b8a9ff57728eec12729a4a3623f276b9bb5d283d1c5eda89935a9d04bc", - "motion/durhyb": "4c4d9b6eaa12ef474d5147d5fcc230341fe74285f67255687bd889c9ca95ac52", - "motion/profhyb": "3eb3004d4bf2e926a85b9d1a44e39e5fd7e2e46059851a6db521fc17a53a6e51", - "motion/durtri": "9f68f2c3ebc373e790a9166ead1f32f51cccd8e3c1d661ef5dab3d7a16214b68", - "motion/proftri": "1a53518e0443895936bd6522387a985af24ffc19c442984712cfd8af2b3dc625", - "motion/durevt": "44d9487249797e387d09ae9f11402336c63301c7fa541fc58bd4495cbd4ba671", - "motion/stkevt": "b411a8edf5a2f7bfea162ae4aa4eeaa882097787954b7dc6459a9009f1380832", - "motion/durref": "bf6273d564daa76e7b9a846a0724c1f4dbe3b279f575382584ca2ebd8735ac83", - "motion/stkref": "02e34d462586e6d2969c00ccf0ad5eeba15d9275bc9be3f8ca6341584cfc4fc1", - "hold/mean": "68f29b544b65f626c3259c7a70d99526939f19cdf8eddf031dfc0b035e7a572f", - "hold/profile": "9c0db6929876854086ddaba0fe67763ea1304ecd5c5d8075d707b7c4ae2816d7", - "hold/logdur": "0ab689937df851f021052424f4aa09eedd04a7f06d06b159235307062e4c1c37", - "hold/dur": "9c0db6929876854086ddaba0fe67763ea1304ecd5c5d8075d707b7c4ae2816d7", - "hold/durhyb": "519fc38d68309b6e2cb5c08ca8ea8c4037687c6536a387e5d49dc3047749fff9", - "hold/profhyb": "4dfdebd0ced4b15e10874b09564eeb3aa64931e27a4ed27fc9d725e20e681053", - "hold/durtri": "2c315840ab1fba1572920815c3b3f63ae3b530ff64f9c135b03c2c04761d4779", - "hold/proftri": "a038fc0e34de75f7da5646b5c967afa05b0236337d9bff70a7733635b0d2832f", - "hold/durevt": "a3e4f5317fb649b144281428e889e378ad1f7ff202d2401cafcec7498974c403", - "hold/stkevt": "8ee78c048b8e808fe76641c67ab6a30274f0ad3acfc8fa2b9b8f3aa3051cd75e", - "hold/durref": "e1074cc4893eb4bcfd9495a3d73ed438fbaf3f048e1d44139a0f8a25123c348f", - "hold/stkref": "7cdbb56c003684d51679863bc161f3016ff8960d45b84880eb38763263ef77c9", - "delayed/mean": "23599335b5b07fd33c7fa1ba5b93a9884cc8a79d5753f3592a7c1792ec8550a1", - "delayed/profile": "8d98fc4d4a81d572983fb6e432b8d78845cffdb516d0fa737122d23d6c07266c", - "delayed/logdur": "0a3fe9da66a43441e70978587adecd9fc137d86b228334235514ceaebba546e2", - "delayed/dur": "2b0a368851cfbebaacaecf0c9fc392da57e2b15d0867afd48042df8d2253f04d", - "delayed/durhyb": "9956527df703396df6352897c681502ff686153bb9f0ad06ce61731fc69de78e", - "delayed/profhyb": "a66e43eef4f630e2ef5c120c820059c4e6f25fb56da30b6a33af00e9e79fcd50", - "delayed/durtri": "2db07d70918af05ad829cf4281337092f1025f271020604bad5c3c47a455b205", - "delayed/proftri": "521e27873e1bddb7d0f6fc520cb68272c433c5331c95626382d978d41b768799", - "delayed/durevt": "2a84c28f9607b5f6d3c20fd6351a610e337a9c0de4c14ecb254e2a6323148581", - "delayed/stkevt": "fca3113de8cf6aec42eac460490a44b3e8f4aa25ccbb102e9a8c83113d526549", - "delayed/durref": "f97333f55bf13ac2cb1da76b5c0085a381a37e69331efc6d15e52ff531a32526", - "delayed/stkref": "cb6a901566a5552a0aa62862f591e170ea7a282793617f346b07b8f3d976ed50" - }, - "models": { - "bc_rl2_stattempo_slowpace366_arcdur_m200_d033": { - "parameters": 180424144, - "state_schema_sha256": "3bd8f28767616d8acb1357e0ca544a42418c5727411d272842f2754caad38694" - }, - "bc_rl2_stattempo_slowpace366_arcvel_m200_d033": { - "parameters": 180424144, - "state_schema_sha256": "3bd8f28767616d8acb1357e0ca544a42418c5727411d272842f2754caad38694" - }, - "bc_rl2_stattempo_slowpace366_arcdur_abstime_m200_d033": { - "parameters": 180424144, - "state_schema_sha256": "3bd8f28767616d8acb1357e0ca544a42418c5727411d272842f2754caad38694" - }, - "bc_rl2_stattempo_slowpace366_arcvel_nosmooth_m200_d033": { - "parameters": 180424144, - "state_schema_sha256": "3bd8f28767616d8acb1357e0ca544a42418c5727411d272842f2754caad38694" - }, - "bc_rl2_stattempo_slowpace366_arc_projection_dur_m200_d033": { - "parameters": 180424144, - "state_schema_sha256": "3bd8f28767616d8acb1357e0ca544a42418c5727411d272842f2754caad38694" - }, - "bc_rl2_stattempo_slowpace366_arc_projection_vel_m200_d033": { - "parameters": 180424144, - "state_schema_sha256": "3bd8f28767616d8acb1357e0ca544a42418c5727411d272842f2754caad38694" - } - } -} diff --git a/tests/libero_ddp_resume_worker.py b/tests/libero_ddp_resume_worker.py deleted file mode 100644 index 278453ecab..0000000000 --- a/tests/libero_ddp_resume_worker.py +++ /dev/null @@ -1,64 +0,0 @@ -"""Subprocess fixture for a real one-process to two-process Lightning resume.""" - -import argparse -import json -from pathlib import Path - -import torch -from omegaconf import OmegaConf, open_dict - -from egomimic.trainHydra import train -from tests.test_libero_benchmark import make_replay -from tests.test_oat_training import config_for - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("--root", type=Path, required=True) - parser.add_argument("--resume", action="store_true") - args = parser.parse_args() - torch.set_num_threads(1) - args.root.mkdir(parents=True, exist_ok=True) - replay = args.root / "replay.zarr" - if not args.resume: - make_replay(replay) - cfg = config_for("libero_oattok", replay, args.root / "train") - spec = cfg.model.pipeline.stages[0].tokenizer - spec.emb_dim, spec.head_dim = 32, 8 - spec.encoder_depth, spec.decoder_depth, spec.num_registers = 1, 1, 4 - cfg.trainer.limit_train_batches = 1.0 - cfg.trainer.accumulate_grad_batches = 2 if args.resume else 4 - cfg.trainer.devices = 2 if args.resume else 1 - cfg.trainer.max_epochs = 2 if args.resume else 1 - cfg.callbacks.batch_budget = OmegaConf.create( - { - "_target_": "egomimic.pl_utils.oat_training.OATBatchBudgetCallback", - "global_batch_size": 8, - } - ) - with open_dict(cfg.trainer): - cfg.trainer.enable_progress_bar = False - if args.resume: - cfg.trainer.strategy = "ddp_find_unused_parameters_true" - if args.resume: - cfg.ckpt_path = str(args.root / "train/checkpoints/last.ckpt") - _, objects = train(cfg) - if objects["trainer"].is_global_zero: - payload = torch.load( - args.root / "train/checkpoints/last.ckpt", - map_location="cpu", - weights_only=False, - ) - report = { - "global_step": payload["global_step"], - "ema_num_updates": payload["ema_num_updates"], - "training_budget": payload["training_budget"], - "optimizer_states": bool(payload["optimizer_states"]), - } - (args.root / ("resumed.json" if args.resume else "initial.json")).write_text( - json.dumps(report) - ) - - -if __name__ == "__main__": - main() diff --git a/tests/test_action_flow_affine_configs.py b/tests/test_action_flow_affine_configs.py deleted file mode 100644 index 5c192ab8b2..0000000000 --- a/tests/test_action_flow_affine_configs.py +++ /dev/null @@ -1,138 +0,0 @@ -import hashlib -import json -import subprocess -import sys -from pathlib import Path - - -def test_builder_emits_frozen_eleven_variant_three_seed_matrix(tmp_path): - source = Path(__file__).parents[1] - output = tmp_path / "config" - experiment = tmp_path / "experiment" - training_dataset = tmp_path / "train.npz" - evaluation_dataset = tmp_path / "eval.npz" - training_dataset.write_bytes(b"frozen-training-data") - evaluation_dataset.write_bytes(b"frozen-evaluation-data") - subprocess.run( - [ - sys.executable, - str(source / "scripts/experiments/build_action_flow_affine_configs.py"), - "--output-dir", - str(output), - "--experiment-root", - str(experiment), - "--training-dataset", - str(training_dataset), - "--evaluation-dataset", - str(evaluation_dataset), - ], - check=True, - ) - configs = sorted(output.glob("gaussian-torus-*.json")) - assert len(configs) == 33 - resolved = [json.loads(path.read_text()) for path in configs] - assert {config["seed"] for config in resolved} == {42, 43, 44} - assert {config["checkpoint_every"] for config in resolved} == {50_000} - assert {config["evaluation_particles"] for config in resolved} == {2048} - assert {config["evaluation_dataset"] for config in resolved} == { - str(evaluation_dataset) - } - action_configs = [ - config for config in resolved if config["architecture"] == "action_adapter_flow" - ] - assert len(action_configs) == 30 - assert {config["model"]["latent_dim"] for config in action_configs} == {8} - manifest = json.loads((output / "manifest.json").read_text()) - assert manifest["primary_metric"] == "validation_generation_symmetric_nn_mse" - assert manifest["pass_threshold"] is None - assert len(manifest["variants"]) == 11 - assert manifest["datasets"]["training"] == { - "path": str(training_dataset.resolve()), - "sha256": hashlib.sha256(training_dataset.read_bytes()).hexdigest(), - } - assert manifest["datasets"]["evaluation"] == { - "path": str(evaluation_dataset.resolve()), - "sha256": hashlib.sha256(evaluation_dataset.read_bytes()).hexdigest(), - } - - for index, config in enumerate(resolved): - run = Path(config["output_dir"]) - run.mkdir(parents=True) - (run / "summary.json").write_text( - json.dumps( - { - "validation_generation_symmetric_nn_mse": float(index + 1), - "validation_generation_energy_distance": float(index + 2), - } - ) - ) - summary_json = tmp_path / "summary.json" - summary_markdown = tmp_path / "summary.md" - subprocess.run( - [ - sys.executable, - str(source / "scripts/experiments/summarize_action_flow_affine.py"), - "--manifest", - str(output / "manifest.json"), - "--output-json", - str(summary_json), - "--output-markdown", - str(summary_markdown), - ], - check=True, - ) - aggregate = json.loads(summary_json.read_text()) - assert len(aggregate["rows"]) == 11 - assert "mean" in aggregate["rows"][0][manifest["primary_metric"]] - assert "Symmetric NN MSE" in summary_markdown.read_text() - - e_configs = [config for config in resolved if config["variant"].startswith("e-")] - g_configs = [config for config in resolved if config["variant"].startswith("g-")] - assert {config["lambda_reconstruction"] for config in e_configs} == { - 1.0, - 10.0, - 100.0, - } - assert {config["lambda_reconstruction"] for config in g_configs} == { - 1.0, - 10.0, - 100.0, - } - assert {config["lambda_action_velocity"] for config in e_configs} == {0.0} - assert {config["lambda_action_velocity"] for config in g_configs} == {1.0} - - -def test_builder_run_suffix_produces_distinct_retry_identities(tmp_path): - source = Path(__file__).parents[1] - training_dataset = tmp_path / "train.npz" - evaluation_dataset = tmp_path / "eval.npz" - training_dataset.write_bytes(b"train") - evaluation_dataset.write_bytes(b"eval") - output = tmp_path / "config" - subprocess.run( - [ - sys.executable, - str(source / "scripts/experiments/build_action_flow_affine_configs.py"), - "--output-dir", - str(output), - "--experiment-root", - str(tmp_path / "experiment"), - "--training-dataset", - str(training_dataset), - "--evaluation-dataset", - str(evaluation_dataset), - "--seeds", - "42", - "--run-suffix", - "smoke-retry1", - ], - check=True, - ) - config = json.loads( - ( - output / "gaussian-torus-f-nonlinear-path-seed42-smoke-retry1.json" - ).read_text() - ) - assert config["wandb"]["id"].endswith("-smoke-retry1") - assert config["wandb"]["dir"] == str(tmp_path / "experiment" / "wandb") - assert config["output_dir"].endswith("-smoke-retry1") diff --git a/tests/test_action_flow_candidate_codec.py b/tests/test_action_flow_candidate_codec.py deleted file mode 100644 index ef6a7a8e7d..0000000000 --- a/tests/test_action_flow_candidate_codec.py +++ /dev/null @@ -1,151 +0,0 @@ -"""Exact-section and JVP behavior of the restricted graph diagnostic.""" - -import pytest -import torch -import torch.nn as nn -from torch.func import jvp - -from egomimic.models.action_flow_codec import GraphSectionSequenceCodec -from egomimic.pipeline.core import Pipeline -from egomimic.pipeline.stages_action_flow import ( - ActionFlowObjectiveStage, - ConditionalVelocityStage, - ContentDecoderStage, - ContentEncoderStage, - LatentBridgeStage, -) - - -class _EncoderView(nn.Module): - def __init__(self, codec): - super().__init__() - self.graph = codec.graph - - def forward(self, content): - return torch.cat((content, self.graph(content)), dim=-1) - - -class _Field(nn.Module): - def __init__(self): - super().__init__() - self.linear = nn.Linear(8, 8) - self.condition = nn.Linear(3, 8) - - def forward(self, value, time, condition, *, condition_drop_mask): - condition = condition.masked_fill(condition_drop_mask[:, None], 0) - return self.linear(value).tanh() + self.condition(condition)[:, None] - - -def _codec(): - return GraphSectionSequenceCodec(action_dim=4, latent_dim=8, horizon=4) - - -def test_graph_section_identity_survives_joint_optimizer_updates_and_shared_ownership(): - torch.manual_seed(46) - codec = _codec().double() - encoder = ContentEncoderStage(_EncoderView(codec)) - decoder = ContentDecoderStage(codec) - field = ConditionalVelocityStage(_Field().double()) - objective = ActionFlowObjectiveStage( - reconstruction_weight=0.0, residual_key="action_flow/fm_velocity_residual" - ) - pipeline = Pipeline( - [encoder, LatentBridgeStage(samples_per_content=2), field, decoder, objective] - ) - parameters = list(pipeline.parameters()) - assert len({id(parameter) for parameter in parameters}) == len(parameters) - assert encoder.encoder.graph is decoder.decoder.graph - optimizer = torch.optim.AdamW(parameters, lr=1e-3) - assert len(optimizer.param_groups[0]["params"]) == len(parameters) - snapshots = { - name: [parameter.detach().clone() for parameter in module.parameters()] - for name, module in (("graph", codec.graph), ("residual", codec.residual)) - } - target = torch.randn(2, 4, 4, dtype=torch.float64) - for _ in range(2): - latent = codec.encode(target) - torch.testing.assert_close(latent[..., :4], target, rtol=0, atol=0) - torch.testing.assert_close(codec(latent), target, rtol=0, atol=1e-12) - batch = pipeline( - { - "target": target, - "sampler/noise": torch.randn(2, 4, 8, dtype=torch.float64), - "condition": torch.randn(2, 3, dtype=torch.float64), - } - ) - torch.testing.assert_close( - batch["loss/action_flow"], - batch["log/action_flow_fm"] + batch["log/action_flow_action_velocity"], - rtol=0, - atol=0, - ) - optimizer.zero_grad() - batch["loss/action_flow"].backward() - for module in (codec.graph, codec.residual, field.field): - gradients = [p.grad for p in module.parameters() if p.grad is not None] - assert gradients and all(torch.isfinite(g).all() for g in gradients) - assert sum(g.abs().sum() for g in gradients) > 0 - optimizer.step() - torch.testing.assert_close(codec(codec.encode(target)), target, rtol=0, atol=1e-12) - for name, module in (("graph", codec.graph), ("residual", codec.residual)): - assert any( - not torch.equal(old, new) - for old, new in zip(snapshots[name], module.parameters()) - ) - - -def test_graph_section_jvp_matches_finite_difference_with_trainable_graph(): - torch.manual_seed(7) - codec = _codec().double() - latent = torch.randn(2, 4, 8, dtype=torch.float64, requires_grad=True) - tangent = torch.randn_like(latent, requires_grad=True) - _, predicted = jvp(codec, (latent,), (tangent,)) - delta = 1e-6 - expected = (codec(latent + delta * tangent) - codec(latent - delta * tangent)) / ( - 2 * delta - ) - torch.testing.assert_close(predicted, expected, rtol=2e-5, atol=2e-6) - gradients = torch.autograd.grad( - predicted.square().mean(), (latent, tangent, *codec.graph.parameters()) - ) - assert all(torch.isfinite(gradient).all() for gradient in gradients) - assert sum(gradient.abs().sum() for gradient in gradients[2:]) > 0 - - -def test_graph_section_context_free_shape_and_small_private_networks(): - codec = GraphSectionSequenceCodec(action_dim=4, latent_dim=8, horizon=16) - for module in (codec.graph, codec.residual): - assert module.depth == 2 - assert sum(parameter.numel() for parameter in module.parameters()) < 12_000 - with pytest.raises(ValueError, match="expected sequence shape"): - codec(torch.randn(2, 15, 8)) - with pytest.raises(ValueError, match="expected sequence shape"): - codec.encode(torch.randn(2, 16, 3)) - - -def test_graph_section_mixed_precision_identity_and_jvp_backward_are_finite(): - torch.manual_seed(19) - codec = _codec() - content = torch.randn(2, 4, 4) - latent = torch.randn(2, 4, 8, requires_grad=True) - tangent = torch.randn_like(latent, requires_grad=True) - with torch.autocast("cpu", dtype=torch.bfloat16): - reconstructed = codec(codec.encode(content)) - _, decoded_tangent = jvp(codec, (latent,), (tangent,)) - loss = decoded_tangent.square().mean() - torch.testing.assert_close(reconstructed, content, rtol=0, atol=0) - assert torch.isfinite(decoded_tangent).all() - loss.backward() - for module in (codec.graph, codec.residual): - gradients = [p.grad for p in module.parameters() if p.grad is not None] - assert gradients and all(torch.isfinite(g).all() for g in gradients) - assert sum(g.abs().sum() for g in gradients) > 0 - - -@pytest.mark.parametrize( - "kwargs", [{"action_dim": 8}, {"latent_dim": 3}, {"dropout": 0.1}] -) -def test_graph_section_rejects_invalid_or_stochastic_section(kwargs): - options = {"action_dim": 4, "latent_dim": 8, "horizon": 4, **kwargs} - with pytest.raises(ValueError): - GraphSectionSequenceCodec(**options) diff --git a/tests/test_action_flow_candidate_configs.py b/tests/test_action_flow_candidate_configs.py deleted file mode 100644 index 72d6231066..0000000000 --- a/tests/test_action_flow_candidate_configs.py +++ /dev/null @@ -1,181 +0,0 @@ -"""Exercise the maintained Hydra rows and their candidate-specific telemetry.""" - -from pathlib import Path - -import hydra -import pytest -import torch -from hydra import compose, initialize_config_dir -from omegaconf import OmegaConf - -from egomimic.eval.action_flow_diagnostics import ActionFlowDiagnostics -from egomimic.pl_utils.pl_model_action_flow import ActionFlowModelWrapper - -CONFIG_DIR = Path(__file__).parents[1] / "egomimic/hydra_configs" -ROWS = [ - ("latent_fm_sg_recon1", "latent_fm_stopgrad", 1.0), - ("graph_section", "graph_section_diagnostic", 0.0), -] - - -def _compose(row): - with initialize_config_dir(version_base=None, config_dir=str(CONFIG_DIR.resolve())): - return compose( - config_name="train_zarr_cartesian", - overrides=[ - f"+experiment=sim/pusht/action_flow_bc_usocket_{row}_s42", - "++paths.root_dir=.", - ], - ) - - -@pytest.mark.parametrize("row,method,reconstruction_weight", ROWS) -def test_candidate_row_instantiates_exact_dimensions_and_shared_codec( - row, method, reconstruction_weight -): - cfg = _compose(row) - assert cfg.model.action_flow_method == cfg.train.action_flow_method == method - assert cfg.run_provenance.objective.method == method - assert cfg.model.action_horizon == 16 - assert cfg.model.latent_dim == 8 - assert cfg.model.action_dim == 4 - assert cfg.model.reconstruction_weight == reconstruction_weight - assert cfg.run_provenance.objective.decoded_noise_scale_weight == 0 - assert cfg.model.flow_samples_per_content == 14 - assert cfg.model.condition_dropout_probability == 0.3 - assert cfg.model.optimizer.lr == 3e-5 - - algo = hydra.utils.instantiate(cfg.model.pipeline, device="cpu") - stages = algo.pipeline.stages - encoder, field, decoder, objective = stages[3], stages[5], stages[6], stages[7] - assert field.field.horizon == 16 - assert field.field.input_dim == field.field.output_dim == 8 - assert field.field.condition_dim == 67 - assert field.field.depth == 12 - assert 39_000_000 <= sum(p.numel() for p in field.parameters()) <= 41_000_000 - assert objective.reconstruction_weight == reconstruction_weight - assert stages[1].num_tokens == 16 and stages[1].latent_dim == 8 - parameters = list(algo.nets.parameters()) - assert len(parameters) == len({id(parameter) for parameter in parameters}) - if method == "latent_fm_stopgrad": - assert field.flow_clean_gradient_mode == "all_stopgrad" - assert objective.residual_key == field.flow_residual_key - assert decoder.residual_key == field.residual_key - assert field.flow_residual_key != field.residual_key - assert encoder.encoder.depth == decoder.decoder.depth == 2 - else: - assert field.flow_clean_gradient_mode == "full" - assert encoder.encoder.graph is decoder.decoder.graph - encoder_parameters = {id(parameter) for parameter in encoder.parameters()} - residual_parameters = { - id(parameter) for parameter in decoder.decoder.residual.parameters() - } - assert encoder_parameters.isdisjoint(residual_parameters) - assert decoder.decoder.latent_dim - decoder.decoder.action_dim == 4 - assert not cfg.run_provenance.objective.reconstruction_is_optimizer_objective - assert cfg.run_provenance.requirements.r6 == "FAIL_restricted_diagnostic" - assert not cfg.evaluator.action_flow_diagnostics.capture_activations - target = torch.randn(2, 16, 4) - torch.testing.assert_close( - decoder.decoder(encoder.encoder(target)), target, rtol=0, atol=1e-6 - ) - - -@pytest.mark.parametrize("row,method,reconstruction_weight", ROWS) -def test_candidate_wrapper_gradient_routes_and_compute_contract( - monkeypatch, row, method, reconstruction_weight -): - cfg = _compose(row) - # Preserve the real Hydra codec/factory/stage wiring and all sequence - # dimensions; replace only the expensive shared field width/depth. Inputs - # are already encoded observations, so image/data nodes are not exercised. - pipeline_cfg = OmegaConf.create( - OmegaConf.to_container(cfg.model.pipeline, resolve=True) - ) - pipeline_cfg.stages = list(pipeline_cfg.stages)[3:] - field_config = pipeline_cfg.stages[2].field - field_config.hidden_dim = 32 - field_config.depth = 2 - field_config.num_heads = 4 - field_config.feedforward_dim = 64 - field_config.time_embedding_dim = 32 - algo = hydra.utils.instantiate(pipeline_cfg, device="cpu") - wrapper = ActionFlowModelWrapper(pipeline=algo, gradient_telemetry_cadence=1) - logged = {} - monkeypatch.setattr( - wrapper, "log", lambda key, value, **kwargs: logged.update({key: value}) - ) - torch.manual_seed(51) - loss = wrapper.training_step( - { - "fixture": { - "target": torch.randn(2, 16, 4), - "condition": torch.randn(2, 67), - "sampler/noise": torch.randn(2, 16, 8), - } - }, - 0, - ) - assert torch.isfinite(loss) - loss.backward() - assert wrapper._gradient_route_manifest is not None - routes = wrapper._gradient_route_manifest["routes"] - intersections = wrapper._gradient_route_manifest["intersections"] - assert routes["FM"] and routes["ActionVelocity"] - assert intersections["FM__ActionVelocity"] - assert all( - torch.isfinite(p.grad).all() for p in wrapper.parameters() if p.grad is not None - ) - calls = 2 if method == "latent_fm_stopgrad" else 1 - assert logged["Train/ActionFlow/Compute/FieldForwardCallsPerStep"] == calls - assert ( - logged["Train/ActionFlow/Compute/FieldSampleEquivalentsPerStep"] == calls * 14 - ) - if method == "latent_fm_stopgrad": - assert intersections["FM__Reconstruction"] == [] - assert logged["Train/ActionFlow/GradientCosineDefined/FM__Reconstruction"] == 0 - assert ( - logged[ - "Train/ActionFlow/GradientIntersectionParameterCount/FM__Reconstruction" - ] - == 0 - ) - assert routes["Reconstruction"] - else: - assert "Reconstruction" not in routes - assert "Train/ActionFlow/GradientNorm/Reconstruction" not in logged - torch.testing.assert_close( - loss, - logged["Train/ActionFlow/FlowMatchingLoss"] - + logged["Train/ActionFlow/ActionVelocityLoss"], - ) - - -def test_graph_factory_rejects_independent_encoder_configuration(): - cfg = _compose("graph_section") - pipeline_cfg = OmegaConf.create( - OmegaConf.to_container(cfg.model.pipeline, resolve=True) - ) - pipeline_cfg.stages = [pipeline_cfg.stages[3], pipeline_cfg.stages[6]] - pipeline_cfg.stages[0].encoder = {"_target_": "torch.nn.Identity"} - with pytest.raises(hydra.errors.InstantiationException, match="second codec"): - hydra.utils.instantiate(pipeline_cfg, device="cpu") - - -@pytest.mark.parametrize("row,method,reconstruction_weight", ROWS) -def test_candidate_diagnostic_config_constructs_without_stale_activation_settings( - tmp_path, row, method, reconstruction_weight -): - cfg = _compose(row) - diagnostics = cfg.evaluator.action_flow_diagnostics - diagnostics.noise_seed_bank_path = str( - CONFIG_DIR / "evaluator/energy_score_seed_bank_k32_v1.json" - ) - diagnostics.artifact_root = str(tmp_path / "diagnostics") - config = OmegaConf.to_container(diagnostics, resolve=True) - instance = ActionFlowDiagnostics(config) - assert instance.jacobian_samples == 2 - assert instance.capture_activations == (method == "latent_fm_stopgrad") - if method == "graph_section_diagnostic": - assert instance.activation_layer_map == () - assert instance.cknna_k == 0 diff --git a/tests/test_action_flow_candidate_gates.py b/tests/test_action_flow_candidate_gates.py deleted file mode 100644 index 915d02a014..0000000000 --- a/tests/test_action_flow_candidate_gates.py +++ /dev/null @@ -1,295 +0,0 @@ -"""Method-specific gates: real configs plus tiny, honest telemetry receipts.""" - -import hashlib -import re -import subprocess - -import pytest -import torch -from hydra.core.hydra_config import HydraConfig -from omegaconf import OmegaConf, open_dict - -from tests.test_verify_action_flow_training_smoke import HEAD, MODULE, _history_row -from tools.validate_action_flow_config import ( - CHECKPOINT_METHOD_CONTRACTS, - DEFAULT_CONFIG_ROOT, - GRAPH_METHOD, - LIKELIHOOD_METHOD, - STOPGRAD_METHOD, - STOPGRAD_UNITE_METHOD, - PreflightError, - compose_experiment, - validate_experiment, - validate_method_contract, -) - -# Historical method identities are not selectable recipes. Exercise every current -# recipe with the full gates, and require explicit rejection for removed ones. -CURRENT_RECIPE_METHODS = { - recipe: method - for recipe, method in CHECKPOINT_METHOD_CONTRACTS.items() - if (DEFAULT_CONFIG_ROOT / "experiment" / f"{recipe}.yaml").is_file() -} -RETIRED_METHOD_RECIPES = set(CHECKPOINT_METHOD_CONTRACTS) - set(CURRENT_RECIPE_METHODS) - - -@pytest.mark.parametrize("experiment", sorted(RETIRED_METHOD_RECIPES)) -def test_retired_method_identity_cannot_select_a_training_recipe(experiment): - with pytest.raises(PreflightError, match="absent or retired"): - compose_experiment(experiment) - - -@pytest.fixture(autouse=True) -def offline_campaign_roots(monkeypatch, tmp_path): - # Config composition only: no dataset reads or launch receipts are fabricated. - monkeypatch.setenv("PUSHSHAPES_USOCKET_ROOT", str(tmp_path / "usocket")) - monkeypatch.setenv("PUSHSHAPES_CHAIN_GRIPPER_ROOT", str(tmp_path / "chain")) - - -@pytest.mark.parametrize("experiment", CURRENT_RECIPE_METHODS) -def test_real_candidate_config_and_parameter_manifest(experiment): - if ( - "chain_points6_latent_fm_sg_unite_h512" in experiment - or "cotrain_uc_latent_fm_sg_unite_h512" in experiment - ): - # Campaign-owned manifests are deliberately absent from the shipped YAML; - # this unit test must verify the gate, not invent production receipts. - with pytest.raises(PreflightError, match="manifest.*missing"): - validate_experiment(experiment) - return - report, _ = validate_experiment(experiment) - assert report["status"] == "PASS" - assert report["action_flow_method"] == CURRENT_RECIPE_METHODS[experiment] - assert report["topology"]["shared_field_instance"] - topology = report["topology"] - if "private_codec_routes" in topology: - assert set(topology["private_codec_routes"]) == set( - report["dimensions"]["actions"] - ) - assert "shared_decoder_instance" not in topology - else: - assert topology["shared_decoder_instance"] - expected_stages = ( - 9 if CURRENT_RECIPE_METHODS[experiment] == STOPGRAD_UNITE_METHOD else 8 - ) - assert len(topology["train_order"]) == expected_stages - assert ( - sum( - v["total"] for k, v in report["parameters"].items() if k != "pipeline_total" - ) - == report["parameters"]["pipeline_total"]["total"] - ) - - -@pytest.mark.parametrize("experiment", CURRENT_RECIPE_METHODS) -def test_candidate_two_optimizer_update_config_gate(experiment, tmp_path, monkeypatch): - if experiment not in MODULE.APPROVED_EXPERIMENTS: - # A shipped candidate config is not automatically an approved smoke recipe. - with pytest.raises( - MODULE.SmokeVerificationError, match="unapproved experiment" - ): - MODULE._validate_config( - config_path=tmp_path / "unused.yaml", - experiment=experiment, - run_dir=tmp_path, - expected_head=HEAD, - expected_config_sha256=None, - expected_split_sha256=None, - expected_normalization_sha256=None, - ) - return - cfg = compose_experiment(experiment) - normalization = tmp_path / "norm_stats.json" - normalization.write_text("{}") - norm_hash = hashlib.sha256(normalization.read_bytes()).hexdigest() - with open_dict(cfg): - cfg.trainer.max_steps = 2 - cfg.trainer.val_check_interval = ( - 2 if CURRENT_RECIPE_METHODS[experiment] == STOPGRAD_UNITE_METHOD else 1 - ) - cfg.trainer.limit_val_batches = 1 - cfg.trainer.precision = "bf16" - cfg.callbacks.model_checkpoint.every_n_train_steps = 1 - cfg.model.gradient_telemetry_cadence = 2 - cfg.norm_stats.precomputed_norm_path = str(normalization) - cfg.run_provenance.source_commit = HEAD - cfg.run_provenance.normalization_sha256 = norm_hash - cfg.evaluator.artifact_root = str(tmp_path / "energy") - if CURRENT_RECIPE_METHODS[experiment] != LIKELIHOOD_METHOD: - cfg.evaluator.action_flow_diagnostics.artifact_root = str( - tmp_path / "diagnostics" - ) - cfg.hydra.runtime.cwd = str(MODULE.REPOSITORY_ROOT) - cfg.hydra.runtime.output_dir = str(tmp_path) - HydraConfig.instance().set_config(cfg) - selected = OmegaConf.masked_copy(cfg, [key for key in cfg if key != "hydra"]) - path = tmp_path / "config.yaml" - OmegaConf.save( - OmegaConf.create(OmegaConf.to_container(selected, resolve=True)), path - ) - monkeypatch.setattr(MODULE, "_git_head", lambda: HEAD) - MODULE._validate_config( - config_path=path, - experiment=experiment, - run_dir=tmp_path, - expected_head=HEAD, - expected_config_sha256=None, - expected_split_sha256=None, - expected_normalization_sha256=norm_hash, - ) - - -@pytest.mark.parametrize( - "experiment,path,value", - [ - ( - "sim/pusht/action_flow_bc_usocket_latent_fm_sg_recon1_s42", - "model.pipeline.stages.5.flow_clean_gradient_mode", - "attached", - ), - ( - "sim/pusht/action_flow_bc_usocket_graph_section_s42", - "model.reconstruction_weight", - 1.0, - ), - ( - "sim/pusht/action_flow_bc_usocket_bridge_likelihood_s42", - "run_provenance.inference.sampler", - "reverse_euler", - ), - ( - "sim/pusht/action_flow_bc_usocket_bridge_likelihood_s42", - "model.pipeline.stages.7.tau", - 0.01, - ), - ], -) -def test_method_contract_rejects_scientific_identity_drift(experiment, path, value): - cfg = compose_experiment(experiment) - OmegaConf.update(cfg, path, value) - with pytest.raises(PreflightError): - validate_method_contract(cfg, experiment) - - -def test_stopgrad_telemetry_requires_empty_fm_reconstruction_intersection(): - row = _history_row() - for key in list(row): - if key.endswith("/FM__Reconstruction"): - row[key] = 0.0 - row["Train/ActionFlow/Compute/FieldForwardCallsPerStep"] = 2.0 - row["Train/ActionFlow/Compute/FieldSampleEquivalentsPerStep"] = 28.0 - MODULE._validate_history({1: row}, method=STOPGRAD_METHOD) - row["Train/ActionFlow/GradientIntersectionParameterCount/FM__Reconstruction"] = 1.0 - with pytest.raises(MODULE.SmokeVerificationError, match="reachability"): - MODULE._validate_history({1: row}, method=STOPGRAD_METHOD) - - -def test_graph_telemetry_does_not_invent_reconstruction_gradients(): - row = { - key: value - for key, value in _history_row().items() - if not ("/Gradient" in key and "Reconstruction" in key) - and "/Alignment/CK" not in key - } - row["Train/ActionFlow/TotalLoss_step"] = 2.5 - row[f"Train/ActionFlow/TotalLoss/{MODULE.SOURCE_LABEL}_step"] = 2.5 - row["Valid/ActionFlow/TotalLoss"] = 3.0 - MODULE._validate_history({1: row}, method=GRAPH_METHOD, reconstruction_weight=0.0) - - -def test_likelihood_history_requires_real_nll_and_common_validation_only(): - row = { - key: value - for key, value in _history_row().items() - if "/ActionFlow/" not in key or "/Compute/" in key - } - for component in ("InteriorBridgeNLL", "BoundaryNLL", "TotalLoss"): - value = 2.5 if component == "TotalLoss" else 1.25 - row[f"Train/ActionFlow/{component}_step"] = value - row[f"Train/ActionFlow/{component}/{MODULE.SOURCE_LABEL}_step"] = value - row[f"Valid/ActionFlow/{component}"] = value - for label in ("InteriorBridgeNLL", "BoundaryNLL"): - row[f"Train/ActionFlow/GradientNorm/{label}"] = 0.5 - row[f"Train/ActionFlow/GradientParameterCount/{label}"] = 100.0 - pair = "InteriorBridgeNLL__BoundaryNLL" - row[f"Train/ActionFlow/GradientCosine/{pair}"] = 0.25 - row[f"Train/ActionFlow/GradientCosineDefined/{pair}"] = 1.0 - row[f"Train/ActionFlow/GradientIntersectionParameterCount/{pair}"] = 50.0 - row["Train/ActionFlow/Compute/FieldSampleEquivalentsPerStep"] = 15.0 - row["Train/ActionFlow/Compute/DecoderJVPCallsPerStep"] = 0.0 - MODULE._validate_history({1: row}, method=LIKELIHOOD_METHOD) - del row["Valid/EnergyScore@32"] - with pytest.raises(MODULE.SmokeVerificationError): - MODULE._validate_history({1: row}, method=LIKELIHOOD_METHOD) - - -@pytest.mark.parametrize("method", [STOPGRAD_METHOD, GRAPH_METHOD, LIKELIHOOD_METHOD]) -def test_checkpoint_gradient_receipt_uses_exact_method_routes(method): - named = [ - (f"nets.pipeline.stages.{stage}.weight", torch.nn.Parameter(torch.ones(2))) - for stage in (0, 3, 5, 6) - ] - groups = { - "FM": [0, 2] if method == STOPGRAD_METHOD else [0, 1, 2], - "Reconstruction": [1, 3], - "ActionVelocity": [0, 1, 2, 3], - } - if method == GRAPH_METHOD: - del groups["Reconstruction"] - elif method == LIKELIHOOD_METHOD: - groups = {"InteriorBridgeNLL": [0, 1, 2], "BoundaryNLL": [0, 1, 2, 3]} - routes = { - label: [ - {"name": named[i][0], "shape": [2], "numel": 2, "dtype": "torch.float32"} - for i in indices - ] - for label, indices in groups.items() - } - labels = list(groups) - core = { - "schema_version": 1, - "routes": routes, - "route_sha256": { - k: MODULE._canonical_json_sha256(v) for k, v in routes.items() - }, - "intersections": { - f"{left}__{right}": [ - named[i][0] for i in groups[left] if i in groups[right] - ] - for index, left in enumerate(labels) - for right in labels[index + 1 :] - }, - } - manifest = {**core, "manifest_sha256": MODULE._canonical_json_sha256(core)} - MODULE._validate_gradient_route_manifest(manifest, named, method=method) - - -@pytest.mark.parametrize( - "requested,selected,accepted", - [ - ("gpu-h100,gpu-h200", "gpu-h100", True), - ("gpu-h100,gpu-h200", "gpu-h200", True), - ("ice-gpu", "ice-gpu", True), - ("gpu-h100,gpu-h200", "gpu", False), - ("gpu-h100", "gpu-h200", False), - ("gpu-h100,", "gpu-h100", False), - ], -) -def test_portable_launcher_accepts_only_explicit_partition_members( - requested, selected, accepted -): - source = ( - MODULE.REPOSITORY_ROOT / "scripts/train/launch_action_flow_usocket.sbatch" - ).read_text() - function = re.search(r"partition_allowed\(\) \{.*?\n\}", source, re.S).group() - result = subprocess.run( - [ - "bash", - "-c", - function + '\npartition_allowed "$1" "$2"', - "test", - requested, - selected, - ] - ) - assert (result.returncode == 0) is accepted diff --git a/tests/test_action_flow_candidate_stages.py b/tests/test_action_flow_candidate_stages.py deleted file mode 100644 index 7bf20cf6d9..0000000000 --- a/tests/test_action_flow_candidate_stages.py +++ /dev/null @@ -1,246 +0,0 @@ -"""Gradient routes for the separately approved latent-FM-only SG candidate.""" - -import copy - -import pytest -import torch -import torch.nn as nn - -from egomimic.pipeline.stages_action_flow import ( - ActionFlowObjectiveStage, - ConditionalVelocityStage, - ContentDecoderStage, - ContentEncoderStage, - LatentBridgeStage, -) - - -class _Field(nn.Module): - def __init__(self): - super().__init__() - self.weight = nn.Parameter(torch.tensor(0.4, dtype=torch.float64)) - self.condition = nn.Linear(3, 8).double() - self.calls = [] - - def forward(self, state, time, condition, *, condition_drop_mask): - self.calls.append((state, time, condition, condition_drop_mask)) - masked = condition.masked_fill(condition_drop_mask[:, None], 0) - return ( - self.weight * state.tanh() - + self.condition(masked)[:, None] - + time[:, None, None] - ) - - -def _forward( - mode, encoder, decoder, field, target, noise, condition, execution="separate" -): - stages = [ - ContentEncoderStage(encoder), - LatentBridgeStage(samples_per_content=3, condition_dropout_probability=0.3), - ConditionalVelocityStage( - field, flow_clean_gradient_mode=mode, fm_field_execution=execution - ), - ContentDecoderStage(decoder), - ActionFlowObjectiveStage(residual_key="action_flow/fm_velocity_residual"), - ] - batch = {"target": target, "sampler/noise": noise, "condition": condition} - torch.manual_seed(72) - for stage in stages: - batch = stage(batch) - return batch - - -def _models_and_inputs(): - torch.manual_seed(41) - return ( - nn.Sequential(nn.Linear(4, 8), nn.Tanh()).double(), - nn.Sequential(nn.Linear(8, 6), nn.SiLU(), nn.Linear(6, 4)).double(), - _Field(), - torch.randn(3, 4, 4, dtype=torch.float64), - torch.randn(3, 4, 8, dtype=torch.float64), - torch.randn(3, 3, dtype=torch.float64, requires_grad=True), - ) - - -def test_latent_only_stopgrad_blocks_both_clean_fm_routes_not_shared_condition(): - encoder, decoder, field, target, noise, condition = _models_and_inputs() - batch = _forward("all_stopgrad", encoder, decoder, field, target, noise, condition) - fm = batch["log/action_flow_fm"] - gradients = torch.autograd.grad( - fm, tuple(encoder.parameters()), allow_unused=True, retain_graph=True - ) - assert all( - gradient is None or torch.count_nonzero(gradient) == 0 for gradient in gradients - ) - field_gradients = torch.autograd.grad( - fm, tuple(field.parameters()), retain_graph=True - ) - assert all(torch.isfinite(gradient).all() for gradient in field_gradients) - assert sum(gradient.abs().sum() for gradient in field_gradients) > 0 - condition_gradient = torch.autograd.grad(fm, condition)[0] - assert torch.isfinite(condition_gradient).all() - assert condition_gradient.abs().sum() > 0 - - attached, detached = field.calls - assert attached[0].requires_grad and not detached[0].requires_grad - torch.testing.assert_close(attached[0], detached[0], rtol=0, atol=0) - # No new bridge noise, time, observation encoding, or dropout mask draw. - for index in (1, 2, 3): - assert attached[index] is detached[index] - torch.testing.assert_close( - batch["action_flow/fm_velocity_residual"], - batch["action_flow/velocity_residual"], - rtol=0, - atol=0, - ) - - -def test_shared_forward_matches_two_forward_reference_gradients(): - encoder, decoder, field, target, noise, condition = _models_and_inputs() - reference_encoder, reference_decoder, reference_field = ( - copy.deepcopy(encoder), - copy.deepcopy(decoder), - copy.deepcopy(field), - ) - reference_condition = condition.detach().clone().requires_grad_() - - output = _forward( - "all_stopgrad", - encoder, - decoder, - field, - target, - noise, - condition, - execution="shared", - ) - torch.manual_seed(72) - clean = reference_encoder(target) - bridge = LatentBridgeStage( - samples_per_content=3, - condition_dropout_probability=0.3, - )( - { - "action_flow/clean_latent": clean, - "sampler/noise": noise, - "condition": reference_condition, - } - ) - state = bridge["action_flow/state"] - time = bridge["action_flow/time"] - repeated_condition = bridge["action_flow/condition"] - drop_mask = bridge["action_flow/condition_drop_mask"] - target_velocity = bridge["action_flow/target_velocity"] - action_prediction = reference_field( - state, - time, - repeated_condition, - condition_drop_mask=drop_mask, - ) - flow_prediction = reference_field( - state.detach(), - time, - repeated_condition, - condition_drop_mask=drop_mask, - ) - reference_batch = ContentDecoderStage(reference_decoder)( - { - **bridge, - "target": target, - "action_flow/velocity_residual": action_prediction - target_velocity, - } - ) - reference_batch["action_flow/fm_velocity_residual"] = ( - flow_prediction - target_velocity.detach() - ) - reference_output = ActionFlowObjectiveStage( - residual_key="action_flow/fm_velocity_residual" - )(reference_batch) - - parameters = ( - *encoder.parameters(), - *decoder.parameters(), - *field.parameters(), - condition, - ) - reference_parameters = ( - *reference_encoder.parameters(), - *reference_decoder.parameters(), - *reference_field.parameters(), - reference_condition, - ) - gradients = torch.autograd.grad(output["loss/action_flow"], parameters) - reference_gradients = torch.autograd.grad( - reference_output["loss/action_flow"], reference_parameters - ) - for actual, expected in zip(gradients, reference_gradients): - torch.testing.assert_close(actual, expected, rtol=1.0e-10, atol=1.0e-12) - assert len(field.calls) == 1 - assert len(reference_field.calls) == 2 - - -def test_latent_only_sg_preserves_all_action_flow_gradients_and_diagnostics(): - originals = _models_and_inputs() - results = [] - for mode in ("full", "all_stopgrad"): - encoder, decoder, field = [copy.deepcopy(module) for module in originals[:3]] - target, noise = originals[3:5] - condition = originals[5].detach().clone().requires_grad_() - batch = _forward(mode, encoder, decoder, field, target, noise, condition) - params = ( - *encoder.parameters(), - *decoder.parameters(), - *field.parameters(), - condition, - ) - gradients = torch.autograd.grad( - batch["log/action_flow_action_velocity"], - params, - allow_unused=True, - retain_graph=True, - ) - # The encoder must still receive generative supervision through the - # bridge state, target tangent, and decoder Jacobian evaluation point. - encoder_gradients = gradients[: len(tuple(encoder.parameters()))] - assert all(gradient is not None for gradient in encoder_gradients) - assert sum(gradient.abs().sum() for gradient in encoder_gradients) > 0 - if mode == "full": - fm_gradients = torch.autograd.grad( - batch["log/action_flow_fm"], tuple(encoder.parameters()) - ) - assert sum(gradient.abs().sum() for gradient in fm_gradients) > 0 - assert len(field.calls) == 1 - assert ( - batch["action_flow/fm_velocity_residual"] - is batch["action_flow/velocity_residual"] - ) - results.append((batch, gradients)) - for key in ( - "action_flow/state", - "action_flow/target_velocity", - "action_flow/predicted_velocity", - "action_flow/velocity_residual", - "action_flow/decoded_velocity_residual", - ): - torch.testing.assert_close( - results[0][0][key], results[1][0][key], rtol=0, atol=0 - ) - for full, isolated in zip(results[0][1], results[1][1]): - if full is None: - assert isolated is None - else: - torch.testing.assert_close(full, isolated, rtol=0, atol=0) - - -@pytest.mark.parametrize("mode", ["target_stopgrad", "all", None]) -def test_candidate_rejects_unsupported_gradient_modes(mode): - with pytest.raises(ValueError, match="flow_clean_gradient_mode"): - ConditionalVelocityStage(_Field(), flow_clean_gradient_mode=mode) - - -def test_fm_key_cannot_clobber_action_flow_residual(): - with pytest.raises(ValueError, match="must be distinct"): - ConditionalVelocityStage( - _Field(), flow_residual_key="action_flow/velocity_residual" - ) diff --git a/tests/test_action_flow_diagnostics.py b/tests/test_action_flow_diagnostics.py deleted file mode 100644 index 7f1d8c55e1..0000000000 --- a/tests/test_action_flow_diagnostics.py +++ /dev/null @@ -1,929 +0,0 @@ -from __future__ import annotations - -import hashlib -import json -from pathlib import Path -from types import SimpleNamespace - -import pytest -import torch -import torch.nn as nn -from hydra import compose, initialize_config_dir -from omegaconf import OmegaConf - -from egomimic.eval.action_flow_diagnostic_forward import ( - _decoder_singular_values, - _fixed_decoder_singular_values, - collect_action_flow_diagnostics, -) -from egomimic.eval.action_flow_diagnostics import ActionFlowDiagnostics -from egomimic.eval.pipeline_diagnostics import ActionFlowDiagnosticProvider -from egomimic.eval.planar_action_eval import ( - USOCKET_NATIVE_ERROR_CONFIG, - PlanarActionEval, -) -from egomimic.models.unite_action_decoder import UniteActionDecoder -from egomimic.pipeline.algo import PipelineAlgo -from egomimic.pipeline.pushshapes import USocketRotVecNativeDecoder -from egomimic.pipeline.stages_action_flow import ( - ConditionalVelocityStage, - ContentDecoderStage, - ContentEncoderStage, - RoutedContentDecoderStage, - RoutedContentEncoderStage, -) -from egomimic.pl_utils.pl_model import ModelWrapper -from egomimic.pl_utils.training_behavior_action_flow import ActionFlowTrainingBehavior - -_CONFIG_DIR = Path(__file__).parents[1] / "egomimic/hydra_configs" - - -def test_chunked_decoder_jacobian_singular_values_match_full_jacrev(): - torch.manual_seed(29) - decoder = nn.Sequential(nn.Linear(3, 12), nn.Tanh(), nn.Linear(12, 10)) - values = torch.randn(2, 5, 3) - actual = _decoder_singular_values(decoder, values, sample_count=2) - expected = [] - for value in values: - jacobian = torch.func.jacrev( - lambda item: decoder(item.unsqueeze(0)).squeeze(0) - )(value) - matrix = jacobian.float().reshape(jacobian.numel() // value.numel(), -1) - expected.append(torch.linalg.svdvals(matrix)) - torch.testing.assert_close(actual, torch.stack(expected)) - - -def test_forward_math_decoder_jacobian_matches_reverse_for_transformer(): - torch.manual_seed(31) - decoder = UniteActionDecoder( - latent_dim=4, - action_dim=2, - num_latent_tokens=2, - action_horizon=3, - hidden_dim=24, - depth=2, - num_heads=4, - dropout=0.0, - gradient_checkpointing=False, - ).eval() - values = torch.randn(2, 2, 4) - forward = _decoder_singular_values( - decoder, values, sample_count=2, jacobian_method="forward_math_chunk4" - ) - reverse = _decoder_singular_values( - decoder, values, sample_count=2, jacobian_method="reverse_chunked" - ) - torch.testing.assert_close(forward, reverse, rtol=1e-4, atol=1e-5) - - -def _endpoint_decoder(): - return UniteActionDecoder( - latent_dim=4, - action_dim=2, - num_latent_tokens=2, - action_horizon=3, - hidden_dim=24, - depth=2, - num_heads=4, - dropout=0.0, - gradient_checkpointing=False, - ).eval() - - -def test_exact_endpoint_reuse_preserves_spectra_state_and_rng(monkeypatch): - import egomimic.eval.action_flow_diagnostic_forward as owner - - torch.manual_seed(51) - decoder = _endpoint_decoder() - clean, noise = torch.randn(2, 2, 4), torch.randn(2, 2, 4) - levels = (0.0, 0.25, 0.5, 0.75, 1.0) - fixed = torch.stack([(1 - t) * clean + t * noise for t in levels]) - method = "forward_math_chunk4" - clean_s = _decoder_singular_values(decoder, clean, 2, jacobian_method=method) - noise_s = _decoder_singular_values(decoder, noise, 2, jacobian_method=method) - expected = torch.stack( - [ - _decoder_singular_values(decoder, value, 2, jacobian_method=method) - for value in fixed - ] - ) - weights = {k: v.clone() for k, v in decoder.state_dict().items()} - flags = [p.requires_grad for p in decoder.parameters()] - rng = torch.get_rng_state().clone() - calls = [] - - def counted(*args, **kwargs): - calls.append(True) - return _decoder_singular_values(*args, **kwargs) - - monkeypatch.setattr(owner, "_decoder_singular_values", counted) - actual = _fixed_decoder_singular_values( - decoder, - fixed, - clean, - noise, - clean_s, - noise_s, - 2, - jacobian_method=method, - ) - assert len(calls) == 3 - torch.testing.assert_close(actual, expected, rtol=0, atol=0) - assert torch.equal(rng, torch.get_rng_state()) - assert flags == [p.requires_grad for p in decoder.parameters()] - assert all(torch.equal(v, weights[k]) for k, v in decoder.state_dict().items()) - assert all(p.grad is None for p in decoder.parameters()) - - -@pytest.mark.parametrize( - "unsupported", ["training", "child_training", "hook", "subclass", "other"] -) -def test_endpoint_reuse_keeps_legacy_calls_for_unproven_decoders( - monkeypatch, unsupported -): - import egomimic.eval.action_flow_diagnostic_forward as owner - - decoder = _endpoint_decoder() - if unsupported == "training": - decoder.train() - elif unsupported == "child_training": - next(iter(decoder.children())).train() - elif unsupported == "hook": - decoder.register_forward_hook(lambda module, inputs, output: output) - elif unsupported == "subclass": - - class CustomDecoder(UniteActionDecoder): - pass - - decoder.__class__ = CustomDecoder - else: - decoder = nn.Identity().eval() - clean, noise = torch.ones(2, 2, 4), torch.zeros(2, 2, 4) - fixed = torch.stack([clean, 0.5 * clean, noise]) - calls = [] - - def counted(_decoder, _value, count, **kwargs): - calls.append(True) - return torch.ones(count, 2) - - monkeypatch.setattr(owner, "_decoder_singular_values", counted) - _fixed_decoder_singular_values( - decoder, - fixed, - clean, - noise, - torch.ones(2, 2), - torch.ones(2, 2), - 2, - jacobian_method="forward_math_chunk4", - ) - assert len(calls) == 3 - - -def test_endpoint_reuse_requires_exact_selected_input_bytes(monkeypatch): - import egomimic.eval.action_flow_diagnostic_forward as owner - - decoder = _endpoint_decoder() - clean, noise = torch.ones(2, 2, 4), torch.zeros(2, 2, 4) - changed = clean.clone() - changed[0, 0, 0] = torch.nextafter(changed[0, 0, 0], torch.tensor(2.0)) - negative_zero = noise.clone() - negative_zero[0, 0, 0] = -0.0 - unsampled_change = clean.clone() - unsampled_change[1] += 1 - calls = [] - - def counted(_decoder, _value, count, **kwargs): - calls.append(True) - return torch.ones(count, 2) - - monkeypatch.setattr(owner, "_decoder_singular_values", counted) - _fixed_decoder_singular_values( - decoder, - torch.stack([changed, negative_zero, unsampled_change]), - clean, - noise, - torch.ones(1, 2), - torch.ones(1, 2), - 1, - jacobian_method="forward_math_chunk4", - ) - assert len(calls) == 2 # Only the unchanged sampled row can reuse its spectrum. - - -@pytest.fixture(autouse=True) -def _isolate_scheduler_execution_identity(monkeypatch): - """Keep artifact-path tests independent of the scheduler running pytest.""" - monkeypatch.delenv("SLURM_JOB_ID", raising=False) - monkeypatch.delenv("SLURM_RESTART_COUNT", raising=False) - - -class _IdentityNormalizer: - @staticmethod - def unnormalize(values, _selector): - return values - - -def _seed_bank(tmp_path, seeds=(101, 202, 303, 404)): - path = tmp_path / "diagnostic-seeds.json" - payload = json.dumps({"seeds": list(seeds)}, sort_keys=True).encode() - path.write_bytes(payload) - return path, hashlib.sha256(payload).hexdigest() - - -def _config(tmp_path, **overrides): - path, digest = _seed_bank(tmp_path) - config = { - "noise_seed_bank_path": str(path), - "noise_seed_bank_sha256": digest, - "raw_noise_levels": [0.0, 0.5, 1.0], - "max_batches_per_rank": 1, - "max_samples": None, - "jacobian_samples": 2, - "capture_activations": True, - "activation_layer_map": {0: 0, 1: 2}, - "cknna_k": 2, - "artifact_root": str(tmp_path / "action-flow-artifacts"), - "validation_view": { - "definition": "first_fixed_validation_batch_per_rank", - "per_rank_batch_size": 6, - "world_size": 1, - "split_manifest_sha256": "split-sha", - }, - "provenance": { - "source_commit": "0123456789abcdef", - "resolved_config_sha256": "config-sha", - "normalization_sha256": "normalization-sha", - "sampler": "reverse_euler", - "sampler_steps": 2, - }, - } - config.update(overrides) - return config - - -def _decode(latent, action_dim=2): - if action_dim == 4: - return torch.cat( - ( - latent[..., :2], - torch.cos(latent[..., 2:3]), - torch.sin(latent[..., 2:3]), - ), - dim=-1, - ) - return latent[..., :action_dim] - - -def _diagnostic( - seed=101, - *, - nonfinite=False, - include_activations=True, - action_dim=2, - latent_tokens=2, - action_horizon=None, - zero_clean=False, -): - batch_size, horizon, latent_dim = 6, latent_tokens, 3 - action_horizon = horizon if action_horizon is None else int(action_horizon) - if action_horizon % horizon: - raise ValueError("test action_horizon must be a multiple of latent_tokens") - - def decode_action(value): - decoded = _decode(value, action_dim) - return decoded.repeat_interleave(action_horizon // horizon, dim=-2) - - levels = torch.tensor([0.0, 0.5, 1.0]) - clean = ( - torch.arange(batch_size * horizon * latent_dim, dtype=torch.float32) - .reshape(batch_size, horizon, latent_dim) - .div(10.0) - .add(0.25) - ) - if zero_clean: - clean = torch.zeros_like(clean) - noise = torch.flip(clean, dims=(0,)).neg().sub(0.5) - fixed_states = torch.stack( - [(1.0 - level) * clean + level * noise for level in levels] - ) - predicted_clean = torch.stack([clean + float(level) * 0.03 for level in levels]) - fixed_final = torch.stack([clean + float(level) * 0.01 for level in levels]) - generated = fixed_final[-1] - middle = 0.5 * (noise + generated) - trajectory = torch.stack((noise, middle, generated)) - target = decode_action(clean) - diagnostic = { - "schema": "action-flow-validation-diagnostics/v1", - "source": "validation/usocket", - "noise_seed": seed, - "returned_samples": batch_size, - "jacobian_samples": 2, - "integration_semantics": ( - "reverse_euler_partial_to_lower_canonical_grid_then_uniform_to_zero" - ), - "noise_levels": levels, - "num_inference_steps": 2, - "target": target, - "condition": torch.arange(batch_size * 4, dtype=torch.float32).reshape( - batch_size, 4 - ), - "latent/clean": clean, - "latent/noise": noise, - "latent/generated": generated, - "latent/fixed_states": fixed_states, - "latent/predicted_clean": predicted_clean, - "latent/fixed_final": fixed_final, - "field/predicted_velocity": noise.unsqueeze(0).repeat(3, 1, 1, 1), - "field/velocity_residual": torch.full_like(fixed_states, 0.125), - "latent/trajectory": trajectory, - "decoded/reconstruction": target, - "decoded/noise": decode_action(noise), - "decoded/generated": decode_action(generated), - "decoded/fixed_states": decode_action(fixed_states), - "decoded/predicted_clean": decode_action(predicted_clean), - "decoded/fixed_final": decode_action(fixed_final), - "decoded/trajectory": decode_action(trajectory), - "fixed_level_field_evaluations": torch.tensor([0, 1, 2]), - "decoder_jacobian/clean_singular_values": torch.tensor( - [[2.0, 1.0, 0.5, 0.25], [1.8, 0.9, 0.4, 0.2]] - ), - "decoder_jacobian/noise_singular_values": torch.tensor( - [[1.5, 0.75, 0.3, 0.1], [1.4, 0.7, 0.25, 0.08]] - ), - "decoder_jacobian/fixed_singular_values": torch.tensor( - [ - [[2.0, 1.0, 0.5, 0.25], [1.8, 0.9, 0.4, 0.2]], - [[1.7, 0.8, 0.4, 0.2], [1.6, 0.7, 0.3, 0.1]], - [[1.5, 0.7, 0.3, 0.1], [1.4, 0.6, 0.2, 0.08]], - ] - ), - } - if include_activations: - row = torch.arange(batch_size, dtype=torch.float32) - base = torch.stack((row, row.square(), row.sin(), row.cos()), dim=-1) - base = base[:, None, :].repeat(1, horizon, 1) - encoder = torch.stack((base, 2.0 * base + 0.1)) - field = torch.empty(3, 3, batch_size, horizon, 4) - for level in range(3): - field[level, 0] = encoder[0] * (1.0 + level) - field[level, 1] = base.roll(1, dims=0) - field[level, 2] = encoder[1] * (1.0 + 0.5 * level) - diagnostic.update( - { - "activation/encoder_blocks": encoder, - "activation/encoder_block_indices": torch.tensor([0, 1]), - "activation/field_blocks": field, - "activation/field_block_indices": torch.tensor([0, 1, 2]), - } - ) - if nonfinite: - diagnostic["latent/generated"] = generated.clone() - diagnostic["latent/generated"][0, 0, 0] = float("nan") - return diagnostic - - -class _DiagnosticModel: - def __init__(self, diagnostic): - self.diagnostic = diagnostic - self.calls = [] - - @staticmethod - def forward_eval(batch): - return { - source: {"pred_action": source_batch["actions"]} - for source, source_batch in batch.items() - } - - def run_diagnostic(self, capability, batch, **kwargs): - assert capability == "action_flow" - assert not torch.is_inference_mode_enabled() - self.calls.append(kwargs) - return {source: self.diagnostic for source in batch} - - -class _TwoBlockCodec(nn.Module): - def __init__(self): - super().__init__() - self.blocks = nn.ModuleList([nn.Linear(2, 2), nn.Linear(2, 2)]) - - def forward(self, value): - for block in self.blocks: - value = torch.tanh(block(value)) - return value - - -class _TwoBlockField(nn.Module): - def __init__(self): - super().__init__() - self.blocks = nn.ModuleList([nn.Linear(2, 2), nn.Linear(2, 2)]) - - def forward(self, value, time, condition, *, condition_drop_mask=None): - if condition_drop_mask is None: - raise AssertionError("diagnostics must supply an explicit drop mask") - effective = torch.where( - condition_drop_mask[:, None], torch.zeros_like(condition), condition - ) - value = value + time[:, None, None] + effective[:, None, :] - for block in self.blocks: - value = torch.tanh(block(value)) - return value - - -def _batch(target): - return { - "validation/usocket": { - "actions": target, - "embodiment": torch.full((target.shape[0],), 19), - } - } - - -def test_planar_evaluator_logs_and_hashes_action_flow_diagnostics(tmp_path): - diagnostic = _diagnostic() - config = _config(tmp_path) - evaluator = PlanarActionEval( - energy_score_enabled=False, - action_flow_diagnostics={"enabled": True, **config}, - ) - evaluator.bind_data_context(normalizer=_IdentityNormalizer()) - evaluator.model = _DiagnosticModel(diagnostic) - logged = {} - evaluator.trainer = SimpleNamespace( - current_epoch=3, - global_step=41, - global_rank=0, - precision="bf16-mixed", - lightning_module=SimpleNamespace( - log_dict=lambda metrics, **_kwargs: logged.update(metrics) - ), - ) - evaluator.on_validation_start() - torch.manual_seed(8675309) - rng_state = torch.random.get_rng_state().clone() - - evaluator.on_validation_step(_batch(diagnostic["target"]), batch_idx=7) - - assert torch.equal(torch.random.get_rng_state(), rng_state) - assert evaluator.model.calls == [ - { - "raw_noise_levels": [0.0, 0.5, 1.0], - "noise_seed": 101, - "max_samples": None, - "jacobian_samples": 2, - "capture_activations": True, - } - ] - required_metrics = ( - "Valid/ActionFlow/Latent/clean/RMS", - "Valid/ActionFlow/Latent/generated/EffectiveRank", - "Valid/ActionFlow/Latent/clean/CovarianceEigenvalue/eig_00", - "Valid/ActionFlow/DecoderJacobian/clean/spectral_norm_mean", - "Valid/ActionFlow/DecodedFullNoise/token_radius_mean", - "Valid/ActionFlow/Compute/InferenceFieldEvaluations", - "Valid/ActionFlow/DenoisingTrajectory/LatentMSE/t1000", - "Valid/ActionFlow/DenoisingTrajectory/DecodedMSE/t0000", - "Valid/ActionFlow/FixedLevel/final_latent_mse/t0500", - "Valid/ActionFlow/Alignment/FinalLatentCosine/t1000", - "Valid/ActionFlow/Alignment/CKA/encoder_00__field_00/t0500", - "Valid/ActionFlow/Alignment/CKNNA/encoder_01__field_02/t1000", - ) - for name in required_metrics: - assert name in logged - assert logged[name].ndim == 0 - assert bool(torch.isfinite(logged[name])) - assert f"{name}/pushshapes_sim_u_socket" in logged - assert float( - logged["Valid/ActionFlow/Alignment/CKA/encoder_00__field_00/t0500"] - ) == pytest.approx(1.0, abs=1.0e-5) - - artifact = tmp_path / "action-flow-artifacts/epoch-3-step-41/rank-0-batch-7.pt" - sidecar = artifact.with_name(f"{artifact.name}.sha256") - assert artifact.is_file() and sidecar.is_file() - sidecar_payload = json.loads(sidecar.read_text()) - assert ( - sidecar_payload["sha256"] == hashlib.sha256(artifact.read_bytes()).hexdigest() - ) - payload = torch.load(artifact, map_location="cpu", weights_only=False) - assert payload["metric"] == "ActionFlowValidationDiagnostics" - assert ( - payload["identity_sha256"] == evaluator._action_flow_diagnostics.identity_sha256 - ) - assert payload["noise_seed"] == 101 - assert payload["sampler"]["name"] == "reverse_euler" - assert payload["statistics"]["trajectory_error"] == ( - "paired_diagnostic_not_distributional_score" - ) - source = payload["sources"]["pushshapes_sim_u_socket"] - assert source["computed"]["final_latent_cosine_by_condition"].shape == (3, 6) - assert source["computed"]["latent_statistics"]["clean"][ - "covariance_eigenvalues" - ].shape == (3,) - - evaluator.on_validation_start() - with pytest.raises(FileExistsError, match="refusing to overwrite"): - evaluator.on_validation_step(_batch(diagnostic["target"]), batch_idx=7) - - -def test_action_flow_diagnostics_allow_latent_tokens_to_differ_from_action_horizon( - tmp_path, -): - diagnostic = _diagnostic(latent_tokens=2, action_horizon=4) - config = _config(tmp_path) - config["provenance"]["latent_shape"] = [2, 3] - evaluator = PlanarActionEval( - energy_score_enabled=False, - action_flow_diagnostics={"enabled": True, **config}, - ) - evaluator.bind_data_context(normalizer=_IdentityNormalizer()) - evaluator.model = _DiagnosticModel(diagnostic) - logged = {} - evaluator.trainer = SimpleNamespace( - current_epoch=0, - global_step=1, - global_rank=0, - precision="bf16-mixed", - lightning_module=SimpleNamespace( - log_dict=lambda metrics, **_kwargs: logged.update(metrics) - ), - ) - evaluator.on_validation_start() - - evaluator.on_validation_step(_batch(diagnostic["target"]), batch_idx=0) - - assert "Valid/ActionFlow/CleanReconstructionMSE" in logged - artifact = tmp_path / "action-flow-artifacts/epoch-0-step-1/rank-0-batch-0.pt" - payload = torch.load(artifact, map_location="cpu", weights_only=False) - saved = payload["sources"]["pushshapes_sim_u_socket"]["diagnostic"] - assert saved["latent/clean"].shape == (6, 2, 3) - assert saved["target"].shape == (6, 4, 2) - - -def test_action_flow_diagnostics_reject_undefined_zero_target_cosine(tmp_path): - diagnostic = _diagnostic(zero_clean=True) - runner = ActionFlowDiagnostics(_config(tmp_path)) - - with pytest.raises(ValueError, match="zero clean-latent norm"): - runner.run( - model=_DiagnosticModel(diagnostic), - batch=_batch(diagnostic["target"]), - batch_idx=0, - rank=0, - epoch=0, - global_step=1, - precision="bf16-mixed", - source_labels={"validation/usocket": "usocket"}, - ) - - -def test_action_flow_artifacts_preserve_slurm_attempts_and_same_attempt_refusal( - tmp_path, monkeypatch -): - diagnostic = _diagnostic() - legacy = tmp_path / "action-flow-artifacts/epoch-3-step-41/rank-0-batch-7.pt" - legacy.parent.mkdir(parents=True) - legacy.write_bytes(b"historical diagnostic") - for job_id, restart in (("5714540", 0), ("5714540", 1), ("5715500", 0)): - monkeypatch.setenv("SLURM_JOB_ID", job_id) - monkeypatch.setenv("SLURM_RESTART_COUNT", str(restart)) - evaluator = PlanarActionEval( - energy_score_enabled=False, - action_flow_diagnostics={"enabled": True, **_config(tmp_path)}, - ) - evaluator.bind_data_context(normalizer=_IdentityNormalizer()) - evaluator.model = _DiagnosticModel(diagnostic) - evaluator.trainer = SimpleNamespace( - current_epoch=3, - global_step=41, - global_rank=0, - precision="32-true", - lightning_module=SimpleNamespace(log_dict=lambda *_args, **_kwargs: None), - ) - evaluator.on_validation_start() - evaluator.on_validation_step(_batch(diagnostic["target"]), batch_idx=7) - artifact = ( - legacy.parent.parent - / f"job-{job_id}-restart-{restart}" - / legacy.parent.name - / legacy.name - ) - payload = torch.load(artifact, map_location="cpu", weights_only=False) - assert payload["execution"] == { - "slurm_job_id": job_id, - "slurm_restart_count": restart, - } - assert ( - payload["identity_sha256"] - == evaluator._action_flow_diagnostics.identity_sha256 - ) - sidecar = json.loads(Path(f"{artifact}.sha256").read_text()) - assert sidecar["sha256"] == hashlib.sha256(artifact.read_bytes()).hexdigest() - evaluator.on_validation_start() - with pytest.raises(FileExistsError, match="refusing to overwrite"): - evaluator.on_validation_step(_batch(diagnostic["target"]), batch_idx=7) - assert legacy.read_bytes() == b"historical diagnostic" - - -def test_planar_action_flow_diagnostics_emit_native_circular_errors(tmp_path): - diagnostic = _diagnostic(action_dim=4) - config = _config(tmp_path, native_error=USOCKET_NATIVE_ERROR_CONFIG) - evaluator = PlanarActionEval( - energy_score_enabled=False, - native_decoder=USocketRotVecNativeDecoder(), - action_flow_diagnostics={"enabled": True, **config}, - ) - evaluator.bind_data_context(normalizer=_IdentityNormalizer()) - evaluator.model = _DiagnosticModel(diagnostic) - logged = {} - evaluator.trainer = SimpleNamespace( - current_epoch=0, - global_step=2, - global_rank=0, - precision="bf16-mixed", - lightning_module=SimpleNamespace( - log_dict=lambda metrics, **_kwargs: logged.update(metrics) - ), - ) - evaluator.on_validation_start() - - evaluator.on_validation_step(_batch(diagnostic["target"]), batch_idx=0) - - required = ( - "Valid/ActionFlow/CleanReconstructionNativeMSE", - "Valid/ActionFlow/DenoisingTrajectory/DecodedNativeMSE/t1000", - "Valid/ActionFlow/DenoisingTrajectory/DecodedNativeMSE/t0500", - "Valid/ActionFlow/DenoisingTrajectory/DecodedNativeMSE/t0000", - "Valid/ActionFlow/FixedLevel/final_decoded_native_mse/t0500", - ) - for name in required: - assert name in logged - assert f"{name}/pushshapes_sim_u_socket" in logged - assert bool(torch.isfinite(logged[name])) - assert float(logged["Valid/ActionFlow/CleanReconstructionNativeMSE"]) == 0.0 - - artifact = tmp_path / "action-flow-artifacts/epoch-0-step-2/rank-0-batch-0.pt" - payload = torch.load(artifact, map_location="cpu", weights_only=False) - assert payload["identity"]["native_error"] == USOCKET_NATIVE_ERROR_CONFIG - assert payload["statistics"]["native_action_error"] == (USOCKET_NATIVE_ERROR_CONFIG) - computed = payload["sources"]["pushshapes_sim_u_socket"]["computed"] - assert computed["clean_reconstruction_native_mse_by_condition"].shape == (6,) - assert computed["trajectory_decoded_native_mse_by_condition"].shape == (3, 6) - - -def test_native_action_flow_error_wraps_theta_at_pi_boundary(): - target_theta = torch.full((2, 3), torch.pi - 0.01) - predicted_theta = torch.full((2, 3), -torch.pi + 0.01) - - def rotvec(theta): - return torch.stack( - ( - torch.zeros_like(theta), - torch.zeros_like(theta), - torch.cos(theta), - torch.sin(theta), - ), - dim=-1, - ) - - decoder = USocketRotVecNativeDecoder() - values = PlanarActionEval._native_mse_by_condition( - decoder.decode(rotvec(predicted_theta)), - decoder.decode(rotvec(target_theta)), - decoder, - ) - - expected = torch.full((2,), (0.02**2) / 3.0) - torch.testing.assert_close(values, expected, atol=1.0e-7, rtol=0.0) - - -def test_action_flow_consumer_matches_real_wrapper_schema(tmp_path): - torch.manual_seed(91) - wrapper = ModelWrapper( - pipeline=PipelineAlgo( - stages=[ - ContentEncoderStage(_TwoBlockCodec()), - ConditionalVelocityStage(_TwoBlockField(), num_inference_steps=2), - ContentDecoderStage(_TwoBlockCodec()), - ], - device="cpu", - ), - training_behavior=ActionFlowTrainingBehavior(gradient_telemetry_cadence=0), - diagnostic_provider=ActionFlowDiagnosticProvider(), - ) - wrapper.eval() - runner = ActionFlowDiagnostics(_config(tmp_path, activation_layer_map={0: 0, 1: 1})) - batch = { - "opaque_source": { - "target": torch.randn(6, 2, 2) + 0.25, - "condition": torch.randn(6, 2), - } - } - - metrics = runner.run( - model=wrapper, - batch=batch, - batch_idx=0, - rank=0, - epoch=0, - global_step=0, - precision="32-true", - source_labels={"opaque_source": "opaque"}, - ) - - assert "Valid/ActionFlow/Latent/clean/RMS" in metrics - assert "Valid/ActionFlow/Alignment/CKA/encoder_01__field_01/t1000" in metrics - assert all(bool(torch.isfinite(value)) for value in metrics.values()) - - -def test_action_flow_diagnostic_forward_selects_private_routed_codecs(): - torch.manual_seed(92) - model = PipelineAlgo( - stages=[ - RoutedContentEncoderStage( - encoders={"route_a": _TwoBlockCodec(), "route_b": _TwoBlockCodec()}, - route_key="route", - ), - ConditionalVelocityStage(_TwoBlockField(), num_inference_steps=2), - RoutedContentDecoderStage( - decoders={"route_a": _TwoBlockCodec(), "route_b": _TwoBlockCodec()}, - route_key="route", - ), - ], - device="cpu", - ) - batch = { - route: { - "target": torch.randn(3, 2, 2), - "condition": torch.randn(3, 2), - "route": route, - } - for route in ("route_a", "route_b") - } - - diagnostics = collect_action_flow_diagnostics( - model, - batch, - raw_noise_levels=(0.0, 1.0), - noise_seed=101, - max_samples=3, - jacobian_samples=1, - capture_activations=True, - already_processed=True, - ) - - assert tuple(diagnostics) == ("route_a", "route_b") - for diagnostic in diagnostics.values(): - assert diagnostic["provenance/encoder_class"] == "_TwoBlockCodec" - assert diagnostic["provenance/decoder_class"] == "_TwoBlockCodec" - assert diagnostic["activation/encoder_blocks"].shape[:2] == (2, 3) - assert diagnostic["decoded/generated"].shape == (3, 2, 2) - - -@pytest.mark.parametrize( - "experiment", - ( - "action_flow_bc_usocket_recon1_s42", - "action_flow_bc_usocket_recon10_s42", - ), -) -def test_real_action_flow_config_constructs_strict_diagnostics(tmp_path, experiment): - with initialize_config_dir( - version_base=None, config_dir=str(_CONFIG_DIR.resolve()) - ): - config = compose( - config_name="train_zarr_cartesian", - overrides=[f"+experiment=sim/pusht/{experiment}", "++paths.root_dir=."], - ) - diagnostics = config.evaluator.action_flow_diagnostics - diagnostics.noise_seed_bank_path = str( - _CONFIG_DIR / "evaluator/energy_score_seed_bank_k32_v1.json" - ) - diagnostics.artifact_root = str(tmp_path / experiment) - diagnostics = OmegaConf.to_container(diagnostics, resolve=True) - - evaluator = PlanarActionEval( - energy_score_enabled=False, - action_flow_diagnostics=diagnostics, - ) - - runner = evaluator._action_flow_diagnostics - assert evaluator.action_flow_diagnostics_enabled - assert runner.noise_levels == [0.0, 0.25, 0.5, 0.75, 1.0] - assert runner.max_samples == 16 - assert runner.jacobian_samples == 2 - assert runner.activation_layer_map == ((0, 0), (1, 11)) - assert runner.cknna_k == 10 - - -def test_action_flow_diagnostics_fail_closed_without_requested_activations(tmp_path): - runner = ActionFlowDiagnostics(_config(tmp_path)) - diagnostic = _diagnostic(include_activations=False) - model = _DiagnosticModel(diagnostic) - batch = _batch(diagnostic["target"]) - - with pytest.raises(TypeError, match="activation/encoder_blocks"): - runner.run( - model=model, - batch=batch, - batch_idx=0, - rank=0, - epoch=0, - global_step=0, - precision="32-true", - source_labels={"validation/usocket": "usocket"}, - ) - assert not list((tmp_path / "action-flow-artifacts").rglob("*.pt")) - - -def test_action_flow_diagnostics_reject_nonfinite_model_outputs(tmp_path): - runner = ActionFlowDiagnostics(_config(tmp_path)) - diagnostic = _diagnostic(nonfinite=True) - - with pytest.raises(ValueError, match="non-finite.*latent/generated"): - runner.run( - model=_DiagnosticModel(diagnostic), - batch=_batch(diagnostic["target"]), - batch_idx=0, - rank=0, - epoch=0, - global_step=0, - precision=None, - source_labels={"validation/usocket": "usocket"}, - ) - - -@pytest.mark.parametrize( - ("overrides", "match"), - ( - ({"jacobian_samples": 0}, "jacobian_samples must be positive"), - ({"max_samples": 2, "jacobian_samples": 3}, "cannot exceed max_samples"), - ({"capture_activations": True, "activation_layer_map": {}}, "non-empty"), - ({"cknna_k": 6}, "k < diagnostic batch size"), - ({"raw_noise_levels": [0.5, 0.5]}, "strictly increase"), - ), -) -def test_action_flow_diagnostic_config_fails_closed(tmp_path, overrides, match): - with pytest.raises(ValueError, match=match): - ActionFlowDiagnostics(_config(tmp_path, **overrides)) - - -@pytest.mark.parametrize("restart", [None, 0, 1]) -def test_existing_latent_diagnostic_native_conversion_uses_decoder_argument( - tmp_path, - monkeypatch, - restart, -): - monkeypatch.delenv("SLURM_JOB_ID", raising=False) - if restart is not None: - monkeypatch.setenv("SLURM_JOB_ID", "5714540") - monkeypatch.setenv("SLURM_RESTART_COUNT", str(restart)) - evaluator = PlanarActionEval(energy_score_enabled=False) - evaluator.bind_data_context(normalizer=_IdentityNormalizer()) - evaluator.unite_diagnostics = {"artifact_root": str(tmp_path / "legacy-diagnostic")} - evaluator._unite_noise_seeds = [101] - evaluator._unite_noise_seed_sha256 = "seed-sha" - evaluator._unite_noise_levels = [0.0, 1.0] - evaluator._unite_noise_labels = ["t000", "t1000"] - evaluator._unite_cknna_k = 2 - target = _diagnostic()["target"] - clean = torch.cat((target, torch.ones_like(target[..., :1])), dim=-1) - feature = torch.arange(1, 7, dtype=torch.float32)[:, None, None].repeat(1, 2, 3) - evaluator.model = SimpleNamespace( - run_diagnostic=lambda capability, _batch, raw_noise_levels: { - "validation/usocket": { - "clean_latent": clean, - "sampler_latents": torch.stack((clean + 1.0, clean)), - "decoded_actions_normalized": torch.stack((target + 1.0, target)), - "noise_level_final_predictions": torch.stack((clean, clean + 0.1)), - "tokenization_activations": {"layer": feature}, - "denoising_activations": { - "layer": torch.stack((feature, feature * 2.0)) - }, - } - } - if capability == "unite" - else None - ) - evaluator.trainer = SimpleNamespace( - current_epoch=1, - global_step=2, - global_rank=0, - ) - batch = _batch(target) - - metrics = evaluator._unite_metrics_and_artifact(batch, batch_idx=0) - - assert "Valid/DenoisingTrajectory/DecodedNativeMSE/step_0" in metrics - root = tmp_path / "legacy-diagnostic" - if restart is not None: - root = root / f"job-5714540-restart-{restart}" - artifact = root / "epoch-1-step-2/rank-0-batch-0.pt" - assert artifact.is_file() - payload = torch.load(artifact, map_location="cpu", weights_only=False) - assert payload["execution"] == ( - None - if restart is None - else {"slurm_job_id": "5714540", "slurm_restart_count": restart} - ) - with pytest.raises(FileExistsError, match="refusing to overwrite"): - evaluator._unite_metrics_and_artifact(batch, batch_idx=0) diff --git a/tests/test_action_flow_jacobian_checkpoint_performance.py b/tests/test_action_flow_jacobian_checkpoint_performance.py deleted file mode 100644 index 5408c6ad14..0000000000 --- a/tests/test_action_flow_jacobian_checkpoint_performance.py +++ /dev/null @@ -1,555 +0,0 @@ -"""Opt-in, read-only checkpoint equivalence/timing test for exact endpoint reuse. - -Set ACTION_FLOW_JACOBIAN_PROFILE_REQUEST to a JSON request outside the checkout. -Ordinary CPU suites skip the GPU benchmark. No optimizer or policy sampler runs. -""" - -from __future__ import annotations - -import copy -import gc -import hashlib -import json -import os -import statistics -import time -from pathlib import Path -from types import SimpleNamespace -from unittest.mock import patch - -import numpy as np -import pytest -import torch -from hydra.utils import instantiate -from omegaconf import OmegaConf - -import egomimic.eval.action_flow_diagnostic_forward as owner - - -def prepare_training_profile(request): - """Restore native data/model state on CPU; capture one real training batch.""" - from torch.utils.data import default_collate - - from egomimic.rldb.zarr.zarr_dataset_multi import MultiDataset - - assert _digest(request["split"]) == request["split_sha256"] - split = json.loads(Path(request["split"]).read_text()) - for domain in ("yam", "human"): - for part in ("train", "valid"): - os.environ[f"{domain.upper()}_{part.upper()}_IDS_SHA256"] = split[domain][ - "summary" - ][f"{part}_ids_sha256"] - assert _digest(request["checkpoint"]) == request["checkpoint_sha256"] - payload = torch.load( - request["checkpoint"], map_location="cpu", mmap=True, weights_only=False - ) - assert payload["global_step"] == request["checkpoint_step"] - cfg = OmegaConf.load(request["config"]) - assert payload["hyper_parameters"]["config_tree"][ - "model" - ] == OmegaConf.to_container(cfg.model, resolve=True) - assert [int(cfg.model.num_latent_tokens), int(cfg.model.latent_dim)] == request[ - "latent_shape" - ] - assert ( - int(cfg.data.train_dataloader_params[request["source_key"]].batch_size) - == request["batch_size"] - ) - state = copy.deepcopy(payload["data_context"]["normalizer_state"]) - for field in ("key_types", "zarr_keys", "shapes", "norm_stats"): - state[field] = {int(k): v for k, v in state[field].items()} - normalizer = MultiDataset.from_state(state) - fixture_path = Path(request["fixture"]) - if fixture_path.exists(): - if request.get("fixture_sha256"): - assert _digest(fixture_path) == request["fixture_sha256"] - fixture = torch.load(fixture_path, weights_only=False) - assert fixture["checkpoint"] == request.get( - "fixture_checkpoint", request["checkpoint"] - ) - assert fixture["source_key"] == request["source_key"] - else: - dataset = instantiate(cfg.data.train_datasets[request["source_key"]]) - dataset.set_norm_stats_from(normalizer) - indices = np.linspace( - 0, len(dataset) - 1, request["batch_size"], dtype=int - ).tolist() - flat = default_collate([dataset[index] for index in indices]) - fixture = { - "checkpoint": request["checkpoint"], - "source_key": request["source_key"], - "indices": indices, - "batch": flat, - } - torch.save(fixture, fixture_path) - algo = instantiate(cfg.model.pipeline, device="cpu") - algo.bind_data_context(normalizer=normalizer) - weights = { - k.removeprefix("nets."): v - for k, v in payload["state_dict"].items() - if k.startswith("nets.") - } - algo.nets.load_state_dict(weights, strict=True) - return cfg, payload, algo, fixture - - -def _cpu_tree(value): - if torch.is_tensor(value): - return value.detach().cpu().clone() - if isinstance(value, dict): - return {k: _cpu_tree(v) for k, v in value.items()} - if isinstance(value, (tuple, list)): - return type(value)(_cpu_tree(v) for v in value) - return copy.deepcopy(value) - - -def _compare_training_tree(actual, expected): - """Compare every tensor; retain exact mismatch counts and largest errors.""" - mismatches, tensor_count = [], 0 - - def visit(a, b, path): - nonlocal tensor_count - if torch.is_tensor(a): - tensor_count += 1 - if not (torch.is_tensor(b) and a.shape == b.shape and a.dtype == b.dtype): - mismatches.append( - {"path": path, "max_abs": 0.0, "contract_changed": True} - ) - return - if not torch.equal(a, b): - diff = (a.double() - b.double()).abs() - mismatches.append( - { - "path": path, - "max_abs": float(diff.max()), - "different_elements": int(torch.count_nonzero(diff)), - "relative_rms": float( - torch.linalg.vector_norm(diff) - / torch.linalg.vector_norm(b.double()).clamp_min(1e-30) - ), - } - ) - elif isinstance(a, dict): - assert a.keys() == b.keys(), path - for k in a: - visit(a[k], b[k], path + "/" + str(k)) - elif isinstance(a, (tuple, list)): - assert len(a) == len(b), path - for k, (x, y) in enumerate(zip(a, b)): - visit(x, y, path + "/" + str(k)) - elif torch.is_tensor(b) or a != b: - mismatches.append( - {"path": path, "actual": str(a), "expected": str(b), "max_abs": 0.0} - ) - - visit(actual, expected, "") - return { - "bit_exact": not mismatches, - "tensor_count": tensor_count, - "mismatch_count": len(mismatches), - "largest_errors": sorted(mismatches, key=lambda x: x["max_abs"], reverse=True)[ - :12 - ], - } - - -@pytest.mark.skipif( - not os.environ.get("ACTION_FLOW_TRAINING_PROFILE_REQUEST"), - reason="Opt-in native checkpoint training test", -) -def test_checkpoint_training_execution_equivalence_and_timing(): - """Test existing execution options without changing/adopting model weights.""" - from egomimic.utils.libero_dit_half import LiberoDiTHalf - - assert torch.cuda.is_available() and torch.cuda.device_count() == 1 - request = json.loads( - Path(os.environ["ACTION_FLOW_TRAINING_PROFILE_REQUEST"]).read_text() - ) - out = Path(request["output_root"]) - out.mkdir(parents=True, exist_ok=True) - cfg, payload, initial, fixture = prepare_training_profile(request) - assert str(cfg.trainer.precision) == "bf16-mixed" - baseline, records = [], [] - initial_state = _cpu_tree(initial.nets.state_dict()) - del initial - for case_index, variant in enumerate(request["variants"]): - algo = instantiate(cfg.model.pipeline, device="cpu") - algo.nets.load_state_dict(initial_state, strict=True) - holder = torch.nn.Module() - holder.nets = algo.nets - holder.cuda().train() - algo.device = torch.device("cuda") - optimizer = instantiate( - cfg.model.optimizer, named_params=tuple(holder.named_parameters()) - )() - scheduler = instantiate(cfg.model.scheduler, optimizer=optimizer)() - optimizer.load_state_dict(copy.deepcopy(payload["optimizer_states"][0])) - scheduler.load_state_dict(copy.deepcopy(payload["lr_schedulers"][0])) - ema = instantiate(cfg.callbacks.ema) - ema.on_load_checkpoint(None, holder, payload) - ema.on_fit_start(SimpleNamespace(global_step=payload["global_step"]), holder) - policy = LiberoDiTHalf( - source_key=request["source_key"], - checkpoint_periods={ - "encoder": 2, - "velocity": { - "third_flow_recomputation": 3, - "quarter_flow_recomputation": 4, - }[variant], - } - if variant in {"third_flow_recomputation", "quarter_flow_recomputation"} - else None, - ) - policy.on_fit_start(None, SimpleNamespace(model=algo)) - encoder, field, decoder = ( - algo.pipeline.stages[3].encoder, - algo.pipeline.stages[5], - algo.pipeline.stages[6].decoder, - ) - if variant == "no_dit_recomputation": - encoder.backbone.gradient_checkpointing = False - field.field.backbone.gradient_checkpointing = False - elif variant == "no_reconstruction_recomputation": - decoder.gradient_checkpointing = False - elif variant == "shared_field_prediction": - field.fm_field_execution = "shared" - else: - assert variant in { - "baseline", - "third_flow_recomputation", - "quarter_flow_recomputation", - } - batch = algo.process_batch_for_training( - {request["source_key"]: fixture["batch"]} - ) - timings, comparisons = [], [] - failure = None - try: - for step in range(request["steps"]): - torch.manual_seed(request["seed"] + step) - torch.cuda.synchronize() - torch.cuda.reset_peak_memory_stats() - started = time.perf_counter() - optimizer.zero_grad(set_to_none=True) - with torch.autocast("cuda", dtype=torch.bfloat16): - prediction = algo.forward_training(batch) - loss = algo.compute_losses(prediction, batch)["loss"] - assert bool(torch.isfinite(loss)) - loss.backward() - torch.cuda.synchronize() - backward_done = time.perf_counter() - gradients = _cpu_tree( - {name: p.grad for name, p in holder.named_parameters()} - ) - assert all( - value is None or bool(torch.isfinite(value).all()) - for value in gradients.values() - ) - components = _cpu_tree( - { - k: v - for k, v in prediction[request["source_key"]].items() - if k.startswith("log/action_flow_") or k == "loss/action_flow" - } - ) - # Copying proof tensors is deliberately outside measured GPU work. - optimizer_start = time.perf_counter() - clip = float(cfg.trainer.get("gradient_clip_val", 0) or 0) - if clip: - assert cfg.trainer.get("gradient_clip_algorithm", "norm") == "norm" - torch.nn.utils.clip_grad_norm_(holder.parameters(), clip) - optimizer.step() - scheduler.step() - ema._update(holder) - torch.cuda.synchronize() - ended = time.perf_counter() - timings.append( - { - "step": step, - "forward_backward_seconds": backward_done - started, - "optimizer_scheduler_ema_seconds": ended - optimizer_start, - "seconds": backward_done - started + ended - optimizer_start, - "peak_cuda_bytes": torch.cuda.max_memory_allocated(), - } - ) - actual = { - "gradients": gradients, - "components": components, - "state": _cpu_tree(holder.state_dict()), - "optimizer": _cpu_tree(optimizer.state_dict()), - "scheduler": _cpu_tree(scheduler.state_dict()), - "ema": _cpu_tree(ema._shadow), - "rng_cpu": torch.get_rng_state(), - "rng_cuda": torch.cuda.get_rng_state(), - } - if case_index == 0: - baseline.append(actual) - else: - comparisons.append( - { - section: _compare_training_tree( - value, baseline[step][section] - ) - for section, value in actual.items() - } - ) - print( - "TRAINING_PROFILE_STEP", - variant, - json.dumps(timings[-1]), - flush=True, - ) - del prediction, loss, actual, gradients, components - except (torch.cuda.OutOfMemoryError, RuntimeError) as exc: - failure = {"type": type(exc).__name__, "message": str(exc)} - if case_index == 0: - raise - finally: - policy.on_fit_end(None, None) - record = { - "variant": variant, - "timings": timings, - "comparisons": comparisons, - "failure": failure, - "bit_exact": failure is None - and all(all(s["bit_exact"] for s in step.values()) for step in comparisons), - "median_seconds": statistics.median(t["seconds"] for t in timings[1:]) - if len(timings) > 1 - else None, - } - records.append(record) - (out / "TRAINING_PROGRESS.json").write_text(json.dumps(records, indent=2)) - prediction = loss = actual = gradients = components = None - del ( - prediction, - loss, - actual, - gradients, - components, - algo, - holder, - optimizer, - scheduler, - ema, - batch, - policy, - encoder, - field, - decoder, - ) - gc.collect() - torch.cuda.empty_cache() - result = { - "status": "COMPLETE", - "checkpoint_step": request["checkpoint_step"], - "batch_size": request["batch_size"], - "latent_shape": request["latent_shape"], - "precision": "bf16-mixed_native_FP32_decoder_JVP", - "torch": torch.__version__, - "gpu": torch.cuda.get_device_name(), - "records": records, - "scope": "native full pipeline forward/backward/optimizer/scheduler/EMA; frozen real training batch; excludes data-loader, telemetry and proof-copy overhead; no weights adopted", - } - (out / "TRAINING_RESULT.json").write_text(json.dumps(result, indent=2)) - - -def _digest(path): - result = hashlib.sha256() - with Path(path).open("rb") as handle: - for chunk in iter(lambda: handle.read(1024 * 1024), b""): - result.update(chunk) - return result.hexdigest() - - -def load_profile_components(request, device="cpu"): - """Strict component restoration; also used by the scheduled CPU preflight.""" - assert _digest(request["checkpoint"]) == request["checkpoint_sha256"] - cfg = OmegaConf.load(request["config"]) - state = torch.load( - request["checkpoint"], map_location="cpu", mmap=True, weights_only=False - ) - assert state["global_step"] == request["checkpoint_step"] - assert state["hyper_parameters"]["config_tree"]["model"] == OmegaConf.to_container( - cfg.model, resolve=True - ) - result = {} - for name, spec in request["components"].items(): - module = instantiate(OmegaConf.select(cfg, spec["config_key"])) - prefix = spec["state_prefix"] - weights = { - k[len(prefix) :]: v - for k, v in state["state_dict"].items() - if k.startswith(prefix) - } - assert weights, f"Checkpoint component prefix absent: {prefix}" - module.load_state_dict(weights, strict=True) - module.eval().float().to(device) - for parameter in module.parameters(): - parameter.requires_grad_(False) - result[name] = module - assert int(cfg.model.num_latent_tokens) == request["latent_shape"][0] - assert int(cfg.model.latent_dim) == request["latent_shape"][1] - assert int(cfg.model.action_horizon) == request["action_shape"][0] - assert int(cfg.model.action_dim) == request["action_shape"][1] - return result - - -@pytest.mark.skipif( - not os.environ.get("ACTION_FLOW_JACOBIAN_PROFILE_REQUEST"), - reason="Opt-in checkpoint GPU benchmark", -) -def test_checkpoint_endpoint_reuse_equivalence_and_timing(): - assert torch.cuda.is_available() and torch.cuda.device_count() == 1 - request_path = Path(os.environ["ACTION_FLOW_JACOBIAN_PROFILE_REQUEST"]) - request = json.loads(request_path.read_text()) - request_sha = _digest(request_path) - out = Path(request["output_root"]) - out.mkdir(parents=True, exist_ok=True) - torch.backends.cuda.matmul.allow_tf32 = False - torch.backends.cudnn.allow_tf32 = False - modules = load_profile_components(request, "cuda") - encoder, decoder = modules["encoder"], modules["decoder"] - assert _digest(request["targets"]) == request["targets_sha256"] - with np.load(request["targets"], allow_pickle=False) as artifact: - target = ( - torch.from_numpy(artifact["target"][request["target_indices"]].copy()) - .float() - .cuda() - ) - assert target.shape[1:] == tuple(request["action_shape"]) - torch.manual_seed(request["seed"]) - with torch.no_grad(): - clean = encoder(target) - assert clean.shape[1:] == tuple(request["latent_shape"]) - generator = torch.Generator(device=clean.device).manual_seed(request["seed"]) - noise = torch.randn( - clean.shape, device=clean.device, dtype=clean.dtype, generator=generator - ) - fixed = torch.stack( - [(1.0 - level) * clean + level * noise for level in request["noise_levels"]] - ) - count = request["jacobian_samples"] - assert count <= len(target) - method = "forward_math_chunk4" - native = owner._decoder_singular_values - parameters = {k: v.detach().cpu().clone() for k, v in decoder.state_dict().items()} - rng_cpu, rng_cuda = torch.get_rng_state(), torch.cuda.get_rng_state() - flags = [p.requires_grad for p in decoder.parameters()] - - def measure(kind): - with patch.object(owner, "_decoder_singular_values", wraps=native) as calls: - clean_s = owner._decoder_singular_values( - decoder, clean, count, jacobian_method=method - ) - noise_s = owner._decoder_singular_values( - decoder, noise, count, jacobian_method=method - ) - if kind == "baseline": - fixed_s = torch.stack( - [ - owner._decoder_singular_values( - decoder, value, count, jacobian_method=method - ) - for value in fixed - ] - ) - else: - fixed_s = owner._fixed_decoder_singular_values( - decoder, - fixed, - clean, - noise, - clean_s, - noise_s, - count, - jacobian_method=method, - ) - assert calls.call_count == ( - 2 + len(fixed) if kind == "baseline" else len(fixed) - ) - return { - "clean": clean_s.cpu(), - "noise": noise_s.cpu(), - "fixed": fixed_s.cpu(), - }, calls.call_count - - records, reference = [], None - # Alternate order to reduce first-case/allocator bias; completed cases are reusable. - order = [ - kind - for repeat in range(request["repeats"]) - for kind in ( - ("baseline", "optimized") if repeat % 2 == 0 else ("optimized", "baseline") - ) - ] - for index, kind in enumerate(order): - artifact = out / f"case-{index:02d}-{kind}.pt" - if artifact.exists(): - payload = torch.load(artifact, map_location="cpu", weights_only=False) - assert payload["request_sha256"] == request_sha - else: - torch.cuda.synchronize() - torch.cuda.reset_peak_memory_stats() - started = time.perf_counter() - spectra, calls = measure(kind) - torch.cuda.synchronize() - payload = { - "request_sha256": request_sha, - "kind": kind, - "seconds": time.perf_counter() - started, - "full_jacobians": calls * count, - "peak_cuda_bytes": torch.cuda.max_memory_allocated(), - "spectra": spectra, - } - temporary = artifact.with_suffix(".partial") - torch.save(payload, temporary) - temporary.replace(artifact) - if reference is None: - reference = payload["spectra"] - maximum = 0.0 - bit_exact = True - for key, actual in payload["spectra"].items(): - torch.testing.assert_close(actual, reference[key], rtol=1e-5, atol=1e-6) - maximum = max(maximum, float((actual - reference[key]).abs().max())) - bit_exact = bit_exact and torch.equal(actual, reference[key]) - record = {k: v for k, v in payload.items() if k != "spectra"} - record.update( - case=index, max_spectrum_absolute_difference=maximum, bit_exact=bit_exact - ) - records.append(record) - print("JACOBIAN_PROFILE_CASE", json.dumps(record), flush=True) - (out / "progress.json").write_text( - json.dumps({"status": "RUNNING", "cases": records}, indent=2) - ) - - assert torch.equal(rng_cpu, torch.get_rng_state()) - assert torch.equal(rng_cuda, torch.cuda.get_rng_state()) - assert flags == [p.requires_grad for p in decoder.parameters()] - assert all( - torch.equal(v.cpu(), parameters[k]) for k, v in decoder.state_dict().items() - ) - assert all(p.grad is None for p in decoder.parameters()) - baseline = statistics.median( - r["seconds"] for r in records if r["kind"] == "baseline" - ) - optimized = statistics.median( - r["seconds"] for r in records if r["kind"] == "optimized" - ) - result = { - "status": "PASS", - "request_sha256": request_sha, - "checkpoint_step": request["checkpoint_step"], - "weights": "raw_strict_components", - "precision": "FP32_math_SDPA_noTF32", - "torch": torch.__version__, - "gpu": torch.cuda.get_device_name(), - "cases": records, - "baseline_median_seconds": baseline, - "optimized_median_seconds": optimized, - "speedup": baseline / optimized, - "parameter_state_unchanged": True, - "rng_unchanged": True, - "all_singular_values_checked": True, - } - (out / "RESULT.json").write_text(json.dumps(result, indent=2)) - print("JACOBIAN_PROFILE_PASS", json.dumps(result), flush=True) diff --git a/tests/test_action_flow_launcher.py b/tests/test_action_flow_launcher.py deleted file mode 100644 index 3a622c52a2..0000000000 --- a/tests/test_action_flow_launcher.py +++ /dev/null @@ -1,315 +0,0 @@ -from __future__ import annotations - -import os -import re -import subprocess -from pathlib import Path - -import pytest - -ROOT = Path(__file__).resolve().parents[1] -LAUNCHER = ROOT / "scripts" / "train" / "launch_action_flow_usocket.sbatch" -DATASET_VALIDATOR = ROOT / "scripts" / "ice" / "validate_planar_dataset.py" - - -def _source() -> str: - return LAUNCHER.read_text() - - -def test_launcher_has_valid_shell_syntax(): - completed = subprocess.run( - ["bash", "-n", str(LAUNCHER)], - check=False, - capture_output=True, - text=True, - ) - assert completed.returncode == 0, completed.stderr - assert "#SBATCH --kill-on-invalid-dep=yes" in _source() - - -def test_launcher_uses_the_maintained_dataset_validator_success_token(): - validator_source = DATASET_VALIDATOR.read_text() - match = re.search(r'"status":\s*"([A-Z_]+)"', validator_source) - assert match is not None - assert f'dataset_report["status"] == "{match.group(1)}"' in _source() - - -def test_launcher_is_one_portable_fail_closed_contract(): - source = _source() - assert "AF_EXPECTED_HEAD" in source - assert "AF_EXPECTED_LAUNCHER_SHA256" in source - assert "AF_EXPECTED_SPLIT_MANIFEST_SHA256" in source - assert "AF_EXPECTED_CONTENT_MANIFEST_SHA256" in source - assert "AF_EXPECTED_DATASET_CONTENT_AGGREGATE_SHA256" in source - assert "AF_EXPECTED_NORM_SHA256" in source - assert "AF_PREFLIGHT_RESULT" in source - assert "AF_EXPECTED_PREFLIGHT_SHA256" in source - assert "validate_action_flow_config.py" in source - assert "capture_runtime_lock.py" in source - assert "dataset_loader_probe.json" in source - assert "dataset = instantiate(dataset_cfg)" in source - assert "validate_slurm_job_contract.py" in source - assert "check_checkpoint_storage.py" in source - assert "--gres=gpu:1 --constraint='H100|H200'" in source - assert '--allowed-gpu-name "NVIDIA H100 80GB HBM3"' in source - assert '--allowed-gpu-name "NVIDIA H200"' in source - assert "AF_EXPECTED_GPU_CONSTRAINT" in source - assert '--expected-constraint "$AF_EXPECTED_GPU_CONSTRAINT"' in source - assert "trainer.devices=1" in source - assert "trainer.strategy=auto" in source - assert "#SBATCH --requeue" in source - assert "#SBATCH --signal=B:USR1@600" in source - assert "/coc/" not in source - assert "/storage/ice" not in source - assert "/storage/project" not in source - assert 'absolute_path AF_EXTRA_PYTHONPATH "$AF_EXTRA_PYTHONPATH"' in source - assert ( - 'AF_EXTRA_PYTHONPATH=${AF_EXTRA_PYTHONPATH:-$(dirname "$AF_CONFIG_VALIDATOR")}' - in source - ) - assert ( - 'PYTHONPATH="$AF_REPO:${AF_RESUME_HELPER_DIR:+$AF_RESUME_HELPER_DIR:}' - '$(dirname "$AF_CONFIG_VALIDATOR")${AF_EXTRA_PYTHONPATH:+:$AF_EXTRA_PYTHONPATH}"' - in source - ) - - -def test_launcher_accepts_only_the_approved_sweep_and_pins_training_semantics(): - source = _source() - assert "sim/pusht/action_flow_bc_usocket_recon1_s42" in source - assert "sim/pusht/action_flow_bc_usocket_recon10_s42" in source - assert "sim/pusht/action_flow_bc_usocket_recon100_s42" in source - assert ( - "sim/pusht/action_flow_bc_usocket_latent_fm_sg_recon1_200m_muon_lr1e5_s42" - in source - ) - assert ( - "sim/pusht/action_flow_bc_usocket_latent_fm_sg_recon1_200m_adamw_lr1e5_s42" - in source - ) - assert "AF_EXPECTED_CONFIG_NAME=action_flow_bc_usocket_recon1_s42" in source - assert "AF_EXPECTED_CONFIG_NAME=action_flow_bc_usocket_recon10_s42" in source - assert "AF_EXPECTED_CONFIG_NAME=action_flow_bc_usocket_recon100_s42" in source - assert "expected_name = {" not in source - assert "AF_FULL_MAX_STEPS=240000" in source - assert "AF_FULL_VALIDATE_EVERY=10000" in source - assert "AF_FULL_CHECKPOINT_EVERY=40000" in source - assert "AF_FULL_MAX_STEPS=150000" in source - assert "TELEMETRY_EVERY=100" in source - assert "data.train_dataloader_params.$AF_SOURCE.batch_size=32" in source - assert ( - "data.valid_dataloader_params.$AF_SOURCE.batch_size=$AF_VALID_BATCH_SIZE" - in source - ) - assert "AF_FULL_LIMIT_VAL_BATCHES=8" in source - assert "AF_VALID_BATCH_SIZE=32" in source - assert ( - "cfg.data.valid_dataloader_params[source].batch_size == valid_batch_size" - in source - ) - assert ( - "diagnostics.validation_view.per_rank_batch_size == valid_batch_size" in source - ) - assert "cfg.trainer.val_check_interval == full_validate_every" in source - assert ( - "cfg.callbacks.model_checkpoint.every_n_train_steps == full_checkpoint_every" - in source - ) - assert "ckpt_path=null" in source - assert "norm_stats.precomputed_norm_path=$AF_NORM_STATS_PATH" in source - assert "++run_provenance.source_commit=$AF_EXPECTED_HEAD" in source - assert "++run_provenance.content_manifest_sha256=" in source - assert "++run_provenance.dataset_content_aggregate_sha256=" in source - assert "++run_provenance.normalization_sha256=$AF_EXPECTED_NORM_SHA256" in source - assert ( - "++run_provenance.preflight_result_sha256=$AF_EXPECTED_PREFLIGHT_SHA256" - in source - ) - assert '"runtime_lock_sha256": digest(runtime_lock_path)' in source - assert 'current["installed_distributions"]["canonical_sha256"]' in source - assert "[run-preflight] PASS kind={run_kind} parameters={count}" in source - assert "expected_count = 50_725_221" in source - assert "50_801_685" in source - assert "expected_count = 199_754_837" in source - assert "AF_EXPECTED_PARAMETER_COUNT=97956100" in source - assert "ReleasedUniteCompositeOptimizer" in source - assert 'optimizer.muon_adjust_lr_fn == "match_rms_adamw"' in source - assert "AF_SECOND_DATASET_DIR" in source - assert "AF_SECOND_SPLIT_MANIFEST" in source - assert "AF_EXPECTED_SECOND_DATASET_CONTENT_AGGREGATE_SHA256" in source - assert "RoutedContentEncoderStage" in source - assert "RoutedContentDecoderStage" in source - assert "cfg.model.flow_mini_batch == 14" in source - assert "activation_layer_map.items()" in source - - -def test_smoke_runs_optimizer_validation_checkpoint_and_verifier(): - source = _source() - assert "MAX_STEPS=2" in source - assert "VALIDATE_EVERY=2" in source - assert "LIMIT_VAL_BATCHES=1" in source - assert "CHECKPOINT_EVERY=1" in source - assert "TELEMETRY_EVERY=2" in source - assert "verify_action_flow_training_smoke.py" in source - assert "--planned-checkpoint-count 14" in source - assert '--expected-constraint "$AF_EXPECTED_GPU_CONSTRAINT"' in source - assert "--expected-reconstruction-weight" in source - assert "--expected-config-sha256" in source - assert ( - '--expected-config-sha256 "$(sha256 ' - '"$AF_OUTPUT_DIR/.hydra/config.yaml")"' in source - ) - assert "--expected-normalization-sha256" in source - assert "--expected-content-manifest-sha256" in source - assert "--expected-dataset-content-aggregate-sha256" in source - assert "--expected-preflight-sha256" in source - assert "callbacks.model_checkpoint.save_last=link" in source - assert "single-gpu,$AF_RUN_KIND" in source - - -def test_full_and_smoke_share_the_requeue_and_strict_checkpoint_path(): - source = _source() - assert 'source "$AF_REPO/scripts/train/action_flow_run_child.sh"' in source - # The canonical runner arguments live in the shared child entry point. - child = (ROOT / "scripts/train/action_flow_run_child.sh").read_text() - assert "ice_requeue_runner.py" in source - assert "validate_lightning_checkpoint.py" in source - assert '--checkpoint-validator "$AF_CHECKPOINT_VALIDATOR"' in child - assert "--requeue-owner runner" in child - assert "--confirm-child-requeue-disabled" in child - assert "runtime.slurm_requeue_owner=runner" in source - assert "runtime.slurm_save_signal=SIGUSR2" in source - - -def test_full_mode_requires_the_exact_passed_smoke_gate(): - source = _source() - assert "required AF_SMOKE_RESULT" in source - assert "required AF_EXPECTED_SMOKE_SHA256" in source - assert 'payload["status"] == "PASS"' in source - assert 'payload["experiment"] == experiment' in source - assert 'identities["repo_head"] == head' in source - assert 'identities["split_manifest_sha256"] == split_sha' in source - assert 'identities["normalization_sha256"] == norm_sha' in source - assert 'identities["content_manifest_sha256"] == content_sha' in source - assert 'payload["preflight"]["sha256"] == preflight_sha' in source - assert "++run_provenance.smoke_result_sha256=$AF_EXPECTED_SMOKE_SHA256" in source - assert 'cp -- "$AF_SMOKE_RESULT" "$ATTEMPT/SMOKE_RESULT.json"' in source - - -def test_second_preflight_can_reuse_the_first_train_only_normalization(): - source = _source() - assert 'if test -z "${AF_NORM_STATS_PATH:-}"; then' in source - assert 'test -s "$EFFECTIVE_NORM_FILE"' in source - assert "normalization SHA-256 mismatch" in source - - -def test_preflight_reuses_hashed_dataset_evidence_and_removes_logger_group(): - source = _source() - assert "AF_CACHED_DATASET_VALIDATION" in source - assert "AF_EXPECTED_CACHED_DATASET_VALIDATION_SHA256" in source - assert 'payload["status"] == "DATASET_VALIDATED"' in source - assert "'~logger'" in source - assert "logger=null" not in source - assert "++logger.wandb.offline=false" in source - assert "++logger.wandb.name=$AF_WANDB_NAME" in source - assert "++logger.wandb.resume=allow" in source - for key in ("entity", "project", "group", "id", "tags"): - assert f"++logger.wandb.{key}=" in source - - -@pytest.mark.parametrize( - "selector", - [ - "pusht/action_flow_chain_manual4919_avln_80k_s42", - "sim/pusht/parents/action_flow_usocket_h384", - "pusht_historical/action_flow_cotrain_uc_speed_interpolation", - ], -) -def test_retired_and_parent_selectors_fail_before_runtime(selector): - source = _source() - required = source.split("for variable in ", 1)[1].split("; do", 1)[0] - variables = required.replace("\\", " ").split() - env = {**os.environ, **dict.fromkeys(variables, "fixture")} - env.update(AF_REPO=str(ROOT), AF_EXPERIMENT=selector, AF_PYTHON="/must-not-execute") - completed = subprocess.run( - ["bash", str(LAUNCHER)], env=env, capture_output=True, text=True - ) - assert completed.returncode != 0 - assert "retired" in completed.stderr or "historical config" in completed.stderr - assert "must-not-execute" not in completed.stderr - - -@pytest.mark.parametrize( - "run_kind,expected", - [("smoke", ["evaluator.limit_val_batches=1"]), ("full", [])], -) -def test_raw_multiplier_smoke_bounds_actual_evaluator_override(run_kind, expected): - from omegaconf import OmegaConf - - from egomimic.eval.planar_action_eval import PlanarActionEval - - source = _source() - start = source.index("# Bound the evaluator too:") - end = source.index("fi\n\n", start) + len("fi\n\n") - block = source[start:end] - command = ( - "BASE_OVERRIDES=(); " - + block - + '\nfor item in "${BASE_OVERRIDES[@]}"; do printf "%s\\n" "$item"; done' - ) - completed = subprocess.run( - ["bash", "-c", command], - env={ - **os.environ, - "AF_RUN_KIND": run_kind, - "AF_SPEED_DIAGNOSTIC": "true", - "LIMIT_VAL_BATCHES": "1" if run_kind == "smoke" else "8", - }, - capture_output=True, - text=True, - check=True, - ) - overrides = completed.stdout.splitlines() - assert overrides == expected - limit = ( - OmegaConf.from_dotlist(overrides).evaluator.limit_val_batches - if overrides - else 1.0 - ) - evaluator = PlanarActionEval(limit_val_batches=limit, energy_score_enabled=False) - actual = evaluator.trainer_overrides()["limit_val_batches"] - assert type(actual) is (int if run_kind == "smoke" else float) - assert actual == (1 if run_kind == "smoke" else 1.0) - - -@pytest.mark.parametrize( - "mode,step,message", - [ - ("schema", "0", "typed preflight"), - ("run", "extern", "numeric scheduled step"), - ("run", "0", "AF_JOINT_PHASE_INPUTS"), - ], -) -def test_joint_canonical_route_rejects_missing_operational_identity_before_runtime( - mode, step, message -): - required = _source().split("for variable in ", 1)[1].split("; do", 1)[0] - variables = required.replace("\\", " ").split() - env = {key: value for key, value in os.environ.items() if not key.startswith("AF_")} - env.update(dict.fromkeys(variables, "fixture")) - env.update( - AF_REPO=str(ROOT), - AF_EXPERIMENT="sim/libero/action_flow_libero_spatial_sharpa_gripper_keypoints_s42", - AF_PYTHON="/must-not-execute", - AF_LAUNCH_MODE=mode, - AF_RUN_KIND="smoke", - AF_EXPECTED_MEMORY="128G", - AF_EXPECTED_TIME_LIMIT="1:00:00", - SLURM_STEP_ID=step, - ) - result = subprocess.run( - ["bash", str(LAUNCHER)], env=env, capture_output=True, text=True - ) - assert result.returncode != 0 - assert message in result.stderr - assert "must-not-execute" not in result.stderr diff --git a/tests/test_action_flow_manual4919_config.py b/tests/test_action_flow_manual4919_config.py deleted file mode 100644 index 1a17a54196..0000000000 --- a/tests/test_action_flow_manual4919_config.py +++ /dev/null @@ -1,134 +0,0 @@ -"""Guard the corrected manual standard + obstacle-gen ChainGripper recipes.""" - -from pathlib import Path - -import pytest -from hydra import compose, initialize_config_dir -from hydra.utils import instantiate -from omegaconf import OmegaConf - -from egomimic.eval.energy_score import USOCKET_ENERGY_DISTANCE_CONFIG -from egomimic.eval.planar_action_eval import PlanarActionEval -from egomimic.pipeline.pushshapes import ChainGripperPointsNativeDecoder - -CONFIG_DIR = Path(__file__).parents[1] / "egomimic" / "hydra_configs" -ROWS = ("parents/action_flow_cotrain_uc_manual4919",) - - -@pytest.mark.parametrize("row", ROWS) -def test_manual4919_recipe_uses_effective_split_and_cadence(monkeypatch, row): - monkeypatch.setenv("PUSHSHAPES_USOCKET_ROOT", "/verified/usocket-clean2999") - monkeypatch.setenv("PUSHSHAPES_CHAIN_GRIPPER_ROOT", "/verified/manual4919-view") - with initialize_config_dir( - version_base="1.3", config_dir=str(CONFIG_DIR.resolve()) - ): - cfg = compose( - config_name="train_zarr_cartesian", - overrides=[f"+experiment=sim/pusht/{row}"], - ) - chain = cfg.data.train_datasets.pushshapes_sim_chain_gripper - valid = cfg.data.valid_datasets.pushshapes_sim_chain_gripper - assert "data" not in cfg.data - assert set(cfg.data.train_datasets) == ( - {"pushshapes_sim_chain_gripper"} - if row.startswith("action_flow_chain_") - else {"pushshapes_sim_u_socket", "pushshapes_sim_chain_gripper"} - ) - for dataset in (chain, valid): - assert dataset.resolver.folder_path == "/verified/manual4919-view" - assert dataset.resolver.expected_episode_count == 4919 - assert dataset.expected_train_episode_count == 4870 - assert dataset.expected_valid_episode_count == 49 - assert dataset.valid_ratio == 0.01 - assert dataset.split_seed == 42 - assert cfg.run_provenance.chain_collection.standard_manual_episodes == 3000 - assert cfg.run_provenance.chain_collection.obstacle_gen_manual_episodes == 1919 - assert cfg.run_provenance.chain_collection.excluded_frozen_mixed_view - assert cfg.trainer.max_steps == 80000 - assert cfg.trainer.val_check_interval == 15000 - assert cfg.val_at_end - assert cfg.callbacks.model_checkpoint.every_n_train_steps == 5000 - assert cfg.callbacks.model_checkpoint.save_top_k == -1 - assert cfg.evaluator.action_flow_diagnostics.native_error is None - - -@pytest.mark.parametrize("row", ROWS) -def test_manual4919_native_evaluator_decoder_instantiates(monkeypatch, row): - """Catch inherited Hydra kwargs before an allocated GPU smoke starts.""" - monkeypatch.setenv("PUSHSHAPES_USOCKET_ROOT", "/verified/usocket-clean2999") - monkeypatch.setenv("PUSHSHAPES_CHAIN_GRIPPER_ROOT", "/verified/manual4919-view") - with initialize_config_dir( - version_base="1.3", config_dir=str(CONFIG_DIR.resolve()) - ): - cfg = compose( - config_name="train_zarr_cartesian", - overrides=[f"+experiment=sim/pusht/{row}"], - ) - decoder_cfg = ( - cfg.evaluator.native_decoder - if row.startswith("action_flow_chain_") - else cfg.evaluator.native_decoders.pushshapes_sim_chain_gripper - ) - decoder = instantiate(decoder_cfg) - assert isinstance(decoder, ChainGripperPointsNativeDecoder) - if row.startswith("action_flow_chain_"): - assert decoder_cfg.action_horizon == 16 - assert decoder_cfg.native_action_dim == 4 - - -def test_chain_native_decoder_rejects_wrong_native_shape(): - with pytest.raises(ValueError, match="native_action_dim=4"): - ChainGripperPointsNativeDecoder(native_action_dim=6) - - -def test_manual4919_cotrain_resolves_per_domain_energy_distance(monkeypatch): - monkeypatch.setenv("PUSHSHAPES_USOCKET_ROOT", "/verified/usocket-clean2999") - monkeypatch.setenv("PUSHSHAPES_CHAIN_GRIPPER_ROOT", "/verified/manual4919-view") - with initialize_config_dir( - version_base="1.3", config_dir=str(CONFIG_DIR.resolve()) - ): - cfg = compose( - config_name="train_zarr_cartesian", - overrides=[ - "+experiment=sim/pusht/parents/action_flow_cotrain_uc_manual4919" - ], - ) - assert cfg.evaluator.energy_score_distance is None - distances = OmegaConf.to_container( - cfg.evaluator.energy_score_distances_by_embodiment, resolve=True - ) - assert distances == { - "pushshapes_sim_u_socket": { - **USOCKET_ENERGY_DISTANCE_CONFIG, - "complete_normalized_chunk_shape": [16, 4], - "normalized_translation_indices": [0, 1], - }, - "pushshapes_sim_chain_gripper": None, - } - assert ( - OmegaConf.to_container( - cfg.evaluator.energy_score_provenance.distance_contract, resolve=True - ) - == distances - ) - assert ( - OmegaConf.to_container( - cfg.run_provenance.energy_score_contract.distance, resolve=True - ) - == distances - ) - assert cfg.evaluator.action_flow_diagnostics.native_error is None - evaluator = PlanarActionEval( - energy_score_enabled=False, - native_decoders={ - label: instantiate(decoder) - for label, decoder in cfg.evaluator.native_decoders.items() - }, - semantic_blocks_by_embodiment=OmegaConf.to_container( - cfg.evaluator.semantic_blocks_by_embodiment, resolve=True - ), - energy_score_distances_by_embodiment=distances, - ) - assert set(evaluator.energy_score_distance_metadata["by_embodiment"]) == set( - distances - ) diff --git a/tests/test_action_flow_smoke_schedule.py b/tests/test_action_flow_smoke_schedule.py deleted file mode 100644 index dd9d53dac6..0000000000 --- a/tests/test_action_flow_smoke_schedule.py +++ /dev/null @@ -1,336 +0,0 @@ -"""Regressions for the actual Phoenix 12935484/12935488 verifier failures.""" - -from types import SimpleNamespace - -import pytest -import torch -from omegaconf import OmegaConf - -from tests.test_verify_action_flow_training_smoke import MODULE, _checkpoint_payload -from tools.validate_action_flow_config import ( - GRAPH_METHOD, - STOPGRAD_METHOD, - compose_experiment, -) - -# Exact six-key schedules printed by the failed final verifiers. The jobs' -# training steps exited 0; both experiments intentionally train jointly at 0. -OBSERVED_SCHEDULES = ( - ( - "sim/pusht/action_flow_bc_usocket_latent_fm_sg_recon1_s42", - STOPGRAD_METHOD, - { - "joint_objective_begins_at_global_step": 0, - "reconstruction_only_optimizer_steps": 0, - "joint_flow_weight": 1.0, - "joint_reconstruction_weight": 1.0, - "joint_action_velocity_weight": 1.0, - "schema_version": 1, - }, - ), - ( - "sim/pusht/action_flow_bc_usocket_graph_section_s42", - GRAPH_METHOD, - { - "joint_objective_begins_at_global_step": 0, - "reconstruction_only_optimizer_steps": 0, - "joint_flow_weight": 1.0, - "joint_reconstruction_weight": 0.0, - "joint_action_velocity_weight": 1.0, - "schema_version": 1, - }, - ), -) - - -@pytest.mark.parametrize("experiment,method,schedule", OBSERVED_SCHEDULES) -@pytest.mark.parametrize("restored_warmup", (0, 1)) -def test_observed_joint_schedule_and_strict_reload( - tmp_path, monkeypatch, experiment, method, schedule, restored_warmup -): - config = compose_experiment(experiment, overrides=["hydra/launcher=basic"]) - checkpoint_dir = tmp_path / "checkpoints" - checkpoint_dir.mkdir() - payload = _checkpoint_payload() - payload["action_flow_loss_schedule"] = schedule - immutable = checkpoint_dir / "epoch-0-step-2.ckpt" - torch.save(payload, immutable) - (checkpoint_dir / "last.ckpt").symlink_to(immutable.name) - - class Wrapper: - reconstruction_only_warmup_steps = restored_warmup - encoder_e, field_v, decoder_g = object(), object(), object() - model = SimpleNamespace(pipeline=SimpleNamespace(stages=[])) - nets = SimpleNamespace(named_parameters=lambda **kwargs: []) - - @classmethod - def load_from_checkpoint(cls, *args, **kwargs): - assert kwargs["strict"] is True - return cls() - - def parameters(self): - return [SimpleNamespace(numel=lambda: MODULE.EXPECTED_PARAMETER_COUNT)] - - monkeypatch.setattr(MODULE, "ActionFlowModelWrapper", Wrapper) - monkeypatch.setattr(MODULE, "_validate_dimensions_and_modules", lambda *args: None) - monkeypatch.setattr(MODULE, "_validate_gradient_route_manifest", lambda *args: {}) - kwargs = dict( - reconstruction_weight=schedule["joint_reconstruction_weight"], - flow_weight=1.0, - method=method, - config=config, - ) - if restored_warmup: - with pytest.raises(MODULE.SmokeVerificationError, match="strict reload lost"): - MODULE._validate_checkpoint(tmp_path, **kwargs) - else: - result = MODULE._validate_checkpoint(tmp_path, **kwargs) - assert result["loss_schedule"] == schedule - assert result["strict_checkpoint_reload"] == "passed" - - -@pytest.mark.parametrize("key", tuple(OBSERVED_SCHEDULES[0][2])) -def test_every_schedule_field_remains_checked(key): - experiment, _, schedule = OBSERVED_SCHEDULES[0] - changed = {**schedule, key: schedule[key] + 1} - with pytest.raises(MODULE.SmokeVerificationError, match="unexpected.*schedule"): - MODULE._validate_checkpoint_loss_schedule( - changed, - compose_experiment(experiment, overrides=["hydra/launcher=basic"]), - reconstruction_weight=1, - flow_weight=1, - ) - - -@pytest.mark.parametrize("flow_weight", (1.0, 0.01)) -def test_actual_warmup_smoke_still_requires_configured_step_one(flow_weight): - config = compose_experiment( - "sim/pusht/action_flow_bc_usocket_recon10_warmup10k_s42", - overrides=["hydra/launcher=basic"], - ) - # Exercise both verifier weights without reviving the retired flow001 leaf. - OmegaConf.update(config, "model.flow_weight", flow_weight) - # The maintained launcher reduces this experiment's full 10k warmup to one - # update for the real two-update warmup smoke. - OmegaConf.update(config, "model.reconstruction_only_warmup_steps", 1) - schedule = { - "joint_objective_begins_at_global_step": 1, - "reconstruction_only_optimizer_steps": 1, - "joint_flow_weight": flow_weight, - "joint_reconstruction_weight": 10.0, - "joint_action_velocity_weight": 1.0, - "schema_version": 1, - } - assert ( - MODULE._validate_checkpoint_loss_schedule( - schedule, config, reconstruction_weight=10, flow_weight=flow_weight - ) - == 1 - ) - schedule["joint_objective_begins_at_global_step"] = 0 - schedule["reconstruction_only_optimizer_steps"] = 0 - with pytest.raises(MODULE.SmokeVerificationError, match="unexpected.*schedule"): - MODULE._validate_checkpoint_loss_schedule( - schedule, config, reconstruction_weight=10, flow_weight=flow_weight - ) - - -def test_schedule_cannot_be_validated_without_resolved_config(): - with pytest.raises(MODULE.SmokeVerificationError, match="exact config"): - MODULE._validate_checkpoint_loss_schedule( - OBSERVED_SCHEDULES[0][2], None, reconstruction_weight=1, flow_weight=1 - ) - - -def test_joint_checkpoint_owners_reject_codec_and_parameter_aliases(): - from egomimic.pipeline.stages_action_flow import ( - RoutedContentDecoderStage, - RoutedContentEncoderStage, - ) - - def stages_with(encoders, decoders, field): - stages = [None] * 8 - stages[4] = ( - RoutedContentEncoderStage(encoders, route_key="embodiment") - if encoders - else SimpleNamespace(encoder={}) - ) - stages[6] = SimpleNamespace(field=field) - stages[7] = RoutedContentDecoderStage(decoders, route_key="embodiment") - return stages - - names = ("libero_panda", "libero_panda_sharpa") - encoders = {name: torch.nn.Linear(2, 2) for name in names} - decoders = {name: torch.nn.Linear(2, 2) for name in names} - field = torch.nn.Linear(2, 2) - assert ( - len(MODULE._joint_checkpoint_owners(stages_with(encoders, decoders, field))) - == 5 - ) - decoders[names[0]].weight = encoders[names[0]].weight - with pytest.raises(MODULE.SmokeVerificationError, match="share parameters"): - MODULE._joint_checkpoint_owners(stages_with(encoders, decoders, field)) - decoders[names[0]] = encoders[names[0]] - with pytest.raises(MODULE.SmokeVerificationError, match="aliased"): - MODULE._joint_checkpoint_owners(stages_with(encoders, decoders, field)) - with pytest.raises(MODULE.SmokeVerificationError, match="coverage"): - MODULE._joint_checkpoint_owners(stages_with({}, decoders, field)) - - -def test_joint_smoke_uses_unique_immutable_step_eight_without_rolling_last(tmp_path): - checkpoint = tmp_path / "epoch-0000-step-000000008.ckpt" - with pytest.raises(MODULE.SmokeVerificationError, match="one immutable"): - MODULE._joint_smoke_checkpoint_path(tmp_path) - checkpoint.write_bytes(b"path-contract-only") - assert MODULE._joint_smoke_checkpoint_path(tmp_path) == checkpoint - duplicate = tmp_path / "epoch-0001-step-000000008.ckpt" - duplicate.write_bytes(b"path-contract-only") - with pytest.raises(MODULE.SmokeVerificationError, match="one immutable"): - MODULE._joint_smoke_checkpoint_path(tmp_path) - duplicate.unlink() - (tmp_path / "last.ckpt").symlink_to(checkpoint.name) - with pytest.raises(MODULE.SmokeVerificationError, match="rolling"): - MODULE._joint_smoke_checkpoint_path(tmp_path) - - -def test_joint_history_requires_both_source_rows_at_step_seven(): - from tests.test_verify_action_flow_training_smoke import _history_row - - row = _history_row() - for key, value in list(row.items()): - if MODULE.SOURCE_LABEL in key: - for label in ("libero_panda", "libero_panda_sharpa"): - row[key.replace(MODULE.SOURCE_LABEL, label)] = value - row["Train/ActionFlow/Compute/FieldForwardCallsPerStep"] = 2.0 - row["Train/ActionFlow/Compute/FieldSampleEquivalentsPerStep"] = 28.0 - for prefix in ( - "GradientCosine", - "GradientCosineDefined", - "GradientIntersectionParameterCount", - ): - row[f"Train/ActionFlow/{prefix}/FM__Reconstruction"] = 0.0 - row["Optimizer/LR/AdamW"] = row["Optimizer/LR/Muon"] = 2.5e-8 - options = dict( - method=MODULE.STOPGRAD_UNITE_METHOD, - source_labels=("libero_panda", "libero_panda_sharpa"), - minimum_step=7, - ) - assert MODULE._validate_history({7: row}, **options)["valid_step"] == 7 - with pytest.raises(MODULE.SmokeVerificationError, match="gradient telemetry"): - MODULE._validate_history({1: row}, **options) - del row["Train/MSE/libero_panda_sharpa"] - with pytest.raises(MODULE.SmokeVerificationError, match="gradient telemetry"): - MODULE._validate_history({7: row}, **options) - - -def test_joint_ema_reload_updates_every_alias_and_rejects_wrong_updates(): - from scripts.train.verify_libero_native_action_flow_smoke import ( - validate_joint_ema_reload, - ) - - model = torch.nn.Module() - model.model = torch.nn.Sequential(torch.nn.Linear(3, 2)) - model.nets = model.model - # The native normalizer rebuilds its ParameterDict during strict reload. - from egomimic.models.oat.model.common.dict_of_tensor_mixin import DictOfTensorMixin - - model.model.add_module( - "normalizer", - DictOfTensorMixin( - torch.nn.ParameterDict( - {"scale": torch.nn.Parameter(torch.ones(2), requires_grad=False)} - ) - ), - ) - settings = dict( - decay=0.9978, - use_warmup=False, - update_after_step=0, - inv_gamma=1.0, - power=0.75, - min_decay=0.0, - ) - cfg = dict(callbacks=dict(ema=settings)) - ema = { - name: torch.full_like(parameter, 7) - for name, parameter in model.named_parameters() - } - payload = dict( - global_step=8, ema_num_updates=8, ema_config=settings, ema_state_dict=ema - ) - result = validate_joint_ema_reload(model, payload, cfg) - assert result["state_parameter_aliases"] > result["canonical_parameters"] - assert all( - torch.equal(value, torch.full_like(value, 7)) - for value in model.state_dict().values() - ) - payload["ema_num_updates"] = 7 - with pytest.raises(ValueError, match="update count"): - validate_joint_ema_reload(model, payload, cfg) - payload["ema_num_updates"] = 8 - ema[next(iter(ema))].fill_(float("nan")) - with pytest.raises(ValueError, match="EMA tensor differs"): - validate_joint_ema_reload(model, payload, cfg) - - -def test_joint_smoke_cli_dispatch_is_explicit_and_requires_all_hashes( - tmp_path, monkeypatch -): - from scripts.train import verify_libero_native_action_flow_smoke as native - - experiment = "sim/libero/action_flow_libero_spatial_sharpa_gripper_keypoints_s42" - args = MODULE._parser().parse_args( - [str(tmp_path), "--experiment", experiment, "--expected-head", "a" * 40] - ) - assert args.experiment == experiment - # Missing identities must fail before source inspection, checkpoint loads or API reads. - with pytest.raises(ValueError, match="every explicit expected SHA"): - MODULE.verify_smoke(tmp_path, experiment=experiment, expected_head="a" * 40) - captured = {} - - def routed(**kwargs): - captured.update(kwargs) - return {"dispatch_only": True} - - monkeypatch.setattr(native, "verify_joint_smoke", routed) - assert MODULE.verify_smoke( - tmp_path, - experiment=experiment, - expected_head="a" * 40, - expected_config_sha256="b" * 64, - expected_split_sha256="c" * 64, - expected_normalization_sha256="d" * 64, - expected_preflight_sha256="e" * 64, - ) == {"dispatch_only": True} - assert captured["expected_preflight_sha256"] == "e" * 64 - assert captured["run_dir"] == tmp_path - - -@pytest.mark.parametrize("transport", [None, "slurm-steps", "trainer-pidfd"]) -def test_joint_shared_child_rejects_unbound_signal_transport(transport): - import os - import subprocess - from pathlib import Path - - env = { - key: value - for key, value in os.environ.items() - if not key.startswith(("AF_", "ICE_")) - } - env["AF_EXPERIMENT"] = ( - "sim/libero/action_flow_libero_spatial_sharpa_gripper_keypoints_s42" - ) - if transport is not None: - env["AF_CHECKPOINT_TRANSPORT"] = transport - child = ( - Path(__file__).resolve().parents[1] / "scripts/train/action_flow_run_child.sh" - ) - result = subprocess.run( - ["bash", str(child)], env=env, capture_output=True, text=True - ) - assert result.returncode == 2 - assert ( - "requires audited trainer-pidfd" - if transport != "trainer-pidfd" - else "PIDfd binding missing" - ) in result.stderr diff --git a/tests/test_alignment_parity.py b/tests/test_alignment_parity.py deleted file mode 100644 index b76a4be95c..0000000000 --- a/tests/test_alignment_parity.py +++ /dev/null @@ -1,189 +0,0 @@ -"""Numerical parity with immutable source methods and configured graph training.""" - -from copy import deepcopy -from functools import partial - -import pytest -import torch -from hydra import compose, initialize_config_dir -from hydra.utils import instantiate - -from egomimic.models.cores.hpt_transformer import MultiheadAttention, SimpleTransformer -from egomimic.pipeline.core import Pipeline -from egomimic.pipeline.stages_alignment import ( - RepresentationAlignment, - ScheduledLossReduction, -) -from egomimic.pipeline.stages_hpt import HPTTrunkStage -from tests.test_retained_recipe_steps import CONFIGS, small_cpu_model - -pytest.importorskip("geomloss") -SoftDTWLossPyTorch = pytest.importorskip("tslearn.metrics").SoftDTWLossPyTorch -LegacyAlignment = pytest.importorskip( - "tests.fixtures.legacy_alignment_reference" -).LegacyAlignment - - -@pytest.mark.parametrize("supervision", [None, "mse", "soft_dtw"]) -def test_sinkhorn_loss_and_gradients_match_source(supervision): - torch.manual_seed(121) - left = torch.randn(3, 4, 8, requires_grad=True) - right = torch.randn(3, 4, 8, requires_grad=True) - actions = torch.randn(3, 5, 6) - other_actions = actions.flip(0) + 0.01 - reference = LegacyAlignment() - reference.device = torch.device("cpu") - reference.ot_6dof = False - reference.use_dtw = supervision == "soft_dtw" - reference.dtw = SoftDTWLossPyTorch(gamma=0.1) - expected, expected_distance = reference.compute_ot( - left, right, actions, other_actions, supervision is not None, 0.5 - ) - expected_grads = torch.autograd.grad(expected, (left, right)) - stage = RepresentationAlignment( - "left", - "right", - supervision=supervision, - cost_layout="legacy_broadcast", - left_actions="actions", - right_actions="other_actions", - left_action_indices=[0, 1, 2], - right_action_indices=[0, 1, 2], - ) - result = stage( - { - "left": left, - "right": right, - "actions": actions, - "other_actions": other_actions, - } - ) - torch.testing.assert_close(result["alignment/loss"], expected) - torch.testing.assert_close(result["log/feature_distance"], expected_distance) - for actual, expected_gradient in zip( - torch.autograd.grad(result["alignment/loss"], (left, right)), expected_grads - ): - torch.testing.assert_close(actual, expected_gradient) - - -def test_freeze_boundary_preserves_alignment_gradients(): - torch.manual_seed(42) - trunk = SimpleTransformer( - partial(MultiheadAttention, embed_dim=8, num_heads=2, batch_first=True), - embed_dim=8, - num_blocks=3, - ) - stage = HPTTrunkStage( - trunk, - 8, - action_token_count=2, - squeeze_action_token=False, - representation_block=1, - detach_condition_before_block=2, - ) - result = stage({"hpt/tokens": torch.randn(2, 3, 8)}) - result["condition"].square().sum().backward(retain_graph=True) - assert trunk.blocks[0].attn.in_proj_weight.grad is None - assert trunk.blocks[1].attn.in_proj_weight.grad is None - assert trunk.blocks[2].attn.in_proj_weight.grad.abs().sum() > 0 - stage.zero_grad() - result["hpt/representation"].square().sum().backward() - assert trunk.blocks[0].attn.in_proj_weight.grad.abs().sum() > 0 - assert trunk.blocks[1].attn.in_proj_weight.grad.abs().sum() > 0 - assert trunk.blocks[2].attn.in_proj_weight.grad is None - # The resumed path is the source implementation, with unchanged weights. - _, blocks = trunk(torch.randn(2, 5, 8)) - reference = LegacyAlignment() - reference.trunk = {"trunk": trunk} - reference.postprocess_tokens = lambda value: value - torch.testing.assert_close( - trunk.resume_from_block(blocks, 2), reference.resume_from_depth(blocks, 2) - ) - assert "hpt/representation" not in stage.execute( - {"hpt/tokens": torch.randn(2, 3, 8)}, mode="inference" - ) - - -def test_loss_schedule_and_resume_use_training_batches(): - def graph(): - return Pipeline( - [ - ScheduledLossReduction( - { - "source_losses": {"start_batch": 2}, - "alignment": {"weight": 0.7, "start_batch": 3}, - }, - denominator_key="source_count", - ) - ] - ) - - reducer = graph() - batch = { - "source_losses": torch.tensor([2.0, 4.0]), - "alignment": torch.tensor(10.0), - "source_count": 2, - } - assert reducer(batch)["loss/total"] == 0 - assert reducer(batch)["loss/total"] == 3 - restored = graph() - restored.load_state_dict(deepcopy(reducer.state_dict()), strict=True) - assert restored(batch)["loss/total"] == pytest.approx(6.5) - assert restored(batch)["log/training_batches"] == 4 - - -def test_egobridge_two_source_optimizer_and_independent_feature_passes(): - with initialize_config_dir(version_base=None, config_dir=str(CONFIGS)): - config = compose( - config_name="train_zarr_cartesian", overrides=["model=egobridge"] - ) - cpu = small_cpu_model(config, "hpt") - cpu.model.pipeline.stages[1].representation_block = 0 - cpu.model.pipeline.loss_pipeline.stages[0].supervision = "mse" - model = instantiate(cpu.model.pipeline, device="cpu") - calls = [] - hook = model.pipeline.stage_by_id("stems").register_forward_hook( - lambda *args: calls.append(1) - ) - batch = {} - for name, identity, width in [("eva_bimanual", 6, 14), ("human_bimanual", 3, 12)]: - batch[name] = { - "embodiment": torch.tensor([identity, identity]), - "actions_cartesian": torch.randn(2, 100, width), - "observations.state.ee_pose": torch.randn(2, width), - **{ - f"observations.images.{key}": torch.rand(2, 3, 32, 32) - for key in ("front_img_1", "left_wrist_img", "right_wrist_img") - }, - } - optimizer = torch.optim.Adam(model.nets.parameters(), lr=1e-4) - for _ in range(2): - optimizer.zero_grad() - predictions = model.forward_training(batch) - losses = model.compute_losses(predictions, batch) - expected = ( - losses["source_0_loss"] - + losses["source_1_loss"] - + 0.7 * losses["log_alignment_loss"] - ) / 2 - torch.testing.assert_close(losses["loss"], expected) - losses["loss"].backward() - assert model.pipeline.stage_by_id("trunk").action_token.grad.abs().sum() > 0 - optimizer.step() - hook.remove() - assert ( - len(calls) == 8 - ), "Two sources, two steps, independent BC and OT feature passes" - clone = instantiate(cpu.model.pipeline, device="cpu") - clone.nets.load_state_dict(deepcopy(model.nets.state_dict()), strict=True) - assert clone.nets["loss_pipeline"].stages[1].training_batches == 2 - clone.nets.eval() - assert clone.forward_eval({"human_bimanual": batch["human_bimanual"]})[ - "human_bimanual" - ]["pred_action"].shape == (2, 100, 12) - - -def test_alignment_rejects_unmatched_batches(): - stage = RepresentationAlignment("left", "right") - with pytest.raises(ValueError, match="matching"): - stage({"left": torch.zeros(2, 3), "right": torch.zeros(3, 3)}) diff --git a/tests/test_arc_tsne.py b/tests/test_arc_tsne.py deleted file mode 100644 index 89092da7b9..0000000000 --- a/tests/test_arc_tsne.py +++ /dev/null @@ -1,295 +0,0 @@ -import json - -import numpy as np -import pytest - -from egomimic.scripts.data_visualization.arc_tsne import ( - TokenizeBimanualArcLengthCartesian, - arc_features, - baseline_features, - baseline_leaf, - collect, - load_data, - project, - write_report, -) - - -def raw_chunk(frame=0): - t = np.arange(201) / 30 - raw = np.zeros((len(t), 14)) - raw[:, 0] = t * (0.1 + frame * 0.003) - raw[:, 7] = t * (0.07 + frame * 0.001) - raw[:, 1] = np.sin(t + frame) * 0.04 - raw[:, 10] = t * (0.03 + frame * 0.001) - return raw - - -def codec(**kwargs): - return TokenizeBimanualArcLengthCartesian( - min_distance_unit=0.4, - rotation_distance_unit=0.3, - resampled_vector_length=28, - **kwargs, - ) - - -@pytest.mark.parametrize("mode", ["velocity", "duration"]) -@pytest.mark.parametrize("streams", ["full_separation", "translation_rotation"]) -def test_clock_removal_and_layout_parity(mode, streams): - c = codec(clock_mode=mode, streams=streams) - token = c.transform({"actions_cartesian": raw_chunk()})["actions_cartesian"] - geometry = arc_features(token, c, False) - assert geometry.shape == (28 * 14,) - mutated = token.copy() - mutated[:, 14:] += 55 - np.testing.assert_array_equal(geometry, arc_features(mutated, c, False)) - assert not np.array_equal(arc_features(token, c), arc_features(mutated, c)) - - -class Leaf: - def __init__(self): - self.transform = [codec(clock_mode="velocity")] - self.metadata = {"task": "fold "} - - def __len__(self): - return 18 - - def __getitem__(self, frame): - data = {"actions_cartesian": raw_chunk(frame), "frame_index": frame} - for transform in self.transform: - data = transform.transform(data) - return data - - -def test_paired_sampling_and_report(tmp_path): - leaf = Leaf() - original = leaf.transform.copy() - first, records, codecs = collect({"one": leaf}, samples_per_episode=8) - second, records2, _ = collect({"one": leaf}, samples_per_episode=8) - assert records == records2 - assert leaf.transform == original - assert codecs[0]["tokenizer_config"]["min_distance_unit"] == 0.4 - for k in first: - np.testing.assert_array_equal(first[k], second[k]) - for i, record in enumerate(records): - np.testing.assert_array_equal( - first["baseline"][i], raw_chunk(record["frame"])[:100].ravel() - ) - xy, diag = project(first) - xy2, _ = project(first) - assert xy == xy2 - assert all(np.asarray(v).shape == (8, 2) for v in xy.values()) - output = tmp_path / "report" - write_report(output, first, records, xy, {"projections": diag}) - html = (output / "index.html").read_text() - assert "fold " not in html - assert "__REPORT_DATA__" not in html - assert "https://" not in html - assert json.loads((output / "report.json").read_text())["records"] == records - assert set(np.load(output / "features.npz").files) == set(first) - with pytest.raises(FileExistsError): - write_report(output, first, records, xy, {}) - - -def test_baseline_padding_and_bad_values(): - raw = raw_chunk()[:2] - padded = baseline_features(raw, 4).reshape(4, 14) - np.testing.assert_array_equal(padded[2:], np.repeat(raw[-1:], 2, axis=0)) - with pytest.raises(ValueError, match="identical"): - project({"bad": np.ones((4, 12))}) - with pytest.raises(ValueError, match="Nonfinite"): - baseline_features(np.full((3, 14), np.nan)) - - -def test_baseline_window_does_not_inherit_distance_cutoff(): - leaf = Leaf() - leaf.key_map = { - "actions": {"horizon": {"distance": 0.4}, "key_type": "action_keys"}, - "obs": {"horizon": 1, "key_type": "proprio_keys"}, - } - time = baseline_leaf(leaf, leaf.transform, 0, 100) - assert time.key_map["actions"]["horizon"] == 100 - assert time.key_map["obs"]["horizon"] == 1 - assert leaf.key_map["actions"]["horizon"] == {"distance": 0.4} - assert time.transform == [] - assert len(leaf.transform) == 1 - - -@pytest.mark.parametrize("embodiment", ["human_bimanual", "yam_bimanual"]) -def test_rl2_inventory_configs_preserve_native_source(embodiment): - from pathlib import Path - - from hydra.utils import instantiate - from omegaconf import OmegaConf - - from egomimic.rldb.zarr.action_chunk_transforms import ( - InterpolateLinear, - InterpolatePose, - ) - - path = ( - Path(__file__).parents[1] - / "egomimic/hydra_configs/visualization" - / f"rl2_organize_{embodiment}.yaml" - ) - cfg = OmegaConf.load(path) - assert cfg.data.valid_ratio == 0.05 - key_map = instantiate(cfg.data.key_map) - assert not any(v.get("key_type") == "camera_keys" for v in key_map.values()) - transforms = instantiate(cfg.data.transform_list) - for transform in transforms: - if isinstance(transform, (InterpolatePose, InterpolateLinear)): - assert transform.new_chunk_length is None - tokenizer = next( - t for t in transforms if isinstance(t, TokenizeBimanualArcLengthCartesian) - ) - assert tokenizer.M == 100 - assert tokenizer.clock_mode == "duration" - assert tokenizer.streams == "translation_rotation" - assert tokenizer.tokenizer.config.min_distance_unit == 0.42 - assert "elmo" in str(cfg.data.filters.filter_lambdas[0]) - - -def test_clock_layout_features_separate_waypoints_from_clocks(): - from egomimic.rldb.zarr.arc_length_tokenizer import CLOCK_COLUMNS - from egomimic.scripts.data_visualization.arc_tsne import arc_features - - class Codec: - M, clock_mode, streams = 4, "duration", "translation_rotation" - - token = np.arange(4 * 18, dtype=float).reshape(4, 18) + 1 - with_clock = arc_features(token, Codec, True).reshape(-1, 14) - without = arc_features(token, Codec, False).reshape(-1, 14) - assert np.array_equal(without, token[:, :14]) - assert np.array_equal(with_clock[4:, list(CLOCK_COLUMNS)], token[:, 14:]) - - -def test_joint_fit_drops_gripper_and_scores_mixing(): - from egomimic.scripts.data_visualization.arc_tsne import ( - drop_gripper, - mixing, - project, - ) - - rng = np.random.default_rng(0) - human = rng.normal(size=(40, 2 * 14)) - robot = rng.normal(size=(40, 2 * 14)) - human.reshape(40, 2, 14)[:, :, [6, 13]] = 0.0 - robot.reshape(40, 2, 14)[:, :, [6, 13]] = 5.0 + rng.normal(size=(40, 2, 2)) - dropped = drop_gripper({"baseline": np.concatenate([human, robot])})["baseline"] - assert dropped.shape == (80, 2 * 12) - groups = ["human"] * 40 + ["robot"] * 40 - _, mixed = mixing(dropped, groups) - _, split = mixing(np.concatenate([human, robot]), groups) - assert mixed > 0.7 and split < 0.2 - xy, diagnostics, neighbors = project({"baseline": dropped}, groups=groups) - assert len(xy["baseline"]) == 80 and len(neighbors["baseline"][0]) == 8 - assert diagnostics["baseline"]["mixing"] > 0.7 - - -def test_inventory_sampling_uses_full_split(): - from egomimic.scripts.data_visualization.arc_tsne_inventory import ( - select_episode_ids, - ) - - names = [f"episode_{i:03}" for i in range(100)] - sampled, train, valid = select_episode_ids(names, 0.05, 42, 42, 12) - assert len(valid) == 5 - assert len(train) == 95 - assert len(sampled) == 12 - assert set(sampled) <= set(train) - assert not set(sampled) & set(valid) - assert (sampled, train, valid) == select_episode_ids(names[::-1], 0.05, 42, 42, 12) - - -def test_missing_codec_and_baseline_only(): - leaf = Leaf() - leaf.transform = [] - with pytest.raises(ValueError, match="requires a data config"): - collect({"one": leaf}) - values, _, _ = collect({"one": leaf}, representations="baseline") - assert set(values) == {"baseline"} - - -def test_fallback_is_rejected_and_transforms_restored(): - class BadLeaf(Leaf): - def __getitem__(self, frame): - return super().__getitem__(frame + 1) - - leaf = BadLeaf() - original = leaf.transform.copy() - with pytest.raises(ValueError, match="substituted"): - collect({"one": leaf}) - assert leaf.transform == original - - -def test_real_zarr_config_pipeline(tmp_path): - import zarr - from omegaconf import OmegaConf - - root = tmp_path / "episodes" - for i in range(4): - store = zarr.open_group(str(root / f"episode_{i}.zarr"), mode="w") - raw = raw_chunk(i) - store.create_array("actions", data=raw) - store.attrs.update( - total_frames=len(raw), - embodiment="yam_bimanual", - task="test", - features={"actions": {"dtype": "float64"}}, - ) - target = "egomimic.rldb.zarr." - config = { - "train_datasets": { - "yam": { - "mode": "train", - "valid_ratio": 0.05, - "split_seed": 42, - "resolver": { - "_target_": target + "zarr_dataset_multi.LocalEpisodeResolver", - "folder_path": str(root), - "key_map": { - "actions_cartesian": {"zarr_key": "actions", "horizon": 150} - }, - "transform_list": [ - { - "_target_": target - + "arc_length_tokenizer.TokenizeBimanualArcLengthCartesian", - "rotation_distance_unit": 0.3, - "clock_mode": "velocity", - "resampled_vector_length": 28, - } - ], - }, - } - } - } - config_path = tmp_path / "data.yaml" - OmegaConf.save(OmegaConf.create(config), config_path) - leaves, provenance = load_data(config_path, [], None, "train", False) - assert len(leaves) == 3 # 5% split retains one validation episode. - assert provenance["config"]["valid_ratio"] == 0.05 - values, records, codecs = collect(leaves, samples_per_episode=3) - assert len(records) == 9 - assert values["arc_clock"].shape == (9, 28 * 28) - assert values["arc_no_clock"].shape == (9, 28 * 14) - assert codecs[0]["streams"] == "full_separation" - projections, diagnostics = project(values) - write_report( - tmp_path / "report", - values, - records, - projections, - {**provenance, "projections": diagnostics}, - ) - # The same file is valid under a full resolved training-config data section. - OmegaConf.save(OmegaConf.create({"data": config}), config_path) - full_leaves, _ = load_data(config_path, [], None, "train", False) - assert set(full_leaves) == set(leaves) - config["train_datasets"]["yam"]["resolver"]["_target_"] = ( - target + "zarr_dataset_multi.S3EpisodeResolver" - ) - OmegaConf.save(OmegaConf.create(config), config_path) - with pytest.raises(ValueError, match="Non-local resolver refused"): - load_data(config_path, [], None, "train", False) diff --git a/tests/test_batch_signal_relay.py b/tests/test_batch_signal_relay.py deleted file mode 100644 index 35870aed7f..0000000000 --- a/tests/test_batch_signal_relay.py +++ /dev/null @@ -1,161 +0,0 @@ -"""Real bash/child regression for Slurm batch-shell-only signal delivery.""" - -from __future__ import annotations - -import json -import os -import shlex -import signal -import subprocess -import sys -import time -from pathlib import Path - -import pytest - -ROOT = Path(__file__).parents[1] -HELPER = ROOT / "scripts/ice/relay_batch_signals.sh" -CHILD = r""" -import json, os, pathlib, signal, sys, time -root = pathlib.Path(sys.argv[1]) -root.joinpath("started").write_text(str(os.getpid())) -if sys.argv[2] == "early": - raise SystemExit(int(sys.argv[3])) -time.sleep(float(sys.argv[4])) -def handle(received, frame): - root.joinpath("received").write_text(signal.Signals(received).name) - if received == signal.SIGUSR1 and sys.argv[2] != "boundary": - root.joinpath("COMPLETE.json").write_text("complete") - time.sleep(.05) - raise SystemExit(0 if received == signal.SIGUSR1 else 128 + received) -for received in (signal.SIGUSR1, signal.SIGTERM, signal.SIGINT): - signal.signal(received, handle) -pathlib.Path(os.environ["ICE_RUNNER_SIGNAL_READY_FILE"]).write_text(str(os.getpid())) -if sys.argv[2] == "normal": - raise SystemExit(int(sys.argv[3])) -time.sleep(5) -""" - - -def wait_file(path): - deadline = time.monotonic() + 3 - while time.monotonic() < deadline: - if path.exists() and path.stat().st_size: - return - time.sleep(0.01) - raise AssertionError(f"child readiness timeout: {path}") - - -def launch(root, mode="signal", exit_code=0, delay=0): - argv = [sys.executable, "-c", CHILD, str(root), mode, str(exit_code), str(delay)] - command = ( - f"set -euo pipefail; source {shlex.quote(str(HELPER))}; " - f"ice_run_with_signal_relay {shlex.quote(str(root / 'ready'))} " - f"{shlex.join(argv)}; " - f'if test "$ICE_BATCH_BOUNDARY_FORWARDED" = 1 && test ! -s {shlex.quote(str(root / "COMPLETE.json"))}; ' - "then printf 'REQUEUE_ACCEPTED\\n'; exit 0; fi; " - "printf 'POST_RUNNER_VERIFIER\\n'" - ) - return subprocess.Popen( - ["bash", "-c", command], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - - -def assert_reaped(root): - pid = int((root / "started").read_text()) - with pytest.raises(ProcessLookupError): - os.kill(pid, 0) - - -@pytest.mark.parametrize("received", [signal.SIGUSR1, signal.SIGTERM, signal.SIGINT]) -@pytest.mark.parametrize("during_startup", [False, True]) -def test_exact_signal_reaches_child_and_wait_preserves_status( - tmp_path, received, during_startup -): - child = launch(tmp_path, delay=0.2 if during_startup else 0) - try: - wait_file(tmp_path / ("started" if during_startup else "ready")) - child.send_signal(received) - output, errors = child.communicate(timeout=4) - expected = 0 if received == signal.SIGUSR1 else 128 + received - assert child.returncode == expected, (output, errors) - assert (tmp_path / "received").read_text() == received.name - assert ("POST_RUNNER_VERIFIER" in output) == (expected == 0) - assert_reaped(tmp_path) - finally: - if child.poll() is None: - child.kill() - child.wait(timeout=3) - - -@pytest.mark.parametrize("mode", ["normal", "early"]) -@pytest.mark.parametrize("exit_code", [0, 7]) -def test_early_and_normal_exit_status_is_not_replaced_by_wait( - tmp_path, mode, exit_code -): - child = launch(tmp_path, mode=mode, exit_code=exit_code) - output, errors = child.communicate(timeout=4) - assert child.returncode == exit_code, (output, errors) - assert ("POST_RUNNER_VERIFIER" in output) == (exit_code == 0) - assert_reaped(tmp_path) - - -def test_existing_readiness_marker_refuses_before_launch(tmp_path): - (tmp_path / "ready").write_text("stale") - child = launch(tmp_path) - output, errors = child.communicate(timeout=3) - assert child.returncode == 73, (output, errors) - assert not (tmp_path / "started").exists() - assert "POST_RUNNER_VERIFIER" not in output - - -def test_successful_requeue_without_completion_does_not_run_final_verifier(tmp_path): - child = launch(tmp_path, mode="boundary") - wait_file(tmp_path / "ready") - child.send_signal(signal.SIGUSR1) - output, errors = child.communicate(timeout=4) - assert child.returncode == 0, (output, errors) - assert "REQUEUE_ACCEPTED" in output - assert "POST_RUNNER_VERIFIER" not in output - assert_reaped(tmp_path) - - -def test_actual_runner_publishes_readiness_before_command_validation(tmp_path): - ready = tmp_path / "ready.json" - environment = {**os.environ, "ICE_RUNNER_SIGNAL_READY_FILE": str(ready)} - result = subprocess.run( - [ - sys.executable, - str(ROOT / "scripts/ice/ice_requeue_runner.py"), - "--state-dir", - str(tmp_path / "state"), - "--checkpoint-glob", - str(tmp_path / "*.ckpt"), - "--checkpoint-signal", - "USR2", - "--checkpoint-forwarding", - "slurm-steps", - "--requeue-owner", - "runner", - ], - env=environment, - capture_output=True, - text=True, - timeout=3, - ) - assert result.returncode != 0 - payload = json.loads(ready.read_text()) - assert payload["signal_handlers_ready"] is True - assert payload["pid"] > 0 - launcher = (ROOT / "scripts/train/launch_action_flow_usocket.sbatch").read_text() - assert ( - "AF_SIGNAL_RELAY=${AF_SIGNAL_RELAY:-$AF_TOOL_DIR/relay_batch_signals.sh}" - in launcher - ) - assert 'source "$AF_REPO/scripts/train/action_flow_run_child.sh"' in launcher - child = (ROOT / "scripts/train/action_flow_run_child.sh").read_text() - assert 'source "$AF_SIGNAL_RELAY"' in child - assert 'ice_run_with_signal_relay "$ATTEMPT/runner-signal-ready.json"' in child diff --git a/tests/test_c12_compatibility.py b/tests/test_c12_compatibility.py deleted file mode 100644 index 6f1b0e9cfb..0000000000 --- a/tests/test_c12_compatibility.py +++ /dev/null @@ -1,224 +0,0 @@ -"""Historical behavior contracts, not a claim of full training equivalence.""" - -import ast -import os -import random -from pathlib import Path - -import numpy as np -import pytest -import torch - -from egomimic.pipeline.algo import PipelineAlgo -from egomimic.pl_utils.pl_data_utils import MultiDataModuleWrapper -from egomimic.rldb.zarr import zarr_dataset_multi as dm -from tests.test_bounds_check_gate import _dataset - - -def module(mode): - return MultiDataModuleWrapper({}, {}, {}, {}, compatibility_mode=mode) - - -def test_invalid_mode_fails_closed(): - for make in ( - module, - lambda mode: PipelineAlgo([], device="cpu", compatibility_mode=mode), - lambda mode: dm.MultiDataset(state={}, compatibility_mode=mode), - ): - with pytest.raises(ValueError, match="compatibility_mode"): - make("typo") - - -def test_bounds_false_is_explicitly_overridden_only_in_legacy_mode(): - ds = dm.MultiDataset(state={}, bounds_check=False, compatibility_mode="legacy_c12") - assert ds.bounds_check is True - assert ds.bounds_semantics == "legacy_full_vector" - assert ds.fallback_policy == "legacy_random" - assert dm.MultiDataset(state={}, bounds_check=False).bounds_check is False - - -@pytest.mark.parametrize("bad_value", [1e6, float("nan"), float("inf"), -1e-7]) -def test_legacy_filter_rejects_nonfinite_and_strict_outliers(bad_value): - ds, leaf = _dataset(bounds_check=False, bad_value=bad_value) - # Constructor-derived settings, transplanted onto a tiny normalized fixture. - settings = dm.MultiDataset( - state={}, bounds_check=False, compatibility_mode="legacy_c12" - ) - for key in ( - "bounds_check", - "bounds_semantics", - "fallback_policy", - "compatibility_mode", - ): - setattr(ds, key, getattr(settings, key)) - ds[1] - assert leaf.served == [1, 0] - - -def test_loader_matches_default_shuffle_worker_seed_and_global_rng(): - legacy = module("legacy_c12") - for workers in (0, 2): - results = [] - for generator in (None, legacy._loader_generator("train", "source")): - torch.manual_seed(42) - loader = torch.utils.data.DataLoader( - torch.arange(64), - batch_size=8, - shuffle=True, - num_workers=workers, - generator=generator, - ) - iterator = iter(loader) - worker_seed = iterator._base_seed - batches = torch.cat(list(iterator)) - results.append((worker_seed, batches, torch.get_rng_state().clone())) - assert results[0][0] == results[1][0] - assert torch.equal(results[0][1], results[1][1]) - assert torch.equal(results[0][2], results[1][2]) - assert not legacy._loader_generators - - -def test_checkpoint_cannot_silently_change_loader_semantics(): - for left, right in (("current", "legacy_c12"), ("legacy_c12", "current")): - with pytest.raises(ValueError, match="compatibility mode"): - module(right).load_state_dict(module(left).state_dict()) - legacy = module("legacy_c12") - legacy.load_state_dict(legacy.state_dict()) - - -def test_input_dtype_preserved_recursively_only_in_legacy_mode(): - value = torch.tensor([1.0000000001], dtype=torch.float64) - for mode, expected in (("current", torch.float32), ("legacy_c12", torch.float64)): - algo = PipelineAlgo([], device="cpu", compatibility_mode=mode) - batch = algo._move_value({"a": [value, (value,)], "i": torch.tensor([1])}) - assert batch["a"][0].dtype == expected - assert batch["a"][1][0].dtype == expected - assert batch["i"].dtype == torch.int64 - if mode == "legacy_c12": - assert torch.equal(batch["a"][0], value) - - -def test_retry_against_pinned_historical_source(): - path = os.environ.get("C12_DATASET_SOURCE") - if not path: - pytest.skip("set C12_DATASET_SOURCE to the pinned historical dataset module") - tree = ast.parse(Path(path).read_text()) - helper = next( - n - for n in tree.body - if isinstance(n, ast.FunctionDef) and n.name == "get_fallback_idx" - ) - cls = next( - n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == "MultiDataset" - ) - method = next( - n - for n in cls.body - if isinstance(n, ast.FunctionDef) and n.name == "_next_after_failure" - ) - ns = dict(vars(dm)) - exec( - compile(ast.Module(body=[helper, method], type_ignores=[]), str(path), "exec"), - ns, - ) - ds = dm.MultiDataset(state={}, compatibility_mode="legacy_c12") - ds.index_map = [("a", i) for i in range(64)] + [("b", i) for i in range(4)] - ds._global_indices_by_dataset = {"a": list(range(64)), "b": list(range(64, 68))} - original_rng = random.getstate() - try: - for seed in (0, 42, 999): - for attempt in (None, 0, 24, 25, 26, 62, 63, 999): - results = [] - for method in ( - ns["_next_after_failure"], - dm.MultiDataset._next_after_failure, - ): - random.seed(seed) - try: - result = method(ds, 0, "a", attempt, reason="fixture") - except RuntimeError as exc: - result = (type(exc).__name__, str(exc)) - results.append((result, random.getstate())) - assert results[0] == results[1], (seed, attempt) - finally: - random.setstate(original_rng) - - -@pytest.mark.parametrize("dtype", [torch.float32, torch.float64, torch.bfloat16]) -def test_metric_dtype_matches_historical_reducer_without_changing_gradient(dtype): - from collections import OrderedDict - - from egomimic.pl_utils.training_behavior_action_flow import ( - ActionFlowTrainingBehavior, - ) - from egomimic.pl_utils.training_metrics import reduce_component_means - - path = os.environ.get("C12_DATASET_SOURCE") - if not path: - pytest.skip("set C12_DATASET_SOURCE to the pinned historical dataset module") - source = Path(path).parents[2] / "pl_utils/pl_model_action_flow.py" - tree = ast.parse(source.read_text()) - cls = next( - n - for n in tree.body - if isinstance(n, ast.ClassDef) and n.name == "ActionFlowModelWrapper" - ) - method = next( - n - for n in cls.body - if isinstance(n, ast.FunctionDef) and n.name == "_reduce_component_means" - ) - method.decorator_list = [] - ns = {"torch": torch, "OrderedDict": OrderedDict, "Mapping": dict} - exec(compile(ast.Module(body=[method], type_ignores=[]), str(source), "exec"), ns) - value = torch.tensor(0.123456789, dtype=dtype, requires_grad=True) - state = torch.get_rng_state().clone() - expected, count = ns["_reduce_component_means"]({"loss": value}, 17) - actual, actual_count = reduce_component_means( - {"loss": value}, 17, label="fixture", preserve_input_dtype=True - ) - assert count == actual_count - assert actual["loss"].dtype == expected["loss"].dtype - assert torch.equal(actual["loss"], expected["loss"]) - assert value.grad is None and not actual["loss"].requires_grad - assert torch.equal(state, torch.get_rng_state()) - assert ( - ActionFlowTrainingBehavior(compatibility_mode="legacy_c12").compatibility_mode - == "legacy_c12" - ) - with pytest.raises(ValueError, match="compatibility_mode"): - ActionFlowTrainingBehavior(compatibility_mode="invalid") - - -def test_normalization_collection_matches_historical_precision(): - path = os.environ.get("C12_DATASET_SOURCE") - if not path: - pytest.skip("set C12_DATASET_SOURCE to the pinned historical dataset module") - tree = ast.parse(Path(path).read_text()) - cls = next( - n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == "MultiDataset" - ) - method = next( - n - for n in cls.body - if isinstance(n, ast.FunctionDef) and n.name == "_collect_norm_samples" - ) - ns = dict(vars(dm)) - exec(compile(ast.Module(body=[method], type_ignores=[]), str(path), "exec"), ns) - for dtype in (torch.float32, torch.float64): - samples = torch.tensor( - [[1.0000000001], [1.0000000003], [1.0000000009]], dtype=dtype - ) - ds = dm.MultiDataset(state={}, compatibility_mode="legacy_c12") - ds.keyname_to_zarr_key = lambda key, embodiment: key - args = ([{"action": samples}], ["action"], "fixture", 2, 3, 0) - expected = ns["_collect_norm_samples"](ds, *args)["action"][0] - actual = ds._collect_norm_samples(*args)["action"][0] - assert actual.dtype == expected.dtype - np.testing.assert_array_equal(actual, expected) - for key, value in ds._compute_stats_for_array(expected).items(): - np.testing.assert_array_equal( - ds._compute_stats_for_array(actual)[key], value - ) - ds.compatibility_mode = "current" - assert ds._collect_norm_samples(*args)["action"][0].dtype == np.float32 diff --git a/tests/test_capture_runtime_lock.py b/tests/test_capture_runtime_lock.py deleted file mode 100644 index 9dc5eec1b7..0000000000 --- a/tests/test_capture_runtime_lock.py +++ /dev/null @@ -1,38 +0,0 @@ -import importlib.util -import json -from pathlib import Path - -import pytest - -MODULE_PATH = Path(__file__).parents[1] / "scripts" / "ice" / "capture_runtime_lock.py" -SPEC = importlib.util.spec_from_file_location("capture_runtime_lock", MODULE_PATH) -assert SPEC is not None and SPEC.loader is not None -MODULE = importlib.util.module_from_spec(SPEC) -SPEC.loader.exec_module(MODULE) - - -def test_canonical_bytes_are_stable_and_newline_terminated(): - first = MODULE.canonical_bytes({"z": 1, "a": [2, 3]}) - second = MODULE.canonical_bytes({"a": [2, 3], "z": 1}) - - assert first == second - assert first.endswith(b"\n") - assert json.loads(first) == {"a": [2, 3], "z": 1} - - -def test_atomic_create_refuses_overwrite(tmp_path): - output = tmp_path / "locks" / "runtime.json" - MODULE.atomic_create(output, b"first\n") - - with pytest.raises(FileExistsError, match="refusing to overwrite"): - MODULE.atomic_create(output, b"second\n") - - assert output.read_bytes() == b"first\n" - - -def test_runtime_lock_source_has_no_deployment_specific_paths(): - text = MODULE_PATH.read_text() - - assert "/coc/" not in text - assert "/storage/" not in text - assert "paphiwetsa3" not in text diff --git a/tests/test_check_checkpoint_storage.py b/tests/test_check_checkpoint_storage.py deleted file mode 100644 index 8aebbea1b1..0000000000 --- a/tests/test_check_checkpoint_storage.py +++ /dev/null @@ -1,150 +0,0 @@ -import importlib.util -import json -import os -import subprocess -import sys -from pathlib import Path -from types import SimpleNamespace - -ROOT = Path(__file__).parents[1] -SCRIPT = ROOT / "scripts" / "train" / "check_checkpoint_storage.py" - - -def _load_module(): - spec = importlib.util.spec_from_file_location("check_checkpoint_storage", SCRIPT) - assert spec is not None and spec.loader is not None - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -def _filesystem(*, available_blocks=1000, free_blocks=1100, read_only=False): - return SimpleNamespace( - f_frsize=4096, - f_bavail=available_blocks, - f_bfree=free_blocks, - f_blocks=2000, - f_flag=int(getattr(os, "ST_RDONLY", 1)) if read_only else 0, - ) - - -def test_storage_budget_uses_larger_measured_or_expected_size(tmp_path, monkeypatch): - module = _load_module() - monkeypatch.setattr(module.os, "statvfs", lambda _: _filesystem()) - report, failures = module.storage_evidence( - tmp_path, - planned_checkpoint_count=3, - measured_checkpoint_bytes=100, - expected_checkpoint_bytes=120, - safety_reserve_bytes=500, - ) - assert failures == [] - assert report["bytes_per_checkpoint_budget"] == 120 - assert report["checkpoint_budget_bytes"] == 360 - assert report["required_available_bytes"] == 860 - assert report["filesystem_available_bytes"] == 1000 * 4096 - - -def test_storage_budget_reports_shortfall_and_read_only(tmp_path, monkeypatch): - module = _load_module() - monkeypatch.setattr( - module.os, - "statvfs", - lambda _: _filesystem(available_blocks=1, free_blocks=2, read_only=True), - ) - report, failures = module.storage_evidence( - tmp_path, - planned_checkpoint_count=3, - measured_checkpoint_bytes=2000, - expected_checkpoint_bytes=None, - safety_reserve_bytes=1000, - ) - assert report["required_available_bytes"] == 7000 - assert {failure["field"] for failure in failures} == { - "filesystem_read_only", - "filesystem_available_bytes", - } - - -def test_storage_cli_writes_pass_evidence(tmp_path): - output = tmp_path / "storage.json" - subprocess.run( - [ - sys.executable, - str(SCRIPT), - "--target", - str(tmp_path), - "--planned-checkpoint-count", - "1", - "--measured-checkpoint-bytes", - "1", - "--expected-checkpoint-bytes", - "2", - "--safety-reserve-bytes", - "1", - "--output", - str(output), - ], - check=True, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - payload = json.loads(output.read_text()) - assert payload["status"] == "CHECKPOINT_STORAGE_VALIDATED" - assert payload["failures"] == [] - assert payload["storage"]["target"] == str(tmp_path.resolve()) - - -def test_storage_cli_writes_failure_evidence_for_insufficient_space(tmp_path): - available = os.statvfs(tmp_path).f_bavail * os.statvfs(tmp_path).f_frsize - output = tmp_path / "storage.json" - completed = subprocess.run( - [ - sys.executable, - str(SCRIPT), - "--target", - str(tmp_path), - "--planned-checkpoint-count", - "2", - "--expected-checkpoint-bytes", - str(available + 1), - "--safety-reserve-bytes", - "1", - "--output", - str(output), - ], - check=False, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - assert completed.returncode == 1 - payload = json.loads(output.read_text()) - assert payload["status"] == "CHECKPOINT_STORAGE_FAILED" - failure = payload["failures"][0] - assert failure["field"] == "filesystem_available_bytes" - assert failure["shortfall_bytes"] > 0 - - -def test_storage_cli_requires_checkpoint_size(tmp_path): - completed = subprocess.run( - [ - sys.executable, - str(SCRIPT), - "--target", - str(tmp_path), - "--planned-checkpoint-count", - "1", - "--safety-reserve-bytes", - "1", - "--output", - str(tmp_path / "storage.json"), - ], - check=False, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - assert completed.returncode != 0 - assert "one or both" in completed.stderr diff --git a/tests/test_config_graph.py b/tests/test_config_graph.py deleted file mode 100644 index 43d7c028d9..0000000000 --- a/tests/test_config_graph.py +++ /dev/null @@ -1,249 +0,0 @@ -from __future__ import annotations - -import importlib.util -import json -import os -import subprocess -import sys -from pathlib import Path - -import pytest - -from egomimic.pipeline.stages_sampler import GaussianLatentNoise - -_TOOL = Path(__file__).resolve().parents[1] / "tools/config_graph.py" -_SPEC = importlib.util.spec_from_file_location("config_graph_under_test", _TOOL) -assert _SPEC is not None and _SPEC.loader is not None -config_graph = importlib.util.module_from_spec(_SPEC) -_SPEC.loader.exec_module(config_graph) - - -def _model_yaml(note: str = "") -> str: - return f""" -_target_: egomimic.pl_utils.pl_model.ModelWrapper -pipeline: - _target_: egomimic.pipeline.algo.PipelineAlgo - stages: - - _target_: egomimic.pipeline.stages_sampler.KeyedFeatureProjection - selector_key: route - input_key: features - output_key: condition - output_dim: 3 - selector_aliases: {{1: image}} - projections: - image: - input_dim: 2 - hidden_dim: 4 - semantic: {json.dumps(note)} - - _target_: egomimic.pipeline.stages_sampler.GaussianLatentNoise - num_tokens: 4 - latent_dim: 3 - - _target_: egomimic.pipeline.stages_io.ActionTargetBuilder -""".lstrip() - - -def _data_yaml(*, second_source_has_route: bool = True) -> str: - second_batch_keys = "[route]" if second_source_has_route else "[]" - return f""" -train_datasets: - opaque/source-A: - batch_keys: [route] - key_map: &keymap - features: {{zarr_key: observations.features, key_type: proprio_keys}} - actions: {{zarr_key: actions, key_type: action_keys}} - "42": - batch_keys: {second_batch_keys} - key_map: - <<: *keymap -""".lstrip() - - -def _write_full_config( - path: Path, *, note: str = "", second_source_has_route: bool = True -) -> None: - model = "\n".join(f" {line}" for line in _model_yaml(note).splitlines()) - data = "\n".join( - f" {line}" - for line in _data_yaml( - second_source_has_route=second_source_has_route - ).splitlines() - ) - path.write_text(f"model:\n{model}\ndata:\n{data}\n") - - -def test_clean_pipeline_contracts_and_nested_params_are_preserved( - tmp_path: Path, monkeypatch -) -> None: - selected = tmp_path / "selected.yaml" - note = "nested-stage-parameter-" * 30 - _write_full_config(selected, note=note) - calls = [] - original = GaussianLatentNoise.contract - - def record_contract(self, mode="train"): - calls.append(mode) - return original(self, mode) - - monkeypatch.setattr(GaussianLatentNoise, "contract", record_contract) - train = config_graph.build_graph(selected, mode="train") - inference = config_graph.build_graph(selected, mode="inference") - - assert calls == ["train", "inference"] - assert train["lint"] == inference["lint"] == [] - assert train["seed_keys_by_source"] == { - "opaque/source-A": ["actions", "features", "route"], - "42": ["actions", "features", "route"], - } - assert inference["seed_keys"] == ["features", "route"] - assert [node["t"] for node in inference["nodes"]] == [ - "KeyedFeatureProjection", - "GaussianLatentNoise", - ] - assert inference["skipped_stages"] == [ - { - "source_i": 2, - "t": "ActionTargetBuilder", - "target": "egomimic.pipeline.stages_io.ActionTargetBuilder", - "reason": "train-only", - } - ] - assert train["edges"] == [{"a": 0, "b": 1, "k": "condition", "s": "shared"}] - params = train["nodes"][0]["p"] - assert params["projections"]["image"]["semantic"] == note - assert len(params["projections"]["image"]["semantic"]) > 400 - - -def test_source_names_are_opaque_and_missing_keymap_inputs_are_linted( - tmp_path: Path, -) -> None: - selected = tmp_path / "missing.yaml" - _write_full_config(selected, second_source_has_route=False) - - graph = config_graph.build_graph(selected) - - assert set(graph["seed_keys_by_source"]) == {"opaque/source-A", "42"} - assert any( - "source '42'" in problem and "reads 'route'" in problem - for problem in graph["lint"] - ) - assert not any( - "source 'opaque/source-A'" in problem and "reads 'route'" in problem - for problem in graph["lint"] - ) - - -def test_hydra_experiment_accepts_exact_model_override(tmp_path: Path) -> None: - root = tmp_path / "hydra_configs" - (root / "model").mkdir(parents=True) - (root / "data").mkdir() - (root / "experiment").mkdir() - (root / "train_zarr_cartesian.yaml").write_text( - "defaults:\n - model: null\n - data: null\n - _self_\n" - ) - (root / "model/selected.yaml").write_text(_model_yaml()) - (root / "data/selected.yaml").write_text(_data_yaml()) - experiment = root / "experiment/selected.yaml" - experiment.write_text( - "# @package _global_\n" - "defaults:\n" - " - override /data: selected\n" - " - _self_\n" - ) - - overrides = ["model=selected", "++run_tag=exact-row"] - graph = config_graph.build_graph(experiment, overrides=overrides) - - assert graph["lint"] == [] - assert graph["seed_key_source"] == "selected-data-keymap" - assert graph["component_sources"]["config_root"] == str(root) - assert graph["component_sources"]["overrides"] == overrides - - -def test_legacy_model_key_is_not_considered(tmp_path: Path) -> None: - selected = tmp_path / "legacy.yaml" - selected.write_text( - "robomimic_model:\n" - " stages:\n" - " - _target_: egomimic.pipeline.stages_io.ActionTargetBuilder\n" - ) - - with pytest.raises(ValueError, match=r"model\.pipeline\.stages"): - config_graph.build_graph(selected) - - -def test_lint_reports_duplicate_concrete_writers_and_cycles() -> None: - graph = { - "nodes": [ - {"i": 0, "t": "First", "in": ["b"], "out": ["a"]}, - {"i": 1, "t": "Second", "in": ["a"], "out": ["b", "a"]}, - ], - "edges": [ - {"a": 1, "b": 0, "k": "b", "s": "shared"}, - {"a": 0, "b": 1, "k": "a", "s": "shared"}, - ], - "seed_keys_by_source": {"anything": []}, - } - - problems = config_graph.lint(graph) - - assert any("reads 'b' before" in problem for problem in problems) - assert any("duplicate writer for 'a'" in problem for problem in problems) - assert any("dependency cycle" in problem for problem in problems) - - -def test_cli_emits_renderer_compatible_both_mode_json( - tmp_path: Path, monkeypatch -) -> None: - selected = tmp_path / "selected.yaml" - output = tmp_path / "graph.json" - _write_full_config(selected) - - instantiations = 0 - original = config_graph._stages - - def count_instantiations(pipeline_config): - nonlocal instantiations - instantiations += 1 - return original(pipeline_config) - - monkeypatch.setattr(config_graph, "_stages", count_instantiations) - result = config_graph.main([str(output), str(selected), "--mode", "both", "--lint"]) - payload = json.loads(output.read_text()) - - assert result == 0 - assert instantiations == 1 - assert set(payload) == {"selected [train]", "selected [inference]"} - for graph in payload.values(): - assert isinstance(graph["nodes"], list) - assert isinstance(graph["edges"], list) - assert all({"i", "t", "in", "out", "p"} <= set(node) for node in graph["nodes"]) - assert all({"a", "b", "k", "s"} <= set(edge) for edge in graph["edges"]) - - -def test_cli_resolves_repo_imports_without_pythonpath(tmp_path: Path) -> None: - output = tmp_path / "action-flow.graph.json" - experiment = ( - _TOOL.parents[1] / "egomimic/hydra_configs/experiment/sim/pusht/" - "action_flow_bc_usocket_recon1_s42.yaml" - ) - environment = os.environ.copy() - environment.pop("PYTHONPATH", None) - completed = subprocess.run( - [ - sys.executable, - str(_TOOL), - str(output), - str(experiment), - "--mode", - "both", - "--lint", - ], - cwd=tmp_path, - check=False, - capture_output=True, - text=True, - env=environment, - ) - - assert completed.returncode == 0, completed.stderr - assert output.is_file() diff --git a/tests/test_current_family_profiles.py b/tests/test_current_family_profiles.py deleted file mode 100644 index 8e17c14b32..0000000000 --- a/tests/test_current_family_profiles.py +++ /dev/null @@ -1,224 +0,0 @@ -"""Guard owner-selected contracts without launching or reading a training corpus.""" - -import runpy -import subprocess -from pathlib import Path - -import pytest -from hydra import compose, initialize_config_dir - -ROOT = Path(__file__).resolve().parents[1] -SUITES = [ - ("libero10", 605121), - ("libero_spatial", 270054), - ("libero_object", 325065), - ("libero_goal", 280056), -] - - -@pytest.mark.parametrize("suite,steps", SUITES) -def test_libero_current_and_historical_profiles_remain_distinct(suite, steps): - with initialize_config_dir( - config_dir=str(ROOT / "egomimic/hydra_configs"), version_base="1.3" - ): - af = compose( - config_name="train_zarr_cartesian", - overrides=[ - f"+experiment=sim/libero/action_flow_{suite}_oat_dp_matched_s42" - ], - ) - dp = compose( - config_name="train_zarr_cartesian", - overrides=[f"+experiment=sim/libero/dp_{suite}_oat_batch16_keep_steps_s42"], - ) - old = compose( - config_name="train_zarr_cartesian", - overrides=[ - f"+experiment=sim/libero_historical/dp_{suite}_oat_dp_matched_s42" - ], - ) - assert ( - af.model.action_horizon, - af.model.num_latent_tokens, - af.model.latent_dim, - ) == (32, 16, 16) - assert (af.benchmark.n_obs_steps, af.benchmark.n_action_steps) == (2, 16) - assert af.model.pipeline.stages[0]._target_.endswith("OATObservationStage") - assert af.model.pipeline.stages[-1].action_velocity_weight == 1.0 - assert af.model.pipeline.stages[5].inference_method == "euler" - assert (af.model.num_inference_steps, af.model.cfg_scale, af.model.hidden_dim) == ( - 50, - 4.0, - 240, - ) - assert af.data.train_datasets.libero_panda.valid_ratio == 0.1 - assert af.data.valid_datasets.libero_panda.valid_ratio == 0.1 - assert ( - af.benchmark.batch_size, - af.trainer.accumulate_grad_batches, - af.trainer.gradient_clip_val, - ) == (32, 1, 3.0) - assert af.callbacks.ema._target_.endswith("ActionFlowFixedEMACallback") - assert af.callbacks.ema.decay == 0.9978 and af.callbacks.ema.use_warmup is False - assert af.model.optimizer._target_.endswith("ReleasedUniteCompositeOptimizer") - assert ( - dp.benchmark.batch_size, - dp.trainer.accumulate_grad_batches, - dp.trainer.max_steps, - ) == (16, 1, steps) - assert dp.benchmark.original_optimizer_step_target == steps - assert dp.callbacks.batch_budget.global_batch_size == 16 - assert dp.norm_stats.precomputed_norm_path is None - assert dp.callbacks.model_checkpoint.save_top_k == -1 - assert old.benchmark.batch_size * old.trainer.accumulate_grad_batches == 1024 - - -def test_dp_multiplier_actual_phase_arguments(monkeypatch): - for k, v in dict( - DP_COTRAIN_STANDARD="usocket_chain_manual4919_af_obs_multiplier_261m_v1", - DP_REPO=str(ROOT), - DP_WANDB_ID="fixture", - DP_EXPECTED_HEAD="a" * 40, - DP_NORM_SHA256="b" * 64, - DP_SPLIT_SHA256="c" * 64, - DP_CONTENT_MANIFEST="/fixture/content.json", - DP_CONTENT_SHA256="d" * 64, - DP_CONTENT_AGGREGATE_SHA256="e" * 64, - ).items(): - monkeypatch.setenv(k, v) - monkeypatch.delenv("DP_SINGLE_SOURCE_STANDARD", raising=False) - api = runpy.run_path(str(ROOT / "scripts/train/standard_dp_launch.py")) - for phase in ("normalize", "smoke", "full"): - argv = api["arguments"](phase, "/fixture/" + phase, "/fixture/norm") - with initialize_config_dir( - config_dir=str(ROOT / "egomimic/hydra_configs"), version_base="1.3" - ): - cfg = compose(config_name="train_zarr_cartesian", overrides=argv[1:]) - assert cfg.name == "planar_uc_manual4919_dp_261m_af_obs_multiplier" - assert ( - cfg.planar.batch_size == 32 - and cfg.model.pipeline.stages[5].condition_input_dim == 128 - ) - assert cfg.model.pipeline.stages[2].conditioning_input == "retiming_multiplier" - assert cfg.model.pipeline.stages[2].get("speed_reference") is None - assert cfg.callbacks.get("ema") is None - assert cfg.model.optimizer._target_ == "torch.optim.AdamW" - assert cfg.model.scheduler.warmup_steps == 500 - assert cfg.norm_stats.sample_frac == 1.0 - - -@pytest.mark.parametrize( - "cotrain,single", - [ - ("unknown", ""), - ("", "unknown"), - ( - "usocket_chain_manual4919_af_obs_multiplier_261m_v1", - "chain_manual4919_retimed_v1", - ), - ], -) -def test_unrecognized_or_conflicting_profiles_fail_before_launch( - monkeypatch, cotrain, single -): - monkeypatch.setenv("DP_COTRAIN_STANDARD", cotrain) - monkeypatch.setenv("DP_SINGLE_SOURCE_STANDARD", single) - api = runpy.run_path(str(ROOT / "scripts/train/standard_dp_launch.py")) - with pytest.raises(ValueError): - api["recipe"]() - env = dict(__import__("os").environ) - result = subprocess.run( - [ - "bash", - str(ROOT / "scripts/train/planar_v2_dp_cotrain_clean_skynet_v1.sbatch"), - ], - env=env, - capture_output=True, - ) - assert result.returncode == 64 - - -def test_current_pair_helper_composes_all_eight_without_claiming_split_proof(tmp_path): - api = runpy.run_path(str(ROOT / "tools/libero_oat_pair_config.py")) - result = api["compose_all"](ROOT, tmp_path) - assert len(result["rows"]) == 8 - assert result["recipe_contract"] == "current" - assert ( - result["episode_split_match"] - == "UNVERIFIED_REQUIRES_MATERIALIZED_EPISODE_LISTS" - ) - assert result["full_training_ready"] is False - import json - - for suite, steps in SUITES: - af = json.loads((tmp_path / f"action_flow-{suite}.json").read_text()) - dp = json.loads((tmp_path / f"dp-{suite}.json").read_text()) - assert af["trainer"]["max_steps"] == 120000 - assert dp["trainer"]["max_steps"] == steps - assert api["validate"](dp, "dp", suite) == 16 - with pytest.raises(AssertionError): - api["validate"](dp, "dp", suite, recipe_contract="historical-v7") - af["trainer"]["max_steps"] = 80000 - with pytest.raises(AssertionError): - api["validate"](af, "action_flow", suite) - assert ( - api["validate"](af, "action_flow", suite, recipe_contract="historical-v7") - == 32 - ) - - -def test_historical_native_names_preserve_aliases_and_reject_current_recipe(): - from egomimic.benchmarks.libero.native_launch_profiles import ( - HISTORICAL_NATIVE_PROFILES, - PROFILES, - historical_profile_for_suite, - profile_for_experiment, - profile_for_suite, - ) - - assert PROFILES is HISTORICAL_NATIVE_PROFILES - assert profile_for_suite is historical_profile_for_suite - for suite, _ in SUITES: - assert historical_profile_for_suite(suite).experiment.startswith( - "sim/libero_historical/" - ) - with pytest.raises(ValueError): - profile_for_experiment(f"sim/libero/action_flow_{suite}_oat_dp_matched_s42") - - -@pytest.mark.parametrize("receipt", [None, {"recipe_contract": "historical-v7"}]) -def test_current_full_proof_rejects_legacy_gate_before_creating_output( - monkeypatch, tmp_path, receipt -): - import json - import sys - - monkeypatch.syspath_prepend(str(ROOT / "tools")) - api = runpy.run_path(str(ROOT / "tools/libero_oat_training_proof.py")) - monkeypatch.setenv("SLURM_STEP_ID", "fixture") - monkeypatch.setattr( - api["subprocess"], - "check_output", - lambda argv, **kwargs: "a" * 40 if argv[1] == "rev-parse" else "", - ) - argv = [ - "proof", - "--root", - str(tmp_path), - "--source-commit", - "a" * 40, - "--index", - "0", - "--phase", - "full", - "--attempt", - "v1", - ] - if receipt is not None: - path = tmp_path / "legacy.json" - path.write_text(json.dumps(receipt)) - argv += ["--readiness-receipt", str(path)] - monkeypatch.setattr(sys, "argv", argv) - with pytest.raises((ValueError, AssertionError)): - api["main"]() - assert not (tmp_path / "full-v1").exists() diff --git a/tests/test_gaussian_paraboloid_dataset.py b/tests/test_gaussian_paraboloid_dataset.py deleted file mode 100644 index 6c18929a0b..0000000000 --- a/tests/test_gaussian_paraboloid_dataset.py +++ /dev/null @@ -1,40 +0,0 @@ -import torch - -from egomimic.synthetic import ( - GaussianParaboloidDataset, - generate_gaussian_paraboloid, -) - - -def test_gaussian_paraboloid_is_deterministic_and_on_surface(): - first = generate_gaussian_paraboloid(128, seed=7, curvature=0.25) - second = generate_gaussian_paraboloid(128, seed=7, curvature=0.25) - torch.testing.assert_close(first.source_latent, second.source_latent) - torch.testing.assert_close(first.source_gaussian_3d, second.source_gaussian_3d) - torch.testing.assert_close(first.target_3d, second.target_3d) - torch.testing.assert_close(first.target_3d[:, :2], first.source_2d) - torch.testing.assert_close( - first.target_3d[:, 2], 0.25 * first.source_2d.square().sum(dim=-1) - ) - - -def test_latent_four_preserves_paired_problem_and_adds_nuisance_dimensions(): - wide = generate_gaussian_paraboloid(64, seed=9, source_dim=4) - reference = generate_gaussian_paraboloid(64, seed=9, source_dim=2) - assert wide.source_latent.shape == (64, 4) - torch.testing.assert_close(wide.source_latent[:, :2], reference.source_latent) - torch.testing.assert_close(wide.target_3d, reference.target_3d) - torch.testing.assert_close(wide.source_gaussian_3d, reference.source_gaussian_3d) - - -def test_dataset_exposes_exact_linear_bridge(): - dataset = GaussianParaboloidDataset(32, seed=11) - indices = torch.tensor([0, 3, 7]) - time = torch.tensor([0.0, 0.5, 1.0]) - state, velocity = dataset.cfm_state_velocity(indices, time) - source = dataset.data.source_3d[indices] - target = dataset.data.target_3d[indices] - torch.testing.assert_close(velocity, target - source) - torch.testing.assert_close(state[0], source[0]) - torch.testing.assert_close(state[1], 0.5 * (source[1] + target[1])) - torch.testing.assert_close(state[2], target[2]) diff --git a/tests/test_gaussian_torus_dataset.py b/tests/test_gaussian_torus_dataset.py deleted file mode 100644 index 6591cb47d7..0000000000 --- a/tests/test_gaussian_torus_dataset.py +++ /dev/null @@ -1,63 +0,0 @@ -import math -import subprocess -import sys -from pathlib import Path - -import torch - -from egomimic.synthetic import GaussianTorusDataset, generate_gaussian_torus - - -def test_generator_script_resolves_the_checkout_outside_repo_cwd(tmp_path): - source = Path(__file__).parents[1] - output = tmp_path / "generated.npz" - subprocess.run( - [ - sys.executable, - str(source / "scripts/data/generate_gaussian_torus.py"), - "--output", - str(output), - "--count", - "32", - "--source-dim", - "8", - ], - cwd=tmp_path, - check=True, - ) - assert output.is_file() - - -def test_gaussian_torus_is_deterministic_and_on_surface(): - first = generate_gaussian_torus(128, seed=7) - second = generate_gaussian_torus(128, seed=7) - assert torch.equal(first.source_2d, second.source_2d) - assert torch.equal(first.source_gaussian_3d, second.source_gaussian_3d) - assert torch.equal(first.target_3d, second.target_3d) - radial = first.target_3d[:, :2].norm(dim=-1) - residual = (radial - 2.0).square() + first.target_3d[:, 2].square() - assert torch.allclose(residual, torch.full_like(residual, 0.65**2), atol=2e-6) - assert torch.all((first.angles >= 0) & (first.angles <= 2 * math.pi)) - - -def test_linear_cfm_bridge_endpoints_and_velocity(): - dataset = GaussianTorusDataset(16, seed=11) - indices = torch.tensor([1, 4, 9]) - start, velocity = dataset.cfm_state_velocity(indices, torch.zeros(3)) - end, velocity_end = dataset.cfm_state_velocity(indices, torch.ones(3)) - assert torch.equal(start, dataset.data.source_3d[indices]) - assert torch.allclose(end, dataset.data.target_3d[indices]) - assert torch.equal(velocity, velocity_end) - assert torch.allclose(end - start, velocity) - - -def test_four_dimensional_source_keeps_same_torus_coupling(): - batch = generate_gaussian_torus(64, seed=9, source_dim=4) - assert batch.source_latent.shape == (64, 4) - assert torch.equal(batch.source_latent[:, :2], batch.source_2d) - reference = generate_gaussian_torus(64, seed=9, source_dim=2) - assert torch.equal(batch.source_2d, reference.source_2d) - assert torch.equal(batch.target_3d, reference.target_3d) - assert torch.equal(batch.source_gaussian_3d, reference.source_gaussian_3d) - assert batch.source_gaussian_latent.shape == (64, 4) - assert torch.equal(batch.source_gaussian_3d, batch.source_gaussian_latent[:, :3]) diff --git a/tests/test_ice_checkpoint_archive_inventory.py b/tests/test_ice_checkpoint_archive_inventory.py deleted file mode 100644 index 5c4fff60d4..0000000000 --- a/tests/test_ice_checkpoint_archive_inventory.py +++ /dev/null @@ -1,216 +0,0 @@ -#!/usr/bin/env python3 - -from __future__ import annotations - -import hashlib -import importlib.util -import json -import sys -import tempfile -import unittest -from pathlib import Path - -ICE_DIR = Path(__file__).parents[1] / "scripts" / "ice" -INVENTORY_PATH = ICE_DIR / "ice_checkpoint_archive_inventory.py" -sys.path.insert(0, str(ICE_DIR)) -SPEC = importlib.util.spec_from_file_location( - "ice_checkpoint_archive_inventory", INVENTORY_PATH -) -INVENTORY = importlib.util.module_from_spec(SPEC) -assert SPEC.loader is not None -sys.modules[SPEC.name] = INVENTORY -SPEC.loader.exec_module(INVENTORY) - - -def make_validator(root: Path) -> Path: - validator = root / "validate.py" - validator.write_text("#!/bin/sh\nexit 0\n") - validator.chmod(0o700) - return validator - - -def write_complete(run: Path, checkpoint: Path, step: int) -> None: - (run / "COMPLETE.json").write_text( - json.dumps( - { - "schema_version": 1, - "status": "COMPLETE", - "checkpoint": { - "global_step": step, - "sha256": hashlib.sha256(checkpoint.read_bytes()).hexdigest(), - }, - } - ) - ) - - -class ArchiveInventoryTest(unittest.TestCase): - def test_distinguishes_completed_archived_pending_and_unregistered(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) / "scratch" / "campaign" - state_dir = root / "service" / "state" - state_dir.mkdir(parents=True) - validator = make_validator(Path(raw)) - - archived = root / "archived" - archived_ckpt = archived / "checkpoints" / "step-10.ckpt" - archived_ckpt.parent.mkdir(parents=True) - archived_ckpt.write_text("archived") - write_complete(archived, archived_ckpt, 10) - - pending = root / "pending" - pending_ckpt = pending / "checkpoints" / "step-20.ckpt" - pending_ckpt.parent.mkdir(parents=True) - pending_ckpt.write_text("pending") - write_complete(pending, pending_ckpt, 20) - - unknown = root / "unknown" - unknown_ckpt = unknown / "checkpoints" / "step-30.ckpt" - unknown_ckpt.parent.mkdir(parents=True) - unknown_ckpt.write_text("unknown") - - manifest = Path(raw) / "manifest.json" - manifest.write_text( - json.dumps( - { - "schema_version": 1, - "runs": [ - { - "id": "archived", - "local_root": str(archived), - "checkpoint_glob": "checkpoints/*.ckpt", - "validator": str(validator), - "remote_dir": "/remote/archive/archived", - }, - { - "id": "pending", - "local_root": str(pending), - "checkpoint_glob": "checkpoints/*.ckpt", - "validator": str(validator), - "remote_dir": "/remote/archive/pending", - }, - ], - } - ) - ) - (state_dir / "mirror-state.json").write_text( - json.dumps( - { - "schema_version": 2, - "files": { - str(archived_ckpt.resolve()): { - "run_id": "archived", - "remote_verified": True, - "global_step": 10, - "sha256": hashlib.sha256( - archived_ckpt.read_bytes() - ).hexdigest(), - } - }, - } - ) - ) - output = state_dir / "archive-inventory.json" - code = INVENTORY.main( - [ - "--search-root", - str(root), - "--manifest", - str(manifest), - "--state-dir", - str(state_dir), - "--output", - str(output), - "--require-clear", - ] - ) - self.assertEqual(code, 1) - report = json.loads(output.read_text()) - statuses = { - Path(row["run_root"]).name: row["status"] for row in report["runs"] - } - self.assertEqual(statuses["archived"], "complete_archived") - self.assertEqual(statuses["pending"], "complete_needs_transfer") - self.assertEqual(statuses["unknown"], "unregistered") - - def test_slurm_like_metadata_does_not_imply_completion(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) / "scratch" / "campaign" - run = root / "run" - checkpoint = run / "checkpoints" / "step.ckpt" - checkpoint.parent.mkdir(parents=True) - checkpoint.write_text("checkpoint") - (run / "slurm-COMPLETED.txt").write_text("COMPLETED") - report = INVENTORY.build_inventory( - root, - {"schema_version": 1, "runs": []}, - {"schema_version": 2, "files": {}}, - 4, - ) - self.assertFalse(report["runs"][0]["complete"]) - - def test_completed_run_with_missing_checkpoint_is_not_invisible(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) / "scratch" / "campaign" - run = root / "run" - run.mkdir(parents=True) - (run / "COMPLETE.json").write_text( - json.dumps( - { - "schema_version": 1, - "status": "COMPLETE", - "checkpoint": {"global_step": 10, "sha256": "a" * 64}, - } - ) - ) - report = INVENTORY.build_inventory( - root, - {"schema_version": 1, "runs": []}, - {"schema_version": 2, "files": {}}, - 4, - ) - self.assertEqual(report["runs"][0]["status"], "unregistered") - self.assertTrue(report["runs"][0]["complete"]) - self.assertEqual(report["runs"][0]["checkpoint_count"], 0) - - def test_registered_complete_run_with_missing_checkpoint_requires_attention(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) / "scratch" / "campaign" - run = root / "run" - run.mkdir(parents=True) - sentinel = run / "COMPLETE.json" - sentinel.write_text( - json.dumps( - { - "schema_version": 1, - "status": "COMPLETE", - "checkpoint": {"global_step": 10, "sha256": "a" * 64}, - } - ) - ) - report = INVENTORY.build_inventory( - root, - { - "schema_version": 1, - "runs": [ - { - "id": "run", - "local_root": str(run), - "checkpoint_glob": "checkpoints/*.ckpt", - "validator": "/unused/by_inventory", - "remote_dir": "/remote/archive/run", - "completion_sentinel": str(sentinel), - } - ], - }, - {"schema_version": 2, "files": {}}, - 4, - ) - self.assertEqual( - report["runs"][0]["status"], "complete_missing_checkpoints" - ) - self.assertEqual(report["attention_required"], [str(run.resolve())]) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_ice_checkpoint_mirror.py b/tests/test_ice_checkpoint_mirror.py deleted file mode 100644 index 88f69ee049..0000000000 --- a/tests/test_ice_checkpoint_mirror.py +++ /dev/null @@ -1,420 +0,0 @@ -#!/usr/bin/env python3 - -from __future__ import annotations - -import hashlib -import importlib.util -import json -import os -import subprocess -import sys -import tempfile -import time -import unittest -from pathlib import Path -from unittest import mock - -MODULE_PATH = Path(__file__).parents[1] / "scripts" / "ice" / "ice_checkpoint_mirror.py" -SBATCH_PATH = ( - Path(__file__).parents[1] / "scripts" / "ice" / "ice_checkpoint_mirror.sbatch" -) -SPEC = importlib.util.spec_from_file_location("ice_checkpoint_mirror", MODULE_PATH) -MODULE = importlib.util.module_from_spec(SPEC) -assert SPEC.loader is not None -sys.modules[SPEC.name] = MODULE -SPEC.loader.exec_module(MODULE) - - -def make_validator(root: Path) -> Path: - validator = root / "validate_checkpoint.py" - validator.write_text( - "#!/usr/bin/env python3\n" - "import json,sys\n" - "try:\n" - " data=json.load(open(sys.argv[1]))\n" - "except Exception:\n" - " raise SystemExit(2)\n" - "if not data.get('valid', True): raise SystemExit(3)\n" - "print(json.dumps({'global_step':data['global_step'],'run_id':data.get('run_id','run')}))\n" - ) - validator.chmod(0o700) - return validator - - -def write_checkpoint(path: Path, step: int, *, valid: bool = True) -> None: - temporary = path.with_suffix(path.suffix + ".tmp") - temporary.write_text( - json.dumps({"global_step": step, "valid": valid, "run_id": "run"}) - ) - os.replace(temporary, path) - - -def write_manifest( - path: Path, - run: Path, - validator: Path, - *, - retain: int = 2, - sentinel: Path | None = None, -) -> None: - row = { - "id": "run", - "local_root": str(run), - "checkpoint_glob": "*.ckpt", - "validator": str(validator), - "remote_dir": "/remote/archive/run", - "retain_local": retain, - } - if sentinel is not None: - row["completion_sentinel"] = str(sentinel) - path.write_text(json.dumps({"schema_version": 1, "runs": [row]})) - - -def mirror_args(manifest: Path, state: Path, scratch: Path) -> list[str]: - return [ - "--manifest", - str(manifest), - "--state-dir", - str(state), - "--scratch-root", - str(scratch), - "--quota-bytes", - "1000000", - "--remote-host", - "Skynet", - "--prune-after-verify", - "--once", - ] - - -class CheckpointMetadataTest(unittest.TestCase): - def test_cpu_wrapper_forwards_configured_du_timeout(self): - wrapper = SBATCH_PATH.read_text() - self.assertIn('"${ICE_MIRROR_PYTHON}" "${ICE_MIRROR_SCRIPT}"', wrapper) - self.assertIn( - 'export PATH="$(dirname "${ICE_MIRROR_PYTHON}"):${PATH}"', wrapper - ) - self.assertIn( - 'export PYTHONPATH="${ICE_MIRROR_PYTHONPATH}${PYTHONPATH:+:${PYTHONPATH}}"', - wrapper, - ) - self.assertIn('if [[ -n "${ICE_MIRROR_DU_TIMEOUT_SECONDS:-}" ]]', wrapper) - self.assertIn( - 'ARGS+=(--du-timeout-seconds "${ICE_MIRROR_DU_TIMEOUT_SECONDS}")', - wrapper, - ) - - def test_stable_validation_requires_json_global_step(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - checkpoint = root / "x.ckpt" - write_checkpoint(checkpoint, 7) - validator = make_validator(root) - info = MODULE.stable_valid_info(checkpoint.resolve(), validator) - self.assertIsNotNone(info) - self.assertEqual(info.global_step, 7) - self.assertEqual( - info.sha256, hashlib.sha256(checkpoint.read_bytes()).hexdigest() - ) - bad_validator = root / "bad_validator" - bad_validator.write_text("#!/bin/sh\nprintf 'not-json\\n'\n") - bad_validator.chmod(0o700) - rejection = {} - self.assertIsNone( - MODULE.stable_valid_info( - checkpoint.resolve(), - bad_validator, - rejection=rejection, - ) - ) - self.assertEqual( - rejection["reason"], "invalid_metadata_or_unreadable_checkpoint" - ) - - def test_validator_failure_preserves_bounded_stderr_and_returncode(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - checkpoint = root / "x.ckpt" - write_checkpoint(checkpoint, 7) - validator = root / "bad_validator" - validator.write_text( - "#!/bin/sh\nprintf '%s' '" - + "x" * 3000 - + "ModuleNotFoundError: No module named torch' >&2\nexit 9\n" - ) - validator.chmod(0o700) - rejection = {} - self.assertIsNone( - MODULE.stable_valid_info(checkpoint, validator, rejection=rejection) - ) - self.assertEqual(rejection["reason"], "validator_failed") - self.assertEqual(rejection["validator_returncode"], 9) - self.assertEqual(len(rejection["validator_stderr_tail"]), 2048) - self.assertIn("No module named torch", rejection["validator_stderr_tail"]) - self.assertTrue(checkpoint.exists()) - - def test_changing_checkpoint_remains_deferred_with_explicit_reason(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - checkpoint = root / "x.ckpt" - write_checkpoint(checkpoint, 7) - validator = make_validator(root) - - def changing_validator(*args, **kwargs): - checkpoint.write_text(json.dumps({"global_step": 777777})) - return subprocess.CompletedProcess(args[0], 0, '{"global_step": 7}', "") - - rejection = {} - with mock.patch.object( - MODULE.subprocess, "run", side_effect=changing_validator - ): - self.assertIsNone( - MODULE.stable_valid_info(checkpoint, validator, rejection=rejection) - ) - self.assertEqual( - rejection, {"reason": "checkpoint_changed_during_validation"} - ) - self.assertTrue(checkpoint.exists()) - - def test_specific_absolute_rejects_relative_path(self): - with self.assertRaisesRegex(SystemExit, "specific absolute"): - MODULE.specific_absolute(Path("relative/run/path"), "test") - - -class MirrorSafetyTest(unittest.TestCase): - def test_invalid_newer_files_do_not_count_toward_two_valid_retained(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - scratch = root / "scratch" - run = scratch / "project" / "run" - state = scratch / "state" / "mirror" - run.mkdir(parents=True) - state.mkdir(parents=True) - validator = make_validator(root) - first = run / "step-1.ckpt" - second = run / "step-2.ckpt" - corrupt_a = run / "step-100.ckpt" - corrupt_b = run / "step-101.ckpt" - write_checkpoint(first, 1) - write_checkpoint(second, 2) - write_checkpoint(corrupt_a, 100, valid=False) - write_checkpoint(corrupt_b, 101, valid=False) - now = time.time() - os.utime(corrupt_a, (now + 10, now + 10)) - os.utime(corrupt_b, (now + 20, now + 20)) - manifest = root / "manifest.json" - write_manifest(manifest, run, validator) - remote: dict[str, str] = {} - - def fake_mirror(path, digest, remote_dir, host, ssh, rsync, rsync_rsh): - destination = f"/remote/{digest}" - remote[destination] = digest - return destination - - with ( - mock.patch.object(MODULE, "mirror_one", side_effect=fake_mirror), - mock.patch.object( - MODULE, - "remote_sha", - side_effect=lambda ssh, host, path: remote.get(path), - ), - mock.patch.object(MODULE, "scratch_bytes", return_value=100), - ): - code = MODULE.main(mirror_args(manifest, state, scratch)) - self.assertEqual( - code, 1, "invalid candidates must keep completion unresolved" - ) - self.assertTrue(first.exists()) - self.assertTrue(second.exists()) - self.assertTrue(corrupt_a.exists()) - self.assertTrue(corrupt_b.exists()) - pressure = json.loads((state / "storage-pressure.json").read_text()) - self.assertEqual(pressure["cycle_errors"], 2) - rejected = [ - json.loads(line) - for line in (state / "mirror-events.jsonl").read_text().splitlines() - if json.loads(line)["event"] == "checkpoint_not_stably_valid" - ] - self.assertEqual(len(rejected), 2) - self.assertTrue( - all(event["validator_returncode"] == 3 for event in rejected) - ) - self.assertTrue( - all(event["reason"] == "validator_failed" for event in rejected) - ) - - def test_prune_rechecks_remote_sha_immediately(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - scratch = root / "scratch" - run = scratch / "project" / "run" - state = scratch / "state" / "mirror" - run.mkdir(parents=True) - state.mkdir(parents=True) - validator = make_validator(root) - checkpoints = [] - for step in (1, 2, 3): - path = run / f"step-{step}.ckpt" - write_checkpoint(path, step) - checkpoints.append(path) - manifest = root / "manifest.json" - write_manifest(manifest, run, validator) - files = {} - remote = {} - for path in checkpoints: - digest = hashlib.sha256(path.read_bytes()).hexdigest() - remote_path = f"/remote/{digest}" - files[str(path.resolve())] = { - "sha256": digest, - "run_id": "run", - "remote_path": remote_path, - "remote_verified": True, - } - remote[remote_path] = digest - (state / "mirror-state.json").write_text( - json.dumps({"schema_version": 1, "files": files}) - ) - oldest_remote = files[str(checkpoints[0].resolve())]["remote_path"] - calls = {oldest_remote: 0} - - def fake_remote_sha(ssh, host, path): - if path == oldest_remote: - calls[path] += 1 - return remote[path] if calls[path] == 1 else None - return remote.get(path) - - with ( - mock.patch.object( - MODULE, - "mirror_one", - side_effect=AssertionError("no upload expected"), - ), - mock.patch.object(MODULE, "remote_sha", side_effect=fake_remote_sha), - mock.patch.object(MODULE, "scratch_bytes", return_value=100), - ): - code = MODULE.main(mirror_args(manifest, state, scratch)) - self.assertEqual(code, 1) - self.assertTrue( - checkpoints[0].exists(), - "local file was pruned after remote disappeared", - ) - final_state = json.loads((state / "mirror-state.json").read_text()) - self.assertFalse( - final_state["files"][str(checkpoints[0].resolve())]["remote_verified"] - ) - - def test_du_failure_is_recorded_without_uncaught_exit(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - scratch = root / "scratch" - run = scratch / "project" / "run" - state = scratch / "state" / "mirror" - run.mkdir(parents=True) - state.mkdir(parents=True) - validator = make_validator(root) - manifest = root / "manifest.json" - write_manifest(manifest, run, validator) - with mock.patch.object( - MODULE, "scratch_bytes", side_effect=RuntimeError("du failed") - ): - code = MODULE.main(mirror_args(manifest, state, scratch)) - self.assertEqual(code, 1) - pressure = json.loads((state / "storage-pressure.json").read_text()) - self.assertEqual(pressure["pressure"], "unknown") - self.assertIn("du failed", pressure["error"]) - - def test_deterministic_resumable_temp_and_full_digest_name(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - checkpoint = root / "step-9.ckpt" - checkpoint.write_bytes(b"checkpoint") - digest = hashlib.sha256(checkpoint.read_bytes()).hexdigest() - remote_commands = [] - local_commands = [] - sha_results = iter((None, digest, digest)) - - def fake_remote_command(ssh, host, argv, capture=False): - remote_commands.append(argv) - return "" - - def fake_run_checked(command, capture=False, timeout=None): - local_commands.append(command) - return subprocess.CompletedProcess(command, 0, "", "") - - with ( - mock.patch.object( - MODULE, "remote_sha", side_effect=lambda *args: next(sha_results) - ), - mock.patch.object( - MODULE, "remote_command", side_effect=fake_remote_command - ), - mock.patch.object(MODULE, "run_checked", side_effect=fake_run_checked), - ): - final = MODULE.mirror_one( - checkpoint, - digest, - "/remote/archive/run", - "Skynet", - "ssh", - "rsync", - None, - ) - self.assertIn(digest, final) - self.assertEqual(local_commands[0][-1], f"Skynet:{final}.partial") - self.assertNotRegex(local_commands[0][-1], r"\.partial\.\d+$") - self.assertIn( - ["mv", "-n", "--", f"{final}.partial", final], remote_commands - ) - - def test_completion_sentinel_stops_monitor_after_final_mirror(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - scratch = root / "scratch" - run = scratch / "project" / "run" - state = scratch / "state" / "mirror" - run.mkdir(parents=True) - state.mkdir(parents=True) - validator = make_validator(root) - checkpoint = run / "step-10.ckpt" - write_checkpoint(checkpoint, 10) - sentinel = run / "COMPLETE.json" - sentinel.write_text( - json.dumps( - { - "schema_version": 1, - "status": "COMPLETE", - "checkpoint": { - "global_step": 10, - "sha256": hashlib.sha256( - checkpoint.read_bytes() - ).hexdigest(), - }, - } - ) - ) - manifest = root / "manifest.json" - write_manifest(manifest, run, validator, sentinel=sentinel) - remote = {} - - def fake_mirror(path, digest, remote_dir, host, ssh, rsync, rsync_rsh): - destination = f"/remote/{digest}" - remote[destination] = digest - return destination - - with ( - mock.patch.object(MODULE, "mirror_one", side_effect=fake_mirror), - mock.patch.object( - MODULE, - "remote_sha", - side_effect=lambda ssh, host, path: remote.get(path), - ), - mock.patch.object(MODULE, "scratch_bytes", return_value=100), - ): - code = MODULE.main(mirror_args(manifest, state, scratch)) - self.assertEqual(code, 0) - complete = json.loads((state / "mirror-complete.json").read_text()) - self.assertEqual(complete["status"], "COMPLETE") - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_ice_checkpoint_mirror_pool.py b/tests/test_ice_checkpoint_mirror_pool.py deleted file mode 100644 index beed452522..0000000000 --- a/tests/test_ice_checkpoint_mirror_pool.py +++ /dev/null @@ -1,310 +0,0 @@ -#!/usr/bin/env python3 - -from __future__ import annotations - -import hashlib -import importlib.util -import json -import sys -import tempfile -import unittest -from pathlib import Path -from unittest import mock - -ICE_DIR = Path(__file__).parents[1] / "scripts" / "ice" -POOL_PATH = ICE_DIR / "ice_checkpoint_mirror_pool.py" -SBATCH_PATH = ICE_DIR / "ice_checkpoint_mirror_pool.sbatch" -sys.path.insert(0, str(ICE_DIR)) -SPEC = importlib.util.spec_from_file_location("ice_checkpoint_mirror_pool", POOL_PATH) -POOL = importlib.util.module_from_spec(SPEC) -assert SPEC.loader is not None -sys.modules[SPEC.name] = POOL -SPEC.loader.exec_module(POOL) - - -def make_validator(root: Path) -> Path: - validator = root / "validate_checkpoint.py" - validator.write_text( - "#!/usr/bin/env python3\n" - "import json,sys\n" - "data=json.load(open(sys.argv[1]))\n" - "print(json.dumps({'global_step':data['global_step']}))\n" - ) - validator.chmod(0o700) - return validator - - -def make_layout(root: Path) -> tuple[Path, Path, Path, Path, Path]: - scratch = root / "scratch" - run = scratch / "project" / "run" - state = scratch / "state" / "mirror" - run.mkdir(parents=True) - state.mkdir(parents=True) - validator = make_validator(root) - checkpoint = run / "step-10.ckpt" - checkpoint.write_text(json.dumps({"global_step": 10})) - sentinel = run / "COMPLETE.json" - sentinel.write_text( - json.dumps( - { - "schema_version": 1, - "status": "COMPLETE", - "checkpoint": { - "global_step": 10, - "sha256": hashlib.sha256(checkpoint.read_bytes()).hexdigest(), - }, - } - ) - ) - manifest = root / "manifest.json" - manifest.write_text( - json.dumps( - { - "schema_version": 1, - "runs": [ - { - "id": "run", - "local_root": str(run), - "checkpoint_glob": "*.ckpt", - "validator": str(validator), - "remote_dir": "/remote/archive/run", - "retain_local": 2, - "completion_sentinel": str(sentinel), - } - ], - } - ) - ) - return scratch, run, state, checkpoint, manifest - - -def worker_args( - manifest: Path, state: Path, scratch: Path, index: int = 0 -) -> list[str]: - return [ - "--manifest", - str(manifest), - "--state-dir", - str(state), - "--scratch-root", - str(scratch), - "--quota-bytes", - "1000000", - "--remote-host", - "Skynet", - "--worker-index", - str(index), - "--worker-count", - "2", - "--once", - ] - - -class MirrorPoolTest(unittest.TestCase): - def test_wrapper_requires_matching_array_and_distinct_nodes(self): - wrapper = SBATCH_PATH.read_text() - self.assertIn("#SBATCH --exclusive", wrapper) - self.assertIn("SLURM_ARRAY_TASK_COUNT", wrapper) - self.assertIn("SLURM_ARRAY_TASK_ID", wrapper) - self.assertIn("ICE_MIRROR_POOL_SIZE", wrapper) - self.assertIn('"${ICE_MIRROR_PYTHON}" "${ICE_MIRROR_POOL_SCRIPT}"', wrapper) - self.assertIn( - 'export PATH="$(dirname "${ICE_MIRROR_PYTHON}"):${PATH}"', wrapper - ) - self.assertIn( - 'export PYTHONPATH="${ICE_MIRROR_PYTHONPATH}${PYTHONPATH:+:${PYTHONPATH}}"', - wrapper, - ) - self.assertIn("ICE_MIRROR_INVENTORY_ROOT", wrapper) - - def test_nonblocking_claim_allows_only_one_worker(self): - with tempfile.TemporaryDirectory() as raw: - state = Path(raw) / "shared" / "state" - checkpoint = Path(raw) / "shared" / "run" / "step.ckpt" - checkpoint.parent.mkdir(parents=True) - checkpoint.touch() - with POOL.claim(state, checkpoint) as first: - self.assertIsNotNone(first) - with POOL.claim(state, checkpoint) as second: - self.assertIsNone(second) - with POOL.claim(state, checkpoint) as third: - self.assertIsNotNone(third) - - def test_pool_mirrors_and_publishes_completion(self): - with tempfile.TemporaryDirectory() as raw: - scratch, _run, state, checkpoint, manifest = make_layout(Path(raw)) - remote: dict[str, str] = {} - - def fake_mirror( - path, - digest, - remote_dir, - host, - ssh, - rsync, - rsync_rsh, - temporary_tag=None, - ): - self.assertEqual(temporary_tag, POOL.task_key(path)[:24]) - destination = f"/remote/{digest}" - remote[destination] = digest - return destination - - with ( - mock.patch.object(POOL.core, "mirror_one", side_effect=fake_mirror), - mock.patch.object( - POOL.core, - "remote_sha", - side_effect=lambda ssh, host, path: remote.get(path), - ), - mock.patch.object(POOL.core, "scratch_bytes", return_value=100), - ): - code = POOL.main(worker_args(manifest, state, scratch)) - self.assertEqual(code, 0) - saved = json.loads((state / "mirror-state.json").read_text()) - entry = saved["files"][str(checkpoint.resolve())] - self.assertTrue(entry["remote_verified"]) - complete = json.loads((state / "mirror-complete.json").read_text()) - self.assertEqual(complete["worker_count"], 2) - - def test_nonmaintenance_worker_never_prunes_or_completes(self): - with tempfile.TemporaryDirectory() as raw: - scratch, _run, state, _checkpoint, manifest = make_layout(Path(raw)) - remote: dict[str, str] = {} - - def fake_mirror( - path, - digest, - remote_dir, - host, - ssh, - rsync, - rsync_rsh, - temporary_tag=None, - ): - destination = f"/remote/{digest}" - remote[destination] = digest - return destination - - with ( - mock.patch.object(POOL.core, "mirror_one", side_effect=fake_mirror), - mock.patch.object( - POOL.core, - "remote_sha", - side_effect=lambda ssh, host, path: remote.get(path), - ), - mock.patch.object( - POOL, - "maintain", - side_effect=AssertionError("worker one ran maintenance"), - ), - ): - code = POOL.main(worker_args(manifest, state, scratch, index=1)) - self.assertEqual(code, 0) - self.assertFalse((state / "mirror-complete.json").exists()) - - def test_rejected_candidate_is_reported_and_blocks_completion(self): - for worker_index in (0, 1): - with ( - self.subTest(worker_index=worker_index), - tempfile.TemporaryDirectory() as raw, - ): - scratch, run, state, checkpoint, manifest = make_layout(Path(raw)) - invalid = run / "step-100.ckpt" - invalid.write_text("incomplete checkpoint") - remote: dict[str, str] = {} - - def fake_mirror(path, digest, *args, **kwargs): - destination = f"/remote/{digest}" - remote[destination] = digest - return destination - - with ( - mock.patch.object(POOL.core, "mirror_one", side_effect=fake_mirror), - mock.patch.object( - POOL.core, - "remote_sha", - side_effect=lambda ssh, host, path: remote.get(path), - ), - mock.patch.object(POOL.core, "scratch_bytes", return_value=100), - ): - code = POOL.main( - worker_args(manifest, state, scratch, index=worker_index) - ) - self.assertEqual(code, 1) - self.assertTrue(checkpoint.exists()) - self.assertTrue(invalid.exists()) - self.assertFalse((state / "mirror-complete.json").exists()) - status = json.loads( - (state / f"worker-{worker_index}-status.json").read_text() - ) - self.assertEqual(status["cycle_errors"], 1) - events = [ - json.loads(line) - for line in (state / "mirror-events.jsonl").read_text().splitlines() - ] - rejected = [ - event - for event in events - if event["event"] == "checkpoint_not_stably_valid" - ] - self.assertEqual(len(rejected), 1) - self.assertEqual(rejected[0]["reason"], "validator_failed") - self.assertEqual(rejected[0]["validator_returncode"], 1) - self.assertIn("JSONDecodeError", rejected[0]["validator_stderr_tail"]) - if worker_index == 0: - pressure = json.loads((state / "storage-pressure.json").read_text()) - self.assertEqual(pressure["cycle_errors"], 1) - - def test_completion_requires_exact_terminal_checkpoint(self): - with tempfile.TemporaryDirectory() as raw: - scratch, run, state, checkpoint, manifest = make_layout(Path(raw)) - sentinel = run / "COMPLETE.json" - payload = json.loads(sentinel.read_text()) - payload["checkpoint"]["global_step"] = 999 - payload["checkpoint"]["sha256"] = "a" * 64 - sentinel.write_text(json.dumps(payload)) - remote: dict[str, str] = {} - - def fake_mirror( - path, - digest, - remote_dir, - host, - ssh, - rsync, - rsync_rsh, - temporary_tag=None, - ): - destination = f"/remote/{digest}" - remote[destination] = digest - return destination - - with ( - mock.patch.object(POOL.core, "mirror_one", side_effect=fake_mirror), - mock.patch.object( - POOL.core, - "remote_sha", - side_effect=lambda ssh, host, path: remote.get(path), - ), - mock.patch.object(POOL.core, "scratch_bytes", return_value=100), - ): - code = POOL.main(worker_args(manifest, state, scratch)) - self.assertEqual(code, 0) - self.assertFalse((state / "mirror-complete.json").exists()) - saved = json.loads((state / "mirror-state.json").read_text()) - self.assertTrue( - saved["files"][str(checkpoint.resolve())]["remote_verified"] - ) - - def test_worker_count_and_index_are_fail_closed(self): - with tempfile.TemporaryDirectory() as raw: - scratch, _run, state, _checkpoint, manifest = make_layout(Path(raw)) - args = worker_args(manifest, state, scratch) - args[args.index("--worker-count") + 1] = "1" - with self.assertRaisesRegex(SystemExit, "worker-count"): - POOL.main(args) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_ice_dp_campaign_contract.py b/tests/test_ice_dp_campaign_contract.py deleted file mode 100644 index c8e40ffc01..0000000000 --- a/tests/test_ice_dp_campaign_contract.py +++ /dev/null @@ -1,28 +0,0 @@ -from pathlib import Path - -ROOT = Path(__file__).parents[1] -LAUNCHER = ROOT / "scripts/ice/launch_planar_v2_dp_cotrain_clean.sbatch" -README = ROOT / "scripts/ice/README.md" - - -def test_dp_ice_launcher_has_one_entrypoint_and_fresh_run_guards(): - text = LAUNCHER.read_text() - assert "MODE=${MODE:-preflight}" in text - assert 'case "$MODE" in preflight|smoke|full)' in text - assert "ckpt_path=null" in text - assert 'test -z "${ICE_INITIAL_CHECKPOINT:-}"' in text - assert 'test -z "${ICE_INITIAL_CHECKPOINT_SHA256:-}"' in text - assert 'test -z "${CKPT_PATH:-}"' in text - assert "PASSED_SMOKE_DIR" in text - assert "trainer.max_steps=$max_steps" in text - assert "FULL_STEPS=240000" in text - assert "VALIDATE_EVERY=10000" in text - assert "CHECKPOINT_EVERY=20000" in text - assert "NVIDIA H100 80GB HBM3" in text and "NVIDIA H200" in text - - -def test_dp_ice_docs_distinguish_smoke_full_and_requeue_from_restore(): - text = README.read_text() - assert "Do not infer full-run completion from a completed smoke" in text - assert "scheduler-generated\nHPC checkpoint" in text - assert "H100 and H200 are\ninterchangeable" in text diff --git a/tests/test_ice_gpu_probe.py b/tests/test_ice_gpu_probe.py deleted file mode 100644 index b3fb1b4fec..0000000000 --- a/tests/test_ice_gpu_probe.py +++ /dev/null @@ -1,50 +0,0 @@ -import importlib.util -from pathlib import Path - -import pytest - -MODULE_PATH = Path(__file__).parents[1] / "scripts" / "ice" / "ice_gpu_probe.py" -SPEC = importlib.util.spec_from_file_location("ice_gpu_probe", MODULE_PATH) -assert SPEC is not None and SPEC.loader is not None -MODULE = importlib.util.module_from_spec(SPEC) -SPEC.loader.exec_module(MODULE) - - -def test_parse_ecc_health_accepts_zero_counters_and_clear_flags(): - report = """ - Volatile Uncorrectable ECC : 0 - Aggregate Uncorrectable ECC : 0 - Pending Page Blacklist : No - Reset Required : No - Remapping Failure Occurred : No - """ - - health = MODULE.parse_ecc_health(report) - - assert len(health["uncorrectable_ecc_counters"]) == 2 - assert health["unhealthy_flags"] == [] - - -@pytest.mark.parametrize( - "report", - ( - "Volatile Uncorrectable ECC : 1\nReset Required : No\n", - "Volatile Uncorrectable ECC : 0\nReset Required : Yes\n", - "Reset Required : No\n", - "Volatile Uncorrectable ECC : unknown\nReset Required : No\n", - ), -) -def test_parse_ecc_health_fails_closed(report): - with pytest.raises(RuntimeError): - MODULE.parse_ecc_health(report) - - -def test_probe_exercises_backward_and_one_rank_nccl_lifecycle(): - text = MODULE_PATH.read_text() - - assert "loss.backward()" in text - assert "dist.init_process_group(" in text - assert 'backend="nccl"' in text - assert "dist.all_reduce(collective)" in text - assert "dist.destroy_process_group()" in text - assert '"world_size": world_size' in text diff --git a/tests/test_ice_planar_bc_launcher.py b/tests/test_ice_planar_bc_launcher.py deleted file mode 100644 index 44132b4f39..0000000000 --- a/tests/test_ice_planar_bc_launcher.py +++ /dev/null @@ -1,110 +0,0 @@ -import re -import subprocess -from pathlib import Path - -ROOT = Path(__file__).parents[1] -LAUNCHER = ROOT / "scripts" / "ice" / "launch_planar_bc.sbatch" - - -def test_launcher_is_valid_bash_and_has_single_gpu_requeue_contract(): - subprocess.run(["bash", "-n", str(LAUNCHER)], check=True) - text = LAUNCHER.read_text() - - assert "#SBATCH --account=coc" in text - assert "#SBATCH --partition=ice-gpu" in text - assert "#SBATCH --qos=coc-ice" in text - assert "#SBATCH --nodes=1" in text - assert "#SBATCH --ntasks-per-node=1" in text - assert "#SBATCH --gres=gpu:1" in text - assert "#SBATCH --constraint=H100|H200" in text - assert "#SBATCH --requeue" in text - assert "#SBATCH --signal=B:USR1@600" in text - assert "trainer.strategy=auto" in text - assert "trainer.devices=1" in text - assert "trainer.num_nodes=1" in text - assert "trainer.precision=$ICE_PRECISION" in text - assert "ICE_EXPECTED_GPU_NAME" not in text - assert text.count("--allowed-gpu-name") == 2 - - -def test_launcher_uses_strict_external_runner_and_completion_sentinel(): - text = LAUNCHER.read_text() - - assert "runtime.slurm_requeue_owner=runner" in text - assert "runtime.slurm_save_signal=SIGUSR2" in text - assert "--checkpoint-validator" in text - assert "--checkpoint-signal USR2" in text - assert "--checkpoint-forwarding slurm-steps" in text - assert "--requeue-owner runner" in text - assert "--confirm-child-requeue-disabled" in text - assert "ICE_OUTPUT_DIR/COMPLETE.json" in text - assert "allow-unvalidated" not in text - assert "ICE_RESUME_CHECKPOINT" in text - assert "ICE_RESUME_CHECKPOINT_SHA256" in text - assert "ICE_RESUME_GLOBAL_STEP" in text - - -def test_parent_runner_inherits_exact_repo_pythonpath_for_checkpoint_validation(): - text = LAUNCHER.read_text() - - parent_export = text.index("export PYTHONPATH=$ICE_REPO") - preflight_runner = text.index('"$ICE_PYTHON" "$ICE_REQUEUE_RUNNER"') - final_runner = text.rindex('exec "$ICE_PYTHON" "$ICE_REQUEUE_RUNNER"') - child_definition = text.index("read -r -d '' CHILD_CODE") - - assert parent_export < preflight_runner < child_definition < final_runner - - -def test_launcher_exposes_supported_bc_configs_and_explicit_training_limits(): - text = LAUNCHER.read_text() - - assert "sim/pusht/planar_v2_usocket_direct_bc" in text - assert "sim/pusht/planar_v2_usocket_arc_bc" in text - assert "sim/pusht/planar_v2_usocket_arc_hybrid_D40_M16_R24deg_bc" in text - assert "sim/pusht/planar_v2_usocket_arc_paper_uniform_D40_M16_R24deg" in text - assert "ICE_VALIDATION_DEFERRED" in text - assert "ICE_LIMIT_VAL_BATCHES must be zero when validation is deferred" in text - assert '"++run_provenance.validation_deferred=$ICE_VALIDATION_DEFERRED"' in text - assert ( - text.count("data.train_datasets.pushshapes_sim_u_socket.resolver.folder_path=") - == 1 - ) - assert ( - text.count("data.valid_datasets.pushshapes_sim_u_socket.resolver.folder_path=") - == 1 - ) - for value in ( - "trainer.max_steps=$ICE_MAX_STEPS", - "trainer.val_check_interval=$ICE_VAL_CHECK_INTERVAL", - "trainer.limit_train_batches=$ICE_LIMIT_TRAIN_BATCHES", - "trainer.limit_val_batches=$ICE_LIMIT_VAL_BATCHES", - "+callbacks.model_checkpoint.every_n_train_steps=$ICE_CHECKPOINT_EVERY_N_STEPS", - ): - assert value in text - assert "unique ICE_OUTPUT_DIR already exists" in text - assert "ICE_LAUNCH_MODE" in text - assert "--dry-run" in text - assert "validate_planar_dataset.py" in text - assert "ICE_EXPECTED_SPLIT_MANIFEST_SHA256" in text - assert "--expected-manifest-sha256" in text - assert '"++paths.root_dir=$ICE_REPO"' in text - assert '"norm_stats.save_cache_dir=$ICE_OUTPUT_DIR"' in text - assert '"norm_stats.save_cache_dir=$ICE_OUTPUT_DIR/norm_stats"' not in text - assert "$ICE_OUTPUT_DIR/norm_stats/norm_stats.json" in text - assert "requeue requires the original run's norm_stats/norm_stats.json" in text - assert "norm_stats.save_cache_dir=null" in text - - -def test_launcher_contains_no_deployment_identity_or_credentials(): - text = LAUNCHER.read_text() - - forbidden = ( - "/coc/", - "/storage/", - "paphiwetsa3", - "WANDB_API_KEY=", - "wandb.ai/", - ) - assert not any(value in text for value in forbidden) - assert re.search(r"\b[0-9]{7}\b", text) is None - assert "BASH_SOURCE" not in text diff --git a/tests/test_ice_requeue_runner.py b/tests/test_ice_requeue_runner.py deleted file mode 100644 index c652e216f3..0000000000 --- a/tests/test_ice_requeue_runner.py +++ /dev/null @@ -1,1476 +0,0 @@ -#!/usr/bin/env python3 - -from __future__ import annotations - -import fcntl -import hashlib -import importlib.util -import json -import os -import shlex -import signal -import subprocess -import sys -import tempfile -import time -import unittest -from pathlib import Path -from unittest import mock - -MODULE_PATH = Path(__file__).parents[1] / "scripts" / "ice" / "ice_requeue_runner.py" -SPEC = importlib.util.spec_from_file_location("ice_requeue_runner", MODULE_PATH) -MODULE = importlib.util.module_from_spec(SPEC) -assert SPEC.loader is not None -sys.modules[SPEC.name] = MODULE -SPEC.loader.exec_module(MODULE) - - -def make_validator(root: Path) -> Path: - validator = root / "validate_checkpoint.py" - validator.write_text( - "#!/usr/bin/env python3\n" - "import json,os,sys\n" - "log=os.environ.get('VALIDATOR_LOG')\n" - "if log:\n" - " with open(log,'a') as stream: stream.write(os.path.realpath(sys.argv[1])+'\\n')\n" - "try:\n" - " data=json.load(open(sys.argv[1]))\n" - "except Exception:\n" - " raise SystemExit(2)\n" - "if not data.get('valid', True): raise SystemExit(3)\n" - "metadata={'global_step': data['global_step']}\n" - "for key in ('run_id','wandb_run_id','config_sha256','source_commit'):\n" - " if key in data: metadata[key]=data[key]\n" - "print(json.dumps(metadata, sort_keys=True))\n" - ) - validator.chmod(0o700) - return validator - - -def write_checkpoint( - path: Path, step: int, *, valid: bool = True, run_id: str = "run" -) -> None: - temporary = path.with_suffix(path.suffix + ".tmp") - temporary.write_text( - json.dumps( - { - "global_step": step, - "valid": valid, - "run_id": run_id, - "wandb_run_id": "wandb-run", - } - ) - ) - os.replace(temporary, path) - - -def make_signaling_scancel(root: Path) -> Path: - executable = root / "scancel" - executable.write_text( - "#!/bin/sh\n" - 'printf \'%s\\n\' "$*" > "$ICE_TEST_SCANCEL_LOG"\n' - 'kill -USR2 "$(cat "$CHILD_PID_FILE")"\n' - ) - executable.chmod(0o700) - return executable - - -def base_args( - state_dir: Path, - checkpoint_glob: str, - validator: Path, - command: list[str], - *, - owner: str = "child", - initial_checkpoint: Path | None = None, - scancel: Path | str = "/usr/bin/true", -) -> list[str]: - result = [ - "--state-dir", - str(state_dir), - ] - if initial_checkpoint is not None: - result.extend(["--initial-checkpoint", str(initial_checkpoint)]) - result.extend( - [ - "--checkpoint-glob", - checkpoint_glob, - "--checkpoint-validator", - str(validator), - "--checkpoint-signal", - "USR2", - "--checkpoint-forwarding", - "slurm-steps", - "--requeue-owner", - owner, - "--scancel", - str(scancel), - "--poll-seconds", - "0.05", - ] - ) - if owner == "runner": - result.append("--confirm-child-requeue-disabled") - result.extend(["--", *command]) - return result - - -class CheckpointSelectionTest(unittest.TestCase): - def test_semantic_global_step_beats_newer_mtime(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - validator = make_validator(root) - step_60 = root / "epoch-6-step-60000.ckpt" - step_40 = root / "staged-step-40000.ckpt" - write_checkpoint(step_60, 60_000) - write_checkpoint(step_40, 40_000) - now = time.time() - os.utime(step_60, (now - 20, now - 20)) - os.utime(step_40, (now, now)) - selected = MODULE.select_checkpoint("*.ckpt", root, validator) - self.assertIsNotNone(selected) - self.assertEqual(selected.path, step_60.resolve()) - self.assertEqual(selected.global_step, 60_000) - - def test_invalid_and_partial_candidates_are_ignored(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - validator = make_validator(root) - valid = root / "step-10.ckpt" - invalid = root / "step-20.ckpt" - partial = root / "step-30.ckpt.partial" - write_checkpoint(valid, 10) - write_checkpoint(invalid, 20, valid=False) - write_checkpoint(partial, 30) - selected = MODULE.select_checkpoint("*.ckpt*", root, validator) - self.assertEqual(selected.path, valid.resolve()) - - def test_multiple_roots_select_highest_semantic_step(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - staged = root / "staged" - live = root / "live" - staged.mkdir() - live.mkdir() - validator = make_validator(root) - source = staged / "step-40000.ckpt" - resumed = live / "step-42000.ckpt" - write_checkpoint(source, 40_000) - write_checkpoint(resumed, 42_000) - selected = MODULE.select_checkpoint( - [str(staged / "*.ckpt"), str(live / "*.ckpt")], - root, - validator, - ) - self.assertEqual(selected.path, resumed.resolve()) - - def test_fresh_checkpoint_requires_strictly_higher_global_step(self): - def info(path: str, step: int, digest: str) -> MODULE.CheckpointInfo: - return MODULE.CheckpointInfo( - path=Path(path), - sha256=digest, - global_step=step, - size=1, - mtime_ns=1, - inode=1, - metadata={"global_step": step}, - ) - - baseline = info("/tmp/run/a.ckpt", 10, "a" * 64) - self.assertFalse(MODULE.is_fresh(None, baseline)) - self.assertFalse( - MODULE.is_fresh(info("/tmp/run/b.ckpt", 9, "b" * 64), baseline) - ) - self.assertFalse( - MODULE.is_fresh(info("/tmp/run/b.ckpt", 10, "a" * 64), baseline) - ) - self.assertFalse( - MODULE.is_fresh(info("/tmp/run/a.ckpt", 10, "b" * 64), baseline) - ) - self.assertTrue( - MODULE.is_fresh(info("/tmp/run/a.ckpt", 11, "a" * 64), baseline) - ) - - def test_semantic_maximum_same_step_fork_is_rejected(self): - def info(path: str, digest: str) -> MODULE.CheckpointInfo: - return MODULE.CheckpointInfo( - path=Path(path), - sha256=digest, - global_step=10, - size=1, - mtime_ns=1, - inode=1, - metadata={"global_step": 10}, - ) - - with self.assertRaisesRegex(SystemExit, "semantic-maximum checkpoints"): - MODULE.newest_checkpoint( - info("/tmp/run/a.ckpt", "a" * 64), - info("/tmp/run/b.ckpt", "b" * 64), - ) - - def test_checkpoint_forwarding_is_exact_scancel_and_never_killpg(self): - completed = subprocess.CompletedProcess([], 0, stdout="", stderr="") - with mock.patch.dict( - os.environ, - {"SCANCEL_BATCH": "1", "SCANCEL_FULL": "1", "SLURM_CLUSTERS": "wrong"}, - ): - with ( - mock.patch.object( - MODULE.subprocess, "run", return_value=completed - ) as run, - mock.patch.object(MODULE.os, "killpg") as killpg, - ): - result = MODULE.forward_checkpoint_to_slurm_steps( - "/mock/bin/scancel", - "12345", - signal.SIGUSR2, - ) - self.assertIs(result, completed) - run.assert_called_once() - positional, keyword = run.call_args - self.assertEqual(positional, (["/mock/bin/scancel", "--signal=USR2", "12345"],)) - self.assertEqual(keyword["text"], True) - self.assertIs(keyword["stdout"], subprocess.PIPE) - self.assertIs(keyword["stderr"], subprocess.PIPE) - self.assertEqual(keyword["check"], False) - self.assertNotIn("SCANCEL_BATCH", keyword["env"]) - self.assertNotIn("SCANCEL_FULL", keyword["env"]) - self.assertNotIn("SLURM_CLUSTERS", keyword["env"]) - killpg.assert_not_called() - - -class RunnerSafetyTest(unittest.TestCase): - def test_signal_handlers_are_installed_before_child_spawn(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - state = root / "state" / "run" - validator = make_validator(root) - checkpoint_path = root / "step-1.ckpt" - write_checkpoint(checkpoint_path, 1) - checkpoint = MODULE.stable_checkpoint_info( - checkpoint_path.resolve(), validator - ) - self.assertIsNotNone(checkpoint) - installed: list[signal.Signals] = [] - gate_readers: list[int] = [] - - class FinishedChild: - pid = 9876 - - @staticmethod - def poll() -> int: - while gate_readers: - os.close(gate_readers.pop()) - return 0 - - def install(received: signal.Signals, _handler: object) -> object: - installed.append(received) - return signal.SIG_DFL - - def spawn(*_args: object, **kwargs: object) -> FinishedChild: - self.assertEqual( - installed, - [signal.SIGUSR1, signal.SIGTERM, signal.SIGINT], - ) - pass_fds = kwargs["pass_fds"] - self.assertIsInstance(pass_fds, tuple) - gate_readers.append(os.dup(pass_fds[0])) - return FinishedChild() - - def validate(*_args: object, **_kwargs: object) -> MODULE.CheckpointInfo: - self.assertEqual( - installed, - [signal.SIGUSR1, signal.SIGTERM, signal.SIGINT], - ) - return checkpoint - - args = base_args(state, str(root / "*.ckpt"), validator, ["unused-child"]) - with ( - mock.patch.dict( - os.environ, - {"SLURM_JOB_ID": "990", "SLURM_RESTART_COUNT": "0"}, - clear=False, - ), - mock.patch.object(MODULE.signal, "signal", side_effect=install), - mock.patch.object( - MODULE, "stable_checkpoint_info", side_effect=validate - ), - mock.patch.object( - MODULE.subprocess, "Popen", side_effect=spawn - ) as popen, - ): - self.assertEqual(MODULE.main(args), 0) - popen.assert_called_once() - - def test_usr1_during_strict_discovery_requeues_without_starting_work(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - state = root / "state" / "run" - checkpoint = root / "step-1.ckpt" - write_checkpoint(checkpoint, 1) - early_signal_marker = root / "early-signal-sent" - validator = root / "early_signal_validator.py" - validator.write_text( - "#!/usr/bin/env python3\n" - "import json,os,signal,sys,time\n" - "marker=os.environ['EARLY_SIGNAL_MARKER']\n" - "if not os.path.exists(marker):\n" - " open(marker,'w').write('sent')\n" - " os.kill(os.getppid(),signal.SIGUSR1)\n" - " time.sleep(0.1)\n" - "with open(sys.argv[1]) as stream: data=json.load(stream)\n" - "print(json.dumps({'global_step':data['global_step'],'run_id':data['run_id']}))\n" - ) - validator.chmod(0o700) - child_started = root / "child-started" - child = root / "child.py" - child.write_text( - "import os\n" "open(os.environ['CHILD_STARTED'],'w').write('unsafe')\n" - ) - attempt = state / "requeue" / "job-12400-restart-000" / "attempt.json" - scancel_log = root / "scancel.log" - fake_scancel = root / "scancel" - fake_scancel.write_text( - "#!/bin/sh\n" 'printf \'%s\\n\' "$*" > "$ICE_TEST_SCANCEL_LOG"\n' - ) - fake_scancel.chmod(0o700) - scontrol_log = root / "scontrol.log" - fake_scontrol = root / "scontrol" - fake_scontrol.write_text( - "#!/bin/sh\n" 'printf \'%s\\n\' "$*" > "$SCONTROL_LOG"\n' - ) - fake_scontrol.chmod(0o700) - env = os.environ.copy() - env.update( - { - "SLURM_JOB_ID": "12400", - "SLURM_RESTART_COUNT": "0", - "EARLY_SIGNAL_MARKER": str(early_signal_marker), - "CHILD_STARTED": str(child_started), - "ICE_TEST_SCANCEL_LOG": str(scancel_log), - "SCONTROL_LOG": str(scontrol_log), - } - ) - args = base_args( - state, - str(root / "*.ckpt"), - validator, - [sys.executable, str(child)], - owner="runner", - scancel=fake_scancel, - ) - args[args.index("--") : args.index("--")] = [ - "--scontrol", - str(fake_scontrol), - ] - result = subprocess.run( - [sys.executable, str(MODULE_PATH), *args], - env=env, - text=True, - capture_output=True, - timeout=10, - check=False, - ) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertTrue(early_signal_marker.is_file()) - self.assertFalse(child_started.exists()) - self.assertFalse(scancel_log.exists()) - self.assertEqual(scontrol_log.read_text().strip(), "requeue 12400") - final = json.loads(attempt.read_text()) - self.assertEqual(final["status"], "EARLY_BOUNDARY_REQUEUE_ACCEPTED") - self.assertNotIn("child_pid", final) - proof_path = Path(final["early_boundary_proof"]) - proof = json.loads(proof_path.read_text()) - self.assertEqual( - final["early_boundary_proof_sha256"], - hashlib.sha256(proof_path.read_bytes()).hexdigest(), - ) - self.assertEqual( - proof["status"], - "EARLY_BOUNDARY_NO_WORK_CHECKPOINT_AUTHENTICATED", - ) - self.assertFalse(proof["child_spawned"]) - self.assertFalse(proof["optimizer_work_performed"]) - self.assertFalse(proof["checkpoint_signal_forwarded"]) - self.assertEqual(proof["checkpoint"]["global_step"], 1) - events = [ - json.loads(line) - for line in (attempt.parent / "events.jsonl").read_text().splitlines() - ] - self.assertEqual( - [event["event"] for event in events].count( - "early_boundary_no_work_checkpoint_authenticated" - ), - 1, - ) - - def test_incremental_restart_revalidates_high_water_and_only_new_candidates(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - state = root / "state" / "run" - validator = make_validator(root) - checkpoints = [root / f"step-{step}.ckpt" for step in (10, 20, 30)] - for step, checkpoint in zip((10, 20, 30), checkpoints): - write_checkpoint(checkpoint, step) - log = root / "validator.log" - args = base_args(state, str(root / "*.ckpt"), validator, ["/bin/true"]) - args.insert(args.index("--"), "--dry-run") - - with mock.patch.dict( - os.environ, - {"SLURM_RESTART_COUNT": "0", "VALIDATOR_LOG": str(log)}, - clear=False, - ): - self.assertEqual(MODULE.main(args), 0) - first = log.read_text().splitlines() - self.assertEqual( - set(first), {str(path.resolve()) for path in checkpoints} - ) - self.assertEqual(len(first), 3) - - log.write_text("") - self.assertEqual(MODULE.main(args), 0) - self.assertEqual( - log.read_text().splitlines(), [str(checkpoints[-1].resolve())] - ) - - newer = root / "step-40.ckpt" - write_checkpoint(newer, 40) - log.write_text("") - self.assertEqual(MODULE.main(args), 0) - self.assertEqual( - set(log.read_text().splitlines()), - {str(checkpoints[-1].resolve()), str(newer.resolve())}, - ) - - log.write_text("") - self.assertEqual(MODULE.main(args), 0) - self.assertEqual(log.read_text().splitlines(), [str(newer.resolve())]) - - def test_unbound_observation_index_forces_validator_authoritative_rescan(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - state = root / "state" / "run" - validator = make_validator(root) - checkpoints = [root / f"step-{step}.ckpt" for step in (10, 20, 30)] - for step, checkpoint in zip((10, 20, 30), checkpoints): - write_checkpoint(checkpoint, step) - log = root / "validator.log" - args = base_args(state, str(root / "*.ckpt"), validator, ["/bin/true"]) - args.insert(args.index("--"), "--dry-run") - with mock.patch.dict( - os.environ, - {"SLURM_RESTART_COUNT": "0", "VALIDATOR_LOG": str(log)}, - clear=False, - ): - self.assertEqual(MODULE.main(args), 0) - index = state / MODULE.OBSERVATION_INDEX_NAME - index.write_text(index.read_text() + "\n") - log.write_text("") - self.assertEqual(MODULE.main(args), 0) - self.assertEqual( - set(log.read_text().splitlines()), - {str(path.resolve()) for path in checkpoints}, - ) - - def test_validator_change_invalidates_observation_index(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - state = root / "state" / "run" - validator = make_validator(root) - checkpoints = [root / f"step-{step}.ckpt" for step in (10, 20)] - for step, checkpoint in zip((10, 20), checkpoints): - write_checkpoint(checkpoint, step) - log = root / "validator.log" - args = base_args(state, str(root / "*.ckpt"), validator, ["/bin/true"]) - args.insert(args.index("--"), "--dry-run") - with mock.patch.dict( - os.environ, - {"SLURM_RESTART_COUNT": "0", "VALIDATOR_LOG": str(log)}, - clear=False, - ): - self.assertEqual(MODULE.main(args), 0) - validator.write_text(validator.read_text() + "# authority revision\n") - log.write_text("") - self.assertEqual(MODULE.main(args), 0) - self.assertEqual( - set(log.read_text().splitlines()), - {str(path.resolve()) for path in checkpoints}, - ) - - def test_validator_change_during_validation_refuses_state_commit(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - state = root / "state" / "run" - checkpoint = root / "step-10.ckpt" - write_checkpoint(checkpoint, 10) - marker = root / "validator-mutated" - validator = root / "mutating_validator.py" - validator.write_text( - "#!/usr/bin/env python3\n" - "import json,os,sys\n" - "marker=os.environ['VALIDATOR_MUTATION_MARKER']\n" - "if not os.path.exists(marker):\n" - " open(marker,'w').write('mutated')\n" - " with open(__file__,'a') as stream: stream.write('# changed\\n')\n" - "with open(sys.argv[1]) as stream: data=json.load(stream)\n" - "print(json.dumps({'global_step':data['global_step'],'run_id':data['run_id']}))\n" - ) - validator.chmod(0o700) - args = base_args(state, str(root / "*.ckpt"), validator, ["/bin/true"]) - args.insert(args.index("--"), "--dry-run") - with mock.patch.dict( - os.environ, - { - "SLURM_RESTART_COUNT": "0", - "VALIDATOR_MUTATION_MARKER": str(marker), - }, - clear=False, - ): - with self.assertRaisesRegex( - SystemExit, "validator changed after validation" - ): - MODULE.main(args) - self.assertTrue(marker.is_file()) - self.assertFalse((state / MODULE.OBSERVATION_INDEX_NAME).exists()) - self.assertFalse((state / "checkpoint-high-water.json").exists()) - - def test_failed_candidate_is_not_cached_when_sidecar_appears(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - state = root / "state" / "run" - validator = root / "sidecar_validator.py" - validator.write_text( - "#!/usr/bin/env python3\n" - "import json,os,sys\n" - "with open(os.environ['VALIDATOR_LOG'],'a') as stream:\n" - " stream.write(os.path.realpath(sys.argv[1])+'\\n')\n" - "if not os.path.isfile(sys.argv[1]+'.proof'): raise SystemExit(4)\n" - "with open(sys.argv[1]) as stream: data=json.load(stream)\n" - "print(json.dumps({'global_step':data['global_step'],'run_id':data['run_id']}))\n" - ) - validator.chmod(0o700) - lower = root / "step-10.ckpt" - higher = root / "step-20.ckpt" - write_checkpoint(lower, 10) - write_checkpoint(higher, 20) - Path(str(lower) + ".proof").write_text("ready") - original_higher_stat = higher.stat() - log = root / "validator.log" - args = base_args(state, str(root / "*.ckpt"), validator, ["/bin/true"]) - args.insert(args.index("--"), "--dry-run") - with mock.patch.dict( - os.environ, - {"SLURM_RESTART_COUNT": "0", "VALIDATOR_LOG": str(log)}, - clear=False, - ): - self.assertEqual(MODULE.main(args), 0) - first = json.loads( - ( - state / "requeue" / "job-dry-run-restart-000" / "attempt.json" - ).read_text() - ) - self.assertEqual(first["checkpoint"]["global_step"], 10) - - Path(str(higher) + ".proof").write_text("ready") - self.assertEqual( - MODULE.stat_identity(higher.stat()), - MODULE.stat_identity(original_higher_stat), - ) - log.write_text("") - self.assertEqual(MODULE.main(args), 0) - self.assertIn(str(higher.resolve()), log.read_text().splitlines()) - final = json.loads( - ( - state / "requeue" / "job-dry-run-restart-000" / "attempt.json" - ).read_text() - ) - self.assertEqual(final["checkpoint"]["global_step"], 20) - - def test_atomic_replacement_with_preserved_size_and_mtime_is_revalidated(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - state = root / "state" / "run" - validator = make_validator(root) - historical = root / "step-10.ckpt" - high_water = root / "step-20.ckpt" - write_checkpoint(historical, 10) - write_checkpoint(high_water, 20) - args = base_args(state, str(root / "*.ckpt"), validator, ["/bin/true"]) - args.insert(args.index("--"), "--dry-run") - with mock.patch.dict(os.environ, {"SLURM_RESTART_COUNT": "0"}, clear=False): - self.assertEqual(MODULE.main(args), 0) - - original = historical.stat() - replacement = root / "replacement.ckpt" - write_checkpoint(replacement, 30) - self.assertEqual(replacement.stat().st_size, original.st_size) - os.utime(replacement, ns=(original.st_atime_ns, original.st_mtime_ns)) - os.replace(replacement, historical) - replaced = historical.stat() - self.assertEqual(replaced.st_size, original.st_size) - self.assertEqual(replaced.st_mtime_ns, original.st_mtime_ns) - self.assertNotEqual( - (replaced.st_dev, replaced.st_ino, replaced.st_ctime_ns), - (original.st_dev, original.st_ino, original.st_ctime_ns), - ) - - with mock.patch.dict(os.environ, {"SLURM_RESTART_COUNT": "1"}, clear=False): - self.assertEqual(MODULE.main(args), 0) - final = json.loads( - ( - state / "requeue" / "job-dry-run-restart-001" / "attempt.json" - ).read_text() - ) - self.assertEqual(final["checkpoint"]["global_step"], 30) - - def test_runner_ownership_requires_explicit_child_disable_confirmation(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - state = root / "state" / "run" - validator = make_validator(root) - args = base_args( - state, - str(root / "*.ckpt"), - validator, - ["/bin/true"], - owner="runner", - ) - args.remove("--confirm-child-requeue-disabled") - args.insert(args.index("--"), "--dry-run") - with self.assertRaisesRegex(SystemExit, "disable framework auto-requeue"): - MODULE.main(args) - - def test_required_initial_checkpoint_is_refused(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - state = root / "state" / "run" - validator = make_validator(root) - args = base_args(state, str(root / "*.ckpt"), validator, ["/bin/true"]) - args.insert(-2, "--require-initial-checkpoint") - args.insert(-2, "--dry-run") - with mock.patch.dict(os.environ, {"SLURM_RESTART_COUNT": "0"}, clear=False): - with self.assertRaisesRegex( - SystemExit, "requires an exact --initial-checkpoint" - ): - MODULE.main(args) - - def test_exact_initial_pins_first_attempt_then_live_wins_restart(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - state = root / "state" / "run" - staged = root / "staged" - live = root / "live" - staged.mkdir() - live.mkdir() - validator = make_validator(root) - initial = staged / "step-40000.ckpt" - newer_live = live / "step-60000.ckpt" - write_checkpoint(initial, 40_000) - write_checkpoint(newer_live, 60_000) - args = base_args( - state, - str(live / "*.ckpt"), - validator, - ["/bin/true"], - initial_checkpoint=initial, - ) - args.insert(args.index("--"), "--require-initial-checkpoint") - args.insert(args.index("--"), "--dry-run") - - with mock.patch.dict(os.environ, {"SLURM_RESTART_COUNT": "0"}, clear=False): - self.assertEqual(MODULE.main(args), 0) - first = json.loads( - ( - state / "requeue" / "job-dry-run-restart-000" / "attempt.json" - ).read_text() - ) - self.assertEqual(first["checkpoint"]["global_step"], 40_000) - self.assertEqual(first["checkpoint"]["path"], str(initial.resolve())) - - initial.unlink() - with mock.patch.dict(os.environ, {"SLURM_RESTART_COUNT": "1"}, clear=False): - self.assertEqual(MODULE.main(args), 0) - restarted = json.loads( - ( - state / "requeue" / "job-dry-run-restart-001" / "attempt.json" - ).read_text() - ) - self.assertEqual(restarted["checkpoint"]["global_step"], 60_000) - self.assertEqual(restarted["checkpoint"]["path"], str(newer_live.resolve())) - - def test_explicit_missing_initial_checkpoint_fails_closed(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - state = root / "state" / "run" - validator = make_validator(root) - args = base_args( - state, - str(root / "live" / "*.ckpt"), - validator, - ["/bin/true"], - initial_checkpoint=root / "staged" / "missing.ckpt", - ) - args.insert(args.index("--"), "--dry-run") - with self.assertRaisesRegex( - SystemExit, "initial checkpoint is unavailable" - ): - MODULE.main(args) - - def test_restart_without_checkpoint_is_always_refused(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - state = root / "state" / "run" - validator = make_validator(root) - args = base_args(state, str(root / "*.ckpt"), validator, ["/bin/true"]) - args.insert(-2, "--dry-run") - with mock.patch.dict(os.environ, {"SLURM_RESTART_COUNT": "1"}, clear=False): - with self.assertRaisesRegex(SystemExit, "required resume checkpoint"): - MODULE.main(args) - - def test_high_water_refuses_rollback(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - state = root / "state" / "run" - validator = make_validator(root) - newest = root / "step-60000.ckpt" - write_checkpoint(newest, 60_000) - args = base_args(state, str(root / "*.ckpt"), validator, ["/bin/true"]) - args.insert(-2, "--dry-run") - with mock.patch.dict(os.environ, {"SLURM_RESTART_COUNT": "0"}, clear=False): - self.assertEqual(MODULE.main(args), 0) - newest.unlink() - write_checkpoint(root / "step-40000.ckpt", 40_000) - with mock.patch.dict(os.environ, {"SLURM_RESTART_COUNT": "1"}, clear=False): - with self.assertRaisesRegex(SystemExit, "rollback refused"): - MODULE.main(args) - - def test_high_water_refuses_newer_same_step_different_sha(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - state = root / "state" / "run" - validator = make_validator(root) - checkpoint = root / "step-60000.ckpt" - write_checkpoint(checkpoint, 60_000) - args = base_args(state, str(root / "*.ckpt"), validator, ["/bin/true"]) - args.insert(-2, "--dry-run") - with mock.patch.dict(os.environ, {"SLURM_RESTART_COUNT": "0"}, clear=False): - self.assertEqual(MODULE.main(args), 0) - time.sleep(0.01) - checkpoint.unlink() - checkpoint.write_text( - json.dumps( - { - "global_step": 60_000, - "valid": True, - "run_id": "run", - "wandb_run_id": "wandb-run", - "different_payload": True, - } - ) - ) - with mock.patch.dict(os.environ, {"SLURM_RESTART_COUNT": "1"}, clear=False): - with self.assertRaisesRegex(SystemExit, "identity fork refused"): - MODULE.main(args) - - def test_state_directory_lock_refuses_duplicate_runner(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - state = root / "state" / "run" - state.mkdir(parents=True) - validator = make_validator(root) - write_checkpoint(root / "step-1.ckpt", 1) - lock = (state / "runner.lock").open("a+") - fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) - try: - args = base_args(state, str(root / "*.ckpt"), validator, ["/bin/true"]) - args.insert(args.index("--"), "--dry-run") - command = [ - sys.executable, - str(MODULE_PATH), - *args, - ] - result = subprocess.run( - command, text=True, capture_output=True, check=False - ) - finally: - lock.close() - self.assertNotEqual(result.returncode, 0) - self.assertIn("another requeue runner", result.stderr) - - def test_exports_checkpoint_path_sha_step_and_metadata(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - state = root / "state" / "run" - validator = make_validator(root) - checkpoint = root / "step-12.ckpt" - write_checkpoint(checkpoint, 12) - output = root / "environment.json" - child = root / "child.py" - child.write_text( - "import json,os\n" - "keys=['ICE_RESUME_CHECKPOINT','ICE_RESUME_CHECKPOINT_SHA256'," - "'ICE_RESUME_GLOBAL_STEP','ICE_RESUME_CHECKPOINT_METADATA_JSON'," - "'ICE_REQUEUE_OWNER']\n" - "open(os.environ['OUTPUT'],'w').write(json.dumps({k:os.environ[k] for k in keys}))\n" - ) - env = os.environ.copy() - env.update( - { - "SLURM_JOB_ID": "991", - "SLURM_RESTART_COUNT": "0", - "OUTPUT": str(output), - } - ) - command = [ - sys.executable, - str(MODULE_PATH), - *base_args( - state, str(root / "*.ckpt"), validator, [sys.executable, str(child)] - ), - ] - result = subprocess.run( - command, env=env, text=True, capture_output=True, check=False - ) - self.assertEqual(result.returncode, 0, result.stderr) - exported = json.loads(output.read_text()) - self.assertEqual( - exported["ICE_RESUME_CHECKPOINT"], str(checkpoint.resolve()) - ) - self.assertEqual(exported["ICE_RESUME_GLOBAL_STEP"], "12") - self.assertEqual( - exported["ICE_RESUME_CHECKPOINT_SHA256"], - hashlib.sha256(checkpoint.read_bytes()).hexdigest(), - ) - metadata = json.loads(exported["ICE_RESUME_CHECKPOINT_METADATA_JSON"]) - self.assertEqual(metadata["global_step"], 12) - self.assertEqual(metadata["checkpoint_path"], str(checkpoint.resolve())) - self.assertEqual(exported["ICE_REQUEUE_OWNER"], "child") - - def test_child_signal_has_shell_exit_status_without_requeue(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - state = root / "state" / "run" - validator = make_validator(root) - write_checkpoint(root / "step-12.ckpt", 12) - scontrol_log = root / "scontrol.log" - fake_scontrol = root / "scontrol" - fake_scontrol.write_text( - "#!/bin/sh\n" f"touch {shlex.quote(str(scontrol_log))}\n" - ) - fake_scontrol.chmod(0o700) - env = os.environ.copy() - env.update({"SLURM_JOB_ID": "993", "SLURM_RESTART_COUNT": "0"}) - args = base_args( - state, - str(root / "*.ckpt"), - validator, - [ - sys.executable, - "-c", - "import os,signal; os.kill(os.getpid(), signal.SIGTERM)", - ], - owner="runner", - ) - args[args.index("--") : args.index("--")] = [ - "--scontrol", - str(fake_scontrol), - ] - result = subprocess.run( - [sys.executable, str(MODULE_PATH), *args], - env=env, - text=True, - capture_output=True, - check=False, - timeout=10, - ) - self.assertEqual(result.returncode, 128 + signal.SIGTERM, result.stderr) - self.assertFalse(scontrol_log.exists()) - attempt = json.loads( - (state / "requeue" / "job-993-restart-000" / "attempt.json").read_text() - ) - self.assertEqual(attempt["status"], "CHILD_FAILED_NO_REQUEUE") - self.assertEqual(attempt["child_exit_code"], -signal.SIGTERM) - self.assertEqual(attempt["child_exit_signal"], "SIGTERM") - - def test_signal_baseline_waits_for_post_signal_checkpoint(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - state = root / "state" / "run" - staged = root / "staged" - live = root / "live" - staged.mkdir() - live.mkdir() - validator = make_validator(root) - initial = staged / "step-1.ckpt" - write_checkpoint(initial, 1) - ready = root / "ready" - child_pid = root / "child.pid" - resume_seen = root / "resume-seen" - child = root / "child.py" - child.write_text( - "import json,os,signal,time\n" - "root=os.environ['ROOT']\n" - "def write(step):\n" - " p=os.path.join(root,f'step-{step}.ckpt'); t=p+'.tmp'\n" - " open(t,'w').write(json.dumps({'global_step':step,'run_id':'run','wandb_run_id':'wandb-run'}))\n" - " os.replace(t,p)\n" - "def save(*_):\n" - " time.sleep(0.6); write(3); raise SystemExit(0)\n" - "signal.signal(signal.SIGUSR2,save)\n" - "open(os.environ['CHILD_PID_FILE'],'w').write(str(os.getpid()))\n" - "open(os.environ['RESUME_SEEN'],'w').write(os.environ['ICE_RESUME_CHECKPOINT'])\n" - "write(2); open(os.environ['READY'],'w').write('ready')\n" - "while True: time.sleep(0.05)\n" - ) - scancel_log = root / "scancel.log" - fake_scancel = make_signaling_scancel(root) - scontrol_log = root / "scontrol.log" - fake_scontrol = root / "scontrol" - fake_scontrol.write_text( - '#!/bin/sh\nprintf \'%s\\n\' "$*" > "$SCONTROL_LOG"\n' - ) - fake_scontrol.chmod(0o700) - env = os.environ.copy() - env.update( - { - "SLURM_JOB_ID": "12345", - "SLURM_RESTART_COUNT": "0", - "ROOT": str(live), - "READY": str(ready), - "CHILD_PID_FILE": str(child_pid), - "RESUME_SEEN": str(resume_seen), - "ICE_TEST_SCANCEL_LOG": str(scancel_log), - "SCONTROL_LOG": str(scontrol_log), - } - ) - args = base_args( - state, - str(live / "*.ckpt"), - validator, - [sys.executable, str(child)], - owner="runner", - initial_checkpoint=initial, - scancel=fake_scancel, - ) - args[args.index("--poll-seconds") : args.index("--poll-seconds") + 2] = [ - "--poll-seconds", - "0.02", - ] - args[args.index("--") : args.index("--")] = [ - "--checkpoint-grace-seconds", - "4", - "--scontrol", - str(fake_scontrol), - ] - command = [sys.executable, str(MODULE_PATH), *args] - process = subprocess.Popen( - command, - env=env, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - attempt = state / "requeue" / "job-12345-restart-000" / "attempt.json" - deadline = time.time() + 5 - while time.time() < deadline: - if ( - ready.exists() - and attempt.exists() - and json.loads(attempt.read_text()).get("status") == "RUNNING" - ): - break - time.sleep(0.02) - else: - process.kill() - self.fail("runner/child did not become ready") - process.send_signal(signal.SIGUSR1) - time.sleep(0.2) - self.assertFalse( - scontrol_log.exists(), "runner requeued before post-signal save" - ) - stdout, stderr = process.communicate(timeout=10) - self.assertEqual(process.returncode, 0, f"{stdout}\n{stderr}") - self.assertEqual(resume_seen.read_text(), str(initial.resolve())) - self.assertEqual(scancel_log.read_text().strip(), "--signal=USR2 12345") - self.assertEqual(scontrol_log.read_text().strip(), "requeue 12345") - final = json.loads(attempt.read_text()) - self.assertEqual(final["checkpoint"]["global_step"], 3) - self.assertTrue(final["fresh_checkpoint_after_signal"]) - events = [ - json.loads(line) - for line in (attempt.parent / "events.jsonl").read_text().splitlines() - ] - signal_event = next(event for event in events if event["event"] == "signal") - self.assertEqual(signal_event["baseline"]["global_step"], 2) - - def test_runner_refuses_requeue_without_fresh_post_signal_checkpoint(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - state = root / "state" / "run" - staged = root / "staged" - live = root / "live" - staged.mkdir() - live.mkdir() - validator = make_validator(root) - initial = staged / "step-1.ckpt" - write_checkpoint(initial, 1) - ready = root / "ready" - child_pid = root / "child.pid" - child = root / "child.py" - child.write_text( - "import os,signal,time\n" - "signal.signal(signal.SIGUSR2, lambda *_: (_ for _ in ()).throw(SystemExit(0)))\n" - "open(os.environ['CHILD_PID_FILE'],'w').write(str(os.getpid()))\n" - "open(os.environ['READY'],'w').write('ready')\n" - "while True: time.sleep(0.05)\n" - ) - scancel_log = root / "scancel.log" - fake_scancel = make_signaling_scancel(root) - called = root / "scontrol-called" - fake_scontrol = root / "scontrol" - fake_scontrol.write_text('#!/bin/sh\ntouch "$SCONTROL_CALLED"\n') - fake_scontrol.chmod(0o700) - env = os.environ.copy() - env.update( - { - "SLURM_JOB_ID": "12348", - "SLURM_RESTART_COUNT": "0", - "READY": str(ready), - "CHILD_PID_FILE": str(child_pid), - "ICE_TEST_SCANCEL_LOG": str(scancel_log), - "SCONTROL_CALLED": str(called), - } - ) - args = base_args( - state, - str(live / "*.ckpt"), - validator, - [sys.executable, str(child)], - owner="runner", - initial_checkpoint=initial, - scancel=fake_scancel, - ) - args[args.index("--") : args.index("--")] = [ - "--checkpoint-grace-seconds", - "1", - "--scontrol", - str(fake_scontrol), - ] - process = subprocess.Popen( - [sys.executable, str(MODULE_PATH), *args], - env=env, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - attempt = state / "requeue" / "job-12348-restart-000" / "attempt.json" - deadline = time.time() + 5 - while time.time() < deadline: - if ( - ready.exists() - and attempt.exists() - and json.loads(attempt.read_text()).get("status") == "RUNNING" - ): - break - time.sleep(0.02) - else: - process.kill() - self.fail("runner/child did not become ready") - process.send_signal(signal.SIGUSR1) - stdout, stderr = process.communicate(timeout=10) - self.assertEqual(process.returncode, 74, f"{stdout}\n{stderr}") - self.assertFalse(called.exists()) - self.assertEqual(scancel_log.read_text().strip(), "--signal=USR2 12348") - final = json.loads(attempt.read_text()) - self.assertEqual(final["status"], "REQUEUE_REFUSED_NO_FRESH_CHECKPOINT") - self.assertFalse(final["fresh_checkpoint_after_signal"]) - - def test_child_requeue_owner_never_calls_scontrol(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - state = root / "state" / "run" - staged = root / "staged" - live = root / "live" - staged.mkdir() - live.mkdir() - validator = make_validator(root) - initial = staged / "step-1.ckpt" - write_checkpoint(initial, 1) - child_pid = root / "child.pid" - child = root / "child.py" - child.write_text( - "import json,os,signal,time\n" - "def save(*_):\n" - " p=os.path.join(os.environ['ROOT'],'step-2.ckpt'); t=p+'.tmp'\n" - " open(t,'w').write(json.dumps({'global_step':2,'run_id':'run','wandb_run_id':'wandb-run'}))\n" - " os.replace(t,p); raise SystemExit(0)\n" - "signal.signal(signal.SIGUSR2,save)\n" - "open(os.environ['CHILD_PID_FILE'],'w').write(str(os.getpid()))\n" - "while True: time.sleep(0.05)\n" - ) - scancel_log = root / "scancel.log" - fake_scancel = make_signaling_scancel(root) - called = root / "scontrol-called" - fake_scontrol = root / "scontrol" - fake_scontrol.write_text('#!/bin/sh\ntouch "$SCONTROL_CALLED"\n') - fake_scontrol.chmod(0o700) - env = os.environ.copy() - env.update( - { - "SLURM_JOB_ID": "12346", - "SLURM_RESTART_COUNT": "0", - "ROOT": str(live), - "CHILD_PID_FILE": str(child_pid), - "ICE_TEST_SCANCEL_LOG": str(scancel_log), - "SCONTROL_CALLED": str(called), - } - ) - args = base_args( - state, - str(live / "*.ckpt"), - validator, - [sys.executable, str(child)], - owner="child", - initial_checkpoint=initial, - scancel=fake_scancel, - ) - args[args.index("--") : args.index("--")] = [ - "--checkpoint-grace-seconds", - "3", - "--scontrol", - str(fake_scontrol), - ] - process = subprocess.Popen( - [sys.executable, str(MODULE_PATH), *args], - env=env, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - attempt = state / "requeue" / "job-12346-restart-000" / "attempt.json" - deadline = time.time() + 5 - while time.time() < deadline: - if ( - child_pid.exists() - and attempt.exists() - and json.loads(attempt.read_text()).get("status") == "RUNNING" - ): - break - time.sleep(0.02) - else: - process.kill() - self.fail("runner did not become ready") - process.send_signal(signal.SIGUSR1) - stdout, stderr = process.communicate(timeout=10) - self.assertEqual(process.returncode, 0, f"{stdout}\n{stderr}") - self.assertFalse(called.exists()) - self.assertEqual(scancel_log.read_text().strip(), "--signal=USR2 12346") - final = json.loads(attempt.read_text()) - self.assertEqual(final["status"], "COMPLETE") - - def test_duplicate_usr1_boundary_invokes_scancel_only_once(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - state = root / "state" / "run" - staged = root / "staged" - live = root / "live" - staged.mkdir() - live.mkdir() - validator = make_validator(root) - initial = staged / "step-1.ckpt" - write_checkpoint(initial, 1) - ready = root / "ready" - saved = root / "saved" - child_pid = root / "child.pid" - child = root / "child.py" - child.write_text( - "import json,os,signal,time\n" - "def save(*_):\n" - " p=os.path.join(os.environ['ROOT'],'step-2.ckpt'); t=p+'.tmp'\n" - " open(t,'w').write(json.dumps({'global_step':2,'run_id':'run','wandb_run_id':'wandb-run'}))\n" - " os.replace(t,p); open(os.environ['SAVED'],'w').write('saved')\n" - "signal.signal(signal.SIGUSR2,save)\n" - "open(os.environ['CHILD_PID_FILE'],'w').write(str(os.getpid()))\n" - "open(os.environ['READY'],'w').write('ready')\n" - "while True: time.sleep(0.05)\n" - ) - scancel_log = root / "scancel.log" - fake_scancel = root / "scancel" - fake_scancel.write_text( - "#!/bin/sh\n" - 'printf \'%s\\n\' "$*" >> "$ICE_TEST_SCANCEL_LOG"\n' - 'kill -USR2 "$(cat "$CHILD_PID_FILE")"\n' - ) - fake_scancel.chmod(0o700) - env = os.environ.copy() - env.update( - { - "SLURM_JOB_ID": "12350", - "SLURM_RESTART_COUNT": "0", - "ROOT": str(live), - "READY": str(ready), - "SAVED": str(saved), - "CHILD_PID_FILE": str(child_pid), - "ICE_TEST_SCANCEL_LOG": str(scancel_log), - } - ) - args = base_args( - state, - str(live / "*.ckpt"), - validator, - [sys.executable, str(child)], - owner="child", - initial_checkpoint=initial, - scancel=fake_scancel, - ) - process = subprocess.Popen( - [sys.executable, str(MODULE_PATH), *args], - env=env, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - attempt = state / "requeue" / "job-12350-restart-000" / "attempt.json" - deadline = time.time() + 5 - while time.time() < deadline: - if ready.exists() and attempt.exists(): - break - time.sleep(0.02) - else: - process.kill() - self.fail("runner/child did not become ready") - process.send_signal(signal.SIGUSR1) - deadline = time.time() + 5 - while time.time() < deadline: - if ( - saved.exists() - and json.loads(attempt.read_text()).get("status") - == "CHILD_REQUEUE_OWNER_CHECKPOINT_READY" - ): - break - time.sleep(0.02) - else: - process.kill() - self.fail("first checkpoint boundary did not complete") - process.send_signal(signal.SIGUSR1) - time.sleep(0.2) - self.assertEqual( - scancel_log.read_text().splitlines(), ["--signal=USR2 12350"] - ) - process.send_signal(signal.SIGTERM) - stdout, stderr = process.communicate(timeout=10) - self.assertEqual( - process.returncode, 128 + signal.SIGTERM, f"{stdout}\n{stderr}" - ) - - def test_scancel_failure_is_persisted_and_never_requeues(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - state = root / "state" / "run" - staged = root / "staged" - live = root / "live" - staged.mkdir() - live.mkdir() - validator = make_validator(root) - initial = staged / "step-1.ckpt" - write_checkpoint(initial, 1) - ready = root / "ready" - child_pid = root / "child.pid" - child = root / "child.py" - child.write_text( - "import os,time\n" - "open(os.environ['CHILD_PID_FILE'],'w').write(str(os.getpid()))\n" - "open(os.environ['READY'],'w').write('ready')\n" - "while True: time.sleep(0.05)\n" - ) - scancel_log = root / "scancel.log" - fake_scancel = root / "scancel" - fake_scancel.write_text( - "#!/bin/sh\n" - 'printf \'%s\\n\' "$*" > "$ICE_TEST_SCANCEL_LOG"\n' - 'kill -TERM "$(cat "$CHILD_PID_FILE")"\n' - "echo signal-refused >&2\n" - "exit 9\n" - ) - fake_scancel.chmod(0o700) - scontrol_called = root / "scontrol-called" - fake_scontrol = root / "scontrol" - fake_scontrol.write_text('#!/bin/sh\ntouch "$SCONTROL_CALLED"\n') - fake_scontrol.chmod(0o700) - env = os.environ.copy() - env.update( - { - "SLURM_JOB_ID": "12349", - "SLURM_RESTART_COUNT": "0", - "READY": str(ready), - "CHILD_PID_FILE": str(child_pid), - "ICE_TEST_SCANCEL_LOG": str(scancel_log), - "SCONTROL_CALLED": str(scontrol_called), - } - ) - args = base_args( - state, - str(live / "*.ckpt"), - validator, - [sys.executable, str(child)], - owner="runner", - initial_checkpoint=initial, - scancel=fake_scancel, - ) - args[args.index("--") : args.index("--")] = [ - "--scontrol", - str(fake_scontrol), - ] - process = subprocess.Popen( - [sys.executable, str(MODULE_PATH), *args], - env=env, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - attempt = state / "requeue" / "job-12349-restart-000" / "attempt.json" - deadline = time.time() + 5 - while time.time() < deadline: - if ready.exists() and attempt.exists(): - break - time.sleep(0.02) - else: - process.kill() - self.fail("runner/child did not become ready") - process.send_signal(signal.SIGUSR1) - stdout, stderr = process.communicate(timeout=10) - self.assertEqual(process.returncode, 9, f"{stdout}\n{stderr}") - self.assertEqual(scancel_log.read_text().strip(), "--signal=USR2 12349") - self.assertFalse(scontrol_called.exists()) - final = json.loads(attempt.read_text()) - self.assertEqual(final["status"], "CHECKPOINT_SIGNAL_FORWARD_FAILED") - self.assertEqual(final["checkpoint_signal_returncode"], 9) - self.assertEqual( - final["checkpoint_signal_argv"], - [str(fake_scancel.resolve()), "--signal=USR2", "12349"], - ) - self.assertIn("signal-refused", final["checkpoint_signal_stderr"]) - - def test_scontrol_failure_is_persisted(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - state = root / "state" / "run" - staged = root / "staged" - live = root / "live" - staged.mkdir() - live.mkdir() - validator = make_validator(root) - initial = staged / "step-1.ckpt" - write_checkpoint(initial, 1) - child_pid = root / "child.pid" - child = root / "child.py" - child.write_text( - "import json,os,signal,time\n" - "def save(*_):\n" - " p=os.path.join(os.environ['ROOT'],'step-2.ckpt'); t=p+'.tmp'\n" - " open(t,'w').write(json.dumps({'global_step':2,'run_id':'run','wandb_run_id':'wandb-run'}))\n" - " os.replace(t,p); raise SystemExit(0)\n" - "signal.signal(signal.SIGUSR2,save)\n" - "open(os.environ['CHILD_PID_FILE'],'w').write(str(os.getpid()))\n" - "while True: time.sleep(0.05)\n" - ) - scancel_log = root / "scancel.log" - fake_scancel = make_signaling_scancel(root) - fake_scontrol = root / "scontrol" - fake_scontrol.write_text("#!/bin/sh\necho refused >&2\nexit 7\n") - fake_scontrol.chmod(0o700) - env = os.environ.copy() - env.update( - { - "SLURM_JOB_ID": "12347", - "SLURM_RESTART_COUNT": "0", - "ROOT": str(live), - "CHILD_PID_FILE": str(child_pid), - "ICE_TEST_SCANCEL_LOG": str(scancel_log), - } - ) - args = base_args( - state, - str(live / "*.ckpt"), - validator, - [sys.executable, str(child)], - owner="runner", - initial_checkpoint=initial, - scancel=fake_scancel, - ) - args[args.index("--") : args.index("--")] = [ - "--checkpoint-grace-seconds", - "3", - "--scontrol", - str(fake_scontrol), - ] - process = subprocess.Popen( - [sys.executable, str(MODULE_PATH), *args], - env=env, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - attempt = state / "requeue" / "job-12347-restart-000" / "attempt.json" - deadline = time.time() + 5 - while time.time() < deadline: - if ( - child_pid.exists() - and attempt.exists() - and json.loads(attempt.read_text()).get("status") == "RUNNING" - ): - break - time.sleep(0.02) - else: - process.kill() - self.fail("runner did not become ready") - process.send_signal(signal.SIGUSR1) - stdout, stderr = process.communicate(timeout=10) - self.assertEqual(process.returncode, 7, f"{stdout}\n{stderr}") - final = json.loads(attempt.read_text()) - self.assertEqual(final["status"], "REQUEUE_FAILED") - self.assertEqual(final["scontrol_returncode"], 7) - self.assertIn("refused", final["scontrol_stderr"]) - - def test_completion_sentinel_contains_terminal_checkpoint(self): - with tempfile.TemporaryDirectory() as raw: - root = Path(raw) - state = root / "state" / "run" - validator = make_validator(root) - write_checkpoint(root / "step-5.ckpt", 5) - sentinel = root / "run" / "COMPLETE.json" - child = root / "child.py" - child.write_text( - "import json,os\n" - "p=os.path.join(os.environ['ROOT'],'step-6.ckpt'); t=p+'.tmp'\n" - "open(t,'w').write(json.dumps({'global_step':6,'run_id':'run','wandb_run_id':'wandb-run'}))\n" - "os.replace(t,p)\n" - ) - env = os.environ.copy() - env.update( - {"SLURM_JOB_ID": "992", "SLURM_RESTART_COUNT": "0", "ROOT": str(root)} - ) - args = base_args( - state, str(root / "*.ckpt"), validator, [sys.executable, str(child)] - ) - args[args.index("--") : args.index("--")] = [ - "--completion-sentinel", - str(sentinel), - ] - result = subprocess.run( - [sys.executable, str(MODULE_PATH), *args], - env=env, - text=True, - capture_output=True, - check=False, - ) - self.assertEqual(result.returncode, 0, result.stderr) - payload = json.loads(sentinel.read_text()) - self.assertEqual(payload["status"], "COMPLETE") - self.assertEqual(payload["checkpoint"]["global_step"], 6) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_ice_wandb_resume.py b/tests/test_ice_wandb_resume.py deleted file mode 100644 index cf04f0d74c..0000000000 --- a/tests/test_ice_wandb_resume.py +++ /dev/null @@ -1,118 +0,0 @@ -from pathlib import Path - -import pytest -from lightning import LightningModule -from omegaconf import OmegaConf - -from egomimic.pl_utils.pl_model import ModelWrapper -from egomimic.utils.logging_utils import configure_runner_wandb - - -def _cfg(tmp_path: Path, *, run_id="stable-run", checkpoint=None): - return OmegaConf.create( - { - "paths": {"output_dir": str(tmp_path)}, - "ckpt_path": checkpoint, - "logger": { - "wandb": { - "_target_": "lightning.pytorch.loggers.wandb.WandbLogger", - "id": run_id, - "entity": "rl2-group", - "project": "abc-yam", - } - }, - } - ) - - -def test_explicit_checkpoint_preserves_fresh_wandb_identity(tmp_path): - checkpoint = tmp_path / "source.ckpt" - checkpoint.touch() - cfg = _cfg(tmp_path, checkpoint=str(checkpoint)) - cfg.logger.wandb.resume = "never" - (tmp_path / "wandb-run-id.txt").write_text("old-run\n") - - configure_runner_wandb(cfg, {"SLURM_RESTART_COUNT": "0"}) - - assert cfg.logger.wandb.id == "stable-run" - assert cfg.logger.wandb.resume == "never" - - -def test_lightning_restart_recovers_sidecar_and_last_checkpoint(tmp_path): - checkpoint = tmp_path / "checkpoints" / "last.ckpt" - checkpoint.parent.mkdir() - checkpoint.touch() - (tmp_path / "wandb-run-id.txt").write_text("original-run\n") - cfg = _cfg(tmp_path, run_id="${now:%Y%m%d}") - - configure_runner_wandb(cfg, {"SLURM_RESTART_COUNT": "2"}) - - assert cfg.ckpt_path == str(checkpoint) - assert cfg.logger.wandb.id == "original-run" - assert cfg.logger.wandb.resume == "must" - - -def test_resume_rejects_conflicting_wandb_identities(tmp_path): - checkpoint = tmp_path / "source.ckpt" - checkpoint.touch() - (tmp_path / "wandb-run-id.txt").write_text("sidecar-run\n") - cfg = _cfg(tmp_path, run_id="configured-run", checkpoint=str(checkpoint)) - - with pytest.raises(ValueError, match="conflicting W&B run identities"): - configure_runner_wandb( - cfg, {"SLURM_RESTART_COUNT": "1", "ICE_RESUME_CHECKPOINT": str(checkpoint)} - ) - - -def test_offline_evaluation_keeps_its_separate_wandb_run(tmp_path): - cfg = _cfg(tmp_path, run_id="validation-run", checkpoint="source.ckpt") - cfg.mode = "eval" - configure_runner_wandb(cfg, {"SLURM_RESTART_COUNT": "0"}) - assert cfg.logger.wandb.id == "validation-run" - assert "resume" not in cfg.logger.wandb - - -def test_lightning_resume_recovers_actual_wandb_artifact_filename(tmp_path): - artifact = ( - tmp_path / "wandb/run-20260922_120000-original-run/run-original-run.wandb" - ) - artifact.parent.mkdir(parents=True) - artifact.write_bytes(b"wandb") - cfg = _cfg(tmp_path, run_id="${now:%Y%m%d}", checkpoint="source.ckpt") - configure_runner_wandb( - cfg, {"SLURM_RESTART_COUNT": "1", "ICE_RESUME_CHECKPOINT": "source.ckpt"} - ) - assert cfg.logger.wandb.id == "original-run" - assert cfg.logger.wandb.resume == "must" - - -def test_model_wrapper_disables_dataloader_suffix_by_default(monkeypatch): - observed = [] - - def fake_log(self, *args, **kwargs): - observed.append(kwargs) - - monkeypatch.setattr(LightningModule, "log", fake_log) - wrapper = ModelWrapper.__new__(ModelWrapper) - LightningModule.__init__(wrapper) - - wrapper.log("metric", 1.0) - wrapper.log("explicit", 1.0, add_dataloader_idx=True) - - assert observed[0]["add_dataloader_idx"] is False - assert observed[1]["add_dataloader_idx"] is True - - -def test_model_wrapper_log_dict_disables_dataloader_suffix(monkeypatch): - observed = {} - - def fake_log_dict(self, *args, **kwargs): - observed.update(kwargs) - - monkeypatch.setattr(LightningModule, "log_dict", fake_log_dict) - wrapper = ModelWrapper.__new__(ModelWrapper) - LightningModule.__init__(wrapper) - - wrapper.log_dict({"metric": 1.0}) - - assert observed["add_dataloader_idx"] is False diff --git a/tests/test_imported_recipe_framework_parity.py b/tests/test_imported_recipe_framework_parity.py deleted file mode 100644 index 475fac391f..0000000000 --- a/tests/test_imported_recipe_framework_parity.py +++ /dev/null @@ -1,68 +0,0 @@ -"""Imported recipes retain the exact source7c scientific payload. - -Hashes were extracted from immutable7c253e5 YAML after removing only the -framework target. This remains executable in shallow CI without source fetches. -""" - -import hashlib -import json -from pathlib import Path - -import pytest -import yaml - - -@pytest.mark.parametrize( - "name,expected", - [ - ( - "us_action_flow_bc_latent_fm_sg_200m_adamw", - "a2007390f6497a04198308190bb259983eee4d75a9beed810926152e5c2f2fb9", - ), - ( - "us_action_flow_bc_graph_section", - "bca57f3f847f55fb4c06e7b53af8ddd22e6d7e9252d288177a18c148e8d91100", - ), - ( - "us_action_flow_bc_latent_fm_sg_200m_muon", - "0d7e4b03b9c5e1321ee009b78a0a89ad5ba9b954402f75833430a3bc5fb0239e", - ), - ( - "us_action_flow_bc_latent_fm_sg", - "4cce3e22ad1a379ec2dfca757631f60d320e903e0a86d2c5cde02def3ab249e3", - ), - ( - "us_action_flow_bc_nt16_d8_h512", - "d909b72505196be3c46d7dfb78b37894e55a9f4f6b812c61af07bb86325a9b00", - ), - ( - "cotrain_uc_action_flow_latent_fm_sg_unite_h384d12h12", - "caba2d2a9432ef36876405a4affa76dc3d32c93ad8728de66cf58d7b9c4fb615", - ), - ], -) -def test_framework_port_keeps_every_scientific_field(name, expected): - root = Path(__file__).resolve().parents[1] - cfg = yaml.safe_load( - (root / "egomimic/hydra_configs/model/bf" / (name + ".yaml")).read_text() - ) - assert cfg.pop("_target_") == "egomimic.pl_utils.pl_model.ModelWrapper" - assert cfg.pop("training_behavior") == { - "_target_": "egomimic.pl_utils.training_behavior_action_flow.ActionFlowTrainingBehavior" - } - assert cfg.pop("diagnostic_provider") == { - "_target_": "egomimic.eval.pipeline_diagnostics.ActionFlowDiagnosticProvider" - } - if name in { - "us_action_flow_bc_latent_fm_sg_200m_adamw", - "us_action_flow_bc_latent_fm_sg_200m_muon", - }: - # Main adds an explicit non-deployable declaration, not a new adapter. - assert cfg.pop("inference") == { - "status": "unsupported", - "reason": "This simulation recipe has no deployment observation/action adapter. Use its configured simulator evaluator; declare an inference adapter before deployment.", - } - actual = hashlib.sha256( - json.dumps(cfg, sort_keys=True, separators=(",", ":")).encode() - ).hexdigest() - assert actual == expected diff --git a/tests/test_installed_package.py b/tests/test_installed_package.py deleted file mode 100644 index 9ba6a94ecf..0000000000 --- a/tests/test_installed_package.py +++ /dev/null @@ -1,102 +0,0 @@ -"""Build independently and check wheel resources without checkout fallbacks.""" - -import json -import os -import shutil -import subprocess -import sys -import zipfile -from pathlib import Path - -import pytest - -ROOT = Path(__file__).resolve().parents[1] - - -@pytest.fixture(scope="module") -def installed_wheel(tmp_path_factory): - work = tmp_path_factory.mktemp("package") - source = work / "source" - source.mkdir() - shutil.copytree( - ROOT / "egomimic", - source / "egomimic", - ignore=shutil.ignore_patterns("__pycache__", "*.pyc"), - ) - for name in ("pyproject.toml", "README.md", "LICENSE"): - if (ROOT / name).exists(): - shutil.copy2(ROOT / name, source / name) - proc = subprocess.run( - ["uv", "build", "--wheel", "--out-dir", str(work / "dist")], - cwd=source, - text=True, - capture_output=True, - timeout=240, - ) - assert proc.returncode == 0, proc.stderr[-4000:] - wheel = next((work / "dist").glob("*.whl")) - with zipfile.ZipFile(wheel) as archive: - names = archive.namelist() - subprocess.run( - ["uv", "venv", str(work / "env"), "--python", sys.executable], - check=True, - capture_output=True, - ) - python = work / "env/bin/python" - subprocess.run( - ["uv", "pip", "install", "--python", str(python), "--no-deps", str(wheel)], - check=True, - capture_output=True, - ) - return work, python, names - - -def test_all_declared_resources_are_in_wheel(installed_wheel): - _, _, names = installed_wheel - required = set() - for folder in ( - "hydra_configs", - "resources", - "robot/teleop_dashboard_static", - "robot/rollout_dashboard_static", - ): - required.update( - str(path.relative_to(ROOT)) - for path in (ROOT / "egomimic" / folder).rglob("*") - if path.suffix - in {".yaml", ".json", ".xml", ".urdf", ".html", ".css", ".js"} - ) - required.update( - str(path.relative_to(ROOT)) - for path in (ROOT / "egomimic/robot/eva/stanford_repo/models/meshes/X5").glob( - "*.STL" - ) - ) - assert required <= set(names), sorted(required - set(names)) - assert not any(name.startswith(("tests/", "external/")) for name in names) - - -def test_fresh_install_loads_resources_outside_checkout(installed_wheel): - work, python, names = installed_wheel - resources = [name for name in names if name.startswith("egomimic/resources/")] - code = """ -import egomimic, json, pathlib, sys, xml.etree.ElementTree as ET -root = pathlib.Path(egomimic.__file__).parent -assert 'site-packages' in str(root), root -for name in json.loads(sys.argv[1]): - path = root.parent / name - ET.parse(path) -tree = ET.parse(root / 'resources/model_x5.xml') -meshdir = root / 'resources' / tree.find('compiler').attrib['meshdir'] -for mesh in tree.findall('asset/mesh'): - assert (meshdir / mesh.attrib['file']).is_file(), mesh.attrib -""" - env = {k: v for k, v in os.environ.items() if k != "PYTHONPATH"} - result = subprocess.run( - [str(python), "-c", code, json.dumps(resources)], - cwd=work, - env=env, - capture_output=True, - text=True, - ) - assert result.returncode == 0, result.stderr diff --git a/tests/test_legacy_data_metric_parity.py b/tests/test_legacy_data_metric_parity.py deleted file mode 100644 index dcf4d86833..0000000000 --- a/tests/test_legacy_data_metric_parity.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Numerical references executed in the immutable EgoVerse source checkout.""" - -import hashlib -import json -from pathlib import Path - -import numpy as np -import pytest -import torch -from hydra import compose, initialize_config_dir -from hydra.utils import instantiate - -from scripts.integration.reference_probe import probe -from tests.fixtures.synthetic_episodes import write_moving_episode - -ROOT = Path(__file__).parents[1] -REFERENCES = Path(__file__).parent / "fixtures/legacy_pipeline" -CASES = [ - (v, "cartesian", f) - for v in ("eva", "aria", "mecka", "scale") - for f in ("camframe", "eef_frame") -] + [("aria", "keypoints", f) for f in ("camframe", "eef_frame")] - - -@pytest.fixture(scope="module") -def episodes(tmp_path_factory): - root = tmp_path_factory.mktemp("moving-legacy-fixtures") - for vendor in ("eva", "aria", "mecka", "scale"): - for seed in range(3): - write_moving_episode(root / vendor, vendor, seed=seed) - return root - - -@pytest.mark.parametrize("vendor,representation,frame", CASES) -def test_preprocessing_normalization_and_headline_metrics_match_legacy( - episodes, vendor, representation, frame -): - name = f"{vendor}-{representation}-{frame}" - archive = REFERENCES / (name + ".npz") - reference = json.loads(archive.with_suffix(".json").read_text()) - assert reference["source_commit"] == "ec5c903c067bf1b29bbff781bc414c6df2bcf1f2" - assert ( - hashlib.sha256(archive.read_bytes()).hexdigest() == reference["archive_sha256"] - ) - actual, metadata = probe(episodes, ROOT, vendor, representation, frame, "graph") - assert metadata["model_keys"] == reference["model_keys"] - assert metadata["normalization_frames"] == reference["normalization_frames"] - with np.load(archive, allow_pickle=False) as expected: - assert set(actual) == set(expected.files) - for key, value in actual.items(): - assert value.shape == expected[key].shape, key - if "observations.images." in key: - # JPEG decoder rounding can differ between platform wheels; - # compare routing/shape and pixels to one quantization unit. - np.testing.assert_allclose( - value, expected[key], rtol=0, atol=1 / 255, err_msg=key - ) - else: - np.testing.assert_allclose( - value, expected[key], rtol=2e-5, atol=2e-6, err_msg=key - ) - - -@pytest.mark.parametrize("kind", ["pooled", "pertoken"]) -def test_retained_language_schedule_matches_all_source_epochs(kind): - expected = json.loads( - (Path(__file__).parent / "fixtures/legacy_language_schedule.json").read_text() - ) - with initialize_config_dir( - version_base=None, config_dir=str(ROOT / "egomimic/hydra_configs") - ): - cfg = compose( - config_name="train_zarr_cartesian", - overrides=[f"model=hpt_bc_pickplace_qwen_{kind}"], - ) - assert cfg.model.scheduler_interval == "epoch" - optimizer = instantiate(cfg.model.optimizer)( - params=[torch.nn.Parameter(torch.ones(1))] - ) - scheduler = instantiate(cfg.model.scheduler)(optimizer=optimizer) - values = [optimizer.param_groups[0]["lr"]] - for _ in range(expected["total_epochs"]): - optimizer.step() - scheduler.step() - values.append(optimizer.param_groups[0]["lr"]) - assert values == pytest.approx(expected["values"], rel=1e-12, abs=1e-15) diff --git a/tests/test_legacy_hpt_trunk_parity.py b/tests/test_legacy_hpt_trunk_parity.py deleted file mode 100644 index 6c77aac535..0000000000 --- a/tests/test_legacy_hpt_trunk_parity.py +++ /dev/null @@ -1,93 +0,0 @@ -"""The generic initializer retains the source HPT trunk namespace and numerics.""" - -import hashlib -import json -from pathlib import Path - -import pytest -import torch -from hydra import compose, initialize_config_dir -from hydra.utils import instantiate - -from egomimic.pipeline.algo import PipelineAlgo -from egomimic.pipeline.stages_hpt import HPTTrunkStage -from scripts.audit_hydra_configs import CONFIGS - -FIXTURE = Path(__file__).parent / "fixtures/legacy_hpt_trunk" - - -@pytest.mark.parametrize("transport", ["local", "huggingface"]) -def test_legacy_trunk_loading_outputs_and_gradients(transport, monkeypatch): - metadata = json.loads((FIXTURE / "metadata.json").read_text()) - assert metadata["source_commit"] == "ec5c903c067bf1b29bbff781bc414c6df2bcf1f2" - for filename, key in ( - ("trunk.pth", "weights_sha256"), - ("reference.pt", "reference_sha256"), - ): - assert ( - hashlib.sha256((FIXTURE / filename).read_bytes()).hexdigest() - == metadata[key] - ) - reference = torch.load(FIXTURE / "reference.pt", weights_only=True) - with initialize_config_dir(version_base=None, config_dir=str(CONFIGS)): - cfg = compose( - config_name="train_zarr_cartesian", overrides=["model=hpt_bc_flow_eva"] - ) - core = cfg.model.pipeline.stages[1].trunk - core.embed_dim = core.attn_target.embed_dim = 16 - core.num_blocks = core.attn_target.num_heads = 2 - stage = HPTTrunkStage(instantiate(core), embed_dim=16) - source = str(FIXTURE / "trunk.pth") - downloads = [] - if transport == "huggingface": - # Substitute only transport: tensor loading/validation and the actual - # source HPT namespace remain the same as the local-file case. - source = { - "repo_id": "fixture/hpt", - "revision": "a" * 40, - "filename": "trunk.pth", - } - - def fetch(**kwargs): - downloads.append(kwargs) - return str(FIXTURE / "trunk.pth") - - monkeypatch.setattr("huggingface_hub.hf_hub_download", fetch) - graph = PipelineAlgo( - [stage], - device="cpu", - stage_ids={"trunk": 0}, - initialization=[ - { - "stage_id": "trunk", - "module_path": "trunk", - "source": source, - "source_prefix": metadata["source_namespace"], - "sha256": metadata["weights_sha256"], - } - ], - ) - assert graph.initialization_receipts[0]["sha256"] == metadata["weights_sha256"] - assert downloads == ([source] if transport == "huggingface" else []) - trunk = graph.pipeline.stage_by_id("trunk").trunk.eval() - inputs = reference["input"].clone().requires_grad_() - output, blocks = trunk(inputs, attn_mask=reference["mask"]) - loss = (output * reference["target"]).mean() + 0.1 * output.square().mean() - loss.backward() - torch.testing.assert_close(output, reference["output"], rtol=2e-5, atol=2e-6) - torch.testing.assert_close(loss, reference["loss"], rtol=2e-5, atol=2e-6) - torch.testing.assert_close( - inputs.grad, reference["input_grad"], rtol=2e-5, atol=2e-6 - ) - for actual, expected in zip(blocks, reference["blocks"], strict=True): - torch.testing.assert_close(actual, expected, rtol=2e-5, atol=2e-6) - assert {"trunk." + key for key, _ in trunk.named_parameters()} == set( - reference["parameter_grad"] - ) - for name, parameter in trunk.named_parameters(): - torch.testing.assert_close( - parameter.grad, - reference["parameter_grad"]["trunk." + name], - rtol=2e-5, - atol=2e-6, - ) diff --git a/tests/test_libero_action_flow_artifacts.py b/tests/test_libero_action_flow_artifacts.py deleted file mode 100644 index 24d6287ce1..0000000000 --- a/tests/test_libero_action_flow_artifacts.py +++ /dev/null @@ -1,153 +0,0 @@ -"""Tiny pure-metadata validation; no model or data construction.""" - -import pytest - -from egomimic.benchmarks.libero.action_flow_artifacts import ( - REQUIRED, - SEED_SHA, - canonical_sha, - validate_payload, -) - - -def fixture(): - identity = { - "suite": "libero10", - "source": "libero_panda", - "action_dim": 7, - "action_horizon": 16, - "seed": 42, - "sample_count": 32, - "energy_seed_bank_sha256": SEED_SHA, - "inference_method": "euler", - "inference_steps": 50, - "normalization_scope": "training_episodes_only", - "effective_batch_size": 32, - "homogeneous": "not_applicable_single_source", - "source_commit": "a" * 40, - "resolved_config_sha256": "b" * 64, - "split_sha256": "c" * 64, - "normalizer_state_sha256": "d" * 64, - "dataset_logical_sha256": "e" * 64, - } - tensor_metadata = { - name: { - "shape": [32, 16, 7], - "axes": ["batch", "horizon", "action"], - "finite": True, - "sha256": "f" * 64, - } - for name in ( - "normalized_prediction", - "native_prediction", - "normalized_target", - "native_target", - ) - } - for name in ("normalized_samples", "native_samples"): - tensor_metadata[name] = { - "shape": [32, 32, 16, 7], - "axes": ["sample", "batch", "horizon", "action"], - "finite": True, - "sha256": "f" * 64, - } - for name in ("diagnostic/latent/clean", "diagnostic/decoded/reconstruction"): - tensor_metadata[name] = {"finite": True, "sha256": "f" * 64} - return { - "schema": "libero-native-action-flow-metrics/v2", - "identity": identity, - "identity_sha256": canonical_sha(identity), - "source": "libero_panda", - "global_step": 2, - "batches": [ - { - "batch_index": 0, - "batch_size": 32, - "normalized_target_sha256": "f" * 64, - "metrics": {key: 1.0 for key in REQUIRED}, - # Pure metadata fixture: validates reference contracts only, - # never proves payload bytes, actual episodes or a run PASS. - "tensor_payload": { - "schema": "libero-native-tensors/v1", - "filename": "fixture.pt", - "sha256": "f" * 64, - "tensors": tensor_metadata, - "alignment": {"fixture": "explicit-synthetic-metadata"}, - "shared_analysis": { - "sha256": "f" * 64, - "identity_sha256": "f" * 64, - "path": "fixture-analysis.pt", - "global_step": 2, - }, - }, - } - ], - } - - -def test_native_complete(): - assert validate_payload(fixture()) - - -def test_legacy_metric_only_schema_does_not_certify_native_tensors(): - payload = fixture() - payload["schema"] = "libero-native-action-flow-metrics/v1" - with pytest.raises(ValueError, match="wrong native artifact schema"): - validate_payload(payload) - - -def test_reference_rejects_wrong_sample_axes(): - payload = fixture() - payload["batches"][0]["tensor_payload"]["tensors"]["native_samples"]["axes"] = [ - "batch", - "sample", - "horizon", - "action", - ] - with pytest.raises(ValueError, match="K32 tensor contract"): - validate_payload(payload) - - -@pytest.mark.parametrize("key", REQUIRED) -def test_reject_nonfinite(key): - payload = fixture() - payload["batches"][0]["metrics"][key] = float("nan") - with pytest.raises(ValueError): - validate_payload(payload) - - -@pytest.mark.parametrize( - "key", - ( - "source_commit", - "resolved_config_sha256", - "split_sha256", - "normalizer_state_sha256", - "dataset_logical_sha256", - "energy_seed_bank_sha256", - "normalization_scope", - "inference_method", - "inference_steps", - "effective_batch_size", - ), -) -def test_reject_identity(key): - payload = fixture() - payload["identity"][key] = "wrong" - payload["identity_sha256"] = canonical_sha(payload["identity"]) - with pytest.raises(ValueError): - validate_payload(payload) - - -def test_missing_first_batch(): - payload = fixture() - payload["batches"][0]["batch_index"] = 1 - with pytest.raises(ValueError): - validate_payload(payload) - - -def test_no_implicit_checkpoint_completion(): - payload = fixture() - payload["global_step"] = 0 - with pytest.raises(ValueError): - validate_payload(payload) diff --git a/tests/test_libero_active_recipes.py b/tests/test_libero_active_recipes.py deleted file mode 100644 index 3a7afa1de5..0000000000 --- a/tests/test_libero_active_recipes.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Keep active LIBERO choices unambiguous without loading data or checkpoints.""" - -from pathlib import Path - -import pytest -from hydra import compose, initialize_config_dir - -ROOT = Path(__file__).resolve().parents[1] -CONFIG = ROOT / "egomimic/hydra_configs" -TARGETS = { - "libero10": 605121, - "libero_spatial": 270054, - "libero_object": 325065, - "libero_goal": 280056, -} - - -def test_active_directory_contains_selected_eight_and_explicit_joint_recipe(): - expected = { - f"{family}_{suite}_{suffix}_s42.yaml" - for suite in TARGETS - for family, suffix in ( - ("action_flow", "oat_dp_matched"), - ("dp", "oat_batch16_keep_steps"), - ) - } - # The joint experiment is separate from the original four AF/DP pairs. - expected.add("action_flow_libero_spatial_sharpa_gripper_keypoints_s42.yaml") - expected.add("action_flow_libero_spatial_sharpa_mot_s42.yaml") - assert { - p.name for p in (CONFIG / "experiment/sim/libero").glob("*.yaml") - } == expected - - -@pytest.mark.parametrize("suite,steps", TARGETS.items()) -def test_active_pair_preserves_selected_scientific_contract(suite, steps): - with initialize_config_dir(config_dir=str(CONFIG), version_base="1.3"): - af = compose( - config_name="train_zarr_cartesian", - overrides=[ - f"+experiment=sim/libero/action_flow_{suite}_oat_dp_matched_s42" - ], - ) - dp = compose( - config_name="train_zarr_cartesian", - overrides=[f"+experiment=sim/libero/dp_{suite}_oat_batch16_keep_steps_s42"], - ) - assert (af.model.num_latent_tokens, af.model.latent_dim) == (16, 16) - assert af.model.pipeline.stages[-1].action_velocity_weight == 1 - assert (af.trainer.max_steps, af.trainer.val_check_interval) == (120000, 20000) - assert af.trainer.check_val_every_n_epoch is None - assert af.callbacks.ema.decay == 0.9978 - assert af.trainer.gradient_clip_val == 3 - assert af.benchmark.batch_size == 32 - assert ( - dp.benchmark.batch_size, - dp.trainer.accumulate_grad_batches, - dp.trainer.max_steps, - ) == (16, 1, steps) - assert dp.callbacks.batch_budget.global_batch_size == 16 - if suite == "libero_spatial": - assert dp.callbacks.model_checkpoint.every_n_train_steps == 15000 - assert dp.callbacks.model_checkpoint.every_n_epochs is None - assert af.model.pipeline.stages[0] == dp.model.pipeline.stages[0] - for cfg in (af, dp): - assert cfg.data.train_datasets.libero_panda.valid_ratio == 0.1 - assert cfg.data.valid_datasets.libero_panda.valid_ratio == 0.1 - - -@pytest.mark.parametrize( - "recipe", sorted((CONFIG / "experiment/sim/libero_historical").glob("*.yaml")) -) -def test_historical_profiles_remain_composable(recipe): - with initialize_config_dir(config_dir=str(CONFIG), version_base="1.3"): - compose( - config_name="train_zarr_cartesian", - overrides=[f"+experiment=sim/libero_historical/{recipe.stem}"], - ) diff --git a/tests/test_libero_arc_sweep.py b/tests/test_libero_arc_sweep.py deleted file mode 100644 index 9328e9cce4..0000000000 --- a/tests/test_libero_arc_sweep.py +++ /dev/null @@ -1,329 +0,0 @@ -import importlib.util -import json -import sys -from pathlib import Path -from types import SimpleNamespace - -import pytest -import yaml - -from egomimic.benchmarks.libero.arc_sweep import fixed_replay_spec, profile_settings -from egomimic.benchmarks.libero.replay import candidates_from_spec, validate_spec - - -@pytest.mark.parametrize( - "profile,mode", - [ - ("stk_1", "stk"), - ("stk_2", "stk"), - ("dur_1", "dur"), - ("dur_2", "dur"), - ("shared", "stk"), - ("shared", "dur"), - ], -) -def test_frozen_sweep_replay_preserves_choice_and_uses_fresh_confirmation( - profile, mode -): - settings = profile_settings(profile, mode) - spec = fixed_replay_spec(profile, mode) - candidates = candidates_from_spec(spec) - assert len(candidates) == 1 - candidate = next(iter(candidates.values())) - assert candidate == { - "mode": mode, - "num_waypoints": settings["arc_waypoints"], - "max_rotation_degrees": settings["arc_max_rotation_degrees"], - "max_translation": settings["arc_max_translation"], - } - assert spec["calibration_demos"] == [0, 1] - assert spec["selection_demos"] == list(range(2, 10)) - assert spec["confirmation_demos"] == list(range(10, 25)) - assert not set(spec["confirmation_demos"]) & set(range(30, 50)) - assert spec["minimum_execution_coverage"] == 0.99 - assert spec["max_raw_repeat_state_error"] == 0 - assert spec["max_dense_action_mse"] == 1e-12 - - -def test_profile_rejects_wrong_mode_and_workflow_omissions(): - with pytest.raises(ValueError, match="incompatible"): - profile_settings("stk_1", "dur") - path = Path(__file__).parents[1] / "scripts/benchmarks/launch_libero_osmo.py" - spec = importlib.util.spec_from_file_location("sweep_osmo", path) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - args = dict( - commit="a" * 40, - run_id="sweep-stk1-10", - suite="libero_10", - mode="full", - arc_modes=["stk"], - arc_profile="stk_1", - ) - with pytest.raises(ValueError, match="requires calibration"): - module.workflow(**args) - result = module.workflow( - **args, calibration_parent="parent", oat_reference_run="baseline" - )["workflow"] - env = result["tasks"][0]["environment"] - assert env["RUN_KIND"] == "arc_sweep" and env["ARC_PROFILE"] == "stk_1" - assert env["OAT_REFERENCE_RUN"] == "baseline" - assert result["resources"]["default"]["gpu"] == 1 - assert result["resources"]["default"]["memory"] == "128Gi" - assert result["resources"]["default"]["cpu"] == 20 - - -def test_fresh_backbone_run_reuses_audited_replay_without_resuming_weights( - tmp_path, monkeypatch -): - from egomimic.benchmarks.libero import arc_sweep, cluster - from scripts.benchmarks.launch_libero_osmo import workflow - - result = workflow( - "a" * 40, - "arc-dp-stk1-spatial", - "libero_spatial", - mode="full", - arc_modes=["stk"], - arc_profile="stk_1", - arc_backbone="oat_dp", - arc_replay_runs={"stk": "audited-replay"}, - oat_reference_run="baseline", - gpus=4, - )["workflow"] - env = result["tasks"][0]["environment"] - assert env["ARC_BACKBONE"] == "oat_dp" - assert env["RESUME_FROM_RUN"] == "" - assert result["resources"]["default"]["gpu"] == 4 - for key, value in env.items(): - monkeypatch.setenv(key, value) - calls = [] - monkeypatch.setattr(cluster, "execute", lambda args, log: calls.append(args)) - monkeypatch.setattr( - sys, - "argv", - [ - "sweep", - "--root", - str(tmp_path), - "--suite", - "libero_spatial", - "--run-id", - "arc-dp-stk1-spatial", - "--profile", - "stk_1", - "--arc-mode", - "stk", - ], - ) - arc_sweep.main() - assert len(calls) == 1 and calls[0][2].endswith(".cluster") - assert json.loads(__import__("os").environ["ARC_REPLAY_RUNS_JSON"]) == { - "stk": "audited-replay" - } - - -@pytest.mark.parametrize("replay_failure", [False, True]) -def test_policy_launch_follows_replay_and_stops_on_preflight_failure( - tmp_path, monkeypatch, replay_failure -): - from egomimic.benchmarks.libero import arc_sweep, cluster - - calls = [] - - def execute(argv, log): - calls.append(argv) - if argv[2].endswith(".replay"): - # Exercise exactly the disk format read in the real subprocess. - loaded = yaml.safe_load(Path(argv[argv.index("--spec") + 1]).read_text()) - validate_spec(loaded) - assert loaded["max_dense_action_mse"] == 1e-12 - if replay_failure: - raise RuntimeError("preflight failed") - - monkeypatch.setattr(cluster, "execute", execute) - monkeypatch.setenv("REPLAY_CALIBRATION_PARENT", "parent-stk") - monkeypatch.setattr( - sys, - "argv", - [ - "sweep", - "--root", - str(tmp_path), - "--suite", - "libero_10", - "--run-id", - "sweep-stk1-10", - "--profile", - "stk_1", - "--arc-mode", - "stk", - ], - ) - if replay_failure: - with pytest.raises(RuntimeError, match="preflight failed"): - arc_sweep.main() - assert len(calls) == 1 - else: - arc_sweep.main() - assert len(calls) == 2 and "--arc-only" in calls[1] - assert calls[0][2].endswith(".replay") and calls[1][2].endswith(".cluster") - assert ( - yaml.safe_load((tmp_path / "fixed-replay-spec.yaml").read_text())[ - "frozen_profile" - ] - == "stk_1" - ) - - -@pytest.mark.parametrize( - "run_mode,bad_replay,gpu_type,backbone", - [ - ("smoke", False, "L40S", "unet"), - ("full", False, "L40S", "unet"), - ("full", True, "L40S", "unet"), - ("full", False, "H100", "unet"), - ("smoke", False, "L40S", "oat_dp"), - ("full", False, "H100", "oat_dp"), - ], -) -def test_arc_only_never_trains_oat_and_checks_frozen_profile( - tmp_path, monkeypatch, run_mode, bad_replay, gpu_type, backbone -): - import torch - - from egomimic.benchmarks.libero import cluster - from egomimic.benchmarks.libero.rollout import rollout_plan, run_rollouts - from tests.test_libero_benchmark import FakeEnvironment, FakePolicy, make_replay - - monkeypatch.setenv("SOURCE_COMMIT", "a" * 40) - monkeypatch.setenv("BENCHMARK_GPU_TYPE", gpu_type) - monkeypatch.setenv("ARC_BACKBONE", backbone) - for name in ( - "CAMPAIGN_ID", - "CAMPAIGN_RUNS_JSON", - "RESUME_FROM_RUN", - "EVALUATE_FROM_RUN", - "ARC_REPLAY_RUN", - ): - monkeypatch.delenv(name, raising=False) - monkeypatch.setenv("ARC_MODES_JSON", '["stk"]') - monkeypatch.setenv("ARC_REPLAY_RUNS_JSON", '{"stk":"verified-replay"}') - monkeypatch.setattr(torch.cuda, "is_available", lambda: True) - monkeypatch.setattr(torch.cuda, "device_count", lambda: 1) - monkeypatch.setattr(torch.cuda, "get_device_name", lambda _: gpu_type) - monkeypatch.setattr(cluster.subprocess, "check_output", lambda *a, **k: "a" * 40) - - def no_op(*a, **k): - pass - - monkeypatch.setattr(torch.cuda, "synchronize", no_op) - - class Uploader: - def __init__(self, *a): - self.client = None - self.prefix = "test/" - self.thread = SimpleNamespace(start=no_op, join=no_op) - self.stop = SimpleNamespace(set=no_op) - - upload = no_op - - monkeypatch.setattr(cluster, "ArtifactUploader", Uploader) - replay_path = tmp_path / "data.zarr" - make_replay(replay_path, suite="libero_10") - monkeypatch.setattr(cluster, "stage_dataset", lambda *a: replay_path) - monkeypatch.setattr(cluster, "configure_simulator", no_op) - settings = profile_settings("stk_1", "stk") - monkeypatch.setattr( - cluster, - "load_arc_calibration", - lambda *a, **k: settings | ({"arc_waypoints": 16} if bad_replay else {}), - ) - calls = [] - - def execute(argv, log): - calls.append(argv) - if "egomimic.trainHydra" in argv: - expected_recipe = ( - "libero_arc_oat_dp_policy" - if backbone == "oat_dp" - else "libero_arc_stk_policy" - ) - assert f"+experiment=sim/oat/{expected_recipe}" in argv - assert "benchmark.arc_waypoints=36" in argv - checkpoint = tmp_path / "evidence/training/arc_stk/checkpoints/last.ckpt" - checkpoint.parent.mkdir(parents=True) - checkpoint.write_bytes(b"checkpoint") - if "rollout" in argv: - output = Path(argv[argv.index("--output") + 1]) - run_rollouts( - FakePolicy(), - rollout_plan("libero_10") - if run_mode == "full" - else rollout_plan("libero_10", trials_per_task=1, repetitions=1), - output, - max_episode_steps=550 if run_mode == "full" else 4, - env_factory=FakeEnvironment, - metadata={ - "suite": "libero_10", - "method": "arc", - "representation": { - "mode": "stk", - "waypoints": 36, - "max_translation": 1.6, - "max_rotation_degrees": 192, - }, - "horizon": 32, - "n_obs_steps": 2, - "n_action_steps": 16, - "use_ema": True, - }, - ) - - from egomimic.benchmarks.libero import evaluate - - parallel_calls = [] - - def parallel_rollouts(checkpoint, evidence, method, suite): - parallel_calls.append((method, suite)) - execute(["rollout", "--output", str(evidence / method / suite)], None) - - monkeypatch.setattr(evaluate, "parallel_rollouts", parallel_rollouts) - monkeypatch.setattr(cluster, "execute", execute) - monkeypatch.setattr( - sys, - "argv", - [ - "cluster", - "--root", - str(tmp_path), - "--suite", - "libero_10", - "--run-id", - "sweep", - "--mode", - run_mode, - "--arc-only", - "--arc-profile", - "stk_1", - ], - ) - if bad_replay: - with pytest.raises(ValueError, match="differs from frozen"): - cluster.main() - assert not any("egomimic.trainHydra" in c for c in calls) - else: - cluster.main() - assert parallel_calls == ( - [("arc_stk", "libero_10")] - if backbone == "oat_dp" and run_mode == "full" - else [] - ) - assert sum("egomimic.trainHydra" in c for c in calls) == 1 - assert not any("reconstruct" in c for c in calls) - result = json.loads((tmp_path / "evidence/arc-results.json").read_text()) - assert set(result["variants"]) == {"stk"} - assert result["paired_oat_comparison_complete"] is False - assert json.loads((tmp_path / "evidence/status.json").read_text())["state"] == ( - "ARC_POLICIES_COMPLETE" if run_mode == "full" else "ARC_SMOKE_PASSED" - ) diff --git a/tests/test_libero_basis_scaling_audit.py b/tests/test_libero_basis_scaling_audit.py deleted file mode 100644 index 5b14747014..0000000000 --- a/tests/test_libero_basis_scaling_audit.py +++ /dev/null @@ -1,113 +0,0 @@ -import numpy as np -import pytest - -from egomimic.rldb.zarr.libero_arc_global import LiberoArcGlobalCodec -from scripts.benchmarks.compare_libero_arc_bases import fit_bounded_scaling, noise_audit - - -def test_scaling_is_train_only_bounded_and_reversible(): - rng = np.random.default_rng(8) - values = rng.normal(size=(8, 32, 12)) - values[:, 0, 0] = 4 - mask = np.arange(8) < 4 - center, scale, std = fit_bounded_scaling(values, mask, 32) - changed = values.copy() - changed[~mask] += 1000 - for left, right in zip( - (center, scale, std), fit_bounded_scaling(changed, mask, 32) - ): - np.testing.assert_array_equal(left, right) - assert np.isfinite(scale).all() and scale.min() >= 1e-4 - assert std[0, 0] == 0 - np.testing.assert_allclose((values - center) / scale * scale + center, values) - with pytest.raises(ValueError): - fit_bounded_scaling(values, np.zeros(8, dtype=bool), 32) - - -@pytest.mark.parametrize("basis", ["fourier", "chebyshev"]) -def test_noise_audit_is_deterministic_and_does_not_change_codec(basis): - codec = LiberoArcGlobalCodec(basis=basis) - raw = np.random.default_rng(3).uniform(-0.1, 0.1, (4, 32, 7)).astype(np.float32) - tokens = np.stack([codec.encode(row) for row in raw]) - frozen = tokens.copy() - identities = [{"split": "train" if i < 2 else "valid"} for i in range(4)] - first = noise_audit(codec, tokens, raw, identities, sigmas=(0.0, 0.001)) - assert first == noise_audit(codec, tokens, raw, identities, sigmas=(0.0, 0.001)) - np.testing.assert_array_equal(tokens, frozen) - for variant in first["variants"].values(): - assert variant["perturbations"]["0.0"]["decoded_delta_mse_mean"] < 1e-12 - assert variant["perturbations"]["0.001"]["decoded_delta_mse_mean"] > 0 - - -def test_optional_affine_stage_roundtrip_strict_state_and_identity(): - import copy - - import torch - - from egomimic.pipeline.stages_libero_arc import LiberoArcStage - - codec = LiberoArcGlobalCodec(basis="fourier", geometry=64) - raw = np.random.default_rng(1).uniform(-0.1, 0.1, (6, 32, 7)).astype(np.float32) - tokens = np.stack([codec.encode(row) for row in raw]) - audit = noise_audit( - codec, - tokens, - raw, - [{"split": "train" if i < 4 else "valid"} for i in range(6)], - sigmas=(0.0,), - ) - affine = audit["affine"] - stage = LiberoArcStage(codec=codec, token_affine=affine, reconstruction=True) - stage.action_scale.copy_(torch.arange(1, 8)) - stage.action_offset.copy_(torch.arange(7) / 5) - actions = torch.from_numpy(raw) * stage.action_scale + stage.action_offset - result = stage.execute({"actions": actions}, mode="inference") - expected = np.stack([codec.decode(row) for row in tokens]) - torch.testing.assert_close( - result["pred_action"], - torch.from_numpy(expected) * stage.action_scale + stage.action_offset, - atol=2e-5, - rtol=1e-5, - ) - restored = LiberoArcStage(codec=codec, token_affine=affine, operation="decode") - restored.load_state_dict(stage.state_dict(), strict=True) - torch.testing.assert_close( - restored.execute({"pred_arc": result["target"]}, mode="inference")[ - "pred_action" - ], - result["pred_action"], - ) - assert torch.isfinite( - restored.execute({"pred_arc": result["target"].bfloat16()}, mode="inference")[ - "pred_action" - ] - ).all() - assert stage.representation_context() == restored.representation_context() - wrong = copy.deepcopy(affine) - wrong["geometry"] = 128 - with pytest.raises(ValueError, match="identity differ"): - LiberoArcStage(codec=codec, token_affine=wrong) - wrong = copy.deepcopy(affine) - wrong["scale"][0][0] = 0 - with pytest.raises(ValueError, match="affine arrays"): - LiberoArcStage(codec=codec, token_affine=wrong) - - -def test_default_uniform_has_no_affine_state_or_numeric_change(): - import torch - - from egomimic.pipeline.stages_libero_arc import LiberoArcStage - - stage = LiberoArcStage(codec=LiberoArcGlobalCodec(basis="uniform")) - raw = torch.from_numpy( - np.random.default_rng(2).uniform(-0.1, 0.1, (2, 32, 7)).astype(np.float32) - ) - expected = torch.tensor( - np.stack([stage.codec.encode(a) for a in raw.numpy()]) - ) / torch.tensor(stage.codec.token_scale()) - assert not any("token_" in key for key in stage.state_dict()) - assert "token_affine" not in stage.representation_context() - assert "geometry_fit_grid" not in stage.representation_context() - assert torch.equal( - stage.execute({"actions": raw}, mode="train")["target"], expected - ) diff --git a/tests/test_libero_cluster.py b/tests/test_libero_cluster.py deleted file mode 100644 index d982dbf3aa..0000000000 --- a/tests/test_libero_cluster.py +++ /dev/null @@ -1,750 +0,0 @@ -import importlib.util -import json -import zipfile -from pathlib import Path - -import pytest -from hydra import compose, initialize_config_dir - -from egomimic.benchmarks.libero.cluster import ( - ArtifactUploader, - CalibrationPending, - campaign_sources, - digest, - extract_replay, - load_arc_calibration, - publish_campaign, - restore_checkpoints, - stage_dataset, - training_arguments, - validate_gpu_allocation, -) - - -@pytest.mark.parametrize("mode", ["smoke", "full"]) -@pytest.mark.parametrize("method", ["tokenizer", "oat", "arc", "arc_dur", "arc_stk"]) -def test_cluster_arguments_compose_native_graph(mode, method, tmp_path): - args = training_arguments( - method, "libero_10", tmp_path / "replay.zarr", tmp_path, mode, 5001 - ) - config_dir = Path(__file__).parents[1] / "egomimic/hydra_configs" - with initialize_config_dir(config_dir=str(config_dir), version_base="1.3"): - cfg = compose(config_name="train_zarr_cartesian", overrides=args[3:]) - assert cfg.model.pipeline._target_ == "egomimic.pipeline.algo.PipelineAlgo" - assert cfg.trainer.devices == 1 and cfg.trainer.precision == "bf16-mixed" - assert cfg.benchmark.batch_size == (4 if mode == "smoke" else 256) - assert cfg.trainer.max_epochs == (1 if mode == "smoke" else 5001) - if mode == "full": - assert cfg.logger.csv._target_ == "lightning.pytorch.loggers.CSVLogger" - assert cfg.trainer.accumulate_grad_batches == 4 - assert cfg.callbacks.batch_budget.global_batch_size == 1024 - else: - assert cfg.trainer.accumulate_grad_batches == 1 - assert cfg.callbacks.batch_budget is None - assert cfg.callbacks.ema.final_checkpoint_path.endswith("checkpoints/last.ckpt") - if method == "oat": - assert cfg.benchmark.tokenizer_checkpoint.endswith( - "tokenizer/checkpoints/last.ckpt" - ) - if method in {"arc_dur", "arc_stk"}: - mode = method.removeprefix("arc_") - assert cfg.benchmark.arc_mode == mode - assert cfg.benchmark.arc_action_dim == 12 - stages = cfg.model.pipeline.stages - assert stages[1].arc_mode == stages[-1].arc_mode == mode - assert ( - stages[1].velocity_norm_bound - == stages[-1].velocity_norm_bound - == pytest.approx(3**0.5) - ) - assert stages[2].action_dim == stages[3].policy.model.input_dim == 12 - - -def test_workflow_pins_source_and_requests_one_gpu(): - path = Path(__file__).parents[1] / "scripts/benchmarks/launch_libero_osmo.py" - spec = importlib.util.spec_from_file_location("libero_osmo", path) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - workflow = module.workflow("a" * 40, "libero-smoke-test", "libero_10")["workflow"] - assert workflow["resources"]["default"]["gpu"] == 1 - assert workflow["resources"]["default"]["platform"] == "ovx-l40s" - assert workflow["tasks"][0]["environment"]["BENCHMARK_GPU_TYPE"] == "L40S" - assert workflow["tasks"][0]["environment"]["SOURCE_COMMIT"] == "a" * 40 - assert json.loads(workflow["tasks"][0]["environment"]["ARC_MODES_JSON"]) == [ - "dur", - "stk", - ] - with pytest.raises(ValueError, match="immutable"): - module.workflow("main", "libero-smoke-test", "libero_10") - h100 = module.workflow( - "a" * 40, - "h100-resume", - "libero_10", - mode="full", - resume_from_run="prior-run", - gpus=8, - gpu_type="H100", - )["workflow"] - assert h100["resources"]["default"]["platform"] == "dgx-h100" - assert h100["resources"]["default"]["gpu"] == 8 - assert h100["resources"]["default"]["cpu"] == 80 - h100_env = h100["tasks"][0]["environment"] - assert h100_env["BENCHMARK_GPU_TYPE"] == "H100" - assert h100_env["TRAINING_GPUS"] == "8" - assert h100_env["EPOCHS"] == "5001" - assert h100_env["RESUME_FROM_RUN"] == "prior-run" - with pytest.raises(ValueError, match="GPU type"): - module.workflow("a" * 40, "invalid-gpu", "libero_10", gpu_type="A100") - full = module.workflow( - "a" * 40, "study-libero-10", "libero_10", mode="full", campaign_id="study" - )["workflow"] - assert full["timeout"]["exec_timeout"] == "60d" - assert full["tasks"][0]["environment"]["CAMPAIGN_ID"] == "study" - replay = module.workflow( - "a" * 40, - "replay-refine", - "libero_90", - replay=True, - replay_spec="libero_arc_replay_refine", - )["workflow"] - assert replay["resources"]["default"]["cpu"] >= 32 - assert replay["resources"]["default"]["gpu"] == 1 - assert replay["resources"]["default"]["memory"] == "192Gi" - replacements = campaign_sources("study", "a" * 40) - replacements["libero_90"] = { - "run_id": "recovered-libero-90", - "source_commit": "b" * 40, - } - replacement = module.workflow( - "b" * 40, - "recovered-libero-90", - "libero_90", - mode="full", - campaign_id="study", - campaign_runs=replacements, - )["workflow"] - assert ( - json.loads(replacement["tasks"][0]["environment"]["CAMPAIGN_RUNS_JSON"]) - == replacements - ) - with pytest.raises(ValueError, match="Campaign"): - module.workflow( - "a" * 40, - "recovered-libero-90", - "libero_90", - mode="full", - campaign_id="study", - campaign_runs=replacements, - ) - with pytest.raises(ValueError, match="Campaign"): - module.workflow( - "a" * 40, "unrelated-run", "libero_10", mode="full", campaign_id="study" - ) - variants = module.workflow( - "a" * 40, - "both-modes", - "libero_10", - mode="full", - arc_replay_runs={"dur": "dur-replay", "stk": "stk-replay"}, - )["workflow"] - assert json.loads(variants["tasks"][0]["environment"]["ARC_REPLAY_RUNS_JSON"]) == { - "dur": "dur-replay", - "stk": "stk-replay", - } - with pytest.raises(ValueError, match="mode-specific"): - module.workflow( - "a" * 40, - "ambiguous-mode", - "libero_10", - arc_modes=["dur", "stk"], - arc_replay_run="legacy-run", - ) - - -@pytest.mark.parametrize( - "devices,gpu_type,error", - [ - (["NVIDIA L40S"] * 8, "L40S", None), - (["NVIDIA H100 80GB HBM3"] * 8, "H100", None), - (["NVIDIA L40S"] * 8, "H100", RuntimeError), - (["NVIDIA H100 80GB HBM3"] * 7, "H100", RuntimeError), - (["NVIDIA H100 80GB HBM3"] * 7 + ["NVIDIA L40S"], "H100", RuntimeError), - ([], "H100", RuntimeError), - (["NVIDIA A100"] * 8, "A100", ValueError), - ], -) -def test_gpu_allocation_rejects_missing_mixed_or_wrong_devices( - monkeypatch, devices, gpu_type, error -): - import torch - - monkeypatch.setattr(torch.cuda, "is_available", lambda: bool(devices)) - monkeypatch.setattr(torch.cuda, "device_count", lambda: len(devices)) - monkeypatch.setattr(torch.cuda, "get_device_name", devices.__getitem__) - if error: - with pytest.raises(error): - validate_gpu_allocation(8, gpu_type) - else: - validate_gpu_allocation(8, gpu_type) - - -@pytest.mark.parametrize( - "condition", ["complete", "missing", "running", "source", "budget"] -) -def test_campaign_only_publishes_complete_matching_results(condition): - import io - - from botocore.exceptions import ClientError - - from egomimic.benchmarks.libero.catalog import TASKS - - class Storage: - report = None - - def get_object(self, Bucket, Key): - suite = next( - suite for suite in TASKS if f"study-{suite.replace('_', '-')}/" in Key - ) - if suite == "libero_90" and condition == "missing": - raise ClientError({"Error": {"Code": "NoSuchKey"}}, "GetObject") - payload = { - "status.json": { - "state": "TRAINING" if condition == "running" else "SUITE_COMPLETE" - }, - "runtime.json": { - "source_commit": "b" * 40 if condition == "source" else "a" * 40, - "mode": "full", - "epochs": 1 if condition == "budget" else 5001, - "global_batch_size": 1024, - "suite": suite, - }, - "comparison.json": {"suite": suite, "complete_protocol": True}, - }[Key.rsplit("/", 1)[1]] - return {"Body": io.BytesIO(json.dumps(payload).encode())} - - def put_object(self, **kwargs): - self.report = json.loads(kwargs["Body"]) - - storage = Storage() - if condition in {"source", "budget"}: - with pytest.raises(ValueError, match="Campaign"): - publish_campaign(storage, "study", commit="a" * 40, epochs=5001) - else: - assert publish_campaign(storage, "study", commit="a" * 40, epochs=5001) == ( - condition == "complete" - ) - if condition == "complete": - assert storage.report["unique_tasks"] == 130 - assert set(storage.report["suites"]) == set(TASKS) - assert storage.report["full_released_training_budget"] is True - else: - assert storage.report is None - - -def test_released_replay_archive_detection_and_traversal_rejection(tmp_path): - archive = tmp_path / "replay.zip" - with zipfile.ZipFile(archive, "w") as handle: - handle.writestr("libero10_N500.zarr/.zgroup", "{}") - handle.writestr("libero10_N500.zarr/meta/episode_ends/.zarray", "{}") - assert ( - extract_replay(archive, tmp_path / "out") == tmp_path / "out/libero10_N500.zarr" - ) - with zipfile.ZipFile(archive, "w") as handle: - handle.writestr("../outside", "bad") - with pytest.raises(ValueError, match="Unsafe"): - extract_replay(archive, tmp_path / "bad") - assert not (tmp_path / "outside").exists() - - -@pytest.mark.parametrize("bad_source", [False, True]) -def test_campaign_can_pin_a_replacement_without_restarting_other_suites(bad_source): - import io - - from egomimic.benchmarks.libero.catalog import TASKS - - pinned = campaign_sources("study", "a" * 40) - pinned["libero_90"] = {"run_id": "recovered-libero-90", "source_commit": "b" * 40} - - class Storage: - report = None - - def get_object(self, Bucket, Key): - suite = next(s for s, r in pinned.items() if f"/{r['run_id']}/" in Key) - row = { - "status.json": {"state": "SUITE_COMPLETE"}, - "runtime.json": { - "source_commit": "c" * 40 - if bad_source - else pinned[suite]["source_commit"], - "suite": suite, - "mode": "full", - "epochs": 5001, - "global_batch_size": 1024, - }, - "comparison.json": {"suite": suite, "complete_protocol": True}, - }[Key.rsplit("/", 1)[1]] - return {"Body": io.BytesIO(json.dumps(row).encode())} - - def put_object(self, **kwargs): - self.report = json.loads(kwargs["Body"]) - - storage = Storage() - if bad_source: - with pytest.raises(ValueError, match="source"): - publish_campaign( - storage, "study", commit="b" * 40, epochs=5001, run_sources=pinned - ) - assert storage.report is None - else: - assert publish_campaign( - storage, "study", commit="b" * 40, epochs=5001, run_sources=pinned - ) - assert storage.report["source_commit"] is None - assert storage.report["source_commits"]["libero_90"] == "b" * 40 - assert set(storage.report["suites"]) == set(TASKS) - with pytest.raises(ValueError, match="all suites"): - campaign_sources("study", "a" * 40, {"libero_90": pinned["libero_90"]}) - - -@pytest.mark.parametrize("missing_mode", [False, True]) -def test_dual_mode_campaign_requires_every_variant(missing_mode): - import io - - from egomimic.benchmarks.libero.catalog import TASKS - - class Storage: - report = None - - def get_object(self, Bucket, Key): - suite = next(s for s in TASKS if f"study-{s.replace('_', '-')}/" in Key) - variants = { - mode: {"suite": suite, "complete_protocol": True} - for mode in ("dur", "stk") - } - if missing_mode and suite == "libero_90": - del variants["stk"] - value = { - "runtime.json": { - "suite": suite, - "mode": "full", - "epochs": 5001, - "global_batch_size": 1024, - "source_commit": "a" * 40, - "arc_modes": ["dur", "stk"], - }, - "status.json": {"state": "SUITE_COMPLETE"}, - "comparison.json": { - "suite": suite, - "complete_protocol": True, - "variants": variants, - }, - }[Key.rsplit("/", 1)[1]] - return {"Body": io.BytesIO(json.dumps(value).encode())} - - def put_object(self, **kwargs): - self.report = json.loads(kwargs["Body"]) - - storage = Storage() - if missing_mode: - with pytest.raises(ValueError, match="every ARC mode"): - publish_campaign( - storage, "study", commit="a" * 40, epochs=5001, arc_modes=["dur", "stk"] - ) - assert storage.report is None - else: - assert publish_campaign( - storage, "study", commit="a" * 40, epochs=5001, arc_modes=["dur", "stk"] - ) - assert storage.report["arc_modes"] == ["dur", "stk"] - - -def test_campaign_plan_shares_oat_and_schedules_both_arc_variants(): - from egomimic.benchmarks.libero.campaign import campaign_plan - from egomimic.benchmarks.libero.catalog import TASKS - - plan = campaign_plan("/data", "/results") - jobs = {job["id"]: job for job in plan["jobs"]} - assert len(jobs) == len(plan["jobs"]) == 42 - for suite in TASKS: - assert jobs[f"{suite}/oat"]["requires"] == [f"{suite}/tokenizer"] - for mode in ("dur", "stk"): - assert ( - f"+experiment=sim/oat/libero_arc_{mode}_policy" - in jobs[f"{suite}/arc_{mode}"]["argv"] - ) - assert ( - f"{suite}/arc_{mode}_rollout" - in jobs[f"complete_comparison_{mode}"]["requires"] - ) - - -def test_full_training_converts_only_verified_cached_demonstrations( - tmp_path, monkeypatch -): - import hashlib - import io - - from egomimic.benchmarks.libero import cluster, convert - - raw = tmp_path / "cache/libero_90/task_demo.hdf5" - raw.parent.mkdir(parents=True) - raw.write_bytes(b"official demonstration") - metadata = { - "sha": cluster.DATA_REVISION, - "siblings": [ - { - "rfilename": "libero_90/task_demo.hdf5", - "lfs": {"sha256": hashlib.sha256(raw.read_bytes()).hexdigest()}, - } - ], - } - monkeypatch.setenv("LIBERO_RAW_CACHE", str(raw.parent.parent)) - monkeypatch.setattr(cluster, "TASKS", {"libero_90": ["task"]}) - monkeypatch.setattr( - cluster.urllib.request, - "urlopen", - lambda *a, **k: io.BytesIO(json.dumps(metadata).encode()), - ) - monkeypatch.setattr( - cluster, "download", lambda *a, **k: pytest.fail("Cache must not download") - ) - calls = [] - monkeypatch.setattr(convert, "convert_suite", lambda *args: calls.append(args)) - evidence = tmp_path / "evidence" - evidence.mkdir() - result = stage_dataset(tmp_path / "root", "libero_90", evidence) - assert calls == [(raw.parent, result, "libero_90")] - raw.write_bytes(b"corrupt") - with pytest.raises(ValueError, match="checksum"): - stage_dataset(tmp_path / "root", "libero_90", evidence) - assert raw.read_bytes() == b"corrupt" and len(calls) == 1 - - -def test_checkpoint_snapshots_are_content_addressed_and_receipted( - tmp_path, monkeypatch -): - import boto3 - - class Storage: - def __init__(self): - self.objects = {} - - def list_objects_v2(self, **kwargs): - return {"KeyCount": 0} - - def upload_file(self, source, bucket, key, ExtraArgs): - self.objects[key] = Path(source).read_bytes() - assert ExtraArgs["Metadata"]["sha256"] == digest(source) - - storage = Storage() - monkeypatch.setattr(boto3, "client", lambda *args, **kwargs: storage) - for name in ("R2_ENDPOINT_URL", "R2_ACCESS_KEY_ID", "R2_SECRET_ACCESS_KEY"): - monkeypatch.setenv(name, "test") - evidence = tmp_path / "evidence" - evidence.mkdir() - path = evidence / "last.ckpt" - path.write_bytes(b"first checkpoint") - uploader = ArtifactUploader(evidence, "test") - uploader.upload(final=True) - first_hash = digest(path) - path.write_bytes(b"final checkpoint") - uploader.upload(final=True) - uploader.upload(final=True) - receipts = json.loads((evidence / "checkpoint-receipts.json").read_text()) - assert receipts["last.ckpt"]["sha256"] == digest(path) - assert any(first_hash in key for key in storage.objects) - assert any(digest(path) in key for key in storage.objects) - - -@pytest.mark.parametrize("invalid", [None, "budget", "hash", "incomplete", "outside"]) -def test_recovery_verifies_checkpoints_and_complete_training(tmp_path, invalid): - import io - import shutil - - import torch - - source = tmp_path / "source.ckpt" - torch.save( - { - "global_step": 2, - "loops": { - "fit_loop": { - "epoch_progress": { - "current": {"completed": 0 if invalid == "incomplete" else 1} - } - } - }, - }, - source, - ) - prefix = "experiments/arc-oat-20260919/source-run/" - runtime = { - "suite": "libero_10", - "mode": "full" if invalid == "budget" else "smoke", - "epochs": 1, - } - receipt = { - "sha256": "bad" if invalid == "hash" else digest(source), - "bytes": source.stat().st_size, - "uri": "s3://rldb/" - + ("another-run/" if invalid == "outside" else prefix + "checkpoints/") - + "last.ckpt", - } - receipts = { - f"training/{method}/checkpoints/last.ckpt": receipt - for method in ("tokenizer", "oat", "arc") - } - - class Storage: - def get_object(self, Bucket, Key): - value = runtime if Key.endswith("runtime.json") else receipts - return {"Body": io.BytesIO(json.dumps(value).encode())} - - def download_file(self, bucket, key, destination): - shutil.copyfile(source, destination) - - evidence = tmp_path / "evidence" - if invalid is not None: - with pytest.raises(ValueError): - restore_checkpoints( - Storage(), - "source-run", - evidence, - suite="libero_10", - mode="smoke", - epochs=5001, - ) - else: - restore_checkpoints( - Storage(), - "source-run", - evidence, - suite="libero_10", - mode="smoke", - epochs=5001, - ) - assert ( - len( - json.loads((evidence / "recovered-training.json").read_text())[ - "checkpoints" - ] - ) - == 3 - ) - - -@pytest.mark.parametrize("invalid", [None, "optimizer", "normalizer", "budget"]) -@pytest.mark.parametrize( - "methods", [("tokenizer", "oat", "arc"), ("tokenizer", "oat", "arc_dur", "arc_stk")] -) -def test_partial_resume_preserves_only_existing_verified_checkpoints( - tmp_path, invalid, methods -): - import io - import shutil - - import torch - - payload = { - "global_step": 140, - "loops": {"fit_loop": {"epoch_progress": {"current": {"completed": 2}}}}, - "training_budget": { - "global_batch_size": 256 if invalid == "budget" else 1024, - "epochs": 5001, - }, - "optimizer_states": [{"state": {}}], - "normalizer_state": {}, - } - if invalid == "optimizer": - del payload["optimizer_states"] - if invalid == "normalizer": - del payload["normalizer_state"] - source = tmp_path / "partial.ckpt" - torch.save(payload, source) - runtime = { - "suite": "libero_10", - "mode": "full", - "epochs": 5001, - "global_batch_size": 1024, - } - receipts = { - "training/tokenizer/checkpoints/last.ckpt": { - "sha256": digest(source), - "bytes": source.stat().st_size, - "uri": "s3://rldb/experiments/arc-oat-20260919/source-run/checkpoints/partial.ckpt", - } - } - - class Storage: - def get_object(self, Bucket, Key): - value = runtime if Key.endswith("runtime.json") else receipts - return {"Body": io.BytesIO(json.dumps(value).encode())} - - def download_file(self, bucket, key, destination): - shutil.copyfile(source, destination) - - if invalid: - with pytest.raises(ValueError, match="Partial checkpoint"): - restore_checkpoints( - Storage(), - "source-run", - tmp_path / "evidence", - suite="libero_10", - mode="full", - epochs=5001, - allow_partial=True, - methods=methods, - ) - else: - restored = restore_checkpoints( - Storage(), - "source-run", - tmp_path / "evidence", - suite="libero_10", - mode="full", - epochs=5001, - allow_partial=True, - methods=methods, - ) - assert set(restored) == {"tokenizer"} - assert restored["tokenizer"]["global_step"] == 140 - assert restored["tokenizer"]["complete"] is False - assert not (tmp_path / "evidence/training/oat").exists() - - -@pytest.mark.parametrize( - "invalid", - [ - None, - "pending", - "suite", - "protocol", - "spec", - "codec", - "retention", - "incomplete", - "evaluated", - "unpermitted_gap", - "mode", - ], -) -@pytest.mark.parametrize("arc_mode", ["joint_dur", "stk", "dur"]) -def test_arc_training_requires_matching_confirmed_replay( - tmp_path, monkeypatch, invalid, arc_mode -): - import hashlib - import io - import subprocess - - import yaml - - from egomimic.benchmarks.libero.replay import candidate_id - - root = Path(__file__).parents[1] - spec = yaml.safe_load( - (root / "egomimic/hydra_configs/benchmark/libero_arc_replay.yaml").read_text() - ) - if invalid in {"evaluated", "unpermitted_gap"}: - spec.update( - selection_objective="success_then_tokens", - maximum_success_rate_drop=1.0, - allow_reference_gap=invalid == "evaluated", - ) - codec = {"num_waypoints": 16, "max_translation": 0.2, "max_rotation_degrees": 48} - if arc_mode != "joint_dur": - codec["mode"] = spec["arc_mode"] = arc_mode - selected = candidate_id(codec) - result = { - "confirmed": invalid not in {"pending", "evaluated", "unpermitted_gap"}, - "confirmation_complete": True, - "arc_mode": "unmatched" if invalid == "mode" else arc_mode, - "suite": "libero_10", - "codec": codec, - "candidate_id": selected, - "selected_before_confirmation": True, - "spec_sha256": hashlib.sha256( - (json.dumps(spec, indent=2) + "\n").encode() - ).hexdigest(), - "confirmation": { - "raw": {"success_rate": 1, "episodes": 50}, - "raw_repeat": {"max_state_l2_vs_raw_mean": 0, "episodes": 50}, - "dense": {"retention": 1, "max_action_mse": 0, "episodes": 50}, - selected: { - "retention": 0.9 if invalid == "retention" else 1, - "success_rate": 0.9 - if invalid in {"retention", "evaluated", "unpermitted_gap"} - else 1, - "raw_successes": 50, - "episodes": 50, - "successes": 50, - "action_mse": 0.1, - }, - }, - } - runtime = { - "suite": "libero_goal" if invalid == "suite" else "libero_10", - "source_commit": "a" * 40, - } - if invalid == "protocol": - spec["execute_steps"] = 8 - if invalid == "spec": - result["spec_sha256"] = "wrong" - if invalid == "incomplete": - result["confirmation"][selected]["episodes"] = 49 - content = (root / "egomimic/rldb/zarr/libero_arc.py").read_bytes() - monkeypatch.setattr(subprocess, "run", lambda *args, **kwargs: None) - monkeypatch.setattr( - subprocess, - "check_output", - lambda argv, **kwargs: ( - b"different codec" - if invalid == "codec" - else (root / argv[-1].split(":", 1)[1]).read_bytes() - ), - ) - - class Storage: - def get_object(self, Bucket, Key): - value = { - "status.json": { - "state": "REPLAY_EVALUATED" - if invalid in {"evaluated", "unpermitted_gap"} - else "CONFIRMED" - }, - "result.json": result, - "runtime.json": runtime, - "spec.json": spec, - }[Key.rsplit("/", 1)[-1]] - return {"Body": io.BytesIO(json.dumps(value).encode())} - - if invalid and invalid != "evaluated": - with pytest.raises((ValueError, CalibrationPending)): - load_arc_calibration( - Storage(), "replay-run", tmp_path, suite="libero_10", arc_mode=arc_mode - ) - assert not (tmp_path / "arc-calibration.json").exists() - else: - overrides = load_arc_calibration( - Storage(), "replay-run", tmp_path, suite="libero_10", arc_mode=arc_mode - ) - expected = { - "arc_waypoints": 16, - "arc_max_translation": 0.2, - "arc_max_rotation_degrees": 48, - } - if arc_mode != "joint_dur": - expected.update(arc_mode=arc_mode, arc_action_dim=12) - if arc_mode == "stk": - expected["arc_velocity_norm_bound"] = 3**0.5 - assert overrides == expected - receipt = ( - "arc-calibration.json" - if arc_mode == "joint_dur" - else f"arc-{arc_mode}-calibration.json" - ) - assert ( - json.loads((tmp_path / receipt).read_text())["verified_codec_sha256"] - == hashlib.sha256(content).hexdigest() - ) - with pytest.raises(CalibrationPending, match="requires"): - load_arc_calibration(Storage(), None, tmp_path, suite="libero_10") diff --git a/tests/test_libero_multi_embodiment_native.py b/tests/test_libero_multi_embodiment_native.py deleted file mode 100644 index 17a1932d32..0000000000 --- a/tests/test_libero_multi_embodiment_native.py +++ /dev/null @@ -1,1286 +0,0 @@ -import json - -import h5py -import numpy as np -import pytest -import torch -import zarr - -from egomimic.benchmarks.libero.catalog import TASK_IDS, get_tasks -from egomimic.benchmarks.libero.convert import ( - convert_collection_manifest, - native_episode_values, -) -from egomimic.pipeline.stages_sampler import KeyedFeatureProjection -from egomimic.rldb.zarr.libero_dataset import ( - LiberoDataset, - LiberoMultiEmbodimentNormalizer, - LiberoReplayResolver, - keymap, -) - - -def replay(path, action_dim, hand_dim, demos_per_task=2): - tasks = get_tasks("libero_spatial") - ids = np.repeat([TASK_IDS[t][2] for t in tasks for _ in range(demos_per_task)], 2) - n = len(ids) - rng = np.random.default_rng(4) - group = zarr.open_group(str(path), mode="w", zarr_format=2) - data = group.create_group("data") - for key, values in { - "action": rng.uniform(-1, 1, (n, action_dim)).astype("float32"), - "robot0_gripper_qpos": rng.uniform(0, 1, (n, hand_dim)).astype("float32"), - "robot0_eef_pos": rng.uniform(0, 1, (n, 3)).astype("float32"), - "robot0_eef_quat": np.tile(np.array([0, 0, 0, 1], dtype="float32"), (n, 1)), - "task_uid": ids[:, None], - "agentview_rgb": rng.integers(0, 255, (n, 2, 2, 3), dtype="uint8"), - "robot0_eye_in_hand_rgb": rng.integers(0, 255, (n, 2, 2, 3), dtype="uint8"), - }.items(): - data.create_array(key, data=values) - group.create_group("meta").create_array("episode_ends", data=np.arange(2, n + 1, 2)) - return group - - -def resolver(path, sharpa=False): - return LiberoReplayResolver( - path, - keymap(2, 32), - "libero_spatial", - **( - dict(action_dim=28, gripper_state_dim=22, embodiment="libero_panda_sharpa") - if sharpa - else {} - ), - ) - - -def test_distinct_native_widths_and_reject_sharpa_as_original(tmp_path): - path = tmp_path / "sharpa.zarr" - replay(path, 28, 22) - with pytest.raises(ValueError, match="action must"): - resolver(path).resolve() - sample = next(iter(resolver(path, True).resolve().values()))[0] - assert sample["embodiment"] == 22 - assert sample["actions"].shape == (32, 28) - assert sample["robot0_gripper_qpos"].shape == (2, 22) - assert torch.equal(sample["actions"][1], sample["actions"][-1]) - with pytest.raises(ValueError, match="Sharpa contract"): - LiberoReplayResolver( - path, - keymap(), - "libero_spatial", - action_dim=7, - gripper_state_dim=22, - embodiment="libero_panda_sharpa", - ) - - -def test_configured_replay_split_proves_rows_and_rejects_leakage(tmp_path): - from egomimic.rldb.zarr.libero_saved_state import make_replay_split_proof - - path = tmp_path / "replay.zarr" - replay(path, 7, 2) - r = resolver(path) - train = LiberoDataset._from_resolver(r, mode="train", valid_ratio=0.1) - valid = LiberoDataset._from_resolver(r, mode="valid", valid_ratio=0.1) - kwargs = dict(expected_episodes=20, valid_ratio=0.1, seed=42) - proof = make_replay_split_proof(train, valid, **kwargs) - assert len(proof["train"]) == 18 and len(proof["valid"]) == 2 - rows = sorted(proof["train"] + proof["valid"], key=lambda row: row["start"]) - assert [(row["start"], row["end"]) for row in rows] == [ - (i, i + 2) for i in range(0, 40, 2) - ] - assert set(row["physical_id"] for row in proof["train"]).isdisjoint( - row["physical_id"] for row in proof["valid"] - ) - with pytest.raises(ValueError, match="coverage"): - make_replay_split_proof(train, valid, **{**kwargs, "expected_episodes": 21}) - key = next(iter(valid.datasets)) - valid.datasets[key] = next(iter(train.datasets.values())) - with pytest.raises(ValueError, match="physical rows"): - make_replay_split_proof(train, valid, **kwargs) - - -def test_joint_operational_split_materializes_450_50_and_rejects_wrong_binding( - tmp_path, monkeypatch -): - import copy - from pathlib import Path - - import hydra - from hydra.core.hydra_config import HydraConfig - from omegaconf import OmegaConf - - from egomimic.benchmarks.libero.native_launch_profiles import ( - SPATIAL_HAND_JOINT_PROFILE, - ) - from scripts.train.libero_native_launch_contract import ( - joint_split_contract, - normalize_joint_context, - sha, - ) - - root = Path(__file__).resolve().parents[1] - manifests = {} - for name, width, hand in (("libero_panda", 7, 2), ("libero_panda_sharpa", 69, 22)): - path = tmp_path / name - replay(path, width, hand, demos_per_task=50) - manifest = dict(complete=True, action_dim=width) - if width == 69: - manifest.update( - action_representation="wrist_hand_keypoints", - keypoint_frame="wrist_relative_Quest_XYZ_m", - observation_alignment="causal_pre_action_raw_lossless", - ) - (path / "benchmark_manifest.json").write_text(json.dumps(manifest)) - manifests[name] = sha(path / "benchmark_manifest.json") - monkeypatch.setenv("LIBERO_ORIGINAL_SPATIAL_ROOT", str(tmp_path / "libero_panda")) - monkeypatch.setenv( - "LIBERO_SHARPA_SPATIAL_ROOT", str(tmp_path / "libero_panda_sharpa") - ) - monkeypatch.setenv("LIBERO_DIAGNOSTIC_SEED_BANK", str(tmp_path / "seeds.json")) - with hydra.initialize_config_dir( - version_base="1.2", config_dir=str(root / "egomimic/hydra_configs") - ): - cfg = hydra.compose( - config_name="train_zarr_cartesian", - return_hydra_config=True, - overrides=[ - "hydra/launcher=basic", - "+experiment=" + SPATIAL_HAND_JOINT_PROFILE.experiment, - "trainer.precision=bf16-mixed", - "trainer.devices=1", - ], - ) - cfg.hydra.runtime.output_dir = str(tmp_path / "output") - HydraConfig.instance().set_config(cfg) - resolved = OmegaConf.to_container( - OmegaConf.masked_copy(cfg, [k for k in cfg if k != "hydra"]), resolve=True - ) - result = joint_split_contract(resolved, manifests, expected_episodes=500) - assert result["status"] == "PASS" and result["launch_ready"] is False - for proof in result["sources"].values(): - assert len(proof["train"]) == 450 and len(proof["valid"]) == 50 - assert proof["frames"] == 1000 and proof["complete_episode_and_frame_coverage"] - with pytest.raises(ValueError, match="manifest SHA"): - joint_split_contract( - resolved, {**manifests, "libero_panda": "0" * 64}, expected_episodes=500 - ) - wrong = copy.deepcopy(resolved) - for mode in ("train", "valid"): - wrong["data"][mode + "_datasets"]["libero_panda_sharpa"]["resolver"][ - "folder_path" - ] = str(tmp_path / "libero_panda") - with pytest.raises(ValueError, match="distinct"): - joint_split_contract(wrong, manifests, expected_episodes=500) - # Real operational handoff: physical proof from a YAML config -> configured - # DataModule fitting -> complete context, with no model or trainer. - original_config = tmp_path / "split-config.yaml" - OmegaConf.save(OmegaConf.create(resolved), original_config) - result["resolved_config"] = dict( - path=str(original_config), sha256=sha(original_config) - ) - split_path = tmp_path / "split.json" - split_path.write_text(json.dumps(result)) - fit = copy.deepcopy(resolved) - fit["norm_stats_only"] = True - fit["norm_stats"].update( - precomputed_norm_path=None, - save_cache_dir=str(tmp_path / "stats"), - export_context_path=str(tmp_path / "context.json"), - ) - fit["run_provenance"].update( - split_manifest_path=str(split_path), split_manifest_sha256=sha(split_path) - ) - fit["data"]["run_provenance"] = copy.deepcopy(fit["run_provenance"]) - norm = normalize_joint_context(fit) - assert norm["status"] == "PASS" and norm["launch_ready"] is False - context = json.loads((tmp_path / "context.json").read_text())["data_context"] - assert set(context["normalizer_state"]["norm_stats"]) == {"21", "22"} - assert ( - len(context["normalizer_state"]["norm_stats"]["22"]["actions"]["scale"]) == 69 - ) - # The checkpoint verifier must restore the same complete state without data IO. - from egomimic.pipeline.checkpoint_binding import checkpoint_binding - from egomimic.rldb.zarr.data_module import load_data_context - from scripts.train.verify_libero_native_action_flow_smoke import ( - validate_joint_checkpoint_context, - ) - - checkpoint_config = copy.deepcopy(fit) - checkpoint_config["norm_stats"]["precomputed_norm_path"] = str( - tmp_path / "context.json" - ) - checkpoint_config["run_provenance"].update( - source_commit="1" * 40, normalization_sha256=sha(tmp_path / "context.json") - ) - restored_context = load_data_context(tmp_path / "context.json") - checkpoint = dict( - hyper_parameters=dict(config_tree=checkpoint_config), - data_context=restored_context.snapshot(), - inference_binding=checkpoint_binding(checkpoint_config, restored_context), - ) - assert ( - validate_joint_checkpoint_context(checkpoint, checkpoint_config)[ - "checkpoint_binding" - ] - == checkpoint["inference_binding"] - ) - # Native checkpoints keep integer embodiment keys/tuples; JSON exports do not. - native_context = copy.deepcopy(checkpoint["data_context"]) - native_context["normalizer_state"] = restored_context.normalizer.to_state() - assert native_context != checkpoint["data_context"] - checkpoint["data_context"] = native_context - assert ( - validate_joint_checkpoint_context(checkpoint, checkpoint_config)[ - "checkpoint_binding" - ] - == checkpoint["inference_binding"] - ) - wrong_checkpoint = copy.deepcopy(checkpoint) - wrong_checkpoint["data_context"].pop("preprocessing") - with pytest.raises(ValueError, match="full data context differs"): - validate_joint_checkpoint_context(wrong_checkpoint, checkpoint_config) - # Consume genuine fixture context/split plus explicitly synthetic constructor/runtime metadata. - from scripts.train.libero_native_launch_contract import ( - contract_sha, - joint_preflight_evidence, - reference, - ) - - pre = copy.deepcopy(checkpoint_config) - pre["norm_stats_only"] = False - pre["trainer"]["accelerator"] = "cpu" - pre["run_provenance"]["run_kind"] = "preflight" - config_path = tmp_path / "preflight.yaml" - OmegaConf.save(OmegaConf.create(pre), config_path) - constructor_path = tmp_path / "synthetic-constructor.json" - constructor = dict( - status="PASS", - full_model_constructor=True, - parameter_count=78182614, - source_commit="1" * 40, - resolved_config_sha256=sha(config_path), - model_contract_sha256=contract_sha(pre), - ) - constructor_path.write_text(json.dumps(constructor)) - runtime_path = tmp_path / "synthetic-runtime.json" - runtime_path.write_text( - json.dumps( - { - "python": {}, - "installed_distributions": {}, - "core_distribution_versions": {}, - "dependency_inputs": {}, - "torch_runtime": {}, - "source": {"head": "1" * 40, "dirty": False}, - } - ) - ) - proof = joint_preflight_evidence( - pre, - config_path, - constructor_ref=reference(constructor_path), - runtime_ref=reference(runtime_path), - source_head="1" * 40, - ) - assert proof["launch_ready"] is False - assert proof["complete_context"]["sha256"] == sha(tmp_path / "context.json") - constructor["source_commit"] = "0" * 40 - constructor_path.write_text(json.dumps(constructor)) - with pytest.raises(ValueError, match="never relabel cached"): - joint_preflight_evidence( - pre, - config_path, - constructor_ref=reference(constructor_path), - runtime_ref=reference(runtime_path), - source_head="1" * 40, - ) - with pytest.raises(ValueError, match="fresh absolute"): - normalize_joint_context(fit) - fit["norm_stats"]["export_context_path"] = str(tmp_path / "other-context.json") - result["sources"]["libero_panda"]["episodes"] = 499 - split_path.write_text(json.dumps(result)) - fit["run_provenance"]["split_manifest_sha256"] = sha(split_path) - with pytest.raises(ValueError, match="split/content binding"): - normalize_joint_context(fit) - fit["data"]["train_datasets"]["libero_panda"]["valid_ratio"] = 0.01 - with pytest.raises(ValueError, match="episode split differs"): - normalize_joint_context(fit) - - -@pytest.mark.parametrize( - "action", ["joint-normalize", "joint-preflight", "joint-compose", "joint-prepare"] -) -@pytest.mark.parametrize("step", ["batch", "extern"]) -def test_joint_data_cli_rejects_non_numeric_scheduler_step( - tmp_path, monkeypatch, step, action -): - import sys - - from scripts.train.libero_native_launch_contract import main - - monkeypatch.setenv("SLURM_JOB_ID", "1234") - monkeypatch.setenv("SLURM_STEP_ID", step) - monkeypatch.setattr( - sys, - "argv", - [ - "contract", - "--action", - action, - "--repo", - str(tmp_path), - "--output", - str(tmp_path / "out"), - ], - ) - with pytest.raises(ValueError, match="scheduled numeric srun"): - main() - assert not (tmp_path / "out").exists() - - -def test_train_only_limits_separate_bodies_and_roundtrip(tmp_path): - datasets = {} - for name, width, hand in [("libero_panda", 7, 2), ("libero_panda_sharpa", 28, 22)]: - path = tmp_path / (name + ".zarr") - group = replay(path, width, hand) - r = resolver(path, width == 28) - train = LiberoDataset._from_resolver(r, mode="train", valid_ratio=0.1) - valid = LiberoDataset._from_resolver(r, mode="valid", valid_ratio=0.1) - assert not set(train.datasets) & set(valid.datasets) - for episode in valid.datasets.values(): - group["data/action"][episode.start : episode.end] = 1000 - datasets[name] = train - norm = LiberoMultiEmbodimentNormalizer() - norm.populate_from_datasets(datasets) - for name, dataset in datasets.items(): - norm.infer_shapes_from_batch(dataset[0]) - norm.infer_norm_from_dataset(dataset, name) - ident = dataset.resolver.embodiment_id - assert norm.norm_stats[ident]["actions"]["max"].max() < 1 - dataset.set_norm_stats_from(norm) - sample = dataset[0] - raw = next(iter(dataset.datasets.values()))[0] - restored = norm.unnormalize({"actions": sample["actions"]}, ident) - torch.testing.assert_close(restored["actions"], raw["actions"]) - assert norm.norm_stats[21]["actions"]["scale"].shape == (7,) - assert norm.norm_stats[22]["actions"]["scale"].shape == (28,) - restored = LiberoMultiEmbodimentNormalizer.from_state(norm.to_state()) - assert restored.tokenizer_context() == norm.tokenizer_context() - valid = LiberoDataset._from_resolver( - datasets["libero_panda"].resolver, mode="valid" - ) - with pytest.raises(ValueError, match="training episodes"): - LiberoMultiEmbodimentNormalizer().infer_norm_from_dataset(valid, "invalid") - - -def test_complete_joint_context_export_restores_without_refitting( - tmp_path, monkeypatch -): - import copy - - from omegaconf import OmegaConf - - from egomimic.rldb.zarr.data_module import ZarrDataModule - - train, valid = {}, {} - for name, width, hand in (("libero_panda", 7, 2), ("libero_panda_sharpa", 69, 22)): - path = tmp_path / (name + ".zarr") - replay(path, width, hand) - if width == 69: - (path / "benchmark_manifest.json").write_text( - json.dumps( - dict( - complete=True, - action_dim=69, - action_representation="wrist_hand_keypoints", - keypoint_frame="wrist_relative_Quest_XYZ_m", - observation_alignment="causal_pre_action_raw_lossless", - ) - ) - ) - train[name] = dict( - _target_="egomimic.rldb.zarr.libero_dataset.LiberoDataset._from_resolver", - resolver=dict( - _target_="egomimic.rldb.zarr.libero_dataset.LiberoReplayResolver", - folder_path=str(path), - suite="libero_spatial", - embodiment=name, - action_dim=width, - gripper_state_dim=hand, - action_representation="native" - if width == 7 - else "wrist_hand_keypoints", - key_map=dict( - _target_="egomimic.rldb.zarr.libero_dataset.keymap", - n_obs_steps=2, - horizon=32, - ), - ), - mode="train", - valid_ratio=0.1, - split_seed=42, - ) - valid[name] = {**copy.deepcopy(train[name]), "mode": "valid"} - - def module(): - return ZarrDataModule(OmegaConf.create(train), OmegaConf.create(valid), {}, {}) - - normalizer = OmegaConf.create( - dict( - _target_="egomimic.rldb.zarr.libero_dataset.LiberoMultiEmbodimentNormalizer" - ) - ) - exported = tmp_path / "complete-context.json" - context = module().prepare_context( - mode="normalization", - normalizer=normalizer, - normalization=dict( - norm_mode="libero_train_limits", - save_cache_dir=str(tmp_path / "stats"), - export_context_path=str(exported), - ), - ) - payload = json.loads(exported.read_text())["data_context"] - assert payload["normalizer_config"] == OmegaConf.to_container(normalizer) - assert set(payload["preprocessing"]) == {"21", "22"} - assert ( - payload["normalizer_state"]["native_embodiment_contexts"]["22"]["action_dim"] - == 69 - ) - - def forbidden(*args, **kwargs): - raise AssertionError("A complete-context cache must not refit normalization") - - monkeypatch.setattr( - LiberoMultiEmbodimentNormalizer, "infer_norm_from_dataset", forbidden - ) - restored = module().prepare_context( - mode="train", - normalizer=normalizer, - normalization=dict( - norm_mode="libero_train_limits", precomputed_norm_path=str(exported) - ), - ) - assert ( - restored.normalizer.tokenizer_context() - == context.normalizer.tokenizer_context() - ) - assert restored.fingerprint() == context.fingerprint() - # A changed export target is a hard collision, not an overwrite or refit. - exported.write_text('{"different": true}\n') - with pytest.raises(ValueError, match="Refusing to replace"): - module().prepare_context( - mode="normalization", - normalizer=normalizer, - restored_state=payload, - normalization=dict( - norm_mode="libero_train_limits", - save_cache_dir=str(tmp_path / "other-stats"), - export_context_path=str(exported), - ), - ) - assert exported.read_text() == '{"different": true}\n' - assert not list(tmp_path.glob(".context-*")) - - -def test_original_state_is_identity_and_sharpa_projector_gets_gradient(): - stage = KeyedFeatureProjection( - selector_key="embodiment", - input_key=["eef", "quat", "hand", "task"], - output_key="proprio_condition", - output_dim=10, - selector_aliases={21: "panda", 22: "sharpa"}, - projections={ - "panda": {"input_dim": 10, "identity": True}, - "sharpa": {"input_dim": 30, "hidden_dim": 32}, - }, - ) - for ident, hand_width in [(21, 2), (22, 22)]: - batch = { - "embodiment": torch.tensor([ident, ident]), - "eef": torch.randn(2, 2, 3), - "quat": torch.randn(2, 2, 4), - "hand": torch.randn(2, 2, hand_width), - "task": torch.randn(2, 2, 1), - } - expected = torch.cat([batch[k] for k in ("eef", "quat", "hand", "task")], -1) - result = stage(batch)["proprio_condition"] - assert result.shape == (2, 2, 10) - if ident == 21: - assert torch.equal(result, expected) - else: - result.square().mean().backward() - assert all( - p.grad is not None and torch.isfinite(p.grad).all() - for p in stage.projections["sharpa"].parameters() - ) - with pytest.raises(ValueError, match="must match"): - KeyedFeatureProjection( - "body", - "x", - "y", - {"panda": {"input_dim": 9, "identity": True}}, - output_dim=10, - ) - - -def test_native_collection_keeps_upright_rows_quaternion_and_timestamps(tmp_path): - root = tmp_path / "raw" - root.mkdir() - records = [] - image = np.zeros((2, 2, 2, 3), dtype="uint8") - image[:, 0, :, 0] = 240 - image[:, 1, :, 2] = 170 - for i, task in enumerate(get_tasks("libero_spatial")): - filename = f"task-{i}.hdf5" - with h5py.File(root / filename, "w") as handle: - handle.attrs["metadata_json"] = json.dumps( - {"suite": "libero_spatial", "task": task, "embodiment": "sharpa"} - ) - handle.attrs["automatic_success"] = True - data = handle.create_group("data") - data.attrs["rgb_row_transform"] = ( - "vertical_flip_from_raw_opengl_to_original_zarr_upright" - ) - demo = data.create_group("demo_0") - demo.attrs["observation_alignment"] = "after_action; raw timeline retained" - demo.create_dataset("actions", data=np.zeros((2, 28), dtype="float32")) - demo.create_dataset("action_host_ns", data=[10, 30]) - demo.create_dataset("observation_host_ns", data=[20, 40]) - obs = demo.create_group("obs") - obs.create_dataset("agentview_rgb", data=image) - obs.create_dataset("eye_in_hand_rgb", data=image) - obs.create_dataset("ee_pos", data=np.zeros((2, 3))) - obs.create_dataset("ee_quat", data=[[0, 0, 0, 1]] * 2) - obs.create_dataset("ee_ori", data=np.zeros((2, 3))) - obs.create_dataset("gripper_states", data=np.zeros((2, 22))) - legacy_view = native_episode_values(demo, task, action_dim=28, hand_dim=22) - np.testing.assert_array_equal(legacy_view["agentview_rgb"], image[:, ::-1]) - records.append( - { - "path": filename, - "task": task, - "suite": "libero_spatial", - "action_dimension": 28, - "automatic_success": True, - } - ) - manifest = { - "suite": "libero_spatial", - "embodiment": "custom_Panda_SharpaWaveRight_LIBERO", - "episodes": records, - "episode_count": 10, - "per_task": {r["task"]: 1 for r in records}, - } - m = root / "manifest.json" - m.write_text(json.dumps(manifest)) - output = tmp_path / "converted.zarr" - receipt = convert_collection_manifest(root, output, m) - arrays = zarr.open_group(str(output), mode="r")["data"] - np.testing.assert_array_equal(arrays["agentview_rgb"][:2], image) - np.testing.assert_array_equal(arrays["robot0_eef_quat"][:2], [[0, 0, 0, 1]] * 2) - np.testing.assert_array_equal(arrays["action_host_ns"][:2], [10, 30]) - np.testing.assert_array_equal(arrays["observation_host_ns"][:2], [20, 40]) - assert receipt["complete"] and not receipt["split_assigned"] - assert len(receipt["episodes"]) == 10 - manifest["episodes"][0]["path"] = "../outside.hdf5" - m.write_text(json.dumps(manifest)) - with pytest.raises(ValueError, match="outside root"): - convert_collection_manifest(root, tmp_path / "rejected.zarr", m) - assert not (tmp_path / "rejected.zarr").exists() - - -def test_keypoint_alignment_rejects_asynchronous_latest_guess(tmp_path): - from egomimic.benchmarks.libero.convert import aligned_hand_keypoint_actions - - metadata = { - "hand_calibration": { - "operator_wrist_position_quest_m": [0, 0, 0], - "position_world_m": [0, 0, 0], - }, - "world_from_quest": np.eye(3).tolist(), - "control_settings": {"translation_gain": 1.0, "vertical_gain": 1.0}, - } - points = np.arange(63).reshape(21, 3) * 0.001 - packets = [ - { - "source": "quest_webxr", - "hand": "right", - "joint_layout": "mediapipe21.v2", - "tracking_valid": True, - "joint_valid": [True] * 21, - "sequence": i, - "wrist": {"position_m": [x, 0, 0]}, - "joints_m": (points + x).tolist(), - } - for i, x in enumerate((0.01, 0.02)) - ] - with h5py.File(tmp_path / "record.hdf5", "w") as h: - h.create_dataset("packet_json", data=[json.dumps(p) for p in packets]) - h.create_dataset("packet_receive_ns", data=[10, 20]) - h.create_dataset("action_host_ns", data=[30, 40]) - h.create_dataset( - "intent_target_position_world_m", data=[[0.01, 0, 0], [0.02, 0, 0]] - ) - h.create_dataset("actions", data=np.zeros((2, 28))) - h.create_dataset("tracking_valid", data=[True, True]) - actions, start, sequence = aligned_hand_keypoint_actions(h, metadata) - assert actions.shape == (2, 69) and start == 0 - assert sequence.tolist() == [ - 0, - 1, - ] # Latest-at-action would wrongly choose 1 twice. - np.testing.assert_allclose( - actions[0, 6:], (points - points[0]).reshape(63), atol=1e-8 - ) - h["intent_target_position_world_m"][1] = [0.03, 0, 0] - with pytest.raises(ValueError, match="correspondence at step 1"): - aligned_hand_keypoint_actions(h, metadata) - h["intent_target_position_world_m"][1] = [0.02, 0, 0] - # Recorded IK failure pauses the control after applying a valid-hand - # command; this is not a missing operator hand target. - metadata["action_contract"] = { - "arm_control": {"name": "mink_direct_joint_servo_v4", "mode": "mink"} - } - h.create_dataset("control_reason", data=[b"active", b"arm_ik_infeasible"]) - h["tracking_valid"][1] = False - repaired, _, _ = aligned_hand_keypoint_actions(h, metadata) - np.testing.assert_array_equal(repaired, actions) - h["control_reason"][1] = b"tracking_lost" - with pytest.raises(ValueError, match="hand tracking target"): - aligned_hand_keypoint_actions(h, metadata) - h["control_reason"][1] = b"arm_ik_infeasible" - metadata["action_contract"]["arm_control"]["name"] = "unbound_controller" - with pytest.raises(ValueError, match="hand tracking target"): - aligned_hand_keypoint_actions(h, metadata) - metadata["action_contract"]["arm_control"]["name"] = ( - "mink_direct_joint_servo_v4" - ) - h.attrs["recenter_events_json"] = "[{}]" - with pytest.raises(ValueError, match="recenter calibration"): - aligned_hand_keypoint_actions(h, metadata) - - -def test_keypoint_resolver_requires_declared_frame_manifest(tmp_path): - path = tmp_path / "sharpa69.zarr" - replay(path, 69, 22) - r = LiberoReplayResolver( - path, - keymap(2, 32), - "libero_spatial", - action_dim=69, - gripper_state_dim=22, - embodiment="libero_panda_sharpa", - action_representation="wrist_hand_keypoints", - ) - manifest = path / "benchmark_manifest.json" - manifest.write_text( - json.dumps( - { - "complete": True, - "action_dim": 69, - "action_representation": "wrist_hand_keypoints", - "keypoint_frame": "wrist_relative_Quest_XYZ_m", - "observation_alignment": "causal_pre_action_raw_lossless", - } - ) - ) - assert next(iter(r.resolve().values()))[0]["actions"].shape == (32, 69) - manifest.write_text(json.dumps({"complete": True, "action_dim": 69})) - with pytest.raises(ValueError, match="frame manifest"): - r.resolve() - - -def test_sharpa_raw_pre_action_views_cannot_leak_post_action_frames(tmp_path): - from egomimic.benchmarks.libero.convert import causal_collection_episode_values - - with h5py.File(tmp_path / "timeline.hdf5", "w") as h: - h.create_dataset("actions", data=np.zeros((3, 28))) - h.create_dataset("observation_host_ns", data=[10, 30, 50, 70]) - h.create_dataset("action_host_ns", data=[20, 40, 60]) - data = h.create_group("data") - data.attrs["rgb_row_transform"] = ( - "vertical_flip_from_raw_opengl_to_original_zarr_upright" - ) - rgb = h.create_group("rgb") - pixels = np.zeros((4, 2, 2, 3), dtype=np.uint8) - for t in range(4): - pixels[t, 0, :, 0] = 10 + t - pixels[t, 1, :, 2] = 100 + t - rgb.create_dataset("agentview", data=pixels) - rgb.create_dataset("robot0_eye_in_hand", data=pixels) - obs = h.create_group("numeric_observations") - obs.create_dataset("robot0_eef_pos", data=np.arange(12).reshape(4, 3)) - obs.create_dataset("robot0_eef_quat", data=[[0, 0, 0, 1]] * 4) - obs.create_dataset( - "robot0_gripper_qpos", data=np.repeat(np.arange(4)[:, None], 22, axis=1) - ) - meta = {"observation_order": "before_action_and_terminal"} - result = causal_collection_episode_values( - h, - meta, - get_tasks("libero_spatial")[0], - np.ones((2, 69), dtype=np.float32), - 1, - np.array([2, 3]), - ) - np.testing.assert_array_equal(result["agentview_rgb"], pixels[1:3, ::-1]) - np.testing.assert_array_equal( - result["robot0_eef_pos"], np.arange(12).reshape(4, 3)[1:3] - ) - np.testing.assert_array_equal(result["observation_host_ns"], [30, 50]) - np.testing.assert_array_equal(result["action_host_ns"], [40, 60]) - assert np.all(result["observation_host_ns"] < result["action_host_ns"]) - assert result["robot0_gripper_qpos"][:, 0].tolist() == [1, 2] - h.create_dataset( - "control_reason", data=[b"operator_stop", b"stale_3d_view", b"active"] - ) - resumed_target = np.ones((1, 69), dtype=np.float32) - with pytest.raises(ValueError, match="resume proof"): - causal_collection_episode_values( - h, - meta, - get_tasks("libero_spatial")[0], - resumed_target, - 2, - np.array([3]), - ) - proof = { - "status": "PASS_UNIQUE_SOURCE_BACKED_RESUME_CALIBRATION", - "source_start_step": 2, - "source_path": h.filename, - "solutions": [{"recorded_anchor": True}], - "no_later_gap_discarded": True, - } - resumed = causal_collection_episode_values( - h, - meta, - get_tasks("libero_spatial")[0], - resumed_target, - 2, - np.array([3]), - resume_proof=proof, - ) - np.testing.assert_array_equal(resumed["observation_host_ns"], [50]) - h["control_reason"][0] = b"active" - with pytest.raises(ValueError, match="discard active"): - causal_collection_episode_values( - h, - meta, - get_tasks("libero_spatial")[0], - resumed_target, - 2, - np.array([3]), - resume_proof=proof, - ) - h["observation_host_ns"][1] = 45 - with pytest.raises(ValueError, match="not causal"): - causal_collection_episode_values( - h, - meta, - get_tasks("libero_spatial")[0], - np.ones((2, 69), dtype=np.float32), - 1, - np.array([2, 3]), - ) - - -def test_routed_dit_half_preserves_three_owner_gradients_and_batch_contract(): - from copy import deepcopy - from types import SimpleNamespace - - from torch.utils.checkpoint import checkpoint - - from egomimic.utils.libero_dit_half import HalfCheckpointScope, LiberoDiTHalf - - torch.manual_seed(4) - backbones = { - name: SimpleNamespace( - blocks=torch.nn.ModuleList([torch.nn.Linear(4, 4), torch.nn.Linear(4, 4)]), - gradient_checkpointing=True, - ) - for name in ("encoder/libero_panda", "encoder/libero_panda_sharpa", "velocity") - } - baseline = deepcopy(backbones) - native = SimpleNamespace(checkpoint=checkpoint) - scope = HalfCheckpointScope(native, backbones, expected_depth=2) - inputs = torch.randn(2, 4) - - def evaluate(owners, dispatcher): - x = inputs.clone().requires_grad_() - values = [] - for owner in owners.values(): - value = x - for block in owner.blocks: - value = dispatcher(block, value, use_reentrant=False) - values.append(value) - output = sum(values) - output.square().sum().backward() - return output, x.grad - - expected, expected_gradient = evaluate(baseline, checkpoint) - scope.install() - try: - actual, actual_gradient = evaluate(backbones, native.checkpoint) - torch.testing.assert_close(actual, expected, rtol=0, atol=0) - torch.testing.assert_close(actual_gradient, expected_gradient, rtol=0, atol=0) - for name in backbones: - for actual_block, reference_block in zip( - backbones[name].blocks, baseline[name].blocks - ): - torch.testing.assert_close( - actual_block.weight.grad, - reference_block.weight.grad, - rtol=0, - atol=0, - ) - assert all(count > 0 for count in scope.counts.values()) - callback = LiberoDiTHalf({"libero_panda": 16, "libero_panda_sharpa": 16}) - callback.scope = scope - batch = { - name: {"actions": torch.zeros(16, 32, width)} - for name, width in [("libero_panda", 7), ("libero_panda_sharpa", 69)] - } - callback.on_train_batch_start(None, None, batch, 0) - batch["libero_panda_sharpa"]["actions"] = torch.zeros(32, 32, 69) - with pytest.raises(ValueError, match="per-source batch"): - callback.on_train_batch_start(None, None, batch, 0) - finally: - scope.restore() - assert native.checkpoint is checkpoint - - -def test_collection_image_chunk_layout_keeps_pixels_actions_and_temporal_reads_exact( - tmp_path, -): - from egomimic.benchmarks.libero.convert import append_replay - - rng = np.random.default_rng(7) - values = { - "action": rng.normal(size=(19, 69)).astype(np.float32), - "agentview_rgb": rng.integers(0, 256, (19, 3, 4, 3), dtype=np.uint8), - "robot0_eye_in_hand_rgb": rng.integers(0, 256, (19, 3, 4, 3), dtype=np.uint8), - } - outputs = [] - for chunk in (1, 8): - group = zarr.open_group(str(tmp_path / str(chunk)), mode="w", zarr_format=2) - offset = 0 - for start, end in ((0, 3), (3, 11), (11, 19)): - offset = append_replay( - group, - {k: v[start:end] for k, v in values.items()}, - offset, - image_chunk_frames=chunk, - ) - assert offset == 19 and group["agentview_rgb"].chunks[0] == chunk - for key, expected in values.items(): - np.testing.assert_array_equal(group[key][:], expected) - np.testing.assert_array_equal(group[key][7:9], expected[7:9]) - outputs.append(group) - for key in values: - np.testing.assert_array_equal(outputs[0][key][:], outputs[1][key][:]) - with pytest.raises(ValueError, match="positive frame count"): - append_replay(outputs[0], values, 19, image_chunk_frames=0) - - -def test_hand_target_cache_reuses_verified_targets_and_rejects_source_or_cache_drift( - tmp_path, -): - import hashlib - - from egomimic.benchmarks.libero.convert import ( - cached_hand_keypoint_actions, - verified_hand_target_cache, - ) - - source = tmp_path / "episode.hdf5" - with h5py.File(source, "w") as h: - h.create_dataset("actions", data=np.zeros((3, 28))) - records = [{"path": source.name, "task": "task"}] - manifest = tmp_path / "manifest.json" - manifest.write_text(json.dumps({"episodes": records})) - cache = tmp_path / "targets.npz" - targets = np.arange(2 * 69, dtype=np.float32).reshape(2, 69) - np.savez_compressed( - cache, - episode_000000_actions=targets, - episode_000000_sequence=np.array([10, 11], dtype=np.int64), - ) - stat = source.stat() - row = { - "index": 0, - **records[0], - "source_bytes": stat.st_size, - "source_mtime_ns": stat.st_mtime_ns, - "source_start_step": 1, - "source_control_count": 3, - "target_shape": [2, 69], - "cache_key": "episode_000000", - } - audit = { - "status": "PASS_ALL_500_EXACT_CAUSAL_HAND_TARGETS", - "source_episode_count": 1, - "passed_episode_count": 1, - "failed_episode_count": 0, - "source_manifest_sha256": hashlib.sha256(manifest.read_bytes()).hexdigest(), - "target_cache": str(cache), - "target_cache_sha256": hashlib.sha256(cache.read_bytes()).hexdigest(), - "passed": [row], - } - audit_path = tmp_path / "audit.json" - audit_path.write_text(json.dumps(audit)) - _, rows = verified_hand_target_cache(audit_path, manifest, tmp_path, records) - with np.load(cache, allow_pickle=False) as arrays, h5py.File(source, "r") as h: - actual, start, sequence = cached_hand_keypoint_actions(arrays, rows[0], h) - np.testing.assert_array_equal(actual, targets) - assert start == 1 and sequence.tolist() == [10, 11] - with source.open("ab") as stream: - stream.write(b"changed") - with pytest.raises(ValueError, match="source identity changed"): - verified_hand_target_cache(audit_path, manifest, tmp_path, records) - with cache.open("ab") as stream: - stream.write(b"changed") - with pytest.raises(ValueError, match="cache SHA differs"): - verified_hand_target_cache(audit_path, manifest, tmp_path, records) - - -def test_joint_energy_verifier_consumes_real_generic_saved_artifact( - tmp_path, monkeypatch -): - import copy - from pathlib import Path - from types import SimpleNamespace - - import hydra - from hydra.core.hydra_config import HydraConfig - from omegaconf import OmegaConf - - from egomimic.eval.energy_score import energy_score - from egomimic.eval.planar_action_eval import PlanarActionEval - from scripts.train.verify_libero_native_action_flow_smoke import ( - validate_joint_energy_artifact, - ) - - root = Path(__file__).resolve().parents[1] - monkeypatch.setenv("LIBERO_ORIGINAL_SPATIAL_ROOT", str(tmp_path / "original")) - monkeypatch.setenv("LIBERO_SHARPA_SPATIAL_ROOT", str(tmp_path / "sharpa")) - monkeypatch.setenv( - "LIBERO_DIAGNOSTIC_SEED_BANK", - str( - root / "egomimic/hydra_configs/evaluator/energy_score_seed_bank_k32_v1.json" - ), - ) - with hydra.initialize_config_dir( - version_base="1.2", config_dir=str(root / "egomimic/hydra_configs") - ): - cfg = hydra.compose( - config_name="train_zarr_cartesian", - return_hydra_config=True, - overrides=[ - "hydra/launcher=basic", - "+experiment=sim/libero/action_flow_libero_spatial_sharpa_gripper_keypoints_s42", - ], - ) - cfg.hydra.runtime.output_dir = str(tmp_path) - HydraConfig.instance().set_config(cfg) - cfg.evaluator.seed_bank_path = str( - root / "egomimic/hydra_configs/evaluator/energy_score_seed_bank_k32_v1.json" - ) - cfg.evaluator.artifact_root = str(tmp_path / "energy") - cfg.run_provenance.source_commit = "1" * 40 - cfg.run_provenance.normalization_sha256 = "2" * 64 - cfg.run_provenance.split_manifest_sha256 = "3" * 64 - resolved = OmegaConf.to_container( - OmegaConf.masked_copy(cfg, [k for k in cfg if k != "hydra"]), resolve=True - ) - options = copy.deepcopy(resolved["evaluator"]) - options.pop("_target_") - options["action_flow_diagnostics"] = None - evaluator = PlanarActionEval(**options) - evaluator.trainer = SimpleNamespace( - global_step=8, current_epoch=0, global_rank=0, precision="bf16-mixed" - ) - samples, scores, batch = {}, {}, {} - generator = torch.Generator().manual_seed(42) - for name, identity, width in ( - ("libero_panda", 21, 7), - ("libero_panda_sharpa", 22, 69), - ): - samples[name] = torch.randn(32, 16, 32, width, generator=generator) - target = torch.randn(16, 32, width, generator=generator) - batch[name] = dict(embodiment=torch.full((16,), identity), actions=target) - scores[name] = energy_score( - samples[name], - target, - resolved["evaluator"]["semantic_blocks_by_embodiment"][name], - ) - evaluator._save_artifact(0, samples, scores, batch) - path = next((tmp_path / "energy").rglob("*.pt")) - from tests.test_verify_action_flow_training_smoke import MODULE - - selected, payload = MODULE._step_artifact( - tmp_path / "energy", label="joint EnergyScore", expected_step=8 - ) - assert selected == path - with pytest.raises(MODULE.SmokeVerificationError, match="one step-2"): - MODULE._step_two_artifact(tmp_path / "energy", label="joint EnergyScore") - result = validate_joint_energy_artifact(payload, resolved) - assert set(result["sources"]) == {"libero_panda", "libero_panda_sharpa"} - payload["domains"]["libero_panda_sharpa"]["score_by_condition"][0] += 1 - with pytest.raises(ValueError, match="public math differs"): - validate_joint_energy_artifact(payload, resolved) - - # Exercise the real immutable diagnostic writer with synthetic captured tensors. - from egomimic.eval.action_flow_diagnostics import ActionFlowDiagnostics - from scripts.train.verify_libero_native_action_flow_smoke import ( - validate_joint_diagnostic_artifact, - ) - - diagnostic_seeds = tmp_path / "diagnostic-seeds.json" - diagnostic_seeds.write_text( - json.dumps({"seeds": [730001 + 7919 * index for index in range(32)]}, indent=2) - + "\n" - ) - resolved["evaluator"]["action_flow_diagnostics"]["noise_seed_bank_path"] = str( - diagnostic_seeds - ) - inspector = ActionFlowDiagnostics(resolved["evaluator"]["action_flow_diagnostics"]) - diagnostic = dict( - schema_version=1, - metric="ActionFlowValidationDiagnostics", - global_step=8, - epoch=0, - rank=0, - batch_idx=0, - precision="bf16-mixed", - identity=inspector.identity, - identity_sha256=inspector.identity_sha256, - provenance=inspector.provenance, - validation_view=inspector.validation_view, - noise_seed_bank_sha256=inspector.noise_seed_bank_sha256, - noise_seed=inspector.noise_seeds[0], - raw_noise_levels=inspector.noise_levels, - sampler=dict(name="reverse_euler"), - sources={}, - ) - for name, width in (("libero_panda", 7), ("libero_panda_sharpa", 69)): - raw = dict( - source="validation/" + name, - schema=inspector.schema, - noise_seed=inspector.noise_seeds[0], - num_inference_steps=50, - returned_samples=8, - ) - for key, shape in ( - ("target", (8, 32, width)), - ("condition", (8, 276)), - ("latent/clean", (8, 16, 16)), - ("latent/noise", (8, 16, 16)), - ("latent/generated", (8, 16, 16)), - ("decoded/reconstruction", (8, 32, width)), - ("decoded/generated", (8, 32, width)), - ("latent/trajectory", (51, 8, 16, 16)), - ("decoded/trajectory", (51, 8, 32, width)), - ): - raw[key] = torch.zeros(shape) - diagnostic["sources"][name] = dict( - source_id=raw["source"], - diagnostic=raw, - computed=dict(clean_reconstruction_mse_by_condition=torch.zeros(8)), - ) - diagnostic_path = tmp_path / "diagnostics.pt" - inspector._write_immutable_artifact(diagnostic_path, diagnostic) - assert validate_joint_diagnostic_artifact(diagnostic_path, diagnostic, resolved)[ - "sha256" - ] - sidecar = Path(str(diagnostic_path) + ".sha256") - sidecar.write_text(sidecar.read_text().replace(inspector.identity_sha256, "0" * 64)) - with pytest.raises(ValueError, match="sidecar differs"): - validate_joint_diagnostic_artifact(diagnostic_path, diagnostic, resolved) - - # Metadata-only producer fixture: real saved tensor files, synthetic runtime/preflight. - (tmp_path / "checkpoints").mkdir() - (tmp_path / "checkpoints/epoch-0000-step-000000008.ckpt").write_bytes(b"fixture") - from scripts.train.libero_native_launch_contract import ( - contract_sha, - joint_smoke_evidence, - reference, - ) - - resolved["run_provenance"]["run_kind"] = "smoke" - resolved["trainer"]["max_steps"] = 8 - artifact_root = tmp_path / "diag-envelope" - resolved["evaluator"]["action_flow_diagnostics"]["artifact_root"] = str( - artifact_root - ) - nested = artifact_root / "epoch-0-step-8/rank-0-batch-0.pt" - nested.parent.mkdir(parents=True) - inspector._write_immutable_artifact(nested, diagnostic) - (tmp_path / ".hydra").mkdir() - OmegaConf.save(OmegaConf.create(resolved), tmp_path / ".hydra/config.yaml") - runtime = { - key: {} - for key in ( - "python", - "installed_distributions", - "core_distribution_versions", - "dependency_inputs", - "source", - "torch_runtime", - ) - } - runtime_path = tmp_path / "runtime-lock.json" - runtime_path.write_text(json.dumps(runtime)) - synthetic_preflight = dict( - schema="libero-joint-launch-preflight/v1", - status="PASS", - profile="sim/libero/action_flow_libero_spatial_sharpa_gripper_keypoints_s42", - model_contract_sha256=contract_sha(resolved), - runtime_lock=reference(runtime_path), - **{ - key: resolved["run_provenance"][key] - for key in ( - "source_commit", - "split_manifest_sha256", - "normalization_sha256", - ) - }, - ) - preflight_path = tmp_path / "synthetic-preflight.json" - preflight_path.write_text(json.dumps(synthetic_preflight)) - envelope = joint_smoke_evidence( - resolved, - tmp_path, - preflight_ref=reference(preflight_path), - runtime_ref=reference(runtime_path), - shared=MODULE, - ) - assert envelope["energy_artifact"] == reference(path) - assert envelope["diagnostic_artifact"] == reference(nested) - assert ( - "status" not in envelope - ) # An envelope never claims smoke or launch acceptance. - resolved["run_provenance"]["run_kind"] = "full" - with pytest.raises(ValueError, match="eight-update smoke"): - joint_smoke_evidence( - resolved, - tmp_path, - preflight_ref=reference(preflight_path), - runtime_ref=reference(runtime_path), - shared=MODULE, - ) - resolved["run_provenance"]["run_kind"] = "smoke" - runtime["source"] = {"wrong": True} - different_runtime = tmp_path / "wrong-runtime.json" - different_runtime.write_text(json.dumps(runtime)) - with pytest.raises(AssertionError, match="native runtime differs"): - joint_smoke_evidence( - resolved, - tmp_path, - preflight_ref=reference(preflight_path), - runtime_ref=reference(different_runtime), - shared=MODULE, - ) - - from scripts.train.verify_libero_native_action_flow_smoke import ( - validate_joint_wandb_records, - ) - from tests.test_verify_action_flow_training_smoke import _history_row - - row = _history_row() - for key, value in list(row.items()): - if MODULE.SOURCE_LABEL in key: - for label in ("libero_panda", "libero_panda_sharpa"): - row[key.replace(MODULE.SOURCE_LABEL, label)] = value - row["Train/ActionFlow/Compute/FieldForwardCallsPerStep"] = 2.0 - row["Train/ActionFlow/Compute/FieldSampleEquivalentsPerStep"] = 28.0 - for prefix in ( - "GradientCosine", - "GradientCosineDefined", - "GradientIntersectionParameterCount", - ): - row[f"Train/ActionFlow/{prefix}/FM__Reconstruction"] = 0.0 - row["Optimizer/LR/AdamW"] = row["Optimizer/LR/Muon"] = 2.5e-8 - for owner in ("encoder/libero_panda", "encoder/libero_panda_sharpa", "velocity"): - for mode in ("direct", "checkpoint"): - row["Train/Execution/DiTHalf/" + owner + "/" + mode] = 6.0 - resolved["logger"] = dict(wandb=dict(mode="online", id="synthetic-history-fixture")) - histories = dict( - offline={7: row}, - online={7: row}, - online_config=resolved, - online_state="finished", - cfg=resolved, - shared=MODULE, - ) - assert validate_joint_wandb_records(**histories)["online"]["valid_step"] == 7 - import copy - - logged = copy.deepcopy(resolved) - logged["model"] = json.loads(json.dumps(logged["model"])) - logged["model"]["action_horizon"] = "${libero_blueprint.action_horizon}" - histories["online_config"] = logged - assert validate_joint_wandb_records(**histories)["online"]["valid_step"] == 7 - logged["model"]["action_horizon"] = 999 - with pytest.raises(ValueError, match="online W&B model differs"): - validate_joint_wandb_records(**histories) - histories["online_config"] = resolved - histories["online_state"] = "running" - with pytest.raises(ValueError, match="finished online"): - validate_joint_wandb_records(**histories) - histories["online_state"] = "finished" - row["Train/Execution/DiTHalf/encoder/libero_panda_sharpa/checkpoint"] = 0.0 - with pytest.raises(ValueError, match="path inactive"): - validate_joint_wandb_records(**histories) - - -def test_joint_constructor_explicit_waiver_keeps_cached_evidence_immutable(tmp_path): - import hashlib - - from scripts.train.libero_native_launch_contract import validate_joint_constructor - - original = tmp_path / "original.json" - original.write_text( - json.dumps( - dict( - status="PASS_ACTUAL_CPU_MODEL_CONSTRUCTION_NOT_TRAINING_SMOKE", - total_parameters=78182614, - private_codecs_per_embodiment=True, - single_shared_velocity_stage=True, - all_parameters_finite=True, - ) - ) - ) - before = original.read_bytes() - waiver = tmp_path / "waiver.json" - data = dict( - schema="libero-joint-constructor-provenance-waiver/v1", - authorization="explicit_human_override", - waived_gate="cached_constructor_source_config_binding", - source_commit="a" * 40, - human_message="bypass the provenance gate", - original_constructor=dict( - path=str(original), sha256=hashlib.sha256(before).hexdigest() - ), - ) - - def check(): - waiver.write_text(json.dumps(data)) - ref = dict( - path=str(waiver), sha256=hashlib.sha256(waiver.read_bytes()).hexdigest() - ) - return validate_joint_constructor( - ref, source_head="a" * 40, resolved_sha="b" * 64, fingerprint="c" * 64 - ) - - assert check() == "HUMAN_WAIVED_SOURCE_CONFIG_BINDING" - assert original.read_bytes() == before - data["source_commit"] = "d" * 40 - with pytest.raises(ValueError, match="authorization/source"): - check() - data["source_commit"] = "a" * 40 - data["authorization"] = "" - with pytest.raises(ValueError, match="authorization/source"): - check() diff --git a/tests/test_libero_native_binding_parser.py b/tests/test_libero_native_binding_parser.py deleted file mode 100644 index 96973f9df6..0000000000 --- a/tests/test_libero_native_binding_parser.py +++ /dev/null @@ -1,75 +0,0 @@ -"""Real Hydra grammar regression; no model construction or smoke claims.""" - -import importlib.util -import unittest -from pathlib import Path - -spec = importlib.util.spec_from_file_location( - "native_schema", - Path(__file__).parents[1] / "scripts/train/libero_native_schema_contract.py", -) -module = importlib.util.module_from_spec(spec) -spec.loader.exec_module(module) -KEYS = ( - "path", - "file_sha256", - "normalizer_module_sha256", - "source_commit", - "dataset_receipt_path", - "dataset_receipt_sha256", - "split_receipt_path", - "split_receipt_sha256", - "data_root", - "normalizer_target", - "physical_proof_path", - "physical_proof_sha256", -) - - -class BindingParserTests(unittest.TestCase): - def test_real_twelve_field_hydra_mapping(self): - expected = { - k: ( - "/workspace/" + k - if k.endswith("path") or k == "data_root" - else "abc123" - ) - for k in KEYS - } - arg = ( - "++norm_stats.native_saved_state_binding={" - + ",".join(k + ":" + v for k, v in expected.items()) - + "}" - ) - self.assertEqual(module.parse_native_saved_state_binding([arg]), expected) - - def test_malformed_and_incomplete(self): - for value in ("{path:/workspace/x}", "{path:/workspace/x", "null", "[x,y]"): - with self.subTest(value=value), self.assertRaises(Exception): - module.parse_native_saved_state_binding( - ["++norm_stats.native_saved_state_binding=" + value] - ) - - def test_sweep_and_delete(self): - arg = ( - "++norm_stats.native_saved_state_binding={" - + ",".join(k + ":abc" for k in KEYS) - + "}" - ) - for anomaly in ("trainer.max_steps=2,3", "~trainer.max_steps"): - with self.subTest(anomaly=anomaly), self.assertRaises(ValueError): - module.parse_native_saved_state_binding([arg, anomaly]) - - def test_missing_and_duplicate(self): - arg = ( - "++norm_stats.native_saved_state_binding={" - + ",".join(k + ":abc" for k in KEYS) - + "}" - ) - for argv in ([], [arg, arg], [arg, arg.replace("++", "", 1)]): - with self.subTest(argv=argv), self.assertRaises(ValueError): - module.parse_native_saved_state_binding(argv) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_libero_native_half_factory_identity.py b/tests/test_libero_native_half_factory_identity.py deleted file mode 100644 index f63c645f6e..0000000000 --- a/tests/test_libero_native_half_factory_identity.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Historical half-ResNet factory identity; no torch import for static guard.""" - -import ast -import hashlib -from pathlib import Path - - -def test_native_half_factory_matches_historical_architecture_and_initialization(): - path = Path(__file__).parents[1] / "egomimic/models/stems/visual_core.py" - source = path.read_text() - tree = ast.parse(source) - branches = [ - n - for n in ast.walk(tree) - if isinstance(n, ast.If) - and ast.unparse(n.test) == "resnet_model == 'resnet18_half'" - ] - assert len(branches) == 1, "native historical half-ResNet factory unavailable" - assert ( - # Pinned from training source7c253e5, including the non-half else arm. - # Ignore whitespace only; retain every call, initializer and argument. - hashlib.sha256( - ast.dump(branches[0], include_attributes=False).encode() - ).hexdigest() - == "9521e4594c50d2e98b25fc9c07ef2d0c9aac42c83b57e0d0f55c8128056d8a75" - ) diff --git a/tests/test_libero_native_launch_contract.py b/tests/test_libero_native_launch_contract.py deleted file mode 100644 index d595828a2d..0000000000 --- a/tests/test_libero_native_launch_contract.py +++ /dev/null @@ -1,102 +0,0 @@ -"""Focused fingerprint invariants only; not native constructor or smoke proof.""" - -import copy -import unittest - -from scripts.train.libero_native_launch_contract import contract_sha - - -class Contract(unittest.TestCase): - def setUp(self): - self.cfg = dict( - model={ - "pipeline": { - "stages": [ - { - "flow_aggregation": "sum_samples", - "inference_method": "euler", - "num_inference_steps": 50, - } - ] - }, - "gradient_telemetry_cadence": 100, - }, - normalizer={"_target_": "native"}, - norm_stats={"native_saved_state_binding": {"source_commit": "fixed"}}, - data={ - "train_datasets": {"libero_panda": {"split_seed": 42}}, - "valid_datasets": {"libero_panda": {"split_seed": 42}}, - "train_dataloader_params": {"libero_panda": {"batch_size": 32}}, - "valid_dataloader_params": {"libero_panda": {"batch_size": 32}}, - }, - trainer={ - "precision": "bf16-mixed", - "accumulate_grad_batches": 1, - "max_steps": 80000, - }, - seed=42, - callbacks={ - "ema": {"validate_with_ema": True}, - "dit_half": {"_target_": "native"}, - }, - evaluator={ - "energy_sample_count": 32, - "energy_seed_bank_sha256": "fixed", - "artifact_root": "/out/a", - "artifact_identity": { - "_target_": "factory", - "config_path": "/out/a/.hydra/config.yaml", - }, - "native_diagnostic_config": { - "_target_": "diagfactory", - "config_path": "/out/a/.hydra/config.yaml", - "artifact_root": "/out/a/diagnostic", - }, - }, - ) - - def test_phase_only_equivalent(self): - cfg = copy.deepcopy(self.cfg) - cfg["trainer"]["max_steps"] = 2 - cfg["model"]["gradient_telemetry_cadence"] = 2 - self.assertEqual(contract_sha(cfg), contract_sha(self.cfg)) - - def test_output_only_equivalent(self): - cfg = copy.deepcopy(self.cfg) - cfg["evaluator"]["artifact_root"] = "/out/b" - cfg["evaluator"]["artifact_identity"]["config_path"] = ( - "/out/b/.hydra/config.yaml" - ) - cfg["evaluator"]["native_diagnostic_config"]["artifact_root"] = ( - "/out/b/diagnostic" - ) - self.assertEqual(contract_sha(cfg), contract_sha(self.cfg)) - - def test_mean_rejected(self): - cfg = copy.deepcopy(self.cfg) - cfg["model"]["pipeline"]["stages"][0]["flow_aggregation"] = "mean" - self.assertNotEqual(contract_sha(cfg), contract_sha(self.cfg)) - - def test_seedbank_rejected(self): - cfg = copy.deepcopy(self.cfg) - cfg["evaluator"]["energy_seed_bank_sha256"] = "changed" - self.assertNotEqual(contract_sha(cfg), contract_sha(self.cfg)) - - def test_k_changed_rejected(self): - cfg = copy.deepcopy(self.cfg) - cfg["evaluator"]["energy_sample_count"] = 16 - self.assertNotEqual(contract_sha(cfg), contract_sha(self.cfg)) - - def test_factory_target_rejected(self): - cfg = copy.deepcopy(self.cfg) - cfg["evaluator"]["native_diagnostic_config"]["_target_"] = "otherfactory" - self.assertNotEqual(contract_sha(cfg), contract_sha(self.cfg)) - - def test_sampling_rejected(self): - cfg = copy.deepcopy(self.cfg) - cfg["model"]["pipeline"]["stages"][0]["num_inference_steps"] = 8 - self.assertNotEqual(contract_sha(cfg), contract_sha(self.cfg)) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_libero_native_scheduler_contract.py b/tests/test_libero_native_scheduler_contract.py deleted file mode 100644 index bd35a9c9a6..0000000000 --- a/tests/test_libero_native_scheduler_contract.py +++ /dev/null @@ -1,101 +0,0 @@ -import importlib.util -from pathlib import Path - -import pytest - -spec = importlib.util.spec_from_file_location( - "guard", Path(__file__).parents[1] / "scripts/train/validate_slurm_job_contract.py" -) -m = importlib.util.module_from_spec(spec) -spec.loader.exec_module(m) -PROFILE = "sim/libero_historical/action_flow_libero10_h240_euler50_dithalf_80k_s42" - - -def inputs(): - fields = m.parse_scontrol_record( - ( - Path(__file__).parent / "fixtures/libero_native_scontrol_408787.txt" - ).read_text() - ) - args = dict( - expected_job_id="408787", - expected_account="loaner", - expected_partition="rl2", - expected_qos="loaner-normal", - expected_cpus=8, - expected_memory="128G", - expected_time_limit="02:00:00", - expected_constraint="(null)", - native_profile=PROFILE, - gpu_probe=dict( - status="PASSED", - gpu_name="NVIDIA H100 80GB HBM3", - world_size=1, - rank=0, - local_rank=0, - bf16_supported=True, - bf16_forward_backward=dict(finite=True), - ), - ) - return fields, args - - -def test_native_actual_h100_no_constraint(): - f, a = inputs() - assert not m.evaluate_contract(f, **a)[2] - - -@pytest.mark.parametrize( - "change", - [ - "wrongprofile", - "missingprobe", - "wronggpu", - "nonfinite", - "wrongallocation", - "constraint", - "planar", - ], -) -def test_reject_contract_drift(change): - f, a = inputs() - if change == "wrongprofile": - a["native_profile"] = "planar" - if change == "missingprobe": - a["gpu_probe"] = None - if change == "wronggpu": - a["gpu_probe"]["gpu_name"] = "NVIDIA H200" - if change == "nonfinite": - a["gpu_probe"]["bf16_forward_backward"]["finite"] = False - if change == "wrongallocation": - f["GRES"] = "gpu:a40:1(IDX:3)" - if change == "constraint": - f["Features"] = "H100" - if change == "planar": - a["native_profile"] = None - try: - failures = m.evaluate_contract(f, **a)[2] - except m.ContractError: - return - assert failures - - -@pytest.mark.parametrize( - "field,value", - [ - ("JOB_GRES", "gpu:h100:2"), - ("TresPerNode", "gres/gpu:h200:1"), - ("Nodes", "gpu18"), - ("GRES", "gpu:h100:1"), - ("AllocTRES", "cpu=4,mem=128G,node=1"), - ("ReqTRES", "cpu=8,mem=128G,node=1,gres/gpu=1"), - ], -) -def test_actual_allocation_drift(field, value): - f, a = inputs() - f[field] = value - try: - failures = m.evaluate_contract(f, **a)[2] - except m.ContractError: - return - assert failures diff --git a/tests/test_libero_native_smoke_contract.py b/tests/test_libero_native_smoke_contract.py deleted file mode 100644 index 5b35b5da89..0000000000 --- a/tests/test_libero_native_smoke_contract.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Focused pure metadata tests; never a full-model smoke proof.""" - -import unittest - -from scripts.train.verify_libero_native_action_flow_smoke import ( - PARAMETER_COUNT, - PROFILE, - validate_dit_counts, - validate_preflight, -) - - -class Contract(unittest.TestCase): - def test_missing_preflight(self): - with self.assertRaises(KeyError): - validate_preflight({}, {}) - - def test_tiny_count_not_accepted(self): - self.assertEqual(PARAMETER_COUNT, 39750391) - self.assertEqual( - PROFILE, - "sim/libero_historical/action_flow_libero10_h240_euler50_dithalf_80k_s42", - ) - - def test_preparation_rejected(self): - with self.assertRaisesRegex(ValueError, "did not pass"): - validate_preflight( - {"schema": "libero-native-launch-preflight/v1", "status": "PREPARED"}, - {}, - ) - - def test_wrong_profile_rejected(self): - with self.assertRaisesRegex(ValueError, "profile"): - validate_preflight( - { - "schema": "libero-native-launch-preflight/v1", - "status": "PASS", - "profile": "invented", - }, - {}, - ) - - def test_dit_inactive_rejected(self): - class Shared: - @staticmethod - def _complete_row(rows, required, **kw): - return 1, {name: 0 for name in required} - - with self.assertRaisesRegex(ValueError, "inactive"): - validate_dit_counts({}, Shared) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_libero_native_suite_profiles.py b/tests/test_libero_native_suite_profiles.py deleted file mode 100644 index d65bcbbf8b..0000000000 --- a/tests/test_libero_native_suite_profiles.py +++ /dev/null @@ -1,857 +0,0 @@ -"""Suite/seed isolation and portable allocated-resource contracts, without models.""" - -import copy -import importlib.util -import sys -from pathlib import Path - -import pytest - -ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(ROOT)) -sys.path.insert(0, str(ROOT / "tools")) -from libero_maintained_dispatch_v1 import ( # noqa: E402 - task tool path initialized above - validate_data_receipt, -) - - -def test_joint_schedule_reuse_hashes_exact_git_diff_and_rejects_model_changes(tmp_path): - import hashlib - import json - import subprocess - - from scripts.train.libero_native_launch_contract import ( - joint_smoke_schedule_reuse, - reference, - ) - - repo = tmp_path / "source" - repo.mkdir() - - def git(*args): - return subprocess.check_output( - [ - "git", - "-C", - str(repo), - "-c", - "user.name=fixture", - "-c", - "user.email=fixture@example.invalid", - *args, - ] - ) - - git("init", "-q") - source = repo / "egomimic/eval/eval.py" - source.parent.mkdir(parents=True) - source.write_text("old schedule\n") - git("add", ".") - git("commit", "-qm", "old") - old = git("rev-parse", "HEAD").decode().strip() - source.write_text("repaired schedule\n") - git("commit", "-qam", "repair") - head = git("rev-parse", "HEAD").decode().strip() - preflight_path = tmp_path / "preflight.json" - preflight_path.write_text(json.dumps({"source_commit": old})) - smoke_path = tmp_path / "smoke.json" - smoke_path.write_text( - json.dumps( - { - "identities": {"source_commit": old}, - "preflight": reference(preflight_path), - } - ) - ) - regression = tmp_path / "regression.log" - regression.write_text("actual CPU evidence fixture\n") - proof = dict( - schema="libero-joint-smoke-schedule-reuse/v1", - scope="explicit_cross_epoch_validation_schedule", - from_source_commit=old, - to_source_commit=head, - target_repo=str(repo), - smoke=reference(smoke_path), - cpu_regression_status="PASS", - changed_paths=["egomimic/eval/eval.py"], - audited_diff_sha256=hashlib.sha256( - git("diff", "--no-ext-diff", "--binary", old, head) - ).hexdigest(), - cpu_regression=reference(regression), - ) - proof_path = tmp_path / "proof.json" - - def check(): - proof_path.write_text(json.dumps(proof)) - return joint_smoke_schedule_reuse( - reference(proof_path), - smoke_ref=reference(smoke_path), - target_source=proof["to_source_commit"], - ) - - assert check()[0] == old - proof["audited_diff_sha256"] = "0" * 64 - with pytest.raises(ValueError, match="audited diff differs"): - check() - model = repo / "egomimic/model.py" - model.write_text("changed model\n") - git("add", ".") - git("commit", "-qm", "forbidden") - proof["to_source_commit"] = git("rev-parse", "HEAD").decode().strip() - proof["changed_paths"] = ( - git("diff", "--name-only", old, "HEAD").decode().splitlines() - ) - proof["audited_diff_sha256"] = hashlib.sha256( - git("diff", "--no-ext-diff", "--binary", old, "HEAD") - ).hexdigest() - with pytest.raises(ValueError, match="unaudited source paths"): - check() - - -from egomimic.benchmarks.libero.native_launch_profiles import ( # noqa: E402 - task tool path initialized above - PROFILES, - profile_for_argv, - profile_for_config, -) - - -def cfg(suite): - p = PROFILES[suite] - return dict( - name=p.name, - benchmark=dict(suite=suite), - data={ - mode + "_datasets": {"libero_panda": {"resolver": {"suite": suite}}} - for mode in ("train", "valid") - }, - ) - - -@pytest.mark.parametrize("suite", list(PROFILES)) -def test_typed_suite_and_receipt_isolation(suite): - p = PROFILES[suite] - c = cfg(suite) - assert profile_for_config(c) == p - assert profile_for_argv(["+experiment=" + p.experiment]) == p - receipt = dict( - suite=suite, - replay_path="/replay", - logical_dataset_sha256="a" * 64, - episodes=500, - valid_ratio=0.01, - split_seed=42, - train_episodes=495, - valid_episodes=5, - action_dim=7, - task_uids=list(p.task_uids), - ) - # Use exact maintained receipt schema field names. - receipt.update( - dataset_logical_sha256="a" * 64, - frames=5000, - action_shape=[5000, 7], - train_episode_indices=list(range(495)), - valid_episode_indices=list(range(495, 500)), - ) - assert ( - validate_data_receipt( - receipt, - replay_path="/replay", - expected_logical_sha="a" * 64, - profile=p.experiment, - )["id_overlap"] - == 0 - ) - for other in PROFILES: - if other == suite: - continue - wrong = copy.deepcopy(c) - wrong["benchmark"]["suite"] = other - with pytest.raises(ValueError): - profile_for_config(wrong) - with pytest.raises((ValueError, KeyError)): - validate_data_receipt( - receipt, - replay_path="/replay", - expected_logical_sha="a" * 64, - profile=PROFILES[other].experiment, - ) - - -@pytest.mark.parametrize("suite", ["libero_spatial", "libero_goal", "libero_object"]) -def test_recipe_only_changes_suite_identity(suite): - text = ( - ROOT - / "egomimic/hydra_configs/experiment/sim/libero_historical" - / (PROFILES[suite].name + ".yaml") - ).read_text() - assert "action_flow_libero10_h240_euler50_dithalf_80k_s42" in text - assert "suite: " + suite in text - assert PROFILES[suite].replay_environment in text - assert all( - key not in text - for key in ("model:", "trainer:", "normalizer:", "optimizer:", "\nseed:") - ) - - -def test_catalog_task_uids_match_registry(): - from egomimic.benchmarks.libero.catalog import TASK_IDS, get_tasks - - for suite, p in PROFILES.items(): - assert tuple(TASK_IDS[t][2] for t in get_tasks(suite)) == p.task_uids - - -def scheduler(): - spec = importlib.util.spec_from_file_location( - "scheduler_suite", ROOT / "scripts/train/validate_slurm_job_contract.py" - ) - m = importlib.util.module_from_spec(spec) - spec.loader.exec_module(m) - return m - - -@pytest.mark.parametrize( - "constraint,gpu", - [ - ("H100", "NVIDIA H100 80GB HBM3"), - ("H200", "NVIDIA H200"), - ("H100|H200", "NVIDIA H200"), - ], -) -@pytest.mark.parametrize( - "profile", - [ - PROFILES["libero_spatial"].experiment, - "sim/libero/action_flow_libero_spatial_sharpa_gripper_keypoints_s42", - ], -) -def test_ice_native_preserves_real_resource_bindings(constraint, gpu, profile): - m = scheduler() - fields = dict( - JobId="123", - Account="account", - Partition="partition", - QOS="qos", - NumNodes="1", - NumTasks="1", - NumCPUs="8", - MinCPUsNode="8", - MinMemoryNode="128G", - Features=constraint, - TimeLimit="1-00:00:00", - ReqTRES="cpu=8,mem=128G,node=1,gres/gpu=1", - ) - fields["CPUs/Task"] = "8" - args = dict( - expected_job_id="123", - expected_account="account", - expected_partition="partition", - expected_qos="qos", - expected_cpus=8, - expected_memory="128G", - expected_time_limit="1-00:00:00", - expected_constraint=constraint, - native_profile=profile, - gpu_probe=dict( - status="PASSED", - gpu_name=gpu, - world_size=1, - rank=0, - local_rank=0, - bf16_supported=True, - bf16_forward_backward=dict(finite=True), - ), - ) - assert m.evaluate_contract(fields, **args)[2] == [] - for field in ("Account", "Partition", "QOS", "Features", "NumCPUs"): - bad = dict(fields) - bad[field] = "wrong" if field != "NumCPUs" else "4" - assert m.evaluate_contract(bad, **args)[2] - args["gpu_probe"]["bf16_forward_backward"]["finite"] = False - with pytest.raises(m.ContractError): - m.evaluate_contract(fields, **args) - - -@pytest.mark.parametrize("suite", list(PROFILES)) -@pytest.mark.parametrize("seed", [42, 43]) -def test_metric_training_seed_is_typed_and_not_split_seed(suite, seed): - from egomimic.benchmarks.libero.action_flow_artifacts import ( - SEED_SHA, - validate_identity, - ) - - identity = dict( - suite=suite, - source="libero_panda", - action_dim=7, - action_horizon=16, - seed=seed, - sample_count=32, - energy_seed_bank_sha256=SEED_SHA, - inference_method="euler", - inference_steps=50, - normalization_scope="training_episodes_only", - effective_batch_size=32, - homogeneous="not_applicable_single_source", - source_commit="a" * 40, - resolved_config_sha256="a" * 64, - split_sha256="a" * 64, - normalizer_state_sha256="a" * 64, - dataset_logical_sha256="a" * 64, - ) - if seed == 43 and suite not in ("libero_goal", "libero_object"): - with pytest.raises(ValueError): - validate_identity(identity) - else: - assert validate_identity(identity) - - -def test_late_preflight_cross_suite_rejected_before_evidence_reads(): - from scripts.train.verify_libero_native_action_flow_smoke import validate_preflight - - pf = dict( - schema="libero-native-launch-preflight/v1", - status="PASS", - profile=PROFILES["libero10"].experiment, - ) - for suite in ("libero_spatial", "libero_goal", "libero_object"): - with pytest.raises(ValueError, match="native profile mismatch"): - validate_preflight(pf, {}, PROFILES[suite].experiment) - - -@pytest.mark.parametrize( - "suite", ["libero_spatial", "libero_goal", "libero_object", "libero10"] -) -def test_actual_hydra_recipe_composes_same_science_and_fixed_split(monkeypatch, suite): - from hydra import compose, initialize_config_dir - from omegaconf import OmegaConf - - p = PROFILES[suite] - monkeypatch.setenv(p.replay_environment, "/replay") - for seed in [42, 43] if suite in ("libero_goal", "libero_object") else [42]: - with initialize_config_dir( - version_base="1.3", config_dir=str(ROOT / "egomimic/hydra_configs") - ): - c = compose( - config_name="train_zarr_cartesian", - overrides=[ - "hydra/launcher=basic", - "+experiment=" + p.experiment, - "seed=" + str(seed), - ], - ) - assert c.name == p.name and c.benchmark.suite == suite and c.seed == seed - assert c.data.train_datasets.libero_panda.split_seed == 42 - assert c.data.valid_datasets.libero_panda.split_seed == 42 - assert ( - c.model.hidden_dim == 240 - and c.model.action_horizon == 16 - and c.model.action_dim == 7 - ) - assert c.data.train_dataloader_params.libero_panda.batch_size == 32 - assert c.model.pipeline.stages[6].inference_method == "euler" - monkeypatch.setenv("LIBERO10_REPLAY_ROOT", "/reference") - with initialize_config_dir( - version_base="1.3", config_dir=str(ROOT / "egomimic/hydra_configs") - ): - reference = compose( - config_name="train_zarr_cartesian", - overrides=[ - "hydra/launcher=basic", - "+experiment=" + PROFILES["libero10"].experiment, - ], - ) - for field in ("model", "normalizer", "trainer", "callbacks", "evaluator"): - assert OmegaConf.to_container( - c[field], resolve=False - ) == OmegaConf.to_container(reference[field], resolve=False) - - -def test_explicit_joint_profile_and_contract_reject_single_source_or_semantic_drift( - tmp_path, monkeypatch -): - import hydra - from hydra.core.hydra_config import HydraConfig - from omegaconf import OmegaConf - - from egomimic.benchmarks.libero.native_launch_profiles import ( - joint_profile_for_config, - ) - - spec = importlib.util.spec_from_file_location( - "joint_contract_test", ROOT / "scripts/train/libero_native_launch_contract.py" - ) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - monkeypatch.setenv("LIBERO_ORIGINAL_SPATIAL_ROOT", str(tmp_path / "original")) - monkeypatch.setenv("LIBERO_SHARPA_SPATIAL_ROOT", str(tmp_path / "sharpa")) - monkeypatch.setenv("LIBERO_DIAGNOSTIC_SEED_BANK", str(tmp_path / "SEEDS.json")) - with hydra.initialize_config_dir( - version_base="1.2", config_dir=str(ROOT / "egomimic/hydra_configs") - ): - config = hydra.compose( - config_name="train_zarr_cartesian", - return_hydra_config=True, - overrides=[ - "hydra/launcher=basic", - "+experiment=sim/libero/action_flow_libero_spatial_sharpa_gripper_keypoints_s42", - "paths.output_dir=" + str(tmp_path), - "paths.work_dir=" + str(ROOT), - ], - ) - config.hydra.runtime.output_dir = str(tmp_path) - HydraConfig.instance().set_config(config) - c = OmegaConf.to_container( - OmegaConf.masked_copy(config, [k for k in config if k != "hydra"]), - resolve=True, - ) - assert joint_profile_for_config(c).sources[1] == ("libero_panda_sharpa", 22, 69, 22) - missing_latent = copy.deepcopy(c) - missing_latent["evaluator"]["action_flow_diagnostics"]["provenance"].pop( - "latent_shape" - ) - with pytest.raises(ValueError, match="diagnostic latent shape"): - joint_profile_for_config(missing_latent) - with pytest.raises(ValueError, match="unsupported native"): - profile_for_config(c) - contract = module.model_contract(c) - assert contract["schema"] == "libero-joint-scientific-contract/v1" - assert contract["train_batch"] == {"libero_panda": 16, "libero_panda_sharpa": 16} - assert module.contract_sha(c) == module.contract_sha(copy.deepcopy(c)) - relocated = copy.deepcopy(c) - relocated["callbacks"]["ema"]["final_checkpoint_path"] = ( - "/other/output/checkpoints/last.ckpt" - ) - relocated["evaluator"]["action_flow_diagnostics"]["artifact_root"] = ( - "/other/output/diagnostics" - ) - assert module.contract_sha(c) == module.contract_sha(relocated) - changed_ema = copy.deepcopy(c) - changed_ema["callbacks"]["ema"]["decay"] = 0.99 - assert module.contract_sha(c) != module.contract_sha(changed_ema) - changes = [ - ( - "model.pipeline.stages.4.encoders.libero_panda_sharpa.backbone.hidden_dim", - 384, - ), - ("model.pipeline.stages.6.field.backbone.depth", 24), - ("model.pipeline.stages.4.encoders.libero_panda_sharpa.action_dim", 7), - ("model.pipeline.stages.7.decoders.libero_panda_sharpa.condition_dim", 276), - ("model.pipeline.stages.6.experts", {}), - ("model.pipeline.stages.3.num_tokens", 8), - ("data.train_dataloader_params.libero_panda_sharpa.batch_size", 32), - ("data.train_datasets.libero_panda_sharpa.valid_ratio", 0.01), - ( - "data.train_datasets.libero_panda_sharpa.resolver.action_representation", - "native", - ), - ("model.pipeline.stages.6.num_inference_steps", 8), - ("normalizer._target_", "egomimic.rldb.zarr.libero_dataset.LiberoNormalizer"), - ] - for path, replacement in changes: - wrong = copy.deepcopy(c) - keys = path.split(".") - node = wrong - for key in keys[:-1]: - node = node[int(key)] if isinstance(node, list) else node[key] - node[keys[-1]] = replacement - with pytest.raises(ValueError, match="joint LIBERO"): - module.contract_sha(wrong) - - -@pytest.mark.parametrize("phase", ["normalization", "preflight", "smoke", "full"]) -def test_joint_phase_factory_composes_actual_paths_and_private_shared_contract( - tmp_path, monkeypatch, phase -): - import hydra - from hydra.core.hydra_config import HydraConfig - from omegaconf import OmegaConf - - from egomimic.benchmarks.libero.native_launch_profiles import ( - joint_phase_argv, - joint_profile_for_config, - ) - - # These env values must be replaced by the exact typed arguments, including - # spaces and '=' which must survive the shell/Hydra boundary literally. - for key in ( - "LIBERO_ORIGINAL_SPATIAL_ROOT", - "LIBERO_SHARPA_SPATIAL_ROOT", - "LIBERO_DIAGNOSTIC_SEED_BANK", - ): - monkeypatch.setenv(key, "/wrong/unselected") - original = str(tmp_path / "original data=1") - sharpa = str(tmp_path / "hand data=2") - args = dict( - source_root=str(ROOT), - original_root=original, - sharpa_root=sharpa, - output_root=str(tmp_path / phase), - context_path=str(tmp_path / "context.json"), - split_path=str(tmp_path / "split.json"), - split_sha256="a" * 64, - normalization_sha256=None if phase == "normalization" else "b" * 64, - source_commit="c" * 40, - diagnostic_seed_bank=str(tmp_path / "seeds.json"), - wandb_entity="test", - wandb_project="test", - wandb_run_id="joint-" + phase, - ) - argv = joint_phase_argv(phase=phase, **args) - with hydra.initialize_config_dir( - version_base="1.2", config_dir=str(ROOT / "egomimic/hydra_configs") - ): - cfg = hydra.compose( - config_name="train_zarr_cartesian", - return_hydra_config=True, - overrides=argv[1:], - ) - cfg.hydra.runtime.output_dir = str(tmp_path / phase) - HydraConfig.instance().set_config(cfg) - resolved = OmegaConf.to_container( - OmegaConf.masked_copy(cfg, [k for k in cfg if k != "hydra"]), resolve=True - ) - joint_profile_for_config(resolved) - assert resolved["evaluator"]["action_flow_diagnostics"]["provenance"][ - "latent_shape" - ] == [16, 16] - assert resolved["trainer"]["log_every_n_steps"] == (1 if phase == "smoke" else 10) - if phase != "normalization": - from scripts.train import libero_native_launch_contract as adapter - - def compose_exact(command, *, cwd, text): - assert command[1:3] == ["-m", "egomimic.trainHydra"] - assert command[3:-3] == argv - assert command[-3:] == ["--cfg", "job", "--resolve"] - assert cwd == ROOT and text is True - return OmegaConf.to_yaml(OmegaConf.create(resolved)) - - monkeypatch.setattr(adapter.subprocess, "check_output", compose_exact) - destination = tmp_path / (phase + "-composition") - destination.mkdir() - composed = adapter.compose_joint_phase( - args, phase=phase, output=destination, repo=ROOT - ) - assert composed["launch_ready"] is False - assert (destination / "exact-phase.argv0").read_bytes() == b"\0".join( - arg.encode() for arg in argv - ) + b"\0" - with pytest.raises(FileExistsError): - adapter.compose_joint_phase( - args, phase=phase, output=destination, repo=ROOT - ) - if phase in {"smoke", "full"}: - import json - - from scripts.train.libero_native_launch_contract import ( - contract_sha, - joint_prepare_evidence, - reference, - ) - - runtime = { - key: {} - for key in ( - "python", - "installed_distributions", - "core_distribution_versions", - "dependency_inputs", - "source", - "torch_runtime", - ) - } - runtime["dependency_inputs"] = [] - runtime_path = tmp_path / "synthetic-runtime.json" - runtime_path.write_text(json.dumps(runtime)) - identity = { - key: resolved["run_provenance"][key] - for key in ( - "source_commit", - "normalization_sha256", - "split_manifest_sha256", - ) - } - preflight = dict( - schema="libero-joint-launch-preflight/v1", - status="PASS", - profile="sim/libero/action_flow_libero_spatial_sharpa_gripper_keypoints_s42", - model_contract_sha256=contract_sha(resolved), - runtime_lock=reference(runtime_path), - **identity, - ) - preflight_path = tmp_path / "synthetic-preflight.json" - preflight_path.write_text(json.dumps(preflight)) - kwargs = dict( - phase=phase, - preflight_ref=reference(preflight_path), - runtime_ref=reference(runtime_path), - ) - if phase == "full": - with pytest.raises(ValueError, match="genuine eight-update"): - joint_prepare_evidence(resolved, **kwargs) - smoke = dict( - schema="libero-joint-action-flow-smoke/v1", - status="PASS", - profile=preflight["profile"], - model_contract_sha256=contract_sha(resolved), - identities=identity, - preflight=reference(preflight_path), - strict_online_and_alias_safe_ema_reload=True, - checkpoint=dict( - parameter_count=78182614, - global_step=8, - joint_ema=dict(alias_safe_strict_reload=True), - ), - ) - smoke_path = tmp_path / "synthetic-smoke.json" - smoke_path.write_text(json.dumps(smoke)) - kwargs["smoke_ref"] = reference(smoke_path) - assert joint_prepare_evidence(resolved, **kwargs)["launch_ready"] is False - if phase == "full": - from scripts.train import libero_native_launch_contract as adapter - - original_smoke = copy.deepcopy(smoke) - previous = dict(preflight, source_commit="d" * 40) - previous["model_contract_sha256"] = adapter.joint_contract_at_source( - resolved, "d" * 40 - ) - previous_runtime = dict(runtime, source={"head": "d" * 40, "dirty": False}) - old_runtime_path = tmp_path / "previous-runtime.json" - old_runtime_path.write_text(json.dumps(previous_runtime)) - previous["runtime_lock"] = reference(old_runtime_path) - runtime["source"] = {"head": identity["source_commit"], "dirty": False} - runtime_path.write_text(json.dumps(runtime)) - preflight["runtime_lock"] = reference(runtime_path) - preflight_path.write_text(json.dumps(preflight)) - kwargs["runtime_ref"] = reference(runtime_path) - kwargs["preflight_ref"] = reference(preflight_path) - previous_path = tmp_path / "previous-preflight.json" - previous_path.write_text(json.dumps(previous)) - smoke["identities"] = dict(identity, source_commit="d" * 40) - smoke["model_contract_sha256"] = previous["model_contract_sha256"] - smoke["preflight"] = reference(previous_path) - smoke_path.write_text(json.dumps(smoke)) - kwargs["smoke_ref"] = reference(smoke_path) - with pytest.raises(ValueError, match="smoke proof differs"): - joint_prepare_evidence(resolved, **kwargs) - repair_ref = {"path": "separate-repair-proof", "sha256": "e" * 64} - monkeypatch.setattr( - adapter, - "joint_smoke_schedule_reuse", - lambda *a, **k: ("d" * 40, previous), - ) - reused = joint_prepare_evidence( - resolved, **kwargs, smoke_source_repair_ref=repair_ref - ) - assert reused["smoke_source_repair"] == repair_ref - assert ( - json.loads(smoke_path.read_text())["identities"]["source_commit"] - == "d" * 40 - ) - # Source-local assets move with immutable checkout roots. Compare - # their actual bytes against the configured SHA before rebinding - # this one path, keeping every other scientific field exact. - asset = Path(resolved["evaluator"]["seed_bank_path"]) - payload = asset.read_bytes() - old_repo, new_repo = tmp_path / "old-source", tmp_path / "new-source" - old_repo.mkdir() - new_repo.mkdir() - old_seed, new_seed = old_repo / "seeds.json", new_repo / "seeds.json" - old_seed.write_bytes(payload) - new_seed.write_bytes(payload) - before = copy.deepcopy(resolved) - before["evaluator"]["seed_bank_path"] = str(old_seed) - expected = adapter.joint_contract_at_source(before, "d" * 40) - after = copy.deepcopy(resolved) - after["evaluator"]["seed_bank_path"] = str(new_seed) - rebound = dict(source_repo=old_repo, current_repo=new_repo) - assert ( - adapter.joint_contract_at_source(after, "d" * 40, **rebound) == expected - ) - assert after["evaluator"]["seed_bank_path"] == str(new_seed) - for changed in (old_seed, new_seed): - changed.write_bytes(b"changed seed bank") - with pytest.raises(ValueError, match="seed asset SHA differs"): - adapter.joint_contract_at_source(after, "d" * 40, **rebound) - changed.write_bytes(payload) - after["model"]["cfg_scale"] += 1 - assert ( - adapter.joint_contract_at_source(after, "d" * 40, **rebound) != expected - ) - previous["model_contract_sha256"] = "wrong" - with pytest.raises(ValueError, match="scientific contract differs"): - joint_prepare_evidence( - resolved, **kwargs, smoke_source_repair_ref=repair_ref - ) - smoke = original_smoke - smoke["checkpoint"]["global_step"] = 2 - smoke_path.write_text(json.dumps(smoke)) - kwargs["smoke_ref"] = reference(smoke_path) - with pytest.raises(ValueError, match="smoke proof differs"): - joint_prepare_evidence(resolved, **kwargs) - if phase == "preflight": - from scripts.train.libero_native_launch_contract import contract_sha - - for other_phase in ("smoke", "full"): - other_args = { - **args, - "output_root": str(tmp_path / other_phase), - "wandb_run_id": "joint-" + other_phase, - } - with hydra.initialize_config_dir( - version_base="1.2", config_dir=str(ROOT / "egomimic/hydra_configs") - ): - other = hydra.compose( - config_name="train_zarr_cartesian", - return_hydra_config=True, - overrides=joint_phase_argv(phase=other_phase, **other_args)[1:], - ) - other.hydra.runtime.output_dir = other_args["output_root"] - HydraConfig.instance().set_config(other) - other_resolved = OmegaConf.to_container( - OmegaConf.masked_copy(other, [k for k in other if k != "hydra"]), - resolve=True, - ) - assert contract_sha(resolved) == contract_sha(other_resolved) - if phase == "smoke": - from scripts.train.libero_native_launch_contract import joint_prepare_evidence - - assert resolved["model"]["gradient_telemetry_cadence"] == 1 - missing_telemetry = copy.deepcopy(resolved) - missing_telemetry["model"]["gradient_telemetry_cadence"] = 100 - with pytest.raises(ValueError, match="per-step gradient telemetry"): - joint_prepare_evidence( - missing_telemetry, phase="smoke", preflight_ref={}, runtime_ref={} - ) - - from scripts.train.verify_libero_native_action_flow_smoke import ( - validate_joint_smoke_config, - ) - - assert ( - validate_joint_smoke_config(resolved, args["output_root"], "c" * 40)[ - "optimizer_updates" - ] - == 8 - ) - for key, value in ( - ("trainer.max_steps", 2), - ("trainer.accumulate_grad_batches", 2), - ("run_provenance.run_kind", "full"), - ("logger.wandb.mode", "offline"), - ("callbacks.model_checkpoint.save_last", True), - ("run_provenance.source_commit", None), - ): - altered = OmegaConf.create(copy.deepcopy(resolved)) - OmegaConf.update(altered, key, value) - with pytest.raises( - ValueError, match="joint (smoke config mismatch|LIBERO:)" - ): - validate_joint_smoke_config(altered, args["output_root"], "c" * 40) - for mode in ("train", "valid"): - for source, root in ( - ("libero_panda", original), - ("libero_panda_sharpa", sharpa), - ): - assert ( - resolved["data"][mode + "_datasets"][source]["resolver"]["folder_path"] - == root - ) - assert resolved["callbacks"]["model_checkpoint"]["save_last"] is False - assert resolved["trainer"]["default_root_dir"] == args["output_root"] - assert ( - resolved["callbacks"]["model_checkpoint"]["dirpath"] - == args["output_root"] + "/checkpoints" - ) - assert ( - resolved["callbacks"]["ema"]["final_checkpoint_path"] - == args["output_root"] + "/checkpoints/last.ckpt" - ) - assert resolved["runtime"]["slurm_requeue_owner"] == "runner" - assert resolved["callbacks"]["early_recovery"]["first_step"] == 1000 - assert resolved["trainer"]["max_steps"] == (8 if phase == "smoke" else 120000) - if phase == "normalization": - assert "logger" not in resolved - # The experiment itself extends the evaluator body; group deletion - # would leave an invalid target-less partial mapping. Normalization-only - # trainHydra returns before model/trainer construction or evaluation. - assert resolved["evaluator"]["_target_"].endswith(".PlanarActionEval") - assert resolved["norm_stats"]["precomputed_norm_path"] is None - assert resolved["norm_stats_only"] is True - assert resolved["norm_stats"]["export_context_path"] == args["context_path"] - assert resolved["run_provenance"]["normalization_sha256"] is None - else: - assert resolved["norm_stats"]["precomputed_norm_path"] == args["context_path"] - assert resolved["logger"]["wandb"]["id"] == args["wandb_run_id"] - assert resolved["logger"]["wandb"]["save_dir"] == args["output_root"] - assert resolved["trainer"]["val_check_interval"] == ( - 8 if phase == "smoke" else 15000 - ) - with pytest.raises(ValueError, match="normalization_sha256"): - joint_phase_argv(phase=phase, **{**args, "normalization_sha256": None}) - for key, bad_value in ( - ("original_root", "relative/root"), - ("sharpa_root", "/path\nwith-newline"), - ("source_commit", "unknown"), - ("wandb_run_id", "unsafe id"), - ): - with pytest.raises(ValueError): - joint_phase_argv(phase=phase, **{**args, key: bad_value}) - - -def test_joint_phase_factory_data_only_snapshot_never_authorizes_training(tmp_path): - import hydra - from hydra.core.hydra_config import HydraConfig - from omegaconf import OmegaConf - - from egomimic.benchmarks.libero.native_launch_profiles import ( - joint_phase_argv, - joint_profile_for_config, - ) - - args = dict( - source_root=str(ROOT), - original_root=str(tmp_path / "original"), - sharpa_root=str(tmp_path / "sharpa"), - output_root=str(tmp_path / "out"), - context_path=str(tmp_path / "context.json"), - split_path=str(tmp_path / "split.json"), - split_sha256="a" * 64, - normalization_sha256=None, - source_commit=None, - diagnostic_seed_bank=str(tmp_path / "bank.json"), - wandb_entity="test", - wandb_project="test", - wandb_run_id="data-only", - data_preparation_source_sha256="d" * 64, - ) - argv = joint_phase_argv(phase="normalization", **args) - with hydra.initialize_config_dir( - version_base="1.2", config_dir=str(ROOT / "egomimic/hydra_configs") - ): - cfg = hydra.compose( - config_name="train_zarr_cartesian", - return_hydra_config=True, - overrides=argv[1:], - ) - cfg.hydra.runtime.output_dir = str(tmp_path / "out") - HydraConfig.instance().set_config(cfg) - resolved = OmegaConf.to_container( - OmegaConf.masked_copy(cfg, [k for k in cfg if k != "hydra"]), resolve=True - ) - joint_profile_for_config(resolved) - assert ( - resolved["norm_stats_only"] is True - and resolved["run_provenance"]["source_commit"] is None - ) - assert ( - resolved["run_provenance"]["data_preparation_source_snapshot_sha256"] - == "d" * 64 - ) - for phase in ("preflight", "smoke", "full"): - with pytest.raises(ValueError, match="data-only source snapshots"): - joint_phase_argv(phase=phase, **args) - with pytest.raises(ValueError, match="exact SHA"): - joint_phase_argv( - phase="normalization", **{**args, "data_preparation_source_sha256": None} - ) diff --git a/tests/test_libero_native_velocity_ablation.py b/tests/test_libero_native_velocity_ablation.py deleted file mode 100644 index 79879d24b0..0000000000 --- a/tests/test_libero_native_velocity_ablation.py +++ /dev/null @@ -1,276 +0,0 @@ -"""Explicit objective identity and single-variable resolved recipe regression.""" - -import copy -import sys -from pathlib import Path - -import pytest -from hydra import compose, initialize_config_dir -from hydra.core.hydra_config import HydraConfig -from omegaconf import OmegaConf - -ROOT = Path(__file__).resolve().parents[1] -sys.path[:0] = [str(ROOT), str(ROOT / "tools")] -from typed_libero_profile_v3 import ( # noqa: E402 - task tool path initialized above - COMMON, - PHASES, - validate_flat_config, -) - -from egomimic.benchmarks.libero.native_launch_profiles import ( # noqa: E402 - task tool path initialized above - AV0_PROFILES, - PROFILES, - profile_for_argv, - profile_for_config, -) - - -def recipe(profile, monkeypatch): - monkeypatch.setenv(profile.replay_environment, "/unchanged/replay") - with initialize_config_dir( - version_base="1.3", config_dir=str(ROOT / "egomimic/hydra_configs") - ): - cfg = compose( - config_name="train_zarr_cartesian", - overrides=["hydra/launcher=basic", "+experiment=" + profile.experiment], - return_hydra_config=True, - ) - cfg.hydra.runtime.output_dir = "/unchanged/output" - HydraConfig.instance().set_config(cfg) - OmegaConf.set_struct(cfg, False) - del cfg["hydra"] - return OmegaConf.to_container(cfg, resolve=True) - - -@pytest.mark.parametrize("suite", list(PROFILES)) -def test_only_resolved_model_velocity_weight_changes(suite, monkeypatch): - base = recipe(PROFILES[suite], monkeypatch) - ablated = recipe(AV0_PROFILES[suite], monkeypatch) - assert profile_for_config(base) == PROFILES[suite] - assert profile_for_config(ablated) == AV0_PROFILES[suite] - assert ( - profile_for_argv(["+experiment=" + AV0_PROFILES[suite].experiment]) - == AV0_PROFILES[suite] - ) - expected = copy.deepcopy(base["model"]) - expected["pipeline"]["stages"][8]["action_velocity_weight"] = 0.0 - assert ablated["model"] == expected - expected_data = copy.deepcopy(base["data"]) - expected_data["run_provenance"]["objective"]["action_velocity_weight"] = 0.0 - expected_data["run_provenance"]["ablation"] = "action_velocity_off" - assert ablated["data"] == expected_data - for key in ("trainer", "normalizer", "callbacks", "evaluator", "seed"): - assert ablated[key] == base[key], key - assert ablated["run_provenance"]["objective"] == { - **base["run_provenance"]["objective"], - "action_velocity_weight": 0.0, - } - bad = copy.deepcopy(ablated) - bad["run_provenance"]["objective"]["action_velocity_weight"] = 1.0 - with pytest.raises(ValueError, match="objective provenance"): - profile_for_config(bad) - bad = copy.deepcopy(base) - bad["run_provenance"]["ablation"] = "action_velocity_off" - with pytest.raises(ValueError, match="ablation provenance"): - profile_for_config(bad) - - -@pytest.mark.parametrize("weight", [0.0, 1.0]) -def test_typed_flat_guard_rejects_opposite_objective(weight): - flat = { - **COMMON, - **PHASES["smoke"], - "model.pipeline.stages.8.action_velocity_weight": weight, - } - for idx, owner in [(4, "encoder"), (6, "field")]: - for key, value in [("hidden_dim", 240), ("depth", 12), ("num_heads", 8)]: - flat[f"model.pipeline.stages.{idx}.{owner}.backbone.{key}"] = value - flat.update( - { - "data.train_source_names": ["libero_panda"], - "data.valid_source_names": ["libero_panda"], - "typed_profile.velocity_augmentation": False, - "typed_profile.homogeneous": "not_applicable_single_source", - "typed_profile.checkpoint_policy": "dit-half", - } - ) - assert validate_flat_config(flat, "smoke", action_velocity_weight=weight) - with pytest.raises(ValueError, match="resolved profile mismatch"): - validate_flat_config(flat, "smoke", action_velocity_weight=1.0 - weight) - - -def verifier_functions(): - # Execute exact pure verifier functions without importing GPU/model dependencies. - import ast - import math - from collections.abc import Mapping, Sequence - from typing import Any - - source = ast.parse( - (ROOT / "scripts/train/verify_action_flow_training_smoke.py").read_text() - ) - names = { - "SmokeVerificationError", - "_require", - "_metric", - "_complete_row", - "_validate_history", - "_validate_checkpoint_loss_schedule", - } - constants = { - "LEGACY_METHOD", - "GRAPH_METHOD", - "LIKELIHOOD_METHOD", - "STOPGRAD_METHOD", - "STOPGRAD_UNITE_METHOD", - "SOURCE_LABEL", - } - nodes = [ - ast.ImportFrom( - module="__future__", names=[ast.alias(name="annotations")], level=0 - ) - ] - namespace = dict( - math=math, Any=Any, Mapping=Mapping, Sequence=Sequence, OmegaConf=OmegaConf - ) - for node in ast.parse( - (ROOT / "tools/validate_action_flow_config.py").read_text() - ).body: - if isinstance(node, ast.Assign): - for target in node.targets: - if isinstance(target, ast.Name) and target.id in constants: - namespace[target.id] = ast.literal_eval(node.value) - for node in source.body: - if isinstance(node, (ast.FunctionDef, ast.ClassDef)) and node.name in names: - nodes.append(node) - if isinstance(node, ast.Assign): - for target in node.targets: - if isinstance(target, ast.Name) and target.id in constants: - namespace[target.id] = ast.literal_eval(node.value) - exec( - compile( - ast.fix_missing_locations(ast.Module(body=nodes, type_ignores=[])), - str(ROOT / "scripts/train/verify_action_flow_training_smoke.py"), - "exec", - ), - namespace, - ) - fixture = ast.parse( - (ROOT / "tests/test_verify_action_flow_training_smoke.py").read_text() - ) - import types - - namespace["MODULE"] = types.SimpleNamespace(**namespace) - row_node = next( - node - for node in fixture.body - if isinstance(node, ast.FunctionDef) and node.name == "_history_row" - ) - exec( - compile( - ast.fix_missing_locations(ast.Module(body=[row_node], type_ignores=[])), - "existing_history_fixture", - "exec", - ), - namespace, - ) - return namespace - - -def test_disabled_velocity_must_be_zero_weight_in_schedule_and_total(): - v = verifier_functions() - row = v["_history_row"]() - row["Train/ActionFlow/Schedule/EffectiveActionVelocityWeight"] = 0.0 - for suffix in ("", "/" + v["SOURCE_LABEL"]): - row[f"Train/ActionFlow/TotalLoss{suffix}_step"] = 2.5 - row["Valid/ActionFlow/TotalLoss"] = 3.0 - assert ( - v["_validate_history"]({2: row}, action_velocity_weight=0.0)["train_step"] == 2 - ) - bad = dict(row) - bad["Train/ActionFlow/TotalLoss_step"] = 3.75 - with pytest.raises(v["SmokeVerificationError"], match="weighted total"): - v["_validate_history"]({2: bad}, action_velocity_weight=0.0) - bad = dict(row) - bad["Train/ActionFlow/Schedule/EffectiveActionVelocityWeight"] = 1.0 - with pytest.raises(v["SmokeVerificationError"], match="joint smoke step"): - v["_validate_history"]({2: bad}, action_velocity_weight=0.0) - stages = [{} for _ in range(9)] - stages[8] = {"action_velocity_weight": 0.0} - cfg = OmegaConf.create( - { - "name": AV0_PROFILES["libero10"].name, - "benchmark": {"suite": "libero10"}, - "data": { - mode + "_datasets": { - "libero_panda": {"resolver": {"suite": "libero10"}} - } - for mode in ("train", "valid") - }, - "run_provenance": { - "ablation": "action_velocity_off", - "objective": {"action_velocity_weight": 0.0}, - }, - "model": { - "reconstruction_only_warmup_steps": 0, - "pipeline": {"stages": stages}, - }, - } - ) - schedule = { - "joint_objective_begins_at_global_step": 0, - "reconstruction_only_optimizer_steps": 0, - "joint_flow_weight": 1.0, - "joint_reconstruction_weight": 1.0, - "joint_action_velocity_weight": 0.0, - "schema_version": 1, - } - assert ( - v["_validate_checkpoint_loss_schedule"]( - schedule, cfg, reconstruction_weight=1.0, flow_weight=1.0 - ) - == 0 - ) - with pytest.raises(v["SmokeVerificationError"], match="loss schedule"): - v["_validate_checkpoint_loss_schedule"]( - {**schedule, "joint_action_velocity_weight": 1.0}, - cfg, - reconstruction_weight=1.0, - flow_weight=1.0, - ) - - # A nonnative config cannot weaken the historical checkpoint guard by setting stage7=0. - nonnative = OmegaConf.create( - { - "name": "unrelated", - "model": { - "reconstruction_only_warmup_steps": 0, - "pipeline": {"stages": [{}] * 7 + [{"action_velocity_weight": 0.0}]}, - }, - } - ) - with pytest.raises( - v["_validate_checkpoint_loss_schedule"].__globals__["SmokeVerificationError"], - match="loss schedule", - ): - v["_validate_checkpoint_loss_schedule"]( - schedule, nonnative, reconstruction_weight=1.0, flow_weight=1.0 - ) - assert ( - v["_validate_checkpoint_loss_schedule"]( - {**schedule, "joint_action_velocity_weight": 1.0}, - nonnative, - reconstruction_weight=1.0, - flow_weight=1.0, - ) - == 0 - ) - baseline = OmegaConf.create(OmegaConf.to_container(cfg, resolve=True)) - baseline.name = PROFILES["libero10"].name - baseline.run_provenance.objective.action_velocity_weight = 1.0 - baseline.run_provenance.ablation = None - baseline.model.pipeline.stages[8].action_velocity_weight = 1.0 - with pytest.raises(v["SmokeVerificationError"], match="loss schedule"): - v["_validate_checkpoint_loss_schedule"]( - schedule, baseline, reconstruction_weight=1.0, flow_weight=1.0 - ) diff --git a/tests/test_libero_native_whole_launcher.py b/tests/test_libero_native_whole_launcher.py deleted file mode 100644 index 7968d6454c..0000000000 --- a/tests/test_libero_native_whole_launcher.py +++ /dev/null @@ -1,417 +0,0 @@ -"""Execute the complete real Bash launcher through safe native tool handoff. - -No model, data, Python tool, GPU, scheduler, credential or training execution. -Every preceding guard/hash/path/clean-Git check and actual array remains real. -""" - -import hashlib -import json -import os -import shutil -import subprocess -import tempfile -import unittest -from pathlib import Path - -R = Path(__file__).parents[1] -PROFILE = "sim/libero_historical/action_flow_libero10_h240_euler50_dithalf_80k_s42" -PLANAR = "sim/pusht/action_flow_bc_usocket_recon1_s42" - - -def sha(p): - return hashlib.sha256(Path(p).read_bytes()).hexdigest() - - -class WholeLauncher(unittest.TestCase): - def setUp(self): - self.temp = tempfile.TemporaryDirectory() - self.root = Path(self.temp.name) - self.repo = self.root / "source" - self.repo.mkdir() - files = [ - "scripts/train/launch_action_flow_usocket.sbatch", - "scripts/train/libero_native_launch_phases.sh", - "scripts/train/libero_native_schema_contract.py", - "scripts/train/verify_action_flow_training_smoke.py", - "scripts/train/validate_slurm_job_contract.py", - "scripts/train/check_checkpoint_storage.py", - "scripts/ice/ice_gpu_probe.py", - "scripts/ice/ice_requeue_runner.py", - "scripts/ice/relay_batch_signals.sh", - "scripts/ice/validate_lightning_checkpoint.py", - "scripts/ice/capture_runtime_lock.py", - "tools/libero_maintained_dispatch_v1.py", - "tools/validate_action_flow_config.py", - "scripts/ice/validate_planar_dataset.py", - "egomimic/hydra_configs/experiment/" + PROFILE + ".yaml", - "egomimic/hydra_configs/experiment/" + PLANAR + ".yaml", - "egomimic/hydra_configs/experiment/sim/libero_historical/action_flow_libero_spatial_h240_euler50_dithalf_80k_s42.yaml", - "egomimic/hydra_configs/experiment/sim/libero_historical/action_flow_libero_goal_h240_euler50_dithalf_80k_s42.yaml", - "egomimic/hydra_configs/experiment/sim/libero_historical/action_flow_libero_object_h240_euler50_dithalf_80k_s42.yaml", - ] - from egomimic.benchmarks.libero.native_launch_profiles import AV0_PROFILES - - files += [ - "egomimic/hydra_configs/experiment/" + p.experiment + ".yaml" - for p in AV0_PROFILES.values() - ] - for rel in files: - p = self.repo / rel - p.parent.mkdir(parents=True, exist_ok=True) - shutil.copy2(R / rel, p) - self.launcher = self.repo / files[0] - for name in ["data", "output-parent"]: - (self.root / name).mkdir() - self.split = self.root / "split.json" - self.split.write_text("{}\n") - self.receipt = self.root / "receipt.json" - self.receipt.write_text("{}\n") - self.norm = self.root / "norm.json" - self.norm.write_text("{}\n") - self.preflight = self.root / "preflight.json" - self.preflight.write_text("{}\n") - self.override = self.root / "binding.override" - self.override.write_text( - '++norm_stats.native_saved_state_binding={test:"fixture-only-no-execution"}\n' - ) - self.mock = self.repo / "safe-tools/mock-python" - self.mock.parent.mkdir() - self.mock.write_text("""#!/bin/bash -set -Eeuo pipefail -case "$1" in - */capture_runtime_lock.py) - while test "$#" -gt 0; do - if test "$1" = --output; then shift; printf '{"scope":"mocked-runtime-no-python-execution"}\\n' > "$1"; break; fi - shift - done ;; - */libero_native_launch_contract.py) - printf 'NATIVE_DISPATCH_PREPARE_STOP\\n' - printf '%s\\n' "$*" > "$SAFE_HANDOFF_LOG" - replay_variable=${SAFE_REPLAY_VARIABLE:-LIBERO_SPATIAL_REPLAY_ROOT} - printf '%s\\n' "${!replay_variable:-}" > "$SAFE_HANDOFF_LOG.replay" - exit 91 ;; - *) printf 'UNEXPECTED_EXTERNAL_TOOL\\n' >&2; exit 92 ;; -esac -""") - self.mock.chmod(0o755) - cp = self.repo / "scripts/ice/validate_lightning_checkpoint.py" - cp.chmod(0o755) - subprocess.run(["git", "init", "-q", str(self.repo)], check=True) - subprocess.run(["git", "-C", str(self.repo), "add", "."], check=True) - subprocess.run( - [ - "git", - "-C", - str(self.repo), - "-c", - "user.name=Fixture", - "-c", - "user.email=fixture@example.invalid", - "commit", - "-qm", - "safe launcher fixture", - ], - check=True, - ) - head = subprocess.check_output( - ["git", "-C", str(self.repo), "rev-parse", "HEAD"], text=True - ).strip() - env = { - k: v - for k, v in os.environ.items() - if not k.startswith(("AF_", "ICE_", "SLURM_", "WANDB_", "LIBERO_")) - } - env.update( - AF_REPO=str(self.repo), - AF_EXPECTED_HEAD=head, - AF_PYTHON=str(self.mock), - AF_DATASET_DIR=str(self.root / "data"), - AF_OUTPUT_DIR=str(self.root / "output-parent/run"), - AF_EXPERIMENT=PROFILE, - AF_WANDB_ENTITY="rl2-group", - AF_WANDB_PROJECT="pushshapes-action-flow", - AF_WANDB_RUN_ID="lib10-noaug-smoke-ce2c-20261007", - AF_WANDB_GROUP="cotrain-velocity-first-wave-20261007", - AF_WANDB_TAGS="[libero10,noaug,action-flow,h240,dithalf,bf16,euler50,smoke]", - AF_EXPECTED_SPLIT_MANIFEST_SHA256=sha(self.split), - AF_EXPECTED_LAUNCHER_SHA256=sha(self.launcher), - AF_CONTENT_MANIFEST=str(self.receipt), - AF_EXPECTED_CONTENT_MANIFEST_SHA256=sha(self.receipt), - AF_EXPECTED_DATASET_CONTENT_AGGREGATE_SHA256="3ed2b86cf97eaa27045af81e6d6c4834c8659d15eda69a2394d835e602014e1d", - AF_NATIVE_DATASET_SHA256="3ed2b86cf97eaa27045af81e6d6c4834c8659d15eda69a2394d835e602014e1d", - AF_NATIVE_HISTORICAL_SPLIT_SHA256="c588c831ef0deab61a59a21840ea9e11dafe92a170d9023a87e0acee831df236", - AF_CLUSTER_LABEL="lambda", - AF_EXPECTED_ACCOUNT="loaner", - AF_EXPECTED_PARTITION="rl2", - AF_EXPECTED_QOS="loaner-normal", - AF_SPLIT_MANIFEST=str(self.split), - AF_NORM_STATS_PATH=str(self.norm), - AF_EXPECTED_NORM_SHA256=sha(self.norm), - AF_NATIVE_SCHEMA_DRIVER=str( - self.repo / "scripts/train/libero_native_schema_contract.py" - ), - AF_NATIVE_SCHEMA_DRIVER_SHA256=sha( - self.repo / "scripts/train/libero_native_schema_contract.py" - ), - AF_NATIVE_BINDING_OVERRIDE=str(self.override), - AF_NATIVE_BINDING_OVERRIDE_SHA256=sha(self.override), - AF_NATIVE_DATASET_VALIDATOR=str( - self.repo / "tools/libero_maintained_dispatch_v1.py" - ), - AF_NATIVE_CONFIG_VALIDATOR=str( - self.repo / "tools/libero_maintained_dispatch_v1.py" - ), - AF_NATIVE_SMOKE_VERIFIER=str( - self.repo / "scripts/train/verify_action_flow_training_smoke.py" - ), - AF_NATIVE_PHASE="smoke", - AF_PRECISION="bf16-mixed", - AF_LAUNCH_MODE="run", - AF_RUN_KIND="smoke", - AF_MAX_RESTARTS="0", - AF_EXPECTED_GPU_CONSTRAINT="(null)", - AF_EXPECTED_MEMORY="128G", - AF_EXPECTED_TIME_LIMIT="02:00:00", - AF_PREFLIGHT_RESULT=str(self.preflight), - AF_EXPECTED_PREFLIGHT_SHA256=sha(self.preflight), - AF_NATIVE_SCHEMA_ALLOW_PENDING_PROOFS="false", - SLURM_STEP_ID="fixture-step", - SLURM_JOB_ID="123456", - SLURM_JOB_ACCOUNT="loaner", - SLURM_JOB_PARTITION="rl2", - SLURM_JOB_QOS="loaner-normal", - SLURM_JOB_NUM_NODES="1", - SLURM_NTASKS="1", - SLURM_CPUS_PER_TASK="8", - SLURM_RESTART_COUNT="0", - SAFE_HANDOFF_LOG=str(self.root / "handoff.log"), - ) - self.env = env - - def tearDown(self): - self.temp.cleanup() - - def invoke(self, **changes): - env = dict(self.env) - env.update(changes) - return subprocess.run( - ["bash", str(self.launcher)], - env=env, - capture_output=True, - text=True, - timeout=15, - ) - - def assertRejected(self, result, text): - self.assertEqual(result.returncode, 64, result.stdout + result.stderr) - self.assertIn(text, result.stderr) - self.assertFalse((self.root / "handoff.log").exists()) - - def test_exact_v7_native_smoke_envelope_reaches_real_native_dispatch(self): - result = self.invoke() - self.assertEqual(result.returncode, 91, result.stdout + result.stderr) - self.assertIn("NATIVE_DISPATCH_PREPARE_STOP", result.stdout) - self.assertIn("--action prepare", (self.root / "handoff.log").read_text()) - argv = ( - self.root / "output-parent/run/provenance/restart-0/exact-phase.argv0" - ).read_bytes() - self.assertIn(b"trainer.max_steps=2\0", argv) - self.assertIn(b"trainer.precision=bf16-mixed\0", argv) - self.assertIn(b"++run_provenance.preflight_result_sha256=", argv) - - def suite_values(self, suite, seed): - record = json.loads( - (R / "tests/fixtures/libero_native_suite_corpus_authority.json").read_text() - )["suites"][suite] - return dict( - AF_EXPERIMENT="sim/libero_historical/action_flow_" - + suite - + "_h240_euler50_dithalf_80k_s42", - AF_SEED=str(seed), - AF_NATIVE_DATASET_SHA256=record["dataset_logical_sha256"], - AF_EXPECTED_DATASET_CONTENT_AGGREGATE_SHA256=record[ - "dataset_logical_sha256" - ], - AF_NATIVE_HISTORICAL_SPLIT_SHA256=record["historical_split_sha256"], - AF_EXPECTED_GPU_CONSTRAINT="H100|H200" - if suite == "libero_spatial" - else "(null)", - AF_CLUSTER_LABEL="ice" if suite == "libero_spatial" else "lambda", - ) - - def test_actual_goal_object_corpus_and_both_training_seeds_reach_dispatch(self): - for suite in ("libero_goal", "libero_object"): - for seed in (42, 43): - with self.subTest(suite=suite, seed=seed): - values = self.suite_values(suite, seed) - values["AF_OUTPUT_DIR"] = str( - self.root / "output-parent" / (suite + "-" + str(seed)) - ) - result = self.invoke(**values) - self.assertEqual( - result.returncode, 91, result.stdout + result.stderr - ) - argv = ( - Path(values["AF_OUTPUT_DIR"]) - / "provenance/restart-0/exact-phase.argv0" - ).read_bytes() - self.assertIn(("seed=" + str(seed) + "\0").encode(), argv) - self.assertIn( - ( - "++run_provenance.dataset_content_aggregate_sha256=" - + values["AF_NATIVE_DATASET_SHA256"] - + "\0" - ).encode(), - argv, - ) - self.assertIn( - ( - "++run_provenance.historical_split_manifest_sha256=" - + values["AF_NATIVE_HISTORICAL_SPLIT_SHA256"] - + "\0" - ).encode(), - argv, - ) - - def test_cross_suite_corpus_and_historical_split_rejected(self): - for suite in ("libero_spatial", "libero_goal", "libero_object"): - for field, message in [ - ("AF_NATIVE_DATASET_SHA256", "native dataset identity mismatch"), - ( - "AF_NATIVE_HISTORICAL_SPLIT_SHA256", - "historical split identity mismatch", - ), - ( - "AF_EXPECTED_DATASET_CONTENT_AGGREGATE_SHA256", - "native dataset aggregate identity mismatch", - ), - ]: - with self.subTest(suite=suite, field=field): - values = self.suite_values(suite, 42) - values[field] = self.env[field] - self.assertRejected(self.invoke(**values), message) - - def test_spatial_ice_actual_whole_path_constrained_dispatch(self): - result = self.invoke(**self.suite_values("libero_spatial", 42)) - self.assertEqual(result.returncode, 91, result.stdout + result.stderr) - argv = ( - self.root / "output-parent/run/provenance/restart-0/exact-phase.argv0" - ).read_bytes() - self.assertIn( - b"+experiment=sim/libero_historical/action_flow_libero_spatial_h240_euler50_dithalf_80k_s42\0", - argv, - ) - self.assertEqual( - (self.root / "handoff.log.replay").read_text().strip(), - str(self.root / "data"), - ) - - def test_spatial_native_actual_whole_path_null_reaches_hardware_guard(self): - result = self.invoke( - **{ - **self.suite_values("libero_spatial", 42), - "AF_EXPECTED_GPU_CONSTRAINT": "(null)", - } - ) - self.assertEqual(result.returncode, 91, result.stdout + result.stderr) - - def test_native_full_positive_restart_cap_and_null_constraint_reach_dispatch(self): - result = self.invoke( - AF_RUN_KIND="full", - AF_MAX_RESTARTS="4", - AF_SMOKE_RESULT=str(self.preflight), - AF_EXPECTED_SMOKE_SHA256=sha(self.preflight), - ) - self.assertEqual(result.returncode, 91, result.stdout + result.stderr) - self.assertIn("--phase full", (self.root / "handoff.log").read_text()) - argv = ( - self.root / "output-parent/run/provenance/restart-0/exact-phase.argv0" - ).read_bytes() - self.assertIn(b"trainer.max_steps=80000\0", argv) - self.assertIn(b"trainer.val_check_interval=15000\0", argv) - self.assertIn(b"++callbacks.model_checkpoint.every_n_train_steps=5000\0", argv) - - def test_native_unsupported_gpu_constraint_rejected(self): - self.assertRejected( - self.invoke(AF_EXPECTED_GPU_CONSTRAINT="RTX4090"), - "AF_EXPECTED_GPU_CONSTRAINT must be H100", - ) - - def test_native_invalid_restart_counter_rejected(self): - self.assertRejected( - self.invoke(SLURM_RESTART_COUNT="bad"), "invalid SLURM_RESTART_COUNT" - ) - - def test_native_smoke_requeue_cap_must_be_zero(self): - self.assertRejected( - self.invoke(AF_MAX_RESTARTS="1"), "native smoke requires AF_MAX_RESTARTS=0" - ) - - def test_negative_restart_cap_rejected(self): - self.assertRejected( - self.invoke(AF_MAX_RESTARTS="-1"), "native smoke requires AF_MAX_RESTARTS=0" - ) - - def test_native_restart_attempt_rejected(self): - self.assertRejected( - self.invoke(SLURM_RESTART_COUNT="1"), "restart limit exceeded" - ) - - def test_native_h100_constraint_reaches_hardware_guard(self): - result = self.invoke(AF_EXPECTED_GPU_CONSTRAINT="H100") - self.assertEqual(result.returncode, 91, result.stdout + result.stderr) - - def test_native_wrong_precision_rejected(self): - self.assertRejected( - self.invoke(AF_PRECISION="bf16"), "native BF16-mixed required" - ) - - def test_native_wrong_cpu_envelope_rejected_before_tool(self): - self.assertRejected( - self.invoke(SLURM_CPUS_PER_TASK="16"), "exact eight CPUs required" - ) - - def planar(self, **changes): - params = dict( - AF_EXPERIMENT=PLANAR, - AF_PRECISION="bf16", - AF_MAX_RESTARTS="4", - AF_EXPECTED_CONTENT_MANIFEST_SHA256="a1c81fb0ce8967aba795383a293180f9ba08a0ecfdd6f4a878afb20b39733761", - AF_EXPECTED_DATASET_CONTENT_AGGREGATE_SHA256="80f835ad37c3d5c5b7b2d5c3e1656c307ee567a1f63f51081165bf404b8ceb52", - ) - params.update(changes) - return self.invoke(**params) - - def test_av0_suites_reach_same_canonical_phase_with_distinct_identity(self): - from egomimic.benchmarks.libero.native_launch_profiles import AV0_PROFILES - - for suite, profile in AV0_PROFILES.items(): - with self.subTest(suite=suite): - values = self.suite_values(suite, 42) - values["AF_EXPERIMENT"] = profile.experiment - values["SAFE_REPLAY_VARIABLE"] = profile.replay_environment - output = self.root / "output-parent" / ("run-" + suite) - values["AF_OUTPUT_DIR"] = str(output) - result = self.invoke(**values) - self.assertEqual(result.returncode, 91, result.stdout + result.stderr) - argv = (output / "provenance/restart-0/exact-phase.argv0").read_bytes() - self.assertIn( - ("+experiment=" + profile.experiment + "\0").encode(), argv - ) - self.assertEqual( - (self.root / "handoff.log.replay").read_text().strip(), - str(self.root / "data"), - ) - - def test_general_zero_restart_gate_preserved(self): - self.assertRejected(self.planar(AF_MAX_RESTARTS="0"), "invalid AF_MAX_RESTARTS") - - def test_general_null_gpu_constraint_gate_preserved(self): - self.assertRejected(self.planar(), "AF_EXPECTED_GPU_CONSTRAINT must be H100") - - def test_native_full_zero_restart_gate_unchanged(self): - self.assertRejected(self.invoke(AF_RUN_KIND="full"), "invalid AF_MAX_RESTARTS") - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_libero_performance.py b/tests/test_libero_performance.py deleted file mode 100644 index 8a2aa92672..0000000000 --- a/tests/test_libero_performance.py +++ /dev/null @@ -1,313 +0,0 @@ -"""Performance paths must preserve exact targets, data and training budgets.""" - -import importlib.util -import json -import os -import pickle -import subprocess -import sys -from pathlib import Path - -import numpy as np -import pytest -import torch -from hydra import compose, initialize_config_dir - -from egomimic.benchmarks.libero.cluster import ( - arc_checkpoint_settings, - digest, - restore_checkpoints, - training_arguments, - training_layout, -) -from egomimic.pipeline.stages_libero_arc import LiberoArcStage - - -@pytest.mark.parametrize("mode", ["joint_dur", "stk", "dur"]) -def test_target_cache_is_exact_bounded_and_invalidated_by_codec_changes(mode): - options = dict( - arc_mode=mode, - num_waypoints=24, - max_translation=0.8, - max_rotation_degrees=192, - velocity_norm_bound=3**0.5, - ) - cached = LiberoArcStage(**options, encode_cache_size=2) - reference = LiberoArcStage(**options) - actions = torch.from_numpy( - np.random.default_rng(5).uniform(-1, 1, (3, 32, 7)).astype(np.float32) - ) - actions[0, 4:12, :6] = 0 # Exercise duration dwells and velocity limitations. - calls = [] - original = cached.codec.encode - - def count(row): - calls.append(row.copy()) - return original(row) - - cached.codec.encode = count - for indices, count_expected in [([0, 1, 0], 2), ([1, 0], 2), ([2, 0], 3), ([1], 4)]: - batch = actions[indices] - expected = reference.execute({"actions": batch}, mode="train")["target"] - actual = cached.execute({"actions": batch}, mode="train")["target"] - torch.testing.assert_close(actual, expected, rtol=0, atol=0) - assert len(calls) == count_expected - assert len(cached._encode_cache) <= 2 - actual.zero_() # Returned values must never alias cached physical tokens. - cached.codec.max_translation = reference.codec.max_translation = 0.01 - actual = cached.execute({"actions": actions[1:2]}, mode="train")["target"] - expected = reference.execute({"actions": actions[1:2]}, mode="train")["target"] - torch.testing.assert_close(actual, expected, rtol=0, atol=0) - assert len(calls) == 5 - assert cached.state_dict().keys() == reference.state_dict().keys() - with pytest.raises(ValueError, match="batch"): - cached._encode(actions[1:2].numpy().reshape(1, 7, 32)) - - -def test_target_cache_preserves_normalizer_changes_and_rejects_nonfinite(): - cached = LiberoArcStage(arc_mode="stk", encode_cache_size=8) - reference = LiberoArcStage(arc_mode="stk") - actions = torch.ones(1, 32, 7) * 0.25 - cached.execute({"actions": actions}, mode="train") - for stage in (cached, reference): - stage.action_scale.fill_(2) - stage.action_offset.fill_(0.1) - torch.testing.assert_close( - cached.execute({"actions": actions}, mode="train")["target"], - reference.execute({"actions": actions}, mode="train")["target"], - rtol=0, - atol=0, - ) - assert len(cached._encode_cache) == 2 - with pytest.raises(ValueError, match="finite"): - cached.execute( - {"actions": torch.full_like(actions, float("nan"))}, mode="train" - ) - - -def test_replay_array_handles_survive_repeated_reads_and_reset_on_spawn( - tmp_path, monkeypatch -): - import zarr - - from egomimic.rldb.zarr.libero_dataset import LiberoReplayResolver, keymap - from tests.test_libero_benchmark import make_replay - - path = tmp_path / "replay.zarr" - make_replay(path) - leaf = next( - iter(LiberoReplayResolver(path, keymap(), "libero10").resolve().values()) - ) - expected = leaf[0] - original = zarr.Group.__getitem__ - calls = [] - - def tracked(group, key): - calls.append(key) - return original(group, key) - - monkeypatch.setattr(zarr.Group, "__getitem__", tracked) - actual = leaf[0] - assert calls == [] - restored = pickle.loads(pickle.dumps(leaf)) - assert restored._arrays is None - restored_sample = restored[0] - assert calls - for key, value in expected.items(): - if torch.is_tensor(value): - torch.testing.assert_close(actual[key], value, rtol=0, atol=0) - torch.testing.assert_close(restored_sample[key], value, rtol=0, atol=0) - - -@pytest.mark.parametrize("gpus", [1, 2, 4, 8]) -@pytest.mark.parametrize("mode", ["full", "smoke"]) -def test_distributed_arguments_preserve_global_batch(gpus, mode, tmp_path): - args = training_arguments( - "arc_stk", "libero_10", tmp_path / "replay", tmp_path, mode, 5001, gpus=gpus - ) - with initialize_config_dir( - version_base=None, - config_dir=str(Path(__file__).parents[1] / "egomimic/hydra_configs"), - ): - cfg = compose(config_name="train_zarr_cartesian", overrides=args[3:]) - assert cfg.trainer.devices == gpus - effective = cfg.benchmark.batch_size * gpus * cfg.trainer.accumulate_grad_batches - assert effective == (1024 if mode == "full" else 4 * gpus) - if mode == "full": - assert cfg.callbacks.batch_budget.global_batch_size == effective - assert cfg.trainer.max_epochs == 5001 - if gpus > 1: - assert cfg.trainer.strategy == "ddp_find_unused_parameters_true" - with pytest.raises(ValueError, match="GPUs"): - training_layout(3, mode) - - -def test_sweep_resume_reuses_confirmed_replay_and_resources(tmp_path, monkeypatch): - from egomimic.benchmarks.libero import arc_sweep, cluster - - path = Path(__file__).parents[1] / "scripts/benchmarks/launch_libero_osmo.py" - spec = importlib.util.spec_from_file_location("ddp_launcher", path) - launcher = importlib.util.module_from_spec(spec) - spec.loader.exec_module(launcher) - args = dict( - commit="a" * 40, - run_id="ddp-stk1", - suite="libero_10", - mode="full", - arc_modes=["stk"], - arc_profile="stk_1", - resume_from_run="old-stk1", - oat_reference_run="baseline", - gpus=4, - ) - with pytest.raises(ValueError, match="completed mode-specific replay"): - launcher.workflow(**args) - workflow = launcher.workflow(**args, arc_replay_runs={"stk": "confirmed-stk1"})[ - "workflow" - ] - assert workflow["resources"]["default"]["gpu"] == 4 - assert workflow["resources"]["default"]["cpu"] == 48 - assert workflow["resources"]["default"]["memory"] == "256Gi" - env = workflow["tasks"][0]["environment"] - for name, value in env.items(): - monkeypatch.setenv(name, value) - assert env["TRAINING_GPUS"] == "4" - calls = [] - monkeypatch.setattr(cluster, "execute", lambda argv, log: calls.append(argv)) - monkeypatch.setattr( - sys, - "argv", - [ - "sweep", - "--root", - str(tmp_path), - "--suite", - "libero_10", - "--run-id", - "ddp-stk1", - "--profile", - "stk_1", - "--arc-mode", - "stk", - ], - ) - arc_sweep.main() - assert len(calls) == 1 and calls[0][2].endswith(".cluster") - assert "--arc-only" in calls[0] - assert os.environ["ARC_REPLAY_RUNS_JSON"] == '{"stk": "confirmed-stk1"}' - - -def test_resume_changes_world_size_without_resetting_optimizer_or_ema(tmp_path): - script = Path(__file__).with_name("libero_ddp_resume_worker.py") - env = dict( - os.environ, OMP_NUM_THREADS="1", MKL_NUM_THREADS="1", OPENBLAS_NUM_THREADS="1" - ) - # Execute against this audited checkout, not a globally installed package or - # the worker script directory. This also survives a task-local test venv. - env["PYTHONPATH"] = os.pathsep.join( - [str(Path(__file__).resolve().parents[1]), env.get("PYTHONPATH", "")] - ) - for suffix in ([], ["--resume"]): - result = subprocess.run( - [sys.executable, str(script), "--root", str(tmp_path), *suffix], - env=env, - capture_output=True, - text=True, - timeout=180, - ) - assert result.returncode == 0, result.stdout[-8000:] + result.stderr[-8000:] - initial = json.loads((tmp_path / "initial.json").read_text()) - resumed = json.loads((tmp_path / "resumed.json").read_text()) - assert initial["global_step"] == initial["ema_num_updates"] == 11 - assert resumed["global_step"] == resumed["ema_num_updates"] == 22 - assert resumed["optimizer_states"] - budget = resumed["training_budget"] - assert budget["world_size"] == budget["gradient_accumulation"] == 2 - assert budget["global_batch_size"] == 8 - assert budget["optimizer_steps_per_epoch"] == 11 - - -@pytest.mark.parametrize("method", ["arc", "arc_stk", "arc_dur"]) -@pytest.mark.parametrize("backbone", ["unet", "oat_dp"]) -def test_arc_resume_reads_actual_saved_graph_config(tmp_path, method, backbone): - import io - import shutil - - from egomimic.trainHydra import _build_model_config_tree - - args = training_arguments( - method, - "libero_10", - tmp_path, - tmp_path, - "full", - 5001, - arc_backbone=backbone, - ) - with initialize_config_dir( - version_base=None, - config_dir=str(Path(__file__).parents[1] / "egomimic/hydra_configs"), - ): - cfg = compose(config_name="train_zarr_cartesian", overrides=args[3:]) - config = _build_model_config_tree(cfg) - assert "benchmark" not in config # Actual trainHydra checkpoint format. - source = tmp_path / "source.ckpt" - torch.save( - { - "global_step": 140, - "loops": {"fit_loop": {"epoch_progress": {"current": {"completed": 2}}}}, - "training_budget": {"global_batch_size": 1024, "epochs": 5001}, - "optimizer_states": [{"state": {}}], - "normalizer_state": {}, - "hyper_parameters": {"config_tree": config}, - }, - source, - ) - runtime = { - "suite": "libero_10", - "mode": "full", - "epochs": 5001, - "global_batch_size": 1024, - } - receipts = { - f"training/{method}/checkpoints/last.ckpt": { - "sha256": digest(source), - "bytes": source.stat().st_size, - "uri": "s3://rldb/experiments/arc-oat-20260919/source/checkpoints/last.ckpt", - } - } - - class Storage: - def get_object(self, Bucket, Key): - value = runtime if Key.endswith("runtime.json") else receipts - return {"Body": io.BytesIO(json.dumps(value).encode())} - - def download_file(self, bucket, key, destination): - shutil.copyfile(source, destination) - - restored = restore_checkpoints( - Storage(), - "source", - tmp_path / "evidence", - suite="libero_10", - mode="full", - epochs=5001, - allow_partial=True, - methods=[method], - )[method] - assert restored["global_step"] == 140 - assert restored["benchmark"].get("arc_backbone", "unet") == backbone - for key, value in restored["benchmark"].items(): - assert value == cfg.benchmark[key] - with pytest.raises(ValueError, match="protocol"): - arc_checkpoint_settings(config, suite="libero_spatial") - from omegaconf import open_dict - - with open_dict(config.model.benchmark_protocol): - config.model.benchmark_protocol.arc_backbone = "unknown" - with pytest.raises(ValueError, match="unknown backbone"): - arc_checkpoint_settings(config, suite="libero_10") - config.model.benchmark_protocol.arc_backbone = backbone - config.model.pipeline.stages[-1].num_waypoints += 1 - with pytest.raises(ValueError, match="parameters differ"): - arc_checkpoint_settings(config, suite="libero_10") diff --git a/tests/test_libero_saved_hydra_context.py b/tests/test_libero_saved_hydra_context.py deleted file mode 100644 index 3ee9521d08..0000000000 --- a/tests/test_libero_saved_hydra_context.py +++ /dev/null @@ -1,341 +0,0 @@ -"""Real Hydra/OmegaConf restoration, no model/data/cluster execution.""" - -import hashlib -import json -import os -from pathlib import Path -from unittest.mock import patch - -import pytest -from hydra import __version__ as hydra_version -from hydra.conf import HydraConf -from hydra.core.hydra_config import HydraConfig -from omegaconf import OmegaConf - -from egomimic.benchmarks.libero.native_launch_profiles import PROFILES -from egomimic.benchmarks.libero.saved_hydra_context import load_saved_native_config - - -def setup(tmp_path, suite): - repo = tmp_path / "source" - repo.mkdir() - run = tmp_path / "run" - (run / ".hydra").mkdir(parents=True) - p = PROFILES[suite] - receipt = tmp_path / "receipt.json" - logical_sha256 = "a" * 64 - receipt.write_text( - json.dumps( - dict( - suite=suite, - replay_path="/replay", - dataset_logical_sha256=logical_sha256, - ) - ) - ) - proof = tmp_path / "physical.json" - proof.write_text("{}") - - def sha(path): - return hashlib.sha256(path.read_bytes()).hexdigest() - - binding = dict( - source_commit="head", - data_root="/replay", - dataset_receipt_path=str(receipt), - dataset_receipt_sha256=sha(receipt), - physical_proof_path=str(proof), - physical_proof_sha256=sha(proof), - ) - config = dict( - name=p.name, - benchmark=dict(suite=suite, dataset="${oc.env:" + p.replay_environment + "}"), - run_provenance=dict( - source_commit="head", - dataset_sha256=logical_sha256, - dataset_content_aggregate_sha256=logical_sha256, - ), - norm_stats=dict(native_saved_state_binding=binding), - data={ - group: { - "libero_panda": { - "resolver": dict( - suite="${benchmark.suite}", - folder_path="${oc.env:" + p.replay_environment + "}", - ) - } - } - for group in ("train_datasets", "valid_datasets") - }, - evaluator=dict( - energy_seed_bank_path="${hydra:runtime.cwd}/egomimic/hydra_configs/evaluator/energy_score_seed_bank_k32_v1.json" - ), - ) - OmegaConf.save(OmegaConf.create(config), run / ".hydra/config.yaml") - hydra = OmegaConf.structured(HydraConf) - hydra.runtime.cwd = str(repo) - hydra.runtime.output_dir = str(run) - hydra.runtime.version = hydra_version - saved = OmegaConf.to_container(hydra, resolve=False) - saved["env"] = {} - OmegaConf.save(OmegaConf.create({"hydra": saved}), run / ".hydra/hydra.yaml") - return repo, run, p - - -@pytest.mark.parametrize("suite", list(PROFILES)) -def test_saved_context_preserves_all_fields_and_missing_env( - tmp_path, monkeypatch, suite -): - repo, run, p = setup(tmp_path, suite) - monkeypatch.delenv(p.replay_environment, raising=False) - with patch("subprocess.check_output", return_value="head\n"): - cfg = load_saved_native_config(run, repo) - assert os.environ[p.replay_environment] == "/replay" - assert cfg.benchmark.dataset == "/replay" - assert OmegaConf.get_type(HydraConfig.get()) is HydraConf - assert dict(HydraConfig.get().env) == {} - saved = OmegaConf.to_container( - OmegaConf.load(run / ".hydra/hydra.yaml").hydra, resolve=False, enum_to_str=True - ) - actual = OmegaConf.to_container(HydraConfig.get(), resolve=False, enum_to_str=True) - assert all(actual[k] == v for k, v in saved.items()) - - -@pytest.mark.parametrize( - "fault", - ["conflict", "unknown_env", "cwd", "output", "source", "receipt", "resolver"], -) -def test_context_rejects_identity_drift(tmp_path, monkeypatch, fault): - repo, run, p = setup(tmp_path, "libero_spatial") - monkeypatch.delenv(p.replay_environment, raising=False) - cfg = OmegaConf.load(run / ".hydra/config.yaml") - saved = OmegaConf.load(run / ".hydra/hydra.yaml") - if fault == "conflict": - monkeypatch.setenv(p.replay_environment, "/wrong") - if fault == "unknown_env": - cfg.extra = "${oc.env:UNEXPECTED}" - if fault == "cwd": - saved.hydra.runtime.cwd = "/wrong" - if fault == "output": - saved.hydra.runtime.output_dir = "/wrong" - if fault == "source": - cfg.run_provenance.source_commit = "wrong" - if fault == "receipt": - Path(cfg.norm_stats.native_saved_state_binding.dataset_receipt_path).write_text( - "{}" - ) - if fault == "resolver": - cfg.data.valid_datasets.libero_panda.resolver.folder_path = "/wrong" - OmegaConf.save(cfg, run / ".hydra/config.yaml") - OmegaConf.save(saved, run / ".hydra/hydra.yaml") - with ( - patch("subprocess.check_output", return_value="head\n"), - pytest.raises(ValueError), - ): - load_saved_native_config(run, repo) - - -@pytest.mark.parametrize("fault", [None, "context", "source", "resolver"]) -def test_joint_saved_context_uses_complete_context_without_historical_binding( - tmp_path, monkeypatch, fault -): - import hydra - - from egomimic.benchmarks.libero.native_launch_profiles import joint_phase_argv - - repo = Path(__file__).resolve().parents[1] - run = tmp_path / "joint" - (run / ".hydra").mkdir(parents=True) - context = tmp_path / "context.json" - split = tmp_path / "split.json" - context.write_text("{}") - split.write_text("{}") - - def sha(path): - return hashlib.sha256(path.read_bytes()).hexdigest() - - args = dict( - source_root=str(repo), - original_root=str(tmp_path / "original"), - sharpa_root=str(tmp_path / "sharpa"), - output_root=str(run), - context_path=str(context), - split_path=str(split), - split_sha256=sha(split), - normalization_sha256=sha(context), - source_commit="c" * 40, - diagnostic_seed_bank=str(tmp_path / "seeds.json"), - wandb_entity="test", - wandb_project="test", - wandb_run_id="joint", - ) - with hydra.initialize_config_dir( - version_base="1.2", config_dir=str(repo / "egomimic/hydra_configs") - ): - cfg = hydra.compose( - config_name="train_zarr_cartesian", - return_hydra_config=True, - overrides=joint_phase_argv(phase="smoke", **args)[1:] - + ["hydra/launcher=basic"], - ) - cfg.hydra.runtime.cwd = str(repo) - cfg.hydra.runtime.output_dir = str(run) - HydraConfig.instance().set_config(cfg) - payload = OmegaConf.to_container( - OmegaConf.masked_copy(cfg, [k for k in cfg if k != "hydra"]), resolve=True - ) - saved = OmegaConf.to_container(cfg.hydra, resolve=False) - if fault == "context": - context.write_text("changed") - if fault == "source": - payload["run_provenance"]["source_commit"] = "wrong" - if fault == "resolver": - payload["data"]["valid_datasets"]["libero_panda_sharpa"]["resolver"][ - "folder_path" - ] = "/wrong" - OmegaConf.save(OmegaConf.create(payload), run / ".hydra/config.yaml") - OmegaConf.save(OmegaConf.create({"hydra": saved}), run / ".hydra/hydra.yaml") - with patch("subprocess.check_output", return_value="c" * 40 + "\n"): - if fault: - with pytest.raises(ValueError): - load_saved_native_config(run, repo) - else: - loaded = load_saved_native_config(run, repo) - assert OmegaConf.to_container(loaded, resolve=True) == payload - - -@pytest.mark.parametrize("fault", [None, "rolling", "wrong_callback", "wrong_path"]) -def test_joint_checkpoint_distinguishes_configured_terminal_from_rolling_alias( - tmp_path, fault -): - from scripts.train.verify_action_flow_training_smoke import ( - _joint_smoke_checkpoint_path, - ) - - cadence = tmp_path / "epoch-0000-step-000000008.ckpt" - terminal = tmp_path / "last.ckpt" - cadence.write_bytes(b"cadence") - terminal.write_bytes(b"terminal") - cfg = OmegaConf.create( - { - "callbacks": { - "model_checkpoint": {"save_last": False}, - "ema": { - "_target_": "egomimic.pl_utils.oat_matched_training.ActionFlowFixedEMACallback", - "final_checkpoint_path": str(terminal), - }, - } - } - ) - if fault == "rolling": - cfg.callbacks.model_checkpoint.save_last = True - if fault == "wrong_callback": - cfg.callbacks.ema._target_ = "unknown.Callback" - if fault == "wrong_path": - cfg.callbacks.ema.final_checkpoint_path = "/wrong" - if fault: - with pytest.raises(Exception, match="rolling last"): - _joint_smoke_checkpoint_path(tmp_path, cfg) - else: - assert _joint_smoke_checkpoint_path(tmp_path, cfg) == cadence - assert terminal.read_bytes() == b"terminal" - with pytest.raises(Exception, match="rolling last"): - _joint_smoke_checkpoint_path(tmp_path) - - -def test_separate_verifier_finalizes_through_same_loaded_helpers(tmp_path, monkeypatch): - from egomimic.benchmarks.libero import saved_hydra_context as loader - from scripts.train import libero_native_launch_contract as producer - from scripts.train import verify_action_flow_training_smoke as shared - - training, verifier, run = [tmp_path / p for p in ("training", "verifier", "run")] - for p in (training, verifier, run): - p.mkdir() - preflight = tmp_path / "preflight.json" - preflight.write_text("{}") - runtime = tmp_path / "runtime.json" - runtime.write_text("{}") - monkeypatch.setattr(shared, "REPOSITORY_ROOT", verifier) - monkeypatch.setattr(shared, "VERIFIER_PROVENANCE", None) - monkeypatch.setattr( - shared, - "_git_head", - lambda repository_root=None: "a" * 40 - if repository_root == verifier - else "b" * 40, - ) - monkeypatch.setattr(shared.subprocess, "check_output", lambda *a, **k: "") - monkeypatch.setattr(shared.subprocess, "run", lambda *a, **k: None) - monkeypatch.setenv("SLURM_STEP_ID", "0") - monkeypatch.setenv("AF_PREFLIGHT_RESULT", str(preflight)) - calls = [] - - def load(root, repo): - assert root == run and repo == training - return OmegaConf.create({"original": True}) - - def evidence(cfg, root, **kwargs): - assert isinstance(cfg, dict) and cfg["original"] and root == run - json.dumps(cfg, allow_nan=False) - assert kwargs["shared"] is shared - calls.append("finalize") - return {"actual": True} - - monkeypatch.setattr(loader, "load_saved_native_config", load) - monkeypatch.setattr(producer, "joint_smoke_evidence", evidence) - monkeypatch.setattr(producer, "write", lambda path, value: calls.append("write")) - monkeypatch.setattr( - shared, - "verify_smoke", - lambda *a, **k: calls.append("verify") or {"run_dir": str(run)}, - ) - assert ( - shared.main( - [ - str(run), - "--experiment", - "sim/libero/action_flow_libero_spatial_sharpa_gripper_keypoints_s42", - "--expected-head", - "b" * 40, - "--training-repo", - str(training), - "--expected-verifier-head", - "a" * 40, - "--finalize-runtime-lock", - str(runtime), - "--expected-preflight-sha256", - hashlib.sha256(preflight.read_bytes()).hexdigest(), - ] - ) - == 0 - ) - assert calls == ["finalize", "write", "verify"] - - -def test_joint_artifacts_bind_cadence_epoch_and_preserve_postfit(tmp_path, monkeypatch): - import torch - - from scripts.train import verify_action_flow_training_smoke as shared - - monkeypatch.delenv("SLURM_JOB_ID", raising=False) - run = tmp_path / "run" - checkpoint = run / "checkpoints/epoch-0000-step-000000008.ckpt" - checkpoint.parent.mkdir(parents=True) - checkpoint.write_bytes(b"filename-only-selection") - root = run / "artifacts" - for epoch in (0, 1): - path = root / f"epoch-{epoch}-step-8/rank-0-batch-0.pt" - path.parent.mkdir(parents=True) - torch.save({"global_step": 8, "epoch": epoch}, path) - epoch = shared._joint_smoke_artifact_epoch(run, None) - assert epoch == 0 - selected, payload = shared._step_artifact( - root, label="joint", expected_step=8, expected_epoch=epoch - ) - assert selected.parent.name == "epoch-0-step-8" and payload["epoch"] == 0 - assert (root / "epoch-1-step-8/rank-0-batch-0.pt").is_file() - with pytest.raises(shared.SmokeVerificationError, match="expected one step-8"): - shared._step_artifact(root, label="joint", expected_step=8) - torch.save({"global_step": 8, "epoch": 1}, selected) - with pytest.raises(shared.SmokeVerificationError, match="epoch differs"): - shared._step_artifact(root, label="joint", expected_step=8, expected_epoch=0) diff --git a/tests/test_likelihood_training_behavior_parity.py b/tests/test_likelihood_training_behavior_parity.py deleted file mode 100644 index f15916de2c..0000000000 --- a/tests/test_likelihood_training_behavior_parity.py +++ /dev/null @@ -1,171 +0,0 @@ -"""Compare likelihood mechanics against the retained source-bound wrapper.""" - -import ast -import copy -import hashlib -import json -from pathlib import Path - -import pytest -import torch -import yaml - -from egomimic.pipeline.algo import PipelineAlgo -from egomimic.pl_utils.pl_model import ModelWrapper -from egomimic.pl_utils.pl_model_action_flow_likelihood import ( - ActionFlowLikelihoodModelWrapper, -) -from egomimic.pl_utils.training_behavior_action_flow_likelihood import ( - ActionFlowLikelihoodTrainingBehavior, -) -from tests.test_action_flow_likelihood import batch, stages - - -def test_likelihood_recipe_changes_only_framework_selection(): - root = Path(__file__).resolve().parents[1] - path = "egomimic/hydra_configs/model/bf/us_action_flow_bc_bridge_likelihood.yaml" - candidate = yaml.safe_load((root / path).read_text()) - assert candidate.pop("_target_") == "egomimic.pl_utils.pl_model.ModelWrapper" - assert candidate.pop("training_behavior") == { - "_target_": "egomimic.pl_utils.training_behavior_action_flow_likelihood.ActionFlowLikelihoodTrainingBehavior" - } - # Canonical YAML payload (without the framework target), independently - # extracted from exact training source7c253e5. No full-history checkout is - # required in shallow CI, and every scientific field is included. - digest = hashlib.sha256( - json.dumps(candidate, sort_keys=True, separators=(",", ":")).encode() - ).hexdigest() - assert digest == "e55a71bec35d62e694119ea3c42fed1a8e7855f2d97fc3e7310c21a37831e033" - - -def test_port_preserves_historical_tensor_and_checkpoint_algorithms(): - root = Path(__file__).resolve().parents[1] / "egomimic/pl_utils" - - def methods(path, name): - tree = ast.parse((root / path).read_text()) - cls = next( - node - for node in tree.body - if isinstance(node, ast.ClassDef) and node.name == name - ) - return { - node.name: node for node in cls.body if isinstance(node, ast.FunctionDef) - } - - historical = methods( - "pl_model_action_flow_likelihood.py", "ActionFlowLikelihoodModelWrapper" - ) - candidate = methods( - "training_behavior_action_flow_likelihood.py", - "ActionFlowLikelihoodTrainingBehavior", - ) - - class ContextToHistorical(ast.NodeTransformer): - def visit_Attribute(self, node): - node = self.generic_visit(node) - if ( - isinstance(node.value, ast.Attribute) - and node.value.attr == "context" - and isinstance(node.value.value, ast.Name) - and node.value.value.id == "self" - ): - node.value = ast.Name(id="self", ctx=ast.Load()) - return node - - for name in ( - "_log_telemetry", - "_capture_gradient_routes", - "_log_prediction_metrics", - "validation_step", - "on_save_checkpoint", - ): - port_name = ( - "log_prediction_metrics" if name == "_log_prediction_metrics" else name - ) - port = ContextToHistorical().visit(copy.deepcopy(candidate[port_name])) - port.name = name - assert ast.dump(port, include_attributes=False) == ast.dump( - historical[name], include_attributes=False - ), name - - -@pytest.mark.parametrize("cadence", [0, 1]) -def test_training_validation_checkpoint_and_rng_match_historical(monkeypatch, cadence): - def observe(shared): - torch.manual_seed(73) - algo = PipelineAlgo(stages(dropout=0.3), device="cpu") - wrapper = ( - ModelWrapper( - pipeline=algo, - enable_grad_norm=False, - training_behavior=ActionFlowLikelihoodTrainingBehavior( - gradient_telemetry_cadence=cadence - ), - ) - if shared - else ActionFlowLikelihoodModelWrapper( - pipeline=algo, - enable_grad_norm=False, - gradient_telemetry_cadence=cadence, - ) - ) - logged = {} - monkeypatch.setattr( - wrapper, - "log", - lambda name, value, **kw: logged.update( - {name: (torch.as_tensor(value).detach().clone(), kw)} - ), - ) - optimizer = torch.optim.SGD(wrapper.parameters(), lr=0.001) - losses, gradients = [], [] - for index in range(2): - optimizer.zero_grad() - loss = wrapper.training_step({"example": batch()}, index) - losses.append(loss.detach().clone()) - loss.backward() - gradients.append( - [ - None if p.grad is None else p.grad.detach().clone() - for p in wrapper.parameters() - ] - ) - wrapper.on_after_backward() - wrapper.on_before_optimizer_step(optimizer) - optimizer.step() - wrapper.eval() - before = torch.get_rng_state().clone() - with torch.inference_mode(): - wrapper.validation_step({"example": batch(), "inactive": None}, 2, 1) - # batch() constructs random data, so compare resulting RNG across sources. - after = torch.get_rng_state().clone() - checkpoint = {} - wrapper.on_save_checkpoint(checkpoint) - return ( - copy.deepcopy(wrapper.nets.state_dict()), - losses, - gradients, - {k: v for k, v in logged.items() if not k.startswith("Timing/")}, - before, - after, - {k: v for k, v in checkpoint.items() if k.startswith("action_flow_")}, - ) - - historical, candidate = observe(False), observe(True) - assert historical[0].keys() == candidate[0].keys() - for key in historical[0]: - assert torch.equal(historical[0][key], candidate[0][key]), key - for a, b in zip(historical[1], candidate[1]): - assert torch.equal(a, b) - for left, right in zip(historical[2], candidate[2]): - for a, b in zip(left, right): - assert (a is None) == (b is None) - if a is not None: - assert torch.equal(a, b) - assert historical[3].keys() == candidate[3].keys() - for key, (value, options) in historical[3].items(): - assert torch.equal(value, candidate[3][key][0]), key - assert options == candidate[3][key][1], key - assert torch.equal(historical[4], candidate[4]) - assert torch.equal(historical[5], candidate[5]) - assert historical[6] == candidate[6] diff --git a/tests/test_local_cpu_storage_guard.py b/tests/test_local_cpu_storage_guard.py deleted file mode 100644 index dd11a6d4d2..0000000000 --- a/tests/test_local_cpu_storage_guard.py +++ /dev/null @@ -1,25 +0,0 @@ -from types import SimpleNamespace - -import pytest - -from scripts.local_cpu_storage_guard import pytest_runtest_setup - - -@pytest.mark.parametrize("free_gib", [3, 4, 8]) -def test_per_test_headroom_gate(monkeypatch, free_gib): - monkeypatch.delenv("LOCAL_CPU_TEST_MIN_FREE_KIB", raising=False) - monkeypatch.setattr( - "scripts.local_cpu_storage_guard.shutil.disk_usage", - lambda path: SimpleNamespace(free=free_gib * 1024**3), - ) - if free_gib < 4: - with pytest.raises(pytest.UsageError, match="blocked before test"): - pytest_runtest_setup(None) - else: - pytest_runtest_setup(None) - - -def test_per_test_floor_cannot_be_lowered(monkeypatch): - monkeypatch.setenv("LOCAL_CPU_TEST_MIN_FREE_KIB", "1") - with pytest.raises(pytest.UsageError, match="cannot be below"): - pytest_runtest_setup(None) diff --git a/tests/test_local_cpu_validation_guard.py b/tests/test_local_cpu_validation_guard.py deleted file mode 100644 index 3b17dc3fa0..0000000000 --- a/tests/test_local_cpu_validation_guard.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Local validation limits cannot be lowered into misleading storage failures.""" - -import os -import subprocess -from pathlib import Path - -import pytest - - -@pytest.mark.parametrize("value", ["0", "4194303", "invalid", "999999999999"]) -def test_low_invalid_or_unavailable_space_blocks_before_python(value): - script = Path(__file__).resolve().parents[1] / "scripts/run_local_cpu_validation.sh" - env = dict( - os.environ, - LOCAL_CPU_TEST_MIN_FREE_KIB=value, - LOCAL_CPU_TEST_PYTHON="/does-not-exist", - ) - result = subprocess.run( - ["bash", str(script), "--version"], env=env, capture_output=True, text=True - ) - assert result.returncode == 64 - assert "Local CPU validation" in result.stderr - assert "/does-not-exist" not in result.stderr - - -@pytest.mark.parametrize( - ("target", "timeout"), - [("tests/test_recursive_config_audit.py", 600), ("tests/test_oat_native.py", 60)], -) -def test_long_constructor_audit_has_its_own_diagnostic_threshold(target, timeout): - script = Path(__file__).resolve().parents[1] / "scripts/run_local_cpu_validation.sh" - # Print the runner's invocation, without executing or skipping any real test. - result = subprocess.run( - ["bash", str(script), target], - env=dict(os.environ, LOCAL_CPU_TEST_PYTHON="/bin/echo"), - capture_output=True, - text=True, - ) - assert result.returncode == 0, result.stderr - assert f"faulthandler_timeout={timeout}" in result.stdout diff --git a/tests/test_measured_speed_removed.py b/tests/test_measured_speed_removed.py deleted file mode 100644 index 387c844836..0000000000 --- a/tests/test_measured_speed_removed.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Retired measured-speed paths must never become selectable again.""" - -from pathlib import Path - -import pytest -from omegaconf import OmegaConf - -from egomimic.pipeline.stages_speed import ( - SharedSpeedCondition, - build_speed_conditioned_pipeline, - requested_rollout_condition, -) -from egomimic.rldb.zarr.physical_retiming import PhysicalWindowRetiming - -ROOT = Path(__file__).parents[1] - - -@pytest.mark.parametrize("mode", ["native_speed", "physical_speed", "unknown"]) -def test_conditioner_and_factory_reject_retired_modes(mode): - with pytest.raises(ValueError, match="removed"): - SharedSpeedCondition(conditioning_input=mode) - with pytest.raises(ValueError, match="removed"): - build_speed_conditioned_pipeline([], conditioning_input=mode) - - -def test_default_is_direct_multiplier_and_reference_cannot_select_old_mode(): - model = SharedSpeedCondition() - assert model.speed_key == "retiming_rate" - assert model.conditioning_input == "retiming_multiplier" - assert "speed_reference" not in model.state_dict() - with pytest.raises(ValueError, match="removed"): - SharedSpeedCondition(1.0) - with pytest.raises(ValueError, match="retiming_rate"): - SharedSpeedCondition(speed_key="requested_speed") - - -def test_retimer_rejects_retired_mode_before_loading_data(): - with pytest.raises(ValueError, match="removed"): - PhysicalWindowRetiming( - [1.0], - {"left": "pose_wxyz", "right": "pose_wxyz"}, - ["left", "right"], - 30, - timestamp_key="clock", - conditioning_input="native_speed", - ) - - -@pytest.mark.parametrize("value", [None, "native_speed"]) -def test_rollout_never_falls_back_to_measured_speed(value): - pipeline = { - "_target_": "egomimic.pipeline.stages_speed.build_speed_conditioned_pipeline" - } - if value is not None: - pipeline["conditioning_input"] = value - cfg = OmegaConf.create( - {"model": {"pipeline": pipeline}, "deployment": {"requested_speed": 1.0}} - ) - with pytest.raises(ValueError): - requested_rollout_condition(cfg) - - -def test_configs_and_launchers_have_no_retired_routes(): - retired = ( - "human_speed_v1", - "action_flow_cotrain_uc_speed_interpolation", - "yam_human_keypoints_speed_h816_private512_s42", - "AF_STATIONARY_SPEED_REFERENCE", - "AF_SPEED_REFERENCE", - ) - for root in (ROOT / "egomimic/hydra_configs", ROOT / "scripts/train"): - for file in root.rglob("*"): - if file.suffix not in {".yaml", ".sh", ".sbatch", ".py"}: - continue - body = file.read_text() - assert not any(name in body for name in retired), file diff --git a/tests/test_multiplier_training_launch.py b/tests/test_multiplier_training_launch.py deleted file mode 100644 index df8cdd827b..0000000000 --- a/tests/test_multiplier_training_launch.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Typed multiplier route preserves native per-source execution and full batches.""" - -import runpy -import subprocess -from pathlib import Path - -import pytest -from hydra import compose, initialize_config_dir - -from egomimic.pipeline.stages_speed import configure_dit_checkpoint_policy - - -def test_multiplier_training_contract(): - root = Path(__file__).resolve().parents[1] - with initialize_config_dir( - config_dir=str(root / "egomimic/hydra_configs"), version_base="1.3" - ): - cfg = compose( - config_name="train_zarr_cartesian", - overrides=[ - "hydra/launcher=basic", - "+experiment=sim/pusht/action_flow_cotrain_uc_multiplier_interpolation", - "trainer.precision=bf16", - "trainer.devices=1", - "trainer.num_nodes=1", - "model.pipeline.stages.6.inference_method=euler", - "model.pipeline.stages.6.num_inference_steps=50", - ], - ) - validator = runpy.run_path( - str(root / "scripts/train/validate_multiplier_config.py") - ) - validator["validate_speed_contract"](cfg, "scalar") - assert cfg.callbacks.ema.decay == 0.9978 - assert cfg.callbacks.ema.use_warmup is False - assert "homogeneous_dithalf" not in cfg.callbacks - assert cfg.model.pipeline.dit_checkpoint_policy == "dit_half" - assert not (root / "egomimic/pl_utils/homogeneous_training/callback.py").exists() - assert cfg.norm_stats.sample_frac == 1.0 - assert cfg.norm_stats.norm_mode == "minmax" - # The typed validator above requires one full batch32 per source. - assert len(cfg.data.train_datasets) == 2 - - -def _owners(): - def owner(): - return { - "backbone": { - "_target_": "egomimic.models.unite_dit.UniteDiTBackbone", - "gradient_checkpointing": True, - "depth": 12, - } - } - - return {"encoders": {"u": owner(), "chain": owner()}}, {"field": owner()} - - -def test_native_dit_half_does_not_change_image_or_decoder_execution(): - encoder, field = _owners() - configure_dit_checkpoint_policy(encoder, field, "dit_half") - for owner in [*encoder["encoders"].values(), field["field"]]: - assert owner["backbone"]["checkpoint_policy"] == "dit_half" - assert set(encoder) == {"encoders"} - assert set(field) == {"field"} - - -@pytest.mark.parametrize("invalid", ["disabled", "odd_depth", "wrong_owner", "unknown"]) -def test_native_dit_half_rejects_mismatch_atomically(invalid): - encoder, field = _owners() - backbone = field["field"]["backbone"] - policy = "dit_half" - if invalid == "disabled": - backbone["gradient_checkpointing"] = False - elif invalid == "odd_depth": - backbone["depth"] = 11 - elif invalid == "wrong_owner": - backbone["_target_"] = "unreviewed.Backbone" - else: - policy = "guess" - with pytest.raises(ValueError): - configure_dit_checkpoint_policy(encoder, field, policy) - assert all( - "checkpoint_policy" not in owner["backbone"] - for owner in [*encoder["encoders"].values(), field["field"]] - ) - - -def test_launcher_child_helper_exists_and_parses(): - root = Path(__file__).resolve().parents[1] - helper = root / "scripts/train/action_flow_run_child.sh" - assert helper.is_file() - subprocess.run(["bash", "-n", str(helper)], check=True) - assert "--cpu-bind=none" in helper.read_text() diff --git a/tests/test_native_final_validation_lifecycle.py b/tests/test_native_final_validation_lifecycle.py deleted file mode 100644 index d1a0392d5d..0000000000 --- a/tests/test_native_final_validation_lifecycle.py +++ /dev/null @@ -1,135 +0,0 @@ -"""Execute maintained lifecycle methods without model/data/runtime imports.""" - -import ast -import sys -import types -import unittest -from pathlib import Path -from unittest.mock import patch - -ROOT = Path(__file__).resolve().parents[1] - - -def functions(path, names): - tree = ast.parse((ROOT / path).read_text()) - nodes = [] - for node in ast.walk(tree): - if isinstance(node, ast.FunctionDef) and node.name in names: - node.decorator_list = [] - nodes.append(node) - ns = {} - exec(compile(ast.Module(body=nodes, type_ignores=[]), str(path), "exec"), ns) - return ns - - -F = functions("egomimic/trainHydra.py", {"_needs_final_validation"})[ - "_needs_final_validation" -] -N = functions( - "egomimic/eval/libero_action_flow_eval.py", - {"on_validation_end", "has_completed_validation"}, -) - - -class Native: - on_validation_end = N["on_validation_end"] - has_completed_validation = N["has_completed_validation"] - - def __init__(self, group="valid", batches=None, sanity=False): - self.artifact_root = "unused" - self.artifact_identity = {} - self.group = group - self.artifact_batches = [{"batch_index": 0}] if batches is None else batches - self.model = types.SimpleNamespace(global_step=2) - self.trainer = types.SimpleNamespace(sanity_checking=sanity) - self._completed_validation_step = None - - -class Lifecycle(unittest.TestCase): - def publication(self, e, error=None): - module = types.ModuleType("egomimic.benchmarks.libero.action_flow_artifacts") - module.canonical_sha = lambda identity: "identity" - - def writer(root, payload): - if error: - raise error - - module.write_artifact = writer - with patch.dict(sys.modules, {module.__name__: module}): - e.on_validation_end() - - def test_periodic_terminal_completion_skips_only_duplicate(self): - e = Native() - self.assertTrue(F(e, 2)) - self.publication(e) - self.assertFalse(F(e, 2)) - self.assertTrue(F(e, 3)) - self.assertTrue(F(e, 80000)) - - def test_missing_generic_and_nonboolean_capability_keep_final(self): - for e in ( - None, - object(), - types.SimpleNamespace(has_completed_validation=lambda step: 1), - ): - self.assertTrue(F(e, 2)) - - def test_empty_sanity_and_other_group_keep_final(self): - for e in (Native(batches=[]), Native(sanity=True), Native(group="train_viz")): - self.publication(e) - self.assertTrue(F(e, 2)) - - def test_new_empty_or_failed_pass_invalidates_prior_completion(self): - tree = ast.parse( - (ROOT / "egomimic/eval/libero_action_flow_eval.py").read_text() - ) - method = next( - n - for n in ast.walk(tree) - if isinstance(n, ast.FunctionDef) and n.name == "on_validation_start" - ) - e = Native() - e._completed_validation_step = 2 - exec( - compile( - ast.Module(body=method.body[:1], type_ignores=[]), - "native-start", - "exec", - ), - {"self": e}, - ) - e.artifact_batches = [] - self.publication(e) - self.assertTrue(F(e, 2)) - - def test_failed_immutable_publication_does_not_complete(self): - e = Native() - with self.assertRaises(FileExistsError): - self.publication(e, FileExistsError("immutable collision")) - self.assertTrue(F(e, 2)) - self.assertTrue(e.artifact_batches) - - def test_completion_reset_precedes_replayed_valid_batch(self): - tree = ast.parse( - (ROOT / "egomimic/eval/libero_action_flow_eval.py").read_text() - ) - method = next( - n - for n in ast.walk(tree) - if isinstance(n, ast.FunctionDef) and n.name == "on_validation_step" - ) - # Execute the actual initial statements, stopping before model inference. - statements = method.body[:2] - e = Native() - e._completed_validation_step = 2 - exec( - compile( - ast.Module(body=statements, type_ignores=[]), "native-reset", "exec" - ), - {"self": e, "batch_idx": 0, "dataloader_idx": 0}, - ) - self.assertTrue(F(e, 2)) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_oat_step_budget.py b/tests/test_oat_step_budget.py deleted file mode 100644 index 45b12d0023..0000000000 --- a/tests/test_oat_step_budget.py +++ /dev/null @@ -1,214 +0,0 @@ -"""Step-limited pilot provenance and weights-only callback metadata.""" - -from types import SimpleNamespace - -import pytest -import torch - -from egomimic.pl_utils.oat_training import ( - OATBatchBudgetCallback, - OATEMACallback, - OATTrainingBehavior, -) - - -@pytest.mark.parametrize( - "epochs,max_steps,expected", - [(3, -1, 30), (3, 17, 17), (-1, 17, 17), (3, 45, 30)], -) -def test_step_budget_honors_first_limit(epochs, max_steps, expected): - trainer = SimpleNamespace( - datamodule=SimpleNamespace( - train_datasets={"x": range(83)}, - train_dataloader_params={"x": {"batch_size": 2, "drop_last": True}}, - ), - accumulate_grad_batches=4, - world_size=1, - limit_train_batches=1.0, - max_epochs=epochs, - max_steps=max_steps, - is_global_zero=False, - ) - callback = OATBatchBudgetCallback(global_batch_size=8) - callback.on_fit_start(trainer, None) - assert callback.budget["total_optimizer_steps"] == expected - assert callback.budget["max_steps"] == max_steps - assert callback.budget["dropped_examples_per_epoch"] == 3 - assert trainer.limit_train_batches == 40 - - -def test_joint_budget_matches_actual_cycling_loader_and_preserves_both_sources(): - from lightning.pytorch.utilities.combined_loader import CombinedLoader - from torch.utils.data import DataLoader - - datasets = {"original": range(83), "sharpa": range(37)} - params = {name: {"batch_size": 16, "drop_last": True} for name in datasets} - trainer = SimpleNamespace( - datamodule=SimpleNamespace( - train_datasets=datasets, - train_dataloader_params=params, - train_loader_mode="max_size_cycle", - ), - accumulate_grad_batches=1, - world_size=1, - limit_train_batches=1.0, - max_epochs=-1, - max_steps=120000, - is_global_zero=False, - ) - callback = OATBatchBudgetCallback( - global_batch_size=32, expected_source_batches={"original": 16, "sharpa": 16} - ) - callback.on_fit_start(trainer, None) - loader = CombinedLoader( - {name: DataLoader(data, **params[name]) for name, data in datasets.items()}, - "max_size_cycle", - ) - seen = list(loader) - assert trainer.limit_train_batches == len(seen) == 5 - assert all( - {name: len(batch) for name, batch in item[0].items()} - == {"original": 16, "sharpa": 16} - for item in seen - ) - assert callback.budget["total_optimizer_steps"] == 120000 - assert callback.budget["global_batch_size"] == 32 - params["sharpa"]["batch_size"] = 8 - trainer.limit_train_batches = 1.0 - with pytest.raises(ValueError, match="preserve batch size"): - callback.on_fit_start(trainer, None) - - -@pytest.mark.parametrize("weights_only", [True, False]) -def test_weights_only_retains_ema_and_budget_without_optimizer( - monkeypatch, weights_only -): - from egomimic.models.oat import checkpoint as contract - - monkeypatch.setattr(contract, "validate_input_representation", lambda stages: {}) - stage = SimpleNamespace(normalizer_state={"scale": 1}, data_context={"split": 42}) - - class Model(torch.nn.Module): - def __init__(self): - super().__init__() - self.weight = torch.nn.Parameter(torch.tensor([2.0])) - self.model = SimpleNamespace(pipeline=SimpleNamespace(stages=[stage])) - - model = Model() - budget = OATBatchBudgetCallback() - budget.budget = {"total_optimizer_steps": 10000} - ema = OATEMACallback() - trainer = SimpleNamespace(callbacks=[budget, ema], global_step=0) - model.trainer = trainer - ema.on_fit_start(trainer, model) - ema._num_updates = 3 - ema._shadow["weight"].fill_(1.0) - behavior = OATTrainingBehavior() - behavior.bind(model) - checkpoint = {"state_dict": model.state_dict(), "global_step": 3} - if not weights_only: - checkpoint["optimizer_states"] = [{"sentinel": True}] - for callback in trainer.callbacks: - callback.on_save_checkpoint(trainer, model, checkpoint) - # Full-state model hook must not call callbacks a second time. - monkeypatch.setattr( - ema, "on_save_checkpoint", lambda *args: pytest.fail("twice") - ) - behavior.on_save_checkpoint(checkpoint) - assert checkpoint["ema_num_updates"] == 3 - assert checkpoint["ema_state_dict"]["weight"].item() == 1.0 - assert checkpoint["state_dict"]["weight"].item() == 2.0 - assert checkpoint["training_budget"]["total_optimizer_steps"] == 10000 - assert checkpoint["normalizer_state"] == {"scale": 1} - assert ("optimizer_states" not in checkpoint) == weights_only - - -@pytest.mark.parametrize("basis", ["uniform", "fourier", "chebyshev"]) -@pytest.mark.parametrize("mode", ["smoke", "full"]) -@pytest.mark.parametrize("gpus", [1, 4]) -def test_pilot_arguments_compose_matched_budget_and_checkpoint_policy( - tmp_path, basis, mode, gpus -): - from pathlib import Path - - from hydra import compose, initialize_config_dir - from hydra.utils import instantiate - - from egomimic.benchmarks.libero.cluster import global_basis_training_arguments - - args = global_basis_training_arguments( - basis, tmp_path / "data.zarr", tmp_path, mode, f"test-{basis}-{mode}", gpus=gpus - ) - with initialize_config_dir( - config_dir=str(Path(__file__).parents[1] / "egomimic/hydra_configs"), - version_base="1.3", - ): - cfg = compose(config_name="train_zarr_cartesian", overrides=args[3:]) - assert cfg.benchmark.arc_basis == basis - assert cfg.trainer.max_steps == (16 if mode == "smoke" else 10000) - assert cfg.trainer.max_epochs == -1 - assert cfg.trainer.devices == gpus - assert cfg.model.train_log_on_step - assert cfg.trainer.check_val_every_n_epoch is None - cp = cfg.callbacks.model_checkpoint - assert cp.every_n_train_steps == (8 if mode == "smoke" else 2500) - assert ( - cfg.trainer.val_check_interval - == cp.every_n_train_steps * cfg.trainer.accumulate_grad_batches - ) - assert cp.every_n_epochs == 0 - assert cp.save_weights_only and not cp.save_last and cp.save_top_k == -1 - assert not cp.save_on_train_epoch_end - assert cfg.callbacks.ema.final_checkpoint_path is None - assert "{epoch:" in cp.filename and "{step:" in cp.filename - instantiate(cp) # Real callback argument validation, no training. - expected_batch = 4 * gpus if mode == "smoke" else 1024 - assert cfg.callbacks.batch_budget.global_batch_size == expected_batch - assert ( - cfg.benchmark.batch_size * gpus * cfg.trainer.accumulate_grad_batches - == expected_batch - ) - assert cfg.logger.wandb.resume == "never" and not cfg.logger.wandb.offline - - -def test_early_recovery_save_has_full_state_and_refuses_overwrite(tmp_path): - import json - from types import SimpleNamespace - - import torch - - from egomimic.pl_utils.oat_training import EarlyRecoveryCheckpoint - - callback = EarlyRecoveryCheckpoint(str(tmp_path), first_step=2) - state = { - "global_step": 2, - "ema_num_updates": 2, - "state_dict": {"weight": torch.ones(2)}, - "optimizer_states": [{"state": {0: {"moment": torch.zeros(2)}}}], - "lr_schedulers": [{"last_epoch": 2}], - "data_context": {"bound": True}, - } - - def save(path, weights_only): - assert weights_only is False - torch.save(state, path) - - trainer = SimpleNamespace( - global_step=1, current_epoch=0, world_size=1, save_checkpoint=save - ) - callback.on_train_batch_end(trainer, None, None, None, 0) - assert not list(tmp_path.iterdir()) - trainer.global_step = 2 - callback.on_train_batch_end(trainer, None, None, None, 1) - path = next((tmp_path / "checkpoints").glob("*.ckpt")) - proof = json.loads(path.with_suffix(".ckpt.recovery.json").read_text()) - assert ( - proof["global_step"] == 2 - and proof["full_state"] - and proof["bytes"] == path.stat().st_size - ) - callback.on_train_batch_end(trainer, None, None, None, 1) - with pytest.raises(FileExistsError, match="overwrite"): - EarlyRecoveryCheckpoint(str(tmp_path), 2).on_train_batch_end( - trainer, None, None, None, 1 - ) diff --git a/tests/test_offline_constructor_inputs.py b/tests/test_offline_constructor_inputs.py deleted file mode 100644 index 705391cf92..0000000000 --- a/tests/test_offline_constructor_inputs.py +++ /dev/null @@ -1,119 +0,0 @@ -"""Structural fixtures cannot become learned initialization or run evidence.""" - -import json - -import pytest -import torch -from hydra.utils import instantiate -from omegaconf import OmegaConf - -from scripts.audit_components import offline_static_buffer_construction -from scripts.audit_constructor_inputs import evaluator_inputs, model_inputs -from scripts.audit_hydra_configs import CONFIGS, compose_for_audit - - -def test_native_fixture_is_explicit_and_does_not_read_runtime_snapshot(monkeypatch): - monkeypatch.setattr( - "egomimic.benchmarks.libero.native_artifact_identity._snapshot", - lambda *args: pytest.fail("offline constructor read a runtime snapshot"), - ) - path = ( - CONFIGS - / "experiment/sim/libero_historical/action_flow_libero10_h240_euler50_dithalf_80k_s42.yaml" - ) - with compose_for_audit(path) as cfg: - before = OmegaConf.to_container(cfg.evaluator, resolve=True) - inputs, evidence = evaluator_inputs(cfg.evaluator) - evaluator = instantiate(inputs) - assert evaluator.shared_diagnostics.max_samples == 8 - assert evaluator.shared_diagnostics.activation_layer_map == tuple( - (i, i) for i in range(12) - ) - assert evidence["external_binding"].startswith("UNVERIFIED") - assert OmegaConf.to_container(cfg.evaluator, resolve=True) == before - invalid = OmegaConf.create(before) - invalid.artifact_identity._target_ = "not.a.canonical.factory" - with pytest.raises(ValueError, match="canonical descriptors"): - evaluator_inputs(invalid) - - -@pytest.mark.parametrize( - ("recipe", "dimension", "horizon"), - [ - ("libero_oatpolicy", 7, 32), - # Use the declared ARC recipe geometry, not the native-action horizon. - ("libero_arc_oatpolicy", 12, 32), - ], -) -def test_meta_tokenizer_comes_from_declared_template_and_scope_restores( - recipe, dimension, horizon -): - from egomimic.models.oat import factory - - original = factory.load_tokenizer - with compose_for_audit(CONFIGS / f"experiment/sim/oat/{recipe}.yaml") as cfg: - before = json.dumps(OmegaConf.to_container(cfg, resolve=True), sort_keys=True) - with ( - torch.device("meta"), - offline_static_buffer_construction(), - model_inputs(cfg, CONFIGS) as evidence, - ): - tokenizer = factory.load_tokenizer(None) - assert tokenizer.decoder.sample_dim == dimension - assert tokenizer.decoder.sample_horizon == horizon - assert all( - p.device.type == "meta" and not p.requires_grad - for p in tokenizer.parameters() - ) - assert evidence["external_binding"].startswith("UNVERIFIED") - assert not hasattr(tokenizer, "_training_data_context") - if recipe == "libero_arc_oatpolicy": - from egomimic.pipeline.stages_libero_arc import LiberoArcStage - - codec = instantiate(cfg.model.pipeline.stages[0]) - assert isinstance(codec, LiberoArcStage) - assert ( - tokenizer._training_input_representation - == codec.representation_context() - ) - else: - assert tokenizer._training_input_representation is None - assert factory.load_tokenizer is original - - assert ( - json.dumps(OmegaConf.to_container(cfg, resolve=True), sort_keys=True) - == before - ) - with pytest.raises(ValueError, match="trained OAT tokenizer"): - factory.load_tokenizer(None) - - -def test_initializer_rejects_cpu_use_and_explicit_checkpoint_and_restores(): - from egomimic.models.oat import factory - - original = factory.load_tokenizer - with compose_for_audit(CONFIGS / "experiment/sim/oat/libero_oatpolicy.yaml") as cfg: - with pytest.raises(ValueError, match="restricted to meta"): - with model_inputs(cfg, CONFIGS): - factory.load_tokenizer(None) - assert factory.load_tokenizer is original - cfg.benchmark.tokenizer_checkpoint = "/real/run/tokenizer.ckpt" - with pytest.raises(ValueError, match="explicit learned checkpoint"): - with model_inputs(cfg, CONFIGS): - pytest.fail("explicit checkpoint replaced") - assert factory.load_tokenizer is original - - -def test_arc_structural_metadata_preserves_real_values_rng_and_method(): - from egomimic.pipeline.stages_libero_arc import LiberoArcStage - - original = LiberoArcStage.representation_context - with torch.device("cpu"): - expected = LiberoArcStage(num_waypoints=32, horizon=32).representation_context() - rng = torch.get_rng_state().clone() - with torch.device("meta"), offline_static_buffer_construction(): - codec = LiberoArcStage(num_waypoints=32, horizon=32) - assert codec.action_scale.device.type == "meta" - assert codec.representation_context() == expected - assert LiberoArcStage.representation_context is original - assert torch.equal(torch.get_rng_state(), rng) diff --git a/tests/test_offline_static_buffer_audit.py b/tests/test_offline_static_buffer_audit.py deleted file mode 100644 index a45daddc55..0000000000 --- a/tests/test_offline_static_buffer_audit.py +++ /dev/null @@ -1,51 +0,0 @@ -import pytest -import torch - -from egomimic.models.oat.tokenizer.oat.quantizer.fsq import FSQ -from scripts.audit_components import offline_static_buffer_construction - - -@pytest.mark.parametrize("levels", [(8, 5, 5, 5), (3, 4)]) -def test_static_lookup_buffers_match_real_constructor_and_rng(levels): - original = FSQ.__init__ - baseline = FSQ(list(levels)) - before = torch.get_rng_state().clone() - with offline_static_buffer_construction(): - candidate = FSQ(list(levels)) - with torch.device("meta"): - meta = FSQ(list(levels)) - assert FSQ.__init__ is original - assert torch.equal(before, torch.get_rng_state()) - assert candidate.codebook_size == meta.codebook_size == baseline.codebook_size - assert not tuple(candidate.parameters()) - for name, value in baseline.named_buffers(): - actual = dict(candidate.named_buffers())[name] - metadata = dict(meta.named_buffers())[name] - assert torch.equal(value, actual) - assert metadata.device.type == "meta" - assert metadata.shape == value.shape and metadata.dtype == value.dtype - - -def test_scope_restores_constructor_on_failure(): - original = FSQ.__init__ - linspace = torch.linspace - with pytest.raises(RuntimeError, match="fixture failure"): - with offline_static_buffer_construction(): - raise RuntimeError("fixture failure") - assert FSQ.__init__ is original - assert torch.linspace is linspace - - -def test_full_tokenizer_layout_and_scalar_schedule_scope(): - from egomimic.models.oat.factory import make_tokenizer - - baseline = make_tokenizer() - before = torch.get_rng_state().clone() - with offline_static_buffer_construction(), torch.device("meta"): - meta = make_tokenizer() - assert torch.linspace(0, 1, 3).device.type == "meta" - assert torch.equal(before, torch.get_rng_state()) - assert baseline.state_dict().keys() == meta.state_dict().keys() - for name, value in baseline.state_dict().items(): - candidate = meta.state_dict()[name] - assert candidate.shape == value.shape and candidate.dtype == value.dtype diff --git a/tests/test_original_size_action_flow_cotrain_config.py b/tests/test_original_size_action_flow_cotrain_config.py deleted file mode 100644 index 9b73022548..0000000000 --- a/tests/test_original_size_action_flow_cotrain_config.py +++ /dev/null @@ -1,72 +0,0 @@ -from pathlib import Path - -import pytest -from hydra import compose, initialize_config_dir - -CONFIG_DIR = Path(__file__).parents[1] / "egomimic" / "hydra_configs" -ROW = "parents/action_flow_cotrain_uc_manual4919" - - -def test_original_size_cotrain_contract(monkeypatch): - monkeypatch.setenv("PUSHSHAPES_USOCKET_ROOT", "/verified/usocket") - monkeypatch.setenv("PUSHSHAPES_CHAIN_GRIPPER_ROOT", "/verified/chain") - with initialize_config_dir(version_base=None, config_dir=str(CONFIG_DIR.resolve())): - cfg = compose( - config_name="train_zarr_cartesian", - # Component compatibility fixture, not a selectable training recipe. - # Keep the historical schedule assertions explicit after leaf retirement. - overrides=[ - f"+experiment=sim/pusht/{ROW}", - "++paths.root_dir=.", - "trainer.max_steps=150000", - "trainer.val_check_interval=10000", - "callbacks.model_checkpoint.every_n_train_steps=30000", - ], - ) - - stages = cfg.model.pipeline.stages - encoder = next( - s for s in stages if s._target_.endswith("RoutedContentEncoderStage") - ) - decoder = next( - s for s in stages if s._target_.endswith("RoutedContentDecoderStage") - ) - fields = [s for s in stages if s._target_.endswith("ConditionalVelocityStage")] - field = fields[0] - objective = stages[-1] - routes = {"pushshapes_sim_u_socket", "pushshapes_sim_chain_gripper"} - - assert cfg.model._target_ == "egomimic.pl_utils.pl_model.ModelWrapper" - assert cfg.model.training_behavior._target_.endswith("ActionFlowTrainingBehavior") - assert cfg.model.hidden_dim == 384 - assert cfg.model.flow_samples_per_content == 14 - assert cfg.model.flow_mini_batch == 14 - assert cfg.model.cfg_scale == pytest.approx(4.0) - assert cfg.model.num_inference_steps == 50 - assert field.inference_method == "dopri5" - assert field.field.backbone.hidden_dim == 384 - assert field.field.backbone.depth == 12 - assert field.field.backbone.num_heads == 12 - assert len(fields) == 1 - assert set(encoder.encoders) == routes - assert set(decoder.decoders) == routes - assert encoder.encoders.pushshapes_sim_u_socket.action_dim == 4 - assert encoder.encoders.pushshapes_sim_chain_gripper.action_dim == 6 - assert decoder.decoders.pushshapes_sim_u_socket.action_dim == 4 - assert decoder.decoders.pushshapes_sim_chain_gripper.action_dim == 6 - assert objective.flow_aggregation == "sum_samples" - assert objective.action_velocity_weight == pytest.approx(1.0) - assert cfg.model.optimizer.lr == pytest.approx(1e-4) - assert cfg.model.scheduler.base_lr_1 == pytest.approx(1e-4) - assert cfg.model.scheduler.base_lr_2 == pytest.approx(5e-5) - assert cfg.model.scheduler.final_lr == pytest.approx(5e-5) - assert cfg.trainer.max_steps == 150_000 - assert cfg.trainer.val_check_interval == 10_000 - assert cfg.callbacks.model_checkpoint.every_n_train_steps == 30_000 - assert cfg.data.train_dataloader_params.pushshapes_sim_u_socket.batch_size == 32 - assert ( - cfg.data.train_dataloader_params.pushshapes_sim_chain_gripper.batch_size == 32 - ) - assert cfg.run_provenance.objective.domain_aggregation == ( - "equal_mean_from_one_batch32_per_domain" - ) diff --git a/tests/test_osmo_gate.py b/tests/test_osmo_gate.py deleted file mode 100644 index f87599913f..0000000000 --- a/tests/test_osmo_gate.py +++ /dev/null @@ -1,122 +0,0 @@ -"""Exercise the same five-update/save/strict-reload harness before OSMO allocation.""" - -import hashlib -import io -import json - -import pytest -from omegaconf import OmegaConf, open_dict - -from scripts.integration import run_gate -from scripts.integration.prepare_inputs import copy_stream, relative_path -from tests.fixtures.synthetic_episodes import write_episode -from tests.test_retained_recipe_steps import small_cpu_model - - -def test_input_staging_checks_hashes_and_never_replaces_existing_data(tmp_path): - path = tmp_path / "verified.bin" - value = b"immutable-input" - sha = hashlib.sha256(value).hexdigest() - receipt = copy_stream( - io.BytesIO(value), path, expected_bytes=len(value), expected_sha256=sha - ) - assert receipt["sha256"] == sha and path.read_bytes() == value - with pytest.raises(FileExistsError): - copy_stream( - io.BytesIO(value), path, expected_bytes=len(value), expected_sha256=sha - ) - assert path.read_bytes() == value - with pytest.raises(ValueError, match="SHA-256"): - copy_stream( - io.BytesIO(b"bad"), - tmp_path / "changed.bin", - expected_bytes=3, - expected_sha256=sha, - ) - assert not (tmp_path / "changed.bin").exists() - with pytest.raises(ValueError, match="relative"): - relative_path("../../outside") - - -def harness_inputs(monkeypatch, tmp_path): - inputs, output = tmp_path / "inputs", tmp_path / "result" - output.mkdir() - data = inputs / "data/eva" - episodes = [] - for index in range(3): - write_episode(data, "eva", T=8, H=32, W=32, seed=index) - episodes.append( - { - "source": "eva", - "split": "train" if index < 2 else "valid", - "episode_hash": f"eva_{index:02}", - } - ) - (inputs / "data-manifest.json").write_text(json.dumps({"episodes": episodes})) - (inputs / "input-receipt.json").write_text('{"scope":"synthetic CPU fixture"}') - monkeypatch.setenv("SMOKE_INPUT_ROOT", str(inputs)) - matrix = OmegaConf.load(run_gate.ROOT / "scripts/integration/matrix.yaml") - matrix.trainer.accelerator = "cpu" - matrix.trainer.precision = "32-true" - # Tiny 32px fixtures collapse ResNet's last feature map to 1x1; BatchNorm - # still needs two samples. Real GPU cases retain their full image sizes. - matrix.cases["hpt-eva"].batch_size = 2 - matrix_path = tmp_path / "matrix.yaml" - OmegaConf.save(matrix, matrix_path) - return inputs, output, matrix_path - - -@pytest.mark.parametrize("export", [True, False]) -def test_wrong_target_frame_rejected_before_network_construction( - monkeypatch, tmp_path, export -): - import egomimic.trainHydra as entrypoint - - inputs, output, matrix_path = harness_inputs(monkeypatch, tmp_path) - cfg, _, _ = run_gate.configured_case(matrix_path, "hpt-eva", inputs, output) - cfg.inference_config.enabled = export - for split in ("train_datasets", "valid_datasets"): - ds = cfg.data[split].eva_bimanual - with open_dict(ds): - ds.bounds_check = False - ds.resolver.transform_list.coord_frame = "eef_frame" - - def forbidden(*args, **kwargs): - raise AssertionError("Network was constructed before frame preflight") - - monkeypatch.setattr(entrypoint, "_instantiate_model_wrapper", forbidden) - with pytest.raises(ValueError, match="coord_frame.*camframe.*eef_frame"): - entrypoint.train(cfg) - assert not (output / "checkpoints/inference-config.yaml").exists() - - -def test_training_harness_runs_five_updates_validation_and_bound_reload( - monkeypatch, tmp_path -): - inputs, output, matrix_path = harness_inputs(monkeypatch, tmp_path) - original = run_gate.configured_case - - def configured(*args, **kwargs): - cfg, _, spec = original(*args, **kwargs) - cfg = small_cpu_model(cfg, "hpt") - with open_dict(cfg): - cfg.model.pipeline.device = "cpu" - for split in ("train_datasets", "valid_datasets"): - cfg.data[split].eva_bimanual.bounds_check = False - saved = OmegaConf.masked_copy(cfg, [key for key in cfg if key != "hydra"]) - saved = OmegaConf.create(OmegaConf.to_container(saved, resolve=True)) - return cfg, saved, spec - - monkeypatch.setattr(run_gate, "configured_case", configured) - run_gate.training_gate(matrix_path, "hpt-eva", inputs, output) - receipt = json.loads((output / "training-receipt.json").read_text()) - assert receipt["steps"] == 5 and len(receipt["gradients"]) == 5 - assert receipt["gpu"] is None - assert ( - receipt["validation"] - and receipt["initial_parameter_probe"] != receipt["final_parameter_probe"] - ) - run_gate.verification_gate(output, device="cpu") - verified = json.loads((output / "inference-receipt.json").read_text()) - assert verified["checkpoint_sha256"] == receipt["checkpoint_sha256"] - assert [case["executed_shape"][1] for case in verified["cases"]] == [1, 2] diff --git a/tests/test_pi_source_patch.py b/tests/test_pi_source_patch.py deleted file mode 100644 index 9cf7d3b088..0000000000 --- a/tests/test_pi_source_patch.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Pinned OpenPI patching must never modify another environment's hardlinks.""" - -import hashlib -import os - -import pytest - -from scripts.install_pi05_source import apply_transformers_patch - - -def test_verified_patch_preserves_cache_hardlink_and_original(tmp_path): - cache = tmp_path / "cached.py" - cache.write_bytes(b"original code") - root = tmp_path / "transformers" - root.mkdir() - target = root / "model.py" - os.link(cache, target) - content = b"pinned patched code" - manifest = {"model.py": hashlib.sha256(content).hexdigest()} - receipt = apply_transformers_patch( - root, manifest, lambda path: content, tmp_path / "backups" - ) - assert target.read_bytes() == content - assert cache.read_bytes() == b"original code" - assert target.stat().st_ino != cache.stat().st_ino - from pathlib import Path - - assert Path(receipt[0]["backup"]).read_bytes() == b"original code" - assert ( - apply_transformers_patch( - root, manifest, lambda path: content, tmp_path / "backups" - )[0]["sha256"] - == receipt[0]["sha256"] - ) - - -def test_every_hash_is_preflighted_before_patch_mutation(tmp_path): - root = tmp_path / "transformers" - root.mkdir() - (root / "a.py").write_bytes(b"original") - expected = hashlib.sha256(b"valid").hexdigest() - with pytest.raises(ValueError, match="hash mismatch"): - apply_transformers_patch( - root, - {"a.py": expected, "b.py": "wrong"}, - lambda path: b"valid", - tmp_path / "backups", - ) - assert (root / "a.py").read_bytes() == b"original" - assert not (root / "b.py").exists() diff --git a/tests/test_pipeline_only_surface.py b/tests/test_pipeline_only_surface.py deleted file mode 100644 index f183f89b74..0000000000 --- a/tests/test_pipeline_only_surface.py +++ /dev/null @@ -1,51 +0,0 @@ -import tomllib -from pathlib import Path - -ROOT = Path(__file__).parents[1] - - -def test_legacy_first_party_policy_surface_is_removed(): - removed = [ - "egomimic/models/act_nets.py", - "egomimic/models/hpt_nets.py", - "egomimic/models/preprocess_pi_obs.py", - "egomimic/models/denoising_policy.py", - "egomimic/models/fm_policy.py", - "egomimic/eval/eval_act.py", - "egomimic/eval/eval_hpt.py", - "egomimic/eval/eval_pi.py", - "egomimic/eval/eval_latent.py", - "egomimic/eval/eval_video.py", - "egomimic/eval/latent_dataset.py", - "egomimic/utils/hydra_resolvers.py", - "egomimic/utils/scheduler_utils.py", - "egomimic/utils/tensor_utils.py", - "external/openpi", - ] - leftovers = [relative for relative in removed if (ROOT / relative).exists()] - assert leftovers == [] - assert list((ROOT / "egomimic/algo").glob("*.py")) == [] - - -def test_surviving_model_configs_are_pipeline_only(): - config_dir = ROOT / "egomimic/hydra_configs/model" - configs = sorted(config_dir.rglob("*.yaml")) - for config in configs: - text = config.read_text() - assert "egomimic.algo." not in text, config - assert "robomimic_model" not in text, config - - -def test_distribution_discovers_only_the_egomimic_package(): - project = tomllib.loads((ROOT / "pyproject.toml").read_text()) - package_find = project["tool"]["setuptools"]["packages"]["find"] - assert package_find["include"] == ["egomimic*"] - assert not (ROOT / "setup.py").exists() - - -def test_training_entrypoint_has_one_strict_evaluation_path(): - entrypoint = (ROOT / "egomimic/trainHydra.py").read_text() - assert 'hasattr(eval_obj, "run")' not in entrypoint - assert 'hasattr(eval_obj, "bind_data_context")' not in entrypoint - assert "strict=False" not in entrypoint - assert "register_new_resolver" not in entrypoint diff --git a/tests/test_pusht_parent_cleanup.py b/tests/test_pusht_parent_cleanup.py deleted file mode 100644 index 01250053c1..0000000000 --- a/tests/test_pusht_parent_cleanup.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Keep every PushT recipe's science identical when deduplicating its parents.""" - -import hashlib -import json -from pathlib import Path - -import pytest -from hydra.utils import instantiate -from omegaconf import OmegaConf - -from scripts.audit_hydra_configs import CONFIGS, compose_for_audit - -BASELINE = json.loads( - ( - Path(__file__).parent / "fixtures/pusht_config_before_parent_cleanup.json" - ).read_text() -) - - -@pytest.mark.parametrize( - "parent", - [ - "episode_split", - "action_flow_uc_h16", - "action_flow_points6_uc_h16", - "standard_dp_uc_h16", - ], -) -def test_shared_parents_construct_their_data_boundary(parent): - with compose_for_audit(CONFIGS / "data/pusht/parents" / f"{parent}.yaml") as cfg: - data = instantiate(cfg.data, _recursive_=False) - assert data.preflight_configuration() - - -MOVE_BASELINE = json.loads( - (Path(__file__).parent / "fixtures/pusht_before_real_sim_cleanup.json").read_text() -) - - -@pytest.mark.parametrize("recipe,expected", sorted(MOVE_BASELINE["recipes"].items())) -def test_existing_pusht_recipe_keeps_exact_configuration(recipe, expected): - with compose_for_audit(CONFIGS / "experiment/sim/pusht" / f"{recipe}.yaml") as cfg: - actual = OmegaConf.to_container(cfg, resolve=False) - actual.pop("hydra", None) # A parent's selection path intentionally changed. - encoded = json.dumps(actual, sort_keys=True, separators=(",", ":")).encode() - assert hashlib.sha256(encoded).hexdigest() == expected - - -def test_retired_parent_paths_have_no_selectable_config_references(): - for group, retired in ( - ("data", BASELINE["retired_data"]), - ("experiment", BASELINE["retired_recipes"]), - ): - for old in retired: - assert not (CONFIGS / group / "pusht" / f"{old}.yaml").exists() - for path in CONFIGS.rglob("*.yaml"): - assert f"pusht/{old}" not in path.read_text() - assert not ( - CONFIGS / "data/pusht/planar_usocket_chain_manual4919_standard_retimed.yaml" - ).exists() - - -@pytest.mark.parametrize( - "recipe", - [ - "planar_uc_manual4919_dp_261m_af_obs_multiplier", - "action_flow_cotrain_uc_multiplier_interpolation", - ], -) -def test_selected_pair_still_has_2999_and_4919_with_two_full_batches(recipe): - with compose_for_audit(CONFIGS / "experiment/sim/pusht" / f"{recipe}.yaml") as cfg: - for group in ("train_datasets", "valid_datasets"): - datasets = cfg.data[group] - assert ( - datasets.pushshapes_sim_u_socket.resolver.expected_episode_count == 2999 - ) - assert ( - datasets.pushshapes_sim_chain_gripper.resolver.expected_episode_count - == 4919 - ) - assert all( - d.valid_ratio == 0.01 and d.split_seed == 42 for d in datasets.values() - ) - assert all( - d.batch_size == 32 for d in cfg.data.train_dataloader_params.values() - ) diff --git a/tests/test_rank_synthetic_trajectory_nn.py b/tests/test_rank_synthetic_trajectory_nn.py deleted file mode 100644 index f22602274e..0000000000 --- a/tests/test_rank_synthetic_trajectory_nn.py +++ /dev/null @@ -1,207 +0,0 @@ -import hashlib -import json -import subprocess -import sys -from pathlib import Path - -import numpy as np - -SCRIPT = ( - Path(__file__).parents[1] - / "scripts" - / "experiments" - / "rank_synthetic_trajectory_nn.py" -) - - -def _matrix(tmp_path, *, variants=("a", "b"), seeds=(42, 43, 44), particles=16): - rng = np.random.default_rng(7) - targets = rng.normal(size=(particles, 3)).astype(np.float32) - dataset = tmp_path / "eval.npz" - np.savez_compressed( - dataset, - split=np.ones(particles, dtype=np.int64), - target_3d=targets, - ) - config_dir = tmp_path / "config" - config_dir.mkdir() - trajectory_root = tmp_path / "trajectories" - trajectory_root.mkdir() - configs = [] - trajectories = {} - for variant in variants: - for seed in seeds: - run_id = f"run-{variant}-{seed}" - output = tmp_path / "runs" / run_id - (output / "checkpoints").mkdir(parents=True) - (output / "summary.json").write_text("{}\n") - checkpoint = ( - output / "checkpoints" / "epoch-equivalent-000001-global-step-000010.pt" - ) - checkpoint.write_bytes(f"checkpoint-{variant}-{seed}".encode()) - config = { - "variant": variant, - "seed": seed, - "evaluation_dataset": str(dataset), - "output_dir": str(output), - "max_steps": 10, - "wandb": {"id": run_id}, - } - config_path = config_dir / f"{run_id}.json" - config_path.write_text(json.dumps(config)) - configs.append(str(config_path)) - final = targets if variant == "a" else targets + 5.0 - points = np.stack([np.zeros_like(targets), final]) - trajectory = trajectory_root / f"{run_id}.npz" - np.savez_compressed( - trajectory, - times=np.array([0.0, 1.0], dtype=np.float32), - points=points, - target=targets, - ) - trajectories[run_id] = trajectory - manifest = { - "schema_version": 1, - "variants": list(variants), - "seeds": list(seeds), - "datasets": { - "evaluation": { - "path": str(dataset), - "sha256": hashlib.sha256(dataset.read_bytes()).hexdigest(), - } - }, - "configs": configs, - } - manifest_path = config_dir / "manifest.json" - manifest_path.write_text(json.dumps(manifest)) - return manifest_path, dataset, trajectory_root, trajectories, targets - - -def _command(manifest, output_json, output_markdown, particles): - return [ - sys.executable, - str(SCRIPT), - "--manifest", - str(manifest), - "--expected-particles", - str(particles), - "--distance-chunk-size", - "5", - "--output-json", - str(output_json), - "--output-markdown", - str(output_markdown), - ] - - -def test_ranks_complete_matrix_and_records_exact_provenance(tmp_path): - manifest, dataset, root, trajectories, _ = _matrix(tmp_path) - output_json = tmp_path / "ranking.json" - output_markdown = tmp_path / "ranking.md" - subprocess.run( - _command(manifest, output_json, output_markdown, 16) - + ["--trajectory-root", str(root)], - check=True, - ) - - result = json.loads(output_json.read_text()) - assert [row["variant"] for row in result["ranking"]] == ["a", "b"] - assert result["ranking"][0]["mean"] == 0.0 - assert result["ranking"][0]["sample_std"] == 0.0 - assert [row["seed"] for row in result["ranking"][0]["per_seed"]] == [ - 42, - 43, - 44, - ] - assert result["comparison"]["particles"] == 16 - assert ( - result["comparison"]["dataset"]["sha256"] - == hashlib.sha256(dataset.read_bytes()).hexdigest() - ) - run = next(row for row in result["runs"] if row["run_id"] == "run-a-42") - assert ( - run["trajectory"]["sha256"] - == hashlib.sha256(trajectories["run-a-42"].read_bytes()).hexdigest() - ) - assert run["checkpoint"]["global_step"] == 10 - assert len(run["checkpoint"]["sha256"]) == 64 - markdown = output_markdown.read_text() - assert "Symmetric NN MSE" in markdown - assert "42: 0" in markdown - - -def test_accepts_only_a_complete_set_of_exact_npz_paths(tmp_path): - manifest, _, _, trajectories, _ = _matrix( - tmp_path, variants=("a",), seeds=(42, 43), particles=8 - ) - output_json = tmp_path / "ranking.json" - output_markdown = tmp_path / "ranking.md" - explicit = [] - for run_id, path in trajectories.items(): - explicit.extend(["--trajectory-npz", f"{run_id}={path}"]) - subprocess.run( - _command(manifest, output_json, output_markdown, 8) + explicit, - check=True, - ) - assert len(json.loads(output_json.read_text())["runs"]) == 2 - - incomplete = subprocess.run( - _command(manifest, output_json, output_markdown, 8) - + ["--trajectory-npz", f"run-a-42={trajectories['run-a-42']}"], - text=True, - capture_output=True, - ) - assert incomplete.returncode != 0 - assert "explicit trajectory set does not match complete matrix" in incomplete.stderr - - -def test_rejects_target_or_time_grid_mismatch(tmp_path): - manifest, _, root, trajectories, targets = _matrix( - tmp_path, variants=("a",), seeds=(42, 43), particles=8 - ) - mismatched = trajectories["run-a-43"] - np.savez_compressed( - mismatched, - times=np.array([0.0, 0.4, 1.0], dtype=np.float32), - points=np.stack([targets, targets, targets]), - target=targets, - ) - result = subprocess.run( - _command(manifest, tmp_path / "ranking.json", tmp_path / "ranking.md", 8) - + ["--trajectory-root", str(root)], - text=True, - capture_output=True, - ) - assert result.returncode != 0 - assert "time grid differs" in result.stderr - - np.savez_compressed( - mismatched, - times=np.array([0.0, 1.0], dtype=np.float32), - points=np.stack([targets, targets]), - target=targets + 1.0, - ) - result = subprocess.run( - _command(manifest, tmp_path / "ranking.json", tmp_path / "ranking.md", 8) - + ["--trajectory-root", str(root)], - text=True, - capture_output=True, - ) - assert result.returncode != 0 - assert "does not exactly match evaluation dataset" in result.stderr - - -def test_rejects_run_without_terminal_completion_artifacts(tmp_path): - manifest, _, root, _, _ = _matrix( - tmp_path, variants=("a",), seeds=(42,), particles=8 - ) - config = json.loads(next((tmp_path / "config").glob("run-*.json")).read_text()) - (Path(config["output_dir"]) / "summary.json").unlink() - result = subprocess.run( - _command(manifest, tmp_path / "ranking.json", tmp_path / "ranking.md", 8) - + ["--trajectory-root", str(root)], - text=True, - capture_output=True, - ) - assert result.returncode != 0 - assert "run is incomplete because summary.json is missing" in result.stderr diff --git a/tests/test_speed_retiming_diagnostic.py b/tests/test_speed_retiming_diagnostic.py deleted file mode 100644 index ba1f02022d..0000000000 --- a/tests/test_speed_retiming_diagnostic.py +++ /dev/null @@ -1,123 +0,0 @@ -import numpy as np -import pytest -import torch - -from egomimic.pipeline.stages_speed import SharedSpeedCondition -from egomimic.rldb.zarr.planar_retiming import PlanarCommandRetiming - - -@pytest.mark.parametrize("view,rate", list(enumerate((1, 1.25, 1.5, 1.75, 2)))) -def test_constant_speed_native_retiming(view, rate): - t = PlanarCommandRetiming((1, 1.25, 1.5, 1.75, 2)) - native = np.stack( - (np.arange(31), np.zeros(31), np.zeros(31), np.linspace(0, 1, 31)), axis=-1 - ) - obs = np.array([5, 6, 7]) - b = t.transform({"actions": native, "_retiming_view": view, "obs": obs}) - np.testing.assert_allclose(b["actions"][:, 0], np.arange(16) * rate) - np.testing.assert_allclose(b["actions"][:, 3], np.arange(16) * rate / 30) - np.testing.assert_allclose(b["retiming_rate"], [rate]) - assert "requested_speed" not in b - assert b["obs"] is obs - assert np.array_equal(native[:, 0], np.arange(31)) - - -def test_wrap_stationary_and_tail_rejection(): - t = PlanarCommandRetiming((1.5,), horizon=3) - a = np.zeros((4, 3)) - a[:, 2] = [3.0, 3.1, -3.1, -3.0] - b = t.transform({"actions": a, "_retiming_view": 0}) - assert abs(abs(b["actions"][1, 2]) - np.pi) < 1e-6 - assert b["retiming_rate"].item() == 1.5 - assert "requested_speed" not in b - with pytest.raises(ValueError, match="unpadded"): - t.transform({"actions": a[:3], "_retiming_view": 0}) - - -@pytest.mark.parametrize("encoding", ("scalar", "fourier")) -def test_condition_modes_rng_and_learning(encoding): - torch.manual_seed(123) - rng = torch.get_rng_state().clone() - stage = SharedSpeedCondition(None, encoding) - assert torch.equal(rng, torch.get_rng_state()) - assert stage.contract("train") == stage.contract("inference") - cond = torch.zeros(2, 128) - speed = torch.tensor([[0.25], [2.0]]) - out = stage({"condition": cond, "retiming_rate": speed})["speed_condition"] - assert torch.equal(out, cond) - opt = torch.optim.SGD(stage.parameters(), lr=0.01) - for _ in range(2): - opt.zero_grad() - out = stage({"condition": cond, "retiming_rate": speed})["speed_condition"] - (out - 1).square().mean().backward() - opt.step() - assert stage.mlp[0].weight.grad.abs().sum() > 0 - assert not torch.equal(out[0], out[1]) - with pytest.raises(KeyError): - stage({"condition": cond}) - with pytest.raises(ValueError): - stage({"condition": cond, "retiming_rate": -torch.ones(2, 1)}) - - -def test_unchanged_initialization_across_arms(): - weights = [] - for encoding in ("scalar", "fourier"): - torch.manual_seed(42) - before = torch.nn.Linear(10, 10) - SharedSpeedCondition(None, encoding) - after = torch.nn.Linear(10, 10) - weights.append((before.weight.detach(), after.weight.detach())) - assert all(torch.equal(a, b) for a, b in zip(*weights)) - - -def test_condition_initializer_never_reseeds_cuda(monkeypatch): - def forbidden(*args, **kwargs): - raise AssertionError("Condition initializer must not reseed CUDA") - - monkeypatch.setattr(torch, "manual_seed", forbidden) - monkeypatch.setattr(torch.cuda, "manual_seed_all", forbidden) - rng = torch.get_rng_state().clone() - for encoding in ("scalar", "fourier"): - SharedSpeedCondition(None, encoding) - assert torch.equal(rng, torch.get_rng_state()) - - -@pytest.mark.parametrize("encoding", ("scalar", "fourier")) -def test_real_graph_consumes_speed_in_both_modes(encoding): - from egomimic.pipeline.core import Pipeline - from egomimic.pipeline.stages_action_flow import ( - ConditionalVelocityStage, - LatentBridgeStage, - ) - - class Field(torch.nn.Module): - def forward(self, x, t, condition, **kwargs): - return condition[:, : x.shape[-1]].unsqueeze(1).expand_as(x) - - speed = SharedSpeedCondition(None, encoding, condition_dim=4) - torch.nn.init.constant_(speed.mlp[-1].weight, 0.1) - bridge = LatentBridgeStage( - samples_per_content=1, - condition_key="speed_condition", - condition_dropout_probability=0, - ) - velocity = ConditionalVelocityStage( - Field(), num_inference_steps=2, inference_condition_key="speed_condition" - ) - graph = Pipeline([speed, bridge, velocity]) - b = { - "condition": torch.zeros(2, 4), - "retiming_rate": torch.tensor([[0.25], [2.0]]), - "action_flow/clean_latent": torch.zeros(2, 2, 4), - "sampler/noise": torch.zeros(2, 2, 4), - } - for mode in ("train", "inference"): - _, excluded = graph.plan(b.keys(), mode) - assert all(missing == [""] for _, missing in excluded), excluded - result = graph.execute(b, mode=mode) - key = ( - "action_flow/predicted_velocity" - if mode == "train" - else "action_flow/generated_latent" - ) - assert not torch.equal(result[key][0], result[key][1]) diff --git a/tests/test_standard_dp_launch_args.py b/tests/test_standard_dp_launch_args.py deleted file mode 100644 index ea31518103..0000000000 --- a/tests/test_standard_dp_launch_args.py +++ /dev/null @@ -1,16 +0,0 @@ -"""The maintained DP helper cannot silently select a retired single-source recipe.""" - -import runpy -from pathlib import Path - -import pytest - - -@pytest.mark.parametrize("profile", ["", "chain_manual4919_retimed_v1"]) -def test_retired_standard_dp_default_rejected(monkeypatch, profile): - monkeypatch.delenv("DP_COTRAIN_STANDARD", raising=False) - monkeypatch.setenv("DP_SINGLE_SOURCE_STANDARD", profile) - root = Path(__file__).resolve().parents[1] - recipe = runpy.run_path(str(root / "scripts/train/standard_dp_launch.py"))["recipe"] - with pytest.raises(ValueError, match="retired"): - recipe() diff --git a/tests/test_sync_runtime_script.py b/tests/test_sync_runtime_script.py deleted file mode 100644 index 49b15a0ee0..0000000000 --- a/tests/test_sync_runtime_script.py +++ /dev/null @@ -1,29 +0,0 @@ -import subprocess -from pathlib import Path - -SCRIPT = ( - Path(__file__).parents[1] / "scripts" / "clusters" / "common" / "sync_runtime.sh" -) - - -def test_sync_runtime_is_valid_bash_and_rejects_relative_paths(): - subprocess.run(["bash", "-n", str(SCRIPT)], check=True) - result = subprocess.run( - [str(SCRIPT), "relative/source", "/tmp/runtime"], - check=False, - capture_output=True, - text=True, - ) - - assert result.returncode == 2 - assert "must be absolute paths" in result.stderr - - -def test_sync_runtime_uses_frozen_lock_and_verifies_torchdiffeq(): - text = SCRIPT.read_text() - - assert 'uv sync --frozen --project "$repo"' in text - assert 'version("torchdiffeq")' in text - assert 'item.startswith("torchdiffeq==")' in text - assert "/coc/" not in text - assert "/storage/" not in text diff --git a/tests/test_synthetic_action_adapter_flow.py b/tests/test_synthetic_action_adapter_flow.py deleted file mode 100644 index e415ea383a..0000000000 --- a/tests/test_synthetic_action_adapter_flow.py +++ /dev/null @@ -1,234 +0,0 @@ -import pytest -import torch - -from egomimic.synthetic.action_adapter_flow import SyntheticActionAdapterFlow - - -def _perturb_residual_outputs(model: SyntheticActionAdapterFlow) -> None: - with torch.no_grad(): - for adapter in (model.encoder, model.decoder): - adapter.residual[-1].weight.normal_(std=0.02) - adapter.residual[-1].bias.normal_(std=0.02) - - -def test_fixed_lift_reconstructs_and_projects_unit_noise_exactly(): - model = SyntheticActionAdapterFlow(latent_dim=8, adapter_family="fixed_affine") - action = torch.randn(11, 3) - torch.testing.assert_close(model.decoder(model.encoder(action)), action) - torch.testing.assert_close( - model.decoder.weight @ model.decoder.weight.T, torch.eye(3) - ) - torch.testing.assert_close(model.scale_loss(torch.randn(16, 8)), torch.tensor(0.0)) - assert not any(parameter.requires_grad for parameter in model.encoder.parameters()) - assert not any(parameter.requires_grad for parameter in model.decoder.parameters()) - - -def test_affine_path_identity_equals_reconstruction(): - model = SyntheticActionAdapterFlow(latent_dim=8, adapter_family="joint_affine") - with torch.no_grad(): - model.encoder.weight.add_(0.05 * torch.randn_like(model.encoder.weight)) - model.decoder.weight.add_(0.05 * torch.randn_like(model.decoder.weight)) - model.encoder.bias.add_(0.02 * torch.randn_like(model.encoder.bias)) - model.decoder.bias.add_(0.02 * torch.randn_like(model.decoder.bias)) - action = torch.randn(13, 3) - noise = torch.randn(13, 8) - time = torch.rand(13, 1) - torch.testing.assert_close( - model.path_consistency_loss(action, noise, time), - model.reconstruction_loss(action), - rtol=2e-5, - atol=2e-6, - ) - - -def test_nonlinear_adapters_start_at_the_same_fixed_lift(): - model = SyntheticActionAdapterFlow(latent_dim=8, adapter_family="nonlinear") - action = torch.randn(9, 3) - torch.testing.assert_close(model.decoder(model.encoder(action)), action) - - -def test_nonlinear_decoder_jvp_matches_centered_finite_difference(): - model = SyntheticActionAdapterFlow(latent_dim=8, adapter_family="nonlinear") - _perturb_residual_outputs(model) - state = torch.randn(7, 8) - tangent = torch.randn(7, 8) - epsilon = 1e-3 - finite_difference = ( - model.decoder(state + epsilon * tangent) - - model.decoder(state - epsilon * tangent) - ) / (2 * epsilon) - torch.testing.assert_close( - model.decoder_jvp(state, tangent), - finite_difference, - rtol=3e-3, - atol=3e-4, - ) - - -def test_flow_gradient_reaches_trainable_encoder_through_state_and_target(): - model = SyntheticActionAdapterFlow(latent_dim=8, adapter_family="joint_affine") - losses = model.losses( - torch.randn(12, 3), objective="reconstruction", flow_samples=3 - ) - losses["flow_loss"].backward() - assert all(parameter.grad is not None for parameter in model.encoder.parameters()) - assert ( - sum( - float(parameter.grad.abs().sum()) - for parameter in model.encoder.parameters() - ) - > 0 - ) - - -def test_loss_uses_the_supplied_fixed_noise_cloud_for_all_flow_samples(): - model = SyntheticActionAdapterFlow( - latent_dim=8, adapter_family="fixed_affine", field_width=16, field_depth=2 - ) - action = torch.randn(5, 3) - noise = torch.randn(5, 8) - time = torch.rand(15, 1) - losses = model.losses( - action, - objective="none", - flow_samples=3, - noise=noise, - time=time, - ) - clean = model.encoder(action) - clean_many = clean[:, None].expand(-1, 3, -1).reshape(-1, 8) - noise_many = noise[:, None].expand(-1, 3, -1).reshape(-1, 8) - state = (1.0 - time) * clean_many + time * noise_many - expected = (model.velocity(state, time) - (noise_many - clean_many)).square().mean() - torch.testing.assert_close(losses["flow_loss"], expected) - - -def test_clean_gradient_modes_keep_forward_loss_identical_and_change_encoder_route(): - model = SyntheticActionAdapterFlow( - latent_dim=8, adapter_family="nonlinear", field_width=16, field_depth=2 - ) - action, noise, time = torch.randn(12, 3), torch.randn(12, 8), torch.rand(36, 1) - losses = {} - gradients = {} - for mode in ("full", "target_stopgrad", "all_stopgrad"): - model.zero_grad(set_to_none=True) - losses[mode] = model.losses( - action, - objective="action_velocity", - flow_samples=3, - noise=noise, - time=time, - clean_gradient_mode=mode, - )["flow_loss"] - losses[mode].backward() - gradients[mode] = sum( - float(parameter.grad.abs().sum()) - for parameter in model.encoder.parameters() - if parameter.grad is not None - ) - torch.testing.assert_close(losses["full"], losses["target_stopgrad"]) - torch.testing.assert_close(losses["full"], losses["all_stopgrad"]) - assert gradients["full"] > 0 - assert gradients["target_stopgrad"] > 0 - assert gradients["all_stopgrad"] == 0 - - -def test_unknown_clean_gradient_mode_is_rejected(): - model = SyntheticActionAdapterFlow(latent_dim=8, adapter_family="nonlinear") - with pytest.raises(ValueError, match="unknown clean gradient mode"): - model.losses( - torch.randn(8, 3), objective="action_velocity", clean_gradient_mode="bad" - ) - - -def test_path_loss_gradients_reach_both_nonlinear_adapters(): - model = SyntheticActionAdapterFlow(latent_dim=8, adapter_family="nonlinear") - _perturb_residual_outputs(model) - losses = model.losses(torch.randn(10, 3), objective="path", flow_samples=2) - losses["path_loss"].backward() - for adapter in (model.encoder, model.decoder): - gradients = [ - parameter.grad - for parameter in adapter.parameters() - if parameter.requires_grad - ] - assert any( - gradient is not None and bool(gradient.abs().sum()) - for gradient in gradients - ) - - -def test_action_velocity_loss_matches_explicit_decoder_jacobian_product(): - model = SyntheticActionAdapterFlow(latent_dim=8, adapter_family="nonlinear") - _perturb_residual_outputs(model) - state = torch.randn(7, 8) - residual = torch.randn(7, 8) - - jacobians = torch.func.vmap(torch.func.jacrev(model.decoder))(state) - explicit_action_residual = torch.einsum("bai,bi->ba", jacobians, residual) - expected = explicit_action_residual.square().sum(dim=-1).mean() / 3.0 - - torch.testing.assert_close(model.action_velocity_loss(state, residual), expected) - - -def test_action_velocity_objective_reaches_encoder_decoder_and_field(): - model = SyntheticActionAdapterFlow( - latent_dim=8, - adapter_family="nonlinear", - field_width=16, - field_depth=2, - ) - _perturb_residual_outputs(model) - losses = model.losses( - torch.randn(10, 3), - objective="action_velocity", - flow_samples=2, - lambda_reconstruction=0.7, - lambda_scale=0.2, - lambda_action_velocity=0.4, - ) - losses["action_velocity_loss"].backward() - - for module in (model.encoder, model.decoder, model.field): - gradients = [ - parameter.grad - for parameter in module.parameters() - if parameter.requires_grad - ] - assert any( - gradient is not None and bool(gradient.abs().sum()) - for gradient in gradients - ) - - -def test_action_velocity_objective_combines_reconstruction_scale_and_metric_loss(): - model = SyntheticActionAdapterFlow(latent_dim=8, adapter_family="joint_affine") - losses = model.losses( - torch.randn(9, 3), - objective="action_velocity", - lambda_reconstruction=0.7, - lambda_scale=0.2, - lambda_action_velocity=0.4, - ) - expected = ( - losses["flow_loss"] - + 0.7 * losses["reconstruction_loss"] - + 0.2 * losses["scale_loss"] - + 0.4 * losses["action_velocity_loss"] - ) - torch.testing.assert_close(losses["loss"], expected) - - -def test_affine_path_objective_is_rejected_as_duplicate(): - model = SyntheticActionAdapterFlow(latent_dim=8, adapter_family="joint_affine") - with pytest.raises(ValueError, match="duplicates reconstruction"): - model.losses(torch.randn(8, 3), objective="path") - - -def test_reverse_time_trajectory_decodes_every_state(): - model = SyntheticActionAdapterFlow( - latent_dim=8, adapter_family="fixed_affine", field_width=16, field_depth=2 - ) - trajectory = model.trajectory(torch.randn(6, 8), steps=4) - assert trajectory.shape == (5, 6, 3) - assert torch.isfinite(trajectory).all() diff --git a/tests/test_synthetic_multi_action_adapter_flow.py b/tests/test_synthetic_multi_action_adapter_flow.py deleted file mode 100644 index 649c565748..0000000000 --- a/tests/test_synthetic_multi_action_adapter_flow.py +++ /dev/null @@ -1,125 +0,0 @@ -import json -import subprocess -import sys -from pathlib import Path - -import numpy as np -import torch - -from egomimic.synthetic.multi_action_adapter_flow import ( - SyntheticMultiActionAdapterFlow, -) - - -def test_private_adapters_and_shared_field_receive_expected_gradients(): - model = SyntheticMultiActionAdapterFlow( - embodiments=["shallow", "steep"], field_width=16, field_depth=2 - ) - losses = model.losses_for_embodiment("shallow", torch.randn(8, 3), flow_samples=2) - losses["loss"].backward() - assert any( - p.grad is not None and bool(p.grad.abs().sum()) - for p in model.field.parameters() - ) - assert any( - p.grad is not None and bool(p.grad.abs().sum()) - for p in model.encoders["shallow"].parameters() - ) - assert any( - p.grad is not None and bool(p.grad.abs().sum()) - for p in model.decoders["shallow"].parameters() - ) - assert all(p.grad is None for p in model.encoders["steep"].parameters()) - assert all(p.grad is None for p in model.decoders["steep"].parameters()) - - -def test_private_decoders_share_one_generated_latent_path(): - model = SyntheticMultiActionAdapterFlow( - embodiments=["shallow", "steep"], field_width=16, field_depth=2 - ) - noise = torch.randn(5, 8) - shallow = model.trajectory(noise, embodiment="shallow", steps=3) - steep = model.trajectory(noise, embodiment="steep", steps=3) - assert shallow.shape == steep.shape == (4, 5, 3) - torch.testing.assert_close(shallow, steep) - - -def test_paraboloid_surface_metric_is_zero_on_surface(): - from egomimic.eval.synthetic_trajectory_eval import SyntheticTrajectoryEval - - xy = torch.randn(20, 2) - curvature = 0.5 - points = torch.cat([xy, curvature * xy.square().sum(dim=-1, keepdim=True)], dim=-1) - torch.testing.assert_close( - SyntheticTrajectoryEval.paraboloid_surface_rmse(points, curvature=curvature), - torch.tensor(0.0), - ) - - -def test_multi_trainer_runs_optimizer_validation_and_immutable_checkpoint(tmp_path): - repository = Path(__file__).parents[1] - source = tmp_path / "source" - source.mkdir() - datasets = {} - evaluations = {} - rng = np.random.default_rng(5) - for name, curvature in (("shallow", 0.125), ("steep", 0.5)): - latent = rng.normal(size=(24, 8)).astype(np.float32) - xy = rng.normal(size=(24, 2)).astype(np.float32) - target = np.concatenate( - [xy, curvature * np.square(xy).sum(axis=1, keepdims=True)], axis=1 - ).astype(np.float32) - split = np.array([0] * 20 + [1] * 4, dtype=np.uint8) - path = source / f"{name}.npz" - np.savez_compressed( - path, - source_gaussian_latent=latent, - target_3d=target, - split=split, - ) - datasets[name] = str(path) - evaluations[name] = str(path) - output = tmp_path / "run" - config = { - "seed": 42, - "output_dir": str(output), - "datasets": datasets, - "evaluation_datasets": evaluations, - "curvatures": {"shallow": 0.125, "steep": 0.5}, - "source_key": "source_gaussian_latent", - "batch_size_per_embodiment": 4, - "flow_samples": 2, - "learning_rate": 3e-4, - "max_steps": 1, - "log_every": 1, - "checkpoint_every": 1, - "inference_steps": 2, - "evaluation_particles": 4, - "model": { - "embodiments": ["shallow", "steep"], - "latent_dim": 8, - "residual_width": 8, - "residual_depth": 1, - "field_width": 8, - "field_depth": 1, - }, - } - config_path = tmp_path / "config.json" - config_path.write_text(json.dumps(config)) - subprocess.run( - [ - sys.executable, - str(repository / "scripts/train/train_synthetic_multi_action_flow.py"), - "--config", - str(config_path), - ], - check=True, - ) - assert list( - (output / "checkpoints").glob("epoch-equivalent-*-global-step-000001.pt") - ) - summary = json.loads((output / "summary.json").read_text()) - assert set(summary["embodiments"]) == {"shallow", "steep"} - assert summary["shared_field_parameters"] > 0 - for name in ("shallow", "steep"): - assert (output / f"validation_trajectory_{name}.npz").is_file() diff --git a/tests/test_synthetic_shared_latent_flow.py b/tests/test_synthetic_shared_latent_flow.py deleted file mode 100644 index 7e51b47421..0000000000 --- a/tests/test_synthetic_shared_latent_flow.py +++ /dev/null @@ -1,55 +0,0 @@ -import torch - -from egomimic.synthetic.shared_latent_flow import ( - SyntheticDirectFlow, - SyntheticSharedLatentFlow, -) -from scripts.train.train_synthetic_manifold import energy_distance - - -def test_direct_flow_has_no_encoder_or_decoder_and_updates_field(): - model = SyntheticDirectFlow(data_dim=3, field_width=16, field_depth=2) - assert not hasattr(model, "encoder") - assert not hasattr(model, "decoder") - source = torch.randn(8, 3) - target = torch.randn(8, 3) - losses = model.losses(source, target, flow_samples=2) - losses["loss"].backward() - assert set(losses) == {"loss", "flow_loss"} - assert all(parameter.grad is not None for parameter in model.parameters()) - assert model.integrate(source, steps=2).shape == source.shape - - -def test_energy_distance_is_zero_for_identical_empirical_distributions(): - points = torch.randn(12, 3) - torch.testing.assert_close(energy_distance(points, points), torch.tensor(0.0)) - - -def test_both_objectives_are_finite_and_differentiable(): - for latent_dim in (2, 4): - for method in ("unite", "vfm"): - model = SyntheticSharedLatentFlow(latent_dim=latent_dim) - losses = model.losses( - torch.randn(8, latent_dim), torch.randn(8, 3), method=method - ) - assert all(torch.isfinite(value) for value in losses.values()) - losses["loss"].backward() - assert all(parameter.grad is not None for parameter in model.parameters()) - - -def test_vfm_reconstruction_can_freeze_only_field_parameters(): - model = SyntheticSharedLatentFlow(latent_dim=4) - losses = model.losses( - torch.randn(8, 4), - torch.randn(8, 3), - method="vfm", - reconstruction_updates_field=False, - ) - losses["reconstruction_loss"].backward(retain_graph=True) - assert all(parameter.grad is None for parameter in model.field.parameters()) - assert all(parameter.grad is not None for parameter in model.encoder.parameters()) - assert all(parameter.grad is not None for parameter in model.decoder.parameters()) - - model.zero_grad(set_to_none=True) - losses["flow_loss"].backward() - assert all(parameter.grad is not None for parameter in model.field.parameters()) diff --git a/tests/test_synthetic_trainer_resume.py b/tests/test_synthetic_trainer_resume.py deleted file mode 100644 index f5eeedb45d..0000000000 --- a/tests/test_synthetic_trainer_resume.py +++ /dev/null @@ -1,188 +0,0 @@ -import json -import subprocess -import sys -from pathlib import Path - -import numpy as np -import pytest -import torch - -from scripts.train.train_synthetic_manifold import ( - _capture_rng_state, - _restore_rng_state, -) - - -def _run(source: Path, config: Path, resume: Path | None = None) -> None: - command = [ - sys.executable, - str(source / "scripts/train/train_synthetic_manifold.py"), - "--config", - str(config), - ] - if resume is not None: - command.extend(("--resume", str(resume))) - subprocess.run(command, cwd=source, check=True) - - -def test_training_resume_preserves_checkpoints_and_appends_metrics(tmp_path): - source = Path(__file__).parents[1] - dataset = tmp_path / "dataset.npz" - rng = np.random.default_rng(7) - split = np.array([0] * 30 + [1] * 5 + [2] * 5, dtype=np.uint8) - np.savez_compressed( - dataset, - source_latent=rng.normal(size=(40, 2)).astype(np.float32), - target_3d=rng.normal(size=(40, 3)).astype(np.float32), - split=split, - ) - output = tmp_path / "run" - config_path = tmp_path / "config.json" - config = { - "method": "unite", - "seed": 42, - "dataset": str(dataset), - "source_key": "source_latent", - "output_dir": str(output), - "model": { - "latent_dim": 2, - "codec_width": 8, - "codec_depth": 1, - "field_width": 8, - "field_depth": 1, - }, - "flow_samples": 2, - "reconstruction_noise_range": [0.5, 1.0], - "learning_rate": 0.0003, - "batch_size": 4, - "max_steps": 2, - "inference_steps": 2, - "log_every": 1, - "checkpoint_every": 1, - } - config_path.write_text(json.dumps(config)) - _run(source, config_path) - checkpoint = next((output / "checkpoints").glob("*global-step-000002.pt")) - legacy_state = torch.load(checkpoint, map_location="cpu", weights_only=False) - legacy_state.pop("rng") - torch.save(legacy_state, checkpoint) - - config["max_steps"] = 4 - config_path.write_text(json.dumps(config)) - _run(source, config_path, checkpoint) - - steps = sorted( - int(path.stem.rsplit("-", 1)[-1]) - for path in (output / "checkpoints").glob("*.pt") - ) - assert steps == [1, 2, 3, 4] - metric_steps = [ - json.loads(row)["step"] - for row in (output / "metrics.jsonl").read_text().splitlines() - ] - assert metric_steps == [1, 2, 3, 4] - final = torch.load( - next((output / "checkpoints").glob("*global-step-000004.pt")), - map_location="cpu", - weights_only=False, - ) - assert final["step"] == 4 - assert final["resume"]["checkpoint"] == str(checkpoint) - assert final["resume"]["rng_restored"] is False - assert "rng" in final - - -def test_restore_rng_state_moves_saved_cuda_states_to_cpu(monkeypatch): - generator = torch.Generator().manual_seed(9) - state = _capture_rng_state(generator) - - class DeviceState: - def __init__(self): - self.cpu_called = False - - def cpu(self): - self.cpu_called = True - return torch.tensor([1, 2, 3], dtype=torch.uint8) - - device_state = DeviceState() - state["cuda"] = [device_state] - captured = [] - monkeypatch.setattr(torch.cuda, "is_available", lambda: True) - monkeypatch.setattr(torch.cuda, "set_rng_state_all", captured.append) - - _restore_rng_state(state, generator) - - assert device_state.cpu_called - assert captured[0][0].device.type == "cpu" - assert captured[0][0].dtype == torch.uint8 - - -@pytest.mark.parametrize( - ("adapter_family", "adapter_objective"), - ( - ("fixed_affine", "none"), - ("nonlinear", "path"), - ("nonlinear", "action_velocity"), - ), -) -def test_action_adapter_training_uses_50k_default_and_real_validation( - tmp_path, adapter_family, adapter_objective -): - source = Path(__file__).parents[1] - dataset = tmp_path / "dataset.npz" - rng = np.random.default_rng(17) - split = np.array([0] * 30 + [1] * 5 + [2] * 5, dtype=np.uint8) - np.savez_compressed( - dataset, - source_gaussian_latent=rng.normal(size=(40, 8)).astype(np.float32), - target_3d=rng.normal(size=(40, 3)).astype(np.float32), - split=split, - ) - output = tmp_path / f"action-adapter-{adapter_family}-{adapter_objective}" - config_path = tmp_path / f"action-adapter-{adapter_family}-{adapter_objective}.json" - config = { - "architecture": "action_adapter_flow", - "adapter_objective": adapter_objective, - "seed": 42, - "dataset": str(dataset), - "evaluation_dataset": str(dataset), - "evaluation_particles": 5, - "source_key": "source_gaussian_latent", - "output_dir": str(output), - "model": { - "latent_dim": 8, - "adapter_family": adapter_family, - "residual_width": 8, - "residual_depth": 1, - "field_width": 8, - "field_depth": 1, - }, - "flow_samples": 2, - "lambda_reconstruction": 0.0, - "lambda_scale": 0.0, - "lambda_path": 0.0, - "lambda_action_velocity": 1.0, - "learning_rate": 0.0003, - "batch_size": 4, - "max_steps": 2, - "inference_steps": 2, - "diagnostic_noise_samples": 16, - "angular_bins": 4, - "log_every": 1, - } - config_path.write_text(json.dumps(config)) - _run(source, config_path) - checkpoints = list((output / "checkpoints").glob("*.pt")) - assert len(checkpoints) == 1 - assert checkpoints[0].name.endswith("global-step-000002.pt") - summary = json.loads((output / "summary.json").read_text()) - for key in ( - "validation_generation_symmetric_nn_mse", - "validation_path_consistency_mse", - "validation_action_velocity_mse", - "validation_scale_loss", - "validation_torus_surface_rmse", - "validation_angular_histogram_l1", - "validation_decoder_jacobian_singular_max", - ): - assert np.isfinite(summary[key]) diff --git a/tests/test_synthetic_trainer_signals.py b/tests/test_synthetic_trainer_signals.py deleted file mode 100644 index 46cad3b9e7..0000000000 --- a/tests/test_synthetic_trainer_signals.py +++ /dev/null @@ -1,181 +0,0 @@ -"""Regression coverage for checkpoint requests during startup and saving.""" - -import json -import os -import signal -import subprocess -import sys -from pathlib import Path - -import numpy as np -import pytest -import torch - -from scripts.train.train_synthetic_manifold import ( - _atomic_torch_save, - _CheckpointRequests, -) - - -def test_request_received_during_save_stays_pending(): - requests = _CheckpointRequests() - requests.request(0, None) - snapshot = requests.received - requests.request(0, None) - requests.saved = snapshot - assert requests.received != requests.saved - - -def test_failed_atomic_save_preserves_checkpoint_and_removes_temporary( - tmp_path, monkeypatch -): - checkpoint = tmp_path / "checkpoint.pt" - torch.save({"step": 1}, checkpoint) - original = checkpoint.read_bytes() - - def fail_save(state, temporary): - Path(temporary).write_bytes(b"incomplete") - raise OSError("simulated interrupted write") - - monkeypatch.setattr(torch, "save", fail_save) - with pytest.raises(OSError, match="interrupted write"): - _atomic_torch_save({"step": 2}, checkpoint) - assert checkpoint.read_bytes() == original - assert list(tmp_path.iterdir()) == [checkpoint] - - -def _assert_equal(left, right): - if isinstance(left, torch.Tensor): - torch.testing.assert_close(left, right, rtol=0, atol=0) - elif isinstance(left, np.ndarray): - np.testing.assert_array_equal(left, right) - elif isinstance(left, dict): - assert left.keys() == right.keys() - for key in left: - _assert_equal(left[key], right[key]) - elif isinstance(left, (tuple, list)): - assert len(left) == len(right) - for lhs, rhs in zip(left, right, strict=True): - _assert_equal(lhs, rhs) - else: - assert left == right - - -@pytest.mark.skipif(not hasattr(signal, "SIGUSR2"), reason="requires POSIX signals") -def test_startup_signal_checkpoints_and_resumes_exactly(tmp_path): - source = Path(__file__).resolve().parents[1] - trainer = source / "scripts/train/train_synthetic_manifold.py" - dataset = tmp_path / "dataset.npz" - rng = np.random.default_rng(17) - np.savez_compressed( - dataset, - source_gaussian_latent=rng.normal(size=(40, 8)).astype(np.float32), - target_3d=rng.normal(size=(40, 3)).astype(np.float32), - split=np.array([0] * 30 + [1] * 5 + [2] * 5, dtype=np.uint8), - ) - config = { - "architecture": "action_adapter_flow", - "adapter_objective": "action_velocity", - "seed": 42, - "dataset": str(dataset), - "source_key": "source_gaussian_latent", - "output_dir": str(tmp_path / "interrupted"), - "model": { - "latent_dim": 8, - "adapter_family": "nonlinear", - "residual_width": 8, - "residual_depth": 1, - "field_width": 8, - "field_depth": 1, - }, - "flow_samples": 2, - "lambda_reconstruction": 100.0, - "learning_rate": 0.0003, - "batch_size": 4, - "max_steps": 4, - "inference_steps": 2, - "diagnostic_noise_samples": 16, - "angular_bins": 4, - "log_every": 1, - "checkpoint_every": 50_000, - } - config_path = tmp_path / "config.json" - config_path.write_text(json.dumps(config)) - # Inject a real OS signal precisely at the first heavyweight import. There - # is no sleep-based startup race and no production-only test hook. Exit only - # after the requested atomic checkpoint is published, simulating a restart. - bootstrap = """ -import builtins -import os -import runpy -import signal -import sys - -original_import = builtins.__import__ -sent = False -def signal_before_numpy(name, *args, **kwargs): - global sent - if name == 'numpy' and not sent: - sent = True - assert callable(signal.getsignal(signal.SIGUSR2)), 'handler installed too late' - os.kill(os.getpid(), signal.SIGUSR2) - return original_import(name, *args, **kwargs) -builtins.__import__ = signal_before_numpy -original_replace = os.replace -def interrupt_after_checkpoint(src, dst): - original_replace(src, dst) - if str(dst).endswith('.pt'): - os._exit(75) -os.replace = interrupt_after_checkpoint -sys.argv = sys.argv[1:] -runpy.run_path(sys.argv[0], run_name='__main__') -""" - env = {**os.environ, "CUDA_VISIBLE_DEVICES": "", "OMP_NUM_THREADS": "1"} - - def run(command, expected=0): - result = subprocess.run( - command, cwd=tmp_path, env=env, capture_output=True, text=True, timeout=60 - ) - assert result.returncode == expected, result.stdout + result.stderr - - run( - [sys.executable, "-c", bootstrap, str(trainer), "--config", str(config_path)], - expected=75, - ) - output = Path(config["output_dir"]) - checkpoints = list((output / "checkpoints").glob("*.pt")) - assert len(checkpoints) == 1 - checkpoint = checkpoints[0] - assert checkpoint.name.endswith("global-step-000001.pt") - checkpoint_bytes = checkpoint.read_bytes() - run( - [ - sys.executable, - str(trainer), - "--config", - str(config_path), - "--resume", - str(checkpoint), - ] - ) - assert checkpoint.read_bytes() == checkpoint_bytes - final_path = next((output / "checkpoints").glob("*global-step-000004.pt")) - resumed = torch.load(final_path, map_location="cpu", weights_only=False) - assert resumed["resume"]["rng_restored"] is True - assert [ - json.loads(row)["step"] - for row in (output / "metrics.jsonl").read_text().splitlines() - ] == [1, 2, 3, 4] - summary = json.loads((output / "summary.json").read_text()) - assert np.isfinite(summary["validation_generation_symmetric_nn_mse"]) - assert np.isfinite(summary["validation_action_velocity_mse"]) - - config["output_dir"] = str(tmp_path / "uninterrupted") - config_path.write_text(json.dumps(config)) - run([sys.executable, str(trainer), "--config", str(config_path)]) - control_path = next((tmp_path / "uninterrupted/checkpoints").glob("*.pt")) - control = torch.load(control_path, map_location="cpu", weights_only=False) - for key in ("model", "optimizer", "rng", "step"): - _assert_equal(resumed[key], control[key]) - control_summary = json.loads((tmp_path / "uninterrupted/summary.json").read_text()) - assert summary == control_summary diff --git a/tests/test_synthetic_trajectory_eval.py b/tests/test_synthetic_trajectory_eval.py deleted file mode 100644 index a26ccdb389..0000000000 --- a/tests/test_synthetic_trajectory_eval.py +++ /dev/null @@ -1,120 +0,0 @@ -import numpy as np -import pytest -import torch - -from egomimic.eval.synthetic_trajectory_eval import SyntheticTrajectoryEval -from egomimic.synthetic.action_adapter_flow import SyntheticActionAdapterFlow -from egomimic.synthetic.multi_action_adapter_flow import ( - SyntheticMultiActionAdapterFlow, -) -from egomimic.synthetic.shared_latent_flow import ( - SyntheticDirectFlow, - SyntheticSharedLatentFlow, -) -from scripts.eval.export_synthetic_trajectory_npz import ( - _EmbodimentTrajectoryView, - resolve_device, -) - - -def test_shared_eval_exports_same_npz_contract_for_both_model_families(tmp_path): - target = torch.randn(7, 3) - models_and_sources = ( - (SyntheticSharedLatentFlow(latent_dim=2), torch.randn(7, 2)), - (SyntheticDirectFlow(data_dim=3), torch.randn(7, 3)), - ( - SyntheticActionAdapterFlow(latent_dim=8, adapter_family="fixed_affine"), - torch.randn(7, 8), - ), - ) - for index, (model, source) in enumerate(models_and_sources): - output = tmp_path / f"trajectory-{index}.npz" - points = SyntheticTrajectoryEval.export(model, source, target, output, steps=3) - archive = np.load(output, allow_pickle=False) - assert set(archive.files) == {"times", "points", "target"} - assert points.shape == (4, 7, 3) - assert archive["points"].shape == (4, 7, 3) - np.testing.assert_array_equal(archive["target"], target.numpy()) - - -def test_ground_truth_uses_same_npz_contract(tmp_path): - source = torch.randn(5, 2) - target = torch.randn(5, 3) - output = tmp_path / "ground-truth.npz" - points = SyntheticTrajectoryEval.export_linear_ground_truth( - source, target, output, steps=2 - ) - torch.testing.assert_close(points[0, :, :2], source) - torch.testing.assert_close(points[0, :, 2], torch.zeros(5)) - torch.testing.assert_close(points[-1], target) - - -def test_validation_loader_requires_exact_requested_particle_count(tmp_path): - dataset = tmp_path / "small.npz" - np.savez_compressed( - dataset, - source_latent=np.zeros((5, 2), dtype=np.float32), - target_3d=np.zeros((5, 3), dtype=np.float32), - split=np.array([0, 1, 1, 2, 2], dtype=np.uint8), - ) - source, target = SyntheticTrajectoryEval.load_validation_data( - dataset, "source_latent", 2 - ) - assert source.shape == (2, 2) - assert target.shape == (2, 3) - with pytest.raises(ValueError, match="requested 3 validation particles"): - SyntheticTrajectoryEval.load_validation_data(dataset, "source_latent", 3) - - -def test_symmetric_nearest_neighbor_mse_checks_both_directions(): - samples = torch.tensor([[0.0, 0.0], [2.0, 0.0]]) - targets = torch.tensor([[0.0, 0.0], [4.0, 0.0]]) - loss = SyntheticTrajectoryEval.symmetric_nearest_neighbor_mse(samples, targets) - torch.testing.assert_close(loss, torch.tensor(2.0)) - - collapsed = torch.zeros(2, 2) - collapsed_loss = SyntheticTrajectoryEval.symmetric_nearest_neighbor_mse( - collapsed, targets - ) - assert collapsed_loss > 0 - - -def test_torus_metrics_are_exact_on_uniform_surface_samples(): - theta = torch.linspace(0, 2 * torch.pi, 32)[:-1] - phi = torch.linspace(0, 2 * torch.pi, 32)[:-1] - theta, phi = torch.meshgrid(theta, phi, indexing="ij") - tube = 2.0 + 0.65 * phi.cos() - points = torch.stack( - (tube * theta.cos(), tube * theta.sin(), 0.65 * phi.sin()), dim=-1 - ).reshape(-1, 3) - torch.testing.assert_close( - SyntheticTrajectoryEval.torus_surface_rmse( - points, major_radius=2.0, minor_radius=0.65 - ), - torch.tensor(0.0), - atol=2e-7, - rtol=0, - ) - coverage = SyntheticTrajectoryEval.torus_angular_coverage(points, points, bins=8) - torch.testing.assert_close(coverage["angular_histogram_l1"], torch.tensor(0.0)) - torch.testing.assert_close(coverage["angular_support_recall"], torch.tensor(1.0)) - - -def test_trajectory_export_device_resolution_rejects_missing_cuda(monkeypatch): - assert resolve_device("cpu") == torch.device("cpu") - monkeypatch.setattr(torch.cuda, "is_available", lambda: False) - assert resolve_device("auto") == torch.device("cpu") - with pytest.raises(RuntimeError, match="CUDA was requested"): - resolve_device("cuda") - - -def test_multi_action_trajectory_view_selects_private_decoder(): - model = SyntheticMultiActionAdapterFlow( - embodiments=["shallow", "steep"], field_width=8, field_depth=1 - ) - source = torch.randn(5, 8) - view = _EmbodimentTrajectoryView(model, "steep") - torch.testing.assert_close( - view.trajectory(source, steps=2), - model.trajectory(source, embodiment="steep", steps=2), - ) diff --git a/tests/test_token_diagnostics.py b/tests/test_token_diagnostics.py deleted file mode 100644 index 85817625e4..0000000000 --- a/tests/test_token_diagnostics.py +++ /dev/null @@ -1,297 +0,0 @@ -"""Provider-scoped attention capture, source identities and reusable archives.""" - -from functools import wraps -from types import SimpleNamespace - -import numpy as np -import pytest -import torch - -from egomimic.eval.eval import EvaluationDataRequirements -from egomimic.eval.latent_archive import ( - read_archive, - rebuild_archives, - reduce_features, - write_archive, -) -from egomimic.eval.token_diagnostics import TokenDiagnosticsEval -from egomimic.models.pi05.diagnostics import PI05AttentionProvider, capture_attention - - -def attention_layers(): - layer = torch.nn.Module() - layer.self_attn = torch.nn.Module() - layer.self_attn.k_proj = torch.nn.Linear(4, 4) - return torch.nn.ModuleList([layer]) - - -class AttentionBackend(torch.nn.Module): - def __init__(self): - super().__init__() - self.paligemma_with_expert = torch.nn.Module() - self.paligemma_with_expert.paligemma = torch.nn.Module() - self.paligemma_with_expert.paligemma.language_model = torch.nn.Module() - self.paligemma_with_expert.paligemma.language_model.layers = attention_layers() - self.paligemma_with_expert.gemma_expert = torch.nn.Module() - self.paligemma_with_expert.gemma_expert.model = torch.nn.Module() - self.paligemma_with_expert.gemma_expert.model.layers = attention_layers() - - def embed_prefix(self, images, image_masks, language_tokens, language_masks): - embeddings = torch.ones(len(language_tokens), 5, 4) - padding = torch.cat( - [torch.ones(len(language_tokens), 2, dtype=torch.bool), language_masks], 1 - ) - return embeddings, padding, torch.zeros_like(padding) - - def sample(self): - prefix, _, _ = self.embed_prefix( - None, - None, - torch.ones(2, 3), - torch.tensor([[True, False, False], [True, True, False]]), - ) - language = self.paligemma_with_expert.paligemma.language_model.layers[ - 0 - ].self_attn.k_proj(prefix) - expert = self.paligemma_with_expert.gemma_expert.model.layers[ - 0 - ].self_attn.k_proj - first = expert(torch.ones(2, 2, 4)) - expert(torch.ones(2, 2, 4) * 5) - return language, first - - -def test_capture_slices_first_step_masks_and_cleanup(): - backend = AttentionBackend() - with capture_attention(backend, mask_padding=True) as output: - language, first = backend.sample() - torch.testing.assert_close(output["expert_layer_00"]["tokens"], first) - torch.testing.assert_close( - output["paligemma_layer_00_img"]["tokens"], language[:, :2] - ) - assert output["paligemma_layer_00_lang"]["mask"].sum() == 3 - assert "embed_prefix" not in backend.__dict__ - for module in backend.modules(): - assert not module._forward_hooks - with pytest.raises(RuntimeError, match="sample failure"): - with capture_attention(backend): - raise RuntimeError("sample failure") - assert "embed_prefix" not in backend.__dict__ - assert all(not module._forward_hooks for module in backend.modules()) - - -def test_provider_uses_declared_id_and_normal_graph_forward(): - backend = AttentionBackend() - seen = [] - stage = SimpleNamespace(backend=True, policy_nets={"policy": backend}) - - def lookup(name): - assert name == "explicit-policy-id" - return stage - - def forward(batch): - seen.append(next(iter(batch))) - backend.sample() - return {source: {"pred_action": torch.ones(2, 4, 3)} for source in batch} - - graph = SimpleNamespace( - pipeline=SimpleNamespace(stage_by_id=lookup), - forward_eval=forward, - process_batch_for_training=lambda batch: batch, - ) - provider = PI05AttentionProvider(stage_id="explicit-policy-id") - result = provider.run( - graph, {"source_a": {}, "source_b": {}}, already_processed=False - ) - assert seen == ["source_a", "source_b"] - assert result["source_b"]["predictions"]["pred_action"].shape == (2, 4, 3) - assert len(result["source_a"]["activations"]) == 4 - - -def test_capture_bypasses_compiled_sampler_and_restores_it(): - backend = AttentionBackend() - - @wraps(backend.sample) - def compiled(): - raise AssertionError("Capture must use the original eager sampler") - - backend.sample_actions = compiled - with capture_attention(backend) as output: - backend.sample_actions() - assert output and backend.sample_actions is compiled - with pytest.raises(RuntimeError, match="inference failed"): - with capture_attention(backend): - raise RuntimeError("inference failed") - assert backend.sample_actions is compiled - - -def test_prediction_requirements_are_preserved_and_conflicts_rejected(): - prediction = SimpleNamespace( - data_requirements=lambda: EvaluationDataRequirements( - ordered=True, - complete_episodes=True, - source_fps=20, - max_episodes=2, - sample_id_key="episode_hash", - frame_index_key="frame_index", - ), - trainer_overrides=lambda: {"inference_mode": False}, - ) - evaluator = TokenDiagnosticsEval( - prediction_evaluator=prediction, limit_val_batches=1.0 - ) - assert evaluator.data_requirements() == prediction.data_requirements() - assert evaluator.trainer_overrides() == { - "inference_mode": False, - "limit_val_batches": 1.0, - "num_sanity_val_steps": 0, - } - evaluator.sample_id_key = "different_episode_id" - with pytest.raises(ValueError, match="sample_id_key"): - evaluator.data_requirements() - - -def test_diagnostics_score_the_captured_prediction_through_shared_metrics(tmp_path): - from egomimic.eval.bimanual_cartesian_eval import BimanualCartesianEval - - prediction_evaluator = BimanualCartesianEval( - viz_every_n_epochs=0, pose_metrics=False - ) - evaluator = TokenDiagnosticsEval( - prediction_evaluator=prediction_evaluator, methods=["pca"], save_plots=False - ) - logged, calls = [], [] - evaluator.trainer = SimpleNamespace( - default_root_dir=str(tmp_path), - current_epoch=0, - global_step=1, - global_rank=0, - is_global_zero=True, - world_size=1, - lightning_module=SimpleNamespace( - log_dict=lambda values, **kw: logged.append(values) - ), - ) - evaluator.bind_data_context( - normalizer=SimpleNamespace( - unnormalize=lambda values, identity: {k: v * 2 for k, v in values.items()} - ) - ) - batch = { - "opaque": { - "embodiment": torch.tensor([7]), - "episode_hash": ["episode"], - "frame_index": torch.tensor([0]), - "actions_cartesian": torch.zeros(1, 4, 14), - } - } - - def run(capability, values): - calls.append(capability) - return { - "opaque": { - "predictions": {"pred_action": torch.ones(1, 4, 14)}, - "activations": { - "layer": { - "tokens": torch.eye(4)[None], - "mask": torch.ones(1, 4, dtype=torch.bool), - } - }, - } - } - - evaluator.model = SimpleNamespace(run_diagnostic=run) - evaluator.on_validation_start() - metrics = evaluator.on_validation_step(batch, 0) - evaluator.on_validation_end() - assert calls == ["token_activations"] - assert metrics["Valid/MSE"] == 1 - assert metrics["Valid/Native_MSE"] == 4 - assert len(logged) == 1 - - -def test_archives_preserve_frames_balance_sources_and_rebuild(tmp_path): - evaluator = TokenDiagnosticsEval( - methods=["pca"], max_tokens_per_source_layer=6, save_plots=True - ) - evaluator.trainer = SimpleNamespace( - default_root_dir=str(tmp_path), current_epoch=0, global_step=10, global_rank=1 - ) - batch = { - name: { - "episode_hash": [f"{name}-episode"] * 2, - "frame_index": torch.tensor([4, 17]), - } - for name in ["A", "B"] - } - - def diagnostics(capability, values): - assert capability == "token_activations" - return { - name: { - "activations": { - "layer_00": { - "tokens": torch.arange(32).reshape(2, 4, 4).float(), - "mask": torch.ones(2, 4, dtype=torch.bool), - } - } - } - for name in values - } - - evaluator.model = SimpleNamespace(run_diagnostic=diagnostics) - evaluator.on_validation_start() - evaluator.on_validation_step(batch, 0) - evaluator.on_validation_step( - batch, 1 - ) # repeated distributed/combined-loader samples - evaluator.on_validation_end() - path = next(evaluator.output_dir.rglob("*.csv")) - keys, rows = read_archive(path) - assert keys.shape == (12, 4) - assert {row["frame_idx"] for row in rows} == {"4", "17"} - assert sum(row["embodiment"] == "A" for row in rows) == 6 - assert sum(row["embodiment"] == "B" for row in rows) == 6 - assert ( - len( - { - tuple(row[key] for key in ("video_hash", "frame_idx", "token_idx")) - for row in rows - } - ) - == 12 - ) - rebuilt = tmp_path / "rebuild" - rebuild_archives(path.parent, rebuilt, methods=["pca"], seed=0) - restored, metadata = read_archive(rebuilt / path.name) - np.testing.assert_array_equal(restored, keys) - assert metadata == rows - assert list(rebuilt.glob("*.png")) - with pytest.raises(FileExistsError): - rebuild_archives(path.parent, rebuilt, methods=["pca"]) - with pytest.raises(ValueError, match="differ"): - rebuild_archives(path.parent, path.parent, methods=["pca"]) - - -def test_all_reduction_methods_are_finite_and_reproducible(): - pytest.importorskip("umap") - features = np.random.default_rng(42).normal(size=(16, 8)).astype(np.float32) - methods = ["pca", "umap", "pca_umap", "tsne2d", "tsne3d"] - result, receipt = reduce_features(features, methods=methods, pca_components=5) - assert set(result) == set(methods) - assert len(receipt["pca_explained_variance"]) == 5 - for name, coordinates in result.items(): - assert coordinates.shape == (16, 2 if name == "tsne2d" else 3) - assert np.isfinite(coordinates).all() - - -def test_old_inline_csv_rebuild_and_bad_row_count(tmp_path): - source = tmp_path / "old.csv" - source.write_text( - "video_hash,embodiment,frame_idx,token_idx,k0,k1\nhash,A,7,0,1.0,2.0\n" - ) - keys, rows = read_archive(source) - np.testing.assert_array_equal(keys, [[1, 2]]) - assert rows[0]["frame_idx"] == "7" - with pytest.raises(ValueError, match="row counts"): - write_archive(tmp_path / "bad", "layer", keys, [], reductions={}, receipt={}) diff --git a/tests/test_unite_diagnostics.py b/tests/test_unite_diagnostics.py deleted file mode 100644 index bcdbc08880..0000000000 --- a/tests/test_unite_diagnostics.py +++ /dev/null @@ -1,117 +0,0 @@ -import json - -import numpy as np -import pytest -import torch - -from egomimic.eval.diagnostics.gradients import gradient_cosine_similarity -from egomimic.eval.diagnostics.latent_projection import project_pca -from egomimic.eval.diagnostics.trajectory import ( - chunk_seam_metrics, - validate_chunk_seam_artifact, - write_chunk_seam_artifact, -) -from egomimic.eval.diagnostics.unite_artifact import ( - UniteDiagnosticArtifactWriter, - validate_unite_artifact, -) - - -def test_gradient_cosine_detects_exact_conflict_without_populating_grad(): - parameter = torch.nn.Parameter(torch.tensor([1.0, -2.0])) - first = parameter.sum() - second = -parameter.sum() - - cosine = gradient_cosine_similarity(first, second, [parameter]) - - assert cosine.item() == pytest.approx(-1.0) - assert parameter.grad is None - - -def test_chunk_seam_metrics_use_circular_rotation_and_derivatives(): - old = np.asarray( - [ - [0.0, 0.0, 3.10, 0.0], - [1.0, 0.0, 3.11, 0.5], - [2.0, 0.0, 3.12, 1.0], - [3.0, 0.0, 3.13, 1.0], - ], - dtype=np.float32, - ) - new = old.copy() - new[:, 2] = -3.10 - metrics = chunk_seam_metrics(old, new) - - assert metrics["aligned_steps"] == 4 - assert metrics["position_mean_l2"] == pytest.approx(0.0) - assert metrics["rotation_mean_abs_radians"] < 0.1 - assert metrics["velocity_rms_difference"] == pytest.approx(0.0) - assert metrics["jerk_rms_difference"] == pytest.approx(0.0) - - -def test_chunk_seam_artifact_is_immutable_and_hash_validated(tmp_path): - event = { - "t": 8, - "embodiment_id": 19, - "episode_index": 0, - "previous_tail": np.zeros((8, 4), dtype=np.float32), - "new_head": np.ones((8, 4), dtype=np.float32), - "executed_head": np.ones((8, 4), dtype=np.float32), - } - artifact = write_chunk_seam_artifact([event], tmp_path / "seams") - - manifest = validate_chunk_seam_artifact(artifact) - - assert manifest["event_count"] == 1 - with pytest.raises(FileExistsError): - write_chunk_seam_artifact([event], artifact) - - -def test_shared_latent_pca_has_stable_shape(): - features = np.arange(48, dtype=np.float32).reshape(8, 6) - - projected, ratio = project_pca(features, 3) - - assert projected.shape == (8, 3) - assert ratio.shape == (3,) - assert np.all(np.isfinite(projected)) - - -def test_unite_artifact_round_trip(tmp_path): - request = tmp_path / "request.json" - request.write_text(json.dumps({"schema_version": 1, "checkpoint_sha256": "abc"})) - writer = UniteDiagnosticArtifactWriter( - str(tmp_path / "unite"), request_path=str(request) - ) - batch, horizon, action_dim, tokens, latent_dim = 2, 4, 3, 4, 8 - steps, samples = 3, 5 - writer.append( - embodiment_id=17, - embodiment_name="pushshapes_sim_u_socket", - action_key="actions", - target_action=np.zeros((batch, horizon, action_dim), dtype=np.float32), - reconstructed_action=np.zeros((batch, horizon, action_dim), dtype=np.float32), - generated_action=np.zeros((batch, horizon, action_dim), dtype=np.float32), - clean_latent=np.zeros((batch, tokens, latent_dim), dtype=np.float32), - generated_latent=np.zeros((batch, tokens, latent_dim), dtype=np.float32), - denoising_latents=np.zeros( - (batch, steps, tokens, latent_dim), dtype=np.float32 - ), - denoising_actions=np.zeros( - (batch, steps, horizon, action_dim), dtype=np.float32 - ), - diversity_latents=np.zeros( - (batch, samples, tokens, latent_dim), dtype=np.float32 - ), - diversity_actions=np.zeros( - (batch, samples, horizon, action_dim), dtype=np.float32 - ), - episode_hashes=["episode-a", "episode-b"], - ) - artifact = writer.finalize() - - manifest = validate_unite_artifact(artifact, request) - - assert manifest["sample_count"] == 2 - assert manifest["parts"][0]["denoising_steps"] == steps - assert manifest["parts"][0]["diversity_samples"] == samples diff --git a/tests/test_unite_fixed_step_requeue_boundary.py b/tests/test_unite_fixed_step_requeue_boundary.py deleted file mode 100644 index 7c50632e2d..0000000000 --- a/tests/test_unite_fixed_step_requeue_boundary.py +++ /dev/null @@ -1,121 +0,0 @@ -import json -from pathlib import Path -from types import SimpleNamespace - -import pytest - -from egomimic.utils.slurm_requeue import SaveOnlySignalCheckpoint -from scripts.ice.unite.fixed_step_requeue_boundary import FixedStepRequeueBoundary - - -class _Strategy: - world_size = 2 - - def __init__(self): - self.barriers = [] - - def barrier(self, name): - self.barriers.append(name) - - @staticmethod - def broadcast(value, src=0): - return value - - -def _callback(tmp_path: Path, scancel: Path) -> FixedStepRequeueBoundary: - return FixedStepRequeueBoundary( - initial_global_step=3, - trigger_global_step=8, - slurm_job_id="1234", - scancel=str(scancel), - record_path=str(tmp_path / "boundary.json"), - signal_ack_timeout_seconds=0.1, - requeue_wait_timeout_seconds=0.01, - ) - - -def test_boundary_requires_exactly_five_steps(tmp_path): - executable = tmp_path / "scancel" - executable.write_text("#!/bin/sh\n", encoding="utf-8") - executable.chmod(0o700) - with pytest.raises(ValueError, match="exactly five"): - FixedStepRequeueBoundary( - initial_global_step=3, - trigger_global_step=9, - slurm_job_id="1234", - scancel=str(executable), - record_path=str(tmp_path / "boundary.json"), - ) - - -def test_step_eight_requests_boundary_and_records_all_rank_ack(monkeypatch, tmp_path): - executable = tmp_path / "scancel" - executable.write_text("#!/bin/sh\n", encoding="utf-8") - executable.chmod(0o700) - callback = _callback(tmp_path, executable) - save_callback = SaveOnlySignalCheckpoint(tmp_path / "checkpoints") - strategy = _Strategy() - trainer = SimpleNamespace( - callbacks=[callback, save_callback], - global_step=8, - is_global_zero=True, - strategy=strategy, - ) - callback.setup(trainer, SimpleNamespace(), "fit") - monkeypatch.setenv("ICE_REQUEUE_OWNER", "runner") - monkeypatch.setenv("ICE_CHILD_REQUEUE_DISABLED", "1") - monkeypatch.setenv("SLURM_JOB_ID", "1234") - monkeypatch.delenv("SLURM_RESTART_COUNT", raising=False) - - def request(*_args, **_kwargs): - save_callback._request_generation += 1 - return SimpleNamespace(returncode=0, stdout="", stderr="") - - monkeypatch.setattr("subprocess.run", request) - callback.on_train_batch_end(trainer, SimpleNamespace(), None, None, 0) - - proof = json.loads((tmp_path / "boundary.json").read_text(encoding="utf-8")) - assert proof["observed_global_step"] == 8 - assert proof["optimizer_steps_advanced"] == 5 - assert proof["step_after_signal_executed"] is False - assert proof["save_acknowledged_ranks"] == [0, 1] - assert proof["world_size"] == 2 - assert strategy.barriers == [ - "unite-fixed-step-boundary-ready", - "unite-fixed-step-boundary-signal-acknowledged", - ] - - -def test_boundary_rejects_wrong_owner_before_signalling(monkeypatch, tmp_path): - executable = tmp_path / "scancel" - executable.write_text("#!/bin/sh\n", encoding="utf-8") - executable.chmod(0o700) - callback = _callback(tmp_path, executable) - save_callback = SaveOnlySignalCheckpoint(tmp_path / "checkpoints") - trainer = SimpleNamespace( - callbacks=[callback, save_callback], - global_step=8, - is_global_zero=True, - strategy=_Strategy(), - ) - callback.setup(trainer, SimpleNamespace(), "fit") - monkeypatch.setenv("ICE_REQUEUE_OWNER", "child") - monkeypatch.setenv("ICE_CHILD_REQUEUE_DISABLED", "1") - monkeypatch.setenv("SLURM_JOB_ID", "1234") - - with pytest.raises(RuntimeError, match="sole requeue owner"): - callback.on_train_batch_end(trainer, SimpleNamespace(), None, None, 0) - assert not (tmp_path / "boundary.json").exists() - - -def test_step_after_boundary_is_blocked(monkeypatch, tmp_path): - executable = tmp_path / "scancel" - executable.write_text("#!/bin/sh\n", encoding="utf-8") - executable.chmod(0o700) - callback = _callback(tmp_path, executable) - callback._sent = True - monotonic_values = iter((0.0, 0.0, 0.02)) - monkeypatch.setattr("time.monotonic", lambda: next(monotonic_values)) - monkeypatch.setattr("time.sleep", lambda _seconds: None) - with pytest.raises(RuntimeError, match="did not terminate"): - callback.on_train_batch_start(SimpleNamespace(), SimpleNamespace(), None, 0) diff --git a/tests/test_unite_ice_contract.py b/tests/test_unite_ice_contract.py deleted file mode 100644 index 52d7e0737d..0000000000 --- a/tests/test_unite_ice_contract.py +++ /dev/null @@ -1,106 +0,0 @@ -from __future__ import annotations - -from pathlib import Path - -import pytest -from hydra import compose, initialize_config_dir -from omegaconf import OmegaConf - -from scripts.ice.unite.unite_contract import validate_unite_config - - -@pytest.fixture -def resolved_config(): - config_dir = Path("egomimic/hydra_configs").resolve() - with initialize_config_dir(config_dir=str(config_dir), version_base="1.3"): - return compose( - config_name="train_zarr_cartesian", - overrides=[ - "model=bf/us_unite_register_shared_nt4_s42", - "+experiment=sim/pusht/unite_usocket_register_sweep_val01_h16", - "trainer.devices=2", - "++paths.root_dir=.", - ], - ) - - -@pytest.mark.parametrize( - ("shared", "tokens"), - [(True, 4), (True, 8), (False, 4), (False, 8)], -) -def test_clean_rows_bind_energy_view_to_world_size(resolved_config, shared, tokens): - config = OmegaConf.create(OmegaConf.to_container(resolved_config, resolve=False)) - config.model.share_encoder_denoiser = shared - config.model.num_latent_tokens = tokens - result = validate_unite_config(config, expected_world_size=2) - assert result["status"] == "READY" - assert result["row"] == { - "share_encoder_denoiser": shared, - "num_latent_tokens": tokens, - "latent_dim": 16, - } - assert result["energy_score_conditions"] == 32 - assert result["diagnostics"] == "ABSENT" - - -def test_single_gpu_view_is_supported_when_total_is_32(resolved_config): - config = OmegaConf.create(OmegaConf.to_container(resolved_config, resolve=False)) - config.trainer.devices = 1 - config.evaluator.energy_score_validation_view.world_size = 1 - config.evaluator.energy_score_validation_view.per_rank_batch_size = 32 - assert validate_unite_config(config, expected_world_size=1)["world_size"] == 1 - - -def test_energy_view_rejects_world_size_or_total_mismatch(resolved_config): - config = OmegaConf.create(OmegaConf.to_container(resolved_config, resolve=False)) - config.evaluator.energy_score_validation_view.per_rank_batch_size = 8 - with pytest.raises(ValueError, match="exactly 32 conditions"): - validate_unite_config(config, expected_world_size=2) - - -def test_optional_diagnostics_use_the_same_world_size_contract(resolved_config): - config = OmegaConf.create(OmegaConf.to_container(resolved_config, resolve=False)) - config.evaluator.unite_diagnostics = { - "validation_view": {"world_size": 2, "per_rank_batch_size": 16} - } - assert ( - validate_unite_config(config, expected_world_size=2)["diagnostics"] - == "BOUND_TO_WORLD_SIZE" - ) - config.evaluator.unite_diagnostics.validation_view.per_rank_batch_size = 15 - with pytest.raises(ValueError, match="diagnostic validation view"): - validate_unite_config(config, expected_world_size=2) - - -def test_contract_rejects_scientific_drift(resolved_config): - config = OmegaConf.create(OmegaConf.to_container(resolved_config, resolve=False)) - policy = next( - stage - for stage in config.model.pipeline.stages - if str(stage.get("_target_", "")).endswith("ReleasedRecipeUniteLatentPolicy") - ) - policy.flow_steps_per_reconstruction = 13 - with pytest.raises(ValueError, match="unsupported UNITE"): - validate_unite_config(config, expected_world_size=2) - - -def test_n8_flow42_ablation_is_explicit_and_contract_valid(): - config_dir = Path("egomimic/hydra_configs").resolve() - with initialize_config_dir(config_dir=str(config_dir), version_base="1.3"): - config = compose( - config_name="train_zarr_cartesian", - overrides=[ - "model=bf/us_unite_register_shared_nt8_flow42_s42", - "+experiment=sim/pusht/unite_usocket_register_sweep_val01_h16", - "trainer.devices=1", - "evaluator.energy_score_validation_view.per_rank_batch_size=32", - "++paths.root_dir=.", - ], - ) - result = validate_unite_config(config, expected_world_size=1) - assert result["row"]["num_latent_tokens"] == 8 - assert result["flow_steps_per_reconstruction"] == 42 - assert ( - config.run_provenance.unite_update_schedule.flow_samples_per_reconstruction - == 42 - ) diff --git a/tests/test_validate_action_flow_config.py b/tests/test_validate_action_flow_config.py deleted file mode 100644 index b532df776b..0000000000 --- a/tests/test_validate_action_flow_config.py +++ /dev/null @@ -1,421 +0,0 @@ -from __future__ import annotations - -import copy -import hashlib -import importlib.util -import json -import subprocess -import sys -from pathlib import Path - -import pytest - -ROOT = Path(__file__).resolve().parents[1] -CONFIG_ROOT = ROOT / "egomimic" / "hydra_configs" -TOOL = ROOT / "tools" / "validate_action_flow_config.py" -SPEC = importlib.util.spec_from_file_location("action_flow_preflight", TOOL) -assert SPEC is not None and SPEC.loader is not None -preflight = importlib.util.module_from_spec(SPEC) -SPEC.loader.exec_module(preflight) - - -def test_real_action_flow_config_instantiates_and_passes_full_preflight(): - report, _ = preflight.validate_experiment( - "sim/pusht/action_flow_bc_usocket_recon1_s42", - config_root=CONFIG_ROOT, - ) - - assert report["status"] == "PASS" - assert report["config_name"] == "action_flow_bc_usocket_recon1_s42" - assert len(report["resolved_config_sha256"]) == 64 - assert report["dimensions"] == { - "action": [16, 4], - "condition": 67, - "image_feature": 64, - "latent": [16, 8], - "normalized_state": 3, - } - expected_counts = { - "decoder_g": 10_744, - "encoder_e": 10_748, - "field_v": 39_506_641, - "observation_encoder": 11_197_088, - "pipeline_total": 50_725_221, - } - assert set(report["parameters"]) == set(expected_counts) - for name, expected in expected_counts.items(): - manifest = report["parameters"][name] - assert manifest["total"] == manifest["trainable"] == expected - assert len(manifest["manifest_sha256"]) == 64 - assert sum(entry["numel"] for entry in manifest["entries"]) == expected - assert len({entry["name"] for entry in manifest["entries"]}) == len( - manifest["entries"] - ) - assert report["topology"]["shared_field_instance"] is True - assert report["topology"]["shared_decoder_instance"] is True - assert report["topology"]["inference_order"] == [ - "FusedObsEncoder", - "GaussianLatentNoise", - "ConditionalVelocityStage", - "ContentDecoderStage", - ] - assert report["launch"]["effective_global_batch"] == 32 - assert report["split"]["zero_id_overlap"] is True - assert report["split"]["zero_resolved_path_overlap"] is True - content_manifest_path = ROOT / preflight.CANONICAL_CONTENT_MANIFEST_RELATIVE_PATH - content_manifest = json.loads(content_manifest_path.read_text()) - assert report["split"]["content_manifest_path"] == ( - preflight.CANONICAL_CONTENT_MANIFEST_RELATIVE_PATH.as_posix() - ) - assert ( - report["split"]["content_manifest_sha256"] - == hashlib.sha256(content_manifest_path.read_bytes()).hexdigest() - ) - assert ( - report["split"]["dataset_content_aggregate_sha256"] - == content_manifest["aggregate_sha256"] - ) - - -def test_option_a_200m_muon_config_has_exact_capacity_optimizer_and_schedule(): - report, _ = preflight.validate_experiment( - "sim/pusht/action_flow_bc_usocket_latent_fm_sg_recon1_200m_muon_lr1e5_s42", - config_root=CONFIG_ROOT, - ) - - assert report["status"] == "PASS" - assert report["parameters"]["encoder_e"]["total"] == 1_010_420 - assert report["parameters"]["decoder_g"]["total"] == 1_010_416 - assert report["parameters"]["field_v"]["total"] == 186_536_913 - assert report["parameters"]["pipeline_total"]["total"] == 199_754_837 - optimization = report["optimization"] - assert optimization["optimizer"]["target"].endswith( - "ReleasedUniteCompositeOptimizer" - ) - assert optimization["optimizer"]["lr"] == pytest.approx(1.0e-5) - assert optimization["scheduler"]["eta_min"] == pytest.approx(1.0e-6) - assert optimization["parameter_groups"]["complete"] is True - assert optimization["parameter_groups"]["disjoint"] is True - - -def test_option_a_200m_adamw_changes_only_optimizer_family(): - report, _ = preflight.validate_experiment( - "sim/pusht/action_flow_bc_usocket_latent_fm_sg_recon1_200m_adamw_lr1e5_s42", - config_root=CONFIG_ROOT, - ) - - assert report["status"] == "PASS" - assert report["parameters"]["encoder_e"]["total"] == 1_010_420 - assert report["parameters"]["decoder_g"]["total"] == 1_010_416 - assert report["parameters"]["field_v"]["total"] == 186_536_913 - assert report["parameters"]["pipeline_total"]["total"] == 199_754_837 - optimization = report["optimization"] - assert optimization["optimizer"]["target"] == "torch.optim.AdamW" - assert optimization["optimizer"]["lr"] == pytest.approx(1.0e-5) - assert optimization["scheduler"]["eta_min"] == pytest.approx(1.0e-6) - assert optimization["parameter_groups"]["adamw_parameters"] == 199_754_837 - assert optimization["parameter_groups"]["muon_parameters"] == 0 - - -def test_resolved_hash_is_stable_and_uses_runtime_sentinels(): - first = preflight.compose_experiment( - "sim/pusht/action_flow_bc_usocket_recon1_s42", config_root=CONFIG_ROOT - ) - second = preflight.compose_experiment( - "sim/pusht/action_flow_bc_usocket_recon1_s42", config_root=CONFIG_ROOT - ) - first_payload, first_hash = preflight.resolved_config_payload(first) - second_payload, second_hash = preflight.resolved_config_payload(second) - - assert first_hash == second_hash - assert first_payload == second_payload - assert first_payload["paths"]["output_dir"] == "" - assert first_payload["logger"]["wandb"]["id"] == "" - - -def test_pair_diff_is_only_the_declared_reconstruction_setting(): - report = preflight.validate_pair( - "sim/pusht/action_flow_bc_usocket_recon1_s42", - "sim/pusht/action_flow_bc_usocket_recon10_s42", - config_root=CONFIG_ROOT, - ) - - assert report["status"] == "PASS" - assert report["comparison"] == { - "differing_paths": sorted(preflight.ALLOWED_PAIR_DIFFERENCES), - "only_declared_reconstruction_differences": True, - "status": "PASS", - } - assert { - item["objective"]["reconstruction_weight"] for item in report["experiments"] - } == {1.0, 10.0} - - -def test_recon100_diff_is_only_the_declared_reconstruction_setting(): - report = preflight.validate_pair( - "sim/pusht/action_flow_bc_usocket_recon1_s42", - "sim/pusht/action_flow_bc_usocket_recon100_s42", - config_root=CONFIG_ROOT, - ) - - assert report["status"] == "PASS" - assert report["comparison"]["only_declared_reconstruction_differences"] is True - assert { - item["objective"]["reconstruction_weight"] for item in report["experiments"] - } == {1.0, 100.0} - - -@pytest.mark.parametrize( - ("path", "value", "message"), - [ - ("model.pipeline.stages.5.field.time_scale", 1.0, "time scale"), - ("model.pipeline.stages.4.samples_per_content", 13, "bridge samples"), - ("callbacks.model_checkpoint.every_n_train_steps", 20_000, "checkpoint"), - ("launch_params.gpus_per_node", 2, "GPUs per node"), - ( - "evaluator.action_flow_diagnostics.enabled", - False, - "diagnostics enabled", - ), - ( - "evaluator.action_flow_diagnostics.native_error.native_theta_index", - 1, - "unsupported USocket native error contract", - ), - ( - "run_provenance.content_manifest_path", - "egomimic/hydra_configs/data/pusht/manifests/not-canonical.json", - "canonical dataset-content manifest path", - ), - ( - "run_provenance.content_manifest_sha256", - "0" * 64, - "canonical dataset-content manifest hash", - ), - ( - "run_provenance.dataset_content_aggregate_sha256", - "0" * 64, - "canonical dataset aggregate content hash", - ), - ( - "evaluator.energy_score_provenance.source_commit", - "0" * 40, - "EnergyScore source commit provenance", - ), - ( - "evaluator.energy_score_provenance.normalization_sha256", - "0" * 64, - "EnergyScore normalization provenance", - ), - ( - "evaluator.energy_score_provenance.split_manifest_sha256", - "0" * 64, - "EnergyScore split provenance", - ), - ( - "evaluator.energy_score_validation_view.split_manifest_sha256", - "0" * 64, - "EnergyScore validation-view split provenance", - ), - ( - "evaluator.energy_score_provenance.dataset_content.manifest_path", - "not-the-run-manifest.json", - "evaluator dataset-content manifest path provenance", - ), - ( - "evaluator.energy_score_provenance.dataset_content.manifest_sha256", - "0" * 64, - "evaluator dataset-content manifest hash provenance", - ), - ( - "evaluator.energy_score_provenance.dataset_content.aggregate_sha256", - "0" * 64, - "evaluator dataset aggregate content hash provenance", - ), - ( - "evaluator.action_flow_diagnostics.provenance.source_commit", - "0" * 40, - "diagnostic source commit provenance", - ), - ( - "evaluator.action_flow_diagnostics.provenance.normalization_sha256", - "0" * 64, - "diagnostic normalization provenance", - ), - ( - "evaluator.action_flow_diagnostics.provenance.split_manifest_sha256", - "0" * 64, - "diagnostic split provenance", - ), - ( - "evaluator.action_flow_diagnostics.validation_view.split_manifest_sha256", - "0" * 64, - "diagnostic validation-view split provenance", - ), - ( - "evaluator.action_flow_diagnostics.provenance.dataset_content.manifest_sha256", - "0" * 64, - "diagnostic dataset-content manifest provenance", - ), - ( - "evaluator.action_flow_diagnostics.provenance.dataset_content.aggregate_sha256", - "0" * 64, - "diagnostic dataset aggregate content provenance", - ), - ], -) -def test_preflight_fails_closed_before_training(path, value, message): - config = preflight.compose_experiment( - "sim/pusht/action_flow_bc_usocket_recon1_s42", config_root=CONFIG_ROOT - ) - from omegaconf import OmegaConf - - OmegaConf.update(config, path, value) - with pytest.raises(preflight.PreflightError, match=message): - preflight.validate_config( - config, - experiment="sim/pusht/action_flow_bc_usocket_recon1_s42", - config_root=CONFIG_ROOT, - ) - - -@pytest.mark.parametrize( - "path", - [ - "evaluator.energy_score_distance", - "run_provenance.energy_score_contract.distance", - "evaluator.energy_score_provenance.distance_contract", - ], -) -def test_every_energy_distance_surface_requires_the_typed_usocket_contract(path): - config = preflight.compose_experiment( - "sim/pusht/action_flow_bc_usocket_recon1_s42", config_root=CONFIG_ROOT - ) - from omegaconf import OmegaConf - - invalid = copy.deepcopy(preflight.USOCKET_ENERGY_DISTANCE_CONFIG) - invalid["rotation_scale_radians"] = 1.0 - OmegaConf.update(config, path, invalid, merge=False) - - with pytest.raises( - preflight.PreflightError, - match="unsupported USocket EnergyScore distance contract", - ): - preflight.validate_config( - config, - experiment="sim/pusht/action_flow_bc_usocket_recon1_s42", - config_root=CONFIG_ROOT, - ) - - -def test_native_error_surface_rejects_undeclared_contract_keys(): - config = preflight.compose_experiment( - "sim/pusht/action_flow_bc_usocket_recon1_s42", config_root=CONFIG_ROOT - ) - from omegaconf import OmegaConf - - invalid = copy.deepcopy(preflight.USOCKET_NATIVE_ERROR_CONFIG) - invalid["undeclared"] = True - OmegaConf.update( - config, - "evaluator.action_flow_diagnostics.native_error", - invalid, - merge=False, - ) - - with pytest.raises(preflight.PreflightError, match="native error keys differ"): - preflight.validate_config( - config, - experiment="sim/pusht/action_flow_bc_usocket_recon1_s42", - config_root=CONFIG_ROOT, - ) - - -@pytest.mark.parametrize( - "path", - [ - "evaluator.energy_score_provenance.dataset_content", - "evaluator.action_flow_diagnostics.provenance.dataset_content", - ], -) -def test_dataset_content_provenance_rejects_undeclared_keys(path): - config = preflight.compose_experiment( - "sim/pusht/action_flow_bc_usocket_recon1_s42", config_root=CONFIG_ROOT - ) - from omegaconf import OmegaConf - - payload = OmegaConf.to_container(OmegaConf.select(config, path), resolve=True) - assert isinstance(payload, dict) - payload["undeclared"] = True - OmegaConf.update(config, path, payload, merge=False) - - with pytest.raises(preflight.PreflightError, match="provenance keys"): - preflight.validate_config( - config, - experiment="sim/pusht/action_flow_bc_usocket_recon1_s42", - config_root=CONFIG_ROOT, - ) - - -def test_cli_emits_deterministic_machine_readable_failure(tmp_path, monkeypatch): - output = tmp_path / "preflight.json" - - def fail(*_args, **_kwargs): - raise preflight.PreflightError("deliberate contract failure") - - monkeypatch.setattr(preflight, "validate_experiment", fail) - status = preflight.main( - [ - "--experiment", - "sim/pusht/action_flow_bc_usocket_recon1_s42", - "--output", - str(output), - ] - ) - - assert status == 1 - assert json.loads(output.read_text()) == { - "error": { - "message": "deliberate contract failure", - "type": "PreflightError", - }, - "schema_version": 1, - "status": "FAIL", - } - assert output.read_text().endswith("\n") - - -def test_direct_script_cli_resolves_repository_package(tmp_path): - output = tmp_path / "preflight.json" - completed = subprocess.run( - [ - sys.executable, - str(TOOL), - "--experiment", - "sim/pusht/action_flow_bc_usocket_recon1_s42", - "--output", - str(output), - ], - cwd=ROOT, - check=False, - capture_output=True, - text=True, - ) - - assert completed.returncode == 0, completed.stderr or completed.stdout - payload = json.loads(output.read_text()) - assert payload["status"] == "PASS" - assert payload["parameters"]["field_v"]["total"] == 39_506_641 - - -@pytest.mark.parametrize( - "selector", - [ - "pusht/action_flow_chain_manual4919_avln_80k_s42", - "sim/pusht/parents/action_flow_usocket_h384", - "pusht_historical/action_flow_cotrain_uc_speed_interpolation", - ], -) -def test_fresh_composition_rejects_retired_or_non_recipe_selectors(selector): - with pytest.raises(preflight.PreflightError, match="retired|historical"): - preflight.compose_experiment(selector, config_root=CONFIG_ROOT) diff --git a/tests/test_validate_planar_dataset.py b/tests/test_validate_planar_dataset.py deleted file mode 100644 index b93f493bde..0000000000 --- a/tests/test_validate_planar_dataset.py +++ /dev/null @@ -1,241 +0,0 @@ -import copy -import hashlib -import importlib.util -import json -import subprocess -import sys -from pathlib import Path - -import pytest -import zarr - -ROOT = Path(__file__).parents[1] -MODULE_PATH = ROOT / "scripts" / "ice" / "validate_planar_dataset.py" -MANIFEST_PATH = ( - ROOT - / "egomimic" - / "hydra_configs" - / "data" - / "pusht" - / "planar_v2_usocket_dp_3k_split_seed42_v1.json" -) -SPEC = importlib.util.spec_from_file_location("validate_planar_dataset", MODULE_PATH) -assert SPEC is not None and SPEC.loader is not None -MODULE = importlib.util.module_from_spec(SPEC) -SPEC.loader.exec_module(MODULE) - - -def _manifest(names, source_root, domain=MODULE.DOMAIN): - train, valid = MODULE.split_names(names) - source_paths = {name: source_root / name for name in names} - train_paths = {source_paths[name] for name in train} - valid_paths = {source_paths[name] for name in valid} - return { - "schema_version": 1, - "status": "PASS", - "split_algorithm": MODULE.SPLIT_ALGORITHM, - "split_seed": 42, - "valid_ratio": 0.01, - "generator_sha256": "a" * 64, - "cross_domain_train_valid_resolved_path_overlap_count": 0, - "domains": { - domain: { - "folder_path": str(source_root), - "total_count": len(names), - "inventory_names_sha256": MODULE.names_sha256(names), - "train_count": len(train), - "train_names_sha256": MODULE.names_sha256(train), - "train_resolved_paths_sha256": MODULE.paths_sha256(train_paths), - "train_ids": sorted(train), - "valid_count": len(valid), - "valid_names_sha256": MODULE.names_sha256(valid), - "valid_resolved_paths_sha256": MODULE.paths_sha256(valid_paths), - "valid_ids": sorted(valid), - "id_overlap_count": 0, - "resolved_path_overlap_count": 0, - "union_count": len(names), - "union_matches_inventory": True, - } - }, - } - - -def test_committed_manifest_has_exact_hash_and_contract(): - expected = "3683e3461596eef8df2432fa865779b3c77b2a2057dabd0fea125595729cf313" - assert hashlib.sha256(MANIFEST_PATH.read_bytes()).hexdigest() == expected - - result = MODULE.validate_manifest_structure(json.loads(MANIFEST_PATH.read_text())) - - assert result["total_count"] == 2999 - assert result["train_count"] == 2970 - assert result["valid_count"] == 29 - assert result["id_overlap_count"] == 0 - assert result["union_matches_inventory"] is True - - -def test_committed_chain_manifest_has_exact_hash_and_contract(): - path = MANIFEST_PATH.with_name("planar_v2_chain_gripper_dp_3k_split_seed42_v1.json") - expected = "3ced944ea3af8e875ea88fc5c2df3a5d2865a9f95223d109fb4bd28c8be7cf69" - assert hashlib.sha256(path.read_bytes()).hexdigest() == expected - result = MODULE.validate_manifest_structure(json.loads(path.read_text())) - assert result["domain"] == "pushshapes_sim_chain_gripper" - assert (result["total_count"], result["train_count"], result["valid_count"]) == ( - 3000, - 2970, - 30, - ) - - -def test_unsupported_domain_still_fails_closed(): - manifest = _manifest({"episode_000"}, Path("/original/dataset"), "other_domain") - with pytest.raises(RuntimeError, match="supported PushT domain"): - MODULE.validate_manifest_structure(manifest) - - -def test_portable_inventory_reproduces_split_and_has_no_path_overlap(tmp_path): - names = {f"episode_{index:03d}" for index in range(100)} - source_root = Path("/original/dataset") - manifest = _manifest(names, source_root) - dataset_root = tmp_path / "portable-dataset" - dataset_root.mkdir() - entries = [] - for name in sorted(names): - path = dataset_root / f"{name}.zarr" - path.mkdir() - entries.append((path, name)) - - result = MODULE.validate_physical_inventory(manifest, entries, dataset_root) - - assert result["domain"] == MODULE.DOMAIN - assert result["total_count"] == 100 - assert result["train_count"] == 99 - assert result["valid_count"] == 1 - assert result["resolved_path_overlap_count"] == 0 - assert result["union_count"] == 100 - assert result["union_matches_inventory"] is True - - -def test_inventory_mismatch_fails_closed(tmp_path): - names = {f"episode_{index:03d}" for index in range(10)} - manifest = _manifest(names, Path("/original/dataset")) - dataset_root = tmp_path / "dataset" - dataset_root.mkdir() - entries = [] - for name in sorted(names - {"episode_009"}): - path = dataset_root / name - path.mkdir() - entries.append((path, name)) - - with pytest.raises(RuntimeError, match="physical episode inventory mismatch"): - MODULE.validate_physical_inventory(manifest, entries, dataset_root) - - -def test_manifest_split_tampering_fails_closed(): - names = {f"episode_{index:03d}" for index in range(100)} - manifest = _manifest(names, Path("/original/dataset")) - tampered = copy.deepcopy(manifest) - domain = tampered["domains"][MODULE.DOMAIN] - domain["train_ids"][0], domain["valid_ids"][0] = ( - domain["valid_ids"][0], - domain["train_ids"][0], - ) - domain["train_ids"].sort() - domain["valid_ids"].sort() - domain["train_names_sha256"] = MODULE.names_sha256(domain["train_ids"]) - domain["valid_names_sha256"] = MODULE.names_sha256(domain["valid_ids"]) - domain["inventory_names_sha256"] = MODULE.names_sha256( - domain["train_ids"] + domain["valid_ids"] - ) - - with pytest.raises(RuntimeError, match="do not reproduce"): - MODULE.validate_manifest_structure(tampered) - - -def test_two_ids_resolving_to_one_physical_directory_fail(tmp_path): - names = {"episode_a", "episode_b"} - manifest = _manifest(names, Path("/original/dataset")) - dataset_root = tmp_path / "dataset" - dataset_root.mkdir() - first = dataset_root / "episode_a" - first.mkdir() - second = dataset_root / "episode_b" - second.symlink_to(first, target_is_directory=True) - - with pytest.raises(RuntimeError, match="same physical path"): - MODULE.validate_physical_inventory( - manifest, - [(first, "episode_a"), (second, "episode_b")], - dataset_root, - ) - - -def test_explicit_deleted_directory_is_excluded_but_accounted(tmp_path): - names = {"episode_a", "episode_b"} - manifest = _manifest(names, Path("/original/dataset")) - dataset_root = tmp_path / "dataset" - dataset_root.mkdir() - entries = [] - for name in sorted(names): - path = dataset_root / name - path.mkdir() - entries.append((path, name)) - deleted = dataset_root / "deleted_episode" - deleted.mkdir() - - result = MODULE.validate_physical_inventory( - manifest, - entries, - dataset_root, - excluded_deleted_paths=[deleted], - ) - - assert result["excluded_deleted_directory_count"] == 1 - - -def test_cli_reuses_training_resolver_and_allows_only_explicitly_deleted_store( - tmp_path, -): - names = {f"episode_{index:03d}" for index in range(10)} - dataset_root = tmp_path / "dataset" - dataset_root.mkdir() - for name in names: - zarr.open_group(str(dataset_root / f"{name}.zarr"), mode="w") - deleted = zarr.open_group(str(dataset_root / "deleted.zarr"), mode="w") - deleted.attrs["is_deleted"] = True - - manifest = _manifest(names, Path("/original/dataset")) - generator = ROOT / "scripts" / "data" / "materialize_local_episode_split.py" - manifest["generator_sha256"] = hashlib.sha256(generator.read_bytes()).hexdigest() - manifest_path = tmp_path / "manifest.json" - manifest_path.write_text( - json.dumps(manifest, sort_keys=True, separators=(",", ":")) + "\n" - ) - expected_sha = hashlib.sha256(manifest_path.read_bytes()).hexdigest() - output = tmp_path / "validation.json" - - completed = subprocess.run( - [ - sys.executable, - str(MODULE_PATH), - "--repo", - str(ROOT), - "--dataset-root", - str(dataset_root), - "--manifest", - str(manifest_path), - "--expected-manifest-sha256", - expected_sha, - "--output", - str(output), - ], - check=True, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - - result = json.loads(output.read_text()) - assert result["status"] == "DATASET_VALIDATED" - assert result["physical_inventory"]["total_count"] == 10 - assert result["physical_inventory"]["excluded_deleted_directory_count"] == 1 - assert json.loads(completed.stdout.splitlines()[-1]) == result diff --git a/tests/test_validate_slurm_job_contract.py b/tests/test_validate_slurm_job_contract.py deleted file mode 100644 index 138581fd3c..0000000000 --- a/tests/test_validate_slurm_job_contract.py +++ /dev/null @@ -1,240 +0,0 @@ -import importlib.util -import json -import subprocess -import sys -from pathlib import Path - -import pytest - -ROOT = Path(__file__).parents[1] -SCRIPT = ROOT / "scripts" / "train" / "validate_slurm_job_contract.py" - - -def _load_module(): - spec = importlib.util.spec_from_file_location("validate_slurm_job_contract", SCRIPT) - assert spec is not None and spec.loader is not None - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -def _record(**replacements): - fields = { - "JobId": "12345", - "JobName": "action-flow", - "Account": "gts-dxu345-rl2", - "QOS": "inferno", - "JobState": "RUNNING", - "TimeLimit": "3-00:00:00", - "Partition": "gpu-h200", - "NumNodes": "1", - "NumCPUs": "8", - "NumTasks": "1", - "CPUs/Task": "8", - "ReqTRES": "cpu=8,mem=250G,node=1,billing=8,gres/gpu=1", - "MinCPUsNode": "8", - "MinMemoryNode": "250G", - "Features": "H100|H200", - "Command": "/task/launch.sh", - } - fields.update(replacements) - return " ".join(f"{key}={value}" for key, value in fields.items()) + "\n" - - -def _expectations(): - return { - "expected_job_id": "12345", - "expected_account": "gts-dxu345-rl2", - "expected_partition": "gpu-h200", - "expected_qos": "inferno", - "expected_cpus": 8, - "expected_memory": "250G", - "expected_time_limit": "3-00:00:00", - "expected_constraint": "H100|H200", - } - - -def test_contract_cli_writes_immutable_pass_evidence(tmp_path): - record = tmp_path / "scontrol.txt" - record.write_text(_record()) - output = tmp_path / "contract.json" - completed = subprocess.run( - [ - sys.executable, - str(SCRIPT), - "--record", - str(record), - "--expected-job-id", - "12345", - "--expected-account", - "gts-dxu345-rl2", - "--expected-partition", - "gpu-h200", - "--expected-qos", - "inferno", - "--expected-cpus", - "8", - "--expected-memory", - "250G", - "--expected-time-limit", - "3-00:00:00", - "--expected-constraint", - "H100|H200", - "--output", - str(output), - ], - check=True, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - payload = json.loads(output.read_text()) - assert payload["status"] == "SLURM_JOB_CONTRACT_VALIDATED" - assert payload["failures"] == [] - assert payload["observed"]["memory_bytes"] == 250 * 1024**3 - assert payload["observed"]["requested_tres_gpus"] == 1 - - repeated = subprocess.run( - completed.args, - check=False, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - assert repeated.returncode != 0 - assert json.loads(output.read_text())["status"] == "SLURM_JOB_CONTRACT_VALIDATED" - - -@pytest.mark.parametrize("constraint", ["H100", "H200", "H100|H200"]) -def test_exact_supported_gpu_constraints_pass(constraint): - module = _load_module() - fields = module.parse_scontrol_record(_record(Features=constraint)) - expectations = _expectations() - expectations["expected_constraint"] = constraint - expected, _, failures = module.evaluate_contract(fields, **expectations) - assert failures == [] - assert expected["constraint"] == constraint - - -@pytest.mark.parametrize( - ("replacement", "failure_field"), - [ - ({"JobId": "999"}, "job_id"), - ({"Account": "coc"}, "account"), - ({"Partition": "ice-gpu"}, "partition"), - ({"QOS": "coc-ice"}, "qos"), - ({"NumNodes": "2"}, "nodes"), - ({"NumTasks": "2"}, "tasks"), - ({"NumCPUs": "16"}, "cpus"), - ({"CPUs/Task": "4"}, "cpus_per_task"), - ({"MinCPUsNode": "4"}, "minimum_cpus_per_node"), - ({"MinMemoryNode": "200G"}, "memory_bytes"), - ({"Features": "H100"}, "constraint"), - ({"TimeLimit": "2-00:00:00"}, "time_limit_seconds"), - ( - {"ReqTRES": "cpu=8,mem=250G,node=2,billing=8,gres/gpu=1"}, - "requested_tres_nodes", - ), - ( - {"ReqTRES": "cpu=4,mem=250G,node=1,billing=8,gres/gpu=1"}, - "requested_tres_cpus", - ), - ( - {"ReqTRES": "cpu=8,mem=200G,node=1,billing=8,gres/gpu=1"}, - "requested_tres_memory_bytes", - ), - ( - {"ReqTRES": "cpu=8,mem=250G,node=1,billing=8,gres/gpu=2"}, - "requested_tres_gpus", - ), - ( - { - "ReqTRES": ( - "cpu=8,mem=250G,node=1,billing=8," "gres/gpu=1,gres/gpu:h100=1" - ) - }, - "typed_gpu_tres", - ), - ], -) -def test_each_scheduler_contract_mismatch_fails(replacement, failure_field): - module = _load_module() - fields = module.parse_scontrol_record(_record(**replacement)) - _, _, failures = module.evaluate_contract(fields, **_expectations()) - assert failure_field in {failure["field"] for failure in failures} - - -def test_contract_cli_records_failure_and_exits_nonzero(tmp_path): - record = tmp_path / "scontrol.txt" - record.write_text(_record(Features="H200")) - output = tmp_path / "contract.json" - completed = subprocess.run( - [ - sys.executable, - str(SCRIPT), - "--record", - str(record), - "--expected-job-id", - "12345", - "--expected-account", - "gts-dxu345-rl2", - "--expected-partition", - "gpu-h200", - "--expected-qos", - "inferno", - "--expected-cpus", - "8", - "--expected-memory", - "250G", - "--expected-time-limit", - "3-00:00:00", - "--expected-constraint", - "H100|H200", - "--output", - str(output), - ], - check=False, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - assert completed.returncode == 1 - payload = json.loads(output.read_text()) - assert payload["status"] == "SLURM_JOB_CONTRACT_FAILED" - assert payload["failures"] == [ - {"field": "constraint", "expected": "H100|H200", "observed": "H200"} - ] - - -def test_parser_rejects_multiline_and_duplicate_fields(): - module = _load_module() - with pytest.raises(module.ContractError, match="one-line"): - module.parse_scontrol_record("JobId=1\nAccount=x\n") - with pytest.raises(module.ContractError, match="repeats field JobId"): - module.parse_scontrol_record("JobId=1 JobId=2") - - -def test_parser_accepts_empty_unrelated_slurm_fields(): - module = _load_module() - fields = module.parse_scontrol_record(_record(StdErr="")) - assert fields["StdErr"] == "" - _, _, failures = module.evaluate_contract(fields, **_expectations()) - assert failures == [] - - -def test_contract_still_rejects_empty_required_fields(): - module = _load_module() - fields = module.parse_scontrol_record(_record(Account="")) - with pytest.raises( - module.ContractError, match="required scontrol field Account is empty" - ): - module.evaluate_contract(fields, **_expectations()) - - -@pytest.mark.parametrize( - ("raw", "seconds"), - [("3-00:00:00", 3 * 86400), ("16:00:00", 16 * 3600), ("30", 1800)], -) -def test_slurm_duration_forms(raw, seconds): - module = _load_module() - assert module.parse_slurm_duration(raw) == seconds diff --git a/tests/test_verify_action_flow_training_smoke.py b/tests/test_verify_action_flow_training_smoke.py deleted file mode 100644 index 3505141301..0000000000 --- a/tests/test_verify_action_flow_training_smoke.py +++ /dev/null @@ -1,1028 +0,0 @@ -from __future__ import annotations - -import hashlib -import importlib.util -import json -import sys -from pathlib import Path - -import pytest -import torch -from hydra import compose, initialize_config_dir -from hydra.core.hydra_config import HydraConfig -from omegaconf import OmegaConf, open_dict - -from tools.validate_action_flow_config import PreflightError, compose_experiment - -SCRIPT = ( - Path(__file__).parents[1] - / "scripts" - / "train" - / "verify_action_flow_training_smoke.py" -) -SPEC = importlib.util.spec_from_file_location( - "verify_action_flow_training_smoke", SCRIPT -) -MODULE = importlib.util.module_from_spec(SPEC) -assert SPEC.loader is not None -sys.modules[SPEC.name] = MODULE -SPEC.loader.exec_module(MODULE) - -HEAD = "a" * 40 - - -def _resolved_smoke_config( - tmp_path: Path, - *, - reconstruction_weight: float = 1.0, - experiment: str | None = None, -): - if experiment is None: - suffix = {1.0: "1", 10.0: "10", 100.0: "100"}[reconstruction_weight] - experiment = f"sim/pusht/action_flow_bc_usocket_recon{suffix}_s42" - cfg = compose_experiment(experiment) - run_dir = tmp_path / "run" - run_dir.mkdir() - normalization = tmp_path / "normalization.pt" - normalization.write_bytes(b"train-only-normalization") - normalization_hash = hashlib.sha256(normalization.read_bytes()).hexdigest() - - with open_dict(cfg): - cfg.trainer.max_steps = 2 - cfg.trainer.val_check_interval = 1 - cfg.trainer.limit_val_batches = 1 - cfg.trainer.log_every_n_steps = 1 - cfg.trainer.precision = "bf16" - cfg.callbacks.model_checkpoint.every_n_train_steps = 1 - cfg.model.gradient_telemetry_cadence = 2 - cfg.norm_stats.precomputed_norm_path = str(normalization) - cfg.run_provenance.source_commit = HEAD - cfg.run_provenance.normalization_sha256 = normalization_hash - cfg.evaluator.artifact_root = str(run_dir / "energy") - cfg.evaluator.action_flow_diagnostics.artifact_root = str( - run_dir / "diagnostics" - ) - cfg.hydra.runtime.cwd = str(MODULE.REPOSITORY_ROOT) - cfg.hydra.runtime.output_dir = str(run_dir) - HydraConfig.instance().set_config(cfg) - selected = OmegaConf.masked_copy(cfg, [key for key in cfg.keys() if key != "hydra"]) - resolved = OmegaConf.create(OmegaConf.to_container(selected, resolve=True)) - config_path = run_dir / ".hydra/config.yaml" - config_path.parent.mkdir() - OmegaConf.save(resolved, config_path) - return experiment, run_dir, config_path, normalization_hash - - -def test_config_gate_accepts_only_exact_two_step_contract(tmp_path, monkeypatch): - experiment, run_dir, config_path, normalization_hash = _resolved_smoke_config( - tmp_path - ) - monkeypatch.setattr(MODULE, "_git_head", lambda: HEAD) - - _, identities = MODULE._validate_config( - config_path=config_path, - experiment=experiment, - run_dir=run_dir, - expected_head=HEAD, - expected_config_sha256=hashlib.sha256(config_path.read_bytes()).hexdigest(), - expected_split_sha256=None, - expected_normalization_sha256=normalization_hash, - ) - - assert identities["repo_head"] == HEAD - assert identities["normalization_sha256"] == normalization_hash - assert ( - identities["config_sha256"] - == hashlib.sha256(config_path.read_bytes()).hexdigest() - ) - - -def test_config_gate_accepts_option_a_200m_muon_contract(tmp_path, monkeypatch): - experiment, run_dir, config_path, normalization_hash = _resolved_smoke_config( - tmp_path, - experiment=( - "sim/pusht/action_flow_bc_usocket_latent_fm_sg_recon1_200m_muon_lr1e5_s42" - ), - ) - monkeypatch.setattr(MODULE, "_git_head", lambda: HEAD) - - config, _ = MODULE._validate_config( - config_path=config_path, - experiment=experiment, - run_dir=run_dir, - expected_head=HEAD, - expected_config_sha256=hashlib.sha256(config_path.read_bytes()).hexdigest(), - expected_split_sha256=None, - expected_normalization_sha256=normalization_hash, - ) - - assert config.model.pipeline.stages[5].field.hidden_dim == 1_024 - assert config.model.pipeline.stages[5].field.depth == 14 - assert config.model.optimizer.lr == pytest.approx(1.0e-5) - - -def test_config_gate_accepts_option_a_200m_adamw_contract(tmp_path, monkeypatch): - experiment, run_dir, config_path, normalization_hash = _resolved_smoke_config( - tmp_path, - experiment=( - "sim/pusht/action_flow_bc_usocket_latent_fm_sg_recon1_200m_adamw_lr1e5_s42" - ), - ) - monkeypatch.setattr(MODULE, "_git_head", lambda: HEAD) - - config, _ = MODULE._validate_config( - config_path=config_path, - experiment=experiment, - run_dir=run_dir, - expected_head=HEAD, - expected_config_sha256=hashlib.sha256(config_path.read_bytes()).hexdigest(), - expected_split_sha256=None, - expected_normalization_sha256=normalization_hash, - ) - - assert config.model.pipeline.stages[5].field.hidden_dim == 1_024 - assert config.model.pipeline.stages[5].field.depth == 14 - assert config.model.optimizer._target_ == "torch.optim.AdamW" - assert config.model.optimizer.lr == pytest.approx(1.0e-5) - assert config.model.optimizer_named_parameters is False - - -@pytest.mark.parametrize( - "experiment", - [ - "pusht/action_flow_usocket_latent_fm_sg_unite_h384_s42", - "pusht/action_flow_usocket_latent_fm_sg_unite_h384_sum14_cfg4_val8_s42", - ], -) -def test_config_gate_rejects_retired_unite_h384_recipes(experiment): - with pytest.raises(PreflightError, match="absent or retired"): - compose_experiment(experiment) - - -def test_unite_h384_parent_preserves_component_contract(): - # Direct component composition is not authorization for a fresh smoke/run. - config_dir = MODULE.REPOSITORY_ROOT / "egomimic" / "hydra_configs" - with initialize_config_dir(version_base=None, config_dir=str(config_dir)): - config = compose( - config_name="train_zarr_cartesian", - overrides=[ - "+experiment=sim/pusht/parents/action_flow_usocket_h384", - "++paths.root_dir=.", - "model.cfg_scale=4.0", - "model.flow_loss_aggregation=sum_samples", - "data.valid_dataloader_params.pushshapes_sim_u_socket.batch_size=32", - ], - ) - assert config.model.latent_dim == 16 - assert config.model.condition_dim == 128 - assert config.model.pipeline.stages[6].field.backbone.hidden_dim == 384 - assert config.model.pipeline.stages[6].field.backbone.depth == 12 - assert config.model.optimizer.lr == pytest.approx(1.0e-4) - assert config.norm_stats.norm_mode == "minmax" - assert config.model.pipeline.stages[6].cfg_scale == pytest.approx(4.0) - assert config.model.pipeline.stages[8].flow_aggregation == "sum_samples" - assert config.data.valid_dataloader_params.pushshapes_sim_u_socket.batch_size == 32 - - -@pytest.mark.parametrize( - ("path", "value", "message"), - [ - ("model.flow_samples_per_content", 1, "flow_samples_per_content"), - ("model.gradient_telemetry_cadence", 1, "gradient_telemetry_cadence"), - ("trainer.devices", 2, "trainer.devices"), - ("callbacks.model_checkpoint.every_n_train_steps", 2, "every_n_train_steps"), - ], -) -def test_config_gate_rejects_smoke_shortcuts( - tmp_path, monkeypatch, path, value, message -): - experiment, run_dir, config_path, _ = _resolved_smoke_config(tmp_path) - cfg = OmegaConf.load(config_path) - OmegaConf.update(cfg, path, value) - OmegaConf.save(cfg, config_path) - monkeypatch.setattr(MODULE, "_git_head", lambda: HEAD) - - with pytest.raises(MODULE.SmokeVerificationError, match=message): - MODULE._validate_config( - config_path=config_path, - experiment=experiment, - run_dir=run_dir, - expected_head=HEAD, - expected_config_sha256=None, - expected_split_sha256=None, - expected_normalization_sha256=None, - ) - - -def _history_row(): - row = {} - for name in ( - "TotalLoss", - "FlowMatchingLoss", - "ReconstructionLoss", - "ReconstructionL1", - "ActionVelocityLoss", - ): - train_value = 3.75 if name == "TotalLoss" else 1.25 - valid_value = 4.5 if name == "TotalLoss" else 1.5 - row[f"Train/ActionFlow/{name}_step"] = train_value - row[f"Train/ActionFlow/{name}/{MODULE.SOURCE_LABEL}_step"] = train_value - row[f"Valid/ActionFlow/{name}"] = valid_value - for label in ("FM", "Reconstruction", "ActionVelocity"): - row[f"Train/ActionFlow/GradientNorm/{label}"] = 0.5 - row[f"Train/ActionFlow/GradientParameterCount/{label}"] = 100.0 - for pair in ( - "FM__Reconstruction", - "FM__ActionVelocity", - "Reconstruction__ActionVelocity", - ): - row[f"Train/ActionFlow/GradientCosine/{pair}"] = 0.25 - row[f"Train/ActionFlow/GradientCosineDefined/{pair}"] = 1.0 - row[f"Train/ActionFlow/GradientIntersectionParameterCount/{pair}"] = 50.0 - row["Train/MSE"] = 1.25 - row[f"Train/MSE/{MODULE.SOURCE_LABEL}"] = 1.25 - row["Train/ActionFlow/Compute/FieldForwardCallsPerStep"] = 1.0 - row["Train/ActionFlow/Compute/FieldSampleEquivalentsPerStep"] = 14.0 - row["Train/ActionFlow/Compute/DecoderJVPCallsPerStep"] = 1.0 - row["Train/ActionFlow/Compute/PeakAllocatedBytes"] = 1024.0 - row["Train/ActionFlow/Schedule/ReconstructionOnly"] = 0.0 - row["Train/ActionFlow/Schedule/EffectiveFlowWeight"] = 1.0 - row["Train/ActionFlow/Schedule/EffectiveActionVelocityWeight"] = 1.0 - for base in ( - "Valid/MSE", - "Valid/Native_MSE", - "Valid/EnergyScore@32", - "Valid/EnergyScoreAccuracy@32", - "Valid/EnergyScoreDiversity@32", - ): - row[base] = 0.75 - row[f"{base}/{MODULE.SOURCE_LABEL}"] = 0.75 - for name in ( - "Valid/ActionFlow/CleanReconstructionMSE", - "Valid/ActionFlow/CleanReconstructionNativeMSE", - "Valid/ActionFlow/Latent/clean/RMS", - "Valid/ActionFlow/Latent/generated/EffectiveRank", - "Valid/ActionFlow/DecoderJacobian/clean/spectral_norm_mean", - "Valid/ActionFlow/DecodedFullNoise/token_radius_mean", - "Valid/ActionFlow/DenoisingTrajectory/LatentMSE/t1000", - "Valid/ActionFlow/DenoisingTrajectory/DecodedMSE/t0000", - "Valid/ActionFlow/DenoisingTrajectory/DecodedNativeMSE/t0000", - "Valid/ActionFlow/Alignment/FinalLatentCosine/t1000", - "Valid/ActionFlow/Alignment/CKA/encoder_00__field_00/t0500", - "Valid/ActionFlow/Alignment/CKNNA/encoder_00__field_00/t0500", - ): - row[name] = 0.5 - if "NativeMSE" in name or "decoded_native_mse" in name: - row[f"{name}/{MODULE.SOURCE_LABEL}"] = 0.5 - return row - - -def test_history_gate_requires_components_gradients_and_scheduled_validation(): - result = MODULE._validate_history({0: {}, 2: _history_row()}) - - assert result["train_step"] == 2 - assert result["valid_step"] == 2 - assert result["train"]["Train/ActionFlow/FlowMatchingLoss"] == 1.25 - - -def test_history_gate_accepts_float32_flow_weight_telemetry(): - row = _history_row() - row["Train/ActionFlow/Schedule/EffectiveFlowWeight"] = float(torch.tensor(0.01)) - for suffix in ("", f"/{MODULE.SOURCE_LABEL}"): - row[f"Train/ActionFlow/TotalLoss{suffix}_step"] = 2.5125 - row["Valid/ActionFlow/TotalLoss"] = 3.015 - - result = MODULE._validate_history( - {2: row}, reconstruction_weight=1.0, flow_weight=0.01 - ) - - assert result["train_step"] == 2 - - -def test_unite_history_gate_requires_both_optimizer_family_rates(): - row = _history_row() - row["Train/ActionFlow/Compute/FieldForwardCallsPerStep"] = 2.0 - row["Train/ActionFlow/Compute/FieldSampleEquivalentsPerStep"] = 28.0 - row["Train/ActionFlow/GradientCosine/FM__Reconstruction"] = 0.0 - row["Train/ActionFlow/GradientCosineDefined/FM__Reconstruction"] = 0.0 - row["Train/ActionFlow/GradientIntersectionParameterCount/FM__Reconstruction"] = 0.0 - row["Optimizer/LR/AdamW"] = 2.5e-8 - row["Optimizer/LR/Muon"] = 2.5e-8 - - result = MODULE._validate_history({2: row}, method=MODULE.STOPGRAD_UNITE_METHOD) - - assert result["train"]["Optimizer/LR/AdamW"] == pytest.approx(2.5e-8) - assert result["train"]["Optimizer/LR/Muon"] == pytest.approx(2.5e-8) - - del row["Optimizer/LR/Muon"] - with pytest.raises(MODULE.SmokeVerificationError, match="gradient telemetry"): - MODULE._validate_history({2: row}, method=MODULE.STOPGRAD_UNITE_METHOD) - - -def test_history_gate_rejects_missing_gradient_telemetry(): - row = _history_row() - del row["Train/ActionFlow/GradientCosine/FM__ActionVelocity"] - - with pytest.raises(MODULE.SmokeVerificationError, match="gradient telemetry"): - MODULE._validate_history({2: row}) - - -def _write_artifacts(tmp_path: Path): - split_hash = "b" * 64 - norm_hash = "d" * 64 - content_hash = "e" * 64 - aggregate_hash = "f" * 64 - energy_root = tmp_path / "energy" - diagnostic_root = tmp_path / "diagnostics" - config_path = tmp_path / ".hydra/config.yaml" - config_path.parent.mkdir() - config_path.write_text("name: test\n") - content_path = tmp_path / "content.json" - content_path.write_text("{}\n") - leaf = Path("epoch-0-step-2/rank-0-batch-0.pt") - energy_path = energy_root / leaf - diagnostic_path = diagnostic_root / leaf - energy_path.parent.mkdir(parents=True) - diagnostic_path.parent.mkdir(parents=True) - seed_hash = "c" * 64 - targets = torch.zeros(16, 16, 4) - condition_ids = [ - { - "batch_position": index, - "episode_hash": f"episode-{index:04d}", - "frame_index": index, - "normalized_target_sha256": MODULE._target_tensor_sha256(targets[index]), - } - for index in range(16) - ] - energy_view = { - "definition": "first_deterministic_validation_batch_per_ddp_rank", - "split_manifest_sha256": split_hash, - "per_rank_batch_size": 16, - "world_size": 1, - } - distance = MODULE.usocket_energy_distance_metadata( - MODULE.USOCKET_ENERGY_DISTANCE_CONFIG - ) - wandb = {"entity": "rl2-group", "project": "test", "run_id": "smoke"} - energy_identity = { - "schema": "energy-score-validation-artifact/v2", - "metric": { - "name": "EnergyScore@32", - "sample_count": 32, - "deterministic_seed": 420042, - }, - "source_commit": HEAD, - "normalization_sha256": norm_hash, - "split_manifest_sha256": split_hash, - "resolved_config_path": str(config_path.resolve()), - "resolved_config_sha256": hashlib.sha256(config_path.read_bytes()).hexdigest(), - "wandb": wandb, - "dataset_content": { - "manifest_path": str(content_path.resolve()), - "manifest_sha256": content_hash, - "aggregate_sha256": aggregate_hash, - }, - "distance": distance, - "seed_bank_sha256": seed_hash, - "validation_view": energy_view, - "validation_conditions": {MODULE.SOURCE_LABEL: condition_ids}, - "checkpoint_binding": { - "global_step": 2, - "checkpoint_sha256": None, - "sha256_status": MODULE.ENERGY_CHECKPOINT_STATUS, - }, - } - torch.save( - { - "schema_version": 2, - "metric": "EnergyScore@32", - "sample_count": 32, - "seed_bank": list(range(32)), - "seed_bank_sha256": seed_hash, - "deterministic_seed": 420042, - "distance": distance, - "global_step": 2, - "validation_view": energy_view, - "identity": energy_identity, - "identity_sha256": MODULE._canonical_json_sha256(energy_identity), - "domains": { - MODULE.SOURCE_LABEL: { - "predictions": torch.zeros(32, 16, 16, 4), - "targets": targets, - "native_predictions": torch.zeros(32, 16, 16, 3), - "native_targets": torch.zeros(16, 16, 3), - "condition_ids": condition_ids, - } - }, - }, - energy_path, - ) - diagnostic_provenance = { - "source_commit": HEAD, - "normalization_sha256": norm_hash, - "split_manifest_sha256": split_hash, - "sampler_steps": 16, - "dataset_content": { - "manifest_sha256": content_hash, - "aggregate_sha256": aggregate_hash, - }, - } - identity = { - "schema": "action-flow-validation-diagnostics/v1", - "validation_view": energy_view, - "provenance": diagnostic_provenance, - "native_error": MODULE.USOCKET_NATIVE_ERROR_CONFIG, - } - identity_hash = hashlib.sha256( - json.dumps( - identity, - sort_keys=True, - separators=(",", ":"), - ensure_ascii=True, - allow_nan=False, - ).encode() - ).hexdigest() - torch.save( - { - "schema_version": 1, - "metric": "ActionFlowValidationDiagnostics", - "global_step": 2, - "identity": identity, - "identity_sha256": identity_hash, - "sampler": {"name": "reverse_euler"}, - "validation_view": energy_view, - "provenance": diagnostic_provenance, - "statistics": {"native_action_error": MODULE.USOCKET_NATIVE_ERROR_CONFIG}, - "sources": { - MODULE.SOURCE_LABEL: { - "computed": { - "clean_reconstruction_native_mse_by_condition": torch.ones(16), - "trajectory_decoded_native_mse_by_condition": torch.ones( - 17, 16 - ), - "fixed_level_metrics": { - label: { - metric: torch.ones(16) - for metric in ( - "state_decoded_native_mse", - "predicted_clean_decoded_native_mse", - "final_decoded_native_mse", - ) - } - for label in MODULE.NATIVE_LEVELS - }, - } - } - }, - }, - diagnostic_path, - ) - diagnostic_hash = hashlib.sha256(diagnostic_path.read_bytes()).hexdigest() - Path(f"{diagnostic_path}.sha256").write_text( - json.dumps( - { - "artifact": diagnostic_path.name, - "identity_sha256": identity_hash, - "sha256": diagnostic_hash, - } - ) - ) - config = OmegaConf.create( - { - "evaluator": { - "artifact_root": str(energy_root), - "seed_bank_sha256": seed_hash, - "energy_score_validation_view": energy_view, - "action_flow_diagnostics": { - "artifact_root": str(diagnostic_root), - "validation_view": energy_view, - "provenance": {"sampler_steps": 16}, - }, - }, - "logger": { - "wandb": { - "entity": wandb["entity"], - "project": wandb["project"], - "id": wandb["run_id"], - } - }, - } - ) - identities = { - "config_sha256": hashlib.sha256(config_path.read_bytes()).hexdigest(), - "content_manifest_path": str(content_path.resolve()), - "content_manifest_sha256": content_hash, - "dataset_content_aggregate_sha256": aggregate_hash, - "normalization_sha256": norm_hash, - "repo_head": HEAD, - "split_manifest_sha256": split_hash, - } - checkpoint = { - "checkpoint_sha256": "1" * 64, - "file_size_bytes": 100, - "global_step": 2, - } - return config, identities, checkpoint, diagnostic_path - - -def test_artifact_gate_verifies_energy_and_diagnostic_immutability(tmp_path): - config, identities, checkpoint, diagnostic_path = _write_artifacts(tmp_path) - - result = MODULE._validate_artifacts( - config=config, - run_dir=tmp_path, - identities=identities, - checkpoint=checkpoint, - ) - - assert result["energy_score"]["sha256"] - assert result["action_flow_diagnostics"]["path"] == str(diagnostic_path) - - -def test_artifact_gate_selects_exact_slurm_attempt_and_checks_execution( - tmp_path, monkeypatch -): - root = tmp_path / "artifacts" - for restart in (0, 1): - artifact = ( - root / f"job-5714540-restart-{restart}" / "epoch-0-step-2/rank-0-batch-0.pt" - ) - artifact.parent.mkdir(parents=True) - torch.save( - { - "global_step": 2, - "execution": { - "slurm_job_id": "5714540", - "slurm_restart_count": restart, - }, - }, - artifact, - ) - monkeypatch.setenv("SLURM_JOB_ID", "5714540") - monkeypatch.setenv("SLURM_RESTART_COUNT", "1") - selected, payload = MODULE._step_two_artifact(root, label="test") - assert "job-5714540-restart-1" in str(selected) - payload["execution"]["slurm_restart_count"] = 0 - torch.save(payload, selected) - with pytest.raises( - MODULE.SmokeVerificationError, match="execution identity mismatch" - ): - MODULE._step_two_artifact(root, label="test") - monkeypatch.delenv("SLURM_JOB_ID") - with pytest.raises(MODULE.SmokeVerificationError, match="expected one step-2"): - MODULE._step_two_artifact(root, label="test") - - -def test_artifact_gate_rejects_tampered_diagnostic_sidecar(tmp_path): - config, identities, checkpoint, diagnostic_path = _write_artifacts(tmp_path) - sidecar = Path(f"{diagnostic_path}.sha256") - payload = json.loads(sidecar.read_text()) - payload["sha256"] = "0" * 64 - sidecar.write_text(json.dumps(payload)) - - with pytest.raises(MODULE.SmokeVerificationError, match="sidecar mismatch"): - MODULE._validate_artifacts( - config=config, - run_dir=tmp_path, - identities=identities, - checkpoint=checkpoint, - ) - - -def test_artifact_gate_accepts_unique_origin_from_separate_validation_job( - tmp_path, monkeypatch -): - root = tmp_path / "artifacts" - artifact = root / "job-5714540-restart-1/epoch-0-step-2/rank-0-batch-0.pt" - artifact.parent.mkdir(parents=True) - torch.save( - { - "global_step": 2, - "execution": {"slurm_job_id": "5714540", "slurm_restart_count": 1}, - }, - artifact, - ) - monkeypatch.setenv("SLURM_JOB_ID", "5719999") - monkeypatch.setenv("SLURM_RESTART_COUNT", "0") - selected, payload = MODULE._step_two_artifact(root, label="test") - assert selected == artifact - assert payload["execution"]["slurm_job_id"] == "5714540" - - -def test_preflight_gate_binds_source_experiment_split_and_normalization(tmp_path): - experiment = "sim/pusht/action_flow_bc_usocket_recon1_s42" - split_hash = "b" * 64 - normalization_hash = "c" * 64 - content_hash = "d" * 64 - aggregate_hash = "e" * 64 - destination = tmp_path / "provenance/restart-0/PREFLIGHT_RESULT.json" - destination.parent.mkdir(parents=True) - destination.write_text( - json.dumps( - { - "status": "PASS", - "source": {"head": HEAD}, - "experiment": experiment, - "split_manifest_sha256": split_hash, - "normalization_sha256": normalization_hash, - "content_manifest_sha256": content_hash, - "dataset_content_aggregate_sha256": aggregate_hash, - }, - sort_keys=True, - ) - ) - digest = hashlib.sha256(destination.read_bytes()).hexdigest() - - result = MODULE._validate_preflight( - run_dir=tmp_path, - expected_sha256=digest, - expected_head=HEAD, - experiment=experiment, - split_sha256=split_hash, - normalization_sha256=normalization_hash, - content_manifest_sha256=content_hash, - dataset_content_aggregate_sha256=aggregate_hash, - ) - - assert result == {"path": str(destination), "sha256": digest, "copy_count": 1} - - -def test_cli_accepts_launcher_compatibility_names(): - args = MODULE._parser().parse_args( - [ - "/tmp/run", - "--expected-head", - HEAD, - "--expected-experiment", - "sim/pusht/action_flow_bc_usocket_recon10_s42", - "--expected-reconstruction-weight", - "10", - "--expected-preflight-sha256", - "d" * 64, - ] - ) - - assert args.experiment.endswith("recon10_s42") - assert args.expected_reconstruction_weight == 10.0 - - -def test_cli_accepts_codec98k_experiment(): - args = MODULE._parser().parse_args( - [ - "/tmp/run", - "--expected-head", - HEAD, - "--expected-experiment", - "pusht/action_flow_bc_usocket_latent_fm_sg_recon1_codec98k_s42", - "--expected-preflight-sha256", - "d" * 64, - ] - ) - - assert args.experiment.endswith("recon1_codec98k_s42") - - -def test_cli_accepts_option_a_200m_muon_experiment(): - args = MODULE._parser().parse_args( - [ - "/tmp/run", - "--expected-head", - HEAD, - "--expected-experiment", - "sim/pusht/action_flow_bc_usocket_latent_fm_sg_recon1_200m_muon_lr1e5_s42", - "--expected-preflight-sha256", - "d" * 64, - ] - ) - - assert args.experiment.endswith("recon1_200m_muon_lr1e5_s42") - - -def test_cli_accepts_option_a_200m_adamw_experiment(): - args = MODULE._parser().parse_args( - [ - "/tmp/run", - "--expected-head", - HEAD, - "--expected-experiment", - "sim/pusht/action_flow_bc_usocket_latent_fm_sg_recon1_200m_adamw_lr1e5_s42", - "--expected-preflight-sha256", - "d" * 64, - ] - ) - - assert args.experiment.endswith("recon1_200m_adamw_lr1e5_s42") - - -def test_gpu_probe_gate_requires_real_single_h100_or_h200_bf16(tmp_path): - path = tmp_path / "provenance/restart-0/gpu_probe.json" - path.parent.mkdir(parents=True) - path.write_text( - json.dumps( - { - "status": "PASSED", - "world_size": 1, - "gpu_name": "NVIDIA H200", - "bf16_supported": True, - "bf16_forward_backward": { - "dtype": "torch.bfloat16", - "gradient_dtype": "torch.bfloat16", - "finite": True, - }, - "nccl": {"world_size": 1, "all_reduce": 1.0, "destroyed": True}, - } - ) - ) - - records = MODULE._validate_gpu_probes(tmp_path) - - assert records[0]["gpu_name"] == "NVIDIA H200" - - -def test_gpu_probe_gate_rejects_non_target_gpu(tmp_path): - path = tmp_path / "provenance/restart-0/gpu_probe.json" - path.parent.mkdir(parents=True) - path.write_text( - json.dumps( - { - "status": "PASSED", - "world_size": 1, - "gpu_name": "NVIDIA A100-SXM4-80GB", - "bf16_supported": True, - "bf16_forward_backward": { - "dtype": "torch.bfloat16", - "gradient_dtype": "torch.bfloat16", - "finite": True, - }, - "nccl": {"world_size": 1, "all_reduce": 1.0, "destroyed": True}, - } - ) - ) - - with pytest.raises(MODULE.SmokeVerificationError, match="H100 or H200"): - MODULE._validate_gpu_probes(tmp_path) - - -def _scaled_muon_optimizer_state(): - return { - "adamw": { - "state": {0: {"exp_avg": torch.zeros(1)}}, - "param_groups": [{"lr": 1.0e-5, "params": [0]}], - }, - "muon": { - "state": {1: {"momentum_buffer": torch.zeros(1)}}, - "param_groups": [{"lr": 1.0e-5, "params": [1]}], - }, - "group_manifest": { - "adamw_parameter_names": ("encoder.bias",), - "muon_parameter_names": ("field.weight",), - "muon_source_commit": "a" * 40, - }, - } - - -def test_optimizer_state_gate_accepts_scaled_muon_composite_state(): - config = OmegaConf.create( - {"name": "action_flow_bc_usocket_latent_fm_sg_recon1_200m_muon_lr1e5_s42"} - ) - - MODULE._validate_optimizer_state(_scaled_muon_optimizer_state(), config) - - -@pytest.mark.parametrize("group", ["adamw", "muon"]) -def test_optimizer_state_gate_rejects_empty_scaled_muon_nested_state(group): - config = OmegaConf.create( - {"name": "action_flow_bc_usocket_latent_fm_sg_recon1_200m_muon_lr1e5_s42"} - ) - optimizer_state = _scaled_muon_optimizer_state() - optimizer_state[group]["state"] = {} - - with pytest.raises( - MODULE.SmokeVerificationError, match=f"{group} optimizer state is empty" - ): - MODULE._validate_optimizer_state(optimizer_state, config) - - -def _checkpoint_payload(): - return { - "global_step": 2, - "state_dict": {"model.weight": torch.ones(1)}, - "optimizer_states": [ - { - "state": {0: {"exp_avg": torch.zeros(1)}}, - "param_groups": [{"lr": 3.0e-5, "params": [0]}], - } - ], - "lr_schedulers": [{"last_epoch": 2, "_last_lr": [3.0e-6]}], - "loops": {"fit_loop": {"completed": 2}}, - } - - -def test_checkpoint_gate_strictly_reloads_exact_action_flow_wrapper( - tmp_path, monkeypatch -): - checkpoint_dir = tmp_path / "checkpoints" - checkpoint_dir.mkdir() - payload = _checkpoint_payload() - immutable = checkpoint_dir / "epoch-epoch=0-step-step=2.ckpt" - torch.save(payload, immutable) - (checkpoint_dir / "last.ckpt").symlink_to(immutable.name) - - class _Parameter: - @staticmethod - def numel(): - return MODULE.EXPECTED_PARAMETER_COUNT - - class _Wrapper: - encoder_e = object() - field_v = object() - decoder_g = object() - - class _Nets: - @staticmethod - def named_parameters(**_kwargs): - return [] - - nets = _Nets() - - @classmethod - def load_from_checkpoint(cls, *_args, **_kwargs): - return cls() - - @staticmethod - def parameters(): - return [_Parameter()] - - monkeypatch.setattr(MODULE, "ActionFlowModelWrapper", _Wrapper) - monkeypatch.setattr( - MODULE, - "_validate_gradient_route_manifest", - lambda *_args: {"manifest_sha256": "f" * 64}, - ) - - result = MODULE._validate_checkpoint( - tmp_path, reconstruction_weight=1.0, flow_weight=1.0 - ) - - assert result["global_step"] == 2 - assert result["parameter_count"] == MODULE.EXPECTED_PARAMETER_COUNT - assert result["strict_checkpoint_reload"] == "passed" - assert result["file_size_bytes"] > 0 - - -def _gradient_manifest(named_parameters): - route_names = { - "FM": [name for name, _ in named_parameters[:3]], - "Reconstruction": [named_parameters[1][0], named_parameters[3][0]], - "ActionVelocity": [name for name, _ in named_parameters], - } - routes = {} - route_hashes = {} - for label, names in route_names.items(): - parameters = dict(named_parameters) - route = [ - { - "dtype": str(parameters[name].dtype), - "name": name, - "numel": parameters[name].numel(), - "shape": list(parameters[name].shape), - } - for name in names - ] - routes[label] = route - route_hashes[label] = MODULE._canonical_json_sha256(route) - intersections = { - "FM__Reconstruction": [named_parameters[1][0]], - "FM__ActionVelocity": [name for name, _ in named_parameters[:3]], - "Reconstruction__ActionVelocity": [ - named_parameters[1][0], - named_parameters[3][0], - ], - } - core = { - "routes": routes, - "route_sha256": route_hashes, - "intersections": intersections, - "schema_version": 1, - } - return {**core, "manifest_sha256": MODULE._canonical_json_sha256(core)} - - -def test_gradient_route_manifest_binds_exact_named_shared_paths(): - named = ( - ("nets.pipeline.stages.0.observation", torch.nn.Parameter(torch.ones(2))), - ("nets.pipeline.stages.3.encoder", torch.nn.Parameter(torch.ones(3))), - ("nets.pipeline.stages.5.field", torch.nn.Parameter(torch.ones(4))), - ("nets.pipeline.stages.6.decoder", torch.nn.Parameter(torch.ones(5))), - ) - manifest = _gradient_manifest(named) - - result = MODULE._validate_gradient_route_manifest(manifest, named) - - assert result["manifest_sha256"] == manifest["manifest_sha256"] - assert result["route_parameter_counts"] == { - "FM": 9, - "Reconstruction": 8, - "ActionVelocity": 14, - } - assert result["shared_path_parameter_counts"]["FM__Reconstruction"] == 3 - - -def test_gradient_route_manifest_rejects_tampered_named_route(): - named = ( - ("nets.pipeline.stages.0.observation", torch.nn.Parameter(torch.ones(2))), - ("nets.pipeline.stages.3.encoder", torch.nn.Parameter(torch.ones(3))), - ("nets.pipeline.stages.5.field", torch.nn.Parameter(torch.ones(4))), - ("nets.pipeline.stages.6.decoder", torch.nn.Parameter(torch.ones(5))), - ) - manifest = _gradient_manifest(named) - manifest["routes"]["FM"][0]["numel"] = 99 - - with pytest.raises(MODULE.SmokeVerificationError, match="size mismatch"): - MODULE._validate_gradient_route_manifest(manifest, named) - - -def test_checkpoint_gate_rejects_missing_scheduler(tmp_path): - checkpoint_dir = tmp_path / "checkpoints" - checkpoint_dir.mkdir() - payload = _checkpoint_payload() - payload["lr_schedulers"] = [] - immutable = checkpoint_dir / "epoch-epoch=0-step-step=2.ckpt" - torch.save(payload, immutable) - (checkpoint_dir / "last.ckpt").symlink_to(immutable.name) - - with pytest.raises(MODULE.SmokeVerificationError, match="scheduler state"): - MODULE._validate_checkpoint( - tmp_path, reconstruction_weight=1.0, flow_weight=1.0 - ) - - -def _raw_multiplier_verifier(): - import importlib.util - - path = ( - Path(__file__).resolve().parents[1] - / "scripts/train/verify_scaled_action_flow_training_smoke.py" - ) - spec = importlib.util.spec_from_file_location("raw_multiplier_verifier", path) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -@pytest.mark.parametrize( - "damage", [None, "missing", "decay", "updates", "shape", "nan"] -) -def test_raw_multiplier_native_ema_verification(damage): - from types import SimpleNamespace - - from omegaconf import OmegaConf - - from egomimic.utils.ema_callback import EMACallback - - verifier = _raw_multiplier_verifier() - row = verifier.EXPERIMENTS[ - "sim/pusht/action_flow_cotrain_uc_multiplier_interpolation" - ] - assert row["parameter_count"] == 471549402 and row["hidden_dim"] == 696 - model = torch.nn.Linear(2, 2) - callback = EMACallback(decay=0.9978) - trainer = SimpleNamespace(global_step=0) - callback.on_fit_start(trainer, model) - callback._update(model) - callback._update(model) - checkpoint = {"global_step": 2, "state_dict": model.state_dict()} - callback.on_save_checkpoint(trainer, model, checkpoint) - cfg = OmegaConf.create( - { - "callbacks": { - "ema": { - "_target_": "egomimic.utils.ema_callback.EMACallback", - "decay": 0.9978, - "validate_with_ema": True, - "use_warmup": False, - } - } - } - ) - if damage == "missing": - checkpoint.pop("ema_state_dict") - elif damage == "decay": - checkpoint["ema_config"]["decay"] = 0.9 - elif damage == "updates": - checkpoint["ema_num_updates"] = 1 - elif damage == "shape": - checkpoint["ema_state_dict"]["weight"] = torch.zeros(3, 3) - elif damage == "nan": - checkpoint["ema_state_dict"]["weight"].fill_(float("nan")) - if damage: - with pytest.raises((RuntimeError, verifier.VerificationError)): - verifier.validate_raw_multiplier_ema(cfg, model, checkpoint) - else: - assert ( - verifier.validate_raw_multiplier_ema(cfg, model, checkpoint)[ - "strict_ema_reload" - ] - == "passed" - ) diff --git a/tests/test_wandb_health.py b/tests/test_wandb_health.py deleted file mode 100644 index 4c1dc3b70c..0000000000 --- a/tests/test_wandb_health.py +++ /dev/null @@ -1,120 +0,0 @@ -import importlib.util -from pathlib import Path - -import pytest - -PATH = Path(__file__).parents[1] / "scripts/train/check_wandb_health.py" -SPEC = importlib.util.spec_from_file_location("check_wandb_health", PATH) -health = importlib.util.module_from_spec(SPEC) -SPEC.loader.exec_module(health) - - -class FakeRun: - path = ["team", "project", "run"] - url = "https://wandb.ai/team/project/runs/run" - state = "running" - lastHistoryStep = 20545 - config = {"run_provenance": {"source_commit": "exact-source"}} - - def __init__(self, rows, summary=None): - self.rows = rows - self.summary = summary or {} - self.scan_arguments = None - - def scan_history(self, **kwargs): - self.scan_arguments = kwargs - assert kwargs["keys"] is None # Sparse keys must never be intersected. - return iter(self.rows) - - -class FakeApi: - def __init__(self, run): - self.run_object = run - - def run(self, path): - assert path == "team/project/run" - return self.run_object - - -def inspect(run, **kwargs): - return health.collect_health(FakeApi(run), "team/project/run", **kwargs) - - -def test_sparse_latest_metrics_and_optimizer_axis_use_bounded_history(): - run = FakeRun( - [ - {"_step": 20544, "trainer/global_step": 200000, "Train/loss": 0.2}, - {"_step": 20545, "trainer/global_step": 200010, "Schedule/flow": 0.01}, - ] - ) - result = inspect( - run, - metrics=["Train/loss", "Schedule/flow"], - window=20, - expected_config={"run_provenance.source_commit": "exact-source"}, - ) - assert result["status"] == "PASS" - assert result["optimizer_step"] == 200010 - assert result["wandb_history_step"] == 20545 - assert result["metrics"]["Train/loss"]["optimizer_step"] == 200000 - assert run.scan_arguments == { - "keys": None, - "min_step": 20526, - "max_step": 20546, - "page_size": 20, - } - - -def test_latest_nan_is_not_hidden_by_an_earlier_finite_value(): - run = FakeRun( - [ - {"_step": 20544, "trainer/global_step": 20, "loss": 0.1}, - {"_step": 20545, "trainer/global_step": 30, "loss": float("nan")}, - ] - ) - result = inspect(run, metrics=["loss"]) - assert result["status"] == "ERROR" - assert result["metrics"]["loss"]["finite"] is False - - -def test_summary_only_metric_is_reported_without_inventing_a_step(): - run = FakeRun([{"_step": 20545, "trainer/global_step": 20}], {"Valid/loss": 0.3}) - result = inspect(run, metrics=["Valid/loss"]) - assert result["status"] == "WARN" - assert result["metrics"]["Valid/loss"]["source"] == "summary_only" - assert result["metrics"]["Valid/loss"]["optimizer_step"] is None - - -def test_missing_and_wrong_config_identity_fail_closed(): - run = FakeRun([{"trainer/global_step": 20}]) - result = inspect( - run, expected_config={"run_provenance.source_commit": "wrong", "missing": 1} - ) - assert result["status"] == "ERROR" - assert len(result["errors"]) == 2 - - -def test_history_counter_is_never_substituted_for_missing_optimizer_step(): - result = inspect(FakeRun([{"_step": 20545, "loss": 0.1}]), metrics=["loss"]) - assert result["status"] == "INCOMPLETE" - assert result["optimizer_step"] is None - - -def test_recent_resume_regression_and_stale_metrics_are_visible(): - run = FakeRun( - [ - {"_step": 20544, "trainer/global_step": 200000, "_timestamp": 10}, - {"_step": 20545, "trainer/global_step": 160000, "_timestamp": 20}, - ] - ) - result = inspect(run, max_age_seconds=30, now=100) - assert result["status"] == "WARN" - assert result["optimizer_step_regressions"] == [ - {"from": 200000, "to": 160000, "wandb_step": 20545} - ] - assert result["latest_history_age_seconds"] == 80 - - -def test_no_unbounded_history_request(): - with pytest.raises(ValueError, match="window"): - inspect(FakeRun([]), window=1000000) diff --git a/tests/test_yam_gpu_uuid_contract.py b/tests/test_yam_gpu_uuid_contract.py deleted file mode 100644 index be82ec9e1d..0000000000 --- a/tests/test_yam_gpu_uuid_contract.py +++ /dev/null @@ -1,67 +0,0 @@ -"""CPU-only contract checks; no CUDA or dataset initialization.""" - -import ast -import re -from pathlib import Path - -import pytest - - -def helper(): - path = Path(__file__).resolve().parents[1] / "scripts/ice/ice_gpu_probe.py" - tree = ast.parse(path.read_text()) - function = next( - n - for n in tree.body - if isinstance(n, ast.FunctionDef) and n.name == "nvml_gpu_uuid" - ) - namespace = {"re": re} - exec( - compile(ast.Module(body=[function], type_ignores=[]), str(path), "exec"), - namespace, - ) - return namespace["nvml_gpu_uuid"] - - -@pytest.mark.parametrize("prefix", ["", "GPU-"]) -def test_actual_cuda_uuid_is_normalized(prefix): - bare = "12345678-1234-1234-1234-123456789abc" - assert helper()(prefix + bare) == "GPU-" + bare - - -@pytest.mark.parametrize( - "value", - [ - "0", - "MIG-1234", - "GPU-", - None, - b"1234", - "GPU-12345678-1234-1234-1234-123456789abc; x", - ], -) -def test_physical_indices_or_unknown_uuid_rejected(value): - with pytest.raises(RuntimeError): - helper()(value) - - -@pytest.mark.parametrize("text", ["f8ae3ac1-0e5a-3e23-c619-d06dd44000a2", "invalid"]) -def test_torch_cuuuid_wrapper_keeps_strict_uuid_validation(text): - wrapper = type( - "_CUuuid", (), {"__module__": "torch._C", "__str__": lambda self: text} - ) - if text == "invalid": - with pytest.raises(RuntimeError): - helper()(wrapper()) - else: - assert helper()(wrapper()) == "GPU-" + text - - -def test_unrelated_stringifiable_object_is_rejected(): - wrapper = type( - "OtherUUID", - (), - {"__str__": lambda self: "f8ae3ac1-0e5a-3e23-c619-d06dd44000a2"}, - ) - with pytest.raises(RuntimeError): - helper()(wrapper())