File

services/hazard-plots.service.ts

Description

Base options for creating hazard plots

Index

Properties

Properties

availableModels
availableModels: Parameter[]
Type : Parameter[]

Current avialable models

form
form: ControlForm
Type : ControlForm

Control panel form values

hazardCurveFilter
hazardCurveFilter: function
Type : function

Function to filter hazard curve data

name
name: function
Type : function

Function to return the name of the data

resetColor
resetColor: boolean
Type : boolean

Whether to reset the color of the line per model

import {Injectable} from '@angular/core';
import {hazardUtils} from '@ghsc/nshmp-lib-ng/hazard';
import {FormGroupControls, NumberBounds} from '@ghsc/nshmp-lib-ng/nshmp';
import {NshmpPlot, plotUtils} from '@ghsc/nshmp-lib-ng/plot';
import {
  HazardCurve,
  HazardResponse,
} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/hazard-service';
import {XySequence} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/data';
import {Imt, imtToPeriod} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/gmm';
import {
  SourceType,
  sourceTypeToPascalCase,
} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/model';
import {Parameter} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-utils/metadata';
import {PlotlyPlot} from '@ghsc/nshmp-utils-ts/libs/plotly';
import {Dash, PlotData} from 'plotly.js';

import {
  AppState,
  ControlForm,
  Plots,
  ServiceResponse,
  ServiceResponses,
} from '../models/state.model';
import {MathService} from './math.service';

/**
 * Base options for creating hazard plots
 */
interface BaseCreateHazardPlotOptions {
  /** Current avialable models */
  availableModels: Parameter[];
  /** Control panel form values */
  form: ControlForm;
  /** Function to filter hazard curve data */
  hazardCurveFilter: (data: HazardCurve) => boolean;
  /** Function to return the name of the data */
  name: (modelInfo: Parameter, data: HazardCurve) => string;
  /** Whether to reset the color of the line per model */
  resetColor: boolean;
}

/**
 * Options for creating hazard plot data.
 */
interface CreateHazardPlotDataOptions extends BaseCreateHazardPlotOptions {
  /** The service response for a model */
  serviceResponse: ServiceResponse;

  /** The dash type */
  dash?: Dash;
}

/**
 * Options for creating a hazard plot.
 */
interface CreateHazardPlotOptions extends BaseCreateHazardPlotOptions {
  /** Plot id */
  id: Plots;
  /** The current plot */
  plot: NshmpPlot;
  /** The service responses */
  serviceResponses: ServiceResponses;

  /** Dash types per model */
  dash?: PlotDash;
}

/**
 * Dash type for models
 */
interface PlotDash {
  /** Dash type for model A */
  modelA?: Dash;
  /** Dash type for model B */
  modelB?: Dash;
}

@Injectable({
  providedIn: 'root',
})
export class HazardPlotsService {
  constructor(private mathService: MathService) {}

  /**
   * Create the plots.
   *
   * @param state The app state
   */
  createHazardPlots(
    state: AppState,
    formGroup: FormGroupControls<ControlForm>,
  ): Map<string, NshmpPlot> {
    const plots = new Map<string, NshmpPlot>();

    const hazardPlot = state.plots.get(Plots.HAZARD);
    const hazardComponentsPlot = state.plots.get(Plots.HAZARD_COMPONENTS);
    const hazardDiffPlot = state.plots.get(Plots.HAZARD_DIFFERENCES);

    const hazardCurves = this.createHazardCurvesPlot(
      state.serviceResponses,
      state.availableModels,
      hazardPlot,
      formGroup.getRawValue(),
    );

    plots.set(Plots.HAZARD, {
      ...hazardPlot,
      plotData: hazardCurves,
    });

    const hazardCurveDifference = this.createHazardDiffPlot(
      state.serviceResponses,
      formGroup.getRawValue().imt,
      hazardDiffPlot,
    );

    plots.set(Plots.HAZARD_DIFFERENCES, {
      ...hazardDiffPlot,
      plotData: hazardCurveDifference,
    });

    const hazardCurveComponents = this.createHazardComponentsPlot(
      state.serviceResponses,
      state.availableModels,
      hazardComponentsPlot,
      formGroup.getRawValue(),
    );

    plots.set(Plots.HAZARD_COMPONENTS, {
      ...hazardComponentsPlot,
      plotData: hazardCurveComponents,
    });

    return plots;
  }

  /**
   * Create a new Plotly plot from the current model responses.
   *
   * @param serviceResponses The service responses for both models
   * @param imt The control panel IMT value
   * @param plot The current hazard difference plot
   */
  private createHazardDiffPlot(
    serviceResponses: ServiceResponses,
    imt: Imt,
    plot: NshmpPlot,
  ): PlotlyPlot {
    const data = this.createHazardDiffPlotData(serviceResponses, imt);
    const yValues = data.y as number[];

    const yMax = Math.max(...yValues.map(y => Math.abs(y))) * 1.3;
    const yRange = [-yMax, yMax];

    const {layout, mobileLayout} = plot.plotData;

    return {
      config: {...plot.plotData.config},
      data: [data],
      id: Plots.HAZARD_DIFFERENCES,
      layout: {
        ...layout,
        yaxis: {
          ...layout.yaxis,
          autorange: false,
          range: yRange,
        },
      },
      mobileConfig: {...plot.plotData.mobileConfig},
      mobileLayout: {
        ...mobileLayout,
        yaxis: {
          ...mobileLayout.yaxis,
          autorange: false,
          range: yRange,
        },
      },
    };
  }

  /**
   * Returns the Plotly plot data associated with the service responses
   * for hazard difference plot.
   *
   * @param serviceResponses The service responses for both models
   * @param imt The control panel IMT value
   */
  private createHazardDiffPlotData(
    serviceResponses: ServiceResponses,
    imt: Imt,
  ): Partial<PlotData> {
    const {modelA, modelB} = serviceResponses;

    const modelAResponse = this.getHazardResponse(modelA, imt);
    const modelBResponse = this.getHazardResponse(modelB, imt);

    const modelAData = hazardUtils.getSourceTypeData(
      modelAResponse.data,
      sourceTypeToPascalCase(SourceType.TOTAL),
    );
    const modelBData = hazardUtils.getSourceTypeData(
      modelBResponse.data,
      sourceTypeToPascalCase(SourceType.TOTAL),
    );

    const xValues = modelAData.values.xs.filter(x =>
      modelBData.values.xs.includes(x),
    );

    const yValues = this.mathService.percentDifferences(
      this.filterYValues(xValues, modelAData.values),
      this.filterYValues(xValues, modelBData.values),
    );

    return {
      hovertemplate: '%{x} g, %{y} %',
      mode: 'lines+markers',
      name: '% Difference',
      showlegend: false,
      x: xValues,
      y: yValues,
    };
  }

  /**
   * Create the hazard curves plot.
   *
   * @param serviceResponses The model responses
   * @param availableModels The available models
   * @param plot The current hazard plot
   * @param form The form values
   */
  private createHazardCurvesPlot(
    serviceResponses: ServiceResponses,
    availableModels: Parameter[],
    plot: NshmpPlot,
    form: ControlForm,
  ): PlotlyPlot {
    const imt = form.imt;

    const imtStr =
      imt === Imt.PGA || imt === Imt.PGV
        ? imt
        : `${imtToPeriod(imt)} s ${imt.substring(0, 2)}`;

    const hazardCurveFilter = (data: HazardCurve) =>
      data.component === sourceTypeToPascalCase(SourceType.TOTAL);

    const name = (modelInfo: Parameter) => `${modelInfo.display} - ${imtStr}`;

    return this.createHazardPlot({
      availableModels,
      form,
      hazardCurveFilter,
      id: Plots.HAZARD,
      name,
      plot,
      resetColor: false,
      serviceResponses,
    });
  }

  /**
   * Create the hazard components plot.
   *
   * @param serviceResponses The model responses
   * @param availableModels The available models
   * @param plot The current hazard plot
   * @param form The form values
   */
  private createHazardComponentsPlot(
    serviceResponses: ServiceResponses,
    availableModels: Parameter[],
    plot: NshmpPlot,
    form: ControlForm,
  ): PlotlyPlot {
    const hazardCurveFilter = (data: HazardCurve) =>
      data.component !== sourceTypeToPascalCase(SourceType.TOTAL);
    const name = (modelInfo: Parameter, data: HazardCurve) =>
      `${modelInfo.display} - ${data.component}`;

    return this.createHazardPlot({
      availableModels,
      dash: {
        modelA: 'solid',
        modelB: 'dash',
      },
      form,
      hazardCurveFilter,
      id: Plots.HAZARD_COMPONENTS,
      name,
      plot,
      resetColor: true,
      serviceResponses,
    });
  }

  /**
   * Returns a `PlotlyPlot` for a hazard plot.
   *
   * @param options The hazard plot options
   */
  private createHazardPlot(options: CreateHazardPlotOptions): PlotlyPlot {
    const dataA = this.createHazardPlotData({
      ...options,
      dash: options.dash?.modelA,
      serviceResponse: options.serviceResponses.modelA,
    });

    const dataB = this.createHazardPlotData({
      ...options,
      dash: options.dash?.modelB,
      serviceResponse: options.serviceResponses.modelB,
    });

    const limitsA = this.hazardCurveLimits(
      options.serviceResponses.modelA,
      options.form.imt,
      options.hazardCurveFilter,
    );

    const limitsB = this.hazardCurveLimits(
      options.serviceResponses.modelB,
      options.form.imt,
      options.hazardCurveFilter,
    );

    const xMin = Math.min(limitsA.min, limitsB.min);
    const xMax = Math.min(limitsA.max, limitsB.max);

    const returnPeriodData = this.createReturnPeriodPlotData(
      options.form.returnPeriod,
      [xMin, xMax],
    );

    const metadata =
      options.serviceResponses.modelA.hazardResponse.response.metadata;

    const layout = plotUtils.updatePlotLabels({
      layout: options.plot.plotData.layout,
      xLabel: metadata.xlabel,
      yLabel: metadata.ylabel,
    });

    const mobileLayout = plotUtils.updatePlotLabels({
      layout: options.plot.plotData.mobileLayout,
      xLabel: metadata.xlabel,
      yLabel: metadata.ylabel,
    });

    return {
      config: {...options.plot.plotData.config},
      data: [returnPeriodData, ...dataA, ...dataB],
      id: options.id,
      layout,
      mobileConfig: {...options.plot.plotData.mobileConfig},
      mobileLayout,
    };
  }

  /**
   * Create the plot data for hazard component plot.
   *
   * @param options The plot data options
   */
  private createHazardPlotData(
    options: CreateHazardPlotDataOptions,
  ): Partial<PlotData>[] {
    const imt = options.form.imt;
    const hazardCurves = this.getHazardResponse(
      options.serviceResponse,
      imt,
    ).data.filter(data => options.hazardCurveFilter(data));

    const modelInfo = this.mathService.getModelInfo(
      options.serviceResponse.model,
      options.availableModels,
    );

    const colors = plotUtils.COLORWAY;
    let count = 0;

    return hazardCurves.map(data => {
      const color = colors[count++ % colors.length];

      const xy = hazardUtils.updateXySequence(
        options.form,
        hazardUtils.cleanXySequence(data.values),
        imt,
      );

      const plotlyData: Partial<PlotData> = {
        hovertemplate: '%{x} g, %{y} AFE',
        line: {
          color: options.resetColor ? color : undefined,
          dash: options.dash,
        },
        mode: 'lines+markers',
        name: options.name(modelInfo, data),
        x: xy.xs,
        y: xy.ys,
      };

      return plotlyData;
    });
  }

  /**
   * Returns the plot data for the return period.
   *
   * @param returnPeriod Retrun period
   * @param xLimits The X limits
   */
  private createReturnPeriodPlotData(
    returnPeriod: number,
    xLimits: number[],
  ): Partial<PlotData> {
    return {
      hoverinfo: 'none',
      line: {
        color: 'black',
      },
      mode: 'lines',
      name: `${returnPeriod} yr.`,
      x: xLimits,
      y: [1 / returnPeriod, 1 / returnPeriod],
    };
  }

  /**
   * Returns the Y values associated with the common X values.
   *
   * @param xValues The common X values
   * @param xySequence The sequence
   */
  private filterYValues(xValues: number[], xySequence: XySequence) {
    const ys = xySequence.xs
      .map((x, i) => [x, xySequence.ys[i]])
      .filter(([x]) => xValues.includes(x))
      .map(([, y]) => y);

    return ys;
  }

  /**
   * Returns the hazard response associated with a IMT.
   *
   * @param serviceResponse The service response for a single model
   * @param imt The control panel IMT value
   */
  private getHazardResponse(
    serviceResponse: ServiceResponse,
    imt: Imt,
  ): HazardResponse {
    const hazardResponse =
      serviceResponse.hazardResponse.response.hazardCurves.find(
        curves => curves.imt.value === imt.toString(),
      );

    if (hazardResponse === undefined) {
      throw new Error(`Service response does not contain the IMT ${imt}`);
    }

    return hazardResponse;
  }

  /**
   * Returns the X min and max value for a service reponse.
   *
   * @param serviceResponse The service response
   * @param imt The imt value
   * @param hazardCurveFilter Function to filter data
   */
  private hazardCurveLimits(
    serviceResponse: ServiceResponse,
    imt: Imt,
    hazardCurveFilter: (data: HazardCurve) => boolean,
  ): NumberBounds {
    const hazardCurves = this.getHazardResponse(
      serviceResponse,
      imt,
    ).data.filter(data => hazardCurveFilter(data));

    const data = hazardCurves.map(curve => curve.values);

    const min = Math.min(...data.map(xy => Math.min(...xy.xs)));
    const max = Math.max(...data.map(xy => Math.max(...xy.xs)));

    return {
      max,
      min,
    };
  }
}

results matching ""

    No results matching ""