diff --git a/.gitignore b/.gitignore
index d8a429dbb44ca75d3f872392c8b07ec55a93ed1e..d1038274ce481b044d9904724d0ebbe2b2509e71 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,8 +4,6 @@
 /dist
 /docs
 /tmp
-/results
-/etc/results
+curves/
 Scratch*.java
 /src/META-INF
-/etc/examples/results
diff --git a/README.md b/README.md
index db3033806408b6a47ccdcaef562add15a9aac907..e1d67a222b0ac29299d95e223705aa3d15fe86f8 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 nshmp-haz
 =========
 
-USGS National Seismic Hazard Model Program Codes
+U.S. Geological Survey (USGS) National Seismic Hazard Model Project (NSHMP) codes.
 
-Information relevant to obtaining, building, and running the code may be found in the [wiki](https://github.com/usgs/nshmp-haz/wiki/).
+Information relevant to obtaining, building, and running the code may be found in the [Wiki](https://github.com/usgs/nshmp-haz/wiki/).
diff --git a/etc/README.md b/etc/README.md
index 58641b18aadab1fddefbd696ad5ae5c2a0b2be3d..6f852a99ef0242d4e322e372d80c86b8ff112277 100644
--- a/etc/README.md
+++ b/etc/README.md
@@ -1,8 +1,5 @@
-See the [examples](examples) directory as a starting point for command-line use of nshmp-haz.
-
-[Matlab](matlab) shows how to take advantage of the ground motion model implementations in nshmp-haz.
-
-[Peer](peer) is a collection of simple source models that are also a good starting point.
-
+See the [examples](examples) directory as a starting point for command-line use of `nshmp-haz`.
 
+[Matlab](matlab) shows how to take advantage of the ground motion model implementations in `nshmp-haz`.
 
+[Peer](peer) is a collection of simple source models that are used for testing.
diff --git a/etc/examples/1-hazard-curve/README.md b/etc/examples/1-hazard-curve/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..84b901d3ef430fd0220b75980b0e6a3dba355c8a
--- /dev/null
+++ b/etc/examples/1-hazard-curve/README.md
@@ -0,0 +1,18 @@
+Example 1: A simple hazard calculation
+--------------------------------------
+
+__Working directory:__ `/path/to/nshmp-haz/etc/examples/1-hazard-curve`
+
+On the command line, navigate to this directory and execute the following:
+
+```Shell
+hazard ../../peer/models/Set1-Case1
+```
+
+The PEER models, such as that designated above, consist of simple cases for different source types commonly encountered in a PSHA and are included in the nshmp-haz repository to support testing. See the [PEER directory](../../peer/) for more information.
+
+The result of this calculation should be available as a single comma-delimited file containing several total mean hazard curves for PGA in a newly created 'curves' directory. In this example, the calculation configuration was derived from the model directory. Note that not all calculation [configuration](https://github.com/usgs/nshmp-haz/wiki/Configuration) parameters need be supplied; see the [configuration file](../../peer/models/Set1-Case1/config.json) for this example model.
+
+Also note that when only a model is supplied to `HazardCalc`, all output is written to the current working directory. In the next example, we'll override the model supplied configuration with a custom file.
+
+#### Next: [Example 2 – A custom configuration](../2-custom-config)
diff --git a/etc/examples/2-custom-config/README.md b/etc/examples/2-custom-config/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..e4d3ad7398c828733c4af475223ff24a1dd8a402
--- /dev/null
+++ b/etc/examples/2-custom-config/README.md
@@ -0,0 +1,23 @@
+Example 2: A custom configuration
+-------------------------------
+
+__Working directory:__ `/path/to/nshmp-haz/etc/examples`
+
+Navigate up one level to the `examples/` directory and execute the following:
+
+```Shell
+hazard ../peer/models/Set1-Case1 2-custom-config/config.json
+```
+
+In this example we've overridden the configuration supplied by the model. Specifically:
+
+* The upper end of each hazard curve has been truncated at 3 standard deviations.
+* Hazard curves have been calculated for 3 `imts` ([intensity measure types](http://usgs.github.io/nshmp-haz/javadoc/index.html?org/opensha2/gmm/Imt.html), or spectral periods) and written to the directory containing the config file.
+* The `imls` (intensity measure levels or x-values) of the resultant curves have been explicitely defined for each `imt`.
+* And two different sites have been specified.
+
+See the [configuration specification](https://github.com/usgs/nshmp-haz/wiki/Configuration) for details on default values and supported options and formats.
+
+**A note on output:** Because we supplied a specific configuration file, all program output is written to the directory where the config resides, thus keeping a record of calculation settings along with any results.
+
+#### Next: [Example 3 – Using a custom sites file](../3-sites-file)
diff --git a/etc/examples/2-custom-config/config.json b/etc/examples/2-custom-config/config.json
new file mode 100644
index 0000000000000000000000000000000000000000..a96569992dea45040e636a0e45f971856f192d53
--- /dev/null
+++ b/etc/examples/2-custom-config/config.json
@@ -0,0 +1,24 @@
+{
+  "exceedanceModel": "TRUNCATION_UPPER_ONLY",
+  "truncationLevel": 3.0,
+  "imts": ["PGA", "SA0P2", "SA1P0"],
+  "customImls": {
+    "PGA":   [0.0050, 0.0070, 0.0098, 0.0137, 0.0192, 0.0269, 0.0376, 0.0527, 0.0738, 0.103, 0.145, 0.203, 0.284, 0.397, 0.556, 0.778, 1.09, 1.52, 2.2, 3.3],
+    "SA0P2": [0.0050, 0.0075, 0.0113, 0.0169, 0.0253, 0.0380, 0.0570, 0.0854, 0.128, 0.192, 0.288, 0.432, 0.649, 0.973, 1.46, 2.19, 3.28, 4.92, 7.38],
+    "SA1P0": [0.0025, 0.00375, 0.00563, 0.00844, 0.0127, 0.0190, 0.0285, 0.0427, 0.0641, 0.0961, 0.144, 0.216, 0.324, 0.487, 0.730, 1.09, 1.64, 2.46, 3.69, 5.54]
+  },
+  "sites": [
+    {
+    "name": "San Francisco",
+    "location": [-122.40, 37.75],
+    "vs30": 760,
+    "vsInf": true
+    },
+    {
+    "name": "Oakland",
+    "location": [-122.25, 37.80],
+    "vs30": 760,
+    "vsInf": true
+    }
+  ]
+}
\ No newline at end of file
diff --git a/etc/examples/3-sites-file/README.md b/etc/examples/3-sites-file/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..8e0f05af7ffd81956d058a9a3c3a290c964b3ff4
--- /dev/null
+++ b/etc/examples/3-sites-file/README.md
@@ -0,0 +1,14 @@
+Example 3: Using a custom sites file
+------------------------------------
+
+__Working directory:__ `/path/to/nshmp-haz/etc/examples`
+
+One may also supply a comma-delimited site data file, which may be easier to work with in some applications.
+
+```Shell
+hazard ../peer/models/Set1-Case1 3-sites-file/config.json 3-sites-file/sites.csv
+```
+
+See the [site file](sites.csv) itself for details on the expected file structure. Under all use cases, if the name of a site is supplied, it will be included in the first column of any curve files.
+
+#### Next: [Example 4 – A simple hazard map](../4-hazard-map)
diff --git a/etc/examples/3-sites-file/config.json b/etc/examples/3-sites-file/config.json
new file mode 100644
index 0000000000000000000000000000000000000000..a96569992dea45040e636a0e45f971856f192d53
--- /dev/null
+++ b/etc/examples/3-sites-file/config.json
@@ -0,0 +1,24 @@
+{
+  "exceedanceModel": "TRUNCATION_UPPER_ONLY",
+  "truncationLevel": 3.0,
+  "imts": ["PGA", "SA0P2", "SA1P0"],
+  "customImls": {
+    "PGA":   [0.0050, 0.0070, 0.0098, 0.0137, 0.0192, 0.0269, 0.0376, 0.0527, 0.0738, 0.103, 0.145, 0.203, 0.284, 0.397, 0.556, 0.778, 1.09, 1.52, 2.2, 3.3],
+    "SA0P2": [0.0050, 0.0075, 0.0113, 0.0169, 0.0253, 0.0380, 0.0570, 0.0854, 0.128, 0.192, 0.288, 0.432, 0.649, 0.973, 1.46, 2.19, 3.28, 4.92, 7.38],
+    "SA1P0": [0.0025, 0.00375, 0.00563, 0.00844, 0.0127, 0.0190, 0.0285, 0.0427, 0.0641, 0.0961, 0.144, 0.216, 0.324, 0.487, 0.730, 1.09, 1.64, 2.46, 3.69, 5.54]
+  },
+  "sites": [
+    {
+    "name": "San Francisco",
+    "location": [-122.40, 37.75],
+    "vs30": 760,
+    "vsInf": true
+    },
+    {
+    "name": "Oakland",
+    "location": [-122.25, 37.80],
+    "vs30": 760,
+    "vsInf": true
+    }
+  ]
+}
\ No newline at end of file
diff --git a/etc/examples/3-sites-file/sites.csv b/etc/examples/3-sites-file/sites.csv
new file mode 100644
index 0000000000000000000000000000000000000000..f631722875f138305dd5af0fb148d326dffba039
--- /dev/null
+++ b/etc/examples/3-sites-file/sites.csv
@@ -0,0 +1,24 @@
+#
+# Comma-delimited site file example.
+#
+# Header row must identify columns. At a minimum, lon and lat
+# must be defined. Columns can be in any order and any missing
+# fields will be populated with default values. 
+#
+# [name,] lon, lat [, vs30] [, vsInf] [, z1p0] [, z2p5]
+#
+# Defaults:
+#     name: Unnamed Site
+#     vs30: 760.0
+#    vsInf: true
+#     z1p0: NaN (GMM will use default basin depth model)
+#     z2p5: NaN (GMM will use default basin depth model)
+#
+name, lon, lat
+Concord,       -122.00, 37.95
+Oakland,       -122.25, 37.80
+San Francisco, -122.40, 37.75
+San Jose,      -121.90, 37.35
+San Mateo,     -122.30, 37.55
+Santa Rosa,    -122.70, 38.45
+Vallejo,       -122.25, 38.10
diff --git a/etc/examples/4-hazard-map/README.md b/etc/examples/4-hazard-map/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..81278f0f835aa665a7b1622f375136771505c63d
--- /dev/null
+++ b/etc/examples/4-hazard-map/README.md
@@ -0,0 +1,12 @@
+Example 4: A simple hazard map
+------------------------------
+
+__Working directory:__ `/path/to/nshmp-haz/etc/examples`
+
+A hazard map is just a lot of hazard curves. To compute curves at reqularly spaced intervals in latitude and longitude, the [sites configuration](https://github.com/usgs/nshmp-haz/wiki/Configuration#four-sites) can instead be specified as a polygon.
+
+```Shell
+hazard ../peer/models/Set1-Case1 4-hazard-map/config.json
+```
+
+#### Next: [Example 5 – A more complex model](../5-complex-model)
diff --git a/etc/examples/4-hazard-map/config.json b/etc/examples/4-hazard-map/config.json
new file mode 100644
index 0000000000000000000000000000000000000000..c81727a865cc5177c99feea88e091b009ebfc31d
--- /dev/null
+++ b/etc/examples/4-hazard-map/config.json
@@ -0,0 +1,17 @@
+{
+  "exceedanceModel": "TRUNCATION_UPPER_ONLY",
+  "truncationLevel": 3.0,
+  "imts": ["PGA", "SA0P2", "SA1P0"],
+  "sites": {  
+    "region": {
+      "name": "Bay Area",
+      "spacing": 0.1,
+      "border": [  
+        [-123.0, 37.0],
+        [-121.0, 39.0]
+      ],
+      "vs30": 760,
+      "vsInf": true
+    }
+  }
+}
\ No newline at end of file
diff --git a/etc/examples/5-complex-model/README.md b/etc/examples/5-complex-model/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..3f8e321e4dbafb66524fea79b39367b54d684183
--- /dev/null
+++ b/etc/examples/5-complex-model/README.md
@@ -0,0 +1,28 @@
+Example 5: A more complex model
+-------------------------------
+
+__Working directory:__ `/path/to/nshmp-haz/etc/examples`
+
+Most PSHAs involve the use of more complex source models, the components of which might use different ground motion models. For this and ensuing examples, we'll use the 2008 USGS National Seismic Hazard Model (NSHM) for the western U.S.
+
+First, clone the 2008 USGS NSHM. Assuming `examples/` is the current working directory, the following will create a copy of the model adjacent to nshmp-haz:
+
+```Shell
+git clone https://github.com/usgs/nshmp-model-cous-2008.git ../../../nshmp-model-cous-2008
+```
+
+The 2008 NSHM repository contains two source models: one for the western U.S. and a one for the central and eastern U.S. To compute hazard for a few sites in the Western U.S., execute:
+
+```Shell
+hazard ../../../nshmp-model-cous-2008/Western\ US 5-complex-model/config-sites.json
+```
+
+Note that more complex models take longer to initialize, although this only occurs once per calculation, and make for longer, per-site calculations. However, `HazardCalc` will automatically use all cores available and therefore performs better on multi-core systems. To compute a small, low-resolution map for the central San Francisco bay area, execute:
+
+```Shell
+hazard ../../../nshmp-model-cous-2008/Western\ US 5-complex-model/config-map.json
+```
+
+This computes 121 curves over a 2° by 2° area and will give you a sense of how long a larger map might take. These results overwrite those of the prior site-specific calculation.
+
+#### Next: [Example 6 – Enhanced output](../6-enhanced-output)
diff --git a/etc/examples/5-complex-model/config-map.json b/etc/examples/5-complex-model/config-map.json
new file mode 100644
index 0000000000000000000000000000000000000000..178352754f358289b35ed905c01eba4d01f77221
--- /dev/null
+++ b/etc/examples/5-complex-model/config-map.json
@@ -0,0 +1,12 @@
+{
+  "sites": {  
+    "region": {
+      "name": "Bay Area",
+      "spacing": 0.2,
+      "border": [  
+        [-123.0, 37.0],
+        [-121.0, 39.0]
+      ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/etc/examples/5-complex-model/config-sites.json b/etc/examples/5-complex-model/config-sites.json
new file mode 100644
index 0000000000000000000000000000000000000000..c913fccbb793e64a45a55ed3fd70dd57864f196c
--- /dev/null
+++ b/etc/examples/5-complex-model/config-sites.json
@@ -0,0 +1,20 @@
+{
+  "sites": [
+    {
+    "name": "Los Angeles",
+    "location": [-118.25, 34.05]
+    },
+    {
+    "name": "San Francisco",
+    "location": [-122.40, 37.75]
+    },
+    {
+    "name": "Seattle",
+    "location": [-122.30, 47.60]
+    },
+    {
+    "name": "Salt Lake City",
+    "location": [-111.90, 40.75]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/etc/examples/6-enhanced-output/README.md b/etc/examples/6-enhanced-output/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..eafa33ccfe715f1af87e3d0d6d4f9dda5dd91d88
--- /dev/null
+++ b/etc/examples/6-enhanced-output/README.md
@@ -0,0 +1,14 @@
+Example 6: Enhanced output
+--------------------------
+
+__Working directory:__ `/path/to/nshmp-haz/etc/examples`
+
+While mean hazard is of broad interest, it can be useful to preserve individual components of a total curve, particularly with more complex models. Execute the following to write curves for each source type and ground motion model (GMM) used in the 2008 NSHM:
+
+```Shell
+hazard ../../../nshmp-model-cous-2008/Western\ US 6-enhanced-output/config.json
+```
+
+Note that the output curves directory now contains additional derectories of curves by source type and GMM.
+
+See the `nshmp-haz` wiki and JavDocs for more information on source types ([Wiki](https://github.com/usgs/nshmp-haz/wiki/Source-Types), [JavaDoc](http://usgs.github.io/nshmp-haz/javadoc/index.html?org/opensha2/eq/model/SourceType.html)) and GMMs ([Wiki](https://github.com/usgs/nshmp-haz/wiki/Ground-Motion-Models), [JavaDoc](http://usgs.github.io/nshmp-haz/javadoc/index.html?org/opensha2/gmm/Gmm.html)).
diff --git a/etc/examples/6-enhanced-output/config.json b/etc/examples/6-enhanced-output/config.json
new file mode 100644
index 0000000000000000000000000000000000000000..c1fc16cab5599723bc74817a1e9507f182642031
--- /dev/null
+++ b/etc/examples/6-enhanced-output/config.json
@@ -0,0 +1,21 @@
+{
+  "hazardFormat": "DETAILED",
+  "sites": [
+    {
+    "name": "Los Angeles",
+    "location": [-118.25, 34.05]
+    },
+    {
+    "name": "San Francisco",
+    "location": [-122.40, 37.75]
+    },
+    {
+    "name": "Seattle",
+    "location": [-122.30, 47.60]
+    },
+    {
+    "name": "Salt Lake City",
+    "location": [-111.90, 40.75]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/etc/examples/README.md b/etc/examples/README.md
index 1d77ec7ba0ecb9bbc9cb9162166b50dc1e5ae921..5f0873f322eb1776ed57cc00ffb8c2e68f482a8c 100644
--- a/etc/examples/README.md
+++ b/etc/examples/README.md
@@ -1,29 +1,13 @@
-##### Hazard Curves
-The simplest way to run the HazardCalc program via the command-line is to supply it with a source model; all model initialization and calculation configuration data will be read from the model itself. Navigate to the `/etc` in the repository and run:
-```
-java -cp ../dist/nshmp-haz.jar org.opensha2.programs.HazardCalc peer/models/Set1-Case1
-```
-The result of this calculation should be available as a single file containing several hazard curves for PGA in a newly created 'results' directory. Note that not all calculation [configuration](https://github.com/usgs/nshmp-haz/wiki/Configuration) parameters need be supplied; see the [configuration file](../peer/models/Set1-Case1/config.json) for this example model.  
-One can override calculation configuration parameters by supplying an alternate configuration file. For example:
-```
-java -cp ../dist/nshmp-haz.jar org.opensha2.programs.HazardCalc peer/models/Set1-Case1 examples/config-sites.json
-```
-In this case:
-* a truncation will be applied at 3 standard deviations.
-* the list of `imts` (intensity measure types, or periods) for which curves will be calculated has been expanded to 3.
-* the `imls` (the intensity measure levels or x-values) of the resultant curves, have been explicitely defined for each `imt`.
-* two sites have been specified
-The 'results' directory should now include 3 files, one for each `imt`.
+Examples
+--------
 
-One can also supply a comma-delimited site data file, which may be easier to work with in some applications.
-```
-java -cp ../dist/nshmp-haz.jar org.opensha2.programs.HazardCalc peer/models/Set1-Case1 examples/config-sites.json examples/sites-sf.csv
-```
-See the site file itself for details on the expected file structure. Under all use cases, if the name of a site is supplied, it will be included in the first column of any result files.
+These examples are designed to be executed locally while following the READMEs on GitHub. All examples avoid a lengthy call to Java and the `HazardCalc` program by using the following system alias:
 
-##### Hazard Maps
-Hazard maps are generated from numerous uniformely spaced hazard curves. To compute such a curve set, the same program is used, but sites are instead specified as a region.
-```
-java -cp ../dist/nshmp-haz.jar org.opensha2.programs.HazardCalc peer/models/Set1-Case1 examples/config-region-sf.json
+```Shell
+alias hazard='java -cp /path/to/nshmp-haz/dist/nshmp-haz.jar org.opensha2.programs.HazardCalc'
 ```
 
+Because each example builds on prior concepts, it is best step through all the examples, however quickly.
+
+#### Start: [Example 1 – A simple hazard calculation](1-hazard-curve)
+
diff --git a/etc/peer/README.md b/etc/peer/README.md
index 5f359e6887c542713f1c4edb1b5b947fe00a0795..ec9359cf42b7fbed0659f0c4c8a34c7c7d875274 100644
--- a/etc/peer/README.md
+++ b/etc/peer/README.md
@@ -1,5 +1,5 @@
 #### PEER Test Cases
 
-The PEER PSHA validation project developed a number of source models for the purpose of examining the sensitivity of seismic hazard to different PSHA implementations (codes). These test cases are used as end-to-end unit tests in nshmp-haz. 
+The PEER PSHA validation project developed a number of source models for the purpose of examining the sensitivity of seismic hazard to different PSHA implementations (codes). These test cases are used as end-to-end unit tests in `nshmp-haz`. 
 
 For more information, including source model specifications, result tables, and summary reports, please see the [PEER](http://peer.berkeley.edu) PSHA validation project (TODO: link; no site as yet).
diff --git a/src/org/opensha2/calc/CalcConfig.java b/src/org/opensha2/calc/CalcConfig.java
index f686df1225d7a48002c7f1522111fc57d6683d2a..972c1289af3a29a009702bf2d2c5949cda86e5de 100644
--- a/src/org/opensha2/calc/CalcConfig.java
+++ b/src/org/opensha2/calc/CalcConfig.java
@@ -6,7 +6,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.opensha2.util.TextUtils.format;
-
+import static org.opensha2.util.TextUtils.wrap;
 import static org.opensha2.data.XySequence.*;
 
 import java.io.IOException;
@@ -19,6 +19,7 @@ import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
 
+import org.opensha2.calc.Results.HazardFormat;
 import org.opensha2.data.Data;
 import org.opensha2.data.XySequence;
 import org.opensha2.gmm.Imt;
@@ -47,6 +48,8 @@ public final class CalcConfig {
 	private final Map<Imt, double[]> customImls;
 	private final boolean optimizeGrids;
 	private final boolean gmmUncertainty;
+	
+	private final HazardFormat hazardFormat;
 
 	private final DeaggData deagg;
 
@@ -69,6 +72,7 @@ public final class CalcConfig {
 			Map<Imt, double[]> customImls,
 			boolean optimizeGrids,
 			boolean gmmUncertainty,
+			HazardFormat hazardFormat,
 			DeaggData deagg,
 			SiteSet sites,
 			Map<Imt, XySequence> modelCurves,
@@ -82,6 +86,7 @@ public final class CalcConfig {
 		this.customImls = customImls;
 		this.optimizeGrids = optimizeGrids;
 		this.gmmUncertainty = gmmUncertainty;
+		this.hazardFormat = hazardFormat;
 		this.deagg = deagg;
 		this.sites = sites;
 		this.modelCurves = modelCurves;
@@ -96,6 +101,7 @@ public final class CalcConfig {
 		DEFAULT_IMLS,
 		CUSTOM_IMLS,
 		GMM_UNCERTAINTY,
+		HAZARD_FORMAT,
 		OPTIMIZE_GRIDS,
 		DEAGG,
 		SITES;
@@ -117,7 +123,7 @@ public final class CalcConfig {
 			StringBuilder sb = new StringBuilder();
 			for (Entry<Imt, double[]> entry : customImls.entrySet()) {
 				String imtStr = "(override) " + entry.getKey().name();
-				sb.append(format(imtStr)).append(Arrays.toString(entry.getValue()));
+				sb.append(format(imtStr)).append(wrap(Arrays.toString(entry.getValue())));
 			}
 			customImlStr = sb.toString();
 		}
@@ -127,10 +133,11 @@ public final class CalcConfig {
 			.append(format(Key.EXCEEDANCE_MODEL)).append(exceedanceModel)
 			.append(format(Key.TRUNCATION_LEVEL)).append(truncationLevel)
 			.append(format(Key.IMTS)).append(Parsing.enumsToString(imts, Imt.class))
-			.append(format(Key.DEFAULT_IMLS)).append(Arrays.toString(defaultImls))
+			.append(format(Key.DEFAULT_IMLS)).append(wrap(Arrays.toString(defaultImls)))
 			.append(customImlStr)
 			.append(format(Key.OPTIMIZE_GRIDS)).append(optimizeGrids)
 			.append(format(Key.GMM_UNCERTAINTY)).append(gmmUncertainty)
+			.append(format(Key.HAZARD_FORMAT)).append(hazardFormat)
 			.append(format("Deaggregation R"))
 			.append("min=").append(deagg.rMin).append(", ")
 			.append("max=").append(deagg.rMax).append(", ")
@@ -184,6 +191,9 @@ public final class CalcConfig {
 		return gmmUncertainty;
 	}
 
+	public HazardFormat hazardFormat() {
+		return hazardFormat;
+	}
 	/**
 	 * Deaggregation configuration data.
 	 */
@@ -294,6 +304,7 @@ public final class CalcConfig {
 		private Map<Imt, double[]> customImls;
 		private Boolean optimizeGrids;
 		private Boolean gmmUncertainty;
+		private HazardFormat hazardFormat;
 		private DeaggData deagg;
 		private SiteSet sites;
 
@@ -310,6 +321,7 @@ public final class CalcConfig {
 			this.customImls = config.customImls;
 			this.optimizeGrids = config.optimizeGrids;
 			this.gmmUncertainty = config.gmmUncertainty;
+			this.hazardFormat = config.hazardFormat;
 			this.deagg = config.deagg;
 			this.sites = config.sites;
 			return this;
@@ -329,6 +341,7 @@ public final class CalcConfig {
 			this.customImls = Maps.newHashMap();
 			this.optimizeGrids = true;
 			this.gmmUncertainty = false;
+			this.hazardFormat = HazardFormat.TOTAL;
 			this.deagg = new DeaggData();
 			this.sites = new SiteSet(Lists.newArrayList(Site.builder().build()));
 			return this;
@@ -348,6 +361,7 @@ public final class CalcConfig {
 			if (that.customImls != null) this.customImls = that.customImls;
 			if (that.optimizeGrids != null) this.optimizeGrids = that.optimizeGrids;
 			if (that.gmmUncertainty != null) this.gmmUncertainty = that.gmmUncertainty;
+			if (that.hazardFormat != null) this.hazardFormat = that.hazardFormat;
 			if (that.deagg != null) this.deagg = that.deagg;
 			if (that.sites != null) this.sites = that.sites;
 			return this;
@@ -397,6 +411,7 @@ public final class CalcConfig {
 			checkNotNull(customImls, MSSG, buildId, Key.CUSTOM_IMLS);
 			checkNotNull(optimizeGrids, MSSG, buildId, Key.OPTIMIZE_GRIDS);
 			checkNotNull(gmmUncertainty, MSSG, buildId, Key.GMM_UNCERTAINTY);
+			checkNotNull(hazardFormat, MSSG, buildId, Key.HAZARD_FORMAT);
 			checkNotNull(deagg, MSSG, buildId, Key.DEAGG);
 			checkNotNull(sites, MSSG, buildId, Key.SITES);
 			built = true;
@@ -419,6 +434,7 @@ public final class CalcConfig {
 				customImls,
 				optimizeGrids,
 				gmmUncertainty,
+				hazardFormat,
 				deagg,
 				sites,
 				curves, logCurves);
diff --git a/src/org/opensha2/calc/CalcFactory.java b/src/org/opensha2/calc/CalcFactory.java
index 9e666e3aa6b912abf21a8f64cb382edbc31ea753..d52808ab94809bedb16b8806db030ff4d47c99d2 100644
--- a/src/org/opensha2/calc/CalcFactory.java
+++ b/src/org/opensha2/calc/CalcFactory.java
@@ -162,8 +162,7 @@ final class CalcFactory {
 
 		return transform(
 			allAsList(curveSets),
-			new CurveSetConsolidator(model, config, site),
-			ex).get();
+			new CurveSetConsolidator(model, config, site), ex).get();
 	}
 
 }
diff --git a/src/org/opensha2/calc/HazardCurveSet.java b/src/org/opensha2/calc/HazardCurveSet.java
index 77036e6e7d0c6d4e0a9c9e2b497976029bd38e36..d3bfbed22a03c6f7eaaf7698e83c9a7a1a51d2db 100644
--- a/src/org/opensha2/calc/HazardCurveSet.java
+++ b/src/org/opensha2/calc/HazardCurveSet.java
@@ -22,8 +22,8 @@ import org.opensha2.gmm.Imt;
 
 /**
  * Container class for hazard curves derived from a {@code SourceSet}. Class
- * stores the {@code HazardGroundMotions}s associated with each {@code Source}
- * used in a hazard calculation and the combined curves for each
+ * stores the {@code GroundMotions}s associated with each {@code Source} used in
+ * a hazard calculation and the individual curves for each
  * {@code GroundMotionModel} used.
  * 
  * <p>The {@code Builder} for this class is used to aggregate the HazardCurves
@@ -37,8 +37,8 @@ import org.opensha2.gmm.Imt;
  * including {@code ClusterSourceSet}s, which are handled differently in hazard
  * calculations. This container marks a point in the calculation pipeline where
  * results from cluster and other sources may be recombined into a single
- * {@code HazardResult}, regardless of {@code SourceSet.type()} for all relevant
- * {@code SourceSet}s.</p>
+ * {@code Hazard} result, regardless of {@code SourceSet.type()} for all
+ * relevant {@code SourceSet}s.</p>
  * 
  * @author Peter Powers
  */
@@ -134,8 +134,10 @@ final class HazardCurveSet {
 				Map<Gmm, XySequence> curveMapBuild = curveMap.get(imt);
 				// loop Gmms based on what's supported at this distance
 				for (Gmm gmm : gmmWeightMap.keySet()) {
-					double weight = gmmWeightMap.get(gmm);
-					curveMapBuild.get(gmm).add(copyOf(curveMapIn.get(gmm)).multiply(weight));
+					double gmmWeight = gmmWeightMap.get(gmm);
+					curveMapBuild.get(gmm).add(copyOf(curveMapIn.get(gmm))
+						.multiply(gmmWeight)
+						.multiply(sourceSet.weight()));
 				}
 			}
 			return this;
@@ -154,7 +156,9 @@ final class HazardCurveSet {
 				// loop Gmms based on what's supported at this distance
 				for (Gmm gmm : gmmWeightMap.keySet()) {
 					double weight = gmmWeightMap.get(gmm) * clusterWeight;
-					curveMapBuild.get(gmm).add(copyOf(curveMapIn.get(gmm)).multiply(weight));
+					curveMapBuild.get(gmm).add(copyOf(curveMapIn.get(gmm))
+						.multiply(weight)
+						.multiply(sourceSet.weight()));
 				}
 			}
 			return this;
@@ -177,14 +181,12 @@ final class HazardCurveSet {
 		 * scaled by their weights while building (above).
 		 */
 		private void computeFinal() {
-			double sourceSetWeight = sourceSet.weight();
 			for (Entry<Imt, Map<Gmm, XySequence>> entry : curveMap.entrySet()) {
 				Imt imt = entry.getKey();
 				XySequence totalCurve = emptyCopyOf(modelCurves.get(imt));
 				for (XySequence curve : entry.getValue().values()) {
 					totalCurve.add(curve);
 				}
-				totalCurve.multiply(sourceSetWeight);
 				totalCurves.put(imt, immutableCopyOf(totalCurve));
 			}
 		}
diff --git a/src/org/opensha2/calc/Results.java b/src/org/opensha2/calc/Results.java
index cb40d4e4f0384cb67085139bf69a36c1da29832f..58dddd3e9ceedd86434f3438f521033cf7437dce 100644
--- a/src/org/opensha2/calc/Results.java
+++ b/src/org/opensha2/calc/Results.java
@@ -1,9 +1,7 @@
 package org.opensha2.calc;
 
 import static java.nio.charset.StandardCharsets.US_ASCII;
-import static org.opensha2.data.XySequence.copyOf;
 import static org.opensha2.data.XySequence.emptyCopyOf;
-import static org.opensha2.data.XySequence.immutableCopyOf;
 
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
@@ -18,18 +16,22 @@ import java.util.Map.Entry;
 import java.util.Set;
 
 import org.opensha2.data.XySequence;
+import org.opensha2.eq.model.Source;
+import org.opensha2.eq.model.SourceSet;
 import org.opensha2.eq.model.SourceType;
 import org.opensha2.geo.Location;
+import org.opensha2.gmm.Gmm;
 import org.opensha2.gmm.Imt;
 import org.opensha2.util.Parsing;
 import org.opensha2.util.Parsing.Delimiter;
 
 import com.google.common.base.Function;
-import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.FluentIterable;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
-import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.common.primitives.Doubles;
 
 /**
  * Factory class for reducing and exporting various result types.
@@ -38,8 +40,33 @@ import com.google.common.collect.Multimap;
  */
 public class Results {
 
-	private static final String CURVE_FILE_SUFFIX = "-curves.csv";
+	private static final String CURVE_FILE_SUFFIX = ".csv";
 	private static final String RATE_FMT = "%.8e";
+	private static final String CURVE_DIR = "curves";
+
+	/**
+	 * Hazard output format.
+	 */
+	public enum HazardFormat {
+
+		/** Total mean hazard only. */
+		TOTAL,
+
+		/** Additional curves by {@link Gmm} and {@link SourceType}. */
+		DETAILED;
+	}
+	
+	/**
+	 * Curve format.
+	 */
+	public enum CurveFormat {
+		
+		/** Write curves as annual-rate. */
+		ANNUAL_RATE,
+		
+		/** Write curves as Poisson probabilities. */
+		POISSON;
+	}
 
 	/**
 	 * Write a {@code batch} of {@code HazardResult}s to files in the specified
@@ -57,7 +84,8 @@ public class Results {
 	 * @throws IOException if a problem is encountered
 	 * @see Files#write(Path, Iterable, java.nio.charset.Charset, OpenOption...)
 	 */
-	public static void writeResults(Path dir, List<Hazard> batch, OpenOption... options)
+	@Deprecated
+	public static void writeResultsOLD(Path dir, List<Hazard> batch, OpenOption... options)
 			throws IOException {
 
 		Function<Double, String> locFmtFunc = Parsing.formatDoubleFunction(Location.FORMAT);
@@ -76,7 +104,7 @@ public class Results {
 				if (namedSites) headings.add("name");
 				headings.add("lon");
 				headings.add("lat");
-				Iterable<? extends Object> header = Iterables.concat(
+				Iterable<?> header = Iterables.concat(
 					headings,
 					demo.config.modelCurves().get(imt).xValues());
 				lineList.add(Parsing.join(header, Delimiter.COMMA));
@@ -91,13 +119,13 @@ public class Results {
 					result.site.location.lat()),
 				locFmtFunc);
 			String name = result.site.name;
-			for (Entry<Imt, ? extends XySequence> entry : result.totalCurves.entrySet()) {
+			for (Entry<Imt, XySequence> entry : result.totalCurves.entrySet()) {
 
 				// enable to output poisson probability - used when running
 				// PEER test cases - TODO should be configurable
-//				Function<Double, String> valueFunction = Functions.compose(
-//					rateFmtFunc,
-//					Mfds.rateToProbConverter());
+				// Function<Double, String> valueFunction = Functions.compose(
+				// rateFmtFunc,
+				// Mfds.rateToProbConverter());
 
 				// enable to output annual rate
 				Function<Double, String> valueFunction = rateFmtFunc;
@@ -121,31 +149,232 @@ public class Results {
 		}
 	}
 
-	public static Map<Imt, Map<SourceType, XySequence>> totalsByType(Hazard hazard) {
+	/*
+	 * Individual Hazard results only contain data relevant to the site of
+	 * interest (e.g. for the NSHM WUS models, hazard in San Fancisco is
+	 * influenced by slab sources whereas hazard in Los Angeles is not because
+	 * it is too far away). For consistency when outputting batches of results,
+	 * files are written for all source types and ground motion models supported
+	 * by the HazardModel being used. This yields curve sets that are consistent
+	 * across all locations in a batch, however, some of the curves may be
+	 * empty. Depending on the extents of a map or list of sites, some curve
+	 * sets may consist exclusively of zero-valued curves.
+	 */
+
+	public static void writeResults(
+			Path dir,
+			List<Hazard> batch,
+			OpenOption... options) throws IOException {
+
+		Function<Double, String> formatter = Parsing.formatDoubleFunction(RATE_FMT);
+
+		Hazard demo = batch.get(0);
+		boolean newFile = options.length == 0;
+		boolean namedSites = demo.site.name != Site.NO_NAME;
+		boolean detailed = demo.config.hazardFormat().equals(HazardFormat.DETAILED);
+
+		Map<Imt, List<String>> totalLineMap = Maps.newEnumMap(Imt.class);
+		Map<Imt, Map<SourceType, List<String>>> typeLineMap = Maps.newEnumMap(Imt.class);
+		Map<Imt, Map<Gmm, List<String>>> gmmLineMap = Maps.newEnumMap(Imt.class);
+
+		/* Initialize line maps for all types and gmms referenced by a model */
+		for (Imt imt : demo.totalCurves.keySet()) {
+			List<String> lines = new ArrayList<>();
+			if (newFile) {
+				Iterable<?> header = Iterables.concat(
+					Lists.newArrayList(namedSites ? "name" : null, "lon", "lat"),
+					demo.config.modelCurves().get(imt).xValues());
+				lines.add(Parsing.join(header, Delimiter.COMMA));
+			}
+			totalLineMap.put(imt, lines);
+
+			if (detailed) {
+
+				Map<SourceType, List<String>> typeLines = Maps.newEnumMap(SourceType.class);
+				for (SourceType type : demo.model.types()) {
+					typeLines.put(type, Lists.newArrayList(lines));
+				}
+				typeLineMap.put(imt, typeLines);
+
+				Map<Gmm, List<String>> gmmLines = Maps.newEnumMap(Gmm.class);
+				for (Gmm gmm : gmmSet(demo.model)) {
+					gmmLines.put(gmm, Lists.newArrayList(lines));
+				}
+				gmmLineMap.put(imt, gmmLines);
+			}
+		}
+
+		/* Process batch */
+		for (Hazard hazard : batch) {
+
+			String name = namedSites ? hazard.site.name : null;
+			List<String> locData = Lists.newArrayList(
+				name,
+				String.format(Location.FORMAT, hazard.site.location.lon()),
+				String.format(Location.FORMAT, hazard.site.location.lat()));
+
+			Map<Imt, Map<SourceType, XySequence>> curvesByType = detailed ?
+				curvesByType(hazard) : null;
+			Map<Imt, Map<Gmm, XySequence>> curvesByGmm = detailed ?
+				curvesByGmm(hazard) : null;
+
+			for (Entry<Imt, XySequence> imtEntry : hazard.totalCurves.entrySet()) {
+				Imt imt = imtEntry.getKey();
+
+				XySequence totalCurve = imtEntry.getValue();
+				Iterable<Double> emptyValues = Doubles.asList(new double[totalCurve.size()]);
+				String emptyLine = toLine(locData, emptyValues, formatter);
+
+				totalLineMap.get(imt).add(toLine(
+					locData,
+					imtEntry.getValue().yValues(),
+					formatter));
+
+				if (detailed) {
+
+					Map<SourceType, XySequence> typeCurves = curvesByType.get(imt);
+					for (Entry<SourceType, List<String>> typeEntry : typeLineMap.get(imt)
+						.entrySet()) {
+						SourceType type = typeEntry.getKey();
+						String typeLine = typeCurves.containsKey(type) ?
+							toLine(locData, typeCurves.get(type).yValues(), formatter) :
+							emptyLine;
+						typeEntry.getValue().add(typeLine);
+					}
+
+					Map<Gmm, XySequence> gmmCurves = curvesByGmm.get(imt);
+					for (Entry<Gmm, List<String>> gmmEntry : gmmLineMap.get(imt).entrySet()) {
+						Gmm gmm = gmmEntry.getKey();
+						String gmmLine = gmmCurves.containsKey(gmm) ?
+							toLine(locData, gmmCurves.get(gmm).yValues(), formatter) :
+							emptyLine;
+						gmmEntry.getValue().add(gmmLine);
+					}
+				}
+			}
+		}
+
+		/* write/append */
+		for (Entry<Imt, List<String>> totalEntry : totalLineMap.entrySet()) {
+			Imt imt = totalEntry.getKey();
+
+			Path imtDir = dir.resolve(CURVE_DIR).resolve(imt.name());
+			Files.createDirectories(imtDir);
+			Path totalFile = imtDir.resolve("total" + CURVE_FILE_SUFFIX);
+			Files.write(totalFile, totalEntry.getValue(), US_ASCII, options);
+
+			if (detailed) {
+
+				Path typeDir = imtDir.resolve("type");
+				Files.createDirectories(typeDir);
+				for (Entry<SourceType, List<String>> typeEntry : typeLineMap.get(imt).entrySet()) {
+					Path typeFile = typeDir.resolve(
+						typeEntry.getKey().toString() + CURVE_FILE_SUFFIX);
+					Files.write(typeFile, typeEntry.getValue(), US_ASCII, options);
+				}
+
+				Path gmmDir = imtDir.resolve("gmm");
+				Files.createDirectories(gmmDir);
+				for (Entry<Gmm, List<String>> gmmEntry : gmmLineMap.get(imt).entrySet()) {
+					Path gmmFile = gmmDir.resolve(gmmEntry.getKey().name() + CURVE_FILE_SUFFIX);
+					Files.write(gmmFile, gmmEntry.getValue(), US_ASCII, options);
+				}
+			}
+		}
+	}
+
+	private static String toLine(
+			Iterable<String> location,
+			Iterable<Double> values,
+			Function<Double, String> formatter) {
+
+		return Parsing.join(
+			FluentIterable.from(location).append(Iterables.transform(values, formatter)),
+			Delimiter.COMMA);
+	}
+
+	/**
+	 * Derive maps of curves by source type for each Imt in a {@code Hazard}
+	 * result.
+	 */
+	public static Map<Imt, Map<SourceType, XySequence>> curvesByType(Hazard hazard) {
+
+		EnumMap<Imt, Map<SourceType, XySequence>> imtMap = Maps.newEnumMap(Imt.class);
 
-		ImmutableMap.Builder<Imt, Map<SourceType, XySequence>> imtMapBuilder =
-			ImmutableMap.builder();
+		// initialize receiver
+		Set<SourceType> types = hazard.sourceSetCurves.keySet();
+		for (Entry<Imt, XySequence> entry : hazard.curves().entrySet()) {
+			imtMap.put(entry.getKey(), initCurves(types, entry.getValue()));
+		}
+
+		for (Entry<SourceType, HazardCurveSet> curveSet : hazard.sourceSetCurves.entries()) {
+			for (Entry<Imt, XySequence> typeTotals : curveSet.getValue().totalCurves.entrySet()) {
+				imtMap.get(typeTotals.getKey())
+					.get(curveSet.getKey())
+					.add(typeTotals.getValue());
+			}
+		}
+		return Maps.immutableEnumMap(imtMap);
+	}
 
-		Map<Imt, XySequence> curves = hazard.curves();
-		Set<Imt> imts = curves.keySet();
+	/**
+	 * Derive maps of curves by groudn motion model for each Imt in a
+	 * {@code Hazard} result.
+	 */
+	public static Map<Imt, Map<Gmm, XySequence>> curvesByGmm(Hazard hazard) {
 
-		for (Imt imt : imts) {
+		EnumMap<Imt, Map<Gmm, XySequence>> imtMap = Maps.newEnumMap(Imt.class);
 
-			XySequence modelCurve = emptyCopyOf(curves.get(imt));
-			Map<SourceType, XySequence> typeCurves = new EnumMap<>(SourceType.class);
+		// initialize receiver
+		Iterable<SourceSet<? extends Source>> sources = Iterables.transform(
+			hazard.sourceSetCurves.values(),
+			CURVE_SET_TO_SOURCE_SET);
+		Set<Gmm> gmms = gmmSet(sources);
+		for (Entry<Imt, XySequence> entry : hazard.curves().entrySet()) {
+			imtMap.put(entry.getKey(), initCurves(gmms, entry.getValue()));
+		}
 
-			Multimap<SourceType, HazardCurveSet> curveSets = hazard.sourceSetCurves;
-			for (SourceType type : curveSets.keySet()) {
-				XySequence typeCurve = copyOf(modelCurve);
-				for (HazardCurveSet curveSet : curveSets.get(type)) {
-					typeCurve.add(curveSet.totalCurves.get(imt));
+		for (HazardCurveSet curveSet : hazard.sourceSetCurves.values()) {
+			for (Entry<Imt, Map<Gmm, XySequence>> imtEntry : curveSet.curveMap.entrySet()) {
+				for (Entry<Gmm, XySequence> gmmEntry : imtEntry.getValue().entrySet()) {
+					imtMap.get(imtEntry.getKey()).get(gmmEntry.getKey()).add(gmmEntry.getValue());
 				}
-				typeCurves.put(type, immutableCopyOf(typeCurve));
 			}
-			imtMapBuilder.put(imt, Maps.immutableEnumMap(typeCurves));
 		}
+		return Maps.immutableEnumMap(imtMap);
+	}
 
-		return imtMapBuilder.build();
+	/* Scan the supplied source sets for the set of all GMMs used. */
+	private static Set<Gmm> gmmSet(final Iterable<SourceSet<? extends Source>> sourceSets) {
+		return Sets.immutableEnumSet(
+			FluentIterable.from(sourceSets).transformAndConcat(
+				new Function<SourceSet<? extends Source>, Set<Gmm>>() {
+					@Override public Set<Gmm> apply(SourceSet<? extends Source> sourceSet) {
+						return sourceSet.groundMotionModels().gmms();
+					}
+				})
+			);
 	}
 
+	/* Initalize a map of curves, one entry for each of the supplied enum keys. */
+	private static <K extends Enum<K>> Map<K, XySequence> initCurves(
+			final Set<K> keys,
+			final XySequence model) {
+		return Maps.immutableEnumMap(
+			FluentIterable.from(keys).toMap(
+				new Function<K, XySequence>() {
+					@Override public XySequence apply(K key) {
+						return emptyCopyOf(model);
+					}
+				})
+			);
+	}
+
+	private static final Function<HazardCurveSet, SourceSet<? extends Source>> CURVE_SET_TO_SOURCE_SET =
+		new Function<HazardCurveSet, SourceSet<? extends Source>>() {
+			@Override public SourceSet<? extends Source> apply(HazardCurveSet curves) {
+				return curves.sourceSet;
+			}
+		};
+
 }
diff --git a/src/org/opensha2/calc/Site.java b/src/org/opensha2/calc/Site.java
index 06c0b679f462d5547e4ceee84c9f17414d09a57c..76ae1ba99e01c54c185e635a519ce3c210c8a4b9 100644
--- a/src/org/opensha2/calc/Site.java
+++ b/src/org/opensha2/calc/Site.java
@@ -14,6 +14,7 @@ import org.opensha2.geo.Location;
 import org.opensha2.gmm.GroundMotionModel;
 import org.opensha2.util.Named;
 
+import com.google.common.base.Strings;
 import com.google.common.collect.Range;
 import com.google.gson.JsonArray;
 import com.google.gson.JsonDeserializationContext;
@@ -51,14 +52,14 @@ public class Site implements Named {
 	public static final double MAX_Z1P0 = 2.0;
 
 	/** The name used for {@code Site}s with no supplied name. */
-	public static final String NO_NAME = "Unnamed Site";
+	public static final String NO_NAME = "Unnamed";
 
 	/** {@link #MIN_VS_30} and {@link #MAX_VS_30} as a closed {@code Range}. */
 	public static final Range<Double> VS30_RANGE = Range.closed(MIN_VS_30, MAX_VS_30);
-	
+
 	/** {@link #MIN_Z2P5} and {@link #MAX_Z2P5} as a closed {@code Range}. */
 	public static final Range<Double> Z2P5_RANGE = Range.closed(MIN_Z2P5, MAX_Z2P5);
-	
+
 	/** {@link #MIN_Z1P0} and {@link #MAX_Z1P0} as a closed {@code Range}. */
 	public static final Range<Double> Z1P0_RANGE = Range.closed(MIN_Z1P0, MAX_Z1P0);
 
@@ -133,8 +134,11 @@ public class Site implements Named {
 	}
 
 	@Override public String toString() {
-		return String.format("Site: {name=%s, loc=%s, vs30=%s, vsInf=%s, z1p0=%s, z2p5=%s}", name,
-			location, vs30, vsInferred, z1p0, z2p5);
+		return new StringBuilder(Strings.padEnd(name, 16, ' '))
+			.append(String.format("%.3f %.3f Vs30=%s ", location.lon(), location.lat(), vs30))
+			.append(vsInferred ? "inferred " : "measured ")
+			.append(String.format("Z1.0=%s Z2.5=%s", z1p0, z2p5))
+			.toString();
 	}
 
 	@Override public String name() {
diff --git a/src/org/opensha2/calc/SiteSet.java b/src/org/opensha2/calc/SiteSet.java
index f697395e90c92eafe63a4df2ac1d4153a3d66924..490fe074fb127e436b175e8aff8ba137e7154262 100644
--- a/src/org/opensha2/calc/SiteSet.java
+++ b/src/org/opensha2/calc/SiteSet.java
@@ -12,6 +12,7 @@ import static org.opensha2.calc.Site.Key.Z1P0;
 import static org.opensha2.calc.Site.Key.Z2P5;
 import static org.opensha2.geo.BorderType.MERCATOR_LINEAR;
 import static org.opensha2.util.TextUtils.ALIGN_COL;
+import static org.opensha2.util.TextUtils.format;
 
 import java.io.IOException;
 import java.lang.reflect.Type;
@@ -117,9 +118,10 @@ final class SiteSet implements Iterable<Site> {
 			.append(" [size=").append(size()).append("]");
 		if (region == null) {
 			for (Site site : Iterables.limit(sites, TO_STRING_LIMIT)) {
-				sb.append(TextUtils.NEWLINE)
-					.append(repeat(" ", ALIGN_COL - 4))
-					.append(site);
+				sb.append(format("Site")).append(site);
+//				sb.append(TextUtils.NEWLINE)
+//					.append(repeat(" ", ALIGN_COL - 4))
+//					.append(site);
 			}
 			if (sites.size() > TO_STRING_LIMIT) {
 				int delta = sites.size() - TO_STRING_LIMIT;
diff --git a/src/org/opensha2/eq/model/HazardModel.java b/src/org/opensha2/eq/model/HazardModel.java
index 52e6f13426f2e603176092934eb43b2d0d887edb..0a2e3ca59bd51ffbec859a48a294beb1c738624a 100644
--- a/src/org/opensha2/eq/model/HazardModel.java
+++ b/src/org/opensha2/eq/model/HazardModel.java
@@ -7,6 +7,7 @@ import static org.opensha2.util.TextUtils.validateName;
 
 import java.nio.file.Path;
 import java.util.Iterator;
+import java.util.Set;
 
 import org.opensha2.calc.CalcConfig;
 import org.opensha2.gmm.GroundMotionModel;
@@ -99,6 +100,13 @@ public final class HazardModel implements Iterable<SourceSet<? extends Source>>,
 	@Override public String name() {
 		return name;
 	}
+	
+	/**
+	 * The set of {@code SourceType}s included in this model.
+	 */
+	public Set<SourceType> types() {
+		return sourceSetMap.keySet();
+	}
 
 	/**
 	 * Return the default calculation configuration. This may be overridden.
diff --git a/src/org/opensha2/programs/HazardCalc.java b/src/org/opensha2/programs/HazardCalc.java
index 03a792207a0dc9254aad4fa784aab71e95bd7131..b59781e02f00a8ce4c2a3b4134544406afab547f 100644
--- a/src/org/opensha2/programs/HazardCalc.java
+++ b/src/org/opensha2/programs/HazardCalc.java
@@ -7,7 +7,6 @@ import static org.opensha2.util.TextUtils.NEWLINE;
 import static org.opensha2.util.TextUtils.format;
 
 import java.io.IOException;
-import java.nio.file.Files;
 import java.nio.file.OpenOption;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -100,16 +99,19 @@ public class HazardCalc {
 
 		try {
 
-			log.info("Hazard curve: init...");
+			log.info(PROGRAM + ": init...");
 			Path modelPath = Paths.get(args[0]);
 			HazardModel model = HazardModel.load(modelPath);
 
 			CalcConfig config = model.config();
+			Path out = Paths.get(StandardSystemProperty.USER_DIR.value());
 			if (argCount > 1) {
+				Path userConfigPath = Paths.get(args[1]);
 				config = CalcConfig.builder()
 					.copy(model.config())
-					.extend(CalcConfig.builder(Paths.get(args[1])))
+					.extend(CalcConfig.builder(userConfigPath))
 					.build();
+				out = userConfigPath.getParent();
 			}
 			log.info(config.toString());
 
@@ -126,14 +128,14 @@ public class HazardCalc {
 				log.info(sb.toString());
 			}
 
-			calc(model, config, sites, log);
+			calc(model, config, sites, out, log);
 			return null;
 
 		} catch (Exception e) {
 			return new StringBuilder()
 				.append(NEWLINE)
-				.append("Hazard Curve: error").append(NEWLINE)
-				.append("   Arguments: ").append(Arrays.toString(args)).append(NEWLINE)
+				.append(PROGRAM + ": error").append(NEWLINE)
+				.append(" Arguments: ").append(Arrays.toString(args)).append(NEWLINE)
 				.append(NEWLINE)
 				.append(Throwables.getStackTraceAsString(e)).append(NEWLINE)
 				.append(NEWLINE)
@@ -152,20 +154,19 @@ public class HazardCalc {
 			HazardModel model,
 			CalcConfig config,
 			Iterable<Site> sites,
+			Path out,
 			Logger log) throws IOException {
 
 		ExecutorService execSvc = createExecutor();
 		Optional<Executor> executor = Optional.<Executor> of(execSvc);
 
-		log.info("Hazard Curve: calculating ...");
+		log.info(PROGRAM + ": calculating ...");
 		Stopwatch batchWatch = Stopwatch.createStarted();
 		Stopwatch totalWatch = Stopwatch.createStarted();
 		int count = 0;
 
 		List<Hazard> results = new ArrayList<>();
 		boolean firstBatch = true;
-		Path dir = Paths.get(StandardSystemProperty.USER_DIR.value(), "results");
-		Files.createDirectories(dir);
 
 		for (Site site : sites) {
 			Hazard result = calc(model, config, site, executor);
@@ -174,11 +175,12 @@ public class HazardCalc {
 			if (results.size() == FLUSH_LIMIT) {
 				OpenOption[] opts = firstBatch ? WRITE_OPTIONS : APPEND_OPTIONS;
 				firstBatch = false;
-				Results.writeResults(dir, results, opts);
-				log.info("       batch: " + (count + 1) + "  " + batchWatch + "  total: " +
-					totalWatch);
+				Results.writeResults(out, results, opts);
+				log.info(
+					"     batch: " + (count + 1) + "  " + batchWatch +
+					"  total: " + totalWatch);
 				results.clear();
-				batchWatch.reset();
+				batchWatch.reset().start();
 			}
 
 			count++;
@@ -186,9 +188,9 @@ public class HazardCalc {
 		// write final batch
 		if (!results.isEmpty()) {
 			OpenOption[] opts = firstBatch ? WRITE_OPTIONS : APPEND_OPTIONS;
-			Results.writeResults(dir, results, opts);
+			Results.writeResults(out, results, opts);
 		}
-		log.info("Hazard Curve: " + count + " complete " + totalWatch);
+		log.info(PROGRAM + ": " + count + " complete " + totalWatch);
 
 		execSvc.shutdown();
 	}
@@ -229,12 +231,13 @@ public class HazardCalc {
 		return newFixedThreadPool(getRuntime().availableProcessors());
 	}
 
-	private static final String USAGE_COMMAND = "java -cp nshmp-haz.jar org.opensha.programs.HazardCurve model [config [sites]]";
-	private static final String USAGE_URL1 = "https://github.com/usgs/nshmp-haz/wiki/Earthquake-Source-Models";
-	private static final String USAGE_URL2 = "https://github.com/usgs/nshmp-haz/wiki/Hazard-Calculations";
+	private static final String PROGRAM = HazardCalc.class.getSimpleName();
+	private static final String USAGE_COMMAND = "java -cp nshmp-haz.jar org.opensha.programs.HazardCalc model [config [sites]]";
+	private static final String USAGE_URL1 = "https://github.com/usgs/nshmp-haz/wiki";
+	private static final String USAGE_URL2 = "https://github.com/usgs/nshmp-haz/tree/master/etc";
 
 	static final String USAGE = new StringBuilder()
-		.append("HazardCurve usage:").append(NEWLINE)
+		.append(PROGRAM).append(" usage:").append(NEWLINE)
 		.append("  ").append(USAGE_COMMAND).append(NEWLINE)
 		.append(NEWLINE)
 		.append("Where:").append(NEWLINE)
diff --git a/src/org/opensha2/util/Parsing.java b/src/org/opensha2/util/Parsing.java
index 01ca380e725f90da4fc7ceef002e41b1a2c77842..be5df9f2618c84608c73ad8e5d5c565fa0d64637 100644
--- a/src/org/opensha2/util/Parsing.java
+++ b/src/org/opensha2/util/Parsing.java
@@ -829,7 +829,8 @@ public final class Parsing {
 
 	/**
 	 * Returns a {@link Function} for converting {@code double}s to formatted
-	 * strings.
+	 * strings. If a value to format is 0.0, the format string is ignored in 
+	 * favor of always printing the often more compact string: "0.0".
 	 * 
 	 * @param format a format string
 	 * @see String#format(String, Object...)
@@ -846,7 +847,7 @@ public final class Parsing {
 		}
 
 		@Override public String apply(Double value) {
-			return String.format(format, value);
+			return (value == 0.0) ? "0.0" : String.format(format, value);
 		}
 	}
 
diff --git a/src/org/opensha2/util/TextUtils.java b/src/org/opensha2/util/TextUtils.java
index 9dfad5696b97daa5a61a352fac46e8c5bc117d0d..a7cc5f146c5c3dcabef2faa8e8540fe3e5949506 100644
--- a/src/org/opensha2/util/TextUtils.java
+++ b/src/org/opensha2/util/TextUtils.java
@@ -2,8 +2,7 @@ package org.opensha2.util;
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Strings.padStart;
-import static org.opensha2.util.TextUtils.ALIGN_COL;
-import static org.opensha2.util.TextUtils.NEWLINE;
+import static com.google.common.base.Strings.repeat;
 
 import java.util.Map;
 
@@ -20,6 +19,9 @@ public class TextUtils {
 
 	public static final String NEWLINE = StandardSystemProperty.LINE_SEPARATOR.value();
 	public static final int ALIGN_COL = 24;
+	private static final int MAX_COL = 100;
+	private static final int DELTA_COL = MAX_COL - ALIGN_COL - 2;
+	private static final String INDENT = NEWLINE + repeat(" ", ALIGN_COL + 2);
 	
 	
 	private static final Joiner.MapJoiner MAP_JOIN = Joiner.on(", ").withKeyValueSeparator(": ");
@@ -35,8 +37,23 @@ public class TextUtils {
 	public static String format(String s) {
 		return NEWLINE + padStart(s, ALIGN_COL, ' ') + ": ";
 	}
-
-
+	
+	public static String wrap(String s) {
+		return wrap(s, false);
+	}
+	
+	/*
+	 * Used internally; pad flag indents lines consistent with format(s)
+	 */
+	private static String wrap(String s, boolean pad) {
+		if (s.length() <= DELTA_COL) return pad ? INDENT + s : s;
+		StringBuilder sb = new StringBuilder();
+		int lastCommaIndex = s.substring(0, DELTA_COL).lastIndexOf(',') + 1;
+		if (pad) sb.append(INDENT);
+		sb.append(s.substring(0, lastCommaIndex));
+		sb.append(wrap(s.substring(lastCommaIndex).trim(), true));
+		return sb.toString();
+	}
 
 	/**
 	 * Verifies that the supplied {@code String} is neither {@code null} or
diff --git a/test/etc/SequenceBenchmark.java b/test/etc/SequenceBenchmark.java
index 558fcff6310701f029c9a8e8614ce981a0af6343..7254ba0817bdbb4cbc3ef94237b61008e5c32f9d 100644
--- a/test/etc/SequenceBenchmark.java
+++ b/test/etc/SequenceBenchmark.java
@@ -46,8 +46,8 @@ class SequenceBenchmark {
 			}
 
 			for (int k = 0; k < numPoints; k++) {
-				double y = adfReceiver.get(k).getY();
-				adfReceiver.set(k, copy.get(k).getY() + y);
+				double y = adfReceiver.getY(k);
+				adfReceiver.set(k, copy.getY(k) + y);
 			}
 		}
 		System.out.println("Time: " + sw.stop());
@@ -76,8 +76,8 @@ class SequenceBenchmark {
 			}
 
 			for (int k = 0; k < numPoints; k++) {
-				double y = edfReceiver.get(k).getY();
-				edfReceiver.set(k, copy.get(k).getY() + y);
+				double y = edfReceiver.getY(k);
+				edfReceiver.set(k, copy.getY(k) + y);
 			}
 		}
 		System.out.println("Time: " + sw.stop());
@@ -91,8 +91,7 @@ class SequenceBenchmark {
 		sw.reset().start();
 		for (int i = 0; i < its; i++) {
 			XySequence copy = XySequence.copyOf(xy);
-			copy.multiply(xy);
-			xyReceiver.add(copy);
+			xyReceiver.add(copy.multiply(xy));
 		}
 		System.out.println("Time: " + sw.stop());
 		System.out.println(xyReceiver);