tueniuu commited on
Commit
78fefe8
·
verified ·
1 Parent(s): fb68a45

Upload 48 files

Browse files
Files changed (49) hide show
  1. .gitattributes +1 -0
  2. Dockerfile +17 -0
  3. Procfile +1 -0
  4. __init__.py +1 -0
  5. app/__init__.py +0 -0
  6. app/__pycache__/__init__.cpython-311.pyc +0 -0
  7. app/__pycache__/main.cpython-311.pyc +0 -0
  8. app/data/CATHODES_DATASET.csv +26 -0
  9. app/logic/__init__.py +0 -0
  10. app/logic/__pycache__/__init__.cpython-311.pyc +0 -0
  11. app/logic/__pycache__/calc_a.cpython-311.pyc +0 -0
  12. app/logic/__pycache__/calc_b.cpython-311.pyc +0 -0
  13. app/logic/__pycache__/calc_c.cpython-311.pyc +0 -0
  14. app/logic/__pycache__/calc_d.cpython-311.pyc +0 -0
  15. app/logic/__pycache__/calc_e.cpython-311.pyc +0 -0
  16. app/logic/calc_a.py +218 -0
  17. app/logic/calc_b.py +206 -0
  18. app/logic/calc_c.py +272 -0
  19. app/logic/calc_d.py +220 -0
  20. app/logic/calc_e.py +236 -0
  21. app/main.py +24 -0
  22. app/models/__init__.py +0 -0
  23. app/models/__pycache__/__init__.cpython-311.pyc +0 -0
  24. app/models/__pycache__/model_a.cpython-311.pyc +0 -0
  25. app/models/__pycache__/model_b.cpython-311.pyc +0 -0
  26. app/models/__pycache__/model_c.cpython-311.pyc +0 -0
  27. app/models/__pycache__/model_d.cpython-311.pyc +0 -0
  28. app/models/__pycache__/model_e.cpython-311.pyc +0 -0
  29. app/models/model_a.py +9 -0
  30. app/models/model_b.py +29 -0
  31. app/models/model_c.py +27 -0
  32. app/models/model_d.py +21 -0
  33. app/models/model_e.py +21 -0
  34. app/routes/__init__.py +0 -0
  35. app/routes/__pycache__/__init__.cpython-311.pyc +0 -0
  36. app/routes/__pycache__/route_a.cpython-311.pyc +0 -0
  37. app/routes/__pycache__/route_b.cpython-311.pyc +0 -0
  38. app/routes/__pycache__/route_c.cpython-311.pyc +0 -0
  39. app/routes/__pycache__/route_d.cpython-311.pyc +0 -0
  40. app/routes/__pycache__/route_e.cpython-311.pyc +0 -0
  41. app/routes/route_a.py +17 -0
  42. app/routes/route_b.py +23 -0
  43. app/routes/route_c.py +14 -0
  44. app/routes/route_d.py +10 -0
  45. app/routes/route_e.py +18 -0
  46. app/sentiment/faiss_index.idx +3 -0
  47. app/sentiment/metadata.json +0 -0
  48. app/sentiment/process.py +141 -0
  49. 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