diff --git a/docker/quantum/Dockerfile b/docker/quantum/Dockerfile new file mode 100644 index 0000000..681f8a9 --- /dev/null +++ b/docker/quantum/Dockerfile @@ -0,0 +1,31 @@ +FROM python:3.7 +#FROM continuumio/miniconda3:4.8.2 as build + +# Switch back to root, set up the environment. +#USER 0 +#ENV PATH /opt/conda/bin/:$PATH + +COPY src/* /app/ +COPY requirements.txt /app/ + +# Update apt, install blas/lapack for psi4, then Python deps, and then build. +RUN apt-get update && \ + apt-get install -y libblas-dev liblapack-dev && \ + cd /root && \ + # Needed for projectq + pip install pybind11 && \ + pip install -r /app/requirements.txt && \ + wget https://github.com/psi4/psi4/archive/v1.3.2.tar.gz && \ + tar zxvf v1.3.2.tar.gz && \ + mkdir build && \ + cd build && \ + cmake ../psi4-1.3.2 && make -j10 install && \ + rm -rf /usr/local/psi4/share/psi4/samples && \ + cd / && rm -rf /root && mkdir /root && \ + rm -rf /var/lib/apt/lists/* + +ENV PYTHONPATH /usr/local/psi4/lib:$PYTHONPATH +ENV LC_ALL=C.UTF-8 +ENV LANG=C.UTF-8 + +ENTRYPOINT ["python", "/app/main.py"] diff --git a/docker/quantum/README.md b/docker/quantum/README.md new file mode 100644 index 0000000..fb24d09 --- /dev/null +++ b/docker/quantum/README.md @@ -0,0 +1,47 @@ +Running the Qiskit Docker Container +================================= + +In order to run the Qiskit docker container, first build the +docker image: + +``` +docker build -t openchemistry/qiskit:1.0 . +``` + +Two input files are required: a geometry file and a parameters file. +The geometry file should be in `xyz` format. The parameters file should be in +`json` format. Example input files are provided [here](example). Look in the +example parameters file to see the different parameters that may be set. + +The following options should be specified in the command line: + +*After `docker run`:* +* `-v`: Mount the input directory into the docker container. + +*After the image name:* +* `-g`: The location of the input geometry in the docker container. +* `-p`: The location of the parameters file in the docker container. +* `-o`: The location and name of the output file in the docker container + (it should be placed in the mounted directory). +* `-s`: The location of the scratch directory in the docker container. + +A complete example can be seen below: + +``` +cd example/ +docker run -v $(pwd):/data openchemistry/qiskit:1.0 -g /data/geometry.xyz -p /data/parameters.json -o /data/out.cjson -s /data/scratch +``` + +Or to optimize the geometry with Psi4 before running the calculation: + +``` +docker run -v $(pwd):/data openchemistry/qiskit:1.0 -g /data/geometry.xyz -p /data/optimization_parameters.json -o /data/out.cjson -s /data/scratch +``` + +After the docker container finishes, the output file will be located in +the example directory. The output file is in `cjson` format. + +A json description of the image and some of the options may be obtained via: +``` +docker run openchemistry/qiskit:1.0 -d +``` diff --git a/docker/quantum/example/geometry.xyz b/docker/quantum/example/geometry.xyz new file mode 100644 index 0000000..cada1ea --- /dev/null +++ b/docker/quantum/example/geometry.xyz @@ -0,0 +1,4 @@ +2 + +Li 0.00000 0.00000 0.00000 +Li 0.00000 0.00000 2.67298 \ No newline at end of file diff --git a/docker/quantum/example/optimization_parameters.json b/docker/quantum/example/optimization_parameters.json new file mode 100644 index 0000000..142c7e0 --- /dev/null +++ b/docker/quantum/example/optimization_parameters.json @@ -0,0 +1,12 @@ +{ + "basis": "sto3g", + "charge": 0, + "multiplicity": 1, + "orbital_reduction": [4, 10], + "optimization":{ + "theory": "hf", + "basis": "6-31g", + "charge": 0, + "multiplicity": 1 + } +} \ No newline at end of file diff --git a/docker/quantum/example/parameters.json b/docker/quantum/example/parameters.json new file mode 100644 index 0000000..142c7e0 --- /dev/null +++ b/docker/quantum/example/parameters.json @@ -0,0 +1,12 @@ +{ + "basis": "sto3g", + "charge": 0, + "multiplicity": 1, + "orbital_reduction": [4, 10], + "optimization":{ + "theory": "hf", + "basis": "6-31g", + "charge": 0, + "multiplicity": 1 + } +} \ No newline at end of file diff --git a/docker/quantum/requirements.txt b/docker/quantum/requirements.txt new file mode 100644 index 0000000..3a04f26 --- /dev/null +++ b/docker/quantum/requirements.txt @@ -0,0 +1,11 @@ +scipy +qiskit +projectq +avogadro +click +jinja2 +cmake +pint +pydantic +deepdiff # Need for psi4 +pyscf diff --git a/docker/quantum/src/__init__.py b/docker/quantum/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docker/quantum/src/describe.py b/docker/quantum/src/describe.py new file mode 100644 index 0000000..0423bee --- /dev/null +++ b/docker/quantum/src/describe.py @@ -0,0 +1,6 @@ +import json +import os + +def get_description(): + with open(os.path.join(os.path.dirname(__file__), 'description.json')) as f: + return json.load(f) diff --git a/docker/quantum/src/description.json b/docker/quantum/src/description.json new file mode 100644 index 0000000..801bef0 --- /dev/null +++ b/docker/quantum/src/description.json @@ -0,0 +1,59 @@ +{ + "name": "Quantum", + "version": "0.22", + "input": { + "format": "xyz", + "parameters": { + "basis": { + "type": "string", + "description": "The basis set.", + "default": "sto3g" + }, + "charge": { + "type": "number", + "description": "The net charge of the system.", + "default": 0 + }, + "multiplicity": { + "type": "number", + "description": "The spin multiplicity.", + "default": 1 + }, + "orbital_reduction": { + "type": "array", + "description": "Complexity of the calculation", + "default": [4, 10] + }, + "optimization": { + "theory": { + "type": "string", + "description": "The theory level: hf | dft", + "default": "hf" + }, + "basis": { + "type": "string", + "description": "The basis set.", + "default": "cc-pvdz" + }, + "functional": { + "type": "string", + "description": "The XC functional (if ks theory).", + "default": "b3lyp" + }, + "charge": { + "type": "number", + "description": "The net charge of the system.", + "default": 0 + }, + "multiplicity": { + "type": "number", + "description": "The spin multiplicity.", + "default": 1 + } + } + } + }, + "output": { + "format": "cjson" + } +} \ No newline at end of file diff --git a/docker/quantum/src/main.py b/docker/quantum/src/main.py new file mode 100644 index 0000000..5dd55eb --- /dev/null +++ b/docker/quantum/src/main.py @@ -0,0 +1,43 @@ +import click +import sys +import json + +import describe +import run + +def describe_callback(ctx, param, value): + if not value: + return + # Print the description object and stop execution of the container if --describe option is passed + description = json.dumps(describe.get_description(), indent=2) + click.echo(description, file=sys.stdout) + ctx.exit() + +@click.command() +@click.option('--describe', '-d', + help='Return an object to std output that describes the expected input and output parameters of this container.', + is_flag=True, is_eager=True, expose_value=False, callback=describe_callback) +@click.option('--geometry', '-g', 'geometry_file', multiple=True, + type=click.Path(exists=True, dir_okay=False, resolve_path=True), + required=True, + help='The path to the file containing the input geometry') +@click.option('--parameters', '-p', 'parameters_file', + type=click.Path(exists=True, dir_okay=False, resolve_path=True), + help='The path to the JSON file containing the input input parameters.') +@click.option('--output', '-o', 'output_file', multiple=True, + type=click.Path(exists=False, dir_okay=False, resolve_path=True), + required=True, + help='The path to the file that will contain the converted calculation output.') +@click.option('--scratch', '-s', 'scratch_dir', + type=click.Path(exists=False, dir_okay=True, file_okay=False, resolve_path=True), + help='The path to the directory that will be used as scratch space while running the calculation.') +def main(geometry_file, parameters_file, output_file, scratch_dir): + with open(parameters_file) as f: + params = json.load(f) + + for g, o in zip(geometry_file, output_file): + run.run_calculation(g, o, params, scratch_dir) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/docker/quantum/src/run.py b/docker/quantum/src/run.py new file mode 100644 index 0000000..7e0309b --- /dev/null +++ b/docker/quantum/src/run.py @@ -0,0 +1,118 @@ +import json +import psi4 + +from qiskit import BasicAer +from qiskit.aqua import QuantumInstance +from qiskit.chemistry.algorithms.ground_state_solvers.minimum_eigensolver_factories import VQEUCCSDFactory +from qiskit.chemistry.drivers import PySCFDriver, UnitsType, Molecule +from qiskit.chemistry.transformations import FermionicTransformation, FermionicQubitMappingType +from qiskit.aqua.algorithms import NumPyMinimumEigensolver, VQE +from qiskit.chemistry.algorithms.ground_state_solvers import GroundStateEigensolver + + +def optimize_geometry(geometry, params): + opt = params.get('optimization', {}) + + # Get parameters for optimization + # Use qiskit parameters if no optimization values are provided + theory = opt.get('theory', params.get('theory', 'hf')) + basis = opt.get('basis', params.get('basis', 'cc-pvdz')) + functional = opt.get('functional', params.get('functional', 'b3lyp')) + charge = opt.get('charge', params.get('charge', 0)) + multiplicity = opt.get('multiplicity', params.get('multiplicty', 1)) + + if theory.lower() == 'dft': + _theory = functional + reference = 'ks' + else: + _theory = 'scf' + reference = 'hf' + + if multiplicity == 1: + reference = 'r' + reference + else: + reference = 'u' + reference + + # Create molecule + geometry.insert(0, f'{charge} {multiplicity}') + mol = psi4.geometry(('\n').join(geometry)) + + # Optimize geometry + psi4.set_options({'reference': reference}) + psi4.core.be_quiet() + energy = psi4.optimize(f'{_theory}/{basis}', molecule=mol) + results = mol.to_dict() + + coords = results['geom'] + geom = [] + for elem, coord in zip(results['elem'], coords.reshape(-1, 3)): + geom.append([elem, coord]) + + return list(coords), geom, energy + + +def run_calculation(geometry_file, output_file, params, scratch_dir): + # Read in the geometry from the geometry file + # This container expects the geometry file to be in .xyz format + with open(geometry_file) as f: + atoms, comment, *xyz_structure = f.read().splitlines() + + # Warn the user that molecule should have 3 or fewer atoms + # TODO: Add warning + + # Optimize geometry if input has been provided + if params.get('optimization', {}): + coords, geom, psi4_energy = optimize_geometry(xyz_structure, params) + else: + geom, coords = [], [] + for atom in xyz_structure: + name, vals = atom.split(' ', 1) + vals = [val for val in vals.split(' ') if val] + coords.extend(vals) + geom.append([name, vals]) + + # Read the input parameters + basis = params.get('basis', 'cc-pvdz') + charge = params.get('charge', 0) + multiplicity = params.get('multiplicity', 1) + range_start, range_end = params.get('orbital_reduction', [4, 10]) + orbital_reduction = list(range(range_start, range_end)) + + # Set up the molcule, here we can feed data from OpenChemistry + molecule = Molecule(geometry=geom, charge=charge, + multiplicity=multiplicity) + + # Define the classical quantum chemistry driver to get the molecular integrals + # Also gives us the Hartree-Fock energy and orbital energies if desired + driver = PySCFDriver(molecule = molecule, unit=UnitsType.ANGSTROM, basis=basis) + + # Generating a small quantum calculation freezing core and throwing away virtuals, + # only keeping 2 electrons in 2 orbitals + transformation = FermionicTransformation(qubit_mapping=FermionicQubitMappingType.JORDAN_WIGNER, + freeze_core=True, orbital_reduction=orbital_reduction) + + # Quantum solver and wave function structure generator (UCCSD) + vqe_solver = VQEUCCSDFactory(QuantumInstance(BasicAer.get_backend('qasm_simulator')), + include_custom=True, method_doubles='succ_full') + + # Run the smallest quantum simulation + calc = GroundStateEigensolver(transformation, vqe_solver) + res = calc.solve(driver) + + cjson = { + 'chemicalJson': 1, + 'atoms': { + 'coords': { + '3d': coords + } + }, + 'properties': { + 'ground_state_energy': res.total_energies[0] + } + } + + if params.get('optimization', {}): + cjson['properties']['optimization_energy'] = psi4_energy + + with open(output_file, 'w') as f: + json.dump(cjson, f)