Skip to content

Commit 5fb024c

Browse files
Tutorial (#3)
* expand python versions * release: 0.2.0 * tutorial
1 parent 3072992 commit 5fb024c

8 files changed

Lines changed: 146 additions & 45 deletions

File tree

README.md

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -56,43 +56,34 @@ phi login
5656

5757
## Quick start
5858

59-
### Single-sequence / single-structure jobs
59+
### Try the tutorial
6060

61-
```bash
62-
# Structure prediction (ESMFold)
63-
phi folding --fasta sequences.fasta
64-
65-
# Complex structure prediction (AlphaFold2 multimer)
66-
phi complex_folding --fasta binder_target.fasta
61+
The fastest way to get started — downloads five example PD-L1 binder structures
62+
and walks you through the full pipeline:
6763

68-
# Sequence design via inverse folding (ProteinMPNN)
69-
phi inverse_folding --pdb design.pdb --num-sequences 20
64+
```bash
65+
phi tutorial
7066
```
7167

72-
### Batch scoring workflow
68+
This fetches the example files, prints step-by-step instructions, and leaves
69+
you ready to run `phi filter`.
70+
71+
### Scoring your own structures
7372

7473
```bash
75-
# 1. Upload a directory of PDB/CIF files
74+
# 1. Upload PDB/CIF files
7675
phi upload ./designs/
7776

78-
# Output:
79-
# dataset_id d7c3a1b2-...
80-
# Dashboard: https://design.dynotx.com/dashboard/datasets/d7c3a1b2-...
81-
# Run a job against this dataset:
82-
# phi folding --dataset-id d7c3a1b2-...
83-
# phi complex_folding --dataset-id d7c3a1b2-...
84-
# phi inverse_folding --dataset-id d7c3a1b2-...
85-
# phi filter --dataset-id d7c3a1b2-... --preset default --wait
86-
87-
# 2. Run the full filter pipeline (inverse folding → folding → complex folding → score)
88-
phi filter --dataset-id d7c3a1b2-... --preset default --wait
77+
# 2. Run the full filter pipeline
78+
phi filter --preset default --wait
8979

90-
# 3. Download results (structures, scores CSV, raw score JSONs)
80+
# 3. View scores and download results
81+
phi scores
9182
phi download --out ./results/
9283
```
9384

94-
After each command, `phi` prints the active dataset and job IDs and a link to
95-
the dashboard:
85+
After each command, `phi` prints the active dataset and a link to the
86+
dashboard:
9687

9788
```
9889
Active: dataset [d7c3a1b2-...] · job [cb4553f5-...]
@@ -105,6 +96,7 @@ Dashboard: https://design.dynotx.com/dashboard/datasets/d7c3a1b2-...
10596

10697
| Command | Alias | Description |
10798
|---|---|---|
99+
| `phi tutorial` || Download example structures and print a step-by-step walkthrough |
108100
| `phi login` || Verify API key and print identity |
109101
| `phi upload` || Upload PDB/CIF files or a directory |
110102
| `phi fetch` || Download a structure from RCSB PDB or AlphaFold DB, crop, and optionally upload |

src/phi/cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616
)
1717
from phi.commands.research import cmd_notes, cmd_research
1818
from phi.commands.structure import cmd_fetch
19+
from phi.commands.tutorial import cmd_tutorial
1920
from phi.config import _load_state
2021
from phi.display import _C_BLUE, _die, console
2122
from phi.parser import build_parser
2223
from phi.types import PhiApiError
2324

2425
COMMANDS = {
2526
"login": cmd_login,
27+
"tutorial": cmd_tutorial,
2628
"upload": cmd_upload,
2729
"ingest-session": cmd_ingest_session,
2830
"datasets": cmd_datasets,

src/phi/commands/models.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
1+
from __future__ import annotations
2+
13
import argparse
4+
import os
25
from pathlib import Path
36

47
from phi.api import _require_key, _submit
58
from phi.display import _die, _print_status, _print_submission, console
69
from phi.download import _download_job, _read_fasta
710
from phi.polling import _poll
811

12+
_DESIGN_ENABLED = os.environ.get("DYNO_ENABLE_DESIGN", "").lower() in ("1", "true", "yes")
13+
14+
15+
def _require_design_flag() -> None:
16+
if not _DESIGN_ENABLED:
17+
_die("This command is not yet available.")
18+
919

1020
def _run_model_job(job_type: str, params: dict, args: argparse.Namespace) -> None:
1121
from phi.config import POLL_INTERVAL as _INTERVAL
@@ -92,6 +102,7 @@ def cmd_boltz(args: argparse.Namespace) -> None:
92102

93103

94104
def cmd_rfdiffusion3(args: argparse.Namespace) -> None:
105+
_require_design_flag()
95106
params: dict = {
96107
"num_designs": args.num_designs,
97108
"inference_steps": args.steps,
@@ -129,6 +140,7 @@ def cmd_rfdiffusion3(args: argparse.Namespace) -> None:
129140

130141

131142
def cmd_boltzgen(args: argparse.Namespace) -> None:
143+
_require_design_flag()
132144
params: dict = {
133145
"protocol": args.protocol,
134146
"num_designs": args.num_designs,

src/phi/commands/research.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ def cmd_notes(args: argparse.Namespace) -> None:
111111
return
112112

113113
content: str = data.get("content") or ""
114-
gcs_url: str | None = data.get("gcs_url")
115-
gcs_uri: str | None = data.get("gcs_uri")
114+
data.get("gcs_url")
115+
data.get("gcs_uri")
116116

117117
if args.out:
118118
out = Path(args.out)
@@ -124,8 +124,6 @@ def cmd_notes(args: argparse.Namespace) -> None:
124124
dest.parent.mkdir(parents=True, exist_ok=True)
125125
dest.write_text(content, encoding="utf-8")
126126
console.print(f"[{_C_SAND}]Notes saved[/] → {dest}")
127-
if gcs_url:
128-
console.print(f"[dim]Download URL:[/] {gcs_url}")
129127
return
130128

131129
if args.json:
@@ -141,5 +139,3 @@ def cmd_notes(args: argparse.Namespace) -> None:
141139
padding=(1, 2),
142140
)
143141
)
144-
if gcs_uri:
145-
console.print(f"[dim]Storage URI:[/] {gcs_uri}")

src/phi/commands/structure.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,14 +153,12 @@ def _fetch_and_upload(
153153
("Source ", source_url),
154154
("File ", str(out_path)),
155155
("Dataset ", dataset_id),
156-
("GCS URI ", gcs_uri),
157156
]:
158157
console.print(f" [bold]{label}[/bold] {value}")
159158
console.print()
160159
console.print(" [dim]Next steps:[/dim]")
161-
console.print(
162-
f" [dim] phi design --target-pdb-gcs {gcs_uri} --hotspots <A45,A67> --num-designs 50[/dim]"
163-
)
160+
console.print(f" [dim] phi upload {out_path}[/dim]")
161+
console.print(" [dim] phi filter --preset default --wait[/dim]")
164162
console.rule()
165163

166164

src/phi/commands/tutorial.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
import urllib.request
5+
from pathlib import Path
6+
7+
from phi.api import _request
8+
from phi.config import _save_state, _ssl_context
9+
from phi.display import _C_BLUE, _C_SAND, _die, console
10+
11+
12+
def cmd_tutorial(args: argparse.Namespace) -> None:
13+
out = Path(args.out)
14+
15+
# ── 1. Fetch manifest (standard Clerk JWT auth, same as all endpoints) ───
16+
console.print("[dim]Fetching tutorial dataset …[/]")
17+
try:
18+
manifest = _request("GET", "/tutorial")
19+
except Exception as exc:
20+
_die(
21+
f"Could not reach the tutorial endpoint: {exc}\n"
22+
" Check your connection and API key, then try again."
23+
)
24+
25+
files: list[dict] = manifest.get("files", [])
26+
dataset_id: str | None = manifest.get("dataset_id")
27+
message: str | None = manifest.get("message")
28+
29+
if not files:
30+
_die("No tutorial files returned by the API.")
31+
32+
# ── 2. Download each file (plain HTTP — signed URLs are self-authenticating)
33+
out.mkdir(parents=True, exist_ok=True)
34+
console.print(f" Downloading {len(files)} file(s) to [{_C_BLUE}]{out}/[/] …\n")
35+
36+
for entry in files:
37+
filename: str = entry["filename"]
38+
url: str = entry["url"]
39+
dest = out / filename
40+
dest.parent.mkdir(parents=True, exist_ok=True)
41+
try:
42+
req = urllib.request.Request(url)
43+
with urllib.request.urlopen(req, context=_ssl_context()) as resp:
44+
dest.write_bytes(resp.read())
45+
console.print(f" [bold {_C_SAND}]✓[/] {filename}")
46+
except Exception as exc:
47+
_die(f"Failed to download {filename}: {exc}")
48+
49+
# ── 3. Cache dataset_id so phi filter needs zero extra flags ─────────────
50+
if dataset_id:
51+
_save_state({"dataset_id": dataset_id})
52+
console.print(
53+
f"\n[dim]dataset_id [{_C_BLUE}]{dataset_id}[/] cached — "
54+
f"run [bold]phi filter[/] to start scoring.[/]"
55+
)
56+
57+
# ── 4. Print step-by-step guide ──────────────────────────────────────────
58+
if message:
59+
console.print(f"\n[dim]{message}[/]")
60+
61+
if dataset_id:
62+
upload_step = "[dim] (skipped — dataset already ready)[/]"
63+
else:
64+
upload_step = f" [{_C_SAND}]phi upload {out}/[/]"
65+
66+
console.print(f"""
67+
[bold]── Tutorial: PD-L1 binder scoring pipeline ──────────────────[/]
68+
69+
You have {len(files)} example binder structures in [{_C_BLUE}]{out}/[/].
70+
71+
[bold]Step 1 — Upload[/]
72+
{upload_step}
73+
74+
[bold]Step 2 — Run the filter pipeline[/]
75+
[{_C_SAND}]phi filter --preset default --wait[/]
76+
77+
Runs: ProteinMPNN → ESMFold → AlphaFold2 → score
78+
Typical runtime: 10–30 min for {len(files)} structures.
79+
80+
[bold]Step 3 — View scores[/]
81+
[{_C_SAND}]phi scores[/]
82+
83+
[bold]Step 4 — Download results[/]
84+
[{_C_SAND}]phi download --out ./results[/]
85+
86+
[bold]Dashboard[/]
87+
[{_C_BLUE}]https://design.dynotx.com/dashboard[/]
88+
""")

src/phi/display.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,8 @@ def _print_status(s: dict) -> None:
272272
table.add_column("filename", style=_C_BLUE, no_wrap=True)
273273
table.add_column("type", style="dim")
274274
for f in files[:10]:
275-
fname = f.get("filename") or f.get("gcs_url", "?")
275+
raw = f.get("filename") or f.get("gcs_url", "?")
276+
fname = raw.split("/")[-1] if raw.startswith("gs://") else raw
276277
ftype = f.get("artifact_type", "")
277278
table.add_row(fname, ftype)
278279
if len(files) > 10:

src/phi/parser.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,27 @@
44
from phi.config import _FILTER_PRESETS, POLL_INTERVAL
55

66
_CLI_EPILOG = """\
7+
Quick start:
8+
phi tutorial # download example structures + print step-by-step guide
9+
phi filter --preset default --wait
10+
phi scores
11+
phi download --out ./results
12+
713
Fetch and prepare target structures:
814
phi fetch --pdb 4ZQK --chain A --residues 56-290 --out target.pdb
915
phi fetch --uniprot Q9NZQ7 --trim-low-confidence 70 --upload
1016
11-
Design (backbone generation):
12-
phi design --target-pdb target.pdb --hotspots A45,A67 --num-designs 50
13-
phi design --length 80 --num-designs 20
14-
phi boltzgen --yaml design.yaml --protocol protein-anything --num-designs 10
15-
1617
Validation (fold + score):
1718
phi esmfold --fasta sequences.fasta
1819
phi alphafold --fasta complex.fasta
1920
phi proteinmpnn --pdb design.pdb --num-sequences 20
2021
phi esm2 --fasta sequences.fasta
2122
phi boltz --fasta complex.fasta
2223
23-
Batch filter pipeline (100-50,000 designs):
24-
phi upload --dir ./designs/ --file-type pdb
25-
phi filter --dataset-id <id> --preset default --wait
26-
phi download --out ./results
24+
Batch filter pipeline:
25+
phi upload ./designs/
26+
phi filter --preset default --wait
27+
phi download --out ./results
2728
2829
Dataset management:
2930
phi datasets # list your datasets
@@ -253,6 +254,17 @@ def build_parser() -> argparse.ArgumentParser:
253254
)
254255
sub = root.add_subparsers(dest="command", required=True)
255256

257+
p = sub.add_parser(
258+
"tutorial",
259+
help="Download example structures and print a step-by-step scoring walkthrough",
260+
)
261+
p.add_argument(
262+
"--out",
263+
metavar="DIR",
264+
default="examples",
265+
help="Directory to download example files into (default: ./examples)",
266+
)
267+
256268
p = sub.add_parser("login", help="Verify API key and print connection + identity details")
257269
p.add_argument("--json", action="store_true")
258270

0 commit comments

Comments
 (0)