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:
@@ -154,6 +154,40 @@ fn mark_events(triggers: &mut [GrTrigger], event_orb_deg: f32) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Orbe de la convergencia GR más cerrada de un conjunto de triggers
|
||||
/// computado a una edad dada: por cada punto natal tocado a la vez por
|
||||
/// un trigger directo y otro converso, la suma de los dos orbes; se
|
||||
/// devuelve la MENOR de esas sumas. `None` si ningún punto natal recibe
|
||||
/// ambas direcciones — no hubo convergencia.
|
||||
///
|
||||
/// Es la medida **continua** de «qué tan bien una carta explica un
|
||||
/// evento»: a diferencia del flag binario `event` (dentro o fuera del
|
||||
/// micro-orbe), esta suma decrece de forma suave a medida que la hora
|
||||
/// candidata acerca el directo y el converso al punto natal. El
|
||||
/// rectificador automático la minimiza barriendo horas de nacimiento.
|
||||
pub fn convergencia_minima(triggers: &[GrTrigger]) -> Option<f32> {
|
||||
use std::collections::HashMap;
|
||||
// Por punto natal: el mejor orbe directo y el mejor orbe converso.
|
||||
let mut por_objetivo: HashMap<&str, (Option<f32>, Option<f32>)> = HashMap::new();
|
||||
for t in triggers {
|
||||
let (directo, converso) = por_objetivo
|
||||
.entry(t.natal_target.as_str())
|
||||
.or_insert((None, None));
|
||||
let ranura = match t.direction {
|
||||
GrDirection::Direct => directo,
|
||||
GrDirection::Converse => converso,
|
||||
};
|
||||
*ranura = Some(ranura.map_or(t.orb_deg, |previo| previo.min(t.orb_deg)));
|
||||
}
|
||||
por_objetivo
|
||||
.values()
|
||||
.filter_map(|par| match par {
|
||||
(Some(directo), Some(converso)) => Some(directo + converso),
|
||||
_ => None,
|
||||
})
|
||||
.reduce(f32::min)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -242,4 +276,45 @@ mod tests {
|
||||
assert_eq!(out.len(), 1);
|
||||
assert!((out[0].orb_deg - 2.0).abs() < 1e-3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convergencia_minima_suma_directo_y_converso() {
|
||||
// Marte directo a 0.1° del Sol, Venus converso a 0.2°.
|
||||
let directed = vec![
|
||||
d("mars", GrDirection::Direct, 100.1),
|
||||
d("venus", GrDirection::Converse, 99.8),
|
||||
];
|
||||
let targets = vec![("sun".to_string(), 100.0)];
|
||||
let triggers = compute_gr_triggers(&directed, &targets, 2.0, 0.083, 60);
|
||||
let conv = convergencia_minima(&triggers).expect("hay convergencia");
|
||||
assert!((conv - 0.3).abs() < 1e-3, "0.1 directo + 0.2 converso: {conv}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convergencia_minima_none_sin_ambas_direcciones() {
|
||||
// Sólo toques directos sobre el Sol: no hay convergencia.
|
||||
let directed = vec![
|
||||
d("mars", GrDirection::Direct, 100.1),
|
||||
d("venus", GrDirection::Direct, 99.9),
|
||||
];
|
||||
let targets = vec![("sun".to_string(), 100.0)];
|
||||
let triggers = compute_gr_triggers(&directed, &targets, 2.0, 0.083, 60);
|
||||
assert!(convergencia_minima(&triggers).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convergencia_minima_elige_el_punto_natal_mas_cerrado() {
|
||||
// Convergencia floja sobre el Sol (0.5+0.5), cerrada sobre la
|
||||
// Luna (0.05+0.05): gana la de la Luna.
|
||||
let directed = vec![
|
||||
d("mars", GrDirection::Direct, 100.5),
|
||||
d("venus", GrDirection::Converse, 99.5),
|
||||
d("jupiter", GrDirection::Direct, 200.05),
|
||||
d("saturn", GrDirection::Converse, 199.95),
|
||||
];
|
||||
let targets = vec![("sun".to_string(), 100.0), ("moon".to_string(), 200.0)];
|
||||
let triggers = compute_gr_triggers(&directed, &targets, 2.0, 0.083, 60);
|
||||
let conv = convergencia_minima(&triggers).expect("hay convergencia");
|
||||
assert!((conv - 0.1).abs() < 1e-3, "gana la Luna (0.05+0.05): {conv}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ pub mod palette;
|
||||
pub use draw::{
|
||||
compose_wheel, draw_commands_to_svg, CompositionOpts, DrawCommand, Rgba, TextAnchor,
|
||||
};
|
||||
pub use gr::{compute_gr_triggers, GrDirection, GrTrigger};
|
||||
pub use gr::{compute_gr_triggers, convergencia_minima, GrDirection, GrTrigger};
|
||||
pub use harmonic::apply_harmonic;
|
||||
pub use math::{
|
||||
find_clusters, format_coord_compact, polar_to_screen, spread_angles, Radii,
|
||||
|
||||
Reference in New Issue
Block a user