booleannet 2.0__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.
- booleannet-2.0/PKG-INFO +7 -0
- booleannet-2.0/README.md +113 -0
- booleannet-2.0/booleannet/__init__.py +1 -0
- booleannet-2.0/booleannet/__main__.py +3 -0
- booleannet-2.0/booleannet/bbm2json.py +207 -0
- booleannet-2.0/booleannet/cli.py +27 -0
- booleannet-2.0/booleannet/commands/__init__.py +1 -0
- booleannet-2.0/booleannet/commands/gviz.py +107 -0
- booleannet-2.0/booleannet/commands/models.py +122 -0
- booleannet-2.0/booleannet/data/models.json.gz +0 -0
- booleannet-2.0/booleannet.egg-info/PKG-INFO +7 -0
- booleannet-2.0/booleannet.egg-info/SOURCES.txt +16 -0
- booleannet-2.0/booleannet.egg-info/dependency_links.txt +1 -0
- booleannet-2.0/booleannet.egg-info/entry_points.txt +2 -0
- booleannet-2.0/booleannet.egg-info/requires.txt +1 -0
- booleannet-2.0/booleannet.egg-info/top_level.txt +1 -0
- booleannet-2.0/pyproject.toml +20 -0
- booleannet-2.0/setup.cfg +4 -0
booleannet-2.0/PKG-INFO
ADDED
booleannet-2.0/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# BooleanNet
|
|
2
|
+
|
|
3
|
+
BooleanNet is a training tool that makes use of existing Boolean network models, methods and algorithms.
|
|
4
|
+
|
|
5
|
+
## Environment setup
|
|
6
|
+
|
|
7
|
+
`booleannet` does not automatically install all of its dependencies as there might be conflicts across dependencies. We recommend using [pixi][pixi] as a virtual environment manager.
|
|
8
|
+
|
|
9
|
+
An example using `pixi` to set up the enviroment is:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pixi init
|
|
13
|
+
pixi shell
|
|
14
|
+
pixi add python=3.12 pip
|
|
15
|
+
pip install git+https://github.com/hklarner/pyboolnet@3.0.16
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Your enviroment is now set up with the necessary dependencies to use `booleannet`.
|
|
19
|
+
|
|
20
|
+
[pixi]: https://pixi.prefix.dev/latest/
|
|
21
|
+
|
|
22
|
+
## Install booleannet
|
|
23
|
+
|
|
24
|
+
The libraires
|
|
25
|
+
```bash
|
|
26
|
+
pip install booleannet
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
It installs the `bnet` command line tool that implements a number of subcommands.
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# bnet models: manage known models
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# List all models
|
|
37
|
+
bnet models | head
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
prints models by increasing number of variables:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
id name var in reg
|
|
44
|
+
165 EGGSHELL-PATTERNING-PHENOMOENOLOGICAL 4 4 16
|
|
45
|
+
170 DROSOPHILA-GAP-B 4 3 15
|
|
46
|
+
007 CORTICAL-AREA-DEVELOPMENT 5 0 14
|
|
47
|
+
109 ASYMMETRIC-CELL-DIVISION-A 5 0 15
|
|
48
|
+
169 DROSOPHILA-GAP-A 5 2 17
|
|
49
|
+
171 DROSOPHILA-GAP-C 5 2 20
|
|
50
|
+
172 DROSOPHILA-GAP-D 5 2 12
|
|
51
|
+
184 P53-MDM2-NETWORK 5 1 15
|
|
52
|
+
189 TRP-BIOSYNTHESIS 5 1 13
|
|
53
|
+
...
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
To get the rules for a specific model:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# Get rules for model 7
|
|
60
|
+
bnet models 7
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
prints:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
Coup_fti* = not (Fgf8 or Sp8) or not (Sp8 or Fgf8)
|
|
67
|
+
Emx2* = Coup_fti and not (Fgf8 or Sp8 or Pax6)
|
|
68
|
+
Fgf8* = Fgf8 and Sp8 and not Emx2
|
|
69
|
+
Pax6* = Sp8 and not (Emx2 or Coup_fti)
|
|
70
|
+
Sp8* = Fgf8 and not Emx2
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
You can also get the rules for a model by name:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Get rules for model by name
|
|
77
|
+
bnet models CORTICAL-AREA-DEVELOPMENT
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Get the rules in other formats:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Get model 7 in BNet format
|
|
84
|
+
bnet models 7 -f bnet
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## bnet graphviz: visualize a model
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# If you have a model in a file
|
|
91
|
+
bnet graphviz -i model.txt
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# You can pipe the rule to graphviz
|
|
96
|
+
bnet models CORTICAL-AREA-DEVELOPMENT | bnet graphviz
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Convert BBMB to JSON
|
|
100
|
+
|
|
101
|
+
This is used internally to transform the BBMB model repository to a single JSON file.
|
|
102
|
+
|
|
103
|
+
Skips a few large models that make the file too large.
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
python booleannet/bbm2json.py \
|
|
107
|
+
--summary ~/src/biodivine-boolean-models/models/summary.csv
|
|
108
|
+
--models ~/src/biodivine-boolean-models/models
|
|
109
|
+
--skip 253,248,79,261,256
|
|
110
|
+
--output models.json.gz
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Convert rules to images
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Boolean network modeling."""
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Convert BBM models files to a JSON data file.
|
|
4
|
+
|
|
5
|
+
Join models/summary.csv to each model directory and write one JSON object.
|
|
6
|
+
|
|
7
|
+
Directory names follow the summary columns (regulations is not in the name):
|
|
8
|
+
|
|
9
|
+
\b
|
|
10
|
+
[id-009]__[var-60]__[in-13]__[YEAST-APOPTOSIS]
|
|
11
|
+
|
|
12
|
+
The output is a JSON object keyed by the zero-padded id from the CSV:
|
|
13
|
+
|
|
14
|
+
\b
|
|
15
|
+
{
|
|
16
|
+
"009": {
|
|
17
|
+
"summary": {
|
|
18
|
+
"name": "YEAST-APOPTOSIS",
|
|
19
|
+
"variables": 60,
|
|
20
|
+
"inputs": 13,
|
|
21
|
+
"regulations": 114
|
|
22
|
+
},
|
|
23
|
+
"metadata": { ... }, # metadata.json, parsed
|
|
24
|
+
"readme": "...", # README.md
|
|
25
|
+
"bnet": "...", # model.bnet
|
|
26
|
+
"aeon": "...", # model.aeon
|
|
27
|
+
"sbml": "...", # model.sbml
|
|
28
|
+
"bma": { ... }, # model.bma.json, parsed
|
|
29
|
+
"booleannet": "...", # model.booleannet.txt
|
|
30
|
+
"inferred_graph": "..." # model.inferred-graph.aeon
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
JSON files are stored as objects. The other formats stay as text.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
import csv
|
|
38
|
+
import gzip
|
|
39
|
+
import json
|
|
40
|
+
import re
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
|
|
43
|
+
import click
|
|
44
|
+
|
|
45
|
+
ROOT = Path(__file__).resolve().parent
|
|
46
|
+
|
|
47
|
+
DIR_RE = re.compile(
|
|
48
|
+
r"^\[id-(?P<id>\d+)\]__\[var-(?P<variables>\d+)\]__\[in-(?P<inputs>\d+)\]__\[(?P<name>.+)\]$"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# filename -> (output field, "json" or "text")
|
|
52
|
+
FILES = {
|
|
53
|
+
"metadata.json": ("metadata", "json"),
|
|
54
|
+
"README.md": ("readme", "text"),
|
|
55
|
+
"model.bnet": ("bnet", "text"),
|
|
56
|
+
"model.aeon": ("aeon", "text"),
|
|
57
|
+
"model.sbml": ("sbml", "text"),
|
|
58
|
+
"model.bma.json": ("bma", "json"),
|
|
59
|
+
"model.booleannet.txt": ("booleannet", "text"),
|
|
60
|
+
"model.inferred-graph.aeon": ("inferred_graph", "text"),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def read_summary(path):
|
|
65
|
+
with path.open(newline="") as f:
|
|
66
|
+
rows = list(csv.DictReader(f, skipinitialspace=True))
|
|
67
|
+
by_id = {}
|
|
68
|
+
for row in rows:
|
|
69
|
+
model_id = row["ID"].zfill(3)
|
|
70
|
+
if model_id in by_id:
|
|
71
|
+
raise SystemExit(f"duplicate id {model_id} in {path}")
|
|
72
|
+
by_id[model_id] = {
|
|
73
|
+
"name": row["name"],
|
|
74
|
+
"variables": int(row["variables"]),
|
|
75
|
+
"inputs": int(row["inputs"]),
|
|
76
|
+
"regulations": int(row["regulations"]),
|
|
77
|
+
}
|
|
78
|
+
return by_id
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def index_dirs(models_dir):
|
|
82
|
+
by_id = {}
|
|
83
|
+
for path in sorted(models_dir.iterdir()):
|
|
84
|
+
if not path.is_dir():
|
|
85
|
+
continue
|
|
86
|
+
match = DIR_RE.match(path.name)
|
|
87
|
+
if not match:
|
|
88
|
+
raise SystemExit(f"directory name does not match summary pattern: {path.name}")
|
|
89
|
+
model_id = match.group("id").zfill(3)
|
|
90
|
+
if model_id in by_id:
|
|
91
|
+
raise SystemExit(f"duplicate id {model_id}: {path.name}")
|
|
92
|
+
by_id[model_id] = (path, match.groupdict())
|
|
93
|
+
return by_id
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def load_files(directory):
|
|
97
|
+
data = {}
|
|
98
|
+
for filename, (field, kind) in FILES.items():
|
|
99
|
+
path = directory / filename
|
|
100
|
+
if not path.is_file():
|
|
101
|
+
raise SystemExit(f"missing {path}")
|
|
102
|
+
text = path.read_text()
|
|
103
|
+
data[field] = json.loads(text) if kind == "json" else text
|
|
104
|
+
return data
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def parse_skip(ctx, param, value):
|
|
108
|
+
ids = set()
|
|
109
|
+
for part in value.split(","):
|
|
110
|
+
part = part.strip()
|
|
111
|
+
if not part:
|
|
112
|
+
continue
|
|
113
|
+
if not part.isdigit():
|
|
114
|
+
raise click.BadParameter(f"not a model id: {part}")
|
|
115
|
+
ids.add(part.zfill(3))
|
|
116
|
+
return ids
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def drop_skipped(summary, dirs, skip):
|
|
120
|
+
unknown = sorted(skip - set(summary) - set(dirs))
|
|
121
|
+
if unknown:
|
|
122
|
+
raise SystemExit(f"unknown skip id: {', '.join(unknown)}")
|
|
123
|
+
for model_id in skip:
|
|
124
|
+
summary.pop(model_id, None)
|
|
125
|
+
dirs.pop(model_id, None)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def build(summary, dirs):
|
|
129
|
+
missing = sorted(set(summary) - set(dirs))
|
|
130
|
+
extra = sorted(set(dirs) - set(summary))
|
|
131
|
+
if missing or extra:
|
|
132
|
+
click.echo(
|
|
133
|
+
f"warning: summary/directory mismatch missing={missing} extra={extra}",
|
|
134
|
+
err=True,
|
|
135
|
+
)
|
|
136
|
+
for model_id in missing:
|
|
137
|
+
del summary[model_id]
|
|
138
|
+
for model_id in extra:
|
|
139
|
+
del dirs[model_id]
|
|
140
|
+
|
|
141
|
+
models = {}
|
|
142
|
+
for model_id in sorted(summary):
|
|
143
|
+
row = summary[model_id]
|
|
144
|
+
print(f"Processing: {row}")
|
|
145
|
+
path, parsed = dirs[model_id]
|
|
146
|
+
parsed_vars = int(parsed["variables"])
|
|
147
|
+
parsed_inputs = int(parsed["inputs"])
|
|
148
|
+
if (
|
|
149
|
+
parsed["name"] != row["name"]
|
|
150
|
+
or parsed_vars != row["variables"]
|
|
151
|
+
or parsed_inputs != row["inputs"]
|
|
152
|
+
):
|
|
153
|
+
raise SystemExit(
|
|
154
|
+
f"id {model_id} summary {row} does not match directory {path.name}"
|
|
155
|
+
)
|
|
156
|
+
models[model_id] = {
|
|
157
|
+
"summary": row,
|
|
158
|
+
**load_files(path),
|
|
159
|
+
}
|
|
160
|
+
return models
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@click.command(help=__doc__)
|
|
164
|
+
@click.option(
|
|
165
|
+
"--summary",
|
|
166
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
167
|
+
default= Path("models") / "summary.csv",
|
|
168
|
+
show_default=True,
|
|
169
|
+
help="Summary CSV joined onto each model.",
|
|
170
|
+
)
|
|
171
|
+
@click.option(
|
|
172
|
+
"--models",
|
|
173
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
174
|
+
default=Path("models"),
|
|
175
|
+
show_default=True,
|
|
176
|
+
help="Directory of model folders.",
|
|
177
|
+
)
|
|
178
|
+
@click.option(
|
|
179
|
+
"--skip",
|
|
180
|
+
default="",
|
|
181
|
+
show_default=True,
|
|
182
|
+
callback=parse_skip,
|
|
183
|
+
help="Comma-separated model ids to leave out of the JSON. Ids are zero-padded to 3 digits.",
|
|
184
|
+
)
|
|
185
|
+
@click.option(
|
|
186
|
+
"-o",
|
|
187
|
+
"--output",
|
|
188
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
189
|
+
default="models.json.gz",
|
|
190
|
+
show_default=True,
|
|
191
|
+
help="JSON file to write.",
|
|
192
|
+
)
|
|
193
|
+
def main(summary: Path, models: Path, output: Path, skip: set[str]) -> None:
|
|
194
|
+
rows = read_summary(summary)
|
|
195
|
+
dirs = index_dirs(models)
|
|
196
|
+
drop_skipped(rows, dirs, skip)
|
|
197
|
+
data = build(rows, dirs)
|
|
198
|
+
stream = gzip.open(output, "wt") if output.suffix == ".gz" else output.open("wt")
|
|
199
|
+
with stream as f:
|
|
200
|
+
json.dump(data, f, ensure_ascii=False, separators=(",", ":"), indent=2)
|
|
201
|
+
f.write("\n")
|
|
202
|
+
note = f", skipped {len(skip)}" if skip else ""
|
|
203
|
+
click.echo(f"wrote {len(data)} models to {output}{note}", err=True)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
if __name__ == "__main__":
|
|
207
|
+
main()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""bnet command line."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from booleannet.commands.gviz import cli as gviz
|
|
6
|
+
from booleannet.commands.models import main as models
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import pyboolnet
|
|
10
|
+
except ImportError:
|
|
11
|
+
print("# Error: pyboolnet is not installed. See the docs`.")
|
|
12
|
+
exit(1)
|
|
13
|
+
|
|
14
|
+
# Subcommand name -> click command. Add a new tool here.
|
|
15
|
+
COMMANDS = {
|
|
16
|
+
"models": models,
|
|
17
|
+
"graphviz": gviz,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@click.group("bnet", no_args_is_help=True)
|
|
22
|
+
def main() -> None:
|
|
23
|
+
"""BooleanNet command line tools."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
for name, cmd in COMMANDS.items():
|
|
27
|
+
main.add_command(cmd, name=name)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Subcommands for ``bnet``. Register each one in ``booleannet.cli.COMMANDS``."""
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Build a Graphviz interaction graph from BooleanNet rules."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from pyboolnet import log
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
from pyboolnet.boolean_normal_forms import functions2primes
|
|
13
|
+
from pyboolnet.interaction_graphs import (
|
|
14
|
+
add_style_interactionsigns,
|
|
15
|
+
igraph2dot,
|
|
16
|
+
primes2igraph,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
KEYWORDS = {"and", "or", "not", "True", "False"}
|
|
20
|
+
ENGINES = ("dot", "neato", "fdp", "sfdp", "circo", "twopi")
|
|
21
|
+
IDENT = re.compile(r"\b[A-Za-z_][A-Za-z0-9_]*\b")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def booleannet2functions(text: str) -> dict:
|
|
25
|
+
"""Turn BooleanNet update rules into callables for pyboolnet.
|
|
26
|
+
|
|
27
|
+
Parameter names are sorted because functions2primes calls each
|
|
28
|
+
function with arguments in alphabetical order.
|
|
29
|
+
"""
|
|
30
|
+
funcs = {}
|
|
31
|
+
for raw in text.splitlines():
|
|
32
|
+
line = raw.split("#", 1)[0].strip()
|
|
33
|
+
if not line:
|
|
34
|
+
continue
|
|
35
|
+
lhs, rhs = line.split("=", 1)
|
|
36
|
+
name = lhs.strip().removesuffix("*").strip()
|
|
37
|
+
expr = rhs.strip()
|
|
38
|
+
args = sorted(set(IDENT.findall(expr)) - KEYWORDS)
|
|
39
|
+
params = ", ".join(args)
|
|
40
|
+
src = f"lambda {params}: {expr}" if args else f"lambda: {expr}"
|
|
41
|
+
funcs[name] = eval(src, {"True": True, "False": False})
|
|
42
|
+
return funcs
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def read_rules(path: Path | None) -> tuple[str, Path | None]:
|
|
46
|
+
"""Return rules text and the file output names inherit from.
|
|
47
|
+
|
|
48
|
+
A pipe is the rules source when stdin is not a terminal. Otherwise the
|
|
49
|
+
source is ``path`` or ``input.txt``. The returned path is that file, or
|
|
50
|
+
``None`` when the rules came from stdin and no input file was given.
|
|
51
|
+
"""
|
|
52
|
+
if not sys.stdin.isatty():
|
|
53
|
+
return sys.stdin.read(), path
|
|
54
|
+
path = path or Path("input.txt")
|
|
55
|
+
if not path.is_file():
|
|
56
|
+
raise click.ClickException(f"rules file not found: {path}")
|
|
57
|
+
return path.read_text(), path
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def rules2dot(text: str, dot_path: Path) -> None:
|
|
61
|
+
primes = functions2primes(booleannet2functions(text))
|
|
62
|
+
graph = primes2igraph(primes)
|
|
63
|
+
# Default width is ~0.2in for one-letter names, which clips the labels.
|
|
64
|
+
graph.graph["node"]["width"] = "0.55"
|
|
65
|
+
graph.graph["node"]["fontsize"] = "14"
|
|
66
|
+
graph.graph["node"]["color"] = "gray20"
|
|
67
|
+
graph.graph["node"]["penwidth"] = "1.4"
|
|
68
|
+
# width is a minimum; let longer labels expand the circle
|
|
69
|
+
graph.graph["node"]["fixedsize"] = "false"
|
|
70
|
+
for name in graph.nodes:
|
|
71
|
+
if name.startswith("v_"):
|
|
72
|
+
graph.nodes[name]["label"] = name.removeprefix("v_")
|
|
73
|
+
add_style_interactionsigns(graph)
|
|
74
|
+
igraph2dot(graph, str(dot_path))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def write_image(dot: Path, image: Path, engine: str = "neato") -> None:
|
|
78
|
+
"""Render a dot file. The image format is the output suffix, such as png or pdf.
|
|
79
|
+
|
|
80
|
+
pyboolnet's layout lookup is hardcoded to /usr/bin, so this calls the engine on PATH.
|
|
81
|
+
"""
|
|
82
|
+
exe = shutil.which(engine)
|
|
83
|
+
if exe is None:
|
|
84
|
+
raise click.ClickException(f"{engine} not found on PATH")
|
|
85
|
+
fmt = image.suffix.lstrip(".").lower()
|
|
86
|
+
if not fmt:
|
|
87
|
+
raise click.ClickException(f"image path needs an extension such as .png or .pdf: {image}")
|
|
88
|
+
subprocess.run([exe, f"-T{fmt}", str(dot), "-o", str(image)], check=True)
|
|
89
|
+
log.info(f"image written to {image}")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@click.command()
|
|
93
|
+
@click.option("-i", "--input", "rules", type=click.Path(dir_okay=False, path_type=Path), help="BooleanNet rules file. Used when stdin is a terminal. Default: input.txt.")
|
|
94
|
+
@click.option("-d", "--dot", "dot", type=click.Path(dir_okay=False, path_type=Path), help="Dot output. Default: input path with a .dot suffix, or output.dot when reading stdin with no input file.")
|
|
95
|
+
@click.option("-o", "--output", "image", type=click.Path(dir_okay=False, path_type=Path), help="Image output. Default: input path with a .pdf suffix, or output.pdf when reading stdin with no input file. Format follows the extension (png, pdf, svg).")
|
|
96
|
+
@click.option("-e", "--engine", default="circo", show_default=True, type=click.Choice(ENGINES), help="Graphviz layout engine.")
|
|
97
|
+
def cli(rules: Path | None, dot: Path | None, image: Path | None, engine: str) -> None:
|
|
98
|
+
"""Generates a Graphviz graph from a model."""
|
|
99
|
+
text, source = read_rules(rules)
|
|
100
|
+
if source is None:
|
|
101
|
+
dot = dot or Path("output.dot")
|
|
102
|
+
image = image or Path("output.pdf")
|
|
103
|
+
else:
|
|
104
|
+
dot = dot or source.with_suffix(".dot")
|
|
105
|
+
image = image or source.with_suffix(".pdf")
|
|
106
|
+
rules2dot(text, dot)
|
|
107
|
+
write_image(dot, image, engine)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""List models or print one model in a chosen format.
|
|
3
|
+
|
|
4
|
+
bnet models one summary row per model
|
|
5
|
+
bnet models 1 bnet for model 001
|
|
6
|
+
bnet models CORTICAL model whose name contains CORTICAL
|
|
7
|
+
bnet models 1 -f aeon aeon for model 001
|
|
8
|
+
bnet models -d models.json read a chosen JSON database
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import gzip
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
from importlib.resources import files
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
|
|
19
|
+
DATA = files("booleannet").joinpath("data", "models.json.gz")
|
|
20
|
+
FORMATS = (
|
|
21
|
+
"bnet",
|
|
22
|
+
"booleannet",
|
|
23
|
+
"sbml",
|
|
24
|
+
"aeon",
|
|
25
|
+
"bma",
|
|
26
|
+
"inferred_graph",
|
|
27
|
+
"metadata",
|
|
28
|
+
"readme",
|
|
29
|
+
)
|
|
30
|
+
JSON_FORMATS = {"bma", "metadata"}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_models(path: Path | None):
|
|
34
|
+
if path is None:
|
|
35
|
+
with DATA.open("rb") as raw, gzip.open(raw, "rt") as f:
|
|
36
|
+
return json.load(f)
|
|
37
|
+
opener = gzip.open if path.suffix == ".gz" else open
|
|
38
|
+
with opener(path, "rt") as f:
|
|
39
|
+
return json.load(f)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def list_summaries(models):
|
|
43
|
+
rows = []
|
|
44
|
+
for model_id, model in models.items():
|
|
45
|
+
summary = model["summary"]
|
|
46
|
+
rows.append((
|
|
47
|
+
model_id,
|
|
48
|
+
summary["name"],
|
|
49
|
+
summary["variables"],
|
|
50
|
+
summary["inputs"],
|
|
51
|
+
summary["regulations"],
|
|
52
|
+
))
|
|
53
|
+
rows.sort(key=lambda row: (row[2], row[0]))
|
|
54
|
+
name_w = max(len(name) for _, name, _, _, _ in rows)
|
|
55
|
+
click.echo(f"{'id':3} {'name':<{name_w}} {'var':>5} {'in':>4} {'reg':>5}")
|
|
56
|
+
for model_id, name, variables, inputs, regulations in rows:
|
|
57
|
+
click.echo(f"{model_id} {name:<{name_w}} {variables:5} {inputs:4} {regulations:5}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def find_models(models, key: str) -> list[str]:
|
|
61
|
+
"""Match a number to an id, or text to an id or name."""
|
|
62
|
+
if key.isdigit():
|
|
63
|
+
model_id = key.zfill(3)
|
|
64
|
+
return [model_id] if model_id in models else []
|
|
65
|
+
needle = key.casefold()
|
|
66
|
+
return [
|
|
67
|
+
model_id
|
|
68
|
+
for model_id, model in models.items()
|
|
69
|
+
if needle in model_id.casefold() or needle in model["summary"]["name"].casefold()
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def emit(model, fmt):
|
|
74
|
+
value = model[fmt]
|
|
75
|
+
if fmt in JSON_FORMATS:
|
|
76
|
+
click.echo(json.dumps(value, ensure_ascii=False))
|
|
77
|
+
return
|
|
78
|
+
click.echo(value, nl=not value.endswith("\n"))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@click.command()
|
|
82
|
+
@click.argument("key", required=False)
|
|
83
|
+
@click.option(
|
|
84
|
+
"-d",
|
|
85
|
+
"--database",
|
|
86
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
87
|
+
help="JSON or JSON.gz database. Default: packaged data/models.json.gz.",
|
|
88
|
+
)
|
|
89
|
+
@click.option(
|
|
90
|
+
"-f",
|
|
91
|
+
"--format",
|
|
92
|
+
"fmt",
|
|
93
|
+
default="booleannet",
|
|
94
|
+
show_default=True,
|
|
95
|
+
type=click.Choice(FORMATS),
|
|
96
|
+
help="Model file to print when KEY is given.",
|
|
97
|
+
)
|
|
98
|
+
def main(key, database, fmt):
|
|
99
|
+
"""List model summaries, or print one model in the chosen format.
|
|
100
|
+
|
|
101
|
+
KEY is a number (1 or 001) or text matched against the id or name.
|
|
102
|
+
"""
|
|
103
|
+
models = load_models(database)
|
|
104
|
+
if key is None:
|
|
105
|
+
list_summaries(models)
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
hits = find_models(models, key)
|
|
109
|
+
if not hits:
|
|
110
|
+
raise click.ClickException(f"no model {key}")
|
|
111
|
+
if len(hits) > 1:
|
|
112
|
+
lines = [f"{i} {models[i]['summary']['name']}" for i in sorted(hits)]
|
|
113
|
+
raise click.ClickException("several models match:\n" + "\n".join(lines))
|
|
114
|
+
emit(models[hits[0]], fmt)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if __name__ == "__main__":
|
|
118
|
+
try:
|
|
119
|
+
main()
|
|
120
|
+
except BrokenPipeError:
|
|
121
|
+
sys.stdout.close()
|
|
122
|
+
sys.exit(0)
|
|
Binary file
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
booleannet/__init__.py
|
|
4
|
+
booleannet/__main__.py
|
|
5
|
+
booleannet/bbm2json.py
|
|
6
|
+
booleannet/cli.py
|
|
7
|
+
booleannet.egg-info/PKG-INFO
|
|
8
|
+
booleannet.egg-info/SOURCES.txt
|
|
9
|
+
booleannet.egg-info/dependency_links.txt
|
|
10
|
+
booleannet.egg-info/entry_points.txt
|
|
11
|
+
booleannet.egg-info/requires.txt
|
|
12
|
+
booleannet.egg-info/top_level.txt
|
|
13
|
+
booleannet/commands/__init__.py
|
|
14
|
+
booleannet/commands/gviz.py
|
|
15
|
+
booleannet/commands/models.py
|
|
16
|
+
booleannet/data/models.json.gz
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
click<9,>=8.5
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
booleannet
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "booleannet"
|
|
7
|
+
version = "2.0"
|
|
8
|
+
description = "Boolean network modeling"
|
|
9
|
+
authors = [{ name = "Istvan Albert", email = "istvan.albert@gmail.com" }]
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
dependencies = ["click>=8.5,<9"]
|
|
12
|
+
|
|
13
|
+
[project.scripts]
|
|
14
|
+
bnet = "booleannet.cli:main"
|
|
15
|
+
|
|
16
|
+
[tool.setuptools.packages.find]
|
|
17
|
+
include = ["booleannet*"]
|
|
18
|
+
|
|
19
|
+
[tool.setuptools.package-data]
|
|
20
|
+
booleannet = ["data/*.json.gz"]
|
booleannet-2.0/setup.cfg
ADDED