Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Graph theoretic fragmentation via graphgen. #86

Merged
merged 46 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
4a3a4b3
Added `graphgen` fragmentation.
ShaunWeatherly Jan 14, 2025
cacf442
Fix reference to `mf.mol` when `lo_method="pipek"`
ShaunWeatherly Jan 14, 2025
61907e2
Fix reference to `frag_scratch` when `solver=="DMRG"`
ShaunWeatherly Jan 14, 2025
17f1b84
Ruff fixes.
ShaunWeatherly Jan 14, 2025
a37a0e2
Formatting and removed unused code.
ShaunWeatherly Jan 14, 2025
1f17bcd
Final formatting
ShaunWeatherly Jan 14, 2025
865b9c2
Update dependencies.
ShaunWeatherly Jan 14, 2025
364ae2f
`mypy` static typing.
ShaunWeatherly Jan 14, 2025
f0fd882
Suppress `mypy` for `networkx` imports.
ShaunWeatherly Jan 14, 2025
cd77b7d
Remove debug prints
ShaunWeatherly Jan 14, 2025
cff391d
Added complete docstring for `graphgen`.
ShaunWeatherly Jan 14, 2025
ec70e05
Add types package for `networkx`
ShaunWeatherly Jan 15, 2025
49c20d5
Unsuppress mypy warnings
ShaunWeatherly Jan 15, 2025
24adc16
`norm`
ShaunWeatherly Jan 15, 2025
5fec9ed
New `FragmentMap` data class.
ShaunWeatherly Jan 15, 2025
7a4e53d
Edits to `FragmentMap` typing.
ShaunWeatherly Jan 15, 2025
f94c164
Remove unused kwargs.
ShaunWeatherly Jan 15, 2025
79dce30
Fix formatting
ShaunWeatherly Jan 15, 2025
a4333a6
Final formatting
ShaunWeatherly Jan 15, 2025
083e275
Additions to `nitpick_exceptions`.
ShaunWeatherly Jan 15, 2025
bfa1273
`FragmentMap` Docstring edits.
ShaunWeatherly Jan 15, 2025
31186f7
Remove unfinished code.
ShaunWeatherly Jan 15, 2025
98b4f72
Use `np.floating[Any]`
ShaunWeatherly Jan 15, 2025
7d4e7b5
Organize imports.
ShaunWeatherly Jan 15, 2025
815f0fe
Fixes for `FragmentMap`
ShaunWeatherly Jan 15, 2025
e9d5db9
Add unit tests for `autogen` and `graphgen`
ShaunWeatherly Jan 15, 2025
79a0627
Long line fix.
ShaunWeatherly Jan 15, 2025
546ee00
Suppress E501 in `fragmentation_test`
ShaunWeatherly Jan 15, 2025
d21a66a
Add ruff exclusion rule for `fragmentation_tests`
ShaunWeatherly Jan 15, 2025
c5b60d9
Ruff formatting yet again.
ShaunWeatherly Jan 15, 2025
5ebb8d4
Add `fragmentation_test` to mypy blacklist
ShaunWeatherly Jan 15, 2025
432efcc
Remove defaults in `FragmentMap` init.
ShaunWeatherly Jan 16, 2025
5a6ec5b
Add unit tests for energy comparisons across `autogen` and `graphgen`
ShaunWeatherly Jan 16, 2025
0fddbff
Add checks for IAOs in `graphgen`
ShaunWeatherly Jan 16, 2025
d9fee42
Update `graphgen` docstring
ShaunWeatherly Jan 16, 2025
1eab146
Formatting.
ShaunWeatherly Jan 16, 2025
b8f1eaa
Test `intersphinxlink`
ShaunWeatherly Jan 16, 2025
fcf5c23
More strict typing for `adjacency_mat`
ShaunWeatherly Jan 16, 2025
e07aa2d
Test removing `fragment_map` from nitpick exceptions.
ShaunWeatherly Jan 16, 2025
5b2108b
Rename `valence_basis` to `iao_valence_basis`
ShaunWeatherly Jan 16, 2025
bbbbe9b
Finish renaming `valence_basis` to `iao_valence_basis`
ShaunWeatherly Jan 16, 2025
5cc31d7
Ruff fixes.
ShaunWeatherly Jan 16, 2025
32a1eff
Final formatting.
ShaunWeatherly Jan 16, 2025
af8a8cf
Update `molbe_ppp`
ShaunWeatherly Jan 16, 2025
b4e2b5a
Address Oskar comments.
ShaunWeatherly Jan 17, 2025
8253907
Simplified check for `fragpart.mol`
ShaunWeatherly Jan 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"numpy>=1.22.0",
"scipy>=1.7.0",
"pyscf>=2.0.0",
"networkx",
"matplotlib",
"libdmet @ git+https://github.com/gkclab/libdmet_preview.git",
"attrs",
Expand Down
213 changes: 211 additions & 2 deletions src/quemb/molbe/autofrag.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,221 @@
# Author: Oinam Romesh Meitei

# Author: Oinam Romesh Meitei, Shaun Weatherly

import networkx as nx # type: ignore
import numpy as np
ShaunWeatherly marked this conversation as resolved.
Show resolved Hide resolved
from numpy.linalg import norm
from pyscf import gto

from quemb.molbe.helper import get_core
from quemb.shared.helper import unused


def euclidean_norm(
i_coord: float,
mcocdawc marked this conversation as resolved.
Show resolved Hide resolved
j_coord: float,
):
return np.linalg.norm(np.asarray(i_coord - j_coord))
ShaunWeatherly marked this conversation as resolved.
Show resolved Hide resolved

ShaunWeatherly marked this conversation as resolved.
Show resolved Hide resolved

def remove_nonnunique_frags(
fragment_map: dict,
ShaunWeatherly marked this conversation as resolved.
Show resolved Hide resolved
):
for adx, basa in enumerate(fragment_map["fsites"]):
for bdx, basb in enumerate(fragment_map["fsites"]):
if adx == bdx:
pass
elif set(basb).issubset(set(basa)):
fragment_map["center"][adx] = (
fragment_map["center"][adx] + fragment_map["center"][bdx]
)
del fragment_map["center"][bdx]
del fragment_map["fsites"][bdx]
del fragment_map["fs"][bdx]

return fragment_map


def graphgen(
mol: gto.Mole,
be_type: str = "BE2",
frozen_core: bool = True,
remove_nonunique_frags: bool = True,
frag_prefix: str = "f",
connectivity: str = "euclidean",
# draw_graph: bool = True,
):
mcocdawc marked this conversation as resolved.
Show resolved Hide resolved
"""Generate fragments via adjacency graph.

Generalizes the BEn fragmentation scheme to arbitrary fragment sizes using a
graph theoretic heuristic. In brief: atoms are assigned to nodes in an
adjacency graph and edges are weighted by some distance metric. For a given
fragment center site, Dijkstra's algorithm is used to find the shortest path
from that center to its neighbors. The number of nodes visited on that shortest
path determines the degree of separation of the corresponding neighbor. I.e.,
all atoms whose shortest paths from the center site visit at most 1 node must
be direct neighbors to the center site, which gives BE2-type fragments; all
atoms whose shortest paths visit at most 2 nodes must then be second-order
neighbors, hence BE3; and so on.

Parameters
----------
mol : pyscf.gto.mole.Mole
The molecule object.
ShaunWeatherly marked this conversation as resolved.
Show resolved Hide resolved
be_type : str
The order of nearest neighbors (with respect to the center atom)
included in a fragment. Supports all 'BEn', with 'n' in -
[1, 2, 3, 4, 5, 6, 7, 8, 9] having been tested.
frozen_core: bool
Whether to exclude core AO indices from the fragmentation process.
True by default.
remove_nonunique_frags: bool
Whether to remove fragments which are strict subsets of another
fragment in the system. True by default.
frag_prefix: str
Prefix to be appended to the fragment datanames. Useful for managing
fragment scratch directories.
connectivity: str
Keyword string specifying the distance metric to be used for edge
weights in the fragment adjacency graph. Currently supports "euclidean"
(which uses the square of the distance between atoms in real
space to determine connectivity within a fragment.)
"""
assert mol is not None

fragment_type_order = int(be_type[-1])
natm = mol.natm

adx_map = {
adx: {
"bas": bas,
"label": mol.atom_symbol(adx),
"coord": mol.atom_coord(adx),
"shortest_paths": dict(),
}
for adx, bas in enumerate(mol.aoslice_by_atom())
}

fragment_map = {
"fsites": list(tuple()),
ShaunWeatherly marked this conversation as resolved.
Show resolved Hide resolved
"fs": list(tuple(tuple())),
"edge": list(tuple(tuple())),
"center": list(tuple()),
"centerf_idx": list(tuple()),
"ebe_weights": list(tuple()),
"sites": list(),
"dnames": list(),
"core_offset": int(0),
"adjacency_mat": np.zeros((natm, natm), np.float64),
"adjacency_graph": nx.Graph(),
}
fragment_map["adjacency_graph"].add_nodes_from(adx_map)

for adx, map in adx_map.items():
start_ = map["bas"][2]
stop_ = map["bas"][3]
if frozen_core:
_, _, core_list = get_core(mol)
start_ -= fragment_map["core_offset"]
ncore_ = int(core_list[adx])
stop_ -= fragment_map["core_offset"] + ncore_
fragment_map["core_offset"] += ncore_
fragment_map["sites"].append(tuple([i for i in range(start_, stop_)]))
else:
fragment_map["sites"].append(tuple([i for i in range(start_, stop_)]))

if connectivity.lower() in ["euclidean_distance", "euclidean"]:
# Begin by constructing the adjacency matrix and adjacency graph
# for the system. Each node corresponds to an atom, such that each
# pair of nodes can be assigned an edge weighted by the square of
# their distance in real space.
for adx in range(natm):
for bdx in range(adx + 1, natm):
dr = (
euclidean_norm(
adx_map[adx]["coord"],
adx_map[bdx]["coord"],
)
** 2
)
fragment_map["adjacency_mat"][adx, bdx] = dr
fragment_map["adjacency_graph"].add_edge(adx, bdx, weight=dr)

# For a given center site (adx), find the set of shortest
# paths to all other sites. The number of nodes visited
# on that path gives the degree of separation of the
# sites.
for adx, map in adx_map.items():
fsites_temp = fragment_map["sites"][adx]
fs_temp = []
fs_temp.append(fragment_map["sites"][adx])
map["shortest_paths"] = dict(
nx.single_source_all_shortest_paths(
fragment_map["adjacency_graph"],
source=adx,
weight=lambda a, b, _: (
fragment_map["adjacency_graph"][a][b]["weight"]
),
method="dijkstra",
)
)

# If the degree of separation is smaller than the *n*
# in your fragment type, BE*n*, then that site is appended to
# the set of fragment sites for adx.
for bdx, path in map["shortest_paths"].items():
if 0 < (len(path[0]) - 1) < fragment_type_order:
fsites_temp = fsites_temp + fragment_map["sites"][bdx]
fs_temp.append(fragment_map["sites"][bdx])

fragment_map["fsites"].append(tuple(fsites_temp))
ShaunWeatherly marked this conversation as resolved.
Show resolved Hide resolved
fragment_map["fs"].append(tuple(fs_temp))
fragment_map["center"].append(tuple(fragment_map["sites"][adx]))

elif connectivity.lower() in ["resistance_distance", "resistance"]:
raise NotImplementedError("Work in progress...")

elif connectivity.lower() in ["entanglement"]:
raise NotImplementedError("Work in progress...")

else:
raise AttributeError(f"Connectivity metric not recognized: '{connectivity}'")

# Remove all fragments whose AO indices can be identified as subsets of
# another fragment's. The center site for the removed frag is then
# added to that of the superset. Because doing so will necessarily
# change the definition of fragments, we repeat it up to `natm` times
# such that all fragments are guaranteed to be distinct sets.
if remove_nonunique_frags:
for _ in range(0, natm):
fragment_map = remove_nonnunique_frags(fragment_map)
mcocdawc marked this conversation as resolved.
Show resolved Hide resolved

ShaunWeatherly marked this conversation as resolved.
Show resolved Hide resolved
# Define the 'edges' for fragment A as the intersect of its sites
# with the set of all center sites outside of A:
for adx, fs in enumerate(fragment_map["fs"]):
edge: set[tuple] = set()
for bdx, center in enumerate(fragment_map["center"]):
if adx == bdx:
pass
else:
overlap = set(fs).intersection(set((center,)))
if overlap:
mcocdawc marked this conversation as resolved.
Show resolved Hide resolved
edge = edge.union(overlap)
fragment_map["edge"].append(tuple(edge))

# Update relative center site indices (centerf_idx) and weights
# for center site contributions to the energy (ebe_weights):
for adx, center in enumerate(fragment_map["center"]):
centerf_idx = [fragment_map["fsites"][adx].index(cdx) for cdx in center]
ebe_weight = [1.0, tuple(centerf_idx)]
fragment_map["centerf_idx"].append(tuple(centerf_idx))
fragment_map["ebe_weights"].append(tuple(ebe_weight))

# Finally, set fragment data names for scratch and bookkeeping:
for adx, _ in enumerate(fragment_map["fs"]):
fragment_map["dnames"].append(str(frag_prefix) + str(adx))

return fragment_map


def autogen(
mol,
frozen_core=True,
Expand Down
30 changes: 27 additions & 3 deletions src/quemb/molbe/fragment.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Author: Oinam Romesh Meitei


from quemb.molbe.autofrag import autogen
from quemb.molbe.autofrag import autogen, graphgen
from quemb.molbe.helper import get_core
from quemb.molbe.lchain import chain as _ext_chain
from quemb.shared.helper import copy_docstring
Expand Down Expand Up @@ -82,24 +82,47 @@ def __init__(

# Check for frozen core approximation
if frozen_core:
self.ncore, self.no_core_idx, self.core_list = get_core(mol)
self.ncore, self.no_core_idx, self.core_list = get_core(self.mol)

# Check type of fragmentation function
if frag_type == "hchain_simple":
# This is an experimental feature.
self.hchain_simple()

elif frag_type == "chain":
if mol is None:
raise ValueError(
"Provide pyscf gto.M object in fragpart() and restart!"
)
self.chain(mol, frozen_core=frozen_core, closed=closed)

elif frag_type == "graphgen":
if self.mol is None:
mcocdawc marked this conversation as resolved.
Show resolved Hide resolved
raise ValueError(
"Provide pyscf gto.M object in fragpart() and restart!"
)
fragment_map = graphgen(
mol=self.mol.copy(),
be_type=be_type,
frozen_core=frozen_core,
remove_nonunique_frags=True,
frag_prefix="f",
connectivity="euclidean",
)

self.fsites = fragment_map["fsites"]
self.edge = fragment_map["edge"]
self.center = fragment_map["center"]
# self.edge_idx = fragment_map["edge"]
ShaunWeatherly marked this conversation as resolved.
Show resolved Hide resolved
self.centerf_idx = fragment_map["centerf_idx"]
self.ebe_weight = fragment_map["ebe_weights"]
self.Nfrag = len(self.fsites)

elif frag_type == "autogen":
if mol is None:
raise ValueError(
"Provide pyscf gto.M object in fragpart() and restart!"
)

fgs = autogen(
mol,
be_type=be_type,
Expand All @@ -124,6 +147,7 @@ def __init__(
self.add_center_atom,
) = fgs
self.Nfrag = len(self.fsites)

else:
raise ValueError(f"Fragmentation type = {frag_type} not implemented!")

Expand Down
2 changes: 1 addition & 1 deletion src/quemb/molbe/lo.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ def localize(
W_ = C_ @ W_

self.W = get_loc(
self.mol, W_, "PM", pop_method=pop_method, init_guess=init_guess
self.mf.mol, W_, "PM", pop_method=pop_method, init_guess=init_guess
mcocdawc marked this conversation as resolved.
Show resolved Hide resolved
)

if not self.frozen_core:
Expand Down
6 changes: 3 additions & 3 deletions src/quemb/molbe/solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -908,10 +908,10 @@ def solve_block2(
mc.fcisolver.twodot_to_onedot = DMRG_args.twodot_to_onedot
mc.fcisolver.maxIter = DMRG_args.max_iter
mc.fcisolver.block_extra_keyword = DMRG_args.block_extra_keyword
mc.fcisolver.scratchDirectory = str(frag_scratch)
mc.fcisolver.runtimeDir = str(frag_scratch)
mc.fcisolver.scratchDirectory = frag_scratch.path
mc.fcisolver.runtimeDir = frag_scratch.path
mc.fcisolver.memory = DMRG_args.max_mem
os.chdir(frag_scratch)
os.chdir(frag_scratch.path)
mcocdawc marked this conversation as resolved.
Show resolved Hide resolved

mc.kernel(orbs)
rdm1, rdm2 = dmrgscf.DMRGCI.make_rdm12(
Expand Down
Loading