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 +7 -0
- lcvk-0.1.1/MANIFEST.in +3 -0
- lcvk-0.1.1/PKG-INFO +85 -0
- lcvk-0.1.1/README.md +47 -0
- lcvk-0.1.1/lcvk/__init__.py +5 -0
- lcvk-0.1.1/lcvk/main.py +23 -0
- lcvk-0.1.1/lcvk/metModels.py +169 -0
- lcvk-0.1.1/lcvk/polarPlot.py +497 -0
- lcvk-0.1.1/lcvk/recipes.py +204 -0
- lcvk-0.1.1/lcvk.egg-info/PKG-INFO +85 -0
- lcvk-0.1.1/lcvk.egg-info/SOURCES.txt +16 -0
- lcvk-0.1.1/lcvk.egg-info/dependency_links.txt +1 -0
- lcvk-0.1.1/lcvk.egg-info/requires.txt +4 -0
- lcvk-0.1.1/lcvk.egg-info/top_level.txt +1 -0
- lcvk-0.1.1/lcvk.egg-info/zip-safe +1 -0
- lcvk-0.1.1/requirements.txt +4 -0
- lcvk-0.1.1/setup.cfg +4 -0
- lcvk-0.1.1/setup.py +53 -0
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
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
|
+

|
|
50
|
+
|
|
51
|
+

|
|
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
|
+

|
|
12
|
+
|
|
13
|
+

|
|
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
|
lcvk-0.1.1/lcvk/main.py
ADDED
|
@@ -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
|
+

|
|
50
|
+
|
|
51
|
+

|
|
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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
lcvk
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
lcvk-0.1.1/setup.cfg
ADDED
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
|
+
)
|