Spaces:
Sleeping
Sleeping
from transformers import pipeline | |
import gradio as gr | |
import networkx as nx | |
import sympy as sp | |
from collections import defaultdict | |
import re | |
from dotenv import load_dotenv | |
import google.generativeai as genai | |
import os | |
from gradio.themes import Ocean | |
load_dotenv() | |
API_KEY = os.getenv("GEMINI_API") | |
genai.configure(api_key=API_KEY) | |
# Initialize the Gemini Flash Model | |
model = genai.GenerativeModel('gemini-2.5-flash-lite-preview-06-17') | |
# Gradio App with Support for Multi-Reactant Networks (e.g. A + B -> AB) | |
# --- Parsing Functions --- | |
def parse_species(expr): | |
# e.g., "A + B" -> ["A", "B"] | |
return [s.strip() for s in re.split(r'\s*[\+\-]\s*', expr)] | |
def parse_network(input_string): | |
edges = [] | |
reversible_edges = [] | |
for part in input_string.split(','): | |
part = part.strip() | |
if '<->' in part: | |
lhs, rhs = part.split('<->') | |
lhs_species = parse_species(lhs) | |
rhs_species = parse_species(rhs) | |
reversible_edges.append((lhs_species, rhs_species)) | |
elif '->' in part: | |
lhs, rhs = part.split('->') | |
lhs_species = parse_species(lhs) | |
rhs_species = parse_species(rhs) | |
edges.append((lhs_species, rhs_species)) | |
return edges, reversible_edges | |
def build_graph(edges, reversible_edges): | |
G = nx.DiGraph() | |
for a, b in edges: | |
lhs = " + ".join(a) | |
rhs = " + ".join(b) | |
G.add_edge(lhs, rhs) | |
for a, b in reversible_edges: | |
lhs = " + ".join(a) | |
rhs = " + ".join(b) | |
G.add_edge(lhs, rhs) | |
G.add_edge(rhs, lhs) | |
return G | |
def analyze_graph(G): | |
return { | |
"nodes": list(G.nodes), | |
"edges": list(G.edges), | |
"num_nodes": G.number_of_nodes(), | |
"num_edges": G.number_of_edges(), | |
"is_cyclic": not nx.is_directed_acyclic_graph(G) | |
} | |
# --- ODE Generator for Complex Reactions --- | |
def mass_action_odes(edges, reversible_edges): | |
species = set() | |
odes = defaultdict(lambda: 0) | |
rate_counter = 1 | |
def term(species_list): | |
term_expr = 1 | |
for s in species_list: | |
sym = sp.symbols(s) | |
species.add(sym) | |
term_expr *= sym | |
return term_expr | |
for lhs_species, rhs_species in edges: | |
k = sp.symbols(f'k{rate_counter}') | |
rate_counter += 1 | |
flux = k * term(lhs_species) | |
for s in lhs_species: | |
sym = sp.symbols(s) | |
odes[sym] -= flux | |
for s in rhs_species: | |
sym = sp.symbols(s) | |
odes[sym] += flux | |
for lhs_species, rhs_species in reversible_edges: | |
kf = sp.symbols(f'k{rate_counter}') | |
rate_counter += 1 | |
kr = sp.symbols(f'k{rate_counter}') | |
rate_counter += 1 | |
forward_flux = kf * term(lhs_species) | |
reverse_flux = kr * term(rhs_species) | |
for s in lhs_species: | |
sym = sp.symbols(s) | |
odes[sym] -= forward_flux | |
odes[sym] += reverse_flux | |
for s in rhs_species: | |
sym = sp.symbols(s) | |
odes[sym] += forward_flux | |
odes[sym] -= reverse_flux | |
return dict(odes) | |
def format_odes(odes): | |
return "\n".join([f"d{var}/dt = {sp.simplify(expr)}" for var, expr in odes.items()]) | |
def compute_jacobian(odes): | |
variables = list(odes.keys()) | |
F = sp.Matrix([odes[var] for var in variables]) | |
J = F.jacobian(variables) | |
return sp.pretty(J) | |
def extract_network_from_image(image): | |
prompt = ( | |
"Analyze this network diagram and list the network only. " | |
"Use reaction format like 'A + B -> C' or 'X <-> Y'. " | |
"List multiple reactions separated by commas." | |
) | |
gemini_response = model.generate_content([prompt, image]) | |
return gemini_response.text.strip() | |
def full_process(image, text_input, query): | |
if text_input.strip(): # If text is given, use it | |
network_description = text_input.strip() | |
elif image is not None: # Else if image is given, extract network from image | |
network_description = extract_network_from_image(image) | |
else: | |
return "❌ Please provide either a network image or a textual description." | |
# Step 2: Process extracted/generated network | |
return process_network(network_description, query) | |
qa = pipeline("text2text-generation", model="google/flan-t5-base") | |
def process_network(input_string, query): | |
edges, reversible_edges = parse_network(input_string) | |
G = build_graph(edges, reversible_edges) | |
info = analyze_graph(G) | |
if 'ode' in query.lower(): | |
ode_sys = mass_action_odes(edges, reversible_edges) | |
return format_odes(ode_sys) | |
elif 'jacobian' in query.lower(): | |
ode_sys = mass_action_odes(edges, reversible_edges) | |
return f"Jacobian Matrix:\n{compute_jacobian(ode_sys)}" | |
elif 'variables' in query.lower(): | |
return f"There are {info['num_nodes']} variables: {info['nodes']}" | |
elif 'edges' in query.lower(): | |
return f"Edges: {info['edges']}" | |
elif 'cyclic' or 'cycle' in query.lower(): | |
cycles = list(nx.simple_cycles(G)) | |
if cycles: | |
cycles_str = "\n".join([" -> ".join(cycle + [cycle[0]]) for cycle in cycles]) | |
return f"Cycles found:\n{cycles_str}" | |
else: | |
return "No cycles found." | |
else: | |
prompt = f"Given the network with nodes: {info['nodes']} and edges: {info['edges']}, answer the query: {query}" | |
answer = qa(prompt, max_length=128)[0]['generated_text'] | |
return answer | |
iface = gr.Interface( | |
fn=full_process, | |
inputs=[ | |
gr.Image(type="pil", label="Upload Network Image (optional)"), | |
gr.Textbox(label="Text Input (optional)", placeholder="Or paste network: A + B -> C, X <-> Y"), | |
gr.Textbox(label="Query", placeholder="Ask about ODEs, Jacobian, edges, etc.") | |
], | |
outputs="text", | |
title="Biological Network Analyzer", | |
description="Upload an image or enter network text. Then ask a query like 'Give ODEs' or 'Is it cyclic?'.", | |
theme=Ocean() | |
) | |
iface.launch(share=True) | |