amina-cli 0.2.4__tar.gz → 0.2.6__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 (63) hide show
  1. {amina_cli-0.2.4 → amina_cli-0.2.6}/PKG-INFO +1 -1
  2. {amina_cli-0.2.4 → amina_cli-0.2.6}/pyproject.toml +1 -1
  3. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/__init__.py +1 -1
  4. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/auth.py +31 -0
  5. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/client.py +15 -3
  6. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/jobs_cmd.py +70 -27
  7. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/properties/esm2_embedding.py +49 -5
  8. {amina_cli-0.2.4 → amina_cli-0.2.6}/.gitignore +0 -0
  9. {amina_cli-0.2.4 → amina_cli-0.2.6}/LICENSE +0 -0
  10. {amina_cli-0.2.4 → amina_cli-0.2.6}/README.md +0 -0
  11. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/__init__.py +0 -0
  12. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/auth_cmd.py +0 -0
  13. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/run_cmd.py +0 -0
  14. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/__init__.py +0 -0
  15. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/analysis/__init__.py +0 -0
  16. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/analysis/hydrophobicity.py +0 -0
  17. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/analysis/mmseqs2_cluster.py +0 -0
  18. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/analysis/rmsd.py +0 -0
  19. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/analysis/sasa.py +0 -0
  20. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/analysis/simple_rmsd.py +0 -0
  21. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/analysis/surface_charge.py +0 -0
  22. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/analysis/usalign.py +0 -0
  23. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/design/__init__.py +0 -0
  24. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/design/esm_if1.py +0 -0
  25. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/design/protein_mc.py +0 -0
  26. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/design/proteinmpnn.py +0 -0
  27. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/design/rfdiffusion.py +0 -0
  28. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/display.py +0 -0
  29. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/folding/__init__.py +0 -0
  30. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/folding/boltz2.py +0 -0
  31. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/folding/esmfold.py +0 -0
  32. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/folding/openfold3.py +0 -0
  33. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/folding/protenix.py +0 -0
  34. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/interactions/__init__.py +0 -0
  35. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/interactions/autodock_vina.py +0 -0
  36. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/interactions/diffdock.py +0 -0
  37. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/interactions/dockq.py +0 -0
  38. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/interactions/emngly.py +0 -0
  39. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/interactions/glycosylation_ensemble.py +0 -0
  40. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/interactions/interface_identifier.py +0 -0
  41. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/interactions/isoglyp.py +0 -0
  42. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/interactions/lmngly.py +0 -0
  43. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/interactions/p2rank.py +0 -0
  44. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/interactions/pesto.py +0 -0
  45. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/properties/__init__.py +0 -0
  46. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/properties/aminosol.py +0 -0
  47. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/properties/esm1v.py +0 -0
  48. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/utilities/__init__.py +0 -0
  49. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/utilities/activesite_verifier.py +0 -0
  50. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/utilities/chain_select.py +0 -0
  51. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/utilities/distance_calculator.py +0 -0
  52. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/utilities/maxit_convert.py +0 -0
  53. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/utilities/mol_size_calculator.py +0 -0
  54. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/utilities/obabel_convert.py +0 -0
  55. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/utilities/pdb_bfactor_overwrite.py +0 -0
  56. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/utilities/pdb_cleaner.py +0 -0
  57. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/utilities/pdb_quality_assessment.py +0 -0
  58. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/utilities/pdb_to_fasta.py +0 -0
  59. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools/utilities/protein_relaxer.py +0 -0
  60. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/commands/tools_cmd.py +0 -0
  61. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/main.py +0 -0
  62. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/registry.py +0 -0
  63. {amina_cli-0.2.4 → amina_cli-0.2.6}/src/amina_cli/storage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amina-cli
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: CLI for AminoAnalytica protein engineering platform
5
5
  Project-URL: Homepage, https://aminoanalytica.com
6
6
  Project-URL: Documentation, https://docs.aminoanalytica.com
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "amina-cli"
3
- version = "0.2.4"
3
+ version = "0.2.6"
4
4
  description = "CLI for AminoAnalytica protein engineering platform"
5
5
  readme = "README.md"
6
6
  license = {text = "Apache-2.0"}
@@ -9,4 +9,4 @@ Quick start:
9
9
  amina run esmfold --sequence "MKFLILLFNILCLFPVLAADNH"
10
10
  """
11
11
 
12
- __version__ = "0.2.4"
12
+ __version__ = "0.2.6"
@@ -336,3 +336,34 @@ def update_job_status(job_id: str, status: str) -> None:
336
336
  flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
337
337
  with os.fdopen(os.open(JOBS_FILE, flags, 0o600), "w") as f:
338
338
  f.write(content)
339
+
340
+
341
+ def update_job_info(job_id: str, updates: dict) -> None:
342
+ """
343
+ Merge fields into a saved job entry.
344
+
345
+ Used to persist a resolved call_id so subsequent polls skip the queue check.
346
+
347
+ Args:
348
+ job_id: Job ID to update
349
+ updates: Dict of fields to merge (e.g., {"call_id": "fc-xxx"})
350
+ """
351
+ import os
352
+
353
+ if not JOBS_FILE.exists():
354
+ return
355
+
356
+ try:
357
+ jobs = json.loads(JOBS_FILE.read_text())
358
+ except (json.JSONDecodeError, IOError):
359
+ return
360
+
361
+ for job in jobs:
362
+ if job.get("job_id") == job_id:
363
+ job.update(updates)
364
+ break
365
+
366
+ content = json.dumps(jobs, indent=2)
367
+ flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
368
+ with os.fdopen(os.open(JOBS_FILE, flags, 0o600), "w") as f:
369
+ f.write(content)
@@ -92,10 +92,12 @@ class InsufficientCreditsError(ClientError):
92
92
  class JobLimitError(ClientError):
93
93
  """Raised when user has reached job concurrency limits."""
94
94
 
95
- def __init__(self, limit_type: str, current: int, limit: int, message: str):
95
+ def __init__(self, limit_type: str, current: int, limit: int, message: str, **kwargs: Any):
96
96
  self.limit_type = limit_type
97
97
  self.current = current
98
98
  self.limit = limit
99
+ self.running = kwargs.get("running", 0)
100
+ self.queued = kwargs.get("queued", 0)
99
101
  super().__init__(message)
100
102
 
101
103
 
@@ -185,6 +187,7 @@ async def _poll_queued_until_submitted(
185
187
  poll_interval: float = 5.0, # Longer interval for queue polling
186
188
  max_poll_interval: float = 30.0,
187
189
  on_position_update: Optional[QueuePositionCallback] = None,
190
+ headers: Optional[dict[str, str]] = None,
188
191
  ) -> tuple[str, str]:
189
192
  """
190
193
  Poll queued_job_status until job is submitted.
@@ -229,7 +232,7 @@ async def _poll_queued_until_submitted(
229
232
  status_url = f"{queued_endpoint}?job_id={job_id}"
230
233
 
231
234
  try:
232
- response = await client.get(status_url)
235
+ response = await client.get(status_url, headers=headers or {})
233
236
  except httpx.RequestError as e:
234
237
  # Log transient error and continue polling
235
238
  print(f"Queue poll network error (will retry): {type(e).__name__}: {e}")
@@ -396,6 +399,8 @@ async def run_tool(
396
399
  current=detail.get("current", 0),
397
400
  limit=detail.get("limit", 0),
398
401
  message=detail.get("message", "Job limit reached. Please wait for running jobs to complete."),
402
+ running=detail.get("running", 0),
403
+ queued=detail.get("queued", 0),
399
404
  )
400
405
  except (ValueError, KeyError):
401
406
  raise JobLimitError(
@@ -438,6 +443,7 @@ async def run_tool(
438
443
  poll_interval=5.0, # Slower for queue
439
444
  max_poll_interval=30.0,
440
445
  on_position_update=on_queue_position_update,
446
+ headers=auth_headers,
441
447
  )
442
448
  else:
443
449
  # Job was submitted immediately
@@ -663,6 +669,8 @@ async def submit_tool(
663
669
  current=detail.get("current", 0),
664
670
  limit=detail.get("limit", 0),
665
671
  message=detail.get("message", "Job limit reached. Please wait for running jobs to complete."),
672
+ running=detail.get("running", 0),
673
+ queued=detail.get("queued", 0),
666
674
  )
667
675
  except (ValueError, KeyError):
668
676
  raise JobLimitError(
@@ -855,12 +863,16 @@ async def check_queued_job_status(job_id: str) -> dict[str, Any]:
855
863
  - {"status": "submitted", "call_id": "..."} if spawned
856
864
  - {"status": "failed", "error": "..."} if failed/expired
857
865
  """
866
+ from amina_cli.auth import get_api_key
867
+
858
868
  queued_endpoint = get_queued_status_endpoint()
859
869
  status_url = f"{queued_endpoint}?job_id={job_id}"
870
+ api_key = get_api_key()
871
+ headers = {"Authorization": f"Bearer {api_key}"} if api_key else {}
860
872
 
861
873
  async with httpx.AsyncClient(timeout=30.0) as client:
862
874
  try:
863
- response = await client.get(status_url)
875
+ response = await client.get(status_url, headers=headers)
864
876
  except httpx.RequestError as e:
865
877
  raise ToolExecutionError(f"Network error checking queued job status: {str(e)}")
866
878
 
@@ -21,6 +21,65 @@ app = typer.Typer(no_args_is_help=True)
21
21
  console = Console()
22
22
 
23
23
 
24
+ def _resolve_job_status(job_info: dict) -> dict:
25
+ """
26
+ Resolve the current status of a job, handling both spawned and queued jobs.
27
+
28
+ If call_id is present, polls Modal directly. If missing (queued job),
29
+ checks the queue first and resolves call_id when available.
30
+
31
+ Args:
32
+ job_info: Full job info dict from local history
33
+
34
+ Returns:
35
+ Dict with status info (same shape as check_job_status_sync)
36
+ """
37
+ from amina_cli.client import check_job_status_sync, check_queued_job_status_sync
38
+ from amina_cli.auth import update_job_info
39
+
40
+ call_id = job_info.get("call_id", "")
41
+ job_id = job_info.get("job_id", "")
42
+
43
+ if call_id:
44
+ # Normal path: job was spawned, poll Modal directly
45
+ return check_job_status_sync(
46
+ call_id=call_id,
47
+ job_id=job_id,
48
+ user_id=job_info.get("user_id", ""),
49
+ tool_name=job_info.get("tool_name", ""),
50
+ reserved_cost=job_info.get("reserved_cost", 0.0),
51
+ compute_type=job_info.get("compute_type", "cpu"),
52
+ )
53
+
54
+ # Queued path: no call_id yet, check queue status
55
+ queue_result = check_queued_job_status_sync(job_id)
56
+ queue_status = queue_result.get("status", "")
57
+
58
+ if queue_status in ("queued", "not_found"):
59
+ return queue_result
60
+
61
+ if queue_status == "failed":
62
+ return queue_result
63
+
64
+ # Job has been spawned (submitted/running/completed) — try to get call_id
65
+ resolved_call_id = queue_result.get("call_id", "")
66
+ if not resolved_call_id:
67
+ # Race condition: status changed but call_id not yet written
68
+ return {"status": "queued", "message": "Job is being spawned"}
69
+
70
+ # Persist call_id so future polls skip the queue check
71
+ update_job_info(job_id, {"call_id": resolved_call_id})
72
+
73
+ return check_job_status_sync(
74
+ call_id=resolved_call_id,
75
+ job_id=job_id,
76
+ user_id=job_info.get("user_id", ""),
77
+ tool_name=job_info.get("tool_name", ""),
78
+ reserved_cost=job_info.get("reserved_cost", 0.0),
79
+ compute_type=job_info.get("compute_type", "cpu"),
80
+ )
81
+
82
+
24
83
  @app.command("list")
25
84
  def list_jobs(
26
85
  limit: int = typer.Option(
@@ -101,7 +160,7 @@ def status(
101
160
  amina jobs status abc123 --json
102
161
  """
103
162
  from amina_cli.auth import get_job_info
104
- from amina_cli.client import check_job_status_sync, ToolExecutionError
163
+ from amina_cli.client import ToolExecutionError
105
164
 
106
165
  results = []
107
166
 
@@ -121,14 +180,7 @@ def status(
121
180
 
122
181
  # Query server for current status
123
182
  try:
124
- status_result = check_job_status_sync(
125
- call_id=job_info.get("call_id", ""),
126
- job_id=job_info.get("job_id", job_id),
127
- user_id=job_info.get("user_id", ""),
128
- tool_name=job_info.get("tool_name", ""),
129
- reserved_cost=job_info.get("reserved_cost", 0.0),
130
- compute_type=job_info.get("compute_type", "cpu"),
131
- )
183
+ status_result = _resolve_job_status(job_info)
132
184
  results.append(
133
185
  {
134
186
  "job_id": job_info.get("job_id", job_id),
@@ -212,7 +264,7 @@ def wait(
212
264
  amina jobs wait abc123 --timeout 7200 --poll-interval 30
213
265
  """
214
266
  from amina_cli.auth import get_job_info
215
- from amina_cli.client import check_job_status_sync, ToolExecutionError
267
+ from amina_cli.client import ToolExecutionError
216
268
 
217
269
  start_time = time.time()
218
270
  pending_jobs = set(job_ids)
@@ -237,14 +289,7 @@ def wait(
237
289
  continue
238
290
 
239
291
  try:
240
- status_result = check_job_status_sync(
241
- call_id=job_info.get("call_id", ""),
242
- job_id=job_info.get("job_id", job_id),
243
- user_id=job_info.get("user_id", ""),
244
- tool_name=job_info.get("tool_name", ""),
245
- reserved_cost=job_info.get("reserved_cost", 0.0),
246
- compute_type=job_info.get("compute_type", "cpu"),
247
- )
292
+ status_result = _resolve_job_status(job_info)
248
293
 
249
294
  if status_result.get("status") in ("completed", "failed"):
250
295
  results[job_id] = {
@@ -312,7 +357,7 @@ def download(
312
357
  amina jobs download abc123 -o ./results/
313
358
  """
314
359
  from amina_cli.auth import get_job_info
315
- from amina_cli.client import check_job_status_sync, ToolExecutionError
360
+ from amina_cli.client import ToolExecutionError
316
361
  from amina_cli.storage import download_results, StorageError
317
362
 
318
363
  # Look up job info
@@ -324,18 +369,16 @@ def download(
324
369
 
325
370
  # Check status
326
371
  try:
327
- status_result = check_job_status_sync(
328
- call_id=job_info.get("call_id", ""),
329
- job_id=job_info.get("job_id", job_id),
330
- user_id=job_info.get("user_id", ""),
331
- tool_name=job_info.get("tool_name", ""),
332
- reserved_cost=job_info.get("reserved_cost", 0.0),
333
- compute_type=job_info.get("compute_type", "cpu"),
334
- )
372
+ status_result = _resolve_job_status(job_info)
335
373
  except ToolExecutionError as e:
336
374
  console.print(f"[red]Error checking job status:[/red] {e}")
337
375
  raise typer.Exit(1)
338
376
 
377
+ if status_result.get("status") == "queued":
378
+ console.print("[yellow]Job is still queued.[/yellow] Wait for completion first:")
379
+ console.print(f" amina jobs wait {job_id}")
380
+ raise typer.Exit(1)
381
+
339
382
  if status_result.get("status") == "running":
340
383
  console.print("[yellow]Job is still running.[/yellow] Wait for completion first:")
341
384
  console.print(f" amina jobs wait {job_id}")
@@ -9,16 +9,20 @@ METADATA = {
9
9
  "name": "esm2-embedding",
10
10
  "display_name": "ESM2 Embedding",
11
11
  "category": "properties",
12
- "description": "Extract sequence-level protein embeddings from FASTA files using ESM2 models",
12
+ "description": "Extract protein embeddings from FASTA files using ESM2 models",
13
13
  "modal_function_name": "esm2_embedding_worker",
14
14
  "modal_app_name": "esm2-embedding-api",
15
15
  "status": "available",
16
16
  "outputs": {
17
+ # Sequence mode outputs
17
18
  "csv_filepath": "CSV file with sequence embeddings (sequence_id, sequence, dim_0...dim_N)",
18
19
  "tsne_plot_filepath": "Static t-SNE visualization of embeddings (PNG)",
19
20
  "tsne_plot_html_filepath": "Interactive t-SNE visualization with zoom/pan/hover (HTML)",
20
21
  "clusters_csv_filepath": "Cluster assignments (CSV, unless --no-cluster)",
21
22
  "cluster_summary_filepath": "Cluster statistics and quality metrics (JSON, unless --no-cluster)",
23
+ # Residue mode outputs
24
+ "manifest_filepath": "Manifest JSON with file list and metadata (residue mode)",
25
+ "residue_N": "Per-sequence NPZ files with residue embeddings (residue mode)",
22
26
  },
23
27
  }
24
28
 
@@ -76,6 +80,17 @@ def register(app: typer.Typer):
76
80
  "--cluster-min-samples",
77
81
  help="Min samples for density estimation (auto-tuned if not specified)",
78
82
  ),
83
+ embedding_mode: str = typer.Option(
84
+ "sequence",
85
+ "--mode",
86
+ "-M",
87
+ help="Embedding mode: 'sequence' (default) for mean-pooled embeddings, 'residue' for per-residue embeddings",
88
+ ),
89
+ include_special_tokens: bool = typer.Option(
90
+ False,
91
+ "--include-special-tokens",
92
+ help="Include BOS/EOS tokens in residue embeddings (residue mode only)",
93
+ ),
79
94
  job_name: Optional[str] = typer.Option(
80
95
  None,
81
96
  "--job-name",
@@ -94,8 +109,11 @@ def register(app: typer.Typer):
94
109
  Generates dense vector representations (embeddings) of protein sequences
95
110
  that capture evolutionary, structural, and functional information.
96
111
 
97
- By default, also performs HDBSCAN clustering to group similar sequences.
98
- Use --no-cluster to disable clustering.
112
+ Two embedding modes:
113
+ - sequence (default): Mean-pooled sequence embeddings (CSV output)
114
+ with optional t-SNE visualization and HDBSCAN clustering
115
+ - residue: Per-residue embeddings (NPZ output per sequence)
116
+ with manifest JSON, no t-SNE or clustering
99
117
 
100
118
  Model variants (embedding dimensions):
101
119
  - 650M: 1280-dim - Highest accuracy (default)
@@ -104,11 +122,15 @@ def register(app: typer.Typer):
104
122
  - 8M: 320-dim - Fastest, lower accuracy
105
123
 
106
124
  Examples:
125
+ # Sequence mode (default)
107
126
  amina run esm2-embedding --fasta sequences.fasta -o ./output/
108
127
  amina run esm2-embedding -f seqs.fa -m 8M -o ./output/ # Fast mode
109
128
  amina run esm2-embedding -f seqs.fa --no-tsne -o ./output/
110
- amina run esm2-embedding -f seqs.fa --no-cluster -o ./output/ # Skip clustering
111
- amina run esm2-embedding -f seqs.fa --cluster-min-size 5 -o ./output/ # Custom params
129
+ amina run esm2-embedding -f seqs.fa --no-cluster -o ./output/
130
+
131
+ # Residue mode
132
+ amina run esm2-embedding -f seqs.fa --mode residue -o ./output/
133
+ amina run esm2-embedding -f seqs.fa -M residue --include-special-tokens -o ./output/
112
134
  """
113
135
  # Validate required options
114
136
  if output is None and not background:
@@ -128,6 +150,26 @@ def register(app: typer.Typer):
128
150
  console.print("[red]Error:[/red] Batch size must be between 1 and 32")
129
151
  raise typer.Exit(1)
130
152
 
153
+ # Validate embedding mode
154
+ valid_modes = ["sequence", "residue"]
155
+ if embedding_mode.lower() not in valid_modes:
156
+ console.print(f"[red]Error:[/red] Invalid mode '{embedding_mode}'. Choose from: {', '.join(valid_modes)}")
157
+ raise typer.Exit(1)
158
+ embedding_mode = embedding_mode.lower()
159
+
160
+ # Validate mode-specific options
161
+ if embedding_mode == "sequence" and include_special_tokens:
162
+ console.print("[red]Error:[/red] --include-special-tokens only applies to residue mode")
163
+ raise typer.Exit(1)
164
+
165
+ if embedding_mode == "residue":
166
+ if not no_tsne:
167
+ console.print("[yellow]Note:[/yellow] t-SNE visualization is not available in residue mode (skipping)")
168
+ no_tsne = True
169
+ if not no_cluster:
170
+ console.print("[yellow]Note:[/yellow] Clustering is not available in residue mode (skipping)")
171
+ no_cluster = True
172
+
131
173
  # Read FASTA content
132
174
  fasta_content = fasta.read_text()
133
175
  if not fasta_content.strip().startswith(">"):
@@ -148,6 +190,8 @@ def register(app: typer.Typer):
148
190
  "cluster_by_embeddings": not no_cluster,
149
191
  "cluster_min_size": cluster_min_size,
150
192
  "cluster_min_samples": cluster_min_samples,
193
+ "embedding_mode": embedding_mode,
194
+ "include_special_tokens": include_special_tokens,
151
195
  }
152
196
 
153
197
  if job_name:
File without changes
File without changes
File without changes