diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmCalc.java b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmCalc.java
index 1e3a6516eb01e642d1df16fe650f26d9d327277c..ef285d34066e82be80aa31bc54e82c70f08d8a81 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmCalc.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmCalc.java
@@ -11,10 +11,10 @@ import java.util.Arrays;
 import java.util.EnumMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 
-import com.google.common.collect.ImmutableList;
 import com.google.common.primitives.Doubles;
 
 import gov.usgs.earthquake.nshmp.Maths;
@@ -29,6 +29,7 @@ import gov.usgs.earthquake.nshmp.tree.LogicTree;
 import gov.usgs.earthquake.nshmp.www.gmm.GmmService.Distance;
 import gov.usgs.earthquake.nshmp.www.gmm.GmmService.Magnitude;
 import gov.usgs.earthquake.nshmp.www.gmm.GmmService.Request;
+import gov.usgs.earthquake.nshmp.www.gmm.XyDataGroup.EpiSeries;
 
 import jakarta.inject.Singleton;
 
@@ -40,74 +41,90 @@ import jakarta.inject.Singleton;
 @Singleton
 class GmmCalc {
 
-  private static final double PGA_PERIOD = 0.001;
-
   /* Compute ground motion response spectra. */
-  static Map<Gmm, GmmData> spectra(Request request, boolean commonImts) {
-
-    /*
-     * NOTE: At present, program assumes that all supplied Gmms support PGA.
-     * Although most currently implemented models do, this may not be the case
-     * in the future and program may produce unexpected results.
-     */
-
-    /* Common imts and periods; may not be used. */
+  static Map<Gmm, GmmSpectraData> spectra(Request request) {
     Set<Imt> saImts = Gmm.responseSpectrumImts(request.gmms);
-    List<Double> periods = new ArrayList<>();
-    periods.add(PGA_PERIOD);
-    periods.addAll(Imt.periods(saImts));
 
-    Map<Gmm, GmmData> gmmSpectra = new EnumMap<>(Gmm.class);
+    Map<Gmm, GmmSpectraData> gmmSpectra = new EnumMap<>(Gmm.class);
+
     for (Gmm gmm : request.gmms) {
-      if (!commonImts) {
-        saImts = gmm.responseSpectrumImts();
-        periods = ImmutableList.<Double> builder()
-            .add(PGA_PERIOD)
-            .addAll(Imt.periods(saImts))
-            .build();
-      }
+      List<LogicTree<GroundMotion>> saImtTrees = saImts.stream()
+          .map(imt -> gmm.instance(imt).calc(request.input))
+          .collect(Collectors.toList());
+
+      gmmSpectra.put(
+          gmm,
+          new GmmSpectraData(
+              treeToDataGroup(gmm, Imt.PGA, request.input),
+              treeToDataGroup(gmm, Imt.PGV, request.input),
+              treesToDataGroup(Imt.periods(saImts), saImtTrees)));
+    }
 
-      List<LogicTree<GroundMotion>> imtTrees = new ArrayList<>();
-      imtTrees.add(gmm.instance(Imt.PGA).calc(request.input));
-      for (Imt imt : saImts) {
-        imtTrees.add(gmm.instance(imt).calc(request.input));
-      }
+    return gmmSpectra;
+  }
 
-      GmmData dataGroup = treesToDataGroup(periods, imtTrees);
-      gmmSpectra.put(gmm, dataGroup);
+  private static Optional<GmmData<Double>> treeToDataGroup(Gmm gmm, Imt imt, GmmInput input) {
+    Optional<GmmData<Double>> dataGroup = Optional.empty();
+
+    if (gmm.supportedImts().contains(imt)) {
+      LogicTree<GroundMotion> imtTrees = gmm.instance(imt).calc(input);
+      dataGroup = Optional.of(treeToDataGroup(imtTrees));
     }
-    return gmmSpectra;
+
+    return dataGroup;
   }
 
-  private static GmmData treesToDataGroup(
+  private static GmmData<Double> treeToDataGroup(LogicTree<GroundMotion> tree) {
+    List<String> branchIds = tree.stream()
+        .map(Branch::id)
+        .collect(Collectors.toList());
+
+    GroundMotion combined = GroundMotions.combine(tree);
+    double μs = combined.mean();
+    double σs = combined.sigma();
+
+    List<EpiBranch<Double>> epiBranches = new ArrayList<>();
+
+    // short circuit if tree is single branch
+    if (tree.size() > 1) {
+      // branch index, imt index
+      double[] μBranches = tree.stream().mapToDouble(branch -> branch.value().mean()).toArray();
+      double[] σBranches = tree.stream().mapToDouble(branch -> branch.value().sigma()).toArray();
+      double[] weights = tree.stream().mapToDouble(branch -> branch.weight()).toArray();
+
+      for (int i = 0; i < tree.size(); i++) {
+        EpiBranch<Double> epiBranch = new EpiBranch<>(
+            branchIds.get(i),
+            μBranches[i],
+            σBranches[i],
+            weights[i]);
+
+        epiBranches.add(epiBranch);
+      }
+    }
+
+    return new GmmData<>(μs, σs, epiBranches);
+  }
+
+  private static GmmDataXs<double[]> treesToDataGroup(
       List<Double> xValues,
       List<LogicTree<GroundMotion>> trees) {
 
     // Can't use Trees.transpose() because some
     // GMMs have period dependent weights
 
+    List<GmmData<Double>> imtData = trees.stream()
+        .map(tree -> treeToDataGroup(tree))
+        .collect(Collectors.toList());
+
     LogicTree<GroundMotion> modelTree = trees.get(0);
+    List<EpiBranch<double[]>> epiBranches = new ArrayList<>();
+
     List<String> branchIds = modelTree.stream()
         .map(Branch::id)
         .collect(Collectors.toList());
 
-    double[] xs = xValues.stream().mapToDouble(Double::doubleValue).toArray();
-    double[] μs = new double[xValues.size()];
-    double[] σs = new double[xValues.size()];
-
-    // build combined μ and σ
-    for (int i = 0; i < trees.size(); i++) {
-      GroundMotion combined = GroundMotions.combine(trees.get(i));
-      μs[i] = combined.mean();
-      σs[i] = combined.sigma();
-    }
-
-    List<EpiBranch> epiBranches = new ArrayList<>();
-
-    // short circuit if tree is single branch
     if (modelTree.size() > 1) {
-
-      // branch index, imt index
       List<double[]> μBranches = new ArrayList<>();
       List<double[]> σBranches = new ArrayList<>();
       List<double[]> weights = new ArrayList<>();
@@ -118,19 +135,20 @@ class GmmCalc {
       }
 
       // imt indexing
-      for (int i = 0; i < trees.size(); i++) {
-        LogicTree<GroundMotion> tree = trees.get(i);
-        // epi branch indexing
-        for (int j = 0; j < tree.size(); j++) {
-          Branch<GroundMotion> branch = tree.get(j);
-          μBranches.get(j)[i] = branch.value().mean();
-          σBranches.get(j)[i] = branch.value().sigma();
-          weights.get(j)[i] = branch.weight();
+      for (int i = 0; i < imtData.size(); i++) {
+        GmmData<Double> data = imtData.get(i);
+
+        // branch indexing
+        for (int j = 0; j < data.tree.size(); j++) {
+          EpiBranch<Double> branch = data.tree.get(j);
+          μBranches.get(j)[i] = branch.μs;
+          σBranches.get(j)[i] = branch.σs;
+          weights.get(j)[i] = branch.weights;
         }
       }
 
       for (int i = 0; i < modelTree.size(); i++) {
-        EpiBranch epiBranch = new EpiBranch(
+        EpiBranch<double[]> epiBranch = new EpiBranch<>(
             branchIds.get(i),
             μBranches.get(i),
             σBranches.get(i),
@@ -139,18 +157,21 @@ class GmmCalc {
       }
     }
 
-    GmmData gmmData = new GmmData(xs, μs, σs, epiBranches);
-    return gmmData;
+    double[] xs = xValues.stream().mapToDouble(Double::doubleValue).toArray();
+    double[] μs = imtData.stream().mapToDouble(data -> data.μs).toArray();
+    double[] σs = imtData.stream().mapToDouble(data -> data.σs).toArray();
+
+    return new GmmDataXs<>(xs, μs, σs, epiBranches);
   }
 
   /* Compute ground motions over a range of distances. */
-  static Map<Gmm, GmmData> distance(Distance.Request request, double[] distances) {
+  static Map<Gmm, GmmDataXs<double[]>> distance(Distance.Request request, double[] distances) {
     var inputList = hangingWallDistances(request.input, distances);
     return calculateGroundMotions(request.gmms, inputList, request.imt, distances);
   }
 
   /* Compute ground motions over a range of magnitudes. */
-  static Map<Gmm, GmmData> magnitude(
+  static Map<Gmm, GmmDataXs<double[]>> magnitude(
       Magnitude.Request request,
       double[] magnitudes,
       double distance) {
@@ -205,13 +226,13 @@ class GmmCalc {
         .build();
   }
 
-  private static Map<Gmm, GmmData> calculateGroundMotions(
+  private static Map<Gmm, GmmDataXs<double[]>> calculateGroundMotions(
       Set<Gmm> gmms,
       List<GmmInput> gmmInputs,
       Imt imt,
       double[] distances) {
 
-    Map<Gmm, GmmData> gmValues = new EnumMap<>(Gmm.class);
+    Map<Gmm, GmmDataXs<double[]>> gmValues = new EnumMap<>(Gmm.class);
 
     for (Gmm gmm : gmms) {
       List<LogicTree<GroundMotion>> trees = new ArrayList<>();
@@ -219,7 +240,7 @@ class GmmCalc {
       for (GmmInput gmmInput : gmmInputs) {
         trees.add(model.calc(gmmInput));
       }
-      GmmData dataGroup = GmmCalc.treesToDataGroup(Doubles.asList(distances), trees);
+      GmmDataXs<double[]> dataGroup = GmmCalc.treesToDataGroup(Doubles.asList(distances), trees);
       gmValues.put(gmm, dataGroup);
     }
     return gmValues;
@@ -241,34 +262,59 @@ class GmmCalc {
     return rRupLo + (r - rCutLo) / rCutΔ * rRupΔ;
   }
 
-  static class GmmData {
+  static class GmmSpectraData {
+    final Optional<GmmData<Double>> pga;
+    final Optional<GmmData<Double>> pgv;
+    final GmmDataXs<double[]> sa;
+
+    GmmSpectraData(
+        Optional<GmmData<Double>> pga,
+        Optional<GmmData<Double>> pgv,
+        GmmDataXs<double[]> sa) {
+      this.pga = pga;
+      this.pgv = pgv;
+      this.sa = sa;
+    }
+  }
 
-    final double[] xs;
-    final double[] μs;
-    final double[] σs;
-    final List<EpiBranch> tree;
+  static class GmmData<T> {
+    final T μs;
+    final T σs;
+    final List<EpiBranch<T>> tree;
 
     GmmData(
-        double[] xs,
-        double[] μs,
-        double[] σs,
-        List<EpiBranch> tree) {
+        T μs,
+        T σs,
+        List<EpiBranch<T>> tree) {
 
-      this.xs = xs;
       this.μs = μs;
       this.σs = σs;
       this.tree = tree;
     }
   }
 
-  static class EpiBranch {
+  static class GmmDataXs<T> extends GmmData<T> {
+
+    final T xs;
+
+    GmmDataXs(
+        T xs,
+        T μs,
+        T σs,
+        List<EpiBranch<T>> tree) {
+      super(μs, σs, tree);
+      this.xs = xs;
+    }
+  }
+
+  static class EpiBranch<T> {
 
     final String id;
-    final double[] μs;
-    final double[] σs;
-    final double[] weights;
+    final T μs;
+    final T σs;
+    final T weights;
 
-    EpiBranch(String id, double[] μs, double[] σs, double[] weights) {
+    EpiBranch(String id, T μs, T σs, T weights) {
       this.id = id;
       this.μs = μs;
       this.σs = σs;
@@ -276,4 +322,58 @@ class GmmCalc {
     }
   }
 
+  static class SpectraTree {
+    final String id;
+    final TreeValues values;
+    final TreeValues weights;
+
+    SpectraTree(String id, TreeValues values, TreeValues weights) {
+      this.id = id;
+      this.values = values;
+      this.weights = weights;
+    }
+
+    static List<SpectraTree> toList(
+        List<EpiSeries<Double>> pgaBranches,
+        List<EpiSeries<Double>> pgvBranches,
+        List<EpiSeries<double[]>> saBranches) {
+      List<SpectraTree> trees = new ArrayList<>();
+
+      for (int i = 0; i < saBranches.size(); i++) {
+        Optional<EpiSeries<Double>> pga =
+            pgaBranches.isEmpty() ? Optional.empty() : Optional.of(pgaBranches.get(i));
+        Optional<EpiSeries<Double>> pgv =
+            pgvBranches.isEmpty() ? Optional.empty() : Optional.of(pgvBranches.get(i));
+
+        EpiSeries<double[]> sa = saBranches.get(i);
+
+        TreeValues values = new TreeValues(
+            pga.isPresent() ? pga.get().values : null,
+            pgv.isPresent() ? pgv.get().values : null,
+            sa.values);
+
+        TreeValues weights = new TreeValues(
+            pga.isPresent() ? pga.get().weights : null,
+            pgv.isPresent() ? pgv.get().weights : null,
+            sa.weights);
+
+        trees.add(new SpectraTree(sa.id, values, weights));
+      }
+
+      return trees;
+    }
+  }
+
+  static class TreeValues {
+    final Double pga;
+    final Double pgv;
+    final double[] sa;
+
+    TreeValues(Double pga, Double pgv, double[] sa) {
+      this.pga = pga;
+      this.pgv = pgv;
+      this.sa = sa;
+    }
+  }
+
 }
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmController.java b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmController.java
index bea64c3f8044c5e40c9f0a90fe05bf6fccd4e306..393c912a260a071359447f91dbaa9c085d51fb84 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmController.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmController.java
@@ -545,43 +545,4 @@ class GmmController {
       return Utils.handleError(e, id.name, http.getUri().getPath());
     }
   }
-
-  // /**
-  // * POST method for computing bulk response spectra.
-  // *
-  // * <p> Web service path: /nshmp/data/gmm/spectra
-  // *
-  // * <p> The body of the POST request must be a CSV file of Gmm inputs
-  // *
-  // * @param request The HTTP request
-  // * @param gmm The ground motion models
-  // * @param gmmInputs The CSV file
-  // */
-  // @Operation(
-  // summary = "Return response spectrum given a ground motion model and
-  // parameters",
-  // description = "Returns multiple response spectra given a GMM and a " +
-  // " CSV file of parameters.\n\n" +
-  // "For supported GMMs and *GmmInput* parameters with default values " +
-  // "see the usage information.\n\n",
-  // operationId = "gmm_spectra_doPostSpectra")
-  // @ApiResponse(
-  // description = "Response spectrum",
-  // responseCode = "200",
-  // content = @Content(schema = @Schema(type = "string")))
-  // @Post(
-  // uri = "/spectra{?gmm}",
-  // consumes = MediaType.TEXT_PLAIN,
-  // produces = MediaType.APPLICATION_JSON)
-  // public HttpResponse<String> doPost(
-  // HttpRequest<?> request,
-  // @QueryValue @Nullable Set<Gmm> gmm,
-  // @Body @Nullable String gmmInputs) {
-  // try {
-  // return GmmSpectraService.handleDoPost(request, gmm, gmmInputs);
-  // } catch (Exception e) {
-  // return Utils.handleError(e, Id.SPECTRA.name, request.getUri().getPath());
-  // }
-  // }
-
 }
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmService.java b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmService.java
index e1b737a2ef2dda0c087fe57fa7c283036ec77e23..4b1c53ab1529841b1b403bb0f628bdb06c41aeb4 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmService.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmService.java
@@ -19,7 +19,10 @@ import gov.usgs.earthquake.nshmp.gmm.Imt;
 import gov.usgs.earthquake.nshmp.www.ResponseBody;
 import gov.usgs.earthquake.nshmp.www.ResponseMetadata;
 import gov.usgs.earthquake.nshmp.www.WsVersion;
-import gov.usgs.earthquake.nshmp.www.gmm.GmmCalc.GmmData;
+import gov.usgs.earthquake.nshmp.www.gmm.GmmCalc.EpiBranch;
+import gov.usgs.earthquake.nshmp.www.gmm.GmmCalc.GmmDataXs;
+import gov.usgs.earthquake.nshmp.www.gmm.GmmCalc.GmmSpectraData;
+import gov.usgs.earthquake.nshmp.www.gmm.GmmCalc.SpectraTree;
 import gov.usgs.earthquake.nshmp.www.gmm.XyDataGroup.EpiSeries;
 
 import io.micronaut.http.HttpRequest;
@@ -54,10 +57,10 @@ class GmmService {
   }
 
   /* Response object for all GMM services. */
-  static class Response {
+  static class Response<T, U> {
 
-    XyDataGroup means;
-    XyDataGroup sigmas;
+    XyDataGroup<T, U> means;
+    XyDataGroup<T, U> sigmas;
 
     private Response(Id service, Optional<Imt> imt) {
       String yLabelMedian = imt.isPresent()
@@ -75,43 +78,128 @@ class GmmService {
           service.yLabelSigma);
     }
 
-    static Response create(
+    static Response<XySequence, EpiSeries<double[]>> create(
         Id service,
-        Map<Gmm, GmmData> result,
+        Map<Gmm, GmmDataXs<double[]>> result,
         Optional<Imt> imt) {
 
-      Response response = new Response(service, imt);
+      Response<XySequence, EpiSeries<double[]>> response = new Response<>(service, imt);
 
-      for (Entry<Gmm, GmmData> entry : result.entrySet()) {
+      for (Entry<Gmm, GmmDataXs<double[]>> entry : result.entrySet()) {
         Gmm gmm = entry.getKey();
-        GmmData data = entry.getValue();
+        GmmDataXs<double[]> data = entry.getValue();
 
         XySequence μTotal = XySequence.create(data.xs, formatExp(data.μs));
         XySequence σTotal = XySequence.create(data.xs, format(data.σs));
 
-        List<EpiSeries> μEpi = List.of();
-        List<EpiSeries> sigmaEpi = List.of();
+        EpiGroup<double[]> group = treeToEpiGroup(data.tree);
+        response.means.add(gmm.name(), gmm.toString(), μTotal, group.μ);
+        response.sigmas.add(gmm.name(), gmm.toString(), σTotal, group.sigma);
+      }
+
+      return response;
+    }
 
-        // If we have a GMM with a single GroundMotion branch,
-        // then the DataGroup branch list is empty
-        if (!data.tree.isEmpty()) {
-          boolean hasSigmaBranches = data.tree.get(0).id.contains(" : ");
+    private static EpiGroup<Double> treeToEpiGroupSingle(List<EpiBranch<Double>> tree) {
+      List<EpiSeries<Double>> μEpi = List.of();
+      List<EpiSeries<Double>> sigmaEpi = List.of();
 
-          μEpi = data.tree.stream()
-              .map(b -> new EpiSeries(b.id, formatExp(b.μs), b.weights))
+      // If we have a GMM with a single GroundMotion branch,
+      // then the DataGroup branch list is empty
+      if (!tree.isEmpty()) {
+        boolean hasSigmaBranches = tree.get(0).id.contains(" : ");
+
+        μEpi = tree.stream()
+            .map(b -> new EpiSeries<>(b.id, formatExp(b.μs), b.weights))
+            .collect(Collectors.toList());
+
+        if (hasSigmaBranches) {
+          μEpi = collapseGmTreeSingle(μEpi, 0);
+
+          sigmaEpi = tree.stream()
+              .map(b -> new EpiSeries<>(b.id, format(b.σs), b.weights))
               .collect(Collectors.toList());
+          sigmaEpi = collapseGmTreeSingle(sigmaEpi, 1);
+        }
+      }
+
+      return new EpiGroup<>(μEpi, sigmaEpi);
+    }
+
+    private static EpiGroup<double[]> treeToEpiGroup(List<EpiBranch<double[]>> tree) {
+      List<EpiSeries<double[]>> μEpi = List.of();
+      List<EpiSeries<double[]>> sigmaEpi = List.of();
+
+      // If we have a GMM with a single GroundMotion branch,
+      // then the DataGroup branch list is empty
+      if (!tree.isEmpty()) {
+        boolean hasSigmaBranches = tree.get(0).id.contains(" : ");
+
+        μEpi = tree.stream()
+            .map(b -> new EpiSeries<>(b.id, formatExp(b.μs), b.weights))
+            .collect(Collectors.toList());
+
+        if (hasSigmaBranches) {
+          μEpi = collapseGmTree(μEpi, 0);
+
+          sigmaEpi = tree.stream()
+              .map(b -> new EpiSeries<>(b.id, format(b.σs), b.weights))
+              .collect(Collectors.toList());
+          sigmaEpi = collapseGmTree(sigmaEpi, 1);
+        }
+      }
+
+      return new EpiGroup<>(μEpi, sigmaEpi);
+    }
+
+    static Response<SpectraData, SpectraTree> spectraCreate(
+        Id service,
+        Map<Gmm, GmmSpectraData> result,
+        Optional<Imt> imt) {
+
+      Response<SpectraData, SpectraTree> response = new Response<>(service, imt);
+
+      for (Entry<Gmm, GmmSpectraData> entry : result.entrySet()) {
+        Gmm gmm = entry.getKey();
+        GmmSpectraData data = entry.getValue();
+
+        XySequence saμTotal = XySequence.create(data.sa.xs, formatExp(data.sa.μs));
+        XySequence saσTotal = XySequence.create(data.sa.xs, format(data.sa.σs));
+
+        SpectraData μTotal = new SpectraData(
+            data.pga.isPresent() ? formatExp(data.pga.get().μs) : null,
+            data.pgv.isPresent() ? formatExp(data.pgv.get().μs) : null,
+            saμTotal);
+
+        SpectraData σTotal = new SpectraData(
+            data.pga.isPresent() ? format(data.pga.get().σs) : null,
+            data.pgv.isPresent() ? format(data.pgv.get().σs) : null,
+            saσTotal);
+
+        EpiGroup<double[]> saGroup = treeToEpiGroup(data.sa.tree);
+        Optional<EpiGroup<Double>> pgaGroup = Optional.empty();
+        Optional<EpiGroup<Double>> pgvGroup = Optional.empty();
 
-          if (hasSigmaBranches) {
-            μEpi = collapseGmTree(μEpi, 0);
+        if (data.pga.isPresent()) {
+          pgaGroup = Optional.of(treeToEpiGroupSingle(data.pga.get().tree));
+        }
 
-            sigmaEpi = data.tree.stream()
-                .map(b -> new EpiSeries(b.id, format(b.σs), b.weights))
-                .collect(Collectors.toList());
-            sigmaEpi = collapseGmTree(sigmaEpi, 1);
-          }
+        if (data.pgv.isPresent()) {
+          pgvGroup = Optional.of(treeToEpiGroupSingle(data.pgv.get().tree));
         }
-        response.means.add(gmm.name(), gmm.toString(), μTotal, μEpi);
-        response.sigmas.add(gmm.name(), gmm.toString(), σTotal, sigmaEpi);
+
+        List<SpectraTree> μTrees = SpectraTree.toList(
+            pgaGroup.isPresent() ? pgaGroup.get().μ : List.of(),
+            pgvGroup.isPresent() ? pgvGroup.get().μ : List.of(),
+            saGroup.μ);
+
+        List<SpectraTree> sigmaTrees = SpectraTree.toList(
+            pgaGroup.isPresent() ? pgaGroup.get().sigma : List.of(),
+            pgvGroup.isPresent() ? pgvGroup.get().sigma : List.of(),
+            saGroup.sigma);
+
+        response.means.add(gmm.name(), gmm.toString(), μTotal, μTrees);
+        response.sigmas.add(gmm.name(), gmm.toString(), σTotal, sigmaTrees);
       }
 
       return response;
@@ -121,8 +209,9 @@ class GmmService {
   static class Spectra {
 
     static HttpResponse<String> process(Request request) {
-      Map<Gmm, GmmData> spectra = GmmCalc.spectra(request, false);
-      Response response = Response.create(request.serviceId, spectra, Optional.empty());
+      Map<Gmm, GmmSpectraData> spectra = GmmCalc.spectra(request);
+      Response<SpectraData, SpectraTree> response =
+          Response.spectraCreate(request.serviceId, spectra, Optional.empty());
       var body = ResponseBody.success()
           .name(request.serviceId.name)
           .url(request.http.getUri().getPath())
@@ -146,8 +235,9 @@ class GmmService {
 
       double[] rArray = distanceArray(request);
 
-      Map<Gmm, GmmData> gmvr = GmmCalc.distance(request, rArray);
-      Response response = Response.create(request.serviceId, gmvr, Optional.of(request.imt));
+      Map<Gmm, GmmDataXs<double[]>> gmvr = GmmCalc.distance(request, rArray);
+      Response<XySequence, EpiSeries<double[]>> response =
+          Response.create(request.serviceId, gmvr, Optional.of(request.imt));
       var body = ResponseBody.success()
           .name(request.serviceId.name)
           .url(request.http.getUri().getPath())
@@ -203,8 +293,9 @@ class GmmService {
       double[] mArray = ServiceUtil.sequenceLinear(
           request.mMin, request.mMax, request.step);
 
-      Map<Gmm, GmmData> gmvm = GmmCalc.magnitude(request, mArray, request.distance);
-      Response response = Response.create(request.serviceId, gmvm, Optional.of(request.imt));
+      Map<Gmm, GmmDataXs<double[]>> gmvm = GmmCalc.magnitude(request, mArray, request.distance);
+      Response<XySequence, EpiSeries<double[]>> response =
+          Response.create(request.serviceId, gmvm, Optional.of(request.imt));
       var body = ResponseBody.success()
           .name(request.serviceId.name)
           .url(request.http.getUri().getPath())
@@ -255,13 +346,35 @@ class GmmService {
    * branches.
    */
 
-  static List<EpiSeries> collapseGmTree(List<EpiSeries> epiList, int index) {
+  static List<EpiSeries<Double>> collapseGmTreeSingle(List<EpiSeries<Double>> epiList, int index) {
+    List<String> ids = new ArrayList<>();
+    Map<String, Double> valueMap = new HashMap<>();
+    Map<String, Double> wtMap = new HashMap<>();
+
+    for (EpiSeries<Double> epi : epiList) {
+      String id = epi.id.split(" : ")[index];
+
+      if (!ids.contains(id)) {
+        ids.add(id);
+        valueMap.put(id, epi.values);
+        wtMap.put(id, epi.weights);
+      } else {
+        wtMap.merge(id, epi.weights, (a, b) -> a + b);
+      }
+    }
+
+    return ids.stream()
+        .map(id -> new EpiSeries<>(id, valueMap.get(id), format(wtMap.get(id))))
+        .collect(Collectors.toList());
+  }
+
+  static List<EpiSeries<double[]>> collapseGmTree(List<EpiSeries<double[]>> epiList, int index) {
 
     List<String> ids = new ArrayList<>();
     Map<String, double[]> valueMap = new HashMap<>();
     Map<String, double[]> wtMap = new HashMap<>();
 
-    for (EpiSeries epi : epiList) {
+    for (EpiSeries<double[]> epi : epiList) {
       String id = epi.id.split(" : ")[index];
       if (!ids.contains(id)) {
         ids.add(id);
@@ -272,23 +385,52 @@ class GmmService {
       }
     }
     return ids.stream()
-        .map(id -> new EpiSeries(id, valueMap.get(id), format(wtMap.get(id))))
+        .map(id -> new EpiSeries<>(id, valueMap.get(id), format(wtMap.get(id))))
         .collect(Collectors.toList());
   }
 
+  private static double formatExp(double value) {
+    return Maths.roundToDigits(Math.exp(value), ServiceUtil.ROUND);
+  }
+
   private static double[] formatExp(double[] values) {
     return Arrays.stream(values)
-        .map(Math::exp)
-        .map(d -> Maths.roundToDigits(d, ServiceUtil.ROUND))
+        .map(d -> formatExp(d))
         .toArray();
   }
 
+  private static double format(double value) {
+    return Maths.roundToDigits(value, ServiceUtil.ROUND);
+  }
+
   private static double[] format(double[] values) {
     return Arrays.stream(values)
-        .map(d -> Maths.roundToDigits(d, ServiceUtil.ROUND))
+        .map(d -> format(d))
         .toArray();
   }
 
+  static class SpectraData {
+    final Double pga;
+    final Double pgv;
+    final XySequence sa;
+
+    SpectraData(Double pga, Double pgv, XySequence sa) {
+      this.pga = pga;
+      this.pgv = pgv;
+      this.sa = sa;
+    }
+  }
+
+  private static class EpiGroup<T> {
+    final List<EpiSeries<T>> μ;
+    final List<EpiSeries<T>> sigma;
+
+    EpiGroup(List<EpiSeries<T>> μ, List<EpiSeries<T>> sigma) {
+      this.μ = μ;
+      this.sigma = sigma;
+    }
+  }
+
   public static enum Id {
 
     DISTANCE(
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/XyDataGroup.java b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/XyDataGroup.java
index 2b3b3caac1b42423aff0ae05e212b2d0a33861d3..c7a233cef7d4dcf868046591f5d8f881936de514 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/XyDataGroup.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/XyDataGroup.java
@@ -3,20 +3,18 @@ package gov.usgs.earthquake.nshmp.www.gmm;
 import java.util.ArrayList;
 import java.util.List;
 
-import gov.usgs.earthquake.nshmp.data.XySequence;
-
 /*
  * XY data sequences serialization object.
  *
  * @author U.S. Geological Survey
  */
 @SuppressWarnings("unused")
-class XyDataGroup {
+class XyDataGroup<T, U> {
 
   private final String label;
   private final String xLabel;
   private final String yLabel;
-  private final List<Series> data;
+  private final List<Series<T, U>> data;
 
   private XyDataGroup(String label, String xLabel, String yLabel) {
     this.label = label;
@@ -26,24 +24,24 @@ class XyDataGroup {
   }
 
   /* Create a data group. */
-  static XyDataGroup create(String name, String xLabel, String yLabel) {
-    return new XyDataGroup(name, xLabel, yLabel);
+  static <T, U> XyDataGroup<T, U> create(String name, String xLabel, String yLabel) {
+    return new XyDataGroup<T, U>(name, xLabel, yLabel);
   }
 
   /* Add a data sequence */
-  XyDataGroup add(String id, String name, XySequence data, List<EpiSeries> tree) {
-    this.data.add(new Series(id, name, data, tree));
+  XyDataGroup<T, U> add(String id, String name, T data, List<U> tree) {
+    this.data.add(new Series<>(id, name, data, tree));
     return this;
   }
 
-  static class Series {
+  static class Series<T, U> {
 
     private final String id;
     private final String label;
-    private final XySequence data;
-    private final List<EpiSeries> tree;
+    private final T data;
+    private final List<U> tree;
 
-    Series(String id, String label, XySequence data, List<EpiSeries> tree) {
+    Series(String id, String label, T data, List<U> tree) {
       this.id = id;
       this.label = label;
       this.data = data;
@@ -51,13 +49,13 @@ class XyDataGroup {
     }
   }
 
-  static class EpiSeries {
+  static class EpiSeries<T> {
 
     final String id;
-    final double[] values;
-    final double[] weights;
+    final T values;
+    final T weights;
 
-    EpiSeries(String id, double[] values, double[] weights) {
+    EpiSeries(String id, T values, T weights) {
       this.id = id;
       this.values = values;
       this.weights = weights;