lcvk 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
lcvk-0.1.1/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ # Licensed under the BSD 3-Clause License.
2
+ #
3
+ # Unless required by applicable law or agreed to in writing, software
4
+ # distributed under the License is distributed on an "AS IS" BASIS,
5
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6
+ # See the License for the specific language governing permissions and
7
+ # limitations under the License.
lcvk-0.1.1/MANIFEST.in ADDED
@@ -0,0 +1,3 @@
1
+ include README.md
2
+ include LICENSE
3
+ include requirements.txt
lcvk-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: lcvk
3
+ Version: 0.1.1
4
+ Summary: Li-circular van Krevelen diagram
5
+ Home-page: https://github.com/shuzhao-li-lab/Li_CVK_diagram
6
+ Author: Shuzhao Li
7
+ Author-email: shuzhao.li@gmail.com
8
+ License: BSD 3-Clause
9
+ Keywords: bioinformatics metabolic pathway visualization
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: BSD License
13
+ Classifier: Natural Language :: English
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
17
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.7
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: numpy
23
+ Requires-Dist: scipy
24
+ Requires-Dist: matplotlib
25
+ Requires-Dist: mass2chem
26
+ Dynamic: author
27
+ Dynamic: author-email
28
+ Dynamic: classifier
29
+ Dynamic: description
30
+ Dynamic: description-content-type
31
+ Dynamic: home-page
32
+ Dynamic: keywords
33
+ Dynamic: license
34
+ Dynamic: license-file
35
+ Dynamic: requires-dist
36
+ Dynamic: requires-python
37
+ Dynamic: summary
38
+
39
+ # Circular van Krevelen diagram for visualizing metabolic pathways
40
+
41
+ Emerging biochemical data require effective pathway visualization, but traditional metabolic maps rely on manual layouts that fall behind new scientific discoveries. `lcvk` implements a circularized van Krevelen diagram: metabolites are placed by chemical formula, using elemental ratios (H:C mapped to the angular axis, NOPS:C or a related ratio mapped to the radial axis) instead of a hand-drawn layout. Because the coordinates come from chemical principles rather than manual placement, pathway diagrams are automated and consistent across datasets, models, and labs.
42
+
43
+ Preprint is released: https://www.biorxiv.org/content/10.1101/2025.05.31.657198v1
44
+
45
+ This is the repo for the `lcvk` package, which includes code, example data and notebook templates.
46
+
47
+ Version 0.1 is proof of principle.
48
+
49
+ ![example_pathway_fa](docs/source/_static/worm_human_fa.png)
50
+
51
+ ![example_pathway_alanine](docs/source/_static/alanine.png)
52
+
53
+ Example applications include
54
+ - Visualization of metabolic pathways and maps.
55
+ - Summurizing differential abundance in metabolomic data.
56
+ - Display of isotopic labeling patterns.
57
+ - Extension of metabolic knowledge by new reactions.
58
+
59
+ ## Install
60
+
61
+ ```bash
62
+ pip install -e .
63
+ ```
64
+
65
+ Requires Python >= 3.7 and `numpy`, `scipy`, `matplotlib`, `mass2chem` (see `requirements.txt`).
66
+
67
+ ## Quickstart
68
+
69
+ ```python
70
+ import lcvk
71
+
72
+ # list_cpds: [{'id': ..., 'name': ..., 'neutral_formula': 'C6H12O6'}, ...]
73
+ # list_edges: [(source_id, target_id), ...]
74
+ lcvk.plot_lcvk_pathway(
75
+ list_cpds, list_edges,
76
+ formula='neutral_formula', cpd_name='name',
77
+ title='My pathway', outfile='pathway.pdf',
78
+ )
79
+ ```
80
+
81
+ See `notebooks/` for complete worked examples (metabolic pathways, differential metabolomics, isotope labeling).
82
+
83
+ ## Citation
84
+
85
+ Shuzhao Li. Circular van Krevelen diagram for visualizing metabolic pathways. *bioRxiv* (2025). https://doi.org/10.1101/2025.05.31.657198
lcvk-0.1.1/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # Circular van Krevelen diagram for visualizing metabolic pathways
2
+
3
+ Emerging biochemical data require effective pathway visualization, but traditional metabolic maps rely on manual layouts that fall behind new scientific discoveries. `lcvk` implements a circularized van Krevelen diagram: metabolites are placed by chemical formula, using elemental ratios (H:C mapped to the angular axis, NOPS:C or a related ratio mapped to the radial axis) instead of a hand-drawn layout. Because the coordinates come from chemical principles rather than manual placement, pathway diagrams are automated and consistent across datasets, models, and labs.
4
+
5
+ Preprint is released: https://www.biorxiv.org/content/10.1101/2025.05.31.657198v1
6
+
7
+ This is the repo for the `lcvk` package, which includes code, example data and notebook templates.
8
+
9
+ Version 0.1 is proof of principle.
10
+
11
+ ![example_pathway_fa](docs/source/_static/worm_human_fa.png)
12
+
13
+ ![example_pathway_alanine](docs/source/_static/alanine.png)
14
+
15
+ Example applications include
16
+ - Visualization of metabolic pathways and maps.
17
+ - Summurizing differential abundance in metabolomic data.
18
+ - Display of isotopic labeling patterns.
19
+ - Extension of metabolic knowledge by new reactions.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ pip install -e .
25
+ ```
26
+
27
+ Requires Python >= 3.7 and `numpy`, `scipy`, `matplotlib`, `mass2chem` (see `requirements.txt`).
28
+
29
+ ## Quickstart
30
+
31
+ ```python
32
+ import lcvk
33
+
34
+ # list_cpds: [{'id': ..., 'name': ..., 'neutral_formula': 'C6H12O6'}, ...]
35
+ # list_edges: [(source_id, target_id), ...]
36
+ lcvk.plot_lcvk_pathway(
37
+ list_cpds, list_edges,
38
+ formula='neutral_formula', cpd_name='name',
39
+ title='My pathway', outfile='pathway.pdf',
40
+ )
41
+ ```
42
+
43
+ See `notebooks/` for complete worked examples (metabolic pathways, differential metabolomics, isotope labeling).
44
+
45
+ ## Citation
46
+
47
+ Shuzhao Li. Circular van Krevelen diagram for visualizing metabolic pathways. *bioRxiv* (2025). https://doi.org/10.1101/2025.05.31.657198
@@ -0,0 +1,5 @@
1
+ __version__ = "0.1.1"
2
+
3
+ from .metModels import *
4
+ from .polarPlot import *
5
+ from .recipes import *
@@ -0,0 +1,23 @@
1
+ import json
2
+ import numpy as np
3
+
4
+ from .metModels import *
5
+ from .polarPlot import *
6
+
7
+ def run_demo(infile='../data/example_network_32.json'):
8
+ net32 = json.load(open(infile))
9
+ list_cpds, list_edges = net32['nodes'], net32['edges']
10
+ plot_lcvk_pathway(list_cpds, list_edges,
11
+ formula='neutral_formula', cpd_name='name',
12
+ ydata='ratio_NOPS',
13
+ min_theta=0.1*np.pi, max_theta=1.9*np.pi,
14
+ min_radius=0, max_radius=2.5,
15
+ padding=0.1, optimization=False,
16
+ optimization_spacer=0.3,
17
+ fontsize=5, rotation=30, show_names=True, show_formula=False,
18
+ width=9, height=9, # inch
19
+ title='Hello chemistry', outfile='demo_lcvk_plot.pdf', dpi=300
20
+ )
21
+
22
+ if __name__ == '__main__':
23
+ run_demo()
@@ -0,0 +1,169 @@
1
+ #
2
+ # Handling metabolic models, metabolties, reactions, pathways
3
+ #
4
+
5
+ __all__ = ['json_convert_azmuth_mummichog', 'get_pathways_from_gem']
6
+
7
+ # modified from mummichog 2
8
+ def json_convert_azmuth_mummichog(jmodel):
9
+ '''
10
+ The jmodel was parsed from one of the Genome scale metabolic models.
11
+ All compound identifiers and other identifiers are expected to be contained within the model.
12
+ >>> jmodel.keys()
13
+ dict_keys(['id', 'list_of_reactions', 'list_of_compounds', 'list_of_pathways', 'meta_data'])
14
+ >>> jmodel['list_of_compounds'][772]
15
+ {'id': 'ebastineoh', 'name': 'Hydroxylated Ebastine', 'identifiers': {'inchi': 'InChI=1S/C32H39NO3/c1-32(2,24-34)28-17-15-25(16-18-28)30(35)14-9-21-33-22-19-29(20-23-33)36-31(26-10-5-3-6-11-26)27-12-7-4-8-13-27/h3-8,10-13,15-18,29,31,34H,9,14,19-24H2,1-2H3'}, 'neutral_formula': '', 'charge': 0, 'charged_formula': 'C32H39NO3', 'neutral_mono_mass': 0.0, 'SMILES': '', 'inchi': ''}
16
+ >>> jmodel['list_of_pathways'][2]
17
+ {'id': 'group3', 'name': 'Aminoacyl-tRNA biosynthesis',
18
+ 'list_of_reactions': ['HMR_5130', 'HMR_5131', 'HMR_5132', 'HMR_5133', 'HMR_5134', 'HMR_5135',
19
+ 'HMR_5136', 'HMR_5137', 'HMR_5138', 'HMR_5139', 'HMR_5140', 'HMR_5141', 'HMR_5142', 'HMR_5143',
20
+ 'HMR_5144', 'HMR_5145', 'HMR_5146', 'HMR_5147', 'HMR_5148', 'HMR_5149', 'HMR_5150']}
21
+ >>>
22
+ >>> jmodel['list_of_reactions'][22]
23
+ {'id': '24_25DHVITD3tm', 'reactants': ['2425dhvitd3'], 'products': ['2425dhvitd3']}
24
+
25
+ >>> mfn['metabolic_pathways'][51]
26
+ {'cpds': ['C00024', 'C00027', 'strdnccoa', 'C00100', 'C01944', 'q10h2', 'dmhptcoa', 'etfrd', 'C03035', 'etfox', 'dmnoncoa', 'arachdcoa', 'dcsptn1coa', 'ptdcacoa', 'C00412', 'od2coa', 'hdcoa', 'C02593', 'dlnlcgcoa', 'vacccoa', 'C00010', 'C00016', 'adrncoa', 'C00154', 'lneldccoa', 'lnlccoa', 'c226coa', 'lnlncacoa', 'C00399', 'odecoa', 'hpdcacoa', 'C01352', 'eicostetcoa', 'tmndnccoa', 'arachcoa'],
27
+ 'rxns': ['FAOXC2242046m', 'FAOXC200180m', 'FAOXC204', 'FAOXC160', 'FAOXC16080m', 'FAOXC180', 'FAOXC80', 'FAOXC140', 'FAOXC1811601m', 'FAOXC1811603m', 'FAOXC150m', 'FAOXC18480m', 'FAOXC1811602m', 'FAOXC16180m', 'FAOXC170m', 'FAOXC18280m', 'FAOXC182806m', 'FAOXC183803m', 'FAOXC2252053m', 'FAOXC2031836m', 'FAOXC11', 'FAOXC204184m', 'FAOXC226205m', 'FAOXC226', 'ETFQO', 'ETF'],
28
+ 'ecs': ['1.3.99.3', '1.5.5.1'], 'id': 'mfn1v10path136', 'name': 'Fatty acid oxidation'}
29
+ >>> mfn['metabolic_rxns'][2]
30
+ {'reactants': ['C06611'], 'id': 'R05233', 'source': 'kegg_dre', 'ecs': ['1.1.1.1'], 'products': ['C06613'], 'cpds': ['C06611', 'C06613'], 'pathway': '3-Chloroacrylic acid degradation'}
31
+ >>>
32
+ mfn['cpd2pathways'] = {
33
+ 'C06125': ['Glycosphingolipid metabolism', 'Glycosphingolipid biosynthesis - ganglioseries'], ...
34
+ }
35
+ >>> mfn['cpd_edges'][:5]
36
+ [ ['C00106', 'C00526'], ['C05977', 'CE6326'], ['C00043', 'G00174'], ['C00199', 'C00231'], ['C00014', 'C00655']]
37
+ '''
38
+ new = {}
39
+ new['id'] = jmodel['id']
40
+ new['version'] = jmodel['meta_data']['version']
41
+ cpdDict, dict_cpds_def = {}, {}
42
+ for cpd in jmodel['list_of_compounds']:
43
+ cpdDict[cpd['id']] = cpd # no mcg conversion here
44
+ dict_cpds_def[cpd['id']] = cpd['name']
45
+ new['Compounds'] = cpdDict
46
+ new['dict_cpds_def'] = dict_cpds_def
47
+ new['metabolic_rxns'] = jmodel['list_of_reactions']
48
+
49
+ # bunch of indexing
50
+ dict_rxns, rxn2edges, edge2rxn, edge2enzyme = {}, {}, {}, {}
51
+ cpd_edges = []
52
+ for rxn in jmodel['list_of_reactions']:
53
+ dict_rxns[rxn['id']] = rxn
54
+ _edges = []
55
+ for cpd1 in rxn['reactants']:
56
+ for cpd2 in rxn['products']:
57
+ k = [cpd1, cpd2]
58
+ cpd_edges.append(k)
59
+ _edges.append(k)
60
+ edge2rxn[','.join(sorted(k))] = rxn['id']
61
+ # enzymes not in current test model and not tested ------------------ watch out for this, could be rxn['ecs']
62
+ if 'enzymes' in rxn:
63
+ for e in rxn['enzymes']:
64
+ edge2enzyme[','.join(sorted(k))] = e
65
+
66
+ rxn2edges[rxn['id']] = _edges
67
+
68
+ new['cpd_edges'] = cpd_edges
69
+ new['edge2rxn'] = edge2rxn
70
+ new['rxn2edges'] = rxn2edges
71
+ new['edge2enzyme'] = edge2enzyme
72
+ # now do pathways
73
+ metabolic_pathways = []
74
+ for P in jmodel['list_of_pathways']:
75
+ _p = {}
76
+ _p['id'] = P['id']
77
+ _p['name'] = P['name']
78
+ _p['rxns'] = P['list_of_reactions']
79
+ cpds, ecs, edges = [], [], []
80
+ for ii in P['list_of_reactions']:
81
+ rxn = dict_rxns[ii]
82
+ cpds += rxn['reactants'] + rxn['products']
83
+ edges += rxn2edges[ii]
84
+ # enzymes not in current test model and not tested ------------------ watch out for this, could be rxn['ecs']
85
+ if 'enzymes' in rxn:
86
+ ecs += rxn['enzymes']
87
+ _p['cpds'] = list(set(cpds))
88
+ _p['edges'] = edges
89
+ _p['ecs'] = list(set(ecs))
90
+ metabolic_pathways.append(_p)
91
+
92
+ cpd2pathways = {}
93
+ for _p in metabolic_pathways:
94
+ for cpd in _p['cpds']:
95
+ if cpd in cpd2pathways:
96
+ cpd2pathways[cpd].append(_p['name'])
97
+ else:
98
+ cpd2pathways[cpd] = [_p['name']]
99
+
100
+ new['metabolic_pathways'] = metabolic_pathways
101
+ new['cpd2pathways'] = cpd2pathways
102
+
103
+ return new
104
+
105
+
106
+ def get_pathways_from_gem(jmodel):
107
+ '''
108
+ Extract and index pathways from a JSON style,
109
+ concise genome scale metabolic models as in mummichog style.
110
+ '''
111
+ new = {}
112
+ new['id'] = jmodel['id']
113
+ new['version'] = jmodel['meta_data']['version']
114
+ cpdDict, dict_cpds_def = {}, {}
115
+ for cpd in jmodel['list_of_compounds']:
116
+ if 'neutral_mono_mass' in cpd:
117
+ cpd['neutral_formula_mass'] = cpd['neutral_mono_mass']
118
+ if 'neutral_formula_mass' in cpd and cpd['neutral_formula_mass']:
119
+ cpdDict[cpd['id']] = cpd # convert_compound_azimuth_mummichog(cpd, hmdb_dict, kegg_dict)
120
+ dict_cpds_def[cpd['id']] = cpd['name']
121
+ new['Compounds'] = cpdDict
122
+ new['dict_cpds_def'] = dict_cpds_def
123
+ new['metabolic_rxns'] = jmodel['list_of_reactions']
124
+
125
+ # bunch of indexing
126
+ dict_rxns, edge2rxn, rxn2edges, edge2enzyme = {}, {}, {}, {}
127
+ cpd_edges = []
128
+ for rxn in jmodel['list_of_reactions']:
129
+ dict_rxns[rxn['id']] = rxn
130
+ _edges = []
131
+ for cpd1 in rxn['reactants']:
132
+ for cpd2 in rxn['products']:
133
+ k = [cpd1, cpd2]
134
+ cpd_edges.append( tuple(k) ) # tuple to support set
135
+ edge2rxn[','.join(sorted(k))] = rxn['id']
136
+ _edges.append( tuple(k))
137
+ enzymes = rxn.get('ecs', []) or rxn.get('enzymes', [])
138
+ for e in enzymes:
139
+ edge2enzyme[','.join(sorted(k))] = e
140
+
141
+ rxn2edges[rxn['id']] = _edges
142
+
143
+ new['cpd_edges'] = cpd_edges
144
+ new['edge2rxn'] = edge2rxn
145
+ new['edge2enzyme'] = edge2enzyme
146
+ # now do pathways
147
+ metabolic_pathways = []
148
+ for P in jmodel['list_of_pathways']:
149
+ _p = {}
150
+ _p['id'] = P['id']
151
+ _p['name'] = P['name']
152
+ _p['rxns'] = P['list_of_reactions']
153
+ _p['edges'] = []
154
+ cpds, ecs, genes = [], [], []
155
+ for ii in P['list_of_reactions']:
156
+ rxn = dict_rxns[ii]
157
+ _p['edges'] += rxn2edges[rxn['id']]
158
+ cpds += rxn['reactants'] + rxn['products']
159
+ enzymes = rxn.get('ecs', []) or rxn.get('enzymes', [])
160
+ ecs += enzymes
161
+ if 'genes' in rxn:
162
+ genes += rxn['genes']
163
+ _p['cpds'] = list(set(cpds))
164
+ _p['ecs'] = list(set(ecs))
165
+ _p['genes'] = list(set(genes))
166
+ metabolic_pathways.append(_p)
167
+
168
+ return metabolic_pathways
169
+
@@ -0,0 +1,497 @@
1
+ '''
2
+ Core plotting functions for the circular van Krevelen (LCVK) diagram.
3
+
4
+ Metabolites are laid out from their chemical formula rather than a manual
5
+ layout: the H:C ratio is mapped to the angular (theta) axis via
6
+ project_hc_data_radial/standardize_data, and a second ratio (NOPS:C by
7
+ default; see get_chnops_ratios/get_lcvk_ratios) is mapped to the radial axis.
8
+
9
+ Reference: Li S. Circular van Krevelen diagram for visualizing metabolic
10
+ pathways. bioRxiv (2025). https://doi.org/10.1101/2025.05.31.657198
11
+ '''
12
+ import logging
13
+
14
+ import matplotlib.pyplot as plt
15
+ import numpy as np
16
+ from mass2chem.formula import parse_chemformula_dict
17
+
18
+ # used for optimization # import networkx as nx
19
+ from scipy.cluster.hierarchy import linkage, fcluster
20
+ from scipy.spatial.distance import pdist
21
+ from scipy.spatial.distance import euclidean
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ __all__ = [
26
+ 'get_chnops_ratios', 'get_isotopic_chnops_ratios', 'get_lcvk_ratios',
27
+ 'project_hc_data_radial', 'standardize_data', 'clean_pathway_name',
28
+ 'calculate_clean_network', 'get_pathway_coordinates', 'generate_landmarks',
29
+ 'plot_lcvk_pathway', 'assign_new_values', 'part_twins', 'optimize_list_coordinates',
30
+ 'populate_cpd_coordinates', 'plot_lcvk_cartesian_pathway', 'nx_fruchterman_reingold',
31
+ ]
32
+
33
+
34
+ def get_chnops_ratios(f):
35
+ '''
36
+ f: formula
37
+ Returns H/C, O/C, N/C and weighted NOPS/C ratios
38
+ '''
39
+ _d = parse_chemformula_dict(f)
40
+ if 'C' in _d:
41
+ hc = _d['H']/_d['C']
42
+ nc, oc, pc, sc = _d.get('N', 0), _d.get('O', 0), _d.get('P', 0), _d.get('S', 0)
43
+ nops_c = (nc*14 + oc*16 + pc*31 + sc*32) /(_d['C'] * 12)
44
+ oc, nc = oc/_d['C'], nc/_d['C']
45
+ return hc, oc, nc, nops_c
46
+ else:
47
+ return [0] * 4
48
+
49
+ def get_isotopic_chnops_ratios(f):
50
+ '''To implement'''
51
+ raise NotImplementedError
52
+
53
+ def get_lcvk_ratios(f, ccs_offset):
54
+ '''
55
+ Returns H/C, NOPS coefficient, (weighted NOPS/C), CCS offset (CCS deviation from all isomers in pathway).
56
+ ccs_deviation to be calculated elsewhere, as delta to average CCS of all isomers in model.
57
+ '''
58
+ hc, oc, nc, nops_c = get_chnops_ratios(f)
59
+ return hc, oc, nc, nops_c, ccs_offset
60
+
61
+ def project_hc_data_radial(hcList,
62
+ min_theta=0.1*np.pi, max_theta=1.9*np.pi,
63
+ # min_hc=0.5, max_hc=2.5,
64
+ ):
65
+ '''
66
+ Convert H:C ratios to radial theta angles.
67
+ Use standardize_data for fixed H/C scale.
68
+ '''
69
+ _low, _high = min(hcList), max(hcList)
70
+ range = _high - _low
71
+ if range==0:
72
+ return hcList
73
+ else:
74
+ _slope = (max_theta - min_theta)/range
75
+ return [_slope*(x-_low) + min_theta for x in hcList]
76
+
77
+ def standardize_data(hcList, range_degrees=1.8):
78
+ '''
79
+ Test using fixed scale. Use at risk.
80
+ Theta ranges 0.1*pi to 1.9*pi. H:C btw 0.5 and 2.5.
81
+ Therefore HC = 2.5703939893 * x + 0.3
82
+ '''
83
+ return [2.827433388230814 * (x-0.5) + 0.314159265359 for x in hcList]
84
+
85
+ def clean_pathway_name(name):
86
+ return name.replace('/', '_')
87
+
88
+ def calculate_clean_network(list_cpds, list_edges, formula='neutral_formula', max_H_C_allowed=5):
89
+ pathway_nodes, pathway_edges = [], []
90
+ for cpd in list_cpds:
91
+ if formula in cpd and 'C' in cpd[formula] and 'H' in cpd[formula]:
92
+ hc, oc, nc, nops_c, ccs_offset = get_lcvk_ratios(cpd[formula], 0)
93
+ if hc <= max_H_C_allowed:
94
+ pathway_nodes.append(
95
+ {
96
+ **cpd,
97
+ 'ratio_H_C': hc,
98
+ 'ratio_O_C': oc,
99
+ 'ratio_N_C': nc,
100
+ 'ratio_NOPS': nops_c,
101
+ 'ccs_offset': ccs_offset,
102
+ 'centrality': None, # future use
103
+ }
104
+ )
105
+ valid_cpd_ids = [x['id'] for x in pathway_nodes]
106
+ for edge in list_edges:
107
+ if edge[0] in valid_cpd_ids and edge[1] in valid_cpd_ids:
108
+ pathway_edges.append(tuple(edge))
109
+
110
+ return pathway_nodes, set(pathway_edges)
111
+
112
+ def get_pathway_coordinates(pathway, cpdDict, formula='neutral_formula'):
113
+ '''
114
+ Input a pathway in mummichog style.
115
+ formula : neutral_formula, charged_formula
116
+ Return coordinates {cpd: (hc, nops_c, ccs_offset), ...}
117
+
118
+ Maybe add ccs_offset or other offset methods later.
119
+ '''
120
+ cpds = []
121
+ for C in pathway['cpds']:
122
+ if C in cpdDict and cpdDict[C][formula]:
123
+ try:
124
+ f = cpdDict[C][formula]
125
+ if 'C' in f and 'H' in f:
126
+ hc, oc, nc, nops_c, ccs_offset = get_lcvk_ratios(f, 0) # not using ccs_offset for now
127
+ cpds.append(
128
+ {
129
+ 'id': cpdDict[C]['id'],
130
+ 'name': cpdDict[C]['name'],
131
+ formula: f,
132
+ 'ratio_H_C': hc,
133
+ 'ratio_O_C': oc,
134
+ 'ratio_N_C': nc,
135
+ 'ratio_NOPS': nops_c,
136
+ 'ccs_offset': ccs_offset,
137
+ 'centrality': None, # future use
138
+ }
139
+ )
140
+ else:
141
+ logger.warning("Compound %s has malformed formula: %s", C, f)
142
+ except KeyError:
143
+ logger.warning("Compound %s missing from cpdDict or formula field", C)
144
+ return cpds
145
+
146
+ def generate_landmarks(hcList, yList):
147
+ '''returns the geo center and the max of input coordinates'''
148
+ # Not used now, since there's usually conversion btw Cartesian and Polar
149
+ return (np.mean(hcList), np.mean(yList)), (max(hcList), max(yList))
150
+
151
+ #
152
+ # Main function for Li-cvK diagram
153
+ #
154
+ def plot_lcvk_pathway(list_cpds, list_edges,
155
+ formula='neutral_formula', cpd_name='name',
156
+ ydata='ratio_NOPS',
157
+ show_names=True, show_formula=False,
158
+ flexible_theta=True,
159
+ show_landmarks=False,
160
+ max_H_C_allowed=5,
161
+ min_theta=0.1*np.pi, max_theta=1.9*np.pi,
162
+ min_radius=0, max_radius=2.5,
163
+ padding=0.1, optimization=False,
164
+ optimization_spacer=0.3, optimization_iter=3,
165
+ fontsize=5, rotation=30,
166
+ width=15, height=15, # inch
167
+ yLabel_off=False,
168
+ # marker and edge parameters
169
+ marker='o', facecolors='blue', linewidths=.2, edgecolors='m', s=48,
170
+ alpha=0.5, edge_alpha=0.1,
171
+ title='', outfile='lcvk_plot.pdf', dpi=300
172
+ ):
173
+ '''
174
+ formula='neutral_formula' is recommended; 'charged_formula' also accepted.
175
+ cpd_name can use 'id'
176
+ '''
177
+ pathway_nodes, pathway_edges = calculate_clean_network(list_cpds, list_edges, formula,
178
+ max_H_C_allowed)
179
+ hcList = [x['ratio_H_C'] for x in pathway_nodes]
180
+ cartesian_mean_x, cartesian_min_x, cartesian_max_x = np.mean(hcList), min(hcList), max(1, max(hcList))
181
+ hcList = project_hc_data_radial(hcList)
182
+ if flexible_theta:
183
+ min_theta, max_theta = min(hcList), max(hcList)
184
+ # tick labels; showing original data before polar projection
185
+ xlabels = [ 0.8, 1.0, 1.2, 1.4, 1.6, 1.8, 2, 2.2, 2.4]
186
+ xlabels = [x for x in xlabels if cartesian_min_x < x < cartesian_max_x]
187
+ if not xlabels: # data falling out of range, forcing
188
+ xlabels = [1.0, 1.1]
189
+ xticks = project_hc_data_radial(xlabels)
190
+
191
+ ylabels = {'ratio_NOPS': 'NOPS:C', 'ratio_O_C': 'O:C', 'ratio_H_C': 'H:C'} # proper text for axis labels
192
+ yList = [min(max(x[ydata], min_radius), max_radius)
193
+ for x in pathway_nodes]
194
+ formulas = [x[formula] for x in pathway_nodes]
195
+ names = [x[cpd_name] for x in pathway_nodes]
196
+
197
+ # optionally optimize text labels here, to avoid too much overlap in cpd names
198
+ if optimization:
199
+ hcList, yList = optimize_list_coordinates(hcList, yList,
200
+ distance_cut=0.1, xspacer=optimization_spacer,
201
+ rspacer=optimization_spacer,
202
+ optimization_iter=optimization_iter,
203
+ )
204
+
205
+ layout = dict((x['id'], (hcList[ii], yList[ii])) for ii,x in enumerate(pathway_nodes))
206
+
207
+ fig = plt.figure(figsize=(width, height))
208
+ ax = fig.subplots(subplot_kw={'projection': 'polar'})
209
+ ax.set_theta_zero_location('S')
210
+ ax.set_theta_direction(-1)
211
+ ax.set_thetalim((min_theta-padding, max_theta+padding))
212
+
213
+ ax.scatter(hcList, yList, marker=marker, facecolors=facecolors,
214
+ linewidths=linewidths, edgecolors=edgecolors, s=s, alpha=alpha,
215
+ )
216
+
217
+ if show_landmarks: # show landmarks if desired
218
+ center_x, center_r, min_x, max_r = np.mean(hcList), np.mean(yList), min(hcList), max(yList)
219
+ ax.scatter([center_x, min_x], [center_r, max_r], marker='X', facecolors='w',
220
+ linewidths=.5, edgecolors='r', s=128)
221
+ # labels need to be original H:C values
222
+ ax.text(5.7, max_r+.5, f"H:C marks\n{cartesian_min_x:.2f}, {cartesian_mean_x:.2f}", color='r', alpha=.5)
223
+ # ax.text(center_x, center_r-padding, f"({cartesian_mean_x:.2f}, {center_r:.2f})", color='r', alpha=0.5)
224
+ # ax.text(min_x, max_r-padding, f"({cartesian_min_x:.2f}, {max_r:.2f})", color='r', alpha=0.5)
225
+
226
+ for ii, f in enumerate(formulas):
227
+ L = names[ii]
228
+ if show_names:
229
+ ax.text(hcList[ii] + .2*padding, yList[ii] - .2*padding, L, color='k',
230
+ fontsize=fontsize, rotation=rotation, alpha=0.9)
231
+ if show_formula:
232
+ L = f + ', \n' + names[ii]
233
+ ax.text(hcList[ii] + .2*padding, yList[ii] - .2*padding, L, color='k',
234
+ fontsize=fontsize, rotation=rotation, alpha=0.9)
235
+
236
+ for edge in pathway_edges: # cleaned edges to use
237
+ ax.annotate("",
238
+ xy=layout[edge[0]], xycoords='data',
239
+ xytext=layout[edge[1]], textcoords='data',
240
+ arrowprops=dict(arrowstyle="->", color="0.5", alpha=edge_alpha,
241
+ shrinkA=5, shrinkB=5,
242
+ patchA=None, patchB=None,
243
+ connectionstyle="arc3,rad=-0.3",
244
+ ),
245
+ )
246
+ ax.set_rorigin(-1)
247
+ for p in ax.spines.values():
248
+ # inner, polar, start, end
249
+ p.set_color('r')
250
+ p.set_linewidth(0.1)
251
+
252
+ median_y = np.median(yList)
253
+ ax.set_rgrids([median_y])
254
+ ax.grid(color='r', linewidth=0.05)
255
+ # if xticks:
256
+ ax.set_xticks(xticks)
257
+ ax.set_xticklabels([str(x) for x in xlabels])
258
+ ax.text(1.5, max(yList) + .5, "H:C", ) #
259
+ if not yLabel_off:
260
+ ax.text(1.97*np.pi, 1.6, ylabels[ydata], rotation=283)
261
+
262
+ ax.set_title(title)
263
+ plt.savefig(outfile, dpi=dpi)
264
+ plt.close(fig)
265
+
266
+
267
+ def assign_new_values(hcList, yList, cluttered, distance_min=0.05, iter=5,
268
+ xweight=0.02, rweight=0.01):
269
+ '''
270
+ cluttered : e.g. [18, 47, 49, 57, 69, 71, 73, 74, 93]
271
+ add random moves to clustered, and replace values in hcList, yList
272
+ distance_min : target min euclidean distance btw any two nodes
273
+ '''
274
+ new = sorted([(hcList[ii], yList[ii], ii) for ii in cluttered])
275
+
276
+ for ii in range(iter):
277
+ replacements = [new[0]]
278
+ for x in new[1:]:
279
+ if euclidean(x[:2], replacements[-1][:2]) > distance_min:
280
+ replacements.append(x)
281
+ else:
282
+ replacements.append(
283
+ (x[0]+ xweight*np.random.random_sample(),
284
+ x[1]+rweight*np.random.random_sample(), x[2])
285
+ )
286
+ new = sorted(replacements)
287
+
288
+ for x in replacements:
289
+ hcList[x[2]] = x[0]
290
+ yList[x[2]] = x[1]
291
+ return hcList, yList
292
+
293
+ def part_twins(hcList, yList, threshod=0.01, xspacer=0.01, rspacer=0.01):
294
+ '''
295
+ Separate nodes of identical coordinates
296
+ '''
297
+ def _match_(LL, y, threshod):
298
+ _r_ = False
299
+ for x in LL:
300
+ if euclidean(x, y) < threshod:
301
+ _r_ = True
302
+ return _r_
303
+
304
+ LL = list(zip(hcList, yList))
305
+ new = [LL[0]]
306
+ for pair in LL[1:]:
307
+ if _match_(new, pair, threshod):
308
+ new.append((pair[0]+xspacer, pair[1]+rspacer))
309
+ else:
310
+ new.append(pair)
311
+ return [x[0] for x in new], [x[1] for x in new]
312
+
313
+ def optimize_list_coordinates(hcList, yList,
314
+ clustersize=2,
315
+ distance_cut=0.1,
316
+ optimization_iter=5,
317
+ xspacer=0.01, rspacer=0.01):
318
+ '''
319
+ add spacer to cluttered nodes
320
+ Example cluster:
321
+ 3.5241459360629888 0.26
322
+ 3.5241459360629888 0.26
323
+ 3.5241459360629888 0.26
324
+ 3.4270902040763933 0.2826086956521739
325
+ 3.4776400644860788 0.2708333333333333
326
+ 3.5241459360629888 0.26
327
+ 3.4270902040763933 0.2826086956521739
328
+ 3.4776400644860788 0.2708333333333333
329
+ 3.5241459360629888 0.26
330
+
331
+ '''
332
+ YM = pdist(list(zip(hcList, yList)), 'euclidean')
333
+ ZM = linkage(YM, method='complete')
334
+ metClus = fcluster(ZM, distance_cut, criterion='distance')
335
+ # Compile clusters
336
+ metClusDict = {}
337
+ for ii in range(len(metClus)):
338
+ if metClus[ii] in metClusDict:
339
+ metClusDict[ metClus[ii] ].append(ii)
340
+ else:
341
+ metClusDict[ metClus[ii] ] = [ii]
342
+
343
+ # print("number of clusters: ", len(metClusDict.keys()))
344
+ for k,v in metClusDict.items():
345
+ if len(v) >= clustersize:
346
+ hcList, yList = assign_new_values(hcList, yList, v,
347
+ distance_min=0.05,
348
+ iter=optimization_iter)
349
+
350
+ hcList, yList = part_twins(hcList, yList, threshod=0.1, xspacer=xspacer, rspacer=rspacer)
351
+ return hcList, yList
352
+
353
+
354
+ def populate_cpd_coordinates(cpdDict, formula='neutral_formula'):
355
+ # full cpds
356
+ allcpds = []
357
+ for C in cpdDict.values():
358
+ if C[formula]:
359
+ try:
360
+ f = C[formula]
361
+ if 'C' in f and 'H' in f:
362
+ hc, oc, nc, nops_c, ccs_offset = get_lcvk_ratios(f, 0) #
363
+ allcpds.append(
364
+ {
365
+ 'id': C['id'],
366
+ 'name': C['name'],
367
+ formula: f,
368
+ 'ratio_H_C': hc,
369
+ 'ratio_O_C': oc,
370
+ 'ratio_N_C': nc,
371
+ 'ratio_NOPS': nops_c,
372
+ 'ccs_offset': ccs_offset,
373
+ 'centrality': None, # future use
374
+ }
375
+ )
376
+ else:
377
+ logger.warning("Compound %s has malformed formula: %s", C, f)
378
+ except KeyError:
379
+ logger.warning("Compound %s missing 'id' or formula field", C)
380
+
381
+ return allcpds
382
+
383
+
384
+ def plot_lcvk_cartesian_pathway(pathway_nodes, pathway_edges,
385
+ ydata='ratio_NOPS',
386
+ show_names=True, show_formula=False,
387
+ fontsize=5, rotation=30, padding=0.01,
388
+ width=8, height=8, # inch
389
+ ylim=None, # or max value of yaxis
390
+ title='', outfile='cartesian_vk_plot.pdf', dpi=300
391
+ ):
392
+ hcList = [x['ratio_H_C'] for x in pathway_nodes]
393
+ yList = [x[ydata] for x in pathway_nodes]
394
+ formulas = [x['neutral_formula'] for x in pathway_nodes]
395
+ names = [x['name'] for x in pathway_nodes]
396
+ ylabels = {'ratio_NOPS': 'NOPS:C', 'ratio_O_C': 'O:C', 'ratio_H_C': 'H:C'} # proper text for axis labels
397
+
398
+ layout = dict((x['id'], (hcList[ii], yList[ii])) for ii,x in enumerate(pathway_nodes))
399
+
400
+ _nodes = [x['id'] for x in pathway_nodes]
401
+ #sizes = [len(fdict[x[0]]) * 3 for x in kv_list_gem]
402
+
403
+ text_offset = padding
404
+ plt.figure(figsize=(width, height))
405
+ ax = plt.subplot()
406
+ ax.scatter(hcList, yList, marker='o', facecolors='blue',
407
+ linewidths=.2, edgecolors='m', s=48, alpha=0.5,
408
+ )
409
+ for ii, f in enumerate(formulas):
410
+ L = names[ii]
411
+ if show_names:
412
+ ax.text(hcList[ii] + text_offset/2, yList[ii] + text_offset, L,
413
+ fontsize=fontsize, rotation=rotation, alpha=0.5)
414
+ if show_formula:
415
+ L = f + ', \n' + names[ii]
416
+ ax.text(hcList[ii] + text_offset/2, yList[ii] + text_offset, L,
417
+ fontsize=fontsize, rotation=rotation, alpha=0.5)
418
+
419
+ for edge in pathway_edges:
420
+ if edge[0] in _nodes and edge[1] in _nodes:
421
+ ax.annotate("",
422
+ xy=layout[edge[0]], xycoords='data',
423
+ xytext=layout[edge[1]], textcoords='data',
424
+ arrowprops=dict(arrowstyle="->", color="0.5", alpha=0.2,
425
+ shrinkA=5, shrinkB=5,
426
+ patchA=None, patchB=None,
427
+ connectionstyle="arc3,rad=-0.3",
428
+ ),
429
+ )
430
+
431
+ ax.set_xlabel("H:C ratio")
432
+ if ylim:
433
+ ax.set_ylim((0, ylim))
434
+ ax.set_ylabel(ylabels[ydata])
435
+ ax.set_title(title)
436
+ plt.savefig(outfile, dpi=dpi)
437
+ plt.close()
438
+
439
+
440
+ # experimenting
441
+ def nx_fruchterman_reingold(pathway_edges, posDict,
442
+ k=0.05, fixed=None, iterations=5, threshold=0.0001, dim=2,
443
+ ):
444
+ '''
445
+ Modified from networkx._fruchterman_reingold
446
+ '''
447
+ import networkx as nx
448
+
449
+ G = nx.from_edgelist(pathway_edges)
450
+ A = nx.to_scipy_sparse_array(G)
451
+ nnodes = len(G)
452
+
453
+ pos = np.zeros((len(G), dim))
454
+ for i, n in enumerate(G):
455
+ if n in posDict:
456
+ pos[i] = np.asarray(posDict[n])
457
+
458
+ # the initial "temperature" is about .1 of domain area (=1x1)
459
+ # this is the largest step allowed in the dynamics.
460
+ # We need to calculate this in case our fixed positions force our domain
461
+ # to be much bigger than 1x1
462
+ t = 0.001
463
+ # simple cooling scheme.
464
+ # linearly step down by dt on each iteration so last iteration is size dt.
465
+ dt = t / (iterations + 1)
466
+ delta = np.zeros((pos.shape[0], pos.shape[0], pos.shape[1]), dtype=A.dtype)
467
+ # the inscrutable (but fast) version
468
+ # this is still O(V^2)
469
+ # could use multilevel methods to speed this up significantly
470
+ for iteration in range(iterations):
471
+
472
+ print(pos[0])
473
+
474
+ # matrix of difference between points
475
+ delta = pos[:, np.newaxis, :] - pos[np.newaxis, :, :]
476
+ # distance between points
477
+ distance = np.linalg.norm(delta, axis=-1)
478
+ # enforce minimum distance of 0.01
479
+ np.clip(distance, 0.01, None, out=distance)
480
+ # displacement "force"
481
+ displacement = np.einsum(
482
+ "ijk,ij->ik", delta, (k * k / distance**2 - A * distance / k)
483
+ )
484
+ # update positions
485
+ length = np.linalg.norm(displacement, axis=-1)
486
+ length = np.where(length < 0.01, 0.1, length)
487
+ delta_pos = np.einsum("ij,i->ij", displacement, t / length)
488
+ if fixed is not None:
489
+ # don't change positions of fixed nodes
490
+ delta_pos[fixed] = 0.0
491
+ pos += delta_pos
492
+ # cool temperature
493
+ t -= dt
494
+ if (np.linalg.norm(delta_pos) / nnodes) < threshold:
495
+ break
496
+ return pos
497
+
@@ -0,0 +1,204 @@
1
+ #
2
+ # Misc. receipes; likely of bad taste
3
+ #
4
+ import os
5
+
6
+ import numpy as np
7
+ import matplotlib.pyplot as plt
8
+
9
+ from .polarPlot import standardize_data
10
+
11
+ __all__ = [
12
+ 'differential_metabolites', 'plot_lcvk_global_metabolites',
13
+ 'plot_lcvk_global_metabolites_colored', 'plot_lcvk_pathway_batch',
14
+ ]
15
+
16
+ def differential_metabolites(list_cpds):
17
+ '''
18
+ Coloring differential metabolites from metabolomics data
19
+ '''
20
+ raise NotImplementedError
21
+
22
+
23
+
24
+ def plot_lcvk_global_metabolites(allcpds):
25
+ '''Illustration only'''
26
+ YMAX = 2.5
27
+ theta_limit = (0.1*np.pi, 1.9*np.pi)
28
+ theta_offset = 0.05 # so that nodes do not sit on border lines
29
+ hcList = [max(min(x['ratio_H_C'], 2.5), 0.5) for x in allcpds]
30
+ hcList = standardize_data(hcList)
31
+
32
+ # ticks reverse function of standardize_data
33
+ xlabels = [ 0.8, 1.1, 1.4, 1.7, 2, 2.3]
34
+ xticks = standardize_data(xlabels)
35
+
36
+ yList = [min(x['ratio_NOPS'] + x['ccs_offset'], YMAX)
37
+ for x in allcpds]
38
+
39
+ fig = plt.figure(figsize=(12, 12))
40
+ ax = fig.subplots(subplot_kw={'projection': 'polar', }
41
+ )
42
+ c = ax.scatter(hcList, yList, marker='o', facecolors='w',
43
+ linewidths=.3, edgecolors='k',
44
+ s=16,
45
+ )
46
+ ax.set_theta_zero_location('S')
47
+ ax.set_theta_direction(-1)
48
+ ax.set_thetalim((theta_limit[0]-theta_offset, theta_limit[1]+theta_offset))
49
+ ax.set_ylim(0, YMAX + 0.1)
50
+ ax.set_xticks(xticks)
51
+ ax.set_xticklabels([str(x) for x in xlabels])
52
+
53
+ ax.set_rgrids([0.5, 1.5])
54
+ ax.grid(color='r', linewidth=0.1)
55
+ for p in ax.spines.values():
56
+ # inner, polar, start, end
57
+ p.set_color('r')
58
+ p.set_linewidth(0.2)
59
+
60
+ ax.set_rorigin(-1)
61
+ ax.text(1.7, YMAX + .45, "H:C", )
62
+ ax.text(1.96*np.pi, 1.6, "NOPS:C", rotation=285)
63
+ ax.set_title("Global metabolites")
64
+ plt.savefig('gem_mar2025_ratio_NOPS.pdf', dpi=300)
65
+ plt.close(fig)
66
+
67
+
68
+ def plot_lcvk_global_metabolites_colored(allcpds, collections, colors_collections,
69
+ outfile='gem_mar2025_ratio_NOPS_colorExamples.pdf'):
70
+ '''
71
+ Illustration only.
72
+ collections: list of index lists into allcpds/hcList/yList, one per highlighted group.
73
+ colors_collections: matplotlib color name for each group in collections.
74
+ '''
75
+ YMAX = 2.5
76
+ theta_limit = (0.1*np.pi, 1.9*np.pi)
77
+ theta_offset = 0.05 # so that nodes do not sit on border lines
78
+ hcList = [max(min(x['ratio_H_C'], 2.5), 0.5) for x in allcpds]
79
+ hcList = standardize_data(hcList)
80
+
81
+ # ticks reverse function of standardize_data
82
+ xlabels = [ 0.8, 1.1, 1.4, 1.7, 2, 2.3]
83
+ xticks = standardize_data(xlabels)
84
+
85
+ yList = [min(x['ratio_NOPS'] + x['ccs_offset'], YMAX)
86
+ for x in allcpds]
87
+
88
+ fig = plt.figure(figsize=(8,8))
89
+ ax = fig.subplots(subplot_kw={'projection': 'polar', }
90
+ )
91
+
92
+ c = ax.scatter(hcList, yList, marker='o', facecolors='w',
93
+ linewidths=.3, edgecolors='k',
94
+ s=16,
95
+ )
96
+
97
+ for ii in range(len(collections)):
98
+ XX = [hcList[j] for j in collections[ii]]
99
+ YY = [yList[j] for j in collections[ii]]
100
+ ax.scatter(XX, YY, marker='o', facecolors=colors_collections[ii], s=24, alpha=.5)
101
+
102
+ ax.set_theta_zero_location('S')
103
+ ax.set_theta_direction(-1)
104
+ ax.set_thetalim((theta_limit[0]-theta_offset, theta_limit[1]+theta_offset))
105
+ ax.set_ylim(0, YMAX + 0.1)
106
+ ax.set_xticks(xticks)
107
+ ax.set_xticklabels([str(x) for x in xlabels])
108
+
109
+ ax.set_rgrids([0.5, 1.5])
110
+ ax.grid(color='r', linewidth=0.1)
111
+ for p in ax.spines.values():
112
+ # inner, polar, start, end
113
+ p.set_color('r')
114
+ p.set_linewidth(0.2)
115
+
116
+ ax.set_rorigin(-1)
117
+ ax.text(1.7, YMAX + .45, "H:C", )
118
+ ax.text(1.96*np.pi, 1.6, "NOPS:C", rotation=285)
119
+ ax.set_title("Global metabolites")
120
+ plt.savefig(outfile, dpi=300)
121
+ plt.close(fig)
122
+
123
+
124
+ def plot_lcvk_pathway_batch(pathway_nodes, pathway_edges, title,
125
+ ydata='ratio_NOPS',
126
+ fontsize=5, rotation=30, show_names=True, show_formula=False, outdir='.'
127
+ ):
128
+ theta_limit = (0.1*np.pi, 1.9*np.pi)
129
+ theta_offset = 0.05 # so that nodes do not sit on border lines
130
+ hcList = [max(min(x['ratio_H_C'], 2.5), 0.3) for x in pathway_nodes]
131
+ hcList = standardize_data(hcList)
132
+
133
+ # ticks reverse function of standardize_data
134
+ xlabels = [0.8, 1.1, 1.4, 1.7, 2, 2.3]
135
+ xticks = standardize_data(xlabels)
136
+
137
+ YMAX = 3
138
+ yList = [min(x[ydata] + x['ccs_offset'], YMAX) for x in pathway_nodes]
139
+
140
+ formulas = [x['neutral_formula'] for x in pathway_nodes]
141
+ names = [x['name'] for x in pathway_nodes]
142
+
143
+ layout = dict((x['id'], (hcList[ii], yList[ii])) for ii,x in enumerate(pathway_nodes))
144
+
145
+ _nodes = [x['id'] for x in pathway_nodes]
146
+ #sizes = [len(fdict[x[0]]) * 3 for x in kv_list_gem]
147
+
148
+ text_offset = 0.01
149
+ fig = plt.figure(figsize=(15,15))
150
+ ax = fig.subplots(subplot_kw={'projection': 'polar'})
151
+ ax.set_theta_zero_location('S')
152
+ ax.set_theta_direction(-1)
153
+ ax.set_thetalim((theta_limit[0]-theta_offset, theta_limit[1]+theta_offset))
154
+ ax.set_ylim(0, 3.1)
155
+
156
+ ax.scatter(hcList, yList, marker='o', facecolors='blue',
157
+ linewidths=.2, edgecolors='m',
158
+ s=48, alpha=0.5,
159
+ )
160
+
161
+ # optionally optimize text labels here, to avoid too much overlap in cpd names
162
+
163
+ for ii, f in enumerate(formulas):
164
+ L = names[ii]
165
+ if show_names:
166
+ ax.text(hcList[ii], yList[ii] - text_offset, L, color='k',
167
+ fontsize=fontsize, rotation=rotation, alpha=0.9)
168
+ if show_formula:
169
+ L = f + ', \n' + names[ii]
170
+ ax.text(hcList[ii], yList[ii] - text_offset, L, color='k',
171
+ fontsize=fontsize, rotation=rotation, alpha=0.9)
172
+
173
+ for edge in set(pathway_edges): # set to remove repeated edges; edge has to be tuple
174
+ if edge[0] in _nodes and edge[1] in _nodes:
175
+ ax.annotate("",
176
+ xy=layout[edge[0]], xycoords='data',
177
+ xytext=layout[edge[1]], textcoords='data',
178
+ arrowprops=dict(arrowstyle="->", color="0.5", alpha=0.2,
179
+ shrinkA=5, shrinkB=5,
180
+ patchA=None, patchB=None,
181
+ connectionstyle="arc3,rad=-0.3",
182
+ ),
183
+ )
184
+
185
+ ax.set_xticks(xticks)
186
+ ax.set_xticklabels([str(x) for x in xlabels])
187
+
188
+ ax.set_rgrids([0.5, 2])
189
+ ax.grid(color='r', linewidth=0.05)
190
+ for p in ax.spines.values():
191
+ # inner, polar, start, end
192
+ p.set_color('r')
193
+ p.set_linewidth(0.1)
194
+
195
+ ax.set_rorigin(-1)
196
+ ax.text(1.7, YMAX + .35, "H:C", )
197
+ ax.text(1.945*np.pi, 1.6, ydata, rotation=285)
198
+
199
+ ax.set_title(title) # "KV-Li plot"
200
+ plt.savefig(
201
+ os.path.join(outdir,
202
+ title.replace('/', '__') + '.pdf')
203
+ )
204
+ plt.close(fig)
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: lcvk
3
+ Version: 0.1.1
4
+ Summary: Li-circular van Krevelen diagram
5
+ Home-page: https://github.com/shuzhao-li-lab/Li_CVK_diagram
6
+ Author: Shuzhao Li
7
+ Author-email: shuzhao.li@gmail.com
8
+ License: BSD 3-Clause
9
+ Keywords: bioinformatics metabolic pathway visualization
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: BSD License
13
+ Classifier: Natural Language :: English
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
17
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.7
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: numpy
23
+ Requires-Dist: scipy
24
+ Requires-Dist: matplotlib
25
+ Requires-Dist: mass2chem
26
+ Dynamic: author
27
+ Dynamic: author-email
28
+ Dynamic: classifier
29
+ Dynamic: description
30
+ Dynamic: description-content-type
31
+ Dynamic: home-page
32
+ Dynamic: keywords
33
+ Dynamic: license
34
+ Dynamic: license-file
35
+ Dynamic: requires-dist
36
+ Dynamic: requires-python
37
+ Dynamic: summary
38
+
39
+ # Circular van Krevelen diagram for visualizing metabolic pathways
40
+
41
+ Emerging biochemical data require effective pathway visualization, but traditional metabolic maps rely on manual layouts that fall behind new scientific discoveries. `lcvk` implements a circularized van Krevelen diagram: metabolites are placed by chemical formula, using elemental ratios (H:C mapped to the angular axis, NOPS:C or a related ratio mapped to the radial axis) instead of a hand-drawn layout. Because the coordinates come from chemical principles rather than manual placement, pathway diagrams are automated and consistent across datasets, models, and labs.
42
+
43
+ Preprint is released: https://www.biorxiv.org/content/10.1101/2025.05.31.657198v1
44
+
45
+ This is the repo for the `lcvk` package, which includes code, example data and notebook templates.
46
+
47
+ Version 0.1 is proof of principle.
48
+
49
+ ![example_pathway_fa](docs/source/_static/worm_human_fa.png)
50
+
51
+ ![example_pathway_alanine](docs/source/_static/alanine.png)
52
+
53
+ Example applications include
54
+ - Visualization of metabolic pathways and maps.
55
+ - Summurizing differential abundance in metabolomic data.
56
+ - Display of isotopic labeling patterns.
57
+ - Extension of metabolic knowledge by new reactions.
58
+
59
+ ## Install
60
+
61
+ ```bash
62
+ pip install -e .
63
+ ```
64
+
65
+ Requires Python >= 3.7 and `numpy`, `scipy`, `matplotlib`, `mass2chem` (see `requirements.txt`).
66
+
67
+ ## Quickstart
68
+
69
+ ```python
70
+ import lcvk
71
+
72
+ # list_cpds: [{'id': ..., 'name': ..., 'neutral_formula': 'C6H12O6'}, ...]
73
+ # list_edges: [(source_id, target_id), ...]
74
+ lcvk.plot_lcvk_pathway(
75
+ list_cpds, list_edges,
76
+ formula='neutral_formula', cpd_name='name',
77
+ title='My pathway', outfile='pathway.pdf',
78
+ )
79
+ ```
80
+
81
+ See `notebooks/` for complete worked examples (metabolic pathways, differential metabolomics, isotope labeling).
82
+
83
+ ## Citation
84
+
85
+ Shuzhao Li. Circular van Krevelen diagram for visualizing metabolic pathways. *bioRxiv* (2025). https://doi.org/10.1101/2025.05.31.657198
@@ -0,0 +1,16 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ requirements.txt
5
+ setup.py
6
+ lcvk/__init__.py
7
+ lcvk/main.py
8
+ lcvk/metModels.py
9
+ lcvk/polarPlot.py
10
+ lcvk/recipes.py
11
+ lcvk.egg-info/PKG-INFO
12
+ lcvk.egg-info/SOURCES.txt
13
+ lcvk.egg-info/dependency_links.txt
14
+ lcvk.egg-info/requires.txt
15
+ lcvk.egg-info/top_level.txt
16
+ lcvk.egg-info/zip-safe
@@ -0,0 +1,4 @@
1
+ numpy
2
+ scipy
3
+ matplotlib
4
+ mass2chem
@@ -0,0 +1 @@
1
+ lcvk
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,4 @@
1
+ numpy
2
+ scipy
3
+ matplotlib
4
+ mass2chem
lcvk-0.1.1/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
lcvk-0.1.1/setup.py ADDED
@@ -0,0 +1,53 @@
1
+ import os
2
+
3
+ from setuptools import setup, find_packages
4
+
5
+ here = os.path.abspath(os.path.dirname(__file__))
6
+
7
+ with open(os.path.join(here, "lcvk", "__init__.py")) as f:
8
+ exec([x for x in f.readlines() if '__version__' in x][0])
9
+
10
+ with open(os.path.join(here, "README.md"), "r") as fh:
11
+ long_description = fh.read()
12
+
13
+ with open(os.path.join(here, "requirements.txt"), "r") as f:
14
+ requirements = f.read()
15
+
16
+
17
+ setup(
18
+ name='lcvk',
19
+ version=__version__,
20
+ author='Shuzhao Li',
21
+ author_email='shuzhao.li@gmail.com',
22
+ description='Li-circular van Krevelen diagram',
23
+ long_description=long_description,
24
+ long_description_content_type="text/markdown",
25
+ url='https://github.com/shuzhao-li-lab/Li_CVK_diagram',
26
+ license='BSD 3-Clause',
27
+ keywords='bioinformatics metabolic pathway visualization',
28
+
29
+ classifiers=[
30
+ 'Intended Audience :: Developers',
31
+ 'Intended Audience :: Science/Research',
32
+ 'License :: OSI Approved :: BSD License',
33
+ 'Natural Language :: English',
34
+ 'Operating System :: OS Independent',
35
+ 'Programming Language :: Python :: 3',
36
+ 'Topic :: Scientific/Engineering :: Bio-Informatics',
37
+ 'Topic :: Scientific/Engineering :: Chemistry',
38
+ 'Topic :: Software Development :: Libraries :: Python Modules',
39
+ ],
40
+
41
+ packages=find_packages(
42
+ include=['lcvk', 'lcvk.*']
43
+ ),
44
+ data_files=[ ],
45
+ include_package_data=True,
46
+ zip_safe=True,
47
+ entry_points = {
48
+ },
49
+
50
+ python_requires='>=3.7',
51
+ install_requires=requirements,
52
+
53
+ )