Skip to content

Commit 5725ae2

Browse files
committed
improve QRS
1 parent 56f8393 commit 5725ae2

2 files changed

Lines changed: 127 additions & 28 deletions

File tree

examples/example_08_QRS_known_cell.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
writer = csv.writer(fcsv)
2020
writer.writerow(["code", "smiles", "sg", "success_rate"])
2121

22-
for code in db.get_all_codes():
22+
for code in db.get_all_codes()[3:]:
2323
# ── Molecule ──────────────────────────────────────────────────────────────────
2424
row = db.get_row(code=code)
2525
ref_xtal = db.get_pyxtal(code=code)
@@ -30,32 +30,32 @@
3030

3131
sites = [[] for _ in range(len(ref_xtal.numMols))]
3232
for site in ref_xtal.mol_sites:
33-
print(site.type, site.wp.get_label())
3433
sites[site.type].append(site.wp.get_label())
35-
print("Molecule sites:", sites)
3634

3735
# ── QRS setup ─────────────────────────────────────────────────────────────────
3836
qrs = QRS(
39-
smiles = row.mol_smi, # molecule as SMILES string
40-
workdir = row.csd_code, # working directory for this QRS run
41-
sg = ref_xtal.group.hall_number, # space group number (P2_1/c = 81)
42-
tag = row.csd_code.lower(), # tag for output files
43-
use_hall = True, # interpret sg as a Hall number
44-
lattice = ref_xtal.lattice, # fix the cell; only WP positions are sampled
45-
N_gen = 5, # number of QRS generations
46-
N_pop = 20, # structures per generation
47-
N_cpu = 1,
48-
cif = "all.cif", # save all relaxed structures
49-
skip_mlp = True, # no machine-learning potential relaxation
50-
verbose = False,
51-
sites = sites,
37+
smiles = row.mol_smi, # molecule as SMILES string
38+
workdir = row.csd_code, # working directory for this QRS run
39+
sg = ref_xtal.group.hall_number, # space group number (P2_1/c = 81)
40+
tag = row.csd_code.lower(), # tag for output files
41+
use_hall = True, # interpret sg as a Hall number
42+
lattice = ref_xtal.lattice, # fix the cell; only WP positions are sampled
43+
N_gen = 10, # number of QRS generations
44+
N_pop = 50, # structures per generation
45+
N_cpu = 1,
46+
cif = "all.cif", # save all relaxed structures
47+
skip_mlp = True, # no machine-learning potential relaxation
48+
verbose = False,
49+
sites = sites,
50+
delta_length = 1.5, # grid spacing for fractional coords (Å)
51+
delta_angle = 60.0, # grid spacing for Euler/torsion angles (°)
5252
)
5353

5454
# ── Run and check for match ───────────────────────────────────────────────────
5555
success_rate = qrs.run(ref_pmg=ref_pmg)
5656

5757
if success_rate is not None and success_rate > 0:
58-
print(f"\nMatch found! Success rate: {success_rate:.1%}")
58+
print(f"\nMatch found! Success rate: {success_rate}%")
5959
else:
6060
print("\nNo match found within the given generations/population.")
6161

pyxtal/optimize/QRS.py

Lines changed: 110 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def generate_qrs_cell(sampler, cell_bounds, ref_volume, ltype):
3535
if count == 1000:
3636
raise ValueError("Cannot generate valid cell with 1000 attempts")
3737

38-
def generate_qrs_xtals(cell, wp_bounds, N_pop, smiles, comp, sampler_wp=None, d_tol=0.85):
38+
def generate_qrs_xtals(cell, wp_bounds, N_pop, smiles, comp, sampler_wp=None, d_tol=0.9):
3939
"""
4040
Get the qrs xtal samples
4141
@@ -60,10 +60,13 @@ def generate_qrs_xtals(cell, wp_bounds, N_pop, smiles, comp, sampler_wp=None, d_
6060
if sampler_wp is None:
6161
sampler_wp = qmc.Sobol(d=len(lb), scramble=False)
6262

63-
m = max([int(np.log2(N_pop))+3, 9])
63+
max_attempts = 2 ** max(int(np.log2(N_pop)) + 3, 9)
6464

65-
for i in range(2**m):
66-
sample_wp = sampler_wp.random()#; print(sample_wp)
65+
for i in range(max_attempts):
66+
try:
67+
sample_wp = sampler_wp.random()
68+
except StopIteration:
69+
break
6770
sample_wp = qmc.scale(sample_wp, lb, ub)[0].tolist()
6871
x = [cell]
6972
prev = 0
@@ -82,6 +85,79 @@ def generate_qrs_xtals(cell, wp_bounds, N_pop, smiles, comp, sampler_wp=None, d_
8285
return xtals
8386

8487

88+
def compute_wp_resolutions(wp_bounds, cell_lengths, delta_length=1.0, delta_angle=30.0):
89+
"""
90+
Compute per-DOF grid resolution (number of levels) for an uneven QRS grid.
91+
92+
Rules:
93+
- Fractional coordinate DOF (bounds [0, 1]): n = max(1, int(edge / delta_length))
94+
Cell edges are assigned in order a, b, c as coordinate DOF are encountered.
95+
- Angle DOF (any other bounds): n = max(1, round(span / delta_angle))
96+
97+
Args:
98+
wp_bounds (list): per-site bounds from mol_site.get_bounds()
99+
cell_lengths (list): [a, b, c] in Angstroms
100+
delta_length (float): Angstrom resolution for coordinate dims
101+
delta_angle (float): degree resolution for angle dims
102+
103+
Returns:
104+
list[int]: number of levels per DOF (flat, same ordering as wp_bounds)
105+
"""
106+
n_levels = []
107+
for site_bounds in wp_bounds:
108+
coord_idx = 0
109+
for (lb, ub) in site_bounds:
110+
span = ub - lb
111+
if abs(span - 1.0) < 1e-6: # fractional coordinate DOF
112+
edge = cell_lengths[coord_idx] if coord_idx < len(cell_lengths) else 1.0
113+
n = max(1, int(edge / delta_length))
114+
coord_idx += 1
115+
else: # angle DOF (Euler or torsion)
116+
n = max(1, int(round(span / delta_angle)))
117+
n_levels.append(n)
118+
return n_levels
119+
120+
121+
class GridSampler:
122+
"""
123+
Quantized quasi-random sampler for uneven discrete grids.
124+
125+
Draws points from a Sobol sequence and snaps each coordinate to the centre
126+
of the nearest bin defined by n_levels (the number of grid levels per
127+
dimension). This gives Sobol's multi-dimensional low-discrepancy coverage
128+
from the very first sample — every dimension varies immediately — while
129+
keeping all output values on a per-dimension discrete grid.
130+
131+
Has the same .random() interface as qmc.Sobol, so it is a drop-in
132+
replacement. self.current advances on every call; successive generation
133+
runs automatically draw the next block of the Sobol sequence.
134+
"""
135+
136+
def __init__(self, n_levels):
137+
self.n_levels = list(n_levels)
138+
self.d = len(n_levels)
139+
self.total = int(np.prod(n_levels)) if n_levels else 0
140+
self.current = 0
141+
self._sobol = qmc.Sobol(d=self.d, scramble=False)
142+
143+
@property
144+
def exhausted(self):
145+
return self.current >= self.total
146+
147+
def random(self):
148+
"""Return the next quantized Sobol point as a (1, d) array in [0, 1]^d."""
149+
raw = self._sobol.random()[0] # values in [0, 1)
150+
snapped = [(min(int(v * n), n - 1) + 0.5) / n
151+
for v, n in zip(raw, self.n_levels)]
152+
self.current += 1
153+
return np.array([snapped])
154+
155+
def __repr__(self):
156+
pct = 100.0 * self.current / self.total if self.total else 0.0
157+
return (f"GridSampler(d={self.d}, total={self.total:,}, "
158+
f"progress={self.current}/{self.total} [{pct:.1f}%])")
159+
160+
85161
class QRS(GlobalOptimize):
86162
"""
87163
Quasi-Random Sampling
@@ -114,6 +190,8 @@ class QRS(GlobalOptimize):
114190
E_max (float): maximum energy defined as an invalid structure
115191
verbose (bool): show more details
116192
use_mpi (bool): whether or not use mpi for parallel calculation
193+
delta_length (float): grid resolution in Å for fractional-coordinate DOF (0 = use Sobol)
194+
delta_angle (float): grid resolution in degrees for angle DOF (0 = use Sobol)
117195
"""
118196

119197
def __init__(
@@ -152,13 +230,17 @@ def __init__(
152230
early_quit: bool = False,
153231
check_stable: bool = False,
154232
use_mpi: bool = False,
233+
delta_length: float = 1.0,
234+
delta_angle: float = 60.0,
155235
):
156236

157237
# POPULATION parameters:
158238
self.N_gen = N_gen # Number of lattice points
159239
self.N_pop = N_pop # Number of wp varieties
160240
self.verbose = verbose
161241
self.name = 'QRS'
242+
self.delta_length = delta_length
243+
self.delta_angle = delta_angle
162244

163245
# initialize other base parameters
164246
GlobalOptimize.__init__(
@@ -218,25 +300,39 @@ def _init_qrs_params(self):
218300
"""
219301
from pyxtal import pyxtal
220302
from pyxtal.symmetry import Group
303+
from pyxtal.wyckoff_site import mol_site
304+
221305
tmp = pyxtal(molecular=True)
222306
#print(self.smiles, self.sg, self.lattice); import sys; sys.exit()
223307
smiles = [smi + ".smi" for smi in self.smiles]
224308
sg = self.sg[0]
225309
wp = Group(sg, use_hall=True)[0] if self.use_hall else Group(sg)[0]
226310
mult = len(wp)
227311
numIons = [int(c * mult) for c in self.composition]
228-
for _ in range(20):
312+
313+
for _ in range(200):
229314
tmp.from_random(3, sg, smiles, numIons,
230315
lattice=self.lattice, use_hall=self.use_hall,
231-
sites=self.sites,
232-
force_pass=True) # skip structures with bad distances
316+
sites=self.sites) # skip structures with bad distances
233317
if tmp.valid:
234318
break
235319
self.hall_number = sg
236320
self.ltype = self.lattice.ltype
237321
self.wp_bounds = [site.get_bounds() for site in tmp.mol_sites]
238-
len_reps = sum(len(b) for b in self.wp_bounds)
239-
self.sampler = qmc.Sobol(d=len_reps, scramble=False)
322+
323+
if self.delta_length > 0 or self.delta_angle > 0:
324+
# Uneven grid: per-dim resolution derived from cell lengths / angle range
325+
dl = self.delta_length if self.delta_length > 0 else 1.0
326+
da = self.delta_angle if self.delta_angle > 0 else 30.0
327+
a, b, c = self.lattice.get_para()[:3]
328+
n_levels = compute_wp_resolutions(self.wp_bounds, [a, b, c], dl, da)
329+
self.sampler = GridSampler(n_levels)
330+
print(f"GridSampler initialised: {self.sampler}")
331+
else:
332+
len_reps = sum(len(b) for b in self.wp_bounds)
333+
self.sampler = qmc.Sobol(d=len_reps, scramble=False)
334+
#print(f"Bootstrap QRS parameters from random structure: hall={self.hall_number}, "
335+
# f"ltype={self.ltype}, wp_bounds={self.wp_bounds}")
240336

241337
def _run(self, pool=None):
242338
"""
@@ -259,8 +355,11 @@ def _run(self, pool=None):
259355
cur_xtals = None
260356

261357
if self.rank == 0:
262-
print(f"\nGeneration {gen:d} starts")
263-
self.logging.info(f"Generation {gen:d} starts")
358+
gen_hdr = f"\nGeneration {gen:d} starts"
359+
if self.lattice is not None and isinstance(self.sampler, GridSampler):
360+
gen_hdr += f" [{self.sampler}]"
361+
print(gen_hdr)
362+
self.logging.info(gen_hdr)
264363
t0 = time()
265364

266365
if self.lattice is not None:

0 commit comments

Comments
 (0)