microlens-submit 0.12.2__py3-none-any.whl → 0.16.0__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 +499 -0
- microlens_submit/dossier/event_page.py +369 -0
- microlens_submit/dossier/full_report.py +330 -0
- microlens_submit/dossier/solution_page.py +533 -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.0.dist-info}/METADATA +42 -27
- microlens_submit-0.16.0.dist-info/RECORD +32 -0
- {microlens_submit-0.12.2.dist-info → microlens_submit-0.16.0.dist-info}/WHEEL +1 -1
- 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/licenses → microlens_submit-0.16.0.dist-info}/LICENSE +0 -0
- {microlens_submit-0.12.2.dist-info → microlens_submit-0.16.0.dist-info}/entry_points.txt +0 -0
- {microlens_submit-0.12.2.dist-info → microlens_submit-0.16.0.dist-info}/top_level.txt +0 -0
microlens_submit/dossier.py
DELETED
|
@@ -1,1443 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Dossier generation module for microlens-submit.
|
|
3
|
-
|
|
4
|
-
This module provides functionality to generate HTML dossiers and dashboards
|
|
5
|
-
for submission review and documentation. It creates comprehensive, printable
|
|
6
|
-
HTML reports that showcase microlensing challenge submissions with detailed
|
|
7
|
-
statistics, visualizations, and participant notes.
|
|
8
|
-
|
|
9
|
-
The module generates three types of HTML pages:
|
|
10
|
-
1. Dashboard (index.html) - Overview of all events and solutions
|
|
11
|
-
2. Event pages - Detailed view of each event with its solutions
|
|
12
|
-
3. Solution pages - Individual solution details with parameters and notes
|
|
13
|
-
|
|
14
|
-
All pages use Tailwind CSS for styling and include syntax highlighting for
|
|
15
|
-
code blocks in participant notes.
|
|
16
|
-
|
|
17
|
-
Example:
|
|
18
|
-
>>> from microlens_submit import load
|
|
19
|
-
>>> from microlens_submit.dossier import generate_dashboard_html
|
|
20
|
-
>>>
|
|
21
|
-
>>> # Load a submission
|
|
22
|
-
>>> submission = load("./my_project")
|
|
23
|
-
>>>
|
|
24
|
-
>>> # Generate the complete dossier
|
|
25
|
-
>>> generate_dashboard_html(submission, Path("./dossier_output"))
|
|
26
|
-
>>>
|
|
27
|
-
>>> # The dossier will be created at ./dossier_output/index.html
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
from pathlib import Path
|
|
31
|
-
from typing import Dict, List, Any, Optional
|
|
32
|
-
from datetime import datetime
|
|
33
|
-
import markdown # Add this import at the top
|
|
34
|
-
import re
|
|
35
|
-
import os
|
|
36
|
-
import sys
|
|
37
|
-
|
|
38
|
-
from .api import Submission, Event, Solution
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def generate_dashboard_html(submission: Submission, output_dir: Path) -> None:
|
|
42
|
-
"""Generate a complete HTML dossier for the submission.
|
|
43
|
-
|
|
44
|
-
Creates a comprehensive HTML dashboard that provides an overview of the submission,
|
|
45
|
-
including event summaries, solution statistics, and metadata. The dossier includes:
|
|
46
|
-
- Main dashboard (index.html) with submission overview
|
|
47
|
-
- Individual event pages for each event
|
|
48
|
-
- Individual solution pages for each solution
|
|
49
|
-
- Full comprehensive dossier (full_dossier_report.html) for printing
|
|
50
|
-
|
|
51
|
-
The function creates the output directory structure and copies necessary assets
|
|
52
|
-
like logos and GitHub icons.
|
|
53
|
-
|
|
54
|
-
Args:
|
|
55
|
-
submission: The submission object containing events and solutions.
|
|
56
|
-
output_dir: Directory where the HTML files will be saved. Will be created
|
|
57
|
-
if it doesn't exist.
|
|
58
|
-
|
|
59
|
-
Raises:
|
|
60
|
-
OSError: If unable to create output directory or write files.
|
|
61
|
-
ValueError: If submission data is invalid or missing required fields.
|
|
62
|
-
|
|
63
|
-
Example:
|
|
64
|
-
>>> from microlens_submit import load
|
|
65
|
-
>>> from microlens_submit.dossier import generate_dashboard_html
|
|
66
|
-
>>> from pathlib import Path
|
|
67
|
-
>>>
|
|
68
|
-
>>> # Load a submission project
|
|
69
|
-
>>> submission = load("./my_project")
|
|
70
|
-
>>>
|
|
71
|
-
>>> # Generate the complete dossier
|
|
72
|
-
>>> generate_dashboard_html(submission, Path("./dossier_output"))
|
|
73
|
-
>>>
|
|
74
|
-
>>> # Files created:
|
|
75
|
-
>>> # - ./dossier_output/index.html (main dashboard)
|
|
76
|
-
>>> # - ./dossier_output/EVENT001.html (event page)
|
|
77
|
-
>>> # - ./dossier_output/solution_id.html (solution pages)
|
|
78
|
-
>>> # - ./dossier_output/full_dossier_report.html (printable version)
|
|
79
|
-
>>> # - ./dossier_output/assets/ (logos and icons)
|
|
80
|
-
|
|
81
|
-
Note:
|
|
82
|
-
This function generates all dossier components. For partial generation
|
|
83
|
-
(e.g., only specific events), use the CLI command with --event-id or
|
|
84
|
-
--solution-id flags instead.
|
|
85
|
-
"""
|
|
86
|
-
# Create output directory structure
|
|
87
|
-
output_dir.mkdir(parents=True, exist_ok=True)
|
|
88
|
-
(output_dir / "assets").mkdir(exist_ok=True)
|
|
89
|
-
# (No events or solutions subfolders)
|
|
90
|
-
|
|
91
|
-
# Check if full dossier report exists
|
|
92
|
-
full_dossier_exists = (output_dir / "full_dossier_report.html").exists()
|
|
93
|
-
# Generate the main dashboard HTML
|
|
94
|
-
html_content = _generate_dashboard_content(submission, full_dossier_exists=full_dossier_exists)
|
|
95
|
-
|
|
96
|
-
# Write the HTML file
|
|
97
|
-
with (output_dir / "index.html").open("w", encoding="utf-8") as f:
|
|
98
|
-
f.write(html_content)
|
|
99
|
-
|
|
100
|
-
# Copy logos if they exist in the project
|
|
101
|
-
logo_source = Path(__file__).parent / "assets" / "rges-pit_logo.png"
|
|
102
|
-
if logo_source.exists():
|
|
103
|
-
import shutil
|
|
104
|
-
shutil.copy2(logo_source, output_dir / "assets" / "rges-pit_logo.png")
|
|
105
|
-
|
|
106
|
-
# Copy GitHub logo if it exists in the project
|
|
107
|
-
github_logo_source = Path(__file__).parent / "assets" / "github-desktop_logo.png"
|
|
108
|
-
if github_logo_source.exists():
|
|
109
|
-
import shutil
|
|
110
|
-
shutil.copy2(github_logo_source, output_dir / "assets" / "github-desktop_logo.png")
|
|
111
|
-
|
|
112
|
-
# After generating index.html, generate event pages
|
|
113
|
-
for event in submission.events.values():
|
|
114
|
-
generate_event_page(event, submission, output_dir)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def _generate_dashboard_content(submission: Submission, full_dossier_exists: bool = False) -> str:
|
|
118
|
-
"""Generate the HTML content for the submission dashboard.
|
|
119
|
-
|
|
120
|
-
Creates the main dashboard HTML following the Dashboard_Design.md specification.
|
|
121
|
-
The dashboard includes submission statistics, progress tracking, event tables,
|
|
122
|
-
and aggregate parameter distributions.
|
|
123
|
-
|
|
124
|
-
Args:
|
|
125
|
-
submission: The submission object containing events and solutions.
|
|
126
|
-
full_dossier_exists: Whether the full dossier report exists. Currently
|
|
127
|
-
ignored but kept for future use.
|
|
128
|
-
|
|
129
|
-
Returns:
|
|
130
|
-
str: Complete HTML content as a string, ready to be written to index.html.
|
|
131
|
-
|
|
132
|
-
Example:
|
|
133
|
-
>>> from microlens_submit import load
|
|
134
|
-
>>> from microlens_submit.dossier import _generate_dashboard_content
|
|
135
|
-
>>>
|
|
136
|
-
>>> submission = load("./my_project")
|
|
137
|
-
>>> html_content = _generate_dashboard_content(submission)
|
|
138
|
-
>>>
|
|
139
|
-
>>> # Write to file
|
|
140
|
-
>>> with open("dashboard.html", "w") as f:
|
|
141
|
-
... f.write(html_content)
|
|
142
|
-
|
|
143
|
-
Note:
|
|
144
|
-
This is an internal function. Use generate_dashboard_html() for the
|
|
145
|
-
complete dossier generation workflow.
|
|
146
|
-
"""
|
|
147
|
-
# Calculate statistics
|
|
148
|
-
total_events = len(submission.events)
|
|
149
|
-
total_active_solutions = sum(len(event.get_active_solutions()) for event in submission.events.values())
|
|
150
|
-
total_cpu_hours = 0
|
|
151
|
-
total_wall_time_hours = 0
|
|
152
|
-
|
|
153
|
-
# Calculate compute time
|
|
154
|
-
for event in submission.events.values():
|
|
155
|
-
for solution in event.solutions.values():
|
|
156
|
-
if solution.compute_info:
|
|
157
|
-
total_cpu_hours += solution.compute_info.get('cpu_hours', 0)
|
|
158
|
-
total_wall_time_hours += solution.compute_info.get('wall_time_hours', 0)
|
|
159
|
-
|
|
160
|
-
# Format hardware info
|
|
161
|
-
hardware_info_str = _format_hardware_info(submission.hardware_info)
|
|
162
|
-
|
|
163
|
-
# Calculate progress (hardcoded total from design spec)
|
|
164
|
-
TOTAL_CHALLENGE_EVENTS = 293
|
|
165
|
-
progress_percentage = (total_events / TOTAL_CHALLENGE_EVENTS) * 100 if TOTAL_CHALLENGE_EVENTS > 0 else 0
|
|
166
|
-
|
|
167
|
-
# Generate event table
|
|
168
|
-
event_rows = []
|
|
169
|
-
for event in sorted(submission.events.values(), key=lambda e: e.event_id):
|
|
170
|
-
active_solutions = event.get_active_solutions()
|
|
171
|
-
model_types = set(sol.model_type for sol in active_solutions)
|
|
172
|
-
model_types_str = ", ".join(sorted(model_types)) if model_types else "None"
|
|
173
|
-
|
|
174
|
-
event_rows.append(f"""
|
|
175
|
-
<tr class="border-b border-gray-200 hover:bg-gray-50">
|
|
176
|
-
<td class="py-3 px-4">
|
|
177
|
-
<a href="{event.event_id}.html" class="font-medium text-rtd-accent hover:underline">
|
|
178
|
-
{event.event_id}
|
|
179
|
-
</a>
|
|
180
|
-
</td>
|
|
181
|
-
<td class="py-3 px-4">{len(active_solutions)}</td>
|
|
182
|
-
<td class="py-3 px-4">{model_types_str}</td>
|
|
183
|
-
</tr>
|
|
184
|
-
""")
|
|
185
|
-
|
|
186
|
-
event_table = "\n".join(event_rows) if event_rows else """
|
|
187
|
-
<tr class="border-b border-gray-200">
|
|
188
|
-
<td colspan="3" class="py-3 px-4 text-center text-gray-500">No events found</td>
|
|
189
|
-
</tr>
|
|
190
|
-
"""
|
|
191
|
-
|
|
192
|
-
# Insert Print Full Dossier placeholder before the footer
|
|
193
|
-
print_link_html = "<!--FULL_DOSSIER_LINK_PLACEHOLDER-->"
|
|
194
|
-
|
|
195
|
-
# GitHub repo link (if present)
|
|
196
|
-
github_html = ""
|
|
197
|
-
repo_url = getattr(submission, 'repo_url', None) or (submission.repo_url if hasattr(submission, 'repo_url') else None)
|
|
198
|
-
if repo_url:
|
|
199
|
-
repo_name = _extract_github_repo_name(repo_url)
|
|
200
|
-
github_html = f'''
|
|
201
|
-
<div class="flex items-center justify-center mb-4">
|
|
202
|
-
<a href="{repo_url}" target="_blank" rel="noopener" class="flex items-center space-x-2 group">
|
|
203
|
-
<img src="assets/github-desktop_logo.png" alt="GitHub" class="w-6 h-6 inline-block align-middle mr-2 group-hover:opacity-80" style="display:inline;vertical-align:middle;">
|
|
204
|
-
<span class="text-base text-rtd-accent font-semibold group-hover:underline">{repo_name}</span>
|
|
205
|
-
</a>
|
|
206
|
-
</div>
|
|
207
|
-
'''
|
|
208
|
-
|
|
209
|
-
# Generate the complete HTML following the design spec
|
|
210
|
-
html = f"""<!DOCTYPE html>
|
|
211
|
-
<html lang="en">
|
|
212
|
-
<head>
|
|
213
|
-
<meta charset="UTF-8">
|
|
214
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
215
|
-
<title>Microlensing Data Challenge Submission Dossier - {submission.team_name}</title>
|
|
216
|
-
<script src="https://cdn.tailwindcss.com"></script>
|
|
217
|
-
<script>
|
|
218
|
-
tailwind.config = {{
|
|
219
|
-
theme: {{
|
|
220
|
-
extend: {{
|
|
221
|
-
colors: {{
|
|
222
|
-
'rtd-primary': '#dfc5fa',
|
|
223
|
-
'rtd-secondary': '#361d49',
|
|
224
|
-
'rtd-accent': '#a859e4',
|
|
225
|
-
'rtd-background': '#faf7fd',
|
|
226
|
-
'rtd-text': '#000',
|
|
227
|
-
}},
|
|
228
|
-
fontFamily: {{
|
|
229
|
-
inter: ['Inter', 'sans-serif'],
|
|
230
|
-
}},
|
|
231
|
-
}},
|
|
232
|
-
}},
|
|
233
|
-
}};
|
|
234
|
-
</script>
|
|
235
|
-
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
|
236
|
-
<!-- Highlight.js for code syntax highlighting -->
|
|
237
|
-
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
|
|
238
|
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
239
|
-
<script>hljs.highlightAll();</script>
|
|
240
|
-
<style>
|
|
241
|
-
.prose {{
|
|
242
|
-
color: #000;
|
|
243
|
-
line-height: 1.6;
|
|
244
|
-
}}
|
|
245
|
-
.prose h1 {{
|
|
246
|
-
font-size: 1.5rem;
|
|
247
|
-
font-weight: 700;
|
|
248
|
-
color: #361d49;
|
|
249
|
-
margin-top: 1.5rem;
|
|
250
|
-
margin-bottom: 0.75rem;
|
|
251
|
-
}}
|
|
252
|
-
.prose h2 {{
|
|
253
|
-
font-size: 1.25rem;
|
|
254
|
-
font-weight: 600;
|
|
255
|
-
color: #361d49;
|
|
256
|
-
margin-top: 1.25rem;
|
|
257
|
-
margin-bottom: 0.5rem;
|
|
258
|
-
}}
|
|
259
|
-
.prose h3 {{
|
|
260
|
-
font-size: 1.125rem;
|
|
261
|
-
font-weight: 600;
|
|
262
|
-
color: #a859e4;
|
|
263
|
-
margin-top: 1rem;
|
|
264
|
-
margin-bottom: 0.5rem;
|
|
265
|
-
}}
|
|
266
|
-
.prose p {{
|
|
267
|
-
margin-bottom: 0.75rem;
|
|
268
|
-
}}
|
|
269
|
-
.prose ul, .prose ol {{
|
|
270
|
-
margin-left: 1.5rem;
|
|
271
|
-
margin-bottom: 0.75rem;
|
|
272
|
-
}}
|
|
273
|
-
.prose ul {{ list-style-type: disc; }}
|
|
274
|
-
.prose ol {{ list-style-type: decimal; }}
|
|
275
|
-
.prose li {{
|
|
276
|
-
margin-bottom: 0.25rem;
|
|
277
|
-
}}
|
|
278
|
-
.prose code {{
|
|
279
|
-
background: #f3f3f3;
|
|
280
|
-
padding: 2px 4px;
|
|
281
|
-
border-radius: 4px;
|
|
282
|
-
font-family: 'Courier New', monospace;
|
|
283
|
-
font-size: 0.875rem;
|
|
284
|
-
}}
|
|
285
|
-
.prose pre {{
|
|
286
|
-
background: #f8f8f8;
|
|
287
|
-
padding: 1rem;
|
|
288
|
-
border-radius: 8px;
|
|
289
|
-
overflow-x: auto;
|
|
290
|
-
margin: 1rem 0;
|
|
291
|
-
border: 1px solid #e5e5e5;
|
|
292
|
-
}}
|
|
293
|
-
.prose pre code {{
|
|
294
|
-
background: none;
|
|
295
|
-
padding: 0;
|
|
296
|
-
}}
|
|
297
|
-
.prose blockquote {{
|
|
298
|
-
border-left: 4px solid #a859e4;
|
|
299
|
-
padding-left: 1rem;
|
|
300
|
-
margin: 1rem 0;
|
|
301
|
-
font-style: italic;
|
|
302
|
-
color: #666;
|
|
303
|
-
}}
|
|
304
|
-
</style>
|
|
305
|
-
</head>
|
|
306
|
-
<body class="font-inter bg-rtd-background">
|
|
307
|
-
<div class="max-w-7xl mx-auto p-6 lg:p-8">
|
|
308
|
-
<div class="bg-white shadow-xl rounded-lg">
|
|
309
|
-
<!-- Header Section -->
|
|
310
|
-
<div class="text-center py-8">
|
|
311
|
-
<img src="./assets/rges-pit_logo.png" alt="RGES-PIT Logo" class="w-48 mx-auto mb-6">
|
|
312
|
-
<h1 class="text-4xl font-bold text-rtd-secondary text-center mb-2">
|
|
313
|
-
Microlensing Data Challenge Submission Dossier
|
|
314
|
-
</h1>
|
|
315
|
-
<p class="text-xl text-rtd-accent text-center mb-8">
|
|
316
|
-
Team: {submission.team_name or 'Not specified'} | Tier: {submission.tier or 'Not specified'}
|
|
317
|
-
</p>
|
|
318
|
-
{github_html}
|
|
319
|
-
</div>
|
|
320
|
-
|
|
321
|
-
<hr class="border-t-4 border-rtd-accent my-8 mx-8">
|
|
322
|
-
|
|
323
|
-
<!-- Regex Start -->
|
|
324
|
-
|
|
325
|
-
<!-- Submission Summary Section -->
|
|
326
|
-
<section class="mb-10 px-8">
|
|
327
|
-
<h2 class="text-2xl font-semibold text-rtd-secondary mb-4">Submission Overview</h2>
|
|
328
|
-
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
329
|
-
<div class="bg-rtd-primary p-6 rounded-lg shadow-md text-center">
|
|
330
|
-
<p class="text-sm font-medium text-rtd-secondary">Total Events Submitted</p>
|
|
331
|
-
<p class="text-4xl font-bold text-rtd-accent mt-2">{total_events}</p>
|
|
332
|
-
</div>
|
|
333
|
-
<div class="bg-rtd-primary p-6 rounded-lg shadow-md text-center">
|
|
334
|
-
<p class="text-sm font-medium text-rtd-secondary">Total Active Solutions</p>
|
|
335
|
-
<p class="text-4xl font-bold text-rtd-accent mt-2">{total_active_solutions}</p>
|
|
336
|
-
</div>
|
|
337
|
-
<div class="bg-rtd-primary p-6 rounded-lg shadow-md text-center">
|
|
338
|
-
<p class="text-sm font-medium text-rtd-secondary">Hardware Information</p>
|
|
339
|
-
<p class="text-lg text-rtd-text mt-2">{hardware_info_str}</p>
|
|
340
|
-
</div>
|
|
341
|
-
</div>
|
|
342
|
-
</section>
|
|
343
|
-
|
|
344
|
-
<!-- Overall Progress & Compute Time -->
|
|
345
|
-
<section class="mb-10 px-8">
|
|
346
|
-
<h2 class="text-2xl font-semibold text-rtd-secondary mb-4">Challenge Progress & Compute Summary</h2>
|
|
347
|
-
|
|
348
|
-
<!-- Progress Bar -->
|
|
349
|
-
<div class="w-full bg-gray-200 rounded-full h-4 mb-4">
|
|
350
|
-
<div class="bg-rtd-accent h-4 rounded-full" style="width: {progress_percentage}%"></div>
|
|
351
|
-
</div>
|
|
352
|
-
<p class="text-sm text-rtd-text text-center mb-6">
|
|
353
|
-
{total_events} / {TOTAL_CHALLENGE_EVENTS} Events Processed ({progress_percentage:.1f}%)
|
|
354
|
-
</p>
|
|
355
|
-
|
|
356
|
-
<!-- Compute Time Summary -->
|
|
357
|
-
<div class="text-lg text-rtd-text mb-2">
|
|
358
|
-
<p><strong>Total CPU Hours:</strong> {total_cpu_hours:.2f}</p>
|
|
359
|
-
<p><strong>Total Wall Time Hours:</strong> {total_wall_time_hours:.2f}</p>
|
|
360
|
-
</div>
|
|
361
|
-
<p class="text-sm text-gray-500 italic">
|
|
362
|
-
Note: Comparison to other teams' compute times is available in the Evaluator Dossier.
|
|
363
|
-
</p>
|
|
364
|
-
</section>
|
|
365
|
-
|
|
366
|
-
<!-- Event List -->
|
|
367
|
-
<section class="mb-10 px-8">
|
|
368
|
-
<h2 class="text-2xl font-semibold text-rtd-secondary mb-4">Submitted Events</h2>
|
|
369
|
-
<table class="w-full text-left table-auto border-collapse">
|
|
370
|
-
<thead class="bg-rtd-primary text-rtd-secondary uppercase text-sm">
|
|
371
|
-
<tr>
|
|
372
|
-
<th class="py-3 px-4">Event ID</th>
|
|
373
|
-
<th class="py-3 px-4">Active Solutions</th>
|
|
374
|
-
<th class="py-3 px-4">Model Types Submitted</th>
|
|
375
|
-
</tr>
|
|
376
|
-
</thead>
|
|
377
|
-
<tbody class="text-rtd-text">
|
|
378
|
-
{event_table}
|
|
379
|
-
</tbody>
|
|
380
|
-
</table>
|
|
381
|
-
</section>
|
|
382
|
-
|
|
383
|
-
<!-- Aggregate Parameter Distributions (Placeholders) -->
|
|
384
|
-
<section class="mb-10 px-8">
|
|
385
|
-
<h2 class="text-2xl font-semibold text-rtd-secondary mb-4">Aggregate Parameter Distributions</h2>
|
|
386
|
-
<p class="text-sm text-gray-500 italic mb-4">
|
|
387
|
-
Note: These plots show distributions from <em>your</em> submitted solutions. Comparisons to simulation truths and other teams' results are available in the Evaluator Dossier.
|
|
388
|
-
</p>
|
|
389
|
-
|
|
390
|
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
391
|
-
<div class="text-center">
|
|
392
|
-
<img src="https://placehold.co/600x300/dfc5fa/361d49?text=tE+Distribution+from+Your+Solutions"
|
|
393
|
-
alt="tE Distribution" class="w-full rounded-lg shadow-md">
|
|
394
|
-
<p class="text-sm text-gray-600 mt-2">Histogram of Einstein Crossing Times (tE) from your active solutions.</p>
|
|
395
|
-
</div>
|
|
396
|
-
<div class="text-center">
|
|
397
|
-
<img src="https://placehold.co/600x300/dfc5fa/361d49?text=u0+Distribution+from+Your+Solutions"
|
|
398
|
-
alt="u0 Distribution" class="w-full rounded-lg shadow-md">
|
|
399
|
-
<p class="text-sm text-gray-600 mt-2">Histogram of Impact Parameters (u0) from your active solutions.</p>
|
|
400
|
-
</div>
|
|
401
|
-
<div class="text-center">
|
|
402
|
-
<img src="https://placehold.co/600x300/dfc5fa/361d49?text=Lens+Mass+Distribution+from+Your+Solutions"
|
|
403
|
-
alt="M_L Distribution" class="w-full rounded-lg shadow-md">
|
|
404
|
-
<p class="text-sm text-gray-600 mt-2">Histogram of derived Lens Masses (M_L) from your active solutions.</p>
|
|
405
|
-
</div>
|
|
406
|
-
<div class="text-center">
|
|
407
|
-
<img src="https://placehold.co/600x300/dfc5fa/361d49?text=Lens+Distance+Distribution+from+Your+Solutions"
|
|
408
|
-
alt="D_L Distribution" class="w-full rounded-lg shadow-md">
|
|
409
|
-
<p class="text-sm text-gray-600 mt-2">Histogram of derived Lens Distances (D_L) from your active solutions.</p>
|
|
410
|
-
</div>
|
|
411
|
-
</div>
|
|
412
|
-
</section>
|
|
413
|
-
{print_link_html}
|
|
414
|
-
|
|
415
|
-
<!-- Footer -->
|
|
416
|
-
<div class="text-sm text-gray-500 text-center pt-8 pb-6">
|
|
417
|
-
Generated by microlens-submit v0.12.2 on {datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')}
|
|
418
|
-
</div>
|
|
419
|
-
|
|
420
|
-
<!-- Regex Finish -->
|
|
421
|
-
|
|
422
|
-
</div>
|
|
423
|
-
</div>
|
|
424
|
-
</body>
|
|
425
|
-
</html>"""
|
|
426
|
-
|
|
427
|
-
return html
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
def _format_hardware_info(hardware_info: Optional[Dict[str, Any]]) -> str:
|
|
431
|
-
"""Format hardware information for display in the dashboard.
|
|
432
|
-
|
|
433
|
-
Converts hardware information dictionary into a human-readable string
|
|
434
|
-
suitable for display in the dashboard. Handles various hardware info
|
|
435
|
-
formats and provides fallbacks for missing information.
|
|
436
|
-
|
|
437
|
-
Args:
|
|
438
|
-
hardware_info: Dictionary containing hardware information. Can include
|
|
439
|
-
keys like 'cpu_details', 'cpu', 'memory_gb', 'ram_gb', 'nexus_image'.
|
|
440
|
-
If None or empty, returns "Not specified".
|
|
441
|
-
|
|
442
|
-
Returns:
|
|
443
|
-
str: Formatted hardware information string for display.
|
|
444
|
-
|
|
445
|
-
Example:
|
|
446
|
-
>>> hardware_info = {
|
|
447
|
-
... 'cpu_details': 'Intel Xeon E5-2680 v4',
|
|
448
|
-
... 'memory_gb': 64,
|
|
449
|
-
... 'nexus_image': 'roman-science-platform:latest'
|
|
450
|
-
... }
|
|
451
|
-
>>> _format_hardware_info(hardware_info)
|
|
452
|
-
'CPU: Intel Xeon E5-2680 v4, RAM: 64GB, Platform: Roman Nexus'
|
|
453
|
-
|
|
454
|
-
>>> _format_hardware_info(None)
|
|
455
|
-
'Not specified'
|
|
456
|
-
|
|
457
|
-
>>> _format_hardware_info({'custom_field': 'custom_value'})
|
|
458
|
-
'custom_field: custom_value'
|
|
459
|
-
|
|
460
|
-
Note:
|
|
461
|
-
This function handles multiple hardware info formats for compatibility
|
|
462
|
-
with different submission sources. It prioritizes detailed CPU info
|
|
463
|
-
over basic CPU info and provides fallbacks for missing data.
|
|
464
|
-
"""
|
|
465
|
-
if not hardware_info:
|
|
466
|
-
return "Not specified"
|
|
467
|
-
|
|
468
|
-
parts = []
|
|
469
|
-
|
|
470
|
-
# Common hardware fields
|
|
471
|
-
if 'cpu_details' in hardware_info:
|
|
472
|
-
parts.append(f"CPU: {hardware_info['cpu_details']}")
|
|
473
|
-
elif 'cpu' in hardware_info:
|
|
474
|
-
parts.append(f"CPU: {hardware_info['cpu']}")
|
|
475
|
-
|
|
476
|
-
if 'memory_gb' in hardware_info:
|
|
477
|
-
parts.append(f"RAM: {hardware_info['memory_gb']}GB")
|
|
478
|
-
elif 'ram_gb' in hardware_info:
|
|
479
|
-
parts.append(f"RAM: {hardware_info['ram_gb']}GB")
|
|
480
|
-
|
|
481
|
-
if 'nexus_image' in hardware_info:
|
|
482
|
-
parts.append(f"Platform: Roman Nexus")
|
|
483
|
-
|
|
484
|
-
if parts:
|
|
485
|
-
return ", ".join(parts)
|
|
486
|
-
else:
|
|
487
|
-
# Fallback: show any available info
|
|
488
|
-
return ", ".join(f"{k}: {v}" for k, v in hardware_info.items() if v is not None)
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
def generate_event_page(event: Event, submission: Submission, output_dir: Path) -> None:
|
|
492
|
-
"""Generate an HTML dossier page for a single event.
|
|
493
|
-
|
|
494
|
-
Creates a detailed HTML page for a specific microlensing event, following
|
|
495
|
-
the Event_Page_Design.md specification. The page includes event overview,
|
|
496
|
-
solutions table, and evaluator-only visualizations.
|
|
497
|
-
|
|
498
|
-
Args:
|
|
499
|
-
event: The Event object containing solutions and metadata.
|
|
500
|
-
submission: The parent Submission object for context and metadata.
|
|
501
|
-
output_dir: The dossier directory where the HTML file will be saved.
|
|
502
|
-
The file will be named {event.event_id}.html.
|
|
503
|
-
|
|
504
|
-
Raises:
|
|
505
|
-
OSError: If unable to write the HTML file.
|
|
506
|
-
ValueError: If event data is invalid.
|
|
507
|
-
|
|
508
|
-
Example:
|
|
509
|
-
>>> from microlens_submit import load
|
|
510
|
-
>>> from microlens_submit.dossier import generate_event_page
|
|
511
|
-
>>> from pathlib import Path
|
|
512
|
-
>>>
|
|
513
|
-
>>> submission = load("./my_project")
|
|
514
|
-
>>> event = submission.get_event("EVENT001")
|
|
515
|
-
>>>
|
|
516
|
-
>>> # Generate event page
|
|
517
|
-
>>> generate_event_page(event, submission, Path("./dossier_output"))
|
|
518
|
-
>>>
|
|
519
|
-
>>> # Creates: ./dossier_output/EVENT001.html
|
|
520
|
-
|
|
521
|
-
Note:
|
|
522
|
-
This function also triggers generation of solution pages for all
|
|
523
|
-
solutions in the event. The event page includes navigation links
|
|
524
|
-
to individual solution pages.
|
|
525
|
-
"""
|
|
526
|
-
# Prepare output directory (already created)
|
|
527
|
-
html = _generate_event_page_content(event, submission)
|
|
528
|
-
with (output_dir / f"{event.event_id}.html").open("w", encoding="utf-8") as f:
|
|
529
|
-
f.write(html)
|
|
530
|
-
|
|
531
|
-
# After generating the event page, generate solution pages
|
|
532
|
-
for sol in event.solutions.values():
|
|
533
|
-
generate_solution_page(sol, event, submission, output_dir)
|
|
534
|
-
|
|
535
|
-
def _generate_event_page_content(event: Event, submission: Submission) -> str:
|
|
536
|
-
"""Generate the HTML content for an event dossier page.
|
|
537
|
-
|
|
538
|
-
Creates the complete HTML content for a single event page, including
|
|
539
|
-
event overview, solutions table with sorting, and evaluator-only
|
|
540
|
-
visualization placeholders.
|
|
541
|
-
|
|
542
|
-
Args:
|
|
543
|
-
event: The Event object containing solutions and metadata.
|
|
544
|
-
submission: The parent Submission object for context and metadata.
|
|
545
|
-
|
|
546
|
-
Returns:
|
|
547
|
-
str: Complete HTML content as a string for the event page.
|
|
548
|
-
|
|
549
|
-
Example:
|
|
550
|
-
>>> from microlens_submit import load
|
|
551
|
-
>>> from microlens_submit.dossier import _generate_event_page_content
|
|
552
|
-
>>>
|
|
553
|
-
>>> submission = load("./my_project")
|
|
554
|
-
>>> event = submission.get_event("EVENT001")
|
|
555
|
-
>>> html_content = _generate_event_page_content(event, submission)
|
|
556
|
-
>>>
|
|
557
|
-
>>> # Write to file
|
|
558
|
-
>>> with open("event_page.html", "w") as f:
|
|
559
|
-
... f.write(html_content)
|
|
560
|
-
|
|
561
|
-
Note:
|
|
562
|
-
Solutions are sorted by: active status (active first), relative
|
|
563
|
-
probability (descending), then solution ID. The page includes
|
|
564
|
-
navigation back to the dashboard and links to individual solution pages.
|
|
565
|
-
"""
|
|
566
|
-
# Sort solutions: active first, then by relative_probability (desc, None last), then by solution_id
|
|
567
|
-
def sort_key(sol):
|
|
568
|
-
return (
|
|
569
|
-
not sol.is_active, # active first
|
|
570
|
-
-(sol.relative_probability if sol.relative_probability is not None else float('-inf')),
|
|
571
|
-
sol.solution_id
|
|
572
|
-
)
|
|
573
|
-
solutions = sorted(event.solutions.values(), key=sort_key)
|
|
574
|
-
# Table rows
|
|
575
|
-
rows = []
|
|
576
|
-
for sol in solutions:
|
|
577
|
-
status = '<span class="text-green-600">Active</span>' if sol.is_active else '<span class="text-red-600">Inactive</span>'
|
|
578
|
-
logl = f"{sol.log_likelihood:.2f}" if sol.log_likelihood is not None else "N/A"
|
|
579
|
-
ndp = str(sol.n_data_points) if sol.n_data_points is not None else "N/A"
|
|
580
|
-
relprob = f"{sol.relative_probability:.3f}" if sol.relative_probability is not None else "N/A"
|
|
581
|
-
# Read notes snippet from file
|
|
582
|
-
notes_snip = (sol.get_notes(project_root=Path(submission.project_path))[:50] + ("..." if len(sol.get_notes(project_root=Path(submission.project_path))) > 50 else "")) if sol.notes_path else ""
|
|
583
|
-
rows.append(f"""
|
|
584
|
-
<tr class='border-b border-gray-200 hover:bg-gray-50'>
|
|
585
|
-
<td class='py-3 px-4'>
|
|
586
|
-
<a href="{sol.solution_id}.html" class="font-medium text-rtd-accent hover:underline">{sol.solution_id[:8]}...</a>
|
|
587
|
-
</td>
|
|
588
|
-
<td class='py-3 px-4'>{sol.model_type}</td>
|
|
589
|
-
<td class='py-3 px-4'>{status}</td>
|
|
590
|
-
<td class='py-3 px-4'>{logl}</td>
|
|
591
|
-
<td class='py-3 px-4'>{ndp}</td>
|
|
592
|
-
<td class='py-3 px-4'>{relprob}</td>
|
|
593
|
-
<td class='py-3 px-4 text-gray-600 italic'>{notes_snip}</td>
|
|
594
|
-
</tr>
|
|
595
|
-
""")
|
|
596
|
-
table_body = "\n".join(rows) if rows else """
|
|
597
|
-
<tr class='border-b border-gray-200'><td colspan='7' class='py-3 px-4 text-center text-gray-500'>No solutions found</td></tr>
|
|
598
|
-
"""
|
|
599
|
-
# Optional raw data link
|
|
600
|
-
raw_data_html = ""
|
|
601
|
-
if hasattr(event, "event_data_path") and event.event_data_path:
|
|
602
|
-
raw_data_html = f'<p class="text-rtd-text">Raw Event Data: <a href="{event.event_data_path}" class="text-rtd-accent hover:underline">Download Data</a></p>'
|
|
603
|
-
# HTML content
|
|
604
|
-
html = f"""<!DOCTYPE html>
|
|
605
|
-
<html lang='en'>
|
|
606
|
-
<head>
|
|
607
|
-
<meta charset='UTF-8'>
|
|
608
|
-
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
|
609
|
-
<title>Event Dossier: {event.event_id} - {submission.team_name}</title>
|
|
610
|
-
<script src='https://cdn.tailwindcss.com'></script>
|
|
611
|
-
<script>
|
|
612
|
-
tailwind.config = {{
|
|
613
|
-
theme: {{
|
|
614
|
-
extend: {{
|
|
615
|
-
colors: {{
|
|
616
|
-
'rtd-primary': '#dfc5fa',
|
|
617
|
-
'rtd-secondary': '#361d49',
|
|
618
|
-
'rtd-accent': '#a859e4',
|
|
619
|
-
'rtd-background': '#faf7fd',
|
|
620
|
-
'rtd-text': '#000',
|
|
621
|
-
}},
|
|
622
|
-
fontFamily: {{
|
|
623
|
-
inter: ['Inter', 'sans-serif'],
|
|
624
|
-
}},
|
|
625
|
-
}},
|
|
626
|
-
}},
|
|
627
|
-
}};
|
|
628
|
-
</script>
|
|
629
|
-
<link href='https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap' rel='stylesheet'>
|
|
630
|
-
<!-- Highlight.js for code syntax highlighting -->
|
|
631
|
-
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
|
|
632
|
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
633
|
-
<script>hljs.highlightAll();</script>
|
|
634
|
-
<style>
|
|
635
|
-
.prose {{
|
|
636
|
-
color: #000;
|
|
637
|
-
line-height: 1.6;
|
|
638
|
-
}}
|
|
639
|
-
.prose h1 {{
|
|
640
|
-
font-size: 1.5rem;
|
|
641
|
-
font-weight: 700;
|
|
642
|
-
color: #361d49;
|
|
643
|
-
margin-top: 1.5rem;
|
|
644
|
-
margin-bottom: 0.75rem;
|
|
645
|
-
}}
|
|
646
|
-
.prose h2 {{
|
|
647
|
-
font-size: 1.25rem;
|
|
648
|
-
font-weight: 600;
|
|
649
|
-
color: #361d49;
|
|
650
|
-
margin-top: 1.25rem;
|
|
651
|
-
margin-bottom: 0.5rem;
|
|
652
|
-
}}
|
|
653
|
-
.prose h3 {{
|
|
654
|
-
font-size: 1.125rem;
|
|
655
|
-
font-weight: 600;
|
|
656
|
-
color: #a859e4;
|
|
657
|
-
margin-top: 1rem;
|
|
658
|
-
margin-bottom: 0.5rem;
|
|
659
|
-
}}
|
|
660
|
-
.prose p {{
|
|
661
|
-
margin-bottom: 0.75rem;
|
|
662
|
-
}}
|
|
663
|
-
.prose ul, .prose ol {{
|
|
664
|
-
margin-left: 1.5rem;
|
|
665
|
-
margin-bottom: 0.75rem;
|
|
666
|
-
}}
|
|
667
|
-
.prose ul {{ list-style-type: disc; }}
|
|
668
|
-
.prose ol {{ list-style-type: decimal; }}
|
|
669
|
-
.prose li {{
|
|
670
|
-
margin-bottom: 0.25rem;
|
|
671
|
-
}}
|
|
672
|
-
.prose code {{
|
|
673
|
-
background: #f3f3f3;
|
|
674
|
-
padding: 2px 4px;
|
|
675
|
-
border-radius: 4px;
|
|
676
|
-
font-family: 'Courier New', monospace;
|
|
677
|
-
font-size: 0.875rem;
|
|
678
|
-
}}
|
|
679
|
-
.prose pre {{
|
|
680
|
-
background: #f8f8f8;
|
|
681
|
-
padding: 1rem;
|
|
682
|
-
border-radius: 8px;
|
|
683
|
-
overflow-x: auto;
|
|
684
|
-
margin: 1rem 0;
|
|
685
|
-
border: 1px solid #e5e5e5;
|
|
686
|
-
}}
|
|
687
|
-
.prose pre code {{
|
|
688
|
-
background: none;
|
|
689
|
-
padding: 0;
|
|
690
|
-
}}
|
|
691
|
-
.prose blockquote {{
|
|
692
|
-
border-left: 4px solid #a859e4;
|
|
693
|
-
padding-left: 1rem;
|
|
694
|
-
margin: 1rem 0;
|
|
695
|
-
font-style: italic;
|
|
696
|
-
color: #666;
|
|
697
|
-
}}
|
|
698
|
-
</style>
|
|
699
|
-
</head>
|
|
700
|
-
<body class='font-inter bg-rtd-background'>
|
|
701
|
-
<div class='max-w-7xl mx-auto p-6 lg:p-8'>
|
|
702
|
-
<div class='bg-white shadow-xl rounded-lg'>
|
|
703
|
-
<!-- Header & Navigation -->
|
|
704
|
-
<div class='text-center py-8'>
|
|
705
|
-
<img src='assets/rges-pit_logo.png' alt='RGES-PIT Logo' class='w-48 mx-auto mb-6'>
|
|
706
|
-
<h1 class='text-4xl font-bold text-rtd-secondary text-center mb-2'>Event Dossier: {event.event_id}</h1>
|
|
707
|
-
<p class='text-xl text-rtd-accent text-center mb-4'>Team: {submission.team_name or 'Not specified'} | Tier: {submission.tier or 'Not specified'}</p>
|
|
708
|
-
<nav class='flex justify-center space-x-4 mb-8'>
|
|
709
|
-
<a href='index.html' class='text-rtd-accent hover:underline'>← Back to Dashboard</a>
|
|
710
|
-
</nav>
|
|
711
|
-
</div>
|
|
712
|
-
|
|
713
|
-
<hr class="border-t-4 border-rtd-accent my-8 mx-8">
|
|
714
|
-
|
|
715
|
-
<!-- Regex Start -->
|
|
716
|
-
|
|
717
|
-
<!-- Event Summary -->
|
|
718
|
-
<section class='mb-10 px-8'>
|
|
719
|
-
<h2 class='text-2xl font-semibold text-rtd-secondary mb-4'>Event Overview</h2>
|
|
720
|
-
<p class='text-rtd-text'>This page provides details for microlensing event {event.event_id}.</p>
|
|
721
|
-
{raw_data_html}
|
|
722
|
-
</section>
|
|
723
|
-
<!-- Solutions Table -->
|
|
724
|
-
<section class='mb-10 px-8'>
|
|
725
|
-
<h2 class='text-2xl font-semibold text-rtd-secondary mb-4'>Solutions for Event {event.event_id}</h2>
|
|
726
|
-
<table class='w-full text-left table-auto border-collapse'>
|
|
727
|
-
<thead class='bg-rtd-primary text-rtd-secondary uppercase text-sm'>
|
|
728
|
-
<tr>
|
|
729
|
-
<th class='py-3 px-4'>Solution ID</th>
|
|
730
|
-
<th class='py-3 px-4'>Model Type</th>
|
|
731
|
-
<th class='py-3 px-4'>Status</th>
|
|
732
|
-
<th class='py-3 px-4'>Log-Likelihood</th>
|
|
733
|
-
<th class='py-3 px-4'>N Data Points</th>
|
|
734
|
-
<th class='py-3 px-4'>Relative Probability</th>
|
|
735
|
-
<th class='py-3 px-4'>Notes Snippet</th>
|
|
736
|
-
</tr>
|
|
737
|
-
</thead>
|
|
738
|
-
<tbody class='text-rtd-text'>
|
|
739
|
-
{table_body}
|
|
740
|
-
</tbody>
|
|
741
|
-
</table>
|
|
742
|
-
</section>
|
|
743
|
-
<!-- Event-Specific Data Visualizations (Evaluator-Only Placeholders) -->
|
|
744
|
-
<section class='mb-10 px-8'>
|
|
745
|
-
<h2 class='text-2xl font-semibold text-rtd-secondary mb-4'>Event Data Visualizations (Evaluator-Only)</h2>
|
|
746
|
-
<p class='text-sm text-gray-500 italic mb-4'>Note: These advanced plots, including comparisons to simulation truths and other teams' results, are available in the Evaluator Dossier.</p>
|
|
747
|
-
<div class='mb-6'>
|
|
748
|
-
<img src='https://placehold.co/800x450/dfc5fa/361d49?text=Raw+Lightcurve+and+Astrometry+Data+(Evaluator+Only)' alt='Raw Data Plot' class='w-full rounded-lg shadow-md'>
|
|
749
|
-
<p class='text-sm text-gray-600 mt-2'>Raw lightcurve and astrometry data for Event {event.event_id}, with true model overlaid (Evaluator View).</p>
|
|
750
|
-
</div>
|
|
751
|
-
<div class='mb-6'>
|
|
752
|
-
<img src='https://placehold.co/600x400/dfc5fa/361d49?text=Mass+vs+Distance+Scatter+Plot+(Evaluator+Only)' alt='Mass vs Distance Plot' class='w-full rounded-lg shadow-md'>
|
|
753
|
-
<p class='text-sm text-gray-600 mt-2'>Derived Lens Mass vs. Lens Distance for solutions of Event {event.event_id}. Points colored by Relative Probability (Evaluator View).</p>
|
|
754
|
-
</div>
|
|
755
|
-
<div class='mb-6'>
|
|
756
|
-
<img src='https://placehold.co/600x400/dfc5fa/361d49?text=Proper+Motion+N+vs+E+Plot+(Evaluator+Only)' alt='Proper Motion Plot' class='w-full rounded-lg shadow-md'>
|
|
757
|
-
<p class='text-sm text-gray-600 mt-2'>Proper Motion North vs. East components for solutions of Event {event.event_id}. Points colored by Relative Probability (Evaluator View).</p>
|
|
758
|
-
</div>
|
|
759
|
-
</section>
|
|
760
|
-
|
|
761
|
-
<!-- Footer -->
|
|
762
|
-
<div class='text-sm text-gray-500 text-center pt-8 border-t border-gray-200 mt-10'>
|
|
763
|
-
Generated by microlens-submit v0.12.2 on {datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')}
|
|
764
|
-
</div>
|
|
765
|
-
|
|
766
|
-
<!-- Regex Finish -->
|
|
767
|
-
|
|
768
|
-
</div>
|
|
769
|
-
</div>
|
|
770
|
-
</body>
|
|
771
|
-
</html>"""
|
|
772
|
-
return html
|
|
773
|
-
|
|
774
|
-
def generate_solution_page(solution: Solution, event: Event, submission: Submission, output_dir: Path) -> None:
|
|
775
|
-
"""Generate an HTML dossier page for a single solution.
|
|
776
|
-
|
|
777
|
-
Creates a detailed HTML page for a specific microlensing solution, following
|
|
778
|
-
the Solution_Page_Design.md specification. The page includes solution overview,
|
|
779
|
-
parameter tables, notes (with markdown rendering), and evaluator-only sections.
|
|
780
|
-
|
|
781
|
-
Args:
|
|
782
|
-
solution: The Solution object containing parameters, notes, and metadata.
|
|
783
|
-
event: The parent Event object for context and navigation.
|
|
784
|
-
submission: The grandparent Submission object for context and metadata.
|
|
785
|
-
output_dir: The dossier directory where the HTML file will be saved.
|
|
786
|
-
The file will be named {solution.solution_id}.html.
|
|
787
|
-
|
|
788
|
-
Raises:
|
|
789
|
-
OSError: If unable to write the HTML file or read notes file.
|
|
790
|
-
ValueError: If solution data is invalid.
|
|
791
|
-
|
|
792
|
-
Example:
|
|
793
|
-
>>> from microlens_submit import load
|
|
794
|
-
>>> from microlens_submit.dossier import generate_solution_page
|
|
795
|
-
>>> from pathlib import Path
|
|
796
|
-
>>>
|
|
797
|
-
>>> submission = load("./my_project")
|
|
798
|
-
>>> event = submission.get_event("EVENT001")
|
|
799
|
-
>>> solution = event.get_solution("solution_uuid_here")
|
|
800
|
-
>>>
|
|
801
|
-
>>> # Generate solution page
|
|
802
|
-
>>> generate_solution_page(solution, event, submission, Path("./dossier_output"))
|
|
803
|
-
>>>
|
|
804
|
-
>>> # Creates: ./dossier_output/solution_uuid_here.html
|
|
805
|
-
|
|
806
|
-
Note:
|
|
807
|
-
The solution page includes GitHub commit links if available, markdown
|
|
808
|
-
rendering for notes, and navigation back to the event page and dashboard.
|
|
809
|
-
Notes are rendered with syntax highlighting for code blocks.
|
|
810
|
-
"""
|
|
811
|
-
# Prepare output directory (already created)
|
|
812
|
-
html = _generate_solution_page_content(solution, event, submission)
|
|
813
|
-
with (output_dir / f"{solution.solution_id}.html").open("w", encoding="utf-8") as f:
|
|
814
|
-
f.write(html)
|
|
815
|
-
|
|
816
|
-
def _generate_solution_page_content(solution: Solution, event: Event, submission: Submission) -> str:
|
|
817
|
-
"""Generate the HTML content for a solution dossier page.
|
|
818
|
-
|
|
819
|
-
Creates the complete HTML content for a single solution page, including
|
|
820
|
-
parameter tables, markdown-rendered notes, plot placeholders, and
|
|
821
|
-
evaluator-only sections.
|
|
822
|
-
|
|
823
|
-
Args:
|
|
824
|
-
solution: The Solution object containing parameters, notes, and metadata.
|
|
825
|
-
event: The parent Event object for context and navigation.
|
|
826
|
-
submission: The grandparent Submission object for context and metadata.
|
|
827
|
-
|
|
828
|
-
Returns:
|
|
829
|
-
str: Complete HTML content as a string for the solution page.
|
|
830
|
-
|
|
831
|
-
Example:
|
|
832
|
-
>>> from microlens_submit import load
|
|
833
|
-
>>> from microlens_submit.dossier import _generate_solution_page_content
|
|
834
|
-
>>>
|
|
835
|
-
>>> submission = load("./my_project")
|
|
836
|
-
>>> event = submission.get_event("EVENT001")
|
|
837
|
-
>>> solution = event.get_solution("solution_uuid_here")
|
|
838
|
-
>>> html_content = _generate_solution_page_content(solution, event, submission)
|
|
839
|
-
>>>
|
|
840
|
-
>>> # Write to file
|
|
841
|
-
>>> with open("solution_page.html", "w") as f:
|
|
842
|
-
... f.write(html_content)
|
|
843
|
-
|
|
844
|
-
Note:
|
|
845
|
-
Parameter uncertainties are formatted as ±value or +upper/-lower
|
|
846
|
-
depending on the uncertainty format. Notes are rendered from markdown
|
|
847
|
-
with syntax highlighting for code blocks. GitHub commit links are
|
|
848
|
-
included if git information is available in compute_info.
|
|
849
|
-
"""
|
|
850
|
-
# Render notes as HTML from file
|
|
851
|
-
notes_md = solution.get_notes(project_root=Path(submission.project_path))
|
|
852
|
-
notes_html = markdown.markdown(notes_md or "", extensions=["extra", "tables", "fenced_code"])
|
|
853
|
-
# Parameters table
|
|
854
|
-
param_rows = []
|
|
855
|
-
params = solution.parameters or {}
|
|
856
|
-
uncertainties = solution.parameter_uncertainties or {}
|
|
857
|
-
for k, v in params.items():
|
|
858
|
-
unc = uncertainties.get(k)
|
|
859
|
-
if unc is None:
|
|
860
|
-
unc_str = "N/A"
|
|
861
|
-
elif isinstance(unc, (list, tuple)) and len(unc) == 2:
|
|
862
|
-
unc_str = f"+{unc[1]}/-{unc[0]}"
|
|
863
|
-
else:
|
|
864
|
-
unc_str = f"±{unc}"
|
|
865
|
-
param_rows.append(f"""
|
|
866
|
-
<tr class='border-b border-gray-200 hover:bg-gray-50'>
|
|
867
|
-
<td class='py-3 px-4'>{k}</td>
|
|
868
|
-
<td class='py-3 px-4'>{v}</td>
|
|
869
|
-
<td class='py-3 px-4'>{unc_str}</td>
|
|
870
|
-
</tr>
|
|
871
|
-
""")
|
|
872
|
-
param_table = "\n".join(param_rows) if param_rows else """
|
|
873
|
-
<tr class='border-b border-gray-200'><td colspan='3' class='py-3 px-4 text-center text-gray-500'>No parameters found</td></tr>
|
|
874
|
-
"""
|
|
875
|
-
# Higher-order effects
|
|
876
|
-
hoe_str = ", ".join(solution.higher_order_effects) if solution.higher_order_effects else "None"
|
|
877
|
-
# Plot paths (relative to solution page)
|
|
878
|
-
lc_plot = solution.lightcurve_plot_path or ""
|
|
879
|
-
lens_plot = solution.lens_plane_plot_path or ""
|
|
880
|
-
posterior = solution.posterior_path or ""
|
|
881
|
-
# Physical parameters table
|
|
882
|
-
phys_rows = []
|
|
883
|
-
phys = solution.physical_parameters or {}
|
|
884
|
-
for k, v in phys.items():
|
|
885
|
-
phys_rows.append(f"""
|
|
886
|
-
<tr class='border-b border-gray-200 hover:bg-gray-50'>
|
|
887
|
-
<td class='py-3 px-4'>{k}</td>
|
|
888
|
-
<td class='py-3 px-4'>{v}</td>
|
|
889
|
-
</tr>
|
|
890
|
-
""")
|
|
891
|
-
phys_table = "\n".join(phys_rows) if phys_rows else """
|
|
892
|
-
<tr class='border-b border-gray-200'><td colspan='2' class='py-3 px-4 text-center text-gray-500'>No physical parameters found</td></tr>
|
|
893
|
-
"""
|
|
894
|
-
# GitHub commit link (if present)
|
|
895
|
-
repo_url = getattr(submission, 'repo_url', None) or (submission.repo_url if hasattr(submission, 'repo_url') else None)
|
|
896
|
-
commit = None
|
|
897
|
-
if solution.compute_info:
|
|
898
|
-
git_info = solution.compute_info.get('git_info')
|
|
899
|
-
if git_info:
|
|
900
|
-
commit = git_info.get('commit')
|
|
901
|
-
commit_html = ""
|
|
902
|
-
if repo_url and commit:
|
|
903
|
-
commit_short = commit[:8]
|
|
904
|
-
commit_url = f"{repo_url.rstrip('/')}/commit/{commit}"
|
|
905
|
-
commit_html = f'''<a href="{commit_url}" target="_blank" rel="noopener" title="View this commit on GitHub" class="inline-flex items-center space-x-1 ml-2 align-middle">
|
|
906
|
-
<img src="assets/github-desktop_logo.png" alt="GitHub Commit" class="w-4 h-4 inline-block align-middle" style="display:inline;vertical-align:middle;">
|
|
907
|
-
<span class="text-xs text-rtd-accent font-mono">{commit_short}</span>
|
|
908
|
-
</a>'''
|
|
909
|
-
# HTML content
|
|
910
|
-
html = f"""<!DOCTYPE html>
|
|
911
|
-
<html lang='en'>
|
|
912
|
-
<head>
|
|
913
|
-
<meta charset='UTF-8'>
|
|
914
|
-
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
|
915
|
-
<title>Solution Dossier: {solution.solution_id[:8]}... - {submission.team_name}</title>
|
|
916
|
-
<script src='https://cdn.tailwindcss.com'></script>
|
|
917
|
-
<script>
|
|
918
|
-
tailwind.config = {{
|
|
919
|
-
theme: {{
|
|
920
|
-
extend: {{
|
|
921
|
-
colors: {{
|
|
922
|
-
'rtd-primary': '#dfc5fa',
|
|
923
|
-
'rtd-secondary': '#361d49',
|
|
924
|
-
'rtd-accent': '#a859e4',
|
|
925
|
-
'rtd-background': '#faf7fd',
|
|
926
|
-
'rtd-text': '#000',
|
|
927
|
-
}},
|
|
928
|
-
fontFamily: {{
|
|
929
|
-
inter: ['Inter', 'sans-serif'],
|
|
930
|
-
}},
|
|
931
|
-
}},
|
|
932
|
-
}},
|
|
933
|
-
}};
|
|
934
|
-
</script>
|
|
935
|
-
<link href='https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap' rel='stylesheet'>
|
|
936
|
-
<!-- Highlight.js for code syntax highlighting -->
|
|
937
|
-
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
|
|
938
|
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
939
|
-
<script>hljs.highlightAll();</script>
|
|
940
|
-
<style>
|
|
941
|
-
.prose {{
|
|
942
|
-
color: #000;
|
|
943
|
-
line-height: 1.6;
|
|
944
|
-
}}
|
|
945
|
-
.prose h1 {{
|
|
946
|
-
font-size: 1.5rem;
|
|
947
|
-
font-weight: 700;
|
|
948
|
-
color: #361d49;
|
|
949
|
-
margin-top: 1.5rem;
|
|
950
|
-
margin-bottom: 0.75rem;
|
|
951
|
-
}}
|
|
952
|
-
.prose h2 {{
|
|
953
|
-
font-size: 1.25rem;
|
|
954
|
-
font-weight: 600;
|
|
955
|
-
color: #361d49;
|
|
956
|
-
margin-top: 1.25rem;
|
|
957
|
-
margin-bottom: 0.5rem;
|
|
958
|
-
}}
|
|
959
|
-
.prose h3 {{
|
|
960
|
-
font-size: 1.125rem;
|
|
961
|
-
font-weight: 600;
|
|
962
|
-
color: #a859e4;
|
|
963
|
-
margin-top: 1rem;
|
|
964
|
-
margin-bottom: 0.5rem;
|
|
965
|
-
}}
|
|
966
|
-
.prose p {{
|
|
967
|
-
margin-bottom: 0.75rem;
|
|
968
|
-
}}
|
|
969
|
-
.prose ul, .prose ol {{
|
|
970
|
-
margin-left: 1.5rem;
|
|
971
|
-
margin-bottom: 0.75rem;
|
|
972
|
-
}}
|
|
973
|
-
.prose ul {{ list-style-type: disc; }}
|
|
974
|
-
.prose ol {{ list-style-type: decimal; }}
|
|
975
|
-
.prose li {{
|
|
976
|
-
margin-bottom: 0.25rem;
|
|
977
|
-
}}
|
|
978
|
-
.prose code {{
|
|
979
|
-
background: #f3f3f3;
|
|
980
|
-
padding: 2px 4px;
|
|
981
|
-
border-radius: 4px;
|
|
982
|
-
font-family: 'Courier New', monospace;
|
|
983
|
-
font-size: 0.875rem;
|
|
984
|
-
}}
|
|
985
|
-
.prose pre {{
|
|
986
|
-
background: #f8f8f8;
|
|
987
|
-
padding: 1rem;
|
|
988
|
-
border-radius: 8px;
|
|
989
|
-
overflow-x: auto;
|
|
990
|
-
margin: 1rem 0;
|
|
991
|
-
border: 1px solid #e5e5e5;
|
|
992
|
-
}}
|
|
993
|
-
.prose pre code {{
|
|
994
|
-
background: none;
|
|
995
|
-
padding: 0;
|
|
996
|
-
}}
|
|
997
|
-
.prose blockquote {{
|
|
998
|
-
border-left: 4px solid #a859e4;
|
|
999
|
-
padding-left: 1rem;
|
|
1000
|
-
margin: 1rem 0;
|
|
1001
|
-
font-style: italic;
|
|
1002
|
-
color: #666;
|
|
1003
|
-
}}
|
|
1004
|
-
</style>
|
|
1005
|
-
</head>
|
|
1006
|
-
<body class='font-inter bg-rtd-background'>
|
|
1007
|
-
<div class='max-w-7xl mx-auto p-6 lg:p-8'>
|
|
1008
|
-
<div class='bg-white shadow-xl rounded-lg'>
|
|
1009
|
-
<!-- Header & Navigation -->
|
|
1010
|
-
<div class='text-center py-8'>
|
|
1011
|
-
<img src='assets/rges-pit_logo.png' alt='RGES-PIT Logo' class='w-48 mx-auto mb-6'>
|
|
1012
|
-
<h1 class='text-4xl font-bold text-rtd-secondary text-center mb-2'>Solution Dossier: {solution.solution_id[:8]}...</h1>
|
|
1013
|
-
<p class='text-xl text-rtd-accent text-center mb-4'>Event: {event.event_id} | Team: {submission.team_name or 'Not specified'} | Tier: {submission.tier or 'Not specified'} {commit_html}</p>
|
|
1014
|
-
<nav class='flex justify-center space-x-4 mb-8'>
|
|
1015
|
-
<a href='{event.event_id}.html' class='text-rtd-accent hover:underline'>← Back to Event {event.event_id}</a>
|
|
1016
|
-
<a href='index.html' class='text-rtd-accent hover:underline'>← Back to Dashboard</a>
|
|
1017
|
-
</nav>
|
|
1018
|
-
</div>
|
|
1019
|
-
|
|
1020
|
-
<hr class="border-t-4 border-rtd-accent my-8 mx-8">
|
|
1021
|
-
|
|
1022
|
-
<!-- Regex Start -->
|
|
1023
|
-
|
|
1024
|
-
<!-- Solution Overview & Notes -->
|
|
1025
|
-
<section class='mb-10 px-8'>
|
|
1026
|
-
<h2 class='text-2xl font-semibold text-rtd-secondary mb-4'>Solution Overview & Notes</h2>
|
|
1027
|
-
<table class='w-full text-left table-auto border-collapse mb-4'>
|
|
1028
|
-
<thead class='bg-rtd-primary text-rtd-secondary uppercase text-sm'>
|
|
1029
|
-
<tr><th>Parameter</th><th>Value</th><th>Uncertainty</th></tr>
|
|
1030
|
-
</thead>
|
|
1031
|
-
<tbody class='text-rtd-text'>
|
|
1032
|
-
{param_table}
|
|
1033
|
-
</tbody>
|
|
1034
|
-
</table>
|
|
1035
|
-
<p class='text-rtd-text mt-4'>Higher-Order Effects: {hoe_str}</p>
|
|
1036
|
-
<h3 class='text-xl font-semibold text-rtd-secondary mt-6 mb-2'>Participant's Detailed Notes</h3>
|
|
1037
|
-
<div class='bg-gray-50 p-4 rounded-lg shadow-inner text-rtd-text prose max-w-none'>{notes_html}</div>
|
|
1038
|
-
</section>
|
|
1039
|
-
<!-- Lightcurve & Lens Plane Visuals -->
|
|
1040
|
-
<section class='mb-10 px-8'>
|
|
1041
|
-
<h2 class='text-2xl font-semibold text-rtd-secondary mb-4'>Lightcurve & Lens Plane Visuals</h2>
|
|
1042
|
-
<div class='grid grid-cols-1 md:grid-cols-2 gap-6'>
|
|
1043
|
-
<div class='text-center bg-rtd-primary p-4 rounded-lg shadow-md'>
|
|
1044
|
-
<img src='{lc_plot}' alt='Lightcurve Plot' class='w-full h-auto rounded-md mb-2'>
|
|
1045
|
-
<p class="text-sm text-rtd-secondary">Caption: Lightcurve fit for Solution {solution.solution_id[:8]}...</p>
|
|
1046
|
-
</div>
|
|
1047
|
-
<div class='text-center bg-rtd-primary p-4 rounded-lg shadow-md'>
|
|
1048
|
-
<img src='{lens_plot}' alt='Lens Plane Plot' class='w-full h-auto rounded-md mb-2'>
|
|
1049
|
-
<p class='text-sm text-rtd-secondary'>Caption: Lens plane geometry for Solution {solution.solution_id[:8]}...</p>
|
|
1050
|
-
</div>
|
|
1051
|
-
</div>
|
|
1052
|
-
{f"<p class='text-rtd-text mt-4 text-center'>Posterior Samples: <a href='{posterior}' class='text-rtd-accent hover:underline'>Download Posterior Data</a></p>" if posterior else ''}
|
|
1053
|
-
</section>
|
|
1054
|
-
<!-- Fit Statistics & Data Utilization -->
|
|
1055
|
-
<section class='mb-10 px-8'>
|
|
1056
|
-
<h2 class='text-2xl font-semibold text-rtd-secondary mb-4'>Fit Statistics & Data Utilization</h2>
|
|
1057
|
-
<div class='grid grid-cols-1 md:grid-cols-2 gap-6'>
|
|
1058
|
-
<div class='bg-rtd-primary p-6 rounded-lg shadow-md text-center'>
|
|
1059
|
-
<p class='text-sm font-medium text-rtd-secondary'>Log-Likelihood</p>
|
|
1060
|
-
<p class='text-4xl font-bold text-rtd-accent mt-2'>{solution.log_likelihood if solution.log_likelihood is not None else 'N/A'}</p>
|
|
1061
|
-
</div>
|
|
1062
|
-
<div class='bg-rtd-primary p-6 rounded-lg shadow-md text-center'>
|
|
1063
|
-
<p class='text-sm font-medium text-rtd-secondary'>N Data Points Used</p>
|
|
1064
|
-
<p class='text-4xl font-bold text-rtd-accent mt-2'>{solution.n_data_points if solution.n_data_points is not None else 'N/A'}</p>
|
|
1065
|
-
</div>
|
|
1066
|
-
</div>
|
|
1067
|
-
<h3 class='text-xl font-semibold text-rtd-secondary mt-6 mb-2'>Data Utilization Ratio</h3>
|
|
1068
|
-
<div class='text-center bg-rtd-primary p-4 rounded-lg shadow-md'>
|
|
1069
|
-
<img src='https://placehold.co/600x100/dfc5fa/361d49?text=Data+Utilization+Infographic' alt='Data Utilization' class='w-full h-auto rounded-md mb-2'>
|
|
1070
|
-
<p class='text-sm text-rtd-secondary'>Caption: Percentage of total event data points utilized in this solution's fit.</p>
|
|
1071
|
-
</div>
|
|
1072
|
-
</section>
|
|
1073
|
-
<!-- Compute Performance -->
|
|
1074
|
-
<section class='mb-10 px-8'>
|
|
1075
|
-
<h2 class='text-2xl font-semibold text-rtd-secondary mb-4'>Compute Performance</h2>
|
|
1076
|
-
<table class='w-full text-left table-auto border-collapse'>
|
|
1077
|
-
<thead class='bg-rtd-primary text-rtd-secondary uppercase text-sm'>
|
|
1078
|
-
<tr><th>Metric</th><th>Your Solution</th><th>Same-Team Average</th><th>All-Submission Average</th></tr>
|
|
1079
|
-
</thead>
|
|
1080
|
-
<tbody class='text-rtd-text'>
|
|
1081
|
-
<tr><td>CPU Hours</td><td>{solution.compute_info.get('cpu_hours', 'N/A') if solution.compute_info else 'N/A'}</td><td>N/A for Participants</td><td>N/A for Participants</td></tr>
|
|
1082
|
-
<tr><td>Wall Time (Hrs)</td><td>{solution.compute_info.get('wall_time_hours', 'N/A') if solution.compute_info else 'N/A'}</td><td>N/A for Participants</td><td>N/A for Participants</td></tr>
|
|
1083
|
-
</tbody>
|
|
1084
|
-
</table>
|
|
1085
|
-
<p class='text-sm text-gray-500 italic mt-4'>Note: Comparison to other teams' compute times is available in the Evaluator Dossier.</p>
|
|
1086
|
-
</section>
|
|
1087
|
-
<!-- Parameter Accuracy vs. Truths (Evaluator-Only) -->
|
|
1088
|
-
<section class='mb-10 px-8'>
|
|
1089
|
-
<h2 class='text-2xl font-semibold text-rtd-secondary mb-4'>Parameter Accuracy vs. Truths (Evaluator-Only)</h2>
|
|
1090
|
-
<p class='text-sm text-gray-500 italic mb-4'>You haven't fucked up. This just isn't for you. Detailed comparisons of your fitted parameters against simulation truths are available in the Evaluator Dossier.</p>
|
|
1091
|
-
<div class='text-center bg-rtd-primary p-4 rounded-lg shadow-md'>
|
|
1092
|
-
<img src='https://placehold.co/800x300/dfc5fa/361d49?text=Parameter+Comparison+Table+(Evaluator+Only)' alt='Parameter Comparison Table' class='w-full h-auto rounded-md mb-2'>
|
|
1093
|
-
<p class='text-sm text-rtd-secondary'>Caption: A table comparing fitted parameters to true values (Evaluator View).</p>
|
|
1094
|
-
</div>
|
|
1095
|
-
<div class='text-center bg-rtd-primary p-4 rounded-lg shadow-md mt-6'>
|
|
1096
|
-
<img src='https://placehold.co/800x400/dfc5fa/361d49?text=Parameter+Difference+Distributions+(Evaluator+Only)' alt='Parameter Difference Distributions' class='w-full h-auto rounded-md mb-2'>
|
|
1097
|
-
<p class='text-sm text-rtd-secondary'>Caption: Distributions of (True - Fit) for key parameters across all challenge submissions (Evaluator View).</p>
|
|
1098
|
-
</div>
|
|
1099
|
-
</section>
|
|
1100
|
-
<!-- Physical Parameter Context (Evaluator-Only) -->
|
|
1101
|
-
<section class='mb-10 px-8'>
|
|
1102
|
-
<h2 class='text-2xl font-semibold text-rtd-secondary mb-4'>Physical Parameter Context (Evaluator-Only)</h2>
|
|
1103
|
-
<p class='text-sm text-gray-500 italic mb-4'>You haven't fucked up. This just isn't for you. Contextual plots of derived physical parameters against population models are available in the Evaluator Dossier.</p>
|
|
1104
|
-
<table class='w-full text-left table-auto border-collapse'>
|
|
1105
|
-
<thead class='bg-rtd-primary text-rtd-secondary uppercase text-sm'>
|
|
1106
|
-
<tr><th>Parameter</th><th>Value</th></tr>
|
|
1107
|
-
</thead>
|
|
1108
|
-
<tbody class='text-rtd-text'>
|
|
1109
|
-
{phys_table}
|
|
1110
|
-
</tbody>
|
|
1111
|
-
</table>
|
|
1112
|
-
<div class='text-center bg-rtd-primary p-4 rounded-lg shadow-md mt-6'>
|
|
1113
|
-
<img src='https://placehold.co/600x400/dfc5fa/361d49?text=Physical+Parameter+Distribution+(Evaluator+Only)' alt='Physical Parameter Distribution' class='w-full h-auto rounded-md mb-2'>
|
|
1114
|
-
<p class='text-sm text-rtd-secondary'>Caption: Your solution's derived physical parameters plotted against a simulated test set (Evaluator View).</p>
|
|
1115
|
-
</div>
|
|
1116
|
-
</section>
|
|
1117
|
-
<!-- Source Properties & CMD (Evaluator-Only) -->
|
|
1118
|
-
<section class='mb-10 px-8'>
|
|
1119
|
-
<h2 class='text-2xl font-semibold text-rtd-secondary mb-4'>Source Properties & CMD (Evaluator-Only)</h2>
|
|
1120
|
-
<p class='text-sm text-gray-500 italic mb-4'>You haven't fucked up. This just isn't for you. Source color and magnitude diagrams are available in the Evaluator Dossier.</p>
|
|
1121
|
-
<div class='text-rtd-text'>
|
|
1122
|
-
<!-- Placeholder for source color/mag details -->
|
|
1123
|
-
</div>
|
|
1124
|
-
<div class='text-center bg-rtd-primary p-4 rounded-lg shadow-md mt-6'>
|
|
1125
|
-
<img src='https://placehold.co/600x400/dfc5fa/361d49?text=Color-Magnitude+Diagram+with+Source+(Evaluator+Only)' alt='Color-Magnitude Diagram' class='w-full h-auto rounded-md mb-2'>
|
|
1126
|
-
<p class='text-sm text-rtd-secondary'>Caption: Color-Magnitude Diagram for the event's field with source marked (Evaluator View).</p>
|
|
1127
|
-
</div>
|
|
1128
|
-
</section>
|
|
1129
|
-
|
|
1130
|
-
<!-- Footer -->
|
|
1131
|
-
<div class='text-sm text-gray-500 text-center pt-8 border-t border-gray-200 mt-10'>
|
|
1132
|
-
Generated by microlens-submit v0.12.2 on {datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')}
|
|
1133
|
-
</div>
|
|
1134
|
-
|
|
1135
|
-
<!-- Regex Finish -->
|
|
1136
|
-
|
|
1137
|
-
</div>
|
|
1138
|
-
</div>
|
|
1139
|
-
</body>
|
|
1140
|
-
</html>"""
|
|
1141
|
-
return html
|
|
1142
|
-
|
|
1143
|
-
def _extract_main_content_body(html: str, section_type: str = None, section_id: str = None) -> str:
|
|
1144
|
-
"""Extract main content for the full dossier using explicit markers.
|
|
1145
|
-
|
|
1146
|
-
Extracts the main content from HTML pages using explicit marker comments.
|
|
1147
|
-
This function is used to create the comprehensive full dossier report by
|
|
1148
|
-
extracting content from individual pages and combining them.
|
|
1149
|
-
|
|
1150
|
-
Args:
|
|
1151
|
-
html: The complete HTML content to extract from.
|
|
1152
|
-
section_type: Type of section being extracted. If None, extracts dashboard
|
|
1153
|
-
content. If 'event' or 'solution', extracts and formats accordingly.
|
|
1154
|
-
section_id: Identifier for the section (event_id or solution_id). Used
|
|
1155
|
-
to create section headings in the full dossier.
|
|
1156
|
-
|
|
1157
|
-
Returns:
|
|
1158
|
-
str: Extracted and formatted HTML content ready for inclusion in
|
|
1159
|
-
the full dossier report.
|
|
1160
|
-
|
|
1161
|
-
Raises:
|
|
1162
|
-
ValueError: If required regex markers are not found in the HTML.
|
|
1163
|
-
|
|
1164
|
-
Example:
|
|
1165
|
-
>>> # Extract dashboard content
|
|
1166
|
-
>>> dashboard_html = _generate_dashboard_content(submission)
|
|
1167
|
-
>>> dashboard_body = _extract_main_content_body(dashboard_html)
|
|
1168
|
-
>>>
|
|
1169
|
-
>>> # Extract event content
|
|
1170
|
-
>>> event_html = _generate_event_page_content(event, submission)
|
|
1171
|
-
>>> event_body = _extract_main_content_body(event_html, 'event', 'EVENT001')
|
|
1172
|
-
>>>
|
|
1173
|
-
>>> # Extract solution content
|
|
1174
|
-
>>> solution_html = _generate_solution_page_content(solution, event, submission)
|
|
1175
|
-
>>> solution_body = _extract_main_content_body(solution_html, 'solution', 'sol_uuid')
|
|
1176
|
-
|
|
1177
|
-
Note:
|
|
1178
|
-
This function relies on HTML comments <!-- Regex Start --> and
|
|
1179
|
-
<!-- Regex Finish --> to identify content boundaries. These markers
|
|
1180
|
-
must be present in the source HTML for extraction to work.
|
|
1181
|
-
"""
|
|
1182
|
-
if section_type is None: # dashboard
|
|
1183
|
-
# Extract everything between the markers
|
|
1184
|
-
start_marker = "<!-- Regex Start -->"
|
|
1185
|
-
finish_marker = "<!-- Regex Finish -->"
|
|
1186
|
-
|
|
1187
|
-
start_pos = html.find(start_marker)
|
|
1188
|
-
finish_pos = html.find(finish_marker)
|
|
1189
|
-
|
|
1190
|
-
if start_pos == -1 or finish_pos == -1:
|
|
1191
|
-
raise ValueError("Could not find regex markers in dashboard HTML")
|
|
1192
|
-
|
|
1193
|
-
# Extract content between markers (including the markers themselves)
|
|
1194
|
-
content = html[start_pos:finish_pos + len(finish_marker)]
|
|
1195
|
-
|
|
1196
|
-
# Remove the markers
|
|
1197
|
-
content = content.replace(start_marker, "").replace(finish_marker, "")
|
|
1198
|
-
|
|
1199
|
-
return content.strip()
|
|
1200
|
-
else:
|
|
1201
|
-
# For event/solution: extract content between markers, remove header/nav/logo, add heading, wrap in <section>
|
|
1202
|
-
start_marker = "<!-- Regex Start -->"
|
|
1203
|
-
finish_marker = "<!-- Regex Finish -->"
|
|
1204
|
-
|
|
1205
|
-
start_pos = html.find(start_marker)
|
|
1206
|
-
finish_pos = html.find(finish_marker)
|
|
1207
|
-
|
|
1208
|
-
if start_pos == -1 or finish_pos == -1:
|
|
1209
|
-
raise ValueError("Could not find regex markers in HTML")
|
|
1210
|
-
|
|
1211
|
-
# Extract content between markers
|
|
1212
|
-
content = html[start_pos:finish_pos + len(finish_marker)]
|
|
1213
|
-
|
|
1214
|
-
# Remove the markers
|
|
1215
|
-
content = content.replace(start_marker, "").replace(finish_marker, "")
|
|
1216
|
-
|
|
1217
|
-
# Optionally add a heading
|
|
1218
|
-
heading = ''
|
|
1219
|
-
section_class = ''
|
|
1220
|
-
if section_type == 'event' and section_id:
|
|
1221
|
-
heading = f'<h2 class="text-3xl font-bold text-rtd-accent my-8">Event: {section_id}</h2>'
|
|
1222
|
-
section_class = 'dossier-event-section'
|
|
1223
|
-
elif section_type == 'solution' and section_id:
|
|
1224
|
-
heading = f'<h2 class="text-3xl font-bold text-rtd-accent my-6">Solution: {section_id}</h2>'
|
|
1225
|
-
section_class = 'dossier-solution-section'
|
|
1226
|
-
|
|
1227
|
-
# Wrap in a section for clarity
|
|
1228
|
-
return f'<section class="{section_class}">\n{heading}\n{content.strip()}\n</section>'
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
def _generate_full_dossier_report_html(submission, output_dir):
|
|
1232
|
-
"""Generate a comprehensive printable HTML dossier report.
|
|
1233
|
-
|
|
1234
|
-
Creates a single HTML file that concatenates all dossier sections (dashboard,
|
|
1235
|
-
events, and solutions) into one comprehensive, printable document. This is
|
|
1236
|
-
useful for creating a complete submission overview that can be printed or
|
|
1237
|
-
shared as a single file.
|
|
1238
|
-
|
|
1239
|
-
Args:
|
|
1240
|
-
submission: The submission object containing all events and solutions.
|
|
1241
|
-
output_dir: Directory where the full dossier report will be saved.
|
|
1242
|
-
The file will be named full_dossier_report.html.
|
|
1243
|
-
|
|
1244
|
-
Raises:
|
|
1245
|
-
OSError: If unable to write the HTML file.
|
|
1246
|
-
ValueError: If submission data is invalid or extraction fails.
|
|
1247
|
-
|
|
1248
|
-
Example:
|
|
1249
|
-
>>> from microlens_submit import load
|
|
1250
|
-
>>> from microlens_submit.dossier import _generate_full_dossier_report_html
|
|
1251
|
-
>>> from pathlib import Path
|
|
1252
|
-
>>>
|
|
1253
|
-
>>> submission = load("./my_project")
|
|
1254
|
-
>>>
|
|
1255
|
-
>>> # Generate comprehensive dossier
|
|
1256
|
-
>>> _generate_full_dossier_report_html(submission, Path("./dossier_output"))
|
|
1257
|
-
>>>
|
|
1258
|
-
>>> # Creates: ./dossier_output/full_dossier_report.html
|
|
1259
|
-
>>> # This file contains all dashboard, event, and solution content
|
|
1260
|
-
>>> # in a single, printable HTML document
|
|
1261
|
-
|
|
1262
|
-
Note:
|
|
1263
|
-
This function creates a comprehensive report by extracting content from
|
|
1264
|
-
individual pages and combining them with section dividers. The report
|
|
1265
|
-
includes all active solutions and maintains the same styling as
|
|
1266
|
-
individual pages. This is typically called automatically by
|
|
1267
|
-
generate_dashboard_html() when creating a full dossier.
|
|
1268
|
-
"""
|
|
1269
|
-
all_html_sections = []
|
|
1270
|
-
# Dashboard (extract only main content, skip header/logo)
|
|
1271
|
-
dash_html = _generate_dashboard_content(submission, full_dossier_exists=True)
|
|
1272
|
-
dash_body = _extract_main_content_body(dash_html)
|
|
1273
|
-
all_html_sections.append(dash_body)
|
|
1274
|
-
all_html_sections.append('<hr class="my-8 border-t-2 border-rtd-accent">') # Divider after dashboard
|
|
1275
|
-
|
|
1276
|
-
# Events and solutions
|
|
1277
|
-
for event in submission.events.values():
|
|
1278
|
-
event_html = _generate_event_page_content(event, submission)
|
|
1279
|
-
event_body = _extract_main_content_body(event_html, section_type='event', section_id=event.event_id)
|
|
1280
|
-
all_html_sections.append(event_body)
|
|
1281
|
-
all_html_sections.append('<hr class="my-8 border-t-2 border-rtd-accent">') # Divider after event
|
|
1282
|
-
|
|
1283
|
-
for sol in event.get_active_solutions():
|
|
1284
|
-
sol_html = _generate_solution_page_content(sol, event, submission)
|
|
1285
|
-
sol_body = _extract_main_content_body(sol_html, section_type='solution', section_id=sol.solution_id)
|
|
1286
|
-
all_html_sections.append(sol_body)
|
|
1287
|
-
all_html_sections.append('<hr class="my-8 border-t-2 border-rtd-accent">') # Divider after solution
|
|
1288
|
-
|
|
1289
|
-
# Compose the full HTML
|
|
1290
|
-
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')
|
|
1291
|
-
header = f'''
|
|
1292
|
-
<div class="text-center py-8 bg-rtd-primary text-rtd-secondary">
|
|
1293
|
-
<img src='assets/rges-pit_logo.png' alt='RGES-PIT Logo' class='w-48 mx-auto mb-6'>
|
|
1294
|
-
<h1 class="text-3xl font-bold mb-2">Comprehensive Submission Dossier</h1>
|
|
1295
|
-
<p class="text-lg">Generated on: {now}</p>
|
|
1296
|
-
<p class="text-md">Team: {submission.team_name} | Tier: {submission.tier}</p>
|
|
1297
|
-
</div>
|
|
1298
|
-
<hr class="border-t-4 border-rtd-accent my-8">
|
|
1299
|
-
'''
|
|
1300
|
-
html = f'''<!DOCTYPE html>
|
|
1301
|
-
<html lang="en">
|
|
1302
|
-
<head>
|
|
1303
|
-
<meta charset="UTF-8">
|
|
1304
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1305
|
-
<title>Full Dossier Report - {submission.team_name}</title>
|
|
1306
|
-
<script src="https://cdn.tailwindcss.com"></script>
|
|
1307
|
-
<script>
|
|
1308
|
-
tailwind.config = {{
|
|
1309
|
-
theme: {{
|
|
1310
|
-
extend: {{
|
|
1311
|
-
colors: {{
|
|
1312
|
-
'rtd-primary': '#dfc5fa',
|
|
1313
|
-
'rtd-secondary': '#361d49',
|
|
1314
|
-
'rtd-accent': '#a859e4',
|
|
1315
|
-
'rtd-background': '#faf7fd',
|
|
1316
|
-
'rtd-text': '#000',
|
|
1317
|
-
}},
|
|
1318
|
-
fontFamily: {{
|
|
1319
|
-
inter: ['Inter', 'sans-serif'],
|
|
1320
|
-
}},
|
|
1321
|
-
}},
|
|
1322
|
-
}},
|
|
1323
|
-
}};
|
|
1324
|
-
</script>
|
|
1325
|
-
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
|
1326
|
-
<!-- Highlight.js for code syntax highlighting -->
|
|
1327
|
-
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
|
|
1328
|
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
1329
|
-
<script>hljs.highlightAll();</script>
|
|
1330
|
-
<style>
|
|
1331
|
-
.prose {{
|
|
1332
|
-
color: #000;
|
|
1333
|
-
line-height: 1.6;
|
|
1334
|
-
}}
|
|
1335
|
-
.prose h1 {{
|
|
1336
|
-
font-size: 1.5rem;
|
|
1337
|
-
font-weight: 700;
|
|
1338
|
-
color: #361d49;
|
|
1339
|
-
margin-top: 1.5rem;
|
|
1340
|
-
margin-bottom: 0.75rem;
|
|
1341
|
-
}}
|
|
1342
|
-
.prose h2 {{
|
|
1343
|
-
font-size: 1.25rem;
|
|
1344
|
-
font-weight: 600;
|
|
1345
|
-
color: #361d49;
|
|
1346
|
-
margin-top: 1.25rem;
|
|
1347
|
-
margin-bottom: 0.5rem;
|
|
1348
|
-
}}
|
|
1349
|
-
.prose h3 {{
|
|
1350
|
-
font-size: 1.125rem;
|
|
1351
|
-
font-weight: 600;
|
|
1352
|
-
color: #a859e4;
|
|
1353
|
-
margin-top: 1rem;
|
|
1354
|
-
margin-bottom: 0.5rem;
|
|
1355
|
-
}}
|
|
1356
|
-
.prose p {{
|
|
1357
|
-
margin-bottom: 0.75rem;
|
|
1358
|
-
}}
|
|
1359
|
-
.prose ul, .prose ol {{
|
|
1360
|
-
margin-left: 1.5rem;
|
|
1361
|
-
margin-bottom: 0.75rem;
|
|
1362
|
-
}}
|
|
1363
|
-
.prose ul {{ list-style-type: disc; }}
|
|
1364
|
-
.prose ol {{ list-style-type: decimal; }}
|
|
1365
|
-
.prose li {{
|
|
1366
|
-
margin-bottom: 0.25rem;
|
|
1367
|
-
}}
|
|
1368
|
-
.prose code {{
|
|
1369
|
-
background: #f3f3f3;
|
|
1370
|
-
padding: 2px 4px;
|
|
1371
|
-
border-radius: 4px;
|
|
1372
|
-
font-family: 'Courier New', monospace;
|
|
1373
|
-
font-size: 0.875rem;
|
|
1374
|
-
}}
|
|
1375
|
-
.prose pre {{
|
|
1376
|
-
background: #f8f8f8;
|
|
1377
|
-
padding: 1rem;
|
|
1378
|
-
border-radius: 8px;
|
|
1379
|
-
overflow-x: auto;
|
|
1380
|
-
margin: 1rem 0;
|
|
1381
|
-
border: 1px solid #e5e5e5;
|
|
1382
|
-
}}
|
|
1383
|
-
.prose pre code {{
|
|
1384
|
-
background: none;
|
|
1385
|
-
padding: 0;
|
|
1386
|
-
}}
|
|
1387
|
-
.prose blockquote {{
|
|
1388
|
-
border-left: 4px solid #a859e4;
|
|
1389
|
-
padding-left: 1rem;
|
|
1390
|
-
margin: 1rem 0;
|
|
1391
|
-
font-style: italic;
|
|
1392
|
-
color: #666;
|
|
1393
|
-
}}
|
|
1394
|
-
</style>
|
|
1395
|
-
</head>
|
|
1396
|
-
<body class="font-inter bg-rtd-background">
|
|
1397
|
-
<div class="max-w-7xl mx-auto p-6 lg:p-8">
|
|
1398
|
-
{header}
|
|
1399
|
-
{''.join(all_html_sections)}
|
|
1400
|
-
</div>
|
|
1401
|
-
</body>
|
|
1402
|
-
</html>'''
|
|
1403
|
-
with (output_dir / "full_dossier_report.html").open("w", encoding="utf-8") as f:
|
|
1404
|
-
f.write(html)
|
|
1405
|
-
|
|
1406
|
-
def _extract_github_repo_name(repo_url: str) -> str:
|
|
1407
|
-
"""Extract owner/repo name from a GitHub URL.
|
|
1408
|
-
|
|
1409
|
-
Parses GitHub repository URLs to extract the owner and repository name
|
|
1410
|
-
in a display-friendly format. Handles various GitHub URL formats including
|
|
1411
|
-
HTTPS, SSH, and URLs with .git extension.
|
|
1412
|
-
|
|
1413
|
-
Args:
|
|
1414
|
-
repo_url: GitHub repository URL (e.g., https://github.com/owner/repo,
|
|
1415
|
-
git@github.com:owner/repo.git, etc.)
|
|
1416
|
-
|
|
1417
|
-
Returns:
|
|
1418
|
-
str: Repository name in "owner/repo" format, or the original URL
|
|
1419
|
-
if parsing fails.
|
|
1420
|
-
|
|
1421
|
-
Example:
|
|
1422
|
-
>>> _extract_github_repo_name("https://github.com/username/microlens-submit")
|
|
1423
|
-
'username/microlens-submit'
|
|
1424
|
-
|
|
1425
|
-
>>> _extract_github_repo_name("git@github.com:username/microlens-submit.git")
|
|
1426
|
-
'username/microlens-submit'
|
|
1427
|
-
|
|
1428
|
-
>>> _extract_github_repo_name("https://github.com/org/repo-name")
|
|
1429
|
-
'org/repo-name'
|
|
1430
|
-
|
|
1431
|
-
>>> _extract_github_repo_name("invalid-url")
|
|
1432
|
-
'invalid-url'
|
|
1433
|
-
|
|
1434
|
-
Note:
|
|
1435
|
-
This function uses regex to parse GitHub URLs and handles common
|
|
1436
|
-
variations. If the URL doesn't match expected patterns, it returns
|
|
1437
|
-
the original URL unchanged.
|
|
1438
|
-
"""
|
|
1439
|
-
import re
|
|
1440
|
-
match = re.search(r'github\.com[:/]+([\w.-]+)/([\w.-]+)', repo_url)
|
|
1441
|
-
if match:
|
|
1442
|
-
return f"{match.group(1)}/{match.group(2).replace('.git','')}"
|
|
1443
|
-
return repo_url
|