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.

Files changed (60) hide show
  1. LoopStructural/datatypes/_bounding_box.py +19 -4
  2. LoopStructural/datatypes/_point.py +36 -2
  3. LoopStructural/datatypes/_structured_grid.py +17 -0
  4. LoopStructural/datatypes/_surface.py +17 -0
  5. LoopStructural/export/omf_wrapper.py +49 -21
  6. LoopStructural/interpolators/__init__.py +13 -0
  7. LoopStructural/interpolators/_api.py +81 -13
  8. LoopStructural/interpolators/_discrete_fold_interpolator.py +11 -4
  9. LoopStructural/interpolators/_discrete_interpolator.py +100 -53
  10. LoopStructural/interpolators/_finite_difference_interpolator.py +68 -78
  11. LoopStructural/interpolators/_geological_interpolator.py +27 -10
  12. LoopStructural/interpolators/_p1interpolator.py +3 -3
  13. LoopStructural/interpolators/_surfe_wrapper.py +42 -12
  14. LoopStructural/interpolators/supports/_2d_base_unstructured.py +16 -0
  15. LoopStructural/interpolators/supports/_2d_structured_grid.py +44 -9
  16. LoopStructural/interpolators/supports/_3d_base_structured.py +24 -7
  17. LoopStructural/interpolators/supports/_3d_structured_grid.py +38 -12
  18. LoopStructural/interpolators/supports/_3d_structured_tetra.py +7 -3
  19. LoopStructural/interpolators/supports/_3d_unstructured_tetra.py +8 -2
  20. LoopStructural/interpolators/supports/__init__.py +7 -0
  21. LoopStructural/interpolators/supports/_base_support.py +7 -0
  22. LoopStructural/modelling/__init__.py +1 -3
  23. LoopStructural/modelling/core/geological_model.py +2 -4
  24. LoopStructural/modelling/features/_analytical_feature.py +25 -16
  25. LoopStructural/modelling/features/_base_geological_feature.py +21 -8
  26. LoopStructural/modelling/features/_geological_feature.py +47 -11
  27. LoopStructural/modelling/features/_structural_frame.py +10 -18
  28. LoopStructural/modelling/features/_unconformity_feature.py +3 -3
  29. LoopStructural/modelling/features/builders/_base_builder.py +8 -0
  30. LoopStructural/modelling/features/builders/_folded_feature_builder.py +45 -14
  31. LoopStructural/modelling/features/builders/_geological_feature_builder.py +29 -13
  32. LoopStructural/modelling/features/builders/_structural_frame_builder.py +5 -0
  33. LoopStructural/modelling/features/fault/__init__.py +1 -1
  34. LoopStructural/modelling/features/fault/_fault_function.py +19 -1
  35. LoopStructural/modelling/features/fault/_fault_segment.py +40 -51
  36. LoopStructural/modelling/features/fold/__init__.py +1 -2
  37. LoopStructural/modelling/features/fold/_fold_rotation_angle_feature.py +0 -23
  38. LoopStructural/modelling/features/fold/_foldframe.py +4 -4
  39. LoopStructural/modelling/features/fold/_svariogram.py +81 -46
  40. LoopStructural/modelling/features/fold/fold_function/__init__.py +27 -0
  41. LoopStructural/modelling/features/fold/fold_function/_base_fold_rotation_angle.py +253 -0
  42. LoopStructural/modelling/features/fold/fold_function/_fourier_series_fold_rotation_angle.py +153 -0
  43. LoopStructural/modelling/features/fold/fold_function/_lambda_fold_rotation_angle.py +46 -0
  44. LoopStructural/modelling/features/fold/fold_function/_trigo_fold_rotation_angle.py +151 -0
  45. LoopStructural/modelling/input/process_data.py +47 -26
  46. LoopStructural/modelling/input/project_file.py +49 -23
  47. LoopStructural/utils/__init__.py +1 -0
  48. LoopStructural/utils/_surface.py +11 -4
  49. LoopStructural/utils/colours.py +26 -0
  50. LoopStructural/utils/features.py +5 -0
  51. LoopStructural/utils/maths.py +51 -0
  52. LoopStructural/version.py +1 -1
  53. LoopStructural-1.6.5.dist-info/METADATA +146 -0
  54. {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.5.dist-info}/RECORD +57 -52
  55. {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.5.dist-info}/WHEEL +1 -1
  56. LoopStructural/interpolators/_non_linear_discrete_interpolator.py +0 -0
  57. LoopStructural/modelling/features/fold/_fold_rotation_angle.py +0 -149
  58. LoopStructural-1.6.2.dist-info/METADATA +0 -81
  59. {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.5.dist-info}/LICENSE +0 -0
  60. {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.interpolator.get_gradient_constraints()[:, :6]
104
- npoints = feature_builder.interpolator.get_norm_constraints()[:, :6]
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.interpolator.get_gradient_constraints()[:, :6]
177
- npoints = feature_builder.interpolator.get_norm_constraints()[:, :6]
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
- return False
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.wavelength_guess = [None, None]
66
+ self.wavelength_guesses = []
62
67
 
63
- def calc_semivariogram(self, lag=None, nlag=None, lags=None):
68
+ def initialise_lags(self, step: Optional[float] = None, nsteps: Optional[int] = None):
64
69
  """
65
- Calculate a semi-variogram for the x and y data for this object.
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
- step: float
74
- lag distance for the s-variogram
75
- nstep: int
76
- number of lags for the s-variogram
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
- self.lags = np.arange(step / 2.0, nstep * step, step)
94
-
95
- if nlag is None and lag is not None:
96
- nstep = int(np.ceil((np.nanmax(self.xdata) - np.nanmin(self.xdata)) / step))
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
- self.lags = np.arange(step / 2.0, nstep * step, step)
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
- if lags is not None:
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
- nstep = int(np.ceil((np.nanmax(self.xdata) - np.nanmin(self.xdata)) / step))
113
- if nstep > 200:
114
- logger.warning(f"Variogram has too many steps: {nstep}, using 200")
115
- maximum = step * nstep
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, nstep * step, step)
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 {nstep} lags"
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(self, **kwargs):
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, npairs = self.calc_semivariogram(**kwargs)
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
- px2, py2 = find_peaks_and_troughs(averagex, averagey)
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 self.wavelength_guess
220
+ return [wl2]
186
221
  # wavelength is 2x the peak on the curve
187
- self.wavelength_guess = np.array([wl1 * 2.0, wl2 * 2.0])
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)))