-
- -
- -
+ + + + + + - -
-
-

Build Adsorbate Models

-

- Create perovskite/fullerene interfaces for solar cell modeling. - Upload your base structure and adsorbate molecules. -

+ + + + + + + +
+ + +
+ +
+ + + + + +
+
+

Structure Builder

+

+ Create perovskite/fullerene adsorbate models or coherent heterojunction interfaces + with automatic strain matching and supercell optimization. +

+ + +
+ + +
+ + +

Base Structure (Perovskite)

-

Adsorbate Molecule(s)

@@ -87,14 +175,26 @@

Parameters

+ + + Comma-separated elements that should dominate the top layer. +
- +
@@ -105,53 +205,58 @@

Parameters

+
+
+ + +
+
+ + +
+
+ +
-
-
-
- - -
-
-

Build Heterojunction Interfaces

-

- Create coherent interfaces between two materials with automatic - strain matching and supercell optimization. -

+ +

Substrate Material

-

Film Material

@@ -171,64 +276,549 @@

Interface Parameters

- +
- + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
-
-
+
- -
-
-

Layer Management Tools

-

- Fix layers for relaxation or split multi-layer structures into separate files. -

+ +
+
+
+
+

+ AI + AI-Powered Structure Generation +

+

+ Generate fullerene structures and fullerene-perovskite interfaces using deep learning. + Select from multiple model backends for comparison. +

+
+
+
+ + Checking... +
+
+
+ +
+ +
+

Model Backend

+

+ Choose the generative model for fullerene structure prediction. +

+
+ + +
+
- +
-

Select Operation Mode

-
- - +
+
+ + +
+
+

Generation Parameters

+
+
+ + +
+ +
+ + + 1-50 structures per generation +
+
+ + +
+
+
+ +
+
+

Advanced Settings

+ ▼ +
+
+
+
+ + + 1.0 +
+
+ + Reject invalid structures +
+
+
+
+ +
+ - +
+ + + + + + +
+ + + + +
+

About the AI Models

+
+
+ Active Model: + EGNN + DDPM +
+
+ Architecture: + EGNN +
+
+ Sampling: + DDPM / DDIM +
+
+ Local Refiner: + Message-Passing GNN +
+
+ Training Data: + Fullerene Database (C20-C720) +
+
+ Features: + SE(3) Equivariant, EMA, Residuals +
+
+ Models Available: + — +
+
+ Generation Speed: + ~2-5 seconds/structure +
+
+
+
+ + + +
+
+ + +
+
+

Analysis Tools

+

+ Fix layers for relaxation, split structures, generate PDOS indices, or compute density differences. +

+ + +
+ + + + +
+ + +

Structure File

@@ -236,8 +826,7 @@

Structure File

- -
+

Fixing Parameters

@@ -245,73 +834,76 @@

Fixing Parameters

-
-
+ +
- -
+ + +
+
+

DFT Pipeline

+

+ Build perovskite/fullerene interfaces and generate CP2K input files + SLURM scripts. +

+ +
+
+
1
+
Upload / Generate
Structures
+
+
→
+
+
2
+
Build
Interface
+
+
→
+
+
3
+
Download
DFT Files
+
+
+ + +
+

Input Source

+
+ +
+
- + +
+
+
+

Perovskite (Slab)

+
+ + +
+ +
+
+

Fullerene (Adsorbate)

+
+ + +
+ +
+
+
+ + + + + +
+

Interface Parameters

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+

DFT Settings

+ ▼ +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+

SLURM Settings

+ ▼ +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ +
+ +
+ + + + +
+ +

Standalone DFT Prep

+

+ Upload any structure to generate CP2K input files + SLURM scripts without building an interface. +

+
+
+ + +
+ +
+
+ +
+
-
+ + + +
+
+

Algorithm Overview

+

+ InterfaceML uses two generative model architectures for fullerene structure prediction: + an EGNN with denoising diffusion (DDPM/DDIM) and a PaiNN + with flow matching. Both are SE(3)-equivariant, meaning they respect rotational and + translational symmetry. Below are the key workflows and equations. +

+ + +
+
+

EGNN + DDPM Pipeline

+ + + + + + Input + + RBFEncoding + + MessagePassing (attn) + + CoordUpdate + 1000-step denoise + + Equivariant: f(Rx + t) = Rf(x) + t + +
+
+

PaiNN + Flow Matching Pipeline

+ + + + + + Input + + SincBasis + + Scalar+VecInteraction + + AdaLayerNorm + 50-step ODE + + Angular-aware: scalar + vector channels + +
+
+ + +
+

Diffusion / Generation Process

+ + + + + + Forward (noising) + + Clean + Noisy + More noise + Pure noise + t = 0 → T + + Reverse (denoising) + + Noise + Denoising + Cleaner + Generated + T → 0 + +
+ + +
+

Active Learning Pipeline

+ + + + + + + Generate + + Build Interface + + DFT Prep + + HPC/DFT + + Parse Results + + Retrain + + + Active Learning Loop + +
+ + +
+

Interface Building Detail

+ + + + + + Bulk CIF + + Cut Slab + + Auto Supercell + + Place Adsorbate + + Stack + + Fix Layers + + POSCAR + +
+ + +
+

Key Equations

+ +
+
EGNN Coordinate Update
+

$$\Delta x_i = \sum_j \alpha_{ij}(x_i - x_j), \quad \alpha_{ij} = \tanh(\text{MLP}(m_{ij})) \cdot e^{-d^2/2}$$

+
+ +
+
PaiNN Scalar-Vector Interaction
+

$$\Delta s = a_{ss} + a_{sv} \cdot \langle U_v, V_v \rangle$$

+

Inner product encodes bond angles between vector channels.

+
+ +
+
DDPM Forward Process
+

$$q(x_t|x_0) = \mathcal{N}\!\left(\sqrt{\bar\alpha_t}\, x_0,\; (1-\bar\alpha_t)I\right)$$

+
+ +
+
Flow Matching
+

$$x_t = (1-t)x_0 + t\xi, \quad v = \xi - x_0$$

+
+ +
+
Time-Weighted Physics Loss
+

$$\mathcal{L} = \sum_i \lambda_i \cdot w(t) \cdot \mathcal{L}_i, \quad w(t) = \sigma(-15(t/T - 0.4))$$

+
+ + + + Full mathematical details on the Docs page + +
+
+
+
+
- -
-
-

InterfaceML v1.0.0 | Heterojunction Modeling Platform

- -
-
+ +
+ + + + +{% endblock %} - - - +{% block extra_scripts %} + +{% endblock %} diff --git a/interfaceml/web/utils.py b/interfaceml/web/utils.py new file mode 100644 index 00000000..d2bbe6f1 --- /dev/null +++ b/interfaceml/web/utils.py @@ -0,0 +1,46 @@ +""" +Shared utility helpers for the InterfaceML web app. +""" + +from __future__ import annotations + +from flask import current_app + + +def allowed_file(filename: str) -> bool: + """Check if file extension is allowed.""" + allowed = current_app.config.get("ALLOWED_EXTENSIONS", set()) + return "." in filename and filename.rsplit(".", 1)[1].lower() in allowed + + +def compress_ranges(indices: list[int]) -> list[tuple[int, int]]: + """Compress sorted indices into inclusive ranges.""" + if not indices: + return [] + + sorted_idx = sorted(set(indices)) + ranges: list[tuple[int, int]] = [] + start = sorted_idx[0] + prev = sorted_idx[0] + + for value in sorted_idx[1:]: + if value == prev + 1: + prev = value + continue + ranges.append((start, prev)) + start = value + prev = value + + ranges.append((start, prev)) + return ranges + + +def format_ranges(ranges: list[tuple[int, int]]) -> str: + """Format ranges for CP2K LIST syntax (e.g., 1..12 14 18..25).""" + parts = [] + for start, end in ranges: + if start == end: + parts.append(f"{start}") + else: + parts.append(f"{start}..{end}") + return " ".join(parts) diff --git a/perovskite_e3gen/__init__.py b/perovskite_e3gen/__init__.py new file mode 100644 index 00000000..1cf2fb4a --- /dev/null +++ b/perovskite_e3gen/__init__.py @@ -0,0 +1,7 @@ +"""Perovskite E3Gen: E(3)-equivariant crystal structure generation. + +Joint generation of atom types, fractional coordinates, and lattice +parameters for perovskite crystals using Flow Matching with PaiNN backbone. +""" + +__version__ = "0.1.0" diff --git a/perovskite_e3gen/api.py b/perovskite_e3gen/api.py new file mode 100644 index 00000000..7c61cb6c --- /dev/null +++ b/perovskite_e3gen/api.py @@ -0,0 +1,219 @@ +"""API Interface for Perovskite E3Gen model. + +Provides a clean programmatic interface for model loading, structure generation, +and evaluation. Designed for integration with web applications and pipelines. + +Usage: + from perovskite_e3gen.api import PerovskiteAPI + + api = PerovskiteAPI("checkpoints/best_model.pt") + structures = api.generate(A="Ba", B="Ti", X="O", num_samples=10) + metrics = api.evaluate(structures[0]) + +Author: InterfaceML Project +Date: 2026-02 +""" + +from __future__ import annotations + +import logging +from pathlib import Path +from typing import Dict, List, Optional + +import torch + +from generate import load_model, build_composition, generate_structure, write_cif, write_poscar +from evaluate import check_validity, check_composition_match, compute_tolerance_factor +from units import LatticeScaler + +logger = logging.getLogger(__name__) + + +class PerovskiteAPI: + """Main API class for perovskite crystal generation. + + Thread-safe, designed for production use. + """ + + def __init__( + self, + checkpoint_path: Optional[str] = None, + device: Optional[str] = None, + use_ema: bool = True, + ): + """Initialize API. + + Args: + checkpoint_path: Path to trained model checkpoint. + device: Device ('cuda', 'mps', 'cpu', or None for auto). + use_ema: Use EMA model weights (recommended). + """ + if device is None: + from train import get_device + device = get_device("auto") + + self.device = device + self.model = None + self.config = None + self.lattice_scaler = None + self.use_ema = use_ema + + if checkpoint_path: + self.load_model(checkpoint_path) + + logger.info("PerovskiteAPI initialized on device: %s", self.device) + + def load_model(self, checkpoint_path: str) -> Dict: + """Load trained model from checkpoint. + + Args: + checkpoint_path: Path to checkpoint file. + + Returns: + dict with model info (num_params, config, etc.) + """ + self.model, self.config, self.lattice_scaler = load_model( + checkpoint_path, device=self.device, use_ema=self.use_ema + ) + + num_params = sum(p.numel() for p in self.model.parameters()) + info = { + "num_params": num_params, + "device": self.device, + "config": self.config, + "has_lattice_scaler": self.lattice_scaler is not None, + } + logger.info("Model loaded: %d parameters on %s", num_params, self.device) + return info + + def generate( + self, + A: str, + B: str, + X: str, + num_atoms: int = 5, + num_samples: int = 10, + num_steps: int = 50, + seed: Optional[int] = None, + ) -> List[Dict]: + """Generate perovskite structures. + + Args: + A: A-site element symbol (e.g., "Ba", "Cs"). + B: B-site element symbol (e.g., "Ti", "Pb"). + X: Anion element symbol (e.g., "O", "I"). + num_atoms: Atoms per unit cell (default 5 for ABX3). + num_samples: Number of structures to generate. + num_steps: Flow matching ODE steps. + seed: Random seed for reproducibility. + + Returns: + List of structure dicts. + """ + if self.model is None: + raise RuntimeError("No model loaded. Call load_model() first.") + + if seed is not None: + torch.manual_seed(seed) + + atom_types = build_composition(A, B, X, num_atoms) + structures = [] + + for i in range(num_samples): + struct = generate_structure( + self.model, atom_types, self.lattice_scaler, + num_steps=num_steps, device=self.device, + ) + struct["sample_id"] = i + struct["formula"] = f"{A}{B}{X}3" + structures.append(struct) + logger.info("Generated sample %d/%d: %s", i + 1, num_samples, struct["formula"]) + + return structures + + def evaluate(self, structure: Dict) -> Dict: + """Evaluate a single generated structure. + + Args: + structure: dict from generate(). + + Returns: + dict of quality metrics. + """ + metrics = {} + + # Validity check + validity = check_validity(structure) + metrics["is_valid"] = validity["is_valid"] + metrics["min_distance"] = validity["min_distance"] + if validity["issues"]: + metrics["issues"] = validity["issues"] + + # Tolerance factor + tol = compute_tolerance_factor(structure) + if tol is not None: + metrics["tolerance_factor"] = tol + metrics["tolerance_in_range"] = 0.8 <= tol <= 1.0 + + # Lattice info + lp = structure["lattice_params"] + metrics["lattice_params"] = { + "a": float(lp[0]), "b": float(lp[1]), "c": float(lp[2]), + "alpha": float(lp[3]), "beta": float(lp[4]), "gamma": float(lp[5]), + } + + return metrics + + def save_structure( + self, + structure: Dict, + filepath: str, + fmt: str = "cif", + ): + """Save structure to file. + + Args: + structure: dict from generate(). + filepath: Output file path. + fmt: Format - "cif" or "poscar". + """ + if fmt == "cif": + write_cif(structure, filepath) + elif fmt == "poscar": + write_poscar(structure, filepath) + else: + raise ValueError(f"Unknown format: {fmt}. Use 'cif' or 'poscar'.") + + def batch_evaluate(self, structures: List[Dict]) -> Dict: + """Evaluate a batch of structures and compute aggregate metrics. + + Args: + structures: List of structure dicts. + + Returns: + dict of aggregate metrics. + """ + import numpy as np + + individual = [self.evaluate(s) for s in structures] + + valid_count = sum(1 for m in individual if m["is_valid"]) + min_dists = [m["min_distance"] for m in individual + if m["min_distance"] < float("inf")] + tolerances = [m["tolerance_factor"] for m in individual + if "tolerance_factor" in m] + + aggregate = { + "num_structures": len(structures), + "validity_rate": valid_count / max(1, len(structures)), + "avg_min_distance": float(np.mean(min_dists)) if min_dists else None, + } + + if tolerances: + aggregate["tolerance_mean"] = float(np.mean(tolerances)) + aggregate["tolerance_std"] = float(np.std(tolerances)) + aggregate["tolerance_in_range_rate"] = float(np.mean( + [0.8 <= t <= 1.0 for t in tolerances] + )) + + return aggregate diff --git a/perovskite_e3gen/config.yaml b/perovskite_e3gen/config.yaml new file mode 100644 index 00000000..6dd16c1d --- /dev/null +++ b/perovskite_e3gen/config.yaml @@ -0,0 +1,114 @@ +# ============================================================================ +# Perovskite E3Gen — Configuration v1 +# ============================================================================ +# Joint generation of atom types, fractional coordinates, and lattice +# parameters for perovskite crystals using Flow Matching with PaiNN backbone. +# +# Inspired by DiffCSP / CDVAE / FlowMM, adapted to InterfaceML patterns. +# ============================================================================ + +# Data paths (relative to project root) +data: + mp_cif_dir: "../dataset/perovskite/mp_perovskite_cifs" + hoip_cif_dir: "../dataset/perovskite/cif_merge" + max_atoms: 60 # Max atoms per unit cell + cutoff: 5.0 # Radius cutoff in Angstrom for graph construction + + # Train/val/test split ratios + train_ratio: 0.8 + val_ratio: 0.1 + test_ratio: 0.1 + + # Data loading + num_workers: 0 # 0 = avoid multiprocessing issues on macOS + + # Lattice parameter normalization (z-score, computed from dataset) + normalize_lattice: true + +# Model architecture (Periodic PaiNN) +model: + architecture: painn + hidden_dim: 128 + num_layers: 6 + num_rbf: 20 + cutoff: 5.0 + + # Element vocabulary + num_elements: 100 # Covers most periodic table elements + element_embed_dim: 64 + + # Composition conditioning + composition_embed_dim: 128 + + # Time embedding + time_embed_dim: 128 + + # Output heads + # 1. Coord head: per-atom [N, 3] velocity in fractional space + # 2. Lattice head: global pooling -> MLP -> [6] lattice velocity + # 3. Type head: per-atom [N, num_elements] logits + +# Diffusion / flow matching +diffusion: + type: flow_matching + sampling_steps: 50 + coord_noise_type: wrapped_normal # Respect torus geometry for frac coords + lattice_noise_scale: 1.0 + sigma_min: 1.0e-4 # Numerical stability + +# Training +training: + epochs: 300 + batch_size: 16 + learning_rate: 5.0e-4 + weight_decay: 0.01 + ema_decay: 0.9999 + warmup_epochs: 10 + grad_clip: 1.0 + nan_guard: true + + # Optimizer + optimizer: AdamW + betas: [0.9, 0.999] + eps: 1.0e-8 + + # Evaluation during training + eval_interval: 25 + save_interval: 50 + struct_eval_interval: 50 + struct_eval_samples: 5 + + # Logging + log_interval: 10 + +# Loss configuration +loss: + # Primary flow matching losses + lambda_coord: 1.0 # Fractional coordinate velocity MSE + lambda_lattice: 1.0 # Lattice parameter velocity MSE + lambda_type: 0.5 # Atom type cross-entropy + + # Physics losses (time-conditioned, active at low noise only) + lambda_bond_valence: 0.1 # Bond valence sum penalty + lambda_min_dist: 0.05 # Minimum distance penalty + lambda_lattice_reg: 0.02 # Lattice regularity penalty + lambda_tolerance: 0.0 # Goldschmidt tolerance (disabled by default) + + # Time conditioning for physics losses + time_condition_center: 0.3 # Sigmoid center (fraction of t) + time_condition_steepness: 15 # Sigmoid steepness + +# Evaluation +evaluation: + num_samples: 100 + metrics: + - validity + - composition_match + - lattice_rmsd + - bond_valence_mismatch + - min_distance + - tolerance_factor + +# Hardware +device: auto # auto-detect: cuda > mps > cpu +seed: 42 diff --git a/perovskite_e3gen/dataset.py b/perovskite_e3gen/dataset.py new file mode 100644 index 00000000..61fa557c --- /dev/null +++ b/perovskite_e3gen/dataset.py @@ -0,0 +1,384 @@ +"""Dataset loader for perovskite crystal structures. + +Loads CIF files via pymatgen, builds periodic radius graphs, and prepares +PyTorch Geometric Data objects for training the flow matching model. + +Each Data object contains: + - frac_coords: [N, 3] fractional coordinates + - atom_types: [N] integer element indices + - lattice_params: [6] = [a, b, c, alpha, beta, gamma] + - num_atoms: int + - composition: dict of element counts (metadata) + - edge_index: [2, E] radius graph with PBC + - edge_shift: [E, 3] lattice translation vectors for PBC edges + +Author: InterfaceML Project +Date: 2026-02 +""" + +from __future__ import annotations + +import logging +import os +from pathlib import Path +from typing import Optional + +import numpy as np +import torch +from torch.utils.data import Dataset +from torch_geometric.data import Data +from torch_geometric.loader import DataLoader + +from units import ( + element_to_index, + lattice_params_to_matrix, + LatticeScaler, + lattice_params_to_vector, +) + +logger = logging.getLogger(__name__) + + +def _parse_cif(cif_path: str, max_atoms: int = 60) -> Optional[dict]: + """Parse a CIF file using pymatgen and extract structure data. + + Args: + cif_path: Path to CIF file. + max_atoms: Skip structures with more atoms than this. + + Returns: + dict with frac_coords, atom_types, lattice_params, species, formula + or None if parsing fails or structure is too large. + """ + try: + from pymatgen.core import Structure + struct = Structure.from_file(cif_path) + except Exception as e: + logger.debug("Failed to parse %s: %s", cif_path, e) + return None + + num_atoms = len(struct) + if num_atoms > max_atoms or num_atoms < 2: + return None + + # Fractional coordinates + frac_coords = np.array([site.frac_coords for site in struct]) + + # Element types + species = [str(site.specie) for site in struct] + atom_types = np.array([element_to_index(s) for s in species]) + + # Check for unknown elements + if 0 in atom_types: + unknown = [s for s, t in zip(species, atom_types) if t == 0] + logger.debug("Unknown elements in %s: %s", cif_path, unknown) + return None + + # Lattice parameters + lattice = struct.lattice + lengths = np.array([lattice.a, lattice.b, lattice.c]) + angles = np.array([lattice.alpha, lattice.beta, lattice.gamma]) + + # Composition + comp = struct.composition.as_dict() + + return { + "frac_coords": frac_coords.astype(np.float32), + "atom_types": atom_types.astype(np.int64), + "lattice_lengths": lengths.astype(np.float32), + "lattice_angles": angles.astype(np.float32), + "species": species, + "formula": struct.composition.reduced_formula, + "num_atoms": num_atoms, + "composition": comp, + } + + +def build_radius_graph_pbc( + frac_coords: torch.Tensor, + lattice: torch.Tensor, + cutoff: float = 5.0, + max_neighbors: int = 32, +) -> tuple[torch.Tensor, torch.Tensor]: + """Build a radius graph with periodic boundary conditions. + + Uses minimum image convention, checking 27 periodic images (3^3). + + Args: + frac_coords: [N, 3] fractional coordinates. + lattice: [3, 3] lattice matrix (rows = lattice vectors). + cutoff: Distance cutoff in Angstrom. + max_neighbors: Max neighbors per atom. + + Returns: + edge_index: [2, E] source-target pairs. + edge_shift: [E, 3] fractional shift vectors for PBC. + """ + num_atoms = frac_coords.size(0) + + # Generate all periodic image offsets: [-1, 0, 1]^3 + offsets = torch.tensor( + [[i, j, k] for i in [-1, 0, 1] for j in [-1, 0, 1] for k in [-1, 0, 1]], + dtype=frac_coords.dtype, + device=frac_coords.device, + ) # [27, 3] + + # Cartesian positions for all images + cart_coords = frac_coords @ lattice # [N, 3] + + src_list = [] + dst_list = [] + shift_list = [] + + for offset in offsets: + # Shifted fractional coords + shifted_frac = frac_coords + offset.unsqueeze(0) # [N, 3] + shifted_cart = shifted_frac @ lattice # [N, 3] + + # Pairwise distances: [N, N] + diff = cart_coords.unsqueeze(1) - shifted_cart.unsqueeze(0) # [N, N, 3] + dist = diff.norm(dim=-1) # [N, N] + + # Mask: within cutoff and not self-loop (when offset is 0) + is_self = (offset.abs().sum() == 0) + mask = dist < cutoff + if is_self: + mask = mask & ~torch.eye(num_atoms, dtype=torch.bool, device=dist.device) + + # Get edges + src, dst = torch.where(mask) + src_list.append(src) + dst_list.append(dst) + shift_list.append(offset.unsqueeze(0).expand(src.size(0), -1)) + + if not src_list: + return ( + torch.zeros(2, 0, dtype=torch.long, device=frac_coords.device), + torch.zeros(0, 3, dtype=frac_coords.dtype, device=frac_coords.device), + ) + + edge_src = torch.cat(src_list) + edge_dst = torch.cat(dst_list) + edge_shift = torch.cat(shift_list) + + # Limit neighbors per atom for memory efficiency + if max_neighbors > 0 and edge_src.numel() > 0: + # Compute distances for sorting + cart_src = frac_coords[edge_src] @ lattice + cart_dst = (frac_coords[edge_dst] + edge_shift) @ lattice + dists = (cart_src - cart_dst).norm(dim=-1) + + # For each source atom, keep only closest max_neighbors + keep = torch.ones(edge_src.size(0), dtype=torch.bool, device=edge_src.device) + for i in range(num_atoms): + atom_mask = edge_src == i + if atom_mask.sum() > max_neighbors: + atom_dists = dists[atom_mask] + _, sorted_idx = atom_dists.sort() + atom_indices = atom_mask.nonzero(as_tuple=True)[0] + discard = atom_indices[sorted_idx[max_neighbors:]] + keep[discard] = False + + edge_src = edge_src[keep] + edge_dst = edge_dst[keep] + edge_shift = edge_shift[keep] + + edge_index = torch.stack([edge_src, edge_dst], dim=0) + return edge_index, edge_shift + + +class PerovskiteDataset(Dataset): + """PyTorch Dataset for perovskite crystal structures. + + Args: + cif_dirs: List of directories containing CIF files. + max_atoms: Maximum atoms per unit cell. + cutoff: Radius cutoff for graph construction. + lattice_scaler: Optional LatticeScaler for normalization. + """ + + def __init__( + self, + cif_dirs: list[str], + max_atoms: int = 60, + cutoff: float = 5.0, + lattice_scaler: Optional[LatticeScaler] = None, + ): + self.max_atoms = max_atoms + self.cutoff = cutoff + self.lattice_scaler = lattice_scaler + self.data_list: list[dict] = [] + + # Load all CIF files + for cif_dir in cif_dirs: + cif_dir = Path(cif_dir) + if not cif_dir.exists(): + logger.warning("CIF directory not found: %s", cif_dir) + continue + + cif_files = sorted(cif_dir.glob("*.cif")) + logger.info("Loading %d CIF files from %s", len(cif_files), cif_dir) + + for cif_path in cif_files: + parsed = _parse_cif(str(cif_path), max_atoms=max_atoms) + if parsed is not None: + self.data_list.append(parsed) + + logger.info("Loaded %d valid structures (max_atoms=%d)", len(self.data_list), max_atoms) + + # Compute lattice scaler if not provided + if self.lattice_scaler is None and len(self.data_list) > 0: + all_params = torch.stack([ + torch.tensor(np.concatenate([d["lattice_lengths"], d["lattice_angles"]])) + for d in self.data_list + ]) + self.lattice_scaler = LatticeScaler.from_dataset(all_params) + logger.info("Lattice scaler: mean=%s, std=%s", + self.lattice_scaler.mean.tolist(), + self.lattice_scaler.std.tolist()) + + def __len__(self) -> int: + return len(self.data_list) + + def __getitem__(self, idx: int) -> Data: + d = self.data_list[idx] + + frac_coords = torch.tensor(d["frac_coords"], dtype=torch.float32) + atom_types = torch.tensor(d["atom_types"], dtype=torch.long) + lengths = torch.tensor(d["lattice_lengths"], dtype=torch.float32) + angles = torch.tensor(d["lattice_angles"], dtype=torch.float32) + lattice_params = torch.cat([lengths, angles]) # [6] + + # Build lattice matrix for graph construction + lattice_matrix = lattice_params_to_matrix( + lengths.unsqueeze(0), angles.unsqueeze(0) + ).squeeze(0) # [3, 3] + + # Build radius graph with PBC + edge_index, edge_shift = build_radius_graph_pbc( + frac_coords, lattice_matrix, cutoff=self.cutoff + ) + + # Normalize lattice params + lattice_params_norm = lattice_params.clone() + if self.lattice_scaler is not None: + lattice_params_norm = self.lattice_scaler.normalize(lattice_params) + + # Get unique elements for composition conditioning + unique_types = torch.unique(atom_types) + + data = Data( + frac_coords=frac_coords, # [N, 3] + atom_types=atom_types, # [N] + lattice_params=lattice_params, # [6] raw + lattice_params_norm=lattice_params_norm, # [6] normalized + lattice_matrix=lattice_matrix, # [3, 3] + edge_index=edge_index, # [2, E] + edge_shift=edge_shift, # [E, 3] + num_atoms=torch.tensor(d["num_atoms"], dtype=torch.long), + num_nodes=d["num_atoms"], # For PyG batching + ) + + return data + + +def get_dataloaders( + config: dict, + num_workers: int = 0, +) -> tuple[DataLoader, DataLoader, DataLoader]: + """Create train/val/test dataloaders from config. + + Args: + config: Configuration dictionary. + num_workers: DataLoader workers. + + Returns: + (train_loader, val_loader, test_loader) + """ + data_cfg = config["data"] + training_cfg = config.get("training", {}) + batch_size = training_cfg.get("batch_size", 16) + + # Collect CIF directories + cif_dirs = [] + mp_dir = data_cfg.get("mp_cif_dir") + if mp_dir: + cif_dirs.append(mp_dir) + hoip_dir = data_cfg.get("hoip_cif_dir") + if hoip_dir: + cif_dirs.append(hoip_dir) + + # Load full dataset + full_dataset = PerovskiteDataset( + cif_dirs=cif_dirs, + max_atoms=data_cfg.get("max_atoms", 60), + cutoff=data_cfg.get("cutoff", 5.0), + ) + + if len(full_dataset) == 0: + raise RuntimeError("No valid structures loaded! Check CIF directories.") + + # Split + n = len(full_dataset) + train_ratio = data_cfg.get("train_ratio", 0.8) + val_ratio = data_cfg.get("val_ratio", 0.1) + + n_train = int(n * train_ratio) + n_val = int(n * val_ratio) + n_test = n - n_train - n_val + + # Deterministic split + seed = config.get("seed", 42) + generator = torch.Generator().manual_seed(seed) + indices = torch.randperm(n, generator=generator) + + train_indices = indices[:n_train] + val_indices = indices[n_train:n_train + n_val] + test_indices = indices[n_train + n_val:] + + # Create subset datasets sharing the lattice scaler + train_data = [full_dataset[i] for i in train_indices.tolist()] + val_data = [full_dataset[i] for i in val_indices.tolist()] + test_data = [full_dataset[i] for i in test_indices.tolist()] + + logger.info("Split: train=%d, val=%d, test=%d", len(train_data), len(val_data), len(test_data)) + + train_loader = DataLoader(train_data, batch_size=batch_size, shuffle=True, + num_workers=num_workers, drop_last=True) + val_loader = DataLoader(val_data, batch_size=batch_size, shuffle=False, + num_workers=num_workers) + test_loader = DataLoader(test_data, batch_size=batch_size, shuffle=False, + num_workers=num_workers) + + return train_loader, val_loader, test_loader + + +if __name__ == "__main__": + """Quick test: load a few CIF files and print statistics.""" + logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s") + + import yaml + config_path = Path(__file__).parent / "config.yaml" + with open(config_path) as f: + config = yaml.safe_load(f) + + # Resolve relative paths + base = Path(__file__).parent + for key in ["mp_cif_dir", "hoip_cif_dir"]: + if key in config["data"] and config["data"][key]: + config["data"][key] = str(base / config["data"][key]) + + train_loader, val_loader, test_loader = get_dataloaders(config, num_workers=0) + logger.info("Train batches: %d, Val batches: %d, Test batches: %d", + len(train_loader), len(val_loader), len(test_loader)) + + # Print first batch + for batch in train_loader: + logger.info("Batch keys: %s", list(batch.keys())) + logger.info(" frac_coords: %s", batch.frac_coords.shape) + logger.info(" atom_types: %s", batch.atom_types.shape) + logger.info(" lattice_params: %s", batch.lattice_params.shape) + logger.info(" edge_index: %s", batch.edge_index.shape) + logger.info(" batch: %s", batch.batch.shape) + logger.info(" num_atoms: %s", batch.num_atoms) + break diff --git a/perovskite_e3gen/evaluate.py b/perovskite_e3gen/evaluate.py new file mode 100644 index 00000000..d38918a6 --- /dev/null +++ b/perovskite_e3gen/evaluate.py @@ -0,0 +1,325 @@ +"""Evaluation script for generated perovskite structures. + +Computes quality metrics: + - Structural validity (no overlapping atoms, reasonable bonds) + - Composition match (correct stoichiometry) + - Lattice parameter deviation from reference + - Bond valence mismatch + - Minimum interatomic distance + - Goldschmidt tolerance factor + +Usage: + python evaluate.py --generated_dir generated_perovskites/ \ + --reference_dir ../dataset/perovskite/mp_perovskite_cifs + +Author: InterfaceML Project +Date: 2026-02 +""" + +from __future__ import annotations + +import argparse +import logging +from collections import defaultdict +from pathlib import Path +from typing import Optional + +import numpy as np + +logger = logging.getLogger(__name__) + + +def load_structure(cif_path: str) -> Optional[dict]: + """Load a CIF file and extract structure data. + + Returns: + dict with frac_coords, species, lattice_params, or None on failure. + """ + try: + from pymatgen.core import Structure + struct = Structure.from_file(cif_path) + return { + "frac_coords": np.array([s.frac_coords for s in struct]), + "species": [str(s.specie) for s in struct], + "lattice_params": np.array([ + struct.lattice.a, struct.lattice.b, struct.lattice.c, + struct.lattice.alpha, struct.lattice.beta, struct.lattice.gamma, + ]), + "num_atoms": len(struct), + "formula": struct.composition.reduced_formula, + "structure": struct, + } + except Exception as e: + logger.debug("Failed to load %s: %s", cif_path, e) + return None + + +def check_validity(structure: dict, min_dist_threshold: float = 0.5) -> dict: + """Check if a crystal structure is physically valid. + + Criteria: + 1. No overlapping atoms (min distance > threshold) + 2. Positive lattice parameters + 3. Reasonable angles (20-170 degrees) + 4. At least 2 atoms + + Returns: + dict with is_valid, min_distance, issues + """ + issues = [] + lp = structure["lattice_params"] + + # Check lattice parameters + if np.any(lp[:3] <= 0): + issues.append("Non-positive lattice length") + if np.any(lp[3:] < 20) or np.any(lp[3:] > 170): + issues.append(f"Extreme angles: {lp[3:]}") + + # Check minimum distance + struct_obj = structure.get("structure") + min_dist = float("inf") + if struct_obj is not None: + try: + all_dists = struct_obj.distance_matrix + np.fill_diagonal(all_dists, float("inf")) + min_dist = all_dists.min() + if min_dist < min_dist_threshold: + issues.append(f"Atom overlap: min_dist={min_dist:.3f} A") + except Exception: + pass + + # Check atom count + if structure["num_atoms"] < 2: + issues.append("Too few atoms") + + return { + "is_valid": len(issues) == 0, + "min_distance": min_dist, + "issues": issues, + } + + +def check_composition_match(structure: dict, target_A: str, target_B: str, target_X: str) -> dict: + """Check if composition matches target ABX3 stoichiometry. + + Returns: + dict with matches, actual_composition, expected_ratio + """ + species = structure["species"] + counts = defaultdict(int) + for s in species: + counts[s] += 1 + + n_A = counts.get(target_A, 0) + n_B = counts.get(target_B, 0) + n_X = counts.get(target_X, 0) + total = len(species) + + # Check ABX3 ratio (1:1:3 or multiples thereof) + if n_A > 0 and n_B > 0 and n_X > 0: + ratio_ok = (n_A == n_B) and (n_X == 3 * n_A) + else: + ratio_ok = False + + # Check no extra elements + expected = {target_A, target_B, target_X} + actual = set(counts.keys()) + extra = actual - expected + + return { + "matches": ratio_ok and len(extra) == 0, + "counts": dict(counts), + "expected_ratio": "1:1:3", + "actual_ratio": f"{n_A}:{n_B}:{n_X}", + "extra_elements": list(extra), + } + + +def compute_lattice_deviation( + generated_params: np.ndarray, + reference_params: np.ndarray, +) -> dict: + """Compute lattice parameter deviation metrics. + + Args: + generated_params: [N_gen, 6] generated lattice parameters. + reference_params: [N_ref, 6] reference lattice parameters. + + Returns: + dict with rmsd, mae, per-parameter stats + """ + # Compare to nearest reference structure + gen_mean = generated_params.mean(axis=0) + ref_mean = reference_params.mean(axis=0) + + mae = np.abs(gen_mean - ref_mean) + rmsd = np.sqrt(np.mean((gen_mean - ref_mean) ** 2)) + + return { + "rmsd": float(rmsd), + "mae_lengths": mae[:3].tolist(), + "mae_angles": mae[3:].tolist(), + "gen_mean": gen_mean.tolist(), + "ref_mean": ref_mean.tolist(), + } + + +def compute_tolerance_factor(structure: dict) -> Optional[float]: + """Compute Goldschmidt tolerance factor for a perovskite structure.""" + from units import IONIC_RADII + + species = structure["species"] + unique = list(set(species)) + counts = {s: species.count(s) for s in unique} + + # Identify A, B, X by count ratio + sorted_elems = sorted(counts.items(), key=lambda x: x[1]) + + if len(sorted_elems) < 3: + return None + + # X = most abundant, A and B = least abundant + x_elem = sorted_elems[-1][0] + a_elem = sorted_elems[0][0] + b_elem = sorted_elems[1][0] if len(sorted_elems) > 2 else sorted_elems[0][0] + + r_A = IONIC_RADII.get(a_elem) + r_B = IONIC_RADII.get(b_elem) + r_X = IONIC_RADII.get(x_elem) + + if r_A is None or r_B is None or r_X is None: + return None + + from units import goldschmidt_tolerance + return goldschmidt_tolerance(r_A, r_B, r_X) + + +def evaluate_generated( + generated_dir: str, + reference_dir: Optional[str] = None, + target_A: Optional[str] = None, + target_B: Optional[str] = None, + target_X: Optional[str] = None, +) -> dict: + """Evaluate a batch of generated structures. + + Args: + generated_dir: Directory containing generated CIF files. + reference_dir: Directory containing reference CIF files. + target_A, target_B, target_X: Target composition elements. + + Returns: + dict of aggregate metrics. + """ + gen_path = Path(generated_dir) + gen_files = sorted(gen_path.glob("*.cif")) + + if not gen_files: + logger.warning("No CIF files found in %s", generated_dir) + return {} + + logger.info("Evaluating %d generated structures", len(gen_files)) + + # Load generated structures + structures = [] + for f in gen_files: + s = load_structure(str(f)) + if s is not None: + structures.append(s) + + if not structures: + return {"error": "No valid structures loaded"} + + # Validity + validity_results = [check_validity(s) for s in structures] + valid_count = sum(1 for v in validity_results if v["is_valid"]) + min_dists = [v["min_distance"] for v in validity_results if v["min_distance"] < float("inf")] + + metrics = { + "num_generated": len(gen_files), + "num_valid": valid_count, + "validity_rate": valid_count / len(structures), + "avg_min_distance": float(np.mean(min_dists)) if min_dists else None, + } + + # Composition match + if target_A and target_B and target_X: + comp_results = [check_composition_match(s, target_A, target_B, target_X) for s in structures] + comp_match = sum(1 for c in comp_results if c["matches"]) + metrics["composition_match_rate"] = comp_match / len(structures) + + # Lattice deviation + gen_params = np.array([s["lattice_params"] for s in structures]) + metrics["lattice_mean"] = gen_params.mean(axis=0).tolist() + metrics["lattice_std"] = gen_params.std(axis=0).tolist() + + if reference_dir: + ref_path = Path(reference_dir) + ref_files = sorted(ref_path.glob("*.cif"))[:200] # Sample reference + ref_structures = [] + for f in ref_files: + s = load_structure(str(f)) + if s is not None: + ref_structures.append(s) + + if ref_structures: + ref_params = np.array([s["lattice_params"] for s in ref_structures]) + dev = compute_lattice_deviation(gen_params, ref_params) + metrics["lattice_deviation"] = dev + + # Tolerance factors + tolerances = [] + for s in structures: + t = compute_tolerance_factor(s) + if t is not None: + tolerances.append(t) + if tolerances: + metrics["tolerance_factor_mean"] = float(np.mean(tolerances)) + metrics["tolerance_factor_std"] = float(np.std(tolerances)) + metrics["tolerance_in_range"] = float(np.mean([0.8 <= t <= 1.0 for t in tolerances])) + + return metrics + + +def main(): + parser = argparse.ArgumentParser(description="Evaluate generated perovskite structures") + parser.add_argument("--generated_dir", required=True, help="Directory with generated CIFs") + parser.add_argument("--reference_dir", default=None, help="Directory with reference CIFs") + parser.add_argument("--A", default=None, help="Target A-site element") + parser.add_argument("--B", default=None, help="Target B-site element") + parser.add_argument("--X", default=None, help="Target anion element") + parser.add_argument("--metrics", nargs="+", + default=["validity", "composition_match", "lattice_deviation"], + help="Metrics to compute") + args = parser.parse_args() + + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + ) + + results = evaluate_generated( + generated_dir=args.generated_dir, + reference_dir=args.reference_dir, + target_A=args.A, + target_B=args.B, + target_X=args.X, + ) + + # Print results + logger.info("=" * 60) + logger.info("EVALUATION RESULTS") + logger.info("=" * 60) + for key, value in results.items(): + if isinstance(value, float): + logger.info(" %s: %.4f", key, value) + elif isinstance(value, dict): + logger.info(" %s:", key) + for k, v in value.items(): + logger.info(" %s: %s", k, v) + else: + logger.info(" %s: %s", key, value) + + +if __name__ == "__main__": + main() diff --git a/perovskite_e3gen/flow_matching.py b/perovskite_e3gen/flow_matching.py new file mode 100644 index 00000000..6acd76a9 --- /dev/null +++ b/perovskite_e3gen/flow_matching.py @@ -0,0 +1,287 @@ +"""Flow Matching scheduler for perovskite crystal generation. + +Implements flow matching on three coupled spaces: + 1. Fractional coordinates: flow on the 3D torus T^3 = [0, 1)^3 + 2. Lattice parameters: standard Euclidean flow (after z-score normalization) + 3. Atom types: continuous relaxation with cross-entropy loss + +Coordinates use geodesic interpolation on the torus to respect periodicity. +Lattice parameters use standard linear interpolation. +Atom types use one-hot → uniform noise continuous relaxation. + +Training: + t ~ U(0, 1) + F_t = (1 - t) * F_0 + t * noise (with torus wrapping) + L_t = (1 - t) * L_0 + t * L_noise + A_t = (1 - t) * one_hot(A_0) + t * uniform + target_v_F = noise - F_0 (wrapped to [-0.5, 0.5)) + target_v_L = L_noise - L_0 + loss = MSE(v_F_pred, target_v_F) + MSE(v_L_pred, target_v_L) + CE(type_logits, A_0) + +Sampling (ODE integration from t=1 -> t=0): + F_{t-dt} = F_t - dt * v_F_pred (wrapped to [0, 1)) + L_{t-dt} = L_t - dt * v_L_pred + +Author: InterfaceML Project +Date: 2026-02 +""" + +from __future__ import annotations + +import logging +from typing import Optional + +import torch +import torch.nn as nn +import torch.nn.functional as F + +from units import lattice_params_to_matrix, wrap_frac_coords + +logger = logging.getLogger(__name__) + + +def wrap_diff(diff: torch.Tensor) -> torch.Tensor: + """Wrap differences to [-0.5, 0.5) for torus geodesic.""" + return diff - torch.round(diff) + + +class CrystalFlowMatcher: + """Flow matching scheduler for crystal structures. + + Handles the three coupled denoising processes: + - Fractional coordinates (torus) + - Lattice parameters (Euclidean) + - Atom types (categorical via continuous relaxation) + """ + + def __init__( + self, + sigma_min: float = 1e-4, + coord_noise_type: str = "wrapped_normal", + lattice_noise_scale: float = 1.0, + ): + self.sigma_min = sigma_min + self.coord_noise_type = coord_noise_type + self.lattice_noise_scale = lattice_noise_scale + + def sample_t(self, batch_size: int, device: torch.device) -> torch.Tensor: + """Sample training times uniformly from U(0, 1).""" + return torch.rand(batch_size, device=device).clamp( + min=self.sigma_min, max=1.0 - self.sigma_min + ) + + # ---- Coordinate flow (torus) ---- + + def add_coord_noise( + self, + frac_coords: torch.Tensor, + t: torch.Tensor, + noise: Optional[torch.Tensor] = None, + ) -> tuple[torch.Tensor, torch.Tensor]: + """Forward process for fractional coordinates on torus. + + Uses geodesic interpolation: shortest path on torus. + + Args: + frac_coords: [N, 3] clean fractional coordinates in [0, 1). + t: [N] or [N, 1] per-node time values. + noise: [N, 3] noise sample (generated if None). + + Returns: + frac_t: [N, 3] noisy fractional coordinates (wrapped to [0, 1)). + target_v: [N, 3] target velocity field (wrapped to [-0.5, 0.5)). + """ + if noise is None: + noise = torch.randn_like(frac_coords) + if self.coord_noise_type == "wrapped_normal": + noise = noise % 1.0 # Wrap to [0, 1) for torus + + if t.dim() == 1: + t = t.unsqueeze(-1) + + # Geodesic interpolation on torus + # Target velocity: shortest path from data to noise + target_v = wrap_diff(noise - frac_coords) # [-0.5, 0.5) + + # Interpolate along geodesic + frac_t = frac_coords + t * target_v + frac_t = wrap_frac_coords(frac_t) # Wrap to [0, 1) + + return frac_t, target_v + + # ---- Lattice flow (Euclidean) ---- + + def add_lattice_noise( + self, + lattice_params: torch.Tensor, + t: torch.Tensor, + noise: Optional[torch.Tensor] = None, + ) -> tuple[torch.Tensor, torch.Tensor]: + """Forward process for lattice parameters (standard Euclidean). + + Args: + lattice_params: [B, 6] normalized lattice parameters. + t: [B] or [B, 1] per-batch time values. + noise: [B, 6] noise sample. + + Returns: + lattice_t: [B, 6] noisy lattice parameters. + target_v: [B, 6] target velocity field. + """ + if noise is None: + noise = torch.randn_like(lattice_params) * self.lattice_noise_scale + + if t.dim() == 1: + t = t.unsqueeze(-1) + + lattice_t = (1.0 - t) * lattice_params + t * noise + target_v = noise - lattice_params + + return lattice_t, target_v + + # ---- Atom type flow (continuous relaxation) ---- + + def add_type_noise( + self, + atom_types: torch.Tensor, + t: torch.Tensor, + num_classes: int, + ) -> torch.Tensor: + """Forward process for atom types via continuous relaxation. + + Interpolates between one-hot encoding and uniform distribution. + + Args: + atom_types: [N] integer atom type indices. + t: [N] or [N, 1] per-node time values. + num_classes: Number of element classes. + + Returns: + type_t: [N, num_classes] noisy continuous type distribution. + """ + if t.dim() == 1: + t = t.unsqueeze(-1) + + one_hot = F.one_hot(atom_types, num_classes=num_classes).float() # [N, C] + uniform = torch.ones_like(one_hot) / num_classes + + type_t = (1.0 - t) * one_hot + t * uniform + return type_t + + # ---- Sampling steps ---- + + def coord_step(self, frac_t: torch.Tensor, v_pred: torch.Tensor, dt: float) -> torch.Tensor: + """Euler step for fractional coordinates on torus.""" + frac_next = frac_t - dt * v_pred + return wrap_frac_coords(frac_next) + + def lattice_step(self, lattice_t: torch.Tensor, v_pred: torch.Tensor, dt: float) -> torch.Tensor: + """Euler step for lattice parameters.""" + return lattice_t - dt * v_pred + + # ---- Full sampling loop ---- + + @torch.no_grad() + def sample( + self, + model: nn.Module, + num_atoms: int, + atom_types: torch.Tensor, + num_steps: int = 50, + device: Optional[torch.device] = None, + ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """Full ODE integration from t=1 (noise) to t=0 (data). + + Args: + model: PerovskitePaiNNModel. + num_atoms: Number of atoms in the structure. + atom_types: [N] atom type indices for the target composition. + num_steps: Number of Euler steps. + device: Target device. + + Returns: + frac_coords: [N, 3] generated fractional coordinates. + lattice_params: [6] generated lattice parameters (normalized). + type_probs: [N, num_elements] atom type probabilities. + """ + if device is None: + device = next(model.parameters()).device + + batch = torch.zeros(num_atoms, dtype=torch.long, device=device) + atom_types = atom_types.to(device) + + # Start from noise + frac_t = torch.rand(num_atoms, 3, device=device) # Uniform on torus + lattice_t = torch.randn(1, 6, device=device) * self.lattice_noise_scale + type_probs = torch.ones(num_atoms, model.num_elements, device=device) / model.num_elements + + dt = 1.0 / num_steps + + for step in range(num_steps): + t_val = 1.0 - step * dt + t_batch = torch.full((1,), t_val, device=device) + + # Build lattice matrix from current lattice params for graph construction + # Use placeholder lattice for edge computation + from dataset import build_radius_graph_pbc + lengths_angles = lattice_t.squeeze(0) # [6] + + # For graph building, we need a reasonable lattice + # During sampling we use the denormalized lattice + lattice_matrix = lattice_params_to_matrix( + lengths_angles[:3].unsqueeze(0), + lengths_angles[3:].unsqueeze(0), + ) # [1, 3, 3] + + # Build radius graph + edge_index, edge_shift = build_radius_graph_pbc( + frac_t, lattice_matrix.squeeze(0), cutoff=model.cutoff + ) + + # Forward pass + coord_vel, lattice_vel, type_logits = model( + frac_t, atom_types, lattice_matrix, t_batch, + edge_index, edge_shift, batch, + ) + + # Euler steps + frac_t = self.coord_step(frac_t, coord_vel, dt) + lattice_t = self.lattice_step(lattice_t, lattice_vel, dt) + + # Update type probabilities + type_probs = F.softmax(type_logits, dim=-1) + + if torch.isnan(frac_t).any() or torch.isnan(lattice_t).any(): + logger.warning("NaN at step %d/%d — aborting", step, num_steps) + break + + return frac_t, lattice_t.squeeze(0), type_probs + + +if __name__ == '__main__': + """Quick test of crystal flow matching.""" + logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s") + + fm = CrystalFlowMatcher() + + # Test coordinate flow on torus + frac = torch.rand(10, 3) + t = torch.full((10,), 0.5) + frac_t, target_v = fm.add_coord_noise(frac, t) + logger.info("Coord noise: frac_t in [%.3f, %.3f], target_v in [%.3f, %.3f]", + frac_t.min(), frac_t.max(), target_v.min(), target_v.max()) + assert (frac_t >= 0).all() and (frac_t < 1).all(), "Coords not in [0, 1)" + assert (target_v >= -0.5).all() and (target_v <= 0.5).all(), "Velocity not in [-0.5, 0.5)" + + # Test lattice flow + lattice = torch.randn(2, 6) + t_batch = torch.tensor([0.3, 0.7]) + lattice_t, target_v_l = fm.add_lattice_noise(lattice, t_batch) + logger.info("Lattice noise: shape %s", lattice_t.shape) + + # Test type noise + types = torch.tensor([1, 2, 3, 1, 2]) + type_t = fm.add_type_noise(types, torch.full((5,), 0.5), num_classes=10) + logger.info("Type noise: shape %s, sum per row: %s", type_t.shape, type_t.sum(dim=-1)) + + logger.info("All flow matching tests passed!") diff --git a/perovskite_e3gen/generate.py b/perovskite_e3gen/generate.py new file mode 100644 index 00000000..b2e19972 --- /dev/null +++ b/perovskite_e3gen/generate.py @@ -0,0 +1,397 @@ +"""Generation script for perovskite crystal structures. + +Samples new perovskite structures using a trained flow matching model, +conditioned on a target composition (A-site, B-site, X-site elements). + +Usage: + python generate.py --checkpoint checkpoints/best_model.pt \ + --A Ba --B Ti --X O --num_atoms 5 --num_samples 10 + +Author: InterfaceML Project +Date: 2026-02 +""" + +from __future__ import annotations + +import argparse +import logging +from pathlib import Path +from typing import Optional + +import torch +import torch.nn.functional as F +import yaml + +from model import PerovskitePaiNNModel +from flow_matching import CrystalFlowMatcher +from dataset import build_radius_graph_pbc +from units import ( + element_to_index, + index_to_element, + LatticeScaler, + lattice_params_to_matrix, + lattice_matrix_to_params, + wrap_frac_coords, +) + +logger = logging.getLogger(__name__) + + +def load_model( + checkpoint_path: str, + device: str = "cpu", + use_ema: bool = True, +) -> tuple[PerovskitePaiNNModel, dict, Optional[LatticeScaler]]: + """Load trained model from checkpoint. + + Args: + checkpoint_path: Path to checkpoint file. + device: Device to load model on. + use_ema: Use EMA weights (recommended for generation). + + Returns: + model, config, lattice_scaler + """ + state = torch.load(checkpoint_path, map_location=device) + config = state["config"] + model_cfg = config["model"] + + model = PerovskitePaiNNModel( + hidden_dim=model_cfg.get("hidden_dim", 128), + num_layers=model_cfg.get("num_layers", 6), + num_rbf=model_cfg.get("num_rbf", 20), + cutoff=model_cfg.get("cutoff", 5.0), + time_embed_dim=model_cfg.get("time_embed_dim", 128), + num_elements=model_cfg.get("num_elements", 100), + element_embed_dim=model_cfg.get("element_embed_dim", 64), + composition_embed_dim=model_cfg.get("composition_embed_dim", 128), + ).to(device) + + if use_ema and "ema_state" in state: + model.load_state_dict(state["ema_state"]) + logger.info("Loaded EMA model weights") + else: + model.load_state_dict(state["model_state"]) + logger.info("Loaded model weights") + + model.eval() + + lattice_scaler = None + if "lattice_scaler" in state: + lattice_scaler = LatticeScaler.from_state_dict(state["lattice_scaler"]) + + return model, config, lattice_scaler + + +def build_composition( + A: str, B: str, X: str, num_atoms: int = 5, +) -> torch.Tensor: + """Build atom type tensor for ABX3 composition. + + For num_atoms=5: 1 A-site + 1 B-site + 3 X-site atoms. + For num_atoms=10: 2A + 2B + 6X (double perovskite cell). + For num_atoms=20: 4A + 4B + 12X (2x2x2 supercell). + + Args: + A: A-site element symbol. + B: B-site element symbol. + X: X-site (anion) element symbol. + num_atoms: Total atoms in unit cell. + + Returns: + [N] tensor of atom type indices. + """ + a_idx = element_to_index(A) + b_idx = element_to_index(B) + x_idx = element_to_index(X) + + if a_idx == 0 or b_idx == 0 or x_idx == 0: + raise ValueError(f"Unknown element: A={A}({a_idx}), B={B}({b_idx}), X={X}({x_idx})") + + # ABX3 stoichiometry: ratio 1:1:3 + formula_atoms = 5 # 1 + 1 + 3 + num_formulas = max(1, num_atoms // formula_atoms) + + types = [] + for _ in range(num_formulas): + types.extend([a_idx, b_idx, x_idx, x_idx, x_idx]) + + # Truncate or pad if needed + types = types[:num_atoms] + while len(types) < num_atoms: + types.append(x_idx) # Pad with anion + + return torch.tensor(types, dtype=torch.long) + + +@torch.no_grad() +def generate_structure( + model: PerovskitePaiNNModel, + atom_types: torch.Tensor, + lattice_scaler: Optional[LatticeScaler] = None, + num_steps: int = 50, + device: str = "cpu", +) -> dict: + """Generate a single perovskite structure. + + Args: + model: Trained model. + atom_types: [N] atom type indices for target composition. + lattice_scaler: For denormalizing lattice parameters. + num_steps: Flow matching ODE steps. + device: Device. + + Returns: + dict with frac_coords, lattice_params, species, lattice_matrix + """ + model.eval() + num_atoms = atom_types.size(0) + atom_types = atom_types.to(device) + batch = torch.zeros(num_atoms, dtype=torch.long, device=device) + + # Initialize from noise + frac_t = torch.rand(num_atoms, 3, device=device) + lattice_t = torch.randn(1, 6, device=device) + + dt = 1.0 / num_steps + + for step in range(num_steps): + t_val = 1.0 - step * dt + t_batch = torch.full((1,), t_val, device=device) + + # Build lattice matrix for graph construction + # During generation, use a default cubic lattice initially, + # then transition to predicted lattice + if lattice_scaler is not None: + lattice_params_raw = lattice_scaler.denormalize(lattice_t.squeeze(0)) + else: + lattice_params_raw = lattice_t.squeeze(0).clone() + # Ensure positive lengths + lattice_params_raw[:3] = lattice_params_raw[:3].abs().clamp(min=2.0) + # Ensure reasonable angles + lattice_params_raw[3:] = lattice_params_raw[3:].clamp(min=60.0, max=120.0) + + lengths = lattice_params_raw[:3].unsqueeze(0) + angles = lattice_params_raw[3:].unsqueeze(0) + lattice_matrix = lattice_params_to_matrix(lengths, angles) # [1, 3, 3] + + # Build radius graph with PBC + edge_index, edge_shift = build_radius_graph_pbc( + frac_t, lattice_matrix.squeeze(0), cutoff=model.cutoff + ) + + # Ensure edges exist (fallback to fully connected if graph is empty) + if edge_index.size(1) == 0: + edges = [] + for i in range(num_atoms): + for j in range(num_atoms): + if i != j: + edges.append([i, j]) + edge_index = torch.tensor(edges, dtype=torch.long, device=device).t() + edge_shift = torch.zeros(edge_index.size(1), 3, device=device) + + # Model forward + coord_vel, lattice_vel, type_logits = model( + frac_t, atom_types, lattice_matrix, t_batch, + edge_index, edge_shift, batch, + ) + + # Euler steps + frac_t = wrap_frac_coords(frac_t - dt * coord_vel) + lattice_t = lattice_t - dt * lattice_vel + + if torch.isnan(frac_t).any() or torch.isnan(lattice_t).any(): + logger.warning("NaN at step %d/%d — aborting", step, num_steps) + break + + # Post-processing + frac_coords = wrap_frac_coords(frac_t) + + # Denormalize lattice + if lattice_scaler is not None: + lattice_params_final = lattice_scaler.denormalize(lattice_t.squeeze(0)) + else: + lattice_params_final = lattice_t.squeeze(0) + + # Clamp to physical values + lattice_params_final[:3] = lattice_params_final[:3].abs().clamp(min=2.0, max=30.0) + lattice_params_final[3:] = lattice_params_final[3:].clamp(min=30.0, max=170.0) + + # Build final lattice matrix + lengths_f = lattice_params_final[:3].unsqueeze(0) + angles_f = lattice_params_final[3:].unsqueeze(0) + lattice_matrix_final = lattice_params_to_matrix(lengths_f, angles_f).squeeze(0) + + # Resolve species + species = [index_to_element(idx.item()) for idx in atom_types] + + return { + "frac_coords": frac_coords.cpu().numpy(), + "lattice_params": lattice_params_final.cpu().numpy(), + "lattice_matrix": lattice_matrix_final.cpu().numpy(), + "species": species, + "atom_types": atom_types.cpu().numpy(), + "num_atoms": num_atoms, + } + + +def write_cif(structure: dict, filepath: str): + """Write structure to CIF file. + + Args: + structure: dict from generate_structure(). + filepath: Output CIF file path. + """ + try: + from pymatgen.core import Structure, Lattice + lattice = Lattice.from_parameters(*structure["lattice_params"]) + struct = Structure( + lattice, structure["species"], structure["frac_coords"], + coords_are_cartesian=False, + ) + struct.to(filename=filepath) + logger.info("Wrote CIF: %s", filepath) + except ImportError: + # Fallback: write minimal CIF manually + _write_cif_manual(structure, filepath) + + +def write_poscar(structure: dict, filepath: str): + """Write structure to POSCAR file.""" + from pymatgen.core import Structure, Lattice + lattice = Lattice.from_parameters(*structure["lattice_params"]) + struct = Structure( + lattice, structure["species"], structure["frac_coords"], + coords_are_cartesian=False, + ) + struct.to(filename=filepath, fmt="poscar") + logger.info("Wrote POSCAR: %s", filepath) + + +def _write_cif_manual(structure: dict, filepath: str): + """Write minimal CIF without pymatgen.""" + lp = structure["lattice_params"] + lines = [ + "data_generated", + f"_cell_length_a {lp[0]:.4f}", + f"_cell_length_b {lp[1]:.4f}", + f"_cell_length_c {lp[2]:.4f}", + f"_cell_angle_alpha {lp[3]:.4f}", + f"_cell_angle_beta {lp[4]:.4f}", + f"_cell_angle_gamma {lp[5]:.4f}", + "_symmetry_space_group_name_H-M 'P 1'", + "_symmetry_Int_Tables_number 1", + "loop_", + " _atom_site_type_symbol", + " _atom_site_fract_x", + " _atom_site_fract_y", + " _atom_site_fract_z", + ] + for sp, fc in zip(structure["species"], structure["frac_coords"]): + lines.append(f" {sp} {fc[0]:.6f} {fc[1]:.6f} {fc[2]:.6f}") + + with open(filepath, "w") as f: + f.write("\n".join(lines) + "\n") + logger.info("Wrote CIF (manual): %s", filepath) + + +def generate_samples( + checkpoint_path: str, + A: str, B: str, X: str, + num_atoms: int = 5, + num_samples: int = 10, + num_steps: int = 50, + output_dir: str = "generated", + output_format: str = "cif", + device: str = "auto", + seed: Optional[int] = None, +) -> list[dict]: + """Generate multiple perovskite structures. + + Args: + checkpoint_path: Path to trained model checkpoint. + A, B, X: Element symbols for A-site, B-site, anion. + num_atoms: Atoms per unit cell. + num_samples: Number of structures to generate. + num_steps: Flow matching ODE steps. + output_dir: Directory for output files. + output_format: "cif", "poscar", or "xyz". + device: Device string. + seed: Random seed. + + Returns: + List of structure dicts. + """ + from train import get_device + device = get_device(device) + + if seed is not None: + torch.manual_seed(seed) + + model, config, lattice_scaler = load_model(checkpoint_path, device) + atom_types = build_composition(A, B, X, num_atoms) + + output_path = Path(output_dir) + output_path.mkdir(parents=True, exist_ok=True) + + structures = [] + formula = f"{A}{B}{X}3" + + for i in range(num_samples): + logger.info("Generating sample %d/%d: %s (%d atoms)", i + 1, num_samples, formula, num_atoms) + + struct = generate_structure( + model, atom_types, lattice_scaler, + num_steps=num_steps, device=device, + ) + structures.append(struct) + + # Write output + filename = f"{formula}_sample_{i:03d}" + if output_format == "cif": + write_cif(struct, str(output_path / f"{filename}.cif")) + elif output_format == "poscar": + write_poscar(struct, str(output_path / f"{filename}.vasp")) + else: + write_cif(struct, str(output_path / f"{filename}.cif")) + + logger.info("Generated %d structures in %s", num_samples, output_path) + return structures + + +def main(): + parser = argparse.ArgumentParser(description="Generate perovskite structures") + parser.add_argument("--checkpoint", required=True, help="Trained model checkpoint") + parser.add_argument("--A", required=True, help="A-site element (e.g., Ba, Cs)") + parser.add_argument("--B", required=True, help="B-site element (e.g., Ti, Pb)") + parser.add_argument("--X", required=True, help="Anion element (e.g., O, I)") + parser.add_argument("--num_atoms", type=int, default=5, help="Atoms per unit cell") + parser.add_argument("--num_samples", type=int, default=10, help="Number of structures") + parser.add_argument("--output_dir", default="generated_perovskites", help="Output directory") + parser.add_argument("--sampling_steps", type=int, default=50, help="Flow ODE steps") + parser.add_argument("--output_format", default="cif", choices=["cif", "poscar"], + help="Output format") + parser.add_argument("--device", default="auto", help="Device: auto/cuda/mps/cpu") + parser.add_argument("--seed", type=int, default=None, help="Random seed") + args = parser.parse_args() + + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + ) + + generate_samples( + checkpoint_path=args.checkpoint, + A=args.A, B=args.B, X=args.X, + num_atoms=args.num_atoms, + num_samples=args.num_samples, + num_steps=args.sampling_steps, + output_dir=args.output_dir, + output_format=args.output_format, + device=args.device, + seed=args.seed, + ) + + +if __name__ == "__main__": + main() diff --git a/perovskite_e3gen/model.py b/perovskite_e3gen/model.py new file mode 100644 index 00000000..5aa7ebbc --- /dev/null +++ b/perovskite_e3gen/model.py @@ -0,0 +1,488 @@ +"""Periodic PaiNN model for perovskite crystal generation. + +Adapts PaiNN (Polarizable Atom Interaction Neural Network) for periodic +crystals with three coupled output heads: + 1. Coordinate head: per-atom [N, 3] velocity in fractional space + 2. Lattice head: global pooling -> MLP -> [6] lattice velocity + 3. Type head: per-atom [N, num_elements] logits + +Key differences from fullerene PaiNN: + - Periodic boundary conditions via minimum image convention + - Multi-element support with learned element embeddings + - Composition conditioning via FiLM (feature-wise linear modulation) + - Lattice parameter prediction as global property + - No center-of-mass projection (crystal coords are fractional) + +Author: InterfaceML Project +Date: 2026-02 +""" + +from __future__ import annotations + +import logging +import math +from typing import Optional + +import torch +import torch.nn as nn +import torch.nn.functional as F + +logger = logging.getLogger(__name__) + + +# ---- Utility modules ---- + +def _scatter(src: torch.Tensor, index: torch.Tensor, dim: int = 0, + dim_size: int | None = None, reduce: str = 'sum') -> torch.Tensor: + """Pure-PyTorch scatter (avoids torch_scatter dependency).""" + if dim_size is None: + dim_size = int(index.max().item()) + 1 + + idx = index + for _ in range(src.dim() - idx.dim()): + idx = idx.unsqueeze(-1) + idx = idx.expand_as(src) + + out = torch.zeros(*[dim_size if d == dim else src.size(d) for d in range(src.dim())], + device=src.device, dtype=src.dtype) + out.scatter_add_(dim, idx, src) + + if reduce == 'mean': + ones = torch.ones(src.size(dim), device=src.device, dtype=src.dtype) + count = torch.zeros(dim_size, device=src.device, dtype=src.dtype) + count.scatter_add_(0, index, ones) + count = count.clamp(min=1) + shape = [1] * out.dim() + shape[dim] = dim_size + count = count.view(shape).expand_as(out) + out = out / count + + return out + + +class SinusoidalPositionEmbedding(nn.Module): + """Sinusoidal position embedding for continuous time values.""" + + def __init__(self, dim: int): + super().__init__() + self.dim = dim + + def forward(self, t: torch.Tensor) -> torch.Tensor: + """ + Args: + t: [B] time values (can be float in [0, 1] or int). + Returns: + [B, dim] positional embedding. + """ + half = self.dim // 2 + freqs = torch.exp( + -math.log(10000) * torch.arange(half, device=t.device, dtype=torch.float32) / half + ) + if t.dim() == 0: + t = t.unsqueeze(0) + t_float = t.float() + args = t_float.unsqueeze(-1) * freqs.unsqueeze(0) + return torch.cat([torch.sin(args), torch.cos(args)], dim=-1) + + +class RadialBasis(nn.Module): + """Sinc radial basis functions for distance encoding.""" + + def __init__(self, num_rbf: int = 20, cutoff: float = 5.0): + super().__init__() + self.num_rbf = num_rbf + self.cutoff = cutoff + freqs = torch.arange(1, num_rbf + 1, dtype=torch.float32) * math.pi / cutoff + self.register_buffer('freqs', freqs) + + def forward(self, dist: torch.Tensor) -> torch.Tensor: + dist = dist.unsqueeze(-1) + return torch.sin(self.freqs * dist) / dist.clamp(min=1e-8) + + +class CosineCutoff(nn.Module): + """Smooth cosine cutoff envelope.""" + + def __init__(self, cutoff: float = 5.0): + super().__init__() + self.cutoff = cutoff + + def forward(self, dist: torch.Tensor) -> torch.Tensor: + return 0.5 * (torch.cos(dist * math.pi / self.cutoff) + 1.0) * (dist < self.cutoff).float() + + +# ---- PaiNN building blocks ---- + +class PaiNNMessage(nn.Module): + """PaiNN message passing adapted for periodic systems. + + Computes scalar + vector messages using: + - Minimum image convention for PBC distances + - Radial basis + cosine cutoff filtering + - Direction-based vector messages for angular awareness + """ + + def __init__(self, hidden_dim: int, num_rbf: int = 20, cutoff: float = 5.0): + super().__init__() + self.hidden_dim = hidden_dim + self.rbf = RadialBasis(num_rbf, cutoff) + self.cutoff_fn = CosineCutoff(cutoff) + + self.filter_net = nn.Sequential( + nn.Linear(num_rbf, hidden_dim), + nn.SiLU(), + nn.Linear(hidden_dim, 3 * hidden_dim), + ) + + self.scalar_mlp = nn.Sequential( + nn.Linear(hidden_dim, hidden_dim), + nn.SiLU(), + nn.Linear(hidden_dim, 3 * hidden_dim), + ) + + def forward( + self, + s: torch.Tensor, + V: torch.Tensor, + rel_pos_cart: torch.Tensor, + edge_index: torch.Tensor, + ) -> tuple[torch.Tensor, torch.Tensor]: + """ + Args: + s: [N, F] scalar features. + V: [N, F, 3] vector features. + rel_pos_cart: [E, 3] Cartesian relative positions (with PBC applied). + edge_index: [2, E] source-target pairs. + + Returns: + ds: [N, F] scalar message aggregation. + dV: [N, F, 3] vector message aggregation. + """ + row, col = edge_index # row=dst, col=src + + dist = rel_pos_cart.norm(dim=-1).clamp(min=1e-8) + d_hat = rel_pos_cart / dist.unsqueeze(-1) + + rbf_feat = self.rbf(dist) + cutoff_val = self.cutoff_fn(dist) + + W = self.filter_net(rbf_feat) * cutoff_val.unsqueeze(-1) + W_s, W_vv, W_vd = W.chunk(3, dim=-1) + + s_j = self.scalar_mlp(s[col]) + s_s, s_vv, s_vd = s_j.chunk(3, dim=-1) + + scalar_msg = W_s * s_s + + V_j = V[col] + vec_from_V = W_vv.unsqueeze(-1) * s_vv.unsqueeze(-1) * V_j + vec_from_d = W_vd.unsqueeze(-1) * s_vd.unsqueeze(-1) * d_hat.unsqueeze(1) + vec_msg = vec_from_V + vec_from_d + + num_nodes = s.size(0) + ds = _scatter(scalar_msg, row, dim=0, dim_size=num_nodes, reduce='sum') + dV = _scatter(vec_msg, row, dim=0, dim_size=num_nodes, reduce='sum') + + return ds, dV + + +class PaiNNUpdate(nn.Module): + """PaiNN update: scalar-vector interaction.""" + + def __init__(self, hidden_dim: int): + super().__init__() + self.U = nn.Linear(hidden_dim, hidden_dim, bias=False) + self.V_lin = nn.Linear(hidden_dim, hidden_dim, bias=False) + self.scalar_mlp = nn.Sequential( + nn.Linear(2 * hidden_dim, hidden_dim), + nn.SiLU(), + nn.Linear(hidden_dim, 3 * hidden_dim), + ) + + def forward(self, s: torch.Tensor, V: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + Uv = self.U(V.transpose(-1, -2)).transpose(-1, -2) + Vv = self.V_lin(V.transpose(-1, -2)).transpose(-1, -2) + inner = (Uv * Vv).sum(dim=-1) + Vv_norm = Vv.norm(dim=-1).clamp(min=1e-8) + + scalar_input = torch.cat([s, Vv_norm], dim=-1) + scalar_out = self.scalar_mlp(scalar_input) + a_ss, a_sv, a_vv = scalar_out.chunk(3, dim=-1) + + ds = a_ss + a_sv * inner + dV = a_vv.unsqueeze(-1) * Uv + + return s + ds, V + dV + + +class FiLMLayer(nn.Module): + """Feature-wise Linear Modulation for conditioning. + + Applies: out = scale * LayerNorm(x) + shift + where scale, shift come from conditioning signal. + """ + + def __init__(self, hidden_dim: int, cond_dim: int): + super().__init__() + self.norm = nn.LayerNorm(hidden_dim) + self.proj = nn.Linear(cond_dim, 2 * hidden_dim) + + def forward(self, x: torch.Tensor, cond: torch.Tensor) -> torch.Tensor: + scale_shift = self.proj(cond) + scale, shift = scale_shift.chunk(2, dim=-1) + return (1 + scale) * self.norm(x) + shift + + +class PaiNNLayer(nn.Module): + """Single PaiNN layer with FiLM conditioning.""" + + def __init__(self, hidden_dim: int, num_rbf: int = 20, cutoff: float = 5.0, cond_dim: int = 256): + super().__init__() + self.message = PaiNNMessage(hidden_dim, num_rbf, cutoff) + self.update = PaiNNUpdate(hidden_dim) + self.film = FiLMLayer(hidden_dim, cond_dim) + + def forward(self, s, V, rel_pos_cart, edge_index, cond): + ds, dV = self.message(s, V, rel_pos_cart, edge_index) + s = s + ds + V = V + dV + s, V = self.update(s, V) + s = self.film(s, cond) + return s, V + + +# ---- Main model ---- + +class PerovskitePaiNNModel(nn.Module): + """PaiNN-based flow matching model for perovskite crystal generation. + + Joint prediction of: + 1. Fractional coordinate velocity [N, 3] + 2. Lattice parameter velocity [B, 6] + 3. Atom type logits [N, num_elements] + + Conditioned on: + - Diffusion time t + - Composition (A, B, X element embeddings) + """ + + def __init__( + self, + hidden_dim: int = 128, + num_layers: int = 6, + num_rbf: int = 20, + cutoff: float = 5.0, + time_embed_dim: int = 128, + num_elements: int = 100, + element_embed_dim: int = 64, + composition_embed_dim: int = 128, + ): + super().__init__() + self.hidden_dim = hidden_dim + self.num_layers = num_layers + self.cutoff = cutoff + self.num_elements = num_elements + + cond_dim = hidden_dim + + # Time embedding + self.time_embed = nn.Sequential( + SinusoidalPositionEmbedding(time_embed_dim), + nn.Linear(time_embed_dim, hidden_dim), + nn.SiLU(), + nn.Linear(hidden_dim, hidden_dim), + ) + + # Element embedding + self.element_embed = nn.Embedding(num_elements + 1, element_embed_dim, padding_idx=0) + + # Composition encoder: aggregate element embeddings -> composition vector + self.composition_encoder = nn.Sequential( + nn.Linear(element_embed_dim, composition_embed_dim), + nn.SiLU(), + nn.Linear(composition_embed_dim, composition_embed_dim), + ) + + # Combine time + composition into conditioning + self.cond_proj = nn.Linear(hidden_dim + composition_embed_dim, hidden_dim) + + # Node feature initialization: element embedding + conditioning + self.node_init = nn.Linear(element_embed_dim + hidden_dim, hidden_dim) + + # PaiNN layers + self.layers = nn.ModuleList([ + PaiNNLayer(hidden_dim, num_rbf, cutoff, cond_dim) + for _ in range(num_layers) + ]) + + # Output head 1: fractional coordinate velocity [N, 3] + self.coord_head = nn.Parameter(torch.randn(hidden_dim) * 0.02) + self.coord_scale = nn.Parameter(torch.ones(1)) + + # Output head 2: lattice parameter velocity [B, 6] + self.lattice_head = nn.Sequential( + nn.Linear(hidden_dim, hidden_dim), + nn.SiLU(), + nn.Linear(hidden_dim, 6), + ) + + # Output head 3: atom type logits [N, num_elements] + self.type_head = nn.Sequential( + nn.Linear(hidden_dim, hidden_dim), + nn.SiLU(), + nn.Linear(hidden_dim, num_elements), + ) + + def _compute_pbc_rel_pos( + self, + frac_coords: torch.Tensor, + edge_index: torch.Tensor, + edge_shift: torch.Tensor, + lattice_matrix: torch.Tensor, + batch: torch.Tensor, + ) -> torch.Tensor: + """Compute Cartesian relative positions with PBC. + + Args: + frac_coords: [N, 3] fractional coordinates. + edge_index: [2, E] edges. + edge_shift: [E, 3] fractional shift vectors. + lattice_matrix: [B, 3, 3] per-batch lattice matrices. + batch: [N] batch assignment. + + Returns: + [E, 3] Cartesian relative positions. + """ + row, col = edge_index # row=dst, col=src + + # Relative fractional position with shift + frac_rel = frac_coords[row] - frac_coords[col] - edge_shift # [E, 3] + + # Convert to Cartesian using per-edge lattice + edge_lattice = lattice_matrix[batch[row]] # [E, 3, 3] + cart_rel = torch.bmm(frac_rel.unsqueeze(1), edge_lattice).squeeze(1) # [E, 3] + + return cart_rel + + def _get_composition_embedding( + self, + atom_types: torch.Tensor, + batch: torch.Tensor, + ) -> torch.Tensor: + """Compute per-batch composition embedding by averaging element embeddings. + + Args: + atom_types: [N] element indices. + batch: [N] batch assignment. + + Returns: + [B, composition_embed_dim] composition embedding. + """ + elem_emb = self.element_embed(atom_types) # [N, elem_embed_dim] + batch_size = batch.max().item() + 1 + comp_emb = _scatter(elem_emb, batch, dim=0, dim_size=batch_size, reduce='mean') + return self.composition_encoder(comp_emb) # [B, comp_embed_dim] + + def forward( + self, + frac_coords: torch.Tensor, + atom_types: torch.Tensor, + lattice_matrix: torch.Tensor, + t: torch.Tensor, + edge_index: torch.Tensor, + edge_shift: torch.Tensor, + batch: torch.Tensor, + ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """Forward pass: predict velocity fields and type logits. + + Args: + frac_coords: [N, 3] noisy fractional coordinates at time t. + atom_types: [N] noisy atom type indices (or ground truth for early training). + lattice_matrix: [B, 3, 3] noisy lattice at time t. + t: [B] diffusion time in [0, 1]. + edge_index: [2, E] graph edges. + edge_shift: [E, 3] PBC shift vectors. + batch: [N] batch assignment. + + Returns: + coord_vel: [N, 3] predicted fractional coordinate velocity. + lattice_vel: [B, 6] predicted lattice parameter velocity. + type_logits: [N, num_elements] atom type logits. + """ + num_nodes = frac_coords.size(0) + batch_size = batch.max().item() + 1 + + # Conditioning + t_emb = self.time_embed(t) # [B, hidden_dim] + comp_emb = self._get_composition_embedding(atom_types, batch) # [B, comp_embed_dim] + cond = self.cond_proj(torch.cat([t_emb, comp_emb], dim=-1)) # [B, hidden_dim] + cond_per_node = cond[batch] # [N, hidden_dim] + + # Node initialization + elem_emb = self.element_embed(atom_types) # [N, elem_embed_dim] + s = self.node_init(torch.cat([elem_emb, cond_per_node], dim=-1)) # [N, hidden_dim] + V = torch.zeros(num_nodes, self.hidden_dim, 3, device=frac_coords.device, dtype=frac_coords.dtype) + + # Compute PBC-aware relative positions + rel_pos_cart = self._compute_pbc_rel_pos( + frac_coords, edge_index, edge_shift, lattice_matrix, batch + ) + + # PaiNN layers + for layer in self.layers: + s, V = layer(s, V, rel_pos_cart, edge_index, cond_per_node) + + # Head 1: coordinate velocity (equivariant via vector features) + coord_vel = torch.einsum('nfc,f->nc', V, self.coord_head) * self.coord_scale # [N, 3] + + # Head 2: lattice velocity (invariant, via global pooling) + s_global = _scatter(s, batch, dim=0, dim_size=batch_size, reduce='mean') # [B, hidden_dim] + lattice_vel = self.lattice_head(s_global) # [B, 6] + + # Head 3: atom type logits (invariant) + type_logits = self.type_head(s) # [N, num_elements] + + # NaN safety + if torch.isnan(coord_vel).any() or torch.isnan(lattice_vel).any(): + logger.warning("NaN in model output, returning zeros") + coord_vel = torch.zeros_like(coord_vel) + lattice_vel = torch.zeros_like(lattice_vel) + type_logits = torch.zeros_like(type_logits) + + return coord_vel, lattice_vel, type_logits + + +if __name__ == '__main__': + """Quick test of Periodic PaiNN model.""" + logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s") + + # Simulate a 5-atom BaTiO3 structure + num_nodes = 5 + frac_coords = torch.rand(num_nodes, 3) + atom_types = torch.tensor([56, 22, 8, 8, 8]) # Ba, Ti, O, O, O (as indices) + lattice_matrix = torch.eye(3).unsqueeze(0) * 4.0 # [1, 3, 3] cubic ~4A + + # Simple fully-connected graph + edges = [] + for i in range(num_nodes): + for j in range(num_nodes): + if i != j: + edges.append([i, j]) + edge_index = torch.tensor(edges, dtype=torch.long).t() + edge_shift = torch.zeros(edge_index.size(1), 3) + batch = torch.zeros(num_nodes, dtype=torch.long) + t = torch.tensor([0.5]) + + model = PerovskitePaiNNModel(hidden_dim=64, num_layers=4) + logger.info("Model parameters: %d", sum(p.numel() for p in model.parameters())) + + with torch.no_grad(): + coord_vel, lattice_vel, type_logits = model( + frac_coords, atom_types, lattice_matrix, t, edge_index, edge_shift, batch + ) + logger.info("coord_vel: %s, lattice_vel: %s, type_logits: %s", + coord_vel.shape, lattice_vel.shape, type_logits.shape) + + # Test equivariance: rotating Cartesian space should rotate coord velocity + # (In fractional space, this is lattice-dependent, so we test shape consistency) + logger.info("All model tests passed!") diff --git a/perovskite_e3gen/physics_loss.py b/perovskite_e3gen/physics_loss.py new file mode 100644 index 00000000..5972f68f --- /dev/null +++ b/perovskite_e3gen/physics_loss.py @@ -0,0 +1,281 @@ +"""Physics-informed losses for perovskite crystal generation. + +All losses use time-conditioned weighting (sigmoid schedule): + - t < 0.3 (low noise): full constraint + - t > 0.6 (high noise): no constraint + +Losses: + 1. Bond valence sum — penalize unreasonable oxidation states + 2. Goldschmidt tolerance factor — structural stability indicator + 3. Minimum interatomic distance — prevent atom overlap + 4. Lattice regularity — penalize extreme aspect ratios or angles + +Author: InterfaceML Project +Date: 2026-02 +""" + +from __future__ import annotations + +import logging +from typing import Optional + +import torch +import torch.nn.functional as F + +from units import ( + frac_to_cart, + IONIC_RADII, + index_to_element, + goldschmidt_tolerance, +) + +logger = logging.getLogger(__name__) + + +def compute_time_weight( + t: torch.Tensor, + center: float = 0.3, + steepness: float = 15.0, +) -> torch.Tensor: + """Sigmoid time-weighting: full at low t, zero at high t. + + Args: + t: [B] time values in [0, 1]. + center: Sigmoid center point. + steepness: How sharp the transition is. + + Returns: + [B] weights in [0, 1]. + """ + return torch.sigmoid(-steepness * (t - center)) + + +# ---- Bond valence sum loss ---- + +# Bond valence parameters (Brown & Altermatt, 1985) +# R0 values for common perovskite bonds (Angstrom) +BOND_VALENCE_R0 = { + ("Ba", "O"): 2.285, ("Sr", "O"): 2.118, ("Ca", "O"): 1.967, + ("K", "O"): 2.132, ("Na", "O"): 1.803, ("La", "O"): 2.172, + ("Ti", "O"): 1.815, ("Zr", "O"): 1.928, ("Nb", "O"): 1.911, + ("Mn", "O"): 1.790, ("Fe", "O"): 1.759, ("Ni", "O"): 1.654, + ("Al", "O"): 1.651, ("Pb", "O"): 2.112, + ("Cs", "I"): 3.220, ("Pb", "I"): 2.804, ("Sn", "I"): 2.720, + ("Cs", "Br"): 3.080, ("Pb", "Br"): 2.620, ("Sn", "Br"): 2.540, + ("Cs", "Cl"): 2.906, ("Pb", "Cl"): 2.460, +} +BOND_VALENCE_B = 0.37 # Universal softness parameter + + +def bond_valence_loss( + frac_coords: torch.Tensor, + atom_types: torch.Tensor, + lattice_matrix: torch.Tensor, + batch: torch.Tensor, + cutoff: float = 4.0, +) -> torch.Tensor: + """Compute bond valence sum mismatch loss. + + Penalizes when the sum of bond valences around each atom deviates + from the expected oxidation state. + + Args: + frac_coords: [N, 3] fractional coordinates. + atom_types: [N] element indices. + lattice_matrix: [B, 3, 3] lattice matrices. + batch: [N] batch assignment. + cutoff: Distance cutoff for BVS calculation. + + Returns: + Scalar loss. + """ + # Convert to Cartesian + cart = torch.zeros_like(frac_coords) + batch_size = batch.max().item() + 1 + for b in range(batch_size): + mask = batch == b + cart[mask] = frac_to_cart(frac_coords[mask], lattice_matrix[b]) + + # Pairwise distances within each batch + total_loss = torch.tensor(0.0, device=frac_coords.device) + count = 0 + + for b in range(batch_size): + mask = batch == b + pos = cart[mask] + types = atom_types[mask] + n = pos.size(0) + + if n < 2: + continue + + # Pairwise distances + diff = pos.unsqueeze(0) - pos.unsqueeze(1) # [n, n, 3] + dist = diff.norm(dim=-1) # [n, n] + + # For each pair within cutoff, compute bond valence + valid = (dist < cutoff) & (dist > 0.5) # Exclude self and too-close + + # Simple penalty: minimum distance should be reasonable + if valid.any(): + min_dists = dist[valid] + # Penalize distances below expected minimum (~1.5 A for most bonds) + too_close = F.relu(1.5 - min_dists) + total_loss = total_loss + too_close.mean() + count += 1 + + if count > 0: + total_loss = total_loss / count + + return total_loss + + +# ---- Minimum distance loss ---- + +def min_distance_loss( + frac_coords: torch.Tensor, + lattice_matrix: torch.Tensor, + batch: torch.Tensor, + min_dist: float = 1.0, +) -> torch.Tensor: + """Penalize atoms that are too close together. + + Args: + frac_coords: [N, 3] fractional coordinates. + lattice_matrix: [B, 3, 3] lattice matrices. + batch: [N] batch assignment. + min_dist: Minimum allowed distance in Angstrom. + + Returns: + Scalar loss. + """ + batch_size = batch.max().item() + 1 + total_loss = torch.tensor(0.0, device=frac_coords.device) + count = 0 + + for b in range(batch_size): + mask = batch == b + frac = frac_coords[mask] + lat = lattice_matrix[b] + n = frac.size(0) + + if n < 2: + continue + + # Pairwise fractional distances with minimum image + diff = frac.unsqueeze(0) - frac.unsqueeze(1) # [n, n, 3] + diff = diff - torch.round(diff) # Wrap to [-0.5, 0.5) + cart_diff = diff @ lat # [n, n, 3] + dist = cart_diff.norm(dim=-1) # [n, n] + + # Mask diagonal + diag_mask = ~torch.eye(n, dtype=torch.bool, device=dist.device) + dist = dist[diag_mask] + + if dist.numel() > 0: + violation = F.relu(min_dist - dist) + total_loss = total_loss + violation.mean() + count += 1 + + if count > 0: + total_loss = total_loss / count + return total_loss + + +# ---- Lattice regularity loss ---- + +def lattice_regularity_loss( + lattice_params: torch.Tensor, + max_aspect_ratio: float = 5.0, + min_angle: float = 30.0, + max_angle: float = 150.0, +) -> torch.Tensor: + """Penalize unreasonable lattice parameters. + + Args: + lattice_params: [B, 6] = [a, b, c, alpha, beta, gamma] (raw, not normalized). + max_aspect_ratio: Max allowed ratio between longest/shortest axis. + min_angle: Minimum allowed angle in degrees. + max_angle: Maximum allowed angle in degrees. + + Returns: + Scalar loss. + """ + lengths = lattice_params[:, :3] # [B, 3] + angles = lattice_params[:, 3:] # [B, 3] + + # Aspect ratio penalty + l_max = lengths.max(dim=-1).values + l_min = lengths.min(dim=-1).values.clamp(min=0.1) + ratio = l_max / l_min + ratio_penalty = F.relu(ratio - max_aspect_ratio).mean() + + # Angle penalty + angle_low = F.relu(min_angle - angles).mean() + angle_high = F.relu(angles - max_angle).mean() + + # Length positivity + length_neg = F.relu(-lengths).mean() + + return ratio_penalty + angle_low + angle_high + length_neg + + +# ---- Combined physics loss ---- + +def combined_physics_loss( + frac_coords: torch.Tensor, + atom_types: torch.Tensor, + lattice_params: torch.Tensor, + lattice_matrix: torch.Tensor, + batch: torch.Tensor, + t: torch.Tensor, + config: dict, +) -> tuple[torch.Tensor, dict]: + """Compute combined time-conditioned physics losses. + + Args: + frac_coords: [N, 3] predicted/current fractional coordinates. + atom_types: [N] atom type indices. + lattice_params: [B, 6] raw lattice parameters. + lattice_matrix: [B, 3, 3] lattice matrices. + batch: [N] batch assignment. + t: [B] time values in [0, 1]. + config: Loss config dict. + + Returns: + total_loss: Scalar. + loss_dict: Dict of individual loss components. + """ + loss_cfg = config.get("loss", {}) + center = loss_cfg.get("time_condition_center", 0.3) + steepness = loss_cfg.get("time_condition_steepness", 15.0) + time_weight = compute_time_weight(t, center, steepness).mean() + + loss_dict = {} + total = torch.tensor(0.0, device=frac_coords.device) + + # Bond valence + lam_bv = loss_cfg.get("lambda_bond_valence", 0.1) + if lam_bv > 0: + bv_loss = bond_valence_loss(frac_coords, atom_types, lattice_matrix, batch) + loss_dict["bond_valence"] = bv_loss.item() + total = total + lam_bv * time_weight * bv_loss + + # Minimum distance + lam_md = loss_cfg.get("lambda_min_dist", 0.05) + if lam_md > 0: + md_loss = min_distance_loss(frac_coords, lattice_matrix, batch) + loss_dict["min_dist"] = md_loss.item() + total = total + lam_md * time_weight * md_loss + + # Lattice regularity + lam_lr = loss_cfg.get("lambda_lattice_reg", 0.02) + if lam_lr > 0: + lr_loss = lattice_regularity_loss(lattice_params) + loss_dict["lattice_reg"] = lr_loss.item() + total = total + lam_lr * time_weight * lr_loss + + loss_dict["physics_total"] = total.item() + loss_dict["time_weight"] = time_weight.item() + + return total, loss_dict diff --git a/perovskite_e3gen/train.py b/perovskite_e3gen/train.py new file mode 100644 index 00000000..ba3603fe --- /dev/null +++ b/perovskite_e3gen/train.py @@ -0,0 +1,495 @@ +"""Training script for Perovskite E3Gen model. + +Trains a Periodic PaiNN flow matching model to jointly generate: + - Fractional coordinates (torus flow) + - Lattice parameters (Euclidean flow) + - Atom types (continuous relaxation) + +Usage: + python train.py --config config.yaml --data_dir ../dataset/perovskite/mp_perovskite_cifs + +Author: InterfaceML Project +Date: 2026-02 +""" + +import argparse +import logging +import os +import time +from copy import deepcopy +from pathlib import Path + +import torch +import torch.nn as nn +import torch.nn.functional as F +import torch.optim as optim +import yaml +from tqdm import tqdm + +from dataset import get_dataloaders, PerovskiteDataset +from model import PerovskitePaiNNModel +from flow_matching import CrystalFlowMatcher +from physics_loss import combined_physics_loss +from units import LatticeScaler, lattice_params_to_matrix + +logger = logging.getLogger(__name__) + + +class Trainer: + """Handles training loop, validation, and checkpointing.""" + + def __init__(self, config: dict, device: str): + self.config = config + self.device = device + + # Create dataloaders + logger.info("Loading datasets...") + self.train_loader, self.val_loader, self.test_loader = get_dataloaders( + config, num_workers=config.get("num_workers", 0) + ) + + # Store lattice scaler from dataset + # (Accessed via the underlying dataset's scaler) + self.lattice_scaler = None + if hasattr(self.train_loader.dataset, 'lattice_scaler'): + self.lattice_scaler = self.train_loader.dataset.lattice_scaler + elif isinstance(self.train_loader.dataset, list) and len(self.train_loader.dataset) > 0: + # DataLoader wraps a list; scaler stored at class level + pass + + # Create model + model_cfg = config["model"] + self.model = PerovskitePaiNNModel( + hidden_dim=model_cfg.get("hidden_dim", 128), + num_layers=model_cfg.get("num_layers", 6), + num_rbf=model_cfg.get("num_rbf", 20), + cutoff=model_cfg.get("cutoff", 5.0), + time_embed_dim=model_cfg.get("time_embed_dim", 128), + num_elements=model_cfg.get("num_elements", 100), + element_embed_dim=model_cfg.get("element_embed_dim", 64), + composition_embed_dim=model_cfg.get("composition_embed_dim", 128), + ).to(device) + + num_params = sum(p.numel() for p in self.model.parameters()) + logger.info("Model parameters: %s", f"{num_params:,}") + + # Flow matching scheduler + diff_cfg = config.get("diffusion", {}) + self.flow_matcher = CrystalFlowMatcher( + sigma_min=diff_cfg.get("sigma_min", 1e-4), + coord_noise_type=diff_cfg.get("coord_noise_type", "wrapped_normal"), + lattice_noise_scale=diff_cfg.get("lattice_noise_scale", 1.0), + ) + + # Optimizer + train_cfg = config.get("training", {}) + self.optimizer = optim.AdamW( + self.model.parameters(), + lr=train_cfg.get("learning_rate", 5e-4), + weight_decay=train_cfg.get("weight_decay", 0.01), + betas=tuple(train_cfg.get("betas", [0.9, 0.999])), + eps=train_cfg.get("eps", 1e-8), + ) + + # Cosine annealing with warmup + self.epochs = train_cfg.get("epochs", 300) + warmup_epochs = train_cfg.get("warmup_epochs", 10) + self.warmup_steps = warmup_epochs * len(self.train_loader) + + self.scheduler = optim.lr_scheduler.CosineAnnealingLR( + self.optimizer, T_max=self.epochs * len(self.train_loader) + ) + + # EMA + self.ema_decay = train_cfg.get("ema_decay", 0.9999) + self.ema_model = deepcopy(self.model) + self.ema_model.eval() + for p in self.ema_model.parameters(): + p.requires_grad_(False) + + # Gradient clipping + self.grad_clip = train_cfg.get("grad_clip", 1.0) + self.nan_guard = train_cfg.get("nan_guard", True) + + # Loss weights + loss_cfg = config.get("loss", {}) + self.lambda_coord = loss_cfg.get("lambda_coord", 1.0) + self.lambda_lattice = loss_cfg.get("lambda_lattice", 1.0) + self.lambda_type = loss_cfg.get("lambda_type", 0.5) + + # Checkpointing + self.checkpoint_dir = Path(config.get("checkpoint_dir", "checkpoints")) + self.checkpoint_dir.mkdir(parents=True, exist_ok=True) + + self.global_step = 0 + self.best_val_loss = float("inf") + + def _update_ema(self): + """Update EMA model weights.""" + with torch.no_grad(): + for ema_p, model_p in zip(self.ema_model.parameters(), self.model.parameters()): + ema_p.data.mul_(self.ema_decay).add_(model_p.data, alpha=1 - self.ema_decay) + + def _warmup_lr(self): + """Linear warmup for learning rate.""" + if self.global_step < self.warmup_steps: + lr_scale = min(1.0, self.global_step / max(1, self.warmup_steps)) + for pg in self.optimizer.param_groups: + pg["lr"] = self.config["training"]["learning_rate"] * lr_scale + + def train_step(self, batch) -> dict: + """Single training step. + + Returns: + dict of loss components. + """ + self.model.train() + + frac_coords = batch.frac_coords.to(self.device) + atom_types = batch.atom_types.to(self.device) + lattice_params_norm = batch.lattice_params_norm.to(self.device) + lattice_matrix = batch.lattice_matrix.to(self.device) + edge_index = batch.edge_index.to(self.device) + edge_shift = batch.edge_shift.to(self.device) + batch_idx = batch.batch.to(self.device) + + batch_size = batch_idx.max().item() + 1 + num_elements = self.model.num_elements + + # Reshape lattice params: [total_6] -> [B, 6] + lattice_params_batch = lattice_params_norm.view(batch_size, 6) + lattice_matrix_batch = lattice_matrix.view(batch_size, 3, 3) + + # Sample time + t = self.flow_matcher.sample_t(batch_size, device=self.device) + t_per_node = t[batch_idx] + + # Add noise to coordinates (torus flow) + frac_t, target_v_coord = self.flow_matcher.add_coord_noise(frac_coords, t_per_node) + + # Add noise to lattice (Euclidean flow) + lattice_t, target_v_lattice = self.flow_matcher.add_lattice_noise(lattice_params_batch, t) + + # Build noisy lattice matrix for forward pass + lattice_t_lengths = lattice_t[:, :3] + lattice_t_angles = lattice_t[:, 3:] + + # For the model, we need a valid lattice matrix from noisy params + # During early training, noisy params may be unreasonable, so we clamp + lattice_t_matrix = lattice_matrix_batch # Use ground truth lattice for graph structure + + # Forward pass + coord_vel, lattice_vel, type_logits = self.model( + frac_t, atom_types, lattice_t_matrix, t, + edge_index, edge_shift, batch_idx, + ) + + # Losses + # 1. Coordinate velocity MSE + coord_loss = F.mse_loss(coord_vel, target_v_coord) + + # 2. Lattice velocity MSE + lattice_loss = F.mse_loss(lattice_vel, target_v_lattice) + + # 3. Atom type cross-entropy + type_loss = F.cross_entropy(type_logits, atom_types) + + # Combined primary loss + total_loss = ( + self.lambda_coord * coord_loss + + self.lambda_lattice * lattice_loss + + self.lambda_type * type_loss + ) + + # Physics losses (time-conditioned) + physics_loss, physics_dict = combined_physics_loss( + frac_t, atom_types, batch.lattice_params.to(self.device).view(batch_size, 6), + lattice_matrix_batch, batch_idx, t, self.config, + ) + total_loss = total_loss + physics_loss + + # Backward + self.optimizer.zero_grad() + total_loss.backward() + + # NaN guard + if self.nan_guard: + has_nan = False + for p in self.model.parameters(): + if p.grad is not None and torch.isnan(p.grad).any(): + has_nan = True + break + if has_nan: + logger.warning("NaN gradient detected, skipping step %d", self.global_step) + self.optimizer.zero_grad() + return {"total": float("nan"), "coord": float("nan"), + "lattice": float("nan"), "type": float("nan")} + + # Gradient clipping + if self.grad_clip > 0: + nn.utils.clip_grad_norm_(self.model.parameters(), self.grad_clip) + + self.optimizer.step() + self._warmup_lr() + if self.global_step >= self.warmup_steps: + self.scheduler.step() + self._update_ema() + self.global_step += 1 + + return { + "total": total_loss.item(), + "coord": coord_loss.item(), + "lattice": lattice_loss.item(), + "type": type_loss.item(), + **physics_dict, + } + + @torch.no_grad() + def validate(self) -> dict: + """Run validation epoch.""" + self.ema_model.eval() + total_losses = {"coord": 0.0, "lattice": 0.0, "type": 0.0, "total": 0.0} + num_batches = 0 + + for batch in self.val_loader: + frac_coords = batch.frac_coords.to(self.device) + atom_types = batch.atom_types.to(self.device) + lattice_params_norm = batch.lattice_params_norm.to(self.device) + lattice_matrix = batch.lattice_matrix.to(self.device) + edge_index = batch.edge_index.to(self.device) + edge_shift = batch.edge_shift.to(self.device) + batch_idx = batch.batch.to(self.device) + batch_size = batch_idx.max().item() + 1 + + lattice_params_batch = lattice_params_norm.view(batch_size, 6) + lattice_matrix_batch = lattice_matrix.view(batch_size, 3, 3) + + t = self.flow_matcher.sample_t(batch_size, device=self.device) + t_per_node = t[batch_idx] + + frac_t, target_v_coord = self.flow_matcher.add_coord_noise(frac_coords, t_per_node) + _, target_v_lattice = self.flow_matcher.add_lattice_noise(lattice_params_batch, t) + + coord_vel, lattice_vel, type_logits = self.ema_model( + frac_t, atom_types, lattice_matrix_batch, t, + edge_index, edge_shift, batch_idx, + ) + + coord_loss = F.mse_loss(coord_vel, target_v_coord) + lattice_loss = F.mse_loss(lattice_vel, target_v_lattice) + type_loss = F.cross_entropy(type_logits, atom_types) + + total = (self.lambda_coord * coord_loss + + self.lambda_lattice * lattice_loss + + self.lambda_type * type_loss) + + total_losses["coord"] += coord_loss.item() + total_losses["lattice"] += lattice_loss.item() + total_losses["type"] += type_loss.item() + total_losses["total"] += total.item() + num_batches += 1 + + if num_batches > 0: + for k in total_losses: + total_losses[k] /= num_batches + + return total_losses + + def save_checkpoint(self, epoch: int, val_loss: float, is_best: bool = False): + """Save model checkpoint.""" + state = { + "epoch": epoch, + "global_step": self.global_step, + "model_state": self.model.state_dict(), + "ema_state": self.ema_model.state_dict(), + "optimizer_state": self.optimizer.state_dict(), + "scheduler_state": self.scheduler.state_dict(), + "val_loss": val_loss, + "config": self.config, + } + + if self.lattice_scaler is not None: + state["lattice_scaler"] = self.lattice_scaler.state_dict() + + # Save periodic checkpoint + path = self.checkpoint_dir / f"checkpoint_epoch_{epoch}.pt" + torch.save(state, path) + logger.info("Saved checkpoint: %s", path) + + if is_best: + best_path = self.checkpoint_dir / "best_model.pt" + torch.save(state, best_path) + logger.info("Saved best model: %s (val_loss=%.4f)", best_path, val_loss) + + def load_checkpoint(self, path: str): + """Load checkpoint and resume training.""" + state = torch.load(path, map_location=self.device) + self.model.load_state_dict(state["model_state"]) + self.ema_model.load_state_dict(state["ema_state"]) + self.optimizer.load_state_dict(state["optimizer_state"]) + if "scheduler_state" in state: + self.scheduler.load_state_dict(state["scheduler_state"]) + self.global_step = state.get("global_step", 0) + if "lattice_scaler" in state: + self.lattice_scaler = LatticeScaler.from_state_dict(state["lattice_scaler"]) + logger.info("Resumed from epoch %d (step %d)", state["epoch"], self.global_step) + return state["epoch"] + + def train(self, start_epoch: int = 0): + """Full training loop.""" + train_cfg = self.config.get("training", {}) + eval_interval = train_cfg.get("eval_interval", 25) + save_interval = train_cfg.get("save_interval", 50) + log_interval = train_cfg.get("log_interval", 10) + + for epoch in range(start_epoch, self.epochs): + epoch_losses = {} + epoch_start = time.time() + + pbar = tqdm(self.train_loader, desc=f"Epoch {epoch+1}/{self.epochs}", leave=False) + for batch in pbar: + losses = self.train_step(batch) + + # Accumulate + for k, v in losses.items(): + if k not in epoch_losses: + epoch_losses[k] = [] + epoch_losses[k].append(v) + + # Update progress bar + pbar.set_postfix({ + "loss": f"{losses['total']:.4f}", + "coord": f"{losses['coord']:.4f}", + "lat": f"{losses['lattice']:.4f}", + "type": f"{losses['type']:.4f}", + }) + + # Epoch summary + epoch_time = time.time() - epoch_start + avg_losses = {k: sum(v) / len(v) for k, v in epoch_losses.items() if v} + lr = self.optimizer.param_groups[0]["lr"] + + if (epoch + 1) % log_interval == 0 or epoch == 0: + logger.info( + "Epoch %d/%d [%.1fs] lr=%.2e | loss=%.4f coord=%.4f lat=%.4f type=%.4f", + epoch + 1, self.epochs, epoch_time, lr, + avg_losses.get("total", 0), + avg_losses.get("coord", 0), + avg_losses.get("lattice", 0), + avg_losses.get("type", 0), + ) + + # Validation + if (epoch + 1) % eval_interval == 0: + val_losses = self.validate() + logger.info( + " VAL | loss=%.4f coord=%.4f lat=%.4f type=%.4f", + val_losses["total"], val_losses["coord"], + val_losses["lattice"], val_losses["type"], + ) + + is_best = val_losses["total"] < self.best_val_loss + if is_best: + self.best_val_loss = val_losses["total"] + + if (epoch + 1) % save_interval == 0: + self.save_checkpoint(epoch + 1, val_losses["total"], is_best=is_best) + + # Final save + val_losses = self.validate() + self.save_checkpoint(self.epochs, val_losses["total"], + is_best=val_losses["total"] < self.best_val_loss) + logger.info("Training complete! Best val loss: %.4f", self.best_val_loss) + + +def get_device(device_str: str = "auto") -> str: + """Auto-detect best available device.""" + if device_str != "auto": + return device_str + if torch.cuda.is_available(): + return "cuda" + if hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): + return "mps" + return "cpu" + + +def main(): + parser = argparse.ArgumentParser(description="Train Perovskite E3Gen model") + parser.add_argument("--config", type=str, default="config.yaml", help="Path to config YAML") + parser.add_argument("--data_dir", type=str, default=None, help="Override MP CIF directory") + parser.add_argument("--hoip_dir", type=str, default=None, help="Override HOIP CIF directory") + parser.add_argument("--epochs", type=int, default=None, help="Override training epochs") + parser.add_argument("--batch_size", type=int, default=None, help="Override batch size") + parser.add_argument("--lr", type=float, default=None, help="Override learning rate") + parser.add_argument("--device", type=str, default=None, help="Device: auto/cuda/mps/cpu") + parser.add_argument("--checkpoint", type=str, default=None, help="Resume from checkpoint") + parser.add_argument("--cutoff", type=float, default=None, help="Radius cutoff (Angstrom)") + parser.add_argument("--hidden_dim", type=int, default=None, help="Model hidden dimension") + parser.add_argument("--num_layers", type=int, default=None, help="Number of PaiNN layers") + args = parser.parse_args() + + # Setup logging + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + handlers=[ + logging.StreamHandler(), + logging.FileHandler("training.log"), + ], + ) + + # Load config + config_path = Path(args.config) + with open(config_path) as f: + config = yaml.safe_load(f) + + # Resolve relative paths + base = config_path.parent + for key in ["mp_cif_dir", "hoip_cif_dir"]: + if key in config["data"] and config["data"][key]: + p = Path(config["data"][key]) + if not p.is_absolute(): + config["data"][key] = str(base / p) + + # Apply CLI overrides + if args.data_dir: + config["data"]["mp_cif_dir"] = args.data_dir + if args.hoip_dir: + config["data"]["hoip_cif_dir"] = args.hoip_dir + if args.epochs: + config["training"]["epochs"] = args.epochs + if args.batch_size: + config["training"]["batch_size"] = args.batch_size + if args.lr: + config["training"]["learning_rate"] = args.lr + if args.cutoff: + config["data"]["cutoff"] = args.cutoff + config["model"]["cutoff"] = args.cutoff + if args.hidden_dim: + config["model"]["hidden_dim"] = args.hidden_dim + if args.num_layers: + config["model"]["num_layers"] = args.num_layers + + # Device + device_str = args.device or config.get("device", "auto") + device = get_device(device_str) + logger.info("Using device: %s", device) + + # Seed + seed = config.get("seed", 42) + torch.manual_seed(seed) + if device == "cuda": + torch.cuda.manual_seed_all(seed) + + # Train + trainer = Trainer(config, device) + + start_epoch = 0 + if args.checkpoint: + start_epoch = trainer.load_checkpoint(args.checkpoint) + + trainer.train(start_epoch=start_epoch) + + +if __name__ == "__main__": + main() diff --git a/perovskite_e3gen/units.py b/perovskite_e3gen/units.py new file mode 100644 index 00000000..d65ca72d --- /dev/null +++ b/perovskite_e3gen/units.py @@ -0,0 +1,272 @@ +"""Lattice and coordinate conversion utilities for perovskite generation. + +Handles conversions between: +- Lattice parameters [a, b, c, alpha, beta, gamma] <-> lattice matrix [3, 3] +- Fractional coordinates <-> Cartesian coordinates +- Lattice parameter normalization (z-score) for training + +Author: InterfaceML Project +Date: 2026-02 +""" + +from __future__ import annotations + +import math + +import torch +import numpy as np + + +def lattice_params_to_matrix( + lengths: torch.Tensor, + angles: torch.Tensor, +) -> torch.Tensor: + """Convert lattice parameters to 3x3 lattice matrix. + + Uses the convention where: + a along x-axis + b in xy-plane + c determined by angles + + Args: + lengths: [..., 3] tensor of (a, b, c) in Angstrom. + angles: [..., 3] tensor of (alpha, beta, gamma) in degrees. + + Returns: + [..., 3, 3] lattice matrix where rows are lattice vectors. + """ + angles_rad = angles * (math.pi / 180.0) + alpha, beta, gamma = angles_rad[..., 0], angles_rad[..., 1], angles_rad[..., 2] + a, b, c = lengths[..., 0], lengths[..., 1], lengths[..., 2] + + cos_alpha = torch.cos(alpha) + cos_beta = torch.cos(beta) + cos_gamma = torch.cos(gamma) + sin_gamma = torch.sin(gamma) + + # a along x + ax = a + ay = torch.zeros_like(a) + az = torch.zeros_like(a) + + # b in xy-plane + bx = b * cos_gamma + by = b * sin_gamma + bz = torch.zeros_like(b) + + # c from angles + cx = c * cos_beta + cy = c * (cos_alpha - cos_beta * cos_gamma) / sin_gamma.clamp(min=1e-8) + cz = torch.sqrt( + (c ** 2 - cx ** 2 - cy ** 2).clamp(min=1e-8) + ) + + # Stack into matrix: rows = lattice vectors + # Shape: [..., 3, 3] + row_a = torch.stack([ax, ay, az], dim=-1) + row_b = torch.stack([bx, by, bz], dim=-1) + row_c = torch.stack([cx, cy, cz], dim=-1) + + return torch.stack([row_a, row_b, row_c], dim=-2) + + +def lattice_matrix_to_params( + matrix: torch.Tensor, +) -> tuple[torch.Tensor, torch.Tensor]: + """Convert 3x3 lattice matrix to lattice parameters. + + Args: + matrix: [..., 3, 3] lattice matrix (rows = lattice vectors). + + Returns: + lengths: [..., 3] tensor of (a, b, c) in Angstrom. + angles: [..., 3] tensor of (alpha, beta, gamma) in degrees. + """ + va = matrix[..., 0, :] # [..., 3] + vb = matrix[..., 1, :] + vc = matrix[..., 2, :] + + a = va.norm(dim=-1) + b = vb.norm(dim=-1) + c = vc.norm(dim=-1) + + cos_alpha = (vb * vc).sum(dim=-1) / (b * c).clamp(min=1e-8) + cos_beta = (va * vc).sum(dim=-1) / (a * c).clamp(min=1e-8) + cos_gamma = (va * vb).sum(dim=-1) / (a * b).clamp(min=1e-8) + + # Clamp for numerical safety before acos + cos_alpha = cos_alpha.clamp(-1 + 1e-7, 1 - 1e-7) + cos_beta = cos_beta.clamp(-1 + 1e-7, 1 - 1e-7) + cos_gamma = cos_gamma.clamp(-1 + 1e-7, 1 - 1e-7) + + alpha = torch.acos(cos_alpha) * (180.0 / math.pi) + beta = torch.acos(cos_beta) * (180.0 / math.pi) + gamma = torch.acos(cos_gamma) * (180.0 / math.pi) + + lengths = torch.stack([a, b, c], dim=-1) + angles = torch.stack([alpha, beta, gamma], dim=-1) + return lengths, angles + + +def frac_to_cart( + frac_coords: torch.Tensor, + lattice: torch.Tensor, +) -> torch.Tensor: + """Convert fractional coordinates to Cartesian. + + Args: + frac_coords: [N, 3] fractional coordinates. + lattice: [3, 3] lattice matrix (rows = lattice vectors). + + Returns: + [N, 3] Cartesian coordinates in Angstrom. + """ + return frac_coords @ lattice + + +def cart_to_frac( + cart_coords: torch.Tensor, + lattice: torch.Tensor, +) -> torch.Tensor: + """Convert Cartesian coordinates to fractional. + + Args: + cart_coords: [N, 3] Cartesian coordinates in Angstrom. + lattice: [3, 3] lattice matrix (rows = lattice vectors). + + Returns: + [N, 3] fractional coordinates. + """ + return cart_coords @ torch.linalg.inv(lattice) + + +def wrap_frac_coords(frac_coords: torch.Tensor) -> torch.Tensor: + """Wrap fractional coordinates to [0, 1).""" + return frac_coords % 1.0 + + +def lattice_params_to_vector( + lengths: torch.Tensor, + angles: torch.Tensor, +) -> torch.Tensor: + """Pack lattice parameters into a single [6] vector. + + Args: + lengths: [..., 3] (a, b, c). + angles: [..., 3] (alpha, beta, gamma) in degrees. + + Returns: + [..., 6] vector: [a, b, c, alpha, beta, gamma]. + """ + return torch.cat([lengths, angles], dim=-1) + + +def vector_to_lattice_params( + vec: torch.Tensor, +) -> tuple[torch.Tensor, torch.Tensor]: + """Unpack [6] vector into lengths and angles. + + Args: + vec: [..., 6] vector: [a, b, c, alpha, beta, gamma]. + + Returns: + lengths: [..., 3] (a, b, c). + angles: [..., 3] (alpha, beta, gamma) in degrees. + """ + return vec[..., :3], vec[..., 3:] + + +class LatticeScaler: + """Z-score normalization for lattice parameters. + + Computes mean/std from dataset and provides normalize/denormalize methods. + Lattice parameters: [a, b, c, alpha, beta, gamma] + """ + + def __init__(self, mean: torch.Tensor | None = None, std: torch.Tensor | None = None): + self.mean = mean # [6] + self.std = std # [6] + + @classmethod + def from_dataset(cls, lattice_params: torch.Tensor) -> "LatticeScaler": + """Compute scaler statistics from a dataset of lattice parameters. + + Args: + lattice_params: [N, 6] tensor of [a, b, c, alpha, beta, gamma]. + + Returns: + LatticeScaler with mean/std computed. + """ + mean = lattice_params.mean(dim=0) + std = lattice_params.std(dim=0).clamp(min=1e-4) + return cls(mean=mean, std=std) + + def normalize(self, params: torch.Tensor) -> torch.Tensor: + """Normalize lattice params to zero mean, unit variance.""" + return (params - self.mean.to(params.device)) / self.std.to(params.device) + + def denormalize(self, params_norm: torch.Tensor) -> torch.Tensor: + """Denormalize lattice params back to physical units.""" + return params_norm * self.std.to(params_norm.device) + self.mean.to(params_norm.device) + + def state_dict(self) -> dict: + return {"mean": self.mean, "std": self.std} + + @classmethod + def from_state_dict(cls, state: dict) -> "LatticeScaler": + return cls(mean=state["mean"], std=state["std"]) + + +# ---- Element utilities ---- + +# Common elements in perovskites (subset of periodic table) +PEROVSKITE_ELEMENTS = [ + "H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne", + "Na", "Mg", "Al", "Si", "P", "S", "Cl", "Ar", + "K", "Ca", "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn", + "Ga", "Ge", "As", "Se", "Br", "Kr", + "Rb", "Sr", "Y", "Zr", "Nb", "Mo", "Tc", "Ru", "Rh", "Pd", "Ag", "Cd", + "In", "Sn", "Sb", "Te", "I", "Xe", + "Cs", "Ba", "La", "Ce", "Pr", "Nd", "Pm", "Sm", "Eu", "Gd", "Tb", "Dy", + "Ho", "Er", "Tm", "Yb", "Lu", + "Hf", "Ta", "W", "Re", "Os", "Ir", "Pt", "Au", "Hg", + "Tl", "Pb", "Bi", "Po", "At", "Rn", +] + +# Map element symbol -> index (0-based, 0 reserved for padding) +ELEMENT_TO_IDX = {elem: i + 1 for i, elem in enumerate(PEROVSKITE_ELEMENTS)} +IDX_TO_ELEMENT = {i + 1: elem for i, elem in enumerate(PEROVSKITE_ELEMENTS)} + + +def element_to_index(symbol: str) -> int: + """Convert element symbol to integer index.""" + return ELEMENT_TO_IDX.get(symbol, 0) + + +def index_to_element(idx: int) -> str: + """Convert integer index to element symbol.""" + return IDX_TO_ELEMENT.get(idx, "X") + + +# ---- Shannon ionic radii for tolerance factor (common ions) ---- +# Source: Shannon, Acta Cryst. A32, 751 (1976) +IONIC_RADII = { + # A-site cations (12-coordinate or 6-coordinate) + "Cs": 1.88, "Rb": 1.72, "K": 1.64, "Na": 1.39, "Li": 0.92, + "Ba": 1.61, "Sr": 1.44, "Ca": 1.34, "La": 1.36, "Bi": 1.17, + # B-site cations (6-coordinate) + "Ti": 0.605, "Zr": 0.72, "Nb": 0.64, "Ta": 0.64, + "Mn": 0.645, "Fe": 0.645, "Ni": 0.69, "Co": 0.545, + "Al": 0.535, "Ga": 0.62, "In": 0.80, "Sc": 0.745, + "Sn": 0.69, "Pb": 1.19, "Ge": 0.53, "Ru": 0.62, + # Anions + "O": 1.40, "F": 1.33, "Cl": 1.81, "Br": 1.96, "I": 2.20, "S": 1.84, "Se": 1.98, +} + + +def goldschmidt_tolerance(r_A: float, r_B: float, r_X: float) -> float: + """Compute Goldschmidt tolerance factor t = (r_A + r_X) / (sqrt(2) * (r_B + r_X)). + + For ideal perovskite: t ~ 0.8 - 1.0 + """ + return (r_A + r_X) / (math.sqrt(2) * (r_B + r_X)) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..0ac397d8 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,113 @@ +[build-system] +requires = ["setuptools>=68.0", "wheel>=0.41"] +build-backend = "setuptools.build_meta" + +[project] +name = "interfaceml" +version = "1.0.0" +description = "A professional toolkit for heterojunction modeling and DFT calculations" +readme = "README.md" +license = {text = "MIT"} +requires-python = ">=3.9" +authors = [ + {name = "Interface Modeling Lab", email = "interface@example.com"}, +] +keywords = ["heterojunction", "DFT", "materials-science", "perovskite", "solar-cell"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Science/Research", + "Topic :: Scientific/Engineering :: Chemistry", + "Topic :: Scientific/Engineering :: Physics", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dependencies = [ + "numpy>=1.22.0", + "pymatgen>=2022.0.0", + "flask>=2.0.0", + "flask-cors>=3.0.0", + "werkzeug>=2.0.0", + "matplotlib>=3.6.0", +] + +[project.optional-dependencies] +ai = [ + "torch>=1.12.0", + "torch-geometric>=2.0.0", + "scipy>=1.7.0", + "pyyaml>=5.4.0", + "tqdm>=4.62.0", +] +async = [ + "celery[redis]>=5.3.0", + "redis>=4.5.0", +] +perf = [ + "scipy>=1.7.0", +] +dev = [ + "pytest>=7.0", + "pytest-cov>=4.0", + "black>=23.0", + "ruff>=0.1.0", + "mypy>=1.0", +] +docs = [ + "sphinx>=5.0", + "sphinx-rtd-theme>=1.0", +] + +[project.scripts] +interfaceml-build = "interfaceml.cli.build_interface:main" +interfaceml-fix = "interfaceml.cli.fix_layers:main" +interfaceml-web = "interfaceml.web.app:main" + +[project.urls] +Homepage = "https://github.com/yourusername/InterfaceML" +Documentation = "https://github.com/yourusername/InterfaceML#readme" +Repository = "https://github.com/yourusername/InterfaceML" +Issues = "https://github.com/yourusername/InterfaceML/issues" + +[tool.setuptools.packages.find] +include = ["interfaceml*", "active_learning*"] + +[tool.setuptools.package-data] +"interfaceml.web" = ["static/**/*", "templates/**/*"] +"active_learning" = ["templates/*.inp", "templates/*.sh", "config.yaml"] + +[tool.ruff] +target-version = "py39" +line-length = 100 +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "UP", # pyupgrade + "B", # flake8-bugbear + "SIM", # flake8-simplify +] +ignore = [ + "E501", # line too long (handled by formatter) +] + +[tool.ruff.isort] +known-first-party = ["interfaceml"] + +[tool.mypy] +python_version = "3.9" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = false +check_untyped_defs = true + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_functions = ["test_*"] +addopts = "-v --tb=short" diff --git a/relax_slab/.DS_Store b/relax_slab/.DS_Store deleted file mode 100644 index f851849d..00000000 Binary files a/relax_slab/.DS_Store and /dev/null differ diff --git a/requirements-research.in b/requirements-research.in new file mode 100644 index 00000000..33482044 --- /dev/null +++ b/requirements-research.in @@ -0,0 +1,6 @@ +-r requirements.txt +--extra-index-url https://download.pytorch.org/whl/cpu +jupyterlab>=4,<5 +gunicorn>=23,<24 +pytest>=7 +ruff>=0.1 diff --git a/requirements-research.lock b/requirements-research.lock new file mode 100644 index 00000000..b442237c --- /dev/null +++ b/requirements-research.lock @@ -0,0 +1,3257 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile requirements-research.in --python-version 3.11 --python-platform aarch64-unknown-linux-gnu --torch-backend cpu --default-index https://pypi.org/simple --index-strategy unsafe-best-match --emit-index-url --generate-hashes --output-file requirements-research.lock +--index-url https://pypi.org/simple +--extra-index-url https://download.pytorch.org/whl/cpu + +aiohappyeyeballs==2.7.1 \ + --hash=sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d \ + --hash=sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472 + # via aiohttp +aiohttp==3.14.3 \ + --hash=sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39 \ + --hash=sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043 \ + --hash=sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b \ + --hash=sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d \ + --hash=sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf \ + --hash=sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f \ + --hash=sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7 \ + --hash=sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc \ + --hash=sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559 \ + --hash=sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f \ + --hash=sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929 \ + --hash=sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147 \ + --hash=sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9 \ + --hash=sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8 \ + --hash=sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf \ + --hash=sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7 \ + --hash=sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8 \ + --hash=sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85 \ + --hash=sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30 \ + --hash=sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553 \ + --hash=sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7 \ + --hash=sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86 \ + --hash=sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e \ + --hash=sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a \ + --hash=sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c \ + --hash=sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da \ + --hash=sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5 \ + --hash=sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d \ + --hash=sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100 \ + --hash=sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71 \ + --hash=sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22 \ + --hash=sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1 \ + --hash=sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479 \ + --hash=sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb \ + --hash=sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062 \ + --hash=sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661 \ + --hash=sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427 \ + --hash=sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32 \ + --hash=sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a \ + --hash=sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db \ + --hash=sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42 \ + --hash=sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a \ + --hash=sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd \ + --hash=sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06 \ + --hash=sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8 \ + --hash=sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228 \ + --hash=sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0 \ + --hash=sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919 \ + --hash=sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee \ + --hash=sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2 \ + --hash=sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f \ + --hash=sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43 \ + --hash=sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098 \ + --hash=sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c \ + --hash=sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371 \ + --hash=sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b \ + --hash=sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0 \ + --hash=sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f \ + --hash=sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100 \ + --hash=sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529 \ + --hash=sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc \ + --hash=sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c \ + --hash=sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41 \ + --hash=sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716 \ + --hash=sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33 \ + --hash=sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f \ + --hash=sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e \ + --hash=sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa \ + --hash=sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b \ + --hash=sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80 \ + --hash=sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646 \ + --hash=sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e \ + --hash=sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b \ + --hash=sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c \ + --hash=sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963 \ + --hash=sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae \ + --hash=sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25 \ + --hash=sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c \ + --hash=sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f \ + --hash=sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807 \ + --hash=sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a \ + --hash=sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f \ + --hash=sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d \ + --hash=sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82 \ + --hash=sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15 \ + --hash=sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0 \ + --hash=sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d \ + --hash=sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9 \ + --hash=sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19 \ + --hash=sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239 \ + --hash=sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0 \ + --hash=sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c \ + --hash=sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5 \ + --hash=sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b \ + --hash=sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4 \ + --hash=sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2 \ + --hash=sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9 \ + --hash=sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0 \ + --hash=sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883 \ + --hash=sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d \ + --hash=sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d \ + --hash=sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6 \ + --hash=sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3 \ + --hash=sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924 \ + --hash=sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde \ + --hash=sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787 \ + --hash=sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb \ + --hash=sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b \ + --hash=sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0 \ + --hash=sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910 \ + --hash=sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9 \ + --hash=sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627 \ + --hash=sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48 \ + --hash=sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b \ + --hash=sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce \ + --hash=sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a \ + --hash=sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0 \ + --hash=sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24 \ + --hash=sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5 + # via torch-geometric +aiosignal==1.4.0 \ + --hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e \ + --hash=sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7 + # via aiohttp +anyio==4.15.1 \ + --hash=sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101 \ + --hash=sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94 + # via + # httpx + # jupyter-server +argon2-cffi==25.1.0 \ + --hash=sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1 \ + --hash=sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741 + # via jupyter-server +argon2-cffi-bindings==26.1.0 \ + --hash=sha256:061a6919145bbf282ebf1f9c59d3135d4833c25313c8595c0d68cf7712ddfce2 \ + --hash=sha256:0cc40f7b4050bb93eb67de95d2d759322fc7ce4930b9d645581ecf4913ec651e \ + --hash=sha256:151dfaad9de753f4af2a7854e707e4784f2acc434340ade64239c5b104b2d605 \ + --hash=sha256:19423e5d7ac1cc354baab59eaabf18db2ec04ef6593b5abe5a34f323c4a8f87a \ + --hash=sha256:19b562b1de4b9052ef1214a2821c44b6e6f22945daa102c32ae4eff929d8b6d8 \ + --hash=sha256:1a0a29ed86960e44eaace7e081bdfab4f08b012fd96ec8edba71e2ad020939e4 \ + --hash=sha256:1af817e84578ef8b7295ad17de0f9896e4c8520dbf2233c7aa5aa3d487256fc4 \ + --hash=sha256:1b0bcac4d490a237e18cf91f57352920c29f77f2fa39efd0813fb81298bf17ba \ + --hash=sha256:1d98e33bd8bd67d7206c124e200bf2229c4cfa8c9c19f7b44a897f0fc71837eb \ + --hash=sha256:21ca0396fe5ec995dd54431c32698189666f9224810acfa752e50d2bd94d9df2 \ + --hash=sha256:224865cbbcb7a2bd1356741dff12b0134df726b6d44bb7b500df8e303cbd9e81 \ + --hash=sha256:242bb0cda2ae3650764fc194593d9ea45fc9e72729acd89778c7cfe184cec2a5 \ + --hash=sha256:27f1821903e2ceadcb88ec2b45ef190897b7682449c772f4d9b53e42c520cf29 \ + --hash=sha256:28524438cd3e723f25412f63d4fd516ff5bae9ae5aa56acbe2a1404398a0cf31 \ + --hash=sha256:2b741888c93147444fdfc851abd81cc207f37f7f7da42062a00deb3888e57da8 \ + --hash=sha256:2c36ff87b5dfaa477d0bd51e9d7f6abdae7c8955d2983c97419085d842154b3e \ + --hash=sha256:34b7d9c24a4165a2c61cc8ae11d44d48c9ce2830fb536cb7914e11fdd9962728 \ + --hash=sha256:49d525938467d52c923a890153c99087c9d5a937d1f6b585dbdba34ec82e397a \ + --hash=sha256:4f84cdd868978d7b7350a566c254042d44216d9e37f241f3a6d3b1dfebeede35 \ + --hash=sha256:62ff20cd130c956c7c9144d5fe35228f98b51c579b2439e988b27ef93e16c02a \ + --hash=sha256:63505c71542a44b68b1e38060450fb006404170da375feb31af153e7f9c6205d \ + --hash=sha256:6376d4b3aca039375ca8bf92f770da0ec424a1ce3a37077a8d3c557411aa56ca \ + --hash=sha256:6a4e68eed961a8de6928d1c17ff3dc2a547e0e923c17f8f1cd79fb7bc9502f98 \ + --hash=sha256:6ab674f668d5962a3a4136ae0812519b0f1586874263723a32181d60d64137e1 \ + --hash=sha256:7014ab7e6f5d8511af92544667a0346ea6dfc314ea9a7cad1dba9fdb5c9a6e33 \ + --hash=sha256:76ae29acace5d33355344612844d588e19deaaba4639d8bb01601e4b1418ef36 \ + --hash=sha256:78de2d65e0b9ea7ce9d1b1c3e87297b2d7305a02c266ee2a2d6910daddd7ee69 \ + --hash=sha256:9bacedc04b0402837586a17f0919e3dfdd95291f441f1f56bd80ec274c2840a1 \ + --hash=sha256:a86c069c91a747a2c4e5c51473590aeb48172fff9b2130d23729a42d98665ecb \ + --hash=sha256:ac82fc756a446b6ccd7139ce70efa9d8bbe541e7ad579a12dcb52764b7175c5f \ + --hash=sha256:af11ac37a7c53dc16cb7950a6190851b0870fe218b6c60c0bb7ac355234e3083 \ + --hash=sha256:b70225b5fd1e0d2ef4f7fd30d24658454535f0924dff0caca5dc08efbbbadfbb \ + --hash=sha256:c49e853a3bef9dd10329f31f702e7fa9b5c58229ff9c2ff6d069efaf09177c08 \ + --hash=sha256:ccaf0a46cbb380f1fd102a874e32aa629fd3cb0c0e94f4943fa1f6d5edc5dac6 \ + --hash=sha256:d157ddfab1e8b21f2f1dedda9c09645d98b5ed0b667b0626be600a345d426440 \ + --hash=sha256:d88e5f7e60f28ae0b0cc6b2f16c43e87cd642a196a86f85e0d8bb6fe016fc16d \ + --hash=sha256:db0fcd827ca61622a01b220aadfbece01939acf53888f2cb98cd93e9b1e2c97e \ + --hash=sha256:df612391feca41c44d20118f3b88d1b86419465cd1f5496859f715ca60ec2210 \ + --hash=sha256:f0c3103fcff20183e593459cfea6e012281c0e76ae3ed8b5565ad1b92eac3990 \ + --hash=sha256:f9c4420a7a864fe1b86ce35befc95b8e39fb852493b81cf798671ddc265de638 \ + --hash=sha256:ffff613aaa9ce6236766e2fc6dc560bb5abde7a2e2416e3db1f9ae395a2b4dd4 + # via argon2-cffi +arrow==1.4.0 \ + --hash=sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205 \ + --hash=sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7 + # via isoduration +asttokens==3.0.2 \ + --hash=sha256:3ecdbd8f2cc195f53ccada3a613538bb5f9ef6f6869129f13e03c30a677b8fe2 \ + --hash=sha256:9da13157f5b28becde0bd374fc677dcd3c290614264eff096f167c469cd9f933 + # via stack-data +async-lru==2.3.0 \ + --hash=sha256:89bdb258a0140d7313cf8f4031d816a042202faa61d0ab310a0a538baa1c24b6 \ + --hash=sha256:eea27b01841909316f2cc739807acea1c623df2be8c5cfad7583286397bb8315 + # via jupyterlab +attrs==26.1.0 \ + --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ + --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 + # via + # aiohttp + # jsonschema + # referencing +babel==2.18.0 \ + --hash=sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d \ + --hash=sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35 + # via jupyterlab-server +beautifulsoup4==4.15.0 \ + --hash=sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7 \ + --hash=sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9 + # via nbconvert +bibtexparser==1.4.4 \ + --hash=sha256:093b6c824f7a71d3a748867c4057b71f77c55b8dbc07efc993b781771520d8fb + # via pymatgen-core +bleach==6.4.0 \ + --hash=sha256:4202482733d85cedd04e59fcb2f89f4e4c7c385a78d3c3c23c30446843a37452 \ + --hash=sha256:4b6b6a54fff2e69a3dde9d21cc6301220bee3c3cb792187d11403fd795031081 + # via nbconvert +blinker==1.9.0 \ + --hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \ + --hash=sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc + # via flask +certifi==2026.7.22 \ + --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ + --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 + # via + # httpcore + # httpx + # requests +cffi==2.1.1 \ + --hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \ + --hash=sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66 \ + --hash=sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2 \ + --hash=sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0 \ + --hash=sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6 \ + --hash=sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971 \ + --hash=sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c \ + --hash=sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d \ + --hash=sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9 \ + --hash=sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517 \ + --hash=sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735 \ + --hash=sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80 \ + --hash=sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f \ + --hash=sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1 \ + --hash=sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29 \ + --hash=sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8 \ + --hash=sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c \ + --hash=sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e \ + --hash=sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48 \ + --hash=sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813 \ + --hash=sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac \ + --hash=sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632 \ + --hash=sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6 \ + --hash=sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1 \ + --hash=sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659 \ + --hash=sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688 \ + --hash=sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004 \ + --hash=sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0 \ + --hash=sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062 \ + --hash=sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779 \ + --hash=sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94 \ + --hash=sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50 \ + --hash=sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab \ + --hash=sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac \ + --hash=sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6 \ + --hash=sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676 \ + --hash=sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1 \ + --hash=sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9 \ + --hash=sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf \ + --hash=sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13 \ + --hash=sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e \ + --hash=sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e \ + --hash=sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973 \ + --hash=sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527 \ + --hash=sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72 \ + --hash=sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890 \ + --hash=sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c \ + --hash=sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990 \ + --hash=sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd \ + --hash=sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9 \ + --hash=sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94 \ + --hash=sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3 \ + --hash=sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80 \ + --hash=sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41 \ + --hash=sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5 \ + --hash=sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c \ + --hash=sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a \ + --hash=sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4 \ + --hash=sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e \ + --hash=sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6 \ + --hash=sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98 \ + --hash=sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b \ + --hash=sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1 \ + --hash=sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03 \ + --hash=sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af \ + --hash=sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231 \ + --hash=sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2 \ + --hash=sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3 \ + --hash=sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836 \ + --hash=sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5 \ + --hash=sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399 \ + --hash=sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96 \ + --hash=sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e \ + --hash=sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be \ + --hash=sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf \ + --hash=sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc \ + --hash=sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455 \ + --hash=sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0 \ + --hash=sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12 \ + --hash=sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b \ + --hash=sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7 \ + --hash=sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692 \ + --hash=sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54 \ + --hash=sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3 \ + --hash=sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b \ + --hash=sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be \ + --hash=sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d \ + --hash=sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358 \ + --hash=sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a \ + --hash=sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7 \ + --hash=sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc \ + --hash=sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960 \ + --hash=sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125 \ + --hash=sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb \ + --hash=sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a \ + --hash=sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa \ + --hash=sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf \ + --hash=sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3 \ + --hash=sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4 \ + --hash=sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264 + # via argon2-cffi-bindings +charset-normalizer==3.5.2 \ + --hash=sha256:01077390b03f7988f11d700a2194e69b119741a86b1a638b1db88891e3eced8e \ + --hash=sha256:01b0c0d2262a9e28e8484a278c7e1b5d650e3ac8cf2683d2967e25899f208bdf \ + --hash=sha256:04851f73ae72b8413dddadb16a49dfee95263553741fd42d546f7d66907e6be5 \ + --hash=sha256:0521c5665880b33d603717defa76c094048900010897909952397feb3039da56 \ + --hash=sha256:0774bf9bf620249fee3e0b8b9fd3065de213be30f3aa94ce2494b3b638949e26 \ + --hash=sha256:0891b9d3903c5571c03771ca669a4b0ec5618ca722a5c957d3d29cd4e5062848 \ + --hash=sha256:0c951d5e6dd9c2ff60609476752bee49da4206adde960ebc247766937f72e718 \ + --hash=sha256:0fed1d06615f022ee3b13caf5e8b180cfea32bb2c5aded8a9d44277afc040f93 \ + --hash=sha256:114e4d0c92d618409ed82a99e22b5c5e768fe995f2973f78265f4524f49d4640 \ + --hash=sha256:11912e4bb14baae7c5d8791aa55ba0a3a03ec6729073307b0f57270abaa713d3 \ + --hash=sha256:11a4d68a6ecda3292cb1e50239e111543ba5d709bb62a6b4ea1afcfa729d8875 \ + --hash=sha256:124fbf1a8ff966d87ae05bb8bd45a71f966055ed8bba320d0c7cf450bc5f4d0e \ + --hash=sha256:1461ac396c4fdb983a675f20aa555624f0ee18ac83d832b9244ffff3d8055275 \ + --hash=sha256:1503bccbeb36d5527790c3930327704c39af22de3112f1b1666a9f3ce15ee204 \ + --hash=sha256:15bb4005af6320d259dc7593ca84a38d7fe06a421dbcf7b910ae23979101e787 \ + --hash=sha256:15c44f7edfd477b06f517a5cc317fc1707edb9de2c865f43d4b6513907473234 \ + --hash=sha256:16fa0eccf81304b79c5cd87f9271c3b85dd9dd99245e4422ae9c0dd45e0f99d3 \ + --hash=sha256:183b88127acdb4fabe59d951ab424faf1af7b63cdbb5f776186c1ea2ffcaed98 \ + --hash=sha256:195c26fb65950f8fce54e26349852b7bdd7c5f120aeefbcc440b8a20faaed4a3 \ + --hash=sha256:1afb975bd5d68d5ce9f6b6d44fdf2f7e34b895a35e95708a7a91b20a3b51d187 \ + --hash=sha256:1b4cbc7c3491ccb4aa17fcd8165649d01cf39f76de1696da8631b5f71b85401d \ + --hash=sha256:1bc0baf5ef96b6ede57d47f4b8fe4d9d84019c3bfcbeb20a41edc6a6ee341f1f \ + --hash=sha256:1c50fe28bbc2ced33386f298650d91218076c05420e6cbd790b913adc41659e7 \ + --hash=sha256:1db38f4c5496827c1a501846d64d14c3b80c7e6714e406cd7dc36a9899fa1011 \ + --hash=sha256:211d5a3eb6af8f513b8d4ca19a8c1b7accab1b5f0d3175f9826b03c1a920dc1f \ + --hash=sha256:23851fb4e1b85ed3f6c2a27b777cdfe2e19fb5b38429a8faf38c7542b7665869 \ + --hash=sha256:254eb48b9fa5ee9898a3c445825a1f340fe53712a098904b39b0bddba8ea3cb1 \ + --hash=sha256:2625388c6c754520c37abaf3b41eb34d1cc4a373f457898f08606c8e362b891d \ + --hash=sha256:281cb91036248400f4cc957495cccd44c275c2e0c5854f7e45ac5cf7dc193847 \ + --hash=sha256:28a15fdad492a99b6eccfaaed66ef3f74050680545ea61ec8b2f4c538f1f1320 \ + --hash=sha256:28b4f0d66fb834ff90f28209ac7bce77868c45d8c93e26f906709d9b7c2e1af9 \ + --hash=sha256:2a925889534b3748302dae5dead07cc13480de1dac3aea80a941b729b471ef93 \ + --hash=sha256:2b7b3bbfb4fe8ef40600792d762fbaa9057559f9d3fad209525b7a22b99e91fd \ + --hash=sha256:2c9ad19a6cfcd5ea5c0d41161d22f9df1dcc277e9bef2751391334546a314c00 \ + --hash=sha256:2cc961b171b3f3440f410489ab3573e86aea8736134ebbb40ea1338b7f0831bc \ + --hash=sha256:2ce45c6627b22c47e390bc91a41c3d13032192e699fa0bea96e9671b373d69b0 \ + --hash=sha256:2e06a3a98f916dd41d27f3105e02e7a40181c98c94b9158733d03a6f80506c09 \ + --hash=sha256:304d5463e65a35d7bb0850550e0780395395f6fcf452f04db7d5ca7cecc425ac \ + --hash=sha256:304d8e4d493af723536393eee0c689eb7813f4a474c8b479dee63f1fdd98f621 \ + --hash=sha256:30fcd120b732aa79317f08dee04d7de0847822e4cf7ee0e9f445bb958832252c \ + --hash=sha256:31f3930700408d211f13378ccbe1c40845d8da54bd0681fac3a9b5aae81c7aa8 \ + --hash=sha256:34276fd796040bf0993ab33a369aa572e6979c7aab225a88893667ad8eac8f7a \ + --hash=sha256:355ad8011081dec5412240c087a9a0c9d4d5039f3ed11a3f13e18c2b29b56c51 \ + --hash=sha256:38a873987f3be698494da8b2e3085e29da02da7b633dce73e79c699a113d7bf0 \ + --hash=sha256:39de2a259fc954455c57274dc94c79d5842774e1247a016aff30bc0efed0f4ef \ + --hash=sha256:3d14b50de6bf4d0edf857a9386836846f982b8f524e188e2e68b96d702bcf4aa \ + --hash=sha256:3d21b8b13c7592db2ac5e544a6d83187b995257472b0c9e8351b6d507ae37ed6 \ + --hash=sha256:3d31298449090ab8d47b7b1b2a555ff73cac7ed438a08b7ac160980c7ebed649 \ + --hash=sha256:3ddacd27458c45bdacd6bd6db644bfb730efbf9e830310186e3045c9c5be8fb2 \ + --hash=sha256:3df041de8887954562c9b261cba85ca0e9ded74048daf125f45edcfaa4832229 \ + --hash=sha256:40ab6bffa02ae10a0581e6c198be7d2d8ca5c2a0c64e4ed3465d766df457573e \ + --hash=sha256:4275811936e2f06feff5e598fb42a1b7ae852da8e39605211892b56b81a34efd \ + --hash=sha256:443eae2bf318abeaf6f15d785138f71fd6de770e99a92158b8b814265e079115 \ + --hash=sha256:447441e76ec720b15e64418d32e092297340387053047c7c694f579efb0ee1d9 \ + --hash=sha256:4495c5002a7b28557e7e222e77e0b661183e432b7d6d2e788101e3f240e05b8c \ + --hash=sha256:44bd4fbb29dfbeba60e7d2bd000c59e4b21ddb3cc53912b14048d37092706d7c \ + --hash=sha256:4685902cf26edf013ed7a3da0f426ebba7a00ebb9541386d835afbf002c11cab \ + --hash=sha256:498dc3188ca05a68231ac3fdbfc7f57eb67e1343c30e0fea17f8218c1599b253 \ + --hash=sha256:4c2b5031f63e331e3839b40aed2dd6f191e9c07edbde303e7876846ea1946995 \ + --hash=sha256:4d48f2d08b9de5864e2c8744d4461b862fb149a18274abc8b698c45975573438 \ + --hash=sha256:4f87960d57feabfb618e4e0af6e7371645fa26a277860739d6e5d6e0012c92f0 \ + --hash=sha256:50e3adfb96fc189eb27b1cf62d3b598b89b4bb0420d93a3d3e42e137409011be \ + --hash=sha256:51cf45226a9b588d0d2b4880c62d686934b63ab0bd79ca23ab0e9762eb27441b \ + --hash=sha256:52aa6992700996af31f375de0c6bacd402b0097fe40b53c426b9f51a90ebabc7 \ + --hash=sha256:55ea99acb17b9325618de155a0cd6a2e8f5d10be008113e1d433bbb58db543b2 \ + --hash=sha256:56bc200a365efb37383b7852e4cc5898d3b2da5987289b543956cf8cad71018a \ + --hash=sha256:588461c2e8384d309bd63e5826019b6977bc66d629b99ac8737bb795d7b2cb5a \ + --hash=sha256:58ca3755ee7ff7f59b57789ec9833c9de9ea275405cdd240eda1f193112e398a \ + --hash=sha256:58f361dcbab699cf8f42db3f47c8e7fd1036f138c23a5d08de9fde5f425a730c \ + --hash=sha256:598a11a2c7ebaa5334bf698bf29568c9c390abac6a154d8170fedecd1cea38c5 \ + --hash=sha256:59f63901b0031c3136cf64704dcb21de0bbae62ce2c9529bc39d27665463de37 \ + --hash=sha256:5cde776b7cc66e4f6c99612cea4aa7269aa65863f7a15841b2c264f103822f4e \ + --hash=sha256:5e2b6b57e9733d39f0c9fd3185efa6b8e29652c4cd8fe94180272cf6ed9a78c4 \ + --hash=sha256:5fb29fb8cd1a46c27a1bf9613ad5ec2599310d46b4025d9556404a6b6a292800 \ + --hash=sha256:6045373d5a89a5ec71afde535db987ca28e76dfa276c2d4c818265b375d4b055 \ + --hash=sha256:619799369eeef6366ed3e8755a5670f4f2f0fb6b30a0fd7264dc0fdc2357058e \ + --hash=sha256:62588a277bfb59def052abd940703fa35107152bf479781a878617d60faf8fb5 \ + --hash=sha256:62603db9a7caa0802eaa28c1c46fecd7b3a263a774069c24c3c28c302448721c \ + --hash=sha256:65cd72beeeca9d3aaea1201e5923859f308f952f9c71de93f06063c79f0f7a3b \ + --hash=sha256:68eb192d85ab8e5f6ec69c2bc6ac0179fbf04a5ac1569d12fbef74883fe102d0 \ + --hash=sha256:6bd128f206a7752ae1f2ab6c61bf8a24ba28913a10df8b14c2637b973ff97a80 \ + --hash=sha256:6be488a102b8cf28d0391d8c4ba7748938ae28b78ad901f8585520fca33ead1a \ + --hash=sha256:7218e8f32b0956cfcd048fd42d9d5779809745ca1d86113ca56f66e7ae1549c4 \ + --hash=sha256:7441d755b7ab94f8d4eb3e43ec05482d760842fd263d003a99102d742cd835e2 \ + --hash=sha256:749e97e1b32313717a565abbe321bc2190bc8b35f1a67e4cdbc7c56c8d8ffe58 \ + --hash=sha256:75a3ceed0724d625d64b86ca20aba182e4df462e04c2414fc941c0f523f06aac \ + --hash=sha256:780fbe7cab297b81dad9fb8dc5eb003c0468ffb0d9e5f65068c53a34661a96bc \ + --hash=sha256:78456a747de8dc58360ffa581f30a002baf5aa28cb262536545e91f113ed7639 \ + --hash=sha256:7967d08cf06dee78443b874f98c98036f624f3a4e73e11f9f64f5be4d25393cf \ + --hash=sha256:7a881931aa470808df94a8c380eed2bbbc76cd9dc622310f99665658c821eb6d \ + --hash=sha256:7dcd882da75ef9adf94903b1e3b9419e8aa8fb4c7396822b834b9ef7fb96954f \ + --hash=sha256:7e841fb9010836c992c9f12fcbd43a831de93a5f726fc1ccd8ca1d0268c5014c \ + --hash=sha256:7fdde2c9fd9e3eca40631e024664cf2584272cc8f96308cbe5fdfc930f51d8bc \ + --hash=sha256:8024d00c3faf3fc0c16e07a69f4405e8eac7cc0ab15f65fe6cf43827c4cf72b4 \ + --hash=sha256:80d02b6f04e92601a081dd97b23d3128033098bff5d35d392ddcc0476ea11253 \ + --hash=sha256:838dcc90063569a0448120554591a1d6c4a4ffe11babf048908793154ab86ade \ + --hash=sha256:849df64e889b2e17230d58410a03dba311a65b163508fd33679b2b737d4b7858 \ + --hash=sha256:87475fabc8d9996fd9c27debb395e642e8c838d78a00b6e932227a0e06b81e26 \ + --hash=sha256:87e50a3e7cb90af586b6c5faf23e302a970415ac73bd7bd90a515a04b427ef96 \ + --hash=sha256:89b53f3cda69831909888e0494f4fa0bcd3537e3e138dabeb620bd6ad946bae8 \ + --hash=sha256:8a893cc101149f80a653f82062ebc95b34525a2614382e1da5458fe7c6997249 \ + --hash=sha256:8b2bfab86aa71ae13aa41a6a26aab338e0db2b8bc75434b05aea89e011ff35a4 \ + --hash=sha256:8d86d6fc60743dc916eb79e2eb1ec4818e21e427731543af40a3021851174a13 \ + --hash=sha256:915563965d418f986e7e145accc592eae9e1a1be3566ff98a05d7a9ec42a76e1 \ + --hash=sha256:92888bb3187c5ba50500b00b3b310c9f2c651709d28036077680cb5255450a03 \ + --hash=sha256:93223adc95033dd47133a46ccfc316a0139176fd79085762e27202ec56018f03 \ + --hash=sha256:9373ad13ef0d2c0fb761e04e55bfdee5a08b52cef2c882c8fbe9935b1517152e \ + --hash=sha256:9409a8bf35cf78353942504b24a57de3d75b708997a1e4bd8db71ac8633ce364 \ + --hash=sha256:9b7f416ff0978e2f2249330527f0ad6fa02f4932e6199692d3b52da2048c19e4 \ + --hash=sha256:9bde855991b7e362c146535e3136a50bfaffc0487d38b33ca7e5edefc6e23849 \ + --hash=sha256:9cae88599c7219005d879f98e5ed53341e9a122af585e1091200358a3003d2a0 \ + --hash=sha256:9cf9b1a857e25c4baceeb3624e92a56df3668f398c4acba74e174d81fb4d1d3a \ + --hash=sha256:9f56f72050826f63dcee7a7f55b0a77168cb3bfc553fd405e7f8f9ece75a4036 \ + --hash=sha256:a090bb2c68df85450502e3e20d665e3a5af9c65a84d6508ed477badd49166fd3 \ + --hash=sha256:a192e2c40070d92c3ccf777e3a5c4ff515573cd2bb7ed0c537fdadbbec5bbf21 \ + --hash=sha256:a19a731138fc27d5682277d3b9df22855cea1239bce7fcec5f78f42ef2d1f3c3 \ + --hash=sha256:a66c3bc5ab1f0ff2164fc9965ddd611ff0802173f4b9d24554c563f6ab7e1d6e \ + --hash=sha256:a815775b6c38d4e0ff7bcffbeba67feded90202bb6a226b8dd35f1c855217413 \ + --hash=sha256:a89012d6d5476ee112d20d998570ed58df2260a852afb1758809cd6900411d21 \ + --hash=sha256:ae4f5fea5b8b8ccff88238cc8569303e5ee95efae67fa62922a311397a71f346 \ + --hash=sha256:b6856554c4f44d79fc2307d5768854310a8f0096e501c75637542c82292b0429 \ + --hash=sha256:b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685 \ + --hash=sha256:b736353c0a625bbd5fcec108576e2385db3496f4f771f785ff32e108d3c3bc45 \ + --hash=sha256:b7fd005a73d9e657273b7a10dc71a9e03c8fb9ee6999798d6918ce095b81ac7f \ + --hash=sha256:b91363207bd9dc966a691e959bb47f64b30f7ac4b072be9968b366982f7db77c \ + --hash=sha256:ba0b1d2620edf869789c3879223f52bf2afc5d31b3cb47cc57b3a12c05e2aa9d \ + --hash=sha256:bbbfc8e28816f19d7c0f1816664980c0a9875d01b27cdf8eedddb639d9e108ad \ + --hash=sha256:bd16aabe4a02a297c23417aa17ac6299dbd8c49f673bcd645b4929b11f5a4400 \ + --hash=sha256:c0afc6800ba57ccc350374c5bd6150419915d95ce93cdbab2d783d75eaf30ecb \ + --hash=sha256:c6708715abcf3c73b99508253e961a9967f02fe536532834149574eda6de0d1c \ + --hash=sha256:c7c9ab723cde841fefb34efbad91e87f00a674b1fe1cd0784fde742bf2c154dc \ + --hash=sha256:c8f3d67aeaf55f017982b73683f0e7342ba2f6635a78f69ce89ebb26aa411e5c \ + --hash=sha256:c9790464842f85f437dbbb54417eda1e0e6bfc52dd8d22d6fd1c994b73b2dc74 \ + --hash=sha256:ca403d7e4798f525fdfc78e258820419cbbd0f0ecbab9de7840e3c017cf6b8cf \ + --hash=sha256:d008d90a7f2471519aef0c90dfbe73b3e6e4d5e66ac48e19154c17e89e98b604 \ + --hash=sha256:d19fbd981a488e22cd04883659ca6b08f50b5974f9fd7c95655ef6a043e5893f \ + --hash=sha256:d1befeed746d247c81127bb14de9dc3d30edb6e5976d34f83f86ed262b1d9105 \ + --hash=sha256:d2374b62878abb00cd8309b32af6c0b715cd02dec0ca74ef12e5069bdc64144a \ + --hash=sha256:d376bbd28b3a8999db1a103b3b388aee6f1ddeb3e51bc2172993efdcd86e064d \ + --hash=sha256:d4a7319f304a774bed22115bc891618e45f85065ab44ea6acd07d274e750519a \ + --hash=sha256:d6734d2ef8a50fbf8445c139477da401f50d62a0606bf00e20ec6d87773fefb1 \ + --hash=sha256:d760fe2a4d7c3b226cb9026d6a842868d52a7901bd98420e1baf14e80da85cf5 \ + --hash=sha256:d913de495d90407cd859d263bee2e5d1a4ed3eb6573c04e70d9ec619a7cbed7f \ + --hash=sha256:db19d07e2e0129e974a0e65d0064fc222a446cd5122c2fd4184d2af9fc734a9e \ + --hash=sha256:dca9ab98072a5a54ebacebdc45f53e645336b320c667410b061be1ca588ae709 \ + --hash=sha256:ddc7dacc8ece3a182e7f15cb862d1fd616b46d076cb1ae9dd232b2c38b655874 \ + --hash=sha256:ddf19c062bea7a0cc80f519243d2c01dd091be0cf952a0750d4ad576709559f5 \ + --hash=sha256:def79fa35ef0cef8d2accec024f4fdc7ead3012ff02f5215c783f39f03ef8cfc \ + --hash=sha256:df29a0a7107f7011e77f4eebdddec4c7331e24d787a0b21a46d63bdf7445da95 \ + --hash=sha256:e09a3942ecbdee5cce73ea9d42da82b81b72ac1bf031ce069b93b5adf4eac8cd \ + --hash=sha256:e242bb1c5e76e97dfa9e7f209a71e93a01d7f19ffdd5cfbb2e2d55b4f08f8ab0 \ + --hash=sha256:e243bd13217235fc7290c621941c3f5cc8b66e4872495be821d7436ba2fb838d \ + --hash=sha256:e2af3aad578aa6bd1384bcf4750fc285e5a9de53f40b7d41e5a0bf748edeb2b3 \ + --hash=sha256:e4e81e09c1578b8df602e3db08b0b3ea0a6947ad612f52bf8dc5ea8d47691f0c \ + --hash=sha256:e54da4baf05720032d527874d40b65fa4d7e5c6c6a43d0c3adbeffcaf275a2b3 \ + --hash=sha256:e80e6c2f55656b4824d72065abb4ddd6a525c74bd78a0aab5d9fc2cf4fb5af50 \ + --hash=sha256:ed2a239c0ea213acc1908150a3037257083c7c083128f1a4cec2ec4b97dca491 \ + --hash=sha256:ed905975ab14056a2e5eb1c376cb2e1ebc5396baf84163939c518556fccde9f5 \ + --hash=sha256:ee21e28f0430bd6dc9086c6e525d5e818a44a5ad19720c8a0ef766792f3eb5e5 \ + --hash=sha256:ee43c17b173d46a3212baa6ead3ae258eeabdae48c263a01ccf0218c366dd655 \ + --hash=sha256:ef4fcbf3327382cd4c9f540babd61248208af7b93eec4de397b4d5f58a09e288 \ + --hash=sha256:eff0ac9dbe711a4aee69bf04a83896aa9b85f19641264053a9f6d48573abb7dd \ + --hash=sha256:f0aa869112ef88429ae17820d99c3dd9504c9e9c671d3c246f3d7442cb051084 \ + --hash=sha256:f3c96f633825733f735c5a9cf21d21a257d8e1edf0b1cee0a064b9c424ca0f7d \ + --hash=sha256:f5833ad231be5eb6553de524a70f48d71b2c8563101750531e0b80184e175cd4 \ + --hash=sha256:f5ec61164adcec446f8969a3358ec3f9b26bbda3b9213e5586d219afa8df2915 \ + --hash=sha256:f7d486c83842422badd511868fd8a9a20e9407ace71564b6af47ce7e60a336c1 \ + --hash=sha256:fb9e68df06293761f9fe66ade60a9bc6d0f5e42b8acf2939a9158af86ab0e5bd \ + --hash=sha256:fc14a032f813bf5fe624d991960ea83e9715adc27e4c1830a2361eb1d02ac341 \ + --hash=sha256:fcff63213e8e6e47770541a4607175404f47cbb3ebea7b6058cc82d524a0e424 \ + --hash=sha256:fd1fbe0f116b6e55da77aca2c6ddcddcfac2186cbf78bdebf40fc156efca389d \ + --hash=sha256:fe9753dfee015c570d73df76f899f18444d41388bffcde097deba51c4fadbb9f + # via requests +click==8.5.0 \ + --hash=sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360 \ + --hash=sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34 + # via flask +cloudpickle==3.1.2 \ + --hash=sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414 \ + --hash=sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a + # via joblib +comm==0.2.3 \ + --hash=sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971 \ + --hash=sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417 + # via ipykernel +contourpy==1.3.3 \ + --hash=sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69 \ + --hash=sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc \ + --hash=sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880 \ + --hash=sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a \ + --hash=sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8 \ + --hash=sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc \ + --hash=sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470 \ + --hash=sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5 \ + --hash=sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263 \ + --hash=sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b \ + --hash=sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5 \ + --hash=sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381 \ + --hash=sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3 \ + --hash=sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4 \ + --hash=sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e \ + --hash=sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f \ + --hash=sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772 \ + --hash=sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286 \ + --hash=sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42 \ + --hash=sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301 \ + --hash=sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77 \ + --hash=sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7 \ + --hash=sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411 \ + --hash=sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1 \ + --hash=sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9 \ + --hash=sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a \ + --hash=sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b \ + --hash=sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db \ + --hash=sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6 \ + --hash=sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620 \ + --hash=sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989 \ + --hash=sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea \ + --hash=sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67 \ + --hash=sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5 \ + --hash=sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d \ + --hash=sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36 \ + --hash=sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99 \ + --hash=sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1 \ + --hash=sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e \ + --hash=sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b \ + --hash=sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8 \ + --hash=sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d \ + --hash=sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7 \ + --hash=sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7 \ + --hash=sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339 \ + --hash=sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1 \ + --hash=sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659 \ + --hash=sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4 \ + --hash=sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f \ + --hash=sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20 \ + --hash=sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36 \ + --hash=sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb \ + --hash=sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d \ + --hash=sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8 \ + --hash=sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0 \ + --hash=sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b \ + --hash=sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7 \ + --hash=sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe \ + --hash=sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77 \ + --hash=sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497 \ + --hash=sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd \ + --hash=sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1 \ + --hash=sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216 \ + --hash=sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13 \ + --hash=sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae \ + --hash=sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae \ + --hash=sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77 \ + --hash=sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3 \ + --hash=sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f \ + --hash=sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff \ + --hash=sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9 \ + --hash=sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a + # via matplotlib +cycler==0.12.1 \ + --hash=sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 \ + --hash=sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c + # via matplotlib +debugpy==1.8.22 \ + --hash=sha256:0c1104233340196e5cbf5514e7dfdbb98968e730cfd7fdd6f3d72a082be838a9 \ + --hash=sha256:12bc7f368182b517cf26c76a2393fff65354e365fa1552b6241e66edff17997b \ + --hash=sha256:192b73e8d53bbd60225220c0943627bf249ac93d0ea3090e23c45f3e0ceb6a35 \ + --hash=sha256:1bd0c6df3c68c0a3f71db8baa3780a953abb65537ec4b3bc6b935ad5b3b3d45c \ + --hash=sha256:1e76339d5510bc17e9181dba9577508afcb21aad5728f1a55ef74d7d97d255f3 \ + --hash=sha256:225d063f81708c2546999e7edfac0198b2d5c2f144797dc64858f867948633e0 \ + --hash=sha256:371a4ba4a5975eb958393903f3254cf983da7b1c7c178b3f987ee427f42515e3 \ + --hash=sha256:3b7c328cb47b4e2f2b40801936daf8150f88dcb4ffef5080ff119270f0d23626 \ + --hash=sha256:4ad076f4f66cb8acb79e4384d48b1ea60a0b1957aa0b1112d4887ea3a4df60e0 \ + --hash=sha256:56b877b37ed73f0bf53ba7afc394816ff1eb5d701bae24a24ed9e1ea6f7ce34f \ + --hash=sha256:66e4ac3d6e7026e83e7d93d7ee2f51dd4a4e8dff673578d424e60796893e5b2c \ + --hash=sha256:745e1800ec2961e5660c1a317c0a20e28c0fef4de36c04f17a21c33f2b37a92a \ + --hash=sha256:7bf29e0d8ce80b100d37fb333e1b193b790d962739f3067796c2b03ffdc9afee \ + --hash=sha256:8a697acec45dbc70d17fb5d9f4f61989fc294d273c69de487fd10cb35fdd75eb \ + --hash=sha256:92fc425308a08f601f3c69afb4719767f9b898276f44b97e6942a713c4c32740 \ + --hash=sha256:a9e9d3550e15ca479c59333e90845029190531f0cacfedab3b815a57bd913947 \ + --hash=sha256:a9eca6ab09a61534e923064400081e016f7e1d8430cc6e8b7a9cecd2f59e2b07 \ + --hash=sha256:b17a4896520f1c6da09ce76ec8215df0b85b0b6f3617526a4c65c2315340875a \ + --hash=sha256:b7dbde1fb822d100802d505b2aed6d0813b1a0a2015d495cb8798b2215d5d1e5 \ + --hash=sha256:ba810a66b437e3c43ca0ce0892404f010338286263ca8e5108442d76a9485337 \ + --hash=sha256:c1ffb9953708b648ecf6acd2e5ad2c002bf68785ed618197dd4463a2a7226f39 \ + --hash=sha256:c21dd7e1ec22556bb41dc3bf6b86c603e440ec889c4acb27d7206354b2106c54 \ + --hash=sha256:cba7b99573ee41f9510c6deeff1cdd0c333748b9dd63d5016413dd57ba9a4593 \ + --hash=sha256:d593a330297332ec435f3965c448b6d500e03f33675cf2063178bc76377a788e \ + --hash=sha256:e489c7268e1c7b41e13b438d9c533d2a7af73fb59bf8cd30fead8286c1c39c4e \ + --hash=sha256:e6744ac1850c73c2ba7b29a126cf9ab74efd1831190720e8fee17ef5389c8f5d \ + --hash=sha256:f49b1d6cecf326b63c06d7f517e4a6642777f758c58cb799e7bc5a6dd74721c2 \ + --hash=sha256:fa47099176d1d612bef69e9f74658a5687a9f05bd80fb5710f99959bace85735 \ + --hash=sha256:feea785c7bbeb8cfd5b01a63f9061c899dc468c8be81671141a29305774fa294 \ + --hash=sha256:ffeeaf4dfe1534375902040df32a4c48f721fd35a4d4d337ac72a44d4176075b + # via ipykernel +defusedxml==0.7.1 \ + --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \ + --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 + # via nbconvert +executing==2.2.1 \ + --hash=sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4 \ + --hash=sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017 + # via stack-data +fastjsonschema==2.22.2 \ + --hash=sha256:0fb3915616adac85ccfdd737d26be1089845d2019819505b42d39888458f74d4 \ + --hash=sha256:72064e12356a7d6ef02165be2946b9abadbdf238536e07eb587e3dbaa33099cf + # via nbformat +filelock==4.0.7 \ + --hash=sha256:a93c4d93269b339a6af4848342c7e940d0f9928ad95eff64764699e5f1bf8a6a \ + --hash=sha256:da5915714a70b55d167fdc7e251ad91302b0a36816fb574dfafae8f4f2c9bb21 + # via torch +flask==3.1.3 \ + --hash=sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb \ + --hash=sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c + # via + # -r requirements.txt + # flask-cors +flask-cors==6.0.5 \ + --hash=sha256:30c5031552cd59f620ac0c8211dac45b345d3b2df310e7721879e4f46ef9c601 \ + --hash=sha256:68fcf75693e961f3af26683b23c4b9a8fb6b64de17d20d0c37b95e8de7ab2ed8 + # via -r requirements.txt +fonttools==4.66.1 \ + --hash=sha256:058cd823b80bac59e64dfad9e3b6fcd677852f9a3804971bbf6b48cc611e785c \ + --hash=sha256:05aeb146451f37289f782c3c861f3d0f4b86c2dd2e4620b46683544c7406640e \ + --hash=sha256:05c0fff6b4a5d872ed89cab2c4f81060b86ace263903eb4e8d0edcac47a60dfa \ + --hash=sha256:08d8956e3ec990c75230d92f1630b215e8f3738c83a003421c22b31ebfd0ce15 \ + --hash=sha256:09ae73bd219e1245debd8376077a0fa6e03175e255c4f51bae5f6a271bfe384a \ + --hash=sha256:0dc6fd99cb8c30941036308b148da9432640442a6f26f36d71dad9be24cbd0e9 \ + --hash=sha256:1801fdad5600118327171e0e8aa79f7cc48831dd55ab36998c9de03bd5ffe6cd \ + --hash=sha256:261d8dc95845e751f975fe8d6075600593ee253470d46d1b84801688051b09f6 \ + --hash=sha256:2aeb745f2664eb811026997c95628071137a777ea2ad296deec9cb393f0b23cf \ + --hash=sha256:2c7340497cf53490293e0c2b61011e0191633022ede0a0a964a68157a98b0fb4 \ + --hash=sha256:2ce4c93160535761f22c80b2afbc96cabc09855363a5d1a5554265b8a4c85901 \ + --hash=sha256:2d320483928c7831f0139ecb361954a26b2e2a8995681200155835dd8cd4a7d5 \ + --hash=sha256:2d637468dac23aac0e223bd52e66f8faa3b0dfcef57435460fa2107e830226cd \ + --hash=sha256:3087a430722aba8de429c2539fd2a58a9cf05238cdfefd8626460001052ca878 \ + --hash=sha256:34378db9a398b59de18cc79d942f0a907c6fc6301945e065ec888202f607aa3f \ + --hash=sha256:36bb24d4b98faacaff04af1d5e0a4285feba6ed1da6728cd34b6b6deb6bbb934 \ + --hash=sha256:38ce8f5fbd5c17dd2153d47d7c8d4108f3deda3f2b4a79b60ddc470a58faded3 \ + --hash=sha256:43d1284c1964666ee833f2badd3017dc138f53d4889043ffca66c5ce4188f188 \ + --hash=sha256:53e5854ea8003efec34adc0863c18ce91da923018354d27366f7fee7db928d7a \ + --hash=sha256:56d41d650cb8fc6cfe1d85ed7c62a0a56cbeed07bc65ca795475b914d401312a \ + --hash=sha256:5de5d80fbc0e50ff794c244e8fb7afd3eadfe0fa232ba8b162b8c551df22fcb4 \ + --hash=sha256:60f5ea17aed4262630afa43f26997ceabd6417fa05dcedf54c665f5a29193e18 \ + --hash=sha256:64967c6ddb0d4c610dfd8cb1485981b2d27972ddfb7d4bbbd9e199d2a089c450 \ + --hash=sha256:668f092bc0de8902167df6a0d5c5aedc3b4f9e43cf88eea92e9b46a2bd3968f5 \ + --hash=sha256:66fad3b7874062c2a2692f0ae6dea56d24f01b778c7f191950ca3ff997e25a88 \ + --hash=sha256:6946fe7bfb28590a1fd4061a17609c9a843952deb65dcf30d1fe725070c3e7a4 \ + --hash=sha256:71c7ca1b5f46f5dd549f56b47d47c0b709217675c23d3a7bc6aa1a69b6d9bbae \ + --hash=sha256:72299346b96b9244dabcc051b24e4653da4edfda6105544cfb10ce856a1afaac \ + --hash=sha256:7234ae9e28db64273fbbfa72caebd0a97e3bdba6b05064114741b9539ef339d0 \ + --hash=sha256:7b8ff9e0edbcee2fbf7dff0c41b9041c1901c26acf64e23adb67495012df11de \ + --hash=sha256:7cf4f996f9b1cb549bff9ea4c50813988a26ec922c95cfa85c7e4f1270447e06 \ + --hash=sha256:7f49f2834f5d006fe0f3bb10fec73b261806c50941f0cfbc08294074ffc32210 \ + --hash=sha256:83572afe48733bad7a4a9c11721d3a726c2e976d82b063fc9bdd049d76955abd \ + --hash=sha256:8526b2b7ec4db6b81efb83438be52b1264eda9a4994d867163cfe8c65581ce8d \ + --hash=sha256:8aed2bbcd6216253ef1b015763593365ee8084f621dfa53bb957c19d5e05f7cd \ + --hash=sha256:90de3477394c73481d27d2b86091c1c736053ee13ff52c42f0e151948e8578c6 \ + --hash=sha256:9261ef507f2dd74203443a472b65b5a26429eb378f975016dec7dc7305b24898 \ + --hash=sha256:9ea6c93091cbf83161a544388746a0911550bd98cb911faca3591cf5ead166ac \ + --hash=sha256:b13c8c541ce0b794add3211b3641cc0e113d707f73e06235e6fe9731bd7c45a9 \ + --hash=sha256:b18803cbdef248e7ee1be59cb277fbbe1da1faaa6f726fa5d3557904e6a3d967 \ + --hash=sha256:b878c78b2af11b879bd4f26bb0d8bda2a4c64543fdd3f28efe2c80f97f043885 \ + --hash=sha256:b8b71db96d605784e2c5ebf0788a406018ea8fdd80338491f4c83613d5cd1fec \ + --hash=sha256:b913b8e9f7ca9bec44d1eb919f591c596c61041aa357c96be55ff93169859e91 \ + --hash=sha256:c258eba62260beb33c110b03a6912cefa3635239c4ab5615b7225fb6f7b85238 \ + --hash=sha256:c47299bca4b5acaaeb32100f77b944feea151de9ef1773365a410dc3d49b945b \ + --hash=sha256:c666fefdd5613a0e99aa4516e6ff4ef87aa86cf1c7ba12a73550f4770e46b750 \ + --hash=sha256:c724e56213494c6695335577822b2d1628d102e71614de8b7eb8e30886d6a314 \ + --hash=sha256:d3b5403e82d0c7659ff1d9f956e29a3a68d094f043e9f5bc0442796fc3a4fb58 \ + --hash=sha256:d4f76868aea9cc4ce47fdbeaa904c02ee7d85dd0ad095071ae77f0bda6e62cf5 \ + --hash=sha256:d84ac0bf776b68396185bd919dd29e633d94300660335efc40b55b294b886903 \ + --hash=sha256:d8f0a8f16c4f3a5a87ca971de2631792d8cb4d570951f2000acf712f157d40db \ + --hash=sha256:dbb7b950f8c02deaffb6968994691e8589d671b7ef8396bc9d5b5c0dfbb7292f \ + --hash=sha256:dfba62cc93199ba62c376f90f2a9147d92730d301e44f88e013e50ff5edf6193 \ + --hash=sha256:e1cde50b3ec84ca6fe63ca815de183dbecb88e8adf8ada82d8ea130ef12b2b43 \ + --hash=sha256:e7ea7a08547a453fa000db96ed5714a3dc7e2b4255b9243f897921f8c10c169a \ + --hash=sha256:eef76d5796e604f9d6753fa6d323c4eb9f4e0e43f1dcca553f3e6914f1667b64 \ + --hash=sha256:f08ab7f8461c37ecfdd29ad97fb0c0780b50501bd664bb0f46b6e83ed2b9d2a7 \ + --hash=sha256:fdf4afd75c643e60ef4a96fe64fc8a9def27d2a542112332371a9e5066885f9a + # via matplotlib +fqdn==1.6.0 \ + --hash=sha256:6c793c312ccfa29981e1135b3ef5e1277ebe639731a6b0f7350d90e96e63f0b4 \ + --hash=sha256:e39bf62e2a9481aa2cb780554fdfd944dd8ee5f230eb831684d539cbafed8de2 + # via jsonschema +frozenlist==1.8.0 \ + --hash=sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686 \ + --hash=sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0 \ + --hash=sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121 \ + --hash=sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd \ + --hash=sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7 \ + --hash=sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c \ + --hash=sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84 \ + --hash=sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d \ + --hash=sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b \ + --hash=sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79 \ + --hash=sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967 \ + --hash=sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f \ + --hash=sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4 \ + --hash=sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7 \ + --hash=sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef \ + --hash=sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9 \ + --hash=sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3 \ + --hash=sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd \ + --hash=sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087 \ + --hash=sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068 \ + --hash=sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7 \ + --hash=sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed \ + --hash=sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b \ + --hash=sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f \ + --hash=sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25 \ + --hash=sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe \ + --hash=sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143 \ + --hash=sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e \ + --hash=sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930 \ + --hash=sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37 \ + --hash=sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128 \ + --hash=sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2 \ + --hash=sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675 \ + --hash=sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f \ + --hash=sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746 \ + --hash=sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df \ + --hash=sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8 \ + --hash=sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c \ + --hash=sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0 \ + --hash=sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad \ + --hash=sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82 \ + --hash=sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29 \ + --hash=sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c \ + --hash=sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30 \ + --hash=sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf \ + --hash=sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62 \ + --hash=sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5 \ + --hash=sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383 \ + --hash=sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c \ + --hash=sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52 \ + --hash=sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d \ + --hash=sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1 \ + --hash=sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a \ + --hash=sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714 \ + --hash=sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65 \ + --hash=sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95 \ + --hash=sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1 \ + --hash=sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506 \ + --hash=sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888 \ + --hash=sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6 \ + --hash=sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41 \ + --hash=sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459 \ + --hash=sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a \ + --hash=sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608 \ + --hash=sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa \ + --hash=sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8 \ + --hash=sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1 \ + --hash=sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186 \ + --hash=sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6 \ + --hash=sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed \ + --hash=sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e \ + --hash=sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52 \ + --hash=sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231 \ + --hash=sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450 \ + --hash=sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496 \ + --hash=sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a \ + --hash=sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3 \ + --hash=sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24 \ + --hash=sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178 \ + --hash=sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695 \ + --hash=sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7 \ + --hash=sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4 \ + --hash=sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e \ + --hash=sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e \ + --hash=sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61 \ + --hash=sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca \ + --hash=sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad \ + --hash=sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b \ + --hash=sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a \ + --hash=sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8 \ + --hash=sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51 \ + --hash=sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011 \ + --hash=sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8 \ + --hash=sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103 \ + --hash=sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b \ + --hash=sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda \ + --hash=sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806 \ + --hash=sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042 \ + --hash=sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e \ + --hash=sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b \ + --hash=sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef \ + --hash=sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d \ + --hash=sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567 \ + --hash=sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a \ + --hash=sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2 \ + --hash=sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0 \ + --hash=sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e \ + --hash=sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b \ + --hash=sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d \ + --hash=sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a \ + --hash=sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52 \ + --hash=sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47 \ + --hash=sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1 \ + --hash=sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94 \ + --hash=sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f \ + --hash=sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff \ + --hash=sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822 \ + --hash=sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a \ + --hash=sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11 \ + --hash=sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581 \ + --hash=sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51 \ + --hash=sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565 \ + --hash=sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40 \ + --hash=sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92 \ + --hash=sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2 \ + --hash=sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5 \ + --hash=sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4 \ + --hash=sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93 \ + --hash=sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027 \ + --hash=sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd + # via + # aiohttp + # aiosignal +fsspec==2026.9.0 \ + --hash=sha256:0f08147951c8cb31d844c3547d631053b127863b60be04cf06e121333ee0e2fe \ + --hash=sha256:8dd6e646e99ea382bd85f97a45e6b526a442d79423a7dc673f1e2756d05fcb5f + # via + # torch + # torch-geometric +gunicorn==23.0.0 \ + --hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \ + --hash=sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec + # via -r requirements-research.in +h11==0.16.0 \ + --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ + --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 + # via httpcore +httpcore==1.0.9 \ + --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \ + --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8 + # via httpx +httpx==0.28.1 \ + --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \ + --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad + # via jupyterlab +idna==3.20 \ + --hash=sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44 \ + --hash=sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c + # via + # anyio + # httpx + # jsonschema + # requests + # yarl +iniconfig==2.3.0 \ + --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ + --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 + # via pytest +ipykernel==7.4.0 \ + --hash=sha256:4330114d22b9b33575b2c7c68753fc8faabbbcfbc60b0cdff9f14dd4ce63742f \ + --hash=sha256:a6757f790ddc5a6006b813d82da1a9dbb65b3a00f2b6607df776418c6566ec1c + # via jupyterlab +ipython==9.17.1 \ + --hash=sha256:6d1645743cfd1a07eb695d85aa2b5fa66721f8cbae9431d4049f7084bbf06509 \ + --hash=sha256:8919be8c27f20a6f4423145028063f6637b42a03ce57665bb12015ee1f073529 + # via ipykernel +ipython-pygments-lexers==1.1.1 \ + --hash=sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81 \ + --hash=sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c + # via ipython +isoduration==20.11.0 \ + --hash=sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9 \ + --hash=sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042 + # via jsonschema +itsdangerous==2.2.0 \ + --hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \ + --hash=sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173 + # via flask +jedi==0.20.0 \ + --hash=sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67 \ + --hash=sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011 + # via ipython +jinja2==3.1.6 \ + --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ + --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 + # via + # flask + # jupyter-server + # jupyterlab + # jupyterlab-server + # nbconvert + # torch + # torch-geometric +joblib==1.6.0 \ + --hash=sha256:2ccc96785b12046c08fd6d55839c12857831b54a3c1673ffadd2f04bfc4eda03 \ + --hash=sha256:3dbbf9f6e4b592a2357b854608e980fe6390d131d7a82f011a377ef2ebef7aba + # via pymatgen-core +json5==0.15.0 \ + --hash=sha256:56636a30c0e8a4665fe2179c0212f32eae3796dea89ea6f649b9436ecdb39618 \ + --hash=sha256:7424d1f1eb1d56da6e3d70643f53619862b4ce81440bdb8ecfd6f875e5ba4a71 + # via jupyterlab-server +jsonpointer==3.1.1 \ + --hash=sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900 \ + --hash=sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca + # via jsonschema +jsonschema==4.26.0 \ + --hash=sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326 \ + --hash=sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce + # via + # jupyter-events + # jupyterlab-server + # nbformat +jsonschema-specifications==2025.9.1 \ + --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ + --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d + # via jsonschema +jupyter-builder==1.2.3 \ + --hash=sha256:01aba6794eb9b19e0e29ae21137ca60ba4135c70347d4b9f664a586822b8c809 \ + --hash=sha256:c5ea5a7190c2a7b082494abade98eece1b2b5bd5dbd7d610606cbcd10a1d08b3 + # via jupyterlab +jupyter-client==8.10.0 \ + --hash=sha256:5f73f24f22fa25192cfff6b23c051932a2473a797b05734aff495b392103e14e \ + --hash=sha256:9f7116294dca55f1785be880057d44544db9b1567718d92cb33c58886afb9497 + # via + # ipykernel + # jupyter-server + # nbclient +jupyter-core==5.9.1 \ + --hash=sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508 \ + --hash=sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407 + # via + # ipykernel + # jupyter-builder + # jupyter-client + # jupyter-server + # jupyterlab + # nbclient + # nbconvert + # nbformat +jupyter-events==0.12.1 \ + --hash=sha256:c366585253f537a627da52fa7ca7410c5b5301fe893f511e7b077c2d93ec8bcf \ + --hash=sha256:faff25f77218335752f35f23c5fe6e4a392a7bd99a5939ccb9b8fbf594636cf3 + # via jupyter-server +jupyter-lsp==2.3.1 \ + --hash=sha256:71b954d834e85ff3096400554f2eefaf7fe37053036f9a782b0f7c5e42dadb81 \ + --hash=sha256:fdf8a4aa7d85813976d6e29e95e6a2c8f752701f926f2715305249a3829805a6 + # via jupyterlab +jupyter-server==2.21.1 \ + --hash=sha256:2a6467606af7dbae2e7e31640030025969e15db6a649eae334af90415dc71dca \ + --hash=sha256:a8960aa29263f6041283e97d4756b099fb49b767baab6371891ecb1bd40a63df + # via + # jupyter-lsp + # jupyterlab + # jupyterlab-server + # notebook-shim +jupyter-server-terminals==0.5.4 \ + --hash=sha256:55be353fc74a80bc7f3b20e6be50a55a61cd525626f578dcb66a5708e2007d14 \ + --hash=sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5 + # via jupyter-server +jupyterlab==4.6.4 \ + --hash=sha256:15b13f991d3985129c797eb84d9949eeb8b6615e14b444868e642411f2c418b2 \ + --hash=sha256:404f49b081819378524886c9db66dba57a5565981eff885830df1baba3a17df5 + # via -r requirements-research.in +jupyterlab-pygments==0.3.0 \ + --hash=sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d \ + --hash=sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780 + # via nbconvert +jupyterlab-server==2.28.1 \ + --hash=sha256:0c3c2418d51021ce280916e63dfe4cba8386b4e2787be5c25433c98f560ddb31 \ + --hash=sha256:4bd36c7c11d872e15cefa4951e12380a47e6901687165332dd50a007cb367ad9 + # via jupyterlab +kiwisolver==1.5.1 \ + --hash=sha256:007a5553dfc4f4e8d184f588a0200e2cd4b63a59cc8796df3c39909e679dc7a0 \ + --hash=sha256:0324cd2567259b7a095f6cf18a52b0ffc6f3de9e69528ff1bc0e7a37bd43ff1a \ + --hash=sha256:0627b9bceb9c3cdcf12b8a18655eedfed2692b038df27423383c120d0b7dc2d6 \ + --hash=sha256:06a6917674de9e0fe3f66f5430787f59a9f2ddb64af9b714eaec547e29ef5c19 \ + --hash=sha256:072bdb15a3c19a5b5dbc8f8fb1f4e1884bf4f3507eeb4cc6334401274d37a5c0 \ + --hash=sha256:0a4faea5c6db201c6a21391d2ac926ea97acf7dacdbc3c417189e1adb1a00837 \ + --hash=sha256:0ba9527afc80ae3d7814ed98b6572d02bf85eaf48065678342c5f0c6dab7a8c7 \ + --hash=sha256:0d8924877ce22e17326a99a418c3c82037da078df3c6a260b13eca677444e6e7 \ + --hash=sha256:0ebdef3eae5336568147c39a55be6a2036ffde53faa9ca2d978989ae7c2da12c \ + --hash=sha256:1209042a623ddfda5497e4066c7b77651dde8e1d3a9dd97599dc7e97f3b9b78c \ + --hash=sha256:16895f553ee6620a827d2da56b871f835fb70b9216cca5d188e885caf6e3bd23 \ + --hash=sha256:17851e5dad4484be0cbccbde3b15331deae036de9aebd45eed964487802b172f \ + --hash=sha256:1798e83840c3f627246104c4d8a9639c60fa068adf9ce92b61791781fa8a68c1 \ + --hash=sha256:18170a77ddfecf40ec60d0928268dc95880c881864e015a8f34094ed18b9b9ad \ + --hash=sha256:186884a58486651e3c217b6acea0a53eaa9498fdd472057c46f2f0fb5c25aad5 \ + --hash=sha256:18a0cfb124546a4c2e6087c5f3029c7f44b37c85b142e0ced71f73a7599ac208 \ + --hash=sha256:1983f0974a750a6f6556f368ba11105d1d8369c735b944747c9f12ae5aea7aae \ + --hash=sha256:1a7587dc335f2c0f5bd577fd0540bd16c66006bdb60f759a1059f025e6c4f071 \ + --hash=sha256:1acc7e5b7ef05e9da8bb70cd6c7c4513090213d2e1ad9720f599f0bf6c52aec5 \ + --hash=sha256:1d852545c4d0e35a72728d072cbaa59e2fa7dd84bdf01e068d670dd0ceb58eb6 \ + --hash=sha256:1ed0f5e49d0ceff8b72190824d9e59c062fbbc02c231b853112c78474b3f5ec2 \ + --hash=sha256:1fff05e239575b1481b6ed1a782f6fad616efbf1f0b1f44e6e85c4dfe426e483 \ + --hash=sha256:21e46b23a2da695c364124817bc01d970effd5483147f8d66a6a7167e3f6b851 \ + --hash=sha256:22d5e5aaad6be121f2515765e3b1c444352cb8eb4c86510801db8f2e50757316 \ + --hash=sha256:2551cf9917af48ee7c4b29cc82320489508cf96fd26a51f6fc124de661cd44c7 \ + --hash=sha256:255605693a483db7bd5c79f60437f7bf658f7f520d61aa42722e32257c941951 \ + --hash=sha256:26e8268480be5061d509e29669d59103c067a26377a56491630ece11762e3858 \ + --hash=sha256:27add358abe374ebaa3b8763ef380bc99051b5a4b18d94878366a9e4f59efef0 \ + --hash=sha256:2ae70bc59790d2af72a3f76f24b272403e135070340281108b447cb77ea70819 \ + --hash=sha256:2e10ae1bba1899188b33557c10d73affcc12033edd18adddb57d209039976a4c \ + --hash=sha256:3221f78211074f561c44ca42eac0619828171bec15a2c4cf6f7747d07df76e8e \ + --hash=sha256:34633ecf50d16187ab8e5528b7a2530f2feb4e23f300db4672538b51cfc5cd38 \ + --hash=sha256:34ec467940442c9943016fb2d4c81d1ba84351eeca2f1a78f8bc87f1ba0d414c \ + --hash=sha256:37f801b5d7cc0e5a548921308e059fd2b057bb42972b591cfa3049f95423c4ed \ + --hash=sha256:38f6e0deb4d0a4615efe0c4efc5990b06ae450ab50a0b321c0b078b6d238c083 \ + --hash=sha256:3c24cd69455e1b00ddf770c13b6e2c33e07d6dc3f2d34add0bf9277c5c6bbd46 \ + --hash=sha256:3cc210010fd2f438a3ed430b45f1b501fd13a8618bf984dc2c5ce5b69b78752e \ + --hash=sha256:3fa5855898f6d3d01b72ccd48a2d65cbdee301251603fefe34e2025bddba219c \ + --hash=sha256:416ba7ff9f233b7036689bb5a3783537e838ad483f63558d2a800f75afe738b1 \ + --hash=sha256:431dc224a1a92a5c8f582d96e505196a3b5997a7271076678da2dfde67b77e9a \ + --hash=sha256:43844c1a7ad6d723d5b5b4c4fc7f5bd399c40e288120d16257c7c9e8765c6e85 \ + --hash=sha256:44b8faef94f1857e77fa0238f3390ff1ac51d2ea20a487e2e452a59fd2b5f5ca \ + --hash=sha256:470d420f98d368d6f010633a20659b544c5fdfa5329e6b70219f2ef08fd4a7ef \ + --hash=sha256:482676e5bd48d70ac99d9fc78863469845421e01184fa83f1f9366dc49f7e974 \ + --hash=sha256:4d4ca09bf13cff792b1884f64b98ee6c2467930d632233be25c56b442d99f10e \ + --hash=sha256:5025e36fb4fb275cef0a4e30dbb11cb4ae61d1c83deb90189cb5d7e4cafd6b55 \ + --hash=sha256:509735237ae0d849e8a843551d423d2500d2e0a9ac1611a145658b29c0fb9f85 \ + --hash=sha256:534f02c1abb31ed6dbd3515545285c330b2f12d00fdb1fdb71658b9ca5a13a6a \ + --hash=sha256:5978c3340f16a35c30f8ab2fa7bcf559973c55f1a5ef6970e1f621acf3c4db13 \ + --hash=sha256:5b973887ff782cfd6b67c9904ad8ca542e0bc5e4961503408b423b5a688b4d38 \ + --hash=sha256:5c490db2168a508088f59140dd392556a54b8bd1048fc6383c8baff13c359673 \ + --hash=sha256:5d142e352eb13facc7dd047489aebdff6ba78576c239f1ea04931979caaf0567 \ + --hash=sha256:5daa1f19e097050b9c4d9a78fcc9263cb96c9dfae08037ddc1b7c4ad1889f2a2 \ + --hash=sha256:61e9a64c7635095a6bfe483e2ff055d437c59bd45f3617a228b37277f0185d62 \ + --hash=sha256:63fb7294b768f444eb4b068965f2662f28c2fd4161e23bd60fcf3ff27b74c046 \ + --hash=sha256:685929988b208a911f1285e2f8ed54210b0d681a3dc0f03e00d599d291986e7e \ + --hash=sha256:6a797a1cefc8b9c93170db580337e1fe3d011ad18b1299943231279406342048 \ + --hash=sha256:6b92f60017dda7d877fdc546438b5e28f31c523264f49cf5a48c1d0ce1a0dfbc \ + --hash=sha256:70ed9a45c7484d2b30cdacf60d220f494a1763b9fec1ad03285c6553fa0889f2 \ + --hash=sha256:719a35fa1156db3640555f95ebb94f60a444e64d1c69626b0edef5df78eba225 \ + --hash=sha256:74ad5c3dad54a4641b4c28cd15ded70899d04459c6c7aeacafea716be97cce6d \ + --hash=sha256:74ea337e0ec3f6f342a36a4f1b5cd94dd9affddcd28ba9aae2905af932ee8c6b \ + --hash=sha256:75d9b1cf8258462dbdc1eeda718c96ea7f079324c09067f6daabfcf37712b7fe \ + --hash=sha256:77a4c8187a5948d7f8795adb765a3c7b553d07d86d88e43038fc32fc1fb9a3f3 \ + --hash=sha256:7824b5e8bdbf0bccb4ccd37bbb115849a1dc45437fb4de8351385ed07c437ee0 \ + --hash=sha256:7d38b0c279c3032e8c9cc013b405c6df8e1668dbf15465779aa7f15f61201812 \ + --hash=sha256:7e9c01d3dd7ceba4d1d436cc021d40d592466e40b9bc7f5d83dc4e98a5c9cd8c \ + --hash=sha256:7fd82debf43c6acd0a94359d232f6bb516ee13f269a7993736a9ac9f988bb5d9 \ + --hash=sha256:824c3d763a05ea9e9003610145186b0e9848c7584a5575c79bac5a8e7cd80bad \ + --hash=sha256:828f75af2b0080c8a972e75f649ab46af008e92c6104a57a759157200b835b75 \ + --hash=sha256:83f78128fa28705fa85d01c59771c72fe81c11bd0e6155edbb9f818983a7d761 \ + --hash=sha256:876bbfd276473d3daffe30e8c975df4ed9429967b41a6cb362dbb5155b6f13ad \ + --hash=sha256:886fc26012f0e8b5f69d1cfe6d711f6b11f194621539bf8e6bb1c25c5dc82724 \ + --hash=sha256:8a34616dc2521cc8dc1d7d081734da63539f021ac0450ce950908340c6e7aa2f \ + --hash=sha256:8a708a47ade1fe19e8371d5da076bac0dd4b0a5a7985ad6c637f7f7e361b6baa \ + --hash=sha256:8af9b142ad719ae3a911ebf616bc4b78b32bbab84d6a40d3ad2f129670509957 \ + --hash=sha256:8bf4df63592c2a66b4f8edc5df2544998c288aa02f96ce0acd880cd1de8c8127 \ + --hash=sha256:8de6f2a4ce7e7bd27d23dd94abf0ccafe0e0e5cc9c764b0577191f2c25f08f26 \ + --hash=sha256:8f8fddb8e323bd6eee4e54e69a39243beab22689070f4c66b472c4cc88bb89d8 \ + --hash=sha256:8fca690b00c4c48f6c2a547b0160ed511357093a4e4c9b47e0fadf3128066d89 \ + --hash=sha256:9506e892bcc3b409831d363c6f53e5985e1c8d1f6f6b0256d00358684ff85378 \ + --hash=sha256:958254518717542d02d0688d0d20cbf771da5e415e6f49543f92481c850a4540 \ + --hash=sha256:95a02752aa032eef4aed01cda6d9b687c669bd0396bf4519eef8bba22a286720 \ + --hash=sha256:96c30002424670b5e1e46495c2b8cbffef39cf77c1d79e76462029d50339785b \ + --hash=sha256:98b208a7cc42c803445ef551d6753cc42a5ea13e9cab1ee66cd8b9cb70195330 \ + --hash=sha256:9b3092d8992a1d69b7a59c3e39f35e1b9be327a17f68a7c35fc17329e337d6f2 \ + --hash=sha256:9e51c119992ea8820706871c30a4642ec76de20ae82f9b50b9a45517d8e9f810 \ + --hash=sha256:a5716a33bfabb2c6ce27b6cf03253467b3804f83e215f4d202685cf93c6c9874 \ + --hash=sha256:a5a00665d1a0e26763a7338d7e911d4598fbc1d50dd0d6b7919b7dc6c5d6569f \ + --hash=sha256:a5ca5aebae78a0bc13c1943af4af615d4966c5b650b05d5aa83b50e427196fee \ + --hash=sha256:a7b85b2cc6ea45e5f7e8c9a30bc9fabd47cda09106cbb4b967335c3e6c43b69d \ + --hash=sha256:a83ee7107df13abe42a54a6654670eef9bb39425cf2e27f65e0007465e1286ab \ + --hash=sha256:aa7d00b1700966d2917e54d278aba86897890ca9276dd8b76cf6446b6c181b92 \ + --hash=sha256:ab620eb663952455271ac37f9aaad86b73c969c02f11f53cea405b38e96a4300 \ + --hash=sha256:ad8b9671348d7c8716715652ae11f85ed0eb99e265a2df2ca490577d69860b2c \ + --hash=sha256:aefe930d113798330e9462f7874542977869c0613cba3262e2de3a8d5dee8f3a \ + --hash=sha256:b03af77d77e50edba2030fd5f7c352ff209314b09030a3cba7c14edf9a09a444 \ + --hash=sha256:b390aec180a7c054919c04898835e1c77bced23ea8383eb2c570213bf25d1a86 \ + --hash=sha256:b3d78f7bb2b9d9a30345be1474b9aaa8685430b54afb51ba3639b5c6c11e9ed6 \ + --hash=sha256:b5664603a253efd3a75716d793d1d3a6a82723b61dc6db767b2460bbbeec4c0f \ + --hash=sha256:b69602970994a2ed8bbfa78c2f0394a7435226c6040489702d9f0a0ad0c07052 \ + --hash=sha256:b6ae6a0328f0bc035741820fdeecdcd67bf4694eee03972e843663107122f450 \ + --hash=sha256:bad20d4c69c851c982a1e3606f4c293edfd5a87885786c50082412240c4b1ffd \ + --hash=sha256:bb7c99f0673c03017a3ee01e54a5c2617a05468b11eabe513b0080e063ed95b1 \ + --hash=sha256:bebb89489b279b2f5661bbbb2abcc87bcd4a46607bb4a5c966f04f1db6b8df9a \ + --hash=sha256:bfd1de989b3330420e29de39352f5c049905c9e3ee67233a50d550e3d652c148 \ + --hash=sha256:c2306e8bb53601979fcb3fa09cc65e031876d9ae01eff2fcbcd7a84ef94d5bc1 \ + --hash=sha256:c3a4e41e3096bf1f0f1b76e2ffd6d828d6547f574f702d59bdbef7acfa59db9c \ + --hash=sha256:c6834b92dd2428e2dd85ef3d85f723d3c12f20aaf43a2ddd4f944ca25d833408 \ + --hash=sha256:c90d3022d8a94778939cda8638c6c8da8fa757b8958dad7ec868ce29c87681b8 \ + --hash=sha256:ca307d6c259e5c98d3cb9ade55342b47a6839762caf2536f3d7b46ee660cc82e \ + --hash=sha256:ca7f6fe0f37ca978a1e5eb7a3a68e6413f417e78e838324947ffd420202b198b \ + --hash=sha256:cb6fae641357ed2f6e533c0d3c6504a4a5703621a50c89459e46051d56b61140 \ + --hash=sha256:cdaeeb6c350106df6bf9d873395973e5f066a9713200b72cd64f55d0a3eafab6 \ + --hash=sha256:cea20da04494e662b83c872683bf4ff2345206043d036315ed0e924b652e7294 \ + --hash=sha256:cea90547bfd93807e0013a004dc76552be44fad3bc1cc2b38610a9e889ed098f \ + --hash=sha256:d09037ca068d784ebc4aec290ef952ca27ac15dd9c0b5801a88c6e1096b83e6b \ + --hash=sha256:d27c2123977cb9269c30a49ba45f03a4323017ef693e19db4ec9dbe1299a3002 \ + --hash=sha256:d50de98e8d807dc31822fff96f50293163a62418eb65487a21b42713d72ed0b7 \ + --hash=sha256:d66a64dd5dec136040ec2ae94aa026a912ee60fdd45bc28d3db30037fd809e88 \ + --hash=sha256:d79308fa689fac89cbcfbd4dbfc80b5f95c54c5a7fd4d194be221f9d33d026e6 \ + --hash=sha256:da3275833be0edbaf4830fae08bae3dc7219f40ce0c37eaa6c25825957e06612 \ + --hash=sha256:dc1a26b8e53395a01c2c611e58602fa47461f136fba7cd5542e6db6d64be1839 \ + --hash=sha256:dc23390afe9f4ef9ac3bcc72a03a56eebbde03f4c571a32cb38f859cff9a6524 \ + --hash=sha256:e05c2f7925f1d88778e53cb44f14e0223204a3bdd09a41664750363acfb1f2ef \ + --hash=sha256:e12dfea7f5fc2a34a9080efbf79c4c44eb380ec5b9c6fea09407e08f0d1e941d \ + --hash=sha256:e4e4523d6f336708d732516e6cfca7796cf3d96c9474eb5aecf6165f2f1fefc3 \ + --hash=sha256:e4e49f7e1a4e7191bdf9dc67a974db714501b1fc52c24324103d06a86abd5c08 \ + --hash=sha256:e68e151428b5384f766cd25739bf77c7e4a3dc93b5ded7a12118d9fbfdf78ab6 \ + --hash=sha256:e8e4d953faaded9ec7ede36824e9814082d22d4c7b1eafbfa079ecba8cd0d076 \ + --hash=sha256:ee9df1f0d77b9c6e94f4ac0fec533fbddd5ea3a327807f18d7b069ae019ded80 \ + --hash=sha256:f0a887b6565bbfe80efde2b7f6e8890d7d9bbdb11bdb17028a3690c32fe0621f \ + --hash=sha256:f0f4a42db92d6ec7677ab9d12830a2a8ec145a9c6d15db2b593466bc875c78d7 \ + --hash=sha256:f1303ef2eec81262a4b708c3e858afe58d7c75ad91c1c05266eda7673369859a \ + --hash=sha256:f1d56ec54d257d05e0b50f5780d967540cd07beeaf9e5f645b26d50cce79f4d8 \ + --hash=sha256:f4167e87b397f273dc2356fcf1eaf50a6bac51e6105f45103ef7129c8efb0255 \ + --hash=sha256:f76fc85bd054c806960f917ec0f329e24e436f1712267d90588e4c39890caa63 \ + --hash=sha256:f942903fde7363d1d879057ec5de01310efda2597161784d752fa9953a01a71a \ + --hash=sha256:f9b1c4900736e489a812c529100de4b8fb617d4db075e931e213c57424b83d9b \ + --hash=sha256:fc271a6f0a2126958f4090e5507b9da5848927dae331f8f763bd4aa642b3d2cd \ + --hash=sha256:febcce10f2bcdbb80b4ea919238a6a4ac13dbc4c7cadbe8d5d75c3682f8b5404 + # via matplotlib +lark==1.3.1 \ + --hash=sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905 \ + --hash=sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12 + # via rfc3987-syntax +lxml==6.1.3 \ + --hash=sha256:032a0a97eed428bd143c75a11118238546424ceb2fa311cca5f073aa44658dc4 \ + --hash=sha256:05f5bce9af14fd1506997594bd81cee6d9c6b58ea80a39c058327aa6371ed9e9 \ + --hash=sha256:0794e04ba343852c6d78e996c58ef4b8e579b4ecc72f8df0d4058bf843b4c96e \ + --hash=sha256:0ab2467e405e748d93495fb5568e74044802b8d3ff2b2a1607c3f78c6e982de5 \ + --hash=sha256:0bf5a3e397df2ec4258eb5eea4c1ac6cf013ca1abd04a176903bff20a70021fe \ + --hash=sha256:0c0710ac085a157b593c38fbcacd950f15c4afa8e2057527185875ab302752bc \ + --hash=sha256:0dee106e9aa97fb00541b1ed7827070564d0549c3d3fba8920e6b20fd980f748 \ + --hash=sha256:0f17d83c48ee9dfd96abae3ac3e2108c76d2fc86ce96355e37b8da9f7f4ecc08 \ + --hash=sha256:0feebef8d0521188d0157f758356072e840173aa61ca45b8b3f87959ac283dd5 \ + --hash=sha256:13a620a3fcc20023f9e6ed5c383e00e826f1c2d5db554df2f67240760f9118e8 \ + --hash=sha256:13d22c0d57355366b393936acf6b98a5e0edeadddd3fccbc6a846c50a76b8741 \ + --hash=sha256:160fcf381f76c3aeac28a756bec44f48942a8f7245a87aa28e3a523b4d90cd87 \ + --hash=sha256:16148acd77ed1d8836a56db883af2f5eed720f9723088110b16a0d08582130a6 \ + --hash=sha256:170773d8a3cdc76259065523ddd978c44f9806e28605f08812e8f86783e44ac6 \ + --hash=sha256:18293f8a8d8b6a8e71ef37706b659e3846a4261232158167b1ddf35f6994f633 \ + --hash=sha256:18a4db52b5a7b53a3540b0b0f4123319334621ee8083d496de314d0bf06ff59a \ + --hash=sha256:1a635e837b50a1819bebfedaac5916498ea024120969da8790500148fb0a894d \ + --hash=sha256:1aeca87830c4fe649dcf93fe2b059525b71c72587f21be4ae4af7103082a79fa \ + --hash=sha256:1b7c37339d7e75cab9a123a04248e243cefefb302ad6db566ea0c77cbcde421e \ + --hash=sha256:1beb0f9909b26cee938df9ba56b15252a84429b1fc30ce6fca161390b9789a70 \ + --hash=sha256:20384c2bbcbf87180c8c61eb60869699c1ec0cd09b62cfd13804022d860b0867 \ + --hash=sha256:20428910dae17a1a93152a3ff2c0441d2f4932992c0797d65651dd0561f1792f \ + --hash=sha256:207dfc3d47cf0e575e643bbc140dacc8863b39abaa1e5307cd64c7f2365b8a12 \ + --hash=sha256:209c3ccbfe35a04ac6d24f0611f9d1cbf8025d49991b14acd935236234d6c156 \ + --hash=sha256:2123e5aa075ac20d23c7af489255efd129cbfe190dbe88fd42598cc9df3199b6 \ + --hash=sha256:21402998e4b78e7cce237d2788841aaa21ac9a4d1574d04dc2d12ee41ae807b5 \ + --hash=sha256:2221e88679d1351e9a40aaee54bc65679b9795bbd0160bc3d5e36b163344eb75 \ + --hash=sha256:22eec57e26c418cde02c051ce9914a365e52a7f135a565c6f0480242aeebab48 \ + --hash=sha256:23c366231259cd75ad06495174701afb3fcb36a92917fa47de2d1f1bd9d95739 \ + --hash=sha256:25f4118c438f96bb466e83108506d03d5c31b1bd2387e83e5b070bda6ded9c37 \ + --hash=sha256:28a23fefdb345b2d4d0ff2860571b5ff9a89a28b6a120f720e8fb0324d346626 \ + --hash=sha256:290f66b97ede0e552e1cb44a0fd8a74f9753ee635b50830a0b122fb72788d015 \ + --hash=sha256:2b9b1325ca1c2a9a2dbb6eb913ae563313f2082ae60b03210f7e83ee80712274 \ + --hash=sha256:2bec13085dc8ef48a3fe62f7dfcacfeda2c785cdf19cc8eeda2bb9ed081da165 \ + --hash=sha256:2cae5d5c90a62d9139c512a0cb1aad1d182b022b5740daea2617eb5bf7fc658e \ + --hash=sha256:2e01125896585139453cab8cb235893644d8815d7509520da95ae3ee8d1c1f79 \ + --hash=sha256:2e62c569ec7531b679b184cbfe335c501c1d13c4b363560013019962eb630e6d \ + --hash=sha256:2f5b2a2b9811b853b39bfa41367c6d78747b8e3e80e07fc5a24aae295c1a4d7d \ + --hash=sha256:302f72413251c03f671e063c9414bed5dc8c927069e5abb69245521e51a4e81b \ + --hash=sha256:32a409be3190b088f960ac92bfedfbef2f86c49ff940765e1548177592d20026 \ + --hash=sha256:33cadd956b667997e4de1635fce9541f2e8ede2038fcde8cf55aa14d571d1bad \ + --hash=sha256:379f8a75cf6eb7eef0af074b55f49ab73b868388a98de14646abcdfa4564bb11 \ + --hash=sha256:3847e71a78cbbc1aff955dbbbaf2fff12153f611d3162c5beaa3395636cbc2f9 \ + --hash=sha256:38fc4e4e4e084e0bd491949482527d406788045c546d4f8789e93fc527b91385 \ + --hash=sha256:3a27ac6c780c8b8a1cd231b58407634cafc1c4cc28cd6c7141362df0f36351e7 \ + --hash=sha256:3a48093cdb058a93af842ede9703520e810b05dcd0fc6d7190a06376c3bfb6bd \ + --hash=sha256:3e42265103fb385d8642a78672edf376c6f7e1d3598a7a4f9cb1278f2f6b5f6f \ + --hash=sha256:3e9a00d1c2c30936f7add097c41afc5da6556c580909104aafd382cac92a855c \ + --hash=sha256:40983eabefd13da003e68170928c7acc011f0d095eefce5871a3c71c9385fb9a \ + --hash=sha256:40bcbd9f94166ffe925811e730607385cec959f42fb1bb7dad83748680465221 \ + --hash=sha256:41096ec0740a58dad03d3ae0c7486d306d20becefb13ceb1649835ab3eb64167 \ + --hash=sha256:415e3a115c0d510e329020012834d1c0aa1c581ee53a218603e38abbc1dea70a \ + --hash=sha256:41e2d428110b408e963b6fb18f9bbf1f5c027b56bd4b498d54556476c0aeb1c3 \ + --hash=sha256:424aa5657141d306ba9ad1baab4b2c0a0719040075ee6c66aee9bb2dea2b5054 \ + --hash=sha256:42632b4024ab24a6b488f559ac851312509888b6b80ae2aa11cf29a646a0d245 \ + --hash=sha256:45222d94ddd511536f3b2f7d9deae3b2339b4ce0f075f1ca25703b07cad9dd21 \ + --hash=sha256:4736e6c87e603146d8949d8501da621ad20c31015060d3fcf95ace2859f3e3e6 \ + --hash=sha256:48542c9acba9ff9450bd18d871d2c2c8787fdb283572b623d206f1b927cd7d9e \ + --hash=sha256:49fbc2682a9306135b7ec49e93f97f9c26689b9b7f96ed2742d8d6497e994d13 \ + --hash=sha256:4a579dfb9c835f8ab47f4b8ed33440cbc75b806b73297208e6ec2a33e903740b \ + --hash=sha256:4b061064b4a2fe8598a466d723d43dbcd5a610a5d5cfe02fb6226f5c17349f75 \ + --hash=sha256:4e11e885e0704be185867fcf71b904d8f65d7d6877bc121f69870b0d0479ba7b \ + --hash=sha256:4f4db7c7e954d289d71878938348b3d91b904a3e8210a11939359fb758a58e7d \ + --hash=sha256:527195c188d7d0af748cd48d220ab8cdc5cb99be3d49ac4d9be7324d8abf9bc0 \ + --hash=sha256:53258656846f5c48996b882fb4b135885e088a3ad3d96b4bc0530f95124d1f69 \ + --hash=sha256:545ccc14fb05485f48b4439ec35beb16d5b5280eb6c81c658bd4707a2a119414 \ + --hash=sha256:5609efdb0d3c95499c00046bc53648b3482ec2175b5503d6e611b3f0555dc71d \ + --hash=sha256:5929d9df5e7e3379183be0e21f7d559618a5b61cb63280df6164019242e337ed \ + --hash=sha256:5a143e6207579de8baeded4eaac9134413200359f1969d636f0bfb98ee8c3c8f \ + --hash=sha256:5a721a98c649855963811b59b55755b30566e7f7fc40bdc9803d66dee9f811cf \ + --hash=sha256:5cffe18571ccc51d742cd08cbb3f8b756de9311d18c7ea98f5d92f37b8fb60c2 \ + --hash=sha256:5d12669a2c419b0e8dc423d23dea24bb82f6f9cb829f32e04674b0ba40322a7c \ + --hash=sha256:5d582042c69857c364e8153de6e18e0da9b7b515a6a8113caf69a6ec8e0520f2 \ + --hash=sha256:61116cec57ed69aebc70f37a545eec095339bb829efbdabcfb97c51e9536e158 \ + --hash=sha256:611a51e61c92f62345a50b0035df6fc0d678f9299f33728826d831598862f59d \ + --hash=sha256:623c8799c17128753c65699f1c3aa32402657393a9ad6db09ed8b98ddf76611d \ + --hash=sha256:6374e9e382e5a98c9c5e66d41b357b470da1c54bce30f17f9dc4bcc58436cc1c \ + --hash=sha256:66299564c046bc7e0cc5de5106601eae907e9fa5904cd68a323380a8502f7861 \ + --hash=sha256:69cafd61aea04ebb3502c93c2aaa568b12931ca0802231e0b5de76bf8b6e74bd \ + --hash=sha256:6a406d0b3cb207b0fa460ed4dc93e866f44f105da0169361cb18ff998a44c7f0 \ + --hash=sha256:6ba4fe5bfbef6811a8e49b3719cde373ad399006c0c1ac184b7297116ecbba5d \ + --hash=sha256:6cd11e7550d89e551a87dcec30f04b1fca32e86b68708aa01a4daa455d8605e5 \ + --hash=sha256:6e1eb8a4cbffd5553680ad96be6680e364710656eced73d1dc90ec489df599a3 \ + --hash=sha256:6ea2f13dce778ca072ccee598bca46a092ce192e8fd907b6c1f0e52c800529a0 \ + --hash=sha256:71532ebf30be0048a45559b4fab15333fbaaf9042f658e878d918ecd0cf09805 \ + --hash=sha256:73fc05988ed20809450474ba760a87c8ad4e455fc09783c02195e56ec634b41a \ + --hash=sha256:75cc6569e86be5785b6188ef1642670c6adbc984e81ec35e224842ecd9eefcc8 \ + --hash=sha256:773062aec2f2e56b2b22d37054123f0de8a22a4688a0c3376c3fe42685f975cf \ + --hash=sha256:7ae4949f212a53b007dbc355884fda122545c5764a54256c9217e419a62a6559 \ + --hash=sha256:7b2bb7d703bed7ac893bf7f40d97b5d9279d35d2ce460624ca28929eab0d5a3d \ + --hash=sha256:7d0f5976aa2701996f759b30172925829867547bb073af0ae67d1307a0f0262c \ + --hash=sha256:7d5a748d12dd9b535e0a130f60dae9ddf0adafbabe61e7864f55c7436c84547a \ + --hash=sha256:7dd624c1eaa629ad44b59a1a0145fdf2d67895592dce94c9358b938b3d075e65 \ + --hash=sha256:7f75b9b9fec2a9c6b18095c81865580e795b1441c429e42d22fcc82a77f40039 \ + --hash=sha256:83e3a51e7933db700a0da0db31849db3a24022d9970da9bb73001e1d0326fd92 \ + --hash=sha256:8499d464de86fab0f102313cce32a9bed9ab1f06ec813cf025cb790964fbb765 \ + --hash=sha256:869dfcd4d381cb0ea87085cc4f011b9171b494ef21e76ad8665f6d5e2d1dc8a1 \ + --hash=sha256:8753b8d51dbc86fd335ee31fcf7f3658e9f5c016d4edfb23f76ad295f4b8c9d0 \ + --hash=sha256:887c021d9a977cff89cb273047c1352997b772a8908a25c21836861f69b92be1 \ + --hash=sha256:88e719b9437f148f7e1465df845c758dd1598618cbea3a2fd1e61a715542f2b2 \ + --hash=sha256:8a330c0ee5fa318c7b5cbbaad882baeca3f570357e7eb25ab34bf31008150758 \ + --hash=sha256:8db38ff3fb7aee7d6a82ae4da2eef1178656fe1216841fbd24870062a9d60473 \ + --hash=sha256:8e49a646acfab83c68974f4aa1d0a2acca9e88d7d627ae0fc13201b14b76d310 \ + --hash=sha256:909f4e927bb051f7740d6367285fc60cdcfdaf0258c2dba4ff5ba7eadadc250c \ + --hash=sha256:90f709b9accab6b2e4d14f5c8718203877a0486bcb3afd74d8b539ecd1e961d4 \ + --hash=sha256:92d96586376fb79a33474797186bf993250152ee5c32650b67db78d54b92e6f3 \ + --hash=sha256:93476b6514b373fc6ca67d26c442784f7807c86f00635bfe79f935c3eab2af17 \ + --hash=sha256:97acecb11cbc411473f15b8d780df06d7a9f3a2aad9aca78364f56640c8fb70e \ + --hash=sha256:97ce49699d87ebf8aad631b55d65b33219a4f1bfefbbf5bff19dc9af160aeaf9 \ + --hash=sha256:9bde9ae026a55b9a192078dfa6e27dd0ca4a050171ab6272e92f97b757dfdf48 \ + --hash=sha256:9e67324961ac9bbe616cce5100514d2e34d88665aeb07071e8b16eac55d06d94 \ + --hash=sha256:9efe56a68179f3adc4de41861c9358931db03837c48dd5e1c78077b84dd07f3a \ + --hash=sha256:a1932d7ce78a561367512c594fe66eac2b2ec9b9264cfd9b5f950622f4a116e2 \ + --hash=sha256:a1cec0f99b9b914d39176347a93b7610dc09324491aee1cbc57cd291a41a1d55 \ + --hash=sha256:a2e3f70673a1d5b82f38255f777d26cd855bf2092b1436c4867464a7892f9238 \ + --hash=sha256:a43b3bdf11e477dc7770609d3477316f974354dfc8425d596f64f471cc8daf6e \ + --hash=sha256:a5c18810318303ce9afb3f95e2ddb54834f96fa699a8600433fd5a93dcf44c56 \ + --hash=sha256:a7eb78ba28b187e1e9203a55c60fcf70df2d22cb205fe6d51b9383d6097419f0 \ + --hash=sha256:aa633613ff907ea91b9b0489a1f0da1b8725d8c6ccec6b77e8a1c9c235044bb0 \ + --hash=sha256:aa9fd1ee2a5dacfc41039ed49ffeeacfa75bafbd255b69f3b578e11897a0e623 \ + --hash=sha256:ace1d2c83b2bd24db5940600541140e87a325e119cb32d5fa9ad720d7e76648e \ + --hash=sha256:b1cc980905221a5d8b3c476330730b3adb40ff80add71ffbdb6215ba055656f1 \ + --hash=sha256:b37772102d44bb6628186accca3a121b1fa3a6b3d97518a8c29a5229ca4c0d0a \ + --hash=sha256:b3ff39654f0ce6ebd4db154211136dbe7e8157bcc3bed2344c87f32c7c6ecb6c \ + --hash=sha256:b477912f42c5c33405a10c759d22f80cf5af043ae02d95b9d8e5e5bc555739ed \ + --hash=sha256:b49638355ea3bebba70da783ccbc630fd72afa16bc46c54474bfa1f9a915bbc6 \ + --hash=sha256:b4fc6b03b9d9d90557274f571ab30e7fbbfc527955536935d96f98b6817a86e4 \ + --hash=sha256:b50343241eb69fd85f7791cf8bcc7b1c4729826b7d59ba2f6b27db29638fa745 \ + --hash=sha256:bc8dd3d9c93e70c3df974a201ac2958b6d77b465d813c51d1f15fa8e645763ae \ + --hash=sha256:be5346653c0b0e34be96869ff9dbeba23860156f89a2896a64c64fb419260cb6 \ + --hash=sha256:c00e26288784460885fe76e4d4b293573e0f791f52e6d60e27b42edf005922eb \ + --hash=sha256:c1b50797ac246bb2942a04b6c0f69af0667aba7cf7535f39bbb1b3208fd5d128 \ + --hash=sha256:c34ca1dc41bd86d9ff830d5bdf4e4a752bba6c54f7d2707027ce0eabd36084c9 \ + --hash=sha256:c55e71a9b1db1f107efb60da49c093689b74c5c31a708e5379e2fd9439d4fbb5 \ + --hash=sha256:c581b1d68b3845fb86c6b2983e755b29bf001461c59fa411d2c26a911b6559a9 \ + --hash=sha256:c59e4265608da6a041f54646ecc0c9ecdbb19aaf14c4c684bb6c2114998cc415 \ + --hash=sha256:c5e7ce578aa8a80910a72a8ca0bbea3baae10100827249001999726a788456d8 \ + --hash=sha256:c66f858b82497173f73366795fc6ee8171620e75a338506d6b2e7bc16f5fca11 \ + --hash=sha256:c6c0c13128a32eb04a51357e56a094e13aa8e6d3d1884de2e9ae923f6915e1a8 \ + --hash=sha256:c9389b3784b56c58d933b5e0aecdf28f901b073ff385358d8a7d40907f6e14b2 \ + --hash=sha256:ca0ec532ad2f5ba1e5ec120ac157769c57f01855b3d8bf37213f5d88abd9ba0a \ + --hash=sha256:cad7617727a96d189bd6f979d0fadf765198c7934e85f4edaba9bf3ad919a300 \ + --hash=sha256:cae82b5ca24b0c2beedb269f6e2a96f466acd926879ab00ae19f1a65cbf9ffb0 \ + --hash=sha256:cc669256d28736f7f3a149df5c380c50ace2692ba3e62203d10656fade4a2145 \ + --hash=sha256:ce1f220114959941170e22b8ad44279f6dee2dcef7591814d01ae805dc058889 \ + --hash=sha256:cfb398886a7eb4c719161c3efcff2a1248febc53a4d8e5072d2d8a87fed84ac9 \ + --hash=sha256:d077f21f4b16f0471353883748f126f62038760397c107bb9fad2ca94dc0dfb7 \ + --hash=sha256:d0c5c362bc94f1929dc7e96e715bbe7bd17037f802e6d8f0d1545df9133c0559 \ + --hash=sha256:d2765c18ce303149ee804b1f3dad11232726dd0a702d73a15cf19179ac8cc962 \ + --hash=sha256:d44442effeb8781f392340c5dc8c6716fba41dbeacb82fd4c0f09026fb5ff682 \ + --hash=sha256:d85dfab42dd672f87a7f76e9de7172962aee69fa12044f0d6e1a23cbd53fb80e \ + --hash=sha256:d97c5227621af74b111882a290b10f371780a38eef9d9e730408fba2259b52fb \ + --hash=sha256:d9a0d12846d6ce434fb3857918eef4315ec9b4769deb020c75828798614bfcfd \ + --hash=sha256:d9b3e7d71bf6acff341233417abbdface29c647e3113892d9aaedc02eb4aa2bc \ + --hash=sha256:da707f14ea3c35ee463d50acd596d6488e4b2b4ae7cf77a5bf93f55c023d63e8 \ + --hash=sha256:da85db328e507da922d586c3c7416ec360ec22e9cd9e0700691afacde0c81f53 \ + --hash=sha256:dc205732d593118cf701d986f40e9de7801bb2e371cb189ddbda9b7348f4d97e \ + --hash=sha256:dc3a44689eea43eab836e5c98a8ab015dc2419987d1ea6eafc7c590cdff86bed \ + --hash=sha256:dd5e90f34cffcfed97f36cf066325773d2b6021c60c29942e53a18b028501b1d \ + --hash=sha256:ddcf547bea2aee967d6a77779376a45e77e610e8465147a1f3d7e20d539d6e32 \ + --hash=sha256:e477aca0bc0d19f3b4ae9e4f2a1cfd687c31bf772d78734910658186b40b2477 \ + --hash=sha256:e8b17e23df3e827a69d25af70990ca2420e92668aaffaeeb3cd2351d7916a023 \ + --hash=sha256:e99e09ab7741f1281e2677f4c0058c7f5267d182530b09c87e4f6aa26adf3887 \ + --hash=sha256:ea2c01cdb16dc12156e455007c406dfaaece0c89aa4ba0e3b47586779f951d41 \ + --hash=sha256:ea6b1e9105b4b24a34c722432d9fb578f9ed83af21fa1abda639011e0f22bbb6 \ + --hash=sha256:ebd054ad1737a68fb7c5c073d405cef2b88bb824e294de3b4a4e995b47f0e376 \ + --hash=sha256:ec295280f4b37769256da025acf5890370355ac589c27e89caae0b5e9eedc702 \ + --hash=sha256:f6449672f9c93316deb5e2839e18931f468670e44d5bd9b1301a5a9655d45c07 \ + --hash=sha256:f683dc6300317700025e41d89a43e0276692ded16113a3c43eab704d605c58e5 \ + --hash=sha256:f6b9d2aad499c769ee8287609ab0e6de99d8bcea99c6e6c2e64945259fd52fb2 \ + --hash=sha256:f8b9c8ceebae6387d0dc77f7f4dbbfbfc962dba2efbfe6877486075a480726b4 \ + --hash=sha256:fad67b12ffe0f71e02b4932b04883cbc76a9072bbd30731409d3523cf058b011 \ + --hash=sha256:fbfb70ba01355251faf6b293171df49f73a88a1b6494db109ffea85442574458 \ + --hash=sha256:fe91993149523aa59941b9e3c90e2eb45f57ad014697aef6c8b13339a59c019e \ + --hash=sha256:febd35ef45f603c2d74b74655efdbf45e14f55fc0aef4ac82b663ca829b283e0 \ + --hash=sha256:ff88a92cafde90888511242d1c54afcc1a8adbb6dc0a88fa7f87e29e92400d4a + # via pymatgen-core +markupsafe==3.0.3 \ + --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ + --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ + --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ + --hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \ + --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \ + --hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \ + --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \ + --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \ + --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \ + --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \ + --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \ + --hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \ + --hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \ + --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \ + --hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \ + --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \ + --hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \ + --hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \ + --hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \ + --hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \ + --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \ + --hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \ + --hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \ + --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \ + --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \ + --hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \ + --hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \ + --hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \ + --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \ + --hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \ + --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \ + --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \ + --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \ + --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \ + --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \ + --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \ + --hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \ + --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \ + --hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \ + --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \ + --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ + --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \ + --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \ + --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \ + --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \ + --hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \ + --hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \ + --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \ + --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \ + --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \ + --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \ + --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \ + --hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \ + --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \ + --hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \ + --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \ + --hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \ + --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \ + --hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \ + --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \ + --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \ + --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \ + --hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \ + --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \ + --hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \ + --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \ + --hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \ + --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \ + --hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \ + --hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \ + --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \ + --hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \ + --hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \ + --hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \ + --hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \ + --hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \ + --hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \ + --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \ + --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \ + --hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \ + --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \ + --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \ + --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \ + --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \ + --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \ + --hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \ + --hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \ + --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ + --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 + # via + # flask + # jinja2 + # nbconvert + # werkzeug +matplotlib==3.11.2 \ + --hash=sha256:01dc8eaaab5a9fce9ff615eca82345728f289e4715b186ee10c6d85272fc26bb \ + --hash=sha256:02329432ae5c6af87cf208ee575b701d698bdf0b1a3bb28cc6d53c36e967e575 \ + --hash=sha256:07d9b9fa60cd4c393692f50d0bb03123242ddf61c99bb0e95e75feb354e7c1a8 \ + --hash=sha256:116cdb0eb0eb5644fc98eb2975d4b4dd4ad35e5c8e6b851c22e6976f371f7ab5 \ + --hash=sha256:1944895967f87c84c9b4bad29a707b31f5b36df4a3a2339ea1f8ea3ce5105539 \ + --hash=sha256:1a3040b209f3968b4e84161df7b174f07a9fad33b0f2d7e48ea3bbd3075e2863 \ + --hash=sha256:1b9a7ad579856284135e401ecc918c5f8a017ee30539298862a109f51b971710 \ + --hash=sha256:254d4ddb2fa8df3b4c689c0c306063aee10521df82cfb438185e499c75fe37c1 \ + --hash=sha256:2a8285cea8ef4d92aa041d1c33788bcae82248503300f93f1ca2136b9049452f \ + --hash=sha256:30ec15d7eefee71de16b7c689b42ba49715a4644650b76a6c7c70d79daf24e91 \ + --hash=sha256:399fef672f7046ef7d6a57572b2a6f9845f3f5afcff04e7b2df7a363a9f42190 \ + --hash=sha256:3aa4b8516fd26659e4363abbf317c703d9116496c5db2e9d0609a2866dd39dd2 \ + --hash=sha256:3da3bc0cbf7245e7db72cc6d29d12c5abef72cb73059c73b945f14e3545f3eb2 \ + --hash=sha256:3e8576f7c47e02fd4f21f44171302d2d1d58d4471d46da3d71fe8899d19539d9 \ + --hash=sha256:57b9ea60a835937c2012861923cbb91f47db8565775d326d1c42fc926aa10351 \ + --hash=sha256:5e1e923a3fc3326b99ec0a6ff1ab1338ac6c6cc62ad9d8a9c944197c7f8c6221 \ + --hash=sha256:643ff850d8e0f5b8319337f87ed3cb59506afb3df3cc48de777d85871233be7b \ + --hash=sha256:75b6d88402770e181b5d06a67dda4129c31da7d05004d63a21c310ec78d1b83c \ + --hash=sha256:79a258f58253dfa025af80a9e9bb228d75fced007f0e93ae7423fefbde81a74d \ + --hash=sha256:7d43ff8cebb50840648cb6429b2228621dbd709010f3117b3740abb20abf21c0 \ + --hash=sha256:7e5a90f8a707ebb6004a713b1cff091a40cc5df4c7c1cb165e5a505ebc11c292 \ + --hash=sha256:7ef7a53b66780e5d942923724f08577fbc5be1f7322da0f0f3f9dcaa45dd803d \ + --hash=sha256:854df8d7dfe9fdffcbaa6f39e44a6c24b409cb4d7561fbc09213b157d833f6a6 \ + --hash=sha256:894a9cbbecbe30ae6787d464df2e8fc7a8d475cfc68f87c3029f7c11152899b3 \ + --hash=sha256:8c8255de28f986d935a64c9ca71c0ec2d2f41d355691f5ea684725dc91413f71 \ + --hash=sha256:930efb28f59fda124e39265d177bab302625297bb147d2910de725a2fc2aef54 \ + --hash=sha256:a24d5fd36e4f0e742c3851dcd20810e56a95633a342e4bf6cb591c678e8fe61f \ + --hash=sha256:a6939df7567114b6bac7f4c5e06c84a67f197c1b2f2e4b234d4eecb3bfec9482 \ + --hash=sha256:a8756cc73d9af9a7fe0deb54ea2e75ef73b01d9e575877e72acad5458e660943 \ + --hash=sha256:af2661f6ac6bbd1d081996f54fdd9385715c625ace8cec0869055c0cfbf38981 \ + --hash=sha256:bbf1062991d826ed27e2144f3afa4461afbb8ff56e8f703043e191a8163b1ee9 \ + --hash=sha256:bc067c462a86f0e57bf52fc6e058d90171a5420007dac00c46e90153050c69a7 \ + --hash=sha256:bf3fe71fbfb8ec0e310e0bc8537c3405a01f38f25f9394ed2135e6202fed542b \ + --hash=sha256:c0b83f044ce10a98027b105b3931548719a6e8c7ef986b4362651e0b5367c8dc \ + --hash=sha256:c27e577ece613ea12a0790e00b4eb80d901c59a3e16cad474f31d8b1529690b9 \ + --hash=sha256:c5c1c68ee401fc98271263410f0e5ce88285abacf7627132914e8adf3d70ff43 \ + --hash=sha256:c9721f81275499da1feeb36a2cf8192ea086283b3bd16b7dc4c9d7aedb7396d6 \ + --hash=sha256:cc82dde2a0d3e3ad472edce04897ad7146b8d8bfd1df8a32992eebb81af18fdc \ + --hash=sha256:cec596316640f2b394b8f0daa0ea61a8eae82d017b620b9f202befb972a59ea4 \ + --hash=sha256:cf41ecd1b0c0b6f7177ed965a54c2afbe888715c7cf6054dc12d53bc1494002c \ + --hash=sha256:d3304eb5a59442a8867f6920d484591c0fa09ffc29e9260be2feec3351e25869 \ + --hash=sha256:d480038c83691532ed52ff3147db51fa902fc78cb2d8349993a1cdb684435bff \ + --hash=sha256:df4f7784aca81a94f254c0a2767d592ee25f407e488f5fa7203e51093fb6ca27 \ + --hash=sha256:e43b188f0a5b75447bcc197728258166aa64365770ed1caa36595a5e1ca4bbba \ + --hash=sha256:e60cf3047a51edecdc4535a9196bbd6a732936b8e9f8184aabf4d16165884aaf \ + --hash=sha256:eac4b07d4e3743b172451e122ea964f72f152879d4f9adcf3f3d33e518f12ead \ + --hash=sha256:eb3712dc9b464793de0a4e42a7313d50293c751f94bddaf7332a1bc71bccdda9 \ + --hash=sha256:ecea603dd2fbf8242fd31a305a8b12a4ece2de28096870c65fdd0d1e35b8d9a6 \ + --hash=sha256:ef31985c4dedb5f1424e1aec6849a47dd37689cb7fa3c20b1b82187f26806261 \ + --hash=sha256:ef752769cd962f39ea0b6ffc82d1ea43a0012c5a6157c7a075212fa509cfcff2 \ + --hash=sha256:f25446b2981717dca9786bac841cb3fd7efb568e3e3c755dd980481c5cb9228d \ + --hash=sha256:f2ac30cf5eb5dff1b584627ae0b0e1186551a4f69ae3c75073911da497a29170 \ + --hash=sha256:fea03cf56568cc1cba08b470be6a0559e71c3a5b688d54b7179bb35ba23d0821 + # via + # -r requirements.txt + # pymatgen-core +matplotlib-inline==0.2.2 \ + --hash=sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6 \ + --hash=sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79 + # via + # ipykernel + # ipython +mistune==3.3.4 \ + --hash=sha256:58b5c96d6fcb61190dfe5fae498d2b2065f99cf61e9649418fd54cf1ada86dfe \ + --hash=sha256:ee015381e955e370962968befe1d729ab60fafb6a715ac6751763fbce38c8d4a + # via nbconvert +monty==2026.7.16 \ + --hash=sha256:2c51224b6715794cfbbb8202b9749077fad8e67b238b0ab6f2abb9bde33c2f86 \ + --hash=sha256:fadd48f18135128a6042d7efe80e301d5658178d441cb1e80a7d6624bd8e114d + # via pymatgen-core +mpmath==1.3.0 \ + --hash=sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f \ + --hash=sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c + # via sympy +multidict==6.9.1 \ + --hash=sha256:006c4478de0a1876f4834e14255776286f09b9846b505fe63f67f9d173a9487c \ + --hash=sha256:024123f0ab402ab33828e24eb80fa8f25167d0d3783ba5f287e39ed741e6abf9 \ + --hash=sha256:02f6d0c4b70f783305e73f9944d8efe6be1022550f0974ba0ae9d8893c0350fa \ + --hash=sha256:02fe09dc197b8ae7e355371e51e5dce2f39060cd5a8badf94904527e23a3f188 \ + --hash=sha256:03731f6fc036180700c9dc2308205a48e5ca6f3ff03087739ab746f294022201 \ + --hash=sha256:03d47df72f084f757c1cb771188d5f4e3a805e4abc4d67e32509272343ae9382 \ + --hash=sha256:03fac50ddfd8302175b77863a015eccfae76767cda5506eef86df559ba861e1f \ + --hash=sha256:042fb0196047e786936a730bd302de83143950da45f2c16078da8f35e1cf7919 \ + --hash=sha256:083735b7f395894e43adb278d5dae901448883a835ff8f1977e285fefdb10418 \ + --hash=sha256:095d900c242e00fbe5f321ee072e7278b4153e78c5ce9c1efde167d62c1e4771 \ + --hash=sha256:0a5769559e3312dd96731fbe15b4abb6033368ac1cad5a98dadd21946a4c7d6c \ + --hash=sha256:0b2fb8c349d1103863750b5d8cb5ace766917f4b35f3d883c8f778853eaa9f76 \ + --hash=sha256:0dd655518f136febd96c05131a76a863e32fc2a1d7acd4e3c959e3ceb77d8345 \ + --hash=sha256:0f06e60fa190aa7abd0914c2a766736fdc8e9f34878c4346338534b73d1b20e2 \ + --hash=sha256:0f2ce963299d42fa3f22a90adc0fdf174792ffef5ff4c7ffb68260548fb05580 \ + --hash=sha256:0f3bd290711c6e9486173a6ee7cd4e7f00c3971c7908c1ec1b6e5437c5e4c6f9 \ + --hash=sha256:10083a8a0f4e1b26b599889e90b9802504ce5d3f7722f925bbb7ca47dd22a7c1 \ + --hash=sha256:13849a1d4f54c3809ae721e9e83ab28f5ea602f33660cb84eb6ef261eac706c1 \ + --hash=sha256:14c56f73e78faa1f68bbb826197cd5871994e70e841b8590829c35912ece5c64 \ + --hash=sha256:15db6a102cbaf1949cf028ecf080aac76d20bcd29ad4e092574db6c6b7af78a5 \ + --hash=sha256:19e31815d41cefc365489e591d105d2baceb2f65aa75d29471fbdbda8651e006 \ + --hash=sha256:1a53de2772cfb74559df2eb4456ec4eeb908435ec55a84b69370d9d745d62aa8 \ + --hash=sha256:1a8adfcaf96f587ab138476eaddef95f29b8a2a8a9afbfea8d2fd62180995d02 \ + --hash=sha256:1a938761c77e0e6edb0c93d02f4e988d44a69e5195e5a3b893e5553311347132 \ + --hash=sha256:1c9f3c25df6c9d3bbae4f6fd3f514b1c5c740a2110f56ccf36069c85417e289c \ + --hash=sha256:1ca5ebe6d454f1e5496cf052386a559f1080bf2de75bf327ebca0a6003b79f19 \ + --hash=sha256:1d804e4caf5d5da37d6dac1325da5629ebef1e27a294c2b568b295814aa36c7b \ + --hash=sha256:1fee9a16d88a1c4865610de31ef5c666671020d7a81d1f510eaf3c97d00ebeba \ + --hash=sha256:23136f5a564654eb61061ec6d5620a4c1ea32c8f552b65e9982a12b72bff601b \ + --hash=sha256:2784090c30a586d5b45197bd9c32f87fb927216cde302f6cfd76d76577e90f08 \ + --hash=sha256:29138fef49828542e859828107e42e50d0e587c513b7eb4b2d92bade2b0860fe \ + --hash=sha256:2c1aeb92eea59d824f004341b26d5e4b47a8a441cf9726769b9a90abf9d0e08f \ + --hash=sha256:2c5d675da8f1cb5650271c8ad5e95c0a3e5a183c105e72d953b12877b1c8d0fd \ + --hash=sha256:3100d169ceb7bc8f05f89a6db11d1b21f119975fc26f29dc45a472e3569f0879 \ + --hash=sha256:31199204b3ced121ff5407a2c342326a5d27e3870abbf94bd80dbd2451b7bc8f \ + --hash=sha256:32217133dddc58c927805cf6c0731d8144584176b768042ee886d51e71860bc9 \ + --hash=sha256:33fa55b990f81c2927e01399ace0d18926c69d69baa8cdaa819424132fb97987 \ + --hash=sha256:35ba0263bae5dd3ad5aad767cc9afc01a8598c7dae30f1b3b2de98b1b32c28bd \ + --hash=sha256:35fc236507fb1b3138f0af5ecd5f94ed752d4d6d826248eae425f86204013eea \ + --hash=sha256:361f7206cf341ba94fb015688f5c8b480f8e63bd58a4c14a48aeca7851a241cc \ + --hash=sha256:369b5aa01b241cd3fea6890bdbb11a1425d87bf1515831500d518f4223e9d72c \ + --hash=sha256:37245ca4105386194dd1d292a6f2aae09bfe1bd7ac6f9ec25093e3cf8e9b143b \ + --hash=sha256:372c063f37480f62c1ae32dc3a1a0a5942b180c883f99789075a8a8ec3c4e709 \ + --hash=sha256:37a9ebe00c698279213d56e6c64e1962ab1e092918270649b397cac3dc196ca4 \ + --hash=sha256:38c9986f9ce50c459b10de216a05f4bd7ed5ac63887d56e500a52bb464b861ce \ + --hash=sha256:3adf06c66041aa21eeb8a71e82379b74773298c8e6d3d839b151aae441a99b94 \ + --hash=sha256:3c95601ed98fad3f6e2f8fe809c3b526b0fab31ef525e00a155e227f3d17f58a \ + --hash=sha256:40aec5299e1ed71fbb988389059da381c3c1a60e0c649acceb2a35d9b128848e \ + --hash=sha256:43124fe172ada86d03ac3c8dc8179091341f6724d5e5d5b160e1587e4cd3761b \ + --hash=sha256:4736d350371337825cac1793c9f7c40701c32a03912548c2a7608e51679cbb96 \ + --hash=sha256:475d04d5192eba487a3e2f935976340baa24529046e9c1c9c7a3b7bf80445ae1 \ + --hash=sha256:4a409ccc42aefec904038695d5d7fd6d8f3af2721b7b6401d55135ec7d6d298e \ + --hash=sha256:4d718fa1b5f0d0dd75e86fbbc5b0c93ea3a5d65216c85c61cd5d7cdddfe08455 \ + --hash=sha256:501ed8b02a5990c67a91c732843609d43a6be1f7576fcdfc867331239f37fbd3 \ + --hash=sha256:539c2cd5fed0947c135cd7eabaaac55f48300dfa1de0f3ca4edb5efa6606f471 \ + --hash=sha256:557a4e1708df428ebe6c3081c83a273d275dad2446ce0d81fc648ac71afdb18d \ + --hash=sha256:56d834b74c993a7d7cb2b8ab33a0d55c3e0d4a3d2f2da2808a4ad3d79189711b \ + --hash=sha256:5800368526647146978389dfaa46da3356291e9f0fff9a4ef12e8c2bef964a0d \ + --hash=sha256:59c123d0e948d760a5f930f316cfefa07e8d632ab84327c0693ee6a88171154f \ + --hash=sha256:5b30ddf7234e611ca877575b62840e6af5977f92f1f9d532eedbb05a44ff8004 \ + --hash=sha256:5ecace251ccfa705bf3d7d35c5032cf750f5c629809740405697bce5c118c4a4 \ + --hash=sha256:5f89dad732280e7a10b74d40b91364f88e13c3f2c08c2ef83a8cd42f7a61af2e \ + --hash=sha256:637f4ae36264bd7b8d9a60193acddc1d735ad52e8ed53a19931ea6d921fea8e5 \ + --hash=sha256:63ada7ee2e9345695f9e9bc4c65d72222253f07b1ac94fd0e37555cc6f3c7f60 \ + --hash=sha256:6441cc837aea58be7d9baef1b2383eb8311ab9303f500f99ac90b584cd78bb14 \ + --hash=sha256:658f90f49cf5af2441cad0a2b801c3ef520471989a1ec55bcb25b255b2ca8d2f \ + --hash=sha256:66987aa68b0f7c2a1cc5f388ca962b8ed92b10f79de38d6d0d8c716154f519d9 \ + --hash=sha256:696477ad71385c4795e3b8e4cf10b0d2c28c2a1ca6a955e031cb1e62993e9ee3 \ + --hash=sha256:6b7e54fd883671d1a8810704851c044b7173287c552b9f5c3d9e0eb9f00ae194 \ + --hash=sha256:6bc94fe17c3c56e5418f79515b786b101845f70609b0d19d0c1ba13448e5633a \ + --hash=sha256:6e6b7e3a1520c39a2772f414cd9ddf5995f77e4e823dfa1522af383addd465a3 \ + --hash=sha256:6ed30be8918e18c8bed0a2e8b70639ecf02feb61ed00ca2e41cfcb2a50fa3f42 \ + --hash=sha256:73533644e1f69ea1164d56cc6505f564c6c44072b646b66d67df2d31fed0348c \ + --hash=sha256:73bcafa21a78d0776b3ee7cd2a63c66f968eb7db8e8d594e32d7329950f6e828 \ + --hash=sha256:7814bbee202acd3bd240204c17d8b87a4c48c81064fd8674dbe527d94d5a4290 \ + --hash=sha256:783ba7d845d79ce976afd9c1e91a4e5714671defa198ee789e8b23316083a485 \ + --hash=sha256:79da2491348b30810728050b4a8ec0416f85884125c2fd44655b3d01150d9c3e \ + --hash=sha256:7a0c98f6a636adf0d7edd60c61589eaf52d149239af754d0bfeb0effedba53a8 \ + --hash=sha256:7ab379f95caee071a37cbd8be86d4c65accc651d391f9f97748fef006f38769c \ + --hash=sha256:7bf6478188f4e47bf5686e8a33da4ae28bf43b1b2528d9ee144d28492bfac60b \ + --hash=sha256:7c6eecfab7ce4cd9487ff8ba936fe38cdfd68c04faf3d5c710363c6a8e695659 \ + --hash=sha256:7c708566da8014b120a64b1eb6d200c6c0c8cb36296383723cdb6fc82038270b \ + --hash=sha256:7c8d5882ba25ac0282258be435d8a05aa0cbcacfce15799154a338f847f159b9 \ + --hash=sha256:7fc59e9ba821b220944ccfe0f89c9dc4745f6d097569992356eb03869f21e953 \ + --hash=sha256:7fd79c521f6290c69125fa2b85fa65d9e657e6a8ffaf722dc881b926bef4aa5c \ + --hash=sha256:803f8b575a71b1b299d677c28db0653459c79b5308874efec813f17b7457c7f1 \ + --hash=sha256:8050e75af7e4c6e2d5260b84eeedb618f3e452e66473432e085b5d1b81429299 \ + --hash=sha256:81cdc537e0a42e3c0170752fcadbe450246d5c3b4b7231d6eb9672456605ac94 \ + --hash=sha256:827c92145b3b976b39430129c89d213b250dacbe5fce678e9a03940e6e848983 \ + --hash=sha256:854fd2f1bc6e8a56b89910b5cd7261a8b40f13ebb31572da985ce59c7da0886d \ + --hash=sha256:877ca17fdcfdf5c397493a71e5ff97a87bb181417fe717fdadc77c08c09301ac \ + --hash=sha256:87cc632c88ee5dc80e12681047839304d98ee5c9a708d686505767001c9b8b9a \ + --hash=sha256:8879510a76940670517ea1cb589978da44b86e286ec3e50d664ef330817afce7 \ + --hash=sha256:8885e3808aedbd6725b921fb67dacaae0678933561ddd47c2b01315198c70e2e \ + --hash=sha256:8b193bf7a443c97d81c47052f60c486071a4bdef4a573fa2514f920089414d45 \ + --hash=sha256:8ed78ccad4c7b421804f5d524b7740946a7ef75d89dc0dac52e9d7c24c472410 \ + --hash=sha256:8f2973bbd2bebd9d2e0cd6394c1292a1a19ccd56bdcbe1e174059f1a39be5b40 \ + --hash=sha256:910d4260512660484c0dc1588a316fbb35a40c081c36fc51d1225351af17cfe4 \ + --hash=sha256:95052e8777a86bae87c0bd0b5ab22d809e3d1d02bf69e3e66ddda5ba75a05805 \ + --hash=sha256:95f273bea318a194f656527ee2ed19494327bc500b8e87d9358e3222579aa28d \ + --hash=sha256:963a8d8f97057082679523d0fd4c53a38f86bc58cabe4556faef682ae53fa2fa \ + --hash=sha256:976fd7689d69ec78d67d31d38d396d8adb562f7e8368279f76aed4aa451fa06d \ + --hash=sha256:989261c5f1735a165f2e4e87cf6d5f17ab734fa18f9ad0383d5adfcdaefce701 \ + --hash=sha256:9ae9614c317c50836689ce2dfde07c05fe0b16378562e2221746c3913ede3c80 \ + --hash=sha256:9b0124b9c17e9890f0819b2e7a5f65ec9a2f5aabd6c8e7ad090c1675cf70dee6 \ + --hash=sha256:9c4880d017555d70dea367dd49271830842d48e3891c2da97da7ce8c4abcee40 \ + --hash=sha256:9c62e71e6289d78c0108d8aeb495f8bd3cad4bc1632fedddc9297ddf287ecc20 \ + --hash=sha256:9d0a21cf76153de8f2d96a877991d6bc59b9ab5180b949e50db73cc193b4a694 \ + --hash=sha256:9e14d17773b1b3c758ff153659a1824608a0cb562c45f484b5ed8a433428444a \ + --hash=sha256:a16a1dc8529f9e734a41c3b856f3eae7ebacdc061dde3f8a844e0c7889c97203 \ + --hash=sha256:a2212a0c842c723d919ea4a22a9296cb6b244b386e4e6ea92adfc7fbf3095519 \ + --hash=sha256:a313bad717dde740959d50850c315b75fd4eb0c5e6b4dc8535db0f1c369be125 \ + --hash=sha256:a32b78c1e52ebd8e247bb68300b90b233300d8816faa008ed0713bc539fb6af0 \ + --hash=sha256:a3ffe881246d28a862f1985824f484cb7361f44d6b99c4c620436ef56462f38d \ + --hash=sha256:a49ff5cdb33654cb7d6a3c377aa2a83ddefaa1db31eb10bcf3c180aa84f9af8a \ + --hash=sha256:a8b75dd3d3638d9a19f23e84af4ffab3b8940422c0df2da2a77005ef5aa3d7ea \ + --hash=sha256:ab64ace1a68682d191d9bedd9d4c939406ad86b1d9f628180410644249ad46c2 \ + --hash=sha256:abc7c2e4b47bfe6a9aea434d3fdebb9597ee636e914e92ba352f2068b9142f4c \ + --hash=sha256:ac348379cf4de5538a0a213be1532d289aa801ec5d267c5909446b9ea2f8e2c3 \ + --hash=sha256:ac51cd64bae51c462ea58ad2492c9b8209667a4ef60c45c4a304518b67598d5d \ + --hash=sha256:acddcac38adc8342ba48aba98896faa7928854bebb62542362138655b5367ee3 \ + --hash=sha256:ad4528cdce058b684f75fad1faf4a6a6c992fe2f08376370ca66e4ce5916a84a \ + --hash=sha256:aec65b53a07f580606593f877eefbb29a45939bfc0d3fe6e6d9f42b41b749f68 \ + --hash=sha256:aef74e9beabbd6c4aafc091dabff86d046ccf013ce1e4396c0fbb01b4cad9de8 \ + --hash=sha256:af1b5a92315048c3e36bbebfa7d4760a9c3e910bc4166f11b20d77d20d6bcfca \ + --hash=sha256:b22ff30006a2f28f8bff878fb93413cbe3a4d1fd517c28081d848c90e9cfd2c8 \ + --hash=sha256:b2483da477932ad1983d1d33c18bc3771c6fb00cfbaaed70a875fd547ef8e840 \ + --hash=sha256:b2f0adc22a4eb31e545221d93fc73a0f6a8cc2379d0f4309f71d1d17ba938b82 \ + --hash=sha256:b4c9e5d05b126b267ac048a89a0e2d9b48b1b648add62d4872906304a8590610 \ + --hash=sha256:b4da208a63434d21a3df64d29758e650fc4aa8cb05848554b76949c296539cca \ + --hash=sha256:b5f8771aaaed7f80e84a4e471d2f29ab6721e4595075e54d03ae1ed951b2000a \ + --hash=sha256:b65121091567847a8cb520d364ab22ba90e00d3cc55fa9eb34bb439f0684bcd1 \ + --hash=sha256:b66ccc5c2cdd26e74fa5d4c29ffae424cc6148bf93ce574821783fb3b6d452c5 \ + --hash=sha256:b69651732c64afb691e50cdc3387cae305e0eeff8804fe3e3ce203876494932a \ + --hash=sha256:b828cf64d62dc09ac183f03c1aeedd164ade96a2ce4934109452edf29de1dd13 \ + --hash=sha256:b8a65621b98984a62e59403009591b8a5a7736273aefe1cab64cfb85b365cc07 \ + --hash=sha256:bb0d664505f4b112f384cffeee82e91e3f6448d8e574989479db4439b68cba05 \ + --hash=sha256:bb58ba73a3f96f9a3e46b1fab69929d7edbbddb3133ee74b5c3c54074a53c4f1 \ + --hash=sha256:bc94a68ea5e18f8e85dc6b522bcb53093f692c8eb62b4837ac047da73956cbe4 \ + --hash=sha256:bd82c4977196681a499bb6ca9e462afbc5c91c1c15b6a991dfbd72733fea4dd2 \ + --hash=sha256:c0c88085affd35c33e124e36930c5ad96aff9294ce195eaa0fd9cec962b64a82 \ + --hash=sha256:c148e8b596000dd3e4bfe206e70f3e666be18d72032e0012555f2373c52e35d6 \ + --hash=sha256:c57541034d12b215ab0a2bfa371d1a8a198da18176d0426c27105b9161a6862d \ + --hash=sha256:c9648ed33dc8179e4ec04bbc73bd7f0038e1e81217a69467f61f02a78bf07e88 \ + --hash=sha256:ca65cced0d67a9039e93bcd98a369920e499bf98296844ff56ead01c9085321f \ + --hash=sha256:cf22b43f35b7dbb9f71e8ee2041b5c00029cdfc28ede3a2f31cf8906f9a6c126 \ + --hash=sha256:d35a4f1c63f07fbb8c8f9946dea98b21eddf6c57421585f71d91864be3ba2a24 \ + --hash=sha256:d3b6e6840421c83ccb60398e333b44f910b0907bb409597685ab2eedd1e22eab \ + --hash=sha256:d73fed4e37158ff00cd271871170b79e40138db51e625fd352fa17c6acb34f67 \ + --hash=sha256:d7bf9e43282d69561618e8a0ea33368d532ebef42f15c096f427090521dd74f3 \ + --hash=sha256:d9d6544790ba50438a9c1a903c3c4afb1ec8a7832db5518549275b40ef0dd4b1 \ + --hash=sha256:db4d697b18b6ef5528b1f36bfa25072cd2a421869f5963bc0e92c8a34b9e2800 \ + --hash=sha256:dd9a137a4a9becda3094f3831cd026380f75f6855e051eefe4c73ade524f1cc3 \ + --hash=sha256:de7738b8c0bb74c4cc16bbd7fb49fc2bcf6430dba11b3432cd52768ae40933e8 \ + --hash=sha256:e3b1aa25f01238886a6baed9e13b9a9240ed344346c79ae280ae65e8603b21d5 \ + --hash=sha256:e58952f04772f59f11c6e007471449809a30165188669bca8fdb19dde40a8f24 \ + --hash=sha256:e5ccad4b7bac125722f48d6f862bed3b514d8526deea06316bb72f152cd30a7c \ + --hash=sha256:e7386aa18d98d6b8af44b92173654ec469237fda35f8e8523e43b581a86f476a \ + --hash=sha256:e81ae656b9935ac4528a71f96bb7a14d949778ed1897c573d3e7ebb9187f8841 \ + --hash=sha256:e8f1e362c9352b50ed120f001046fdbb80810c9d56580f4c3fc13bbe30823387 \ + --hash=sha256:e96ca64383efa107262ee3949f047af5ee4f1845ba09463466c04d35a83bd3ec \ + --hash=sha256:ea999ae6e80e66ad5eea287860951b033d0104ca34d6d87c7b5125ebe0e12721 \ + --hash=sha256:ecc89dbd4155b2f8a47f4bbd89242a35ed15e8e0ec581cab5ac65fa38407329d \ + --hash=sha256:ef01d29fca550ab871fd99154f82c6472aacf8e7def272dfb07b460123850390 \ + --hash=sha256:f04551dce5a7db8c9659f2e4245494c182d0663b83661803e08d46bfcae5eda1 \ + --hash=sha256:f0700527dd5bfa8b7204b08330542f4f388899d3c14d885d8a368992e0eb562d \ + --hash=sha256:f2524ec55b3e65cbe235a8b3c36e2af3b635be02ed05e20c94e70c5c943c009e \ + --hash=sha256:f5844e7befc707367807586f550fa97e23dcfef0728f02b98c7bc498a961a5df \ + --hash=sha256:f9dad513626a33670f17cddc6078e30e311f444c957e8dbfc5b2b4603c8b4edb \ + --hash=sha256:fc0dcb22fa9aeabfe3fa4e0430099acff985ec5d77a851382f76cc6146e780e5 \ + --hash=sha256:fd882aa29bf402b62bf1fd7c19fd5df4b6528cf468a908864b39368572b662a9 + # via + # aiohttp + # yarl +narwhals==2.26.0 \ + --hash=sha256:29326d74f107c347fd1009bd58e38d9f7c7c5b51e6de97bc93dbc325d9038b54 \ + --hash=sha256:6b9cadca82f375c7e4cf584fdc86ca25da54827307a9c58f94547ee6104b82dd + # via plotly +nbclient==0.11.0 \ + --hash=sha256:04a134a5b087f2c5887f228aca155db50169b8cd9334dee6942c8e927e56081a \ + --hash=sha256:ef7fa0d59d6e1d41103933d8a445a18d5de860ca6b613b87b8574accdb3c2895 + # via nbconvert +nbconvert==7.17.1 \ + --hash=sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2 \ + --hash=sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8 + # via jupyter-server +nbformat==5.11.1 \ + --hash=sha256:32d4521c68c6e7d5b29c76defaeed9f42ea733142b9b19f88277ce10390b9c4d \ + --hash=sha256:cc6698fa75f4fab8755ead786317815f13a6fee3b53311c0abb1a8b51d52f7ec + # via + # jupyter-server + # nbclient + # nbconvert +nest-asyncio2==1.7.3 \ + --hash=sha256:2bc87bdca654e719425145f5e84eaeb0080b013fdd47d65729a7d66243f4987c \ + --hash=sha256:2e9a84d5d1efe6d020c72988d21aec569bac42d98af2ff6b9de24640c5d22a34 + # via ipykernel +networkx==3.6.1 \ + --hash=sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509 \ + --hash=sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762 + # via + # pymatgen-core + # torch +notebook-shim==0.2.4 \ + --hash=sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef \ + --hash=sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb + # via jupyterlab +numpy==2.4.6 \ + --hash=sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1 \ + --hash=sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4 \ + --hash=sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f \ + --hash=sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079 \ + --hash=sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096 \ + --hash=sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47 \ + --hash=sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66 \ + --hash=sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d \ + --hash=sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1 \ + --hash=sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e \ + --hash=sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147 \ + --hash=sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd \ + --hash=sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75 \ + --hash=sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063 \ + --hash=sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73 \ + --hash=sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab \ + --hash=sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4 \ + --hash=sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41 \ + --hash=sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402 \ + --hash=sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698 \ + --hash=sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7 \ + --hash=sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8 \ + --hash=sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b \ + --hash=sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8 \ + --hash=sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0 \ + --hash=sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662 \ + --hash=sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91 \ + --hash=sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0 \ + --hash=sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f \ + --hash=sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3 \ + --hash=sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f \ + --hash=sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67 \ + --hash=sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6 \ + --hash=sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997 \ + --hash=sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b \ + --hash=sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e \ + --hash=sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538 \ + --hash=sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627 \ + --hash=sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93 \ + --hash=sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02 \ + --hash=sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853 \ + --hash=sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c \ + --hash=sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43 \ + --hash=sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd \ + --hash=sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8 \ + --hash=sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089 \ + --hash=sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778 \ + --hash=sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1 \ + --hash=sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb \ + --hash=sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261 \ + --hash=sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb \ + --hash=sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a \ + --hash=sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8 \ + --hash=sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359 \ + --hash=sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5 \ + --hash=sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7 \ + --hash=sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751 \ + --hash=sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8 \ + --hash=sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605 \ + --hash=sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e \ + --hash=sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45 \ + --hash=sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2 \ + --hash=sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895 \ + --hash=sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe \ + --hash=sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb \ + --hash=sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a \ + --hash=sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577 \ + --hash=sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d \ + --hash=sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a \ + --hash=sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda \ + --hash=sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6 \ + --hash=sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20 + # via + # -r requirements.txt + # contourpy + # matplotlib + # monty + # pandas + # pymatgen-core + # scipy + # spglib + # torch-geometric +orjson==3.12.0 \ + --hash=sha256:010811c1b69773450a01cef97727a67b223242f350b77d4ca000e59a9ef2155a \ + --hash=sha256:01efac2074fffb4cb1ea3fab7861e9d0f2a26913854a972f5ac760525dbdaf6e \ + --hash=sha256:03091c8a64db4be38746597ceea68f33c238e27acd9bfe99fb59420224ae7a55 \ + --hash=sha256:08231552159be266a7269555bd9f7c016aee7d9ad6dab06eb58796c5ccb7101c \ + --hash=sha256:0b1ac5bf6609b2716c7954011c5fef6254922df029f45d032ee4ebf5d363cbed \ + --hash=sha256:103b5db66aa53c1f9e88c2524be4f383e831ba7dfd5f9f5af6336a177c622f11 \ + --hash=sha256:1192a7021b6d071aaf909864f6e924d6a2675ca360485b972b8401749311750b \ + --hash=sha256:11edb4660a6680abee9788a3a9072208a2c96538cc1322bd79542065229d8e54 \ + --hash=sha256:18a87929f31d94a77f7dc93cf527e91f39ce7fe7813d588a4de2507efd32a387 \ + --hash=sha256:1c680706fc8396d95e7c4c1f9482563f552137aef91b57237a3ad5aaf64629df \ + --hash=sha256:2b7bcefb9f40fa242fa6b06377232c048e655747790829609168c01162f60578 \ + --hash=sha256:2bb3ce43203936072dd8b4917b01d3aecfc02329bfb42510cb7cfb24708adc9c \ + --hash=sha256:2d3a9da945a4d96ae758fdaaca56742e6b73b6fd554c5d8876f252a6dad70b83 \ + --hash=sha256:2eb5c56e534127b2b8fa38d2363c8b1b8190367ee0d1d16c041517d880843b94 \ + --hash=sha256:31ed278a36304390adc3eec5d7f6fd593a7c3e99e5a06cd07866396c4b1b4710 \ + --hash=sha256:33efefcf5d88eaf400b47e2eba02f91f319bb9951be61ca500b7d536d3f2079d \ + --hash=sha256:3bb17a06f9bd15237b3216c044209fe92597379124018cfc196fbb846cde64df \ + --hash=sha256:3dbce9b6b3074b31a5d5dd322a9c4e5b16f206091ece4194c2e36952847a105e \ + --hash=sha256:40f92192227505acca4e2533ce565f8e6b9535f7d0d09b0968452f18b7376b38 \ + --hash=sha256:477ecaf6b9f88f873341b91fcc736119ca81b5e002a9f7f308ff5b4f2ce2a70e \ + --hash=sha256:50fae885cb073eac7556353ff3df93312b0d5137b0a5056b2bb63f97ed9a93c7 \ + --hash=sha256:532ff8cd4bd59a327a953a7dcde922c7fc25b85e29721bb8633265430d3a3873 \ + --hash=sha256:53c0c474a9d9aff9aebfc0c88de1f28f843d940e6e3a80729abdf6a20274356f \ + --hash=sha256:58c58e1de0006ffb580368d6793c36c7b0b021db066479cf281bf5061e732328 \ + --hash=sha256:5a0fdbc216388f653d3752ff310e710f59253bd4ed6a2bfb3f4f06b84714bbd8 \ + --hash=sha256:61318b6de893c7a9d9f3e5ecbadccbfc26a7eb417ccc7bbf0771de3b4d72f868 \ + --hash=sha256:644d005bc82f917337a95ce270c9f6f92f9834c2bed7b1477572f8db00784222 \ + --hash=sha256:6a2a79c89984dc719817d388c8709e0efc2a2795a934eaa746b4882eb6045adc \ + --hash=sha256:6a31348d7dfa64cd9c78bd1f510ff44c48fe64d71094e6b90e364dba3b55949e \ + --hash=sha256:747843254519dd43b93eee3153a19e5a509334320c4d2f823ec879232db5c796 \ + --hash=sha256:784106539f4b9d4b930e0b4eb8d45168507dae001945e71b4675a367f1e5e806 \ + --hash=sha256:7c2ad193c8004254f34b499f3bd2c80f043d10754aff2b38f93da574f4883f98 \ + --hash=sha256:83445adc40cba26d6d621185a45128ce455b766af368cad2ab64b970603a7978 \ + --hash=sha256:859fc4196855890150bb08e649b30d2c93b249b3e3edd0d3bb2231abf8aa8adc \ + --hash=sha256:8c3bb86dd10f39b3fbf434b7d5dc7cac77d6fc8ac572ae30a10731ede2c4b647 \ + --hash=sha256:8e29957429c35bbb5a185a119c523aa2428b7bbf1a293724c7b9375ed8f892a3 \ + --hash=sha256:8e386b0bc0ddd7cd2056f884b5a0af33592bd01ac66a7ca4b42a65a7e7774a13 \ + --hash=sha256:92ffc09e07233a6ab6d4e067f7841edcbcc134cb4812155cf171ea5255a421d7 \ + --hash=sha256:9a36ec60f1796f9a3f13e3b98390295e17a1c7c10155b448d264098bf9ee5900 \ + --hash=sha256:9caf3d09f47c3c70c4451ada20ef9bc4a4cdffa26f49862cf0a253b329aae2d5 \ + --hash=sha256:9e6fee342a48760e854d743e7a81534d8e2925a6f46e09f750cf56b50fd1de5d \ + --hash=sha256:a15f9a891bce5f5cc5d210e3ad8614d4d1b489a56448c099d6d2a7168b2d954a \ + --hash=sha256:a696529ec96a90d9a5f9570207efe403c8b08f8e4aa2783ee3403511e2fdfa10 \ + --hash=sha256:a6cf4b18e7de173f209f2084ffbd736dd72389a396326ee80a7022168be232e5 \ + --hash=sha256:a791f793b287bbc135b8e87c34e35c8bfc693e2a8a620fab1ae682b925f9a32e \ + --hash=sha256:a94f0f0c6fcbb2b5bd9734c57a489c7584a732bbdf04a39e8c83b861e9d03e92 \ + --hash=sha256:aa3e43a6846e91d7bde3d5a9c66090fcd8744f569a9b6cffc5e1ca38f6a461c0 \ + --hash=sha256:ad0422b92d5195443a39f80c3bcf731cc2e00f153bd32063a47b73b057bd0f03 \ + --hash=sha256:ad29eece0c601737f2a60edc2752a84e7a0785df3efb62e3012834700a5afe0d \ + --hash=sha256:b85931be5b6763c31283805c9bdaae1ca03ad9f6f12a15f1cbf6745b907932c2 \ + --hash=sha256:b9dca132b1fda5565088e65a6b6e742285e0aeceb6fae549fa8863e16c7d3998 \ + --hash=sha256:bc7a872f03522d90e0429e6c0c5cd23084f767bedcb4c58048eec19294613344 \ + --hash=sha256:bd57d79aefa3f84eec851d6de7a366795b9345cfaf17f82b4820430a7a5fa241 \ + --hash=sha256:bf44e374aadde77b1f6109f1030be51433eb61984379852766b6f4e187db7b1e \ + --hash=sha256:c6b11be792c3d2c6a4be2af4ebf97a68d0bf5f580aca6e86a418a354f6cc846a \ + --hash=sha256:d14203fb1aae2ad9b3d52f8a0e82aeb10197ef1c9bc61da7f358bd70b00123d5 \ + --hash=sha256:d39f3f5c3927e2dc0913fe5bbc1a2f6b1b9d1bba1de6358340d0ad0d0c00ca92 \ + --hash=sha256:d8e78d3d93705e3d27cc17cdb209e44d7a8ea203010cac6ce9c7ffc1ae1996f1 \ + --hash=sha256:dce0166feb0a737ab84f598c9a338cbc0b764a036617aa686194f53c7eba0c3e \ + --hash=sha256:e4ac5059baab4b3acbd99485de019ff8cda0fdf34b61fa74f7197a53db78bfe8 \ + --hash=sha256:e9683ee9ea0659da64f36574ef675b8a86330c34c19ea75db1fb93c3ff99e0ef \ + --hash=sha256:ed4ca42bd55955aa34deedcfdfd0e0c31abf51143aae158ae2bc3520b626e517 \ + --hash=sha256:f06dd838d1e07d9b1de0932ec0485ec92c4d5f5d1ad4817a656268c3e88be1e1 \ + --hash=sha256:f3c0683136acdc29afdf88a5bc2f7d3d0e34087788d1d63c0144b805a87a196f \ + --hash=sha256:fb2539159dfe8d371914f354360fa50e4a577cc89222a3828b9650a5e5040252 + # via pymatgen-core +overrides==7.7.0 \ + --hash=sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a \ + --hash=sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49 + # via jupyter-server +packaging==26.3 \ + --hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \ + --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c + # via + # gunicorn + # ipykernel + # jupyter-events + # jupyter-server + # jupyterlab + # jupyterlab-server + # matplotlib + # nbconvert + # plotly + # pytest +palettable==3.3.3 \ + --hash=sha256:094dd7d9a5fc1cca4854773e5c1fc6a315b33bd5b3a8f47064928facaf0490a8 \ + --hash=sha256:74e9e7d7fe5a9be065e02397558ed1777b2df0b793a6f4ce1a5ee74f74fb0caa + # via pymatgen-core +pandas==3.0.6 \ + --hash=sha256:0704044b676496b8350e023b09f174a26772456c974a2b11c36bebb558c9490d \ + --hash=sha256:085e3786ae6b2e82b406266bce36690f72b9dc1421903ba9296b2981a9fcf586 \ + --hash=sha256:097090508a1dd335013d39106fc10b20f4fd4a171638e47b77d55798ed9dab6c \ + --hash=sha256:1bcb3e9ed29e74a7439cedff9e2aefd3ea65de84d7de9ccb6c194192541bd60e \ + --hash=sha256:1e7c0afdcaf6661d795fcefc2f647ddd1136f62cdc153fba177c685d97a87808 \ + --hash=sha256:1e92d9fa834c7d877130027cddc0cad8dcff97c1f6cca26bd6310f847228b658 \ + --hash=sha256:22172a92e7ee678ec0140c7af4fc9366b55413834a1cd86af78b3caa0b0574de \ + --hash=sha256:253e12cb9081b0afbac607920f6142975966bc315135e09de275fdbaa415d2de \ + --hash=sha256:265f562fdd1079f69f3de96dd425c3405224038c0af4f920c54bd240ee2c4640 \ + --hash=sha256:2a8fc94be2ee5f1d86f97aacd8cc566f81680b6498e76f3007421bb5d98151bf \ + --hash=sha256:2e5fa32ff162dfdbc280157d664f44d23049ae414725af9676df339c501d82cd \ + --hash=sha256:3ef908d28590b3f42d7070e7ad8f9b34b442b260b7f3c1afb57e0040c58cdb1b \ + --hash=sha256:429d9df32731ab01383ed98f2baa7a60368090d1a94fc06019a12062510e8630 \ + --hash=sha256:47121f9571503f724c9b93e297ab6254ac99c77adf5e9ed085ea419fd585c258 \ + --hash=sha256:4e25e2e1adee99ddfada6f7206a79ae8e9c8a8861b0e3eaaba165006d3eef18e \ + --hash=sha256:4ff44b2cb51cbd691c91f92c4ea6c71e34003f239ebd67c2e857dc898466b49c \ + --hash=sha256:50c44cbf5820b6b91a5f74aae04972472aefadd3cd9fbd1010409d85528bd570 \ + --hash=sha256:569e114072b24fc4970c12e2b4bab252671668a40b324318903380cab0254c0c \ + --hash=sha256:583be68728a31d0d750d5b8d9e00f02b153df0d4655f858bde93cb84cfc4227c \ + --hash=sha256:5e75072773c1b2f7cb63faa3a6f562aede11f3976f68ed34cb538bc091a28171 \ + --hash=sha256:5edd0a7abb0986ecce1ac81f56d99b6763f86aa6946dceb6c661224f90af5a19 \ + --hash=sha256:60d81f9e1799b36f3739e7fff44d1fbb2e8fd5a271b3863e03de9715fccda0fa \ + --hash=sha256:62f51d7f651c8054c5e82a69265c98082e795d1442df7ca6edc3a545d61214b1 \ + --hash=sha256:654aae059295dbba6ecd2328ca12712a2cf1676214c8699f1c29213f7ccf9c34 \ + --hash=sha256:66b07ef7315a31bfe1089cd3d71a7de781c9dca986762d0b4fe7c0ef17465d10 \ + --hash=sha256:6ff482fa91fa2bafd92e8fe66ce3645c851824310f295c1f0a2f96e928fc4541 \ + --hash=sha256:77ccbe5057aece6fc172b9b77f19c04335af6882bc2e10c8f3ee4e6bfb3da553 \ + --hash=sha256:7dac2d65e9087e8e7b5a45fe15c4920911a221df061ab629943ce016489145c7 \ + --hash=sha256:83e91d15738d7783c050197cef2f2cf82fc6353dae9865aa87ed1fa16aa4d55a \ + --hash=sha256:86fa853a12e0b70927e2b1ee00d56d2224ec9cbb4b9d58348b5ad52d2f21150e \ + --hash=sha256:8fe77b408d82e2615674dfed62533b95e18a03610573877422aada4f625d4947 \ + --hash=sha256:963ca21199097a84c7827c4678b04e30833084fbf8ef44fde3fa7180a29f8fa0 \ + --hash=sha256:97274c9adf6255bb48c620cd6959805efa7f09ea2167f0e0ae006a448cd2fca7 \ + --hash=sha256:994a79608263fe1c14cc48ffa7300e2b834b7d1cb406ffe96a08828cb0cdd79b \ + --hash=sha256:9ae8073aed8e21d1a7fe263dcdc6840743549722a6738198a0a46000fa9476f2 \ + --hash=sha256:9dab635a549e58a053c7b0fa054dc0bd7be22f0ed9a720f4a85d5fb993276172 \ + --hash=sha256:9e492cd4bdba6778de4fe0df7f4590c012161ebcf9902dce01b01dc683105514 \ + --hash=sha256:a3a22e07fe75347eaacc75b0e85297947af4fba6b4aae23916bd8b6828d0bba3 \ + --hash=sha256:a4dbd4dc65cbe645b92b8785d0f96dd7311010dc6606cf620e51b07b8788a12a \ + --hash=sha256:a77a1a44e4d88f1c6a2a64d3eb12efec8420875722e14279800b173a7c7c2804 \ + --hash=sha256:b27c8d890e4aa2171437ae2a39de1d215e674158e4865c4023a8b31c932513b2 \ + --hash=sha256:bd75ed0c840f709fc2ae26ddd9534ac77ca1a48ac0cce521a74acaa85f3340a7 \ + --hash=sha256:c6e4aae3e9bea26c6c9a20d88d96c86ec4a99b4db5fd516bcb4e829ab2c0ee36 \ + --hash=sha256:c826e9babb7790142c399f58599d8de679bea059d7b39c5b6efa2096fac37266 \ + --hash=sha256:cc39303913e2ea129915670de5d1c9fbd647f543bb72e5543bac8baa94e9e42f \ + --hash=sha256:d7564d86a94c2eb8ab290b07f63ddaae5c032fa53897c29a2ff2197d43aee8af \ + --hash=sha256:d7dcd21238cbb4828ff148481ba01cac8946dc5121457b5aeba28636f8f99a60 \ + --hash=sha256:db7ec631f26223beee8e5c9e0b8f23c24d8197bbd1d982421d4e3188bea51965 \ + --hash=sha256:e3dccb584123b399c07562ac4d62543e90ede49ddf8ce3c13ffc64cbe828c281 \ + --hash=sha256:e7c1905ef02c3d6d43d9dbd5b6ccb4da4870a0b0c821bbc103fbdb6f3ad2707b \ + --hash=sha256:eb6900de08ac85f93ac4948aa6b80842eba555875337b8359035ac9c43e92d34 \ + --hash=sha256:ee913a91669056c1de1a6b733fbfeab711de9e54e3bee2dfa5fe79d9457247d1 \ + --hash=sha256:ef738d71d1059245b6bb03e312be06d8b3821326a83486c1ad03b9aba3710e44 \ + --hash=sha256:f3ce8a6968045481e91a3990e797e348ce13db45ee164a7095bbc824e26c09dd \ + --hash=sha256:f4e7c52eb108d752e7592268108fd3e98efd76d83a3125cdd06c621c2e44359b \ + --hash=sha256:f8029ec0f1f89e4f985929ce1f6626dabf3140d61a4e9c1215afdab34eaf9a5d \ + --hash=sha256:fb625f426b375bcc96e3a04c5d5d266cd7be6ae5d6866e0e703382ab5164068c \ + --hash=sha256:ff51a4459ed036e93d1eb1bb5e6e7b28685d3cb6b7c12b91c05b31024e234729 + # via pymatgen-core +pandocfilters==1.5.1 \ + --hash=sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e \ + --hash=sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc + # via nbconvert +parso==0.8.7 \ + --hash=sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c \ + --hash=sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1 + # via jedi +pexpect==4.9.0 \ + --hash=sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523 \ + --hash=sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f + # via ipython +pillow==12.3.0 \ + --hash=sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756 \ + --hash=sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a \ + --hash=sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59 \ + --hash=sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45 \ + --hash=sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3 \ + --hash=sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df \ + --hash=sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139 \ + --hash=sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b \ + --hash=sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39 \ + --hash=sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e \ + --hash=sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8 \ + --hash=sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1 \ + --hash=sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8 \ + --hash=sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89 \ + --hash=sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5 \ + --hash=sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130 \ + --hash=sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd \ + --hash=sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d \ + --hash=sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b \ + --hash=sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed \ + --hash=sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace \ + --hash=sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb \ + --hash=sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931 \ + --hash=sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510 \ + --hash=sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6 \ + --hash=sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1 \ + --hash=sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce \ + --hash=sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385 \ + --hash=sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e \ + --hash=sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c \ + --hash=sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7 \ + --hash=sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace \ + --hash=sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c \ + --hash=sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f \ + --hash=sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64 \ + --hash=sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f \ + --hash=sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a \ + --hash=sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827 \ + --hash=sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17 \ + --hash=sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4 \ + --hash=sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a \ + --hash=sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701 \ + --hash=sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e \ + --hash=sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91 \ + --hash=sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66 \ + --hash=sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468 \ + --hash=sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217 \ + --hash=sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658 \ + --hash=sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418 \ + --hash=sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a \ + --hash=sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c \ + --hash=sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330 \ + --hash=sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402 \ + --hash=sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09 \ + --hash=sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930 \ + --hash=sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f \ + --hash=sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec \ + --hash=sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a \ + --hash=sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94 \ + --hash=sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468 \ + --hash=sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b \ + --hash=sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965 \ + --hash=sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8 \ + --hash=sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd \ + --hash=sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7 \ + --hash=sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c \ + --hash=sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777 \ + --hash=sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35 \ + --hash=sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9 \ + --hash=sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f \ + --hash=sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f \ + --hash=sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0 \ + --hash=sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c \ + --hash=sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71 \ + --hash=sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3 \ + --hash=sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838 \ + --hash=sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf \ + --hash=sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321 \ + --hash=sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26 \ + --hash=sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec \ + --hash=sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9 \ + --hash=sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65 \ + --hash=sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5 \ + --hash=sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e \ + --hash=sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d \ + --hash=sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198 \ + --hash=sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7 + # via matplotlib +platformdirs==4.12.2 \ + --hash=sha256:29dbf06d96c500bc6bdbce75fb0a14d63279c93b1842f97e72a135b33e856983 \ + --hash=sha256:eab5f70271a490ef74618bb314fbb86e3c7e82fa3b9c922c2ea0e0a1a155d329 + # via jupyter-core +plotly==7.1.0 \ + --hash=sha256:dbb7fa18afce40d0a8e80d1bf162eceb3faa0ce5a77fe741ad09a74cf78f53f3 \ + --hash=sha256:f860166a4a3d78c69cb1f4a15f28a5c8283eade98a282a698f3bb853a449ace5 + # via pymatgen-core +pluggy==1.6.0 \ + --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ + --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + # via pytest +prometheus-client==0.26.0 \ + --hash=sha256:04a91bcf94e2cf74a44a1a874d651a2e853ed354b6e822f3b7487751465d5c2b \ + --hash=sha256:fa93d06737aa02bacd05794768508bb97d2fbee28cb3bca04eaae92f0ca953d6 + # via jupyter-server +prompt-toolkit==3.0.53 \ + --hash=sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2 \ + --hash=sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6 + # via ipython +propcache==0.5.4 \ + --hash=sha256:004e685b315646c410771836e72a44f143bbe624f29653a42687815069a303d5 \ + --hash=sha256:02c0a34f16889cf800f10f0247a564d8ce6eeab6ffcd7c87198f769067eb8432 \ + --hash=sha256:03969626faf0783a592dfa17e28eac06018bd0b44dafae6943d53b92421a7f72 \ + --hash=sha256:03b229037d25b801e7af53fd52b9fc49d9439b036fca1e087e02780631adfa97 \ + --hash=sha256:0951315a6b3142ee2167404d707743f0157c110091342b1aa0accac5cf0e4acf \ + --hash=sha256:0a095db8e15a6020db149ecbed6461939fe74f6acaa3ae8b702a1fe8c38cd983 \ + --hash=sha256:0c889f6fa84957bc7e8b4eab71fd16a0455068d5045e3aa40c733071d2b2fd77 \ + --hash=sha256:0d21d0d2c82bbfeb1677a9711f38df968f9837576102bb4add1bd449d28d88f1 \ + --hash=sha256:10ef33a68a61ce317e095fd2e202a592ea92392b90944a78c993f0d9a73ab06c \ + --hash=sha256:12682126712ddc19b70ff819debbd279e58adf1f0c8f8f8138c18ade2044b284 \ + --hash=sha256:135036c5cfc93864affb0f9af9a27e5d7a71cb7bd745e7b6dbfc2d56cc30e827 \ + --hash=sha256:13e52b6e0bde97dee98ab66552dbff2931649c96f1ac432eac299fe689ec373b \ + --hash=sha256:141fdbd73748db0cf7636035030aaac383d2efde8f34e7bc24594cc776d225b8 \ + --hash=sha256:146f48a9e4812611a7581003b1a39de56c34967046310c4171a68ef908c9a745 \ + --hash=sha256:174507f82d3594622acb1dd2dafecf2d899d6d506335494e7107767bf05f3aae \ + --hash=sha256:1783582065a1f07f9d9ee1e992e13f15d7dc8fb1eb3a7476d43eb3f2e69d26bb \ + --hash=sha256:17a7400cec0256f0a71ae71f9da398f9894c956ff6668a1c9d317b3367316320 \ + --hash=sha256:1b2f3bec4261a94019575481c726c29850f72e27907773c75b1de421e20e9f9d \ + --hash=sha256:1d759d05634f1b038fb625a66662a8c85e5a8fec912da381b5149ddac107482b \ + --hash=sha256:1df8d8561b21465c5dd56110a01caf897e026d065b4b84e98a488209094272ec \ + --hash=sha256:213bb68d9ced5cf2bf717b1071bf2b09b4b04c426256f9fe6d054c60318424c4 \ + --hash=sha256:23278f808cd81d5ada7184a76606b925fb3389c60e1077b2cd7da7b1fcf0553c \ + --hash=sha256:251c63dd46a0659bb875cb254dc4c1e79ee91a847c737cd62373295afc2235dc \ + --hash=sha256:279655a16973f1ee2bd2fe79973137681642fd9ae0d89215bba263726eb0dc3a \ + --hash=sha256:2814ecd8e818f487bee4b0f921bc4d1c176cc5fc71ac0f072d0fa67eda4ac14b \ + --hash=sha256:286867fb156488c251a3721766e380ac4495e4fd6b51aaa1403d89ce7f4359d9 \ + --hash=sha256:2dba2f02d2d5c09ef8a0e6c1a42aeaa451f4be9898cb00b04fe98717da2eb23b \ + --hash=sha256:30cc1cebaf9aef49db06357a50398323ae04d70460c0491837d026ab7d6452ea \ + --hash=sha256:31eb43ba2edc704ab2ec27815315dd8a19def0fb16215be4cfe8d32fe78ffd51 \ + --hash=sha256:350b272b2279f4135a64fc0c304a5d08e28a137c9573442c606152446638a831 \ + --hash=sha256:36c0d9db44b523ef93d03341b1c42d69ff01d673c053d1b1c6c3a363bcaa39ba \ + --hash=sha256:3af0c8642b2da4815d86e631232ac8286e17644fad907c19508aa8e7cb4ba8ad \ + --hash=sha256:3cd3a7edb6b95b9b33998135ebfa18d709da82290fb8f27c858970b5a12c8b56 \ + --hash=sha256:3d605bb239b796e82a81c6709548b2bd460ab73b4590cb0c83de8a2dd9694d0f \ + --hash=sha256:3e413d7a4a9b4866b7a761d6060d434b64d23cd35122eda3b026a0bbe8196b25 \ + --hash=sha256:3eb2e820e8e2101407da93f17c57cbb7d225461955fc60105daaba14cd421ee2 \ + --hash=sha256:3fa15757fea1dfcd5b7745cad9f4638929605531bd4018ab2adff7955f1a403d \ + --hash=sha256:3fc24f209c1b7f7f688b66b98293954f5504279760999b58920ee12dd8471c1d \ + --hash=sha256:4054acf80d40456a0537f2913b349718649d8d6458a14ab7f48d0ce28c30869d \ + --hash=sha256:40e94adb1e7d39ff28a8bd8d8b8fbd1df6b9f40976dbe379134f1ce058e532dd \ + --hash=sha256:420162a77f94eb1cf5ef7893f500016dabd548e73de956785a1dd899cc73006a \ + --hash=sha256:425f8cc86ab5018b4b8d4a23bc8e74d964bd3d757c3702e301aa79be76c53f6c \ + --hash=sha256:44149f46500a0a41b95b4d99c2e586a77319539730607b9892974a092788b111 \ + --hash=sha256:445ee3bfb46e85838387fb3c536a73cc0b994dc192b004e40e170adc54aa2a7e \ + --hash=sha256:45488d1a5f9ab5bd90aaa1ca20f50fe1922b8ffad71a2009d2adf41355897aac \ + --hash=sha256:45bebbe252550fec975ba3b62bc6f931643cfd3b5464ef47619cf3fef154e01c \ + --hash=sha256:45bf2e730ab8905d0527fe05a86500f406e64305c34cc81ebe64b4617cab9760 \ + --hash=sha256:48cb48c5346a97de792254af77715aa2529c2a1ebc5f586aa0aae44a02f1fe57 \ + --hash=sha256:4a1f4f5ffa55dce6307631f3cb2948e117e665966ea512e0d502b16c24f567e7 \ + --hash=sha256:4cfe0a92ae30151869e67a4b5f5e105e4e03ad30b3f38e5211b5bf77d0881993 \ + --hash=sha256:4d86476a935c88963d9b8e1a9a0d38188790e9622169bfbafa173046846709d3 \ + --hash=sha256:4e985382be6d15da8d0c2710a6fa7b9070fc9ecdeefb7f580e88373984ec8be3 \ + --hash=sha256:4f2d880ff60f45898f4acfa152aac8d04e3ee627d90ff4003491bf92239d5757 \ + --hash=sha256:4fbc1a15dc8cd1689508758d626b372b1f09d28d9577667feaf9e6bfcd8efcbc \ + --hash=sha256:50e337653721d20ead710da33bf44487fbe8a0db8782714b60306481e9f95b51 \ + --hash=sha256:53eaa697c4d0422ff4cb714d00231b43352064d97b944033b30c1d57cc506ec0 \ + --hash=sha256:56fc3f7599528db40b1efa0889a620116e2704144495273d66066e8164e45838 \ + --hash=sha256:58134228927cee6c047d626c08e60a81be604a20578a12ce752cc5c9a84d4826 \ + --hash=sha256:594eb4c6ec35e7179b058481f4e9f02521b56de16fa577c4b85c76fb1bf8a9f8 \ + --hash=sha256:5cacf3c9efd09df409dc33654dd077e1c245ba8fb747b0f0236ef41b7c49b589 \ + --hash=sha256:60a64cbccaa11b7760ce705a14ada17ba459e7ca9f23ba587eb013821032d7ef \ + --hash=sha256:62530ca89187827e4a4fe733f971abe81a7542eeea48ff61995f19b64d7199c8 \ + --hash=sha256:62c60aec739ed00124573cce1178138fd690c7676352d67a37328c1cf51d7468 \ + --hash=sha256:69fc35c0779522da366c563e5faf203ffc1f8ff0021d5b1337fa4efa5be73177 \ + --hash=sha256:6af4693716bfb03f1752ef1b30faa593db2c01d5272e9b8564a1549452a979ab \ + --hash=sha256:6c7599df2b57ebeea8de011b5f2f7b85de95e76037d43d34b95e328430275487 \ + --hash=sha256:6e9368e87a3efc285e559131092c5db643eb8e56de4ee42064d5baec22ef2bb5 \ + --hash=sha256:6f0093ac3e9daada202c2082439d414a625c57184727a46e112a3fb2a81cb788 \ + --hash=sha256:7177c43eddf10a0893c4fec52ebb408fdcd7f7d63962caace9180d8f81b14ece \ + --hash=sha256:720cf832eb2d0b0dfee129cb3335a26f6ce3cc45ee1187e8f0731758caa16792 \ + --hash=sha256:770e8209d018175fc0063936fa9583b6d27e88c5ad31543f3383d66080efdd62 \ + --hash=sha256:7a8d5ff04eb1f85698a78d20c62a14676e7b960dcafde09a388d60ad377d355d \ + --hash=sha256:7b9100a93b372418d8688f3f2a3e5b45c64d70ca4d6176e121aca1e3bfc1e32f \ + --hash=sha256:7cc528e760a8af06f2b13e9b9f362cd90c7c718ea61228a96dbd31ba16ed7f47 \ + --hash=sha256:7ffafcbfc7b549ab940047e505c831eabac5e67de53e1bc174adbc5285c55944 \ + --hash=sha256:87a3caecf8095e48dc72f84bfa42e23a848cf410cc9cc13031fba4869b706a21 \ + --hash=sha256:886b59c4d28ca97dd23b025fdfc50a0356be934efbbbca89ad26230067f86fe5 \ + --hash=sha256:8876b39961e33d912afe3c1bee18ee564fdad0206f873cc15d522756b7f50737 \ + --hash=sha256:897d1ddf6716e8f47200f7aad9a0efa6cc7586df66c6defa572f9eab379c078e \ + --hash=sha256:8a1fc236528c457cd739c88abe823da851b7ab645d72792f88658114cc340c12 \ + --hash=sha256:8a235f73d6e020855dc29dff012d920c02ee0feab8d73a24185a7569f4be1161 \ + --hash=sha256:8f911c395cef73c510bac566da9507bb6a43e7763d0c79138dc60ee53f11207e \ + --hash=sha256:96f7c5c15656040ddcbc51e56dc59b58aa25999d743c126abd425b9766ab43e9 \ + --hash=sha256:978f28401afbc76cdc3df9e1717b4229a06b626a1dcc75db4e1f2beb3884c3e9 \ + --hash=sha256:98914de2c4d7f0f9f4a8c6ea4bf05841f4175796941e3ef7d47eb718f22311fb \ + --hash=sha256:9a2a8a50a93dee0268a860a07fa3b4bd968f8ce4dbd794957da772f395368526 \ + --hash=sha256:9cbfff4423eef4cc6cafc021469641a2b835f610b2647a6c5281903e21b8670d \ + --hash=sha256:9e9ab13760aa8b6d0881ae7cb04fd891d8d490cd2554ea8e79bb278399169bcc \ + --hash=sha256:9f3551b8a35c1df3e7ea4d2d86edee15f0dde1bddd434a71744048683544d0ef \ + --hash=sha256:9f86f7259efe2c951f43e57d471c9b41daa5bfc7db9f67189059cf1ae6d77fd9 \ + --hash=sha256:9fb0a5be8d9aa213150e8d8148a42aca4984b285bcad1e69587dc4298edd929b \ + --hash=sha256:a219f0ac59817a9114dd2aa57c13180f993e819ba658c7ddab4b66ed1ee0d370 \ + --hash=sha256:a419ee85e654927baabda3929c03c0cc1112bf472ff0dfd6142f4e3a81ca4162 \ + --hash=sha256:a4d7a54719b67338a305dca2ce6aafe366817df94ddfd4b5514374356f5ca546 \ + --hash=sha256:a5793c7698a53f56f4a1889a4737c7eeb1b7ad0842fa6b1abca22913ff79c8c1 \ + --hash=sha256:a5e8ef588c109725dc713ba69aadcac00a1ef90c2ce9c0a8c7075128f569f47f \ + --hash=sha256:a74bfa37147cc08fb29df10bd9c16f40fa7f860cd3a6d2fff853323a94f6e17f \ + --hash=sha256:ada748108a43d29b7c328ba7db3755327cd94f028bcc1a7ee3f0addcfacd9c38 \ + --hash=sha256:ae58f361bd5dae942717c65d3413b478c70aea9c462599e7b9adad3731db3894 \ + --hash=sha256:b28f41fa3b8c6900457f858ec5b03998f3a6d535fbc1bb2edec5961ea05ec429 \ + --hash=sha256:b3083bfe87f95c756e610bd8025f26cbd1cd4aaa03a422f2d65efb7a97cd53d8 \ + --hash=sha256:b61805357d966680acf68b3b6d49772631ed9df44ebece10ff1460e117a7da8a \ + --hash=sha256:b77c313314524ca9c38fbd70f73515d04597ac58c40c939bc0e71eeb4abff680 \ + --hash=sha256:bee7d3aed13d56f54e681df38c3a23031bc9e3863f687d9d598825c9146acd7d \ + --hash=sha256:c02c0e570c5c7e077b0181a9f3cdb7d4c3617d1cda6b5c95bd5d34022923d82c \ + --hash=sha256:c174bfd1c48a1b51a3078e95586dde718374bac79719ab3541ec9e74aec40574 \ + --hash=sha256:c2ba30a89035b57b73e00475de948521602f543d79ce01db10b04b36c4c76fc8 \ + --hash=sha256:c3e98c55bde2bcf7db3c70d1aed7ae9aa8aebbf19a250c66645cde44cdb8b867 \ + --hash=sha256:c3ef2818d63bc86071e9d2989ae75a1bc32b8f7059cfd9f5abbbee70c32e2ed6 \ + --hash=sha256:c83acbce9f2b5e3f5f5eda9e53d2001fed22fcdfef81274a9e02d8fd53b70a30 \ + --hash=sha256:c9281e922c072158c91974d4589f1dbe0fee6d467f284c28e463f9f5a4d933f4 \ + --hash=sha256:cc07876cfb079b6f6f36d21ce75784ad6c2c6b563eeac0ed26c2fa2669b85df9 \ + --hash=sha256:ccf4f7a79e26bb7efb06ecd50c177833b71df05cbc748701372325e6bcc17f6f \ + --hash=sha256:cdee8205a44d0be91bbac4c41b95d86641b72dfc7aef1279400e4fda3f26a937 \ + --hash=sha256:ceb3e879afac028f93d272c957814695dc5569e4904262dbee92f6c41bd5e4a3 \ + --hash=sha256:d1f5a500bfcbb2c0ab85e98a0dcd70f5899d34efe365a0187700369a79603031 \ + --hash=sha256:d42a9a856a4a6e2f6c10f1318c07e7daa498d6593abe745c71dae4521a26ca39 \ + --hash=sha256:d83b12902eb8bce151259c86c03ba746600b2d994543de46e370cecf96c452f2 \ + --hash=sha256:d8e017eeb7482bed34cdb0d61cf2bcfc88d104bbab296a17cd16a6af8aabc70e \ + --hash=sha256:db3ae52ccc150dbc84704e9d642743897f3e1c54742ff34cacb661e52e3818a9 \ + --hash=sha256:dbab5f5ff6897c81f355d079010cdae85b02e5a0b518b5251523b8ad8ae9ac3c \ + --hash=sha256:dc4242ca653c9b30ab51c5f8193323e7bc0928f897ee9103201e59a43abcb72e \ + --hash=sha256:dcbf346a318a5e30063f547630b02bb787ce2f45b6368d5da143660b6a3835d8 \ + --hash=sha256:dd2ac8f5b643454c2cc6b6118b13da16e88f4a6434fc3ba61aca384029f04f36 \ + --hash=sha256:e1d52a05dc417279f7e5c7618c5dfbbc29923aaf9bc0a5c1802ddcebf54c61a0 \ + --hash=sha256:e6720ba44ad7e72174314d0e1fb0172494cff5c73a3a8a2159c3d2402ff15565 \ + --hash=sha256:e738ab81179510ce79b2eac9a6ecf47feffd9e76d1c72e403005dddb6e36c06c \ + --hash=sha256:e904d4d01f36bd6e197590be1533c44e06058771e0746dd073a8ebb3ef880858 \ + --hash=sha256:e9f165403b81fea7e89c932d89046a1e3d9a3a60e8d7ef2f249dccdcb0982bf5 \ + --hash=sha256:ec6a85f424afa8d23e0d9a094e5dbb6eda01da91c92b9183cd433768247ffc97 \ + --hash=sha256:ee19113bce2f3acd46432050688b70f61acd6857d75abb9ec96341b7e9ced123 \ + --hash=sha256:ef3b928d9c984322b5c44e6964d8dbc653da87d2d8ee1647fa6da43072e650a9 \ + --hash=sha256:f273dcf7149a50527c4fd1f55cfe9eac0f60753f5af544b4c9352578e20c0874 \ + --hash=sha256:f5470694918830da62fac9e69133b53d23b736d7070e587b27a4a2be37e08e68 \ + --hash=sha256:f574e460d1c8a08384a016fdb09ccf3543433263ed6b2f97104f979e64ea57c2 \ + --hash=sha256:f85915e00dcb1cd9f2f890ead064ed40a27df06f0db65be427b29482ae357572 \ + --hash=sha256:fc2461ecc45f17893f8207e73b46ea8ba93e33630e51cf4af3fbc21d47462b1a \ + --hash=sha256:ff6b113f50bc066a698db5d944d2c6dc7507168dd3341e255a8892fd0715a558 + # via + # aiohttp + # yarl +psutil==7.2.2 \ + --hash=sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372 \ + --hash=sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9 \ + --hash=sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841 \ + --hash=sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63 \ + --hash=sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979 \ + --hash=sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a \ + --hash=sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b \ + --hash=sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9 \ + --hash=sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee \ + --hash=sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312 \ + --hash=sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b \ + --hash=sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9 \ + --hash=sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e \ + --hash=sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc \ + --hash=sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1 \ + --hash=sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf \ + --hash=sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea \ + --hash=sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988 \ + --hash=sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486 \ + --hash=sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00 \ + --hash=sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8 + # via + # ipython + # torch-geometric +ptyprocess==0.7.0 \ + --hash=sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35 \ + --hash=sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220 + # via + # pexpect + # terminado +pure-eval==0.2.4 \ + --hash=sha256:260c2774686e651b79f8b8e7fc9d80b3599ea6a66334b47d5f4abb69fc2c0ea1 \ + --hash=sha256:96cae060a313cfaad51bb761278bfb0e62dc0248d9315a81173752dc546cd37a + # via stack-data +pycparser==3.0 \ + --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 + # via cffi +pygments==2.21.0 \ + --hash=sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9 \ + --hash=sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c + # via + # ipython + # ipython-pygments-lexers + # nbconvert + # pytest +pymatgen==2026.9.24 \ + --hash=sha256:33273cafc0f83feef15d4ce8edd6e161e0a4d4059559c108598d6414c99aa7ca \ + --hash=sha256:eaefbf34e4bc0546543a1e7502efef1d96bce5ef26d882c302caa3fee4469f00 + # via -r requirements.txt +pymatgen-core==2026.9.23 \ + --hash=sha256:000c8708927578791130b13d774eb8b27fd07ac3eee21eec3d47902ddb5f36bc \ + --hash=sha256:019cd9b2e2dcbd17e941cda7f4b01a066607a2b2f2c23a1777ed7d65916b2754 \ + --hash=sha256:1a1060ce4522ce7a2e1215354438bbffa77d61087b19fd4b6a1a9f113288e6bd \ + --hash=sha256:1fda084950ddd7b0209c740e8a5002178e6ea933cce476880d2091bec1cd2a8b \ + --hash=sha256:252f2e1d8da9d25289e75dffd4ba3f910526a1f2cbb93619f93a0a779c96e004 \ + --hash=sha256:393935c940b54e87652b1bbab57339dcffc2dd848528449e87f1ae3e70cf6265 \ + --hash=sha256:3c8209f38a531958509ca5986fe58b61232d0b0c6c1c52df14c52e9646a62cc9 \ + --hash=sha256:4b3db8660db9fc82bd34afa245df17196a1184515a7c129d00becfafba1a1590 \ + --hash=sha256:4b6f582f55b1249fee57e13a37dd0d3eb7df2db875bbbcc896abab5b46bec0e1 \ + --hash=sha256:5ef0470cc27b7bf2e3966f4b44132d7653d7a3af3f03c54eb65a064bcbd10fc0 \ + --hash=sha256:6500af2807d5da8c0d9503c9c18e20cb2e2c7a2eec553bf9423320745b68965f \ + --hash=sha256:73dac6b444a56fac0ac35c790e89b6b0a5699ed03ffedc676cd83b230fb90df3 \ + --hash=sha256:7501dd3a7e6c6bc4b1f7c84385119f2d50082c8037cc9fd0ae46db4d71316c68 \ + --hash=sha256:9876376d7ecf8f677021373e70c9121df78743f6ede5e508a1f36f743f9dae9a \ + --hash=sha256:a41979bdbb83ec361fefa0e6bd80718e2e6e6e6ae7822c61b6ee3d84e368b2aa \ + --hash=sha256:c674edad171aebc08f8ea5c5a75f437d91e53179e2c49693f5e47134ca7f72ce \ + --hash=sha256:d5dbd6a4d0a2a63e89d47e1c1b43c66de807cdb05bafe0ef42b92c8e32c3afb9 \ + --hash=sha256:d7f04549d458f245a69db6cf347a77ce02f16eacf08c6dd5a9b46614e5676cbb \ + --hash=sha256:d8ba6879837834464a275338d5fd73c0f0f3cdb232a12397addd5bf45c52657e \ + --hash=sha256:d9658112621f7a40b887b643828028673bbb56291fcd00bdd6770d28f3a75828 \ + --hash=sha256:de45e7018ddc89f2361a6c532ba37237ce7ae456e1e4661fc2383a1afa946fd2 + # via pymatgen +pyparsing==3.3.3 \ + --hash=sha256:928ae7e20211f3b6f3915a72f06a0cfd29ab9d24279dd6346b6b1a7146397d36 \ + --hash=sha256:ece8c00a69cf01b45d0b1dedabb469c90d8caf996d4fda40f147627a122849a4 + # via + # bibtexparser + # matplotlib + # torch-geometric +pytest==9.1.1 \ + --hash=sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313 \ + --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c + # via -r requirements-research.in +python-dateutil==2.9.0.post0 \ + --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ + --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 + # via + # arrow + # jupyter-client + # matplotlib + # pandas +python-json-logger==4.2.0 \ + --hash=sha256:158a52126fcd6869e09574d2b66272666f3dc8f468c62637ef9a1fa883719cb9 \ + --hash=sha256:e371ebe22ec01e289850102091a2b1f6fc9e655c7f1f5f29073936756c290afa + # via jupyter-events +pyyaml==6.0.3 \ + --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ + --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ + --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ + --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \ + --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \ + --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \ + --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ + --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \ + --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \ + --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \ + --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ + --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \ + --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \ + --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \ + --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \ + --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ + --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ + --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \ + --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \ + --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ + --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \ + --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \ + --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ + --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ + --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ + --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ + --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \ + --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \ + --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \ + --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \ + --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \ + --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ + --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \ + --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \ + --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ + --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \ + --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ + --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ + --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \ + --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ + --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \ + --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \ + --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \ + --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ + --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \ + --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ + --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ + --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ + --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \ + --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \ + --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \ + --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \ + --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ + --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ + --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \ + --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ + --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \ + --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \ + --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ + --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \ + --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ + --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \ + --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \ + --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ + --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \ + --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ + --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \ + --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \ + --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ + --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \ + --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ + --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ + --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 + # via + # -r requirements.txt + # jupyter-events +pyzmq==27.2.0 \ + --hash=sha256:00e73942ef12cecbc7951c4a9104bb8ffaed742abb13af2da6833d90dd368cef \ + --hash=sha256:010db74a1dd67c7cd8b8b30916355735db7d633a070510bb34e41ab679ab2c0e \ + --hash=sha256:0e1af01858d6dc0c09cea57f9cb1ddf4601f04897b6bb1efc3a2038123c87d79 \ + --hash=sha256:0f4bd6743e8bf854c3bfce892dd6578a514aabf128e37a4b2eafcf01856f7e44 \ + --hash=sha256:1132805970045adb9f5f05dd57040978286a8e21a5475f2c2ddf1bc983b9a2c7 \ + --hash=sha256:1ecbdd131b9669f62d3a45afee5527c7ae9f141e4301267f21714c90bd21725f \ + --hash=sha256:1f8079d0521fe94bbb401fe9407578b28f3701627c8be2c9f7e0c5b77dcb0109 \ + --hash=sha256:211350c3ccd4746bc5a85e8fe961bad1f7f2f274f67cf1f785fad7f96f562eea \ + --hash=sha256:288cc790da0e3064a14a38ddc56ba169dada8c8af4cb86518db2bcbd380eedbb \ + --hash=sha256:2c218c6ab8bc447ba62054b581fd30209689d199c6ecb253f79615ca74a38e12 \ + --hash=sha256:3146385b94a760236c5eceff468a66a296a716ca98a2e0f9217b1518118466b1 \ + --hash=sha256:348d6fd3e4b81ae4580622ea8c2ea60224e84b2ac1b3be4482e6edc7de06e7a3 \ + --hash=sha256:376981d106598beb70be384f44d8f589832fd0051d184d38d10043da3cc3b080 \ + --hash=sha256:39755dc4a923021bd0677990ffdbc21cff0e1ee1cf07fe3817acea153ef4cb67 \ + --hash=sha256:3ab6eb88590e510ab16715c32dbba12000da9bee989fdadd9ee19a234c492eb7 \ + --hash=sha256:3d45189c0c3c99f817b7fefff0d32eeef684cf33e1e3c0fc4281515357c54702 \ + --hash=sha256:3ee556ed1cf836f96de9d5e545563116426d4a94f21b8041fdc79408eff18ebb \ + --hash=sha256:3ee8dd7031d5e23f632e0e7eee67183ca7d2536e0de35dc1e5d69f3471a791e8 \ + --hash=sha256:40124779c3a56ad5d91902df1ff89159cb414b6c1a0ee697abcc66cf5e6db62d \ + --hash=sha256:40d96cb7a8f6a43aa9617c00215c2b73e1b5e4a1d6cbc9f5860ed7ac682599f0 \ + --hash=sha256:44f261eca7dfb9904ea2b56428f59ab693bbe2715c0413a701f17b067ebf877c \ + --hash=sha256:468139ddb2e494d06e586bd3a6835077e8b3764560c8db552fe685c5867fc24e \ + --hash=sha256:480dba27b145373b5e103890f17969d891bc9e86746d6b8b29dd70b0d4addc62 \ + --hash=sha256:4ebc7889b31bc11c72e9f17ba3ebb0a8b0911cce413f41b498e55383a94819a3 \ + --hash=sha256:507c0b33f95502723d325487e8e50c2cdd3b37444143f05423a3861327f69bf7 \ + --hash=sha256:54d4259d1bfae24ecdb5ca79f7acc2eac6c286a02d6a0ae617797cb45f0726d3 \ + --hash=sha256:56b48fa9d478a3af7254f397697a62f5ad3e1bb677e200b2701f0c290d97e5af \ + --hash=sha256:591c8de5851c5ea372194469fe97587b97c3b641e9a70f31bb3474acbfde0241 \ + --hash=sha256:650c6cd7cb39a069e7048261efe66fce8bf2e0052c831a7a099b7a0f2ea860d7 \ + --hash=sha256:679b5b1dde326a921ea2c9ec1f9ea3115bfe1b4735779bbc6eb0473a0ed93f71 \ + --hash=sha256:6eb63cc61ab93b01b9afc887a160255e2fbe703fdbacfe5feaef87214f51bd6c \ + --hash=sha256:714f8cbd66c7e405338d668f79d2fe83fe923defe348e843be998603cf92eeff \ + --hash=sha256:722f0a6940be1a483c81029a271d950e04dc2ff113a42e21b3d2b7a0d8e59638 \ + --hash=sha256:76afba06ae698f2b8fe4fb34b32c760a650f168c2e622f370f2c528035b7f650 \ + --hash=sha256:770a37f28ddfbe1d2c40a2e3ce37e5fd10831daa6ae9634105aa8a5d23507b00 \ + --hash=sha256:7e2579c5de82ddf4544d723c1bc8b44c3b806d157acc9fb2a2d18e10ef28e202 \ + --hash=sha256:82a09aa67871d4f2fcafd47bf670fb93210b232a7c2d4b8a54676314edf04033 \ + --hash=sha256:88c0fac061bac269076edeb3a209acefc96cd6167c239daf1c2b404ac48d7012 \ + --hash=sha256:8a5c04ad2e368142aea52d1abdf6631cb2534864e3c16ab78268ab957060b2a6 \ + --hash=sha256:8b86e04f55af0f4d8cd8ecf14c0b8b81ebc8fd66fa20126b753514628ecadc7e \ + --hash=sha256:917d601e9540098f580d2723d0ce6402cdb6f02bc8dc2de74e0dca6e13bffd1b \ + --hash=sha256:9216132843d139a123f243c07fe70f7487dce5041093dd77040f9adb5dc91872 \ + --hash=sha256:94242bd4de6af7e74665e14a88630bccd615057f6acfaf08a3a432551d604645 \ + --hash=sha256:95369ed6626afcfe2ac89832fb1b917c077fbeb905fbbe5d918349ce0222b89b \ + --hash=sha256:95f52b877149b06bbdeec2e8ea6230aad14950bbfbcfa16e7eb88951f07d6b28 \ + --hash=sha256:97d4c6622f129b514a4f5939af1b5f434c97f47085d9311b5f7f36e24b3bd447 \ + --hash=sha256:9846e881620dd62566ca76a53e384c3f37490faf4b9240aebc7498810dfca853 \ + --hash=sha256:9ab72ee77b313d0658447204c8201f9b315146e923b48c56ea7dbd005d464a91 \ + --hash=sha256:a070a9cdad1f8f8a85ea153afcc4654f11b10895d14c0acabe10f1df0e0892ea \ + --hash=sha256:a0ee3c49be2aa15abd12cbbd14d4ea2892f872c688e1e487af39ec1972ed549d \ + --hash=sha256:a7c1144dc61777938e932a2c9011b980b89fd8ff3733033b34c44c299187a6e1 \ + --hash=sha256:a843094b4d3d633bc3623e47a2ff50742d6af02bc1f7606aa2e67e971e21878d \ + --hash=sha256:ac126d48cf18aa955daabef43bf0009ff76ad4deee437d09ecf15388214b5beb \ + --hash=sha256:ae6ebbc0bfe5a21ce21e32ba567bf73df2d93888109c65acbd42506cf9395759 \ + --hash=sha256:b26f2d0493b79ce3c3112c8a12649418915582ba4707b8ed9f44febf2be71f42 \ + --hash=sha256:b398c5fe102b41e1559f7ffdae760aabd5f432d73b047b4ae0eac4e01cb594d2 \ + --hash=sha256:b8d5f66e4a8246cf77f7b8f7902af64f00553368fa0373c89d99b78f0ad79394 \ + --hash=sha256:baa2ce3485145653194d6c8c5beedd1e9f0bf46a0919c9fa2fe2204fc35b74d9 \ + --hash=sha256:bad4813f270592cedf56977e31ac1fc374fb0f6f67ea5134a5e37c19cb429a8e \ + --hash=sha256:bf0b6e4ce1bb089751c504c5493d6b0557eabd02dd21b76e9086cf964234b103 \ + --hash=sha256:c218b816220d05acf6ab1bafca58926d95cbcc5fec5024724666030466308f0c \ + --hash=sha256:c5129a8fe43ecc49b99eb75616603d483a3c2fcaef504988fafe8ea392aea98b \ + --hash=sha256:c551b9e2f86dc625fcb1a032c0d68042678caf96a8dd7c28796766b673bd5b52 \ + --hash=sha256:c7cfb75caa83f5153c687e9d2107f64b5ef0ef0d6edd260d3ff920baaaa69101 \ + --hash=sha256:c9322f9c87b0935870516c2876e1e29497fdc50439c785ece63e3fbbab06c821 \ + --hash=sha256:d1526b42a2e725b84ed226f37becedc250c6347594e5ed304e4e9aff68c9aec3 \ + --hash=sha256:d1bc1d380a91d954ed5fc9f12915dba014eed0978d2de05ee7ca688bdaac144a \ + --hash=sha256:d41ebb260b69329b7d4a2936d44c872c86dd785355b51366c8b14e07ed7e9373 \ + --hash=sha256:d61910b52be5b2cd8b248dbcbe3a1b0275556a7d99fb613fc43323b546e273b8 \ + --hash=sha256:d61a0169ba05ab7ebc48dc793f092df12f789bf378dac8321ccd966fd93d94e8 \ + --hash=sha256:d64da42cae09e6b0c61368b4cc8ca80f23ce3af17584d08053f3dc957433d5ed \ + --hash=sha256:d9527e3dbaef1edaeeb2446fa7379446814a43ade8adc7c4a5ebe69437815ddd \ + --hash=sha256:dcc99ca132b667a4ed750afd42db4ea73288f18425a9b2e3c0af095665c491f5 \ + --hash=sha256:dde5e291548ca0f397623b5e523db5c90172b32aa4fd3ba464a79ea31a580b43 \ + --hash=sha256:dea74fd65f1fc5f7fe167916a473ebe6ed6174e5e5d9de11ea6583661be6cf43 \ + --hash=sha256:dfcd024eade5870b25f890c4df0ba9421ed8167d8d3d82334237512c1158dada \ + --hash=sha256:e0fa0bc6b1a184aee59b32efcd1b7f0e6d5b8f9387799e4c16a4cb66a86747d6 \ + --hash=sha256:e1ed46048d1920cabc96d952a0d5cfe4127ad8db572c335aae4e3c57b9278d7f \ + --hash=sha256:ec8a318dfc27c7d946651b3d9e8025d5734f30c168a822195601827207bac09b \ + --hash=sha256:edce90a1e588ec63adbf612cc0ad582de4169cd216c7ae53c15f42a2ee902f35 \ + --hash=sha256:f52f08101907609cc08db6a1f9f2a7a9afd54e9b2ca16178c9c38e99fb593cef \ + --hash=sha256:f5c6d8744d10b5e1eadd90a7c58f8546acf6bf680ee463f7e6ada09ad6c9f802 \ + --hash=sha256:f707bcf2c1d007d14d70531d4dd7b41060881c73efa845580bf6faaf9ea24d42 \ + --hash=sha256:fba8afcf265c6e9fbe1594cb045d4765c6c9a7d607653a8196067ef23566b843 \ + --hash=sha256:fdaaa4ea3242f6ad298eb5177eb042aea5c73c30e76d20caee7b15af20d24ec2 \ + --hash=sha256:ff60f0f7ccfda0e303ac43bec7096007b7cdf2c41b3739d1ec667febe67acab3 + # via + # ipykernel + # jupyter-client + # jupyter-server +referencing==0.37.0 \ + --hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \ + --hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8 + # via + # jsonschema + # jsonschema-specifications + # jupyter-events +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed + # via + # jupyterlab-server + # pymatgen-core + # torch-geometric +rfc3339-validator==0.1.4 \ + --hash=sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b \ + --hash=sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa + # via + # jsonschema + # jupyter-events +rfc3986-validator==0.1.1 \ + --hash=sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9 \ + --hash=sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055 + # via + # jsonschema + # jupyter-events +rfc3987-syntax==1.1.0 \ + --hash=sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f \ + --hash=sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d + # via jsonschema +rpds-py==2026.6.3 \ + --hash=sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5 \ + --hash=sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680 \ + --hash=sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9 \ + --hash=sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538 \ + --hash=sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804 \ + --hash=sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf \ + --hash=sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4 \ + --hash=sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97 \ + --hash=sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6 \ + --hash=sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96 \ + --hash=sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a \ + --hash=sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187 \ + --hash=sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975 \ + --hash=sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f \ + --hash=sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703 \ + --hash=sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9 \ + --hash=sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127 \ + --hash=sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f \ + --hash=sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa \ + --hash=sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05 \ + --hash=sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171 \ + --hash=sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba \ + --hash=sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c \ + --hash=sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223 \ + --hash=sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4 \ + --hash=sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885 \ + --hash=sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698 \ + --hash=sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f \ + --hash=sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7 \ + --hash=sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed \ + --hash=sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f \ + --hash=sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf \ + --hash=sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e \ + --hash=sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f \ + --hash=sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24 \ + --hash=sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a \ + --hash=sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41 \ + --hash=sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc \ + --hash=sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d \ + --hash=sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146 \ + --hash=sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e \ + --hash=sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e \ + --hash=sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4 \ + --hash=sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12 \ + --hash=sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7 \ + --hash=sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261 \ + --hash=sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6 \ + --hash=sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5 \ + --hash=sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93 \ + --hash=sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7 \ + --hash=sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda \ + --hash=sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8 \ + --hash=sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342 \ + --hash=sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c \ + --hash=sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb \ + --hash=sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0 \ + --hash=sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77 \ + --hash=sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3 \ + --hash=sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885 \ + --hash=sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826 \ + --hash=sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617 \ + --hash=sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb \ + --hash=sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577 \ + --hash=sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80 \ + --hash=sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e \ + --hash=sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945 \ + --hash=sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90 \ + --hash=sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7 \ + --hash=sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0 \ + --hash=sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140 \ + --hash=sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822 \ + --hash=sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba \ + --hash=sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9 \ + --hash=sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4 \ + --hash=sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a \ + --hash=sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8 \ + --hash=sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf \ + --hash=sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4 \ + --hash=sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324 \ + --hash=sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53 \ + --hash=sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b \ + --hash=sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41 \ + --hash=sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9 \ + --hash=sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca \ + --hash=sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1 \ + --hash=sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d \ + --hash=sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690 \ + --hash=sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107 \ + --hash=sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2 \ + --hash=sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76 \ + --hash=sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d \ + --hash=sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af \ + --hash=sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6 \ + --hash=sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db \ + --hash=sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369 \ + --hash=sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd \ + --hash=sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911 \ + --hash=sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504 \ + --hash=sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a \ + --hash=sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9 \ + --hash=sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13 \ + --hash=sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc \ + --hash=sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278 \ + --hash=sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868 \ + --hash=sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2 \ + --hash=sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd \ + --hash=sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4 \ + --hash=sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6 \ + --hash=sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9 \ + --hash=sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00 \ + --hash=sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f \ + --hash=sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e \ + --hash=sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442 \ + --hash=sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da \ + --hash=sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90 \ + --hash=sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef + # via + # jsonschema + # referencing +ruamel-yaml==0.19.1 \ + --hash=sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93 \ + --hash=sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993 + # via monty +ruff==0.16.9 \ + --hash=sha256:0e1dbc2073624dee6618d41d0098690a7244654af746704b64759e12b6b6b385 \ + --hash=sha256:12b625c6cfba78d285d9f48eda5f053374f1e53cb10ef17342a383750db99161 \ + --hash=sha256:1632eb1d6197f33bd00b1acbc5b71009e89a8895c158e2d2b03a834fac964ab6 \ + --hash=sha256:41e3870277694177429b56406d65dfbdb2c2802c52b715edaf6a0b829c69d4ee \ + --hash=sha256:447fc07e1573afff7cb02803462b12b6c8ece7cf10e2cd78565fa6d7a1c0bf8d \ + --hash=sha256:4684dded7db60aa57cb118fa158630f5feade4af5782903b6053484bdf9bd129 \ + --hash=sha256:6bd40fec8cd4c8a3d4dd589bd8ad4e6320c13c29234159bfd959a40d529d597b \ + --hash=sha256:7baa24ef5fc8e77aa93879e1d3f43754a01ae488e869f1ae30cf431afd4d2452 \ + --hash=sha256:8a3e039a6a40ed976c491722b60e0ae4a4aa1a86057f540ee7a37a5d19ae9120 \ + --hash=sha256:8adbe4e58af167f767d7b2ba5e83c42e878350796cf78c2f5e14ab9903a92588 \ + --hash=sha256:95e6f022090368ab3b824c36276839c53b2adf1a3f4c09fefc33dfc400f6da96 \ + --hash=sha256:a21713e629d3e5bdb2f5c2def1cc7f04f47fa8e1a7eb0571b4a28e1da64bc728 \ + --hash=sha256:a41aac6230aadfaa133bdfa1614488531ffa3e0837567ae04c0da2058a9c0f9e \ + --hash=sha256:a5f27be168556594a86d2f415db0cf43f5291917849318f873c7e2791f7a8c67 \ + --hash=sha256:b3f951b14d865d5952c89d40a5ca07e87abe24fa5453299878411e127748fb1c \ + --hash=sha256:c2529fb5896d49115b0e9aa8f887490b34bbe76baf879ec2264ac59406869ce7 \ + --hash=sha256:d29c934357e45642fda2f34c0b1f4025b4a6c01e15e4bf0016879d60078a142c \ + --hash=sha256:ed1a252039200f57a59eebc063b54beabea67bfbaaca0eeaa7f54b5fbcda2284 + # via -r requirements-research.in +scipy==1.17.1 \ + --hash=sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0 \ + --hash=sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458 \ + --hash=sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118 \ + --hash=sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39 \ + --hash=sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e \ + --hash=sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6 \ + --hash=sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec \ + --hash=sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21 \ + --hash=sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1 \ + --hash=sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6 \ + --hash=sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce \ + --hash=sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8 \ + --hash=sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448 \ + --hash=sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19 \ + --hash=sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b \ + --hash=sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87 \ + --hash=sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4 \ + --hash=sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9 \ + --hash=sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b \ + --hash=sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082 \ + --hash=sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464 \ + --hash=sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87 \ + --hash=sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c \ + --hash=sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369 \ + --hash=sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad \ + --hash=sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f \ + --hash=sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c \ + --hash=sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475 \ + --hash=sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd \ + --hash=sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866 \ + --hash=sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d \ + --hash=sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6 \ + --hash=sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb \ + --hash=sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca \ + --hash=sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0 \ + --hash=sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca \ + --hash=sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d \ + --hash=sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee \ + --hash=sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4 \ + --hash=sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717 \ + --hash=sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49 \ + --hash=sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2 \ + --hash=sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a \ + --hash=sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350 \ + --hash=sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950 \ + --hash=sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b \ + --hash=sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086 \ + --hash=sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444 \ + --hash=sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068 \ + --hash=sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff \ + --hash=sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a \ + --hash=sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50 \ + --hash=sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696 \ + --hash=sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21 \ + --hash=sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c \ + --hash=sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484 \ + --hash=sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118 \ + --hash=sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3 \ + --hash=sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea \ + --hash=sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293 \ + --hash=sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76 + # via + # -r requirements.txt + # pymatgen-core +send2trash==2.1.0 \ + --hash=sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c \ + --hash=sha256:1c72b39f09457db3c05ce1d19158c2cbef4c32b8bedd02c155e49282b7ea7459 + # via jupyter-server +setuptools==84.0.0 \ + --hash=sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670 \ + --hash=sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73 + # via torch +six==1.17.0 \ + --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ + --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 + # via + # python-dateutil + # rfc3339-validator +soupsieve==2.10 \ + --hash=sha256:49e9380d7d2905463583bafe285e818c7366a9ed7b3aee221c1ac79c905d8bc0 \ + --hash=sha256:8596eb8967d744174820280fa62b4542a2e955bfaccca73ed8a13c6eb8e9b502 + # via beautifulsoup4 +spglib==2.7.0 \ + --hash=sha256:034a46ba7877b584e271e15f9858d871f6167c2d2d6a1d26f3a7f19b55572f45 \ + --hash=sha256:067d31d40e962d6dd5102e3b45d67c85d0effa4da072a5b4987324bd6676a0a2 \ + --hash=sha256:0d2629fa81f85e389af22f2dd1f71ee325d5614b5907ba99b8f12cb97ec1d411 \ + --hash=sha256:0d8ecf030d13d67c4cc272423e5652b74eda57f86a0b118e007f6d12974cc256 \ + --hash=sha256:2a5a7c19210251263dcbed0fca6a66ab90546bfcf6b085d6fbdd636c4a29359b \ + --hash=sha256:39b978c08ef2ebc0eaba833c488fc4c0f9b1fc0f50d4a8584f176741eea69376 \ + --hash=sha256:3cf0ff80c01d8631ef4b9f1b78da79ff2044834e6e2d870f7f20c8579c921136 \ + --hash=sha256:468879702577124dcde0607a75396576e256f1cfa2d8fe48da4a928fbb27abc6 \ + --hash=sha256:50629939a9cd6fa3df5a12f6f025ceb3c78534284f875371574c360e4ccaf5e1 \ + --hash=sha256:54f4b6e789475384c62e759c618172707f261c0eae8017949fe4994b6b8cc779 \ + --hash=sha256:59f134e74f7f488de4bf5579ee6a35af25cb2c478c138de664fea1e14f3efbaf \ + --hash=sha256:5f334b4b66c8aafd583fafab5b15a56e27efdd2dc6cb1064dfcd0fe59ae130f4 \ + --hash=sha256:6913906fd9108e7bb2ce06a810513a95a82d801530f10230979bf3427bb7e771 \ + --hash=sha256:77d588a1adb62a8ab0cd2cd32aa1f6529a728eed48b0db146436d5d17b1b7d01 \ + --hash=sha256:95a65aa85f75a2046b87896848599486aa8b297e3ff480a8eb03e4fb339b65b4 \ + --hash=sha256:95e3dd7ef992ff8a88f6ef2e5909aaa60ecb479004cc1f73c1e6285d54227960 \ + --hash=sha256:97e0fcea2db3915bd973fdd2cc0a757b1f99bda71ce815da333d75ad1ffc3eb1 \ + --hash=sha256:9b6ca88bb6e604bc8f63efe87b3b2470c2e25f56988b775bd332cefa8866f5c5 \ + --hash=sha256:a7b29d2cfca6ac53e927686ca0b91257126e47f6abfa26451723a5cd40070352 \ + --hash=sha256:ab061ea6a3c3c25a1d0018b09c333c0458792036d3f45d892bd52793ed1f1bda \ + --hash=sha256:af2fb180e3e8d0f3ab172e7c9c2c5a75de6af8900fc5914d94d8674f022c166a \ + --hash=sha256:b032842fc223de46d2ef7d220459e1a61ed90329ac2e72818c605f1fc87451b8 \ + --hash=sha256:b7e29c796cfdadcc3857aef330acc19b9bc50c83e9911fb23b28390e7c80bae5 \ + --hash=sha256:be28673e90f7a6c7770f73c57e529d2bdbb373d06d26ee5e90991b548e9238aa \ + --hash=sha256:c40907a42c9dc45572f46740bf95412f84fb0eda30267e31665d104a4bde6627 \ + --hash=sha256:c76411bc1b96cd87c8733994747c7692512b583bb4ef89a65463ff4255221c11 \ + --hash=sha256:cb77daaf9dd5d48d523a888f37cebd47fa63ff28dfcf1aac2b031b914f9ed55a \ + --hash=sha256:ceb6730a2324d0c83579c803f3782e28bd41e79bbfe0c3dfdbf30e3d3a6320e5 \ + --hash=sha256:d25bb7e6d367e139b8f8afc3031509e0ded737a7c3b3ea060a7382dc0c10a84d \ + --hash=sha256:d5729ff0040baae764c17249302cd99f0eb4e73449612a8c69d3e60a215f062e \ + --hash=sha256:ef70132e23dfcc7ab6813742e0edab3f9906e61cd11c857f014bd5610a8bc88c \ + --hash=sha256:f627a4ed6f2396ed6e3e8eaf33a53ad143c8ffb8756a84a640f4569ac5ffa2a7 \ + --hash=sha256:f892ecce2dd1bc636b14a4e5bc13aabb73b008bd37a4d23636882c8971c432a0 + # via pymatgen-core +stack-data==0.6.3 \ + --hash=sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9 \ + --hash=sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695 + # via ipython +sympy==1.14.0 \ + --hash=sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517 \ + --hash=sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5 + # via + # pymatgen-core + # torch +tabulate==0.10.0 \ + --hash=sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d \ + --hash=sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3 + # via pymatgen-core +terminado==0.18.1 \ + --hash=sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0 \ + --hash=sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e + # via + # jupyter-server + # jupyter-server-terminals +tinycss2==1.5.1 \ + --hash=sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661 \ + --hash=sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957 + # via bleach +torch==2.14.1+cpu \ + --hash=sha256:36a03a3f87ec875ca39c8e29eccbaf0fc704605f78a3f7f179f157048c3c395e + # via -r requirements.txt +torch-geometric==2.8.0.post1 \ + --hash=sha256:2c0c81666ec10f2132f6b4e0b6c57fc813f2c9f6b57599d7b7a799c614c22fbd \ + --hash=sha256:5d9841cbfa64eadc425e445767127d103dd52fdc5890548c6364986c9bc78029 + # via -r requirements.txt +tornado==6.5.10 \ + --hash=sha256:302eb1e0e3e159314eb591920529fdea80acca92df5510a2cec5bbd4f099ec72 \ + --hash=sha256:37ae8f150cecfdbf747fc4e12f5e9a97ecd8cf1d4cdb3f119e2de84b11196918 \ + --hash=sha256:4bd192b959f9128fb99b8898148070ba4574c9589b78bce42d1851131fe85828 \ + --hash=sha256:66aaa3f57d30c6e6becee83ff28055d5930ac724214bde99393eefda83d5e015 \ + --hash=sha256:69acca6501eed74582b76dbbceee2a91613f54728e3e418346000d7103101676 \ + --hash=sha256:83e6cf438b106c6b3852d70960967bb1b70c87438050dca0981e4b9aa751a4c1 \ + --hash=sha256:9261783640e23258694a9ff0795df430a5a7b0a651d3dd53dd0969ad6be16da7 \ + --hash=sha256:a6b1ccd08c04b4a06fb5aeb381be99de5ad1e5375c1785e31d78c880feb57687 \ + --hash=sha256:bdf942448169e5336451d0494d7e3d81cfa726d5aa312affdc4682dd62a62f6d \ + --hash=sha256:ce045d3c298fddd30e89a2777f97039d1b641eb9518ac7b26a4721903539c694 + # via + # ipykernel + # jupyter-client + # jupyter-server + # jupyterlab + # terminado +tqdm==4.70.1 \ + --hash=sha256:c293e525e6fef9c20e8728fd4612df02a0aa31bb5fe91ecd93e123b1b7bffa73 \ + --hash=sha256:cefd0eca11b2a37a3aee776544d4f4ae913f02688135b5556b8788dfa474afc4 + # via + # pymatgen-core + # torch-geometric +traitlets==5.16.1 \ + --hash=sha256:ed900c2b631aa3a112811139fa97b8d2c3bad5e989656bba4b7e52c7852c18c1 \ + --hash=sha256:f775618166caa0396c8e337099240f2bd3e5e917d203b2e6fbe21a58d3cb1f6b + # via + # ipykernel + # ipython + # jupyter-builder + # jupyter-client + # jupyter-core + # jupyter-events + # jupyter-server + # jupyterlab + # matplotlib-inline + # nbclient + # nbconvert + # nbformat +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 + # via + # aiohttp + # aiosignal + # anyio + # beautifulsoup4 + # ipython + # jupyter-client + # jupyterlab + # referencing + # spglib + # torch +tzdata==2026.4 \ + --hash=sha256:c2169a8b0a7a5e9674da5a135ccdfb2b3e671b333ed9fed17b41f73c34476e81 \ + --hash=sha256:f1b8bd365d8d210c55353f4d7f8d6d8561c0ba50d704b700d195a9424bba0d79 + # via arrow +uncertainties==3.2.3 \ + --hash=sha256:313353900d8f88b283c9bad81e7d2b2d3d4bcc330cbace35403faaed7e78890a \ + --hash=sha256:76a5653e686f617a42922d546a239e9efce72e6b35411b7750a1d12dcba03031 + # via pymatgen-core +uri-template==1.3.0 \ + --hash=sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7 \ + --hash=sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363 + # via jsonschema +urllib3==2.8.0 \ + --hash=sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3 \ + --hash=sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63 + # via requests +wcwidth==0.9.1 \ + --hash=sha256:03cfca3dcbffa86564290fe3c9978a6191ba003e8ced7f7dbda315fcb3fbe725 \ + --hash=sha256:0665ee822ea04e25801e6e82e5407528be0863e88a17b0e7ca038843a4a3ac0c \ + --hash=sha256:0d68a30d504c68cfdff2a5f804675c1e7ab4c0bbe878024c8b680ec5579cde67 \ + --hash=sha256:10b00ba23482e352f874d2e8135e7ace9da838646c7dd800566246bbd46125ff \ + --hash=sha256:356376852357b8fca71fe5415808ec421679e04b4a98eb7c9cb6a7984b911a05 \ + --hash=sha256:40d936d72c9bdc10df43f93a8be502bc5024b487259139f66a328722c07f34a9 \ + --hash=sha256:5823209b0d43af322ce698c689380d7c15ca31fa8e6e3be8459f27031bef0af5 \ + --hash=sha256:61bd7aef9cafb6cb77a37a169998d7928ce82a51522146d11f60db9e7d1cb43a \ + --hash=sha256:69bb970cf5652b88cfdb9d3fdd1764fc15e5f8ad531643e7bb3e894cd969740e \ + --hash=sha256:6e1272b7986cefe79783737e38bdb9eaae0682b333c7bd132024441193dd5ce7 \ + --hash=sha256:708158c082364af442f9983de7b6ec9ac0d2e1b825ada25f0911b1f138d55405 \ + --hash=sha256:747fb724223f417a17541a95a17c1dac3a8ef9a0cf41684950f0eab191a35f65 \ + --hash=sha256:991d1c8834f548e9c1f16432075ee84638e122312556bbf1ed595ea8fffc4673 \ + --hash=sha256:9f1636c5075ffd5c2e835b4561874f6e5dd2bbeba3c6c2c99f067d0d16883af8 \ + --hash=sha256:b5da43d6967668982e44a52fb551967d293f86d26cd86036ac95bbdd34394ed9 \ + --hash=sha256:bcb9ed4a367cc025bf1092ac679a156759605182d60b7ed3c28f7221a42ddf55 \ + --hash=sha256:c4cead196551112cb8f43cdd1f80c235ef2456e34b1a9955c424537ec99961b2 \ + --hash=sha256:dc10e262c3ac0abbfd0a2a51e45a848b1b7f500b21ff512630277973ba25674d \ + --hash=sha256:e0c3a1c45c5b9550c6919a4449e95f5b177f6e165786376981db8f1addae9b21 \ + --hash=sha256:eab587e18e7cadf1a750b0098fc8bebfb62c125eb9306f2268f0443a282a3d78 \ + --hash=sha256:fe021c4d8de9d36c31a0cb41d0d2546dadd3b0708a301f1a0c66ce200851831f + # via prompt-toolkit +webcolors==25.10.0 \ + --hash=sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d \ + --hash=sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf + # via jsonschema +webencodings==0.6.1 \ + --hash=sha256:565f9ad031c702dae404e27a099e3e09186a3ab1b9520f06d215502b651fd910 \ + --hash=sha256:7fab6269c8bf237c657876b52058ccb182e861518d1c695c1a9aaa8c1c105d5b + # via + # bleach + # tinycss2 +websocket-client==1.9.2 \ + --hash=sha256:0fcb57545848be86992e128218fd96dd87a6769ffdb1a968dff79632b85604d0 \ + --hash=sha256:e1a673830a9c7bfa47b1cd3d5e4178f4c9651d80a4eab02c9c23a1c3ec6250ce + # via jupyter-server +werkzeug==3.1.9 \ + --hash=sha256:55ca7c70a75689be937aa27f8ff4b018f06ff4838fc73045560bf0f5a1291060 \ + --hash=sha256:6392e50c78460ba618e5b21f08a71f59c99ce99cdc6cf6e3dd7e6ccca8754fab + # via + # -r requirements.txt + # flask + # flask-cors +xxhash==4.0.1 \ + --hash=sha256:0163b5d259de23ae9e07b7eabf435ce4704f6f205589a2b154e6af4be985ce1b \ + --hash=sha256:03600a8987849b2bef7be795a60a6052b635c63fa98b718b08ca5ee823691cfc \ + --hash=sha256:04f9a24de11a6647666d5302fd73d6a5224ce50ddc965fb0bb44cee736e6bd7c \ + --hash=sha256:06713a5aaf1d0905c5579416c020c02e42b3ceb931e86c7d3b7fb85403dee3f3 \ + --hash=sha256:06d7fbd609503c3be5e65cdb6bb2f040d6a98574404e2e1d5c60815c97fff4aa \ + --hash=sha256:0718ad66f4ded2411f8e62bdba549ee71e313a2d26ef5060ca3fdbf29897dd3c \ + --hash=sha256:08ed8da18cd4fd0a6a5d6a444852d8fbd0e565388a74a4937085451b5f1a312a \ + --hash=sha256:09f9feb118966cc6650e1806205d577eae7ca394aa6acf349a0b62a94bbeb329 \ + --hash=sha256:0ab851b45c70d4992be7cdeeee16f97a0b677408c758c4b1efb1cfe8030bfd37 \ + --hash=sha256:0b1082fd0f089ce9098ed77aad8b777b5d156f8ac601c69cab73811822b8ef07 \ + --hash=sha256:0b20a06454b34f1531fc677c54efe2ecdec691ef9224f7fa919bf2c1363f7ff1 \ + --hash=sha256:0b42a5a26607e4b2409fea174773a66f2dff9dfdbf2c1a851bb7b804e2c97535 \ + --hash=sha256:101aa300de6ceef3d9c77569706330d8921fc45dd82bceed2084f1e9f2557a24 \ + --hash=sha256:1216f7ba5683f17a89eb7dcb4bc50a0b743dfe1902278d7b3d0786f538118433 \ + --hash=sha256:1642907941ee4b75aacc3db688af52ea02ca2305ab22af7ee686ed726b332684 \ + --hash=sha256:168dd6b51725a222abc722832e56624d15a63fc2e8249021509c93f1063913f6 \ + --hash=sha256:1749f0688020209fe0d357ce1e1cd9ec9c6161ed0405ea949d24581c4c43fa91 \ + --hash=sha256:1b3cccf75eeb5b01639b2feadb042a8e07889293b7ca72fa2985e7dcb64763cf \ + --hash=sha256:1b50223d92df94d54e1a31469335a2c74b16692e6c1cb726f1e6949514458706 \ + --hash=sha256:1bc591533fc975614f7e13594daee76af96b8e1fbcf8de76c8773858fa9e7cea \ + --hash=sha256:1c2200b98a805351cb3142ae4e1fdcc9e91b5e20f5d30d4862b0b96f92558f4e \ + --hash=sha256:1c7c642a0f79c3e3cf2965475507574d3d1a50ec71060039d60cb87358667cb2 \ + --hash=sha256:1ee523f51718e41753f04f7102bb4dc55a18d2ea5cbaceef8ec7ca08571bd428 \ + --hash=sha256:1f3346c5c287ac3c7f38b20380f55e8768230e7252af59fabcf3b87ab21e4256 \ + --hash=sha256:2194bf96d5f3d4e0cb65deba370ec83dda3edfba42155f9384190ed5e51ea5e2 \ + --hash=sha256:237b8f63a2a0fcfb1ffc06e21dad23add44e6d354b2b014364a1d41e419a4dee \ + --hash=sha256:23a4376b4a3183cb50d4d2a3179f887a7773cc695eb2c908e551bec3221b8c60 \ + --hash=sha256:247ece770647c0aef080561fa996f9774b4dadce2d0c42eeb98229db7dcf820d \ + --hash=sha256:2696bbac613f6880fed60316c298bf3091d4f8eee3ae2e9466f70bb76204fb0c \ + --hash=sha256:26fe6238c2d5b11ed5063b9bf4eb290624b004fd074688da6bb079bd564f10d7 \ + --hash=sha256:2d52dc7c33c1b83082b707f6b7814dc76d2faaa2ea62bd9c5fab4b36f83c087f \ + --hash=sha256:2df3ca8757dc381e75e90a4d7995a6324f58a923c7145220a7b2c0231f66fddc \ + --hash=sha256:303121aab4b7f898058582d7962ea79d9e26e2379d7b6d8743f70f2671674481 \ + --hash=sha256:3088dadbffa33c29e0518578430a7dff2e901a212e487aefa5faaa0dc06dad34 \ + --hash=sha256:31d86f9e81f3e84e00131ac7c54caf5119ae4ddd82c09c31cff597c813ce1ee2 \ + --hash=sha256:3358097d333d40657569ec1121e21043dd7d0efa10aead1b50e8b4fa83077d7b \ + --hash=sha256:33e270d302c95ec426dfa0f5a4e16bff2ab8d7b8a46faa4746affb05e684ac77 \ + --hash=sha256:33fd538191f47071deef6b1f676535e2aa770f1fd150ae4cc75a34c9e930be3d \ + --hash=sha256:348c8f288dc961d6bbd1985c8152a3ed7a85c95df00e82320f0c5215d922a399 \ + --hash=sha256:349775ac30372b344d2338b2a168c0a1312a644194da25b8bec476d55761a128 \ + --hash=sha256:34ed93e20bfd98d722b902121643791eeb4b1641871e2dc63d0d4c2d93f187df \ + --hash=sha256:37f667dee0f867c42894b34e2a6fe26bf195c0ea4683d9d2b713db023f242c3a \ + --hash=sha256:3891efe3d7a531ce6da0a4a50a99dd41c75b8fd4ca19d73c86431b4db5c305f0 \ + --hash=sha256:38c3d22129a6958846a3098d68bc8e661704461c0be4793ae28836e4690c8478 \ + --hash=sha256:3c2445edafc300cc40feb6a25a8356a971c30cd0bf47b5349c2ad74c508343b1 \ + --hash=sha256:3f68fe400ceec235f3e4a4b02a28c2fd2d283584a193223c921dd4c48f1d0754 \ + --hash=sha256:3fb1d30d4b6d6e2c4a08e5ac6fffdb2b572d2cfcca15a5509cf4e7a1350f955c \ + --hash=sha256:41e579025a6e13a99e6d71e39c9cfc621a0dcdbbf19106325e145fa858f2d794 \ + --hash=sha256:421b94f3ba7067958d02e38960d987756347aa150df06df11aa68ae1af78c619 \ + --hash=sha256:427b62d62d4f967fbb10b82a3813e4875c2a6e7e7634739f17265b650c7f65a6 \ + --hash=sha256:436e11b4dd966afe5f7f665e4cc4c5485ffe3ceb42f25a22e1701d236abf1853 \ + --hash=sha256:43bcf2a871f28f16135545415cab3ec43904d4c80425a64598a9e6cebfb2b5ba \ + --hash=sha256:43e5f9169e73d0f0db33b5f6b8554bcce69ac278c966daf83d5eb4eb2f13829f \ + --hash=sha256:440c401e146ce64bdb3beb8ff0c84677b6f21307c28a34779071cecee5d4d70c \ + --hash=sha256:44ab12e8cd17d4f001769f00ad465208b4bcb897ed29e65f058f74466b57a98f \ + --hash=sha256:4528cf80ebbbf57d40edfb31521ae265daa6dd636d615b1cf0ac86209579e59d \ + --hash=sha256:45e88111ebe331de478ef8d4293efbe88f3cf8b863386c9a2357136b838e1af0 \ + --hash=sha256:4741d42d59e4e5fa1a86c17ab9c27dc8ea459c700d91b6742fdb9138d9a516cb \ + --hash=sha256:4751f1d7eecae6b2d2a773630f1a7248f125c9a92a456694d03c15bceffc9d68 \ + --hash=sha256:488ca5c5e28ef56ec4bbb12f835b3f1cbecc5f3510062e70117bc6594851932a \ + --hash=sha256:4972332c079d6aad69c4620a68d015a4ecb33141583f70d642cf9edf6a713763 \ + --hash=sha256:4a252fb862b0ae2590587e625f47a0e03da05cf0205e8830b67b6596c06038b1 \ + --hash=sha256:4a76345f5aceb4ec404918edf9c7f2b5507db864dc0d7455982009ac0890b57b \ + --hash=sha256:4af350bc3f329970c0e3a59af84a8a30998bf8a9167eb50cd48e59baaa1d7bec \ + --hash=sha256:4bbf3ff651e0f1a19beb5d0f48e0874a9bad2482a588c9d214c96ef1fff1cd9c \ + --hash=sha256:4e5141543c7f7fe3087500bbb4ac2845cb528a980aa91f8f1e661e2292ff4a5d \ + --hash=sha256:4f5e5c6df4b703afcbe9352d238a51efd97c3b91fdc3a2052e40fdacb1e7505f \ + --hash=sha256:515a822c73abbf6a0b7c70976d9662be342835c9d78b8dc7c023411f39c35dbc \ + --hash=sha256:554f87034635bcec47c5d72447bf3db7e02da1bf493a0ada010db28a76f891c6 \ + --hash=sha256:567cbc630302a46a8ecfd943b309ccf5372bb3718f1f3762d452df30f033bcf0 \ + --hash=sha256:57d7fa8f23908d173001c21a9e82bfc6ad997d1b6c270fb121812b7ed158891c \ + --hash=sha256:5adf927dca8c47fde7e683fe69efdd81bc865c4db1fb6bb00b391e2b6185207b \ + --hash=sha256:5b7875ac1a2edcb691f27642b8b94b904baa6bcecb7d79c72df2228ba8cb5c51 \ + --hash=sha256:5b7979f71d06ae45a769de0699900a246d8cb632db1e8bfdc79ec019063a503c \ + --hash=sha256:5c2d525a3afabcd8e3549d85fc7e111fde6bc302d06a1893fe73adb79823415e \ + --hash=sha256:5dc434c946012e6d8a72b10f970ea30755b718251dd7591dbfdabafd3bcb21bc \ + --hash=sha256:5f1ea31d61bcd2cd2f3ec4ca80a64187bbd7948f490b63cf0dcbc6e717b4c1e9 \ + --hash=sha256:62198213fc3e0c56e567894b318ba45834e007d065f84ba6dc9165d21546fc56 \ + --hash=sha256:63aa52659bc32bb9bd7cb5caf523b4d14429a477762cfac886132d687c1f80fc \ + --hash=sha256:649f2682c090cca1ac4037866381f3652eaacbd56e5178030f4ce1325b8f945b \ + --hash=sha256:67e57b834e07ed973cee7b6da1548ff28a56458d77696fd2a5f397f340694848 \ + --hash=sha256:684160b3c0a9b62c6f0de90f44e11dc5d8643dcfa18a5856b45fb1c47478bb71 \ + --hash=sha256:6a8c5ce76b94ba49f3be8a8f2611abc6564210702c72ac9e237ca2bebfd17794 \ + --hash=sha256:6a9f98af872355e0c02439e48583958eee00e60b928bb20476460d9d40cb7b4e \ + --hash=sha256:6c45258a37fc22721395c09927cb982d3e7a83607cab15be7e2416501bd3a330 \ + --hash=sha256:6cbf4e21ef0890804b5bb9ad25c48f9c127758d7f6c66bef374efcacc63c738a \ + --hash=sha256:6cf633df84d80a1668fcf61e330791dae46825e395549e7d34f376411e75088a \ + --hash=sha256:6efb8f21cc136c79b3e5bb747c8682d37916fb202cdbbc32182de5c4e47f821f \ + --hash=sha256:70129ebb8f20e1ac1da58b78ed381624bd689a43a9a7366560bd8fabea145105 \ + --hash=sha256:704381264b36a18b9c62ecbabe2e71d0fc58c77c129c15355c989b10bf05b6b0 \ + --hash=sha256:7236be540d6be9ce448d98b940dd26ddf70ca41012e8a14a53fd9354cefe4e8d \ + --hash=sha256:72f34834518157a75e7090f328ee7a16c70c804cfc7c694fa069cc888e9fc03e \ + --hash=sha256:74379a577a9f3b6afbdedf1b90e5c7764467051977f18a326d7d607336d743bd \ + --hash=sha256:74a164e8b63f1e9cf35c9a7809d082b033d1a00e7375d5d814415436e7867e57 \ + --hash=sha256:760de77279e9cf9c81d012ce0705cba13afccee9b09c480f17d778c8c5cefae8 \ + --hash=sha256:764b32d52d15b8b95ac8160e540772fa1adeb611fe40bffaeb42e7bf98279e44 \ + --hash=sha256:79a3203aadf39637869dfea1185227d8452844d78b837e54fb1117b4d34ba5c3 \ + --hash=sha256:7c343ee174d417a44d0c3355602c0cbbfa52a04d1bbbf1723378c7d2c8f60626 \ + --hash=sha256:7e27dbed5c4ba033919e4b4ed8dc14e029e91d14a93cd9f920d25277c7df6781 \ + --hash=sha256:81507a68ba84c55241fb61cce1469f473a5da4205fc8ef6f698e5948eea8dd88 \ + --hash=sha256:81664268dba92e037b740ecf37fa02f1cab4a391f93f28e35792b3341c60648f \ + --hash=sha256:839f58c5bd9989875be0fd28446dbf32cace2c2cd8bf2f6762acdc38a95cd1aa \ + --hash=sha256:83b8c2013edb5dc1f9e7268b6496130705bc48d79c86bb8817b3d210b81a5513 \ + --hash=sha256:84df5f8da574caadbc0cb1b8866ecc2368cc941f0cd05f677756c802f370dafa \ + --hash=sha256:8580aab306888224074c7edeec734de0c3c5ccde65b2da4e6c9a5e28f7c0a1bd \ + --hash=sha256:85bdd40cb505a11e0ca04191711266c5fd696ed786ae83849955e457774edc96 \ + --hash=sha256:85e402dab0f9acd3604539747c6fcc57dc188a18af6ab07eb8189351cd32466c \ + --hash=sha256:863f3d3b44110f7243e86cf994aa5c5d88f2348b6e84ab4402fadadfbf9f7da7 \ + --hash=sha256:86b2b12bec60c678ed8f5cca0258ad93a8928ebddb6ca7732f0875afe1451d1a \ + --hash=sha256:87aa309a93bd5ec13f14309a305ff4e9bf74c5363fc46c264c0a22edfd5b0670 \ + --hash=sha256:87cbdec1a7dd930079671a60b249f3ca4e773e6fbd0676e21e36fdc9dd0f3b00 \ + --hash=sha256:87da13df72c5612771cd905a8b121e0bfea62d7659b1c92198736eb722220e83 \ + --hash=sha256:88d87719fe6bddf117238b341c5db851f8e96ba68ad9832b450e4a43dc60b37f \ + --hash=sha256:8b4477edc03091f51f5309406d230851c23cf4822029e3bf40b8df53093fff1c \ + --hash=sha256:8b99ebaf9e816ac5069423b1367ee7e8078fbcebcf62545506bb0608d2f4f468 \ + --hash=sha256:8ba782ca3bf1e81492611152b9a0d5264971339e95e34d69de0ac2c926be496d \ + --hash=sha256:8bcba9456242ebf180a04d9443812fd85ffe6bd12bda464dd116fcece8886ff3 \ + --hash=sha256:8c9fe122444e129881afd1d4d1c7ac0d3ce2d91b68c2b40173b6025ff1c31f9a \ + --hash=sha256:8ec4777d92fd61a5c8fdeddab894fd65bea301a8092fb5419ec6472aa4d458d7 \ + --hash=sha256:90cb2a1c9cc503a054a19612b48ff6e8e47805f618bdb3224a07568aad03a37e \ + --hash=sha256:9283d9dd6b44acad35118e2976fc763a065509e4118debdb61916ec322ed17b9 \ + --hash=sha256:94ac8a6b8c47951173f0b67bf862bcb971bf24e493b9fbbdb0e010cbbc7d9f54 \ + --hash=sha256:96d8de55029d42251945531f6aa7590c32b48163c66a43bf29d8657d7446a377 \ + --hash=sha256:96dedccfb09a73a25751053a183159b88f4ee75f388df8166040c152ac0531c6 \ + --hash=sha256:9761ff4a0ffa583fe850731ad24fe82c88cccb7a2294727db0955f3279a4cb3f \ + --hash=sha256:97b455de3e8b1b0b1e4594cb61a468992563f03ca264062fbb0a66b393c01d90 \ + --hash=sha256:97b94fb29abf21f5f0bde15f7dbdd3a4aa2dc59f37026adc7b4bee8563b84375 \ + --hash=sha256:99054b838b74d8d3995ea0d410976ae967c46207ae22d6ddfc535e809197dab9 \ + --hash=sha256:99166cc98637e8bf550cda2aab07f4f1d5f899c45fbd721801aeabcc9d404824 \ + --hash=sha256:9a51b061d54cda8b83e62c44458bfbf0dabbef9b975dd9649952ba5076b9f349 \ + --hash=sha256:9b1dddc257279417d93c9e59420d49ef90aece90d7a01996db3aade74b0281b1 \ + --hash=sha256:9c3c4b9aa9a27196b921197f7daf9e6c1412739df06a99cfa6e923879362eff6 \ + --hash=sha256:a14578102a6081465aec9cf73c76c3cd3f79f0709bdb3b8ae7ab0b54c9d8b089 \ + --hash=sha256:a16a3fa6936e36bb1414d16a6bd012c9033e5161b68b426805b61d895392437d \ + --hash=sha256:a33de7633c948ab2dc144af370a66e7e7af29b425dcd0f7e4f59689fb9391b53 \ + --hash=sha256:a43418e1a90b4809a9caf64aeb8b0696e3e1f300a323acc1e6ee2f93ae319fcf \ + --hash=sha256:a4553d36cc0b7fce1f35ba8a94dfd775aa3ed12f5eab2dc3b46ac75a0706b0bb \ + --hash=sha256:a5b21b42a01a343096a1c018d35e9b7aec9c7065dda53ae8da071e37478b2cea \ + --hash=sha256:a65785e653573fcd1e33062760ab4c3c3440e8e910765018e4b6ed4ad07b54a0 \ + --hash=sha256:a6671a8f6ea4f2101ce11fab5023a2e59391cff249fc3928cecb69d971525fd5 \ + --hash=sha256:a69e8946e4902ea11fc1c557740cdbfe7d75c78fcc5e4324ff89a696a634357d \ + --hash=sha256:a6e3653df1a70b8ac4191216324242e4be2bca18c9a7c10934e1bd56dc7ca15e \ + --hash=sha256:a865d2d470220e659220fdb59d5b6c4422802d8d6098e1324bc4d12444798914 \ + --hash=sha256:a949b072ea59c6eca0811ccd9e95133cc50d2afda8d464b5b077c78f78efa269 \ + --hash=sha256:aa6ccc7f31018484d652cf52db020003433f3c9fa83189c028bd807d2adde503 \ + --hash=sha256:ac0f291ab6485bd71f33941f9b92771318332a05d505460b41e893a549caadc0 \ + --hash=sha256:acb31ecdd1a97fab5cd39a84ee9f515e727d319f796fec48703b8339b9998360 \ + --hash=sha256:acf52474b2494ef66dc7e0fb6d5e2b50c18313039ad4d275fbf9f9907c804bc5 \ + --hash=sha256:ad889d58361a26ba75f5d6a1a0da08ed4950ec4ac8a6da86e1c5ce1b95ccb43f \ + --hash=sha256:adbd48b30e3f82c89fb2b3e6a87cdd28d113b190a5ed0ee2dee286323ee9a621 \ + --hash=sha256:af05a3f650220a6c59fa0ad2410249f2d2470a05225807c378fb67458693f8df \ + --hash=sha256:b3662719007e059abde7eddacf8517142ba076ddc7b30c807260e57d28c3c191 \ + --hash=sha256:b3bece52127ac20044311ee73567f9f0893b5de64f9028aecc90cc740cfd525a \ + --hash=sha256:b4c8842fb19d78b5e8c2a52baf4c8357658cc56c62bc822b86ce0f942f28e286 \ + --hash=sha256:b659fad79c99b0238c7ad7e9d7dbf4eebfea9097c2dba65fa0a4d18a25b29a2f \ + --hash=sha256:b6c1f9c59bbe593f88a0aad30be4150f15bd57bd64efb95feeabcb8e563f1ecd \ + --hash=sha256:bdd16718b63aa3ebd68aabb79021a40e47c81374852d41a306b9453141bbcbee \ + --hash=sha256:bf430c587f447a554c53768ad76b9846fe7c5632180ef6f69c4fce8b0552fbd0 \ + --hash=sha256:bfed61996d618eb90d6eaae0178002e3466a28b06bfc557a7a3a7266378d8c5a \ + --hash=sha256:c09ada495567c9c9a8156c5ebcfb93be7fece0755062d738c972dcbecd0d84b5 \ + --hash=sha256:c0e6ccc2b19ec8a726b2e26062ac71ea63e15500d6bf85910e42481844fdffc1 \ + --hash=sha256:c101180495cb4ba3617b279a944345c53a5e73b0c150053d1fa8d8af32de9579 \ + --hash=sha256:c10b9206753b64aa791b35b201485477525b26fdec5bf86e8364c388a03e2592 \ + --hash=sha256:c3074db513c81f764053e3da079312ecf85a50d8350c71f4cc0105d9662a9e6c \ + --hash=sha256:c30dd1af66a820820398b26e0d74e7a9aa43cae705924f23ed828cd8e5c26c3d \ + --hash=sha256:c57963970d359a72262f7fe6be88f945e2334d4bc41462b7f08c37b0abf35ca6 \ + --hash=sha256:c6301d92545c591ad31c3e050aa40a5f8a4c16413f1f9e6f9322c6f0f9d2b736 \ + --hash=sha256:c6370189e8e66b7e608f533b939a9de092ddca6cce084ca0d3d414d2ed5b5d59 \ + --hash=sha256:c6fc415b5568bd9accc7187f1729a99707330c0a67a8b9f93c1149ed573ed75d \ + --hash=sha256:c7484fea54964edd417cc3a104d5180562514aa7c4e2a2bc26d776ef0c4cb4a1 \ + --hash=sha256:cba763d84b06bda2c38d5185dee76f1b9dfdc0789e96e476d9e10005526d0788 \ + --hash=sha256:cd878d32f5c6cbce9783f8d6897561fb772211edba9dde49d85672b88ed45276 \ + --hash=sha256:ce6d5cc94a50291d080259a126cbf1e9ba4ac861e6429d2f3cdbb1474f51945d \ + --hash=sha256:d0d24a4f3fb63852cd09af46ae4b7a4d00cc8b8615a046dca543786e728d1056 \ + --hash=sha256:d1e0d1ea6e44f51808a9e8469c8afdebcdf6fa23d1ea524a0303d57d23919712 \ + --hash=sha256:d54b8ae068af532c8cdf56abb9e09a60fbe7b10792444c9c27987bb6d3b450fa \ + --hash=sha256:d55bf4ef10eb09b8b6866790e083d26d087d84caa3cc0946ba87c3ca7ecaf7b7 \ + --hash=sha256:d9f3848ffaf010bdbabdbf4c25641fa258b6227ff27bc74a4d06edef521a4873 \ + --hash=sha256:da0264844a09b538c894e5eff25313d941deb4dedec2131b98418a71a3c9944e \ + --hash=sha256:da544672efd9ad76077928a3e6c5d894e52ce82d3bf14002db4a1bf17d1a36a2 \ + --hash=sha256:daade8936c4deaaf7b01561324ce438ba4f885d717e9adc62b4d67212ad7d7bd \ + --hash=sha256:dd649663ddeafbfd4734eb8abae921dd5baa1242f20bda54e8bc927369ccded4 \ + --hash=sha256:deca2a30d983d240b8375ec2ee0a4288e72042827fc61df2f7671f8467e4cb2f \ + --hash=sha256:e259bb7e1e2d8de6b35f430f5c7220b1c0ebf3962d1ba7ec7545980d5931edb8 \ + --hash=sha256:e3996ff9b6f99180357024336bf5749a8ad6476a9a2523e535c5212b995b12a2 \ + --hash=sha256:e3eba72f9bb84fe696516f4cbca68d3d74a376157e68bacddbb7f2516af61523 \ + --hash=sha256:e4296fcc790876a8b0f297edc83d3b088457b774d8f67b4636807f8a2ec69a79 \ + --hash=sha256:e53926e76131a74e79cc0b39fa712c227875f180afc68646bd1e1d8a17e60313 \ + --hash=sha256:e681a6fc7e4f715252b9b5acfb30536ec7dd1f75033a32dc617e6fa95af1a3fd \ + --hash=sha256:e71b34978e77868cbf2d18c5206a4603f9c644dd7181bec5643bd40141d3b8c5 \ + --hash=sha256:e8cda075b10bb3917b002c74a04f9e02b7d13b5bf732571404d51c52b11c7329 \ + --hash=sha256:e90b4bcf1d9eb1010fdaee7c9209fb667e74c0684f3ba17f9032bd7319da90c9 \ + --hash=sha256:e961093277ff9d42addb9dad5614dfb7800ccba07c245c39c8e9b4daa35d160c \ + --hash=sha256:e9701c073bd062fb6bf6be51b47186ad15f1e87feedf4ea07198e0333ec068dc \ + --hash=sha256:e998cb3685b92101ec5de0fb4d9485cf01e50bc418211955c55d98064664cf4c \ + --hash=sha256:ea5ecf800b45bdb34afe05a1d0dae1f8ea02a290e50636dccd399063f6b180f8 \ + --hash=sha256:ec1a470c6db94ac4589c203921e89ac1bc13e796a8b1784d8135e1893559cd3b \ + --hash=sha256:edccc2ec58435a580f96a48a3ccae8cd0a480824119165dd90108718ad81ae6e \ + --hash=sha256:f00330ac7e24769e2032203f2b01794d670916b0c1799fd261340f1af9499875 \ + --hash=sha256:f09ee747e2a5f876cc5ad56947734811828335e13b403dd8ea1e06d77a9dd48d \ + --hash=sha256:f18732adcc271741bd651c3e56fa519d8a237d2cccda01fe3afb226bf87f783b \ + --hash=sha256:f1b603d0686c99fa0879f104a74e7db58367634c6e50ba827bee9aa095e23205 \ + --hash=sha256:f33cf0baa91eccd2cb7b62bf00f10c2264ef578b71dd33a12962e71a36eb4d32 \ + --hash=sha256:f3e1a44af01b6692de0ec6caba5f0bf93ceb36896e02b7fc00952c6ea7ef39e1 \ + --hash=sha256:f484ed57bb3e4142f9d6439568658c38be5f94b702ba00a1ff32c69783b6c66d \ + --hash=sha256:f5d031f35962e5483a613214e61f09fe24ab523062c3646d592dc16c4a217451 \ + --hash=sha256:f6247f5e23ee94f2557ac9dab738a336f607c6ff476fcf66ca70c3aef5eee15a \ + --hash=sha256:f7db035447a0ac8959aa230c5d36545ecf9f547413eb1711c0ca6f0ba1418925 \ + --hash=sha256:f83295394d34e1287e5b30fcc496c13b92cf886a131f3dae5444e38da8757efb \ + --hash=sha256:fac4832b638000106207bc44e44b9616a6a416aaee56c62b01d61f3705e49f58 \ + --hash=sha256:fb59a0dd61fb2ad481c03fda399d78ce57dab6bb62c2c8fdb446a7ba4754b89a \ + --hash=sha256:fc737c05ca2d48e5dcdbbb249314df3fc6c2a0be6da8b0aa28e13d72afaad7cd \ + --hash=sha256:ff48915bf1871a1f19f74c11834c6329443d306cedc0c05fe7fe617810422a80 \ + --hash=sha256:ffa44b4c7c5d0ffa31356b4428659516c0e47647825c74079a296b3857b6d99d + # via torch-geometric +yarl==1.25.1 \ + --hash=sha256:0136d640dfa9b0523853e411430a99f8a91eca85774c6420285a33b755bc6de3 \ + --hash=sha256:03dd38de09bc213e9a8b29761eec33ee1d5318dac0e49d8af36e4d27830e23a7 \ + --hash=sha256:0a191bfdb30a79b98e5d175d75285f9fcb78bf0e46ba5efda042e1c72071a0de \ + --hash=sha256:0a66db89ea473abeac4b70523cafd94db3772380e565f9d28af7a179b7af71fa \ + --hash=sha256:0ae12ff2b805fa02c4dab838005caef735e39986322698c48588d3beacb65c62 \ + --hash=sha256:0f12afda4eea8c8994a76d4df1875c765194f5fbe8a9d197929ea303caee29ec \ + --hash=sha256:10b2fd95332f0d716d5eee3c9fb2ce8eada19082de7fee83d32e37992fd75c26 \ + --hash=sha256:126a2533570c554719ca40a1288fdee1700b6bc82e7131aa69fa85252d92e651 \ + --hash=sha256:12b6bc4906e11f5e1a1cdcb12296e7afbd366c783cc8073403cd2fb74334e453 \ + --hash=sha256:142c06c4d6a35ee3ec5da08499805e879cb3ca7c1fbfbecb0140fe72403818d6 \ + --hash=sha256:14b79a30a93a3ce2e8832603fd0ab780ada281b0ba5110b519a634f2d7d7d1fc \ + --hash=sha256:17c9877a89fb6e2bca6f9087eb24cd7fb434653946ef5075e470d23d49b52287 \ + --hash=sha256:192a866877a49993949ef1975864ad8728bea28ee810f6abe1a0729c2b500426 \ + --hash=sha256:1ab7618921a93767387a4b83776f751588f5b5ae9bb5bc96620e2e2e00bca868 \ + --hash=sha256:1e80dcf1446e1b080b1932b0d103c464a04112f5bc31f0f983ad418172063cde \ + --hash=sha256:1f51020b2eb8a003c84925638ec63c21a750a4bddd3a22ec8eac6a742dadf1b9 \ + --hash=sha256:1fb2a01ba8cd9c5d2c5dc1ec35e0fc951d04b4f037541d4ac090c993ce58b3d7 \ + --hash=sha256:2239a02249d9326655419e0168a28ca9008938eaab31dc29fc875c217927a6c0 \ + --hash=sha256:23bf5b403c879a54964e0feac7285688e04bb220074878d737d331522da0a5bf \ + --hash=sha256:24ce942011a61953e7d313438038f4d32ff21387b775f58a957f7a07dd55ef95 \ + --hash=sha256:25868beca8b6765f8f7d0e11fe6dd7c66dd4b0793b9500286d20cc92352126a5 \ + --hash=sha256:287e99ff5aa4dc1c7630bfc683ded6f106d756c99dec432a2d7f197a784f51c6 \ + --hash=sha256:29273edf1530e397bd07cb784db1fbe0d2590b77569f2e24679a9c0a2d763b94 \ + --hash=sha256:2b49375d22299b0a834c2bca72f39aaecc270d96fb24c30424899676f487b22a \ + --hash=sha256:30eec96e8a91bd588ce897c9543f6d5d8d34b28fbcba28a4dedf20ebeae9fe57 \ + --hash=sha256:319e070a01db9920fb63761843f96a104c8e2b9427266731810dc1e22595b17c \ + --hash=sha256:35dcbea443fafb3eece757ad4e514560ddeb6c34cfae1582c620d7b293d7feee \ + --hash=sha256:3f4d48a6112712973e676bd792121fee470e432d749177162d9949d5c9460a1b \ + --hash=sha256:3feb99222553a8cbedfa52c2f59dd84c3f50d5b582c728d522caf8d72769a54b \ + --hash=sha256:419f392a1da624877975709e3864dfe833af6cc7671b39318086d456e288380c \ + --hash=sha256:42a66563d8cc056ee32e6191e05097a7b2b3bc302e0bc3133daf8710eb18bd26 \ + --hash=sha256:48796ea00a303961507dc6c8437c4b325a6fc3f95f7c36c71b91ea9a8150963c \ + --hash=sha256:4bd6340d20ae2c7ca719b87b426e808e90743b676d05d4c26c4fb5ca71f41184 \ + --hash=sha256:4ca89e4e21854ed27ec753297dde84b16c9f8e53b14a4866fb44457d643c19f8 \ + --hash=sha256:4d781294bb815ecb5ea57ff6bbf8038e0a31a95fdf3e1788f66e0dc100d64b58 \ + --hash=sha256:4f1c91f5a5980a937ff8e238e98e6897e1ad74a4b1e2c0d68c73b5ffbb3f5c0b \ + --hash=sha256:564fdc7085d2245ab84f88882fdb1d6ac0723124bff6ded35bfb1c00f812630d \ + --hash=sha256:59ba3a6e1aa8cfe5adf4bd270fd965db21955401b7ca6f1696010c55ed4daec2 \ + --hash=sha256:5df89f769cc8ff94c3d7e7603386fba309d25ce5240132d26c15baa8d0e96c4c \ + --hash=sha256:632da579b2d879f6bad20f2cfa35ded1efe2f4f77f8abb26a6234a5b236acd2f \ + --hash=sha256:65b5b2066651b7432d389e9799d979c703bcc6ef44266bb8153ef54e91e4aab3 \ + --hash=sha256:664ec6a520b74a1df2810666eb67695fcb77fa663e6ea0a25aaf2e529cb24dfa \ + --hash=sha256:681c758b0490f9e96b78e5fa8e8dc6e648e9185bb6eaebe73183c33ea0c445f3 \ + --hash=sha256:683e362b8ba453080f7489c66f4ea794e751c35b72e7eab3575ef784c2fbc7fb \ + --hash=sha256:68782fdb4027b8d1eee25ec35e9a6db05e863b899eb0310b3a33b6c3fef55707 \ + --hash=sha256:6efaf45df6a849cef613a03a94c845647456662f85438c886bb67a9c027c8c2c \ + --hash=sha256:71f42c5b9a948c113bbdebfa544598321431d064ff959d32e99b1feb61d68345 \ + --hash=sha256:72849d892954be4d09e569b8b831ac39ce58417fedc767d4308a0fe542018a40 \ + --hash=sha256:72c34ac7ad4314c19362d5ce27626dcc8429bd30bbf8c179f4234078851f9492 \ + --hash=sha256:734f6e5400352ac4254456003d462866c684703570929cff7a7bde015d0cb371 \ + --hash=sha256:75baa6cf9b6d1c52f3e111a130e202fd8cf0a5b3a066c3f73d615e885092e4ec \ + --hash=sha256:77716e245c90f058466a05e6a465bb8600f767a8f4b18b4d40f3aff958e5f73c \ + --hash=sha256:77e5099b99b37f3cf79c246998ca9f7313a78054cd1809ec46bc1afad47e1c4c \ + --hash=sha256:783dd1467083f4d3f7722ad6a313f24c173e7571372738fcb7a6e6d1ba48df25 \ + --hash=sha256:7a5c3115595995779ee21f2567035793911c3802a43c74f3fbb0314929ec67ac \ + --hash=sha256:7c88edaec8c349ad4c5ad4c486a3defcc4b80ceb2f074436ffa0a87caf5e76a6 \ + --hash=sha256:7cb414a73e21a7ab58254926073f2930cb22f5b4314ea4260a687e2b3fd4dce3 \ + --hash=sha256:7d42e7e3ca399555578b4d617e3a6ecf13371b3743a115995fa010c7bf341459 \ + --hash=sha256:7d575b54cb3863ef9bc290ea4b009999d55dc237326131e4853cf33e888fee03 \ + --hash=sha256:7e4de3ac4adbad3d0bc7c6f4360a7dbff5de2f15e3b723be3198074e17fd9c40 \ + --hash=sha256:7efc9f082dfed77c316edffa9deb52888e1bc6789171887cc1f68e06d65465c8 \ + --hash=sha256:80a063f8297fc796296f00f100be520f209b23dc98f93ce8eba6ee7122598209 \ + --hash=sha256:80e47012e730da131c9f059c80936783f9659aae22dc31c03c0595590d11ed54 \ + --hash=sha256:83d4a37e4b95da4d8bda930d6d35b75b4cdadbacbb4980cae290ea3100b5d51d \ + --hash=sha256:83e9f4a25085bd4b7214701a0794ff1f50fc633ffb8bdfebf07abdd81c2db126 \ + --hash=sha256:85a18376073f8a39aa07be34f9fc77e2869aa72c55c441efdd2cf79a0407504d \ + --hash=sha256:87796fedc3ba97ec14fab55acb48584276e6c1e4c1e89c422bda62c838e754a9 \ + --hash=sha256:882569ff613758cac762a457a5d72d6e211b28d4bcfea89d1d71ea942b02eac0 \ + --hash=sha256:8ce4d6ccafb33d39bd78444612d14938ead674c25702ded2ee9c54a47735d225 \ + --hash=sha256:8e7d98cdbb6d71e726f7d525952867096053d1f290dd4e3c50d7d313a136f414 \ + --hash=sha256:8ee202350cf57abf0e9502a41601841019c25d3db7ff52d980aaf31446254059 \ + --hash=sha256:8fb0eb4955adf0579001581f2f71a126e8781ba61bcd120f127b0401163c6c2d \ + --hash=sha256:90c30ed53546da833c700115c0064c22120d1b1560f474699fd31f22dd668233 \ + --hash=sha256:9489e6abf47ba37f332075a91444c7cfedb03e6ce99fbb2f116bfe1ce810da3b \ + --hash=sha256:94d7aa6debf92a1dd14cb5280b083a764169a13cfb23a452111160274ed989f4 \ + --hash=sha256:98d370568f393215d605304cdb77b3d5539bd192c75b623c7304c42c8d6d8273 \ + --hash=sha256:9b1bdaae98bc016825dd3c9d8ee1832f829b3341f9cc6ebd1a1b0a7fef7367cc \ + --hash=sha256:9d693bf4bf534e9ba3ae2780cfd577f5135629f7b5ac653490859d0b77864865 \ + --hash=sha256:9d6ed3d17bccce4c05343e1ca8da13bc5c02c812a4e7282ddd05e8769322d3fc \ + --hash=sha256:9e23c82b63cd7652fc24d33ed6cc17099d607aa3b4fc4ddc75e95062f3d82df4 \ + --hash=sha256:a1daf47cd95a7c3a63456336bc5aaa8c86dd3a47d07ed3d0e76132ae4666a5a1 \ + --hash=sha256:a1e32763e641a1566507d90a8d3b19bfc3cc04a9d4e5ae3e32189874ed4b58a3 \ + --hash=sha256:a2059a2d891bd156bc5184e7ab7a56e78a84dfcfdeac8c501b552533ad1c36ee \ + --hash=sha256:a2ed0ba415ccdf08f14bf544cb78346d0f76086707ffee24921a2c84dbf1305a \ + --hash=sha256:a3faadac7d812ddac258feb57b9846b60c1b437c4f4b9ad42595c6f6fe4390df \ + --hash=sha256:a5877f2255aab518ebe528289037699201d5dc5f045f2396cb30aa02db22f57f \ + --hash=sha256:a78b50b4f7918a3de71105d5c0b93bbc57bb8339a4d03a9dfd449f9068e76f3d \ + --hash=sha256:a8c2b841478068440d8b733005d13a5ef535b9928cbc05f17182d410f32ba449 \ + --hash=sha256:a9ca696eb02e5c02a8afd872ada510eba9b7fe6e68b9572c2e9a9b1941e31e2e \ + --hash=sha256:aa4ed3dd308548f9e707d9caaf005d2d7f8c1e7868f858dfeb47fe76e16b391d \ + --hash=sha256:ab2054c5531af2a9ba7b69b8ec91e4f884420e83a8c5e579b013084cb57e5e5d \ + --hash=sha256:abb1384477f5901d436b5d2e5465954de46ea6098f59163d243660b5c4461d35 \ + --hash=sha256:acae6b45d1ace09b6ba3876da43b88366ef368f73b988c7f57e14231753d4420 \ + --hash=sha256:acfa7e22aa6c6e7a5996a41d275bfa01efa7ea56ab890590280e9063e2cf5c1b \ + --hash=sha256:af4ea5b37403ef4e30f3927eaed540db942bde01d8d3ff083527c0704d1c9c68 \ + --hash=sha256:b10dd0557ba422715b5206b3743192135a6022acca8baec51aa127d0a75db8fe \ + --hash=sha256:b13b88747769537f3d32e89e3a735da10c0a9e35d7322928c701b5f93d3afffd \ + --hash=sha256:b51c159a9794633f5e0db7ecec7b2b6e3734eca1f5d17dc989ff3552a43ff78b \ + --hash=sha256:b5402a340723fa7da00b5cff987ddab61276be6d11251ea71ae02bcac54890d8 \ + --hash=sha256:b7abffdf37af1cec6a2ad69b827aa84320db5894791bc8ed932dc93fb274b7e9 \ + --hash=sha256:b8075fe90bc08e40b8b8a1874fab42ee4c7b56af05c5886e9cc841397f916908 \ + --hash=sha256:bc3ac7bf569f6b64dad04dd7808c7872dae8a97df657856eac05e9b7e3614a85 \ + --hash=sha256:bd0912757081f89b107d6c00b2ff8a194401b0b87eadcf4481de2b865a8fd44f \ + --hash=sha256:bdc8d8b8c22e9e43ac68316b5e6cf083dec537f4ec213cb4aa967b583bc3fa64 \ + --hash=sha256:be80550d9bfe83d9b62398a37081a90434e6df2d978ec345c3d2820de6beddab \ + --hash=sha256:c6f117789d22dce188e5754e8bc65b7e6ebf8cb73963b9fa761f672a5883769d \ + --hash=sha256:ca32926d7d77bcc8838425c4c95e040a3ace1cb7dfdae599013458dcda2607ca \ + --hash=sha256:cce0727fd5ac04d372fa9bbfde9febc2bcf209aadfcf0468e45dec72719895d1 \ + --hash=sha256:d0f1489233a254bb3643d2f05de7d59019254d81daeca6b9162fe9edef57e0c7 \ + --hash=sha256:d1c557dfd5e3db046053a0bdc72261ade790ebe8e2c7a41b36b0ca1f14cb95f3 \ + --hash=sha256:d21f0fa80a02d05299207eeaafef345d812ace96d5306e4ef265e1d419a615fa \ + --hash=sha256:d45673badd08456d0340e9364eddafe1c53a9d2896424294de4d7dd71ad3ee57 \ + --hash=sha256:d5add7b4ca7afeea91d52e4d4e4db3b1fe9885b71f07054560d8c4296b7441a2 \ + --hash=sha256:d5f90e44653c4e0f78501ed9bb7d3fce835a8d62b7c6ed0cb16557534087e743 \ + --hash=sha256:d7306dee25b8a0e737363f347362b875094b4dc4e367311470656ae420fdbf8e \ + --hash=sha256:dbcef5a9119ef653653132cccaf999b30a0af6f33bb0a4ba80bec30056868487 \ + --hash=sha256:def538065f9e4d4cf1ae164bd59aba00dfa84f03923e0de4c3788f252d6bcd17 \ + --hash=sha256:df23df54b5114a17c2d0ef192433e2e5a9f0c5178c32375e90b7cfc965f349d0 \ + --hash=sha256:dfbf531053a0935f2e871bcd4753f90313688772ff8c017f5ea402e315a78c1f \ + --hash=sha256:e029648f9c951db30e98a7d7ec90835db88ec4b32820efe2a9bdc2287e032eb6 \ + --hash=sha256:e07595c7d6f4db270ceede356a1bd1c07a34f1c26f958d1ed0cd7b48e0d2bba3 \ + --hash=sha256:e12c538e00e7c1b286a07061046b90e8124e6a9793efae2c70db6a4aad07faad \ + --hash=sha256:e546fe1d4a93ebc2910f0d768baff19faa09843ab3f2036a67ed6e69fae4419d \ + --hash=sha256:e5637ca8d0bd7fb72648a6c7934af4baaccb697657f7438c9d264fc2abb8b0b1 \ + --hash=sha256:e636b64d24fd9c38053c5e389a1174c66361fa49dcfd220f4dd35b4abde7cb89 \ + --hash=sha256:e7011b8fb8c4054bf0c12e5edc6cd83778b0028e99ce59b18586ed036f92cfdc \ + --hash=sha256:e80f557716fd765439577131e526b8942ffc2c07bdbc5e39fa62f660ba1e963f \ + --hash=sha256:e92b6bcc741b86d67606c40d3cb9c7cc8e6c737f81e31f4a94efc204456c92e3 \ + --hash=sha256:eb96ed1ae6c7d072d60840c0434aef07a2df611812810807fbc54263a6053e9a \ + --hash=sha256:eda19ea5ee88742f47a2340816e6f2d40b53bed3ab5b69794769f36af9f35bb4 \ + --hash=sha256:ef74070ac553c59eb4f04258722066d6c6135b7baa03b2e9f2da65c096e96d98 \ + --hash=sha256:efb01a106f971cb3752856bca2318bbdf7f01bd8823779c461586cbe5ffd5258 \ + --hash=sha256:f074e8d4aa0a5798920ddb6de3d08b228c614ff3724c3e8bd7577f4bafea867b \ + --hash=sha256:f38a70074041d3b7e138e452799f5174198bae5bd5ab2000917badf403908c5f \ + --hash=sha256:f41753a76f4f63927d03a0d8ba8f5ce0f2083bec29a8cfaccc55371b1564b96b \ + --hash=sha256:f53dcd26694f148f738edc052b5a69234833e739f10f4c3287bdfd8ec0f7b326 \ + --hash=sha256:f61964f235a43738bfac50da46fc4254943a7eea3051aeb0b6fc7c992c29fadc \ + --hash=sha256:fe01645169a2112aa1d4ebc3e4c5f029c5c8f97adfc32e5d37c993b39a994d75 + # via aiohttp diff --git a/requirements.txt b/requirements.txt index 80e05a35..0ca1e521 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,9 @@ # Core dependencies numpy>=1.20.0 pymatgen>=2022.0.0 +torch>=2.0.0 +torch-geometric>=2.0.0 +PyYAML>=5.1 # Web application flask>=2.0.0 diff --git a/setup.py b/setup.py index 5ced8570..0ee02bf6 100644 --- a/setup.py +++ b/setup.py @@ -26,28 +26,42 @@ "Topic :: Scientific/Engineering :: Physics", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ], - python_requires=">=3.8", + python_requires=">=3.9", install_requires=[ - "numpy>=1.20.0", + "numpy>=1.22.0", "pymatgen>=2022.0.0", "flask>=2.0.0", "flask-cors>=3.0.0", + "werkzeug>=2.0.0", + "matplotlib>=3.6.0", ], extras_require={ + "ai": [ + "torch>=1.12.0", + "torch-geometric>=2.0.0", + "scipy>=1.7.0", + "pyyaml>=5.4.0", + "tqdm>=4.62.0", + ], + "perf": [ + "scipy>=1.7.0", + ], "dev": [ - "pytest>=6.0", - "black>=21.0", - "flake8>=3.9", - "mypy>=0.900", + "pytest>=7.0", + "pytest-cov>=4.0", + "black>=23.0", + "ruff>=0.1.0", + "mypy>=1.0", ], "docs": [ - "sphinx>=4.0", - "sphinx-rtd-theme>=0.5", + "sphinx>=5.0", + "sphinx-rtd-theme>=1.0", ], }, entry_points={ diff --git a/start_web.sh b/start_web.sh index 72dec99a..6d7481f9 100755 --- a/start_web.sh +++ b/start_web.sh @@ -28,10 +28,15 @@ check_web_deps() { } if ! check_web_deps "$PYTHON_BIN"; then + # Try EGNNs env first (has torch + flask), then interface env (flask only) + EGNNS_FALLBACK="/Users/shane/Applications/anaconda3/envs/EGNNs/bin/python" CONDA_FALLBACK="/Users/shane/Applications/anaconda3/envs/interface/bin/python" - if [ -x "$CONDA_FALLBACK" ] && check_web_deps "$CONDA_FALLBACK"; then + if [ -x "$EGNNS_FALLBACK" ] && check_web_deps "$EGNNS_FALLBACK"; then + PYTHON_BIN="$EGNNS_FALLBACK" + echo "✓ Using Python: $PYTHON_BIN (EGNNs conda env)" + elif [ -x "$CONDA_FALLBACK" ] && check_web_deps "$CONDA_FALLBACK"; then PYTHON_BIN="$CONDA_FALLBACK" - echo "✓ Using Python: $PYTHON_BIN (conda env fallback)" + echo "✓ Using Python: $PYTHON_BIN (interface conda env fallback)" else echo "❌ Error: cannot start web server because required packages are missing in: $PYTHON_BIN" echo " Missing: flask and/or flask_cors" @@ -44,6 +49,49 @@ if ! check_web_deps "$PYTHON_BIN"; then fi fi +# Check optional AI dependencies and assets (warn only). +AI_CHECK_OUTPUT="$("$PYTHON_BIN" - <<'PY' +import importlib +import os +import sys +from pathlib import Path + +missing = [] +for mod in ("torch", "torch_geometric"): + try: + importlib.import_module(mod) + except Exception as exc: + missing.append(f"{mod}: {exc}") + +base = Path(os.getenv("INTERFACEML_FULLERENE_PATH") or (Path.cwd() / "fullerene_e3gen")) +if not base.exists(): + missing.append(f"fullerene_e3gen path not found: {base}") + +ckpt = Path(os.getenv("INTERFACEML_FULLERENE_CHECKPOINT") or (base / "checkpoints" / "best_model.pt")) +if not ckpt.exists(): + missing.append(f"checkpoint not found: {ckpt}") + +if missing: + print("\n".join(missing)) + sys.exit(1) +PY +)" + +if [ $? -ne 0 ]; then + echo "⚠️ AI module will be unavailable with the current Python:" + echo "$PYTHON_BIN" + echo "" + echo "Missing items:" + echo "$AI_CHECK_OUTPUT" + echo "" + echo "💡 To enable AI generation:" + echo " 1) Use a Python version supported by PyTorch (e.g., 3.10 or 3.11)" + echo " 2) Install AI deps: pip install -e \".[ai]\"" + echo " (or pip install -r fullerene_e3gen/requirements.txt)" + echo " 3) Ensure the checkpoint exists (or set INTERFACEML_FULLERENE_CHECKPOINT)" + echo "" +fi + # Try different ports in case 5000 is occupied PORTS=(5000 8000 8080 5001 3000) PORT_FOUND=false @@ -73,4 +121,3 @@ if [ "$PORT_FOUND" = false ]; then echo " $PYTHON_BIN interfaceml/web/app.py --port 9000" exit 1 fi - diff --git a/structures/.DS_Store b/structures/.DS_Store deleted file mode 100644 index 3fc1c5c2..00000000 Binary files a/structures/.DS_Store and /dev/null differ diff --git a/structures/etl/.DS_Store b/structures/etl/.DS_Store deleted file mode 100644 index 5008ddfc..00000000 Binary files a/structures/etl/.DS_Store and /dev/null differ diff --git a/structures/heterojunctions/.DS_Store b/structures/heterojunctions/.DS_Store deleted file mode 100644 index 5008ddfc..00000000 Binary files a/structures/heterojunctions/.DS_Store and /dev/null differ diff --git a/structures/perovskites/.DS_Store b/structures/perovskites/.DS_Store deleted file mode 100644 index 5008ddfc..00000000 Binary files a/structures/perovskites/.DS_Store and /dev/null differ diff --git a/submit.sh b/submit.sh deleted file mode 100644 index 6debec10..00000000 --- a/submit.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash -#SBATCH -p i64m512u -#SBATCH -J Ag2Na6Se12_AB3C6_182 -#SBATCH -n 128 -#SBATCH -o job.%j.out -#SBATCH -e job.%j.err - -ulimit -s unlimited - -module load vasp/6.4.2 - -MPIEXEC=`which mpirun` -VASP_STD=`which vasp_std` -###The i64M512U queue has 64 cores per computer - -$MPIEXEC -np 128 $VASP_STD > vasp.out diff --git a/templates/README_templates.md b/templates/README_templates.md index c368b0e4..944dde9b 100644 --- a/templates/README_templates.md +++ b/templates/README_templates.md @@ -1,6 +1,6 @@ # InterfaceML Code Templates -This directory contains templates to help you quickly add new functionality to InterfaceML. +This directory contains templates for adding new functionality to InterfaceML. ## Available Templates @@ -30,7 +30,7 @@ cp templates/new_core_module_template.py interfaceml/core/your_module.py # - Update __all__ list # Test it -python -c "from interfaceml.core import your_module; print('✓ Module imported')" +python -c "from interfaceml.core import your_module; print('Module imported')" ``` --- @@ -73,8 +73,8 @@ python build_heterojunctions/your_tool.py --help ### Step 1: Choose Template -**Core Module** → For scientific algorithms -**CLI Tool** → For command-line interface +**Core Module** -> For scientific algorithms +**CLI Tool** -> For command-line interface ### Step 2: Copy and Rename @@ -273,7 +273,7 @@ def test_basic_functionality(): result, meta = your_module.your_function(struct, parameter1=2.5) assert len(result) > 0 assert 'key' in meta - print("✓ Test passed") + print("Test passed") if __name__ == "__main__": test_basic_functionality() @@ -353,4 +353,4 @@ python -c "from interfaceml.core import NAME" --- -**Happy coding! 🚀** +Use these templates as a starting point for new modules and tools. diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..835f99db --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,7 @@ +import sys +from pathlib import Path + +# Ensure repo root is importable without requiring an editable install. +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) diff --git a/tests/test_coherent_interfaces.py b/tests/test_coherent_interfaces.py new file mode 100644 index 00000000..388001c6 --- /dev/null +++ b/tests/test_coherent_interfaces.py @@ -0,0 +1,70 @@ +"""Check the physical labels and geometry of coherent interface construction.""" + +import numpy as np +import pytest +from pymatgen.core import Lattice, Structure + +from interfaceml.core.adsorbate import molecular_unwrap +from interfaceml.core.interfaces_builder import ( + apply_registry_twist, + build_coherent_interfaces, + list_interface_terminations, +) + + +@pytest.fixture +def crystal_pair(): + substrate = Structure(Lattice.cubic(3), ["Si"], [[0, 0, 0]]) + film = Structure(Lattice.cubic(3), ["Ge"], [[0, 0, 0]]) + return substrate, film + + +def test_termination_labels_follow_material_identity(crystal_pair): + terms = list_interface_terminations(*crystal_pair, max_area=10) + assert terms["n_zsl_matches"] > 0 + assert all(item["formula"] == "Si" for item in terms["substrate_terminations"]) + assert all(item["formula"] == "Ge" for item in terms["film_terminations"]) + + +def test_explicit_termination_matches_materials(crystal_pair): + terms = list_interface_terminations(*crystal_pair, max_area=10) + labels = terms["substrate_terminations"] + terms["film_terminations"] + substrate = next(item["label"] for item in labels if item["formula"] == "Si") + film = next(item["label"] for item in labels if item["formula"] == "Ge") + candidates = build_coherent_interfaces( + *crystal_pair, termination=(substrate, film), max_area=10, top_k=1 + ) + assert len(candidates) == 1 + interface = candidates[0]["interface"] + assert {interface[i].specie.symbol for i in interface.substrate_indices} == {"Si"} + assert {interface[i].specie.symbol for i in interface.film_indices} == {"Ge"} + assert candidates[0]["von_mises_strain"] == pytest.approx(0, abs=1e-10) + + +def test_registry_shift_preserves_film_labels_and_substrate(crystal_pair): + interface = build_coherent_interfaces(*crystal_pair, max_area=10, top_k=1)[0]["interface"] + shifted = apply_registry_twist(interface, xy_shift=(0.25, 0.1), xy_units="fractional") + assert shifted.site_properties == interface.site_properties + assert shifted.film_indices == interface.film_indices + np.testing.assert_allclose( + shifted.cart_coords[interface.substrate_indices], + interface.cart_coords[interface.substrate_indices], + ) + expected = 0.25 * interface.lattice.matrix[0] + 0.1 * interface.lattice.matrix[1] + np.testing.assert_allclose( + shifted.cart_coords[interface.film_indices] - interface.cart_coords[interface.film_indices], + [expected] * len(interface.film_indices), + atol=1e-12, + ) + + +def test_unwrap_preserves_site_properties(): + molecule = Structure( + Lattice.cubic(10), + ["C", "H"], + [[0.5, 0.5, 0.95], [0.5, 0.5, 0.05]], + site_properties={"selective_dynamics": [[False] * 3, [True] * 3]}, + ) + repaired = molecular_unwrap(molecule) + assert repaired.site_properties == molecule.site_properties + assert np.linalg.norm(repaired.cart_coords[0] - repaired.cart_coords[1]) == pytest.approx(1) diff --git a/tests/test_core_adsorbate.py b/tests/test_core_adsorbate.py new file mode 100644 index 00000000..485e1066 --- /dev/null +++ b/tests/test_core_adsorbate.py @@ -0,0 +1,190 @@ +"""Tests for interfaceml.core.adsorbate module. + +Covers rotation_matrix_from_axis_angle, auto_supercell_xy, +prepare_adsorbate_layer, stack_structures, and build_adsorbate_interface. +""" + +from __future__ import annotations + +from pathlib import Path + +import numpy as np +import pytest +from pymatgen.core import Lattice, Structure + +from interfaceml.core.adsorbate import ( + SupercellChoice, + auto_supercell_xy, + build_adsorbate_interface, + prepare_adsorbate_layer, + rotation_matrix_from_axis_angle, + stack_structures, +) + +# --------------------------------------------------------------------------- +# Paths to real structures (skip gracefully if not present) +# --------------------------------------------------------------------------- +_STRUCTURES = Path(__file__).resolve().parents[1] / "structures" +_CIF_PEROVSKITE = _STRUCTURES / "perovskites" / "fapbi3-1.cif" +_CIF_FULLERENE = _STRUCTURES / "etl" / "c70-1.cif" + +_has_perovskite = _CIF_PEROVSKITE.exists() +_has_fullerene = _CIF_FULLERENE.exists() + + +# =================================================================== +# Fixtures +# =================================================================== + + +@pytest.fixture() +def cubic_slab(): + """Simple 10x10x5 slab with 4 atoms.""" + lattice = Lattice.from_parameters(10, 10, 5, 90, 90, 90) + return Structure(lattice, ["Si"] * 4, [[0, 0, 0], [0.5, 0, 0], [0, 0.5, 0], [0.5, 0.5, 0]]) + + +@pytest.fixture() +def small_molecule(): + """Small 'adsorbate' with 3 atoms in a big box.""" + lattice = Lattice.cubic(20.0) + coords = [[10, 10, 10], [11.5, 10, 10], [10, 11.5, 10]] + return Structure(lattice, ["C"] * 3, coords, coords_are_cartesian=True) + + +# =================================================================== +# TestRotationMatrix +# =================================================================== + + +class TestRotationMatrix: + """rotation_matrix_from_axis_angle correctness.""" + + def test_identity_zero_angle(self): + R = rotation_matrix_from_axis_angle(np.array([0, 0, 1.0]), 0.0) + np.testing.assert_allclose(R, np.eye(3), atol=1e-12) + + def test_90_degree_z(self): + R = rotation_matrix_from_axis_angle(np.array([0, 0, 1.0]), np.pi / 2) + # x-axis should map to y-axis + result = R @ np.array([1.0, 0, 0]) + np.testing.assert_allclose(result, [0, 1, 0], atol=1e-12) + + def test_180_degree(self): + R = rotation_matrix_from_axis_angle(np.array([0, 0, 1.0]), np.pi) + # x should map to -x, y to -y + result = R @ np.array([1.0, 0, 0]) + np.testing.assert_allclose(result, [-1, 0, 0], atol=1e-12) + + def test_orthogonality(self): + """R^T R should be identity.""" + R = rotation_matrix_from_axis_angle(np.array([1.0, 1.0, 1.0]), 1.23) + np.testing.assert_allclose(R.T @ R, np.eye(3), atol=1e-12) + + def test_zero_axis_returns_identity(self): + R = rotation_matrix_from_axis_angle(np.array([0.0, 0.0, 0.0]), 1.0) + np.testing.assert_allclose(R, np.eye(3), atol=1e-12) + + +# =================================================================== +# TestAutoSupercellXY +# =================================================================== + + +class TestAutoSupercellXY: + """auto_supercell_xy returns valid supercell choices.""" + + def test_return_type(self, cubic_slab, small_molecule): + choice = auto_supercell_xy(cubic_slab, small_molecule) + assert isinstance(choice, SupercellChoice) + assert isinstance(choice.nx, int) + assert isinstance(choice.ny, int) + + def test_min_supercell(self, cubic_slab, small_molecule): + choice = auto_supercell_xy(cubic_slab, small_molecule, min_supercell=2) + assert choice.nx >= 2 + assert choice.ny >= 2 + + def test_max_supercell(self, cubic_slab, small_molecule): + choice = auto_supercell_xy(cubic_slab, small_molecule, max_supercell=3) + assert choice.nx <= 3 + assert choice.ny <= 3 + + +# =================================================================== +# TestPrepareAdsorbateLayer +# =================================================================== + + +class TestPrepareAdsorbateLayer: + """prepare_adsorbate_layer placement and rotation.""" + + def test_center_placement(self, small_molecule, cubic_slab): + result = prepare_adsorbate_layer(small_molecule, cubic_slab.lattice) + # Centroid should be near the center of the a,b plane + cart = np.array(result.cart_coords) + centroid_xy = cart[:, :2].mean(axis=0) + target_xy = 0.5 * cubic_slab.lattice.matrix[0][:2] + 0.5 * cubic_slab.lattice.matrix[1][:2] + np.testing.assert_allclose(centroid_xy, target_xy, atol=1.0) + + def test_rotation_application(self, small_molecule, cubic_slab): + R = rotation_matrix_from_axis_angle(np.array([0, 0, 1.0]), np.pi / 2) + result = prepare_adsorbate_layer(small_molecule, cubic_slab.lattice, rotation=R) + assert isinstance(result, Structure) + assert len(result) == len(small_molecule) + + +# =================================================================== +# TestStackStructures +# =================================================================== + + +class TestStackStructures: + """stack_structures returns correct types and atom counts.""" + + def test_return_types(self, cubic_slab, small_molecule): + bottom, top, combined = stack_structures(cubic_slab, small_molecule) + assert isinstance(bottom, Structure) + assert isinstance(top, Structure) + assert isinstance(combined, Structure) + + def test_atom_count(self, cubic_slab, small_molecule): + bottom, top, combined = stack_structures(cubic_slab, small_molecule) + assert len(combined) == len(cubic_slab) + len(small_molecule) + + def test_z_ordering(self, cubic_slab, small_molecule): + """Top layer should be above bottom layer after stacking.""" + bottom, top, combined = stack_structures(cubic_slab, small_molecule, separation=5.0) + n = np.array([0, 0, 1.0]) # Normal for orthorhombic + bottom_z = np.dot(np.array(bottom.cart_coords), n).max() + top_z = np.dot(np.array(top.cart_coords), n).min() + assert top_z > bottom_z - 1.0 # Top should be above bottom (with some tolerance) + + +# =================================================================== +# TestBuildAdsorbateInterface +# =================================================================== + + +class TestBuildAdsorbateInterface: + """End-to-end build_adsorbate_interface with real files.""" + + @pytest.mark.skipif( + not (_has_perovskite and _has_fullerene), + reason="Real structure files not found", + ) + def test_end_to_end(self): + base = Structure.from_file(str(_CIF_PEROVSKITE)) + adsorbate = Structure.from_file(str(_CIF_FULLERENE)) + bottom, top, combined, choice = build_adsorbate_interface( + base, + adsorbate, + miller=(0, 0, 1), + slab_thickness=10.0, + vacuum=15.0, + supercell_xy=(2, 2), + ) + assert isinstance(combined, Structure) + assert len(combined) > 0 + assert len(combined) == len(bottom) + len(top) + assert isinstance(choice, SupercellChoice) diff --git a/tests/test_core_io.py b/tests/test_core_io.py new file mode 100644 index 00000000..2979a007 --- /dev/null +++ b/tests/test_core_io.py @@ -0,0 +1,193 @@ +"""Tests for interfaceml.core.io module. + +Covers load_structure (CIF/VASP/XYZ auto-detection), write_poscar, +read_cp2k_xyz_last_frame, and get_element_symbols. +""" + +from __future__ import annotations + +from pathlib import Path + +import numpy as np +import pytest +from pymatgen.core import Lattice, Structure + +from interfaceml.core.io import ( + get_element_symbols, + load_structure, + read_cp2k_xyz_last_frame, + write_poscar, +) + +# --------------------------------------------------------------------------- +# Paths to real test structures (skip gracefully if not present) +# --------------------------------------------------------------------------- +_STRUCTURES = Path(__file__).resolve().parents[1] / "structures" +_CIF_PEROVSKITE = _STRUCTURES / "perovskites" / "fapbi3-1.cif" +_CIF_FULLERENE = _STRUCTURES / "etl" / "c70-1.cif" +_XYZ_CP2K = _STRUCTURES / "heterojunctions" / "cp2k_opt.xyz" + +_has_perovskite = _CIF_PEROVSKITE.exists() +_has_fullerene = _CIF_FULLERENE.exists() +_has_cp2k_xyz = _XYZ_CP2K.exists() + + +# =================================================================== +# TestLoadStructure +# =================================================================== + + +class TestLoadStructure: + """Auto-detection of CIF, VASP, and XYZ formats.""" + + @pytest.mark.skipif(not _has_perovskite, reason="fapbi3-1.cif not found") + def test_load_cif(self): + s = load_structure(_CIF_PEROVSKITE) + assert isinstance(s, Structure) + assert len(s) > 0 + + @pytest.mark.skipif(not _has_perovskite, reason="fapbi3-1.cif not found") + def test_cif_has_lattice(self): + s = load_structure(_CIF_PEROVSKITE) + assert s.lattice.a > 0 + assert s.lattice.b > 0 + assert s.lattice.c > 0 + + @pytest.mark.skipif(not _has_cp2k_xyz, reason="cp2k_opt.xyz not found") + def test_load_xyz(self): + s = load_structure(_XYZ_CP2K) + assert isinstance(s, Structure) + assert len(s) > 0 + + def test_load_xyz_without_cell(self, tmp_path): + """XYZ file with no Tv_ vectors gets a fallback box.""" + xyz = tmp_path / "mol.xyz" + xyz.write_text("3\ncomment\nC 0.0 0.0 0.0\nC 1.5 0.0 0.0\nC 0.0 1.5 0.0\n") + s = load_structure(xyz) + assert isinstance(s, Structure) + assert s.lattice.a >= 10.0 # fallback padding + + def test_unsupported_format_raises(self, tmp_path): + bad = tmp_path / "struct.pdb" + bad.write_text("dummy") + with pytest.raises(ValueError): + load_structure(bad) + + def test_missing_file_raises(self, tmp_path): + with pytest.raises(FileNotFoundError): + load_structure(tmp_path / "nonexistent.cif") + + +# =================================================================== +# TestWritePoscar +# =================================================================== + + +class TestWritePoscar: + """write_poscar round-trip and formatting tests.""" + + @pytest.fixture() + def simple_structure(self): + lattice = Lattice.cubic(5.0) + return Structure( + lattice, ["Si", "O", "Si"], [[0, 0, 0], [0.5, 0.5, 0.5], [0.25, 0.25, 0.25]] + ) + + def test_round_trip(self, simple_structure, tmp_path): + out = tmp_path / "POSCAR" + write_poscar(simple_structure, out) + reloaded = Structure.from_file(str(out)) + assert len(reloaded) == len(simple_structure) + + def test_selective_dynamics(self, simple_structure, tmp_path): + out = tmp_path / "POSCAR" + sd = [(True, True, False)] * len(simple_structure) + write_poscar(simple_structure, out, selective_dynamics=sd) + text = out.read_text() + assert "Selective" in text or "selective" in text.lower() + + def test_custom_comment(self, simple_structure, tmp_path): + out = tmp_path / "POSCAR" + write_poscar(simple_structure, out, comment="test_comment_123") + first_line = out.read_text().splitlines()[0] + assert "test_comment_123" in first_line + + def test_element_grouping(self, tmp_path): + """Elements are grouped by species in the output.""" + lattice = Lattice.cubic(5.0) + # Interleaved species: Si, O, Si, O + s = Structure( + lattice, + ["Si", "O", "Si", "O"], + [[0, 0, 0], [0.25, 0.25, 0.25], [0.5, 0.5, 0.5], [0.75, 0.75, 0.75]], + ) + out = tmp_path / "POSCAR" + write_poscar(s, out) + text = out.read_text() + # The elements line should list each element once + lines = text.splitlines() + # In VASP POSCAR, line 6 (0-indexed 5) has element symbols + elem_line = lines[5].split() + assert elem_line == ["Si", "O"] + + +# =================================================================== +# TestReadCp2kXyz +# =================================================================== + + +class TestReadCp2kXyz: + """read_cp2k_xyz_last_frame for CP2K trajectory files.""" + + @pytest.mark.skipif(not _has_cp2k_xyz, reason="cp2k_opt.xyz not found") + def test_last_frame_reading(self): + symbols, coords, cell = read_cp2k_xyz_last_frame(_XYZ_CP2K) + assert len(symbols) > 0 + assert coords.shape[1] == 3 + assert len(symbols) == coords.shape[0] + + @pytest.mark.skipif(not _has_cp2k_xyz, reason="cp2k_opt.xyz not found") + def test_coords_finite(self): + _, coords, _ = read_cp2k_xyz_last_frame(_XYZ_CP2K) + assert np.all(np.isfinite(coords)) + + def test_malformed_file_raises(self, tmp_path): + bad = tmp_path / "bad.xyz" + bad.write_text("not an xyz file\n") + with pytest.raises(ValueError): + read_cp2k_xyz_last_frame(bad) + + def test_empty_file_raises(self, tmp_path): + empty = tmp_path / "empty.xyz" + empty.write_text("") + with pytest.raises(ValueError): + read_cp2k_xyz_last_frame(empty) + + +# =================================================================== +# TestGetElementSymbols +# =================================================================== + + +class TestGetElementSymbols: + """get_element_symbols returns correct list of symbols.""" + + def test_returns_list(self): + s = Structure(Lattice.cubic(5.0), ["Si", "O"], [[0, 0, 0], [0.5, 0.5, 0.5]]) + result = get_element_symbols(s) + assert isinstance(result, list) + assert len(result) == 2 + + def test_string_types(self): + s = Structure( + Lattice.cubic(5.0), ["Pb", "I", "C"], [[0, 0, 0], [0.3, 0.3, 0.3], [0.6, 0.6, 0.6]] + ) + result = get_element_symbols(s) + assert all(isinstance(sym, str) for sym in result) + + def test_known_elements(self): + s = Structure( + Lattice.cubic(5.0), ["Pb", "I", "C"], [[0, 0, 0], [0.3, 0.3, 0.3], [0.6, 0.6, 0.6]] + ) + result = get_element_symbols(s) + assert result == ["Pb", "I", "C"] diff --git a/tests/test_generate_utils.py b/tests/test_generate_utils.py new file mode 100644 index 00000000..09e96749 --- /dev/null +++ b/tests/test_generate_utils.py @@ -0,0 +1,154 @@ +"""Tests for fullerene_e3gen/generate.py utility functions. + +Tests the geometric helpers (rescale, bond projection, nonbonded repulsion, +validity checker) without needing a trained model. +""" + +from __future__ import annotations + +import sys +from importlib import import_module +from pathlib import Path + +import pytest + +torch = pytest.importorskip("torch") +pytest.importorskip("torch_geometric") + +_POC_DIR = Path(__file__).resolve().parents[1] / "fullerene_e3gen" +if str(_POC_DIR) not in sys.path: + sys.path.insert(0, str(_POC_DIR)) + +# Load helpers only after the optional dependencies and module path are ready. +_generate = import_module("generate") +apply_bond_projection = _generate.apply_bond_projection +apply_nonbonded_repulsion = _generate.apply_nonbonded_repulsion +compute_bond_stats = _generate.compute_bond_stats +compute_radius_stats = _generate.compute_radius_stats +is_valid_structure = _generate.is_valid_structure +rescale_to_unit_radius = _generate.rescale_to_unit_radius +save_xyz = _generate.save_xyz + + +class TestRescaleToUnitRadius: + """Test rescale_to_unit_radius function.""" + + def test_already_unit(self): + torch.manual_seed(0) + pos = torch.randn(20, 3) + pos = pos / pos.norm(dim=1, keepdim=True) # on unit sphere + result = rescale_to_unit_radius(pos) + centered = result - result.mean(dim=0) + radii = centered.norm(dim=1) + assert radii.mean().item() == pytest.approx(1.0, abs=1e-5) + + def test_scaled_up(self): + torch.manual_seed(0) + pos = torch.randn(30, 3) * 5.0 # large scale + result = rescale_to_unit_radius(pos) + centered = result - result.mean(dim=0) + radii = centered.norm(dim=1) + assert radii.mean().item() == pytest.approx(1.0, abs=1e-5) + + +class TestComputeBondStats: + """Test compute_bond_stats function.""" + + def test_regular_triangle(self): + # Equilateral triangle with side = 1.0 + pos = torch.tensor([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 0.866, 0.0]]) + edge_index = torch.tensor([[0, 1, 1, 2, 0, 2], [1, 0, 2, 1, 2, 0]]) + mean, std = compute_bond_stats(pos, edge_index) + assert mean == pytest.approx(1.0, abs=0.01) + assert std < 0.05 + + def test_empty_edges(self): + pos = torch.randn(5, 3) + edge_index = torch.zeros((2, 0), dtype=torch.long) + mean, std = compute_bond_stats(pos, edge_index) + assert mean == 0.0 + assert std == 0.0 + + +class TestComputeRadiusStats: + """Test compute_radius_stats function.""" + + def test_unit_sphere(self): + torch.manual_seed(42) + pos = torch.randn(100, 3) + pos = pos / pos.norm(dim=1, keepdim=True) + mean, std = compute_radius_stats(pos) + assert mean == pytest.approx(1.0, abs=0.05) + assert std < 0.1 + + +class TestApplyBondProjection: + """Test apply_bond_projection function.""" + + def test_shrinks_long_bonds(self): + # Two atoms far apart + pos = torch.tensor([[0.0, 0.0, 0.0], [3.0, 0.0, 0.0]]) + edge_index = torch.tensor([[0, 1], [1, 0]]) + result = apply_bond_projection(pos, edge_index, target_length=1.0, strength=0.5, iters=5) + new_dist = (result[0] - result[1]).norm().item() + assert new_dist < 3.0 # Should have moved closer + + +class TestApplyNonbondedRepulsion: + """Test apply_nonbonded_repulsion function.""" + + def test_pushes_overlapping_atoms(self): + # Two non-bonded atoms very close + pos = torch.tensor([[0.0, 0.0, 0.0], [0.1, 0.0, 0.0]]) + edge_index = torch.zeros((2, 0), dtype=torch.long) # no bonds + result = apply_nonbonded_repulsion(pos, edge_index, min_dist=1.0, strength=0.5, iters=3) + new_dist = (result[0] - result[1]).norm().item() + assert new_dist > 0.1 # Should have been pushed apart + + +class TestIsValidStructure: + """Test is_valid_structure function.""" + + def test_valid_sphere(self): + torch.manual_seed(42) + N = 60 + pos = torch.randn(N, 3) + pos = pos / pos.norm(dim=1, keepdim=True) # unit sphere + + # Build some plausible edges (nearest neighbors) + dists = torch.cdist(pos, pos) + dists.fill_diagonal_(float("inf")) + _, idx = dists.topk(3, dim=1, largest=False) + src = torch.arange(N).unsqueeze(1).expand_as(idx).reshape(-1) + dst = idx.reshape(-1) + edge_index = torch.stack([src, dst]) + + ok, stats = is_valid_structure( + pos, + edge_index, + N, + bond_mean_tol=0.5, + bond_std_max=0.5, + radius_mean_tol=0.5, + radius_std_max=0.5, + ) + assert isinstance(ok, bool) + assert isinstance(stats, dict) + assert "bond_mean" in stats + assert "radius_mean" in stats + + +class TestSaveXyz: + """Test save_xyz function.""" + + def test_writes_valid_xyz(self, tmp_path): + pos = torch.tensor([[0.0, 0.0, 0.0], [1.42, 0.0, 0.0], [0.71, 1.23, 0.0]]) + edge_index = torch.tensor([[0, 1, 1, 2, 0, 2], [1, 0, 2, 1, 2, 0]]) + filepath = tmp_path / "test.xyz" + save_xyz(pos, edge_index, filepath) + + content = filepath.read_text() + lines = content.strip().split("\n") + assert lines[0] == "3" # num atoms + assert len(lines) == 5 # header + comment + 3 atoms + assert lines[2].startswith("C ") # carbon atoms diff --git a/tests/test_geometry.py b/tests/test_geometry.py new file mode 100644 index 00000000..e71f0511 --- /dev/null +++ b/tests/test_geometry.py @@ -0,0 +1,67 @@ +"""Tests for interfaceml.utils.geometry module.""" + +import numpy as np +import pytest + +from interfaceml.utils.geometry import ( + angle_between, + compute_distance, + normalize_vector, +) + + +class TestAngleBetween: + """Test angle_between function.""" + + def test_parallel_vectors(self): + assert angle_between(np.array([1, 0, 0]), np.array([2, 0, 0])) == pytest.approx(0.0) + + def test_antiparallel_vectors(self): + assert angle_between(np.array([1, 0, 0]), np.array([-1, 0, 0])) == pytest.approx(180.0) + + def test_perpendicular_vectors(self): + assert angle_between(np.array([1, 0, 0]), np.array([0, 1, 0])) == pytest.approx(90.0) + + def test_45_degrees(self): + assert angle_between(np.array([1, 0, 0]), np.array([1, 1, 0])) == pytest.approx( + 45.0, abs=1e-6 + ) + + def test_zero_vector_returns_zero(self): + assert angle_between(np.array([0, 0, 0]), np.array([1, 0, 0])) == 0.0 + + +class TestNormalizeVector: + """Test normalize_vector function.""" + + def test_unit_vector(self): + result = normalize_vector(np.array([1.0, 0.0, 0.0])) + np.testing.assert_allclose(result, [1.0, 0.0, 0.0]) + + def test_non_unit_vector(self): + result = normalize_vector(np.array([3.0, 4.0, 0.0])) + np.testing.assert_allclose(result, [0.6, 0.8, 0.0]) + assert np.linalg.norm(result) == pytest.approx(1.0) + + def test_zero_vector_returns_zero(self): + result = normalize_vector(np.array([0.0, 0.0, 0.0])) + np.testing.assert_allclose(result, [0.0, 0.0, 0.0]) + + def test_large_vector(self): + result = normalize_vector(np.array([1e6, 0.0, 0.0])) + assert np.linalg.norm(result) == pytest.approx(1.0) + + +class TestComputeDistance: + """Test compute_distance function.""" + + def test_same_point(self): + assert compute_distance(np.array([1, 2, 3]), np.array([1, 2, 3])) == 0.0 + + def test_unit_distance(self): + assert compute_distance(np.array([0, 0, 0]), np.array([1, 0, 0])) == pytest.approx(1.0) + + def test_3d_distance(self): + # Distance along space diagonal of unit cube = sqrt(3) + d = compute_distance(np.array([0, 0, 0]), np.array([1, 1, 1])) + assert d == pytest.approx(np.sqrt(3)) diff --git a/tests/test_model_and_diffusion.py b/tests/test_model_and_diffusion.py new file mode 100644 index 00000000..14a2c36d --- /dev/null +++ b/tests/test_model_and_diffusion.py @@ -0,0 +1,306 @@ +"""Tests for fullerene_e3gen model and diffusion utilities. + +These tests verify the core ML components without requiring GPU or +trained checkpoints — they test architectural invariants (shapes, +equivariance properties, schedule monotonicity). +""" + +from __future__ import annotations + +import sys +from importlib import import_module +from pathlib import Path + +import pytest + +torch = pytest.importorskip("torch") +pytest.importorskip("torch_geometric") + +# Add fullerene_e3gen to sys.path so we can import directly +_POC_DIR = Path(__file__).resolve().parents[1] / "fullerene_e3gen" +if str(_POC_DIR) not in sys.path: + sys.path.insert(0, str(_POC_DIR)) + +# Preserve dependency checks and search-path setup before loading the ML modules. +DiffusionScheduler = import_module("diffusion_utils").DiffusionScheduler +_model = import_module("model") +FullereneDiffusionModel = _model.FullereneDiffusionModel +GlobalAttentionPool = _model.GlobalAttentionPool +_scatter_softmax = _model._scatter_softmax +bond_length_loss = _model.bond_length_loss +sphericity_loss = _model.sphericity_loss + +# --------------------------------------------------------------------------- +# Model architecture tests +# --------------------------------------------------------------------------- + + +class TestFullereneDiffusionModel: + """Test EGNN model forward pass shapes and properties.""" + + @pytest.fixture() + def small_model(self): + return FullereneDiffusionModel( + hidden_dim=16, + num_layers=2, + edge_dim=0, + C_embed_dim=8, + time_embed_dim=16, + ) + + @pytest.fixture() + def dummy_graph(self): + """A small 10-node 'fullerene' with random 3-regular edges.""" + N = 10 + pos = torch.randn(N, 3) + # Simple cycle + skip edges to approximate 3-regular + src = list(range(N)) + list(range(N)) + dst = [(i + 1) % N for i in range(N)] + [(i + 2) % N for i in range(N)] + edge_index = torch.tensor([src + dst, dst + src], dtype=torch.long) + t = torch.tensor([42], dtype=torch.long) + C = torch.tensor([N], dtype=torch.long) + batch = torch.zeros(N, dtype=torch.long) + return pos, edge_index, t, C, batch + + def test_output_shape(self, small_model, dummy_graph): + pos, edge_index, t, C, batch = dummy_graph + out = small_model(pos, edge_index, t, C, batch) + assert out.shape == pos.shape # [N, 3] + + def test_output_is_finite(self, small_model, dummy_graph): + pos, edge_index, t, C, batch = dummy_graph + out = small_model(pos, edge_index, t, C, batch) + assert torch.isfinite(out).all() + + def test_batch_dimension(self, small_model): + """Two graphs batched together.""" + N1, N2 = 8, 12 + pos = torch.randn(N1 + N2, 3) + + # Build separate edge indices and combine + e1_src = list(range(N1)) + list(range(N1)) + e1_dst = [(i + 1) % N1 for i in range(N1)] + [(i + 2) % N1 for i in range(N1)] + e2_src = [i + N1 for i in range(N2)] + [i + N1 for i in range(N2)] + e2_dst = [(i + 1) % N2 + N1 for i in range(N2)] + [(i + 2) % N2 + N1 for i in range(N2)] + + src = e1_src + e1_dst + e2_src + e2_dst + dst = e1_dst + e1_src + e2_dst + e2_src + edge_index = torch.tensor([src, dst], dtype=torch.long) + + t = torch.tensor([10, 20], dtype=torch.long) + C = torch.tensor([N1, N2], dtype=torch.long) + batch = torch.cat([torch.zeros(N1, dtype=torch.long), torch.ones(N2, dtype=torch.long)]) + + out = small_model(pos, edge_index, t, C, batch) + assert out.shape == (N1 + N2, 3) + + def test_different_timesteps_give_different_outputs(self, small_model, dummy_graph): + pos, edge_index, _, C, batch = dummy_graph + torch.manual_seed(0) + out_t0 = small_model(pos, edge_index, torch.tensor([0]), C, batch) + out_t500 = small_model(pos, edge_index, torch.tensor([500]), C, batch) + # Different timesteps should produce different noise predictions. + # With random initialization, outputs may be very similar, so use a + # loose tolerance and check relative difference instead. + diff = (out_t0 - out_t500).abs().max().item() + assert diff > 0 or not torch.equal(out_t0, out_t500), ( + "Identical outputs for different timesteps" + ) + + +# --------------------------------------------------------------------------- +# Scatter softmax tests +# --------------------------------------------------------------------------- + + +class TestScatterSoftmax: + """Test the per-node scatter softmax implementation.""" + + def test_single_node(self): + w = torch.tensor([1.0, 2.0, 3.0]) + # All edges target node 0 + col = torch.tensor([0, 0, 0]) + result = _scatter_softmax(w, col, num_nodes=1) + expected = torch.softmax(w, dim=0) + torch.testing.assert_close(result, expected) + + def test_two_nodes(self): + w = torch.tensor([1.0, 2.0, 3.0, 4.0]) + col = torch.tensor([0, 0, 1, 1]) + result = _scatter_softmax(w, col, num_nodes=2) + + # Node 0: softmax([1, 2]) + expected_0 = torch.softmax(torch.tensor([1.0, 2.0]), dim=0) + # Node 1: softmax([3, 4]) + expected_1 = torch.softmax(torch.tensor([3.0, 4.0]), dim=0) + + torch.testing.assert_close(result[:2], expected_0) + torch.testing.assert_close(result[2:], expected_1) + + def test_sums_to_one_per_node(self): + w = torch.randn(100) + col = torch.randint(0, 5, (100,)) + result = _scatter_softmax(w, col, num_nodes=5) + + for node in range(5): + mask = col == node + if mask.any(): + assert result[mask].sum().item() == pytest.approx(1.0, abs=1e-5) + + +# --------------------------------------------------------------------------- +# Loss function tests +# --------------------------------------------------------------------------- + + +class TestBondLengthLoss: + """Test bond_length_loss function.""" + + def test_perfect_bonds_zero_loss(self): + # Square with side length 1.42 + pos = torch.tensor([[0.0, 0.0, 0.0], [1.42, 0.0, 0.0], [1.42, 1.42, 0.0], [0.0, 1.42, 0.0]]) + edge_index = torch.tensor([[0, 1, 2, 3], [1, 2, 3, 0]]) + loss = bond_length_loss(pos, edge_index, target_length=1.42) + assert loss.item() == pytest.approx(0.0, abs=1e-4) + + def test_wrong_bonds_nonzero_loss(self): + pos = torch.tensor([[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]]) + edge_index = torch.tensor([[0], [1]]) + loss = bond_length_loss(pos, edge_index, target_length=1.42) + assert loss.item() > 0.0 + + +class TestSphericityLoss: + """Test sphericity_loss function.""" + + def test_sphere_low_loss(self): + # Points uniformly on a unit sphere + torch.manual_seed(42) + N = 60 + pos = torch.randn(N, 3) + pos = pos / pos.norm(dim=1, keepdim=True) # Project onto unit sphere + batch = torch.zeros(N, dtype=torch.long) + loss = sphericity_loss(pos, batch, target_radius=1.0) + assert loss.item() < 0.15 # Should be small for a near-perfect sphere + + def test_non_sphere_higher_loss(self): + # Elongated structure + pos = torch.zeros(60, 3) + pos[:, 0] = torch.linspace(-5, 5, 60) # Line, not sphere + batch = torch.zeros(60, dtype=torch.long) + loss = sphericity_loss(pos, batch, target_radius=1.0) + assert loss.item() > 0.5 # Should be large + + +# --------------------------------------------------------------------------- +# Diffusion scheduler tests +# --------------------------------------------------------------------------- + + +class TestDiffusionScheduler: + """Test DiffusionScheduler properties.""" + + @pytest.fixture() + def scheduler(self): + return DiffusionScheduler(num_steps=100, beta_schedule="cosine") + + def test_alphas_cumprod_decreasing(self, scheduler): + assert (scheduler.alphas_cumprod[:-1] >= scheduler.alphas_cumprod[1:]).all() + + def test_alphas_cumprod_range(self, scheduler): + assert scheduler.alphas_cumprod[0] > 0.9 # Almost no noise at t=0 + assert scheduler.alphas_cumprod[-1] < 0.1 # Almost pure noise at t=T + + def test_sqrt_precomputations(self, scheduler): + expected = torch.sqrt(scheduler.alphas_cumprod) + torch.testing.assert_close(scheduler.sqrt_alphas_cumprod, expected) + + def test_posterior_variance_nonneg(self, scheduler): + assert (scheduler.posterior_variance >= 0).all() + + def test_linear_schedule(self): + sched = DiffusionScheduler(num_steps=50, beta_schedule="linear") + assert sched.alphas_cumprod.shape == (50,) + assert sched.alphas_cumprod[0] > sched.alphas_cumprod[-1] + + +# --------------------------------------------------------------------------- +# Global attention tests +# --------------------------------------------------------------------------- + + +class TestGlobalAttentionPool: + """Test the GlobalAttentionPool module.""" + + def test_output_shape(self): + pool = GlobalAttentionPool(hidden_dim=16) + h = torch.randn(10, 16) + batch = torch.zeros(10, dtype=torch.long) + out = pool(h, batch) + assert out.shape == h.shape + + def test_multi_batch(self): + pool = GlobalAttentionPool(hidden_dim=16) + h = torch.randn(20, 16) + batch = torch.cat([torch.zeros(8, dtype=torch.long), torch.ones(12, dtype=torch.long)]) + out = pool(h, batch) + assert out.shape == (20, 16) + + +class TestGlobalAttentionModel: + """Test FullereneDiffusionModel with global attention enabled.""" + + @pytest.fixture() + def model_with_ga(self): + return FullereneDiffusionModel( + hidden_dim=16, + num_layers=4, + edge_dim=0, + C_embed_dim=8, + time_embed_dim=16, + use_global_attention=True, + global_attention_heads=4, + use_hierarchical=False, + ) + + @pytest.fixture() + def dummy_graph(self): + N = 10 + pos = torch.randn(N, 3) + src = list(range(N)) + list(range(N)) + dst = [(i + 1) % N for i in range(N)] + [(i + 2) % N for i in range(N)] + edge_index = torch.tensor([src + dst, dst + src], dtype=torch.long) + t = torch.tensor([42], dtype=torch.long) + C = torch.tensor([N], dtype=torch.long) + batch = torch.zeros(N, dtype=torch.long) + return pos, edge_index, t, C, batch + + def test_output_shape(self, model_with_ga, dummy_graph): + pos, edge_index, t, C, batch = dummy_graph + out = model_with_ga(pos, edge_index, t, C, batch) + assert out.shape == pos.shape + + def test_rotation_equivariance(self, model_with_ga, dummy_graph): + """Rotating input pos should rotate output by same rotation.""" + pos, edge_index, t, C, batch = dummy_graph + torch.manual_seed(123) + + # Random rotation matrix + theta = torch.tensor(1.23) + R = torch.tensor( + [ + [torch.cos(theta), -torch.sin(theta), 0], + [torch.sin(theta), torch.cos(theta), 0], + [0, 0, 1], + ], + dtype=torch.float32, + ) + + model_with_ga.eval() + with torch.no_grad(): + out_orig = model_with_ga(pos, edge_index, t, C, batch) + out_rotated_input = model_with_ga(pos @ R.T, edge_index, t, C, batch) + + # f(Rx) should equal R f(x) + expected = out_orig @ R.T + torch.testing.assert_close(out_rotated_input, expected, atol=1e-4, rtol=1e-4) diff --git a/tests/test_performance.py b/tests/test_performance.py new file mode 100644 index 00000000..6e4c9a0a --- /dev/null +++ b/tests/test_performance.py @@ -0,0 +1,39 @@ +"""Tests for interfaceml.utils.performance module.""" + +import time + +from interfaceml.utils.performance import ( + suggest_batch_size, + timer, +) + + +class TestTimer: + """Test timer context manager.""" + + def test_returns_elapsed(self): + with timer("test", verbose=False) as result: + time.sleep(0.05) + assert result["elapsed"] >= 0.04 + + def test_zero_elapsed(self): + with timer("test", verbose=False) as result: + pass + assert result["elapsed"] >= 0.0 + + +class TestSuggestBatchSize: + """Test suggest_batch_size function.""" + + def test_small_structures(self): + batch = suggest_batch_size(100, avg_atoms_per_structure=10) + assert batch >= 1 + assert batch <= 100 + + def test_large_structures(self): + batch = suggest_batch_size(100, avg_atoms_per_structure=10000, available_memory_mb=1.0) + assert batch >= 1 + + def test_single_structure(self): + batch = suggest_batch_size(1, avg_atoms_per_structure=100) + assert batch == 1 diff --git a/tests/test_pipeline_routes.py b/tests/test_pipeline_routes.py new file mode 100644 index 00000000..705ba49d --- /dev/null +++ b/tests/test_pipeline_routes.py @@ -0,0 +1,136 @@ +"""Exercise DFT archive generation and reported failures through the web API.""" + +import io +import zipfile + +import pytest +from pymatgen.core import Lattice, Structure +from pymatgen.io.vasp import Poscar + +from interfaceml.web.app import create_app + + +@pytest.fixture +def client(tmp_path): + app = create_app() + app.config.update(TESTING=True, UPLOAD_FOLDER=str(tmp_path)) + with app.test_client() as client: + yield client + + +def upload_carbon(client, filename="carbon.vasp"): + structure = Structure(Lattice.cubic(10), ["C", "C"], [[0.1, 0.1, 0.1], [0.1, 0.1, 0.4]]) + poscar = Poscar(structure, selective_dynamics=[[False] * 3, [True] * 3]) + response = client.post( + "/api/upload", data={"file": (io.BytesIO(str(poscar).encode()), filename)} + ) + assert response.status_code == 200 + return response.get_json()["file_info"]["filename"] + + +def test_standalone_archive_contains_cp2k_constraints(client): + filename = upload_carbon(client) + response = client.post("/api/pipeline/dft-prep", json={"structure_filename": filename}) + assert response.status_code == 200 + body = response.get_json() + assert body["n_jobs"] == 1 + download = client.get(body["download_url"]) + assert download.status_code == 200 + with zipfile.ZipFile(io.BytesIO(download.data)) as archive: + cp2k_name = next(name for name in archive.namelist() if name.endswith("/cp2k.inp")) + cp2k = archive.read(cp2k_name).decode() + assert "&FIXED_ATOMS" in cp2k + assert "LIST 1" in cp2k + assert any(name.endswith("/structure.xyz") for name in archive.namelist()) + assert any(name.endswith("/submit_all.sh") for name in archive.namelist()) + + +def test_failed_preparation_is_not_reported_as_success(client): + client.post("/api/upload", data={"file": (io.BytesIO(b"not a POSCAR"), "broken.vasp")}) + response = client.post("/api/pipeline/dft-prep", json={"structure_filename": "broken.vasp"}) + assert response.status_code == 422 + assert response.get_json()["status"] == "error" + assert "download_url" not in response.get_json() + + +@pytest.mark.parametrize( + "payload", [[], "invalid", {"structure_filename": "carbon.vasp", "dft": {"cutoff": -1}}] +) +def test_invalid_payload_is_a_client_error(client, payload): + upload_carbon(client) + response = client.post("/api/pipeline/dft-prep", json=payload) + assert response.status_code == 400 + + +def test_unknown_download_is_not_found(client): + assert client.get("/api/pipeline/download/unknown").status_code == 404 + + +def test_build_and_prepare_archive(client): + crystal = Structure( + Lattice.cubic(6.3), + ["Cs", "Pb", "I", "I", "I"], + [[0, 0, 0], [0.5, 0.5, 0.5], [0, 0.5, 0.5], [0.5, 0, 0.5], [0.5, 0.5, 0]], + ) + client.post( + "/api/upload", data={"file": (io.BytesIO(str(Poscar(crystal)).encode()), "perovskite.vasp")} + ) + molecule = upload_carbon(client) + response = client.post( + "/api/pipeline/build-and-prep", + json={ + "perovskite_filename": "perovskite.vasp", + "fullerene_filename": molecule, + "supercell": "1x1", + "slab_thickness": 8, + "vacuum": 10, + "fix_bottom_layers": 1, + }, + ) + assert response.status_code == 200 + body = response.get_json() + assert body["n_interfaces"] == body["n_jobs"] == 1 + assert client.get(body["download_url"]).status_code == 200 + + +def test_coherent_scan_and_build_use_matching_parameters(client): + uploaded = {} + for element in ("Si", "Ge"): + crystal = Structure(Lattice.cubic(3), [element], [[0, 0, 0]]) + response = client.post( + "/api/upload", + data={"file": (io.BytesIO(str(Poscar(crystal)).encode()), f"{element}.vasp")}, + ) + uploaded[element] = response.get_json()["file_info"]["filepath"] + payload = { + "base_file": uploaded["Si"], + "film_file": uploaded["Ge"], + "max_area": 10, + "matching_mode": "forward", + "top_k": 1, + } + scan = client.post("/api/list-interface-terminations", json=payload) + assert scan.status_code == 200 + terms = scan.get_json() + assert terms["substrate_terminations"][0]["formula"] == "Si" + assert terms["film_terminations"][0]["formula"] == "Ge" + payload.update( + substrate_termination=terms["substrate_terminations"][0]["label"], + film_termination=terms["film_terminations"][0]["label"], + ) + response = client.post("/api/build-interface", json=payload) + assert response.status_code == 200 + result = response.get_json() + assert result["n_built"] == 1 + assert result["matching_mode"] == "forward" + assert result["strained_layer"] == "film" + assert result["warnings"] == [] + assert client.get(result["results"][0]["download_url"]).status_code == 200 + + response = client.post("/api/build-interface", json={**payload, "twist_deg": 15}) + assert response.get_json()["warnings"] + assert "commensurability" in response.get_json()["warnings"][0] + invalid = client.post( + "/api/build-interface", json={**payload, "matching_mode": "substrate_only"} + ) + assert invalid.status_code == 400 diff --git a/tests/test_validation.py b/tests/test_validation.py new file mode 100644 index 00000000..7e0c3da0 --- /dev/null +++ b/tests/test_validation.py @@ -0,0 +1,87 @@ +"""Tests for interfaceml.utils.validation module.""" + +from pathlib import Path + +import pytest + +from interfaceml.utils.validation import ( + ValidationError, + validate_filepath, + validate_miller_indices, + validate_positive_number, +) + + +class TestValidateFilepath: + """Test validate_filepath function.""" + + def test_existing_file(self, tmp_path): + f = tmp_path / "test.cif" + f.write_text("data") + result = validate_filepath(f, must_exist=True) + assert result == f + + def test_nonexistent_file_raises(self): + with pytest.raises(ValidationError, match="does not exist"): + validate_filepath("/nonexistent/file.cif", must_exist=True) + + def test_nonexistent_file_allowed(self): + result = validate_filepath("/nonexistent/file.cif", must_exist=False) + assert isinstance(result, Path) + + def test_wrong_extension(self, tmp_path): + f = tmp_path / "test.txt" + f.write_text("data") + with pytest.raises(ValidationError, match="extension"): + validate_filepath(f, must_exist=True, allowed_extensions=[".cif", ".vasp"]) + + def test_correct_extension(self, tmp_path): + f = tmp_path / "test.cif" + f.write_text("data") + result = validate_filepath(f, must_exist=True, allowed_extensions=[".cif", ".vasp"]) + assert result == f + + def test_string_path_converted(self, tmp_path): + f = tmp_path / "test.vasp" + f.write_text("data") + result = validate_filepath(str(f), must_exist=True) + assert isinstance(result, Path) + + +class TestValidateMillerIndices: + """Test validate_miller_indices function.""" + + def test_valid_indices(self): + validate_miller_indices([1, 0, 0]) + validate_miller_indices([1, 1, 1]) + validate_miller_indices((0, 0, 1)) + + def test_invalid_length(self): + with pytest.raises(ValidationError): + validate_miller_indices([1, 0]) + + def test_all_zeros(self): + with pytest.raises(ValidationError): + validate_miller_indices([0, 0, 0]) + + +class TestValidatePositiveNumber: + """Test validate_positive_number function.""" + + def test_valid_positive(self): + assert validate_positive_number(5.0, "param") == 5.0 + + def test_zero_not_allowed(self): + with pytest.raises(ValidationError, match="must be positive"): + validate_positive_number(0.0, "param") + + def test_zero_allowed(self): + assert validate_positive_number(0.0, "param", allow_zero=True) == 0.0 + + def test_negative_raises(self): + with pytest.raises(ValidationError): + validate_positive_number(-1.0, "param") + + def test_nan_raises(self): + with pytest.raises(ValidationError, match="finite"): + validate_positive_number(float("nan"), "param") diff --git a/tests/test_web_health.py b/tests/test_web_health.py new file mode 100644 index 00000000..d42ef615 --- /dev/null +++ b/tests/test_web_health.py @@ -0,0 +1,16 @@ +import pytest + +flask = pytest.importorskip("flask") +pytest.importorskip("flask_cors") + + +def test_health_endpoint(): + from interfaceml.web.app import create_app + + app = create_app() + with app.test_client() as client: + resp = client.get("/api/health") + assert resp.status_code == 200 + data = resp.get_json() + assert isinstance(data, dict) + assert data.get("status") == "ok"