Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
28f6cb1
Create a reusable database view into gauge metabolite labelling
Sep 18, 2025
f0e8de0
Draft the database query for the "tracer labelling" filter step
Sep 18, 2025
307c7ee
Display of tracer labeling is now working
Sep 22, 2025
4c5cb38
Basics of the new page are now working completely
Sep 22, 2025
f7ae088
Fix a typing issue
Sep 23, 2025
66c6362
Make some extra room in the UI (remove some unused space)
Sep 23, 2025
df3d4cd
Fix current tests, types, lints
Sep 23, 2025
d988e62
Add tests for the gauge labelling filter step.
Sep 25, 2025
b4d3ad5
Add metabolite sizes to the test dataset
Sep 26, 2025
678bd8e
Add new UI tests for the gaugelabelling filter step
Sep 26, 2025
d2233cd
Undo commented line that should not have been commented
Sep 26, 2025
941875d
Remove commented queries
Sep 26, 2025
6e66a20
Tracer labeling tests are now passing
Oct 3, 2025
d3f27ce
Passing initial version of pathway tests
Oct 6, 2025
0e01122
Ensure that views display correct data
Oct 8, 2025
c661d18
Locally pass tests and linting, formatting
Oct 24, 2025
21f47b3
Correctly set the gauge_label_positions
Oct 24, 2025
7853562
Fix some bugs in counting for different filter configurations
Oct 27, 2025
47da6f0
Make sure the "metabolites" step displays correct filtered results
Oct 27, 2025
11a84f0
Make the stepper UI work properly with the new page
Oct 27, 2025
d26069b
Type checking errors
Oct 27, 2025
5d12969
Fix CLI build
Oct 27, 2025
7e64201
Remove duplicated URL generator
Oct 27, 2025
e9ec933
Correct a misunderstanding about atom labels and atom positions
Oct 29, 2025
7a021a7
Formatting
Oct 29, 2025
ef58fd6
Small typing fix from pyright
Oct 29, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions cli/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,17 @@ pub async fn delete_server_data(
}
}

pub async fn refresh_views(http_client: &Client, base_url: &str, dry_run: bool) -> Result<()> {
let url = format!("{}/api/v1/admin/refresh-views", base_url);
if dry_run {
println!("Would refresh views.");
Ok(())
} else {
let response = http_client.post(url).send().await;
server_response(response).await.map(|_| println!("OK"))
}
}

pub async fn find_metabolite(http_client: &Client, base_url: &str, query: &str) -> Result<()> {
let url = format!("{}/api/v1/metabolites/?limit=1&query={}", base_url, query);
let res = http_client.get(url).send().await;
Expand Down
12 changes: 11 additions & 1 deletion cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use clap::{Args, Parser, Subcommand};
use reqwest::{redirect::Policy, ClientBuilder};

use crate::api::{
delete_server_data, find_metabolite, find_path, get_server_state, get_user_info,
delete_server_data, find_metabolite, find_path, get_server_state, get_user_info, refresh_views,
set_server_state, submit_all, submit_biocyc, submit_labellings,
};
use crate::data_sources::DataSources;
Expand Down Expand Up @@ -133,6 +133,8 @@ enum AdminCommand {
#[arg(value_parser = clap::builder::PossibleValuesParser::new(["all", "biocyc", "labellings"]))]
what: String,
},

RefreshViews,
}

#[derive(Debug, Args)]
Expand Down Expand Up @@ -228,6 +230,14 @@ async fn main() -> Result<()> {
)
.await
}
AdminCommand::RefreshViews => {
refresh_views(
&client,
&args.global_opts.base_url,
args.global_opts.dry_run,
)
.await
}
},
Command::Query(QueryArgs {
command: QueryCommand::Metabolite { query },
Expand Down
7 changes: 6 additions & 1 deletion server/alembic.ini
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ sqlalchemy.url = driver://user:pass@localhost/dbname

# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
keys = root,sqlalchemy,alembic,alembic_utils

[handlers]
keys = console
Expand All @@ -99,6 +99,11 @@ level = INFO
handlers =
qualname = alembic

[logger_alembic_utils]
level = INFO
handlers =
qualname = alembic_utils

[handler_console]
class = StreamHandler
args = (sys.stderr,)
Expand Down
47 changes: 36 additions & 11 deletions server/dries/TTFD.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@

# include_labeled_elements = []
#include_labeled_elements = ['x_x____']
exclude_labeled_elements = []
exclude_labeled_elements = None
"""
exclude_labeled_elements = ['x__________________________________________________',
'___________x_______________________________________',
Expand All @@ -89,6 +89,11 @@

pathway_id2name = {'PWY66-398': 'TCA cycle'}

def trace(idx: int, msg: str) -> None:
# if idx in (19,):
# print(f"{idx}: {msg}")
pass


def do_the_thing(data,
tracer_metabolite_id,
Expand All @@ -98,14 +103,13 @@ def do_the_thing(data,
num_pathways_filter=None,
include_labeled_elements=None,
include_labeled_elements_origins=None):
if include_labeled_elements is None:
include_labeled_elements = []
# print(f"{gauge_num_label_filter=}\n{num_pathways_filter=}\n{include_labeled_elements=}\n{include_labeled_elements_origins=}", flush=True)
#i_f = open("data/ALPHA-GLUCOSE__L-LACTATE__30.newformat", 'r')
tracer_labeled_atoms = [str(tla) for tla in tracer_labeled_atoms]
path_list = []

results = []
for (idx, datum) in enumerate(data):
for (idx, datum) in enumerate(data, start=1):
reaction_info = datum.reaction_info
compressed_reaction_path = reaction_info[0]
reduced_reaction_list = reaction_info[1]
Expand Down Expand Up @@ -138,6 +142,7 @@ def do_the_thing(data,
emi_list = emi.split('_')
emi_list = [l if l in tracer_labeled_atoms else '' for l in emi_list]
label_positions = ['x' if l != '' else '' for l in emi_list]
trace(idx, f"{emi} -> {label_positions}")
label_positions_txt = '_'.join(label_positions)
emi_filtered = '_'.join(emi_list)
if label_positions_txt not in labeled_elements_incorporations:
Expand All @@ -160,27 +165,47 @@ def do_the_thing(data,

# the groupings functionality
if num_pathways_filter != None:
if min_num_pathways not in num_pathways_filter: continue
if min_num_pathways not in num_pathways_filter:
trace(idx, f"{min_num_pathways} not in {num_pathways_filter} = skipped")
continue
else:
trace(idx, f"{min_num_pathways} in {num_pathways_filter} = passed")

if gauge_num_label_filter != None:
if not (set(gauge_num_label_filter) & set(num_labels)): continue
if not (set(gauge_num_label_filter) & set(num_labels)):
trace(idx, f"{set(gauge_num_label_filter)} & {set(num_labels)} = skipped")
continue
else:
trace(idx, f"{set(gauge_num_label_filter)} & {set(num_labels)} = passed")
# else:
# print("*", end="", flush=True)

if include_labeled_elements:
if include_labeled_elements is not None:
if not (set(include_labeled_elements) & set(labeled_elements_incorporations)):
trace(idx, f"{include_labeled_elements} & {labeled_elements_incorporations=} = skipped")
continue
else:
trace(idx, f"{include_labeled_elements} & {labeled_elements_incorporations=} = passed")
else:
trace(idx, "include_labeled_elements is not defined")

if include_labeled_elements_origins:
if include_labeled_elements_origins is not None:
#print(set(include_labeled_elements_origins))
#print(set([emi for emi, _ in end_metabolite_incorporations]))
#print("------------------")
emi_set = set([emi for emi, _ in end_metabolite_incorporations])
#print(f"{idx} ({emi_set=}) {include_labeled_elements_origins[0]}: {set(include_labeled_elements_origins) & emi_set}")
if not (set(include_labeled_elements_origins) & set([emi for emi, _ in end_metabolite_incorporations])):
trace(idx, f"{include_labeled_elements_origins} & {emi_set=} = skipped")
continue
else:
trace(idx, f"{include_labeled_elements_origins} & {emi_set=} = passed")

if exclude_labeled_elements:
if exclude_labeled_elements is not None:
if not (set(labeled_elements_incorporations) - set(exclude_labeled_elements)):
continue

if tracer_labeling_filter != None:
if tracer_labeling_filter is not None:
if tracer_labeling_filter not in tracer_labeling_incorporations: continue

"""
Expand Down Expand Up @@ -314,4 +339,4 @@ def do_the_thing(data,


if __name__ == '__main__':
do_the_thing("L-LACTATE")
do_the_thing("L-LACTATE")
139 changes: 121 additions & 18 deletions server/dries/counts.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from typing import List, Tuple
import sys
from collections.abc import Container
from itertools import chain, combinations, product

from dries.preload import Datum
from dries.TTFD import do_the_thing


def counts_per_label(data: List[Datum], tracer: str, gauge: str, start: List[int]):
def counts_per_label(data: list[Datum], tracer: str, gauge: str, start: list[int]):
counts = {}
for label in range(1, 50):
results = do_the_thing(data, tracer, gauge, start, [label])
Expand All @@ -13,50 +15,83 @@ def counts_per_label(data: List[Datum], tracer: str, gauge: str, start: List[int

return counts

def mklabeled(gauge: str, labeled_elements: list[int]) -> str:
def metabolite_size(metabolite: str) -> int:
size = {
"ALPHA-GLUCOSE": 12,
"L-LACTATE": 6,
"GLN": 10,
"GLT": 10,
"UMP": 21,
"RIBULOSE-5P": 14,
"PRO": 8,
}[gauge]
return "_".join("x" if p in labeled_elements else "" for p in range(size))
"PYRUVATE": 6,
}
return size.get(metabolite, 0)

def metabolite_carbons(metabolite: str) -> int:
size = {
"ALPHA-GLUCOSE": 6,
"GLN": 5,
"GLT": 5,
"L-LACTATE": 3,
"PRO": 5,
"PYRUVATE": 3,
"RIBULOSE-5P": 5,
"UMP": 9
}
return size.get(metabolite, 0)

def mklabeled(metabolite: str, labeled_positions: Container[int]) -> str:
return "_".join(
"x" if p in labeled_positions else ""
for p in range(metabolite_size(metabolite))
)

def counts_per_label_and_pathways(
data: List[Datum],
data: list[Datum],
tracer: str,
gauge: str,
start: List[int],
label_count: int,
labeled_elements: list[int] | None,
):
start: list[int],
label_count: int | None,
gauge_labeled_positions: list[int] | None,
) -> dict[int, int]:
counts = {}
labeled = (
None
if labeled_elements is None
else [mklabeled(gauge, labeled_elements)]
)
labeled = None
lc = None
if gauge_labeled_positions:
required = set(gauge_labeled_positions)
label_positions = metabolite_carbons(gauge) + 1
labeled = set()

for patterns in chain(combinations(range(label_positions), r=label_count) for label_count in range(1, label_positions + 1)):
for pattern in patterns:
if not set(pattern).issuperset(required):
continue
labeled.add(mklabeled(gauge, list(pattern)))
# assert len(labeled) != 0, f"{label_positions=}, {required=}"
else:
lc = [label_count]

for pathway_count in range(1, 9):
results = do_the_thing(
data,
tracer,
gauge,
start, # [1,2] (`tracer_labeled_atoms`)
[label_count], # [2] (`gauge_num_label_filter`)
lc, # [label_count], # [2] (`gauge_num_label_filter`)
[pathway_count], # [1] (`num_pathways_filter`)
include_labeled_elements=labeled, # ["x_x____"] (`include_labeled_elements`)
)
# print(pathway_count, results)
if len(results) > 0:
# print(pathway_count, results)
counts[pathway_count] = len(results)

return counts


def counts_per_position(
data: List[Datum], tracer: str, gauge: str, start: List[int], positions: List[int]
data: list[Datum], tracer: str, gauge: str, start: list[int], positions: list[int]
):
labeling_mask = "_".join(["x" if p in positions else "" for p in range(6)])
counts = {}
Expand All @@ -72,7 +107,7 @@ def counts_per_position(


def counts_per_labeled_position(
data: List[Datum], tracer, gauge, start: List[Tuple[int, int]]
data: list[Datum], tracer, gauge, start: list[tuple[int, int]]
):
labels, positions = zip(*start)
labeling_mask = "_".join(
Expand All @@ -88,3 +123,71 @@ def counts_per_labeled_position(
counts[pathway_count] = len(results)

return counts


def counts_per_destination_labeling(
data: list[Datum],
tracer: str,
gauge: str,
label_count: int,
) -> dict[str, int]:
counts = {}
gauge_positions = range(metabolite_size(gauge))
start = list(gauge_positions)
for gauge_label_positions in combinations(gauge_positions, label_count):
gauge_labels = mklabeled(gauge, gauge_label_positions)
results = do_the_thing(
data,
tracer,
gauge,
start,
[label_count],
num_pathways_filter=None,
include_labeled_elements=[gauge_labels]
)
if len(results) > 0:
counts[gauge_labels] = len(results)
return counts

def _all_origin_labelings(tracer: str, gauge: str, label_count: int) -> list[str]:
"size comes from the gauge, labels come from the tracer"
options = []
possible_labels = list(range(metabolite_carbons(tracer) + 2))
possible_positions = list(range(metabolite_carbons(gauge) + 1))
#print(metabolite, start)
check = 0
for positions in combinations(possible_positions, label_count):
#for labelling in permutations(possible_labels, len(positions)):
for labelling in product(possible_labels, repeat=len(positions)):
ls = {p: l for (p, l) in zip(positions, labelling, strict=True)}
options.append("_".join([str(ls.get(l, "")) for l in range(metabolite_size(gauge))]))
#print(metabolite, options[-1])
check += 1
if check > 100_000_000:
raise ValueError("Too large query")

assert check < 100_000_000, f"CHECK FAILED: {check} from label_count = {label_count}"
# options.append("_".join(str(label) if i == pos else "" for i, (pos, label) in enumerate(zip(positions, labelling, strict=True))))
return options

def counts_per_origin_labeling(
data: list[Datum],
tracer: str,
gauge: str,
gauge_label_positions: list[int],
):
gauge_labels = [mklabeled(gauge, gauge_label_positions)]
start = list(range(metabolite_size(tracer)))
origin_labels = _all_origin_labelings(tracer, gauge, len(gauge_label_positions))
return do_the_thing(
data,
tracer,
gauge,
start, # [1,2] (`tracer_labeled_atoms`)
gauge_num_label_filter=None, # [2] (`gauge_num_label_filter`)
num_pathways_filter=None, # [1] (`num_pathways_filter`)
include_labeled_elements=gauge_labels, # ["x_x____"] (`include_labeled_elements`)
include_labeled_elements_origins=origin_labels
)


Loading