This commit is contained in:
Gianluca Campanella 2019-08-14 11:25:46 +00:00
Родитель a2dd264caf
Коммит b99f5de00d
21 изменённых файлов: 756 добавлений и 1 удалений

6
.vscode/settings.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,6 @@
{
"python.formatting.provider": "black",
"python.linting.enabled": true,
"python.linting.flake8Enabled": true,
"python.linting.pylintEnabled": false,
}

9
CODE_OF_CONDUCT.md Normal file
Просмотреть файл

@ -0,0 +1,9 @@
# Microsoft Open Source Code of Conduct
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
Resources:
- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns

Просмотреть файл

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) Microsoft Corporation. All rights reserved. Copyright (c) Microsoft Corporation.
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

Просмотреть файл

@ -1 +1,9 @@
# DeepSeismic # DeepSeismic
## Contributing
This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA.
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.

6
bin/ds Normal file
Просмотреть файл

@ -0,0 +1,6 @@
#!/usr/bin/env python
from deepseismic import cli
if __name__ == "__main__":
cli.main()

3
deepseismic/__init__.py Normal file
Просмотреть файл

@ -0,0 +1,3 @@
from . import cli, forward, velocity
__all__ = ["cli", "forward", "velocity"]

Просмотреть файл

@ -0,0 +1,21 @@
from functools import partial
import click
from . import forward, velocity
click.option = partial(click.option, show_default=True)
@click.group()
@click.pass_context
def cli(ctx):
ctx.ensure_object(dict)
cli.add_command(forward.fwd)
cli.add_command(velocity.vp)
def main():
cli(obj={})

123
deepseismic/cli/forward.py Normal file
Просмотреть файл

@ -0,0 +1,123 @@
from functools import partial
import click
import h5py
import numpy as np
from ..forward import Receiver, RickerSource, TimeAxis, VelocityModel
click.option = partial(click.option, show_default=True)
@click.group()
@click.argument("input", type=click.Path())
@click.argument("output", type=click.Path())
@click.option(
"-d",
"--duration",
default=1000.0,
type=float,
help="Simulation duration (in ms)",
)
@click.option("-dt", default=2.0, type=float, help="Time increment (in ms)")
@click.option(
"--n-pml", default=10, type=int, help="PML size (in grid points)"
)
@click.option(
"--n-receivers",
default=11,
type=int,
help="Number of receivers per horizontal dimension",
)
@click.option("--space-order", default=2, type=int, help="Space order")
@click.option(
"--spacing", default=10.0, type=float, help="Spacing between grid points"
)
@click.pass_context
def fwd(
ctx,
dt: float,
duration: float,
input: str,
n_pml: int,
n_receivers: int,
output: str,
space_order: int,
spacing: float,
):
"""Forward modelling"""
if dt:
ctx.obj["dt"] = dt
ctx.obj["duration"] = duration
ctx.obj["input_file"] = h5py.File(input, mode="r")
ctx.obj["n_pml"] = n_pml
ctx.obj["n_receivers"] = n_receivers
ctx.obj["output_file"] = h5py.File(output, mode="w")
ctx.obj["space_order"] = space_order
ctx.obj["spacing"] = spacing
@fwd.command()
@click.option(
"-f0", default=0.01, type=float, help="Source peak frequency (in kHz)"
)
@click.pass_context
def ricker(ctx, f0: float):
"""Ricker source"""
input_file = ctx.obj["input_file"]
output_file = ctx.obj["output_file"]
n = sum(len(x.values()) for x in input_file.values())
with click.progressbar(length=n) as bar:
for input_group_name, input_group in input_file.items():
for dataset in input_group.values():
first_dataset = dataset
break
model = VelocityModel(
shape=first_dataset.shape,
origin=tuple(0.0 for _ in first_dataset.shape),
spacing=tuple(ctx.obj["spacing"] for _ in first_dataset.shape),
vp=first_dataset[()],
space_order=ctx.obj["space_order"],
n_pml=ctx.obj["n_pml"],
)
time_range = TimeAxis(
start=0.0, stop=ctx.obj["duration"], step=ctx.obj["dt"]
)
source = RickerSource(
name="source",
grid=model.grid,
f0=f0,
npoint=1,
time_range=time_range,
)
source.coordinates.data[0, :] = np.array(model.domain_size) * 0.5
source.coordinates.data[0, -1] = 0.0
n_receivers = ctx.obj["n_receivers"]
total_receivers = n_receivers ** (len(model.shape) - 1)
receivers = Receiver(
name="receivers",
grid=model.grid,
npoint=total_receivers,
time_range=time_range,
)
receivers_coords = np.meshgrid(
*(
np.linspace(start=0, stop=s, num=n_receivers + 2)[1:-1]
for s in model.domain_size[:-1]
)
)
for d in range(len(receivers_coords)):
receivers.coordinates.data[:, d] = receivers_coords[
d
].flatten()
receivers.coordinates.data[:, -1] = 0.0
output_group = output_file.create_group(input_group_name)
for input_dataset_name, vp in input_group.items():
model.vp = vp[()]
seismograms = model.solve(
source=source, receivers=receivers, time_range=time_range
)
output_group.create_dataset(
input_dataset_name, data=seismograms
)
bar.update(1)

Просмотреть файл

@ -0,0 +1,96 @@
from functools import partial
from itertools import islice
from typing import Tuple
import click
import h5py
from ..velocity import RoethTarantolaGenerator
click.option = partial(click.option, show_default=True)
@click.group()
@click.argument("output", type=click.Path())
@click.option(
"--append/--no-append",
default=False,
help="Whether to append to output file",
)
@click.option("-n", default=1, type=int, help="Number of simulations")
@click.option(
"-nx",
default=100,
type=int,
help="Number of grid points along the first dimension",
)
@click.option(
"-ny",
default=100,
type=int,
help="Number of grid points along the second dimension",
)
@click.option(
"-nz", type=int, help="Number of grid points along the third dimension"
)
@click.option("-s", "--seed", default=42, type=int, help="Random seed")
@click.pass_context
def vp(
ctx,
append: bool,
n: int,
nx: int,
ny: int,
nz: int,
output: str,
seed: int,
):
"""Vp simulation"""
shape = (nx, ny)
if nz is not None:
shape += (nz,)
output_file = h5py.File(output, mode=("a" if append else "w"))
output_group = output_file.create_group(
str(max((int(x) for x in output_file.keys()), default=-1) + 1)
)
ctx.obj["n"] = n
ctx.obj["output_file"] = output_file
ctx.obj["output_group"] = output_group
ctx.obj["seed"] = seed
ctx.obj["shape"] = shape
@vp.command()
@click.option("--n-layers", default=8, type=int, help="Number of layers")
@click.option(
"--initial-vp",
default=(1350.0, 1650.0),
type=(float, float),
help="Initial Vp (in km/s)",
)
@click.option(
"--vp-perturbation",
default=(-190.0, 570.0),
type=(float, float),
help="Per-layer Vp perturbation (in km/s)",
)
@click.pass_context
def rt(
ctx,
initial_vp: Tuple[float, float],
n_layers: int,
vp_perturbation: Tuple[float, float],
):
"""Röth-Tarantola model"""
model = RoethTarantolaGenerator(
shape=ctx.obj["shape"],
seed=ctx.obj["seed"],
n_layers=n_layers,
initial_vp=initial_vp,
vp_perturbation=vp_perturbation,
)
group = ctx.obj["output_group"]
with click.progressbar(length=ctx.obj["n"]) as bar:
for i, data in enumerate(islice(model.generate_many(), ctx.obj["n"])):
group.create_dataset(str(i), data=data, compression="gzip")
bar.update(1)

Просмотреть файл

@ -0,0 +1,14 @@
from .models import Model, VelocityModel
from .sources import Receiver, RickerSource, WaveletSource
from .time import TimeAxis
from .types import Kernel
__all__ = [
"Kernel",
"Model",
"Receiver",
"RickerSource",
"TimeAxis",
"VelocityModel",
"WaveletSource",
]

Просмотреть файл

@ -0,0 +1,162 @@
from typing import Optional, Tuple, Union
import numpy as np
from devito import (
Constant,
Eq,
Function,
Grid,
Operator,
SubDomain,
TimeFunction,
logger,
solve,
)
from .sources import PointSource
from .subdomains import PhysicalDomain
from .time import TimeAxis
from .types import Kernel
logger.set_log_level("WARNING")
class Model(object):
def __init__(
self,
shape: Tuple[int, ...],
origin: Tuple[float, ...],
spacing: Tuple[float, ...],
n_pml: Optional[int] = 0,
dtype: Optional[type] = np.float32,
subdomains: Optional[Tuple[SubDomain]] = (),
):
shape = tuple(int(x) for x in shape)
origin = tuple(dtype(x) for x in origin)
n_pml = int(n_pml)
subdomains = tuple(subdomains) + (PhysicalDomain(n_pml),)
shape_pml = tuple(x + 2 * n_pml for x in shape)
extent_pml = tuple(s * (d - 1) for s, d in zip(spacing, shape_pml))
origin_pml = tuple(
dtype(o - s * n_pml) for o, s in zip(origin, spacing)
)
self.grid = Grid(
shape=shape_pml,
extent=extent_pml,
origin=origin_pml,
dtype=dtype,
subdomains=subdomains,
)
self.n_pml = n_pml
self.pml = Function(name="pml", grid=self.grid)
pml_data = np.pad(
np.zeros(shape, dtype=dtype),
[(n_pml,) * 2 for _ in range(self.pml.ndim)],
mode="edge",
)
pml_coef = 1.5 * np.log(1000.0) / 40.0
for d in range(self.pml.ndim):
for i in range(n_pml):
pos = np.abs((n_pml - i + 1) / n_pml)
val = pml_coef * (pos - np.sin(2 * np.pi * pos) / (2 * np.pi))
idx = [slice(0, x) for x in pml_data.shape]
idx[d] = slice(i, i + 1)
pml_data[tuple(idx)] += val / self.grid.spacing[d]
idx[d] = slice(
pml_data.shape[d] - i, pml_data.shape[d] - i + 1
)
pml_data[tuple(idx)] += val / self.grid.spacing[d]
pml_data = np.pad(
pml_data,
[(i.left, i.right) for i in self.pml._size_halo],
mode="edge",
)
self.pml.data_with_halo[:] = pml_data
self.shape = shape
@property
def domain_size(self) -> Tuple[float, ...]:
return tuple((d - 1) * s for d, s in zip(self.shape, self.spacing))
@property
def dtype(self) -> type:
return self.grid.dtype
@property
def spacing(self):
return self.grid.spacing
@property
def spacing_map(self):
return self.grid.spacing_map
@property
def time_spacing(self):
return self.grid.stepping_dim.spacing
class VelocityModel(Model):
def __init__(
self,
shape: Tuple[int, ...],
origin: Tuple[float, ...],
spacing: Tuple[float, ...],
vp: Union[float, np.ndarray],
space_order: Optional[int] = None,
n_pml: Optional[int] = 0,
dtype: Optional[type] = np.float32,
subdomains: Optional[Tuple[SubDomain]] = (),
):
super().__init__(shape, origin, spacing, n_pml, dtype, subdomains)
if isinstance(vp, np.ndarray):
assert space_order is not None
self.m = Function(
name="m", grid=self.grid, space_order=int(space_order)
)
else:
self.m = Constant(name="m", value=1.0 / float(vp) ** 2.0)
self.vp = vp
@property
def vp(self) -> Union[float, np.ndarray]:
return self._vp
@vp.setter
def vp(self, vp: Union[float, np.ndarray]) -> None:
self._vp = vp
if isinstance(vp, np.ndarray):
pad_widths = [
(self.n_pml + i.left, self.n_pml + i.right)
for i in self.m._size_halo
]
self.m.data_with_halo[:] = np.pad(
1.0 / self.vp ** 2.0, pad_widths, mode="edge"
)
else:
self.m.data = 1.0 / float(vp) ** 2.0
def solve(
self,
source: PointSource,
receivers: PointSource,
time_range: TimeAxis,
space_order: Optional[int] = 4,
kernel: Optional[Kernel] = Kernel.OT2,
) -> np.ndarray:
assert isinstance(kernel, Kernel)
u = TimeFunction(
name="u", grid=self.grid, time_order=2, space_order=space_order
)
H = u.laplace
if kernel is Kernel.OT4:
H += self.time_spacing ** 2 / 12 * u.laplace2(1 / self.m)
eq = Eq(
u.forward, solve(self.m * u.dt2 - H + self.pml * u.dt, u.forward)
)
src_term = source.inject(
field=u.forward, expr=source * self.time_spacing ** 2 / self.m
)
rec_term = receivers.interpolate(expr=u)
op = Operator([eq] + src_term + rec_term, subs=self.spacing_map)
op(time=time_range.num - 1, dt=time_range.step)
return receivers.data

Просмотреть файл

@ -0,0 +1,132 @@
from typing import Optional
import numpy as np
import sympy
from devito.types import Dimension, SparseTimeFunction
from devito.types.basic import _SymbolCache
from scipy import interpolate
from .time import TimeAxis
class PointSource(SparseTimeFunction):
def __new__(cls, *args, **kwargs):
if cls in _SymbolCache:
options = kwargs.get("options", {})
obj = sympy.Function.__new__(cls, *args, **options)
obj._cached_init()
return obj
name = kwargs.pop("name")
grid = kwargs.pop("grid")
time_range = kwargs.pop("time_range")
time_order = kwargs.pop("time_order", 2)
p_dim = kwargs.pop("dimension", Dimension(name="p_%s" % name))
npoint = kwargs.pop("npoint", None)
coordinates = kwargs.pop(
"coordinates", kwargs.pop("coordinates_data", None)
)
if npoint is None:
assert (
coordinates is not None
), "Either `npoint` or `coordinates` must be provided"
npoint = coordinates.shape[0]
obj = SparseTimeFunction.__new__(
cls,
name=name,
grid=grid,
dimensions=(grid.time_dim, p_dim),
npoint=npoint,
nt=time_range.num,
time_order=time_order,
coordinates=coordinates,
**kwargs
)
obj._time_range = time_range
data = kwargs.get("data")
if data is not None:
obj.data[:] = data
return obj
@property
def time_range(self) -> TimeAxis:
return self._time_range
@property
def time_values(self) -> np.ndarray:
return self._time_range.time_values
def resample(
self,
dt: Optional[float] = None,
num: Optional[int] = None,
rtol: Optional[float] = 1.0e-5,
order: Optional[int] = 3,
):
assert (dt is not None) ^ (
num is not None
), "Exactly one of `dt` or `num` must be provided"
start = self._time_range.start
stop = self._time_range.stop
dt0 = self._time_range.step
if dt is not None:
new_time_range = TimeAxis(start=start, stop=stop, step=dt)
else:
new_time_range = TimeAxis(start=start, stop=stop, num=num)
dt = new_time_range.step
if np.isclose(dt0, dt, rtol=rtol):
return self
n_traces = self.data.shape[1]
new_traces = np.zeros(
(new_time_range.num, n_traces), dtype=self.data.dtype
)
for j in range(n_traces):
tck = interpolate.splrep(
self._time_range.time_values, self.data[:, j], k=order
)
new_traces[:, j] = interpolate.splev(
new_time_range.time_values, tck
)
return PointSource(
name=self.name,
grid=self.grid,
time_range=new_time_range,
coordinates=self.coordinates.data,
data=new_traces,
)
_pickle_kwargs = SparseTimeFunction._pickle_kwargs + ["time_range"]
_pickle_kwargs.remove("nt") # Inferred from time_range
class Receiver(PointSource):
pass
class WaveletSource(PointSource):
def __new__(cls, *args, **kwargs):
if cls in _SymbolCache:
options = kwargs.get("options", {})
obj = sympy.Function.__new__(cls, *args, **options)
obj._cached_init()
return obj
npoint = kwargs.pop("npoint", 1)
obj = PointSource.__new__(cls, npoint=npoint, **kwargs)
obj.f0 = kwargs.get("f0")
for p in range(npoint):
obj.data[:, p] = obj.wavelet(obj.f0, obj.time_values)
return obj
def __init__(self, *args, **kwargs):
if not self._cached():
super(WaveletSource, self).__init__(*args, **kwargs)
def wavelet(self, f0: float, t: np.ndarray) -> np.ndarray:
raise NotImplementedError
_pickle_kwargs = PointSource._pickle_kwargs + ["f0"]
class RickerSource(WaveletSource):
def wavelet(self, f0: float, t: np.ndarray) -> np.ndarray:
r = np.pi * f0 * (t - 1.0 / f0)
return (1.0 - 2.0 * r ** 2.0) * np.exp(-r ** 2.0)

Просмотреть файл

@ -0,0 +1,16 @@
from typing import Dict, Iterable, Tuple
from devito import Dimension, SubDomain
class PhysicalDomain(SubDomain):
name = "physical_domain"
def __init__(self, n_pml: int):
super().__init__()
self.n_pml = n_pml
def define(
self, dimensions: Iterable[Dimension]
) -> Dict[Dimension, Tuple[str, int, int]]:
return {d: ("middle", self.n_pml, self.n_pml) for d in dimensions}

Просмотреть файл

@ -0,0 +1,34 @@
from typing import Optional
import numpy as np
class TimeAxis(object):
def __init__(
self,
start: Optional[float] = None,
stop: Optional[float] = None,
num: Optional[int] = None,
step: Optional[float] = None,
dtype: Optional[type] = np.float32,
):
if start is None:
start = step * (1 - num) + stop
elif stop is None:
stop = step * (num - 1) + start
elif num is None:
num = int(np.ceil((stop - start + step) / step))
stop = step * (num - 1) + start
elif step is None:
step = (stop - start) / (num - 1)
else:
raise ValueError
self.start = start
self.stop = stop
self.num = num
self.step = step
self.dtype = dtype
@property
def time_values(self) -> np.ndarray:
return np.linspace(self.start, self.stop, self.num, dtype=self.dtype)

Просмотреть файл

@ -0,0 +1,6 @@
from enum import Enum, auto
class Kernel(Enum):
OT2 = auto()
OT4 = auto()

Просмотреть файл

@ -0,0 +1,4 @@
from .generator import Generator
from .roeth_tarantola import RoethTarantolaGenerator
__all__ = ["Generator", "RoethTarantolaGenerator"]

Просмотреть файл

@ -0,0 +1,22 @@
from typing import Optional, Tuple
import numpy as np
class Generator(object):
def __init__(
self,
shape: Tuple[int, ...],
dtype: Optional[type] = np.float32,
seed: Optional[int] = None,
):
self.shape = shape
self.dtype = dtype
self._prng = np.random.RandomState(seed)
def generate(self) -> np.ndarray:
raise NotImplementedError
def generate_many(self) -> np.ndarray:
while True:
yield self.generate()

Просмотреть файл

@ -0,0 +1,41 @@
from typing import Optional, Tuple
import numpy as np
from .generator import Generator
class RoethTarantolaGenerator(Generator):
def __init__(
self,
shape: Tuple[int, ...],
dtype: Optional[type] = np.float32,
seed: Optional[int] = None,
depth_dim: Optional[int] = -1,
n_layers: Optional[int] = 8,
initial_vp: Optional[Tuple[float, float]] = (1.35, 1.65),
vp_perturbation: Optional[Tuple[float, float]] = (-0.19, 0.57),
):
super().__init__(shape, dtype, seed)
self.depth_dim = depth_dim
self.n_layers = n_layers
self.initial_vp = initial_vp
self.vp_perturbation = vp_perturbation
def generate(self) -> np.ndarray:
vp = np.zeros(self.shape, dtype=self.dtype)
dim = self.depth_dim
layer_idx = np.round(
np.linspace(0, self.shape[dim], self.n_layers + 1)
).astype(np.int)
vp_idx = [slice(0, x) for x in vp.shape]
layer_vp = None
for i in range(self.n_layers):
vp_idx[dim] = slice(layer_idx[i], layer_idx[i + 1])
layer_vp = (
self._prng.uniform(*self.initial_vp)
if layer_vp is None
else layer_vp + self._prng.uniform(*self.vp_perturbation)
)
vp[tuple(vp_idx)] = layer_vp
return vp

2
pyproject.toml Normal file
Просмотреть файл

@ -0,0 +1,2 @@
[tool.black]
line-length = 79

2
setup.cfg Normal file
Просмотреть файл

@ -0,0 +1,2 @@
[aliases]
test=pytest

48
setup.py Normal file
Просмотреть файл

@ -0,0 +1,48 @@
import setuptools
with open("README.md", "r") as f:
long_description = f.read()
setuptools.setup(
author="DeepSeismic Maintainers",
author_email="deepseismic@microsoft.com",
classifiers=[
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Topic :: Scientific/Engineering",
"Topic :: Software Development",
],
dependency_links=[
"https://github.com/opesci/devito/archive/v3.5.tar.gz#egg=devito-3.5"
],
description="DeepSeismic",
install_requires=[
"click==7.0",
"devito==3.5",
"h5py==2.9.0",
"numpy==1.17.0",
"scipy==1.3.0",
"sympy==1.4",
],
license="MIT",
long_description=long_description,
long_description_content_type="text/markdown",
name="deepseismic",
packages=setuptools.find_packages(
include=["deepseismic", "deepseismic.*"]
),
platforms="any",
python_requires=">= 3.5",
scripts=["bin/ds"],
setup_requires=["pytest-runner"],
tests_require=["pytest"],
url="https://github.com/microsoft/deepseismic",
version="0.1.0",
zip_safe=False,
)