Spaces:
Sleeping
Sleeping
Upload 9 files
Browse files- .gitattributes +1 -0
- Dockerfile +35 -0
- readme.md +26 -0
- requirements.txt +10 -0
- src/__init__.py +2 -0
- src/api.py +55 -0
- src/app.py +118 -0
- src/doper.py +97 -0
- static/favicon.png +3 -0
- static/index.html +213 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
static/favicon.png filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
# Set working directory
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
|
| 6 |
+
# Install system dependencies if needed
|
| 7 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 8 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 9 |
+
|
| 10 |
+
# Copy requirements first for better caching
|
| 11 |
+
COPY requirements.txt .
|
| 12 |
+
|
| 13 |
+
# Install Python dependencies
|
| 14 |
+
RUN pip install --no-cache-dir --upgrade pip && \
|
| 15 |
+
pip install --no-cache-dir -r requirements.txt
|
| 16 |
+
|
| 17 |
+
# Copy application code
|
| 18 |
+
COPY src/ ./src/
|
| 19 |
+
|
| 20 |
+
# Copy static files for UI
|
| 21 |
+
COPY static/ ./static/
|
| 22 |
+
|
| 23 |
+
# Create __init__.py if it doesn't exist (for proper Python package structure)
|
| 24 |
+
RUN touch src/__init__.py
|
| 25 |
+
|
| 26 |
+
# Expose port (Hugging Face Spaces uses 7860 by default, but can use PORT env var)
|
| 27 |
+
EXPOSE 7860
|
| 28 |
+
|
| 29 |
+
# Set environment variables
|
| 30 |
+
ENV PYTHONUNBUFFERED=1
|
| 31 |
+
ENV PORT=7860
|
| 32 |
+
|
| 33 |
+
# Run the application
|
| 34 |
+
# Hugging Face Spaces will set the PORT environment variable
|
| 35 |
+
CMD uvicorn src.app:app --host 0.0.0.0 --port ${PORT:-7860}
|
readme.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AURELIUS Battery Optimizer
|
| 2 |
+
|
| 3 |
+
  
|
| 4 |
+
|
| 5 |
+
## 🔬 The Problem
|
| 6 |
+
Traditional materials discovery is slow. Simulating the stability of a doped battery material usually requires setting up complex DFT calculations or managing messy scripts.
|
| 7 |
+
|
| 8 |
+
## 🚀 The Solution
|
| 9 |
+
**Aurelius** is a microservice architecture that allows researchers to screen thousands of doping strategies in seconds.
|
| 10 |
+
|
| 11 |
+
* **Universal Physics Engine:** Dynamically calculates Vegard's Law lattice strain and electronegativity mismatches for *any* host-dopant system using periodic table data (`mendeleev`).
|
| 12 |
+
* **Real-Time Data Integration:** Connects to the **Materials Project API** to fetch ground-truth thermodynamic properties (Band Gap, Volume) for the host material.
|
| 13 |
+
* **High-Entropy Support:** Capable of simulating complex co-doping recipes (mixtures of 2+ elements) to predict stability in high-entropy configurations.
|
| 14 |
+
|
| 15 |
+
## 🛠️ Engineering Architecture
|
| 16 |
+
This is not just a script; it is a production-ready system designed for scale.
|
| 17 |
+
|
| 18 |
+
* **Backend:** FastAPI (Python) serving an async REST API.
|
| 19 |
+
* **Streaming:** Implements **NDJSON Streaming** to deliver inference results row-by-row, preventing Gateway Timeouts during large batch processing.
|
| 20 |
+
* **Resilience:** Features a "Graceful Degradation" adapter that switches to theoretical estimation if the external Materials Project API is down or the material is novel.
|
| 21 |
+
* **Deployment:** Fully Dockerized container serving both the API and a lightweight static UI.
|
| 22 |
+
|
| 23 |
+
## 💻 Usage
|
| 24 |
+
1. Enter a host formula (e.g., `Li3PS4`).
|
| 25 |
+
2. Define a batch of doping recipes (JSON).
|
| 26 |
+
3. The system streams stability predictions back in real-time.
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
pydantic
|
| 4 |
+
mendeleev
|
| 5 |
+
mp-api
|
| 6 |
+
numpy
|
| 7 |
+
scikit-learn
|
| 8 |
+
scipy
|
| 9 |
+
python-dotenv
|
| 10 |
+
typing_extensions
|
src/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Project Aurelius: Material Architect
|
| 2 |
+
# Generative Design API for Solid State Batteries
|
src/api.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
from typing import Dict, Any
|
| 4 |
+
|
| 5 |
+
# Workaround for Python 3.10: Add NotRequired to typing module
|
| 6 |
+
if sys.version_info < (3, 11):
|
| 7 |
+
try:
|
| 8 |
+
from typing_extensions import NotRequired
|
| 9 |
+
import typing
|
| 10 |
+
if not hasattr(typing, 'NotRequired'):
|
| 11 |
+
typing.NotRequired = NotRequired
|
| 12 |
+
except ImportError:
|
| 13 |
+
pass
|
| 14 |
+
|
| 15 |
+
# Try to import MPRester, handle case where key is missing/invalid gracefully
|
| 16 |
+
try:
|
| 17 |
+
from mp_api.client import MPRester
|
| 18 |
+
except ImportError:
|
| 19 |
+
MPRester = None
|
| 20 |
+
|
| 21 |
+
class MaterialsProjectAdapter:
|
| 22 |
+
def __init__(self, api_key: str = None):
|
| 23 |
+
self.api_key = api_key
|
| 24 |
+
self.has_key = api_key is not None and len(api_key) > 10 and MPRester is not None
|
| 25 |
+
|
| 26 |
+
def get_material_properties(self, formula: str) -> Dict[str, Any]:
|
| 27 |
+
print(f"CONTACTING MATERIALS PROJECT: {formula}...")
|
| 28 |
+
|
| 29 |
+
if self.has_key:
|
| 30 |
+
try:
|
| 31 |
+
with MPRester(self.api_key) as mpr:
|
| 32 |
+
docs = mpr.materials.summary.search(
|
| 33 |
+
formula=[formula],
|
| 34 |
+
fields=["band_gap", "volume", "formation_energy_per_atom"]
|
| 35 |
+
)
|
| 36 |
+
if docs:
|
| 37 |
+
best_match = min(docs, key=lambda x: x.formation_energy_per_atom)
|
| 38 |
+
return {
|
| 39 |
+
"source": "Materials Project (Real Data)",
|
| 40 |
+
"band_gap": float(best_match.band_gap),
|
| 41 |
+
"volume": float(best_match.volume),
|
| 42 |
+
"is_estimated": False
|
| 43 |
+
}
|
| 44 |
+
except Exception as e:
|
| 45 |
+
print(f"❌ API ERROR: {str(e)}. Switching to fallback.")
|
| 46 |
+
# Fallback if no key or API fails
|
| 47 |
+
return self._generate_fallback(formula)
|
| 48 |
+
|
| 49 |
+
def _generate_fallback(self, formula: str) -> Dict[str, Any]:
|
| 50 |
+
return {
|
| 51 |
+
"source": "Theoretical Estimation (Fallback)",
|
| 52 |
+
"band_gap": 2.0,
|
| 53 |
+
"volume": 200.0,
|
| 54 |
+
"is_estimated": True
|
| 55 |
+
}
|
src/app.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
from fastapi import FastAPI
|
| 4 |
+
from fastapi.responses import StreamingResponse, FileResponse
|
| 5 |
+
from fastapi.staticfiles import StaticFiles
|
| 6 |
+
from pydantic import BaseModel, Field
|
| 7 |
+
from typing import Dict, List
|
| 8 |
+
from src.doper import UniversalMaterialsOptimizer
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
|
| 11 |
+
app = FastAPI(
|
| 12 |
+
title="AURELIUS Battery Optimizer",
|
| 13 |
+
description="Generative Design API for Co-Doped Solid State Batteries with Streaming Support",
|
| 14 |
+
version="4.0"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
# Load environment variables from .env file
|
| 18 |
+
load_dotenv()
|
| 19 |
+
|
| 20 |
+
# Get API Key from Hugging Face Secrets (Environment Variable)
|
| 21 |
+
MP_API_KEY = os.getenv("MP_API_KEY") or os.getenv("MP_API")
|
| 22 |
+
|
| 23 |
+
# --- SERVE THE UI ---
|
| 24 |
+
# This makes the "static" folder accessible
|
| 25 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 26 |
+
|
| 27 |
+
# --- NEW INPUT MODEL ---
|
| 28 |
+
class MultiDopantRequest(BaseModel):
|
| 29 |
+
host_formula: str = Field(..., example="Li3PS4")
|
| 30 |
+
host_site_element: str = Field(..., example="S")
|
| 31 |
+
|
| 32 |
+
# Now we accept a list of RECIPES to test
|
| 33 |
+
# Example: Check "Just Cl" AND "Cl + Br mixed"
|
| 34 |
+
recipes: List[Dict[str, float]] = Field(..., example=[
|
| 35 |
+
{"Cl": 0.2}, # Pure Cl doping
|
| 36 |
+
{"Cl": 0.1, "Br": 0.1}, # Co-doping (High Entropy)
|
| 37 |
+
{"I": 0.05, "F": 0.05} # Exotic mix
|
| 38 |
+
])
|
| 39 |
+
|
| 40 |
+
@app.get("/")
|
| 41 |
+
async def read_index():
|
| 42 |
+
# When user hits the homepage, send the HTML file
|
| 43 |
+
return FileResponse('static/index.html')
|
| 44 |
+
|
| 45 |
+
# --- THE GENERATOR FUNCTION ---
|
| 46 |
+
# This function doesn't run all at once. It pauses and resumes.
|
| 47 |
+
async def run_simulation_stream(job: MultiDopantRequest):
|
| 48 |
+
|
| 49 |
+
# 1. Initialize Engine (Once)
|
| 50 |
+
optimizer = UniversalMaterialsOptimizer(
|
| 51 |
+
host_formula=job.host_formula,
|
| 52 |
+
host_element_symbol=job.host_site_element,
|
| 53 |
+
api_key=MP_API_KEY
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
# 2. First, stream the metadata header
|
| 57 |
+
# This tells the client "I am ready, here is the context"
|
| 58 |
+
metadata = {
|
| 59 |
+
"type": "meta",
|
| 60 |
+
"host_system": job.host_formula,
|
| 61 |
+
"base_properties": optimizer.props
|
| 62 |
+
}
|
| 63 |
+
yield json.dumps(metadata) + "\n"
|
| 64 |
+
|
| 65 |
+
# 3. Stream results one by one
|
| 66 |
+
for recipe in job.recipes:
|
| 67 |
+
try:
|
| 68 |
+
result = optimizer.analyze_mixture(recipe)
|
| 69 |
+
# Add a type tag so client knows this is a data row
|
| 70 |
+
result["type"] = "data"
|
| 71 |
+
|
| 72 |
+
# Send this chunk immediately!
|
| 73 |
+
yield json.dumps(result) + "\n"
|
| 74 |
+
|
| 75 |
+
except Exception as e:
|
| 76 |
+
error_msg = {"type": "error", "recipe": recipe, "msg": str(e)}
|
| 77 |
+
yield json.dumps(error_msg) + "\n"
|
| 78 |
+
|
| 79 |
+
@app.post("/simulate_stream")
|
| 80 |
+
def simulate_mixture_stream(job: MultiDopantRequest):
|
| 81 |
+
"""
|
| 82 |
+
Returns a StreamingResponse.
|
| 83 |
+
The client will receive line-by-line JSON as calculations finish.
|
| 84 |
+
Ideally, use this endpoint for large numbers of recipes.
|
| 85 |
+
For small numbers of recipes, use /simulate_mix instead.
|
| 86 |
+
"""
|
| 87 |
+
return StreamingResponse(
|
| 88 |
+
run_simulation_stream(job),
|
| 89 |
+
media_type="application/x-ndjson"
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
@app.post("/simulate_mix")
|
| 93 |
+
def simulate_mixture(job: MultiDopantRequest):
|
| 94 |
+
"""
|
| 95 |
+
Non-streaming endpoint for backward compatibility.
|
| 96 |
+
Returns all results at once after all calculations complete.
|
| 97 |
+
Use this endpoint if there are relatively few recipes to test.
|
| 98 |
+
For large numbers of recipes, use /simulate_stream instead.
|
| 99 |
+
"""
|
| 100 |
+
optimizer = UniversalMaterialsOptimizer(
|
| 101 |
+
host_formula=job.host_formula,
|
| 102 |
+
host_element_symbol=job.host_site_element,
|
| 103 |
+
api_key=MP_API_KEY
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
results = []
|
| 107 |
+
for recipe in job.recipes:
|
| 108 |
+
try:
|
| 109 |
+
res = optimizer.analyze_mixture(recipe)
|
| 110 |
+
results.append(res)
|
| 111 |
+
except Exception as e:
|
| 112 |
+
results.append({"recipe": recipe, "error": str(e)})
|
| 113 |
+
|
| 114 |
+
return {
|
| 115 |
+
"host_system": job.host_formula,
|
| 116 |
+
"base_properties": optimizer.props,
|
| 117 |
+
"results": results
|
| 118 |
+
}
|
src/doper.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from src.api import MaterialsProjectAdapter
|
| 2 |
+
from mendeleev import element
|
| 3 |
+
|
| 4 |
+
class UniversalMaterialsOptimizer:
|
| 5 |
+
def __init__(self, host_formula: str, host_element_symbol: str, api_key: str = None):
|
| 6 |
+
self.adapter = MaterialsProjectAdapter(api_key)
|
| 7 |
+
self.props = self.adapter.get_material_properties(host_formula)
|
| 8 |
+
self.base_voltage = self.props['band_gap']
|
| 9 |
+
|
| 10 |
+
host = element(host_element_symbol)
|
| 11 |
+
self.host_radius = host.ionic_radii[0].ionic_radius
|
| 12 |
+
self.host_en = host.en_pauling
|
| 13 |
+
self.host_symbol = host_element_symbol
|
| 14 |
+
|
| 15 |
+
def analyze_dopant(self, dopant_symbol: str, concentration: float):
|
| 16 |
+
dopant = element(dopant_symbol)
|
| 17 |
+
|
| 18 |
+
# 1. Physics Calculations
|
| 19 |
+
delta_en = dopant.en_pauling - self.host_en
|
| 20 |
+
# Simple ionic radius lookup (using first available radius for simplicity)
|
| 21 |
+
dop_rad = dopant.ionic_radii[0].ionic_radius if dopant.ionic_radii else self.host_radius
|
| 22 |
+
radius_diff = dop_rad - self.host_radius
|
| 23 |
+
|
| 24 |
+
voltage_gain = 1.5 * concentration * delta_en
|
| 25 |
+
predicted_voltage = self.base_voltage + voltage_gain
|
| 26 |
+
strain_energy = (radius_diff ** 2) * concentration * 100
|
| 27 |
+
|
| 28 |
+
# 2. Verdict Logic
|
| 29 |
+
status = "Stable"
|
| 30 |
+
if strain_energy > 500: status = "Critical Strain - Phase Separation"
|
| 31 |
+
elif predicted_voltage < 1.0: status = "Voltage Collapse"
|
| 32 |
+
|
| 33 |
+
return {
|
| 34 |
+
"dopant": dopant_symbol,
|
| 35 |
+
"concentration": concentration,
|
| 36 |
+
"predicted_voltage": round(predicted_voltage, 3),
|
| 37 |
+
"lattice_strain": round(strain_energy, 2),
|
| 38 |
+
"stability_status": status,
|
| 39 |
+
"data_confidence": "Low (Estimated)" if self.props['is_estimated'] else "High (Real Data)"
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
def analyze_mixture(self, dopant_recipe: dict):
|
| 43 |
+
"""
|
| 44 |
+
Analyzes a mixture of dopants (Co-doping).
|
| 45 |
+
Input: {"Cl": 0.1, "Br": 0.1} -> implies 0.2 total doping
|
| 46 |
+
"""
|
| 47 |
+
total_conc = sum(dopant_recipe.values())
|
| 48 |
+
|
| 49 |
+
# 1. Edge Case: Empty Recipe
|
| 50 |
+
if total_conc == 0:
|
| 51 |
+
return {"error": "Recipe is empty. Please add at least one dopant."}
|
| 52 |
+
|
| 53 |
+
# 2. Calculate Weighted Properties (Vegard's Law)
|
| 54 |
+
weighted_radius = 0.0
|
| 55 |
+
weighted_en = 0.0
|
| 56 |
+
|
| 57 |
+
detailed_breakdown = []
|
| 58 |
+
|
| 59 |
+
for symbol, amount in dopant_recipe.items():
|
| 60 |
+
atom = element(symbol)
|
| 61 |
+
|
| 62 |
+
# Get radius (default to host radius if data missing to prevent crash)
|
| 63 |
+
r = atom.ionic_radii[0].ionic_radius if atom.ionic_radii else self.host_radius
|
| 64 |
+
en = atom.en_pauling if atom.en_pauling else self.host_en
|
| 65 |
+
|
| 66 |
+
# Contribution to the average
|
| 67 |
+
fraction = amount / total_conc
|
| 68 |
+
weighted_radius += r * fraction
|
| 69 |
+
weighted_en += en * fraction
|
| 70 |
+
|
| 71 |
+
detailed_breakdown.append(f"{symbol} ({amount*100:.1f}%)")
|
| 72 |
+
|
| 73 |
+
# 3. Compare 'Virtual Dopant' vs Host
|
| 74 |
+
radius_diff = weighted_radius - self.host_radius
|
| 75 |
+
delta_en = weighted_en - self.host_en
|
| 76 |
+
|
| 77 |
+
# 4. Physics Engine
|
| 78 |
+
# Voltage is boosted by the total amount of dopant * average electronegativity gain
|
| 79 |
+
voltage_gain = 1.5 * total_conc * delta_en
|
| 80 |
+
predicted_voltage = self.base_voltage + voltage_gain
|
| 81 |
+
|
| 82 |
+
# Strain is proportional to the mismatch squared * total concentration
|
| 83 |
+
strain_energy = (radius_diff ** 2) * total_conc * 100
|
| 84 |
+
|
| 85 |
+
# 5. Verdict
|
| 86 |
+
status = "Stable"
|
| 87 |
+
if strain_energy > 500: status = "Critical Strain - Phase Separation"
|
| 88 |
+
elif predicted_voltage < 1.0: status = "Voltage Collapse"
|
| 89 |
+
|
| 90 |
+
return {
|
| 91 |
+
"recipe_description": ", ".join(detailed_breakdown),
|
| 92 |
+
"total_doping_level": round(total_conc, 3),
|
| 93 |
+
"predicted_voltage": round(predicted_voltage, 3),
|
| 94 |
+
"lattice_strain": round(strain_energy, 2),
|
| 95 |
+
"stability_status": status,
|
| 96 |
+
"data_confidence": "Low (Estimated)" if self.props['is_estimated'] else "High (Real Data)"
|
| 97 |
+
}
|
static/favicon.png
ADDED
|
|
Git LFS Details
|
static/index.html
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>AURELIUS | BATTERY OPTIMIZER</title>
|
| 7 |
+
<link rel="icon" type="image/png" href="/static/favicon.png">
|
| 8 |
+
<style>
|
| 9 |
+
/* "Cyberpunk Lab" Aesthetic */
|
| 10 |
+
body { font-family: 'Courier New', monospace; background-color: #0d1117; color: #c9d1d9; max-width: 950px; margin: 40px auto; padding: 20px; font-size: 16px; }
|
| 11 |
+
h1 { border-bottom: 1px solid #30363d; padding-bottom: 10px; color: #58a6ff; letter-spacing: 2px;}
|
| 12 |
+
|
| 13 |
+
.control-panel { background: #161b22; padding: 25px; border: 1px solid #30363d; border-radius: 6px; margin-bottom: 25px; box-shadow: 0 4px 12px rgba(0,0,0,0.5); }
|
| 14 |
+
|
| 15 |
+
label { display: block; margin-top: 15px; margin-bottom: 5px; color: #8b949e; font-size: 15px; text-transform: uppercase; letter-spacing: 1px; }
|
| 16 |
+
input, textarea, button { width: 100%; box-sizing: border-box; background: #0d1117; border: 1px solid #30363d; color: #c9d1d9; font-family: inherit; font-size: 16px; }
|
| 17 |
+
|
| 18 |
+
input { padding: 10px; }
|
| 19 |
+
textarea { padding: 10px; resize: vertical; font-size: 16px; }
|
| 20 |
+
textarea:focus { outline: none; border-color: #58a6ff; }
|
| 21 |
+
|
| 22 |
+
/* Action Buttons */
|
| 23 |
+
.btn-group { display: flex; gap: 10px; margin-bottom: 10px; }
|
| 24 |
+
.btn-small { width: auto; padding: 5px 15px; font-size: 14px; background: #21262d; cursor: pointer; border-radius: 4px; transition: all 0.2s; }
|
| 25 |
+
.btn-small:hover { background: #30363d; border-color: #8b949e; }
|
| 26 |
+
|
| 27 |
+
.btn-primary { margin-top: 20px; padding: 15px; background: #238636; color: white; cursor: pointer; font-weight: bold; border: none; font-size: 14px; text-transform: uppercase; letter-spacing: 1px; }
|
| 28 |
+
.btn-primary:hover { background: #2ea043; }
|
| 29 |
+
|
| 30 |
+
/* Results Table */
|
| 31 |
+
table { width: 100%; border-collapse: collapse; margin-top: 30px; font-size: 16px; }
|
| 32 |
+
th, td { text-align: left; padding: 12px; border-bottom: 1px solid #21262d; }
|
| 33 |
+
th { color: #8b949e; text-transform: uppercase; font-size: 14px; }
|
| 34 |
+
|
| 35 |
+
/* Status Indicators */
|
| 36 |
+
.status-stable { color: #3fb950; font-weight: bold; }
|
| 37 |
+
.status-critical { color: #f85149; font-weight: bold; }
|
| 38 |
+
.status-warn { color: #d29922; }
|
| 39 |
+
|
| 40 |
+
/* Animation */
|
| 41 |
+
.log-entry { opacity: 0; animation: fadeIn 0.4s forwards; }
|
| 42 |
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } }
|
| 43 |
+
</style>
|
| 44 |
+
</head>
|
| 45 |
+
<body>
|
| 46 |
+
|
| 47 |
+
<h1>AURELIUS // BATTERY OPTIMIZER</h1>
|
| 48 |
+
|
| 49 |
+
<div class="control-panel">
|
| 50 |
+
<div style="display: flex; gap: 20px;">
|
| 51 |
+
<div style="flex: 1;">
|
| 52 |
+
<label>Host Formula</label>
|
| 53 |
+
<input type="text" id="host" value="Li3PS4">
|
| 54 |
+
</div>
|
| 55 |
+
<div style="flex: 1;">
|
| 56 |
+
<label>Substitution Site</label>
|
| 57 |
+
<input type="text" id="site" value="S">
|
| 58 |
+
</div>
|
| 59 |
+
</div>
|
| 60 |
+
|
| 61 |
+
<label>Doping Strategies (JSON Batch)</label>
|
| 62 |
+
|
| 63 |
+
<div class="btn-group">
|
| 64 |
+
<button class="btn-small" onclick="loadExample('simple')">LOAD: Single Dopants</button>
|
| 65 |
+
<button class="btn-small" onclick="loadExample('complex')">LOAD: High-Entropy Mix</button>
|
| 66 |
+
<button class="btn-small" onclick="loadExample('stress')">LOAD: Stress Test (Solubility)</button>
|
| 67 |
+
</div>
|
| 68 |
+
|
| 69 |
+
<textarea id="jsonInput" rows="8"></textarea>
|
| 70 |
+
|
| 71 |
+
<button class="btn-primary" onclick="startSimulation()">>> INITIATE SIMULATION STREAM</button>
|
| 72 |
+
</div>
|
| 73 |
+
|
| 74 |
+
<div id="statusLine" style="color: #8b949e; margin-bottom: 10px; font-size: 15px; min-height: 20px;">SYSTEM READY. AWAITING INPUT.</div>
|
| 75 |
+
|
| 76 |
+
<table id="resultsTable">
|
| 77 |
+
<thead>
|
| 78 |
+
<tr>
|
| 79 |
+
<th style="width: 30%">Composition Strategy</th>
|
| 80 |
+
<th>Pred. Voltage</th>
|
| 81 |
+
<th>Lattice Strain</th>
|
| 82 |
+
<th>Stability Verdict</th>
|
| 83 |
+
</tr>
|
| 84 |
+
</thead>
|
| 85 |
+
<tbody></tbody>
|
| 86 |
+
</table>
|
| 87 |
+
|
| 88 |
+
<script>
|
| 89 |
+
// --- 1. PRE-LOAD DATA FUNCTION ---
|
| 90 |
+
function loadExample(type) {
|
| 91 |
+
const data = {
|
| 92 |
+
simple: `[
|
| 93 |
+
{"Cl": 0.1},
|
| 94 |
+
{"Br": 0.1},
|
| 95 |
+
{"I": 0.05}
|
| 96 |
+
]`,
|
| 97 |
+
complex: `[
|
| 98 |
+
{"Cl": 0.1, "Br": 0.1},
|
| 99 |
+
{"I": 0.05, "F": 0.05},
|
| 100 |
+
{"Cl": 0.2, "I": 0.02},
|
| 101 |
+
{"Br": 0.15, "Cl": 0.05}
|
| 102 |
+
]`,
|
| 103 |
+
stress: `[
|
| 104 |
+
{"Cl": 0.9},
|
| 105 |
+
{"I": 0.5},
|
| 106 |
+
{"F": 0.5, "Cl": 0.5}
|
| 107 |
+
]`
|
| 108 |
+
};
|
| 109 |
+
document.getElementById("jsonInput").value = data[type];
|
| 110 |
+
document.getElementById("jsonInput").style.borderColor = "#30363d"; // Reset color
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
// Initialize with simple data
|
| 114 |
+
loadExample('simple');
|
| 115 |
+
|
| 116 |
+
// --- 2. MAIN SIMULATION LOGIC ---
|
| 117 |
+
async function startSimulation() {
|
| 118 |
+
const tableBody = document.querySelector("#resultsTable tbody");
|
| 119 |
+
const statusLine = document.getElementById("statusLine");
|
| 120 |
+
const inputArea = document.getElementById("jsonInput");
|
| 121 |
+
|
| 122 |
+
// UX: Reset
|
| 123 |
+
tableBody.innerHTML = "";
|
| 124 |
+
statusLine.innerText = "PARSING BATCH INSTRUCTIONS...";
|
| 125 |
+
inputArea.style.borderColor = "#30363d";
|
| 126 |
+
|
| 127 |
+
// A. VALIDATE JSON
|
| 128 |
+
let parsedRecipes;
|
| 129 |
+
try {
|
| 130 |
+
parsedRecipes = JSON.parse(inputArea.value);
|
| 131 |
+
} catch (e) {
|
| 132 |
+
statusLine.innerHTML = `<span style="color: #f85149">⚠️ SYNTAX ERROR: ${e.message}</span>`;
|
| 133 |
+
inputArea.style.borderColor = "#f85149";
|
| 134 |
+
return;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
if (!Array.isArray(parsedRecipes)) {
|
| 138 |
+
statusLine.innerHTML = `<span style="color: #f85149">⚠️ FORMAT ERROR: Root must be a list [...]</span>`;
|
| 139 |
+
return;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
// B. CONNECT TO STREAM
|
| 143 |
+
statusLine.innerText = "ESTABLISHING UPLINK TO PHYSICS ENGINE...";
|
| 144 |
+
|
| 145 |
+
try {
|
| 146 |
+
const payload = {
|
| 147 |
+
host_formula: document.getElementById("host").value,
|
| 148 |
+
host_site_element: document.getElementById("site").value,
|
| 149 |
+
recipes: parsedRecipes
|
| 150 |
+
};
|
| 151 |
+
|
| 152 |
+
const response = await fetch("/simulate_stream", {
|
| 153 |
+
method: "POST",
|
| 154 |
+
headers: { "Content-Type": "application/json" },
|
| 155 |
+
body: JSON.stringify(payload)
|
| 156 |
+
});
|
| 157 |
+
|
| 158 |
+
if (!response.ok) throw new Error(await response.text());
|
| 159 |
+
|
| 160 |
+
// C. READ THE STREAM
|
| 161 |
+
const reader = response.body.getReader();
|
| 162 |
+
const decoder = new TextDecoder();
|
| 163 |
+
statusLine.innerText = "RECEIVING TELEMETRY STREAM...";
|
| 164 |
+
|
| 165 |
+
while (true) {
|
| 166 |
+
const { done, value } = await reader.read();
|
| 167 |
+
if (done) break;
|
| 168 |
+
|
| 169 |
+
const chunk = decoder.decode(value, { stream: true });
|
| 170 |
+
const lines = chunk.split("\n");
|
| 171 |
+
|
| 172 |
+
for (const line of lines) {
|
| 173 |
+
if (!line.trim()) continue;
|
| 174 |
+
try {
|
| 175 |
+
const data = JSON.parse(line);
|
| 176 |
+
|
| 177 |
+
if (data.type === "meta") {
|
| 178 |
+
const source = data.base_properties.source.includes("Real") ? "REAL" : "ESTIMATED";
|
| 179 |
+
statusLine.innerText = `CONNECTED: ${data.host_system} | DATA SOURCE: ${source}`;
|
| 180 |
+
}
|
| 181 |
+
else if (data.type === "data") {
|
| 182 |
+
addRow(data);
|
| 183 |
+
}
|
| 184 |
+
} catch (e) { console.error(e); }
|
| 185 |
+
}
|
| 186 |
+
}
|
| 187 |
+
statusLine.innerText = "BATCH PROCESSING COMPLETE.";
|
| 188 |
+
|
| 189 |
+
} catch (err) {
|
| 190 |
+
statusLine.innerHTML = `<span style="color: #f85149">❌ SYSTEM FAILURE: ${err.message}</span>`;
|
| 191 |
+
}
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
function addRow(data) {
|
| 195 |
+
const tableBody = document.querySelector("#resultsTable tbody");
|
| 196 |
+
const row = document.createElement("tr");
|
| 197 |
+
row.className = "log-entry";
|
| 198 |
+
|
| 199 |
+
let statusClass = "status-warn";
|
| 200 |
+
if (data.stability_status.includes("Stable")) statusClass = "status-stable";
|
| 201 |
+
if (data.stability_status.includes("Critical") || data.stability_status.includes("Collapse")) statusClass = "status-critical";
|
| 202 |
+
|
| 203 |
+
row.innerHTML = `
|
| 204 |
+
<td><strong style="color: #c9d1d9">${data.recipe_description}</strong></td>
|
| 205 |
+
<td>${data.predicted_voltage.toFixed(3)} V</td>
|
| 206 |
+
<td>${data.lattice_strain.toFixed(1)} MJ</td>
|
| 207 |
+
<td class="${statusClass}">${data.stability_status}</td>
|
| 208 |
+
`;
|
| 209 |
+
tableBody.appendChild(row);
|
| 210 |
+
}
|
| 211 |
+
</script>
|
| 212 |
+
</body>
|
| 213 |
+
</html>
|