@@ -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+
85161class 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"\n Generation { gen :d} starts" )
263- self .logging .info (f"Generation { gen :d} starts" )
358+ gen_hdr = f"\n Generation { 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