embkit 0.3__tar.gz → 0.4__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.
- {embkit-0.3 → embkit-0.4}/PKG-INFO +1 -1
- {embkit-0.3 → embkit-0.4}/pyproject.toml +1 -1
- {embkit-0.3 → embkit-0.4}/setup.py +1 -1
- {embkit-0.3 → embkit-0.4}/src/embkit/c_bio/api.py +4 -1
- {embkit-0.3 → embkit-0.4}/src/embkit/commands/align.py +3 -3
- {embkit-0.3 → embkit-0.4}/src/embkit/commands/matrix.py +21 -3
- embkit-0.4/src/embkit/commands/model.py +401 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/commands/protein.py +12 -8
- embkit-0.4/src/embkit/constraints/__init__.py +7 -0
- embkit-0.4/src/embkit/constraints/pathway_constraint.py +110 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/encoding/__init__.py +49 -3
- embkit-0.4/src/embkit/encoding/genome.py +74 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/factory/__init__.py +1 -1
- embkit-0.4/src/embkit/factory/core.py +100 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/factory/layers.py +33 -108
- {embkit-0.3 → embkit-0.4}/src/embkit/files/h5.py +17 -26
- {embkit-0.3 → embkit-0.4}/src/embkit/files/read_csv.py +28 -9
- {embkit-0.3 → embkit-0.4}/src/embkit/losses/vae_loss.py +8 -3
- embkit-0.4/src/embkit/models/ffnn.py +127 -0
- embkit-0.4/src/embkit/models/vae/base_vae.py +240 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/models/vae/decoder.py +17 -4
- {embkit-0.3 → embkit-0.4}/src/embkit/models/vae/encoder.py +35 -20
- embkit-0.4/src/embkit/models/vae/net_vae.py +292 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/models/vae/rna_vae.py +63 -9
- {embkit-0.3 → embkit-0.4}/src/embkit/models/vae/vae.py +13 -4
- {embkit-0.3 → embkit-0.4}/src/embkit/modules/masked_linear.py +17 -2
- {embkit-0.3 → embkit-0.4}/src/embkit/modules/tsp.py +10 -6
- {embkit-0.3 → embkit-0.4}/src/embkit/optimize/__init__.py +60 -67
- embkit-0.4/src/embkit/pathway.py +227 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/preprocessing/normalize.py +5 -5
- {embkit-0.3 → embkit-0.4}/src/embkit/resources/resource.py +19 -6
- {embkit-0.3 → embkit-0.4}/src/embkit/utilities/pca.py +7 -4
- {embkit-0.3 → embkit-0.4}/src/embkit.egg-info/PKG-INFO +1 -1
- {embkit-0.3 → embkit-0.4}/src/embkit.egg-info/SOURCES.txt +2 -1
- embkit-0.3/src/embkit/commands/model.py +0 -233
- embkit-0.3/src/embkit/constraints/__init__.py +0 -1
- embkit-0.3/src/embkit/constraints/network_constraint.py +0 -72
- embkit-0.3/src/embkit/factory/core.py +0 -46
- embkit-0.3/src/embkit/models/ffnn.py +0 -50
- embkit-0.3/src/embkit/models/vae/base_vae.py +0 -112
- embkit-0.3/src/embkit/models/vae/net_vae.py +0 -115
- embkit-0.3/src/embkit/pathway.py +0 -227
- {embkit-0.3 → embkit-0.4}/README.md +0 -0
- {embkit-0.3 → embkit-0.4}/setup.cfg +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/__init__.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/__main__.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/align.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/bmeg.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/c_bio/__init__.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/commands/__init__.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/commands/cbio.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/commands/resources.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/datasets/__init__.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/encoding/protein.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/estimator/__init__.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/estimator/vae_estimator.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/factory/mapping.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/factory/registry.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/files/__init__.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/files/json.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/files/loaders.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/losses/__init__.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/metrics/__init__.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/metrics/distance/__init__.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/metrics/distance/metrics.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/models/__init__.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/models/vae/__init__.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/modules/__init__.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/modules/pairwise_comparison_layer.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/preprocessing/__init__.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/preprocessing/dataset.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/resources/__init__.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/resources/c_bio_portal.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/resources/gtex.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/resources/hugo.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/resources/sif.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/resources/util.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/utilities/__init__.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/utilities/kmeans.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit/version.py +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit.egg-info/dependency_links.txt +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit.egg-info/entry_points.txt +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit.egg-info/requires.txt +0 -0
- {embkit-0.3 → embkit-0.4}/src/embkit.egg-info/top_level.txt +0 -0
- {embkit-0.3 → embkit-0.4}/tests/test_main.py +0 -0
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import requests
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
logger = logging.getLogger(__name__)
|
|
2
5
|
|
|
3
6
|
|
|
4
7
|
class CBIOAPI:
|
|
@@ -9,5 +12,5 @@ class CBIOAPI:
|
|
|
9
12
|
studies = requests.get("https://www.cbioportal.org/api/studies").json()
|
|
10
13
|
return studies
|
|
11
14
|
except requests.RequestException as e:
|
|
12
|
-
|
|
15
|
+
logger.error("Error fetching studies: %s", e)
|
|
13
16
|
return None
|
|
@@ -25,6 +25,6 @@ def pair(matrix1, matrix2, method, cutoff):
|
|
|
25
25
|
m2 = pd.read_csv(matrix2, sep="\t", index_col=0)
|
|
26
26
|
|
|
27
27
|
if method == "linear":
|
|
28
|
-
|
|
29
|
-
for
|
|
30
|
-
|
|
28
|
+
out_a, out_b, out_score = matrix_spearman_alignment_linear(m1, m2, cutoff)
|
|
29
|
+
for a_id, b_id, score in zip(out_a, out_b, out_score):
|
|
30
|
+
click.echo(f"{a_id}\t{b_id}\t{score}")
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import click
|
|
2
2
|
import pandas as pd
|
|
3
|
+
from pathlib import Path
|
|
3
4
|
|
|
4
5
|
matrix = click.Group(name="matrix", help="Model commands.")
|
|
5
6
|
|
|
@@ -14,15 +15,14 @@ def normalize(srcs, out, features, col_quantile, quantile_max, precision):
|
|
|
14
15
|
|
|
15
16
|
features_list = None
|
|
16
17
|
if features is not None:
|
|
17
|
-
features = []
|
|
18
18
|
with open(features, encoding="ascii") as handle:
|
|
19
19
|
features_list = list(line.rstrip() for line in handle)
|
|
20
20
|
|
|
21
21
|
dfs = []
|
|
22
22
|
for i in srcs:
|
|
23
23
|
df = pd.read_csv(i, sep="\t", index_col=0)
|
|
24
|
-
if
|
|
25
|
-
df = df[
|
|
24
|
+
if features_list is not None:
|
|
25
|
+
df = df[features_list]
|
|
26
26
|
dfs.append(df)
|
|
27
27
|
if len(dfs) == 0:
|
|
28
28
|
click.echo("No matrices defined")
|
|
@@ -34,3 +34,21 @@ def normalize(srcs, out, features, col_quantile, quantile_max, precision):
|
|
|
34
34
|
else:
|
|
35
35
|
normDF = (df / df.quantile(quantile_max)).clip(upper=1.0, lower=0.0).fillna(0.0)
|
|
36
36
|
normDF.round(decimals=precision).to_csv(out, sep="\t")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@matrix.command()
|
|
40
|
+
@click.argument("input_path", type=click.Path(exists=True, dir_okay=False, readable=True, path_type=str))
|
|
41
|
+
@click.option("--pca-size", required=True, type=int, help="Number of principal components.")
|
|
42
|
+
@click.option("--out", "-o", default=None, type=str, help="Output TSV path.")
|
|
43
|
+
def pca(input_path, pca_size, out):
|
|
44
|
+
if pca_size <= 0:
|
|
45
|
+
raise click.BadParameter("--pca-size must be a positive integer.")
|
|
46
|
+
|
|
47
|
+
from ..utilities.pca import run_pca
|
|
48
|
+
|
|
49
|
+
if out is None:
|
|
50
|
+
out = f"{Path(input_path).stem}.pca.tsv"
|
|
51
|
+
click.echo(f"No output path provided, using default naming: {out}")
|
|
52
|
+
|
|
53
|
+
run_pca(input_file=input_path, pca_size=pca_size, output_file=out)
|
|
54
|
+
click.echo(f"PCA saved to {out}")
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
|
|
2
|
+
import click
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from sklearn.preprocessing import MinMaxScaler
|
|
7
|
+
|
|
8
|
+
import torch
|
|
9
|
+
from torch.utils.data import DataLoader
|
|
10
|
+
|
|
11
|
+
from .. import dataframe_loader, dataframe_tensor, get_device, dataframe_dataset
|
|
12
|
+
from ..files import H5Reader
|
|
13
|
+
from ..factory import save, load
|
|
14
|
+
from ..factory.layers import Layer, LayerList
|
|
15
|
+
from ..optimize import fit_vae
|
|
16
|
+
from ..models.vae.vae import VAE
|
|
17
|
+
from ..models.vae.net_vae import NetVAE
|
|
18
|
+
from ..preprocessing import ExpMinMaxScaler, get_dataset_nonzero_mask
|
|
19
|
+
from ..datasets import DatasetMask
|
|
20
|
+
from ..losses import bce_with_logits, bce, mse
|
|
21
|
+
from ..pathway import extract_sif_interactions, feature_map_intersect, build_feature_map_indices
|
|
22
|
+
|
|
23
|
+
model = click.Group(name="model", help="VAE Model commands.")
|
|
24
|
+
|
|
25
|
+
@model.command()
|
|
26
|
+
@click.argument("input_path", type=click.Path(exists=True, dir_okay=False, readable=True, path_type=str))
|
|
27
|
+
@click.option("--group", "-g", default=None, help="HD5F group name")
|
|
28
|
+
@click.option("--latent", "-l", type=int, default=256, show_default=True, help="Latent dimension size.")
|
|
29
|
+
@click.option("--epochs", "-e", type=int, default=20, show_default=True, help="Training epochs.")
|
|
30
|
+
@click.option("--batch-size", "-b", type=int, default=256)
|
|
31
|
+
@click.option("--encode-layers", type=str, default="400,200")
|
|
32
|
+
@click.option("--decode-layers", type=str, default="200,400")
|
|
33
|
+
@click.option("--normalize", "-n", type=click.Choice(["none", "expMinMax", "minMax"]), default="none")
|
|
34
|
+
@click.option("--final-activation", default="none", type=click.Choice(["none", "sigmoid", "relu"]))
|
|
35
|
+
@click.option("--learning-rate", "-r", type=float, default=0.0001)
|
|
36
|
+
@click.option("--out", "-o", type=str, default=None)
|
|
37
|
+
@click.option("--schedule", "-s", type=str, default=None, help="20:0,20:0.1,40:.3,40:.4")
|
|
38
|
+
@click.option("--loss", type=click.Choice(["mse", "bce", "bce-logit"]), default="bce-logit")
|
|
39
|
+
@click.option("--save-stats", is_flag=True)
|
|
40
|
+
@click.option("--zero-mask", default=None, type=float)
|
|
41
|
+
@click.option("--seed", default=42, type=int)
|
|
42
|
+
@click.option("--bfloat16", is_flag=True)
|
|
43
|
+
@click.option("--sampling/--no-sampling", default=True, show_default=True, help="Enable reparameterization sampling during training (VAE). Use --no-sampling for a standard autoencoder.")
|
|
44
|
+
def train_vae(input_path: str,
|
|
45
|
+
group: str,
|
|
46
|
+
latent: int,
|
|
47
|
+
epochs: int,
|
|
48
|
+
batch_size: int,
|
|
49
|
+
out: str, normalize:str,
|
|
50
|
+
encode_layers:str, decode_layers:str,
|
|
51
|
+
learning_rate: float,
|
|
52
|
+
final_activation: str,
|
|
53
|
+
loss: str,
|
|
54
|
+
schedule:str,
|
|
55
|
+
zero_mask: float,
|
|
56
|
+
save_stats: bool,
|
|
57
|
+
seed: int,
|
|
58
|
+
bfloat16: bool,
|
|
59
|
+
sampling: bool
|
|
60
|
+
):
|
|
61
|
+
"""
|
|
62
|
+
Train VAE model from a TSV file.
|
|
63
|
+
"""
|
|
64
|
+
device = get_device()
|
|
65
|
+
dtype = torch.float32
|
|
66
|
+
if bfloat16:
|
|
67
|
+
dtype = torch.bfloat16
|
|
68
|
+
|
|
69
|
+
torch.manual_seed(seed)
|
|
70
|
+
df = None
|
|
71
|
+
if group is not None:
|
|
72
|
+
if normalize != "none":
|
|
73
|
+
raise click.BadParameter(
|
|
74
|
+
"Normalization for HDF5 input is not supported in train-vae. "
|
|
75
|
+
"Use '--normalize none' for HDF5 or provide TSV input for normalization."
|
|
76
|
+
)
|
|
77
|
+
dataset = H5Reader(input_path, group)
|
|
78
|
+
if zero_mask is not None:
|
|
79
|
+
dataset_mask = get_dataset_nonzero_mask(dataset, zero_mask)
|
|
80
|
+
mask = dataset_mask[0].cpu().numpy()
|
|
81
|
+
features = dataset.columns[mask]
|
|
82
|
+
dataset = DatasetMask(dataset, dataset_mask, device)
|
|
83
|
+
else:
|
|
84
|
+
dataset.to(device)
|
|
85
|
+
features = dataset.columns
|
|
86
|
+
|
|
87
|
+
else:
|
|
88
|
+
df = pd.read_csv(input_path, sep="\t", index_col=0)
|
|
89
|
+
|
|
90
|
+
if normalize == "expMinMax":
|
|
91
|
+
norm = ExpMinMaxScaler()
|
|
92
|
+
norm.fit(df)
|
|
93
|
+
df = pd.DataFrame( norm.transform(df), index=df.index, columns=df.columns)
|
|
94
|
+
elif normalize == "minMax":
|
|
95
|
+
norm = MinMaxScaler()
|
|
96
|
+
norm.fit(df)
|
|
97
|
+
df = pd.DataFrame( norm.transform(df), index=df.index, columns=df.columns)
|
|
98
|
+
features = df.columns
|
|
99
|
+
dataset = dataframe_dataset(df, device=device, dtype=dtype)
|
|
100
|
+
|
|
101
|
+
dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
layer_sizes = list( int(i) for i in encode_layers.split(",") )
|
|
105
|
+
enc_layers_list = LayerList( layer_sizes )
|
|
106
|
+
|
|
107
|
+
layer_sizes = list( int(i) for i in decode_layers.split(",") )
|
|
108
|
+
dec_layers_list = LayerList( layer_sizes, end_activation=final_activation )
|
|
109
|
+
|
|
110
|
+
beta_schedule = None
|
|
111
|
+
if schedule is not None:
|
|
112
|
+
beta_schedule = []
|
|
113
|
+
for b in schedule.split(","):
|
|
114
|
+
e, b = b.split(":")
|
|
115
|
+
beta_schedule.append( (float(b), int(e)) )
|
|
116
|
+
vae = VAE(features=features,
|
|
117
|
+
latent_dim=latent,
|
|
118
|
+
encoder_layers=enc_layers_list,
|
|
119
|
+
decoder_layers=dec_layers_list,
|
|
120
|
+
sampling=sampling,
|
|
121
|
+
device=device, dtype=dtype)
|
|
122
|
+
|
|
123
|
+
loss_func = bce_with_logits
|
|
124
|
+
if loss == "mse":
|
|
125
|
+
loss_func = mse
|
|
126
|
+
elif loss == "bce":
|
|
127
|
+
loss_func = bce
|
|
128
|
+
|
|
129
|
+
fit_vae(vae, dataloader, epochs=epochs,
|
|
130
|
+
beta_schedule=beta_schedule, lr=learning_rate, loss=loss_func)
|
|
131
|
+
click.echo("Training complete.")
|
|
132
|
+
|
|
133
|
+
if out is None:
|
|
134
|
+
click.echo(f"No output path provided, using default naming.")
|
|
135
|
+
out = f"vae_latent{latent}_epochs{epochs}.model"
|
|
136
|
+
|
|
137
|
+
save(vae, out)
|
|
138
|
+
click.echo(f"Model saved, to {out}")
|
|
139
|
+
|
|
140
|
+
if save_stats and df is not None:
|
|
141
|
+
stats = pd.DataFrame({"mean": df.mean(), "std": df.std(ddof=0)})
|
|
142
|
+
stats_path = f"{out}.stats.tsv"
|
|
143
|
+
stats.to_csv(stats_path, sep="\t")
|
|
144
|
+
click.echo(f"Stats saved, to {stats_path}")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@model.command()
|
|
148
|
+
@click.argument("input_path", type=click.Path(exists=True, dir_okay=False, readable=True, path_type=str))
|
|
149
|
+
@click.argument("pathway_sif", type=click.Path(exists=True, dir_okay=False, readable=True, path_type=str))
|
|
150
|
+
@click.option("--epochs", "-e", type=int, default=20, show_default=True, help="Training epochs.")
|
|
151
|
+
@click.option("--normalize", "-n", type=str, default="none")
|
|
152
|
+
@click.option("--learning-rate", "-r", type=float, default=0.0001)
|
|
153
|
+
@click.option("--out", "-o", type=str, default=None)
|
|
154
|
+
@click.option("--schedule", "-s", type=str, default=None, help="20:0,20:0.1,40:.3,40:.4")
|
|
155
|
+
@click.option("--loss", type=click.Choice(["mse", "bce", "bce-logit"]), default="bce-logit")
|
|
156
|
+
@click.option("--min-group-size", type=int, default=0, show_default=True, help="Minimum group size filter for pathway feature map (including self if present).")
|
|
157
|
+
@click.option("--group-layer-scale", default="5,2,1", show_default=True,
|
|
158
|
+
help="Comma-separated per-group widths for NetVAE masked layers.")
|
|
159
|
+
@click.option("--save-stats", is_flag=True)
|
|
160
|
+
def train_netvae(input_path: str, pathway_sif:str, out:str,
|
|
161
|
+
epochs: int, normalize: str,
|
|
162
|
+
learning_rate: float,
|
|
163
|
+
loss:str,
|
|
164
|
+
group_layer_scale: str,
|
|
165
|
+
min_group_size: int,
|
|
166
|
+
schedule: str,
|
|
167
|
+
save_stats:bool):
|
|
168
|
+
"""Train VAE model from a TSV file."""
|
|
169
|
+
df = pd.read_csv(input_path, sep="\t", index_col=0)
|
|
170
|
+
|
|
171
|
+
feature_map = extract_sif_interactions(pathway_sif)
|
|
172
|
+
feature_map = feature_map_intersect(feature_map, df.columns, min_group_size=min_group_size)
|
|
173
|
+
feature_idx, group_idx = build_feature_map_indices(feature_map)
|
|
174
|
+
|
|
175
|
+
df = df[feature_idx]
|
|
176
|
+
if normalize == "expMinMax":
|
|
177
|
+
norm = ExpMinMaxScaler()
|
|
178
|
+
norm.fit(df)
|
|
179
|
+
df = pd.DataFrame( norm.transform(df), index=df.index, columns=df.columns)
|
|
180
|
+
|
|
181
|
+
beta_schedule = None
|
|
182
|
+
if schedule is not None:
|
|
183
|
+
beta_schedule = []
|
|
184
|
+
for b in schedule.split(","):
|
|
185
|
+
e, b = b.split(":")
|
|
186
|
+
beta_schedule.append( (float(b), int(e)) )
|
|
187
|
+
|
|
188
|
+
batch_size=256
|
|
189
|
+
dataloader = dataframe_loader(df, batch_size=batch_size)
|
|
190
|
+
|
|
191
|
+
group_count = len(group_idx)
|
|
192
|
+
feature_count = len(feature_idx)
|
|
193
|
+
|
|
194
|
+
click.echo(f"Feature count {feature_count} latent_size: {group_count}")
|
|
195
|
+
|
|
196
|
+
gcounts = [int(v.strip()) for v in group_layer_scale.split(",") if v.strip()]
|
|
197
|
+
if not gcounts or any(v <= 0 for v in gcounts):
|
|
198
|
+
raise click.BadParameter("--group-layer-scale must contain one or more positive integers.")
|
|
199
|
+
|
|
200
|
+
loss_func = bce_with_logits
|
|
201
|
+
if loss == "mse":
|
|
202
|
+
loss_func = mse
|
|
203
|
+
elif loss == "bce":
|
|
204
|
+
loss_func = bce
|
|
205
|
+
|
|
206
|
+
if beta_schedule is None:
|
|
207
|
+
beta_schedule = [(0.0, epochs)]
|
|
208
|
+
vae = NetVAE(list(df.columns), latent_groups=feature_map, group_layer_scale=gcounts)
|
|
209
|
+
fit_vae(vae, X=dataloader, beta_schedule=beta_schedule, lr=learning_rate, loss=loss_func)
|
|
210
|
+
|
|
211
|
+
click.echo("Training complete.")
|
|
212
|
+
|
|
213
|
+
if out is None:
|
|
214
|
+
click.echo("No output path provided, using default naming.")
|
|
215
|
+
out = f"netvae_latent{group_count}_epochs{epochs}.model"
|
|
216
|
+
|
|
217
|
+
save(vae, out)
|
|
218
|
+
click.echo(f"Model saved, to {out}")
|
|
219
|
+
|
|
220
|
+
if save_stats:
|
|
221
|
+
stats = pd.DataFrame({"mean": df.mean(), "std": df.std(ddof=0)})
|
|
222
|
+
stats_path = f"{out}.stats.tsv"
|
|
223
|
+
stats.to_csv(stats_path, sep="\t")
|
|
224
|
+
click.echo(f"Stats saved, to {stats_path}")
|
|
225
|
+
|
|
226
|
+
@model.command()
|
|
227
|
+
@click.argument("input_path", type=click.Path(exists=True, dir_okay=False, readable=True, path_type=str))
|
|
228
|
+
@click.argument("model_path", type=click.Path(exists=True, dir_okay=True, readable=True, path_type=str))
|
|
229
|
+
@click.option("--normalize", "-n", type=str, default="none")
|
|
230
|
+
@click.option("--out", "-o", type=str, default="embedding.tsv")
|
|
231
|
+
def encode(input_path: str, model_path:str, normalize:str, out:str):
|
|
232
|
+
|
|
233
|
+
m = load(model_path)
|
|
234
|
+
df = pd.read_csv(input_path, sep="\t", index_col=0)
|
|
235
|
+
|
|
236
|
+
df = df[m.features]
|
|
237
|
+
if normalize == "expMinMax":
|
|
238
|
+
norm = ExpMinMaxScaler()
|
|
239
|
+
norm.fit(df)
|
|
240
|
+
df = pd.DataFrame( norm.transform(df), index=df.index, columns=df.columns)
|
|
241
|
+
|
|
242
|
+
df_tensor = dataframe_tensor(df).to(get_device())
|
|
243
|
+
m.to(get_device())
|
|
244
|
+
result = m.encoder(df_tensor)
|
|
245
|
+
|
|
246
|
+
matrix = result[0].detach().cpu().numpy()
|
|
247
|
+
out_df = pd.DataFrame(matrix, index=df.index)
|
|
248
|
+
out_df.to_csv(out, sep="\t")
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@model.command()
|
|
252
|
+
@click.argument("model_path", type=click.Path(exists=True, dir_okay=True, readable=True, path_type=str))
|
|
253
|
+
@click.option("--json", "as_json", is_flag=True, help="Emit a machine-readable JSON report.")
|
|
254
|
+
@click.option("--ci", is_flag=True, help="CI mode: same as '--json --fail-on-unhealthy'.")
|
|
255
|
+
@click.option("--fail-on-unhealthy/--no-fail-on-unhealthy", default=False, show_default=True,
|
|
256
|
+
help="Return non-zero exit code when integrity checks fail.")
|
|
257
|
+
@click.option("--strict", is_flag=True, help="Enable strict identity checks against expected model shape.")
|
|
258
|
+
@click.option("--expected-feature-count", type=int, default=None,
|
|
259
|
+
help="Expected number of input features in strict mode.")
|
|
260
|
+
@click.option("--expected-latent-dim", type=int, default=None,
|
|
261
|
+
help="Expected latent dimension in strict mode.")
|
|
262
|
+
@click.option("--expected-features-file",
|
|
263
|
+
type=click.Path(exists=True, dir_okay=False, readable=True, path_type=str),
|
|
264
|
+
default=None,
|
|
265
|
+
help="Path to newline-delimited expected feature names in strict mode.")
|
|
266
|
+
def verify(
|
|
267
|
+
model_path: str,
|
|
268
|
+
as_json: bool,
|
|
269
|
+
ci: bool,
|
|
270
|
+
fail_on_unhealthy: bool,
|
|
271
|
+
strict: bool,
|
|
272
|
+
expected_feature_count: int | None,
|
|
273
|
+
expected_latent_dim: int | None,
|
|
274
|
+
expected_features_file: str | None,
|
|
275
|
+
):
|
|
276
|
+
"""
|
|
277
|
+
Verify model integrity and architecture sanity.
|
|
278
|
+
"""
|
|
279
|
+
from ..factory.core import run_model_verification
|
|
280
|
+
from .. import get_device
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
if ci:
|
|
284
|
+
as_json = True
|
|
285
|
+
fail_on_unhealthy = True
|
|
286
|
+
|
|
287
|
+
if not as_json:
|
|
288
|
+
click.secho(f"Starting integrity verification: {model_path}...", fg="cyan", bold=True)
|
|
289
|
+
report = run_model_verification(model_path, device=get_device())
|
|
290
|
+
report.setdefault("issues", [])
|
|
291
|
+
|
|
292
|
+
expected_features = None
|
|
293
|
+
if expected_features_file is not None:
|
|
294
|
+
with open(expected_features_file, encoding="utf-8") as handle:
|
|
295
|
+
expected_features = [line.strip() for line in handle if line.strip()]
|
|
296
|
+
|
|
297
|
+
if strict:
|
|
298
|
+
strict_issues = []
|
|
299
|
+
actual_feature_names = report.get("feature_names")
|
|
300
|
+
actual_feature_count = report.get("features_count")
|
|
301
|
+
|
|
302
|
+
if expected_features is not None:
|
|
303
|
+
if actual_feature_names is None:
|
|
304
|
+
strict_issues.append(
|
|
305
|
+
"Strict check failed: model report does not expose feature names."
|
|
306
|
+
)
|
|
307
|
+
elif list(actual_feature_names) != list(expected_features):
|
|
308
|
+
strict_issues.append(
|
|
309
|
+
"Strict check failed: feature names/order do not match expected list."
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
if expected_feature_count is not None:
|
|
313
|
+
if actual_feature_count is None:
|
|
314
|
+
strict_issues.append(
|
|
315
|
+
"Strict check failed: model report does not expose feature count."
|
|
316
|
+
)
|
|
317
|
+
elif int(actual_feature_count) != int(expected_feature_count):
|
|
318
|
+
strict_issues.append(
|
|
319
|
+
f"Strict check failed: expected feature count {expected_feature_count}, got {actual_feature_count}."
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
if expected_latent_dim is not None:
|
|
323
|
+
actual_latent_dim = None
|
|
324
|
+
deep_audit = report.get("deep_audit")
|
|
325
|
+
if isinstance(deep_audit, dict):
|
|
326
|
+
actual_latent_dim = deep_audit.get("latent_dim")
|
|
327
|
+
if actual_latent_dim is None:
|
|
328
|
+
actual_latent_dim = report.get("declared_latent_dim")
|
|
329
|
+
if actual_latent_dim is None:
|
|
330
|
+
strict_issues.append(
|
|
331
|
+
"Strict check failed: model report does not expose latent dim."
|
|
332
|
+
)
|
|
333
|
+
elif int(actual_latent_dim) != int(expected_latent_dim):
|
|
334
|
+
strict_issues.append(
|
|
335
|
+
f"Strict check failed: expected latent dim {expected_latent_dim}, got {actual_latent_dim}."
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
report["strict_mode"] = True
|
|
339
|
+
report["strict_issues"] = strict_issues
|
|
340
|
+
if strict_issues:
|
|
341
|
+
report["healthy"] = False
|
|
342
|
+
report["issues"].extend(strict_issues)
|
|
343
|
+
else:
|
|
344
|
+
report["strict_mode"] = False
|
|
345
|
+
report["strict_issues"] = []
|
|
346
|
+
|
|
347
|
+
if as_json:
|
|
348
|
+
click.echo(json.dumps(report, indent=2, sort_keys=True))
|
|
349
|
+
else:
|
|
350
|
+
if report["healthy"]:
|
|
351
|
+
click.secho("PASS: model integrity checks passed.", fg="green", bold=True)
|
|
352
|
+
else:
|
|
353
|
+
click.secho("FAIL: model integrity checks failed.", fg="red", bold=True)
|
|
354
|
+
|
|
355
|
+
click.echo(f"Model Type: {report['model_type']}")
|
|
356
|
+
click.echo("\n--- Integrity Diagnostics ---")
|
|
357
|
+
param_issues = [i for i in report["issues"] if any(k in i for k in ["parameter", "NaN", "weight norm"])]
|
|
358
|
+
click.echo(f" {'PASS' if not param_issues else 'FAIL'} Weight Health: {report.get('weight_norm_max', 0.0):.2f} (max norm)")
|
|
359
|
+
|
|
360
|
+
if "history_summary" in report:
|
|
361
|
+
hist = report["history_summary"]
|
|
362
|
+
click.echo(f" PASS Training Trace: {hist['epochs']} epochs, loss improvement {hist['improvement']:.4f}")
|
|
363
|
+
else:
|
|
364
|
+
click.secho(" WARN Training Trace: Missing history; cannot assess learning trend.", fg="yellow")
|
|
365
|
+
|
|
366
|
+
for issue in report["issues"]:
|
|
367
|
+
color = "red" if not report["healthy"] else "yellow"
|
|
368
|
+
click.secho(f" - {issue}", fg=color)
|
|
369
|
+
|
|
370
|
+
if "sparsity_audit" in report:
|
|
371
|
+
click.echo("\n--- Sparsity & Leakage Audit ---")
|
|
372
|
+
for layer in report["sparsity_audit"]:
|
|
373
|
+
status = "PASS" if layer["is_healthy"] else "FAIL"
|
|
374
|
+
leak_str = f"Leakage Sum: {layer['leakage_sum']:.2e}" if layer['leakage_sum'] > 1e-12 else "Zero Leakage"
|
|
375
|
+
click.echo(f" {status} {layer['layer']}:")
|
|
376
|
+
click.echo(f" Mask Sparsity: {layer['effective_sparsity']:.2%}")
|
|
377
|
+
click.echo(f" Raw Sparsity: {layer['raw_sparsity']:.2%}")
|
|
378
|
+
click.echo(f" {leak_str}")
|
|
379
|
+
|
|
380
|
+
if "deep_audit" in report:
|
|
381
|
+
audit = report["deep_audit"]
|
|
382
|
+
click.echo("\n--- Latent Manifold Audit ---")
|
|
383
|
+
click.echo(f" Latent Units: {audit.get('latent_dim', 'unknown')}")
|
|
384
|
+
click.echo(f" Dead Units: {audit.get('dead_units', 'unknown')}")
|
|
385
|
+
if "reconstruction_mse" in audit:
|
|
386
|
+
click.echo(f" Reconstruction MSE: {audit['reconstruction_mse']:.4f}")
|
|
387
|
+
|
|
388
|
+
if "rna_diagnostics" in report:
|
|
389
|
+
diag = report["rna_diagnostics"]
|
|
390
|
+
click.echo("\n--- RNA Specific Diagnostics ---")
|
|
391
|
+
click.echo(f" Non-negative heads: {'PASS' if diag['is_non_negative'] else 'FAIL'}")
|
|
392
|
+
click.echo(f" Min mu: {diag['min_mu']:.4f}, Min logvar: {diag['min_logvar']:.4f}")
|
|
393
|
+
|
|
394
|
+
if fail_on_unhealthy and not report["healthy"]:
|
|
395
|
+
raise click.ClickException("Model integrity check failed.")
|
|
396
|
+
|
|
397
|
+
except click.ClickException:
|
|
398
|
+
raise
|
|
399
|
+
except Exception as e:
|
|
400
|
+
click.secho(f"Error during verification: {e}", fg="red")
|
|
401
|
+
raise click.Abort()
|
|
@@ -18,13 +18,14 @@ from ..encoding.protein import ProteinEncoder
|
|
|
18
18
|
protein = click.Group(name="protein", help="Protein commands.")
|
|
19
19
|
|
|
20
20
|
def fasta_reader(path, filter=None):
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if not
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
with open(path, "rt") as handle:
|
|
22
|
+
for record in SeqIO.parse(handle, "fasta"):
|
|
23
|
+
use = True
|
|
24
|
+
if filter is not None:
|
|
25
|
+
if not re.match(filter, record.id):
|
|
26
|
+
use = False
|
|
27
|
+
if use:
|
|
28
|
+
yield (record.id, str(record.seq))
|
|
28
29
|
|
|
29
30
|
def stringify(l:List[float], trim=None) -> List[str]:
|
|
30
31
|
out = []
|
|
@@ -50,8 +51,10 @@ def encode(fasta: str, filter:str, batch_size:int, model:str, trim:int, pool:str
|
|
|
50
51
|
"sum" : "sum-pool"
|
|
51
52
|
}
|
|
52
53
|
out = sys.stdout
|
|
54
|
+
should_close = False
|
|
53
55
|
if output is not None:
|
|
54
56
|
out = open(output, "wt")
|
|
57
|
+
should_close = True
|
|
55
58
|
|
|
56
59
|
enc = ProteinEncoder(batch_size=batch_size, model=model)
|
|
57
60
|
enc.to(get_device())
|
|
@@ -68,4 +71,5 @@ def encode(fasta: str, filter:str, batch_size:int, model:str, trim:int, pool:str
|
|
|
68
71
|
for i, emb in enc.encode(fasta_reader(fasta, filter=filter), output=pool_map[pool]):
|
|
69
72
|
out.write( f"{i}\t" + "\t".join(stringify(emb.tolist(), trim)))
|
|
70
73
|
out.write("\n")
|
|
71
|
-
|
|
74
|
+
if should_close:
|
|
75
|
+
out.close()
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Literal, Optional
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from ..factory.layers import ConstraintInfo
|
|
6
|
+
from ..pathway import (
|
|
7
|
+
_normalize_index,
|
|
8
|
+
build_feature_map_indices,
|
|
9
|
+
build_features_to_group_mask,
|
|
10
|
+
build_group_to_group_mask,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
ConstraintOP = Literal["features-to-group", "group-to-features", "group-to-group"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PathwayConstraintInfo(ConstraintInfo):
|
|
17
|
+
"""
|
|
18
|
+
ConstraintInfo for pathway-based masking.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
op: ConstraintOP,
|
|
24
|
+
feature_map: Dict[str, List[str]],
|
|
25
|
+
feature_index: Optional[Any] = None,
|
|
26
|
+
group_index: Optional[Any] = None,
|
|
27
|
+
in_group_scaling: int = 1,
|
|
28
|
+
out_group_scaling: int = 1,
|
|
29
|
+
):
|
|
30
|
+
self.op = op
|
|
31
|
+
self.feature_map = feature_map
|
|
32
|
+
self.feature_index = _normalize_index(feature_index) if feature_index is not None else None
|
|
33
|
+
self.group_index = _normalize_index(group_index) if group_index is not None else None
|
|
34
|
+
self.in_group_scaling = in_group_scaling
|
|
35
|
+
self.out_group_scaling = out_group_scaling
|
|
36
|
+
self.active = True
|
|
37
|
+
|
|
38
|
+
def set_active(self, active: bool) -> None:
|
|
39
|
+
self.active = bool(active)
|
|
40
|
+
|
|
41
|
+
def update_membership(self, feature_map: Dict[str, List[str]]) -> None:
|
|
42
|
+
self.feature_map = feature_map
|
|
43
|
+
|
|
44
|
+
def gen_mask(self, in_features: Optional[int] = None, out_features: Optional[int] = None):
|
|
45
|
+
feature_index = self.feature_index
|
|
46
|
+
group_index = self.group_index
|
|
47
|
+
if feature_index is None or group_index is None:
|
|
48
|
+
feature_index, group_index = build_feature_map_indices(self.feature_map)
|
|
49
|
+
group_count = len(group_index)
|
|
50
|
+
if group_count == 0:
|
|
51
|
+
raise ValueError("Cannot generate pathway mask with zero groups.")
|
|
52
|
+
if in_features is not None and out_features is not None and not self.active:
|
|
53
|
+
return np.ones((out_features, in_features), dtype=np.float32)
|
|
54
|
+
|
|
55
|
+
if self.op == "features-to-group":
|
|
56
|
+
if out_features is None or out_features % group_count != 0:
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"features-to-group expects out_features divisible by group count ({group_count}); got {out_features}."
|
|
59
|
+
)
|
|
60
|
+
group_node_count = out_features // group_count
|
|
61
|
+
return build_features_to_group_mask(
|
|
62
|
+
self.feature_map, feature_index, group_index, group_node_count=group_node_count
|
|
63
|
+
)
|
|
64
|
+
if self.op == "group-to-features":
|
|
65
|
+
if in_features is None or in_features % group_count != 0:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"group-to-features expects in_features divisible by group count ({group_count}); got {in_features}."
|
|
68
|
+
)
|
|
69
|
+
group_node_count = in_features // group_count
|
|
70
|
+
return build_features_to_group_mask(
|
|
71
|
+
self.feature_map, feature_index, group_index, group_node_count=group_node_count, forward=False
|
|
72
|
+
)
|
|
73
|
+
if self.op == "group-to-group":
|
|
74
|
+
if in_features is None or out_features is None:
|
|
75
|
+
raise ValueError("group-to-group requires in_features and out_features.")
|
|
76
|
+
if in_features % group_count != 0 or out_features % group_count != 0:
|
|
77
|
+
raise ValueError(
|
|
78
|
+
f"group-to-group expects both dimensions divisible by group count ({group_count}); "
|
|
79
|
+
f"got in_features={in_features}, out_features={out_features}."
|
|
80
|
+
)
|
|
81
|
+
in_group_nodes = in_features // group_count
|
|
82
|
+
out_group_nodes = out_features // group_count
|
|
83
|
+
return build_group_to_group_mask(group_count, in_group_nodes, out_group_nodes)
|
|
84
|
+
raise ValueError(f"Unknown ConstraintInfo.op '{self.op}'")
|
|
85
|
+
|
|
86
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
87
|
+
return {
|
|
88
|
+
"op": self.op,
|
|
89
|
+
"feature_map": self.feature_map,
|
|
90
|
+
"feature_index": (list(self.feature_index) if self.feature_index is not None else None),
|
|
91
|
+
"group_index": (list(self.group_index) if self.group_index is not None else None),
|
|
92
|
+
"in_group_scaling": self.in_group_scaling,
|
|
93
|
+
"out_group_scaling": self.out_group_scaling,
|
|
94
|
+
"active": self.active,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def from_dict(d: Dict[str, Any]) -> "PathwayConstraintInfo":
|
|
99
|
+
obj = PathwayConstraintInfo(
|
|
100
|
+
op=d["op"],
|
|
101
|
+
feature_map=d["feature_map"],
|
|
102
|
+
feature_index=d.get("feature_index"),
|
|
103
|
+
group_index=d.get("group_index"),
|
|
104
|
+
in_group_scaling=d.get("in_group_scaling", 1),
|
|
105
|
+
out_group_scaling=d.get("out_group_scaling", 1),
|
|
106
|
+
)
|
|
107
|
+
obj.set_active(d.get("active", True))
|
|
108
|
+
return obj
|
|
109
|
+
|
|
110
|
+
__all__ = ["PathwayConstraintInfo"]
|