Skip to content

Commit 283c427

Browse files
committed
Release 1.11.0
1 parent 24d6c51 commit 283c427

File tree

10 files changed

+79
-30
lines changed

10 files changed

+79
-30
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
!/tests/test_*.py
44
/data/
55
build/*
6-
csubst/__pycache__
6+
__pycache__/
77
csubst/*-darwin.so
88
csubst/*.c
99
csubst/.DS_Store

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,15 @@ pip install git+https://github.com/kfuku52/csubst
5555
csubst dataset --name PGK
5656
5757
# Run csubst analyze
58-
csubst analyze --alignment_file alignment.fa --rooted_tree_file tree.nwk --foreground foreground.txt
58+
csubst analyze --alignment_file alignment.fa.gz --rooted_tree_file tree.nwk --foreground foreground.txt
5959
```
6060

6161
## Usage
62-
CSUBST provides four main subcommands:
62+
CSUBST provides five main subcommands:
6363

6464
- `csubst dataset`: generate bundled example datasets (e.g., `PGK`, `PEPC`).
6565
- `csubst analyze`: run convergence analysis and output metrics such as `omegaC`, `dNC`, and `dSC`.
66+
- `csubst inspect`: summarize branch mappings and inspect ancestral states.
6667
- `csubst site`: compute site-wise combinatorial substitutions for selected branch combinations, generate tree + site summary plots, and optionally map sites to protein structures.
6768
- `csubst simulate`: simulate codon sequence evolution under user-defined convergent scenarios.
6869

@@ -81,13 +82,13 @@ csubst dataset --name PGK
8182

8283
# 2) Run convergence analysis
8384
csubst analyze \
84-
--alignment_file alignment.fa \
85+
--alignment_file alignment.fa.gz \
8586
--rooted_tree_file tree.nwk \
8687
--foreground foreground.txt
8788

8889
# 3) Inspect site-wise convergence for a branch pair (example)
8990
csubst site \
90-
--alignment_file alignment.fa \
91+
--alignment_file alignment.fa.gz \
9192
--rooted_tree_file tree.nwk \
9293
--branch_id 23,51
9394
```

csubst/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '1.10.2'
1+
__version__ = '1.11.0'

csubst/csubst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ def _resolve_output_namespace_defaults(argv):
6565
'outdir': 'csubst_inspect',
6666
'output_prefix': 'csubst',
6767
}
68+
if token == 'site':
69+
return {
70+
'outdir': 'csubst_site',
71+
'output_prefix': 'csubst',
72+
}
6873
break
6974
return {
7075
'outdir': '.',
@@ -143,6 +148,8 @@ def command_simulate(args):
143148
def command_site(args):
144149
print('csubst site start:', datetime.datetime.now(datetime.timezone.utc), flush=True)
145150
start = time.time()
151+
if str(getattr(args, 'log_file', '')).strip() == '':
152+
args.log_file = runtime.default_site_log_path(base_dir=os.getcwd(), create_dir=False)
146153
g = get_global_parameters_or_exit(args)
147154
from csubst.main_site import main_site
148155
main_site(g)

csubst/main_analyze.py

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from csubst import omega
1111
from csubst import param
1212
from csubst import parser_misc
13-
from csubst import sequence
1413
from csubst import substitution
1514
from csubst import table
1615
from csubst import ete
@@ -598,29 +597,6 @@ def main_analyze(g):
598597
prepare_state=False,
599598
)
600599
g = parser_misc.prep_state(g, apply_site_filtering=False)
601-
loaded_branch_ids = g.get('state_loaded_branch_ids', None)
602-
if loaded_branch_ids is not None:
603-
txt = 'Selective state loading active: writing alignments only for loaded nodes ({:,}).'
604-
print(txt.format(loaded_branch_ids.shape[0]), flush=True)
605-
sequence.write_alignment(
606-
runtime.output_path(g, 'alignment_codon.fa'),
607-
mode='codon',
608-
g=g,
609-
branch_ids=loaded_branch_ids,
610-
)
611-
sequence.write_alignment(
612-
runtime.output_path(g, 'alignment_aa.fa'),
613-
mode='aa',
614-
g=g,
615-
branch_ids=loaded_branch_ids,
616-
)
617-
if str(g.get('nonsyn_recode', 'no')).strip().lower() == '3di20':
618-
sequence.write_alignment(
619-
runtime.output_path(g, 'alignment_3di.fa'),
620-
mode='nsy',
621-
g=g,
622-
branch_ids=loaded_branch_ids,
623-
)
624600
g = parser_misc.apply_site_filters(g)
625601
g = combination.get_dep_ids(g)
626602
ON_tensor = substitution.get_substitution_tensor(state_tensor=g['state_nsy'], mode='asis', g=g, mmap_attr='N')

csubst/main_site.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3231,6 +3231,30 @@ def _build_site_outdir(mode, branch_txt, lineage_input_branch_txt=None, mode_exp
32313231
return './csubst_site.mode' + mode + '.branch_id' + branch_txt
32323232

32333233

3234+
def _maybe_relocate_site_log_file(g):
3235+
site_jobs = g.get('site_jobs', [])
3236+
if len(site_jobs) != 1:
3237+
return g
3238+
log_file = str(g.get('log_file', '')).strip()
3239+
if log_file == '':
3240+
return g
3241+
current_log_path = os.path.abspath(log_file)
3242+
default_log_path = runtime.default_site_log_path(base_dir=os.getcwd(), create_dir=False)
3243+
if current_log_path != default_log_path:
3244+
return g
3245+
target_dir = os.path.abspath(site_jobs[0]['site_outdir'])
3246+
os.makedirs(target_dir, exist_ok=True)
3247+
target_log_path = os.path.join(target_dir, os.path.basename(default_log_path))
3248+
if current_log_path != target_log_path:
3249+
if os.path.exists(current_log_path):
3250+
os.replace(current_log_path, target_log_path)
3251+
parent_dir = os.path.dirname(current_log_path)
3252+
if (parent_dir != '') and os.path.isdir(parent_dir) and (len(os.listdir(parent_dir)) == 0):
3253+
os.rmdir(parent_dir)
3254+
g['log_file'] = target_log_path
3255+
return g
3256+
3257+
32343258
def resolve_site_jobs(g):
32353259
raw_mode = str(g.get('mode', 'intersection')).strip()
32363260
mode, mode_expression, set_stat_type = _parse_mode_and_expression(raw_mode)
@@ -3335,6 +3359,7 @@ def main_site(g):
33353359
OS_tensor = substitution.get_substitution_tensor(state_tensor=g['state_cdn'], mode='syn', g=g, mmap_attr='S')
33363360
OS_tensor = substitution.apply_min_sub_pp(g, OS_tensor)
33373361
g = resolve_site_jobs(g)
3362+
g = _maybe_relocate_site_log_file(g)
33383363
for site_job in g['site_jobs']:
33393364
branch_ids = _normalize_branch_ids(site_job['branch_ids'])
33403365
g['single_branch_mode'] = site_job['single_branch_mode']

csubst/runtime.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
_DEFAULT_OUTPUT_DIR = "."
1313
_DEFAULT_OUTPUT_PREFIX = "csubst"
1414
_DEFAULT_IQTREE_OUTDIR = "csubst_iqtree"
15+
_DEFAULT_SITE_LOG_DIR = "csubst_site"
1516
_MISSING = object()
1617

1718

@@ -144,6 +145,15 @@ def ensure_iqtree_layout(g, create_dir=False):
144145
return g
145146

146147

148+
def default_site_log_path(base_dir=None, create_dir=False):
149+
if base_dir is None:
150+
base_dir = os.getcwd()
151+
site_log_dir = os.path.abspath(os.path.join(str(base_dir), _DEFAULT_SITE_LOG_DIR))
152+
if create_dir:
153+
os.makedirs(site_log_dir, exist_ok=True)
154+
return os.path.join(site_log_dir, _DEFAULT_OUTPUT_PREFIX + ".log")
155+
156+
147157
def _strip_gzip_suffix(path):
148158
path_txt = str(path).strip()
149159
if path_txt.lower().endswith(".gz"):

tests/test_cli_logging.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ def test_simulate_help_uses_simulate_default_log_name(tmp_path):
6262
assert not (tmp_path / "csubst.log").exists()
6363

6464

65+
def test_site_help_uses_site_default_log_name(tmp_path):
66+
result = _run_csubst(["site", "-h"], cwd=tmp_path)
67+
assert result.returncode == 0
68+
log_file = tmp_path / "csubst_site" / "csubst.log"
69+
assert log_file.exists()
70+
assert not (tmp_path / "csubst.log").exists()
71+
72+
6573
def test_subcommand_output_namespace_defaults_are_command_specific():
6674
repo_root = Path(__file__).resolve().parents[1]
6775
script_path = repo_root / "csubst" / "csubst"

tests/test_cli_validation.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ def _resolve_log_path(repo_root, args):
1919
return repo_root / "csubst_analyze" / "csubst.log"
2020
if args[0] == "inspect":
2121
return repo_root / "csubst_inspect" / "csubst.log"
22+
if args[0] == "site":
23+
return repo_root / "csubst_site" / "csubst.log"
2224
return repo_root / "csubst.log"
2325

2426

tests/test_main_site_and_substitution.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from pathlib import Path
66

77
from csubst import main_site
8+
from csubst import runtime
89
from csubst import substitution
910
from csubst import substitution_sparse
1011
from csubst import tree
@@ -587,6 +588,25 @@ def test_resolve_site_jobs_rejects_duplicate_branch_ids(tiny_tree):
587588
main_site.resolve_site_jobs(g)
588589

589590

591+
def test_maybe_relocate_site_log_file_moves_default_log_into_single_job_outdir(tmp_path, monkeypatch):
592+
default_log = Path(runtime.default_site_log_path(base_dir=tmp_path, create_dir=True))
593+
default_log.write_text("hello\n", encoding="utf-8")
594+
site_outdir = tmp_path / "csubst_site.branch_id1,2"
595+
g = {
596+
"log_file": str(default_log),
597+
"site_jobs": [{"site_outdir": str(site_outdir)}],
598+
}
599+
600+
monkeypatch.chdir(tmp_path)
601+
out = main_site._maybe_relocate_site_log_file(g)
602+
603+
relocated_log = site_outdir / "csubst.log"
604+
assert out["log_file"] == str(relocated_log.resolve())
605+
assert relocated_log.exists()
606+
assert relocated_log.read_text(encoding="utf-8") == "hello\n"
607+
assert not default_log.exists()
608+
609+
590610
def test_normalize_branch_ids_rejects_non_integer_like_values():
591611
with pytest.raises(ValueError, match="integer-like"):
592612
main_site._normalize_branch_ids(np.array([1.5]))

0 commit comments

Comments
 (0)