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.
Files changed (34) hide show
  1. microlens_submit/__init__.py +7 -157
  2. microlens_submit/cli/__init__.py +5 -0
  3. microlens_submit/cli/__main__.py +6 -0
  4. microlens_submit/cli/commands/__init__.py +1 -0
  5. microlens_submit/cli/commands/dossier.py +139 -0
  6. microlens_submit/cli/commands/export.py +177 -0
  7. microlens_submit/cli/commands/init.py +172 -0
  8. microlens_submit/cli/commands/solutions.py +722 -0
  9. microlens_submit/cli/commands/validation.py +241 -0
  10. microlens_submit/cli/main.py +120 -0
  11. microlens_submit/dossier/__init__.py +51 -0
  12. microlens_submit/dossier/dashboard.py +503 -0
  13. microlens_submit/dossier/event_page.py +370 -0
  14. microlens_submit/dossier/full_report.py +330 -0
  15. microlens_submit/dossier/solution_page.py +534 -0
  16. microlens_submit/dossier/utils.py +111 -0
  17. microlens_submit/error_messages.py +283 -0
  18. microlens_submit/models/__init__.py +28 -0
  19. microlens_submit/models/event.py +406 -0
  20. microlens_submit/models/solution.py +569 -0
  21. microlens_submit/models/submission.py +569 -0
  22. microlens_submit/tier_validation.py +208 -0
  23. microlens_submit/utils.py +373 -0
  24. microlens_submit/validate_parameters.py +478 -180
  25. {microlens_submit-0.12.2.dist-info → microlens_submit-0.16.1.dist-info}/METADATA +52 -14
  26. microlens_submit-0.16.1.dist-info/RECORD +32 -0
  27. microlens_submit/api.py +0 -1257
  28. microlens_submit/cli.py +0 -1803
  29. microlens_submit/dossier.py +0 -1443
  30. microlens_submit-0.12.2.dist-info/RECORD +0 -13
  31. {microlens_submit-0.12.2.dist-info → microlens_submit-0.16.1.dist-info}/WHEEL +0 -0
  32. {microlens_submit-0.12.2.dist-info → microlens_submit-0.16.1.dist-info}/entry_points.txt +0 -0
  33. {microlens_submit-0.12.2.dist-info → microlens_submit-0.16.1.dist-info}/licenses/LICENSE +0 -0
  34. {microlens_submit-0.12.2.dist-info → microlens_submit-0.16.1.dist-info}/top_level.txt +0 -0
@@ -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'>&larr; 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'>&larr; Back to Event {event.event_id}</a>
1016
- <a href='index.html' class='text-rtd-accent hover:underline'>&larr; 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