Spaces:
Sleeping
Sleeping
File size: 10,597 Bytes
cd470c1 f120404 cd470c1 f120404 cd470c1 f120404 cd470c1 f120404 cd470c1 f120404 cd470c1 f120404 cd470c1 f120404 cd470c1 f120404 cd470c1 f120404 cd470c1 f120404 cd470c1 f120404 cd470c1 f120404 cd470c1 f120404 cd470c1 f120404 cd470c1 f120404 cd470c1 f120404 cd470c1 f120404 cd470c1 f120404 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 | # Heuristic property estimation, phase classification, and structural scoring.
# This module acts as the "Physics Engine" of the environment.
from typing import Dict
try:
from .config import ATOM_TYPES, EMPTY, PROPERTY_NAMES
from .lattice import Lattice
except ImportError:
from config import ATOM_TYPES, EMPTY, PROPERTY_NAMES
from lattice import Lattice
from collections import deque
# Main entry point for property calculation: converts grid state to physical metrics.
def estimate_properties(lattice: Lattice) -> Dict[str, float]:
"""Estimate material properties (0-100) from the current lattice state.
Each property is a weighted combination of:
- Atom fraction * base contribution
- Neighbor bonding bonus
- Density/packing bonus
"""
total_cells = lattice.size * lattice.size
n_atoms = lattice.atom_count()
if n_atoms == 0:
return {p: 0.0 for p in PROPERTY_NAMES}
counts = lattice.count_atoms()
density = n_atoms / total_cells
# Base contributions from atom fractions
props: Dict[str, float] = {p: 0.0 for p in PROPERTY_NAMES}
for sym, count in counts.items():
if count == 0:
continue
frac = count / n_atoms
for prop in PROPERTY_NAMES:
props[prop] += frac * ATOM_TYPES[sym]["contributions"][prop]
# Neighbor bonding bonus: atoms with same-type neighbors boost their contributions
bonding_bonus: Dict[str, float] = {p: 0.0 for p in PROPERTY_NAMES}
for r in range(lattice.size):
for c in range(lattice.size):
cell = lattice.get(r, c)
if cell == EMPTY:
continue
neighbors = lattice.get_neighbors(r, c)
same_type = sum(1 for n in neighbors if n == cell)
occupied = sum(1 for n in neighbors if n != EMPTY)
if same_type > 0:
# Same-type clustering boosts the atom's primary property
for prop in PROPERTY_NAMES:
bonding_bonus[prop] += (
ATOM_TYPES[cell]["contributions"][prop]
* same_type
/ (8.0 * n_atoms)
)
# Connectivity-driven conductivity: B-B bonds boost significantly
if cell == "B":
b_neighbors = sum(1 for n in neighbors if n == "B")
bonding_bonus["conductivity"] += b_neighbors * 0.05
# Percolation check: Does a continuous path of B atoms span the grid?
# This simulates long-range conductivity pathways.
percolation_bonus = 0.0
if counts.get("B", 0) >= lattice.size:
b_coords = [(r, c) for r in range(lattice.size) for c in range(lattice.size) if lattice.get(r, c) == "B"]
if b_coords:
# BFS to find if top spans to bottom
start_nodes = [node for node in b_coords if node[0] == 0]
if start_nodes:
visited = set(start_nodes)
queue = deque(start_nodes)
while queue:
r, c = queue.popleft()
if r == lattice.size - 1:
percolation_bonus = 15.0 # Found path
break
for nr, nc in [(r-1, c), (r+1, c), (r, c-1), (r, c+1)]:
if (nr, nc) in b_coords and (nr, nc) not in visited:
visited.add((nr, nc))
queue.append((nr, nc))
# Density bonus: more filled grid generally means stronger properties
density_mult = 0.6 + 0.4 * density
# Void fraction helps elasticity (some voids = flexible)
void_fraction = 1.0 - density
elasticity_void_bonus = 0.15 * min(void_fraction / 0.5, 1.0) if void_fraction > 0.1 else 0.0
# Scale to 0-100. With primary contribution = 1.0, a full grid of one
# atom type should be able to reach ~95 for its primary property.
# base_max = 1.0 * 80 = 80, bonding_max ~ 15, density_max = 10 → ~105 clamped to 100.
result: Dict[str, float] = {}
for prop in PROPERTY_NAMES:
base = props[prop] * 80.0 # base contribution (up to ~80)
bonding = bonding_bonus[prop] * 25.0 # bonding bonus (up to ~15)
dens = (density_mult - 0.6) * 25.0 # density bonus (up to ~10)
val = base + bonding + dens
if prop == "conductivity":
val += percolation_bonus
if prop == "elasticity":
val += elasticity_void_bonus * 100.0
result[prop] = round(min(max(val, 0.0), 100.0), 2)
return result
# Determines the crystalline quality of the lattice based on pattern repetition.
def classify_phase(lattice: Lattice) -> str:
"""Classify the crystal phase based on structural regularity.
Returns "crystalline", "polycrystalline", or "amorphous".
"""
n_atoms = lattice.atom_count()
if n_atoms < 4:
return "amorphous"
grid = lattice.get_grid()
size = lattice.size
# Check for repeating 2x2 sub-patterns to detect long-range order.
pattern_counts: Dict[str, int] = {}
for r in range(size - 1):
for c in range(size - 1):
block = (grid[r][c], grid[r][c + 1], grid[r + 1][c], grid[r + 1][c + 1])
# Skip all-empty blocks
if all(cell == EMPTY for cell in block):
continue
key = "|".join(block)
pattern_counts[key] = pattern_counts.get(key, 0) + 1
if not pattern_counts:
return "amorphous"
total_blocks = sum(pattern_counts.values())
max_pattern = max(pattern_counts.values())
top_patterns = sorted(pattern_counts.values(), reverse=True)
# High regularity = crystalline
if max_pattern / total_blocks > 0.35:
return "crystalline"
# Multiple distinct regions with local regularity = polycrystalline
if len(top_patterns) >= 2 and top_patterns[1] / total_blocks > 0.15:
return "polycrystalline"
# Row/column periodicity check for 1D symmetry.
row_repeats = 0
for r in range(size):
occupied = [grid[r][c] for c in range(size) if grid[r][c] != EMPTY]
if len(occupied) >= 2 and len(set(occupied)) == 1:
row_repeats += 1
col_repeats = 0
for c in range(size):
occupied = [grid[r][c] for r in range(size) if grid[r][c] != EMPTY]
if len(occupied) >= 2 and len(set(occupied)) == 1:
col_repeats += 1
regularity = (row_repeats + col_repeats) / (2 * size)
if regularity > 0.4:
return "crystalline"
if regularity > 0.2:
return "polycrystalline"
return "amorphous"
# Calculates structural integrity based on bond density and mirror symmetry.
def compute_structural_stability(lattice: Lattice) -> float:
"""Compute structural stability score (0.0 to 1.0).
Analyses the Gibbs-like stability of the lattice configuration.
This logic specifically penalizes 1D 'linear' chains and rewards 2D clusters.
"""
n_atoms = lattice.atom_count()
if n_atoms == 0:
return 0.0
# Coordination-based stability: Non-linear rewards for high neighbor density.
# Penalizes dangling bonds (1 neighbor) and isolated atoms (0 neighbors).
coordination_energy = 0.0
for r in range(lattice.size):
for c in range(lattice.size):
if lattice.get(r, c) == EMPTY:
continue
neighbors = lattice.get_neighbors(r, c)
cn = sum(1 for n in neighbors if n != EMPTY)
if cn == 0:
coordination_energy -= 1.5 # Isolated atom
elif cn == 1:
coordination_energy -= 0.5 # Dangling bond (end of a line)
elif cn == 2:
coordination_energy += 0.3 # Simple line (low stability)
elif cn == 3:
coordination_energy += 1.0 # Cluster edge (good stability)
else:
coordination_energy += 1.6 # Crystal core (excellent stability)
# Normalize based on atom count and clamp to [0, 1].
# A perfectly packed 3x3 square would have high cn, maximizing this score.
stability_norm = min(max(coordination_energy / n_atoms, -1.0), 1.0)
stability_norm = (stability_norm + 1.0) / 2.0
# Point-group Symmetry (Mirror Planes) check.
grid = lattice.get_grid()
size = lattice.size
h_mirrors = 0
v_mirrors = 0
total_checks = 0
for r in range(size):
for c in range(size // 2):
total_checks += 1
if grid[r][c] == grid[r][size - 1 - c]:
h_mirrors += 1
for c in range(size):
for r in range(size // 2):
if grid[r][c] == grid[size - 1 - r][c]:
v_mirrors += 1
symmetry_factor = (h_mirrors + v_mirrors) / (2 * total_checks) if total_checks > 0 else 0.0
return round(min(0.65 * stability_norm + 0.35 * symmetry_factor, 1.0), 4)
# Measures the positional entropy of the atomic arrangement.
def compute_lattice_order(lattice: Lattice) -> float:
"""Compute lattice structural order (0.0 to 1.0).
Measures the positional entropy and distribution uniformity of the
atomic ensemble.
"""
n_atoms = lattice.atom_count()
if n_atoms == 0:
return 0.0
size = lattice.size
grid = lattice.get_grid()
# Bragg-like Order: rewards alignment with periodic lattice sites.
order_metric = 0
for r in range(size):
for c in range(size):
if grid[r][c] == EMPTY:
continue
# Atoms on even/odd intersections contribute to cubic order
if (r + c) % 2 == 0:
order_metric += 1
order_factor = order_metric / n_atoms if n_atoms > 0 else 0.0
# Quadrant Distribution Entropy: measures how evenly atoms are distributed.
half = size // 2
quads = [0, 0, 0, 0]
for r in range(size):
for c in range(size):
if grid[r][c] == EMPTY:
continue
idx = (0 if r < half else 2) + (0 if c < half else 1)
quads[idx] += 1
expected_q = n_atoms / 4.0
q_deviation = sum(abs(q - expected_q) for q in quads) / (4.0 * max(expected_q, 1))
homogeneity = max(1.0 - q_deviation, 0.0)
return round(min(0.55 * order_factor + 0.45 * homogeneity, 1.0), 4)
|