# JESIM WebSocket JSON protocol

Transport:

- The C tsunami GUI appends newline-delimited JSON to `jesim_ws_outbox.jsonl`.
- `jesim_ws_relay.py` forwards each line over WebSocket to `ws://127.0.0.1:8765`.
- One JSON line = one WebSocket text message.

Time base:

- Earthquake origin time is fixed as `1일 00:00`.
- `sim_s` is seconds after origin.
- `issue_time` and `first_arrival_time` are formatted as `N일 HH:MM`.
- `first_arrival_5min_s` is the first-wave ETA rounded up to the next 5-minute mark.

Message kinds:

- `observation_10min`: emitted once per 10 simulated minutes.
- `6h_complete`: emitted once when simulation time reaches 6 h.
- `12h_complete`: emitted once when simulation time reaches 12 h.
- `24h_complete`: emitted once when simulation time reaches 24 h.

Forecast height steps:

- Below 0.05 m is omitted.
- Values are rounded upward into: `0.05m`, `0.2m`, `1m`, `3m`, `5m`, `10m`, `10m+`.

Arrival status labels:

- `곧 도착`: ETA is within 10 minutes of the issue time.
- `도달로 추정`: issue time is already later than the ETA.
- `예상`: ETA is more than 10 minutes away.
- `none`: no simulated first arrival yet.

Observation values:

- `height_m`/`height_label`, `level_m`/`level_label`, and `rise_m`/`rise_label` are kept for Scratch compatibility and use 0.1 m display units.
- `rise` is now a peak-update marker: `+` only when the displayed positive peak is newly updated, `0` otherwise. It no longer sends `-` for falling tide.
- `rise_state` is the same state as text: `rising`, `steady`, or `falling`.
- `peak_m` is the maximum positive rise observed so far.
- `trend` is `rising`, `falling`, `steady`, or `unknown`.
- `trend_cm_per_min` is computed from the 10-minute numerical difference.

Actual JMA observation-point mapping next step:

Create a CSV named `jma_observation_points.csv` with columns like:

```csv
zone_id,station_name,lat,lon,source,preferred
10,釜石,39.2750,141.8850,JMA tide station,1
```

The intended next patch is to load this CSV, convert lat/lon to the current Mercator-like simulation grid using the existing GUI map constants, then choose the nearest simulation station for each actual observation point.


Debug / handshake messages
--------------------------

The relay now sends these messages immediately after Scratch/TurboWarp connects:

- `{"type":"jesim.relay","kind":"hello",...}`
- `{"type":"jesim.relay","kind":"relay_status",...}`

The C GUI also writes these before the first 10-minute bucket:

- `{"type":"jesim.tsunami","kind":"metadata",...}`
- `{"type":"jesim.tsunami","kind":"test_snapshot",...}`

So the Scratch side can confirm the connection without waiting 10 simulated minutes.

Observation-station mode update
-------------------------------

`observation_10min` now sends **observation stations**, not forecast-zone grouped observations.
Each observation entry has fields like:

```json
{
  "obs_id": "MIYAKO",
  "name": "宮古",
  "agency": "JMA",
  "lat": 39.6333,
  "lon": 141.9667,
  "sim_station_no": 1234,
  "update_time": "1일 00:10",
  "height_m": 0.2,
  "height_label": "0.2m",
  "rise": "+",
  "rise_state": "rising",
  "rise_m": 0.2,
  "rise_label": "0.2m",
  "peak_m": 0.3,
  "trend": "rising",
  "trend_cm_per_min": 1.25
}
```

Forecast-zone arrays are only included in `6h_complete`, `12h_complete`, and `24h_complete`.
For other messages, check `zone_forecast_included:false`.

Observation point source:

- The program loads `jesim_observation_points.csv` next to the EXE.
- CSV columns: `id,name,agency,lat,lon`
- Each lat/lon is projected to the current JESIM grid and matched to the nearest enabled simulation observation point.
- If the CSV is missing or empty, a compact built-in starter list is used.


Outbox clearing
---------------

This debug build clears stale JSONL state in two places:

- `jesim_ws_relay.py` truncates `jesim_ws_outbox.jsonl` when the relay starts.
- `tsunami_gui.exe` truncates the same file and writes a `simulation_reset` marker when the simulation is reset/reloaded.

After reset, the next simulation step writes fresh `metadata` and `test_snapshot`, then the normal 10-minute observation and 6/12/24-hour forecast messages.


Real-data-only relay mode
-------------------------

The relay no longer sends `jesim.relay` messages to Scratch/TurboWarp.
It forwards only these `jesim.tsunami.kind` values:

- `observation_10min`
- `6h_complete`
- `12h_complete`
- `24h_complete`

Console status remains available in the Python relay terminal.


Warning-only mode
-----------------

Temporary mode for Scratch/JESIM performance:

- `observation_10min` is paused.
- WebSocket forwards only:
  - `6h_complete`
  - `12h_complete`
  - `24h_complete`
- These messages contain `zones` forecast/warning data only.
- `observations` is not included in 6/12/24 warning messages in this build.


6h send fix
-----------

Outbox path is now folder-local:

- GUI writes to the folder containing `tsunami_gui.exe`.
- Relay tails the folder containing `jesim_ws_relay.py`.

Keep the EXE and relay script in the same folder.


Realtime arrival status mode
----------------------------

After the internal 24h model snapshot is complete, the GUI starts a real-time
warning clock. Every real 5 minutes it sends:

- `arrival_status_update_5min`

Arrival status labels are Japanese:

- `到達予想`
- `まもなく到達`
- `到達中`
- `到達と推定`

`sim_s` remains internal model time. `warning_elapsed_s` and `issue_time` are
the real-time warning-system clock.


Realtime ETA fix
----------------

Zone arrival status now compares real warning elapsed time with compressed
warning-system ETA, not raw model propagation seconds.

Fields:

- `model_first_arrival_s`: raw internal model arrival time.
- `first_arrival_s`: JESIM warning-system ETA seconds.
- `first_arrival_5min_s`: warning-system ETA rounded up to 5 minutes.


Missing arrival handling
------------------------

Warning-zone list is now fixed for IDs 1..93 except 67.

If there is no arrival information for a zone, the zone is still emitted with:

- `model_first_arrival_s: 0`
- `first_arrival_s: 0`
- `first_arrival_5min_s: 0`
- `first_arrival_time: "0"`
- `arrival_status: "0"`
- `has_arrival: false`

If there is no warning height, `height_step` is `"0m"` and `raw_max_m` is `0`.


ETA compression removed
-----------------------

`first_arrival_s` is no longer compressed.

If the internal model arrival is 10800 seconds, the outgoing fields are:

- `model_first_arrival_s: 10800`
- `first_arrival_s: 10800`
- `first_arrival_5min_s: 10800`

`arrival_status` is calculated against exact `first_arrival_s`, not the rounded
display value.  Missing arrival information is still emitted as zero.


Payload-only mode
-----------------

Scratch/JESIM is now responsible for arrival-state decisions with its own timer.
The WebSocket payload contains only warning-zone height and ETA data.

Allowed kinds:

- `6h_complete`
- `12h_complete`
- `24h_complete`

Zone object fields:

- `zone`
- `name`
- `height_step`
- `raw_max_m`
- `first_arrival_s`
- `first_arrival_5min_s`
- `first_arrival_time`

Missing arrival remains zero.


Observation point coordinate CSV
--------------------------------

Added:

- `jesim_observation_points_assigned_coords.csv`
- `jesim_observation_points_assigned_coords_all_raw.csv`
- `jesim_forecast_zone_index.csv`

Coordinate transform:

- Center is `(0, 0)`.
- x range is `-897 .. 897`.
- y range is `-540 .. 540`.
- Positive y is north/up.

Main CSV columns:

- `station_no`
- `forecast_zone_id`
- `forecast_zone_name`
- `grid_x`
- `grid_y`
- `jesim_x`
- `jesim_y`
- `enabled`
- `edge_artifact`


Transmit-only render mode
-------------------------

The GUI now starts in a light render mode for JESIM transmission:

- simulation and WebSocket export continue normally
- wave overlay drawing is skipped
- coastal height markers and hover station tooltips are skipped
- source drawing/settings remain available
- while running, the window is not invalidated every simulation step

Toggle with `Ctrl+R`; plain `R` remains reset.


Depth/dip source patch
----------------------

Depth attenuation is now stronger and asymptotic:

- thrust/reverse: factor = 1 up to 20 km, 0.05 at 80 km, then exponential to 0
- strike-slip: factor = 1 up to 10 km, 0.05 at 50 km, then exponential to 0

`g_source_depth_km` is interpreted as the shallowest fault-edge depth.
For dipping reverse-fault profile sources, each source cell gets a local depth:

`local_depth = shallow_depth + down_dip_distance * sin(dip)`


Global 6h decay removed
-----------------------

The old whole-field amplitude decay has been disabled:

- `GLOBAL_HEIGHT_DECAY_KEEP_6H` is now `0.775`
- `global_height_decay_per_step()` returns the per-substep factor for 6% amplitude loss per 6h
- solver damping now uses only hidden outer absorber and coastal reflection logic

This avoids double-counting energy loss now that coastline absorption/reflection is active.


Weak global decay
-----------------

Global decay is reintroduced at a weak level:

- `GLOBAL_HEIGHT_DECAY_KEEP_6H = 0.850`
- about 4.5% amplitude loss per 6 simulated hours
- solver total damping = hidden outer absorber * weak global decay


Delayed warning decision fields
-------------------------------

For warning downgrade/release logic, the payload now includes delayed decision
fields.  These are not immediate actions.

Top-level:

- `effective_time_s`
- `effective_time`
- `timer_rule`

Per zone:

- `height_rank`
- `warning_decision`
- `decision_effective_s`

Rules:

- `height_step < 1m` -> `warning_decision: "release"`
- `height_step >= 1m` -> `warning_decision: "keep_or_downgrade_to_height"`

Scratch/JESIM must apply decisions only when:

`JESIM timer >= decision_effective_s`


Warning check schedule and split decisions
------------------------------------------

The relay now forwards only:

- `warning_check`

Schedule:

- first check at 6h
- then every 2h until 24h
- 6, 8, 10, 12, 14, 16, 18, 20, 22, 24h

Decision semantics:

- `upgrade_decision`: forecast-based; Scratch/JESIM may apply immediately.
- `downgrade_decision`: observation/elapsed-time based; apply only when
  `JESIM timer >= downgrade_effective_s`.

Per-zone fields:

- `height_rank`
- `upgrade_decision`
- `upgrade_effective_s`
- `downgrade_decision`
- `downgrade_effective_s`
- `decision_effective_s`


JSON key compatibility
----------------------

Existing keys are preserved.  New split-decision keys are additive.

Legacy aliases:

- `warning_decision` = same value as `downgrade_decision`
- `decision_effective_s` = same value as `downgrade_effective_s`

New keys:

- `upgrade_decision`
- `upgrade_effective_s`
- `downgrade_decision`
- `downgrade_effective_s`


Forecast upgrade is wavefield-based
-----------------------------------

Existing keys remain observation/station based for compatibility:

- `height_step`
- `height_rank`
- `raw_max_m`
- `first_arrival_s`
- `first_arrival_5min_s`
- `first_arrival_time`
- `warning_decision`

New forecast keys are wavefield based and should be used for upgrade:

- `forecast_height_step`
- `forecast_height_rank`
- `forecast_raw_max_m`
- `forecast_first_arrival_s`
- `forecast_first_arrival_5min_s`
- `forecast_first_arrival_time`
- `forecast_basis`

Decision split:

- `upgrade_decision` uses the wavefield forecast fields.
- `downgrade_decision` and legacy `warning_decision` use observation-point fields.


Compile fix
-----------

Added forward declarations for wavefield forecast zone-map helpers:

- `jesim_free_wave_zone_map`
- `jesim_rebuild_wave_zone_map`

This fixes MinGW implicit declaration errors when resetting JESIM export state.


3h update-only warning checks through 36h
-----------------------------------------

Schedule:

- 6h first check
- then every 3h through 36h:
  6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36

Transmission filter:

- Only send `warning_check` when at least one tsunami-advisory-or-higher zone changed.
- Advisory-or-higher threshold for this filter is `>= 0.2m`.
- Release/downgrade is still sent when a previously active zone drops below advisory.

Top-level fields:

- `changed_zone_count`
- `advisory_update_only`
- `check_interval_h`
- `forecast_until_h`

Per-zone:

- `update_changed`


Upgrade-only mode
-----------------

Downgrade/release is temporarily suspended.

Kept for JSON compatibility:

- `warning_decision`
- `downgrade_decision`
- `decision_effective_s`
- `downgrade_effective_s`

But downgrade-related decision values are now `"none"`.

Transmission filter now sends only forecast upgrade changes:

- newly forecast advisory-or-higher area
- forecast rank increase
- forecast arrival-time change while forecast is advisory-or-higher

It does not send release/downgrade-only updates.


Compile fix: update-only precompute placement
---------------------------------------------

The update-only precompute block was moved from `jesim_export_metadata()` to
`jesim_export_snapshot()`, where the zone arrays and `JESIM_MAX_ZONE` are in scope.


Compile fix 2
-------------

The misplaced update-only precompute block was removed from the actual
`jesim_export_metadata()` function definition. It remains only in
`jesim_export_snapshot()`.


Per-cell slip moment
--------------------

`SourceSpec` now contains `slip_m`.

Moment calculation is now:

`M0 = sum(mu * slip_m_i * fault_plane_area_i)`

This supports mixed-slip source drawings where the user changes slip while
painting the fault.  Presets now save an optional 5th source column `slip_m`;
old presets without it remain readable.

Source blending
---------------

Profile-mode repainting no longer hard-overwrites existing cells. Height,
slip metric, and delay are blended/combined to avoid unnatural overwrite bands
when slip or source thickness changes mid-drawing.


Paint field fix
---------------

The previous per-cell slip patch blended water-height deformation too strongly.
This could leave uplift mainly around the final mouse position.

Now:

- `slip_m` remains blended for moment calculation.
- `height_m` keeps the stronger same-sign source deformation.
- footprint-only cells no longer erase existing water deformation.
- force repaint is a real replacement again for finite strike-slip cleanup.


Korean zone TTS names
---------------------

For WebSocket JSON only, Korean forecast-zone names 68..93 are converted to
Japanese-readable TTS form.

Examples:

- `강원 북부 해안` -> `カンウォン 北部 沿岸`
- `전남 동부 남해 해안` -> `チョンナム 東部 南海 沿岸`
- `인천.경기 북부 해안` -> `インチョン・キョンギ 北部 沿岸`
- `울릉도 해안` -> `ウルルンド 沿岸`

Internal zone IDs and GUI/CSV names are unchanged.


External fault importer removed
-------------------------------

External finite-fault file import was removed from this build. Use manual source painting, presets, or source CSV import instead.


5-minute batched observation arrivals
---------------------------------

In addition to the 3-hour `warning_check` payload, the GUI now emits separate
observation-point reports.  A station first locks tsunami arrival when the mapped observation
station first detects positive rise, but the public first-arrival report is delayed to the next 5-minute model-time bucket. Later reports are queued whenever that
station's displayed positive peak has newly increased by a 0.1 m step and the next 5-minute model-time bucket is reached. Live-level-only changes are suppressed.  This uses the points
from `jesim_observation_points.csv`, which has `id,name,agency,lat,lon` columns.

The event kind is:

- `observation_arrival`

It is intentionally distinguishable from the 3-hour warning payload:

```json
{
  "type": "jesim.tsunami",
  "kind": "observation_arrival",
  "bulletin_class": "observation_point_event",
  "event_driven": false,
  "is_3hour_check": false,
  "timer_rule": "real-time scheduled observation-point reports; first arrivals and later observation value updates are both batched to 5-minute model-time buckets; independent from 3-hour warning_check"
}
```

The 3-hour warning payload now includes:

```json
{
  "kind": "warning_check",
  "bulletin_class": "forecast_warning_check",
  "event_driven": false,
  "is_3hour_check": true
}
```

Each observation entry includes the actual model observation time, not only the
transmission time.  It also includes NHK-style `rise` / `rise_state` fields for
peak-update marker only.  Existing numeric fields such as `rise_m`, `level_m`,
`drawdown_m`, and `peak_m` are kept for Scratch compatibility:

```json
{
  "obs_id": "MIYAKO",
  "name": "宮古",
  "agency": "JMA",
  "lat": 39.6333,
  "lon": 141.9667,
  "sim_station_no": 1234,
  "forecast_zone_id": 12,
  "forecast_zone_name": "...",
  "observed_time_s": 2520,
  "observed_time": "1일 00:42",
  "update_time": "1일 00:42",
  "height_m": 0.1,
  "rise": "+",
  "rise_state": "rising",
  "rise_m": 0.1,
  "level_m": 0.1,
  "drawdown_m": 0.0,
  "peak_m": 0.1,
  "raw_live_m": 0.052,
  "raw_rise_m": 0.052,
  "raw_peak_m": 0.052,
  "report_reason": "first_arrival_at_observation_point",
  "arrival_threshold_m": 0.05
}
```


Later height updates use the same top-level `kind` but change the per-observation
reason:

```json
{
  "kind": "observation_arrival",
  "contains_height_updates": true,
  "observations": [
    {
      "obs_id": "MIYAKO",
      "observed_time": "1日 01:20",
      "height_m": 0.3,
      "rise": "+",
      "rise_state": "rising",
      "rise_m": 0.3,
      "peak_m": 0.4,
      "report_reason": "height_update_at_observation_point"
    }
  ]
}
```

The relay allow-list includes both `warning_check` and `observation_arrival`, so
Scratch/TurboWarp can route observation news separately from the 3-hour forecast
checks by reading `kind`, `bulletin_class`, or `is_3hour_check`.


Real-time scheduled bulletin dispatch
-------------------------------------

`observation_arrival` is no longer written immediately when the numerical model
passes a station observation/report time.  The GUI now queues first-arrival and
later 0.1 m peak-update records, then holds each record until the synchronized
real clock reaches that record's model observation time.

Dispatch rule:

```text
real_sync_time_s = real_elapsed_s + calc_start_delay_s
send when real_sync_time_s >= observed_time_s
```

Example:

```text
observed_time = 1日 01:10  -> observed_time_s = 4200
calc_start_delay_s = 0    -> send after 4200 real seconds
calc_start_delay_s = 600  -> send after 3600 real seconds
```

`calc_start_delay_s` represents the delay between earthquake origin time and the
moment the JESIM calculation/broadcast synchronization clock is started.  It can
be edited in Settings -> Main as `JESIM start delay (s)`.

The payload now includes synchronization fields:

```json
{
  "kind": "observation_arrival",
  "sim_s": 4200,
  "computed_model_s": 28800,
  "real_elapsed_s": 4200,
  "real_sync_time_s": 4200,
  "calc_start_delay_s": 0,
  "observed_time_s": 4200,
  "observed_time": "1日 01:10"
}
```

`warning_check` is not gated by the synchronized real clock.  It is a forecast/
warning check and is dispatched when the numerical model reaches the check time.
For example, a 6h check requires `computed_model_s >= 21600`; it does not wait
for `real_sync_time_s >= 21600`.  The synchronized real clock is only used for
`observation_arrival` reports.


V21/V22/V24/V25 note: observation `rise` was changed to a peak-update marker. In v25, JESIM observation_arrival suppresses non-peak-updating records, so sent observation records should normally have `rise: "+"`.


V24 note: first-arrival reports and later observation value updates are both sampled at 300-second model-time buckets, so a 30-second solver step no longer produces a websocket observation update every 30 seconds. First arrivals may therefore appear with up to about 5 minutes of bulletin delay.


## v24 first-arrival timing note

For `observation_arrival`, first-arrival messages are sent only on the 5-minute observation bulletin cadence. The actual threshold-crossing time is not rounded; use `observed_time_s` / `observed_time` for the true arrival time. Use `report_bucket_s` or `scheduled_bulletin_s` only as the websocket/bulletin send bucket.


V25 note: JESIM observation_arrival is peak-update-gated. If a 5-minute bucket contains no newly updated displayed positive peak at any observation point, no observation_arrival message is sent for that bucket.


### v83 observation peak-update fields

Each `observation_arrival.observations[]` record now includes explicit per-observation maximum-update flags:

- `peak_updated`
- `individual_peak_updated`
- `max_updated`
- `is_new_individual_max`
- `peak_delta_dm`
- `peak_delta_m`
- `peak_update_scope` = `individual_observation_point`

These booleans are `true` only when that specific observation point newly updates its displayed positive peak. This is the same judgement that was previously only implied by `rise: "+"`. Packet-level `packet_meaningful_update` is still true when at least one observation in the whole packet updated its peak.
