From 4f0efb49ac4fe846faeb8e324ce977705f5aa8fc Mon Sep 17 00:00:00 2001 From: "Brendan K. Krueger" Date: Wed, 19 Aug 2026 10:22:30 -0600 Subject: [PATCH 01/11] Claude took a first pass at implementing a capability to pass command-line parameters to the configuration file. I still need to take a look. --- analysis/CMDLINE_PARAMS.md | 180 ++++++++++++++++++ analysis/configuration.py | 50 ++++- .../examples/cmdline_params_demo_config.py | 53 ++++++ analysis/tests/test_cmdline_params.py | 144 ++++++++++++++ 4 files changed, 422 insertions(+), 5 deletions(-) create mode 100644 analysis/CMDLINE_PARAMS.md create mode 100644 analysis/examples/cmdline_params_demo_config.py create mode 100644 analysis/tests/test_cmdline_params.py diff --git a/analysis/CMDLINE_PARAMS.md b/analysis/CMDLINE_PARAMS.md new file mode 100644 index 00000000..f3114625 --- /dev/null +++ b/analysis/CMDLINE_PARAMS.md @@ -0,0 +1,180 @@ +# Command-Line Parameters for Configuration Files + +This feature allows you to pass parameters from the command line into your configuration files, enabling easy parametric studies and configuration reuse without modifying the config file itself. + +## Usage + +Pass parameters using the `-p` or `--param` flag with `KEY=VALUE` format: + +```bash +python3.11 driver.py my_config.py -p distance=1.5 -p basis=sto-6g +``` + +Multiple parameters can be specified: + +```bash +python3.11 driver.py my_config.py -p distance=1.5 -p basis=sto-6g -p num_steps=100 +``` + +## Accessing Parameters in Configuration Files + +Parameters are available in two ways within your configuration script: + +### Method 1: Direct access (if parameter is guaranteed to exist) + +```python +# Access parameter directly as a variable +hamiltonian.add_atom("H", distance, 0, 0) +hamiltonian.basis = basis +``` + +**Note:** This will raise a `NameError` if the parameter wasn't provided on the command line. + +### Method 2: Using the `params` dictionary with defaults (recommended) + +```python +# Access with a default value +distance = params.get('distance', 1.30) # Defaults to 1.30 if not provided +basis_set = params.get('basis', 'sto-3g') # Defaults to 'sto-3g' if not provided + +hamiltonian.add_atom("H", distance, 0, 0) +hamiltonian.basis = basis_set +``` + +## Type Conversion + +Values are automatically evaluated as Python expressions when possible: + +| Command Line | Type | Value | +|--------------|------|-------| +| `-p x=1.5` | float | `1.5` | +| `-p n=42` | int | `42` | +| `-p items=[1,2,3]` | list | `[1, 2, 3]` | +| `-p flag=True` | bool | `True` | +| `-p name=hello` | string | `'hello'` (eval fails, treated as string) | + +## Examples + +### Example 1: Parametric distance study + +**Configuration file** (`distance_study_config.py`): +```python +# Use command-line parameter for distance +distance = params.get('distance', 1.30) + +general.print_verbose() +general.file_stub = f"Be-H_{distance:.2f}_sto-6g" +general.file_format = "default" + +hamiltonian.add_atom("Be", 0.00, 0, 0) +hamiltonian.add_atom("H", distance, 0, 0) +hamiltonian.basis = "sto-6g" +hamiltonian.num_active_occupied = 3 +hamiltonian.num_active_vacant = 3 +``` + +**Run with different distances**: +```bash +python3.11 driver.py distance_study_config.py -p distance=1.2 +python3.11 driver.py distance_study_config.py -p distance=1.5 +python3.11 driver.py distance_study_config.py -p distance=2.0 +``` + +### Example 2: Basis set comparison + +```bash +# Same config, different basis sets +python3.11 driver.py config.py -p basis=sto-3g +python3.11 driver.py config.py -p basis=sto-6g +python3.11 driver.py config.py -p basis=6-31g +``` + +### Example 3: Multiple parameters + +**Configuration file**: +```python +distance = params.get('distance', 1.30) +basis = params.get('basis', 'sto-3g') +n_occ = int(params.get('num_occupied', 3)) +n_vac = int(params.get('num_vacant', 3)) + +general.file_stub = f"Be-H_{distance}_{basis}_occ{n_occ}_vac{n_vac}" + +hamiltonian.add_atom("Be", 0.00, 0, 0) +hamiltonian.add_atom("H", distance, 0, 0) +hamiltonian.basis = basis +hamiltonian.num_active_occupied = n_occ +hamiltonian.num_active_vacant = n_vac +``` + +**Run**: +```bash +python3.11 driver.py config.py -p distance=1.5 -p basis=sto-6g -p num_occupied=4 -p num_vacant=4 +``` + +### Example 4: Scripting parametric studies + +```bash +#!/bin/bash +# Run a sweep over different distances + +for dist in 1.0 1.2 1.4 1.6 1.8 2.0; do + echo "Running with distance = $dist" + python3.11 driver.py my_config.py -p distance=$dist +done +``` + +## Advanced Usage + +### Type conversion and validation + +```python +# Ensure integer types +num_steps = int(params.get('num_steps', 100)) + +# Convert string to float +energy = float(params.get('energy', '0.0')) + +# Parse complex structures +if 'atoms' in params: + for atom_type, position in params['atoms']: + hamiltonian.add_atom(atom_type, position, 0, 0) +``` + +### Conditional logic based on parameters + +```python +mode = params.get('mode', 'production') + +if mode == 'debug': + general.print_debug() +elif mode == 'verbose': + general.print_verbose() +else: + general.print_default() + +# Adjust other settings based on mode +algorithm.num_phase_qubits = 8 if mode == 'debug' else 12 +``` + +## Error Handling + +### Invalid format +```bash +# Missing '=' will raise ValueError +python3.11 driver.py config.py -p bad_param +# Error: Parameter must be in KEY=VALUE format, got: bad_param +``` + +### Using undefined parameters without defaults +```python +# In config file: +x = params['missing_key'] # KeyError if not provided +# Better: +x = params.get('missing_key', default_value) # Safe with default +``` + +## See Also + +- `examples/cmdline_params_demo_config.py` - Full demonstration of parameter usage +- `tests/test_cmdline_params.py` - Test suite showing various use cases diff --git a/analysis/configuration.py b/analysis/configuration.py index fd5c3443..47293f3a 100644 --- a/analysis/configuration.py +++ b/analysis/configuration.py @@ -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() @@ -40,15 +63,32 @@ def load_configuration() -> State: analysis = AnalysisConfiguration() def meV_to_Hartree(meV): return 3.67493221757e-5 * meV - exec(config_script) + + # Create namespace with config objects and injected parameters + exec_namespace = { + 'general': general, + 'hamiltonian': hamiltonian, + 'unitary': unitary, + 'algorithm': algorithm, + 'analysis': analysis, + 'meV_to_Hartree': meV_to_Hartree, + 'params': config_params, # All params available as a dict + **config_params # Individual params available as variables + } + 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 - ])) + # Log configuration file contents and any parameters passed + log_messages = [f"Contents of configuration file \"{args.configuration_file}\":"] + log_messages.append(config_script) + if config_params: + log_messages.append("\nCommand-line parameters:") + for key, value in config_params.items(): + log_messages.append(f" {key} = {value!r}") + + logger.info("\n".join(log_messages)) return state diff --git a/analysis/examples/cmdline_params_demo_config.py b/analysis/examples/cmdline_params_demo_config.py new file mode 100644 index 00000000..ecade7c4 --- /dev/null +++ b/analysis/examples/cmdline_params_demo_config.py @@ -0,0 +1,53 @@ +# _________________________________________________________________________________________________ +# Example configuration demonstrating command-line parameter usage +# +# This config can be run with parameters like: +# python3.11 driver.py examples/cmdline_params_demo_config.py -p distance=1.5 -p basis=sto-6g +# +# Parameters can be accessed in two ways: +# 1. Directly as variables: distance, basis, etc. +# 2. Via the params dict: params.get('distance', default_value) +# _________________________________________________________________________________________________ + +# _________________________________________________________________________________________________ +# General configuration + +general.print_verbose() +general.file_stub = "Be-H_cmdline_demo" +general.file_format = "default" + +# _________________________________________________________________________________________________ +# Demonstrate different ways to use command-line parameters + +# Method 1: Use parameter directly with a default value using params dict +distance = params.get('distance', 1.30) # Default to 1.30 if not provided +basis_set = params.get('basis', 'sto-3g') # Default to 'sto-3g' if not provided + +# Method 2: Use parameter directly (will fail if not provided) +# Uncomment this to require the parameter: +# distance = distance # Will raise NameError if -p distance=... not provided + +# Method 3: Use parameter with type conversion and validation +num_occupied = int(params.get('num_occupied', 3)) +num_vacant = int(params.get('num_vacant', 3)) + +# _________________________________________________________________________________________________ +# Describe the Hamiltonian using the parameters + +hamiltonian.add_atom("Be", 0.00, 0, 0) +hamiltonian.add_atom("H", distance, 0, 0) # Uses the distance parameter + +hamiltonian.basis = basis_set # Uses the basis parameter + +hamiltonian.num_active_occupied = num_occupied # Uses the num_occupied parameter +hamiltonian.num_active_vacant = num_vacant # Uses the num_vacant parameter + +# _________________________________________________________________________________________________ +# Parameters can also be used in more complex expressions + +# Example: You could pass a list of atoms +# python3.11 driver.py config.py -p atoms='[("Be",0.0),("H",1.3)]' +# Then use it like: +# if 'atoms' in params: +# for atom_type, position in params['atoms']: +# hamiltonian.add_atom(atom_type, position, 0, 0) diff --git a/analysis/tests/test_cmdline_params.py b/analysis/tests/test_cmdline_params.py new file mode 100644 index 00000000..e1c2f05a --- /dev/null +++ b/analysis/tests/test_cmdline_params.py @@ -0,0 +1,144 @@ +"""Tests for command-line parameter functionality in configuration loading.""" + +import pytest +import sys +import tempfile +import os +from pathlib import Path + +from qhat.analysis.configuration import load_configuration + + +def test_cmdline_params_basic(monkeypatch, tmp_path): + """Test basic command-line parameter passing.""" + # Create a simple test config file + config_file = tmp_path / "test_config.py" + config_file.write_text(""" +# Use command-line parameters +test_value = params.get('test_param', 'default') +numeric_value = params.get('number', 42) + +# Use output_directory which is preserved in GeneralConfiguration +general.output_directory = f"test_{test_value}_{numeric_value}" +""") + + # Mock sys.argv to simulate command-line arguments + test_args = [ + 'driver.py', + str(config_file), + '-p', 'test_param=hello', + '-p', 'number=100' + ] + monkeypatch.setattr(sys, 'argv', test_args) + + # Load configuration + state = load_configuration() + + # Verify the parameters were used + assert 'hello' in state.config_general.output_directory + assert '100' in state.config_general.output_directory + + +def test_cmdline_params_type_conversion(monkeypatch, tmp_path): + """Test that parameters are properly converted to Python types.""" + config_file = tmp_path / "test_config.py" + config_file.write_text(""" +# Access parameters directly if they're provided +float_val = float_param +int_val = int_param +list_val = list_param + +general.file_stub = f"test" +""") + + test_args = [ + 'driver.py', + str(config_file), + '-p', 'float_param=3.14', + '-p', 'int_param=42', + '-p', 'list_param=[1,2,3]' + ] + monkeypatch.setattr(sys, 'argv', test_args) + + state = load_configuration() + # If we got here without errors, the types were correctly evaluated + assert True + + +def test_cmdline_params_string_fallback(monkeypatch, tmp_path): + """Test that invalid Python expressions are treated as strings.""" + config_file = tmp_path / "test_config.py" + config_file.write_text(""" +# This should be a string since "hello-world" isn't valid Python +string_val = params.get('weird_string', 'default') + +general.output_directory = string_val +""") + + test_args = [ + 'driver.py', + str(config_file), + '-p', 'weird_string=hello-world' + ] + monkeypatch.setattr(sys, 'argv', test_args) + + state = load_configuration() + assert state.config_general.output_directory == 'hello-world' + + +def test_cmdline_params_no_params(monkeypatch, tmp_path): + """Test that configs work without any command-line parameters.""" + config_file = tmp_path / "test_config.py" + config_file.write_text(""" +# Use defaults when no params provided +test_value = params.get('missing_param', 'default_value') + +general.output_directory = test_value +""") + + test_args = ['driver.py', str(config_file)] + monkeypatch.setattr(sys, 'argv', test_args) + + state = load_configuration() + assert state.config_general.output_directory == 'default_value' + + +def test_cmdline_params_invalid_format(monkeypatch, tmp_path): + """Test that invalid parameter format raises an error.""" + config_file = tmp_path / "test_config.py" + config_file.write_text("general.file_stub = 'test'") + + # Parameter without '=' should raise ValueError + test_args = [ + 'driver.py', + str(config_file), + '-p', 'invalid_param_no_equals' + ] + monkeypatch.setattr(sys, 'argv', test_args) + + with pytest.raises(ValueError, match="Parameter must be in KEY=VALUE format"): + load_configuration() + + +def test_cmdline_params_multiple_params(monkeypatch, tmp_path): + """Test passing multiple parameters.""" + config_file = tmp_path / "test_config.py" + config_file.write_text(""" +a = params.get('a', 0) +b = params.get('b', 0) +c = params.get('c', 0) + +general.output_directory = f"test_{a}_{b}_{c}" +""") + + test_args = [ + 'driver.py', + str(config_file), + '-p', 'a=1', + '-p', 'b=2', + '-p', 'c=3' + ] + monkeypatch.setattr(sys, 'argv', test_args) + + state = load_configuration() + assert state.config_general.output_directory == 'test_1_2_3' From 4fb9262d74d6bf2059541912989afc9b78b4d217 Mon Sep 17 00:00:00 2001 From: "Brendan K. Krueger" Date: Wed, 19 Aug 2026 11:05:15 -0600 Subject: [PATCH 02/11] Revision to remove global/local variables for command-line parameters, and provide command-line parameters only through a dictionary. --- analysis/CMDLINE_PARAMS.md | 24 +++++----- analysis/configuration.py | 5 +-- .../examples/cmdline_params_demo_config.py | 10 ++--- analysis/tests/test_cmdline_params.py | 45 ++++++++++++++++--- 4 files changed, 57 insertions(+), 27 deletions(-) diff --git a/analysis/CMDLINE_PARAMS.md b/analysis/CMDLINE_PARAMS.md index f3114625..8c0cf21a 100644 --- a/analysis/CMDLINE_PARAMS.md +++ b/analysis/CMDLINE_PARAMS.md @@ -18,19 +18,9 @@ python3.11 driver.py my_config.py -p distance=1.5 -p basis=sto-6g -p num_steps=1 ## Accessing Parameters in Configuration Files -Parameters are available in two ways within your configuration script: +All command-line parameters are available via the `params` dictionary that is automatically provided to your configuration file. -### Method 1: Direct access (if parameter is guaranteed to exist) - -```python -# Access parameter directly as a variable -hamiltonian.add_atom("H", distance, 0, 0) -hamiltonian.basis = basis -``` - -**Note:** This will raise a `NameError` if the parameter wasn't provided on the command line. - -### Method 2: Using the `params` dictionary with defaults (recommended) +### Method 1: Using `params.get()` with defaults (recommended) ```python # Access with a default value @@ -41,6 +31,16 @@ hamiltonian.add_atom("H", distance, 0, 0) hamiltonian.basis = basis_set ``` +### Method 2: Direct dictionary access (if parameter is required) + +```python +# Access parameter directly - will raise KeyError if not provided +distance = params['distance'] +hamiltonian.add_atom("H", distance, 0, 0) +``` + +**Note:** This will raise a `KeyError` if the parameter wasn't provided on the command line. + ## Type Conversion Values are automatically evaluated as Python expressions when possible: diff --git a/analysis/configuration.py b/analysis/configuration.py index 47293f3a..010e838c 100644 --- a/analysis/configuration.py +++ b/analysis/configuration.py @@ -64,7 +64,7 @@ def load_configuration() -> State: def meV_to_Hartree(meV): return 3.67493221757e-5 * meV - # Create namespace with config objects and injected parameters + # Create namespace with config objects and params dictionary exec_namespace = { 'general': general, 'hamiltonian': hamiltonian, @@ -72,8 +72,7 @@ def meV_to_Hartree(meV): 'algorithm': algorithm, 'analysis': analysis, 'meV_to_Hartree': meV_to_Hartree, - 'params': config_params, # All params available as a dict - **config_params # Individual params available as variables + 'params': config_params, } exec(config_script, exec_namespace) diff --git a/analysis/examples/cmdline_params_demo_config.py b/analysis/examples/cmdline_params_demo_config.py index ecade7c4..75ea7505 100644 --- a/analysis/examples/cmdline_params_demo_config.py +++ b/analysis/examples/cmdline_params_demo_config.py @@ -4,9 +4,7 @@ # This config can be run with parameters like: # python3.11 driver.py examples/cmdline_params_demo_config.py -p distance=1.5 -p basis=sto-6g # -# Parameters can be accessed in two ways: -# 1. Directly as variables: distance, basis, etc. -# 2. Via the params dict: params.get('distance', default_value) +# Parameters are accessed via the 'params' dictionary that is provided to all configuration files. # _________________________________________________________________________________________________ # _________________________________________________________________________________________________ @@ -19,13 +17,13 @@ # _________________________________________________________________________________________________ # Demonstrate different ways to use command-line parameters -# Method 1: Use parameter directly with a default value using params dict +# Method 1: Use parameter with a default value (recommended) distance = params.get('distance', 1.30) # Default to 1.30 if not provided basis_set = params.get('basis', 'sto-3g') # Default to 'sto-3g' if not provided -# Method 2: Use parameter directly (will fail if not provided) +# Method 2: Require parameter (will raise KeyError if not provided) # Uncomment this to require the parameter: -# distance = distance # Will raise NameError if -p distance=... not provided +# distance = params['distance'] # Will raise KeyError if -p distance=... not provided # Method 3: Use parameter with type conversion and validation num_occupied = int(params.get('num_occupied', 3)) diff --git a/analysis/tests/test_cmdline_params.py b/analysis/tests/test_cmdline_params.py index e1c2f05a..5a12dba9 100644 --- a/analysis/tests/test_cmdline_params.py +++ b/analysis/tests/test_cmdline_params.py @@ -43,12 +43,20 @@ def test_cmdline_params_type_conversion(monkeypatch, tmp_path): """Test that parameters are properly converted to Python types.""" config_file = tmp_path / "test_config.py" config_file.write_text(""" -# Access parameters directly if they're provided -float_val = float_param -int_val = int_param -list_val = list_param - -general.file_stub = f"test" +# Access parameters from params dict +float_val = params['float_param'] +int_val = params['int_param'] +list_val = params['list_param'] + +# Verify types +assert isinstance(float_val, float) +assert isinstance(int_val, int) +assert isinstance(list_val, list) +assert float_val == 3.14 +assert int_val == 42 +assert list_val == [1, 2, 3] + +general.output_directory = "test" """) test_args = [ @@ -142,3 +150,28 @@ def test_cmdline_params_multiple_params(monkeypatch, tmp_path): state = load_configuration() assert state.config_general.output_directory == 'test_1_2_3' + + +def test_cmdline_params_not_in_global_namespace(monkeypatch, tmp_path): + """Test that parameters are NOT available as direct variables.""" + config_file = tmp_path / "test_config.py" + config_file.write_text(""" +# Try to access parameter directly as a variable (should fail) +try: + x = my_param # This should raise NameError + general.output_directory = "SHOULD_NOT_GET_HERE" +except NameError: + # Expected - parameter not in namespace + general.output_directory = "params_not_global" +""") + + test_args = [ + 'driver.py', + str(config_file), + '-p', 'my_param=42' + ] + monkeypatch.setattr(sys, 'argv', test_args) + + state = load_configuration() + # Should have caught the NameError, not accessed my_param directly + assert state.config_general.output_directory == 'params_not_global' From 4819be91b8f26a7d5e5ac155d2ac2d2723fe67a5 Mon Sep 17 00:00:00 2001 From: "Brendan K. Krueger" Date: Wed, 19 Aug 2026 11:39:37 -0600 Subject: [PATCH 03/11] Fix some logging. --- analysis/configuration.py | 19 ++++++++++--------- analysis/driver.py | 7 +++++-- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/analysis/configuration.py b/analysis/configuration.py index 010e838c..d70d728c 100644 --- a/analysis/configuration.py +++ b/analysis/configuration.py @@ -14,7 +14,7 @@ # ------------------------------------------------------------------------------------------------- -def load_configuration() -> State: +def load_configuration() -> tuple[State, str]: # Set up and read command-line arguments parser = argparse.ArgumentParser() @@ -79,15 +79,16 @@ def meV_to_Hartree(meV): # Build the state (does some post-processing of user configuration) state = State(config_script, general, hamiltonian, unitary, algorithm, analysis) - # Log configuration file contents and any parameters passed - log_messages = [f"Contents of configuration file \"{args.configuration_file}\":"] - log_messages.append(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: - log_messages.append("\nCommand-line parameters:") + params_lines = ["Command-line parameters:"] for key, value in config_params.items(): - log_messages.append(f" {key} = {value!r}") - - logger.info("\n".join(log_messages)) + params_lines.append(f" {key} = {value!r}") + params_message = "\n".join(params_lines) - return state + return state, config_file_message, params_message diff --git a/analysis/driver.py b/analysis/driver.py index 406c6251..68e043b3 100644 --- a/analysis/driver.py +++ b/analysis/driver.py @@ -15,7 +15,7 @@ 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") # ================================================================================================= @@ -23,7 +23,7 @@ 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) @@ -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 _________________________________________________________________________________ From b12bc1ecd3b9a7f0b6f30bfbdca78e1a0d3319f9 Mon Sep 17 00:00:00 2001 From: "Brendan K. Krueger" Date: Wed, 19 Aug 2026 11:48:47 -0600 Subject: [PATCH 04/11] A utility function that takes a string and generates an integer that can be used as a random seed. This is likely to be useful in large suites. --- analysis/configuration.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/analysis/configuration.py b/analysis/configuration.py index d70d728c..8393d572 100644 --- a/analysis/configuration.py +++ b/analysis/configuration.py @@ -63,16 +63,28 @@ def load_configuration() -> tuple[State, str]: analysis = AnalysisConfiguration() def meV_to_Hartree(meV): return 3.67493221757e-5 * meV + 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, - 'meV_to_Hartree': meV_to_Hartree, + # 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) From 60474a7886c884d57838e5f85e48fdd71e2d72a5 Mon Sep 17 00:00:00 2001 From: "Brendan K. Krueger" Date: Wed, 19 Aug 2026 11:49:28 -0600 Subject: [PATCH 05/11] Clean up configuration file comment. --- analysis/config.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/analysis/config.py b/analysis/config.py index 582c1724..7ff7f644 100644 --- a/analysis/config.py +++ b/analysis/config.py @@ -1,12 +1,5 @@ """ -QHAT Analysis Configuration: Trotter Method - -This config demonstrates resource estimation using Trotter decomposition. -For comprehensive analysis including error metrics, see examples/config_full_analysis.py - -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! +QHAT Analysis Configuration: basic example """ my_method = "Trotter" From c41aa33960581217accc68627fbbb39d5ed5ce7a Mon Sep 17 00:00:00 2001 From: "Brendan K. Krueger" Date: Wed, 19 Aug 2026 11:52:33 -0600 Subject: [PATCH 06/11] Remove Claude-generated 'documentation'. --- analysis/CMDLINE_PARAMS.md | 180 ------------------------------------- 1 file changed, 180 deletions(-) delete mode 100644 analysis/CMDLINE_PARAMS.md diff --git a/analysis/CMDLINE_PARAMS.md b/analysis/CMDLINE_PARAMS.md deleted file mode 100644 index 8c0cf21a..00000000 --- a/analysis/CMDLINE_PARAMS.md +++ /dev/null @@ -1,180 +0,0 @@ -# Command-Line Parameters for Configuration Files - -This feature allows you to pass parameters from the command line into your configuration files, enabling easy parametric studies and configuration reuse without modifying the config file itself. - -## Usage - -Pass parameters using the `-p` or `--param` flag with `KEY=VALUE` format: - -```bash -python3.11 driver.py my_config.py -p distance=1.5 -p basis=sto-6g -``` - -Multiple parameters can be specified: - -```bash -python3.11 driver.py my_config.py -p distance=1.5 -p basis=sto-6g -p num_steps=100 -``` - -## Accessing Parameters in Configuration Files - -All command-line parameters are available via the `params` dictionary that is automatically provided to your configuration file. - -### Method 1: Using `params.get()` with defaults (recommended) - -```python -# Access with a default value -distance = params.get('distance', 1.30) # Defaults to 1.30 if not provided -basis_set = params.get('basis', 'sto-3g') # Defaults to 'sto-3g' if not provided - -hamiltonian.add_atom("H", distance, 0, 0) -hamiltonian.basis = basis_set -``` - -### Method 2: Direct dictionary access (if parameter is required) - -```python -# Access parameter directly - will raise KeyError if not provided -distance = params['distance'] -hamiltonian.add_atom("H", distance, 0, 0) -``` - -**Note:** This will raise a `KeyError` if the parameter wasn't provided on the command line. - -## Type Conversion - -Values are automatically evaluated as Python expressions when possible: - -| Command Line | Type | Value | -|--------------|------|-------| -| `-p x=1.5` | float | `1.5` | -| `-p n=42` | int | `42` | -| `-p items=[1,2,3]` | list | `[1, 2, 3]` | -| `-p flag=True` | bool | `True` | -| `-p name=hello` | string | `'hello'` (eval fails, treated as string) | - -## Examples - -### Example 1: Parametric distance study - -**Configuration file** (`distance_study_config.py`): -```python -# Use command-line parameter for distance -distance = params.get('distance', 1.30) - -general.print_verbose() -general.file_stub = f"Be-H_{distance:.2f}_sto-6g" -general.file_format = "default" - -hamiltonian.add_atom("Be", 0.00, 0, 0) -hamiltonian.add_atom("H", distance, 0, 0) -hamiltonian.basis = "sto-6g" -hamiltonian.num_active_occupied = 3 -hamiltonian.num_active_vacant = 3 -``` - -**Run with different distances**: -```bash -python3.11 driver.py distance_study_config.py -p distance=1.2 -python3.11 driver.py distance_study_config.py -p distance=1.5 -python3.11 driver.py distance_study_config.py -p distance=2.0 -``` - -### Example 2: Basis set comparison - -```bash -# Same config, different basis sets -python3.11 driver.py config.py -p basis=sto-3g -python3.11 driver.py config.py -p basis=sto-6g -python3.11 driver.py config.py -p basis=6-31g -``` - -### Example 3: Multiple parameters - -**Configuration file**: -```python -distance = params.get('distance', 1.30) -basis = params.get('basis', 'sto-3g') -n_occ = int(params.get('num_occupied', 3)) -n_vac = int(params.get('num_vacant', 3)) - -general.file_stub = f"Be-H_{distance}_{basis}_occ{n_occ}_vac{n_vac}" - -hamiltonian.add_atom("Be", 0.00, 0, 0) -hamiltonian.add_atom("H", distance, 0, 0) -hamiltonian.basis = basis -hamiltonian.num_active_occupied = n_occ -hamiltonian.num_active_vacant = n_vac -``` - -**Run**: -```bash -python3.11 driver.py config.py -p distance=1.5 -p basis=sto-6g -p num_occupied=4 -p num_vacant=4 -``` - -### Example 4: Scripting parametric studies - -```bash -#!/bin/bash -# Run a sweep over different distances - -for dist in 1.0 1.2 1.4 1.6 1.8 2.0; do - echo "Running with distance = $dist" - python3.11 driver.py my_config.py -p distance=$dist -done -``` - -## Advanced Usage - -### Type conversion and validation - -```python -# Ensure integer types -num_steps = int(params.get('num_steps', 100)) - -# Convert string to float -energy = float(params.get('energy', '0.0')) - -# Parse complex structures -if 'atoms' in params: - for atom_type, position in params['atoms']: - hamiltonian.add_atom(atom_type, position, 0, 0) -``` - -### Conditional logic based on parameters - -```python -mode = params.get('mode', 'production') - -if mode == 'debug': - general.print_debug() -elif mode == 'verbose': - general.print_verbose() -else: - general.print_default() - -# Adjust other settings based on mode -algorithm.num_phase_qubits = 8 if mode == 'debug' else 12 -``` - -## Error Handling - -### Invalid format -```bash -# Missing '=' will raise ValueError -python3.11 driver.py config.py -p bad_param -# Error: Parameter must be in KEY=VALUE format, got: bad_param -``` - -### Using undefined parameters without defaults -```python -# In config file: -x = params['missing_key'] # KeyError if not provided -# Better: -x = params.get('missing_key', default_value) # Safe with default -``` - -## See Also - -- `examples/cmdline_params_demo_config.py` - Full demonstration of parameter usage -- `tests/test_cmdline_params.py` - Test suite showing various use cases From 688067729499bea6066080548b46319b739acc05 Mon Sep 17 00:00:00 2001 From: "Brendan K. Krueger" Date: Wed, 19 Aug 2026 11:56:39 -0600 Subject: [PATCH 07/11] Change config.py to demonstrate the use of command-line arguments. --- analysis/config.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/analysis/config.py b/analysis/config.py index 7ff7f644..e6867c6d 100644 --- a/analysis/config.py +++ b/analysis/config.py @@ -15,10 +15,11 @@ 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 for all generated files (logfiles, matrices, eigendecompositions, etc.). If +# not set or empty, files are written to the current directory. Can be changed on the command line +# using `-p "output_directory=directory_name"`, demonstrating the use of `params` to allow +# command-line arguments. +general.output_directory = params.get("output_directory", "Be-H") general.logfile = "Be-H.log" From 0dd8d4e0988602c45de6dc042435861c04a668ca Mon Sep 17 00:00:00 2001 From: "Brendan K. Krueger" Date: Wed, 19 Aug 2026 11:57:05 -0600 Subject: [PATCH 08/11] Remove new example that doens't work the way I want. --- .../examples/cmdline_params_demo_config.py | 51 ------------------- 1 file changed, 51 deletions(-) delete mode 100644 analysis/examples/cmdline_params_demo_config.py diff --git a/analysis/examples/cmdline_params_demo_config.py b/analysis/examples/cmdline_params_demo_config.py deleted file mode 100644 index 75ea7505..00000000 --- a/analysis/examples/cmdline_params_demo_config.py +++ /dev/null @@ -1,51 +0,0 @@ -# _________________________________________________________________________________________________ -# Example configuration demonstrating command-line parameter usage -# -# This config can be run with parameters like: -# python3.11 driver.py examples/cmdline_params_demo_config.py -p distance=1.5 -p basis=sto-6g -# -# Parameters are accessed via the 'params' dictionary that is provided to all configuration files. -# _________________________________________________________________________________________________ - -# _________________________________________________________________________________________________ -# General configuration - -general.print_verbose() -general.file_stub = "Be-H_cmdline_demo" -general.file_format = "default" - -# _________________________________________________________________________________________________ -# Demonstrate different ways to use command-line parameters - -# Method 1: Use parameter with a default value (recommended) -distance = params.get('distance', 1.30) # Default to 1.30 if not provided -basis_set = params.get('basis', 'sto-3g') # Default to 'sto-3g' if not provided - -# Method 2: Require parameter (will raise KeyError if not provided) -# Uncomment this to require the parameter: -# distance = params['distance'] # Will raise KeyError if -p distance=... not provided - -# Method 3: Use parameter with type conversion and validation -num_occupied = int(params.get('num_occupied', 3)) -num_vacant = int(params.get('num_vacant', 3)) - -# _________________________________________________________________________________________________ -# Describe the Hamiltonian using the parameters - -hamiltonian.add_atom("Be", 0.00, 0, 0) -hamiltonian.add_atom("H", distance, 0, 0) # Uses the distance parameter - -hamiltonian.basis = basis_set # Uses the basis parameter - -hamiltonian.num_active_occupied = num_occupied # Uses the num_occupied parameter -hamiltonian.num_active_vacant = num_vacant # Uses the num_vacant parameter - -# _________________________________________________________________________________________________ -# Parameters can also be used in more complex expressions - -# Example: You could pass a list of atoms -# python3.11 driver.py config.py -p atoms='[("Be",0.0),("H",1.3)]' -# Then use it like: -# if 'atoms' in params: -# for atom_type, position in params['atoms']: -# hamiltonian.add_atom(atom_type, position, 0, 0) From afeeedab6f052caf7145b1a4bff4494e4b9d74f3 Mon Sep 17 00:00:00 2001 From: "Brendan K. Krueger" Date: Wed, 19 Aug 2026 12:42:33 -0600 Subject: [PATCH 09/11] Update documentation. --- analysis/README.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/analysis/README.md b/analysis/README.md index 026664d3..b2ee1983 100644 --- a/analysis/README.md +++ b/analysis/README.md @@ -13,6 +13,40 @@ 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 @@ -20,6 +54,25 @@ 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 @@ -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 From d9328f3e1e32d6d63eb5017e62bd0d239fa72f20 Mon Sep 17 00:00:00 2001 From: "Brendan K. Krueger" Date: Wed, 19 Aug 2026 12:42:49 -0600 Subject: [PATCH 10/11] Update tests. --- analysis/tests/test_cmdline_params.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/analysis/tests/test_cmdline_params.py b/analysis/tests/test_cmdline_params.py index 5a12dba9..c79fd8ae 100644 --- a/analysis/tests/test_cmdline_params.py +++ b/analysis/tests/test_cmdline_params.py @@ -32,7 +32,7 @@ def test_cmdline_params_basic(monkeypatch, tmp_path): monkeypatch.setattr(sys, 'argv', test_args) # Load configuration - state = load_configuration() + state, _, _ = load_configuration() # Verify the parameters were used assert 'hello' in state.config_general.output_directory @@ -68,7 +68,7 @@ def test_cmdline_params_type_conversion(monkeypatch, tmp_path): ] monkeypatch.setattr(sys, 'argv', test_args) - state = load_configuration() + state, _, _ = load_configuration() # If we got here without errors, the types were correctly evaluated assert True @@ -90,7 +90,7 @@ def test_cmdline_params_string_fallback(monkeypatch, tmp_path): ] monkeypatch.setattr(sys, 'argv', test_args) - state = load_configuration() + state, _, _ = load_configuration() assert state.config_general.output_directory == 'hello-world' @@ -107,7 +107,7 @@ def test_cmdline_params_no_params(monkeypatch, tmp_path): test_args = ['driver.py', str(config_file)] monkeypatch.setattr(sys, 'argv', test_args) - state = load_configuration() + state, _, _ = load_configuration() assert state.config_general.output_directory == 'default_value' @@ -148,7 +148,7 @@ def test_cmdline_params_multiple_params(monkeypatch, tmp_path): ] monkeypatch.setattr(sys, 'argv', test_args) - state = load_configuration() + state, _, _ = load_configuration() assert state.config_general.output_directory == 'test_1_2_3' @@ -172,6 +172,6 @@ def test_cmdline_params_not_in_global_namespace(monkeypatch, tmp_path): ] monkeypatch.setattr(sys, 'argv', test_args) - state = load_configuration() + state, _, _ = load_configuration() # Should have caught the NameError, not accessed my_param directly assert state.config_general.output_directory == 'params_not_global' From adccd4c711134c07c8310a0ffc2a96ed4c7d6532 Mon Sep 17 00:00:00 2001 From: "Brendan K. Krueger" Date: Wed, 19 Aug 2026 12:55:53 -0600 Subject: [PATCH 11/11] Slightly streamline the CI pipelines using command-line arguments. --- .github/workflows/python-app.yml | 21 ++++++++------------- .gitlab-ci.yml | 17 +++++++++++++++-- analysis/config.py | 31 +++++++++++++++++++++++-------- 3 files changed, 46 insertions(+), 23 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index ab74dfaa..98d14ec4 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -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 }} @@ -117,12 +118,6 @@ 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 @@ -130,5 +125,5 @@ jobs: 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 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dd317387..4734b5d3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -52,6 +52,20 @@ 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: @@ -59,8 +73,7 @@ run-analysis-dbl-fact: - 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: diff --git a/analysis/config.py b/analysis/config.py index e6867c6d..1af0a437 100644 --- a/analysis/config.py +++ b/analysis/config.py @@ -1,10 +1,21 @@ """ QHAT Analysis Configuration: basic example + +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. + +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= +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) @@ -15,11 +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. Can be changed on the command line -# using `-p "output_directory=directory_name"`, demonstrating the use of `params` to allow -# command-line arguments. -general.output_directory = params.get("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"