Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Quantum (Facade)
- **PythonBridge** only passes non-null env vars to preserve boto3 credential chain (IAM Roles).
- **QPU safety:** Drivers with `synchronous_safe: false` throw on `->run()` to prevent HTTP timeouts.
- **EntropyGenerator::integer()** uses rejection sampling on a 256-bit batch buffer — never modulo.
- **aether:install never touches the default driver:** the smoke test runs on the local simulator, through the venv interpreter when the command just created one; an existing config/aether.php is kept unless --force.

## Config

Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ Run the install command to publish the config, check Python dependencies, and ve
php artisan aether:install
```

This will optionally create a `.aether-venv` virtual environment and install the required Python packages for you.
This will:

- Publish `config/aether.php` — kept as-is if it already exists, unless you pass `--force` to overwrite it.
- Check the configured Python interpreter and compare the installed `amazon-braket-sdk` version against the floor pinned in `bin/python/requirements.txt`.
- Optionally create a `.aether-venv` virtual environment and install the required Python packages for you.
- Run a one-qubit smoke test on the local simulator, through the interpreter it just verified. This never runs on your configured default driver, so it never submits a billable task even if `AETHER_DRIVER` is set to `aws`.

## Configuration

Expand Down
207 changes: 171 additions & 36 deletions src/Commands/AetherInstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace Aether\Commands;

use Aether\Circuit\CircuitBuilder;
use Aether\Contracts\QuantumDevice;
use Aether\QuantumManager;
use Illuminate\Console\Command;
use Symfony\Component\Process\Process;
Expand All @@ -19,7 +21,7 @@ class AetherInstallCommand extends Command
*
* @var string
*/
protected $signature = 'aether:install';
protected $signature = 'aether:install {--force : Overwrite an existing config/aether.php}';

/**
* The console command description.
Expand All @@ -40,18 +42,25 @@ public function handle(QuantumManager $manager): int
$pythonPath = (string) config('aether.python_path', 'python3');

$pythonOk = $this->checkPython($pythonPath);
$venvPython = null;

if ($pythonOk) {
$depsOk = $this->checkDependencies($pythonPath);

if (! $depsOk) {
$this->handleMissingDependencies($pythonPath);
$venvPython = $this->handleMissingDependencies($pythonPath);
}
}

$this->suggestGitignore();

if ($pythonOk && ! $this->runTestCircuit($manager)) {
if (! $pythonOk) {
$this->components->warn('Installation incomplete: no Python interpreter to run the circuits with.');

return self::FAILURE;
}

if (! $this->verifyInstallation($manager, $venvPython)) {
$this->components->warn(
'The test circuit failed to run. Check that your Python dependencies are '
.'installed correctly, or that AETHER_PYTHON_PATH points to a valid interpreter.'
Expand All @@ -67,9 +76,30 @@ public function handle(QuantumManager $manager): int

/**
* Publish the Aether configuration file.
*
* An existing config/aether.php is kept unless --force is given. When
* kept, and the input is interactive, the user is asked whether to
* overwrite it; a non-interactive run keeps it without asking.
*/
protected function publishConfig(): void
{
$configExists = file_exists(config_path('aether.php'));

$shouldPublish = ! $configExists || $this->option('force');

if ($configExists && ! $this->option('force') && $this->input->isInteractive()) {
$shouldPublish = $this->components->confirm(
'config/aether.php already exists. Overwrite it with the package default?',
false
);
}

if (! $shouldPublish) {
$this->components->twoColumnDetail('Config file', '<fg=yellow>KEPT (pass --force to overwrite)</>');

return;
}

$this->call('vendor:publish', [
'--tag' => 'aether-config',
'--force' => true,
Expand Down Expand Up @@ -103,47 +133,87 @@ protected function checkPython(string $pythonPath): bool
}

/**
* Check whether the required Python dependencies (braket) are installed.
* Check whether the required Python dependencies (amazon-braket-sdk) are
* installed, and whether the installed version meets the floor pinned in
* bin/python/requirements.txt.
*
* braket itself is a namespace package with no __version__ attribute, so
* the probe reads the installed distribution's version through
* importlib.metadata instead of importing braket directly.
*/
protected function checkDependencies(string $pythonPath): bool
{
$process = new Process([
$pythonPath, '-c', 'import braket; print(braket.__version__)',
$pythonPath, '-c', 'import importlib.metadata as m; print(m.version("amazon-braket-sdk"))',
]);
$process->run();

if ($process->isSuccessful()) {
$version = trim($process->getOutput());
$this->components->twoColumnDetail('amazon-braket-sdk', "<fg=green>{$version}</>");
if (! $process->isSuccessful()) {
$this->components->twoColumnDetail('amazon-braket-sdk', '<fg=yellow>NOT INSTALLED</>');

return false;
}

$version = trim($process->getOutput());
$floor = static::parseRequirementsFloor((string) file_get_contents(self::requirementsPath()));

if ($floor !== null && ! version_compare($version, $floor, '>=')) {
$this->components->twoColumnDetail(
'amazon-braket-sdk',
"<fg=yellow>{$version} (requires >= {$floor})</>"
);

$this->components->warn(
'The installed amazon-braket-sdk is older than this package requires. Upgrade it with:'
."\n {$pythonPath} -m pip install --upgrade -r ".self::requirementsPath()
);

return true;
}

$this->components->twoColumnDetail('amazon-braket-sdk', '<fg=yellow>NOT INSTALLED</>');
$this->components->twoColumnDetail('amazon-braket-sdk', "<fg=green>{$version}</>");

return false;
return true;
}

/**
* Parse the minimum amazon-braket-sdk version pinned in a
* requirements.txt file's contents, or null when no floor is pinned.
*/
protected static function parseRequirementsFloor(string $contents): ?string
{
if (preg_match('/^amazon-braket-sdk\s*>=\s*([\d.]+)/m', $contents, $matches) === 1) {
return $matches[1];
}

return null;
}

/**
* Handle missing Python dependencies.
*
* Returns the venv interpreter path when a virtual environment was
* created and its dependencies installed successfully, otherwise null.
*/
protected function handleMissingDependencies(string $pythonPath): void
protected function handleMissingDependencies(string $pythonPath): ?string
{
if (! $this->input->isInteractive()) {
$this->showManualInstructions();
$this->showManualInstructions($pythonPath);

return;
return null;
}

$createVenv = $this->components->confirm(
'Would you like Aether to create a virtual environment and install dependencies automatically?'
);

if ($createVenv) {
$this->createVenv($pythonPath);
} else {
$this->showManualInstructions();
return $this->createVenv($pythonPath);
}

$this->showManualInstructions($pythonPath);

return null;
}

/**
Expand All @@ -167,17 +237,46 @@ protected function suggestGitignore(): void
}

/**
* Run a minimal test circuit to verify the installation end-to-end.
* Resolve the local-simulator device to run the smoke test through, and
* run it. Never resolves the configured default driver — through the
* venv interpreter when createVenv() just produced one, otherwise
* through the local driver directly — so it never submits a billable
* task even when AETHER_DRIVER is set to 'aws'.
*
* Resolving the device is wrapped in the same failure handling as the
* test circuit itself: a device that fails to resolve counts as a
* failed smoke test rather than an uncaught exception.
*/
protected function runTestCircuit(QuantumManager $manager): bool
protected function verifyInstallation(QuantumManager $manager, ?string $venvPython): bool
{
try {
$device = $venvPython !== null
? $manager->localDriver($venvPython)
: $manager->driver('local');
} catch (Throwable) {
return false;
}

return $this->runTestCircuit($device);
}

/**
* Run a minimal test circuit on the local simulator to verify the
* installation end-to-end.
*
* This never touches the configured default driver — it verifies the
* Python bridge, nothing else — so it never submits a billable task even
* when AETHER_DRIVER is set to 'aws'.
*/
protected function runTestCircuit(QuantumDevice $device): bool
{
$succeeded = false;

// components->task() renders the DONE/FAIL line but does not return
// the callback's result, so it is captured via reference instead.
$this->components->task('Running test circuit', function () use ($manager, &$succeeded): bool {
$this->components->task('Running test circuit', function () use ($device, &$succeeded): bool {
try {
$manager->circuit()->qubits(1)->h(0)->measure()->run();
(new CircuitBuilder($device, 'local'))->qubits(1)->h(0)->measure()->run();
$succeeded = true;
} catch (Throwable) {
$succeeded = false;
Expand All @@ -191,27 +290,51 @@ protected function runTestCircuit(QuantumManager $manager): bool

/**
* Create a Python virtual environment and install Aether's dependencies.
*
* Returns the venv's interpreter path when both steps succeeded, so the
* caller can run the rest of the installation through it instead of the
* interpreter that was configured when the process started; returns null
* when either step failed.
*/
protected function createVenv(string $pythonPath): void
protected function createVenv(string $pythonPath): ?string
{
$venvPath = base_path('.aether-venv');
$venvPython = $this->venvPythonPath($venvPath);
$requirementsPath = __DIR__.'/../../bin/python/requirements.txt';
$requirementsPath = self::requirementsPath();

$this->components->task('Creating virtual environment', function () use ($pythonPath, $venvPath): bool {
$venvCreated = false;

$this->components->task('Creating virtual environment', function () use ($pythonPath, $venvPath, &$venvCreated): bool {
$process = new Process([$pythonPath, '-m', 'venv', $venvPath]);
$process->run();

return $process->isSuccessful();
$venvCreated = $process->isSuccessful();

return $venvCreated;
});

$this->components->task('Installing Python dependencies', function () use ($venvPython, $requirementsPath): bool {
$process = new Process([$venvPython, '-m', 'pip', 'install', '-r', $requirementsPath]);
$process->setTimeout(300);
$process->run();
$depsInstalled = false;

return $process->isSuccessful();
});
if ($venvCreated) {
$this->components->task('Installing Python dependencies', function () use ($venvPython, $requirementsPath, &$depsInstalled): bool {
$process = new Process([$venvPython, '-m', 'pip', 'install', '-r', $requirementsPath]);
$process->setTimeout(300);
$process->run();

$depsInstalled = $process->isSuccessful();

return $depsInstalled;
});
}

if (! $depsInstalled) {
$this->components->warn(
'The virtual environment could not be prepared. Fix the error above, or follow the manual steps:'
);
$this->showManualInstructions($pythonPath);

return null;
}

$this->components->twoColumnDetail(
'Add to <fg=cyan>.env</>',
Expand All @@ -222,22 +345,26 @@ protected function createVenv(string $pythonPath): void
"Never commit your .env file. Add the following line manually:\n"
." AETHER_PYTHON_PATH={$venvPython}"
);

return $venvPython;
}

/**
* Display manual installation instructions for Python dependencies.
* Display manual installation instructions for Python dependencies,
* built around the configured interpreter and this platform's venv layout.
*/
protected function showManualInstructions(): void
protected function showManualInstructions(string $pythonPath): void
{
$requirementsPath = __DIR__.'/../../bin/python/requirements.txt';
$venvPath = base_path('.aether-venv');
$venvPython = $this->venvPythonPath($venvPath);

$this->components->warn('Manual installation required. Run the following commands:');
$this->line('');
$this->line(' python3 -m venv .aether-venv');
$this->line(' .aether-venv/bin/pip install -r '.$requirementsPath);
$this->line(" {$pythonPath} -m venv {$venvPath}");
$this->line(" {$venvPython} -m pip install -r ".self::requirementsPath());
$this->line('');
$this->line('Then add to your <fg=cyan>.env</>:');
$this->line(' AETHER_PYTHON_PATH='.base_path('.aether-venv').'/bin/python');
$this->line(" AETHER_PYTHON_PATH={$venvPython}");
$this->line('');
}

Expand All @@ -252,4 +379,12 @@ protected function venvPythonPath(string $venvPath): string

return $venvPath.'/bin/python';
}

/**
* Return the absolute path to the package's bin/python/requirements.txt.
*/
private static function requirementsPath(): string
{
return __DIR__.'/../../bin/python/requirements.txt';
}
}
Loading