Repository 'gem_escher_visualization'
hg clone https://toolshed.g2.bx.psu.edu/repos/iuc/gem_escher_visualization

Changeset 0:b79cf44068bc (2024-12-13)
Commit message:
planemo upload for repository https://github.com/AlmaasLab/elixir-galaxy-tools-systemsbiology commit 3f7bec1264a86e1488ee1315dbac0f44675f5171
added:
gem_escher_visualization.py
gem_escher_visualization.xml
gem_extract_exchange.py
gem_flux_distribution.py
gem_flux_variability_analysis.py
gem_knockout.py
gem_macros.xml
gem_phenotype_phase_plane.py
test-data/e_coli_core_test_map.json
test-data/expected_single_knockout.csv
test-data/invalid_format.txt
test-data/textbook_model_cobrapy.xml
test-data/textbook_model_cobrapy_exchange.csv
b
diff -r 000000000000 -r b79cf44068bc gem_escher_visualization.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/gem_escher_visualization.py Fri Dec 13 21:32:58 2024 +0000
[
@@ -0,0 +1,233 @@
+import argparse
+
+import cobra
+import pandas as pd
+from escher import Builder
+
+
+def __main__():
+    parser = argparse.ArgumentParser(
+        prog="EscherVisualization",
+        description="This program visualizes an Escher map",
+        epilog="Adding an epilog, but doubt it's needed.",
+    )
+    parser.add_argument(
+        "-m",
+        "--cb_model_location",
+        dest="cb_model_location",
+        action="store",
+        type=str,
+        default=None,
+        required=False,
+        help="The model to use."
+    )
+    parser.add_argument(
+        "-f",
+        "--flux_distribution_location",
+        dest="flux_distribution_location",
+        action="store",
+        type=str,
+        default=None,
+        required=False,
+        help="The flux distribution to visualize."
+    )
+    parser.add_argument(
+        "-e",
+        "--expect_map",
+        dest="expect_map",
+        action="store",
+        type=str,
+        default=None,
+        required=True,
+        help="Is a map expected to be uploaded?"
+    )
+    parser.add_argument(
+        "-l",
+        "--model_to_download",
+        dest="model_to_download",
+        action="store",
+        type=str,
+        default=None,
+        required=False,
+        help="The model to download."
+    )
+    parser.add_argument(
+        "--map_load_name",
+        dest="map_load_name",
+        action="store",
+        type=str,
+        default=None,
+        required=False,
+        help="The name of the map to use."
+    )
+    parser.add_argument(
+        "--map_upload_name",
+        dest="map_upload_name",
+        action="store",
+        type=str,
+        default=None,
+        required=False,
+        help="The name of the map to use."
+    )
+    parser.add_argument(
+        "-u",
+        "--uptake_constraints_file",
+        dest="uptake_constraints_file",
+        action="store",
+        type=str,
+        default=None,
+        required=False,
+        help="File containing new uptake constraits."
+    )
+    parser.add_argument(
+        "-output",
+        "--output",
+        dest="out_file",
+        action="store",
+        type=str,
+        default=None,
+        required=True,
+        help="The output file."
+    )
+
+    args = parser.parse_args()
+
+    if args.expect_map not in ["True", "False"]:
+        raise Exception("The expect_map argument must be either True or False.")
+    if args.expect_map == "True" and args.map_load_name is None and \
+            args.map_upload_name is None:
+        raise Exception(
+            "You must specify a map name if a map is expected to be uploaded."
+        )
+
+    cb_model = None
+    model_name = None
+    if args.model_to_download is not None and args.model_to_download != "None":
+        if args.cb_model_location is not None \
+                and args.cb_model_location != "None":
+            raise Exception(
+                "You cannot specify both a model to "
+                "download and a model to use."
+            )
+        model_name = args.model_to_download
+    elif args.cb_model_location is not None\
+            and args.cb_model_location != "None":
+        try:
+            cb_model = cobra.io.read_sbml_model(args.cb_model_location)
+        except Exception as e:
+            raise Exception(
+                "The model could not be read. "
+                "Ensure it is in correct SBML format."
+            ) from e
+
+    map_name = None
+    map_location = None
+    if args.map_upload_name is not None and args.map_upload_name != "None":
+        if args.map_load_name is not None and args.map_load_name != "None":
+            raise Exception(
+                "You cannot specify both a map to upload and a map to load."
+            )
+        map_location = args.map_upload_name
+    elif args.map_load_name is not None and args.map_load_name != "None":
+        map_name = args.map_load_name
+
+    if args.uptake_constraints_file is not None and \
+            args.uptake_constraints_file != "None":
+        if cb_model is None:
+            raise Exception(
+                "You cannot specify uptake constraints "
+                "without uploading a model."
+            )
+        else:
+            constraints_df = pd.read_csv(
+                args.uptake_constraints_file,
+                sep=";",
+                header=0,
+                index_col=False
+            )
+            for index, row in constraints_df.iterrows():
+                rxn_id = row["reaction_id"]
+                cb_model.reactions.get_by_id(rxn_id).lower_bound = \
+                    row["lower_bound"]
+                cb_model.reactions.get_by_id(rxn_id).upper_bound = \
+                    row["upper_bound"]
+
+    flux_dict = None
+    if args.flux_distribution_location is not None and \
+            args.flux_distribution_location != "None":
+        if cb_model is None:
+            raise Exception(
+                "You cannot specify a flux distribution "
+                "without uploading a model."
+            )
+        if args.uptake_constraints_file is not None and \
+                args.uptake_constraints_file != "None":
+            raise Exception(
+                "You cannot specify both uptake constraints and a flux "
+                "distribution."
+            )
+        try:
+            flux_df = pd.read_csv(
+                args.flux_distribution_location,
+                sep=";",
+                header=0,
+                index_col=False
+            )
+            flux_dict = {
+                key: value for key, value in zip(
+                    flux_df['reaction_name'],
+                    flux_df['flux']
+                )
+            }
+        except Exception as e:
+            raise Exception(
+                "The flux distribution file could not be read. "
+                "Ensure the file has semicolon-separated "
+                "columns and a header row."
+            ) from e
+
+    if cb_model is not None and flux_dict is None:
+        solution = cobra.flux_analysis.pfba(cb_model)
+
+        # make a dataframe with the reaction names, reaction ids, and flux
+        flux_distribution = pd.DataFrame(
+            columns=["reaction_name", "reaction_id", "flux"]
+        )
+        flux_distribution["reaction_name"] = [
+            reaction.name for reaction in cb_model.reactions
+        ]
+        flux_distribution["reaction_id"] = [
+            reaction.id for reaction in cb_model.reactions
+        ]
+        flux_distribution["flux"] = [
+            solution.fluxes[reaction.id] for reaction in cb_model.reactions
+        ]
+        flux_dict = {
+            key: value for key, value in zip(
+                flux_distribution['reaction_name'],
+                flux_distribution['flux']
+            )
+        }
+
+    builder = Builder()
+    if map_name is not None:
+        builder.map_name = map_name
+        print("Downloading map...")
+    if map_location is not None:
+        builder.map_json = map_location
+        print("Uploading map...")
+    if model_name is not None:
+        builder.model_name = model_name
+        print("Downloading model...")
+    if cb_model is not None:
+        builder.model = cb_model
+        print("Uploading model...")
+
+    if flux_dict is not None:
+        builder.reaction_data = flux_dict
+
+    builder.save_html(args.out_file)
+
+
+if __name__ == "__main__":
+    __main__()
b
diff -r 000000000000 -r b79cf44068bc gem_escher_visualization.xml
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/gem_escher_visualization.xml Fri Dec 13 21:32:58 2024 +0000
[
@@ -0,0 +1,154 @@
+<tool id="gem_escher_visualization" name="Pathway visualization" version="@VERSION@" profile="@PROFILE@">
+    <description>
+        of a GEM using Escher
+    </description>
+    <macros>
+        <import>gem_macros.xml</import>
+    </macros>
+    <expand macro="requirements"/>
+    <expand macro="version_command_escher"/>
+    <command>
+        python '$__tool_directory__/gem_escher_visualization.py'
+            -output '${output}'
+            --cb_model_location '${cb_model_location}'
+            #if not $flux_distribution_location == "None":
+                --flux_distribution_location '${flux_distribution_location}'
+            #end if
+            #if not $uptake_constraints_file == "None":
+                --uptake_constraints_file '${uptake_constraints_file}'
+            #end if
+            #if $map_selection.map_selection_select == "upload_map" or $map_selection.map_selection_select == "load_map":
+                --expect_map 'True'
+            #elif $map_selection.map_selection_select == "no_map":
+                --expect_map 'False'
+            #end if
+            #if $map_selection.map_selection_select == "upload_map":
+                --map_upload_name '${map_selection.map_upload_name}'
+            #elif $map_selection.map_selection_select == "load_map":
+                --map_load_name '${map_selection.map_load_name}'
+            #end if
+    </command>
+    <inputs>
+        <expand macro="input_model"/>
+        <param format="tabular,csv" name="flux_distribution_location" type="data" label="Flux distribution to visualize flux for" optional="true"/>
+        <expand macro="input_uptake_constraints"/>
+        <conditional name="map_selection">
+            <param format="select" name="map_selection_select" type="select" label="How would you like to select a map?">
+                <option value="load_map">Load a map from the list below</option>
+                <option value="upload_map">Upload a map</option>
+                <option value="no_map">Don't use a map (I'll draw my own from scratch)</option>
+            </param>
+            <when value="load_map">
+                <param format="select" name="map_load_name" type="select" label="Map to load" optional="false">
+                    <option value="iMM904.Central carbon metabolism">iMM904 (S. cerevisiae), Central carbon metabolism</option>
+                    <option value="RECON1.Inositol retinol metabolism">RECON1 (H. sapiens), Inositol retinol metabolism</option>
+                    <option value="RECON1.Glycolysis TCA PPP">RECON1 (H. sapiens), Glycolysis TCA PPP</option>
+                    <option value="RECON1.Tryptophan metabolism">RECON1 (H. sapiens), Tryptophan metabolism</option>
+                    <option value="RECON1.Carbohydrate metabolism">RECON1 (H. sapiens), Carbohydrate metabolism</option>
+                    <option value="RECON1.Amino acid metabolism (partial)">RECON1 (H. sapiens), Amino acid metabolism (partial)</option>
+                    <option value="iJO1366.Nucleotide metabolism">iJO1366 (E. coli), Nucleotide metabolism</option>
+                    <option value="iJO1366.Fatty acid biosynthesis (saturated)">iJO1366 (E. coli), Fatty acid biosynthesis (saturated)</option>
+                    <option value="iJO1366.Nucleotide and histidine biosynthesis">iJO1366 (E. coli), Nucleotide and histidine biosynthesis</option>
+                    <option value="e_coli_core.Core metabolism">e_coli_core, Core metabolism</option>
+                    <option value="iJO1366.Central metabolism">iJO1366 (E. coli), Central metabolism</option>
+                    <option value="iJO1366.Fatty acid beta-oxidation">iJO1366 (E. coli), Fatty acid beta-oxidation</option>
+                </param>
+            </when>
+            <when value="upload_map">
+                <param format="txt" name="map_upload_name" type="data" label="Map to use for visualizing fluxes" optional="true"/>
+            </when>
+            <when value="no_map">
+                <!-- Do nothing -->
+            </when>
+        </conditional>
+    </inputs>
+    <outputs>
+        <data name="output" format="html" label="${tool.name} on ${on_string}"/>
+    </outputs>
+    <tests>
+        <!-- Test 1: Valid E. coli core model, no map -->
+        <test>
+            <param name="cb_model_location" value="textbook_model_cobrapy.xml"/>
+            <conditional name="map_selection">
+                <param name="map_selection_select" value="no_map"/>
+            </conditional>
+            <output name="output">
+                <assert_contents>
+                    <has_line line="     var data = get_data();"/>
+                </assert_contents>
+            </output>
+        </test>
+        <!-- Test 2: Valid E. coli core model, load map -->
+        <test>
+            <param name="cb_model_location" value="textbook_model_cobrapy.xml"/>
+            <conditional name="map_selection">
+                <param name="map_selection_select" value="load_map"/>
+                <param name="map_load_name" value="e_coli_core.Core metabolism"/>
+            </conditional>
+            <output name="output">
+                <assert_contents>
+                    <has_line line="     var data = get_data();"/>
+                </assert_contents>
+            </output>
+        </test>
+        <!-- Test 3: Valid E. coli core model, upload map -->
+        <test>
+            <param name="cb_model_location" value="textbook_model_cobrapy.xml"/>
+            <conditional name="map_selection">
+                <param name="map_selection_select" value="upload_map"/>
+                <param name="map_upload_name" value="e_coli_core_test_map.json"/>
+            </conditional>
+            <output name="output">
+                <assert_contents>
+                    <has_line line="     var data = get_data();"/>
+                </assert_contents>
+            </output>
+        </test>
+        <!-- Test 4: Invalid model, no map -->
+        <test expect_failure="true">
+            <param name="cb_model_location" value="invalid_format.txt"/>
+            <conditional name="map_selection">
+                <param name="map_selection_select" value="no_map"/>
+            </conditional>
+            <assert_stderr>
+                <has_text text="The model could not be read"/>
+            </assert_stderr>
+        </test>
+    </tests>
+    <help><![CDATA[
+        Escher Visualization Tool
+
+        Overview
+
+        This tool generates an interactive HTML visualization of metabolic flux distributions in a Genome-scale Metabolic Model (GEM) using Escher.
+
+        Inputs
+
+        * Model File (Required): COBRA-compatible metabolic model file
+        * Flux Distribution (Optional): a pre-calculated flux distribution
+        * Model constraints (Optional): constraints to apply during model simulation if no flux distribution is provided
+        * Map Selection (Optional): maps for common metabolic models
+
+        Output
+
+        The tool generates an interactive HTML file that:
+        * Displays metabolic pathways with reaction fluxes
+        * Allows zooming and panning
+        * Enables hovering over reactions for detailed information
+        * Provides options for customizing the visualization
+
+        Tips
+
+        * Ensure your model is properly formatted and constrained
+        * For large models, consider visualizing specific subsystems
+        * Custom maps should follow Escher's JSON format
+        * The visualization works best with modern web browsers
+
+        For more information about Escher, visit: https://escher.github.io/
+    ]]></help>
+    <citations>
+        <expand macro="citation_escher"/>
+        <expand macro="citation_pandas"/>
+        <expand macro="citation_cobrapy"/>
+    </citations>
+</tool>
b
diff -r 000000000000 -r b79cf44068bc gem_extract_exchange.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/gem_extract_exchange.py Fri Dec 13 21:32:58 2024 +0000
[
@@ -0,0 +1,80 @@
+import argparse
+
+import cobra
+
+
+def read_model(model_location):
+    model = cobra.io.read_sbml_model(model_location)
+    return model
+
+
+def get_exchange_reactions_info(model):
+    exchange_reactions = model.exchanges
+    exchange_reactions_info = []
+    for reaction in exchange_reactions:
+        exchange_reactions_info.append(
+            [
+                reaction.id,
+                reaction.name,
+                reaction.reaction,
+                reaction.lower_bound,
+                reaction.upper_bound
+            ])
+    txt_object = (
+        "reaction_id;reaction_name;reaction_stoichiometry;"
+        "lower_bound;upper_bound\n"
+    )
+    for reaction in exchange_reactions_info:
+        txt_object += ";".join([str(x) for x in reaction]) + "\n"
+    return txt_object
+
+
+def __main__():
+
+    # Parsing arguments
+    parser = argparse.ArgumentParser(
+        prog="GEM ",
+        description="This program retrieves the exchange fluxes "
+        "of a GEM model to be used in Galaxy.",
+        epilog="Adding an epilog, but doubt it's needed.",
+    )
+    parser.add_argument(
+        "-m",
+        "--cb_model_location",
+        dest="cb_model_location",
+        action="store",
+        type=str,
+        default=None,
+        required=True,
+        help="The model to use."
+    )
+    parser.add_argument(
+        "-output",
+        "--output",
+        dest="out_file",
+        action="store",
+        type=str,
+        default=None,
+        required=True,
+        help="The output file."
+    )
+    args = parser.parse_args()
+
+    # Reading model from file
+    try:
+        cb_model = read_model(args.cb_model_location)
+    except Exception as e:
+        raise Exception(
+            "The model could not be read. Ensure it is in correct SBML format."
+        ) from e
+
+    # Getting exchange reactions info
+    answer = get_exchange_reactions_info(cb_model)
+
+    # Writing exchange reactions info to file
+    with open(args.out_file, "w") as outfile:
+        outfile.write(str(answer))
+
+
+if __name__ == "__main__":
+    __main__()
b
diff -r 000000000000 -r b79cf44068bc gem_flux_distribution.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/gem_flux_distribution.py Fri Dec 13 21:32:58 2024 +0000
[
@@ -0,0 +1,90 @@
+import argparse
+
+import cobra
+import pandas as pd
+
+
+def __main__():
+    parser = argparse.ArgumentParser(
+        prog="FluxDistribution",
+        description="This program calculates the flux distribution of a GEM",
+        epilog="Adding an epilog, but doubt it's needed.",
+    )
+    parser.add_argument(
+        "-m",
+        "--cb_model_location",
+        dest="cb_model_location",
+        action="store",
+        type=str,
+        default=None,
+        required=True,
+        help="The model to use."
+    )
+    parser.add_argument(
+        "-output",
+        "--output",
+        dest="out_file",
+        action="store",
+        type=str,
+        default=None,
+        required=True,
+        help="The output file."
+    )
+    parser.add_argument(
+        "-u",
+        "--uptake_constraints_file",
+        dest="uptake_constraints_file",
+        action="store",
+        type=str,
+        default=None,
+        required=False,
+        help="File containing new uptake constraits."
+    )
+
+    args = parser.parse_args()
+
+    try:
+        cb_model = cobra.io.read_sbml_model(args.cb_model_location)
+    except Exception as e:
+        raise Exception(
+            "The model could not be read. Ensure "
+            "it is in correct SBML format."
+        ) from e
+
+    if args.uptake_constraints_file is not None\
+            and args.uptake_constraints_file != "None":
+        constraints_df = pd.read_csv(
+            args.uptake_constraints_file,
+            sep=";",
+            header=0,
+            index_col=False
+        )
+        for _, row in constraints_df.iterrows():
+            cb_model.reactions.get_by_id(
+                row["reaction_id"]
+            ).lower_bound = row["lower_bound"]
+            cb_model.reactions.get_by_id(
+                row["reaction_id"]
+            ).upper_bound = row["upper_bound"]
+
+    # do pFBA
+    solution = cobra.flux_analysis.pfba(cb_model)
+
+    # make a dataframe with the reaction names,
+    # reaction ids, and flux distribution
+    flux_distribution = pd.DataFrame(
+        columns=["reaction_name", "reaction_id", "flux"]
+    )
+
+    flux_distribution["reaction_name"] = \
+        [reaction.name for reaction in cb_model.reactions]
+    flux_distribution["reaction_id"] = \
+        [reaction.id for reaction in cb_model.reactions]
+    flux_distribution["flux"] = \
+        [solution.fluxes[reaction.id] for reaction in cb_model.reactions]
+
+    flux_distribution.to_csv(args.out_file, sep=";", index=False)
+
+
+if __name__ == "__main__":
+    __main__()
b
diff -r 000000000000 -r b79cf44068bc gem_flux_variability_analysis.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/gem_flux_variability_analysis.py Fri Dec 13 21:32:58 2024 +0000
[
@@ -0,0 +1,121 @@
+import argparse
+
+import cobra
+import pandas as pd
+
+
+def __main__():
+    parser = argparse.ArgumentParser(
+        prog="FluxVariabilityAnalysis",
+        description="This program performs flux variability "
+        "analysis on a GEM",
+        epilog="Adding an epilog, but doubt it's needed.",
+    )
+    parser.add_argument(
+        "-m", "--cb_model_location",
+        dest="cb_model_location",
+        action="store",
+        type=str,
+        default=None,
+        required=True,
+        help="The model to use."
+    )
+    parser.add_argument(
+        "-output",
+        "--output",
+        dest="out_file",
+        action="store",
+        type=str,
+        default=None,
+        required=True,
+        help="The output file."
+    )
+    parser.add_argument(
+        "-f",
+        "--fraction",
+        dest="fraction_of_optimum",
+        action="store",
+        type=float,
+        default=None,
+        required=True,
+        help="The fraction of optimum the FVA solutions should come within."
+    )
+    parser.add_argument(
+        "-u",
+        "--uptake_constraints_file",
+        dest="uptake_constraints_file",
+        action="store",
+        type=str,
+        default=None,
+        required=False,
+        help="File containing new uptake constraits."
+    )
+
+    args = parser.parse_args()
+
+    # Validate constraints file first if provided
+    constraints_df = None
+    if args.uptake_constraints_file is not None\
+            and args.uptake_constraints_file != "None":
+        try:
+            constraints_df = pd.read_csv(
+                args.uptake_constraints_file,
+                sep=";",
+                header=0,
+                index_col=False
+            )
+
+            required_columns = ['reaction_id', 'lower_bound', 'upper_bound']
+            missing_columns = [col for col in required_columns if
+                               col not in constraints_df.columns]
+
+            if missing_columns:
+                raise ValueError(
+                    f"Constraints file is missing required columns: "
+                    f"{', '.join(missing_columns)}. "
+                    f"Required columns are: {', '.join(required_columns)}"
+                )
+        except FileNotFoundError:
+            raise FileNotFoundError(
+                f"Constraints file not found: {args.uptake_constraints_file}"
+            )
+        except pd.errors.EmptyDataError:
+            raise ValueError("Constraints file is empty")
+        except Exception as e:
+            raise ValueError(f"Error processing constraints file: {str(e)}")
+
+    # Load model
+    cb_model = cobra.io.read_sbml_model(args.cb_model_location)
+
+    # Apply constraints if they were loaded successfully
+    if constraints_df is not None:
+        for index, row in constraints_df.iterrows():
+            cb_model.reactions.get_by_id(
+                row["reaction_id"]).lower_bound = float(row["lower_bound"])
+            cb_model.reactions.get_by_id(
+                row["reaction_id"]).upper_bound = float(row["upper_bound"])
+
+    fraction_of_optimum = args.fraction_of_optimum
+
+    # perform fva
+    fva_result = cobra.flux_analysis.flux_variability_analysis(
+        cb_model,
+        fraction_of_optimum=fraction_of_optimum
+    )
+
+    # add reaction names and ids to the dataframe
+    fva_result["reaction_id"] = fva_result.index
+    fva_result["reaction_name"] = fva_result["reaction_id"].apply(
+        lambda x: cb_model.reactions.get_by_id(x).name
+    )
+
+    # reorder the columns
+    fva_result = fva_result[[
+        "reaction_id", "reaction_name", "minimum", "maximum"
+    ]]
+
+    fva_result.to_csv(args.out_file, sep=";", index=False, header=True)
+
+
+if __name__ == "__main__":
+    __main__()
b
diff -r 000000000000 -r b79cf44068bc gem_knockout.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/gem_knockout.py Fri Dec 13 21:32:58 2024 +0000
[
@@ -0,0 +1,152 @@
+import argparse
+
+import cobra
+import pandas as pd
+
+
+def __main__():
+    parser = argparse.ArgumentParser(
+        prog="FluxDistribution",
+        description="Performs FBA knockout analysis on a GEM.",
+        epilog="Adding an epilog, but doubt it's needed.",
+    )
+    parser.add_argument(
+        "-m",
+        "--cb_model_location",
+        dest="cb_model_location",
+        action="store",
+        type=str,
+        default=None,
+        required=True,
+        help="The model to use."
+    )
+    parser.add_argument(
+        "-output",
+        "--output",
+        dest="out_file",
+        action="store",
+        type=str,
+        default=None,
+        required=True,
+        help="The output file."
+    )
+    parser.add_argument(
+        "-k",
+        "--knockout_type",
+        dest="knockout_type",
+        action="store",
+        type=str,
+        default="single",
+        required=False,
+        help="Type of knockout to perform, single or double"
+    )
+    parser.add_argument(
+        "-g",
+        "--gene_knockouts",
+        dest="gene_knockouts",
+        action="store",
+        type=str, default=None,
+        required=False,
+        help="List of genes to knock out. Defaults to all."
+    )
+    parser.add_argument(
+        "-u",
+        "--uptake_constraints_file",
+        dest="uptake_constraints_file",
+        action="store",
+        type=str,
+        default=None,
+        required=False,
+        help="File containing new uptake constraits."
+    )
+
+    args = parser.parse_args()
+
+    # Reading the model
+    try:
+        cb_model = cobra.io.read_sbml_model(args.cb_model_location)
+    except Exception as e:
+        raise Exception(
+            "The model could not be read. "
+            "Ensure it is in correct SBML format."
+        ) from e
+
+    # Verifying the genes are present in the model
+    gene_ids = [gene.id for gene in cb_model.genes]
+
+    genes_to_knockout_1 = args.gene_knockouts.split(',')\
+        if args.gene_knockouts is not None else []
+    gene_bool = [
+        True if gene in gene_ids else False for gene in genes_to_knockout_1
+    ]
+    if not all(gene_bool):
+        print(
+            f'Found {sum(gene_bool)} of {len(genes_to_knockout_1)} genes '
+            'in the model.'
+        )
+        raise Exception(
+            "One or more of the genes to knockout are not present "
+            "in the model."
+        )
+
+    # Adding all genes to knockout if none are specified
+    if genes_to_knockout_1 is None or len(genes_to_knockout_1) == 0:
+        genes_to_knockout_1 = [gene.id for gene in cb_model.genes]
+    # Applying uptake constraints
+    if (args.uptake_constraints_file is not None
+            and args.uptake_constraints_file != "None"):
+        constraints_df = pd.read_csv(
+            args.uptake_constraints_file,
+            sep=";",
+            header=0,
+            index_col=False
+        )
+        for index, row in constraints_df.iterrows():
+            reaction = cb_model.reactions.get_by_id(row["reaction_id"])
+            reaction.lower_bound = row["lower_bound"]
+            reaction.upper_bound = row["upper_bound"]
+
+    result = pd.DataFrame(columns=[
+        "reaction_id", "ko_gene_id_1", "ko_gene_id_2",
+        "reaction", "wildtype_flux", "knockout_flux"
+    ])
+
+    if args.knockout_type == "single":
+        genes_to_knockout_2 = [0]
+    elif args.knockout_type == "double":
+        genes_to_knockout_2 = genes_to_knockout_1.copy()
+    else:
+        raise Exception(
+            f"Invalid knockout type {args.knockout_type}. "
+            "Only single and double are allowed."
+        )
+
+    # Wildtype pFBA
+    with cb_model as model:
+        wildtype_solution = model.optimize()
+
+    # Performing gene knockouts
+    for gene1 in genes_to_knockout_1:
+        for gene2 in genes_to_knockout_2:
+            with cb_model as model:
+                model.genes.get_by_id(gene1).knock_out()
+                if args.knockout_type == "double":
+                    model.genes.get_by_id(gene2).knock_out()
+                solution = model.optimize()
+                for reaction in model.reactions:
+                    result = pd.concat([result, pd.DataFrame([{
+                        "reaction_id": reaction.id,
+                        "ko_gene_id_1": gene1,
+                        "ko_gene_id_2": gene2
+                        if args.knockout_type == "double" else None,
+                        "reaction": reaction.reaction,
+                        "wildtype_flux": wildtype_solution.fluxes[reaction.id],
+                        "knockout_flux": solution.fluxes[reaction.id],
+                    }])], ignore_index=True)
+
+    # Writing the results to file
+    result.to_csv(args.out_file, sep=";", index=False)
+
+
+if __name__ == "__main__":
+    __main__()
b
diff -r 000000000000 -r b79cf44068bc gem_macros.xml
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/gem_macros.xml Fri Dec 13 21:32:58 2024 +0000
b
@@ -0,0 +1,75 @@
+<macros>
+    <!-- Tokens -->
+    <token name="@VERSION@">0.29.1</token>
+    <token name="@PROFILE@">23.0</token>
+    
+    <!-- Add version commands for different tools -->
+    <xml name="version_command_cobra">
+        <version_command>echo '@VERSION@'</version_command>
+    </xml>
+    
+    <xml name="version_command_escher">
+        <version_command>python -c 'from escher import __version__; print(__version__)'</version_command>
+    </xml>
+
+    <xml name="version_command_memote">
+        <version_command>python -c 'from memote import __version__; print(__version__)'</version_command>
+    </xml>
+    
+    <!-- Version command uses cobra requirement version -->
+    <xml name="version_command">
+        <version_command>echo '@VERSION@'</version_command>
+    </xml>
+
+    <!-- Setting cobra requirement to use version token -->
+    <xml name="requirements">
+        <requirements>
+            <requirement type="package" version="@VERSION@">cobra</requirement>
+            <requirement type="package" version="2.2.3">pandas</requirement>
+            <requirement type="package" version="0.17.0">memote</requirement>
+            <requirement type="package" version="1.7.3">escher</requirement>
+        </requirements>
+    </xml>
+
+    <!-- Common inputs -->
+    <xml name="input_model">
+        <param format="sbml" name="cb_model_location" type="data" label="Model to analyze"/>
+    </xml>
+
+    <xml name="input_uptake_constraints">
+        <param format="csv" name="uptake_constraints_file" type="data" label="Uptake constraints CSV file" optional="true"/>
+    </xml>
+
+    <!-- Common outputs -->
+    <xml name="output">
+        <data name="output" format="csv" label="${tool.name} on ${on_string}"/>
+    </xml>
+
+    <!-- Common test elements -->
+    <xml name="test_invalid_model">
+        <test expect_failure="true">
+            <param name="cb_model_location" value="invalid_format.txt"/>
+            <assert_stderr>
+                <has_text text="The model could not be read"/>
+            </assert_stderr>
+        </test>
+    </xml>
+
+    <!-- Citations -->
+    <xml name="citation_cobrapy">
+        <citation type="doi">10.1186/1752-0509-7-74</citation>
+    </xml>
+
+    <xml name="citation_pandas">
+        <citation type="doi">10.5281/zenodo.3509134</citation>
+    </xml>
+
+    <xml name="citation_escher">
+        <citation type="doi">10.1371/journal.pcbi.1004321</citation>
+    </xml>
+
+    <xml name="citation_memote">
+        <citation type="doi">10.1038/s41587-020-0446-y</citation>
+    </xml>
+
+</macros>
b
diff -r 000000000000 -r b79cf44068bc gem_phenotype_phase_plane.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/gem_phenotype_phase_plane.py Fri Dec 13 21:32:58 2024 +0000
[
@@ -0,0 +1,151 @@
+import argparse
+
+import cobra
+import pandas as pd
+
+
+def __main__():
+    parser = argparse.ArgumentParser(
+        prog="ExpectedGrowthRate",
+        description="This program calculates the production envelope of a GEM",
+        epilog="Adding an epilog, but doubt it's needed.",
+    )
+    parser.add_argument(
+        "-m",
+        "--cb_model_location",
+        dest="cb_model_location",
+        action="store",
+        type=str,
+        default=None,
+        required=True,
+        help="The model to use."
+    )
+    parser.add_argument(
+        "-output_csv",
+        "--output_csv",
+        dest="out_file_csv",
+        action="store",
+        type=str,
+        default=None,
+        required=True,
+        help="The output csv file name."
+    )
+    parser.add_argument(
+        "-r1",
+        "--reaction1",
+        dest="reaction1",
+        action="store",
+        type=str,
+        default=None,
+        required=True,
+        help="The first reaction to scan."
+    )
+    parser.add_argument(
+        "-r2",
+        "--reaction2",
+        dest="reaction2",
+        action="store",
+        type=str,
+        default=None,
+        required=True,
+        help="The second reaction to scan."
+    )
+    parser.add_argument(
+        "-p",
+        "--points",
+        dest="points",
+        action="store",
+        type=int,
+        default=10,
+        required=False,
+        help="The number of points to scan."
+    )
+    parser.add_argument(
+        "-c",
+        "--objective",
+        dest="objective",
+        action="store",
+        type=str,
+        default=None,
+        required=False,
+        help="The reaction to use as objective."
+    )
+    parser.add_argument(
+        "-u",
+        "--uptake_constraints_file",
+        dest="uptake_constraints_file",
+        action="store",
+        type=str,
+        default=None,
+        required=False,
+        help="The file containing the uptake constraints."
+    )
+
+    args = parser.parse_args()
+    try:
+        assert len(vars(args)) == 7
+    except AssertionError:
+        raise Exception(
+            f"{len(vars(args))} arguments were received. 7 were expected."
+        )
+
+    try:
+        cb_model = cobra.io.read_sbml_model(args.cb_model_location)
+    except Exception as e:
+        raise Exception(
+            "The model could not be read. Ensure it is in correct SBML format."
+        ) from e
+
+    # set the uptake constraints if provided
+    if (args.uptake_constraints_file is not None
+            and args.uptake_constraints_file != "None"):
+        constraints_df = pd.read_csv(
+            args.uptake_constraints_file,
+            sep=";",
+            header=0,
+            index_col=False
+        )
+        for index, row in constraints_df.iterrows():
+            cb_model.reactions.get_by_id(row["reaction_id"])\
+                .lower_bound = row["lower_bound"]
+            cb_model.reactions.get_by_id(row["reaction_id"])\
+                .upper_bound = row["upper_bound"]
+
+    # get the reactions
+    reactions = [args.reaction1, args.reaction2]
+
+    # checking if reactions are in model
+    for reaction in reactions:
+        if reaction not in cb_model.reactions:
+            raise Exception(
+                f"Reaction {reaction} not found in model "
+                f"{args.cb_model_location.split('/')[-1]}"
+            )
+
+    # get the points
+    points = args.points
+    if not isinstance(points, int):
+        raise Exception("Points must be an integer")
+    if points < 1:
+        raise Exception("Must have at least one point in the phase plane")
+
+    # perform phenotype phase plane analysis
+    if args.objective is not None and args.objective != "None" \
+            and args.objective != '':
+        obj = args.objective
+    else:
+        obj = None
+
+    results = cobra.flux_analysis.phenotype_phase_plane.production_envelope(
+        model=cb_model,
+        reactions=reactions,
+        points=points,
+        objective=obj,
+    )
+
+    # save the results
+    results.to_csv(args.out_file_csv, sep=";", header=True, index=False)
+
+
+if __name__ == "__main__":
+    __main__()
b
diff -r 000000000000 -r b79cf44068bc test-data/e_coli_core_test_map.json
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/e_coli_core_test_map.json Fri Dec 13 21:32:58 2024 +0000
[
b'@@ -0,0 +1,1 @@\n+[{"map_name":"e_coli_core.Core metabolism","map_id":"0df3827fde8464e80f455a773a52c274","map_description":"E. coli core metabolic network\\nLast Modified Thu Dec 12 2024 23:15:48 GMT+0100 (Central European Standard Time)","homepage":"https://escher.github.io","schema":"https://escher.github.io/escher/jsonschema/1-0-0#"},{"reactions":{"1576693":{"name":"Phosphoglycerate kinase","bigg_id":"PGK","reversibility":true,"label_x":1065,"label_y":2715,"gene_reaction_rule":"b2926","genes":[{"bigg_id":"b2926","name":"pgk"}],"metabolites":[{"coefficient":-1,"bigg_id":"3pg_c"},{"coefficient":1,"bigg_id":"13dpg_c"},{"coefficient":-1,"bigg_id":"atp_c"},{"coefficient":1,"bigg_id":"adp_c"}],"segments":{"291":{"from_node_id":"1576835","to_node_id":"1576836","b1":null,"b2":null},"292":{"from_node_id":"1576836","to_node_id":"1576834","b1":null,"b2":null},"293":{"from_node_id":"1576485","to_node_id":"1576835","b1":{"y":2822.4341649025255,"x":1055},"b2":{"y":2789.230249470758,"x":1055}},"294":{"from_node_id":"1576486","to_node_id":"1576835","b1":{"y":2825,"x":1055},"b2":{"y":2790,"x":1055}},"295":{"from_node_id":"1576834","to_node_id":"1576487","b1":{"y":2650,"x":1055},"b2":{"y":2615,"x":1055}},"296":{"from_node_id":"1576834","to_node_id":"1576488","b1":{"y":2650.769750529242,"x":1055},"b2":{"y":2617.5658350974745,"x":1055}}}},"1576694":{"name":"Phosphogluconate dehydrogenase","bigg_id":"GND","reversibility":false,"label_x":1930.5045166015625,"label_y":1313.710205078125,"gene_reaction_rule":"b2029","genes":[{"bigg_id":"b2029","name":"gnd"}],"metabolites":[{"coefficient":1,"bigg_id":"ru5p__D_c"},{"coefficient":1,"bigg_id":"nadph_c"},{"coefficient":1,"bigg_id":"co2_c"},{"coefficient":-1,"bigg_id":"nadp_c"},{"coefficient":-1,"bigg_id":"6pgc_c"}],"segments":{"297":{"from_node_id":"1576838","to_node_id":"1576837","b1":null,"b2":null},"298":{"from_node_id":"1576837","to_node_id":"1576839","b1":null,"b2":null},"299":{"from_node_id":"1576489","to_node_id":"1576838","b1":{"y":1265,"x":1884.7984674554473},"b2":{"y":1265,"x":1921.339540236634}},"300":{"from_node_id":"1576490","to_node_id":"1576838","b1":{"y":1265,"x":1882},"b2":{"y":1265,"x":1920.5}},"301":{"from_node_id":"1576839","to_node_id":"1576491","b1":{"y":1265,"x":1992.660459763366},"b2":{"y":1265,"x":2029.2015325445527}},"302":{"from_node_id":"1576839","to_node_id":"1576492","b1":{"y":1265,"x":1996.2093727122985},"b2":{"y":1265,"x":2041.0312423743285}},"303":{"from_node_id":"1576839","to_node_id":"1576493","b1":{"y":1265,"x":2003.7},"b2":{"y":1265,"x":2066}}}},"1576695":{"name":"O2 transport  diffusion ","bigg_id":"O2t","reversibility":true,"label_x":4557.1943359375,"label_y":1635.4964599609375,"gene_reaction_rule":"s0001","genes":[{"bigg_id":"s0001","name":"None"}],"metabolites":[{"coefficient":1,"bigg_id":"o2_c"},{"coefficient":-1,"bigg_id":"o2_e"}],"segments":{"304":{"from_node_id":"1576494","to_node_id":"1576840","b1":{"y":1660,"x":4821.5},"b2":{"y":1660,"x":4755}},"305":{"from_node_id":"1576840","to_node_id":"1576495","b1":{"y":1660,"x":4610.5},"b2":{"y":1660,"x":4495}}}},"1576696":{"name":"NAD P  transhydrogenase","bigg_id":"THD2","reversibility":false,"label_x":3532.891845703125,"label_y":872.8489990234375,"gene_reaction_rule":"b1602 and b1603","genes":[{"bigg_id":"b1602","name":"pntB"},{"bigg_id":"b1603","name":"pntA"}],"metabolites":[{"coefficient":-2,"bigg_id":"h_e"},{"coefficient":-1,"bigg_id":"nadp_c"},{"coefficient":-1,"bigg_id":"nadh_c"},{"coefficient":1,"bigg_id":"nadph_c"},{"coefficient":2,"bigg_id":"h_c"},{"coefficient":1,"bigg_id":"nad_c"}],"segments":{"306":{"from_node_id":"1576842","to_node_id":"1576841","b1":null,"b2":null},"307":{"from_node_id":"1576841","to_node_id":"1576843","b1":null,"b2":null},"308":{"from_node_id":"1576496","to_node_id":"1576842","b1":{"y":580.5856339661426,"x":3331.1220703125},"b2":{"y":724.5066228070302,"x":3510}},"309":{"from_node_id":"1576497","to_node_id":"1576842","b1":{"y":654.7419446101471,"x":3418.143798828125},"b2":{"y":750.137689'..b'hate","label_x":5052.6396484375,"label_y":4477.9873046875,"node_is_primary":false},"1577085":{"node_type":"metabolite","x":4988.05859375,"y":4509.18115234375,"bigg_id":"pyr_c","name":"Pyruvate","label_x":4962.07666015625,"label_y":4488.5498046875,"node_is_primary":false},"1577086":{"node_type":"metabolite","x":4893.27099609375,"y":4529.25048828125,"bigg_id":"nad_c","name":"Nicotinamide adenine dinucleotide","label_x":4860.95166015625,"label_y":4501.22509765625,"node_is_primary":false},"1577087":{"node_type":"metabolite","x":5208.85205078125,"y":4931.947265625,"bigg_id":"coa_c","name":"Coenzyme A","label_x":5175.48828125,"label_y":4975.3330078125,"node_is_primary":false},"1577088":{"node_type":"metabolite","x":4783.69580078125,"y":4544.03759765625,"bigg_id":"oaa_c","name":"Oxaloacetate","label_x":4770.38916015625,"label_y":4522.349609375,"node_is_primary":false},"1577089":{"node_type":"metabolite","x":4700.52734375,"y":4559.88134765625,"bigg_id":"g6p_c","name":"D-Glucose 6-phosphate","label_x":4679.8271484375,"label_y":4537.1376953125,"node_is_primary":false},"1577090":{"node_type":"metabolite","x":5134.0517578125,"y":4897.1259765625,"bigg_id":"nadh_c","name":"Nicotinamide adenine dinucleotide - reduced","label_x":5078.88623046875,"label_y":4936.322265625,"node_is_primary":false},"1577091":{"node_type":"metabolite","x":5481.046875,"y":4979.51025390625,"bigg_id":"akg_c","name":"2-Oxoglutarate","label_x":5497.31591796875,"label_y":5004.2978515625,"node_is_primary":false},"1577092":{"node_type":"metabolite","x":4601.51416015625,"y":4576.78173828125,"bigg_id":"accoa_c","name":"Acetyl-CoA","label_x":4556.52001953125,"label_y":4549.81298828125,"node_is_primary":false},"1577093":{"node_type":"metabolite","x":4508.83935546875,"y":4602.1318359375,"bigg_id":"3pg_c","name":"3-Phospho-D-glycerate","label_x":4459.6201171875,"label_y":4576.21923828125,"node_is_primary":false},"1577094":{"node_type":"metabolite","x":4414.0517578125,"y":4629.59423828125,"bigg_id":"pep_c","name":"Phosphoenolpyruvate","label_x":4379.6201171875,"label_y":4607.90673828125,"node_is_primary":false},"1577095":{"node_type":"metabolite","x":4332.9951171875,"y":4650.7197265625,"bigg_id":"gln__L_c","name":"L-Glutamine","label_x":4253.14453125,"label_y":4627.97607421875,"node_is_primary":false},"1577096":{"node_type":"metabolite","x":5028.71435546875,"y":4864.818359375,"bigg_id":"nadp_c","name":"Nicotinamide adenine dinucleotide phosphate","label_x":4968.08447265625,"label_y":4902.36181640625,"node_is_primary":false},"1577097":{"node_type":"metabolite","x":4267.78271484375,"y":4682.4072265625,"bigg_id":"nadph_c","name":"Nicotinamide adenine dinucleotide phosphate - reduced","label_x":4203.775390625,"label_y":4664.94482421875,"node_is_primary":false},"1577098":{"node_type":"metabolite","x":4933.43505859375,"y":4831.37548828125,"bigg_id":"pi_c","name":"Phosphate","label_x":4884.8583984375,"label_y":4859.1142578125,"node_is_primary":false},"1577099":{"node_type":"multimarker","x":3055.6239109998846,"y":4023.6457547875243},"1577100":{"node_type":"midmarker","x":3074.275683525833,"y":4030.8645761345497},"1577101":{"node_type":"multimarker","x":3092.9274560517806,"y":4038.0833974815746},"1577102":{"node_type":"metabolite","x":3188.1170476587818,"y":3976.144484067998,"bigg_id":"h2o_c","name":"H2O","label_x":3189.1795476587818,"label_y":3930.712355161748,"node_is_primary":false},"1577103":{"node_type":"metabolite","x":3197.4642088485407,"y":4053.2845721909744,"bigg_id":"acon_C_c","name":"Cis-Aconitate","label_x":3156.1556150985407,"label_y":4098.026271409724,"node_is_primary":true},"1577104":{"node_type":"multimarker","x":3294.488060371458,"y":4035.7165277261324},"1577105":{"node_type":"midmarker","x":3312.895717745038,"y":4027.896160086635},"1577106":{"node_type":"multimarker","x":3331.3033751186185,"y":4020.0757924471372}},"text_labels":{},"canvas":{"x":7.857062530517567,"y":314.36893920898433,"width":5894.515691375733,"height":4860.457037353515}}]\n\\ No newline at end of file\n'
b
diff -r 000000000000 -r b79cf44068bc test-data/expected_single_knockout.csv
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/expected_single_knockout.csv Fri Dec 13 21:32:58 2024 +0000
b
b'@@ -0,0 +1,13016 @@\n+reaction_id;ko_gene_id_1;ko_gene_id_2;reaction;wildtype_flux;knockout_flux\n+ACALD;b1241;;acald_c + coa_c + nad_c <=> accoa_c + h_c + nadh_c;0.0;0.0\n+ACALDt;b1241;;acald_e <=> acald_c;0.0;0.0\n+ACKr;b1241;;ac_c + atp_c <=> actp_c + adp_c;3.1922147150048876e-14;0.0\n+ACONTa;b1241;;cit_c <=> acon_C_c + h2o_c;6.007249575350396;6.007249575350337\n+ACONTb;b1241;;acon_C_c + h2o_c <=> icit_c;6.007249575350396;6.007249575350338\n+ACt2r;b1241;;ac_e + h_e <=> ac_c + h_c;3.192214715004888e-14;0.0\n+ADK1;b1241;;amp_c + atp_c <=> 2.0 adp_c;0.0;0.0\n+AKGDH;b1241;;akg_c + coa_c + nad_c --> co2_c + nadh_c + succoa_c;5.064375661482158;5.064375661482093\n+AKGt2r;b1241;;akg_e + h_e <=> akg_c + h_c;0.0;0.0\n+ALCD2x;b1241;;etoh_c + nad_c <=> acald_c + h_c + nadh_c;0.0;0.0\n+ATPM;b1241;;atp_c + h2o_c --> adp_c + h_c + pi_c;8.39;8.39\n+ATPS4r;b1241;;adp_c + 4.0 h_e + pi_c <=> atp_c + h2o_c + 3.0 h_c;45.51400977451776;45.514009774517476\n+Biomass_Ecoli_core;b1241;;1.496 3pg_c + 3.7478 accoa_c + 59.81 atp_c + 0.361 e4p_c + 0.0709 f6p_c + 0.129 g3p_c + 0.205 g6p_c + 0.2557 gln__L_c + 4.9414 glu__L_c + 59.81 h2o_c + 3.547 nad_c + 13.0279 nadph_c + 1.7867 oaa_c + 0.5191 pep_c + 2.8328 pyr_c + 0.8977 r5p_c --> 59.81 adp_c + 4.1182 akg_c + 3.7478 coa_c + 59.81 h_c + 3.547 nadh_c + 13.0279 nadp_c + 59.81 pi_c;0.8739215069684279;0.8739215069684305\n+CO2t;b1241;;co2_e <=> co2_c;-22.809833310205086;-22.80983331020497\n+CS;b1241;;accoa_c + h2o_c + oaa_c --> cit_c + coa_c + h_c;6.007249575350396;6.007249575350337\n+CYTBD;b1241;;2.0 h_c + 0.5 o2_c + q8h2_c --> h2o_c + 2.0 h_e + q8_c;43.59898531199778;43.59898531199753\n+D_LACt2;b1241;;h_e + lac__D_e <=> h_c + lac__D_c;0.0;0.0\n+ENO;b1241;;2pg_c <=> h2o_c + pep_c;14.716139568742859;14.716139568742836\n+ETOHt2r;b1241;;etoh_e + h_e <=> etoh_c + h_c;0.0;0.0\n+EX_ac_e;b1241;;ac_e --> ;0.0;0.0\n+EX_acald_e;b1241;;acald_e --> ;0.0;0.0\n+EX_akg_e;b1241;;akg_e --> ;0.0;0.0\n+EX_co2_e;b1241;;co2_e <=> ;22.809833310205086;22.80983331020497\n+EX_etoh_e;b1241;;etoh_e --> ;0.0;0.0\n+EX_for_e;b1241;;for_e --> ;0.0;0.0\n+EX_fru_e;b1241;;fru_e --> ;0.0;0.0\n+EX_fum_e;b1241;;fum_e --> ;0.0;0.0\n+EX_glc__D_e;b1241;;glc__D_e <=> ;-10.0;-10.0\n+EX_gln__L_e;b1241;;gln__L_e --> ;0.0;0.0\n+EX_glu__L_e;b1241;;glu__L_e --> ;0.0;0.0\n+EX_h_e;b1241;;h_e <=> ;17.530865429786523;17.530865429786648\n+EX_h2o_e;b1241;;h2o_e <=> ;29.175827135565836;29.175827135565825\n+EX_lac__D_e;b1241;;lac__D_e --> ;0.0;0.0\n+EX_mal__L_e;b1241;;mal__L_e --> ;0.0;0.0\n+EX_nh4_e;b1241;;nh4_e <=> ;-4.765319193197444;-4.765319193197457\n+EX_o2_e;b1241;;o2_e <=> ;-21.799492655998886;-21.799492655998762\n+EX_pi_e;b1241;;pi_e <=> ;-3.214895047684769;-3.214895047684796\n+EX_pyr_e;b1241;;pyr_e --> ;0.0;0.0\n+EX_succ_e;b1241;;succ_e --> ;0.0;0.0\n+FBA;b1241;;fdp_c <=> dhap_c + g3p_c;7.477381962160304;7.477381962160286\n+FBP;b1241;;fdp_c + h2o_c --> f6p_c + pi_c;0.0;0.0\n+FORt2;b1241;;for_e + h_e --> for_c + h_c;0.0;0.0\n+FORti;b1241;;for_c --> for_e;0.0;0.0\n+FRD7;b1241;;fum_c + q8h2_c --> q8_c + succ_c;0.0;0.0\n+FRUpts2;b1241;;fru_e + pep_c --> f6p_c + pyr_c;0.0;0.0\n+FUM;b1241;;fum_c + h2o_c <=> mal__L_c;5.064375661482159;5.064375661482093\n+FUMt2_2;b1241;;fum_e + 2.0 h_e --> fum_c + 2.0 h_c;0.0;0.0\n+G6PDH2r;b1241;;g6p_c + nadp_c <=> 6pgl_c + h_c + nadph_c;4.959984944574603;4.959984944574654\n+GAPD;b1241;;g3p_c + nad_c + pi_c <=> 13dpg_c + h_c + nadh_c;16.023526143167626;16.023526143167608\n+GLCpts;b1241;;glc__D_e + pep_c --> g6p_c + pyr_c;10.000000000000002;10.000000000000002\n+GLNS;b1241;;atp_c + glu__L_c + nh4_c --> adp_c + gln__L_c + h_c + pi_c;0.22346172933182715;0.22346172933182762\n+GLNabc;b1241;;atp_c + gln__L_e + h2o_c --> adp_c + gln__L_c + h_c + pi_c;0.0;0.0\n+GLUDy;b1241;;glu__L_c + h2o_c + nadp_c <=> akg_c + h_c + nadph_c + nh4_c;-4.541857463865617;-4.54185746386563\n+GLUN;b1241;;gln__L_c + h2o_c --> glu__L_c + nh4_c;0.0;0.0\n+GLUSy;b1241;;akg_c + gln__L_c + h_c + nadph_c --> 2.0 glu__L_c + nadp_c;0.0;0.0\n+GLUt2r;b1241;;glu__L_e + h_e <=> glu__L_c + h_c;0.0;0.0\n+GND;b1241;;6pgc_c + nadp'..b'9;;fum_c + q8h2_c --> q8_c + succ_c;0.0;0.0\n+FRUpts2;b3919;;fru_e + pep_c --> f6p_c + pyr_c;0.0;0.0\n+FUM;b3919;;fum_c + h2o_c <=> mal__L_c;5.064375661482159;1.8194300213456418e-15\n+FUMt2_2;b3919;;fum_e + 2.0 h_e --> fum_c + 2.0 h_c;0.0;0.0\n+G6PDH2r;b3919;;g6p_c + nadp_c <=> 6pgl_c + h_c + nadph_c;4.959984944574603;27.899083343893903\n+GAPD;b3919;;g3p_c + nad_c + pi_c <=> 13dpg_c + h_c + nadh_c;16.023526143167626;8.828764133541728\n+GLCpts;b3919;;glc__D_e + pep_c --> g6p_c + pyr_c;10.000000000000002;10.000000000000002\n+GLNS;b3919;;atp_c + glu__L_c + nh4_c --> adp_c + gln__L_c + h_c + pi_c;0.22346172933182715;0.18002224756755264\n+GLNabc;b3919;;atp_c + gln__L_e + h2o_c --> adp_c + gln__L_c + h_c + pi_c;0.0;0.0\n+GLUDy;b3919;;glu__L_c + h2o_c + nadp_c <=> akg_c + h_c + nadph_c + nh4_c;-4.541857463865617;-3.6589504217181372\n+GLUN;b3919;;gln__L_c + h2o_c --> glu__L_c + nh4_c;0.0;0.0\n+GLUSy;b3919;;akg_c + gln__L_c + h_c + nadph_c --> 2.0 glu__L_c + nadp_c;0.0;0.0\n+GLUt2r;b3919;;glu__L_e + h_e <=> glu__L_c + h_c;0.0;0.0\n+GND;b3919;;6pgc_c + nadp_c --> co2_c + nadph_c + ru5p__D_c;4.959984944574602;27.8990833438939\n+H2Ot;b3919;;h2o_e <=> h2o_c;-29.175827135565836;-35.16784240836959\n+ICDHyr;b3919;;icit_c + nadp_c <=> akg_c + co2_c + nadph_c;6.007249575350396;0.7595854630451014\n+ICL;b3919;;icit_c --> glx_c + succ_c;0.0;0.0\n+LDH_D;b3919;;lac__D_c + nad_c <=> h_c + nadh_c + pyr_c;-2.8851667222789664e-15;0.0\n+MALS;b3919;;accoa_c + glx_c + h2o_c --> coa_c + h_c + mal__L_c;0.0;0.0\n+MALt2_2;b3919;;2.0 h_e + mal__L_e --> 2.0 h_c + mal__L_c;0.0;0.0\n+MDH;b3919;;mal__L_c + nad_c <=> h_c + nadh_c + oaa_c;5.064375661482158;1.8194300213456418e-15\n+ME1;b3919;;mal__L_c + nad_c --> co2_c + nadh_c + pyr_c;0.0;0.0\n+ME2;b3919;;mal__L_c + nadp_c --> co2_c + nadph_c + pyr_c;0.0;0.0\n+NADH16;b3919;;4.0 h_c + nadh_c + q8_c --> 3.0 h_e + nad_c + q8h2_c;38.53460965051561;58.450837099931015\n+NADTRHD;b3919;;nad_c + nadph_c --> nadh_c + nadp_c;0.0;43.72667877610217\n+NH4t;b3919;;nh4_e <=> nh4_c;4.765319193197444;3.8389726692856874\n+O2t;b3919;;o2_e <=> o2_c;21.799492655998886;29.225418549965504\n+PDH;b3919;;coa_c + nad_c + pyr_c --> accoa_c + co2_c + nadh_c;9.282532599166657;3.3981751362311448\n+PFK;b3919;;atp_c + f6p_c --> adp_c + fdp_c + h_c;7.477381962160304;-3.3456810303021106e-15\n+PFL;b3919;;coa_c + pyr_c --> accoa_c + for_c;0.0;0.0\n+PGI;b3919;;g6p_c <=> f6p_c;4.860861146496871;-18.043410918204998\n+PGK;b3919;;3pg_c + atp_c <=> 13dpg_c + adp_c;-16.02352614316763;-8.828764133541728\n+PGL;b3919;;6pgl_c + h2o_c --> 6pgc_c + h_c;4.959984944574602;27.899083343893903\n+PGM;b3919;;2pg_c <=> 3pg_c;-14.716139568742859;-7.775524859544626\n+PIt2r;b3919;;h_e + pi_e <=> h_c + pi_c;3.214895047684769;2.5899407200890217\n+PPC;b3919;;co2_c + h2o_c + pep_c --> h_c + oaa_c + pi_c;2.504309470368726;2.017488277784812\n+PPCK;b3919;;atp_c + oaa_c --> adp_c + co2_c + pep_c;0.0;0.0\n+PPS;b3919;;atp_c + h2o_c + pyr_c --> amp_c + 2.0 h_c + pep_c + pi_c;0.0;4.60742899787381\n+PTAr;b3919;;accoa_c + pi_c <=> actp_c + coa_c;-1.2124702739418495e-14;0.0\n+PYK;b3919;;adp_c + h_c + pep_c --> atp_c + pyr_c;1.758177444106819;0.0\n+PYRt2;b3919;;h_e + pyr_e <=> h_c + pyr_c;-7.59778544700626e-16;0.0\n+RPE;b3919;;ru5p__D_c <=> xu5p__D_c;2.6784818505074948;18.0933271378082\n+RPI;b3919;;r5p_c <=> ru5p__D_c;-2.2815030940671064;-9.8057562060857\n+SUCCt2_2;b3919;;2.0 h_e + succ_e --> 2.0 h_c + succ_c;0.0;0.0\n+SUCCt3;b3919;;h_e + succ_c --> h_c + succ_e;0.0;0.0\n+SUCDi;b3919;;q8_c + succ_c --> fum_c + q8h2_c;5.064375661482159;1.819430021345642e-15\n+SUCOAS;b3919;;atp_c + coa_c + succ_c <=> adp_c + pi_c + succoa_c;-5.064375661482159;-5.68756655051901e-15\n+TALA;b3919;;g3p_c + s7p_c <=> e4p_c + f6p_c;1.4969837572615485;9.173742237992654\n+THD2;b3919;;2.0 h_e + nadh_c + nadp_c --> 2.0 h_c + nad_c + nadph_c;0.0;0.0\n+TKT1;b3919;;r5p_c + xu5p__D_c <=> g3p_c + s7p_c;1.4969837572615483;9.173742237992654\n+TKT2;b3919;;e4p_c + xu5p__D_c <=> f6p_c + g3p_c;1.1814980932459462;8.919584899815547\n+TPI;b3919;;dhap_c --> g3p_c;7.477381962160304;0.0\n'
b
diff -r 000000000000 -r b79cf44068bc test-data/invalid_format.txt
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/invalid_format.txt Fri Dec 13 21:32:58 2024 +0000
b
@@ -0,0 +1,1 @@
+This is just a junk file for testing.
\ No newline at end of file
b
diff -r 000000000000 -r b79cf44068bc test-data/textbook_model_cobrapy.xml
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/textbook_model_cobrapy.xml Fri Dec 13 21:32:58 2024 +0000
b
b'@@ -0,0 +1,5467 @@\n+<?xml version="1.0" encoding="UTF-8"?>\n+<sbml xmlns="http://www.sbml.org/sbml/level3/version1/core" xmlns:fbc="http://www.sbml.org/sbml/level3/version1/fbc/version2" metaid="meta_" sboTerm="SBO:0000624" level="3" version="1" fbc:required="false">\n+  <model metaid="meta_e_coli_core" id="e_coli_core" fbc:strict="true">\n+    <listOfUnitDefinitions>\n+      <unitDefinition id="mmol_per_gDW_per_hr">\n+        <listOfUnits>\n+          <unit kind="mole" exponent="1" scale="-3" multiplier="1"/>\n+          <unit kind="gram" exponent="-1" scale="0" multiplier="1"/>\n+          <unit kind="second" exponent="-1" scale="0" multiplier="3600"/>\n+        </listOfUnits>\n+      </unitDefinition>\n+    </listOfUnitDefinitions>\n+    <listOfCompartments>\n+      <compartment id="c" name="cytosol" constant="true"/>\n+      <compartment id="e" name="extracellular" constant="true"/>\n+    </listOfCompartments>\n+    <listOfSpecies>\n+      <species metaid="meta_M_13dpg_c" id="M_13dpg_c" name="3-Phospho-D-glyceroyl phosphate" compartment="c" hasOnlySubstanceUnits="false" boundaryCondition="false" constant="false" fbc:charge="-4" fbc:chemicalFormula="C3H4O10P2">\n+        <annotation>\n+          <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:vCard="http://www.w3.org/2001/vcard-rdf/3.0#" xmlns:vCard4="http://www.w3.org/2006/vcard/ns#" xmlns:bqbiol="http://biomodels.net/biology-qualifiers/" xmlns:bqmodel="http://biomodels.net/model-qualifiers/">\n+            <rdf:Description rdf:about="#meta_M_13dpg_c">\n+              <bqbiol:is>\n+                <rdf:Bag>\n+                  <rdf:li rdf:resource="https://identifiers.org/bigg.metabolite/13dpg"/>\n+                  <rdf:li rdf:resource="https://identifiers.org/biocyc/DPG"/>\n+                  <rdf:li rdf:resource="https://identifiers.org/chebi/CHEBI:16001"/>\n+                  <rdf:li rdf:resource="https://identifiers.org/chebi/CHEBI:1658"/>\n+                  <rdf:li rdf:resource="https://identifiers.org/chebi/CHEBI:20189"/>\n+                  <rdf:li rdf:resource="https://identifiers.org/chebi/CHEBI:57604"/>\n+                  <rdf:li rdf:resource="https://identifiers.org/chebi/CHEBI:11881"/>\n+                  <rdf:li rdf:resource="https://identifiers.org/hmdb/HMDB01270"/>\n+                  <rdf:li rdf:resource="https://identifiers.org/kegg.compound/C00236"/>\n+                  <rdf:li rdf:resource="https://identifiers.org/pubchem.substance/3535"/>\n+                  <rdf:li rdf:resource="https://identifiers.org/reactome/REACT_29800"/>\n+                  <rdf:li rdf:resource="https://identifiers.org/seed.compound/cpd00203"/>\n+                  <rdf:li rdf:resource="https://identifiers.org/unipathway.compound/UPC00236"/>\n+                </rdf:Bag>\n+              </bqbiol:is>\n+            </rdf:Description>\n+          </rdf:RDF>\n+        </annotation>\n+      </species>\n+      <species metaid="meta_M_2pg_c" id="M_2pg_c" name="D-Glycerate 2-phosphate" compartment="c" hasOnlySubstanceUnits="false" boundaryCondition="false" constant="false" fbc:charge="-3" fbc:chemicalFormula="C3H4O7P">\n+        <annotation>\n+          <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:vCard="http://www.w3.org/2001/vcard-rdf/3.0#" xmlns:vCard4="http://www.w3.org/2006/vcard/ns#" xmlns:bqbiol="http://biomodels.net/biology-qualifiers/" xmlns:bqmodel="http://biomodels.net/model-qualifiers/">\n+            <rdf:Description rdf:about="#meta_M_2pg_c">\n+              <bqbiol:is>\n+                <rdf:Bag>\n+                  <rdf:li rdf:resource="https://identifiers.org/bigg.metabolite/2pg"/>\n+                  <rdf:li rdf:resource="https://identifiers.org/biocyc/2-PG"/>\n+                  <rdf:li rdf:resource="https://identifiers.org/chebi/CHEBI:1267"/>\n+                  <rdf:li rdf:resource="https://identifiers.org/chebi/CHEBI:58289"/>\n+                  <rdf:li rdf:resource="https://identifier'..b':li rdf:resource="https://identifiers.org/ncbiprotein/1652853"/>\n+                  <rdf:li rdf:resource="https://identifiers.org/ncbiprotein/1673321"/>\n+                </rdf:Bag>\n+              </bqbiol:is>\n+            </rdf:Description>\n+          </rdf:RDF>\n+        </annotation>\n+      </fbc:geneProduct>\n+      <fbc:geneProduct fbc:id="G_b0721" fbc:name="sdhC" fbc:label="G_b0721"/>\n+      <fbc:geneProduct fbc:id="G_b0723" fbc:name="sdhA" fbc:label="G_b0723"/>\n+      <fbc:geneProduct metaid="meta_G_b0728" fbc:id="G_b0728" fbc:name="sucC" fbc:label="G_b0728">\n+        <annotation>\n+          <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:vCard="http://www.w3.org/2001/vcard-rdf/3.0#" xmlns:vCard4="http://www.w3.org/2006/vcard/ns#" xmlns:bqbiol="http://biomodels.net/biology-qualifiers/" xmlns:bqmodel="http://biomodels.net/model-qualifiers/">\n+            <rdf:Description rdf:about="#meta_G_b0728">\n+              <bqbiol:is>\n+                <rdf:Bag>\n+                  <rdf:li rdf:resource="https://identifiers.org/ncbiprotein/1652018"/>\n+                </rdf:Bag>\n+              </bqbiol:is>\n+            </rdf:Description>\n+          </rdf:RDF>\n+        </annotation>\n+      </fbc:geneProduct>\n+      <fbc:geneProduct metaid="meta_G_b0729" fbc:id="G_b0729" fbc:name="sucD" fbc:label="G_b0729">\n+        <annotation>\n+          <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:vCard="http://www.w3.org/2001/vcard-rdf/3.0#" xmlns:vCard4="http://www.w3.org/2006/vcard/ns#" xmlns:bqbiol="http://biomodels.net/biology-qualifiers/" xmlns:bqmodel="http://biomodels.net/model-qualifiers/">\n+            <rdf:Description rdf:about="#meta_G_b0729">\n+              <bqbiol:is>\n+                <rdf:Bag>\n+                  <rdf:li rdf:resource="https://identifiers.org/ncbiprotein/1653466"/>\n+                </rdf:Bag>\n+              </bqbiol:is>\n+            </rdf:Description>\n+          </rdf:RDF>\n+        </annotation>\n+      </fbc:geneProduct>\n+      <fbc:geneProduct fbc:id="G_b2464" fbc:name="talA" fbc:label="G_b2464"/>\n+      <fbc:geneProduct metaid="meta_G_b0008" fbc:id="G_b0008" fbc:name="talB" fbc:label="G_b0008">\n+        <annotation>\n+          <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:vCard="http://www.w3.org/2001/vcard-rdf/3.0#" xmlns:vCard4="http://www.w3.org/2006/vcard/ns#" xmlns:bqbiol="http://biomodels.net/biology-qualifiers/" xmlns:bqmodel="http://biomodels.net/model-qualifiers/">\n+            <rdf:Description rdf:about="#meta_G_b0008">\n+              <bqbiol:is>\n+                <rdf:Bag>\n+                  <rdf:li rdf:resource="https://identifiers.org/ncbiprotein/1651885"/>\n+                </rdf:Bag>\n+              </bqbiol:is>\n+            </rdf:Description>\n+          </rdf:RDF>\n+        </annotation>\n+      </fbc:geneProduct>\n+      <fbc:geneProduct metaid="meta_G_b2935" fbc:id="G_b2935" fbc:name="tktA" fbc:label="G_b2935">\n+        <annotation>\n+          <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:vCard="http://www.w3.org/2001/vcard-rdf/3.0#" xmlns:vCard4="http://www.w3.org/2006/vcard/ns#" xmlns:bqbiol="http://biomodels.net/biology-qualifiers/" xmlns:bqmodel="http://biomodels.net/model-qualifiers/">\n+            <rdf:Description rdf:about="#meta_G_b2935">\n+              <bqbiol:is>\n+                <rdf:Bag>\n+                  <rdf:li rdf:resource="https://identifiers.org/ncbiprotein/1652388"/>\n+                </rdf:Bag>\n+              </bqbiol:is>\n+            </rdf:Description>\n+          </rdf:RDF>\n+        </annotation>\n+      </fbc:geneProduct>\n+      <fbc:geneProduct fbc:id="G_b2465" fbc:name="tktB" fbc:label="G_b2465"/>\n+      <fbc:geneProduct fbc:id="G_b3919" fbc:name="tpiA" fbc:label="G_b3919"/>\n+    </fbc:listOfGeneProducts>\n+  </model>\n+</sbml>\n'
b
diff -r 000000000000 -r b79cf44068bc test-data/textbook_model_cobrapy_exchange.csv
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/textbook_model_cobrapy_exchange.csv Fri Dec 13 21:32:58 2024 +0000
b
@@ -0,0 +1,21 @@
+reaction_id;reaction_name;reaction_stoichiometry;lower_bound;upper_bound
+EX_ac_e;Acetate exchange;ac_e --> ;0.0;1000.0
+EX_acald_e;Acetaldehyde exchange;acald_e --> ;0.0;1000.0
+EX_akg_e;2-Oxoglutarate exchange;akg_e --> ;0.0;1000.0
+EX_co2_e;CO2 exchange;co2_e <=> ;-1000.0;1000.0
+EX_etoh_e;Ethanol exchange;etoh_e --> ;0.0;1000.0
+EX_for_e;Formate exchange;for_e --> ;0.0;1000.0
+EX_fru_e;D-Fructose exchange;fru_e --> ;0.0;1000.0
+EX_fum_e;Fumarate exchange;fum_e --> ;0.0;1000.0
+EX_glc__D_e;D-Glucose exchange;glc__D_e <=> ;-10.0;1000.0
+EX_gln__L_e;L-Glutamine exchange;gln__L_e --> ;0.0;1000.0
+EX_glu__L_e;L-Glutamate exchange;glu__L_e --> ;0.0;1000.0
+EX_h_e;H+ exchange;h_e <=> ;-1000.0;1000.0
+EX_h2o_e;H2O exchange;h2o_e <=> ;-1000.0;1000.0
+EX_lac__D_e;D-lactate exchange;lac__D_e --> ;0.0;1000.0
+EX_mal__L_e;L-Malate exchange;mal__L_e --> ;0.0;1000.0
+EX_nh4_e;Ammonia exchange;nh4_e <=> ;-1000.0;1000.0
+EX_o2_e;O2 exchange;o2_e <=> ;-1000.0;1000.0
+EX_pi_e;Phosphate exchange;pi_e <=> ;-1000.0;1000.0
+EX_pyr_e;Pyruvate exchange;pyr_e --> ;0.0;1000.0
+EX_succ_e;Succinate exchange;succ_e --> ;0.0;1000.0