LoopStructural 1.6.2__py3-none-any.whl → 1.6.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of LoopStructural might be problematic. Click here for more details.
- LoopStructural/datatypes/_bounding_box.py +19 -4
- LoopStructural/datatypes/_point.py +36 -2
- LoopStructural/datatypes/_structured_grid.py +17 -0
- LoopStructural/datatypes/_surface.py +17 -0
- LoopStructural/export/omf_wrapper.py +49 -21
- LoopStructural/interpolators/__init__.py +13 -0
- LoopStructural/interpolators/_api.py +81 -13
- LoopStructural/interpolators/_discrete_fold_interpolator.py +11 -4
- LoopStructural/interpolators/_discrete_interpolator.py +100 -53
- LoopStructural/interpolators/_finite_difference_interpolator.py +68 -78
- LoopStructural/interpolators/_geological_interpolator.py +27 -10
- LoopStructural/interpolators/_p1interpolator.py +3 -3
- LoopStructural/interpolators/_surfe_wrapper.py +42 -12
- LoopStructural/interpolators/supports/_2d_base_unstructured.py +16 -0
- LoopStructural/interpolators/supports/_2d_structured_grid.py +44 -9
- LoopStructural/interpolators/supports/_3d_base_structured.py +24 -7
- LoopStructural/interpolators/supports/_3d_structured_grid.py +38 -12
- LoopStructural/interpolators/supports/_3d_structured_tetra.py +7 -3
- LoopStructural/interpolators/supports/_3d_unstructured_tetra.py +8 -2
- LoopStructural/interpolators/supports/__init__.py +7 -0
- LoopStructural/interpolators/supports/_base_support.py +7 -0
- LoopStructural/modelling/__init__.py +1 -3
- LoopStructural/modelling/core/geological_model.py +2 -4
- LoopStructural/modelling/features/_analytical_feature.py +25 -16
- LoopStructural/modelling/features/_base_geological_feature.py +21 -8
- LoopStructural/modelling/features/_geological_feature.py +47 -11
- LoopStructural/modelling/features/_structural_frame.py +10 -18
- LoopStructural/modelling/features/_unconformity_feature.py +3 -3
- LoopStructural/modelling/features/builders/_base_builder.py +8 -0
- LoopStructural/modelling/features/builders/_folded_feature_builder.py +45 -14
- LoopStructural/modelling/features/builders/_geological_feature_builder.py +29 -13
- LoopStructural/modelling/features/builders/_structural_frame_builder.py +5 -0
- LoopStructural/modelling/features/fault/__init__.py +1 -1
- LoopStructural/modelling/features/fault/_fault_function.py +19 -1
- LoopStructural/modelling/features/fault/_fault_segment.py +40 -51
- LoopStructural/modelling/features/fold/__init__.py +1 -2
- LoopStructural/modelling/features/fold/_fold_rotation_angle_feature.py +0 -23
- LoopStructural/modelling/features/fold/_foldframe.py +4 -4
- LoopStructural/modelling/features/fold/_svariogram.py +81 -46
- LoopStructural/modelling/features/fold/fold_function/__init__.py +27 -0
- LoopStructural/modelling/features/fold/fold_function/_base_fold_rotation_angle.py +253 -0
- LoopStructural/modelling/features/fold/fold_function/_fourier_series_fold_rotation_angle.py +153 -0
- LoopStructural/modelling/features/fold/fold_function/_lambda_fold_rotation_angle.py +46 -0
- LoopStructural/modelling/features/fold/fold_function/_trigo_fold_rotation_angle.py +151 -0
- LoopStructural/modelling/input/process_data.py +47 -26
- LoopStructural/modelling/input/project_file.py +49 -23
- LoopStructural/utils/__init__.py +1 -0
- LoopStructural/utils/_surface.py +11 -4
- LoopStructural/utils/colours.py +26 -0
- LoopStructural/utils/features.py +5 -0
- LoopStructural/utils/maths.py +51 -0
- LoopStructural/version.py +1 -1
- LoopStructural-1.6.5.dist-info/METADATA +146 -0
- {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.5.dist-info}/RECORD +57 -52
- {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.5.dist-info}/WHEEL +1 -1
- LoopStructural/interpolators/_non_linear_discrete_interpolator.py +0 -0
- LoopStructural/modelling/features/fold/_fold_rotation_angle.py +0 -149
- LoopStructural-1.6.2.dist-info/METADATA +0 -81
- {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.5.dist-info}/LICENSE +0 -0
- {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.5.dist-info}/top_level.txt +0 -0
|
@@ -100,8 +100,8 @@ class FoldFrame(StructuralFrame):
|
|
|
100
100
|
# self.features[0].faults_enabled = False
|
|
101
101
|
# self.features[1].faults_enabled = False
|
|
102
102
|
|
|
103
|
-
gpoints = feature_builder.
|
|
104
|
-
npoints = feature_builder.
|
|
103
|
+
gpoints = feature_builder.get_gradient_constraints()[:, :6]
|
|
104
|
+
npoints = feature_builder.get_norm_constraints()[:, :6]
|
|
105
105
|
points = []
|
|
106
106
|
if gpoints.shape[0] > 0:
|
|
107
107
|
points.append(gpoints)
|
|
@@ -173,8 +173,8 @@ class FoldFrame(StructuralFrame):
|
|
|
173
173
|
|
|
174
174
|
"""
|
|
175
175
|
self.features[0].faults_enabled = False
|
|
176
|
-
gpoints = feature_builder.
|
|
177
|
-
npoints = feature_builder.
|
|
176
|
+
gpoints = feature_builder.get_gradient_constraints()[:, :6]
|
|
177
|
+
npoints = feature_builder.get_norm_constraints()[:, :6]
|
|
178
178
|
points = []
|
|
179
179
|
if gpoints.shape[0] > 0:
|
|
180
180
|
points.append(gpoints)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import numpy as np
|
|
2
|
-
|
|
2
|
+
from typing import List, Tuple, Optional
|
|
3
3
|
from ....utils import getLogger
|
|
4
4
|
|
|
5
5
|
logger = getLogger(__name__)
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
def find_peaks_and_troughs(x, y):
|
|
8
|
+
def find_peaks_and_troughs(x: np.ndarray, y: np.ndarray) -> Tuple[List, List]:
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
Parameters
|
|
@@ -24,7 +24,7 @@ def find_peaks_and_troughs(x, y):
|
|
|
24
24
|
finding the change in derivative
|
|
25
25
|
"""
|
|
26
26
|
if len(x) != len(y):
|
|
27
|
-
|
|
27
|
+
raise ValueError("Cannot guess wavelength, x and y must be the same length")
|
|
28
28
|
pairsx = []
|
|
29
29
|
pairsy = []
|
|
30
30
|
# #TODO numpyize
|
|
@@ -51,55 +51,46 @@ class SVariogram:
|
|
|
51
51
|
The SVariogram is an experimental semi-variogram.
|
|
52
52
|
"""
|
|
53
53
|
|
|
54
|
-
def __init__(self, xdata, ydata):
|
|
55
|
-
self.xdata = xdata
|
|
56
|
-
self.ydata = ydata
|
|
54
|
+
def __init__(self, xdata: np.ndarray, ydata: np.ndarray):
|
|
55
|
+
self.xdata = np.asarray(xdata)
|
|
56
|
+
self.ydata = np.asarray(ydata)
|
|
57
|
+
mask = np.logical_or(np.isnan(self.xdata), np.isnan(self.ydata))
|
|
58
|
+
self.xdata = self.xdata[~mask]
|
|
59
|
+
self.ydata = self.ydata[~mask]
|
|
60
|
+
## maybe check that xdata is not too big here, if it is then this should be done
|
|
61
|
+
## using another library maybe gstools?
|
|
57
62
|
self.dist = np.abs(self.xdata[:, None] - self.xdata[None, :])
|
|
58
63
|
self.variance_matrix = (self.ydata[:, None] - self.ydata[None, :]) ** 2
|
|
59
64
|
self.lags = None
|
|
60
65
|
self.variogram = None
|
|
61
|
-
self.
|
|
66
|
+
self.wavelength_guesses = []
|
|
62
67
|
|
|
63
|
-
def
|
|
68
|
+
def initialise_lags(self, step: Optional[float] = None, nsteps: Optional[int] = None):
|
|
64
69
|
"""
|
|
65
|
-
|
|
66
|
-
You can specify the lags as an array or specify the step size and
|
|
67
|
-
number of steps.
|
|
68
|
-
If neither are specified then the lags are created to be the average
|
|
69
|
-
spacing of the data
|
|
70
|
+
Initialise the lags for the s-variogram
|
|
70
71
|
|
|
71
72
|
Parameters
|
|
72
73
|
----------
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
lags: array
|
|
78
|
-
num
|
|
74
|
+
lag: float
|
|
75
|
+
lag distance for the s-variogram
|
|
76
|
+
nlag: int
|
|
77
|
+
number of lags for the s-variogram
|
|
79
78
|
|
|
80
79
|
Returns
|
|
81
80
|
-------
|
|
82
81
|
|
|
83
82
|
"""
|
|
84
|
-
logger.info("Calculating S-Variogram")
|
|
85
|
-
if lag is not None:
|
|
86
|
-
step = lag
|
|
87
|
-
logger.info(f"Using lag: {step} kwarg for S-variogram")
|
|
88
|
-
|
|
89
|
-
if nlag is not None:
|
|
90
|
-
nstep = nlag
|
|
91
|
-
logger.info(f"Using nlag {nstep} kwarg for s-variogram")
|
|
92
83
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
logger.info(f"Using lag kwarg but calculating nlag as {nstep} for s-variogram")
|
|
84
|
+
if nsteps is not None and step is not None:
|
|
85
|
+
logger.info(f"Using nlag {nsteps} kwarg for s-variogram")
|
|
86
|
+
logger.info(f"Using lag: {step} kwarg for S-variogram")
|
|
87
|
+
self.lags = np.arange(step / 2.0, nsteps * step, step)
|
|
98
88
|
|
|
99
|
-
|
|
89
|
+
if nsteps is None and step is not None:
|
|
90
|
+
nsteps = int(np.ceil((np.nanmax(self.xdata) - np.nanmin(self.xdata)) / step))
|
|
91
|
+
logger.info(f"Using lag kwarg but calculating nlag as {nsteps} for s-variogram")
|
|
100
92
|
|
|
101
|
-
|
|
102
|
-
self.lags = lags
|
|
93
|
+
self.lags = np.arange(step / 2.0, nsteps * step, step)
|
|
103
94
|
|
|
104
95
|
if self.lags is None:
|
|
105
96
|
# time to guess the step size
|
|
@@ -109,15 +100,50 @@ class SVariogram:
|
|
|
109
100
|
|
|
110
101
|
step = np.nanmean(np.nanmin(d, axis=1)) * 4.0
|
|
111
102
|
# find number of steps to cover range in data
|
|
112
|
-
|
|
113
|
-
if
|
|
114
|
-
logger.warning(f"Variogram has too many steps: {
|
|
115
|
-
maximum = step *
|
|
103
|
+
nsteps = int(np.ceil((np.nanmax(self.xdata) - np.nanmin(self.xdata)) / step))
|
|
104
|
+
if nsteps > 200:
|
|
105
|
+
logger.warning(f"Variogram has too many steps: {nsteps}, using 200")
|
|
106
|
+
maximum = step * nsteps
|
|
116
107
|
nstep = 200
|
|
117
108
|
step = maximum / nstep
|
|
118
|
-
self.lags = np.arange(step / 2.0,
|
|
109
|
+
self.lags = np.arange(step / 2.0, nsteps * step, step)
|
|
119
110
|
logger.info(
|
|
120
|
-
f"Using average minimum nearest neighbour distance as lag distance size {step} and using {
|
|
111
|
+
f"Using average minimum nearest neighbour distance as lag distance size {step} and using {nsteps} lags"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def calc_semivariogram(
|
|
115
|
+
self,
|
|
116
|
+
step: Optional[float] = None,
|
|
117
|
+
nsteps: Optional[int] = None,
|
|
118
|
+
lags: Optional[np.ndarray] = None,
|
|
119
|
+
):
|
|
120
|
+
"""
|
|
121
|
+
Calculate a semi-variogram for the x and y data for this object.
|
|
122
|
+
You can specify the lags as an array or specify the step size and
|
|
123
|
+
number of steps.
|
|
124
|
+
If neither are specified then the lags are created to be the average
|
|
125
|
+
spacing of the data
|
|
126
|
+
|
|
127
|
+
Parameters
|
|
128
|
+
----------
|
|
129
|
+
step: float
|
|
130
|
+
lag distance for the s-variogram
|
|
131
|
+
nstep: int
|
|
132
|
+
number of lags for the s-variogram
|
|
133
|
+
lags: array
|
|
134
|
+
num
|
|
135
|
+
|
|
136
|
+
Returns
|
|
137
|
+
-------
|
|
138
|
+
|
|
139
|
+
"""
|
|
140
|
+
logger.info("Calculating S-Variogram")
|
|
141
|
+
if lags is not None:
|
|
142
|
+
self.lags = lags
|
|
143
|
+
self.initialise_lags(step, nsteps)
|
|
144
|
+
if self.lags is None:
|
|
145
|
+
raise ValueError(
|
|
146
|
+
"S-Variogram cannot calculate the variogram step size, please specify either step or nsteps"
|
|
121
147
|
)
|
|
122
148
|
tol = self.lags[1] - self.lags[0]
|
|
123
149
|
self.variogram = np.zeros(self.lags.shape)
|
|
@@ -133,7 +159,12 @@ class SVariogram:
|
|
|
133
159
|
self.variogram[i] = np.mean(self.variance_matrix[logic])
|
|
134
160
|
return self.lags, self.variogram, npairs
|
|
135
161
|
|
|
136
|
-
def find_wavelengths(
|
|
162
|
+
def find_wavelengths(
|
|
163
|
+
self,
|
|
164
|
+
step: Optional[float] = None,
|
|
165
|
+
nsteps: Optional[int] = None,
|
|
166
|
+
lags: Optional[np.ndarray] = None,
|
|
167
|
+
) -> List:
|
|
137
168
|
"""
|
|
138
169
|
Picks the wavelengths of the fold by finding the maximum and
|
|
139
170
|
minimums of the s-variogram
|
|
@@ -145,7 +176,7 @@ class SVariogram:
|
|
|
145
176
|
----------
|
|
146
177
|
kwargs : object
|
|
147
178
|
"""
|
|
148
|
-
h, var,
|
|
179
|
+
h, var, _npairs = self.calc_semivariogram(step=step, nsteps=nsteps, lags=lags)
|
|
149
180
|
|
|
150
181
|
px, py = find_peaks_and_troughs(h, var)
|
|
151
182
|
|
|
@@ -156,7 +187,8 @@ class SVariogram:
|
|
|
156
187
|
averagey.append((py[i] + py[i + 1]) / 2.0)
|
|
157
188
|
i += 1 # iterate twice
|
|
158
189
|
# find the extrema of the average curve
|
|
159
|
-
|
|
190
|
+
res = find_peaks_and_troughs(np.array(averagex), np.array(averagey))
|
|
191
|
+
px2, py2 = res
|
|
160
192
|
wl1 = 0.0
|
|
161
193
|
wl1py = 0.0
|
|
162
194
|
for i in range(len(px)):
|
|
@@ -178,11 +210,14 @@ class SVariogram:
|
|
|
178
210
|
if wl2 > 0.0 and wl2 > wl1 * 2 and wl1py < py2[i]:
|
|
179
211
|
break
|
|
180
212
|
if wl1 == 0.0 and wl2 == 0.0:
|
|
213
|
+
logger.warning(
|
|
214
|
+
'Could not automatically guess the wavelength, using 2x the range of the data'
|
|
215
|
+
)
|
|
181
216
|
self.wavelength_guess = [2 * (np.max(self.xdata) - np.min(self.xdata)), 0.0]
|
|
182
217
|
return self.wavelength_guess
|
|
183
218
|
if np.isclose(wl1, 0.0):
|
|
184
219
|
self.wavelength_guess = np.array([wl2 * 2.0, wl1 * 2.0])
|
|
185
|
-
return
|
|
220
|
+
return [wl2]
|
|
186
221
|
# wavelength is 2x the peak on the curve
|
|
187
|
-
self.wavelength_guess =
|
|
222
|
+
self.wavelength_guess = [wl1 * 2.0, wl2 * 2.0]
|
|
188
223
|
return self.wavelength_guess
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from ._trigo_fold_rotation_angle import TrigoFoldRotationAngleProfile
|
|
2
|
+
from ._fourier_series_fold_rotation_angle import FourierSeriesFoldRotationAngleProfile
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Optional
|
|
5
|
+
import numpy.typing as npt
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FoldRotationType(Enum):
|
|
10
|
+
TRIGONOMETRIC = TrigoFoldRotationAngleProfile
|
|
11
|
+
FOURIER_SERIES = FourierSeriesFoldRotationAngleProfile
|
|
12
|
+
# ADDITIONAL = AdditionalFoldRotationAngle
|
|
13
|
+
|
|
14
|
+
def __str__(self):
|
|
15
|
+
return self.name
|
|
16
|
+
|
|
17
|
+
def __repr__(self):
|
|
18
|
+
return self.name
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_fold_rotation_profile(
|
|
22
|
+
fold_rotation_type,
|
|
23
|
+
rotation_angle: Optional[npt.NDArray[np.float64]] = None,
|
|
24
|
+
fold_frame_coordinate: Optional[npt.NDArray[np.float64]] = None,
|
|
25
|
+
**kwargs,
|
|
26
|
+
):
|
|
27
|
+
return fold_rotation_type.value(rotation_angle, fold_frame_coordinate, **kwargs)
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
from abc import ABCMeta, abstractmethod
|
|
2
|
+
from ast import List
|
|
3
|
+
from typing import Union, Optional
|
|
4
|
+
import numpy as np
|
|
5
|
+
import numpy.typing as npt
|
|
6
|
+
from .._svariogram import SVariogram
|
|
7
|
+
from scipy.optimize import curve_fit
|
|
8
|
+
|
|
9
|
+
from .....utils import getLogger
|
|
10
|
+
|
|
11
|
+
logger = getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseFoldRotationAngleProfile(metaclass=ABCMeta):
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
rotation_angle: Optional[npt.NDArray[np.float64]] = None,
|
|
19
|
+
fold_frame_coordinate: Optional[npt.NDArray[np.float64]] = None,
|
|
20
|
+
):
|
|
21
|
+
"""Base class for fold rotation angle functions
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
rotation_angle : npt.NDArray[np.float64], optional
|
|
26
|
+
the calculated fold rotation angle from observations in degrees, by default None
|
|
27
|
+
fold_frame_coordinate : npt.NDArray[np.float64], optional
|
|
28
|
+
fold frame coordinate scalar field value, by default None
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
self.rotation_angle = rotation_angle
|
|
32
|
+
self.fold_frame_coordinate = fold_frame_coordinate
|
|
33
|
+
self._evaluation_points = None
|
|
34
|
+
self._observers = []
|
|
35
|
+
self._svariogram = None
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def svario(self) -> SVariogram:
|
|
39
|
+
if self.fold_frame_coordinate is None or self.rotation_angle is None:
|
|
40
|
+
raise ValueError("Fold rotation angle and fold frame coordinate must be set")
|
|
41
|
+
if self._svariogram is None:
|
|
42
|
+
self._svariogram = SVariogram(self.fold_frame_coordinate, self.rotation_angle)
|
|
43
|
+
return self._svariogram
|
|
44
|
+
|
|
45
|
+
@svario.setter
|
|
46
|
+
def svario(self, value: SVariogram):
|
|
47
|
+
if isinstance(value, SVariogram):
|
|
48
|
+
self._svariogram = value
|
|
49
|
+
else:
|
|
50
|
+
logger.error("svario must be an instance of SVariogram")
|
|
51
|
+
raise ValueError("svario must be an instance of SVariogram")
|
|
52
|
+
|
|
53
|
+
def add_observer(self, watcher):
|
|
54
|
+
self._observers.append(watcher)
|
|
55
|
+
|
|
56
|
+
def notify_observers(self):
|
|
57
|
+
for observer in self._observers:
|
|
58
|
+
observer.set_not_up_to_date(self)
|
|
59
|
+
|
|
60
|
+
def calculate_misfit(
|
|
61
|
+
self,
|
|
62
|
+
rotation_angle: np.ndarray,
|
|
63
|
+
fold_frame_coordinate: np.ndarray,
|
|
64
|
+
) -> np.ndarray:
|
|
65
|
+
"""Calculate the rotation angle for the fold frame coordinate and return the misfit
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
params : dict, optional
|
|
70
|
+
Any parameters required to fit the function to the data, by default {}
|
|
71
|
+
rotation_angle : np.ndarray
|
|
72
|
+
fold rotation angle in degrees
|
|
73
|
+
fold_frame_coordinate : np.ndarray
|
|
74
|
+
fold frame coordinate
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
misfit : np.ndarray
|
|
79
|
+
returns misfit in degrees"""
|
|
80
|
+
return np.tan(np.deg2rad(rotation_angle)) - np.tan(
|
|
81
|
+
np.deg2rad(self.__call__(fold_frame_coordinate))
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def estimate_wavelength(
|
|
85
|
+
self, svariogram_parameters: dict = {}, wavelength_number: int = 1
|
|
86
|
+
) -> Union[float, np.ndarray]:
|
|
87
|
+
"""Estimate the wavelength of the fold profile using the svariogram parameters
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
svariogram_parameters : dict
|
|
92
|
+
svariogram parameters
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
float
|
|
97
|
+
estimated wavelength
|
|
98
|
+
"""
|
|
99
|
+
wl = self.svario.find_wavelengths(**svariogram_parameters)
|
|
100
|
+
if wavelength_number == 1:
|
|
101
|
+
return wl[0]
|
|
102
|
+
return wl
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def evaluation_points(self):
|
|
106
|
+
"""Return the evaluation points for the fold rotation angle function
|
|
107
|
+
|
|
108
|
+
Returns
|
|
109
|
+
-------
|
|
110
|
+
np.ndarray
|
|
111
|
+
evaluation points
|
|
112
|
+
"""
|
|
113
|
+
if self._evaluation_points is not None:
|
|
114
|
+
return self._evaluation_points
|
|
115
|
+
return np.linspace(
|
|
116
|
+
np.min(self.fold_frame_coordinate), np.max(self.fold_frame_coordinate), 300
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
@evaluation_points.setter
|
|
120
|
+
def evaluation_points(self, value):
|
|
121
|
+
self._evaluation_points = value
|
|
122
|
+
|
|
123
|
+
def fit(self, params: dict = {}) -> bool:
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
Parameters
|
|
127
|
+
----------
|
|
128
|
+
params : dict, optional
|
|
129
|
+
_description_, by default {}
|
|
130
|
+
|
|
131
|
+
Returns
|
|
132
|
+
-------
|
|
133
|
+
bool
|
|
134
|
+
_description_
|
|
135
|
+
"""
|
|
136
|
+
if len(self.params) > 0:
|
|
137
|
+
success = False
|
|
138
|
+
if self.rotation_angle is None or self.fold_frame_coordinate is None:
|
|
139
|
+
logger.error("Fold rotation angle and fold frame coordinate must be set")
|
|
140
|
+
return False
|
|
141
|
+
guess = params.get(
|
|
142
|
+
"guess",
|
|
143
|
+
self.initial_guess(
|
|
144
|
+
wavelength=params.get("wavelength", None),
|
|
145
|
+
reset=params.get("reset", False),
|
|
146
|
+
svariogram_parameters=params.get("svariogram_parameters", {}),
|
|
147
|
+
calculate_wavelength=params.get("calculate_wavelength", True),
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
mask = np.logical_or(
|
|
151
|
+
~np.isnan(self.fold_frame_coordinate), ~np.isnan(self.rotation_angle)
|
|
152
|
+
)
|
|
153
|
+
logger.info(f"Percentage of points not used {np.sum(~mask)/len(mask)*100}")
|
|
154
|
+
try:
|
|
155
|
+
logger.info(f"Trying to fit fold rotation angle with guess {guess}")
|
|
156
|
+
logger.info(f"Fold profile type: {self.__class__.__name__}")
|
|
157
|
+
res = curve_fit(
|
|
158
|
+
self._function,
|
|
159
|
+
self.fold_frame_coordinate[mask],
|
|
160
|
+
np.tan(np.deg2rad(self.rotation_angle[mask])),
|
|
161
|
+
p0=guess,
|
|
162
|
+
full_output=True,
|
|
163
|
+
)
|
|
164
|
+
logger.info(f'Fit results: {res[0]}')
|
|
165
|
+
guess = res[0]
|
|
166
|
+
logger.info(res[3])
|
|
167
|
+
success = True
|
|
168
|
+
except Exception as _e:
|
|
169
|
+
logger.error("Could not fit curve to S-Plot, check the wavelength")
|
|
170
|
+
try:
|
|
171
|
+
self.update_params(guess)
|
|
172
|
+
except Exception as _e:
|
|
173
|
+
logger.error("Could not update parameters")
|
|
174
|
+
return False
|
|
175
|
+
return success
|
|
176
|
+
return True
|
|
177
|
+
|
|
178
|
+
@abstractmethod
|
|
179
|
+
def update_params(self, params: Union[List, npt.NDArray[np.float64]]) -> None:
|
|
180
|
+
"""Update the parameters of the fold rotation angle function
|
|
181
|
+
|
|
182
|
+
Parameters
|
|
183
|
+
----------
|
|
184
|
+
params : dict
|
|
185
|
+
parameters to update
|
|
186
|
+
"""
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
@abstractmethod
|
|
190
|
+
def initial_guess(
|
|
191
|
+
self,
|
|
192
|
+
wavelength: Optional[float] = None,
|
|
193
|
+
calculate_wavelength: bool = True,
|
|
194
|
+
svariogram_parameters: dict = {},
|
|
195
|
+
reset: bool = False,
|
|
196
|
+
) -> np.ndarray:
|
|
197
|
+
"""_summary_
|
|
198
|
+
|
|
199
|
+
Parameters
|
|
200
|
+
----------
|
|
201
|
+
selfcalculate_wavelength : bool, optional
|
|
202
|
+
_description_, by default True
|
|
203
|
+
svariogram_parameters : dict, optional
|
|
204
|
+
_description_, by default {}
|
|
205
|
+
reset : bool, optional
|
|
206
|
+
_description_, by default False
|
|
207
|
+
|
|
208
|
+
Returns
|
|
209
|
+
-------
|
|
210
|
+
np.ndarray
|
|
211
|
+
_description_
|
|
212
|
+
"""
|
|
213
|
+
pass
|
|
214
|
+
|
|
215
|
+
@staticmethod
|
|
216
|
+
@abstractmethod
|
|
217
|
+
def _function(s, *args, **kwargs):
|
|
218
|
+
"""This is the function that is used to calculate the fold rotation angle
|
|
219
|
+
for a given fold frame coordinate
|
|
220
|
+
it is not called directly but is used by the __call__ method
|
|
221
|
+
|
|
222
|
+
Parameters
|
|
223
|
+
----------
|
|
224
|
+
s
|
|
225
|
+
*args
|
|
226
|
+
|
|
227
|
+
Returns
|
|
228
|
+
-------
|
|
229
|
+
_description_
|
|
230
|
+
"""
|
|
231
|
+
pass
|
|
232
|
+
|
|
233
|
+
def plot(self, ax=None, show_data=True, **kwargs):
|
|
234
|
+
"""Plot the fold rotation angle function
|
|
235
|
+
|
|
236
|
+
Parameters
|
|
237
|
+
----------
|
|
238
|
+
ax : _description_, optional
|
|
239
|
+
_description_, by default None
|
|
240
|
+
**kwargs
|
|
241
|
+
passed to matplotlib plot
|
|
242
|
+
"""
|
|
243
|
+
if ax is None:
|
|
244
|
+
import matplotlib.pyplot as plt
|
|
245
|
+
|
|
246
|
+
fig, ax = plt.subplots()
|
|
247
|
+
if show_data:
|
|
248
|
+
ax.scatter(self.fold_frame_coordinate, self.rotation_angle, c="r")
|
|
249
|
+
ax.plot(self.evaluation_points, self(self.evaluation_points), **kwargs)
|
|
250
|
+
return ax
|
|
251
|
+
|
|
252
|
+
def __call__(self, s):
|
|
253
|
+
return np.rad2deg(np.arctan(self._function(s, **self.params)))
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from ._base_fold_rotation_angle import BaseFoldRotationAngleProfile
|
|
2
|
+
import numpy as np
|
|
3
|
+
import numpy.typing as npt
|
|
4
|
+
from typing import Optional, List, Union
|
|
5
|
+
from .....utils import getLogger
|
|
6
|
+
|
|
7
|
+
logger = getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FourierSeriesFoldRotationAngleProfile(BaseFoldRotationAngleProfile):
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
rotation_angle: Optional[npt.NDArray[np.float64]] = None,
|
|
14
|
+
fold_frame_coordinate: Optional[npt.NDArray[np.float64]] = None,
|
|
15
|
+
c0=0,
|
|
16
|
+
c1=0,
|
|
17
|
+
c2=0,
|
|
18
|
+
w=1,
|
|
19
|
+
):
|
|
20
|
+
"""_summary_
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
rotation_angle : Optional[npt.NDArray[np.float64]], optional
|
|
25
|
+
_description_, by default None
|
|
26
|
+
fold_frame_coordinate : Optional[npt.NDArray[np.float64]], optional
|
|
27
|
+
_description_, by default None
|
|
28
|
+
c0 : int, optional
|
|
29
|
+
_description_, by default 0
|
|
30
|
+
c1 : int, optional
|
|
31
|
+
_description_, by default 0
|
|
32
|
+
c2 : int, optional
|
|
33
|
+
_description_, by default 0
|
|
34
|
+
w : int, optional
|
|
35
|
+
_description_, by default 1
|
|
36
|
+
"""
|
|
37
|
+
super().__init__(rotation_angle, fold_frame_coordinate)
|
|
38
|
+
self._c0 = c0
|
|
39
|
+
self._c1 = c1
|
|
40
|
+
self._c2 = c2
|
|
41
|
+
self._w = w
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def c0(self):
|
|
45
|
+
return self._c0
|
|
46
|
+
|
|
47
|
+
@c0.setter
|
|
48
|
+
def c0(self, value):
|
|
49
|
+
self.notify_observers()
|
|
50
|
+
self._c0 = value
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def c1(self):
|
|
54
|
+
return self._c1
|
|
55
|
+
|
|
56
|
+
@c1.setter
|
|
57
|
+
def c1(self, value):
|
|
58
|
+
self.notify_observers()
|
|
59
|
+
self._c1 = value
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def c2(self):
|
|
63
|
+
return self._c2
|
|
64
|
+
|
|
65
|
+
@c2.setter
|
|
66
|
+
def c2(self, value):
|
|
67
|
+
self.notify_observers()
|
|
68
|
+
self._c2 = value
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def w(self):
|
|
72
|
+
return self._w
|
|
73
|
+
|
|
74
|
+
@w.setter
|
|
75
|
+
def w(self, value):
|
|
76
|
+
if value <= 0:
|
|
77
|
+
raise ValueError('wavelength must be greater than 0')
|
|
78
|
+
self.notify_observers()
|
|
79
|
+
self._w = value
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def _function(x, c0, c1, c2, w):
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
x
|
|
88
|
+
c0
|
|
89
|
+
c1
|
|
90
|
+
c2
|
|
91
|
+
w
|
|
92
|
+
|
|
93
|
+
Returns
|
|
94
|
+
-------
|
|
95
|
+
|
|
96
|
+
"""
|
|
97
|
+
v = np.array(x.astype(float))
|
|
98
|
+
# v.fill(c0)
|
|
99
|
+
v = c0 + c1 * np.cos(2 * np.pi / w * x) + c2 * np.sin(2 * np.pi / w * x)
|
|
100
|
+
return v
|
|
101
|
+
|
|
102
|
+
def initial_guess(
|
|
103
|
+
self,
|
|
104
|
+
wavelength: Optional[float] = None,
|
|
105
|
+
calculate_wavelength: bool = True,
|
|
106
|
+
svariogram_parameters: dict = {},
|
|
107
|
+
reset: bool = False,
|
|
108
|
+
):
|
|
109
|
+
# reset the fold paramters before fitting
|
|
110
|
+
# otherwise use the current values to fit
|
|
111
|
+
if reset:
|
|
112
|
+
self.c0 = np.mean(np.arctan(np.deg2rad(self.rotation_angle)))
|
|
113
|
+
self.c1 = 0
|
|
114
|
+
self.c2 = np.max(np.arctan(np.deg2rad(self.rotation_angle)))
|
|
115
|
+
self.w = 1
|
|
116
|
+
if calculate_wavelength:
|
|
117
|
+
self.w = self.estimate_wavelength(svariogram_parameters=svariogram_parameters)
|
|
118
|
+
if wavelength is not None:
|
|
119
|
+
self.w = wavelength
|
|
120
|
+
guess = [self.c0, self.c1, self.c2, self.w]
|
|
121
|
+
return guess
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def params(self):
|
|
125
|
+
return {
|
|
126
|
+
"c0": self.c0,
|
|
127
|
+
"c1": self.c1,
|
|
128
|
+
"c2": self.c2,
|
|
129
|
+
"w": self.w,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@params.setter
|
|
133
|
+
def params(self, params):
|
|
134
|
+
for key in params:
|
|
135
|
+
if key == 'w':
|
|
136
|
+
if params[key] <= 0:
|
|
137
|
+
raise ValueError('wavelength must be greater than 0')
|
|
138
|
+
setattr(self, key, params[key])
|
|
139
|
+
self.c0 = params["c0"]
|
|
140
|
+
self.c1 = params["c1"]
|
|
141
|
+
self.c2 = params["c2"]
|
|
142
|
+
self.w = params["w"]
|
|
143
|
+
|
|
144
|
+
def update_params(self, params: Union[List[float], npt.NDArray[np.float64]]):
|
|
145
|
+
if len(params) != 4:
|
|
146
|
+
raise ValueError('params must have 4 elements')
|
|
147
|
+
self.c0 = params[0]
|
|
148
|
+
self.c1 = params[1]
|
|
149
|
+
self.c2 = params[2]
|
|
150
|
+
self.w = params[3]
|
|
151
|
+
|
|
152
|
+
def calculate_misfit(self, s, rotation_angle):
|
|
153
|
+
return np.tan(np.deg2rad(rotation_angle)) - np.tan(np.deg2rad(self.__call__(s)))
|