0 Resumen Ejecutivo

Plataforma SaaS que automatiza la obtención, procesamiento y análisis de datos sísmicos para generar Seismic Source Models (SSM) e informes de Probabilistic Seismic Hazard Analysis (PSHA). Motor de cálculo PSHA propio en Python (integral Cornell-McGuire + librería GMPEs). Validado contra R-CRISIS y OpenQuake como benchmarks independientes. Nuestro valor: stack 100% cloud-native que transforma coordenadas en informes técnicos profesionales.

<60s
Tier 1 — SSM Geométrico
<30m
Tier 2 — SSM Completo
2-5d
Tier 3 — PSHA Completo
€0
Coste marginal Tier 1
Coordenadas EXTRACT ANALYZE VALIDATE TRANSFORM Informe PDF

1 Decisiones Cerradas

Todas las decisiones estratégicas y técnicas, con justificación.

D1 — Motor PSHA: Propio (Python/SciPy)

Motor de cálculo PSHA propio en Python. Integral Cornell-McGuire con librería GMPEs propia. R-CRISIS y OpenQuake como benchmarks de validación, no dependencias de producción.

Razón: Elimina dependencia Windows (R-CRISIS = .NET/CLI), mantiene stack 100% Python/Linux/cloud-native, escala horizontal con workers, coste infra ~$5/mes vs $120/mes con EC2 Windows. Validación triple: nuestro motor vs R-CRISIS (Carlos local) vs OpenQuake (container Linux).
D2 — Backend: Python + FastAPI

Python 3.12 con FastAPI como framework web. Todo el pipeline científico en Python.

Razón: Ecosistema científico (NumPy, SciPy, ObsPy, Shapely, GeoPandas), async nativo, tipado fuerte con Pydantic, rendimiento superior a Flask/Django para APIs.
D3 — Frontend: Next.js 15 + Mapbox GL

Next.js con App Router, TypeScript, Tailwind CSS. Mapbox GL JS para visualización geoespacial.

Razón: SSR para SEO del dashboard público, Mapbox supera a Leaflet en rendimiento con datasets grandes (miles de fallas + terremotos), integración nativa con GeoJSON.
D4 — Base de Datos: PostgreSQL + PostGIS

PostgreSQL 16 con extensión PostGIS para queries geoespaciales nativas.

Razón: ST_DWithin para búsquedas por radio, indexación GIST para fallas/terremotos, JSON nativo para metadata variable, maduro y self-hosted.
D5 — Orchestration: Temporal.io

Temporal.io para orquestación del pipeline multi-paso con retry y estado persistente.

Razón: Ya tenemos experiencia (AMYGO/Foro), perfecto para workflows largos (Tier 3 puede tardar horas), retry automático por etapa, visibilidad de estado.
D6 — AI: Claude API (Sonnet) para reports + validation

Claude Sonnet para generación de informes narrativos, quality reports y resúmenes ejecutivos. Scikit-learn para anomaly detection en datos.

Razón: Coste/calidad óptimo para generación de texto técnico. No necesitamos Opus para reportes. Anomaly detection es ML clásico, no requiere LLM.
D7 — Infra: Railway (Fase 0-2) → AWS ECS (Fase 3+)

Railway para PoC y MVP. Migrar a AWS ECS Fargate cuando necesitemos workers dedicados para cálculos PSHA pesados (Tier 3). Todo Linux, todo containerizado.

Razón: Motor PSHA propio elimina necesidad de Windows Server. Stack 100% Linux permite containers Docker estándar. ECS Fargate escala workers de cálculo bajo demanda. R2/Cloudflare para storage de PDFs.
D8 — Monorepo con Turborepo

Monorepo: /apps/api (FastAPI), /apps/web (Next.js), /packages/pipeline (core Python), /packages/shared (tipos compartidos).

Razón: Pipeline y API comparten modelos y lógica. Un solo repo simplifica CI/CD en fase temprana. Turborepo para builds incrementales del frontend.

2 Arquitectura del Sistema

6 capas, de presentación a datos externos. El pipeline de datos es el core — todo lo demás son interfaces.

Capa 1 — Presentación
Next.js Dashboard Mapbox GL JS Plotly.js Charts PDF Generator (WeasyPrint) REST API (public)
Capa 2 — AI & Intelligence
Claude API (reports) Anomaly Detection (sklearn) Tectonic Classifier Validation Engine Quality Scorer
Capa 3 — Pipeline de Datos (CORE)
EXTRACT: Multi-Source Fetcher ANALYZE: Dedup + Homogenize + Decluster + G-R Fit VALIDATE: Integrity + Stats + Cross-ref TRANSFORM: PDF + NRML + PSHA Results + GeoJSON
Capa 4 — Orchestration
Temporal.io Workflows PSHA Compute Workers Redis Cache Celery Workers (PDF gen)
Capa 5 — Data & Storage
PostgreSQL + PostGIS GAF-DB Mirror (local) USGS Cache (historical) Vs30 Raster Store S3/R2 (reports)
Capa 6 — Fuentes Externas
USGS FDSN API GEM GAF-DB (GitHub) ISC Bulletin GEM Risk Profiles USGS Vs30 SHARE/ESHM

3 Stack Tecnológico

ComponenteTecnologíaVersiónPropósito
RuntimePython3.12Backend + Pipeline
Web FrameworkFastAPI0.110+REST API async
ValidationPydantic v22.6+Input/output validation, schemas
DBPostgreSQL + PostGIS16 + 3.4Data + geospatial queries
ORMSQLAlchemy + GeoAlchemy22.0+Async ORM con soporte geo
MigrationsAlembic1.13+Schema migrations
CacheRedis7+Query cache + job queue
OrchestrationTemporal.ioPython SDKPipeline workflow orchestration
FrontendNext.js15+Dashboard SPA + SSR
MapsMapbox GL JS3.xMapas interactivos geoespaciales
ChartsPlotly.js2.xGráficos científicos interactivos
PDFWeasyPrint62+HTML → PDF profesional
Geo libsShapely, GeoPandas, PyProjlatestOperaciones geoespaciales en Python
ScienceNumPy, SciPy, ObsPylatestCálculos numéricos + sismología
MLscikit-learn1.4+Anomaly detection, clustering
AIAnthropic Claude APISonnet 4.5Report generation, QA narrativo
PSHA EngineCustom (NumPy/SciPy)Cornell-McGuire integral, GMPEs, UHS, disaggregation
AuthSupabase AuthlatestJWT + OAuth + Row Level Security
StorageCloudflare R2PDFs generados, exports

4 Fuentes de Datos Externas

Todas las fuentes son públicas, gratuitas, accesibles vía API o descarga directa. Sin intermediarios humanos.

FuenteDatosAccesoFormatoLicencia
USGS FDSN Catálogo sísmico global (~1890+). Magnitud, coords, profundidad, tipo. REST API GeoJSON, CSV Público
GEM GAF-DB Fallas activas mundiales. Geometría, dip, rake, slip rate, tipo cinemático. GitHub raw GeoJSON CC-BY-SA 4.0
ISC Bulletin Catálogo revisado alta precisión. Complementa USGS pre-1970. REST API CSV, ISF Público
USGS Vs30 Velocidad onda corte 30m. Clasificación suelo del sitio. Download GeoTIFF Público
GEM Exposure Inventario edificaciones por país (tipo, ocupación, valor). GitHub CSV, XML CC-BY
SHARE/ESHM Modelo hazard europeo, fuentes sismogénicas Europa. EFEHR portal GeoJSON, XML CC-BY

USGS FDSN API — Query de Referencia

USGS API — Manila, M≥5, 300km radius
GET https://earthquake.usgs.gov/fdsnws/event/1/query
  ?format=geojson
  &latitude=14.5995
  &longitude=120.9842
  &maxradiuskm=300
  &minmagnitude=5
  &starttime=1900-01-01
  &orderby=magnitude
  &limit=20000

GEM GAF-DB — Acceso Directo

GAF-DB GeoJSON (GitHub Raw)
https://raw.githubusercontent.com/GEMScienceTools/
  gem-global-active-faults/master/geojson/
  gem_active_faults.geojson

# Campos por feature:
# - geometry: LineString/MultiLineString (traza de falla)
# - properties.dip_dir, properties.dip
# - properties.rake, properties.slip_type
# - properties.slip_rate_min/max/pref
# - properties.net_slip_rate_min/max/pref
# - properties.fault_name, properties.catalog_id
Estrategia de cache: GAF-DB se descarga una vez al mes (actualización infrecuente). USGS se cachea por query (TTL 24h para consultas idénticas). ISC se consulta bajo demanda para períodos pre-1970.

5 Pipeline de Datos — Las 4 Fases

Input mínimo: (latitude, longitude, radius_km). Cada fase es un Activity de Temporal.io con retry independiente.

FASE 1 — EXTRACT (Multi-Source Fetcher)

Conecta con todas las fuentes simultáneamente y devuelve datos crudos normalizados.

extract/fetchers.py — Interfaz base
class BaseFetcher(ABC):
    @abstractmethod
    async def fetch(self, params: SearchParams) -> RawDataset: ...

class USGSFetcher(BaseFetcher):
    """USGS FDSN API — catálogo sísmico"""
    BASE_URL = "https://earthquake.usgs.gov/fdsnws/event/1/query"

    async def fetch(self, params: SearchParams) -> EarthquakeCatalog:
        query = {
            "format": "geojson",
            "latitude": params.latitude,
            "longitude": params.longitude,
            "maxradiuskm": params.radius_km,
            "minmagnitude": params.min_magnitude,  # default 5.0
            "starttime": params.start_date,         # default 1900-01-01
            "orderby": "magnitude",
            "limit": 20000,
        }
        async with httpx.AsyncClient() as client:
            resp = await client.get(self.BASE_URL, params=query)
        return self._parse_geojson(resp.json())

class GAFDBFetcher(BaseFetcher):
    """GEM Global Active Faults — geometría fallas"""
    async def fetch(self, params: SearchParams) -> FaultCatalog:
        # Spatial query sobre GeoJSON local (cacheado)
        point = Point(params.longitude, params.latitude)
        buffer = point.buffer(params.radius_km / 111.32)  # deg approx
        faults_gdf = gpd.read_file(self.LOCAL_GEOJSON_PATH)
        nearby = faults_gdf[faults_gdf.intersects(buffer)]
        return self._normalize(nearby)

class ISCFetcher(BaseFetcher):
    """ISC Bulletin — catálogo complementario pre-1970"""
    ...

class Vs30Fetcher(BaseFetcher):
    """USGS Vs30 raster — site classification"""
    ...

El extractor ejecuta todos los fetchers en paralelo con asyncio.gather(). Si un fetcher falla, no bloquea el resto — se marca como degradado en el quality report.

FASE 2 — ANALYZE (Procesador Inteligente)

Cada paso es una función pura que transforma datos. Orden estricto.

#PasoInputOutputLibrería
2.1DeduplicaciónUSGS + ISC catalogsCatálogo unificado (prioriza magnitud revisada ISC)Pandas
2.2Homogeneización MwCatálogo con mb, Ms, MlTodas las magnitudes convertidas a MwObsPy / relaciones empíricas
2.3DeclusteringCatálogo completoCatálogo mainshock-only (sin réplicas/premonitorios)Gardner-Knopoff (NumPy)
2.4CompletitudCatálogo declusteredMc (magnitud completitud) + períodos completosMétodo Stepp (SciPy)
2.5Separación Falla/BGCatálogo + geometría fallasEventos asignados a falla (≤5km) vs backgroundShapely + GeoPandas
2.6Gutenberg-RichterEventos por zonaParámetros a, b, λ, β con CI 95%SciPy (least squares fit)
analyze/gutenberg_richter.py
@dataclass
class GRParameters:
    a_value: float          # Intersección
    b_value: float          # Pendiente (típico 0.7-1.3)
    lambda_m0: float        # Tasa anual M≥M0
    beta: float             # b * ln(10)
    r_squared: float        # Calidad del ajuste
    n_events: int           # Eventos usados
    mc: float               # Magnitud completitud
    confidence_95: tuple    # (b_lower, b_upper)

def fit_gutenberg_richter(
    magnitudes: np.ndarray,
    mc: float,
    bin_width: float = 0.1
) -> GRParameters:
    """Ajuste G-R por mínimos cuadrados sobre distribución acumulada."""
    mags = magnitudes[magnitudes >= mc]
    bins = np.arange(mc, mags.max() + bin_width, bin_width)
    cum_rates = np.array([np.sum(mags >= m) for m in bins])
    # Log10(N) = a - b*M
    log_rates = np.log10(cum_rates[cum_rates > 0])
    slope, intercept, r, _, stderr = linregress(
        bins[:len(log_rates)], log_rates
    )
    return GRParameters(
        a_value=intercept, b_value=-slope,
        lambda_m0=10**intercept, beta=-slope * np.log(10),
        r_squared=r**2, n_events=len(mags), mc=mc,
        confidence_95=(-slope - 1.96*stderr, -slope + 1.96*stderr)
    )

FASE 3 — VALIDATE (Quality Assurance)

Tres niveles de validación. Cada check produce un score parcial que se agrega en quality score global.

NivelCheckCriterio PasaFlag
Data IntegrityCoordenadas válidaslat ∈ [-90,90], lon ∈ [-180,180]🟢/🔴
Magnitudes coherentesMw ∈ [0, 10]🟢/🔴
Profundidades razonablesdepth ∈ [0, 700] km🟢/🟡
StatisticalValor b en rangob ∈ [0.5, 1.8]🟢/🟡/🔴
R² de G-RR² ≥ 0.90🟢/🟡
Eventos suficientesN ≥ 30 (post Mc)🟢/🟡/🔴
Cross-referenceCoherencia con GEM Risk ProfilePGA dentro de ±30% del perfil país🟢/🟡
Consistencia fallas vs sismicidadFallas activas con sismicidad asociada🟢/🟡
validate/quality_scorer.py
class QualityScore(BaseModel):
    overall: Literal["HIGH", "MEDIUM", "LOW"]  # 🟢🟡🔴
    score: float  # 0-100
    checks: list[CheckResult]
    flags: list[str]  # Warnings para el usuario
    ai_summary: str   # Narrativa generada por Claude

    @property
    def emoji(self) -> str:
        return {"HIGH": "🟢", "MEDIUM": "🟡", "LOW": "🔴"}[self.overall]

FASE 4 — TRANSFORM (Output Generator)

Convierte resultados validados a formatos de entrega según el Tier contratado.

OutputFormatoTierGenerador
Mapa interactivoMapbox GL (web)1, 2, 3Frontend render
Informe PDF básicoPDF (A4)1WeasyPrint
Informe PDF técnicoPDF (A4, 30-50 pp)2, 3WeasyPrint + Claude
PSHA Results PackageJSON + CSV3PSHA Engine output
NRML XML (OpenQuake).xml2, 3NRMLSerializer
GIS PackageGeoJSON + Shapefile2, 3GeoPandas export
Hazard CurvesCSV + Plot3PSHA Engine (propio)
UHS (Uniform Hazard Spectra)CSV + Plot3PSHA Engine (propio)
Resumen ejecutivo AIMarkdown → PDF2, 3Claude API

6 Modelo de Datos

PostgreSQL con PostGIS. Tablas principales del dominio sísmico + gestión de proyectos.

projects
idUUIDPK, auto-generated
user_idUUIDFK → auth.users
nameVARCHAR(255)Nombre del proyecto (ej: "Manila PSHA 2026")
latitudeFLOATCentro de búsqueda
longitudeFLOATCentro de búsqueda
radius_kmFLOATRadio de búsqueda (default 300km)
centerGEOMETRY(Point,4326)PostGIS point para spatial queries
tierENUM(1,2,3)Nivel de producto contratado
statusENUMpending | extracting | analyzing | validating | transforming | review | completed | failed
quality_scoreJSONBResultado del QA (overall, score, checks, flags)
configJSONBParámetros: min_magnitude, start_date, gmpes[], Mc override, etc.
temporal_workflow_idVARCHARID del workflow en Temporal.io
created_at / updated_atTIMESTAMPTZTimestamps
earthquakes (catálogo por proyecto)
idUUIDPK
project_idUUIDFK → projects
sourceENUMusgs | isc | merged
external_idVARCHARID en fuente original (ej: us7000abc)
locationGEOMETRY(Point,4326)Epicentro PostGIS
depth_kmFLOATProfundidad hipocentral
mag_originalFLOATMagnitud reportada original
mag_type_originalVARCHARTipo original (mb, Ms, Ml, Mw)
mag_mwFLOATMagnitud homogeneizada a Mw
event_timeTIMESTAMPTZFecha/hora del evento
is_mainshockBOOLEANTrue tras declustering
assigned_fault_idUUID NULLFK → faults (NULL = background)
cluster_idINTEGER NULLID de cluster (pre-declustering)
faults (fallas activas por proyecto)
idUUIDPK
project_idUUIDFK → projects
gafdb_idVARCHARID original en GAF-DB
nameVARCHARNombre de la falla
geometryGEOMETRY(Multi LineString,4326)Traza de la falla
dipFLOATÁngulo de buzamiento (°)
rakeFLOATÁngulo de deslizamiento (°)
slip_typeVARCHARNormal, Reverse, Strike-Slip, Oblique
slip_rate_mm_yrFLOATTasa de deslizamiento preferida (mm/año)
length_kmFLOATLongitud total calculada
max_magnitudeFLOAT NULLMmax estimada (Wells & Coppersmith)
gr_paramsJSONB NULL{a, b, lambda, beta, mc, r2} — Tier 2+
seismic_zones (zonas sismogénicas — Tier 2+)
idUUIDPK
project_idUUIDFK → projects
nameVARCHARNombre de la zona (ej: "Manila Trench Subduction")
zone_typeENUMfault | area | background
geometryGEOMETRY(Polygon,4326)Polígono de la zona
gr_paramsJSONBParámetros G-R de la zona
depth_distributionJSONB{min, max, mean} km
tectonic_regimeVARCHARSubduction, Crustal, Stable Continental
reports (outputs generados)
idUUIDPK
project_idUUIDFK → projects
report_typeENUMpdf_basic | pdf_technical | nrml | psha_results | geojson | hazard_curves
storage_urlVARCHARURL en R2/S3
file_size_bytesBIGINTTamaño del archivo
metadataJSONBMetadata del informe (páginas, secciones, etc.)
created_atTIMESTAMPTZFecha generación
Índices PostGIS: CREATE INDEX idx_earthquakes_location ON earthquakes USING GIST(location); — Permite ST_DWithin(location, center, radius) en <50ms para 100K+ eventos.

7 API REST

FastAPI con autenticación JWT (Supabase). Todos los endpoints retornan JSON. Paginación cursor-based.

Proyectos

POST /api/v1/projects
Crear nuevo proyecto. Inicia el pipeline automáticamente según tier. Body: {name, latitude, longitude, radius_km, tier, config?}
GET /api/v1/projects/{id}
Detalle del proyecto con status del pipeline, quality score, contadores de fallas/terremotos.
GET /api/v1/projects/{id}/status
Status real-time del workflow Temporal. Incluye fase actual, progreso %, errores parciales. Soporta SSE para updates en vivo.

Datos Sísmicos

GET /api/v1/projects/{id}/earthquakes
Catálogo sísmico del proyecto. Filtros: ?min_mag=&max_mag=&mainshock_only=true&assigned_fault=. Formato: GeoJSON FeatureCollection.
GET /api/v1/projects/{id}/faults
Fallas activas del proyecto con propiedades. Formato: GeoJSON FeatureCollection.
GET /api/v1/projects/{id}/zones
Zonas sismogénicas con parámetros G-R (Tier 2+). Formato: GeoJSON.

Análisis & Reportes

GET /api/v1/projects/{id}/analysis
Resultados del análisis: G-R params, completitud, declustering stats, quality score con AI summary.
GET /api/v1/projects/{id}/reports
Lista de reportes generados con download URLs (signed, expiran en 1h).
POST /api/v1/projects/{id}/reports/generate
Regenerar reportes. Body: {types: ["pdf_technical", "nrml", "hazard_curves"]}

Quick Analysis (sin proyecto)

GET /api/v1/quick/preview
Preview rápido sin guardar. Params: ?lat=&lon=&radius=300. Retorna: conteo de fallas/terremotos, mapa preview, estimación de calidad. Para el landing page.

8 Motor PSHA Propio

Implementación Python de la integral de Cornell-McGuire con librería de GMPEs, logic trees, disaggregación y UHS. Validado contra R-CRISIS (Carlos) + OpenQuake (container).

Stack 100% Python, 100% Linux, 100% cloud-native

Sin dependencias Windows, sin CLI externo, sin formatos propietarios. El Tier 3 es simplemente más pasos del mismo pipeline Python.

Ventajas: containerizable, escala horizontal con workers, debuggeable, testeable con pytest, coste infra ~$5/mes vs $120/mes con EC2 Windows.

8.1 — Ecuación Core (Cornell-McGuire)

psha/hazard_calculator.py
"""
PSHA Hazard Calculator — Integral de Cornell-McGuire

λ(a) = Σ_sources ∫∫ P(A>a|m,r) × f(m) × f(r) dm dr

Donde:
  λ(a)       = tasa anual de excedencia del nivel de aceleración 'a'
  P(A>a|m,r) = probabilidad de excedencia dada magnitud m y distancia r (GMPE)
  f(m)       = PDF de magnitudes (Gutenberg-Richter truncada)
  f(r)       = PDF de distancias (geometría fuente-sitio)
"""

@dataclass
class HazardCurveResult:
    imls: np.ndarray          # Intensity Measure Levels (g)
    poes: np.ndarray          # Probabilities of Exceedance (annual)
    return_periods: np.ndarray # 1/poes (años)
    source_contributions: dict # {source_name: poe_array}

class HazardCalculator:
    def __init__(self, gmpe_registry: GMPERegistry):
        self.gmpe_registry = gmpe_registry

    def compute_hazard_curve(
        self,
        site: Site,                    # lat, lon, vs30
        sources: list[SeismicSource],  # faults + area sources
        imls: np.ndarray,              # acceleration levels to evaluate
        gmpe_logic_tree: LogicTree,    # weighted GMPE combinations
        truncation: float = 3.0,       # sigma truncation
        integration_step_m: float = 0.1,
        integration_step_r: float = 1.0,  # km
    ) -> HazardCurveResult:
        """Calcula hazard curve para un sitio."""
        total_poe = np.zeros(len(imls))
        contributions = {}

        for source in sources:
            source_poe = np.zeros(len(imls))
            mag_range = np.arange(source.mmin, source.mmax, integration_step_m)
            dist_range = source.distance_distribution(site, step=integration_step_r)

            for m in mag_range:
                rate_m = source.occurrence_rate(m, integration_step_m)  # G-R
                for r, weight_r in dist_range:
                    # Weighted sum over GMPE logic tree
                    for gmpe, weight_gmpe in gmpe_logic_tree.branches:
                        mean_ln, sigma_ln = gmpe.get_mean_and_sigma(
                            m, r, site.vs30, source.tectonic_type
                        )
                        for iml_idx, iml in enumerate(imls):
                            ln_iml = np.log(iml)
                            # P(A > a | m, r) via complementary normal CDF
                            epsilon = (ln_iml - mean_ln) / sigma_ln
                            epsilon = min(epsilon, truncation)
                            p_exceed = 1 - norm.cdf(epsilon)
                            source_poe[iml_idx] += (
                                rate_m * weight_r * weight_gmpe * p_exceed
                            )

            contributions[source.name] = source_poe
            total_poe += source_poe

        return HazardCurveResult(
            imls=imls,
            poes=1 - np.exp(-total_poe),  # Poisson probability
            return_periods=1 / total_poe,
            source_contributions=contributions,
        )

8.2 — Librería de GMPEs

Las 15 GMPEs que cubren >95% de los casos globales. Cada una es una función Python con coeficientes publicados en papers peer-reviewed.

GMPERégimenRegiónReferenciaEst.
Boore et al. (2014)Crustal activeGlobalNGA-West2~150 líneas
Abrahamson et al. (2014)Crustal activeGlobalNGA-West2~200 líneas
Campbell & Bozorgnia (2014)Crustal activeGlobalNGA-West2~200 líneas
Chiou & Youngs (2014)Crustal activeGlobalNGA-West2~200 líneas
Zhao et al. (2006)SubductionJapón / globalBSSA~120 líneas
Atkinson & Boore (2003)SubductionGlobalBSSA~100 líneas
Youngs et al. (1997)SubductionGlobalSRL~80 líneas
Akkar & Bommer (2010)Crustal activeEuropa / Med.SRL~100 líneas
Cauzzi et al. (2015)Crustal activeEuropaBSSA~120 líneas
Atkinson & Boore (2006)Stable continentalENABSSA~100 líneas
García et al. (2005)Subduction inslabMéxico / LATAMBSSA~80 líneas
Kanno et al. (2006)Crustal + subductionJapónBSSA~100 líneas
Total estimado~1,600 líneas
psha/gmpes/base.py — Interfaz GMPE
class GMPE(ABC):
    """Interfaz base para Ground Motion Prediction Equations."""

    name: str
    tectonic_types: list[str]  # ["crustal_active", "subduction_interface", ...]
    supported_imts: list[str]  # ["PGA", "SA(0.1)", "SA(0.2)", ...]

    @abstractmethod
    def get_mean_and_sigma(
        self,
        magnitude: float,
        distance: float,         # Rjb, Rrup, o Rhypo según GMPE
        vs30: float,
        imt: str = "PGA",
        **kwargs,                # Parámetros adicionales (Ztor, dip, etc.)
    ) -> tuple[float, float]:   # (mean_ln_acceleration, sigma_ln)
        """Retorna media y desviación estándar en log-natural."""
        ...

class BooreEtAl2014(GMPE):
    """Boore, Stewart, Seyhan & Atkinson (2014) — NGA-West2"""
    name = "BooreEtAl2014"
    tectonic_types = ["crustal_active"]

    # Coeficientes tabla publicada (período-dependiente)
    COEFFS = {
        "PGA":    {"e0": 0.4473, "e1": 0.4856, "e2": 0.2459, ...},
        "SA(0.1)": {"e0": 0.9445, "e1": 1.0145, "e2": 0.4328, ...},
        ...
    }

    def get_mean_and_sigma(self, magnitude, distance, vs30, imt="PGA", **kw):
        c = self.COEFFS[imt]
        # Implementación directa de ecuación del paper
        f_mag = c["e0"] + c["e1"] * (magnitude - 4.5) + ...
        f_dist = (c["c1"] + c["c2"] * (magnitude - 4.5)) * np.log(...)
        f_site = c["c"] * np.log(vs30 / 760) + ...
        mean_ln = f_mag + f_dist + f_site
        sigma_ln = np.sqrt(c["tau"]**2 + c["phi"]**2)
        return mean_ln, sigma_ln

8.3 — Componentes Adicionales Tier 3

ComponenteDescripciónEstimación
Logic TreeCombinación ponderada de GMPEs + modelos de fuentes. Captura incertidumbre epistémica.2-3 días
UHS CalculatorUniform Hazard Spectra: hazard curves para múltiples períodos → interpolar a probabilidad fija.1-2 días
DisaggregationDescomposición contribución M-R-ε. Mismo cálculo pero guardando contribuciones parciales.1 semana
Hazard MapsGrid de sitios → hazard curve por punto → interpolación espacial → raster/contornos.1 semana
Distance CalculatorRjb, Rrup, Rhypo, Rx desde geometría 3D de fallas. Crítico para GMPEs.3-4 días

8.4 — Estrategia de Validación

Triple validación: Motor propio vs R-CRISIS vs OpenQuake

Tolerancia ≤5% en PGA y SA para 3 zonas piloto. Carlos genera ground truth con R-CRISIS. OpenQuake como tercer benchmark independiente en container Linux.

Zona PilotoRégimenValidación cubre
Manila, FilipinasSubducción activaGMPEs subducción (Zhao, Atkinson-Boore), fallas thrust, catálogo denso
Lima, PerúSubducción + crustalMix de fuentes, inslab events, GMPEs LATAM (García)
Madrid, EspañaModerada / estableGMPEs Europa (Akkar-Bommer, Cauzzi), catálogo escaso, background-dominated
Carlos ejecuta R-CRISIS local Ground truth (hazard curves) Nuestro motor Python Compare ≤5% OpenQuake container Triple match ✓
De Carlos necesitamos (una vez): Ejecutar PSHA en R-CRISIS para las 3 zonas piloto con los mismos inputs que usará nuestro pipeline. Entregable: hazard curves en CSV (PGA + SA a 0.1, 0.2, 0.5, 1.0, 2.0s). Esto es su benchmark, no una dependencia operativa.

9 AI & LLM Integration

Usos de Claude API

FunciónModeloInputOutputTier
Quality ReportSonnetValidation checks + statsNarrativa de confianza en lenguaje natural1, 2, 3
Resumen EjecutivoSonnetSSM completo + G-R params1-2 páginas resumen para no-técnicos2, 3
Contexto TectónicoSonnetCoordenadas + datos extraídosDescripción del régimen tectónico de la zona1, 2, 3
RecomendacionesSonnetQuality score + gapsAcciones sugeridas si calidad baja2, 3
Parámetro AdvisorSonnetCoordenadas + contextoSugerencia de radio, Mc, GMPEs por regiónInterno
ai/report_writer.py — Prompt template
QUALITY_REPORT_PROMPT = """
Eres un experto en sismología. Analiza los resultados de un Seismic Source Model
y genera un Quality Report en español profesional.

## Datos del Proyecto
- Ubicación: {location_name} ({lat}, {lon})
- Radio: {radius_km} km
- Terremotos encontrados: {n_earthquakes} (M≥{min_mag})
- Fallas activas: {n_faults}
- Mc (magnitud completitud): {mc}

## Resultados del Análisis
- Valor b (Gutenberg-Richter): {b_value} ± {b_uncertainty}
- R² del ajuste: {r_squared}
- Eventos post-declustering: {n_mainshocks}
- Eventos asignados a fallas: {n_fault_events} ({pct_fault}%)
- Eventos background: {n_bg_events} ({pct_bg}%)

## Validation Checks
{validation_results}

Genera:
1. Valoración general de la calidad (HIGH/MEDIUM/LOW con justificación)
2. Principales fortalezas de los datos
3. Gaps o limitaciones identificadas
4. Recomendaciones si la calidad es MEDIUM o LOW
Máximo 400 palabras. Tono técnico pero accesible.
"""

ML Clásico (sin LLM)

Scikit-learn para tareas que no requieren generación de texto:

TareaAlgoritmoPropósito
Anomaly DetectionIsolation ForestDetectar eventos con magnitud/profundidad inconsistente
Cluster AnalysisDBSCANIdentificar swarms sísmicos (complementa declustering)
Tectonic ClassificationRandom ForestClasificar régimen tectónico por features geográficas

10 Productos — Definición de Tiers

Tier 1 — SSM Geométrico
€2K - 5K
100% automatizado
  • Mapa fallas activas en radio
  • Catálogo sísmico M≥5 filtrado
  • Tabla propiedades por falla
  • Histograma de magnitudes
  • Mapa interactivo web
  • Quality score automático
  • Contexto tectónico AI
  • PDF informe básico (8-12 pp)
Tiempo: <60 segundos
Intervención humana: Ninguna
Coste marginal: ~€0
Tier 3 — PSHA Completo
€15K - 25K+
Pipeline + Motor PSHA propio
  • Todo de Tier 2
  • PSHA completo (motor propio Python)
  • Hazard curves + hazard maps
  • Uniform Hazard Spectra (UHS)
  • Disaggregación M-R-ε
  • Logic tree multi-GMPE
  • Análisis de sensibilidad
  • PML para aseguradoras
  • Review técnico Carlos (validación)
Tiempo: 1-3 días
Review: Carlos valida
Coste: ~$5 compute + Carlos

11 Infraestructura & Deploy

Fase 0-2 (PoC → MVP)

ServicioProveedorPlanCoste/mes
Backend (FastAPI)RailwayPro~$20
PostgreSQL + PostGISRailway (addon)Managed~$15
RedisRailway (addon)Managed~$10
Frontend (Next.js)VercelPro~$20
Temporal.ioTemporal CloudFree tier (25K actions)$0
Storage (PDFs)Cloudflare R2Pay-per-use~$5
AuthSupabaseFree tier$0
AIAnthropicAPI pay-per-use~$30
MapsMapboxFree tier (50K loads)$0
Total Fase 0-2~$100/mes

Fase 3+ (Producción con motor PSHA propio)

ServicioProveedorConfigCoste/mes
PSHA WorkersAWS ECS Fargate1 vCPU, 2GB × auto-scale~$15 (estimado 40h/mes uso)
Backend APIAWS ECS Fargate0.5 vCPU, 1GB~$30
RDS PostgreSQLAWS RDSdb.t3.medium + PostGIS~$50
Resto igual~$65
Total Fase 3+~$160/mes
Ahorro vs arquitectura R-CRISIS: $160/mes vs $265/mes anteriores. Sin instancia Windows on-demand. Los PSHA workers son containers Linux estándar que escalan con Fargate — si hay 5 Tier 3 simultáneos, se levantan 5 containers, y se destruyen al terminar. A €15K-25K por Tier 3, el coste es irrelevante.

12 Roadmap de Implementación

Semanas 1-3 — AHORA
Fase 0 — PoC "Extractor Inteligente"
Script Python: (lat, lon, radius) → USGS API + GAF-DB parse. Mapa Folium con ambas capas. Zona piloto: Manila. Entregable: Jupyter Notebook funcional. Demo para Carlos: "30 segundos vs horas".
Semanas 4-8
Fase 1 — Pipeline Tier 1 Completo
Backend FastAPI con endpoints. Módulos: USGS Fetcher, GAF-DB Fetcher, Data Merger. PDF generator con mapa + tablas. Frontend: input → mapa → PDF. Quality scoring + resumen AI. Validar contra benchmark Carlos.
Semanas 9-14
Fase 2 — Pipeline Tier 2
Declustering, homogeneización, completitud, separación falla/BG, regresión G-R. Export NRML. Temporal.io orchestration. Resumen ejecutivo AI. PDF técnico completo.
Semanas 15-22
Fase 3 — SaaS + Tier 3
Motor PSHA propio: GMPEs (15), hazard calculator, UHS, disaggregación. Validación triple (vs R-CRISIS Carlos + OpenQuake). Dashboard con auth + pagos. Multi-geografía. API pública. Primer cliente piloto.

Criterio de Éxito — PoC (Semana 3)

Input: (14.5995, 120.9842, 300) (Manila). En <60 segundos produce:

#OutputValidación
1Mapa interactivo con fallas + terremotos M≥5Visual: datos correctos para zona
2Tabla de fallas con dip, rake, slip rate≥5 fallas identificadas (Manila Trench, Philippine Fault...)
3Catálogo sísmico filtrado y deduplicado≥200 eventos M≥5 en radio 300km
4Histograma de magnitudes + timelineDistribución G-R visible
5Quality score automáticoCarlos dice: "datos correctos para empezar"

13 Riesgos & Mitigaciones

RiesgoImpactoProb.Mitigación
Precisión del pipeline
Declustering o separación falla/BG difiere de Carlos
Alto Media Benchmark riguroso contra ground truth de Carlos. 3 zonas piloto (Manila, Lima, Madrid). Parámetros configurables por override manual.
Precisión GMPEs propias
Implementación de coeficientes difiere de papers publicados
Alto Media Validación unitaria GMPE por GMPE contra tablas publicadas en papers. Triple benchmark (nuestro motor vs R-CRISIS Carlos vs OpenQuake). Test suite con valores de referencia conocidos. Tolerancia ≤5%.
APIs externas cambian
USGS cambia API, GEM reestructura repos
Medio Baja Cache local de datos. Versionado de datasets. Fetchers con interfaz abstracta — swap sin cambiar pipeline.
Complejidad motor PSHA
Tiempo de desarrollo o edge cases no previstos
Medio Media Empezar con 5 GMPEs core (NGA-West2), expandir incrementalmente. OpenQuake como fallback temporal si el motor propio no está listo. Fase 3 tiene 8 semanas de buffer.
Calidad datos regionales
Zonas con poco catálogo histórico
Medio Media Quality scorer detecta automáticamente (N < 30 → flag). AI genera advertencia. Tier 1 sigue siendo útil incluso con datos limitados.
Disponibilidad Carlos
Dependency en un solo domain expert
Medio Baja Documentar toda su metodología en Fase 0. Calibrar pipeline para que Tier 1-2 funcionen sin intervención. Tier 3 requiere expert — buscar segundo advisor a medio plazo.

Próximos Pasos Inmediatos

Esta semana (Appgile)

#TareaOwnerHoras est.
1Setup monorepo (Turborepo + FastAPI + Next.js skeleton)Dev4h
2Implementar USGSFetcher — query API + parse GeoJSONDev6h
3Implementar GAFDBFetcher — clone repo + spatial queryDev6h
4Mapa Folium: overlay fallas + terremotos para ManilaDev4h
5Jupyter Notebook PoC demo completoDev4h

Próxima reunión con Carlos

#AgendaDuración
1Demo PoC Manila — validar que datos extraídos son correctos30 min
2Sesión metodología: ¿cómo decide radio, Mc, GMPEs por región?2-3h
3Benchmark: Carlos ejecuta Manila + Lima + Madrid en R-CRISIS → ground truth CSVTarea para Carlos
4Validar lista de GMPEs priorizadas para motor propio30 min
5NDA15 min