diff --git a/src/main/java/gov/usgs/earthquake/nshmp/fault/FocalMech.java b/src/main/java/gov/usgs/earthquake/nshmp/fault/FocalMech.java
index b066ecf3ce53a0808eb1e8001f9325ceeea64428..a34747bc3e79f50cd5fdc0bb01a83ac11e150a49 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/fault/FocalMech.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/fault/FocalMech.java
@@ -27,6 +27,15 @@ public enum FocalMech {
     this.rake = rake;
   }
 
+  /**
+   * Returns the focal mechanism for the supplied rake. Divisions are on 45°
+   * diagonals.
+   */
+  public static FocalMech fromRake(double rake) {
+    return (rake >= 45 && rake <= 135) ? REVERSE
+        : (rake >= -135 && rake <= -45) ? NORMAL : STRIKE_SLIP;
+  }
+
   /**
    * Returns a 'standard' dip value for this mechanism.
    * @return the dip
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/model/ClusterRuptureSet.java b/src/main/java/gov/usgs/earthquake/nshmp/model/ClusterRuptureSet.java
index d5a4e8d1d11b02a841cf5b44348f68faf6e02c5e..a7576d800c284f56c66ca7102b3443e0af11d1c5 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/model/ClusterRuptureSet.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/model/ClusterRuptureSet.java
@@ -222,8 +222,6 @@ public class ClusterRuptureSet implements RuptureSet {
       faultRuptureSets = new ArrayList<>();
       for (FaultRuptureSet.Builder frsb : faultRuptureSetBuilders) {
         frsb.modelData(data);
-        frsb.featureMap(featureMap);
-
         faultRuptureSets.add(frsb.build());
       }
 
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/model/Deserialize.java b/src/main/java/gov/usgs/earthquake/nshmp/model/Deserialize.java
index 0a67ca867abd701ff5616f21cebef3e3fd0bbcf7..b13027fdf9291e99a8660add6d3610acdd163bd3 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/model/Deserialize.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/model/Deserialize.java
@@ -238,13 +238,18 @@ class Deserialize {
   }
 
   /* Create a rupture set builder, initialized with data from file. */
-  static FaultRuptureSet.Builder ruptureSet(Path json, ModelData data) {
+  static FaultRuptureSet.Builder faultRuptureSet(
+      Path json,
+      ModelData data) {
+
     JsonObject obj = jsonObject(json);
-    return ruptureSet(obj, data);
+    return faultRuptureSet(obj, data);
   }
 
   /* Create a rupture set builder, initialized with data from file. */
-  private static FaultRuptureSet.Builder ruptureSet(JsonObject obj, ModelData data) {
+  private static FaultRuptureSet.Builder faultRuptureSet(
+      JsonObject obj,
+      ModelData data) {
 
     FaultRuptureSet.Builder ruptureSet = FaultRuptureSet.builder();
 
@@ -267,7 +272,9 @@ class Deserialize {
   }
 
   /* Create a rupture set builder, initialized with data from file. */
-  static ClusterRuptureSet.Builder clusterSet(Path json, ModelData data) {
+  static ClusterRuptureSet.Builder clusterRuptureSet(
+      Path json,
+      ModelData data) {
 
     JsonObject obj = jsonObject(json);
     ClusterRuptureSet.Builder clusterSet = ClusterRuptureSet.builder();
@@ -277,7 +284,7 @@ class Deserialize {
 
     JsonArray ruptures = obj.get(RUPTURE_SETS).getAsJsonArray();
     for (JsonElement rupture : ruptures) {
-      FaultRuptureSet.Builder ruptureSet = ruptureSet(rupture.getAsJsonObject(), data);
+      FaultRuptureSet.Builder ruptureSet = faultRuptureSet(rupture.getAsJsonObject(), data);
       clusterSet.addRuptureSet(ruptureSet);
     }
 
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/model/FaultRuptureSet.java b/src/main/java/gov/usgs/earthquake/nshmp/model/FaultRuptureSet.java
index c5a872b6a3be5952b57cc29bc7e1ae4d4ee196da..114ef37c2efb511be9d7be89ebaa7dc1cf536b7d 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/model/FaultRuptureSet.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/model/FaultRuptureSet.java
@@ -3,21 +3,16 @@ package gov.usgs.earthquake.nshmp.model;
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
-import static gov.usgs.earthquake.nshmp.Text.checkName;
 import static gov.usgs.earthquake.nshmp.model.SourceType.FAULT;
 import static java.util.stream.Collectors.collectingAndThen;
 import static java.util.stream.Collectors.toList;
-import static java.util.stream.Collectors.toUnmodifiableList;
 
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 import java.util.stream.Collectors;
 
 import gov.usgs.earthquake.nshmp.Earthquakes;
 import gov.usgs.earthquake.nshmp.Faults;
-import gov.usgs.earthquake.nshmp.data.XyPoint;
 import gov.usgs.earthquake.nshmp.data.XySequence;
 import gov.usgs.earthquake.nshmp.fault.surface.DefaultGriddedSurface;
 import gov.usgs.earthquake.nshmp.fault.surface.GriddedSurface;
@@ -32,70 +27,46 @@ import gov.usgs.earthquake.nshmp.tree.Branch;
 import gov.usgs.earthquake.nshmp.tree.LogicTree;
 
 /**
- * Fault source representation. This class wraps a model of a fault geometry and
- * a list of magnitude frequency distributions that characterize how the fault
- * might rupture (e.g. as one, single geometry-filling event, or as multiple
- * smaller events) during earthquakes. Smaller events are modeled as 'floating'
- * ruptures; they occur in multiple locations on the fault surface with
- * appropriately scaled rates.
- *
- * <p>A {@code FaultSource} cannot be created directly; it may only be created
- * by a private parser.
+ * Crustal finite fault rupture set.
  *
  * @author U.S. Geological Survey
  */
 public class FaultRuptureSet implements RuptureSet {
 
-  final String name;
-  final int id;
+  final SourceFeature.NshmFault feature;
+  final ModelData data;
 
   final LogicTree<Mfd> mfdTree;
+  // final Mfd mfdTotal;
 
-  @Deprecated
-  final LogicTree<Mfd.Properties> mfdPropsTree;
-  @Deprecated
-  XySequence mfdTotal;
+  final List<Integer> sectionIds; // reference: actually needed?
 
-  final List<Integer> sections;
-  final SourceFeature.NshmFault feature;
   final GriddedSurface surface;
 
-  // private final List<List<Rupture>> ruptureLists; // 1:1 with Mfds
-
-  @Deprecated // shouldn't need field once a FRS is a source
-  final SourceConfig.Fault config;
-
   FaultRuptureSet(Builder builder) {
-
-    this.name = builder.name;
-    this.id = builder.id;
+    this.feature = builder.feature;
+    this.data = builder.data;
 
     this.mfdTree = builder.mfdTree;
-    this.mfdPropsTree = builder.mfdPropsTreeOut;
-    // this.mfdTree = builder.mfdTree;
-    // this.mfdTotal = builder.mfdTotal;
-
-    this.config = builder.config;
-    this.sections = builder.sections;
-    this.feature = builder.feature;
+    this.sectionIds = builder.sectionIds;
     this.surface = builder.surface;
-
-    // this.ruptureLists = builder.ruptureLists;
   }
 
   @Override
   public String name() {
-    return name;
+    return feature.name;
   }
 
   @Override
-  public int size() {
-    return 0;
+  public int id() {
+    return feature.id;
   }
 
   @Override
-  public int id() {
-    return id;
+  public int size() {
+    // TODO what should this be ??
+    // a value based on the size of the Mfds??
+    return 1;
   }
 
   @Override
@@ -112,22 +83,11 @@ public class FaultRuptureSet implements RuptureSet {
     return Locations.closestPoint(site, feature.trace);
   }
 
-  // TODO this needs significant improvement; the mfd tree size
-  // is the product of the props tree and rate tree,
-  // if present; when FRS implements source should
-  // be able to deal with logic trees
-  @Deprecated
-  public List<Mfd.Properties> mfdProps() {
-    return mfdPropsTree.branches().stream()
-        .map(Branch::value)
-        .collect(toUnmodifiableList());
-  }
-
   @Override
   public String toString() {
     return getClass().getSimpleName() + " " + Map.of(
-        "name", name,
-        "id", id,
+        "name", name(),
+        "id", id(),
         "feature", feature.source.toJson());
   }
 
@@ -140,46 +100,37 @@ public class FaultRuptureSet implements RuptureSet {
 
     private boolean built = false;
 
+    private SourceFeature.NshmFault feature;
     private ModelData data;
-    private LogicTree<Mfd.Properties> mfdPropsTree;
-    private Optional<String> mfdTreeKey = Optional.empty();
 
-    // required
-    private String name;
-    private Integer id;
-    private List<Integer> sections;
-    private Map<Integer, SourceFeature.NshmFault> featureMap;
-    private SourceFeature.NshmFault feature;
+    private LogicTree<Mfd.Properties> mfdPropsTree;
+    private LogicTree<Mfd.Properties> mfdPropsTreeOut;
 
     /* created on build */
     private LogicTree<Mfd> mfdTree;
     private GriddedSurface surface;
 
-    @Deprecated
-    private LogicTree<Mfd.Properties> mfdPropsTreeOut;
-
-    private SourceConfig.Fault config;
+    /*
+     * Name, id, and and sectionIds Validated and carried forward in rupture set
+     * feature.
+     */
+    private String name;
+    private Integer id;
+    private List<Integer> sectionIds;
 
     Builder name(String name) {
-      this.name = checkName(name, "FaultRuptureSet");
+      this.name = name;
       return this;
     }
 
     Builder id(int id) {
-      checkArgument(id > 0, "ID [%s] < 1", id);
       this.id = id;
       return this;
     }
 
-    /* Set the section IDs; optional. */
     Builder sections(List<Integer> sections) {
       checkArgument(sections.size() >= 1);
-      this.sections = List.copyOf(sections);
-      return this;
-    }
-
-    Builder featureMap(Map<Integer, SourceFeature.NshmFault> featureMap) {
-      this.featureMap = Map.copyOf(featureMap);
+      this.sectionIds = List.copyOf(sections);
       return this;
     }
 
@@ -195,38 +146,36 @@ public class FaultRuptureSet implements RuptureSet {
       return this;
     }
 
+    FaultRuptureSet build() {
+      validateAndInit("FaultRuptureSet.Builder");
+      return new FaultRuptureSet(this);
+    }
+
     private void validateAndInit(String label) {
       checkState(!built, "Single use builder");
-      checkNotNull(name, "%s name", label);
-      checkNotNull(id, "%s geometry ID", label);
-      checkNotNull(data, "%s model reference data", label);
+      checkNotNull(sectionIds, "%s section ID list", label);
+      checkNotNull(data, "%s model data", label);
 
-      if (mfdTreeKey.isPresent()) {
-        String key = mfdTreeKey.get();
-        mfdPropsTree = data.mfdMap().get().get(key);
-      }
-      checkNotNull(mfdPropsTree, "%s MFD tree", label);
+      checkNotNull(name, "%s name", label);
+      checkNotNull(id, "%s id", label);
+      checkNotNull(mfdPropsTree, "%s MFD logic tree", label);
 
       mfdTree = buildMfdTree(data, mfdPropsTree);
       // mfdTotal = buildTotalMfd(mfdTree);
 
       mfdPropsTreeOut = buildMfdPropsTree(data, mfdPropsTree);
 
-      checkNotNull(sections, "%s section ID list", label);
-      checkNotNull(featureMap, "%s feature map", label);
-      checkState(featureMap.keySet().containsAll(sections));
+      // if feature.rateMap
+      feature = createFeature();
 
-      if (sections.size() > 1) {
-        Feature f = joinFeatures(sections.stream()
-            .map(featureMap::get)
-            .map(feature -> feature.source)
-            .collect(toList()));
-        feature = new SourceFeature.NshmFault(f);
-      } else {
-        feature = featureMap.get(sections.get(0));
-      }
+      System.out.println();
+      System.out.println(mfdPropsTree);
+      System.out.println(feature);
+      System.out.println(feature.rateMap);
+      System.out.println();
+      System.out.println();
 
-      config = (SourceConfig.Fault) data.sourceConfig();
+      SourceConfig.Fault config = (SourceConfig.Fault) data.sourceConfig();
       surface = DefaultGriddedSurface.builder()
           .trace(feature.trace)
           .depth(feature.upperDepth)
@@ -238,26 +187,46 @@ public class FaultRuptureSet implements RuptureSet {
           .spacing(config.surfaceSpacing)
           .build();
 
-      // ruptureLists = initRuptureLists();
-      // TODO reenable
-      // checkState(Iterables.size(Iterables.concat(ruptureLists)) > 0,
-      // "%s has no ruptures", label);
-
       built = true;
     }
 
-    FaultRuptureSet build() {
-      validateAndInit("FaultRuptureSet.Builder");
-      return new FaultRuptureSet(this);
+    private SourceFeature.NshmFault createFeature() {
+      Map<Integer, SourceFeature.NshmFault> featureMap =
+          data.faultFeatureMap().orElseThrow();
+      checkState(featureMap.keySet().containsAll(sectionIds));
+
+      Feature f = joinFeatures(sectionIds.stream()
+          .map(featureMap::get)
+          .map(feature -> feature.source)
+          .collect(toList()));
+      return new SourceFeature.NshmFault(f);
     }
 
+    /*
+     * TODO: consider short citcuiting singletons if feature as represented in
+     * model is consistent with rupture set requirements
+     *
+     * TODO: Split into 3 features for normal faults; this may happen prior to
+     * getting here
+     *
+     * TODO: Copy properties from multi segment ruptures (mostly 2008 model)
+     *
+     * TODO: For documentaiton; there are many options avilable when defining
+     * both sfautl and grid based sources that may necessarily be able to be
+     * combined. For example, normal fault dip variants are difficult to handle
+     * in the context of a cluster model.
+     */
+
     /*
      * Joins the traces in the supplied features into a new Feature with the
-     * properties of the last sfeature supplied.
+     * properties of the first feature supplied.
      */
     private Feature joinFeatures(List<Feature> features) {
-      if (features.size() == 1) {
-        return features.get(0);
+      Feature.Builder feature = (features.size() == 1)
+          ? Feature.copyOf(features.get(0))
+          : joinFaultSections(features);
+      if (features.size() > 1) {
+
       }
       Feature last = features.get(features.size() - 1);
       Map<String, ?> props = Properties.fromFeature(last).build();
@@ -272,6 +241,7 @@ public class FaultRuptureSet implements RuptureSet {
      * identical points at the ends of sections.
      */
     private static Feature.Builder joinFaultSections(List<Feature> features) {
+      // TODO copy properties of first feature into builder
       LocationList joined = features.stream()
           .map(Feature::asLineString)
           .flatMap(LocationList::stream)
@@ -281,58 +251,13 @@ public class FaultRuptureSet implements RuptureSet {
               LocationList::copyOf));
       return Feature.lineString(joined);
     }
-
-    // see FaultSource
-    @Deprecated
-    private List<List<Rupture>> initRuptureLists() {
-      List<List<Rupture>> rupLists = new ArrayList<>();
-
-      List<Mfd> mfds = mfdTree.branches().stream()
-          .map(Branch::value)
-          .collect(toList());
-      for (Mfd mfd : mfds) {
-        List<Rupture> rupList = createRuptureList(mfd);
-        // TODO reenable
-        // checkState(rupList.size() > 0, "Rupture list is empty");
-        rupLists.add(rupList);
-      }
-      return List.copyOf(rupLists);
-    }
-
-    // see FaultSource
-    @Deprecated
-    private List<Rupture> createRuptureList(Mfd mfd) {
-      List<Rupture> rupList = new ArrayList<>();
-      for (XyPoint xy : mfd.data()) {
-        double mag = xy.x();
-        double rate = xy.y();
-
-        // TODO necessary? derive from config and apply elsewhere
-        if (rate < 1e-14) {
-          continue; // shortcut low rates
-        }
-
-        // TODO revisit, floating rupture behavior controlled by
-        // magnitude and RuptureFloating; OFF forces full rupture
-        // at whatever magnitude
-
-        // former singleton rupture TODO clean
-        // Rupture rup = Rupture.create(mag, rate, feature.rake, surface);
-        // rupListbuilder.add(rup);
-
-        List<Rupture> floaters = config.ruptureFloating.createFloatingRuptures(
-            surface, config.ruptureScaling, mag, rate, feature.rake, false);
-        rupList.addAll(floaters);
-
-      }
-      return List.copyOf(rupList);
-    }
   }
 
   private static LogicTree<Mfd.Properties> buildMfdPropsTree(
       ModelData mfdData,
       LogicTree<Mfd.Properties> mfdPropertiesTree) {
 
+    // Not true, def model branches have epitemic uncertainty applied
     // shouldn't have rate-tree AND epistemic uncertainty, both
     // of which increase the number of branches
     if (mfdData.rateTree().isPresent()) {
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/model/HazardModel.java b/src/main/java/gov/usgs/earthquake/nshmp/model/HazardModel.java
index da914d18d018e0758a8023d076fb1f6bcf50d5b6..5bdd233b510cea47388ec791fdbc15c33ec30ee1 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/model/HazardModel.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/model/HazardModel.java
@@ -294,22 +294,25 @@ public final class HazardModel implements Iterable<SourceSet<? extends Source>>
         FaultRuptureSet frs,
         double leafWeight) {
 
+      SourceFeature.NshmFault feature = frs.feature;
+      SourceConfig.Fault config = (SourceConfig.Fault) frs.data.sourceConfig();
+
       return new FaultSource.Builder()
-          .name(frs.name)
-          .id(frs.id)
-          .trace(frs.feature.trace)
-          .dip(frs.feature.dip)
+          .name(feature.name)
+          .id(feature.id)
+          .trace(feature.trace)
+          .dip(feature.dip)
           .width(Faults.width(
-              frs.feature.dip,
-              frs.feature.upperDepth,
-              frs.feature.lowerDepth))
-          .depth(frs.feature.upperDepth)
-          .rake(frs.feature.rake)
+              feature.dip,
+              feature.upperDepth,
+              feature.lowerDepth))
+          .depth(feature.upperDepth)
+          .rake(feature.rake)
           .mfdTree(frs.mfdTree)
-          .surfaceSpacing(frs.config.surfaceSpacing)
-          .ruptureScaling(frs.config.ruptureScaling)
-          .ruptureFloating(frs.config.ruptureFloating)
-          .ruptureVariability(frs.config.ruptureVariability)
+          .surfaceSpacing(config.surfaceSpacing)
+          .ruptureScaling(config.ruptureScaling)
+          .ruptureFloating(config.ruptureFloating)
+          .ruptureVariability(config.ruptureVariability)
           .branchWeight(leafWeight)
           // List.of(Mfd.copyOf(frs.mfdTotal).scale(leafWeight).build()))
           .buildFaultSource();
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/model/InterfaceRuptureSet.java b/src/main/java/gov/usgs/earthquake/nshmp/model/InterfaceRuptureSet.java
index a070df8b4dfa79685ecb2d7919fbaf2cc77339b0..29ea7bb809728940a5752c5db9a991514ff56f3b 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/model/InterfaceRuptureSet.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/model/InterfaceRuptureSet.java
@@ -22,20 +22,19 @@ import gov.usgs.earthquake.nshmp.tree.Branch;
 import gov.usgs.earthquake.nshmp.tree.LogicTree;
 
 /**
- * Subduction interface rupture set implemention.
+ * Subduction interface rupture set.
  *
  * @author U.S. Geological Survey
  */
 public class InterfaceRuptureSet implements RuptureSet {
 
-  // TODO if single section, should be fault section; if stitched,
-  // should be new feature with id and name from reupture-set.json
+  /* May be original interface section or stitched. */
   final SourceFeature.Interface feature;
   final ModelData data;
 
   final LogicTree<Mfd> mfdTree;
 
-  final List<Integer> sections;// reference: actually needed?
+  final List<Integer> sectionIds;// reference: actually needed?
 
   final GriddedSurface surface;
 
@@ -44,7 +43,7 @@ public class InterfaceRuptureSet implements RuptureSet {
     this.data = builder.data;
 
     this.mfdTree = builder.mfdTree;
-    this.sections = builder.sections;
+    this.sectionIds = builder.sectionIds;
     this.surface = builder.surface;
   }
 
@@ -54,15 +53,15 @@ public class InterfaceRuptureSet implements RuptureSet {
   }
 
   @Override
-  public int size() {
-    // TODO what should this be for fault and interface rupture sets??
-    // a value based on the size of the Mfds??
-    return 1;
+  public int id() {
+    return feature.id;
   }
 
   @Override
-  public int id() {
-    return feature.id;
+  public int size() {
+    // TODO what should this be ??
+    // a value based on the size of the Mfds??
+    return 1;
   }
 
   @Override
@@ -87,8 +86,8 @@ public class InterfaceRuptureSet implements RuptureSet {
   @Override
   public String toString() {
     Map<Object, Object> data = Map.of(
-        "name", feature.name,
-        "id", feature.id,
+        "name", name(),
+        "id", id(),
         "feature", feature.source.toJson());
     return getClass().getSimpleName() + " " + data;
   }
@@ -104,7 +103,7 @@ public class InterfaceRuptureSet implements RuptureSet {
 
     private SourceFeature.Interface feature;
     private ModelData data;
-    private List<Integer> sections;
+    private List<Integer> sectionIds;
 
     private LogicTree<Mfd.Properties> mfdPropsTree;
 
@@ -112,7 +111,7 @@ public class InterfaceRuptureSet implements RuptureSet {
     private LogicTree<Mfd> mfdTree;
     private GriddedSurface surface;
 
-    /* These will be validated when creating rupture set feature. */
+    /* Validated and carried forward in rupture set feature. */
     private String name;
     private Integer id;
 
@@ -128,7 +127,7 @@ public class InterfaceRuptureSet implements RuptureSet {
 
     Builder sections(List<Integer> sections) {
       checkArgument(sections.size() >= 1);
-      this.sections = List.copyOf(sections);
+      this.sectionIds = List.copyOf(sections);
       return this;
     }
 
@@ -144,14 +143,18 @@ public class InterfaceRuptureSet implements RuptureSet {
       return this;
     }
 
+    InterfaceRuptureSet build() {
+      validateAndInit("InterfaceRuptureSet.Builder");
+      return new InterfaceRuptureSet(this);
+    }
+
     private void validateAndInit(String label) {
       checkState(!built, "Single use builder");
-      checkNotNull(sections, "%s section ID list", label);
+      checkNotNull(sectionIds, "%s section ID list", label);
       checkNotNull(data, "%s model data", label);
 
       checkNotNull(name, "%s name", label);
       checkNotNull(id, "%s id", label);
-
       checkNotNull(mfdPropsTree, "%s MFD logic tree", label);
 
       // TODO temp checking props are always the same;
@@ -175,16 +178,12 @@ public class InterfaceRuptureSet implements RuptureSet {
       built = true;
     }
 
-    InterfaceRuptureSet build() {
-      validateAndInit("InterfaceRuptureSet.Builder");
-      return new InterfaceRuptureSet(this);
-    }
-
     private SourceFeature.Interface createFeature() {
-      Map<Integer, SourceFeature.Interface> featureMap = data.interfaceFeatureMap().orElseThrow();
-      checkState(featureMap.keySet().containsAll(sections));
+      Map<Integer, SourceFeature.Interface> featureMap =
+          data.interfaceFeatureMap().orElseThrow();
+      checkState(featureMap.keySet().containsAll(sectionIds));
 
-      Feature f = joinFeatures(sections.stream()
+      Feature f = joinFeatures(sectionIds.stream()
           .map(featureMap::get)
           .map(feature -> feature.source)
           .collect(toList()));
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/model/ModelData.java b/src/main/java/gov/usgs/earthquake/nshmp/model/ModelData.java
index b532573df348b61ba83764bef5498a5ddaab4167..56875ddeef4871d3ffc76bfad8f0f9295fecc2ff 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/model/ModelData.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/model/ModelData.java
@@ -15,10 +15,6 @@ import gov.usgs.earthquake.nshmp.tree.LogicTree;
  * information used when parsing models and (2) static classes and methods used
  * when building MFDs.
  *
- * Although ModelLoader will require certain resources in specific locations
- * (e.g. the fault-sections directory), this class enforces that resources that
- * can only exist in one location are only set once per instance.
- *
  * @author U.S. Geological Survey
  */
 class ModelData {
@@ -121,7 +117,6 @@ class ModelData {
   }
 
   void interfaceFeatureMap(Map<Integer, SourceFeature.Interface> interfaceFeatureMap) {
-    checkState(this.interfaceFeatureMap.isEmpty());
     this.interfaceFeatureMap = Optional.of(checkNotNull(interfaceFeatureMap));
   }
 
@@ -130,7 +125,6 @@ class ModelData {
   }
 
   void faultFeatureMap(Map<Integer, SourceFeature.NshmFault> faultFeatureMap) {
-    checkState(this.faultFeatureMap.isEmpty());
     this.faultFeatureMap = Optional.of(checkNotNull(faultFeatureMap));
   }
 
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/model/ModelLoader.java b/src/main/java/gov/usgs/earthquake/nshmp/model/ModelLoader.java
index 317f0e8f2f3b35219a471d25440e6a7dc58b2086..176de270dbb03dcd7009a80a32738bda8049c963 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/model/ModelLoader.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/model/ModelLoader.java
@@ -6,8 +6,8 @@ import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
 import static gov.usgs.earthquake.nshmp.model.ModelFiles.CALC_CONFIG;
 import static gov.usgs.earthquake.nshmp.model.ModelFiles.CLUSTER_SET;
+import static gov.usgs.earthquake.nshmp.model.ModelFiles.FAULT_SOURCES;
 import static gov.usgs.earthquake.nshmp.model.ModelFiles.GRID_DATA_DIR;
-import static gov.usgs.earthquake.nshmp.model.ModelFiles.GRID_SOURCES;
 import static gov.usgs.earthquake.nshmp.model.ModelFiles.MODEL_INFO;
 import static gov.usgs.earthquake.nshmp.model.ModelFiles.RUPTURE_SET;
 import static gov.usgs.earthquake.nshmp.model.ModelFiles.SOURCE_TREE;
@@ -48,8 +48,11 @@ import com.google.common.collect.Iterables;
 import com.google.gson.JsonElement;
 
 import gov.usgs.earthquake.nshmp.calc.CalcConfig;
+import gov.usgs.earthquake.nshmp.fault.FocalMech;
+import gov.usgs.earthquake.nshmp.geo.json.Feature;
 import gov.usgs.earthquake.nshmp.geo.json.Properties;
 import gov.usgs.earthquake.nshmp.mfd.Mfd;
+import gov.usgs.earthquake.nshmp.model.SourceFeature.Key;
 import gov.usgs.earthquake.nshmp.tree.Branch;
 import gov.usgs.earthquake.nshmp.tree.LogicGroup;
 import gov.usgs.earthquake.nshmp.tree.LogicTree;
@@ -65,7 +68,7 @@ import gov.usgs.earthquake.nshmp.tree.LogicTree;
 abstract class ModelLoader {
 
   public static void main(String[] args) throws IOException {
-    Path testModel = Paths.get("../nshm-conus-2018");
+    Path testModel = Paths.get("../nshm-conus-2018-tmp");
     HazardModel model = ModelLoader.load(testModel);
     System.out.println(model);
   }
@@ -123,9 +126,9 @@ abstract class ModelLoader {
       Path dir) {
 
     switch (setting) {
-      // case ACTIVE_CRUST:
-      // case STABLE_CRUST:
-      // return loadCrustalSources(dir);
+      case ACTIVE_CRUST:
+        // case STABLE_CRUST:
+        return loadCrustalSources(dir);
       case SUBDUCTION_INTERFACE:
         return ModelLoader.interfaceSources(dir);
       // case SUBDUCTION_SLAB:
@@ -147,12 +150,12 @@ abstract class ModelLoader {
         if (!Files.isDirectory(path)) continue;
         String filename = checkNotNull(path.getFileName()).toString();
         switch (filename) {
-          // case FAULT_SOURCES:
-          // trees.addAll(ModelLoader.faultSources(path, gmms));
-          // break;
-          case GRID_SOURCES:
-            trees.addAll(ModelLoader.gridSources(path, gmms));
+          case FAULT_SOURCES:
+            trees.addAll(ModelLoader.faultSources(path, gmms));
             break;
+          // case GRID_SOURCES:
+          // trees.addAll(ModelLoader.gridSources(path, gmms));
+          // break;
           // case ZONE_SOURCES:
           // trees.addAll(ModelLoader.zoneSources(path, gmms));
           // break;
@@ -233,6 +236,10 @@ abstract class ModelLoader {
     /* (3) Lastly, recurse into any nested directories. */
     try (DirectoryStream<Path> nestedDirStream = newDirectoryStream(dir, NESTED_DIR_FILTER)) {
       for (Path path : nestedDirStream) {
+        // TODO temp clean
+        if (path.getFileName().toString().equals("ucerf3")) {
+          continue;
+        }
         trees.addAll(loadSourceDirectory(path, data));
       }
     } catch (IOException ioe) {
@@ -284,38 +291,111 @@ abstract class ModelLoader {
         Path geojson,
         ModelData data) {
 
+      SourceFeature.NshmFault feature = SourceFeature.newNshmFault(geojson);
+
+      /* Possibly split on normal fault dip variants. */
+      SourceConfig.Fault config = (SourceConfig.Fault) data.sourceConfig();
+      boolean isNormal = FocalMech.fromRake(feature.rake) == FocalMech.NORMAL;
+      if (config.normalDipTree.isPresent() && isNormal) {
+        System.out.println("dip tree: " + root.relativize(geojson));
+        return loadNormalDipTree(feature, data);
+      }
+
       System.out.println("  source: " + root.relativize(geojson));
-      SourceFeature.NshmFault fault = SourceFeature.newNshmFault(geojson);
+
+      // SourceFeature.NshmFault feature = SourceFeature.newNshmFault(geojson);
+      data.faultFeatureMap(Map.of(feature.id, feature));
+
+      // FaultRuptureSet ruptureSet = FaultRuptureSet.builder()
+      // .modelData(data)
+      // .name(feature.name)
+      // .id(feature.id)
+      // .sections(List.of(feature.id))
+      // .mfdTree(mfdTree)
+      // .build();
+
+      LogicTree<Path> root = LogicTree.singleton(geojson);
+      SourceTree.Builder treeBuilder = SourceTree.builder()
+          .name(feature.name)
+          .type(FAULT)
+          .gmms(data.gmms())
+          .root(root);
+
+      // .addLeaf(root.branches().get(0), ruptureSet)
+      // .build();
+
+      return loadSingleRuptureSet(
+          root.branches().get(0),
+          treeBuilder,
+          data,
+          feature);
+    }
+
+    private SourceTree loadSingleRuptureSet(
+        Branch<Path> branch,
+        SourceTree.Builder treeBuilder,
+        ModelData data,
+        SourceFeature.NshmFault feature) {
 
       // TODO dummy mfds for now
       Mfd.Properties.Single mfdProps = Mfd.Properties.Single.dummy();
-      // singleProps.m = 7.0;
-      // double rate = 0.001;
-      // Mfd.Properties mfdProps = new Mfd.Properties(Type.SINGLE, singleProps);
+      LogicTree<Mfd.Properties> mfdTree = LogicTree.singleton(mfdProps);
 
-      LogicTree<Mfd.Properties> mfdTree = LogicTree.<Mfd.Properties> builder()
-          .addBranch("singleton", mfdProps, 1.0)
-          .build();
+      data.faultFeatureMap(Map.of(feature.id, feature));
 
       FaultRuptureSet ruptureSet = FaultRuptureSet.builder()
           .modelData(data)
-          .name(fault.name)
-          .id(fault.id)
-          .sections(List.of(fault.id))
-          .featureMap(Map.of(fault.id, fault))
+          .name(feature.name)
+          .id(feature.id)
+          .sections(List.of(feature.id))
           .mfdTree(mfdTree)
           .build();
 
-      LogicTree<Path> root = LogicTree.singleton(geojson);
-      SourceTree tree = SourceTree.builder()
-          .name(fault.name)
+      return treeBuilder.addLeaf(branch, ruptureSet).build();
+    }
+
+    SourceTree loadNormalDipTree(
+        SourceFeature.NshmFault feature,
+        ModelData data) {
+
+      SourceConfig.Fault config = (SourceConfig.Fault) data.sourceConfig();
+
+      // Feature source = GeoJson.from(geojson).toFeature();
+      // Properties props = source.properties();
+      double dip = feature.dip;// props.getDouble(Key.DIP).orElseThrow();
+      String name = feature.name;// props.getString(Key.NAME).orElseThrow();
+
+      LogicTree<Double> dipTree = config.normalDipTree.orElseThrow();
+      LogicTree<Path> dipPathTree = ModelUtil.toPathTree(dipTree);
+
+      SourceTree.Builder treeBuilder = SourceTree.builder()
+          .name(name)
           .type(FAULT)
           .gmms(data.gmms())
-          .root(root)
-          .addLeaf(root.branches().get(0), ruptureSet)
-          .build();
+          .root(dipPathTree);
+
+      // SourceFeature.NshmFault feature = SourceFeature.newNshmFault(geojson);
+
+      // Properties.Builder props = Properties.fromFeature(source);
+
+      for (int i = 0; i < dipTree.size(); i++) {
+        // for (Branch<Double> dipBranch : dipTree) {
+        Branch<Double> dipBranch = dipTree.branches().get(i);
+        int branchDip = (int) (dip + dipBranch.value());
+        String branchName = String.format("%s [%s°]", name, dip);
+        Feature dipFeature = Feature.copyOf(feature.source)
+            .properties(Properties.fromFeature(feature.source)
+                .put(Key.NAME, name)
+                .put(Key.DIP, dip + dipBranch.value())
+                .build())
+            .build();
 
-      return tree;
+        // createSingleRuptureSet
+
+        processBranch(dipPathTree.branches().get(i), treeBuilder, data);
+      }
+
+      return treeBuilder.build();
     }
 
     @Override
@@ -325,8 +405,7 @@ abstract class ModelLoader {
 
       System.out.println("    tree: " + root.relativize(dir));
       LogicTree<Path> tree = readSourceTree(dir).orElseThrow();
-      Map<Integer, SourceFeature.NshmFault> featureMap = readFaultSections(dir).orElseThrow();
-      data.faultFeatureMap(featureMap);
+      data.faultFeatureMap(readFaultSections(dir).orElseThrow());
 
       SourceTree.Builder treeBuilder = SourceTree.builder()
           .name(checkNotNull(dir.getFileName()).toString())
@@ -373,25 +452,21 @@ abstract class ModelLoader {
       } else {
 
         Path ruptureSetPath = dir.resolve(RUPTURE_SET);
-        boolean isRuptureSet = Files.exists(ruptureSetPath);
+        boolean isFaultRuptureSet = Files.exists(ruptureSetPath);
 
-        Path setPath = isRuptureSet
+        Path setPath = isFaultRuptureSet
             ? ruptureSetPath
             : dir.resolve(CLUSTER_SET);
 
-        if (isRuptureSet) {
+        if (isFaultRuptureSet) {
 
-          FaultRuptureSet frs = Deserialize.ruptureSet(setPath, data)
-              .featureMap(data.faultFeatureMap().orElseThrow())
-              .modelData(data)
+          FaultRuptureSet frs = Deserialize.faultRuptureSet(setPath, data)
               .build();
           treeBuilder.addLeaf(branch, frs);
 
         } else {
 
-          ClusterRuptureSet crs = Deserialize.clusterSet(setPath, data)
-              .featureMap(data.faultFeatureMap().orElseThrow())
-              .modelData(data)
+          ClusterRuptureSet crs = Deserialize.clusterRuptureSet(setPath, data)
               .build();
 
           treeBuilder.addLeaf(branch, crs);
@@ -425,7 +500,7 @@ abstract class ModelLoader {
         ModelData data) {
 
       System.out.println("  source: " + root.relativize(geojson));
-      SourceFeature.Interface fault = SourceFeature.newInterfaceSection(geojson);
+      SourceFeature.Interface feature = SourceFeature.newInterfaceSection(geojson);
 
       // TODO dummy mfds for now TODO TODO TODO
       System.out.println(" -- STOP -- this isnt working correctly");
@@ -437,16 +512,15 @@ abstract class ModelLoader {
 
       InterfaceRuptureSet ruptureSet = InterfaceRuptureSet.builder()
           .modelData(data)
-          .name(fault.name)
-          .id(fault.id)
-          .sections(List.of(fault.id))
-          // .featureMap(Map.of(fault.id, fault))
+          .name(feature.name)
+          .id(feature.id)
+          .sections(List.of(feature.id))
           .mfdTree(mfdTree)
           .build();
 
       LogicTree<Path> root = LogicTree.singleton(geojson);
       SourceTree tree = SourceTree.builder()
-          .name(fault.name)
+          .name(feature.name)
           .type(INTERFACE)
           .gmms(data.gmms())
           .root(root)
@@ -463,8 +537,7 @@ abstract class ModelLoader {
 
       System.out.println("    tree: " + root.relativize(dir));
       LogicTree<Path> tree = readSourceTree(dir).orElseThrow();
-      Map<Integer, SourceFeature.Interface> featureMap = readInterfaceSections(dir).orElseThrow();
-      data.interfaceFeatureMap(featureMap);
+      data.interfaceFeatureMap(readInterfaceSections(dir).orElseThrow());
 
       SourceTree.Builder treeBuilder = SourceTree.builder()
           .name(checkNotNull(dir.getFileName()).toString())
@@ -512,7 +585,6 @@ abstract class ModelLoader {
         Path ruptureSetPath = dir.resolve(RUPTURE_SET);
 
         InterfaceRuptureSet irs = Deserialize.interfaceRuptureSet(ruptureSetPath, data)
-            // .featureMap(data.interfaceFeatureMap().orElseThrow())
             .build();
         treeBuilder.addLeaf(branch, irs);
       }
@@ -639,7 +711,7 @@ abstract class ModelLoader {
          */
 
         LogicTree<Path> rateTree = data.gridRateTree().orElseThrow();
-        LogicTree<Path> rateChildren = ModelUtil.rateToSourceTree(rateTree);
+        LogicTree<Path> rateChildren = ModelUtil.toPathTree(rateTree);
         treeBuilder.addBranches(branch, rateChildren);
 
         for (int i = 0; i < rateTree.branches().size(); i++) {
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/model/ModelUtil.java b/src/main/java/gov/usgs/earthquake/nshmp/model/ModelUtil.java
index 383abbe498c1721bde43cbfb1eab4658253d3455..d3cb43aebf5b5478caf746bc7448a6f47407d9ac 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/model/ModelUtil.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/model/ModelUtil.java
@@ -136,17 +136,47 @@ class ModelUtil {
         .toImmutable();
   }
 
+  // TODO clean
+  // /*
+  // * Convert grid rate-tree (of paths to *.csv files) to a tree of
+  // pseudo-paths
+  // * to mimic branches in a source-tree.
+  // */
+  // static LogicTree<Path> rateToPathTree(LogicTree<Path> rateTree) {
+  // LogicTree.Builder<Path> sourceTree = LogicTree.builder();
+  // rateTree.forEach(branch -> sourceTree.addBranch(
+  // branch.id(),
+  // Path.of(branch.id()),
+  // branch.weight()));
+  // return sourceTree.build();
+  // }
+  //
+  // /*
+  // * Convert normal fault dip-tree to a tree of pseudo-paths to mimic branches
+  // * in a source-tree.
+  // */
+  // static LogicTree<Path> dipToPathTree(LogicTree<Double> dipTree) {
+  // LogicTree.Builder<Path> sourceTree = LogicTree.builder();
+  // dipTree.forEach(branch -> sourceTree.addBranch(
+  // branch.id(),
+  // Path.of(branch.id()),
+  // branch.weight()));
+  // return sourceTree.build();
+  // }
+
   /*
-   * Convert grid rate-tree (of paths to *.csv files) to a tree of pseudo-paths
-   * to mimic branches in a source-tree.
+   * Convert any LogicTree to a tree of pseudo-paths based on the id() field of
+   * the supplied tree. This is used to mimic branches in a source-tree.
+   * Examples uses include conversion of a grid rate tree of paths to *.csv
+   * files or a fault slip variant tree.
    */
-  static LogicTree<Path> rateToSourceTree(LogicTree<Path> rateTree) {
-    LogicTree.Builder<Path> sourceTree = LogicTree.builder();
-    rateTree.forEach(branch -> sourceTree.addBranch(
+  static <T> LogicTree<Path> toPathTree(LogicTree<T> tree) {
+    LogicTree.Builder<Path> pathTree = LogicTree.builder();
+    tree.forEach(branch -> pathTree.addBranch(
         branch.id(),
         Path.of(branch.id()),
         branch.weight()));
-    return sourceTree.build();
+    return pathTree.build();
   }
 
   /*
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/model/SourceConfig.java b/src/main/java/gov/usgs/earthquake/nshmp/model/SourceConfig.java
index 672625b2130178c8ab114043b9917c71d6753b67..8692293c994fcb735c2a5b8bcfe88e1269cb6123 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/model/SourceConfig.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/model/SourceConfig.java
@@ -31,6 +31,14 @@ abstract class SourceConfig {
     this.resource = resource;
   }
 
+  Fault getAsFault() {
+    return (Fault) this;
+  }
+
+  Grid getAsGrid() {
+    return (Grid) this;
+  }
+
   static class Fault extends SourceConfig {
 
     final double surfaceSpacing;