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.
Files changed (85) hide show
  1. {embkit-0.3 → embkit-0.4}/PKG-INFO +1 -1
  2. {embkit-0.3 → embkit-0.4}/pyproject.toml +1 -1
  3. {embkit-0.3 → embkit-0.4}/setup.py +1 -1
  4. {embkit-0.3 → embkit-0.4}/src/embkit/c_bio/api.py +4 -1
  5. {embkit-0.3 → embkit-0.4}/src/embkit/commands/align.py +3 -3
  6. {embkit-0.3 → embkit-0.4}/src/embkit/commands/matrix.py +21 -3
  7. embkit-0.4/src/embkit/commands/model.py +401 -0
  8. {embkit-0.3 → embkit-0.4}/src/embkit/commands/protein.py +12 -8
  9. embkit-0.4/src/embkit/constraints/__init__.py +7 -0
  10. embkit-0.4/src/embkit/constraints/pathway_constraint.py +110 -0
  11. {embkit-0.3 → embkit-0.4}/src/embkit/encoding/__init__.py +49 -3
  12. embkit-0.4/src/embkit/encoding/genome.py +74 -0
  13. {embkit-0.3 → embkit-0.4}/src/embkit/factory/__init__.py +1 -1
  14. embkit-0.4/src/embkit/factory/core.py +100 -0
  15. {embkit-0.3 → embkit-0.4}/src/embkit/factory/layers.py +33 -108
  16. {embkit-0.3 → embkit-0.4}/src/embkit/files/h5.py +17 -26
  17. {embkit-0.3 → embkit-0.4}/src/embkit/files/read_csv.py +28 -9
  18. {embkit-0.3 → embkit-0.4}/src/embkit/losses/vae_loss.py +8 -3
  19. embkit-0.4/src/embkit/models/ffnn.py +127 -0
  20. embkit-0.4/src/embkit/models/vae/base_vae.py +240 -0
  21. {embkit-0.3 → embkit-0.4}/src/embkit/models/vae/decoder.py +17 -4
  22. {embkit-0.3 → embkit-0.4}/src/embkit/models/vae/encoder.py +35 -20
  23. embkit-0.4/src/embkit/models/vae/net_vae.py +292 -0
  24. {embkit-0.3 → embkit-0.4}/src/embkit/models/vae/rna_vae.py +63 -9
  25. {embkit-0.3 → embkit-0.4}/src/embkit/models/vae/vae.py +13 -4
  26. {embkit-0.3 → embkit-0.4}/src/embkit/modules/masked_linear.py +17 -2
  27. {embkit-0.3 → embkit-0.4}/src/embkit/modules/tsp.py +10 -6
  28. {embkit-0.3 → embkit-0.4}/src/embkit/optimize/__init__.py +60 -67
  29. embkit-0.4/src/embkit/pathway.py +227 -0
  30. {embkit-0.3 → embkit-0.4}/src/embkit/preprocessing/normalize.py +5 -5
  31. {embkit-0.3 → embkit-0.4}/src/embkit/resources/resource.py +19 -6
  32. {embkit-0.3 → embkit-0.4}/src/embkit/utilities/pca.py +7 -4
  33. {embkit-0.3 → embkit-0.4}/src/embkit.egg-info/PKG-INFO +1 -1
  34. {embkit-0.3 → embkit-0.4}/src/embkit.egg-info/SOURCES.txt +2 -1
  35. embkit-0.3/src/embkit/commands/model.py +0 -233
  36. embkit-0.3/src/embkit/constraints/__init__.py +0 -1
  37. embkit-0.3/src/embkit/constraints/network_constraint.py +0 -72
  38. embkit-0.3/src/embkit/factory/core.py +0 -46
  39. embkit-0.3/src/embkit/models/ffnn.py +0 -50
  40. embkit-0.3/src/embkit/models/vae/base_vae.py +0 -112
  41. embkit-0.3/src/embkit/models/vae/net_vae.py +0 -115
  42. embkit-0.3/src/embkit/pathway.py +0 -227
  43. {embkit-0.3 → embkit-0.4}/README.md +0 -0
  44. {embkit-0.3 → embkit-0.4}/setup.cfg +0 -0
  45. {embkit-0.3 → embkit-0.4}/src/embkit/__init__.py +0 -0
  46. {embkit-0.3 → embkit-0.4}/src/embkit/__main__.py +0 -0
  47. {embkit-0.3 → embkit-0.4}/src/embkit/align.py +0 -0
  48. {embkit-0.3 → embkit-0.4}/src/embkit/bmeg.py +0 -0
  49. {embkit-0.3 → embkit-0.4}/src/embkit/c_bio/__init__.py +0 -0
  50. {embkit-0.3 → embkit-0.4}/src/embkit/commands/__init__.py +0 -0
  51. {embkit-0.3 → embkit-0.4}/src/embkit/commands/cbio.py +0 -0
  52. {embkit-0.3 → embkit-0.4}/src/embkit/commands/resources.py +0 -0
  53. {embkit-0.3 → embkit-0.4}/src/embkit/datasets/__init__.py +0 -0
  54. {embkit-0.3 → embkit-0.4}/src/embkit/encoding/protein.py +0 -0
  55. {embkit-0.3 → embkit-0.4}/src/embkit/estimator/__init__.py +0 -0
  56. {embkit-0.3 → embkit-0.4}/src/embkit/estimator/vae_estimator.py +0 -0
  57. {embkit-0.3 → embkit-0.4}/src/embkit/factory/mapping.py +0 -0
  58. {embkit-0.3 → embkit-0.4}/src/embkit/factory/registry.py +0 -0
  59. {embkit-0.3 → embkit-0.4}/src/embkit/files/__init__.py +0 -0
  60. {embkit-0.3 → embkit-0.4}/src/embkit/files/json.py +0 -0
  61. {embkit-0.3 → embkit-0.4}/src/embkit/files/loaders.py +0 -0
  62. {embkit-0.3 → embkit-0.4}/src/embkit/losses/__init__.py +0 -0
  63. {embkit-0.3 → embkit-0.4}/src/embkit/metrics/__init__.py +0 -0
  64. {embkit-0.3 → embkit-0.4}/src/embkit/metrics/distance/__init__.py +0 -0
  65. {embkit-0.3 → embkit-0.4}/src/embkit/metrics/distance/metrics.py +0 -0
  66. {embkit-0.3 → embkit-0.4}/src/embkit/models/__init__.py +0 -0
  67. {embkit-0.3 → embkit-0.4}/src/embkit/models/vae/__init__.py +0 -0
  68. {embkit-0.3 → embkit-0.4}/src/embkit/modules/__init__.py +0 -0
  69. {embkit-0.3 → embkit-0.4}/src/embkit/modules/pairwise_comparison_layer.py +0 -0
  70. {embkit-0.3 → embkit-0.4}/src/embkit/preprocessing/__init__.py +0 -0
  71. {embkit-0.3 → embkit-0.4}/src/embkit/preprocessing/dataset.py +0 -0
  72. {embkit-0.3 → embkit-0.4}/src/embkit/resources/__init__.py +0 -0
  73. {embkit-0.3 → embkit-0.4}/src/embkit/resources/c_bio_portal.py +0 -0
  74. {embkit-0.3 → embkit-0.4}/src/embkit/resources/gtex.py +0 -0
  75. {embkit-0.3 → embkit-0.4}/src/embkit/resources/hugo.py +0 -0
  76. {embkit-0.3 → embkit-0.4}/src/embkit/resources/sif.py +0 -0
  77. {embkit-0.3 → embkit-0.4}/src/embkit/resources/util.py +0 -0
  78. {embkit-0.3 → embkit-0.4}/src/embkit/utilities/__init__.py +0 -0
  79. {embkit-0.3 → embkit-0.4}/src/embkit/utilities/kmeans.py +0 -0
  80. {embkit-0.3 → embkit-0.4}/src/embkit/version.py +0 -0
  81. {embkit-0.3 → embkit-0.4}/src/embkit.egg-info/dependency_links.txt +0 -0
  82. {embkit-0.3 → embkit-0.4}/src/embkit.egg-info/entry_points.txt +0 -0
  83. {embkit-0.3 → embkit-0.4}/src/embkit.egg-info/requires.txt +0 -0
  84. {embkit-0.3 → embkit-0.4}/src/embkit.egg-info/top_level.txt +0 -0
  85. {embkit-0.3 → embkit-0.4}/tests/test_main.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: embkit
3
- Version: 0.3
3
+ Version: 0.4
4
4
  Summary: Embedding Kit for embedding models
5
5
  Author-email: Kyle Ellrott <ellrott@ohsu.edu>, Raphael Kirchgaessner <kirchgae@ohsu.edu>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "embkit"
7
- version = "0.3"
7
+ version = "0.4"
8
8
  description = "Embedding Kit for embedding models"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='embedding-kit',
5
- version='0.1',
5
+ version='0.4',
6
6
  packages=find_packages(),
7
7
  install_requires=[
8
8
  # List your dependencies here!
@@ -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
- print(f"Error fetching studies: {e}")
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
- out = matrix_spearman_alignment_linear(m1, m2, cutoff)
29
- for k, v in out.items():
30
- print(f"{k}\t{v[0]}\t{v[1]}")
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 features is not None:
25
- df = df[features]
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
- for record in SeqIO.parse(path, "fasta"):
22
- use = True
23
- if filter is not None:
24
- if not re.match(filter, record.id):
25
- use = False
26
- if use:
27
- yield (record.id, str(record.seq))
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
- out.close()
74
+ if should_close:
75
+ out.close()
@@ -0,0 +1,7 @@
1
+ """
2
+ Constraint entry points.
3
+ """
4
+
5
+ from .pathway_constraint import PathwayConstraintInfo
6
+
7
+ __all__ = ["PathwayConstraintInfo"]
@@ -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"]