diff --git a/docs/README.md b/docs/README.md
index 8b2bfaf034f9a1c144480f7b7e71414dfc140c29..6a952c9a80f9eb8e65ade42981d5639c794a3679 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -30,6 +30,7 @@ use *nshmp-haz* as well as underlying model implementation details.
 * [USGS Models](./pages/USGS-Models.md)
   * [Model Editions](./pages/Model-Editions.md)
   * [Logic Trees & Uncertainty](./pages/Logic-Trees-&-Uncertainty.md)
+  * [Code Versions](./pages/Code-Versions.md)
 
 ## Related Information
 
diff --git a/docs/pages/Code-Versions.md b/docs/pages/Code-Versions.md
new file mode 100644
index 0000000000000000000000000000000000000000..801a9b66faf3a8eef92d1605d50b02a7a16b6d39
--- /dev/null
+++ b/docs/pages/Code-Versions.md
@@ -0,0 +1,54 @@
+# Code Versions
+
+The static datasets of USGS NSHMs prior to 2014 were computed using Fortran (see
+[_nshmp-haz-fortran_](https://github.com/usgs/nshmp-haz-fortran])). The static datasets for the
+2014 Conterminous U.S. NSHM were computed using the Fortran codes and
+[OpenSHA](https://opensha.org/) (for the California portion of the model). The dynamic versions
+of the 2008 and 2014 Conterminous U.S. models were then implemented in the 1st version of
+[_nshmp-haz_](https://github.com/usgs/nshmp-haz) (on GitHub). This updated Java codebase uses XML
+source models and supports the web services behind the dynamic calculations of the [Unified Hazard
+Hazard Tool](https://earthquake.usgs.gov/hazards/interactive/) (UHT).
+
+The 2nd version of _nshmp-haz_ (this repository) supercedes prior codebases. The development of this
+version involved a significant refactoring of both the computational code and source model format.
+The source models are now defined using JSON, GeoJSON, and CSV files to better reflect the
+underlying logic trees and support uncertainty analysis.
+
+## Transitioning from _nshmp-haz_ v1 to v2
+
+NSHMs are very detailed and migrating from one format to another is not trivial and prone to error.
+Moreover, approximations (e.g. using 3.1415 for Pi rather than the the value built into most
+languages) can yield different results. When multiple such small changes exist, deciphering what
+is giving rise to differences in results can be challenging.
+
+To document the transition from _nshmp-haz_ v1 to v2, we here attach comparison maps at four return
+periods (475, 975, 2475, and 10,000 year) for the 2018 Conterminous U.S. model. Maps are included
+for PGA and 3 spectral periods ( 0.2 s, 1 s, and 5 s). There are no differences in the Central &
+Eastern U.S. and differences in the WUS are <<1%. The difference in hazard in the vicinity of
+Salt Lake City arises from the cluster models in _nshmp-haz_ v1 not being able to consider the
+additional epistemic uncertainty added to the the NGA-West2 ground motion models.
+
+We continue to investigate the cause of other differences but they are small enough that we are
+comfortable moving forward deploying _nshmp-haz_ v2 codes and models to our public web services and
+applications. This repository includes end-to-end tests for supported NSHMs that may be run
+on demand.
+
+[Download v1 to v2 difference and ratio maps](https://code.usgs.gov/ghsc/users/pmpowers/nshmp-haz/-/raw/code-version-doc-506/docs/pages/images/comp_JSON_vs_XML_0p2-grid-20220216-BC.pdf?inline=false)
+
+## Example map: 0.2 s, 2475-yr
+
+![0.2 s SA, 2475-yr](./images/JSON_vs_XML-SA0P2-2475.jpg)
+
+---
+
+## Related Pages
+
+* [USGS Models](./USGS-Models.md#usgs-models)
+  * [Model Editions](./Model-Editions.md#model-editions)
+  * [Logic Trees & Uncertainty](./Logic-Trees-&-Uncertainty.md#logic-trees-&-uncertainty)
+  * [Code Versions](./Code-Versions.md#code-versions)
+* [**Documentation Index**](../README.md)
+
+---
+![USGS logo](./images/usgs-icon.png) &nbsp;[U.S. Geological Survey](https://www.usgs.gov)
+National Seismic Hazard Mapping Project ([NSHMP](https://earthquake.usgs.gov/hazards/))
diff --git a/docs/pages/Logic-Trees-&-Uncertainty.md b/docs/pages/Logic-Trees-&-Uncertainty.md
index 87a4e0c86136a6f1c633d1f4350ec6e8bf9a0a94..313fb029db6a4a395abeecf9ac0f10d01a28b1d1 100644
--- a/docs/pages/Logic-Trees-&-Uncertainty.md
+++ b/docs/pages/Logic-Trees-&-Uncertainty.md
@@ -114,6 +114,7 @@ alternative models.
 * [USGS Models](./USGS-Models.md#usgs-models)
   * [Model Editions](./Model-Editions.md#model-editions)
   * [Logic Trees & Uncertainty](./Logic-Trees-&-Uncertainty.md#logic-trees-&-uncertainty)
+  * [Code Versions](./Code-Versions.md#code-versions)
 * [**Documentation Index**](../README.md)
 
 ---
diff --git a/docs/pages/Model-Editions.md b/docs/pages/Model-Editions.md
index 0509a8bc7aea43f5cee1712af7f8d5b425c5dc9d..b875007e6755685aac757fc9e3d804c765357083 100644
--- a/docs/pages/Model-Editions.md
+++ b/docs/pages/Model-Editions.md
@@ -24,26 +24,26 @@ version numbers. Links to documentation and data for each of the models below ar
 page. Changes between editions in model regions are documented in the release notes of the
 individual model repositories.
 
-Region | Year | Version | Static | Dynamic | Notes |
--------|:----:|:-------:|:------:|:-------:|-------|
-Conterminous U.S. | 2018 | [v5.0.0](https://code.usgs.gov/ghsc/nshmp/nshms/nshm-conus)<sup>†</sup> | |:small_blue_diamond:| |
-Conterminous U.S. | 2014 | [v4.2.0](https://code.usgs.gov/ghsc/nshmp/nshms/nshm-conus)<sup>†</sup> | |:small_blue_diamond:| |
-Conterminous U.S. | 2014 | [v4.1.4](https://code.usgs.gov/ghsc/nshmp/nshms/nshm-conus)<sup>†</sup> | |:small_blue_diamond:| |
-Conterminous U.S. | 2014 | [v4.0.0](https://github.com/usgs/nshmp-haz-fortran/releases/tag/nshm2014r1) |:small_blue_diamond:| | ASCE7-16 |
-Conterminous U.S. | 2008 | v3.3.3 | |:small_blue_diamond:| |
-Conterminous U.S. | 2008 | [v3.2.0](https://github.com/usgs/nshmp-haz-fortran/releases/tag/nshm2008r3) |:small_blue_diamond:| | |
-Conterminous U.S. | 2008 | v3.1.0 |:small_blue_diamond:| | ASCE7-10 |
-Conterminous U.S. | 2008 | v3.0.0 | | | |
-Conterminous U.S. | 2002 | v2.0.0 | | | |
-Conterminous U.S. | 1996 | v1.0.0 | | | |
-Alaska            | 2007 | v2.1.0 | |:small_blue_diamond:| |
-Alaska            | 2007 | v2.0.0 |:small_blue_diamond:| | ASCE7-10 |
-Alaska            | 1999 | v1.0.0 | | | |
-American Samoa    | 2012 | v1.0.0 | | | |
-Guam              | 2012 | v1.0.0 | | | |
-Hawaii            | 2018 | v2.0.0 | | TBD | |
-Hawaii            | 1998 | v1.1.0 | | TBD | |
-Hawaii            | 1998 | v1.0.0 |:small_blue_diamond:| | ASCE7-10 |
+Region            | Year | Version | Static | Dynamic | Notes |
+------------------|:----:|:-------:|:------:|:-------:|-------|
+Conterminous U.S. | 2018 | [5.0.0](https://code.usgs.gov/ghsc/nshmp/nshms/nshm-conus)<sup>†</sup> | |:small_blue_diamond:| |
+Conterminous U.S. | 2014 | [4.2.0](https://code.usgs.gov/ghsc/nshmp/nshms/nshm-conus)<sup>†</sup> | |:small_blue_diamond:| |
+Conterminous U.S. | 2014 | [4.1.4](https://code.usgs.gov/ghsc/nshmp/nshms/nshm-conus)<sup>†</sup> | |:small_blue_diamond:| |
+Conterminous U.S. | 2014 | [4.0.0](https://github.com/usgs/nshmp-haz-fortran/releases/tag/nshm2014r1) |:small_blue_diamond:| | ASCE7-16 |
+Conterminous U.S. | 2008 | 3.3.3 | |:small_blue_diamond:| |
+Conterminous U.S. | 2008 | [3.2.0](https://github.com/usgs/nshmp-haz-fortran/releases/tag/nshm2008r3) |:small_blue_diamond:| | |
+Conterminous U.S. | 2008 | 3.1.0 |:small_blue_diamond:| | ASCE7-10 |
+Conterminous U.S. | 2008 | 3.0.0 | | | |
+Conterminous U.S. | 2002 | 2.0.0 | | | |
+Conterminous U.S. | 1996 | 1.0.0 | | | |
+Alaska            | 2007 | 2.1.0 | |:small_blue_diamond:| |
+Alaska            | 2007 | 2.0.0 |:small_blue_diamond:| | ASCE7-10 |
+Alaska            | 1999 | 1.0.0 | | | |
+American Samoa    | 2012 | 1.0.0 | | | |
+Guam              | 2012 | 1.0.0 | | | |
+Hawaii            | 2018 | 2.0.0 | | TBD | |
+Hawaii            | 1998 | 1.1.0 | | TBD | |
+Hawaii            | 1998 | 1.0.0 |:small_blue_diamond:| | ASCE7-10 |
 Puerto Rico & <br/> U.S. Virgin Islands | 2003 | v1.0.0 | | | |
 
 <sup>†</sup> __Note on the 2014 conterminous U.S. NSHM:__ Initial publication of the
@@ -98,6 +98,7 @@ one of the dynamic editions is likely better.
 * [USGS Models](./USGS-Models.md#usgs-models)
   * [Model Editions](./Model-Editions.md#model-editions)
   * [Logic Trees & Uncertainty](./Logic-Trees-&-Uncertainty.md#logic-trees-&-uncertainty)
+  * [Code Versions](./Code-Versions.md#code-versions)
 * [**Documentation Index**](../README.md)
 
 ---
diff --git a/docs/pages/USGS-Models.md b/docs/pages/USGS-Models.md
index 087c0af1389b34de60792bdaebc3b7396b8cda29..63f3ce6a0cbbb8efcac07365b404d91b1bbb924d 100644
--- a/docs/pages/USGS-Models.md
+++ b/docs/pages/USGS-Models.md
@@ -32,6 +32,7 @@ Each model region has a dedicated repository with version tags marking different
 * [USGS Models](./USGS-Models.md#usgs-models)
   * [Model Editions](./Model-Editions.md#model-editions)
   * [Logic Trees & Uncertainty](./Logic-Trees-&-Uncertainty.md#logic-trees-&-uncertainty)
+  * [Code Versions](./Code-Versions.md#code-versions)
 * [**Documentation Index**](../README.md)
 
 ---
diff --git a/docs/pages/images/JSON_vs_XML-SA0P2-2475.jpg b/docs/pages/images/JSON_vs_XML-SA0P2-2475.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..ca8058979292940704d68964ca215fc9347ec40b
Binary files /dev/null and b/docs/pages/images/JSON_vs_XML-SA0P2-2475.jpg differ
diff --git a/docs/pages/images/comp_JSON_vs_XML_0p2-grid-20220216-BC.pdf b/docs/pages/images/comp_JSON_vs_XML_0p2-grid-20220216-BC.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..ad3bce8c30c86f53ce637f925e44ed243dc40c96
Binary files /dev/null and b/docs/pages/images/comp_JSON_vs_XML_0p2-grid-20220216-BC.pdf differ
diff --git a/etc/examples/README.md b/etc/examples/README.md
index 343bc5b7fc525aa731e601cd2aebeeabc27459c5..75b377a93b08ca4cdd70acdb7520248aadbc5e9c 100644
--- a/etc/examples/README.md
+++ b/etc/examples/README.md
@@ -1,6 +1,6 @@
 # Examples
 
-These examples are designed to be executed locally while following the READMEs on GitLub.
+These examples are designed to be executed locally while following the READMEs on GitLab.
 All examples avoid a lengthy call to Java and the `HazardCalc` program by using the following
 system alias:
 
diff --git a/gradle.properties b/gradle.properties
index 5f64b407c17c5874ee42ac159ae4929b48bbedf8..bc1f8a0e2edb13853946e2aa5850530248f86af8 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -10,7 +10,7 @@ micronautRxVersion = 2.1.1
 micronautPluginVersion = 3.1.1
 nodePluginVersion = 3.0.1
 nodeVersion = 16.3.0
-nshmpLibVersion = 0.9.4
+nshmpLibVersion = 0.9.10
 nshmpWsUtilsVersion = 0.1.7
 shadowVersion = 7.1.2
 spotbugsVersion = 4.7.0
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/DisaggCalc.java b/src/main/java/gov/usgs/earthquake/nshmp/DisaggCalc.java
index 2b268e5b92ef29a307470c56d5df8b6680c8c077..b54668199f1745ef42b6b2ac17a116fb8efb7ec7 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/DisaggCalc.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/DisaggCalc.java
@@ -22,6 +22,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Optional;
+import java.util.OptionalDouble;
 import java.util.Set;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -155,26 +156,35 @@ public class DisaggCalc {
       int colsToSkip = siteColumns.size(); // needed?
       log.info("Site data columns: " + colsToSkip);
 
+      /* Possible batch vs30 from config.vs30s. */
+      checkArgument(
+          config.hazard.vs30s.size() <= 1,
+          "config.hazard.vs30s may only have one value for disagg");
+      OptionalDouble vs30 = (config.hazard.vs30s.size() == 1)
+          ? OptionalDouble.of(config.hazard.vs30s.iterator().next())
+          : OptionalDouble.empty();
+
       /* Sites */
-      List<Site> sites = Sites.fromCsv(siteFile, config, model.siteData());
+      List<Site> sites = Sites.fromCsv(siteFile, model.siteData(), vs30);
       log.info("Sites: " + sites.size());
 
       Set<Imt> modelImts = model.config().hazard.imts;
 
+      Path out = HazardCalc.createOutputDir(config.output.directory);
+
       /*
        * If no IML columns present, disaggregate at IMTs and return period from
        * config, otherwise disaggregate at target IMLs are present.
        *
-       * We've removed support for gejson site files at present.
+       * We've removed support for geojson site files at present.
        */
-      Path out;
       if (siteColumns.size() == allColumns.size()) {
 
         checkArgument(
             modelImts.containsAll(config.hazard.imts),
             "Config specifies IMTs not supported by model");
         double returnPeriod = config.disagg.returnPeriod;
-        out = calcRp(model, config, sites, returnPeriod, log);
+        calcRp(model, config, sites, returnPeriod, out, log);
 
       } else {
 
@@ -187,7 +197,7 @@ public class DisaggCalc {
             sites.size() == imls.size(),
             "Sites and spectra lists different sizes");
         log.info("Spectra: " + imls.size()); // 1:1 with sites
-        out = calcIml(model, config, sites, imls, log);
+        calcIml(model, config, sites, imls, out, log);
 
       }
 
@@ -257,15 +267,13 @@ public class DisaggCalc {
     return imtImlMap;
   }
 
-  /*
-   * Compute hazard curves using the supplied model, config, and sites. Method
-   * returns the path to the directory where results were written.
-   */
-  private static Path calcRp(
+  /* Compute hazard curves using the supplied model, config, and sites. */
+  private static void calcRp(
       HazardModel model,
       CalcConfig config,
       List<Site> sites,
       double returnPeriod,
+      Path out,
       Logger log) throws IOException {
 
     ExecutorService exec = null;
@@ -280,8 +288,8 @@ public class DisaggCalc {
 
     log.info(PROGRAM + " (return period): calculating ...");
 
-    HazardExport handler = HazardExport.create(model, config, sites);
-    Path disaggDir = handler.outputDir().resolve("disagg");
+    HazardExport handler = HazardExport.create(model, config, sites, out);
+    Path disaggDir = out.resolve("disagg");
     Files.createDirectory(disaggDir);
 
     Stopwatch stopwatch = Stopwatch.createStarted();
@@ -317,14 +325,10 @@ public class DisaggCalc {
             count, sites.size(), stopwatch));
       }
     }
-    handler.expire();
-
     log.info(String.format(
         PROGRAM + " (return period): %s sites completed in %s",
-        sites.size(), stopwatch.stop()));
-
+        sites.size(), stopwatch));
     exec.shutdown();
-    return handler.outputDir();
   }
 
   /* Hazard curves are already in log-x space. */
@@ -351,11 +355,12 @@ public class DisaggCalc {
    * Compute hazard curves using the supplied model, config, and sites. Method
    * returns the path to the directory where results were written.
    */
-  private static Path calcIml(
+  private static void calcIml(
       HazardModel model,
       CalcConfig config,
       List<Site> sites,
       List<Map<Imt, Double>> imls,
+      Path out,
       Logger log) throws IOException {
 
     ExecutorService exec = null;
@@ -369,8 +374,8 @@ public class DisaggCalc {
     }
 
     log.info(PROGRAM + " (IML): calculating ...");
-    Path outDir = createOutputDir(config.output.directory);
-    Path disaggDir = outDir.resolve("disagg");
+
+    Path disaggDir = out.resolve("disagg");
     Files.createDirectory(disaggDir);
 
     Stopwatch stopwatch = Stopwatch.createStarted();
@@ -404,13 +409,10 @@ public class DisaggCalc {
             count, sites.size(), stopwatch));
       }
     }
-
     log.info(String.format(
         PROGRAM + " (IML): %s sites completed in %s",
-        sites.size(), stopwatch.stop()));
-
+        sites.size(), stopwatch));
     exec.shutdown();
-    return outDir;
   }
 
   private static final class Response {
@@ -509,18 +511,6 @@ public class DisaggCalc {
     }
   }
 
-  // duplicate of that in HazardExport
-  private static Path createOutputDir(Path dir) throws IOException {
-    int i = 1;
-    Path incrementedDir = dir;
-    while (Files.exists(incrementedDir)) {
-      incrementedDir = incrementedDir.resolveSibling(dir.getFileName() + "-" + i);
-      i++;
-    }
-    Files.createDirectories(incrementedDir);
-    return incrementedDir;
-  }
-
   private static String disaggFilename(Site site) {
     return site.name().equals(Site.NO_NAME)
         ? String.format(
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/HazardCalc.java b/src/main/java/gov/usgs/earthquake/nshmp/HazardCalc.java
index 3342bca22cd0ef921c542fb752dd5a1907e1423d..f9fc796fbefee6dd8db5e1158aa7af9a481219e7 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/HazardCalc.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/HazardCalc.java
@@ -11,12 +11,10 @@ import java.nio.file.Paths;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Optional;
-import java.util.concurrent.Callable;
+import java.util.OptionalDouble;
 import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.logging.FileHandler;
 import java.util.logging.Logger;
@@ -110,12 +108,28 @@ public class HazardCalc {
             .build();
       }
       log.info(config.toString());
-
       log.info("");
-      List<Site> sites = readSites(args[1], config, model.siteData(), log);
-      log.info("Sites: " + Sites.toString(sites));
 
-      Path out = calc(model, config, sites, log);
+      Path out = createOutputDir(config.output.directory);
+
+      if (config.hazard.vs30s.isEmpty()) {
+
+        List<Site> sites = readSites(args[1], model.siteData(), OptionalDouble.empty(), log);
+        log.info("Sites: " + Sites.toString(sites));
+        calc(model, config, sites, out, log);
+
+      } else {
+
+        for (double vs30 : config.hazard.vs30s) {
+          log.info("Vs30 batch: " + vs30);
+          List<Site> sites = readSites(args[1], model.siteData(), OptionalDouble.of(vs30), log);
+          log.info("Sites: " + Sites.toString(sites));
+          Path vs30dir = out.resolve("vs30-" + ((int) vs30));
+          Files.createDirectory(vs30dir);
+          calc(model, config, sites, vs30dir, log);
+        }
+
+      }
 
       if (config.output.dataTypes.contains(DataType.MAP)) {
         HazardMaps.createDataSets(out, config.output.returnPeriods, log);
@@ -136,35 +150,33 @@ public class HazardCalc {
   }
 
   static List<Site> readSites(
-      String arg,
-      CalcConfig defaults,
+      String siteFile,
       SiteData siteData,
+      OptionalDouble vs30,
       Logger log) {
 
-    Path path = Paths.get(arg);
+    Path path = Paths.get(siteFile);
     log.info("Sites file: " + path.toAbsolutePath().normalize());
-    String fname = arg.toLowerCase();
+    String fname = siteFile.toLowerCase();
     checkArgument(fname.endsWith(".csv") || fname.endsWith(".geojson"),
-        "Sites file [%s] must be a path to a *.csv or *.geojson file", arg);
+        "Sites file [%s] must be a path to a *.csv or *.geojson file", siteFile);
 
     try {
       return fname.endsWith(".csv")
-          ? Sites.fromCsv(path, defaults, siteData)
-          : Sites.fromGeoJson(path, defaults, siteData);
+          ? Sites.fromCsv(path, siteData, vs30)
+          : Sites.fromGeoJson(path, siteData, vs30);
     } catch (IOException ioe) {
       throw new IllegalArgumentException(
           "Error parsing sites file [%s]; see sites file documentation");
     }
   }
 
-  /*
-   * Compute hazard curves using the supplied model, config, and sites. Method
-   * returns the path to the directory where results were written.
-   */
-  private static Path calc(
+  /* Compute hazard curves using the supplied model, config, and sites. */
+  private static void calc(
       HazardModel model,
       CalcConfig config,
       List<Site> sites,
+      Path out,
       Logger log) throws IOException, InterruptedException, ExecutionException {
 
     int threadCount = config.performance.threadCount.value();
@@ -172,18 +184,14 @@ public class HazardCalc {
     log.info("Threads: " + ((ThreadPoolExecutor) exec).getCorePoolSize());
     log.info(PROGRAM + ": calculating ...");
 
-    HazardExport handler = HazardExport.create(model, config, sites);
-    CalcTask.Builder calcTask = new CalcTask.Builder(model, config, exec);
-    WriteTask.Builder writeTask = new WriteTask.Builder(handler);
-
+    HazardExport handler = HazardExport.create(model, config, sites, out);
     Stopwatch stopwatch = Stopwatch.createStarted();
     int logInterval = sites.size() < 100 ? 1 : sites.size() < 1000 ? 10 : 100;
 
-    Future<Path> out = null;
     for (int i = 0; i < sites.size(); i++) {
       Site site = sites.get(i);
-      Hazard hazard = calcTask.withSite(site).call();
-      out = exec.submit(writeTask.withResult(hazard));
+      Hazard hazard = HazardCalcs.hazard(model, config, site, exec);
+      handler.write(hazard);
       int count = i + 1;
       if (count % logInterval == 0) {
         log.info(String.format(
@@ -191,16 +199,22 @@ public class HazardCalc {
             count, sites.size(), stopwatch));
       }
     }
-    /* Block shutdown until last task is returned. */
-    Path outputDir = out.get();
-
-    handler.expire();
     exec.shutdown();
     log.info(String.format(
         PROGRAM + ": %s sites completed in %s",
-        handler.resultCount(), stopwatch.stop()));
+        sites.size(), stopwatch));
+  }
 
-    return outputDir;
+  /* Avoid clobbering exsting result directories via incrementing. */
+  static Path createOutputDir(Path dir) throws IOException {
+    int i = 1;
+    Path outDir = dir;
+    while (Files.exists(outDir)) {
+      outDir = outDir.resolveSibling(dir.getFileName() + "-" + i);
+      i++;
+    }
+    Files.createDirectories(outDir);
+    return outDir;
   }
 
   private static ExecutorService initExecutor(int threadCount) {
@@ -211,83 +225,6 @@ public class HazardCalc {
     }
   }
 
-  private static final class CalcTask implements
-      Callable<Hazard> {
-
-    final HazardModel model;
-    final CalcConfig config;
-    final Site site;
-    final Executor exec;
-
-    CalcTask(
-        HazardModel model,
-        CalcConfig config,
-        Site site,
-        Executor exec) {
-
-      this.model = model;
-      this.config = config;
-      this.site = site;
-      this.exec = exec;
-    }
-
-    @Override
-    public Hazard call() {
-      return HazardCalcs.hazard(model, config, site, exec);
-    }
-
-    static class Builder {
-
-      final HazardModel model;
-      final CalcConfig config;
-      final Executor exec;
-
-      Builder(HazardModel model, CalcConfig config, Executor exec) {
-        this.model = model;
-        this.config = config;
-        this.exec = exec;
-      }
-
-      /* Builds and returns the task. */
-      CalcTask withSite(Site site) {
-        return new CalcTask(model, config, site, exec);
-      }
-    }
-  }
-
-  private static final class WriteTask implements Callable<Path> {
-
-    final HazardExport handler;
-    final Hazard hazard;
-
-    WriteTask(
-        HazardExport handler,
-        Hazard hazard) {
-      this.handler = handler;
-      this.hazard = hazard;
-    }
-
-    @Override
-    public Path call() throws IOException {
-      handler.write(hazard);
-      return handler.outputDir();
-    }
-
-    static class Builder {
-
-      final HazardExport handler;
-
-      Builder(HazardExport handler) {
-        this.handler = handler;
-      }
-
-      /* Builds and returns the task. */
-      WriteTask withResult(Hazard hazard) {
-        return new WriteTask(handler, hazard);
-      }
-    }
-  }
-
   static final String TMP_LOG = "nshmp-haz-log";
 
   static Path createTempLog() {
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/RateCalc.java b/src/main/java/gov/usgs/earthquake/nshmp/RateCalc.java
index 581b40dca6728473e1965fdd02f1bc06070f5b96..097d10b1bd4c1f8473c51c0da7a014d4fbf2c362 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/RateCalc.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/RateCalc.java
@@ -10,6 +10,7 @@ import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
+import java.util.OptionalDouble;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
@@ -18,6 +19,7 @@ import java.util.logging.FileHandler;
 import java.util.logging.Logger;
 
 import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
@@ -105,12 +107,15 @@ public class RateCalc {
             .build();
       }
       log.info(config.toString());
-
       log.info("");
-      List<Site> sites = HazardCalc.readSites(args[1], config, model.siteData(), log);
+
+      Path out = HazardCalc.createOutputDir(config.output.directory);
+
+      List<Site> sites = HazardCalc.readSites(
+          args[1], model.siteData(), OptionalDouble.empty(), log);
       log.info("Sites: " + Sites.toString(sites));
 
-      Path out = calc(model, config, sites, log);
+      calc(model, config, sites, out, log);
       log.info(PROGRAM + ": finished");
 
       /* Transfer log and write config, windows requires fh.close() */
@@ -134,12 +139,14 @@ public class RateCalc {
    * a single calculation, rate calculations are single threaded. Concurrent
    * calculations for multiple sites are handled below.
    */
-  private static Path calc(
+  private static void calc(
       HazardModel model,
       CalcConfig config,
       List<Site> sites,
+      Path out,
       Logger log) throws IOException, ExecutionException, InterruptedException {
 
+    Stopwatch stopwatch = Stopwatch.createStarted();
     ThreadCount threadCount = config.performance.threadCount;
     EqRateExport export = null;
     if (threadCount != ThreadCount.ONE) {
@@ -147,35 +154,31 @@ public class RateCalc {
       ListeningExecutorService executor = MoreExecutors.listeningDecorator(poolExecutor);
       log.info("Threads: " + ((ThreadPoolExecutor) poolExecutor).getCorePoolSize());
       log.info(PROGRAM + ": calculating ...");
-      export = concurrentCalc(model, config, sites, log, executor);
+      export = concurrentCalc(model, config, sites, out, executor);
       executor.shutdown();
     } else {
       log.info("Threads: Running on calling thread");
       log.info(PROGRAM + ": calculating ...");
-      export = EqRateExport.create(model, config, sites, log);
+      export = EqRateExport.create(model, config, sites, out);
       for (Site site : sites) {
         EqRate rate = EqRate.create(model, config, site);
         export.write(rate);
       }
     }
-    export.expire();
-
     log.info(String.format(
         PROGRAM + ": %s sites completed in %s",
-        export.resultCount(), export.elapsedTime()));
-
-    return export.outputDir();
+        sites.size(), stopwatch));
   }
 
   private static EqRateExport concurrentCalc(
       HazardModel model,
       CalcConfig config,
       List<Site> sites,
-      Logger log,
+      Path out,
       ListeningExecutorService executor)
       throws InterruptedException, ExecutionException, IOException {
 
-    EqRateExport export = EqRateExport.create(model, config, sites, log);
+    EqRateExport export = EqRateExport.create(model, config, sites, out);
 
     int submitted = 0;
     int batchSize = 10;
diff --git a/src/test/java/gov/usgs/earthquake/nshmp/model/peer/PeerTest.java b/src/test/java/gov/usgs/earthquake/nshmp/model/peer/PeerTest.java
index a6de60423ee7ab90bd07b04cb46a9441581f24c5..b18913f725d0367f5b9354842db1b0730cec99b1 100644
--- a/src/test/java/gov/usgs/earthquake/nshmp/model/peer/PeerTest.java
+++ b/src/test/java/gov/usgs/earthquake/nshmp/model/peer/PeerTest.java
@@ -16,6 +16,7 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.OptionalDouble;
 import java.util.concurrent.ExecutorService;
 import java.util.logging.LogManager;
 import java.util.stream.Collectors;
@@ -138,10 +139,10 @@ class PeerTest {
     Map<String, double[]> expectedsMap = loadExpecteds(modelId);
     HazardModel model = HazardModel.load(MODEL_DIR.resolve(modelId));
     CalcConfig config = model.config();
-    Iterable<Site> sites = Sites.fromCsv(MODEL_DIR.resolve(
-        modelId).resolve("sites.csv"),
-        config,
-        model.siteData());
+    Iterable<Site> sites = Sites.fromCsv(
+        MODEL_DIR.resolve(modelId).resolve("sites.csv"),
+        model.siteData(),
+        OptionalDouble.empty());
 
     // ensure that only PGA is being used
     checkState(config.hazard.imts.size() == 1);