py-pilecore 0.5.0__py3-none-any.whl → 0.6.0__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 py-pilecore might be problematic. Click here for more details.

@@ -0,0 +1,193 @@
1
+ from __future__ import annotations # noqa: F404
2
+
3
+ from typing import Hashable, List
4
+
5
+ import pandas as pd
6
+ import plotly.express as px
7
+ import plotly.graph_objects as go
8
+ from natsort import natsort_keygen
9
+
10
+ from pypilecore.results.cases_multi_cpt_results import CasesMultiCPTBearingResults
11
+ from pypilecore.results.result_definitions import CPTGroupResultDefinitions
12
+
13
+
14
+ class FigureCPTGroupResultsVersusPtls:
15
+ """
16
+ Interactive figure to show the CPT grouped results of the bearing capacity calculations
17
+ versus the pile tip levels (PTLs).
18
+
19
+ The layout of the figure is:
20
+ - X axis: result values.
21
+ - Y axis: pile tip level w.r.t. NAP.
22
+ - Each trace represents a different case.
23
+
24
+ The figure has a method to switch between results.
25
+ """
26
+
27
+ def __init__(self, cases_multi_results: CasesMultiCPTBearingResults) -> None:
28
+ """
29
+ Initializes the figure.
30
+
31
+ Parameters
32
+ ----------
33
+ cases_multi_results : CasesMultiCPTBearingResults
34
+ The results of the bearing capacity calculations.
35
+
36
+ Raises
37
+ ------
38
+ TypeError
39
+ If `cases_multi_results` are not of type 'CasesMultiCPTBearingResults'.
40
+ """
41
+ # Validate the data
42
+ self._set_results(cases_multi_results)
43
+
44
+ # Initialize the figure
45
+ self._figure = go.FigureWidget(
46
+ layout=go.Layout(
47
+ height=800,
48
+ width=800,
49
+ title="CPT Group Results vs. Pile tip level for all cases<br>Result: ",
50
+ legend_title="Case",
51
+ colorway=px.colors.qualitative.Plotly,
52
+ xaxis_title="",
53
+ yaxis=go.layout.YAxis(
54
+ title="Pile tip level [m NAP]",
55
+ title_font_size=18,
56
+ ),
57
+ autosize=False,
58
+ )
59
+ )
60
+
61
+ def _set_results(self, value: CasesMultiCPTBearingResults) -> None:
62
+ """Private setter for the results."""
63
+ if not isinstance(value, CasesMultiCPTBearingResults):
64
+ raise TypeError(
65
+ f"Expected type 'CasesMultiCPTBearingResults' for 'cases_multi_results', but got {type(value)}"
66
+ )
67
+ self._results = value
68
+
69
+ @property
70
+ def results(self) -> CasesMultiCPTBearingResults:
71
+ """The results of the bearing capacity calculations."""
72
+ return self._results
73
+
74
+ @property
75
+ def data(self) -> pd.DataFrame:
76
+ """The dataframe used to plot the results."""
77
+ return self.results.cpt_group_results_dataframe
78
+
79
+ @property
80
+ def cases(self) -> List[Hashable]:
81
+ """The case names of each MultiCPTBearingResults."""
82
+ return self.results.cases
83
+
84
+ @property
85
+ def figure(self) -> go.FigureWidget:
86
+ """The figure widget."""
87
+ return self._figure
88
+
89
+ def get_visible_cases(self) -> List[Hashable]:
90
+ """
91
+ Returns the visible cases in the figure widget.
92
+
93
+ Notes
94
+ -----
95
+ - If there are no traces in the figure, it returns all the case names.
96
+ - If a case is None, then it is represented as "None".
97
+ """
98
+ if len(self.figure.data) == 0:
99
+ # Select all cases if there are no traces in the figure yet.
100
+ cases: List[Hashable] = []
101
+ for case in self.cases:
102
+ if case is None:
103
+ cases.append("None")
104
+ else:
105
+ cases.append(case)
106
+ return cases
107
+ return [trace.name for trace in self.figure.data if trace.visible is True]
108
+
109
+ def show_result(self, result_name: str) -> None:
110
+ """Shows the group results for all pile tip levels for the requested `result_name`.
111
+
112
+ Parameters
113
+ ----------
114
+ result_name : str
115
+ The name of the result to show.
116
+
117
+ Raises
118
+ ------
119
+ ValueError
120
+ If the `result_name` is not found in the CPTResultDefinitions.
121
+ """
122
+ # Get the result definition that corresponds to the result name.
123
+ result_definition = CPTGroupResultDefinitions.get(result_name)
124
+
125
+ # Get the visible cases
126
+ visible_cases = self.get_visible_cases()
127
+
128
+ # Select data for result type
129
+ selected_data = self.data.loc[
130
+ self.data["result_name"] == result_definition.name
131
+ ]
132
+
133
+ # Organize data and format depending on result type
134
+ mode = "lines+markers"
135
+ marker_size = 6
136
+ if result_definition in [
137
+ CPTGroupResultDefinitions.cpt_Rc_min,
138
+ CPTGroupResultDefinitions.cpt_Rc_max,
139
+ CPTGroupResultDefinitions.cpt_normative,
140
+ CPTGroupResultDefinitions.use_group_average,
141
+ CPTGroupResultDefinitions.xi_normative,
142
+ CPTGroupResultDefinitions.n_cpts,
143
+ ]:
144
+ selected_data = selected_data.sort_values(by="result", key=natsort_keygen())
145
+ mode = "markers"
146
+ marker_size = 10
147
+
148
+ traces = []
149
+ for case in self.cases:
150
+ if case is not None:
151
+ df = selected_data.loc[selected_data["case_name"] == case]
152
+ else:
153
+ df = selected_data.loc[selected_data["case_name"].isna()]
154
+
155
+ traces.append(
156
+ go.Scatter(
157
+ x=df["result"],
158
+ y=df["pile_tip_level_nap"],
159
+ mode=mode,
160
+ name=case if case is not None else "None",
161
+ marker_size=marker_size,
162
+ )
163
+ )
164
+
165
+ with self.figure.batch_update():
166
+ # Empty traces
167
+ self.figure.data = []
168
+
169
+ # Apply changes
170
+ self.figure.add_traces(traces)
171
+
172
+ self.figure.update_layout(
173
+ title=f"CPT Group Results vs. Pile tip level for all cases<br>Result: {result_definition.value.html}",
174
+ xaxis=go.layout.XAxis(
175
+ title=f"{result_definition.value.html} [{result_definition.value.unit}]",
176
+ title_font_size=18,
177
+ ),
178
+ showlegend=True,
179
+ )
180
+
181
+ self.figure.update_traces(
182
+ selector=lambda x: x.name in visible_cases,
183
+ patch=dict(
184
+ visible=True,
185
+ ),
186
+ )
187
+
188
+ self.figure.update_traces(
189
+ selector=lambda x: x.name not in visible_cases,
190
+ patch=dict(
191
+ visible="legendonly",
192
+ ),
193
+ )
@@ -0,0 +1,268 @@
1
+ from __future__ import annotations # noqa: F404
2
+
3
+ from typing import Hashable, List
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ import plotly.express as px
8
+ import plotly.graph_objects as go
9
+
10
+ from pypilecore.results.cases_multi_cpt_results import CasesMultiCPTBearingResults
11
+ from pypilecore.results.result_definitions import CPTResultDefinitions
12
+ from pypilecore.viewers.interactive_figures.utils import get_continuous_color
13
+
14
+
15
+ class FigureCPTResultsPlanView:
16
+ """
17
+ Interactive figure to show the CPT results of the bearing capacity calculations
18
+ in plan view for a fixed pile tip level (PTL).
19
+
20
+ The layout of the figure is:
21
+ - X axis: X coordinate.
22
+ - Y axis: Y coordinate.
23
+ - Each point represents a different CPT, but the same pile tip level.
24
+
25
+ The figure has a method to switch between case, result and pile tip level.
26
+ """
27
+
28
+ def __init__(self, cases_multi_results: CasesMultiCPTBearingResults) -> None:
29
+ """
30
+ Initializes the figure.
31
+
32
+ Parameters
33
+ ----------
34
+ cases_multi_results : CasesMultiCPTBearingResults
35
+ The results of the bearing capacity calculations.
36
+
37
+ Raises
38
+ ------
39
+ TypeError
40
+ If `cases_multi_results` are not of type 'CasesMultiCPTBearingResults'.
41
+ """
42
+ # Validate the data
43
+ self._set_results(cases_multi_results)
44
+
45
+ # Initialize the figure
46
+ self._figure = go.FigureWidget(
47
+ layout=go.Layout(
48
+ height=800,
49
+ width=1200,
50
+ title="CPT Results in Plan View<br>Case: , Pile tip level [m NAP]: <br>Result: ",
51
+ legend_title="CPT",
52
+ colorway=px.colors.qualitative.Plotly,
53
+ xaxis=go.layout.XAxis(
54
+ title="X [m]",
55
+ title_font_size=18,
56
+ ),
57
+ yaxis=go.layout.YAxis(
58
+ title="Y [m]",
59
+ title_font_size=18,
60
+ ),
61
+ autosize=False,
62
+ )
63
+ )
64
+
65
+ def _set_results(self, value: CasesMultiCPTBearingResults) -> None:
66
+ """Private setter for the results."""
67
+ if not isinstance(value, CasesMultiCPTBearingResults):
68
+ raise TypeError(
69
+ f"Expected type 'CasesMultiCPTBearingResults' for 'cases_multi_results', but got {type(value)}"
70
+ )
71
+ self._results = value
72
+
73
+ @property
74
+ def results(self) -> CasesMultiCPTBearingResults:
75
+ """The results of the bearing capacity calculations."""
76
+ return self._results
77
+
78
+ @property
79
+ def data(self) -> pd.DataFrame:
80
+ """The dataframe used to plot the results."""
81
+ return self.results.cpt_results_dataframe
82
+
83
+ @property
84
+ def cases(self) -> List[Hashable]:
85
+ """The case names of each MultiCPTBearingResults."""
86
+ return self.results.cases
87
+
88
+ @property
89
+ def test_ids(self) -> List[str]:
90
+ """The test_ids (cpt names) of all the MultiCPTBearingResults."""
91
+ return self.results.test_ids
92
+
93
+ @property
94
+ def pile_tip_levels_nap(self) -> List[float]:
95
+ """The pile tip levels w.r.t. NAP of all the MultiCPTBearingResults."""
96
+ return self.results.pile_tip_levels_nap
97
+
98
+ @property
99
+ def figure(self) -> go.FigureWidget:
100
+ """The figure widget."""
101
+ return self._figure
102
+
103
+ def get_visible_test_ids(self) -> List[str]:
104
+ """Returns the visible `test_id` (s) in the figure widget."""
105
+ return [trace.name for trace in self.figure.data if trace.visible is True]
106
+
107
+ def show_case_result_and_ptl(
108
+ self, case_name: Hashable, result_name: str, pile_tip_level_nap: float
109
+ ) -> None:
110
+ """Shows the results for all CPTs for the requested `case_name`, `result_name` and `pile_tip_level`.
111
+
112
+ Parameters
113
+ ----------
114
+ case_name : str
115
+ The name of the case to show.
116
+ result_name : str
117
+ The name of the result to show.
118
+ pile_tip_level_nap : float
119
+ The pile tip level w.r.t. NAP to show.
120
+
121
+
122
+ Raises
123
+ ------
124
+ TypeError
125
+ If the `pile_tip_level_nap` is not of type 'float'.
126
+ ValueError
127
+ If the `case_name` is not found in the cases.
128
+ If the `result_name` is not found in the CPTResultDefinitions.
129
+ If the `pile_tip_level_nap` is not found in the pile tip levels.
130
+ """
131
+ # Check that case name is in cases.
132
+ if case_name not in self.cases:
133
+ raise ValueError(f"Case name '{case_name}' not found in cases.")
134
+
135
+ # Get the result definition that corresponds to the result name.
136
+ result_definition = CPTResultDefinitions.get(result_name)
137
+
138
+ # Check that pile tip level NAP is in pile tip levels.
139
+ if not isinstance(pile_tip_level_nap, (int, float)):
140
+ raise TypeError(
141
+ f"Expected type 'float' for 'pile_tip_level_nap', but got {type(pile_tip_level_nap)}"
142
+ )
143
+ if not any(
144
+ np.isclose(pile_tip_level_nap, self.pile_tip_levels_nap, rtol=0, atol=1e-4)
145
+ ):
146
+ raise ValueError(
147
+ f"Pile tip level NAP '{pile_tip_level_nap}' not found in pile tip levels NAP."
148
+ )
149
+
150
+ # Get the visible test_ids
151
+ if len(self.figure.data) == 0:
152
+ # Select all test_ids if there are no traces in the figure yet.
153
+ visible_test_ids = self.test_ids
154
+ else:
155
+ visible_test_ids = self.get_visible_test_ids()
156
+
157
+ # Select data for case name and result name.
158
+ mask_case_name = (
159
+ self.data["case_name"] == case_name
160
+ if case_name is not None
161
+ else self.data["case_name"].isna()
162
+ )
163
+ mask_pile_tip_level = np.isclose(
164
+ pile_tip_level_nap,
165
+ self.data["pile_tip_level_nap"].to_numpy(),
166
+ rtol=0,
167
+ atol=1e-4,
168
+ )
169
+ selected_data = self.data.loc[
170
+ (mask_case_name)
171
+ & (self.data["result_name"] == result_definition.name)
172
+ & (mask_pile_tip_level)
173
+ ]
174
+
175
+ # Get the min and max result values for the color scale.
176
+ result_max = selected_data["result"].max()
177
+ result_min = selected_data["result"].min()
178
+ colorscale = px.colors.get_colorscale("picnic")
179
+
180
+ traces = []
181
+ for test_id in self.test_ids:
182
+ df = selected_data.loc[selected_data["test_id"] == test_id]
183
+ result = round(df["result"].values[0], 1)
184
+ color = get_continuous_color(
185
+ colorscale=colorscale,
186
+ intermed=(result - result_min) / (result_max - result_min),
187
+ )
188
+ traces.append(
189
+ go.Scatter(
190
+ x=df["x"],
191
+ y=df["y"],
192
+ text=f"CPT {test_id}<br>{result}",
193
+ mode="markers+text",
194
+ name=test_id,
195
+ marker=dict(
196
+ size=8,
197
+ color=color,
198
+ line=dict(
199
+ width=0.5,
200
+ color="black",
201
+ ),
202
+ ),
203
+ textposition="top center",
204
+ hoverinfo="none",
205
+ )
206
+ )
207
+
208
+ # Add the colorbar
209
+ traces.append(
210
+ go.Scatter(
211
+ x=[None],
212
+ y=[None],
213
+ mode="markers",
214
+ name="__colorbar__",
215
+ marker=dict(
216
+ colorscale=colorscale,
217
+ showscale=True,
218
+ cmin=result_min,
219
+ cmax=result_max,
220
+ colorbar=dict(
221
+ thickness=16,
222
+ orientation="h",
223
+ x=0.75,
224
+ y=1.0,
225
+ len=0.5,
226
+ title=f"{result_definition.value.html} [{result_definition.value.unit}]",
227
+ title_font_size=14,
228
+ ),
229
+ ),
230
+ hoverinfo="none",
231
+ showlegend=False,
232
+ )
233
+ )
234
+
235
+ with self.figure.batch_update():
236
+ # Empty traces
237
+ self.figure.data = []
238
+
239
+ # Apply changes
240
+ self.figure.add_traces(traces)
241
+
242
+ self.figure.update_layout(
243
+ title=f"CPT Results in Plan View<br>Case: {case_name}, "
244
+ + f"Pile tip level [m NAP]: {pile_tip_level_nap}<br>"
245
+ + f"Result: {result_definition.value.html} [{result_definition.value.unit}]"
246
+ )
247
+
248
+ self.figure.update_traces(
249
+ selector=lambda x: x.name in visible_test_ids,
250
+ patch=dict(
251
+ visible=True,
252
+ ),
253
+ )
254
+
255
+ self.figure.update_traces(
256
+ selector=lambda x: x.name not in visible_test_ids,
257
+ patch=dict(
258
+ visible="legendonly",
259
+ ),
260
+ )
261
+
262
+ self.figure.update_traces(
263
+ selector=lambda x: x.name == "__colorbar__",
264
+ patch=dict(
265
+ visible=True,
266
+ showlegend=False,
267
+ ),
268
+ )
@@ -0,0 +1,176 @@
1
+ from __future__ import annotations # noqa: F404
2
+
3
+ from typing import Hashable, List
4
+
5
+ import pandas as pd
6
+ import plotly.express as px
7
+ import plotly.graph_objects as go
8
+
9
+ from pypilecore.results.cases_multi_cpt_results import CasesMultiCPTBearingResults
10
+ from pypilecore.results.result_definitions import CPTResultDefinitions
11
+
12
+
13
+ class FigureCPTResultsVersusPtls:
14
+ """
15
+ Interactive figure to show the CPT results of the bearing capacity calculations
16
+ versus the pile tip levels (PTLs).
17
+
18
+ The layout of the figure is:
19
+ - X axis: result values.
20
+ - Y axis: pile tip level w.r.t. NAP.
21
+ - Each trace represents a different CPT.
22
+
23
+ The figure has a method to switch between cases and results.
24
+ """
25
+
26
+ def __init__(self, cases_multi_results: CasesMultiCPTBearingResults) -> None:
27
+ """
28
+ Initializes the figure.
29
+
30
+ Parameters
31
+ ----------
32
+ cases_multi_results : CasesMultiCPTBearingResults
33
+ The results of the bearing capacity calculations.
34
+
35
+ Raises
36
+ ------
37
+ TypeError
38
+ If `cases_multi_results` are not of type 'CasesMultiCPTBearingResults'.
39
+ """
40
+ # Validate the data
41
+ self._set_results(cases_multi_results)
42
+
43
+ # Initialize the figure
44
+ self._figure = go.FigureWidget(
45
+ layout=go.Layout(
46
+ height=800,
47
+ width=800,
48
+ title="CPT Results vs. Pile tip level<br>Case: , Result: ",
49
+ legend_title="CPT",
50
+ colorway=px.colors.qualitative.Plotly,
51
+ xaxis_title="",
52
+ yaxis=go.layout.YAxis(
53
+ title="Pile tip level [m NAP]",
54
+ title_font_size=18,
55
+ ),
56
+ autosize=False,
57
+ )
58
+ )
59
+
60
+ def _set_results(self, value: CasesMultiCPTBearingResults) -> None:
61
+ """Private setter for the results."""
62
+ if not isinstance(value, CasesMultiCPTBearingResults):
63
+ raise TypeError(
64
+ f"Expected type 'CasesMultiCPTBearingResults' for 'cases_multi_results', but got {type(value)}"
65
+ )
66
+ self._results = value
67
+
68
+ @property
69
+ def results(self) -> CasesMultiCPTBearingResults:
70
+ """The results of the bearing capacity calculations."""
71
+ return self._results
72
+
73
+ @property
74
+ def data(self) -> pd.DataFrame:
75
+ """The dataframe used to plot the results."""
76
+ return self.results.cpt_results_dataframe
77
+
78
+ @property
79
+ def cases(self) -> List[Hashable]:
80
+ """The case names of each MultiCPTBearingResults."""
81
+ return self.results.cases
82
+
83
+ @property
84
+ def test_ids(self) -> List[str]:
85
+ """The test_ids (cpt names) of all the MultiCPTBearingResults."""
86
+ return self.results.test_ids
87
+
88
+ @property
89
+ def figure(self) -> go.FigureWidget:
90
+ """The figure widget."""
91
+ return self._figure
92
+
93
+ def get_visible_test_ids(self) -> List[go.Scatter]:
94
+ """Returns the visible `test_id` (s) in the figure widget."""
95
+ return [trace.name for trace in self.figure.data if trace.visible is True]
96
+
97
+ def show_case_and_result(self, case_name: Hashable, result_name: str) -> None:
98
+ """Shows the results for all CPTs and pile tip levels for the requested `case_name` and `result_name`.
99
+
100
+ Parameters
101
+ ----------
102
+ case_name : str
103
+ The name of the case to show.
104
+ result_name : str
105
+ The name of the result to show.
106
+
107
+ Raises
108
+ ------
109
+ ValueError
110
+ If the `case_name` is not found in the cases.
111
+ If the `result_name` is not found in the CPTResultDefinitions.
112
+ """
113
+ # Check that case name is in cases.
114
+ if case_name not in self.cases:
115
+ raise ValueError(f"Case name '{case_name}' not found in cases.")
116
+
117
+ # Get the result definition that corresponds to the result name.
118
+ result_definition = CPTResultDefinitions.get(result_name)
119
+
120
+ # Get the visible test_ids
121
+ if len(self.figure.data) == 0:
122
+ # Select all test_ids if there are no traces in the figure yet.
123
+ visible_test_ids = self.test_ids
124
+ else:
125
+ visible_test_ids = self.get_visible_test_ids()
126
+
127
+ # Select data for case name and result name.
128
+ mask_case_name = (
129
+ self.data["case_name"] == case_name
130
+ if case_name is not None
131
+ else self.data["case_name"].isna()
132
+ )
133
+ selected_data = self.data.loc[
134
+ (mask_case_name) & (self.data["result_name"] == result_definition.name)
135
+ ]
136
+ traces = []
137
+ for test_id in self.test_ids:
138
+ df = selected_data.loc[selected_data["test_id"] == test_id]
139
+ traces.append(
140
+ go.Scatter(
141
+ x=df["result"],
142
+ y=df["pile_tip_level_nap"],
143
+ mode="lines+markers",
144
+ name=test_id,
145
+ )
146
+ )
147
+
148
+ with self.figure.batch_update():
149
+ # Empty traces
150
+ self.figure.data = []
151
+
152
+ # Apply changes
153
+ self.figure.add_traces(traces)
154
+
155
+ self.figure.update_layout(
156
+ title=f"CPT Results vs. Pile tip level<br>Case: {case_name}, Result: {result_definition.value.html}",
157
+ xaxis=go.layout.XAxis(
158
+ title=f"{result_definition.value.html} [{result_definition.value.unit}]",
159
+ title_font_size=18,
160
+ ),
161
+ showlegend=True,
162
+ )
163
+
164
+ self.figure.update_traces(
165
+ selector=lambda x: x.name in visible_test_ids,
166
+ patch=dict(
167
+ visible=True,
168
+ ),
169
+ )
170
+
171
+ self.figure.update_traces(
172
+ selector=lambda x: x.name not in visible_test_ids,
173
+ patch=dict(
174
+ visible="legendonly",
175
+ ),
176
+ )
@@ -0,0 +1,47 @@
1
+ from typing import Any
2
+
3
+ import plotly
4
+
5
+
6
+ def get_continuous_color(colorscale: Any, intermed: float) -> Any:
7
+ """
8
+ Plotly continuous colorscales assign colors to the range [0, 1]. This function computes the intermediate
9
+ color for any value in that range.
10
+
11
+ Plotly doesn't make the colorscales directly accessible in a common format.
12
+ Some are ready to use:
13
+
14
+ colorscale = plotly.colors.PLOTLY_SCALES["Greens"]
15
+
16
+ Others are just swatches that need to be constructed into a colorscale:
17
+
18
+ viridis_colors, scale = plotly.colors.convert_colors_to_same_type(plotly.colors.sequential.Viridis)
19
+ colorscale = plotly.colors.make_colorscale(viridis_colors, scale=scale)
20
+
21
+ :param colorscale: A plotly continuous colorscale defined with RGB string colors.
22
+ :param intermed: value in the range [0, 1]
23
+ :return: color in rgb string format
24
+ :rtype: str
25
+ """
26
+ if len(colorscale) < 1:
27
+ raise ValueError("colorscale must have at least one color")
28
+
29
+ if intermed <= 0 or len(colorscale) == 1:
30
+ return colorscale[0][1]
31
+ if intermed >= 1:
32
+ return colorscale[-1][1]
33
+
34
+ for cutoff, color in colorscale:
35
+ if intermed > cutoff:
36
+ low_cutoff, low_color = cutoff, color
37
+ else:
38
+ high_cutoff, high_color = cutoff, color
39
+ break
40
+
41
+ # noinspection PyUnboundLocalVariable
42
+ return plotly.colors.find_intermediate_color(
43
+ lowcolor=low_color,
44
+ highcolor=high_color,
45
+ intermed=((intermed - low_cutoff) / (high_cutoff - low_cutoff)),
46
+ colortype="rgb",
47
+ )