Skip to content

Commit d2e7e95

Browse files
committed
Merge branch 'master' of github.com:Loop3D/LoopStructural
2 parents 47f10d9 + 63825b1 commit d2e7e95

File tree

115 files changed

+2221
-3941
lines changed

Some content is hidden

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

115 files changed

+2221
-3941
lines changed

LoopStructural/__init__.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,32 @@
2121
from .modelling.core.geological_model import GeologicalModel
2222
from .interpolators._api import LoopInterpolator
2323
from .datatypes import BoundingBox
24-
from .utils import log_to_console, log_to_file, getLogger, rng
24+
from .utils import log_to_console, log_to_file, getLogger, rng, get_levels
2525

2626
logger = getLogger(__name__)
2727
logger.info("Imported LoopStructural")
28+
29+
30+
def setLogging(level="info"):
31+
"""
32+
Set the logging parameters for log file
33+
34+
Parameters
35+
----------
36+
filename : string
37+
name of file or path to file
38+
level : str, optional
39+
'info', 'warning', 'error', 'debug' mapped to logging levels, by default 'info'
40+
"""
41+
import LoopStructural
42+
43+
logger = getLogger(__name__)
44+
45+
levels = get_levels()
46+
level = levels.get(level, logging.WARNING)
47+
LoopStructural.ch.setLevel(level)
48+
49+
for name in LoopStructural.loggers:
50+
logger = logging.getLogger(name)
51+
logger.setLevel(level)
52+
logger.info(f'Set logging to {level}')

LoopStructural/api/__init__.py

Lines changed: 0 additions & 2 deletions
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
from ._surface import Surface
22
from ._bounding_box import BoundingBox
3+
from ._point import ValuePoints, VectorPoints

LoopStructural/datatypes/_bounding_box.py

Lines changed: 232 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
from __future__ import annotations
2-
from typing import Optional
2+
from typing import Optional, Union
33
from LoopStructural.utils.exceptions import LoopValueError
44
from LoopStructural.utils import rng
55
import numpy as np
66

7+
from LoopStructural.utils.logging import getLogger
8+
9+
logger = getLogger(__name__)
10+
711

812
class BoundingBox:
913
def __init__(
1014
self,
1115
origin: Optional[np.ndarray] = None,
1216
maximum: Optional[np.ndarray] = None,
17+
global_origin: Optional[np.ndarray] = None,
1318
nsteps: Optional[np.ndarray] = None,
1419
step_vector: Optional[np.ndarray] = None,
15-
dimensions: Optional[int] = None,
20+
dimensions: Optional[int] = 3,
1621
):
1722
"""A bounding box for a model, defined by the
1823
origin, maximum and number of steps in each direction
@@ -28,19 +33,30 @@ def __init__(
2833
nsteps : Optional[np.ndarray], optional
2934
_description_, by default None
3035
"""
36+
# reproject relative to the global origin, if origin is not provided.
37+
# we want the local coordinates to start at 0
38+
# otherwise uses provided origin. This is useful for having multiple bounding boxes rela
39+
if global_origin is not None and origin is None:
40+
origin = np.zeros(global_origin.shape)
3141
if maximum is None and nsteps is not None and step_vector is not None:
3242
maximum = origin + nsteps * step_vector
43+
if origin is not None and global_origin is None:
44+
global_origin = origin
3345
self._origin = np.array(origin)
3446
self._maximum = np.array(maximum)
35-
if dimensions is None:
36-
if self.origin is None:
37-
raise LoopValueError("Origin is not set")
38-
self.dimensions = len(self.origin)
39-
print(self.dimensions)
47+
self.dimensions = dimensions
48+
if self.origin.shape:
49+
if self.origin.shape[0] != self.dimensions:
50+
logger.warning(
51+
f"Origin has {self.origin.shape[0]} dimensions but bounding box has {self.dimensions}"
52+
)
53+
4054
else:
4155
self.dimensions = dimensions
42-
if nsteps is None:
43-
self.nsteps = np.array([50, 50, 25])
56+
self._global_origin = global_origin
57+
self.nsteps = np.array([50, 50, 25])
58+
if nsteps is not None:
59+
self.nsteps = np.array(nsteps)
4460
self.name_map = {
4561
"xmin": (0, 0),
4662
"ymin": (0, 1),
@@ -58,6 +74,22 @@ def __init__(
5874
"maxz": (1, 2),
5975
}
6076

77+
@property
78+
def global_origin(self):
79+
return self._global_origin
80+
81+
@global_origin.setter
82+
def global_origin(self, global_origin):
83+
if self.dimensions != len(global_origin):
84+
logger.warning(
85+
f"Global origin has {len(global_origin)} dimensions but bounding box has {self.dimensions}"
86+
)
87+
self._global_origin = global_origin
88+
89+
@property
90+
def global_maximum(self):
91+
return self.maximum - self.origin + self._global_origin
92+
6193
@property
6294
def valid(self):
6395
return self._origin is not None and self._maximum is not None
@@ -70,6 +102,10 @@ def origin(self) -> np.ndarray:
70102

71103
@origin.setter
72104
def origin(self, origin: np.ndarray):
105+
if self.dimensions != len(origin):
106+
logger.warning(
107+
f"Origin has {len(origin)} dimensions but bounding box has {self.dimensions}"
108+
)
73109
self._origin = origin
74110

75111
@property
@@ -95,7 +131,17 @@ def bb(self):
95131
return np.array([self.origin, self.maximum])
96132

97133
@nelements.setter
98-
def nelements(self, nelements):
134+
def nelements(self, nelements: Union[int, float]):
135+
"""Update the number of elements in the associated grid
136+
This is for visualisation, not for the interpolation
137+
When set it will update the nsteps/step vector for cubic
138+
elements
139+
140+
Parameters
141+
----------
142+
nelements : int,float
143+
The new number of elements
144+
"""
99145
box_vol = self.volume
100146
ele_vol = box_vol / nelements
101147
# calculate the step vector of a regular cube
@@ -106,15 +152,17 @@ def nelements(self, nelements):
106152
self.nsteps = nsteps
107153

108154
@property
109-
def corners(self):
110-
"""Returns the corners of the bounding box
155+
def corners(self) -> np.ndarray:
156+
"""Returns the corners of the bounding box in local coordinates
157+
111158
112159
113160
Returns
114161
-------
115-
_type_
116-
_description_
162+
np.ndarray
163+
array of corners in clockwise order
117164
"""
165+
118166
return np.array(
119167
[
120168
self.origin.tolist(),
@@ -128,6 +176,29 @@ def corners(self):
128176
]
129177
)
130178

179+
@property
180+
def corners_global(self) -> np.ndarray:
181+
"""Returns the corners of the bounding box
182+
in the original space
183+
184+
Returns
185+
-------
186+
np.ndarray
187+
corners of the bounding box
188+
"""
189+
return np.array(
190+
[
191+
self.global_origin.tolist(),
192+
[self.global_maximum[0], self.global_origin[1], self.global_origin[2]],
193+
[self.global_maximum[0], self.global_maximum[1], self.global_origin[2]],
194+
[self.global_origin[0], self.global_maximum[1], self.global_origin[2]],
195+
[self.global_origin[0], self.global_origin[1], self.global_maximum[2]],
196+
[self.global_maximum[0], self.global_origin[1], self.global_maximum[2]],
197+
self.global_maximum.tolist(),
198+
[self.global_origin[0], self.global_maximum[1], self.global_maximum[2]],
199+
]
200+
)
201+
131202
@property
132203
def step_vector(self):
133204
return (self.maximum - self.origin) / self.nsteps
@@ -136,21 +207,69 @@ def step_vector(self):
136207
def length(self):
137208
return self.maximum - self.origin
138209

139-
def fit(self, locations: np.ndarray):
210+
def fit(self, locations: np.ndarray, local_coordinate: bool = False) -> BoundingBox:
211+
"""Initialise the bounding box from a set of points.
212+
213+
Parameters
214+
----------
215+
locations : np.ndarray
216+
xyz locations of the points to fit the bbox
217+
local_coordinate : bool, optional
218+
whether to set the origin to [0,0,0], by default False
219+
220+
Returns
221+
-------
222+
BoundingBox
223+
A reference to the bounding box object, note this is not a new bounding box
224+
it updates the current one in place.
225+
226+
Raises
227+
------
228+
LoopValueError
229+
_description_
230+
"""
140231
if locations.shape[1] != self.dimensions:
141232
raise LoopValueError(
142233
f"locations array is {locations.shape[1]}D but bounding box is {self.dimensions}"
143234
)
144-
self.origin = locations.min(axis=0)
145-
self.maximum = locations.max(axis=0)
235+
origin = locations.min(axis=0)
236+
maximum = locations.max(axis=0)
237+
if local_coordinate:
238+
self.global_origin = origin
239+
self.origin = np.zeros(3)
240+
self.maximum = maximum - origin
241+
else:
242+
self.origin = origin
243+
self.maximum = maximum
244+
self.global_origin = np.zeros(3)
146245
return self
147246

148247
def with_buffer(self, buffer: float = 0.2) -> BoundingBox:
248+
"""Create a new bounding box with a buffer around the existing bounding box
249+
250+
Parameters
251+
----------
252+
buffer : float, optional
253+
percentage to expand the dimensions by, by default 0.2
254+
255+
Returns
256+
-------
257+
BoundingBox
258+
The new bounding box object.
259+
260+
Raises
261+
------
262+
LoopValueError
263+
if the current bounding box is invalid
264+
"""
149265
if self.origin is None or self.maximum is None:
150266
raise LoopValueError("Cannot create bounding box with buffer, no origin or maximum")
267+
# local coordinates, rescale into the original bounding boxes global coordinates
151268
origin = self.origin - buffer * (self.maximum - self.origin)
152269
maximum = self.maximum + buffer * (self.maximum - self.origin)
153-
return BoundingBox(origin=origin, maximum=maximum)
270+
return BoundingBox(
271+
origin=origin, maximum=maximum, global_origin=self.global_origin + origin
272+
)
154273

155274
def get_value(self, name):
156275
ix, iy = self.name_map.get(name, (-1, -1))
@@ -185,17 +304,110 @@ def is_inside(self, xyz):
185304
inside = np.logical_and(inside, xyz[:, 2] < self.maximum[2])
186305
return inside
187306

188-
def regular_grid(self, nsteps=None, shuffle=False, order="C"):
307+
def regular_grid(
308+
self,
309+
nsteps: Optional[Union[list, np.ndarray]] = None,
310+
shuffle: bool = False,
311+
order: str = "C",
312+
local: bool = True,
313+
) -> np.ndarray:
314+
"""Get the grid of points from the bounding box
315+
316+
Parameters
317+
----------
318+
nsteps : Optional[Union[list, np.ndarray]], optional
319+
number of steps, by default None uses self.nsteps
320+
shuffle : bool, optional
321+
Whether to return points in order or random, by default False
322+
order : str, optional
323+
when flattening using numpy "C" or "F", by default "C"
324+
local : bool, optional
325+
Whether to return the points in the local coordinate system of global
326+
, by default True
327+
328+
Returns
329+
-------
330+
np.ndarray
331+
numpy array N,3 of the points
332+
"""
333+
189334
if nsteps is None:
190335
nsteps = self.nsteps
191336
coordinates = [
192337
np.linspace(self.origin[i], self.maximum[i], nsteps[i]) for i in range(self.dimensions)
193338
]
194339

340+
if not local:
341+
coordinates = [
342+
np.linspace(self.global_origin[i], self.global_maximum[i], nsteps[i])
343+
for i in range(self.dimensions)
344+
]
195345
coordinate_grid = np.meshgrid(*coordinates, indexing="ij")
346+
locs = np.array([coord.flatten(order=order) for coord in coordinate_grid]).T
196347

197-
locs = np.array([c.flatten(order=order) for c in coordinate_grid]).T
198348
if shuffle:
199349
# logger.info("Shuffling points")
200350
rng.shuffle(locs)
201351
return locs
352+
353+
def cell_centers(self, order: str = "F") -> np.ndarray:
354+
"""Get the cell centers of a regular grid
355+
356+
Parameters
357+
----------
358+
order : str, optional
359+
order of the grid, by default "C"
360+
361+
Returns
362+
-------
363+
np.ndarray
364+
array of cell centers
365+
"""
366+
locs = self.regular_grid(order=order, nsteps=self.nsteps - 1)
367+
368+
return locs + 0.5 * self.step_vector
369+
370+
def to_dict(self) -> dict:
371+
"""Export the defining characteristics of the bounding
372+
box to a dictionary for json serialisation
373+
374+
Returns
375+
-------
376+
dict
377+
dictionary with origin, maximum and nsteps
378+
"""
379+
return {
380+
"origin": self.origin.tolist(),
381+
"maximum": self.maximum.tolist(),
382+
"nsteps": self.nsteps.tolist(),
383+
}
384+
385+
def vtk(self):
386+
"""Export the model as a pyvista RectilinearGrid
387+
388+
Returns
389+
-------
390+
pv.RectilinearGrid
391+
a pyvista grid object
392+
393+
Raises
394+
------
395+
ImportError
396+
If pyvista is not installed raise import error
397+
"""
398+
try:
399+
import pyvista as pv
400+
except ImportError:
401+
raise ImportError("pyvista is required for vtk support")
402+
x = np.linspace(self.global_origin[0], self.global_maximum[0], self.nsteps[0])
403+
y = np.linspace(self.global_origin[1], self.global_maximum[1], self.nsteps[1])
404+
z = np.linspace(self.global_origin[2], self.global_maximum[2], self.nsteps[2])
405+
return pv.RectilinearGrid(
406+
x,
407+
y,
408+
z,
409+
)
410+
411+
@property
412+
def structured_grid(self):
413+
pass

LoopStructural/datatypes/_inequality.py

Whitespace-only changes.

LoopStructural/datatypes/_normal.py

Whitespace-only changes.

0 commit comments

Comments
 (0)