|
|
import numpy as np |
|
|
import scipy |
|
|
from scipy.signal import savgol_filter |
|
|
from typing import Dict, List |
|
|
import matplotlib.pyplot as plt |
|
|
import io |
|
|
import base64 |
|
|
import google.generativeai as genai |
|
|
import faiss |
|
|
from openai import OpenAI |
|
|
import json |
|
|
import os |
|
|
import pandas as pd |
|
|
|
|
|
genai.configure( |
|
|
api_key=os.environ.get("GOOGLE_API_KEY") |
|
|
) |
|
|
model = genai.GenerativeModel("gemini-2.5-flash") |
|
|
|
|
|
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_cv(input_data: Dict) -> Dict: |
|
|
V_start = input_data["V_start"] |
|
|
V_switch = input_data["V_switch"] |
|
|
scan_rate = input_data["scan_rate"] |
|
|
dt = input_data["dt"] |
|
|
sigma = input_data["sigma"] |
|
|
E0 = input_data["E0"] |
|
|
Ip = input_data["Ip"] |
|
|
|
|
|
|
|
|
t_up = np.arange(0, (V_switch - V_start) / scan_rate, dt) |
|
|
t_down = np.arange(0, (V_switch - V_start) / scan_rate, dt) |
|
|
V_up = V_start + scan_rate * t_up |
|
|
V_down = V_switch - scan_rate * t_down |
|
|
V = np.concatenate([V_up, V_down]) |
|
|
|
|
|
|
|
|
I_ox = Ip * np.exp(-((V - E0) ** 2) / (2 * sigma ** 2)) |
|
|
I_red = -Ip * np.exp(-((V - (E0 - 0.06)) ** 2) / (2 * sigma ** 2)) |
|
|
I = I_ox + I_red |
|
|
|
|
|
|
|
|
idx_ox = np.argmax(I) |
|
|
idx_red = np.argmin(I) |
|
|
V_ox = float(V[idx_ox]) |
|
|
V_red = float(V[idx_red]) |
|
|
delta_V_peak = V_ox - V_red |
|
|
|
|
|
|
|
|
|
|
|
Q = float(scipy.integrate.simpson(I, V) / scan_rate) |
|
|
|
|
|
return { |
|
|
"t": np.concatenate([t_up, t_down]).tolist(), |
|
|
"V": V.tolist(), |
|
|
"I": I.tolist(), |
|
|
"V_ox": V_ox, |
|
|
"V_red": V_red, |
|
|
"delta_V_peak": delta_V_peak, |
|
|
"Q_integrated": Q |
|
|
} |
|
|
|
|
|
def calculate_eis(data: Dict) -> Dict: |
|
|
freqs = np.array(data["frequencies"], dtype=float) |
|
|
Rs, Rct, Cdl, sigma_w = data["Rs"], data["Rct"], data["Cdl"], data["sigma_w"] |
|
|
omega = 2*np.pi*freqs |
|
|
j = 1j |
|
|
|
|
|
|
|
|
Zw = sigma_w*(1 - j)/np.sqrt(omega) |
|
|
|
|
|
|
|
|
Y_Rct = 1/Rct |
|
|
Y_Cdl = j * omega * Cdl |
|
|
Y_W = 1/Zw |
|
|
|
|
|
Y_par = Y_Rct + Y_Cdl + Y_W |
|
|
Z_parallel= 1/Y_par |
|
|
Z_total = Rs + Z_parallel |
|
|
|
|
|
|
|
|
return { |
|
|
"frequencies": freqs.tolist(), |
|
|
"Z_real": np.real(Z_total).tolist(), |
|
|
"Z_imag": np.imag(Z_total).tolist() |
|
|
} |
|
|
|
|
|
def compute_d2QdV2( |
|
|
V: List[float], |
|
|
Q: List[float], |
|
|
window: int = 21, |
|
|
poly: int = 3 |
|
|
) -> Dict[str, List[float]]: |
|
|
V = np.array(V, dtype=float) |
|
|
Q = np.array(Q, dtype=float) |
|
|
|
|
|
|
|
|
if not np.all(np.diff(V) > 0): |
|
|
idx = np.argsort(V) |
|
|
V = V[idx] |
|
|
Q = Q[idx] |
|
|
|
|
|
|
|
|
Qs = savgol_filter(Q, window_length=window, polyorder=poly) |
|
|
dQdV = np.gradient(Qs, V) |
|
|
d2QdV2 = np.gradient(dQdV, V) |
|
|
|
|
|
return {"dQdV": dQdV.tolist(), "d2QdV2": d2QdV2.tolist()} |
|
|
|
|
|
def plot_cv(V, I): |
|
|
fig, ax = plt.subplots() |
|
|
ax.plot(V, I, color='blue') |
|
|
ax.set_title("Cyclic Voltammetry") |
|
|
ax.set_xlabel("Voltage (V)") |
|
|
ax.set_ylabel("Current (A)") |
|
|
ax.grid(True) |
|
|
return fig_to_base64(fig) |
|
|
|
|
|
def plot_eis(Z_real, Z_imag): |
|
|
fig, ax = plt.subplots() |
|
|
ax.plot(Z_real, -np.array(Z_imag), 'o-', color='green') |
|
|
ax.set_title("Nyquist Plot (EIS)") |
|
|
ax.set_xlabel("Z' (Ω)") |
|
|
ax.set_ylabel("-Z'' (Ω)") |
|
|
ax.grid(True) |
|
|
return fig_to_base64(fig) |
|
|
|
|
|
def plot_dqdv(V, dQdV, d2QdV2): |
|
|
fig, ax = plt.subplots() |
|
|
ax.plot(V, dQdV, label="dQ/dV", color='orange') |
|
|
ax.plot(V, d2QdV2, label="d²Q/dV²", color='red') |
|
|
ax.set_title("Q–V Derivatives") |
|
|
ax.set_xlabel("Voltage (V)") |
|
|
ax.set_ylabel("Derivative") |
|
|
ax.legend() |
|
|
ax.grid(True) |
|
|
return fig_to_base64(fig) |
|
|
|
|
|
def fig_to_base64(fig): |
|
|
buf = io.BytesIO() |
|
|
fig.savefig(buf, format="png", bbox_inches="tight") |
|
|
buf.seek(0) |
|
|
img_base64 = base64.b64encode(buf.read()).decode("utf-8") |
|
|
plt.close(fig) |
|
|
return img_base64 |
|
|
|
|
|
def image_to_part(base64_str: str) -> dict: |
|
|
return { |
|
|
"inline_data": { |
|
|
"mime_type": "image/png", |
|
|
"data": base64_str |
|
|
} |
|
|
} |
|
|
|
|
|
def analyze_plots_with_gemini(cv_img: str, eis_img: str, qv_img: str, cathode_name: str, faiss_results: list) -> 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. |
|
|
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. |
|
|
|
|
|
These are three plots from sodium-ion battery electrochemical analysis. |
|
|
Please summarize the main features observed in: |
|
|
1. Cyclic Voltammetry (CV) |
|
|
2. Electrochemical Impedance Spectroscopy (EIS) |
|
|
3. Q–V and d²Q/dV² analysis |
|
|
Include observations on redox peaks, charge transfer resistance, and plateau features. |
|
|
|
|
|
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, |
|
|
image_to_part(cv_img), |
|
|
image_to_part(eis_img), |
|
|
image_to_part(qv_img), |
|
|
]) |
|
|
|
|
|
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"Error from Gemini API: {e}" |
|
|
|
|
|
def calculate_all_c(cathode_name: str, input_data: Dict) -> Dict: |
|
|
|
|
|
query_text = ( |
|
|
f"Sodium-ion battery with hard carbon anode and cathode {cathode_name}. " |
|
|
) |
|
|
|
|
|
faiss_results = query_faiss_index(query_text, top_k=5) |
|
|
|
|
|
cv = calculate_cv(input_data) |
|
|
eis = calculate_eis(input_data) |
|
|
deriv = compute_d2QdV2( |
|
|
V = input_data["V_qv"], |
|
|
Q = input_data["Q_qv"], |
|
|
window = input_data["window"], |
|
|
poly = input_data["poly"] |
|
|
) |
|
|
|
|
|
cv_plot = plot_cv(cv["V"], cv["I"]) |
|
|
eis_plot = plot_eis(eis["Z_real"], eis["Z_imag"]) |
|
|
qv_plot = plot_dqdv(input_data["V_qv"], deriv["dQdV"], deriv["d2QdV2"]) |
|
|
|
|
|
summary = analyze_plots_with_gemini(cv_plot, eis_plot, qv_plot, cathode_name, faiss_results) |
|
|
|
|
|
return { |
|
|
"plots": { |
|
|
"cv_plot": cv_plot, |
|
|
"eis_plot": eis_plot, |
|
|
"qv_plot": qv_plot, |
|
|
}, |
|
|
"Gemini_Explanation": summary |
|
|
} |
|
|
|