From 19b51bee9f00b00af5d435583347c827460b2db7 Mon Sep 17 00:00:00 2001 From: soranjh Date: Wed, 5 Nov 2025 16:25:01 -0500 Subject: [PATCH 01/13] add demo --- .../tutorial_qchem_chemphys/demo.py | 484 ++++++++++++++++++ .../tutorial_qchem_chemphys/metadata.json | 39 ++ .../tutorial_qchem_chemphys/requirements.in | 2 + 3 files changed, 525 insertions(+) create mode 100644 demonstrations_v2/tutorial_qchem_chemphys/demo.py create mode 100644 demonstrations_v2/tutorial_qchem_chemphys/metadata.json create mode 100644 demonstrations_v2/tutorial_qchem_chemphys/requirements.in diff --git a/demonstrations_v2/tutorial_qchem_chemphys/demo.py b/demonstrations_v2/tutorial_qchem_chemphys/demo.py new file mode 100644 index 0000000000..1986c59560 --- /dev/null +++ b/demonstrations_v2/tutorial_qchem_chemphys/demo.py @@ -0,0 +1,484 @@ +r""" + +Molecular Hamiltonian Representations +===================================== + +.. meta:: + :property="og:description": Learn how to use chemist, physicist and quantum notations + :property="og:image": https://pennylane.ai/qml/_static/demonstration_assets/thumbnail_tutorial_external_libs.png + + +.. related:: + tutorial_quantum_chemistry Quantum chemistry with PennyLane + +Molecular Hamiltonians can be constructed in different ways depending on the the arrangement of +the two-electron integral tensor. Here, we review the common ways to represent a molecular +Hamiltonian. We use three common two-electron integral notations that are typically referred to +as physicists', chemists' and quantum computing notation. The two-electron integrals computed with +one convention can be easily converted to the other notations. Such conversions allow constructing +different representations of the molecular Hamiltonian without re-calculating the integrals. +""" + +############################################################################## +# Quantum computing notation +# -------------------------- +# This notation is commonly used in the quantum computing literature and quantum computing +# software libraries. +# +# The two-electron integral tensor in this notation is defined as +# +# .. math:: +# +# \langle \langle pq | rs \rangle \rangle = \int dr_1 dr_2 \phi_p^*(r_1) \phi_q^*(r_2) \frac{1}{r_{12}} \phi_r(r_2) \phi_s(r_1). +# +# The corresponding Hamiltonian in second quantization is defined in terms of the fermionic +# creation and annihilation operators as +# +# .. math:: +# +# H = \sum_{pq} h_{pq} a_p^{\dagger} a_q + \frac{1}{2} \sum_{pqrs} h_{pqrs} a_p^{\dagger} a_q^{\dagger} a_r a_s. +# +# where :math:`h_{pq}` denotes the one-electron integral. We have skipped the spin indices and +# the core constant for brevity. Note that the order of the creation and annihilation operator +# indices matches the order of the coefficient indices for both :math:`h_{pq}` and h_{pqrs} terms. +# +# We will now construct the Hamiltonian using PennyLane. PennyLane employs the quantum computing +# convention for the two-electron integral tensor, which can be efficiently computed via the +# built-in electron_integrals function. We use the water molecule as an example. + +import numpy as np +import pennylane as qml +from pennylane.fermi import FermiWord, FermiSentence, from_string + +symbols = ["H", "O", "H"] +geometry = np.array([[-0.0399, -0.0038, 0.0000], + [1.5780, 0.8540, 0.0000], + [2.7909, -0.5159, 0.0000]]) + +mol = qml.qchem.Molecule(symbols, geometry) + +core_constant, one_mo, two_mo = qml.qchem.electron_integrals(mol)() + + +############################################################################## +# These integrals are obtained over spatial molecular orbitals. That means, for water, we have +# only reported the integrals over the 1s, 2s, and the 2p_x, 2p_y and 2p_z orbitals, without +# accounting for the spin component. To construct the full +# Hamiltonian, the integrals objects need to be expanded to include spin orbitals, i.e., 1s_alpha, +# 1s_beta etc. Assuming an interleaved convention for the order of spin orbitals, i.e., +# |alpha, beta, alpha, beta, ...>, the following functions give the expanded one-electron and +# two-electron objects. + +def transform_one(h_mo: np.ndarray) -> np.ndarray: + """ + """ + n_so = 2 * h_mo.shape[0] + h_so = np.zeros((n_so, n_so)) + + alpha, beta = slice(0, n_so, 2), slice(1, n_so, 2) + + h_so[alpha, alpha] = h_mo + h_so[beta, beta] = h_mo + + return h_so + + +def transform_two(g_mo: np.ndarray) -> np.ndarray: + """ + """ + n_so = 2 * g_mo.shape[0] + + g_so = np.zeros((n_so, n_so, n_so, n_so)) + + alpha = slice(0, n_so, 2) + beta = slice(1, n_so, 2) + + g_so[alpha, alpha, alpha, alpha] = g_mo + g_so[alpha, beta, beta, alpha] = g_mo + g_so[beta, alpha, alpha, beta] = g_mo + g_so[beta, beta, beta, beta] = g_mo + + return g_so + + +############################################################################## +# Note that the transformation must respect the spin-selection rules. For instance, the integral +# :math:`\langle \chi_p | \hat{h} | \chi_q \rangle = \langle \phi_i | \hat{h}^{\text{spatial}} | \phi_j \rangle \cdot \langle \sigma_p | \sigma_q \rangle` +# must be zero if the spins of the initial and final spin orbitals are different. For the +# one-electron integrals, only αα or ββ combinations have a non-zero +# value. Similarly, the two-electron operator 1/r_{12} is spin-independent and if we use the +# compact notation '1221' to represent the order in the two-electron integral, the non-zero +# combinations are: αααα, αββα, βααβ and ββββ. The integrals computed using molecular orbitals +# can be expanded to include spin orbitals using these combination rules. We now obtain the full +# electron integrals in the basis of spin orbitals. + +one_so = transform_one(one_mo) +two_so = transform_two(two_mo) + +############################################################################## +# Having the electron integrals objects, computing the Hamiltonian is straightforward. We simply +# loop over the elements of the tensors and multiply them with the fermionic operators with the +# indices matching those of the integral elements. For better performance, we can skip the zero +# integral elements and just obtain the operator indices for the non-zero integrals. + +one_operators = qml.math.argwhere(abs(one_so) >= 1e-12) +two_operators = qml.math.argwhere(abs(two_so) >= 1e-12) + +############################################################################## +# Now we construct the Hamiltonian as a FermiSentence object. + +sentence = FermiSentence({FermiWord({}): core_constant[0]}) + +for o in one_operators: + sentence.update({from_string(f'{o[0]}+ {o[1]}-'): one_so[*o]}) + +for o in two_operators: + sentence.update({from_string(f'{o[0]}+ {o[1]}+ {o[2]}- {o[3]}-'): two_so[*o] / 2}) + +sentence.simplify() + +############################################################################## +# Note that the order of indices for the fermionic creation and annihilation operators matches +# the order of indices in the integral objects. We finally map the fermionic Hamiltonian to the +# qubit bases and compute its ground-state energy, which should match the value -75.01562736 a.u. + +h = qml.jordan_wigner(sentence) +qml.eigvals(qml.SparseHamiltonian(h.sparse_matrix(), wires=h.wires)) + +############################################################################## +# We can further verify this value by using the PennyLane function molecular_hamiltonian, which +# automatically constructs the Hamiltonian. + +h = qml.qchem.molecular_hamiltonian(mol)[0] +qml.eigvals(qml.SparseHamiltonian(h.sparse_matrix(), wires=h.wires)) + +# Chemists' notation +# ------------------ +# This notation is commonly used by quantum chemistry software libraries such as PySCF. The two- +# electron integral tensor in this notation is defined as +# +# .. math:: +# +# (pq | rs) = \int dr_1 dr_2 \phi_p^*(r_1) \phi_q(r_1) \frac{1}{r_{12}} \phi_r^*(r_2) \phi_s(r_2). +# +# The corresponding Hamiltonian in second quantization is then defined as +# +# .. math:: +# +# H = \sum_{pq} (h_{pq} - \frac{1}{2} \sum_s h_{pssq}) a_p^{\dagger} a_q + \frac{1}{2} \sum_{pqrs} h_{pqrs} a_p^{\dagger} a_q a_r^{\dagger} a_s. +# +# Note that a one-body correction term should be included. Also note that the order of indices of +# the creation and annihilation operators in the second term matches the order of the two-electron +# integral coefficients. See the appendix for a full derivation of the Hamiltonian. +# +# Let's now build the Hamiltonian using electron integrals computed by PySCF. + +from pyscf import gto, ao2mo, scf + +mol_pyscf = gto.M(atom='''H -0.02111417 -0.00201087 0.; + O 0.83504162 0.45191733 0.; + H 1.47688065 -0.27300252 0.''') +rhf = scf.RHF(mol_pyscf) +energy = rhf.kernel() + +one_ao = mol_pyscf.intor_symmetric('int1e_kin') + mol_pyscf.intor_symmetric('int1e_nuc') +two_ao = mol_pyscf.intor('int2e_sph') + +one_mo = np.einsum('pi,pq,qj->ij', rhf.mo_coeff, one_ao, rhf.mo_coeff) +two_mo = ao2mo.incore.full(two_ao, rhf.mo_coeff) + +core_constant = np.array([rhf.energy_nuc()]) + + +############################################################################## +# These integrals are also obtained for the spatial orbitals and we need to expand them to include +# spin orbitals. To do that, we need to slightly upgrade our transform_two_interleaved function +# because the allowed spin combination in the chemists' notation, also denoted by '1122', are +# αααα, ααββ, ββαα and ββββ. + +def transform_two(g_mo: np.ndarray, notation) -> np.ndarray: + """ + """ + n = g_mo.shape[0] + n_so = 2 * n + + g_so = np.zeros((n_so, n_so, n_so, n_so)) + + alpha = slice(0, n_so, 2) + beta = slice(1, n_so, 2) + + g_so[alpha, alpha, alpha, alpha] = g_mo + g_so[beta, beta, beta, beta] = g_mo + + if notation == '1221': + g_so[alpha, beta, beta, alpha] = g_mo + g_so[beta, alpha, alpha, beta] = g_mo + return g_so + + if notation == '1122': + g_so[alpha, alpha, beta, beta] = g_mo + g_so[beta, beta, alpha, alpha] = g_mo + return g_so + + +############################################################################## +# We can compute the full integrals objects and add the one-body correction term + +one_so = transform_one(one_mo) +two_so = transform_two(two_mo, '1122') + +one_so_corrected = one_so - 0.5 * np.einsum('pssq->pq', two_so) + +############################################################################## +# Constructing the Hamiltonian is now straightforward. + +one_operators = qml.math.argwhere(abs(one_so_corrected) >= 1e-12) +two_operators = qml.math.argwhere(abs(two_so) >= 1e-12) + +sentence = FermiSentence({FermiWord({}): core_constant[0]}) + +for o in one_operators: + sentence.update({from_string(f'{o[0]}+ {o[1]}-'): one_so_corrected[*o]}) + +for o in two_operators: + sentence.update({from_string(f'{o[0]}+ {o[1]}- {o[2]}+ {o[3]}-'): two_so[*o] / 2}) + +sentence.simplify() + +############################################################################## +# Note that the order of the indices for the fermionic operators match those of the electron +# integral coefficients. Also note that the two-body operator has the order +# :math:`a^{\dagger} a a^{\dagger} a'. Let's now validate the Hamiltonian by computing the ground- +# state energy. + +h = qml.jordan_wigner(sentence) +qml.eigvals(qml.SparseHamiltonian(h.sparse_matrix(), wires=h.wires)) + +############################################################################## +# Physicists' notation +# -------------------- +# The two-electron integral tensor in this notation is defined as +# +# .. math:: +# +# \langle pq | rs \rangle = \int dr_1 dr_2 \phi_p^*(r_1) \phi_q^*(r_2) \frac{1}{r_{12}} \phi_r(r_1) \phi_s(r_2). +# +# The corresponding Hamiltonian in second quantization is then constructed as +# +# .. math:: +# +# H = \sum_{pq} h_{pq} a_p^{\dagger} a_q + \frac{1}{2} \sum_{pqrs} h_{pqrs} a_p^{\dagger} a_q^{\dagger} a_s a_r, +# +# It is important to note the order of the creation and annihilation +# operators in the second term, which is :math:`a_p^{\dagger} a_q^{\dagger} a_s a_r`. This order +# must be preserved when the Hamiltonian is constracted with the two-electron integral tensor +# represented in the physicists' notation. +# +# Let's now build this Hamiltonian step-by-step. There is not a commonly-used software library +# to compute integrals in this notation but we can easily convert integrals obtained in other +# notations. For instance, we use PySCF integrals and convert them to physicists' notation. We can +# do this in two different ways: converting the integrals in the spatial orbital basis or +# converting them in the spin orbital basis. Let's do both. +# +# The conversion can be done by the transformation the transformation +# :math:`(pq | rs) \to (pr | qs)` where we have swapped the :math:`1,2` +# indices. This transformation can be done with + +one_mo = one_mo.copy() +two_mo = two_mo.transpose(0, 2, 1, 3) + + +############################################################################## +# We intentionally converted the integrals obtained for the spatial orbitals, so we need to expand +# them to include spin orbitals. To do that, we need to again slightly upgrade our +# transform_two_interleaved function to include the allowed spin combination in the physicists' +# notation, denoted by '1212', as αααα, αβαβ, βαβα and ββββ. + +def transform_two(g_mo: np.ndarray, notation) -> np.ndarray: + """ + """ + n = g_mo.shape[0] + n_so = 2 * n + + g_so = np.zeros((n_so, n_so, n_so, n_so)) + + alpha = slice(0, n_so, 2) + beta = slice(1, n_so, 2) + + g_so[alpha, alpha, alpha, alpha] = g_mo + g_so[beta, beta, beta, beta] = g_mo + + if notation == '1221': + g_so[alpha, beta, beta, alpha] = g_mo + g_so[beta, alpha, alpha, beta] = g_mo + return g_so + + if notation == '1122': + g_so[alpha, alpha, beta, beta] = g_mo + g_so[beta, beta, alpha, alpha] = g_mo + return g_so + + if notation == '1212': + g_so[alpha, beta, alpha, beta] = g_mo + g_so[beta, alpha, beta, alpha] = g_mo + return g_so + + ############################################################################## + + +# The new tensor can then be used to construct the Hamiltonian + +one_so = transform_one(one_mo) +two_so = transform_two(two_mo, '1212') + +one_operators = qml.math.argwhere(abs(one_so) >= 1e-12) +two_operators = qml.math.argwhere(abs(two_so) >= 1e-12) + +sentence = FermiSentence({FermiWord({}): core_constant[0]}) + +for o in one_operators: + sentence.update({from_string(f'{o[0]}+ {o[1]}-'): one_so[*o]}) + +for o in two_operators: + sentence.update({from_string(f'{o[0]}+ {o[1]}+ {o[3]}- {o[2]}-'): two_so[*o] / 2}) + +sentence.simplify() + +############################################################################## +# Note the order of the creation and annihilation operator indices. Now we compute the ground +# state energy to validate the Hamiltonian. + +h = qml.jordan_wigner(sentence) +qml.eigvals(qml.SparseHamiltonian(h.sparse_matrix(), wires=h.wires)) + + +############################################################################## +# Integral conversion +# ------------------- +# The two-electron integrals computed with one convention can be easily converted to the other +# conventions as we already did for the chemist to physicist conversion. Such conversions allow +# constructing different representations of the molecular Hamiltonian without re-calculating the +# integrals. Here we provide the conversion rules for all three conventions. + +def convert_integrals(two_body, in_notation, out_notation): + """ """ + if in_notation == out_notation: + return two_body + + if in_notation == "chemist" and out_notation == "physicist": + return two_body.transpose(0, 2, 1, 3) + + if in_notation == "chemist" and out_notation == "quantum": + return two_body.transpose(0, 2, 3, 1) + + if in_notation == "quantum" and out_notation == "chemist": + return two_body.transpose(0, 3, 1, 2) + + if in_notation == "quantum" and out_notation == "physicist": + return two_body.transpose(0, 1, 3, 2) + + if in_notation == "physicist" and out_notation == "chemist": + return two_body.transpose(0, 2, 1, 3) + + if in_notation == "physicist" and out_notation == "quantum": + return two_body.transpose(0, 1, 3, 2) + + +############################################################################## +# We can also create a versatile function that computes the Hamiltonian for each convention. + +def hamiltonian(one_body, two_body, notation, cutoff=1e-12): + if notation == "chemist": + one_body = one_body - 0.5 * np.einsum('pssq->pq', two_body) + + op_one = qml.math.argwhere(abs(one_body) >= cutoff) + op_two = qml.math.argwhere(abs(two_body) >= cutoff) + + sentence = FermiSentence({FermiWord({}): core_constant[0]}) + + for o in op_one: + sentence.update({FermiWord({(0, o[0]): "+", (1, o[1]): "-"}): one_body[*o]}) + + if notation == "quantum": + for o in op_two: + sentence.update({FermiWord( + {(0, o[0]): "+", (1, o[1]): "+", (2, o[2]): "-", (3, o[3]): "-"}): two_body[*o] / 2}) + + if notation == "chemist": + for o in op_two: + sentence.update({FermiWord( + {(0, o[0]): "+", (1, o[1]): "-", (2, o[2]): "+", (3, o[3]): "-"}): two_body[*o] / 2}) + + if notation == "physicist": + for o in op_two: + sentence.update({FermiWord( + {(0, o[0]): "+", (1, o[1]): "+", (2, o[3]): "-", (3, o[2]): "-"}): two_body[*o] / 2}) + + sentence.simplify() + + return qml.jordan_wigner(sentence) + + +############################################################################## +# We now have all the necessary tools in our arsenal to convert the integrals to a desired +# notation and construct the corresponding Hamiltonian. Let's look at a few examples. +# +# First we convert the integrals obtained with PennyLane in 'quantum' notation to the 'chemist' +# notation and compute the corresponding Hamiltonian. This conversion can be done by the +# transformation :math:` \to \to ij', rhf.mo_coeff, one_ao, rhf.mo_coeff) +two_mo = ao2mo.incore.full(two_ao, rhf.mo_coeff) +core_constant = np.array([rhf.energy_nuc()]) + +one_so = transform_one(one_mo) +two_so = transform_two(two_mo, '1122') + +two_so_converted = convert_integrals(two_so, 'chemist', 'quantum') + +h = hamiltonian(one_so, two_so_converted, 'quantum') +qml.eigvals(qml.SparseHamiltonian(h.sparse_matrix(), wires=h.wires)) + +############################################################################## +# Similarly, we go from chemist notation to the physicist notation + +two_so_converted = convert_integrals(two_so, 'chemist', 'physicist') + +h = hamiltonian(one_so, two_so_converted, 'physicist') +qml.eigvals(qml.SparseHamiltonian(h.sparse_matrix(), wires=h.wires)) + +############################################################################## +# The other converstions follow a similar logic. +# +# References +# ---------- +# +# .. [#szabo1996] +# +# Attila Szabo, Neil S. Ostlund, "Modern Quantum Chemistry: Introduction to Advanced Electronic +# Structure Theory". Dover Publications, 1996. diff --git a/demonstrations_v2/tutorial_qchem_chemphys/metadata.json b/demonstrations_v2/tutorial_qchem_chemphys/metadata.json new file mode 100644 index 0000000000..b0f8fa48c6 --- /dev/null +++ b/demonstrations_v2/tutorial_qchem_chemphys/metadata.json @@ -0,0 +1,39 @@ +{ + "title": "Molecular Hamiltonian Representations", + "authors": [ + { + "username": "soran" + } + ], + "executable_stable": true, + "executable_latest": true, + "dateOfPublication": "2025-11-05T00:00:00+00:00", + "dateOfLastModification": "2025-11-05T15:48:14+00:00", + "categories": [ + "Quantum Chemistry", + "Devices and Performance" + ], + "tags": [], + "previewImages": [ + { + "type": "thumbnail", + "uri": "/_static/demonstration_assets/external_libs/thumbnail_tutorial_external_libs.png" + }, + { + "type": "large_thumbnail", + "uri": "/_static/demo_thumbnails/large_demo_thumbnails/thumbnail_large_external_libs.png" + } + ], + "seoDescription": "Learn how to integrate external quantum chemistry libraries with PennyLane.", + "doi": "", + "references": [], + "basedOnPapers": [], + "referencedByPapers": [], + "relatedContent": [ + { + "type": "demonstration", + "id": "tutorial_quantum_chemistry", + "weight": 1.0 + } + ] +} \ No newline at end of file diff --git a/demonstrations_v2/tutorial_qchem_chemphys/requirements.in b/demonstrations_v2/tutorial_qchem_chemphys/requirements.in new file mode 100644 index 0000000000..58186cb893 --- /dev/null +++ b/demonstrations_v2/tutorial_qchem_chemphys/requirements.in @@ -0,0 +1,2 @@ +openfermion +pyscf From f7fac471123e29ab521f613860342ae5872f8e76 Mon Sep 17 00:00:00 2001 From: soranjh Date: Wed, 5 Nov 2025 16:51:04 -0500 Subject: [PATCH 02/13] correct format --- demonstrations_v2/tutorial_qchem_chemphys/demo.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/demonstrations_v2/tutorial_qchem_chemphys/demo.py b/demonstrations_v2/tutorial_qchem_chemphys/demo.py index 1986c59560..6c909a9dfa 100644 --- a/demonstrations_v2/tutorial_qchem_chemphys/demo.py +++ b/demonstrations_v2/tutorial_qchem_chemphys/demo.py @@ -152,6 +152,7 @@ def transform_two(g_mo: np.ndarray) -> np.ndarray: h = qml.qchem.molecular_hamiltonian(mol)[0] qml.eigvals(qml.SparseHamiltonian(h.sparse_matrix(), wires=h.wires)) +############################################################################## # Chemists' notation # ------------------ # This notation is commonly used by quantum chemistry software libraries such as PySCF. The two- @@ -248,8 +249,8 @@ def transform_two(g_mo: np.ndarray, notation) -> np.ndarray: ############################################################################## # Note that the order of the indices for the fermionic operators match those of the electron # integral coefficients. Also note that the two-body operator has the order -# :math:`a^{\dagger} a a^{\dagger} a'. Let's now validate the Hamiltonian by computing the ground- -# state energy. +# :math:`a^{\dagger} a a^{\dagger} a'. Let's now validate the Hamiltonian by computing the +# ground-state energy. h = qml.jordan_wigner(sentence) qml.eigvals(qml.SparseHamiltonian(h.sparse_matrix(), wires=h.wires)) @@ -323,9 +324,7 @@ def transform_two(g_mo: np.ndarray, notation) -> np.ndarray: g_so[beta, alpha, beta, alpha] = g_mo return g_so - ############################################################################## - - +############################################################################## # The new tensor can then be used to construct the Hamiltonian one_so = transform_one(one_mo) From 3fcadee8af27d6a8476128ae21b9aa024984d14f Mon Sep 17 00:00:00 2001 From: soranjh Date: Wed, 5 Nov 2025 17:29:10 -0500 Subject: [PATCH 03/13] update text and notation --- .../tutorial_qchem_chemphys/demo.py | 98 ++++++++++++------- .../tutorial_qchem_chemphys/metadata.json | 2 +- 2 files changed, 65 insertions(+), 35 deletions(-) diff --git a/demonstrations_v2/tutorial_qchem_chemphys/demo.py b/demonstrations_v2/tutorial_qchem_chemphys/demo.py index 6c909a9dfa..1e0d12f036 100644 --- a/demonstrations_v2/tutorial_qchem_chemphys/demo.py +++ b/demonstrations_v2/tutorial_qchem_chemphys/demo.py @@ -40,7 +40,8 @@ # # where :math:`h_{pq}` denotes the one-electron integral. We have skipped the spin indices and # the core constant for brevity. Note that the order of the creation and annihilation operator -# indices matches the order of the coefficient indices for both :math:`h_{pq}` and h_{pqrs} terms. +# indices matches the order of the coefficient indices for both :math:`h_{pq}` and :math:`h_{pqrs}` +# terms. # # We will now construct the Hamiltonian using PennyLane. PennyLane employs the quantum computing # convention for the two-electron integral tensor, which can be efficiently computed via the @@ -62,16 +63,25 @@ ############################################################################## # These integrals are obtained over spatial molecular orbitals. That means, for water, we have -# only reported the integrals over the 1s, 2s, and the 2p_x, 2p_y and 2p_z orbitals, without +# only reported the integrals over the :math:`1s`, :math:`2s`, and the :math:`2p_x`, :math:`2p_y` +# and :math:`2p_z` orbitals, without # accounting for the spin component. To construct the full -# Hamiltonian, the integrals objects need to be expanded to include spin orbitals, i.e., 1s_alpha, -# 1s_beta etc. Assuming an interleaved convention for the order of spin orbitals, i.e., -# |alpha, beta, alpha, beta, ...>, the following functions give the expanded one-electron and +# Hamiltonian, the integrals objects need to be expanded to include spin orbitals, i.e., :math:`1s_alpha`, +# :math:`1s_beta` etc. Assuming an interleaved convention for the order of spin orbitals, i.e., +# :math:`|\alpha, \beta, \alpha, \beta, ...>`, the following functions give the expanded one-electron and # two-electron objects. def transform_one(h_mo: np.ndarray) -> np.ndarray: + """Converts a one-electron integral matrix from the molecular orbital (MO) basis to the + spin-orbital (SO) basis. + + Args: + h_mo (array): The one-electron matrix with shape (n, n) where n is the number of spatial orbitals. + + Returns: + The one-electron matrix with shape (2n, 2n) where n is the number of spatial orbitals. """ - """ + n_so = 2 * h_mo.shape[0] h_so = np.zeros((n_so, n_so)) @@ -83,9 +93,17 @@ def transform_one(h_mo: np.ndarray) -> np.ndarray: return h_so -def transform_two(g_mo: np.ndarray) -> np.ndarray: - """ +def transform_two(g_mo): + """Converts a two-electron integral tensor from the molecular orbital (MO) basis to the + spin-orbital (SO) basis. + + Args: + g_mo (array): The two-electron tensor with shape (n, n, n, n) where n is the number of spatial orbitals. + + Returns: + The two-electron matrix with shape (2n, 2n, 2n, 2n) where n is the number of spatial orbitals. """ + n_so = 2 * g_mo.shape[0] g_so = np.zeros((n_so, n_so, n_so, n_so)) @@ -100,14 +118,13 @@ def transform_two(g_mo: np.ndarray) -> np.ndarray: return g_so - ############################################################################## # Note that the transformation must respect the spin-selection rules. For instance, the integral # :math:`\langle \chi_p | \hat{h} | \chi_q \rangle = \langle \phi_i | \hat{h}^{\text{spatial}} | \phi_j \rangle \cdot \langle \sigma_p | \sigma_q \rangle` # must be zero if the spins of the initial and final spin orbitals are different. For the # one-electron integrals, only αα or ββ combinations have a non-zero -# value. Similarly, the two-electron operator 1/r_{12} is spin-independent and if we use the -# compact notation '1221' to represent the order in the two-electron integral, the non-zero +# value. Similarly, the two-electron operator :math:`1/r_{12}` is spin-independent and if we use the +# compact notation ``'1221'`` to represent the order in the two-electron integral, the non-zero # combinations are: αααα, αββα, βααβ and ββββ. The integrals computed using molecular orbitals # can be expanded to include spin orbitals using these combination rules. We now obtain the full # electron integrals in the basis of spin orbitals. @@ -140,7 +157,7 @@ def transform_two(g_mo: np.ndarray) -> np.ndarray: ############################################################################## # Note that the order of indices for the fermionic creation and annihilation operators matches # the order of indices in the integral objects. We finally map the fermionic Hamiltonian to the -# qubit bases and compute its ground-state energy, which should match the value -75.01562736 a.u. +# qubit bases and compute its ground-state energy, which should match the value :math:`-75.01562736` a.u. h = qml.jordan_wigner(sentence) qml.eigvals(qml.SparseHamiltonian(h.sparse_matrix(), wires=h.wires)) @@ -194,12 +211,20 @@ def transform_two(g_mo: np.ndarray) -> np.ndarray: ############################################################################## # These integrals are also obtained for the spatial orbitals and we need to expand them to include # spin orbitals. To do that, we need to slightly upgrade our transform_two_interleaved function -# because the allowed spin combination in the chemists' notation, also denoted by '1122', are +# because the allowed spin combination in the chemists' notation, also denoted by ``'1122'``, are # αααα, ααββ, ββαα and ββββ. -def transform_two(g_mo: np.ndarray, notation) -> np.ndarray: - """ +def transform_two(g_mo, notation): + """Converts a two-electron integral tensor from the molecular orbital (MO) basis to the + spin-orbital (SO) basis. + + Args: + g_mo (array): The two-electron tensor with shape (n, n, n, n) where n is the number of spatial orbitals. + + Returns: + The two-electron matrix with shape (2n, 2n, 2n, 2n) where n is the number of spatial orbitals. """ + n = g_mo.shape[0] n_so = 2 * n @@ -211,12 +236,12 @@ def transform_two(g_mo: np.ndarray, notation) -> np.ndarray: g_so[alpha, alpha, alpha, alpha] = g_mo g_so[beta, beta, beta, beta] = g_mo - if notation == '1221': + if notation == 'quantum': # '1221' g_so[alpha, beta, beta, alpha] = g_mo g_so[beta, alpha, alpha, beta] = g_mo return g_so - if notation == '1122': + if notation == 'chemist': # '1122' g_so[alpha, alpha, beta, beta] = g_mo g_so[beta, beta, alpha, alpha] = g_mo return g_so @@ -226,7 +251,7 @@ def transform_two(g_mo: np.ndarray, notation) -> np.ndarray: # We can compute the full integrals objects and add the one-body correction term one_so = transform_one(one_mo) -two_so = transform_two(two_mo, '1122') +two_so = transform_two(two_mo, 'chemist') one_so_corrected = one_so - 0.5 * np.einsum('pssq->pq', two_so) @@ -293,11 +318,19 @@ def transform_two(g_mo: np.ndarray, notation) -> np.ndarray: # We intentionally converted the integrals obtained for the spatial orbitals, so we need to expand # them to include spin orbitals. To do that, we need to again slightly upgrade our # transform_two_interleaved function to include the allowed spin combination in the physicists' -# notation, denoted by '1212', as αααα, αβαβ, βαβα and ββββ. +# notation, denoted by ``'1212'``, as αααα, αβαβ, βαβα and ββββ. -def transform_two(g_mo: np.ndarray, notation) -> np.ndarray: - """ +def transform_two(g_mo, notation): + """Converts a two-electron integral tensor from the molecular orbital (MO) basis to the + spin-orbital (SO) basis. + + Args: + g_mo (array): The two-electron tensor with shape (n, n, n, n) where n is the number of spatial orbitals. + + Returns: + The two-electron matrix with shape (2n, 2n, 2n, 2n) where n is the number of spatial orbitals. """ + n = g_mo.shape[0] n_so = 2 * n @@ -309,17 +342,17 @@ def transform_two(g_mo: np.ndarray, notation) -> np.ndarray: g_so[alpha, alpha, alpha, alpha] = g_mo g_so[beta, beta, beta, beta] = g_mo - if notation == '1221': + if notation == 'quantum': # '1221' g_so[alpha, beta, beta, alpha] = g_mo g_so[beta, alpha, alpha, beta] = g_mo return g_so - if notation == '1122': + if notation == 'chemist': # '1122' g_so[alpha, alpha, beta, beta] = g_mo g_so[beta, beta, alpha, alpha] = g_mo return g_so - if notation == '1212': + if notation == 'physicist': # '1212' g_so[alpha, beta, alpha, beta] = g_mo g_so[beta, alpha, beta, alpha] = g_mo return g_so @@ -328,7 +361,7 @@ def transform_two(g_mo: np.ndarray, notation) -> np.ndarray: # The new tensor can then be used to construct the Hamiltonian one_so = transform_one(one_mo) -two_so = transform_two(two_mo, '1212') +two_so = transform_two(two_mo, 'physicist') one_operators = qml.math.argwhere(abs(one_so) >= 1e-12) two_operators = qml.math.argwhere(abs(two_so) >= 1e-12) @@ -396,22 +429,19 @@ def hamiltonian(one_body, two_body, notation, cutoff=1e-12): sentence = FermiSentence({FermiWord({}): core_constant[0]}) for o in op_one: - sentence.update({FermiWord({(0, o[0]): "+", (1, o[1]): "-"}): one_body[*o]}) + sentence.update({from_string(f'{o[0]}+ {o[1]}-'): one_body[*o]}) if notation == "quantum": for o in op_two: - sentence.update({FermiWord( - {(0, o[0]): "+", (1, o[1]): "+", (2, o[2]): "-", (3, o[3]): "-"}): two_body[*o] / 2}) + sentence.update({from_string(f'{o[0]}+ {o[1]}+ {o[2]}- {o[3]}-'): two_body[*o] / 2}) if notation == "chemist": for o in op_two: - sentence.update({FermiWord( - {(0, o[0]): "+", (1, o[1]): "-", (2, o[2]): "+", (3, o[3]): "-"}): two_body[*o] / 2}) + sentence.update({from_string(f'{o[0]}+ {o[1]}- {o[2]}+ {o[3]}-'): two_body[*o] / 2}) if notation == "physicist": for o in op_two: - sentence.update({FermiWord( - {(0, o[0]): "+", (1, o[1]): "+", (2, o[3]): "-", (3, o[2]): "-"}): two_body[*o] / 2}) + sentence.update({from_string(f'{o[0]}+ {o[1]}+ {o[3]}- {o[2]}-'): two_body[*o] / 2}) sentence.simplify() @@ -432,7 +462,7 @@ def hamiltonian(one_body, two_body, notation, cutoff=1e-12): core_constant, one_mo, two_mo = qml.qchem.electron_integrals(mol)() one_so = transform_one(one_mo) -two_so = transform_two(two_mo, '1221') +two_so = transform_two(two_mo, 'quantum') two_so_converted = convert_integrals(two_so, 'quantum', 'physicist') @@ -456,7 +486,7 @@ def hamiltonian(one_body, two_body, notation, cutoff=1e-12): core_constant = np.array([rhf.energy_nuc()]) one_so = transform_one(one_mo) -two_so = transform_two(two_mo, '1122') +two_so = transform_two(two_mo, 'chemist') two_so_converted = convert_integrals(two_so, 'chemist', 'quantum') diff --git a/demonstrations_v2/tutorial_qchem_chemphys/metadata.json b/demonstrations_v2/tutorial_qchem_chemphys/metadata.json index b0f8fa48c6..a5b0e71fdc 100644 --- a/demonstrations_v2/tutorial_qchem_chemphys/metadata.json +++ b/demonstrations_v2/tutorial_qchem_chemphys/metadata.json @@ -2,7 +2,7 @@ "title": "Molecular Hamiltonian Representations", "authors": [ { - "username": "soran" + "username": " " } ], "executable_stable": true, From 594b38571fd9690eede8e9f295b6ab342c9071f3 Mon Sep 17 00:00:00 2001 From: soranjh Date: Wed, 5 Nov 2025 17:43:19 -0500 Subject: [PATCH 04/13] update name --- demonstrations_v2/tutorial_qchem_chemphys/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demonstrations_v2/tutorial_qchem_chemphys/metadata.json b/demonstrations_v2/tutorial_qchem_chemphys/metadata.json index a5b0e71fdc..b0f8fa48c6 100644 --- a/demonstrations_v2/tutorial_qchem_chemphys/metadata.json +++ b/demonstrations_v2/tutorial_qchem_chemphys/metadata.json @@ -2,7 +2,7 @@ "title": "Molecular Hamiltonian Representations", "authors": [ { - "username": " " + "username": "soran" } ], "executable_stable": true, From eef135e2e6f49a5ed65d4b929da31295c55bbfbc Mon Sep 17 00:00:00 2001 From: soranjh Date: Thu, 6 Nov 2025 13:59:49 -0500 Subject: [PATCH 05/13] update text --- .../tutorial_qchem_chemphys/demo.py | 218 +++++++++++------- 1 file changed, 130 insertions(+), 88 deletions(-) diff --git a/demonstrations_v2/tutorial_qchem_chemphys/demo.py b/demonstrations_v2/tutorial_qchem_chemphys/demo.py index 1e0d12f036..d3aef1f36e 100644 --- a/demonstrations_v2/tutorial_qchem_chemphys/demo.py +++ b/demonstrations_v2/tutorial_qchem_chemphys/demo.py @@ -11,17 +11,18 @@ .. related:: tutorial_quantum_chemistry Quantum chemistry with PennyLane -Molecular Hamiltonians can be constructed in different ways depending on the the arrangement of -the two-electron integral tensor. Here, we review the common ways to represent a molecular -Hamiltonian. We use three common two-electron integral notations that are typically referred to -as physicists', chemists' and quantum computing notation. The two-electron integrals computed with -one convention can be easily converted to the other notations. Such conversions allow constructing +Molecular Hamiltonians can be constructed in different ways depending on the arrangement of +the two-electron integral tensor. Here, we review the common ways to construct a fermionic molecular +Hamiltonian using two-electron integral notations that are referred to as the ``Physicist's`` and +``Chemist's`` notations as well as a notation that is used in the quantum computing community. We +the term ``Quantum`` notation for the latter. The two-electron integrals computed with any of these +conventions can be easily converted to the other notations. The conversions allow constructing different representations of the molecular Hamiltonian without re-calculating the integrals. """ ############################################################################## -# Quantum computing notation -# -------------------------- +# Quantum notation +# ---------------- # This notation is commonly used in the quantum computing literature and quantum computing # software libraries. # @@ -31,8 +32,9 @@ # # \langle \langle pq | rs \rangle \rangle = \int dr_1 dr_2 \phi_p^*(r_1) \phi_q^*(r_2) \frac{1}{r_{12}} \phi_r(r_2) \phi_s(r_1). # -# The corresponding Hamiltonian in second quantization is defined in terms of the fermionic -# creation and annihilation operators as +# The double bracket :math:`\langle \langle \cdot \rangle \rangle` is used to distinguish from +# another notation that will be introduced later. The corresponding Hamiltonian in second +# quantization is defined in terms of the fermionic creation and annihilation operators as # # .. math:: # @@ -43,9 +45,9 @@ # indices matches the order of the coefficient indices for both :math:`h_{pq}` and :math:`h_{pqrs}` # terms. # -# We will now construct the Hamiltonian using PennyLane. PennyLane employs the quantum computing +# We now construct the Hamiltonian using PennyLane. PennyLane employs the quantum computing # convention for the two-electron integral tensor, which can be efficiently computed via the -# built-in electron_integrals function. We use the water molecule as an example. +# built-in ``electron_integrals`` function. We use the water molecule as an example. import numpy as np import pennylane as qml @@ -60,26 +62,26 @@ core_constant, one_mo, two_mo = qml.qchem.electron_integrals(mol)() - ############################################################################## -# These integrals are obtained over spatial molecular orbitals. That means, for water, we have -# only reported the integrals over the :math:`1s`, :math:`2s`, and the :math:`2p_x`, :math:`2p_y` -# and :math:`2p_z` orbitals, without -# accounting for the spin component. To construct the full -# Hamiltonian, the integrals objects need to be expanded to include spin orbitals, i.e., :math:`1s_alpha`, -# :math:`1s_beta` etc. Assuming an interleaved convention for the order of spin orbitals, i.e., -# :math:`|\alpha, \beta, \alpha, \beta, ...>`, the following functions give the expanded one-electron and -# two-electron objects. +# These integrals are obtained over spatial molecular orbitals. That means, for the water molecule, +# we have computed the integrals over the :math:`1s`, :math:`2s`, and the :math:`2p_x`, :math:`2p_y` +# and :math:`2p_z` orbitals, without accounting for the spin component. To construct the full +# Hamiltonian, the integrals objects need to be expanded to include spin orbitals, i.e., +# :math:`1s_{\alpha}`, :math:`1s_{\beta}` etc. Assuming an interleaved convention for the order of +# spin orbitals, i.e., :math:`|\alpha, \beta, \alpha, \beta, ...>`, the following functions give the +# expanded one-electron and two-electron objects. def transform_one(h_mo: np.ndarray) -> np.ndarray: """Converts a one-electron integral matrix from the molecular orbital (MO) basis to the spin-orbital (SO) basis. Args: - h_mo (array): The one-electron matrix with shape (n, n) where n is the number of spatial orbitals. + h_mo (array): The one-electron matrix with shape (n, n) where n is the number of + spatial orbitals. Returns: - The one-electron matrix with shape (2n, 2n) where n is the number of spatial orbitals. + The one-electron matrix with shape (2n, 2n) where n is the number of + spatial orbitals. """ n_so = 2 * h_mo.shape[0] @@ -98,10 +100,12 @@ def transform_two(g_mo): spin-orbital (SO) basis. Args: - g_mo (array): The two-electron tensor with shape (n, n, n, n) where n is the number of spatial orbitals. + g_mo (array): The two-electron tensor with shape (n, n, n, n) where n is the number + of spatial orbitals. Returns: - The two-electron matrix with shape (2n, 2n, 2n, 2n) where n is the number of spatial orbitals. + The two-electron matrix with shape (2n, 2n, 2n, 2n) where n is the number of + spatial orbitals. """ n_so = 2 * g_mo.shape[0] @@ -119,22 +123,27 @@ def transform_two(g_mo): return g_so ############################################################################## -# Note that the transformation must respect the spin-selection rules. For instance, the integral -# :math:`\langle \chi_p | \hat{h} | \chi_q \rangle = \langle \phi_i | \hat{h}^{\text{spatial}} | \phi_j \rangle \cdot \langle \sigma_p | \sigma_q \rangle` -# must be zero if the spins of the initial and final spin orbitals are different. For the -# one-electron integrals, only αα or ββ combinations have a non-zero -# value. Similarly, the two-electron operator :math:`1/r_{12}` is spin-independent and if we use the -# compact notation ``'1221'`` to represent the order in the two-electron integral, the non-zero -# combinations are: αααα, αββα, βααβ and ββββ. The integrals computed using molecular orbitals -# can be expanded to include spin orbitals using these combination rules. We now obtain the full -# electron integrals in the basis of spin orbitals. +# Note that the transformation must respect the spin-selection rules. For instance, the following +# integral must be zero if the spins of the initial and final spin orbitals are different. +# +# .. math:: +# +# \langle \chi_p | \hat{h} | \chi_q \rangle = \langle \phi_i | \hat{h}^{\text{spatial}} | \phi_j \rangle \cdot \langle \sigma_p | \sigma_q \rangle. +# +# For the one-electron integrals, only the :math:`\alpha \alpha` and :math:`\beta \beta` +# combinations have a non-zero value. Similarly, the two-electron operator :math:`1/r_{12}` is +# spin-independent and if we use the compact notation ``'1221'`` to represent the order in the +# two-electron integral, the non-zero combinations are: +# :math:`\alpha \alpha \alpha \alpha`, :math:`\alpha \beta \beta \alpha`, :math:`\beta \alpha \alpha\beta` and :math:`\beta \beta \beta \beta`. +# The integrals computed using molecular orbitals can be expanded to include spin orbitals using +# these combination rules. We now obtain the full electron integrals in the basis of spin orbitals. one_so = transform_one(one_mo) two_so = transform_two(two_mo) ############################################################################## # Having the electron integrals objects, computing the Hamiltonian is straightforward. We simply -# loop over the elements of the tensors and multiply them with the fermionic operators with the +# loop over the elements of the tensors and multiply them by the fermionic operators with the # indices matching those of the integral elements. For better performance, we can skip the zero # integral elements and just obtain the operator indices for the non-zero integrals. @@ -142,7 +151,7 @@ def transform_two(g_mo): two_operators = qml.math.argwhere(abs(two_so) >= 1e-12) ############################################################################## -# Now we construct the Hamiltonian as a FermiSentence object. +# Now we construct the Hamiltonian as a ``FermiSentence`` object. sentence = FermiSentence({FermiWord({}): core_constant[0]}) @@ -157,23 +166,24 @@ def transform_two(g_mo): ############################################################################## # Note that the order of indices for the fermionic creation and annihilation operators matches # the order of indices in the integral objects. We finally map the fermionic Hamiltonian to the -# qubit bases and compute its ground-state energy, which should match the value :math:`-75.01562736` a.u. +# qubit bases and compute its ground-state energy, which should match the reference value +# :math:`-75.01562736 \ \text{H}`. h = qml.jordan_wigner(sentence) qml.eigvals(qml.SparseHamiltonian(h.sparse_matrix(), wires=h.wires)) ############################################################################## -# We can further verify this value by using the PennyLane function molecular_hamiltonian, which +# We can further verify this value by using the PennyLane function ``molecular_hamiltonian`` which # automatically constructs the Hamiltonian. h = qml.qchem.molecular_hamiltonian(mol)[0] qml.eigvals(qml.SparseHamiltonian(h.sparse_matrix(), wires=h.wires)) ############################################################################## -# Chemists' notation +# Chemist's notation # ------------------ -# This notation is commonly used by quantum chemistry software libraries such as PySCF. The two- -# electron integral tensor in this notation is defined as +# This notation is commonly used by quantum chemistry software libraries such as PySCF. The +# two-electron integral tensor in this notation is defined as # # .. math:: # @@ -189,13 +199,15 @@ def transform_two(g_mo): # the creation and annihilation operators in the second term matches the order of the two-electron # integral coefficients. See the appendix for a full derivation of the Hamiltonian. # -# Let's now build the Hamiltonian using electron integrals computed by PySCF. +# Let's now build the Hamiltonian using electron integrals computed by ``PySCF`` which adopts the +# Chemist's notation. Note that ``PySCF`` one-body integral does not include the correction term +# mentioned above for the Hamiltonian. from pyscf import gto, ao2mo, scf mol_pyscf = gto.M(atom='''H -0.02111417 -0.00201087 0.; - O 0.83504162 0.45191733 0.; - H 1.47688065 -0.27300252 0.''') + O 0.83504162 0.45191733 0.; + H 1.47688065 -0.27300252 0.''') rhf = scf.RHF(mol_pyscf) energy = rhf.kernel() @@ -207,22 +219,25 @@ def transform_two(g_mo): core_constant = np.array([rhf.energy_nuc()]) - ############################################################################## -# These integrals are also obtained for the spatial orbitals and we need to expand them to include -# spin orbitals. To do that, we need to slightly upgrade our transform_two_interleaved function -# because the allowed spin combination in the chemists' notation, also denoted by ``'1122'``, are -# αααα, ααββ, ββαα and ββββ. +# These integrals are also obtained for the spatial orbitals, so we need to expand them to account +# for spin orbitals. To do that, we need to slightly upgrade our ``transform_two`` function +# because the allowed spin combination in the Chemist's notation, denoted by ``'1122'``, are +# :math:`\alpha \alpha \alpha \alpha`, :math:`\alpha \alpha \beta \beta`, +# :math:`\beta \beta \alpha \alpha` and :math:`\beta \beta \beta \beta`. def transform_two(g_mo, notation): """Converts a two-electron integral tensor from the molecular orbital (MO) basis to the spin-orbital (SO) basis. Args: - g_mo (array): The two-electron tensor with shape (n, n, n, n) where n is the number of spatial orbitals. + g_mo (array): The two-electron tensor with shape (n, n, n, n) where n is the number + of spatial orbitals. + notation (str): The notation used to compute the two-electron integrals tensor. Returns: - The two-electron matrix with shape (2n, 2n, 2n, 2n) where n is the number of spatial orbitals. + The two-electron matrix with shape (2n, 2n, 2n, 2n) where n is the number of + spatial orbitals. """ n = g_mo.shape[0] @@ -248,7 +263,7 @@ def transform_two(g_mo, notation): ############################################################################## -# We can compute the full integrals objects and add the one-body correction term +# We now compute the integrals in the spin-orbitals basis and add the one-body correction term. one_so = transform_one(one_mo) two_so = transform_two(two_mo, 'chemist') @@ -274,14 +289,14 @@ def transform_two(g_mo, notation): ############################################################################## # Note that the order of the indices for the fermionic operators match those of the electron # integral coefficients. Also note that the two-body operator has the order -# :math:`a^{\dagger} a a^{\dagger} a'. Let's now validate the Hamiltonian by computing the +# :math:`a^{\dagger} a a^{\dagger} a`. Let's now validate the Hamiltonian by computing the # ground-state energy. h = qml.jordan_wigner(sentence) qml.eigvals(qml.SparseHamiltonian(h.sparse_matrix(), wires=h.wires)) ############################################################################## -# Physicists' notation +# Physicist's notation # -------------------- # The two-electron integral tensor in this notation is defined as # @@ -295,40 +310,43 @@ def transform_two(g_mo, notation): # # H = \sum_{pq} h_{pq} a_p^{\dagger} a_q + \frac{1}{2} \sum_{pqrs} h_{pqrs} a_p^{\dagger} a_q^{\dagger} a_s a_r, # -# It is important to note the order of the creation and annihilation -# operators in the second term, which is :math:`a_p^{\dagger} a_q^{\dagger} a_s a_r`. This order -# must be preserved when the Hamiltonian is constracted with the two-electron integral tensor -# represented in the physicists' notation. +# It is important to note the order of the creation and annihilation operators in the second term, +# which is :math:`a_p^{\dagger} a_q^{\dagger} a_s a_r`. This order must be preserved when the +# Hamiltonian is constructed with the two-electron integral tensor represented in the Physicist's +# notation. # # Let's now build this Hamiltonian step-by-step. There is not a commonly-used software library -# to compute integrals in this notation but we can easily convert integrals obtained in other -# notations. For instance, we use PySCF integrals and convert them to physicists' notation. We can -# do this in two different ways: converting the integrals in the spatial orbital basis or -# converting them in the spin orbital basis. Let's do both. +# to compute integrals in this notation. However, we can easily convert the integrals we already +# computed in other notations to the Physicist's notation. For instance, we use ``PySCF`` integrals +# and convert them to Physicist's notation. We can do this in two different ways: converting the +# integrals in the spatial orbital basis or converting them in the spin orbital basis. # -# The conversion can be done by the transformation the transformation -# :math:`(pq | rs) \to (pr | qs)` where we have swapped the :math:`1,2` -# indices. This transformation can be done with +# The conversion can be done by the transformation :math:`(pq | rs) \to (pr | qs)` where we have +# swapped the :math:`1,2` indices. This transformation can be done with one_mo = one_mo.copy() two_mo = two_mo.transpose(0, 2, 1, 3) ############################################################################## -# We intentionally converted the integrals obtained for the spatial orbitals, so we need to expand -# them to include spin orbitals. To do that, we need to again slightly upgrade our -# transform_two_interleaved function to include the allowed spin combination in the physicists' -# notation, denoted by ``'1212'``, as αααα, αβαβ, βαβα and ββββ. +# Now we need to expand the integrals to the spin orbital basis. To do that, we need to again +# slightly upgrade our ``transform_two`` function to include the allowed spin combination in the +# Physicist's notation, denoted by ``'1212'``, as :math:`\alpha \alpha \alpha \alpha`, +# :math:`\alpha \beta \alpha \beta`, :math:`\beta \alpha \beta \alpha` +# and :math:`\beta \beta \beta \beta`. def transform_two(g_mo, notation): """Converts a two-electron integral tensor from the molecular orbital (MO) basis to the spin-orbital (SO) basis. Args: - g_mo (array): The two-electron tensor with shape (n, n, n, n) where n is the number of spatial orbitals. + g_mo (array): The two-electron tensor with shape (n, n, n, n) where n is the number + of spatial orbitals. + notation (str): The notation used to compute the two-electron integrals tensor. Returns: - The two-electron matrix with shape (2n, 2n, 2n, 2n) where n is the number of spatial orbitals. + The two-electron matrix with shape (2n, 2n, 2n, 2n) where n is the number of + spatial orbitals. """ n = g_mo.shape[0] @@ -358,7 +376,7 @@ def transform_two(g_mo, notation): return g_so ############################################################################## -# The new tensor can then be used to construct the Hamiltonian +# The new tensor can then be used to construct the Hamiltonian. one_so = transform_one(one_mo) two_so = transform_two(two_mo, 'physicist') @@ -383,17 +401,27 @@ def transform_two(g_mo, notation): h = qml.jordan_wigner(sentence) qml.eigvals(qml.SparseHamiltonian(h.sparse_matrix(), wires=h.wires)) - ############################################################################## # Integral conversion # ------------------- # The two-electron integrals computed with one convention can be easily converted to the other -# conventions as we already did for the chemist to physicist conversion. Such conversions allow +# conventions as we already did for the Chemist's to Physicist's conversion. Such conversions allow # constructing different representations of the molecular Hamiltonian without re-calculating the -# integrals. Here we provide the conversion rules for all three conventions. +# integrals. The following function applies the conversion rules for all three conventions. def convert_integrals(two_body, in_notation, out_notation): - """ """ + """Converts a two-electron integral tensor between different conventions. + + Args: + two_body (array): The two-electron tensor with shape (2n, 2n, 2n, 2n) where n is the number + of spatial orbitals. + in_notation (str): The notation used to compute the two-electron integrals tensor. + out_notation (str): The desired notation to represent the two-electron integrals tensor. + + Returns: + The two-electron matrix with shape (2n, 2n, 2n, 2n) where n is the number of + spatial orbitals. + """ if in_notation == out_notation: return two_body @@ -420,6 +448,19 @@ def convert_integrals(two_body, in_notation, out_notation): # We can also create a versatile function that computes the Hamiltonian for each convention. def hamiltonian(one_body, two_body, notation, cutoff=1e-12): + """Converts a two-electron integral tensor between different conventions. + + Args: + one_body (array): The one-electron tensor with shape (2n, 2n) where n is the number + of spatial orbitals. + two_body (array): The two-electron tensor with shape (2n, 2n, 2n, 2n) where n is the number + of spatial orbitals. + notation (str): The notation used to compute the two-electron integrals tensor. + cutoff (float). The tolerance for neglecting an integral. Defaults is 1e-12. + + Returns: + The molecular Hamiltonian in the Pauli basis. + """ if notation == "chemist": one_body = one_body - 0.5 * np.einsum('pssq->pq', two_body) @@ -450,15 +491,15 @@ def hamiltonian(one_body, two_body, notation, cutoff=1e-12): ############################################################################## # We now have all the necessary tools in our arsenal to convert the integrals to a desired -# notation and construct the corresponding Hamiltonian. Let's look at a few examples. +# notation and construct the corresponding Hamiltonian automatically. Let's look at a few examples. # -# First we convert the integrals obtained with PennyLane in 'quantum' notation to the 'chemist' -# notation and compute the corresponding Hamiltonian. This conversion can be done by the -# transformation :math:` \to \to \to \to ij', rhf.mo_coeff, one_ao, rhf.mo_coeff) two_mo = ao2mo.incore.full(two_ao, rhf.mo_coeff) @@ -494,7 +536,7 @@ def hamiltonian(one_body, two_body, notation, cutoff=1e-12): qml.eigvals(qml.SparseHamiltonian(h.sparse_matrix(), wires=h.wires)) ############################################################################## -# Similarly, we go from chemist notation to the physicist notation +# Similarly, we can go from Chemist's notation to the Physicist's notation. two_so_converted = convert_integrals(two_so, 'chemist', 'physicist') @@ -502,7 +544,7 @@ def hamiltonian(one_body, two_body, notation, cutoff=1e-12): qml.eigvals(qml.SparseHamiltonian(h.sparse_matrix(), wires=h.wires)) ############################################################################## -# The other converstions follow a similar logic. +# The other possible conversions follow a similar logic. # # References # ---------- From 25ca1406b693bf7e699577a9812abd05607dee29 Mon Sep 17 00:00:00 2001 From: soranjh Date: Thu, 6 Nov 2025 16:55:13 -0500 Subject: [PATCH 06/13] update format --- .../tutorial_qchem_chemphys/demo.py | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/demonstrations_v2/tutorial_qchem_chemphys/demo.py b/demonstrations_v2/tutorial_qchem_chemphys/demo.py index d3aef1f36e..2bbdd6f092 100644 --- a/demonstrations_v2/tutorial_qchem_chemphys/demo.py +++ b/demonstrations_v2/tutorial_qchem_chemphys/demo.py @@ -14,10 +14,11 @@ Molecular Hamiltonians can be constructed in different ways depending on the arrangement of the two-electron integral tensor. Here, we review the common ways to construct a fermionic molecular Hamiltonian using two-electron integral notations that are referred to as the ``Physicist's`` and -``Chemist's`` notations as well as a notation that is used in the quantum computing community. We -the term ``Quantum`` notation for the latter. The two-electron integrals computed with any of these +``Chemist's`` notations as well as a notation that is used in the quantum computing community, which +we refer to as ``Quantum`` notation here. The two-electron integrals computed with any of these conventions can be easily converted to the other notations. The conversions allow constructing different representations of the molecular Hamiltonian without re-calculating the integrals. + """ ############################################################################## @@ -132,7 +133,7 @@ def transform_two(g_mo): # # For the one-electron integrals, only the :math:`\alpha \alpha` and :math:`\beta \beta` # combinations have a non-zero value. Similarly, the two-electron operator :math:`1/r_{12}` is -# spin-independent and if we use the compact notation ``'1221'`` to represent the order in the +# spin-independent and if we use the compact notation ``1221`` to represent the order in the # two-electron integral, the non-zero combinations are: # :math:`\alpha \alpha \alpha \alpha`, :math:`\alpha \beta \beta \alpha`, :math:`\beta \alpha \alpha\beta` and :math:`\beta \beta \beta \beta`. # The integrals computed using molecular orbitals can be expanded to include spin orbitals using @@ -222,7 +223,7 @@ def transform_two(g_mo): ############################################################################## # These integrals are also obtained for the spatial orbitals, so we need to expand them to account # for spin orbitals. To do that, we need to slightly upgrade our ``transform_two`` function -# because the allowed spin combination in the Chemist's notation, denoted by ``'1122'``, are +# because the allowed spin combination in the Chemist's notation, denoted by ``1122``, are # :math:`\alpha \alpha \alpha \alpha`, :math:`\alpha \alpha \beta \beta`, # :math:`\beta \beta \alpha \alpha` and :math:`\beta \beta \beta \beta`. @@ -331,7 +332,7 @@ def transform_two(g_mo, notation): ############################################################################## # Now we need to expand the integrals to the spin orbital basis. To do that, we need to again # slightly upgrade our ``transform_two`` function to include the allowed spin combination in the -# Physicist's notation, denoted by ``'1212'``, as :math:`\alpha \alpha \alpha \alpha`, +# Physicist's notation, denoted by ``1212``, as :math:`\alpha \alpha \alpha \alpha`, # :math:`\alpha \beta \alpha \beta`, :math:`\beta \alpha \beta \alpha` # and :math:`\beta \beta \beta \beta`. @@ -413,10 +414,11 @@ def convert_integrals(two_body, in_notation, out_notation): """Converts a two-electron integral tensor between different conventions. Args: - two_body (array): The two-electron tensor with shape (2n, 2n, 2n, 2n) where n is the number - of spatial orbitals. + two_body (array): The two-electron tensor with shape (2n, 2n, 2n, 2n) where n is + the number of spatial orbitals. in_notation (str): The notation used to compute the two-electron integrals tensor. - out_notation (str): The desired notation to represent the two-electron integrals tensor. + out_notation (str): The desired notation to represent the two-electron + integrals tensor. Returns: The two-electron matrix with shape (2n, 2n, 2n, 2n) where n is the number of @@ -451,10 +453,10 @@ def hamiltonian(one_body, two_body, notation, cutoff=1e-12): """Converts a two-electron integral tensor between different conventions. Args: - one_body (array): The one-electron tensor with shape (2n, 2n) where n is the number - of spatial orbitals. - two_body (array): The two-electron tensor with shape (2n, 2n, 2n, 2n) where n is the number - of spatial orbitals. + one_body (array): The one-electron tensor with shape (2n, 2n) where n is + the number of spatial orbitals. + two_body (array): The two-electron tensor with shape (2n, 2n, 2n, 2n) where + n is the number of spatial orbitals. notation (str): The notation used to compute the two-electron integrals tensor. cutoff (float). The tolerance for neglecting an integral. Defaults is 1e-12. From 8cad53257708940c00358b1099fa04389264b08a Mon Sep 17 00:00:00 2001 From: soranjh Date: Fri, 7 Nov 2025 16:49:15 -0500 Subject: [PATCH 07/13] update text --- demonstrations_v2/tutorial_qchem_chemphys/demo.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/demonstrations_v2/tutorial_qchem_chemphys/demo.py b/demonstrations_v2/tutorial_qchem_chemphys/demo.py index 2bbdd6f092..4b98d0e67a 100644 --- a/demonstrations_v2/tutorial_qchem_chemphys/demo.py +++ b/demonstrations_v2/tutorial_qchem_chemphys/demo.py @@ -15,9 +15,12 @@ the two-electron integral tensor. Here, we review the common ways to construct a fermionic molecular Hamiltonian using two-electron integral notations that are referred to as the ``Physicist's`` and ``Chemist's`` notations as well as a notation that is used in the quantum computing community, which -we refer to as ``Quantum`` notation here. The two-electron integrals computed with any of these +we refer to as the ``Quantum`` notation here. The two-electron integrals computed with any of these conventions can be easily converted to the other notations. The conversions allow constructing -different representations of the molecular Hamiltonian without re-calculating the integrals. +different representations of the molecular Hamiltonian without re-calculating the integrals. We +start by a detailed step-by-step construction of the integrals and their corresponding Hamiltonians +and at the end provide compact functions that automate the conversion of the integrals and the +construction of the Hamiltonains. """ From 0068f9dc70711b79f15d8861d114d79f121c8ebf Mon Sep 17 00:00:00 2001 From: soranjh Date: Mon, 10 Nov 2025 17:39:06 -0500 Subject: [PATCH 08/13] use compact code --- .../tutorial_qchem_chemphys/demo.py | 187 +++++++++--------- 1 file changed, 97 insertions(+), 90 deletions(-) diff --git a/demonstrations_v2/tutorial_qchem_chemphys/demo.py b/demonstrations_v2/tutorial_qchem_chemphys/demo.py index 4b98d0e67a..ea5edb8e48 100644 --- a/demonstrations_v2/tutorial_qchem_chemphys/demo.py +++ b/demonstrations_v2/tutorial_qchem_chemphys/demo.py @@ -4,47 +4,50 @@ ===================================== .. meta:: - :property="og:description": Learn how to use chemist, physicist and quantum notations + :property="og:description": Learn how to construct a Hamiltonian using use chemist, physicist and quantum conventions :property="og:image": https://pennylane.ai/qml/_static/demonstration_assets/thumbnail_tutorial_external_libs.png .. related:: tutorial_quantum_chemistry Quantum chemistry with PennyLane - -Molecular Hamiltonians can be constructed in different ways depending on the arrangement of -the two-electron integral tensor. Here, we review the common ways to construct a fermionic molecular -Hamiltonian using two-electron integral notations that are referred to as the ``Physicist's`` and -``Chemist's`` notations as well as a notation that is used in the quantum computing community, which -we refer to as the ``Quantum`` notation here. The two-electron integrals computed with any of these -conventions can be easily converted to the other notations. The conversions allow constructing -different representations of the molecular Hamiltonian without re-calculating the integrals. We -start by a detailed step-by-step construction of the integrals and their corresponding Hamiltonians -and at the end provide compact functions that automate the conversion of the integrals and the -construction of the Hamiltonains. - + tutorial_fermionic_operators Fermionic operators + +Molecular Hamiltonians in second quantization can be constructed in different ways depending on the +arrangement of the two-electron integral tensor. Here, we show you how to construct a fermionic +molecular Hamiltonian from two-electron integral tensors represented in different conventions. The +integrals computed with any of these conventions can be easily converted to the others. This allows +constructing any desired representation of the molecular Hamiltonian without re-calculating the +integrals. We start by a detailed step-by-step construction of the integrals, and the corresponding +Hamiltonians, and then provide compact helper functions that automate the conversion of the +integrals and the construction of the Hamiltonains. """ ############################################################################## +# Integral Notations +# ------------------ +# We use three common notations for two-electron integrals. First, we look at a notation that is +# commonly used in the quantum computing community and quantum software libraries, which we refer to +# as the ``Quantum`` notation. Then we review two other notations that are typically referred to as +# the ``Chemist's`` and ``Physicist's`` notations. +# # Quantum notation # ---------------- -# This notation is commonly used in the quantum computing literature and quantum computing -# software libraries. -# -# The two-electron integral tensor in this notation is defined as +# The two-electron integrals in this notation are defined as # # .. math:: # -# \langle \langle pq | rs \rangle \rangle = \int dr_1 dr_2 \phi_p^*(r_1) \phi_q^*(r_2) \frac{1}{r_{12}} \phi_r(r_2) \phi_s(r_1). +# \langle \langle pq | rs \rangle \rangle = \int dr_1 dr_2 \phi_p^*(r_1) \phi_q^*(r_2) \frac{1}{r_{12}} \phi_r(r_2) \phi_s(r_1), # -# The double bracket :math:`\langle \langle \cdot \rangle \rangle` is used to distinguish from -# another notation that will be introduced later. The corresponding Hamiltonian in second -# quantization is defined in terms of the fermionic creation and annihilation operators as +# where :math:`\phi` is a spatial molecular orbital. We have used the double bracket +# :math:`\langle \langle \cdot \rangle \rangle` to distinguish this notation from the others. The +# corresponding Hamiltonian in second quantization is defined in terms of the fermionic creation and +# annihilation operators, :math:`a^{\dagger}` and :math:`a`, as # # .. math:: # # H = \sum_{pq} h_{pq} a_p^{\dagger} a_q + \frac{1}{2} \sum_{pqrs} h_{pqrs} a_p^{\dagger} a_q^{\dagger} a_r a_s. # -# where :math:`h_{pq}` denotes the one-electron integral. We have skipped the spin indices and +# where :math:`h_{pq}` denotes a one-electron integral. We have skipped the spin indices and # the core constant for brevity. Note that the order of the creation and annihilation operator # indices matches the order of the coefficient indices for both :math:`h_{pq}` and :math:`h_{pqrs}` # terms. @@ -67,13 +70,15 @@ core_constant, one_mo, two_mo = qml.qchem.electron_integrals(mol)() ############################################################################## -# These integrals are obtained over spatial molecular orbitals. That means, for the water molecule, -# we have computed the integrals over the :math:`1s`, :math:`2s`, and the :math:`2p_x`, :math:`2p_y` -# and :math:`2p_z` orbitals, without accounting for the spin component. To construct the full -# Hamiltonian, the integrals objects need to be expanded to include spin orbitals, i.e., -# :math:`1s_{\alpha}`, :math:`1s_{\beta}` etc. Assuming an interleaved convention for the order of -# spin orbitals, i.e., :math:`|\alpha, \beta, \alpha, \beta, ...>`, the following functions give the -# expanded one-electron and two-electron objects. +# PennyLane uses the restricted Hartree-Fock method by default which returns the integrals in the +# basis of spatial molecular orbitals. That means, for the water molecule, we have computed the +# integrals over the :math:`1s`, :math:`2s`, and the :math:`2p_x, 2p_y, 2p_z` orbitals without +# accounting for spin. To construct the full Hamiltonian, the integrals objects need to be expanded +# to include spin orbitals, i.e., :math:`1s_{\alpha}`, :math:`1s_{\beta}` etc. Assuming an +# interleaved convention for the order of spin orbitals, i.e., :math:`|\alpha, \beta, \alpha, \beta, ...>`, +# the following functions give the expanded one-electron and two-electron objects. Note using the +# unrestricted Hartree-Fock method provides the full integrals objects in the basis of spin orbitals +# and the expansion is not needed. def transform_one(h_mo: np.ndarray) -> np.ndarray: """Converts a one-electron integral matrix from the molecular orbital (MO) basis to the @@ -127,29 +132,31 @@ def transform_two(g_mo): return g_so ############################################################################## -# Note that the transformation must respect the spin-selection rules. For instance, the following -# integral must be zero if the spins of the initial and final spin orbitals are different. +# The transformation to the spin orbital basis must respect the spin-selection rules. For instance, +# the following integral over spin orbitals :math:`\chi` must be zero if the spins :math:`\sigma` of +# the initial and final spin orbitals are different. # # .. math:: # -# \langle \chi_p | \hat{h} | \chi_q \rangle = \langle \phi_i | \hat{h}^{\text{spatial}} | \phi_j \rangle \cdot \langle \sigma_p | \sigma_q \rangle. +# \langle \chi_p | \hat{h} | \chi_q \rangle = \langle \phi_i | \hat{h} | \phi_j \rangle \cdot \langle \sigma_p | \sigma_q \rangle, +# +# since the one-electron operator :math:`\hat{h}` is spin-independent. For the one-electron +# integrals, only the :math:`\alpha \alpha` and :math:`\beta \beta` combinations have a non-zero +# value. Similarly, the two-electron operator :math:`1/r_{12}` is spin-independent and if we use the +# compact notation ``1221`` to represent the order in the two-electron integral, the non-zero +# combinations are: :math:`\alpha \alpha \alpha \alpha`, :math:`\alpha \beta \beta \alpha`, :math:`\beta \alpha \alpha\beta` and :math:`\beta \beta \beta \beta`. +# These combination rules are used in our functions. # -# For the one-electron integrals, only the :math:`\alpha \alpha` and :math:`\beta \beta` -# combinations have a non-zero value. Similarly, the two-electron operator :math:`1/r_{12}` is -# spin-independent and if we use the compact notation ``1221`` to represent the order in the -# two-electron integral, the non-zero combinations are: -# :math:`\alpha \alpha \alpha \alpha`, :math:`\alpha \beta \beta \alpha`, :math:`\beta \alpha \alpha\beta` and :math:`\beta \beta \beta \beta`. -# The integrals computed using molecular orbitals can be expanded to include spin orbitals using -# these combination rules. We now obtain the full electron integrals in the basis of spin orbitals. +# We now obtain the full electron integrals in the basis of spin orbitals. one_so = transform_one(one_mo) two_so = transform_two(two_mo) ############################################################################## # Having the electron integrals objects, computing the Hamiltonian is straightforward. We simply -# loop over the elements of the tensors and multiply them by the fermionic operators with the -# indices matching those of the integral elements. For better performance, we can skip the zero -# integral elements and just obtain the operator indices for the non-zero integrals. +# loop over the elements of the tensors and multiply them by the corresponding fermionic operators. +# For better performance, we can skip the negligible integral components and just obtain the +# operator indices for the non-zero integrals. one_operators = qml.math.argwhere(abs(one_so) >= 1e-12) two_operators = qml.math.argwhere(abs(two_so) >= 1e-12) @@ -159,19 +166,19 @@ def transform_two(g_mo): sentence = FermiSentence({FermiWord({}): core_constant[0]}) -for o in one_operators: - sentence.update({from_string(f'{o[0]}+ {o[1]}-'): one_so[*o]}) +for p, q in one_operators: + sentence.update({from_string(f'{p}+ {q}-'): one_so[p, q]}) -for o in two_operators: - sentence.update({from_string(f'{o[0]}+ {o[1]}+ {o[2]}- {o[3]}-'): two_so[*o] / 2}) +for p, q, r, s in two_operators: + sentence.update({from_string(f'{p}+ {q}+ {r}- {s}-'): two_so[p, q, r, s] / 2}) sentence.simplify() ############################################################################## # Note that the order of indices for the fermionic creation and annihilation operators matches -# the order of indices in the integral objects. We finally map the fermionic Hamiltonian to the -# qubit bases and compute its ground-state energy, which should match the reference value -# :math:`-75.01562736 \ \text{H}`. +# the order of indices in the integral objects, i.e., :math:`pq` and :math:`pqrs`. We finally map +# the fermionic Hamiltonian to the qubit basis and compute its ground-state energy, which should +# match the reference value :math:`-75.01562736 \ \text{H}`. h = qml.jordan_wigner(sentence) qml.eigvals(qml.SparseHamiltonian(h.sparse_matrix(), wires=h.wires)) @@ -186,7 +193,7 @@ def transform_two(g_mo): ############################################################################## # Chemist's notation # ------------------ -# This notation is commonly used by quantum chemistry software libraries such as PySCF. The +# This notation is commonly used by quantum chemistry software libraries such as ``PySCF``. The # two-electron integral tensor in this notation is defined as # # .. math:: @@ -205,7 +212,7 @@ def transform_two(g_mo): # # Let's now build the Hamiltonian using electron integrals computed by ``PySCF`` which adopts the # Chemist's notation. Note that ``PySCF`` one-body integral does not include the correction term -# mentioned above for the Hamiltonian. +# mentioned above for the Hamiltonian and we need to add it manually. from pyscf import gto, ao2mo, scf @@ -224,9 +231,9 @@ def transform_two(g_mo): core_constant = np.array([rhf.energy_nuc()]) ############################################################################## -# These integrals are also obtained for the spatial orbitals, so we need to expand them to account +# We used the restricted Hartree-Fock method and need to expand the integrals to account # for spin orbitals. To do that, we need to slightly upgrade our ``transform_two`` function -# because the allowed spin combination in the Chemist's notation, denoted by ``1122``, are +# because the allowed spin combinations in the Chemist's notation, denoted by ``1122``, are # :math:`\alpha \alpha \alpha \alpha`, :math:`\alpha \alpha \beta \beta`, # :math:`\beta \beta \alpha \alpha` and :math:`\beta \beta \beta \beta`. @@ -282,11 +289,11 @@ def transform_two(g_mo, notation): sentence = FermiSentence({FermiWord({}): core_constant[0]}) -for o in one_operators: - sentence.update({from_string(f'{o[0]}+ {o[1]}-'): one_so_corrected[*o]}) +for p, q in one_operators: + sentence.update({from_string(f'{p}+ {q}-'): one_so_corrected[p, q]}) -for o in two_operators: - sentence.update({from_string(f'{o[0]}+ {o[1]}- {o[2]}+ {o[3]}-'): two_so[*o] / 2}) +for p, q, r, s in two_operators: + sentence.update({from_string(f'{p}+ {q}- {r}+ {s}-'): two_so[p, q, r, s]/2}) sentence.simplify() @@ -322,16 +329,13 @@ def transform_two(g_mo, notation): # Let's now build this Hamiltonian step-by-step. There is not a commonly-used software library # to compute integrals in this notation. However, we can easily convert the integrals we already # computed in other notations to the Physicist's notation. For instance, we use ``PySCF`` integrals -# and convert them to Physicist's notation. We can do this in two different ways: converting the -# integrals in the spatial orbital basis or converting them in the spin orbital basis. +# and convert them to the Physicist's notation. # # The conversion can be done by the transformation :math:`(pq | rs) \to (pr | qs)` where we have # swapped the :math:`1,2` indices. This transformation can be done with -one_mo = one_mo.copy() two_mo = two_mo.transpose(0, 2, 1, 3) - ############################################################################## # Now we need to expand the integrals to the spin orbital basis. To do that, we need to again # slightly upgrade our ``transform_two`` function to include the allowed spin combination in the @@ -390,17 +394,18 @@ def transform_two(g_mo, notation): sentence = FermiSentence({FermiWord({}): core_constant[0]}) -for o in one_operators: - sentence.update({from_string(f'{o[0]}+ {o[1]}-'): one_so[*o]}) +for p, q in one_operators: + sentence.update({from_string(f'{p}+ {q}-'): one_so[p, q]}) -for o in two_operators: - sentence.update({from_string(f'{o[0]}+ {o[1]}+ {o[3]}- {o[2]}-'): two_so[*o] / 2}) +for p, q, r, s in two_operators: + sentence.update({from_string(f'{p}+ {q}+ {s}- {r}-'): two_so[p, q, r, s]/2}) sentence.simplify() ############################################################################## -# Note the order of the creation and annihilation operator indices. Now we compute the ground -# state energy to validate the Hamiltonian. +# Note the order of the creation and annihilation operator indices, which is +# :math:`a_p^{\dagger} a_q^{\dagger} a_s a_r`. Now we compute the ground state energy to validate +# the Hamiltonian. h = qml.jordan_wigner(sentence) qml.eigvals(qml.SparseHamiltonian(h.sparse_matrix(), wires=h.wires)) @@ -409,9 +414,10 @@ def transform_two(g_mo, notation): # Integral conversion # ------------------- # The two-electron integrals computed with one convention can be easily converted to the other -# conventions as we already did for the Chemist's to Physicist's conversion. Such conversions allow -# constructing different representations of the molecular Hamiltonian without re-calculating the -# integrals. The following function applies the conversion rules for all three conventions. +# conventions, as we already did to convert the Chemist's notation to the Physicist's one. Such +# conversions allow constructing different representations of the molecular Hamiltonian without +# re-calculating the integrals. The following function applies the conversion rules for all three +# conventions. def convert_integrals(two_body, in_notation, out_notation): """Converts a two-electron integral tensor between different conventions. @@ -452,10 +458,11 @@ def convert_integrals(two_body, in_notation, out_notation): ############################################################################## # We can also create a versatile function that computes the Hamiltonian for each convention. -def hamiltonian(one_body, two_body, notation, cutoff=1e-12): +def hamiltonian(core_constant, one_body, two_body, notation, cutoff=1e-12): """Converts a two-electron integral tensor between different conventions. Args: + core_constant (float): The core constant of the Hamiltonian one_body (array): The one-electron tensor with shape (2n, 2n) where n is the number of spatial orbitals. two_body (array): The two-electron tensor with shape (2n, 2n, 2n, 2n) where @@ -472,22 +479,22 @@ def hamiltonian(one_body, two_body, notation, cutoff=1e-12): op_one = qml.math.argwhere(abs(one_body) >= cutoff) op_two = qml.math.argwhere(abs(two_body) >= cutoff) - sentence = FermiSentence({FermiWord({}): core_constant[0]}) + sentence = FermiSentence({FermiWord({}): core_constant}) - for o in op_one: - sentence.update({from_string(f'{o[0]}+ {o[1]}-'): one_body[*o]}) + for p, q in op_one: + sentence.update({from_string(f'{p}+ {q}-'): one_body[p, q]}) if notation == "quantum": - for o in op_two: - sentence.update({from_string(f'{o[0]}+ {o[1]}+ {o[2]}- {o[3]}-'): two_body[*o] / 2}) + for p, q, r, s in op_two: + sentence.update({from_string(f'{p}+ {q}+ {r}- {s}-'): two_body[p, q, r, s] / 2}) if notation == "chemist": - for o in op_two: - sentence.update({from_string(f'{o[0]}+ {o[1]}- {o[2]}+ {o[3]}-'): two_body[*o] / 2}) + for p, q, r, s in op_two: + sentence.update({from_string(f'{p}+ {q}+ {r}- {s}-'): two_body[p, q, r, s] / 2}) if notation == "physicist": - for o in op_two: - sentence.update({from_string(f'{o[0]}+ {o[1]}+ {o[3]}- {o[2]}-'): two_body[*o] / 2}) + for p, q, r, s in op_two: + sentence.update({from_string(f'{p}+ {q}+ {s}- {r}-'): two_body[p, q, r, s] / 2}) sentence.simplify() @@ -498,13 +505,13 @@ def hamiltonian(one_body, two_body, notation, cutoff=1e-12): # We now have all the necessary tools in our arsenal to convert the integrals to a desired # notation and construct the corresponding Hamiltonian automatically. Let's look at a few examples. # -# First we convert the integrals obtained with PennyLane in ```quantum'`` notation to the -# ``'chemist'`` notation and compute the corresponding Hamiltonian. This conversion can be done by -# the transformation :math:` \to \to Date: Mon, 10 Nov 2025 17:49:58 -0500 Subject: [PATCH 09/13] rename function --- demonstrations_v2/tutorial_qchem_chemphys/demo.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/demonstrations_v2/tutorial_qchem_chemphys/demo.py b/demonstrations_v2/tutorial_qchem_chemphys/demo.py index ea5edb8e48..f4a7cc869a 100644 --- a/demonstrations_v2/tutorial_qchem_chemphys/demo.py +++ b/demonstrations_v2/tutorial_qchem_chemphys/demo.py @@ -458,11 +458,12 @@ def convert_integrals(two_body, in_notation, out_notation): ############################################################################## # We can also create a versatile function that computes the Hamiltonian for each convention. -def hamiltonian(core_constant, one_body, two_body, notation, cutoff=1e-12): +def fermionic_observable(core_constant, one_body, two_body, notation, cutoff=1e-12): """Converts a two-electron integral tensor between different conventions. Args: - core_constant (float): The core constant of the Hamiltonian + core_constant (float): The core constant containing the contribution of the + core orbitals and nuclei one_body (array): The one-electron tensor with shape (2n, 2n) where n is the number of spatial orbitals. two_body (array): The two-electron tensor with shape (2n, 2n, 2n, 2n) where @@ -519,7 +520,7 @@ def hamiltonian(core_constant, one_body, two_body, notation, cutoff=1e-12): two_so_converted = convert_integrals(two_so, 'quantum', 'physicist') -h = hamiltonian(core_constant[0], one_so, two_so_converted, 'physicist') +h = fermionic_observable(core_constant[0], one_so, two_so_converted, 'physicist') qml.eigvals(qml.SparseHamiltonian(h.sparse_matrix(), wires=h.wires)) ############################################################################## @@ -529,7 +530,7 @@ def hamiltonian(core_constant, one_body, two_body, notation, cutoff=1e-12): two_so_converted = convert_integrals(two_so, 'quantum', 'chemist') -h = hamiltonian(core_constant[0], one_so, two_so_converted, 'chemist') +h = fermionic_observable(core_constant[0], one_so, two_so_converted, 'chemist') qml.eigvals(qml.SparseHamiltonian(h.sparse_matrix(), wires=h.wires)) ############################################################################## @@ -544,7 +545,7 @@ def hamiltonian(core_constant, one_body, two_body, notation, cutoff=1e-12): two_so_converted = convert_integrals(two_so, 'chemist', 'quantum') -h = hamiltonian(core_constant[0], one_so, two_so_converted, 'quantum') +h = fermionic_observable(core_constant[0], one_so, two_so_converted, 'quantum') qml.eigvals(qml.SparseHamiltonian(h.sparse_matrix(), wires=h.wires)) ############################################################################## @@ -552,7 +553,7 @@ def hamiltonian(core_constant, one_body, two_body, notation, cutoff=1e-12): two_so_converted = convert_integrals(two_so, 'chemist', 'physicist') -h = hamiltonian(one_so, two_so_converted, 'physicist') +h = fermionic_observable(one_so, two_so_converted, 'physicist') qml.eigvals(qml.SparseHamiltonian(h.sparse_matrix(), wires=h.wires)) ############################################################################## From 3ec1e203c86f2547ec618aab455cea3f0cec8545 Mon Sep 17 00:00:00 2001 From: soranjh Date: Mon, 10 Nov 2025 18:59:31 -0500 Subject: [PATCH 10/13] add appendix --- .../tutorial_qchem_chemphys/demo.py | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/demonstrations_v2/tutorial_qchem_chemphys/demo.py b/demonstrations_v2/tutorial_qchem_chemphys/demo.py index f4a7cc869a..488409d34e 100644 --- a/demonstrations_v2/tutorial_qchem_chemphys/demo.py +++ b/demonstrations_v2/tutorial_qchem_chemphys/demo.py @@ -11,6 +11,7 @@ .. related:: tutorial_quantum_chemistry Quantum chemistry with PennyLane tutorial_fermionic_operators Fermionic operators + tutorial_mapping Mapping fermionic operators to qubit operators Molecular Hamiltonians in second quantization can be constructed in different ways depending on the arrangement of the two-electron integral tensor. Here, we show you how to construct a fermionic @@ -31,7 +32,7 @@ # the ``Chemist's`` and ``Physicist's`` notations. # # Quantum notation -# ---------------- +# ~~~~~~~~~~~~~~~~ # The two-electron integrals in this notation are defined as # # .. math:: @@ -192,7 +193,7 @@ def transform_two(g_mo): ############################################################################## # Chemist's notation -# ------------------ +# ~~~~~~~~~~~~~~~~~~ # This notation is commonly used by quantum chemistry software libraries such as ``PySCF``. The # two-electron integral tensor in this notation is defined as # @@ -308,7 +309,7 @@ def transform_two(g_mo, notation): ############################################################################## # Physicist's notation -# -------------------- +# ~~~~~~~~~~~~~~~~~~~~ # The two-electron integral tensor in this notation is defined as # # .. math:: @@ -559,6 +560,24 @@ def fermionic_observable(core_constant, one_body, two_body, notation, cutoff=1e- ############################################################################## # The other possible conversions follow a similar logic. # +# Appendix +# -------- +# We use the molecular Hamiltonian corresponding to Physicist's convention to derive the Hamiltonian +# corresponding to the Chemist's convention. Recall the following anti-commutation rules for the +# fermionic creation and annihilation operators, +# +# .. math:: +# +# [a^{\dagger}_i, a^{\dagger}_j]_+ = 0, \:\:\:\:\:\:\: [a_i, a_j]_+=0, \:\:\:\:\:\:\: [a_i, a^{\dagger}_j]_+ = \delta_{ij} I,, +# +# where :math:`\delta_{ij}` and :math:`I` are the Kronecker delta and the identity operator, +# respectively. In the Hamiltonian represented by the Physicist's convention, we use these +# anti-commutation rules to move the :math:`a_r` operator from right to left. We first swap the +# operator with :math:`a_s` and then swap it again with :math:`a_q^{\dagger}`. This gives us the +# two-body term :math:`a_p^{\dagger} a_r a_q^{\dagger} a_s` and the one-body operator +# :math:`a_p^{\dagger} a_s`. We can re-arrange the indices to get the Hamiltonian in the Chemist's +# convention. +# # References # ---------- # From a53fc7ad2582241eaf9f16d8e8ba5db4f4012d6f Mon Sep 17 00:00:00 2001 From: soranjh Date: Tue, 11 Nov 2025 11:44:54 -0500 Subject: [PATCH 11/13] add conclusions --- .../tutorial_qchem_chemphys/demo.py | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/demonstrations_v2/tutorial_qchem_chemphys/demo.py b/demonstrations_v2/tutorial_qchem_chemphys/demo.py index 488409d34e..2d77b08926 100644 --- a/demonstrations_v2/tutorial_qchem_chemphys/demo.py +++ b/demonstrations_v2/tutorial_qchem_chemphys/demo.py @@ -492,7 +492,7 @@ def fermionic_observable(core_constant, one_body, two_body, notation, cutoff=1e- if notation == "chemist": for p, q, r, s in op_two: - sentence.update({from_string(f'{p}+ {q}+ {r}- {s}-'): two_body[p, q, r, s] / 2}) + sentence.update({from_string(f'{p}+ {q} {r}+ {s}-'): two_body[p, q, r, s] / 2}) if notation == "physicist": for p, q, r, s in op_two: @@ -554,7 +554,7 @@ def fermionic_observable(core_constant, one_body, two_body, notation, cutoff=1e- two_so_converted = convert_integrals(two_so, 'chemist', 'physicist') -h = fermionic_observable(one_so, two_so_converted, 'physicist') +h = fermionic_observable(core_constant[0], one_so, two_so_converted, 'physicist') qml.eigvals(qml.SparseHamiltonian(h.sparse_matrix(), wires=h.wires)) ############################################################################## @@ -564,20 +564,32 @@ def fermionic_observable(core_constant, one_body, two_body, notation, cutoff=1e- # -------- # We use the molecular Hamiltonian corresponding to Physicist's convention to derive the Hamiltonian # corresponding to the Chemist's convention. Recall the following anti-commutation rules for the -# fermionic creation and annihilation operators, +# fermionic creation and annihilation operators # # .. math:: # -# [a^{\dagger}_i, a^{\dagger}_j]_+ = 0, \:\:\:\:\:\:\: [a_i, a_j]_+=0, \:\:\:\:\:\:\: [a_i, a^{\dagger}_j]_+ = \delta_{ij} I,, +# [a^{\dagger}_i, a^{\dagger}_j]_+ = 0, \:\:\:\:\:\:\: [a_i, a_j]_+=0, \:\:\:\:\:\:\: [a_i, a^{\dagger}_j]_+ = \delta_{ij} I, # # where :math:`\delta_{ij}` and :math:`I` are the Kronecker delta and the identity operator, # respectively. In the Hamiltonian represented by the Physicist's convention, we use these # anti-commutation rules to move the :math:`a_r` operator from right to left. We first swap the # operator with :math:`a_s` and then swap it again with :math:`a_q^{\dagger}`. This gives us the # two-body term :math:`a_p^{\dagger} a_r a_q^{\dagger} a_s` and the one-body operator -# :math:`a_p^{\dagger} a_s`. We can re-arrange the indices to get the Hamiltonian in the Chemist's +# :math:`a_p^{\dagger} a_s`. Re-arranging the indices gives the Hamiltonian in the Chemist's # convention. # +# Conclusions +# ----------- +# We recommend the following upgrades to PennyLane. +# +# 1. Add functions to convert one- and two-electron integral tensor from the molecular orbital (MO) +# basis to the spin-orbital (SO) basis. +# +# 2. Add a function that converts a two-electron integral tensor between different conventions. +# +# 3. Upgrade the qchem.fermionic_observable function to support the Chemist's and Physicist's +# conventions. +# # References # ---------- # From 1a4011d8fbddb8d15deabb3d14423f860a3d9e87 Mon Sep 17 00:00:00 2001 From: soranjh Date: Tue, 11 Nov 2025 11:52:03 -0500 Subject: [PATCH 12/13] correct format --- demonstrations_v2/tutorial_qchem_chemphys/demo.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/demonstrations_v2/tutorial_qchem_chemphys/demo.py b/demonstrations_v2/tutorial_qchem_chemphys/demo.py index 2d77b08926..edd976ecce 100644 --- a/demonstrations_v2/tutorial_qchem_chemphys/demo.py +++ b/demonstrations_v2/tutorial_qchem_chemphys/demo.py @@ -20,7 +20,7 @@ constructing any desired representation of the molecular Hamiltonian without re-calculating the integrals. We start by a detailed step-by-step construction of the integrals, and the corresponding Hamiltonians, and then provide compact helper functions that automate the conversion of the -integrals and the construction of the Hamiltonains. +integrals and the construction of the Hamiltonians. """ ############################################################################## @@ -71,15 +71,17 @@ core_constant, one_mo, two_mo = qml.qchem.electron_integrals(mol)() ############################################################################## -# PennyLane uses the restricted Hartree-Fock method by default which returns the integrals in the +# PennyLane uses the restricted `Hartree-Fock `__ +# method by default which returns the integrals in the # basis of spatial molecular orbitals. That means, for the water molecule, we have computed the # integrals over the :math:`1s`, :math:`2s`, and the :math:`2p_x, 2p_y, 2p_z` orbitals without # accounting for spin. To construct the full Hamiltonian, the integrals objects need to be expanded # to include spin orbitals, i.e., :math:`1s_{\alpha}`, :math:`1s_{\beta}` etc. Assuming an # interleaved convention for the order of spin orbitals, i.e., :math:`|\alpha, \beta, \alpha, \beta, ...>`, # the following functions give the expanded one-electron and two-electron objects. Note using the -# unrestricted Hartree-Fock method provides the full integrals objects in the basis of spin orbitals -# and the expansion is not needed. +# `unrestricted Hartree-Fock `__ +# method provides the full integrals objects in the basis of spin orbitals and the expansion is not +# needed. def transform_one(h_mo: np.ndarray) -> np.ndarray: """Converts a one-electron integral matrix from the molecular orbital (MO) basis to the @@ -587,7 +589,7 @@ def fermionic_observable(core_constant, one_body, two_body, notation, cutoff=1e- # # 2. Add a function that converts a two-electron integral tensor between different conventions. # -# 3. Upgrade the qchem.fermionic_observable function to support the Chemist's and Physicist's +# 3. Upgrade the ``qchem.fermionic_observable`` function to support the Chemist's and Physicist's # conventions. # # References From cb3d5368c913e100d3ec74dd6ec3656a89704513 Mon Sep 17 00:00:00 2001 From: soranjh Date: Tue, 11 Nov 2025 12:03:24 -0500 Subject: [PATCH 13/13] update notation --- demonstrations_v2/tutorial_qchem_chemphys/demo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demonstrations_v2/tutorial_qchem_chemphys/demo.py b/demonstrations_v2/tutorial_qchem_chemphys/demo.py index edd976ecce..785fed6b0b 100644 --- a/demonstrations_v2/tutorial_qchem_chemphys/demo.py +++ b/demonstrations_v2/tutorial_qchem_chemphys/demo.py @@ -494,7 +494,7 @@ def fermionic_observable(core_constant, one_body, two_body, notation, cutoff=1e- if notation == "chemist": for p, q, r, s in op_two: - sentence.update({from_string(f'{p}+ {q} {r}+ {s}-'): two_body[p, q, r, s] / 2}) + sentence.update({from_string(f'{p}+ {q}- {r}+ {s}-'): two_body[p, q, r, s] / 2}) if notation == "physicist": for p, q, r, s in op_two: