From 5e383283411e8103cf73950a2b86b966c41f8adc Mon Sep 17 00:00:00 2001
From: Peter Powers <pmpowers@usgs.gov>
Date: Fri, 28 Mar 2025 14:59:11 -0600
Subject: [PATCH] support for planar loader

---
 .../earthquake/nshmp/model/GridLoader.java    | 198 +++++++++++++++++-
 1 file changed, 194 insertions(+), 4 deletions(-)

diff --git a/src/main/java/gov/usgs/earthquake/nshmp/model/GridLoader.java b/src/main/java/gov/usgs/earthquake/nshmp/model/GridLoader.java
index 11a1a09d..ca45dd3f 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/model/GridLoader.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/model/GridLoader.java
@@ -4,6 +4,7 @@ import static com.google.common.base.Preconditions.checkState;
 import static gov.usgs.earthquake.nshmp.model.Deserialize.MFD_TREE;
 import static java.lang.Math.log10;
 import static java.lang.Math.pow;
+import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toUnmodifiableList;
 
 import java.nio.file.Path;
@@ -65,8 +66,13 @@ class GridLoader {
     static final String LON = "lon";
     static final String LAT = "lat";
     static final String DEPTH = "depth";
-    static final String STRIKE = "strike";
+    static final String MAG = "m";
     static final String RATE = "rate";
+    static final String STRIKE = "strike";
+    static final String DIP = "dip";
+    static final String UPPER_DEPTH = "zTor";
+    static final String LOWER_DEPTH = "zBor";
+    static final String LENGTH = "length";
     static final String NAME = "name";
     static final String PDF = "pdf";
     static final String MMAX = "mMax";
@@ -82,8 +88,13 @@ class GridLoader {
       String mfdTreeKey,
       ModelData data) {
 
-    SlabLoader loader = new SlabLoader(id, name, featureId, mfdTreeKey, data);
-    return loader.createSlabRuptureSet();
+    if (data.spatialPdf()) {
+      SlabLoader loader = new SlabLoader(id, name, featureId, mfdTreeKey, data);
+      return loader.createSlabRuptureSet();
+    }
+    PlanarLoader loader = new PlanarLoader(id, name, featureId, mfdTreeKey, data);
+    return loader.createRuptureSet();
+
   }
 
   static GridRuptureSet createGridRuptureSet(
@@ -107,6 +118,183 @@ class GridLoader {
     return loader.createRuptureSet();
   }
 
+  /*
+   * Explicit INCR MFD with one line per magnitude and necessary data to realize
+   * each rupture as a finite, planar surface. Introduced for PRVI NSHM for
+   * subduction interface gridded seismicity.
+   */
+  private static class PlanarLoader {
+
+    private final int id;
+    private final String name;
+    private final ModelData data;
+    private final SourceFeature.Grid feature;
+    private final Path mfdsPath;
+    private final String mfdTreeKey;
+    private final FeatureData featureData;
+
+    private PlanarLoader(
+        int id,
+        String name,
+        int featureId,
+        String mfdTreeKey,
+        ModelData data) {
+
+      this.id = id;
+      this.name = name;
+      this.data = data;
+      this.feature = data.gridFeatureMap().orElseThrow().get(featureId);
+      this.mfdsPath = data.gridRateFile().orElseThrow();
+      this.mfdTreeKey = mfdTreeKey;
+      this.featureData = new FeatureData();
+      processNodes();
+    }
+
+    private SlabRuptureSet createRuptureSet() {
+      return SlabRuptureSet.builder()
+          .name(name)
+          .id(id)
+          .modelData(data)
+          .feature(feature)
+          .nodeData(
+              featureData.locations,
+              featureData.mfds,
+              featureData.mfdsTree,
+              featureData.ruptureDataLists)
+          .build();
+    }
+
+    private void processNodes() {
+
+      DelimitedData ratefile = DelimitedData.comma(mfdsPath);
+
+      /* Because we have explicit MFDs, we expect a single branch mfd-tree. */
+      LogicTree<Mfd.Properties> mfdTree = data.mfdMap().orElseThrow().get(mfdTreeKey);
+      checkState(mfdTree.size() == 1);
+      Mfd.Properties mfdProps = mfdTree.get(0).value();
+
+      List<Record> records;
+      try (Stream<Record> s = ratefile.records()) {
+        records = s.collect(toList());
+      }
+      List<List<Record>> nodeRecordsList = splitRecords(records);
+      // process record group for each grid node
+      for (List<Record> nodeRecords : nodeRecordsList) {
+        processRecords(nodeRecords, mfdProps);
+      }
+
+      featureData.mfdsTree = LogicTree.singleton(
+          feature.name,
+          mfdTree.get(0).id(),
+          featureData.mfds);
+    }
+
+    /*
+     * Break records into groups by node; method assumes no repeating lat-lons
+     * with different depths
+     */
+    private List<List<Record>> splitRecords(List<Record> records) {
+      List<List<Record>> nodeRecordLists = new ArrayList<>();
+      List<Record> nodeRecords = new ArrayList<>();
+      for (int i = 0; i < records.size(); i++) {
+        Record cRecord = records.get(i);
+        String cLon = cRecord.get(Key.LON);
+        String cLat = cRecord.get(Key.LAT);
+        if (i == records.size() - 1) {
+          nodeRecords.add(cRecord);
+          nodeRecordLists.add(nodeRecords);
+        } else {
+          nodeRecords.add(cRecord);
+          // check next record and possibly reset list
+          Record nRecord = records.get(i + 1);
+          String nLon = nRecord.get(Key.LON);
+          String nLat = nRecord.get(Key.LAT);
+          if (!nLon.equals(cLon) || !nLat.equals(cLat)) {
+            nodeRecordLists.add(nodeRecords);
+            nodeRecords = new ArrayList<>();
+          }
+        }
+      }
+      return nodeRecordLists;
+    }
+
+    private void processRecords(List<Record> records, Mfd.Properties incrMfdProps) {
+
+      Location loc = Location.create(
+          records.get(0).getDouble(Key.LON),
+          records.get(0).getDouble(Key.LAT),
+          records.get(0).getDouble(Key.DEPTH));
+
+      if (!feature.contains(loc)) {
+        // TODO clean
+        System.out.println("Point outside poly");
+        return;
+      }
+      featureData.locations.add(loc);
+
+      Mfd.Builder mfdBuilder = incrMfdProps.toBuilder();
+      IncrRates incrRates = new IncrRates(records);
+      mfdBuilder.transform(incrRates::set);
+      featureData.mfds.add(mfdBuilder.build());
+
+      List<RuptureData> rupDataList = new ArrayList<>();
+      for (int i = 0; i < records.size(); i++) {
+        Record r = records.get(i);
+        RuptureData rupDat = new RuptureData(
+            r.getDouble(Key.STRIKE),
+            r.getDouble(Key.DIP),
+            r.getDouble(Key.UPPER_DEPTH),
+            r.getDouble(Key.LOWER_DEPTH),
+            r.getDouble(Key.LENGTH));
+        rupDataList.add(rupDat);
+      }
+      featureData.ruptureDataLists.add(rupDataList);
+    }
+
+    private static class IncrRates {
+
+      final List<Record> records;
+
+      IncrRates(List<Record> records) {
+        this.records = records;
+      }
+
+      /*
+       * Using XyPoint.index() makes the assumption that there is agreement in
+       * magnitude range and ascending order. The number of records for each
+       * node can be less than the
+       */
+      //
+      void set(XyPoint mBin) {
+        int index = mBin.index();
+        if (index >= records.size()) {
+          return;
+        }
+        // Record record = records.get(index);
+        mBin.set(records.get(index).getDouble(Key.RATE));
+        double m = records.get(index).getDouble(Key.MAG);
+        checkState(m == mBin.x(), "Magnitudes don't match; mfd: %s  record: %s", mBin.x(), m);
+      }
+    }
+  }
+
+  static class RuptureData {
+
+    final double strike;
+    final double dip;
+    final double zTor;
+    final double zBor;
+    final double length;
+
+    RuptureData(double strike, double dip, double zTor, double zBor, double length) {
+      this.strike = strike;
+      this.dip = dip;
+      this.zTor = zTor;
+      this.zBor = zBor;
+      this.length = length;
+    }
+  }
+
   /* Explicit INCR MFD (fault systems, e.g. UCERF3). */
   private static class IncrLoader {
 
@@ -350,7 +538,8 @@ class GridLoader {
           .nodeData(
               featureData.locations,
               featureData.mfds,
-              featureData.mfdsTree);
+              featureData.mfdsTree,
+              featureData.ruptureDataLists);
       return b.build();
     }
 
@@ -443,6 +632,7 @@ class GridLoader {
     /* transposed and combined data */
     LogicTree<List<Mfd>> mfdsTree;
     List<Mfd> mfds = new ArrayList<>();
+    List<List<RuptureData>> ruptureDataLists = new ArrayList<>();
   }
 
   /* Truncate special GR cases; e.g. WUS double counting. */
-- 
GitLab