from src.api import MaterialsProjectAdapter from mendeleev import element class UniversalMaterialsOptimizer: def __init__(self, host_formula: str, host_element_symbol: str, api_key: str = None): self.adapter = MaterialsProjectAdapter(api_key) self.props = self.adapter.get_material_properties(host_formula) self.base_voltage = self.props['band_gap'] host = element(host_element_symbol) self.host_radius = host.ionic_radii[0].ionic_radius self.host_en = host.en_pauling self.host_symbol = host_element_symbol def analyze_dopant(self, dopant_symbol: str, concentration: float): dopant = element(dopant_symbol) # 1. Physics Calculations delta_en = dopant.en_pauling - self.host_en # Simple ionic radius lookup (using first available radius for simplicity) dop_rad = dopant.ionic_radii[0].ionic_radius if dopant.ionic_radii else self.host_radius radius_diff = dop_rad - self.host_radius voltage_gain = 1.5 * concentration * delta_en predicted_voltage = self.base_voltage + voltage_gain strain_energy = (radius_diff ** 2) * concentration * 100 # 2. Verdict Logic status = "Stable" if strain_energy > 500: status = "Critical Strain - Phase Separation" elif predicted_voltage < 1.0: status = "Voltage Collapse" return { "dopant": dopant_symbol, "concentration": concentration, "predicted_voltage": round(predicted_voltage, 3), "lattice_strain": round(strain_energy, 2), "stability_status": status, "data_confidence": "Low (Estimated)" if self.props['is_estimated'] else "High (Real Data)" } def analyze_mixture(self, dopant_recipe: dict): """ Analyzes a mixture of dopants (Co-doping). Input: {"Cl": 0.1, "Br": 0.1} -> implies 0.2 total doping """ total_conc = sum(dopant_recipe.values()) # 1. Edge Case: Empty Recipe if total_conc == 0: return {"error": "Recipe is empty. Please add at least one dopant."} # 2. Calculate Weighted Properties (Vegard's Law) weighted_radius = 0.0 weighted_en = 0.0 detailed_breakdown = [] for symbol, amount in dopant_recipe.items(): atom = element(symbol) # Get radius (default to host radius if data missing to prevent crash) r = atom.ionic_radii[0].ionic_radius if atom.ionic_radii else self.host_radius en = atom.en_pauling if atom.en_pauling else self.host_en # Contribution to the average fraction = amount / total_conc weighted_radius += r * fraction weighted_en += en * fraction detailed_breakdown.append(f"{symbol} ({amount*100:.1f}%)") # 3. Compare 'Virtual Dopant' vs Host radius_diff = weighted_radius - self.host_radius delta_en = weighted_en - self.host_en # 4. Physics Engine # Voltage is boosted by the total amount of dopant * average electronegativity gain voltage_gain = 1.5 * total_conc * delta_en predicted_voltage = self.base_voltage + voltage_gain # Strain is proportional to the mismatch squared * total concentration strain_energy = (radius_diff ** 2) * total_conc * 100 # 5. Verdict status = "Stable" if strain_energy > 500: status = "Critical Strain - Phase Separation" elif predicted_voltage < 1.0: status = "Voltage Collapse" return { "recipe_description": ", ".join(detailed_breakdown), "total_doping_level": round(total_conc, 3), "predicted_voltage": round(predicted_voltage, 3), "lattice_strain": round(strain_energy, 2), "stability_status": status, "data_confidence": "Low (Estimated)" if self.props['is_estimated'] else "High (Real Data)" }