Skip to content

Commit 6bc2f00

Browse files
committed
2 parents 4229d6a + 528c7a6 commit 6bc2f00

File tree

65 files changed

+1418
-597
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+1418
-597
lines changed

.github/workflows/release-please.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99
fail-fast: false
1010
matrix:
1111
os: ["ubuntu-latest", "windows-latest"] #"macos-latest",
12-
python-version: ["3.7","3.8","3.9","3.10"]
12+
python-version: ["3.8","3.9","3.10"]
1313
steps:
1414
- uses: actions/checkout@v2
1515
- uses: conda-incubator/setup-miniconda@v2
@@ -87,7 +87,7 @@ jobs:
8787
fail-fast: false
8888
matrix:
8989
os: ["ubuntu-latest", "windows-latest"]
90-
python-version: ["3.10","3.9","3.8","3.7"]
90+
python-version: ["3.10","3.9","3.8"]
9191
steps:
9292
- uses: conda-incubator/setup-miniconda@v2
9393
with:

LoopStructural/api/__init__.py

Whitespace-only changes.

LoopStructural/api/_interpolate.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
from ast import LShift
2+
import numpy as np
3+
import pandas as pd
4+
5+
from typing import Optional
6+
from LoopStructural.interpolators import (
7+
GeologicalInterpolator,
8+
InterpolatorFactory,
9+
)
10+
from LoopStructural.utils import BoundingBox
11+
from LoopStructural.utils import getLogger
12+
13+
logger = getLogger(__name__)
14+
15+
16+
class LoopInterpolator:
17+
def __init__(
18+
self,
19+
bounding_box: BoundingBox,
20+
dimensions: int = 3,
21+
type: str = "FDI",
22+
nelements: int = 1000,
23+
):
24+
"""Scikitlearn like interface for LoopStructural interpolators
25+
useful for quickly building an interpolator to apply to a dataset
26+
build a generic interpolation object speficying the bounding box
27+
and then fit to constraints and evaluate the interpolator
28+
29+
Parameters
30+
----------
31+
bounding_box : BoundingBox
32+
Area where the interpolation will work
33+
dimensions : int, optional
34+
number of dimensions e.g. 3d or 2d, by default 3
35+
type : str, optional
36+
type of interpolation algorithm FDI- finite difference, PLI - linear finite elements,
37+
by default "FDI"
38+
nelements : int, optional
39+
degrees of freedom for interpolator, by default 1000
40+
"""
41+
self.dimensions = dimensions
42+
self.type = "FDI"
43+
self.interpolator: GeologicalInterpolator = (
44+
InterpolatorFactory.create_interpolator(
45+
type,
46+
bounding_box,
47+
nelements,
48+
)
49+
)
50+
51+
def fit(
52+
self,
53+
values: Optional[np.ndarray] = None,
54+
tangent_vectors: Optional[np.ndarray] = None,
55+
normal_vectors: Optional[np.ndarray] = None,
56+
inequality_constraints: Optional[np.ndarray] = None,
57+
):
58+
"""_summary_
59+
60+
Parameters
61+
----------
62+
values : Optional[np.ndarray], optional
63+
Value constraints for implicit function, by default None
64+
tangent_vectors : Optional[np.ndarray], optional
65+
tangent constraints for implicit function, by default None
66+
normal_vectors : Optional[np.ndarray], optional
67+
gradient norm constraints for implicit function, by default None
68+
inequality_constraints : Optional[np.ndarray], optional
69+
_description_, by default None
70+
"""
71+
if values is not None:
72+
self.interpolator.set_value_constraints(values)
73+
if tangent_vectors is not None:
74+
self.interpolator.set_tangent_constraints(tangent_vectors)
75+
if normal_vectors is not None:
76+
self.interpolator.set_normal_constraints(normal_vectors)
77+
if inequality_constraints:
78+
pass
79+
80+
self.interpolator.setup()
81+
82+
def evaluate_scalar_value(self, locations: np.ndarray) -> np.ndarray:
83+
"""Evaluate the value of the interpolator at locations
84+
85+
Parameters
86+
----------
87+
locations : np.ndarray
88+
Nx3 array of locations to evaluate the interpolator at
89+
90+
Returns
91+
-------
92+
np.ndarray
93+
value of implicit function at locations
94+
"""
95+
self.interpolator.update()
96+
return self.interpolator.evaluate_value(locations)
97+
98+
def evaluate_gradient(self, locations: np.ndarray) -> np.ndarray:
99+
"""Evaluate the gradient of the interpolator at locations
100+
101+
Parameters
102+
----------
103+
locations : np.ndarray
104+
Nx3 locations
105+
106+
Returns
107+
-------
108+
np.ndarray
109+
Nx3 gradient of implicit function
110+
"""
111+
self.interpolator.update()
112+
return self.interpolator.evaluate_gradient(locations)
113+
114+
def fit_and_evaluate_value(
115+
self,
116+
values: Optional[np.ndarray] = None,
117+
tangent_vectors: Optional[np.ndarray] = None,
118+
normal_vectors: Optional[np.ndarray] = None,
119+
inequality_constraints: Optional[np.ndarray] = None,
120+
):
121+
# get locations
122+
self.fit(
123+
values=values,
124+
tangent_vectors=tangent_vectors,
125+
normal_vectors=normal_vectors,
126+
inequality_constraints=inequality_constraints,
127+
)
128+
locations = self.interpolator.get_data_locations()
129+
return self.evalute_scalar_value(locations)
130+
131+
def fit_and_evaluate_gradient(
132+
self,
133+
values: Optional[np.ndarray] = None,
134+
tangent_vectors: Optional[np.ndarray] = None,
135+
normal_vectors: Optional[np.ndarray] = None,
136+
inequality_constraints: Optional[np.ndarray] = None,
137+
):
138+
self.fit(
139+
values=values,
140+
tangent_vectors=tangent_vectors,
141+
normal_vectors=normal_vectors,
142+
inequality_constraints=inequality_constraints,
143+
)
144+
locations = self.interpolator.get_data_locations()
145+
return self.evaluate_gradient(locations)

LoopStructural/api/_surface.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from typing import Optional, Union
2+
import numpy as np
3+
from LoopStructural.utils import getLogger
4+
5+
logger = getLogger(__name__)
6+
try:
7+
from skimage.measure import marching_cubes
8+
except ImportError:
9+
logger.warning("Using deprecated version of scikit-image")
10+
from skimage.measure import marching_cubes_lewiner as marching_cubes
11+
12+
from LoopStructural.interpolators import GeologicalInterpolator
13+
from LoopStructural.utils import BoundingBox
14+
from LoopStructural.datatypes import Surface
15+
16+
surface_list = dict[str, tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]]
17+
18+
19+
class LoopIsosurfacer:
20+
def __init__(self, bounding_box: BoundingBox, interpolator: GeologicalInterpolator):
21+
self.bounding_box = bounding_box
22+
self.interpolator = interpolator
23+
24+
def fit(self, values: Union[list, int, float]) -> surface_list:
25+
surfaces = {}
26+
all_values = self.interpolator.evaluate_value(self.bounding_box.regular_grid())
27+
if isinstance(values, list):
28+
isovalues = values
29+
elif isinstance(values, float):
30+
isovalues = [values]
31+
elif isinstance(values, int):
32+
isovalues = np.linspace(np.min(all_values), np.max(all_values), values)
33+
for isovalue in isovalues:
34+
verts, faces, normals, values = marching_cubes(
35+
all_values.reshape(self.bounding_box.nsteps, order="C"),
36+
isovalue,
37+
spacing=self.bounding_box.step_vector,
38+
)
39+
values = np.zeros(verts.shape[0]) + isovalue
40+
surfaces[f"surface_{isovalue}"] = Surface(
41+
vertices=verts,
42+
triangles=faces,
43+
normals=normals,
44+
name=f"surface_{isovalue}",
45+
values=values,
46+
)
47+
return surfaces

LoopStructural/api/model.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
def CreateModelWithSingleScalarField():
2+
pass
3+
4+
5+
def AddFaultToModel():
6+
pass
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from ._surface import Surface
2+
from ._bounding_box import BoundingBox
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
from __future__ import annotations
2+
from typing import Optional
3+
from LoopStructural.utils.exceptions import LoopValueError
4+
import numpy as np
5+
6+
7+
class BoundingBox:
8+
def __init__(
9+
self,
10+
dimensions: int = 3,
11+
origin: Optional[np.ndarray] = None,
12+
maximum: Optional[np.ndarray] = None,
13+
nsteps: Optional[np.ndarray] = None,
14+
):
15+
self._origin = origin
16+
self._maximum = maximum
17+
self.dimensions = dimensions
18+
if nsteps is None:
19+
self.nsteps = np.array([50, 50, 25])
20+
self.name_map = {
21+
"xmin": (0, 0),
22+
"ymin": (0, 1),
23+
"zmin": (0, 2),
24+
"xmax": (1, 0),
25+
"ymax": (1, 1),
26+
"zmax": (1, 2),
27+
"lower": (0, 2),
28+
"upper": (1, 2),
29+
"minx": (0, 0),
30+
"miny": (0, 1),
31+
"minz": (0, 2),
32+
"maxx": (1, 0),
33+
"maxy": (1, 1),
34+
"maxz": (1, 2),
35+
}
36+
37+
@property
38+
def valid(self):
39+
return self._origin is not None and self._maximum is not None
40+
41+
@property
42+
def origin(self) -> np.ndarray:
43+
if self._origin is None:
44+
raise LoopValueError("Origin is not set")
45+
return self._origin
46+
47+
@origin.setter
48+
def origin(self, origin: np.ndarray):
49+
self._origin = origin
50+
51+
@property
52+
def maximum(self) -> np.ndarray:
53+
if self._maximum is None:
54+
raise LoopValueError("Maximum is not set")
55+
return self._maximum
56+
57+
@maximum.setter
58+
def maximum(self, maximum: np.ndarray):
59+
self._maximum = maximum
60+
61+
@property
62+
def nelements(self):
63+
return self.nsteps.prod()
64+
65+
@property
66+
def volume(self):
67+
return np.product(self.maximum - self.origin)
68+
69+
@property
70+
def bb(self):
71+
return np.array([self.origin, self.maximum])
72+
73+
@nelements.setter
74+
def nelements(self, nelements):
75+
box_vol = self.volume
76+
ele_vol = box_vol / nelements
77+
# calculate the step vector of a regular cube
78+
step_vector = np.zeros(3)
79+
step_vector[:] = ele_vol ** (1.0 / 3.0)
80+
# step_vector /= np.array([1,1,2])
81+
# number of steps is the length of the box / step vector
82+
nsteps = np.ceil((self.maximum - self.origin) / step_vector).astype(int)
83+
self.nsteps = nsteps
84+
85+
@property
86+
def step_vector(self):
87+
return (self.maximum - self.origin) / self.nsteps
88+
89+
def fit(self, locations: np.ndarray):
90+
if locations.shape[1] != self.dimensions:
91+
raise LoopValueError(
92+
f"locations array is {locations.shape[1]}D but bounding box is {self.dimensions}"
93+
)
94+
print("fitting")
95+
self.origin = locations.min(axis=0)
96+
self.maximum = locations.max(axis=0)
97+
return self
98+
99+
def with_buffer(self, buffer: float = 0.2) -> BoundingBox:
100+
if self.origin is None or self.maximum is None:
101+
raise LoopValueError(
102+
"Cannot create bounding box with buffer, no origin or maximum"
103+
)
104+
origin = self.origin - buffer * (self.maximum - self.origin)
105+
maximum = self.maximum + buffer * (self.maximum - self.origin)
106+
return BoundingBox(origin=origin, maximum=maximum)
107+
108+
def get_value(self, name):
109+
ix, iy = self.name_map.get(name, (-1, -1))
110+
if ix == -1 and iy == -1:
111+
raise LoopValueError(f"{name} is not a valid bounding box name")
112+
if iy == -1:
113+
return self.origin[ix]
114+
115+
return self.bb[ix,]
116+
117+
def __getitem__(self, name):
118+
if isinstance(name, str):
119+
return self.get_value(name)
120+
elif isinstance(name, tuple):
121+
return self.origin
122+
return self.get_value(name)
123+
124+
def is_inside(self, xyz):
125+
inside = np.zeros(xyz.shape[0], dtype=bool)
126+
inside = np.logical_and(inside, xyz[:, 0] > self.origin[0])
127+
inside = np.logical_and(inside, xyz[:, 0] < self.maximum[0])
128+
inside = np.logical_and(inside, xyz[:, 1] > self.origin[1])
129+
inside = np.logical_and(inside, xyz[:, 1] < self.maximum[1])
130+
inside = np.logical_and(inside, xyz[:, 2] > self.origin[2])
131+
inside = np.logical_and(inside, xyz[:, 2] < self.maximum[2])
132+
return inside
133+
134+
def regular_grid(self, nsteps=None, shuffle=False, order="C"):
135+
if nsteps is None:
136+
nsteps = self.nsteps
137+
x = np.linspace(self.origin[0], self.maximum[0], nsteps[0])
138+
y = np.linspace(self.origin[1], self.maximum[1], nsteps[1])
139+
z = np.linspace(self.origin[2], self.maximum[2], nsteps[2])
140+
xx, yy, zz = np.meshgrid(x, y, z, indexing="ij")
141+
locs = np.array(
142+
[xx.flatten(order=order), yy.flatten(order=order), zz.flatten(order=order)]
143+
).T
144+
if shuffle:
145+
# logger.info("Shuffling points")
146+
np.random.shuffle(locs)
147+
return locs

LoopStructural/datatypes/_inequality.py

Whitespace-only changes.

LoopStructural/datatypes/_normal.py

Whitespace-only changes.

LoopStructural/datatypes/_point.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# from dataclasses import dataclass
2+
3+
4+
# @dataclass
5+
# class Value:

0 commit comments

Comments
 (0)