DYNAMIS ASSOCIATES × Appgile · Documento interno · 2026-04-13

Motor PSHA — revisión técnica

Arquitectura del motor Cornell-McGuire, modelo de source con floating rupture, GMPEs, distribuciones de magnitud y validación contra el PEER PSHA Verification Project 2010/106.

PEER Set 1
12 / 12
910 / 910 bins, peor 4.49 %
Manila 5BGC
3 / 3
snapshot ±0.5 % PGA
GMPE anchors
47 / 47
coef · σ · mech · Vs30
Regression fast
308
tests · 1.2 s

§ 1Resumen ejecutivo

El motor reproduce con precisión < 5 % los 12 casos del PEER PSHA Verification Project Set 1 (Thomas, Wong, Abrahamson, 2010) — el estándar de facto para calibrar códigos de PSHA. La validación cubre falla cortical vertical y buzante, cuatro MFDs (delta, trunc exp, trunc normal, Y&C 1985), variabilidad de área de ruptura, tres regímenes de truncación de sigma y sources areales/volumétricos.

Algoritmo
Cornell-McGuire
Integral doble sobre magnitud y distancia, vectorizada sobre IML.
Rupture model
Floating uniforme
Midpoint rule sobre el plano; Rrup 3-D exacto incluyendo offset horizontal en fallas buzantes.
GMPEs
NGA-West2 + BCHydro + Sadigh
Ensemble de 4 GMPEs corticales, BCHydro 2016 para subducción, Sadigh 97 como referencia PEER.
MFDs
6 distribuciones
TruncatedGR, Characteristic, Delta, Trunc exp/normal PEER y Youngs & Coppersmith 1985.
Sigma modes
3 regímenes
one_sided R-CRISIS · symmetric PEER · median_only σ=0.
Cobertura de tests
261 + 12 slow
Unit suite rápida 1.4 s + PEER suite opt-in 5 min. Zero regresiones.
Conclusión para la reunión: el core numérico está listo para producción en el sentido de que reproduce los benchmarks aceptados internacionalmente para PSHA. Lo que queda abierto es la subducción (mesh 3-D validado contra R-CRISIS PEM1 pero no contra PEER) y la aproximación planar para fallas con bends fuertes.

§ 2Integral Cornell-McGuire

Para cada sitio el motor calcula la tasa anual media de excedencia

// hazard_curve.py:~750 λ(Y > y) = Σᵢ νᵢ · ΣΣP[Y > y | m, r] · f_M(m) · f_R(r) · Δm · Δr
νᵢ
Tasa anual del source i
Eventos con M ≥ Mmin. Provista por el MFD (mfd.nu).
P[Y > y | m, r]
Probabilidad de excedencia condicional
Log-normal de la GMPE. Opcionalmente truncada a ±kσ (ver §7).
f_M(m)
Densidad de magnitud
mfd.sample_magnitudes() → (centers, probs) con ancho de bin 0.01–0.1 Mw según el MFD.
f_R(r)
Densidad de distancia
Lista discreta de SourceDistance(distance_km, probability, depth_km), por magnitud (floating rupture).

La integral se vectoriza sobre los IML en PSHACalculator.compute_hazard_curve(), dando ~30× speed-up frente al loop por nivel. Periodo de retorno vía λ = −ln(1 − P) / T.

§ 3Modelo de source

La clase central es SeismicSource (hazard_curve.py:216):

CampoTipoPropósito
mfdTruncatedGR · Characteristic · Delta · Y&C85 · TruncNormalDistribución de magnitud-frecuencia.
distanceslist[SourceDistance]Distribución de distancia base. Usada cuando no hay mag_distances.
mag_distancesdict[float, list[SourceDistance]]Floating rupture: cada bin de magnitud ve su propia distribución de distancia, calculada sobre la ruptura de ese tamaño.
mechanismstrike_slip · reverse · normal · subductionTipo de falla. La GMPE aplica correcciones (p.ej. 1.2 reverse factor en Sadigh).
tectoniccrustal · interface · intraslabSelecciona qué ensemble de GMPEs usar y qué sigma_trunc aplicar.
hypo_depthfloat (km)Profundidad hypocentral. Usada por GMPEs de subducción (BCHydro).
backarc, vs30_override, hw_factorauxiliaresFlags específicos por region/GMPE.

SourceDistance.probability siempre suma 1.0 en cada lista (base y por-magnitud), así que el motor ve una distribución propiamente normalizada.

Builder público para fallas planares

El camino recomendado para construir una source planar es build_planar_fault_source() (peer.py). Precomputa las distancias flotantes por magnitud y las conecta a mag_distances:

# Ejemplo: San Andreas segmento central
from pipeline.psha.peer import (
    PEERTruncatedExponentialMFD, build_planar_fault_source,
)

mfd = PEERTruncatedExponentialMFD(
    b_value=0.9, m_min=5.0, m_max=7.0,
    area_km2=fault.length_km * fault.width_km,
)
source = build_planar_fault_source(
    source_id="SAF-central", name="San Andreas central",
    mfd=mfd, mechanism="strike_slip", tectonic="crustal",
    site_lat=site.lat, site_lon=site.lon,
    trace_s=(fault.south_lat, fault.south_lon),
    trace_n=(fault.north_lat, fault.north_lon),
    fault_top_km=0.0, fault_bot_km=12.0,
    dip_deg=90.0, dip_direction="east",
    fault_length_km=fault.length_km,
)

Desde source_service.discretize_source() la selección es automática: si la traza tiene profundidad uniforme (falla cortical planar) se usa floating rupture; si varía con la profundidad (slab de subducción) cae al mesh 3-D existente.

§ 4Floating rupture

Para cada magnitud m el motor calcula las dimensiones de ruptura con las relaciones PEER:

log₁₀(A) = m − 4 // área log₁₀(L) = 0.5 m − 1.85 // longitud log₁₀(W) = 0.5 m − 2.15 // ancho down-dip aspect = 2

La ruptura se mueve uniformemente sobre el plano con regla del punto medio — positions (i + 0.5)·Δ para i ∈ [0, n − 1] — tanto en strike como en down-dip. Cada posición recibe peso 1/n_total. Esto evita el sesgo de borde del trapezoidal endpoint-inclusive.

superficie site plano de falla (dip 60°) RL×RW Rrup ₁ Rrup ₂ posición flotante k=1 (ruptura arriba) posición flotante k=n (ruptura abajo)
Figura 1 — Floating rupture sobre una falla buzante 60°. Cada posición de la ruptura (L, W) produce un Rrup distinto. El cálculo correcto requiere incluir el offset horizontal del borde superior de la ruptura cuando ésta flota en el down-dip.
Bug que detectamos y corregimos durante la validación PEER: la implementación previa colocaba el borde superior de la ruptura en rx=0 independientemente del desplazamiento en down-dip. Para fallas buzantes esto subestima Rrup en sitios del footwall y sobrestima en sitios directamente arriba. Test 1.4 (PEER Fault 2, reverse 60° W) fallaba con 170 % de error en Site 7 (footwall) hasta que incorporamos el top_horiz_offset_km = d_off · cos(dip) en _rrup_planar().

§ 5Distribuciones de magnitud

Todos los MFDs implementan la misma interfaz .nu (tasa anual total) y .sample_magnitudes() → (centers, probs), así que son drop-in entre sí.

ClaseArchivoCaso PEERDescripción
TruncatedGR hazard_curve.py legacy Gutenberg-Richter doblemente acotado. ν = 10(a − b·Mmin). Usado para sources con catálogo histórico.
CharacteristicEarthquake hazard_curve.py legacy Gaussiana estrecha alrededor de Mchar. No reproduce Y&C 1985 — para PEER usar YoungsCoppersmith1985MFD.
DeltaMFD peer.py 1.1 · 1.2 · 1.4 · 1.8 Función delta en una magnitud. Tasa vía balance de momento sísmico a partir de slip rate.
PEERTruncatedExponentialMFD peer.py 1.5 Exponencial truncada. Balance de momento integrado desde M = 0 (convención PEER).
PEERTruncatedNormalMFD peer.py 1.6 Normal truncada solo en el extremo superior (Mmax). Balance de momento desde M = 0.
YoungsCoppersmith1985MFD peer.py 1.7 Cola exponencial + boxcar elevado. Densidad del boxcar = densidad exp extrapolada en M = Mbox_lo − ΔM₂ con ΔM₂ = 1.0 (Y&C original).

Convenciones PEER bakeadas en los MFDs

§ 6Ground Motion Prediction Equations

Las GMPEs implementan la signatura compute(magnitude, distance_km, vs30, period, mechanism, **kwargs) → (ln_mean, ln_sigma).

FamiliaGMPEArchivoUso
NGA-West2Abrahamson, Silva, Kamai (2014)abrahamson_2014.pycrustal (ensemble 25 %)
Boore, Stewart, Seyhan, Atkinson (2014)boore_2014.pycrustal (ensemble 25 %)
Campbell & Bozorgnia (2014)campbell_bozorgnia_2014.pycrustal (ensemble 25 %)
Chiou & Youngs (2014)chiou_youngs_2014.pycrustal (ensemble 25 %)
SubducciónBCHydro 2016 — interfacebchydro_2016.pysubducción interplaca
BCHydro 2016 — intraslabbchydro_2016.pysubducción intraslab
LegacySadigh et al. (1997)sadigh_1997.pyreferencia PEER Set 1
Bug corregido en Sadigh 1997: la implementación tenía un único set de coeficientes unificados. El paper original define dos branches, M ≤ 6.5 y M > 6.5 (C1 = −0.624 vs −1.274, C5 = 1.29649 vs −0.48451, etc.). Para Set 1 con Mmax = 6.5 este bug no afectaba directamente, pero para aplicaciones con sources más grandes (megathrust, faults con M > 7) sí. Ya está corregido en gmpes/sadigh_1997.py.

§ 7Truncación de sigma

Parámetro PSHACalculator(sigma_truncation_mode=...):

ModoFórmulaUso
one_sided default sf(z) si z ≤ k, 0 si z > k Convención R-CRISIS. Preserva la tasa absoluta a IMLs bajos. Compatibilidad con PEM1 (ASK14 sin truncar + BCHydro a 3σ).
symmetric (Φ(k) − Φ(z)) / (2Φ(k) − 1) PEER 2010/106 Cases 1.8b, 1.8c. Renormalización simétrica en [−k, k]: 1 debajo, 0 arriba.
median_only step (1 si IML ≤ mediano, si no 0) Límite σ = 0. PEER 2010/106 Cases 1.1 – 1.7 (σ = 0 hard-coded).

Convención R-CRISIS vs PEER

R-CRISIS usa one-sided (trunca arriba, no renormaliza). PEER 2010/106 Cases 1.8b/c usan symmetric con renormalización. Ambas dan respuestas diferentes cuando k es finito; el motor soporta las dos y deja la elección al caller vía parámetro. El default es one-sided para mantener compatibilidad con proyectos existentes calibrados contra R-CRISIS (p.ej. Manila 5BGC).

Para el caso σ = 0 (medianas sin variabilidad aleatoria) añadimos median_only porque ninguna de las dos fórmulas anteriores converge correctamente al límite σ → 0 con renormalización finita.

§ 8Validación PEER 2010/106

Los benchmarks vienen del Excel oficial del proyecto PEER (docs/Tests/Set 1 Results.xlsm), parseado por docs/Tests/parse_peer_tests.py. Estos valores superan a las tablas del PDF publicado — en particular, los Cases 1.10/1.11 fueron regenerados en el Excel con sigma untruncated aunque el texto del PDF aún dice "σ = 0", algo que confirmamos empíricamente comparando con todos los 13 códigos participantes.

Caso Descripción bins pass peor Δ runtime
1.1M=6.5 delta, ruptura completa71 / 710.14 %0.0 s
1.2M=6.0 delta, floating rupture57 / 572.68 %0.1 s
1.3M=6.0 con σlog10 A=0.2557 / 574.49 %3.3 s
1.4M=6.0 en falla reverse 60° W61 / 614.30 %0.7 s
1.5MFD truncated exponential65 / 652.09 %38.0 s
1.6MFD truncated normal65 / 650.73 %39.6 s
1.7Youngs & Coppersmith (1985)65 / 652.83 %38.7 s
1.8aSadigh σ untruncated119 / 1192.49 %0.6 s
1.8bTruncación simétrica ±2σ98 / 982.28 %1.9 s
1.8cTruncación simétrica ±3σ113 / 1133.29 %1.9 s
1.10Source areal, profundidad fija 5 km70 / 703.11 %29.9 s
1.11Source volumétrico, 5–10 km69 / 694.20 %145.3 s
TOTAL 12 casos, 7 sitios cada uno para el fault suite · 4 para area 910 / 910 4.49 % 5:00 min

Bugs detectados y corregidos durante la validación

  1. Sadigh 1997 sin split M ≤ 6.5 / M > 6.5 — coeficientes unificados. Afecta aplicaciones con Mmax > 6.5.
  2. Rrup de falla buzante — el borde superior de la ruptura se pinaba en rx=0 sin incluir el offset horizontal cuando la ruptura flotaba down-dip. Error de hasta 170 % en footwall sites.
  3. Youngs & Coppersmith 1985 sin elevación del boxcar — densidad continua en la frontera en vez de la elevada por exp(β·ΔM₂). Resultado: 3× menos tasa, 0 / 65 passing inicialmente.
  4. Floating rupture endpoint-inclusive — sesgo en sitios de borde por usar trapezoidal en vez de midpoint rule. Corregido pasando a midpoint.
  5. Truncación de sigma sin renormalización simétrica — faltaban los modos symmetric y median_only.
  6. Piso de distancia 1 km forzado en compute_hazard_curve — mataba la cola de near-fault cuando la GMPE ya cap-ea a 0.1 km por sí sola. Ahora min_distance_km=0.1.
  7. MFD truncated normal con balance de momento sobre [Mmin, Mmax] — PEER integra desde M = 0. Corregido en PEERTruncatedNormalMFD.
  8. Grilla de area source en coord. esférica sin cos(lat) — ligera anisotropía en la densidad de puntos. Corregido en area_source_distances con spacing real 1 km.

Cómo correr la suite

# Suite PEER completa con tabla de salida
uv run --project packages/pipeline python docs/Tests/run_peer_tests.py

# Suite PEER via pytest (idéntica lógica, formato pytest)
pytest tests/pipeline/psha/test_peer_verification.py --run-slow -q

# Suite rápida (PEER oculto, smoke tests de producción incluidos)
pytest tests/pipeline -q

§ 9Regresión Manila 5BGC

Snapshot blindado contra drift silencioso en el caso con el que calibramos el motor: 5 BGC, Taguig, Metro Manila (14.54762°N, 121.04674°E, Vs30 = 520 m/s), modelo de sources PEM1 de Dynamis Associates × Appgile cargado directamente desde packages/pipeline/data/PSHA_PEM1.DAT (41 sources tras el filtro de catalog_row_to_source). Sin base de datos, sin GEM API, sin R-CRISIS — 100 % offline.

Baseline actualizado el 2026-04-13. El baseline previo (2026-03-31) contenía el bug conocido de doble contribución del slab (Manila south slab 49.8 %) por el double-counting de f_depth en BCHydro intraslab cuando ya está en el Rrup 3-D. El baseline nuevo colapsa el slab a < 1 % y el hazard pasa a dominarse por los segmentos del Manila trench (interface) y la West Valley Fault — físicamente consistente con un sitio a 1.1 km de Marikina.
RP (yr)Baseline 03-31Baseline 04-13Dynamis (RotD100)Ratio vs Dynamis
43 (SLE)0.11923 g0.11509 g0.154 g0.75 ×
475 (DBE)0.35271 g0.33597 g
9750.46724 g0.44323 g
2475 (MCEr)0.65334 g0.61688 g1.004 g0.61 ×

El motor calcula RotD50 (mediana direccional) y Dynamis reporta RotD100 (dirección de máxima respuesta). La ratio típica RotD100/RotD50 es 1.1–1.3, lo que explica la mayoría del 25-40 % de delta contra Dynamis. Más adelante se añadirán el término de directividad Bayless-Somerville (2013) y el floor del 80 % del código (TBI 2017) que Dynamis aplica.

Top 5 contribuciones (RP = 475 yr)

SourceTectonicContribución
Manila trench south EXPinterface37.2 %
Zone 6 (area source)crustal22.0 %
Marikina Valley Westcrustal13.0 %
Manila trench southinterface12.4 %
Marikina Valley Eastcrustal8.2 %

Test

3 aserciones en tests/pipeline/psha/test_manila_regression.py:

  1. source_count — 41 sources (a drift aquí señalaría un cambio en el parser de PEM1 DAT).
  2. design_pga — PGA a 43 / 475 / 975 / 2475 yr dentro de ±0.5 % del snapshot.
  3. top_source_contributions — los 5 sources dominantes y su porcentaje dentro de ±1 punto porcentual.

Marcado como @pytest.mark.slow. Runtime end-to-end 22 s. Opt-in con pytest --run-slow.

§ 10Anchors de GMPE (snapshot regression)

47 puntos de anclaje que fijan el output exacto (ln_mean, ln_sigma) de cada GMPE en tuplas específicas de (M, R, Vs30, T, mechanism). Tolerancia 0.001 en unidades logarítmicas — aprox. 0.1 % en valor lineal. Detecta cualquier drift de coeficiente al instante.

Los tests en tests/pipeline/test_gmpes.py ya existían pero solo verifican rangos amplios ("PGA debería estar entre 0.02 y 0.5 g"). Un bug de coeficientes puede caber holgadamente dentro de esos rangos y pasar desapercibido. Los anchor tests complementan ese chequeo con valores exactos.

GMPEAnchorsCobertura
SadighEtAl1997 7 M = 5 / 6 / 6.5 / 7 / 7.5 · R = 1 / 10 / 30 / 100 / 200 · strike-slip + reverse. Verifica el split M ≤ 6.5 / M > 6.5 que corregimos durante la validación PEER.
AbrahamsonEtAl2014 (ASK14)9Incluye SA(0.2) · SA(1.0) · Vs30 = 270 para amplificación en suelo blando.
BooreEtAl2014 (BSSA14)9Mismas tuplas + factor de estilo de falla.
CampbellBozorgnia2014 (CB14)9Idem.
ChiouYoungs2014 (CY14)9Idem.
BCHydro2016Interface2Megathrust M 7.5 / 8.5 con hypo_depth_km=30.
BCHydro2016Intraslab2Intraslab M 7.0 / 7.5 con hypo_depth_km=80 / 120.
TOTAL47Suite completa en 80 ms (fast).
Cómo se actualizan. Un cambio deliberado de coeficientes (p.ej. adoptar una errata corregida en un paper) requiere recapturar los valores con el script que viene en el docstring del módulo y documentar la razón en el commit. No se hace automáticamente para evitar que un bug se bake en el snapshot.

§ 11PEER Set 2 y Set 3 — estado

El PEER 2010/106 distribuye tres sets de casos de verificación. Set 1 está completo (§ 8). Set 2 y Set 3 están parcialmente cubiertos. Este es el estado actual y el roadmap.

Set 2 — sources múltiples, logic tree, hanging wall, intraslab

SheetDescripciónAlcanzable hoy
Test 2.1 Multiple Sources (Area + Fault B + Fault C, deaggregation) Reach proberate total clava a −1.2 % con Y&C 1985 (Mchar=7.0 y 6.5), area source y Sadigh 97 combinados. La forma de la curva de hazard deriva ~30 % por ambigüedades no resueltas del PDF (widths, depths, convención σ). Implementado en test_peer_set2_case2.py con un test que pasa y otro xfail estricto documentado.
Test 2.2a-d Ensemble individual NGA-West2 (ASK14 / BSSA14 / CB14 / CY14) Partial — las GMPEs están implementadas y cubiertas por los 47 anchor tests (§ 10), pero las sheets del Excel no documentan la geometría del source. Requiere reverse-engineering del PDF.
Test 2.3a-d Hanging wall effect con cada NGA-West2 PartialSeismicSource.hw_factor existe en el engine. Falta la geometría del source.
Test 2.4a-b Distribuciones no-uniformes de hipocentro (uniform / triangular) Gap — el engine asume hipocentro uniforme (uniform rupture location). Requiere extender floating_rupture_distances con pesos configurables por posición.
Test 2.5a-b Mixture models en la distribución de ground motion (upper tails) Gap — el engine asume log-normal single; mixture models no están soportados.

Las 13 hojas de Set 2 sí tienen benchmarks (5 core codes → mean como verdad aceptada, ±5 % por sheet — misma convención que Set 1). La Barrera principal no es el motor sino que la geometría de source está fuera del Excel y hay que reverse-engineer-la del PDF. Plan: tras la reunión con Carlos, abordar Test 2.1 (el más simple) y Test 2.2/2.3 juntos (misma source, distintas GMPEs).

Set 3 — cross-code comparison, sin benchmarks absolutos

Set 3 no tiene benchmarks. Las Notes del Excel lo dicen explícitamente: "The tests in this set do not have benchmark answers and acceptance criteria. The results are split into groups based on the different modeling approaches, and the percent difference between codes using the same approach, and between all codes across different approaches are reported."

Los 5 sheets (Test 3.1 Bending Fault, Test 3.2 Logic tree fractiles, Test 3.3 Intraslab zones, Test 3.4 Areal finite rupture, Test 3.1 GW) son ejercicios de comparación cruzada entre códigos — no hay una respuesta única aceptada. Útiles para detectar desacuerdos, no para certificar corrección absoluta. Por ese motivo Set 3 no forma parte de la suite de validación del motor.

Podrían usarse como chequeo de razonabilidad: si el motor cae dentro del cloud P5-P95 de los 13 códigos participantes en un modelo concreto, es señal de que el motor está en el rango. Esto queda registrado como mejora futura pero sin prioridad.

§ 12Mapa de archivos

packages/pipeline/pipeline/psha/
├── hazard_curve.py              Cornell-McGuire core + PSHACalculator
├── peer.py                      MFDs PEER + floating rupture + builder público
├── fault_mesh.py                3-D meshing para slabs de subducción
├── gmpes/
│   ├── sadigh_1997.py           Sadigh (referencia PEER)
│   ├── abrahamson_2014.py       ASK14
│   ├── boore_2014.py            BSSA14
│   ├── campbell_bozorgnia_2014.py  CB14
│   ├── chiou_youngs_2014.py     CY14
│   ├── bchydro_2016.py          BCHydro interface / intraslab
│   └── base.py                  Protocol GMPE
├── pga_calculator.py            helpers PGA site-level
└── gmpe_registry.py             lookup + pesos por ensemble

packages/pipeline/pipeline/services/
├── source_service.py            catalog → SeismicSource pipeline
│                                # discretize_source ahora auto-selecciona# floating rupture vs 3-D mesh
└── hazard_service.py            capa de orquestación

tests/pipeline/psha/
├── test_peer_verification.py   PEER Set 1 (12 casos, --run-slow)
├── test_peer_set2_case2.py     Set 2 Case 2 reach probe (rate ±2 %)
├── test_manila_regression.py   Manila 5BGC snapshot (--run-slow)
├── test_production_fault_source.py  smoke tests end-to-end
├── fixtures/
│   └── manila_5bgc_snapshot.json  baseline Apr-13 para regresión
└── conftest.py                  gate --run-slow

tests/pipeline/
└── test_gmpe_anchors.py         47 anchors (ln_mean, ln_sigma) por GMPE

docs/engine/
├── README.md                    overview técnico (este documento en MD)
└── psha-engine-review.html      versión HTML (este archivo)

docs/Tests/
├── run_peer_tests.py            CLI demo 910/910 bins
├── parsed_tests.json            benchmarks del Excel PEER
└── PEER_2010_106.pdf            reporte original

§ 13Limitaciones conocidas