Skip to content

Fix #66: read driver options through a typed DriverConfig value object - #103

Open
corgab wants to merge 2 commits into
mainfrom
fix/66-typed-driver-config
Open

Fix #66: read driver options through a typed DriverConfig value object#103
corgab wants to merge 2 commits into
mainfrom
fix/66-typed-driver-config

Conversation

@corgab

@corgab corgab commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Summary

Driver configuration travelled as a bare array $config, read at seven use sites with $this->config['key'] ?? default plus an inline cast. (int) "abc" is 0, so a typo'd env value became a ceiling of zero that rejected every circuit without saying why. This PR introduces a typed value object built once per driver, so every option is validated and cast in one place and a garbage value fails at resolution time with a message naming the driver, the key and the value.

Plan

  1. Add Config\DriverConfig for the shared options (max_qubits, entropy_qubits, synchronous_safe) and Config\AwsDriverConfig on top of it for the pricing rates and max_cost_per_run.
  2. Have AbstractQuantumDriver build the object in its constructor through an overridable makeConfig() hook; AwsBraketDriver overrides it to return AwsDriverConfig.
  3. Collapse every ?? default + cast pair in the drivers to a typed property access.
  4. Keep the raw array verbatim: it still reaches Python as driver_config, and untyped keys (python_provider, provider-specific settings) stay reachable through DriverConfig::get().
  5. Keep the presence check for requiredConfig() lazy, so estimateCost() still works on a driver without a complete remote setup; only values that are present but of the wrong shape throw at construction.

Changes

  • src/Config/DriverConfig.php (new): readonly value object with $maxQubits, $entropyQubits, $synchronousSafe, plus get(), isBlank(), blankKeys(), toArray(). Blank (absent, null, empty string) means default. Validation via filter_var with booleans and non-scalars refused; synchronous_safe accepts the env spellings ("false", "0", "off", ...).
  • src/Config/AwsDriverConfig.php (new): adds $maxCostPerRun, $perTaskRate, $perShotRate, $currency and missingRates(); rejects a pricing that is not an array.
  • src/Exceptions/InvalidDriverConfigException.php: new invalidValue(driver, key, value, expected) factory.
  • src/Drivers/AbstractQuantumDriver.php: protected readonly DriverConfig $config (generic @template TConfig), makeConfig() hook, validateCircuits() resolves the ceiling once, assertConfigured() uses blankKeys(), payload() and the five execute() calls pass toArray(), generateEntropy() reads $entropyQubits.
  • src/Drivers/AwsBraketDriver.php: makeConfig() returns AwsDriverConfig; beforeExecution(), estimateCost() and assertWithinCostCeiling() read typed properties; the cost guard's rate check uses missingRates().
  • src/Drivers/LocalSimulatorDriver.php: @extends AbstractQuantumDriver<DriverConfig>.
  • config/aether.php, README.md, CLAUDE.md: document the accepted shapes and the new failure mode; README tells custom-driver authors to read $this->config->maxQubits / ->get('key').
  • Tests: tests/Unit/Config/DriverConfigTest.php and AwsDriverConfigTest.php (new, dataset-driven: defaults, blank handling, env string casts, every rejection path with its message); driver tests for construction-time rejection, string rates from env, and the raw array still reaching Python untouched.

Behaviour preserved

  • Blank max_qubits / max_cost_per_run still mean unlimited; blank synchronous_safe still means safe.
  • A non-positive entropy_qubits still falls back to 16 (the existing clamp tests pass unchanged); only a non-numeric value now throws.
  • A configured cost ceiling without rates still throws missingKeys('aws', ['pricing.per_task', 'pricing.per_shot']).

Overlap

Supersedes the driver-side numeric helpers in #89 (#44): the same invalidValue() message and the same accepted shapes, now living in the value object. Touches the same requiredConfig() region as #100 (#68). Whichever merges second will need a trivial conflict resolution in favour of this file layout.

Tests

  • vendor/bin/pint --test passes.
  • vendor/bin/pest --compact: 847 tests, 1411 assertions, 1 skipped (791 before: +56).

Closes #66


Generated by Claude Code

…alue object

Driver config used to travel as a bare array, read at each use site with
$this->config['key'] ?? default plus an inline cast: (int) "abc" is 0, so a
typo'd env value became a ceiling of zero that rejected every circuit
without saying why.

AbstractQuantumDriver now builds a Config\DriverConfig once in its
constructor (AwsBraketDriver builds an AwsDriverConfig through the new
makeConfig() hook). The object validates and casts max_qubits,
entropy_qubits and synchronous_safe — and, for aws, the pricing rates and
max_cost_per_run — throwing InvalidDriverConfigException::invalidValue()
for a value that is neither blank nor of the documented shape. Blank still
means default, so existing configs are unaffected.

The raw array is kept verbatim and still reaches Python as driver_config,
so custom providers and untyped keys such as python_provider keep working;
DriverConfig::get() exposes them to custom drivers.

Closes #66
… constructor ordering

config/aether.php pre-cast entropy_qubits and the pricing rates with
(int)/(float), so a garbage env value reached the validator as 0 and passed.
The value object now owns the casting, so the casts are gone.

makeConfig() runs inside the base constructor and calls driverName(); the
docblocks and README now say so, so a custom driver does not return a
property its own constructor sets after parent::__construct().
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[maintainability] Driver configuration is an untyped raw array everywhere, with scattered ad-hoc casts

1 participant