diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/HazVersion.java b/src/main/java/gov/usgs/earthquake/nshmp/www/HazVersion.java index 538964aa5be6630bb36ebd924d8f5930ba95b470..b3c072cb07703efaeff8e8dfb17a9c8ace4d6ddd 100644 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/HazVersion.java +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/HazVersion.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import org.eclipse.jgit.api.Git; +import com.google.common.collect.Lists; import com.google.common.io.Resources; import gov.usgs.earthquake.nshmp.internal.AppVersion; @@ -15,11 +16,7 @@ public class HazVersion implements AppVersion { private static final String MODEL_FILE = "model-version.json"; public static VersionInfo[] appVersions(Path modelPath) { - var versions = new ArrayList<VersionInfo>(); - versions.add(new HazVersion().getVersionInfo()); - versions.add(new LibVersion().getVersionInfo()); - versions.add(new WsUtilsVersion().getVersionInfo()); - + var versions = Lists.newArrayList(appVersions()); var nshmVersion = getNshmVersion(modelPath); if (nshmVersion != null) { @@ -29,6 +26,15 @@ public class HazVersion implements AppVersion { return versions.toArray(new VersionInfo[0]); } + public static VersionInfo[] appVersions() { + var versions = new ArrayList<VersionInfo>(); + versions.add(new HazVersion().getVersionInfo()); + versions.add(new LibVersion().getVersionInfo()); + versions.add(new WsUtilsVersion().getVersionInfo()); + + return versions.toArray(new VersionInfo[0]); + } + /** * Returns the version info from resources file. */ diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/PrimingResource.java b/src/main/java/gov/usgs/earthquake/nshmp/www/PrimingResource.java index 6e1d36196a24f395757ead0f029e74b546771a1c..5a5c3bbd8c4573ad6d93453b96198e1d9994615f 100644 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/PrimingResource.java +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/PrimingResource.java @@ -57,36 +57,40 @@ public class PrimingResource implements OrderedResource { @Value("${nshmp-haz.model-path}") private Path modelPath; + @Value("${nshmp-haz.gmm}") + private boolean gmmDeploy; + @Override public void beforeCheckpoint(Context<? extends Resource> context) throws Exception { var model = ServletUtil.loadModel(modelPath); ServletUtil.model(model); updateParameter(); + if (!gmmDeploy) { + var region = Regions.createRectangular("Bounds", model.bounds().min, model.bounds().max); - var region = Regions.createRectangular("Bounds", model.bounds().min, model.bounds().max); - - try (MicronautLambdaHandler handler = new MicronautLambdaHandler()) { - var paths = List.of("/hazard", "/disagg"); + try (MicronautLambdaHandler handler = new MicronautLambdaHandler()) { + var paths = List.of("/hazard", "/disagg"); - paths.forEach(path -> { - handler.handleRequest( - getAwsProxyRequest(path, Optional.empty()), - new MockLambdaContext()); - }); - - paths.forEach(path -> { - LOCATIONS.forEach(namedLocation -> { - var location = namedLocation.location(); + paths.forEach(path -> { + handler.handleRequest( + getAwsProxyRequest(path, Optional.empty()), + new MockLambdaContext()); + }); - if (region.contains(location)) { - handler.handleRequest( - getAwsProxyRequest( - String.format("%s/%f/%f/760", path, location.longitude, location.latitude), - Optional.of(Imt.PGA)), - new MockLambdaContext()); - } + paths.forEach(path -> { + LOCATIONS.forEach(namedLocation -> { + var location = namedLocation.location(); + + if (region.contains(location)) { + handler.handleRequest( + getAwsProxyRequest( + String.format("%s/%f/%f/760", path, location.longitude, location.latitude), + Optional.of(Imt.PGA)), + new MockLambdaContext()); + } + }); }); - }); + } } } diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/ServletUtil.java b/src/main/java/gov/usgs/earthquake/nshmp/www/ServletUtil.java index b44aaaa6feef89af9af27fb38c2bb75854d1c6cb..448a30b0584101b944b09e33898506c1da5092bf 100644 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/ServletUtil.java +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/ServletUtil.java @@ -62,7 +62,7 @@ public class ServletUtil { private static HazardModel HAZARD_MODEL; - private static Optional<String> awsRuntime = + static Optional<String> awsRuntime = Optional.ofNullable(System.getenv("AWS_LAMBDA_RUNTIME_API")); static { diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java b/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java index 2068cc2e0521d02c2443870daf33a2e4f16f52ea..ee6affd7fd63c5bb18f80e8809222059603dd644 100644 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java @@ -2,12 +2,14 @@ package gov.usgs.earthquake.nshmp.www; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; import org.slf4j.LoggerFactory; import gov.usgs.earthquake.nshmp.model.HazardModel; import gov.usgs.earthquake.nshmp.www.source.FeaturesService; +import io.micronaut.context.annotation.Value; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpResponse; import io.micronaut.http.MediaType; @@ -17,6 +19,7 @@ import io.swagger.v3.core.util.Yaml; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Paths; import io.swagger.v3.parser.OpenAPIV3Parser; import jakarta.inject.Inject; @@ -35,6 +38,9 @@ public class SwaggerController { @Inject private NshmpMicronautServlet servlet; + @Value("${nshmp-haz.gmm}") + private boolean gmmDeploy; + @Get(produces = MediaType.TEXT_EVENT_STREAM) public HttpResponse<String> doGet(HttpRequest<?> request) { try { @@ -52,6 +58,75 @@ public class SwaggerController { HttpRequest<?> request, HazardModel model) { var openApi = new OpenAPIV3Parser().read("META-INF/swagger/nshmp-haz.yml"); + + if (ServletUtil.awsRuntime.isPresent()) { + if (gmmDeploy) { + openApi = gmmSwagger(openApi); + } else { + openApi = hazardSwagger(filterGmms(openApi), model); + } + } else { + openApi = hazardSwagger(openApi, model); + } + + openApi.servers(null); + + return openApi; + } + + /** + * Filter GMM services. + * + * @param openApi OpenAPI docs. + */ + private OpenAPI filterGmms(OpenAPI openApi) { + Paths paths = new Paths(); + openApi.getPaths().forEach((path, pathItem) -> { + if (!path.contains("/gmm")) { + paths.put(path, pathItem); + } + }); + openApi.paths(paths); + + openApi.setTags(openApi.getTags().stream() + .filter(tag -> !tag.getName().contains("Ground Motion Models")) + .collect(Collectors.toList())); + + return openApi; + } + + /** + * Filter all services except GMM. + * + * @param openApi The OpenAPI docs. + */ + private OpenAPI gmmSwagger(OpenAPI openApi) { + Paths paths = new Paths(); + openApi.getPaths().forEach((path, pathItem) -> { + if (path.contains("/gmm")) { + // Remove "/gmm" for AWS deployments + paths.put(path.substring(4), pathItem); + } + }); + openApi.paths(paths); + + openApi.setTags(openApi.getTags().stream() + .filter(tag -> tag.getName().contains("Ground Motion Models")) + .collect(Collectors.toList())); + + openApi.getInfo().setTitle("NSHMP Ground Motion Models"); + openApi.getInfo().setDescription("Ground motion model web services."); + + return openApi; + } + + /** + * Fillter GMM services. + * + * @param openApi OpenAPI docs. + * @param model The current model. + */ + private OpenAPI hazardSwagger(OpenAPI openApi, HazardModel model) { var bounds = model.bounds(); var components = openApi.getComponents(); var schemas = components.getSchemas(); @@ -59,7 +134,6 @@ public class SwaggerController { SwaggerUtils.imtSchema(schemas, List.copyOf(model.config().hazard.imts)); var boundsInfo = SwaggerUtils.locationBoundsInfo(bounds.min, bounds.max, Optional.empty()); FeaturesService.featureTypeSchema(schemas); - openApi.servers(null); openApi.getInfo().setTitle(model.name() + " Web Services"); openApi.getInfo().setDescription( diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmCalc.java b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmCalc.java new file mode 100644 index 0000000000000000000000000000000000000000..31c51dbef9c5c2c6ff666055be865bc89ba6c113 --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmCalc.java @@ -0,0 +1,384 @@ +package gov.usgs.earthquake.nshmp.www.gmm; + +import static java.lang.Math.cos; +import static java.lang.Math.hypot; +import static java.lang.Math.sin; +import static java.lang.Math.tan; +import static java.lang.Math.toRadians; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import com.google.common.primitives.Doubles; + +import gov.usgs.earthquake.nshmp.Maths; +import gov.usgs.earthquake.nshmp.gmm.Gmm; +import gov.usgs.earthquake.nshmp.gmm.GmmInput; +import gov.usgs.earthquake.nshmp.gmm.GroundMotion; +import gov.usgs.earthquake.nshmp.gmm.GroundMotionModel; +import gov.usgs.earthquake.nshmp.gmm.GroundMotions; +import gov.usgs.earthquake.nshmp.gmm.Imt; +import gov.usgs.earthquake.nshmp.tree.Branch; +import gov.usgs.earthquake.nshmp.tree.LogicTree; +import gov.usgs.earthquake.nshmp.www.gmm.GmmService.Distance; +import gov.usgs.earthquake.nshmp.www.gmm.GmmService.Magnitude; +import gov.usgs.earthquake.nshmp.www.gmm.GmmService.Request; +import gov.usgs.earthquake.nshmp.www.gmm.XyDataGroup.EpiSeries; + +import jakarta.inject.Singleton; + +/* + * GMM service calculators. + * + * @author U.S. Geological Survey + */ +@Singleton +class GmmCalc { + + /* Compute ground motion response spectra. */ + static Map<Gmm, GmmSpectraData> spectra(Request request) { + + Map<Gmm, GmmSpectraData> gmmSpectra = new EnumMap<>(Gmm.class); + + for (Gmm gmm : request.gmms) { + Set<Imt> saImts = gmm.responseSpectrumImts(); + + List<LogicTree<GroundMotion>> saImtTrees = saImts.stream() + .map(imt -> gmm.instance(imt).calc(request.input)) + .collect(Collectors.toList()); + + List<Double> saPeriods = saImts.stream() + .map(imt -> imt.period()) + .collect(Collectors.toList()); + + gmmSpectra.put( + gmm, + new GmmSpectraData( + treeToDataGroup(gmm, Imt.PGA, request.input), + treeToDataGroup(gmm, Imt.PGV, request.input), + treesToDataGroup(saPeriods, saImtTrees))); + } + + return gmmSpectra; + } + + private static Optional<GmmData<Double>> treeToDataGroup(Gmm gmm, Imt imt, GmmInput input) { + Optional<GmmData<Double>> dataGroup = Optional.empty(); + + if (gmm.supportedImts().contains(imt)) { + LogicTree<GroundMotion> imtTrees = gmm.instance(imt).calc(input); + dataGroup = Optional.of(treeToDataGroup(imtTrees)); + } + + return dataGroup; + } + + private static GmmData<Double> treeToDataGroup(LogicTree<GroundMotion> tree) { + List<String> branchIds = tree.stream() + .map(Branch::id) + .collect(Collectors.toList()); + + GroundMotion combined = GroundMotions.combine(tree); + double μs = combined.mean(); + double σs = combined.sigma(); + + List<EpiBranch<Double>> epiBranches = new ArrayList<>(); + + // short circuit if tree is single branch + if (tree.size() > 1) { + // branch index, imt index + double[] μBranches = tree.stream().mapToDouble(branch -> branch.value().mean()).toArray(); + double[] σBranches = tree.stream().mapToDouble(branch -> branch.value().sigma()).toArray(); + double[] weights = tree.stream().mapToDouble(branch -> branch.weight()).toArray(); + + for (int i = 0; i < tree.size(); i++) { + EpiBranch<Double> epiBranch = new EpiBranch<>( + branchIds.get(i), + μBranches[i], + σBranches[i], + weights[i]); + + epiBranches.add(epiBranch); + } + } + + return new GmmData<>(μs, σs, epiBranches); + } + + private static GmmDataXs<double[]> treesToDataGroup( + List<Double> xValues, + List<LogicTree<GroundMotion>> trees) { + + // Can't use Trees.transpose() because some + // GMMs have period dependent weights + + List<GmmData<Double>> imtData = trees.stream() + .map(tree -> treeToDataGroup(tree)) + .collect(Collectors.toList()); + + LogicTree<GroundMotion> modelTree = trees.get(0); + List<EpiBranch<double[]>> epiBranches = new ArrayList<>(); + + List<String> branchIds = modelTree.stream() + .map(Branch::id) + .collect(Collectors.toList()); + + if (modelTree.size() > 1) { + List<double[]> μBranches = new ArrayList<>(); + List<double[]> σBranches = new ArrayList<>(); + List<double[]> weights = new ArrayList<>(); + for (int i = 0; i < modelTree.size(); i++) { + μBranches.add(new double[xValues.size()]); + σBranches.add(new double[xValues.size()]); + weights.add(new double[xValues.size()]); + } + + // imt indexing + for (int i = 0; i < imtData.size(); i++) { + GmmData<Double> data = imtData.get(i); + + // branch indexing + for (int j = 0; j < data.tree.size(); j++) { + EpiBranch<Double> branch = data.tree.get(j); + μBranches.get(j)[i] = branch.μs; + σBranches.get(j)[i] = branch.σs; + weights.get(j)[i] = branch.weights; + } + } + + for (int i = 0; i < modelTree.size(); i++) { + EpiBranch<double[]> epiBranch = new EpiBranch<>( + branchIds.get(i), + μBranches.get(i), + σBranches.get(i), + weights.get(i)); + epiBranches.add(epiBranch); + } + } + + double[] xs = xValues.stream().mapToDouble(Double::doubleValue).toArray(); + double[] μs = imtData.stream().mapToDouble(data -> data.μs).toArray(); + double[] σs = imtData.stream().mapToDouble(data -> data.σs).toArray(); + + return new GmmDataXs<>(xs, μs, σs, epiBranches); + } + + /* Compute ground motions over a range of distances. */ + static Map<Gmm, GmmDataXs<double[]>> distance(Distance.Request request, double[] distances) { + var inputList = hangingWallDistances(request.input, distances); + return calculateGroundMotions(request.gmms, inputList, request.imt, distances); + } + + /* Compute ground motions over a range of magnitudes. */ + static Map<Gmm, GmmDataXs<double[]>> magnitude( + Magnitude.Request request, + double[] magnitudes, + double distance) { + + var gmmInputs = Arrays.stream(magnitudes) + .mapToObj(Mw -> GmmInput.builder().fromCopy(request.input).mag(Mw).build()) + .map(gmmInput -> hangingWallDistance(gmmInput, distance)) + .collect(Collectors.toList()); + return calculateGroundMotions(request.gmms, gmmInputs, request.imt, magnitudes); + } + + private static List<GmmInput> hangingWallDistances( + GmmInput inputModel, + double[] rValues) { + + return Arrays.stream(rValues) + .mapToObj(r -> hangingWallDistance(inputModel, r)) + .collect(Collectors.toList()); + } + + /* Compute distance metrics for a fault. */ + private static GmmInput hangingWallDistance(GmmInput in, double r) { + /* Dip in radians */ + double δ = toRadians(in.dip); + + /* Horizontal and vertical widths of fault */ + double h = cos(δ) * in.width; + double v = sin(δ) * in.width; + + /* Depth to bottom of rupture */ + double zBot = in.zTor + v; + + /* Distance range over which site is normal to fault plane */ + double rCutLo = tan(δ) * in.zTor; + double rCutHi = tan(δ) * zBot + h; + + /* rRup values corresponding to cutoffs above */ + double rRupLo = Maths.hypot(in.zTor, rCutLo); + double rRupHi = Maths.hypot(zBot, rCutHi - h); + + double rJB = (r < 0) ? -r : (r < h) ? 0.0 : r - h; + double rRup = (r < rCutLo) + ? hypot(r, in.zTor) + : (r > rCutHi) + ? hypot(r - h, zBot) + : rRupScaled( + r, rCutLo, rCutHi, rRupLo, rRupHi); + + return GmmInput.builder() + .fromCopy(in) + .distances(rJB, rRup, r) + .build(); + } + + private static Map<Gmm, GmmDataXs<double[]>> calculateGroundMotions( + Set<Gmm> gmms, + List<GmmInput> gmmInputs, + Imt imt, + double[] distances) { + + Map<Gmm, GmmDataXs<double[]>> gmValues = new EnumMap<>(Gmm.class); + + for (Gmm gmm : gmms) { + List<LogicTree<GroundMotion>> trees = new ArrayList<>(); + GroundMotionModel model = gmm.instance(imt); + for (GmmInput gmmInput : gmmInputs) { + trees.add(model.calc(gmmInput)); + } + GmmDataXs<double[]> dataGroup = GmmCalc.treesToDataGroup(Doubles.asList(distances), trees); + gmValues.put(gmm, dataGroup); + } + return gmValues; + } + + /* + * Computes rRup for a surface distance r. The range [rCutLo, rCutHi] must + * contain r; rRupLo and rRupHi are rRup at rCutLo and rCutHi, respectively. + */ + private static double rRupScaled( + double r, + double rCutLo, + double rCutHi, + double rRupLo, + double rRupHi) { + + double rRupΔ = rRupHi - rRupLo; + double rCutΔ = rCutHi - rCutLo; + return rRupLo + (r - rCutLo) / rCutΔ * rRupΔ; + } + + static class GmmSpectraData { + final Optional<GmmData<Double>> pga; + final Optional<GmmData<Double>> pgv; + final GmmDataXs<double[]> sa; + + GmmSpectraData( + Optional<GmmData<Double>> pga, + Optional<GmmData<Double>> pgv, + GmmDataXs<double[]> sa) { + this.pga = pga; + this.pgv = pgv; + this.sa = sa; + } + } + + static class GmmData<T> { + final T μs; + final T σs; + final List<EpiBranch<T>> tree; + + GmmData( + T μs, + T σs, + List<EpiBranch<T>> tree) { + + this.μs = μs; + this.σs = σs; + this.tree = tree; + } + } + + static class GmmDataXs<T> extends GmmData<T> { + + final T xs; + + GmmDataXs( + T xs, + T μs, + T σs, + List<EpiBranch<T>> tree) { + super(μs, σs, tree); + this.xs = xs; + } + } + + static class EpiBranch<T> { + + final String id; + final T μs; + final T σs; + final T weights; + + EpiBranch(String id, T μs, T σs, T weights) { + this.id = id; + this.μs = μs; + this.σs = σs; + this.weights = weights; + } + } + + static class SpectraTree { + final String id; + final TreeValues values; + final TreeValues weights; + + SpectraTree(String id, TreeValues values, TreeValues weights) { + this.id = id; + this.values = values; + this.weights = weights; + } + + static List<SpectraTree> toList( + List<EpiSeries<Double>> pgaBranches, + List<EpiSeries<Double>> pgvBranches, + List<EpiSeries<double[]>> saBranches) { + List<SpectraTree> trees = new ArrayList<>(); + + for (int i = 0; i < saBranches.size(); i++) { + Optional<EpiSeries<Double>> pga = + pgaBranches.isEmpty() ? Optional.empty() : Optional.of(pgaBranches.get(i)); + Optional<EpiSeries<Double>> pgv = + pgvBranches.isEmpty() ? Optional.empty() : Optional.of(pgvBranches.get(i)); + + EpiSeries<double[]> sa = saBranches.get(i); + + TreeValues values = new TreeValues( + pga.isPresent() ? pga.get().values : null, + pgv.isPresent() ? pgv.get().values : null, + sa.values); + + TreeValues weights = new TreeValues( + pga.isPresent() ? pga.get().weights : null, + pgv.isPresent() ? pgv.get().weights : null, + sa.weights); + + trees.add(new SpectraTree(sa.id, values, weights)); + } + + return trees; + } + } + + static class TreeValues { + final Double pga; + final Double pgv; + final double[] sa; + + TreeValues(Double pga, Double pgv, double[] sa) { + this.pga = pga; + this.pgv = pgv; + this.sa = sa; + } + } + +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmController.java b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmController.java new file mode 100644 index 0000000000000000000000000000000000000000..c9ff40c624ddf08a7763cbe84586f07ef67ea35c --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmController.java @@ -0,0 +1,547 @@ +package gov.usgs.earthquake.nshmp.www.gmm; + +import java.util.Optional; +import java.util.Set; + +import gov.usgs.earthquake.nshmp.gmm.Gmm; +import gov.usgs.earthquake.nshmp.gmm.GmmInput; +import gov.usgs.earthquake.nshmp.gmm.Imt; +import gov.usgs.earthquake.nshmp.www.NshmpMicronautServlet; +import gov.usgs.earthquake.nshmp.www.gmm.GmmService.Distance; +import gov.usgs.earthquake.nshmp.www.gmm.GmmService.Id; +import gov.usgs.earthquake.nshmp.www.gmm.GmmService.Magnitude; +import gov.usgs.earthquake.nshmp.www.gmm.GmmService.Request; +import gov.usgs.earthquake.nshmp.www.gmm.GmmService.Spectra; + +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.QueryValue; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.inject.Inject; + +@Tag(name = "Ground Motion Models") +@Controller("${nshmp-haz.gmm-path}") +class GmmController { + + private static final String JAVADOC_URL = + "https://earthquake.usgs.gov/nshmp/docs/nshmp-lib/gov/usgs/earthquake/nshmp"; + + private static final String GMM_URL = JAVADOC_URL + "/gmm/Gmm.html"; + private static final String GMM_INPUT_URL = JAVADOC_URL + "/gmm/GmmInput.html"; + private static final String DEFAULTS_URL = + JAVADOC_URL + "/gmm/GmmInput.Builder.html#withDefaults()"; + + private static final String GMM_INPUT_PARAMS = + "<pre>[Mw, rJB, rRup, rX, dip, width, zTor, rake, vs30, z1p0, z2p5, zSed]</pre>"; + + private static final String SPECTRA_QUERY_1 = "/gmm/spectra?gmm=ASK_14"; + private static final String SPECTRA_QUERY_2 = + "/gmm/spectra?gmm=ASK_14,BSSA_14,CB_14,CY_14&Mw=7.2&vs30=530"; + private static final String SPECTRA_EX_1 = + "<a href=\"" + SPECTRA_QUERY_1 + "\">" + SPECTRA_QUERY_1 + "</a>"; + private static final String SPECTRA_EX_2 = + "<a href=\"" + SPECTRA_QUERY_2 + "\">" + SPECTRA_QUERY_2 + "</a>"; + + private static final String SPECTRA_DESCRIPTION = + "Returns the response spectra of one or more GMMs for a given scenario defined " + + "by the following source and site parameters: " + GMM_INPUT_PARAMS + "<br><br>" + + "At a minimum, one GMM argument is required (default source and site parameters are used):" + + "<br><br>" + SPECTRA_EX_1 + "<br><br>" + + "Alternatively, multiple GMMs may be requested with custom source and site parameters:" + + "<br><br>" + SPECTRA_EX_2 + "<br><br>" + + "See the <i>nshmp-haz</i> documentation for the list of supported " + + "<a href=\"" + GMM_URL + "\">GMM IDs</a> and <a href=\"" + GMM_INPUT_URL + "\"> " + + "GMM input parameters</a> and built in <a href=\"" + DEFAULTS_URL + + "\"> default values</a>." + + "<br><br>For supported GMMs and *GmmInput* parameters with default values " + + "see the usage information." + + "<br><br>Given no query parameters the usage information is returned."; + + private static final String SPECTRA_SUMMARY = + "Ground motion model (GMM) deterministic response spectra"; + + @Inject + private NshmpMicronautServlet servlet; + + /** + * GET method for response spectra. + * + * <p> Web service path: /nshmp/data/spectra + * + * <p> A {@code GmmInput} is constructed off the remaining HTTP parameters + * that are not defined in the @Get + * + * @param request The HTTP request + * @param gmm The ground motion models + * @param Mw The moment magnitude of an earthquake ([-2, 9.7]) + * @param rJB The shortest distance from a site to the surface projection of a + * rupture, in kilometers ([0, 1000]) + * @param rRup The shortest distance from a site to a rupture, in kilometers + * ([0, 1000]) + * @param rX The shortest distance from a site to the extended trace a fault, + * in kilometers ([0, 1000]) + * @param dip The dip of a rupture surface, in degrees ([0, 90]) + * @param width The width of a rupture surface, in kilometers ([0, 60]) + * @param zTop The depth to the top of a rupture surface, in kilometers and + * positive-down ([0, 700]) + * @param zHyp The depth to the hypocenter on a rupture surface, in kilometers + * and positive-down ([0, 700]) + * @param rake The rake (or sense of slip) of a rupture surface, in degrees + * ([-180, 1080]) + * @param vs30 The average shear-wave velocity down to 30 meters, in + * kilometers per second ([150, 3000]) + * @param z1p0 Depth to a shear-wave velocity of 1.0 kilometers per second, in + * kilometers ([0, 5]) + * @param z2p5 Depth to a shear-wave velocity of 2.5 kilometers per second, in + * kilometers ([0, 10]) + * @param zSed Sediment thickness, in kilometers ([0, 20]) + */ + @Operation( + summary = SPECTRA_SUMMARY, + description = SPECTRA_DESCRIPTION, + operationId = "gmm_spectra_doGetSpectra") + @ApiResponse( + description = "Response Spectra Service", + responseCode = "200", + content = @Content(schema = @Schema(type = "string"))) + @Get(uri = "/spectra", produces = MediaType.APPLICATION_JSON) + public String doGetSpectra( + HttpRequest<?> http, + @Schema(required = true) @QueryValue @Nullable Set<Gmm> gmm, + @Schema( + defaultValue = "7.5", + minimum = "-2", + maximum = "9.7") @QueryValue @Nullable Double Mw, + @Schema( + defaultValue = "10", + minimum = "0", + maximum = "1000") @QueryValue @Nullable Double rJB, + @Schema( + defaultValue = "10.3", + minimum = "0", + maximum = "1000") @QueryValue @Nullable Double rRup, + @Schema( + defaultValue = "10", + minimum = "0", + maximum = "1000") @QueryValue @Nullable Double rX, + @Schema( + defaultValue = "90", + minimum = "0", + maximum = "90") @QueryValue @Nullable Double dip, + @Schema( + defaultValue = "14", + minimum = "0", + maximum = "60") @QueryValue @Nullable Double width, + @Schema( + defaultValue = "0.5", + minimum = "0", + maximum = "700") @QueryValue @Nullable Double zTop, + @Schema( + defaultValue = "7.5", + minimum = "0", + maximum = "700") @QueryValue @Nullable Double zHyp, + @Schema( + defaultValue = "0", + minimum = "-180", + maximum = "180") @QueryValue @Nullable Double rake, + @Schema( + defaultValue = "760", + minimum = "150", + maximum = "3000") @QueryValue @Nullable Double vs30, + @Schema( + defaultValue = "", + minimum = "0", + maximum = "5") @QueryValue @Nullable Double z1p0, + @Schema( + defaultValue = "", + minimum = "0", + maximum = "10") @QueryValue @Nullable Double z2p5, + @Schema( + defaultValue = "", + minimum = "0", + maximum = "20") @QueryValue @Nullable Double zSed) { + Id id = Id.SPECTRA; + try { + Set<Gmm> gmms = ServiceUtil.readGmms(http); + if (gmms.isEmpty()) { + return ServiceUtil.metadata(http, id); + } + GmmInput in = ServiceUtil.readGmmInput(http); + Request gmmRequest = new Request(http, id, gmms, in); + return Spectra.process(gmmRequest); + } catch (Exception e) { + return Utils.handleError(e, id.name, http.getUri().getPath()); + } + } + + /** + * GET method for ground motion vs. distance, with distance in log space. + * + * <p> Web service path: /nshmp/data/gmm/distance + * + * <p> A {@code GmmInput} is constructed off the remaining HTTP parameters + * that are not defined in the @Get + * + * @param request The HTTP request + * @param gmm The ground motion models + * @param imt The IMT + * @param rMin The minimum distance to compute ((0, 1000]) + * @param rMax The maximum distance to compute ((0, 1000]) + * @param Mw The moment magnitude of an earthquake ([-2, 9.7]) + * @param rJB The shortest distance from a site to the surface projection of a + * rupture, in kilometers ([0, 1000]) + * @param rRup The shortest distance from a site to a rupture, in kilometers + * ([0, 1000]) + * @param rX The shortest distance from a site to the extended trace a fault, + * in kilometers ([0, 1000]) + * @param dip The dip of a rupture surface, in degrees ([0, 90]) + * @param width The width of a rupture surface, in kilometers ([0, 60]) + * @param zTop The depth to the top of a rupture surface, in kilometers and + * positive-down ([0, 700]) + * @param zHyp The depth to the hypocenter on a rupture surface, in kilometers + * and positive-down ([0, 700]) + * @param rake The rake (or sense of slip) of a rupture surface, in degrees + * ([-180, 1080]) + * @param vs30 The average shear-wave velocity down to 30 meters, in + * kilometers per second ([150, 3000]) + * @param z1p0 Depth to a shear-wave velocity of 1.0 kilometers per second, in + * kilometers ([0, 5]) + * @param z2p5 Depth to a shear-wave velocity of 2.5 kilometers per second, in + * kilometers ([0, 10]) + * @param zSed Sediment thickness, in kilometers ([0, 20]) + */ + @Operation( + summary = "Return ground motion Vs. distance in log space given a ground motion model", + description = "Returns ground motion Vs. distance in log space " + + "given a GMM and a *GmmInput*.\n\n" + + "For supported GMMs and *GmmInput* parameters with default values " + + "see the usage information.\n\n" + + "Given no query parameters the usage information is returned.", + operationId = "gmm_distance_doGetDistance") + @ApiResponse( + description = "Ground motion Vs. distance in log space", + responseCode = "200", + content = @Content(schema = @Schema(type = "string"))) + @Get(uri = "/distance{?gmm,imt,rMin,rMax}", produces = MediaType.APPLICATION_JSON) + public String doGetDistance( + HttpRequest<?> http, + @Schema(required = true) @QueryValue @Nullable Set<Gmm> gmm, + @Schema(required = true) @QueryValue Optional<Imt> imt, + @Schema( + defaultValue = "0.001", + minimum = "0", + maximum = "1000", + exclusiveMinimum = true) @QueryValue @Nullable Double rMin, + @Schema( + defaultValue = "100", + minimum = "0", + maximum = "1000", + exclusiveMinimum = true) @QueryValue @Nullable Double rMax, + @Schema( + defaultValue = "6.5", + minimum = "-2", + maximum = "9.7") @QueryValue @Nullable Double Mw, + @Schema( + defaultValue = "10", + minimum = "0", + maximum = "1000") @QueryValue @Nullable Double rJB, + @Schema( + defaultValue = "10.3", + minimum = "0", + maximum = "1000") @QueryValue @Nullable Double rRup, + @Schema( + defaultValue = "10", + minimum = "0", + maximum = "1000") @QueryValue @Nullable Double rX, + @Schema( + defaultValue = "90", + minimum = "0", + maximum = "90") @QueryValue @Nullable Double dip, + @Schema( + defaultValue = "14", + minimum = "0", + maximum = "60") @QueryValue @Nullable Double width, + @Schema( + defaultValue = "0.5", + minimum = "0", + maximum = "700") @QueryValue @Nullable Double zTop, + @Schema( + defaultValue = "7.5", + minimum = "0", + maximum = "700") @QueryValue @Nullable Double zHyp, + @Schema( + defaultValue = "0", + minimum = "-180", + maximum = "180") @QueryValue @Nullable Double rake, + @Schema( + defaultValue = "760", + minimum = "150", + maximum = "3000") @QueryValue @Nullable Double vs30, + @Schema( + defaultValue = "", + minimum = "0", + maximum = "5") @QueryValue @Nullable Double z1p0, + @Schema( + defaultValue = "", + minimum = "0", + maximum = "10") @QueryValue @Nullable Double z2p5, + @Schema( + defaultValue = "", + minimum = "0", + maximum = "20") @QueryValue @Nullable Double zSed) { + Id id = Id.DISTANCE; + try { + Set<Gmm> gmms = ServiceUtil.readGmms(http); + if (gmms.isEmpty()) { + return ServiceUtil.metadata(http, id); + } + GmmInput in = ServiceUtil.readGmmInput(http); + Distance.Request request = new Distance.Request( + http, id, gmms, in, imt, Optional.ofNullable(rMin), Optional.ofNullable(rMax)); + return Distance.process(request); + } catch (Exception e) { + return Utils.handleError(e, id.name, http.getUri().getPath()); + } + } + + /** + * GET method for ground motion vs. distance. + * + * <p> Web service path: /nshmp/data/gmm/distance + * + * <p> A {@code GmmInput} is constructed off the remaining HTTP parameters + * that are not defined in the @Get + * + * @param request The HTTP request + * @param gmm The ground motion models + * @param imt The IMT + * @param rMin The minimum distance to compute ([-1000, 1000]) + * @param rMax The maximum distance to compute ([-1000, 1000]) + * @param Mw The moment magnitude of an earthquake ([-2, 9.7]) + * @param dip The dip of a rupture surface, in degrees ([0, 90]) + * @param width The width of a rupture surface, in kilometers ([0, 60]) + * @param zTop The depth to the top of a rupture surface, in kilometers and + * positive-down ([0, 700]) + * @param zHyp The depth to the hypocenter on a rupture surface, in kilometers + * and positive-down ([0, 700]) + * @param rake The rake (or sense of slip) of a rupture surface, in degrees + * ([-180, 1080]) + * @param vs30 The average shear-wave velocity down to 30 meters, in + * kilometers per second ([150, 3000]) + * @param z1p0 Depth to a shear-wave velocity of 1.0 kilometers per second, in + * kilometers ([0, 5]) + * @param z2p5 Depth to a shear-wave velocity of 2.5 kilometers per second, in + * kilometers ([0, 10]) + * @param zSed Sediment thickness, in kilometers ([0, 20]) + */ + @Operation( + summary = "Return ground motion Vs. distance given a ground motion model", + description = "Returns ground motion Vs. distance given a GMM and a *GmmInput*.\n\n" + + "For supported GMMs and *GmmInput* parameters with default values " + + "see the usage information.\n\n" + + "Given no query parameters the usage information is returned.", + operationId = "gmm_hwfw_doGetHwFw") + @ApiResponse( + description = "Ground motion Vs. distance", + responseCode = "200", + content = @Content(schema = @Schema(type = "string"))) + @Get(uri = "/hw-fw{?gmm,imt,rMin,rMax}", produces = MediaType.APPLICATION_JSON) + public String doGetHwFw( + HttpRequest<?> http, + @Schema(required = true) @QueryValue @Nullable Set<Gmm> gmm, + @Schema(required = true) @QueryValue Optional<Imt> imt, + @Schema( + defaultValue = "-100", + minimum = "-1000", + maximum = "1000", + exclusiveMinimum = true) @QueryValue @Nullable Double rMin, + @Schema( + defaultValue = "100", + minimum = "-1000", + maximum = "1000", + exclusiveMinimum = true) @QueryValue @Nullable Double rMax, + @Schema( + defaultValue = "6.5", + minimum = "-2", + maximum = "9.7") @QueryValue @Nullable Double Mw, + @Schema( + defaultValue = "90", + minimum = "0", + maximum = "90") @QueryValue @Nullable Double dip, + @Schema( + defaultValue = "14", + minimum = "0", + maximum = "60") @QueryValue @Nullable Double width, + @Schema( + defaultValue = "0.5", + minimum = "0", + maximum = "700") @QueryValue @Nullable Double zTop, + @Schema( + defaultValue = "7.5", + minimum = "0", + maximum = "700") @QueryValue @Nullable Double zHyp, + @Schema( + defaultValue = "0", + minimum = "-180", + maximum = "180") @QueryValue @Nullable Double rake, + @Schema( + defaultValue = "760", + minimum = "150", + maximum = "3000") @QueryValue @Nullable Double vs30, + @Schema( + defaultValue = "", + minimum = "0", + maximum = "5") @QueryValue @Nullable Double z1p0, + @Schema( + defaultValue = "", + minimum = "0", + maximum = "10") @QueryValue @Nullable Double z2p5, + @Schema( + defaultValue = "", + minimum = "0", + maximum = "20") @QueryValue @Nullable Double zSed) { + + Id id = Id.HW_FW; + try { + Set<Gmm> gmms = ServiceUtil.readGmms(http); + if (gmms.isEmpty()) { + return ServiceUtil.metadata(http, id); + } + GmmInput in = ServiceUtil.readGmmInput(http); + Distance.Request request = new Distance.Request( + http, id, gmms, in, imt, Optional.ofNullable(rMin), Optional.ofNullable(rMax)); + return Distance.process(request); + } catch (Exception e) { + return Utils.handleError(e, id.name, http.getUri().getPath()); + } + } + + /** + * GET method for ground motion vs. distance. + * + * <p> Web service path: /nshmp/data/gmm/distance + * + * <p> A {@code GmmInput} is constructed off the remaining HTTP parameters + * that are not defined in the @Get + * + * @param request The HTTP request + * @param gmm The ground motion models + * @param imt The IMT + * @param rMin The minimum distance to compute ([-1000, 1000]) + * @param rMax The maximum distance to compute ([-1000, 1000]) + * @param Mw The moment magnitude of an earthquake ([-2, 9.7]) + * @param dip The dip of a rupture surface, in degrees ([0, 90]) + * @param width The width of a rupture surface, in kilometers ([0, 60]) + * @param zTop The depth to the top of a rupture surface, in kilometers and + * positive-down ([0, 700]) + * @param zHyp The depth to the hypocenter on a rupture surface, in kilometers + * and positive-down ([0, 700]) + * @param rake The rake (or sense of slip) of a rupture surface, in degrees + * ([-180, 1080]) + * @param vs30 The average shear-wave velocity down to 30 meters, in + * kilometers per second ([150, 3000]) + * @param z1p0 Depth to a shear-wave velocity of 1.0 kilometers per second, in + * kilometers ([0, 5]) + * @param z2p5 Depth to a shear-wave velocity of 2.5 kilometers per second, in + * kilometers ([0, 10]) + * @param zSed Sediment thickness, in kilometers ([0, 20]) + */ + @Operation( + summary = "Return ground motion Vs. magnitude given a ground motion model", + description = "Returns ground motion Vs. magnitude given a GMM and a *GmmInput*.\n\n" + + "For supported GMMs and *GmmInput* parameters with default values " + + "see the usage information.\n\n" + + "Given no query parameters the usage information is returned.", + operationId = "gmm_magnitude_doGetMagnitude") + @ApiResponse( + description = "Ground motion Vs. magnitude", + responseCode = "200", + content = @Content(schema = @Schema(type = "string"))) + @Get(uri = "/magnitude{?gmm,imt,mMin,mMax,step}", produces = MediaType.APPLICATION_JSON) + public String doGetMagnitude( + HttpRequest<?> http, + @Schema(required = true) @QueryValue @Nullable Set<Gmm> gmm, + @Schema(required = true) @QueryValue Optional<Imt> imt, + @Schema( + defaultValue = "5", + minimum = "-2", + maximum = "9.7", + exclusiveMinimum = true) @QueryValue @Nullable Double mMin, + @Schema( + defaultValue = "8", + minimum = "-2", + maximum = "9.7", + exclusiveMinimum = true) @QueryValue @Nullable Double mMax, + @Schema( + defaultValue = "0.1", + minimum = "0", + maximum = "1", + exclusiveMinimum = true) @QueryValue @Nullable Double step, + @Schema( + defaultValue = "10", + minimum = "0", + maximum = "1000", + exclusiveMinimum = true) @QueryValue @Nullable Double distance, + @Schema( + defaultValue = "6.5", + minimum = "-2", + maximum = "9.7") @QueryValue @Nullable Double Mw, + @Schema( + defaultValue = "90", + minimum = "0", + maximum = "90") @QueryValue @Nullable Double dip, + @Schema( + defaultValue = "14", + minimum = "0", + maximum = "60") @QueryValue @Nullable Double width, + @Schema( + defaultValue = "0.5", + minimum = "0", + maximum = "700") @QueryValue @Nullable Double zTop, + @Schema( + defaultValue = "7.5", + minimum = "0", + maximum = "700") @QueryValue @Nullable Double zHyp, + @Schema( + defaultValue = "0", + minimum = "-180", + maximum = "180") @QueryValue @Nullable Double rake, + @Schema( + defaultValue = "760", + minimum = "150", + maximum = "3000") @QueryValue @Nullable Double vs30, + @Schema( + defaultValue = "", + minimum = "0", + maximum = "5") @QueryValue @Nullable Double z1p0, + @Schema( + defaultValue = "", + minimum = "0", + maximum = "10") @QueryValue @Nullable Double z2p5, + @Schema( + defaultValue = "", + minimum = "0", + maximum = "20") @QueryValue @Nullable Double zSed) { + Id id = Id.MAGNITUDE; + try { + Set<Gmm> gmms = ServiceUtil.readGmms(http); + if (gmms.isEmpty()) { + return ServiceUtil.metadata(http, id); + } + GmmInput in = ServiceUtil.readGmmInput(http); + Magnitude.Request gmmRequest = new Magnitude.Request( + http, id, gmms, in, imt, Optional.ofNullable(mMin), Optional.ofNullable(mMax), + Optional.ofNullable(step), Optional.ofNullable(distance)); + return Magnitude.process(gmmRequest); + } catch (Exception e) { + return Utils.handleError(e, id.name, http.getUri().getPath()); + } + } +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmService.java b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmService.java new file mode 100644 index 0000000000000000000000000000000000000000..b4b5587fec5bb67848770678343613f4b2b91d60 --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmService.java @@ -0,0 +1,499 @@ +package gov.usgs.earthquake.nshmp.www.gmm; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import gov.usgs.earthquake.nshmp.Maths; +import gov.usgs.earthquake.nshmp.data.DoubleData; +import gov.usgs.earthquake.nshmp.data.XySequence; +import gov.usgs.earthquake.nshmp.gmm.Gmm; +import gov.usgs.earthquake.nshmp.gmm.GmmInput; +import gov.usgs.earthquake.nshmp.gmm.Imt; +import gov.usgs.earthquake.nshmp.www.HazVersion; +import gov.usgs.earthquake.nshmp.www.ResponseBody; +import gov.usgs.earthquake.nshmp.www.ResponseMetadata; +import gov.usgs.earthquake.nshmp.www.gmm.GmmCalc.EpiBranch; +import gov.usgs.earthquake.nshmp.www.gmm.GmmCalc.GmmDataXs; +import gov.usgs.earthquake.nshmp.www.gmm.GmmCalc.GmmSpectraData; +import gov.usgs.earthquake.nshmp.www.gmm.GmmCalc.SpectraTree; +import gov.usgs.earthquake.nshmp.www.gmm.XyDataGroup.EpiSeries; + +import io.micronaut.http.HttpRequest; +import jakarta.inject.Singleton; + +/* + * GMM service implementations. + * + * @author U.S. Geological Survey + */ +@Singleton +class GmmService { + + /* Base request object for all GMM services. */ + static class Request { + + transient final HttpRequest<?> http; + transient final Id serviceId; + + final Set<Gmm> gmms; + final GmmInput input; + + public Request( + HttpRequest<?> http, Id id, + Set<Gmm> gmms, GmmInput input) { + this.http = http; + this.serviceId = id; + this.input = input; + this.gmms = gmms; + } + } + + /* Response object for all GMM services. */ + static class Response<T, U> { + + XyDataGroup<T, U> means; + XyDataGroup<T, U> sigmas; + + private Response(Id service, Optional<Imt> imt) { + String yLabelMedian = imt.isPresent() + ? String.format("%s (%s)", service.yLabelMedian, imt.get().units()) + : service.yLabelMedian; + + means = XyDataGroup.create( + service.groupNameMean, + service.xLabel, + yLabelMedian); + + sigmas = XyDataGroup.create( + service.groupNameSigma, + service.xLabel, + service.yLabelSigma); + } + + static Response<XySequence, EpiSeries<double[]>> create( + Id service, + Map<Gmm, GmmDataXs<double[]>> result, + Optional<Imt> imt) { + + Response<XySequence, EpiSeries<double[]>> response = new Response<>(service, imt); + + for (Entry<Gmm, GmmDataXs<double[]>> entry : result.entrySet()) { + Gmm gmm = entry.getKey(); + GmmDataXs<double[]> data = entry.getValue(); + + XySequence μTotal = XySequence.create(data.xs, formatExp(data.μs)); + XySequence σTotal = XySequence.create(data.xs, format(data.σs)); + + EpiGroup<double[]> group = treeToEpiGroup(data.tree); + response.means.add(gmm.name(), gmm.toString(), μTotal, group.μ); + response.sigmas.add(gmm.name(), gmm.toString(), σTotal, group.sigma); + } + + return response; + } + + private static EpiGroup<Double> treeToEpiGroupSingle(List<EpiBranch<Double>> tree) { + List<EpiSeries<Double>> μEpi = List.of(); + List<EpiSeries<Double>> sigmaEpi = List.of(); + + // If we have a GMM with a single GroundMotion branch, + // then the DataGroup branch list is empty + if (!tree.isEmpty()) { + boolean hasSigmaBranches = tree.get(0).id.contains(" : "); + + μEpi = tree.stream() + .map(b -> new EpiSeries<>(b.id, formatExp(b.μs), b.weights)) + .collect(Collectors.toList()); + + if (hasSigmaBranches) { + μEpi = collapseGmTreeSingle(μEpi, 0); + + sigmaEpi = tree.stream() + .map(b -> new EpiSeries<>(b.id, format(b.σs), b.weights)) + .collect(Collectors.toList()); + sigmaEpi = collapseGmTreeSingle(sigmaEpi, 1); + } + } + + return new EpiGroup<>(μEpi, sigmaEpi); + } + + private static EpiGroup<double[]> treeToEpiGroup(List<EpiBranch<double[]>> tree) { + List<EpiSeries<double[]>> μEpi = List.of(); + List<EpiSeries<double[]>> sigmaEpi = List.of(); + + // If we have a GMM with a single GroundMotion branch, + // then the DataGroup branch list is empty + if (!tree.isEmpty()) { + boolean hasSigmaBranches = tree.get(0).id.contains(" : "); + + μEpi = tree.stream() + .map(b -> new EpiSeries<>(b.id, formatExp(b.μs), b.weights)) + .collect(Collectors.toList()); + + if (hasSigmaBranches) { + μEpi = collapseGmTree(μEpi, 0); + + sigmaEpi = tree.stream() + .map(b -> new EpiSeries<>(b.id, format(b.σs), b.weights)) + .collect(Collectors.toList()); + sigmaEpi = collapseGmTree(sigmaEpi, 1); + } + } + + return new EpiGroup<>(μEpi, sigmaEpi); + } + + static Response<SpectraData, SpectraTree> spectraCreate( + Id service, + Map<Gmm, GmmSpectraData> result, + Optional<Imt> imt) { + + Response<SpectraData, SpectraTree> response = new Response<>(service, imt); + + for (Entry<Gmm, GmmSpectraData> entry : result.entrySet()) { + Gmm gmm = entry.getKey(); + GmmSpectraData data = entry.getValue(); + + XySequence saμTotal = XySequence.create(data.sa.xs, formatExp(data.sa.μs)); + XySequence saσTotal = XySequence.create(data.sa.xs, format(data.sa.σs)); + + SpectraData μTotal = new SpectraData( + data.pga.isPresent() ? formatExp(data.pga.get().μs) : null, + data.pgv.isPresent() ? formatExp(data.pgv.get().μs) : null, + saμTotal); + + SpectraData σTotal = new SpectraData( + data.pga.isPresent() ? format(data.pga.get().σs) : null, + data.pgv.isPresent() ? format(data.pgv.get().σs) : null, + saσTotal); + + EpiGroup<double[]> saGroup = treeToEpiGroup(data.sa.tree); + Optional<EpiGroup<Double>> pgaGroup = Optional.empty(); + Optional<EpiGroup<Double>> pgvGroup = Optional.empty(); + + if (data.pga.isPresent()) { + pgaGroup = Optional.of(treeToEpiGroupSingle(data.pga.get().tree)); + } + + if (data.pgv.isPresent()) { + pgvGroup = Optional.of(treeToEpiGroupSingle(data.pgv.get().tree)); + } + + List<SpectraTree> μTrees = SpectraTree.toList( + pgaGroup.isPresent() ? pgaGroup.get().μ : List.of(), + pgvGroup.isPresent() ? pgvGroup.get().μ : List.of(), + saGroup.μ); + + List<SpectraTree> sigmaTrees = SpectraTree.toList( + pgaGroup.isPresent() ? pgaGroup.get().sigma : List.of(), + pgvGroup.isPresent() ? pgvGroup.get().sigma : List.of(), + saGroup.sigma); + + response.means.add(gmm.name(), gmm.toString(), μTotal, μTrees); + response.sigmas.add(gmm.name(), gmm.toString(), σTotal, sigmaTrees); + } + + return response; + } + } + + static class Spectra { + + static String process(Request request) { + Map<Gmm, GmmSpectraData> spectra = GmmCalc.spectra(request); + Response<SpectraData, SpectraTree> response = + Response.spectraCreate(request.serviceId, spectra, Optional.empty()); + var body = ResponseBody.success() + .name(request.serviceId.name) + .url(request.http.getUri().getPath()) + .metadata(new ResponseMetadata(HazVersion.appVersions())) + .request(request) + .response(response) + .build(); + return ServiceUtil.GSON.toJson(body); + } + + } + + static class Distance { + + private static final double R_MIN = -100.0; + private static final double R_MAX = 100.0; + private final static int R_POINTS = 100; + + static String process(Distance.Request request) { + + double[] rArray = distanceArray(request); + + Map<Gmm, GmmDataXs<double[]>> gmvr = GmmCalc.distance(request, rArray); + Response<XySequence, EpiSeries<double[]>> response = + Response.create(request.serviceId, gmvr, Optional.of(request.imt)); + var body = ResponseBody.success() + .name(request.serviceId.name) + .url(request.http.getUri().getPath()) + .metadata(new ResponseMetadata(HazVersion.appVersions())) + .request(request) + .response(response) + .build(); + return ServiceUtil.GSON.toJson(body); + } + + private static double[] distanceArray( + GmmService.Distance.Request request) { + boolean isLog = request.serviceId.equals(Id.DISTANCE) ? true : false; + double rStep = isLog + ? (Math.log10(request.rMax / request.rMin)) / (R_POINTS - 1) + : 1.0; + return isLog + ? ServiceUtil.sequenceLog(request.rMin, request.rMax, rStep) + : ServiceUtil.sequenceLinear(request.rMin, request.rMax, rStep); + } + + static class Request extends GmmService.Request { + + Imt imt; + double rMin; + double rMax; + + Request( + HttpRequest<?> http, Id serviceId, + Set<Gmm> gmms, GmmInput in, + Optional<Imt> imt, + Optional<Double> rMin, + Optional<Double> rMax) { + + super(http, serviceId, gmms, in); + this.imt = imt.orElse(Imt.PGA); + this.rMin = rMin.orElse(R_MIN); + this.rMax = rMax.orElse(R_MAX); + } + } + } + + static class Magnitude { + + public static final double M_MIN = 5.0; + public static final double M_MAX = 8.0; + public static final double M_STEP = 0.1; + public static final double M_DISTANCE = 10.0; + + static String process(Magnitude.Request request) { + + double[] mArray = ServiceUtil.sequenceLinear( + request.mMin, request.mMax, request.step); + + Map<Gmm, GmmDataXs<double[]>> gmvm = GmmCalc.magnitude(request, mArray, request.distance); + Response<XySequence, EpiSeries<double[]>> response = + Response.create(request.serviceId, gmvm, Optional.of(request.imt)); + var body = ResponseBody.success() + .name(request.serviceId.name) + .url(request.http.getUri().getPath()) + .metadata(new ResponseMetadata(HazVersion.appVersions())) + .request(request) + .response(response) + .build(); + return ServiceUtil.GSON.toJson(body); + } + + static class Request extends GmmService.Request { + + Imt imt; + double mMin; + double mMax; + double step; + double distance; + + Request( + HttpRequest<?> http, Id serviceId, + Set<Gmm> gmms, GmmInput in, + Optional<Imt> imt, + Optional<Double> mMin, + Optional<Double> mMax, + Optional<Double> step, + Optional<Double> distance) { + + super(http, serviceId, gmms, in); + this.imt = imt.orElse(Imt.PGA); + this.mMin = mMin.orElse(M_MIN); + this.mMax = mMax.orElse(M_MAX); + this.step = step.orElse(M_STEP); + this.distance = distance.orElse(M_DISTANCE); + } + } + } + + /* + * Consolidators of epi branches. GMMs return logic trees that represent all + * combinations of means and sigmas. For the response spectra service we want + * those branches with either common means or common sigmas recombined. + * + * We do know a bit about the branch IDs in ground motion logic trees and we + * can leverage that when combining branches. They always take the form + * "μ_id : σ_id" so and index of 0 will consolidate using the IDs of the mean + * branches and an index of 1 will consolidate using the IDs of the sigma + * branches. + */ + + static List<EpiSeries<Double>> collapseGmTreeSingle(List<EpiSeries<Double>> epiList, int index) { + List<String> ids = new ArrayList<>(); + Map<String, Double> valueMap = new HashMap<>(); + Map<String, Double> wtMap = new HashMap<>(); + + for (EpiSeries<Double> epi : epiList) { + String id = epi.id.split(" : ")[index]; + + if (!ids.contains(id)) { + ids.add(id); + valueMap.put(id, epi.values); + wtMap.put(id, epi.weights); + } else { + wtMap.merge(id, epi.weights, (a, b) -> a + b); + } + } + + return ids.stream() + .map(id -> new EpiSeries<>(id, valueMap.get(id), format(wtMap.get(id)))) + .collect(Collectors.toList()); + } + + static List<EpiSeries<double[]>> collapseGmTree(List<EpiSeries<double[]>> epiList, int index) { + + List<String> ids = new ArrayList<>(); + Map<String, double[]> valueMap = new HashMap<>(); + Map<String, double[]> wtMap = new HashMap<>(); + + for (EpiSeries<double[]> epi : epiList) { + String id = epi.id.split(" : ")[index]; + if (!ids.contains(id)) { + ids.add(id); + valueMap.put(id, epi.values); + wtMap.put(id, Arrays.copyOf(epi.weights, epi.weights.length)); + } else { + wtMap.merge(id, epi.weights, DoubleData::add); + } + } + return ids.stream() + .map(id -> new EpiSeries<>(id, valueMap.get(id), format(wtMap.get(id)))) + .collect(Collectors.toList()); + } + + private static double formatExp(double value) { + return Maths.roundToDigits(Math.exp(value), ServiceUtil.ROUND); + } + + private static double[] formatExp(double[] values) { + return Arrays.stream(values) + .map(d -> formatExp(d)) + .toArray(); + } + + private static double format(double value) { + return Maths.roundToDigits(value, ServiceUtil.ROUND); + } + + private static double[] format(double[] values) { + return Arrays.stream(values) + .map(d -> format(d)) + .toArray(); + } + + static class SpectraData { + final Double pga; + final Double pgv; + final XySequence sa; + + SpectraData(Double pga, Double pgv, XySequence sa) { + this.pga = pga; + this.pgv = pgv; + this.sa = sa; + } + } + + private static class EpiGroup<T> { + final List<EpiSeries<T>> μ; + final List<EpiSeries<T>> sigma; + + EpiGroup(List<EpiSeries<T>> μ, List<EpiSeries<T>> sigma) { + this.μ = μ; + this.sigma = sigma; + } + } + + public static enum Id { + + DISTANCE( + "Ground Motion Vs. Distance", + "Compute ground motion Vs. distance", + "/distance", + "Means", + "Sigmas", + "Distance (km)", + "Median ground motion", + "Standard deviation"), + + HW_FW( + "Hanging Wall Effect", + "Compute hanging wall effect on ground motion Vs. distance", + "/hw-fw", + "Means", + "Sigmas", + "Distance (km)", + "Median ground motion", + "Standard deviation"), + + MAGNITUDE( + "Ground Motion Vs. Magnitude", + "Compute ground motion Vs. magnitude", + "/magnitude", + "Means", + "Sigmas", + "Magnitude", + "Median ground motion", + "Standard deviation"), + + SPECTRA( + "Deterministic Response Spectra", + "Compute deterministic response spectra", + "/spectra", + "Means", + "Sigmas", + "Period (s)", + "Median ground motion (g)", + "Standard deviation"); + + public final String name; + public final String description; + public final String pathInfo; + public final String resultName; + public final String groupNameMean; + public final String groupNameSigma; + public final String xLabel; + public final String yLabelMedian; + public final String yLabelSigma; + + private Id( + String name, String description, + String pathInfo, String groupNameMean, + String groupNameSigma, String xLabel, + String yLabelMedian, String yLabelSigma) { + this.name = name; + this.description = description; + this.resultName = name + " Results"; + this.pathInfo = pathInfo; + this.groupNameMean = groupNameMean; + this.groupNameSigma = groupNameSigma; + this.xLabel = xLabel; + this.yLabelMedian = yLabelMedian; + this.yLabelSigma = yLabelSigma; + } + } + +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/ServiceUtil.java b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/ServiceUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..d4ad5af48931ddb287659f3ac652e00f2524675d --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/ServiceUtil.java @@ -0,0 +1,459 @@ +package gov.usgs.earthquake.nshmp.www.gmm; + +import static gov.usgs.earthquake.nshmp.gmm.GmmInput.Field.DIP; +import static gov.usgs.earthquake.nshmp.gmm.GmmInput.Field.MW; +import static gov.usgs.earthquake.nshmp.gmm.GmmInput.Field.RAKE; +import static gov.usgs.earthquake.nshmp.gmm.GmmInput.Field.RJB; +import static gov.usgs.earthquake.nshmp.gmm.GmmInput.Field.RRUP; +import static gov.usgs.earthquake.nshmp.gmm.GmmInput.Field.RX; +import static gov.usgs.earthquake.nshmp.gmm.GmmInput.Field.VS30; +import static gov.usgs.earthquake.nshmp.gmm.GmmInput.Field.WIDTH; +import static gov.usgs.earthquake.nshmp.gmm.GmmInput.Field.Z1P0; +import static gov.usgs.earthquake.nshmp.gmm.GmmInput.Field.Z2P5; +import static gov.usgs.earthquake.nshmp.gmm.GmmInput.Field.ZHYP; +import static gov.usgs.earthquake.nshmp.gmm.GmmInput.Field.ZSED; +import static gov.usgs.earthquake.nshmp.gmm.GmmInput.Field.ZTOR; +import static gov.usgs.earthquake.nshmp.gmm.Imt.AI; +import static gov.usgs.earthquake.nshmp.gmm.Imt.PGD; +import static io.micronaut.core.type.Argument.DOUBLE; +import static java.util.stream.Collectors.toCollection; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import com.google.common.collect.Range; +import com.google.common.primitives.Doubles; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import gov.usgs.earthquake.nshmp.data.DoubleData; +import gov.usgs.earthquake.nshmp.data.Sequences; +import gov.usgs.earthquake.nshmp.gmm.Gmm; +import gov.usgs.earthquake.nshmp.gmm.GmmInput; +import gov.usgs.earthquake.nshmp.gmm.GmmInput.Constraints; +import gov.usgs.earthquake.nshmp.gmm.GmmInput.Field; +import gov.usgs.earthquake.nshmp.gmm.Imt; +import gov.usgs.earthquake.nshmp.model.TectonicSetting; +import gov.usgs.earthquake.nshmp.www.HazVersion; +import gov.usgs.earthquake.nshmp.www.ResponseBody; +import gov.usgs.earthquake.nshmp.www.ResponseMetadata; +import gov.usgs.earthquake.nshmp.www.WsUtils; +import gov.usgs.earthquake.nshmp.www.gmm.GmmService.Id; +import gov.usgs.earthquake.nshmp.www.meta.EnumParameter; + +import io.micronaut.http.HttpParameters; +import io.micronaut.http.HttpRequest; + +class ServiceUtil { + + final static int ROUND = 5; + static final Range<Double> MAGNITUDE_DEFAULT_VALUES = Range.closed(5.0, 8.0); + static final double MAGNITUDE_DEFAULT_STEP = 0.1; + static final double MAGNITUDE_DEFAULT_DISTANCE = 10.0; + static final Range<Double> DISTANCE_DEFAULT_VALUES = Range.closed(-100.0, 100.0); + + static final Gson GSON; + + static { + GSON = new GsonBuilder() + .setPrettyPrinting() + .serializeNulls() + .disableHtmlEscaping() + .registerTypeAdapter(Double.class, new WsUtils.NaNSerializer()) + .registerTypeAdapter(ServiceUtil.Parameters.class, + new ServiceUtil.Parameters.Serializer()) + .registerTypeAdapter(Imt.class, new WsUtils.EnumSerializer<Imt>()) + .registerTypeAdapter(Constraints.class, new WsUtils.ConstraintsSerializer()) + .create(); + } + + public static String metadata(HttpRequest<?> request, Id service) { + return GSON.toJson(ServiceUtil.getMetadata(request, service)); + } + + /** Query and JSON reqest/response keys. */ + static final class Key { + public static final String DISTANCE = "distance"; + public static final String IMT = "imt"; + public static final String GMM = "gmm"; + public static final String M_MIN = "mMin"; + public static final String M_MAX = "mMax"; + public static final String R_MAX = "rMax"; + public static final String R_MIN = "rMin"; + public static final String STEP = "step"; + } + + /* Read the GMM input query values. */ + static GmmInput readGmmInput(HttpRequest<?> request) { + HttpParameters params = request.getParameters(); + GmmInput.Builder b = GmmInput.builder().withDefaults(); + params.getFirst(MW.id, DOUBLE).ifPresent(b::mag); + params.getFirst(RJB.id, DOUBLE).ifPresent(b::rJB); + params.getFirst(RRUP.id, DOUBLE).ifPresent(b::rRup); + params.getFirst(RX.id, DOUBLE).ifPresent(b::rX); + params.getFirst(DIP.id, DOUBLE).ifPresent(b::dip); + params.getFirst(WIDTH.id, DOUBLE).ifPresent(b::width); + params.getFirst(ZTOR.id, DOUBLE).ifPresent(b::zTor); + params.getFirst(ZHYP.id, DOUBLE).ifPresent(b::zHyp); + params.getFirst(RAKE.id, DOUBLE).ifPresent(b::rake); + params.getFirst(VS30.id, DOUBLE).ifPresent(b::vs30); + params.getFirst(Z1P0.id, DOUBLE).ifPresent(b::z1p0); + params.getFirst(Z2P5.id, DOUBLE).ifPresent(b::z2p5); + params.getFirst(ZSED.id, DOUBLE).ifPresent(b::zSed); + return b.build(); + } + + /* Read the 'gmm' query values. */ + static Set<Gmm> readGmms(HttpRequest<?> request) { + return request.getParameters() + .getAll(Key.GMM) + .stream() + .map(s -> s.split(",")) + .flatMap(Arrays::stream) + .map(Gmm::valueOf) + .collect(toCollection(() -> EnumSet.noneOf(Gmm.class))); + } + + static ResponseBody<String, MetadataResponse> getMetadata( + HttpRequest<?> request, + Id service) { + + String url = request.getUri().getPath(); + return ResponseBody.<String, MetadataResponse> usage() + .name(service.name) + .url(url) + .metadata(new ResponseMetadata(HazVersion.appVersions())) + .request(url) + .response(new MetadataResponse(request, service)) + .build(); + } + + static double[] sequenceLog(double min, double max, double step) { + return DoubleData.round( + ROUND, + DoubleData.pow10( + Sequences.arrayBuilder(Math.log10(min), Math.log10(max), step) + .centered() + .build())); + } + + static double[] sequenceLinear(double min, double max, double step) { + return Sequences.arrayBuilder(min, max, step) + .scale(ROUND) + .centered() + .build(); + } + + static final class MetadataResponse { + String description; + String syntax; + Parameters parameters; + + public MetadataResponse(HttpRequest<?> request, Id service) { + this.syntax = request.getUri().getPath(); + this.description = service.description; + this.parameters = new Parameters(service); + } + } + + @SuppressWarnings("unchecked") + private static Param createGmmInputParam( + Field field, + Optional<?> constraint) { + + // if (field == VSINF) return new BooleanParam(field); + return new NumberParam(field, (Range<Double>) constraint.orElseThrow()); + } + + /* + * Placeholder class; all parameter serialization is done via the custom + * Serializer. Service reference needed serialize(). + */ + static final class Parameters { + + private final Id service; + + Parameters(Id service) { + this.service = service; + } + + static final class Serializer implements JsonSerializer<Parameters> { + + @Override + public JsonElement serialize( + Parameters meta, + Type type, + JsonSerializationContext context) { + + JsonObject root = new JsonObject(); + + if (!meta.service.equals(Id.SPECTRA)) { + Set<Imt> imtSet = EnumSet.complementOf(EnumSet.range(PGD, AI)); + final EnumParameter<Imt> imts = new EnumParameter<>( + "Intensity measure type", + imtSet); + root.add(Key.IMT, context.serialize(imts)); + } + + /* Serialize input fields. */ + Constraints defaults = Constraints.defaults(); + for (Field field : Field.values()) { + if (!meta.service.equals(Id.SPECTRA) && + (field.equals(Field.RX) || field.equals(Field.RRUP) || field.equals(Field.RJB))) { + continue; + } + + Param param = createGmmInputParam(field, defaults.get(field)); + JsonElement fieldElem = context.serialize(param); + root.add(field.id, fieldElem); + } + + if (meta.service.equals(Id.DISTANCE) || meta.service.equals(Id.HW_FW)) { + double min = meta.service.equals(Id.HW_FW) + ? Range.openClosed(0.0, 1.0).lowerEndpoint() + : DISTANCE_DEFAULT_VALUES.lowerEndpoint(); + NumberParam rMin = new NumberParam( + "Minimum distance", + "The minimum distance to use for calculation", + Field.RX.units.orElse(null), + Range.openClosed(0.0, 1000.0), + min); + root.add(Key.R_MIN, context.serialize(rMin)); + + NumberParam rMax = new NumberParam( + "Maximum distance", + "The maximum distance to use for calculation", + Field.RX.units.orElse(null), + Range.closed(-1000.0, 1000.0), + DISTANCE_DEFAULT_VALUES.upperEndpoint()); + root.add(Key.R_MAX, context.serialize(rMax)); + } + + if (meta.service.equals(Id.MAGNITUDE)) { + @SuppressWarnings("unchecked") + NumberParam mMin = new NumberParam( + "Minimum distance", + "The minimum distance to use for calculation", + Field.MW.units.orElse(null), + (Range<Double>) defaults.get(Field.MW).orElseThrow(), + MAGNITUDE_DEFAULT_VALUES.lowerEndpoint()); + root.add(Key.M_MIN, context.serialize(mMin)); + + @SuppressWarnings("unchecked") + NumberParam mMax = new NumberParam( + "Maximum magnitude", + "The maximum magnitude to use for calculation", + Field.MW.units.orElse(null), + (Range<Double>) defaults.get(Field.MW).orElseThrow(), + MAGNITUDE_DEFAULT_VALUES.upperEndpoint()); + root.add(Key.M_MAX, context.serialize(mMax)); + + @SuppressWarnings("unchecked") + NumberParam distance = new NumberParam( + "Distance", + "The distance between site and trace ", + Field.RX.units.orElse(null), + (Range<Double>) defaults.get(Field.RX).orElseThrow(), + MAGNITUDE_DEFAULT_DISTANCE); + root.add(Key.DISTANCE, context.serialize(distance)); + + NumberParam step = new NumberParam( + "Magnitude step", + "The step between each magnitude ", + null, + Range.closed(0.0001, 1.0), + MAGNITUDE_DEFAULT_STEP); + root.add(Key.STEP, context.serialize(step)); + } + + /* Add only add those Gmms that belong to a Group. */ + List<Gmm> gmms = Arrays.stream(Gmm.Group.values()) + .flatMap(group -> group.gmms().stream()) + .sorted(Comparator.comparing(Object::toString)) + .distinct() + .collect(Collectors.toList()); + + GmmParam gmmParam = new GmmParam( + GMM_NAME, + GMM_INFO, + gmms); + root.add(Key.GMM, context.serialize(gmmParam)); + + /* Add gmm groups. */ + GroupParam groups = new GroupParam( + GROUP_NAME, + GROUP_INFO, + EnumSet.allOf(Gmm.Group.class)); + root.add(GROUP_KEY, context.serialize(groups)); + + return root; + } + } + } + + /* + * Marker interface for spectra parameters. This was previously implemented as + * an abstract class for label, info, and units, but Gson serialized subclass + * fields before parent fields. To maintain a preferred order, one can write + * custom serializers or repeat these four fields in each implementation. + */ + private static interface Param {} + + @SuppressWarnings("unused") + private static final class NumberParam implements Param { + + final String label; + final String info; + final String units; + final Double min; + final Double max; + final Double value; + + NumberParam(GmmInput.Field field, Range<Double> constraint) { + this(field, constraint, field.defaultValue); + } + + NumberParam(GmmInput.Field field, Range<Double> constraint, Double value) { + this.label = field.label; + this.info = field.info; + this.units = field.units.orElse(null); + this.min = constraint.lowerEndpoint(); + this.max = constraint.upperEndpoint(); + this.value = Doubles.isFinite(value) ? value : null; + } + + NumberParam(String label, String info, String units, Range<Double> constraint, Double value) { + this.label = label; + this.info = info; + this.units = units; + this.min = constraint.lowerEndpoint(); + this.max = constraint.upperEndpoint(); + this.value = Doubles.isFinite(value) ? value : null; + } + } + + @SuppressWarnings("unused") + @Deprecated + private static final class BooleanParam implements Param { + + // used to support vsInf + // will likely be used for other flags in future (e.g. vertical GM) + + final String label; + final String info; + final boolean value; + + BooleanParam(GmmInput.Field field) { + this(field, field.defaultValue == 1.0); + } + + BooleanParam(GmmInput.Field field, boolean value) { + this.label = field.label; + this.info = field.info; + this.value = value; + } + } + + private static final String GMM_NAME = "Ground Motion Models"; + private static final String GMM_INFO = "Empirical models of ground motion"; + + @SuppressWarnings("unused") + private static class GmmParam implements Param { + + final String label; + final String info; + final List<Value> values; + + GmmParam(String label, String info, List<Gmm> gmms) { + this.label = label; + this.info = info; + this.values = gmms.stream() + .map(gmm -> new Value(gmm)) + .collect(Collectors.toList()); + } + + private static class Value { + + final String id; + final String label; + final Gmm.Type type; + final ArrayList<String> supportedImts; + final Constraints constraints; + + Value(Gmm gmm) { + this.id = gmm.name(); + this.label = gmm.toString(); + this.type = gmm.type(); + this.supportedImts = supportedImts(gmm.supportedImts()); + this.constraints = gmm.constraints(); + } + } + + private static ArrayList<String> supportedImts(Set<Imt> imts) { + ArrayList<String> supportedImts = new ArrayList<>(); + + for (Imt imt : imts) { + supportedImts.add(imt.name()); + } + + return supportedImts; + } + + } + + private static final String GROUP_KEY = "group"; + private static final String GROUP_NAME = "Ground Motion Model Groups"; + private static final String GROUP_INFO = "Groups of related ground motion models "; + + @SuppressWarnings("unused") + private static final class GroupParam implements Param { + + final String label; + final String info; + final List<Value> values; + + GroupParam(String label, String info, Set<Gmm.Group> groups) { + this.label = label; + this.info = info; + this.values = new ArrayList<>(); + for (Gmm.Group group : groups) { + this.values.add(new Value(group)); + } + } + + private static class Value { + + final String id; + final String label; + final List<Gmm> data; + final String type; + + Value(Gmm.Group group) { + this.id = group.name(); + this.label = group.toString(); + this.data = group.gmms(); + + /* TODO get group gmm type directly from group */ + if (group.toString().contains("Active Volcanic (HI)")) { + this.type = TectonicSetting.VOLCANIC.name(); + } else { + this.type = group.gmms().stream() + .map(gmm -> gmm.type().name()) + .findFirst() + .orElseThrow(); + } + } + } + } +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/Utils.java b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/Utils.java new file mode 100644 index 0000000000000000000000000000000000000000..66668f228229a11c4f7ac2e24aee0e28c8becea7 --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/Utils.java @@ -0,0 +1,65 @@ +package gov.usgs.earthquake.nshmp.www.gmm; + +import static com.google.common.base.CaseFormat.LOWER_CAMEL; +import static com.google.common.base.CaseFormat.UPPER_UNDERSCORE; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import gov.usgs.earthquake.nshmp.www.HazVersion; +import gov.usgs.earthquake.nshmp.www.ResponseBody; +import gov.usgs.earthquake.nshmp.www.ResponseMetadata; + +public class Utils { + public static final Gson GSON; + + static { + GSON = new GsonBuilder() + .disableHtmlEscaping() + .serializeNulls() + .setPrettyPrinting() + .create(); + } + + public enum Key { + DISTANCE, + ID, + IMT, + GMM, + GPSDATASET, + GROUP, + LABEL, + LATITUDE, + LONGITUDE, + M_MIN, + M_MAX, + MODEL, + NAME, + R_MAX, + R_MIN, + STEP, + Z1P0, + Z2P5; + + @Override + public String toString() { + return UPPER_UNDERSCORE.to(LOWER_CAMEL, name()); + } + } + + public static String handleError( + Throwable e, + String name, + String url) { + var msg = e.getMessage() + " (see logs)"; + var svcResponse = ResponseBody.error() + .name(name) + .url(url) + .metadata(new ResponseMetadata(HazVersion.appVersions())) + .request(msg) + .response(url) + .build(); + e.printStackTrace(); + return GSON.toJson(svcResponse); + } +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/XyDataGroup.java b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/XyDataGroup.java new file mode 100644 index 0000000000000000000000000000000000000000..c7a233cef7d4dcf868046591f5d8f881936de514 --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/XyDataGroup.java @@ -0,0 +1,65 @@ +package gov.usgs.earthquake.nshmp.www.gmm; + +import java.util.ArrayList; +import java.util.List; + +/* + * XY data sequences serialization object. + * + * @author U.S. Geological Survey + */ +@SuppressWarnings("unused") +class XyDataGroup<T, U> { + + private final String label; + private final String xLabel; + private final String yLabel; + private final List<Series<T, U>> data; + + private XyDataGroup(String label, String xLabel, String yLabel) { + this.label = label; + this.xLabel = xLabel; + this.yLabel = yLabel; + this.data = new ArrayList<>(); + } + + /* Create a data group. */ + static <T, U> XyDataGroup<T, U> create(String name, String xLabel, String yLabel) { + return new XyDataGroup<T, U>(name, xLabel, yLabel); + } + + /* Add a data sequence */ + XyDataGroup<T, U> add(String id, String name, T data, List<U> tree) { + this.data.add(new Series<>(id, name, data, tree)); + return this; + } + + static class Series<T, U> { + + private final String id; + private final String label; + private final T data; + private final List<U> tree; + + Series(String id, String label, T data, List<U> tree) { + this.id = id; + this.label = label; + this.data = data; + this.tree = tree; + } + } + + static class EpiSeries<T> { + + final String id; + final T values; + final T weights; + + EpiSeries(String id, T values, T weights) { + this.id = id; + this.values = values; + this.weights = weights; + } + } + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 37590421097c60101a0d4a1605d81860d1859c07..ecab27f174462fa9862f1f8363935d9652e5167f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -21,3 +21,11 @@ nshmp-haz: # java -jar build/libs/nshmp-haz.jar --model=<path/to/model> # model-path: ${MODEL:nshms/nshm-conus-2018} + + ## + # Whether this is a GMM service deploy + gmm: ${GMM_DEPLOY:false} + + ## + # The base path for GMM services + gmm-path: ${GMM_PATH:/gmm}