Spaces:
Sleeping
Sleeping
Download environment/physics.py from ArshPathan/material_forge_env: direct link, hf CLI and curl.
- Browser
- Download file 10.6 kB
-
https://huggingface.co/spaces/ArshPathan/material_forge_env/resolve/main/environment/physics.py
- Command line
-
hf download hf://spaces/ArshPathan/material_forge_env/environment/physics.py
-
curl -L -o physics.py https://huggingface.co/spaces/ArshPathan/material_forge_env/resolve/main/environment/physics.py
10.6 kB
| # 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) | |