Repository: PennyLaneAI/pennylane
Pull request #3289 (2022-11-08 13:29:26)

Function to fetch molecular data from PubChem database
**Context:** Adds functionality to fetch molecular data from the PubChem database

**Description of the Change:** Adds `mol_data_from_pubchem` in `qchem/structure.py`

**Benefits:** Building molecules can be automated and made easier

**Possible Drawbacks:**

**Related GitHub Issues:**


github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [`doc/releases/changelog-dev.md`](/PennyLaneAI/pennylane/blob/master/doc/releases/changelog-dev.md) with:
* A one-to-two sentence description of the change. You may include a small working example for new features.
* A link back to this PR.
* Your name (or GitHub username) in the contributors section.
codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3289?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#3289](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3289?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (62f7783) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/1e5083d1e549791cf4be140013701c1e026644eb?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (1e5083d) will **decrease** coverage by `0.08%`.
> The diff coverage is `8.69%`.

```diff
@@            Coverage Diff             @@
##           master    #3289      +/-   ##
==========================================
- Coverage   99.70%   99.62%   -0.09%     
==========================================
  Files         299      299              
  Lines       26114    26137      +23     
==========================================
+ Hits        26037    26039       +2     
- Misses         77       98      +21     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3289?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/qchem/structure.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3289/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3FjaGVtL3N0cnVjdHVyZS5weQ==) | `82.20% <8.69%> (-17.80%)` | :arrow_down: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)


Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 2aeff87e8..f67b7ceed 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -4,9 +4,29 @@
 
 <h3>New features since last release</h3>
 
+* Functionality for fetching symbols and geometry of a compound from the PubChem Database using `qchem.mol_data`.
+  [(#3289)](https://github.com/PennyLaneAI/pennylane/pull/3289)
+ 
+  ```pycon
+  >>> mol_data("BeH2")
+  (['Be', 'H', 'H'],
+  array([[ 4.79405604,  0.29290815,  0.        ],
+         [ 3.77946   , -0.29290815,  0.        ],
+         [ 5.80884105, -0.29290815,  0.        ]]))
+
+  >>> mol_data(223, "CID")
+  (['N', 'H', 'H', 'H', 'H'],
+  array([[ 4.79404621,  0.        ,  0.        ],
+         [ 5.80882913,  0.5858151 ,  0.        ],
+         [ 3.77945225, -0.5858151 ,  0.        ],
+         [ 4.20823111,  1.01459396,  0.        ],
+         [ 5.3798613 , -1.01459396,  0.        ]]))
+  ```
+
 * New basis sets, `6-311g` and `CC-PVDZ`, are added to the qchem basis set repo.
   [#3279](https://github.com/PennyLaneAI/pennylane/pull/3279)
 
+
 <h3>Improvements</h3>
 
 * A representation has been added to the `Molecule` class.
@@ -27,3 +47,5 @@
 <h3>Contributors</h3>
 
 This release contains contributions from (in alphabetical order):
+
+Juan Miguel Arrazola, Utkarsh Azad, Soran Jahangiri,
\ No newline at end of file
diff --git a/pennylane/qchem/__init__.py b/pennylane/qchem/__init__.py
index 9f58a4ee7..59ef34d0d 100644
--- a/pennylane/qchem/__init__.py
+++ b/pennylane/qchem/__init__.py
@@ -58,7 +58,14 @@ from .molecule import Molecule
 from .observable_hf import fermionic_observable, qubit_observable, jordan_wigner
 from .number import particle_number
 from .spin import spin2, spinz
-from .structure import read_structure, active_space, excitations, hf_state, excitations_to_wires
+from .structure import (
+    read_structure,
+    active_space,
+    excitations,
+    hf_state,
+    excitations_to_wires,
+    mol_data,
+)
 from .tapering import (
     clifford,
     paulix_ops,
diff --git a/pennylane/qchem/structure.py b/pennylane/qchem/structure.py
index a06088af9..e1c8329df 100644
--- a/pennylane/qchem/structure.py
+++ b/pennylane/qchem/structure.py
@@ -16,6 +16,7 @@ build an active space and generate single and double excitations.
 """
 # pylint: disable=too-many-locals
 import os
+import re
 from shutil import copyfile
 
 from pennylane import numpy as np
@@ -412,3 +413,129 @@ def excitations_to_wires(singles, doubles, wires=None):
         doubles_wires.append([d1_wires, d2_wires])
 
     return singles_wires, doubles_wires
+
+
+def mol_data(identifier, identifier_type="name"):
+    r"""Obtain symbols and geometry of a compound from the PubChem Database.
+
+    The `PubChem <https://pubchem.ncbi.nlm.nih.gov>`__ database is one of the largest public
+    repositories for information on chemical substances from which symbols and geometry can be
+    retrieved for a compound by its name, SMILES, InChI, InChIKey, or PubChem Compound ID (CID) to
+    build a molecule object for Hartree-Fock calculations. The retrieved atomic coordinates will be
+    converted to atomic units <https://en.wikipedia.org/wiki/Bohr_radius>__ for consistency.
+
+    Args:
+        identifier (str or int): compound's identifier as required by the PubChem database
+        identifier_type (str): type of the provided identifier - name, CAS, CID, SMILES, InChI, InChIKey
+
+    Returns:
+        Tuple(list[str], array[float]): symbols and geometry (in Bohr radius) of the compound
+
+    **Example**
+
+    >>> mol_data("BeH2")
+    (['Be', 'H', 'H'],
+     array([[ 4.79405604,  0.29290815,  0.        ],
+            [ 3.77946   , -0.29290815,  0.        ],
+            [ 5.80884105, -0.29290815,  0.        ]]))
+
+    >>> mol_data(223, "CID")
+    (['N', 'H', 'H', 'H', 'H'],
+    array([[ 4.79404621,  0.        ,  0.        ],
+            [ 5.80882913,  0.5858151 ,  0.        ],
+            [ 3.77945225, -0.5858151 ,  0.        ],
+            [ 4.20823111,  1.01459396,  0.        ],
+            [ 5.3798613 , -1.01459396,  0.        ]]))
+
+    .. details::
+
+        ``mol_data`` can also be used with other chemical identifiers - CAS, SMILES, InChI, InChIKey:
+
+        >>> mol_data("74-82-8", "CAS")
+        (['C', 'H', 'H', 'H', 'H'],
+        array([[ 0.        ,  0.        ,  0.        ],
+                [ 1.04709725,  1.51102501,  0.93824902],
+                [ 1.29124986, -1.53710323, -0.47923455],
+                [-1.47058487, -0.70581271,  1.26460472],
+                [-0.86795121,  0.7320799 , -1.7236192 ]]))
+
+        >>> mol_data("[C]", "SMILES")
+        (['C', 'H', 'H', 'H', 'H'],
+        array([[ 0.        ,  0.        ,  0.        ],
+                [ 1.04709725,  1.51102501,  0.93824902],
+                [ 1.29124986, -1.53710323, -0.47923455],
+                [-1.47058487, -0.70581271,  1.26460472],
+                [-0.86795121,  0.7320799 , -1.7236192 ]]))
+
+        >>> mol_data("InChI=1S/CH4/h1H4", "InChI")
+        (['C', 'H', 'H', 'H', 'H'],
+        array([[ 0.        ,  0.        ,  0.        ],
+                [ 1.04709725,  1.51102501,  0.93824902],
+                [ 1.29124986, -1.53710323, -0.47923455],
+                [-1.47058487, -0.70581271,  1.26460472],
+                [-0.86795121,  0.7320799 , -1.7236192 ]]))
+
+        >>> mol_data("VNWKTOKETHGBQD-UHFFFAOYSA-N", "InChIKey")
+        (['C', 'H', 'H', 'H', 'H'],
+        array([[ 0.        ,  0.        ,  0.        ],
+                [ 1.04709725,  1.51102501,  0.93824902],
+                [ 1.29124986, -1.53710323, -0.47923455],
+                [-1.47058487, -0.70581271,  1.26460472],
+                [-0.86795121,  0.7320799 , -1.7236192 ]]))
+
+    """
+
+    try:
+        # pylint: disable=import-outside-toplevel, unused-import, multiple-imports
+        import pubchempy as pcp
+    except ImportError as Error:
+        raise ImportError(
+            "This feature requires pubchempy.\nIt can be installed with: pip install pubchempy."
+        ) from Error
+
+    # https://gist.github.com/lsauer/1312860/264ae813c2bd2c27a769d261c8c6b38da34e22fb#file-smiles_inchi_annotated-js
+    identifier_patterns = {
+        "name": re.compile(r"^[a-zA-Z0-9_+-]+$"),
+        "cas": re.compile(r"^\d{1,7}\-\d{2}\-\d$"),
+        "smiles": re.compile(
+            r"^(?!InChI=)(?!\d{1,7}\-\d{2}\-\d)(?![A-Z]{14}\-[A-Z]{10}(\-[A-Z])?)[^J][a-zA-Z0-9@+\-\[\]\(\)\\\/%=#$]{1,}$"
+        ),
+        "inchi": re.compile(
+            r"^InChI\=1S?\/[A-Za-z0-9\.]+(\+[0-9]+)?(\/[cnpqbtmsih][A-Za-z0-9\-\+\(\)\,\/\?\;\.]+)*$"
+        ),
+        "inchikey": re.compile(r"^[A-Z]{14}\-[A-Z]{10}(\-[A-Z])?"),
+    }
+    if identifier_type.lower() == "cid":
+        cid = int(identifier)
+    else:
+        if identifier_type.lower() not in identifier_patterns.keys():
+            raise ValueError(
+                "Specified identifier type is not supported. Supported type are: name, CAS, SMILES, InChI, InChIKey."
+            )
+        try:
+            if identifier_patterns[identifier_type.lower()].search(identifier):
+                if identifier_type.lower() == "cas":
+                    identifier_type = "name"
+                cid = pcp.get_cids(identifier, namespace=identifier_type.lower())[0]
+            else:
+                raise ValueError(
+                    f"Specified identifier doesn't seem to match type: {identifier_type}."
+                )
+        except (IndexError, pcp.NotFoundError) as exc:
+            raise ValueError("Specified molecule does not exist in the PubChem Database.") from exc
+
+    try:
+        pcp_molecule = pcp.Compound.from_cid(cid, record_type="3d")
+    except pcp.NotFoundError:
+        pcp_molecule = pcp.Compound.from_cid(cid, record_type="2d")
+    except ValueError as exc:
+        raise ValueError("Provided CID (or Identifier) is None.") from exc
+
+    data_mol = pcp_molecule.to_dict(properties=["atoms"])
+    symbols = [atom["element"] for atom in data_mol["atoms"]]
+    geometry = (
+        np.array([[atom["x"], atom["y"], atom.get("z", 0.0)] for atom in data_mol["atoms"]])
+        / bohr_angs
+    )
+
+    return symbols, geometry


Test patch:
diff --git a/tests/qchem/test_structure.py b/tests/qchem/test_structure.py
index 7bbe967eb..7a6eb1fb1 100644
--- a/tests/qchem/test_structure.py
+++ b/tests/qchem/test_structure.py
@@ -15,6 +15,7 @@
 Unit tests for the functions of the structure module.
 """
 import os
+import sys
 import pytest
 import pennylane as qml
 from pennylane import qchem
@@ -311,3 +312,68 @@ def test_inconsistent_active_spaces(
             active_electrons=act_electrons,
             active_orbitals=act_orbitals,
         )
+
+
+@pytest.mark.parametrize(
+    ("identifier", "identifier_type"),
+    [
+        ("CH4", "name"),
+        ("74-82-8", "CAS"),
+        ("[C]", "SMILES"),
+        ("InChI=1S/CH4/h1H4", "InChI"),
+        ("VNWKTOKETHGBQD-UHFFFAOYSA-N", "InChIKey"),
+        (297, "CID"),
+        ("H2", "name"),
+        ("InChI=1S/H2/h1H", "InChI"),
+        (783, "CID"),
+    ],
+)
+def test_consistent_pubchem_mol_data(identifier, identifier_type):
+    r"""Test that consistent molecular data from PubChem database is returned"""
+    pcp = pytest.importorskip("pubchempy")
+    ref_mol_data_3d = (
+        ["C", "H", "H", "H", "H"],
+        np.array(
+            [
+                [0.0, 0.0, 0.0],
+                [1.04709725, 1.51102501, 0.93824902],
+                [1.29124986, -1.53710323, -0.47923455],
+                [-1.47058487, -0.70581271, 1.26460472],
+                [-0.86795121, 0.7320799, -1.7236192],
+            ]
+        ),
+    )
+    ref_mol_data_2d = (["H", "H"], np.array([[3.77945225, 0.0, 0.0], [5.66917837, 0.0, 0.0]]))
+
+    pub_mol_data = qchem.mol_data(identifier, identifier_type)
+    ref_mol_data = ref_mol_data_2d if pub_mol_data[0] == ["H", "H"] else ref_mol_data_3d
+
+    assert ref_mol_data[0] == pub_mol_data[0]
+    assert np.allclose(ref_mol_data[1], pub_mol_data[1])
+
+
+@pytest.mark.parametrize(
+    ("identifier", "identifier_type", "message_match"),
+    [
+        ("CH4", "IUPAC", "Specified identifier type is not supported"),
+        ("In=1S/H3N/h1H3/p+1", "InChI", "Specified identifier doesn't seem to match type"),
+        ("14798039", "CAS", "Specified identifier doesn't seem to match type"),
+        ("beh2+", "name", "Specified molecule does not exist in the PubChem Database"),
+        (0, "CID", "Provided CID \(or Identifier\) is None"),
+    ],
+)
+def test_inconsistent_pubchem_mol_data(identifier, identifier_type, message_match):
+    r"""Test that an error is raised if an inconsistent parameters are provided to `mol_data`"""
+    pcp = pytest.importorskip("pubchempy")
+    with pytest.raises(ValueError, match=message_match):
+        qchem.mol_data(identifier, identifier_type)
+
+
+def test_import_pubchem_mol_data(monkeypatch):
+    """Test that an exception is caught on import error for pubchempy"""
+
+    with monkeypatch.context() as m:
+        m.setitem(sys.modules, "pubchempy", None)
+
+        with pytest.raises(ImportError, match="This feature requires pubchempy"):
+            qchem.mol_data(227, "CID")