From 418c01f73514b6df9f8c5a39a9c16a1d25d9bdb1 Mon Sep 17 00:00:00 2001
From: Peter Powers <pmpowers@usgs.gov>
Date: Mon, 25 Jan 2016 14:40:30 -0700
Subject: [PATCH] updates to Results class and related changes

---
 src/org/opensha2/calc/CalcConfig.java      |  17 +-
 src/org/opensha2/calc/CalcFactory.java     |   3 +-
 src/org/opensha2/calc/HazardCurveSet.java  |  20 +-
 src/org/opensha2/calc/Results.java         | 269 +++++++++++++++++++--
 src/org/opensha2/eq/model/HazardModel.java |   8 +
 src/org/opensha2/programs/HazardCalc.java  |  25 +-
 src/org/opensha2/util/Parsing.java         |   5 +-
 test/etc/SequenceBenchmark.java            |  11 +-
 8 files changed, 302 insertions(+), 56 deletions(-)

diff --git a/src/org/opensha2/calc/CalcConfig.java b/src/org/opensha2/calc/CalcConfig.java
index f686df122..8f070c487 100644
--- a/src/org/opensha2/calc/CalcConfig.java
+++ b/src/org/opensha2/calc/CalcConfig.java
@@ -6,7 +6,6 @@ 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.data.XySequence.*;
 
 import java.io.IOException;
@@ -19,6 +18,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 +47,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 +71,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 +85,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 +100,7 @@ public final class CalcConfig {
 		DEFAULT_IMLS,
 		CUSTOM_IMLS,
 		GMM_UNCERTAINTY,
+		HAZARD_FORMAT,
 		OPTIMIZE_GRIDS,
 		DEAGG,
 		SITES;
@@ -131,6 +136,7 @@ public final class CalcConfig {
 			.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 +190,9 @@ public final class CalcConfig {
 		return gmmUncertainty;
 	}
 
+	public HazardFormat hazardFormat() {
+		return hazardFormat;
+	}
 	/**
 	 * Deaggregation configuration data.
 	 */
@@ -294,6 +303,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 +320,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 +340,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 +360,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 +410,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 +433,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 9e666e3aa..d52808ab9 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 77036e6e7..d3bfbed22 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 cb40d4e4f..703657ddf 100644
--- a/src/org/opensha2/calc/Results.java
+++ b/src/org/opensha2/calc/Results.java
@@ -11,6 +11,7 @@ import java.nio.file.Files;
 import java.nio.file.OpenOption;
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.EnumMap;
 import java.util.List;
 import java.util.Map;
@@ -18,18 +19,26 @@ import java.util.Map.Entry;
 import java.util.Set;
 
 import org.opensha2.data.XySequence;
+import org.opensha2.eq.model.HazardModel;
+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.base.Joiner;
+import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableMap;
 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,9 +47,21 @@ 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";
 
+	/**
+	 * Output style.
+	 */
+	public enum HazardFormat {
+
+		/** Total mean hazard only. */
+		TOTAL,
+
+		/** Additional curves by {@link Gmm} and {@link SourceType}. */
+		DETAILED;
+	}
+
 	/**
 	 * Write a {@code batch} of {@code HazardResult}s to files in the specified
 	 * directory, one for each {@link Imt} in the {@code batch}. See
@@ -57,7 +78,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 +98,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 +113,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 +143,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);
 
-		ImmutableMap.Builder<Imt, Map<SourceType, XySequence>> imtMapBuilder =
-			ImmutableMap.builder();
+		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, XySequence> curves = hazard.curves();
-		Set<Imt> imts = curves.keySet();
+		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);
 
-		for (Imt imt : imts) {
+		/* 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);
 
-			XySequence modelCurve = emptyCopyOf(curves.get(imt));
-			Map<SourceType, XySequence> typeCurves = new EnumMap<>(SourceType.class);
+			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);
 
-			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));
+				Map<Gmm, List<String>> gmmLines = Maps.newEnumMap(Gmm.class);
+				for (Gmm gmm : gmmSet(demo.model)) {
+					gmmLines.put(gmm, Lists.newArrayList(lines));
 				}
-				typeCurves.put(type, immutableCopyOf(typeCurve));
+				gmmLineMap.put(imt, gmmLines);
 			}
-			imtMapBuilder.put(imt, Maps.immutableEnumMap(typeCurves));
 		}
 
-		return imtMapBuilder.build();
+		/* 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(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);
+
+		// 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);
 	}
 
+	/**
+	 * 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) {
+
+		EnumMap<Imt, Map<Gmm, XySequence>> imtMap = Maps.newEnumMap(Imt.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()));
+		}
+
+		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());
+				}
+			}
+		}
+		return Maps.immutableEnumMap(imtMap);
+	}
+
+	/* 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/eq/model/HazardModel.java b/src/org/opensha2/eq/model/HazardModel.java
index 52e6f1342..0a2e3ca59 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 03a792207..223b3824f 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,7 +99,7 @@ public class HazardCalc {
 
 		try {
 
-			log.info("Hazard curve: init...");
+			log.info(PROGRAM + ": init...");
 			Path modelPath = Paths.get(args[0]);
 			HazardModel model = HazardModel.load(modelPath);
 
@@ -132,8 +131,8 @@ public class HazardCalc {
 		} 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)
@@ -157,15 +156,14 @@ public class HazardCalc {
 		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);
+		Path dir = Paths.get(StandardSystemProperty.USER_DIR.value());
 
 		for (Site site : sites) {
 			Hazard result = calc(model, config, site, executor);
@@ -175,7 +173,7 @@ public class HazardCalc {
 				OpenOption[] opts = firstBatch ? WRITE_OPTIONS : APPEND_OPTIONS;
 				firstBatch = false;
 				Results.writeResults(dir, results, opts);
-				log.info("       batch: " + (count + 1) + "  " + batchWatch + "  total: " +
+				log.info("      batch: " + (count + 1) + "  " + batchWatch + "  total: " +
 					totalWatch);
 				results.clear();
 				batchWatch.reset();
@@ -188,7 +186,7 @@ public class HazardCalc {
 			OpenOption[] opts = firstBatch ? WRITE_OPTIONS : APPEND_OPTIONS;
 			Results.writeResults(dir, results, opts);
 		}
-		log.info("Hazard Curve: " + count + " complete " + totalWatch);
+		log.info(PROGRAM + ": " + count + " complete " + totalWatch);
 
 		execSvc.shutdown();
 	}
@@ -229,12 +227,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 01ca380e7..be5df9f26 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/test/etc/SequenceBenchmark.java b/test/etc/SequenceBenchmark.java
index 558fcff63..7254ba081 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);
-- 
GitLab