Files
PSO/test/heavy_pso_autoresearch.py
jung-geun 813433000a feat: modernize PSO and add convergence research
Migrate the package and examples to the tensor-native PyTorch implementation, add benchmark evidence, and add the guarded post-training convergence protocol with TensorBoard progress monitoring and hash-verified recovery.

Constraint: Preserve one-shot official-test sealing and auditable research artifacts

Rejected: Commit local .omc runs and downloaded datasets | multi-gigabyte runtime state is machine-local

Confidence: high

Scope-risk: broad

Not-tested: Production CUDA run on pieroot-server
2026-09-07 22:03:25 +09:00

1322 lines
55 KiB
Python

"""
Equalized Signed-Hash Subspace Experiment Runner for Heavy Task Autoresearch.
Protocol Version: HEAVY-PSO-AUTORESEARCH 1.0.0
Runs matched equalized signed-hash subspace PSO experiments across four candidate
latent ratios (0.5, 0.25, 0.125, 0.03125) and four heavy workloads:
- mnist_compact (Base geometry: G6)
- mnist_wide (Base geometry: G5)
- fashion_compact (Base geometry: G6)
- fashion_wide (Base geometry: G5)
Configurations are matched to confirmation runs (12 particles, 80 epochs, fixed 10k, seeds 101-103).
Official test splits are NEVER loaded or evaluated (official_test_evaluations = 0).
"""
from __future__ import annotations
import argparse
import json
import hashlib
import math
import sys
import time
from pathlib import Path
from typing import Any, Dict, List, Optional, Sequence, Union
import numpy as np
import torch
import torch.nn as nn
# Ensure test directory and repo root are in Python path
REPO_ROOT = Path(__file__).resolve().parent.parent if Path(__file__).resolve().parent.name != "PSO" else Path(__file__).resolve().parent
TEST_DIR = REPO_ROOT / "test"
if str(TEST_DIR) not in sys.path:
sys.path.insert(0, str(TEST_DIR))
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
from benchmark_suite import (
calc_stats,
compute_model_fingerprint,
get_hardware_provenance,
resolve_execution_device,
save_json_atomic,
)
from deep_pso_v6 import (
V6GeometryConfig,
V6LatentTransform,
compute_equalized_subspace_radius,
get_v6_geometry_table,
run_v6_pso,
artifact_safe_run,
)
from heavy_task_feasibility import (
WORKLOADS,
create_model,
prepare_heavy_task_data,
)
# Protocol Constant
AUTORESEARCH_PROTOCOL_VERSION = "HEAVY-PSO-AUTORESEARCH 1.0.0"
# Defaults
DEFAULT_RATIOS = (1.0, 0.5, 0.25, 0.125, 0.03125)
DEFAULT_PARTICLES = 12
DEFAULT_EPOCHS = 80
DEFAULT_SUBSET_SIZE = 10000
DEFAULT_SEEDS = (101, 102, 103)
DEFAULT_SPLIT_SEED = 20260902
DEFAULT_GEOMETRY_POLICY = "recovered"
DEFAULT_PROJECTION_SCOPE = "global"
PROJECTION_SCOPES = ("global", "tensor_local", "balanced_global", "two_hash_global", "largest_tensor_hash", "largest_tensor_row_hash", "adjacent_pair", "adjacent_difference")
DEFAULT_PROJECTION_SEED_MODE = "coupled"
PROJECTION_SEED_MODES = ("coupled", "fixed", "explicit")
DEFAULT_GEOMETRY_MULTIPLIER = 1.0
def validate_geometry_multiplier(geometry_multiplier: Any) -> None:
"""
Validates that geometry_multiplier is a finite positive float (> 0).
Raises ValueError on non-numeric, non-finite, or non-positive values.
"""
if (
not isinstance(geometry_multiplier, (int, float))
or isinstance(geometry_multiplier, bool)
or not math.isfinite(geometry_multiplier)
or geometry_multiplier <= 0
):
raise ValueError(
f"geometry_multiplier must be a finite positive float (> 0), got {geometry_multiplier!r}"
)
def parse_projection_seed_arg(val: Any) -> Optional[Union[int, Dict[str, int]]]:
"""
Parses projection_seed parameter or CLI arg into None, int, or Dict[str, int].
Accepts integer values, integer strings, JSON dict strings, or comma/colon key-value strings.
"""
if val is None or val == "" or val == "None":
return None
if isinstance(val, (int, dict)):
return val
if isinstance(val, str):
val_str = val.strip()
if not val_str:
return None
try:
return int(val_str)
except ValueError:
pass
if val_str.startswith("{") and val_str.endswith("}"):
try:
parsed = json.loads(val_str)
if isinstance(parsed, dict):
return {str(k): int(v) for k, v in parsed.items()}
except (json.JSONDecodeError, ValueError, TypeError) as exc:
raise ValueError(f"Failed to parse projection_seed JSON dict string '{val}': {exc}")
if ":" in val_str or "=" in val_str:
res = {}
for item in val_str.replace(";", ",").split(","):
item = item.strip()
if not item:
continue
if ":" in item:
k, v = item.split(":", 1)
elif "=" in item:
k, v = item.split("=", 1)
else:
raise ValueError(f"Invalid key-value projection_seed string item '{item}'")
res[k.strip()] = int(v.strip())
if res:
return res
raise ValueError(f"Cannot parse projection_seed value: {val!r}")
def validate_projection_seed_config(
projection_seed_mode: str,
projection_seed: Optional[Union[int, Dict[str, int]]],
) -> None:
"""
Validates projection_seed_mode and projection_seed invariants.
Raises ValueError on invalid modes, missing explicit seeds, out-of-range explicit seeds,
or seeds supplied to non-explicit modes.
"""
if projection_seed_mode not in PROJECTION_SEED_MODES:
raise ValueError(
f"Invalid projection_seed_mode '{projection_seed_mode}'. Must be one of {list(PROJECTION_SEED_MODES)}"
)
if projection_seed_mode == "explicit":
if projection_seed is None:
raise ValueError(
"projection_seed must be provided when projection_seed_mode is 'explicit'"
)
if isinstance(projection_seed, int) and not isinstance(projection_seed, bool):
if not (0 <= projection_seed < (2**31 - 1)):
raise ValueError(
f"projection_seed must be a non-negative integer < 2**31-1, got {projection_seed!r}"
)
elif isinstance(projection_seed, dict):
if not projection_seed:
raise ValueError("projection_seed dictionary cannot be empty")
expected_keys = set(WORKLOADS.keys())
provided_keys = set(projection_seed.keys())
missing_keys = expected_keys - provided_keys
unknown_keys = provided_keys - expected_keys
if missing_keys or unknown_keys:
details = []
if missing_keys:
details.append(f"missing required workload key(s) {sorted(missing_keys)}")
if unknown_keys:
details.append(f"Unknown workload_id key(s) {sorted(unknown_keys)}")
raise ValueError(
f"projection_seed dictionary must contain exactly WORKLOADS keys ({sorted(expected_keys)}); "
+ ", ".join(details)
)
for k, v in projection_seed.items():
if (
not isinstance(v, int)
or isinstance(v, bool)
or not (0 <= v < (2**31 - 1))
):
raise ValueError(
f"projection_seed for workload '{k}' must be a non-negative integer < 2**31-1, got {v!r}"
)
else:
raise ValueError(
f"projection_seed must be a non-negative integer < 2**31-1 or a "
f"Dict[str, int], got {projection_seed!r}"
)
else:
if projection_seed is not None:
raise ValueError(
f"projection_seed can only be provided when projection_seed_mode is 'explicit', got mode='{projection_seed_mode}' and projection_seed={projection_seed!r}"
)
def parse_projection_scope_arg(val: Any) -> Union[str, Dict[str, str]]:
"""
Parses projection_scope parameter or CLI arg into a string or Dict[str, str].
Accepts valid scope strings, JSON dict strings, or comma/colon key-value strings.
"""
if val is None or val == "":
return DEFAULT_PROJECTION_SCOPE
if isinstance(val, dict):
return {str(k): str(v) for k, v in val.items()}
if isinstance(val, str):
val_str = val.strip()
if not val_str:
return DEFAULT_PROJECTION_SCOPE
if val_str in PROJECTION_SCOPES:
return val_str
if val_str.startswith("{") and val_str.endswith("}"):
try:
parsed = json.loads(val_str)
if isinstance(parsed, dict):
return {str(k): str(v) for k, v in parsed.items()}
except Exception as exc:
raise ValueError(f"Failed to parse projection_scope JSON dict string '{val}': {exc}")
if ":" in val_str or "=" in val_str:
res = {}
for item in val_str.replace(";", ",").split(","):
item = item.strip()
if not item:
continue
if ":" in item:
k, v = item.split(":", 1)
elif "=" in item:
k, v = item.split("=", 1)
else:
raise ValueError(f"Invalid key-value projection_scope string item '{item}'")
res[k.strip()] = v.strip()
if res:
return res
return val_str
raise ValueError(f"Cannot parse projection_scope value: {val!r}")
def validate_projection_scope_config(projection_scope: Any) -> None:
"""
Validates projection_scope string or dictionary config.
Raises ValueError if string is not in PROJECTION_SCOPES, or if dict
keys do not match WORKLOADS exactly or values are not in PROJECTION_SCOPES.
"""
if isinstance(projection_scope, str):
if projection_scope not in PROJECTION_SCOPES:
raise ValueError(
f"Invalid projection_scope '{projection_scope}'. Must be one of {list(PROJECTION_SCOPES)}"
)
elif isinstance(projection_scope, dict):
if not projection_scope:
raise ValueError("projection_scope dictionary cannot be empty")
if set(projection_scope.keys()) != set(WORKLOADS.keys()):
missing = sorted(list(set(WORKLOADS.keys()) - set(projection_scope.keys())))
extra = sorted(list(set(projection_scope.keys()) - set(WORKLOADS.keys())))
details = []
if missing:
details.append(f"missing keys {missing}")
if extra:
details.append(f"unknown keys {extra}")
raise ValueError(
f"projection_scope dictionary must contain exact workload keys {sorted(list(WORKLOADS.keys()))}, got {', '.join(details)}"
)
for k, v in projection_scope.items():
if v not in PROJECTION_SCOPES:
raise ValueError(
f"Invalid projection_scope '{v}' for workload '{k}'. Must be one of {list(PROJECTION_SCOPES)}"
)
else:
raise ValueError(
f"projection_scope must be a string or Dict[str, str], got {projection_scope!r}"
)
def get_effective_projection_scope(
projection_scope: Union[str, Dict[str, str]],
workload_id: str,
) -> str:
"""
Returns the effective projection scope string for a given workload.
"""
if isinstance(projection_scope, dict):
if workload_id not in projection_scope:
raise ValueError(f"Missing workload_id '{workload_id}' in projection_scope dict")
return str(projection_scope[workload_id])
return str(projection_scope)
def allocate_tensor_latent_dims(
param_numels: Sequence[int],
aggregate_latent_dim: int,
) -> List[int]:
"""
Allocates aggregate_latent_dim across parameter tensors deterministically,
proportionally to tensor numel, with at least one coordinate per tensor,
no tensor exceeding numel, and exact sum aggregate_latent_dim.
"""
total_dim = sum(param_numels)
if not (0 < aggregate_latent_dim <= total_dim):
raise ValueError(
f"aggregate_latent_dim must be in (0, {total_dim}], got {aggregate_latent_dim}"
)
num_tensors = len(param_numels)
if aggregate_latent_dim < num_tensors:
raise ValueError(
f"aggregate_latent_dim ({aggregate_latent_dim}) must be at least number of parameter tensors ({num_tensors})"
)
if aggregate_latent_dim == total_dim:
return list(param_numels)
quotas = [aggregate_latent_dim * n / total_dim for n in param_numels]
allocs = [max(1, min(n, int(math.floor(q)))) for n, q in zip(param_numels, quotas)]
current_sum = sum(allocs)
if current_sum < aggregate_latent_dim:
deficit = aggregate_latent_dim - current_sum
candidates = [i for i in range(num_tensors) if allocs[i] < param_numels[i]]
candidates.sort(
key=lambda i: (quotas[i] - math.floor(quotas[i]), param_numels[i], -i),
reverse=True,
)
for i in candidates[:deficit]:
allocs[i] += 1
elif current_sum > aggregate_latent_dim:
surplus = current_sum - aggregate_latent_dim
candidates = [i for i in range(num_tensors) if allocs[i] > 1]
candidates.sort(
key=lambda i: (quotas[i] - math.floor(quotas[i]), param_numels[i], -i),
reverse=False,
)
for i in candidates[:surplus]:
allocs[i] -= 1
assert sum(allocs) == aggregate_latent_dim, (
f"Allocation sum {sum(allocs)} does not match aggregate_latent_dim {aggregate_latent_dim}"
)
assert all(1 <= a <= n for a, n in zip(allocs, param_numels)), (
f"Allocation bounds violated: {allocs} vs numels {param_numels}"
)
return allocs
class TensorLocalLatentTransform(V6LatentTransform):
"""
Subclass of V6LatentTransform that isolates signed-hash coordinates within each model
parameter tensor, mapping each tensor's parameters only to its assigned contiguous latent slice.
"""
def __init__(
self,
base_model: nn.Module,
geom_config: V6GeometryConfig,
device: torch.device,
):
super().__init__(base_model, geom_config, device)
if not self.is_full:
self.tensor_latent_dims = allocate_tensor_latent_dims(
self.param_numels, self.latent_dim
)
k_indices = np.zeros(self.total_dim, dtype=np.int64)
h2_signs = np.zeros(self.total_dim, dtype=np.float32)
seed_offset = (
geom_config.projection_seed
if geom_config.projection_seed is not None
else 0
)
j_offset = 0
l_offset = 0
for numel, d_m in zip(self.param_numels, self.tensor_latent_dims):
j_local = np.arange(numel, dtype=np.int64)
j_global = j_offset + j_local
h1 = ((j_global + 1 + seed_offset) * 2654435761) % (2**32)
k_local = h1 % d_m
k_indices[j_global] = l_offset + k_local
h2 = ((j_global + 1 + seed_offset) * 1597334677) % (2**32)
h2_signs[j_global] = np.where((h2 % 2) == 0, 1.0, -1.0)
j_offset += numel
l_offset += d_m
bin_counts = np.bincount(k_indices, minlength=self.latent_dim)
count_per_j = bin_counts[k_indices]
scale_per_j = 1.0 / np.sqrt(np.maximum(count_per_j, 1))
combined_weights = h2_signs * scale_per_j
self.k_indices = torch.tensor(k_indices, dtype=torch.long, device=device)
self.weights = torch.tensor(combined_weights, dtype=torch.float32, device=device)
else:
self.tensor_latent_dims = list(self.param_numels)
class BalancedGlobalLatentTransform(V6LatentTransform):
"""
Subclass of V6LatentTransform that enforces balanced parameter occupancy
across global latent coordinate buckets using a deterministic permutation and sign assignment.
"""
def __init__(
self,
base_model: nn.Module,
geom_config: V6GeometryConfig,
device: torch.device,
):
super().__init__(base_model, geom_config, device)
if not self.is_full:
seed_offset = (
geom_config.projection_seed
if geom_config.projection_seed is not None
else 0
)
rng = np.random.RandomState(seed_offset)
perm = rng.permutation(self.total_dim)
k_indices = np.zeros(self.total_dim, dtype=np.int64)
k_indices[perm] = np.arange(self.total_dim, dtype=np.int64) % self.latent_dim
h2_signs = rng.choice(np.array([1.0, -1.0], dtype=np.float32), size=self.total_dim)
bin_counts = np.bincount(k_indices, minlength=self.latent_dim)
count_per_j = bin_counts[k_indices]
scale_per_j = 1.0 / np.sqrt(np.maximum(count_per_j, 1))
combined_weights = h2_signs * scale_per_j
self.k_indices = torch.tensor(k_indices, dtype=torch.long, device=device)
self.weights = torch.tensor(combined_weights, dtype=torch.float32, device=device)
class TwoHashGlobalLatentTransform(V6LatentTransform):
"""
Subclass of V6LatentTransform that maps each parameter to two distinct global latent
coordinate buckets using independent deterministic signed-hash mappings, normalized by
per-bucket 1/sqrt(count) weights and decoded as (term1 + term2)/sqrt(2).
"""
def __init__(
self,
base_model: nn.Module,
geom_config: V6GeometryConfig,
device: torch.device,
):
super().__init__(base_model, geom_config, device)
if not self.is_full:
j_indices = np.arange(self.total_dim, dtype=np.int64)
seed_offset = (
geom_config.projection_seed
if geom_config.projection_seed is not None
else 0
)
# Hash Map 1 (Primary signed hash projection)
h1_1 = ((j_indices + 1 + seed_offset) * 2654435761) % (2**32)
k1_indices = h1_1 % self.latent_dim
h1_2 = ((j_indices + 1 + seed_offset) * 1597334677) % (2**32)
signs1 = np.where((h1_2 % 2) == 0, 1.0, -1.0)
bin_counts1 = np.bincount(k1_indices, minlength=self.latent_dim)
count_per_j1 = bin_counts1[k1_indices]
scale_per_j1 = 1.0 / np.sqrt(np.maximum(count_per_j1, 1))
combined_weights1 = signs1 * scale_per_j1
# Hash Map 2 (Secondary independent signed hash projection)
h2_1 = ((j_indices + 1 + seed_offset) * 2246822519) % (2**32)
if self.latent_dim > 1:
offset = 1 + (h2_1 % (self.latent_dim - 1))
k2_indices = (k1_indices + offset) % self.latent_dim
else:
k2_indices = np.zeros(self.total_dim, dtype=np.int64)
h2_2 = ((j_indices + 1 + seed_offset) * 3266489917) % (2**32)
signs2 = np.where(((h2_2 >> 16) % 2) == 0, 1.0, -1.0)
bin_counts2 = np.bincount(k2_indices, minlength=self.latent_dim)
count_per_j2 = bin_counts2[k2_indices]
scale_per_j2 = 1.0 / np.sqrt(np.maximum(count_per_j2, 1))
combined_weights2 = signs2 * scale_per_j2
self.k1_indices = torch.tensor(k1_indices, dtype=torch.long, device=device)
self.weights1 = torch.tensor(combined_weights1, dtype=torch.float32, device=device)
self.k2_indices = torch.tensor(k2_indices, dtype=torch.long, device=device)
self.weights2 = torch.tensor(combined_weights2, dtype=torch.float32, device=device)
# Backward compatibility aliases
self.k_indices = self.k1_indices
self.weights = self.weights1
def decode(self, Z: torch.Tensor) -> torch.Tensor:
"""
Transforms latent batch Z (N, d) into full parameter batch (N, D).
When not full, term1 = Z[:, k1] * weights1, term2 = Z[:, k2] * weights2,
delta = (term1 + term2) / sqrt(2).
theta = base_vec + scale_vec * delta
"""
if self.is_full:
delta = Z
else:
term1 = Z[:, self.k1_indices] * self.weights1
term2 = Z[:, self.k2_indices] * self.weights2
delta = (term1 + term2) / math.sqrt(2.0)
return self.base_vec + self.scale_vec * delta
class LargestTensorHashLatentTransform(V6LatentTransform):
"""
Subclass of V6LatentTransform that isolates the single largest parameter tensor by numel
(with stable first-index tie break) for signed-hash projection, while assigning every parameter
in all other tensors a unique direct latent coordinate with weight 1.0.
"""
def __init__(
self,
base_model: nn.Module,
geom_config: V6GeometryConfig,
device: torch.device,
):
super().__init__(base_model, geom_config, device)
if not self.is_full:
largest_idx = int(np.argmax(self.param_numels))
protected_dim = sum(
numel for i, numel in enumerate(self.param_numels) if i != largest_idx
)
residual_dim = self.latent_dim - protected_dim
if residual_dim < 1:
raise ValueError(
f"latent_dim ({self.latent_dim}) must be greater than protected non-largest parameter dimension ({protected_dim}) "
f"to provide at least 1 residual latent coordinate for the largest tensor (index {largest_idx}, numel {self.param_numels[largest_idx]})"
)
k_indices = np.zeros(self.total_dim, dtype=np.int64)
weights = np.zeros(self.total_dim, dtype=np.float32)
seed_offset = (
geom_config.projection_seed
if geom_config.projection_seed is not None
else 0
)
j_offset = 0
direct_coord = 0
for i, numel in enumerate(self.param_numels):
j_indices_tensor = np.arange(j_offset, j_offset + numel, dtype=np.int64)
if i != largest_idx:
k_indices[j_indices_tensor] = direct_coord + np.arange(numel, dtype=np.int64)
weights[j_indices_tensor] = 1.0
direct_coord += numel
else:
h1 = ((j_indices_tensor + 1 + seed_offset) * 2654435761) % (2**32)
k_rel = h1 % residual_dim
k_indices[j_indices_tensor] = protected_dim + k_rel
h2 = ((j_indices_tensor + 1 + seed_offset) * 1597334677) % (2**32)
h2_signs = np.where((h2 % 2) == 0, 1.0, -1.0)
weights[j_indices_tensor] = h2_signs
j_offset += numel
bin_counts = np.bincount(k_indices, minlength=self.latent_dim)
count_per_j = bin_counts[k_indices]
scale_per_j = 1.0 / np.sqrt(np.maximum(count_per_j, 1))
combined_weights = weights * scale_per_j
self.k_indices = torch.tensor(k_indices, dtype=torch.long, device=device)
self.weights = torch.tensor(combined_weights, dtype=torch.float32, device=device)
class LargestTensorRowHashLatentTransform(V6LatentTransform):
"""
Subclass of V6LatentTransform that isolates the single largest parameter tensor by numel
(with stable first-index tie break) for row-partitioned signed-hash projection across its first dimension
(rows), while assigning every parameter in all other tensors a unique direct latent coordinate with weight 1.0.
"""
def __init__(
self,
base_model: nn.Module,
geom_config: V6GeometryConfig,
device: torch.device,
):
super().__init__(base_model, geom_config, device)
largest_idx = int(np.argmax(self.param_numels))
shape = self.param_shapes[largest_idx]
num_rows = shape[0] if len(shape) >= 2 else 1
largest_numel = self.param_numels[largest_idx]
if len(shape) >= 2:
elements_per_row = largest_numel // num_rows
row_numels = [elements_per_row] * num_rows
else:
row_numels = [largest_numel]
if not self.is_full:
protected_dim = sum(
numel for i, numel in enumerate(self.param_numels) if i != largest_idx
)
residual_dim = self.latent_dim - protected_dim
if residual_dim < num_rows:
raise ValueError(
f"latent_dim ({self.latent_dim}) must be at least protected dimension ({protected_dim}) "
f"+ number of rows ({num_rows}) for largest tensor row hashing, got residual_dim {residual_dim}"
)
row_latent_dims = allocate_tensor_latent_dims(row_numels, residual_dim)
k_indices = np.zeros(self.total_dim, dtype=np.int64)
weights = np.zeros(self.total_dim, dtype=np.float32)
seed_offset = (
geom_config.projection_seed
if geom_config.projection_seed is not None
else 0
)
j_offset = 0
direct_coord = 0
for i, numel in enumerate(self.param_numels):
if i != largest_idx:
j_indices_tensor = np.arange(j_offset, j_offset + numel, dtype=np.int64)
k_indices[j_indices_tensor] = direct_coord + np.arange(numel, dtype=np.int64)
weights[j_indices_tensor] = 1.0
direct_coord += numel
j_offset += numel
else:
row_slice_start = protected_dim
for r, (r_numel, r_dim) in enumerate(zip(row_numels, row_latent_dims)):
j_indices_row = np.arange(j_offset, j_offset + r_numel, dtype=np.int64)
h1 = ((j_indices_row + 1 + seed_offset) * 2654435761) % (2**32)
k_rel = h1 % r_dim
k_indices[j_indices_row] = row_slice_start + k_rel
h2 = ((j_indices_row + 1 + seed_offset) * 1597334677) % (2**32)
h2_signs = np.where((h2 % 2) == 0, 1.0, -1.0)
weights[j_indices_row] = h2_signs
j_offset += r_numel
row_slice_start += r_dim
bin_counts = np.bincount(k_indices, minlength=self.latent_dim)
count_per_j = bin_counts[k_indices]
scale_per_j = 1.0 / np.sqrt(np.maximum(count_per_j, 1))
combined_weights = weights * scale_per_j
self.k_indices = torch.tensor(k_indices, dtype=torch.long, device=device)
self.weights = torch.tensor(combined_weights, dtype=torch.float32, device=device)
self.row_latent_dims = row_latent_dims
else:
self.row_latent_dims = row_numels
class AdjacentPairLatentTransform(V6LatentTransform):
"""
Subclass of V6LatentTransform that maps each parameter tensor independently
by assigning consecutive pairs of parameters to one unique latent coordinate with weights 1/sqrt(2),
and a final unpaired parameter (if any) to weight 1.0, concatenating tensor coordinate ranges without sharing.
"""
def __init__(
self,
base_model: nn.Module,
geom_config: V6GeometryConfig,
device: torch.device,
):
super().__init__(base_model, geom_config, device)
if not self.is_full:
required_dim = sum(math.ceil(n / 2) for n in self.param_numels)
if self.latent_dim != required_dim:
raise ValueError(
f"AdjacentPairLatentTransform requires latent_dim == sum(ceil(numel_i/2)) = {required_dim}, got {self.latent_dim}"
)
k_indices = np.zeros(self.total_dim, dtype=np.int64)
weights = np.zeros(self.total_dim, dtype=np.float32)
inv_sqrt2 = 1.0 / math.sqrt(2.0)
j_offset = 0
l_offset = 0
self.tensor_latent_dims = []
for numel in self.param_numels:
d_m = math.ceil(numel / 2)
self.tensor_latent_dims.append(d_m)
for p in range(numel):
j = j_offset + p
k_local = p // 2
k_indices[j] = l_offset + k_local
if p % 2 == 0 and p == numel - 1:
weights[j] = 1.0
else:
weights[j] = inv_sqrt2
j_offset += numel
l_offset += d_m
self.k_indices = torch.tensor(k_indices, dtype=torch.long, device=device)
self.weights = torch.tensor(weights, dtype=torch.float32, device=device)
else:
self.tensor_latent_dims = list(self.param_numels)
class AdjacentDifferenceLatentTransform(AdjacentPairLatentTransform):
"""
Subclass of AdjacentPairLatentTransform that maps each parameter tensor independently
by assigning consecutive pairs of parameters to one unique latent coordinate with opposite weights
(+1/sqrt(2), -1/sqrt(2)), and a final unpaired parameter (if any) to weight 1.0,
concatenating tensor coordinate ranges without sharing.
"""
def __init__(
self,
base_model: nn.Module,
geom_config: V6GeometryConfig,
device: torch.device,
):
try:
super().__init__(base_model, geom_config, device)
except ValueError as e:
raise ValueError(
str(e).replace("AdjacentPairLatentTransform", "AdjacentDifferenceLatentTransform")
) from None
if not self.is_full:
weights = self.weights.clone()
j_offset = 0
for numel in self.param_numels:
for p in range(1, numel, 2):
weights[j_offset + p] = -weights[j_offset + p]
j_offset += numel
self.weights = weights
def format_ratio_id(
ratio: float,
geometry_policy: str = DEFAULT_GEOMETRY_POLICY,
projection_scope: Union[str, Dict[str, str]] = DEFAULT_PROJECTION_SCOPE,
projection_seed_mode: str = DEFAULT_PROJECTION_SEED_MODE,
projection_seed: Optional[Union[int, Dict[str, int]]] = None,
geometry_multiplier: float = DEFAULT_GEOMETRY_MULTIPLIER,
) -> str:
validate_geometry_multiplier(geometry_multiplier)
validate_projection_seed_config(projection_seed_mode, projection_seed)
validate_projection_scope_config(projection_scope)
r_str = f"{ratio:g}"
base_id = f"aligned_r{r_str}" if geometry_policy == "baseline_aligned" else f"r{r_str}"
if isinstance(projection_scope, dict):
unique_scopes = set(projection_scope.values())
if len(unique_scopes) == 1:
eff_scope = next(iter(unique_scopes))
if eff_scope == "tensor_local":
base_id = f"local_{base_id}"
elif eff_scope == "balanced_global":
base_id = f"balanced_{base_id}"
elif eff_scope == "two_hash_global":
base_id = f"two_hash_{base_id}"
elif eff_scope == "largest_tensor_hash":
base_id = f"largest_tensor_hash_{base_id}"
elif eff_scope == "largest_tensor_row_hash":
base_id = f"largest_tensor_row_hash_{base_id}"
elif eff_scope == "adjacent_pair":
base_id = f"adjacent_pair_{base_id}"
elif eff_scope == "adjacent_difference":
base_id = f"adjacent_difference_{base_id}"
else:
base_id = f"mixed_{base_id}"
elif projection_scope == "tensor_local":
base_id = f"local_{base_id}"
elif projection_scope == "balanced_global":
base_id = f"balanced_{base_id}"
elif projection_scope == "two_hash_global":
base_id = f"two_hash_{base_id}"
elif projection_scope == "largest_tensor_hash":
base_id = f"largest_tensor_hash_{base_id}"
elif projection_scope == "largest_tensor_row_hash":
base_id = f"largest_tensor_row_hash_{base_id}"
elif projection_scope == "adjacent_pair":
base_id = f"adjacent_pair_{base_id}"
elif projection_scope == "adjacent_difference":
base_id = f"adjacent_difference_{base_id}"
if projection_seed_mode == "fixed":
base_id = f"fixed_{base_id}"
elif projection_seed_mode == "explicit":
if isinstance(projection_seed, dict):
base_id = f"pexplicit_{base_id}"
else:
base_id = f"p{projection_seed}_{base_id}"
if float(geometry_multiplier) != 1.0:
g_str = f"{geometry_multiplier:g}"
base_id = f"g{g_str}_{base_id}"
return base_id
# Geometry Policies Assignment
GEOMETRY_POLICIES: Dict[str, Dict[str, str]] = {
"recovered": {
"mnist_compact": "G6",
"mnist_wide": "G5",
"fashion_compact": "G6",
"fashion_wide": "G5",
},
"baseline_aligned": {
"mnist_compact": "G8",
"mnist_wide": "G5",
"fashion_compact": "G8",
"fashion_wide": "G5",
},
}
# Base Candidate Geometry Assignment (retained for backward compatibility)
BASE_GEOMETRIES: Dict[str, str] = GEOMETRY_POLICIES["recovered"]
BASELINE_METHODS: Dict[str, str] = {
"mnist_compact": "G8",
"mnist_wide": "G5",
"fashion_compact": "G8",
"fashion_wide": "G5",
}
def compute_latent_dim(total_dim: int, ratio: float) -> int:
"""
Computes exact model-relative latent dimension from total parameter count and ratio.
Validates 0 < ratio <= 1.0 and uses deterministic half-up rounding floor(D*ratio+0.5).
Ensures dimension is at least 1 and capped at total_dim.
"""
if not (0.0 < ratio <= 1.0):
raise ValueError(f"ratio must be in (0, 1], got {ratio}")
dim = int(math.floor(total_dim * ratio + 0.5))
return max(1, min(total_dim, dim))
def derive_projection_seed(
workload_id: str,
ratio: float,
seed: int,
projection_salt: str = "",
mode: str = DEFAULT_PROJECTION_SEED_MODE,
projection_seed_mode: Optional[str] = None,
projection_seed: Optional[Union[int, Dict[str, int]]] = None,
) -> int:
"""
Derives a deterministic 32-bit projection seed from workload_id, ratio, swarm seed,
optional projection_salt, and projection seed mode ('coupled', 'fixed', or 'explicit').
In 'explicit' mode, returns projection_seed directly (or projection_seed[workload_id] if a dict).
"""
if projection_seed_mode is not None:
mode = projection_seed_mode
validate_projection_seed_config(mode, projection_seed)
if mode == "explicit":
if isinstance(projection_seed, dict):
if workload_id not in projection_seed:
raise ValueError(f"Missing explicit projection_seed for workload '{workload_id}'")
return projection_seed[workload_id]
assert projection_seed is not None
return projection_seed
parts = [workload_id, f"{ratio:.5f}"]
if mode != "fixed":
parts.append(str(seed))
if projection_salt:
parts.append(projection_salt)
key = ":".join(parts).encode("utf-8")
h = hashlib.sha256(key).hexdigest()
return int(h[:8], 16) % (2**31 - 1)
def construct_equalized_geometry(
base_geom: V6GeometryConfig,
total_dim: int,
latent_dim: int,
projection_seed: int,
ratio_str: str = "",
geometry_multiplier: float = DEFAULT_GEOMETRY_MULTIPLIER,
) -> V6GeometryConfig:
"""
Constructs an equalized subspace geometry configuration from a base geometry (G6 or G5).
Multiplies position radius, launch velocity radius, mutation reset radius, and reflective bound
by sqrt(total_dim / latent_dim) * geometry_multiplier to hold decoded per-parameter variance constant.
"""
validate_geometry_multiplier(geometry_multiplier)
scale_factor = math.sqrt(total_dim / latent_dim) if latent_dim < total_dim else 1.0
effective_mult = scale_factor * float(geometry_multiplier)
return V6GeometryConfig(
config_id=f"{base_geom.config_id}_eq_{ratio_str}",
scale_type=base_geom.scale_type,
init_position_mode=base_geom.init_position_mode,
position_radius=float(base_geom.position_radius * effective_mult),
initial_velocity_radius=float(base_geom.initial_velocity_radius * effective_mult),
mutation_prob=base_geom.mutation_prob,
reset_velocity_radius=float(base_geom.reset_velocity_radius * effective_mult),
reflective_bound=float(base_geom.reflective_bound * effective_mult),
projection_seed=projection_seed,
latent_dim=latent_dim,
description=f"Equalized subspace (ratio={ratio_str}, d={latent_dim}/{total_dim})",
)
def compute_core_swarm_state_bytes(particles: int, latent_dim: int) -> int:
"""
Computes exact core swarm state memory footprint in bytes.
5 tensors (Z, V, M, V_sq, P) of size (particles, latent_dim) float32 (4 bytes/elem).
"""
return 5 * particles * latent_dim * 4
def compute_baseline_core_swarm_state_bytes(
workload_id: str,
particles: int,
total_dim: int,
) -> int:
"""Match the retained baseline method's persistent core-state accounting."""
particle_states = 5 * particles
if BASELINE_METHODS[workload_id] == "G8":
particle_states += 1
return particle_states * total_dim * 4
def run_heavy_pso_autoresearch(
ratios: Sequence[float] = DEFAULT_RATIOS,
particles: int = DEFAULT_PARTICLES,
epochs: int = DEFAULT_EPOCHS,
subset_size: int = DEFAULT_SUBSET_SIZE,
seeds: Sequence[int] = DEFAULT_SEEDS,
geometry_policy: str = DEFAULT_GEOMETRY_POLICY,
device_str: Optional[str] = None,
cache_dir: Optional[Path] = None,
output_path: Optional[Path] = None,
split_seed: int = DEFAULT_SPLIT_SEED,
projection_salt: str = "",
projection_scope: Union[str, Dict[str, str]] = DEFAULT_PROJECTION_SCOPE,
projection_seed_mode: str = DEFAULT_PROJECTION_SEED_MODE,
projection_seed: Optional[Union[int, Dict[str, int]]] = None,
geometry_multiplier: float = DEFAULT_GEOMETRY_MULTIPLIER,
) -> Dict[str, Any]:
"""
Executes the heavy task autoresearch experiment candidate runs across candidate ratios,
workloads, and seeds. Aggregates metrics and atomically writes the candidate JSON artifact.
"""
validate_geometry_multiplier(geometry_multiplier)
validate_projection_seed_config(projection_seed_mode, projection_seed)
validate_projection_scope_config(projection_scope)
if geometry_policy not in GEOMETRY_POLICIES:
raise ValueError(
f"Invalid geometry_policy '{geometry_policy}'. Must be one of {list(GEOMETRY_POLICIES.keys())}"
)
policy_geometries = GEOMETRY_POLICIES[geometry_policy]
start_time = time.time()
device = resolve_execution_device(device_str)
hardware_info = get_hardware_provenance(device)
geom_table = get_v6_geometry_table()
if cache_dir is None:
cache_dir = REPO_ROOT / "result" / "cache"
data_cache: Dict[str, Any] = {}
workload_meta: Dict[str, Any] = {}
# Pre-load datasets and models metadata
for wl_id, wl_cfg in WORKLOADS.items():
if wl_cfg.dataset_name not in data_cache:
x_search, y_search, x_val, y_val, nested_subsets, data_fp, provenance = prepare_heavy_task_data(
dataset_name=wl_cfg.dataset_name,
split_seed=split_seed,
cache_dir=cache_dir,
)
data_cache[wl_cfg.dataset_name] = {
"x_search": x_search,
"y_search": y_search,
"x_val": x_val,
"y_val": y_val,
"nested_subsets": nested_subsets,
"data_fp": data_fp,
"provenance": provenance,
}
bm = create_model(wl_cfg.model_name, seed=41)
param_count = sum(p.numel() for p in bm.parameters())
model_fp = compute_model_fingerprint(bm)
d_info = data_cache[wl_cfg.dataset_name]
eff_proj_seed = (
projection_seed[wl_id]
if isinstance(projection_seed, dict)
else projection_seed
)
workload_meta[wl_id] = {
"workload_id": wl_id,
"dataset_name": wl_cfg.dataset_name,
"model_name": wl_cfg.model_name,
"parameter_count": param_count,
"total_dim": param_count,
"geometry_policy": geometry_policy,
"geometry_multiplier": float(geometry_multiplier),
"projection_scope": get_effective_projection_scope(projection_scope, wl_id),
"projection_seed_mode": str(projection_seed_mode),
"projection_seed": eff_proj_seed,
"base_geometry_id": policy_geometries[wl_id],
"model_fingerprint": model_fp,
"data_fingerprint": d_info["data_fp"],
"split_fingerprint": d_info["provenance"]["split_fingerprint"],
"description": wl_cfg.description,
}
candidate_runs: Dict[str, Dict[str, Any]] = {}
total_runs_executed = 0
total_queries_executed = 0
total_samples_evaluated = 0
for r in ratios:
ratio_id = format_ratio_id(
r,
geometry_policy=geometry_policy,
projection_scope=projection_scope,
projection_seed_mode=projection_seed_mode,
projection_seed=projection_seed,
geometry_multiplier=geometry_multiplier,
)
wl_candidates: Dict[str, Any] = {}
for wl_id, wl_cfg in WORKLOADS.items():
d_info = data_cache[wl_cfg.dataset_name]
x_search, y_search = d_info["x_search"], d_info["y_search"]
x_val, y_val = d_info["x_val"], d_info["y_val"]
nested_subsets = d_info["nested_subsets"]
base_model = create_model(wl_cfg.model_name, seed=41)
total_dim = sum(p.numel() for p in base_model.parameters())
latent_dim = compute_latent_dim(total_dim, r)
base_geom_id = policy_geometries[wl_id]
base_geom = geom_table[base_geom_id]
state_bytes = compute_core_swarm_state_bytes(particles, latent_dim)
baseline_state_bytes = compute_baseline_core_swarm_state_bytes(
workload_id=wl_id,
particles=particles,
total_dim=total_dim,
)
per_seed_runs: List[Dict[str, Any]] = []
for s in seeds:
proj_seed = derive_projection_seed(
wl_id,
r,
s,
projection_salt=projection_salt,
mode=projection_seed_mode,
projection_seed=projection_seed,
)
geom_cfg = construct_equalized_geometry(
base_geom=base_geom,
total_dim=total_dim,
latent_dim=latent_dim,
projection_seed=proj_seed,
ratio_str=ratio_id,
geometry_multiplier=geometry_multiplier,
)
eff_scope = get_effective_projection_scope(projection_scope, wl_id)
if eff_scope == "tensor_local":
transform = TensorLocalLatentTransform(base_model, geom_cfg, device)
elif eff_scope == "balanced_global":
transform = BalancedGlobalLatentTransform(base_model, geom_cfg, device)
elif eff_scope == "two_hash_global":
transform = TwoHashGlobalLatentTransform(base_model, geom_cfg, device)
elif eff_scope == "largest_tensor_hash":
transform = LargestTensorHashLatentTransform(base_model, geom_cfg, device)
elif eff_scope == "largest_tensor_row_hash":
transform = LargestTensorRowHashLatentTransform(base_model, geom_cfg, device)
elif eff_scope == "adjacent_pair":
transform = AdjacentPairLatentTransform(base_model, geom_cfg, device)
elif eff_scope == "adjacent_difference":
transform = AdjacentDifferenceLatentTransform(base_model, geom_cfg, device)
elif eff_scope == "global":
transform = V6LatentTransform(base_model, geom_cfg, device)
else:
raise ValueError(f"Invalid effective projection_scope '{eff_scope}' for workload '{wl_id}'")
res = run_v6_pso(
transform=transform,
base_model=base_model,
x_search=x_search,
y_search=y_search,
x_val=x_val,
y_val=y_val,
nested_subsets=nested_subsets,
schedule_str=f"{subset_size}:{epochs}",
epochs=epochs,
swarm_size=particles,
seed=s,
device=device,
geom_config=geom_cfg,
val_check_interval=10,
)
opt_time = max(float(res["optimization_wall_time_sec"]), 1e-6)
sps = round(float(res["total_sample_evaluations"]) / opt_time, 2)
val_metrics_raw = artifact_safe_run(res.get("val_metrics", {}))
has_valid_val_metrics = (
isinstance(val_metrics_raw, dict)
and len(val_metrics_raw) > 0
and all(
isinstance(v, (int, float)) and math.isfinite(float(v))
for v in val_metrics_raw.values()
)
)
scalar_metrics = [
res.get("val_selected_loss"),
res.get("val_selected_acc"),
res.get("gbest_loss"),
res.get("gbest_acc"),
res.get("wall_time_sec"),
res.get("optimization_wall_time_sec"),
res.get("validation_wall_time_sec"),
sps,
]
has_valid_scalars = all(
v is not None and isinstance(v, (int, float)) and math.isfinite(float(v))
for v in scalar_metrics
)
is_finite = bool(has_valid_val_metrics and has_valid_scalars)
seed_record = {
"seed": int(s),
"projection_seed": int(proj_seed),
"projection_salt": str(projection_salt),
"projection_scope": get_effective_projection_scope(projection_scope, wl_id),
"projection_seed_mode": str(projection_seed_mode),
"geometry_multiplier": float(geometry_multiplier),
"val_selected_loss": float(res["val_selected_loss"]),
"val_selected_acc": float(res["val_selected_acc"]),
"val_metrics": val_metrics_raw,
"gbest_loss": float(res["gbest_loss"]),
"gbest_acc": float(res["gbest_acc"]),
"wall_time_sec": float(res["wall_time_sec"]),
"optimization_wall_time_sec": float(res["optimization_wall_time_sec"]),
"validation_wall_time_sec": float(res["validation_wall_time_sec"]),
"total_queries": int(res["total_queries"]),
"total_sample_evaluations": int(res["total_sample_evaluations"]),
"validation_evaluations": int(res["validation_evaluations"]),
"official_test_evaluations": 0,
"core_swarm_state_bytes": int(state_bytes),
"throughput_samples_per_sec": float(sps),
"is_finite": is_finite,
}
per_seed_runs.append(seed_record)
total_runs_executed += 1
total_queries_executed += int(res["total_queries"])
total_samples_evaluated += int(res["total_sample_evaluations"])
# Statistics calculation across seeds
acc_list = [r_entry["val_selected_acc"] for r_entry in per_seed_runs]
nll_list = [r_entry["val_selected_loss"] for r_entry in per_seed_runs]
brier_list = [r_entry["val_metrics"]["brier"] for r_entry in per_seed_runs if r_entry.get("val_metrics")]
ece_list = [r_entry["val_metrics"]["ece"] for r_entry in per_seed_runs if r_entry.get("val_metrics")]
g_loss_list = [r_entry["gbest_loss"] for r_entry in per_seed_runs]
g_acc_list = [r_entry["gbest_acc"] for r_entry in per_seed_runs]
wall_list = [r_entry["wall_time_sec"] for r_entry in per_seed_runs]
opt_wall_list = [r_entry["optimization_wall_time_sec"] for r_entry in per_seed_runs]
sps_list = [r_entry["throughput_samples_per_sec"] for r_entry in per_seed_runs]
stats = {
"val_acc": calc_stats(acc_list),
"val_nll": calc_stats(nll_list),
"val_brier": calc_stats(brier_list) if brier_list else None,
"val_ece": calc_stats(ece_list) if ece_list else None,
"gbest_loss": calc_stats(g_loss_list),
"gbest_acc": calc_stats(g_acc_list),
"wall_time_sec": calc_stats(wall_list),
"optimization_wall_time_sec": calc_stats(opt_wall_list),
"throughput_samples_per_sec": calc_stats(sps_list),
}
state_ratio = float(state_bytes / baseline_state_bytes)
wl_candidates[wl_id] = {
"candidate_id": ratio_id,
"ratio": float(r),
"workload_id": wl_id,
"geometry_policy": geometry_policy,
"geometry_multiplier": float(geometry_multiplier),
"projection_scope": get_effective_projection_scope(projection_scope, wl_id),
"projection_seed_mode": str(projection_seed_mode),
"projection_seed": (
projection_seed[wl_id]
if isinstance(projection_seed, dict)
else projection_seed
),
"base_geometry_id": base_geom_id,
"total_dim": total_dim,
"latent_dim": latent_dim,
"state_ratio": state_ratio,
"subset_size": subset_size,
"particles": particles,
"epochs": epochs,
"seeds": list(seeds),
"core_swarm_state_bytes": state_bytes,
"baseline_core_swarm_state_bytes": baseline_state_bytes,
"split_seed": int(split_seed),
"data_fingerprint": d_info["data_fp"],
"split_fingerprint": d_info["provenance"]["split_fingerprint"],
"stats": stats,
"per_seed_runs": per_seed_runs,
}
candidate_runs[ratio_id] = wl_candidates
total_wall_time = round(time.time() - start_time, 4)
payload = {
"protocol_version": AUTORESEARCH_PROTOCOL_VERSION,
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"hardware": hardware_info,
"official_test_data_loaded": False,
"official_test_evaluations": 0,
"experiment_config": {
"geometry_policy": geometry_policy,
"geometry_multiplier": float(geometry_multiplier),
"projection_scope": projection_scope,
"projection_seed_mode": str(projection_seed_mode),
"projection_seed": projection_seed,
"projection_salt": str(projection_salt),
"ratios": [float(r) for r in ratios],
"particles": int(particles),
"epochs": int(epochs),
"subset_size": int(subset_size),
"seeds": [int(s) for s in seeds],
"split_seed": int(split_seed),
"total_runs": total_runs_executed,
"total_queries": total_queries_executed,
"total_sample_evaluations": total_samples_evaluated,
"total_wall_time_sec": total_wall_time,
},
"workloads": workload_meta,
"candidate_runs": candidate_runs,
}
if output_path is not None:
save_json_atomic(payload, Path(output_path))
return payload
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="MNIST / FashionMNIST PSO Heavy Autoresearch Candidate Experiment Runner"
)
parser.add_argument("--device", type=str, default=None, help="Device (cpu, mps, cuda)")
parser.add_argument("--cache-dir", type=str, default=None, help="Dataset cache directory")
parser.add_argument(
"--split-seed",
type=int,
default=DEFAULT_SPLIT_SEED,
help=f"Dataset train/validation split seed (default: {DEFAULT_SPLIT_SEED})",
)
parser.add_argument(
"--output",
type=str,
default="benchmark_results/pso_v6_heavy_autoresearch_candidates.json",
help="Output candidate JSON path",
)
parser.add_argument(
"--geometry-policy",
type=str,
default=DEFAULT_GEOMETRY_POLICY,
choices=list(GEOMETRY_POLICIES.keys()),
help="Geometry policy ('recovered' or 'baseline_aligned')",
)
parser.add_argument(
"--ratios",
type=str,
default="1,0.5,0.25,0.125,0.03125",
help="Comma-separated latent subspace ratios",
)
parser.add_argument("--particles", type=int, default=DEFAULT_PARTICLES, help="Swarm size")
parser.add_argument("--epochs", type=int, default=DEFAULT_EPOCHS, help="PSO epochs")
parser.add_argument(
"--subset-size",
type=int,
default=DEFAULT_SUBSET_SIZE,
help="Training subset size",
)
parser.add_argument("--seeds", type=str, default="101,102,103", help="Comma-separated seeds")
parser.add_argument(
"--projection-salt",
type=str,
default="",
help="Optional salt string for projection seed derivation (default: empty)",
)
parser.add_argument(
"--projection-scope",
type=parse_projection_scope_arg,
default=DEFAULT_PROJECTION_SCOPE,
help="Projection scope ('global', 'tensor_local', 'balanced_global', 'two_hash_global', 'largest_tensor_hash', 'largest_tensor_row_hash', 'adjacent_pair', 'adjacent_difference', or workload dict)",
)
parser.add_argument(
"--projection-seed-mode",
type=str,
default=DEFAULT_PROJECTION_SEED_MODE,
choices=list(PROJECTION_SEED_MODES),
help="Projection seed mode ('coupled', 'fixed', or 'explicit')",
)
parser.add_argument(
"--projection-seed",
type=parse_projection_seed_arg,
default=None,
help="Exact nonnegative projection seed (int or dict) for explicit mode",
)
parser.add_argument(
"--geometry-multiplier",
type=float,
default=DEFAULT_GEOMETRY_MULTIPLIER,
help="Geometry radius/bound multiplier (default: 1.0)",
)
return parser
def main():
parser = build_parser()
args = parser.parse_args()
ratios = [float(x.strip()) for x in args.ratios.split(",") if x.strip()]
seeds = [int(x.strip()) for x in args.seeds.split(",") if x.strip()]
cache_path = Path(args.cache_dir) if args.cache_dir else None
out_path = Path(args.output) if args.output else None
run_heavy_pso_autoresearch(
ratios=ratios,
split_seed=args.split_seed,
projection_seed_mode=args.projection_seed_mode,
projection_seed=args.projection_seed,
particles=args.particles,
epochs=args.epochs,
subset_size=args.subset_size,
seeds=seeds,
geometry_policy=args.geometry_policy,
device_str=args.device,
cache_dir=cache_path,
output_path=out_path,
projection_salt=args.projection_salt,
projection_scope=args.projection_scope,
geometry_multiplier=args.geometry_multiplier,
)
if __name__ == "__main__":
main()