# Embedding-analog mineral prospectivity — governed folia/0.1 recipe.
#
# WHAT IT ASKS: does a place's 64-D AlphaEarth satellite embedding RESEMBLE the embeddings at a
# belt's known-mineralized reference sites more than it resembles random country rock? A random
# forest learns positive(1) vs background(0) on the raw 64-D vectors, its held-out skill is
# measured leave-one-out in the same run, and the model scores a site grid.
#
# RUNNABILITY (2026-08-19). This recipe runs END TO END on the Rust engine, from the AlphaEarth
# bytes onward. `folia run` IS the whole instruction — there is no prefetch step, no local
# materialization, and no Earth Engine anywhere in the chain:
#
#   products/sdk/target/release/folia run \
#       registry/recipes/folia/reproductions/embedding-analog-gold-prospectivity --no-cache
#
# The last gap closed on 2026-08-19 (ADR-0090): `embedding_field`, the engine's `tile-index`
# source arm, reads the published product directly — a column-projected GeoParquet tile-index
# lookup over HTTPS range requests (6.9 MB of a 78 MB index), a windowed read of each
# intersecting COG, int8 dequantisation per the registry record's declared encoding, and a
# first-wins mosaic across UTM zones 35S/36S onto the geographic grid.
# `research/mineral-prospecting/embedding-analog/prefetch_belt_cog.py` remains in `research/` as
# the REFERENCE implementation the engine arm is graded against; it is no longer a step anyone
# has to run. Measured parity, same machine, 2026-08-19: engine-source LOO AUC 0.9253 against the
# prefetch path's 0.9253 on the same 85 positives and 1500 background draws.
#
# ─────────────────────────────────────────────────────────────────────────────
# HONESTY RAILS (read before running or citing)
#   - The output is a RESEMBLANCE score, not a discovery. A high score means the ground's
#     multi-temporal optical/radar/elevation signature, as compressed by AlphaEarth, LOOKS LIKE
#     the ground at known sites. It does NOT mean a deposit is present: AlphaEarth embeds surface
#     appearance and seasonality, not subsurface mineralisation. Never "gold found", never
#     "prospectivity confirmed", never "deposit present".
#   - The leave-one-out AUC this recipe emits is the analog's held-out skill on THIS belt with
#     THIS reference set. It does not transfer as a number to another belt without that belt's own
#     held-out test. New-site outputs are UNVALIDATED until ground truth exists for that site.
#     On Mberengwa the measured band is AUC 0.92-0.96 depending on the background draw and the
#     sampling grid (validations/comparison.yaml decomposes it) against a 0.75 validation floor.
#     There is no single headline number to quote.
#   - The reference set is ~95% one source (Bartholomew) and records DISCOVERED, historically
#     PRODUCED deposits — a biased sample of true endowment. The model can only learn the
#     appearance of the deposits we already know about.
#   - The two anomaly arms of the study (iron-oxide/gossan, alteration, structure) stay PROSE
#     (research/mineral-prospecting/): they have no ground truth and cannot be graded.
# ─────────────────────────────────────────────────────────────────────────────
#
# RUN IT FOR YOUR OWN SITE. Every belt/site/reference-set choice below is a PARAMETER, so a
# second site is a `--set` list, never a source edit:
#
#   folia run <this-dir> --no-cache \
#       --set belt_bbox='[29.3, -19.25, 29.99, -18.45]' \
#       --set site_bbox='[29.669354, -18.789136, 29.741815, -18.752429]' \
#       --set occurrences_uri=local://data/occurrences-midlands-kwekwe.geojson \
#       --set positive_filter='{}'
#
# (that IS the staged second site, verbatim: the Midlands belt scoring Kwekwe block 4. Its
# inventory is gold-only, so `{}` — every feature is a positive — is the honest filter there.)
#
# The fill-in-the-blanks form of that command, its docs, and a generic launcher that writes it
# for you live at research/mineral-prospecting/embedding-analog/sites/ (README.md +
# SITE_SPEC_TEMPLATE.yaml + run_recipe.py). Your reference GeoJSON must sit in this recipe's
# own `data/` directory (`local://data/<file>`) or be an http(s) URL — the recipe directory is
# the portable unit that gets shipped to a runner, so its inputs travel with it.
#
# WHAT A NEW BELT OWES: metrics 1-3 of validations/comparison.yaml are Mberengwa's held-out
# numbers. A new belt RE-RUNS leave-one-out against its own reference set and records its own;
# it does not inherit them. The 0.75 floor is the gate that transfers, not the observed band.
# ─────────────────────────────────────────────────────────────────────────────

spec_version: folia/0.1
name: embedding-analog-gold-prospectivity
version: "0.2.0"
description: >
  Embedding-analog mineral prospectivity on the Mberengwa-Belingwe greenstone belt (Zimbabwe).
  Samples the 64-D AlphaEarth Embedding Fields v1 annual mosaic at 85 confirmed gold occurrences
  and at 1500 drawn background points, fits a random forest on positives vs background, measures
  its leave-one-out skill, and scores the Zvishavane claim AOI. The held-out skill is an OUTPUT
  of the same run that draws the map, never a number quoted from elsewhere: on this belt it
  measures AUC 0.92-0.96 across background draws and sampling grids, against a 0.75 floor
  (validations/comparison.yaml decomposes the band). Belt, site, reference set and every
  hyperparameter are parameters, so another site is a `--set` list rather than a fork.

parameters:
  embeddings_dataset:
    type: string
    description: >
      The published embedding product, as a registry dataset ref. The record
      (registry/datasets/google-deepmind/aef/v1/annual/) is the contract: it declares the tile index's
      location and columns, the int8 dequantisation (((raw/127.5)^2)*sign(raw), -128 -> NaN), and
      the 64-band layout. Swapping products means swapping this ref, not editing the engine.
    default: "@google-deepmind/aef/v1/annual"
  embeddings_year:
    type: number
    description: The annual mosaic to read.
    default: 2024
  embeddings_resolution:
    type: number
    description: >
      Effective ground resolution in metres. Drives BOTH the overview level each 10 m tile is
      read at and the geographic grid step (degrees at the AOI's centre latitude). 80 m = the
      native 10 m at overview factor 8, the grid the reference implementation sampled on.
    default: 80
  site_bbox:
    type: array
    items: number
    description: The site AOI being scored [w, s, e, n] (EPSG:4326) — the Zvishavane claim.
    default: [29.980874, -20.17516, 30.007902, -20.142392]
    min_items: 4
    max_items: 4
  occurrences_uri:
    type: string
    description: >
      The mineral-occurrence point set (CC-BY; Bartholomew 1990 ZGS Mineral Resources Series 23
      via Blenkinsop 2011, plus USGS MRDS). 140 features: 85 classified `gold-occurrence`, 55
      `working` (Great-Dyke asbestos/chromium, NOT gold).
      Another belt points this at its OWN inventory: `local://data/<file>.geojson` (the file must
      live in this recipe's `data/` directory, which travels with the recipe) or an http(s) URL.
    default: "local://data/occurrences.geojson"
  positive_filter:
    type: object
    description: >
      Field -> value equality pairs a feature must match ALL of to count as a POSITIVE. Features
      that do not match stay in the set as EXCLUSION-ONLY rows: they still push the background
      draw away from near-deposit ground, they just never become training positives. `{}` makes
      every feature a positive — right for an inventory that is already gold-only.
    default: { classification: gold-occurrence }
  belt_bbox:
    type: array
    items: number
    description: The belt the background is drawn over [w, s, e, n] (EPSG:4326).
    default: [29.3, -20.8, 30.7, -19.6]
    min_items: 4
    max_items: 4
  reference_window_px:
    type: number
    description: >
      Sampling window RADIUS in pixels: 1 -> a 3x3 (~240 m at 80 m) window mean, which absorbs
      the ~0.5 km coordinate error of the occurrence inventory.
    default: 1
    minimum: 0
  background_n:
    type: number
    description: Random country-rock background points drawn across the belt.
    default: 1500
    minimum: 1
  background_exclude_m:
    type: number
    description: >
      Drop a background draw within this distance of ANY occurrence (gold AND working), so
      background means country rock rather than near-deposit ground.
    default: 1500
    minimum: 0
  background_seed:
    type: number
    description: Seed of the background draw — the same seed redraws the same points forever.
    default: 42
  rf_trees:
    type: number
    description: Trees in the forest.
    default: 300
    minimum: 1
  rf_max_depth:
    type: number
    description: Maximum tree depth (modest — a calibrated probability, not a memoriser).
    default: 12
    minimum: 1
  rf_min_leaf:
    type: number
    description: Minimum bootstrap rows per leaf.
    default: 2
    minimum: 1
  rf_seed:
    type: number
    description: Seed of the forest's bootstrap + feature subsample.
    default: 0
  rf_bins:
    type: number
    description: >
      Quantile bins per feature in the engine's histogram split search. Higher = finer split
      thresholds and a slower fit. 64 is the engine default; this recipe raises it to 256, which
      measurably closes the gap to the reference sklearn fit on the same table (LOO AUC 0.916 at
      64 vs 0.925 at 256, against sklearn's 0.927 — validations/comparison.yaml).
    default: 256
    minimum: 2

layers:

  # ── SOURCE: the belt embedding ────────────────────────────────────────────────────────────
  source/embeddings:
    type: raster
    description: >
      AlphaEarth Embedding Fields v1 annual mosaic (2024), 64 bands A00..A63, over the belt at
      80 m effective (10 m native at overview factor 8). int8 dequantised as
      ((raw/127.5)**2)*sign(raw) with the -128 NODATA sentinel -> NaN, mosaicked across UTM zones
      35S/36S into EPSG:4326. NO Earth Engine anywhere in the chain: the tile index and the 12
      intersecting COGs are read over anonymous HTTPS range requests from Source Cooperative.
    compute:
      # `embedding_field` is the engine's tile-index source arm (ADR-0090). The explicit op
      # spelling is its sanctioned form: the automatic `ref:` desugar targets imagery_composite,
      # which is an index compositor, not a 64-band embedding read.
      op: embedding_field
      params:
        uri: { param: embeddings_dataset }
        bbox: { param: belt_bbox }
        year: { param: embeddings_year }
        resolution: { param: embeddings_resolution }

  # ── SOURCE: the reference occurrences ─────────────────────────────────────────────────────
  occurrences:
    type: vector
    description: >
      The mineral-occurrence points. `positive_filter` below keeps the 85 confirmed gold
      occurrences as positives; the 55 asbestos/chromium workings stay in the set because they
      still widen the background exclusion (near-deposit ground of any kind is not country rock).
    uri: { param: occurrences_uri }
    mechanism: local-file

  # ── SAMPLE: the labeled training table (was SPEC-GAP #1) ──────────────────────────────────
  compute/training-table:
    type: table
    description: >
      One row per sampled point: the 64-D embedding (3x3 window mean) plus `class` (1 = gold
      occurrence, 0 = drawn background), `lon`/`lat` and `origin`. Rows that read a non-finite
      embedding are dropped, so the row count is the honest sample size.
    compute:
      op: raster_sample_points
      inputs:
        raster: { layer: source/embeddings }
        points: { layer: occurrences }
      params:
        window_px: { param: reference_window_px }
        label_field: class
        positive_filter: { param: positive_filter }
        background:
          n: { param: background_n }
          exclude_m: { param: background_exclude_m }
          bbox: { param: belt_bbox }
          seed: { param: background_seed }

  # ── VALIDATE: the held-out skill (was SPEC-GAP #2) ────────────────────────────────────────
  compute/loo-validation:
    type: table
    description: >
      Leave-one-out over the 85 gold occurrences: hold one out, refit on the other 84 + all
      background, score it, record its percentile against that model's background distribution.
      `loo_auc` is held-out gold vs background (0.5 = chance). THIS is what makes the site score
      readable; without it the resemblance map means nothing.
    compute:
      op: point_classifier_loo_validate
      inputs:
        samples: { layer: compute/training-table }
      params:
        label_field: class
        n_trees: { param: rf_trees }
        max_depth: { param: rf_max_depth }
        min_samples_leaf: { param: rf_min_leaf }
        n_bins: { param: rf_bins }
        class_weight: balanced
        seed: { param: rf_seed }

  # ── MODEL ─────────────────────────────────────────────────────────────────────────────────
  compute/rf-model:
    type: table
    description: >
      The fitted forest, P(gold-like) over the 64-D embedding: gold(1) vs background(0), balanced
      class weights. Same hyperparameters as the validation above — a model whose skill was
      measured under different settings would be a different model.
    compute:
      op: point_classifier_train
      inputs:
        samples: { layer: compute/training-table }
      params:
        label_field: class
        n_trees: { param: rf_trees }
        max_depth: { param: rf_max_depth }
        min_samples_leaf: { param: rf_min_leaf }
        n_bins: { param: rf_bins }
        class_weight: balanced
        seed: { param: rf_seed }

  # ── SITE SCORING ──────────────────────────────────────────────────────────────────────────
  source/site-embeddings:
    type: raster
    description: >
      The same AEF read over the site AOI (the Zvishavane claim on the Great Dyke ultramafic
      margin, ~11 km west of the nearest greenstone-belt gold), same product, same year, same
      80 m step — a second windowed read of the same tiles, not a crop of the belt layer.
    compute:
      op: embedding_field
      params:
        uri: { param: embeddings_dataset }
        bbox: { param: site_bbox }
        year: { param: embeddings_year }
        resolution: { param: embeddings_resolution }

  compute/site-resemblance:
    type: raster
    description: >
      Random-forest P(gold-like) over the site AOI at 80 m — the embedding-analog resemblance
      field. Brighter = this ground's 64-D signature looks more like the ground at the known gold
      sites. RESEMBLANCE DIAGNOSTIC, not detection. Read it next to `loo_auc`.
    compute:
      op: point_classifier_predict
      inputs:
        raster: { layer: source/site-embeddings }
        model: { layer: compute/rf-model }
      params:
        output: probability
        positive_class: 1
    style:
      palette: magma
      clim: [0, 1]

outputs:
  site_resemblance:
    layer: compute/site-resemblance
    value_kind: raster
    description: The site P(gold-like) resemblance raster at 80 m — THE product.
  loo_validation:
    layer: compute/loo-validation
    value_kind: table
    description: Held-out skill — per-fold scores + the AUC. Cite the score only with this.
  training_table:
    layer: compute/training-table
    value_kind: table
    description: The labeled reference + background embedding matrix (show-your-work).
  rf_model:
    layer: compute/rf-model
    value_kind: table
    description: The fitted model artifact (show-your-work).

extensions:
  folia/category: prospectivity
  folia/themes: [minerals, embeddings]
  folia/family: embedding-analog-resemblance
  folia/type: reproduction          # ADR-0081 D2 — indexed under @folia/reproductions/
  folia/cites_papers: []            # a method arm, not a reproduced paper
  folia/reference_implementation:
    script: research/mineral-prospecting/embedding-analog/prospectivity_aef.py
    run_record: research/mineral-prospecting/embedding-analog/outputs/loo_results.json
    reported_skill: >
      Mberengwa leave-one-out AUC 0.92-0.96 against a 0.75 floor — 0.9608 measured by this
      reference script (native-UTM sampling, numpy draw, sklearn) and 0.9253 by the engine
      recipe itself, both on 85 gold occurrences. The band, not either endpoint, is the claim;
      validations/comparison.yaml attributes every step of the difference.
  folia/site_template:
    docs: research/mineral-prospecting/embedding-analog/sites/README.md
    template: research/mineral-prospecting/embedding-analog/sites/SITE_SPEC_TEMPLATE.yaml
    launcher: research/mineral-prospecting/embedding-analog/run_recipe.py
    description: >
      Fill a site spec, run one command; a second site is a parameter change, not a script edit.
  folia/license:
    recipe: Apache-2.0
    output_dataset: CC-BY-4.0
    upstream_data:
      - { source: "@google-deepmind/aef/v1/annual", license: CC-BY-4.0 }
      - { source: "Bartholomew (1990) ZGS MRS 23 via Blenkinsop (2011)", license: CC-BY }
