diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/NshmpMicronautServlet.java b/src/main/java/gov/usgs/earthquake/nshmp/www/NshmpMicronautServlet.java new file mode 100644 index 0000000000000000000000000000000000000000..0336511aa1df100b1d8af0489ceca4568c91f7ea --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/NshmpMicronautServlet.java @@ -0,0 +1,51 @@ +package gov.usgs.earthquake.nshmp.www; + +import org.reactivestreams.Publisher; + +import io.micronaut.core.type.MutableHeaders; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.annotation.Filter; +import io.micronaut.http.filter.HttpServerFilter; +import io.micronaut.http.filter.ServerFilterChain; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.schedulers.Schedulers; + +/** + * Custom NSHMP servlet implementation and URL helper class for Micronaut + * services. + * + * <p>This class sets custom response headers and provides a helper class to + * ensure serialized response URLs propagate the correct host and protocol from + * requests on USGS servers and caches that may have been forwarded. + * + * @author U.S. Geological Survey + */ +@Filter("/**") +public class NshmpMicronautServlet implements HttpServerFilter { + + /* + * Set CORS headers and content type. + * + * Because NSHMP services may be called by both the USGS website, other + * websites, and directly by 3rd party applications, responses generated by + * direct requests will not have the necessary header information that would + * be required by security protocols for web requests. This means that any + * initial direct request will pollute intermediate caches with a response + * that a browser will deem invalid. + */ + @Override + public Publisher<MutableHttpResponse<?>> doFilter( + HttpRequest<?> request, + ServerFilterChain chain) { + return Flowable.just(chain).subscribeOn(Schedulers.io()) + .switchMap(bool -> chain.proceed(request)) + .doOnNext(res -> { + MutableHeaders headers = res.getHeaders(); + headers.add("Access-Control-Allow-Origin", "*"); + headers.add("Access-Control-Allow-Methods", "*"); + headers.add("Access-Control-Allow-Headers", "accept,origin,authorization,content-type"); + }); + } + +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/ResponseBody.java b/src/main/java/gov/usgs/earthquake/nshmp/www/ResponseBody.java new file mode 100644 index 0000000000000000000000000000000000000000..0e10375315a5a716f3de429e0c5ce6853f687078 --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/ResponseBody.java @@ -0,0 +1,213 @@ +package gov.usgs.earthquake.nshmp.www; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.time.ZonedDateTime; + +import gov.usgs.earthquake.nshmp.www.meta.Status; + +/** + * Generic wrapper around a web service response object that is typically + * serialized to JSON and sent back to requestor as an HttpResponse 'body'. + * + * <p>To create a response, use one of the three static builder methods: + * {@link Builder#error()}, {@link Builder#success()}, or + * {@link Builder#usage()}. + * + * @author U.S. Geological Survey + * + * @param <T> The request type + * @param <V> The response type + */ +public class ResponseBody<T, V> { + + private final String name; + private final String date; + private final String status; + private final String url; + private final T request; + private final V response; + private final ResponseMetadata metadata; + + private ResponseBody(Builder<T, V> builder) { + name = builder.name; + date = ZonedDateTime.now().format(WsUtils.DATE_FMT); + status = builder.status; + url = builder.url; + request = builder.request; + response = builder.response; + metadata = builder.metadata; + } + + protected ResponseBody() { + date = null; + metadata = null; + name = null; + request = null; + response = null; + status = null; + url = null; + } + + /** + * The date and time this request/response. + */ + public String getDate() { + return date; + } + + /** + * The metadata. + */ + public ResponseMetadata getMetadata() { + return metadata; + } + + /** + * The name of the service. + */ + public String getName() { + return name; + } + + /** + * The request object. + */ + public T getRequest() { + return request; + } + + /** + * The response object. + */ + public V getResponse() { + return response; + } + + /** + * The response status. + */ + public String getStatus() { + return status; + } + + /** + * The URL used to call the service + */ + public String getUrl() { + return url; + } + + /** + * Create a new builder initialized to an error response. + * + * @param <T> The request type + * @param <V> The response type + */ + public static <T, V> Builder<T, V> error() { + return new Builder<T, V>(Status.ERROR); + } + + /** + * Create a new builder initialized to a success response. + * + * @param <T> The request type + * @param <V> The response type + */ + public static <T, V> Builder<T, V> success() { + return new Builder<T, V>(Status.SUCCESS); + } + + /** + * Create a new builder initialized to a usage response. + * + * @param <T> The request type + * @param <V> The response type + */ + public static <T, V> Builder<T, V> usage() { + return new Builder<T, V>(Status.USAGE); + } + + /** + * A {@code ResponseBody} builder. + * + * @param <T> The request type + * @param <V> The response type + */ + public static class Builder<T, V> { + + private String name; + private String status; + private String url; + private ResponseMetadata metadata; + private T request; + private V response; + + private Builder(String status) { + this.status = status; + } + + /** + * Set the metadata. + * + * @param metadata The servie metadata + */ + public Builder<T, V> metadata(ResponseMetadata metadata) { + this.metadata = metadata; + return this; + } + + /** + * Set the service name. + * + * @param name of the service being called + */ + public Builder<T, V> name(String name) { + this.name = name; + return this; + } + + /** + * Set the request object. + * + * @param request object + */ + public Builder<T, V> request(T request) { + this.request = request; + return this; + } + + /** + * Set the response object. + * + * @param response object + */ + public Builder<T, V> response(V response) { + this.response = response; + return this; + } + + /** + * Set the url used to call the service. + * + * @param url used to generate this response + */ + public Builder<T, V> url(String url) { + this.url = url; + return this; + } + + /** + * Returns a new Response + */ + public ResponseBody<T, V> build() { + checkNotNull(metadata); + checkNotNull(name); + checkNotNull(request); + checkNotNull(response); + checkNotNull(url); + checkNotNull(status); + return new ResponseBody<T, V>(this); + } + } +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/ResponseMetadata.java b/src/main/java/gov/usgs/earthquake/nshmp/www/ResponseMetadata.java new file mode 100644 index 0000000000000000000000000000000000000000..9439810fecc6eb97a0336c45e4cb2819aa1339e2 --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/ResponseMetadata.java @@ -0,0 +1,18 @@ +package gov.usgs.earthquake.nshmp.www; + +import gov.usgs.earthquake.nshmp.internal.AppVersion.VersionInfo; + +/** + * The response metadata with version info. + */ +public class ResponseMetadata { + public final VersionInfo[] repositories; + + public ResponseMetadata(VersionInfo... repositories) { + this.repositories = repositories; + } + + public VersionInfo[] getRepositories() { + return repositories; + } +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerUtils.java b/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..92d460b4c930172271658e9c7a52dc37a5b2cfea --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerUtils.java @@ -0,0 +1,157 @@ +package gov.usgs.earthquake.nshmp.www; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import gov.usgs.earthquake.nshmp.geo.Location; +import gov.usgs.earthquake.nshmp.gmm.Imt; +import gov.usgs.earthquake.nshmp.gmm.NehrpSiteClass; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.parameters.Parameter; +import io.swagger.v3.parser.util.SchemaTypeUtil; + +/** + * General Swagger utilities + * + * @author U.S. Geological Survey + */ +public class SwaggerUtils { + + /** + * Update "longitude" and "latitude" parameters with min and max bounds and + * add bounds to description. + * + * @param parameters The Swagger parameters + * @param min The minimum bounds + * @param max The maximum bounds + * @return + */ + public static List<Parameter> addLocationBounds( + List<Parameter> parameters, + Location min, + Location max) { + var latitudeDescription = String.format(" [%s, %s]", min.latitude, max.latitude); + var longtudeDescription = String.format(" [%s, %s]", min.longitude, max.longitude); + + parameters.forEach(parameter -> { + if (parameter.getName().equals("latitude")) { + parameter.setDescription(parameter.getDescription() + latitudeDescription); + parameter.getSchema().setMinimum(BigDecimal.valueOf(min.latitude)); + parameter.getSchema().setMaximum(BigDecimal.valueOf(max.latitude)); + } else if (parameter.getName().equals("longitude")) { + parameter.setDescription(parameter.getDescription() + longtudeDescription); + parameter.getSchema().setMinimum(BigDecimal.valueOf(min.longitude)); + parameter.getSchema().setMaximum(BigDecimal.valueOf(max.longitude)); + } + }); + + return parameters; + } + + /** + * Update any "longitude" and "latitude" parameters with min and max bounds + * and add bounds to description. + * + * @param openApi The Open API + * @param min The minimum location bounds + * @param max The maximum location bounds + */ + public static OpenAPI addLocationBounds(OpenAPI openApi, Location min, Location max) { + openApi.getPaths().values().stream() + .flatMap(path -> path.readOperations().stream()) + .forEach(operation -> addLocationBounds(operation.getParameters(), min, max)); + + return openApi; + } + + /** + * Returns markdown string listing the IMTs. + * + * @param siteClasses The IMTs + * @param heading The Markdown heading value. Default: ### + */ + public static String imtInfo(List<Imt> imts, Optional<String> heading) { + return new StringBuilder() + .append(heading.orElse("###") + " Intensity Measure Types \n") + .append(imts.stream().sorted() + .map(imt -> "- " + imt.toString()) + .collect(Collectors.joining("\n"))) + .append("\n") + .toString(); + } + + /** + * Returns updated Swagger schemas with provided IMTs. + * + * @param imts The IMTs + */ + public static Map<String, Schema> imtSchema( + Map<String, Schema> schemas, + List<Imt> imts) { + var schema = new Schema<String>(); + schema.setType(SchemaTypeUtil.STRING_TYPE); + imts.stream() + .sorted() + .forEach(imt -> schema.addEnumItemObject(imt.name())); + + schemas.put(Imt.class.getSimpleName(), schema); + return schemas; + } + + /** + * Returns markdown string listing the min and max location bounds. + * + * @param min The minimum bounds + * @param max The maximum bounds + * @param heading The Markdown heading value. Default: ### + */ + public static String locationBoundsInfo(Location min, Location max, Optional<String> heading) { + return new StringBuilder() + .append(heading.orElse("###") + " Latitude Bounds\n") + .append(String.format("- Minimum Latitude: %s°\n", min.latitude)) + .append(String.format("- Maximum Latitude: %s°\n", max.latitude)) + .append(heading.orElse("###") + " Longitude Bounds\n") + .append(String.format("- Minimum Longitude: %s°\n", min.longitude)) + .append(String.format("- Maximum Longitude: %s°\n", max.longitude)) + .toString(); + } + + /** + * Returns markdown string listing the site classes. + * + * @param siteClasses The NEHRP site classes + * @param heading The Markdown heading value. Default: ### + */ + public static String siteClassInfo(List<NehrpSiteClass> siteClasses, Optional<String> heading) { + return new StringBuilder() + .append(heading.orElse("###") + " Site Classes\n") + .append(siteClasses.stream().sorted() + .map(siteClass -> "- " + siteClass.toString()) + .collect(Collectors.joining("\n"))) + .append("\n") + .toString(); + } + + /** + * Returns updated Swagger schemas with provided site classes. + * + * @param siteClasses The NERHP site classes + */ + public static Map<String, Schema> siteClassSchema( + Map<String, Schema> schemas, + List<NehrpSiteClass> siteClasses) { + var schema = new Schema<NehrpSiteClass>(); + schema.setType(SchemaTypeUtil.STRING_TYPE); + siteClasses.stream() + .sorted() + .forEach(siteClass -> schema.addEnumItemObject(siteClass)); + + schemas.put(NehrpSiteClass.class.getSimpleName(), schema); + return schemas; + } +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/WsUtils.java b/src/main/java/gov/usgs/earthquake/nshmp/www/WsUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..1e0d6939a995fea473a307235794717c33b1db16 --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/WsUtils.java @@ -0,0 +1,100 @@ +package gov.usgs.earthquake.nshmp.www; + +import java.lang.reflect.Type; +import java.time.format.DateTimeFormatter; +import java.util.Optional; + +import com.google.common.collect.Range; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import gov.usgs.earthquake.nshmp.gmm.GmmInput; +import gov.usgs.earthquake.nshmp.gmm.GmmInput.Field; + +/** + * Web service utilities. + * + * @author U.S. Geological Survey + */ +public class WsUtils { + + public static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern( + "yyyy-MM-dd'T'HH:mm:ssXXX"); + + public static <T, E extends Enum<E>> T checkValue(E key, T value) { + if (value == null) { + throw new IllegalStateException("Missing [" + key.toString() + "]"); + } + + return value; + } + + /* Constrain all doubles to 8 decimal places */ + public static final class DoubleSerializer implements JsonSerializer<Double> { + @Override + public JsonElement serialize(Double d, Type type, JsonSerializationContext context) { + double dOut = Double.valueOf(String.format("%.8g", d)); + return new JsonPrimitive(dOut); + } + } + + /* Convert NaN to null */ + public static final class NaNSerializer implements JsonSerializer<Double> { + @Override + public JsonElement serialize(Double d, Type type, JsonSerializationContext context) { + return Double.isNaN(d) ? null : new JsonPrimitive(d); + } + } + + public static final class ConstraintsSerializer implements JsonSerializer<GmmInput.Constraints> { + @Override + public JsonElement serialize( + GmmInput.Constraints constraints, + Type type, + JsonSerializationContext context) { + JsonArray json = new JsonArray(); + + for (Field field : Field.values()) { + Optional<?> opt = constraints.get(field); + if (opt.isPresent()) { + Range<?> value = (Range<?>) opt.orElseThrow(); + Constraint constraint = new Constraint( + field.id, + value.lowerEndpoint(), + value.upperEndpoint()); + json.add(context.serialize(constraint)); + } + } + + return json; + } + } + + public static final class EnumSerializer<E extends Enum<E>> implements JsonSerializer<E> { + @Override + public JsonElement serialize(E src, Type type, JsonSerializationContext context) { + JsonObject jObj = new JsonObject(); + jObj.addProperty("value", src.name()); + jObj.addProperty("display", src.toString()); + + return jObj; + } + } + + @SuppressWarnings("unused") + private static class Constraint { + final String id; + final Object min; + final Object max; + + Constraint(String id, Object min, Object max) { + this.id = id; + this.min = min; + this.max = max; + } + } +} 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..4415dd3bded846dc8ca31c4b6095802d0799135b --- /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("/gmm") +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/java/gov/usgs/earthquake/nshmp/www/meta/EnumParameter.java b/src/main/java/gov/usgs/earthquake/nshmp/www/meta/EnumParameter.java new file mode 100644 index 0000000000000000000000000000000000000000..154bedc45e213eca83bd954c7936b25bec59903b --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/meta/EnumParameter.java @@ -0,0 +1,22 @@ +package gov.usgs.earthquake.nshmp.www.meta; + +import java.util.Set; + +/** + * An enum parameter. + * + * @author U.S. Geological Survey + * + * @param <E> The enum type + */ +public final class EnumParameter<E extends Enum<E>> { + + private final String label; + private final Set<E> values; + + public EnumParameter(String label, Set<E> values) { + this.label = label; + this.values = values; + } + +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/meta/Status.java b/src/main/java/gov/usgs/earthquake/nshmp/www/meta/Status.java new file mode 100644 index 0000000000000000000000000000000000000000..9511b464d8101fe317e31784006f2945dab9bade --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/meta/Status.java @@ -0,0 +1,18 @@ +package gov.usgs.earthquake.nshmp.www.meta; + +/** + * Service request status identifier. + * + * @author U.S. Geological Survey + */ +public class Status { + + /** Error reponse status. */ + public static final String ERROR = "error"; + + /** Success reponse status. */ + public static final String SUCCESS = "success"; + + /** Usage reponse status. */ + public static final String USAGE = "usage"; +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/meta/StringParameter.java b/src/main/java/gov/usgs/earthquake/nshmp/www/meta/StringParameter.java new file mode 100644 index 0000000000000000000000000000000000000000..d8262749207c60ed72b4aedd2dc1ddcb16354ee8 --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/meta/StringParameter.java @@ -0,0 +1,20 @@ +package gov.usgs.earthquake.nshmp.www.meta; + +import java.util.Set; + +/** + * A string parameter. + * + * @author U.S. Geological Survey + */ +public class StringParameter { + + public final String label; + public final Set<String> values; + + public StringParameter(String label, Set<String> values) { + this.label = label; + this.values = values; + } + +}