|
|
from typing import Dict, Tuple |
|
|
import google.generativeai as genai |
|
|
import json |
|
|
import faiss |
|
|
from openai import OpenAI |
|
|
import numpy as np |
|
|
import os |
|
|
import pandas as pd |
|
|
|
|
|
genai.configure( |
|
|
api_key=os.environ.get("gemini-2.5-flash") |
|
|
) |
|
|
model = genai.GenerativeModel("gemini-2.5-pro") |
|
|
|
|
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__)) |
|
|
csv_path = os.path.join(BASE_DIR, "..", "data", "CATHODES_DATASET.csv") |
|
|
csv_path = os.path.abspath(csv_path) |
|
|
|
|
|
|
|
|
df_base = pd.read_csv(csv_path) |
|
|
|
|
|
def query_faiss_index(query_text, |
|
|
faiss_index_path=None, |
|
|
metadata_path=None, |
|
|
top_k=5): |
|
|
|
|
|
openai_api_key = os.getenv("OPENAI_API_KEY") |
|
|
if not openai_api_key: |
|
|
raise ValueError("OPENAI_API_KEY is not set") |
|
|
|
|
|
client = OpenAI(api_key=openai_api_key) |
|
|
|
|
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__)) |
|
|
|
|
|
if faiss_index_path is None: |
|
|
faiss_index_path = os.path.join(BASE_DIR, "../sentiment/faiss_index.idx") |
|
|
if metadata_path is None: |
|
|
metadata_path = os.path.join(BASE_DIR, "../sentiment/metadata.json") |
|
|
|
|
|
|
|
|
|
|
|
index = faiss.read_index(faiss_index_path) |
|
|
with open(metadata_path, "r", encoding="utf-8") as f: |
|
|
metadata = json.load(f) |
|
|
|
|
|
|
|
|
response = client.embeddings.create( |
|
|
input=query_text.lower(), |
|
|
model="text-embedding-3-large" |
|
|
) |
|
|
query_embedding = response.data[0].embedding |
|
|
query_embedding_np = np.array([query_embedding]).astype("float32") |
|
|
faiss.normalize_L2(query_embedding_np) |
|
|
|
|
|
|
|
|
distances, indices = index.search(query_embedding_np, top_k) |
|
|
results = [] |
|
|
for dist, idx in zip(distances[0], indices[0]): |
|
|
meta = metadata[idx] |
|
|
results.append({ |
|
|
"score": float(dist), |
|
|
"source_pdf": meta["source_pdf"], |
|
|
"page": meta["page"], |
|
|
"chunk_index": meta["chunk_index"], |
|
|
"text_snippet": meta["text"] |
|
|
}) |
|
|
return results |
|
|
|
|
|
def calculate_np_ratio( |
|
|
Q_anode_raw: float, |
|
|
m_anode: float, |
|
|
SEI_loss_fraction: float, |
|
|
Q_cathode_raw: float, |
|
|
m_cathode: float, |
|
|
vacancy_loss_fraction: float |
|
|
) -> float: |
|
|
""" |
|
|
N/P = (Q_anode_raw * m_anode * (1 - SEI_loss)) / |
|
|
(Q_cathode_raw * m_cathode * (1 - vacancy_loss)) |
|
|
""" |
|
|
Q_anode_usable = Q_anode_raw * m_anode * (1 - SEI_loss_fraction) |
|
|
Q_cathode_usable = Q_cathode_raw * m_cathode * (1 - vacancy_loss_fraction) |
|
|
return Q_anode_usable / Q_cathode_usable |
|
|
|
|
|
def recommended_mass_loading_areal( |
|
|
Q_areal: float, |
|
|
Q_anode: float, |
|
|
Q_cathode: float, |
|
|
NP_ratio: float |
|
|
) -> Tuple[float, float]: |
|
|
""" |
|
|
Returns (m_cathode_mg_cm2, m_anode_mg_cm2) |
|
|
""" |
|
|
m_cathode = Q_areal / Q_cathode |
|
|
m_anode = (Q_areal / Q_anode) * NP_ratio |
|
|
|
|
|
return m_cathode * 1000, m_anode * 1000 |
|
|
|
|
|
def calculate_first_cycle_ce( |
|
|
Q_charge_anode: float, |
|
|
Q_discharge_anode: float, |
|
|
Q_charge_cathode: float, |
|
|
Q_discharge_cathode: float, |
|
|
Q_charge_full: float, |
|
|
Q_discharge_full: float |
|
|
) -> Dict[str, float]: |
|
|
ce_anode = (Q_discharge_anode / Q_charge_anode) * 100 |
|
|
ce_cathode= (Q_discharge_cathode / Q_charge_cathode) * 100 |
|
|
ce_full = (Q_discharge_full / Q_charge_full) * 100 |
|
|
return { |
|
|
"CE_anode (%)": ce_anode, |
|
|
"CE_cathode (%)": ce_cathode, |
|
|
"CE_full_cell (%)": ce_full |
|
|
} |
|
|
|
|
|
def estimate_irreversible_na_loss( |
|
|
Q_charge_full: float, |
|
|
Q_discharge_full: float |
|
|
) -> float: |
|
|
""" |
|
|
Irreversible Na⁺ loss per gram (mAh/g) on first cycle |
|
|
""" |
|
|
return Q_charge_full - Q_discharge_full |
|
|
|
|
|
def generate_gemini_insight(input_data: Dict, results: Dict, faiss_results: list, cathode_name: str) -> str: |
|
|
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. |
|
|
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. |
|
|
Respond with an EXTENSIVE/LONG EXPLANATIONS, scientific, and straight-to-the-point explanation without any additional opinions, explaining only the results of the calculations. |
|
|
Lastly, briefly analyze the performance of the full cell battery. |
|
|
|
|
|
### Input Data: |
|
|
{json.dumps(input_data, indent=2)} |
|
|
|
|
|
### Calculated Results: |
|
|
{json.dumps(results, indent=2)} |
|
|
|
|
|
### Formulas Used: |
|
|
- N/P Ratio = (Q_anode_raw × m_anode × (1 − SEI_loss_fraction)) ÷ (Q_cathode_raw × m_cathode × (1 − vacancy_loss_fraction))\n" |
|
|
- Mass Loading Areal (mg/cm²):\n" |
|
|
- m_cathode = Q_areal ÷ Q_cathode |
|
|
- m_anode = (Q_areal ÷ Q_anode) × N/P Ratio |
|
|
- First Cycle Coulombic Efficiency (CE): |
|
|
- CE_anode = (Q_discharge_anode ÷ Q_charge_anode) × 100 |
|
|
- CE_cathode = (Q_discharge_cathode ÷ Q_charge_cathode) × 100 |
|
|
- CE_full = (Q_discharge_full ÷ Q_charge_full) × 100 |
|
|
- Irreversible Na⁺ Loss = Q_charge_full − Q_discharge_full |
|
|
|
|
|
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. |
|
|
At the end of the explanation, list the full source_pdf file names used as references. |
|
|
|
|
|
RAG Section, use only the information from this section to explain the results: |
|
|
{json.dumps(faiss_results, indent=2)} |
|
|
""" |
|
|
try: |
|
|
response = model.generate_content(prompt) |
|
|
|
|
|
if response.candidates: |
|
|
parts = response.candidates[0].content.parts |
|
|
text_output = " ".join( |
|
|
p.text for p in parts if hasattr(p, "text") and p.text |
|
|
) |
|
|
return text_output.strip() |
|
|
else: |
|
|
return "Gemini returned no candidates." |
|
|
|
|
|
except Exception as e: |
|
|
return f"Gemini Error: {str(e)}" |
|
|
|
|
|
def calculate_all_d(cathode_name: str, input_data: Dict) -> Dict: |
|
|
|
|
|
np_ratio = calculate_np_ratio( |
|
|
Q_anode_raw = input_data["Q_anode_raw"], |
|
|
m_anode = input_data["m_anode"], |
|
|
SEI_loss_fraction = input_data["SEI_loss_fraction"], |
|
|
Q_cathode_raw = input_data["Q_cathode_raw"], |
|
|
m_cathode = input_data["m_cathode"], |
|
|
vacancy_loss_fraction = input_data["vacancy_loss_fraction"] |
|
|
) |
|
|
|
|
|
|
|
|
m_cathode_mg_cm2, m_anode_mg_cm2 = recommended_mass_loading_areal( |
|
|
Q_areal = input_data["Q_areal"], |
|
|
Q_anode = input_data["Q_anode_raw"], |
|
|
Q_cathode = input_data["Q_cathode_raw"], |
|
|
NP_ratio = np_ratio |
|
|
) |
|
|
|
|
|
|
|
|
ce = calculate_first_cycle_ce( |
|
|
Q_charge_anode = input_data["Q_charge_anode"], |
|
|
Q_discharge_anode = input_data["Q_discharge_anode"], |
|
|
Q_charge_cathode = input_data["Q_charge_cathode"], |
|
|
Q_discharge_cathode = input_data["Q_discharge_cathode"], |
|
|
Q_charge_full = input_data["Q_charge_full"], |
|
|
Q_discharge_full = input_data["Q_discharge_full"] |
|
|
) |
|
|
|
|
|
|
|
|
ir_loss = estimate_irreversible_na_loss( |
|
|
Q_charge_full = input_data["Q_charge_full"], |
|
|
Q_discharge_full = input_data["Q_discharge_full"] |
|
|
) |
|
|
|
|
|
|
|
|
results = { |
|
|
"NP_ratio": np_ratio, |
|
|
"m_cathode_mg_per_cm2": m_cathode_mg_cm2, |
|
|
"m_anode_mg_per_cm2": m_anode_mg_cm2, |
|
|
**ce, |
|
|
"Irreversible_Na_loss (mAh/g)": ir_loss |
|
|
} |
|
|
|
|
|
query_text = ( |
|
|
f"Sodium-ion battery with hard carbon anode and cathode {cathode_name}. " |
|
|
) |
|
|
|
|
|
faiss_results = query_faiss_index(query_text, top_k=5) |
|
|
|
|
|
|
|
|
gemini_summary = generate_gemini_insight(input_data, results, faiss_results, cathode_name) |
|
|
|
|
|
return { |
|
|
"results": results, |
|
|
"Gemini_Explanation": gemini_summary |
|
|
} |
|
|
|