diff --git a/.gitignore b/.gitignore
index 567fb40..26d827d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,13 @@
uv.lock
site/
**__pycache__**
+.qodo
+.aider*
+.DS_Store
+.ipynb_checkpoints
+.claude
+.ruff_cache
+.vscode/
+.venv/
+*.egg-info/
+*.egg
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 61190cd..13dbc20 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -3,10 +3,15 @@ repos:
rev: v5.0.0
hooks:
- id: check-yaml
+ exclude: ^data/
- id: check-json
+ exclude: ^data/
- id: check-toml
+ exclude: ^data/
- id: end-of-file-fixer
+ exclude: ^data/
- id: trailing-whitespace
+ exclude: ^data/
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
@@ -14,5 +19,7 @@ repos:
hooks:
# Run the linter.
- id: ruff
+ exclude: ^data/
# Run the formatter.
- id: ruff-format
+ exclude: ^data/
diff --git a/data/FluxML-Test/antoniewicz_mininetwork.fml b/data/FluxML-Test/antoniewicz_mininetwork.fml
new file mode 100644
index 0000000..ed91571
--- /dev/null
+++ b/data/FluxML-Test/antoniewicz_mininetwork.fml
@@ -0,0 +1,114 @@
+
+
+
+ Antoniewicz test network
+ 1.0
+ 2007-10-09 00:00:00
+ Network from the EMU paper
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Eine Belegung für die Input-Pools
+
+
+
+
+
+
+ v2 >= v3; v1=100;
+
+
+ v2=0; v3=0; v5=0; v6=0; v4=0
+
+
+
+
+
+
+ F#M0,1,2,3
+
+
+
+
+ 0.0001
+ 0.8008
+ 0.1983
+ 0.0009
+
+
+
+
+
+
+
+
+
+
+ 110
+ 20
+
+
+
+
+
diff --git a/data/FluxML-Test/beispiel.mm b/data/FluxML-Test/beispiel.mm
new file mode 100644
index 0000000..328a9e1
--- /dev/null
+++ b/data/FluxML-Test/beispiel.mm
@@ -0,0 +1,91 @@
+
+
+
+
+ 2007-05-15 11:11:11
+ 1.0
+ kein Kommentar
+ gigamol/year
+ gigamol
+
+
+
+
+
+
+
+
+
+
+ 0.7*Frup#M3 + Frup#1x0x1 - 1/3*Bla#111x + 1.414213562e-7*Gamma#xxx1 - 3.1415;
+ Gonzo#1xx - 3*Kermit#xxx * MissPiggy#xxx1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (bla/flupp)=(bläh/blubb)
+ blöd/upt=upt/blubb
+
+
+
+
+ 2007-05-15 11:11:11
+ 2007-05-15 11:11:12
+ Michael
+ Drosophila coli K42
+ blabla bla flupp frupp
+
+
+ 4.321
+ 1.234
+ 1.234
+ 5.432
+ 2.345
+ 2.345
+ 6.543
+ 3.456
+ 3.456
+
+
+ 0.001
+ 0.002
+ 0.003
+
+
+ 0.11
+ 0.12
+ 0.13
+ 0.14
+
+
+ 0.15
+ 0.16
+ 0.17
+
+
+ 0.111111
+ 0.222222
+
+
+ 9.81
+ 3.14
+
+
+ 1.41
+ 2.82
+
+
diff --git a/data/FluxML-Test/beispiel_stat.mm b/data/FluxML-Test/beispiel_stat.mm
new file mode 100644
index 0000000..f651006
--- /dev/null
+++ b/data/FluxML-Test/beispiel_stat.mm
@@ -0,0 +1,83 @@
+
+
+
+
+ 2007-05-15 11:11:11
+ 1.0
+ kein Kommentar
+ gigamol/year
+ gigamol
+
+
+
+
+
+
+
+
+
+
+ 0.7*Frup#xx01x - 1/3*Bla#111x + 1.414213562e-7*Gamma#xxx1 - 3.1415;
+ Gonzo#1xx - 3*Kermit#xxx * MissPiggy#xxx1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (bla/flupp)=(bläh/blubb)
+ blöd/upt=upt/blubb
+
+
+
+
+ 2007-05-15 11:11:11
+ 2007-05-15 11:11:12
+ Michael
+ Drosophila coli K42
+ blabla bla flupp frupp
+
+
+ 4.321
+ 1.234
+ 1.234
+
+
+ 0.001
+ 0.002
+ 0.003
+
+
+ 0.11
+ 0.12
+
+
+ 0.15
+ 0.16
+ 0.17
+
+
+ 0.111111
+ 0.222222
+
+
+ 9.81
+ 3.14
+
+
+ 1.41
+ 2.82
+
+
diff --git a/data/FluxML-Test/bond_spirale.xml b/data/FluxML-Test/bond_spirale.xml
new file mode 100644
index 0000000..4d2155f
--- /dev/null
+++ b/data/FluxML-Test/bond_spirale.xml
@@ -0,0 +1,95 @@
+
+
+
+
+ Spiralbeispiel mit Bondomeren
+ 1.0
+ 2005-07-16
+ Standard-Sprialbeispiel mit Bonds statt C-Atomen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Erste Test-Konfiguration für ein Bondomer-Netzwerk
+
+
+
+
+ 10
+ 0.70.4
+ 0
+ 0
+ 0
+
+
+
+
+
diff --git a/data/FluxML-Test/bondo_vanwinden.xml b/data/FluxML-Test/bondo_vanwinden.xml
new file mode 100644
index 0000000..527d03c
--- /dev/null
+++ b/data/FluxML-Test/bondo_vanwinden.xml
@@ -0,0 +1,139 @@
+
+
+
+
+ Bondomer-Netzwerk von Winden
+ 1.0
+ 2005-07-16
+ Beispiel-Bondomer-Netzwerk aus dem van Winden-Paper
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Eine Belegung für die Input-Pools
+
+
+ 1
+
+
+
diff --git a/data/FluxML-Test/coryne+output_pools.xml b/data/FluxML-Test/coryne+output_pools.xml
new file mode 100644
index 0000000..5341fcc
--- /dev/null
+++ b/data/FluxML-Test/coryne+output_pools.xml
@@ -0,0 +1,679 @@
+
+
+
+
+
+
+
+
+
+ Standardstoffwechsel Coryne Bacterium Glutamicum
+ 1.2
+ 2005-04-15
+
+ Konvertiert aus FTBL-Datei.
+
+ Eckdaten:
+ Anzahl der Pools : 62 (1 source, 20 sinks, 41 inner)
+ Anzahl der Reaktionen : 69 (1 input, 20 output, 48 inner)
+ Anzahl der Gleichungen : 4
+ Anzahl der Ungleichungen: 0
+
+ Dimension der Matrizen auf den Cumomer-Ebenen:
+ Ebene 1: 206; ff: 0.011170; 167 SCCs: {1:159x,3:3x,4:3x,6:1x,20:1x}
+ Ebene 2: 517; ff: 0.003509; 500 SCCs: {1:493x,3:4x,4:3x}
+ Ebene 3: 872; ff: 0.001878; 867 SCCs: {1:865x,3:1x,4:1x}
+ Ebene 4: 1114; ff: 0.001377; 1114 SCCs: {1:1114x}
+ Ebene 5: 1126; ff: 0.001286; 1126 SCCs: {1:1126x}
+ Ebene 6: 891; ff: 0.001509; 891 SCCs: {1:891x}
+ Ebene 7: 532; ff: 0.002307; 532 SCCs: {1:532x}
+ Ebene 8: 229; ff: 0.004863; 229 SCCs: {1:229x}
+ Ebene 9: 67; ff: 0.015371; 67 SCCs: {1:67x}
+ Ebene 10: 12; ff: 0.083333; 12 SCCs: {1:12x}
+ Ebene 11: 1; ff: 1.000000; 1 SCCs: {1:1x}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ experimental parameter set #1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1.13
+
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0.0010
+
+ 0.2680
+ 0
+ 0
+ 0
+ 0
+ 0
+
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+
+ 10
+ 0
+
+ -0.3760
+ 0
+ 0
+
+ 0.0480
+ 0.048
+
+ 0.050
+ 0.015
+ 0
+
+ 0.5
+ 0.0480
+ 0.0480
+ 0
+
+ 0.0250
+ 0.048
+ 0.048
+
+ 0.20
+ 0
+ 0.0480
+ 0
+ 0.0480
+ 0
+ 0
+ 0
+
+ 0.0480
+ 0.008
+ 0.024
+ 0
+
+ 0
+ 0
+ 0
+ 0
+ 0.010
+ 0.034
+ 0
+ 0
+
+ 1
+
+
+
+
diff --git a/data/FluxML-Test/coryne.xml b/data/FluxML-Test/coryne.xml
new file mode 100644
index 0000000..f5b9e88
--- /dev/null
+++ b/data/FluxML-Test/coryne.xml
@@ -0,0 +1,679 @@
+
+
+
+
+
+
+
+
+
+ Standardstoffwechsel Coryne Bacterium Glutamicum
+ 1.2
+ 2005-04-15
+
+ Konvertiert aus FTBL-Datei.
+
+ Eckdaten:
+ Anzahl der Pools : 62
+ Anzahl der Reaktionen : 69
+ Anzahl der Gleichungen : 4
+ Anzahl der Ungleichungen: 0
+
+ Dimension der Matrizen auf den Cumomer-Ebenen:
+ Ebene 1: 206; ff: 0.011170; 167 SCCs: {1:159x,3:3x,4:3x,6:1x,20:1x}
+ Ebene 2: 517; ff: 0.003509; 500 SCCs: {1:493x,3:4x,4:3x}
+ Ebene 3: 872; ff: 0.001878; 867 SCCs: {1:865x,3:1x,4:1x}
+ Ebene 4: 1114; ff: 0.001377; 1114 SCCs: {1:1114x}
+ Ebene 5: 1126; ff: 0.001286; 1126 SCCs: {1:1126x}
+ Ebene 6: 891; ff: 0.001509; 891 SCCs: {1:891x}
+ Ebene 7: 532; ff: 0.002307; 532 SCCs: {1:532x}
+ Ebene 8: 229; ff: 0.004863; 229 SCCs: {1:229x}
+ Ebene 9: 67; ff: 0.015371; 67 SCCs: {1:67x}
+ Ebene 10: 12; ff: 0.083333; 12 SCCs: {1:12x}
+ Ebene 11: 1; ff: 1.000000; 1 SCCs: {1:1x}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ experimental parameter set #1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1.13
+
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0.0010
+
+ 0.2680
+ 0
+ 0
+ 0
+ 0
+ 0
+
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+
+ 10
+ 0
+
+ -0.3760
+ 0
+ 0
+
+ 0.0480
+ 0.048
+
+ 0.050
+ 0.015
+ 0
+
+ 0.5
+ 0.0480
+ 0.0480
+ 0
+
+ 0.0250
+ 0.048
+ 0.048
+
+ 0.20
+ 0
+ 0.0480
+ 0
+ 0.0480
+ 0
+ 0
+ 0
+
+ 0.0480
+ 0.008
+ 0.024
+ 0
+
+ 0
+ 0
+ 0
+ 0
+ 0.010
+ 0.034
+ 0
+ 0
+
+ 1
+
+
+
+
diff --git a/data/FluxML-Test/d4C.ftbl b/data/FluxML-Test/d4C.ftbl
new file mode 100644
index 0000000..9fd5930
--- /dev/null
+++ b/data/FluxML-Test/d4C.ftbl
@@ -0,0 +1,481 @@
+PROJECT
+ NAME VERSION FORMAT DATE COMMENT
+ AS1 Version 8 + AS 27 02 2007 NET: tca4 F, go1&go2 D, GlyOxEx&CO2out F, AceEx D, XCH: tca3&tca4 F, tca7a/7b/8a/8b C=0, go1&go2 F
+ //1. Startwerte verwendet
+NETWORK
+ FLUX_NAME EDUCT_1 EDUCT_2 PRODUCT_1 PRODUCT_2
+// Mixture Input
+ upt1 Glc1 G6P
+ #ABCDEF #ABCDEF
+ uptU GlcU G6P
+ #ABCDEF #ABCDEF
+ upt0 Glc0 G6P
+ #ABCDEF #ABCDEF
+ uptGlu Glu0 Glu
+ #ABCDE #ABCDE
+// Embden Meyerhof Pathway
+ emp1 G6P F6P
+ #ABCDEF #ABCDEF
+ emp2 F6P FBP
+ #ABCDEF #ABCDEF
+ emp3 FBP DHAP GAP
+ #ABCDEF #CBA #DEF
+ emp4 DHAP GAP
+ #ABC #ABC
+ emp5 GAP PGP
+ #ABC #ABC
+ emp6 PGP PEP
+ #ABC #ABC
+ emp7 PEP Pyr
+ #ABC #ABC
+// Pentose Phosphate Pathway
+ ppp1 G6P CO2 Ru5P
+ #ABCDEF #A #BCDEF
+ ppp2 Ru5P Xyl5P
+ #ABCDE #ABCDE
+ ppp3 Ru5P Ri5P
+ #ABCDE #ABCDE
+ ppp4 Xyl5P E4P GAP F6P
+ #ABCDE #abcd #CDE #ABabcd
+ ppp5 Xyl5P Ri5P S7P GAP
+ #ABCDE #abcde #ABabcde #CDE
+ ppp6 GAP S7P E4P F6P
+ #ABC #abcdefg #defg #abcABC
+// Tricarbonsäurezyclus
+ tca1 Pyr AcCoA CO2
+ #ABC #BC #A
+ tca2 AcCoA OAA Cit
+ #ab #ABCD #DCBAba
+ tca3 Cit ICit
+ #ABCDEF #ABCDEF
+ tca4 ICit AKG CO2
+ #ABCDEF #ABCEF #D
+ tca5 AKG SuccCoA CO2
+ #ABCDE #BCDE #A
+ tca6 SuccCoA Succ
+ #ABCD #ABCD
+ tca7a Succ Fum
+ #ABCD #ABCD
+ tca7b Succ Fum
+ #ABCD #DCBA
+ tca8a Fum MAL
+ #ABCD #ABCD
+ tca8b Fum MAL
+ #ABCD #DCBA
+ tca9 MAL OAA
+ #ABCD #ABCD
+// Glyoxylate shunt
+ go1 ICit GlyOx Succ
+ #ABCDEF #AB #CDEF
+ go2 GlyOx AcCoA MAL
+ #AB #ab #ABba
+// Anaplerose Pyr to OAA nicht in E coli
+// ana1 Pyr CO2 OAA
+// #ABC #a #ABCa
+ ana2 OAA PEP CO2
+ #ABCa #ABC #a
+ ana3 MAL CO2 Pyr
+ #ABCa #a #ABC
+// Glutamate synthesis
+ gdh Glu AKG
+ #ABCDE #ABCDE
+// GlyOx transport
+ glyOxEx GlyOx GlyOx_ex
+ #AB #AB
+// Ace transport
+ aceEx AcCoA Ace_ex
+ #AB #AB
+// CO2 output
+ coOut CO2 CO2_out
+ #A #A
+// Flux to biomass
+ G6P_bm G6P G6P_BM
+ #ABCDEF #ABCDEF
+ F6P_bm F6P F6P_BM
+ #ABCDEF #ABCDEF
+ Ri5P_bm Ri5P Ri5P_BM
+ #ABCDE #ABCDE
+ E4P_bm E4P E4P_BM
+ #ABCD #ABCD
+ GAP_bm GAP GAP_BM
+ #ABC #ABC
+// Phosphoglycerate Family
+
+// Serine synthesis
+ PGP_Ser PGP Ser
+ #ABC #ABC
+
+ PGP_Cys Ser Cys
+ #ABC #ABC
+
+// Glycine synthesis
+ PGP_Gly Ser Gly FTHF
+ #ABC #AB #C
+ FTHF_out FTHF FTHF_ex
+ #A #A
+
+ PGP_Gly_bm Gly Gly_BM
+ #AB #AB
+// further biosynthetic reactions
+ PGP_bm PGP PGP_BM
+ #ABC #ABC
+// PEP Family
+ PEP_bm PEP PEP_BM
+ #ABC #ABC
+// Pyruvate Family
+// Alanine synthesis
+ Pyr_Ala Pyr Ala
+ #ABC #ABC
+
+ Pyr_Ala_bm Ala Ala_BM
+ #ABC #ABC
+
+// Valine synthesis
+ Pyr_Val Pyr Pyr Val CO2
+ #ABC #abc #ABbcC #a
+
+ Pyr_Val_bm Val Val_BM
+ #ABCDE #ABCDE
+
+// further biosynthetic reactions
+ Pyr_bm Pyr Pyr_BM
+ #ABC #ABC
+
+ AcCoA_bm AcCoA AcCoA_BM
+ #AB #AB
+// Oxaloacetate Family
+
+// Aspartate synthesis
+ OAA_Asp OAA Glu Asp AKG
+ #ABCD #abcde #ABCD #abcde
+
+// Threonine synthesis
+ OAA_Thr Asp Thr
+ #ABCD #ABCD
+
+ OAA_Thr_bm Thr Thr_BM
+ #ABCD #ABCD
+// Asparagine synthesis
+ OAA_Asn Asp Asn
+ #ABCD #ABCD
+
+ OAA_Asn_bm Asn Asn_BM
+ #ABCD #ABCD
+
+// further biosynthetic reactions
+ OAA_bm OAA OAA_BM
+ #ABCD #ABCD
+ AKG_bm AKG AKG_BM
+ #ABCDE #ABCDE
+FLUXES
+ NET
+ NAME FCD VALUE(F/C) ED_WEIGHT LOW(F) INC(F) UP(F)
+ upt1 F 1.325
+ uptU F 1.325
+ upt0 D
+ uptGlu F 1.112
+
+ emp1 D
+ emp2 D
+ emp3 D
+ emp4 D
+ emp5 D
+ emp6 D
+ emp7 F 4 // Pk reaction must be constrained to >= Pts-flux
+
+ ppp1 F 0.9
+ ppp2 D
+ ppp3 D
+ ppp4 D
+ ppp5 D
+ ppp6 D
+
+ tca1 D
+ tca2 D //CS EA not available
+ tca3 D
+ tca4 F 0.01 //gene deletion
+ tca5 D
+ tca6 D
+ tca7a D
+ tca7b D
+ tca8a D
+ tca8b D
+ tca9 D
+
+ go1 D
+ go2 D //gene deletion
+
+ ana2 D
+ ana3 D
+
+ gdh D
+ glyOxEx F 0.01
+ aceEx D
+ coOut F 9.268
+
+// mu = 0.325 [1/h]
+ G6P_bm C 0.0679
+ F6P_bm C 0.0234
+ Ri5P_bm C 0.2922
+ E4P_bm C 0.1173
+ GAP_bm C 0.0403
+ PGP_Ser D
+ PGP_Cys C 0.0316 //6.02%
+ PGP_Gly C 0.2113 //40.28%
+ PGP_Gly_bm D
+ PGP_bm D
+ PEP_bm C 0.2519
+ Pyr_Ala C 0.1605 //16.64%
+ Pyr_Ala_bm D
+ Pyr_Val C 0.2644 //27.41%
+ Pyr_Val_bm D
+ Pyr_bm D
+ AcCoA_bm C 0.792
+ OAA_Asp D
+ OAA_Thr C 0.0678 //12.77%
+ OAA_Thr_bm D
+ OAA_Asn C 0.1289 //24.27%
+ OAA_Asn_bm D
+ OAA_bm D
+ AKG_bm C 0.4189
+
+ XCH
+ NAME FCD VALUE(F/C) ED_WEIGHT LOW(F) INC(F) UP(F)
+ upt1 D
+ uptU D
+ upt0 D
+ uptGlu D
+
+ emp1 F 0.2 //n.d.
+ emp2 C 0
+ emp3 C 0 // ! may reversible
+ emp4 F 0.8
+ emp5 F 0.8
+ emp6 F 0.8
+ emp7 C 0
+
+ ppp1 C 0
+ ppp2 F 0.8
+ ppp3 F 0.8
+ ppp4 F 0.8
+ ppp5 F 0.2
+ ppp6 F 0.2
+
+ tca1 C 0
+ tca2 C 0
+ tca3 F 0.8
+ tca4 F 0.1
+ tca5 C 0
+ tca6 C 0
+ tca7a C 0 //n.i
+ tca7b C 0 //n.i
+ tca8a C 0 //n.i
+ tca8b C 0 //n.i
+ tca9 F 0.2
+
+ go1 F 0.1
+ go2 F 0.1
+
+ ana2 F 0.1
+ ana3 F 0.1
+ gdh C 0 //n.i
+
+ PGP_Ser C 0
+ PGP_Gly C 0
+ Pyr_Ala C 0
+ Pyr_Val C 0
+ OAA_Asp C 0
+ OAA_Thr C 0
+ OAA_Asn C 0
+EQUALITIES
+ NET
+ VALUE FORMULA
+ 0 tca7a-tca7b
+ 0 tca8a-tca8b
+ 3.785 upt1+uptU+upt0
+
+ 0.5246 PGP_bm+PGP_Ser
+ 0.9646 Pyr_bm+Pyr_Ala+Pyr_Val
+ 0.5307 OAA_bm+OAA_Asp
+ XCH
+ VALUE FORMULA
+INEQUALITIES
+ NET
+ VALUE COMP FORMULA
+// Inequalities for Input and Output Fluxes are generated automatically
+ 3.785 <= emp7
+ XCH
+ VALUE COMP FORMULA
+// Inequalities for Input and Output Fluxes are generated automatically
+FLUX_MEASUREMENTS
+ FLUX_NAME VALUE DEVIATION // Val Dev
+ upt1 1.325 0.01
+ uptU 1.325 0.01
+ upt0 1.136 0.01
+ coOut 9.268 0.1
+ glyOxEx 0.01 0.01
+ aceEx 4.899 0.05
+LABEL_INPUT
+ META_NAME ISOTOPOMER VALUE
+ Glc1 #000000 0.01 // Natural marked isos !!
+ #100000 0.935 // 99% percent labeled at first position
+ #100001 0.011 // Naturally marked isos!!!
+ #100010 0.011 // Naturally marked isos!!!
+ #100100 0.011 // Naturally marked isos!!!
+ #101000 0.011 // Naturally marked isos!!!
+ #110000 0.011 // Naturally marked isos!!!
+ GlcU #000000 0.01
+ #111111 0.99 // 100% unitary marked
+ Glc0 #000000 0.934 // 100% unlabled
+ #000001 0.011 // Naturally marked isos!!!
+ #000010 0.011 // Naturally marked isos!!!
+ #000100 0.011 // Naturally marked isos!!!
+ #001000 0.011 // Naturally marked isos!!!
+ #010000 0.011 // Naturally marked isos!!!
+ #100000 0.011
+ Glu0 #00000 0.945 // 100% unlabled
+ #00001 0.011 // Naturally marked isos!!!
+ #00010 0.011 // Naturally marked isos!!!
+ #00100 0.011 // Naturally marked isos!!!
+ #01000 0.011 // Naturally marked isos!!!
+ #10000 0.011 // Naturally marked isos!!!
+LABEL_MEASUREMENTS
+ META_NAME CUM_GROUP VALUE DEVIATION CUM_CONSTRAINTS
+ FBP 1 0.241508 0.06151673 #000000
+ 1 0.290511 0.03854726 #000001+#000010+#000100+#001000+#010000+#100000
+ 1 0.055228 0.01508403 #000011+#000101+#000110+#001001+#001010+#001100+#010001+#010010+#010100+#011000+#100001+#100010+#100100+#101000+#110000
+ 1 0.123922 0.03217493 #000111+#001011+#001101+#001110+#010011+#010101+#010110+#011001+#011010+#011100+#100011+#100101+#100110+#101001+#101010+#101100+#110001+#110010+#110100+#111000
+ 1 0.062452 0.01662278 #001111+#010111+#011011+#011101+#011110+#100111+#101011+#101101+#101110+#110011+#110101+#110110+#111001+#111010+#111100
+ 1 0 0.01 #011111+#101111+#110111+#111011+#111101+#111110
+ 1 0.226376 0.05516611 #111111
+
+ DHAP 1 0.495048 0.09644128 #000
+ 1 0.167956 0.02527751 #001+#010+#100
+ 1 0 0.01 #011+#101+#110
+ 1 0.336994 0.06017525 #111
+
+ //Summe 2PG und 3PG
+ PGP 1 0.459685 0.0270704 #000
+ 1 0.191481 0.02629996 #001+#010+#100
+ 1 0.043538 0.01138608 #011+#101+#110
+ 1 0.305293 0.04328686 #111
+
+ PEP 1 0.495395 0.0407671 #000
+ 1 0.172909 0.02673597 #001+#010+#100
+ 1 0.053314 0.01045186 #011+#101+#110
+ 1 0.278379 0.01106022 #111
+
+ Pyr 1 0.446923 0.07327696 #000
+ 1 0.151454 0.024845 #001+#010+#100
+ 1 0.059507 0.0187786 #011+#101+#110
+ 1 0.342114 0.05676698 #111
+
+ //Summe Xyl5P und Ru5P
+ Ru5P 1 0.335677 0.09404718 #00000
+ 1 0.156979 0.04467475 #00001+#00010+#00100+#01000+#10000
+ 1 0.15014 0.04325079 #00011+#00101+#00110+#01001+#01010+#01100+#10001+#10010+#10100+#11000
+ 1 0.15626 0.03842368 #00111+#01011+#01101+#01110+#10011+#10101+#10110+#11001+#11010+#11100
+ 1 0.06586 0.02153168 #01111+#10111+#11011+#11101+#11110
+ 1 0.135082 0.0301896 #11111
+
+ Cit 1 0.8545 0.40050717 #000000
+ 1 0.082247 0.03246088 #000001+#000010+#000100+#001000+#010000+#100000
+ 1 0.030325 0.01101133 #000011+#000101+#000110+#001001+#001010+#001100+#010001+#010010+#010100+#011000+#100001+#100010+#100100+#101000+#110000
+ 1 0.019861 0.01097366 #000111+#001011+#001101+#001110+#010011+#010101+#010110+#011001+#011010+#011100+#100011+#100101+#100110+#101001+#101010+#101100+#110001+#110010+#110100+#111000
+ 1 0.010291 0.01021657 #001111+#010111+#011011+#011101+#011110+#100111+#101011+#101101+#101110+#110011+#110101+#110110+#111001+#111010+#111100
+ 1 0.002773 0.01109386 #011111+#101111+#110111+#111011+#111101+#111110
+ 1 0 0.01 #111111
+
+ //Aco Messdaten vorhanden, Modell muss erweitert werden
+
+ ICit 1 0.7857 0.52751813 #000000
+ 1 0.085407 0.032563 #000001+#000010+#000100+#001000+#010000+#100000
+ 1 0.040532 0.01001118 #000011+#000101+#000110+#001001+#001010+#001100+#010001+#010010+#010100+#011000+#100001+#100010+#100100+#101000+#110000
+ 1 0.055715 0.01500643 #000111+#001011+#001101+#001110+#010011+#010101+#010110+#011001+#011010+#011100+#100011+#100101+#100110+#101001+#101010+#101100+#110001+#110010+#110100+#111000
+ 1 0.017609 0.0100421 #001111+#010111+#011011+#011101+#011110+#100111+#101011+#101101+#101110+#110011+#110101+#110110+#111001+#111010+#111100
+ 1 0.015034 0.01079983 #011111+#101111+#110111+#111011+#111101+#111110
+ 1 0 0.01 #111111
+
+ AKG 1 0.943741 0.10658671 #00000
+ 1 0.048851 0.01170553 #00001+#00010+#00100+#01000+#10000
+ 1 0.007407 0.0103321 #00011+#00101+#00110+#01001+#01010+#01100+#10001+#10010+#10100+#11000
+ 1 0 0.01 #00111+#01011+#01101+#01110+#10011+#10101+#10110+#11001+#11010+#11100
+ 1 0 0.01 #01111+#10111+#11011+#11101+#11110
+ 1 0 0.01 #11111
+
+ Succ 1 0.915977 0.04282368 #0000
+ 1 0.056291 0.01061581 #0001+#0010+#0100+#1000
+ 1 0.01097 0.01007154 #0011+#0101+#0110+#1001+#1010+#1100
+ 1 0.01168 0.01020773 #0111+#1011+#1101+#1110
+ 1 0.005079 0.01003944 #1111
+
+ MAL 1 0.538158 0.05872102 #0000
+ 1 0.205941 0.03461291 #0001+#0010+#0100+#1000
+ 1 0.068843 0.01367785 #0011+#0101+#0110+#1001+#1010+#1100
+ 1 0.131688 0.01830739 #0111+#1011+#1101+#1110
+ 1 0.055368 0.01419667 #1111
+
+ Asp 1 0.46298 0.01651 #0000
+ 1 0.11725 0.01049 #0100+#1000
+ 1 0.10965 0.01164 #0001+#0010
+ 1 0.02033 0.01002 #1100
+ 1 0.04127 0.01005 #0101+#0110+#1001+#1010
+ 1 0.02048 0.01003 #0011
+ 1 0.0731 0.01025 #1101+#1110
+ 1 0.08314 0.01052 #0111+#1011
+ 1 0.07181 0.01039 #1111
+ Glu 1 0.90366 0.03078 #00000
+ 1 0.01848 0.01002 #00001+#00010+#00100+#01000
+ 1 0.07178 0.01033 #10000
+ 1 0.00109 0.01 #00011+#00101+#00110+#01001+#01010+#01100
+ 1 0.00474 0.01 #10001+#10010+#10100+#11000
+ 1 0.00007 0.01 #00111+#01011+#01101+#01110
+ 1 0.00018 0.01 #10011+#10101+#10110+#11001+#11010+#11100
+ 1 0 0.01 #01111
+ 1 0 0.01 #10111+#11011+#11101+#11110
+ 1 0 0.01 #11111
+ Ala 1 0.46798 0.0538 #000
+ 1 0.04221 0.01016 #001+#010
+ 1 0.14241 0.01707 #100
+ 1 0.02499 0.0102 #011
+ 1 0 0.01 #101+#110
+ 1 0.32241 0.03936 #111
+ Val 1 0.30091 0.05058 #00000
+ 1 0.02468 0.01115 #00001+#00010+#00100+#01000
+ 1 0.14034 0.02765 #10000
+ 1 0.0169 0.01066 #00011+#00101+#00110+#01001+#01010+#01100
+ 1 0.19884 0.04304 #10001+#10010+#10100+#11000
+ 1 0.14478 0.03369 #00111+#01011+#01101+#01110
+ 1 0.04199 0.01396 #10011+#10101+#10110+#11001+#11010+#11100
+ 1 0.04082 0.01354 #01111
+ 1 0.01227 0.01086 #10111+#11011+#11101+#11110
+ 1 0.07847 0.01927 #11111
+ Thr 1 0.7501 0.0311 #0000
+ 1 0.0308 0.01035 #0001+#0010+#0100
+ 1 0.07037 0.01334 #1000
+ 1 0.01457 0.01038 #0011+#0101+#0110
+ 1 0.02235 0.0104 #1001+#1010+#1100
+ 1 0.04279 0.01193 #0111
+ 1 0.0352 0.01086 #1011+#1101+#1110
+ 1 0.03382 0.01048 #1111
+ Asn 1 0.8107 0.06762 #0000
+ 1 0 0.01 #0001+#0010+#0100
+ 1 0 0.01 #1000
+ 1 0 0.01 #0011+#0101+#0110
+ 1 0 0.01 #1001+#1010+#1100
+ 1 0.11527 0.01467 #0111
+ 1 0 0.01 #1011+#1101+#1110
+ 1 0.07403 0.01985 #1111
+ Ser 1 0.55777 0.07548 #000
+ 1 0.0226 0.01055 #001+#010
+ 1 0.17596 0.04615 #100
+ 1 0.08054 0.02396 #011
+ 1 0.02258 0.0111 #101+#110
+ 1 0.14055 0.03243 #111
+
+PEAK_MEASUREMENTS
+ META_NAME PEAK_NO VALUE_S VALUE_D- VALUE_D+ VALUE_DD VALUE_T DEVIATION_S DEVIATION_D- DEVIATION_D+ DEVIATION_DD/T
+MASS_SPECTROMETRY
+ META_NAME FRAGMENT WEIGHT VALUE DEVIATION
+OPTIONS
+ OPT_NAME OPT_VALUE
diff --git a/data/FluxML-Test/emu_testnetwork.fml b/data/FluxML-Test/emu_testnetwork.fml
new file mode 100644
index 0000000..2a963b9
--- /dev/null
+++ b/data/FluxML-Test/emu_testnetwork.fml
@@ -0,0 +1,144 @@
+
+
+
+ Antoniewicz test network
+ 1.0
+ 2007-10-09 00:00:00
+ Network from the EMU paper
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Eine Belegung für die Input-Pools
+
+
+
+
+
+
+
+
+
+
+
+
+ v5=0; v6=0; v4=0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+ 1
+ 1
+ 1
+ 1
+
+
+ 1
+ 1
+ 1
+ 1
+
+
+
+
+
+
+
+
+
+ 100
+ 20
+ 50
+
+
+
+
+
+
diff --git a/data/FluxML-Test/fluxml-model/models/ecoli_model_level_1.fml b/data/FluxML-Test/fluxml-model/models/ecoli_model_level_1.fml
new file mode 100644
index 0000000..e340cdc
--- /dev/null
+++ b/data/FluxML-Test/fluxml-model/models/ecoli_model_level_1.fml
@@ -0,0 +1,1277 @@
+
+
+ 2017-10-06 14:05:06
+ modeler: Martin Cerff, Salah Azzouzi, Martin Beyss, Katharina Noeh
+ date: 06.10.2017
+ model adapted from: https://doi.org/10.1016/j.ymben.2015.01.001
+ properties: STAT_MFA, abs. fluxes
+
+
+
+
+
+ InChI=1S/C6H13O9P/c7-3-2(1-14-16(11,12)13)15-6(10)5(9)4(3)8/h2-10H,1H2,(H2,11,12,13)/t2-,3-,4+,5-,6+/m1/s1
+
+
+ InChI=1S/C6H13O9P/c7-2-6(10)5(9)4(8)3(15-6)1-14-16(11,12)13/h3-5,7-10H,1-2H2,(H2,11,12,13)/t3-,4-,5+,6?/m1/s1
+
+
+ InChI=1S/C3H7O6P/c4-1-3(5)2-9-10(6,7)8/h1,3,5H,2H2,(H2,6,7,8)/t3-/m0/s1
+
+
+ 3PG
+ InChI=1S/C3H7O7P/c4-2(3(5)6)1-10-11(7,8)9/h2,4H,1H2,(H,5,6)(H2,7,8,9)/t2-/m1/s1
+
+
+ InChI=1S/C3H5O6P/c1-2(3(4)5)9-10(6,7)8/h1H2,(H,4,5)(H2,6,7,8)
+
+
+ InChI=1S/C3H4O3/c1-2(4)3(5)6/h1H3,(H,5,6)
+
+
+ InChI=1S/C23H38N7O17P3S/c1-12(31)51-7-6-25-14(32)4-5-26-21(35)18(34)23(2,3)9-44-50(41,42)47-49(39,40)43-8-13-17(46-48(36,37)38)16(33)22(45-13)30-11-29-15-19(24)27-10-28-20(15)30/h10-11,13,16-18,22,33-34H,4-9H2,1-3H3,(H,25,32)(H,26,35)(H,39,40)(H,41,42)(H2,24,27,28)(H2,36,37,38)/t13-,16-,17-,18+,22-/m1/s1
+ 0,11
+
+
+ InChI=1S/C4H4O5/c5-2(4(8)9)1-3(6)7/h1H2,(H,6,7)(H,8,9)
+
+
+ InChI=1S/C6H8O7/c7-3(8)1-6(13,5(11)12)2-4(9)10/h13H,1-2H2,(H,7,8)(H,9,10)(H,11,12)
+
+
+ InChI=1S/C6H8O7/c7-3(8)1-2(5(10)11)4(9)6(12)13/h2,4,9H,1H2,(H,7,8)(H,10,11)(H,12,13)
+
+
+ InChI=1S/C4H6O4/c5-3(6)1-2-4(7)8/h1-2H2,(H,5,6)(H,7,8)
+
+
+ InChI=1S/C4H4O4/c5-3(6)1-2-4(7)8/h1-2H,(H,5,6)(H,7,8)/b2-1+
+
+
+ InChI=1S/C4H6O5/c5-2(4(8)9)1-3(6)7/h2,5H,1H2,(H,6,7)(H,8,9)/t2-/m0/s1
+
+
+ InChI=1S/C5H11O8P/c6-1-3(7)5(9)4(8)2-13-14(10,11)12/h4-6,8-9H,1-2H2,(H2,10,11,12)/t4-,5+/m1/s1
+
+
+ InChI=1S/C5H11O8P/c6-1-3(7)5(9)4(8)2-13-14(10,11)12/h4-6,8-9H,1-2H2,(H2,10,11,12)/t4-,5-/m1/s1
+
+
+ InChI=1S/C5H11O8P/c6-3-2(1-12-14(9,10)11)13-5(8)4(3)7/h2-8H,1H2,(H2,9,10,11)/t2-,3-,4-,5?/m1/s1
+
+
+ InChI=1S/C7H15O10P/c8-1-3(9)5(11)7(13)6(12)4(10)2-17-18(14,15)16/h4-8,10-13H,1-2H2,(H2,14,15,16)/t4-,5-,6-,7+/m1/s1
+
+
+ InChI=1S/C4H9O7P/c5-1-3(6)4(7)2-11-12(8,9)10/h1,3-4,6-7H,2H2,(H2,8,9,10)/t3-,4+/m0/s1
+
+
+ InChI=1S/C6H14O12P2/c7-4-3(1-16-19(10,11)12)18-6(9,5(4)8)2-17-20(13,14)15/h3-5,7-9H,1-2H2,(H2,10,11,12)(H2,13,14,15)/t3-,4-,5+,6?/m1/s1
+
+
+ InChI=1S/C5H6O5/c6-3(5(9)10)1-2-4(7)8/h1-2H2,(H,7,8)(H,9,10)
+
+
+ InChI=1S/C5H9NO4/c6-3(5(9)10)1-2-4(7)8/h3H,1-2,6H2,(H,7,8)(H,9,10)/t3-/m0/s1
+
+
+ InChI=1S/C5H10N2O3/c6-3(5(9)10)1-2-4(7)8/h3H,1-2,6H2,(H2,7,8)(H,9,10)/t3-/m0/s1
+
+
+ InChI=1S/C5H9NO2/c7-5(8)4-2-1-3-6-4/h4,6H,1-3H2,(H,7,8)/t4-/m0/s1
+
+
+ InChI=1S/C4H7NO4/c5-2(4(8)9)1-3(6)7/h2H,1,5H2,(H,6,7)(H,8,9)/t2-/m0/s1
+
+
+ InChI=1S/C4H8N2O3/c5-2(4(8)9)1-3(6)7/h2H,1,5H2,(H2,6,7)(H,8,9)/t2-/m0/s1
+
+
+ InChI=1S/C4H9NO3/c1-2(6)3(5)4(7)8/h2-3,6H,5H2,1H3,(H,7,8)/t2-,3+/m1/s1
+
+
+ InChI=1S/C6H13NO2/c1-3-4(2)5(7)6(8)9/h4-5H,3,7H2,1-2H3,(H,8,9)/t4-,5-/m0/s1
+
+
+ InChI=1S/C5H11NO2S/c1-9-3-2-4(6)5(7)8/h4H,2-3,6H2,1H3,(H,7,8)/t4-/m0/s1
+
+
+ InChI=1S/C3H7O6P/c4-1-3(5)2-9-10(6,7)8/h4H,1-2H2,(H2,6,7,8)
+
+
+ InChI=1S/C3H7NO3/c4-2(1-5)3(6)7/h2,5H,1,4H2,(H,6,7)/t2-/m0/s1
+
+
+ InChI=1S/C9H11NO3/c10-8(9(12)13)5-6-1-3-7(11)4-2-6/h1-4,8,11H,5,10H2,(H,12,13)/t8-/m0/s1
+
+
+ InChI=1S/C9H11NO2/c10-8(9(11)12)6-7-4-2-1-3-5-7/h1-5,8H,6,10H2,(H,11,12)/t8-/m0/s1
+
+
+ InChI=1S/CO2/c2-1-3
+
+
+ InChI=1S/C2H5NO2/c3-1-2(4)5/h1,3H2,(H,4,5)
+
+
+ BM.ext
+
+
+
+
+ InChI=1S/C6H14N2O2/c7-4-2-1-3-5(8)6(9)10/h5H,1-4,7-8H2,(H,9,10)/t5-/m0/s1
+
+
+
+ Gluc.ext
+ InChI=1S/C6H12O6/c7-1-2-3(8)4(9)5(10)6(11)12-2/h2-11H,1H2/t2-,3-,4+,5-,6?/m1/s1
+
+
+
+ 6PG
+ InChI=1S/C6H13O10P/c7-2(1-16-17(13,14)15)3(8)4(9)5(10)6(11)12/h2-5,7-10H,1H2,(H,11,12)(H2,13,14,15)
+
+
+
+ TK-C2
+
+
+ TA-C3
+
+
+ InChI=1S/C6H11O9P/c7-3(1-4(8)6(10)11)5(9)2-15-16(12,13)14/h3,5,7,9H,1-2H2,(H,10,11)(H2,12,13,14)
+
+
+ InChI=1S/C25H40N7O19P3S/c1-25(2,20(38)23(39)28-6-5-14(33)27-7-8-55-16(36)4-3-15(34)35)10-48-54(45,46)51-53(43,44)47-9-13-19(50-52(40,41)42)18(37)24(49-13)32-12-31-17-21(26)29-11-30-22(17)32/h11-13,18-20,24,37-38H,3-10H2,1-2H3,(H,27,33)(H,28,39)(H,34,35)(H,43,44)(H,45,46)(H2,26,29,30)(H2,40,41,42)
+ 2,3,14,15
+
+
+
+ InChI=1S/C2H2O3/c3-1-2(4)5/h1H,(H,4,5)/p-1
+
+
+ InChI=1S/C2H4O2/c1-2(3)4/h1H3,(H,3,4)
+
+
+
+ InChI=1S/C3H7NO2S/c4-2(1-7)3(5)6/h2,7H,1,4H2,(H,5,6)
+
+
+
+ InChI=1S/C5H11NO2/c1-3(2)4(6)5(7)8/h3-4H,6H2,1-2H3,(H,7,8)
+
+
+ InChI=1S/C3H7NO2/c1-2(4)3(5)6/h2H,4H2,1H3,(H,5,6)
+
+
+ InChI=1S/C6H14N4O2/c7-4(5(11)12)2-1-3-10-6(8)9/h4H,1-3,7H2,(H,11,12)(H4,8,9,10)
+
+
+
+ InChI=1S/C11H12N2O2/c12-9(11(14)15)5-7-6-13-10-4-2-1-3-8(7)10/h1-4,6,9,13H,5,12H2,(H,14,15)
+
+
+ InChI=1S/C6H9N3O2/c7-5(6(10)11)1-4-2-8-3-9-4/h2-3,5H,1,7H2,(H,8,9)(H,10,11)
+
+
+ LL-DAP
+ InChI=1S/C7H14N2O4/c8-4(6(10)11)2-1-3-5(9)7(12)13/h4-5H,1-3,8-9H2,(H,10,11)(H,12,13)
+
+
+
+ InChI=1S/C6H13NO2/c1-4(2)3-5(7)6(8)9/h4-5H,3,7H2,1-2H3,(H,8,9)
+
+
+
+
+
+
+ O2.ext
+
+
+ NH3.ext
+
+
+ SO4.ext
+
+
+ NADH.ext
+
+
+
+
+ Glycolysis
+
+
+
+
+ Glycolysis
+
+
+
+
+ Glycolysis
+
+
+
+
+
+ Glycolysis
+
+
+
+
+
+ Glycolysis
+
+
+
+
+ Glycolysis
+
+
+
+
+
+
+ Glycolysis
+
+
+
+
+ Glycolysis
+
+
+
+
+
+ TCA
+
+
+
+
+
+
+ TCA
+
+
+
+
+
+ TCA
+
+
+
+
+
+
+ TCA
+
+
+
+
+
+ TCA
+
+
+
+
+
+ TCA
+
+
+
+
+
+ TCA
+
+
+
+
+
+ TCA
+
+
+
+
+ TCA
+
+
+
+
+ TCA
+
+
+
+
+
+ PPP
+
+
+
+
+
+ PPP
+
+
+
+
+ PPP
+
+
+
+
+ Amphibolic R.
+
+
+
+
+
+ Amphibolic R.
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ UPT
+
+
+
+
+ PPP
+
+
+
+
+
+
+ PPP
+
+
+
+
+
+ PPP
+
+
+
+
+
+ PPP
+
+
+
+
+
+ Biomass
+
+
+
+ PPP
+
+
+
+
+
+ PPP
+
+
+
+
+
+ EDP
+
+
+
+
+ EDP
+
+
+
+
+
+ TCA
+
+
+
+
+ TCA
+
+
+
+
+
+
+ Glyox
+
+
+
+
+
+ Glyox
+
+
+
+
+
+ Glyox
+
+
+
+
+
+ Amphibolic R.
+
+
+
+
+
+
+ Amphibolic R.
+
+
+
+
+
+
+ Acetic Acid Formation
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Biomass
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+ Biomass
+
+
+
+ One Carbon Metabolism
+
+
+
+
+
+ One Carbon Metabolism
+
+
+
+
+
+ Oxidative Phosphorylation
+
+
+
+
+
+
+
+ Oxidative Phosphorylation
+
+
+
+
+
+
+
+
+
+ Transport
+
+
+
+
+ Transport
+
+
+
+ ATP Hydrolysis
+
+
+
+ Transport
+
+
+
+ CO2 Exchange
+
+
+
+
+
+
+ Transport
+
+
+
+
+ Transhydrogenation
+
+
+
+
+ Transport
+
+
+
+
+ Biomass
+
+
+
+ CO2 Exchange
+
+
+
+
+ CO2 Exchange
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+
+ Biomass
+
+
+
+
+ Biomass
+
+
+
+
+
+ AA15_v51___1/0.5=AA15_v51___2/0.5;
+ TCA6_v25___1/0.5=TCA6_v25___2/0.5;
+ GOX1_v29___1/0.5=GOX1_v29___2/0.5;
+ TCA7_v26___1/0.5=TCA7_v26___2/0.5;
+ TCA8_v27___1/0.5=TCA8_v27___2/0.5;
+ AA12_v48___1/0.25=AA12_v48___2/0.25;
+ AA12_v48___2/0.25=AA12_v48___3/0.25;
+ AA12_v48___3/0.25=AA12_v48___4/0.25;
+ AA13_v49___1/0.5=AA13_v49___2/0.5
+
+
+ TCA6_v25___1/0.5=TCA6_v25___2/0.5;
+ GOX1_v29___1/0.5=GOX1_v29___2/0.5;
+ TCA7_v26___1/0.5=TCA7_v26___2/0.5;
+ TCA8_v27___1/0.5=TCA8_v27___2/0.5
+
+
+
+ Substrate see Crown 2015
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0=488*mu2_v70___1-Ala_bm___1;
+ 0=281*mu2_v70___1-Arg_bm___1;
+ 0=229*mu2_v70___1-Asn_bm___1;
+ 0=229*mu2_v70___1-Asp_bm___1;
+ 0=87*mu2_v70___1-Cys_bm___1;
+ 0=250*mu2_v70___1-Glu_bm___1;
+ 0=250*mu2_v70___1-Gln_bm___1;
+ 0=582*mu2_v70___1-Gly_bm___1;
+ 0=90*mu2_v70___1-His_bm___1;
+ 0=276*mu2_v70___1-Ile_bm___1;
+ 0=428*mu2_v70___1-Leu_bm___1;
+ 0=326*mu2_v70___1-Lys_bm___1;
+ 0=146*mu2_v70___1-Met_bm___1;
+ 0=176*mu2_v70___1-Phe_bm___1;
+ 0=210*mu2_v70___1-Pro_bm___1;
+ 0=205*mu2_v70___1-Ser_bm___1;
+ 0=241*mu2_v70___1-Thr_bm___1;
+ 0=54*mu2_v70___1-Trp_bm___1;
+ 0=131*mu2_v70___1-Tyr_bm___1;
+ 0=402*mu2_v70___1-Val_bm___1;
+ 0=205*mu2_v70___1-G6P_bm___1;
+ 0=71*mu2_v70___1-F6P_bm___1;
+ 0=754*mu2_v70___1-R5P_bm___1;
+ 0=129*mu2_v70___1-GAP_bm___1;
+ 0=619*mu2_v70___1-_3PG_bm___1;
+ 0=51*mu2_v70___1-PEP_bm___1;
+ 0=83*mu2_v70___1-Pyr_bm___1;
+ 0=2510*mu2_v70___1-AcCoA_bm___1;
+ 0=87*mu2_v70___1-AKG_bm___1;
+ 0=340*mu2_v70___1-OAC_bm___1;
+ 0=443*mu2_v70___1-MEETHF_bm___1;
+ 0=33247*mu2_v70___1-ATP_bm___1;
+ 0=5363*mu2_v70___1-NADPH_bm___1;
+ 0=1455*mu2_v70___1-NADH_bm___1;
+ AC1_v35___1<=6542.235;
+ AC1_v35___1>=4792.4618;
+ ANA1_v31___1<=878.533;
+ ANA1_v31___1>=6.14313E-7;
+ ANA3_v33___1<=3896.9782;
+ ANA3_v33___1>=1426.2412;
+ ATPH1_v64___1<=55487.63;
+ ATPH1_v64___1>=14468.296;
+ EMP8_v8<=3347.7908;
+ EMP8_v8>=1101.9954;
+ Glc_upt___1<=8616.77;
+ Glc_upt___1>=8583.1612;
+ TCA1_v20<=10551.942;
+ TCA1_v20>=7831.0998;
+ TCA9_v28___1<=2835.119;
+ TCA9_v28___1>=0;
+ TH1_v63___1<=10918.302;
+ TH1_v63___1>=1462.7568;
+ mu2_v70___1<=0.77191708;
+ mu2_v70___1>=0.55134342;
+ GAS2_v71___1>=8.6E-7;
+ EMP1_v1=Glc_upt___1
+
+
+ AA8_v44___1>=8.6E-9;
+ AA8_v44___1<=1077.6918;
+ GOX1_v29___1+GOX1_v29___2>=8.6E-7;
+ GOX1_v29___1+GOX1_v29___2<=3385.7598;
+ PPP6_v14___1<=1868.0834;
+ AA9_v45___1>=8.59999E-7;
+ AC1_v35___1>=8.6E-7;
+ EMP2_v2>=8.6E-7;
+ EMP4_v4>=8.6E-7;
+ EMP5_v5>=8.6E-7;
+ EMP6_v6>=8.6E-7;
+ EMP7_v7>=8.6E-7;
+ PPP3_v11___1>=8.6E-7;
+ PPP4_v12___1>=8.6E-7;
+ PPP5_v13___1>=8.6E-7;
+ PPP6_v14___1>=8.6E-7;
+ PPP7_v15___1>=8.6E-7;
+ PPP8_v16___1>=4.26035E-9;
+ PPP9_v17___1>=8.6E-7;
+ TCA3_v22___1>=8.6E-7;
+ TCA4_v23>=86000;
+ TCA6_v25___1+TCA6_v25___2>=8.6E-7;
+ TCA7_v26___1+TCA7_v26___2>=8.6E-7;
+ TCA8_v27___1+TCA8_v27___2>=1935.0516;
+ TCA9_v28___1>=2082.1976;
+ TH1_v63___1>=8.6E-7
+
+
+
+
+ Ala[1-2]#M0,1,2
+
+
+ Ala#M0,1,2,3
+
+
+ Asp[1-3]#M0,1,2,3
+
+
+ Asp#M0,1,2,3,4
+
+
+ Glu#M0,1,2,3,4,5
+
+
+ Tyr[8-9]#M0,1,2
+
+
+ Gly[1]#M0,1
+
+
+ Val[1-4]#M0,1,2,3,4
+
+
+ Val#M0,1,2,3,4,5
+
+
+ Leu[1-5]#M0,1,2,3,4,5
+
+
+ Ile[1-5]#M0,1,2,3,4,5
+
+
+ Ser[1-2]#M0,1,2
+
+
+ Phe[8-9]#M0,1,2
+
+
+ Phe[1-8]#M0,1,2,3,4,5,6,7,8
+
+
+
+
+ AC1_v35___1
+
+
+
+
+ .531082077660435
+ .08855144889523314
+ .3803664734443319
+
+
+ .507071221548345
+ .08876798898076851
+ .3968503503394545
+ .007310439131431961
+
+
+ .4161655933759446
+ .19706273672066257
+ .32299783628753814
+ .06377383361585462
+
+
+ .3682894507838331
+ .1985113107865648
+ .3175416876809832
+ .10531726211398243
+ .010340288634636622
+
+
+ .2195579305437726
+ .15182087697369892
+ .34201857809968894
+ .1410647815293161
+ .12274363745570406
+ .02279419539781947
+
+
+ .54415210704985
+ .43882668855417656
+ .01702120439597351
+
+
+ .5767490939700963
+ .42325090602990373
+
+
+ .2820481732121244
+ .09405617491824442
+ .41185299307982204
+ .06736400466933157
+ .14467865412047765
+
+
+ .26929643786170976
+ .09204497937882693
+ .41149353494376567
+ .07278848364303081
+ .15159591822091464
+ .0027806459517522196
+
+
+ .16549986668171174
+ .1717384686657797
+ .28053258580446955
+ .20971418464167835
+ .11273059588311199
+ .05978429832324863
+
+
+ .22101808798088443
+ .1415085539208451
+ .34728399195656123
+ .13742712466409304
+ .12850481328712798
+ .024257428190488203
+
+
+ .5348947486379718
+ .08166566410561094
+ .38343958725641725
+
+
+ .54415210704985
+ .43882668855417656
+ .01702120439597351
+
+
+ .0709745338412866
+ .15715430102507827
+ .2124309015894156
+ .24555557200360117
+ .1680255212172203
+ .10198116319293168
+ .037407429940670464
+ .006305910202174005
+ .00016466698762175025
+
+
+ 5658.8
+
+
+
+
+ 1005.856
+ 0.6966
+ 5658.8
+ 2287.6
+ 9133.2
+ 6.1431348E-7
+ 2373.6
+ 7129.400000000001
+ 1126.6
+ 27571.600000000002
+ 8600.0
+ 72445.712
+ 14103.054
+ 34585.932
+ 2383.3352
+ 18.741206000000002
+ 5532.294
+ 65867.916
+ 45314.776000000005
+ 265.31086
+ 0.0011562786
+ 4.2611194E-9
+ 47755.799999999996
+ 40030.506
+ 86086.0
+ 16073.4
+ 3044.4
+ 10251.2
+ 16463.667999999998
+ 22.554403
+ 29330.558
+ 8.600000000000001E-9
+ 5939.572800000001
+ 26218.046
+
+
+
+
diff --git a/data/FluxML-Test/fluxml-model/models/ecoli_model_level_2.fml b/data/FluxML-Test/fluxml-model/models/ecoli_model_level_2.fml
new file mode 100644
index 0000000..6209ce7
--- /dev/null
+++ b/data/FluxML-Test/fluxml-model/models/ecoli_model_level_2.fml
@@ -0,0 +1,2345 @@
+
+
+ 2017-10-16 13:26:14
+ modeler: Martin Cerff, Salah Azzouzi, , Martin Beyss, Katharina Noeh
+ date: 16.10.2017
+ model adapted from: https://doi.org/10.1016/j.ymben.2015.01.001
+ properties: INST_MFA, abs. fluxes
+ flux units: mumol/g/min 3 min time range for Chemostat
+
+
+
+
+
+ InChI=1S/C6H13O9P/c7-3-2(1-14-16(11,12)13)15-6(10)5(9)4(3)8/h2-10H,1H2,(H2,11,12,13)/t2-,3-,4+,5-,6+/m1/s1
+
+
+ InChI=1S/C6H13O9P/c7-2-6(10)5(9)4(8)3(15-6)1-14-16(11,12)13/h3-5,7-10H,1-2H2,(H2,11,12,13)/t3-,4-,5+,6?/m1/s1
+
+
+ InChI=1S/C3H7O6P/c4-1-3(5)2-9-10(6,7)8/h1,3,5H,2H2,(H2,6,7,8)/t3-/m0/s1
+
+
+ InChI=1S/C3H7O7P/c4-2(3(5)6)1-10-11(7,8)9/h2,4H,1H2,(H,5,6)(H2,7,8,9)/t2-/m1/s1
+
+
+ InChI=1S/C3H5O6P/c1-2(3(4)5)9-10(6,7)8/h1H2,(H,4,5)(H2,6,7,8)
+
+
+ InChI=1S/C3H4O3/c1-2(4)3(5)6/h1H3,(H,5,6)
+
+
+ InChI=1S/C23H38N7O17P3S/c1-12(31)51-7-6-25-14(32)4-5-26-21(35)18(34)23(2,3)9-44-50(41,42)47-49(39,40)43-8-13-17(46-48(36,37)38)16(33)22(45-13)30-11-29-15-19(24)27-10-28-20(15)30/h10-11,13,16-18,22,33-34H,4-9H2,1-3H3,(H,25,32)(H,26,35)(H,39,40)(H,41,42)(H2,24,27,28)(H2,36,37,38)/t13-,16-,17-,18+,22-/m1/s1
+ 0,11
+
+
+ InChI=1S/C4H4O5/c5-2(4(8)9)1-3(6)7/h1H2,(H,6,7)(H,8,9)
+
+
+ InChI=1S/C6H8O7/c7-3(8)1-6(13,5(11)12)2-4(9)10/h13H,1-2H2,(H,7,8)(H,9,10)(H,11,12)
+
+
+ InChI=1S/C6H8O7/c7-3(8)1-2(5(10)11)4(9)6(12)13/h2,4,9H,1H2,(H,7,8)(H,10,11)(H,12,13)
+
+
+ InChI=1S/C4H6O4/c5-3(6)1-2-4(7)8/h1-2H2,(H,5,6)(H,7,8)
+
+
+ InChI=1S/C4H4O4/c5-3(6)1-2-4(7)8/h1-2H,(H,5,6)(H,7,8)/b2-1+
+
+
+ InChI=1S/C4H6O5/c5-2(4(8)9)1-3(6)7/h2,5H,1H2,(H,6,7)(H,8,9)/t2-/m0/s1
+
+
+ InChI=1S/C5H11O8P/c6-1-3(7)5(9)4(8)2-13-14(10,11)12/h4-6,8-9H,1-2H2,(H2,10,11,12)/t4-,5+/m1/s1
+
+
+ InChI=1S/C5H11O8P/c6-1-3(7)5(9)4(8)2-13-14(10,11)12/h4-6,8-9H,1-2H2,(H2,10,11,12)/t4-,5-/m1/s1
+
+
+ InChI=1S/C5H11O8P/c6-3-2(1-12-14(9,10)11)13-5(8)4(3)7/h2-8H,1H2,(H2,9,10,11)/t2-,3-,4-,5?/m1/s1
+
+
+ InChI=1S/C7H15O10P/c8-1-3(9)5(11)7(13)6(12)4(10)2-17-18(14,15)16/h4-8,10-13H,1-2H2,(H2,14,15,16)/t4-,5-,6-,7+/m1/s1
+
+
+ InChI=1S/C4H9O7P/c5-1-3(6)4(7)2-11-12(8,9)10/h1,3-4,6-7H,2H2,(H2,8,9,10)/t3-,4+/m0/s1
+
+
+ InChI=1S/C6H14O12P2/c7-4-3(1-16-19(10,11)12)18-6(9,5(4)8)2-17-20(13,14)15/h3-5,7-9H,1-2H2,(H2,10,11,12)(H2,13,14,15)/t3-,4-,5+,6?/m1/s1
+
+
+ InChI=1S/C5H6O5/c6-3(5(9)10)1-2-4(7)8/h1-2H2,(H,7,8)(H,9,10)
+
+
+ InChI=1S/C5H9NO4/c6-3(5(9)10)1-2-4(7)8/h3H,1-2,6H2,(H,7,8)(H,9,10)/t3-/m0/s1
+
+
+ InChI=1S/C5H10N2O3/c6-3(5(9)10)1-2-4(7)8/h3H,1-2,6H2,(H2,7,8)(H,9,10)/t3-/m0/s1
+
+
+ InChI=1S/C5H9NO2/c7-5(8)4-2-1-3-6-4/h4,6H,1-3H2,(H,7,8)/t4-/m0/s1
+
+
+ InChI=1S/C4H7NO4/c5-2(4(8)9)1-3(6)7/h2H,1,5H2,(H,6,7)(H,8,9)/t2-/m0/s1
+
+
+ InChI=1S/C4H8N2O3/c5-2(4(8)9)1-3(6)7/h2H,1,5H2,(H2,6,7)(H,8,9)/t2-/m0/s1
+
+
+ InChI=1S/C4H9NO3/c1-2(6)3(5)4(7)8/h2-3,6H,5H2,1H3,(H,7,8)/t2-,3+/m1/s1
+
+
+ InChI=1S/C6H13NO2/c1-3-4(2)5(7)6(8)9/h4-5H,3,7H2,1-2H3,(H,8,9)/t4-,5-/m0/s1
+
+
+ InChI=1S/C5H11NO2S/c1-9-3-2-4(6)5(7)8/h4H,2-3,6H2,1H3,(H,7,8)/t4-/m0/s1
+
+
+ InChI=1S/C3H7O6P/c4-1-3(5)2-9-10(6,7)8/h4H,1-2H2,(H2,6,7,8)
+
+
+ InChI=1S/C3H7NO3/c4-2(1-5)3(6)7/h2,5H,1,4H2,(H,6,7)/t2-/m0/s1
+
+
+ InChI=1S/C9H11NO3/c10-8(9(12)13)5-6-1-3-7(11)4-2-6/h1-4,8,11H,5,10H2,(H,12,13)/t8-/m0/s1
+
+
+ InChI=1S/C9H11NO2/c10-8(9(11)12)6-7-4-2-1-3-5-7/h1-5,8H,6,10H2,(H,11,12)/t8-/m0/s1
+
+
+ InChI=1S/CO2/c2-1-3
+
+
+ InChI=1S/C2H5NO2/c3-1-2(4)5/h1,3H2,(H,4,5)
+
+
+ BM.ext
+
+
+
+
+ InChI=1S/C6H14N2O2/c7-4-2-1-3-5(8)6(9)10/h5H,1-4,7-8H2,(H,9,10)/t5-/m0/s1
+
+
+
+ Gluc.ext
+ InChI=1S/C6H12O6/c7-1-2-3(8)4(9)5(10)6(11)12-2/h2-11H,1H2/t2-,3-,4+,5-,6?/m1/s1
+
+
+
+ InChI=1S/C6H13O10P/c7-2(1-16-17(13,14)15)3(8)4(9)5(10)6(11)12/h2-5,7-10H,1H2,(H,11,12)(H2,13,14,15)
+
+
+
+
+
+ InChI=1S/C6H11O9P/c7-3(1-4(8)6(10)11)5(9)2-15-16(12,13)14/h3,5,7,9H,1-2H2,(H,10,11)(H2,12,13,14)
+
+
+ InChI=1S/C25H40N7O19P3S/c1-25(2,20(38)23(39)28-6-5-14(33)27-7-8-55-16(36)4-3-15(34)35)10-48-54(45,46)51-53(43,44)47-9-13-19(50-52(40,41)42)18(37)24(49-13)32-12-31-17-21(26)29-11-30-22(17)32/h11-13,18-20,24,37-38H,3-10H2,1-2H3,(H,27,33)(H,28,39)(H,34,35)(H,43,44)(H,45,46)(H2,26,29,30)(H2,40,41,42)
+ 2,3,14,15
+
+
+
+ InChI=1S/C2H2O3/c3-1-2(4)5/h1H,(H,4,5)/p-1
+
+
+ InChI=1S/C2H4O2/c1-2(3)4/h1H3,(H,3,4)
+
+
+
+ InChI=1S/C3H7NO2S/c4-2(1-7)3(5)6/h2,7H,1,4H2,(H,5,6)
+
+
+
+ InChI=1S/C5H11NO2/c1-3(2)4(6)5(7)8/h3-4H,6H2,1-2H3,(H,7,8)
+
+
+ InChI=1S/C3H7NO2/c1-2(4)3(5)6/h2H,4H2,1H3,(H,5,6)
+
+
+ InChI=1S/C6H14N4O2/c7-4(5(11)12)2-1-3-10-6(8)9/h4H,1-3,7H2,(H,11,12)(H4,8,9,10)
+
+
+
+ InChI=1S/C11H12N2O2/c12-9(11(14)15)5-7-6-13-10-4-2-1-3-8(7)10/h1-4,6,9,13H,5,12H2,(H,14,15)
+
+
+ InChI=1S/C6H9N3O2/c7-5(6(10)11)1-4-2-8-3-9-4/h2-3,5H,1,7H2,(H,8,9)(H,10,11)
+
+
+ InChI=1S/C7H14N2O4/c8-4(6(10)11)2-1-3-5(9)7(12)13/h4-5H,1-3,8-9H2,(H,10,11)(H,12,13)
+
+
+
+ InChI=1S/C6H13NO2/c1-4(2)3-5(7)6(8)9/h4-5H,3,7H2,1-2H3,(H,8,9)
+
+
+
+
+
+
+ O2.ext
+
+
+ NH3.ext
+
+
+ SO4.ext
+
+
+ NADH.ext
+
+
+
+
+ Glycolysis
+
+
+
+
+ Glycolysis
+
+
+
+
+ Glycolysis
+
+
+
+
+
+ Glycolysis
+
+
+
+
+
+ Glycolysis
+
+
+
+
+ Glycolysis
+
+
+
+
+
+
+ Glycolysis
+
+
+
+
+ Glycolysis
+
+
+
+
+
+ TCA
+
+
+
+
+
+
+ TCA
+
+
+
+
+
+ TCA
+
+
+
+
+
+
+ TCA
+
+
+
+
+
+ TCA
+
+
+
+
+
+ TCA
+
+
+
+
+
+ TCA
+
+
+
+
+
+ TCA
+
+
+
+
+ TCA
+
+
+
+
+ TCA
+
+
+
+
+
+ PPP
+
+
+
+
+
+ PPP
+
+
+
+
+ PPP
+
+
+
+
+ Amphibolic R.
+
+
+
+
+
+ Amphibolic R.
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ UPT
+
+
+
+
+ PPP
+
+
+
+
+
+
+ PPP
+
+
+
+
+
+ PPP
+
+
+
+
+
+ PPP
+
+
+
+
+
+ Biomass
+
+
+
+ PPP
+
+
+
+
+
+ PPP
+
+
+
+
+
+ EDP
+
+
+
+
+ EDP
+
+
+
+
+
+ TCA
+
+
+
+
+ TCA
+
+
+
+
+
+
+ Glyox
+
+
+
+
+
+ Glyox
+
+
+
+
+
+ Glyox
+
+
+
+
+
+ Amphibolic R.
+
+
+
+
+
+
+ Amphibolic R.
+
+
+
+
+
+
+ Acetic Acid Formation
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Biomass
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+ Biomass
+
+
+
+ One Carbon Metabolism
+
+
+
+
+
+ One Carbon Metabolism
+
+
+
+
+
+ Oxidative Phosphorylation
+
+
+
+
+
+
+
+ Oxidative Phosphorylation
+
+
+
+
+
+
+
+
+
+ Transport
+
+
+
+
+ Transport
+
+
+
+ ATP Hydrolysis
+
+
+
+ Transport
+
+
+
+ CO2 Exchange
+
+
+
+
+
+
+ Transport
+
+
+
+
+ Transhydrogenation
+
+
+
+
+ Transport
+
+
+
+
+ Biomass
+
+
+
+ CO2 Exchange
+
+
+
+
+ CO2 Exchange
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+
+ Biomass
+
+
+
+
+ Biomass
+
+
+
+
+
+ TCA7_v26___1/0.5=TCA7_v26___2/0.5;
+ GOX1_v29___1/0.5=GOX1_v29___2/0.5;
+ TCA6_v25___1/0.5=TCA6_v25___2/0.5;
+ AA12_v48___1/0.25=AA12_v48___2/0.25;
+ AA12_v48___2/0.25=AA12_v48___3/0.25;
+ AA12_v48___3/0.25=AA12_v48___4/0.25;
+ TCA8_v27___1/0.5=TCA8_v27___2/0.5;
+ AA15_v51___1/0.5=AA15_v51___2/0.5;
+ AA13_v49___1/0.5=AA13_v49___2/0.5
+
+
+
+ TCA7_v26___1/0.5=TCA7_v26___2/0.5;
+ GOX1_v29___1/0.5=GOX1_v29___2/0.5;
+ TCA6_v25___1/0.5=TCA6_v25___2/0.5;
+ TCA8_v27___1/0.5=TCA8_v27___2/0.5
+
+
+
+
+ Substrate see Crown 2015
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0=488*mu2_v70___1-Ala_bm___1;
+ 0=281*mu2_v70___1-Arg_bm___1;
+ 0=229*mu2_v70___1-Asn_bm___1;
+ 0=229*mu2_v70___1-Asp_bm___1;
+ 0=87*mu2_v70___1-Cys_bm___1;
+ 0=250*mu2_v70___1-Glu_bm___1;
+ 0=250*mu2_v70___1-Gln_bm___1;
+ 0=582*mu2_v70___1-Gly_bm___1;
+ 0=90*mu2_v70___1-His_bm___1;
+ 0=276*mu2_v70___1-Ile_bm___1;
+ 0=428*mu2_v70___1-Leu_bm___1;
+ 0=326*mu2_v70___1-Lys_bm___1;
+ 0=146*mu2_v70___1-Met_bm___1;
+ 0=176*mu2_v70___1-Phe_bm___1;
+ 0=210*mu2_v70___1-Pro_bm___1;
+ 0=205*mu2_v70___1-Ser_bm___1;
+ 0=241*mu2_v70___1-Thr_bm___1;
+ 0=54*mu2_v70___1-Trp_bm___1;
+ 0=131*mu2_v70___1-Tyr_bm___1;
+ 0=402*mu2_v70___1-Val_bm___1;
+ 0=205*mu2_v70___1-G6P_bm___1;
+ 0=71*mu2_v70___1-F6P_bm___1;
+ 0=754*mu2_v70___1-R5P_bm___1;
+ 0=129*mu2_v70___1-GAP_bm___1;
+ 0=619*mu2_v70___1-_3PG_bm___1;
+ 0=51*mu2_v70___1-PEP_bm___1;
+ 0=83*mu2_v70___1-Pyr_bm___1;
+ 0=2510*mu2_v70___1-AcCoA_bm___1;
+ 0=87*mu2_v70___1-AKG_bm___1;
+ 0=340*mu2_v70___1-OAC_bm___1;
+ 0=443*mu2_v70___1-MEETHF_bm___1;
+ 0=33247*mu2_v70___1-ATP_bm___1;
+ 0=5363*mu2_v70___1-NADPH_bm___1;
+ 0=1455*mu2_v70___1-NADH_bm___1;
+ AC1_v35___1<=6542.235/60;
+ AC1_v35___1>=4792.4618/60;
+ ANA1_v31___1<=878.533/60;
+ ANA1_v31___1>=6.14313E-7/60;
+ ANA3_v33___1<=3896.9782/60;
+ ANA3_v33___1>=1426.2412/60;
+ ATPH1_v64___1<=55487.63/60;
+ ATPH1_v64___1>=14468.296/60;
+ EMP8_v8<=3347.7908/60;
+ EMP8_v8>=1101.9954/60;
+ Glc_upt___1<=8616.77/60;
+ Glc_upt___1>=8583.1612/60;
+ TCA1_v20<=10551.942/60;
+ TCA1_v20>=7831.0998/60;
+ TCA9_v28___1<=2835.119/60;
+ TCA9_v28___1>=0/60;
+ TH1_v63___1<=10918.302/60;
+ TH1_v63___1>=1462.7568/60;
+ mu2_v70___1<=0.77191708/60;
+ mu2_v70___1>=0.55134342/60;
+ GAS2_v71___1>=8.6E-7/60;
+ EMP1_v1=Glc_upt___1
+
+
+
+ AA8_v44___1>=8.6E-9/60;
+ AA8_v44___1<=1077.6918/60;
+ GOX1_v29___1+GOX1_v29___2>=8.6E-7/60;
+ GOX1_v29___1+GOX1_v29___2<=3385.7598/60;
+ PPP6_v14___1<=1868.0834/60;
+ AA9_v45___1>=8.59999E-7/60;
+ AC1_v35___1>=8.6E-7/60;
+ EMP2_v2>=8.6E-7/60;
+ EMP4_v4>=8.6E-7/60;
+ EMP5_v5>=8.6E-7/60;
+ EMP6_v6>=8.6E-7/60;
+ EMP7_v7>=8.6E-7/60;
+ PPP3_v11___1>=8.6E-7/60;
+ PPP4_v12___1>=8.6E-7/60;
+ PPP5_v13___1>=8.6E-7/60;
+ PPP6_v14___1>=8.6E-7/60;
+ PPP7_v15___1>=8.6E-7/60;
+ PPP8_v16___1>=4.26035E-9/60;
+ PPP9_v17___1>=8.6E-7/60;
+ TCA3_v22___1>=8.6E-7/60;
+ TCA4_v23>=86000/60;
+ TCA6_v25___1+TCA6_v25___2>=8.6E-7/60;
+ TCA7_v26___1+TCA7_v26___2>=8.6E-7/60;
+ TCA8_v27___1+TCA8_v27___2>=1935.0516/60;
+ TCA9_v28___1>=2082.1976/60;
+ TH1_v63___1>=8.6E-7/60
+
+
+
+ G6P>=0.31444375;
+ G6P<=0.898274134;
+ F6P>=0.313311209;
+ F6P<=0.831196031;
+ FBP>=0.01;
+ FBP<=2;
+ GAP>=0.01;
+ GAP<=2;
+ DHAP>=0.332032465;
+ DHAP<=1.263761343;
+ _3PG>=0.01;
+ _3PG<=2;
+ PEP>=0.052190381;
+ PEP<=0.111607122;
+ Pyr>=2.531221234;
+ Pyr<=19.46808074;
+ AcCoA>=0.142453436;
+ AcCoA<=0.391758806;
+ Cit>=1.857231709;
+ Cit<=32.55619142;
+ ICit>=0.01;
+ ICit<=2;
+ AKG>=0.587799161;
+ AKG<=1.532280769;
+ SucCoA>=0.467403042;
+ SucCoA<=2.823903238;
+ Suc>=1.095216649;
+ Suc<=3.543181319;
+ Fum>=0.221104167;
+ Fum<=0.707750577;
+ Mal>=3.023091181;
+ Mal<=9.214435783;
+ OAC>=0.0377;
+ OAC<=0.08618;
+ _6PG>=0.412127518;
+ _6PG<=1.231481154;
+ Ru5P>=0.0026;
+ Ru5P<=0.00556;
+ R5P>=1.105117926;
+ R5P<=3.244934487;
+ X5P>=1.105117926;
+ X5P<=3.244934487;
+ S7P>=0.01;
+ S7P<=1;
+ E4P>=0.001680761;
+ E4P<=0.007525757;
+ Pro>=0.09295;
+ Pro<=0.59631;
+ Gln>=0.3211;
+ Gln<=2.05998;
+ Glu>=2.7755;
+ Glu<=17.8059;
+ Ala>=0.2327;
+ Ala<=1.49286;
+ Val>=0.11765;
+ Val<=0.75477;
+ Asp>=0.21905;
+ Asp<=1.40529;
+ Ac>=0.8229;
+ Ac<=1.89874;
+ Leu>=0.078;
+ Leu<=0.5004;
+ Arg>=0.0377;
+ Arg<=0.24186;
+ Ser>=0.22295;
+ Ser<=1.43031;
+ Trp>=0.00585;
+ Trp<=0.03753;
+ Phe>=0.0429;
+ Phe<=0.27522;
+ His>=0.0338;
+ His<=0.21684;
+ Cys>=0.2067;
+ Cys<=0.69222;
+ Thr>=0.0247;
+ Thr<=0.15846;
+ Lys>=0.23465;
+ Lys<=1.50537;
+ Ile>=0.02145;
+ Ile<=0.13761;
+ Gly>=0.2067;
+ Gly<=1.32606;
+ Asn>=0.06695;
+ Asn<=0.42951;
+ Glyox>=0.01;
+ Glyox<=1;
+ Met>=0.0195;
+ Met<=0.1251;
+ CO2_out>=0.01;
+ CO2_out<=1;
+ CO2_unlabeled>=0.01;
+ CO2_unlabeled<=1;
+ KDPG>=0.01;
+ KDPG<=2;
+ Tyr>=0.01;
+ Tyr<=2;
+ FTHF>=0.01;
+ FTHF<=1;
+ MEETHF>=0.01;
+ MEETHF<=2;
+ METHF>=0.01;
+ METHF<=2
+
+
+
+
+
+Ala[1-2]#M0,1,2
+
+
+Ala#M0,1,2,3
+
+
+Asp[1-3]#M0,1,2,3
+
+
+Asp#M0,1,2,3,4
+
+
+Glu#M0,1,2,3,4,5
+
+
+Tyr[8-9]#M0,1,2
+
+
+Gly[1]#M0,1
+
+
+Val[1-4]#M0,1,2,3,4
+
+
+Val#M0,1,2,3,4,5
+
+
+Leu[1-5]#M0,1,2,3,4,5
+
+
+Ile[1-5]#M0,1,2,3,4,5
+
+
+Ser[1-2]#M0,1,2
+
+
+Phe[8-9]#M0,1,2
+
+
+Phe[1-8]#M0,1,2,3,4,5,6,7,8
+
+
+AC1_v35___1
+
+
+G6P
+
+
+F6P
+
+
+Mal
+
+
+R5P
+
+
+S7P
+
+
+Pro
+
+
+Glu
+
+
+Ala
+
+
+Val
+
+
+Asp
+
+
+Leu
+
+
+Thr
+
+
+FBP
+
+
+GAP
+
+
+DHAP
+
+
+Pyr
+
+
+Cit
+
+
+AKG
+
+
+Suc
+
+
+Fum
+
+.9783252711678733
+.021170224498232994
+.000504504333893822
+
+
+.974996139773594
+.021145677597857182
+.003858182628548844
+
+
+.9553118754071724
+.021062754220499386
+.02362537037232823
+
+
+.883124329045387
+.02292966517380017
+.0939460057808128
+
+
+.7328308231362376
+.037390816123961196
+.2297783607398012
+
+
+.5857838262497608
+.0690318882208078
+.345184285529431
+
+
+.5363032927522791
+.08603041428544023
+.3776662929622807
+
+
+.5322625170528938
+.0878917064775645
+.379845776469541
+
+
+.5314269459421497
+.08837397124321206
+.3801990828146382
+
+
+.5312106769270468
+.08848979547499197
+.380299527597961
+
+
+.9678571390971008
+.031411712269543014
+.0007257795768240418
+5.36905653209279e-0
+
+
+.9645630746907603
+.031349728741725445
+.004046161521339122
+4.103504617514131e-0
+
+
+.945039552531618
+.031047780706848237
+.023660187130390135
+.0002524796311436981
+
+
+.8727650542647292
+.03229556582952047
+.09388182933013442
+.001057550575615998
+
+
+.7192581431508507
+.04524717250721706
+.23251314843370818
+.002981535908224082
+
+
+.5653024436740488
+.0727751224362316
+.3564911164037462
+.0054313174859733
+
+
+.5125487593560967
+.08683889217909817
+.3939141988448597
+.00669814961994548
+
+
+.5082244875048572
+.08831267080439337
+.3964418161538266
+.007021025536922886
+
+
+.5074041590822169
+.08864027180585474
+.39674096275535
+.0072146063565783
+
+
+.5071945775611788
+.08872205442037939
+.3968114890199665
+.00727187899847540
+
+
+.9645030566941036
+.0315725218892544
+.0038595110053776588
+6.491041126423561e-0
+
+
+.9511057316576902
+.03233427569077106
+.016065364092672806
+.000494628558865942
+
+
+.914255498454127
+.035504726054239946
+.047719904864973586
+.00251987062665953
+
+
+.8435104196301892
+.046405452073353094
+.1021925326158511
+.00789159568060656
+
+
+.7359584020714958
+.07468833761626612
+.17230421197097795
+.01704904834126014
+
+
+.607758925684172
+.11890955753686008
+.24403456694808007
+.0292969498308878
+
+
+.5120626338680379
+.15659597541872977
+.2900749995410601
+.0412663911721723
+
+
+.46152123817379365
+.17806759019551777
+.309059873533009
+.0513512980976794
+
+
+.4294703972844381
+.19158549723605547
+.3192687999514292
+.0596753055280772
+
+
+.42110299066331525
+.1950695987401747
+.32169595075401974
+.06213145984249033
+
+
+.9541821187071787
+.041555607015243634
+.004156507893232856
+.00010505385039958063
+7.12533945223946e-0
+
+
+.940893202587015
+.04219309125223391
+.01624445154356347
+.0006639691816460172
+5.28543554158525e-0
+
+
+.9038943291296146
+.04530837223564781
+.0476424479453334
+.003126385924377809
+2.8464765026379686e-0
+
+
+.8304015484240171
+.05694329662036127
+.1023333927262477
+.010213211554605478
+.0001085506747684554
+
+
+.7138277649130549
+.08584048562541138
+.17528043293935092
+.02470817109119241
+.0003431454309903316
+
+
+.5707658494087753
+.13081583203252606
+.2503910611508882
+.047000562623558576
+.001026694784251742
+
+
+.46394167568214595
+.1679753785792581
+.2957601362841777
+.06959171527347804
+.002731094180940287
+
+
+.4118900721627505
+.1849529260409472
+.3105713058459501
+.08691336856338178
+.00567232738697042
+
+
+.3809907275397695
+.1943269223736184
+.31626500599514584
+.09970403350733206
+.008713310584134
+
+
+.37297775480758477
+.19692652795968726
+.31725014822238073
+.10317324835325849
+.00967232065708882
+
+
+.9476302651785359
+.05124675107290643
+.001110841710214759
+1.2073181050718784e-05
+6.856886600366277e-08
+2.884262311968625e-1
+
+
+.9475902250521991
+.051247560204237146
+.0011481450656966883
+1.3705874424467237e-05
+3.5418957120328193e-07
+9.6138712972503e-0
+
+
+.9470981329887675
+.05125278159973164
+.0015981533866197097
+3.8118860354757716e-05
+1.2224580041142003e-05
+5.885844851984243e-0
+
+
+.9427529478697155
+.051341555074275486
+.0053613318319493284
+.0002803938720899367
+.00024698143568946537
+1.678991628025901e-0
+
+
+.9187546011891993
+.05258711935143606
+.023960334456472596
+.002051205172076009
+.0024384391238282785
+.0002083007069877487
+
+
+.8409306459707979
+.06005068948043382
+.07472993595806707
+.010496352793233816
+.012513506330125884
+.00127886946734147
+
+
+.6829750467784551
+.08082815447993391
+.1589494170629001
+.03504465234955715
+.03766540206088295
+.00453732726827076
+
+
+.4684056211285428
+.11226416618226906
+.25673966261606945
+.07666556314194198
+.07525792302884399
+.01066706390233271
+
+
+.291095696432385
+.13965216754696624
+.3244569396709135
+.11856881751354173
+.10829201541757799
+.01793436341861548
+
+
+.24539402157437235
+.14751760010943246
+.33740357328532605
+.13181161200280433
+.1172591344161609
+.02061405861190390
+
+
+.9717303956013846
+.028080277389375796
+.000189327009239549
+
+
+.9483425599902716
+.0512092179623736
+.000448222047354721
+
+
+.8873931879768651
+.11138734984056115
+.00121946218257387
+
+
+.7785988249689819
+.2180852333743573
+.00331594165666084
+
+
+.6487725145892298
+.3430638131586137
+.00816367225215642
+
+
+.5655073110116616
+.42053497529190725
+.01395771369643112
+
+
+.5464434260008516
+.43730104835747946
+.01625552564166891
+
+
+.5449545933636318
+.43836045143467234
+.01668495520169585
+
+
+.5443920505284343
+.4386846237508987
+.01692332572066709
+
+
+.5442427154052913
+.43877230449751065
+.0169849800971979
+
+
+.9888930100253929
+.0111069899746071
+
+
+.9852900443568302
+.01470995564316977
+
+
+.9635332776378891
+.03646672236211087
+
+
+.8891836707775459
+.1108163292224541
+
+
+.7620041966238444
+.237995803376155
+
+
+.6615934608741968
+.338406539125803
+
+
+.6158051988335219
+.384194801166478
+
+
+.5955901681765572
+.4044098318234427
+
+
+.5825069351672021
+.4174930648327978
+
+
+.5789519906709001
+.421048009329099
+
+
+.9566614434653177
+.04141363802986259
+.0018881487194808005
+3.085916883409273e-05
+5.910616504926883e-0
+
+
+.9465693357847217
+.04115588779306764
+.011796002718493057
+.000251223714990025
+.0002275499887274893
+
+
+.8917575928609923
+.03985921619302785
+.06285197758096477
+.0015584997348735445
+.00397271363014135
+
+
+.7232002694881142
+.03955558449191876
+.20037474818656356
+.007011581554870527
+.02985781627853292
+
+
+.4655291292289463
+.05427903803509021
+.359670495719467
+.027032863322767012
+.0934884736937293
+
+
+.31162134331679747
+.08334247664873551
+.4117369843863494
+.056527114380068355
+.1367720812680491
+
+
+.2852142097536762
+.09280064118633957
+.41240206939341434
+.06588429954322739
+.1436987801233425
+
+
+.2832448822325022
+.09358710173272144
+.41206875396910536
+.06679887100672793
+.1443003910589430
+
+
+.28240098318636847
+.09393314607503345
+.4119040255032526
+.0672061169544187
+.1445557282809268
+
+
+.28217916918930036
+.09401590204273638
+.4118680974910541
+.0673073005665932
+.1446295307103159
+
+
+.9464250472668673
+.051207045204347816
+.0023107991204927847
+5.095160844822032e-05
+6.069101915740591e-06
+8.769792816940202e-0
+
+
+.9364389167574942
+.05084318377771587
+.012112088689188586
+.0003756138799466757
+.00022771538346631203
+2.481512188306533e-0
+
+
+.8821466888184926
+.04896700198621185
+.06266553622859168
+.002220904224692509
+.003957141778818328
+4.2726963193012894e-0
+
+
+.7144799501213874
+.0470124250740589
+.1990704514946454
+.009228313300967394
+.02986869647348233
+.000340163535458525
+
+
+.45591479517429734
+.058582308922156226
+.357753797488743
+.03149369423194144
+.09501122212092096
+.00124418206194108
+
+
+.299537190610899
+.08331227752630108
+.4109507238847757
+.06185982463779714
+.14212915520908473
+.002210828131142277
+
+
+.27244375301311846
+.09107173125355177
+.4121969729147401
+.07118167109409314
+.15053818790730236
+.002567683817194211
+
+
+.2704509494431923
+.0916934575555441
+.41180446246369184
+.07215763406450805
+.15122241223526367
+.002671084237800010
+
+
+.2696353033770935
+.09194986727770466
+.4115842902729295
+.07260364965323973
+.15148266326573506
+.002744226153297604
+
+
+.2694216747796966
+.0920138850923646
+.41152594224126254
+.07272012171796657
+.15155235101515865
+.002766025153551119
+
+
+.9454234458526625
+.05148124025215049
+.00300579040552983
+7.97787192614129e-05
+9.546507252445444e-06
+1.9826314327363145e-0
+
+
+.926556394277943
+.05437601658139453
+.017800566996515853
+.0009020081731832528
+.0003527522771724399
+1.226169379087538e-0
+
+
+.8259241349726132
+.0732958447796132
+.08438149198822897
+.010286047371355083
+.005510383913239648
+.000602096974949847
+
+
+.5603353025791035
+.12191813890469547
+.2128091208794899
+.06203291715413108
+.0336665119007694
+.00923800858181083
+
+
+.2769908853098463
+.16024760425967777
+.2851420154534068
+.15639300716499877
+.08317340744998891
+.0380530803620813
+
+
+.17984935426764215
+.17095895125442306
+.2835971578067646
+.20161257345851885
+.1079631206695546
+.0560188425430968
+
+
+.16817850463753659
+.17155201011125443
+.2815337130020901
+.20788476748047405
+.11195825607009816
+.0588927486985466
+
+
+.16662586433227633
+.17166090164390646
+.2809575954466407
+.20894404251099402
+.11240485255864764
+.0594067435075348
+
+
+.1658303483346833
+.1717237531312802
+.2806577235131742
+.20948722076924908
+.11263186180087831
+.0596690924507348
+
+
+.16562134673890633
+.17173818587430206
+.28057775442849064
+.2096299288626911
+.11269251146678227
+.0597402726288275
+
+
+.9453653868772713
+.05118126768289868
+.003363971222051496
+8.664189594056276e-05
+2.685699201270477e-06
+4.662263662759693e-0
+
+
+.9272316646243355
+.05065636601061413
+.021268534299851382
+.0007147500412277602
+.00012567287714150953
+3.012146829723233e-0
+
+
+.84047237721887
+.049112355784512875
+.10337813952195554
+.0042826405917805255
+.0026391644728087547
+.0001153224100722251
+
+
+.6448875715418878
+.053897781385772246
+.264528598622318
+.015840320582633682
+.019491400272830216
+.001354327594558282
+
+
+.451289377077512
+.08158864077292939
+.36427563302242405
+.04130367276787994
+.056275040507926935
+.005267635851327
+
+
+.336327500378247
+.11254981580133269
+.3711786577949311
+.07777370658202222
+.09162574582347059
+.01054457361999638
+
+
+.2749511958634141
+.12775805484705677
+.36304140798666845
+.10576186214376884
+.11303440406993619
+.0154530750891556
+
+
+.24609660988210882
+.1352411296980973
+.3555799543981291
+.12187989465893666
+.12178016880684428
+.0194222425558838
+
+
+.22836927149880248
+.1397072826879376
+.34996080781103567
+.13266739152010604
+.12663764659909585
+.02265759988302236
+
+
+.22373139807774708
+.1408937210065905
+.3483050192601649
+.13562567775546266
+.12782688339862233
+.02361730050141249
+
+
+.9683974820943555
+.02107744241837484
+.01052507548726973
+
+
+.9362724347905309
+.020762141630290804
+.0429654235791783
+
+
+.8571806771270055
+.0210863809362703
+.1217329419367242
+
+
+.7292579742071148
+.028541055502651402
+.2422009702902337
+
+
+.6030158922419374
+.05208829630286682
+.3448958114551957
+
+
+.5438504704117468
+.0758966776238914
+.3802528519643617
+
+
+.5350801749407745
+.08152586500593456
+.3833939600532908
+
+
+.5348961144289075
+.08166489157233792
+.383438993998754
+
+
+.534895134619221
+.08166546265516172
+.383439402725617
+
+
+.5348948859134397
+.08166560053893968
+.383439513547620
+
+
+.9679873705669014
+.03178318190455596
+.0002294475285427068
+
+
+.9331762376648597
+.06620859883663102
+.000615163498509272
+
+
+.8486618995988513
+.1496427176783984
+.00169538272275033
+
+
+.7194027665488831
+.27623124775494273
+.00436598569617415
+
+
+.6034023419362842
+.3868615898875168
+.00973606817619892
+
+
+.554139658067862
+.4310708275902036
+.01478951434193431
+
+
+.5459671709749572
+.43769987009689465
+.01633295892814819
+
+
+.5449298967470803
+.4383749918239367
+.01669511142898307
+
+
+.5443853366621678
+.4386885298890384
+.01692613344879377
+
+
+.5442401948616782
+.43877378891993374
+.0169860162183880
+
+
+.8975245102771211
+.07858457264134093
+.021330499958966697
+.0014929680981146146
+.0009839752938417478
+8.486392363887535e-05
+2.627137795997462e-06
+1.2256732093454133e-06
+1.1271562771952156e-0
+
+
+.8327917772162673
+.07986177293939081
+.0695641215708074
+.008317306454521597
+.008514817361413653
+.0008692263173031824
+7.451214208070845e-05
+6.262691980062289e-06
+2.0330623529235113e-0
+
+
+.6712991275389347
+.09436389396743172
+.15194842851793525
+.04065039391164924
+.032502799559308224
+.00831597652677665
+.0008084788247286341
+.00010904259222611718
+1.8585610093397743e-0
+
+
+.4119724762235214
+.13280767353154344
+.21856482495825907
+.12217303949098866
+.0735746830993726
+.034677511635392004
+.005358135555621235
+.0008584943040475424
+1.3161201254205041e-0
+
+
+.1772683666749343
+.16149078144907153
+.23269356283191933
+.20859976902784566
+.12477338267393023
+.0729441623770778
+.01907526022140967
+.003094670011573095
+6.00447322382918e-0
+
+
+.08304323681507163
+.16307859591202165
+.2155109715846304
+.24286063440564146
+.16003486048983703
+.09630169959361412
+.03347985484779411
+.005553730151315554
+.0001364162000740198
+
+
+.07171441807362214
+.15808473161699732
+.2127757087014689
+.24524731392811325
+.16734356190849015
+.10130710710554897
+.037104801018632484
+.006260025099072282
+.0001623325480544489
+
+
+.07123052376781779
+.15758816622658725
+.21243937495191595
+.24557919659935334
+.1676560654330021
+.10175287609430536
+.03729670111999114
+.00629288030051414
+.0001642155065128906
+
+
+.07105087425155246
+.1572857191812254
+.21243269213457117
+.24557028724304533
+.1679111186923214
+.10190881130523605
+.03737507870565174
+.006300873018389487
+.0001645454680069163
+
+
+.07100325709124054
+.15719836252100172
+.21245134853931288
+.24553541271364948
+.16800172241147107
+.10194513870592264
+.03739627450316492
+.006303865765011109
+.0001646177492258152
+
+
+ 94.31333333333333
+ .36725
+ .351
+ 3.666
+ 1.31126667
+ .02
+ .1859
+ 5.551
+ .4654
+ .2353
+ .4381
+ .156
+ .0494
+ .05
+ .05
+ .4615
+ 5.8175
+ 8.540675
+ .65216667
+ 1.37605
+ .27603333
+
+
+ 143.33333333333334
+ 38.126666666666665
+ 152.22
+ 16.764266666666668
+ 1.0238558E-8
+ 39.559999999999995
+ 94.31333333333333
+ 118.82333333333332
+ 459.52666666666664
+ 0.01161
+ 18.776666666666664
+ 1207.4285333333332
+ 235.0509
+ 576.4322
+ 39.722253333333335
+ 0.3123534333333334
+ 92.2049
+ 1097.7985999999999
+ 755.2462666666668
+ 4.421847666666666
+ 1.927131E-5
+ 7.101865666666667E-11
+ 795.93
+ 667.1751
+ 1434.7666666666667
+ 267.91866666666664
+ 50.711189999999995
+ 170.87555000000003
+ 274.39446666666663
+ 0.3759067166666667
+ 488.84263333333337
+ 1.4333333333333336E-10
+ 98.99288000000001
+ 436.9674333333333
+ 0.36725
+ 0.351
+ 0.05
+ 0.05
+ 0.05265
+ 5.8175
+ 0.162825
+ 0.039
+ 8.540675
+ 0.05
+ 1.37605
+ 0.27603333
+ 3.666
+ 0.0026
+ 1.31126667
+ 1.31126667
+ 0.02
+ 0.0026
+ 0.05
+ 0.65216667
+ 5.551
+ 0.6422
+ 0.1859
+ 0.4381
+ 0.1339
+ 0.0494
+ 0.0429
+ 0.039
+ 0.4615
+ 0.4459
+ 0.0676
+ 0.0858
+ 1.0
+ 0.4134
+ 0.02
+ 0.4693
+ 0.494
+ 0.05
+ 0.05
+ 0.05
+ 0.89396667
+ 0.02
+ 0.8554
+ 0.2652
+ 0.2353
+ 0.4654
+ 0.0754
+ 0.02
+ 0.0117
+ 0.02
+ 0.156
+ 0.1
+ 0.1
+ 0.05
+ 0.1
+
+
+
+
diff --git a/data/FluxML-Test/fluxml-model/models/ecoli_model_level_3.fml b/data/FluxML-Test/fluxml-model/models/ecoli_model_level_3.fml
new file mode 100644
index 0000000..df548d6
--- /dev/null
+++ b/data/FluxML-Test/fluxml-model/models/ecoli_model_level_3.fml
@@ -0,0 +1,1373 @@
+
+
+ 2017-10-10 16:31:58
+ modeler: Martin Cerff, Salah Azzouzi , Martin Beyss and Katharina Noeh
+ model adapted from: https://doi.org/10.1016/j.ymben.2015.01.001
+ properties: STAT_MFA, abs. fluxes
+
+
+
+
+
+ InChI=1S/C6H13O9P/c7-3-2(1-14-16(11,12)13)15-6(10)5(9)4(3)8/h2-10H,1H2,(H2,11,12,13)/t2-,3-,4+,5-,6+/m1/s1
+
+
+ InChI=1S/C6H13O9P/c7-2-6(10)5(9)4(8)3(15-6)1-14-16(11,12)13/h3-5,7-10H,1-2H2,(H2,11,12,13)/t3-,4-,5+,6?/m1/s1
+
+
+ InChI=1S/C3H7O6P/c4-1-3(5)2-9-10(6,7)8/h1,3,5H,2H2,(H2,6,7,8)/t3-/m0/s1
+
+
+ 3PG
+
+
+ InChI=1S/C3H5O6P/c1-2(3(4)5)9-10(6,7)8/h1H2,(H,4,5)(H2,6,7,8)
+
+
+ InChI=1S/C3H4O3/c1-2(4)3(5)6/h1H3,(H,5,6)
+
+
+ InChI=1S/C23H38N7O17P3S/c1-12(31)51-7-6-25-14(32)4-5-26-21(35)18(34)23(2,3)9-44-50(41,42)47-49(39,40)43-8-13-17(46-48(36,37)38)16(33)22(45-13)30-11-29-15-19(24)27-10-28-20(15)30/h10-11,13,16-18,22,33-34H,4-9H2,1-3H3,(H,25,32)(H,26,35)(H,39,40)(H,41,42)(H2,24,27,28)(H2,36,37,38)/t13-,16-,17-,18+,22-/m1/s1
+
+
+ InChI=1S/C4H4O5/c5-2(4(8)9)1-3(6)7/h1H2,(H,6,7)(H,8,9)
+
+
+ InChI=1S/C6H8O7/c7-3(8)1-6(13,5(11)12)2-4(9)10/h13H,1-2H2,(H,7,8)(H,9,10)(H,11,12)
+
+
+ InChI=1S/C6H8O7/c7-3(8)1-2(5(10)11)4(9)6(12)13/h2,4,9H,1H2,(H,7,8)(H,10,11)(H,12,13)
+
+
+ InChI=1S/C4H6O4/c5-3(6)1-2-4(7)8/h1-2H2,(H,5,6)(H,7,8)
+
+
+ InChI=1S/C4H4O4/c5-3(6)1-2-4(7)8/h1-2H,(H,5,6)(H,7,8)/b2-1+
+
+
+ InChI=1S/C4H6O5/c5-2(4(8)9)1-3(6)7/h2,5H,1H2,(H,6,7)(H,8,9)/t2-/m0/s1
+
+
+ InChI=1S/C5H11O8P/c6-1-3(7)5(9)4(8)2-13-14(10,11)12/h4-6,8-9H,1-2H2,(H2,10,11,12)/t4-,5+/m1/s1
+
+
+ InChI=1S/C5H11O8P/c6-1-3(7)5(9)4(8)2-13-14(10,11)12/h4-6,8-9H,1-2H2,(H2,10,11,12)/t4-,5-/m1/s1
+
+
+ InChI=1S/C5H11O8P/c6-3-2(1-12-14(9,10)11)13-5(8)4(3)7/h2-8H,1H2,(H2,9,10,11)/t2-,3-,4-,5?/m1/s1
+
+
+ InChI=1S/C7H15O10P/c8-1-3(9)5(11)7(13)6(12)4(10)2-17-18(14,15)16/h4-8,10-13H,1-2H2,(H2,14,15,16)/t4-,5-,6-,7+/m1/s1
+
+
+ InChI=1S/C4H9O7P/c5-1-3(6)4(7)2-11-12(8,9)10/h1,3-4,6-7H,2H2,(H2,8,9,10)/t3-,4+/m0/s1
+
+
+ InChI=1S/C6H14O12P2/c7-4-3(1-16-19(10,11)12)18-6(9,5(4)8)2-17-20(13,14)15/h3-5,7-9H,1-2H2,(H2,10,11,12)(H2,13,14,15)/t3-,4-,5+,6?/m1/s1
+
+
+ InChI=1S/C5H6O5/c6-3(5(9)10)1-2-4(7)8/h1-2H2,(H,7,8)(H,9,10)
+
+
+ InChI=1S/C5H9NO4/c6-3(5(9)10)1-2-4(7)8/h3H,1-2,6H2,(H,7,8)(H,9,10)/t3-/m0/s1
+
+
+ InChI=1S/C5H10N2O3/c6-3(5(9)10)1-2-4(7)8/h3H,1-2,6H2,(H2,7,8)(H,9,10)/t3-/m0/s1
+
+
+ InChI=1S/C5H9NO2/c7-5(8)4-2-1-3-6-4/h4,6H,1-3H2,(H,7,8)/t4-/m0/s1
+
+
+ InChI=1S/C4H7NO4/c5-2(4(8)9)1-3(6)7/h2H,1,5H2,(H,6,7)(H,8,9)/t2-/m0/s1
+
+
+ InChI=1S/C4H8N2O3/c5-2(4(8)9)1-3(6)7/h2H,1,5H2,(H2,6,7)(H,8,9)/t2-/m0/s1
+
+
+ InChI=1S/C4H9NO3/c1-2(6)3(5)4(7)8/h2-3,6H,5H2,1H3,(H,7,8)/t2-,3+/m1/s1
+
+
+ InChI=1S/C6H13NO2/c1-3-4(2)5(7)6(8)9/h4-5H,3,7H2,1-2H3,(H,8,9)/t4-,5-/m0/s1
+
+
+ InChI=1S/C5H11NO2S/c1-9-3-2-4(6)5(7)8/h4H,2-3,6H2,1H3,(H,7,8)/t4-/m0/s1
+
+
+ InChI=1S/C3H7O6P/c4-1-3(5)2-9-10(6,7)8/h4H,1-2H2,(H2,6,7,8)
+
+
+ InChI=1S/C3H7NO3/c4-2(1-5)3(6)7/h2,5H,1,4H2,(H,6,7)/t2-/m0/s1
+
+
+ InChI=1S/C9H11NO3/c10-8(9(12)13)5-6-1-3-7(11)4-2-6/h1-4,8,11H,5,10H2,(H,12,13)/t8-/m0/s1
+
+
+ InChI=1S/C9H11NO2/c10-8(9(11)12)6-7-4-2-1-3-5-7/h1-5,8H,6,10H2,(H,11,12)/t8-/m0/s1
+
+
+ InChI=1S/CO2/c2-1-3
+
+
+ InChI=1S/C2H5NO2/c3-1-2(4)5/h1,3H2,(H,4,5)
+
+
+ BM.ext
+
+
+
+
+
+
+
+
+ InChI=1S/C6H14N2O2/c7-4-2-1-3-5(8)6(9)10/h5H,1-4,7-8H2,(H,9,10)/t5-/m0/s1
+
+
+
+
+
+ Gluc.ext
+
+
+
+
+
+ 6PG
+
+
+
+
+
+ TK-C2
+
+
+ TA-C3
+
+
+ InChI=1S/C6H11O9P/c7-3(1-4(8)6(10)11)5(9)2-15-16(12,13)14/h3,5,7,9H,1-2H2,(H,10,11)(H2,12,13,14)
+
+
+ InChI=1S/C25H40N7O19P3S/c1-25(2,20(38)23(39)28-6-5-14(33)27-7-8-55-16(36)4-3-15(34)35)10-48-54(45,46)51-53(43,44)47-9-13-19(50-52(40,41)42)18(37)24(49-13)32-12-31-17-21(26)29-11-30-22(17)32/h11-13,18-20,24,37-38H,3-10H2,1-2H3,(H,27,33)(H,28,39)(H,34,35)(H,43,44)(H,45,46)(H2,26,29,30)(H2,40,41,42)
+
+
+
+
+
+ InChI=1S/C2H2O3/c3-1-2(4)5/h1H,(H,4,5)/p-1
+
+
+ InChI=1S/C2H4O2/c1-2(3)4/h1H3,(H,3,4)
+
+
+
+
+
+ InChI=1S/C3H7NO2S/c4-2(1-7)3(5)6/h2,7H,1,4H2,(H,5,6)
+
+
+
+
+
+ InChI=1S/C5H11NO2/c1-3(2)4(6)5(7)8/h3-4H,6H2,1-2H3,(H,7,8)
+
+
+ InChI=1S/C3H7NO2/c1-2(4)3(5)6/h2H,4H2,1H3,(H,5,6)
+
+
+ InChI=1S/C6H14N4O2/c7-4(5(11)12)2-1-3-10-6(8)9/h4H,1-3,7H2,(H,11,12)(H4,8,9,10)
+
+
+
+
+
+ InChI=1S/C11H12N2O2/c12-9(11(14)15)5-7-6-13-10-4-2-1-3-8(7)10/h1-4,6,9,13H,5,12H2,(H,14,15)
+
+
+ InChI=1S/C6H9N3O2/c7-5(6(10)11)1-4-2-8-3-9-4/h2-3,5H,1,7H2,(H,8,9)(H,10,11)
+
+
+ LL-DAP
+
+
+
+
+
+ InChI=1S/C6H13NO2/c1-4(2)3-5(7)6(8)9/h4-5H,3,7H2,1-2H3,(H,8,9)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ O2.ext
+
+
+ NH3.ext
+
+
+ SO4.ext
+
+
+ NADH.ext
+
+
+
+
+
+
+ Glycolysis
+
+
+
+
+ Glycolysis
+
+
+
+
+ Glycolysis
+
+
+
+
+
+ Glycolysis
+
+
+
+
+
+ Glycolysis
+
+
+
+
+ Glycolysis
+
+
+
+
+
+
+ Glycolysis
+
+
+
+
+ Glycolysis
+
+
+
+
+
+ TCA
+
+
+
+
+
+
+ TCA
+
+
+
+
+
+ TCA
+
+
+
+
+
+
+ TCA
+
+
+
+
+
+ TCA
+
+
+
+
+
+ TCA
+
+
+
+
+
+ TCA
+
+
+
+
+
+ TCA
+
+
+
+
+ TCA
+
+
+
+
+ TCA
+
+
+
+
+
+ PPP
+
+
+
+
+
+ PPP
+
+
+
+
+ PPP
+
+
+
+
+ Amphibolic R.
+
+
+
+
+
+ Amphibolic R.
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ UPT
+
+
+
+
+ PPP
+
+
+
+
+
+
+ PPP
+
+
+
+
+
+ PPP
+
+
+
+
+
+ PPP
+
+
+
+
+
+ Biomass
+
+
+
+ PPP
+
+
+
+
+
+ PPP
+
+
+
+
+
+ EDP
+
+
+
+
+ EDP
+
+
+
+
+
+ TCA
+
+
+
+
+ TCA
+
+
+
+
+
+
+ Glyox
+
+
+
+
+
+ Glyox
+
+
+
+
+
+ Glyox
+
+
+
+
+
+ Amphibolic R.
+
+
+
+
+
+
+ Amphibolic R.
+
+
+
+
+
+
+ Acetic Acid Formation
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Biomass
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+ Amino Acid Biosynthesis
+
+
+
+
+
+
+
+
+
+
+
+
+ Biomass
+
+
+
+ One Carbon Metabolism
+
+
+
+
+
+ One Carbon Metabolism
+
+
+
+
+
+ Oxidative Phosphorylation
+
+
+
+
+
+
+
+ Oxidative Phosphorylation
+
+
+
+
+
+
+
+
+
+ Transport
+
+
+
+
+ Transport
+
+
+
+ ATP Hydrolysis
+
+
+
+ Transport
+
+
+
+ CO2 Exchange
+
+
+
+
+
+
+ Transport
+
+
+
+
+ Transhydrogenation
+
+
+
+
+ Transport
+
+
+
+
+ Biomass
+
+
+
+ CO2 Exchange
+
+
+
+
+ CO2 Exchange
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+ Biomass
+
+
+
+
+ Biomass
+
+
+
+
+ Biomass
+
+
+
+
+
+
+ TCA7_v26___1/0.5=TCA7_v26___2/0.5;
+ AA15_v51___1/0.5=AA15_v51___2/0.5;
+ TCA6_v25___1/0.5=TCA6_v25___2/0.5;
+ AA12_v48___1/0.25=AA12_v48___2/0.25;
+ AA12_v48___2/0.25=AA12_v48___3/0.25;
+ AA12_v48___3/0.25=AA12_v48___4/0.25;
+ TCA8_v27___1/0.5=TCA8_v27___2/0.5;
+ GOX1_v29___1/0.5=GOX1_v29___2/0.5;
+ AA13_v49___1/0.5=AA13_v49___2/0.5
+
+
+
+
+ TCA7_v26___1/0.5=TCA7_v26___2/0.5;
+ TCA6_v25___1/0.5=TCA6_v25___2/0.5;
+ TCA8_v27___1/0.5=TCA8_v27___2/0.5;
+ GOX1_v29___1/0.5=GOX1_v29___2/0.5
+
+
+
+
+ Substrate see Crown 2015
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0=488*mu2_v70___1-Ala_bm___1;
+ 0=281*mu2_v70___1-Arg_bm___1;
+ 0=229*mu2_v70___1-Asn_bm___1;
+ 0=229*mu2_v70___1-Asp_bm___1;
+ 0=87*mu2_v70___1-Cys_bm___1;
+ 0=250*mu2_v70___1-Glu_bm___1;
+ 0=250*mu2_v70___1-Gln_bm___1;
+ 0=582*mu2_v70___1-Gly_bm___1;
+ 0=90*mu2_v70___1-His_bm___1;
+ 0=276*mu2_v70___1-Ile_bm___1;
+ 0=428*mu2_v70___1-Leu_bm___1;
+ 0=326*mu2_v70___1-Lys_bm___1;
+ 0=146*mu2_v70___1-Met_bm___1;
+ 0=176*mu2_v70___1-Phe_bm___1;
+ 0=210*mu2_v70___1-Pro_bm___1;
+ 0=205*mu2_v70___1-Ser_bm___1;
+ 0=241*mu2_v70___1-Thr_bm___1;
+ 0=54*mu2_v70___1-Trp_bm___1;
+ 0=131*mu2_v70___1-Tyr_bm___1;
+ 0=402*mu2_v70___1-Val_bm___1;
+ 0=205*mu2_v70___1-G6P_bm___1;
+ 0=71*mu2_v70___1-F6P_bm___1;
+ 0=754*mu2_v70___1-R5P_bm___1;
+ 0=129*mu2_v70___1-GAP_bm___1;
+ 0=619*mu2_v70___1-_3PG_bm___1;
+ 0=51*mu2_v70___1-PEP_bm___1;
+ 0=83*mu2_v70___1-Pyr_bm___1;
+ 0=2510*mu2_v70___1-AcCoA_bm___1;
+ 0=87*mu2_v70___1-AKG_bm___1;
+ 0=340*mu2_v70___1-OAC_bm___1;
+ 0=443*mu2_v70___1-MEETHF_bm___1;
+ 0=33247*mu2_v70___1-ATP_bm___1;
+ 0=5363*mu2_v70___1-NADPH_bm___1;
+ 0=1455*mu2_v70___1-NADH_bm___1;
+ AC1_v35___1<=6542.235;
+ AC1_v35___1>=4792.4618;
+ ANA1_v31___1<=878.533;
+ ANA1_v31___1>=6.14313E-7;
+ ANA3_v33___1<=3896.9782;
+ ANA3_v33___1>=1426.2412;
+ ATPH1_v64___1<=55487.63;
+ ATPH1_v64___1>=14468.296;
+ EMP8_v8<=3347.7908;
+ EMP8_v8>=1101.9954;
+ Glc_upt___1<=8616.77;
+ Glc_upt___1>=8583.1612;
+ TCA1_v20<=10551.942;
+ TCA1_v20>=7831.0998;
+ TCA9_v28___1<=2835.119;
+ TCA9_v28___1>=0;
+ TH1_v63___1<=10918.302;
+ TH1_v63___1>=1462.7568;
+ mu2_v70___1<=0.77191708;
+ mu2_v70___1>=0.55134342;
+ GAS2_v71___1>=8.6E-7;
+ EMP1_v1=Glc_upt___1
+
+
+
+ AA8_v44___1>=8.6E-9;
+ AA8_v44___1<=1077.6918;
+ GOX1_v29___1+GOX1_v29___2>=8.6E-7;
+ GOX1_v29___1+GOX1_v29___2<=3385.7598;
+ PPP6_v14___1<=1868.0834;
+ AA9_v45___1>=8.59999E-7;
+ AC1_v35___1>=8.6E-7;
+ EMP2_v2>=8.6E-7;
+ EMP4_v4>=8.6E-7;
+ EMP5_v5>=8.6E-7;
+ EMP6_v6>=8.6E-7;
+ EMP7_v7>=8.6E-7;
+ PPP3_v11___1>=8.6E-7;
+ PPP4_v12___1>=8.6E-7;
+ PPP5_v13___1>=8.6E-7;
+ PPP6_v14___1>=8.6E-7;
+ PPP7_v15___1>=8.6E-7;
+ PPP8_v16___1>=4.26035E-9;
+ PPP9_v17___1>=8.6E-7;
+ TCA3_v22___1>=8.6E-7;
+ TCA4_v23>=86000;
+ TCA6_v25___1+TCA6_v25___2>=8.6E-7;
+ TCA7_v26___1+TCA7_v26___2>=8.6E-7;
+ TCA8_v27___1+TCA8_v27___2>=1935.0516;
+ TCA9_v28___1>=2082.1976;
+ TH1_v63___1>=8.6E-7
+
+
+
+
+
+
+
+ Ala[1-2,4]#M(2,0),(0,0),(1,0),(2,1),(0,1),(1,1)
+
+
+ Ala[1-4]#M(3,0),(0,0),(1,0),(2,0),(3,1),(0,1),(1,1),(2,1)
+
+
+ Asp[1-3,5]#M(3,0),(0,0),(1,0),(2,0),(3,1),(0,1),(1,1),(2,1)
+
+
+ Asp[1-5]#M(4,0),(0,0),(1,0),(2,0),(3,0),(4,1),(0,1),(1,1),(2,1),(3,1)
+
+
+ Glu[1-6]#M(5,0),(0,0),(1,0),(2,0),(3,0),(4,0),(5,1),(0,1),(1,1),(2,1),(3,1),(4,1)
+
+
+ Tyr[8-10]#M(2,0),(0,0),(1,0),(2,1),(0,1),(1,1)
+
+
+ Gly[1,3]#M(1,0),(0,0),(1,1),(0,1)
+
+
+ Val[1-4,6]#M(4,0),(0,0),(1,0),(2,0),(3,0),(4,1),(0,1),(1,1),(2,1),(3,1)
+
+
+ Val[1-6]#M(5,0),(0,0),(1,0),(2,0),(3,0),(4,0),(5,1),(0,1),(1,1),(2,1),(3,1),(4,1)
+
+
+ Leu[1-5,7]#M(5,0),(0,0),(1,0),(2,0),(3,0),(4,0),(5,1),(0,1),(1,1),(2,1),(3,1),(4,1)
+
+
+ Ile[1-5]#M(5,0),(0,0),(1,0),(2,0),(3,0),(4,0)
+
+
+ Ser[1-2,4]#M(2,0),(0,0),(1,0),(2,1),(0,1),(1,1)
+
+
+ Phe[8-10]#M(2,0),(0,0),(1,0),(2,1),(0,1),(1,1)
+
+
+ Phe[1-8,10]#M(8,0),(0,0),(1,0),(2,0),(3,0),(4,0),(5,0),(6,0),(7,0),(8,1),(0,1),(1,1),(2,1),(3,1),(4,1),(5,1),(6,1),(7,1)
+
+
+
+
+ AC1_v35___1
+
+
+
+
+
+ .007607329468886703
+ .010621641553208772
+ .001771028977904654
+ .3727591439754452
+ .5204604361072263
+ .08678041991732832
+
+
+ .00014620878262863995
+ .010141424430966973
+ .0017753597796153564
+ .007937007006789159
+ .007164230348803321
+ .49692979711737834
+ .08699262920115292
+ .38891334333266536
+
+
+ .0012754766723170954
+ .008323311867518814
+ .003941254734413251
+ .006459956725750858
+ .062498356943537454
+ .4078422815084274
+ .19312148198624773
+ .3165378795617874
+
+
+ .0002068057726927336
+ .007365789015676524
+ .003970226215731391
+ .006350833753619705
+ .002106345242279664
+ .010133482861943878
+ .3609236617681581
+ .19454108457083172
+ .3111908539273636
+ .10321091687170274
+
+
+ .0004558839079563913
+ .004391158610875686
+ .003036417539473781
+ .006840371561993859
+ .0028212956305863138
+ .0024548727491140974
+ .022338311489863056
+ .21516677193290326
+ .14878445943421847
+ .33517820653769537
+ .13824348589873003
+ .12028876470658978
+
+
+ .0003404240879194727
+ .010883042140997053
+ .008776533771083603
+ .016680780308054047
+ .533269064908853
+ .43005015478309283
+
+
+ .008465018120598133
+ .011534981879401995
+ .4147858879093052
+ .5652141120906946
+
+
+ .0028935730824095895
+ .005640963464242477
+ .001881123498364834
+ .008237059861596646
+ .0013472800933865825
+ .14178508103806806
+ .27640720974788213
+ .09217505141987933
+ .4036159332182256
+ .06601672457594485
+
+
+ 5.5612919035044703e-05
+ .00538592875723418
+ .0018408995875764883
+ .008229870698875513
+ .0014557696728605685
+ .0030319183644183303
+ .002725033032717175
+ .26391050910447583
+ .09020407979125003
+ .4032636642448904
+ .07133271397017008
+ .1485639998564963
+
+
+ .0011956859664649724
+ .003309997333634386
+ .003434769373315487
+ .005610651716089654
+ .004194283692833423
+ .0022546119176623178
+ .058588612356783656
+ .16218986934807766
+ .16830369929246392
+ .27492193408838006
+ .2055199009488448
+ .11047598396544962
+
+
+ .02425742819048818
+ .22101808798088532
+ .14150855392084438
+ .34728399195656173
+ .13742712466409243
+ .128504813287128
+
+
+ .0076687917451283005
+ .01069789497275897
+ .0016333132821124141
+ .37577079551128895
+ .5241968536652131
+ .08003235082349841
+
+
+ .0003404240879194727
+ .010883042140997053
+ .008776533771083603
+ .016680780308054047
+ .533269064908853
+ .43005015478309283
+
+
+ 3.2933397524350044e-06
+ .0014194906768259972
+ .003143086020501046
+ .00424861803178862
+ .004911111440072046
+ .0033605104243444543
+ .002039623263858634
+ .0007481485988134178
+ .00012611820404348002
+ .00016137364786931538
+ .06955504316446087
+ .15401121500457676
+ .20818228355762744
+ .24064446056352892
+ .164665010792876
+ .09994153992907301
+ .03665928134185707
+ .006179791998130524
+
+
+ 5658.8
+
+
+
+
+ 1005.856
+ 0.6966
+ 5658.8
+ 2287.6
+ 9133.2
+ 6.1431348E-7
+ 2373.6
+ 7129.400000000001
+ 1126.6
+ 27571.600000000002
+ 8600.0
+ 72445.712
+ 14103.054
+ 34585.932
+ 2383.3352
+ 18.741206000000002
+ 5532.294
+ 65867.916
+ 45314.776000000005
+ 265.31086
+ 0.0011562786
+ 4.2611194E-9
+ 47755.799999999996
+ 40030.506
+ 86086.0
+ 16073.4
+ 3044.4
+ 10251.2
+ 16463.667999999998
+ 22.554403
+ 29330.558
+ 8.600000000000001E-9
+ 5939.572800000001
+ 26218.046
+
+
+
+
diff --git a/data/FluxML-Test/fluxml-model/models/spirallus_model_level_1.fml b/data/FluxML-Test/fluxml-model/models/spirallus_model_level_1.fml
new file mode 100644
index 0000000..b45efc6
--- /dev/null
+++ b/data/FluxML-Test/fluxml-model/models/spirallus_model_level_1.fml
@@ -0,0 +1,124 @@
+
+
+ 1.1
+ KN: Spirallus-Example With MS measurements
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ v1<= 100;
+ v1>= 1;
+
+ v2<= 70;
+ v2>= 0.7;
+
+ v2<=v1;
+
+
+
+
+
+ v2<= 1;
+ v2>= 0.01;
+
+
+
+
+ spirallus synthetic measurement data
+
+
+
+
+
+
+
+
+
+
+ C[1-2,4]#M0,1,2,3
+
+
+ B[1-2]#M0,1,2
+
+
+ E[1-2]#M0,1,2
+
+
+
+
+
+
+ .00011259286173070926
+ .011450020321709498
+ .18418998165634448
+ .8042474051602153
+
+
+ .0012805904564378023
+ .1169440049278323
+ .8817754046157299
+
+
+ .0017834415975322981
+ .11983876724741327
+ .8783777911550544
+
+
+
+
+
+ 1.0
+ .7
+ 0.4
+
+
+
+
diff --git a/data/FluxML-Test/fluxml-model/models/spirallus_model_level_2.fml b/data/FluxML-Test/fluxml-model/models/spirallus_model_level_2.fml
new file mode 100644
index 0000000..c4ebc4a
--- /dev/null
+++ b/data/FluxML-Test/fluxml-model/models/spirallus_model_level_2.fml
@@ -0,0 +1,226 @@
+
+
+
+ 1.1
+ KN: Spirallus-Example With MS measurements
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ v1<= 100;
+ v1>= 1;
+
+ v2<= 70;
+ v2>= 0.7;
+
+ v2<=v1;
+
+
+
+
+
+ v2<= 1;
+ v2>= 0.01;
+
+
+
+
+
+ B<= 100;
+ B>= 0.0001;
+
+ C<= 100;
+ C>= 0.0001;
+
+ D<= 100;
+ D>= 0.0001;
+
+ E<= 100;
+ E>= 0.0001;
+
+ F<= 100;
+ F>= 0.0001;
+
+ G<= 100;
+ G>= 0.0001;
+
+ H<= 100;
+ H>= 0.0001;
+
+
+
+
+
+
+ spirallus synthetic measurement data
+
+
+
+
+
+
+
+
+
+
+ C[1-2,4]#M0,1,2,3
+
+
+
+ B[1,2]#M0,1,2
+
+
+ E[1,2]#M0,1,2
+
+
+
+
+ .3944812502157005
+ .39213595942762747
+ .18793504481625237
+ .03259579734377384
+
+ .06887428333759157
+ .3159806346776769
+ .4676055999539298
+ .12300796473248234
+
+ .022363373519724637
+ .208132993570771
+ .5502226538801799
+ .19099405118352722
+
+ .022363373519724637
+ .208132993570771
+ .5502226538801799
+ .19099405118352722
+
+ .022363373519724637
+ .208132993570771
+ .5502226538801799
+ .19099405118352722
+
+ .022363373519724637
+ .208132993570771
+ .5502226538801799
+ .19099405118352722
+
+
+ .2519855430395166
+ .6150958770328989
+ .07159673646341977
+
+
+ .12074937271998513
+ .7357157241336526
+ .09622202129460586
+
+ .08553267809239648
+ .7647670399774833
+ .11344858197054312
+
+ .08553267809239648
+ .7647670399774833
+ .11344858197054312
+
+
+ .08553267809239648
+ .7647670399774833
+ .11344858197054312
+
+ .08553267809239648
+ .7647670399774833
+ .11344858197054312
+
+
+ .5336075626878626
+ .408439833891252
+ .04662684589445842
+
+ .2416273255910798
+ .6548831029532279
+ .11408956597276508
+
+ .15466795662924404
+ .699743719755679
+ .15898170825561733
+
+ .15466795662924404
+ .699743719755679
+ .15898170825561733
+
+ .15466795662924404
+ .699743719755679
+ .15898170825561733
+
+ .15466795662924404
+ .699743719755679
+ .15898170825561733
+
+
+
+
+
+ 1.0
+ .7
+ 0.4
+ 1
+ 0.1
+ 20
+ 0.1
+ 1
+ 1
+ 1
+
+
+
+
diff --git a/data/FluxML-Test/fluxml-model/models/spirallus_model_level_3.fml b/data/FluxML-Test/fluxml-model/models/spirallus_model_level_3.fml
new file mode 100644
index 0000000..6577de3
--- /dev/null
+++ b/data/FluxML-Test/fluxml-model/models/spirallus_model_level_3.fml
@@ -0,0 +1,123 @@
+
+
+ 1.1
+ KN: CN-Spirallus-Example With MS measurements
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ v1<= 100;
+ v1>= 1;
+
+ v2<= 70;
+ v2>= 0.7;
+
+ v2<=v1;
+
+
+
+
+
+ v2<= 1;
+ v2>= 0.01;
+
+
+
+
+
+
+ spirallus synthetic CN measurement data
+
+
+
+
+
+
+
+
+
+ B[1-2]#M(0,0),(0,1),(1,0),(1,1)
+
+
+
+ C[1-4]#M(0,0),(1,1),(2,2),(2,1)
+
+
+
+
+ .1244812502157005
+ .56213595942762747
+ .07898954578158745
+ .1204887542762747
+
+
+ .1244812502157005
+ .56213595942762747
+ .07898954578158745
+ .1204887542762747
+
+
+
+
+ 1.0
+ 0.7
+ 0.4
+
+
+
+
+
diff --git a/data/FluxML-Test/fluxml-model/programs/readFluxMLModel.cpp b/data/FluxML-Test/fluxml-model/programs/readFluxMLModel.cpp
new file mode 100644
index 0000000..055ccc0
--- /dev/null
+++ b/data/FluxML-Test/fluxml-model/programs/readFluxMLModel.cpp
@@ -0,0 +1,141 @@
+#include
+
+#include
+#include
+#include
+#include
+
+using namespace std;
+using namespace flux;
+
+std::string make_section_head(const std::string& section_name)
+{
+ std::string line(80, '=');
+ std::string fill((line.length() - section_name.length()) / 2, ' ');
+ std::string header = line + "\n" + fill + section_name + "\n" + line;
+ return header;
+}
+
+int main(int argc, char* argv[])
+{
+
+ /** check number of specified arguments **/
+ if (argc != 2)
+ {
+ cout << endl << "Usage: readFluxML filename" << endl << endl;
+ return 1;
+ }
+
+ /** make sure error Messages go somewhere **/
+ PUBLISHLOG(stderr_log);
+
+ const char* filename = argv[1];
+ xml::FluxMLDocument * fml = 0;
+ try
+ {
+ fml = new xml::FluxMLDocument(filename);
+ }
+ catch (xml::XMLException& e)
+ {
+ cerr << "Problem with opening File " << filename << ":" << endl << e.toString() << endl;
+ exit(1);
+ }
+ cout << make_section_head("metabolites") << endl;
+ /** print network metabolites **/
+ charptr_map * poolMap = fml->getPoolMap();
+ charptr_map::const_iterator pi;
+ for (pi = poolMap->begin(); pi != poolMap->end(); ++pi)
+ {
+ data::Pool* pool = pi->value;
+ cout << "pool: " << pool->getName() << "\t atoms: "
+ << pool->getNumAtoms() << endl;
+ }
+
+ /** print network reactions **/
+ cout << endl << make_section_head("reactions") << endl;
+ const std::list* reactionList = fml->getReactions();
+ std::list::const_iterator ri;
+ for (ri = reactionList->begin(); ri != reactionList->end(); ++ri)
+ {
+ data::IsoReaction* reaction = *ri;
+ cout << "reaction: " << reaction->getName() << "\t atoms: "
+ << reaction->getNumAtoms() << endl;
+
+ /** print reaction educts **/
+ const std::list educts =
+ reaction->getEducts();
+ std::list::const_iterator ei;
+ for (ei = educts.begin(); ei != educts.end(); ++ei)
+ cout << "educt-name: " << (*ei)->name << "\t educt-cfg: "
+ << (*ei)->atom_cfg << endl;
+
+ /** print reaction products **/
+ const std::list products =
+ reaction->getProducts();
+ for (ei = products.begin(); ei != products.end(); ++ei)
+ cout << "educt-name: " << (*ei)->name << "\t educt-cfg: "
+ << (*ei)->atom_cfg << endl;
+ }
+
+ cout << endl << make_section_head("configuration default") << endl;
+ /** select "default" configuration**/
+ data::Configuration * cfg = fml->getConfiguration("default");
+ if (cfg == 0)
+ {
+ fWARNING("aborting: \"default\" configuration not found.");
+ delete fml;
+ xml::framework::terminate();
+ return EXIT_FAILURE;
+ }
+
+ cout << endl << make_section_head("constraints") << endl;
+ /** print model constraints **/
+ charptr_array cons;
+ charptr_array::const_iterator consi;
+
+ /** netflux constraints **/
+ cons = cfg->getConstraintFluxesNet();
+ for (consi = cons.begin(); consi != cons.end(); ++consi)
+ cout << "net: " << *consi << endl;
+
+ /** xchflux constraints **/
+ cons = cfg->getConstraintFluxesXch();
+ for (consi = cons.begin(); consi != cons.end(); ++consi)
+ cout << "xch: " << *consi << endl;
+
+ /** poolsize constraints **/
+ cons = cfg->getConstraintPools();
+ for (consi = cons.begin(); consi != cons.end(); ++consi)
+ cout << "pool: " << *consi << endl;
+
+ cout << endl << make_section_head("input tracers") << endl;
+ /** print tracer specification **/
+ list const & IPL = cfg->getInputPools();
+ list::const_iterator ip;
+ cout << "tracers: " << endl;
+ for (ip = IPL.begin(); ip != IPL.end(); ++ip)
+ {
+ data::InputPool & I = *(*ip);
+ cout << "pool: " << I.getName() << "\t cost: " << I.getCost() << endl;
+ }
+
+ cout << endl << make_section_head("measurement data") << endl;
+ /** print experimental data **/
+ xml::MMDocument * mmdoc = 0;
+ mmdoc = cfg->getMMDocument();
+
+ charptr_array mgroup_names = mmdoc->getGroupNames();
+ charptr_array::const_iterator mgi;
+
+ for (mgi = mgroup_names.begin(); mgi != mgroup_names.end(); mgi++)
+ {
+ xml::MGroup * mgroups = mmdoc->getGroupByName(*mgi);
+ cout << "group-id: " << mgroups->getGroupId() << "\t group-spec: "
+ << mgroups->getSpec() << endl;
+ }
+
+ /** free allocated memory **/
+ delete fml;
+ xml::framework::terminate();
+ return EXIT_SUCCESS;
+}
diff --git a/data/FluxML-Test/fmicb-10-01022.pdf b/data/FluxML-Test/fmicb-10-01022.pdf
new file mode 100644
index 0000000..e69ffd5
Binary files /dev/null and b/data/FluxML-Test/fmicb-10-01022.pdf differ
diff --git a/data/FluxML-Test/ppp_tca.xml b/data/FluxML-Test/ppp_tca.xml
new file mode 100644
index 0000000..3bf2581
--- /dev/null
+++ b/data/FluxML-Test/ppp_tca.xml
@@ -0,0 +1,617 @@
+
+
+
+
+
+
+
+
+ PPP, TCA, Trallalla
+ 1.0
+ 2006-03-08
+
+ Konvertiert aus FTBL-Datei.
+
+ Eckdaten:
+ Anzahl der Pools : 50
+ Anzahl der Reaktionen : 54 (3 input, 14 output, 37 innere)
+ Anzahl der Gleichungen : 2
+ Anzahl der Ungleichungen: 12
+
+ Dimension der Matrizen auf den Cumomer-Ebenen:
+ Ebene 1: 145; ff: 0.021974; 6 SCCs: {1:1x,2:2x,3:1x,44:1x,93:1x}
+ Ebene 2: 281; ff: 0.009397; 86 SCCs: {1:33x,2:19x,3:22x,4:8x,6:1x,32:1x,35:1x,39:1x}
+ Ebene 3: 310; ff: 0.007680; 134 SCCs: {1:56x,2:28x,3:38x,4:10x,12:1x,32:1x}
+ Ebene 4: 207; ff: 0.010502; 107 SCCs: {1:51x,2:20x,3:30x,4:5x,6:1x}
+ Ebene 5: 81; ff: 0.024844; 47 SCCs: {1:27x,2:7x,3:12x,4:1x}
+ Ebene 6: 16; ff: 0.109375; 11 SCCs: {1:8x,2:1x,3:2x}
+ Ebene 7: 1; ff: 1.000000; 1 SCCs: {1:1x}
+
+ Folgende Fluesse koennen als irreversibel angenommen werden:
+ ppp1, tcc1, tcc2, tcc3, tcc4
+ falls nötig:
+ emp5, emp6, emp2, ppp2 (im Notfall: gs1, gs2)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ experimental parameter set #1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0.182489
+ 0.213743
+
+ 0.658565
+
+ 0.01211231
+ 0.004172662
+ 0.05210032
+ 0.020921263
+ 0.007186251
+ 0.09353717
+ 0.044914069
+ 0.172006395
+ 0.141233014
+ 0.074702238
+ 0.094638289
+ 0.127118306
+ 0.0619781
+
+ 0.900576
+ 0
+ 0.0452776
+ 0.969967
+ 0.959334
+ 0
+ 0.958239
+ 0.959762
+ 2.15E-05
+
+ 0.06585941.12E-09
+ 0
+ 0.343549
+ 0.0566556
+
+ 0.1951550
+ 0.120137
+ 0.837387
+ 0.0828023
+ 0.103952
+ 0.0940764
+ 0.158751
+
+ 0
+ 0
+ 0.0264616
+ 0.95892
+ 0
+ 0
+ 0.562319
+ 0.938797
+ 0.952093
+ 0.0658212
+
+ 0.02524190
+ 0
+
+ 0.900778
+ 0.911451
+
+ 0.215355.69E-06
+ 0
+
+
+
+
diff --git a/data/FluxML-Test/spirale.fml b/data/FluxML-Test/spirale.fml
new file mode 100644
index 0000000..33a06ca
--- /dev/null
+++ b/data/FluxML-Test/spirale.fml
@@ -0,0 +1,166 @@
+
+
+
+ Standard-Spirale
+ 1.0
+ 2006-10-30 21:31:12
+ Standard-Mini-Sprialnetzwerk
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Eine Belegung for die Input-Pools
+
+
+
+
+
+
+
+
+
+ v1=1;v8=0.2
+
+
+ v2=0.04;v3=0.03;v4=0.02;v5=0.01;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Eine Belegung für die Input-Pools
+
+
+
+
+
+
+
+
+
+ v1=1;v3=0.2
+
+
+ v2=10;v5=0.01;
+
+
+
+
+
+
+ 0.03
+ 0.02
+ 0.2
+
+
+
+
+
+
diff --git a/data/FluxML-Test/spirale.mm b/data/FluxML-Test/spirale.mm
new file mode 100644
index 0000000..89a4c37
--- /dev/null
+++ b/data/FluxML-Test/spirale.mm
@@ -0,0 +1,37 @@
+
+
+
+
+ 2007-08-07 11:11:11
+ 1.1
+ Messmodell zum Spiral-Netzwerk
+ gigamol/year
+ gigamol
+
+
+
+
+
+
+
+
+ 2007-08-07 11:11:11
+ 2007-08-07 11:11:12
+ Michael
+ Bacillus Spiralus Vulgaris
+ Messwerte für MS-Messung
+
+
+ 4.321
+ 1.2
+ 1.23
+ 5.301
+ 2.219
+ 1.27
+ 3.1
+ 3.88
+ 3.36
+
+
diff --git a/data/FluxML-Test/spirale.xml b/data/FluxML-Test/spirale.xml
new file mode 100644
index 0000000..0e46e16
--- /dev/null
+++ b/data/FluxML-Test/spirale.xml
@@ -0,0 +1,114 @@
+
+
+
+
+ Standard-Spirale
+ 1.0
+ 2006-10-30
+ Standard-Mini-Sprialnetzwerk
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Eine Belegung für die Input-Pools测试øø
+
+
+
+
+
+
+
+ 1
+
+
+
+
diff --git a/data/FluxML-Test/spirale2.fml b/data/FluxML-Test/spirale2.fml
new file mode 100644
index 0000000..682c838
--- /dev/null
+++ b/data/FluxML-Test/spirale2.fml
@@ -0,0 +1,207 @@
+
+
+
+ Standard-Spirale
+ 1.0
+ 2006-10-30 21:31:12
+ Standard-Mini-Sprialnetzwerk
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Eine Belegung for die Input-Pools
+
+
+
+
+
+
+
+
+
+ v1=1;v8=0.2
+
+
+ v2=0.04;v3=0.03;v4=0.02;v5=0.01;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Eine Belegung für die Input-Pools
+
+
+
+
+
+
+
+
+
+ v1=1
+
+
+ v2=10;v5=0.01;v3=0.2
+
+
+
+
+
+
+
+ 2007-08-07 11:11:11
+ 1.1
+ Messmodell zum Spiral-Netzwerk
+ gigamol/year
+ gigamol
+
+
+
+
+
+
+
+
+
+ 2007-08-07 11:11:11
+ 2007-08-07 11:11:12
+ Michael
+ Bacillus Spiralus Vulgaris
+ Messwerte für MS-Messung
+
+
+ 4.321
+ 1.2
+ 1.23
+ 5.301
+ 2.219
+ 1.27
+ 3.1
+ 3.88
+ 3.36
+
+ 5.31
+ 2.21
+
+
+
+
+
+
+ 0.8
+
+ 0.2
+
+
+
+
+
+
diff --git a/data/FluxML-Test/spiralem.fml b/data/FluxML-Test/spiralem.fml
new file mode 100644
index 0000000..3a368d7
--- /dev/null
+++ b/data/FluxML-Test/spiralem.fml
@@ -0,0 +1,111 @@
+
+
+
+ Spiralus
+ 1.0
+ 2008-08-12 13:26:00
+ Standard-Spirale mit Messungen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ v1=3.14
+
+
+ v5=0.4
+
+
+
+
+
+
+
+ C[1-3]#M0,1,2
+
+
+ B[1-2:2]#M(2,0),(2,1)
+
+
+
+
+
+ 2007-08-07 11:11:11
+ 2007-08-07 11:11:12
+ Michael
+ Bacillus Spiralus Vulgaris
+ Messwerte fuer MS-Messung
+
+ 2
+ 2
+ 1.8
+ 0
+ 2.21
+
+
+
+
+
+ 0.039305
+
+ 0.090391
+
+ 8.4867
+
+ 5.5732
+
+
+
+
+
+
diff --git a/data/FluxML-Test/standard.fml b/data/FluxML-Test/standard.fml
new file mode 100644
index 0000000..24d035c
--- /dev/null
+++ b/data/FluxML-Test/standard.fml
@@ -0,0 +1,723 @@
+
+
+
+
+ Escherichia Coli Netzwerk
+ 1.3
+ 2007-04-09 00:00:00
+ Abgeleitet aus ppp_tca.xml; Biosynthese-Reaktionen aus coryne.xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ tcc7a=tcc7b;
+ tcc8a=tcc8b;
+ tcc9a=tcc9b;
+ lys13a=lys13b;
+
+ 0<=ppp1;
+ 0<=tcc1;
+ 0<=tcc3;
+ 0<=tcc4;
+ 0<=tcc5;
+ 0<=tcc6;
+ 0<=tcc7a;
+ 0<=tcc8a;
+ 0<=tcc9a;
+ 0<=tcc10;
+ 0<=ana2;
+
+
+
+ tcc8a=tcc8b
+
+
+
+
+ realistische Reversibilitäten / unrealistische Flußgrößen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ i_Glc1=1.;
+ i_Glc2=1.;
+ i_Glc3=1.;
+
+
+
+
+ edp1=.27;
+ edp2=.28;
+ edp3=.29;
+ edp4=.30;
+
+ emp1=.37;
+ emp2=.24;
+ emp3=.25;
+ emp4=.38;
+ emp5=.39;
+ emp6=.40;
+ emp7=.41;
+ emp8=.42;
+ emp9=.26;
+
+ ppp1=.31;
+ ppp2=.32;
+ ppp3=.33;
+ ppp4=.34;
+ ppp5=.35;
+
+ ppp_P5P=.36;
+
+ gs2 = 0.1;
+ ana2 = 0.1;
+ ana3 = 0.1;
+ lys15 = 0.1;
+ bs_oaa1=0;
+ bs_pep6=0;
+ bs_rib5p3=0;
+ bs_pep1=0;
+ bs_pep4b=0;
+ bs_pep3b=0;
+
+
+
+
+
diff --git a/data/standard.xml b/data/standard.xml
new file mode 100644
index 0000000..45007f4
--- /dev/null
+++ b/data/standard.xml
@@ -0,0 +1,723 @@
+
+
+
+
+ Escherichia Coli Netzwerk
+ 1.3
+ 2007-04-09 00:00:00
+ Abgeleitet aus ppp_tca.xml; Biosynthese-Reaktionen aus coryne.xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ tcc7a=tcc7b;
+ tcc8a=tcc8b;
+ tcc9a=tcc9b;
+ lys13a=lys13b;
+
+ 0<=ppp1;
+ 0<=tcc1;
+ 0<=tcc3;
+ 0<=tcc4;
+ 0<=tcc5;
+ 0<=tcc6;
+ 0<=tcc7a;
+ 0<=tcc8a;
+ 0<=tcc9a;
+ 0<=tcc10;
+ 0<=ana2;
+
+
+
+ tcc8a=tcc8b
+
+
+
+
+ realistische Reversibilitäten / unrealistische Flußgrößen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ i_Glc1=1.;
+ i_Glc2=1.;
+ i_Glc3=1.;
+
+
+
+
+ edp1=.27;
+ edp2=.28;
+ edp3=.29;
+ edp4=.30;
+
+ emp1=.37;
+ emp2=.24;
+ emp3=.25;
+ emp4=.38;
+ emp5=.39;
+ emp6=.40;
+ emp7=.41;
+ emp8=.42;
+ emp9=.26;
+
+ ppp1=.31;
+ ppp2=.32;
+ ppp3=.33;
+ ppp4=.34;
+ ppp5=.35;
+
+ ppp_P5P=.36;
+
+ gs2 = 0.1;
+ ana2 = 0.1;
+ ana3 = 0.1;
+ lys15 = 0.1;
+ bs_oaa1=0;
+ bs_pep6=0;
+ bs_rib5p3=0;
+ bs_pep1=0;
+ bs_pep4b=0;
+ bs_pep3b=0;
+
+
+
+
+
diff --git a/pyproject.toml b/pyproject.toml
index 5ac6717..5c53039 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,7 +5,14 @@ description = "A library specifying a format for fluxomics data"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
+ "jax>=0.4.23",
+ "jaxlib>=0.4.23",
+ "matplotlib>=3.10.1",
+ "pandas>=2.2.3",
"pydantic>=2.10.4",
+ "sbmlmath>=0.4.0",
+ "seaborn>=0.13.2",
+ "sympy>=1.13.3",
]
authors = [
{name = "Teddy Groves", email = "tedgro@dtu.dk"},
@@ -18,7 +25,11 @@ license = {text = "MIT"}
requires = ["hatchling"]
build-backend = "hatchling.build"
+[tool.mypy]
+plugins = ['pydantic.mypy']
+
[tool.ruff]
+extend-exclude = ["data"]
line-length = 80
[tool.ruff.lint]
@@ -30,6 +41,8 @@ known-first-party = ["fluxomics_data_model"]
[dependency-groups]
dev = [
+ "jupyter>=1.0.0",
+ "ipykernel>=6.29.5",
"mkdocs>=1.6.1",
"mkdocs-material>=9.5.49",
"mkdocstrings>=0.27.0",
@@ -37,4 +50,6 @@ dev = [
"pymdown-extensions>=10.14",
"pytest>=8.3.4",
"pytest-cov>=6.0.0",
+ "mypy>=1.0.0",
+ "ruff>=0.3.0",
]
diff --git a/src/fluxomics_data_model/__init__.py b/src/fluxomics_data_model/__init__.py
index e69de29..7d99941 100644
--- a/src/fluxomics_data_model/__init__.py
+++ b/src/fluxomics_data_model/__init__.py
@@ -0,0 +1,64 @@
+"""
+FluxML Data Model
+
+A JAX-compatible Python library for handling FluxML data structures with:
+- Pydantic validation
+- JAX compatibility for numerical operations
+- Immutable data structures
+- Bidirectional XML conversion
+"""
+
+from .model import (
+ FluxomicsDataModel,
+ Metadata,
+ Model,
+ Experiments,
+ Metabolite,
+ Reaction,
+ AtomMapping,
+ AtomMap,
+ AtomAddress,
+ Measurement,
+ MeasurementData,
+ Group,
+ Constraints,
+ NetConstraints,
+ ExchangeConstraints,
+ MetaboliteSizeConstraints,
+ Simulation,
+ FluxValue,
+ MetaboliteSizeValue,
+ Tracers,
+ LabelComposition,
+ Annotation,
+ DictList,
+)
+from .io import parse_fluxml_file
+
+__version__ = "0.1.0"
+__all__ = [
+ "FluxomicsDataModel",
+ "Metadata",
+ "Model",
+ "Experiments",
+ "Metabolite",
+ "Reaction",
+ "AtomMapping",
+ "AtomMap",
+ "AtomAddress",
+ "Measurement",
+ "MeasurementData",
+ "Group",
+ "Constraints",
+ "NetConstraints",
+ "ExchangeConstraints",
+ "MetaboliteSizeConstraints",
+ "Simulation",
+ "FluxValue",
+ "MetaboliteSizeValue",
+ "Tracers",
+ "LabelComposition",
+ "Annotation",
+ "DictList",
+ "parse_fluxml_file",
+]
diff --git a/src/fluxomics_data_model/core/__init__.py b/src/fluxomics_data_model/core/__init__.py
new file mode 100644
index 0000000..b65057a
--- /dev/null
+++ b/src/fluxomics_data_model/core/__init__.py
@@ -0,0 +1,25 @@
+"""
+Core data models for FluxML.
+"""
+
+from .core import FluxomicsDataModel, Metadata, Model
+from .common import (
+ Annotation,
+ ErrorModel,
+ TextualOrMath,
+ DictList,
+ JAXArray,
+ TimeSeries,
+)
+
+__all__ = [
+ "FluxomicsDataModel",
+ "Metadata",
+ "Model",
+ "Annotation",
+ "ErrorModel",
+ "TextualOrMath",
+ "DictList",
+ "JAXArray",
+ "TimeSeries",
+]
diff --git a/src/fluxomics_data_model/core/common.py b/src/fluxomics_data_model/core/common.py
new file mode 100644
index 0000000..b93989d
--- /dev/null
+++ b/src/fluxomics_data_model/core/common.py
@@ -0,0 +1,363 @@
+"""
+Common data structures used across FluxML models.
+"""
+
+from typing import Optional, Union, Any, List, TypeVar, Generic
+from pydantic import BaseModel, Field, GetCoreSchemaHandler
+from pydantic_core import core_schema
+import jax.numpy as jnp
+from itertools import islice
+
+
+class Annotation(BaseModel):
+ """
+ FluxML annotation element for additional metadata.
+
+ Corresponds to fluxml/reactionnetwork/metabolites/metabolite/annotation
+ and fluxml/reactionnetwork/reaction/annotation.
+ """
+
+ name: str = Field(description="Annotation name")
+ content: Optional[str] = Field(
+ default=None, description="Annotation content"
+ )
+
+ class Config:
+ frozen = True
+
+
+class TextualOrMath(BaseModel):
+ """
+ FluxML textual or MathML content.
+
+ Used for constraints and mathematical expressions.
+ """
+
+ textual: Optional[str] = Field(
+ default=None, description="Textual representation"
+ )
+ mathml: Optional[str] = Field(
+ default=None, description="MathML representation"
+ )
+
+ class Config:
+ frozen = True
+
+ def __init__(self, **data):
+ super().__init__(**data)
+ if not self.textual and not self.mathml:
+ raise ValueError("Either textual or mathml must be provided")
+
+
+class ErrorModel(BaseModel):
+ """
+ FluxML error model for measurement uncertainties.
+
+ Corresponds to fluxml/experiments/measurement/model/*/errormodel
+ """
+
+ expression: TextualOrMath = Field(description="Error model expression")
+
+ class Config:
+ frozen = True
+
+
+class JAXArray(BaseModel):
+ """
+ JAX array wrapper for Pydantic serialization.
+ """
+
+ shape: tuple[int, ...] = Field(description="Array shape")
+ dtype: str = Field(description="Array dtype")
+ data: list = Field(description="Array data as nested lists")
+
+ class Config:
+ frozen = True
+
+ @classmethod
+ def from_jax_array(cls, arr: jnp.ndarray) -> "JAXArray":
+ """Create from JAX array."""
+ return cls(shape=arr.shape, dtype=str(arr.dtype), data=arr.tolist())
+
+ def to_jax_array(self) -> jnp.ndarray:
+ """Convert to JAX array."""
+ return jnp.array(self.data, dtype=self.dtype).reshape(self.shape)
+
+
+class TimeSeries(BaseModel):
+ """
+ Time series data structure for non-stationary measurements.
+ """
+
+ times: JAXArray = Field(description="Time points")
+ values: JAXArray = Field(description="Measurement values")
+ errors: Optional[JAXArray] = Field(
+ default=None, description="Measurement errors"
+ )
+
+ class Config:
+ frozen = True
+
+ @classmethod
+ def from_arrays(
+ cls,
+ times: jnp.ndarray,
+ values: jnp.ndarray,
+ errors: Optional[jnp.ndarray] = None,
+ ) -> "TimeSeries":
+ """Create from JAX arrays."""
+ return cls(
+ times=JAXArray.from_jax_array(times),
+ values=JAXArray.from_jax_array(values),
+ errors=(
+ JAXArray.from_jax_array(errors) if errors is not None else None
+ ),
+ )
+
+ def to_arrays(
+ self,
+ ) -> tuple[jnp.ndarray, jnp.ndarray, Optional[jnp.ndarray]]:
+ """Convert to JAX arrays."""
+ return (
+ self.times.to_jax_array(),
+ self.values.to_jax_array(),
+ self.errors.to_jax_array() if self.errors else None,
+ )
+
+
+# Type variable for DictList items
+T = TypeVar("T", bound=BaseModel)
+
+
+class DictList(list, Generic[T]):
+ """
+ A combined dict and list data structure.
+
+ This object behaves like a list but has O(1) speed benefits
+ of a dict when looking up elements by their id attribute.
+
+ Items must have an 'id' attribute.
+ """
+
+ def __init__(self, items: Optional[Union[List[T], "DictList[T]"]] = None):
+ """Initialize a DictList.
+
+ Parameters
+ ----------
+ items : list or DictList, optional
+ Initial items to populate the DictList
+ """
+ super().__init__()
+ self._dict: dict[str, int] = {}
+
+ if items is not None:
+ if isinstance(items, DictList):
+ list.extend(self, items)
+ self._dict = items._dict.copy()
+ else:
+ self.extend(items)
+
+ def _generate_index(self) -> None:
+ """Rebuild the _dict index from current list items."""
+ self._dict = {item.id: idx for idx, item in enumerate(self)}
+
+ def _check_id(self, item_id: str) -> None:
+ """Check if an ID already exists in the DictList."""
+ if item_id in self._dict:
+ raise ValueError(f"ID '{item_id}' is already present in the list")
+
+ def append(self, item: T) -> None:
+ """Append an item to the end of the list."""
+ item_id = item.id
+ self._check_id(item_id)
+ self._dict[item_id] = len(self)
+ list.append(self, item)
+
+ def extend(self, items: Union[List[T], "DictList[T]"]) -> None:
+ """Extend list by appending elements from the iterable."""
+ current_length = len(self)
+
+ # First check all IDs are unique
+ for item in items:
+ self._check_id(item.id)
+
+ # Then extend
+ list.extend(self, items)
+
+ # Update index
+ for idx, item in enumerate(
+ islice(self, current_length, None), current_length
+ ):
+ self._dict[item.id] = idx
+
+ def insert(self, index: int, item: T) -> None:
+ """Insert item before index."""
+ self._check_id(item.id)
+ list.insert(self, index, item)
+
+ # Update indices for all items after insertion point
+ for i, j in list(self._dict.items()):
+ if j >= index:
+ self._dict[i] = j + 1
+ self._dict[item.id] = index
+
+ def remove(self, item: Union[str, T]) -> None:
+ """Remove first occurrence of item."""
+ if isinstance(item, str):
+ # Remove by ID
+ index = self._dict[item]
+ else:
+ # Remove by object
+ index = self.index(item)
+
+ self.pop(index)
+
+ def pop(self, index: int = -1) -> T:
+ """Remove and return item at index (default last)."""
+ item = list.pop(self, index)
+
+ # Remove from dict
+ del self._dict[item.id]
+
+ # Update indices if not popping from end
+ if index != -1 and index < len(self):
+ for key, idx in list(self._dict.items()):
+ if idx > index:
+ self._dict[key] = idx - 1
+
+ return item
+
+ def clear(self) -> None:
+ """Remove all items from the list."""
+ list.clear(self)
+ self._dict.clear()
+
+ def index(self, item: Union[str, T], start: int = 0, stop: int = -1) -> int:
+ """Return index of item in the list."""
+ if isinstance(item, str):
+ # Look up by ID
+ if item not in self._dict:
+ raise ValueError(f"'{item}' is not in list")
+ return self._dict[item]
+ else:
+ # Look up by object
+ return list.index(
+ self, item, start, stop if stop != -1 else len(self)
+ )
+
+ def get_by_id(self, item_id: str) -> T:
+ """Get item by its ID attribute."""
+ if item_id not in self._dict:
+ raise KeyError(f"No item with ID '{item_id}'")
+ return self[self._dict[item_id]]
+
+ def get_by_any(self, key: Union[str, int, T]) -> T:
+ """Get item by ID, index, or the item itself."""
+ if isinstance(key, int):
+ return self[key]
+ elif isinstance(key, str):
+ return self.get_by_id(key)
+ elif key in self:
+ return key
+ else:
+ raise ValueError(f"Item {key} not found in DictList")
+
+ def has_id(self, item_id: str) -> bool:
+ """Check if an ID exists in the DictList."""
+ return item_id in self._dict
+
+ def list_attr(self, attr: str) -> List[Any]:
+ """Return a list of the given attribute for every item."""
+ return [getattr(item, attr) for item in self]
+
+ def __contains__(self, item: Union[str, T]) -> bool:
+ """Check if item or ID is in the DictList."""
+ if isinstance(item, str):
+ return item in self._dict
+ else:
+ return list.__contains__(self, item)
+
+ def __getitem__(self, key: Union[int, slice, str]) -> Union[T, List[T]]:
+ """Get item by index, slice, or ID."""
+ if isinstance(key, str):
+ return self.get_by_id(key)
+ elif isinstance(key, slice):
+ # Return a new DictList for slices
+ result = DictList[T]()
+ result.extend(list.__getitem__(self, key))
+ return result
+ else:
+ return list.__getitem__(self, key)
+
+ def __setitem__(self, index: int, item: T) -> None:
+ """Set item at index."""
+ if not isinstance(index, int):
+ raise TypeError("DictList indices must be integers")
+
+ # Remove old item's ID from dict
+ old_item = self[index]
+ if old_item.id in self._dict:
+ del self._dict[old_item.id]
+
+ # Check new item's ID doesn't conflict
+ if item.id in self._dict and self._dict[item.id] != index:
+ raise ValueError(
+ f"ID '{item.id}' already exists at a different index"
+ )
+
+ # Set the item
+ list.__setitem__(self, index, item)
+ self._dict[item.id] = index
+
+ def __delitem__(self, index: Union[int, slice]) -> None:
+ """Delete item at index."""
+ if isinstance(index, slice):
+ # For slices, we need to regenerate the entire index
+ list.__delitem__(self, index)
+ self._generate_index()
+ else:
+ item = self[index]
+ list.__delitem__(self, index)
+
+ # Remove from dict and update subsequent indices
+ del self._dict[item.id]
+ for key, idx in list(self._dict.items()):
+ if idx > index:
+ self._dict[key] = idx - 1
+
+ def __repr__(self) -> str:
+ """String representation of DictList."""
+ return f"DictList({list.__repr__(self)})"
+
+ def __copy__(self) -> "DictList[T]":
+ """Create a shallow copy of the DictList."""
+ return DictList(self)
+
+ def copy(self) -> "DictList[T]":
+ """Create a shallow copy of the DictList."""
+ return self.__copy__()
+
+ @property
+ def ids(self) -> List[str]:
+ """Get list of all IDs in order."""
+ return [item.id for item in self]
+
+ @classmethod
+ def __get_pydantic_core_schema__(
+ cls, source_type: Any, handler: GetCoreSchemaHandler
+ ) -> core_schema.CoreSchema:
+ """Get Pydantic core schema for serialization/validation."""
+ # Get the inner type from the generic
+ if hasattr(source_type, "__args__") and source_type.__args__:
+ inner_type = source_type.__args__[0]
+ else:
+ inner_type = Any
+
+ # Get the schema for a list of the inner type
+ list_schema = handler.generate_schema(List[inner_type])
+
+ # Return a schema that validates as a list but returns a DictList
+ return core_schema.no_info_after_validator_function(
+ lambda v: cls(v),
+ list_schema,
+ )
diff --git a/src/fluxomics_data_model/core/core.py b/src/fluxomics_data_model/core/core.py
new file mode 100644
index 0000000..c46d158
--- /dev/null
+++ b/src/fluxomics_data_model/core/core.py
@@ -0,0 +1,343 @@
+"""
+Core FluxML data structures.
+"""
+
+from typing import Optional, List, Dict, Any
+from datetime import datetime
+import re
+from pydantic import BaseModel, Field, field_validator
+import jax.numpy as jnp
+from ..model.metabolite import Metabolite
+from .common import DictList
+from ..model.reaction import Reaction
+from ..model.constraint import Constraints
+from ..experiment.experiment import Experiments
+
+
+class Metadata(BaseModel):
+ """
+ FluxML info section containing metadata.
+
+ Corresponds to fluxml/info
+ """
+
+ name: Optional[str] = Field(default=None, description="Model name")
+ version: Optional[str] = Field(default=None, description="Model version")
+ date: Optional[datetime] = Field(
+ default=None, description="Creation/modification timestamp"
+ )
+ comment: Optional[str] = Field(
+ default=None, description="Model description"
+ )
+ signature: Optional[bytes] = Field(
+ default=None, description="Digital signature"
+ )
+ modeler: Optional[str] = Field(
+ default=None, description="Modeler information"
+ )
+ strain: Optional[str] = Field(
+ default=None, description="Strain information"
+ )
+
+ class Config:
+ frozen = True
+ extra = "forbid"
+
+ @field_validator("date", mode="before")
+ @classmethod
+ def parse_date(cls, v):
+ """Parse FluxML timestamp format: YYYY-MM-DD HH:MM:SS"""
+ if isinstance(v, str):
+ # FluxML timestamp pattern: YYYY-MM-DD HH:MM:SS
+ if re.match(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$", v):
+ return datetime.strptime(v, "%Y-%m-%d %H:%M:%S")
+ return v
+
+
+class Model(BaseModel):
+ """
+ FluxML model containing metabolites and reactions.
+
+ Corresponds to fluxml/reactionnetwork
+ """
+
+ metabolites: DictList[Metabolite] = Field(
+ default_factory=lambda: DictList[Metabolite](),
+ description="Metabolite definitions",
+ )
+ reactions: DictList[Reaction] = Field(
+ default_factory=lambda: DictList[Reaction](),
+ description="Reaction definitions",
+ )
+ compartments: List[str] = Field(
+ default_factory=list, description="List of compartments in the model"
+ )
+
+ class Config:
+ frozen = True
+ extra = "forbid"
+
+ def __init__(self, **data):
+ super().__init__(**data)
+ self._validate_cross_references()
+
+ def _validate_cross_references(self):
+ """Validate cross-references between metabolites and reactions."""
+ metabolite_ids = self.metabolite_ids
+
+ # Check all reactant and product IDs exist in metabolites
+ for reaction in self.reactions:
+ for reactant_id in reaction.reactants:
+ if reactant_id not in metabolite_ids:
+ raise ValueError(
+ f"Reactant {reactant_id} in reaction {reaction.id} "
+ f"references unknown metabolite"
+ )
+ for product_id in reaction.products:
+ if product_id not in metabolite_ids:
+ raise ValueError(
+ f"Product {product_id} in reaction {reaction.id} "
+ f"references unknown metabolite"
+ )
+
+ @property
+ def metabolite_ids(self) -> frozenset[str]:
+ """Get all metabolite IDs."""
+ return frozenset(self.metabolites.ids)
+
+ @property
+ def reaction_ids(self) -> frozenset[str]:
+ """Get all reaction IDs."""
+ return frozenset(self.reactions.ids)
+
+ def get_stoichiometric_matrix(self) -> jnp.ndarray:
+ """
+ Get stoichiometric matrix for JAX computations.
+
+ Returns:
+ JAX array of shape (n_metabolites, n_reactions)
+ """
+ metabolite_ids = list(self.metabolite_ids)
+ # reaction_ids = list(self.reaction_ids) # Unused variable
+
+ matrix = []
+ for reaction in self.reactions:
+ column = reaction.get_stoichiometric_vector(metabolite_ids)
+ matrix.append(column)
+
+ return jnp.stack(matrix, axis=1)
+
+
+class FluxomicsDataModel(BaseModel):
+ """
+ Root FluxML object containing complete model specification.
+
+ This is the main entry point for FluxML data, designed for JAX compatibility
+ with immutable data structures and validation.
+
+ Corresponds to fluxml root element
+ """
+
+ model: Model = Field(description="Model definition")
+ info: Optional[Metadata] = Field(default=None, description="Model metadata")
+ constraints: Optional[Constraints] = Field(
+ default=None, description="Model constraints"
+ )
+ experiments: List[Experiments] = Field(
+ default_factory=list, description="Experimental setups"
+ )
+
+ class Config:
+ frozen = True
+ extra = "forbid"
+
+ def __init__(self, **data):
+ super().__init__(**data)
+ self._validate_experiments_references()
+
+ def _validate_experiments_references(self):
+ """Validate experiment references to metabolites and reactions."""
+ metabolite_ids = self.metabolite_ids
+ reaction_ids = self.reaction_ids
+
+ # Check experiment names are unique
+ if len(self.experiments) > 1:
+ experiments_names = [e.name for e in self.experiments]
+ if len(set(experiments_names)) != len(experiments_names):
+ raise ValueError("Experiment names must be unique")
+
+ # Check metabolite and reaction references
+ for experiment in self.experiments:
+ # Check tracer metabolite references
+ for tracer_spec in experiment.tracers:
+ if tracer_spec.metabolite not in metabolite_ids:
+ raise ValueError(
+ f"Tracer metabolite {tracer_spec.metabolite} in "
+ f"experiment {experiment.name} references unknown "
+ f"metabolite"
+ )
+
+ # Check simulation variable references
+ if experiment.simulation and experiment.simulation.variables:
+ for flux_var in experiment.simulation.variables.flux_values:
+ if flux_var.flux not in reaction_ids:
+ raise ValueError(
+ f"Flux variable {flux_var.flux} in configuration "
+ f"{experiment.name} references unknown reaction"
+ )
+
+ for (
+ m_var
+ ) in experiment.simulation.variables.metabolitesize_values:
+ if m_var.metabolite not in metabolite_ids:
+ raise ValueError(
+ f"Metabolite size variable {m_var.metabolite} in "
+ f"experiment {experiment.name} references "
+ f"unknown metabolite"
+ )
+
+ @property
+ def metabolite_ids(self) -> frozenset[str]:
+ """Get all metabolite IDs in the model."""
+ return self.model.metabolite_ids
+
+ @property
+ def reaction_ids(self) -> frozenset[str]:
+ """Get all reaction IDs in the model."""
+ return self.model.reaction_ids
+
+ @property
+ def experiments_names(self) -> frozenset[str]:
+ """Get all experiment names."""
+ return frozenset(exp.name for exp in self.experiments)
+
+ def get_experiments(self, name: str) -> Optional[Experiments]:
+ """Get experiments by name."""
+ for exp in self.experiments:
+ if exp.name == name:
+ return exp
+ return None
+
+ def to_jax_representation(self) -> Dict[str, Any]:
+ """
+ Convert to JAX-compatible dictionary representation.
+
+ Returns:
+ Dictionary with JAX arrays for numerical computations
+ """
+ metabolite_ids = list(self.metabolite_ids)
+ reaction_ids = list(self.reaction_ids)
+
+ # Get stoichiometric matrix
+ S = self.model.get_stoichiometric_matrix()
+
+ # Get bounds matrices from first experiment (if available)
+ flux_bounds = None
+ metabolitesize_bounds = None
+
+ if self.experiments:
+ experiment = self.experiments[0]
+ if experiment.simulation:
+ flux_bounds, metabolitesize_bounds = (
+ experiment.simulation.get_optimization_bounds(
+ reaction_ids, metabolite_ids
+ )
+ )
+
+ if flux_bounds is None:
+ flux_bounds = jnp.array([[-jnp.inf, jnp.inf]] * len(reaction_ids))
+ if metabolitesize_bounds is None:
+ metabolitesize_bounds = jnp.array(
+ [[0.0, jnp.inf]] * len(metabolite_ids)
+ )
+
+ return {
+ "stoichiometric_matrix": S,
+ "flux_bounds": flux_bounds,
+ "metabolitesize_bounds": metabolitesize_bounds,
+ "metabolite_ids": metabolite_ids,
+ "reaction_ids": reaction_ids,
+ "n_metabolites": len(metabolite_ids),
+ "n_reactions": len(reaction_ids),
+ "n_experiments": len(self.experiments),
+ }
+
+ def get_tracer_experiment_data(
+ self, experiments_name: str
+ ) -> Optional[Dict[str, Any]]:
+ """
+ Get tracer experiment data for a specific experiment.
+
+ Returns:
+ Dictionary with JAX arrays for tracer experiment analysis
+ """
+ experiment = self.get_experiments(experiments_name)
+ if not experiment:
+ return None
+
+ metabolite_ids = list(self.metabolite_ids)
+
+ # Get tracer composition matrix
+ tracer_matrix = experiment.get_tracer_composition_matrix(metabolite_ids)
+
+ # Get measurement data
+ measurement_data = None
+ if experiment.measurement:
+ values = experiment.measurement.data.values
+ errors = experiment.measurement.data.errors
+ times = experiment.measurement.data.times
+
+ measurement_data = {
+ "values": values,
+ "errors": errors,
+ "times": times,
+ }
+
+ return {
+ "tracer_composition": tracer_matrix,
+ "measurement_data": measurement_data,
+ "stationary": experiment.stationary,
+ "time_point": experiment.time,
+ }
+
+ def __repr__(self) -> str:
+ """
+ Return a summary of the data model including Metadata and Stats.
+ """
+ lines = ["Fluxomics Data Model Summary", "=" * 30, ""]
+
+ # Metadata table
+ if self.info:
+ lines.append("Model Information:")
+ lines.append("-" * 18)
+ info_items = [
+ ("Name", self.info.name),
+ ("Version", self.info.version),
+ (
+ "Date",
+ self.info.date.strftime("%Y-%m-%d %H:%M:%S")
+ if self.info.date
+ else None,
+ ),
+ ("Comment", self.info.comment),
+ ("Modeler", self.info.modeler),
+ ("Strain", self.info.strain),
+ ]
+
+ max_key_len = max(len(key) for key, _ in info_items)
+ for key, value in info_items:
+ if value is not None:
+ lines.append(f"{key:<{max_key_len}} : {value}")
+ else:
+ lines.append("Model Information: Not available")
+
+ lines.append("")
+
+ # Counts
+ lines.append("Model Components:")
+ lines.append("-" * 17)
+ lines.append(f"Reactions : {len(self.model.reactions)}") # noqa: E501
+ lines.append(f"Metabolites : {len(self.model.metabolites)}")
+ lines.append(f"Experiments : {len(self.experiments)}")
+
+ return "\n".join(lines)
diff --git a/src/fluxomics_data_model/experiment/__init__.py b/src/fluxomics_data_model/experiment/__init__.py
new file mode 100644
index 0000000..fae1500
--- /dev/null
+++ b/src/fluxomics_data_model/experiment/__init__.py
@@ -0,0 +1,41 @@
+"""
+Experiment-related data models for FluxML.
+"""
+
+from .experiment import Experiments
+from .tracer import Tracers, LabelComposition
+from .measurement import (
+ Measurement,
+ MeasurementData,
+ MeasurementModel,
+ Datum,
+ Group,
+ LabelingMeasurement,
+ FluxMeasurement,
+ NetFlux,
+ ExchangeFlux,
+ MetaboliteSizeMeasurement,
+ MetaboliteSize,
+)
+from .simulation import Simulation, Variables, FluxValue, MetaboliteSizeValue
+
+__all__ = [
+ "Experiments",
+ "Tracers",
+ "LabelComposition",
+ "Measurement",
+ "MeasurementData",
+ "MeasurementModel",
+ "Datum",
+ "Group",
+ "LabelingMeasurement",
+ "FluxMeasurement",
+ "NetFlux",
+ "ExchangeFlux",
+ "MetaboliteSizeMeasurement",
+ "MetaboliteSize",
+ "Simulation",
+ "Variables",
+ "FluxValue",
+ "MetaboliteSizeValue",
+]
diff --git a/src/fluxomics_data_model/experiment/experiment.py b/src/fluxomics_data_model/experiment/experiment.py
new file mode 100644
index 0000000..4653d75
--- /dev/null
+++ b/src/fluxomics_data_model/experiment/experiment.py
@@ -0,0 +1,81 @@
+"""
+FluxML experimental setup definitions.
+"""
+
+from typing import Optional, List
+from pydantic import BaseModel, Field
+import jax.numpy as jnp
+from .tracer import Tracers
+from ..model.constraint import Constraints
+from .measurement import Measurement
+from .simulation import Simulation
+
+
+class Experiments(BaseModel):
+ """
+ FluxML experimental setup.
+
+ Corresponds to fluxml/experiments
+ """
+
+ name: str = Field(description="Experiment name")
+ stationary: bool = Field(default=True, description="Stationary assumption")
+ time: Optional[float] = Field(default=None, description="Time point")
+ comment: Optional[str] = Field(
+ default=None, description="Experiment comment"
+ )
+ tracers: List[Tracers] = Field(
+ default_factory=list, description="Tracer specifications"
+ )
+ constraints: Optional[Constraints] = Field(
+ default=None, description="Experiment-specific constraints"
+ )
+ measurement: Optional[Measurement] = Field(
+ default=None, description="Measurement data"
+ )
+ simulation: Optional[Simulation] = Field(
+ default=None, description="Experiment-specific simulation settings"
+ )
+
+ class Config:
+ frozen = True
+ extra = "forbid"
+
+ @property
+ def traced_metabolites(self) -> frozenset[str]:
+ """Get all traced metabolite IDs."""
+ return frozenset(tracer_spec.metabolite for tracer_spec in self.tracers)
+
+ def get_tracer_for_metabolite(
+ self, metabolite_id: str
+ ) -> Optional[Tracers]:
+ """Get tracer specification for a metabolite."""
+ for tracer_spec in self.tracers:
+ if tracer_spec.metabolite == metabolite_id:
+ return tracer_spec
+ return None
+
+ def get_tracer_composition_matrix(
+ self, metabolite_ids: List[str]
+ ) -> jnp.ndarray:
+ """
+ Get tracer composition matrix for JAX computations.
+
+ Returns:
+ JAX array of shape (n_metabolites, n_isotopomers)
+ """
+ compositions = []
+ for metabolite_id in metabolite_ids:
+ tracer_spec = self.get_tracer_for_metabolite(metabolite_id)
+ if tracer_spec:
+ compositions.append(tracer_spec.composition_vector)
+ else:
+ compositions.append(jnp.array([1.0])) # Unlabeled
+
+ # Pad to same length
+ max_len = max(len(comp) for comp in compositions)
+ padded = [
+ jnp.pad(comp, (0, max_len - len(comp))) for comp in compositions
+ ]
+
+ return jnp.stack(padded)
diff --git a/src/fluxomics_data_model/experiment/measurement.py b/src/fluxomics_data_model/experiment/measurement.py
new file mode 100644
index 0000000..b3cb7f3
--- /dev/null
+++ b/src/fluxomics_data_model/experiment/measurement.py
@@ -0,0 +1,321 @@
+"""
+FluxML measurement definitions.
+"""
+
+from typing import Optional, List, Dict, Literal
+from pydantic import BaseModel, Field, field_validator
+import jax.numpy as jnp
+from ..core.common import TextualOrMath, ErrorModel, JAXArray, TimeSeries
+
+
+class Group(BaseModel):
+ """
+ FluxML measurement group for mass spectrometry data.
+
+ Corresponds to fluxml/experiments/measurement/model/
+ labelingmeasurement/group
+ """
+
+ id: str = Field(description="Group identifier")
+ times: Optional[str] = Field(default=None, description="Time points")
+ scale: Literal["auto", "one"] = Field(
+ default="auto", description="Scaling method"
+ )
+ errormodel: Optional[ErrorModel] = Field(
+ default=None, description="Error model"
+ )
+ expression: TextualOrMath = Field(description="Measurement expression")
+
+ # JAX-compatible numerical representation
+ time_points_array: Optional[JAXArray] = Field(
+ default=None, exclude=True, description="Time points array"
+ )
+
+ class Config:
+ frozen = True
+ extra = "forbid"
+
+ @property
+ def time_points(self) -> Optional[jnp.ndarray]:
+ """Get time points as JAX array."""
+ if self.time_points_array is None:
+ if self.times is None:
+ return None
+ # Parse time string - simplified implementation
+ try:
+ times = [float(t.strip()) for t in self.times.split(",")]
+ return jnp.array(times)
+ except Exception:
+ return None
+ return self.time_points_array.to_jax_array()
+
+
+class NetFlux(BaseModel):
+ """
+ FluxML net flux measurement.
+
+ Corresponds to fluxml/experiments/measurement/model/
+ fluxmeasurement/netflux
+ """
+
+ id: str = Field(description="Net flux identifier")
+ errormodel: Optional[ErrorModel] = Field(
+ default=None, description="Error model"
+ )
+ expression: TextualOrMath = Field(description="Net flux expression")
+
+ class Config:
+ frozen = True
+ extra = "forbid"
+
+
+class ExchangeFlux(BaseModel):
+ """
+ FluxML exchange flux measurement.
+
+ Corresponds to fluxml/experiments/measurement/model/
+ fluxmeasurement/xchflux
+ """
+
+ id: str = Field(description="Exchange flux identifier")
+ errormodel: Optional[ErrorModel] = Field(
+ default=None, description="Error model"
+ )
+ expression: TextualOrMath = Field(description="Exchange flux expression")
+
+ class Config:
+ frozen = True
+ extra = "forbid"
+
+
+class MetaboliteSize(BaseModel):
+ """
+ FluxML metabolite size measurement.
+
+ Corresponds to fluxml/experiments/measurement/model/
+ metabolitesizemeasurement/metabolitesize
+ """
+
+ id: str = Field(description="Metabolite size identifier")
+ errormodel: Optional[ErrorModel] = Field(
+ default=None, description="Error model"
+ )
+ expression: TextualOrMath = Field(description="Metabolite size expression")
+
+ class Config:
+ frozen = True
+ extra = "forbid"
+
+
+class LabelingMeasurement(BaseModel):
+ """
+ FluxML labeling measurement collection.
+
+ Corresponds to fluxml/experiments/measurement/model/labelingmeasurement
+ """
+
+ groups: List[Group] = Field(
+ default_factory=list, description="Measurement groups"
+ )
+
+ class Config:
+ frozen = True
+ extra = "forbid"
+
+ @field_validator("groups")
+ @classmethod
+ def validate_unique_ids(cls, v: List[Group]) -> List[Group]:
+ """Validate group IDs are unique."""
+ ids = [group.id for group in v]
+ if len(set(ids)) != len(ids):
+ raise ValueError("Group IDs must be unique")
+ return v
+
+
+class FluxMeasurement(BaseModel):
+ """
+ FluxML flux measurement collection.
+
+ Corresponds to fluxml/experiments/measurement/model/fluxmeasurement
+ """
+
+ net_fluxes: List[NetFlux] = Field(
+ default_factory=list, description="Net flux measurements"
+ )
+ xch_fluxes: List[ExchangeFlux] = Field(
+ default_factory=list, description="Exchange flux measurements"
+ )
+
+ class Config:
+ frozen = True
+ extra = "forbid"
+
+
+class MetaboliteSizeMeasurement(BaseModel):
+ """
+ FluxML metabolite size measurement collection.
+
+ Corresponds to fluxml/experiments/measurement/model/
+ metabolitesizemeasurement
+ """
+
+ metabolite_sizes: List[MetaboliteSize] = Field(
+ default_factory=list, description="Metabolite size measurements"
+ )
+
+ class Config:
+ frozen = True
+ extra = "forbid"
+
+
+class MeasurementModel(BaseModel):
+ """
+ FluxML measurement model.
+
+ Corresponds to fluxml/experiments/measurement/model
+ """
+
+ labeling_measurement: Optional[LabelingMeasurement] = Field(
+ default=None, description="Labeling measurements"
+ )
+ flux_measurement: Optional[FluxMeasurement] = Field(
+ default=None, description="Flux measurements"
+ )
+ metabolitesize_measurement: Optional[MetaboliteSizeMeasurement] = Field(
+ default=None, description="Metabolite size measurements"
+ )
+
+ class Config:
+ frozen = True
+ extra = "forbid"
+
+
+class Datum(BaseModel):
+ """
+ FluxML measurement datum.
+
+ Corresponds to fluxml/experiments/measurement/data/datum
+ """
+
+ id: str = Field(description="Datum identifier")
+ value: float = Field(description="Measurement value")
+ stddev: float = Field(description="Standard deviation")
+ row: Optional[int] = Field(
+ default=None, ge=1, le=256, description="Row index"
+ )
+ time: Optional[float] = Field(default=None, description="Time point")
+ weight: Optional[str] = Field(
+ default=None, description="Weight specification"
+ )
+ pos: Optional[int] = Field(
+ default=None, ge=0, le=1024, description="Position"
+ )
+ type: Optional[Literal["S", "DL", "DR", "DD", "T"]] = Field(
+ default=None, description="Data type"
+ )
+
+ class Config:
+ frozen = True
+ extra = "forbid"
+
+
+class MeasurementData(BaseModel):
+ """
+ FluxML measurement data collection.
+
+ Corresponds to fluxml/experiments/measurement/data
+ """
+
+ data: List[Datum] = Field(
+ default_factory=list, description="Measurement data points"
+ )
+
+ # JAX-compatible numerical representation
+ values_array: Optional[JAXArray] = Field(
+ default=None, exclude=True, description="Values array"
+ )
+ errors_array: Optional[JAXArray] = Field(
+ default=None, exclude=True, description="Errors array"
+ )
+ times_array: Optional[JAXArray] = Field(
+ default=None, exclude=True, description="Times array"
+ )
+
+ class Config:
+ frozen = True
+ extra = "forbid"
+
+ @property
+ def values(self) -> jnp.ndarray:
+ """Get measurement values as JAX array."""
+ if self.values_array is None:
+ return jnp.array([datum.value for datum in self.data])
+ return self.values_array.to_jax_array()
+
+ @property
+ def errors(self) -> jnp.ndarray:
+ """Get measurement errors as JAX array."""
+ if self.errors_array is None:
+ return jnp.array([datum.stddev for datum in self.data])
+ return self.errors_array.to_jax_array()
+
+ @property
+ def times(self) -> Optional[jnp.ndarray]:
+ """Get measurement times as JAX array."""
+ if self.times_array is None:
+ times = [
+ datum.time for datum in self.data if datum.time is not None
+ ]
+ if not times:
+ return None
+ return jnp.array(times)
+ return self.times_array.to_jax_array()
+
+ def get_data_for_id(self, datum_id: str) -> List[Datum]:
+ """Get all data points for a specific ID."""
+ return [datum for datum in self.data if datum.id == datum_id]
+
+ def to_time_series(self, datum_id: str) -> Optional[TimeSeries]:
+ """Convert data for specific ID to time series."""
+ data_points = self.get_data_for_id(datum_id)
+ if not data_points:
+ return None
+
+ times = [dp.time for dp in data_points if dp.time is not None]
+ values = [dp.value for dp in data_points]
+ errors = [dp.stddev for dp in data_points]
+
+ if not times:
+ return None
+
+ return TimeSeries.from_arrays(
+ jnp.array(times), jnp.array(values), jnp.array(errors)
+ )
+
+
+class Measurement(BaseModel):
+ """
+ FluxML measurement specification.
+
+ Corresponds to fluxml/experiments/measurement
+ """
+
+ model: MeasurementModel = Field(description="Measurement model")
+ data: MeasurementData = Field(description="Measurement data")
+
+ class Config:
+ frozen = True
+ extra = "forbid"
+
+ def get_labeling_data(self) -> Optional[Dict[str, TimeSeries]]:
+ """Get labeling measurement data as time series."""
+ if not self.model.labeling_measurement:
+ return None
+
+ result = {}
+ for group in self.model.labeling_measurement.groups:
+ time_series = self.data.to_time_series(group.id)
+ if time_series:
+ result[group.id] = time_series
+
+ return result if result else None
diff --git a/src/fluxomics_data_model/experiment/simulation.py b/src/fluxomics_data_model/experiment/simulation.py
new file mode 100644
index 0000000..821632c
--- /dev/null
+++ b/src/fluxomics_data_model/experiment/simulation.py
@@ -0,0 +1,211 @@
+"""
+FluxML simulation definitions.
+"""
+
+from typing import Optional, List
+from pydantic import BaseModel, Field, field_validator
+import jax.numpy as jnp
+from .measurement import MeasurementModel
+
+
+class FluxValue(BaseModel):
+ """
+ FluxML flux value specification for simulation.
+
+ Corresponds to fluxml/experiments/simulation/variables/fluxvalue
+ """
+
+ flux: str = Field(description="Flux identifier")
+ type: str = Field(description="Flux type")
+ value: Optional[float] = Field(default=None, description="Flux value")
+ lo: Optional[float] = Field(default=None, description="Lower bound")
+ hi: Optional[float] = Field(default=None, description="Upper bound")
+ inc: Optional[float] = Field(default=None, description="Increment")
+ edweight: float = Field(
+ default=1.0, ge=0.0, le=1.0, description="ED weight"
+ )
+
+ class Config:
+ frozen = True
+ extra = "forbid"
+
+ @property
+ def bounds(self) -> tuple[Optional[float], Optional[float]]:
+ """Get flux bounds."""
+ return (self.lo, self.hi)
+
+ @property
+ def bounds_array(self) -> jnp.ndarray:
+ """Get bounds as JAX array."""
+ lo = self.lo if self.lo is not None else -jnp.inf
+ hi = self.hi if self.hi is not None else jnp.inf
+ return jnp.array([lo, hi])
+
+
+class MetaboliteSizeValue(BaseModel):
+ """
+ FluxML metabolite size value specification for simulation.
+
+ Corresponds to fluxml/experiments/simulation/variables/metabolitesizevalue
+ """
+
+ metabolite: str = Field(description="Metabolite identifier")
+ value: Optional[float] = Field(
+ default=None, description="Metabolite size value"
+ )
+ lo: Optional[float] = Field(default=None, description="Lower bound")
+ hi: Optional[float] = Field(default=None, description="Upper bound")
+ inc: Optional[float] = Field(default=None, description="Increment")
+ edweight: float = Field(
+ default=1.0, ge=0.0, le=1.0, description="ED weight"
+ )
+
+ class Config:
+ frozen = True
+ extra = "forbid"
+
+ @property
+ def bounds(self) -> tuple[Optional[float], Optional[float]]:
+ """Get metabolite size bounds."""
+ return (self.lo, self.hi)
+
+ @property
+ def bounds_array(self) -> jnp.ndarray:
+ """Get bounds as JAX array."""
+ lo = self.lo if self.lo is not None else 0.0
+ hi = self.hi if self.hi is not None else jnp.inf
+ return jnp.array([lo, hi])
+
+
+class Variables(BaseModel):
+ """
+ FluxML simulation variables.
+
+ Corresponds to fluxml/experiments/simulation/variables
+ """
+
+ flux_values: List[FluxValue] = Field(
+ default_factory=list, description="Flux variables"
+ )
+ metabolitesize_values: List[MetaboliteSizeValue] = Field(
+ default_factory=list, description="Metabolite size variables"
+ )
+
+ class Config:
+ frozen = True
+ extra = "forbid"
+
+ @field_validator("flux_values")
+ @classmethod
+ def validate_unique_flux_ids(cls, v: List[FluxValue]) -> List[FluxValue]:
+ """Validate flux IDs are unique."""
+ ids = [(fv.flux, fv.type) for fv in v]
+ if len(set(ids)) != len(ids):
+ raise ValueError("Flux variable (flux, type) pairs must be unique")
+ return v
+
+ @field_validator("metabolitesize_values")
+ @classmethod
+ def validate_unique_metabolite_ids(
+ cls, v: List[MetaboliteSizeValue]
+ ) -> List[MetaboliteSizeValue]:
+ """Validate metabolite IDs are unique."""
+ ids = [pv.metabolite for pv in v]
+ if len(set(ids)) != len(ids):
+ raise ValueError(
+ "Metabolite size variable metabolite IDs must be unique"
+ )
+ return v
+
+ def get_flux_bounds_matrix(self, flux_ids: List[str]) -> jnp.ndarray:
+ """
+ Get flux bounds matrix for JAX computations.
+
+ Returns:
+ JAX array of shape (n_fluxes, 2) with [lower, upper] bounds
+ """
+ flux_dict = {(fv.flux, fv.type): fv for fv in self.flux_values}
+
+ bounds = []
+ for flux_id in flux_ids:
+ # Check both net and xch types
+ net_var = flux_dict.get((flux_id, "net"))
+ xch_var = flux_dict.get((flux_id, "xch"))
+
+ if net_var:
+ bounds.append(net_var.bounds_array)
+ elif xch_var:
+ bounds.append(xch_var.bounds_array)
+ else:
+ # Default bounds
+ bounds.append(jnp.array([-jnp.inf, jnp.inf]))
+
+ return jnp.stack(bounds)
+
+ def get_metabolitesize_bounds_matrix(
+ self, metabolite_ids: List[str]
+ ) -> jnp.ndarray:
+ """
+ Get metabolite size bounds matrix for JAX computations.
+
+ Returns:
+ JAX array of shape (n_metabolites, 2) with [lower, upper] bounds
+ """
+ metabolite_dict = {
+ pv.metabolite: pv for pv in self.metabolitesize_values
+ }
+
+ bounds = []
+ for metabolite_id in metabolite_ids:
+ metabolite_var = metabolite_dict.get(metabolite_id)
+ if metabolite_var:
+ bounds.append(metabolite_var.bounds_array)
+ else:
+ # Default bounds for metabolite sizes
+ bounds.append(jnp.array([0.0, jnp.inf]))
+
+ return jnp.stack(bounds)
+
+
+class Simulation(BaseModel):
+ """
+ FluxML simulation specification.
+
+ Corresponds to fluxml/experiments/simulation
+ """
+
+ type: str = Field(default="auto", description="Simulation type")
+ method: str = Field(default="auto", description="Simulation method")
+ model: Optional[MeasurementModel] = Field(
+ default=None, description="Simulation model"
+ )
+ variables: Optional[Variables] = Field(
+ default=None, description="Simulation variables"
+ )
+
+ class Config:
+ frozen = True
+ extra = "forbid"
+
+ def get_optimization_bounds(
+ self, flux_ids: List[str], metabolite_ids: List[str]
+ ) -> tuple[jnp.ndarray, jnp.ndarray]:
+ """
+ Get optimization bounds for JAX optimization.
+
+ Returns:
+ Tuple of (flux_bounds, metabolitesize_bounds) as JAX arrays
+ """
+ if self.variables is None:
+ # Default bounds
+ flux_bounds = jnp.array([[-jnp.inf, jnp.inf]] * len(flux_ids))
+ metabolitesize_bounds = jnp.array(
+ [[0.0, jnp.inf]] * len(metabolite_ids)
+ )
+ else:
+ flux_bounds = self.variables.get_flux_bounds_matrix(flux_ids)
+ metabolitesize_bounds = (
+ self.variables.get_metabolitesize_bounds_matrix(metabolite_ids)
+ )
+
+ return flux_bounds, metabolitesize_bounds
diff --git a/src/fluxomics_data_model/experiment/tracer.py b/src/fluxomics_data_model/experiment/tracer.py
new file mode 100644
index 0000000..389e734
--- /dev/null
+++ b/src/fluxomics_data_model/experiment/tracer.py
@@ -0,0 +1,150 @@
+"""
+FluxML tracer and label composition definitions.
+"""
+
+from typing import Optional, List
+from pydantic import BaseModel, Field, field_validator, model_validator
+import jax.numpy as jnp
+from ..core.common import TextualOrMath, JAXArray, TimeSeries
+
+
+class LabelComposition(BaseModel):
+ """
+ FluxML isotope label composition specification.
+
+ Corresponds to fluxml/experiments/tracers/label
+ """
+
+ labeled_pattern: str = Field(
+ description="Label configuration pattern", pattern=r"[01xX]+"
+ )
+ purity: Optional[float] = Field(
+ default=None,
+ description="Label purity (between 0 and 1)",
+ ge=0.0,
+ le=1.0,
+ )
+ cost: Optional[float] = Field(
+ default=None, description="Label cost (must be positive)", gt=0.0
+ )
+ fraction: Optional[float] = Field(
+ default=None,
+ description="Label fraction (for float fractions, must sum to 1)",
+ )
+ expression: Optional[TextualOrMath] = Field(
+ default=None, description="Mathematical expression"
+ )
+
+ class Config:
+ frozen = True
+ extra = "forbid"
+
+ @field_validator("purity")
+ @classmethod
+ def validate_purity(cls, v: Optional[float]) -> Optional[float]:
+ """Validate purity is between 0 and 1."""
+ if v is None:
+ return v
+ if not 0.0 <= v <= 1.0:
+ raise ValueError(f"Purity must be between 0 and 1, got {v}")
+ return v
+
+ @field_validator("cost")
+ @classmethod
+ def validate_cost(cls, v: Optional[float]) -> Optional[float]:
+ """Validate cost is positive."""
+ if v is None:
+ return v
+ if v <= 0.0:
+ raise ValueError(f"Cost must be positive, got {v}")
+ return v
+
+
+class Tracers(BaseModel):
+ """
+ FluxML tracer specification for tracer experiments.
+
+ Corresponds to fluxml/experiments/tracers
+ """
+
+ id: Optional[str] = Field(default=None, description="Tracer identifier")
+ metabolite: str = Field(description="Metabolite identifier")
+ type: str = Field(default="isotopomer", description="Tracer type")
+ profile: Optional[str] = Field(default=None, description="Time profile")
+ labels: List[LabelComposition] = Field(
+ default_factory=list, description="Isotope label compositions"
+ )
+
+ # JAX-compatible numerical representation
+ composition_array: Optional[JAXArray] = Field(
+ default=None, exclude=True, description="Composition vector"
+ )
+ time_profile_data: Optional[TimeSeries] = Field(
+ default=None, exclude=True, description="Time profile data"
+ )
+
+ class Config:
+ frozen = True
+ extra = "forbid"
+
+ @model_validator(mode="after")
+ def validate_fractions(self) -> "Tracers":
+ """Validate that float fractions sum to 1."""
+ # Only validate if all labels have float fractions
+ float_fractions = []
+ has_float_fractions = False
+
+ for label in self.labels:
+ if label.fraction is not None and isinstance(label.fraction, float):
+ has_float_fractions = True
+ float_fractions.append(label.fraction)
+ elif label.fraction is not None:
+ # If non-float fractions (e.g., expressions) exist,
+ # skip validation
+ return self
+
+ if has_float_fractions and float_fractions:
+ total = sum(float_fractions)
+ if abs(total - 1.0) > 1e-6: # Allow small floating point errors
+ raise ValueError(
+ f"Label fractions must sum to 1.0, got {total}"
+ )
+
+ return self
+
+ @property
+ def composition_vector(self) -> jnp.ndarray:
+ """Get JAX array representation of tracer composition."""
+ if self.composition_array is None:
+ # Default: create composition from labels
+ if not self.labels:
+ return jnp.array([1.0]) # Unlabeled
+ # Simple implementation - would need full label parsing
+ return jnp.array([1.0])
+ return self.composition_array.to_jax_array()
+
+ def with_composition(self, composition: jnp.ndarray) -> "Tracers":
+ """Create new tracer with specified composition."""
+ return Tracers(
+ id=self.id,
+ metabolite=self.metabolite,
+ type=self.type,
+ profile=self.profile,
+ labels=self.labels,
+ composition_array=JAXArray.from_jax_array(composition),
+ time_profile_data=self.time_profile_data,
+ )
+
+ def with_time_profile(
+ self, times: jnp.ndarray, values: jnp.ndarray
+ ) -> "Tracers":
+ """Create new tracer with time profile."""
+ return Tracers(
+ id=self.id,
+ metabolite=self.metabolite,
+ type=self.type,
+ profile=self.profile,
+ labels=self.labels,
+ composition_array=self.composition_array,
+ time_profile_data=TimeSeries.from_arrays(times, values),
+ )
diff --git a/src/fluxomics_data_model/fluxomics_dataset.py b/src/fluxomics_data_model/fluxomics_dataset.py
deleted file mode 100644
index 477bbf2..0000000
--- a/src/fluxomics_data_model/fluxomics_dataset.py
+++ /dev/null
@@ -1,18 +0,0 @@
-"""Provides the class FluxomicsDataset."""
-
-from pydantic import BaseModel
-from fluxomics_data_model.reaction_network import ReactionNetwork
-
-
-class FluxomicsDatasetMetadata(BaseModel):
- """Metadata for a fluxomics dataset."""
-
- fluxomics_data_model_version: str
- description: str
-
-
-class FluxomicsDataset(BaseModel):
- """A fluxomics dataset"""
-
- metadata: FluxomicsDatasetMetadata
- reaction_network: ReactionNetwork
diff --git a/src/fluxomics_data_model/io/__init__.py b/src/fluxomics_data_model/io/__init__.py
new file mode 100644
index 0000000..e4ef3b9
--- /dev/null
+++ b/src/fluxomics_data_model/io/__init__.py
@@ -0,0 +1,12 @@
+"""
+FluxML I/O operations package.
+
+Contains parsers and writers for FluxML data formats.
+"""
+
+from .parser import FluxMLParser, parse_fluxml_file
+
+__all__ = [
+ "FluxMLParser",
+ "parse_fluxml_file",
+]
diff --git a/src/fluxomics_data_model/io/parser.py b/src/fluxomics_data_model/io/parser.py
new file mode 100644
index 0000000..4f1344b
--- /dev/null
+++ b/src/fluxomics_data_model/io/parser.py
@@ -0,0 +1,886 @@
+"""
+FluxML XML parser for reading FluxML files.
+"""
+
+import xml.etree.ElementTree as ET
+from typing import Optional
+from datetime import datetime
+import re
+
+from ..model import (
+ FluxomicsDataModel,
+ Metadata,
+ Model,
+ Metabolite,
+ Reaction,
+ AtomMapping,
+ Experiments,
+ Variables,
+ FluxValue,
+ MetaboliteSizeValue,
+ Tracers,
+ LabelComposition,
+ Measurement,
+ MeasurementData,
+ MeasurementModel,
+ Datum,
+ LabelingMeasurement,
+ Group,
+ FluxMeasurement,
+ NetFlux,
+ ExchangeFlux,
+ MetaboliteSizeMeasurement,
+ MetaboliteSize,
+ Simulation,
+ Constraints,
+ NetConstraints,
+ ExchangeConstraints,
+ MetaboliteSizeConstraints,
+ Annotation,
+ TextualOrMath,
+ ErrorModel,
+ DictList,
+)
+
+
+class FluxMLParser:
+ """Parser for FluxML XML files."""
+
+ def __init__(self):
+ self.namespaces = {
+ "fluxml": "http://www.13cflux.net/fluxml",
+ "mml": "http://www.w3.org/1998/Math/MathML",
+ }
+
+ def parse_file(self, file_path: str) -> FluxomicsDataModel:
+ """Parse a FluxML file and return a FluxomicsDataModel object."""
+ try:
+ # Try to parse with different encodings
+ encodings = ["utf-8", "latin-1", "iso-8859-1", "cp1252"]
+
+ for encoding in encodings:
+ try:
+ with open(file_path, "r", encoding=encoding) as f:
+ content = f.read()
+
+ # Parse the XML content
+ root = ET.fromstring(content)
+ return self._parse_fluxml(root)
+
+ except UnicodeDecodeError:
+ continue
+ except ET.ParseError:
+ # If XML parsing fails, try next encoding
+ continue
+
+ # If all encodings failed, try binary mode with ET.parse
+ try:
+ tree = ET.parse(file_path)
+ root = tree.getroot()
+ return self._parse_fluxml(root)
+ except ET.ParseError as e:
+ raise ValueError(f"XML parsing error in {file_path}: {e}")
+
+ except Exception as e:
+ raise ValueError(f"Error parsing {file_path}: {e}")
+
+ def _parse_fluxml(self, root: ET.Element) -> FluxomicsDataModel:
+ """Parse the root fluxml element."""
+ # Handle namespace prefix
+ if root.tag.startswith("{"):
+ # Namespaced element
+ ns_prefix = root.tag.split("}")[0] + "}"
+ else:
+ # No namespace
+ ns_prefix = ""
+
+ # Parse components
+ info = self._parse_info(root.find(f"{ns_prefix}info"))
+ model = self._parse_model(root.find(f"{ns_prefix}reactionnetwork"))
+ constraints = self._parse_constraints(
+ root.find(f"{ns_prefix}constraints")
+ )
+
+ # Parse experiments
+ experiments = []
+ for exp_elem in root.findall(f"{ns_prefix}configuration"):
+ experiments.append(self._parse_experiments(exp_elem))
+
+ return FluxomicsDataModel(
+ info=info,
+ model=model,
+ constraints=constraints,
+ experiments=experiments,
+ )
+
+ def _parse_info(
+ self, info_elem: Optional[ET.Element]
+ ) -> Optional[Metadata]:
+ """Parse info element."""
+ if info_elem is None:
+ return None
+
+ # Get namespace prefix
+ ns_prefix = self._get_namespace_prefix(info_elem)
+
+ name = self._get_text(info_elem.find(f"{ns_prefix}name"))
+ version = self._get_text(info_elem.find(f"{ns_prefix}version"))
+ date_str = self._get_text(info_elem.find(f"{ns_prefix}date"))
+ comment = self._get_text(info_elem.find(f"{ns_prefix}comment"))
+ modeler = self._get_text(info_elem.find(f"{ns_prefix}modeler"))
+ strain = self._get_text(info_elem.find(f"{ns_prefix}strain"))
+
+ # Parse date
+ date = None
+ if date_str:
+ try:
+ date = datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S")
+ except ValueError:
+ pass # Invalid date format
+
+ return Metadata(
+ name=name,
+ version=version,
+ date=date,
+ comment=comment,
+ modeler=modeler,
+ strain=strain,
+ )
+
+ def _parse_model(self, rn_elem: ET.Element) -> Model:
+ """Parse reactionnetwork element into Model object."""
+ if rn_elem is None:
+ raise ValueError("reactionnetwork element is required")
+
+ ns_prefix = self._get_namespace_prefix(rn_elem)
+
+ # Parse metabolites
+ pools_elem = rn_elem.find(f"{ns_prefix}metabolitepools")
+ if pools_elem is None:
+ raise ValueError("metabolitepools element is required")
+
+ metabolites = DictList[Metabolite]()
+ compartments = set()
+ for pool_elem in pools_elem.findall(f"{ns_prefix}pool"):
+ metabolite = self._parse_metabolite(pool_elem)
+ metabolites.append(metabolite)
+ if metabolite.compartment:
+ compartments.add(metabolite.compartment)
+
+ # Parse reactions
+ reactions = DictList[Reaction]()
+ for reaction_elem in rn_elem.findall(f"{ns_prefix}reaction"):
+ reactions.append(self._parse_reaction(reaction_elem))
+
+ return Model(
+ metabolites=metabolites,
+ reactions=reactions,
+ compartments=list(compartments),
+ )
+
+ def _parse_metabolite(self, pool_elem: ET.Element) -> Metabolite:
+ """Parse metabolite (pool) element."""
+ metabolite_id = pool_elem.get("id")
+ if not metabolite_id:
+ raise ValueError("Metabolite (pool) must have an id attribute")
+
+ # Parse optional attributes - only set if present in XML
+ atoms_str = pool_elem.get("atoms")
+ atoms = int(atoms_str) if atoms_str is not None else None
+
+ weight_str = pool_elem.get("size")
+ weight = float(weight_str) if weight_str is not None else None
+
+ formula = pool_elem.get("cfg")
+ compartment = pool_elem.get("compartment")
+
+ # Parse annotations
+ annotations = []
+ ns_prefix = self._get_namespace_prefix(pool_elem)
+ for ann_elem in pool_elem.findall(f"{ns_prefix}annotation"):
+ annotations.append(self._parse_annotation(ann_elem))
+
+ return Metabolite(
+ id=metabolite_id,
+ name=None, # FluxML doesn't typically have separate name field
+ atoms=atoms,
+ weight=weight,
+ formula=formula,
+ compartment=compartment,
+ annotations=annotations,
+ )
+
+ def _parse_reaction(self, reaction_elem: ET.Element) -> Reaction:
+ """Parse reaction element."""
+ reaction_id = reaction_elem.get("id")
+ if not reaction_id:
+ raise ValueError("Reaction must have an id attribute")
+
+ reversibility = (
+ reaction_elem.get("bidirectional", "true").lower() == "true"
+ )
+
+ ns_prefix = self._get_namespace_prefix(reaction_elem)
+
+ # Parse annotations
+ annotations = []
+ for ann_elem in reaction_elem.findall(f"{ns_prefix}annotation"):
+ annotations.append(self._parse_annotation(ann_elem))
+
+ # Parse reactants
+ reactants = []
+ reactant_cfgs = []
+ for reduct_elem in reaction_elem.findall(f"{ns_prefix}reduct"):
+ reduct_id = reduct_elem.get("id")
+ if not reduct_id:
+ raise ValueError("Reactant must have an id attribute")
+ reactants.append(reduct_id)
+ cfg = reduct_elem.get("cfg")
+ if cfg:
+ reactant_cfgs.append((reduct_id, cfg))
+
+ # Parse products and check for variants
+ products = []
+ product_cfgs = []
+ # Store variants for products with multiple mappings
+ product_variants = {}
+
+ for rproduct_elem in reaction_elem.findall(f"{ns_prefix}rproduct"):
+ rproduct_id = rproduct_elem.get("id")
+ if not rproduct_id:
+ raise ValueError("Product must have an id attribute")
+ products.append(rproduct_id)
+
+ # Check for variant sub-elements
+ variant_elems = rproduct_elem.findall(f"{ns_prefix}variant")
+ if variant_elems:
+ # Product has multiple variants
+ variants = []
+ for variant_elem in variant_elems:
+ cfg = variant_elem.get("cfg")
+ if cfg:
+ variants.append(cfg)
+ if variants:
+ product_variants[rproduct_id] = variants
+ # Use first variant as default for now
+ product_cfgs.append((rproduct_id, variants[0]))
+ else:
+ # Single cfg attribute on product
+ cfg = rproduct_elem.get("cfg")
+ if cfg:
+ product_cfgs.append((rproduct_id, cfg))
+
+ # Build atom mapping if configurations exist
+ atom_mapping = AtomMapping()
+ if product_cfgs:
+ # Check if we have variants
+ if product_variants:
+ # Generate multiple mappings for variants
+ all_maps = []
+
+ # Generate all variant combinations
+ variant_products = [
+ p for p in products if p in product_variants
+ ]
+ if variant_products:
+ # For simplicity, handle the most common case:
+ # one product with variants
+ # Full combinatorial expansion would be more complex
+ for variant_product in variant_products:
+ for variant_cfg in product_variants[variant_product]:
+ # Create product configs with this variant
+ variant_product_cfgs = []
+ for pid, cfg in product_cfgs:
+ if pid == variant_product:
+ variant_product_cfgs.append(
+ (pid, variant_cfg)
+ )
+ else:
+ variant_product_cfgs.append((pid, cfg))
+
+ # Parse based on format
+ sample_cfg = variant_cfg
+ if re.search(r"[A-Z]#\d+@\d+", sample_cfg):
+ # C#1@2 format
+ temp_mapping = AtomMapping.from_fluxml_cfg(
+ reactant_cfgs=reactant_cfgs,
+ product_cfgs=variant_product_cfgs,
+ reactant_order=reactants,
+ )
+ all_maps.extend(temp_mapping.maps)
+ elif re.search(r"[a-zA-Z]", sample_cfg):
+ # Letter notation
+ temp_mapping = AtomMapping.from_letter_notation(
+ reactant_items=reactant_cfgs,
+ product_items=variant_product_cfgs,
+ )
+ all_maps.extend(temp_mapping.maps)
+
+ # Create mapping with all variants and equal weights
+ if all_maps:
+ weights = [1.0 / len(all_maps)] * len(all_maps)
+ atom_mapping = AtomMapping(
+ maps=all_maps, weights=weights
+ )
+ else:
+ atom_mapping = AtomMapping()
+ else:
+ # No variants, single mapping
+ sample_cfg = product_cfgs[0][1]
+ if re.search(r"[A-Z]#\d+@\d+", sample_cfg):
+ # C#1@2 format
+ atom_mapping = AtomMapping.from_fluxml_cfg(
+ reactant_cfgs=reactant_cfgs,
+ product_cfgs=product_cfgs,
+ reactant_order=reactants,
+ )
+ elif re.search(r"[a-zA-Z]", sample_cfg):
+ # Letter notation (abc format)
+ atom_mapping = AtomMapping.from_letter_notation(
+ reactant_items=reactant_cfgs,
+ product_items=product_cfgs,
+ )
+
+ return Reaction(
+ id=reaction_id,
+ name=None,
+ reversibility=reversibility,
+ annotations=annotations,
+ reactants=reactants,
+ products=products,
+ atom_mapping=atom_mapping,
+ )
+
+ def _parse_annotation(self, ann_elem: ET.Element) -> Annotation:
+ """Parse annotation element."""
+ name = ann_elem.get("name")
+ if not name:
+ raise ValueError("Annotation must have a name attribute")
+
+ content = ann_elem.text
+ return Annotation(name=name, content=content)
+
+ def _parse_constraints(
+ self, constraints_elem: Optional[ET.Element]
+ ) -> Optional[Constraints]:
+ """Parse constraints element."""
+ if constraints_elem is None:
+ return None
+
+ ns_prefix = self._get_namespace_prefix(constraints_elem)
+
+ # Parse net constraints
+ net_elem = constraints_elem.find(f"{ns_prefix}net")
+ net = (
+ self._parse_net_constraints(net_elem)
+ if net_elem is not None
+ else None
+ )
+
+ # Parse xch constraints (also check for 'exchange' element)
+ xch_elem = constraints_elem.find(f"{ns_prefix}xch")
+ if xch_elem is None:
+ xch_elem = constraints_elem.find(f"{ns_prefix}exchange")
+ xch = (
+ self._parse_xch_constraints(xch_elem)
+ if xch_elem is not None
+ else None
+ )
+
+ # Parse psize constraints
+ psize_elem = constraints_elem.find(f"{ns_prefix}psize")
+ psize = (
+ self._parse_metabolitesize_constraints(psize_elem)
+ if psize_elem is not None
+ else None
+ )
+
+ return Constraints(net=net, xch=xch, metabolitesize=psize)
+
+ def _parse_net_constraints(self, net_elem: ET.Element) -> NetConstraints:
+ """Parse net constraints element."""
+ expression = self._parse_textual_or_math(net_elem)
+ return NetConstraints(expression=expression)
+
+ def _parse_xch_constraints(
+ self, xch_elem: ET.Element
+ ) -> ExchangeConstraints:
+ """Parse xch constraints element."""
+ expression = self._parse_textual_or_math(xch_elem)
+ return ExchangeConstraints(expression=expression)
+
+ def _parse_metabolitesize_constraints(
+ self, psize_elem: ET.Element
+ ) -> MetaboliteSizeConstraints:
+ """Parse psize constraints element."""
+ expression = self._parse_textual_or_math(psize_elem)
+ return MetaboliteSizeConstraints(expression=expression)
+
+ def _parse_textual_or_math(self, elem: ET.Element) -> TextualOrMath:
+ """Parse textual or MathML content."""
+ ns_prefix = self._get_namespace_prefix(elem)
+
+ textual_elem = elem.find(f"{ns_prefix}textual")
+ textual = textual_elem.text if textual_elem is not None else None
+
+ # For MathML, we'd need to serialize the subtree
+ math_elem = elem.find(f"{ns_prefix}math")
+ mathml = None
+ if math_elem is not None:
+ mathml = ET.tostring(math_elem, encoding="unicode")
+
+ # If neither textual nor mathml, use element text
+ if textual is None and mathml is None:
+ textual = elem.text
+
+ return TextualOrMath(textual=textual, mathml=mathml)
+
+ def _parse_experiments(self, config_elem: ET.Element) -> Experiments:
+ """Parse configuration element into an Experiments object."""
+ name = config_elem.get("name")
+ if not name:
+ raise ValueError(
+ "Experiment (configuration) must have a name attribute"
+ )
+
+ stationary = config_elem.get("stationary", "true").lower() == "true"
+ time_str = config_elem.get("time")
+ time = float(time_str) if time_str else None
+
+ ns_prefix = self._get_namespace_prefix(config_elem)
+
+ # Parse comment
+ comment_elem = config_elem.find(f"{ns_prefix}comment")
+ comment = comment_elem.text if comment_elem is not None else None
+
+ # Parse tracers (inputs)
+ tracers = []
+ for input_elem in config_elem.findall(f"{ns_prefix}input"):
+ tracers.append(self._parse_tracer(input_elem))
+
+ # Parse constraints (optional)
+ constraints_elem = config_elem.find(f"{ns_prefix}constraints")
+ constraints = (
+ self._parse_constraints(constraints_elem)
+ if constraints_elem is not None
+ else None
+ )
+
+ # Parse measurement (optional)
+ measurement_elem = config_elem.find(f"{ns_prefix}measurement")
+ measurement = (
+ self._parse_measurement(measurement_elem)
+ if measurement_elem is not None
+ else None
+ )
+
+ # Parse simulation (optional)
+ simulation_elem = config_elem.find(f"{ns_prefix}simulation")
+ simulation = (
+ self._parse_simulation(simulation_elem)
+ if simulation_elem is not None
+ else None
+ )
+
+ return Experiments(
+ name=name,
+ stationary=stationary,
+ time=time,
+ comment=comment,
+ tracers=tracers,
+ constraints=constraints,
+ measurement=measurement,
+ simulation=simulation,
+ )
+
+ def _parse_tracer(self, input_elem: ET.Element) -> Tracers:
+ """Parse input element into a Tracers object."""
+ metabolite = input_elem.get("pool")
+ if not metabolite:
+ raise ValueError("Tracer (input) must have a pool attribute")
+
+ input_id = input_elem.get("id")
+ input_type = input_elem.get("type", "isotopomer")
+ profile = input_elem.get("profile")
+
+ # Parse labels
+ labels = []
+ ns_prefix = self._get_namespace_prefix(input_elem)
+ for label_elem in input_elem.findall(f"{ns_prefix}label"):
+ labels.append(self._parse_label(label_elem))
+
+ return Tracers(
+ id=input_id,
+ metabolite=metabolite,
+ type=input_type,
+ profile=profile,
+ labels=labels,
+ )
+
+ def _parse_label(self, label_elem: ET.Element) -> LabelComposition:
+ """Parse label element."""
+ cfg = label_elem.get("cfg")
+ if not cfg:
+ raise ValueError("Label must have a cfg attribute")
+
+ # Parse purity as float
+ purity_str = label_elem.get("purity")
+ purity = float(purity_str) if purity_str else None
+
+ # Parse cost as float
+ cost_str = label_elem.get("cost")
+ cost = float(cost_str) if cost_str else None
+
+ # Parse fraction from content
+ fraction_str = label_elem.text
+ fraction = (
+ float(fraction_str.strip())
+ if fraction_str and fraction_str.strip()
+ else None
+ )
+
+ return LabelComposition(
+ labeled_pattern=cfg, purity=purity, cost=cost, fraction=fraction
+ )
+
+ def _parse_measurement(self, measurement_elem: ET.Element) -> Measurement:
+ """Parse measurement element according to FluxML schema."""
+ ns_prefix = self._get_namespace_prefix(measurement_elem)
+
+ # Parse model section (required)
+ model_elem = measurement_elem.find(f"{ns_prefix}model")
+ if model_elem is None:
+ raise ValueError("measurement must have a model element")
+ model = self._parse_measurement_model(model_elem)
+
+ # Parse data section (required)
+ data_elem = measurement_elem.find(f"{ns_prefix}data")
+ if data_elem is None:
+ raise ValueError("measurement must have a data element")
+ data = self._parse_measurement_data(data_elem)
+
+ return Measurement(model=model, data=data)
+
+ def _parse_measurement_model(
+ self, model_elem: ET.Element
+ ) -> MeasurementModel:
+ """Parse measurement model element."""
+ ns_prefix = self._get_namespace_prefix(model_elem)
+
+ # Parse labelingmeasurement (optional)
+ labeling_elem = model_elem.find(f"{ns_prefix}labelingmeasurement")
+ labeling_measurement = None
+ if labeling_elem is not None:
+ labeling_measurement = self._parse_labeling_measurement(
+ labeling_elem
+ )
+
+ # Parse fluxmeasurement (optional)
+ flux_elem = model_elem.find(f"{ns_prefix}fluxmeasurement")
+ flux_measurement = None
+ if flux_elem is not None:
+ flux_measurement = self._parse_flux_measurement(flux_elem)
+
+ # Parse poolsizemeasurement (optional)
+ poolsize_elem = model_elem.find(f"{ns_prefix}poolsizemeasurement")
+ metabolitesize_measurement = None
+ if poolsize_elem is not None:
+ metabolitesize_measurement = self._parse_metabolitesize_measurement(
+ poolsize_elem
+ )
+
+ return MeasurementModel(
+ labeling_measurement=labeling_measurement,
+ flux_measurement=flux_measurement,
+ metabolitesize_measurement=metabolitesize_measurement,
+ )
+
+ def _parse_labeling_measurement(
+ self, labeling_elem: ET.Element
+ ) -> LabelingMeasurement:
+ """Parse labelingmeasurement element."""
+ ns_prefix = self._get_namespace_prefix(labeling_elem)
+
+ groups = []
+ # Check for both 'group' and 'MSgroup' elements
+ for group_elem in labeling_elem.findall(f"{ns_prefix}group"):
+ groups.append(self._parse_group(group_elem))
+ for group_elem in labeling_elem.findall(f"{ns_prefix}MSgroup"):
+ groups.append(self._parse_group(group_elem))
+
+ return LabelingMeasurement(groups=groups)
+
+ def _parse_group(self, group_elem: ET.Element) -> Group:
+ """Parse group element."""
+ group_id = group_elem.get("id")
+ if not group_id:
+ raise ValueError("Group must have an id attribute")
+
+ times = group_elem.get("times")
+ scale = group_elem.get("scale", "auto")
+ spec = group_elem.get("spec")
+
+ ns_prefix = self._get_namespace_prefix(group_elem)
+
+ # Parse errormodel (optional)
+ errormodel_elem = group_elem.find(f"{ns_prefix}errormodel")
+ errormodel = None
+ if errormodel_elem is not None:
+ errormodel = ErrorModel(
+ expression=self._parse_textual_or_math(errormodel_elem)
+ )
+
+ # Parse expression (textual or math)
+ # If spec attribute exists, use it as textual expression
+ if spec:
+ expression = TextualOrMath(textual=spec)
+ else:
+ expression = self._parse_textual_or_math(group_elem)
+
+ return Group(
+ id=group_id,
+ times=times,
+ scale=scale,
+ errormodel=errormodel,
+ expression=expression,
+ )
+
+ def _parse_flux_measurement(self, flux_elem: ET.Element) -> FluxMeasurement:
+ """Parse fluxmeasurement element."""
+ ns_prefix = self._get_namespace_prefix(flux_elem)
+
+ net_fluxes = []
+ for netflux_elem in flux_elem.findall(f"{ns_prefix}netflux"):
+ net_fluxes.append(self._parse_netflux(netflux_elem))
+
+ xch_fluxes = []
+ for xchflux_elem in flux_elem.findall(f"{ns_prefix}xchflux"):
+ xch_fluxes.append(self._parse_xchflux(xchflux_elem))
+
+ return FluxMeasurement(net_fluxes=net_fluxes, xch_fluxes=xch_fluxes)
+
+ def _parse_netflux(self, netflux_elem: ET.Element) -> NetFlux:
+ """Parse netflux element."""
+ netflux_id = netflux_elem.get("id")
+ if not netflux_id:
+ raise ValueError("NetFlux must have an id attribute")
+
+ ns_prefix = self._get_namespace_prefix(netflux_elem)
+
+ # Parse errormodel (optional)
+ errormodel_elem = netflux_elem.find(f"{ns_prefix}errormodel")
+ errormodel = None
+ if errormodel_elem is not None:
+ errormodel = ErrorModel(
+ expression=self._parse_textual_or_math(errormodel_elem)
+ )
+
+ # Parse expression
+ expression = self._parse_textual_or_math(netflux_elem)
+
+ return NetFlux(
+ id=netflux_id, errormodel=errormodel, expression=expression
+ )
+
+ def _parse_xchflux(self, xchflux_elem: ET.Element) -> ExchangeFlux:
+ """Parse xchflux element."""
+ xchflux_id = xchflux_elem.get("id")
+ if not xchflux_id:
+ raise ValueError("ExchangeFlux must have an id attribute")
+
+ ns_prefix = self._get_namespace_prefix(xchflux_elem)
+
+ # Parse errormodel (optional)
+ errormodel_elem = xchflux_elem.find(f"{ns_prefix}errormodel")
+ errormodel = None
+ if errormodel_elem is not None:
+ errormodel = ErrorModel(
+ expression=self._parse_textual_or_math(errormodel_elem)
+ )
+
+ # Parse expression
+ expression = self._parse_textual_or_math(xchflux_elem)
+
+ return ExchangeFlux(
+ id=xchflux_id, errormodel=errormodel, expression=expression
+ )
+
+ def _parse_metabolitesize_measurement(
+ self, poolsize_elem: ET.Element
+ ) -> MetaboliteSizeMeasurement:
+ """Parse poolsizemeasurement element."""
+ ns_prefix = self._get_namespace_prefix(poolsize_elem)
+
+ metabolite_sizes = []
+ for poolsize_item_elem in poolsize_elem.findall(f"{ns_prefix}poolsize"):
+ metabolite_sizes.append(
+ self._parse_metabolitesize(poolsize_item_elem)
+ )
+
+ return MetaboliteSizeMeasurement(metabolite_sizes=metabolite_sizes)
+
+ def _parse_metabolitesize(
+ self, poolsize_elem: ET.Element
+ ) -> MetaboliteSize:
+ """Parse poolsize element."""
+ metabolitesize_id = poolsize_elem.get("id")
+ if not metabolitesize_id:
+ raise ValueError(
+ "MetaboliteSize (poolsize) must have an id attribute"
+ )
+
+ ns_prefix = self._get_namespace_prefix(poolsize_elem)
+
+ # Parse errormodel (optional)
+ errormodel_elem = poolsize_elem.find(f"{ns_prefix}errormodel")
+ errormodel = None
+ if errormodel_elem is not None:
+ errormodel = ErrorModel(
+ expression=self._parse_textual_or_math(errormodel_elem)
+ )
+
+ # Parse expression
+ expression = self._parse_textual_or_math(poolsize_elem)
+
+ return MetaboliteSize(
+ id=metabolitesize_id, errormodel=errormodel, expression=expression
+ )
+
+ def _parse_measurement_data(self, data_elem: ET.Element) -> MeasurementData:
+ """Parse measurement data element."""
+ ns_prefix = self._get_namespace_prefix(data_elem)
+
+ data = []
+ for datum_elem in data_elem.findall(f"{ns_prefix}datum"):
+ data.append(self._parse_datum(datum_elem))
+
+ return MeasurementData(data=data)
+
+ def _parse_datum(self, datum_elem: ET.Element) -> Datum:
+ """Parse datum element."""
+ datum_id = datum_elem.get("id")
+ if not datum_id:
+ raise ValueError("Datum must have an id attribute")
+
+ stddev_str = datum_elem.get("stddev")
+ if not stddev_str:
+ raise ValueError("Datum must have a stddev attribute")
+ stddev = float(stddev_str.strip())
+
+ # Parse optional attributes
+ row_str = datum_elem.get("row")
+ row = int(row_str) if row_str else None
+
+ time_str = datum_elem.get("time")
+ time = float(time_str) if time_str else None
+
+ weight = datum_elem.get("weight")
+
+ pos_str = datum_elem.get("pos")
+ pos = int(pos_str) if pos_str else None
+
+ datum_type = datum_elem.get("type")
+
+ # Parse value from element text
+ value_text = datum_elem.text
+ if not value_text:
+ raise ValueError("Datum must have a value in its text content")
+ value = float(value_text.strip())
+
+ return Datum(
+ id=datum_id,
+ value=value,
+ stddev=stddev,
+ row=row,
+ time=time,
+ weight=weight,
+ pos=pos,
+ type=datum_type,
+ )
+
+ def _parse_simulation(self, simulation_elem: ET.Element) -> Simulation:
+ """Parse simulation element."""
+ sim_type = simulation_elem.get("type", "auto")
+ method = simulation_elem.get("method", "auto")
+
+ ns_prefix = self._get_namespace_prefix(simulation_elem)
+
+ # Parse variables
+ variables_elem = simulation_elem.find(f"{ns_prefix}variables")
+ variables = None
+ if variables_elem is not None:
+ flux_values = []
+ for flux_elem in variables_elem.findall(f"{ns_prefix}fluxvalue"):
+ flux_values.append(self._parse_flux_value(flux_elem))
+
+ metabolite_values = []
+ for met_elem in variables_elem.findall(
+ f"{ns_prefix}metabolitesizevalue"
+ ):
+ metabolite_values.append(
+ self._parse_metabolite_size_value(met_elem)
+ )
+
+ variables = Variables(
+ flux_values=flux_values, metabolitesize_values=metabolite_values
+ )
+
+ return Simulation(type=sim_type, method=method, variables=variables)
+
+ def _parse_flux_value(self, flux_elem: ET.Element) -> FluxValue:
+ """Parse fluxvalue element."""
+ flux = flux_elem.get("flux")
+ if not flux:
+ raise ValueError("FluxValue must have a flux attribute")
+
+ flux_type = flux_elem.get("type", "net")
+
+ # Get the value from element text
+ value_str = flux_elem.text
+ if value_str:
+ try:
+ value = float(value_str.strip())
+ except ValueError:
+ value = 0.0
+ else:
+ value = 0.0
+
+ return FluxValue(flux=flux, type=flux_type, lo=value)
+
+ def _parse_metabolite_size_value(
+ self, met_elem: ET.Element
+ ) -> MetaboliteSizeValue:
+ """Parse metabolitesizevalue element."""
+ pool = met_elem.get("pool")
+ if not pool:
+ raise ValueError("MetaboliteSizeValue must have a pool attribute")
+
+ # Get the value from element text
+ value_str = met_elem.text
+ if value_str:
+ try:
+ value = float(value_str.strip())
+ except ValueError:
+ value = 0.0
+ else:
+ value = 0.0
+
+ return MetaboliteSizeValue(pool=pool, lo=value)
+
+ def _get_namespace_prefix(self, elem: ET.Element) -> str:
+ """Get namespace prefix for element."""
+ if elem.tag.startswith("{"):
+ return elem.tag.split("}")[0] + "}"
+ return ""
+
+ def _get_text(self, elem: Optional[ET.Element]) -> Optional[str]:
+ """Get text content from element."""
+ if elem is None:
+ return None
+ return elem.text.strip() if elem.text else None
+
+
+def parse_fluxml_file(file_path: str) -> FluxomicsDataModel:
+ """Parse a FluxML file and return a FluxML object."""
+ parser = FluxMLParser()
+ return parser.parse_file(file_path)
diff --git a/src/fluxomics_data_model/model/__init__.py b/src/fluxomics_data_model/model/__init__.py
new file mode 100644
index 0000000..d757b38
--- /dev/null
+++ b/src/fluxomics_data_model/model/__init__.py
@@ -0,0 +1,76 @@
+"""
+FluxML data models package.
+
+Contains Pydantic models that correspond to FluxML schema elements,
+designed for JAX compatibility.
+"""
+
+from ..core.core import FluxomicsDataModel, Metadata, Model
+from .metabolite import Metabolite
+from .reaction import Reaction
+from .atom_mapping import AtomMapping, AtomMap, AtomAddress
+from ..experiment.measurement import (
+ Measurement,
+ MeasurementData,
+ Group,
+ LabelingMeasurement,
+ FluxMeasurement,
+ NetFlux,
+ ExchangeFlux,
+ MetaboliteSizeMeasurement,
+ MetaboliteSize,
+ Datum,
+ MeasurementModel,
+)
+from ..experiment.simulation import (
+ Simulation,
+ Variables,
+ FluxValue,
+ MetaboliteSizeValue,
+)
+from .constraint import (
+ Constraints,
+ NetConstraints,
+ ExchangeConstraints,
+ MetaboliteSizeConstraints,
+)
+from ..experiment.experiment import Experiments
+from ..experiment.tracer import Tracers, LabelComposition
+from ..core.common import Annotation, ErrorModel, TextualOrMath, DictList
+
+__all__ = [
+ "FluxomicsDataModel",
+ "Metadata",
+ "Model",
+ "Metabolite",
+ "Reaction",
+ "AtomMapping",
+ "AtomMap",
+ "AtomAddress",
+ "Measurement",
+ "MeasurementData",
+ "Group",
+ "LabelingMeasurement",
+ "FluxMeasurement",
+ "NetFlux",
+ "ExchangeFlux",
+ "MetaboliteSizeMeasurement",
+ "MetaboliteSize",
+ "Datum",
+ "MeasurementModel",
+ "Simulation",
+ "Variables",
+ "FluxValue",
+ "MetaboliteSizeValue",
+ "Constraints",
+ "NetConstraints",
+ "ExchangeConstraints",
+ "MetaboliteSizeConstraints",
+ "Experiments",
+ "Tracers",
+ "LabelComposition",
+ "Annotation",
+ "ErrorModel",
+ "TextualOrMath",
+ "DictList",
+]
diff --git a/src/fluxomics_data_model/model/atom_mapping.py b/src/fluxomics_data_model/model/atom_mapping.py
new file mode 100644
index 0000000..9751833
--- /dev/null
+++ b/src/fluxomics_data_model/model/atom_mapping.py
@@ -0,0 +1,612 @@
+"""
+Atom mapping functionality for fluxomics reactions.
+
+This module provides classes and utilities for handling atom mappings in
+metabolic reactions, including support for:
+- Multiple mapping formats (letter notation, FluxML cfg strings, RDKit SMILES)
+- Symmetric reactions with multiple equivalent mappings
+- Isotopomer transformations
+"""
+
+from typing import Dict, List, Tuple, Optional
+from dataclasses import dataclass
+from pydantic import BaseModel, Field, ConfigDict
+import re
+import numpy as np
+
+
+@dataclass(frozen=True)
+class AtomAddress:
+ """Address for a specific atom in a reaction.
+
+ Attributes:
+ mol: Molecule/compound ID
+ index: Atom index within the molecule (1-based)
+ element: Element type (e.g., 'C', 'N', 'O')
+ instance: Instance number when multiple same compounds exist (1-based)
+ """
+
+ mol: str
+ index: int
+ element: Optional[str] = None
+ instance: int = 1
+
+
+class AtomMap(BaseModel):
+ """A single atom mapping for a reaction.
+
+ Maps product atoms to their source reactant atoms.
+ Keys and values are AtomAddress objects with 1-based indices.
+ """
+
+ # We need to use tuple representation for Pydantic compatibility
+ mapping: Dict[AtomAddress, AtomAddress] = Field(
+ default_factory=dict,
+ description="Mapping from product atoms to source reactant atoms",
+ )
+
+ model_config = ConfigDict(frozen=True, extra="forbid")
+
+ def add(
+ self,
+ product_cpd: str,
+ product_atom: int,
+ src_cpd: str,
+ src_atom: int,
+ atom_type: Optional[str] = "C",
+ product_instance: int = 1,
+ src_instance: int = 1,
+ ) -> "AtomMap":
+ """Add a mapping from product atom to source atom."""
+ # Create new dict to maintain immutability
+ new_mapping = dict(self.mapping)
+ new_mapping[
+ AtomAddress(product_cpd, product_atom, atom_type, product_instance)
+ ] = AtomAddress(src_cpd, src_atom, atom_type, src_instance)
+ return AtomMap(mapping=new_mapping)
+
+ def product_atoms(self) -> AtomAddress:
+ """Return number of atoms per product compound instance."""
+ result: Dict[Tuple[str, int], int] = {}
+ for cpd, atom, _, instance in self.mapping.keys():
+ key = (cpd, instance)
+ result[key] = max(result.get(key, 0), atom)
+ return AtomAddress(result)
+
+ def reactant_atoms(self) -> AtomAddress:
+ """Return number of atoms per reactant compound instance."""
+ result: Dict[Tuple[str, int], int] = {}
+ for cpd, atom, _, instance in self.mapping.values():
+ key = (cpd, instance)
+ result[key] = max(result.get(key, 0), atom)
+ return AtomAddress(result)
+
+
+class AtomMapping(BaseModel):
+ """
+ Complete atom mapping information for a reaction.
+
+ Supports multiple alternative mappings (variants/symmetries).
+ """
+
+ maps: List[AtomMap] = Field(
+ default_factory=lambda: [AtomMap()],
+ description="List of alternative atom mappings",
+ )
+ weights: Optional[List[float]] = Field(
+ default=None,
+ description="Weights for each mapping variant (defaults to uniform)",
+ )
+
+ model_config = ConfigDict(frozen=True, extra="forbid")
+
+ @classmethod
+ def from_letter_notation(
+ cls,
+ reactant_items: List[Tuple[str, str]],
+ product_items: List[Tuple[str, str]],
+ ) -> "AtomMapping":
+ """Parse atom mapping from letter notation data without string building.
+
+ Args:
+ reactant_items: List of (compound_id, letter_cfg) tuples
+ product_items: List of (product_id, letter_cfg) tuples
+ """
+ # Build a global list of all atoms from all reactants
+ # (cpd, atom_idx, instance)
+ all_reactant_atoms: List[Tuple[str, int, int]] = []
+ # Sequence of all letters from reactants
+ letter_sequence: List[str] = []
+
+ # Count instances for each compound on reactant side
+ reactant_instance_counts: Dict[str, int] = {}
+ for cpd, letters in reactant_items:
+ reactant_instance_counts[cpd] = (
+ reactant_instance_counts.get(cpd, 0) + 1
+ )
+ instance = reactant_instance_counts[cpd]
+ for i, ch in enumerate(letters, start=1):
+ all_reactant_atoms.append((cpd, i, instance))
+ letter_sequence.append(ch)
+
+ # Build mapping for products
+ atom_map = AtomMap()
+ letter_usage_count: Dict[str, int] = {}
+
+ # Count instances for each compound on product side
+ product_instance_counts: Dict[str, int] = {}
+ for cpd, letters in product_items:
+ product_instance_counts[cpd] = (
+ product_instance_counts.get(cpd, 0) + 1
+ )
+ instance = product_instance_counts[cpd]
+ for i, ch in enumerate(letters, start=1):
+ # Count how many times we've used this letter
+ usage_count = letter_usage_count.get(ch, 0)
+
+ # Find the nth occurrence of this letter in the sequence
+ occurrences = [
+ idx
+ for idx, letter in enumerate(letter_sequence)
+ if letter == ch
+ ]
+
+ if usage_count >= len(occurrences):
+ raise ValueError(
+ f"Letter '{ch}' used more times in products "
+ f"than available in reactants. Available: "
+ f"{len(occurrences)}, Used: {usage_count + 1}"
+ )
+
+ atom_idx = occurrences[usage_count]
+ src_cpd, src_atom, src_inst = all_reactant_atoms[atom_idx]
+
+ atom_map = atom_map.add(
+ cpd, i, src_cpd, src_atom, None, instance, src_inst
+ )
+
+ letter_usage_count[ch] = usage_count + 1
+
+ return cls(maps=[atom_map])
+
+ def to_letter_notation(self) -> str:
+ """Convert atom mapping to letter notation string.
+
+ Returns a string representation of the atom mapping in letter
+ notation format. If multiple elements are present, they are shown
+ on separate lines. If multiple mapping variants exist, they are
+ shown as alternatives.
+
+ Example: "A(abc) + B(de) -> C(abcde)"
+ With elements: "C: A(ab) -> B(ab)\nN: A(c) -> B(c)"
+
+ Returns:
+ Letter notation string representation of the mapping(s)
+ """
+ from collections import defaultdict
+
+ if not self.maps:
+ return ""
+
+ # Handle multiple mapping variants
+ if len(self.maps) > 1:
+ # For multiple variants, show each on a separate line
+ results = []
+ for i, atom_map in enumerate(self.maps):
+ weight_str = f" [{self.weights[i]:.3f}]" if self.weights else ""
+ single_mapping = AtomMapping(maps=[atom_map])
+ result = single_mapping.to_letter_notation()
+ if result:
+ results.append(f"Variant {i+1}{weight_str}: {result}")
+ return "\n".join(results)
+
+ # Single mapping
+ atom_map = self.maps[0]
+ if not atom_map.mapping:
+ return ""
+
+ # Group mappings by element type
+ element_groups = defaultdict(list)
+ for prod_addr, react_addr in atom_map.mapping.items():
+ element = prod_addr.element or "C"
+ element_groups[element].append((prod_addr, react_addr))
+
+ # Process each element separately
+ element_results = []
+
+ for element in sorted(element_groups.keys()):
+ mappings = element_groups[element]
+
+ # Collect compounds and their atoms for this element
+ reactant_atoms = defaultdict(lambda: defaultdict(list))
+ product_atoms = defaultdict(lambda: defaultdict(list))
+
+ # Assign letters sequentially based on reactant order
+ letters = (
+ "abcdefghijklmnopqrstuvwxyz"
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+ )
+ letter_idx = 0
+ atom_to_letter = {}
+
+ # Sort mappings by reactant position for consistent
+ # letter assignment
+ sorted_mappings = sorted(
+ mappings, key=lambda x: (x[1].mol, x[1].instance, x[1].index)
+ )
+
+ for prod_addr, react_addr in sorted_mappings:
+ key = (react_addr.mol, react_addr.instance, react_addr.index)
+ if key not in atom_to_letter:
+ if letter_idx >= len(letters):
+ # Generate extended letters like aa, ab, ac...
+ div, mod = divmod(letter_idx - len(letters), 26)
+ atom_to_letter[key] = letters[div % 26] + letters[mod]
+ else:
+ atom_to_letter[key] = letters[letter_idx]
+ letter_idx += 1
+
+ letter = atom_to_letter[key]
+ reactant_atoms[react_addr.mol][react_addr.instance].append(
+ (react_addr.index, letter)
+ )
+ product_atoms[prod_addr.mol][prod_addr.instance].append(
+ (prod_addr.index, letter)
+ )
+
+ # Build reaction string for this element
+ def build_side(atoms_dict):
+ """Build one side of the reaction string."""
+ parts = []
+ for cpd in sorted(atoms_dict.keys()):
+ instances = atoms_dict[cpd]
+ for instance in sorted(instances.keys()):
+ atoms = sorted(instances[instance])
+ letter_str = "".join(ltr for _, ltr in atoms)
+ parts.append(f"{cpd}({letter_str})")
+ return parts
+
+ reactant_parts = build_side(reactant_atoms)
+ product_parts = build_side(product_atoms)
+
+ if reactant_parts and product_parts:
+ reaction = (
+ f"{' + '.join(reactant_parts)} -> "
+ f"{' + '.join(product_parts)}"
+ )
+ # Only add element prefix if there are multiple elements
+ if len(element_groups) > 1:
+ reaction = f"{element}: {reaction}"
+ element_results.append(reaction)
+
+ # Join results
+ if len(element_results) == 0:
+ return ""
+ elif len(element_results) == 1:
+ return element_results[0]
+ else:
+ return "\n".join(element_results)
+
+ @classmethod
+ def from_fluxml_cfg(
+ cls,
+ reactant_cfgs: Dict[str, str],
+ product_cfgs: List[Tuple[str, str]],
+ reactant_order: List[str],
+ ) -> "AtomMapping":
+ """Parse FluxML cfg strings.
+
+ Args:
+ reactant_cfgs: Dict of reactant_id -> cfg string
+ product_cfgs: List of (product_id, cfg string) tuples
+ reactant_order: Ordered list of reactant IDs
+ (with repetitions for instances)
+
+ In product cfg "C#1@2 C#3@1", each token means:
+ - C#k@r: take atom k from reactant at position r
+ """
+ atom_map = AtomMap()
+
+ # Build instance mapping: position -> (compound, instance)
+ # This handles repeated compounds in reactant_order
+ position_to_instance = {}
+ compound_counts = {}
+ for idx, cpd in enumerate(reactant_order):
+ compound_counts[cpd] = compound_counts.get(cpd, 0) + 1
+ position_to_instance[idx] = (cpd, compound_counts[cpd])
+
+ # Track product instances
+ product_instances = {}
+ product_atom_indices = {}
+
+ # Process product cfgs
+ for pcid, cfg in product_cfgs:
+ # Track instance number for this product
+ product_instances[pcid] = product_instances.get(pcid, 0) + 1
+ prod_instance = product_instances[pcid]
+
+ # Track atom index for this product instance
+ key = (pcid, prod_instance)
+ if key not in product_atom_indices:
+ product_atom_indices[key] = 0
+
+ for tok in cfg.split():
+ m = re.fullmatch(r"([A-Z])#(\d+)@(\d+)", tok)
+ if not m:
+ raise ValueError(f"Bad product cfg token: {tok}")
+ atom_type = m.group(1)
+ src_atom = int(m.group(2))
+ src_reactant_idx = int(m.group(3)) - 1 # to 0-based
+
+ if src_reactant_idx not in position_to_instance:
+ raise IndexError(
+ f"Reactant index @{src_reactant_idx+1} out of range"
+ )
+
+ src_cpd, src_instance = position_to_instance[src_reactant_idx]
+ product_atom_indices[key] += 1
+ prod_atom_idx = product_atom_indices[key]
+
+ atom_map = atom_map.add(
+ pcid,
+ prod_atom_idx,
+ src_cpd,
+ src_atom,
+ atom_type,
+ prod_instance,
+ src_instance,
+ )
+
+ return cls(maps=[atom_map])
+
+ def to_fluxml_string(
+ self,
+ reactants: Optional[List[str]] = None,
+ products: Optional[List[str]] = None,
+ ) -> Optional[str]:
+ """Convert to FluxML-style atom mapping string.
+
+ Args:
+ reactants: Optional list of reactant IDs. If None,
+ derives from maps.
+ products: Optional list of product IDs. If None,
+ derives from maps.
+
+ Returns:
+ FluxML-style string representation of the mapping
+ """
+ if not self.maps:
+ return None
+
+ # Use first mapping (for variants, caller should iterate)
+ atom_map = self.maps[0]
+
+ # Derive reactants and products from mapping if not provided
+ if reactants is None:
+ reactants = sorted(
+ set(addr.mol for addr in atom_map.mapping.values())
+ )
+ if products is None:
+ products = sorted(set(addr.mol for addr in atom_map.mapping.keys()))
+
+ # Build reactant and product strings
+ reactant_parts = []
+ product_parts = []
+
+ # Group mappings by compound and instance
+ reactant_instances: Dict[str, set] = {}
+ for addr in atom_map.mapping.values():
+ if addr.mol not in reactant_instances:
+ reactant_instances[addr.mol] = set()
+ reactant_instances[addr.mol].add(addr.instance)
+
+ for cpd in reactants:
+ if cpd in reactant_instances:
+ for instance in sorted(reactant_instances[cpd]):
+ atoms = []
+ for addr in atom_map.mapping.values():
+ if addr.mol == cpd and addr.instance == instance:
+ atoms.append((addr.index, addr.element or "C"))
+ if atoms:
+ atom_str = " ".join(
+ f"{at}#{idx}" for idx, at in sorted(atoms)
+ )
+ num_inst = len(reactant_instances[cpd])
+ prefix = f"{num_inst}" if num_inst > 1 else ""
+ reactant_parts.append(f"{prefix}{cpd}({atom_str})")
+
+ product_instances: Dict[str, set] = {}
+ for addr in atom_map.mapping.keys():
+ if addr.mol not in product_instances:
+ product_instances[addr.mol] = set()
+ product_instances[addr.mol].add(addr.instance)
+
+ for cpd in products:
+ if cpd in product_instances:
+ for instance in sorted(product_instances[cpd]):
+ atoms = []
+ for prod_addr in atom_map.mapping.keys():
+ if (
+ prod_addr.mol == cpd
+ and prod_addr.instance == instance
+ ):
+ src_addr = atom_map.mapping[prod_addr]
+ atoms.append(
+ (
+ prod_addr.index,
+ prod_addr.element or "C",
+ src_addr.mol,
+ src_addr.index,
+ )
+ )
+ if atoms:
+ atom_str = " ".join(
+ f"{at}#{pa}" for pa, at, _, _ in sorted(atoms)
+ )
+ num_inst = len(product_instances[cpd])
+ prefix = f"{num_inst}" if num_inst > 1 else ""
+ product_parts.append(f"{prefix}{cpd}({atom_str})")
+
+ if not reactant_parts or not product_parts:
+ return None
+
+ reactant_side = " + ".join(reactant_parts)
+ product_side = " + ".join(product_parts)
+ return f'"{reactant_side} => {product_side}"'
+
+ def merge_symmetric_mappings(self, other: "AtomMapping") -> "AtomMapping":
+ """Merge with another mapping to handle symmetric reactions."""
+ new_maps = list(self.maps) + list(other.maps)
+ # If weights exist, combine them proportionally
+ if self.weights and other.weights:
+ total = len(self.maps) + len(other.maps)
+ self_weight = len(self.maps) / total
+ other_weight = len(other.maps) / total
+ new_weights = [w * self_weight for w in self.weights] + [
+ w * other_weight for w in other.weights
+ ]
+ else:
+ new_weights = None
+
+ return AtomMapping(maps=new_maps, weights=new_weights)
+
+ def transform_isotopomers(
+ self,
+ reactant_distributions: Dict[Tuple[str, int], np.ndarray],
+ reactant_order: List[str],
+ product_order: List[str],
+ which_map: int = 0,
+ ) -> Dict[Tuple[str, int], np.ndarray]:
+ """Transform isotopomer distributions through the reaction.
+
+ Args:
+ reactant_distributions: Dict of (compound_id, instance) ->
+ isotopomer distribution
+ reactant_order: Order of reactants (with repetitions for multiple
+ instances)
+ product_order: Order of products (with repetitions for multiple
+ instances)
+ which_map: Which mapping variant to use
+
+ Returns:
+ Dict of (product_id, instance) -> isotopomer distribution
+ """
+ if which_map >= len(self.maps):
+ raise ValueError(f"Map index {which_map} out of range")
+
+ atom_map = self.maps[which_map]
+
+ # Build concatenated reactant atom order with instances
+ src_atoms: List[Tuple[str, int, str, int]] = []
+ # Count instances per compound
+ reactant_instances: Dict[str, int] = {}
+ for cpd in reactant_order:
+ reactant_instances[cpd] = reactant_instances.get(cpd, 0) + 1
+ instance = reactant_instances[cpd]
+ key = (cpd, instance)
+ if key not in reactant_distributions:
+ raise ValueError(f"Missing distribution for {key}")
+ n_atoms = len(reactant_distributions[key]).bit_length() - 1
+ for i in range(1, n_atoms + 1):
+ src_atoms.append((cpd, i, "C", instance))
+
+ # Build product atom order with instances
+ prod_atoms: List[Tuple[str, int, str, int]] = []
+ product_instances: Dict[str, int] = {}
+ for cpd in product_order:
+ product_instances[cpd] = product_instances.get(cpd, 0) + 1
+ instance = product_instances[cpd]
+ # Find max atom index for this product instance
+ max_idx = 0
+ for p_cpd, p_atom, _, p_inst in atom_map.mapping.keys():
+ if p_cpd == cpd and p_inst == instance:
+ max_idx = max(max_idx, p_atom)
+ for i in range(1, max_idx + 1):
+ prod_atoms.append((cpd, i, "C", instance))
+
+ # Create mapping from product atom positions to source positions
+ src_pos_lookup = {atom: i for i, atom in enumerate(src_atoms)}
+ src_positions = []
+ for prod_atom in prod_atoms:
+ if prod_atom not in atom_map.mapping:
+ raise ValueError(f"Product atom {prod_atom} not in mapping")
+ src_atom = atom_map.mapping[prod_atom]
+ src_positions.append(src_pos_lookup[src_atom])
+
+ # Build joint reactant distribution
+ joint_dist = np.array([1.0])
+ instance_counts: Dict[str, int] = {}
+ for cpd in reactant_order:
+ instance_counts[cpd] = instance_counts.get(cpd, 0) + 1
+ instance = instance_counts[cpd]
+ key = (cpd, instance)
+ joint_dist = np.kron(joint_dist, reactant_distributions[key])
+
+ # Transform to product distribution
+ n_src = len(src_atoms)
+ n_prod = len(prod_atoms)
+
+ if joint_dist.size != (1 << n_src):
+ raise ValueError("Reactant distribution size mismatch")
+
+ # Compute product distribution
+ prod_dist = np.zeros(1 << n_prod)
+ src_indices = np.arange(1 << n_src, dtype=np.uint32)
+
+ # Map bits from source to product positions
+ prod_indices = np.zeros_like(src_indices)
+ for k, src_pos in enumerate(src_positions):
+ bit = (src_indices >> src_pos) & 1
+ prod_indices |= bit << k
+
+ np.add.at(prod_dist, prod_indices, joint_dist)
+
+ # Split into per-product distributions
+ result = {}
+ offset = 0
+ product_instance_counts: Dict[str, int] = {}
+ for cpd in product_order:
+ product_instance_counts[cpd] = (
+ product_instance_counts.get(cpd, 0) + 1
+ )
+ instance = product_instance_counts[cpd]
+ n_atoms = sum(
+ 1
+ for (c, _, _, inst) in prod_atoms
+ if c == cpd and inst == instance
+ )
+ if n_atoms > 0:
+ size = 1 << n_atoms
+ result[(cpd, instance)] = prod_dist[offset : offset + size]
+ offset += size
+
+ return result
+
+ def transform_with_symmetry(
+ self,
+ reactant_distributions: Dict[Tuple[str, int], np.ndarray],
+ reactant_order: List[str],
+ product_order: List[str],
+ ) -> Dict[Tuple[str, int], np.ndarray]:
+ """Average isotopomer transformation over all mapping variants."""
+ if not self.maps:
+ raise ValueError("No atom maps available")
+
+ weights = (
+ self.weights
+ if self.weights
+ else [1.0 / len(self.maps)] * len(self.maps)
+ )
+
+ result: Dict[Tuple[str, int], np.ndarray] = {}
+ for i, weight in enumerate(weights):
+ prod_dists = self.transform_isotopomers(
+ reactant_distributions, reactant_order, product_order, i
+ )
+ for key, dist in prod_dists.items():
+ if key not in result:
+ result[key] = weight * dist
+ else:
+ result[key] += weight * dist
+
+ return result
diff --git a/src/fluxomics_data_model/model/constraint.py b/src/fluxomics_data_model/model/constraint.py
new file mode 100644
index 0000000..feb56a1
--- /dev/null
+++ b/src/fluxomics_data_model/model/constraint.py
@@ -0,0 +1,77 @@
+"""
+FluxML constraint definitions.
+"""
+
+from typing import Optional
+from pydantic import BaseModel, Field
+from ..core.common import TextualOrMath
+
+
+class NetConstraints(BaseModel):
+ """
+ FluxML net flux constraints.
+
+ Corresponds to fluxml/constraints/net
+ """
+
+ expression: TextualOrMath = Field(
+ description="Net flux constraint expression"
+ )
+
+ class Config:
+ frozen = True
+ extra = "forbid"
+
+
+class ExchangeConstraints(BaseModel):
+ """
+ FluxML exchange flux constraints.
+
+ Corresponds to fluxml/constraints/xch
+ """
+
+ expression: TextualOrMath = Field(
+ description="Exchange flux constraint expression"
+ )
+
+ class Config:
+ frozen = True
+ extra = "forbid"
+
+
+class MetaboliteSizeConstraints(BaseModel):
+ """
+ FluxML metabolite size constraints.
+
+ Corresponds to fluxml/constraints/metabolitesize
+ """
+
+ expression: TextualOrMath = Field(
+ description="Metabolite size constraint expression"
+ )
+
+ class Config:
+ frozen = True
+ extra = "forbid"
+
+
+class Constraints(BaseModel):
+ """
+ FluxML constraints collection.
+
+ Corresponds to fluxml/constraints
+ """
+
+ net: Optional[NetConstraints] = Field(
+ default=None, description="Net flux constraints"
+ )
+ xch: Optional[ExchangeConstraints] = Field(
+ default=None, description="Exchange flux constraints"
+ )
+ metabolitesize: Optional[MetaboliteSizeConstraints] = Field(
+ default=None, description="Metabolite size constraints"
+ )
+
+ class Config:
+ frozen = True
+ extra = "forbid"
diff --git a/src/fluxomics_data_model/model/metabolite.py b/src/fluxomics_data_model/model/metabolite.py
new file mode 100644
index 0000000..5c5b15a
--- /dev/null
+++ b/src/fluxomics_data_model/model/metabolite.py
@@ -0,0 +1,245 @@
+"""
+FluxML metabolite definitions.
+"""
+
+from typing import Optional, List
+from pydantic import BaseModel, Field, field_validator, computed_field
+import re
+import jax.numpy as jnp
+from ..core.common import Annotation, JAXArray
+
+
+# Atomic weights of elements (atomic mass units)
+ELEMENTS_AND_MOLECULAR_WEIGHTS = {
+ "H": 1.007940,
+ "He": 4.002602,
+ "Li": 6.941000,
+ "Be": 9.012182,
+ "B": 10.811000,
+ "C": 12.010700,
+ "N": 14.006700,
+ "O": 15.999400,
+ "F": 18.998403,
+ "Ne": 20.179700,
+ "Na": 22.989770,
+ "Mg": 24.305000,
+ "Al": 26.981538,
+ "Si": 28.085500,
+ "P": 30.973761,
+ "S": 32.065000,
+ "Cl": 35.453000,
+ "Ar": 39.948000,
+ "K": 39.098300,
+ "Ca": 40.078000,
+ "Sc": 44.955910,
+ "Ti": 47.867000,
+ "V": 50.941500,
+ "Cr": 51.996100,
+ "Mn": 54.938049,
+ "Fe": 55.845000,
+ "Co": 58.933200,
+ "Ni": 58.693400,
+ "Cu": 63.546000,
+ "Zn": 65.409000,
+ "Ga": 69.723000,
+ "Ge": 72.640000,
+ "As": 74.921600,
+ "Se": 78.960000,
+ "Br": 79.904000,
+ "Kr": 83.798000,
+ "Rb": 85.467800,
+ "Sr": 87.620000,
+ "Y": 88.905850,
+ "Zr": 91.224000,
+ "Nb": 92.906380,
+ "Mo": 95.940000,
+ "Tc": 98.000000,
+ "Ru": 101.070000,
+ "Rh": 102.905500,
+ "Pd": 106.420000,
+ "Ag": 107.868200,
+ "Cd": 112.411000,
+ "In": 114.818000,
+ "Sn": 118.710000,
+ "Sb": 121.760000,
+ "Te": 127.600000,
+ "I": 126.904470,
+ "Xe": 131.293000,
+ "Cs": 132.905450,
+ "Ba": 137.327000,
+ "La": 138.905500,
+ "Ce": 140.116000,
+ "Pr": 140.907650,
+ "Nd": 144.240000,
+ "Pm": 145.000000,
+ "Sm": 150.360000,
+ "Eu": 151.964000,
+ "Gd": 157.250000,
+ "Tb": 158.925340,
+ "Dy": 162.500000,
+ "Ho": 164.930320,
+ "Er": 167.259000,
+ "Tm": 168.934210,
+ "Yb": 173.040000,
+ "Lu": 174.967000,
+ "Hf": 178.490000,
+ "Ta": 180.947900,
+ "W": 183.840000,
+ "Re": 186.207000,
+ "Os": 190.230000,
+ "Ir": 192.217000,
+ "Pt": 195.078000,
+ "Au": 196.966550,
+ "Hg": 200.590000,
+ "Tl": 204.383300,
+ "Pb": 207.200000,
+ "Bi": 208.980380,
+ "Po": 209.000000,
+ "At": 210.000000,
+ "Rn": 222.000000,
+ "Fr": 223.000000,
+ "Ra": 226.000000,
+ "Ac": 227.000000,
+ "Th": 232.038100,
+ "Pa": 231.035880,
+ "U": 238.028910,
+ "Np": 237.000000,
+ "Pu": 244.000000,
+ "Am": 243.000000,
+ "Cm": 247.000000,
+ "Bk": 247.000000,
+ "Cf": 251.000000,
+ "Es": 252.000000,
+ "Fm": 257.000000,
+ "Md": 258.000000,
+ "No": 259.000000,
+ "Lr": 262.000000,
+ "Rf": 261.000000,
+ "Db": 262.000000,
+ "Sg": 266.000000,
+ "Bh": 264.000000,
+ "Hs": 277.000000,
+ "Mt": 268.000000,
+ "Ds": 281.000000,
+ "Rg": 272.000000,
+ "Cn": 285.000000,
+ "Uuq": 289.000000,
+ "Uuh": 292.000000,
+}
+
+
+class Metabolite(BaseModel):
+ """
+ FluxML metabolite definition.
+
+ Corresponds to fluxml/reactionnetwork/metabolites/metabolite
+ """
+
+ id: str = Field(description="Metabolite identifier")
+ name: Optional[str] = Field(default=None, description="Metabolite name")
+ atoms: Optional[int] = Field(
+ default=0,
+ ge=0,
+ le=1024,
+ description="Number of atoms for the labelling experiment",
+ )
+ weight: Optional[float] = Field(
+ default=None, ge=0.0, le=50000.0, description="Molecular weight (g/mol)"
+ )
+ charge: Optional[int] = Field(
+ default=None, ge=-100, le=100, description="Charge of the metabolite"
+ )
+ formula: Optional[str] = Field(
+ default=None,
+ description="Chemical elements invovled in labelling experiment",
+ )
+ compartment: Optional[str] = Field(
+ default=None, description="Compartment where metabolite is located"
+ )
+ annotations: List[Annotation] = Field(
+ default_factory=list, description="Annotations"
+ )
+
+ # JAX-compatible numerical representation
+ jax_atoms: Optional[JAXArray] = Field(
+ default=None, exclude=True, description="JAX atom array"
+ )
+
+ class Config:
+ frozen = True
+ extra = "forbid"
+
+ @field_validator("formula")
+ @classmethod
+ def validate_formula(cls, v: Optional[str]) -> Optional[str]:
+ """Validate chemical formula string."""
+ if not v or v == "0" or v == "":
+ return v
+ pattern = r"^(([A-Z][a-z]?)([0-9.]+[0-9.]?|(?=[A-Z])?))+$"
+ if not re.match(pattern, v):
+ raise ValueError(f"Invalid chemical formula format: {v}")
+ return v
+
+ @staticmethod
+ def _parse_formula(formula: str) -> dict[str, float]:
+ """Parse chemical formula into element counts."""
+ if not formula or formula in ["0", ""]:
+ return {}
+
+ elements = {}
+ # Pattern to match element-count pairs
+ pattern = r"([A-Z][a-z]?)([0-9]*\.?[0-9]*)"
+ matches = re.findall(pattern, formula)
+
+ for element, count_str in matches:
+ if count_str == "":
+ count = 1.0
+ else:
+ count = float(count_str)
+ elements[element] = elements.get(element, 0.0) + count
+
+ return elements
+
+ @computed_field
+ @property
+ def molecular_weight(self) -> Optional[float]:
+ """Calculate molecular weight from formula if available."""
+ if not self.formula or self.formula in ["0", ""]:
+ return None
+
+ try:
+ elements = self._parse_formula(self.formula)
+ total_weight = 0.0
+
+ for element, count in elements.items():
+ if element not in ELEMENTS_AND_MOLECULAR_WEIGHTS:
+ return None # Unknown element, can't calculate
+ total_weight += ELEMENTS_AND_MOLECULAR_WEIGHTS[element] * count
+
+ return round(total_weight, 6)
+ except Exception:
+ return None
+
+ @property
+ def atom_vector(self) -> jnp.ndarray:
+ """Get JAX array representation of atoms."""
+ if self.jax_atoms is None:
+ # Create atom vector based on configuration
+ if self.atoms == 0:
+ return jnp.array([])
+ return jnp.ones(self.atoms, dtype=jnp.float32)
+ return self.jax_atoms.to_jax_array()
+
+ def with_jax_atoms(self, atoms: jnp.ndarray) -> "Metabolite":
+ """Create new metabolite with JAX atom array."""
+ return Metabolite(
+ id=self.id,
+ name=self.name,
+ atoms=self.atoms,
+ weight=self.weight,
+ charge=self.charge,
+ formula=self.formula,
+ compartment=self.compartment,
+ annotations=self.annotations,
+ jax_atoms=JAXArray.from_jax_array(atoms),
+ )
diff --git a/src/fluxomics_data_model/model/reaction.py b/src/fluxomics_data_model/model/reaction.py
new file mode 100644
index 0000000..d25c859
--- /dev/null
+++ b/src/fluxomics_data_model/model/reaction.py
@@ -0,0 +1,146 @@
+"""
+FluxML reaction definitions.
+"""
+
+from typing import Optional, List, Dict
+from pydantic import BaseModel, Field
+import jax.numpy as jnp
+from ..core.common import Annotation, JAXArray
+from .atom_mapping import AtomMapping
+
+
+# AtomMapping is now imported from atom_mapping.py
+
+
+class Reaction(BaseModel):
+ """
+ Fluxomics Data Model reaction definition.
+ """
+
+ id: str = Field(description="Reaction identifier")
+ name: Optional[str] = Field(default=None, description="Reaction name")
+ reversibility: bool = Field(
+ default=True, description="Reaction reversibility"
+ )
+ annotations: List[Annotation] = Field(
+ default_factory=list, description="Annotations"
+ )
+ reactants: List[str] = Field(
+ default_factory=list, description="Reactant metabolite IDs"
+ )
+ products: List[str] = Field(
+ default_factory=list, description="Product metabolite IDs"
+ )
+ atom_mapping: Optional[AtomMapping] = Field(
+ default=None, description="Atom mappings for this reaction"
+ )
+
+ # JAX-compatible numerical representation
+ flux_bounds_array: Optional[JAXArray] = Field(
+ default=None, exclude=True, description="Flux bounds"
+ )
+ stoichiometry_dict: Optional[Dict[str, float]] = Field(
+ default=None, exclude=True, description="Stoichiometry"
+ )
+
+ class Config:
+ frozen = True
+ extra = "forbid"
+
+ @property
+ def reactant_ids(self) -> frozenset[str]:
+ """Get all reactant metabolite IDs."""
+ return frozenset(self.reactants)
+
+ @property
+ def product_ids(self) -> frozenset[str]:
+ """Get all product metabolite IDs."""
+ return frozenset(self.products)
+
+ @property
+ def metabolites(self) -> frozenset[str]:
+ """Get all participating metabolite IDs."""
+ return self.reactant_ids | self.product_ids
+
+ @property
+ def flux_bounds(self) -> jnp.ndarray:
+ """Get flux bounds as JAX array [lower, upper]."""
+ if self.flux_bounds_array is None:
+ if self.reversibility:
+ return jnp.array([-1000.0, 1000.0])
+ else:
+ return jnp.array([0.0, 1000.0])
+ return self.flux_bounds_array.to_jax_array()
+
+ @property
+ def equation(self) -> str:
+ """Get reaction equation string."""
+ reactants = " + ".join(self.reactants)
+ products = " + ".join(self.products)
+ arrow = " <=> " if self.reversibility else " => "
+ return f"{reactants}{arrow}{products}"
+
+ def get_atom_mapping_string(self) -> Optional[str]:
+ """
+ Get the complete atom mapping for this reaction as a string.
+ """
+ if not self.atom_mapping:
+ return None
+
+ return self.atom_mapping.to_fluxml_string(self.reactants, self.products)
+
+ def with_flux_bounds(self, lower: float, upper: float) -> "Reaction":
+ """Create new reaction with specified flux bounds."""
+ bounds_array = jnp.array([lower, upper])
+ return Reaction(
+ id=self.id,
+ reversibility=self.reversibility,
+ annotations=self.annotations,
+ reactants=self.reactants,
+ products=self.products,
+ flux_bounds_array=JAXArray.from_jax_array(bounds_array),
+ stoichiometry_dict=self.stoichiometry_dict,
+ )
+
+ def with_stoichiometry(self, stoichiometry: Dict[str, float]) -> "Reaction":
+ """Create new reaction with specified stoichiometry."""
+ return Reaction(
+ id=self.id,
+ reversibility=self.reversibility,
+ annotations=self.annotations,
+ reactants=self.reactants,
+ products=self.products,
+ flux_bounds_array=self.flux_bounds_array,
+ stoichiometry_dict=stoichiometry,
+ )
+
+ def get_stoichiometric_vector(
+ self, metabolite_ids: List[str]
+ ) -> jnp.ndarray:
+ """
+ Get stoichiometric vector for this reaction.
+
+ Args:
+ metabolite_ids: Ordered list of metabolite IDs
+
+ Returns:
+ JAX array with stoichiometric coefficients
+ """
+ if self.stoichiometry_dict is None:
+ # Default stoichiometry: -1 for reactants, +1 for products
+ coefficients = []
+ for metabolite_id in metabolite_ids:
+ if metabolite_id in self.reactant_ids:
+ coefficients.append(-1.0)
+ elif metabolite_id in self.product_ids:
+ coefficients.append(1.0)
+ else:
+ coefficients.append(0.0)
+ return jnp.array(coefficients)
+ else:
+ return jnp.array(
+ [
+ self.stoichiometry_dict.get(metabolite_id, 0.0)
+ for metabolite_id in metabolite_ids
+ ]
+ )
diff --git a/src/fluxomics_data_model/reaction_network.py b/src/fluxomics_data_model/reaction_network.py
deleted file mode 100644
index 228cfea..0000000
--- a/src/fluxomics_data_model/reaction_network.py
+++ /dev/null
@@ -1,49 +0,0 @@
-"""Format for a reaction network."""
-
-from pydantic import BaseModel, NonNegativeInt
-
-
-class Species(BaseModel):
- """A species"""
-
- id: str
- name: str | None = None
- n_labellable_atom: NonNegativeInt
-
-
-class Reactant(BaseModel):
- """A reactant"""
-
- id: str
- is_product: bool
- atom_pattern: str
- name: str | None = None
-
- @property
- def is_substrate(self) -> bool:
- return not self.is_product
-
-
-class Reaction(BaseModel):
- """A reaction"""
-
- id: str
- bidirectional: bool
- reactants: list[Reactant]
- name: str
- comment: str | None = None
-
- @property
- def substrates(self) -> list[Reactant]:
- return [r for r in self.reactants if r.is_substrate]
-
- @property
- def products(self) -> list[Reactant]:
- return [r for r in self.reactants if r.is_product]
-
-
-class ReactionNetwork(BaseModel):
- """A class representing the reactions in a metabolic network."""
-
- reactions: list[Reaction]
- species: list[Species]