Skip to content
Merged
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
21 changes: 8 additions & 13 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,30 +70,31 @@ jobs:
directory: analysis
script: driver.py
config_file: ''
config_params: ''

- name: analysis-pauli-lcu
directory: analysis
script: driver.py
config_file: config-pl.py
config_setup: 'sed -e ''s/^my_method = "Trotter"/my_method = "pauli-lcu"/'' config.py > config-pl.py'
config_file: ''
config_params: '-p method=pauli-lcu'

- name: analysis-dbl-factor
directory: analysis
script: driver.py
config_file: config-df.py
config_setup: 'sed -e ''s/^my_method = "Trotter"/my_method = "double-factorization"/'' config.py > config-df.py'
config_file: ''
config_params: '-p method=double-factorization'

- name: common-unit-tests
directory: common
script: pytest
config_file: ''
config_setup: ''
config_params: ''

- name: analysis-unit-tests
directory: analysis
script: pytest
config_file: ''
config_setup: ''
config_params: ''

name: Test ${{ matrix.test-suite.name }}

Expand All @@ -117,18 +118,12 @@ jobs:
pip install --upgrade pip
pip install --find-links=python-wheels .

- name: Setup config
if: matrix.test-suite.config_setup != ''
run: |
cd ${{ matrix.test-suite.directory }}
${{ matrix.test-suite.config_setup }}

- name: Run test
run: |
set -euxo pipefail
cd ${{ matrix.test-suite.directory }}
if [ "${{ matrix.test-suite.script }}" = "pytest" ]; then
python -m pytest tests/ -v
else
python ${{ matrix.test-suite.script }} ${{ matrix.test-suite.config_file }}
python ${{ matrix.test-suite.script }} ${{ matrix.test-suite.config_file }} ${{ matrix.test-suite.config_params }}
fi
17 changes: 15 additions & 2 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,28 @@ run-analysis-trotter:
- .venv/
policy: pull

run-analysis-pauli-lcu:
stage: test
script:
- hostname
- source .venv/bin/activate
- module load gcc/13.2.0
- cd analysis
- python driver.py -p method=pauli-lcu
cache:
key: "${CI_COMMIT_REF_SLUG}"
paths:
- .venv/
policy: pull

run-analysis-dbl-fact:
stage: test
script:
- hostname
- source .venv/bin/activate
- module load gcc/13.2.0
- cd analysis
- sed -e 's/^my_method = "Trotter"/my_method = "double-factorization"/' config.py > config-df.py
- python driver.py config-df.py
- python driver.py -p method=double-factorization
cache:
key: "${CI_COMMIT_REF_SLUG}"
paths:
Expand Down
56 changes: 56 additions & 0 deletions analysis/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,66 @@ of a configuration file as a command line argument
python driver.py my_configuration_file.py
```

### Command-line Parameters

Configuration files can access command-line parameters via the `params` dictionary, allowing runtime
customization without modifying the config file itself. Parameters are passed using the `-p` or
`--param` flag with `KEY=VALUE` format:

```bash
python driver.py config.py -p output_directory=my_run -p timestep=0.1
```

**Parameter evaluation**:
- Values are evaluated as Python literals when possible (numbers, lists, etc.)
- If evaluation fails, the value is treated as a string
- Parameters are accessed in config files via `params.get(key, default)`

**Example configuration usage**:
```python
# In your config.py file
general.output_directory = params.get("output_directory", "default_output")
timestep = params.get("timestep", 0.01) # Will be converted to float
run_name = params.get("run_name", "unnamed") # String parameter
```

**Multiple parameters**:
```bash
python driver.py -p dir=results/run1 -p error=1e-4 -p phases=[0,1,2,3]
```

This feature is particularly useful for:
- Running parameter sweeps without editing config files
- Organizing outputs into different directories per run
- Automating analyses with scripts or job schedulers
- Quick testing of different parameter values

## Configuration Options

Configuration files are themselves Python scripts, allowing users to use control logic to build up
complex configuration files.

Configuration is broken down by several parts of the processing script.

### Utility Functions

Configuration files have access to several utility functions:

- **`meV_to_Hartree(meV)`**: Converts energy from milli-electronvolts (meV) to Hartree atomic units.
Useful for specifying energy errors in more intuitive units.
```python
energy_error = meV_to_Hartree(1e4) # 0.01 keV
```

- **`string_to_seed(s)`**: Converts a string to a deterministic integer seed for random number
generation. Uses SHA-256 hashing to ensure the same string always produces the same seed. This is
particularly useful in large test suites where reproducible random behavior is needed.
```python
import random
seed = string_to_seed("my_experiment_name")
random.seed(seed)
```

### General

"General" configuration governs the behavior of the resource analysis script itself, including
Expand Down Expand Up @@ -57,6 +110,9 @@ You can configure the log file that the script will write to by setting **`gener
the name of the logfile you want to use. The default is `analysis.log`. If `output_directory` is
set, the logfile will be written to that directory.

The logfile automatically records the configuration file contents and any command-line parameters
that were passed via `-p`, making it easy to reproduce analyses.

#### Log Level

The log level is set by calling one of the following functions. If you call multiple of these
Expand Down
35 changes: 22 additions & 13 deletions analysis/config.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
"""
QHAT Analysis Configuration: Trotter Method
QHAT Analysis Configuration: basic example

This config demonstrates resource estimation using Trotter decomposition.
For comprehensive analysis including error metrics, see examples/config_full_analysis.py
This configuration demonstrates the use of command-line parameters to select between different
quantum simulation methods without editing the file. This is useful for CI pipelines and automated
testing.

PHASE 2 IMPROVEMENTS (2026-07):
If you enable error analysis, it now uses OperatorRepresentation framework internally
to correctly compare time-evolution operators. No config changes needed!
Usage:
python driver.py # Uses Trotter (default)
python driver.py -p method=pauli-lcu # Uses Pauli-LCU
python driver.py -p method=double-factorization # Uses double-factorization

Each method automatically outputs to a separate directory (Be-H-trotter/, Be-H-pauli-lcu/, etc.)
to avoid overwriting results. You can override this with -p output_directory=custom_dir
"""

my_method = "Trotter"
#my_method = "pauli-lcu"
#my_method = "double-factorization"
# Method selection: override via command-line with -p method=<name>
my_method = params.get("method", "Trotter")

# this configuration file assumes equipartition of energy between Trotterization error and phase
# estimation error (see usage of energy_error below)
Expand All @@ -22,10 +26,15 @@

general.print_verbose()

# Set output directory for all generated files (logfiles, matrices, eigendecompositions, etc.)
# If not set or empty, files are written to the current directory
# Example: "Be-H/" will create a Be-H/ subdirectory for all outputs
general.output_directory = "Be-H/"
# Set output directory based on method to keep results separated
# Override with: -p output_directory=custom_dir
default_output_dir = {
"Trotter": "Be-H-trotter",
"pauli-lcu": "Be-H-pauli-lcu",
"double-factorization": "Be-H-double-factorization"
}.get(my_method, "Be-H")

general.output_directory = params.get("output_directory", default_output_dir)

general.logfile = "Be-H.log"

Expand Down
66 changes: 59 additions & 7 deletions analysis/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

# -------------------------------------------------------------------------------------------------

def load_configuration() -> State:
def load_configuration() -> tuple[State, str]:

# Set up and read command-line arguments
parser = argparse.ArgumentParser()
Expand All @@ -24,8 +24,31 @@ def load_configuration() -> State:
nargs='?',
default=default_config,
help=f"Name of the configuration file; defaults to \"{default_config}\"")

# Add support for arbitrary key=value arguments
parser.add_argument(
'--param', '-p',
action='append',
dest='params',
default=[],
metavar='KEY=VALUE',
help='Parameters to pass to the configuration file (e.g., -p distance=1.5)')

args = parser.parse_args()

# Parse the key=value parameters
config_params = {}
for param in args.params:
if '=' not in param:
raise ValueError(f"Parameter must be in KEY=VALUE format, got: {param}")
key, value = param.split('=', 1)
# Try to evaluate as Python literal (numbers, lists, etc.)
try:
config_params[key] = eval(value)
except:
# If evaluation fails, treat as string
config_params[key] = value

# Read the configuration file
with open(args.configuration_file, 'r') as fin:
config_script = fin.read()
Expand All @@ -40,15 +63,44 @@ def load_configuration() -> State:
analysis = AnalysisConfiguration()
def meV_to_Hartree(meV):
return 3.67493221757e-5 * meV
exec(config_script)
def string_to_seed(s):
import hashlib
"""Convert a string to a deterministic integer seed."""
# Use SHA-256 hash and convert to integer
hash_bytes = hashlib.sha256(s.encode('utf-8')).digest()
# Take first 8 bytes and convert to integer (fits in 64-bit)
seed = int.from_bytes(hash_bytes[:8], byteorder='big')
return seed

# Create namespace with config objects and params dictionary
exec_namespace = {
# configuration objects
'general': general,
'hamiltonian': hamiltonian,
'unitary': unitary,
'algorithm': algorithm,
'analysis': analysis,
# command-line parameters
'params': config_params,
# utility functions
'meV_to_Hartree': meV_to_Hartree,
'string_to_seed': string_to_seed,
}
exec(config_script, exec_namespace)

# Build the state (does some post-processing of user configuration)
state = State(config_script, general, hamiltonian, unitary, algorithm, analysis)

logger.info("\n".join([
f"Contents of configuration file \"{args.configuration_file}\":",
config_script
]))
# Prepare log messages with configuration file contents and any parameters passed
# These will be logged after logging is configured
config_file_message = f"Contents of configuration file \"{args.configuration_file}\":\n{config_script}"

params_message = None
if config_params:
params_lines = ["Command-line parameters:"]
for key, value in config_params.items():
params_lines.append(f" {key} = {value!r}")
params_message = "\n".join(params_lines)

return state
return state, config_file_message, params_message

7 changes: 5 additions & 2 deletions analysis/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@
from qhat.analysis.hamiltonian import get_physical_hamiltonian
from qhat.analysis.unitary import encode_as_unitary

logger = logging.getLogger(__name__)
logger = logging.getLogger("qhat.analysis.driver")

# =================================================================================================

def run():

# Configuration _______________________________________________________________________________

state = load_configuration()
state, config_file_message, params_message = load_configuration()

# Configure logging based on user settings
logfile_path = state.config_general.get_output_path(state.config_general.logfile)
Expand All @@ -34,6 +34,9 @@ def run():
logger.info("=" * 99)
logger.info(f"Logfile: {logfile_path}")
logger.info(f"Git hash: {state.config_general.git_hash}")
logger.info(config_file_message)
if params_message:
logger.info(params_message)

# Hamiltonian _________________________________________________________________________________

Expand Down
Loading