diff --git a/src/main/java/gov/usgs/earthquake/nshmp/Faults.java b/src/main/java/gov/usgs/earthquake/nshmp/Faults.java
index 48122228992ba0e08201792f6de9675aad07d2b2..d1ede73fe9bb49465691b638008a5a5c42614425 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/Faults.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/Faults.java
@@ -138,15 +138,14 @@ public final class Faults {
     return zTor + Math.sin(dip * Maths.TO_RADIANS) * width / 2.0;
   }
 
-  public static double width(double dip, double zUpper, double zLower) {
-    return sinProjection(dip, zLower - zUpper);
-  }
-
-  public static double projectVerticalSlip(double dip, double slip) {
-    return sinProjection(dip, slip);
-  }
-
-  private static double sinProjection(double dip, double value) {
+  /**
+   * Project a vertically measured value (e.g. slip rate, depth) onto a dipping
+   * surface.
+   *
+   * @param dip of surface (i.e. a fault)
+   * @param value to project (vertical)
+   */
+  public static double projectVertical(double dip, double value) {
     return value / Math.sin(dip * Maths.TO_RADIANS);
   }
 
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/gmm/GmmInput.java b/src/main/java/gov/usgs/earthquake/nshmp/gmm/GmmInput.java
index d7b443f286da917e12d8b0ee2e9963a80fe368ea..e612c0c3b90eab5806259cb125acfdd3519011bc 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/gmm/GmmInput.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/gmm/GmmInput.java
@@ -568,14 +568,14 @@ public class GmmInput {
   @Override
   public String toString() {
     return MoreObjects.toStringHelper(this)
-        .add(MW.toString(), String.format("%.2f", Mw))
-        .add(RJB.toString(), String.format("%.3f", rJB))
-        .add(RRUP.toString(), String.format("%.3f", rRup))
-        .add(RX.toString(), String.format("%.3f", rX))
+        .add(MW.toString(), String.format("%.4f", Mw))
+        .add(RJB.toString(), String.format("%.6f", rJB))
+        .add(RRUP.toString(), String.format("%.6f", rRup))
+        .add(RX.toString(), String.format("%.6f", rX))
         .add(DIP.toString(), dip)
-        .add(WIDTH.toString(), String.format("%.3f", width))
+        .add(WIDTH.toString(), String.format("%.6f", width))
         .add(ZTOP.toString(), zTop)
-        .add(ZHYP.toString(), String.format("%.3f", zHyp))
+        .add(ZHYP.toString(), String.format("%.6f", zHyp))
         .add(RAKE.toString(), rake)
         .add(VS30.toString(), vs30)
         .add(VSINF.toString(), vsInf)
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/mfd/Mfd.java b/src/main/java/gov/usgs/earthquake/nshmp/mfd/Mfd.java
index 3378ad66b672cd06e7fac9af1d9fb73cb330513c..4696a965ac8cf107bf9b7d5dd22bd90d1dbbbae0 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/mfd/Mfd.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/mfd/Mfd.java
@@ -650,15 +650,27 @@ public final class Mfd {
       }
 
       /**
-       * Create a new properties object for a Gutenberg-Richter MFD with a copy
-       * of the supplied properties and an updated maximum magnitude.
+       * Create a new Gutenberg-Richter MFD properties object from a copy of the
+       * supplied properties and an updated maximum magnitude.
        *
        * @param gr properties to copy
-       * @param mMax updated maximum magnitude
+       * @param mMax maximum magnitude
        */
-      public GutenbergRichter(GutenbergRichter gr, double mMax) {
-        /* Supports common magnitude arrays for variable mMax MFDs */
-        this(gr.type(), gr.a, gr.b, gr.Δm, gr.mMin, mMax);
+      public static GutenbergRichter copyOfWithMax(GutenbergRichter gr, double mMax) {
+        /* Supports common magnitude arrays for variable mMax MFDs. */
+        return new GutenbergRichter(gr.type(), gr.a, gr.b, gr.Δm, gr.mMin, mMax);
+      }
+
+      /**
+       * Create a new Gutenberg-Richter MFD properties object from a copy of the
+       * supplied properties and an updated a-value.
+       *
+       * @param gr properties to copy
+       * @param a value
+       */
+      public static GutenbergRichter copyOfWithRate(GutenbergRichter gr, double a) {
+        /* Supports prescriptive a-values when mo-rate balancing. */
+        return new GutenbergRichter(gr.type(), a, gr.b, gr.Δm, gr.mMin, gr.mMax);
       }
 
       /** The Gutenberg-Richter {@code a}-value. */
@@ -699,7 +711,7 @@ public final class Mfd {
         double mMin = checkMagnitude(this.mMin);
         double mMax = checkMagnitude(this.mMax);
         double[] magnitudes = Sequences.arrayBuilder(mMin, mMax, this.Δm)
-            .scale(4)
+            .scale(5)
             .build();
         Builder builder = new Builder(this, magnitudes);
         builder.transform(p -> p.set(Mfds.gutenbergRichterRate(a, b, p.x())));
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 9f3243c56b468bad872723f834abc4b9449b25bf..84b8b0145bfd73757ed0b44a82085eb347aecc2e 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/model/Deserialize.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/model/Deserialize.java
@@ -359,6 +359,11 @@ class Deserialize {
     return clusterSet.build();
   }
 
+  /* Convenience method to use Deserialize.GSON instance external to class. */
+  static <T> T fromJson(JsonElement json, Type typeOfT) {
+    return GSON.fromJson(json, typeOfT);
+  }
+
   /* Create an mfd-tree from a JSON element or via map lookup. */
   private static LogicTree<Mfd.Properties> mfdTree(
       JsonElement mfdElement,
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 28fa26e0f32cf64650b45465c7a9172cdeec5dda..f2d829c0b8a7f3a5e591c6f1796b447435a7a1f1 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/model/FaultRuptureSet.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/model/FaultRuptureSet.java
@@ -14,6 +14,7 @@ import static java.util.stream.Collectors.toList;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.OptionalDouble;
 import java.util.stream.Stream;
 
 import gov.usgs.earthquake.nshmp.Earthquakes;
@@ -27,9 +28,12 @@ import gov.usgs.earthquake.nshmp.geo.Locations;
 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.mfd.Mfd.Properties.GutenbergRichter;
 import gov.usgs.earthquake.nshmp.mfd.Mfd.Properties.Single;
 import gov.usgs.earthquake.nshmp.mfd.Mfds;
+import gov.usgs.earthquake.nshmp.model.MfdConfig.AleatoryProperties;
 import gov.usgs.earthquake.nshmp.model.SourceFeature.Key;
+import gov.usgs.earthquake.nshmp.model.SourceFeature.NshmFault.Rates;
 import gov.usgs.earthquake.nshmp.tree.Branch;
 import gov.usgs.earthquake.nshmp.tree.LogicTree;
 
@@ -210,10 +214,12 @@ public class FaultRuptureSet implements RuptureSet {
       /*
        * Fault MFDs are constructed in a variety of ways. Single fault MFDs may
        * be created from a logic tree of slip rates.
+       *
+       *
        */
 
       MfdConfig mfdConfig = data.mfdConfig().orElseThrow();
-      Optional<LogicTree<Double>> moRateTree = Optional.empty();
+      Optional<LogicTree<MoRate>> moRateTree = Optional.empty();
 
       /* Build mfd-tree from fault slip or recurrence rates. */
       if (data.faultRateTree().isPresent()) {
@@ -221,9 +227,7 @@ public class FaultRuptureSet implements RuptureSet {
         // System.out.println(data.faultRateTree().get());
 
         /* Create Mo-rate tree. */
-        moRateTree = Optional.of(createMoRateTree(
-            feature,
-            data.faultRateTree().orElseThrow()));
+        moRateTree = Optional.of(createMoRateTree(feature, data));
 
         /* Update MFD properties tree based on source magnitude. */
         mfdPropsTree = updateMfdsForSlip();
@@ -261,10 +265,7 @@ public class FaultRuptureSet implements RuptureSet {
           .trace(feature.trace)
           .depth(feature.upperDepth)
           .dip(feature.dip)
-          .width(Faults.width(
-              feature.dip,
-              feature.upperDepth,
-              feature.lowerDepth))
+          .width(feature.width)
           .spacing(config.surfaceSpacing)
           .build();
 
@@ -436,12 +437,12 @@ public class FaultRuptureSet implements RuptureSet {
   /* Logic tree of moment rates. */
   private static LogicTree<Mfd> createMfdTree(
       LogicTree<Mfd.Properties> mfdPropsTree,
-      Optional<LogicTree<Double>> moRateTree,
+      Optional<LogicTree<MoRate>> moRateTree,
       MfdConfig mfdConfig) {
 
     LogicTree.Builder<Mfd> mfdTree = LogicTree.builder(MFD_TREE);
     if (moRateTree.isPresent()) {
-      LogicTree<Double> moTree = moRateTree.orElseThrow();
+      LogicTree<MoRate> moTree = moRateTree.orElseThrow();
       moTree.forEach(branch -> addMfds(mfdTree, mfdPropsTree, Optional.of(branch), mfdConfig));
     } else {
       addMfds(mfdTree, mfdPropsTree, Optional.empty(), mfdConfig);
@@ -452,16 +453,31 @@ public class FaultRuptureSet implements RuptureSet {
   private static void addMfds(
       LogicTree.Builder<Mfd> mfdTree, // receiver tree
       LogicTree<Mfd.Properties> mfdPropsTree,
-      Optional<Branch<Double>> moBranch,
+      Optional<Branch<MoRate>> moBranch,
       MfdConfig mfdConfig) {
 
-    for (Branch<Mfd.Properties> mfdBranch : mfdPropsTree) {
+    /*
+     * Working with branch list because weights may not sum to 1.0 as we loop
+     * over moment rate branches to build receiver mfdTree. It's simpler to
+     * update branch names and weights here than do it in both Single and GR MFD
+     * factories below.
+     *
+     * Update properties tree with mo rate branch name and weight.
+     */
+    List<Branch<Mfd.Properties>> propsBranchList = moBranch.isPresent()
+        ? MfdTrees.toBranchList(mfdPropsTree, moBranch.orElseThrow())
+        : mfdPropsTree;
+    Optional<MoRate> moRateIn = moBranch.map(Branch::value);
+
+    for (Branch<Mfd.Properties> mfdBranch : propsBranchList) {
+      double moRate = computeMoRate(mfdBranch.value(), moRateIn);
+
       switch (mfdBranch.value().type()) {
         case SINGLE:
-          addSingleMfds(mfdTree, mfdBranch, moBranch, mfdConfig);
+          addSingleMfds(mfdTree, mfdBranch, moRate, mfdConfig);
           break;
         case GR:
-          addGrMfds(mfdTree, mfdBranch, moBranch, mfdConfig);
+          addGrMfds(mfdTree, mfdBranch, moRate, mfdConfig);
           break;
         default:
           throw new UnsupportedOperationException(
@@ -469,37 +485,108 @@ public class FaultRuptureSet implements RuptureSet {
       }
     }
   }
+  //
+
+  /*
+   * Container class for slip-rate based moRate.
+   *
+   * Optional fields, if present, are used downstream to override slip-based
+   * moRates to yield better agreement with legacy codes.
+   */
+  private static class MoRate {
+
+    final double moRate;
+    final OptionalDouble graValue;
+    final OptionalDouble singleRate;
+
+    MoRate(
+        double moRate,
+        OptionalDouble graValue,
+        OptionalDouble singleRate) {
+
+      this.moRate = moRate;
+      this.graValue = graValue;
+      this.singleRate = singleRate;
+    }
+  }
+
+  private static double computeMoRate(Mfd.Properties props, Optional<MoRate> moRateData) {
+
+    /* No mo-rate data supplied, compute from properties */
+    if (moRateData.isEmpty()) {
+      switch (props.type()) {
+        case SINGLE:
+          Mfd.Properties.Single single = props.getAsSingle();
+          return single.rate() * Earthquakes.magToMoment(single.magnitude());
+        case GR:
+          Mfd.Properties.GutenbergRichter gr = props.getAsGr();
+          return grMomentRate(gr);
+        default:
+          throw new UnsupportedOperationException(
+              props.type() + " MFD not supported");
+      }
+    }
+
+    /* Return supplied mo-rate or compute from updated properties. */
+    MoRate rateData = moRateData.orElseThrow();
+    switch (props.type()) {
+      case SINGLE:
+        if (rateData.singleRate.isPresent()) {
+          return rateData.singleRate.orElseThrow() *
+              Earthquakes.magToMoment(props.getAsSingle().magnitude());
+        }
+        return rateData.moRate;
+      case GR:
+        if (rateData.graValue.isPresent()) {
+          Mfd.Properties.GutenbergRichter grNew =
+              GutenbergRichter.copyOfWithRate(props.getAsGr(), rateData.graValue.orElseThrow());
+          return grMomentRate(grNew);
+        }
+        return rateData.moRate;
+      default:
+        throw new UnsupportedOperationException(
+            props.type() + " MFD not supported");
+    }
+  }
 
   /*
    * Create a logic tree of moment rates for a fault source given a feature and
    * rate data. If present, a rate-dip may be used to compute a constraining
    * moment rate (e.g. WUS normal faults)
    */
-  private static LogicTree<Double> createMoRateTree(
+  private static LogicTree<MoRate> createMoRateTree(
       SourceFeature.NshmFault feature,
-      LogicTree<String> faultRateTree) {
+      ModelData data) {
+
+    double dip = feature.dip;
+    double width = feature.width;
+
+    // TODO this needs revisiting for 2008 and/or
+    // future models if slip rate is to be used
+    // rateDip should not be present for 2014/2018 models
 
-    double dip = feature.rateDip.isPresent()
-        ? feature.rateDip.getAsDouble()
-        : feature.dip;
-    double width = Faults.width(dip, feature.upperDepth, feature.lowerDepth);
-    double length = feature.length.orElseThrow();
+    /* Dip slip model dependent parameters. */
+    if (feature.rateDip.isPresent()) {
+      dip = feature.rateDip.orElseThrow();
+      double height = feature.lowerDepth - feature.upperDepth;
+      width = Faults.projectVertical(dip, height);
+    }
+
+    double length = feature.length;
     double area = length * width * 1e6;
-    double Mw = feature.magnitude.getAsDouble();
+    double Mw = feature.magnitude.orElseThrow();
     double mo = Earthquakes.magToMoment(Mw);
 
-    Map<String, Double> rateMap = feature.rateMap;
+    Map<String, Rates> rateMap = feature.rateMap;
     RateType rateType = feature.rateType;
-    LogicTree.Builder<Double> moRateTree = LogicTree.builder(RATE_TREE);
+    LogicTree.Builder<MoRate> moRateTree = LogicTree.builder(RATE_TREE);
 
     // TODO clean
-    // System.out.println(feature.name);
-    // System.out.println(rateMap);
     if (rateMap.containsValue(null)) {
       System.out.println("          " + rateMap);
     }
 
-    for (Branch<String> branch : faultRateTree) {
+    for (Branch<String> branch : data.faultRateTree().orElseThrow()) {
 
       /*
        * TODO Verify missing fault rates and imbalanced logic trees. In some
@@ -509,30 +596,21 @@ public class FaultRuptureSet implements RuptureSet {
        * because GEO weight is 0.8 and imbalanced; maybe we just handle this
        * with setting rate to 0, as below.
        */
-      Double rateCheck = rateMap.get(branch.id());
-      double rate = (rateCheck != null) ? rateCheck : 0.0;
+      Rates rates = rateMap.get(branch.id());
+
+      double rate = (rates != null) ? rates.rate : 0.0;
 
-      /* CONUS: 2008 are vertical, 2014 are on-plane. */
       if (rateType == RateType.VERTICAL_SLIP) {
-        rate = Faults.projectVerticalSlip(dip, rate);
+        rate = Faults.projectVertical(dip, rate);
       }
 
-      double moRate = (rateType == RECURRENCE)
+      double moRateValue = (rateType == RECURRENCE)
           ? mo * rate
           : Earthquakes.moment(area, rate / 1000.0);
 
-      /*
-       * 2014 model consistency. Convert to annual rate and set scale to seven
-       * decimal places to match the MFD values in fortran and XML input files
-       * and then convert back to moment rate. The steps of going from slip rate
-       * to explicit MFD parameters and then back to build MFDs is no longer
-       * necessary, but the cumulative effects of rounding at various steps in
-       * legacy codes are difficult to replicate without doing things like this.
-       * TODO consider describing different rounding/precision characteristics
-       * in a model defined in a source config.
-       */
-      double eqRate = Maths.round(moRate / mo, 7);
-      moRate = eqRate * mo;
+      MoRate moRate = (rates != null)
+          ? new MoRate(moRateValue, rates.graValue, rates.singleRate)
+          : new MoRate(moRateValue, OptionalDouble.empty(), OptionalDouble.empty());
       moRateTree.addBranch(branch.id(), moRate, branch.weight());
     }
     return moRateTree.build();
@@ -541,8 +619,8 @@ public class FaultRuptureSet implements RuptureSet {
   /* Fault GR */
   private static void addGrMfds(
       LogicTree.Builder<Mfd> mfdTree, // receiver tree
-      Branch<Mfd.Properties> mfdBranch, // properties may include rate
-      Optional<Branch<Double>> moBranch, // or rate supplied separately
+      Branch<Mfd.Properties> mfdBranch,
+      double moRate,
       MfdConfig mfdConfig) {
 
     Mfd.Properties.GutenbergRichter gr = mfdBranch.value().getAsGr();
@@ -550,19 +628,10 @@ public class FaultRuptureSet implements RuptureSet {
     String mfdId = mfdBranch.id();
     double mfdWt = mfdBranch.weight();
 
-    if (moBranch.isPresent()) {
-      mfdId = mfdBranchId(moBranch.orElseThrow().id(), mfdId);
-      mfdWt *= moBranch.orElseThrow().weight();
-    }
-
     // TODO does this ever throw?
     int nMag = magCount(gr.mMin(), gr.mMax(), gr.Δm());
     checkState(nMag > 0, "GR MFD with no magnitudes [%s]", mfdBranch.id());
 
-    double moRate = moBranch.isPresent()
-        ? moBranch.orElseThrow().value()
-        : grMomentRate(gr);
-
     /*
      * Optional epistemic uncertainty; no aleatory with GR. This allows
      * epistemic uncertainty because the cutoff is based on mMin (6.5), but in
@@ -606,10 +675,15 @@ public class FaultRuptureSet implements RuptureSet {
           System.out.println("GR MFD epistemic branch with no magnitudes");
         }
 
-        String id = mfdBranchId(mfdId, epiBranch.id());
-        Mfd mfd = newGrBuilder(gr, mMaxEpi)
+        String id = MfdTrees.combineBranchIds(mfdId, epiBranch.id());
+        Mfd mfd = GutenbergRichter.copyOfWithMax(gr, mMaxEpi)
+            .toBuilder()
             .scaleToMomentRate(epiOk ? moRate : 0)
             .build();
+
+        // Mfd mfd = newGrBuilder(gr, mMaxEpi)
+        // .scaleToMomentRate(epiOk ? moRate : 0)
+        // .build();
         mfdTree.addBranch(id, mfd, weightEpi);
       }
     } else {
@@ -646,80 +720,98 @@ public class FaultRuptureSet implements RuptureSet {
   /* Fault SINGLE */
   private static void addSingleMfds(
       LogicTree.Builder<Mfd> mfdTree, // receiver tree
-      Branch<Mfd.Properties> mfdBranch, // properties may include rate
-      Optional<Branch<Double>> moBranch, // or rate supplied separately
+      Branch<Mfd.Properties> mfdBranch,
+      double moRate,
       MfdConfig mfdConfig) {
 
-    Mfd.Properties.Single single = mfdBranch.value().getAsSingle();
-
-    String mfdId = mfdBranch.id();
-    double mfdWt = mfdBranch.weight();
-
-    if (moBranch.isPresent()) {
-      mfdId = mfdBranchId(moBranch.orElseThrow().id(), mfdId);
-      mfdWt *= moBranch.orElseThrow().weight();
-    }
+    double magnitude = mfdBranch.value().getAsSingle().magnitude();
 
-    double moRate = moBranch.isPresent()
-        ? moBranch.orElseThrow().value()
-        : single.rate() * Earthquakes.magToMoment(single.magnitude());
+    // String mfdId = mfdBranch.id(); // TODO clean
+    // double mfdWt = mfdBranch.weight();
 
     /* Optional epistemic uncertainty. */
-    boolean epistemic = hasEpistemic(mfdConfig, single.magnitude());
+    boolean epistemic = hasEpistemic(mfdConfig, magnitude);
 
     /* Optional aleatory variability. */
-    boolean aleatory = mfdConfig.aleatoryProperties.isPresent();
+    Optional<AleatoryProperties> aleaProps = mfdConfig.aleatoryProperties;
 
     /* mMax epistemic uncertainty branches */
     if (epistemic) {
 
       for (Branch<Double> epiBranch : mfdConfig.epistemicTree.orElseThrow()) {
 
-        double mEpi = single.magnitude() + epiBranch.value();
-        double weightEpi = mfdWt * epiBranch.weight();
-        String id = mfdBranchId(mfdId, epiBranch.id());
+        double mEpi = magnitude + epiBranch.value();
+        double wtEpi = mfdBranch.weight() * epiBranch.weight();
+        String id = MfdTrees.combineBranchIds(mfdBranch.id(), epiBranch.id());
 
         /* Possibly apply aleatory variability */
-        if (aleatory) {
-
-          MfdConfig.AleatoryProperties aleaProps = mfdConfig.aleatoryProperties.orElseThrow();
-          Mfd mfd = newGaussianBuilder(mEpi, aleaProps)
-              .scaleToMomentRate(moRate)
-              .build();
-          mfdTree.addBranch(id, mfd, weightEpi);
+        // Mfd.Builder builder = (aleaProps.isPresent())
+        // ? newGaussianBuilder(mEpi, aleaProps.orElseThrow())
+        // : Mfd.newSingleBuilder(mEpi);
+        // Mfd mfd = builder.scaleToMomentRate(moRate).build();
 
-        } else {
+        Mfd mfd = createSingleMfd(mEpi, moRate, aleaProps);
+        mfdTree.addBranch(id, mfd, wtEpi);
 
-          Mfd mfd = Mfd.newSingleBuilder(mEpi)
-              .scaleToMomentRate(moRate)
-              .build();
-          mfdTree.addBranch(id, mfd, weightEpi);
-        }
+        /* Possibly apply aleatory variability */
+        // TODO clean
+        // if (aleatory) {
+        //
+        // MfdConfig.AleatoryProperties aleaProps =
+        // mfdConfig.aleatoryProperties.orElseThrow();
+        // Mfd mfd = newGaussianBuilder(mEpi, aleaProps)
+        // .scaleToMomentRate(moRate)
+        // .build();
+        // mfdTree.addBranch(id, mfd, weightEpi);
+        //
+        // } else {
+        //
+        // Mfd mfd = Mfd.newSingleBuilder(mEpi)
+        // .scaleToMomentRate(moRate)
+        // .build();
+        // mfdTree.addBranch(id, mfd, weightEpi);
+        // }
       }
 
     } else { /* No epistemic uncertainty */
 
-      /* Possibly apply aleatory variability */
-      if (aleatory) {
-
-        MfdConfig.AleatoryProperties aleaProps = mfdConfig.aleatoryProperties.orElseThrow();
-        Mfd mfd = newGaussianBuilder(single.magnitude(), aleaProps)
-            .scaleToMomentRate(moRate)
-            .build();
-        mfdTree.addBranch(mfdId, mfd, mfdWt);
+      // Mfd.Builder builder = (aleaProps.isPresent())
+      // ? newGaussianBuilder(magnitude, aleaProps.orElseThrow())
+      // : Mfd.newSingleBuilder(magnitude);
+      // Mfd mfd = builder.scaleToMomentRate(moRate).build();
 
-      } else {
+      Mfd mfd = createSingleMfd(magnitude, moRate, aleaProps);
+      mfdTree.addBranch(mfdBranch.id(), mfd, mfdBranch.weight());
 
-        Mfd mfd = Mfd.newSingleBuilder(single.magnitude())
-            .scaleToMomentRate(moRate)
-            .build();
-        mfdTree.addBranch(mfdId, mfd, mfdWt);
-      }
+      /* Possibly apply aleatory variability */
+      // TODO clean
+      // if (aleaProps.isPresent()) {
+      //
+      // MfdConfig.AleatoryProperties aleaProps =
+      // mfdConfig.aleatoryProperties.orElseThrow();
+      // Mfd mfd = newGaussianBuilder(magnitude, aleaProps)
+      // .scaleToMomentRate(moRate)
+      // .build();
+      // mfdTree.addBranch(mfdId, mfd, mfdWt);
+      //
+      // } else {
+      //
+      // Mfd mfd = Mfd.newSingleBuilder(magnitude)
+      // .scaleToMomentRate(moRate)
+      // .build();
+      // mfdTree.addBranch(mfdId, mfd, mfdWt);
+      // }
     }
   }
 
-  private static String mfdBranchId(String... ids) {
-    return String.join(" : ", ids);
+  private static Mfd createSingleMfd(
+      double magnitude, double moRate,
+      Optional<AleatoryProperties> aleaProps) {
+
+    Mfd.Builder builder = (aleaProps.isPresent())
+        ? newGaussianBuilder(magnitude, aleaProps.orElseThrow())
+        : Mfd.newSingleBuilder(magnitude);
+    return builder.scaleToMomentRate(moRate).build();
   }
 
   private static boolean hasEpistemic(MfdConfig mfdConfig, double magnitude) {
@@ -727,8 +819,8 @@ public class FaultRuptureSet implements RuptureSet {
 
     /* No epistemic uncertainty if: m - Δm < mfdConfig.minMagnitude */
     if (epistemic) {
-      double mEpiCutoff = mfdConfig.minMagnitude.getAsDouble();
-      double mEpiOffset = mfdConfig.minEpiOffset.getAsDouble();
+      double mEpiCutoff = mfdConfig.minMagnitude.orElseThrow();
+      double mEpiOffset = mfdConfig.minEpiOffset.orElseThrow();
       epistemic = (magnitude + mEpiOffset) > mEpiCutoff;
     }
     return epistemic;
@@ -767,11 +859,6 @@ public class FaultRuptureSet implements RuptureSet {
         aleaProps.nσ);
   }
 
-  /* GR MFD builder with mMax override. */
-  static Mfd.Builder newGrBuilder(Mfd.Properties.GutenbergRichter gr, double mMax) {
-    return Mfd.newGutenbergRichterBuilder(gr.b(), gr.Δm(), gr.mMin(), mMax);
-  }
-
   /* Expected moment rate of a GR MFD. */
   static double grMomentRate(Mfd.Properties.GutenbergRichter gr) {
     double mMin = gr.mMin() + gr.Δm() / 2;
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/model/FaultSource.java b/src/main/java/gov/usgs/earthquake/nshmp/model/FaultSource.java
index c1047556f4e2ca6ed47b13b035159c4b0b4090c2..acfa83cf6d513f90420cd0e7010f2da72162df33 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/model/FaultSource.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/model/FaultSource.java
@@ -103,6 +103,15 @@ public class FaultSource implements Source {
 
     checkState(Iterables.size(Iterables.concat(ruptureLists)) > 0,
         "FaultSource has no ruptures");
+    // System.out.println(this);
+
+    // System.out.println(scaledMfds);
+    // long nRuptures = ruptureLists.stream().flatMap(List::stream).count();
+    // System.out.println(nRuptures);
+
+    // System.out.println(Mfds.momentRate(scaledMfds.get(0)));
+    // System.out.println(trace);
+    // System.out.println(surface);
   }
 
   @Override
@@ -208,7 +217,10 @@ public class FaultSource implements Source {
     // Mfd.Type type = props.type();
     double length = trace.length();
 
-    // boolean floats = (type == Type.GR) || floatRuptures;
+    Mfd.Type mfdType = mfd.properties().type();
+
+    boolean floatSingle = mfdType == Mfd.Type.SINGLE && floatSingleRuptures;
+    boolean floats = (mfdType == Mfd.Type.GR) || floatSingle;
 
     for (XyPoint xy : mfd.data()) {
       double mag = xy.x();
@@ -231,7 +243,7 @@ public class FaultSource implements Source {
       // TODO we want to get the 'floats' attribute out of MFDs
       // the only reason it is there is to allow SINGLE to flip-flop
       // it should just be a SourceProperty
-      if (floatSingleRuptures && msrLength < length) {
+      if (floats && msrLength < length) {
 
         // TODO change to using RuptureFloating.OFF
 
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 0751493ae567f1ffa5484db8e48e20962e8c6884..c4738f2bc7ffc2d5ed397fd71dda920af210975e 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/model/GridLoader.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/model/GridLoader.java
@@ -284,7 +284,7 @@ class GridLoader {
       /* Create total MFDs and transpose mfd tree lists. */
       for (FeatureData featureData : dataMap.values()) {
         featureData.mfds = reduceMfds(featureData.mfdTrees);
-        featureData.mfdsTree = MfdTrees.transposeTree(featureData.mfdTrees);
+        featureData.mfdsTree = MfdTrees.transpose(featureData.mfdTrees);
       }
     }
 
@@ -399,7 +399,7 @@ class GridLoader {
       double mMax) {
     Mfd.Type type = grProps.type();
     if (type == Type.GR_MMAX_GR) {
-      builder.transform(xy -> zeroAboveM(xy, 6.5));
+      builder.transform(xy -> zeroAboveM(xy, Math.min(mMax, 6.5)));
     } else if (type == Type.GR_MMAX_SINGLE) {
       /*
        * Keeps the magnitude bin just above mMax if mMax > m_gr + Δm/2. Rounding
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 e85733b6a7e3cb4f03356ad80d1264fcfd1884a3..6cf6ee8c073ec0e12eb95adf0f3a04ef77e426bd 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/model/HazardModel.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/model/HazardModel.java
@@ -17,7 +17,6 @@ import com.google.common.collect.SetMultimap;
 import com.google.common.collect.Sets;
 import com.google.common.collect.Streams;
 
-import gov.usgs.earthquake.nshmp.Faults;
 import gov.usgs.earthquake.nshmp.Maths;
 import gov.usgs.earthquake.nshmp.calc.CalcConfig;
 import gov.usgs.earthquake.nshmp.geo.LocationList;
@@ -365,10 +364,7 @@ public final class HazardModel implements Iterable<SourceSet<? extends Source>>
           .id(feature.id)
           .trace(feature.trace)
           .dip(feature.dip)
-          .width(Faults.width(
-              feature.dip,
-              feature.upperDepth,
-              feature.lowerDepth))
+          .width(feature.width)
           .depth(feature.upperDepth)
           .rake(feature.rake)
           .mfdTree(frs.mfdTree)
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/model/MfdTrees.java b/src/main/java/gov/usgs/earthquake/nshmp/model/MfdTrees.java
index 2d5995293343365dd827865bcef50c9bb2c00f46..c0c2a76c47dad31334e6385938a7e520a74a2568 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/model/MfdTrees.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/model/MfdTrees.java
@@ -28,12 +28,16 @@ import gov.usgs.earthquake.nshmp.tree.Branch;
 import gov.usgs.earthquake.nshmp.tree.LogicTree;
 
 /**
- * Factory class for manipulating and querying MFD logic trees.
+ * Factory class for manipulating and querying logic trees. Most, but not all,
+ * methods pertain to MFD logic trees.
+ *
  *
  * @author U.S. Geological Survey
  */
 class MfdTrees {
 
+  // TODO rename to Trees
+
   /* All Gutenberg-Richter MFD types. */
   private static final Set<Type> GR_TYPES = EnumSet.of(
       GR,
@@ -99,7 +103,7 @@ class MfdTrees {
   private static Mfd mMaxGrMfd(Mfd.Properties props, double mMax) {
     Mfd.Properties newProps = (props.type() == Type.GR_TAPER)
         ? new TaperedGr(props.getAsGrTaper(), mMax)
-        : new GutenbergRichter(props.getAsGr(), mMax);
+        : GutenbergRichter.copyOfWithMax(props.getAsGr(), mMax);
     return newProps.toBuilder()
         .transform(p -> p.set((p.x() > props.getAsGr().mMax()) ? 0.0 : p.y()))
         .build();
@@ -248,7 +252,7 @@ class MfdTrees {
    * Transpose a list of logic trees to a logic tree of immutable lists.
    * Supplied logic trees are assumed to have same branch names and IDs.
    */
-  static <T> LogicTree<List<T>> transposeTree(List<LogicTree<T>> treeList) {
+  static <T> LogicTree<List<T>> transpose(List<LogicTree<T>> treeList) {
     LogicTree<T> model = treeList.get(0);
 
     /* Init branch value lists. */
@@ -273,4 +277,54 @@ class MfdTrees {
     }
     return treeOfLists.build();
   }
+
+  /* Join the supplied IDs on ' : ' */
+  static String combineBranchIds(String... ids) {
+    return String.join(" : ", ids);
+  }
+
+  /*
+   * Combine IDs and weights of a tree and branch, preserving tree values.
+   * Branches in resultant list do not likely sum to one.
+   */
+  static <T, R> List<Branch<T>> toBranchList(
+      LogicTree<T> tree,
+      Branch<R> branch) {
+
+    return tree.stream()
+        .map(treeBranch -> new IdWeightBranch<>(
+            combineBranchIds(branch.id(), treeBranch.id()),
+            treeBranch.value(),
+            branch.weight() * treeBranch.weight()))
+        .collect(toUnmodifiableList());
+  }
+
+  private static class IdWeightBranch<T> implements Branch<T> {
+
+    final String id;
+    final double weight;
+    final T value;
+
+    @Override
+    public String id() {
+      return id;
+    }
+
+    @Override
+    public double weight() {
+      return weight;
+    }
+
+    @Override
+    public T value() {
+      return value;
+    }
+
+    IdWeightBranch(String id, T value, double weight) {
+      this.id = id;
+      this.weight = weight;
+      this.value = value;
+    }
+  }
+
 }
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 7096ceeef5248f6b4a26b165f7363be5e861c291..316a4986a6bc5454d9b4d21209ad17f76e95b56b 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/model/ModelLoader.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/model/ModelLoader.java
@@ -37,6 +37,7 @@ import static gov.usgs.earthquake.nshmp.model.SourceType.ZONE;
 import static java.nio.file.Files.newDirectoryStream;
 
 import java.io.IOException;
+import java.math.RoundingMode;
 import java.nio.file.DirectoryStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -51,6 +52,7 @@ import java.util.logging.Logger;
 import com.google.common.collect.Iterables;
 import com.google.gson.JsonElement;
 
+import gov.usgs.earthquake.nshmp.Maths;
 import gov.usgs.earthquake.nshmp.fault.FocalMech;
 import gov.usgs.earthquake.nshmp.geo.json.Feature;
 import gov.usgs.earthquake.nshmp.geo.json.Properties;
@@ -180,12 +182,12 @@ abstract class ModelLoader {
     this.root = root;
   }
 
-  /* Load source type specific rate tree, if present. */
-  abstract void loadRateTree(Path dir, ModelData data);
-
   /* Load source type specific config, if present. */
   abstract void loadSourceConfig(Path dir, ModelData data);
 
+  /* Load source type specific rate tree, if present. */
+  abstract void loadRateTree(Path dir, ModelData data);
+
   /* Load single (branch) type specific source. */
   abstract SourceTree loadSingleSource(
       Path geojson,
@@ -277,12 +279,13 @@ abstract class ModelLoader {
       System.out.println("  source: " + root.relativize(geojson));
 
       /* Create new feature with updated properties. */
-      double length = feature.trace.length();
-      double Mw = FaultRuptureSet.magnitudeWc94(length);
+      double Mw = FaultRuptureSet.magnitudeWc94(feature.length);
+      double Δz = feature.lowerDepth - feature.upperDepth;
+      double width = computeWidth(Δz, feature.dip);
       Feature nshmFeature = Feature.copyOf(feature.source)
           .properties(Properties.fromFeature(feature.source)
-              .put(Key.LENGTH, length)
               .put(Key.MAGNITUDE, Mw)
+              .put(Key.WIDTH, width)
               .build())
           .build();
       feature = new SourceFeature.NshmFault(nshmFeature);
@@ -321,21 +324,21 @@ abstract class ModelLoader {
           .gmms(data.gmms())
           .root(dipSourceTree);
 
-      double length = feature.trace.length();
-      double Mw = FaultRuptureSet.magnitudeWc94(length);
-
+      double Mw = FaultRuptureSet.magnitudeWc94(feature.length);
       for (int i = 0; i < dipTree.size(); i++) {
         Branch<Double> dipBranch = dipTree.get(i);
         int branchDip = (int) (feature.dip + dipBranch.value());
         int rateDip = (int) ((dipSlipModel == FIXED) ? feature.dip : branchDip);
         String branchName = String.format("%s [%s°]", feature.name, branchDip);
+        double Δz = feature.lowerDepth - feature.upperDepth;
+        double width = computeWidth(Δz, branchDip);
         Feature dipFeature = Feature.copyOf(feature.source)
             .properties(Properties.fromFeature(feature.source)
                 .put(Key.NAME, branchName)
                 .put(Key.DIP, branchDip)
                 .put(Key.RATE_DIP, rateDip)
-                .put(Key.LENGTH, length)
                 .put(Key.MAGNITUDE, Mw)
+                .put(Key.WIDTH, width)
                 .build())
             .build();
 
@@ -348,6 +351,15 @@ abstract class ModelLoader {
       return treeBuilder.build();
     }
 
+    /* This exists for consistency with legacy codes */
+    @Deprecated
+    private static final double TO_RAD = 3.14159 / 180.0;
+
+    @Deprecated
+    private static double computeWidth(double Δz, double dip) {
+      return Maths.round(Δz / Math.sin(dip * TO_RAD), 5, RoundingMode.FLOOR);
+    }
+
     private void loadSingleRuptureSet(
         Branch<Path> branch,
         SourceTree.Builder treeBuilder,
@@ -607,13 +619,13 @@ abstract class ModelLoader {
     }
 
     @Override
-    void loadRateTree(Path dir, ModelData data) {
-      readGridRateTree(dir).ifPresent(data::gridRateTree);
+    void loadSourceConfig(Path dir, ModelData data) {
+      readGridConfig(dir).ifPresent(data::sourceConfig);
     }
 
     @Override
-    void loadSourceConfig(Path dir, ModelData data) {
-      readGridConfig(dir).ifPresent(data::sourceConfig);
+    void loadRateTree(Path dir, ModelData data) {
+      readGridRateTree(dir).ifPresent(data::gridRateTree);
     }
 
     @Override
@@ -714,7 +726,7 @@ abstract class ModelLoader {
           /* Attach singleton as leaf. */
           if (ruptureSets.size() == 1) {
             treeBuilder.addLeaf(rateBranch, ruptureSets.get(0));
-            return;
+            continue;
           }
 
           /* Otherwise create logic group of rupture set pseudo paths. */
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/model/SourceFeature.java b/src/main/java/gov/usgs/earthquake/nshmp/model/SourceFeature.java
index b2fd609fc7a2b032f0b15d3eefb62b18d4a5df39..51d35515fd138d442edcdc6d4f2a0eddb2a91187 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/model/SourceFeature.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/model/SourceFeature.java
@@ -1,6 +1,7 @@
 package gov.usgs.earthquake.nshmp.model;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
 import static gov.usgs.earthquake.nshmp.Earthquakes.checkCrustalDepth;
 import static gov.usgs.earthquake.nshmp.Faults.checkAseismicityFactor;
 import static gov.usgs.earthquake.nshmp.Faults.checkDip;
@@ -8,6 +9,7 @@ import static gov.usgs.earthquake.nshmp.Faults.checkDipDirection;
 import static gov.usgs.earthquake.nshmp.Faults.checkRake;
 import static gov.usgs.earthquake.nshmp.Faults.checkSlipRate;
 import static gov.usgs.earthquake.nshmp.Text.checkName;
+import static java.util.stream.Collectors.toMap;
 
 import java.awt.geom.Area;
 import java.lang.reflect.Type;
@@ -16,13 +18,16 @@ import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.NoSuchElementException;
 import java.util.Optional;
 import java.util.OptionalDouble;
 
 import com.google.common.reflect.TypeToken;
+import com.google.gson.JsonElement;
 
 import gov.usgs.earthquake.nshmp.Earthquakes;
+import gov.usgs.earthquake.nshmp.Faults;
 import gov.usgs.earthquake.nshmp.geo.Location;
 import gov.usgs.earthquake.nshmp.geo.LocationList;
 import gov.usgs.earthquake.nshmp.geo.Locations;
@@ -199,6 +204,8 @@ public abstract class SourceFeature {
     static final String RATE = "rate";
     static final String RATE_MAP = "rate-map";
     static final String RATE_TYPE = "rate-type";
+    static final String GR_A_VALUE = "gr-a-value";
+    static final String SINGLE_RATE = "single-rate";
     static final String STRIKE = "strike";
     static final String MFD_TREE = "mfd-tree";
     static final String ASEISMICITY = "aseismicity";
@@ -273,11 +280,17 @@ public abstract class SourceFeature {
     /** The fault section rake. */
     final double rake;
 
+    /** The fault section length. */
+    final double length;
+
+    /** The fault section width. */
+    final double width;
+
     /** The fault section {@code RateType}. */
     final RateType rateType;
 
     /** The mapping of rates from different deformation models. */
-    final Map<String, Double> rateMap;
+    final Map<String, Rates> rateMap;
 
     /*
      * Fields for use in FaultRuptureSet.Builder. length, width, and magnitude
@@ -291,14 +304,14 @@ public abstract class SourceFeature {
      * parameters as used in hazard calculations.
      */
     final OptionalDouble rateDip;
-    final OptionalDouble length;
-    final OptionalDouble width;
     final OptionalDouble magnitude;
 
     NshmFault(Feature section) {
       super(section);
       trace = section.asLineString();
       Properties props = section.properties();
+
+      /* Required properties. */
       dip = props.getDouble(Key.DIP).orElseThrow();
       checkDip(dip);
       upperDepth = props.getDouble(Key.UPPER_DEPTH).orElseThrow();
@@ -310,31 +323,86 @@ public abstract class SourceFeature {
       rateType = RateType.valueOf(props.getString(Key.RATE_TYPE).orElseThrow());
       rateMap = nshmRateMap(props);
       props.getString(Key.STATE).orElseThrow();
+
+      /* Properties computed if absent. */
+      length = props.getDouble(Key.LENGTH).orElseGet(trace::length);
+      width = props.getDouble(Key.WIDTH).orElseGet(this::computeWidth);
+
+      /* Optional properties. */
       rateDip = props.getDouble(Key.RATE_DIP);
-      length = props.getDouble(Key.LENGTH);
-      width = props.getDouble(Key.WIDTH);
       magnitude = props.getDouble(Key.MAGNITUDE);
+    }
 
+    private double computeWidth() {
+      return Faults.projectVertical(dip, lowerDepth - upperDepth);
     }
-  }
 
-  /*
-   * A NSHM feature can have either single rate (maps to 'GEO'), no rate because
-   * type is RECURRENCE (maps to 'GEO' with rate of 0), a rate-map with multiple
-   * rates whose type can be SLIP or RECURRENCE.
-   */
-  private static Map<String, Double> nshmRateMap(Properties props) {
-    OptionalDouble rate = props.getDouble(Key.RATE);
-    RateType type = props.get(Key.RATE_TYPE, RateType.class).orElseThrow();
-    if (rate.isPresent()) {
-      return Map.of("GEO", rate.orElseThrow());
+    /* Utility container for rate values. */
+    static class Rates {
+
+      /* Slip or recurrence rate */
+      final double rate;
+
+      /* Pre-computed Gutenberg-Richter MFD a-value. */
+      final OptionalDouble graValue;
+
+      /* Pre-computed single MFD rate. */
+      final OptionalDouble singleRate;
+
+      Rates(double rate) {
+        this.rate = rate;
+        this.graValue = OptionalDouble.empty();
+        this.singleRate = OptionalDouble.empty();
+      }
+
+      Rates(JsonElement json) {
+        Map<String, Double> rateMap = Deserialize.fromJson(json, RATE_DATA_TOKEN);
+        this.rate = checkNotNull(rateMap.get(Key.RATE));
+        Double graValue = rateMap.get(Key.GR_A_VALUE);
+        this.graValue = (graValue != null)
+            ? OptionalDouble.of(graValue)
+            : OptionalDouble.empty();
+        Double singleRate = rateMap.get(Key.SINGLE_RATE);
+        this.singleRate = (singleRate != null)
+            ? OptionalDouble.of(singleRate)
+            : OptionalDouble.empty();
+      }
+    }
+
+    private static Type RATE_MAP_TOKEN = new TypeToken<Map<String, JsonElement>>() {}.getType();
+    private static Type RATE_DATA_TOKEN = new TypeToken<Map<String, Double>>() {}.getType();
+
+    /*
+     * A NSHM feature can have either single rate (maps to 'GEO'), no rate
+     * because type is RECURRENCE (maps to 'GEO' with rate of 0), a rate-map
+     * with multiple rates whose type can be SLIP or RECURRENCE.
+     */
+    private static Map<String, Rates> nshmRateMap(Properties props) {
+      OptionalDouble rate = props.getDouble(Key.RATE);
+      RateType type = props.get(Key.RATE_TYPE, RateType.class).orElseThrow();
+      if (rate.isPresent()) {
+        return Map.of("GEO", new Rates(rate.orElseThrow()));
+      }
+
+      Optional<Map<String, Rates>> rateMap = readRateMap(props);
+      if (rateMap.isEmpty() && type == RateType.RECURRENCE) {
+        return Map.of("GEO", new Rates(0.0));
+      }
+      return rateMap.orElseThrow();
     }
-    Type mapType = new TypeToken<Map<String, Double>>() {}.getType();
-    Optional<Map<String, Double>> rateMap = props.get(Key.RATE_MAP, mapType);
-    if (rateMap.isEmpty() && type == RateType.RECURRENCE) {
-      return Map.of("GEO", 0.0);
+
+    private static Optional<Map<String, Rates>> readRateMap(Properties props) {
+      Optional<Map<String, JsonElement>> rateElements = props.get(Key.RATE_MAP, RATE_MAP_TOKEN);
+      if (rateElements.isEmpty()) {
+        return Optional.empty();
+      }
+      Map<String, Rates> rateMap = rateElements.orElseThrow().entrySet().stream()
+          .filter(e -> !e.getValue().isJsonNull())
+          .collect(toMap(
+              Entry::getKey,
+              e -> new Rates(e.getValue())));
+      return Optional.of(rateMap);
     }
-    return rateMap.orElseThrow();
   }
 
   /** A sub-section of a crustal fault in a NSHM fault-system. */
diff --git a/src/test/java/gov/usgs/earthquake/nshmp/mfd/MfdTests.java b/src/test/java/gov/usgs/earthquake/nshmp/mfd/MfdTests.java
index 6270622bdf0bdace3ad89574687e77dea941a3fa..7d9ab76955b0119734a67e0dfaa2dac8d7b8b70e 100644
--- a/src/test/java/gov/usgs/earthquake/nshmp/mfd/MfdTests.java
+++ b/src/test/java/gov/usgs/earthquake/nshmp/mfd/MfdTests.java
@@ -186,7 +186,7 @@ class MfdTests {
     assertEquals(5.0, grProps.mMin());
     assertEquals(5.5, grProps.mMax());
 
-    grProps = new GutenbergRichter(grProps, 7.5);
+    grProps = GutenbergRichter.copyOfWithMax(grProps, 7.5);
     assertEquals(1.0, grProps.a());
     assertEquals(1.0, grProps.b());
     assertEquals(0.1, grProps.Δm());