Skip to content
7 changes: 7 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ As shown below, a typical eigenvalue problem is broken up into three pieces: 1)
Subspace Hamiltonians <subspace_hamiltonians.ipynb>
Loading Molecular data <molecule_loading.ipynb>

.. toctree::
:maxdepth: 1
:caption: Tools
:hidden:

Logging <logging.ipynb>

.. toctree::
:maxdepth: 2
:caption: Tutorials
Expand Down
136 changes: 136 additions & 0 deletions docs/logging.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "ab95cab7-0251-429b-b032-285d99ac0120",
"metadata": {},
"source": [
"# Logging\n",
"\n",
"The Python interface for Fulqrum has verbose logging functionality built in. Here we show an example of logging inside the Jupyter notebook from which this docuement is generated."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "2cb548ff-572f-48ff-866e-6fdb496c06f4",
"metadata": {},
"outputs": [],
"source": [
"import fulqrum as fq"
]
},
{
"cell_type": "markdown",
"id": "32711ed5-d447-4df4-b686-c0151b5dc8b7",
"metadata": {},
"source": [
"## Formatting the logger\n",
"\n",
"Here we show how to format the logger for useful output. Importantly, when using Jupyter notebooks we need to include `force=True` in the config in order to have the logs show up in the cell output"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "c0d613c5-e7b9-42ef-8a40-c166b014677b",
"metadata": {},
"outputs": [],
"source": [
"import logging\n",
"\n",
"logging.basicConfig(\n",
" level=logging.INFO,\n",
" force=True,\n",
" format=\"%(asctime)s - %(levelname)s - %(name)s - %(message)s\",\n",
")"
]
},
{
"cell_type": "markdown",
"id": "df505263-a762-46f1-8ab0-65cacdfed18e",
"metadata": {},
"source": [
"## Small example\n",
"\n",
"Here we load a molecule, transform it, build a subspace, and finally put them all together in a `SubspaceHamiltonian`. Having set the logging config above, the ouput from each logger entry will be displayed in the cell output."
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "46c3b7e9-2bb9-459a-9be2-fd892e321003",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"2026-08-19 06:59:27,767 - INFO - fulqrum.core.fermi_operator - Extended JW time: 0.758 ms\n",
"2026-08-19 06:59:27,768 - INFO - fulqrum.core.subspace - Initializing Subspace\n",
"2026-08-19 06:59:27,769 - INFO - fulqrum.core.subspace - Full subspace bit-strings\n",
"2026-08-19 06:59:27,769 - INFO - fulqrum.core.subspace - Subspace size: 4096\n",
"2026-08-19 06:59:27,769 - INFO - fulqrum.core.subspace - Number of bits: 12\n",
"2026-08-19 06:59:27,769 - INFO - fulqrum.core.subspace - Using all bitset blocks: True\n",
"2026-08-19 06:59:27,770 - INFO - fulqrum.core.subspace - Reserve multiplier: 2\n",
"2026-08-19 06:59:27,770 - INFO - fulqrum.core.subspace - Subspace total init time: 1.587 ms\n",
"2026-08-19 06:59:27,771 - INFO - fulqrum.core.linear_operator - Initializing SubspaceHamiltonian\n",
"2026-08-19 06:59:27,771 - INFO - fulqrum.core.linear_operator - Number of qubits: 12\n",
"2026-08-19 06:59:27,771 - INFO - fulqrum.core.linear_operator - Num. diagonal terms 78\n",
"2026-08-19 06:59:27,772 - INFO - fulqrum.core.linear_operator - Num. off-diagonal terms 552\n",
"2026-08-19 06:59:27,772 - INFO - fulqrum.core.qubit_operator - Term grouping time: 0.076 ms\n",
"2026-08-19 06:59:27,772 - INFO - fulqrum.core.linear_operator - Term grouping time: 0.344 ms\n",
"2026-08-19 06:59:27,773 - INFO - fulqrum.core.qubit_operator - Starting ladder int grouping, ladder_width = 2\n",
"2026-08-19 06:59:27,773 - INFO - fulqrum.core.qubit_operator - Ladder int grouping time: 0.096 ms\n",
"2026-08-19 06:59:27,773 - INFO - fulqrum.core.linear_operator - Num. off-diagonal groups: 83\n",
"2026-08-19 06:59:27,774 - INFO - fulqrum.core.spmv - Initializing FulqrumSpMV\n",
"2026-08-19 06:59:27,774 - INFO - fulqrum.core.spmv - Operator is real\n",
"2026-08-19 06:59:27,775 - INFO - fulqrum.core.spmv - Operator type = 2\n",
"2026-08-19 06:59:27,776 - INFO - fulqrum.core.spmv - FulqrumSpMV total init time: 1.683 ms\n",
"2026-08-19 06:59:27,776 - INFO - fulqrum.core.linear_operator - SubspaceHamiltonian total init time: 4.945 ms\n"
]
}
],
"source": [
"fop = fq.FermionicOperator.from_json(\"./data/lih.json\")\n",
"op = fop.extended_jw_transformation()\n",
"\n",
"counts = []\n",
"for kk in range(2**op.width):\n",
" counts.append(bin(kk)[2:].zfill(op.width))\n",
"\n",
"S = fq.Subspace([counts])\n",
"Hsub = fq.SubspaceHamiltonian(op, S)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a30c27e6-6fcc-4933-8bf8-f912d14fe116",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.14.4"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
16 changes: 9 additions & 7 deletions fulqrum/convert/integrals.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import time
import numpy as np
from ..core.fermi_operator cimport FermionicOperator

import logging
logger = logging.getLogger(__name__)

include "../core/includes/types.pxi"


Expand Down Expand Up @@ -55,7 +58,7 @@ def integrals_to_fq_fermionic_op(double_or_complex[:,::1] one_body_integrals, do
return fop


def fcidump_to_fq_fermionic_op(fcidump_path: str | Path, bool verbose=False) -> FermionicOperator:
def fcidump_to_fq_fermionic_op(fcidump_path: str | Path) -> FermionicOperator:
"""Load one- and two-body integrals as numpy arrays into Fulqrum
fermionic operator from FCIDUMP file.

Expand All @@ -65,16 +68,16 @@ def fcidump_to_fq_fermionic_op(fcidump_path: str | Path, bool verbose=False) ->
Returns:
FermionicOperator: Converted operator.
"""
logger.info("Starting import of FCIDump file")
from pyscf import ao2mo, tools
st = time.perf_counter()
scf_start = time.perf_counter()
mf_as = tools.fcidump.to_scf(fcidump_path)
hcore = mf_as.get_hcore()
num_spatial_orbitals = hcore.shape[0]
eri = ao2mo.restore(1, mf_as._eri, num_spatial_orbitals)
nuclear_repulsion_energy = mf_as.mol.energy_nuc()
ft = time.perf_counter()
if verbose:
print("FCIDump import time", round(ft-st, 3))
scf_stop = time.perf_counter()
logger.info("PySCF load time: %s ms", round((scf_stop - scf_start) * 1000, 3))

st = time.perf_counter()
cdef FermionicOperator out = integrals_to_fq_fermionic_op(
Expand All @@ -83,6 +86,5 @@ def fcidump_to_fq_fermionic_op(fcidump_path: str | Path, bool verbose=False) ->
constant=nuclear_repulsion_energy,
)
ft = time.perf_counter()
if verbose:
print("Operator conversion time", round(ft-st, 3))
logger.info("Integrals to FermionicOperator time: %s ms", round((ft - st) * 1000, 3))
return out
13 changes: 13 additions & 0 deletions fulqrum/core/fermi_operator.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,14 @@ from ..convert import fcidump_to_fq_fermionic_op


from pathlib import Path
import time
import warnings
import numpy as np
cimport numpy as np

import logging
logger = logging.getLogger(__name__)

include "includes/base_header.pxi"
include "includes/converters.pxi"
include "includes/io.pxi"
Expand Down Expand Up @@ -443,24 +447,33 @@ cdef class FermionicOperator():
Returns:
FermionicOperator: Deflated operator
"""
st = time.perf_counter()
cdef size_t kk
cdef FermionicOperator out = FermionicOperator(self.width)
out.oper = self.oper.combine_repeat_indices()
ft = time.perf_counter()
logger.info("Combine repeat indices time: %s ms", round((ft - st) * 1000, 3))
return out

def combine_repeat_terms(self, double atol=1e-12):
"""In-place sort terms by their standard weight
"""
st = time.perf_counter()
cdef FermionicOperator out = FermionicOperator(self.width)
out.oper = self.oper.combine_repeat_terms(atol)
ft = time.perf_counter()
logger.info("Combine repeat terms time: %s ms", round((ft - st) * 1000, 3))
return out

def extended_jw_transformation(self):
"""Jordan-Wigner transformation over extended alphabet
from Fermionic -> Qubit operator
"""
st = time.perf_counter()
cdef QubitOperator out = QubitOperator(self.width)
out.oper = self.oper.extended_jw_transformation()
ft = time.perf_counter()
logger.info("Extended JW time: %s ms", round((ft - st) * 1000, 3))
return out

@cython.boundscheck(False)
Expand Down
52 changes: 35 additions & 17 deletions fulqrum/core/linear_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"""Fulqrum linearoperator module"""

import os
import time
import numpy as np
from scipy.sparse.linalg import LinearOperator

Expand All @@ -21,6 +22,10 @@
from .subspace import Subspace
from ..exceptions import FulqrumError

import logging

logger = logging.getLogger(__name__)


class SubspaceHamiltonian(LinearOperator):
"""Encapsulates the details of a subspace Hamiltonian problem
Expand All @@ -35,15 +40,25 @@ def __init__(self, hamiltonian, subspace=None):
"""A SciPy `LinearOperator` that represents a Hamiltonian restricted to the given
subspace.
"""
logger.info("Initializing SubspaceHamiltonian")
hsub_init_start = time.perf_counter()
if subspace:
if hamiltonian.width != subspace.width:
raise FulqrumError("Operator and subspace widths do not match")
else:
subspace = Subspace()
logger.info("Number of qubits: %s", hamiltonian.width)
self.diag_H, self.off_H = hamiltonian.split_diagonal()
self.diag_H, self.const_energy = self.diag_H.remove_constant_terms()
logger.info("Num. diagonal terms %s", self.diag_H.size())
logger.info("Num. off-diagonal terms %s", self.off_H.size())
# if there are no off-diagonal terms then we pass a dummy empty array of len=1
group_start = time.perf_counter()
self.off_H.group_sort()
group_stop = time.perf_counter()
logger.info(
"Term grouping time: %s ms", round((group_stop - group_start) * 1000, 3)
)
self.group_ptrs = np.zeros(1, dtype=np.uintp)
self.group_ladder_ptrs = np.zeros(1, dtype=np.uintp)

Expand All @@ -58,6 +73,8 @@ def __init__(self, hamiltonian, subspace=None):
)
self.group_ladder_ptrs = self.off_H.group_ladder_bin_starts()

logger.info("Num. off-diagonal groups: %s", self.group_ptrs.shape[0] - 1)

self.spmv = FulqrumSpMV(
self.diag_H,
self.const_energy,
Expand All @@ -71,6 +88,11 @@ def __init__(self, hamiltonian, subspace=None):
shape=(len(subspace),) * 2,
dtype=np.dtype(float) if self.spmv.is_real else np.dtype(complex),
)
hsub_init_stop = time.perf_counter()
logger.info(
"SubspaceHamiltonian total init time: %s ms",
round((hsub_init_stop - hsub_init_start) * 1000, 3),
)

@property
def num_groups(self):
Expand All @@ -90,16 +112,15 @@ def update_subspace(self, subspace):
self.spmv.update_subspace(subspace)
self.shape = (len(subspace),) * 2

def diagonal_vector(self, verbose=False, disable_fast_mode=False):
def diagonal_vector(self, disable_fast_mode=False):
"""Return diagonal vector of Hamiltonian in subspace

Parameters:
verbose (bool): optional, verbose output, default=False
disable_fast_mode (bool): optional, disable fast computation for type=2 Hamiltonians, default=False
Returns:
ndarray: Complex vector for diagonal of Hamiltonian
"""
return self.spmv.diagonal_vector(verbose, disable_fast_mode)
return self.spmv.diagonal_vector(disable_fast_mode)

def minimum_diagonal_energy(self):
"""Return the minimum diagonal energy
Expand Down Expand Up @@ -165,6 +186,7 @@ def matvec(self, x):
Returns:
ndarray: Output vector after SpMV on input vector
"""
start = time.perf_counter()
col_vec = False
if len(x.shape) == 2:
col_vec = True
Expand All @@ -176,38 +198,34 @@ def matvec(self, x):
out = self.spmv.matvec(x)
if col_vec:
out = out.view().reshape(x.shape[0], 1)
stop = time.perf_counter()
logger.info("Matvec time: %s ms", round((stop - start) * 1000, 3))
return out

def to_csr_linearoperator(self, verbose=False):
def to_csr_linearoperator(self):
"""Convert subspace Hamiltonian to a LinearOperator wrapping a CSR matrix

Parameters:
verbose (bool): Turn on verbose mode, default=False.

Returns:
CSRLinearOperator: LinearOperator wrapping a CSR matrix.
"""
M = self.spmv.to_csr_array(verbose=verbose)
M = self.spmv.to_csr_array()
return CSRLinearOperator(M, self.spmv.is_real)

def to_csr_linearoperator_fast(self, verbose=False):
def to_csr_linearoperator_fast(self):
"""Convert subspace Hamiltonian to a CSR LinearOperator faster but with a copy

Parameters:
verbose (bool): Turn on verbose mode, default=False.
Returns:
CSRLinearOperator: LinearOperator wrapping a CSR matrix.
"""
M = self.spmv.to_csrlike(verbose).to_csr_array(verbose)
M = self.spmv.to_csrlike().to_csr_array()
return CSRLinearOperator(M, self.spmv.is_real)

def _to_linearoperator(self, verbose=False):
def _to_linearoperator(self):
"""Convert subspace Hamiltonian to a CSR-like format LinearOperator

This saves a matrix-traversal at the expense of a non-standard data type

Parameters:
verbose (bool): Turn on verbose mode, default=False.
"""
out = self.spmv.to_csrlike(verbose)
out = self.spmv.to_csrlike()
return out


Expand Down
Loading
Loading