|
|
import pandas as pd |
|
|
import google.generativeai as genai |
|
|
import os |
|
|
import faiss |
|
|
from openai import OpenAI |
|
|
import json |
|
|
import numpy as np |
|
|
import logging |
|
|
|
|
|
logging.basicConfig(level=logging.INFO) |
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
|
|
|
V_ANODE_MIN = 0.01 |
|
|
V_ANODE_MAX = 2.0 |
|
|
V_ANODE_MID = 0.1 |
|
|
C_SPEC_ANODE = 300 |
|
|
|
|
|
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) |
|
|
|
|
|
genai.configure( |
|
|
api_key=os.environ.get("GOOGLE_API_KEY") |
|
|
) |
|
|
model = genai.GenerativeModel("gemini-2.5-flash") |
|
|
|
|
|
|
|
|
df_base = df_base.rename(columns={ |
|
|
"Practical Capacity (mAh/g)": "C_spec_cathode", |
|
|
"Voltage Window Lower (V)": "V_cathode_min", |
|
|
"Voltage Window Upper (V)": "V_cathode_max", |
|
|
"plateau voltage discharge": "V_cathode_mid" |
|
|
}) |
|
|
|
|
|
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"] |
|
|
}) |
|
|
|
|
|
logger.info(f"Query: {query_text}") |
|
|
logger.info(f"Embedding length: {len(query_embedding)}") |
|
|
logger.info(f"Index dimension: {index.d}") |
|
|
logger.info(f"Index contains: {index.ntotal} vectors") |
|
|
logger.info(f"Distances: {distances}") |
|
|
logger.info(f"Indices: {indices}") |
|
|
logger.info(f"Metadata size: {len(metadata)}") |
|
|
|
|
|
return results |
|
|
|
|
|
def calculate_capacity(cathode_name: str, L_anode: float, L_cathode: float, |
|
|
M_total: float, t_total: float, C_rate: float): |
|
|
df = df_base.copy() |
|
|
df = df[df["Material_Name"] == cathode_name] |
|
|
|
|
|
if df.empty: |
|
|
raise ValueError(f"Cathode '{cathode_name}' not found in dataset.") |
|
|
|
|
|
df["C_spec_anode"] = C_SPEC_ANODE |
|
|
df["L_anode"] = L_anode |
|
|
df["L_cathode"] = L_cathode |
|
|
|
|
|
|
|
|
df["Q_anode"] = C_SPEC_ANODE * L_anode / 1000 |
|
|
df["Q_cathode"] = df["C_spec_cathode"] * L_cathode / 1000 |
|
|
df["Q_full"] = df[["Q_anode", "Q_cathode"]].min(axis=1) |
|
|
|
|
|
|
|
|
df["Areal_Capacity_cell"] = df["Q_full"] |
|
|
df["Specific_Capacity_cell"] = df["Q_full"] / ((L_anode + L_cathode) / 1000) |
|
|
|
|
|
|
|
|
df["V_nominal"] = df["V_cathode_mid"] - V_ANODE_MID |
|
|
|
|
|
|
|
|
df["Gravimetric_Energy_Density"] = (1000 * df["Q_full"] * df["V_nominal"]) / M_total |
|
|
df["Volumetric_Energy_Density"] = (df["Q_full"] * df["V_nominal"] * 1000) / t_total |
|
|
|
|
|
|
|
|
df["V_cell_max"] = df["V_cathode_max"] - V_ANODE_MIN |
|
|
df["V_cell_min"] = df["V_cathode_min"] - V_ANODE_MAX |
|
|
df["Voltage_Window"] = df["V_cell_max"] - df["V_cell_min"] |
|
|
|
|
|
|
|
|
df["Specific_Power"] = df["Gravimetric_Energy_Density"] * C_rate |
|
|
|
|
|
result = { |
|
|
"Cathode": cathode_name, |
|
|
"Anode": "Hard Carbon", |
|
|
"Q_areal": float(df["Q_full"].values[0]), |
|
|
"Q_specific": float(df["Specific_Capacity_cell"].values[0]), |
|
|
"Gravimetric_Energy_Density": float(df["Gravimetric_Energy_Density"].values[0]), |
|
|
"Volumetric_Energy_Density": float(df["Volumetric_Energy_Density"].values[0]), |
|
|
"V_nominal": float(df["V_nominal"].values[0]), |
|
|
"Voltage_Window": float(df["Voltage_Window"].values[0]), |
|
|
"Specific_Power": float(df["Specific_Power"].values[0]) |
|
|
} |
|
|
|
|
|
query_text = ( |
|
|
f"Sodium-ion battery with hard carbon anode and cathode {cathode_name}. " |
|
|
) |
|
|
|
|
|
faiss_results = query_faiss_index(query_text, top_k=5) |
|
|
|
|
|
try: |
|
|
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. |
|
|
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 |
|
|
of the results of the calculations. |
|
|
Lastly shortly analyze the performance of the full cell battery. |
|
|
|
|
|
Below are the formulas used to compute key metrics. After reviewing them, interpret the calculated results: |
|
|
|
|
|
**Fixed Anode Parameters:** |
|
|
- Specific Capacity of Anode (C_SPEC_ANODE): 300 mAh/g |
|
|
- Voltage Window: 0.01 V to 2.0 V |
|
|
- Plateau Midpoint Voltage (V_ANODE_MID): 0.1 V |
|
|
|
|
|
**Formulas Used:** |
|
|
|
|
|
1. **Areal Capacity (mAh/cm²):** |
|
|
- Q_anode = C_SPEC_ANODE × L_anode / 1000 |
|
|
- Q_cathode = C_spec_cathode × L_cathode / 1000 |
|
|
- Q_full = min(Q_anode, Q_cathode) |
|
|
|
|
|
2. **Specific Capacity (mAh/g):** |
|
|
- Q_specific = Q_full / ((L_anode + L_cathode) / 1000) |
|
|
|
|
|
3. **Nominal Voltage (V):** |
|
|
- V_nominal = V_cathode_mid - V_ANODE_MID |
|
|
|
|
|
4. **Gravimetric Energy Density (Wh/kg):** |
|
|
- GED = 1000 × Q_full × V_nominal / M_total |
|
|
|
|
|
5. **Volumetric Energy Density (Wh/L):** |
|
|
- VED = Q_full × V_nominal × 1000 / t_total |
|
|
|
|
|
6. **Voltage Window (V):** |
|
|
- V_cell_max = V_cathode_max - 0.01 |
|
|
- V_cell_min = V_cathode_min - 2.0 |
|
|
- Voltage_Window = V_cell_max - V_cell_min |
|
|
|
|
|
7. **Specific Power (W/kg):** |
|
|
- P = V_nominal × Q_full × 1000 × C_rate / 3600 |
|
|
|
|
|
**Inputs Provided:** |
|
|
- Cathode material: {cathode_name} |
|
|
- L_anode (mg/cm²): {L_anode} |
|
|
- L_cathode (mg/cm²): {L_cathode} |
|
|
- M_total (mg): {M_total} |
|
|
- t_total (mm): {t_total} |
|
|
- C_rate: {C_rate} |
|
|
|
|
|
**Calculated Results:** |
|
|
```json |
|
|
{result } |
|
|
``` |
|
|
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)} |
|
|
""" |
|
|
logger.info("Generated prompt for Gemini model: %s", prompt.strip()) |
|
|
gemini_response = model.generate_content(prompt) |
|
|
|
|
|
if gemini_response.candidates: |
|
|
parts = gemini_response.candidates[0].content.parts |
|
|
explanation = " ".join( |
|
|
p.text for p in parts if hasattr(p, "text") and p.text |
|
|
) |
|
|
else: |
|
|
explanation = "Gemini returned no candidates." |
|
|
|
|
|
result["Gemini_Explanation"] = explanation.strip() |
|
|
|
|
|
except Exception as e: |
|
|
result["Gemini_Explanation"] = f"Gemini error: {str(e)}" |
|
|
|
|
|
|
|
|
return result |
|
|
|