feat(cosmobiologia): rectificador automático — escaneo GR (núcleo)
Primer incremento del rectificador automático (#67): dado un conjunto de eventos conocidos de la vida del sujeto, barre las horas de nacimiento candidatas y devuelve la que mejor los explica vía el Sistema GR. La killer feature pro — desbloqueada al completar el GR. - cosmobiologia-render: `convergencia_minima` — medida CONTINUA de qué tan bien una carta explica un evento (suma de orbes del directo + converso más cerrados sobre un punto natal). 3 tests. - cosmobiologia-engine: módulo `rectify` — `rectificar` barre la ventana de horas candidatas; por candidata computa la carta (una vez, cacheada) y mide la convergencia GR a la edad de cada evento; elige el puntaje mínimo. Devuelve el perfil completo del barrido para que la UI lo dibuje como curva. Test end-to-end con eternal. - bridge: `compute_natal_chart`/`body_symbol`/consts GR → pub(crate). Falta: la UI (capturar eventos conocidos, lanzar el barrido, mostrar la curva y la hora rectificada). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -36,9 +36,9 @@ pub use cosmobiologia_model::{Chart, ChartId, ChartKind};
|
||||
// (`cosmobiologia_engine::Layer`, etc.) sin tener que cambiar
|
||||
// imports en el shell, canvas, modules, tree, panel...
|
||||
pub use cosmobiologia_render::{
|
||||
apply_harmonic, compute_gr_triggers, AspectSummary, Geometry, Glyph, GrDirection, GrTrigger,
|
||||
Layer, LayerKind, LineSeg, OverlayMeta, PointMark, RenderModel, UranianGroup,
|
||||
OUTER_RING_MODULES,
|
||||
apply_harmonic, compute_gr_triggers, convergencia_minima, AspectSummary, Geometry, Glyph,
|
||||
GrDirection, GrTrigger, Layer, LayerKind, LineSeg, OverlayMeta, PointMark, RenderModel,
|
||||
UranianGroup, OUTER_RING_MODULES,
|
||||
};
|
||||
|
||||
// `Chart` reexportado arriba es lo que `PipelineRequest::Synastry`
|
||||
@@ -52,6 +52,8 @@ mod dignity;
|
||||
#[cfg(feature = "eternal-bridge")]
|
||||
mod natal_cache;
|
||||
#[cfg(feature = "eternal-bridge")]
|
||||
mod rectify;
|
||||
#[cfg(feature = "eternal-bridge")]
|
||||
pub mod svg_export;
|
||||
|
||||
// =====================================================================
|
||||
@@ -210,6 +212,53 @@ impl Default for NatalOptions {
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Rectificador automático (Sistema GR)
|
||||
// =====================================================================
|
||||
|
||||
/// Un evento conocido de la vida del sujeto — el ancla de la
|
||||
/// rectificación. La hora de nacimiento verdadera es la que hace caer
|
||||
/// los eventos reales sobre convergencias GR cerradas.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct EventoConocido {
|
||||
/// Edad del sujeto, en años, cuando ocurrió el evento.
|
||||
pub edad_years: f64,
|
||||
}
|
||||
|
||||
/// Resultado de un barrido de rectificación (ver [`rectificar`]).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Rectificacion {
|
||||
/// Desplazamiento, en minutos, sobre la hora registrada, que mejor
|
||||
/// explica los eventos. `0` = la hora registrada ya es la mejor.
|
||||
pub mejor_offset_minutos: i64,
|
||||
/// Puntaje del mejor candidato: la suma de orbes de convergencia GR
|
||||
/// sobre todos los eventos. Menor = mejor; es la «tensión» residual.
|
||||
pub mejor_puntaje: f32,
|
||||
/// El barrido completo: `(offset_minutos, puntaje)` por candidato,
|
||||
/// ordenado por offset ascendente. La UI lo dibuja como una curva —
|
||||
/// su valle marca la hora rectificada.
|
||||
pub perfil: Vec<(i64, f32)>,
|
||||
}
|
||||
|
||||
/// Rectifica la hora de nacimiento por el Sistema GR. Barre las horas
|
||||
/// candidatas en `[-ventana_min, +ventana_min]` minutos sobre la
|
||||
/// registrada, paso a paso (`paso_min`); para cada candidata computa la
|
||||
/// carta y, por cada evento conocido, mide la convergencia GR más
|
||||
/// cerrada a esa edad. La hora del puntaje mínimo es la rectificada.
|
||||
///
|
||||
/// `key` es la clave arco↔año: `"naibod"` (default) o `"ptolemy"`.
|
||||
/// `Err` si la lista de eventos está vacía — sin anclas no hay búsqueda.
|
||||
#[cfg(feature = "eternal-bridge")]
|
||||
pub fn rectificar(
|
||||
chart: &Chart,
|
||||
eventos: &[EventoConocido],
|
||||
ventana_min: i64,
|
||||
paso_min: i64,
|
||||
key: &str,
|
||||
) -> Result<Rectificacion, EngineError> {
|
||||
rectify::rectificar(chart, eventos, ventana_min, paso_min, key)
|
||||
}
|
||||
|
||||
/// Composición canónica: carta natal + todos los overlays pedidos.
|
||||
/// Equivalente a `compose_with_options` con `NatalOptions::default()`.
|
||||
pub fn compose(
|
||||
@@ -535,4 +584,32 @@ mod tests {
|
||||
.any(|(a, b)| (a - b).abs() > 0.01);
|
||||
assert!(moved, "el armónico debe mover los cuerpos");
|
||||
}
|
||||
|
||||
/// El rectificador barre la ventana entera, devuelve un perfil
|
||||
/// ordenado y elige como mejor el candidato de puntaje mínimo.
|
||||
#[cfg(feature = "eternal-bridge")]
|
||||
#[test]
|
||||
fn rectificar_barre_la_ventana_y_elige_el_minimo() {
|
||||
let chart = sample_chart();
|
||||
let eventos = [
|
||||
EventoConocido { edad_years: 20.0 },
|
||||
EventoConocido { edad_years: 35.0 },
|
||||
];
|
||||
let r = rectificar(&chart, &eventos, 10, 2, "naibod").expect("rectificar");
|
||||
|
||||
// Ventana ±10 min, paso 2 → offsets -10,-8,…,10 = 11 candidatos.
|
||||
assert_eq!(r.perfil.len(), 11);
|
||||
// El perfil va ordenado por offset ascendente.
|
||||
for par in r.perfil.windows(2) {
|
||||
assert!(par[0].0 < par[1].0, "perfil desordenado");
|
||||
}
|
||||
// El mejor offset cae dentro de la ventana.
|
||||
assert!(r.mejor_offset_minutos.abs() <= 10);
|
||||
// Y su puntaje es, en efecto, el mínimo del perfil.
|
||||
let minimo = r.perfil.iter().map(|(_, p)| *p).fold(f32::INFINITY, f32::min);
|
||||
assert!((r.mejor_puntaje - minimo).abs() < 1e-4);
|
||||
|
||||
// Sin eventos no hay ancla — debe ser un error.
|
||||
assert!(rectificar(&chart, &[], 10, 2, "naibod").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user