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

Issue 0133 create new spyro version were every dependency not pip installable is optional #137

Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
827 changes: 159 additions & 668 deletions LICENSE

Large diffs are not rendered by default.

15 changes: 8 additions & 7 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@

setup(
name="spyro",
version="0.2.0",
license="GPL v3",
description="acoustic wave modeling with the finite element method",
author="Keith J. Roberts, Alexandre F. G. Olender, Lucas Franceschini",
url="https://github.com/krober10nd/spyro",
version="0.9.0",
license="LGPL v3",
description="Wave modeling with the finite element method",
author="Keith J. Roberts, Alexandre F. G. Olender, Eduardo Moscatelli de Souza, Daiane I. Dolci, Thiago Dias dos Santos, Lucas Franceschini",
url="https://github.com/NDF-Poli-USP/spyro",
packages=find_packages(),
install_requires=[
"firedrake",
"numpy",
"scipy",
"matplotlib",
"exdown==0.7.0",
"segyio"],
"segyio",
"meshio"],
)
14 changes: 11 additions & 3 deletions spyro/meshing/meshing_functions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import firedrake as fire
import SeismicMesh
import meshio

try:
import SeismicMesh
except ImportError:
SeismicMesh = None

Check warning on line 7 in spyro/meshing/meshing_functions.py

View check run for this annotation

Codecov / codecov/patch

spyro/meshing/meshing_functions.py#L6-L7

Added lines #L6 - L7 were not covered by tests


def cells_per_wavelength(method, degree, dimension):
cell_per_wavelength_dictionary = {
Expand All @@ -13,9 +17,9 @@
'mlt3tet': 3.72,
}

if dimension == 2 and (method == 'KMV' or method == 'CG'):
if dimension == 2 and (method == 'MLT' or method == 'CG'):
cell_type = 'tri'
if dimension == 3 and (method == 'KMV' or method == 'CG'):
if dimension == 3 and (method == 'MLT' or method == 'CG'):
cell_type = 'tet'

key = method.lower()+str(degree)+cell_type
Expand Down Expand Up @@ -221,6 +225,8 @@
-------
None
"""
if SeismicMesh is None:
raise ImportError("SeismicMesh is not available. Please install it to use this function.")

Check warning on line 229 in spyro/meshing/meshing_functions.py

View check run for this annotation

Codecov / codecov/patch

spyro/meshing/meshing_functions.py#L229

Added line #L229 was not covered by tests
if cpw is not None:
self.cpw = cpw
if velocity_model is not None:
Expand Down Expand Up @@ -258,6 +264,8 @@
if self.mesh_type == "firedrake_mesh":
return self.create_firedrake_mesh()
elif self.mesh_type == "SeismicMesh":
if SeismicMesh is None:
raise ImportError("SeismicMesh is not available. Please install it to use this function.")

Check warning on line 268 in spyro/meshing/meshing_functions.py

View check run for this annotation

Codecov / codecov/patch

spyro/meshing/meshing_functions.py#L268

Added line #L268 was not covered by tests
return self.create_seismicmesh_mesh()
else:
raise ValueError("mesh_type is not supported")
Expand Down
9 changes: 8 additions & 1 deletion spyro/solvers/acoustic_wave.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import firedrake as fire
import warnings
import os
from SeismicMesh import write_velocity_model

from .wave import Wave

Expand All @@ -20,6 +19,12 @@
from ..utils.typing import override
from .functionals import acoustic_energy

try:
from SeismicMesh import write_velocity_model
SEISMIC_MESH_AVAILABLE = True
except ImportError:
SEISMIC_MESH_AVAILABLE = False

Check warning on line 26 in spyro/solvers/acoustic_wave.py

View check run for this annotation

Codecov / codecov/patch

spyro/solvers/acoustic_wave.py#L25-L26

Added lines #L25 - L26 were not covered by tests


class AcousticWave(Wave):
def __init__(self, dictionary, comm=None):
Expand Down Expand Up @@ -109,6 +114,8 @@
raise ValueError("No velocity model or velocity file to load.")

if self.initial_velocity_model_file.endswith(".segy"):
if not SEISMIC_MESH_AVAILABLE:
raise ImportError("SeismicMesh is required to convert segy files.")

Check warning on line 118 in spyro/solvers/acoustic_wave.py

View check run for this annotation

Codecov / codecov/patch

spyro/solvers/acoustic_wave.py#L117-L118

Added lines #L117 - L118 were not covered by tests
vp_filename, vp_filetype = os.path.splitext(
self.initial_velocity_model_file
)
Expand Down
1 change: 1 addition & 0 deletions spyro/tools/velocity_smoother.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,6 @@
plt.ylabel("z-direction (m)")
ax.axis("equal")
plt.show()
plt.savefig(output_filename+".png")

Check warning on line 74 in spyro/tools/velocity_smoother.py

View check run for this annotation

Codecov / codecov/patch

spyro/tools/velocity_smoother.py#L74

Added line #L74 was not covered by tests
SouzaEM marked this conversation as resolved.
Show resolved Hide resolved

return None
18 changes: 16 additions & 2 deletions test/test_cpw_calc.py
Copy link
Collaborator

Choose a reason for hiding this comment

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

This test will run MLT or spectral methods depending on the environment. In either case, some part of the code will not be tested. I think the test environment should have SeismicMesh installed and run the test for every method that is supported by spyro.

We may also have another environment or mock approach for testing the logic of SeismicMesh availability. However, I guess this is not the scope of this test in particular.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You are absolutely right about the importance of running the test for both types of 2D elements, especially to ensure comprehensive coverage. The reason I initially tested only one of these was that this particular test takes a significant amount of time to complete in our runner. However, I agree that we should incorporate both and fixed this in the latest commit.

As for testing the logic of SeismicMesh availability, I did attempt to implement something using the unittest package and mocks, but I encountered some unexpected issues that I couldn't fully resolve at the time. I plan to revisit this. If you have any suggestions or best practices for handling this kind of dependency testing (e.g., using mocks or creating a lightweight testing environment), I would greatly appreciate your input

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,21 @@
import spyro


def is_seismicmesh_installed():
try:
import SeismicMesh # noqa: F401
return True
except ImportError:
return False


def test_cpw_calc():
if is_seismicmesh_installed():
FEM_method_to_evaluate = "mass_lumped_triangle"
correct_cpw = 2.3
else:
FEM_method_to_evaluate = "spectral_quadrilateral"
correct_cpw = 2.5
grid_point_calculator_parameters = {
# Experiment parameters
# Here we define the frequency of the Ricker wavelet source
Expand All @@ -17,7 +31,7 @@ def test_cpw_calc():
"velocity_model_file_name": None,
# FEM to evaluate such as `KMV` or `spectral`
# (GLL nodes on quads and hexas)
"FEM_method_to_evaluate": "mass_lumped_triangle",
"FEM_method_to_evaluate": FEM_method_to_evaluate,
"dimension": 2, # Domain dimension. Either 2 or 3.
# Either near or line. Near defines a receiver grid near to the source,
"receiver_setup": "near",
Expand Down Expand Up @@ -61,7 +75,7 @@ def test_cpw_calc():
# Check if cpw is within error TOL, starting search at min
min = Cpw_calc.find_minimum()
print(f"Minimum of {min}")
test3 = np.isclose(2.3, min)
test3 = np.isclose(correct_cpw, min)

print("END")
assert all([test1, test2, test3])
Expand Down
17 changes: 17 additions & 0 deletions test/test_elastic_local_abc.py
Copy link
Collaborator

Choose a reason for hiding this comment

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

In this case we can try reducing the memory requirements of the test instead of skipping it. For example, we can try increasing "gradient_sampling_frequency". I left a low (and probably inappropriate) value for this field.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is a good idea. Once the demeter runner finishes the current jobs, I will SSH into it and try out some different sampling values to see if I can reduce memory requirements without compromising the test.

I am working from home because of the holiday. Therefore, I don't want to risk loosing SSH access to my personal workstation in the lab (that only has 16 GB RAM) by accident while testing this. Demeter has significant more RAM (48 GB) and can run this test as is without a problem.

Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,43 @@
expected_mechanical_energy = 0.25


def has_sufficient_memory():
meminfo = {}
with open('/proc/meminfo') as f:
for line in f:
parts = line.split(':')
if len(parts) == 2:
meminfo[parts[0].strip()] = parts[1].strip()
total_memory_kb = int(meminfo.get('MemTotal', '0 kB').split()[0])
total_memory_gb = total_memory_kb / 1024 / 1024
print(f"Total system memory {total_memory_gb}")
return total_memory_gb > 16


@pytest.mark.skipif(not has_sufficient_memory(), reason="Insufficient memory")
def test_stacey_abc():
wave = build_solver("Stacey", "backward")
wave.forward_solve()
last_mechanical_energy = wave.field_logger.get("mechanical_energy")
assert last_mechanical_energy < expected_mechanical_energy


@pytest.mark.skipif(not has_sufficient_memory(), reason="Insufficient memory")
def test_clayton_engquist_abc():
wave = build_solver("CE_A1", "backward")
wave.forward_solve()
last_mechanical_energy = wave.field_logger.get("mechanical_energy")
assert last_mechanical_energy < expected_mechanical_energy


@pytest.mark.skipif(not has_sufficient_memory(), reason="Insufficient memory")
def test_with_central():
wave = build_solver("Stacey", "central")
with pytest.raises(AssertionError):
wave.forward_solve()


@pytest.mark.skipif(not has_sufficient_memory(), reason="Insufficient memory")
def test_with_backward_2nd():
wave = build_solver("Stacey", "backward_2nd")
wave.forward_solve()
Expand Down
16 changes: 16 additions & 0 deletions test/test_meshing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from spyro.meshing.meshing_functions import cells_per_wavelength
import numpy as np


def test_cpw_for_acoustic():
method = 'MLT'
degree = 3
dimension = 2
mlt3tri = cells_per_wavelength(method, degree, dimension)
dimension = 3
mlt3tet = cells_per_wavelength(method, degree, dimension)
assert np.isclose(mlt3tri, 3.70) and np.isclose(mlt3tet, 3.72)


if __name__ == "__main__":
test_cpw_for_acoustic()
10 changes: 10 additions & 0 deletions test/test_seismicmesh_integration.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import spyro
import firedrake as fire
import numpy as np
import pytest


def is_seismicmesh_installed():
try:
import SeismicMesh # noqa: F401
return True
except ImportError:
return False


def mean_edge_length(triangle):
Expand All @@ -14,6 +23,7 @@ def mean_edge_length(triangle):
return (l0+l1+l2)/3.0


@pytest.mark.skipif(not is_seismicmesh_installed(), reason="SeismicMesh is not installed")
def test_spyro_seimicmesh_2d_homogeneous_generation():
Lz = 1.0
Lx = 2.0
Expand Down
73 changes: 73 additions & 0 deletions test/test_velocity_smoother.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import firedrake as fire
import spyro
import segyio
import numpy as np
import matplotlib.pyplot as plt


def get_vp_from_2dsegy(filename):
"""
Extracts velocity profile (vp) data from a 2D SEG-Y file.
Parameters:
filename (str): The path to the SEG-Y file.
Returns:
np.ndarray: A 2D numpy array containing the velocity profile data,
with shape (nz, nx) where nz is the number of samples
and nx is the number of traces.
"""

with segyio.open(filename, ignore_geometry=True) as f:
nz, nx = len(f.samples), len(f.trace)
show_vp = np.zeros(shape=(nz, nx))
for index, trace in enumerate(f.trace):
show_vp[:, index] = trace

return show_vp


def test_write_segy_and_smooth(show=False):
vp_name = "velocity_models/test"
segy_file = vp_name + ".segy"
smoothed_file = "smoothed_test.segy"
mesh = fire.UnitSquareMesh(50, 50)
mesh.coordinates.dat.data[:, 0] *= -1

V = fire.FunctionSpace(mesh, "CG", 3)
x, y = fire.SpatialCoordinate(mesh)
r = 0.2
xc = -0.5
yc = 0.5

vp = fire.Function(V)

c = fire.conditional((x - xc) ** 2 + (y - yc) ** 2 < r**2, 3.0, 1.5)

vp.interpolate(c)

xi, yi, zi = spyro.io.write_function_to_grid(vp, V, 10.0 / 1000.0)
spyro.io.create_segy(zi, segy_file)
original_vp = get_vp_from_2dsegy(segy_file)

if show is True:
fig, ax = plt.subplots()
plt.pcolormesh(original_vp, shading="auto")
plt.title("Non smoothed model model")
plt.colorbar(label="P-wave velocity (km/s)")
plt.xlabel("x-direction (m)")
plt.ylabel("z-direction (m)")
ax.axis("equal")
plt.show()
plt.savefig("nonsmoothedtest.png")
SouzaEM marked this conversation as resolved.
Show resolved Hide resolved

spyro.tools.smooth_velocity_field_file(segy_file, smoothed_file, 5, show=show)

smoothed_vp = get_vp_from_2dsegy(smoothed_file)
check_boundary = np.isclose(original_vp[0, 0], smoothed_vp[0, 0])
check_centre = np.isclose(original_vp[48, 48], smoothed_vp[48, 48], rtol=1e-3)
check_halfway = original_vp[0, 0]*1.1 < smoothed_vp[24, 48] < original_vp[48, 48]*0.9

assert all([check_boundary, check_halfway, check_centre])


if __name__ == "__main__":
test_write_segy_and_smooth(show=True)
Loading