Skip to content

Commit

Permalink
refactor(mp7particledata): match mp7 order in to_coords()/to_prp() (#…
Browse files Browse the repository at this point in the history
…2172)

* return the same particle order from to_coords() and to_prp() as generated by MP7 — preserves particle indexing for MF6 PRT, making it easier to compare results
* refactoring/cleanup, factor out helper methods get_cell_release_points(), get_face_release_points()
* defer materialization — return iterators not lists so the consumer can control memory use, previously done for to_coords() and to_prp() but not get_release_points(), might be relevant for very large release configurations
* test-drive a pytest plugin for previously hardcoded snapshot tests, introduce some fixtures in autotest/conftest.py for nicer array snapshots than the default format, maybe these could live in devtools if they see wider use?
  • Loading branch information
wpbonelli authored May 1, 2024
1 parent 50666a5 commit c7af787
Show file tree
Hide file tree
Showing 9 changed files with 430 additions and 227 deletions.
11 changes: 11 additions & 0 deletions DEVELOPER.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ This document describes how to set up a FloPy development environment, run the e
- [Performance testing](#performance-testing)
- [Benchmarking](#benchmarking)
- [Profiling](#profiling)
- [Snapshot testing](#snapshot-testing)
- [Branching model](#branching-model)
- [Deprecation policy](#deprecation-policy)
- [Miscellaneous](#miscellaneous)
Expand Down Expand Up @@ -345,6 +346,16 @@ Profiling is [distinct](https://stackoverflow.com/a/39381805/6514033) from bench

By default, `pytest-benchmark` will only print profiling results to `stdout`. If the `--benchmark-autosave` flag is provided, performance profile data will be included in the JSON files written to the `.benchmarks` save directory as described in the benchmarking section above.

### Snapshot testing

Snapshot testing is a form of regression testing in which a "snapshot" of the results of some computation is verified and captured by the developer to be compared against when tests are subsequently run. This is accomplished with [`syrupy`](https://github.com/tophat/syrupy), which provides a `snapshot` fixture overriding the equality operator to allow comparison with e.g. `snapshot == result`. A few custom fixtures for snapshots of NumPy arrays are provided in `autotest/conftest.py`:

- `array_snapshot`: saves an array in a binary file for compact storage, can be inspected programmatically with `np.load()`
- `text_array_snapshot`: flattens an array and stores it in a text file, compromise between readability and disk usage
- `readable_array_snapshot`: stores an array in a text file in its original shape, easy to inspect but largest on disk

By default, tests run in comparison mode. This means a newly written test using any of the snapshot fixtures will fail until a snapshot is created. Snapshots can be created/updated by running pytest with the `--snapshot-update` flag.

## Branching model

This project follows the [git flow](https://nvie.com/posts/a-successful-git-branching-model/): development occurs on the `develop` branch, while `master` is reserved for the state of the latest release. Development PRs are typically squashed to `develop`, to avoid merge commits. At release time, release branches are merged to `master`, and then `master` is merged back into `develop`.
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
105 changes: 104 additions & 1 deletion autotest/conftest.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import re
from importlib import metadata
from io import BytesIO, StringIO
from pathlib import Path
from platform import system
from typing import List
from typing import List, Optional

import matplotlib.pyplot as plt
import numpy as np
import pytest
from modflow_devtools.misc import is_in_ci

Expand Down Expand Up @@ -158,3 +160,104 @@ def pytest_report_header(config):
if not_found:
lines.append("optional packages not found: " + ", ".join(not_found))
return "\n".join(lines)


# snapshot classes and fixtures

from syrupy.extensions.single_file import (
SingleFileSnapshotExtension,
WriteMode,
)
from syrupy.types import (
PropertyFilter,
PropertyMatcher,
SerializableData,
SerializedData,
)


def _serialize_bytes(data):
buffer = BytesIO()
np.save(buffer, data)
return buffer.getvalue()


class BinaryArrayExtension(SingleFileSnapshotExtension):
"""
Binary snapshot of a NumPy array. Can be read back into NumPy with
.load(), preserving dtype and shape. This is the recommended array
snapshot approach if human-readability is not a necessity, as disk
space is minimized.
"""

_write_mode = WriteMode.BINARY
_file_extension = "npy"

def serialize(
self,
data,
*,
exclude=None,
include=None,
matcher=None,
):
return _serialize_bytes(data)


class TextArrayExtension(SingleFileSnapshotExtension):
"""
Text snapshot of a NumPy array. Flattens the array before writing.
Can be read back into NumPy with .loadtxt() assuming you know the
shape of the expected data and subsequently reshape it if needed.
"""

_write_mode = WriteMode.TEXT
_file_extension = "txt"

def serialize(
self,
data: "SerializableData",
*,
exclude: Optional["PropertyFilter"] = None,
include: Optional["PropertyFilter"] = None,
matcher: Optional["PropertyMatcher"] = None,
) -> "SerializedData":
buffer = StringIO()
np.savetxt(buffer, data.ravel())
return buffer.getvalue()


class ReadableArrayExtension(SingleFileSnapshotExtension):
"""
Human-readable snapshot of a NumPy array. Preserves array shape
at the expense of possible loss of precision (default 8 places)
and more difficulty loading into NumPy than TextArrayExtension.
"""

_write_mode = WriteMode.TEXT
_file_extension = "txt"

def serialize(
self,
data: "SerializableData",
*,
exclude: Optional["PropertyFilter"] = None,
include: Optional["PropertyFilter"] = None,
matcher: Optional["PropertyMatcher"] = None,
) -> "SerializedData":
return np.array2string(data, threshold=np.inf)


@pytest.fixture
def array_snapshot(snapshot):
return snapshot.use_extension(BinaryArrayExtension)


@pytest.fixture
def text_array_snapshot(snapshot):
return snapshot.use_extension(TextArrayExtension)


@pytest.fixture
def readable_array_snapshot(snapshot):
return snapshot.use_extension(ReadableArrayExtension)
Loading

0 comments on commit c7af787

Please sign in to comment.