| """ |
| In-memory registries for the FastAPI planner service. |
| |
| Two things are cached here for the lifetime of the process: |
| - Maps: named map_id -> quantum.map.Grid and/or quantum.map.Graph, lazily |
| parsed from HDF5 on first use (never eagerly loaded at startup — some |
| maps are 1000x1000). Synthetic maps generally carry both a grid and a |
| graph representation in the same HDF5 file; both are parsed together |
| from a single file read the first time either is requested. |
| - Solvers: named solver key (e.g. "dwave.general") -> solver instance, built |
| once from the config loaded by config_api.py and reused across requests. |
| |
| Both registries are module-level dicts, mutated in place by design (mirrors |
| config_api.py's pattern) so other modules can `from registry import ...` and |
| see live state without re-importing. |
| """ |
| import os |
| from pathlib import Path |
| from typing import Any, Dict, Optional |
|
|
| import quantum |
| from quantum.config.hdf5parser import load_both_from_hdf5 |
| from quantum.map import Grid, Graph |
| from quantum.maps.yaml2HDF5 import generate_map_from_yaml |
| from quantum.solvers.solver_factory import SolverFactory |
|
|
| from config_api import global_solver_configs |
|
|
| |
| |
| |
| |
| |
| QUANTUM_ROOT = Path(quantum.__file__).resolve().parent |
|
|
| |
| |
| |
| |
| |
| MAP_CACHE_ROOT = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")) / "spooky" / "maps" |
|
|
|
|
| class MapEntry: |
| def __init__(self, path: Optional[str] = None, description: str = "", |
| grid: Optional[Grid] = None, graph: Optional[Graph] = None): |
| self.path = path |
| self.description = description |
| self.grid = grid |
| self.graph = graph |
|
|
| @property |
| def loaded(self) -> bool: |
| return self.grid is not None or self.graph is not None |
|
|
|
|
| _map_registry: Dict[str, MapEntry] = {} |
| _solver_instances: Dict[str, Any] = {} |
|
|
|
|
| def load_map_registry(maps_config: Dict[str, dict]) -> None: |
| """Populate the registry from maps.yaml's parsed 'maps' section. Does not load any map data.""" |
| _map_registry.clear() |
| for map_id, entry in maps_config.items(): |
| _map_registry[map_id] = MapEntry(path=entry["path"], description=entry.get("description", "")) |
|
|
|
|
| def register_uploaded_map(map_id: str, grid: Optional[Grid] = None, graph: Optional[Graph] = None, |
| description: str = "uploaded") -> None: |
| """Add a runtime-uploaded map to the registry, already parsed. Not persisted to maps.yaml.""" |
| _map_registry[map_id] = MapEntry(path=None, description=description, grid=grid, graph=graph) |
|
|
|
|
| def _resolve_h5_path(relative_path: str) -> Path: |
| """ |
| Locate the .h5 for a map, generating it into MAP_CACHE_ROOT from the |
| bundled .yaml if no .h5 exists yet (installed copy or previously cached). |
| """ |
| installed = QUANTUM_ROOT / f"{relative_path}.h5" |
| if installed.exists(): |
| return installed |
|
|
| cached = MAP_CACHE_ROOT / f"{relative_path}.h5" |
| if not cached.exists(): |
| yaml_path = QUANTUM_ROOT / f"{relative_path}.yaml" |
| cached.parent.mkdir(parents=True, exist_ok=True) |
| generate_map_from_yaml( |
| str(yaml_path), |
| output_dir=str(cached.parent), |
| materials_path=str(QUANTUM_ROOT / "config" / "materials.yaml"), |
| ) |
| return cached |
|
|
|
|
| def _ensure_loaded(entry: MapEntry) -> None: |
| """Parse both representations from HDF5 in one read, if a registry entry hasn't been loaded yet.""" |
| if entry.loaded or entry.path is None: |
| return |
| h5_path = _resolve_h5_path(entry.path) |
| data = load_both_from_hdf5(str(h5_path)) |
| if data["has_map"] and data["map_data"]: |
| entry.grid = Grid.from_hdf5_data(data["map_data"]) |
| if data["has_graph"] and data["graph_data"]: |
| entry.graph = Graph.from_hdf5_data(data["graph_data"]) |
|
|
|
|
| def get_map(map_id: str, format: str = "grid"): |
| """Return the Grid or Graph for map_id (format: "grid" or "graph"), loading + caching on first access.""" |
| if map_id not in _map_registry: |
| raise KeyError(map_id) |
|
|
| entry = _map_registry[map_id] |
| _ensure_loaded(entry) |
|
|
| representation = entry.grid if format == "grid" else entry.graph |
| if representation is None: |
| raise ValueError(f"Map '{map_id}' has no {format} representation") |
|
|
| return representation |
|
|
|
|
| def list_maps() -> Dict[str, dict]: |
| return { |
| map_id: { |
| "description": entry.description, |
| "loaded": entry.loaded, |
| "grid_size": f"{entry.grid.M}x{entry.grid.N}" if entry.grid else None, |
| "has_grid": entry.grid is not None, |
| "has_graph": entry.graph is not None, |
| "source": "uploaded" if entry.path is None else entry.path, |
| } |
| for map_id, entry in _map_registry.items() |
| } |
|
|
|
|
| def get_solver(solver_key: str): |
| """Return a cached solver instance for solver_key, building it on first use.""" |
| if solver_key not in global_solver_configs: |
| raise KeyError(solver_key) |
|
|
| if solver_key not in _solver_instances: |
| config = global_solver_configs[solver_key] |
| _solver_instances[solver_key] = SolverFactory.create_solver_from_config(config) |
|
|
| return _solver_instances[solver_key] |
|
|