Skip to content

Commit b638fdf

Browse files
committed
temporally add asu script
1 parent ae558f1 commit b638fdf

File tree

1 file changed

+394
-0
lines changed

1 file changed

+394
-0
lines changed

pyxtal/asu_constraints.py

Lines changed: 394 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,394 @@
1+
"""
2+
Module for handling Asymmetric Unit (ASU) constraints with complex conditions.
3+
4+
This module provides tools to:
5+
1. Define ASU box constraints (x_min ≤ x ≤ x_max, etc.)
6+
2. Define additional inequality constraints (e.g., x ≤ y, y ≤ 1/2 - x)
7+
3. Check if coordinates satisfy ASU conditions
8+
4. Generate random points within the ASU
9+
5. Project coordinates into the ASU
10+
11+
Examples of ASU conditions:
12+
- Space Group 1 (P1): 0 ≤ x ≤ 1, 0 ≤ y ≤ 1, 0 ≤ z ≤ 1
13+
- Space Group 75 (P4): 0 ≤ x ≤ 1/2, 0 ≤ y ≤ 1/2, 0 ≤ z ≤ 1, with x ≤ y
14+
- Space Group 146 (R3): x ≤ (1+y)/2, y ≤ min(1-x, (1+x)/2), z ≤ 1
15+
"""
16+
17+
import numpy as np
18+
from fractions import Fraction
19+
from typing import List, Callable, Optional
20+
21+
22+
class ASUCondition:
23+
"""
24+
Represents a single inequality condition for ASU.
25+
26+
Examples:
27+
x ≤ y --> lambda coords: coords[0] <= coords[1]
28+
y ≤ 1/2 - x --> lambda coords: coords[1] <= 0.5 - coords[0]
29+
x ≤ (1+y)/2 --> lambda coords: coords[0] <= (1 + coords[1])/2
30+
"""
31+
32+
def __init__(self, condition: Callable, description: str):
33+
"""
34+
Initialize an ASU condition.
35+
36+
Args:
37+
condition: A callable that takes coords [x, y, z] and returns True/False
38+
description: A human-readable description of the condition
39+
"""
40+
self.condition = condition
41+
self.description = description
42+
43+
def check(self, coords: np.ndarray) -> bool:
44+
"""
45+
Check if coordinates satisfy this condition.
46+
47+
Args:
48+
coords: Array of shape (..., 3) with [x, y, z] coordinates
49+
50+
Returns:
51+
Boolean or array of booleans indicating if condition is satisfied
52+
"""
53+
return self.condition(coords)
54+
55+
def __repr__(self):
56+
return f"ASUCondition({self.description})"
57+
58+
59+
class ASU:
60+
"""
61+
Represents the Asymmetric Unit for a space group with box constraints
62+
and additional inequality conditions.
63+
"""
64+
65+
def __init__(self,
66+
x_min: float, x_max: float,
67+
y_min: float, y_max: float,
68+
z_min: float, z_max: float,
69+
conditions: Optional[List[ASUCondition]] = None):
70+
"""
71+
Initialize ASU with box bounds and optional additional conditions.
72+
73+
Args:
74+
x_min, x_max: x coordinate bounds
75+
y_min, y_max: y coordinate bounds
76+
z_min, z_max: z coordinate bounds
77+
conditions: List of additional ASUCondition objects
78+
"""
79+
self.bounds = np.array([x_min, x_max, y_min, y_max, z_min, z_max])
80+
self.conditions = conditions or []
81+
82+
@property
83+
def x_min(self): return self.bounds[0]
84+
85+
@property
86+
def x_max(self): return self.bounds[1]
87+
88+
@property
89+
def y_min(self): return self.bounds[2]
90+
91+
@property
92+
def y_max(self): return self.bounds[3]
93+
94+
@property
95+
def z_min(self): return self.bounds[4]
96+
97+
@property
98+
def z_max(self): return self.bounds[5]
99+
100+
def check_box_bounds(self, coords: np.ndarray) -> np.ndarray:
101+
"""
102+
Check if coordinates are within the box bounds.
103+
104+
Args:
105+
coords: Array of shape (..., 3) with [x, y, z] coordinates
106+
107+
Returns:
108+
Boolean array indicating if each point is within bounds
109+
"""
110+
coords = np.asarray(coords)
111+
if coords.ndim == 1:
112+
coords = coords.reshape(1, -1)
113+
114+
in_bounds = (
115+
(coords[..., 0] >= self.x_min) & (coords[..., 0] <= self.x_max) &
116+
(coords[..., 1] >= self.y_min) & (coords[..., 1] <= self.y_max) &
117+
(coords[..., 2] >= self.z_min) & (coords[..., 2] <= self.z_max)
118+
)
119+
return in_bounds
120+
121+
def check_conditions(self, coords: np.ndarray) -> np.ndarray:
122+
"""
123+
Check if coordinates satisfy all additional conditions.
124+
125+
Args:
126+
coords: Array of shape (..., 3) with [x, y, z] coordinates
127+
128+
Returns:
129+
Boolean array indicating if each point satisfies all conditions
130+
"""
131+
if not self.conditions:
132+
return np.ones(coords.shape[:-1], dtype=bool)
133+
134+
coords = np.asarray(coords)
135+
result = np.ones(coords.shape[:-1], dtype=bool)
136+
137+
for condition in self.conditions:
138+
result &= condition.check(coords)
139+
140+
return result
141+
142+
def is_valid(self, coords: np.ndarray) -> np.ndarray:
143+
"""
144+
Check if coordinates are within the ASU (both box bounds and conditions).
145+
146+
Args:
147+
coords: Array of shape (..., 3) with [x, y, z] coordinates
148+
149+
Returns:
150+
Boolean array indicating if each point is in the ASU
151+
"""
152+
return self.check_box_bounds(coords) & self.check_conditions(coords)
153+
154+
def generate_random_points(self, n: int, max_attempts: int = 10000) -> np.ndarray:
155+
"""
156+
Generate random points uniformly within the ASU.
157+
158+
Args:
159+
n: Number of points to generate
160+
max_attempts: Maximum number of attempts per point
161+
162+
Returns:
163+
Array of shape (n, 3) with valid ASU coordinates
164+
"""
165+
points = []
166+
attempts = 0
167+
168+
while len(points) < n and attempts < max_attempts:
169+
# Generate random points in the box
170+
candidates = np.random.rand(n - len(points), 3)
171+
candidates[:, 0] = candidates[:, 0] * (self.x_max - self.x_min) + self.x_min
172+
candidates[:, 1] = candidates[:, 1] * (self.y_max - self.y_min) + self.y_min
173+
candidates[:, 2] = candidates[:, 2] * (self.z_max - self.z_min) + self.z_min
174+
175+
# Check which ones satisfy all conditions
176+
valid = self.check_conditions(candidates)
177+
points.extend(candidates[valid])
178+
179+
attempts += 1
180+
181+
if len(points) < n:
182+
raise ValueError(f"Could not generate {n} valid points after {max_attempts} attempts")
183+
184+
return np.array(points[:n])
185+
186+
def project_to_asu(self, coords: np.ndarray, method: str = 'clamp') -> np.ndarray:
187+
"""
188+
Project coordinates to be within the ASU.
189+
190+
Args:
191+
coords: Array of shape (..., 3) with [x, y, z] coordinates
192+
method: Projection method ('clamp' or 'modulo')
193+
194+
Returns:
195+
Projected coordinates within the ASU
196+
"""
197+
coords = np.asarray(coords, dtype=float).copy()
198+
original_shape = coords.shape
199+
coords = coords.reshape(-1, 3)
200+
201+
if method == 'clamp':
202+
# Clamp to box bounds
203+
coords[:, 0] = np.clip(coords[:, 0], self.x_min, self.x_max)
204+
coords[:, 1] = np.clip(coords[:, 1], self.y_min, self.y_max)
205+
coords[:, 2] = np.clip(coords[:, 2], self.z_min, self.z_max)
206+
207+
# Handle additional conditions (simple approach: iterative adjustment)
208+
for _ in range(10): # Max iterations
209+
if np.all(self.check_conditions(coords)):
210+
break
211+
212+
for condition in self.conditions:
213+
invalid = ~condition.check(coords)
214+
if np.any(invalid):
215+
# Simple heuristic: move slightly toward center of ASU
216+
center = np.array([
217+
(self.x_min + self.x_max) / 2,
218+
(self.y_min + self.y_max) / 2,
219+
(self.z_min + self.z_max) / 2
220+
])
221+
coords[invalid] = 0.9 * coords[invalid] + 0.1 * center
222+
223+
elif method == 'modulo':
224+
# Apply periodic boundary conditions first
225+
coords[:, 0] = coords[:, 0] % 1.0
226+
coords[:, 1] = coords[:, 1] % 1.0
227+
coords[:, 2] = coords[:, 2] % 1.0
228+
229+
# Then clamp to ASU box
230+
coords[:, 0] = np.clip(coords[:, 0], self.x_min, self.x_max)
231+
coords[:, 1] = np.clip(coords[:, 1], self.y_min, self.y_max)
232+
coords[:, 2] = np.clip(coords[:, 2], self.z_min, self.z_max)
233+
234+
return coords.reshape(original_shape)
235+
236+
def __repr__(self):
237+
bounds_str = (f"x: [{self.x_min}, {self.x_max}], "
238+
f"y: [{self.y_min}, {self.y_max}], "
239+
f"z: [{self.z_min}, {self.z_max}]")
240+
if self.conditions:
241+
cond_str = ", conditions: " + ", ".join(str(c) for c in self.conditions)
242+
else:
243+
cond_str = ""
244+
return f"ASU({bounds_str}{cond_str})"
245+
246+
247+
# ============================================================================
248+
# Example ASU definitions with additional conditions
249+
# ============================================================================
250+
251+
def create_asu_for_space_group(space_group: int) -> ASU:
252+
"""
253+
Create ASU object for a given space group number.
254+
255+
This is a demonstration with a few examples. For complete implementation,
256+
you would need to add all 230 space groups.
257+
258+
Args:
259+
space_group: Space group number (1-230)
260+
261+
Returns:
262+
ASU object with appropriate bounds and conditions
263+
"""
264+
# Basic ASU bounds for common space groups (partial list for demonstration)
265+
# Full data can be loaded from pyxtal/database/asymmetric_unit.txt
266+
ASU_BOUNDS_DICT = {
267+
1: [0, 1, 0, 1, 0, 1],
268+
2: [0, 0.5, 0, 1, 0, 1],
269+
75: [0, 0.5, 0, 0.5, 0, 1],
270+
143: [0, 2/3, 0, 2/3, 0, 1],
271+
146: [0, 2/3, 0, 2/3, 0, 1/3],
272+
195: [0, 0.5, 0, 0.5, 0, 0.5],
273+
}
274+
275+
bounds = ASU_BOUNDS_DICT.get(space_group, [0, 1, 0, 1, 0, 1])
276+
x_min, x_max, y_min, y_max, z_min, z_max = bounds
277+
278+
# Define additional conditions for specific space groups
279+
conditions = []
280+
281+
# Space Group 75 (P4): Additional condition x ≤ y
282+
if space_group == 75:
283+
conditions.append(ASUCondition(
284+
lambda c: c[..., 0] <= c[..., 1],
285+
"x ≤ y"
286+
))
287+
288+
# Space Group 143 (P3): Additional conditions
289+
elif space_group == 143:
290+
conditions.extend([
291+
ASUCondition(
292+
lambda c: c[..., 0] <= c[..., 1],
293+
"x ≤ y"
294+
),
295+
ASUCondition(
296+
lambda c: c[..., 1] <= 0.5,
297+
"y ≤ 1/2"
298+
)
299+
])
300+
301+
# Space Group 146 (R3): More complex conditions
302+
elif space_group == 146:
303+
conditions.extend([
304+
ASUCondition(
305+
lambda c: c[..., 0] <= (1 + c[..., 1]) / 2,
306+
"x ≤ (1+y)/2"
307+
),
308+
ASUCondition(
309+
lambda c: c[..., 1] <= np.minimum(1 - c[..., 0], (1 + c[..., 0]) / 2),
310+
"y ≤ min(1-x, (1+x)/2)"
311+
)
312+
])
313+
314+
# Space Group 195 (P23): Additional condition x ≤ y and y ≤ z
315+
elif space_group == 195:
316+
conditions.extend([
317+
ASUCondition(
318+
lambda c: c[..., 0] <= c[..., 1],
319+
"x ≤ y"
320+
),
321+
ASUCondition(
322+
lambda c: c[..., 1] <= c[..., 2],
323+
"y ≤ z"
324+
)
325+
])
326+
327+
# Add more space groups as needed...
328+
329+
return ASU(x_min, x_max, y_min, y_max, z_min, z_max, conditions)
330+
331+
332+
# ============================================================================
333+
# Utility functions
334+
# ============================================================================
335+
336+
def parse_fraction(frac_str: str) -> float:
337+
"""Convert fraction string like '1/2' to float."""
338+
frac_str = frac_str.strip()
339+
if '/' in frac_str:
340+
return float(Fraction(frac_str))
341+
return float(frac_str)
342+
343+
344+
def test_asu_implementation():
345+
"""Test the ASU implementation with examples."""
346+
print("Testing ASU implementation\n" + "=" * 60)
347+
348+
# Test 1: Simple box constraint (Space Group 1)
349+
print("\n1. Space Group 1 (P1) - Simple box")
350+
asu1 = create_asu_for_space_group(1)
351+
print(asu1)
352+
353+
test_points = np.array([
354+
[0.5, 0.5, 0.5], # Valid
355+
[1.5, 0.5, 0.5], # Invalid (x > 1)
356+
[0.0, 0.0, 0.0], # Valid
357+
])
358+
valid = asu1.is_valid(test_points)
359+
print(f"Test points validity: {valid}")
360+
361+
# Test 2: Box with condition (Space Group 75)
362+
print("\n2. Space Group 75 (P4) - Box with x ≤ y")
363+
asu75 = create_asu_for_space_group(75)
364+
print(asu75)
365+
366+
test_points = np.array([
367+
[0.3, 0.4, 0.5], # Valid (x < y)
368+
[0.4, 0.3, 0.5], # Invalid (x > y)
369+
[0.25, 0.25, 0.5], # Valid (x = y)
370+
])
371+
valid = asu75.is_valid(test_points)
372+
print(f"Test points validity: {valid}")
373+
374+
# Test 3: Generate random points
375+
print("\n3. Generate random points in ASU 75")
376+
random_points = asu75.generate_random_points(5)
377+
print(f"Generated {len(random_points)} random points:")
378+
for i, pt in enumerate(random_points):
379+
print(f" Point {i+1}: x={pt[0]:.4f}, y={pt[1]:.4f}, z={pt[2]:.4f}, valid={asu75.is_valid(pt)}")
380+
381+
# Test 4: Project to ASU
382+
print("\n4. Project points to ASU")
383+
outside_points = np.array([
384+
[0.6, 0.3, 0.5], # Outside condition (x > y)
385+
[1.5, 0.5, 0.5], # Outside box
386+
])
387+
projected = asu75.project_to_asu(outside_points)
388+
print(f"Original points:\n{outside_points}")
389+
print(f"Projected points:\n{projected}")
390+
print(f"Projected points valid: {asu75.is_valid(projected)}")
391+
392+
393+
if __name__ == "__main__":
394+
test_asu_implementation()

0 commit comments

Comments
 (0)