## General principles
- Prefer clear, explicit code and APIs over cleverness; correctness over premature optimization.
- Keep modules small and focused on a single responsibility.
- One class per file; use module-level functions for utilities.
- Avoid comments in core library code unless strictly necessary; express intent through naming and structure.
- Prefer DRY refactors: factor out common logic (e.g., standardization, trust-region bounds, observation bookkeeping, GP/ENN wrappers) and have higher-level methods delegate to lower-level ones.
- When a trusted reference implementation exists (e.g., TuRBO), prefer matching its behavior and hyperparameter conventions unless there is a strong, documented reason to differ.

## Project layout and packaging
- Treat `src/enn` as the only installable package content; tests, notebooks, and examples must live outside `src/`.
- Place demo notebooks under `examples/` and allow them to keep curated outputs.
- Keep tests under `tests/` and avoid importing test utilities from production code.
- Declare all runtime dependencies in `pyproject.toml`; `requirements.txt` is for development/testing with pinned versions.

## Imports and dependencies
- Minimize top-level imports; prefer imports inside functions/methods where possible.
- Use `from typing import TYPE_CHECKING` for type-only imports to avoid runtime dependencies.
- Keep structural imports (decorators, base classes) at module level only when necessary.

## Public API and data structures
- Expose only essential user-facing objects from `enn.__init__`: `EpistemicNearestNeighbors`, `TurboMode`, `TurboOptimizer`, `enn_fit`.
- Avoid exposing private helpers (names starting with `_`) or internal implementation details.
- Use frozen dataclasses for structured parameter objects (e.g., `ENNParams`); require explicit arguments rather than storing defaults in models.
- Maintain consistent return types across related methods; avoid hidden or mutable global state; require key hyperparameters as explicit function arguments.

## Code organization
- Extract utility functions into dedicated modules: `turbo_utils.py` for sampling/coordinate utilities, `proposal.py` for acquisition functions.
- Acquisition functions (e.g., `select_gp_thompson`, `select_enn_pareto`) should take observation lists and build models internally, matching the pattern of refitting on every call.
- Simple wrapper methods that provide a cleaner API (e.g., single-line convenience methods) are acceptable; avoid complex wrappers that only add indirection.

## ENN modeling conventions
- Use numpy for core algorithms; if you introduce heavier frameworks (e.g., torch), keep them isolated behind small, well-defined interfaces.
- Treat training arrays as: `train_x` shape `(n, d)`, `train_y` shape `(n, m)`, `train_yvar` shape `(n, m)`.
- Internally standardize `x` by feature-wise standard deviation (with a small floor) before passing to FAISS so distances are unitless.
- Keep hyperparameters unitless by scaling likelihood calculations with `std(y)` rather than rescaling user-facing `y`; implement shared "safe std/standardize" helpers with clear fallbacks for degenerate or non-finite scales.
- Combine epistemic variance (from distances) and observation variance (`yvar`) into a single diagonal Gaussian posterior.
- For batch operations, search once with maximum k and subset neighbors for smaller k values to avoid redundant index queries.
- Maintain unit consistency: normalize variances by dividing by `y_scale**2`, then unnormalize final standard errors by multiplying by `y_scale`.
- HNSW index: use `M=32` as default; switch from `IndexFlatL2` to `IndexHNSWFlat` when `num_obs >= hnsw_threshold` (default 1000) for approximate but faster search on large datasets.

## Stochastic optimization and TuRBO conventions
- Express optimization domains via box bounds and map them to the unit cube inside optimizers; internal logic should depend only on unit-space coordinates so behavior is invariant to affine transforms of `x`.
- Use RAASP (Randomly Aggregated Adaptive Sobol Perturbation) for candidate generation: apply random perturbation masks to Sobol sequences, ensuring at least one dimension is perturbed per candidate, with perturbation probability `min(20/num_dim, 1.0)`.
- Centralize trust-region box construction (including anisotropic boxes from ARD lengthscales) and candidate generation in shared utilities so all TuRBO variants share the same geometry and invariance properties.
- When using surrogate models (GP or ENN) inside optimizers, standardize `y` internally (zero mean, unit variance) for fitting while keeping the user-facing `y` scale unchanged.
- Pass explicit `np.random.Generator` instances (`rng`) into any public API that uses randomness; avoid global RNG state.
- For PyTorch operations, create local `torch.Generator` instances seeded from the numpy `rng`; if global torch RNG state must be mutated, use context managers to save/restore state safely.
- Refit surrogate models (GP or ENN) on every `ask` call; avoid redundant refits within a single `ask` by reusing the same model for related calculations.
- For TuRBO-style `TURBO_ONE`, follow the reference: adapt trust-region length based on relative improvement (`1e-3 * abs(best_value)`), define the trust-region center and anisotropic box from trust-region-local data using GP ARD lengthscales as normalized per-dimension weights, and on restart discard trust-region observations and reinitialize with an LHD design of `num_init` points.
- On CPU, keep numeric types consistent with references: use `float64` numpy arrays and `torch.float64` tensors for GP/TuRBO surrogates unless there is a compelling performance reason not to.
- Use Python lists for incremental data growth; convert to numpy arrays only when needed for computation to avoid O(N²) concatenation overhead.

## Hyperparameter fitting
- Implement hyperparameter search in standalone functions (e.g., `enn_fit`), not as model methods.
- Use randomized search over log-spaced `(k, var_scale)` pairs; `k` as integers in [3, 100], `var_scale` drawn log-uniformly in [10^-2, 10^2].
- Use subsampled Gaussian log-likelihood for tuning; exclude the nearest neighbor to approximate leave-one-out behavior.
- Batch parameter evaluations: create lists of parameter objects and evaluate them together using batch methods to avoid redundant computations.

## Testing and linting
- For each new public function or class, add at least one focused pytest test that exercises all methods or key code paths.
- Keep tests deterministic: prefer explicit `np.random.Generator` instances or clearly documented seeds wherever randomness affects assertions.
- Keep tests self-explanatory via clear naming; avoid comments/docstrings in tests unless behavior is truly non-obvious.
- When adding batch operations, verify they produce identical results to individual operations.
- Run `PYTHONPATH=src pytest -sv tests` and `ruff check` after substantive changes; iterate until both pass.

## Roles and change policies
- Use role separation when helpful: a "Tester" only writes/changes tests; an "Implementor" only writes/changes library code.
- Respect change constraints when specified: DCC (do not change code), DCT (do not change tests), OCT (only change tests).

## Notebooks and demos
- Demo notebooks should tell a clean, linear story with a small number of key plots.
- Preserve outputs for demos but avoid excessive logging or huge data displays.
- Use small, fast-running configurations so a full rerun is practical.


