microlens-submit 0.12.2__py3-none-any.whl → 0.16.1__py3-none-any.whl
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.
- microlens_submit/__init__.py +7 -157
- microlens_submit/cli/__init__.py +5 -0
- microlens_submit/cli/__main__.py +6 -0
- microlens_submit/cli/commands/__init__.py +1 -0
- microlens_submit/cli/commands/dossier.py +139 -0
- microlens_submit/cli/commands/export.py +177 -0
- microlens_submit/cli/commands/init.py +172 -0
- microlens_submit/cli/commands/solutions.py +722 -0
- microlens_submit/cli/commands/validation.py +241 -0
- microlens_submit/cli/main.py +120 -0
- microlens_submit/dossier/__init__.py +51 -0
- microlens_submit/dossier/dashboard.py +503 -0
- microlens_submit/dossier/event_page.py +370 -0
- microlens_submit/dossier/full_report.py +330 -0
- microlens_submit/dossier/solution_page.py +534 -0
- microlens_submit/dossier/utils.py +111 -0
- microlens_submit/error_messages.py +283 -0
- microlens_submit/models/__init__.py +28 -0
- microlens_submit/models/event.py +406 -0
- microlens_submit/models/solution.py +569 -0
- microlens_submit/models/submission.py +569 -0
- microlens_submit/tier_validation.py +208 -0
- microlens_submit/utils.py +373 -0
- microlens_submit/validate_parameters.py +478 -180
- {microlens_submit-0.12.2.dist-info → microlens_submit-0.16.1.dist-info}/METADATA +52 -14
- microlens_submit-0.16.1.dist-info/RECORD +32 -0
- microlens_submit/api.py +0 -1257
- microlens_submit/cli.py +0 -1803
- microlens_submit/dossier.py +0 -1443
- microlens_submit-0.12.2.dist-info/RECORD +0 -13
- {microlens_submit-0.12.2.dist-info → microlens_submit-0.16.1.dist-info}/WHEEL +0 -0
- {microlens_submit-0.12.2.dist-info → microlens_submit-0.16.1.dist-info}/entry_points.txt +0 -0
- {microlens_submit-0.12.2.dist-info → microlens_submit-0.16.1.dist-info}/licenses/LICENSE +0 -0
- {microlens_submit-0.12.2.dist-info → microlens_submit-0.16.1.dist-info}/top_level.txt +0 -0
microlens_submit/cli.py
DELETED
|
@@ -1,1803 +0,0 @@
|
|
|
1
|
-
"""Command line interface for microlens-submit.
|
|
2
|
-
|
|
3
|
-
This module provides a comprehensive CLI for managing microlensing challenge
|
|
4
|
-
submissions. It includes commands for project initialization, solution management,
|
|
5
|
-
validation, dossier generation, and export functionality.
|
|
6
|
-
|
|
7
|
-
The CLI is built using Typer and provides rich, colored output with helpful
|
|
8
|
-
error messages and validation feedback. All commands support both interactive
|
|
9
|
-
and scripted usage patterns.
|
|
10
|
-
|
|
11
|
-
**Key Commands:**
|
|
12
|
-
- init: Create new submission projects
|
|
13
|
-
- add-solution: Add microlensing solutions with parameters
|
|
14
|
-
- validate-submission: Check submission completeness
|
|
15
|
-
- generate-dossier: Create HTML documentation
|
|
16
|
-
- export: Create submission archives
|
|
17
|
-
|
|
18
|
-
**Example Workflow:**
|
|
19
|
-
# Initialize a new project
|
|
20
|
-
microlens-submit init --team-name "Team Alpha" --tier "advanced" ./my_project
|
|
21
|
-
|
|
22
|
-
# Add a solution
|
|
23
|
-
microlens-submit add-solution EVENT001 1S1L ./my_project \
|
|
24
|
-
--param t0=2459123.5 --param u0=0.1 --param tE=20.0 \
|
|
25
|
-
--log-likelihood -1234.56 --cpu-hours 2.5
|
|
26
|
-
|
|
27
|
-
# Validate and generate dossier
|
|
28
|
-
microlens-submit validate-submission ./my_project
|
|
29
|
-
microlens-submit generate-dossier ./my_project
|
|
30
|
-
|
|
31
|
-
# Export for submission
|
|
32
|
-
microlens-submit export submission.zip ./my_project
|
|
33
|
-
|
|
34
|
-
**Note:**
|
|
35
|
-
All commands that modify data automatically save changes to disk.
|
|
36
|
-
Use --dry-run flags to preview changes without saving.
|
|
37
|
-
"""
|
|
38
|
-
|
|
39
|
-
from __future__ import annotations
|
|
40
|
-
|
|
41
|
-
import json
|
|
42
|
-
import math
|
|
43
|
-
from pathlib import Path
|
|
44
|
-
from typing import List, Optional, Literal
|
|
45
|
-
import os
|
|
46
|
-
import subprocess
|
|
47
|
-
|
|
48
|
-
import typer
|
|
49
|
-
from rich.console import Console
|
|
50
|
-
from rich.panel import Panel
|
|
51
|
-
from rich.table import Table
|
|
52
|
-
|
|
53
|
-
from .api import load
|
|
54
|
-
from .dossier import generate_dashboard_html
|
|
55
|
-
from . import __version__
|
|
56
|
-
|
|
57
|
-
console = Console()
|
|
58
|
-
app = typer.Typer()
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
@app.command("version")
|
|
62
|
-
def version() -> None:
|
|
63
|
-
"""Show the version of microlens-submit.
|
|
64
|
-
|
|
65
|
-
Displays the current version of the microlens-submit package.
|
|
66
|
-
|
|
67
|
-
Example:
|
|
68
|
-
>>> microlens-submit version
|
|
69
|
-
microlens-submit version 0.12.0-dev
|
|
70
|
-
|
|
71
|
-
Note:
|
|
72
|
-
This command is useful for verifying the installed version
|
|
73
|
-
and for debugging purposes.
|
|
74
|
-
"""
|
|
75
|
-
console.print(f"microlens-submit version {__version__}")
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def _parse_pairs(pairs: Optional[List[str]]) -> Optional[dict]:
|
|
79
|
-
"""Convert CLI key=value options into a dictionary.
|
|
80
|
-
|
|
81
|
-
Parses command-line arguments in the format "key=value" and converts
|
|
82
|
-
them to a Python dictionary. Handles JSON parsing for numeric and
|
|
83
|
-
boolean values, falling back to string values.
|
|
84
|
-
|
|
85
|
-
Args:
|
|
86
|
-
pairs: List of strings in "key=value" format, or None.
|
|
87
|
-
|
|
88
|
-
Returns:
|
|
89
|
-
dict: Parsed key-value pairs, or None if pairs is None/empty.
|
|
90
|
-
|
|
91
|
-
Raises:
|
|
92
|
-
typer.BadParameter: If any pair is not in "key=value" format.
|
|
93
|
-
|
|
94
|
-
Example:
|
|
95
|
-
>>> _parse_pairs(["t0=2459123.5", "u0=0.1", "active=true"])
|
|
96
|
-
{'t0': 2459123.5, 'u0': 0.1, 'active': True}
|
|
97
|
-
|
|
98
|
-
>>> _parse_pairs(["name=test", "value=123"])
|
|
99
|
-
{'name': 'test', 'value': 123}
|
|
100
|
-
|
|
101
|
-
Note:
|
|
102
|
-
This function attempts to parse values as JSON first (for numbers,
|
|
103
|
-
booleans, etc.), then falls back to string values if JSON parsing fails.
|
|
104
|
-
"""
|
|
105
|
-
if not pairs:
|
|
106
|
-
return None
|
|
107
|
-
out: dict = {}
|
|
108
|
-
for item in pairs:
|
|
109
|
-
if "=" not in item:
|
|
110
|
-
raise typer.BadParameter(f"Invalid format: {item}")
|
|
111
|
-
key, value = item.split("=", 1)
|
|
112
|
-
try:
|
|
113
|
-
out[key] = json.loads(value)
|
|
114
|
-
except json.JSONDecodeError:
|
|
115
|
-
out[key] = value
|
|
116
|
-
return out
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def _params_file_callback(ctx: typer.Context, value: Optional[Path]) -> Optional[Path]:
|
|
120
|
-
"""Validate mutually exclusive parameter options.
|
|
121
|
-
|
|
122
|
-
Ensures that --params-file and --param options are not used together,
|
|
123
|
-
as they are mutually exclusive ways of specifying parameters.
|
|
124
|
-
|
|
125
|
-
Args:
|
|
126
|
-
ctx: Typer context containing other parameter values.
|
|
127
|
-
value: The value of the --params-file option.
|
|
128
|
-
|
|
129
|
-
Returns:
|
|
130
|
-
Path: The validated file path, or None.
|
|
131
|
-
|
|
132
|
-
Raises:
|
|
133
|
-
typer.BadParameter: If both --params-file and --param are specified,
|
|
134
|
-
or if neither is specified when required.
|
|
135
|
-
|
|
136
|
-
Example:
|
|
137
|
-
# This would raise an error:
|
|
138
|
-
# microlens-submit add-solution EVENT001 1S1L --param t0=123 --params-file params.json
|
|
139
|
-
|
|
140
|
-
# This is valid:
|
|
141
|
-
# microlens-submit add-solution EVENT001 1S1L --params-file params.json
|
|
142
|
-
|
|
143
|
-
Note:
|
|
144
|
-
This is a Typer callback function used for parameter validation.
|
|
145
|
-
It's automatically called when processing command-line arguments.
|
|
146
|
-
"""
|
|
147
|
-
param_vals = ctx.params.get("param")
|
|
148
|
-
if value is not None and param_vals:
|
|
149
|
-
raise typer.BadParameter("Cannot use --param with --params-file")
|
|
150
|
-
if value is None and not param_vals and not ctx.resilient_parsing:
|
|
151
|
-
raise typer.BadParameter("Provide either --param or --params-file")
|
|
152
|
-
return value
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
@app.callback()
|
|
156
|
-
def main(
|
|
157
|
-
ctx: typer.Context,
|
|
158
|
-
no_color: bool = typer.Option(False, "--no-color", help="Disable colored output"),
|
|
159
|
-
) -> None:
|
|
160
|
-
"""Handle global CLI options.
|
|
161
|
-
|
|
162
|
-
Sets up global configuration for the CLI, including color output
|
|
163
|
-
preferences that apply to all commands.
|
|
164
|
-
|
|
165
|
-
Args:
|
|
166
|
-
ctx: Typer context for command execution.
|
|
167
|
-
no_color: If True, disable colored output for all commands.
|
|
168
|
-
|
|
169
|
-
Example:
|
|
170
|
-
# Disable colors for all commands
|
|
171
|
-
microlens-submit --no-color init --team-name "Team" --tier "basic" ./project
|
|
172
|
-
|
|
173
|
-
Note:
|
|
174
|
-
This is a Typer callback that runs before any command execution.
|
|
175
|
-
It's used to configure global settings like color output.
|
|
176
|
-
"""
|
|
177
|
-
if no_color:
|
|
178
|
-
global console
|
|
179
|
-
console = Console(color_system=None)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
@app.command()
|
|
183
|
-
def init(
|
|
184
|
-
team_name: str = typer.Option(..., help="Team name"),
|
|
185
|
-
tier: str = typer.Option(..., help="Challenge tier"),
|
|
186
|
-
project_path: Path = typer.Argument(Path("."), help="Project directory"),
|
|
187
|
-
) -> None:
|
|
188
|
-
"""Create a new submission project in the specified directory.
|
|
189
|
-
|
|
190
|
-
Initializes a new microlensing submission project with the given team name
|
|
191
|
-
and tier. The project directory structure is created automatically, and
|
|
192
|
-
the submission.json file is initialized with basic metadata.
|
|
193
|
-
|
|
194
|
-
This command also attempts to auto-detect the GitHub repository URL
|
|
195
|
-
from the current git configuration and provides helpful feedback.
|
|
196
|
-
|
|
197
|
-
Args:
|
|
198
|
-
team_name: Name of the participating team (e.g., "Team Alpha").
|
|
199
|
-
tier: Challenge tier level (e.g., "basic", "advanced").
|
|
200
|
-
project_path: Directory where the project will be created.
|
|
201
|
-
Defaults to current directory if not specified.
|
|
202
|
-
|
|
203
|
-
Raises:
|
|
204
|
-
OSError: If unable to create the project directory or write files.
|
|
205
|
-
|
|
206
|
-
Example:
|
|
207
|
-
# Create project in current directory
|
|
208
|
-
microlens-submit init --team-name "Team Alpha" --tier "advanced"
|
|
209
|
-
|
|
210
|
-
# Create project in specific directory
|
|
211
|
-
microlens-submit init --team-name "Team Beta" --tier "basic" ./my_submission
|
|
212
|
-
|
|
213
|
-
# Project structure created:
|
|
214
|
-
# ./my_submission/
|
|
215
|
-
# ├── submission.json
|
|
216
|
-
# └── events/
|
|
217
|
-
|
|
218
|
-
Note:
|
|
219
|
-
If the project directory already exists, it will be used as-is.
|
|
220
|
-
If a git repository is detected, the GitHub URL will be automatically
|
|
221
|
-
set. Otherwise, a warning is shown and you can set it later with
|
|
222
|
-
set-repo-url command.
|
|
223
|
-
"""
|
|
224
|
-
sub = load(str(project_path))
|
|
225
|
-
sub.team_name = team_name
|
|
226
|
-
sub.tier = tier
|
|
227
|
-
# Try to auto-detect repo_url
|
|
228
|
-
try:
|
|
229
|
-
repo_url = subprocess.check_output([
|
|
230
|
-
'git', 'config', '--get', 'remote.origin.url'
|
|
231
|
-
], stderr=subprocess.DEVNULL).decode().strip()
|
|
232
|
-
except Exception:
|
|
233
|
-
repo_url = None
|
|
234
|
-
if repo_url:
|
|
235
|
-
sub.repo_url = repo_url
|
|
236
|
-
console.print(f"[green]Auto-detected GitHub repo URL:[/green] {repo_url}")
|
|
237
|
-
else:
|
|
238
|
-
console.print("[yellow]Could not auto-detect a GitHub repository URL. Please add it using 'microlens-submit set-repo-url <url> <project_dir>'.[/yellow]")
|
|
239
|
-
sub.save()
|
|
240
|
-
console.print(Panel(f"Initialized project at {project_path}", style="bold green"))
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
@app.command("nexus-init")
|
|
244
|
-
def nexus_init(
|
|
245
|
-
team_name: str = typer.Option(..., help="Team name"),
|
|
246
|
-
tier: str = typer.Option(..., help="Challenge tier"),
|
|
247
|
-
project_path: Path = typer.Argument(Path("."), help="Project directory"),
|
|
248
|
-
) -> None:
|
|
249
|
-
"""Create a project and record Roman Nexus environment details.
|
|
250
|
-
|
|
251
|
-
This command combines the functionality of init() with automatic
|
|
252
|
-
detection of Roman Science Platform environment information. It
|
|
253
|
-
populates hardware_info with CPU details, memory information, and
|
|
254
|
-
the Nexus image identifier.
|
|
255
|
-
|
|
256
|
-
Args:
|
|
257
|
-
team_name: Name of the participating team (e.g., "Team Alpha").
|
|
258
|
-
tier: Challenge tier level (e.g., "basic", "advanced").
|
|
259
|
-
project_path: Directory where the project will be created.
|
|
260
|
-
Defaults to current directory if not specified.
|
|
261
|
-
|
|
262
|
-
Example:
|
|
263
|
-
# Initialize project with Nexus platform info
|
|
264
|
-
microlens-submit nexus-init --team-name "Team Alpha" --tier "advanced" ./project
|
|
265
|
-
|
|
266
|
-
# This will automatically detect:
|
|
267
|
-
# - CPU model from /proc/cpuinfo
|
|
268
|
-
# - Memory from /proc/meminfo
|
|
269
|
-
# - Nexus image from JUPYTER_IMAGE_SPEC
|
|
270
|
-
|
|
271
|
-
Note:
|
|
272
|
-
This command is specifically designed for the Roman Science Platform
|
|
273
|
-
environment. It will silently skip any environment information that
|
|
274
|
-
cannot be detected (e.g., if running outside of Nexus).
|
|
275
|
-
"""
|
|
276
|
-
init(team_name=team_name, tier=tier, project_path=project_path)
|
|
277
|
-
sub = load(str(project_path))
|
|
278
|
-
sub.autofill_nexus_info()
|
|
279
|
-
sub.save()
|
|
280
|
-
console.print("Nexus platform info captured.", style="bold green")
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
@app.command("add-solution")
|
|
284
|
-
def add_solution(
|
|
285
|
-
event_id: str,
|
|
286
|
-
model_type: str = typer.Argument(
|
|
287
|
-
...,
|
|
288
|
-
metavar="{1S1L|1S2L|2S1L|2S2L|1S3L|2S3L|other}",
|
|
289
|
-
help="Type of model used for the solution (e.g., 1S1L, 1S2L)",
|
|
290
|
-
),
|
|
291
|
-
param: Optional[List[str]] = typer.Option(
|
|
292
|
-
None, help="Model parameters as key=value"
|
|
293
|
-
),
|
|
294
|
-
params_file: Optional[Path] = typer.Option(
|
|
295
|
-
None,
|
|
296
|
-
"--params-file",
|
|
297
|
-
help="Path to JSON or YAML file with model parameters and uncertainties",
|
|
298
|
-
callback=_params_file_callback,
|
|
299
|
-
),
|
|
300
|
-
bands: Optional[List[str]] = typer.Option(
|
|
301
|
-
None,
|
|
302
|
-
"--bands",
|
|
303
|
-
help="Comma-separated list of photometric bands used (e.g., 0,1,2)",
|
|
304
|
-
),
|
|
305
|
-
higher_order_effect: Optional[List[str]] = typer.Option(
|
|
306
|
-
None,
|
|
307
|
-
"--higher-order-effect",
|
|
308
|
-
help="List of higher-order effects (e.g., parallax, finite-source)",
|
|
309
|
-
),
|
|
310
|
-
t_ref: Optional[float] = typer.Option(
|
|
311
|
-
None,
|
|
312
|
-
"--t-ref",
|
|
313
|
-
help="Reference time for the model",
|
|
314
|
-
),
|
|
315
|
-
used_astrometry: bool = typer.Option(False, help="Set if astrometry was used"),
|
|
316
|
-
used_postage_stamps: bool = typer.Option(
|
|
317
|
-
False, help="Set if postage stamps were used"
|
|
318
|
-
),
|
|
319
|
-
limb_darkening_model: Optional[str] = typer.Option(
|
|
320
|
-
None, help="Limb darkening model name"
|
|
321
|
-
),
|
|
322
|
-
limb_darkening_coeff: Optional[List[str]] = typer.Option(
|
|
323
|
-
None,
|
|
324
|
-
"--limb-darkening-coeff",
|
|
325
|
-
help="Limb darkening coefficients as key=value",
|
|
326
|
-
),
|
|
327
|
-
parameter_uncertainty: Optional[List[str]] = typer.Option(
|
|
328
|
-
None,
|
|
329
|
-
"--param-uncertainty",
|
|
330
|
-
help="Parameter uncertainties as key=value",
|
|
331
|
-
),
|
|
332
|
-
physical_param: Optional[List[str]] = typer.Option(
|
|
333
|
-
None,
|
|
334
|
-
"--physical-param",
|
|
335
|
-
help="Physical parameters as key=value",
|
|
336
|
-
),
|
|
337
|
-
relative_probability: Optional[float] = typer.Option(
|
|
338
|
-
None,
|
|
339
|
-
"--relative-probability",
|
|
340
|
-
help="Relative probability of this solution",
|
|
341
|
-
),
|
|
342
|
-
log_likelihood: Optional[float] = typer.Option(None, help="Log likelihood"),
|
|
343
|
-
n_data_points: Optional[int] = typer.Option(
|
|
344
|
-
None,
|
|
345
|
-
"--n-data-points",
|
|
346
|
-
help="Number of data points used in this solution",
|
|
347
|
-
),
|
|
348
|
-
cpu_hours: Optional[float] = typer.Option(
|
|
349
|
-
None,
|
|
350
|
-
"--cpu-hours",
|
|
351
|
-
help="CPU hours used for this solution",
|
|
352
|
-
),
|
|
353
|
-
wall_time_hours: Optional[float] = typer.Option(
|
|
354
|
-
None,
|
|
355
|
-
"--wall-time-hours",
|
|
356
|
-
help="Wall time hours used for this solution",
|
|
357
|
-
),
|
|
358
|
-
lightcurve_plot_path: Optional[Path] = typer.Option(
|
|
359
|
-
None, "--lightcurve-plot-path", help="Path to lightcurve plot file"
|
|
360
|
-
),
|
|
361
|
-
lens_plane_plot_path: Optional[Path] = typer.Option(
|
|
362
|
-
None, "--lens-plane-plot-path", help="Path to lens plane plot file"
|
|
363
|
-
),
|
|
364
|
-
notes: Optional[str] = typer.Option(None, help="Notes for the solution (supports Markdown formatting)"),
|
|
365
|
-
notes_file: Optional[Path] = typer.Option(None, "--notes-file", help="Path to a Markdown file for solution notes (mutually exclusive with --notes)"),
|
|
366
|
-
dry_run: bool = typer.Option(
|
|
367
|
-
False,
|
|
368
|
-
"--dry-run",
|
|
369
|
-
help="Parse inputs and display the resulting Solution without saving",
|
|
370
|
-
),
|
|
371
|
-
project_path: Path = typer.Argument(Path("."), help="Project directory"),
|
|
372
|
-
) -> None:
|
|
373
|
-
"""Add a new solution entry for a microlensing event.
|
|
374
|
-
|
|
375
|
-
Creates a new solution with the specified model type and parameters,
|
|
376
|
-
automatically generating a unique solution ID. The solution is added
|
|
377
|
-
to the specified event and saved to disk.
|
|
378
|
-
|
|
379
|
-
**Model Types Supported:**
|
|
380
|
-
- 1S1L: Single source, single lens (point source, point lens)
|
|
381
|
-
- 1S2L: Single source, binary lens
|
|
382
|
-
- 2S1L: Binary source, single lens
|
|
383
|
-
- 2S2L: Binary source, binary lens
|
|
384
|
-
- 1S3L: Single source, triple lens
|
|
385
|
-
- 2S3L: Binary source, triple lens
|
|
386
|
-
- other: Custom model type
|
|
387
|
-
|
|
388
|
-
**Parameter Specification:**
|
|
389
|
-
Parameters can be specified either via individual --param options
|
|
390
|
-
or using a structured file with --params-file. The file can be JSON
|
|
391
|
-
or YAML format and can include both parameters and uncertainties.
|
|
392
|
-
|
|
393
|
-
**Higher-Order Effects:**
|
|
394
|
-
- parallax: Annual parallax effect
|
|
395
|
-
- finite-source: Finite source size effects
|
|
396
|
-
- lens-orbital-motion: Lens orbital motion
|
|
397
|
-
- limb-darkening: Limb darkening effects
|
|
398
|
-
- xallarap: Xallarap (source orbital motion)
|
|
399
|
-
- stellar-rotation: Stellar rotation effects
|
|
400
|
-
- fitted-limb-darkening: Fitted limb darkening coefficients
|
|
401
|
-
- gaussian-process: Gaussian process noise modeling
|
|
402
|
-
- other: Custom higher-order effects
|
|
403
|
-
|
|
404
|
-
Args:
|
|
405
|
-
event_id: Identifier for the microlensing event (e.g., "EVENT001").
|
|
406
|
-
model_type: Type of microlensing model used for the fit.
|
|
407
|
-
param: Model parameters as key=value pairs (e.g., "t0=2459123.5").
|
|
408
|
-
params_file: Path to JSON/YAML file containing parameters and uncertainties.
|
|
409
|
-
bands: List of photometric bands used in the fit (e.g., ["0", "1", "2"]).
|
|
410
|
-
higher_order_effect: List of higher-order physical effects included.
|
|
411
|
-
t_ref: Reference time for time-dependent effects (Julian Date).
|
|
412
|
-
used_astrometry: Whether astrometric data was used in the fit.
|
|
413
|
-
used_postage_stamps: Whether postage stamp data was used.
|
|
414
|
-
limb_darkening_model: Name of the limb darkening model employed.
|
|
415
|
-
limb_darkening_coeff: Limb darkening coefficients as key=value pairs.
|
|
416
|
-
parameter_uncertainty: Parameter uncertainties as key=value pairs.
|
|
417
|
-
physical_param: Derived physical parameters as key=value pairs.
|
|
418
|
-
relative_probability: Probability of this solution being the best model.
|
|
419
|
-
log_likelihood: Log-likelihood value of the fit.
|
|
420
|
-
n_data_points: Number of data points used in the fit.
|
|
421
|
-
cpu_hours: Total CPU time consumed by the fit.
|
|
422
|
-
wall_time_hours: Real-world time consumed by the fit.
|
|
423
|
-
lightcurve_plot_path: Path to the lightcurve plot file.
|
|
424
|
-
lens_plane_plot_path: Path to the lens plane plot file.
|
|
425
|
-
notes: Markdown-formatted notes for the solution.
|
|
426
|
-
notes_file: Path to a Markdown file containing solution notes.
|
|
427
|
-
dry_run: If True, display the solution without saving.
|
|
428
|
-
project_path: Directory of the submission project.
|
|
429
|
-
|
|
430
|
-
Raises:
|
|
431
|
-
typer.BadParameter: If parameter format is invalid or model type is unsupported.
|
|
432
|
-
OSError: If unable to write files or create directories.
|
|
433
|
-
|
|
434
|
-
Example:
|
|
435
|
-
# Simple 1S1L solution with inline parameters
|
|
436
|
-
microlens-submit add-solution EVENT001 1S1L ./project \
|
|
437
|
-
--param t0=2459123.5 --param u0=0.1 --param tE=20.0 \
|
|
438
|
-
--log-likelihood -1234.56 --n-data-points 1250 \
|
|
439
|
-
--cpu-hours 2.5 --wall-time-hours 0.5 \
|
|
440
|
-
--relative-probability 0.8 \
|
|
441
|
-
--notes "# Simple Point Lens Fit\n\nThis is a basic 1S1L solution."
|
|
442
|
-
|
|
443
|
-
# Binary lens solution with higher-order effects
|
|
444
|
-
microlens-submit add-solution EVENT002 1S2L ./project \
|
|
445
|
-
--param t0=2459156.2 --param u0=0.08 --param tE=35.7 \
|
|
446
|
-
--param q=0.0005 --param s=0.95 --param alpha=78.3 \
|
|
447
|
-
--higher-order-effect parallax --higher-order-effect finite-source \
|
|
448
|
-
--t-ref 2459156.0 --log-likelihood -2156.78 \
|
|
449
|
-
--cpu-hours 28.5 --wall-time-hours 7.2
|
|
450
|
-
|
|
451
|
-
# Using a parameter file
|
|
452
|
-
microlens-submit add-solution EVENT003 1S1L ./project \
|
|
453
|
-
--params-file parameters.json \
|
|
454
|
-
--log-likelihood -987.65 --cpu-hours 8.1
|
|
455
|
-
|
|
456
|
-
# Dry run to preview without saving
|
|
457
|
-
microlens-submit add-solution EVENT001 1S1L ./project \
|
|
458
|
-
--param t0=2459123.5 --param u0=0.1 --param tE=20.0 \
|
|
459
|
-
--dry-run
|
|
460
|
-
|
|
461
|
-
Note:
|
|
462
|
-
The solution is automatically assigned a unique UUID and marked as active.
|
|
463
|
-
If notes are provided, they are saved as a Markdown file in the project
|
|
464
|
-
structure. Use --dry-run to preview the solution before saving.
|
|
465
|
-
The command automatically validates the solution and displays any warnings.
|
|
466
|
-
"""
|
|
467
|
-
sub = load(str(project_path))
|
|
468
|
-
evt = sub.get_event(event_id)
|
|
469
|
-
params: dict = {}
|
|
470
|
-
uncertainties: dict = {}
|
|
471
|
-
if params_file is not None:
|
|
472
|
-
params, uncertainties = _parse_structured_params_file(params_file)
|
|
473
|
-
else:
|
|
474
|
-
for p in param or []:
|
|
475
|
-
if "=" not in p:
|
|
476
|
-
raise typer.BadParameter(f"Invalid parameter format: {p}")
|
|
477
|
-
key, value = p.split("=", 1)
|
|
478
|
-
try:
|
|
479
|
-
params[key] = json.loads(value)
|
|
480
|
-
except json.JSONDecodeError:
|
|
481
|
-
params[key] = value
|
|
482
|
-
allowed_model_types = [
|
|
483
|
-
"1S1L",
|
|
484
|
-
"1S2L",
|
|
485
|
-
"2S1L",
|
|
486
|
-
"2S2L",
|
|
487
|
-
"1S3L",
|
|
488
|
-
"2S3L",
|
|
489
|
-
"other",
|
|
490
|
-
]
|
|
491
|
-
if model_type not in allowed_model_types:
|
|
492
|
-
raise typer.BadParameter(f"model_type must be one of {allowed_model_types}")
|
|
493
|
-
if bands and len(bands) == 1 and "," in bands[0]:
|
|
494
|
-
bands = bands[0].split(",")
|
|
495
|
-
if (
|
|
496
|
-
higher_order_effect
|
|
497
|
-
and len(higher_order_effect) == 1
|
|
498
|
-
and "," in higher_order_effect[0]
|
|
499
|
-
):
|
|
500
|
-
higher_order_effect = higher_order_effect[0].split(",")
|
|
501
|
-
sol = evt.add_solution(model_type=model_type, parameters=params)
|
|
502
|
-
sol.bands = bands or []
|
|
503
|
-
sol.higher_order_effects = higher_order_effect or []
|
|
504
|
-
sol.t_ref = t_ref
|
|
505
|
-
sol.used_astrometry = used_astrometry
|
|
506
|
-
sol.used_postage_stamps = used_postage_stamps
|
|
507
|
-
sol.limb_darkening_model = limb_darkening_model
|
|
508
|
-
sol.limb_darkening_coeffs = _parse_pairs(limb_darkening_coeff)
|
|
509
|
-
sol.parameter_uncertainties = _parse_pairs(parameter_uncertainty) or uncertainties
|
|
510
|
-
sol.physical_parameters = _parse_pairs(physical_param)
|
|
511
|
-
sol.log_likelihood = log_likelihood
|
|
512
|
-
sol.relative_probability = relative_probability
|
|
513
|
-
sol.n_data_points = n_data_points
|
|
514
|
-
if cpu_hours is not None or wall_time_hours is not None:
|
|
515
|
-
sol.set_compute_info(cpu_hours=cpu_hours, wall_time_hours=wall_time_hours)
|
|
516
|
-
sol.lightcurve_plot_path = (
|
|
517
|
-
str(lightcurve_plot_path) if lightcurve_plot_path else None
|
|
518
|
-
)
|
|
519
|
-
sol.lens_plane_plot_path = (
|
|
520
|
-
str(lens_plane_plot_path) if lens_plane_plot_path else None
|
|
521
|
-
)
|
|
522
|
-
# Handle notes file logic
|
|
523
|
-
canonical_notes_path = Path(project_path) / "events" / event_id / "solutions" / f"{sol.solution_id}.md"
|
|
524
|
-
if notes_file is not None:
|
|
525
|
-
sol.notes_path = str(notes_file)
|
|
526
|
-
else:
|
|
527
|
-
sol.notes_path = str(canonical_notes_path.relative_to(project_path))
|
|
528
|
-
if dry_run:
|
|
529
|
-
parsed = {
|
|
530
|
-
"event_id": event_id,
|
|
531
|
-
"model_type": model_type,
|
|
532
|
-
"parameters": params,
|
|
533
|
-
"bands": bands,
|
|
534
|
-
"higher_order_effects": higher_order_effect,
|
|
535
|
-
"t_ref": t_ref,
|
|
536
|
-
"used_astrometry": used_astrometry,
|
|
537
|
-
"used_postage_stamps": used_postage_stamps,
|
|
538
|
-
"limb_darkening_model": limb_darkening_model,
|
|
539
|
-
"limb_darkening_coeffs": _parse_pairs(limb_darkening_coeff),
|
|
540
|
-
"parameter_uncertainties": _parse_pairs(parameter_uncertainty),
|
|
541
|
-
"physical_parameters": _parse_pairs(physical_param),
|
|
542
|
-
"log_likelihood": log_likelihood,
|
|
543
|
-
"relative_probability": relative_probability,
|
|
544
|
-
"n_data_points": n_data_points,
|
|
545
|
-
"cpu_hours": cpu_hours,
|
|
546
|
-
"wall_time_hours": wall_time_hours,
|
|
547
|
-
"lightcurve_plot_path": (
|
|
548
|
-
str(lightcurve_plot_path) if lightcurve_plot_path else None
|
|
549
|
-
),
|
|
550
|
-
"lens_plane_plot_path": (
|
|
551
|
-
str(lens_plane_plot_path) if lens_plane_plot_path else None
|
|
552
|
-
),
|
|
553
|
-
"notes_path": sol.notes_path,
|
|
554
|
-
}
|
|
555
|
-
console.print(Panel("Parsed Input", style="cyan"))
|
|
556
|
-
console.print(json.dumps(parsed, indent=2))
|
|
557
|
-
console.print(Panel("Schema Output", style="cyan"))
|
|
558
|
-
console.print(sol.model_dump_json(indent=2))
|
|
559
|
-
validation_messages = sol.run_validation()
|
|
560
|
-
if validation_messages:
|
|
561
|
-
console.print(Panel("Validation Warnings", style="yellow"))
|
|
562
|
-
for msg in validation_messages:
|
|
563
|
-
console.print(f" • {msg}")
|
|
564
|
-
else:
|
|
565
|
-
console.print(Panel("Solution validated successfully!", style="green"))
|
|
566
|
-
return
|
|
567
|
-
# Only write files if not dry_run
|
|
568
|
-
if notes_file is not None:
|
|
569
|
-
# If a notes file is provided, do not overwrite it, just ensure path is set
|
|
570
|
-
pass
|
|
571
|
-
else:
|
|
572
|
-
if notes is not None:
|
|
573
|
-
canonical_notes_path.parent.mkdir(parents=True, exist_ok=True)
|
|
574
|
-
canonical_notes_path.write_text(notes, encoding="utf-8")
|
|
575
|
-
elif not canonical_notes_path.exists():
|
|
576
|
-
canonical_notes_path.parent.mkdir(parents=True, exist_ok=True)
|
|
577
|
-
canonical_notes_path.write_text("", encoding="utf-8")
|
|
578
|
-
sub.save()
|
|
579
|
-
validation_messages = sol.run_validation()
|
|
580
|
-
if validation_messages:
|
|
581
|
-
console.print(Panel("Validation Warnings", style="yellow"))
|
|
582
|
-
for msg in validation_messages:
|
|
583
|
-
console.print(f" • {msg}")
|
|
584
|
-
else:
|
|
585
|
-
console.print(Panel("Solution validated successfully!", style="green"))
|
|
586
|
-
console.print(f"Created solution: [bold cyan]{sol.solution_id}[/bold cyan]")
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
@app.command()
|
|
590
|
-
def deactivate(
|
|
591
|
-
solution_id: str,
|
|
592
|
-
project_path: Path = typer.Argument(Path("."), help="Project directory"),
|
|
593
|
-
) -> None:
|
|
594
|
-
"""Mark a solution as inactive so it is excluded from exports.
|
|
595
|
-
|
|
596
|
-
Deactivates a solution by setting its is_active flag to False. Inactive
|
|
597
|
-
solutions are excluded from submission exports and dossier generation,
|
|
598
|
-
but their data remains intact and can be reactivated later.
|
|
599
|
-
|
|
600
|
-
Args:
|
|
601
|
-
solution_id: The unique identifier of the solution to deactivate.
|
|
602
|
-
project_path: Directory of the submission project.
|
|
603
|
-
|
|
604
|
-
Raises:
|
|
605
|
-
typer.Exit: If the solution is not found in any event.
|
|
606
|
-
|
|
607
|
-
Example:
|
|
608
|
-
# Deactivate a specific solution
|
|
609
|
-
microlens-submit deactivate abc12345-def6-7890-ghij-klmnopqrstuv ./project
|
|
610
|
-
|
|
611
|
-
# The solution is now inactive and won't be included in exports
|
|
612
|
-
microlens-submit export submission.zip ./project # Excludes inactive solutions
|
|
613
|
-
|
|
614
|
-
Note:
|
|
615
|
-
This command only changes the active status. The solution data remains
|
|
616
|
-
intact and can be reactivated using the activate command. Use this to
|
|
617
|
-
keep alternative fits without including them in the final submission.
|
|
618
|
-
"""
|
|
619
|
-
sub = load(str(project_path))
|
|
620
|
-
for event in sub.events.values():
|
|
621
|
-
if solution_id in event.solutions:
|
|
622
|
-
event.solutions[solution_id].deactivate()
|
|
623
|
-
sub.save()
|
|
624
|
-
console.print(f"Deactivated {solution_id}")
|
|
625
|
-
return
|
|
626
|
-
console.print(f"Solution {solution_id} not found", style="bold red")
|
|
627
|
-
raise typer.Exit(code=1)
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
@app.command()
|
|
631
|
-
def activate(
|
|
632
|
-
solution_id: str,
|
|
633
|
-
project_path: Path = typer.Argument(Path("."), help="Project directory"),
|
|
634
|
-
) -> None:
|
|
635
|
-
"""Mark a solution as active so it is included in exports.
|
|
636
|
-
|
|
637
|
-
Activates a solution by setting its is_active flag to True. Active
|
|
638
|
-
solutions are included in submission exports and dossier generation.
|
|
639
|
-
This is the default state for newly created solutions.
|
|
640
|
-
|
|
641
|
-
Args:
|
|
642
|
-
solution_id: The unique identifier of the solution to activate.
|
|
643
|
-
project_path: Directory of the submission project.
|
|
644
|
-
|
|
645
|
-
Raises:
|
|
646
|
-
typer.Exit: If the solution is not found in any event.
|
|
647
|
-
|
|
648
|
-
Example:
|
|
649
|
-
# Activate a previously deactivated solution
|
|
650
|
-
microlens-submit activate abc12345-def6-7890-ghij-klmnopqrstuv ./project
|
|
651
|
-
|
|
652
|
-
# The solution is now active and will be included in exports
|
|
653
|
-
microlens-submit export submission.zip ./project # Includes active solutions
|
|
654
|
-
|
|
655
|
-
Note:
|
|
656
|
-
This command only changes the active status. Use this to reactivate
|
|
657
|
-
solutions that were previously deactivated using the deactivate command.
|
|
658
|
-
"""
|
|
659
|
-
sub = load(str(project_path))
|
|
660
|
-
for event in sub.events.values():
|
|
661
|
-
if solution_id in event.solutions:
|
|
662
|
-
event.solutions[solution_id].activate()
|
|
663
|
-
sub.save()
|
|
664
|
-
console.print(f"Activated {solution_id}")
|
|
665
|
-
return
|
|
666
|
-
console.print(f"Solution {solution_id} not found", style="bold red")
|
|
667
|
-
raise typer.Exit(code=1)
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
@app.command()
|
|
671
|
-
def export(
|
|
672
|
-
output_path: Path,
|
|
673
|
-
force: bool = typer.Option(False, "--force", help="Skip validation prompts"),
|
|
674
|
-
project_path: Path = typer.Argument(Path("."), help="Project directory"),
|
|
675
|
-
) -> None:
|
|
676
|
-
"""Generate a zip archive containing all active solutions.
|
|
677
|
-
|
|
678
|
-
Creates a compressed zip file containing the complete submission data,
|
|
679
|
-
including all active solutions, parameters, notes, and referenced files.
|
|
680
|
-
The archive is suitable for submission to the challenge organizers.
|
|
681
|
-
|
|
682
|
-
Before creating the export, the command validates the submission and
|
|
683
|
-
displays any warnings. If validation issues are found, the user is
|
|
684
|
-
prompted to continue or cancel the export (unless --force is used).
|
|
685
|
-
|
|
686
|
-
Args:
|
|
687
|
-
output_path: Path where the zip archive will be created.
|
|
688
|
-
force: If True, skip validation prompts and continue with export.
|
|
689
|
-
project_path: Directory of the submission project.
|
|
690
|
-
|
|
691
|
-
Raises:
|
|
692
|
-
typer.Exit: If validation fails and user cancels export.
|
|
693
|
-
ValueError: If referenced files (plots, posterior data) don't exist.
|
|
694
|
-
OSError: If unable to create the zip file.
|
|
695
|
-
|
|
696
|
-
Example:
|
|
697
|
-
# Export with validation prompts
|
|
698
|
-
microlens-submit export submission.zip ./project
|
|
699
|
-
|
|
700
|
-
# Force export without prompts
|
|
701
|
-
microlens-submit export submission.zip --force ./project
|
|
702
|
-
|
|
703
|
-
# Export to specific directory
|
|
704
|
-
microlens-submit export /path/to/submissions/my_submission.zip ./project
|
|
705
|
-
|
|
706
|
-
Note:
|
|
707
|
-
Only active solutions are included in the export. Inactive solutions
|
|
708
|
-
are excluded even if they exist in the project. The export includes:
|
|
709
|
-
- submission.json with metadata
|
|
710
|
-
- All active solutions with parameters
|
|
711
|
-
- Notes files for each solution
|
|
712
|
-
- Referenced files (plots, posterior data)
|
|
713
|
-
|
|
714
|
-
Relative probabilities are automatically calculated for solutions
|
|
715
|
-
that don't have them set, using BIC if sufficient data is available.
|
|
716
|
-
"""
|
|
717
|
-
sub = load(str(project_path))
|
|
718
|
-
warnings = sub.run_validation()
|
|
719
|
-
if warnings:
|
|
720
|
-
console.print(Panel("Validation Warnings", style="yellow"))
|
|
721
|
-
for w in warnings:
|
|
722
|
-
console.print(f"- {w}")
|
|
723
|
-
if not force:
|
|
724
|
-
if not typer.confirm("Continue with export?"):
|
|
725
|
-
console.print("Export cancelled", style="bold red")
|
|
726
|
-
raise typer.Exit()
|
|
727
|
-
sub.export(str(output_path))
|
|
728
|
-
console.print(Panel(f"Exported submission to {output_path}", style="bold green"))
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
@app.command("generate-dossier")
|
|
732
|
-
def generate_dossier(
|
|
733
|
-
project_path: Path = typer.Argument(Path("."), help="Project directory"),
|
|
734
|
-
event_id: Optional[str] = typer.Option(None, "--event-id", help="Generate dossier for a specific event only (omit for full dossier)"),
|
|
735
|
-
solution_id: Optional[str] = typer.Option(None, "--solution-id", help="Generate dossier for a specific solution only (omit for full dossier)"),
|
|
736
|
-
) -> None:
|
|
737
|
-
"""Generate an HTML dossier for the submission.
|
|
738
|
-
|
|
739
|
-
Creates a comprehensive HTML dashboard that provides an overview of the submission,
|
|
740
|
-
including event summaries, solution statistics, and metadata. The dossier is saved
|
|
741
|
-
to the `dossier/` subdirectory of the project directory with the main dashboard as index.html.
|
|
742
|
-
|
|
743
|
-
The dossier includes:
|
|
744
|
-
- Main dashboard (index.html) with submission overview and statistics
|
|
745
|
-
- Individual event pages for each event with solution tables
|
|
746
|
-
- Individual solution pages with parameters, notes, and metadata
|
|
747
|
-
- Full comprehensive dossier (full_dossier_report.html) for printing
|
|
748
|
-
|
|
749
|
-
All pages use Tailwind CSS for styling and include syntax highlighting for
|
|
750
|
-
code blocks in participant notes.
|
|
751
|
-
|
|
752
|
-
Args:
|
|
753
|
-
project_path: Directory of the submission project.
|
|
754
|
-
event_id: If specified, only generate dossier for this event.
|
|
755
|
-
solution_id: If specified, only generate dossier for this solution.
|
|
756
|
-
|
|
757
|
-
Raises:
|
|
758
|
-
OSError: If unable to create output directory or write files.
|
|
759
|
-
ValueError: If submission data is invalid or missing required fields.
|
|
760
|
-
|
|
761
|
-
Example:
|
|
762
|
-
# Generate complete dossier for all events and solutions
|
|
763
|
-
microlens-submit generate-dossier ./project
|
|
764
|
-
|
|
765
|
-
# Generate dossier for specific event only
|
|
766
|
-
microlens-submit generate-dossier --event-id EVENT001 ./project
|
|
767
|
-
|
|
768
|
-
# Generate dossier for specific solution only
|
|
769
|
-
microlens-submit generate-dossier --solution-id abc12345-def6-7890-ghij-klmnopqrstuv ./project
|
|
770
|
-
|
|
771
|
-
# Files created:
|
|
772
|
-
# ./project/dossier/
|
|
773
|
-
# ├── index.html (main dashboard)
|
|
774
|
-
# ├── full_dossier_report.html (printable version)
|
|
775
|
-
# ├── EVENT001.html (event page)
|
|
776
|
-
# ├── solution_id.html (solution pages)
|
|
777
|
-
# └── assets/ (logos and icons)
|
|
778
|
-
|
|
779
|
-
Note:
|
|
780
|
-
The dossier is generated in the dossier/ subdirectory of the project.
|
|
781
|
-
The main dashboard provides navigation to individual event and solution pages.
|
|
782
|
-
The full dossier report combines all content into a single printable document.
|
|
783
|
-
GitHub repository links are included if available in the submission metadata.
|
|
784
|
-
"""
|
|
785
|
-
sub = load(str(project_path))
|
|
786
|
-
output_dir = Path(project_path) / "dossier"
|
|
787
|
-
# Always generate dashboard (even if partial)
|
|
788
|
-
from .dossier import _generate_full_dossier_report_html
|
|
789
|
-
generate_dashboard_html(sub, output_dir)
|
|
790
|
-
|
|
791
|
-
# Determine if it's a full generation (no specific event/solution requested)
|
|
792
|
-
is_full_generation = not event_id and not solution_id
|
|
793
|
-
|
|
794
|
-
# Generate event/solution pages as needed (for now, always generate all, but this is where you'd restrict)
|
|
795
|
-
# TODO: In future, restrict event/solution generation if flags are set
|
|
796
|
-
|
|
797
|
-
if is_full_generation:
|
|
798
|
-
console.print(Panel("Generating comprehensive printable dossier...", style="cyan"))
|
|
799
|
-
_generate_full_dossier_report_html(sub, output_dir)
|
|
800
|
-
# Replace placeholder in index.html with the real link
|
|
801
|
-
dashboard_path = output_dir / "index.html"
|
|
802
|
-
if dashboard_path.exists():
|
|
803
|
-
with dashboard_path.open("r", encoding="utf-8") as f:
|
|
804
|
-
dashboard_html = f.read()
|
|
805
|
-
dashboard_html = dashboard_html.replace(
|
|
806
|
-
"<!--FULL_DOSSIER_LINK_PLACEHOLDER-->",
|
|
807
|
-
'<div class="text-center"><a href="./full_dossier_report.html" class="inline-block bg-rtd-accent text-white py-3 px-6 rounded-lg shadow-md hover:bg-rtd-secondary transition-colors duration-200 text-lg font-semibold mt-8">View Full Comprehensive Dossier (Printable)</a></div>'
|
|
808
|
-
)
|
|
809
|
-
with dashboard_path.open("w", encoding="utf-8") as f:
|
|
810
|
-
f.write(dashboard_html)
|
|
811
|
-
console.print(Panel("Comprehensive dossier generated!", style="bold green"))
|
|
812
|
-
|
|
813
|
-
console.print(Panel(f"Dossier generated successfully at {output_dir / 'index.html'}", style="bold green"))
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
@app.command("list-solutions")
|
|
817
|
-
def list_solutions(
|
|
818
|
-
event_id: str,
|
|
819
|
-
project_path: Path = typer.Argument(Path("."), help="Project directory"),
|
|
820
|
-
) -> None:
|
|
821
|
-
"""Display a table of solutions for a specific event.
|
|
822
|
-
|
|
823
|
-
Shows a formatted table containing all solutions for the specified event,
|
|
824
|
-
including their model types, active status, and notes snippets. The table
|
|
825
|
-
is displayed using rich formatting with color-coded status indicators.
|
|
826
|
-
|
|
827
|
-
Args:
|
|
828
|
-
event_id: Identifier of the event to list solutions for.
|
|
829
|
-
project_path: Directory of the submission project.
|
|
830
|
-
|
|
831
|
-
Raises:
|
|
832
|
-
typer.Exit: If the event is not found in the project.
|
|
833
|
-
|
|
834
|
-
Example:
|
|
835
|
-
# List all solutions for EVENT001
|
|
836
|
-
microlens-submit list-solutions EVENT001 ./project
|
|
837
|
-
|
|
838
|
-
# Output shows:
|
|
839
|
-
# ┌─────────────────────────────────────────────────────────┬──────────┬────────┬─────────────────┐
|
|
840
|
-
# │ Solution ID │ Model │ Status │ Notes │
|
|
841
|
-
# │ │ Type │ │ │
|
|
842
|
-
# ├─────────────────────────────────────────────────────────┼──────────┼────────┼─────────────────┤
|
|
843
|
-
# │ abc12345-def6-7890-ghij-klmnopqrstuv │ 1S1L │ Active │ Simple point... │
|
|
844
|
-
# │ def67890-abc1-2345-klmn-opqrstuvwxyz │ 1S2L │ Active │ Binary lens... │
|
|
845
|
-
# └─────────────────────────────────────────────────────────┴──────────┴────────┴─────────────────┘
|
|
846
|
-
|
|
847
|
-
Note:
|
|
848
|
-
The table shows both active and inactive solutions. Active solutions
|
|
849
|
-
are marked in green, inactive solutions in red. Notes are truncated
|
|
850
|
-
to fit the table display. Use the solution ID to reference specific
|
|
851
|
-
solutions in other commands.
|
|
852
|
-
"""
|
|
853
|
-
sub = load(str(project_path))
|
|
854
|
-
if event_id not in sub.events:
|
|
855
|
-
console.print(f"Event {event_id} not found", style="bold red")
|
|
856
|
-
raise typer.Exit(code=1)
|
|
857
|
-
evt = sub.events[event_id]
|
|
858
|
-
table = Table(title=f"Solutions for {event_id}")
|
|
859
|
-
table.add_column("Solution ID")
|
|
860
|
-
table.add_column("Model Type")
|
|
861
|
-
table.add_column("Status")
|
|
862
|
-
table.add_column("Notes")
|
|
863
|
-
for sol in evt.solutions.values():
|
|
864
|
-
status = "[green]Active[/green]" if sol.is_active else "[red]Inactive[/red]"
|
|
865
|
-
table.add_row(sol.solution_id, sol.model_type, status, sol.notes)
|
|
866
|
-
console.print(table)
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
@app.command("compare-solutions")
|
|
870
|
-
def compare_solutions(
|
|
871
|
-
event_id: str,
|
|
872
|
-
project_path: Path = typer.Argument(Path("."), help="Project directory"),
|
|
873
|
-
) -> None:
|
|
874
|
-
"""Rank active solutions for an event using the Bayesian Information Criterion.
|
|
875
|
-
|
|
876
|
-
Compares all active solutions for the specified event using BIC to rank
|
|
877
|
-
them by relative probability. The BIC is calculated as:
|
|
878
|
-
|
|
879
|
-
BIC = k * ln(n) - 2 * ln(L)
|
|
880
|
-
|
|
881
|
-
where k is the number of parameters, n is the number of data points,
|
|
882
|
-
and L is the likelihood. Lower BIC values indicate better models.
|
|
883
|
-
|
|
884
|
-
The command displays a table with solution rankings and automatically
|
|
885
|
-
calculates relative probabilities for solutions that don't have them set.
|
|
886
|
-
|
|
887
|
-
Args:
|
|
888
|
-
event_id: Identifier of the event to compare solutions for.
|
|
889
|
-
project_path: Directory of the submission project.
|
|
890
|
-
|
|
891
|
-
Raises:
|
|
892
|
-
typer.Exit: If the event is not found in the project.
|
|
893
|
-
|
|
894
|
-
Example:
|
|
895
|
-
# Compare solutions for EVENT001
|
|
896
|
-
microlens-submit compare-solutions EVENT001 ./project
|
|
897
|
-
|
|
898
|
-
# Output shows:
|
|
899
|
-
# ┌─────────────────────────────────────────────────────────┬──────────┬─────────────────────┬─────────┬─────────────────┬─────────┬─────────────────┐
|
|
900
|
-
# │ Solution ID │ Model │ Higher-Order │ # Params│ Log-Likelihood │ BIC │ Relative Prob │
|
|
901
|
-
# │ │ Type │ Effects │ (k) │ │ │ │
|
|
902
|
-
# ├─────────────────────────────────────────────────────────┼──────────┼─────────────────────┼─────────┼─────────────────┼─────────┼─────────────────┤
|
|
903
|
-
# │ abc12345-def6-7890-ghij-klmnopqrstuv │ 1S1L │ - │ 3 │ -1234.56 │ 2475.12 │ 0.600 │
|
|
904
|
-
# │ def67890-abc1-2345-klmn-opqrstuvwxyz │ 1S2L │ parallax,finite-... │ 6 │ -1189.34 │ 2394.68 │ 0.400 │
|
|
905
|
-
# └─────────────────────────────────────────────────────────┴──────────┴─────────────────────┴─────────┴─────────────────┴─────────┴─────────────────┘
|
|
906
|
-
|
|
907
|
-
Note:
|
|
908
|
-
Only active solutions with valid log_likelihood and n_data_points
|
|
909
|
-
are included in the comparison. Solutions missing these values are
|
|
910
|
-
skipped with a warning. Relative probabilities are automatically
|
|
911
|
-
calculated using BIC if not already set.
|
|
912
|
-
"""
|
|
913
|
-
sub = load(str(project_path))
|
|
914
|
-
if event_id not in sub.events:
|
|
915
|
-
console.print(f"Event {event_id} not found", style="bold red")
|
|
916
|
-
raise typer.Exit(code=1)
|
|
917
|
-
|
|
918
|
-
evt = sub.events[event_id]
|
|
919
|
-
solutions = []
|
|
920
|
-
for s in evt.get_active_solutions():
|
|
921
|
-
if s.log_likelihood is None or s.n_data_points is None:
|
|
922
|
-
continue
|
|
923
|
-
if s.n_data_points <= 0:
|
|
924
|
-
console.print(
|
|
925
|
-
f"Skipping {s.solution_id}: n_data_points <= 0",
|
|
926
|
-
style="bold red",
|
|
927
|
-
)
|
|
928
|
-
continue
|
|
929
|
-
solutions.append(s)
|
|
930
|
-
|
|
931
|
-
table = Table(title=f"Solution Comparison for {event_id}")
|
|
932
|
-
table.add_column("Solution ID")
|
|
933
|
-
table.add_column("Model Type")
|
|
934
|
-
table.add_column("Higher-Order Effects")
|
|
935
|
-
table.add_column("# Params (k)")
|
|
936
|
-
table.add_column("Log-Likelihood")
|
|
937
|
-
table.add_column("BIC")
|
|
938
|
-
table.add_column("Relative Prob")
|
|
939
|
-
|
|
940
|
-
rel_prob_map: dict[str, float] = {}
|
|
941
|
-
note = None
|
|
942
|
-
if solutions:
|
|
943
|
-
provided_sum = sum(
|
|
944
|
-
s.relative_probability or 0.0
|
|
945
|
-
for s in solutions
|
|
946
|
-
if s.relative_probability is not None
|
|
947
|
-
)
|
|
948
|
-
need_calc = [s for s in solutions if s.relative_probability is None]
|
|
949
|
-
if need_calc:
|
|
950
|
-
can_calc = all(
|
|
951
|
-
s.log_likelihood is not None
|
|
952
|
-
and s.n_data_points
|
|
953
|
-
and s.n_data_points > 0
|
|
954
|
-
and len(s.parameters) > 0
|
|
955
|
-
for s in need_calc
|
|
956
|
-
)
|
|
957
|
-
remaining = max(1.0 - provided_sum, 0.0)
|
|
958
|
-
if can_calc:
|
|
959
|
-
bic_vals = {
|
|
960
|
-
s.solution_id: len(s.parameters) * math.log(s.n_data_points)
|
|
961
|
-
- 2 * s.log_likelihood
|
|
962
|
-
for s in need_calc
|
|
963
|
-
}
|
|
964
|
-
bic_min = min(bic_vals.values())
|
|
965
|
-
weights = {
|
|
966
|
-
sid: math.exp(-0.5 * (bic - bic_min))
|
|
967
|
-
for sid, bic in bic_vals.items()
|
|
968
|
-
}
|
|
969
|
-
wsum = sum(weights.values())
|
|
970
|
-
for sid, w in weights.items():
|
|
971
|
-
rel_prob_map[sid] = (
|
|
972
|
-
remaining * w / wsum if wsum > 0 else remaining / len(weights)
|
|
973
|
-
)
|
|
974
|
-
note = "Relative probabilities calculated using BIC"
|
|
975
|
-
else:
|
|
976
|
-
eq = remaining / len(need_calc) if need_calc else 0.0
|
|
977
|
-
for s in need_calc:
|
|
978
|
-
rel_prob_map[s.solution_id] = eq
|
|
979
|
-
note = "Relative probabilities set equal due to missing data"
|
|
980
|
-
|
|
981
|
-
rows = []
|
|
982
|
-
for sol in solutions:
|
|
983
|
-
k = len(sol.parameters)
|
|
984
|
-
bic = k * math.log(sol.n_data_points) - 2 * sol.log_likelihood
|
|
985
|
-
rp = (
|
|
986
|
-
sol.relative_probability
|
|
987
|
-
if sol.relative_probability is not None
|
|
988
|
-
else rel_prob_map.get(sol.solution_id)
|
|
989
|
-
)
|
|
990
|
-
rows.append(
|
|
991
|
-
(
|
|
992
|
-
bic,
|
|
993
|
-
[
|
|
994
|
-
sol.solution_id,
|
|
995
|
-
sol.model_type,
|
|
996
|
-
(
|
|
997
|
-
",".join(sol.higher_order_effects)
|
|
998
|
-
if sol.higher_order_effects
|
|
999
|
-
else "-"
|
|
1000
|
-
),
|
|
1001
|
-
str(k),
|
|
1002
|
-
f"{sol.log_likelihood:.2f}",
|
|
1003
|
-
f"{bic:.2f}",
|
|
1004
|
-
f"{rp:.3f}" if rp is not None else "N/A",
|
|
1005
|
-
],
|
|
1006
|
-
)
|
|
1007
|
-
)
|
|
1008
|
-
|
|
1009
|
-
for _, cols in sorted(rows, key=lambda x: x[0]):
|
|
1010
|
-
table.add_row(*cols)
|
|
1011
|
-
|
|
1012
|
-
console.print(table)
|
|
1013
|
-
if note:
|
|
1014
|
-
console.print(note, style="yellow")
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
@app.command("validate-solution")
|
|
1018
|
-
def validate_solution(
|
|
1019
|
-
solution_id: str,
|
|
1020
|
-
project_path: Path = typer.Argument(Path("."), help="Project directory"),
|
|
1021
|
-
) -> None:
|
|
1022
|
-
"""Validate a specific solution's parameters and configuration.
|
|
1023
|
-
|
|
1024
|
-
This command uses the centralized validation logic to check:
|
|
1025
|
-
- Parameter completeness for the model type
|
|
1026
|
-
- Higher-order effect requirements
|
|
1027
|
-
- Parameter types and value ranges
|
|
1028
|
-
- Physical consistency of parameters
|
|
1029
|
-
|
|
1030
|
-
The validation provides detailed feedback about any issues found,
|
|
1031
|
-
helping ensure solutions are complete and ready for submission.
|
|
1032
|
-
|
|
1033
|
-
Args:
|
|
1034
|
-
solution_id: The unique identifier of the solution to validate.
|
|
1035
|
-
project_path: Directory of the submission project.
|
|
1036
|
-
|
|
1037
|
-
Raises:
|
|
1038
|
-
typer.Exit: If the solution is not found in any event.
|
|
1039
|
-
|
|
1040
|
-
Example:
|
|
1041
|
-
# Validate a specific solution
|
|
1042
|
-
microlens-submit validate-solution abc12345-def6-7890-ghij-klmnopqrstuv ./project
|
|
1043
|
-
|
|
1044
|
-
# Output shows:
|
|
1045
|
-
# ✅ All validations passed for abc12345-def6-7890-ghij-klmnopqrstuv (event EVENT001)
|
|
1046
|
-
|
|
1047
|
-
# Or if issues are found:
|
|
1048
|
-
# ┌─────────────────────────────────────────────────────────────────────────────┐
|
|
1049
|
-
# │ Validation Results for abc12345-def6-7890-ghij-klmnopqrstuv (event EVENT001) │
|
|
1050
|
-
# └─────────────────────────────────────────────────────────────────────────────┘
|
|
1051
|
-
# • Missing required parameter 'tE' for model type '1S1L'
|
|
1052
|
-
# • Parameter 'u0' has invalid value: -0.5 (must be positive)
|
|
1053
|
-
|
|
1054
|
-
Note:
|
|
1055
|
-
The validation checks are comprehensive and cover all model types
|
|
1056
|
-
and higher-order effects. Always validate solutions before submission
|
|
1057
|
-
to catch any issues early.
|
|
1058
|
-
"""
|
|
1059
|
-
sub = load(str(project_path))
|
|
1060
|
-
|
|
1061
|
-
# Find the solution
|
|
1062
|
-
target_solution = None
|
|
1063
|
-
target_event_id = None
|
|
1064
|
-
for event_id, event in sub.events.items():
|
|
1065
|
-
if solution_id in event.solutions:
|
|
1066
|
-
target_solution = event.solutions[solution_id]
|
|
1067
|
-
target_event_id = event_id
|
|
1068
|
-
break
|
|
1069
|
-
|
|
1070
|
-
if target_solution is None:
|
|
1071
|
-
console.print(f"Solution {solution_id} not found", style="bold red")
|
|
1072
|
-
raise typer.Exit(code=1)
|
|
1073
|
-
|
|
1074
|
-
# Run validation
|
|
1075
|
-
messages = target_solution.run_validation()
|
|
1076
|
-
|
|
1077
|
-
if not messages:
|
|
1078
|
-
console.print(
|
|
1079
|
-
Panel(
|
|
1080
|
-
f"✅ All validations passed for {solution_id} (event {target_event_id})",
|
|
1081
|
-
style="bold green",
|
|
1082
|
-
)
|
|
1083
|
-
)
|
|
1084
|
-
else:
|
|
1085
|
-
console.print(
|
|
1086
|
-
Panel(
|
|
1087
|
-
f"Validation Results for {solution_id} (event {target_event_id})",
|
|
1088
|
-
style="yellow",
|
|
1089
|
-
)
|
|
1090
|
-
)
|
|
1091
|
-
for msg in messages:
|
|
1092
|
-
console.print(f" • {msg}")
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
@app.command("validate-submission")
|
|
1096
|
-
def validate_submission(
|
|
1097
|
-
project_path: Path = typer.Argument(Path("."), help="Project directory"),
|
|
1098
|
-
) -> None:
|
|
1099
|
-
"""Validate the entire submission for missing or incomplete information.
|
|
1100
|
-
|
|
1101
|
-
This command performs comprehensive validation of all active solutions
|
|
1102
|
-
and returns a list of warnings describing potential issues. It checks
|
|
1103
|
-
for common problems like missing metadata, incomplete solutions, and
|
|
1104
|
-
validation issues in individual solutions.
|
|
1105
|
-
|
|
1106
|
-
The validation is particularly strict about the GitHub repository URL,
|
|
1107
|
-
which is required for submission. If repo_url is missing or invalid,
|
|
1108
|
-
the command will exit with an error.
|
|
1109
|
-
|
|
1110
|
-
Args:
|
|
1111
|
-
project_path: Directory of the submission project.
|
|
1112
|
-
|
|
1113
|
-
Raises:
|
|
1114
|
-
typer.Exit: If repo_url is missing or invalid (exit code 1).
|
|
1115
|
-
|
|
1116
|
-
Example:
|
|
1117
|
-
# Validate the entire submission
|
|
1118
|
-
microlens-submit validate-submission ./project
|
|
1119
|
-
|
|
1120
|
-
# Output if all validations pass:
|
|
1121
|
-
# ┌─────────────────────────────────────┐
|
|
1122
|
-
# │ ✅ All validations passed! │
|
|
1123
|
-
# └─────────────────────────────────────┘
|
|
1124
|
-
|
|
1125
|
-
# Output if issues are found:
|
|
1126
|
-
# ┌─────────────────────────────────────┐
|
|
1127
|
-
# │ Validation Warnings │
|
|
1128
|
-
# └─────────────────────────────────────┘
|
|
1129
|
-
# • Hardware info is missing
|
|
1130
|
-
# • Event EVENT001: Solution abc12345 is missing log_likelihood
|
|
1131
|
-
# • Solution def67890 in event EVENT002: Missing required parameter 'tE'
|
|
1132
|
-
|
|
1133
|
-
Note:
|
|
1134
|
-
This command checks for:
|
|
1135
|
-
- Missing or invalid repo_url (GitHub repository URL)
|
|
1136
|
-
- Missing hardware information
|
|
1137
|
-
- Events with no active solutions
|
|
1138
|
-
- Solutions with missing required metadata
|
|
1139
|
-
- Individual solution validation issues
|
|
1140
|
-
- Relative probability consistency
|
|
1141
|
-
|
|
1142
|
-
Always run this command before exporting your submission to ensure
|
|
1143
|
-
all required information is present and valid.
|
|
1144
|
-
"""
|
|
1145
|
-
sub = load(str(project_path))
|
|
1146
|
-
warnings = sub.run_validation()
|
|
1147
|
-
|
|
1148
|
-
# Check for missing repo_url
|
|
1149
|
-
repo_url_warning = next((w for w in warnings if 'repo_url' in w.lower() or 'github' in w.lower()), None)
|
|
1150
|
-
if repo_url_warning:
|
|
1151
|
-
console.print(Panel(f"[red]Error: {repo_url_warning}\nPlease add your GitHub repository URL using 'microlens-submit set-repo-url <url> <project_dir>'.[/red]", style="bold red"))
|
|
1152
|
-
raise typer.Exit(code=1)
|
|
1153
|
-
|
|
1154
|
-
if not warnings:
|
|
1155
|
-
console.print(Panel("\u2705 All validations passed!", style="bold green"))
|
|
1156
|
-
else:
|
|
1157
|
-
console.print(Panel("Validation Warnings", style="yellow"))
|
|
1158
|
-
for warning in warnings:
|
|
1159
|
-
console.print(f" \u2022 {warning}")
|
|
1160
|
-
console.print(f"\nFound {len(warnings)} validation issue(s)", style="yellow")
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
@app.command("validate-event")
|
|
1164
|
-
def validate_event(
|
|
1165
|
-
event_id: str,
|
|
1166
|
-
project_path: Path = typer.Argument(Path("."), help="Project directory"),
|
|
1167
|
-
) -> None:
|
|
1168
|
-
"""Validate all solutions for a specific event.
|
|
1169
|
-
|
|
1170
|
-
Runs validation on all solutions (both active and inactive) for the
|
|
1171
|
-
specified event. This provides a focused validation check for a single
|
|
1172
|
-
event, useful when working on specific events or debugging issues.
|
|
1173
|
-
|
|
1174
|
-
Args:
|
|
1175
|
-
event_id: Identifier of the event to validate.
|
|
1176
|
-
project_path: Directory of the submission project.
|
|
1177
|
-
|
|
1178
|
-
Raises:
|
|
1179
|
-
typer.Exit: If the event is not found in the project.
|
|
1180
|
-
|
|
1181
|
-
Example:
|
|
1182
|
-
# Validate all solutions for EVENT001
|
|
1183
|
-
microlens-submit validate-event EVENT001 ./project
|
|
1184
|
-
|
|
1185
|
-
# Output shows:
|
|
1186
|
-
# ┌─────────────────────────────────────┐
|
|
1187
|
-
# │ Validating Event: EVENT001 │
|
|
1188
|
-
# └─────────────────────────────────────┘
|
|
1189
|
-
#
|
|
1190
|
-
# Solution abc12345-def6-7890-ghij-klmnopqrstuv:
|
|
1191
|
-
# • Missing required parameter 'tE' for model type '1S1L'
|
|
1192
|
-
#
|
|
1193
|
-
# ✅ Solution def67890-abc1-2345-klmn-opqrstuvwxyz: All validations passed
|
|
1194
|
-
|
|
1195
|
-
# ┌─────────────────────────────────────┐
|
|
1196
|
-
# │ ✅ All solutions passed validation! │
|
|
1197
|
-
# └─────────────────────────────────────┘
|
|
1198
|
-
|
|
1199
|
-
Note:
|
|
1200
|
-
This command validates all solutions in the event, regardless of
|
|
1201
|
-
their active status. It's useful for checking solutions that might
|
|
1202
|
-
be inactive but could be reactivated later.
|
|
1203
|
-
"""
|
|
1204
|
-
sub = load(str(project_path))
|
|
1205
|
-
|
|
1206
|
-
if event_id not in sub.events:
|
|
1207
|
-
console.print(f"Event {event_id} not found", style="bold red")
|
|
1208
|
-
raise typer.Exit(code=1)
|
|
1209
|
-
|
|
1210
|
-
event = sub.events[event_id]
|
|
1211
|
-
all_messages = []
|
|
1212
|
-
|
|
1213
|
-
console.print(Panel(f"Validating Event: {event_id}", style="cyan"))
|
|
1214
|
-
|
|
1215
|
-
for solution in event.solutions.values():
|
|
1216
|
-
messages = solution.run_validation()
|
|
1217
|
-
if messages:
|
|
1218
|
-
console.print(f"\n[bold]Solution {solution.solution_id}:[/bold]")
|
|
1219
|
-
for msg in messages:
|
|
1220
|
-
console.print(f" • {msg}")
|
|
1221
|
-
all_messages.append(f"{solution.solution_id}: {msg}")
|
|
1222
|
-
else:
|
|
1223
|
-
console.print(f"✅ Solution {solution.solution_id}: All validations passed")
|
|
1224
|
-
|
|
1225
|
-
if not all_messages:
|
|
1226
|
-
console.print(Panel("✅ All solutions passed validation!", style="bold green"))
|
|
1227
|
-
else:
|
|
1228
|
-
console.print(
|
|
1229
|
-
f"\nFound {len(all_messages)} validation issue(s) across all solutions",
|
|
1230
|
-
style="yellow",
|
|
1231
|
-
)
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
def _parse_structured_params_file(params_file: Path) -> tuple[dict, dict]:
|
|
1235
|
-
"""Parse a structured parameter file that can contain both parameters and uncertainties.
|
|
1236
|
-
|
|
1237
|
-
Supports both JSON and YAML formats. The file can have either:
|
|
1238
|
-
1. Simple format: {"param1": value1, "param2": value2, ...}
|
|
1239
|
-
2. Structured format: {"parameters": {...}, "uncertainties": {...}}
|
|
1240
|
-
|
|
1241
|
-
Args:
|
|
1242
|
-
params_file: Path to the parameter file (JSON or YAML format).
|
|
1243
|
-
|
|
1244
|
-
Returns:
|
|
1245
|
-
tuple: (parameters_dict, uncertainties_dict) - Two dictionaries containing
|
|
1246
|
-
the parsed parameters and their uncertainties.
|
|
1247
|
-
|
|
1248
|
-
Raises:
|
|
1249
|
-
OSError: If the file cannot be read.
|
|
1250
|
-
json.JSONDecodeError: If JSON parsing fails.
|
|
1251
|
-
yaml.YAMLError: If YAML parsing fails.
|
|
1252
|
-
|
|
1253
|
-
Example:
|
|
1254
|
-
# Simple format (all keys are parameters)
|
|
1255
|
-
# params.json:
|
|
1256
|
-
# {
|
|
1257
|
-
# "t0": 2459123.5,
|
|
1258
|
-
# "u0": 0.1,
|
|
1259
|
-
# "tE": 20.0
|
|
1260
|
-
# }
|
|
1261
|
-
|
|
1262
|
-
# Structured format (separate parameters and uncertainties)
|
|
1263
|
-
# params.yaml:
|
|
1264
|
-
# parameters:
|
|
1265
|
-
# t0: 2459123.5
|
|
1266
|
-
# u0: 0.1
|
|
1267
|
-
# tE: 20.0
|
|
1268
|
-
# uncertainties:
|
|
1269
|
-
# t0: 0.1
|
|
1270
|
-
# u0: 0.01
|
|
1271
|
-
# tE: 0.5
|
|
1272
|
-
|
|
1273
|
-
params, uncertainties = _parse_structured_params_file(Path("params.json"))
|
|
1274
|
-
|
|
1275
|
-
Note:
|
|
1276
|
-
This function automatically detects the file format based on the file
|
|
1277
|
-
extension (.json, .yaml, .yml). For structured format, both parameters
|
|
1278
|
-
and uncertainties sections are optional - missing sections return empty
|
|
1279
|
-
dictionaries.
|
|
1280
|
-
"""
|
|
1281
|
-
import yaml
|
|
1282
|
-
|
|
1283
|
-
with params_file.open("r", encoding="utf-8") as fh:
|
|
1284
|
-
if params_file.suffix.lower() in ['.yaml', '.yml']:
|
|
1285
|
-
data = yaml.safe_load(fh)
|
|
1286
|
-
else:
|
|
1287
|
-
data = json.load(fh)
|
|
1288
|
-
|
|
1289
|
-
# Handle structured format
|
|
1290
|
-
if isinstance(data, dict) and ('parameters' in data or 'uncertainties' in data):
|
|
1291
|
-
parameters = data.get('parameters', {})
|
|
1292
|
-
uncertainties = data.get('uncertainties', {})
|
|
1293
|
-
else:
|
|
1294
|
-
# Simple format - all keys are parameters
|
|
1295
|
-
parameters = data
|
|
1296
|
-
uncertainties = {}
|
|
1297
|
-
|
|
1298
|
-
return parameters, uncertainties
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
@app.command("edit-solution")
|
|
1302
|
-
def edit_solution(
|
|
1303
|
-
solution_id: str,
|
|
1304
|
-
relative_probability: Optional[float] = typer.Option(
|
|
1305
|
-
None,
|
|
1306
|
-
"--relative-probability",
|
|
1307
|
-
help="Relative probability of this solution",
|
|
1308
|
-
),
|
|
1309
|
-
log_likelihood: Optional[float] = typer.Option(None, help="Log likelihood"),
|
|
1310
|
-
n_data_points: Optional[int] = typer.Option(
|
|
1311
|
-
None,
|
|
1312
|
-
"--n-data-points",
|
|
1313
|
-
help="Number of data points used in this solution",
|
|
1314
|
-
),
|
|
1315
|
-
notes: Optional[str] = typer.Option(None, help="Notes for the solution (supports Markdown formatting)"),
|
|
1316
|
-
notes_file: Optional[Path] = typer.Option(None, "--notes-file", help="Path to a Markdown file for solution notes (mutually exclusive with --notes)"),
|
|
1317
|
-
append_notes: Optional[str] = typer.Option(
|
|
1318
|
-
None,
|
|
1319
|
-
"--append-notes",
|
|
1320
|
-
help="Append text to existing notes (use --notes to replace instead)",
|
|
1321
|
-
),
|
|
1322
|
-
clear_notes: bool = typer.Option(False, help="Clear all notes"),
|
|
1323
|
-
clear_relative_probability: bool = typer.Option(False, help="Clear relative probability"),
|
|
1324
|
-
clear_log_likelihood: bool = typer.Option(False, help="Clear log likelihood"),
|
|
1325
|
-
clear_n_data_points: bool = typer.Option(False, help="Clear n_data_points"),
|
|
1326
|
-
clear_parameter_uncertainties: bool = typer.Option(False, help="Clear parameter uncertainties"),
|
|
1327
|
-
clear_physical_parameters: bool = typer.Option(False, help="Clear physical parameters"),
|
|
1328
|
-
cpu_hours: Optional[float] = typer.Option(None, help="CPU hours used"),
|
|
1329
|
-
wall_time_hours: Optional[float] = typer.Option(None, help="Wall time hours used"),
|
|
1330
|
-
param: Optional[List[str]] = typer.Option(
|
|
1331
|
-
None, help="Model parameters as key=value (updates existing parameters)"
|
|
1332
|
-
),
|
|
1333
|
-
param_uncertainty: Optional[List[str]] = typer.Option(
|
|
1334
|
-
None,
|
|
1335
|
-
"--param-uncertainty",
|
|
1336
|
-
help="Parameter uncertainties as key=value (updates existing uncertainties)"
|
|
1337
|
-
),
|
|
1338
|
-
higher_order_effect: Optional[List[str]] = typer.Option(
|
|
1339
|
-
None,
|
|
1340
|
-
"--higher-order-effect",
|
|
1341
|
-
help="Higher-order effects (replaces existing effects)",
|
|
1342
|
-
),
|
|
1343
|
-
clear_higher_order_effects: bool = typer.Option(False, help="Clear all higher-order effects"),
|
|
1344
|
-
dry_run: bool = typer.Option(
|
|
1345
|
-
False,
|
|
1346
|
-
"--dry-run",
|
|
1347
|
-
help="Show what would be changed without saving",
|
|
1348
|
-
),
|
|
1349
|
-
project_path: Path = typer.Argument(Path("."), help="Project directory"),
|
|
1350
|
-
) -> None:
|
|
1351
|
-
"""Edit an existing solution's attributes, including file-based notes.
|
|
1352
|
-
|
|
1353
|
-
This command allows you to modify various attributes of an existing solution
|
|
1354
|
-
without having to recreate it. It supports updating parameters, metadata,
|
|
1355
|
-
notes, and compute information. The command provides detailed feedback about
|
|
1356
|
-
what changes were made.
|
|
1357
|
-
|
|
1358
|
-
**Notes Management:**
|
|
1359
|
-
- Use --notes to replace existing notes with new content
|
|
1360
|
-
- Use --append-notes to add text to existing notes
|
|
1361
|
-
- Use --notes-file to reference an external Markdown file
|
|
1362
|
-
- Use --clear-notes to remove all notes
|
|
1363
|
-
|
|
1364
|
-
**Parameter Updates:**
|
|
1365
|
-
- Use --param to update individual model parameters
|
|
1366
|
-
- Use --param-uncertainty to update parameter uncertainties
|
|
1367
|
-
- Use --higher-order-effect to replace all higher-order effects
|
|
1368
|
-
|
|
1369
|
-
**Metadata Updates:**
|
|
1370
|
-
- Use --relative-probability, --log-likelihood, --n-data-points
|
|
1371
|
-
- Use --cpu-hours and --wall-time-hours to update compute info
|
|
1372
|
-
- Use --clear-* flags to remove specific fields
|
|
1373
|
-
|
|
1374
|
-
Args:
|
|
1375
|
-
solution_id: The unique identifier of the solution to edit.
|
|
1376
|
-
relative_probability: New relative probability value.
|
|
1377
|
-
log_likelihood: New log-likelihood value.
|
|
1378
|
-
n_data_points: New number of data points value.
|
|
1379
|
-
notes: New notes content (replaces existing notes).
|
|
1380
|
-
notes_file: Path to external notes file.
|
|
1381
|
-
append_notes: Text to append to existing notes.
|
|
1382
|
-
clear_notes: If True, clear all notes.
|
|
1383
|
-
clear_relative_probability: If True, clear relative probability.
|
|
1384
|
-
clear_log_likelihood: If True, clear log likelihood.
|
|
1385
|
-
clear_n_data_points: If True, clear n_data_points.
|
|
1386
|
-
clear_parameter_uncertainties: If True, clear parameter uncertainties.
|
|
1387
|
-
clear_physical_parameters: If True, clear physical parameters.
|
|
1388
|
-
cpu_hours: New CPU hours value.
|
|
1389
|
-
wall_time_hours: New wall time hours value.
|
|
1390
|
-
param: Model parameters as key=value pairs (updates existing).
|
|
1391
|
-
param_uncertainty: Parameter uncertainties as key=value pairs.
|
|
1392
|
-
higher_order_effect: Higher-order effects (replaces existing).
|
|
1393
|
-
clear_higher_order_effects: If True, clear all higher-order effects.
|
|
1394
|
-
dry_run: If True, show changes without saving.
|
|
1395
|
-
project_path: Directory of the submission project.
|
|
1396
|
-
|
|
1397
|
-
Raises:
|
|
1398
|
-
typer.Exit: If the solution is not found.
|
|
1399
|
-
typer.BadParameter: If parameter format is invalid.
|
|
1400
|
-
|
|
1401
|
-
Example:
|
|
1402
|
-
# Update solution metadata
|
|
1403
|
-
microlens-submit edit-solution abc12345-def6-7890-ghij-klmnopqrstuv ./project \
|
|
1404
|
-
--log-likelihood -1200.0 \
|
|
1405
|
-
--relative-probability 0.8 \
|
|
1406
|
-
--cpu-hours 3.5
|
|
1407
|
-
|
|
1408
|
-
# Update model parameters
|
|
1409
|
-
microlens-submit edit-solution abc12345-def6-7890-ghij-klmnopqrstuv ./project \
|
|
1410
|
-
--param t0=2459123.6 --param u0=0.12
|
|
1411
|
-
|
|
1412
|
-
# Replace notes
|
|
1413
|
-
microlens-submit edit-solution abc12345-def6-7890-ghij-klmnopqrstuv ./project \
|
|
1414
|
-
--notes "# Updated Solution Notes\n\nThis is the updated description."
|
|
1415
|
-
|
|
1416
|
-
# Append to existing notes
|
|
1417
|
-
microlens-submit edit-solution abc12345-def6-7890-ghij-klmnopqrstuv ./project \
|
|
1418
|
-
--append-notes "\n\n## Additional Analysis\n\nFurther investigation shows..."
|
|
1419
|
-
|
|
1420
|
-
# Clear specific fields
|
|
1421
|
-
microlens-submit edit-solution abc12345-def6-7890-ghij-klmnopqrstuv ./project \
|
|
1422
|
-
--clear-relative-probability --clear-notes
|
|
1423
|
-
|
|
1424
|
-
# Dry run to preview changes
|
|
1425
|
-
microlens-submit edit-solution abc12345-def6-7890-ghij-klmnopqrstuv ./project \
|
|
1426
|
-
--log-likelihood -1200.0 --dry-run
|
|
1427
|
-
|
|
1428
|
-
Note:
|
|
1429
|
-
This command only modifies the specified fields. Unspecified fields
|
|
1430
|
-
remain unchanged. Use --dry-run to preview changes before applying them.
|
|
1431
|
-
The command automatically saves changes to disk after successful updates.
|
|
1432
|
-
"""
|
|
1433
|
-
sub = load(str(project_path))
|
|
1434
|
-
target_solution = None
|
|
1435
|
-
target_event_id = None
|
|
1436
|
-
for event_id, event in sub.events.items():
|
|
1437
|
-
if solution_id in event.solutions:
|
|
1438
|
-
target_solution = event.solutions[solution_id]
|
|
1439
|
-
target_event_id = event_id
|
|
1440
|
-
break
|
|
1441
|
-
if target_solution is None:
|
|
1442
|
-
console.print(f"Solution {solution_id} not found", style="bold red")
|
|
1443
|
-
raise typer.Exit(code=1)
|
|
1444
|
-
changes = []
|
|
1445
|
-
if clear_relative_probability:
|
|
1446
|
-
if target_solution.relative_probability is not None:
|
|
1447
|
-
changes.append(f"Clear relative_probability: {target_solution.relative_probability}")
|
|
1448
|
-
target_solution.relative_probability = None
|
|
1449
|
-
elif relative_probability is not None:
|
|
1450
|
-
if target_solution.relative_probability != relative_probability:
|
|
1451
|
-
changes.append(f"Update relative_probability: {target_solution.relative_probability} → {relative_probability}")
|
|
1452
|
-
target_solution.relative_probability = relative_probability
|
|
1453
|
-
if clear_log_likelihood:
|
|
1454
|
-
if target_solution.log_likelihood is not None:
|
|
1455
|
-
changes.append(f"Clear log_likelihood: {target_solution.log_likelihood}")
|
|
1456
|
-
target_solution.log_likelihood = None
|
|
1457
|
-
elif log_likelihood is not None:
|
|
1458
|
-
if target_solution.log_likelihood != log_likelihood:
|
|
1459
|
-
changes.append(f"Update log_likelihood: {target_solution.log_likelihood} → {log_likelihood}")
|
|
1460
|
-
target_solution.log_likelihood = log_likelihood
|
|
1461
|
-
if clear_n_data_points:
|
|
1462
|
-
if target_solution.n_data_points is not None:
|
|
1463
|
-
changes.append(f"Clear n_data_points: {target_solution.n_data_points}")
|
|
1464
|
-
target_solution.n_data_points = None
|
|
1465
|
-
elif n_data_points is not None:
|
|
1466
|
-
if target_solution.n_data_points != n_data_points:
|
|
1467
|
-
changes.append(f"Update n_data_points: {target_solution.n_data_points} → {n_data_points}")
|
|
1468
|
-
target_solution.n_data_points = n_data_points
|
|
1469
|
-
# Notes file logic
|
|
1470
|
-
canonical_notes_path = Path(project_path) / "events" / target_event_id / "solutions" / f"{target_solution.solution_id}.md"
|
|
1471
|
-
if notes_file is not None:
|
|
1472
|
-
target_solution.notes_path = str(notes_file)
|
|
1473
|
-
changes.append(f"Set notes_path to {notes_file}")
|
|
1474
|
-
elif notes is not None:
|
|
1475
|
-
target_solution.notes_path = str(canonical_notes_path.relative_to(project_path))
|
|
1476
|
-
canonical_notes_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1477
|
-
canonical_notes_path.write_text(notes, encoding="utf-8")
|
|
1478
|
-
changes.append(f"Updated notes in {canonical_notes_path}")
|
|
1479
|
-
elif append_notes is not None:
|
|
1480
|
-
if target_solution.notes_path:
|
|
1481
|
-
notes_file_path = Path(project_path) / target_solution.notes_path
|
|
1482
|
-
old_content = notes_file_path.read_text(encoding="utf-8") if notes_file_path.exists() else ""
|
|
1483
|
-
notes_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1484
|
-
notes_file_path.write_text(old_content + "\n" + append_notes, encoding="utf-8")
|
|
1485
|
-
changes.append(f"Appended notes in {notes_file_path}")
|
|
1486
|
-
elif clear_notes:
|
|
1487
|
-
if target_solution.notes_path:
|
|
1488
|
-
notes_file_path = Path(project_path) / target_solution.notes_path
|
|
1489
|
-
notes_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1490
|
-
notes_file_path.write_text("", encoding="utf-8")
|
|
1491
|
-
changes.append(f"Cleared notes in {notes_file_path}")
|
|
1492
|
-
if clear_parameter_uncertainties:
|
|
1493
|
-
if target_solution.parameter_uncertainties:
|
|
1494
|
-
changes.append("Clear parameter_uncertainties")
|
|
1495
|
-
target_solution.parameter_uncertainties = None
|
|
1496
|
-
if clear_physical_parameters:
|
|
1497
|
-
if target_solution.physical_parameters:
|
|
1498
|
-
changes.append("Clear physical_parameters")
|
|
1499
|
-
target_solution.physical_parameters = None
|
|
1500
|
-
if cpu_hours is not None or wall_time_hours is not None:
|
|
1501
|
-
old_cpu = target_solution.compute_info.get("cpu_hours")
|
|
1502
|
-
old_wall = target_solution.compute_info.get("wall_time_hours")
|
|
1503
|
-
if cpu_hours is not None and old_cpu != cpu_hours:
|
|
1504
|
-
changes.append(f"Update cpu_hours: {old_cpu} → {cpu_hours}")
|
|
1505
|
-
if wall_time_hours is not None and old_wall != wall_time_hours:
|
|
1506
|
-
changes.append(f"Update wall_time_hours: {old_wall} → {wall_time_hours}")
|
|
1507
|
-
target_solution.set_compute_info(
|
|
1508
|
-
cpu_hours=cpu_hours if cpu_hours is not None else old_cpu,
|
|
1509
|
-
wall_time_hours=wall_time_hours if wall_time_hours is not None else old_wall
|
|
1510
|
-
)
|
|
1511
|
-
if param:
|
|
1512
|
-
for p in param:
|
|
1513
|
-
if "=" not in p:
|
|
1514
|
-
raise typer.BadParameter(f"Invalid parameter format: {p}")
|
|
1515
|
-
key, value = p.split("=", 1)
|
|
1516
|
-
try:
|
|
1517
|
-
new_value = json.loads(value)
|
|
1518
|
-
except json.JSONDecodeError:
|
|
1519
|
-
new_value = value
|
|
1520
|
-
old_value = target_solution.parameters.get(key)
|
|
1521
|
-
if old_value != new_value:
|
|
1522
|
-
changes.append(f"Update parameter {key}: {old_value} → {new_value}")
|
|
1523
|
-
target_solution.parameters[key] = new_value
|
|
1524
|
-
if param_uncertainty:
|
|
1525
|
-
if target_solution.parameter_uncertainties is None:
|
|
1526
|
-
target_solution.parameter_uncertainties = {}
|
|
1527
|
-
for p in param_uncertainty:
|
|
1528
|
-
if "=" not in p:
|
|
1529
|
-
raise typer.BadParameter(f"Invalid uncertainty format: {p}")
|
|
1530
|
-
key, value = p.split("=", 1)
|
|
1531
|
-
try:
|
|
1532
|
-
new_value = json.loads(value)
|
|
1533
|
-
except json.JSONDecodeError:
|
|
1534
|
-
new_value = value
|
|
1535
|
-
old_value = target_solution.parameter_uncertainties.get(key)
|
|
1536
|
-
if old_value != new_value:
|
|
1537
|
-
changes.append(f"Update uncertainty {key}: {old_value} → {new_value}")
|
|
1538
|
-
target_solution.parameter_uncertainties[key] = new_value
|
|
1539
|
-
if clear_higher_order_effects:
|
|
1540
|
-
if target_solution.higher_order_effects:
|
|
1541
|
-
changes.append(f"Clear higher_order_effects: {target_solution.higher_order_effects}")
|
|
1542
|
-
target_solution.higher_order_effects = []
|
|
1543
|
-
elif higher_order_effect:
|
|
1544
|
-
if target_solution.higher_order_effects != higher_order_effect:
|
|
1545
|
-
changes.append(f"Update higher_order_effects: {target_solution.higher_order_effects} → {higher_order_effect}")
|
|
1546
|
-
target_solution.higher_order_effects = higher_order_effect
|
|
1547
|
-
if dry_run:
|
|
1548
|
-
if changes:
|
|
1549
|
-
console.print(Panel(f"Changes for {solution_id} (event {target_event_id})", style="cyan"))
|
|
1550
|
-
for change in changes:
|
|
1551
|
-
console.print(f" • {change}")
|
|
1552
|
-
else:
|
|
1553
|
-
console.print(Panel("No changes would be made", style="yellow"))
|
|
1554
|
-
return
|
|
1555
|
-
if changes:
|
|
1556
|
-
sub.save()
|
|
1557
|
-
console.print(Panel(f"Updated {solution_id} (event {target_event_id})", style="green"))
|
|
1558
|
-
for change in changes:
|
|
1559
|
-
console.print(f" • {change}")
|
|
1560
|
-
else:
|
|
1561
|
-
console.print(Panel("No changes made", style="yellow"))
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
@app.command("notes")
|
|
1565
|
-
def edit_notes(solution_id: str, project_path: Path = typer.Argument(Path("."), help="Project directory")) -> None:
|
|
1566
|
-
"""Open the notes file for a solution in the default text editor.
|
|
1567
|
-
|
|
1568
|
-
Launches the system's default text editor (or a fallback) to edit the
|
|
1569
|
-
notes file for the specified solution. This provides a convenient way
|
|
1570
|
-
to edit solution notes without having to manually locate the file.
|
|
1571
|
-
|
|
1572
|
-
The command uses the $EDITOR environment variable if set, otherwise
|
|
1573
|
-
falls back to nano, then vi. The notes file is created if it doesn't
|
|
1574
|
-
exist.
|
|
1575
|
-
|
|
1576
|
-
Args:
|
|
1577
|
-
solution_id: The unique identifier of the solution to edit notes for.
|
|
1578
|
-
project_path: Directory of the submission project.
|
|
1579
|
-
|
|
1580
|
-
Raises:
|
|
1581
|
-
typer.Exit: If the solution is not found or no editor is available.
|
|
1582
|
-
|
|
1583
|
-
Example:
|
|
1584
|
-
# Edit notes for a specific solution
|
|
1585
|
-
microlens-submit notes abc12345-def6-7890-ghij-klmnopqrstuv ./project
|
|
1586
|
-
|
|
1587
|
-
# This will open the notes file in your default editor:
|
|
1588
|
-
# ./project/events/EVENT001/solutions/abc12345-def6-7890-ghij-klmnopqrstuv.md
|
|
1589
|
-
|
|
1590
|
-
Note:
|
|
1591
|
-
The notes file is opened in the system's default text editor.
|
|
1592
|
-
If $EDITOR is not set, the command tries nano, then vi as fallbacks.
|
|
1593
|
-
The notes file supports Markdown formatting and will be rendered
|
|
1594
|
-
as HTML in the dossier with syntax highlighting for code blocks.
|
|
1595
|
-
"""
|
|
1596
|
-
sub = load(str(project_path))
|
|
1597
|
-
for event in sub.events.values():
|
|
1598
|
-
if solution_id in event.solutions:
|
|
1599
|
-
sol = event.solutions[solution_id]
|
|
1600
|
-
if not sol.notes_path:
|
|
1601
|
-
console.print(f"No notes file associated with solution {solution_id}", style="bold red")
|
|
1602
|
-
raise typer.Exit(code=1)
|
|
1603
|
-
notes_file = Path(project_path) / sol.notes_path
|
|
1604
|
-
notes_file.parent.mkdir(parents=True, exist_ok=True)
|
|
1605
|
-
if not notes_file.exists():
|
|
1606
|
-
notes_file.write_text("", encoding="utf-8")
|
|
1607
|
-
editor = os.environ.get("EDITOR", None)
|
|
1608
|
-
if editor:
|
|
1609
|
-
os.system(f'{editor} "{notes_file}"')
|
|
1610
|
-
else:
|
|
1611
|
-
# Try nano, then vi
|
|
1612
|
-
for fallback in ["nano", "vi"]:
|
|
1613
|
-
if os.system(f"command -v {fallback} > /dev/null 2>&1") == 0:
|
|
1614
|
-
os.system(f'{fallback} "{notes_file}"')
|
|
1615
|
-
break
|
|
1616
|
-
else:
|
|
1617
|
-
console.print(f"Could not find an editor to open {notes_file}", style="bold red")
|
|
1618
|
-
raise typer.Exit(code=1)
|
|
1619
|
-
return
|
|
1620
|
-
console.print(f"Solution {solution_id} not found", style="bold red")
|
|
1621
|
-
raise typer.Exit(code=1)
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
@app.command()
|
|
1625
|
-
def set_repo_url(
|
|
1626
|
-
repo_url: str = typer.Argument(..., help="GitHub repository URL (e.g. https://github.com/owner/repo)"),
|
|
1627
|
-
project_path: Path = typer.Argument(Path("."), help="Project directory"),
|
|
1628
|
-
) -> None:
|
|
1629
|
-
"""Set or update the GitHub repository URL in the submission metadata.
|
|
1630
|
-
|
|
1631
|
-
Updates the repo_url field in submission.json with the provided GitHub
|
|
1632
|
-
repository URL. This URL is required for submission validation and is
|
|
1633
|
-
displayed in the generated dossier.
|
|
1634
|
-
|
|
1635
|
-
The command accepts various GitHub URL formats:
|
|
1636
|
-
- HTTPS: https://github.com/owner/repo
|
|
1637
|
-
- SSH: git@github.com:owner/repo.git
|
|
1638
|
-
- With or without .git extension
|
|
1639
|
-
|
|
1640
|
-
Args:
|
|
1641
|
-
repo_url: GitHub repository URL in any standard format.
|
|
1642
|
-
project_path: Directory of the submission project.
|
|
1643
|
-
|
|
1644
|
-
Raises:
|
|
1645
|
-
OSError: If unable to write to submission.json.
|
|
1646
|
-
|
|
1647
|
-
Example:
|
|
1648
|
-
# Set repository URL using HTTPS format
|
|
1649
|
-
microlens-submit set-repo-url https://github.com/team-alpha/microlens-submit ./project
|
|
1650
|
-
|
|
1651
|
-
# Set repository URL using SSH format
|
|
1652
|
-
microlens-submit set-repo-url git@github.com:team-alpha/microlens-submit.git ./project
|
|
1653
|
-
|
|
1654
|
-
# Update existing repository URL
|
|
1655
|
-
microlens-submit set-repo-url https://github.com/team-alpha/new-repo ./project
|
|
1656
|
-
|
|
1657
|
-
Note:
|
|
1658
|
-
The repository URL is used for:
|
|
1659
|
-
- Submission validation (required field)
|
|
1660
|
-
- Display in the generated dossier
|
|
1661
|
-
- Linking to specific commits in solution pages
|
|
1662
|
-
|
|
1663
|
-
The URL should point to the repository containing your analysis code
|
|
1664
|
-
and submission preparation scripts.
|
|
1665
|
-
"""
|
|
1666
|
-
sub = load(str(project_path))
|
|
1667
|
-
sub.repo_url = repo_url
|
|
1668
|
-
sub.save()
|
|
1669
|
-
console.print(Panel(f"Set repo_url to {repo_url} in {project_path}/submission.json", style="bold green"))
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
@app.command("set-hardware-info")
|
|
1673
|
-
def set_hardware_info(
|
|
1674
|
-
cpu: Optional[str] = typer.Option(None, "--cpu", help="CPU model/description"),
|
|
1675
|
-
cpu_details: Optional[str] = typer.Option(None, "--cpu-details", help="Detailed CPU information"),
|
|
1676
|
-
memory_gb: Optional[float] = typer.Option(None, "--memory-gb", help="Memory in GB"),
|
|
1677
|
-
ram_gb: Optional[float] = typer.Option(None, "--ram-gb", help="RAM in GB (alternative to --memory-gb)"),
|
|
1678
|
-
platform: Optional[str] = typer.Option(None, "--platform", help="Platform description (e.g., 'Local Analysis', 'Roman Nexus')"),
|
|
1679
|
-
nexus_image: Optional[str] = typer.Option(None, "--nexus-image", help="Roman Nexus image identifier"),
|
|
1680
|
-
clear: bool = typer.Option(False, "--clear", help="Clear all existing hardware info"),
|
|
1681
|
-
dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be changed without saving"),
|
|
1682
|
-
project_path: Path = typer.Argument(Path("."), help="Project directory"),
|
|
1683
|
-
) -> None:
|
|
1684
|
-
"""Set or update hardware information in the submission metadata.
|
|
1685
|
-
|
|
1686
|
-
Updates the hardware_info field in submission.json with computational
|
|
1687
|
-
resource details. This information is displayed in the generated dossier
|
|
1688
|
-
and helps with reproducibility and resource tracking.
|
|
1689
|
-
|
|
1690
|
-
The command accepts various hardware information fields and can be used
|
|
1691
|
-
to update existing information or set it for the first time.
|
|
1692
|
-
|
|
1693
|
-
Args:
|
|
1694
|
-
cpu: Basic CPU model/description (e.g., "Intel Xeon E5-2680 v4").
|
|
1695
|
-
cpu_details: Detailed CPU information (takes precedence over --cpu).
|
|
1696
|
-
memory_gb: Memory in gigabytes (e.g., 64.0).
|
|
1697
|
-
ram_gb: RAM in gigabytes (alternative to --memory-gb).
|
|
1698
|
-
platform: Platform description (e.g., "Local Analysis", "Roman Nexus").
|
|
1699
|
-
nexus_image: Roman Nexus image identifier.
|
|
1700
|
-
clear: If True, clear all existing hardware info before setting new values.
|
|
1701
|
-
dry_run: If True, show what would be changed without saving.
|
|
1702
|
-
project_path: Directory of the submission project.
|
|
1703
|
-
|
|
1704
|
-
Raises:
|
|
1705
|
-
OSError: If unable to write to submission.json.
|
|
1706
|
-
|
|
1707
|
-
Example:
|
|
1708
|
-
# Set basic hardware info
|
|
1709
|
-
microlens-submit set-hardware-info --cpu "Intel Xeon E5-2680 v4" --memory-gb 64 ./project
|
|
1710
|
-
|
|
1711
|
-
# Set detailed platform info
|
|
1712
|
-
microlens-submit set-hardware-info --platform "Local Analysis" --cpu-details "Intel Xeon E5-2680 v4 @ 2.4GHz" ./project
|
|
1713
|
-
|
|
1714
|
-
# Set Roman Nexus info
|
|
1715
|
-
microlens-submit set-hardware-info --platform "Roman Nexus" --nexus-image "roman-science-platform:latest" ./project
|
|
1716
|
-
|
|
1717
|
-
# Update existing info
|
|
1718
|
-
microlens-submit set-hardware-info --memory-gb 128 ./project
|
|
1719
|
-
|
|
1720
|
-
# Clear and set new info
|
|
1721
|
-
microlens-submit set-hardware-info --clear --cpu "AMD EPYC" --memory-gb 256 ./project
|
|
1722
|
-
|
|
1723
|
-
# Dry run to preview changes
|
|
1724
|
-
microlens-submit set-hardware-info --cpu "Intel i7" --memory-gb 32 --dry-run ./project
|
|
1725
|
-
|
|
1726
|
-
Note:
|
|
1727
|
-
Hardware information is used for:
|
|
1728
|
-
- Display in the generated dossier
|
|
1729
|
-
- Reproducibility documentation
|
|
1730
|
-
- Resource usage tracking
|
|
1731
|
-
|
|
1732
|
-
The --cpu-details option takes precedence over --cpu if both are provided.
|
|
1733
|
-
The --memory-gb and --ram-gb options are equivalent; use whichever is clearer.
|
|
1734
|
-
Use --clear to replace all existing hardware info with new values.
|
|
1735
|
-
"""
|
|
1736
|
-
sub = load(str(project_path))
|
|
1737
|
-
|
|
1738
|
-
# Initialize hardware_info if it doesn't exist
|
|
1739
|
-
if sub.hardware_info is None:
|
|
1740
|
-
sub.hardware_info = {}
|
|
1741
|
-
|
|
1742
|
-
changes = []
|
|
1743
|
-
old_hardware_info = sub.hardware_info.copy()
|
|
1744
|
-
|
|
1745
|
-
# Clear existing info if requested
|
|
1746
|
-
if clear:
|
|
1747
|
-
if sub.hardware_info:
|
|
1748
|
-
changes.append("Clear all existing hardware info")
|
|
1749
|
-
sub.hardware_info = {}
|
|
1750
|
-
|
|
1751
|
-
# Set new values
|
|
1752
|
-
if cpu_details is not None:
|
|
1753
|
-
if sub.hardware_info.get('cpu_details') != cpu_details:
|
|
1754
|
-
changes.append(f"Set cpu_details: {cpu_details}")
|
|
1755
|
-
sub.hardware_info['cpu_details'] = cpu_details
|
|
1756
|
-
elif cpu is not None:
|
|
1757
|
-
if sub.hardware_info.get('cpu') != cpu:
|
|
1758
|
-
changes.append(f"Set cpu: {cpu}")
|
|
1759
|
-
sub.hardware_info['cpu'] = cpu
|
|
1760
|
-
|
|
1761
|
-
if memory_gb is not None:
|
|
1762
|
-
if sub.hardware_info.get('memory_gb') != memory_gb:
|
|
1763
|
-
changes.append(f"Set memory_gb: {memory_gb}")
|
|
1764
|
-
sub.hardware_info['memory_gb'] = memory_gb
|
|
1765
|
-
elif ram_gb is not None:
|
|
1766
|
-
if sub.hardware_info.get('ram_gb') != ram_gb:
|
|
1767
|
-
changes.append(f"Set ram_gb: {ram_gb}")
|
|
1768
|
-
sub.hardware_info['ram_gb'] = ram_gb
|
|
1769
|
-
|
|
1770
|
-
if platform is not None:
|
|
1771
|
-
if sub.hardware_info.get('platform') != platform:
|
|
1772
|
-
changes.append(f"Set platform: {platform}")
|
|
1773
|
-
sub.hardware_info['platform'] = platform
|
|
1774
|
-
|
|
1775
|
-
if nexus_image is not None:
|
|
1776
|
-
if sub.hardware_info.get('nexus_image') != nexus_image:
|
|
1777
|
-
changes.append(f"Set nexus_image: {nexus_image}")
|
|
1778
|
-
sub.hardware_info['nexus_image'] = nexus_image
|
|
1779
|
-
|
|
1780
|
-
# Show dry run results
|
|
1781
|
-
if dry_run:
|
|
1782
|
-
if changes:
|
|
1783
|
-
console.print(Panel("Hardware info changes (dry run):", style="cyan"))
|
|
1784
|
-
for change in changes:
|
|
1785
|
-
console.print(f" • {change}")
|
|
1786
|
-
console.print(f"\nNew hardware_info: {sub.hardware_info}")
|
|
1787
|
-
else:
|
|
1788
|
-
console.print(Panel("No changes would be made", style="yellow"))
|
|
1789
|
-
return
|
|
1790
|
-
|
|
1791
|
-
# Apply changes
|
|
1792
|
-
if changes:
|
|
1793
|
-
sub.save()
|
|
1794
|
-
console.print(Panel(f"Updated hardware info in {project_path}/submission.json", style="bold green"))
|
|
1795
|
-
for change in changes:
|
|
1796
|
-
console.print(f" • {change}")
|
|
1797
|
-
console.print(f"\nCurrent hardware_info: {sub.hardware_info}")
|
|
1798
|
-
else:
|
|
1799
|
-
console.print(Panel("No changes made", style="yellow"))
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
if __name__ == "__main__": # pragma: no cover
|
|
1803
|
-
app()
|