diff --git a/.gitignore b/.gitignore index 0b023cf..e4abe26 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,6 @@ analysis/** # testing files **/scratch.ipynb **/participants_likelihood*.csv -**/reaction_likelihood*.csv \ No newline at end of file +**/reaction_likelihood*.csv +**/AAAIM_species_result +**/BioModels_251106 \ No newline at end of file diff --git a/core/annotation_workflow.py b/core/annotation_workflow.py index 112388d..c2bd0d7 100644 --- a/core/annotation_workflow.py +++ b/core/annotation_workflow.py @@ -55,7 +55,12 @@ def annotate_single_model( database: str | DatabaseID = DatabaseID.CHEBI, tax_id: str = None, chunk_size: int = 50, - species_recommendations_df = None) -> Tuple[pd.DataFrame, Dict[str, Any]]: + species_recommendations_df = None, + evaluate_candidates: bool = False, + include_exchange_reactions: bool = False, + cofactor_config=None, + disable_ontology_relaxation: bool = False, +) -> Tuple[pd.DataFrame, Dict[str, Any]]: """ Annotate a single model that has no or limited existing annotations. @@ -72,6 +77,20 @@ def annotate_single_model( database: Target database ("chebi", "ncbigene", "uniprot") tax_id: For gene/protein annotations, the organism's tax_id for species-specific lookup chunk_size: Size of chunks to split large models into (default: 50, None for no chunking) + evaluate_candidates: Only used for ``method="rulebased"`` (KEGG reaction workflow). + If True, run scoring + EM-like participant updates; if False (default), run + generation-only and skip evaluation steps. + include_exchange_reactions: Only used for ``method="rulebased"``. + If True, generate reaction candidates for exchange reactions (empty LHS or RHS). + If False (default), exchange reactions are retained but returned with no candidates. + cofactor_config: Only used for ``method="rulebased"``. Instance of + ``CofactorConfig`` controlling which metabolites are ignored during + reaction matching. Pass ``CofactorConfig(cofactors_dict={})`` to + disable cofactor removal entirely. ``None`` uses the default set + (H2O, H+, ATP, NAD+, etc.). + disable_ontology_relaxation: Only used for ``method="rulebased"``. If + True, skips ChEBI ontology relaxation entirely — species are matched + only at their exact annotated ChEBI level with no ancestor traversal. Returns: Tuple of (recommendations_df, metrics_dict) @@ -174,6 +193,10 @@ def annotate_single_model( model_file, species_recommendations_df, existing_annotations=existing_annotations, + evaluate_candidates=bool(evaluate_candidates), + include_exchange_reactions=bool(include_exchange_reactions), + cofactor_config=cofactor_config, + disable_ontology_relaxation=bool(disable_ontology_relaxation), ) @@ -467,14 +490,16 @@ def _generate_recommendation_table(model_file: str, for i, candidate in enumerate(rec.candidates): candidate_display = f"{database.upper()}:{candidate}" is_existing = candidate in existing_annotations.get(rec.id, []) - match_score = rec.match_score[i] + match_score = 0.0 + if getattr(rec, "match_score", None) and i < len(rec.match_score): + match_score = rec.match_score[i] if is_existing: status = 'original and predicted' update_action = 'keep' else: status = 'predicted only' - if i == 0 and match_score > 0.5: + if i == 0 and match_score is not None and float(match_score) > 0.5: update_action = 'add' else: update_action = 'ignore' diff --git a/core/curation_workflow.py b/core/curation_workflow.py index e83939c..0e33441 100644 --- a/core/curation_workflow.py +++ b/core/curation_workflow.py @@ -14,7 +14,12 @@ import warnings from utils.constants import DatabaseID, EntityType -from core.model_info import find_species_with_chebi_annotations, find_species_with_annotations_and_qualifiers, find_species_with_ncbigene_annotations, find_species_with_uniprot_annotations, extract_model_info, format_prompt +from core.model_info import ( + find_species_with_annotations_and_qualifiers, + find_reactions_with_kegg_annotations, + extract_model_info, + format_prompt, +) from core.llm_interface import get_system_prompt, query_llm, parse_llm_response from core.data_types import Recommendation from core.database_search import get_species_recommendations_direct, get_species_recommendations_rag, load_uniprot_label_dict, load_ncbigene_label_dict, load_chebi_label_dict @@ -32,7 +37,11 @@ def curate_single_model(model_file: str, entity_type: str | EntityType = EntityType.CHEMICAL, database: str | DatabaseID = DatabaseID.CHEBI, tax_id: str = None, - chunk_size: int = 50) -> Tuple[pd.DataFrame, Dict[str, Any]]: + chunk_size: int = 50, + *, + evaluate_candidates: bool = False, + include_exchange_reactions: bool = False, + ) -> Tuple[pd.DataFrame, Dict[str, Any]]: """ This is the main function users will call to get curation recommendations for a model that already has existing annotations. @@ -85,6 +94,9 @@ def curate_single_model(model_file: str, elif entity_type == EntityType.PROTEIN and database == DatabaseID.UNIPROT: existing_annotations, qualifier_annotations = find_species_with_annotations_and_qualifiers(model_file, DatabaseID.UNIPROT.value) logger.info(f"Found {len(existing_annotations)} entities with existing annotations") + elif entity_type == EntityType.REACTION and database == DatabaseID.KEGG: + existing_annotations, qualifier_annotations = find_reactions_with_kegg_annotations(model_file) + logger.info(f"Found {len(existing_annotations)} reactions with existing annotations") else: # Future: support other entity types and databases logger.warning(f"Entity type {entity_type.value} with database {database.value} not yet supported") @@ -102,6 +114,27 @@ def curate_single_model(model_file: str, else: specs_to_evaluate = list(existing_annotations.keys()) logger.info(f"Curation all {len(specs_to_evaluate)} entities") + + # Special-case: curate reaction->KEGG using the rulebased workflow. + # This path is LLM-free; it uses existing species annotations (ChEBI, or + # KEGG-compound as a fallback) as the metabolite evidence for KEGG + # reaction matching. See :func:`curate_reactions_kegg_rulebased` for the + # full logic. + if entity_type == EntityType.REACTION and database == DatabaseID.KEGG: + from core.reaction.annotation_workflow import curate_reactions_kegg_rulebased + + return curate_reactions_kegg_rulebased( + model_file, + existing_annotations, + qualifier_annotations, + specs_to_evaluate, + evaluate_candidates=bool(evaluate_candidates), + include_exchange_reactions=bool(include_exchange_reactions), + llm_model=llm_model, + top_k=top_k, + tax_id=tax_id, + start_time=start_time, + ) # Extract model context logger.info(">>>Step 2: Extracting model context...<<<") diff --git a/core/database_search.py b/core/database_search.py index 2c4a961..9888e69 100644 --- a/core/database_search.py +++ b/core/database_search.py @@ -5,23 +5,42 @@ Currently supports ChEBI, extensible to other databases. """ +from pathlib import Path +import sys + +# Make repo root importable when this module is executed directly (e.g. via debugger) +# or when the working directory is not the repository root. +_REPO_ROOT = Path(__file__).resolve().parents[1] +if str(_REPO_ROOT) not in sys.path: + sys.path.insert(0, str(_REPO_ROOT)) + + import os import re import lzma import pickle from typing import Any, Dict, List, Mapping, Optional, Set, Tuple -from pathlib import Path from dataclasses import dataclass import logging from collections import Counter, defaultdict from itertools import product -import sys import chromadb from chromadb.utils import embedding_functions -from utils.constants import REF_CHEBI2LABEL, REF_NAMES2CHEBI, REF_NCBIGENE2LABEL, REF_NAMES2NCBIGENE, REF_UNIPROT2LABEL, REF_NAMES2UNIPROT -from utils.constants import REF_CHEBI2KEGG_COMPOUND, REF_KEGG_REACTION2NAME, REF_KEGG2EC, REF_KEGG_REACTION_FEATURES, REF_KEGG_PARSED_REACTIONS -# from utils.constants import SYNONYM_WORDS_TO_REMOVE +from utils.constants import ( + REF_CHEBI2LABEL, + REF_NAMES2CHEBI, + REF_NCBIGENE2LABEL, + REF_NAMES2NCBIGENE, + REF_UNIPROT2LABEL, + REF_NAMES2UNIPROT, + REF_CHEBI2KEGG_COMPOUND, + REF_KEGG_REACTION2NAME, + REF_KEGG2EC, + REF_KEGG_REACTION_FEATURES, + REF_KEGG_PARSED_REACTIONS + ) # from utils.constants import SYNONYM_WORDS_TO_REMOVE from core.data_types import Recommendation, ReactionRecommendation +from core.model_info import reaction_stoichiometry_lhs_rhs_species from core.reaction.hierarchy_relaxation import ( expand_chebi_with_metadata, iter_chebi_for_species, @@ -29,7 +48,8 @@ merge_chebi_to_kegg_mapping, ) from core.reaction.scoring import unified_reaction_objective -from core.reaction.classification import classify_reaction +# Reaction classification (mappable/non_mappable) was used historically to gate scoring/coverage. +# The current pipeline generates candidates for all reactions without dropping any. from core.reaction.kegg_definition import extract_classifications @@ -766,6 +786,8 @@ def _get_kegg_recommendations_rulebased( top_k: int = None, spectators: bool = False, *, + evaluate_candidates: bool = False, + include_exchange_reactions: bool = False, relaxation_levels_by_entity: Optional[Mapping[str, int]] = None, penalty_lam: float = 0.0, max_relax_level: int = 1, @@ -785,6 +807,12 @@ def _get_kegg_recommendations_rulebased( species_ids: List of reaction IDs to evaluate cofactors_to_ignore: Set of KEGG IDs of cofactors to ignore top_k: Number of top candidates to return per reaction + evaluate_candidates: If True, compute similarity scores / objective ranking. + If False (default), run only candidate generation/filtering and return + ``match_score=[]``. + include_exchange_reactions: If True, attempt candidate generation for + exchange reactions (empty LHS or RHS). If False (default), exchange + reactions are retained but returned with no candidates. Returns: List of Recommendation objects with candidates and match scores @@ -936,35 +964,6 @@ def _build_relaxed_block( } return out - def _species_ids_from_equation_side(side_str: str) -> set: - out = set() - side = str(side_str or "").strip() - if not side: - return out - for term in side.split("+"): - parts = term.strip().split() - if not parts: - continue - if len(parts) == 1: - met = parts[0] - else: - try: - float(parts[0]) - except ValueError: - met = term.strip() - else: - met = parts[-1] - met = met.lstrip("$").strip() - if met: - out.add(met) - return out - - def _reaction_species_ids(reaction_equation: str) -> Tuple[set, set]: - if "=>" in reaction_equation or "->" in reaction_equation: - lhs, rhs = re.split(r"=>|->", reaction_equation, maxsplit=1) - return _species_ids_from_equation_side(lhs), _species_ids_from_equation_side(rhs) - return set(), set() - def _expand_one_species(chebi_id: str, depth: int) -> List[Dict[str, Any]]: if not chebi_id or parent_map is None or chebi_to_kegg is None: return [] @@ -1025,9 +1024,12 @@ def _recover_species_kegg_candidates( try: logger.info(f"Loading KEGG reaction data...") # Load KEGG reaction data - kegg_parsed_reactions_dict = load_kegg_parsed_reactions_dict() + kegg_parsed_reactions_dict = None + if evaluate_candidates: + kegg_parsed_reactions_dict = load_kegg_parsed_reactions_dict() kegg_reaction_features_dict = load_kegg_reaction_features_dict() - logger.info(f"Loaded {len(kegg_parsed_reactions_dict)} parsed KEGG reactions") + if evaluate_candidates and kegg_parsed_reactions_dict is not None: + logger.info(f"Loaded {len(kegg_parsed_reactions_dict)} parsed KEGG reactions") logger.info(f"Loaded {len(kegg_reaction_features_dict)} KEGG reaction features") recommendations = [] @@ -1075,7 +1077,28 @@ def _recover_species_kegg_candidates( # --- Stage 1A: species-level relaxation (independent trigger) --- # Trigger: species has no KEGG candidates (including dropped/unmapped species). - lhs_species, rhs_species = _reaction_species_ids(reaction_str) + lhs_species, rhs_species = reaction_stoichiometry_lhs_rhs_species(reaction_str) + + reaction_class = "exchange" if (not lhs_species or not rhs_species) else "internal" + if reaction_class == "exchange" and not include_exchange_reactions: + # Keep one record so the reaction remains visible downstream, but skip + # candidate generation/scoring for exchange reactions unless requested. + recommendation = ReactionRecommendation( + id=reaction_label, + synonyms=[], + equation=reaction_str, + substrates=dict(model_subs), + products=dict(model_prods), + candidates=[], + candidate_names=[], + match_score=[], + metadata={ + "reaction_class": reaction_class, + "exchange_skipped": True, + }, + ) + recommendations.append(recommendation) + continue if species_to_chebi is not None and parent_map is not None and chebi_to_kegg is not None: _recover_species_kegg_candidates( @@ -1143,16 +1166,43 @@ def _recover_species_kegg_candidates( # not penalized for hierarchy hops (multiple relaxed candidates may coexist). reaction_penalty = 0.0 - # Keep selected mapping (strict or relaxed) on recommendation payload. - model_subs = active_subs - model_prods = active_prods - reaction_type = classify_reaction( - reaction_str, - filtered_species=filtered_species, - candidates=filtered_reaction_list, - ) matches = [] + # Optional short-circuit: return generated candidates only (no scoring / ranking). + if not evaluate_candidates: + candidate_ids = sorted(filtered_reaction_list) if filtered_reaction_list else [] + if top_k: + candidate_ids = candidate_ids[:top_k] + + candidate_names = [] + for kegg_id in candidate_ids: + orthology = kegg_reaction_features_dict.get(kegg_id, kegg_id).get("ORTHOLOGY", "") + candidate_names.append(extract_classifications(orthology, 'orthology')) + + recommendation = ReactionRecommendation( + id=reaction_label, + synonyms=[], + equation=reaction_str, + substrates=active_subs, + products=active_prods, + candidates=candidate_ids, + candidate_names=candidate_names, + match_score=[], + metadata={ + "reaction_class": reaction_class, + "filtered_species_count": int(len(filtered_species)), + "candidate_count": int(len(filtered_reaction_list)), + "participant_relaxation": sorted( + participant_relaxation.values(), + key=lambda x: (x.get("species_id", ""), x.get("kegg_id", "")), + ), + "reaction_penalty": reaction_penalty, + "scoring_skipped": True, + }, + ) + recommendations.append(recommendation) + continue + # Create a (substrates, products) pair in Counter form for similarity scoring cartesian_products = [(sub_counter, prod_counter)] # Compare with each KEGG reaction @@ -1210,13 +1260,13 @@ def _recover_species_kegg_candidates( id=reaction_label, synonyms=[], equation=reaction_str, - substrates=model_subs, - products=model_prods, + substrates=active_subs, + products=active_prods, candidates=candidates, candidate_names=candidate_names, match_score=match_scores, metadata={ - "reaction_type": reaction_type, + "reaction_class": reaction_class, "filtered_species_count": int(len(filtered_species)), "candidate_count": int(len(filtered_reaction_list)), "participant_relaxation": sorted( @@ -1224,16 +1274,10 @@ def _recover_species_kegg_candidates( key=lambda x: (x.get("species_id", ""), x.get("kegg_id", "")), ), "reaction_penalty": reaction_penalty, - "failed_default_score": 0.0, }, ) - if reaction_type == "failed_mapping": - # Keep one record so downstream aggregation can score failed-but-eligible reactions. - recommendation.match_score = [0.0] - recommendations.append(recommendation) - elif reaction_type == "non_mappable": - # Keep one record for coverage tracking; excluded by aggregator from scoring. - recommendation.match_score = [] + if not candidates: + # Keep one record so the reaction remains visible downstream. recommendations.append(recommendation) else: recommendations.extend(split_recommendation(recommendation)) diff --git a/core/llm_interface.py b/core/llm_interface.py index 7fdca5a..98ba45a 100644 --- a/core/llm_interface.py +++ b/core/llm_interface.py @@ -29,6 +29,71 @@ logger = logging.getLogger(__name__) +def _extract_llama_completion_message_text(response: Any) -> str: + """Extract text from the Llama API client response shape used in this project. + + Historically, the Llama API path returned a response with a ``completion_message`` + dict containing ``{\"content\": {\"text\": \"...\"}}``. + """ + if response is None or not hasattr(response, "completion_message"): + return "" + try: + cm = response.completion_message + if isinstance(cm, dict): + content = cm.get("content") or {} + if isinstance(content, dict): + text = content.get("text") + return text if isinstance(text, str) else "" + except Exception: + return "" + return "" + +def _extract_chat_text(response: Any) -> str: + """Best-effort extraction of assistant text from an OpenAI chat completion response. + + Supports: + - OpenAI python client: response.choices[0].message.content (string or list of parts) + - Fallbacks for dict-like responses (rare in this codebase) + """ + if response is None: + return "" + + # OpenAI python client objects + if hasattr(response, "choices") and getattr(response, "choices"): + choice0 = response.choices[0] + msg = getattr(choice0, "message", None) + content = getattr(msg, "content", None) if msg is not None else None + if content is None: + return "" + if isinstance(content, str): + return content + # Some clients return a list of content parts; join text-like fields. + if isinstance(content, list): + parts: list[str] = [] + for p in content: + if isinstance(p, str): + parts.append(p) + elif isinstance(p, dict): + # Common shape: {"type":"text","text":"..."} + t = p.get("text") + if isinstance(t, str): + parts.append(t) + return "\n".join(x for x in parts if x.strip()) + return str(content) + + # Dict-like fallback + if isinstance(response, dict): + try: + choices = response.get("choices") or [] + if choices: + msg = choices[0].get("message") or {} + content = msg.get("content") + return content if isinstance(content, str) else str(content or "") + except Exception: + return "" + + return "" + def get_system_prompt(entity_type: str | EntityType = EntityType.CHEMICAL) -> str: """ @@ -200,11 +265,28 @@ def query_llm(prompt: str, developer_prompt: str = None, model=GPT_MINI_MODEL, e else: raise ValueError(f"Model {model} not supported") - if response is not None and hasattr(response, "completion_message") and "content" in response.completion_message and "text" in response.completion_message["content"]: - return response.completion_message["content"]["text"] + # Keep legacy Llama response extraction, but use the standard OpenAI structure + # for non-Llama models. + if str(model).startswith("Llama"): + text = _extract_llama_completion_message_text(response) or _extract_chat_text(response) else: + text = _extract_chat_text(response) + if text and str(text).strip(): + return str(text) + + # Empty content: log minimal response metadata for debugging. + try: + finish_reason = None + if response is not None and hasattr(response, "choices") and response.choices: + finish_reason = getattr(response.choices[0], "finish_reason", None) + logger.warning( + "No response or empty response from LLM (model=%s, finish_reason=%s).", + model, + finish_reason, + ) + except Exception: print("No response or empty response from LLM.") - return "" + return "" def query_llm_with_history(messages: list, model: str = GPT_MINI_MODEL, max_retries: int = DEFAULT_MAX_RETRIES, @@ -254,11 +336,14 @@ def query_llm_with_history(messages: list, model: str = GPT_MINI_MODEL, else: raise ValueError(f"Model {model} not supported") - if response is not None and hasattr(response, "choices") and response.choices: - return response.choices[0].message.content + if str(model).startswith("Llama"): + text = _extract_llama_completion_message_text(response) or _extract_chat_text(response) else: - print("No response or empty response from LLM. L379") - return "" + text = _extract_chat_text(response) + if text and str(text).strip(): + return str(text) + logger.warning("No response or empty response from LLM (model=%s).", model) + return "" def parse_llm_response( diff --git a/core/model_info.py b/core/model_info.py index dd8fdd5..cfba5e0 100644 --- a/core/model_info.py +++ b/core/model_info.py @@ -18,6 +18,7 @@ ModelType, CHEBI_URI_PATTERNS, KEGG_REACTION_URI_PATTERNS, + KEGG_COMPOUND_URI_PATTERNS, NCBIGENE_URI_PATTERNS, UNIPROT_URI_PATTERNS, ) @@ -299,7 +300,8 @@ def find_species_with_annotations_and_qualifiers(model_file: str, database: str, Args: model_file: Path to the SBML model file - database: Database to search ("chebi", "ncbigene", "uniprot") + database: Database to search ("chebi", "ncbigene", "uniprot", "kegg"). + For species, "kegg" means KEGG compound IDs (C#####). bqbiol_qualifiers: List of bqbiol qualifiers to extract (e.g. ['is', 'isVersionOf', 'hasPart']) Returns: @@ -328,6 +330,8 @@ def find_species_with_annotations_and_qualifiers(model_file: str, database: str, uri_patterns = NCBIGENE_URI_PATTERNS elif database == DatabaseID.UNIPROT.value: uri_patterns = UNIPROT_URI_PATTERNS + elif database == DatabaseID.KEGG.value: + uri_patterns = KEGG_COMPOUND_URI_PATTERNS else: logger.warning(f"Database {database} not supported") return {}, {} @@ -454,6 +458,7 @@ def find_species_with_uniprot_annotations(model_file: str, bqbiol_qualifiers: li return uniprot_annotations + def find_reactions_with_kegg_annotations(model_file: str, bqbiol_qualifiers: list = None) -> Dict[str, List[str]]: """ Find reactions with existing KEGG annotations. @@ -791,6 +796,64 @@ def build_antimony_reaction_index(model_file: str) -> List[Tuple[str, str, Set[s return index +def _species_ids_from_stoichiometry_side(side_str: str) -> Set[str]: + """Metabolite/species tokens on one side of a reaction equation. + + Canonical parsing for the AAAIM pipeline; reused via + :func:`reaction_stoichiometry_lhs_rhs_species` from + :func:`core.database_search._get_kegg_recommendations_rulebased` so that + "exchange-style" reactions (empty LHS or RHS) are detected consistently. + """ + out: Set[str] = set() + side = str(side_str or "").strip() + if not side: + return out + for term in side.split("+"): + parts = term.strip().split() + if not parts: + continue + if len(parts) == 1: + met = parts[0] + else: + try: + float(parts[0]) + except ValueError: + met = term.strip() + else: + met = parts[-1] + met = met.lstrip("$").strip() + if met: + out.add(met) + return out + + +def reaction_stoichiometry_lhs_rhs_species(reaction_str: str) -> Tuple[Set[str], Set[str]]: + """Return ``(lhs_species_ids, rhs_species_ids)`` parsed from a reaction string.""" + s = str(reaction_str or "") + if "=>" in s or "->" in s: + lhs, rhs = re.split(r"=>|->", s, maxsplit=1) + return ( + _species_ids_from_stoichiometry_side(lhs), + _species_ids_from_stoichiometry_side(rhs), + ) + return set(), set() + + +def exchange_constraint_skipped_reaction_ids(model_file: str) -> Set[str]: + """SBML reaction ids with an empty LHS or RHS (source/sink/exchange-style). + + When ``include_exchange_reactions`` is False in the rule-based KEGG matcher, + these reactions get no candidates and are omitted from LLM re-ranking + (see ``exchange_skipped`` metadata in ``_get_kegg_recommendations_rulebased``). + """ + out: Set[str] = set() + for reaction_id, reaction_str, _ in build_antimony_reaction_index(model_file): + lhs, rhs = reaction_stoichiometry_lhs_rhs_species(reaction_str) + if not lhs or not rhs: + out.add(str(reaction_id)) + return out + + def filter_reactions_from_antimony_index( antimony_index: List[Tuple[str, str, Set[str]]], species_ids: List[str], diff --git a/core/reaction/amendment.py b/core/reaction/amendment.py index d35a626..dd5222c 100644 --- a/core/reaction/amendment.py +++ b/core/reaction/amendment.py @@ -897,13 +897,8 @@ def update_participant_likelihoods( top_k=None, ) - allowed_reaction_types = {"mappable", "ambiguous_mapping"} - match_results = [ - rec - for rec in match_results - if str((getattr(rec, "metadata", None) or {}).get("reaction_type", "mappable")) - in allowed_reaction_types - ] + # Keep all reactions (including those with zero candidates) so the workflow + # can attempt candidate generation everywhere. logger.info(f"Iteration {iteration}: Generating recommendation table") updated_kegg_recommendations_df = _generate_recommendation_table( diff --git a/core/reaction/annotation_workflow.py b/core/reaction/annotation_workflow.py index de0b308..968f442 100644 --- a/core/reaction/annotation_workflow.py +++ b/core/reaction/annotation_workflow.py @@ -1,27 +1,32 @@ import logging +import time from collections import Counter from pathlib import Path -from typing import Dict, List, NamedTuple, Optional +from typing import Any, Dict, FrozenSet, List, NamedTuple, Optional, Tuple, Union import pandas as pd from core.annotation_workflow import _generate_recommendation_table +from core.database_search import _get_kegg_recommendations_rulebased from core.llm_interface import query_llm from core.model_info import ( extract_model_info, extract_reactions_from_sbml, + find_species_with_annotations_and_qualifiers, + find_species_with_chebi_annotations, get_all_reaction_ids, map_reaction_ids_to_stoichiometry_strings, ) from .amendment import LikelihoodCalculator, update_participant_likelihoods +from .matching import map_reactions_to_kegg from .scoring import SimilarityCalculator from .amendment_config import CofactorConfig, ConvergenceConfig, MatchingConfig from .kegg_features import KEGGReactionFeatures, REF_KEGG_REACTION_FEATURES from .relaxation_workflow import map_reactions_to_kegg_with_relaxation from .species_probability import init_species_probs_from_dict from .utils import check_environment, extract_reaction_participants, map_chebi_to_kegg -from utils.constants import EntityType +from utils.constants import DatabaseID, EntityType logging.basicConfig( level=logging.INFO, @@ -50,9 +55,17 @@ def run_kegg_annotation_workflow_rulebased( cofactor_config: Optional[CofactorConfig] = None, convergence_config: Optional[ConvergenceConfig] = None, matching_config: Optional[MatchingConfig] = None, + evaluate_candidates: bool = False, + include_exchange_reactions: bool = False, + disable_ontology_relaxation: bool = False, ) -> Optional[KeggAnnotationWorkflowResult]: """Run the complete KEGG annotation workflow. + Args: + disable_ontology_relaxation: If True, skips ChEBI ontology relaxation + entirely (equivalent to ``max_relax_level=0``). Species are only + matched at their exact ChEBI level; no ancestor traversal is attempted. + Returns: KeggAnnotationWorkflowResult with four DataFrames, or ``None`` if the environment check fails (see :func:`~core.reaction.utils.check_environment`). @@ -80,33 +93,44 @@ def run_kegg_annotation_workflow_rulebased( logger.info("\nSample of ChEBI to KEGG mapping:") if not high_score_recommendations.empty: + sample_cols = ["id", "display_name", "annotation", "KEGG_ID", "match_score"] + sample_cols = [c for c in sample_cols if c in high_score_recommendations.columns] logger.info( high_score_recommendations[ - ["id", "display_name", "annotation", "KEGG_ID", "match_score"] + sample_cols ].head() ) logger.info("Step 3: Begin rule-based matching to identify reactions") - reactions, _ = extract_reactions_from_sbml( - model_file, - list(high_score_recommendations["id"].unique()), - ) - _, match_results, _species_relax_levels = map_reactions_to_kegg_with_relaxation( - reactions, - reaction_ids, - high_score_recommendations, - spectators=False, - cofactors_to_ignore=cofactor_config.kegg_ids, - top_k=None, - ) - - # Only keep reaction candidates that are eligible for updating. - allowed_reaction_types = {"mappable", "ambiguous_mapping"} - match_results = [ - rec - for rec in match_results - if str((getattr(rec, "metadata", None) or {}).get("reaction_type", "mappable")) in allowed_reaction_types - ] + # ``map_chebi_to_kegg`` returns an empty DataFrame with no columns when every + # species ChEBI term fails ChEBI→KEGG mapping; avoid ``df["id"]`` on schema-less + # frames. + if high_score_recommendations.empty or "id" not in high_score_recommendations.columns: + mapped_species_ids: List[str] = [] + logger.warning( + "No high-score ChEBI→KEGG compound mappings (species evidence); " + "skipping relaxation-based reaction matching." + ) + else: + mapped_species_ids = list(high_score_recommendations["id"].astype(str).unique()) + + reactions, _ = extract_reactions_from_sbml(model_file, mapped_species_ids) + + if high_score_recommendations.empty or "id" not in high_score_recommendations.columns: + match_results: List[Any] = [] + _species_relax_levels: Dict[str, int] = {} + else: + _, match_results, _species_relax_levels = map_reactions_to_kegg_with_relaxation( + reactions, + reaction_ids, + high_score_recommendations, + spectators=False, + cofactors_to_ignore=cofactor_config.kegg_ids, + top_k=None, + evaluate_candidates=bool(evaluate_candidates), + include_exchange_reactions=bool(include_exchange_reactions), + max_relax_level=0 if disable_ontology_relaxation else 2, + ) kegg_recommendations_df = _generate_recommendation_table( model_file, @@ -118,6 +142,15 @@ def run_kegg_annotation_workflow_rulebased( {}, ) + if not evaluate_candidates: + logger.info("Generation-only mode: skipping scoring/participant update steps.") + return KeggAnnotationWorkflowResult( + high_score_recommendations=high_score_recommendations, + kegg_recommendations=kegg_recommendations_df, + scored_reactions=pd.DataFrame(), + updated_participants=pd.DataFrame(), + ) + kegg_recommendations_df["match_score_norm"] = ( kegg_recommendations_df["match_score"] / kegg_recommendations_df.groupby("id")["match_score"].transform("sum") @@ -195,14 +228,24 @@ def _strip_kegg_prefix(raw) -> str: return s -def _build_reaction_annotation_choices(sub_df: pd.DataFrame) -> str: - """Build a newline-separated string of ``R#####: `` for an LLM prompt.""" +def _build_reaction_annotation_choices( + sub_df: pd.DataFrame, + allowed_kegg_ids: Optional[set] = None, +) -> str: + """Build a newline-separated string of ``R#####: `` for an LLM prompt. + + If *allowed_kegg_ids* is given, only candidates whose KEGG id is in that + set are emitted (used to restrict the prompt to the IUBMB-leaf + representatives selected by :func:`_group_candidates_by_iubmb`). + """ lines: list[str] = [] seen: set[tuple[str, str]] = set() for _, row in sub_df.iterrows(): rid = _strip_kegg_prefix(row.get("annotation", "")) if not rid: continue + if allowed_kegg_ids is not None and rid not in allowed_kegg_ids: + continue definition = row.get("reaction_definition", "") if definition is None or (isinstance(definition, float) and definition != definition): definition = "" @@ -216,6 +259,141 @@ def _build_reaction_annotation_choices(sub_df: pd.DataFrame) -> str: return "\n".join(lines) +def _group_candidates_by_iubmb( + ordered_kegg_ids: List[str], + kegg_features: "KEGGReactionFeatures", +) -> Tuple[Dict[str, List[str]], Dict[str, str], List[str]]: + """Cluster KEGG reaction candidates by their IUBMB BRITE hierarchy. + + Uses the per-reaction IUBMB hierarchy block (``[BR:br08202]``) to figure + out which candidates collapse together. Two candidates land in the same + group when their (ancestors ∪ {self}) sets overlap — a transitive closure + that connects e.g. ``R00299`` and ``R01786`` because both list ``R02848`` + as their IUBMB parent under EC 2.7.1.1. + + For each group we report: + * ``brite_group_members`` — every candidate that ended up in the group, + in input order. Members are the only candidates that share an IUBMB + subtree; reactions that lack a ``br08202`` block (e.g. ``R10049``) + stay in their own singleton group and are never collapsed with + biochemically distinct neighbors. + * ``brite_iubmb_parent`` — the topmost element of the group's + ancestor closure. This is whichever R-number sits above every other + chain element when we read each candidate's IUBMB subblock as a + root-to-self chain. The parent may itself be a candidate + (e.g. ``R01068`` for the aldolase pair) or a non-candidate + (e.g. ``R02848`` for the hexokinase pair when only ``R00299`` + and ``R01786`` are in the input). + * Leaves — candidates that no other candidate has as an ancestor. + These are the "most specific" entries in the candidate set and are + the ones we hand to the LLM. + + Args: + ordered_kegg_ids: Bare KEGG reaction ids (e.g. ``["R00299", "R01600"]``) + in display order. Order matters for stable output ordering only. + kegg_features: Loaded :class:`KEGGReactionFeatures` instance. + + Returns: + ``(kid_to_members, kid_to_iubmb_parent, leaves_in_order)``: + * ``kid_to_members[kid]`` — ordered member list of *kid*'s group. + * ``kid_to_iubmb_parent[kid]`` — IUBMB parent R-number for *kid*'s + group (empty string if neither *kid* nor any group-mate has a + ``br08202`` block). + * ``leaves_in_order`` — group leaves across all groups, in input + order. These are the candidates the LLM should rank. + """ + if not ordered_kegg_ids: + return {}, {}, [] + + cand_to_chains: Dict[str, Tuple[Tuple[str, ...], ...]] = { + kid: kegg_features.get_iubmb_chains(f"KEGG:{kid}") + for kid in ordered_kegg_ids + } + cand_to_ancestors: Dict[str, FrozenSet[str]] = { + kid: kegg_features.get_iubmb_ancestors(f"KEGG:{kid}") + for kid in ordered_kegg_ids + } + cand_to_chain_set: Dict[str, set] = { + kid: set(cand_to_ancestors[kid]) | {kid} for kid in ordered_kegg_ids + } + + parent: Dict[str, str] = {c: c for c in ordered_kegg_ids} + + def _find(x: str) -> str: + while parent[x] != x: + parent[x] = parent[parent[x]] + x = parent[x] + return x + + def _union(a: str, b: str) -> None: + ra, rb = _find(a), _find(b) + if ra != rb: + parent[rb] = ra + + elem_to_candidates: Dict[str, List[str]] = {} + for kid in ordered_kegg_ids: + for elem in cand_to_chain_set[kid]: + elem_to_candidates.setdefault(elem, []).append(kid) + for cands_sharing in elem_to_candidates.values(): + first = cands_sharing[0] + for other in cands_sharing[1:]: + _union(first, other) + + group_to_members: Dict[str, List[str]] = {} + for c in ordered_kegg_ids: + root = _find(c) + group_to_members.setdefault(root, []).append(c) + + kid_to_members: Dict[str, List[str]] = {} + kid_to_iubmb_parent: Dict[str, str] = {} + is_leaf: Dict[str, bool] = {c: True for c in ordered_kegg_ids} + + first_idx = {kid: i for i, kid in enumerate(ordered_kegg_ids)} + + for members in group_to_members.values(): + # Build the closure of every chain element seen across this group's + # candidates and an `ancestor_of` relation (a, b) meaning "a appears + # before b in some candidate's IUBMB chain". + closure: set = set() + ancestor_of: set = set() + elem_first_seen: Dict[str, int] = {} + for m in members: + for chain_idx, chain in enumerate(cand_to_chains[m]): + for pos, node in enumerate(chain): + if node not in elem_first_seen: + elem_first_seen[node] = ( + first_idx[m] * 10000 + chain_idx * 100 + pos + ) + closure.add(node) + for descendant in chain[pos + 1 :]: + ancestor_of.add((node, descendant)) + closure.add(m) + + # IUBMB parent: a closure element with no element above it in the + # closure. Tie-break by earliest appearance across all chains. + topmost = [ + e for e in closure + if not any((other, e) in ancestor_of for other in closure if other != e) + ] + if topmost: + iubmb_parent = min(topmost, key=lambda e: elem_first_seen.get(e, 10**9)) + else: + iubmb_parent = "" + + # Leaves: candidates that no other candidate has as an ancestor. + ancestor_union: set = set() + for m in members: + ancestor_union |= cand_to_ancestors[m] + for m in members: + if m in ancestor_union: + is_leaf[m] = False + kid_to_members[m] = members + kid_to_iubmb_parent[m] = iubmb_parent + + leaves_in_order = [c for c in ordered_kegg_ids if is_leaf.get(c, True)] + return kid_to_members, kid_to_iubmb_parent, leaves_in_order + + def rank_kegg_annotations_with_llm( model_file: str, recommendations_df: pd.DataFrame, @@ -223,13 +401,18 @@ def rank_kegg_annotations_with_llm( kegg_features_file: str = REF_KEGG_REACTION_FEATURES, top_k: int = 10, csv_path: Optional[str] = None, + only_rank_meaningful = True, ) -> pd.DataFrame: """Re-rank KEGG reaction candidates using an LLM and return a filtered DataFrame. For each reaction in *model_file* that has candidate annotations in - *recommendations_df*, build a ranking prompt with the KEGG DEFINITION text - and ask the LLM to select the best-matching KEGG ids. The returned - DataFrame contains only the LLM-selected rows, in order of appearance. + *recommendations_df*, candidates are first collapsed by their IUBMB + BRITE hierarchy: variants like ``R00299`` (D-Glucose hexokinase) and + ``R01786`` (alpha-D-Glucose hexokinase) — both children of IUBMB parent + ``R02848`` — are grouped together and only the leaves of each group + (the most-specific entries in the candidate set) are sent to the LLM. + The LLM is asked to select the best-matching KEGG ids, and the returned + DataFrame contains the LLM-selected rows in ranked order. Args: model_file: Path to the SBML model file. @@ -239,61 +422,193 @@ def rank_kegg_annotations_with_llm( kegg_features_file: Path to the KEGG reaction features lzma file. top_k: Maximum number of KEGG ids to keep per reaction from the LLM response. - csv_path: If given, the enriched recommendations table (with the - ``reaction_definition`` column) is saved to this path before - ranking begins. + csv_path: If given, the enriched recommendations table — augmented + with ``reaction_definition``, ``brite_group_members`` (every + candidate folded into the same IUBMB group, semicolon-joined) + and ``brite_iubmb_parent`` (the topmost IUBMB ancestor of the + group) — is saved to this path before ranking begins. Returns: A DataFrame filtered to the LLM-selected KEGG ids, in ranked order. - A copy is also saved to ``_llm_ranked.csv`` next to - *csv_path* (or next to *model_file* if *csv_path* is not provided). + Carries the ``reaction_definition``, ``brite_group_members`` and + ``brite_iubmb_parent`` columns. A copy is also saved to + ``_llm_ranked.csv`` next to *csv_path* (or next to + *model_file* if *csv_path* is not provided). """ from utils.constants import REACTION_ANNOTATION_RANKING_PROMPT result_df = recommendations_df.copy() kegg_features = KEGGReactionFeatures.load_from_file(kegg_features_file) + # Some models may yield zero candidates (or an unexpected recommendations_df shape). + # In that case, return an empty ranked dataframe instead of failing. + if result_df.empty or "annotation" not in result_df.columns or "id" not in result_df.columns: + if "reaction_definition" not in result_df.columns: + result_df["reaction_definition"] = pd.Series(dtype="object") + + base = Path(csv_path) if csv_path else Path(f"{Path(model_file).name}_recommendations") + ranked_out_path = base.with_name(base.stem + "_llm_ranked.csv") + result_df.iloc[0:0].copy().to_csv(ranked_out_path, index=False) + logger.info("LLM-ranked recommendations saved to %s", ranked_out_path) + return result_df.iloc[0:0].copy() + result_df["reaction_definition"] = result_df["annotation"].map(kegg_features.get_definition) + # Cluster candidates by their IUBMB BRITE hierarchy (``[BR:br08202]``), + # per reaction. Each row gets: + # * ``brite_group_members`` — semicolon-joined list of every candidate + # in the IUBMB group (so the user can see which KEGG entries we + # collapsed away before LLM ranking). + # * ``brite_iubmb_parent`` — the topmost IUBMB ancestor of the group + # (e.g. ``R02848`` for hexokinase variants, ``R01068`` for the + # aldolase pair). Empty if no group member has a ``br08202`` block. + # Only the leaves of each IUBMB group — the most-specific entries the + # rule-based pipeline produced — are forwarded to the LLM. + brite_kid_to_members: Dict[str, Dict[str, List[str]]] = {} + brite_kid_to_parent: Dict[str, Dict[str, str]] = {} + brite_leaves: Dict[str, List[str]] = {} + for reaction_id, sub in result_df.groupby("id", sort=False): + ordered: List[str] = [] + seen_kids: set = set() + for ann in sub["annotation"].tolist(): + kid = _strip_kegg_prefix(ann) + if kid and kid not in seen_kids: + ordered.append(kid) + seen_kids.add(kid) + kid_to_members, kid_to_parent, leaves = _group_candidates_by_iubmb( + ordered, kegg_features + ) + brite_kid_to_members[str(reaction_id)] = kid_to_members + brite_kid_to_parent[str(reaction_id)] = kid_to_parent + brite_leaves[str(reaction_id)] = leaves + + def _members_for_row(row: pd.Series) -> str: + rxn_id = str(row.get("id", "")) + kid = _strip_kegg_prefix(row.get("annotation", "")) + if not kid: + return "" + members = brite_kid_to_members.get(rxn_id, {}).get(kid, [kid]) + return ";".join(members) + + def _parent_for_row(row: pd.Series) -> str: + rxn_id = str(row.get("id", "")) + kid = _strip_kegg_prefix(row.get("annotation", "")) + if not kid: + return "" + return brite_kid_to_parent.get(rxn_id, {}).get(kid, "") + + result_df["brite_group_members"] = result_df.apply(_members_for_row, axis=1) + result_df["brite_iubmb_parent"] = result_df.apply(_parent_for_row, axis=1) + if csv_path is not None: result_df.to_csv(csv_path, index=False) - logger.info("%s updated with KEGG DEFINITIONs", csv_path) + logger.info( + "%s updated with KEGG DEFINITIONs and BRITE IUBMB hierarchy columns", + csv_path, + ) reaction_ids = get_all_reaction_ids(model_file) id_to_equation = map_reaction_ids_to_stoichiometry_strings(model_file) + model_context = "\n".join(id_to_equation.get(rid, str(rid)) for rid in reaction_ids) + + if only_rank_meaningful: + prompt = f""" + You are evaluating whether a biochemical reaction network uses biologically meaningful reaction and species names. + +Criteria for YES: + +* At least some species names correspond to recognizable biochemical entities, metabolites, macromolecules, or chemical abbreviations (e.g. ATP, ADP, IMP, DNA, PRPP, glucose). +* Reactions resemble plausible biochemical transformations or transport/degradation processes. +* The model appears interpretable by a human familiar with biochemistry. + +Criteria for NO: + +* Species and reactions are anonymous placeholders or opaque IDs (e.g. s1, c03493, x103, R13842, etc.) with no biochemical meaning. +* Reactions cannot be interpreted biologically from the names alone. +* The network appears synthetically generated or unlabeled. + +Return ONLY: +TRUE +or +FALSE + +Model: +{model_context} + + """ + response_text = query_llm(prompt, model=llm_model, entity_type=EntityType.REACTION) + logger.info("LLM meaningful evaluation: %s", response_text) + meaningful = response_text.strip().lower() == "true" + else: + meaningful = False ranked_reaction_ids: list[str] = [] ranked_responses: list[list[str]] = [] + ranked_llm_notes: list[str] = [] for reaction_id in reaction_ids: model_reaction = id_to_equation.get(reaction_id, reaction_id) logger.info("Ranking candidates for %s", model_reaction) sub = result_df[result_df["id"] == reaction_id] - reaction_annotation_choices = _build_reaction_annotation_choices(sub) + # Show the LLM only the IUBMB-leaf candidates — the most-specific + # entries within each BRITE group. Non-leaf members (e.g. R00299 when + # R01786 is also a candidate) are still recorded per row in + # `brite_group_members` for the final output. + leaf_ids = set(brite_leaves.get(str(reaction_id), [])) + reaction_annotation_choices = _build_reaction_annotation_choices( + sub, allowed_kegg_ids=leaf_ids if leaf_ids else None + ) if not reaction_annotation_choices.strip(): continue - prompt = REACTION_ANNOTATION_RANKING_PROMPT.format( - model_reaction=model_reaction, - reaction_annotation_choices=reaction_annotation_choices, - ) + if meaningful: + prompt = REACTION_ANNOTATION_RANKING_PROMPT.format( + model_context=model_context, + model_reaction=model_reaction, + reaction_annotation_choices=reaction_annotation_choices, + ) + else: + prompt = REACTION_ANNOTATION_RANKING_PROMPT.format( + model_context="", + model_reaction=model_reaction, + reaction_annotation_choices=reaction_annotation_choices, + ) + + + # If there's only one candidate, still ask the LLM; if it rejects (UNK), + # keep the single candidate anyway with a note. + choice_lines = [ + ln.strip() for ln in reaction_annotation_choices.splitlines() if ln.strip() + ] + single_candidate_kegg: str = "" + if len(choice_lines) == 1: + single_candidate_kegg = choice_lines[0].split(":", 1)[0].strip() response_text = query_llm(prompt, model=llm_model, entity_type=EntityType.REACTION) - response_lines = [ln.strip() for ln in (response_text or "").splitlines() if ln.strip()] + response_lines = [ + ln.strip() for ln in (response_text or "").splitlines() if ln.strip() + ] logger.info("%s -> %s", reaction_id, response_lines) if len(response_lines) == 1 and response_lines[0] == "UNK": + if single_candidate_kegg and single_candidate_kegg.upper() != "UNK": + ranked_reaction_ids.append(reaction_id) + ranked_responses.append([single_candidate_kegg][:top_k]) + ranked_llm_notes.append("LLM does not approve of this annotation") continue ranked_reaction_ids.append(reaction_id) ranked_responses.append(response_lines[:top_k]) + ranked_llm_notes.append("") logger.info("Collected LLM rankings for %d reactions", len(ranked_responses)) ranked_rows: list[pd.DataFrame] = [] - for reaction_id, kegg_ids in zip(ranked_reaction_ids, ranked_responses): + for reaction_id, kegg_ids, llm_note in zip( + ranked_reaction_ids, ranked_responses, ranked_llm_notes + ): for kegg_id in kegg_ids: if not kegg_id: continue @@ -304,9 +619,13 @@ def rank_kegg_annotations_with_llm( rows = result_df[mask] if rows.empty: continue - ranked_rows.append(rows.iloc[[0]]) + one = rows.iloc[[0]].copy() + one["llm_ranking_note"] = llm_note + ranked_rows.append(one) ranked_df = pd.concat(ranked_rows, ignore_index=True) if ranked_rows else result_df.iloc[0:0].copy() + if "llm_ranking_note" not in ranked_df.columns: + ranked_df["llm_ranking_note"] = "" base = Path(csv_path) if csv_path else Path(f"{Path(model_file).name}_recommendations") ranked_out_path = base.with_name(base.stem + "_llm_ranked.csv") @@ -314,3 +633,171 @@ def rank_kegg_annotations_with_llm( logger.info("LLM-ranked recommendations saved to %s", ranked_out_path) return ranked_df + + +# --------------------------------------------------------------------------- +# Reaction-KEGG curation entry point (no LLM) +# --------------------------------------------------------------------------- + +def curate_reactions_kegg_rulebased( + model_file: str, + existing_annotations: Dict[str, List[str]], + qualifier_annotations: Dict[str, Dict[str, str]], + specs_to_evaluate: List[str], + *, + evaluate_candidates: bool = False, + include_exchange_reactions: bool = False, + llm_model: str = "", + top_k: Optional[int] = None, + tax_id: Optional[str] = None, + start_time: Optional[float] = None, +) -> Union["AnnotationResult", Tuple[pd.DataFrame, Dict[str, Any]]]: + """Curate KEGG reaction annotations using the rule-based pipeline. + + Tries species ChEBI annotations first (full ChEBI->KEGG mapping + ontology + relaxation via :func:`run_kegg_annotation_workflow_rulebased`). If no ChEBI + species annotations are present, falls back to species annotated directly + with KEGG compound IDs (no ontology relaxation), feeding the species->KEGG + table straight into :func:`_get_kegg_recommendations_rulebased`. + + The result is filtered to ``specs_to_evaluate`` (the curation target set), + saved as ``_recommendations.csv``, and wrapped in an + :class:`AnnotationResult` with an empty LLM conversation (this path is + LLM-free). + + Returns an :class:`AnnotationResult` on success, or an + ``(empty_df, {"error": ...})`` tuple on early-exit error cases (to match + the error-return convention of :func:`curate_single_model`). + """ + from core.feedback import AnnotationResult, build_initial_conversation + + if start_time is None: + start_time = time.time() + + species_chebi = find_species_with_chebi_annotations(model_file) + kegg_recommendations_df: Optional[pd.DataFrame] = None + + if species_chebi: + # Preferred path: ChEBI species annotations drive the full ChEBI->KEGG + # mapping + ontology relaxation pipeline. + rows = [ + {"id": str(sid), "annotation": str(chebi), "match_score": 1.0} + for sid, chebis in species_chebi.items() + for chebi in chebis + if chebi + ] + species_recommendations_df = pd.DataFrame(rows) + if species_recommendations_df.empty: + return pd.DataFrame(), {"error": "Empty species ChEBI table"} + + result = run_kegg_annotation_workflow_rulebased( + model_file=model_file, + recommendations_df=species_recommendations_df, + existing_annotations=existing_annotations, + evaluate_candidates=bool(evaluate_candidates), + include_exchange_reactions=bool(include_exchange_reactions), + ) + if result is None: + return pd.DataFrame(), {"error": "Rulebased KEGG reaction curation failed"} + kegg_recommendations_df = result.kegg_recommendations + else: + # Fallback: species annotated with KEGG compound IDs directly. + # ChEBI-hierarchy relaxation isn't applicable, so we bypass + # ``map_reactions_to_kegg_with_relaxation`` and feed a direct + # species->KEGG mapping into the rule-based candidate generator. + species_kegg, _ = find_species_with_annotations_and_qualifiers( + model_file, DatabaseID.KEGG.value + ) + if not species_kegg: + logger.warning( + "No existing ChEBI or KEGG-compound species annotations found; " + "cannot run rulebased KEGG reaction curation." + ) + return pd.DataFrame(), { + "error": "No existing ChEBI or KEGG-compound species annotations found" + } + + logger.info( + "No ChEBI species annotations found; falling back to existing KEGG " + "compound species annotations (%d species).", + len(species_kegg), + ) + + id_rows = [ + {"id": str(sid), "KEGG_ID": str(kegg_id)} + for sid, kegg_ids in species_kegg.items() + for kegg_id in kegg_ids + if kegg_id + ] + id_df = pd.DataFrame(id_rows) + if id_df.empty: + return pd.DataFrame(), {"error": "Empty species KEGG compound table"} + + reaction_ids = get_all_reaction_ids(model_file) + rxn_list, _ = extract_reactions_from_sbml(model_file, list(id_df["id"].unique())) + + normalized_reactions = map_reactions_to_kegg( + rxn_list, reaction_ids, id_df, spectators=False + ) + + cofactor_config = CofactorConfig() + match_results = _get_kegg_recommendations_rulebased( + normalized_reactions, + cofactors_to_ignore=cofactor_config.kegg_ids, + top_k=None, + spectators=False, + evaluate_candidates=bool(evaluate_candidates), + include_exchange_reactions=bool(include_exchange_reactions), + ) + + rxn_model_info = extract_model_info(model_file, reaction_ids, EntityType.REACTION) + kegg_recommendations_df = _generate_recommendation_table( + model_file, + match_results, + existing_annotations, + rxn_model_info, + EntityType.REACTION.value, + DatabaseID.KEGG.value, + {}, + ) + + # Filter output down to reactions that had existing KEGG annotations (curation target set). + kegg_recommendations_df = kegg_recommendations_df[ + kegg_recommendations_df["id"].astype(str).isin(set(map(str, specs_to_evaluate))) + ].copy() + + total_time = time.time() - start_time + has_prediction = kegg_recommendations_df["annotation"].astype(str).str.strip() != "" + metrics = { + "total_time": total_time, + "total_entities": len(specs_to_evaluate), + "entities_with_predictions": int(has_prediction.sum()), + "annotation_rate": float(has_prediction.mean() if len(kegg_recommendations_df) else 0.0), + } + + csv_path = f"{Path(model_file).name}_recommendations.csv" + kegg_recommendations_df.to_csv(csv_path, index=False) + print(f"Recommendations saved to {csv_path}") + logger.info( + "Curation completed in %.2fs – %d recommendations", + total_time, + len(kegg_recommendations_df), + ) + + return AnnotationResult( + kegg_recommendations_df, + metrics, + model_file=model_file, + conversation_history=build_initial_conversation("", "", ""), + entities_to_evaluate=specs_to_evaluate, + entity_type=EntityType.REACTION, + database=DatabaseID.KEGG, + method="rulebased", + llm_model=llm_model, + top_k=top_k, + tax_id=tax_id, + existing_annotations=existing_annotations, + qualifier_annotations=qualifier_annotations, + model_info=extract_model_info(model_file, specs_to_evaluate, EntityType.REACTION), + csv_path=csv_path, + ) diff --git a/core/reaction/classification.py b/core/reaction/classification.py index 793edfe..bee8c78 100644 --- a/core/reaction/classification.py +++ b/core/reaction/classification.py @@ -15,10 +15,19 @@ def classify_reaction( """ Classify a reaction by score eligibility. + ``filtered_species`` is the set of KEGG compound IDs still in play after + cofactor filtering (non-ignored metabolites on this reaction). + Returns: - - "non_mappable": no species remain after filtering - - "ambiguous_mapping": mappable species exist but no KEGG candidates - - "mappable": eligible and has at least one candidate + - "ambiguous_mapping": ``filtered_species`` is empty (vacuous ``all()``), + or every ID in it is a configured cofactor KEGG id + (see :attr:`CofactorConfig.kegg_ids`). The transformation is + underdetermined for a unique KEGG reaction mapping. + - "non_mappable": the equation string has no ``->`` or ``=>``, an empty + LHS/RHS after splitting, or there are zero candidate KEGG reactions + after database filtering. + - "mappable": equation parses, at least one candidate KEGG reaction id, + and not classified as ambiguous_mapping above. """ filtered = list(filtered_species) if filtered_species is not None else [] cand = list(candidates) if candidates is not None else [] diff --git a/core/reaction/hierarchy_relaxation.py b/core/reaction/hierarchy_relaxation.py index c2acbf1..8472888 100644 --- a/core/reaction/hierarchy_relaxation.py +++ b/core/reaction/hierarchy_relaxation.py @@ -49,6 +49,8 @@ import pandas as pd +from .kegg_compound_ids import parse_kegg_compound_id + # Baseline vs leave-one-ChEBI-out scores for ``detect_problematic_metabolites``. # Call with ``exclude_chebi=None`` for full reaction; with a ChEBI id to drop # that term's contribution on both sides of the fingerprint. @@ -316,6 +318,11 @@ def chebi_best_kegg_meta_with_ontology_fallback( if not seed: return {} + # Species annotated with bare KEGG compound ids (instead of ChEBI): identity map. + kc = parse_kegg_compound_id(seed) + if kc: + return {kc: {"direction": "exact", "distance": 0}} + # Strict mapping first (no ontology walk). direct_keggs = kegg_ids_for_chebi_term(seed, chebi_to_kegg) if direct_keggs: @@ -414,6 +421,10 @@ def normalize_chebi( for whole reactions). For multi-metabolite ChEBI lists use ``normalize_reaction``. """ + kc_id = parse_kegg_compound_id(chebi_id) + if kc_id: + return {kc_id} + if level <= 0: return kegg_ids_for_chebi_term(chebi_id, chebi_to_kegg) diff --git a/core/reaction/kegg_compound_ids.py b/core/reaction/kegg_compound_ids.py new file mode 100644 index 0000000..0c64234 --- /dev/null +++ b/core/reaction/kegg_compound_ids.py @@ -0,0 +1,31 @@ +"""Helpers for bare KEGG compound identifiers (``C#####``) in species annotations. + +Some curated models annotate species with KEGG compound IDs instead of ChEBI. +Downstream code historically assumed ``annotation`` was always ChEBI; these +helpers detect when a string is already a KEGG compound id so it can be passed +through without ChEBI→KEGG table lookup or ontology walking. +""" + +from __future__ import annotations + +import re +from typing import Optional + +# KEGG compound ids are conventionally ``C`` + five digits (e.g. ``C00002``). +_KEGG_COMPOUND_ID_RE = re.compile(r"^C\d{5}$", re.IGNORECASE) + + +def parse_kegg_compound_id(annotation: object) -> Optional[str]: + """Return canonical ``C#####`` if ``annotation`` is a plain KEGG compound id. + + Accepts strings like ``c00002`` or ``C00002``. Returns ``None`` for ChEBI + terms, free text, or empty values. + """ + if annotation is None: + return None + s = str(annotation).strip() + if not s: + return None + if _KEGG_COMPOUND_ID_RE.fullmatch(s): + return s.upper() + return None diff --git a/core/reaction/kegg_features.py b/core/reaction/kegg_features.py index df994bf..d9cfe15 100644 --- a/core/reaction/kegg_features.py +++ b/core/reaction/kegg_features.py @@ -7,7 +7,7 @@ import pickle import re from pathlib import Path -from typing import Dict +from typing import Dict, FrozenSet, List, Tuple from utils.constants import REF_KEGG_REACTION_FEATURES @@ -15,6 +15,18 @@ logger = logging.getLogger(__name__) +# A KEGG reaction id like ``R01600``. +_R_NUMBER_RE = re.compile(r"\bR\d{5}\b") + +# BRITE section header marker. Examples we've seen in the data: +# ``Enzymatic reactions [BR:br08201]`` +# ``IUBMB reaction hierarchy [BR:br08202]`` +# ``Overall reaction [br08210.html]`` +_BRITE_SECTION_RE = re.compile(r"\[(?:BR:)?br\d+(?:\.html)?\]", re.IGNORECASE) + +# IUBMB-block tag we care about for hierarchical grouping. +_IUBMB_TAG = "br08202" + def _normalize_kegg_reaction_id(annotation) -> str: """Resolve KEGG reaction id (R#####) from table/URI values like ``KEGG:R01600``.""" @@ -85,6 +97,99 @@ def get_definition(self, annotation: str) -> str: self._cache[key] = result return result + def get_iubmb_chains(self, annotation: str) -> Tuple[Tuple[str, ...], ...]: + """Per-EC ancestor chains for a reaction from its IUBMB BRITE hierarchy. + + Parses the ``IUBMB reaction hierarchy [BR:br08202]`` block of the + reaction's BRITE field. Within that block KEGG groups R-numbers by EC + sub-section (e.g. ``2.7.1.1`` followed by an ordered list of R-numbers + that walks the IUBMB tree from root to self). + + Returns a tuple of chains; each chain is an ordered tuple of bare + KEGG reaction ids. The same reaction may appear in multiple chains + (one per EC under which it's classified). Reactions without a + ``br08202`` block return an empty tuple. + + Examples (against KEGG snapshot bundled in ``data/kegg/``):: + + R02848 -> ((R02848,),) + R00299 -> ((R02848, R00299), (R00299,)) + R01786 -> ((R02848, R01786), (R00299, R01786)) + R01068 -> ((R01068,),) + R01070 -> ((R01068, R01070),) + R10049 -> () + """ + key = ("iubmb_chains", annotation) + cached = self._cache.get(key) + if cached is not None: + return cached + + kegg_id = _normalize_kegg_reaction_id(annotation) + if not kegg_id: + self._cache[key] = () + return () + + brite = (self._features.get(kegg_id, {}) or {}).get("BRITE", "") or "" + if not brite: + self._cache[key] = () + return () + + in_iubmb = False + chains: List[Tuple[str, ...]] = [] + current: List[str] = [] + + def _flush() -> None: + if current: + chains.append(tuple(current)) + current.clear() + + for raw_line in str(brite).splitlines(): + line = raw_line.strip() + if _BRITE_SECTION_RE.search(raw_line): + # Entering a new section ends the current chain regardless of + # whether we're leaving or staying inside br08202. + _flush() + in_iubmb = _IUBMB_TAG in raw_line.lower() + continue + if not in_iubmb or not line: + continue + # EC / hierarchy headers like "4.1.2.13" or "2. Transferase reactions" + # delimit chains within the IUBMB block. + if line[0].isdigit(): + _flush() + continue + r_ids = _R_NUMBER_RE.findall(raw_line) + if r_ids: + # Each KEGG line lists at most one R-number; take the first. + current.append(r_ids[0]) + + _flush() + result: Tuple[Tuple[str, ...], ...] = tuple(chains) + self._cache[key] = result + return result + + def get_iubmb_ancestors(self, annotation: str) -> FrozenSet[str]: + """Set of R-numbers above this reaction in its IUBMB BRITE hierarchy. + + Equal to the union of every chain returned by + :meth:`get_iubmb_chains` minus the reaction's own id. + """ + key = ("iubmb_ancestors", annotation) + cached = self._cache.get(key) + if cached is not None: + return cached + kegg_id = _normalize_kegg_reaction_id(annotation) + if not kegg_id: + self._cache[key] = frozenset() + return frozenset() + ancestors: set = set() + for chain in self.get_iubmb_chains(annotation): + ancestors.update(chain) + ancestors.discard(kegg_id) + result = frozenset(ancestors) + self._cache[key] = result + return result + @classmethod def load_from_file(cls, data_path: str) -> "KEGGReactionFeatures": # Allow callers to pass a bare filename (historical default). If the file diff --git a/core/reaction/matching.py b/core/reaction/matching.py index cc77294..ba4e054 100644 --- a/core/reaction/matching.py +++ b/core/reaction/matching.py @@ -221,8 +221,15 @@ def map_metabolites_to_kegg( 'candidates': choices } except (KeyError, IndexError): + # Keep unmapped metabolites so downstream logic can: + # - preserve stoichiometry (coeff) + # - perform ChEBI-based recovery/relaxation to find candidates later logger.debug(f"No KEGG mapping found for metabolite: {met}") - continue + id_choices[met] = { + "species_id": met, + "coeff": coeff, + "candidates": [], + } if not id_choices: return [] diff --git a/core/reaction/relaxation_workflow.py b/core/reaction/relaxation_workflow.py index 8026e2e..49df594 100644 --- a/core/reaction/relaxation_workflow.py +++ b/core/reaction/relaxation_workflow.py @@ -49,7 +49,6 @@ def _per_reaction_best_scores(match_results: List[Any]) -> Dict[str, Optional[fl best_by_rxn: Dict[str, float] = {} classification_by_rxn: Dict[str, str] = {} ambiguous_default_by_rxn: Dict[str, float] = {} - failed_default_by_rxn: Dict[str, float] = {} for rec in match_results: rid = rec.id meta = getattr(rec, "metadata", None) or {} @@ -57,8 +56,6 @@ def _per_reaction_best_scores(match_results: List[Any]) -> Dict[str, Optional[fl classification_by_rxn[rid] = rtype if rtype == "ambiguous_mapping": ambiguous_default_by_rxn[rid] = float(meta.get("ambiguous_default_score", 0.0)) - if rtype == "failed_mapping": - failed_default_by_rxn[rid] = float(meta.get("failed_default_score", 0.0)) if rtype != "mappable": continue if not rec.match_score: @@ -76,9 +73,6 @@ def _per_reaction_best_scores(match_results: List[Any]) -> Dict[str, Optional[fl if rtype == "ambiguous_mapping": out[rid] = float(ambiguous_default_by_rxn.get(rid, 0.0)) continue - if rtype == "failed_mapping": - out[rid] = float(failed_default_by_rxn.get(rid, 0.0)) - continue if rid in best_by_rxn: out[rid] = float(best_by_rxn[rid]) return out @@ -386,6 +380,8 @@ def map_reactions_to_kegg_with_relaxation( top_k: Optional[int] = None, penalty_lam: float = 0.1, run_matching: bool = True, + evaluate_candidates: bool = True, + include_exchange_reactions: bool = False, ) -> Tuple[List[Dict[str, Any]], List[Any], Dict[str, int]]: """ Single iterative loop: normalize -> penalized KEGG matching -> relax targets -> converge. @@ -422,6 +418,13 @@ def map_reactions_to_kegg_with_relaxation( score per reaction changes by less than this vs the previous iteration. run_matching: If False, performs a single mapping pass and returns an empty match list (no refinement). + evaluate_candidates: If False, run only candidate generation/filtering in + ``_get_kegg_recommendations_rulebased`` (no similarity scoring / objective + ranking). This disables relaxation iterations; the function returns after + one normalized mapping pass. + include_exchange_reactions: If True, attempt candidate generation for exchange + reactions (empty LHS or RHS). If False, exchange reactions are retained but + returned with no candidates. Returns: (normalized_reactions, kegg_match_results, species_relax_level_by_id) @@ -491,6 +494,8 @@ def compute_global_score(levels: Mapping[str, int]) -> float: cofactors_to_ignore=cofactors, top_k=top_k, spectators=spectators, + evaluate_candidates=True, + include_exchange_reactions=include_exchange_reactions, relaxation_levels_by_entity=levels, penalty_lam=penalty_lam, max_relax_level=max_relax_level, @@ -567,12 +572,14 @@ def compute_global_score(levels: Mapping[str, int]) -> float: match_results = [] break - # --- Step 2: KEGG matching (raw similarity inside matcher; ranking = penalized only) --- + # --- Step 2: KEGG matching --- match_results = _get_kegg_recommendations_rulebased( normalized_reactions, cofactors_to_ignore=cofactors, top_k=top_k, spectators=spectators, + evaluate_candidates=evaluate_candidates, + include_exchange_reactions=include_exchange_reactions, relaxation_levels_by_entity=relax_level, penalty_lam=penalty_lam, max_relax_level=max_relax_level, @@ -583,6 +590,9 @@ def compute_global_score(levels: Mapping[str, int]) -> float: max_ancestor_depth=max_ancestor_depth, max_descendant_depth=down_depth, ) + if not evaluate_candidates: + # Generation-only mode: do not score, do not relax, return after first pass. + break score = _aggregate_best_penalized_scores(match_results) coverage = _reaction_coverage_stats(match_results) logger.info(f"Reaction coverage: {coverage}") diff --git a/core/reaction/utils.py b/core/reaction/utils.py index d7cb336..e27c187 100644 --- a/core/reaction/utils.py +++ b/core/reaction/utils.py @@ -13,6 +13,7 @@ chebi_best_kegg_ids_with_ontology_fallback, load_chebi_parent_map, ) +from .kegg_compound_ids import parse_kegg_compound_id from .kegg_definition import extract_classifications logger = logging.getLogger(__name__) @@ -55,10 +56,11 @@ def map_chebi_to_kegg( max_descendant_depth: int = 1, ) -> Tuple[pd.DataFrame, pd.DataFrame]: """ - Map ChEBI IDs in recommendation rows to KEGG compound IDs via - ``load_chebi2kegg_dict`` with an ontology walk fallback when no direct - KEGG mapping exists for a given ChEBI term. - + Map species ``annotation`` values to KEGG compound IDs via + ``load_chebi2kegg_dict`` when those values are ChEBI terms, or treat bare + KEGG compound ids (``C#####``) as identity mappings when species were + annotated with KEGG compounds instead of ChEBI. + Duplicate rows are emitted when one ChEBI maps to multiple KEGG compounds. For reaction matching with optional upward relaxation along ChEBI ``is_a``, use ``hierarchy_relaxation.normalize_chebi`` / ``normalize_reaction`` instead. @@ -111,6 +113,14 @@ def _kegg_ids_for_chebi(seed_chebi_id: str) -> List[str]: if not recommendations_df.empty and 'annotation' in recommendations_df.columns: for _, row in recommendations_df.iterrows(): chebi_id = row['annotation'] + + direct_kegg = parse_kegg_compound_id(chebi_id) + if direct_kegg: + row_copy = row.copy() + row_copy["KEGG_ID"] = direct_kegg + expanded_rows.append(row_copy) + continue + kegg_ids = _kegg_ids_for_chebi(chebi_id) if not kegg_ids: @@ -133,14 +143,16 @@ def _kegg_ids_for_chebi(seed_chebi_id: str) -> List[str]: if expanded_df.empty: recommendations_df['KEGG_ID'] = "" - return recommendations_df, pd.DataFrame() + empty_cols = list(recommendations_df.columns) + return recommendations_df, pd.DataFrame(columns=empty_cols) combined_df = pd.concat([recommendations_df, expanded_df]).drop_duplicates( subset=dedup_cols ) else: recommendations_df['KEGG_ID'] = "" - return recommendations_df, pd.DataFrame() + empty_cols = list(recommendations_df.columns) + return recommendations_df, pd.DataFrame(columns=empty_cols) filtered_df = combined_df[ combined_df['KEGG_ID'].notna() & @@ -152,7 +164,7 @@ def _kegg_ids_for_chebi(seed_chebi_id: str) -> List[str]: filtered_df['match_score'] == filtered_df.groupby('id')['match_score'].transform('max') ].reset_index(drop=True) else: - high_score_recommendations = pd.DataFrame() + high_score_recommendations = pd.DataFrame(columns=list(combined_df.columns)) logger.info(f"Expanded {len(recommendations_df)} ChEBI entries to {len(expanded_df)} KEGG mappings") logger.info(f"Found {len(filtered_df)} valid KEGG mappings") diff --git a/examples/kegg_rulebased_annotation_example.py b/examples/kegg_rulebased_annotation_example.py index 13b45db..21c3c4f 100644 --- a/examples/kegg_rulebased_annotation_example.py +++ b/examples/kegg_rulebased_annotation_example.py @@ -41,13 +41,19 @@ recommendations_df = pd.read_csv("./examples/glycolysis_part1-recommendations.csv") TOP_K = 10 +# If False (default), run generation-only (no scoring / participant updates). +# If True, run the full evaluation workflow (scoring + EM-like participant updates). +RUN_EVALUATION = False -def main() -> None: +# If True, attempt candidate generation for exchange reactions (empty LHS or RHS). +# If False (default), exchange reactions are retained but returned with no candidates. +INCLUDE_EXCHANGE_REACTIONS = False + +def main() -> pd.DataFrame: logger.info("AAAIM KEGG Reaction Annotation Example") logger.info("=" * 50) - # ── Example 1: Rule-based KEGG annotation workflow ────────────────────────────── _annotation_result, _metrics = annotate_model( model_file=model_file, llm_model=llm_model, @@ -56,6 +62,8 @@ def main() -> None: database="kegg", top_k=TOP_K, species_recommendations_df=recommendations_df, + evaluate_candidates=RUN_EVALUATION, + include_exchange_reactions=INCLUDE_EXCHANGE_REACTIONS, ) csv_path = Path(f"{Path(model_file).name}_recommendations.csv") diff --git a/tests/aaaim_evaluation_rulebased_kegg.ipynb b/tests/aaaim_evaluation_rulebased_kegg.ipynb new file mode 100644 index 0000000..6e0498d --- /dev/null +++ b/tests/aaaim_evaluation_rulebased_kegg.ipynb @@ -0,0 +1,4225 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "6aaf814e", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import pandas as pd\n", + "pd.set_option('display.max_columns', None)\n", + "pd.set_option('display.max_rows', None)\n", + "import matplotlib.pyplot as plt\n", + "import tellurium as te" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "59ee168f", + "metadata": {}, + "outputs": [], + "source": [ + "from analyze_reaction_complexity_from_eval import run_complexity_analysis" + ] + }, + { + "cell_type": "markdown", + "id": "39a1cbcc", + "metadata": {}, + "source": [ + "## Llama-3.3-70B-Instruct" + ] + }, + { + "cell_type": "markdown", + "id": "b98832b7", + "metadata": {}, + "source": [ + "### Curated species" + ] + }, + { + "cell_type": "code", + "execution_count": 115, + "id": "99f3ebb7", + "metadata": {}, + "outputs": [], + "source": [ + "llm_model = \"Llama-3.3-70B-Instruct\"\n", + "folder = \"reaction_evaluation_results/curated_species/Llama-3.3-70B-Instruct-20260507_173803\"" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "id": "3dbe0a1b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Percentage of reactions that received at least one candidate: 67.0 %\n", + "Ground truth annotation within top 1: 63.8 %\n", + "Ground truth annotation within top 3: 64.6 %\n", + "Ground truth annotation within top 5: 64.6 %\n" + ] + } + ], + "source": [ + "# with curated species annotations\n", + "summary_df = pd.read_csv(f'./{folder}/per_reaction_results.csv')\n", + "\n", + "# Annotation coverage: % of reactions that receive ≥1 candidate\n", + "print(\"Percentage of reactions that received at least one candidate: \", round(len(summary_df[summary_df['num_candidates']>0])/len(summary_df)*100,1), \"%\")\n", + "# top-1 accuracy:\n", + "print(\"Ground truth annotation within top 1: \", round(len(summary_df[summary_df['top1']==True])/len(summary_df)*100,1), \"%\")\n", + "# top-3 accuracy:\n", + "print(\"Ground truth annotation within top 3: \", round(len(summary_df[summary_df['top3']==True])/len(summary_df)*100,1), \"%\")\n", + "# top-5 accuracy:\n", + "print(\"Ground truth annotation within top 5: \", round(len(summary_df[summary_df['top5']==True])/len(summary_df)*100,1), \"%\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "0591613d", + "metadata": {}, + "source": [ + "#### Failure reasons" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "id": "4106f4d1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "failure_reason\n", + "no_candidates 1914\n", + "SSX 11\n", + "Name: count, dtype: int64" + ] + }, + "execution_count": 64, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Failure reasons\n", + "failure_reasons = summary_df[summary_df[\"failure_reason\"].notna()]\n", + "failure_reasons[\"failure_reason\"].value_counts()" + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "id": "e8d0c234", + "metadata": {}, + "outputs": [], + "source": [ + "failure_reasons = summary_df[summary_df[\"failure_reason\"].notna()]\n", + "\n", + "results = []\n", + "\n", + "for _, row in failure_reasons.iterrows():\n", + " if row[\"failure_reason\"] != \"no_candidates\":\n", + " continue\n", + "\n", + " model_id = row[\"model_id\"]\n", + " reaction_id = row[\"reaction_id\"]\n", + " gt = row[\"ground_truth_kegg\"] # e.g. \"R01845\"\n", + " gt_formatted = f\"KEGG:{gt}\"\n", + "\n", + " file_path = os.path.join(folder, \"_work\", f\"{model_id}.xml_recommendations.csv\")\n", + "\n", + " if not os.path.exists(file_path):\n", + " label = \"file not found\"\n", + " else:\n", + " rec_df = pd.read_csv(file_path)\n", + "\n", + " sub = rec_df[rec_df[\"id\"] == reaction_id]\n", + "\n", + " if sub.empty:\n", + " label = \"reaction not found in recommendations\"\n", + " else:\n", + " if sub[\"annotation\"].astype(str).str.contains(gt_formatted).any():\n", + " label = \"LLM did not recognize correct annotation\"\n", + " else:\n", + " label = \"no correct candidates were generated\"\n", + "\n", + " results.append({\n", + " \"model_id\": model_id,\n", + " \"reaction_id\": reaction_id,\n", + " \"ground_truth_kegg\": gt,\n", + " \"label\": label\n", + " })\n", + "\n", + "output_df = pd.DataFrame(results)\n", + "output_df.to_csv(f\"./{folder}/failure_reasons.csv\", index=False)" + ] + }, + { + "cell_type": "markdown", + "id": "8ac67434", + "metadata": {}, + "source": [ + "#### Complexity analysis" + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "id": "59250ec0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Wrote C:\\Users\\user\\Documents\\research\\AAAIM\\tests\\reaction_evaluation_results\\curated_species\\Llama-3.3-70B-Instruct-20260507_173803\\per_reaction_with_complexity.csv\n", + "Wrote C:\\Users\\user\\Documents\\research\\AAAIM\\tests\\reaction_evaluation_results\\curated_species\\Llama-3.3-70B-Instruct-20260507_173803\\complexity_impact_summary.csv\n", + "complexity_bin n_reactions coverage top1 top3 top5 mean_candidates\n", + " 1 17 0.588 0.588 0.588 0.588 0.588\n", + " 2 570 0.719 0.689 0.719 0.719 1.218\n", + " 3 717 0.704 0.683 0.703 0.704 1.064\n", + " 4 1961 0.643 0.635 0.642 0.643 0.886\n", + " 5+ 2573 0.618 0.615 0.618 0.618 0.857\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
complexity_binn_reactionscoveragetop1top3top5mean_candidates
01170.5880.5880.5880.5880.588
125700.7190.6890.7190.7191.218
237170.7040.6830.7030.7041.064
3419610.6430.6350.6420.6430.886
45+25730.6180.6150.6180.6180.857
\n", + "
" + ], + "text/plain": [ + " complexity_bin n_reactions coverage top1 top3 top5 mean_candidates\n", + "0 1 17 0.588 0.588 0.588 0.588 0.588\n", + "1 2 570 0.719 0.689 0.719 0.719 1.218\n", + "2 3 717 0.704 0.683 0.703 0.704 1.064\n", + "3 4 1961 0.643 0.635 0.642 0.643 0.886\n", + "4 5+ 2573 0.618 0.615 0.618 0.618 0.857" + ] + }, + "execution_count": 69, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = run_complexity_analysis(f\"./tests/{folder}/per_reaction_results.csv\") # take 64min\n", + "result.per_reaction.to_csv(f'./{folder}/complexity_list.csv', index=False)\n", + "result.summary" + ] + }, + { + "cell_type": "markdown", + "id": "3dcba36a", + "metadata": {}, + "source": [ + "look at each model, and check the percentage of reactions whose ground truth annotation was found. " + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "id": "56d6b8db", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "On average, 79.8% of reactions in each model were annotated correctly.\n" + ] + } + ], + "source": [ + "pct_found = (\n", + " summary_df\n", + " .sort_values(\"model_id\")\n", + " .groupby(\"model_id\")[\"found\"]\n", + " .mean()\n", + " .mul(100)\n", + " .reset_index(name=\"pct_found\")\n", + ").round(1)\n", + "\n", + "df_llm3 = pct_found\n", + "\n", + "avg_annotated_correctly = round(pct_found['pct_found'].mean(), 1)\n", + "print(f\"On average, {avg_annotated_correctly}% of reactions in each model were annotated correctly.\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "cf5de260", + "metadata": {}, + "source": [ + "Which models did AAAIM fail at annotating?" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "id": "a9f4f06b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
model_idpct_found
19BIOMD00000000880.0
25BIOMD00000001370.0
52BIOMD00000002920.0
65BIOMD00000006910.0
\n", + "
" + ], + "text/plain": [ + " model_id pct_found\n", + "19 BIOMD0000000088 0.0\n", + "25 BIOMD0000000137 0.0\n", + "52 BIOMD0000000292 0.0\n", + "65 BIOMD0000000691 0.0" + ] + }, + "execution_count": 71, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pct_found[pct_found['pct_found']==0]" + ] + }, + { + "cell_type": "markdown", + "id": "198442e1", + "metadata": {}, + "source": [ + "Make a histogram of successfully annotated models" + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Distribution of Percentages\\n Llama-3.3-70B-Instruct with curated species')" + ] + }, + "execution_count": 72, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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AADPIpABAACeRSADAADO/V5LtWvXDntOpZUrV55NmgAAACIbyLRq1SrcRQEAADJWINO3b9/opgQAACCNaCMDAADOr5F9NZfS0KFDzaeffmq2bt1qTp48GfD+/v37I5U+AACAyJbI9O/f3wwZMsTcfffd5tChQ+app54yt99+u8mcObPp169felYJAAAQm0BmwoQJ5t133zVPP/20yZo1q2nbtq157733zEsvvWSWLFkS9noGDRpk6tSpY/LmzWuKFi1qGxSvX78+YJnjx4+bLl26mEKFCpk8efKYNm3amF27dqUn2QAA4ByTrkBm586d5tJLL7X/V3ChUhm59dZbzTfffBP2ehYuXGiDFAU/c+bMMadOnTJNmjQxR48e9S3To0cP89VXX5nPPvvMLr99+3Zb+gMAAJCuNjKlS5c2O3bsMGXLljUVK1Y0s2fPNpdffrlZtmyZyZEjR9jrmTlzZsDz8ePH25KZFStWmOuuu84GSO+//76ZOHGiuemmm+wy48aNM9WqVbPBz9VXX52e5AMAgPM5kGndurWZO3euueqqq0y3bt3MfffdZwMONfxVCUp6uSU7BQsWtH8V0KiUplGjRr5lqlatagOohISEkIHMiRMn7MOVmJho/2o9ekSKu65IrhOhkdexQT7HBvkcG+RzbEUjn8NdZybHcZyz/TKVjvzwww+mcuXKpkWLFulax5kzZ8xtt91mDh48aBYvXmxfU0nMgw8+GBCYSN26dc2NN95oXnvttSTrUWNjNUYOpnXlzp07XWkDAMDruiekq+wiVcPr/RuV9R47dsy0a9fOFnLky5cv2eXStVWLFi0y9evXtw19RSUjevz777/2PVULpZXayqxdu9YXxKRX7969bS8q/xKZMmXK2LY3KWVEeiJFtetp3LixyZYtW8TWi6TI69ggn2ODfI4N8jmp7gmzTbREI5/dGpXUpCuQUWmI2sioPYs/RU16T+PMpEXXrl3N119/bYMgtb9xFS9e3I5Ro1KaAgUK+F5XryW9F4ra6IRqp6MMjsbBHK31IinyOjbI59ggn2ODfPZuPoe7vnT1WlJtVKgJJPft22cuuOCCNK1HQcy0adPMvHnzTIUKFQLev+KKK+yGqD2OS92z1RanXr166Uk6AAA4h6SpRMbt9qwgpkOHDgElHyqFWbNmja1ySkt1ktqufPHFF3YsGXXrlvz585tcuXLZvx07drRVRWoArKohNS5WEEOPJQAAkKZARoGFW5KiwEPBhit79uw2uOjUqVPY6xs1apT9e8MNNwS8ri7WCpREUyFoxGANhKdGv02bNjUjR45MS7IBAMA5Kk2BjAIMKV++vHnmmWfSVI0USjgdpnLmzGlGjBhhHwAAAGfd2Ldv37727549e3xTClSpUsUUKVIkPasDAABIl8zp7dv90EMPmRIlStiu1nqULFnStmfRewAAABk2kNHovZr3SHMgqWu0Hmqwq9c0kSQAAECGrVr6/PPPzZQpUwIa6TZv3tw2/r3rrrt8jXgBAAAyZNVSsWLFkryuAfKoWgIAABk6kNE4Lmrwe/z4cd9r//zzj53jiIHqAABAhq5aGjZsmLn55pvtdAK1atWyr61evdp2lZ41a1ak0wgAABC5QObSSy81GzZsMBMmTDDr1q2zr7Vt29bce++9AYPkAQAARNNZzX4dPIrv2cx+DQAAEJM2Mprhev/+/Uled2e/BgAAiIW4zn4NAADg2dmvAQAAPDv7NQAAgGdnvwYAAIjb7NcAAACeaOyrAfCWLFmS6nKHDx82r732mhkxYsTZpg0AACAyJTJ33nmnadOmjW0n06JFC3PllVeakiVL2tF8Dxw4YH799VezePFi8+2335pbbrnFDB48ONxVAwAARDeQ6dixo7nvvvvMZ599ZiZPnmzGjBljx41xezFVr17dNG3a1CxbtsxUq1YtfakBAACIVhsZdbdWMKOHKJDRZJGFChUy2bJlS8uqAAAA4tPY16VqJrdLNgAAgCdG9gUAAMgICGQAAIBnEcgAAADPIpABAADnVyBz0UUX2Zmugx08eNC+BwAAkGEDmT/++MPOdh3sxIkTZtu2bZFIFwAAQGS7X3/55Ze+/8+aNSug67UCm7lz59oJJQEAADJcINOqVSvfSL7t27cPeE8D4imIeeONNyKbQgAAgEgEMmfOnLF/K1SoYKciKFy4cFo+DgAAEP+Rfbds2RLZVAAAAMRyigK1h9Fj9+7dvpIa19ixY9O7WgAAgOgGMv379zcDBgwwV155pSlRooRtMwMAAOCJQGb06NFm/Pjx5v777498igAAAKI5jszJkydN/fr10/NRAACA+AYyDz/8sJk4cWLkUgEAABCrqqXjx4+bMWPGmO+++87UrFnTjiHjb8iQIelZLQAAQPQDmTVr1pjLLrvM/n/t2rUB79HwFwAAZOhAZv78+RH58kWLFpnBgwebFStWmB07dphp06b5Rg+WDh06mA8++CDgM02bNjUzZ86MyPcDAIDzsI1MpBw9etTUqlXLjBgxItllbr75ZhvkuI9PPvkkpmkEAADnWInMjTfemGIV0rx588JaT7NmzewjJTly5DDFixdPcxoBAMC5L12BjNs+xnXq1CmzatUq214meDLJs7VgwQJTtGhRc+GFF5qbbrrJvPLKK6ZQoULJLn/ixAn7cCUmJvrSqEekuOuK5DoRGnkdG+RzbJDPsUE+x1Y08jncdWZyHMeJ1Jf269fPHDlyxPz3v/9N82dVwhPcRmbSpEkmd+7cdpLKTZs2meeff97kyZPHJCQkmCxZsiSbBo08HEzdxbUuAADOR90T0j0rUYqG1/s3Kus9duyYadeunTl06JDJly9fbAKZjRs3mrp165r9+/dHJJAJtnnzZlOxYkXb7bthw4Zhl8iUKVPG7N27N8WMSE+kOGfOHNO4ceMk3c8RWeR1bJDPsUE+xwb5nFTlPrNNtAKZaOSzrt+FCxdONZCJaHimkpKcOXOaaLnooovsRilgSi6QUZsaPYIpg6NxMEdrvUiKvI4N8jk2yOfYIJ+9m8/hri9dgcztt98e8FyFOupRtHz5ctOnTx8TLX///bfZt2+fnagSAAAgXYFM/vz5A55nzpzZVKlSxc6I3aRJk7DXo/Y0Kl1xbdmyxTYaLliwoH2orUubNm1sryW1kenZs6epVKmSHUsGAAAgXYHMuHHjIvLlKsFRV27XU089Zf+q59OoUaPsCMIaEO/gwYOmZMmSNkh6+eWXQ1YdAQCA889ZtZHRiLy//fab/X+NGjVM7dq10/T5G264wVZLJWfWrFlnkzwAAHCOS1cgs3v3bnPPPffYMV4KFChgX1OpiUpX1GW6SJEikU4nAABAZKYo6Natmzl8+LD55ZdfbFdrPTQYnrpKPfHEE+lZJQAAQGxKZDRpo8ZyqVatmu+16tWr2zmT0tLYFwAAIOYlMmfOnAnZv1uv6T0AAIAMG8hozqPu3bub7du3+17btm2b6dGjR7ID1QEAAGSIQObtt9+27WHKly9vpwzQQ/Mh6bW33nor4okEAACIWBsZzV20cuVK205m3bp19jW1l2nUqFF6VgcAABDbcWQ0yaMmidIDAAAgw1ctzZs3z/ZOUhVSMM1OqUHxvv/++0imDwAAIDKBzLBhw0ynTp1CTqet+ZceffRRM2TIkLSsEgAAIDaBzOrVq83NN9+c7PsaQ0bTFgAAAGS4QGbXrl0hx49xZc2a1ezZsycS6QIAAIhsIFOqVCk7FUFyNFt1iRIl0rJKAACA2AQyzZs3N3369DHHjx9P8t4///xj+vbta2699db0pwYAACBa3a9ffPFFM3XqVHPxxRebrl27mipVqtjXNZaM5lk6ffq0eeGFF9KySgAAgNgEMsWKFTM//PCD6dy5s+ndu7dxHMc3pkzTpk1tMKNlAAAAMuSAeOXKlTPffvutOXDggNm4caMNZipXrmwuvPDC6KQQAAAg0iP7KnCpU6dOej8OAAAQn0kjAQAAMgICGQAA4FkEMgAAwLMIZAAAgGcRyAAAAM8ikAEAAJ5FIAMAADyLQAYAAHgWgQwAAPAsAhkAAOBZBDIAAMCzCGQAAIBnEcgAAADPIpABAACeRSADAAA8i0AGAAB4FoEMAADwLAIZAADgWXENZBYtWmRatGhhSpYsaTJlymSmT58e8L7jOOall14yJUqUMLly5TKNGjUyGzZsiFt6AQBAxhLXQObo0aOmVq1aZsSIESHff/31182bb75pRo8ebZYuXWouuOAC07RpU3P8+PGYpxUAAGQ8WeP55c2aNbOPUFQaM2zYMPPiiy+ali1b2tc+/PBDU6xYMVtyc88998Q4tQAAIKPJsG1ktmzZYnbu3Gmrk1z58+c3V111lUlISIhr2gAAQMYQ1xKZlCiIEZXA+NNz971QTpw4YR+uxMRE+/fUqVP2ESnuuiK5ToRGXscG+Rwb5HNskM+xFY18DnedGTaQSa9BgwaZ/v37J3l99uzZJnfu3BH/vjlz5kR8nQiNvI4N8jk2yOfYIJ9jc8mPRj4fO3bM24FM8eLF7d9du3bZXksuPb/sssuS/Vzv3r3NU089FVAiU6ZMGdOkSROTL1++iEaK2nGNGzc22bJli9h6kRR5HRvkc2yQz7FBPifVPWG2iZZo5LNbo+LZQKZChQo2mJk7d64vcNFGqfdS586dk/1cjhw57COYMjgaB3O01oukyOvYIJ9jg3yODfLZu/kc7vriGsgcOXLEbNy4MaCB76pVq0zBggVN2bJlzZNPPmleeeUVU7lyZRvY9OnTx44506pVq3gmGwAAZBBxDWSWL19ubrzxRt9zt0qoffv2Zvz48aZnz552rJlHHnnEHDx40FxzzTVm5syZJmfOnHFMNQAAyCjiGsjccMMNdryY5Gi03wEDBtgHAACAZ8aRAQAASA2BDAAA8CwCGQAA4FkEMgAAwLMIZAAAgGcRyAAAAM8ikAEAAJ5FIAMAADyLQAYAAHgWgQwAAPAsAhkAAOBZBDIAAMCzCGQAAIBnxXX2awAA8P+Uf+6beCfBcyiRAQAAnkUgAwAAPItABgAAeBaBDAAA8CwCGQAA4FkEMgAAwLMIZAAAgGcRyAAAAM8ikAEAAJ5FIAMAADyLQAYAAHgWgQwAAPAsAhkAAOBZBDIAAMCzCGQAAIBnEcgAAADPIpABAACeRSADAAA8i0AGAAB4FoEMAADwLAIZAADgWQQyAADAswhkAACAZ2XoQKZfv34mU6ZMAY+qVavGO1kAACCDyGoyuBo1apjvvvvO9zxr1gyfZAAAECMZPipQ4FK8ePF4JwMAAGRAGT6Q2bBhgylZsqTJmTOnqVevnhk0aJApW7ZsssufOHHCPlyJiYn276lTp+wjUtx1RXKdCI28jg3yOTbI59ggn2MrGvkc7jozOY7jmAxqxowZ5siRI6ZKlSpmx44dpn///mbbtm1m7dq1Jm/evMm2q9FywSZOnGhy584dg1QDANKie0J07qmH1/vXeE208iKaopXPx44dM+3atTOHDh0y+fLl82YgE+zgwYOmXLlyZsiQIaZjx45hl8iUKVPG7N27N8WMSE+kOGfOHNO4cWOTLVu2iK0XSZHXsUE+xwb5nFTlPrOjdoH1Wj5HKy+iKVr5rOt34cKFUw1kPBX6FShQwFx88cVm48aNyS6TI0cO+wimDI7GwRyt9SIp8jo2yOfYIJ9jg3z2bj6Hu74M3f06mKqZNm3aZEqUKBHvpAAAgAwgQwcyzzzzjFm4cKH5448/zA8//GBat25tsmTJYtq2bRvvpAEAgAwgQ1ct/f333zZo2bdvnylSpIi55pprzJIlS+z/AQAAMnQgM2nSpHgnAQAAZGAZumoJAAAgJQQyAADAswhkAACAZ2XoNjJeGIGxe0J0Bi/649VborJeAMDZK//cN/FOAv4PJTIAAMCzCGQAAIBnEcgAAADPIpABAACeRSADAAA8i0AGAAB4FoEMAADwLAIZAADgWQyIB3hswCwGSwxEPgPnN0pkAACAZxHIAAAAzyKQAQAAnkUgAwAAPItABgAAeBaBDAAA8CwCGQAA4FkEMgAAwLMIZAAAgGcxsi8AIG4jKANnixIZAADgWQQyAADAswhkAACAZxHIAAAAzyKQAQAAnkUgAwAAPItABgAAeBaBDAAA8CwGxINHBszKaronzI7i+r0jmvk8vF501uvFwdS8mM9ezeto6Z7AeeN8QIkMAADwLAIZAADgWQQyAADAswhkAACAZ3kikBkxYoQpX768yZkzp7nqqqvMjz/+GO8kAQCADCDDBzKTJ082Tz31lOnbt69ZuXKlqVWrlmnatKnZvXt3vJMGAADiLMMHMkOGDDGdOnUyDz74oKlevboZPXq0yZ07txk7dmy8kwYAAOIsQwcyJ0+eNCtWrDCNGjXyvZY5c2b7PCEhIa5pAwAA8ZehB8Tbu3evOX36tClWrFjA63q+bt26kJ85ceKEfbgOHTpk/+7fv9+cOnUqYmnTus6cOGaiZd++fcZropkfiI1jx/61x162bNkiul6Ojdjks5DXOFeO58OHD9u/juN4N5BJj0GDBpn+/fsneb1ChQrGSwoPi3cKcD5qF+8EnCfIZ5xL2kV5/Qpo8ufP781ApnDhwiZLlixm165dAa/refHixUN+pnfv3rZxsOvMmTO2NKZQoUImU6ZMEUtbYmKiKVOmjPnrr79Mvnz5IrZeJEVexwb5HBvkc2yQz97PZ5XEKIgpWbJkistl6EAme/bs5oorrjBz5841rVq18gUmet61a9eQn8mRI4d9+CtQoEDU0qgdx48kNsjr2CCfY4N8jg3y2dv5nFJJjCcCGVHpSvv27c2VV15p6tata4YNG2aOHj1qezEBAIDzW4YPZO6++26zZ88e89JLL5mdO3eayy67zMycOTNJA2AAAHD+yfCBjKgaKbmqpHhR9ZUG6QuuxkLkkdexQT7HBvkcG+Tz+ZPPmZzU+jUBAABkUBl6QDwAAICUEMgAAADPIpABAACeRSCTTiNGjDDly5c3OXPmNFdddZX58ccf450kz4/IXKdOHZM3b15TtGhRO27Q+vXrA5Y5fvy46dKlix3cME+ePKZNmzZJBktE2rz66qt2oMgnn3zS9xr5HBnbtm0z9913n83HXLlymUsvvdQsX77c976aJ6o3ZokSJez7mkNuw4YNcU2z12gKmz59+tiR25WHFStWNC+//HLAkPbkc/osWrTItGjRwg5Gp3PE9OnTA94PJ181GO29995rx5fReG4dO3Y0R44cMRGnxr5Im0mTJjnZs2d3xo4d6/zyyy9Op06dnAIFCji7du2Kd9I8q2nTps64ceOctWvXOqtWrXKaN2/ulC1b1jly5Ihvmccee8wpU6aMM3fuXGf58uXO1Vdf7dSvXz+u6fayH3/80SlfvrxTs2ZNp3v37r7Xyeezt3//fqdcuXJOhw4dnKVLlzqbN292Zs2a5WzcuNG3zKuvvurkz5/fmT59urN69WrntttucypUqOD8888/cU27lwwcONApVKiQ8/XXXztbtmxxPvvsMydPnjzO8OHDfcuQz+nz7bffOi+88IIzdepURYXOtGnTAt4PJ19vvvlmp1atWs6SJUuc77//3qlUqZLTtm1bJ9IIZNKhbt26TpcuXXzPT58+7ZQsWdIZNGhQXNN1Ltm9e7f98SxcuNA+P3jwoJMtWzZ7onL99ttvdpmEhIQ4ptSbDh8+7FSuXNmZM2eOc/311/sCGfI5Mnr16uVcc801yb5/5swZp3jx4s7gwYN9rynvc+TI4XzyyScxSqX33XLLLc5DDz0U8Nrtt9/u3Hvvvfb/5HNkBAcy4eTrr7/+aj+3bNky3zIzZsxwMmXK5Gzbts2JJKqW0ujkyZNmxYoVthjNlTlzZvs8ISEhrmk7l7izlhcsWND+VZ5rxnH/fK9ataopW7Ys+Z4Oqjq65ZZbAvJTyOfI+PLLL+1o5HfeeaetKq1du7Z59913fe9v2bLFDvDpn88ail3V1ORz+OrXr2+nrPn999/t89WrV5vFixebZs2a2efkc3SEk6/6q+ok/Q5cWl7Xy6VLl0Y0PZ4YEC8j2bt3r62XDR5ZWM/XrVsXt3SdSzSfltpsNGjQwFxyySX2Nf1oNPdW8LxZyne9h/BNmjTJrFy50ixbtizJe+RzZGzevNmMGjXKTrHy/PPP27x+4oknbN5qyhU3L0OdR8jn8D333HN20kIF25pgWOfmgQMH2nYZQj5HRzj5qr8K4v1lzZrV3pxGOu8JZJAhSwvWrl1r76wQWZqhtnv37mbOnDm2oTqiF4zrTvQ///mPfa4SGR3To0ePtoEMIuPTTz81EyZMMBMnTjQ1atQwq1atsjdBaqBKPp8/qFpKo8KFC9vIP7gXh54XL148buk6V2gqiq+//trMnz/flC5d2ve68lbVegcPHgxYnnxPG1Ud7d6921x++eX27kiPhQsXmjfffNP+X3dU5PPZU0+O6tWrB7xWrVo1s3XrVvt/Ny85j5ydZ5991pbK3HPPPbZX2P3332969Ohhe0EK+Rwd4eSr/upc4+/ff/+1PZkinfcEMmmkouErrrjC1sv6333peb169eKaNi9TezIFMdOmTTPz5s2z3Sn9Kc+zZcsWkO/qnq0LA/kevoYNG5qff/7Z3rm6D5UcqCje/T/5fPZULRo8fIDacZQrV87+X8e3Tub++awqErUdIJ/Dd+zYMdvmwp9uNHVOFvI5OsLJV/3VDZFunlw6t2vfqC1NREW06fB51P1arbPHjx9vW2Y/8sgjtvv1zp074500z+rcubPtyrdgwQJnx44dvsexY8cCugWrS/a8efNst+B69erZB86Of68lIZ8j07U9a9astnvwhg0bnAkTJji5c+d2Pv7444DuqzpvfPHFF86aNWucli1b0i04jdq3b++UKlXK1/1aXYULFy7s9OzZ07cM+Zz+no0//fSTfShUGDJkiP3/n3/+GXa+qvt17dq17RAEixcvtj0l6X6dgbz11lv2ZK/xZNQdW/3kkX76oYR6aGwZl34gjz/+uHPhhRfai0Lr1q1tsIPIBjLkc2R89dVXziWXXGJveqpWreqMGTMm4H11Ye3Tp49TrFgxu0zDhg2d9evXxy29XpSYmGiPXZ2Lc+bM6Vx00UV27JMTJ074liGf02f+/Pkhz8kKHsPN13379tnARWP75MuXz3nwwQdtgBRpzH4NAAA8izYyAADAswhkAACAZxHIAAAAzyKQAQAAnkUgAwAAPItABgAAeBaBDAAA8CwCGQAA4FkEMsA5pl+/fnbyx0yZMpnp06cbL6T3sssui3cy8H/++OMPe+xo7q1z1fmwjecTAhlEXYcOHexJQw9NulmpUiUzYMAAOxNqRueVYMD122+/mf79+5t33nnH7NixwzRr1sxk9Px85plnAiafO58tWLDA5lHw7OOp4cKc8vmnVatW8U4GoihrNFcOuG6++WYzbtw4c+LECfPtt9+aLl262FmWe/funeZ1nT592p60g2e9hTGbNm2yf1u2bGnzKByapUR5mjVrfE4HefLksQ+cv06ePGlvcjLScQnv4EqAmMiRI4ed9r1cuXKmc+fOplGjRubLL7+07ym40V15qVKlzAUXXGCneNedqWv8+PGmQIECdvnq1avbdW3dutV+rlevXqZMmTL2NZX0vP/++77PrV271pZI6CKpqpb777/f7N271/f+DTfcYJ544gnTs2dPU7BgQZs+VXO4ypcvb/+2bt3aBgXucwULChS0Tq27Tp065rvvvgvYXpWG3HLLLSZXrlx2yvuJEyfazw8bNsy3jO66H374YVOkSBGTL18+c9NNN5nVq1enmI8///yzXU7rLVSokHnkkUfMkSNH7HtKe4sWLez/FeQlF8i4d/0zZswwV1xxhc27xYsXmzNnzphBgwbZ9Gr9tWrVMlOmTPF9TheVjh07+t6vUqWKGT58eJL1jx071tSoUcOut0SJEqZr164p5mdw1ZLSoRK70qVL23XovZkzZyYpfZg6daq58cYbTe7cuW1aExISfMv8+eefNi8uvPBCe0wpPQqgk/PRRx+ZK6+80uTNm9ceB+3atTO7d+9OkmcqOdJy+s769eub9evX+5Zxt0Pr0rblz5/f3HPPPebw4cO+ZXTM6pgrWrSoyZkzp7nmmmvMsmXLfNul7RGlW9+n0gTR9mtZ/Q6032+99VZf0CraJ1K7dm37OR3brvfee89Uq1bNfl/VqlXNyJEjA7b9xx9/tJ/T+9q2n376Kdl88t+OlH57CxcuNHXr1vUdA88991xACazSp+PiySefNIULFzZNmzZN93Epv/zyi80T/Y60D6+99lqbP9onH3zwgfniiy98pcL+5xY3YFL6//vf/wa8rtItLb9x48ZU8wNxFvFpKIEgmi1VU7z7u+2225zLL7/c/v/hhx926tev7yxatMjZuHGjM3jwYDub6u+//27f1wzY2bJls8v873//c9atW+ccPXrUueuuu5wyZco4U6dOdTZt2uR89913zqRJk+xnDhw44BQpUsTp3bu389tvvzkrV650Gjdu7Nx4440Bsz5rRtZ+/frZ7/rggw+cTJkyObNnz7bv79692zcDt2Z/1nNZtWqVM3r0aOfnn3+2n3vxxRftzLvu9PbSqFEj57LLLrOzoq9YscJ+V65cuZyhQ4cGLNOiRQtn2bJldj1PP/20U6hQITtjbChHjhxxSpQo4dx+++32u+fOnetUqFDBNxutZpVVWpVmpTe5GavdWW1r1qxpt1V5ru985ZVX7CzNM2fOtPmpdWk/LFiwwH7u5MmTzksvvWTTu3nzZufjjz+2s2NPnjzZt+6RI0favBg2bJidCffHH3/0bXNy+dm3b1+nVq1avnUMGTLE7pdPPvnE7uuePXva/e8eD1u2bLHrUVq//vpr+z133HGHU65cOefUqVN2mVtuucXu7zVr1tht0UzUCxcuTPYYff/9951vv/3WLpuQkODUq1fPadasWZI8u+qqq2x+/PLLL861115rj0mXtkOz/Lr7R8dz8eLFneeff963zBNPPOGULFnSfpfWoX2nWcaV///++6/z+eef2+/RNimPDh48aD83ZcoU+96GDRucn376yR43l156qXP69Gn7vvJZn9NvQJ9zjyHtIx0z+qz2mf4WLFjQGT9+vO+Y0e+kXbt2ztq1a20+aQZprUvfk5yUfnt///23PS40g7p+e9OmTXMKFy5s88el34Py6tlnn7X7WI/0Hpf6Pm2T8l3HpvJu7Nixdp3aPqX15ptv9v0mNDO2ewy52zhw4ECnevXqAduofXXdddclmwfIOAhkENNARlO/z5kzx56InnnmGXvxz5Ili7Nt27aAz2hKeAUh4l6cFUC4dLLSa1pXKC+//LLTpEmTgNf++usv30XCPZlec801AcvUqVPH6dWrl++5lteJODU1atRw3nrrLft/nbz1OZ1UXboA6TX3ov7999/bi/Xx48cD1lOxYkXnnXfeCfkdY8aMsRc9BTSub775xsmcObOzc+dO+1xpTe3+xL1gTJ8+3fea0qGLzw8//BCwbMeOHZ22bdsmu64uXbo4bdq08T3XRfqFF15IdvlQ+RkcyGgdurAE7xddGMW9CL333nu+9xUU6DXlvegirwA1vbTvtD5dCP3zTBds/7zXa//8849vO5SHiYmJvmV0oVbwI9pvCsgmTJjge1/Bobb39ddfD/geBeIp2bNnj11OAZN/ngQHHzqeJk6cmOS3oUBNdKwpeHa3QUaNGpViIJPab0+BW5UqVexv3TVixAgbuLiBl357tWvXjshxqfOEAnrlZbg3UsH5pfOPzkNLly61z7UuBV9uwIeMjcpHxMTXX39tq2FOnTpli4pVdK9iXxXzqsri4osvTlJ0rSJ0l+rPa9asGVDsmyVLFnP99deH/D5V0cyfPz9k2wsVObvf579OUTG4f5VCKKrKUdq/+eYbW4WkIvN//vnHVneJqhtUr3/55Zf7PqOia1UX+KdP6/HfRtF6/KsMghvyqlhdVSWuBg0a2PzUd6qqKy1UjeBS8fmxY8dM48aNk7RdULWDa8SIEbbqSNuqtOp9t1pI+bZ9+3bTsGFDk16JiYl2Hdouf3oeXO3mv++039w0qPpE1Teqwpw9e7atxmzTpk2Sfe1vxYoVdp/qOw4cOGDzVLSdqs5M7TvLli1r/68qJVVthDqetF91/Ptvm9qJqQpG+zYlGzZsMC+99JJZunSprR71T98ll1wS8jNHjx6136nqwE6dOvle1/Gqai/R92qbVK3kqlevXoppSe23p3VqHf5Vm9pmHe9///23L69UfRSJ41LpUVWS8jK9SpYsaauCdWxrf3z11Vf2HHTnnXeme52IHQIZxITq/keNGmUDEp003AZ8OrnppKgLif768w9CVDfuf2LU85RovWoj8dprryV5z70ASfDJT9/hXiSSo/Y8c+bMsXXqClCUljvuuMOeXMOl9CkdwfX1onYQseAfELntbBScqa2SP7VVkEmTJtltf+ONN+yFShfswYMH24trOPsk0vz3nXtsuPtObY/U7kLbo2BGbSyU7m7duoW84GtZPSZMmGDbLClA0PPgfZrSdwa/H+7xFA4dy2pf9u6779rfj9apACalY87dp/qM2p35C/6tpUWk9rP/8Xc2x2Wk0qNjRu3ohg4dajsm3H333bYtFDI+AhnEhE5OuugH012VSmR016q7qnBdeuml9mSuRoW64w6m0pDPP//c3iGfTa8HXZiUPn//+9//bCNMNVp1T7ZqqOlSI1jd9arRpHvXqTtL3en7p2/nzp02bW6j19SowaYaPuvC657slRY17NV3ng3/RtTJ3Wnru9TA9fHHH/e95l96pMBG26IGsW6j1XDy058aa+pCre/yT4ee6045LdQQ9bHHHrMP9Y7TBT1UILNu3Tqzb98+8+qrr9rPyPLly02kVaxY0Qby2hYFJaISGjX2VaNXcXvu+OeR0qYSN6Xf/Y2oEay/UJ9TCZ3ycvPmzebee+9N9phS4+Tjx4/7SmWWLFlyVr89rVO/PdUkusGetlnHhxpwR/q4VImSGvQqL0OVyihvUjrmXM2bN7e/K91wqXH1okWL0pRWxA+9lhBXquLRSfaBBx6wvVC2bNlie1HoDlp3YcnRBbN9+/bmoYcesuOS6HMq3fj000/t++revX//ftO2bVt7odAFd9asWebBBx8M66Tm/z26MCvocAORypUr27SqSFtVEaom87/rVtWGTvDqUaRtUUCj//uXKul9lWpofAuVGCgQ+uGHH8wLL7yQ7EVU+aSLjbZbPbJUdaYLs+4i01qtFEwXGZW29OjRw14UlF8rV640b731ln3ubrfSpnz8/fffTZ8+fXw9blyqnlHJx5tvvmmrQ9x1pJSfwZ599llbkjZ58mR7AVePF+V19+7dw94eBQZKp44LpUF5pQtsKKrq0MVO6dRFX73jXn75ZRNpukiqukvbpwvlr7/+aqt8VHWi6h9RgKNjRFWxe/bssUGyqiRVBTlmzBgbEM+bN8889dRTAetWLygdX1rvrl27zKFDh+zrGlNIvyXtD+0z9XpTacOQIUPs+zp29X1Kh9Kjnl3BvXfS+ttToPvXX3/ZY1NBonoM9e3b16Y5rUMmhHNcqveTqiTVQ0zHp447BWdujzKld82aNfa5quUU8ISiUirdoCjo1bGeWhUbMpB4N9LBuS9UYzt/bm+Y8uXL28aQ6mXRunVr2+PEbeybP3/+JJ9TA8UePXrY5bNnz+5UqlTJ9lZwqZeL1lOgQAHbY0g9H5588klfI0Q1OOzevXvAOpVOtxeQfPnll3a9WbNmtb1i3IaC6v2kdarnxttvv51kXdu3b7e9XtSoWZ9Tg8uiRYva3k4uNQrt1q2bbeyp7da67r33Xmfr1q3J5pXyRN+tnkHqqdGpUydfg9S0NvYNblCqfFFvIzXUVHrUm6Vp06a+3j5qeNmhQwe7L5SnnTt3dp577rmAhrqibXTXoX2jbUwpP4Mb+6pBqBrqlipVyq5D782YMcP3fqiGrdoWvaZtk65du9qGrsp/bcf999/v7N27N9k80f7R8afl1RBW6fT/jlB5pvf0mtITajtEjbvd7XSPWeWHGpLquxo0aGB7HPkbMGCA7e2kHnTusaiGtdWqVbOfUa8e9dgJbjj97rvv2mNIjb91PLrUuFg96PQbUWNx9cRRbyOXemkp3Xpfy7k9p1LqtZTab0/pUwNtvadtUQN6t0dZcr+99B6Xsnr1atu4Xw2D8+bNa3uUqYeTqHecerCpsbF7jCTXOFqf0etu42t4Qyb9E+9gCjjXqZGjqi003szZNIYFED3ff/+9/X2qROlsSzkROwQyQBSo+F/VAmpPoJ5NGnRv27Zttnj/bHpXAIg89VBSVZ6qzDQgohp9wztoIwNEgerhn3/+eTuirBoFqyeM2hEQxAAZzyeffGLbJ2m07ddffz3eyUEaUSIDAAA8ixIZAADgWQQyAADAswhkAACAZxHIAAAAzyKQAQAAnkUgAwAAPItABgAAeBaBDAAA8CwCGQAAYLzq/wO1nsapP26soQAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ax = plt.gca()\n", + "ax.set_axisbelow(True) # grid behind bars\n", + "plt.grid(True) # y-axis gridlines only\n", + "\n", + "plt.hist(pct_found[\"pct_found\"], bins=20)\n", + "plt.xlabel(\"Percentage of reactions annotated correctly\")\n", + "plt.ylabel(\"Count (total n=75)\")\n", + "plt.title(f\"Distribution of Percentages\\n {llm_model} with curated species\")" + ] + }, + { + "cell_type": "code", + "execution_count": 73, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "24 out of 75 models had an annotation accuracy < 80%\n", + "about 32.0%\n", + "\n", + "50 out of 75 models had an annotation accuracy < 80%\n", + "about 66.67%\n" + ] + } + ], + "source": [ + "print(f\"{len(pct_found[(pct_found['pct_found']<80)])} out of 75 models had an annotation accuracy < 80%\")\n", + "print(f\"about {round(len(pct_found[(pct_found['pct_found']<80)])*100/75,2)}%\")\n", + "print()\n", + "print(f\"{len(pct_found[(pct_found['pct_found']>=80)])} out of 75 models had an annotation accuracy < 80%\")\n", + "print(f\"about {round(len(pct_found[(pct_found['pct_found']>=80)])*100/75,2)}%\")" + ] + }, + { + "cell_type": "code", + "execution_count": 74, + "id": "6ec148f5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Recall: 0.65\n" + ] + } + ], + "source": [ + "recall = summary_df['found'].mean()\n", + "print(f\"Recall: {round(recall,2)}\")" + ] + }, + { + "cell_type": "markdown", + "id": "e8ae1695", + "metadata": {}, + "source": [ + "### Curated species, no BRITE" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ae3fd00f", + "metadata": {}, + "outputs": [], + "source": [ + "llm_model = \"Llama-3.3-70B-Instruct\"\n", + "folder = \"reaction_evaluation_results/curated_species/Llama-3.3-70B-Instruct-20260505_093510-no-brite\"" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "1bb6c4c6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Percentage of reactions that received at least one candidate: 66.9 %\n", + "Ground truth annotation within top 1: 63.4 %\n", + "Ground truth annotation within top 3: 64.3 %\n", + "Ground truth annotation within top 5: 64.3 %\n" + ] + } + ], + "source": [ + "# with curated species annotations\n", + "summary_df = pd.read_csv(f'./{folder}/per_reaction_results.csv')\n", + "\n", + "# Annotation coverage: % of reactions that receive ≥1 candidate\n", + "print(\"Percentage of reactions that received at least one candidate: \", round(len(summary_df[summary_df['num_candidates']>0])/len(summary_df)*100,1), \"%\")\n", + "# top-1 accuracy:\n", + "print(\"Ground truth annotation within top 1: \", round(len(summary_df[summary_df['top1']==True])/len(summary_df)*100,1), \"%\")\n", + "# top-3 accuracy:\n", + "print(\"Ground truth annotation within top 3: \", round(len(summary_df[summary_df['top3']==True])/len(summary_df)*100,1), \"%\")\n", + "# top-5 accuracy:\n", + "print(\"Ground truth annotation within top 5: \", round(len(summary_df[summary_df['top5']==True])/len(summary_df)*100,1), \"%\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "b48bb4a4", + "metadata": {}, + "source": [ + "### Curated species, no cofactors" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e655b6f6", + "metadata": {}, + "outputs": [], + "source": [ + "llm_model = \"Llama-3.3-70B-Instruct\"\n", + "folder = \"reaction_evaluation_results/curated_species/Llama-3.3-70B-Instruct-20260503_191147-no-cofactors\"" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "8b9733e0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Percentage of reactions that received at least one candidate: 66.8 %\n", + "Ground truth annotation within top 1: 62.9 %\n", + "Ground truth annotation within top 3: 64.2 %\n", + "Ground truth annotation within top 5: 64.3 %\n" + ] + } + ], + "source": [ + "# with curated species annotations\n", + "summary_df = pd.read_csv(f'./{folder}/per_reaction_results.csv')\n", + "\n", + "# Annotation coverage: % of reactions that receive ≥1 candidate\n", + "print(\"Percentage of reactions that received at least one candidate: \", round(len(summary_df[summary_df['num_candidates']>0])/len(summary_df)*100,1), \"%\")\n", + "# top-1 accuracy:\n", + "print(\"Ground truth annotation within top 1: \", round(len(summary_df[summary_df['top1']==True])/len(summary_df)*100,1), \"%\")\n", + "# top-3 accuracy:\n", + "print(\"Ground truth annotation within top 3: \", round(len(summary_df[summary_df['top3']==True])/len(summary_df)*100,1), \"%\")\n", + "# top-5 accuracy:\n", + "print(\"Ground truth annotation within top 5: \", round(len(summary_df[summary_df['top5']==True])/len(summary_df)*100,1), \"%\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "d3594d43", + "metadata": {}, + "source": [ + "### AAAIM species" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "806a2c10", + "metadata": {}, + "outputs": [], + "source": [ + "llm_model = \"Llama-3.3-70B-Instruct\"\n", + "folder = \"reaction_evaluation_results/aaaim_species/Llama-3.3-70B-Instruct-20260503_020542\"" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Percentage of reactions that received at least one candidate: 71.2 %\n", + "Ground truth annotation within top 1: 69.9 %\n", + "Ground truth annotation within top 3: 70.6 %\n", + "Ground truth annotation within top 5: 70.6 %\n" + ] + } + ], + "source": [ + "# with curated species annotations\n", + "summary_df = pd.read_csv(f'./{folder}/per_reaction_results.csv')\n", + "\n", + "# Annotation coverage: % of reactions that receive ≥1 candidate\n", + "print(\"Percentage of reactions that received at least one candidate: \", round(len(summary_df[summary_df['num_candidates']>0])/len(summary_df)*100,1), \"%\")\n", + "# top-1 accuracy:\n", + "print(\"Ground truth annotation within top 1: \", round(len(summary_df[summary_df['top1']==True])/len(summary_df)*100,1), \"%\")\n", + "# top-3 accuracy:\n", + "print(\"Ground truth annotation within top 3: \", round(len(summary_df[summary_df['top3']==True])/len(summary_df)*100,1), \"%\")\n", + "# top-5 accuracy:\n", + "print(\"Ground truth annotation within top 5: \", round(len(summary_df[summary_df['top5']==True])/len(summary_df)*100,1), \"%\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "9912b193", + "metadata": {}, + "source": [ + "#### Failure reasons" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "2ad60051", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "failure_reason\n", + "no_candidates 1252\n", + "SSX 11\n", + "Name: count, dtype: int64" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Failure reasons\n", + "failure_reasons = summary_df[summary_df[\"failure_reason\"].notna()]\n", + "failure_reasons[\"failure_reason\"].value_counts()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da31fbc9", + "metadata": {}, + "outputs": [], + "source": [ + "failure_reasons = summary_df[summary_df[\"failure_reason\"].notna()]\n", + "\n", + "results = []\n", + "\n", + "for _, row in failure_reasons.iterrows():\n", + " if row[\"failure_reason\"] != \"no_candidates\":\n", + " continue\n", + "\n", + " model_id = row[\"model_id\"]\n", + " reaction_id = row[\"reaction_id\"]\n", + " gt = row[\"ground_truth_kegg\"] \n", + " gt_formatted = f\"KEGG:{gt}\"\n", + "\n", + " file_path = os.path.join(folder, \"_work\", f\"{model_id}.xml_recommendations.csv\")\n", + "\n", + " if not os.path.exists(file_path):\n", + " label = \"file not found\"\n", + " else:\n", + " rec_df = pd.read_csv(file_path)\n", + "\n", + " sub = rec_df[rec_df[\"id\"] == reaction_id]\n", + "\n", + " if sub.empty:\n", + " label = \"reaction not found in recommendations\"\n", + " else:\n", + " if sub[\"annotation\"].astype(str).str.contains(gt_formatted).any():\n", + " label = \"LLM did not recognize correct annotation\"\n", + " else:\n", + " label = \"no correct candidates were generated\"\n", + "\n", + " results.append({\n", + " \"model_id\": model_id,\n", + " \"reaction_id\": reaction_id,\n", + " \"ground_truth_kegg\": gt,\n", + " \"label\": label\n", + " })\n", + "\n", + "failure_reasons_df = pd.DataFrame(results)\n", + "failure_reasons_df.to_csv(f\"./{folder}/failure_reasons.csv\", index=False)" + ] + }, + { + "cell_type": "markdown", + "id": "3343f8d2", + "metadata": {}, + "source": [ + "#### Complexity analysis" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "9f3d92b0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Wrote C:\\Users\\user\\Documents\\research\\AAAIM\\tests\\reaction_evaluation_results\\aaaim_species\\Llama-3.3-70B-Instruct-20260503_020542\\per_reaction_with_complexity.csv\n", + "Wrote C:\\Users\\user\\Documents\\research\\AAAIM\\tests\\reaction_evaluation_results\\aaaim_species\\Llama-3.3-70B-Instruct-20260503_020542\\complexity_impact_summary.csv\n", + "complexity_bin n_reactions coverage top1 top3 top5 mean_candidates\n", + " 1 17 0.588 0.588 0.588 0.588 0.588\n", + " 2 473 0.789 0.770 0.789 0.789 1.121\n", + " 3 582 0.737 0.723 0.735 0.737 1.003\n", + " 4 1452 0.695 0.687 0.695 0.695 0.839\n", + " 5+ 1855 0.685 0.683 0.685 0.685 0.785\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
complexity_binn_reactionscoveragetop1top3top5mean_candidates
01170.5880.5880.5880.5880.588
124730.7890.7700.7890.7891.121
235820.7370.7230.7350.7371.003
3414520.6950.6870.6950.6950.839
45+18550.6850.6830.6850.6850.785
\n", + "
" + ], + "text/plain": [ + " complexity_bin n_reactions coverage top1 top3 top5 mean_candidates\n", + "0 1 17 0.588 0.588 0.588 0.588 0.588\n", + "1 2 473 0.789 0.770 0.789 0.789 1.121\n", + "2 3 582 0.737 0.723 0.735 0.737 1.003\n", + "3 4 1452 0.695 0.687 0.695 0.695 0.839\n", + "4 5+ 1855 0.685 0.683 0.685 0.685 0.785" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = run_complexity_analysis(f\"./tests/{folder}/per_reaction_results.csv\") # take 64min\n", + "result.per_reaction.to_csv(f'./{folder}/complexity_list.csv', index=False)\n", + "result.summary" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "d027d973", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "On average, 81.1% of reactions in each model were annotated correctly.\n" + ] + } + ], + "source": [ + "pct_found = (\n", + " summary_df\n", + " .sort_values(\"model_id\")\n", + " .groupby(\"model_id\")[\"found\"]\n", + " .mean()\n", + " .mul(100)\n", + " .reset_index(name=\"pct_found\")\n", + ").round(1)\n", + "pct_found\n", + "\n", + "avg_annotated_correctly = round(pct_found['pct_found'].mean(), 1)\n", + "print(f\"On average, {avg_annotated_correctly}% of reactions in each model were annotated correctly.\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "281cf5bb", + "metadata": {}, + "source": [ + "Which models did AAAIM fail at annotating?" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "bcd6ded3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
model_idpct_found
19BIOMD00000000880.0
23BIOMD00000001370.0
\n", + "
" + ], + "text/plain": [ + " model_id pct_found\n", + "19 BIOMD0000000088 0.0\n", + "23 BIOMD0000000137 0.0" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pct_found[pct_found['pct_found']==0]" + ] + }, + { + "cell_type": "markdown", + "id": "1d6fb7b1", + "metadata": {}, + "source": [ + "Make a histogram of successfully annotated models" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "1b6d4877", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Distribution of Percentages\\n Llama-3.3-70B-Instruct with curated species')" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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6zo+FNi5lK9q/KvjyX9dOrZCbaOqCcNJv6o91eBbD5U0qZGvGqwTyOBc0UYGSIXaMOjuVVUHHTp0SHE5tXcI5t6lqheLqEeKhAosMuK9994LaLejC5V6mvhfGHSxWb58uW9QPVGvmeBu2ulJm6oFdJF/8803A15XbyVd+CJ1YdL3aGBCt0eOaBDCN954w97pq7dNNKjUQ72q1E5GgUgw/+7OKdFFWseMehEF56lbeqKqSvXKUs8h7beMfE8o2peh9qO2K7jkRm2DgtvhKGDXa/5dwDU2zltvvRWwnKqIdHyp272qXlNKv44VlWj607aretL9bQDnghIZ4P9RI1Xd7etiqa6pCmLUhVZ3qDqppzaKr7ovq2rp5ptvtsvrjlNtEdQlWGPLiE76aocxbtw4e8evC47aagS3rwiXqga0bjUQVnp10VRVhH+jTLXZUYCjcT5UvK+2GxoPx7/xbXrTpkawurNXaZPa46ixrKoL1NBZ1RTB684oNWzVRV7drTW+jsYg0baoS7u2NbiNTiS99NJLtiRNeaD8VJdhBatqFKvSqFCBqz+VIGmUYOWVAlrtI5VI6PhSl2t1V5fRo0fbfaixXfQ9KqXRvkxMTLRVmqHGeEmLAgx9t9oA6XhQ0KAqTnUF13GqtKhr/Q8//GAmT56crJ3KQw89ZINUNbJW92ulW8u5x79bSqNtVLswBa4aY0frVXseBUHKO5XUfP755zbY1u9A7cV0rCgIVR6uXLnSvPbaa+nePiCZc+73BHic27XVfai7cMmSJW33UXVl9u/mm1L364ULFzq33XabU7p0aft5/W3fvr3zyy+/BHxu1qxZTo0aNZzs2bMHdHdWd1l1mw0lpe7XH374oTNgwACnePHiTp48eWz3499++y3Z59WdVl211c22QYMGzqpVq5KtM7W0BXe/lsOHD9uuv9rOHDly2G64r776akA3XNF6QnW7TalbeLDdu3c7999/v1O0aFGbr+qqHKqLeHq7X4ezrL5baU9ISLDbqGNC3aXHjx+fbF98/PHHIdexbNkyexzlz5/fueCCC5xatWol63a+detW57777rPr1/doX91yyy3OJ598kuYQAe73u12iZdeuXXb79J16z93P6n79+OOP227lOl50LCQmJoY8FrZt22bXoeWKFStmP/fpp5/a9S1fvjxg2e+//95p27atU6RIEXuM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KckeuzoX586d26lUqZId++TkyZO+ZcjnjFm0aFHIc7KCx3Dzdf/+/TZw0dg+BQoUcO6//34bIEUas18DAADPoo0MAADwLAIZAADgWQQyAADAswhkAACAZxHIAAAAzyKQAQAAnkUgAwAAPItABgAAeBaBDPAPM3jwYDv5Y5YsWczMmTONF9J72WWXZXYy8P/8+uuv9tjR3Fv/VOfDNp5PCGQQdZ07d7YnDT006WaVKlXM888/b2dCjXdeCQZcP//8sxkyZIj5z3/+Y3bu3GlatGhh4j0/n3jiiYDJ585nixcvtnkUPPt4Wrgwp37+ad26dWYnA1GUPZorB1w33XSTmThxojl58qSZPXu26d69u51lecCAAele15kzZ+xJO3jWWxizdetW+/e2226zeRQOzVKiPM2ePXNOB/ny5bMPnL9OnTplb3Li6biEd3AlQEzkypXLTvtevnx588gjj5gmTZqYzz77zL6n4EZ35WXKlDEXXHCBneJdd6auSZMmmUKFCtnla9SoYde1Y8cO+7n+/fubhIQE+5pKet555x3f5zZs2GBLJHSRVFVLx44dzb59+3zvN2rUyDz22GOmX79+pnDhwjZ9quZwVahQwf5t06aNDQrc5woWFChonVp33bp1zYIFCwK2V6UhN998s8mTJ4+d8n7KlCn28yNHjvQto7vuBx54wBQrVswUKFDA3HjjjWbdunWp5uMPP/xgl9N6ixQpYh588EFz5MgR+57S3qpVK/t/BXkpBTLuXf+cOXPMFVdcYfNu2bJl5uzZs2bYsGE2vVp/7dq1zSeffOL7nC4qXbt29b1ftWpVM2rUqGTrnzBhgqlZs6Zdb6lSpUyPHj1Szc/gqiWlQyV2ZcuWtevQe3Pnzk1W+jB9+nRzww03mLx589q0JiYm+pb57bffbF5ceOGF9phSehRAp+T99983V155pcmfP789Du655x6zZ8+eZHmmkiMtp++sX7++2bRpk28Zdzu0Lm1bwYIFzd13320OHz7sW0bHrI654sWLm9y5c5uGDRualStX+rZL2yNKt75PpQmi7dey+h1ov99yyy2+oFW0T6ROnTr2czq2XW+//bapXr26/b5q1aqZMWPGBGz7d999Zz+n97Vt33//fYr55L8dqf32lixZYq666irfMfDUU08FlMAqfTouevfubYoWLWqaN2+e4eNSfvzxR5sn+h1pH1577bU2f7RP3n33XTNr1ixfqbD/ucUNmJT+f//73wGvq3RLy2/ZsiXN/EAmi/g0lEAQzZaqKd793Xrrrc7ll19u///AAw849evXd5YuXeps2bLFefXVV+1sqr/88ot9XzNg58iRwy7z3//+19m4caNz9OhR584773QSEhKc6dOnO1u3bnUWLFjgTJ061X7mwIEDTrFixZwBAwY4P//8s7NmzRqnadOmzg033BAw67NmZB08eLD9rnfffdfJkiWLM2/ePPv+nj17fDNwa/ZnPZe1a9c648aNc3744Qf7uWeffdbOvOtOby9NmjRxLrvsMjsr+urVq+135cmTxxkxYkTAMq1atXJWrlxp1/P44487RYoUsTPGhnLkyBGnVKlSTtu2be13L1y40KlYsaJvNlrNKqu0Ks1Kb0ozVruz2taqVctuq/Jc3/niiy/aWZrnzp1r81Pr0n5YvHix/dypU6ec5557zqZ327ZtzgcffGBnx542bZpv3WPGjLF5MXLkSDsT7nfffefb5pTyc9CgQU7t2rV96xg+fLjdLx9++KHd1/369bP73z0etm/fbtejtH7xxRf2e26//XanfPnyzunTp+0yN998s93f69evt9uimaiXLFmS4jH6zjvvOLNnz7bLJiYmOvXq1XNatGiRLM+uvvpqmx8//vijc+2119pj0qXt0Cy/7v7R8VyyZEnn6aef9i3z2GOPOaVLl7bfpXVo32mWceX/33//7Xz66af2e7RNyqODBw/az33yySf2vc2bNzvff/+9PW4uvfRS58yZM/Z95bM+p9+APuceQ9pHOmb0We0z/S1cuLAzadIk3zGj38k999zjbNiwweaTZpDWuvQ9KUntt/f777/b40IzqOu3N2PGDKdo0aI2f1z6PSivnnzySbuP9cjocanv0zYp33VsKu8mTJhg16ntU1pvuukm329CM2O7x5C7jUOHDnVq1KgRsI3aV9ddd12KeYD4QSCDmAYymvp9/vz59kT0xBNP2It/tmzZnD/++CPgM5oSXkGIuBdnBRAunaz0mtYVygsvvOA0a9Ys4LX//e9/vouEezJt2LBhwDJ169Z1+vfv73uu5XUiTkvNmjWdN954w/5fJ299TidVly5Aes29qH/zzTf2Yn3ixImA9VSuXNn5z3/+E/I7xo8fby96CmhcX375pZM1a1Zn165d9rnS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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ax = plt.gca()\n", + "ax.set_axisbelow(True) # grid behind bars\n", + "plt.grid(True) # y-axis gridlines only\n", + "\n", + "plt.hist(pct_found[\"pct_found\"], bins=20)\n", + "plt.xlabel(\"Percentage of reactions annotated correctly\")\n", + "plt.ylabel(\"Count (total n=75)\")\n", + "plt.title(f\"Distribution of Percentages\\n {llm_model} with curated species\")" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "e9fe4ac8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "22 out of 75 models had an annotation accuracy < 80%\n" + ] + } + ], + "source": [ + "print(f\"{len(pct_found[(pct_found['pct_found']<80)])} out of 75 models had an annotation accuracy < 80%\")" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "0df03b2f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "22 out of 75 models had an annotation accuracy < 80%\n", + "about 29.33%\n", + "\n", + "46 out of 75 models had an annotation accuracy < 80%\n", + "about 61.33%\n" + ] + } + ], + "source": [ + "print(f\"{len(pct_found[(pct_found['pct_found']<80)])} out of 75 models had an annotation accuracy < 80%\")\n", + "print(f\"about {round(len(pct_found[(pct_found['pct_found']<80)])*100/75,2)}%\")\n", + "print()\n", + "print(f\"{len(pct_found[(pct_found['pct_found']>=80)])} out of 75 models had an annotation accuracy < 80%\")\n", + "print(f\"about {round(len(pct_found[(pct_found['pct_found']>=80)])*100/75,2)}%\")" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "15544205", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Recall: 0.71\n" + ] + } + ], + "source": [ + "recall = summary_df['found'].mean()\n", + "print(f\"Recall: {round(recall,2)}\")" + ] + }, + { + "cell_type": "markdown", + "id": "e2cc441e", + "metadata": {}, + "source": [ + "## Llama-4-Maverick-17B-128E-Instruct-FP8" + ] + }, + { + "cell_type": "markdown", + "id": "95d2400d", + "metadata": {}, + "source": [ + "### Curated species" + ] + }, + { + "cell_type": "code", + "execution_count": 119, + "id": "9b24a39d", + "metadata": {}, + "outputs": [], + "source": [ + "llm_model = \"Llama-4-Maverick-17B-128E-Instruct-FP8\"\n", + "folder = \"reaction_evaluation_results/curated_species/Llama-4-Maverick-17B-128E-Instruct-FP8-20260503_020614\"" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "6b51c20f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Percentage of reactions that received at least one candidate: 74.3 %\n", + "Ground truth annotation within top 1: 69.2 %\n", + "Ground truth annotation within top 3: 70.0 %\n", + "Ground truth annotation within top 5: 70.0 %\n" + ] + } + ], + "source": [ + "# with curated species annotations\n", + "summary_df = pd.read_csv(f'./{folder}/per_reaction_results.csv')\n", + "\n", + "# Annotation coverage: % of reactions that receive ≥1 candidate\n", + "print(\"Percentage of reactions that received at least one candidate: \", round(len(summary_df[summary_df['num_candidates']>0])/len(summary_df)*100,1), \"%\")\n", + "# top-1 accuracy:\n", + "print(\"Ground truth annotation within top 1: \", round(len(summary_df[summary_df['top1']==True])/len(summary_df)*100,1), \"%\")\n", + "# top-3 accuracy:\n", + "print(\"Ground truth annotation within top 3: \", round(len(summary_df[summary_df['top3']==True])/len(summary_df)*100,1), \"%\")\n", + "# top-5 accuracy:\n", + "print(\"Ground truth annotation within top 5: \", round(len(summary_df[summary_df['top5']==True])/len(summary_df)*100,1), \"%\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "88d4712c", + "metadata": {}, + "source": [ + "#### Failure reasons" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "428eeb68", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "failure_reason\n", + "no_candidates 1492\n", + "SSX 10\n", + "Name: count, dtype: int64" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Failure reasons\n", + "failure_reasons = summary_df[summary_df[\"failure_reason\"].notna()]\n", + "failure_reasons[\"failure_reason\"].value_counts()" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "3017f2a7", + "metadata": {}, + "outputs": [], + "source": [ + "failure_reasons = summary_df[summary_df[\"failure_reason\"].notna()]\n", + "\n", + "results = []\n", + "\n", + "for _, row in failure_reasons.iterrows():\n", + " if row[\"failure_reason\"] != \"no_candidates\":\n", + " continue\n", + "\n", + " model_id = row[\"model_id\"]\n", + " reaction_id = row[\"reaction_id\"]\n", + " gt = row[\"ground_truth_kegg\"] # e.g. \"R01845\"\n", + " gt_formatted = f\"KEGG:{gt}\"\n", + "\n", + " file_path = os.path.join(folder, \"_work\", f\"{model_id}.xml_recommendations.csv\")\n", + "\n", + " if not os.path.exists(file_path):\n", + " label = \"file not found\"\n", + " else:\n", + " rec_df = pd.read_csv(file_path)\n", + "\n", + " sub = rec_df[rec_df[\"id\"] == reaction_id]\n", + "\n", + " if sub.empty:\n", + " label = \"reaction not found in recommendations\"\n", + " else:\n", + " if sub[\"annotation\"].astype(str).str.contains(gt_formatted).any():\n", + " label = \"LLM did not recognize correct annotation\"\n", + " else:\n", + " label = \"no correct candidates were generated\"\n", + "\n", + " results.append({\n", + " \"model_id\": model_id,\n", + " \"reaction_id\": reaction_id,\n", + " \"ground_truth_kegg\": gt,\n", + " \"label\": label\n", + " })\n", + "\n", + "output_df = pd.DataFrame(results)\n", + "output_df.to_csv(f\"./{folder}/failure_reasons.csv\", index=False)" + ] + }, + { + "cell_type": "markdown", + "id": "c9f81625", + "metadata": {}, + "source": [ + "#### Complexity analysis" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "08bea550", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Wrote C:\\Users\\user\\Documents\\research\\AAAIM\\tests\\reaction_evaluation_results\\curated_species\\Llama-4-Maverick-17B-128E-Instruct-FP8-20260503_020614\\per_reaction_with_complexity.csv\n", + "Wrote C:\\Users\\user\\Documents\\research\\AAAIM\\tests\\reaction_evaluation_results\\curated_species\\Llama-4-Maverick-17B-128E-Instruct-FP8-20260503_020614\\complexity_impact_summary.csv\n", + "complexity_bin n_reactions coverage top1 top3 top5 mean_candidates\n", + " 1 17 0.647 0.647 0.647 0.647 0.647\n", + " 2 570 0.721 0.696 0.718 0.718 1.086\n", + " 3 717 0.767 0.748 0.767 0.767 0.978\n", + " 4 1961 0.711 0.701 0.711 0.711 0.876\n", + " 5+ 2573 0.670 0.669 0.670 0.670 0.805\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
complexity_binn_reactionscoveragetop1top3top5mean_candidates
01170.6470.6470.6470.6470.647
125700.7210.6960.7180.7181.086
237170.7670.7480.7670.7670.978
3419610.7110.7010.7110.7110.876
45+25730.6700.6690.6700.6700.805
\n", + "
" + ], + "text/plain": [ + " complexity_bin n_reactions coverage top1 top3 top5 mean_candidates\n", + "0 1 17 0.647 0.647 0.647 0.647 0.647\n", + "1 2 570 0.721 0.696 0.718 0.718 1.086\n", + "2 3 717 0.767 0.748 0.767 0.767 0.978\n", + "3 4 1961 0.711 0.701 0.711 0.711 0.876\n", + "4 5+ 2573 0.670 0.669 0.670 0.670 0.805" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = run_complexity_analysis(f\"./tests/{folder}/per_reaction_results.csv\") # take 64min\n", + "result.per_reaction.to_csv(f'./{folder}/complexity_list.csv', index=False)\n", + "result.summary" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "0081321e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "On average, 77.1% of reactions in each model were annotated correctly.\n" + ] + } + ], + "source": [ + "pct_found = (\n", + " summary_df\n", + " .sort_values(\"model_id\")\n", + " .groupby(\"model_id\")[\"found\"]\n", + " .mean()\n", + " .mul(100)\n", + " .reset_index(name=\"pct_found\")\n", + ").round(1)\n", + "\n", + "df_llm4 = pct_found\n", + "\n", + "avg_annotated_correctly = round(pct_found['pct_found'].mean(), 1)\n", + "print(f\"On average, {avg_annotated_correctly}% of reactions in each model were annotated correctly.\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "872ece90", + "metadata": {}, + "source": [ + "Which models did AAAIM fail at annotating?" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "a20c53fc", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
model_idpct_found
22BIOMD00000001080.0
25BIOMD00000001370.0
65BIOMD00000006910.0
\n", + "
" + ], + "text/plain": [ + " model_id pct_found\n", + "22 BIOMD0000000108 0.0\n", + "25 BIOMD0000000137 0.0\n", + "65 BIOMD0000000691 0.0" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pct_found[pct_found['pct_found']==0]" + ] + }, + { + "cell_type": "markdown", + "id": "d6882511", + "metadata": {}, + "source": [ + "Make a histogram of successfully annotated models" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "1ebd6f4b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Distribution of Percentages\\n Llama-4-Maverick-17B-128E-Instruct-FP8 with curated species')" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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TYPabbl27txpL9paJ5WyhAYIadm/f79dro5LXVgUEGjf0klbY2FFk4ZI9JBB94Lh0n4RrnrRa+3atfavLgyii4guso8++qi9sGibKp+0PRWQNGvWLOKydMHVNpw3b549duWbb76x5z3dKOocoRtH7bs6P2kdw4kmH3QebdWqlX3KtLa9jt8BAwbYG1Rto9TogqhzsJan39Exv2jRInsOCVfVHA2do1WSqjSr5EdUgqb11P6iY0TnYp3fFeTpuqHAS+dZlbypmlzBtNbdzQO3ClylrTqPal/WOUrL0EX8xx9/DJznFLh17NjR3izrGNC+qsDRDcRSs23bNrveSr+uWdqGSusnYZo4vPPOO/bcrmuR2vq9+OKLNoDWudk9HnSsNWnSxAakWp72J52bVUqtGgP3Jiqt4yFS6XC0y9c2Vl6olFrnfO1/CvJ0ftD5PCpONrF+/Xo74rBGlk5N6GjSGun4+PHjKZal0YmfeOKJFKMn16lTxzly5EhgukbuzpEjh3PllVcGLaNRo0YpRsI+ePBgivS0bNnSOe2006JaR32/cuXKdiTvaEaUjjQS+fbt251+/fo5Z5xxRuCzBg0a2NGzw43EfezYMTsys9euXbucMmXKOF27dg1M+/LLL+13P/vss6B5W7duHbSO//vf/5xTTjnF+eabb4LmGzVqlP3+t99+G5im95r3l19+SbE++kzr5NLI2Jo3dORrcUcb946OvWXLFjuSutK2YcMGJ720r2lZ2l/SojxUfl1wwQURt0voq1ixYs706dPTnS5t39C8SUu3bt2cnDlzphiFftGiRc7pp58elK7zzjvP5l0066BXjRo10vz9cPuyu0zvPiYdOnSwI4i7hg8fHtivI9H29o5U76URzfWZ9r9QkfIx9Dzy2GOP2Xk/+eSTiPteammIxE1b6Mv7227ejRkzxubB5s2bnc8//9ypWrWqPTd5j4epU6faUd29y9I5aN++fWmmRaO8e/dfjeSul/abadOm2WlLly61y5wyZUrQOnhHjY9mW7zzzjuBaTr3aMT7jh07ppq+1atX2+Nf+0foOd3dBunZpu654qKLLrLHb1rn8gULFqRIu/ZnTdM28lJ+6/ju3r170PStW7c6RYsWDZp+zjnn2G22e/fuwLQZM2bY5YZeY0JNmjQpcL5L69qZP39+588//ww69jW9b9++gWlNmzZ1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kSPtCwAAIFs95wcAACDeCH4AAICvEPwAAABfIfgBAAC+QvADAAB8heAHAAD4Soa6uq9fv9588803ZuPGjXbsjVKlSpn69evbhw+qazoAAEC2CH7ee+898+KLL5rFixfbkdXLly9vn8T8zz//mLVr19rAp1OnTmbAgAGmSpUq8Us1AABAvIMflezkyZPHPnn5448/toOFeunpywsWLDATJkww559/vn0K83XXXZfRdAEAACQ3+Hn22Wft0BKRaDBRDVOh19NPP202bNgQqzQCAAAkPvhJLfAJVaJECfsCAADIdr29jh49alavXm327NkTmxQBAABkluBn6NCh5t9//7X/P378uOnXr58pVKiQqVmzpilZsqTp2rWrDYYAAACyRfAzcOBAs2/fPvv/4cOHmzFjxphRo0aZn3/+2YwbN858/vnndjoAAEC26OruOE7g/+PHj7eNoG+//Xb7vnbt2vbvkCFDzAMPPBDrdAIAACSnzU+OHDns302bNpnGjRsHfab3egAiAABAtnnC8xtvvGHb+eiZP3q4oZeqxNTlHQAAIFsEP5UrV7bBjyjIWbp0qbnkkksCn8+ePdvUqFEj9qkEAABIRvCT1oMLGzZsGBQMAQAAZIuBTSO58MILY7k4AACA5DZ4fuGFFxi2AgAA+Cf46d+/vznjjDNM8+bNzcSJE82RI0filzIAAIDM0NX9zTffNAULFjSdO3c25cuXN3369DErVqyIR9oAAACSH/y0bt3aTJ482fz555/2YYZffvmlqVevnrngggtsTzD3CdAAAADZamDT0qVL2+Bn5cqVZs6cOfYJz3379jXlypWLbQoBAACSFfy4T3cOdfHFF9uxvTZv3szYXgAAIPsEP96xvcIpUqSI6d69+8mmCQAAIHM85+fEiRPxSwkAAEBmbvMDAADgq+Dn/fffNwcOHEjxfwAAgGwZ/Nx1113m77//TvF/AACAbBn8eBs/p9UQGgAAILOgzQ8AAPAVgh8AAOArBD8AAMBXCH4AAICvEPwAAABfIfgBAAC+kuHgZ9q0aaZChQop/g8AAJBtxvbyuuiii8L+HwAAIFsGP3LkyBGzbdu2FAOeVq5c+WTTBQAAkHmCn9WrV5uuXbua7777Lmi6nvScI0cOc/z48VilDwAAIPnBz2233WZy5cplpk6dasqVK2cDHgAAgGwb/CxbtswsWbLE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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ax = plt.gca()\n", + "ax.set_axisbelow(True) # grid behind bars\n", + "plt.grid(True) # y-axis gridlines only\n", + "\n", + "plt.hist(pct_found[\"pct_found\"], bins=20)\n", + "plt.xlabel(\"Percentage of reactions annotated correctly\")\n", + "plt.ylabel(\"Count (total n=75)\")\n", + "plt.title(f\"Distribution of Percentages\\n {llm_model} with curated species\")" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "88000b98", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "29 out of 75 models had an annotation accuracy < 80%\n", + "about 38.67%\n", + "\n", + "45 out of 75 models had an annotation accuracy < 80%\n", + "about 60.0%\n" + ] + } + ], + "source": [ + "print(f\"{len(pct_found[(pct_found['pct_found']<80)])} out of 75 models had an annotation accuracy < 80%\")\n", + "print(f\"about {round(len(pct_found[(pct_found['pct_found']<80)])*100/75,2)}%\")\n", + "print()\n", + "print(f\"{len(pct_found[(pct_found['pct_found']>=80)])} out of 75 models had an annotation accuracy < 80%\")\n", + "print(f\"about {round(len(pct_found[(pct_found['pct_found']>=80)])*100/75,2)}%\")" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "2ff5cfc8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Recall: 0.7\n" + ] + } + ], + "source": [ + "recall = summary_df['found'].mean()\n", + "print(f\"Recall: {round(recall,2)}\")" + ] + }, + { + "cell_type": "markdown", + "id": "777ab037", + "metadata": {}, + "source": [ + "### Curated species, no BRITE" + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "id": "16894e05", + "metadata": {}, + "outputs": [], + "source": [ + "llm_model = \"Llama-3.3-70B-Instruct\"\n", + "folder = \"reaction_evaluation_results/curated_species/Llama-4-Maverick-17B-128E-Instruct-FP8-20260507_214934-no-brite\"" + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "id": "2fec2ea8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Percentage of reactions that received at least one candidate: 73.8 %\n", + "Ground truth annotation within top 1: 68.2 %\n", + "Ground truth annotation within top 3: 69.7 %\n", + "Ground truth annotation within top 5: 69.7 %\n" + ] + } + ], + "source": [ + "# with curated species annotations\n", + "summary_df = pd.read_csv(f'./{folder}/per_reaction_results.csv')\n", + "\n", + "# Annotation coverage: % of reactions that receive ≥1 candidate\n", + "print(\"Percentage of reactions that received at least one candidate: \", round(len(summary_df[summary_df['num_candidates']>0])/len(summary_df)*100,1), \"%\")\n", + "# top-1 accuracy:\n", + "print(\"Ground truth annotation within top 1: \", round(len(summary_df[summary_df['top1']==True])/len(summary_df)*100,1), \"%\")\n", + "# top-3 accuracy:\n", + "print(\"Ground truth annotation within top 3: \", round(len(summary_df[summary_df['top3']==True])/len(summary_df)*100,1), \"%\")\n", + "# top-5 accuracy:\n", + "print(\"Ground truth annotation within top 5: \", round(len(summary_df[summary_df['top5']==True])/len(summary_df)*100,1), \"%\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "ea324d82", + "metadata": {}, + "source": [ + "### Curated species, no cofactors" + ] + }, + { + "cell_type": "code", + "execution_count": 77, + "id": "543d9498", + "metadata": {}, + "outputs": [], + "source": [ + "llm_model = \"Llama-3.3-70B-Instruct\"\n", + "folder = \"reaction_evaluation_results/curated_species/Llama-4-Maverick-17B-128E-Instruct-FP8-20260506_014246-no-cofactors\"" + ] + }, + { + "cell_type": "code", + "execution_count": 78, + "id": "0fc682a8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Percentage of reactions that received at least one candidate: 73.7 %\n", + "Ground truth annotation within top 1: 68.9 %\n", + "Ground truth annotation within top 3: 69.6 %\n", + "Ground truth annotation within top 5: 69.6 %\n" + ] + } + ], + "source": [ + "# with curated species annotations\n", + "summary_df = pd.read_csv(f'./{folder}/per_reaction_results.csv')\n", + "\n", + "# Annotation coverage: % of reactions that receive ≥1 candidate\n", + "print(\"Percentage of reactions that received at least one candidate: \", round(len(summary_df[summary_df['num_candidates']>0])/len(summary_df)*100,1), \"%\")\n", + "# top-1 accuracy:\n", + "print(\"Ground truth annotation within top 1: \", round(len(summary_df[summary_df['top1']==True])/len(summary_df)*100,1), \"%\")\n", + "# top-3 accuracy:\n", + "print(\"Ground truth annotation within top 3: \", round(len(summary_df[summary_df['top3']==True])/len(summary_df)*100,1), \"%\")\n", + "# top-5 accuracy:\n", + "print(\"Ground truth annotation within top 5: \", round(len(summary_df[summary_df['top5']==True])/len(summary_df)*100,1), \"%\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "1c47b054", + "metadata": {}, + "source": [ + "### AAAIM species" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "3fa95101", + "metadata": {}, + "outputs": [], + "source": [ + "llm_model = \"Llama-4-Maverick-17B-128E-Instruct-FP8\"\n", + "folder = \"reaction_evaluation_results/aaaim_species/Llama-4-Maverick-17B-128E-Instruct-FP8-20260506_013857\"" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "1e52781f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Percentage of reactions that received at least one candidate: 80.3 %\n", + "Ground truth annotation within top 1: 78.3 %\n", + "Ground truth annotation within top 3: 79.3 %\n", + "Ground truth annotation within top 5: 79.3 %\n" + ] + } + ], + "source": [ + "# with curated species annotations\n", + "summary_df = pd.read_csv(f'./{folder}/per_reaction_results.csv')\n", + "\n", + "# Annotation coverage: % of reactions that receive ≥1 candidate\n", + "print(\"Percentage of reactions that received at least one candidate: \", round(len(summary_df[summary_df['num_candidates']>0])/len(summary_df)*100,1), \"%\")\n", + "# top-1 accuracy:\n", + "print(\"Ground truth annotation within top 1: \", round(len(summary_df[summary_df['top1']==True])/len(summary_df)*100,1), \"%\")\n", + "# top-3 accuracy:\n", + "print(\"Ground truth annotation within top 3: \", round(len(summary_df[summary_df['top3']==True])/len(summary_df)*100,1), \"%\")\n", + "# top-5 accuracy:\n", + "print(\"Ground truth annotation within top 5: \", round(len(summary_df[summary_df['top5']==True])/len(summary_df)*100,1), \"%\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "4e645122", + "metadata": {}, + "source": [ + "#### Failure reasons" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "789b6a31", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "failure_reason\n", + "no_candidates 713\n", + "SSX 10\n", + "Name: count, dtype: int64" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Failure reasons\n", + "failure_reasons = summary_df[summary_df[\"failure_reason\"].notna()]\n", + "failure_reasons[\"failure_reason\"].value_counts()" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "9c1cb44f", + "metadata": {}, + "outputs": [], + "source": [ + "failure_reasons = summary_df[summary_df[\"failure_reason\"].notna()]\n", + "\n", + "results = []\n", + "\n", + "for _, row in failure_reasons.iterrows():\n", + " if row[\"failure_reason\"] != \"no_candidates\":\n", + " continue\n", + "\n", + " model_id = row[\"model_id\"]\n", + " reaction_id = row[\"reaction_id\"]\n", + " gt = row[\"ground_truth_kegg\"] # e.g. \"R01845\"\n", + " gt_formatted = f\"KEGG:{gt}\"\n", + "\n", + " file_path = os.path.join(folder, \"_work\", f\"{model_id}.xml_recommendations.csv\")\n", + "\n", + " if not os.path.exists(file_path):\n", + " label = \"file not found\"\n", + " else:\n", + " rec_df = pd.read_csv(file_path)\n", + "\n", + " sub = rec_df[rec_df[\"id\"] == reaction_id]\n", + "\n", + " if sub.empty:\n", + " label = \"reaction not found in recommendations\"\n", + " else:\n", + " if sub[\"annotation\"].astype(str).str.contains(gt_formatted).any():\n", + " label = \"LLM did not recognize correct annotation\"\n", + " else:\n", + " label = \"no correct candidates were generated\"\n", + "\n", + " results.append({\n", + " \"model_id\": model_id,\n", + " \"reaction_id\": reaction_id,\n", + " \"ground_truth_kegg\": gt,\n", + " \"label\": label\n", + " })\n", + "\n", + "output_df = pd.DataFrame(results)\n", + "output_df.to_csv(f\"./{folder}/failure_reasons.csv\", index=False)" + ] + }, + { + "cell_type": "markdown", + "id": "279961de", + "metadata": {}, + "source": [ + "#### Complexity analysis" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "2515c057", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Wrote C:\\Users\\user\\Documents\\research\\AAAIM\\tests\\reaction_evaluation_results\\aaaim_species\\Llama-4-Maverick-17B-128E-Instruct-FP8-20260506_013857\\per_reaction_with_complexity.csv\n", + "Wrote C:\\Users\\user\\Documents\\research\\AAAIM\\tests\\reaction_evaluation_results\\aaaim_species\\Llama-4-Maverick-17B-128E-Instruct-FP8-20260506_013857\\complexity_impact_summary.csv\n", + "complexity_bin n_reactions coverage top1 top3 top5 mean_candidates\n", + " 1 17 0.647 0.647 0.647 0.647 0.647\n", + " 2 439 0.879 0.868 0.877 0.877 1.130\n", + " 3 501 0.846 0.820 0.846 0.846 0.986\n", + " 4 1244 0.803 0.788 0.803 0.803 0.850\n", + " 5+ 1476 0.743 0.742 0.743 0.743 0.752\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
complexity_binn_reactionscoveragetop1top3top5mean_candidates
01170.6470.6470.6470.6470.647
124390.8790.8680.8770.8771.130
235010.8460.8200.8460.8460.986
3412440.8030.7880.8030.8030.850
45+14760.7430.7420.7430.7430.752
\n", + "
" + ], + "text/plain": [ + " complexity_bin n_reactions coverage top1 top3 top5 mean_candidates\n", + "0 1 17 0.647 0.647 0.647 0.647 0.647\n", + "1 2 439 0.879 0.868 0.877 0.877 1.130\n", + "2 3 501 0.846 0.820 0.846 0.846 0.986\n", + "3 4 1244 0.803 0.788 0.803 0.803 0.850\n", + "4 5+ 1476 0.743 0.742 0.743 0.743 0.752" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = run_complexity_analysis(f\"./tests/{folder}/per_reaction_results.csv\") # take 64min\n", + "result.per_reaction.to_csv(f'./{folder}/complexity_list.csv', index=False)\n", + "result.summary" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "a2306539", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "On average, 79.3% of reactions in each model were annotated correctly.\n" + ] + } + ], + "source": [ + "pct_found = (\n", + " summary_df\n", + " .sort_values(\"model_id\")\n", + " .groupby(\"model_id\")[\"found\"]\n", + " .mean()\n", + " .mul(100)\n", + " .reset_index(name=\"pct_found\")\n", + ").round(1)\n", + "pct_found\n", + "\n", + "avg_annotated_correctly = round(pct_found['pct_found'].mean(), 1)\n", + "print(f\"On average, {avg_annotated_correctly}% of reactions in each model were annotated correctly.\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "2d55245c", + "metadata": {}, + "source": [ + "Which models did AAAIM fail at annotating?" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "df94abc5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
model_idpct_found
20BIOMD00000001080.0
23BIOMD00000001370.0
62BIOMD00000006910.0
\n", + "
" + ], + "text/plain": [ + " model_id pct_found\n", + "20 BIOMD0000000108 0.0\n", + "23 BIOMD0000000137 0.0\n", + "62 BIOMD0000000691 0.0" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pct_found[pct_found['pct_found']==0]" + ] + }, + { + "cell_type": "markdown", + "id": "1ea2bfa9", + "metadata": {}, + "source": [ + "Make a histogram of successfully annotated models" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "987a81b9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Distribution of Percentages\\n Llama-4-Maverick-17B-128E-Instruct-FP8 with curated species')" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAj8AAAHcCAYAAAA5lMuGAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAPYQAAD2EBqD+naQAAV/1JREFUeJzt3Qm8TPUf//EvrjVrdrIlWQqpVJZWREm2FpISaSUpLSqhjepnKSlRqF9EG4qIZC1kKVIpu192su/L+T/e3///zH9m7sy9c++duXPvPa/n4zGuOXPmzPd8z/Y53+V8szmO4xgAAACPyB7vBAAAAKQngh8AAOApBD8AAMBTCH4AAICnEPwAAABPIfgBAACeQvADAAA8heAHAAB4CsEPAADwFIIfIBX69etnsmXLli6/dd1119mXa+7cufa3v/jii3T5/U6dOpmKFSuajOzw4cPm/vvvN6VKlbJ58/jjj8c7SQAyMIIfeN7YsWPtBdN95cmTx5QpU8Y0bdrUvP322+bQoUNR+Z1t27bZoOnXX381GU1GTlskXnvtNbsdH374YfPf//7XdOzYMey8CuT8t3eJEiXM1VdfbSZNmmSygnfffdfmBYDwsjG2F7xOF4r77rvPvPTSS6ZSpUrm1KlTZseOHbaEZdasWaZ8+fLm66+/NrVq1fJ95/Tp0/alQClSy5YtM3Xr1jVjxoyxpSmROnnypP2bK1cu+1fpuv76683nn39ubrvtthSta2rSpvw4e/asyZ07t8morrrqKpOQkGAWLlyY7LwKfooUKWKefPJJX+D3/vvvmw0bNpj33nvPPPTQQyYzu/jii02xYsXsfgIgtIQw0wHPuemmm8zll1/ue9+7d2/zww8/mFtuucXceuut5s8//zR58+a1n+lCq1csHT161OTLl88X9MRLzpw5TUa3a9cuU6NGjYjnL1u2rLn77rt97++55x5zwQUXmCFDhqQ5+Dl+/LjdZtmzU7AOZFQcnUASbrjhBtOnTx+zefNm88knnyTZ5kelRA0bNjSFCxc2+fPnN1WrVjXPPfec/Ux34SpZEZUyuVUubvWE2vTojn358uXmmmuusUGP+93gNj+uM2fO2HnUzuWcc86xAdr//ve/RKUcoUqZ/JeZXNpCtfk5cuSILTkpV66cLRHSuv7nP/8xwQXJWk63bt3M5MmT7fpp3osuusjMmDEj4qCmS5cupmTJkraUrXbt2uajjz5K1P5p48aNZtq0ab60b9q0yaSE8rB69ep2Oa6tW7eazp0729920z169OiA77m/P2HCBPPCCy/YoErb7uDBg/bzJUuWmJtvvtmWNGkbqfTwrbfeCljGmjVrbAneueeea9dRAbhKGkNVzf7444/miSeeMMWLF7fLa926tdm9e7dvPm2n33//3cybN8+XF+52/vfff02vXr1MzZo17f5ZsGBBG/CvXLkyUX5of9f+pN9QtWDPnj3Nd999Z5cXXKKkdWzWrJkpVKiQXfdrr73WptOfqo7VDkvpU15qmU2aNDErVqxI0XYCooWSHyAZaj+iIGPmzJmma9euIefRBUclRLq4qfpMJ/h169b5LgK6sGr6iy++aB544AHbxkTq16/vW8bevXvtxahdu3a2VEIX3aS8+uqr9mL0zDPP2CBh6NChpnHjxrbdjltCFYlI0uZPAY4ujHPmzLGBySWXXGIvjE899ZQNGFR64k9VUV999ZV55JFHTIECBWw7qrZt25otW7aYokWLhk3XsWPH7IVb+agASlWSqupTMLZ//37To0cPm3a18dHF+bzzzvNVZSk4SAlV7SlwdNOzc+dOW5XmBm9a3vTp0+36KrAJblD98ssv29IeBRcnTpyw/1cwrH2idOnSNq0KsFR6OHXqVPve3W8aNGhgg6Znn33WBhufffaZadWqlfnyyy9tcOOve/fuNpDq27evDfC0zZW+iRMn2s/1XvMouHn++eftNHc/UrWegtDbb7/d5qXWUdV9Clb++OMP287NDWwV9G/fvt2X7vHjx9vtHUwlo9pnL7vsMpsmlXap6lTfX7BggbniiivsfCpNUwN9pVUldNrXtV8oPy699NIUbSsgKtTmB/CyMWPGqLjCWbp0adh5ChUq5NSpU8f3vm/fvvY7riFDhtj3u3fvDrsMLV/z6PeCXXvttfazESNGhPxML9ecOXPsvGXLlnUOHjzom/7ZZ5/Z6W+99ZZvWoUKFZx777032WUmlTZ9X8txTZ482c77yiuvBMx32223OdmyZXPWrVvnm6b5cuXKFTBt5cqVdvqwYcOcpAwdOtTO98knn/imnTx50qlXr56TP3/+gHVX+po3b57k8vznvfHGG+220kvpadeunf2t7t2723m6dOnilC5d2tmzZ0/AdzWf9oWjR48GbIvzzz/f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AADIksHPnDlzop8SAACAjBr8XHvttdFPCQAAQDpgbC8AAOApBD8AAMBTCH4AAICnEPwAAABPIfgBAACeEnFvrzp16kQ8hteKFSvSkiYAAID4Bz+tWrWKXSoAAAAyWvDTt2/f2KYEAAAgHdDmBwAAeEqqnvCssbuGDBliPvvsM7NlyxZz8uTJgM///fffaKUPAAAg/iU//fv3N4MHDzZ33nmnOXDggHniiSdMmzZtTPbs2U2/fv2im0IAAIB4Bz/jxo0zo0aNMk8++aRJSEgw7du3Nx988IF58cUXzeLFi6OZPgAAgPgHPzt27DA1a9a0/8+fP78t/ZFbbrnFTJs2LbopBAAAiHfwc95555nt27fb/1euXNnMnDnT/n/p0qUmd+7c0UwfAABA/IOf1q1bm9mzZ9v/d+/e3fTp08dUqVLF3HPPPaZz587RTSEAAEC8e3sNHDjQ9381eq5QoYL56aefbADUokWLaKYPAAAg/sHP/PnzTf369W1jZ7nqqqvs6/Tp0/aza665JrqpBAAAiGe11/XXXx/yWT5q+KzPAAAAslTw4zhOyEFO9+7da84555xopAsAACD+1V56kKEo8OnUqVNAzy499XnVqlW2OgwAACBLBD+FChXylfwUKFDA5M2b1/dZrly5bLufrl27Rj+VAAAA8Qh+xowZY/9WrFjR9OrViyouAADgjd5effv2tX93795t/vrrL/v/qlWrmuLFi0c3dQAAABmhwfPRo0ftwwxLly5tu7XrVaZMGdOlSxf7GQAAQJYKfnr27GnmzZtnvvnmG7N//377mjJlip2mwU4BAAAyqlRVe3355Zfmiy++MNddd51v2s0332wbQN9xxx3mvffei2YaAQAA4l/tVbJkyUTTS5QoQbUXAADIesFPvXr1bKPn48eP+6YdO3bM9O/f334GAACQpaq9hg4dapo1a2bOO+88U7t2bTtt5cqVJk+ePOa7776LdhoBAADiG/zUrFnTrF271owbN86sWbPGTmvfvr3p0KFDwIMPAQAAstSo7sFPc2ZUdwAAkNExqjsAAPAURnUHAACewqjuAADAUxjVHQAAeAqjugMAAE9J06juAAAAWbbBsx5quHjx4mTnO3TokHn99dfN8OHD05o2AACA+JX83H777aZt27a23U+LFi3M5ZdfbsqUKWOf6rxv3z7zxx9/mIULF5pvv/3W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rI2rB9JF7ty5TalSpUyFChXMww8/bBo3bmy+/vpr+5kCIt39ly1b1pxzzjnmyiuvtHfArrFjx5rChQvb+WvUqGGXtWXLFvu9Z555xpQrV85OU4nShx9+6Pve6tWrbcmHLqyqBurYsaPZs2eP7/PrrrvOPPbYY+bpp5825557rk2fqmBcFStWtH9bt25tAwn3vQIMBRdappZdt25d8/333wesr0pdmjdvbvLmzWsqVapkxo8fb78/dOhQ3zy6u7///vtN8eLFTcGCBc0NN9xgVq5cmWQ+/vbbb3Y+Lbdo0aLmgQceMIcPH7afKe0tWrSw/1dgGC74cUsXpk+fbi677DKbdwsXLjRnz541AwYMsOnV8mvXrm2++OIL3/d0IerSpYvv86pVq5q33nor0fJHjx5tLrroIrvc0qVLm27duiWZn8HVXkqHSgbPO+88uwx9NmPGjESlHF999ZW5/vrrTb58+WxaFy1a5Jtn8+bNNi+KFCli9ymlR0F3OP/973/N5ZdfbgoUKGD3g7vuusvs2rUrUZ6phErz6Tfr169v/vrrL9887npoWVq3QoUKmXbt2plDhw755tE+q32uRIkSJk+ePKZhw4Zm6dKlvvXS+ojSrd9TqYVo/TWvjgNt91tuucUX6Iq2idSpU8d+T/u264MPPjDVq1e3v1etWjXz7rvvBqz7zz//bL+nz7Vuv/zyS9h88l+PpI69efPmmSuuuMK3Dzz77LMBJb1Kn/aLxx9/3BQrVsw0bdo01ful/P777zZPdBxpG1599dU2f7RNPvroIzNlyhRf6bP/ucUNspT+//znPwHTVYqm+detW5dsfiATivfIqsj6NApwy5YtA6bdeuutzqWXXmr/f//99zv169d35s+f76xbt85588037SjBf//9t/1cI7vnzJnTzvPjjz86a9ascY4cOeLccccdTrly5ZyvvvrKWb9+vfP99987EyZMsN/Zt2+fU7x4cad3797On3/+6axYscJp0qSJc/311weMZq6Rhvv162d/66OPPnKyZcvmzJw5036+a9cu38jyGtVc7+XXX391RowY4fz222/2ey+88IIdUXrz5s2+ZTdu3Ni55JJLnMWLFzvLly+3v5U3b15nyJAhAfO0aNHCWbp0qV3Ok08+6RQtWtSOhBzK4cOHndKlSztt2rSxvz179mynUqVKvlGWNVqy0qo0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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ax = plt.gca()\n", + "ax.set_axisbelow(True) # grid behind bars\n", + "plt.grid(True) # y-axis gridlines only\n", + "\n", + "plt.hist(pct_found[\"pct_found\"], bins=20)\n", + "plt.xlabel(\"Percentage of reactions annotated correctly\")\n", + "plt.ylabel(\"Count (total n=75)\")\n", + "plt.title(f\"Distribution of Percentages\\n {llm_model} with curated species\")" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "445c87b3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "25 out of 75 models had an annotation accuracy < 80%\n", + "about 33.33%\n", + "\n", + "42 out of 75 models had an annotation accuracy < 80%\n", + "about 56.0%\n" + ] + } + ], + "source": [ + "print(f\"{len(pct_found[(pct_found['pct_found']<80)])} out of 75 models had an annotation accuracy < 80%\")\n", + "print(f\"about {round(len(pct_found[(pct_found['pct_found']<80)])*100/75,2)}%\")\n", + "print()\n", + "print(f\"{len(pct_found[(pct_found['pct_found']>=80)])} out of 75 models had an annotation accuracy < 80%\")\n", + "print(f\"about {round(len(pct_found[(pct_found['pct_found']>=80)])*100/75,2)}%\")" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "id": "28bcb555", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Recall: 0.79\n" + ] + } + ], + "source": [ + "recall = summary_df['found'].mean()\n", + "print(f\"Recall: {round(recall,2)}\")" + ] + }, + { + "cell_type": "markdown", + "id": "3dfab7c5", + "metadata": {}, + "source": [ + "## gpt-5-mini" + ] + }, + { + "cell_type": "markdown", + "id": "5c01b8f0", + "metadata": {}, + "source": [ + "### Curated species" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "id": "e8a6b6f5", + "metadata": {}, + "outputs": [], + "source": [ + "llm_model = \"gpt-5-mini\"\n", + "folder = \"reaction_evaluation_results/curated_species/gpt-5-mini-2025-08-07-20260503_023732\"" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "id": "327fab6a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Percentage of reactions that received at least one candidate: 83.9 %\n", + "Ground truth annotation within top 1: 78.8 %\n", + "Ground truth annotation within top 3: 79.5 %\n", + "Ground truth annotation within top 5: 79.5 %\n" + ] + } + ], + "source": [ + "# with curated species annotations\n", + "summary_df = pd.read_csv(f'./{folder}/per_reaction_results.csv')\n", + "\n", + "# Annotation coverage: % of reactions that receive ≥1 candidate\n", + "print(\"Percentage of reactions that received at least one candidate: \", round(len(summary_df[summary_df['num_candidates']>0])/len(summary_df)*100,1), \"%\")\n", + "# top-1 accuracy:\n", + "print(\"Ground truth annotation within top 1: \", round(len(summary_df[summary_df['top1']==True])/len(summary_df)*100,1), \"%\")\n", + "# top-3 accuracy:\n", + "print(\"Ground truth annotation within top 3: \", round(len(summary_df[summary_df['top3']==True])/len(summary_df)*100,1), \"%\")\n", + "# top-5 accuracy:\n", + "print(\"Ground truth annotation within top 5: \", round(len(summary_df[summary_df['top5']==True])/len(summary_df)*100,1), \"%\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "b73c90a3", + "metadata": {}, + "source": [ + "#### Failure reasons" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "id": "b680836c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "failure_reason\n", + "no_candidates 940\n", + "SSX 2\n", + "Name: count, dtype: int64" + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Failure reasons\n", + "failure_reasons = summary_df[summary_df[\"failure_reason\"].notna()]\n", + "failure_reasons[\"failure_reason\"].value_counts()" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "id": "7b47bf0b", + "metadata": {}, + "outputs": [], + "source": [ + "failure_reasons = summary_df[summary_df[\"failure_reason\"].notna()]\n", + "\n", + "results = []\n", + "\n", + "for _, row in failure_reasons.iterrows():\n", + " if row[\"failure_reason\"] != \"no_candidates\":\n", + " continue\n", + "\n", + " model_id = row[\"model_id\"]\n", + " reaction_id = row[\"reaction_id\"]\n", + " gt = row[\"ground_truth_kegg\"] # e.g. \"R01845\"\n", + " gt_formatted = f\"KEGG:{gt}\"\n", + "\n", + " file_path = os.path.join(folder, \"_work\", f\"{model_id}.xml_recommendations.csv\")\n", + "\n", + " if not os.path.exists(file_path):\n", + " label = \"file not found\"\n", + " else:\n", + " rec_df = pd.read_csv(file_path)\n", + "\n", + " sub = rec_df[rec_df[\"id\"] == reaction_id]\n", + "\n", + " if sub.empty:\n", + " label = \"reaction not found in recommendations\"\n", + " else:\n", + " if sub[\"annotation\"].astype(str).str.contains(gt_formatted).any():\n", + " label = \"LLM did not recognize correct annotation\"\n", + " else:\n", + " label = \"no correct candidates were generated\"\n", + "\n", + " results.append({\n", + " \"model_id\": model_id,\n", + " \"reaction_id\": reaction_id,\n", + " \"ground_truth_kegg\": gt,\n", + " \"label\": label\n", + " })\n", + "\n", + "output_df = pd.DataFrame(results)\n", + "output_df.to_csv(f\"./{folder}/failure_reasons.csv\", index=False)" + ] + }, + { + "cell_type": "markdown", + "id": "74017eb2", + "metadata": {}, + "source": [ + "#### Complexity analysis" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "id": "d06ea2c8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Wrote C:\\Users\\user\\Documents\\research\\AAAIM\\tests\\reaction_evaluation_results\\curated_species\\gpt-5-mini-2025-08-07-20260503_023732\\per_reaction_with_complexity.csv\n", + "Wrote C:\\Users\\user\\Documents\\research\\AAAIM\\tests\\reaction_evaluation_results\\curated_species\\gpt-5-mini-2025-08-07-20260503_023732\\complexity_impact_summary.csv\n", + "complexity_bin n_reactions coverage top1 top3 top5 mean_candidates\n", + " 1 17 0.882 0.882 0.882 0.882 0.882\n", + " 2 570 0.807 0.779 0.800 0.804 1.068\n", + " 3 717 0.845 0.827 0.844 0.845 0.964\n", + " 4 1961 0.796 0.790 0.795 0.796 0.917\n", + " 5+ 2573 0.779 0.777 0.779 0.779 0.846\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
complexity_binn_reactionscoveragetop1top3top5mean_candidates
01170.8820.8820.8820.8820.882
125700.8070.7790.8000.8041.068
237170.8450.8270.8440.8450.964
3419610.7960.7900.7950.7960.917
45+25730.7790.7770.7790.7790.846
\n", + "
" + ], + "text/plain": [ + " complexity_bin n_reactions coverage top1 top3 top5 mean_candidates\n", + "0 1 17 0.882 0.882 0.882 0.882 0.882\n", + "1 2 570 0.807 0.779 0.800 0.804 1.068\n", + "2 3 717 0.845 0.827 0.844 0.845 0.964\n", + "3 4 1961 0.796 0.790 0.795 0.796 0.917\n", + "4 5+ 2573 0.779 0.777 0.779 0.779 0.846" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = run_complexity_analysis(f\"./tests/{folder}/per_reaction_results.csv\") # take 64min\n", + "result.per_reaction.to_csv(f'./{folder}/complexity_list.csv', index=False)\n", + "result.summary" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "On average, 84.7% of reactions in each model were annotated correctly.\n" + ] + } + ], + "source": [ + "pct_found = (\n", + " summary_df\n", + " .sort_values(\"model_id\")\n", + " .groupby(\"model_id\")[\"found\"]\n", + " .mean()\n", + " .mul(100)\n", + " .reset_index(name=\"pct_found\")\n", + ").round(1)\n", + "\n", + "df_gpt5 = pct_found\n", + "\n", + "avg_annotated_correctly = round(pct_found['pct_found'].mean(), 1)\n", + "print(f\"On average, {avg_annotated_correctly}% of reactions in each model were annotated correctly.\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "0ade0dd6", + "metadata": {}, + "source": [ + "Which models did AAAIM fail at annotating?" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "id": "20c5d060", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
model_idpct_found
22BIOMD00000001080.0
\n", + "
" + ], + "text/plain": [ + " model_id pct_found\n", + "22 BIOMD0000000108 0.0" + ] + }, + "execution_count": 48, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pct_found[pct_found['pct_found']==0]" + ] + }, + { + "cell_type": "markdown", + "id": "8d0ce273", + "metadata": {}, + "source": [ + "Make a histogram of successfully annotated models" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "id": "7d7c747f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Distribution of Percentages\\n gpt-5-mini with curated species')" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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sSeF1q1b22AHkQ1kKOfI+Prrr50rrrjCXvRUrlzZGTVqVMDrGsLap08fp2jRonadRo0aORs2bMiw/HrR4cOH7bGr3+IcOXI4l156qb33ycmTJ33rUM7pM3/+/JC/yQoewy3Xv//+2wYuurdP3rx5nUceecQGSJHG7NcAAMCz6CMDAAA8i0AGAAB4FoEMAADwLAIZAADgWQQyAADAswhkAACAZxHIAAAAzyKQAQAAnkUgA5xn+vXrZyd/zJQpk5k6darxQn6vvPLKjM4G/s/vv/9ujx3NvXW+uhC28UJCIIOoa9++vf3R0EOTbl522WXm1VdftTOhxjuvBAOuX3/91fTv39/8+9//Njt37jTNmzc38V6ezz77bMDkcxeyBQsW2DIKnn08NZyYU/79adWqVUZnA1GUJZqJA65bbrnFjBkzxpw8edJ8++23pkuXLnaW5d69e6c5rTNnztgf7eBZb2HM5s2b7d877rjDllE4NEuJyjRLloz5OcidO7d94MJ16tQpe5ETT8clvIMzAWIie/bsdtr3smXLms6dO5vGjRubr776yr6m4EZX5SVLljQXX3yxneJdV6ausWPHmvz589v1q1atatPatm2bfd/zzz9vSpcubZeppufjjz/2vW/dunW2RkInSTW1PPTQQ2bfvn2+1xs2bGiefvpp06tXL1OgQAGbPzVzuMqVK2f/tm7d2gYF7nMFCwoUlKbSrl27tvnuu+8Ctle1IS1atDA5c+a0U96PHz/evn/IkCG+dXTV/eijj5rChQubvHnzmptvvtmsWbMmxXL86aef7HpKt2DBguaxxx4zR48eta8p7y1btrT/V5CXXCDjXvXPmDHDXH311bbsFi9ebM6ePWsGDBhg86v0a9asaSZPnux7n04qHTt29L1eqVIlM3To0CTpjx492lSrVs2mW7x4cfPUU0+lWJ7BTUvKh2rsSpUqZdPQazNnzkxS+/Dll1+am266yeTKlcvmNSEhwbfOH3/8YcvikksusceU8qMAOjn//e9/zTXXXGPy5Mljj4P777/f7NmzJ0mZqeZI6+kz69WrZzZs2OBbx90OpaVty5cvn7n33nvNkSNHfOvomNUxV6RIEZMjRw5Tv359s3z5ct92aXtE+dbnqTZBtP1aV98D7ffbbrvNF7SK9onUqlXLvk/Htuujjz4yVapUsZ9XuXJl88EHHwRs+w8//GDfp9e1bT/++GOy5eS/HSl99xYuXGiuvfZa3zHwwgsvBNTAKn86Lrp3724KFSpkmjVrlu7jUn7++WdbJvoeaR/ecMMNtny0Tz755BMzbdo0X62w/2+LGzAp///6178Clqt2S+tv2rQp1fJABov4NJRAEM2Wqine/d1+++3OVVddZf//6KOPOvXq1XMWLVrkbNq0yXnnnXfsbKq//fabfV0zYGfNmtWu87///c9Zv369c+zYMefuu+92Spcu7Xz55ZfO5s2bne+++86ZMGGCfc+BAwecwoULO71793Z+/fVXZ9WqVU6TJk2cm266KWDWZ83I2q9fP/tZn3zyiZMpUyZn9uzZ9vU9e/b4ZuDW7M96LqtXr3ZGjhzp/PTTT/Z9L7/8sp15153eXho3buxceeWVdlb0lStX2s/KmTOnM3jw4IB1WrZs6Sxfvtym88wzzzgFCxa0M8aGcvToUad48eJOmzZt7GfPnTvXKV++vG82Ws0qq7wqz8pvcjNWu7Pa1qhRw26rylyf+frrr9tZmmfOnGnLU2lpPyxYsMC+79SpU84rr7xi87tlyxbn008/tbNjT5w40Zf2Bx98YMtiyJAhdibcH374wbfNyZVn3759nZo1a/rSGDRokN0vn332md3XvXr1svvfPR62bt1q01Fep0+fbj/nzjvvdMqWLeucPn3artOiRQu7v9euXWu3RTNRL1y4MNlj9OOPP3a+/fZbu25CQoJTt25dp3nz5knKrE6dOrY8fv75Z+eGG26wx6RL26FZft39o+O5WLFizosvvuhb5+mnn3ZKlChhP0tpaN9plnGV/z///ON88cUX9nO0TSqjgwcP2vdNnjzZvrZx40bnxx9/tMdN9erVnTNnztjXVc56n74Dep97DGkf6ZjRe7XP9LdAgQLO2LFjfceMvif333+/s27dOltOmkFaaelzkpPSd2/79u32uNAM6vruTZkyxSlUqJAtH5e+Dyqr5557zu5jPdJ7XOrztE0qdx2bKrvRo0fbNLV9yustt9zi+05oZmz3GHK38Y033nCqVq0asI3aVw0aNEi2DBA/CGQQ00BGU7/PmTPH/hA9++yz9uSfOXNmZ8eOHQHv0ZTwCkLEPTkrgHDpx0rLlFYor732mtO0adOAZX/++afvJOH+mNavXz9gndq1azvPP/+877nW1w9xaqpVq+YMGzbM/l8/3nqfflRdOgFpmXtS//777+3J+sSJEwHpVKhQwfn3v/8d8jNGjRplT3oKaFzffPONc9FFFzm7du2yz5XX1K5P3BPG1KlTfcuUD518lixZErBux44dnfvuuy/ZtLp06eK0bdvW91wn6ZdeeinZ9UOVZ3AgozR0YgneLzoxinsS+uijj3yvKyjQMpW96CSvADW9tO+Unk6E/mWmE7Z/2WvZ8ePHfduhMjx8+LBvHZ2oFfyI9psCsnHjxvleV3Co7X377bcDPkeBeEr27t1r11PA5F8mwcGHjqfx48cn+W4oUBMdawqe3W2QESNGpBjIpPbdU+BWqVIl+113DR8+3AYubuCl716tWrUiclzqd0IBvcoy3Aup4PLS749+h5YtW2afKy0FX27Ah/hG4yNiYvr06bYZ5vTp07aqWFX3qvZVNa+aLC6//PIkVdeqQnep/bxGjRoB1b6ZM2c2N954Y8jPUxPN/PnzQ/a9UJWz+3n+aYqqwf2bFEJRU47y/s0339gmJFWZHz9+3DZ3iZob1K5/1VVX+d6jqms1F/jnT+n4b6MoHf8mg+COvKpWV1OJ6/rrr7flqc9UU1daqBnBperzxMRE06RJkyR9F9Ts4Bo+fLhtOtK2Kq963W0WUrn99ddfplGjRia9Dh8+bNPQdvnT8+BmN/99p/3m5kHNJ2q+URPm7NmzbTNm27Ztk+xrfytXrrT7VJ9x4MABW6ai7VRzZmqfWaZMGft/NSmpaSPU8aT9quPff9vUT0xNMNq3Kdm4caN55ZVXzLJly2zzqH/+rrjiipDvOXbsmP1MNQd26tTJt1zHq5q9RJ+rbVKzkqtu3bop5iW1757SVBr+TZvaZh3v27dv95WVmo8icVwqP2pKUlmmV4kSJWxTsI5t7Y+vv/7a/gbddddd6U4TsUMgg5hQ2/+IESNsQKIfDbcDn37c9KOoE4n++vMPQtQ27v/DqOcpUbrqI/HWW28lec09AUnwj58+wz1JJEf9eebMmWPb1BWgKC933nmn/XENl/KnfAS314v6QcSCf0Dk9rNRcKa+Sv7UV0EmTJhgt/3dd9+1JyqdsN955x17cg1nn0Sa/75zjw1336nvkfpdaHsUzKiPhfLdtWvXkCd8ravHuHHjbJ8lBQh6HrxPU/rM4NfDPZ7CoWNZ/cs+/PBD+/1RmgpgUjrm3H2q96jfmb/g71paRGo/+x9/53JcRio/OmbUj27w4MF2YMI999xj+0Ih/hHIICb046STfjBdValGRletuqoKV/Xq1e2PuToV6oo7mGpDvvjiC3uFfC6jHnRiUv78/e9//7OdMNVp1f2xVUdNlzrB6qpXnSbdq05dWepK3z9/u3btsnlzO72mRh021fFZJ173x155Ucdefea58O9EndyVtj5LHVyffPJJ3zL/2iMFNtoWdYh1O62GU57+1FlTJ2p9ln8+9FxXymmhjqhPPPGEfWh0nE7ooQKZ9evXm7///tsMHDjQvkdWrFhhIq1ChQo2kNe2KCgR1dCos686vYo7cse/jJQ31bgp/+53RJ1g/YV6n2roVJZbtmwxDzzwQLLHlDonnzhxwlcrs3Tp0nP67ilNfffUkugGe9pmHR/qwB3p41I1SurQq7IMVSujsknpmHPdeuut9nulCy51rl60aFGa8oqMw6glZCg18ehH9uGHH7ajULZu3WpHUegKWldhydEJs127dqZDhw72viR6n2o3Pv/8c/u6hnfv37/f3HffffZEoRPurFmzzCOPPBLWj5r/5+jErKDDDUQqVqxo86oqbTVFqJnM/6pbTRv6gdeIIm2LAhr9379WSa+rVkP3t1CNgQKhJUuWmJdeeinZk6jKSScbbbdGZKnpTCdmXUWmtVkpmE4yqm3p0aOHPSmovFatWmWGDRtmn7vbrbypHH/77TfTp08f34gbl5pnVPPx3nvv2eYQN42UyjPYc889Z2vSJk6caE/gGvGisu7WrVvY26PAQPnUcaE8qKx0gg1FTR062SmfOulrdNxrr71mIk0nSTV3aft0ovzll19sk4+aTtT8IwpwdIyoKXbv3r02SFaTpJogR40aZQPiefPmmZ49ewakrVFQOr6U7u7du82hQ4fsct1TSN8l7Q/tM416U23DoEGD7Os6dvV5yofyo5FdwaN30vrdU6D7559/2mNTQaJGDPXt29fmOa23TAjnuNToJzVJaoSYjk8ddwrO3BFlyu/atWvtczXLKeAJRbVUukBR0KtjPbUmNsSRjO6kg/NfqM52/tzRMOXKlbOdITXKonXr1nbEidvZN1++fEnepw6KPXr0sOtny5bNueyyy+xoBZdGuSid/Pnz2xFDGvnQvXt3XydEdTjs1q1bQJrKpzsKSL766iubbpYsWeyoGLejoEY/KU2N3Hj//feTpPXXX3/ZUS/q1Kz3qcNlkSJF7GgnlzqFdu3a1Xb21HYrrQceeMDZtm1bsmWlMtFna2SQRmp06tTJ1yE1rZ19gzuUqlw02kgdNZUfjWZp1qyZb7SPOl62b9/e7guVaefOnZ0XXnghoKOuaBvdNLRvtI0plWdwZ191CFVH3ZIlS9o09NqMGTN8r4fq2Kpt0TJtmzz11FO2o6vKX9vx0EMPOfv27Uu2TLR/dPxpfXWEVT79PyNUmek1LVN+Qm2HqHO3u53uMavyUEdSfdb1119vRxz5e/XVV+1oJ42gc49FdaytUqWKfY9G9WjETnDH6Q8//NAeQ+r8rePRpc7FGkGn74g6i2skjkYbuTRKS/nW61rPHTmV0qil1L57yp86aOs1bYs60LsjypL77qX3uJQ1a9bYzv3qGJwnTx47okwjnESj4zSCTZ2N3WMkuc7Reo+Wu52v4Q2Z9E9GB1PA+U6dHNVsofvNnEtnWADR8/3339vvp2qUzrWWE7FDIANEgar/1Syg/gQa2aSb7u3YscNW75/L6AoAkacRSmrKU5OZboioTt/wDvrIAFGgdvgXX3zR3lFWnYI1Ekb9CAhigPjz2Wef2f5Jutv222+/ndHZQRpRIwMAADyLGhkAAOBZBDIAAMCzCGQAAIBnEcgAAADPIpABAACeRSADAAA8i0AGAAB4FoEMAADwLAIZAABgvOr/AfOZSQ9fOKC3AAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ax = plt.gca()\n", + "ax.set_axisbelow(True) # grid behind bars\n", + "plt.grid(True) # y-axis gridlines only\n", + "\n", + "plt.hist(pct_found[\"pct_found\"], bins=20)\n", + "plt.xlabel(\"Percentage of reactions annotated correctly\")\n", + "plt.ylabel(\"Count (total n=75)\")\n", + "plt.title(f\"Distribution of Percentages\\n {llm_model} with curated species\")" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "id": "1bd3a776", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "27 out of 75 models had an annotation accuracy < 80%\n", + "about 36.0%\n", + "\n", + "47 out of 75 models had an annotation accuracy < 80%\n", + "about 62.67%\n" + ] + } + ], + "source": [ + "print(f\"{len(pct_found[(pct_found['pct_found']<80)])} out of 75 models had an annotation accuracy < 80%\")\n", + "print(f\"about {round(len(pct_found[(pct_found['pct_found']<80)])*100/75,2)}%\")\n", + "print()\n", + "print(f\"{len(pct_found[(pct_found['pct_found']>=80)])} out of 75 models had an annotation accuracy < 80%\")\n", + "print(f\"about {round(len(pct_found[(pct_found['pct_found']>=80)])*100/75,2)}%\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### AAAIM species" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "id": "aef21f45", + "metadata": {}, + "outputs": [], + "source": [ + "llm_model = \"gpt-5-mini\"\n", + "folder = \"reaction_evaluation_results/aaaim_species/gpt-5-mini-2025-08-07-20260506_014810\"" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "id": "06ce53e6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Percentage of reactions that received at least one candidate: 96.2 %\n", + "Ground truth annotation within top 1: 94.6 %\n", + "Ground truth annotation within top 3: 95.3 %\n", + "Ground truth annotation within top 5: 95.3 %\n" + ] + } + ], + "source": [ + "# with curated species annotations\n", + "summary_df = pd.read_csv(f'./{folder}/per_reaction_results.csv')\n", + "\n", + "# Annotation coverage: % of reactions that receive ≥1 candidate\n", + "print(\"Percentage of reactions that received at least one candidate: \", round(len(summary_df[summary_df['num_candidates']>0])/len(summary_df)*100,1), \"%\")\n", + "# top-1 accuracy:\n", + "print(\"Ground truth annotation within top 1: \", round(len(summary_df[summary_df['top1']==True])/len(summary_df)*100,1), \"%\")\n", + "# top-3 accuracy:\n", + "print(\"Ground truth annotation within top 3: \", round(len(summary_df[summary_df['top3']==True])/len(summary_df)*100,1), \"%\")\n", + "# top-5 accuracy:\n", + "print(\"Ground truth annotation within top 5: \", round(len(summary_df[summary_df['top5']==True])/len(summary_df)*100,1), \"%\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "9976844c", + "metadata": {}, + "source": [ + "#### Failure reasons" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "5135beab", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "failure_reason\n", + "no_candidates 164\n", + "SSX 3\n", + "Name: count, dtype: int64" + ] + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Failure reasons\n", + "failure_reasons = summary_df[summary_df[\"failure_reason\"].notna()]\n", + "failure_reasons[\"failure_reason\"].value_counts()" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "id": "ca6911bc", + "metadata": {}, + "outputs": [], + "source": [ + "failure_reasons = summary_df[summary_df[\"failure_reason\"].notna()]\n", + "\n", + "results = []\n", + "\n", + "for _, row in failure_reasons.iterrows():\n", + " if row[\"failure_reason\"] != \"no_candidates\":\n", + " continue\n", + "\n", + " model_id = row[\"model_id\"]\n", + " reaction_id = row[\"reaction_id\"]\n", + " gt = row[\"ground_truth_kegg\"] # e.g. \"R01845\"\n", + " gt_formatted = f\"KEGG:{gt}\"\n", + "\n", + " file_path = os.path.join(folder, \"_work\", f\"{model_id}.xml_recommendations.csv\")\n", + "\n", + " if not os.path.exists(file_path):\n", + " label = \"file not found\"\n", + " else:\n", + " rec_df = pd.read_csv(file_path)\n", + "\n", + " sub = rec_df[rec_df[\"id\"] == reaction_id]\n", + "\n", + " if sub.empty:\n", + " label = \"reaction not found in recommendations\"\n", + " else:\n", + " if sub[\"annotation\"].astype(str).str.contains(gt_formatted).any():\n", + " label = \"LLM did not recognize correct annotation\"\n", + " else:\n", + " label = \"no correct candidates were generated\"\n", + "\n", + " results.append({\n", + " \"model_id\": model_id,\n", + " \"reaction_id\": reaction_id,\n", + " \"ground_truth_kegg\": gt,\n", + " \"label\": label\n", + " })\n", + "\n", + "output_df = pd.DataFrame(results)\n", + "output_df.to_csv(f\"./{folder}/failure_reasons.csv\", index=False)" + ] + }, + { + "cell_type": "markdown", + "id": "055d4865", + "metadata": {}, + "source": [ + "#### Complexity analysis" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "id": "5c91903f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Wrote C:\\Users\\user\\Documents\\research\\AAAIM\\tests\\reaction_evaluation_results\\aaaim_species\\gpt-5-mini-2025-08-07-20260506_014810\\per_reaction_with_complexity.csv\n", + "Wrote C:\\Users\\user\\Documents\\research\\AAAIM\\tests\\reaction_evaluation_results\\aaaim_species\\gpt-5-mini-2025-08-07-20260506_014810\\complexity_impact_summary.csv\n", + "complexity_bin n_reactions coverage top1 top3 top5 mean_candidates\n", + " 1 17 0.824 0.824 0.824 0.824 0.824\n", + " 2 473 0.911 0.892 0.907 0.907 1.082\n", + " 3 582 0.942 0.921 0.940 0.940 1.050\n", + " 4 1452 0.957 0.949 0.957 0.957 1.003\n", + " 5+ 1855 0.967 0.965 0.967 0.967 0.975\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
complexity_binn_reactionscoveragetop1top3top5mean_candidates
01170.8240.8240.8240.8240.824
124730.9110.8920.9070.9071.082
235820.9420.9210.9400.9401.050
3414520.9570.9490.9570.9571.003
45+18550.9670.9650.9670.9670.975
\n", + "
" + ], + "text/plain": [ + " complexity_bin n_reactions coverage top1 top3 top5 mean_candidates\n", + "0 1 17 0.824 0.824 0.824 0.824 0.824\n", + "1 2 473 0.911 0.892 0.907 0.907 1.082\n", + "2 3 582 0.942 0.921 0.940 0.940 1.050\n", + "3 4 1452 0.957 0.949 0.957 0.957 1.003\n", + "4 5+ 1855 0.967 0.965 0.967 0.967 0.975" + ] + }, + "execution_count": 57, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = run_complexity_analysis(f\"./tests/{folder}/per_reaction_results.csv\") # take 64min\n", + "result.per_reaction.to_csv(f'./{folder}/complexity_list.csv', index=False)\n", + "result.summary" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "id": "a6e0f1a2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "On average, 87.6% of reactions in each model were annotated correctly.\n" + ] + } + ], + "source": [ + "pct_found = (\n", + " summary_df\n", + " .sort_values(\"model_id\")\n", + " .groupby(\"model_id\")[\"found\"]\n", + " .mean()\n", + " .mul(100)\n", + " .reset_index(name=\"pct_found\")\n", + ").round(1)\n", + "pct_found\n", + "\n", + "avg_annotated_correctly = round(pct_found['pct_found'].mean(), 1)\n", + "print(f\"On average, {avg_annotated_correctly}% of reactions in each model were annotated correctly.\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "be531a3d", + "metadata": {}, + "source": [ + "Which models did AAAIM fail at annotating?" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "id": "e7d95b5b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
model_idpct_found
20BIOMD00000001080.0
23BIOMD00000001370.0
\n", + "
" + ], + "text/plain": [ + " model_id pct_found\n", + "20 BIOMD0000000108 0.0\n", + "23 BIOMD0000000137 0.0" + ] + }, + "execution_count": 59, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pct_found[pct_found['pct_found']==0]" + ] + }, + { + "cell_type": "markdown", + "id": "5725c5e6", + "metadata": {}, + "source": [ + "Make a histogram of successfully annotated models" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "id": "790fdc55", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Distribution of Percentages\\n gpt-5-mini with curated species')" + ] + }, + "execution_count": 60, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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O5s8efKEXH/p4YcfNoMHD05PkgAAALEJZNauXWtuuummFJ/XHDJatgAAACDhApndu3eHnD/GlTlzZrN3795I5AsAACCygUyJEiXsUgQpWbdunSlWrFh6kgQAAIhNINO8eXPTp08fc/z48WTPHTt2zPTt29fcfPPNGc8NAABAtIZfv/DCC2batGnmX//6l+nataupWLGifVxzyWidpdOnT5vnn38+PUkCAADEJpApUqSIWbZsmXn00UdN7969jeM4vjllmjZtaoMZbQMAAJCQE+KVLl3afPnll+bvv/82mzZtssFMhQoVzMUXXxydHAIAAER6Zl8FLrVq1croywEAAOKzaCQAAEAiIJABAACeRSADAAA8i0AGAACcf519AQBAZJV59ot4Z8FzqJEBAACeRSADAAA8i0AGAAB4FoEMAADwLAIZAADgWXENZJYsWWJatmxpihcvbheenDFjRsDzWsfpxRdfNMWKFTM5cuQwjRo1Mhs3boxbfgEAQGKJayBz5MgRU6NGDbtqdiivv/66eeutt8yoUaPMihUrzEUXXWRX2T5+/HjM8woAABJPXOeRadasmb2FotqYoUOHmhdeeMHceuut9rH//Oc/pkiRIrbm5q677opxbgEAQKJJ2Anxtm7danbt2mWbk1x58+Y1derUMUlJSSkGMidOnLA318GDB+3fU6dO2VukuGlFMk2ERlnHBuUcG5RzbFDOsRWNcg43zYQNZBTEiGpg/Om++1woAwcONP3790/2+Ny5c03OnDkjns958+ZFPE2ERlnHBuUcG5RzbHivnBP2shzzcj569Og5XGKp6N27t+nVq1dAjUzJkiVNkyZNTJ48eSIaKerANW7c2GTJkiVi6SI5yjo2KOfYoJxjw6vl3D1prvGixlEoZ7dFxbOBTNGiRe3f3bt321FLLt2//PLLU3xdtmzZ7C2YCjgaJ3O00kVylHVsUM6xQTnHBuXs3XION72EnUembNmyNphZsGBBQHSm0Ut169aNa94AAEBiiGuNzOHDh82mTZsCOviuWbPG5M+f35QqVcr06NHDvPLKK6ZChQo2sOnTp4+dc6ZVq1bxzDYAAEgQcQ1kVq1aZW644QbffbdvS8eOHc348ePN008/beeaeeihh8z+/fvNNddcY2bPnm2yZ88ex1wDAIBEEddA5vrrr7fzxaREs/2+9NJL9gYAAOCZ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Hjk7Tz31lK2Vueuuu+yosPbt25uePXvaUZBCOUdHOOWqv/qu8ffPP//YkUyRLnsCmXRS1fCVV15p22X9f33pft26deOaNy9TfzIFMdOnTzcLFy60wyn9qcyzZMkSUO4anq0LA+UevoYNG5rvv//e/nJ1b6o5UFW8+3/K+eypWTR4+gD14yhdurT9v85vfZn7l7OaSNR3gHIO39GjR22fC3/6oanvZKGcoyOcctVf/SDSjyeXvtt1bNSXJqIi2nX4PBp+rd7Z48ePtz2zH3roITv8eteuXfHOmmc9+uijdijfokWLnJ07d/puR48eDRgWrCHZCxcutMOC69ata284O/6jloRyjszQ9syZM9vhwRs3bnQmTJjg5MyZ0/nwww8Dhq/qe+PTTz911q1b59x6660MC06njh07OiVKlPANv9ZQ4YIFCzpPP/20bxvKOeMjG7/77jt7U6gwePBg+//ffvst7HLV8OuaNWvaKQiWLl1qR0oy/DqBDB8+3H7Zaz4ZDcfWOHlknD4ooW6aW8alD8hjjz3mXHzxxfai0Lp1axvsILKBDOUcGZ9//rlz2WWX2R89lSpVckaPHh3wvIaw9unTxylSpIjdpmHDhs7PP/8ct/x60cGDB+25q+/i7NmzO5deeqmd++TEiRO+bSjnjPnqq69CficreAy3XP/8808buGhunzx58jj333+/DZAijdWvAQCAZ9FHBgAAeBaBDAAA8CwCGQAA4FkEMgAAwLMIZAAAgGcRyAAAAM8ikAEAAJ5FIAMAADyLQAY4x/Tr188u/pgpUyYzY8YM44X8Xn755fHOBv7Pr7/+as8drb11rjof9vF8QiCDqLvvvvvsl4ZuWnSzfPny5qWXXrIroSY6rwQDrp9++sn079/fvPvuu2bnzp2mWbNmJtHL88knnwxYfO58tmjRIltGwauPp4ULc+rfP61atYp3NhBFmaOZOOC66aabzLhx48yJEyfMl19+abp06WJXWe7du3e60zp9+rT90g5e9RbGbN682f699dZbbRmFQ6uUqEwzZ47P10GuXLnsDeevkydP2h85iXRewju4EiAmsmXLZpd9L126tHn00UdNo0aNzGeffWafU3CjX+UlSpQwF110kV3iXb9MXePHjzf58uWz21epUsWmtW3bNvu6Z555xpQsWdI+ppqeMWPG+F63fv16WyOhi6SaWtq3b2/27dvne/766683jz/+uHn66adN/vz5bf7UzOEqU6aM/du6dWsbFLj3FSwoUFCaSrtWrVpm/vz5Afur2pAWLVqYHDly2CXvJ06caF8/dOhQ3zb61f3ggw+aQoUKmTx58pgbb7zRrF27NtVy/P777+12SrdAgQLmoYceMocPH7bPKe8tW7a0/1eQl1Ig4/7qnzVrlrnyyitt2S1dutScOXPGDBw40OZX6deoUcNMnTrV9zpdVDp16uR7vmLFimbYsGHJ0h87dqypWrWqTbdYsWKma9euqZZncNOS8qEau0suucSmoedmz56drPZh2rRp5oYbbjA5c+a0eU1KSvJt89tvv9myuPjii+05pfwogE7Jf//7X3PVVVeZ3Llz2/PgnnvuMXv27ElWZqo50nZ6z3r16pmff/7Zt427H0pL+5Y3b15z1113mUOHDvm20Tmrc65w4cIme/bs5pprrjErV6707Zf2R5RvvZ9qE0T7r231OdBxv/nmm31Bq+iYSM2aNe3rdG673n//fVO5cmX7fpUqVTLvvPNOwL5/88039nV6Xvv23XffpVhO/vuR2mdv8eLFpnbt2r5z4Nlnnw2ogVX+dF706NHDFCxY0DRt2jTD56X88MMPtkz0OdIxvPbaa2356Jh88MEH5tNPP/XVCvt/t7gBk/L/73//O+Bx1W5p+02bNqVZHoiziC9DCQTRaqla4t3fLbfc4lxxxRX2/w8++KBTr149Z8mSJc6mTZucN954w66m+ssvv9jntQJ2lixZ7Db/+9//nA0bNjhHjhxx7rjjDqdkyZLOtGnTnM2bNzvz5893Jk2aZF/z999/O4UKFXJ69+7t/PTTT863337rNG7c2LnhhhsCVn3Wiqz9+vWz7/XBBx84mTJlcubOnWuf37Nnj28Fbq3+rPuyZs0aZ9SoUc73339vX/fCCy/YlXfd5e2lUaNGzuWXX25XRV+9erV9rxw5cjhDhgwJ2KZly5bOypUrbTpPPPGEU6BAAbtibCiHDx92ihUr5rRp08a+94IFC5yyZcv6VqPVqrLKq/Ks/Ka0YrW7qm316tXtvqrM9Z6vvPKKXaV59uzZtjyVlo7DokWL7OtOnjzpvPjiiza/W7ZscT788EO7OvbkyZN9ab/zzju2LIYOHWpXwv3mm298+5xSefbt29epUaOGL43Bgwfb4/LRRx/ZY/3000/b4++eD1u3brXpKK8zZ86073Pbbbc5pUuXdk6dOmW3adGihT3e69ats/uilagXL16c4jk6ZswY58svv7TbJiUlOXXr1nWaNWuWrMzq1Kljy+OHH35wrr32WntOurQfWuXXPT46n4sWLeo899xzvm0ef/xxp3jx4va9lIaOnVYZV/n/888/zieffGLfR/ukMtq/f7993dSpU+1zGzdudL777jt73lSrVs05ffq0fV7lrNfpM6DXueeQjpHOGb1Wx0x/8+fP74wfP953zuhzcs899zjr16+35aQVpJWW3iclqX32tm/fbs8LraCuz9706dOdggUL2vJx6fOgsnrqqafsMdYto+el3k/7pHLXuamyGzt2rE1T+6e83nTTTb7PhFbGds8hdx8HDBjgVKlSJWAfdawaNGiQYhkgcRDIIKaBjJZ+nzdvnv0ievLJJ+3F/8ILL3T++OOPgNdoSXgFIeJenBVAuPRlpceUVigvv/yy06RJk4DHfv/9d99Fwv0yveaaawK2qVWrlvPMM8/47mt7fRGnpWrVqs7w4cPt//XlrdfpS9WlC5Aecy/qX3/9tb1YHz9+PCCdcuXKOe+++27I9xg9erS96CmgcX3xxRfOBRdc4OzatcveV17T+n3iXjBmzJjhe0z50MVn2bJlAdt26tTJufvuu1NMq0uXLk7btm1993WRfv7551PcPlR5BgcySkMXluDjogujuBeh999/3/e8ggI9prIXXeQVoGaUjp3S04XQv8x0wfYvez127Ngx336oDA8ePOjbRhdqBT+i46aAbMKECb7nFRxqf19//fWA91Egnpq9e/fa7RQw+ZdJcPCh82nixInJPhsK1ETnmoJndx9k5MiRqQYyaX32FLhVrFjRftZdI0aMsIGLG3jps1ezZs2InJf6nlBAr7IM94dUcHnp+0ffQytWrLD3lZaCLzfgQ2Kj8RExMXPmTNsMc+rUKVtVrKp7VfuqmldNFv/617+SVV2rCt2l9vPq1asHVPteeOGF5rrrrgv5fmqi+eqrr0L2vVCVs/t+/mmKqsH9mxRCUVOO8v7FF1/YJiRVmR87dsw2d4maG9Suf8UVV/heo6prNRf450/p+O+jKB3/JoPgjryqVldTiat+/fq2PPWeaupKDzUjuFR9fvToUdO4ceNkfRfU7OAaMWKEbTrSviqvet5tFlK57dixwzRs2NBk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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ax = plt.gca()\n", + "ax.set_axisbelow(True) # grid behind bars\n", + "plt.grid(True) # y-axis gridlines only\n", + "\n", + "plt.hist(pct_found[\"pct_found\"], bins=20)\n", + "plt.xlabel(\"Percentage of reactions annotated correctly\")\n", + "plt.ylabel(\"Count (total n=75)\")\n", + "plt.title(f\"Distribution of Percentages\\n {llm_model} with curated species\")" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "id": "729c7d50", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "14 out of 75 models had an annotation accuracy < 80%\n", + "about 18.67%\n", + "\n", + "54 out of 75 models had an annotation accuracy < 80%\n", + "about 72.0%\n" + ] + } + ], + "source": [ + "print(f\"{len(pct_found[(pct_found['pct_found']<80)])} out of 75 models had an annotation accuracy < 80%\")\n", + "print(f\"about {round(len(pct_found[(pct_found['pct_found']<80)])*100/75,2)}%\")\n", + "print()\n", + "print(f\"{len(pct_found[(pct_found['pct_found']>=80)])} out of 75 models had an annotation accuracy < 80%\")\n", + "print(f\"about {round(len(pct_found[(pct_found['pct_found']>=80)])*100/75,2)}%\")" + ] + }, + { + "cell_type": "markdown", + "id": "a4cecee0", + "metadata": {}, + "source": [ + "# Combined Statistics" + ] + }, + { + "cell_type": "code", + "execution_count": 79, + "id": "736abb8d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
model_idBIOMD0000000013BIOMD0000000015BIOMD0000000017BIOMD0000000018BIOMD0000000023BIOMD0000000038BIOMD0000000042BIOMD0000000046BIOMD0000000051BIOMD0000000061BIOMD0000000063BIOMD0000000064BIOMD0000000066BIOMD0000000067BIOMD0000000068BIOMD0000000070BIOMD0000000071BIOMD0000000076BIOMD0000000081BIOMD0000000088BIOMD0000000093BIOMD0000000094BIOMD0000000108BIOMD0000000122BIOMD0000000123BIOMD0000000137BIOMD0000000143BIOMD0000000171BIOMD0000000172BIOMD0000000176BIOMD0000000177BIOMD0000000190BIOMD0000000191BIOMD0000000206BIOMD0000000211BIOMD0000000212BIOMD0000000213BIOMD0000000218BIOMD0000000219BIOMD0000000221BIOMD0000000222BIOMD0000000225BIOMD0000000231BIOMD0000000232BIOMD0000000236BIOMD0000000244BIOMD0000000245BIOMD0000000247BIOMD0000000248BIOMD0000000253BIOMD0000000268BIOMD0000000281BIOMD0000000292BIOMD0000000450BIOMD0000000471BIOMD0000000472BIOMD0000000473BIOMD0000000503BIOMD0000000590BIOMD0000000602BIOMD0000000627BIOMD0000000633BIOMD0000000674BIOMD0000000689BIOMD0000000690BIOMD0000000691BIOMD0000000725BIOMD0000001061BIOMD0000001062BIOMD0000001063BIOMD0000001090BIOMD0000001091BIOMD0000001092BIOMD0000001093
llm388.275.077.891.7100.0100.0100.0100.091.3100.033.390.9100.0100.050.095.890.050.0100.00.0100.0100.0100.0100.0100.00.0100.0100.0100.092.9100.077.8100.033.386.793.3100.0100.0100.0100.090.0100.050.077.8100.0100.0100.0100.0100.066.786.457.10.072.729.931.520.8100.077.8100.090.685.281.8100.0100.00.045.299.897.917.973.238.198.398.7
llm488.264.388.9100.0100.050.088.9100.082.690.933.390.9100.0100.050.095.880.0100.0100.030.850.050.00.0100.0100.00.0100.0100.0100.092.9100.066.7100.016.773.380.063.692.392.990.0100.0100.075.066.7100.0100.066.794.7100.066.781.850.0100.077.331.032.061.8100.077.8100.090.692.677.383.383.30.041.999.599.742.774.338.397.799.4
gpt588.275.077.8100.0100.0100.088.9100.095.790.966.790.9100.0100.050.087.570.0100.0100.046.2100.0100.00.0100.0100.0100.0100.0100.0100.0100.0100.077.8100.066.780.093.3100.092.3100.0100.0100.066.775.066.775.0100.066.794.7100.066.777.378.650.081.876.178.279.7100.0100.097.187.592.668.283.383.350.045.299.5100.077.674.538.496.899.7
\n", + "
" + ], + "text/plain": [ + "model_id BIOMD0000000013 BIOMD0000000015 BIOMD0000000017 BIOMD0000000018 \\\n", + "llm3 88.2 75.0 77.8 91.7 \n", + "llm4 88.2 64.3 88.9 100.0 \n", + "gpt5 88.2 75.0 77.8 100.0 \n", + "\n", + "model_id BIOMD0000000023 BIOMD0000000038 BIOMD0000000042 BIOMD0000000046 \\\n", + "llm3 100.0 100.0 100.0 100.0 \n", + "llm4 100.0 50.0 88.9 100.0 \n", + "gpt5 100.0 100.0 88.9 100.0 \n", + "\n", + "model_id BIOMD0000000051 BIOMD0000000061 BIOMD0000000063 BIOMD0000000064 \\\n", + "llm3 91.3 100.0 33.3 90.9 \n", + "llm4 82.6 90.9 33.3 90.9 \n", + "gpt5 95.7 90.9 66.7 90.9 \n", + "\n", + "model_id BIOMD0000000066 BIOMD0000000067 BIOMD0000000068 BIOMD0000000070 \\\n", + "llm3 100.0 100.0 50.0 95.8 \n", + "llm4 100.0 100.0 50.0 95.8 \n", + "gpt5 100.0 100.0 50.0 87.5 \n", + "\n", + "model_id BIOMD0000000071 BIOMD0000000076 BIOMD0000000081 BIOMD0000000088 \\\n", + "llm3 90.0 50.0 100.0 0.0 \n", + "llm4 80.0 100.0 100.0 30.8 \n", + "gpt5 70.0 100.0 100.0 46.2 \n", + "\n", + "model_id BIOMD0000000093 BIOMD0000000094 BIOMD0000000108 BIOMD0000000122 \\\n", + "llm3 100.0 100.0 100.0 100.0 \n", + "llm4 50.0 50.0 0.0 100.0 \n", + "gpt5 100.0 100.0 0.0 100.0 \n", + "\n", + "model_id BIOMD0000000123 BIOMD0000000137 BIOMD0000000143 BIOMD0000000171 \\\n", + "llm3 100.0 0.0 100.0 100.0 \n", + "llm4 100.0 0.0 100.0 100.0 \n", + "gpt5 100.0 100.0 100.0 100.0 \n", + "\n", + "model_id BIOMD0000000172 BIOMD0000000176 BIOMD0000000177 BIOMD0000000190 \\\n", + "llm3 100.0 92.9 100.0 77.8 \n", + "llm4 100.0 92.9 100.0 66.7 \n", + "gpt5 100.0 100.0 100.0 77.8 \n", + "\n", + "model_id BIOMD0000000191 BIOMD0000000206 BIOMD0000000211 BIOMD0000000212 \\\n", + "llm3 100.0 33.3 86.7 93.3 \n", + "llm4 100.0 16.7 73.3 80.0 \n", + "gpt5 100.0 66.7 80.0 93.3 \n", + "\n", + "model_id BIOMD0000000213 BIOMD0000000218 BIOMD0000000219 BIOMD0000000221 \\\n", + "llm3 100.0 100.0 100.0 100.0 \n", + "llm4 63.6 92.3 92.9 90.0 \n", + "gpt5 100.0 92.3 100.0 100.0 \n", + "\n", + "model_id BIOMD0000000222 BIOMD0000000225 BIOMD0000000231 BIOMD0000000232 \\\n", + "llm3 90.0 100.0 50.0 77.8 \n", + "llm4 100.0 100.0 75.0 66.7 \n", + "gpt5 100.0 66.7 75.0 66.7 \n", + "\n", + "model_id BIOMD0000000236 BIOMD0000000244 BIOMD0000000245 BIOMD0000000247 \\\n", + "llm3 100.0 100.0 100.0 100.0 \n", + "llm4 100.0 100.0 66.7 94.7 \n", + "gpt5 75.0 100.0 66.7 94.7 \n", + "\n", + "model_id BIOMD0000000248 BIOMD0000000253 BIOMD0000000268 BIOMD0000000281 \\\n", + "llm3 100.0 66.7 86.4 57.1 \n", + "llm4 100.0 66.7 81.8 50.0 \n", + "gpt5 100.0 66.7 77.3 78.6 \n", + "\n", + "model_id BIOMD0000000292 BIOMD0000000450 BIOMD0000000471 BIOMD0000000472 \\\n", + "llm3 0.0 72.7 29.9 31.5 \n", + "llm4 100.0 77.3 31.0 32.0 \n", + "gpt5 50.0 81.8 76.1 78.2 \n", + "\n", + "model_id BIOMD0000000473 BIOMD0000000503 BIOMD0000000590 BIOMD0000000602 \\\n", + "llm3 20.8 100.0 77.8 100.0 \n", + "llm4 61.8 100.0 77.8 100.0 \n", + "gpt5 79.7 100.0 100.0 97.1 \n", + "\n", + "model_id BIOMD0000000627 BIOMD0000000633 BIOMD0000000674 BIOMD0000000689 \\\n", + "llm3 90.6 85.2 81.8 100.0 \n", + "llm4 90.6 92.6 77.3 83.3 \n", + "gpt5 87.5 92.6 68.2 83.3 \n", + "\n", + "model_id BIOMD0000000690 BIOMD0000000691 BIOMD0000000725 BIOMD0000001061 \\\n", + "llm3 100.0 0.0 45.2 99.8 \n", + "llm4 83.3 0.0 41.9 99.5 \n", + "gpt5 83.3 50.0 45.2 99.5 \n", + "\n", + "model_id BIOMD0000001062 BIOMD0000001063 BIOMD0000001090 BIOMD0000001091 \\\n", + "llm3 97.9 17.9 73.2 38.1 \n", + "llm4 99.7 42.7 74.3 38.3 \n", + "gpt5 100.0 77.6 74.5 38.4 \n", + "\n", + "model_id BIOMD0000001092 BIOMD0000001093 \n", + "llm3 98.3 98.7 \n", + "llm4 97.7 99.4 \n", + "gpt5 96.8 99.7 " + ] + }, + "execution_count": 79, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dfs = {\n", + " \"llm3\": df_llm3,\n", + " \"llm4\": df_llm4,\n", + " \"gpt5\": df_gpt5\n", + "}\n", + "\n", + "# set model_id as index and extract pct_found\n", + "aligned = {\n", + " name: df.set_index(\"model_id\")[\"pct_found\"]\n", + " for name, df in dfs.items()\n", + "}\n", + "\n", + "# combine into one dataframe\n", + "score_comparison_df = pd.DataFrame(aligned).T\n", + "score_comparison_df.dropna(axis=1, inplace=True)\n", + "score_comparison_df.to_csv('annotation_rate_by_model.csv')\n", + "score_comparison_df" + ] + }, + { + "cell_type": "code", + "execution_count": 80, + "id": "83c264c9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "llm3 49\n", + "llm4 31\n", + "gpt5 52\n", + "dtype: int64" + ] + }, + "execution_count": 80, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# how many times did each model have the highest number in the column?\n", + "counts = score_comparison_df.eq(score_comparison_df.max(axis=0), axis=1).sum(axis=1)\n", + "counts" + ] + }, + { + "cell_type": "code", + "execution_count": 81, + "id": "f25bd72b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
model_idBIOMD0000000038BIOMD0000000063BIOMD0000000068BIOMD0000000076BIOMD0000000088BIOMD0000000093BIOMD0000000094BIOMD0000000108BIOMD0000000137BIOMD0000000206BIOMD0000000231BIOMD0000000281BIOMD0000000292BIOMD0000000471BIOMD0000000472BIOMD0000000473BIOMD0000000691BIOMD0000000725BIOMD0000001063BIOMD0000001091
llm3100.033.350.050.00.0100.0100.0100.00.033.350.057.10.029.931.520.80.045.217.938.1
llm450.033.350.0100.030.850.050.00.00.016.775.050.0100.031.032.061.80.041.942.738.3
gpt5100.066.750.0100.046.2100.0100.00.0100.066.775.078.650.076.178.279.750.045.277.638.4
\n", + "
" + ], + "text/plain": [ + "model_id BIOMD0000000038 BIOMD0000000063 BIOMD0000000068 BIOMD0000000076 \\\n", + "llm3 100.0 33.3 50.0 50.0 \n", + "llm4 50.0 33.3 50.0 100.0 \n", + "gpt5 100.0 66.7 50.0 100.0 \n", + "\n", + "model_id BIOMD0000000088 BIOMD0000000093 BIOMD0000000094 BIOMD0000000108 \\\n", + "llm3 0.0 100.0 100.0 100.0 \n", + "llm4 30.8 50.0 50.0 0.0 \n", + "gpt5 46.2 100.0 100.0 0.0 \n", + "\n", + "model_id BIOMD0000000137 BIOMD0000000206 BIOMD0000000231 BIOMD0000000281 \\\n", + "llm3 0.0 33.3 50.0 57.1 \n", + "llm4 0.0 16.7 75.0 50.0 \n", + "gpt5 100.0 66.7 75.0 78.6 \n", + "\n", + "model_id BIOMD0000000292 BIOMD0000000471 BIOMD0000000472 BIOMD0000000473 \\\n", + "llm3 0.0 29.9 31.5 20.8 \n", + "llm4 100.0 31.0 32.0 61.8 \n", + "gpt5 50.0 76.1 78.2 79.7 \n", + "\n", + "model_id BIOMD0000000691 BIOMD0000000725 BIOMD0000001063 BIOMD0000001091 \n", + "llm3 0.0 45.2 17.9 38.1 \n", + "llm4 0.0 41.9 42.7 38.3 \n", + "gpt5 50.0 45.2 77.6 38.4 " + ] + }, + "execution_count": 81, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "FAILURE_CUTOFF = 50\n", + "failed_annotations_df = score_comparison_df.loc[:, (score_comparison_df <= FAILURE_CUTOFF).any(axis=0)]\n", + "failed_annotations_df" + ] + }, + { + "cell_type": "code", + "execution_count": 82, + "id": "689a0094", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Index(['BIOMD0000000068', 'BIOMD0000000088', 'BIOMD0000000691',\n", + " 'BIOMD0000000725', 'BIOMD0000001091'],\n", + " dtype='object', name='model_id')" + ] + }, + "execution_count": 82, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# which models where all 3 LLM models scored a 50% or less?\n", + "score_comparison_df.columns[(score_comparison_df <= 50).all(axis=0)]" + ] + }, + { + "cell_type": "markdown", + "id": "aff38857", + "metadata": {}, + "source": [ + "Looks like there's only 2 kegg reactions to annotate in BIOMD68, and one of them is consistently annotated incorrectly. \n", + "\n", + "For BIOMD 88, 108, and 691, the species and reactions are not named in meaningful ways. \n", + "for BIOMD 725, the species are named in meaningful ways, but the reactions are not. \n", + "\n", + "Which reactions in BIOMD 725 are not being annotated correctly? " + ] + }, + { + "cell_type": "code", + "execution_count": 111, + "id": "7064eb0e", + "metadata": {}, + "outputs": [], + "source": [ + "# mean score per model_id\n", + "def create_model_meaningful_table(llm_model_id):\n", + " col_means = score_comparison_df.loc[llm_model_id] #score_comparison_df.mean(axis=0)\n", + "\n", + " # attach scores\n", + " meaningful_df = pd.read_csv(\"meaningful.csv\")\n", + " meaningful_df = meaningful_df.replace(\"mixed\", \"y\")\n", + " df = meaningful_df.set_index(\"model_id\").join(col_means.rename(\"score\"))\n", + "\n", + " # pivot into 2x2 table\n", + " result = df.pivot_table(\n", + " index=\"species\", # rows\n", + " columns=\"reactions\", # columns\n", + " values=\"score\",\n", + " aggfunc=\"mean\"\n", + " )\n", + "\n", + " # optional: rename for clarity\n", + " result.index = result.index.map({\n", + " \"y\": \"Species meaningful\",\n", + " \"n\": \"Species not meaningful\"\n", + " })\n", + " result.columns = result.columns.map({\n", + " \"y\": \"Reaction meaningful\",\n", + " \"n\": \"Reaction not meaningful\"\n", + " })\n", + "\n", + " print(f\"Results for {llm_model_id}\")\n", + " return result.round(2)" + ] + }, + { + "cell_type": "code", + "execution_count": 113, + "id": "d3ce4147", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Results for llm3\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
reactionsReaction not meaningfulReaction meaningful
species
Species not meaningful32.4543.35
Species meaningful79.7489.17
\n", + "
" + ], + "text/plain": [ + "reactions Reaction not meaningful Reaction meaningful\n", + "species \n", + "Species not meaningful 32.45 43.35\n", + "Species meaningful 79.74 89.17" + ] + }, + "execution_count": 113, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "create_model_meaningful_table(\"llm3\")" + ] + }, + { + "cell_type": "code", + "execution_count": 114, + "id": "7a28b743", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Results for llm4\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
reactionsReaction not meaningfulReaction meaningful
species
Species not meaningful18.4236.65
Species meaningful77.3887.13
\n", + "
" + ], + "text/plain": [ + "reactions Reaction not meaningful Reaction meaningful\n", + "species \n", + "Species not meaningful 18.42 36.65\n", + "Species meaningful 77.38 87.13" + ] + }, + "execution_count": 114, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "create_model_meaningful_table(\"llm4\")" + ] + }, + { + "cell_type": "code", + "execution_count": 112, + "id": "f2468f21", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Results for gpt5\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
reactionsReaction not meaningfulReaction meaningful
species
Species not meaningful61.2065.00
Species meaningful85.4389.11
\n", + "
" + ], + "text/plain": [ + "reactions Reaction not meaningful Reaction meaningful\n", + "species \n", + "Species not meaningful 61.20 65.00\n", + "Species meaningful 85.43 89.11" + ] + }, + "execution_count": 112, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "create_model_meaningful_table(\"gpt5\")" + ] + }, + { + "cell_type": "code", + "execution_count": 88, + "id": "9370d4cc", + "metadata": {}, + "outputs": [], + "source": [ + "# how does length of reactions and species affect the annotation accuracy?\n", + "folder = \"reaction_evaluation_results/curated_species/gpt-5-mini-2025-08-07-20260503_023732\"\n", + "model_times_df = pd.read_csv(f\"./{folder}/per_model_timing.csv\") # take 64min" + ] + }, + { + "cell_type": "code", + "execution_count": 108, + "id": "e2d0ddee", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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TnksCazluZHA+pprrlHBdl779u3DLbfcop4vGYFu06YN/vzzT3z88ccYN24c1q9fr9omF1lXWDrAxo0b0bVrV9hsNvU4d999NzIyMgrsqwkTJqB69epqm2HDhsHpdOZu8/bbb6Nhw4Zqn0hfrr/+epzO0qVLcckll6jHrFmzJh588EFkZmaqvz355JNo165dgdu0aNFCPddi5cqVuPLKK9XzGR8fjy5dumDNmjVFPt7vv/+u+p2SkpK7TtJnZJ2k04hjx46p57JGjRqIiopC8+bNMWPGDL/nQV5rb7zxRu5z6rvtmfal9G3gwIHq7/Icvvrqq6d9foiIShODX6JzYDKZVMA6adIkFXidi99++w3JyclYvHgxJk6cqFIIJMCrWLGiCuDuvfde3HPPPQUeR4LjRx99FGvXrkWHDh3Qu3dvFbgICW4kkGvVqhVWrVqlAshDhw7hxhtv9LuPadOmwWq14o8//sC7775baPskyJEgRYK+DRs2qKC1T58+2LZtW276QLNmzVRb5N8jRowosq/5H6847ZSA6ZFHHlF/l5QKo9GI/v37q6BcSIAqQd/+/fvxww8/qED3scceU3+/6aabVLukfdI2uci6/OQxpF/ynEtA+dVXX+HXX39VwXpeCxcuVIG3LKUvEkj7gmlpnwSvEphK+of05dJLLy3yuZD7ueqqq3Ddddep5/WLL75QwbDvMQcMGIC//vrLL9DfvHmz2vbWW29V19PT0zFo0CB1uxUrVqjAu2fPnmr92crOzkbr1q3x888/q2BWfgRIECtt8b0e5PV211135T6nErgXZ1/Ka1YC5++//x7z5s1TwfjpgnUiolKlEdFZGTRokNa3b1/17/bt22t33nmn+vesWbO0vG+tMWPGaC1atPC77WuvvabVrl3b777kutvtzl3XuHFj7ZJLLsm97nK5tOjoaG3GjBnq+s6dO9XjvPTSS7nbOJ1O7bzzztPGjx+vrj/77LNa9+7d/R5779696nZbt25V17t06aK1atXqjP1NSkrSnn/+eb91bdu21YYOHZp7Xfop/T2dwh6vOO3M78iRI+rvGzduVNffe+89LTY2Vjt27Fih2xe2H4Tch+wzMWXKFK1ixYpaRkZG7t9//vlnzWg0agcPHvTbV7I/fG644QbtpptuUv/+5ptvtLi4OC0tLU0rjiFDhmh3332337olS5aox7Tb7eq6tPuZZ57J/fvIkSO1du3aFXmf8jqS5+LHH38stJ8LFy5U10+cOJH797Vr16p18roqSq9evbRHH33Ub18+9NBDJdqX6enpmtVq1b788svcv8s+s9lsBe6LiKgscOSXqBRI3q+MAP79999nfR8yKimjmT5yuFgONecdZZZD7IcPH/a7nYy++ZjNZnWo39cOGf2U0Uk5vOy7NGnSRP0t70iijPCdTlpamhqV7tSpk996uX42fc7/eMVpp4wwy2H4evXqIS4uLjdt5P/bOX9XWuM4jj+3rtHIYLBQYjAodimrMlswSyaULExsYvFjIbJgUZKd/8AgERaljGRQt9f39rhfzz3OcxzH7dbzetXp3uM43+f740nv5/19fz+3t7dv2/a4jUQeqoWxECcgMhGPEfcYFzdeK9Yjha37dF2IH5B5pp84pdvb28nT09OH12TsuMbx2HGfueb19fWb+7uzsxP+j44lfsDPUnBWcWBxfIk9MD844encVMPr62syNzcX7kHmlH4dHx/ntpm3lrzIecdRDtpva2uruq8iIp/h56d+W0RKwrY2gmV6ejpkIWMQtL+Ntz/E+dCUurq6d+/JUJb6WbrNXwkIIGIQiPMsCLaUWOz9C7LXq6SffI6oXFtbS5qamsI8kHFOD8yRl/1XlFuX+vr6sIXPVj5b+rOzsyH/TIyCvHIWxk6chahElubm5vAvon9ycjK0+/z8nNzd3b2LbRB5IOpCFIE5IpPNQ9FHhwnTh6z4vszek4uLi6E98ukIYNaMnHbeAcW8tby8vCz7fRGR70bxK1IjKHnGIbCsg9XQ0JDc398HoZGWQKtlbV4ynmmmlANyHDpL86JdXV3J3t5ecElxhasFJxHBSUaXXG0K73t6er48hrx+IuxwXhG+HAwD8q0xnZ2dyfr6evL4+FjS/SVjjJtZjvb29uDCkv1NBTpjRCx+xplkDByG5EV2G9FLpntwcLDk2M/Pz5PW1tYP2+OQIfOOi4z4xV1ubGx8+5w+csiOnC8gjuMDZlm4J4GcLvnmUvckbQ4MDCRDQ0PhPeL+4uIi6ejoKDuneWvZ0tISHh7IsafinmoptB3fWyIi34WxB5EagTvGVvTS0tJf1Q0eHh6ShYWFsOW7srKSHB0d1ey6tHdwcBCqPlB1ACExMjISPuM9YhDnEOeR67N1PTw8nCsEs3BICTePA1kI0ampqSCYxsfHvzyGvH4i0Ih8rK6uBucQIcnhtxi+S+UNKjEg3K6uroIIOzs7C58jxogR0GeE4cvLy1/9YP2o0ICTyiEvtu/HxsZCfIEYSiUcHh6Ge4Dr3NzcJJubm0E4fiSecXRPT0/DAwvfId7BQbDsITv6tru7Gw7hxZEHIO6wtbUVYhuISj4v54QjtDmchiPN9TjUlq24QJsnJyehb7SLO028IoY55XpUeWBOGWfeWhKDGB0dDfcT68g8s1sSR35ERL4T/9qI1BBO+GdjCbiJuHKIVPKknJYvVwmhGseZF23jhlLpgJJXkLq1iI7+/v4g0Nm6xon8rNhgWx7BSdUE2uEUP9dCJH2VvH7yQvjhahN1mJiYCNvyMbiQxAxwRHFAaYN5SbO5VFOgqkJvb29wPuOyXSmU9EKoId66u7tDibK+vr5keXm54rHQ5/39/VDxgLWnmgXXIidcChxrKh/gfOJqk1smKsGcxNAXHHDywwj8mI2NjfDQg+uKUGetYmc4C84rfeKBievzUDM/P//ud2ZmZkJ7xHl4gEsfLGK4j5lf3GDmlDxwJfcca8dYiUfgjlPCLi93LiJSK35w6q1mrYmIiIiI/Mfo/IqIiIhIYVD8ioiIiEhhUPyKiIiISGFQ/IqIiIhIYVD8ioiIiEhhUPyKiIiISGFQ/IqIiIhIYVD8ioiIiEhhUPyKiIiISGFQ/IqIiIhIYVD8ioiIiEhhUPyKiIiISFIUfgHpeB7xtiZbZAAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "x = model_times_df[['model_id', 'num_evaluated_reactions']]\n", + "y = score_comparison_df.T\n", + "\n", + "merged_df = pd.merge(x, y, on=\"model_id\")\n", + "\n", + "plt.scatter(merged_df['num_evaluated_reactions']-0.2, merged_df['llm3'], marker=\"s\", alpha=0.8, label=\"Llama-3\")\n", + "plt.scatter(merged_df['num_evaluated_reactions'], merged_df['llm4'], marker=\"d\", alpha=0.8, label=\"Llama-4\")\n", + "plt.scatter(merged_df['num_evaluated_reactions']+0.2, merged_df['gpt5'], marker=\"*\", alpha=0.8, label=\"gpt-5-mini\")\n", + "plt.legend(bbox_to_anchor=(1.02, 1), loc='upper left', borderaxespad=0.)\n", + "# plt.xscale(\"log\")\n", + "plt.grid()\n", + "plt.gca().set_axisbelow(True)\n", + "plt.grid(True)\n", + "plt.xlim((0,40))\n", + "plt.ylabel(\"Annotation accuracy (%)\")\n", + "plt.xlabel(\"Number of reactions evaluated\")\n", + "plt.title(\"Effect of model size on annotation accuracy\")\n", + "plt.savefig(\"model_size_effect-zoomed.svg\", dpi=300)" + ] + }, + { + "cell_type": "markdown", + "id": "a80ef9ed", + "metadata": {}, + "source": [ + "## Failure reasons" + ] + }, + { + "cell_type": "markdown", + "id": "54e18bda", + "metadata": {}, + "source": [ + "Which model reactions were not correctly annotated across the three different models? " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11fffb3e", + "metadata": {}, + "outputs": [], + "source": [ + "llm_model_folders = [\n", + " \"Llama-3.3-70B-Instruct-20260507_173803\",\n", + " \"Llama-4-Maverick-17B-128E-Instruct-FP8-20260503_020614\",\n", + " \"gpt-5-mini-2025-08-07-20260503_023732\"]\n", + "\n", + "for llm_model in llm_model_folders:\n", + " folder = f\"reaction_evaluation_results/curated_species/{llm_model}\"\n", + " failure_reasons_df = pd.read_csv(f\"./{folder}/failure_reasons.csv\")\n", + " print(f\"Failure reasons for {llm_model}:\")\n", + " print(failure_reasons_df[\"failure_reason\"].value_counts())" + ] + }, + { + "cell_type": "markdown", + "id": "bbcf903f", + "metadata": {}, + "source": [ + "## Timing" + ] + }, + { + "cell_type": "code", + "execution_count": 124, + "id": "6ee5a873", + "metadata": {}, + "outputs": [], + "source": [ + "llm3_aaaim = pd.read_csv(\"reaction_evaluation_results/aaaim_species/Llama-3.3-70B-Instruct-20260503_020542/per_model_timing.csv\")\n", + "llm3_curated = pd.read_csv(\"reaction_evaluation_results/curated_species/Llama-3.3-70B-Instruct-20260507_173803/per_model_timing.csv\")\n", + "\n", + "llm4_aaaim = pd.read_csv(\"reaction_evaluation_results/aaaim_species/Llama-4-Maverick-17B-128E-Instruct-FP8-20260506_013857/per_model_timing.csv\")\n", + "llm4_curated = pd.read_csv(\"reaction_evaluation_results/curated_species/Llama-4-Maverick-17B-128E-Instruct-FP8-20260503_020614/per_model_timing.csv\")\n", + "\n", + "\n", + "gpt_aaaim = pd.read_csv(\"reaction_evaluation_results/aaaim_species/gpt-5-mini-2025-08-07-20260506_014810/per_model_timing.csv\")\n", + "gpt_curated = pd.read_csv(\"reaction_evaluation_results/curated_species/gpt-5-mini-2025-08-07-20260503_023732/per_model_timing.csv\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15424318", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA1IAAAHDCAYAAAAugyvIAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAPYQAAD2EBqD+naQAAYdtJREFUeJzt3Qd4FNXawPE3nYQQAgmh9yaKgBQRKSIgCAqi2Buigl4rIIp4RcUGdrBfG4pXryhXUfET6VXkIggoSgcBqQFDSCF1v+c9YZZNg520bf+fz7pM2Zmzszubeeec854gh8PhEAAAAACA24LdXxUAAAAAoAikAAAAAMAmAikAAAAAsIlACgAAAABsIpACAAAAAJsIpAAAAADAJgIpAAAAALCJQAoAAAAAbCKQAgAAAACbCKQA4DSCgoLkiSee8HQxAo6njvstt9wi0dHR4u/0fTZq1KhEr+3Zs6d5eIsPP/zQfF927txZ4ccCQOAikAJQar/++qtceeWV0rBhQ6lUqZLUrVtXLrroInnttdc8XTR4uf/7v/8jSIVPe/bZZ2XmzJniDX7//XdzPpU0oARgD4EUgFL58ccfpWPHjrJu3ToZPny4vP7663L77bdLcHCwTJkyxdPFgw8EUhMmTChyWXp6ujz66KMVXibAlwMpPZ8IpICKEVpB+wHgp5555hmpWrWqrFq1SmJjY/MtO3jwoMfKFcjS0tIkKirK7fVTU1OlcuXK4m20dhMAAG9FjRSAUtm2bZucddZZhYIolZCQUGjev//9b+nQoYNERkZK9erV5dprr5Xdu3cXWm/lypUyYMAAqVatmrnIb9OmTaEargULFkj37t3Nct3/ZZddJn/88Ue+dbSZi/ad2Lp1q+kHoetp4Dds2DATcLjKyMiQUaNGSY0aNaRKlSoyaNAg2bNnT6GyHTt2TEaOHGn6VERERJj3qU0Z16xZc8pjZZVl48aNcvXVV0tMTIzExcXJ/fffL8ePHy/RsdJ+Kq1bt5bVq1dLjx49TAD1yCOPnLb/j35uenz1fd5www1mWW5urkyePNl8nhrE1KxZU+644w75+++/823j66+/lksuuUTq1Klj3n/Tpk3lqaeekpycHFufo5bljTfeMP/W42I9TtVH6pdffpH+/fubY6fvo3fv3vLTTz8V2V9m+fLlMnr0aPN56r4vv/xyOXTokLhr+/bt0q9fP/Nafa9PPvmkOBwOs0yf9fPX71xB+lnqd0yP3aloGe+55x754osv5MwzzzSfc5cuXUxTWfWvf/1LmjVrZj4L/ZyLqmXQ11rfkfj4eLnxxhvlr7/+KrSe1pjo90S3pc9fffVVkWVy9zvgrqlTp0qvXr3MOaLfFX2fb731VqH19FheeumlsmzZMjn33HPNvps0aSLTpk0rtO6GDRvMNvU916tXT55++mlTbne5eyxefPFFOf/88805qvvS4zxjxoxCn6HeiPjoo4+c31/9Xqs///xT7rrrLmnZsqV5vW7nqquuKvQ5ZmVlmVqk5s2bmzLpet26dZO5c+fmW09/N7QJtf4W6HraEuCbb77J973X7asLL7zQWZ5Fixa5fWwA2OQAgFLo27evo0qVKo5ff/31tOs+/fTTjqCgIMc111zjePPNNx0TJkxwxMfHOxo1auT4+++/nevNmTPHER4e7mjYsKHj8ccfd7z11luO++67z9GnTx/nOnPnznWEhoY6WrRo4Xj++eed26pWrZpjx44dzvX09fpTd8455ziuuOIKs9/bb7/dzHvooYfyle/GG28086+//nrH66+/btZv06aNmafbsehyLd/o0aMd7733nuO5555zDBw40PHvf//7lO/fKsvZZ59t1td9WPu86aabSnSsLrjgAketWrUcNWrUcNx7772Of/3rX46ZM2cWW4ahQ4c6IiIiHE2bNjX/fvvttx3Tpk0zy/S46DEdPny4mT927FhH5cqVHZ06dXJkZmY6tzF48GDH1Vdf7XjhhRfMZ3PVVVeZ9zBmzJh8+zrd5/jjjz86LrroIvPajz/+2PmwFDzuv/32mylP7dq1HU899ZRj0qRJjsaNG5v389NPPznXmzp1qvMz79Wrl+O1115zPPDAA46QkBBT7tPR41KpUiVH8+bNzeein9Oll15qtjl+/Hjnev/85z8dYWFhjsOHD+d7/eeff27WXbJkySn3o+vo96t+/frmveijatWqjgYNGph9nnnmmY6XXnrJ8eijj5rjeOGFF+Z7vfU+9fN55ZVXHA8//LAjMjKy0Hfkhx9+cAQHBztat27tePnll025dT9nnXWW+Wxcufsd0O+dPk5HX3fLLbeY8unnoL8XWmZ9f660HC1btnTUrFnT8cgjj5jl7du3N+eAfu6Wffv2me+6nudPPPGE+Q7q52Sdp67n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j7Y2IyjubrX+0GuzAW0DnpKS4tymXoBZf6j04erBBx80d9r0TmFISMhp76wBgcQbztXXXnvN1FiNGzeuAt4xULH+97//mcQpZ511lkmmYtEARc8BrYF97rnnTA1OwaayRXE9f/R8Uhq8aTCk57K2utAaY33W2ialrTFcX7d06VLn9lzPY02MobRvV8F5Bw8eLLZM2qRPmyq6BnanWh+BhWQTgA2fffaZSeqgnWe7dOliflw1u9fKlSuLXF/vanfu3Fl+++23Iu9wK20qpM0mtP+GtuHWzru6basZhHba1X0WRe8G6kMv/PQOud6p0yZH+nogkHnLubpgwQJZsWKFWdeV1k5pLZlr3yzAW2mWPm26pwkfXGkfKVUwQYPe5NOaWA1sNFgprtlfQa7ZLF0TSxR07rnnmia2ShNc6Llr0aQWFtem7lYZipqntU7FKdhcXl9zqvURWAikABu0XbbeCdNOt67tp4ujd+n0sX79ern++uuL3eabb75pmiCo3bt3m47urh1s3clOZP2wu94VBAKVt5yrr776qunwbtEhCvr16yfTp0/Pd/EHeDNtPq59k15//XW59957i+0nZdEbfBp82eXuazTg0pohpTdJXGuMgIpEIIWApW22C47lon8slKYTL7hM70Brk55p06bJDz/8YPpdaOdxzeSl/y6O3pHWTHrFDfKn29Tt6B1qbb+td/JOl35V76rrfrt16ybVqlUzF4jjx483HWepjYK/8eVztWAKdavJn56rrunWAW+nNxE0cYN+/zWbnTab08x5el5t3LjRNL0rDzpWnJ63eqPj+PHj8t5775lzVZu0A55GIIWApYP8nXPOOfnmabpkpSmKC6Yp1gsobaetfZJ0TAyt3r/uuuvMHW9NbVyc0925e//992XEiBFm0EJtmqfjcxTXlM+i7c51EMDHH3/cdI7XO3M6qOijjz5aqAkR4Ot8+VwF/IUG/3pO6fde+/zp2Gv69+bMM88054Fr7W9Z0qazDzzwgLlpon/7NICbN2+eGUsR8LQgBz3TAQAAAMAWsvYBAAAAgE0EUgAAAABgE4EUAAAAANhEIAUAAAAANhFIAQAAAIBNBFIAAAAAYBOBFAAAAADYRCAFAAAAADYRSAEAAACATQRSAAAAAGATgRQAAAAA2EQgBQAAAABiz/8DeA76c33VI4MAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "from matplotlib.patches import Patch\n", + "\n", + "fig, ax = plt.subplots(figsize=(10, 5))\n", + "\n", + "datasets = [\n", + " (\"gpt-5-mini\", gpt_aaaim['seconds_per_reaction'], 'C0', 3.0, \"aaaim\"),\n", + " (\"gpt-5-mini\", gpt_curated['seconds_per_reaction'], 'C1', 3.3, \"curated\"),\n", + "\n", + " (\"llama-3\", llm3_aaaim['seconds_per_reaction'], 'C0', 1.0, None),\n", + " (\"llama-3\", llm3_curated['seconds_per_reaction'], 'C1', 1.3, None),\n", + "\n", + " (\"llama-4\", llm4_aaaim['seconds_per_reaction'], 'C0', 2.0, None),\n", + " (\"llama-4\", llm4_curated['seconds_per_reaction'], 'C1', 2.3, None),\n", + "]\n", + "\n", + "for label, data, color, pos, _ in datasets:\n", + " ax.boxplot(\n", + " data.dropna(),\n", + " positions=[pos],\n", + " widths=0.25,\n", + " patch_artist=True,\n", + " boxprops=dict(facecolor=color, color=color),\n", + " capprops=dict(color=color),\n", + " whiskerprops=dict(color=color),\n", + " medianprops=dict(color='black'),\n", + " flierprops=dict(\n", + " marker='o',\n", + " markerfacecolor=color,\n", + " markeredgecolor=color,\n", + " markersize=5,\n", + " alpha=0.7\n", + " )\n", + " )\n", + "\n", + "# Center ticks between paired boxplots\n", + "ax.set_xticks([1.15, 2.15, 3.15])\n", + "ax.set_xticklabels(['LLaMa-3', 'LLaMa-4', 'GPT-5-mini'])\n", + "\n", + "ax.set_ylabel('Seconds per reaction')\n", + "ax.set_title('Seconds per reaction by model and dataset')\n", + "\n", + "# Legend\n", + "legend_elements = [\n", + " Patch(facecolor='C0', label='AAAIM'),\n", + " Patch(facecolor='C1', label='Curated')\n", + "]\n", + "\n", + "ax.legend(handles=legend_elements)\n", + "\n", + "ax.grid(axis='y')\n", + "ax.set_axisbelow(True)" + ] + }, + { + "cell_type": "code", + "execution_count": 148, + "id": "dea615e1", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAArcAAAHDCAYAAAA+xjI9AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAPYQAAD2EBqD+naQAAaCZJREFUeJzt3QeYU1X6x/E3M0yj96aAIIqCCjYQsSMoKlhw7a4FxV5AVsW1gQV7W9u6q7i21bWB4l9FQUAUEQuioigoAkrvMDDDzOT//M54QybTkpnMpH0/zxNCbu4kJze5yXvPfc97fH6/328AAABAEkiLdQMAAACAaCG4BQAAQNIguAUAAEDSILgFAABA0iC4BQAAQNIguAUAAEDSILgFAABA0iC4BQAAQNIguAUAAEDSILgFIuTz+ezWW2+NdTNSTqy2+7nnnmv169e3ZKfXudNOO1Xpbw877DB3iRfPPvus+7wsXLjQEpXeC70nSIx9APGF4BZR9+2339rJJ59sHTp0sOzsbNthhx2sX79+9o9//CPWTUOc+7//+z8OHACU6/HHH3cHL1X1xx9/uO+Y2bNnR7VdiC91Yt0AJJdPP/3UDj/8cGvfvr1deOGF1rp1a1u8eLF99tln9vDDD9sVV1wR6yYizoPbxx57rMwAd8uWLVanDl9ZSA3z5s2ztDT6n8oKbps3b17lXm0Ft6NGjXI9tD169Chx37/+9S8rKiqKUksRS/xSIKruuOMOa9Sokc2aNcsaN25c4r4VK1bErF2pLDc31+rWrRv2+ps3b7Z69epZvNFZACBVZGVlxboJKScjIyPWTUCUcFiIqFqwYIF169atVGArLVu2LLXshRdesH333ddycnKsadOmdtppp7me3lAzZ860Y445xpo0aeICr7322sv1BAebPHmyHXzwwe5+Pf/xxx9vP/zwQ4l11COoXLz58+e7I3+tp2D8vPPOc0FgsLy8PBs2bJi1aNHCGjRoYIMGDbIlS5aUatvGjRvt6quvdj0B+kHS61QaxldffVXhtvLa8uOPP9opp5xiDRs2tGbNmtlVV11lW7durdK2Ut7jHnvsYV9++aUdcsghLqi94YYbKs0n1fum7avXeeaZZ7r71IPx0EMPufdTgWWrVq3soosusrVr15Z4jPHjx9uxxx5rbdu2da9/5513tttuu80KCwsjeh/VFvXairaLd6ko5/brr7+2AQMGuG2n19G3b193lqCs/MtPPvnEhg8f7t5PPfeJJ55oK1eutHD98ssvdtRRR7m/1WsdPXq0+f1+d5+u9f7rMxdK76U+Y9p2FVEbL7/8cnv11Veta9eu7n3u3bu3S/ORf/7zn9a5c2f3Xuh9LiufVH/rfUbUu3XWWWfZ77//Xmq9cePGuc+JHkvXb775ZpltCvczEK6xY8faEUcc4fYRfVb0Op944olS62lbHnfccTZ9+nTr2bOne+5OnTrZc889V2rd77//3j2mXvOOO+5ot99+e9i9b8uWLXP7vv5O7WnTpo17D4O3rdeWiRMnup4+tUXtfuONN0o93rp169x3Qbt27dzj6f26++67S7VHt/W533PPPd3j6TN59NFH2xdffFHieUN7J8N9/Jdfftl9DrQ/a9/Q84R+X5blvvvuswMPPNB9D2l76jFee+21cj+r3udIbdFn5L333qvy921BQYH73tD3hx5Pr1/fXfoeDt4mer+nTp0a+H7wcr3XrFljI0aMcK9V3wV63fpu+OabbwJ/P2XKFNt///3d/9UG7zG8NIeycm51sH/NNdcEtnmXLl3cdvL2/Ui3CWqJH4ii/v37+xs0aOD/9ttvK1339ttv9/t8Pv+pp57qf/zxx/2jRo3yN2/e3L/TTjv5165dG1hv4sSJ/szMTH+HDh38t9xyi/+JJ57wX3nllf4jjzwysM4HH3zgr1Onjn/XXXf133PPPYHHatKkif/XX38NrKe/18d+77339p900knueS+44AK37Nprry3RvrPOOsstP+OMM/yPPvqoW3+vvfZyy/Q4Ht2v9g0fPtz/73//23/33Xf7Bw4c6H/hhRcqfP1eW/bcc0+3vp7De86zzz67Stvq0EMP9bdu3drfokUL/xVXXOH/5z//6R83bly5bTjnnHP8WVlZ/p133tn9/8knn/Q/99xz7j5tF23TCy+80C2/7rrr/PXq1fPvv//+/vz8/MBjnHDCCf5TTjnFf++997r35i9/+Yt7DSNGjCjxXJW9j59++qm/X79+7m+ff/75wMUTut2/++471542bdr4b7vtNv9dd93l79ixo3s9n332WWC9sWPHBt7zI444wv+Pf/zDf8011/jT09Nduyuj7ZKdne3fZZdd3Pui9+m4445zj3nTTTcF1vv73//uz8jI8K9evbrE3//vf/9z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tjE45qjs8VavXu3WnzJlSpW3yb333uvfd999A7dvueUWf926df0bNmwILPvb3/7mXqfn0EMP9R900EElHmf//ff3X3fdde7/EydO9Kenp/sXLVoUuP/77793bf38888Dz9O9e/dK2/fqq6/6mzVrFrg9duxYf6NGjUqtp5jrwQcfjOj5K3udkXx+wo3XYtpzu3nzZuvevbs99thjZd7vHTF4F51i0JGSjuaC6UgmeL0rrriill4BgFTia5Dhrv1F+n7dzt32b78fQNn0+/3HH3+4nlT1jOoU+D777BM4Xe7p3bt3qdtez616c+fPn+96R73eYJ2237p1q+sl1OxWeo6+ffuG1abKHk//P/fcc13v7sCBA11PrGKNirzyyisudUGpBHq8G2+80Z3CD6bT9HpOj1InvKlnPXvttVeJ28HraHsopUAXT9euXV3ahLetyvPhhx+67bPDDju4Npx99tku71kzy4Ur3OcP53Um1YAy5dNUNFoxNOF7/PjxLk9H+SvBtNFIDgdQ03xdGrvBY7Yuz/yNi1MTXGC7Ls+sfob5GFAGVCo7O9v69evnLko/uOCCC+yWW25xAWQ4lBu677772osvvljmgCedco9EZY8nY8eOdfmv7733ngtcFazqNPwBBxxQ6m+UtqBUCuW7KiBWnunLL79s999/f4n1MjJKHgyr806n+iNdJ1ILFy604447zlWtuOOOO1zwPn36dBsyZIhLw4j2lMk18RqSplqCEpTfeecdlxsSSrkbt912mystcsYZZ9iwYcOsTp3yX5pyfYLzfTZs2BDIu6nOdG8AklzdNLN+O5hNXGK2aov5lXKnTtz6GW55QV2fvkhi3UogpvQ7qjP/CmDCCWJ23313l5MZvK4CxLPOOitw+7PPPnMDk7SOrhVgNm/e3OWFlkW9heqdVJ5sebz2VfZ4Xrt0plkXDRZTr6yCYQ0yC6WcXQ2W00A5jzc4zXus4syI7bcrWhZ621vWpUsXNxBOA9u83lPl+K5bt8522203t44CS02HHPwYKuel2/fee2/gQECvP3ibKIYK/bvQNoXz/OG+ztDtrXX0OUpPTy9xX7gxWsIEtwpq1UN70kknlViuIymd0tCRx6effuo+TDpdoJGA5RkzZow7ogo1ceLEqB+xAEg+WW191nxNhmXmp1l+ZpGtarrN8n7+zeznWLcMiD0FRjqbqh5Rb4CXaOCRemfVq6lBTvpN//rrr91AKZ3F9Tqa5NVXX3XrqGdU///888/dgCyto9QABWa61m++Tq0ryHr77bddTKDbGhw1fPhwF6weeeSRri0alBVco3XLli1hPZ4GlCltQm3U61IKw08//WQnn3xyiTZ72rZt61IQ1Nur+ESxxZtvvukCNm99dbApeAz+e6VBKLDzlul5tf2C19EyBXhapsBaaQCnn366i2t0nwZ+9enTx1Vq0DotW7a0X3/91QXcapdSJPQa9Bj33XefSw3RgcOTTz7pHl9VDBTwqsda20zbYI899nBVKxQfqX1qZ7jPH87rDKXXrPdm2rRp7jGDhZs2kTDBrfJttUPodEYwfXiDc1MyMzPtoosuchta5T/Kog9v8N9pA+uoo3///uUeBQIAgMopeFFwqEAq+Ddbv8kaif/UU0+5XFYFWPrtvfDCC93vsgIojzqg1JurYEk5msG9pPqdVuBz/fXX2znnnOMCMgWkRxxxhLvW/YoDRPmxSn1Qr6zyfYN/4/V8ul3Z4ynQUoCowFx5qWqPKjFcddVVZaZAqLKDgnb18Cq4O+aYY1wb9Jq859e2UK9kcHu0rfR43jIdJCimCV5Hy9Qb6y1T7rIC8GOPPdb9rdIgHnnkkcD96v1WKoVKralH9emnn3avQykSCm41Zunggw+2O++80y3XAYf+Vikj2oZKVdBrvvnmm13qiJ5D7Qz3+cN5nWV9fvTeHHLIIaVivvIC4lA+jSqzOKAcDB3ZqNxGKJXm0IucPXu2OyVQEZW10FHGjz/+6LrMw6GNpZwY1XQjuAUAoOoUnCgY7NixY6ngpLrxAFL787MhzHgtIerc6khDyd6VBbaiAFhHBOqKBwAAQGqJaVqC8jmUu+JRpK7gVPmzGhzmRenKtwkdZeglnCuHRhUU1JWu2xpMpm74Jk0YtQwAAJBqYhrcfvHFFy4w9Xh5sMp58WreqXyGMieUsBxKuRy6X9O7Ka9FXdgKboPzaQEAQOKIk2xJJLC4ybmNJXJuAQCIj5xbpLatqZJzCwAAAISD4BYAAABJg+AWAAAASYPgFgAAAEmD4BYAAABJg+AWAAAASSOmdW4BAAAite2qj2v1+TIePrhKf6fJpQ466CA7+uij7Z133ilznf/+979u8qmLL77YHnvssXIfa8yYMXbjjTfaXXfdZX/7299K3Ke5Aa6++mpbt25d4PZ5551nu+22m/3www8l1tXEWKeccop16NDBFi5caMmInlsAAIAa8PTTT9sVV1xh06ZNsz/++KPcda699loX5KrGa3meeeYZt56uw1GvXj1bsWKFC7BDn8+bBTZZEdwCAABE2aZNm+yVV16xSy65xI499tjAzKvBNFnBp59+atdff73tuuuu9sYbb5T5WFOnTrUtW7bY6NGj3UQG+pvK1KlTx84444wSwfCSJUtsypQpbnkyI7gFAACIsv/9738uLaBLly4u7UBBZuiksGPHjnWBr2bd0jrqVS2Llp9++umWkZHhrstbL9T555/v2pGbm+tuK8BWikSrVq0smRHcAgAARJkCUAWsooBSU8aqB9ZTVFTkgk1vndNOO82mT5/uenODqaf2tddeC6ynawWs6hmuzN57722dOnVyf6/AWs+ngDfZEdwCAABE0bx58+zzzz93vaxeisCpp55aosf1gw8+sM2bN9sxxxzjbjdv3tz69etXKqdWubg777yzde/e3d3u0aOHGwymlIdwnH/++a6HWIF18PMlM4JbAACAKFIQW1BQYG3btnWBrS5PPPGEvf76664H11tnzZo1lpOTE1jn//7v/+w///mP69UNfqzvv/8+sI4uc+fODXtg2ZlnnmmfffaZ3XrrrXb22We7v092yf8KAQAAaomC2ueee87uv/9+69+/f4n7TjjhBNcT+5e//MXGjx9vL7/8snXr1i1wf2FhoSsdNnHiRJfK8O2339oXX3zhBoE1bdo0sJ6C4sMOO8x+/PFHl9dbkaZNm9qgQYNcKsOTTz5pqYDgFgAAIEomTJhga9eutSFDhriBYsEGDx7semJV8qtZs2au3qzP5yuxjtIGtI6CW1337NnTDjnkkFLPs//++7v777333krb9Oyzz9rjjz/unjMVkJYAAAAQJQo4jzzyyFKBrRfcqid2+PDhduKJJ5YKbL113nrrLVej9oUXXnC3y6Ll6iHetm1bpW3KyclJmcBWfP7QuhQpSCMR9SFUHkzDhg1j3RwAABKWeiU14r9jx46WnZ0d6+YgiT4/4cZr9NwCAAAgaRDcAgAAIGkQ3AIAACBpENwCAAAgaRDcAgAAIGkQ3AIAACBpENwCAAAgaRDcAgAAIGkQ3AIAACBpENwCAACgXDvttJM99NBDlijqxLoBAAAAEfng/Np9vn7PVOnPli1bZnfccYe988479vvvv1vLli2tR48edvXVV1vfvn2tpgPSq6++2l1SDcEtAABAlC1cuND69OljjRs3tnvvvdf23HNP27Ztm73//vt22WWX2Y8//hjxY/r9fissLLQ6dQjfKkJaAgAAQJRdeuml5vP57PPPP7fBgwfbrrvuat26dbPhw4fbZ5995oJf3T979uzA36xbt84tmzJliruta91+9913bd9997WsrCybPn26LViwwI4//nhr1aqV1a9f3/bff3/78MMPA49z2GGH2W+//WbDhg1zf6+LR39/8MEHW05OjrVr186uvPJK27x5c+D+FStW2MCBA939HTt2tBdffNESDcEtAABAFK1Zs8bee+8910Nbr169UverNzcS119/vd111132ww8/2F577WWbNm2yY445xiZNmmRff/21HX300S4gXbRokVv/jTfesB133NFGjx5tS5cudRdRUKx1FWzPmTPHXnnlFRfsXn755YHnOvfcc23x4sX20Ucf2WuvvWaPP/64C3gTCf3aAAAAUTR//nyXQrDbbrtF5fEUpPbr1y9wu2nTpta9e/fA7dtuu83efPNNe+utt1ygqvvT09OtQYMG1rp168B6Y8aMsTPPPDOQh7vLLrvYI488Yoceeqg98cQTLjhWL7F6m9UbLE8//bTtvvvulkgIbgEAAKJIgW007bfffiVuq+f21ltvdQPV1CtbUFBgW7ZsCfTcluebb75xPbbBqQZqa1FRkf3666/2008/uXxepUB4FKBH2tMcawS3AAAAUaQeUeW5VjRoLC0trVQgrAFnZQlNbRgxYoR98MEHdt9991nnzp1dfuzJJ59s+fn5FbZLQfFFF13k8mxDtW/f3gW3yYCcWwAAgChSWsBRRx1ljz32WInBWsEDx1q0aOH+7+XDSvDgsop88sknLjf2xBNPdFUYlHqgAWrBMjMzXWWFYPvss4/NnTvXBcShF62vXlr1An/55ZeBv5k3b55rbyIhuAUAAIgyBbYKLnv27Gmvv/66/fzzz25AmHJce/fu7XpbDzjggMBAsalTp9qNN94Yds+wBo0pGFaqwRlnnOFSC0Lr3E6bNs3V1121apVbdt1119mnn37q8nL1t2rT+PHjAwPKunTp4gacqXd35syZLsi94IILXFsTCcEtAABAlHXq1Mm++uorO/zww+2aa66xPfbYww0KU4UDDd6SZ555xvWUKsdVg7xuv/32sB77gQcesCZNmtiBBx7oqiSol1i9sqGD0BYuXGg777xzoJdYlRYURCv9QOXA9t57b7v55putbdu2gb8bO3asu61BZieddJINHTrUTT6RSHz+aGc9J6ANGzZYo0aNbP369dawYcNYNwcAgIS1detWNzhJNVKzs7Nj3Rwk0ecn3HiNnlsAAAAkjZgGt8oFUXe6ur81qnDcuHEl7leytDezhndRLkhooWTVbFMEr1IVQ4YMcaMBAQAAkHpiGtxqBKGKECvpujwKZr3ZNXT573//W+J+Bbbff/+9K4kxYcIEFzArPwQAAACpJ6Z1bgcMGOAuFdE8ysGzawTT6EJNbzdr1qxAgeN//OMfbko61X4LTpAGAABA8ov7SRymTJniRulpVOARRxzhRhI2a9bM3TdjxgyXihA8c8eRRx7pCiOrhIXqv5UlLy/PXYITlL3iyeUVUAYAAJXT6H+NVVcZrNDyVEBl9LnR50efo9CYLNwYLa6DW6UkqAyFRswtWLDAbrjhBtfTq6BWcyYvW7asVHkKTRun4sm6rzyaW3nUqFGllk+cONHq1q1bI68FAIBUoA6mNm3auBHtdBghUhs3bnRpq5MnTy41jXFubm7iB7ennXZa4P+agUP12VSvTb25ffv2rfLjjhw50oYPH16i57Zdu3bWv39/SoEBAFANCkg0cYACFP2metPMApV9bhS8KrjVwVGPHj1KreOdaU/o4LasgsjNmze3+fPnu+BWubgrVqwosY66sVVBobw8XS+PV5dQGRkZ7gIAAKpuhx12cLVKFy9eHOumIMEoDVUxnCpkhQo3Rkuo4HbJkiW2evVqF9GLpq/TfMeaHk6ze4i6sZXj06tXrxi3FgCA1JSZmemmiM3Pz491U5BAFLwq7bS6Yhrcqh6temE9OsrTXMfKmdVFebGDBw92Ebxybq+99lrr3Lmzm2ZOdt99d5eXe+GFF9qTTz7pcns0P7LSGaiUAABA7CgdgRnKkHLT7yp3VnMuhzrnnHPcvMsnnHCCff311653VsGqcmJvu+02a9WqVWBdpSAooH377bfdjqRg+JFHHrH69euH3Q6m3wUAAIhv4cZrMQ1u4wXBLQAAQHLEawxhBAAAQNIguAUAAEDSILgFAABA0iC4BQAAQNIguAUAAEDSILgFAABA0iC4BQAAQNIguAUAAEDSILgFAABA0iC4BQAAQNIguAUAAEDSILgFAABA0iC4BQAAQNIguAUAAEDSILgFAABA0iC4BQAAQNIguAUAAEDSILgFAABA0iC4BQAAQNIguAUAAEDSILgFAABA0iC4BQAAQNIguAUAAEDSILgFAABA0iC4BQAAQNIguAUAAEDSILgFAABA0iC4BQAAQNIguAUAAEDSILgFAABA0iC4BQAAQNIguAUAAEDSILgFAABA0iC4BQAAQNIguAUAAEDSILgFAABA0iC4BQAAQNIguAUAAEDSILgFAABA0ohpcDtt2jQbOHCgtW3b1nw+n40bNy5w37Zt2+y6666zPffc0+rVq+fW+etf/2p//PFHicfYaaed3N8GX+66664YvBoAAACkdHC7efNm6969uz322GOl7svNzbWvvvrKbrrpJnf9xhtv2Lx582zQoEGl1h09erQtXbo0cLniiitq6RUAAAAgntSJ5ZMPGDDAXcrSqFEj++CDD0ose/TRR61nz562aNEia9++fWB5gwYNrHXr1jXeXgAAACRZcFtYWGjPPvusTZo0yVasWGFFRUUl7p88ebLVlPXr17u0g8aNG5dYrjSE2267zQW8Z5xxhg0bNszq1Cn/peXl5bmLZ8OGDYFUCF0AAAAQX8KN0SIObq+66ioX3B577LG2xx57uGCzNmzdutXl4J5++unWsGHDwPIrr7zS9tlnH2vatKl9+umnNnLkSJea8MADD5T7WGPGjLFRo0aVWj5x4kSrW7dujb0GAAAAVI1SVsPh8/v9/kgeuHnz5vbcc8/ZMcccU8WmldMQn8/efPNNO+GEE8qM1AcPHmxLliyxKVOmlAhuQz3zzDN20UUX2aZNmywrKyvsntt27drZqlWrKnxsAAAAxIbiNcWhOpNfUbwWcc9tZmamde7c2WqLAttTTjnFfvvtN5fyUFnw2atXLysoKLCFCxdaly5dylxHQW9ZgW9GRoa7AAAAIL6EG6NFXC3hmmuusYcfftgi7PCtVmD7888/24cffmjNmjWr9G9mz55taWlp1rJlyxpvHwAAAOJLxD2306dPt48++sjeffdd69atW6koWiW7wqXUgfnz5wdu//rrry44Vf5smzZt7OSTT3ZlwCZMmOAGsi1btsytp/vVgzxjxgybOXOmHX744a5igm5rMNlZZ51lTZo0ifSlAQAAIMFFnHN73nnnVXj/2LFjw34s5c8qMA11zjnn2K233modO3Ys8+8UXB922GEu8L300kvtxx9/dDm0Wv/ss8+24cOHl5tvW14Oh0qPVZbDAQAAgNgIN16LOLhNRgS3AAAAyRGvVXkSh5UrV7oZw0QDt1q0aFHVhwIAAACiIq0qU+aef/75Lif2kEMOcZe2bdvakCFDwq4/BgAAAMRFcKt81qlTp9rbb79t69atc5fx48e7ZaqkAAAAAMRKlSZxeO2119yArtBBXirbpXSFREPOLQAAQHLEaxH33Cr1oFWrVqWWq64saQkAAACIpYiD2969e9stt9xiW7duDSzbsmWLjRo1yt0HAAAAxErE1RI0O9lRRx1lO+64o3Xv3t0t++abbyw7O9vef//9mmgjAAAAEJYq1blV+sGLL77oJk+Q3Xff3c4880zLycmxRETOLQAAQArXua1bt65deOGF1WkfAAAAEHVhBbdvvfWWDRgwwDIyMtz/KzJo0KBotQ0AAACIflpCWlqaLVu2zFVE0P/LfTCfzwoLCy3RkJYAAACQQmkJRUVFZf4fAAAASOhSYM8995zl5eWVWp6fn+/uAwAAABKmWkJ6erotXbrUpSgEW716tVtGWgIAAAASZoYyxcLKrQ21ZMkS94QAAABArIRdCmzvvfd2Qa0uffv2tTp1tv+pemt//fVXO/roo2uqnQAAAED0gtsTTjjBXc+ePdvNUFa/fv3AfZmZmbbTTjvZ4MGDw304AAAAIHbB7S233OKuFcSedtpplpWVFf3WAAAAANUQcc5t165dXe9tqJkzZ9oXX3xRnbYAAAAAtRvcXnbZZbZ48eJSy3///Xd3HwAAAJAwwe3cuXNtn332KXPAme4DAAAAEia4Va7t8uXLSy1X7dvgCgoAAABA3Ae3/fv3t5EjR7oCup5169bZDTfcYP369Yt2+wAAAICwRdzVet9999khhxxiHTp0cKkIogFmrVq1sueffz7ShwMAAABiF9zusMMONmfOHHvxxRftm2++sZycHDvvvPPs9NNPt4yMjOi1DAAAAIhQlZJk69WrZ0OHDq3KnwIAAAA1psojwFQZYdGiRZafn19i+aBBg6LRLgAAAKDmg9tffvnFTjzxRPv222/N5/OZ3+93y/V/KSwsjLwVAAAAQCyqJVx11VXWsWNHW7FihdWtW9e+//57mzZtmu233342ZcqUaLQJAAAAqJ2e2xkzZtjkyZOtefPmlpaW5i4HHXSQjRkzxq688kr7+uuvq9YSAAAAoLZ7bpV20KBBA/d/Bbh//PGH+79Kg82bN6+67QEAAABqr+d2jz32cCXAlJrQq1cvu+eeeywzM9Oeeuop69SpU9VbAgAAANR2cHvjjTfa5s2b3f9Hjx5txx13nB188MHWrFkze+WVV6rbHgAAAKDKfH6v3EE1rFmzxpo0aRKomJBoNmzYYI0aNXJTCjds2DDWzQEAAEAV47WIc2498+fPt/fff9+2bNliTZs2rerDAAAAAFETcXC7evVq69u3r+266652zDHH2NKlS93yIUOG2DXXXBO9lgEAAAA1HdwOGzbMMjIy3OxkqnPrOfXUU+29996L9OEAAACA2A0omzhxoktH2HHHHUss32WXXey3336LXssAAACAmu65VaWE4B7b4EFlWVlZkT4cAAAAELvgVmW/nnvuucBtVUgoKipy9W4PP/zwiB5L0/YOHDjQ2rZt6x5n3LhxJe5XIYebb77Z2rRpYzk5OXbkkUfazz//XCqoPvPMM92oucaNG7vc302bNkX6sgAAAJCKwa2CWE3YMGDAAMvPz7drr73WTeygQPXuu++OuBe4e/fu9thjj5X7XI888og9+eSTNnPmTKtXr54dddRRtnXr1sA6Cmy///57++CDD2zChAmuHUOHDo30ZQEAACBV69yqvtijjz7qZipTL+k+++xjl112methrXJDfD5788037YQTTnC31Sz16KoCw4gRIwLP26pVK3v22WfttNNOsx9++MG6du1qs2bNsv3228+to0FtquKwZMkS9/fhoM4tAABAfAs3XotoQNm2bdvs6KOPdj2pf//7360m/frrr7Zs2TKXiuDRC9KUvzNmzHDBra6ViuAFtqL109LSXE/viSeeWOZj5+XluUvwxvJeny4AAACIL+HGaBEFtyoBNmfOHKsNCmxFPbXBdNu7T9ctW7YscX+dOnXcpBLeOmUZM2aMjRo1qsxKEGUNlgMAAEBs5ebm1kwpsLPOOsuefvppu+uuuyxRjRw50oYPH16i57Zdu3bWv39/0hIAAADikHemPerBbUFBgT3zzDP24Ycf2r777usGeQV74IEHLBpat27trpcvX14il1e3e/ToEVhnxYoVpdqnCgre35dFJcvKKlumnmldAAAAEF/CjdEiDm6/++47N4BMfvrpp1KDwqKlY8eOLkCdNGlSIJhVxK5c2ksuucTd7t27t61bt86+/PJLF2jL5MmTXWky5eaievzr88w/b535N24zX4MM83VpbL5G1DIGAADxK+Lg9qOPPorak6vSwvz580sMIps9e7bLmW3fvr1dffXVdvvtt7vZzxTs3nTTTa4CgldRYffdd3cD3C688EI3yE2JxpdffrkbbBZupQSUrWjeWit8b5HZpqDk7RnLLP3o9pbWpUksmwYAABC94DaavvjiixITP3h5sOecc44r96UauqqFq7q16qE96KCDXKmv7OzswN+8+OKLLqDt27evq5IwePBgVxsX1euxdYFtboFZs2zzpfnMX+Q3W1e83Ne6nvkaZca6mQAAANGpc5tsqHNbUtHny63wg8WBwNbjAtzVWy29XztL61myigUAAEDC1blFalCOrQQHtt5tv2/7/ajA1rVma+aa5W8wy2xo1rSrWTbpHAAA1DSCW5SiwWNeT22pnlv/9vtRjtXfm/3ylln+xu3Lfp9m1ul4s2ZdY9kyAACSXlokK2vA1vnnn+8GfiF5qSqC1c9wObYuoPUC23V5brmPAWUV99gqsN2Wa5bTwqxuq+Jr3f5lvFneuli3EACApJYWaX2x119/veZag7igcl+qimA5dVyOrX/VFnet21rOYLIKuFSEjWbZTZXHUbxM17qt5erVBQAA8ZOWoDJc48aNs2HDhtVMixAXVO5LVRH889YG1bltQmBbGeXYihfYerzb3v0AACA+glvVnB09erR98sknZc5QduWVV0azfYghBbI+qiJERoPHxF9UMsDV7eD7AQBAfJQC02QK5T6Yz2e//PKLJRpKgSGqObffPlmcY+ulJiiw3brGLKOu2V6XmGU1jnUrAQBIODVWCozBZEAFVO5LVRE0eGzLyu3LMxsULyewBQAgPkuB5efnu0B35513tjp1qCgGBKjcV/22xYPHvDq3zboR2AIAEG/VEiQ3N9eGDBlidevWtW7dutmiRYvc8iuuuMLuuuuummgjkHgUyLbtY7bTgOJrAlsAKTJ9u5vlctISd63bQNwHtyNHjrRvvvnGpkyZYtnZ2YHlRx55pL3yyivRbh8AAEgARfPWWsGzP7rp24s+W+audVvLgdoUcT6ByoApiD3ggAPcADKPenEXLFgQ7fYBAIA4px7awvcWmeUWmDXLLp6u/c/Jf7RcpSUpJYm47blduXKltWzZstTyzZs3lwh2AQBAavDPW2e2aZtZk6zAtO3uunGWW66a6UDcBrf77befvfPOO4HbXkD773//23r37h3d1gEAgLinyX7EC2w97rZv+/1AXKYl3HnnnTZgwACbO3euFRQU2MMPP+z+/+mnn9rUqVNrppUAACBuaRZLUSpCcIDrUhP82+8H4rLn9qCDDrLZs2e7wHbPPfe0iRMnujSFGTNmuBnLAABAavF1aWxWP8Pl2LqA1gts1+W55Zq+HYjbGcqSETOUAQBQPaqK4AaVKfdWnbeKLupnWPrR7S2N4BbxPEOZFBYW2ptvvmk//PCDu921a1c7/vjjmcwBAIAUpQBWVRE0eEw5tkpFUI8tVRJQ2yKORr///nsbNGiQLVu2zLp06eKW3X333daiRQt7++23bY899qiJdgIAgDinQNbXs1Wsm4EUF3HO7QUXXOBq2i5ZssS++uord1m8eLHttddeNnTo0JppJQAAAFATPbcaTPbFF19Ykybb82f0/zvuuMP233//SB8OAAAAiF3P7a677mrLly8vtXzFihXWuXPnaLULAAAAqPngdsyYMXbllVfaa6+95lITdNH/r776apd7q5Fs3gUAAACI61JgaWlppWYn8x4i+Lb+r6oKiYBSYAAAAClaCuyjjz6qbtsAAACAGhFxcHvooYfWTEsAAACA2s65BQAAAOIVwS0AAACSBsEtAAAAkgbBLQAAAFI3uN2yZYvl5uYGbv/222/20EMP2cSJE6PdNgAAAKBmg9vjjz/ennvuOff/devWWa9evez+++93y5944olIHw4AAACIXXD71Vdf2cEHH+z+r5nJWrVq5XpvFfA+8sgj0WsZAAAAUNPBrVISGjRo4P6vVISTTjrJzVp2wAEHuCAXAAAASJjgtnPnzjZu3DhbvHixvf/++9a/f3+3fMWKFUxdCwAAgMQKbm+++WYbMWKE7bTTTi7ftnfv3oFe3L333rsm2ggAAACExef3+/0WoWXLltnSpUute/fuLiVBPv/8c9dzu9tuu1mi2bBhgzVq1MjWr19P7zMAAEACx2t1qvLgrVu3dpdgPXv2rMpDAQAAAFETVnCrQWPheuONN6rTHgAAgNS0da3Zmrlm+RvMMhuaNe1qlt0k1q1KzpxbdQF7F3UDT5o0yb744ovA/V9++aVbpvujTbm9Pp+v1OWyyy5z9x922GGl7rv44ouj3g4AAIAas/p7s2+fNPv1HbPfPy6+1u3Vc2PdsuTsuR07dmzg/9ddd52dcsop9uSTT1p6erpbVlhYaJdeemmN5KvOmjXLPb7nu+++s379+tlf/vKXwLILL7zQRo8eHbhdt27dqLcDAACgxnpsf3nLbFuuWU4LM1+amb/IbOsas1/Gm9Vva5bVONatTBgR59w+88wzNn369EBgK/r/8OHD7cADD7R77703qg1s0aJFidt33XWX7bzzznbooYeWCGZDc4ABAAASgktF2Lg9sBVdZzc127KyuFe3bZ9YtzJ5g9uCggL78ccfrUuXLiWWa1lRUZHVpPz8fHvhhRdcIK30A8+LL77olivAHThwoN10000V9t7m5eW5S/DoO9m2bZu7AAAA1Ja0LWstze83V75KPbZBfOa3oi1rrYj4xMKN0SIObs877zwbMmSILViwIFAhYebMma5HVffVJE0esW7dOjv33HMDy8444wzr0KGDtW3b1ubMmePSJubNm1fhwLYxY8bYqFGjSi1XrV5SGgAAQG1ql77IdsnYbJs3LXfh7HZ+q+fLtZ9/+NUWf/d/lupyc3Nrps6temfvu+8+e/jhh12tW2nTpo1dddVVds0115RIV4i2o446yjIzM+3tt98ud53Jkydb3759bf78+S59Idye23bt2tmqVauocwsAAGpX3jpLn/uv4pxbVUcI5NyuNcuoa4Vdh5plRX/QfqJRvNa8efNK69xWaRKH4CeR2ggIf/vtN+vUqZPrkT3++OPLXW/z5s1Wv359e++991wwHA4mcQAQCf/6PPPPW2f+jdvM1yDDfF0am69RVqybBSCRqSqCBo8p99aT2cCs0/FmzbrGsmWpMYmDpzYDQVVsaNmypR177LEVrjd79uxAbzIARFvRvLVW+N4is01BuV8zlln60e0trQv1KAFUkQJYVUXQ4DGvzm2zblRJqIKIg9vly5fbiBEjXF3bFStWWGjHb3DZrmhRKoSC23POOcfq1NneZOX9vvTSS3bMMcdYs2bNXM7tsGHD7JBDDrG99tor6u0AkNrUY+sC29wCs2bZ5kvzmb/Ib7aueLmvdT3zNcqMdTMBJCoFslRFqP3gVoO5Fi1a5CoSqHc0uGpBTfnwww/dc55//vklliv/Vvc99NBD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NIvgYLui9REdDChD3Hj55ZfdwC0NIOjdu7f7QtAo75kzZ5a5vnqjevXqZd99912ZPVOi0646Jab8ROVMaQCDHts7xaWBC3rOsqhXQRcFB+rZ0hG/Tt/q74FU3DcmT55sM2bMcOsGUy+uepODc30Bj6ojKO1Ag7qCKedWQgdhqWNBZwgUbCqALC9lIVRw1Y7gwWOhevbs6dJxRIPYtK94NHDNE5yG5rWhrGXqnS1PaCqb/qai9REdBLeIG8qD0hG1Bh4E5yuVR0f7usyZM8fOOOOMch/z8ccfd6eXZPHixW5wTfAgg3BGzXpfRsG9C0Cq7RuPPPKIG2TjUXm8o446yl555ZUSAQIQTKldynV99NFH7Yorrig379ajTgUFxJEK928UBKsHVXSgGNyziuRAcIsaoRyp0NqX+oITldYKvU89Rzo9+txzz9n777/v8go1YEUjuvX/8qgnSRUMyiucrcfU46hnSflS6hGorFSLesP0vAcddJA1adLEBRE33XSTGzxAry1Sed8ILSfmpSto3wguPQaE0oGUBmfp86YqAjrlr4oF+hz/+OOPLm2gJqh2tPYTHext3brV/v3vf7t9Q+lmSF4Et6gRKpy99957l1imUkai8kGhJYT0I6u8KOW4qqahTt2cfvrprqdKZYfKU1kPwNNPP21Dhw51hcCVVqD6iuWlIXiU56XC2rfccosbkKMjfBXGv/HGG0udjgVSad8AqkoHQPoM63OmnG3VYtb3adeuXd3nLvisRDQpzeaaa65xB476bldQ/eGHH7pa5khePj8jZAAAAJAkqJYAAACApEFwCwAAgKRBcAsAAICkQXALAACApEFwCwAAgKRBcAsAAICkQXALAACApEFwCwAAgKRBcAsAAICkQXALAACApEFwCwAAgKRBcAsAAABLFv8PIFr8rxf980gAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "from matplotlib.patches import Patch\n", + "\n", + "entry1 = 1.1\n", + "entry2 = 1.55\n", + "entry3 = 2.0\n", + "\n", + "space_between_boxes = 0.09\n", + "color1 = \"#fa6ec2\"\n", + "color2 = \"#ffba61\"\n", + "\n", + "fig, ax = plt.subplots(figsize=(8, 5))\n", + "\n", + "datasets = [\n", + " (\"gpt-5-mini\", gpt_aaaim['seconds_per_reaction'], color1, entry3-space_between_boxes, \"aaaim\"),\n", + " (\"gpt-5-mini\", gpt_curated['seconds_per_reaction'], color2, entry3+space_between_boxes, \"curated\"),\n", + "\n", + " (\"llama-3\", llm3_aaaim['seconds_per_reaction'], color1, entry1-space_between_boxes, None),\n", + " (\"llama-3\", llm3_curated['seconds_per_reaction'], color2, entry1+space_between_boxes, None),\n", + "\n", + " (\"llama-4\", llm4_aaaim['seconds_per_reaction'], color1, entry2-space_between_boxes, None),\n", + " (\"llama-4\", llm4_curated['seconds_per_reaction'], color2, entry2+space_between_boxes, None),\n", + "]\n", + "\n", + "for label, data, color, pos, _ in datasets:\n", + " ax.boxplot(\n", + " data.dropna(),\n", + " positions=[pos],\n", + " widths=0.15,\n", + " patch_artist=True,\n", + " boxprops=dict(facecolor=color, color=color),\n", + " capprops=dict(color=color),\n", + " whiskerprops=dict(color=color),\n", + " medianprops=dict(color='black'),\n", + " flierprops=dict(\n", + " marker='o',\n", + " markerfacecolor=color,\n", + " markeredgecolor=color,\n", + " markersize=5,\n", + " alpha=0.7\n", + " )\n", + " )\n", + "\n", + "# Center xticks between grouped pairs\n", + "ax.set_xticks([entry1, entry2, entry3])\n", + "ax.set_xticklabels(['LLaMa-3', 'LLaMa-4', 'GPT-5-mini'])\n", + "\n", + "ax.set_ylabel('Seconds per reaction')\n", + "ax.set_title('Seconds per reaction by model and species annotation')\n", + "\n", + "# Legend\n", + "legend_elements = [\n", + " Patch(facecolor=color1, label='AAAIM'),\n", + " Patch(facecolor=color2, label='Curated')\n", + "]\n", + "\n", + "ax.legend(\n", + " handles=legend_elements,\n", + " title='Species annotation'\n", + ")\n", + "\n", + "ax.grid(axis='y')\n", + "ax.set_axisbelow(True)\n", + "# Reduce extra whitespace on left/right\n", + "ax.set_xlim(0.85, 2.25)\n", + "\n", + "plt.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "aaaim", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tests/analyze_reaction_complexity_from_eval.py b/tests/analyze_reaction_complexity_from_eval.py new file mode 100644 index 0000000..1d942a3 --- /dev/null +++ b/tests/analyze_reaction_complexity_from_eval.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python3 +"""Join evaluation CSV rows with SBML reaction complexity (substrate/product counts). + +Reads ``per_reaction_results.csv`` from a reaction-evaluation run (e.g. +``tests/reaction_evaluation_results/curated_species/``), loads each +``model_id`` SBML from ``tests/BioModels_251106`` (or ``--models-dir``), and +adds per-reaction counts from libSBML: + +- ``n_reactant_species``: distinct species on the left (reactants) +- ``n_product_species``: distinct species on the right (products) +- ``n_unique_metabolites``: distinct species across both sides + +Also writes ``complexity_impact_summary.csv`` with aggregate ``found`` / +``top1`` / ``top3`` / ``top5`` rates per complexity bin (unique metabolite +counts: ``1``, ``2``, ``3``, ``4``, ``5+``). + +**CLI** + +:: + + python tests/analyze_reaction_complexity_from_eval.py \\ + --per-reaction-csv tests/reaction_evaluation_results/curated_species/Llama-3.3-70B-Instruct-20260430_120414/per_reaction_results.csv + +**Notebook** + +:: + + from tests.analyze_reaction_complexity_from_eval import run_complexity_analysis + + result = run_complexity_analysis("path/to/per_reaction_results.csv") + display(result.per_reaction) + display(result.summary) +""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path +from typing import Dict, NamedTuple, Optional, Tuple + +import pandas as pd + +REPO_ROOT = Path(__file__).resolve().parent.parent +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +import libsbml # noqa: E402 + +__all__ = [ + "COMPLEXITY_BIN_1", + "COMPLEXITY_BIN_2", + "COMPLEXITY_BIN_3", + "COMPLEXITY_BIN_4", + "COMPLEXITY_BIN_5_PLUS", + "COMPLEXITY_BIN_ORDER", + "ComplexityAnalysisResult", + "complexity_bin_label", + "reaction_complexity", + "run_complexity_analysis", +] + +# Bins for ``n_unique_metabolites`` (distinct species across reactants and products). +COMPLEXITY_BIN_1 = "1" +COMPLEXITY_BIN_2 = "2" +COMPLEXITY_BIN_3 = "3" +COMPLEXITY_BIN_4 = "4" +COMPLEXITY_BIN_5_PLUS = "5+" +COMPLEXITY_BIN_ORDER = ( + COMPLEXITY_BIN_1, + COMPLEXITY_BIN_2, + COMPLEXITY_BIN_3, + COMPLEXITY_BIN_4, + COMPLEXITY_BIN_5_PLUS, +) + + +def complexity_bin_label(n_unique: int) -> str: + if n_unique <= 1: + return COMPLEXITY_BIN_1 + if n_unique == 2: + return COMPLEXITY_BIN_2 + if n_unique == 3: + return COMPLEXITY_BIN_3 + if n_unique == 4: + return COMPLEXITY_BIN_4 + return COMPLEXITY_BIN_5_PLUS + + +def reaction_complexity( + model_path: Path, reaction_id: str +) -> Tuple[Optional[int], Optional[int], Optional[int]]: + """Return (n_reactants, n_products, n_unique) or (None, None, None) if unavailable.""" + reader = libsbml.SBMLReader() + doc = reader.readSBML(str(model_path)) + model = doc.getModel() + if model is None: + return None, None, None + rxn = model.getReaction(reaction_id) + if rxn is None: + return None, None, None + lhs: set[str] = set() + for i in range(rxn.getNumReactants()): + sp = rxn.getReactant(i).getSpecies() + if sp: + lhs.add(sp) + rhs: set[str] = set() + for i in range(rxn.getNumProducts()): + sp = rxn.getProduct(i).getSpecies() + if sp: + rhs.add(sp) + uniq = lhs | rhs + return len(lhs), len(rhs), len(uniq) + + +def _resolve_path(p: Path | str, *, default_relative_to: Path) -> Path: + path = Path(p) + if not path.is_absolute(): + path = default_relative_to / path + return path + + +class ComplexityAnalysisResult(NamedTuple): + """Return value of :func:`run_complexity_analysis`.""" + + per_reaction: pd.DataFrame + summary: Optional[pd.DataFrame] + merged_csv_path: Optional[Path] + summary_csv_path: Optional[Path] + missing_models: tuple[str, ...] + + +def run_complexity_analysis( + per_reaction_csv: Path | str, + *, + models_dir: Path | str | None = None, + output_dir: Path | str | None = None, + write_csv: bool = True, + quiet: bool = False, +) -> ComplexityAnalysisResult: + """Load eval CSV, attach SBML complexity columns, optionally aggregate and write CSVs. + + Parameters + ---------- + per_reaction_csv + Path to ``per_reaction_results.csv`` (relative paths are under ``REPO_ROOT``). + models_dir + Directory of ``.xml``. Default: ``tests/BioModels_251106`` under repo root. + output_dir + Where to write outputs. Default: directory of ``per_reaction_csv``. + write_csv + If True, writes ``per_reaction_with_complexity.csv`` and + ``complexity_impact_summary.csv`` when applicable. + quiet + If False, print paths and summary table (CLI-style). + + Returns + ------- + ComplexityAnalysisResult + DataFrames always populated for ``per_reaction``; ``summary`` is None if no + rows had parsable complexity. ``*_csv_path`` fields are set only when + ``write_csv`` is True and that file was written. + """ + per_path = _resolve_path(per_reaction_csv, default_relative_to=REPO_ROOT) + if not per_path.exists(): + raise FileNotFoundError(f"Missing per-reaction CSV: {per_path}") + + mdir = models_dir if models_dir is not None else REPO_ROOT / "tests" / "BioModels_251106" + models_path = _resolve_path(mdir, default_relative_to=REPO_ROOT) + + out: Path + if output_dir is None: + out = per_path.parent + else: + out = _resolve_path(output_dir, default_relative_to=REPO_ROOT) + out.mkdir(parents=True, exist_ok=True) + + df = pd.read_csv(per_path, encoding="utf-8-sig") + required = {"model_id", "reaction_id"} + if not required.issubset(df.columns): + raise ValueError(f"CSV must contain columns {required}, got {list(df.columns)}") + + model_cache: Dict[str, Optional[Path]] = {} + complexity_cache: Dict[Tuple[str, str], Tuple[Optional[int], Optional[int], Optional[int]]] = {} + + def _model_path(mid: str) -> Optional[Path]: + if mid not in model_cache: + p = models_path / f"{mid}.xml" + model_cache[mid] = p if p.exists() else None + return model_cache[mid] + + def _cached_counts(mid: str, rid: str) -> Tuple[Optional[int], Optional[int], Optional[int]]: + key = (mid, rid) + if key not in complexity_cache: + mp = _model_path(mid) + if mp is None: + complexity_cache[key] = (None, None, None) + else: + complexity_cache[key] = reaction_complexity(mp, rid) + return complexity_cache[key] + + n_lhs: list[Optional[int]] = [] + n_rhs: list[Optional[int]] = [] + n_uni: list[Optional[int]] = [] + for _, row in df.iterrows(): + a, b, u = _cached_counts(str(row["model_id"]), str(row["reaction_id"])) + n_lhs.append(a) + n_rhs.append(b) + n_uni.append(u) + + merged = df.copy() + merged["n_reactant_species"] = n_lhs + merged["n_product_species"] = n_rhs + merged["n_unique_metabolites"] = n_uni + + merged_csv_path: Optional[Path] = None + if write_csv: + merged_csv_path = out / "per_reaction_with_complexity.csv" + merged.to_csv(merged_csv_path, index=False) + if not quiet: + print(f"Wrote {merged_csv_path}") + + work = merged[merged["n_unique_metabolites"].notna()].copy() + summary_df: Optional[pd.DataFrame] = None + summary_csv_path: Optional[Path] = None + + if not work.empty: + work = work.copy() + work["n_unique_metabolites"] = work["n_unique_metabolites"].astype(int) + work["complexity_bin"] = work["n_unique_metabolites"].map(complexity_bin_label) + summary_df = ( + work.groupby("complexity_bin", dropna=False) + .agg( + n_reactions=("reaction_id", "count"), + coverage=("found", "mean"), + top1=("top1", "mean"), + top3=("top3", "mean"), + top5=("top5", "mean"), + mean_candidates=("num_candidates", "mean"), + ) + .reset_index() + ) + summary_df["complexity_bin"] = pd.Categorical( + summary_df["complexity_bin"], + categories=list(COMPLEXITY_BIN_ORDER), + ordered=True, + ) + summary_df = summary_df.sort_values("complexity_bin").round(3) + if write_csv: + summary_csv_path = out / "complexity_impact_summary.csv" + summary_df.to_csv(summary_csv_path, index=False) + if not quiet: + print(f"Wrote {summary_csv_path}") + print(summary_df.to_string(index=False)) + elif not quiet: + print("No rows with parsable reaction complexity; skipped summary.") + + missing_models = tuple(sorted(m for m, p in model_cache.items() if p is None)) + if missing_models and not quiet: + print(f"Warning: {len(missing_models)} model_id(s) had no {models_path}/.xml") + + return ComplexityAnalysisResult( + per_reaction=merged, + summary=summary_df, + merged_csv_path=merged_csv_path, + summary_csv_path=summary_csv_path, + missing_models=missing_models, + ) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--per-reaction-csv", + type=Path, + required=True, + help="Path to per_reaction_results.csv from an evaluation run.", + ) + parser.add_argument( + "--models-dir", + type=Path, + default=REPO_ROOT / "tests" / "BioModels_251106", + help="Directory containing .xml SBML files.", + ) + parser.add_argument( + "--output-dir", + type=Path, + default=None, + help="Directory for outputs (default: same folder as --per-reaction-csv).", + ) + args = parser.parse_args() + + try: + run_complexity_analysis( + args.per_reaction_csv, + models_dir=args.models_dir, + output_dir=args.output_dir, + write_csv=True, + quiet=False, + ) + except (FileNotFoundError, ValueError) as exc: + print(exc, file=sys.stderr) + return 1 + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/evaluate_reaction_annotation.py b/tests/evaluate_reaction_annotation.py new file mode 100644 index 0000000..081f0a3 --- /dev/null +++ b/tests/evaluate_reaction_annotation.py @@ -0,0 +1,824 @@ +#!/usr/bin/env python3 +"""Evaluate reaction annotation performance on curated BioModels. + +For every model listed in ``tests/kegg_annotated_files.txt`` this script: + +1. Extracts ground-truth KEGG reaction IDs from the model (first one per + reaction; reactions without a KEGG annotation are skipped). +2. Builds a species-annotation table (ChEBI preferred, KEGG-compound as a + fallback) in the ``pd.read_csv``-compatible shape expected by + ``annotate_model``. +3. Runs ``annotate_model(method="rulebased", entity_type="reaction", + database="kegg")`` to produce candidate KEGG reaction IDs per reaction, + then (unless ``--skip-llm-ranking``) calls + ``rank_kegg_annotations_with_llm`` so ``_recommendations_llm_ranked.csv`` + is written alongside the rule-based CSV under the run ``_work/`` folder. +4. Normalizes candidate ranks by KEGG-Orthology (BRITE) group — candidates + that share at least one K-number are collapsed to the best (lowest) rank + in their group. +5. Scores each reaction (found?, rank, top1/3/5) and writes per-reaction + results + aggregate summary under the run directory. +6. Records per-model wall time and ``wall_seconds / num_evaluated_reactions`` in + ``per_model_timing.csv`` (same run folder as ``per_reaction_results.csv``). + +Run: ``python tests/evaluate_reaction_annotation.py`` +(or ``conda run -n aaaim python tests/evaluate_reaction_annotation.py``). + +python tests/evaluate_reaction_annotation.py --skip-cofactor-removal +python tests/evaluate_reaction_annotation.py --skip-brite-normalization + + +""" + +from __future__ import annotations + +import argparse +import logging +import re +import sys +import time +from pathlib import Path +from typing import Dict, List, Optional, Set, Tuple + +from dotenv import load_dotenv +import pandas as pd + +# Make repo root importable when running from tests/. +REPO_ROOT = Path(__file__).resolve().parent.parent +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +# API keys (OPENAI_API_KEY, OPENROUTER_API_KEY, etc.) for LLM ranking. +load_dotenv(REPO_ROOT / ".env") + +from core import annotate_model # noqa: E402 +from core.database_search import load_kegg_reaction_features_dict # noqa: E402 +from core.reaction.amendment_config import CofactorConfig # noqa: E402 +from core.reaction.annotation_workflow import rank_kegg_annotations_with_llm # noqa: E402 +from core.model_info import ( # noqa: E402 + exchange_constraint_skipped_reaction_ids, + find_reactions_with_kegg_annotations, + find_species_with_annotations_and_qualifiers, + find_species_with_chebi_annotations, +) +from utils.constants import DatabaseID # noqa: E402 + + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- + +DEFAULT_MODEL_LIST = REPO_ROOT / "tests" / "kegg_annotated_files-test.txt" +DEFAULT_RESULTS_DIR = ( + REPO_ROOT / "tests" / "reaction_evaluation_results" / "curated_species" +) + +# Rule-based generation-only is fast; set True for the slower scored/EM pipeline. +EVALUATE_CANDIDATES = False +INCLUDE_EXCHANGE_REACTIONS = False +# LLM_MODEL = "Llama-3.3-70B-Instruct" +# LLM_MODEL = "Llama-4-Maverick-17B-128E-Instruct-FP8" +LLM_MODEL = "gpt-5-mini-2025-08-07" +DEFAULT_LLM_TOP_K = 10 +# Default ``kegg_reaction_features.lzma`` is resolved under ``data/kegg/`` by the loader. +DEFAULT_KEGG_FEATURES_FILE = "kegg_reaction_features.lzma" + +_K_NUMBER_RE = re.compile(r"\bK\d{5,}\b") + +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger("evaluate_reaction_annotation") + + +# --------------------------------------------------------------------------- +# 1. Load model list +# --------------------------------------------------------------------------- + +def load_model_paths(list_file: Path) -> List[Path]: + """Read non-blank, non-comment lines from the model-list file.""" + with list_file.open("r", encoding="utf-8") as fh: + paths: List[Path] = [] + for raw in fh: + line = raw.strip() + if not line or line.startswith("#"): + continue + p = Path(line) + if not p.is_absolute(): + p = REPO_ROOT / p + paths.append(p) + return paths + + +# --------------------------------------------------------------------------- +# 2. Ground-truth reaction annotations +# --------------------------------------------------------------------------- + +def extract_ground_truth_reactions(model_file: Path) -> Dict[str, str]: + """Return ``{reaction_id: first_kegg_reaction_id}`` (drops reactions w/o KEGG).""" + existing, _ = find_reactions_with_kegg_annotations(str(model_file)) + out: Dict[str, str] = {} + for rxn_id, kegg_ids in existing.items(): + for kid in kegg_ids: + k = str(kid).strip() + if k: + out[str(rxn_id)] = k + break # first one only + return out + + +# --------------------------------------------------------------------------- +# 3. Species annotations -> annotate_model-compatible DataFrame +# --------------------------------------------------------------------------- + +def build_species_recommendations_df(model_file: Path) -> Tuple[Optional[pd.DataFrame], str]: + """Build the species->annotation table expected by the rulebased workflow. + + Per **species**, if ChEBI annotations exist they are used; otherwise KEGG + compound IDs are used for that species. A model may therefore mix rows + where some metabolites came from ChEBI and others from bare KEGG compounds. + + The rule-based pipeline maps ChEBI→KEGG via reference tables and treats bare + KEGG compound ids (``C#####``) as identity mappings (see + :func:`core.reaction.utils.map_chebi_to_kegg` and + :mod:`core.reaction.kegg_compound_ids`). + + Returns ``(df, source)`` where ``source`` is ``"chebi"`` (all rows from + ChEBI), ``"kegg_compound"`` (all from KEGG compounds), ``"mixed"`` + (both conventions appear), or ``"none"``. A ``None`` DataFrame means no + species had usable annotations. + + The returned DataFrame has columns ``id, annotation, match_score`` and + round-trips through ``pd.read_csv`` cleanly. + """ + chebi = find_species_with_chebi_annotations(str(model_file)) + kegg, _ = find_species_with_annotations_and_qualifiers( + str(model_file), DatabaseID.KEGG.value + ) + + rows: List[Dict[str, object]] = [] + + all_ids = sorted(set(chebi.keys()) | set(kegg.keys())) + for sid in all_ids: + sid_str = str(sid) + ch_list = chebi.get(sid) if chebi else None + if ch_list: + for cid in ch_list: + if str(cid).strip(): + rows.append( + { + "id": sid_str, + "annotation": f"CHEBI:{str(cid).split(':', 1)[-1]}", + "match_score": 1.0, + } + ) + continue + + kg_list = kegg.get(sid) if kegg else None + if kg_list: + for kid in kg_list: + ks = str(kid).strip() + if ks: + rows.append( + {"id": sid_str, "annotation": ks, "match_score": 1.0} + ) + + if not rows: + return None, "none" + + species_with_chebi = {s for s in all_ids if chebi and chebi.get(s)} + species_kegg_only = { + s for s in all_ids if (not (chebi and chebi.get(s))) and kegg and kegg.get(s) + } + if species_with_chebi and species_kegg_only: + source = "mixed" + elif species_kegg_only and not species_with_chebi: + source = "kegg_compound" + else: + source = "chebi" + + return pd.DataFrame(rows), source + + +# --------------------------------------------------------------------------- +# 4. Annotation pipeline +# --------------------------------------------------------------------------- + +def run_annotation( + model_file: Path, + species_df: pd.DataFrame, + work_dir: Path, + *, + rank_with_llm: bool = True, + disable_cofactors: bool = False, + disable_ontology_relaxation: bool = False, + llm_model: str = LLM_MODEL, + llm_top_k: int = DEFAULT_LLM_TOP_K, + kegg_features_file: Optional[str] = None, +) -> pd.DataFrame: + """Run ``annotate_model``, optionally LLM-re-rank candidates, return recommendations. + + ``annotate_model`` writes ``_recommendations.csv`` under + ``work_dir`` (cwd is switched there during the call). + + When ``rank_with_llm`` is True, ``rank_kegg_annotations_with_llm`` reads that + CSV (enriched with definitions), queries the LLM per reaction, and writes + ``_llm_ranked.csv`` next to it — evaluation uses the ranked table. + + When ``disable_cofactors`` is True, an empty ``CofactorConfig`` is passed so + H2O, ATP, NAD+, etc. are **not** excluded from reaction matching. + + When ``disable_ontology_relaxation`` is True, ChEBI ancestor traversal is + skipped entirely (``max_relax_level=0``). + """ + species_csv = work_dir / f"{model_file.stem}__species.csv" + species_df.to_csv(species_csv, index=False) + + # Round-trip through read_csv to satisfy the CSV-compatible contract. + # utf-8-sig strips a UTF-8 BOM so the first column stays ``id``, not ``\ufeffid``. + reloaded = pd.read_csv(species_csv, encoding="utf-8-sig") + + recommendations_csv = work_dir / f"{model_file.name}_recommendations.csv" + + cofactor_cfg = CofactorConfig(cofactors_dict={}) if disable_cofactors else None + + cwd = Path.cwd() + try: + import os + + os.chdir(work_dir) + result = annotate_model( + model_file=str(model_file), + llm_model=llm_model, + method="rulebased", + entity_type="reaction", + database="kegg", + species_recommendations_df=reloaded, + evaluate_candidates=EVALUATE_CANDIDATES, + include_exchange_reactions=INCLUDE_EXCHANGE_REACTIONS, + cofactor_config=cofactor_cfg, + disable_ontology_relaxation=disable_ontology_relaxation, + ) + finally: + import os + + os.chdir(cwd) + + # ``result`` supports ``df, metrics = result`` unpacking. + df, _metrics = result + if df is None or df.empty: + return pd.DataFrame(columns=["id", "annotation"]) + + kff = kegg_features_file or DEFAULT_KEGG_FEATURES_FILE + + if rank_with_llm: + try: + df = rank_kegg_annotations_with_llm( + model_file=str(model_file), + recommendations_df=df, + llm_model=llm_model, + kegg_features_file=kff, + top_k=llm_top_k, + csv_path=str(recommendations_csv), + ) + ranked_path = recommendations_csv.with_name( + recommendations_csv.stem + "_llm_ranked.csv" + ) + logger.info("LLM-ranked table: %s", ranked_path) + except Exception as exc: + logger.warning( + "LLM ranking failed (%s); evaluating with rule-based candidate order.", + exc, + ) + + return df + + +# --------------------------------------------------------------------------- +# 5. Rank normalization by BRITE / KEGG-Orthology groups +# --------------------------------------------------------------------------- + +def _strip_kegg_prefix(raw: object) -> str: + """Normalize candidate values like ``"KEGG:R00024"`` -> ``"R00024"``.""" + if raw is None or (isinstance(raw, float) and pd.isna(raw)): + return "" + s = str(raw).strip() + if not s or s.lower() == "nan": + return "" + if s.upper().startswith("KEGG:"): + s = s[5:].strip() + return s + + +def _k_numbers_for(kegg_id: str, features: Dict[str, Dict]) -> Set[str]: + """KEGG Orthology K-numbers for a reaction, parsed from the ORTHOLOGY field.""" + feat = features.get(kegg_id) + if not feat: + return set() + orth = str(feat.get("ORTHOLOGY", "") or "") + return set(_K_NUMBER_RE.findall(orth)) + + +def normalize_ranks_by_brite( + ordered_candidates: List[str], features: Dict[str, Dict] +) -> Dict[str, int]: + """Assign each candidate a rank, then collapse candidates sharing any KEGG + Orthology K-number into one group and give every member the group's best + (lowest) rank. Candidates without any K-numbers keep their own rank. + """ + # Deduplicate while preserving first-seen order (best rank wins). + seen: Dict[str, int] = {} + for idx, cand in enumerate(ordered_candidates, start=1): + if cand and cand not in seen: + seen[cand] = idx + ranks: Dict[str, int] = dict(seen) + if not ranks: + return ranks + + # Union-find over candidates that share any K-number. + parent: Dict[str, str] = {c: c for c in ranks} + + def find(x: str) -> str: + while parent[x] != x: + parent[x] = parent[parent[x]] + x = parent[x] + return x + + def union(a: str, b: str) -> None: + ra, rb = find(a), find(b) + if ra != rb: + # Keep the one with the better (lower) rank as root. + if ranks[ra] <= ranks[rb]: + parent[rb] = ra + else: + parent[ra] = rb + + k_to_candidates: Dict[str, List[str]] = {} + for cand in ranks: + for k in _k_numbers_for(cand, features): + k_to_candidates.setdefault(k, []).append(cand) + for cands in k_to_candidates.values(): + for other in cands[1:]: + union(cands[0], other) + + # Collapse to root rank. + return {c: ranks[find(c)] for c in ranks} + + +def build_normalized_rank_rows( + model_id: str, + reaction_id: str, + ordered_candidates: List[str], + features: Dict[str, Dict], +) -> List[Dict[str, object]]: + """Per-candidate raw vs BRITE-normalized ranks for one reaction.""" + normalized = normalize_ranks_by_brite(ordered_candidates, features) + # normalized only includes first-seen candidates (deduped). + rows: List[Dict[str, object]] = [] + for cand, norm_rank in normalized.items(): + # raw rank is the first-seen rank in ordered_candidates (1-indexed) + raw_rank = None + for i, c in enumerate(ordered_candidates, start=1): + if c == cand: + raw_rank = i + break + k_numbers = sorted(_k_numbers_for(cand, features)) + rows.append( + { + "model_id": model_id, + "reaction_id": reaction_id, + "candidate_kegg": cand, + "raw_rank": int(raw_rank) if raw_rank is not None else None, + "normalized_rank": int(norm_rank), + "k_numbers": ";".join(k_numbers), + } + ) + # Stable output ordering by raw rank then candidate id. + rows.sort(key=lambda r: ((r.get("raw_rank") or 10**9), str(r.get("candidate_kegg") or ""))) + return rows + + +# --------------------------------------------------------------------------- +# 6. Per-reaction scoring +# --------------------------------------------------------------------------- + +def evaluate_model( + model_file: Path, + ground_truth: Dict[str, str], + recommendations_df: pd.DataFrame, + features: Dict[str, Dict], + species_source: str, + ssx_reaction_ids: Optional[Set[str]] = None, + *, + normalize_brite: bool = True, +) -> Tuple[List[Dict], List[Dict]]: + """Produce one evaluation row per ground-truth reaction. + + ``species_source == "none"`` means the model had no usable species + annotations, so every reaction is scored as a mapping failure (no + candidates). Otherwise reactions missing from ``recommendations_df`` are + counted as having zero candidates. + + ``failure_reason`` when ``found`` is False (empty when ``found`` is True): + + - ``no_species_annotations`` — no usable species annotations for the model. + - ``SSX`` — source/sink/exchange-style stoichiometry (empty LHS or RHS) while + ``INCLUDE_EXCHANGE_REACTIONS`` is False; no rule-based candidates and not + sent to the LLM ranker, same as the annotation pipeline. + - ``no_candidates`` — zero candidates for other reasons (e.g. no KEGG match). + - ``""`` — candidates existed but the ground-truth KEGG id was not among them + (aggregate reporting labels these as ``not_in_candidates``). + + When ``normalize_brite`` is False, candidate ranks are used as-is (raw list + order) without collapsing KEGG-Orthology co-members into one group. + """ + model_id = model_file.stem + by_reaction: Dict[str, List[str]] = {} + has_brite_col = ( + not recommendations_df.empty + and "brite_group_members" in recommendations_df.columns + ) + if not recommendations_df.empty and {"id", "annotation"}.issubset(recommendations_df.columns): + for rxn_id, group in recommendations_df.groupby("id", sort=False): + cands: List[str] = [] + for _, row in group.iterrows(): + bare = _strip_kegg_prefix(row.get("annotation", "")) + if bare and bare not in cands: + cands.append(bare) + # Pre-LLM filtering shows the LLM only the BRITE-orthology + # representative; the file lists co-members in + # `brite_group_members`. Expand them here at the same rank slot + # so a ground-truth that is a non-representative member is still + # counted as found (normalize_ranks_by_brite below collapses + # them anyway, but this also handles candidates whose K-numbers + # are absent from the loaded features). + if has_brite_col: + raw = row.get("brite_group_members", "") + if isinstance(raw, str) and raw.strip(): + for m in raw.split(";"): + m = m.strip() + if m and m not in cands: + cands.append(m) + by_reaction[str(rxn_id)] = cands + + ssx_ids = ssx_reaction_ids or set() + + rows: List[Dict] = [] + rank_rows: List[Dict] = [] + for rxn_id, truth in ground_truth.items(): + cands = by_reaction.get(rxn_id, []) + num_candidates = len(cands) + + if species_source == "none": + failure_reason = "no_species_annotations" + elif num_candidates == 0 and rxn_id in ssx_ids: + failure_reason = "SSX" + elif num_candidates == 0: + failure_reason = "no_candidates" + else: + failure_reason = "" + + if num_candidates: + rank_rows.extend(build_normalized_rank_rows(model_id, rxn_id, cands, features)) + + if num_candidates and truth in cands: + if normalize_brite: + normalized = normalize_ranks_by_brite(cands, features) + rank = normalized.get(truth) + else: + rank = cands.index(truth) + 1 + found = True + else: + rank = None + found = False + + rows.append( + { + "model_id": model_id, + "reaction_id": rxn_id, + "ground_truth_kegg": truth, + "found": bool(found), + "rank": int(rank) if rank is not None else None, + "top1": bool(found and rank == 1), + "top3": bool(found and rank is not None and rank <= 3), + "top5": bool(found and rank is not None and rank <= 5), + "num_candidates": num_candidates, + "species_source": species_source, + "failure_reason": failure_reason if not found else "", + } + ) + return rows, rank_rows + + +# --------------------------------------------------------------------------- +# 7. Aggregate reporting +# --------------------------------------------------------------------------- + +def summarize(per_reaction: pd.DataFrame) -> pd.DataFrame: + total = len(per_reaction) + if total == 0: + return pd.DataFrame( + [{"total_reactions": 0, "coverage": 0.0, "top1": 0.0, "top3": 0.0, "top5": 0.0}] + ) + return pd.DataFrame( + [ + { + "total_reactions": int(total), + "coverage": float(per_reaction["found"].mean()), + "top1": float(per_reaction["top1"].mean()), + "top3": float(per_reaction["top3"].mean()), + "top5": float(per_reaction["top5"].mean()), + } + ] + ) + + +# --------------------------------------------------------------------------- +# 8. Driver +# --------------------------------------------------------------------------- + +def process_model( + model_file: Path, + features: Dict[str, Dict], + work_dir: Path, + *, + rank_with_llm: bool = True, + disable_cofactors: bool = False, + disable_ontology_relaxation: bool = False, + normalize_brite: bool = True, + llm_top_k: int = DEFAULT_LLM_TOP_K, + kegg_features_file: Optional[str] = None, +) -> List[Dict]: + if not model_file.exists(): + logger.warning("Model not found, skipping: %s", model_file) + return [] + + logger.info("==> %s", model_file) + + ground_truth = extract_ground_truth_reactions(model_file) + if not ground_truth: + logger.info(" no KEGG reaction ground truth; skipping model") + return [] + logger.info(" %d ground-truth reactions", len(ground_truth)) + + ssx_ids: Set[str] = set() + if not INCLUDE_EXCHANGE_REACTIONS: + try: + ssx_ids = exchange_constraint_skipped_reaction_ids(str(model_file)) + except Exception as exc: # pragma: no cover — antimony / SBML edge cases + logger.warning(" could not classify SSX reactions: %s", exc) + + species_df, source = build_species_recommendations_df(model_file) + if species_df is None: + logger.warning(" no species annotations; all reactions -> mapping failure") + rxn_rows, rank_rows = evaluate_model( + model_file, ground_truth, pd.DataFrame(), features, "none", ssx_ids, + normalize_brite=normalize_brite, + ) + process_model._last_rank_rows = rank_rows # type: ignore[attr-defined] + return rxn_rows + logger.info(" species annotations: %d rows (source=%s)", len(species_df), source) + + try: + rec_df = run_annotation( + model_file, + species_df, + work_dir, + rank_with_llm=rank_with_llm, + disable_cofactors=disable_cofactors, + disable_ontology_relaxation=disable_ontology_relaxation, + llm_top_k=llm_top_k, + kegg_features_file=kegg_features_file, + ) + except Exception as exc: # pragma: no cover — pipeline failures surface here + logger.exception(" annotate_model failed: %s", exc) + rec_df = pd.DataFrame(columns=["id", "annotation"]) + + rxn_rows, rank_rows = evaluate_model( + model_file, ground_truth, rec_df, features, source, ssx_ids, + normalize_brite=normalize_brite, + ) + process_model._last_rank_rows = rank_rows # type: ignore[attr-defined] + return rxn_rows + + +def main() -> int: + wall_start = time.time() + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument( + "--model-list", type=Path, default=DEFAULT_MODEL_LIST, + help="Text file listing model paths (default: tests/kegg_annotated_files.txt).", + ) + parser.add_argument( + "--results-dir", + type=Path, + default=DEFAULT_RESULTS_DIR, + help=( + "Base output directory. Each run writes into a new timestamped " + "subdirectory under this path." + ), + ) + parser.add_argument( + "--limit", type=int, default=None, + help="Process only the first N models (for smoke testing).", + ) + parser.add_argument( + "--run-id", + type=str, + default=None, + help=( + "Optional run identifier (subdirectory name). Defaults to a timestamp, " + "e.g. 20260421_134455." + ), + ) + parser.add_argument( + "--work-dir", + type=Path, + default=None, + help=( + "Optional scratch directory for intermediate CSVs. Defaults to " + "/_work." + ), + ) + parser.add_argument( + "--skip-llm-ranking", + action="store_true", + help=( + "Do not call rank_kegg_annotations_with_llm; evaluate using only the " + "rule-based _recommendations.csv order (faster, no LLM API)." + ), + ) + parser.add_argument( + "--skip-cofactor-removal", + action="store_true", + help=( + "Disable cofactor filtering during reaction matching (H2O, H+, ATP, " + "NAD+, etc. are included as regular participants). By default cofactors " + "are removed before candidate scoring." + ), + ) + parser.add_argument( + "--skip-brite-normalization", + action="store_true", + help=( + "Use raw candidate list order for scoring instead of collapsing " + "KEGG-Orthology co-members (BRITE groups) to their best shared rank." + ), + ) + parser.add_argument( + "--skip-ontology-relaxation", + action="store_true", + help=( + "Disable ChEBI ontology relaxation during reaction matching. Species " + "are matched only at their exact annotated ChEBI level; no ancestor " + "traversal is attempted." + ), + ) + parser.add_argument( + "--llm-top-k", + type=int, + default=DEFAULT_LLM_TOP_K, + help="Max KEGG reaction ids to keep per reaction from the LLM (default: 10).", + ) + parser.add_argument( + "--kegg-features-file", + type=str, + default=None, + help=( + "Path or filename for kegg_reaction_features.lzma (default: " + f"{DEFAULT_KEGG_FEATURES_FILE}, resolved under data/kegg/)." + ), + ) + args = parser.parse_args() + + def _safe_slug(s: str) -> str: + # Windows-safe-ish: avoid characters that break paths. + return ( + str(s) + .strip() + .replace(" ", "_") + .replace("/", "-") + .replace("\\", "-") + .replace(":", "-") + .replace("|", "-") + .replace("*", "-") + .replace("?", "") + .replace("\"", "") + .replace("<", "(") + .replace(">", ")") + ) + + model_paths = load_model_paths(args.model_list) + if args.limit is not None: + model_paths = model_paths[: args.limit] + logger.info("Evaluating %d models", len(model_paths)) + + rank_with_llm = not bool(args.skip_llm_ranking) + disable_cofactors = bool(args.skip_cofactor_removal) + disable_ontology_relaxation = bool(args.skip_ontology_relaxation) + normalize_brite = not bool(args.skip_brite_normalization) + + timestamp = time.strftime("%Y%m%d_%H%M%S") + _run_suffixes = ( + ("-no-cofactors" if disable_cofactors else "") + + ("-no-relaxation" if disable_ontology_relaxation else "") + + ("-no-brite" if not normalize_brite else "") + ) + run_id = args.run_id or f"{_safe_slug(LLM_MODEL)}-{timestamp}{_run_suffixes}" + run_dir = args.results_dir / run_id + run_dir.mkdir(parents=True, exist_ok=True) + + per_reaction_out = run_dir / "per_reaction_results.csv" + summary_out = run_dir / "results_summary.csv" + normalized_ranks_out = run_dir / "normalized_candidate_ranks.csv" + per_model_timing_out = run_dir / "per_model_timing.csv" + + work_dir = args.work_dir or (run_dir / "_work") + work_dir.mkdir(parents=True, exist_ok=True) + + logger.info("Run directory: %s", run_dir) + + logger.info("Loading KEGG reaction features (for BRITE/orthology grouping)...") + features = load_kegg_reaction_features_dict() + if not rank_with_llm: + logger.info("LLM re-ranking disabled (--skip-llm-ranking).") + if disable_cofactors: + logger.info("Cofactor removal disabled (--skip-cofactor-removal).") + if disable_ontology_relaxation: + logger.info("Ontology relaxation disabled (--skip-ontology-relaxation).") + if not normalize_brite: + logger.info("BRITE normalization disabled (--skip-brite-normalization).") + + all_rows: List[Dict] = [] + all_rank_rows: List[Dict] = [] + timing_rows: List[Dict[str, object]] = [] + for i, model_file in enumerate(model_paths, start=1): + t0 = time.time() + try: + rows = process_model( + model_file, + features, + work_dir, + rank_with_llm=rank_with_llm, + disable_cofactors=disable_cofactors, + disable_ontology_relaxation=disable_ontology_relaxation, + normalize_brite=normalize_brite, + llm_top_k=int(args.llm_top_k), + kegg_features_file=args.kegg_features_file, + ) + except Exception as exc: # pragma: no cover + logger.exception(" model %s failed: %s", model_file, exc) + rows = [] + elapsed = time.time() - t0 + n_eval = len(rows) + spd = (elapsed / n_eval) if n_eval else float("nan") + timing_rows.append( + { + "model_id": model_file.stem, + "model_path": str(model_file), + "wall_seconds": round(elapsed, 3), + "num_evaluated_reactions": int(n_eval), + "seconds_per_reaction": spd if n_eval else float("nan"), + } + ) + pd.DataFrame(timing_rows).to_csv(per_model_timing_out, index=False) + all_rows.extend(rows) + # Collected inside process_model via evaluate_model. + all_rank_rows.extend(getattr(process_model, "_last_rank_rows", []) or []) + # Incremental checkpoint so long runs don't lose progress. + pd.DataFrame(all_rows).to_csv(per_reaction_out, index=False) + spd_msg = f"{spd:.3f}s/rxn" if n_eval else "n/a" + logger.info( + " [%d/%d] %s -> %d rows (%.1fs, %s)", + i, len(model_paths), model_file.name, len(rows), elapsed, spd_msg, + ) + + per_reaction_df = pd.DataFrame(all_rows) + per_reaction_df.to_csv(per_reaction_out, index=False) + logger.info("Per-reaction results written to %s (%d rows)", + per_reaction_out, len(per_reaction_df)) + + timing_df = pd.DataFrame(timing_rows) + timing_df.to_csv(per_model_timing_out, index=False) + logger.info("Per-model timing written to %s (%d models)", per_model_timing_out, len(timing_df)) + + rank_df = pd.DataFrame(all_rank_rows) + rank_df.to_csv(normalized_ranks_out, index=False) + logger.info( + "Normalized candidate ranks written to %s (%d rows)", + normalized_ranks_out, + len(rank_df), + ) + + summary_df = summarize(per_reaction_df) + summary_df.to_csv(summary_out, index=False) + logger.info("Summary written to %s", summary_out) + + total_s = time.time() - wall_start + logger.info("Total runtime: %.1fs", total_s) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/find_kegg_in_biomodels.py b/tests/find_kegg_in_biomodels.py new file mode 100644 index 0000000..254a050 --- /dev/null +++ b/tests/find_kegg_in_biomodels.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import argparse +from pathlib import Path + + +SEARCH_TERM = b"kegg.reaction" + + +def file_contains_kegg(path: Path) -> bool: + try: + # Read as bytes to avoid decode issues; search ASCII substring case-insensitively. + data = path.read_bytes() + except OSError: + return False + return SEARCH_TERM in data.lower() + + +def iter_files(root: Path): + for p in root.rglob("*"): + if p.is_file(): + yield p + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Find files containing a search term (case-insensitive) under a directory." + ) + parser.add_argument( + "--root", + default="tests/BioModels_251106", + help="Directory to scan (default: tests/BioModels_251106)", + ) + parser.add_argument( + "--out", + default="kegg_files.txt", + help="Output file listing matches (default: kegg_files.txt)", + ) + args = parser.parse_args() + + root = Path(args.root) + if not root.exists() or not root.is_dir(): + raise SystemExit(f"Root directory not found or not a directory: {root}") + + matches: list[Path] = [] + for p in iter_files(root): + if file_contains_kegg(p): + matches.append(p) + + out_path = Path(args.out) + out_path.write_text( + "\n".join(str(p.as_posix()) for p in sorted(matches)) + ("\n" if matches else ""), + encoding="utf-8", + ) + + print(f"Scanned: {root}") + print(f"Matches: {len(matches)}") + print(f"Wrote: {out_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/tests/kegg_annotated_files.txt b/tests/kegg_annotated_files.txt new file mode 100644 index 0000000..7990eab --- /dev/null +++ b/tests/kegg_annotated_files.txt @@ -0,0 +1,75 @@ +tests/BioModels_251106/BIOMD0000000013.xml +tests/BioModels_251106/BIOMD0000000015.xml +tests/BioModels_251106/BIOMD0000000017.xml +tests/BioModels_251106/BIOMD0000000018.xml +tests/BioModels_251106/BIOMD0000000023.xml +tests/BioModels_251106/BIOMD0000000038.xml +tests/BioModels_251106/BIOMD0000000042.xml +tests/BioModels_251106/BIOMD0000000046.xml +tests/BioModels_251106/BIOMD0000000051.xml +tests/BioModels_251106/BIOMD0000000061.xml +tests/BioModels_251106/BIOMD0000000063.xml +tests/BioModels_251106/BIOMD0000000064.xml +tests/BioModels_251106/BIOMD0000000066.xml +tests/BioModels_251106/BIOMD0000000067.xml +tests/BioModels_251106/BIOMD0000000068.xml +tests/BioModels_251106/BIOMD0000000070.xml +tests/BioModels_251106/BIOMD0000000071.xml +tests/BioModels_251106/BIOMD0000000076.xml +tests/BioModels_251106/BIOMD0000000081.xml +tests/BioModels_251106/BIOMD0000000088.xml +tests/BioModels_251106/BIOMD0000000093.xml +tests/BioModels_251106/BIOMD0000000094.xml +tests/BioModels_251106/BIOMD0000000108.xml +tests/BioModels_251106/BIOMD0000000122.xml +tests/BioModels_251106/BIOMD0000000123.xml +tests/BioModels_251106/BIOMD0000000137.xml +tests/BioModels_251106/BIOMD0000000143.xml +tests/BioModels_251106/BIOMD0000000171.xml +tests/BioModels_251106/BIOMD0000000172.xml +tests/BioModels_251106/BIOMD0000000176.xml +tests/BioModels_251106/BIOMD0000000177.xml +tests/BioModels_251106/BIOMD0000000190.xml +tests/BioModels_251106/BIOMD0000000191.xml +tests/BioModels_251106/BIOMD0000000206.xml +tests/BioModels_251106/BIOMD0000000211.xml +tests/BioModels_251106/BIOMD0000000212.xml +tests/BioModels_251106/BIOMD0000000213.xml +tests/BioModels_251106/BIOMD0000000218.xml +tests/BioModels_251106/BIOMD0000000219.xml +tests/BioModels_251106/BIOMD0000000221.xml +tests/BioModels_251106/BIOMD0000000222.xml +tests/BioModels_251106/BIOMD0000000225.xml +tests/BioModels_251106/BIOMD0000000231.xml +tests/BioModels_251106/BIOMD0000000232.xml +tests/BioModels_251106/BIOMD0000000236.xml +tests/BioModels_251106/BIOMD0000000244.xml +tests/BioModels_251106/BIOMD0000000245.xml +tests/BioModels_251106/BIOMD0000000247.xml +tests/BioModels_251106/BIOMD0000000248.xml +tests/BioModels_251106/BIOMD0000000253.xml +tests/BioModels_251106/BIOMD0000000268.xml +tests/BioModels_251106/BIOMD0000000281.xml +tests/BioModels_251106/BIOMD0000000292.xml +tests/BioModels_251106/BIOMD0000000450.xml +tests/BioModels_251106/BIOMD0000000471.xml +tests/BioModels_251106/BIOMD0000000472.xml +tests/BioModels_251106/BIOMD0000000473.xml +tests/BioModels_251106/BIOMD0000000503.xml +tests/BioModels_251106/BIOMD0000000579.xml +tests/BioModels_251106/BIOMD0000000590.xml +tests/BioModels_251106/BIOMD0000000602.xml +tests/BioModels_251106/BIOMD0000000627.xml +tests/BioModels_251106/BIOMD0000000633.xml +tests/BioModels_251106/BIOMD0000000674.xml +tests/BioModels_251106/BIOMD0000000689.xml +tests/BioModels_251106/BIOMD0000000690.xml +tests/BioModels_251106/BIOMD0000000691.xml +tests/BioModels_251106/BIOMD0000000725.xml +tests/BioModels_251106/BIOMD0000001061.xml +tests/BioModels_251106/BIOMD0000001062.xml +tests/BioModels_251106/BIOMD0000001063.xml +tests/BioModels_251106/BIOMD0000001090.xml +tests/BioModels_251106/BIOMD0000001091.xml +tests/BioModels_251106/BIOMD0000001092.xml +tests/BioModels_251106/BIOMD0000001093.xml diff --git a/utils/constants.py b/utils/constants.py index 8cb326c..ad8298d 100644 --- a/utils/constants.py +++ b/utils/constants.py @@ -147,12 +147,14 @@ class DatabaseID(Enum): ] KEGG_REACTION_URI_PATTERNS = [ - r'https?://identifiers\.org/kegg\.reaction:(R\d+)', + # identifiers.org supports both `prefix:ID` and `prefix/ID` forms + r'https?://identifiers\.org/kegg\.reaction[:/](R\d+)', r'urn:miriam:kegg\.reaction:(R\d+)' ] KEGG_COMPOUND_URI_PATTERNS = [ - r'https?://identifiers\.org/kegg\.compound:(C\d+)', + # identifiers.org supports both `prefix:ID` and `prefix/ID` forms + r'https?://identifiers\.org/kegg\.compound[:/](C\d+)', r'urn:miriam:kegg\.compound:(C\d+)' ] @@ -298,19 +300,13 @@ class DatabaseID(Enum): REACTION_ANNOTATION_RANKING_PROMPT = """Task: Select the best matching KEGG reaction ID(s). - -Model reaction: -{model_reaction} - -Candidate KEGG reactions: -{reaction_annotation_choices} - Instructions: - Choose only from the provided KEGG IDs. - Return the ID(s) only. Do NOT explain your reasoning. Do NOT include any other text. - Order multiple IDs from best to worst match. - If none match, return: UNK -- If multiple candidates differ only in specificity, rank the most general reaction highest (e.g., fructose > D-fructose > beta-D-fructose). +- Interpret the reaction within the full model context (all other reactions). Prefer KEGG reactions that are biochemically consistent with the network (shared metabolites, cofactors, and plausible pathway context). +- If multiple candidates differ only in specificity, rank the most specific reaction highest (e.g., beta-D-fructose > D-fructose > fructose). - Consider biochemical equivalence (e.g., isomers, implicit conversions like DHAP ↔ G3P). Output format: @@ -326,6 +322,19 @@ class DatabaseID(Enum): R01070: beta-D-Fructose 1,6-bisphosphate <=> Glycerone phosphate + D-Glyceraldehyde 3-phosphate Output: -R01068 R01070 +R01068 + +Now you try: + +Model context: +{model_context} + +Model reaction: +{model_reaction} + +Candidate KEGG reactions: +{reaction_annotation_choices} + +Again, return the ID(s) ONLY. Do NOT explain your reasoning. Do NOT include any other text. """ \ No newline at end of file