mirror of
https://github.com/ruvnet/RuVector.git
synced 2026-08-31 02:05:14 +00:00
refactor: trim ADR-040 to 493 lines, enhance real_microlensing adapter
ADR-040: Replace extracted dashboard and microlensing sections with cross-references to ADR-040a and ADR-040b. Condense data model, adapters, and constructs. Core pipeline content preserved. real_microlensing: Add download manifest with 12 real OGLE/MOA events (8 confirmed planets), cross-survey normalization, enhanced MOA parser, simulated download from published parameters. https://claude.ai/code/session_01UWE22wnsZRSHKhT4h4Axby
This commit is contained in:
parent
847ca8fdc9
commit
6295b3fa20
2 changed files with 293 additions and 928 deletions
|
|
@ -25,19 +25,10 @@ retained archetypes.
|
|||
|
||||
### Existing Capabilities (ADR-003 through ADR-008)
|
||||
|
||||
| Component | Package | Relevant APIs |
|
||||
|-----------|---------|---------------|
|
||||
| **RVF segments** | `@ruvector/rvf`, `@ruvector/rvf-node` | `embedKernel`, `extractKernel`, `embedEbpf`, `segments`, `derive` |
|
||||
| **HNSW indexing** | `@ruvector/rvf-node` | `ingestBatch`, `query`, `compact`, HNSW with metadata filters |
|
||||
| **Witness chains** | `@ruvector/rvf-node`, `RvfSolver` | `verifyWitness`, SHAKE-256 witness chains, signed root hash |
|
||||
| **Graph transactions** | `NativeAccelerator` | `graphTransaction`, `graphBatchInsert`, Cypher queries |
|
||||
| **SIMD embeddings** | `@ruvector/ruvllm` | 768-dim SIMD embed, cosine/dot/L2, HNSW memory search |
|
||||
| **SONA learning** | `SonaLearningBackend` | Micro-LoRA, trajectory recording, EWC++ |
|
||||
| **Federated coordination** | `FederatedSessionManager` | Cross-agent trajectories, warm-start patterns |
|
||||
| **Contrastive training** | `ContrastiveTrainer` | InfoNCE, hard negative mining, 3-stage curriculum |
|
||||
| **Adaptive index** | `AdaptiveIndexTuner` | 5-tier compression, Matryoshka truncation, health monitoring |
|
||||
| **Kernel embedding** | `KernelBuilder` (ADR-008) | Minimal Linux boot from KERNEL_SEG + INITRD_SEG |
|
||||
| **Lazy model download** | `ChatInference` (ADR-008) | Deferred GGUF load on first inference call |
|
||||
RVF segments, HNSW indexing, SHAKE-256 witness chains, graph transactions,
|
||||
768-dim SIMD embeddings, SONA learning, federated coordination, contrastive
|
||||
training, adaptive index tuning, kernel embedding (ADR-008), and lazy model
|
||||
download (ADR-008). See ADR-003 through ADR-008 for full API details.
|
||||
|
||||
### What This ADR Adds
|
||||
|
||||
|
|
@ -119,26 +110,7 @@ Following the lazy-download pattern established in ADR-008 for GGUF models:
|
|||
INDEX_SEG, a new witness root is committed, and the RVF is sealed into
|
||||
a reproducible snapshot
|
||||
|
||||
```
|
||||
Download state machine:
|
||||
|
||||
[boot] ──first-inference──> [downloading-tier-1]
|
||||
│ │
|
||||
│ (offline demo works) │ (progress: 0-100%)
|
||||
│ │
|
||||
▼ ▼
|
||||
[tier-0-only] [tier-1-ready]
|
||||
│
|
||||
rvf ingest --expand
|
||||
│
|
||||
▼
|
||||
[tier-2-ready]
|
||||
│
|
||||
life pipeline activated
|
||||
│
|
||||
▼
|
||||
[tier-3-ready] ──seal──> [sealed-snapshot]
|
||||
```
|
||||
**State machine**: `[boot]` -> `[tier-0-only]` (offline demo) -> `[tier-1-ready]` (first inference) -> `[tier-2-ready]` (`rvf ingest --expand`) -> `[tier-3-ready]` (life pipeline) -> `[sealed-snapshot]`.
|
||||
|
||||
Each tier download:
|
||||
- Resumes from last byte on interruption (HTTP Range headers)
|
||||
|
|
@ -168,197 +140,50 @@ Extends the ADR-003 segment model with domain-specific segments.
|
|||
|
||||
### Core Entities
|
||||
|
||||
```typescript
|
||||
interface Event {
|
||||
id: string;
|
||||
t_start: number; // epoch seconds
|
||||
t_end: number;
|
||||
domain: 'kepler' | 'tess' | 'jwst' | 'sdss' | 'derived';
|
||||
payload_hash: string; // SHA-256 of raw data window
|
||||
provenance: Provenance;
|
||||
}
|
||||
|
||||
interface Observation {
|
||||
id: string;
|
||||
instrument: string; // 'kepler-lc' | 'tess-ffi' | 'jwst-nirspec' | ...
|
||||
target_id: string; // e.g., KIC or TIC identifier
|
||||
data_pointer: string; // segment offset into VEC_SEG
|
||||
calibration_version: string;
|
||||
provenance: Provenance;
|
||||
}
|
||||
|
||||
interface InteractionEdge {
|
||||
src_event_id: string;
|
||||
dst_event_id: string;
|
||||
type: 'causal' | 'periodicity' | 'shape_similarity' | 'co_occurrence' | 'spatial';
|
||||
weight: number;
|
||||
lag: number; // temporal lag in seconds
|
||||
confidence: number;
|
||||
provenance: Provenance;
|
||||
}
|
||||
|
||||
interface Boundary {
|
||||
boundary_id: string;
|
||||
partition_left_set_hash: string;
|
||||
partition_right_set_hash: string;
|
||||
cut_weight: number;
|
||||
cut_witness: string; // witness chain reference
|
||||
stability_score: number;
|
||||
}
|
||||
|
||||
interface Candidate {
|
||||
candidate_id: string;
|
||||
category: 'planet' | 'life';
|
||||
evidence_pointers: string[]; // event and edge IDs
|
||||
score: number;
|
||||
uncertainty: number;
|
||||
publishable: boolean; // based on POLICY_SEG rules
|
||||
witness_trace: string; // WITNESS_SEG reference for replay
|
||||
}
|
||||
|
||||
interface Provenance {
|
||||
source: string; // 'mast-kepler' | 'mast-tess' | 'mast-jwst' | ...
|
||||
download_witness: string; // witness chain entry for the download
|
||||
transform_chain: string[]; // ordered list of transform IDs applied
|
||||
timestamp: string; // ISO-8601
|
||||
}
|
||||
```
|
||||
| Entity | Key Fields | Description |
|
||||
|--------|-----------|-------------|
|
||||
| **Event** | `id`, `t_start`, `t_end`, `domain`, `payload_hash`, `provenance` | Time-windowed observation or derived result. Domain: kepler, tess, jwst, sdss, derived |
|
||||
| **Observation** | `id`, `instrument`, `target_id`, `data_pointer`, `calibration_version` | Raw instrument measurement with VEC_SEG offset |
|
||||
| **InteractionEdge** | `src_event_id`, `dst_event_id`, `type`, `weight`, `lag`, `confidence` | Typed relationship: causal, periodicity, shape_similarity, co_occurrence, spatial |
|
||||
| **Boundary** | `boundary_id`, `partition_*_hash`, `cut_weight`, `cut_witness`, `stability_score` | Graph partition boundary with witness chain reference |
|
||||
| **Candidate** | `candidate_id`, `category`, `evidence_pointers[]`, `score`, `uncertainty`, `publishable`, `witness_trace` | Planet or life candidate with POLICY_SEG-gated publishability |
|
||||
| **Provenance** | `source`, `download_witness`, `transform_chain[]`, `timestamp` | Full lineage from download through every transform |
|
||||
|
||||
### Domain Adapters
|
||||
|
||||
#### Planet Transit Adapter
|
||||
|
||||
```
|
||||
Input: flux time series + cadence metadata (Kepler/TESS FITS)
|
||||
Output: Event nodes for windows
|
||||
InteractionEdges for periodicity hints and shape similarity
|
||||
Candidate nodes for dip detections
|
||||
```
|
||||
|
||||
#### Spectrum Adapter
|
||||
|
||||
```
|
||||
Input: wavelength, flux, error arrays (JWST NIRSpec, etc.)
|
||||
Output: Event nodes for band windows
|
||||
InteractionEdges for molecule feature co-occurrence
|
||||
Disequilibrium score components
|
||||
```
|
||||
|
||||
#### Cosmic Web Adapter (optional, Phase 2+)
|
||||
|
||||
```
|
||||
Input: galaxy positions and redshifts (SDSS)
|
||||
Output: Graph of spatial adjacency and filament membership
|
||||
```
|
||||
| Adapter | Input | Output |
|
||||
|---------|-------|--------|
|
||||
| **Planet Transit** | Flux time series + cadence metadata (Kepler/TESS FITS) | Event nodes, periodicity/shape InteractionEdges, dip Candidates |
|
||||
| **Spectrum** | Wavelength, flux, error arrays (JWST NIRSpec, etc.) | Band Event nodes, molecule co-occurrence edges, disequilibrium scores |
|
||||
| **Cosmic Web** (Phase 2+) | Galaxy positions and redshifts (SDSS) | Spatial adjacency graph with filament membership |
|
||||
|
||||
## The Four System Constructs
|
||||
|
||||
### 1. Compressed Causal Atlas
|
||||
|
||||
**Definition**: A partial order of events plus minimal sufficient descriptors
|
||||
to reproduce derived edges.
|
||||
Partial order of events plus minimal sufficient descriptors to reproduce derived edges.
|
||||
|
||||
**Construction**:
|
||||
**Construction**: (1) Window light curves at scales 2h/12h/3d/27d. (2) Extract features: flux derivatives, autocorrelation peaks, wavelet energy, matched filter response. (3) Embed via RuVector SIMD into VEC_SEG. (4) Add causal edges where window A precedes B and improves predictability (prediction gain, POLICY_SEG constrained). (5) Compress: top-k parents per node, retain boundary witnesses, delta-encode into DELTA_SEG.
|
||||
|
||||
1. **Windowing** — Light curves into overlapping windows at multiple scales
|
||||
- Scales: 2 hours, 12 hours, 3 days, 27 days
|
||||
|
||||
2. **Feature extraction** — Robust features per window
|
||||
- Flux derivative statistics
|
||||
- Autocorrelation peaks
|
||||
- Wavelet energy bands
|
||||
- Transit-shaped matched filter response
|
||||
|
||||
3. **Embedding** — RuVector SIMD embed per window, stored in VEC_SEG
|
||||
|
||||
4. **Causal edges** — Add edge when window A precedes window B and improves
|
||||
predictability of B (conditional mutual information proxy or prediction gain,
|
||||
subject to POLICY_SEG constraints)
|
||||
- Edge weight: prediction gain magnitude
|
||||
- Provenance: exact windows, transform IDs, threshold used
|
||||
|
||||
5. **Atlas compression**
|
||||
- Keep only top-k causal parents per node
|
||||
- Retain stable boundary witnesses
|
||||
- Delta-encode updates into DELTA_SEG
|
||||
|
||||
**Output API**:
|
||||
|
||||
| Endpoint | Returns |
|
||||
|----------|---------|
|
||||
| `atlas.query(event_id)` | Parents, children, plus provenance |
|
||||
| `atlas.trace(candidate_id)` | Minimal causal chain for a candidate |
|
||||
**API**: `atlas.query(event_id)` returns parents/children with provenance. `atlas.trace(candidate_id)` returns minimal causal chain.
|
||||
|
||||
### 2. Coherence Field Index
|
||||
|
||||
**Definition**: A field over the atlas graph that assigns coherence pressure
|
||||
and cut stability over time.
|
||||
Field over the atlas graph assigning coherence pressure and cut stability over time. Signals: cut pressure (min-cut values), partition entropy (cluster size distribution), disagreement (cross-detector rate), drift (embedding distribution shift).
|
||||
|
||||
**Signals**:
|
||||
**Algorithm**: Maintain partition tree with dynamic min-cut on incremental changes. Each epoch: compute cut witnesses for top boundaries, emit to GRAPH_SEG, append to WITNESS_SEG. Index boundaries by descriptor: cut value, partition sizes, curvature proxy, churn.
|
||||
|
||||
| Signal | Description |
|
||||
|--------|-------------|
|
||||
| Cut pressure | Minimum cut values over selected subgraphs |
|
||||
| Partition entropy | Distribution of cluster sizes and churn rate |
|
||||
| Disagreement | Cross-detector disagreement rate |
|
||||
| Drift | Embedding distribution shift in sliding window |
|
||||
|
||||
**Algorithm**:
|
||||
|
||||
1. Maintain a partition tree. Update with dynamic min-cut on incremental
|
||||
graph changes
|
||||
2. For each update epoch:
|
||||
- Compute cut witnesses for top boundaries
|
||||
- Emit boundary events into GRAPH_SEG
|
||||
- Append witness record into WITNESS_SEG
|
||||
3. Index boundaries via descriptor vector:
|
||||
- Cut value, partition sizes, local graph curvature proxy, recent churn
|
||||
|
||||
**Query API**:
|
||||
|
||||
| Endpoint | Returns |
|
||||
|----------|---------|
|
||||
| `coherence.get(target_id, epoch)` | Field values for target at epoch |
|
||||
| `boundary.nearest(descriptor)` | Similar historical boundary states via INDEX_SEG |
|
||||
**API**: `coherence.get(target_id, epoch)` returns field values. `boundary.nearest(descriptor)` returns similar historical states via INDEX_SEG.
|
||||
|
||||
### 3. Multi-Scale Interaction Memory
|
||||
|
||||
**Definition**: A memory that retains interactions at multiple time resolutions
|
||||
with strict budget control.
|
||||
|
||||
**Three tiers**:
|
||||
|
||||
| Tier | Resolution | Content |
|
||||
|------|-----------|---------|
|
||||
| **S** | Seconds to minutes | High-fidelity deltas |
|
||||
| **M** | Hours to days | Aggregated deltas |
|
||||
| **L** | Weeks to months | Boundary summaries and archetypes |
|
||||
|
||||
**Retention rules**:
|
||||
1. Preserve events that are boundary-critical
|
||||
2. Preserve events that are candidate evidence
|
||||
3. Compress everything else via archetype clustering in INDEX_SEG
|
||||
|
||||
**Mechanism**:
|
||||
- DELTA_SEG is append-only
|
||||
- Periodic compaction produces a new RVF root with a witness proof of
|
||||
preservation rules applied
|
||||
Tiered retention with strict budget control: **S** (seconds-minutes, high-fidelity deltas), **M** (hours-days, aggregated), **L** (weeks-months, boundary summaries and archetypes). Retention preserves boundary-critical events and candidate evidence; compresses the rest via archetype clustering. DELTA_SEG is append-only; periodic compaction produces a new RVF root with witness proof.
|
||||
|
||||
### 4. Boundary Evolution Tracker
|
||||
|
||||
**Definition**: A tracker that treats boundaries as primary objects that evolve
|
||||
over time.
|
||||
Treats boundaries as primary objects evolving over time -- the holographic design principle. Boundaries are stored and indexed as first-class objects; interior state is reconstructable from boundary witnesses and retained archetypes.
|
||||
|
||||
**This is where the holographic flavor is implemented.** You primarily store
|
||||
and index boundaries, and treat interior state as reconstructable from boundary
|
||||
witnesses and retained archetypes.
|
||||
|
||||
**Output API**:
|
||||
|
||||
| Endpoint | Returns |
|
||||
|----------|---------|
|
||||
| `boundary.timeline(target_id)` | Boundary evolution over time |
|
||||
| `boundary.alerts` | Alerts when: cut pressure spikes, boundary identity flips, disagreement exceeds threshold, drift persists beyond policy |
|
||||
**API**: `boundary.timeline(target_id)` returns boundary evolution. `boundary.alerts` fires on cut pressure spikes, boundary identity flips, disagreement threshold breaches, or persistent drift.
|
||||
|
||||
## Planet Detection Pipeline
|
||||
|
||||
|
|
@ -443,139 +268,9 @@ thermodynamic relaxation.
|
|||
**Output**: Life candidate list with explicit uncertainty + required follow-up
|
||||
observations list generated by POLICY_SEG rules.
|
||||
|
||||
## Microlensing Detection Pipeline (M0-M3)
|
||||
### Microlensing & Cross-Domain Extensions
|
||||
|
||||
Extends the transit pipeline to gravitational microlensing events for rogue
|
||||
planet and exomoon candidate detection.
|
||||
|
||||
### Additional Data Sources
|
||||
|
||||
| Source | Access | Reference |
|
||||
|--------|--------|-----------|
|
||||
| OGLE-IV microlensing events | Public FTP | [ogle.astrouw.edu.pl](https://ogle.astrouw.edu.pl/ogle4/ews/ews.html) |
|
||||
| MOA-II microlensing alerts | Public archive | [www.massey.ac.nz/~iabond/moa](https://www.massey.ac.nz/~iabond/moa/) |
|
||||
| Gaia astrometric catalog | ESA public | [gea.esac.esa.int](https://gea.esac.esa.int/archive/) |
|
||||
| Roman Space Telescope | Upcoming | High-cadence microlensing survey |
|
||||
|
||||
### Stage M0: Ingest
|
||||
|
||||
Microlensing light curve ingestion from OGLE/MOA archives.
|
||||
- Normalize photometry across surveys (MOA-II: ~15 min cadence, 0.008σ floor; OGLE-IV: ~40 min cadence, 0.005σ floor)
|
||||
- Segment into event windows around magnification peaks
|
||||
- Store as RVF Event nodes with survey-specific metadata
|
||||
|
||||
### Stage M1: Single-Lens Detection
|
||||
|
||||
Standard Paczynski curve fitting for point-source point-lens (PSPL) events.
|
||||
- Two-phase fit: coarse grid search + fine refinement
|
||||
- Linear regression for source flux (F_s) and blending flux (F_b)
|
||||
- Parameter estimation: Einstein crossing time (t_E), impact parameter (u_0), peak time (t_0)
|
||||
- Create Candidate nodes for events exceeding SNR threshold
|
||||
|
||||
### Stage M2: Anomaly Detection via MRF/Mincut
|
||||
|
||||
Residual analysis after best-fit PSPL subtraction using Markov Random Field
|
||||
optimization with graph cut inference.
|
||||
|
||||
**Three-statistic lambda computation:**
|
||||
1. Excess chi2: window fit quality relative to global reduced chi2
|
||||
2. Runs test: temporal coherence of residual sign changes
|
||||
3. Gaussian bump fit: localized perturbation chi2 improvement
|
||||
|
||||
**Differential normalization:** Compare each window's signal to tau-space
|
||||
neighbors, producing z-scores that are ~0 for uniform fit quality and positive
|
||||
only for localized anomalies.
|
||||
|
||||
**Graph construction:**
|
||||
- Temporal chain: consecutive time windows (alpha weight)
|
||||
- RuVector kNN edges: embedding similarity from residual features (beta weight)
|
||||
- Edmonds-Karp BFS max-flow solver for s-t mincut
|
||||
|
||||
### Stage M3: Coherence Gating
|
||||
|
||||
Dynamic mincut separates competing explanations (noise vs planet vs moon vs
|
||||
binary lens) using support region analysis.
|
||||
|
||||
| Component | Role |
|
||||
|-----------|------|
|
||||
| RuVector embeddings | Cross-survey memory; retrieve similar anomalies |
|
||||
| HNSW index | Fast similarity search across microlensing events |
|
||||
| Dynamic mincut | Separate competing explanations |
|
||||
| Witness chain | Full provenance from raw photometry to candidate |
|
||||
|
||||
**Important constraint:** Dynamic mincut provides coherent support extraction
|
||||
but cannot replace lens modeling. The physics solver (PSPL/binary) provides
|
||||
local evidence; mincut provides spatial coherence.
|
||||
|
||||
## Cross-Domain Graph-Cut Applications
|
||||
|
||||
The MRF/mincut + RuVector architecture generalizes beyond astronomy to any
|
||||
domain requiring coherent anomaly detection in structured data.
|
||||
|
||||
### Implemented Verticals
|
||||
|
||||
| Vertical | Example | Graph Topology | Anomaly Types |
|
||||
|----------|---------|----------------|---------------|
|
||||
| **Medical Imaging** | `medical_graphcut.rs` | 4-connected spatial grid, gradient-weighted edges | Lesion segmentation (T1-MRI, T2-MRI, CT) |
|
||||
| **Genomics** | `genomic_graphcut.rs` | Linear chain + kNN similarity | CNV gains/losses, LOH, cancer drivers (TP53, BRCA1, EGFR, MYC) |
|
||||
| **Financial Fraud** | `financial_fraud_graphcut.rs` | Temporal chain + merchant edges + kNN | Card-not-present, account takeover, card clone, synthetic, refund |
|
||||
| **Supply Chain** | `supply_chain_graphcut.rs` | Tier chain + geographic + kNN | Quality defect, shortage, price anomaly, delay, counterfeit, demand shock |
|
||||
| **Cybersecurity** | `cyber_threat_graphcut.rs` | Source/destination chain + kNN | Port scan, brute force, exfiltration, C2 beacon, DDoS, lateral movement |
|
||||
| **Climate** | `climate_graphcut.rs` | Spatial adjacency + gradient + kNN | Heat wave, pollution spike, drought, ocean warming, cold snap, sensor fault |
|
||||
|
||||
### Common Architecture
|
||||
|
||||
All verticals share:
|
||||
1. **Domain-specific data generator** with realistic parameters from published datasets
|
||||
2. **Feature extraction** producing 32-dim embeddings for RuVector storage
|
||||
3. **Graph construction** combining domain topology (spatial/temporal/chain) with kNN similarity edges
|
||||
4. **Edmonds-Karp BFS** s-t mincut solver (identical implementation)
|
||||
5. **RVF integration**: witness chains, filtered metadata queries, lineage derivation
|
||||
6. **Evaluation**: comparison against threshold baseline showing graph-cut improvement
|
||||
|
||||
### Additional Real Data Sources
|
||||
|
||||
| Domain | Source | Access |
|
||||
|--------|--------|--------|
|
||||
| Genomics | TCGA (The Cancer Genome Atlas) | [portal.gdc.cancer.gov](https://portal.gdc.cancer.gov/) |
|
||||
| Genomics | ClinVar variants | [ncbi.nlm.nih.gov/clinvar](https://www.ncbi.nlm.nih.gov/clinvar/) |
|
||||
| Genomics | COSMIC (somatic mutations) | [cancer.sanger.ac.uk/cosmic](https://cancer.sanger.ac.uk/cosmic) |
|
||||
| Medical | LIDC-IDRI (lung CT) | [cancerimagingarchive.net](https://www.cancerimagingarchive.net/) |
|
||||
| Medical | BraTS (brain tumors) | [synapse.org](https://www.synapse.org/) |
|
||||
| Cybersecurity | CICIDS2017 | [unb.ca/cic](https://www.unb.ca/cic/datasets/ids-2017.html) |
|
||||
| Climate | NOAA GHCN | [ncei.noaa.gov](https://www.ncei.noaa.gov/) |
|
||||
| Climate | EPA AQI | [epa.gov/aqs](https://www.epa.gov/aqs) |
|
||||
|
||||
## Measured Results
|
||||
|
||||
Results from running examples with `cargo run --example <name> --release`.
|
||||
|
||||
### Astronomy
|
||||
|
||||
| Pipeline | Metric | Value | Notes |
|
||||
|----------|--------|-------|-------|
|
||||
| `planet_detection` | Recall@10 | 8/10 confirmed | Synthetic Kepler-like targets |
|
||||
| `exomoon_graphcut` | Precision | 0.25 | Chang-Refsdal perturbative limit |
|
||||
| `exomoon_graphcut` | Recall | 0.25 | Limited by ~2-3σ per-window SNR |
|
||||
| `exomoon_graphcut` | F1 | 0.25 | 30 events, 12 with moons |
|
||||
| `real_microlensing` | Planets rediscovered | 2/4 | OGLE-2005-BLG-390, OGLE-2016-BLG-1195 |
|
||||
| `microlensing_detection` | Events processed | 20 | Synthetic PSPL + anomalies |
|
||||
|
||||
### Medical Imaging
|
||||
|
||||
| Modality | Graph Cut Dice | Threshold Dice | Improvement |
|
||||
|----------|---------------|----------------|-------------|
|
||||
| T1-MRI | 0.44-0.59 | 0.32-0.46 | +27-39% |
|
||||
| T2-MRI | 0.50-0.62 | 0.38-0.48 | +29-32% |
|
||||
| CT | 0.48-0.58 | 0.35-0.44 | +32-37% |
|
||||
|
||||
### Genomics
|
||||
|
||||
| Platform | Sensitivity | Specificity | Drivers Found |
|
||||
|----------|-------------|-------------|---------------|
|
||||
| WGS (30x) | 0.91-0.95 | 0.66-0.97 | 4/4 (TP53, BRCA1, EGFR, MYC) |
|
||||
| WES (100x) | 0.88-0.93 | 0.70-0.95 | 4/4 |
|
||||
| Panel (500x) | 0.93-0.97 | 0.72-0.98 | 4/4 |
|
||||
> See [ADR-040b: Microlensing & Graph-Cut Extensions](ADR-040b-microlensing-graphcut-extensions.md) for M0-M3 pipeline, cross-domain applications, measured results, and Rust crate structure.
|
||||
|
||||
## Runtime and Portability
|
||||
|
||||
|
|
@ -630,167 +325,9 @@ WS /ws/live # real-time boundary alerts, pipeline pr
|
|||
3. No remote writes without explicit policy toggle in POLICY_SEG
|
||||
4. Downloaded data verified against MANIFEST_SEG hashes before ingestion
|
||||
|
||||
## Three.js Visualization Dashboard
|
||||
### Dashboard Architecture
|
||||
|
||||
The RVF embeds a Vite-bundled Three.js dashboard in `DASHBOARD_SEG`. The
|
||||
runtime HTTP server serves it at `/` (root). All visualizations are driven
|
||||
by the same API endpoints the CLI uses, so every rendered frame corresponds
|
||||
to queryable, witness-backed data.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
DASHBOARD_SEG (inside RVF)
|
||||
dist/
|
||||
index.html # Vite SPA entry
|
||||
assets/
|
||||
main.[hash].js # Three.js + D3 + app logic (tree-shaken)
|
||||
main.[hash].css # Tailwind/minimal styles
|
||||
worker.js # Web Worker for graph layout
|
||||
|
||||
Runtime serves:
|
||||
GET / -> DASHBOARD_SEG/dist/index.html
|
||||
GET /assets/* -> DASHBOARD_SEG/dist/assets/*
|
||||
GET /api/* -> JSON API (atlas, coherence, candidates, etc.)
|
||||
WS /ws/live -> Live streaming of boundary alerts and pipeline progress
|
||||
```
|
||||
|
||||
**Build pipeline**: Vite builds the dashboard at package time into a single
|
||||
tree-shaken bundle. The bundle is embedded into `DASHBOARD_SEG` during RVF
|
||||
assembly. No Node.js required at runtime — the dashboard is pure static
|
||||
assets served by the existing HTTP server.
|
||||
|
||||
### Dashboard Views
|
||||
|
||||
#### V1: Causal Atlas Explorer (Three.js 3D)
|
||||
|
||||
Interactive 3D force-directed graph of the causal atlas.
|
||||
|
||||
| Feature | Implementation |
|
||||
|---------|---------------|
|
||||
| **Node rendering** | `THREE.InstancedMesh` for events — color by domain (Kepler=blue, TESS=cyan, JWST=gold, derived=white) |
|
||||
| **Edge rendering** | `THREE.LineSegments` with opacity mapped to edge weight |
|
||||
| **Causal flow** | Animated particles along causal edges showing temporal direction |
|
||||
| **Scale selector** | Toggle between window scales (2h, 12h, 3d, 27d) — re-layouts graph |
|
||||
| **Candidate highlight** | Click candidate in sidebar to trace its causal chain in 3D, dimming unrelated nodes |
|
||||
| **Witness replay** | Step through witness chain entries, animating graph state forward/backward |
|
||||
| **LOD** | Level-of-detail: far=boundary nodes only, mid=top-k events, close=full subgraph |
|
||||
|
||||
Data source: `GET /api/atlas/query`, `GET /api/atlas/trace`
|
||||
|
||||
#### V2: Coherence Field Heatmap (Three.js + shader)
|
||||
|
||||
Real-time coherence field rendered as a colored surface over the atlas graph.
|
||||
|
||||
| Feature | Implementation |
|
||||
|---------|---------------|
|
||||
| **Field surface** | `THREE.PlaneGeometry` subdivided grid, vertex colors from coherence values |
|
||||
| **Cut pressure** | Red hotspots where cut pressure is high, cool blue where stable |
|
||||
| **Partition boundaries** | Glowing wireframe lines at partition cuts |
|
||||
| **Time scrubber** | Scrub through epochs to see coherence evolution |
|
||||
| **Drift overlay** | Toggle to show embedding drift as animated vector arrows |
|
||||
| **Alert markers** | Pulsing icons at boundary alert locations |
|
||||
|
||||
Data source: `GET /api/coherence`, `GET /api/boundary/timeline`, `WS /ws/live`
|
||||
|
||||
#### V3: Planet Candidate Dashboard (2D panels + 3D orbit)
|
||||
|
||||
Split view combining data panels with 3D orbital visualization.
|
||||
|
||||
| Panel | Content |
|
||||
|-------|---------|
|
||||
| **Ranked list** | Sortable table: candidate ID, score, uncertainty, period, SNR, publishable status |
|
||||
| **Light curve viewer** | Interactive D3 chart: raw flux, detrended flux, transit model overlay, per-window score |
|
||||
| **Phase-folded plot** | All transits folded at detected period, with confidence band |
|
||||
| **3D orbit preview** | `THREE.Line` showing inferred orbital path around host star, sized by uncertainty |
|
||||
| **Evidence trace** | Expandable tree showing witness chain from raw data to final score |
|
||||
| **Score breakdown** | Radar chart: SNR, shape consistency, period stability, coherence stability |
|
||||
|
||||
Data source: `GET /api/candidates/planet`, `GET /api/candidates/:id/trace`
|
||||
|
||||
#### V4: Life Candidate Dashboard (2D panels + 3D molecule)
|
||||
|
||||
Split view for spectral disequilibrium analysis.
|
||||
|
||||
| Panel | Content |
|
||||
|-------|---------|
|
||||
| **Ranked list** | Sortable table: candidate ID, disequilibrium score, uncertainty, molecule flags, publishable |
|
||||
| **Spectrum viewer** | Interactive D3 chart: wavelength vs flux, molecule absorption bands highlighted |
|
||||
| **Molecule presence matrix** | Heatmap of detected molecule families vs confidence |
|
||||
| **3D molecule overlay** | `THREE.Sprite` labels at absorption wavelengths in a 3D wavelength space |
|
||||
| **Reaction graph** | Force-directed graph of molecule co-occurrences vs equilibrium expectations |
|
||||
| **Confound panel** | Bar chart: stellar activity penalty, contamination risk, repeatability score |
|
||||
|
||||
Data source: `GET /api/candidates/life`, `GET /api/candidates/:id/trace`
|
||||
|
||||
#### V5: System Status Dashboard
|
||||
|
||||
Operational health and download progress.
|
||||
|
||||
| Panel | Content |
|
||||
|-------|---------|
|
||||
| **Download progress** | Per-tier progress bars with byte counts and ETA |
|
||||
| **Segment sizes** | Stacked bar chart of RVF segment utilization |
|
||||
| **Memory tiers** | S/M/L tier fill levels and compaction history |
|
||||
| **Witness chain** | Scrolling log of recent witness entries with hash preview |
|
||||
| **Pipeline status** | P0/P1/P2 and L0/L1/L2 stage indicators with event counts |
|
||||
| **Performance** | Query latency histogram, events/second throughput |
|
||||
|
||||
Data source: `GET /api/status`, `GET /api/memory/tiers`, `WS /ws/live`
|
||||
|
||||
### WebSocket Live Stream
|
||||
|
||||
```typescript
|
||||
// WS /ws/live — server pushes events as they happen
|
||||
interface LiveEvent {
|
||||
type: 'boundary_alert' | 'candidate_new' | 'candidate_update' |
|
||||
'download_progress' | 'witness_commit' | 'pipeline_stage' |
|
||||
'coherence_update';
|
||||
timestamp: string;
|
||||
data: Record<string, unknown>;
|
||||
}
|
||||
```
|
||||
|
||||
The dashboard subscribes on connect and updates all views in real-time as
|
||||
pipelines process data and boundaries evolve.
|
||||
|
||||
### Vite Build Configuration
|
||||
|
||||
```typescript
|
||||
// vite.config.ts for dashboard build
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
outDir: 'dist/dashboard',
|
||||
assetsDir: 'assets',
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
three: ['three'], // ~150 KB gzipped
|
||||
d3: ['d3-scale', 'd3-axis', 'd3-shape', 'd3-selection'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Bundle budget**: < 500 KB gzipped total (Three.js ~150 KB, D3 subset ~30 KB,
|
||||
app logic ~50 KB, styles ~10 KB). The dashboard adds minimal overhead to the
|
||||
RVF artifact.
|
||||
|
||||
### Design Decision: D5 — Dashboard Embedded in RVF
|
||||
|
||||
The Three.js dashboard is bundled at build time and embedded in `DASHBOARD_SEG`
|
||||
rather than served from an external CDN or requiring a separate install. This
|
||||
ensures:
|
||||
|
||||
1. **Fully offline**: Works without network after boot
|
||||
2. **Version-locked**: Dashboard always matches the API version it queries
|
||||
3. **Single artifact**: One RVF file = runtime + data + visualization
|
||||
4. **Witness-aligned**: Dashboard renders exactly the data the witness chain
|
||||
can verify
|
||||
> See [ADR-040a: Planet Detection Dashboard](ADR-040a-planet-detection-dashboard.md) for Views V1-V5, WebSocket streaming, Vite build configuration, and design decision D5.
|
||||
|
||||
## Package Structure
|
||||
|
||||
|
|
@ -816,37 +353,7 @@ packages/agentdb-causal-atlas/
|
|||
ProgressiveDownloader.ts # Tiered lazy download with resume
|
||||
DataManifest.ts # URL + hash + size manifests
|
||||
KernelBuilder.ts # Reuse/extend from ADR-008
|
||||
dashboard/ # Vite + Three.js visualization app
|
||||
vite.config.ts # Build config — outputs to dist/dashboard/
|
||||
index.html # SPA entry point
|
||||
src/
|
||||
main.ts # App bootstrap, router, WS connection
|
||||
api.ts # Typed fetch wrappers for /api/* endpoints
|
||||
ws.ts # WebSocket client for /ws/live
|
||||
views/
|
||||
AtlasExplorer.ts # V1: 3D causal atlas (Three.js force graph)
|
||||
CoherenceHeatmap.ts # V2: Coherence field surface + cut pressure
|
||||
PlanetDashboard.ts # V3: Planet candidates + light curves + 3D orbit
|
||||
LifeDashboard.ts # V4: Life candidates + spectra + molecule graph
|
||||
StatusDashboard.ts # V5: System health, downloads, witness log
|
||||
three/
|
||||
AtlasGraph.ts # InstancedMesh nodes, LineSegments edges, particles
|
||||
CoherenceSurface.ts # PlaneGeometry with vertex-colored field
|
||||
OrbitPreview.ts # Orbital path visualization
|
||||
CausalFlow.ts # Animated particles along causal edges
|
||||
LODController.ts # Level-of-detail: boundary → top-k → full
|
||||
charts/
|
||||
LightCurveChart.ts # D3 flux time series with transit overlay
|
||||
SpectrumChart.ts # D3 wavelength vs flux with molecule bands
|
||||
RadarChart.ts # Score breakdown radar
|
||||
MoleculeMatrix.ts # Heatmap of molecule presence vs confidence
|
||||
components/
|
||||
Sidebar.ts # Candidate list, filters, search
|
||||
TimeScrubber.ts # Epoch scrubber for coherence replay
|
||||
WitnessLog.ts # Scrolling witness chain entries
|
||||
DownloadProgress.ts # Tier progress bars
|
||||
styles/
|
||||
main.css # Minimal Tailwind or hand-rolled styles
|
||||
dashboard/ # See ADR-040a for full dashboard tree
|
||||
tests/
|
||||
causal-atlas.test.ts
|
||||
planet-detection.test.ts
|
||||
|
|
@ -854,28 +361,11 @@ packages/agentdb-causal-atlas/
|
|||
progressive-download.test.ts
|
||||
coherence-field.test.ts
|
||||
boundary-tracker.test.ts
|
||||
dashboard.test.ts # Dashboard build + API integration tests
|
||||
```
|
||||
|
||||
### Rust Implementation
|
||||
|
||||
The pipeline examples are implemented in Rust using the RVF runtime crates:
|
||||
|
||||
| Crate | Role |
|
||||
|-------|------|
|
||||
| `rvf-types` | Core types, segment definitions, derivation types |
|
||||
| `rvf-runtime` | RvfStore, HNSW indexing, metadata filters, queries |
|
||||
| `rvf-crypto` | SHAKE-256 witness chains, verification |
|
||||
| `rvf-wire` | Wire format serialization |
|
||||
| `rvf-manifest` | Segment manifests and policies |
|
||||
| `rvf-index` | Progressive HNSW index construction |
|
||||
| `rvf-quant` | Vector quantization (PQ, SQ) |
|
||||
| `rvf-kernel` | Kernel/initrd segment embedding |
|
||||
| `rvf-launch` | Boot sequence and runtime launch |
|
||||
| `rvf-ebpf` | eBPF socket/syscall policies |
|
||||
| `rvf-server` | HTTP/WS server for dashboard and API |
|
||||
|
||||
Examples location: `examples/rvf/examples/`
|
||||
> See [ADR-040b](ADR-040b-microlensing-graphcut-extensions.md#rust-implementation) for the full Rust crate table. Examples location: `examples/rvf/examples/`
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
|
|
@ -883,55 +373,45 @@ Examples location: `examples/rvf/examples/`
|
|||
|
||||
**Scope**: Kepler and TESS only. No spectra. No life scoring.
|
||||
|
||||
1. Implement `ProgressiveDownloader` with tier-0 curated dataset (100 Kepler targets)
|
||||
2. Implement `PlanetTransitAdapter` for FITS light curve ingestion
|
||||
3. Implement `CausalAtlas` with windowing, feature extraction, SIMD embedding
|
||||
4. Implement `PlanetDetection` pipeline (P0-P2)
|
||||
5. Implement `WITNESS_SEG` with SHAKE-256 chain
|
||||
1. `ProgressiveDownloader` with tier-0 curated dataset (100 Kepler targets)
|
||||
2. `PlanetTransitAdapter` for FITS light curve ingestion
|
||||
3. `CausalAtlas` with windowing, feature extraction, SIMD embedding
|
||||
4. `PlanetDetection` pipeline (P0-P2)
|
||||
5. `WITNESS_SEG` with SHAKE-256 chain
|
||||
6. CLI: `rvf run`, `rvf query planet list`, `rvf trace`
|
||||
7. HTTP: `/api/candidates/planet`, `/api/atlas/trace`
|
||||
8. Dashboard: Vite scaffold, V1 Atlas Explorer (Three.js 3D graph), V3 Planet
|
||||
Dashboard (ranked list + light curve chart), V5 Status Dashboard (download
|
||||
progress + witness log). Embedded in `DASHBOARD_SEG`, served at `/`
|
||||
9. WebSocket `/ws/live` for real-time pipeline progress
|
||||
8. Dashboard shell: V1, V3, V5 views (see ADR-040a), WebSocket `/ws/live`
|
||||
|
||||
**Acceptance**: 1,000 Kepler targets, top-100 ranked list includes >= 80
|
||||
confirmed planets, every item replays to same score and witness root on two
|
||||
machines. Dashboard renders atlas graph and candidate list in browser.
|
||||
machines.
|
||||
|
||||
### Phase 2: Coherence Field + Boundary Tracker + Dashboard V2 (v0.2)
|
||||
### Phase 2: Coherence Field + Boundary Tracker (v0.2)
|
||||
|
||||
1. Implement `CoherenceField` with dynamic min-cut, partition entropy
|
||||
2. Implement `BoundaryTracker` with timeline and alerts
|
||||
3. Implement `MultiScaleMemory` with S/M/L tiers and budget control
|
||||
4. Add coherence gating to planet pipeline
|
||||
1. `CoherenceField` with dynamic min-cut, partition entropy
|
||||
2. `BoundaryTracker` with timeline and alerts
|
||||
3. `MultiScaleMemory` with S/M/L tiers and budget control
|
||||
4. Coherence gating added to planet pipeline
|
||||
5. HTTP: `/api/coherence`, `/api/boundary/*`, `/api/memory/tiers`
|
||||
6. Dashboard: V2 Coherence Heatmap (Three.js field surface + cut pressure
|
||||
overlay + time scrubber), boundary alert markers via WebSocket
|
||||
6. Dashboard V2 Coherence Heatmap (see ADR-040a)
|
||||
|
||||
### Phase 3: Life Candidate Pipeline + Dashboard V4 (v0.3)
|
||||
### Phase 3: Life Candidate Pipeline (v0.3)
|
||||
|
||||
1. Implement `SpectrumAdapter` for JWST/archive spectral data
|
||||
2. Implement `LifeCandidate` pipeline (L0-L2)
|
||||
3. Implement disequilibrium scoring with reaction plausibility graph
|
||||
4. Tier-3 progressive download for spectral data
|
||||
5. CLI: `rvf query life list`
|
||||
6. HTTP: `/api/candidates/life`
|
||||
7. Dashboard: V4 Life Dashboard (spectrum viewer + molecule presence matrix
|
||||
+ reaction graph + confound panel)
|
||||
1. `SpectrumAdapter` for JWST/archive spectral data
|
||||
2. `LifeCandidate` pipeline (L0-L2) with disequilibrium scoring
|
||||
3. Tier-3 progressive download for spectral data
|
||||
4. CLI: `rvf query life list`; HTTP: `/api/candidates/life`
|
||||
5. Dashboard V4 Life Dashboard (see ADR-040a)
|
||||
|
||||
**Acceptance**: Published spectra with known atmospheric detections vs nulls,
|
||||
AUC > 0.8, every score includes confound penalties and provenance trace.
|
||||
Dashboard renders spectrum analysis in browser.
|
||||
**Acceptance**: AUC > 0.8 on published spectra with known atmospheric
|
||||
detections vs nulls, every score includes confound penalties and provenance.
|
||||
|
||||
### Phase 4: Cosmic Web + Full Integration (v0.4)
|
||||
|
||||
1. `CosmicWebAdapter` for SDSS spatial graph
|
||||
2. Cross-domain coherence (planet candidates enriched by large-scale context)
|
||||
3. Dashboard: 3D cosmic web view, cross-domain candidate linking
|
||||
4. Full offline demo with sealed RVF snapshot
|
||||
5. `rvf ingest --expand` for tier-2 bulk download
|
||||
6. Dashboard polish: LOD optimization, mobile-responsive layout, dark/light theme
|
||||
3. Full offline demo with sealed RVF snapshot
|
||||
4. `rvf ingest --expand` for tier-2 bulk download
|
||||
|
||||
## Evaluation Plan
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,6 @@
|
|||
//! Real Microlensing Data Analysis Pipeline
|
||||
//!
|
||||
//! Downloads and analyzes real microlensing light curves from public surveys:
|
||||
//! - OGLE Early Warning System (EWS)
|
||||
//! - MOA Alerts
|
||||
//!
|
||||
//! Features progressive download manifest with real OGLE/MOA event parameters,
|
||||
//! cross-survey normalization, and anomaly detection via graph-cut residual analysis.
|
||||
//!
|
||||
//! Real Microlensing Data Analysis Pipeline — simulates OGLE/MOA light curves
|
||||
//! from published event parameters, with cross-survey normalization, binary-lens
|
||||
//! planetary perturbations, and anomaly detection via graph-cut residual analysis.
|
||||
//! Run: cargo run --example real_microlensing --release
|
||||
|
||||
use rvf_runtime::{
|
||||
|
|
@ -22,317 +16,87 @@ const FIELD_SOURCE: u16 = 0;
|
|||
const FIELD_EVENT: u16 = 1;
|
||||
const FIELD_ANOMALY_TYPE: u16 = 2;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LCG deterministic random
|
||||
// ---------------------------------------------------------------------------
|
||||
fn lcg_f64(state: &mut u64) -> f64 {
|
||||
*state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
||||
((*state >> 33) as f64) / (u32::MAX as f64)
|
||||
}
|
||||
|
||||
fn lcg_normal(state: &mut u64) -> f64 {
|
||||
let u1 = lcg_f64(state).max(1e-15);
|
||||
let u2 = lcg_f64(state);
|
||||
const LCG_MUL: u64 = 6364136223846793005;
|
||||
const LCG_INC: u64 = 1442695040888963407;
|
||||
fn lcg_f64(s: &mut u64) -> f64 { *s = s.wrapping_mul(LCG_MUL).wrapping_add(LCG_INC); ((*s >> 33) as f64) / (u32::MAX as f64) }
|
||||
fn lcg_normal(s: &mut u64) -> f64 {
|
||||
let (u1, u2) = (lcg_f64(s).max(1e-15), lcg_f64(s));
|
||||
(-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
|
||||
}
|
||||
|
||||
fn random_vector(dim: usize, seed: u64) -> Vec<f32> {
|
||||
let mut v = Vec::with_capacity(dim);
|
||||
let mut x = seed.wrapping_add(1);
|
||||
for _ in 0..dim {
|
||||
x = x.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
||||
v.push(((x >> 33) as f32) / (u32::MAX as f32) - 0.5);
|
||||
}
|
||||
v
|
||||
(0..dim).map(|_| { x = x.wrapping_mul(LCG_MUL).wrapping_add(LCG_INC); ((x >> 33) as f32) / (u32::MAX as f32) - 0.5 }).collect()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Photometric data types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Observation {
|
||||
hjd: f64, mag: f64, mag_err: f64, flux: f64, flux_err: f64,
|
||||
}
|
||||
|
||||
struct Observation { hjd: f64, mag: f64, mag_err: f64, flux: f64, flux_err: f64 }
|
||||
#[derive(Debug, Clone)]
|
||||
struct MicrolensingEvent {
|
||||
name: String,
|
||||
source: String,
|
||||
observations: Vec<Observation>,
|
||||
baseline_mag: f64,
|
||||
peak_mag: f64,
|
||||
t0_est: f64,
|
||||
t_e_est: f64,
|
||||
u0_est: f64,
|
||||
name: String, source: String, observations: Vec<Observation>,
|
||||
baseline_mag: f64, peak_mag: f64, t0_est: f64, t_e_est: f64, u0_est: f64,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Enhanced OGLE format parser
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Parse OGLE EWS photometry. Handles HJD and JD time formats, magnitude and
|
||||
/// flux columns (detected by value range: mag > 10, flux < 100), comment lines,
|
||||
/// and malformed/NaN/inf values gracefully.
|
||||
fn parse_ogle_format(data: &str, event_name: &str) -> MicrolensingEvent {
|
||||
let mut observations = Vec::new();
|
||||
|
||||
for line in data.lines() {
|
||||
let line = line.trim();
|
||||
if line.is_empty() || line.starts_with('#') || line.starts_with("\\") { continue; }
|
||||
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() < 3 { continue; }
|
||||
|
||||
let (hjd, val, err) = match (parts[0].parse::<f64>(), parts[1].parse::<f64>(), parts[2].parse::<f64>()) {
|
||||
(Ok(t), Ok(v), Ok(e)) => (t, v, e),
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
// Skip NaN/inf values
|
||||
if hjd.is_nan() || hjd.is_infinite() || val.is_nan() || val.is_infinite()
|
||||
|| err.is_nan() || err.is_infinite() { continue; }
|
||||
|
||||
// JD -> HJD conversion: subtract 2450000 if time looks like full JD
|
||||
let hjd = if hjd > 2400000.0 { hjd - 2450000.0 } else { hjd };
|
||||
|
||||
// Detect magnitude vs flux by value range: magnitudes > 10, flux < 100
|
||||
let (mag, mag_err) = if val > 10.0 {
|
||||
(val, err) // already magnitude
|
||||
} else if val > 0.0 {
|
||||
// Flux column: convert to magnitude using arbitrary zero-point 22.0
|
||||
let m = -2.5 * val.log10() + 22.0;
|
||||
let me = 2.5 / (val * 2.302585) * err;
|
||||
(m, me)
|
||||
} else { continue; };
|
||||
|
||||
if mag_err <= 0.0 || mag_err > 1.0 || mag < 5.0 || mag > 25.0 { continue; }
|
||||
|
||||
observations.push(Observation { hjd, mag, mag_err, flux: 0.0, flux_err: 0.0 });
|
||||
}
|
||||
|
||||
finalize_event(observations, event_name, "OGLE")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MOA format parser
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Parse MOA-II photometry. MOA column order: JD, flux, flux_error (no magnitude).
|
||||
/// Normalizes to relative flux using baseline estimated from first/last 20% of data.
|
||||
fn parse_moa_format(data: &str, event_name: &str) -> MicrolensingEvent {
|
||||
let mut observations = Vec::new();
|
||||
|
||||
for line in data.lines() {
|
||||
let line = line.trim();
|
||||
if line.is_empty() || line.starts_with('#') { continue; }
|
||||
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() < 3 { continue; }
|
||||
|
||||
let (jd, flux, flux_err) = match (parts[0].parse::<f64>(), parts[1].parse::<f64>(), parts[2].parse::<f64>()) {
|
||||
(Ok(t), Ok(f), Ok(e)) => (t, f, e),
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
if jd.is_nan() || flux.is_nan() || flux_err.is_nan() { continue; }
|
||||
if flux.is_infinite() || flux_err.is_infinite() { continue; }
|
||||
if flux <= 0.0 || flux_err <= 0.0 { continue; }
|
||||
|
||||
let hjd = if jd > 2400000.0 { jd - 2450000.0 } else { jd };
|
||||
let mag = -2.5 * flux.log10() + 22.0;
|
||||
let mag_err = 2.5 / (flux * 2.302585) * flux_err;
|
||||
|
||||
if mag_err <= 0.0 || mag_err > 1.0 { continue; }
|
||||
observations.push(Observation { hjd, mag, mag_err, flux: 0.0, flux_err: 0.0 });
|
||||
}
|
||||
|
||||
// Baseline from first/last 20% of sorted observations
|
||||
observations.sort_by(|a, b| a.hjd.partial_cmp(&b.hjd).unwrap());
|
||||
let n = observations.len();
|
||||
let wing = (n as f64 * 0.2).ceil() as usize;
|
||||
if n > 0 && wing > 0 {
|
||||
let baseline_flux: f64 = observations[..wing].iter()
|
||||
.chain(observations[n.saturating_sub(wing)..].iter())
|
||||
.map(|o| 10.0f64.powf(-0.4 * (o.mag - 22.0)))
|
||||
.sum::<f64>() / (2 * wing) as f64;
|
||||
// Normalize all to relative flux
|
||||
for obs in &mut observations {
|
||||
let raw = 10.0f64.powf(-0.4 * (obs.mag - 22.0));
|
||||
obs.flux = raw / baseline_flux;
|
||||
obs.flux_err = obs.flux * 0.4 * 2.302585 * obs.mag_err;
|
||||
}
|
||||
}
|
||||
|
||||
finalize_event(observations, event_name, "MOA")
|
||||
}
|
||||
|
||||
/// Shared finalization: sort, compute baseline/peak, convert to flux.
|
||||
fn finalize_event(mut obs: Vec<Observation>, name: &str, source: &str) -> MicrolensingEvent {
|
||||
obs.sort_by(|a, b| a.hjd.partial_cmp(&b.hjd).unwrap());
|
||||
let mut mags: Vec<f64> = obs.iter().map(|o| o.mag).collect();
|
||||
mags.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
let baseline_mag = if !mags.is_empty() { mags[mags.len() * 3 / 4] } else { 20.0 };
|
||||
let peak_mag = mags.first().copied().unwrap_or(20.0);
|
||||
|
||||
for o in &mut obs {
|
||||
if o.flux == 0.0 {
|
||||
o.flux = 10.0f64.powf(-0.4 * (o.mag - baseline_mag));
|
||||
o.flux_err = o.flux * 0.4 * 2.302585 * o.mag_err;
|
||||
}
|
||||
}
|
||||
|
||||
let t0_est = obs.iter().min_by(|a, b| a.mag.partial_cmp(&b.mag).unwrap())
|
||||
.map(|o| o.hjd).unwrap_or(0.0);
|
||||
let half_mag = (baseline_mag + peak_mag) / 2.0;
|
||||
let above: Vec<_> = obs.iter().filter(|o| o.mag < half_mag).collect();
|
||||
let t_e_est = if above.len() >= 2 {
|
||||
(above.last().unwrap().hjd - above.first().unwrap().hjd) / 2.0
|
||||
} else { 20.0 };
|
||||
let peak_flux = 10.0f64.powf(-0.4 * (peak_mag - baseline_mag));
|
||||
let u0_est = if peak_flux > 1.5 { (1.0 / peak_flux).max(0.01) } else { 0.5 };
|
||||
|
||||
MicrolensingEvent {
|
||||
name: name.to_string(), source: source.to_string(), observations: obs,
|
||||
baseline_mag, peak_mag, t0_est, t_e_est, u0_est,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Progressive download manifest
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct PublishedParams {
|
||||
t0: f64, t_e: f64, u0: f64,
|
||||
has_planet: bool,
|
||||
planet_mass_ratio: Option<f64>,
|
||||
einstein_time: f64, // t_E in days
|
||||
impact_param: f64, // u_0
|
||||
planet_mass_ratio: Option<f64>, // q (None = no planet)
|
||||
planet_separation: Option<f64>, // s (None = no planet)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ManifestEntry {
|
||||
name: &'static str,
|
||||
survey: &'static str,
|
||||
url_template: &'static str,
|
||||
year: u16,
|
||||
event_id: &'static str,
|
||||
published_params: Option<PublishedParams>,
|
||||
event_id: String, // e.g. "OGLE-2005-BLG-390"
|
||||
survey: String, // "ogle" or "moa"
|
||||
published: PublishedParams,
|
||||
}
|
||||
|
||||
struct DownloadManifest { events: Vec<ManifestEntry> }
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum DownloadState { Pending, Simulated, #[allow(dead_code)] Downloaded, Failed(String) }
|
||||
|
||||
impl std::fmt::Display for DownloadState {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Pending => write!(f, "PEND"),
|
||||
Self::Simulated => write!(f, "SIM"),
|
||||
Self::Downloaded => write!(f, "DL"),
|
||||
Self::Failed(e) => write!(f, "FAIL:{}", e),
|
||||
Self::Pending => write!(f, "PEND"), Self::Simulated => write!(f, "SIM"),
|
||||
Self::Downloaded => write!(f, "DL"), Self::Failed(e) => write!(f, "FAIL:{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_manifest() -> DownloadManifest {
|
||||
let ogle = "https://ogle.astrouw.edu.pl/ogle4/ews/{year}/blg-{id}/phot.dat";
|
||||
let moa = "https://www.massey.ac.nz/~iabond/moa/alert{year}/fetch.php?path=moa{id}";
|
||||
DownloadManifest { events: vec![
|
||||
ManifestEntry { name: "OGLE-2005-BLG-390", survey: "OGLE", url_template: ogle,
|
||||
year: 2005, event_id: "390",
|
||||
published_params: Some(PublishedParams { t0: 3582.7, t_e: 11.03, u0: 0.359,
|
||||
has_planet: true, planet_mass_ratio: Some(7.6e-5) }) },
|
||||
ManifestEntry { name: "MOA-2011-BLG-262", survey: "MOA", url_template: moa,
|
||||
year: 2011, event_id: "262",
|
||||
published_params: Some(PublishedParams { t0: 5700.5, t_e: 3.3, u0: 0.05,
|
||||
has_planet: true, planet_mass_ratio: Some(4e-4) }) },
|
||||
ManifestEntry { name: "OGLE-2016-BLG-1195", survey: "OGLE", url_template: ogle,
|
||||
year: 2016, event_id: "1195",
|
||||
published_params: Some(PublishedParams { t0: 7568.0, t_e: 8.2, u0: 0.35,
|
||||
has_planet: true, planet_mass_ratio: Some(4.5e-5) }) },
|
||||
ManifestEntry { name: "MOA-2009-BLG-387", survey: "MOA", url_template: moa,
|
||||
year: 2009, event_id: "387",
|
||||
published_params: Some(PublishedParams { t0: 5010.0, t_e: 42.5, u0: 0.012,
|
||||
has_planet: true, planet_mass_ratio: Some(3.2e-3) }) },
|
||||
ManifestEntry { name: "OGLE-2003-BLG-235", survey: "OGLE", url_template: ogle,
|
||||
year: 2003, event_id: "235",
|
||||
published_params: Some(PublishedParams { t0: 2838.5, t_e: 61.5, u0: 0.133,
|
||||
has_planet: true, planet_mass_ratio: Some(3.9e-3) }) },
|
||||
ManifestEntry { name: "OGLE-2005-BLG-071", survey: "OGLE", url_template: ogle,
|
||||
year: 2005, event_id: "071",
|
||||
published_params: Some(PublishedParams { t0: 3469.1, t_e: 70.9, u0: 0.026,
|
||||
has_planet: true, planet_mass_ratio: Some(7.0e-3) }) },
|
||||
ManifestEntry { name: "MOA-2007-BLG-192", survey: "MOA", url_template: moa,
|
||||
year: 2007, event_id: "192",
|
||||
published_params: Some(PublishedParams { t0: 4239.0, t_e: 38.0, u0: 0.29,
|
||||
has_planet: true, planet_mass_ratio: Some(2.0e-4) }) },
|
||||
ManifestEntry { name: "OGLE-2006-BLG-109", survey: "OGLE", url_template: ogle,
|
||||
year: 2006, event_id: "109",
|
||||
published_params: Some(PublishedParams { t0: 3831.0, t_e: 127.3, u0: 0.0035,
|
||||
has_planet: true, planet_mass_ratio: Some(1.4e-3) }) },
|
||||
// Non-planet events for comparison
|
||||
ManifestEntry { name: "OGLE-2018-BLG-0799", survey: "OGLE", url_template: ogle,
|
||||
year: 2018, event_id: "0799",
|
||||
published_params: Some(PublishedParams { t0: 8250.0, t_e: 25.0, u0: 0.008,
|
||||
has_planet: false, planet_mass_ratio: None }) },
|
||||
ManifestEntry { name: "MOA-2019-BLG-008", survey: "MOA", url_template: moa,
|
||||
year: 2019, event_id: "008",
|
||||
published_params: Some(PublishedParams { t0: 8580.0, t_e: 18.0, u0: 0.15,
|
||||
has_planet: false, planet_mass_ratio: None }) },
|
||||
ManifestEntry { name: "OGLE-2012-BLG-0563", survey: "OGLE", url_template: ogle,
|
||||
year: 2012, event_id: "0563",
|
||||
published_params: Some(PublishedParams { t0: 6090.0, t_e: 55.0, u0: 0.045,
|
||||
has_planet: true, planet_mass_ratio: Some(1.5e-3) }) },
|
||||
ManifestEntry { name: "OGLE-2017-BLG-0373", survey: "OGLE", url_template: ogle,
|
||||
year: 2017, event_id: "0373",
|
||||
published_params: Some(PublishedParams { t0: 7870.0, t_e: 40.0, u0: 0.12,
|
||||
has_planet: false, planet_mass_ratio: None }) },
|
||||
ManifestEntry { name: "OGLE-2015-BLG-0966", survey: "OGLE", url_template: ogle,
|
||||
year: 2015, event_id: "0966",
|
||||
published_params: Some(PublishedParams { t0: 7190.0, t_e: 22.0, u0: 0.21,
|
||||
has_planet: false, planet_mass_ratio: None }) },
|
||||
ManifestEntry { name: "MOA-2013-BLG-220", survey: "MOA", url_template: moa,
|
||||
year: 2013, event_id: "220",
|
||||
published_params: Some(PublishedParams { t0: 6440.0, t_e: 60.0, u0: 0.08,
|
||||
has_planet: false, planet_mass_ratio: None }) },
|
||||
ManifestEntry { name: "MOA-2015-BLG-337", survey: "MOA", url_template: moa,
|
||||
year: 2015, event_id: "337",
|
||||
published_params: Some(PublishedParams { t0: 7200.0, t_e: 15.0, u0: 0.30,
|
||||
has_planet: false, planet_mass_ratio: None }) },
|
||||
]}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cross-survey normalization
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Normalize light curves to fractional deviation from baseline: (F - F_base) / F_base.
|
||||
/// OGLE-IV I-band zero-point ~21.0, MOA-II R-band zero-point ~22.0.
|
||||
fn normalize_cross_survey(event: &mut MicrolensingEvent) {
|
||||
let zp = match event.source.as_str() {
|
||||
"MOA" => 22.0,
|
||||
_ => 21.0, // OGLE-IV I-band
|
||||
fn build_manifest() -> Vec<ManifestEntry> {
|
||||
let e = |id: &str, survey: &str, t_e: f64, u0: f64, q: Option<f64>, s: Option<f64>| {
|
||||
ManifestEntry {
|
||||
event_id: id.into(), survey: survey.into(),
|
||||
published: PublishedParams { einstein_time: t_e, impact_param: u0,
|
||||
planet_mass_ratio: q, planet_separation: s },
|
||||
}
|
||||
};
|
||||
// Compute baseline flux from faintest 50% of observations
|
||||
let mut mags: Vec<f64> = event.observations.iter().map(|o| o.mag).collect();
|
||||
mags.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
let mid = mags.len() / 2;
|
||||
let baseline_flux = if mid > 0 {
|
||||
let bm: f64 = mags[mid..].iter().sum::<f64>() / (mags.len() - mid) as f64;
|
||||
10.0f64.powf(-0.4 * (bm - zp))
|
||||
} else { 1.0 };
|
||||
|
||||
for obs in &mut event.observations {
|
||||
let f = 10.0f64.powf(-0.4 * (obs.mag - zp));
|
||||
obs.flux = (f - baseline_flux) / baseline_flux; // fractional deviation
|
||||
obs.flux_err = f * 0.4 * 2.302585 * obs.mag_err / baseline_flux;
|
||||
}
|
||||
vec![
|
||||
// --- Planetary events ---
|
||||
// First cold rocky/icy super-Earth
|
||||
e("OGLE-2005-BLG-390", "ogle", 11.0, 0.359, Some(7.6e-5), Some(1.610)),
|
||||
// Possible rogue planet with moon
|
||||
e("MOA-2011-BLG-262", "moa", 3.3, 0.04, Some(3.8e-4), None),
|
||||
// Ice-line planet, very low mass ratio
|
||||
e("OGLE-2016-BLG-1195","ogle", 9.9, 0.045, Some(4.2e-5), Some(1.04)),
|
||||
// First microlensing planet (OGLE+MOA joint)
|
||||
e("OGLE-2003-BLG-235", "ogle", 61.5, 0.133, Some(0.0039), Some(1.12)),
|
||||
// Very low-mass host star planet
|
||||
e("MOA-2007-BLG-192", "moa", 46.0, 0.001, Some(0.0002), Some(1.05)),
|
||||
// Jupiter/Saturn analog system (two planets)
|
||||
e("OGLE-2006-BLG-109", "ogle",127.3, 0.0035,Some(1.4e-3), Some(2.3)),
|
||||
// 3.8 M_Jup planet
|
||||
e("OGLE-2005-BLG-071", "ogle", 70.9, 0.026, Some(7.0e-3), Some(1.29)),
|
||||
// Saturn-mass planet
|
||||
e("OGLE-2012-BLG-0563","ogle", 55.0, 0.045, Some(1.5e-3), Some(1.42)),
|
||||
// --- Non-planet PSPL comparison events ---
|
||||
e("OGLE-2018-BLG-0799","ogle", 25.0, 0.008, None, None),
|
||||
e("MOA-2019-BLG-008", "moa", 18.0, 0.15, None, None),
|
||||
e("OGLE-2017-BLG-0373","ogle", 40.0, 0.12, None, None),
|
||||
e("OGLE-2015-BLG-0966","ogle", 22.0, 0.21, None, None),
|
||||
e("MOA-2013-BLG-220", "moa", 60.0, 0.08, None, None),
|
||||
]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Simulation from published parameters (replaces actual download)
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---- Paczynski formula & binary-lens perturbation ----
|
||||
|
||||
fn pspl_magnification(u: f64) -> f64 {
|
||||
if u < 1e-10 { return 1e10; }
|
||||
|
|
@ -340,61 +104,189 @@ fn pspl_magnification(u: f64) -> f64 {
|
|||
(u2 + 2.0) / (u * (u2 + 4.0).sqrt())
|
||||
}
|
||||
|
||||
fn simulate_from_manifest(entry: &ManifestEntry, rng: &mut u64) -> (MicrolensingEvent, DownloadState) {
|
||||
let params = match entry.published_params {
|
||||
Some(p) => p,
|
||||
None => return (MicrolensingEvent {
|
||||
name: entry.name.to_string(), source: entry.survey.to_string(),
|
||||
observations: vec![], baseline_mag: 20.0, peak_mag: 20.0,
|
||||
t0_est: 0.0, t_e_est: 20.0, u0_est: 0.5,
|
||||
}, DownloadState::Failed("no published params".into())),
|
||||
};
|
||||
let t0 = 2459000.0 + params.t0 % 1000.0;
|
||||
/// Binary-lens perturbation factor near the planetary caustic.
|
||||
/// Uses the Chang-Refsdal approximation: excess magnification from
|
||||
/// a point-mass perturber at separation `s` with mass ratio `q`.
|
||||
fn binary_lens_perturbation(tau: f64, u0: f64, q: f64, s: f64) -> f64 {
|
||||
// Distance from source to planet position (planet at s along lens axis)
|
||||
let dx = tau - s;
|
||||
let d2 = dx * dx + u0 * u0;
|
||||
// Caustic half-width ~ 4*q/s^2 for close/wide topologies
|
||||
let caustic_r2 = 4.0 * q / (s * s);
|
||||
if d2 > caustic_r2 * 25.0 { return 0.0; } // too far from caustic
|
||||
if d2 < 1e-15 { return q * 1e4; } // avoid singularity
|
||||
// Smooth perturbation envelope
|
||||
let strength = q / d2.max(q * 0.1);
|
||||
strength * (-d2 / (8.0 * caustic_r2.max(1e-10))).exp()
|
||||
}
|
||||
|
||||
// ---- Simulate light curve from published parameters ----
|
||||
|
||||
fn simulate_from_published(entry: &ManifestEntry, rng: &mut u64) -> (MicrolensingEvent, DownloadState) {
|
||||
let p = &entry.published;
|
||||
let is_moa = entry.survey == "moa";
|
||||
let t0 = 2459000.0 + (lcg_f64(rng) * 500.0);
|
||||
let baseline_mag = 18.0 + lcg_f64(rng) * 3.0;
|
||||
let f_b = 0.05 + lcg_f64(rng) * 0.3;
|
||||
let cadence = if entry.survey == "MOA" { 0.25 } else { 0.5 };
|
||||
let sigma_floor = if entry.survey == "MOA" { 0.01 } else { 0.005 };
|
||||
let cadence = if is_moa { 0.25 } else { 0.5 }; // days between obs
|
||||
let sigma_floor = if is_moa { 0.01 } else { 0.005 };
|
||||
let span = p.einstein_time.max(10.0) * 3.0; // cover +-3 t_E
|
||||
|
||||
let mut observations = Vec::new();
|
||||
let mut t = t0 - 60.0;
|
||||
while t < t0 + 60.0 {
|
||||
t += cadence / 24.0 * (0.5 + lcg_f64(rng));
|
||||
if lcg_f64(rng) < 0.3 { continue; }
|
||||
let mut t = t0 - span;
|
||||
while t < t0 + span {
|
||||
t += cadence * (0.5 + lcg_f64(rng));
|
||||
if lcg_f64(rng) < 0.25 { continue; } // weather gaps
|
||||
// Night-time window: keep ~40% of observations
|
||||
if ((t % 1.0) - 0.2).abs() > 0.17 { continue; }
|
||||
|
||||
let tau = (t - t0) / params.t_e;
|
||||
let u = (params.u0 * params.u0 + tau * tau).sqrt();
|
||||
let mut mag_a = pspl_magnification(u);
|
||||
if params.has_planet {
|
||||
let q = params.planet_mass_ratio.unwrap_or(0.0);
|
||||
let s = 1.0 + lcg_f64(rng) * 0.01; // stable separation
|
||||
let d2 = (tau - s).powi(2) + params.u0.powi(2);
|
||||
if d2 < q * 25.0 && d2 > 1e-15 {
|
||||
mag_a += (q / d2.max(q * 0.1)) * (-d2 / (8.0 * q)).exp() * mag_a;
|
||||
}
|
||||
let tau = (t - t0) / p.einstein_time;
|
||||
let u = (p.impact_param * p.impact_param + tau * tau).sqrt();
|
||||
let mut amp = pspl_magnification(u);
|
||||
|
||||
// Add binary-lens planetary perturbation if planet present
|
||||
if let (Some(q), Some(s)) = (p.planet_mass_ratio, p.planet_separation) {
|
||||
amp += binary_lens_perturbation(tau, p.impact_param, q, s) * amp;
|
||||
} else if let Some(q) = p.planet_mass_ratio {
|
||||
// No separation published (e.g. MOA-2011-BLG-262): use s ~ 1.0
|
||||
amp += binary_lens_perturbation(tau, p.impact_param, q, 1.0) * amp;
|
||||
}
|
||||
let true_flux = mag_a + f_b;
|
||||
|
||||
let true_flux = amp + f_b;
|
||||
let true_mag = baseline_mag - 2.5 * true_flux.log10();
|
||||
let sigma = (sigma_floor.powi(2) + 0.003f64.powi(2)).sqrt();
|
||||
let sigma = (sigma_floor * sigma_floor + 0.003f64.powi(2)).sqrt();
|
||||
let obs_mag = true_mag + lcg_normal(rng) * sigma;
|
||||
observations.push(Observation {
|
||||
hjd: t, mag: obs_mag, mag_err: sigma, flux: 0.0, flux_err: 0.0,
|
||||
});
|
||||
observations.push(Observation { hjd: t, mag: obs_mag, mag_err: sigma, flux: 0.0, flux_err: 0.0 });
|
||||
}
|
||||
|
||||
let peak_mag = observations.iter().map(|o| o.mag).fold(f64::INFINITY, f64::min);
|
||||
let mut evt = MicrolensingEvent {
|
||||
name: entry.name.to_string(), source: entry.survey.to_string(),
|
||||
name: entry.event_id.clone(), source: entry.survey.clone(),
|
||||
observations, baseline_mag, peak_mag,
|
||||
t0_est: t0, t_e_est: params.t_e, u0_est: params.u0,
|
||||
t0_est: t0, t_e_est: p.einstein_time, u0_est: p.impact_param,
|
||||
};
|
||||
normalize_cross_survey(&mut evt);
|
||||
(evt, DownloadState::Simulated)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PSPL fitting (compact)
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---- Cross-survey normalization ----
|
||||
|
||||
/// Normalize to fractional deviation (F - F_base) / F_base.
|
||||
/// Survey-specific zero-points: OGLE I-band = 21.0, MOA R-band = 22.0.
|
||||
fn normalize_cross_survey(event: &mut MicrolensingEvent) {
|
||||
let zp = if event.source == "moa" { 22.0 } else { 21.0 };
|
||||
// Baseline flux from faintest 50% of observations
|
||||
let mut mags: Vec<f64> = event.observations.iter().map(|o| o.mag).collect();
|
||||
mags.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
let mid = mags.len() / 2;
|
||||
let baseline_flux = if mid > 0 {
|
||||
let bm: f64 = mags[mid..].iter().sum::<f64>() / (mags.len() - mid) as f64;
|
||||
10.0f64.powf(-0.4 * (bm - zp))
|
||||
} else { 1.0 };
|
||||
for obs in &mut event.observations {
|
||||
let f = 10.0f64.powf(-0.4 * (obs.mag - zp));
|
||||
obs.flux = (f - baseline_flux) / baseline_flux;
|
||||
obs.flux_err = f * 0.4 * 2.302585 * obs.mag_err / baseline_flux;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- OGLE format parser ----
|
||||
|
||||
/// Parse OGLE EWS photometry: HJD/JD, magnitude or flux, error.
|
||||
fn parse_ogle_format(data: &str, event_name: &str) -> MicrolensingEvent {
|
||||
let mut obs = Vec::new();
|
||||
for line in data.lines() {
|
||||
let line = line.trim();
|
||||
if line.is_empty() || line.starts_with('#') || line.starts_with('\\') { continue; }
|
||||
let p: Vec<&str> = line.split_whitespace().collect();
|
||||
if p.len() < 3 { continue; }
|
||||
let (hjd, val, err) = match (p[0].parse::<f64>(), p[1].parse::<f64>(), p[2].parse::<f64>()) {
|
||||
(Ok(t), Ok(v), Ok(e)) => (t, v, e), _ => continue,
|
||||
};
|
||||
if [hjd, val, err].iter().any(|x| x.is_nan() || x.is_infinite()) { continue; }
|
||||
let hjd = if hjd > 2400000.0 { hjd - 2450000.0 } else { hjd };
|
||||
let (mag, mag_err) = if val > 10.0 { (val, err) }
|
||||
else if val > 0.0 {
|
||||
(-2.5 * val.log10() + 22.0, 2.5 / (val * 2.302585) * err)
|
||||
} else { continue; };
|
||||
if mag_err <= 0.0 || mag_err > 1.0 || mag < 5.0 || mag > 25.0 { continue; }
|
||||
obs.push(Observation { hjd, mag, mag_err, flux: 0.0, flux_err: 0.0 });
|
||||
}
|
||||
finalize_event(obs, event_name, "ogle")
|
||||
}
|
||||
|
||||
// ---- Enhanced MOA format parser ----
|
||||
|
||||
/// Parse MOA-II photometry: JD, flux, flux_error columns.
|
||||
/// Baseline estimated from first and last 20% of time-sorted observations.
|
||||
fn parse_moa_format(data: &str, event_name: &str) -> MicrolensingEvent {
|
||||
let mut obs = Vec::new();
|
||||
for line in data.lines() {
|
||||
let line = line.trim();
|
||||
if line.is_empty() || line.starts_with('#') { continue; }
|
||||
let p: Vec<&str> = line.split_whitespace().collect();
|
||||
if p.len() < 3 { continue; }
|
||||
let (jd, flux, ferr) = match (p[0].parse::<f64>(), p[1].parse::<f64>(), p[2].parse::<f64>()) {
|
||||
(Ok(t), Ok(f), Ok(e)) => (t, f, e), _ => continue,
|
||||
};
|
||||
if [jd, flux, ferr].iter().any(|x| x.is_nan() || x.is_infinite()) { continue; }
|
||||
if flux <= 0.0 || ferr <= 0.0 { continue; }
|
||||
let hjd = if jd > 2400000.0 { jd - 2450000.0 } else { jd };
|
||||
// MOA R-band zero-point 22.0
|
||||
let mag = -2.5 * flux.log10() + 22.0;
|
||||
let mag_err = 2.5 / (flux * 2.302585) * ferr;
|
||||
if mag_err <= 0.0 || mag_err > 1.0 { continue; }
|
||||
obs.push(Observation { hjd, mag, mag_err, flux, flux_err: ferr });
|
||||
}
|
||||
// Sort by time, then estimate baseline from first/last 20%
|
||||
obs.sort_by(|a, b| a.hjd.partial_cmp(&b.hjd).unwrap());
|
||||
let n = obs.len();
|
||||
let wing = (n as f64 * 0.2).ceil() as usize;
|
||||
if n > 0 && wing > 0 {
|
||||
let wing_obs: Vec<f64> = obs[..wing].iter()
|
||||
.chain(obs[n.saturating_sub(wing)..].iter())
|
||||
.map(|o| o.flux).collect();
|
||||
let baseline_flux = wing_obs.iter().sum::<f64>() / wing_obs.len() as f64;
|
||||
if baseline_flux > 0.0 {
|
||||
for o in &mut obs {
|
||||
let raw = o.flux;
|
||||
o.flux = (raw - baseline_flux) / baseline_flux; // fractional deviation
|
||||
o.flux_err = o.flux_err / baseline_flux;
|
||||
}
|
||||
}
|
||||
}
|
||||
finalize_event(obs, event_name, "moa")
|
||||
}
|
||||
|
||||
/// Shared finalization: sort, compute baseline/peak, convert flux where needed.
|
||||
fn finalize_event(mut obs: Vec<Observation>, name: &str, source: &str) -> MicrolensingEvent {
|
||||
obs.sort_by(|a, b| a.hjd.partial_cmp(&b.hjd).unwrap());
|
||||
let mut mags: Vec<f64> = obs.iter().map(|o| o.mag).collect();
|
||||
mags.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
let baseline_mag = if !mags.is_empty() { mags[mags.len() * 3 / 4] } else { 20.0 };
|
||||
let peak_mag = mags.first().copied().unwrap_or(20.0);
|
||||
for o in &mut obs {
|
||||
if o.flux == 0.0 {
|
||||
o.flux = 10.0f64.powf(-0.4 * (o.mag - baseline_mag));
|
||||
o.flux_err = o.flux * 0.4 * 2.302585 * o.mag_err;
|
||||
}
|
||||
}
|
||||
let t0_est = obs.iter().min_by(|a, b| a.mag.partial_cmp(&b.mag).unwrap())
|
||||
.map(|o| o.hjd).unwrap_or(0.0);
|
||||
let half = (baseline_mag + peak_mag) / 2.0;
|
||||
let above: Vec<_> = obs.iter().filter(|o| o.mag < half).collect();
|
||||
let t_e_est = if above.len() >= 2 {
|
||||
(above.last().unwrap().hjd - above.first().unwrap().hjd) / 2.0
|
||||
} else { 20.0 };
|
||||
let pf = 10.0f64.powf(-0.4 * (peak_mag - baseline_mag));
|
||||
let u0_est = if pf > 1.5 { (1.0 / pf).max(0.01) } else { 0.5 };
|
||||
MicrolensingEvent {
|
||||
name: name.into(), source: source.into(), observations: obs,
|
||||
baseline_mag, peak_mag, t0_est, t_e_est, u0_est,
|
||||
}
|
||||
}
|
||||
|
||||
// ---- PSPL fitting ----
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct PSPLFit { t0: f64, u0: f64, t_e: f64, f_s: f64, f_b: f64, chi2: f64 }
|
||||
|
|
@ -422,7 +314,6 @@ fn fit_pspl_linear(obs: &[Observation], t0: f64, u0: f64, t_e: f64) -> Option<(f
|
|||
|
||||
fn fit_pspl(event: &MicrolensingEvent) -> PSPLFit {
|
||||
let mut best = PSPLFit { t0: 0.0, u0: 0.0, t_e: 0.0, f_s: 1.0, f_b: 0.1, chi2: f64::MAX };
|
||||
// Coarse grid
|
||||
for t0_i in 0..20 {
|
||||
let t0 = (event.t0_est - 10.0) + 20.0 * t0_i as f64 / 19.0;
|
||||
for te_i in 0..15 {
|
||||
|
|
@ -436,7 +327,6 @@ fn fit_pspl(event: &MicrolensingEvent) -> PSPLFit {
|
|||
}
|
||||
}
|
||||
}
|
||||
// Fine refinement around best
|
||||
let b = best.clone();
|
||||
for dt in -5..=5 { for dte in -5..=5 { for du in -5..=5 {
|
||||
let (t0, t_e, u0) = (b.t0 + dt as f64 * 0.2, b.t_e * (1.0 + dte as f64 * 0.02), b.u0 + du as f64 * 0.005);
|
||||
|
|
@ -448,9 +338,7 @@ fn fit_pspl(event: &MicrolensingEvent) -> PSPLFit {
|
|||
best
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Anomaly detection (compact)
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---- Anomaly detection ----
|
||||
|
||||
fn analyze_residuals(event: &MicrolensingEvent, fit: &PSPLFit) -> Vec<(f64, f64, String, Vec<f32>)> {
|
||||
let rchi2 = fit.chi2 / event.observations.len().max(1) as f64;
|
||||
|
|
@ -477,7 +365,6 @@ fn analyze_residuals(event: &MicrolensingEvent, fit: &PSPLFit) -> Vec<(f64, f64,
|
|||
let combined = 0.3 * excess + 0.7 * coherence_z;
|
||||
let class = if combined > 3.0 { if tau.abs() < 0.5 { "planet" } else { "moon-candidate" } }
|
||||
else if combined > 2.0 { "unknown" } else { "noise" };
|
||||
// 32-dim embedding
|
||||
let mean_r = resid.iter().sum::<f64>() / resid.len() as f64;
|
||||
let var_r = resid.iter().map(|r| (r - mean_r).powi(2)).sum::<f64>() / resid.len() as f64;
|
||||
let mut emb = vec![mean_r as f32, var_r.sqrt() as f32, excess as f32, coherence_z as f32, tau as f32];
|
||||
|
|
@ -494,9 +381,7 @@ fn analyze_residuals(event: &MicrolensingEvent, fit: &PSPLFit) -> Vec<(f64, f64,
|
|||
results
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main pipeline
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---- Main pipeline ----
|
||||
|
||||
fn main() {
|
||||
println!("=== Real Microlensing Analysis Pipeline ===\n");
|
||||
|
|
@ -508,33 +393,33 @@ fn main() {
|
|||
dimension: dim as u16, metric: DistanceMetric::Cosine, ..Default::default()
|
||||
}).expect("failed to create store");
|
||||
|
||||
// Step 1: Build manifest and simulate downloads
|
||||
println!("--- Step 1. Progressive Download Manifest ---\n");
|
||||
// Step 1: Build manifest and simulate from published parameters
|
||||
println!("--- Step 1. Download Manifest ({} real events) ---\n", "13");
|
||||
let manifest = build_manifest();
|
||||
let mut rng = 42u64;
|
||||
let mut events = Vec::new();
|
||||
let mut states = Vec::new();
|
||||
|
||||
for entry in &manifest.events {
|
||||
let (evt, state) = simulate_from_manifest(entry, &mut rng);
|
||||
let p = entry.published_params.unwrap();
|
||||
let planet_label = if p.has_planet {
|
||||
match p.planet_mass_ratio {
|
||||
Some(q) => format!("HAS PLANET (q={:.1e})", q),
|
||||
None => "HAS PLANET".to_string(),
|
||||
}
|
||||
} else { "no planet".to_string() };
|
||||
println!(" [{}] {:24} t_E={:5.1}d, u_0={:5.3}, {}",
|
||||
state, entry.name, p.t_e, p.u0, planet_label);
|
||||
for entry in &manifest {
|
||||
let (evt, state) = simulate_from_published(entry, &mut rng);
|
||||
let p = &entry.published;
|
||||
let label = match p.planet_mass_ratio {
|
||||
Some(q) => match p.planet_separation {
|
||||
Some(s) => format!("PLANET q={:.1e} s={:.2}", q, s),
|
||||
None => format!("PLANET q={:.1e}", q),
|
||||
},
|
||||
None => "PSPL-only".into(),
|
||||
};
|
||||
println!(" [{}] {:24} t_E={:6.1}d u_0={:5.3} {}", state, entry.event_id, p.einstein_time, p.impact_param, label);
|
||||
events.push(evt);
|
||||
states.push(state);
|
||||
}
|
||||
|
||||
// Also parse a synthetic OGLE/MOA data snippet to exercise the parsers
|
||||
// Exercise the parsers on synthetic snippets
|
||||
let _ogle_test = parse_ogle_format(
|
||||
"# OGLE-IV EWS photometry\n2453500.0 18.5 0.02\n2453501.0 17.2 0.015\n", "test-ogle");
|
||||
"# OGLE-IV EWS\n2453500.0 18.5 0.02\n2453501.0 17.2 0.015\n", "test-ogle");
|
||||
let _moa_test = parse_moa_format(
|
||||
"# MOA-II photometry\n2455700.0 500.0 10.0\n2455701.0 800.0 15.0\n", "test-moa");
|
||||
"# MOA-II\n2455700.0 500.0 10.0\n2455701.0 800.0 15.0\n2455702.0 520.0 11.0\n", "test-moa");
|
||||
|
||||
// Step 2: Fit and analyze
|
||||
println!("\n--- Step 2. PSPL Fit + Anomaly Search ---\n");
|
||||
|
|
@ -601,7 +486,7 @@ fn main() {
|
|||
println!(" Witness chain: {} entries, {}", verify_witness_chain(&chain).expect("verify").len(), "VALID");
|
||||
|
||||
println!("\n=== Summary ===\n");
|
||||
println!(" Manifest events: {}", manifest.events.len());
|
||||
println!(" Manifest events: {}", manifest.len());
|
||||
println!(" Simulated: {}", states.iter().filter(|s| matches!(s, DownloadState::Simulated)).count());
|
||||
println!(" Windows ingested: {}", ingest.accepted);
|
||||
println!(" Anomalies found: {}", discoveries.len());
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue