Upload 48 files
Browse files- .gitattributes +1 -0
- Dockerfile +17 -0
- Procfile +1 -0
- __init__.py +1 -0
- app/__init__.py +0 -0
- app/__pycache__/__init__.cpython-311.pyc +0 -0
- app/__pycache__/main.cpython-311.pyc +0 -0
- app/data/CATHODES_DATASET.csv +26 -0
- app/logic/__init__.py +0 -0
- app/logic/__pycache__/__init__.cpython-311.pyc +0 -0
- app/logic/__pycache__/calc_a.cpython-311.pyc +0 -0
- app/logic/__pycache__/calc_b.cpython-311.pyc +0 -0
- app/logic/__pycache__/calc_c.cpython-311.pyc +0 -0
- app/logic/__pycache__/calc_d.cpython-311.pyc +0 -0
- app/logic/__pycache__/calc_e.cpython-311.pyc +0 -0
- app/logic/calc_a.py +218 -0
- app/logic/calc_b.py +206 -0
- app/logic/calc_c.py +272 -0
- app/logic/calc_d.py +220 -0
- app/logic/calc_e.py +236 -0
- app/main.py +24 -0
- app/models/__init__.py +0 -0
- app/models/__pycache__/__init__.cpython-311.pyc +0 -0
- app/models/__pycache__/model_a.cpython-311.pyc +0 -0
- app/models/__pycache__/model_b.cpython-311.pyc +0 -0
- app/models/__pycache__/model_c.cpython-311.pyc +0 -0
- app/models/__pycache__/model_d.cpython-311.pyc +0 -0
- app/models/__pycache__/model_e.cpython-311.pyc +0 -0
- app/models/model_a.py +9 -0
- app/models/model_b.py +29 -0
- app/models/model_c.py +27 -0
- app/models/model_d.py +21 -0
- app/models/model_e.py +21 -0
- app/routes/__init__.py +0 -0
- app/routes/__pycache__/__init__.cpython-311.pyc +0 -0
- app/routes/__pycache__/route_a.cpython-311.pyc +0 -0
- app/routes/__pycache__/route_b.cpython-311.pyc +0 -0
- app/routes/__pycache__/route_c.cpython-311.pyc +0 -0
- app/routes/__pycache__/route_d.cpython-311.pyc +0 -0
- app/routes/__pycache__/route_e.cpython-311.pyc +0 -0
- app/routes/route_a.py +17 -0
- app/routes/route_b.py +23 -0
- app/routes/route_c.py +14 -0
- app/routes/route_d.py +10 -0
- app/routes/route_e.py +18 -0
- app/sentiment/faiss_index.idx +3 -0
- app/sentiment/metadata.json +0 -0
- app/sentiment/process.py +141 -0
- requirements.txt +12 -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 |
+
app/sentiment/faiss_index.idx filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Install system deps if needed later (optional)
|
| 6 |
+
RUN apt-get update && apt-get install -y \
|
| 7 |
+
build-essential \
|
| 8 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 9 |
+
|
| 10 |
+
COPY requirements.txt .
|
| 11 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 12 |
+
|
| 13 |
+
COPY . .
|
| 14 |
+
|
| 15 |
+
EXPOSE 7860
|
| 16 |
+
|
| 17 |
+
CMD ["uvicorn", "backend.app.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
Procfile
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
web: uvicorn app.main:app --host 0.0.0.0 --port 8000
|
__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
app/__init__.py
ADDED
|
File without changes
|
app/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (156 Bytes). View file
|
|
|
app/__pycache__/main.cpython-311.pyc
ADDED
|
Binary file (1.54 kB). View file
|
|
|
app/data/CATHODES_DATASET.csv
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Material_ID,Material_Name,Formula,Structure,Formation Energy (eV/atom),Conductivity(S/cm) ,Activation_Energy_eV,Sigma0 (S/cm),Measurement_Temperature (K),Band Gap (eV),Na_content,N_content,V_content,C_content,Cr_content,Co_content,Mn_content,Fe_content,F_content,Ni_content,Ti_content,P_content,O_content,Type,Electronegativity_Na,Electronegativity_Cr,Electronegativity_V,Electronegativity_Co,Electronegativity_Mn,Electronegativity_Fe,Electronegativity_F,Electronegativity_Ni,Electronegativity_Ti,Electronegativity_P,Electronegativity_O,Electronegativity_C,Electronegativity_N,Ionic_radii_Na,Ionic_radii_V,Ionic_radii_Cr,Ionic_radii_Co,Ionic_radii_Mn,Ionic_radii_Fe,Ionic_radii_F,Ionic_radii_Ni,Ionic_radii_Ti,Ionic_radii_P,Ionic_radii_O,Ionic_radii_C,Ionic_radii_N,Theoretical Capacity (mAh/g),Practical Capacity (mAh/g),Average Voltage (V),Practical Energy Density (Wh/kg),Material Mass (g/mol),Cycle Life to 80% Capacity (cycles),Capacity Retention after 100 Cycles (%),Capacity Retention after 200 Cycles (%),Capacity Retention after 500 Cycles (%),Degradation Rate (%/cycle),C-rate,Voltage Window Lower (V),Voltage Window Upper (V),Power Density (W/kg),Maximum Discharge Current (A/g),Voltage at High C-rate (V),Internal Resistance (Ω),Internal Impedance (S/cm),Capacity at Temperature (mAh/g),Thermal Stability Onset (°C),Ionic Conductivity of Electrolyte (mS/cm),plateau voltage charge ,plateau voltage discharge,knee point charge,knee point discharge,Coulombic Efficiency (%),Charge Time (h),Discharge Time (h),dQ/dV Peak Position Charge (V),dQ/dV Peak Position Discharge (V)
|
| 2 |
+
mp-1179963,Sodium Chromium Oxide,NaCrO2,Hexagonal,-1.136,1.7 × 10⁻⁷ ,0.4,1 × 10⁻³,323,0.71,1,0,0,0,1,0,0,0,0,0,0,0,2,Cathode,0.93,1.66,0,0,0,0,0,0,0,0,3.44,0,0,1.02,0,0.615,0,0,0,0,0,0,0,1.4,0,0,125,110,2.72,177,105.98,1000,90,83,74,0.06,1,2,3.6,150,0.05,2.7,50,2 × 10⁻⁴,103.6 ,250,1.2,3,2.9,3.2,2.7,98.2,2,2,3,2.93
|
| 3 |
+
mp-18245,Sodium Cobalt Phosphate,NaCoPO4,Orthorhombic,-2.31,1.0 × 10⁻⁷,0.73,1.6 × 10⁻³ ,323,2.2,1,0,0,0,0,1,0,0,0,0,0,1,4,Cathode,0.93,0,0,1.88,0,0,0,0,0,2.19,3.44,0,0,1.02,0,0,0.745,0,0,0,0,0,0.17,1.4,0,0,106,85,3.1,132,164.89,500,88,80,70,0.09,1,2,4,120,0.05,2.7,60,2 × 10⁻⁴,84,250,1.2,3.3,3.1,3.4,2.8,98,2,2,3.3,3.1
|
| 4 |
+
mp-18245,Sodium Cobalt Phosphate,NaCoPO4,Orthorhombic,-2.31,2.5 × 10⁻⁶,0.73,1.6 × 10⁻³ ,423,2.2,1,0,0,0,0,1,0,0,0,0,0,1,4,Cathode,0.93,0,0,1.88,0,0,0,0,0,2.19,3.44,0,0,1.02,0,0,0.745,0,0,0,0,0,0.17,1.4,0,0,106,82,3.1,128,164.89,480,86,77,68,0.1,1,2,4,115,0.05,2.6,62,2 × 10⁻⁴,81,250,1.2,3.3,3.1,3.4,2.8,97.5,2,2,3.3,3.1
|
| 5 |
+
mp-18245,Sodium Cobalt Phosphate,NaCoPO4,Orthorhombic,-2.31,1.0 × 10⁻⁵,0.73,1.6 × 10⁻³ ,523,2.2,1,0,0,0,0,1,0,0,0,0,0,1,4,Cathode,0.93,0,0,1.88,0,0,0,0,0,2.19,3.44,0,0,1.02,0,0,0.745,0,0,0,0,0,0.17,1.4,0,0,106,78,3.1,122,164.89,450,84,74,65,0.12,1,2,4,110,0.05,2.5,65,2 × 10⁻⁴,77,250,1.2,3.3,3.1,3.4,2.8,97,2,2,3.3,3.1
|
| 6 |
+
mp-776388,Sodium Nickel Phosphate,NaNiPO₄,Orthorhombic,-2.306,1 × 10⁻⁷,0.75, 1.6 × 10⁻³,323,3.34,1,0,0,0,0,0,0,0,0,1,0,1,4,Cathode,0.93,0,0,0,0,0,0,1.91,0,2.19,3.44,0,0,1.02,0,0,0,0,0,0,0.69,0,0.17,1.4,0,0,128,110,3.3,363,164.67,1000,97,95,92,0.01,1,2,4,120,0.05,2.8,50,2 × 10⁻⁴,109,250,1.2,3.4,3.2,3.5,2.9,99.5,2,2,3.4,3.2
|
| 7 |
+
mp-776388,Sodium Nickel Phosphate,NaNiPO₄,Orthorhombic,-2.306,1 × 10⁻⁶,0.75, 1.6 × 10⁻³,423,3.34,1,0,0,0,0,0,0,0,0,1,0,1,4,Cathode,0.93,0,0,0,0,0,0,1.91,0,2.19,3.44,0,0,1.02,0,0,0,0,0,0,0.69,0,0.17,1.4,0,0,128,105,3.3,315,164.67,900,95,91,85,0.02,1,2,4,115,0.05,2.7,52,2 × 10⁻⁴,104,250,1.2,3.4,3.2,3.5,2.9,99.2,2,2,3.4,3.2
|
| 8 |
+
mp-776388,Sodium Nickel Phosphate,NaNiPO₄,Orthorhombic,-2.306,1 × 10⁻⁵,0.75, 1.6 × 10⁻³,523,3.34,1,0,0,0,0,0,0,0,0,1,0,1,4,Cathode,0.93,0,0,0,0,0,0,1.91,0,2.19,3.44,0,0,1.02,0,0,0,0,0,0,0.69,0,0.17,1.4,0,0,128,98,3.3,294,164.67,800,92,87,80,0.025,1,2,4,110,0.05,2.6,54,2 × 10⁻⁴,97,250,1.2,3.4,3.2,3.5,2.9,99,2,2,3.4,3.2
|
| 9 |
+
mp-19226,Sodium Iron Phosphate (maricite),NaFePO₄,Orthorhombic,–2.411,1.0 × 10⁻⁹,1.5,2.0 × 10⁻⁴,323,1.26,1,0,0,0,0,0,0,1,0,0,0,1,4,Cathode,0.93,0,0,0,0,1.83,0,0,0,2.19,3.44,0,0,1.02,0,0,0,0,0.78,0,0,0,0.17,1.4,0,0,154,142,2.95,169,157.75,950,95,90,84,0.03,1,1.5,4.5,170,0.05,2.6,60,2 × 10⁻⁴,141,250,1.2,2.89,3.06,3.12,2.83,98.5,2,2,2.6,2.1
|
| 10 |
+
mp-19226,Sodium Iron Phosphate (maricite),NaFePO₄,Orthorhombic,–2.411,1.0 × 10⁻⁸,1.5,2.0 × 10⁻⁴,423,1.26,1,0,0,0,0,0,0,1,0,0,0,1,4,Cathode,0.93,0,0,0,0,1.83,0,0,0,2.19,3.44,0,0,1.02,0,0,0,0,0.78,0,0,0,0.17,1.4,0,0,154,135,2.93,158,157.75,900,93,88,80,0.04,1,1.5,4.5,160,0.05,2.5,62,2 × 10⁻⁴,134,250,1.2,2.87,3.03,3.1,2.8,98.2,2,2,2.6,2.1
|
| 11 |
+
mp-19226,Sodium Iron Phosphate (maricite),NaFePO₄,Orthorhombic,–2.411,1.0 × 10⁻⁷,1.5,2.0 × 10⁻⁴,523,1.26,1,0,0,0,0,0,0,1,0,0,0,1,4,Cathode,0.93,0,0,0,0,1.83,0,0,0,2.19,3.44,0,0,1.02,0,0,0,0,0.78,0,0,0,0.17,1.4,0,0,154,128,2.9,148,157.75,850,90,85,75,0.05,1,1.5,4.5,150,0.05,2.4,65,2 × 10⁻⁴,127,250,1.2,2.85,3,3.08,2.75,98,2,2,2.6,2.1
|
| 12 |
+
mp-775855,Sodium Manganese Phosphate,NaMnPO4,Orthorhombic," –2.572",1.0 × 10⁻⁸,1.1,1.0 × 10⁻³,323,3.26,1,0,0,0,0,0,1,0,0,0,0,1,4,Cathode,0.93,0,0,0,1.55,0,0,0,0,2.19,3.44,0,0,1.02,0,0,0,0.83,0,0,0,0,0.17,1.4,0,0,155,90,3.6,162,180.87,800,92,85,75,0.06,1,2,4.2,120,0.05,2.8,55,2 × 10⁻⁴,89,250,1.2,3.7,3.5,3.9,2.7,98.5,2,2,3.7,3.5
|
| 13 |
+
mp-775855,Sodium Manganese Phosphate,NaMnPO4,Orthorhombic,–2.572,1.0 × 10⁻⁷,1.1,1.0 × 10⁻³,423,3.26,1,0,0,0,0,0,1,0,0,0,0,1,4,Cathode,0.93,0,0,0,1.55,0,0,0,0,2.19,3.44,0,0,1.02,0,0,0,0.83,0,0,0,0,0.17,1.4,0,0,155,85,3.6,153,180.87,700,90,82,70,0.07,1,2,4.2,110,0.05,2.7,58,2 × 10⁻⁴,84,250,1.2,3.7,3.5,3.9,2.7,98.2,2,2,3.7,3.5
|
| 14 |
+
mp-775855,Sodium Manganese Phosphate,NaMnPO4,Orthorhombic,–2.572,1.0 × 10⁻⁶,1.1,1.0 × 10⁻³,523,3.26,1,0,0,0,0,0,1,0,0,0,0,1,4,Cathode,0.93,0,0,0,1.55,0,0,0,0,2.19,3.44,0,0,1.02,0,0,0,0.83,0,0,0,0,0.17,1.4,0,0,155,80,3.6,144,180.87,600,88,78,65,0.08,1,2,4.2,105,0.05,2.6,60,2 × 10⁻⁴,79,250,1.2,3.7,3.5,3.9,2.7,98,2,2,3.7,3.5
|
| 15 |
+
mp-776557,Sodium Vanadium Phosphate,Na₃V₂(PO₄)₃,Monoclinic,–2.675,1.1 × 10⁻³,0.32,1.6 × 10⁻³,323,1.92,3,0,2,0,0,0,0,0,0,0,0,3,12,Cathode,0.93,0,1.63,0,0,0,0,0,0,2.19,3.44,0,0,1.02,0.64,0,0,0,0,0,0,0,0.17,1.4,0,0,117.6,107,3.4,225,435.81,5000,98,96,92,0.01,1,2,3.8,3504,2,2.8,40,2 × 10⁻⁴,106,250,1.2,3.4,3.3,3.6,2.7,98.7,1.5,1.5,3.4,3.3
|
| 16 |
+
mp-776557,Sodium Vanadium Phosphate,Na₃V₂(PO₄)₃,Monoclinic,–2.675,2.2 × 10⁻³,0.32,1.6 × 10⁻³,423,1.92,3,0,2,0,0,0,0,0,0,0,0,3,12,Cathode,0.93,0,1.63,0,0,0,0,0,0,2.19,3.44,0,0,1.02,0.64,0,0,0,0,0,0,0,0.17,1.4,0,0,117.6,102,3.4,214,435.81,4000,97,95,90,0.01,1,2,3.8,3200,2,2.7,38,2 × 10⁻⁴,101,250,1.2,3.4,3.3,3.6,2.7,98.5,1.5,1.5,3.4,3.3
|
| 17 |
+
mp-776557,Sodium Vanadium Phosphate,Na₃V₂(PO₄)₃,Monoclinic,–2.675,3.5 × 10⁻³,0.32,1.6 × 10⁻³,523,1.92,3,0,2,0,0,0,0,0,0,0,0,3,12,Cathode,0.93,0,1.63,0,0,0,0,0,0,2.19,3.44,0,0,1.02,0.64,0,0,0,0,0,0,0,0.17,1.4,0,0,117.6,97,3.4,204,435.81,3000,95,92,87,0.012,1,2,3.8,2900,2,2.6,36,2 × 10⁻⁴,96,250,1.2,3.4,3.3,3.6,2.7,98.2,1.5,1.5,3.4,3.3
|
| 18 |
+
mp-19226,Sodium Iron Phosphate,NaFePO₄,Orthorhombic,–2.411,1.0 × 10⁻⁹,1.5,2.0 × 10⁻⁴,323,1.26,1,0,0,0,0,0,0,1,0,0,0,1,4,Cathode,0.93,0,0,0,0,1.83,0,0,0,2.19,3.44,0,0,1.02,0,0,0,0,0.78,0,0,0,0.17,1.4,0,0,154,142,2.95,169,157.75,950,95,90,84,0.03,1,1.5,4.5,170,0.05,2.6,60,2 × 10⁻⁴,141,250,1.2,2.89,3.06,3.12,2.83,98.5,2,2,2.6,2.1
|
| 19 |
+
mp-19226,Sodium Iron Phosphate,NaFePO₄,Orthorhombic,–2.411,1.0 × 10⁻⁸,1.5,2.0 × 10⁻⁴,423,1.26,1,0,0,0,0,0,0,1,0,0,0,1,4,Cathode,0.93,0,0,0,0,1.83,0,0,0,2.19,3.44,0,0,1.02,0,0,0,0,0.78,0,0,0,0.17,1.4,0,0,154,130,2.92,153,157.75,850,92,87,78,0.05,1,1.5,4.5,150,0.05,2.5,62,2 × 10⁻⁴,129,250,1.2,2.87,3.03,3.1,2.8,98.2,2,2,2.6,2.1
|
| 20 |
+
mp-19226,Sodium Iron Phosphate,NaFePO₄,Orthorhombic,–2.411,1.0 × 10⁻⁷,1.5,2.0 × 10⁻⁴,523,1.26,1,0,0,0,0,0,0,1,0,0,0,1,4,Cathode,0.93,0,0,0,0,1.83,0,0,0,2.19,3.44,0,0,1.02,0,0,0,0,0.78,0,0,0,0.17,1.4,0,0,154,125,2.88,140,157.75,800,88,80,70,0.08,1,1.5,4.5,130,0.05,2.3,65,2 × 10⁻⁴,124,250,1.2,2.85,3,3.08,2.75,98,2,2,2.6,2.1
|
| 21 |
+
mp-1194940,Sodium Iron Fluorophosphate,Na₂FePO₄F,Orthorhombic,–2.537,1.1 × 10⁻³,0.6,1.6 × 10⁻³,323,3.53,2,0,0,0,0,0,0,1,1,0,0,1,4,Cathode,0.93,0,0,0,0,1.83,3.98,0,0,2.19,3.44,0,0,1.02,0,0,0,0,0.78,1.33,0,0,0.17,1.4,0,0,124,117,3,351,207.81,2000,98,96,92,0.01,1,2,4.5,760,2,2.7,40,2 × 10⁻⁴,116,250,1.2,3.05,2.92,3.2,2.7,99.5,2,2,3.05,2.92
|
| 22 |
+
mp-1194940,Sodium Iron Fluorophosphate,Na₂FePO₄F,Orthorhombic,–2.537,2.7 × 10⁻⁶,0.6,1.6 × 10⁻³,423,3.53,2,0,0,0,0,0,0,1,1,0,0,1,4,Cathode,0.93,0,0,0,0,1.83,3.98,0,0,2.19,3.44,0,0,1.02,0,0,0,0,0.78,1.33,0,0,0.17,1.4,0,0,124,110,3,330,207.81,1800,97,95,90,0.01,1,2,4.5,700,2,2.6,42,2 × 10⁻⁴,109,250,1.2,3.05,2.92,3.2,2.7,99.4,2,2,3.05,2.92
|
| 23 |
+
mp-1194940,Sodium Iron Fluorophosphate,Na₂FePO₄F,Orthorhombic,–2.537,4.5 × 10⁻⁶,0.6,1.6 × 10⁻³,523,3.53,2,0,0,0,0,0,0,1,1,0,0,1,4,Cathode,0.93,0,0,0,0,1.83,3.98,0,0,2.19,3.44,0,0,1.02,0,0,0,0,0.78,1.33,0,0,0.17,1.4,0,0,124,102,3,306,207.81,1500,95,92,87,0.013,1,2,4.5,650,2,2.5,45,2 × 10⁻⁴,101,250,1.2,3.05,2.92,3.2,2.7,99.2,2,2,3.05,2.92
|
| 24 |
+
mp-694937,Sodium Vanadium Phosphate Fluoride,Na₃V₂P₂O₈F₃,Orthorhombic,–2.823,1.1 × 10⁻³,0.32,1.6 × 10⁻³,323,2.27,3,0,2,0,0,0,0,0,3,0,0,2,8,Cathode,0.93,0,1.63,0,0,0,3.98,0,0,2.19,3.44,0,0,1.02,0.64,0,0,0,0,1.33,0,0,0.17,1.4,0,0,128,114,3.8,433,441.75,2000,98,96,92,0.01,1,2,4.3,800,2,3.2,40,2 × 10⁻⁴,113,250,1.2,4,3.5,4.2,3.2,99.5,2,2,4,3.5
|
| 25 |
+
mp-694937,Sodium Vanadium Phosphate Fluoride,Na₃V₂P₂O₈F₃,Orthorhombic,–2.823,2.2 × 10⁻³,0.32,1.6 × 10⁻³,423,2.27,3,0,2,0,0,0,0,0,3,0,0,2,8,Cathode,0.93,0,1.63,0,0,0,3.98,0,0,2.19,3.44,0,0,1.02,0.64,0,0,0,0,1.33,0,0,0.17,1.4,0,0,128,109,3.8,415,441.75,1800,97,95,90,0.01,1,2,4.3,780,2,3.1,42,2 × 10⁻⁴,108,250,1.2,4,3.5,4.2,3.2,99.3,2,2,4,3.5
|
| 26 |
+
mp-694937,Sodium Vanadium Phosphate Fluoride,Na₃V₂P₂O₈F₃,Orthorhombic,–2.823,3.5 × 10⁻³,0.32,1.6 × 10⁻³,523,2.27,3,0,2,0,0,0,0,0,3,0,0,2,8,Cathode,0.93,0,1.63,0,0,0,3.98,0,0,2.19,3.44,0,0,1.02,0.64,0,0,0,0,1.33,0,0,0.17,1.4,0,0,128,104,3.8,395,441.75,1500,96,94,88,0.012,1,2,4.3,750,2,3,44,2 × 10⁻⁴,103,250,1.2,4,3.5,4.2,3.2,99.1,2,2,4,3.5
|
app/logic/__init__.py
ADDED
|
File without changes
|
app/logic/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (162 Bytes). View file
|
|
|
app/logic/__pycache__/calc_a.cpython-311.pyc
ADDED
|
Binary file (10.8 kB). View file
|
|
|
app/logic/__pycache__/calc_b.cpython-311.pyc
ADDED
|
Binary file (9.27 kB). View file
|
|
|
app/logic/__pycache__/calc_c.cpython-311.pyc
ADDED
|
Binary file (13.2 kB). View file
|
|
|
app/logic/__pycache__/calc_d.cpython-311.pyc
ADDED
|
Binary file (8.54 kB). View file
|
|
|
app/logic/__pycache__/calc_e.cpython-311.pyc
ADDED
|
Binary file (9.91 kB). View file
|
|
|
app/logic/calc_a.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import google.generativeai as genai
|
| 3 |
+
import os
|
| 4 |
+
import faiss
|
| 5 |
+
from openai import OpenAI
|
| 6 |
+
import json
|
| 7 |
+
import numpy as np
|
| 8 |
+
import logging
|
| 9 |
+
|
| 10 |
+
logging.basicConfig(level=logging.INFO)
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
# Fixed hard carbon anode properties
|
| 14 |
+
V_ANODE_MIN = 0.01
|
| 15 |
+
V_ANODE_MAX = 2.0
|
| 16 |
+
V_ANODE_MID = 0.1 # plateau midpoint
|
| 17 |
+
C_SPEC_ANODE = 300 # mAh/g
|
| 18 |
+
|
| 19 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 20 |
+
csv_path = os.path.join(BASE_DIR, "..", "data", "CATHODES_DATASET.csv")
|
| 21 |
+
csv_path = os.path.abspath(csv_path)
|
| 22 |
+
|
| 23 |
+
# Load cathode dataset once (adjust path if needed)
|
| 24 |
+
df_base = pd.read_csv(csv_path)
|
| 25 |
+
|
| 26 |
+
genai.configure(api_key="AIzaSyBwMSL341arzL_FxPzy_DvhDl4Jc46DlaY")
|
| 27 |
+
model = genai.GenerativeModel("gemini-2.5-pro")
|
| 28 |
+
|
| 29 |
+
# Rename columns after loading
|
| 30 |
+
df_base = df_base.rename(columns={
|
| 31 |
+
"Practical Capacity (mAh/g)": "C_spec_cathode",
|
| 32 |
+
"Voltage Window Lower (V)": "V_cathode_min",
|
| 33 |
+
"Voltage Window Upper (V)": "V_cathode_max",
|
| 34 |
+
"plateau voltage discharge": "V_cathode_mid"
|
| 35 |
+
})
|
| 36 |
+
|
| 37 |
+
def query_faiss_index(query_text,
|
| 38 |
+
faiss_index_path=None,
|
| 39 |
+
metadata_path=None,
|
| 40 |
+
openai_api_key="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",
|
| 41 |
+
top_k=5):
|
| 42 |
+
client = OpenAI(api_key=openai_api_key)
|
| 43 |
+
|
| 44 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 45 |
+
|
| 46 |
+
if faiss_index_path is None:
|
| 47 |
+
faiss_index_path = os.path.join(BASE_DIR, "../sentiment/faiss_index.idx")
|
| 48 |
+
if metadata_path is None:
|
| 49 |
+
metadata_path = os.path.join(BASE_DIR, "../sentiment/metadata.json")
|
| 50 |
+
|
| 51 |
+
# Load index and metadata
|
| 52 |
+
index = faiss.read_index(faiss_index_path)
|
| 53 |
+
with open(metadata_path, "r", encoding="utf-8") as f:
|
| 54 |
+
metadata = json.load(f)
|
| 55 |
+
|
| 56 |
+
# Get embedding for query
|
| 57 |
+
response = client.embeddings.create(
|
| 58 |
+
input=query_text.lower(),
|
| 59 |
+
model="text-embedding-3-large"
|
| 60 |
+
)
|
| 61 |
+
query_embedding = response.data[0].embedding
|
| 62 |
+
query_embedding_np = np.array([query_embedding]).astype("float32")
|
| 63 |
+
faiss.normalize_L2(query_embedding_np)
|
| 64 |
+
|
| 65 |
+
# Search index
|
| 66 |
+
distances, indices = index.search(query_embedding_np, top_k)
|
| 67 |
+
results = []
|
| 68 |
+
for dist, idx in zip(distances[0], indices[0]):
|
| 69 |
+
meta = metadata[idx]
|
| 70 |
+
results.append({
|
| 71 |
+
"score": float(dist),
|
| 72 |
+
"source_pdf": meta["source_pdf"],
|
| 73 |
+
"page": meta["page"],
|
| 74 |
+
"chunk_index": meta["chunk_index"],
|
| 75 |
+
"text_snippet": meta["text"]
|
| 76 |
+
})
|
| 77 |
+
|
| 78 |
+
logger.info(f"Query: {query_text}")
|
| 79 |
+
logger.info(f"Embedding length: {len(query_embedding)}")
|
| 80 |
+
logger.info(f"Index dimension: {index.d}")
|
| 81 |
+
logger.info(f"Index contains: {index.ntotal} vectors")
|
| 82 |
+
logger.info(f"Distances: {distances}")
|
| 83 |
+
logger.info(f"Indices: {indices}")
|
| 84 |
+
logger.info(f"Metadata size: {len(metadata)}")
|
| 85 |
+
|
| 86 |
+
return results
|
| 87 |
+
|
| 88 |
+
def calculate_capacity(cathode_name: str, L_anode: float, L_cathode: float,
|
| 89 |
+
M_total: float, t_total: float, C_rate: float):
|
| 90 |
+
df = df_base.copy()
|
| 91 |
+
df = df[df["Material_Name"] == cathode_name]
|
| 92 |
+
|
| 93 |
+
if df.empty:
|
| 94 |
+
raise ValueError(f"Cathode '{cathode_name}' not found in dataset.")
|
| 95 |
+
|
| 96 |
+
df["C_spec_anode"] = C_SPEC_ANODE
|
| 97 |
+
df["L_anode"] = L_anode
|
| 98 |
+
df["L_cathode"] = L_cathode
|
| 99 |
+
|
| 100 |
+
# Areal capacities
|
| 101 |
+
df["Q_anode"] = C_SPEC_ANODE * L_anode / 1000
|
| 102 |
+
df["Q_cathode"] = df["C_spec_cathode"] * L_cathode / 1000
|
| 103 |
+
df["Q_full"] = df[["Q_anode", "Q_cathode"]].min(axis=1)
|
| 104 |
+
|
| 105 |
+
# Base cell capacities
|
| 106 |
+
df["Areal_Capacity_cell"] = df["Q_full"]
|
| 107 |
+
df["Specific_Capacity_cell"] = df["Q_full"] / ((L_anode + L_cathode) / 1000)
|
| 108 |
+
|
| 109 |
+
# Nominal voltage
|
| 110 |
+
df["V_nominal"] = df["V_cathode_mid"] - V_ANODE_MID
|
| 111 |
+
|
| 112 |
+
# Energy densities
|
| 113 |
+
df["Gravimetric_Energy_Density"] = (1000 * df["Q_full"] * df["V_nominal"]) / M_total
|
| 114 |
+
df["Volumetric_Energy_Density"] = (df["Q_full"] * df["V_nominal"] * 1000) / t_total
|
| 115 |
+
|
| 116 |
+
# Voltage window
|
| 117 |
+
df["V_cell_max"] = df["V_cathode_max"] - V_ANODE_MIN
|
| 118 |
+
df["V_cell_min"] = df["V_cathode_min"] - V_ANODE_MAX
|
| 119 |
+
df["Voltage_Window"] = df["V_cell_max"] - df["V_cell_min"]
|
| 120 |
+
|
| 121 |
+
# Specific power
|
| 122 |
+
df["Specific_Power"] = df["Gravimetric_Energy_Density"] * C_rate
|
| 123 |
+
|
| 124 |
+
result = {
|
| 125 |
+
"Cathode": cathode_name,
|
| 126 |
+
"Anode": "Hard Carbon",
|
| 127 |
+
"Q_areal": float(df["Q_full"].values[0]),
|
| 128 |
+
"Q_specific": float(df["Specific_Capacity_cell"].values[0]),
|
| 129 |
+
"Gravimetric_Energy_Density": float(df["Gravimetric_Energy_Density"].values[0]),
|
| 130 |
+
"Volumetric_Energy_Density": float(df["Volumetric_Energy_Density"].values[0]),
|
| 131 |
+
"V_nominal": float(df["V_nominal"].values[0]),
|
| 132 |
+
"Voltage_Window": float(df["Voltage_Window"].values[0]),
|
| 133 |
+
"Specific_Power": float(df["Specific_Power"].values[0])
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
query_text = (
|
| 137 |
+
f"Sodium-ion battery with hard carbon anode and cathode {cathode_name}. "
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
faiss_results = query_faiss_index(query_text, top_k=5)
|
| 141 |
+
|
| 142 |
+
try:
|
| 143 |
+
prompt = f"""
|
| 144 |
+
Remove the underscores and .pdf from the source_pdf field in the RAG section below and put it as References at the end of the explanation.
|
| 145 |
+
You are to explain the results of the calculations of a sodium-ion full cell battery using hard carbon and {cathode_name}. You are a RAG system that takes information only from the RAG section below.
|
| 146 |
+
And respond with extensive/long explanation in a scientific way and straight to the point without any additional text. Do not include opinions just explanations
|
| 147 |
+
of the results of the calculations.
|
| 148 |
+
Lastly shortly analyze the performance of the full cell battery.
|
| 149 |
+
|
| 150 |
+
Below are the formulas used to compute key metrics. After reviewing them, interpret the calculated results:
|
| 151 |
+
|
| 152 |
+
**Fixed Anode Parameters:**
|
| 153 |
+
- Specific Capacity of Anode (C_SPEC_ANODE): 300 mAh/g
|
| 154 |
+
- Voltage Window: 0.01 V to 2.0 V
|
| 155 |
+
- Plateau Midpoint Voltage (V_ANODE_MID): 0.1 V
|
| 156 |
+
|
| 157 |
+
**Formulas Used:**
|
| 158 |
+
|
| 159 |
+
1. **Areal Capacity (mAh/cm²):**
|
| 160 |
+
- Q_anode = C_SPEC_ANODE × L_anode / 1000
|
| 161 |
+
- Q_cathode = C_spec_cathode × L_cathode / 1000
|
| 162 |
+
- Q_full = min(Q_anode, Q_cathode)
|
| 163 |
+
|
| 164 |
+
2. **Specific Capacity (mAh/g):**
|
| 165 |
+
- Q_specific = Q_full / ((L_anode + L_cathode) / 1000)
|
| 166 |
+
|
| 167 |
+
3. **Nominal Voltage (V):**
|
| 168 |
+
- V_nominal = V_cathode_mid - V_ANODE_MID
|
| 169 |
+
|
| 170 |
+
4. **Gravimetric Energy Density (Wh/kg):**
|
| 171 |
+
- GED = 1000 × Q_full × V_nominal / M_total
|
| 172 |
+
|
| 173 |
+
5. **Volumetric Energy Density (Wh/L):**
|
| 174 |
+
- VED = Q_full × V_nominal × 1000 / t_total
|
| 175 |
+
|
| 176 |
+
6. **Voltage Window (V):**
|
| 177 |
+
- V_cell_max = V_cathode_max - 0.01
|
| 178 |
+
- V_cell_min = V_cathode_min - 2.0
|
| 179 |
+
- Voltage_Window = V_cell_max - V_cell_min
|
| 180 |
+
|
| 181 |
+
7. **Specific Power (W/kg):**
|
| 182 |
+
- P = V_nominal × Q_full × 1000 × C_rate / 3600
|
| 183 |
+
|
| 184 |
+
**Inputs Provided:**
|
| 185 |
+
- Cathode material: {cathode_name}
|
| 186 |
+
- L_anode (mg/cm²): {L_anode}
|
| 187 |
+
- L_cathode (mg/cm²): {L_cathode}
|
| 188 |
+
- M_total (mg): {M_total}
|
| 189 |
+
- t_total (mm): {t_total}
|
| 190 |
+
- C_rate: {C_rate}
|
| 191 |
+
|
| 192 |
+
**Calculated Results:**
|
| 193 |
+
```json
|
| 194 |
+
{result }
|
| 195 |
+
```
|
| 196 |
+
Remove the underscores and .pdf from the source_pdf field in the RAG section below and put it as References at the end of the explanation. Do not include the PDF file name directly in the explanation.
|
| 197 |
+
At the end of the explanation, list the full source_pdf file names used as references.
|
| 198 |
+
RAG Section, use only the information from this section to explain the results:
|
| 199 |
+
{json.dumps(faiss_results, indent=2)}
|
| 200 |
+
"""
|
| 201 |
+
logger.info("Generated prompt for Gemini model: %s", prompt.strip())
|
| 202 |
+
gemini_response = model.generate_content(prompt)
|
| 203 |
+
|
| 204 |
+
if gemini_response.candidates:
|
| 205 |
+
parts = gemini_response.candidates[0].content.parts
|
| 206 |
+
explanation = " ".join(
|
| 207 |
+
p.text for p in parts if hasattr(p, "text") and p.text
|
| 208 |
+
)
|
| 209 |
+
else:
|
| 210 |
+
explanation = "Gemini returned no candidates."
|
| 211 |
+
|
| 212 |
+
result["Gemini_Explanation"] = explanation.strip()
|
| 213 |
+
|
| 214 |
+
except Exception as e:
|
| 215 |
+
result["Gemini_Explanation"] = f"Gemini error: {str(e)}"
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
return result
|
app/logic/calc_b.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
from typing import Dict, List
|
| 3 |
+
import google.generativeai as genai
|
| 4 |
+
import faiss
|
| 5 |
+
from openai import OpenAI
|
| 6 |
+
import json
|
| 7 |
+
import os
|
| 8 |
+
import pandas as pd
|
| 9 |
+
|
| 10 |
+
genai.configure(api_key="AIzaSyBwMSL341arzL_FxPzy_DvhDl4Jc46DlaY")
|
| 11 |
+
model = genai.GenerativeModel("gemini-2.5-pro")
|
| 12 |
+
|
| 13 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 14 |
+
csv_path = os.path.join(BASE_DIR, "..", "data", "CATHODES_DATASET.csv")
|
| 15 |
+
csv_path = os.path.abspath(csv_path)
|
| 16 |
+
|
| 17 |
+
df_base = pd.read_csv(csv_path)
|
| 18 |
+
|
| 19 |
+
def query_faiss_index(query_text,
|
| 20 |
+
faiss_index_path=None,
|
| 21 |
+
metadata_path=None,
|
| 22 |
+
openai_api_key="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",
|
| 23 |
+
top_k=5):
|
| 24 |
+
client = OpenAI(api_key=openai_api_key)
|
| 25 |
+
|
| 26 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 27 |
+
|
| 28 |
+
if faiss_index_path is None:
|
| 29 |
+
faiss_index_path = os.path.join(BASE_DIR, "../sentiment/faiss_index.idx")
|
| 30 |
+
if metadata_path is None:
|
| 31 |
+
metadata_path = os.path.join(BASE_DIR, "../sentiment/metadata.json")
|
| 32 |
+
|
| 33 |
+
# Load index and metadata
|
| 34 |
+
index = faiss.read_index(faiss_index_path)
|
| 35 |
+
with open(metadata_path, "r", encoding="utf-8") as f:
|
| 36 |
+
metadata = json.load(f)
|
| 37 |
+
|
| 38 |
+
# Get embedding for query
|
| 39 |
+
response = client.embeddings.create(
|
| 40 |
+
input=query_text.lower(),
|
| 41 |
+
model="text-embedding-3-large"
|
| 42 |
+
)
|
| 43 |
+
query_embedding = response.data[0].embedding
|
| 44 |
+
query_embedding_np = np.array([query_embedding]).astype("float32")
|
| 45 |
+
faiss.normalize_L2(query_embedding_np)
|
| 46 |
+
|
| 47 |
+
# Search index
|
| 48 |
+
distances, indices = index.search(query_embedding_np, top_k)
|
| 49 |
+
results = []
|
| 50 |
+
for dist, idx in zip(distances[0], indices[0]):
|
| 51 |
+
meta = metadata[idx]
|
| 52 |
+
results.append({
|
| 53 |
+
"score": float(dist),
|
| 54 |
+
"source_pdf": meta["source_pdf"],
|
| 55 |
+
"page": meta["page"],
|
| 56 |
+
"chunk_index": meta["chunk_index"],
|
| 57 |
+
"text_snippet": meta["text"]
|
| 58 |
+
})
|
| 59 |
+
return results
|
| 60 |
+
|
| 61 |
+
def calculate_vq_dqdv(V: List[float], I: float, t: List[float]) -> Dict[str, List[float]]:
|
| 62 |
+
V_arr = np.array(V, dtype=float)
|
| 63 |
+
t_arr = np.array(t, dtype=float)
|
| 64 |
+
Q_arr = I * t_arr
|
| 65 |
+
dQ = np.gradient(Q_arr)
|
| 66 |
+
dV = np.gradient(V_arr)
|
| 67 |
+
with np.errstate(divide='ignore', invalid='ignore'):
|
| 68 |
+
dQdV = np.where(dV!=0, dQ/dV, 0.0)
|
| 69 |
+
return {"Q": Q_arr.tolist(), "V": V_arr.tolist(), "dQdV": dQdV.tolist()}
|
| 70 |
+
|
| 71 |
+
def calculate_rate_capability(
|
| 72 |
+
Q_nominal: float,
|
| 73 |
+
C_rates: List[float],
|
| 74 |
+
t_discharge: List[float]
|
| 75 |
+
) -> Dict[str, List[float]]:
|
| 76 |
+
# Convert to arrays
|
| 77 |
+
C = np.array(C_rates, dtype=float)
|
| 78 |
+
t = np.array(t_discharge, dtype=float)
|
| 79 |
+
|
| 80 |
+
# Validation
|
| 81 |
+
if C.shape != t.shape:
|
| 82 |
+
raise ValueError("C_rates and t_discharge must have same length")
|
| 83 |
+
|
| 84 |
+
# Q_Ci = C_i * Q_nominal * t_i
|
| 85 |
+
Q_ci = C * Q_nominal * t
|
| 86 |
+
return {"C_rates": C.tolist(), "Q_Ci": Q_ci.tolist()}
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def calculate_cccv_time(
|
| 90 |
+
Q_nominal: float,
|
| 91 |
+
I_lim: float,
|
| 92 |
+
alpha: float,
|
| 93 |
+
I_end: float,
|
| 94 |
+
tau: float
|
| 95 |
+
) -> float:
|
| 96 |
+
"""
|
| 97 |
+
t_charge = t_CC + t_CV
|
| 98 |
+
= (alpha * Q_nominal)/I_lim + tau * ln(I_lim/I_end)
|
| 99 |
+
"""
|
| 100 |
+
# CC time:
|
| 101 |
+
t_CC = (alpha * Q_nominal) / I_lim
|
| 102 |
+
# CV time:
|
| 103 |
+
t_CV = tau * np.log(I_lim / I_end)
|
| 104 |
+
return float(t_CC + t_CV)
|
| 105 |
+
|
| 106 |
+
def calculate_diffusion(
|
| 107 |
+
L: float,
|
| 108 |
+
tau_pulse: float,
|
| 109 |
+
delta_E_tau: List[float],
|
| 110 |
+
delta_E_s: List[float]
|
| 111 |
+
) -> Dict[str, List[float]]:
|
| 112 |
+
"""
|
| 113 |
+
D_i = (π/4) * (L^2 / τ_pulse) * (ΔEτ_i / ΔEs_i)^2
|
| 114 |
+
"""
|
| 115 |
+
Δτ = np.array(delta_E_tau, dtype=float)
|
| 116 |
+
Δs = np.array(delta_E_s, dtype=float)
|
| 117 |
+
# avoid divide‑by‑zero
|
| 118 |
+
ratio2 = np.where(Δs != 0, (Δτ / Δs)**2, 0.0)
|
| 119 |
+
D = (np.pi / 4) * (L**2 / tau_pulse) * ratio2
|
| 120 |
+
return {"D": D.tolist()}
|
| 121 |
+
|
| 122 |
+
def calculate_all_b(cathode_name: str, input_data: Dict) -> Dict:
|
| 123 |
+
# 1) V–Q & dQ/dV
|
| 124 |
+
vq = calculate_vq_dqdv(
|
| 125 |
+
V=input_data["V"],
|
| 126 |
+
I=input_data["I"],
|
| 127 |
+
t=input_data["t"]
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
# 2) Rate capability
|
| 131 |
+
rate = calculate_rate_capability(
|
| 132 |
+
input_data["Q_nominal"],
|
| 133 |
+
input_data["C_rates"],
|
| 134 |
+
input_data["t_discharge"]
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
# 3) CC–CV charge time
|
| 138 |
+
t_charge = calculate_cccv_time(
|
| 139 |
+
Q_nominal = input_data["Q_nominal_mAh"],
|
| 140 |
+
I_lim = input_data["I_lim"],
|
| 141 |
+
alpha = input_data["alpha"],
|
| 142 |
+
I_end = input_data["I_end"],
|
| 143 |
+
tau = input_data["tau"]
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
diffusion = calculate_diffusion(
|
| 147 |
+
L = input_data["L"],
|
| 148 |
+
tau_pulse = input_data["tau_pulse"],
|
| 149 |
+
delta_E_tau = input_data["delta_E_tau"],
|
| 150 |
+
delta_E_s = input_data["delta_E_s"]
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
query_text = (
|
| 154 |
+
f"Sodium-ion battery with hard carbon anode and cathode {cathode_name}. "
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
faiss_results = query_faiss_index(query_text, top_k=5)
|
| 158 |
+
|
| 159 |
+
prompt = f"""
|
| 160 |
+
Remove the underscores and .pdf from the source_pdf field in the RAG section below and put it as References at the end of the explanation.
|
| 161 |
+
You are to explain the results of the calculations of a sodium-ion full cell battery using hard carbon and {cathode_name}. You are a RAG system that takes information only from the RAG section below.
|
| 162 |
+
And respond with extensive/long explanation in a scientific way and straight to the point without any additional text. Do not include opinions just explanations
|
| 163 |
+
of the results of the calculations.
|
| 164 |
+
Lastly shortly analyze the performance of the full cell battery.
|
| 165 |
+
|
| 166 |
+
1. **V–Q curve**: {vq['Q']} vs {vq['V']}
|
| 167 |
+
2. **dQ/dV curve**: {vq['dQdV']}
|
| 168 |
+
3. **Rate capability test**:
|
| 169 |
+
- C-rates: {rate['C_rates']}
|
| 170 |
+
- Discharge capacities: {rate['Q_Ci']}
|
| 171 |
+
4. **CC–CV charging time**: {t_charge:.2f} seconds
|
| 172 |
+
5. **Diffusion coefficients** (D): {diffusion['D']}
|
| 173 |
+
|
| 174 |
+
Please comment on:
|
| 175 |
+
- Whether the electrode is rate-limited
|
| 176 |
+
- Diffusion characteristics
|
| 177 |
+
- Fast-charging behavior
|
| 178 |
+
- Any obvious limitations or strengths
|
| 179 |
+
|
| 180 |
+
Remove the underscores and .pdf from the source_pdf field in the RAG section below and put it as References at the end of the explanation. Do not include the PDF file name directly in the explanation.
|
| 181 |
+
At the end of the explanation, list the full source_pdf file names used as references.
|
| 182 |
+
RAG Section, use only the information from this section to explain the results:
|
| 183 |
+
{json.dumps(faiss_results, indent=2)}
|
| 184 |
+
"""
|
| 185 |
+
|
| 186 |
+
try:
|
| 187 |
+
gemini_response = model.generate_content(prompt)
|
| 188 |
+
|
| 189 |
+
if gemini_response.candidates:
|
| 190 |
+
parts = gemini_response.candidates[0].content.parts
|
| 191 |
+
gemini_text = " ".join(
|
| 192 |
+
p.text for p in parts if hasattr(p, "text") and p.text
|
| 193 |
+
)
|
| 194 |
+
else:
|
| 195 |
+
gemini_text = "Gemini returned no candidates."
|
| 196 |
+
|
| 197 |
+
except Exception as e:
|
| 198 |
+
gemini_text = f"Gemini analysis failed: {str(e)}"
|
| 199 |
+
|
| 200 |
+
return {
|
| 201 |
+
"vq_curve": vq,
|
| 202 |
+
"rate_capability": rate,
|
| 203 |
+
"cccv_time_s": t_charge,
|
| 204 |
+
"diffusion_D": diffusion["D"],
|
| 205 |
+
"Gemini_Explanation": gemini_text
|
| 206 |
+
}
|
app/logic/calc_c.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import scipy
|
| 3 |
+
from scipy.signal import savgol_filter
|
| 4 |
+
from typing import Dict, List
|
| 5 |
+
import matplotlib.pyplot as plt
|
| 6 |
+
import io
|
| 7 |
+
import base64
|
| 8 |
+
import google.generativeai as genai
|
| 9 |
+
import faiss
|
| 10 |
+
from openai import OpenAI
|
| 11 |
+
import json
|
| 12 |
+
import os
|
| 13 |
+
import pandas as pd
|
| 14 |
+
|
| 15 |
+
genai.configure(api_key="AIzaSyBwMSL341arzL_FxPzy_DvhDl4Jc46DlaY")
|
| 16 |
+
model = genai.GenerativeModel("gemini-2.5-flash")
|
| 17 |
+
|
| 18 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 19 |
+
csv_path = os.path.join(BASE_DIR, "..", "data", "CATHODES_DATASET.csv")
|
| 20 |
+
csv_path = os.path.abspath(csv_path)
|
| 21 |
+
|
| 22 |
+
df_base = pd.read_csv(csv_path)
|
| 23 |
+
|
| 24 |
+
def query_faiss_index(query_text,
|
| 25 |
+
faiss_index_path=None,
|
| 26 |
+
metadata_path=None,
|
| 27 |
+
openai_api_key="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",
|
| 28 |
+
top_k=5):
|
| 29 |
+
client = OpenAI(api_key=openai_api_key)
|
| 30 |
+
|
| 31 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 32 |
+
|
| 33 |
+
if faiss_index_path is None:
|
| 34 |
+
faiss_index_path = os.path.join(BASE_DIR, "../sentiment/faiss_index.idx")
|
| 35 |
+
if metadata_path is None:
|
| 36 |
+
metadata_path = os.path.join(BASE_DIR, "../sentiment/metadata.json")
|
| 37 |
+
|
| 38 |
+
# Load index and metadata
|
| 39 |
+
index = faiss.read_index(faiss_index_path)
|
| 40 |
+
with open(metadata_path, "r", encoding="utf-8") as f:
|
| 41 |
+
metadata = json.load(f)
|
| 42 |
+
|
| 43 |
+
# Get embedding for query
|
| 44 |
+
response = client.embeddings.create(
|
| 45 |
+
input=query_text.lower(),
|
| 46 |
+
model="text-embedding-3-large"
|
| 47 |
+
)
|
| 48 |
+
query_embedding = response.data[0].embedding
|
| 49 |
+
query_embedding_np = np.array([query_embedding]).astype("float32")
|
| 50 |
+
faiss.normalize_L2(query_embedding_np)
|
| 51 |
+
|
| 52 |
+
# Search index
|
| 53 |
+
distances, indices = index.search(query_embedding_np, top_k)
|
| 54 |
+
results = []
|
| 55 |
+
for dist, idx in zip(distances[0], indices[0]):
|
| 56 |
+
meta = metadata[idx]
|
| 57 |
+
results.append({
|
| 58 |
+
"score": float(dist),
|
| 59 |
+
"source_pdf": meta["source_pdf"],
|
| 60 |
+
"page": meta["page"],
|
| 61 |
+
"chunk_index": meta["chunk_index"],
|
| 62 |
+
"text_snippet": meta["text"]
|
| 63 |
+
})
|
| 64 |
+
return results
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def calculate_cv(input_data: Dict) -> Dict:
|
| 68 |
+
V_start = input_data["V_start"]
|
| 69 |
+
V_switch = input_data["V_switch"]
|
| 70 |
+
scan_rate = input_data["scan_rate"]
|
| 71 |
+
dt = input_data["dt"]
|
| 72 |
+
sigma = input_data["sigma"]
|
| 73 |
+
E0 = input_data["E0"]
|
| 74 |
+
Ip = input_data["Ip"]
|
| 75 |
+
|
| 76 |
+
# --- Time & Voltage Arrays ---
|
| 77 |
+
t_up = np.arange(0, (V_switch - V_start) / scan_rate, dt)
|
| 78 |
+
t_down = np.arange(0, (V_switch - V_start) / scan_rate, dt)
|
| 79 |
+
V_up = V_start + scan_rate * t_up
|
| 80 |
+
V_down = V_switch - scan_rate * t_down
|
| 81 |
+
V = np.concatenate([V_up, V_down])
|
| 82 |
+
|
| 83 |
+
# --- Simulated CV current ---
|
| 84 |
+
I_ox = Ip * np.exp(-((V - E0) ** 2) / (2 * sigma ** 2))
|
| 85 |
+
I_red = -Ip * np.exp(-((V - (E0 - 0.06)) ** 2) / (2 * sigma ** 2))
|
| 86 |
+
I = I_ox + I_red
|
| 87 |
+
|
| 88 |
+
# --- Peak Analysis ---
|
| 89 |
+
idx_ox = np.argmax(I)
|
| 90 |
+
idx_red = np.argmin(I)
|
| 91 |
+
V_ox = float(V[idx_ox])
|
| 92 |
+
V_red = float(V[idx_red])
|
| 93 |
+
delta_V_peak = V_ox - V_red
|
| 94 |
+
|
| 95 |
+
# --- Integrated Charge (Coulombs) ---
|
| 96 |
+
# ∫ I dt = ∫ I dV × (1/scan_rate) ⇒ simps(I, V) / scan_rate
|
| 97 |
+
Q = float(scipy.integrate.simpson(I, V) / scan_rate)
|
| 98 |
+
|
| 99 |
+
return {
|
| 100 |
+
"t": np.concatenate([t_up, t_down]).tolist(),
|
| 101 |
+
"V": V.tolist(),
|
| 102 |
+
"I": I.tolist(),
|
| 103 |
+
"V_ox": V_ox,
|
| 104 |
+
"V_red": V_red,
|
| 105 |
+
"delta_V_peak": delta_V_peak,
|
| 106 |
+
"Q_integrated": Q
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
def calculate_eis(data: Dict) -> Dict:
|
| 110 |
+
freqs = np.array(data["frequencies"], dtype=float)
|
| 111 |
+
Rs, Rct, Cdl, sigma_w = data["Rs"], data["Rct"], data["Cdl"], data["sigma_w"]
|
| 112 |
+
omega = 2*np.pi*freqs
|
| 113 |
+
j = 1j
|
| 114 |
+
|
| 115 |
+
# Warburg impedance
|
| 116 |
+
Zw = sigma_w*(1 - j)/np.sqrt(omega)
|
| 117 |
+
|
| 118 |
+
# Admittances
|
| 119 |
+
Y_Rct = 1/Rct
|
| 120 |
+
Y_Cdl = j * omega * Cdl
|
| 121 |
+
Y_W = 1/Zw
|
| 122 |
+
|
| 123 |
+
Y_par = Y_Rct + Y_Cdl + Y_W
|
| 124 |
+
Z_parallel= 1/Y_par
|
| 125 |
+
Z_total = Rs + Z_parallel
|
| 126 |
+
|
| 127 |
+
# Return real & imag parts separately
|
| 128 |
+
return {
|
| 129 |
+
"frequencies": freqs.tolist(),
|
| 130 |
+
"Z_real": np.real(Z_total).tolist(),
|
| 131 |
+
"Z_imag": np.imag(Z_total).tolist()
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
def compute_d2QdV2(
|
| 135 |
+
V: List[float],
|
| 136 |
+
Q: List[float],
|
| 137 |
+
window: int = 21,
|
| 138 |
+
poly: int = 3
|
| 139 |
+
) -> Dict[str, List[float]]:
|
| 140 |
+
V = np.array(V, dtype=float)
|
| 141 |
+
Q = np.array(Q, dtype=float)
|
| 142 |
+
|
| 143 |
+
# Ensure monotonic V
|
| 144 |
+
if not np.all(np.diff(V) > 0):
|
| 145 |
+
idx = np.argsort(V)
|
| 146 |
+
V = V[idx]
|
| 147 |
+
Q = Q[idx]
|
| 148 |
+
|
| 149 |
+
# Smooth and differentiate
|
| 150 |
+
Qs = savgol_filter(Q, window_length=window, polyorder=poly)
|
| 151 |
+
dQdV = np.gradient(Qs, V)
|
| 152 |
+
d2QdV2 = np.gradient(dQdV, V)
|
| 153 |
+
|
| 154 |
+
return {"dQdV": dQdV.tolist(), "d2QdV2": d2QdV2.tolist()}
|
| 155 |
+
|
| 156 |
+
def plot_cv(V, I):
|
| 157 |
+
fig, ax = plt.subplots()
|
| 158 |
+
ax.plot(V, I, color='blue')
|
| 159 |
+
ax.set_title("Cyclic Voltammetry")
|
| 160 |
+
ax.set_xlabel("Voltage (V)")
|
| 161 |
+
ax.set_ylabel("Current (A)")
|
| 162 |
+
ax.grid(True)
|
| 163 |
+
return fig_to_base64(fig)
|
| 164 |
+
|
| 165 |
+
def plot_eis(Z_real, Z_imag):
|
| 166 |
+
fig, ax = plt.subplots()
|
| 167 |
+
ax.plot(Z_real, -np.array(Z_imag), 'o-', color='green')
|
| 168 |
+
ax.set_title("Nyquist Plot (EIS)")
|
| 169 |
+
ax.set_xlabel("Z' (Ω)")
|
| 170 |
+
ax.set_ylabel("-Z'' (Ω)")
|
| 171 |
+
ax.grid(True)
|
| 172 |
+
return fig_to_base64(fig)
|
| 173 |
+
|
| 174 |
+
def plot_dqdv(V, dQdV, d2QdV2):
|
| 175 |
+
fig, ax = plt.subplots()
|
| 176 |
+
ax.plot(V, dQdV, label="dQ/dV", color='orange')
|
| 177 |
+
ax.plot(V, d2QdV2, label="d²Q/dV²", color='red')
|
| 178 |
+
ax.set_title("Q–V Derivatives")
|
| 179 |
+
ax.set_xlabel("Voltage (V)")
|
| 180 |
+
ax.set_ylabel("Derivative")
|
| 181 |
+
ax.legend()
|
| 182 |
+
ax.grid(True)
|
| 183 |
+
return fig_to_base64(fig)
|
| 184 |
+
|
| 185 |
+
def fig_to_base64(fig):
|
| 186 |
+
buf = io.BytesIO()
|
| 187 |
+
fig.savefig(buf, format="png", bbox_inches="tight")
|
| 188 |
+
buf.seek(0)
|
| 189 |
+
img_base64 = base64.b64encode(buf.read()).decode("utf-8")
|
| 190 |
+
plt.close(fig)
|
| 191 |
+
return img_base64
|
| 192 |
+
|
| 193 |
+
def image_to_part(base64_str: str) -> dict:
|
| 194 |
+
return {
|
| 195 |
+
"inline_data": {
|
| 196 |
+
"mime_type": "image/png",
|
| 197 |
+
"data": base64_str
|
| 198 |
+
}
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
def analyze_plots_with_gemini(cv_img: str, eis_img: str, qv_img: str, cathode_name: str, faiss_results: list) -> str:
|
| 202 |
+
prompt = (
|
| 203 |
+
f"""Remove the underscores and .pdf from the source_pdf field in the RAG section below and put it as References at the end of the explanation.
|
| 204 |
+
You are to explain the results of the calculations of a sodium-ion full cell battery using hard carbon and {cathode_name}. You are a RAG system that takes information only from the RAG section below.
|
| 205 |
+
And respond with extensive/long explanation in a scientific way and straight to the point without any additional text. Do not include opinions just explanations
|
| 206 |
+
of the results of the calculations.
|
| 207 |
+
Lastly shortly analyze the performance of the full cell battery.
|
| 208 |
+
|
| 209 |
+
These are three plots from sodium-ion battery electrochemical analysis.
|
| 210 |
+
Please summarize the main features observed in:
|
| 211 |
+
1. Cyclic Voltammetry (CV)
|
| 212 |
+
2. Electrochemical Impedance Spectroscopy (EIS)
|
| 213 |
+
3. Q–V and d²Q/dV² analysis
|
| 214 |
+
Include observations on redox peaks, charge transfer resistance, and plateau features.
|
| 215 |
+
|
| 216 |
+
Remove the underscores and .pdf from the source_pdf field in the RAG section below and put it as References at the end of the explanation. Do not include the PDF file name directly in the explanation.
|
| 217 |
+
At the end of the explanation, list the full source_pdf file names used as references.
|
| 218 |
+
RAG Section, use only the information from this section to explain the results:
|
| 219 |
+
{json.dumps(faiss_results, indent=2)}"""
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
try:
|
| 223 |
+
response = model.generate_content([
|
| 224 |
+
prompt,
|
| 225 |
+
image_to_part(cv_img),
|
| 226 |
+
image_to_part(eis_img),
|
| 227 |
+
image_to_part(qv_img),
|
| 228 |
+
])
|
| 229 |
+
|
| 230 |
+
if response.candidates:
|
| 231 |
+
parts = response.candidates[0].content.parts
|
| 232 |
+
text_output = " ".join(
|
| 233 |
+
p.text for p in parts if hasattr(p, "text") and p.text
|
| 234 |
+
)
|
| 235 |
+
return text_output.strip()
|
| 236 |
+
else:
|
| 237 |
+
return "Gemini returned no candidates."
|
| 238 |
+
|
| 239 |
+
except Exception as e:
|
| 240 |
+
return f"Error from Gemini API: {e}"
|
| 241 |
+
|
| 242 |
+
def calculate_all_c(cathode_name: str, input_data: Dict) -> Dict:
|
| 243 |
+
|
| 244 |
+
query_text = (
|
| 245 |
+
f"Sodium-ion battery with hard carbon anode and cathode {cathode_name}. "
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
faiss_results = query_faiss_index(query_text, top_k=5)
|
| 249 |
+
|
| 250 |
+
cv = calculate_cv(input_data)
|
| 251 |
+
eis = calculate_eis(input_data)
|
| 252 |
+
deriv = compute_d2QdV2(
|
| 253 |
+
V = input_data["V_qv"],
|
| 254 |
+
Q = input_data["Q_qv"],
|
| 255 |
+
window = input_data["window"],
|
| 256 |
+
poly = input_data["poly"]
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
cv_plot = plot_cv(cv["V"], cv["I"])
|
| 260 |
+
eis_plot = plot_eis(eis["Z_real"], eis["Z_imag"])
|
| 261 |
+
qv_plot = plot_dqdv(input_data["V_qv"], deriv["dQdV"], deriv["d2QdV2"])
|
| 262 |
+
|
| 263 |
+
summary = analyze_plots_with_gemini(cv_plot, eis_plot, qv_plot, cathode_name, faiss_results)
|
| 264 |
+
|
| 265 |
+
return {
|
| 266 |
+
"plots": {
|
| 267 |
+
"cv_plot": cv_plot,
|
| 268 |
+
"eis_plot": eis_plot,
|
| 269 |
+
"qv_plot": qv_plot,
|
| 270 |
+
},
|
| 271 |
+
"Gemini_Explanation": summary
|
| 272 |
+
}
|
app/logic/calc_d.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict, Tuple
|
| 2 |
+
import google.generativeai as genai
|
| 3 |
+
import json
|
| 4 |
+
import faiss
|
| 5 |
+
from openai import OpenAI
|
| 6 |
+
import numpy as np
|
| 7 |
+
import os
|
| 8 |
+
import pandas as pd
|
| 9 |
+
|
| 10 |
+
genai.configure(api_key="AIzaSyBwMSL341arzL_FxPzy_DvhDl4Jc46DlaY")
|
| 11 |
+
model = genai.GenerativeModel("gemini-2.5-pro")
|
| 12 |
+
|
| 13 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 14 |
+
csv_path = os.path.join(BASE_DIR, "..", "data", "CATHODES_DATASET.csv")
|
| 15 |
+
csv_path = os.path.abspath(csv_path)
|
| 16 |
+
|
| 17 |
+
# Load cathode dataset once (adjust path if needed)
|
| 18 |
+
df_base = pd.read_csv(csv_path)
|
| 19 |
+
|
| 20 |
+
def query_faiss_index(query_text,
|
| 21 |
+
faiss_index_path=None,
|
| 22 |
+
metadata_path=None,
|
| 23 |
+
openai_api_key="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",
|
| 24 |
+
top_k=5):
|
| 25 |
+
client = OpenAI(api_key=openai_api_key)
|
| 26 |
+
|
| 27 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 28 |
+
|
| 29 |
+
if faiss_index_path is None:
|
| 30 |
+
faiss_index_path = os.path.join(BASE_DIR, "../sentiment/faiss_index.idx")
|
| 31 |
+
if metadata_path is None:
|
| 32 |
+
metadata_path = os.path.join(BASE_DIR, "../sentiment/metadata.json")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
# Load index and metadata
|
| 36 |
+
index = faiss.read_index(faiss_index_path)
|
| 37 |
+
with open(metadata_path, "r", encoding="utf-8") as f:
|
| 38 |
+
metadata = json.load(f)
|
| 39 |
+
|
| 40 |
+
# Get embedding for query
|
| 41 |
+
response = client.embeddings.create(
|
| 42 |
+
input=query_text.lower(),
|
| 43 |
+
model="text-embedding-3-large"
|
| 44 |
+
)
|
| 45 |
+
query_embedding = response.data[0].embedding
|
| 46 |
+
query_embedding_np = np.array([query_embedding]).astype("float32")
|
| 47 |
+
faiss.normalize_L2(query_embedding_np)
|
| 48 |
+
|
| 49 |
+
# Search index
|
| 50 |
+
distances, indices = index.search(query_embedding_np, top_k)
|
| 51 |
+
results = []
|
| 52 |
+
for dist, idx in zip(distances[0], indices[0]):
|
| 53 |
+
meta = metadata[idx]
|
| 54 |
+
results.append({
|
| 55 |
+
"score": float(dist),
|
| 56 |
+
"source_pdf": meta["source_pdf"],
|
| 57 |
+
"page": meta["page"],
|
| 58 |
+
"chunk_index": meta["chunk_index"],
|
| 59 |
+
"text_snippet": meta["text"]
|
| 60 |
+
})
|
| 61 |
+
return results
|
| 62 |
+
|
| 63 |
+
def calculate_np_ratio(
|
| 64 |
+
Q_anode_raw: float,
|
| 65 |
+
m_anode: float,
|
| 66 |
+
SEI_loss_fraction: float,
|
| 67 |
+
Q_cathode_raw: float,
|
| 68 |
+
m_cathode: float,
|
| 69 |
+
vacancy_loss_fraction: float
|
| 70 |
+
) -> float:
|
| 71 |
+
"""
|
| 72 |
+
N/P = (Q_anode_raw * m_anode * (1 - SEI_loss)) /
|
| 73 |
+
(Q_cathode_raw * m_cathode * (1 - vacancy_loss))
|
| 74 |
+
"""
|
| 75 |
+
Q_anode_usable = Q_anode_raw * m_anode * (1 - SEI_loss_fraction)
|
| 76 |
+
Q_cathode_usable = Q_cathode_raw * m_cathode * (1 - vacancy_loss_fraction)
|
| 77 |
+
return Q_anode_usable / Q_cathode_usable
|
| 78 |
+
|
| 79 |
+
def recommended_mass_loading_areal(
|
| 80 |
+
Q_areal: float,
|
| 81 |
+
Q_anode: float,
|
| 82 |
+
Q_cathode: float,
|
| 83 |
+
NP_ratio: float
|
| 84 |
+
) -> Tuple[float, float]:
|
| 85 |
+
"""
|
| 86 |
+
Returns (m_cathode_mg_cm2, m_anode_mg_cm2)
|
| 87 |
+
"""
|
| 88 |
+
m_cathode = Q_areal / Q_cathode
|
| 89 |
+
m_anode = (Q_areal / Q_anode) * NP_ratio
|
| 90 |
+
# convert g/cm² → mg/cm²
|
| 91 |
+
return m_cathode * 1000, m_anode * 1000
|
| 92 |
+
|
| 93 |
+
def calculate_first_cycle_ce(
|
| 94 |
+
Q_charge_anode: float,
|
| 95 |
+
Q_discharge_anode: float,
|
| 96 |
+
Q_charge_cathode: float,
|
| 97 |
+
Q_discharge_cathode: float,
|
| 98 |
+
Q_charge_full: float,
|
| 99 |
+
Q_discharge_full: float
|
| 100 |
+
) -> Dict[str, float]:
|
| 101 |
+
ce_anode = (Q_discharge_anode / Q_charge_anode) * 100
|
| 102 |
+
ce_cathode= (Q_discharge_cathode / Q_charge_cathode) * 100
|
| 103 |
+
ce_full = (Q_discharge_full / Q_charge_full) * 100
|
| 104 |
+
return {
|
| 105 |
+
"CE_anode (%)": ce_anode,
|
| 106 |
+
"CE_cathode (%)": ce_cathode,
|
| 107 |
+
"CE_full_cell (%)": ce_full
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
def estimate_irreversible_na_loss(
|
| 111 |
+
Q_charge_full: float,
|
| 112 |
+
Q_discharge_full: float
|
| 113 |
+
) -> float:
|
| 114 |
+
"""
|
| 115 |
+
Irreversible Na⁺ loss per gram (mAh/g) on first cycle
|
| 116 |
+
"""
|
| 117 |
+
return Q_charge_full - Q_discharge_full
|
| 118 |
+
|
| 119 |
+
def generate_gemini_insight(input_data: Dict, results: Dict, faiss_results: list, cathode_name: str) -> str:
|
| 120 |
+
prompt = f"""Remove the underscores and .pdf from the source_pdf field in the RAG section below and put it as References at the end of the explanation.
|
| 121 |
+
You are to explain the results of the calculations of a sodium-ion full cell battery using hard carbon and {cathode_name}. You are a RAG system that takes information only from the RAG section below.
|
| 122 |
+
Respond with an EXTENSIVE/LONG EXPLANATIONS, scientific, and straight-to-the-point explanation without any additional opinions, explaining only the results of the calculations.
|
| 123 |
+
Lastly, briefly analyze the performance of the full cell battery.
|
| 124 |
+
|
| 125 |
+
### Input Data:
|
| 126 |
+
{json.dumps(input_data, indent=2)}
|
| 127 |
+
|
| 128 |
+
### Calculated Results:
|
| 129 |
+
{json.dumps(results, indent=2)}
|
| 130 |
+
|
| 131 |
+
### Formulas Used:
|
| 132 |
+
- N/P Ratio = (Q_anode_raw × m_anode × (1 − SEI_loss_fraction)) ÷ (Q_cathode_raw × m_cathode × (1 − vacancy_loss_fraction))\n"
|
| 133 |
+
- Mass Loading Areal (mg/cm²):\n"
|
| 134 |
+
- m_cathode = Q_areal ÷ Q_cathode
|
| 135 |
+
- m_anode = (Q_areal ÷ Q_anode) × N/P Ratio
|
| 136 |
+
- First Cycle Coulombic Efficiency (CE):
|
| 137 |
+
- CE_anode = (Q_discharge_anode ÷ Q_charge_anode) × 100
|
| 138 |
+
- CE_cathode = (Q_discharge_cathode ÷ Q_charge_cathode) × 100
|
| 139 |
+
- CE_full = (Q_discharge_full ÷ Q_charge_full) × 100
|
| 140 |
+
- Irreversible Na⁺ Loss = Q_charge_full − Q_discharge_full
|
| 141 |
+
|
| 142 |
+
Remove the underscores and .pdf from the source_pdf field in the RAG section below and put it as References at the end of the explanation.
|
| 143 |
+
Do not include the PDF file name directly in the explanation.
|
| 144 |
+
At the end of the explanation, list the full source_pdf file names used as references.
|
| 145 |
+
|
| 146 |
+
RAG Section, use only the information from this section to explain the results:
|
| 147 |
+
{json.dumps(faiss_results, indent=2)}
|
| 148 |
+
"""
|
| 149 |
+
try:
|
| 150 |
+
response = model.generate_content(prompt)
|
| 151 |
+
|
| 152 |
+
if response.candidates:
|
| 153 |
+
parts = response.candidates[0].content.parts
|
| 154 |
+
text_output = " ".join(
|
| 155 |
+
p.text for p in parts if hasattr(p, "text") and p.text
|
| 156 |
+
)
|
| 157 |
+
return text_output.strip()
|
| 158 |
+
else:
|
| 159 |
+
return "Gemini returned no candidates."
|
| 160 |
+
|
| 161 |
+
except Exception as e:
|
| 162 |
+
return f"Gemini Error: {str(e)}"
|
| 163 |
+
|
| 164 |
+
def calculate_all_d(cathode_name: str, input_data: Dict) -> Dict:
|
| 165 |
+
# 1) N/P ratio
|
| 166 |
+
np_ratio = calculate_np_ratio(
|
| 167 |
+
Q_anode_raw = input_data["Q_anode_raw"],
|
| 168 |
+
m_anode = input_data["m_anode"],
|
| 169 |
+
SEI_loss_fraction = input_data["SEI_loss_fraction"],
|
| 170 |
+
Q_cathode_raw = input_data["Q_cathode_raw"],
|
| 171 |
+
m_cathode = input_data["m_cathode"],
|
| 172 |
+
vacancy_loss_fraction = input_data["vacancy_loss_fraction"]
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
# 2) Recommended mass loading
|
| 176 |
+
m_cathode_mg_cm2, m_anode_mg_cm2 = recommended_mass_loading_areal(
|
| 177 |
+
Q_areal = input_data["Q_areal"],
|
| 178 |
+
Q_anode = input_data["Q_anode_raw"],
|
| 179 |
+
Q_cathode = input_data["Q_cathode_raw"],
|
| 180 |
+
NP_ratio = np_ratio
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
# 3) First‑cycle CE
|
| 184 |
+
ce = calculate_first_cycle_ce(
|
| 185 |
+
Q_charge_anode = input_data["Q_charge_anode"],
|
| 186 |
+
Q_discharge_anode = input_data["Q_discharge_anode"],
|
| 187 |
+
Q_charge_cathode = input_data["Q_charge_cathode"],
|
| 188 |
+
Q_discharge_cathode = input_data["Q_discharge_cathode"],
|
| 189 |
+
Q_charge_full = input_data["Q_charge_full"],
|
| 190 |
+
Q_discharge_full = input_data["Q_discharge_full"]
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
# 4) Irreversible Na⁺ loss
|
| 194 |
+
ir_loss = estimate_irreversible_na_loss(
|
| 195 |
+
Q_charge_full = input_data["Q_charge_full"],
|
| 196 |
+
Q_discharge_full = input_data["Q_discharge_full"]
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
results = {
|
| 201 |
+
"NP_ratio": np_ratio,
|
| 202 |
+
"m_cathode_mg_per_cm2": m_cathode_mg_cm2,
|
| 203 |
+
"m_anode_mg_per_cm2": m_anode_mg_cm2,
|
| 204 |
+
**ce,
|
| 205 |
+
"Irreversible_Na_loss (mAh/g)": ir_loss
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
query_text = (
|
| 209 |
+
f"Sodium-ion battery with hard carbon anode and cathode {cathode_name}. "
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
faiss_results = query_faiss_index(query_text, top_k=5)
|
| 213 |
+
|
| 214 |
+
# 5) Generate Gemini insight
|
| 215 |
+
gemini_summary = generate_gemini_insight(input_data, results, faiss_results, cathode_name)
|
| 216 |
+
|
| 217 |
+
return {
|
| 218 |
+
"results": results,
|
| 219 |
+
"Gemini_Explanation": gemini_summary
|
| 220 |
+
}
|
app/logic/calc_e.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import matplotlib.pyplot as plt
|
| 2 |
+
import io
|
| 3 |
+
import base64
|
| 4 |
+
from typing import Dict, List
|
| 5 |
+
import math
|
| 6 |
+
import google.generativeai as genai
|
| 7 |
+
import json
|
| 8 |
+
import faiss
|
| 9 |
+
from openai import OpenAI
|
| 10 |
+
import numpy as np
|
| 11 |
+
import os
|
| 12 |
+
import pandas as pd
|
| 13 |
+
|
| 14 |
+
genai.configure(api_key="AIzaSyBwMSL341arzL_FxPzy_DvhDl4Jc46DlaY")
|
| 15 |
+
model = genai.GenerativeModel("gemini-2.5-pro")
|
| 16 |
+
|
| 17 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 18 |
+
csv_path = os.path.join(BASE_DIR, "..", "data", "CATHODES_DATASET.csv")
|
| 19 |
+
csv_path = os.path.abspath(csv_path)
|
| 20 |
+
|
| 21 |
+
# Load cathode dataset once (adjust path if needed)
|
| 22 |
+
df_base = pd.read_csv(csv_path)
|
| 23 |
+
|
| 24 |
+
def query_faiss_index(query_text,
|
| 25 |
+
faiss_index_path=None,
|
| 26 |
+
metadata_path=None,
|
| 27 |
+
openai_api_key="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",
|
| 28 |
+
top_k=5):
|
| 29 |
+
client = OpenAI(api_key=openai_api_key)
|
| 30 |
+
|
| 31 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 32 |
+
|
| 33 |
+
if faiss_index_path is None:
|
| 34 |
+
faiss_index_path = os.path.join(BASE_DIR, "../sentiment/faiss_index.idx")
|
| 35 |
+
if metadata_path is None:
|
| 36 |
+
metadata_path = os.path.join(BASE_DIR, "../sentiment/metadata.json")
|
| 37 |
+
|
| 38 |
+
# Load index and metadata
|
| 39 |
+
index = faiss.read_index(faiss_index_path)
|
| 40 |
+
with open(metadata_path, "r", encoding="utf-8") as f:
|
| 41 |
+
metadata = json.load(f)
|
| 42 |
+
|
| 43 |
+
# Get embedding for query
|
| 44 |
+
response = client.embeddings.create(
|
| 45 |
+
input=query_text.lower(),
|
| 46 |
+
model="text-embedding-3-large"
|
| 47 |
+
)
|
| 48 |
+
query_embedding = response.data[0].embedding
|
| 49 |
+
query_embedding_np = np.array([query_embedding]).astype("float32")
|
| 50 |
+
faiss.normalize_L2(query_embedding_np)
|
| 51 |
+
|
| 52 |
+
# Search index
|
| 53 |
+
distances, indices = index.search(query_embedding_np, top_k)
|
| 54 |
+
results = []
|
| 55 |
+
for dist, idx in zip(distances[0], indices[0]):
|
| 56 |
+
meta = metadata[idx]
|
| 57 |
+
results.append({
|
| 58 |
+
"score": float(dist),
|
| 59 |
+
"source_pdf": meta["source_pdf"],
|
| 60 |
+
"page": meta["page"],
|
| 61 |
+
"chunk_index": meta["chunk_index"],
|
| 62 |
+
"text_snippet": meta["text"]
|
| 63 |
+
})
|
| 64 |
+
return results
|
| 65 |
+
|
| 66 |
+
def plot_capacity_fade(cycle_numbers: List[int], Q_discharge_list: List[float]) -> str:
|
| 67 |
+
# Creates a capacity‐fade plot and returns it as a base64‐encoded PNG.
|
| 68 |
+
plt.figure(figsize=(8, 5))
|
| 69 |
+
plt.plot(cycle_numbers, Q_discharge_list, marker='o', linestyle='-')
|
| 70 |
+
plt.title("Capacity-Fade Trajectory")
|
| 71 |
+
plt.xlabel("Cycle Number")
|
| 72 |
+
plt.ylabel("Discharge Capacity (mAh/g)")
|
| 73 |
+
plt.grid(True)
|
| 74 |
+
plt.tight_layout()
|
| 75 |
+
|
| 76 |
+
# Save figure to a PNG in memory
|
| 77 |
+
buf = io.BytesIO()
|
| 78 |
+
plt.savefig(buf, format='png')
|
| 79 |
+
plt.close()
|
| 80 |
+
buf.seek(0)
|
| 81 |
+
|
| 82 |
+
# Encode PNG to base64 for JSON transport
|
| 83 |
+
img_b64 = base64.b64encode(buf.read()).decode('utf-8')
|
| 84 |
+
return img_b64
|
| 85 |
+
|
| 86 |
+
def plot_impedance_growth(
|
| 87 |
+
cycle_numbers: List[int],
|
| 88 |
+
impedance_list: List[float],
|
| 89 |
+
parameter_name: str = "Rct"
|
| 90 |
+
) -> str:
|
| 91 |
+
# Creates impedance growth plot and returns a base64-encoded PNG.
|
| 92 |
+
|
| 93 |
+
plt.figure(figsize=(8, 5))
|
| 94 |
+
plt.plot(cycle_numbers, impedance_list, marker='s', linestyle='-', color='darkred')
|
| 95 |
+
plt.title(f"Impedance Growth Trajectory: {parameter_name} vs Cycle")
|
| 96 |
+
plt.xlabel("Cycle Number")
|
| 97 |
+
plt.ylabel(f"{parameter_name} (Ω)")
|
| 98 |
+
plt.grid(True)
|
| 99 |
+
plt.tight_layout()
|
| 100 |
+
|
| 101 |
+
buf = io.BytesIO()
|
| 102 |
+
plt.savefig(buf, format='png')
|
| 103 |
+
plt.close()
|
| 104 |
+
buf.seek(0)
|
| 105 |
+
|
| 106 |
+
return base64.b64encode(buf.read()).decode('utf-8')
|
| 107 |
+
|
| 108 |
+
def calculate_calendar_ageing(input_data: Dict) -> Dict:
|
| 109 |
+
T_C = input_data["temperature_C"]
|
| 110 |
+
SOC = input_data["SOC_fraction"]
|
| 111 |
+
t_hours = input_data["storage_time_hours"]
|
| 112 |
+
Q0 = input_data["initial_capacity_mAh"]
|
| 113 |
+
|
| 114 |
+
# Empirical constants
|
| 115 |
+
k = 1e-7
|
| 116 |
+
n = 0.6
|
| 117 |
+
Ea = 40000 # J/mol
|
| 118 |
+
R = 8.314 # J/mol·K
|
| 119 |
+
|
| 120 |
+
T_K = T_C + 273.15
|
| 121 |
+
arrh = math.exp(-Ea / (R * T_K))
|
| 122 |
+
|
| 123 |
+
delta_Q = Q0 * k * (t_hours ** n) * arrh * (1 + 2 * (SOC - 0.5) ** 2)
|
| 124 |
+
frac = delta_Q / Q0
|
| 125 |
+
|
| 126 |
+
return {"delta_Q_mAh": delta_Q, "fractional_capacity_loss": frac}
|
| 127 |
+
|
| 128 |
+
def estimate_cycle_life_80(k: float, b: float) -> float:
|
| 129 |
+
if k <= 0 or b <= 0:
|
| 130 |
+
raise ValueError("k and b must be positive")
|
| 131 |
+
return (0.2 / k) ** (1 / b)
|
| 132 |
+
|
| 133 |
+
def image_to_part(base64_str: str) -> dict:
|
| 134 |
+
return {
|
| 135 |
+
"inline_data": {
|
| 136 |
+
"mime_type": "image/png",
|
| 137 |
+
"data": base64_str
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
def extract_gemini_text(response) -> str:
|
| 142 |
+
if not response.candidates:
|
| 143 |
+
return "Gemini returned no candidates."
|
| 144 |
+
parts = response.candidates[0].content.parts
|
| 145 |
+
texts = []
|
| 146 |
+
for p in parts:
|
| 147 |
+
if hasattr(p, "text"):
|
| 148 |
+
if isinstance(p.text, str):
|
| 149 |
+
texts.append(p.text)
|
| 150 |
+
elif hasattr(p.text, "text"):
|
| 151 |
+
texts.append(p.text.text)
|
| 152 |
+
return " ".join(texts).strip()
|
| 153 |
+
|
| 154 |
+
def analyze_ageing_with_gemini(
|
| 155 |
+
input_data: Dict,
|
| 156 |
+
results: Dict,
|
| 157 |
+
fade_img_b64: str,
|
| 158 |
+
imp_img_b64: str,
|
| 159 |
+
cathode_name: str,
|
| 160 |
+
faiss_results: List[Dict]
|
| 161 |
+
) -> str:
|
| 162 |
+
prompt = prompt = f"""
|
| 163 |
+
Remove the underscores and .pdf from the source_pdf field in the RAG section below and put it as References at the end of the explanation.
|
| 164 |
+
You are to explain the results of the calculations of a sodium-ion full cell battery using hard carbon and {cathode_name}. You are a RAG system that takes information only from the RAG section below.
|
| 165 |
+
Respond with extensive/long explanation in a scientific way and straight to the point without any additional text. Do not include opinions, just explanations of the results.
|
| 166 |
+
|
| 167 |
+
Lastly, briefly analyze the performance of the full cell battery.
|
| 168 |
+
Explain the results and plots provided, sticking strictly to scientific explanation.
|
| 169 |
+
Focus on capacity fade, impedance growth, calendar ageing, and cycle life estimation.
|
| 170 |
+
|
| 171 |
+
### Input Data and Numeric Results:
|
| 172 |
+
{json.dumps({'input_data': input_data, 'results': results}, indent=2)}
|
| 173 |
+
|
| 174 |
+
### Plots:
|
| 175 |
+
1. Capacity-Fade Trajectory
|
| 176 |
+
2. Impedance Growth Trajectory
|
| 177 |
+
|
| 178 |
+
Remove the underscores and .pdf from the source_pdf field in the RAG section below and put it as References at the end of the explanation.
|
| 179 |
+
Do not include the PDF file name directly in the explanation.
|
| 180 |
+
At the end of the explanation, list the full source_pdf file names used as references.
|
| 181 |
+
|
| 182 |
+
RAG Section (use only this information to explain the results):
|
| 183 |
+
{json.dumps(faiss_results, indent=2)}
|
| 184 |
+
"""
|
| 185 |
+
|
| 186 |
+
response = model.generate_content([
|
| 187 |
+
prompt,
|
| 188 |
+
image_to_part(fade_img_b64),
|
| 189 |
+
image_to_part(imp_img_b64),
|
| 190 |
+
])
|
| 191 |
+
|
| 192 |
+
return extract_gemini_text(response)
|
| 193 |
+
|
| 194 |
+
def calculate_all_e(cathode_name: str, input_data: Dict) -> Dict:
|
| 195 |
+
# Simply produce the plot
|
| 196 |
+
fade_img = plot_capacity_fade(
|
| 197 |
+
cycle_numbers = input_data["cycle_numbers"],
|
| 198 |
+
Q_discharge_list= input_data["Q_discharge_list"]
|
| 199 |
+
)
|
| 200 |
+
# Impedance growth plot
|
| 201 |
+
imp_img = plot_impedance_growth(
|
| 202 |
+
cycle_numbers = input_data["cycle_numbers_imp"],
|
| 203 |
+
impedance_list= input_data["impedance_list"],
|
| 204 |
+
parameter_name= input_data.get("parameter_name", "Rct")
|
| 205 |
+
)
|
| 206 |
+
cal = calculate_calendar_ageing(input_data)
|
| 207 |
+
N80 = estimate_cycle_life_80(
|
| 208 |
+
k=input_data["k_fade"],
|
| 209 |
+
b=input_data["b_fade"]
|
| 210 |
+
)
|
| 211 |
+
results = {
|
| 212 |
+
"delta_Q_mAh": cal["delta_Q_mAh"],
|
| 213 |
+
"fractional_capacity_loss": cal["fractional_capacity_loss"],
|
| 214 |
+
"cycle_life_80": N80
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
query_text = (
|
| 218 |
+
f"Sodium-ion battery with hard carbon anode and cathode {cathode_name}. "
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
faiss_results = query_faiss_index(query_text, top_k=5)
|
| 222 |
+
|
| 223 |
+
gemini_summary = analyze_ageing_with_gemini(
|
| 224 |
+
input_data=input_data,
|
| 225 |
+
results=results,
|
| 226 |
+
fade_img_b64=fade_img,
|
| 227 |
+
imp_img_b64=imp_img,
|
| 228 |
+
cathode_name=cathode_name,
|
| 229 |
+
faiss_results=faiss_results
|
| 230 |
+
)
|
| 231 |
+
return {
|
| 232 |
+
**results,
|
| 233 |
+
"capacity_fade_png": fade_img,
|
| 234 |
+
"impedance_growth_png": imp_img,
|
| 235 |
+
"Gemini_Explanation": gemini_summary
|
| 236 |
+
}
|
app/main.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI
|
| 2 |
+
from .routes import route_a
|
| 3 |
+
from .routes import route_b
|
| 4 |
+
from .routes import route_c
|
| 5 |
+
from .routes import route_d
|
| 6 |
+
from .routes import route_e
|
| 7 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 8 |
+
|
| 9 |
+
app = FastAPI(title="Battery Simulation API", version="1.0")
|
| 10 |
+
|
| 11 |
+
app.add_middleware(
|
| 12 |
+
CORSMiddleware,
|
| 13 |
+
allow_origins=["*"],
|
| 14 |
+
allow_credentials=True,
|
| 15 |
+
allow_methods=["*"],
|
| 16 |
+
allow_headers=["*"],
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
# Register each route group
|
| 20 |
+
app.include_router(route_a.router, tags=["Core Steady-State Cell Specs (A)"])
|
| 21 |
+
app.include_router(route_b.router, prefix="/b", tags=["B: Dynamic performance curves"])
|
| 22 |
+
app.include_router(route_c.router, prefix="/c", tags=["C: Electrochemical signatures"])
|
| 23 |
+
app.include_router(route_d.router, prefix="/d", tags=["D: Balancing & first-cycle losses"])
|
| 24 |
+
app.include_router(route_e.router, prefix="/e", tags=["E: Ageing & durability forecasts"])
|
app/models/__init__.py
ADDED
|
File without changes
|
app/models/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (163 Bytes). View file
|
|
|
app/models/__pycache__/model_a.cpython-311.pyc
ADDED
|
Binary file (675 Bytes). View file
|
|
|
app/models/__pycache__/model_b.cpython-311.pyc
ADDED
|
Binary file (2.55 kB). View file
|
|
|
app/models/__pycache__/model_c.cpython-311.pyc
ADDED
|
Binary file (2.45 kB). View file
|
|
|
app/models/__pycache__/model_d.cpython-311.pyc
ADDED
|
Binary file (2.23 kB). View file
|
|
|
app/models/__pycache__/model_e.cpython-311.pyc
ADDED
|
Binary file (2.11 kB). View file
|
|
|
app/models/model_a.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel
|
| 2 |
+
|
| 3 |
+
class MassLoadingInput(BaseModel):
|
| 4 |
+
cathode_name: str # Default value for cathode name
|
| 5 |
+
L_anode: float # g/cm²
|
| 6 |
+
L_cathode: float # g/cm²
|
| 7 |
+
M_total: float # g
|
| 8 |
+
t_total: float # cm
|
| 9 |
+
C_rate: float
|
app/models/model_b.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List
|
| 2 |
+
from pydantic import BaseModel, Field
|
| 3 |
+
|
| 4 |
+
class AllBInput(BaseModel):
|
| 5 |
+
cathode_name: str
|
| 6 |
+
# ─── V–Q & dQ/dV inputs ───────────────────────────────────────
|
| 7 |
+
V: List[float] = Field(..., description="Measured voltage at each time step (V)")
|
| 8 |
+
I: float = Field(..., description="Applied current (mA/g)")
|
| 9 |
+
t: List[float] = Field(..., description="Time array matching V (h or s)")
|
| 10 |
+
|
| 11 |
+
# ─── Rate capability inputs ───────────────────────────────────
|
| 12 |
+
Q_nominal: float = Field(..., description="Nominal low‑rate capacity (mAh/g)")
|
| 13 |
+
C_rates: List[float] = Field(..., description="List of C‑rates (e.g. [0.1,0.5,1,2,5])")
|
| 14 |
+
t_discharge: List[float] = Field(..., description="Discharge times at each C‑rate (hours)")
|
| 15 |
+
|
| 16 |
+
# ─── CC–CV charge time inputs ────────────────────────────────
|
| 17 |
+
Q_nominal_mAh: float = Field(..., description="Nominal cell capacity (mAh or Ah)")
|
| 18 |
+
I_lim: float = Field(..., description="Constant current limit during CC (A or mA)")
|
| 19 |
+
alpha: float = Field(..., description="Fraction of total capacity charged at CC (0.0–1.0)")
|
| 20 |
+
I_end: float = Field(..., description="End‐of‐charge current for CV termination (A or mA)")
|
| 21 |
+
tau: float = Field(..., description="Time constant for CV current decay (seconds)")
|
| 22 |
+
|
| 23 |
+
# ─── GITT‑style diffusion inputs ───────────────────────────────
|
| 24 |
+
L: float = Field(..., description="Diffusion length (cm)")
|
| 25 |
+
tau_pulse: float = Field(..., description="Pulse duration τ (s)")
|
| 26 |
+
delta_E_tau: List[float] = Field(..., description="Voltage change during pulse ΔEτ (V)")
|
| 27 |
+
delta_E_s: List[float] = Field(..., description="Steady‑state voltage change ΔEs (V)")
|
| 28 |
+
|
| 29 |
+
|
app/models/model_c.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field
|
| 2 |
+
from typing import List
|
| 3 |
+
|
| 4 |
+
class CVInput(BaseModel):
|
| 5 |
+
cathode_name: str
|
| 6 |
+
V_start: float = Field(..., description="Starting voltage (V)")
|
| 7 |
+
V_switch: float = Field(..., description="Switch voltage (V)")
|
| 8 |
+
scan_rate: float = Field(..., description="Sweep rate (V/s)")
|
| 9 |
+
dt: float = Field(..., description="Time step (s)")
|
| 10 |
+
sigma: float = Field(..., description="Gaussian peak width (V)")
|
| 11 |
+
E0: float = Field(..., description="Redox mid‑potential (V)")
|
| 12 |
+
Ip: float = Field(..., description="Peak current magnitude (A)")
|
| 13 |
+
|
| 14 |
+
# EIS inputs
|
| 15 |
+
frequencies: List[float] = Field(..., description="Frequencies (Hz)")
|
| 16 |
+
Rs: float = Field(..., description="Solution resistance (Ohm)")
|
| 17 |
+
Rct: float = Field(..., description="Charge‐transfer resistance (Ohm)")
|
| 18 |
+
Cdl: float = Field(..., description="Double‐layer capacitance (F)")
|
| 19 |
+
sigma_w: float = Field(..., description="Warburg coefficient (Ω·s^(-1/2))")
|
| 20 |
+
|
| 21 |
+
# Q–V derivative inputs
|
| 22 |
+
V_qv: List[float] = Field(..., description="Voltage array for Q–V curve (monotonic)")
|
| 23 |
+
Q_qv: List[float] = Field(..., description="Capacity array aligned with V_qv")
|
| 24 |
+
window: int = Field(21, description="Savitzky–Golay window length (odd)")
|
| 25 |
+
poly: int = Field(3, description="Savitzky–Golay polynomial order")
|
| 26 |
+
|
| 27 |
+
|
app/models/model_d.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field
|
| 2 |
+
|
| 3 |
+
class NPCalcInput(BaseModel):
|
| 4 |
+
cathode_name: str
|
| 5 |
+
Q_anode_raw: float = Field(..., description="Specific capacity of anode (mAh/g)")
|
| 6 |
+
m_anode: float = Field(..., description="Anode active mass (g or mg/cm²)")
|
| 7 |
+
SEI_loss_fraction: float = Field(..., description="Fractional SEI loss (e.g. 0.1 for 10%)")
|
| 8 |
+
Q_cathode_raw: float = Field(..., description="Specific capacity of cathode (mAh/g)")
|
| 9 |
+
m_cathode: float = Field(..., description="Cathode active mass (g or mg/cm²)")
|
| 10 |
+
vacancy_loss_fraction: float = Field(..., description="Fractional vacancy loss in cathode")
|
| 11 |
+
|
| 12 |
+
# recommended loading
|
| 13 |
+
Q_areal: float = Field(..., description="Target full‑cell areal capacity (mAh/cm²)")
|
| 14 |
+
|
| 15 |
+
# — new CE inputs —
|
| 16 |
+
Q_charge_anode: float = Field(..., description="Anode charge capacity (mAh)")
|
| 17 |
+
Q_discharge_anode: float = Field(..., description="Anode discharge capacity (mAh)")
|
| 18 |
+
Q_charge_cathode: float = Field(..., description="Cathode charge capacity (mAh)")
|
| 19 |
+
Q_discharge_cathode: float = Field(..., description="Cathode discharge capacity (mAh)")
|
| 20 |
+
Q_charge_full: float = Field(..., description="Full‑cell charge capacity (mAh)")
|
| 21 |
+
Q_discharge_full: float = Field(..., description="Full‑cell discharge capacity (mAh)")
|
app/models/model_e.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List
|
| 2 |
+
from pydantic import BaseModel, Field
|
| 3 |
+
|
| 4 |
+
class CapacityFadeInput(BaseModel):
|
| 5 |
+
cathode_name: str
|
| 6 |
+
cycle_numbers: List[int] = Field(..., description="List of cycle numbers")
|
| 7 |
+
Q_discharge_list: List[float] = Field(..., description="List of discharge capacities (mAh/g)")
|
| 8 |
+
|
| 9 |
+
# New impedance growth inputs:
|
| 10 |
+
cycle_numbers_imp: List[int] = Field(..., description="List of cycle numbers for impedance growth plot")
|
| 11 |
+
impedance_list: List[float] = Field(..., description="List of impedance values (Ω)")
|
| 12 |
+
parameter_name: str = Field("Rct", description="Parameter name for impedance (e.g., 'Rct')")
|
| 13 |
+
|
| 14 |
+
# Calendar ageing inputs
|
| 15 |
+
temperature_C: float = Field(..., description="Storage temperature in °C")
|
| 16 |
+
SOC_fraction: float = Field(..., description="State of charge (0–1)")
|
| 17 |
+
storage_time_hours: float = Field(..., description="Storage time in hours")
|
| 18 |
+
initial_capacity_mAh: float = Field(..., description="Initial full‑cell capacity (mAh)")
|
| 19 |
+
|
| 20 |
+
k_fade: float = Field(..., description="Capacity fade constant k for cycle-life estimation")
|
| 21 |
+
b_fade: float = Field(..., description="Exponent b for cycle-life estimation (0.3–0.7)")
|
app/routes/__init__.py
ADDED
|
File without changes
|
app/routes/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (163 Bytes). View file
|
|
|
app/routes/__pycache__/route_a.cpython-311.pyc
ADDED
|
Binary file (929 Bytes). View file
|
|
|
app/routes/__pycache__/route_b.cpython-311.pyc
ADDED
|
Binary file (1.5 kB). View file
|
|
|
app/routes/__pycache__/route_c.cpython-311.pyc
ADDED
|
Binary file (1.03 kB). View file
|
|
|
app/routes/__pycache__/route_d.cpython-311.pyc
ADDED
|
Binary file (780 Bytes). View file
|
|
|
app/routes/__pycache__/route_e.cpython-311.pyc
ADDED
|
Binary file (1.71 kB). View file
|
|
|
app/routes/route_a.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter
|
| 2 |
+
from ..models.model_a import MassLoadingInput
|
| 3 |
+
from ..logic.calc_a import calculate_capacity
|
| 4 |
+
|
| 5 |
+
router = APIRouter()
|
| 6 |
+
|
| 7 |
+
@router.post("/calculate/capacity")
|
| 8 |
+
def calculate(input: MassLoadingInput):
|
| 9 |
+
return {"results": calculate_capacity(
|
| 10 |
+
cathode_name=input.cathode_name,
|
| 11 |
+
L_anode = input.L_anode,
|
| 12 |
+
L_cathode = input.L_cathode,
|
| 13 |
+
M_total = input.M_total,
|
| 14 |
+
t_total = input.t_total,
|
| 15 |
+
C_rate = input.C_rate
|
| 16 |
+
)}
|
| 17 |
+
|
app/routes/route_b.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, HTTPException
|
| 2 |
+
from ..models.model_b import AllBInput
|
| 3 |
+
from ..logic.calc_b import calculate_all_b
|
| 4 |
+
|
| 5 |
+
router = APIRouter()
|
| 6 |
+
|
| 7 |
+
@router.post("/calculate/all")
|
| 8 |
+
def calculate_b(input: AllBInput):
|
| 9 |
+
cathode_name = input.cathode_name
|
| 10 |
+
# 1) V–Q / dQdV arrays
|
| 11 |
+
if len(input.V) != len(input.t):
|
| 12 |
+
raise HTTPException(400, "V and t must be same length")
|
| 13 |
+
|
| 14 |
+
# 2) Rate‑capability arrays
|
| 15 |
+
if len(input.C_rates) != len(input.t_discharge):
|
| 16 |
+
raise HTTPException(400, "C_rates and t_discharge must be same length")
|
| 17 |
+
|
| 18 |
+
# 3) GITT‑diffusion arrays
|
| 19 |
+
if len(input.delta_E_tau) != len(input.delta_E_s):
|
| 20 |
+
raise HTTPException(400, "delta_E_tau and delta_E_s must be same length")
|
| 21 |
+
|
| 22 |
+
# All validations passed, compute everything
|
| 23 |
+
return calculate_all_b(cathode_name, input.dict())
|
app/routes/route_c.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, HTTPException
|
| 2 |
+
from ..models.model_c import CVInput
|
| 3 |
+
from ..logic.calc_c import calculate_all_c
|
| 4 |
+
|
| 5 |
+
router = APIRouter()
|
| 6 |
+
|
| 7 |
+
@router.post("/calculate/c")
|
| 8 |
+
def calc_c(input: CVInput):
|
| 9 |
+
cathode_name = input.cathode_name
|
| 10 |
+
# Q–V match check
|
| 11 |
+
if len(input.V_qv) != len(input.Q_qv):
|
| 12 |
+
raise HTTPException(400, "V_qv and Q_qv must have the same length")
|
| 13 |
+
|
| 14 |
+
return calculate_all_c(cathode_name, input.dict())
|
app/routes/route_d.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter
|
| 2 |
+
from ..models.model_d import NPCalcInput
|
| 3 |
+
from ..logic.calc_d import calculate_all_d
|
| 4 |
+
|
| 5 |
+
router = APIRouter()
|
| 6 |
+
|
| 7 |
+
@router.post("/calculate/d")
|
| 8 |
+
def calc_d(input: NPCalcInput):
|
| 9 |
+
cathode_name = input.cathode_name
|
| 10 |
+
return calculate_all_d(cathode_name, input.dict())
|
app/routes/route_e.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, HTTPException
|
| 2 |
+
from ..models.model_e import CapacityFadeInput
|
| 3 |
+
from ..logic.calc_e import calculate_all_e
|
| 4 |
+
|
| 5 |
+
router = APIRouter()
|
| 6 |
+
|
| 7 |
+
@router.post("/calculate/e")
|
| 8 |
+
async def calc_e(input: CapacityFadeInput):
|
| 9 |
+
cathode_name=input.cathode_name
|
| 10 |
+
if len(input.cycle_numbers) != len(input.Q_discharge_list):
|
| 11 |
+
raise HTTPException(400, "cycle_numbers and Q_discharge_list must match length")
|
| 12 |
+
if len(input.cycle_numbers_imp) != len(input.impedance_list):
|
| 13 |
+
raise HTTPException(400, "cycle_numbers_imp and impedance_list must match length")
|
| 14 |
+
if not (0 <= input.SOC_fraction <= 1):
|
| 15 |
+
raise HTTPException(400, "SOC_fraction must be between 0 and 1")
|
| 16 |
+
if input.k_fade <= 0 or input.b_fade <= 0:
|
| 17 |
+
raise HTTPException(400, "k_fade and b_fade must be positive")
|
| 18 |
+
return calculate_all_e(cathode_name, input.dict())
|
app/sentiment/faiss_index.idx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8edf86598d5dd13cf1f4d960d6e785e6406d705d7522c2c41f99216769c486fe
|
| 3 |
+
size 19992621
|
app/sentiment/metadata.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
app/sentiment/process.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import fitz
|
| 3 |
+
import nltk
|
| 4 |
+
import json
|
| 5 |
+
import faiss
|
| 6 |
+
import numpy as np
|
| 7 |
+
from openai import OpenAI
|
| 8 |
+
import time
|
| 9 |
+
|
| 10 |
+
nltk.download("punkt")
|
| 11 |
+
|
| 12 |
+
PDF_FOLDER = "backend/app/sentiment/pds"
|
| 13 |
+
FAISS_INDEX_PATH = "backend/app/sentiment/faiss_index.idx"
|
| 14 |
+
METADATA_PATH = "backend/app/sentiment/metadata.json"
|
| 15 |
+
EMBEDDING_MODEL = "text-embedding-ada-002"
|
| 16 |
+
OVERLAP_TOKENS = 50
|
| 17 |
+
CHUNK_SIZE_TOKENS = 200
|
| 18 |
+
|
| 19 |
+
OPENAI_API_KEY='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'
|
| 20 |
+
client = OpenAI(api_key=OPENAI_API_KEY)
|
| 21 |
+
|
| 22 |
+
def extract_text_by_page(pdf_path):
|
| 23 |
+
"""Extract text from each page of PDF separately (for metadata)."""
|
| 24 |
+
doc = fitz.open(pdf_path)
|
| 25 |
+
pages_text = []
|
| 26 |
+
for page in doc:
|
| 27 |
+
text = page.get_text()
|
| 28 |
+
pages_text.append(text)
|
| 29 |
+
return pages_text
|
| 30 |
+
|
| 31 |
+
def tokenize_text(text):
|
| 32 |
+
"""Tokenize text into tokens using nltk sentence tokenizer + split."""
|
| 33 |
+
sentences = nltk.sent_tokenize(text)
|
| 34 |
+
tokens = []
|
| 35 |
+
for sentence in sentences:
|
| 36 |
+
tokens.extend(sentence.split())
|
| 37 |
+
return tokens
|
| 38 |
+
|
| 39 |
+
def detokenize_tokens(tokens):
|
| 40 |
+
"""Convert tokens back to text."""
|
| 41 |
+
return " ".join(tokens)
|
| 42 |
+
|
| 43 |
+
def chunk_tokens(tokens, chunk_size=CHUNK_SIZE_TOKENS, overlap=OVERLAP_TOKENS):
|
| 44 |
+
"""Chunk tokens into overlapping chunks."""
|
| 45 |
+
chunks = []
|
| 46 |
+
start = 0
|
| 47 |
+
while start < len(tokens):
|
| 48 |
+
end = start + chunk_size
|
| 49 |
+
chunk = tokens[start:end]
|
| 50 |
+
chunks.append(chunk)
|
| 51 |
+
if end >= len(tokens):
|
| 52 |
+
break
|
| 53 |
+
start = end - overlap
|
| 54 |
+
return chunks
|
| 55 |
+
|
| 56 |
+
def get_embedding(text):
|
| 57 |
+
response = client.embeddings.create(
|
| 58 |
+
input=text,
|
| 59 |
+
model="text-embedding-3-large"
|
| 60 |
+
)
|
| 61 |
+
return response.data[0].embedding
|
| 62 |
+
|
| 63 |
+
def build_index_and_save():
|
| 64 |
+
all_embeddings = []
|
| 65 |
+
metadata = []
|
| 66 |
+
|
| 67 |
+
print("Reading PDFs and chunking text...")
|
| 68 |
+
|
| 69 |
+
for filename in os.listdir(PDF_FOLDER):
|
| 70 |
+
if not filename.lower().endswith(".pdf"):
|
| 71 |
+
continue
|
| 72 |
+
pdf_path = os.path.join(PDF_FOLDER, filename)
|
| 73 |
+
print(f"Processing {filename}")
|
| 74 |
+
pages = extract_text_by_page(pdf_path)
|
| 75 |
+
for page_num, page_text in enumerate(pages):
|
| 76 |
+
page_text = page_text.lower().strip()
|
| 77 |
+
tokens = tokenize_text(page_text)
|
| 78 |
+
chunks_tokens = chunk_tokens(tokens)
|
| 79 |
+
for i, chunk_tokens_ in enumerate(chunks_tokens):
|
| 80 |
+
chunk_text = detokenize_tokens(chunk_tokens_)
|
| 81 |
+
chunk_text = chunk_text.lower()
|
| 82 |
+
|
| 83 |
+
embedding = get_embedding(chunk_text)
|
| 84 |
+
all_embeddings.append(embedding)
|
| 85 |
+
|
| 86 |
+
metadata.append({
|
| 87 |
+
"source_pdf": filename,
|
| 88 |
+
"page": page_num,
|
| 89 |
+
"chunk_index": i,
|
| 90 |
+
"text": chunk_text[:500]
|
| 91 |
+
})
|
| 92 |
+
|
| 93 |
+
if len(all_embeddings) % 50 == 0:
|
| 94 |
+
save_index_and_metadata(all_embeddings, metadata)
|
| 95 |
+
|
| 96 |
+
save_index_and_metadata(all_embeddings, metadata)
|
| 97 |
+
print("Index build completed.")
|
| 98 |
+
|
| 99 |
+
def save_index_and_metadata(embeddings, metadata):
|
| 100 |
+
dimension = len(embeddings[0])
|
| 101 |
+
print(f"Saving index with {len(embeddings)} vectors...")
|
| 102 |
+
embeddings_np = np.array(embeddings).astype("float32")
|
| 103 |
+
|
| 104 |
+
faiss.normalize_L2(embeddings_np)
|
| 105 |
+
|
| 106 |
+
index = faiss.IndexFlatIP(dimension)
|
| 107 |
+
index.add(embeddings_np)
|
| 108 |
+
faiss.write_index(index, FAISS_INDEX_PATH)
|
| 109 |
+
|
| 110 |
+
with open(METADATA_PATH, "w", encoding="utf-8") as f:
|
| 111 |
+
json.dump(metadata, f, ensure_ascii=False, indent=2)
|
| 112 |
+
|
| 113 |
+
def load_index_and_metadata():
|
| 114 |
+
index = faiss.read_index(FAISS_INDEX_PATH)
|
| 115 |
+
with open(METADATA_PATH, "r", encoding="utf-8") as f:
|
| 116 |
+
metadata = json.load(f)
|
| 117 |
+
return index, metadata
|
| 118 |
+
|
| 119 |
+
def query_index(query, top_k=5):
|
| 120 |
+
index, metadata = load_index_and_metadata()
|
| 121 |
+
query = query.lower()
|
| 122 |
+
query_embedding = get_embedding(query)
|
| 123 |
+
query_embedding_np = np.array([query_embedding]).astype("float32")
|
| 124 |
+
faiss.normalize_L2(query_embedding_np)
|
| 125 |
+
|
| 126 |
+
distances, indices = index.search(query_embedding_np, top_k)
|
| 127 |
+
results = []
|
| 128 |
+
for dist, idx in zip(distances[0], indices[0]):
|
| 129 |
+
meta = metadata[idx]
|
| 130 |
+
results.append({
|
| 131 |
+
"score": float(dist),
|
| 132 |
+
"source_pdf": meta["source_pdf"],
|
| 133 |
+
"page": meta["page"],
|
| 134 |
+
"chunk_index": meta["chunk_index"],
|
| 135 |
+
"text_snippet": meta["text"]
|
| 136 |
+
})
|
| 137 |
+
return results
|
| 138 |
+
|
| 139 |
+
if __name__ == "__main__":
|
| 140 |
+
build_index_and_save()
|
| 141 |
+
print("\nQuery results:")
|
requirements.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
google-generativeai
|
| 4 |
+
openai
|
| 5 |
+
faiss-cpu
|
| 6 |
+
numpy
|
| 7 |
+
scipy
|
| 8 |
+
matplotlib
|
| 9 |
+
pandas
|
| 10 |
+
base64io
|
| 11 |
+
requests
|
| 12 |
+
python-dotenv
|