Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated Realisations #6

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open

Updated Realisations #6

wants to merge 15 commits into from

Conversation

lispandfound
Copy link
Contributor

@lispandfound lispandfound commented Oct 16, 2024

Realisations

The following PR updates the realisations for the new workflow. The API for realisations has changed since the last pull request for realisations. The new workflow realisations aim to be a well-documented, modular, and self-contained method for simulation configuration. The idea is that the scientific defaults version, container version, and realisation file should completely determine the simulation output up to hardware-level floating point mathematics differences.

Note

Only the realisations.py, test_realisation.py, and schemas.py files need to be reviewed. The defaults are otherwise as they are in #5 , and are there to allow the tests to pass.

Using the Realisations Module

Reading and writing realisations involves using one of the classes to read and write realisation configurations. To retrieve the domain parameters of the realisation for example you would write something like

# For a realisation with contents
#  {
#    "domain": {
#        "resolution": 0.2,
#        "domain": [
#                  { "latitude": -44.354733203836375, "longitude": 168.32522375230945 },
#                  { "latitude": -45.860251409924196, "longitude": 170.71966358185924 },
#                  { "latitude": -42.084729432307505, "longitude": 174.9917352035785 },
#                  { "latitude": -40.66798264759381, "longitude": 172.63541186873076 }
#        ],
#        "depth": 50.0,
#        "duration": 60.0,
#        "dt": 0.01
#    }
# }
#
#
#
>>> from workflow.realisations import DomainParameters
>>> domain_parameters = DomainParameters.read_from_realisation('realisation.json')
DomainParameters(
        resolution=0.2,
        domain=BoundingBox(...)
        depth=50.0,
        duration=60.0,
        dt=0.01
)
>>> domain_parameters.write_to_realisation('realisation.json')

Realisations can load default values for some configuration sections using read_from_realisation_or_defaults

# For a realisation with contents
#  {
#    "domain": {
#        "resolution": 0.2,
#        "domain": [
#                  { "latitude": -44.354733203836375, "longitude": 168.32522375230945 },
#                  { "latitude": -45.860251409924196, "longitude": 170.71966358185924 },
#                  { "latitude": -42.084729432307505, "longitude": 174.9917352035785 },
#                  { "latitude": -40.66798264759381, "longitude": 172.63541186873076 }
#        ],
#        "depth": 50.0,
#        "duration": 60.0,
#        "dt": 0.01
#    }
# }
#
# NOTE: There is no velocity model parameters in this realisation
#
>>> from workflow.realisations import VelocityModelParameters
>>> from workflow.defaults import DefaultsVersion
>>> VelocityModelParameters.read_from_realisation_or_defaults('realisation.json', DefaultsVersion.v24_2_2_2)
VelocityModelParameters(
    min_vs=0.5,
    version='2.06',
    topo_type='SQUASHED_TAPERED',
    dt=0.01,
    ds_multiplier=1.2,
    resolution=0.2,
    vs30=500.0,
    s_wave_velocity=3500.0,
    pgv_interpolants=array([
      [3.5, 0.015],
      [4.1, 0.0375],
      [4.7, 0.075],
      [5.2, 0.15],
      [5.5, 0.25],
      [5.8, 0.4],
      [6.2, 0.7],
      [6.5, 1.0],
      [6.8, 1.35],
      [7.0, 1.65],
      [7.4, 2.1],
      [7.7, 2.5],
      [8.0, 3.0]
    ])
)
# These values were read from the 24.2.2.2 defaults yaml file.

Realisations have rudimentary value checking applied in the schemas module, which also defines the schema for each realisation section. For instance, here is the schema for the velocity model parameters.

VELOCITY_MODEL_SCHEMA = Schema(
    {
        Literal(
            "min_vs",
            description="The minimum velocity (km/s) produced in the velocity model.",
        ): And(float, is_positive),
        Literal("version", "Velocity model version"): "2.06",
        Literal("topo_type", "Velocity model topology type"): str,
        Literal("dt", "Velocity model timestep resolution"): And(float, is_positive),
        Literal("ds_multiplier", "Velocity model ds multiplier"): And(
            float, is_positive
        ),
        Literal("resolution", "Velocity model spatial resolution"): And(
            float, is_positive
        ),
        Literal("vs30", "VS30 value"): And(float, is_positive),
        Literal("s_wave_velocity", "S-wave velocity"): And(float, is_positive),
        Literal('pgv_interpolants', 'PGV interpolants to estimate domain size'): And([[And(float, is_positive)]], Use(np.array))
    }
)

Adding a new realisation configuration is done by inheriting from the RealisationConfiguration class and then defining the key in the json file, the schema to validate against, and the values to store. Overriding the to_dict method lets you change the JSON serialisation behaviour. Here is an example of that for the velocity model parameters.

@dataclasses.dataclass
class VelocityModelParameters(RealisationConfiguration):
    """Parameters defining the velocity model."""

    _config_key: ClassVar[str] = "velocity_model"
    _schema: ClassVar[Schema] = schemas.VELOCITY_MODEL_SCHEMA

    min_vs: float
    """The minimum velocity in the velocity model."""
    version: str
    """The velocity model version."""
    topo_type: str
    """The topology type of the velocity model."""
    dt: float
    """The velocity model time resolution."""
    ds_multiplier: float
    """The ds multiplier used to adjust simulation duration."""
    resolution: float
    """The resolution of the velocity model (in kilometres)."""
    vs30: float
    """The reference vs30 value for duration estimation."""
    s_wave_velocity: float
    """The s-wave velocity."""
    pgv_interpolants: npt.NDArray[np.float32]
    """PGV interpolation nodes between rupture magnitude and PGV target values."""

    def to_dict(self) -> dict:
        """
        Convert the object to a dictionary representation.

        Returns
        -------
        dict
            Dictionary representation of the object.
        """
        _dict = dataclasses.asdict(self)
        _dict["pgv_interpolants"] = _dict["pgv_interpolants"].tolist()
        return _dict

Some values for the realisation configuration are computed simply using values stored in the file. These values are now computed using Python properties rather than being stored in the realisation. For example the nx, ny, and nz values for the domain parameters are properties computed from the domain.

@dataclasses.dataclass
class DomainParameters(RealisationConfiguration):
    """Parameters defining the spatial and temporal domain for simulation."""

    _config_key: ClassVar[str] = "domain"
    _schema: ClassVar[Schema] = schemas.DOMAIN_SCHEMA

    resolution: float
    """The simulation resoultion in kilometres."""
    domain: BoundingBox
    """The bounding box for the domain."""
    depth: float
    """The depth of the domain (in metres)."""
    duration: float
    """The simulation duration (in seconds)."""
    dt: float
    """The resolution of the domain in time (in seconds)."""

    @property
    def nx(self) -> int:
        """int: The number of x coordinate positions in the discretised domain."""
        return int(np.round(self.domain.extent_x / self.resolution))

    @property
    def ny(self) -> int:
        """int: The number of y coordinate positions in the discretised domain."""
        return int(np.round(self.domain.extent_y / self.resolution))

    @property
    def nz(self) -> int:
        """int: The number of z coordinate positions in the discretised domain."""
        return int(np.round(self.depth / self.resolution))

    def to_dict(self) -> dict:
        """
        Convert the object to a dictionary representation.

        Returns
        -------
        dict
            Dictionary representation of the object.
        """
        param_dict = dataclasses.asdict(self)
        param_dict["domain"] = to_name_coordinate_dictionary(
            self.domain.corners,
        )
        return param_dict

Having these as properties means it is impossible to have the nx, ny and nz values be inconsistent with the domain.

Testing

Inline with the discussions on testing earlier in the year, there is extensive testing for every line of the realisation and schema module. The tests are located in the test_realisation.py file. The tests ensure:

  1. Every realisation section is read and written in the correct way from the JSON file.
  2. Every defined defaults version is readable.

sungeunbae
sungeunbae previously approved these changes Oct 16, 2024
Copy link

@joelridden joelridden left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few comments, deptry issue is interesting

workflow/realisations.py Show resolved Hide resolved
workflow/realisations.py Outdated Show resolved Hide resolved
workflow/schemas.py Outdated Show resolved Hide resolved
tests/test_realisation.py Show resolved Hide resolved
tests/test_realisation.py Outdated Show resolved Hide resolved
@lispandfound lispandfound dismissed sungeunbae’s stale review October 21, 2024 21:43

The merge-base changed after approval.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants